Files
homeagent-sdk/docs/versions.md
JianFeeeee 81a3efa4df feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode) (#3)
* feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode)

## 是什么

agent 统筹开发流程的插件:登记项目 → 开工程线(每条线一个隔离工作目录 +
一个 sidecar 子进程 + 一对通道)→ 投递任务 → 同会话续投 → 评审/合入。

38 个工具:project_*/branch_*/workdir_*/job_*/review_*/test_*/
endpoint_*/agent_*/pi_*/conflict_check。

## sidecar 后端抽象(本轮核心)

传输层(local/ssh)与后端(pi/zcode/自定义)**正交**,四个自由组合:

  driver.json 描述一个后端:script / cliPath / env / args / llmConfig
  三级查找:插件数据目录 → **随包 rawfile** → 内置 pi 描述
  占位符:{workdir} {dataDir} {pluginDir} {rawfile} {sessionId}

★ 内置 pi 回退**与默认后端解耦**(2026-10-17 修):原实现写
`if cli == defaultAgentCLI { return builtinPiDriver() }`,把"给谁兜底"
和"默认选谁"耦合到同一常量 —— 一旦有人改默认,pi 当场失去兜底,
显式 cli="pi" 开始报「驱动不存在」。现在判断字面 "pi"。

默认后端保持 **pi**(我们本地主用);交付方用 zcode 走两种局部开关:
 · 单线:commondev_branch_open(cli="zcode")
 · 按机器:commondev_endpoint_set(host=..., cli="zcode")

## zcode 接入(三条真机判据,全部要求真改工程文件)

 · TestZcodeFullPipeline(29-34s):随包 driver + 真实 ZCode CLI
 · TestZcodeOverSSH(39s):真 ssh + scp,远端 node 跑 zcode
 · TestZcodeOverSSHWithPerHostEndpoints(68s):两个 host 别名配不同
   cli_path,走端点表而不是全局 settings

★ 会话库必须隔离到工作区(sidecar.mjs 的 ensureWorkspaceSessionStore):
实测 ZCode 的会话库路径**硬编码 homedir()**,`ZCODE_DATA_BASE_DIR` 对它
完全无效(26 个会话全落全局库)。但 `<workdir>/.zcode/config.json` 的
`storage.sessionDbPath` **确实生效**。所以启动前写一份工作区配置,
一举解决:多线不共用历史 / 不污染全局 ~/.zcode / 删线不留孤儿会话。
只在不存在时写 —— 用户手工放的要尊重。

## 其他

 · rawfile:项目根 rawfile/ 默认全收,解到 <插件目录>/rawfile/,绝对路径拒绝
 · 凭证:命名引用 + ${ENV_VAR},独立 0600 文件,绝不进 config.db(0644)
 · secretshell:给不受信 agent 用的安全 shell(ackey + safeshell)
 · 冲突检查 / 评审判据 / 镜像 / 每条线独立 workdir(hash 命名,合并后删除)

判据 30+ 条,含 4 条「判据本身的判据」(防止判据假绿)。

* fix(sdk): 协议通道纪律 + hmapdev 模板升级 + 示例插件 stdout 污染

## 核心缺陷:插件往 stdout 打日志 = 污染 JSON-RPC 协议通道

子进程插件与内核用 stdout 传 NDJSON 帧,stderr 才是日志通道。
脚手架模板(及 4 个示例插件)用 `fmt.Printf` 打日志 ⇒ 内核收到非 JSON 文本:

    [proc] weather 收到非法 JSON 帧(21 字节): invalid character 'i'

**生产实测 1038 条**(huawei_smarthome 720 / weather 113 / rss 103 / calendar 102)。
后果不只是日志噪音:那一行日志被**丢弃**,插件看起来"没启动"。

修法:
- hmapdev 模板 `fmt.Printf` → `log.Printf`(9 处)
- 示例插件 4 个 14 处:ai_image(2) calendar(5) rss(4) weather(3)
  + 补 "log" import
- 内核侧(主仓)同步让报错可定位:内容摘要 + 可能原因 + 修法

