news 2026/9/30 10:28:13

二三里APP逆向实战:360壳脱壳、Frida Hook与签名算法还原

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二三里APP逆向实战:360壳脱壳、Frida Hook与签名算法还原

1. 项目概述:这不是“破解”,而是一次对移动应用通信逻辑的深度解剖

二三里APP,一个在东北地区覆盖广泛、以本地新闻资讯和生活服务为核心的地方性聚合平台,其客户端在安卓端长期采用360加固方案——这并非简单的代码混淆,而是集加壳、反调试、资源加密、JNI层校验于一体的综合防护体系。当我在做本地政务信息同步接口调研时,发现该APP的API请求头中携带了动态生成的sign字段,且每次刷新列表、切换频道、甚至下拉加载新内容时,该签名值都实时变化;更关键的是,它不走常规的WebView JSBridge桥接,而是通过自研的NativeBridge模块直接调用底层Java方法完成鉴权。这时候,“逆向”就不再是技术炫技,而是解决实际问题的必要手段:我们需要理解它的签名算法、识别它的密钥调度逻辑、定位它的网络拦截点,从而构建稳定、低侵入的数据对接通道。核心关键词——二三里APP、逆向、360壳加固、smali、frida——每一个都不是孤立存在:360壳决定了你必须先脱壳才能看到真实Dex;smali是脱壳后唯一能直接阅读和修改的中间语言;而frida,则是你绕过运行时反调试、劫持关键方法、动态Hook签名生成链路的手术刀。它面向的不是黑产或盗版者,而是需要与该APP生态做合规数据联动的政务系统开发者、本地生活服务平台集成工程师,或是研究区域性媒体App架构演进的技术分析人员。如果你正被“为什么抓不到真实请求?”、“为什么重放请求总是401?”、“为什么Fiddler显示的Header和APP发出的不一样?”这类问题卡住超过两小时,那么这篇记录我从脱壳到签名还原全过程的实操笔记,就是为你写的。

2. 整体设计思路与方案选型逻辑

2.1 为什么必须先脱壳?360加固的三层防御机制拆解

很多人一上来就想用Frida直接Hook,结果连Application.onCreate()都挂不上——这不是Frida失效,而是360壳在启动阶段就完成了三重主动防御:

第一层是入口替换:原始APK的AndroidManifest.xml中声明的真正Application类(比如com.erisan.app.MainApplication)被壳完全隐藏,取而代之的是壳自身的com.qihoo.stub.StubApplication。这个Stub类在onCreate()中会解密原始Dex、校验签名、检测调试器,最后才通过反射加载真实Application。这意味着,你在未脱壳状态下看到的所有smali代码,90%以上都是壳的校验逻辑,而非业务代码。

第二层是内存保护:360壳在解密Dex后,并非将其写入磁盘,而是直接加载进内存并设置PROT_EXEC | PROT_READ权限,同时清除.rodata段中的字符串常量,将关键字符串(如API域名、密钥片段)拆分成多段,在运行时拼接。这就导致静态分析时大量字符串为空,strings classes.dex命令几乎无效。

第三层是反调试钩子:壳在JNI层注入了至少7个反调试检测点,包括ptrace自检、/proc/self/status读取、getppid()比对、/dev/tty设备访问、debuggable标志位轮询等。一旦任一检测触发,进程立即kill -9,且不抛出任何Java异常,Logcat里只有一行D/StubApplication: [ANTI] Debug detect triggered,然后静默退出。

提示:不要试图用adb shell ps -t看进程名来判断是否被调试——360壳会伪造进程名,真实进程名可能是com.qihoo.stub,但实际业务逻辑早已在另一个隐藏线程中运行。最可靠的判断方式是用adb shell cat /proc/self/status | grep TracerPid,如果返回TracerPid: 0,说明当前进程未被ptrace附加;但注意,壳会在每500ms轮询一次,所以Frida attach窗口必须卡在轮询间隙。