判别法:`package main` = 子进程(协议通道,禁用 stdout 打日志);
普通库包 = 进程内插件(编译进内核,写真实终端,**不是**缺陷)——
`internal/plugins/cli` 的 `fmt.Println` 属后者。

## hmapdev

- `cmd_build.go` +136:平台产物精确匹配(`--target` 是**append** 语义,
  原脚本 `ls dist/*.hmap | head -1` 会按字典序抓到 amd64,
  **导致 arm64 包里装的是 x86-64 二进制**,且退出码 0、校验和通过)
- `cmd_init.go` +46:脚手架 stdio 纪律
- `templates/proc_main.go.tmpl`:补齐 mgrStop/mgrStart/mgrIsRuntimeStopped
  三态能力(插件管理器运行态停启)
- `templates/plg.json.tmpl` 删除(合并进 cmd_init)

## 判据

- `scaffold_stdout_test.go`(3 条):扫模板生成的代码不得写 stdout。
  ★ 判据自身先假红过一次:**模板的注释里就写了 `fmt.Printf`** ⇒
  必须 `stripTemplateComments()` 之后才判
- `example_stdout_test.go`(2 条):扫全部 20 个子进程示例插件,
  报错给**文件:行号**;变异咬住并给出准确行号
- `package_examples_test.go`(2 条):**执行真实打包脚本**
  (第一版测的是 hmapdev 而不是脚本 ⇒ 变异不咬)
- `cmd_build` / `cmd_init` 新判据

## 其他

- sdk/settings.go +15(三态能力透传)
- `docs/versions.md` +§示例插件协议通道纪律(含生产 1038 条实测表格)
- `skills/homeagent-plugin-dev/SKILL.md` +§协议通道纪律(正误代码对比)

* fix(examples): 恢复 go.mod 相对路径 + 移除 plg.json 的 sdk 固定(修 CI 红)

CI 上 `Examples (linux/amd64)` 与 `(darwin/arm64)` 双红,只有两个失败:

  ✗ commondev   构建失败
  ✗ editdoc     构建失败
  error: 项目声明 sdk=1.4.0,但本地 SDK 存储里没有这个版本

其余 20 个示例全部成功 —— 说明不是通用问题,而是这两个被改坏了。

## 根因(两个叠加)

**① go.mod 被本机绝对路径污染**

我在上一个提交里用 `git add -A`,把本机 `go build` **自动改写**过的
go.mod 一并提交了:

  require github.com/JianFeeeee/homeagentsdk v1.4.0
  replace github.com/JianFeeeee/homeagentsdk => /root/.homeagent/hmapdev/sdk/v1.4.0

基线(origin/main)是:

  require github.com/JianFeeeee/homeagentsdk v0.0.0
  replace github.com/JianFeeeee/homeagentsdk => ../../

本机那个目录真的存在 ⇒ 本地全绿;CI 没有 ⇒ 必红。

**② plg.json 声明了 `"sdk": "1.4.0"`**

一声明版本,hmapdev 就去**本地 SDK store** 找它,找不到直接拒绝构建。
全仓 20+ 示例**都不声明**,只有我改过的这两个声明了。
示例的正确依赖表达是 go.mod 的 `v0.0.0` + `replace ../../`。

## 修法

两个文件都恢复到与其余示例一致的写法。