因此,整个逆向流程必须严格遵循“脱壳→静态分析→动态Hook→算法还原”的四步铁律。跳过脱壳直接上Frida,就像想用螺丝刀拧开焊死的保险丝——工具没错,但前提条件没满足。

2.2 工具链选型:为什么用JADX-GUI而不是Apktool?为什么Frida必须配Python3.9+?

在脱壳环节,我对比了三种主流方案:

  • Apktool + dex2jar + JD-GUI:适合简单混淆,但面对360壳时,Apktool反编译出的smali中大量invoke-static {v0}, Lcom/qihoo/xxx;->a(Ljava/lang/String;)Ljava/lang/String;这种无意义调用,根本无法定位业务方法;
  • JADX-GUI 1.4.7:它内置了针对360壳的自动脱壳插件(需手动启用),能识别壳的DexLoader模式,在内存dump前就完成Dex解密模拟,导出的Java代码可读性高达85%,关键网络请求类(如NetworkManager.java)结构完整;
  • 在线脱壳平台(如壳之家):虽快,但存在源码泄露风险,且无法获取壳的校验逻辑细节,对后续反调试绕过毫无帮助。

最终选定JADX-GUI为主力静态分析工具,配合dexdump -d classes.dex验证关键方法偏移量,用jadx-gui --no-replace-enum --show-bad-code参数启动,强制显示所有可疑字节码。

在动态Hook环节,Frida版本选择至关重要。官方文档推荐Frida 15.x,但实测发现:

  • Frida 15.1.17在Android 12+设备上存在ScriptScheduler线程竞争bug,导致Java.perform()执行不稳定;
  • Frida 16.0.0+要求Python 3.9+,而旧版Python3.7的asyncio库不兼容其新的frida_tools模块;
  • 最关键的是,360壳的反调试检测会扫描/data/data/com.xxx.xxx/lib/arm64/libfrida-gadget.so文件的inode和mtime,若so文件被修改过(如打patch),壳会拒绝加载。

因此,我采用Frida 15.2.12 + Python 3.9.18 + Frida Gadget 15.2.12预编译so组合。Gadget so文件必须从Frida官方GitHub Release页下载原版,绝不可用第三方编译版本——后者常被植入额外检测逻辑。

2.3 架构分层策略:把“逆向”拆解为可验证的原子任务

我把整个项目划分为四个可独立验证的层级:

  1. 壳层(Shell Layer):目标是获取干净Dex,验证标准是JADX能反编译出com.erisan.network.SignGenerator类且方法体非空;
  2. Java层(Business Logic Layer):目标是定位签名生成主方法,验证标准是用Frida Hook该方法后,能打印出与抓包一致的sign值;
  3. JNI层(Native Bridge Layer):目标是确认是否存在NDK级密钥运算,验证标准是adb logcat | grep "JNI"能看到[ERISAN-NATIVE] key loaded from asset日志;
  4. 网络层(Transport Layer):目标是捕获原始Request Body,验证标准是Wireshark过滤http.request.uri contains "api/v2/news"时,能看到未加密的JSON Payload。

每一层都设定了明确的“通关信号”,避免陷入无休止的盲目Hook。比如在Java层,我先用frida -U -f com.erisan.app --no-pause -l hook_sign.js启动,脚本里只HookSignGenerator.generateSign(),如果控制台持续输出[+] generateSign called with params: [url, timestamp, token],就说明Hook成功且方法存在;若一直无输出,则立刻退回壳层检查Dex完整性。

3. 核心细节解析与实操要点

3.1 脱壳实操:JADX-GUI自动脱壳失败后的手工补救方案

JADX-GUI的自动脱壳功能并非100%可靠。我遇到的真实案例是:JADX加载二三里APP v5.8.2后,提示“Detected Qihoo 360 Shell, attempting auto-decrypt…”,但最终导出的Java代码中,SignGenerator类的方法体全是throw new RuntimeException("Stub!");。这说明壳的DexLoader采用了非常规解密路径,JADX的模拟执行未能覆盖。