## 判据(3 条,都是新加的)

 · `TestExampleGoModHasNoAbsoluteReplace`:任何示例 go.mod 不得含
   `/root/`、`/home/`、`/Users/`、`C:\`
 · `TestExampleGoModUsesRelativeSDKReplace`:replace 必须是相对路径 `../../`
 · `TestExamplePlgJsonDoesNotPinSDKVersion`:plg.json 不得声明 sdk

变异 2/2 咬住(重新引入绝对路径 / 重新声明 sdk,都准确报出文件路径)。

## 本地验证(模拟 CI 干净环境)

用干净 HOME 跑 `package/build-examples.sh`:
 · 修复前:成功 18 / 失败 4(commondev, editdoc, browser, rss)
 · 修复后:成功 20 / 失败 2(browser, rss —— **本机代理不通**,
   `proxy.golang.org` 超时;用本地 modcache 验证这两个可正常构建)

即 22/22 源码都能构建,CI 有网络不受影响。

## 教训

`git add -A` 会把「构建工具自动改写的文件」一起提交。
提交前必须 `git status` 逐个看,尤其 go.mod / go.sum。

---------

Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
2026-10-17 19:44:15 +08:00

286 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 版本与兼容
## SDK 版本语义
**SDK 版本跟随内核的中版本,patch 位恒为 `.0`。**
整条内核 `1.1.x` 线(1.1.0、1.1.1、1.1.7…)共用 **SDK 1.1.0**;
只有内核进入 `1.2.0` 这种中版本跃迁时,SDK 才升到 1.2.0。
这样插件作者只需关心「我在为哪个中版本写插件」,不必跟着内核的每个 bugfix 换依赖。
当前内核声明的兼容上限是 **SDK 1.3.0**。
## 版本历史
| SDK | 内核 | 变化 | 需要重编? |
|---|---|---|---|
| **1.3.0** | 1.4.0+ | 驻留子 agent、`RecallPolicy` 等 | 想用新 API 才需要 |
| **1.2.0** | 1.2.0 / 1.3.x | `InjectOptions{NoMemory, ContextPolicy}`、六个 `*Opts` 变体、`ChannelDef.ContextPolicy` | 不需要 |
| **1.1.0** | 1.1.x | 多模态贯通:`Triple.SentenceText`、`Doc.Attachments`、`MediaAttachment`、`InsertWithMedia`、媒体注入方法 | 不需要 |
| **1.0.0** | 1.0.0+ | **运行模型变更**:C ABI 动态库 → 子进程 + 共享内存 | **需要** |
### 1.0.0 是唯一一次破坏性变更
- `.so` / `.dylib` / `.dll` **不再被加载**,遇到旧产物会跳过并报可操作错误(不崩溃)。
- **业务代码不用改一行** —— 公开 SDK 接口零改动,用新版 `hmapdev` 重编即可。
- 产物从 `plugin.so` 变为 `plugin.bin`;不再需要 cgo。
### 1.1.0 / 1.2.0 是纯追加
两次都是**新增方法由插件调用、内核实现**,不调就不受影响。
零值 `InjectOptions` 与旧的三参数方法完全等价,因此存量插件**不需要改、也不需要重编**;
想用新字段的重编即可。
### SDK 1.4.0 → 本轮新增(PluginMgr 运行态启停)⚠️ 有一个例外
本轮给 `PluginMgrAPI` 新增 3 个方法(`StopPlugin` / `StartPlugin` /
`IsPluginRuntimeStopped`,见 `docs/zh/kernel-modularization-design.md` §2.6.7)。
按上面的规则,这属于"**插件调用、内核实现**"的纯追加 ⇒ 无需重编。**但有一个例外**:
| 对象 | 是否需要动 | 原因 |
|---|---|---|
| **已编译的存量插件**(`plugin.bin`) | ❌ 不需要 | 不调新方法就不受影响(与 1.1/1.2 一致) |
| **存量插件的源码要重编** | ⚠️ **需要同步 hmapdev** | `proc_main.go.tmpl` 里的 `procPluginMgr` 必须实现新方法,否则**编译不过** |
| **新插件** | ✅ 自动 | 模板已同步(`hmapdev` 已更新) |
!!! warning "为什么这次和 1.1/1.2 不一样"
1.1/1.2 只改了 `sdk/`(插件**调用**的一侧)与内核实现。
本轮还改了 **`tools/hmapdev/templates/proc_main.go.tmpl`** ——
那是**生成插件代码**的模板,属于"插件**被编译**的一侧"。
`plugin.bin` 里的 `procPluginMgr` 是**编译期**由模板生成的具体类型,
它必须满足 `PluginMgrAPI` 的**全部**方法(Go 接口是精确匹配)。
模板不同步 ⇒ 生成物不满足接口 ⇒ **编译失败**(不是运行期降级)。
⇒ 判据 `internal/plugin/proc/bridge_sync_test.go` 已有 3 条自动守这个
(SDK 方法集 ⇒ 能力表 / Handle 分派 / hmapdev 模板,三处必须同步)。
## 脚手架模板的协议通道纪律(`hmapdev init` 生成物)
子进程插件的 **stdout 是 JSON-RPC 协议通道**,**stderr 才是日志通道**
(SDK 侧 `log.SetOutput(os.Stderr)`)。这条纪律**必须写进脚手架**,
否则每个新插件作者都会踩。
### 曾踩的坑(2026-10-16 实测)
`hmapdev init` 生成的 `plugin.go` 原先用 `fmt.Printf` 打日志 ⇒ 写进 stdout
⇒ 内核 readLoop 收到非 JSON 文本:
```
[proc] final_probe 收到非法 JSON 帧(21 字节): invalid character 'i' in literal false
```
两个后果叠加、都很难定位:
1. 那句日志**丢了**(内核把它当坏帧丢弃);
2. 多了一条看起来像**协议故障**的报错 —— 而真因是"插件自己 printf"。
**A/B 实证**(同一内核,两个插件):
| 插件 | 脚手架 | 非法 JSON 帧 | 日志可见 |
|---|---|---|---|
| `final_probe` | 旧(`fmt.Printf`) | **1 次** | ❌ 丢失 |
| `fixed_probe` | 新(`log.Printf`) | **0 次** | ✅ `[fixed_probe] started` |
### 修法
模板改成 `log.Printf`,并把这条纪律写进生成物的**头部注释**
(让每个插件作者第一眼就看到)。
### 判据(`tools/hmapdev/scaffold_stdout_test.go`,3 条)
1. `TestScaffoldTemplateDoesNotWriteStdout` —— **代码行**里不得有
`fmt.Printf/Println/Print/os.Stdout`
2. `TestScaffoldTemplateUsesStderrLogging` —— 反向钉住:光禁 stdout 不够,
把日志全删也能过第一条 ⇒ 必须**真的**用 `log.Printf`(并 import "log")
3. `TestScaffoldLuaTemplateDoesNotWriteStdout` —— Lua 脚手架同理
**变异 2/2 咬住**:改回 `fmt.Printf` ⇒ 红;删光日志 ⇒ 红。
!!! warning "判据自身踩的坑(值得记住)"
第一版判据直接扫描模板字符串 ⇒ **被模板里的注释判红**(注释**故意**
提到 `fmt.Printf` 来讲这个坑)。必须**先剥注释**再扫代码行。
这是"源码扫描判据"的固有脆弱性:本仓规范要求把坑写进注释,
而注释里必然出现那些符号名 ⇒ **任何时候用字符串扫描判据,
都要先想"注释里会不会也有这个串"**(同类事故见
`docs/zh/kernel-modularization-design.md` §8.6,那里连续骗过三次)。
## 示例插件的协议通道纪律(已修)
上面的脚手架纪律同样适用于**示例插件** —— 它们是**子进程插件**
(`package main` + `plugin.bin`),stdout 即协议通道。
### 生产环境实测的缺陷(不是理论问题)
`journalctl -u homeagent | grep -c "非法 JSON 帧"` ⇒ **1038 条**,来源:
| 插件 | 次数 | 本仓? |
|---|---|---|
| `huawei_smarthome` | 720 | 第三方(内核侧报错就是为它准备的) |
| **`weather`** | 113 | ✅ 示例 |
| **`rss`** | 103 | ✅ 示例 |
| **`calendar`** | 102 | ✅ 示例 |
**根因**:这三个示例插件(以及 `ai_image`)用了 `fmt.Printf` 打日志 ⇒
裸文本进入 stdout 协议流 ⇒ 内核报非法帧并**丢弃该日志**。
### 修法(已完成)
4 个插件共 **14 处** `fmt.Printf` → `log.Printf`,并补 `"log"` import
(`calendar`/`rss`/`weather` 原先没 import log;`ai_image` 已有)。
★ 注意:`internal/plugins/cli` 里也有一处 `fmt.Println`,但它是
**进程内插件**(`plugin.RegisterFactory`,编译进内核),
写的是内核的真实终端 ⇒ **不是缺陷**。判据只针对"子进程插件"
(`package main`)。
### 判据(`tools/hmapdev/example_stdout_test.go`,2 条)
1. `TestExamplePluginsDoNotWriteStdout` —— 扫描全部**子进程**示例插件
(实测 20 个),剥注释后不得含 `fmt.Printf/Println/Print/os.Stdout`;
报错时给出**文件:行号**
2. `TestExamplePluginsUseStderrLogging` —— 反向钉住:至少 weather/rss/calendar
真的在用 `log.Printf`(否则"删光日志"也能过第一条)
### 内核侧:让非法帧报错**可定位**(这是对第三方插件的通用修复)
原报错只有 `收到非法 JSON 帧(N 字节): invalid character 'x'`:
不显示内容(不知是哪句话)、不提原因(看起来像内核故障)。
1038 条报错的定位花了一整轮排查 —— 就因为缺这两样。
现在报错含**三要素**:
```
[proc] bad_probe 收到非法 JSON 帧(19 字节): invalid character 'b' looking for beginning of value
↑ 内容摘要: [bad_probe] started
可能原因:插件往 **stdout** 打印了日志,而 stdout 是JSON-RPC **协议通道**(stderr 才是日志通道)。
修法:把插件里的 fmt.Printf/fmt.Println 改成 log.Printf(脚手架模板 1.4.0 起已修正)。
```
(`summarizeFrame` 做控制字符替换 + 截断,保证单行且不刷屏。)
判据在 `internal/plugin/proc/process_test.go`:
`TestSummarizeFrameIsReadable`、`TestIllegalFrameErrorIsActionable`。
**变异 2/2 咬住**(去掉 summarizeFrame 调用 ⇒ 红;报错不提原因 ⇒ 红)。
## 升级已装的 SDK store 与 hmapdev 二进制(操作步骤)
!!! danger "顺序不能反 —— 否则会装出「二进制新、store 旧」的组合"
**模板是 `//go:embed` 进 hmapdev 二进制的**(`tools/hmapdev/proc_runtime.go`),
而 **SDK 接口(`sdk/`)来自 store**。二者必须**同代**:
| 组合 | 结果 |
|---|---|
| 旧二进制 + 旧 store | 自洽(但拿不到新能力) |
| 新二进制 + 新 store | ✅ 自洽 |
| **新二进制 + 旧 store** | ⚠️ 模板的 `procPluginMgr` 有 6 个方法,旧接口只有 3 个 ⇒ 生成物**多出 3 个方法**(Go 允许,但新方法调用的 `plugin.mgrStop` 内核可能不认) |
| **旧二进制 + 新 store** | ❌ 模板 3 个方法 vs 接口 6 个 ⇒ 生成物**不满足接口** ⇒ **编译失败** |
⇒ **先升级 store,再升级二进制**(或两者同一批做完)。
### 步骤(本机,用源码)
```bash
# 1) 用**当前**hmapdev 把本机源码装成 store(--from 在旧版也支持)
hmapdev sdk install --from <SDK 源码目录> # 例:third_party/homeagent-sdk
# 2) 重编 hmapdev 二进制(模板 embed 进去了,必须重编)
cd <SDK 源码>/tools/hmapdev
CGO_ENABLED=1 go build -tags onnxruntime -o /tmp/hmapdev-new . # TAG_ARGS 视内核而定
# 3) 原子替换(先备份!)
cp /usr/local/bin/hmapdev /usr/local/bin/hmapdev.bak.$(date +%Y%m%d-%H%M%S)
install -m 0755 /tmp/hmapdev-new /usr/local/bin/hmapdev
```
### 验证(三步都要过)
```bash
hmapdev --version # 版本号
strings /usr/local/bin/hmapdev | grep -c mgrStop # ≥1 ⇒ 新模板已 embed
diff <源码>/tools/hmapdev/templates/proc_main.go.tmpl \
~/.homeagent/hmapdev/sdk/<ver>/tools/hmapdev/templates/proc_main.go.tmpl # 必须无差异
```
★ **`diff` 陷阱**:本机 PATH 上的 `diff` 是 HarmonyOS 工具链里的一个
**坏二进制**(`/opt/huawei/harmonyos/ohos-sdk/linux/toolchains/diff`),
对**不同**的文件也返回 **exit 0 且无输出** ⇒ 会给出"已同步"的**假结论**。
用 `cmp` 或 Python 比对,或显式用 `/usr/bin/diff`。
### 判据(自动守"三处同步")
`internal/plugin/proc/bridge_sync_test.go` 有 3 条,任一不同步即红:
1. `TestBridgeCoversEveryPublishedPluginMgrMethod` —— SDK 方法集 ↔ 能力表
2. `TestBridgeDispatchesEveryPluginMethod` —— 内核 Handle 分派
3. `TestHmapdevTemplateImplementsAllPluginMgrMethods` —— **hmapdev 模板**
第 3 条直接读 `third_party/homeagent-sdk/tools/hmapdev/templates/proc_main.go.tmpl`,
所以**源码模板**始终被守;store 里的副本靠上面的 `diff` 验证。
!!! tip "什么时候必须重编"
只有一种情况:**内核跨了主版本**(如 1.x → 2.x)。
主版本跃迁是破坏性变更的定义,此后的帧格式与共享段布局都不兼容,
必须用配套 `hmapdev` 重编。
**中版本跃迁(如 1.1 → 1.2)不需要重编**:1.x 内的新增全部是
「插件调用、内核实现」,不调新面就不受影响。
## 协议兼容性:只认主版本号
**没有独立的协议版本号。** 兼容性由双方版本号的**主版本段**表态:
内核的 `meta.Version` 与插件的 `sdk_version`(插件包里的 `sdk_version` 字段,
即编译时链接的 SDK 版本)。
| 内核 | 插件 SDK | 结果 |
|---|---|---|
| 1.4.0 | 1.0.0 / 1.1.0 / 1.3.0 | ✅ 建链(同主版本) |
| 1.4.0 | 2.0.0 | ❌ 拒绝(跨主版本) |
**为什么不设独立的协议号**:语义版本变动频繁(修 bug、加字段),如果每个
这类改动都要人去升一个额外的号,那个号必然会被漏升而腐烂(历史实证:
曾有一个 `ProtocolVersion` 常量,多次布局变更里只升过一次,而对应的守卫
测试长期处于失败状态、无人察觉)。让兼容性只挂在**始终会被发的那个号**上,
就没有「忘同步」这个失效模式。
跨主版本错配**不会静默失效**——握手时会显式拦下,并提示用配套 `hmapdev` 重编。
此外握手还会比对 `field_count`(StageContext 描述符个数):它决定标志位基址,
不一致会让插件读到错位字段,因此也在握手上显式拒绝。
## 怎么确认自己在用什么
装的 SDK 版本:
```bash
hmapdev sdk current
hmapdev sdk list
```
插件声明的目标版本在 `plg.json` 的 `sdk` 字段。若该版本不在本地存储里,
`hmapdev` 会**明确报错**,不静默降级 —— 静默降级会产出与内核协议不匹配的包,
那种失败要到运行时才暴露。
## 文档站对应的版本
本页与 [API 参考](api/index.md) 由 `tools/apidoc` 从源码生成,
内容随源码一起演进。发现文档与代码不一致时,**改的是源码注释**,
`go run ./tools/apidoc` 重新生成即可(见下方「维护」)。
## 维护(给 SDK 维护者)
```bash
cd homeagent-sdk
go run ./tools/apidoc -pkgdir ./sdk -out /tmp/api.json
go run ./tools/apidoc/gensite -api /tmp/api.json -out ./docs -examples ./example
mkdocs serve # 本地预览
mkdocs build # 产出 site_build/
```
API 面的**能力分层**(哪些 API 仅内置可用)记在 `tools/apidoc/tiers.json`,
每条裁定都附源码依据 —— 改这里而不是改生成物。