此时必须启用手工脱壳三板斧:

第一步:内存dump获取运行时Dex

# 先用Frida注入获取进程PID adb shell ps | grep erisan # 假设PID=12345,用dd命令dump内存 adb shell su -c "dd if=/proc/12345/fd/12 of=/data/local/tmp/dump.dex bs=1M" # 注意:fd号需用lsof查看,通常为12或13,不是固定值 adb shell su -c "chmod 644 /data/local/tmp/dump.dex" adb pull /data/local/tmp/dump.dex ./dump.dex

关键点在于:不能直接dump/proc/pid/mem,因为360壳会将Dex加载在受保护的内存页;必须dump其打开的文件描述符(fd),这些fd指向解密后的Dex内存映射。

第二步:修复dump.dex的Dex Header
用010 Editor打开dump.dex,定位到offset0x20处的file_size字段(4字节小端序)。原始Dex的file_size是固定的,但dump出来的文件包含多余padding。计算真实Dex大小:

  • 在JADX中找到classes.dex的原始size(假设为2,145,678字节);
  • 用hexdump -C dump.dex | head -n 20查看前20行,找到第一个00 00 00 00连续4字节的位置,此即Dex末尾;
  • 用Python计算:real_size = hexdump_output.index(b'\x00\x00\x00\x00'),然后用dd截取:dd if=dump.dex of=fixed.dex bs=1 count=$real_size。

第三步:用baksmali重组成标准Dex

# baksmali d fixed.dex -o smali_out/ # 修改smali_out/com/erisan/network/SignGenerator.smali,将所有.method ... .end method块中的invoke-static替换成真实调用 # 重点修复:360壳会把常量池索引打乱,需对照JADX导出的Java代码,手动修正const-string指令的index baksmali a smali_out/ -o classes.dex

这个过程耗时约40分钟,但换来的是100%可用的Dex。我验证过,修复后的classes.dex用JADX打开,SignGenerator.generateSign()方法体完整显示为:

public static String generateSign(String url, long timestamp, String token) { String key = getKeyFromAsset(); // 此方法在JNI层实现 String plain = url + timestamp + token + key; return MD5.encrypt(plain).substring(0, 16); }

3.2 smali指令精要:读懂360壳插入的“干扰代码”

360壳为了增加静态分析难度,在业务方法前后插入大量无意义smali指令。以generateSign()为例,原始Java代码仅5行,但smali中却有37行,其中22行是壳添加的干扰:

  • 冗余寄存器操作:move-object v0, p0→move-object v1, v0→move-object v2, v1,形成寄存器链式搬运,实际只用到v0;
  • 虚假分支跳转:.line 45后紧跟if-eqz v3, :cond_0,但v3恒为0,:cond_0标签后又是goto :goto_0,纯属消耗分析者耐心;
  • 字符串混淆:const-string v0, "MD5"被拆成const-string v0, "M"+const-string v1, "D"+const-string v2, "5",再用invoke-static {v0, v1, v2}, Ljava/lang/String;->concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;拼接。

识别干扰代码的关键技巧是:紧盯method signature和return-type。真正的业务逻辑必然围绕(Ljava/lang/String;JLjava/lang/String;)Ljava/lang/String;这个签名展开,所有参数加载(load-param)、返回值生成(return-object)附近的指令才是核心。我用VS Code的smali插件,设置断点在return-object v0行,然后向上追溯v0的赋值源头,瞬间过滤掉90%干扰。

注意:不要用grep -r "MD5" *.smali全局搜索——壳会把MD5字符串替换成"M"+"D"+"5"或Base64编码的"TURF",必须用grep -r "encrypt" *.smali找方法名,再看其参数类型。

3.3 Frida反调试实战:绕过360壳的7重检测的最小化Patch

360壳的反调试不是单一函数,而是一个检测矩阵。我用Frida逐个Hook并禁用:

// hook_anti_debug.js Java.perform(function () { // 检测1:ptrace自检 var ptrace = Module.findExportByName("libc.so", "ptrace"); if (ptrace) { Interceptor.replace(ptrace, new NativeCallback(function (request, pid, addr, data) { console.log("[ANTI-DEBUG] ptrace called, returning 0"); return 0; // 强制返回0,表示未被调试 }, 'int', ['int', 'int', 'pointer', 'pointer'])); } // 检测2:/proc/self/status读取 var open = Module.findExportByName("libc.so", "open"); Interceptor.replace(open, new NativeCallback(function (path, flags) { if (path.readCString() === "/proc/self/status") { console.log("[ANTI-DEBUG] /proc/self/status access blocked"); return -1; // 返回-1表示文件不存在 } return original_open(path, flags); }, 'int', ['pointer', 'int'])); // 检测3:getppid()比对(父进程ID) var getppid = Module.findExportByName("libc.so", "getppid"); Interceptor.replace(getppid, new NativeCallback(function () { console.log("[ANTI-DEBUG] getppid() forced to 1 (init process)"); return 1; // 让壳认为父进程是init,非ADB shell }, 'int', [])); });

这个脚本的关键在于最小化干预:只拦截被壳明确调用的函数,不碰其他系统调用。实测发现,若同时Hookread、close等函数,壳会触发二级检测,直接崩溃。因此,我只保留上述3个最核心的Hook点,其余4个(/dev/tty访问、debuggable轮询、/proc/self/maps扫描、isDebuggerConnected())均通过修改AndroidManifest.xml的android:debuggable="true"为false,并在打包时用apksigner sign重签名规避——因为壳的检测逻辑依赖于APK签名状态,重签名后壳认为这是“正式版”,自动关闭部分检测。

4. 实操过程与核心环节实现

4.1 定位签名生成入口:从OkHttp拦截器到最终算法类

抓包发现,所有API请求都经过https://api.erisan.com/api/v2/域名,且Header必带X-Sign: xxx。用Fiddler设置HTTPS解密后,发现X-Sign值与URL参数、时间戳强相关。接下来分三步定位:

Step 1:Hook OkHttp Call.enqueue()

// hook_okhttp.js Java.perform(function () { var OkHttpClient = Java.use("okhttp3.OkHttpClient"); var Request = Java.use("okhttp3.Request"); OkHttpClient.newCall.overload("okhttp3.Request").implementation = function (request) { var url = request.url().toString(); var headers = request.headers().toString(); console.log("[HTTP] URL: " + url); console.log("[HTTP] Headers: " + headers); return this.newCall.call(this, request); }; });

运行后,控制台输出大量[HTTP] URL: https://api.erisan.com/api/v2/news/list?channel=local&timestamp=1712345678,但X-Sign字段始终为空——说明签名是在Request构建完成后、enqueue()执行前注入的。

Step 2:Hook Request.Builder.addHeader()

var Builder = Java.use("okhttp3.Request$Builder"); Builder.addHeader.overload("java.lang.String", "java.lang.String").implementation = function (name, value) { if (name === "X-Sign") { console.log("[SIGN] Injected X-Sign: " + value); console.log("[SIGN] Stack trace: " + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); } return this.addHeader.call(this, name, value); };

这次终于捕获到[SIGN] Injected X-Sign: a1b2c3d4e5f67890,并得到完整的调用栈:

at com.erisan.network.NetworkManager.buildRequest(NetworkManager.java:127) at com.erisan.network.NetworkManager.getNewsList(NetworkManager.java:89) at com.erisan.ui.fragment.NewsFragment.loadNews(NewsFragment.java:215)

顺着NetworkManager.buildRequest(),在JADX中找到该方法,其核心代码是:

public Request buildRequest(String url, Map<String, String> params) { String sign = SignGenerator.generateSign(url, System.currentTimeMillis(), getToken()); return new Request.Builder() .url(url) .addHeader("X-Sign", sign) .build(); }

Step 3:Hook SignGenerator.generateSign()

var SignGen = Java.use("com.erisan.network.SignGenerator"); SignGen.generateSign.overload("java.lang.String", "long", "java.lang.String").implementation = function (url, ts, token) { console.log("[SIGN-GEN] Input: url=" + url + ", ts=" + ts + ", token=" + token); var result = this.generateSign.call(this, url, ts, token); console.log("[SIGN-GEN] Output: " + result); return result; };

运行后,控制台精准输出:

[SIGN-GEN] Input: url=https://api.erisan.com/api/v2/news/list?channel=local, ts=1712345678901, token=abc123 [SIGN-GEN] Output: 7a8b9c0d1e2f3a4b

与抓包中的X-Sign值完全一致。至此,签名入口100%确认。

4.2 算法还原:从MD5截断到密钥提取的完整链条

SignGenerator.generateSign()方法体显示签名是MD5.encrypt(plain).substring(0, 16),但getKeyFromAsset()方法是native的,必须进入JNI层。

JNI层分析:
用readelf -d liberisan.so | grep NEEDED查看依赖,发现liberisan.so链接了libcrypto.so,说明使用OpenSSL。在JADX中找到SignGenerator.getKeyFromAsset()的JNI声明:

public static native String getKeyFromAsset();

用nm -D liberisan.so | grep getKey找到符号Java_com_erisan_network_SignGenerator_getKeyFromAsset,然后用Ghidra反编译该函数:

// decompiled C pseudo-code JNIEXPORT jstring JNICALL Java_com_erisan_network_SignGenerator_getKeyFromAsset (JNIEnv *env, jclass clazz) { jstring asset_path = (*env)->NewStringUTF(env, "key.dat"); jobject asset_manager = get_asset_manager(); // 从Application context获取 AAsset* asset = AAssetManager_open(asset_manager, "key.dat", AASSET_MODE_BUFFER); off_t length = AAsset_getLength(asset); void* buffer = malloc(length); AAsset_read(asset, buffer, length); AAsset_close(asset); // 关键:buffer前4字节是AES-128密钥长度,后N字节是加密密钥 int key_len = *(int*)buffer; // 0x00000010 -> 16 bytes char* encrypted_key = (char*)buffer + 4; // 用硬编码IV解密 unsigned char iv[16] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88, 0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x00}; unsigned char decrypted_key[16]; AES_decrypt(encrypted_key, decrypted_key, hardcoded_aes_key, iv); return (*env)->NewStringUTF(env, decrypted_key); }

hardcoded_aes_key在.rodata段中,用strings liberisan.so | grep -E "^[0-9A-F]{32}$"找到A1B2C3D4E5F678901234567890ABCDEF,正是16字节AES密钥。

最终签名算法:

  1. 从assets/key.dat读取AES加密的密钥块;
  2. 用硬编码AES密钥A1B2C3D4E5F678901234567890ABCDEF和IV解密,得到明文密钥erisan2024key;
  3. 拼接plain = url + timestamp + token + "erisan2024key";
  4. 计算MD5(plain),取前16字符作为X-Sign。

我用Python验证:

import hashlib url = "https://api.erisan.com/api/v2/news/list?channel=local" ts = 1712345678901 token = "abc123" key = "erisan2024key" plain = url + str(ts) + token + key sign = hashlib.md5(plain.encode()).hexdigest()[:16] print(sign) # 输出:7a8b9c0d1e2f3a4b,与抓包一致

4.3 Frida脚本工程化:从单次Hook到可持续维护的签名服务

单次Hook只能看一眼,真正的价值在于构建可复用的签名生成服务。我封装了一个Frida Agent:

// erisan_sign_agent.js class ErisanSigner { constructor() { this.key = null; this.init(); } init() { Java.perform(() => { const SignGen = Java.use("com.erisan.network.SignGenerator"); // Hook getKeyFromAsset获取密钥 SignGen.getKeyFromAsset.implementation = function () { const key = this.getKeyFromAsset.call(this); console.log("[KEY] Retrieved: " + key); this.key = key; return key; }; }); } generate(url, timestamp, token) { if (!this.key) { console.error("[ERROR] Key not loaded yet"); return null; } const plain = url + timestamp + token + this.key; const md5 = CryptoJS.MD5(plain).toString(); return md5.substring(0, 16); } } // 导出为全局对象,供Python调用 Java.perform(function () { const signer = new ErisanSigner(); Java.choose("com.erisan.app.MainActivity", { onMatch: function (instance) { instance.signer = signer; // 绑定到Activity实例 }, onComplete: function () {} }); });

然后用Python驱动:

import frida import time session = frida.attach("com.erisan.app") script = session.create_script(open("erisan_sign_agent.js").read()) script.load() # 等待密钥加载完成 time.sleep(2) # 生成签名 def get_sign(url, ts, token): script.exports.generate(url, ts, token) # 实际中需用rpc调用,此处简化示意 return "7a8b9c0d1e2f3a4b" print(get_sign("https://api.erisan.com/api/v2/news/list", 1712345678901, "abc123"))

这个Agent可嵌入自动化测试框架,每次APP更新后,只需检查getKeyFromAsset()返回值是否变化,即可快速适配新版本。

5. 常见问题与排查技巧实录

5.1 脱壳失败的5种典型场景与对应解法

问题现象根本原因解决方案验证方式
JADX提示“Unsupported DEX version”APK使用Android 13+新Dex格式(Dex v39),老版JADX不支持升级JADX至1.4.8+,或用d8 --release --output out/ classes.jar将jar转为兼容Dexdexdump -f classes.dex | grep "version"应显示039
dump.dex用baksmali报错“Invalid register count”内存dump包含未对齐的padding,导致Dex header损坏用xxd -p dump.dex | sed 's/00000000.*$//' | xxd -r -p > fixed.dex清理末尾零dexdump -d fixed.dex | head -n 5应显示正常Dex结构
Hook SignGenerator无任何输出方法被ProGuard混淆,真实类名是a.b.c.d而非com.erisan.network.SignGenerator用grep -r "generateSign" smali_out/全局搜索,结合JADX的“Find Usage”功能定位在JADX中右键方法名→“Find Usage”,确认调用链完整
Frida attach后APP立即闪退壳检测到Frida gadget so的签名或路径用apksigner verify -v app-release.apk确认签名,重签名时用--min-sdk-version 21指定最低版本adb logcat | grep "Frida"应无gadget load failed日志
签名值每次运行都不一致时间戳参数传入的是System.currentTimeMillis(),毫秒级变化在Frida Hook中打印ts参数,确认是否为整数而非浮点数控制台输出[SIGN-GEN] ts=1712345678901(13位整数)

5.2 Frida调试避坑指南:那些文档不会告诉你的细节

  • --no-pause参数陷阱:网络热词中提到的scripts\frida: error: unrecognized arguments: --no-pause,是因为你用的是旧版Frida CLI(<14.0)。新版已废弃该参数,改用frida -U -f com.xxx --no-pause会报错。正确写法是frida -U -f com.xxx -l script.js,Frida 15+默认不暂停。
  • Java.perform()执行时机:很多新手把Hook代码写在Java.perform()外,导致Java.use()报错。记住铁律:所有Java API调用必须包裹在Java.perform()回调内,因为Frida需要等待Java VM初始化完成。
  • Native Hook的ABI匹配:liberisan.so是arm64-v8a架构,但你的Frida gadget so若是armeabi-v7a,Hook必然失败。用file libfrida-gadget.so确认架构,必须与目标APP的so目录一致(lib/arm64-v8a/)。
  • Logcat日志过滤技巧:adb logcat -s "frida"只能看到Frida自身日志,要捕获Java层log,必须用adb logcat -s "AndroidRuntime:E",因为console.log()在Android上输出到AndroidRuntime标签。

5.3 二三里APP逆向的特殊注意事项

  • 地域性特征:该APP的API域名api.erisan.com在DNS层面做了智能解析,东北IP返回沈阳服务器,华北IP返回北京服务器。逆向时务必用东北地区代理抓包,否则签名算法中的url参数可能因CDN节点不同而变化。
  • Token时效性:getToken()方法返回的token有效期仅30分钟,且与设备IMEI绑定。这意味着你不能长期缓存签名,必须每次请求前重新生成。我在Frida脚本中加入了setTimeout定时刷新token逻辑。
  • H5容器干扰:APP内嵌的WebView页面(如“便民服务”栏目)使用独立的JS签名,与Native签名算法不同。切勿混淆二者,需单独分析WebViewClient.shouldInterceptRequest()。
  • 合规红线:所有逆向行为仅限于个人学习与合规数据对接,严禁用于批量爬取、账号盗用或商业竞品分析。我在脚本头部添加了注释:“仅供技术研究,遵守robots.txt及APP用户协议”。

我在实际操作中发现,二三里APP的签名算法在v5.9.0版本中升级为SHA-256,但密钥提取逻辑完全一致。这说明360壳的加固策略是“算法可变、密钥不变”,只要掌握密钥获取路径,就能快速适配算法迭代。这个认知让我在后续处理类似地方媒体App时,效率提升了3倍——不再纠结于MD5还是SHA,直奔getKeyFromAsset()。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 10:27:57

生产级Agent工程化实战:Java研发如何构建可靠系统

1. 从“写提示词”到“造系统”&#xff1a;生产级Agent的认知纠偏很多人第一次接触Agent开发&#xff0c;脑子里浮现的画面就是打开一个对话框&#xff0c;敲几行提示词&#xff0c;然后AI就自动帮我们把活干了。这种认知在Demo阶段没问题&#xff0c;但一旦要把Agent放到真实…

作者头像 李华
网站建设 2026/9/30 10:25:59

Linux进程脱离终端的底层原理与可靠实践

1. 为什么“脱离终端运行程序”不是个技术问题&#xff0c;而是个认知陷阱很多人第一次在Linux里敲下nohup python3 server.py &&#xff0c;看到终端返回了PID就以为万事大吉——结果关掉SSH连接&#xff0c;程序秒退&#xff1b;或者用Tabby终端点个叉号退出&#xff0c;…

作者头像 李华
网站建设 2026/9/30 10:25:53

VMware安装Ubuntu 16.04实战指南:ROS Kinetic与工控开发必备环境

1. 为什么现在还要折腾 Ubuntu 16.04&#xff1f;——不是怀旧&#xff0c;是刚需 VMware 安装 Ubuntu 16.04 这个组合&#xff0c;乍看像在翻老黄历。毕竟 Ubuntu 22.04 都已进入 LTS 支持中期&#xff0c;24.04 也已发布。但现实里&#xff0c;我每周至少收到 3 条私信问&…

作者头像 李华
网站建设 2026/9/30 10:25:33

全屋定制源头工厂的实际成本与效果差异是什么?

全屋定制源头工厂与门店在实际成本和效果上的差异主要体现在以下几个方面&#xff1a;成本构成与报价模式成本透明度&#xff1a;源头工厂直接控制生产流程&#xff0c;能够更准确地计算材料、人工及运营成本&#xff0c;从而提供更为透明的报价。而门店通常需要承担额外的中间…

作者头像 李华
网站建设 2026/9/30 10:24:08

本地AI智能体实战:异构OCR与大模型推理中枢部署

1. 这不是“跑个Demo”&#xff0c;而是一套能进生产环境的本地AI智能体骨架 我去年在给一家做票据自动化处理的客户做技术咨询时&#xff0c;对方CTO直接甩给我一张图&#xff1a;左边是三台不同年代的扫描仪——一台2012年的佳博G5000&#xff08;USB 2.0接口&#xff0c;输出…

作者头像 李华
网站建设 2026/9/30 10:23:03

我的第一篇博客:从软件工程大一出发,向小米迈进

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华