mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 14:27:30 +00:00
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>
This commit is contained in:
3
example/commondev/skill/.gitignore
vendored
Normal file
3
example/commondev/skill/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# skill/ 里的内容是随包分发的纯 SKILL 条目。
|
||||
# 它们是真实需要版本控制的文档(不像 rawfile/ 下可能有上百兆二进制),
|
||||
# 所以这里**不忽略**任何内容,只保留本说明。
|
||||
855
example/commondev/skill/commondev-discipline/SKILL.md
Normal file
855
example/commondev/skill/commondev-discipline/SKILL.md
Normal file
@ -0,0 +1,855 @@
|
||||
---
|
||||
name: commondev-discipline
|
||||
description: commondev 主管式多仓库开发流水线的使用与开发纪律。当你要为某个工程开一条分支、把工程派给一个下属 agent、或用 pi 拉起执行 agent 干活时必读。核心动作:为每个工程对话新建驻留式子 agent 并把该工程的一对通道划给它(否则所有工程都挤在根 agent 上)。**git 托管平台的操作(Gitea/GitHub/GitLab)不在插件里,而在本文** —— 用 enc_shell + 保存的凭据调 curl/CLI 完成。也含 hmap 插件开发与部署纪律。
|
||||
version: 2.0.0
|
||||
author: HomeAgent
|
||||
---
|
||||
|
||||
# commondev 纪律(主管模式 · 工程对话 · 平台操作)
|
||||
|
||||
本技能三部分,**都可能与你的任务直接相关**:
|
||||
|
||||
- **第一部分:使用者纪律** —— 怎么开工程、怎么派活。
|
||||
- **第二部分:★ git 平台操作** —— **本文是权威**。开 PR、审批、分支保护怎么做。
|
||||
- **第三部分:开发与部署纪律** —— 怎么不踩坑。
|
||||
|
||||
> ## ★ 架构边界(用户拍板 2026-10-15)
|
||||
>
|
||||
> | 层 | 负责 |
|
||||
> |---|---|
|
||||
> | **插件 commondev** | 项目台账、工作目录、pi 会话与通道、判据与变异复核、冲突预检、凭据保存与安全执行 |
|
||||
> | **本文(skill)** | **各 git 托管平台的认证头、API 端点、PR 与分支保护流程** |
|
||||
>
|
||||
> **插件里没有平台逻辑。** 操作 Gitea/GitHub/GitLab 等平台一律:
|
||||
> 读本文 → 用 `enc_shell` + `@{凭据名}` 执行 `curl`/`gh`/`tea`。
|
||||
>
|
||||
> ⇒ **新增一个托管平台只需改本文,不需要改插件、不需要重编译、不需要重启服务。**
|
||||
>
|
||||
> 原本这些在插件里(`platformSpec` / `commondev_account_*` / `commondev_pr_*` /
|
||||
> `commondev_protect_branch` / `commondev_repo_search`),代价是:
|
||||
> 每接一个平台都要改 Go 代码 → 重编译 → 重打包 → 重启,
|
||||
> 且**未列出的平台直接报错**(GitLab/Gitee/Bitbucket 全不支持)——
|
||||
> 而 agent 用 curl 本来什么平台都能打。这 10 个工具已移除。
|
||||
|
||||
> 看不懂某个词时的权威文档(按顺序读):
|
||||
> 1. `docs/zh/commondev-design.md` —— 目标机制、台账 schema、判据与变异、安全(§12 是架构变更)。
|
||||
> 2. `docs/zh/commondev-agent-runtime-design.md` —— 运行时与内核接入(续篇)。
|
||||
> 3. `docs/zh/kernel-secret-firewall-design.md` —— 凭据模块(enc_* 的底座)。
|
||||
> 4. `docs/zh/resident-subagent-design.md` —— 驻留子设计的原始文档。
|
||||
>
|
||||
> **本文与文档冲突时,以文档为准,并回来修正本文。**
|
||||
|
||||
---
|
||||
|
||||
## Setup: 第二部分 git 平台操作(本文是权威,不要凭记忆写 API)
|
||||
|
||||
### 11. 所有平台都是同一个形状
|
||||
|
||||
「带认证头发一个 REST 请求」。用 `enc_shell` + `curl` 即可:
|
||||
|
||||
```
|
||||
enc_shell(command="curl -s -H \"<认证头>\" <API前缀><路径>")
|
||||
```
|
||||
|
||||
**差异只有两个维度:API 前缀 与 认证头。** 其余是 REST 常规。
|
||||
|
||||
### 12. 认证头与 API 前缀(逐平台,★ 四种形状互不相同)
|
||||
|
||||
| 平台 | API 前缀 | 认证头 |
|
||||
|---|---|---|
|
||||
| **Gitea** | `<base>/api/v1` | `Authorization: token <T>` ← **不是 Bearer** |
|
||||
| **GitHub** | `https://api.github.com` | `Authorization: Bearer <T>` |
|
||||
| **GitLab** | `<base>/api/v4` | `PRIVATE-TOKEN: <T>` ← **不是 Authorization** |
|
||||
| **GitCode** | `https://api.gitcode.com/api/v5` | `Authorization: Bearer <T>` |
|
||||
| **Gitee** | `https://gitee.com/api/v5` | `access_token=<T>`(**查询参数**,不是头) |
|
||||
|
||||
⚠ 头名、前缀、甚至「用头还是用查询参数」都不同。**未知平台先查官方文档,不要猜形状。**
|
||||
|
||||
### 13. 先保存凭据,再用它
|
||||
|
||||
```
|
||||
enc_store(alias="gitea-token", value="${GITEA_TOKEN}") # 值优先走 ${ENV} 或 @file:
|
||||
enc_shell(command="curl -s -H \"Authorization: token @{gitea-token}\" http://192.168.2.106:3000/api/v1/user")
|
||||
```
|
||||
|
||||
⚠ **优先用 `enc_store`,不要用 `commondev_account_add`** —— 后者已不存在
|
||||
(它内建了平台语义:platform/role/base_url,那正是被移出的东西)。
|
||||
|
||||
**先验凭据再干活**(早失败,别推了一半才发现 token 错):
|
||||
|
||||
```
|
||||
enc_shell(command="curl -s -o /dev/null -w '%{http_code}' -H \"Authorization: token @{gitea-token}\" http://192.168.2.106:3000/api/v1/user")
|
||||
# 期望 200;401/403 = 凭据无效或权限不足
|
||||
```
|
||||
|
||||
### 14. 常见操作(以 Gitea 为例,其他平台换前缀与头)
|
||||
|
||||
```bash
|
||||
# 搜仓库
|
||||
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/search?q=myname&limit=20"
|
||||
|
||||
# 看仓库 / 分支
|
||||
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj"
|
||||
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj/branches"
|
||||
|
||||
# 开 PR(head=你的分支,base=目标分支)
|
||||
curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json' -d '{"title":"...","head":"feat/x","base":"main"}' "http://host/api/v1/repos/jianf/MyProj/pulls"
|
||||
|
||||
# 查 PR 状态
|
||||
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj/pulls/7"
|
||||
|
||||
# 合 PR
|
||||
curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json' -d '{"Do":"merge"}' "http://host/api/v1/repos/jianf/MyProj/pulls/7/merge"
|
||||
```
|
||||
|
||||
**push 时凭据不进 remote URL**:
|
||||
|
||||
```bash
|
||||
# ✅ 一次性注入(仅本次命令);clone 出来的 remote 保持干净
|
||||
enc_shell(command="cd <repo> && git -c http.extraheader=\"Authorization: token @{t}\" push origin feat/x")
|
||||
```
|
||||
|
||||
⚠ 若改用 `https://token@host/...` 形式,token 会**持久化进 `.git/config`** ——
|
||||
删掉临时目录也删不掉凭据(`git remote -v` 就能读出来)。设计 §7.2 明令禁止。
|
||||
|
||||
### 15. Gitea 分支保护:让「必须复审」成为**机制**而非礼貌
|
||||
|
||||
**光有流程约定不算机制** —— 执行 agent 可以直接推主干。必须让**平台**拦。
|
||||
|
||||
```bash
|
||||
curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json' -d '{"enable_push":false,"required_approvals":1,"block_admin_merge_override":true,"dismiss_stale_approvals":true}' "http://host/api/v1/repos/jianf/MyProj/branch_protections?name=main"
|
||||
```
|
||||
|
||||
四个开关的意义(缺一个机制就有洞):
|
||||
|
||||
| 开关 | 作用 | 缺了会怎样 |
|
||||
|---|---|---|
|
||||
| `enable_push: false` | 禁止直接推送 | 执行 agent 直推主干,绕过评审 |
|
||||
| `required_approvals: ≥1` | 至少一个审批 | 自己就能合 |
|
||||
| `block_admin_merge_override: true` | admin 也不能绕过 | 用 admin token 的人绕过一切 |
|
||||
| `dismiss_stale_approvals: true` | 新提交作废旧审批 | 先批准再改代码,批准白拿 |
|
||||
|
||||
**真机实测(Gitea 1.24.2)**:
|
||||
|
||||
| 动作 | 普通用户 token | admin token |
|
||||
|---|---|---|
|
||||
| push `main` | ❌ `pre-receive hook declined` | ⚠ 可能成功(见下)|
|
||||
| push `feat/*` | ✅ | ✅ |
|
||||
| 自合 PR | ❌ `Does not have enough approvals` | ⚠ 取决于开关 |
|
||||
| 审批 → 合 PR | — | ✅ |
|
||||
|
||||
⇒ **推论:执行侧必须用普通用户 token。** 用 admin token 会**静默绕过分支保护**
|
||||
(看起来正常,实际机制失效且无报错)—— 这是最容易搞错的一点。
|
||||
|
||||
⚠ 分支保护已存在时用 **PATCH**(POST 会 403 `already exist`)。
|
||||
|
||||
### 16. 审批:三个必踩的 API 坑(实测)
|
||||
|
||||
1. **`event` 必须是 `APPROVED`,不是 `APPROVE`。**
|
||||
传 `APPROVE` 时请求**返回 200 且不报错**,但 review 状态落成 `PENDING` ——
|
||||
于是 `required_approvals` 永远不满足,合并时报 `Does not have enough approvals`。
|
||||
表象(权限不足)与真因(枚举值少个 `D`)毫无关联,极难自查。
|
||||
2. **必须带 `commit_id`** —— 不带时同样落成 `PENDING`(与 `event` 是两个独立条件)。
|
||||
3. **`required_approvals` 也约束 admin** —— 别指望用 admin 跳过审批。
|
||||
|
||||
```bash
|
||||
# ① 先取 head sha(审批必须绑定**具体提交**,不是"移动的目标")
|
||||
enc_shell(command="curl -s -H \"Authorization: token @{t}\" http://host/api/v1/repos/jianf/MyProj/pulls/7 | grep -o '\"sha\":\"[a-f0-9]*\"' | head -1")
|
||||
# ② 审批(event 必须 APPROVED,且带 commit_id)
|
||||
enc_shell(command="curl -s -X POST -H \"Authorization: token @{t}\" -H 'Content-Type: application/json' -d '{\"body\":\"通过\",\"event\":\"APPROVED\",\"commit_id\":\"<sha>\"}' http://host/api/v1/repos/jianf/MyProj/pulls/7/reviews")
|
||||
```
|
||||
|
||||
**失败时保留平台原话**:`Does not have enough approvals` 是区分「机制生效」
|
||||
与「配置错了」的唯一线索,别把它翻译掉或吞掉。
|
||||
|
||||
---
|
||||
|
||||
## Usage: 第一部分 使用者纪律
|
||||
|
||||
> **★ 设计总纲 [已定]**:插件对 pi 做的事,本质就是
|
||||
> **「把给 pi 发消息包装一下、把 pi 返回包装一下」**,深度融合内核的 IO 抽象与中断系统
|
||||
> —— 即 **pi = 一个 Device**。**不要自己造调度器 / 会话路由 / 中断栈**,
|
||||
> 全部接到内核已有的那套上(`InjectInputTo` / `RegisterOutputChannel` / `InjectInterrupt*Opts`)。
|
||||
|
||||
### 21. 角色与单位
|
||||
|
||||
| 角色 | 是谁 | 干什么 |
|
||||
|---|---|---|
|
||||
| **人** | 老板 | 只与主管对接 |
|
||||
| **主管** | 你(长期驻留的 HomeAgent 主 agent) | 拆任务、**定义判据**、复审、决定合入 |
|
||||
| **执行 agent** | 按工程拉起的一个 pi / dsh 实例 | 在某棵工作树里干活 |
|
||||
| **commondev 插件** | 外部独立插件 | 仓库/凭据/分支/PR/测试,全部机械化操作 |
|
||||
|
||||
**为何能用内核机制**(而不是自己造):
|
||||
|
||||
| 内核概念 | pi 侧对应 | 落点(内核已有) |
|
||||
|---|---|---|
|
||||
| **给 pi 发消息** | `pi.prompt` / `steer` / `follow_up` | `InjectInputTo(source, "commondev/<id>", …)` |
|
||||
| **pi 返回** | sidecar 回调 / `tool_execution_*` / `agent_settled` | `InjectTextTo(source, "commondev/<id>", …)`(按 inputch Owner 路由) |
|
||||
| **中断 pi** | `steer`(插到当前轮之后) | `InjectInterruptTextOpts(..., Priority: L3)` |
|
||||
| **音声/图像产物** | pi 可产出的媒体 | `OutputCapabilities()` 声明 |
|
||||
|
||||
⇒ **插件只做两件额外事**:① 把 pi 接成 Device;② 台账记 `session_id` 并断言未变。
|
||||
调度/中断/路由/记忆策略**全是内核已经在做的事**。
|
||||
|
||||
### 22. 「工程对话」是一等对象
|
||||
|
||||
它是四元组,缺一不可:
|
||||
|
||||
```
|
||||
工程对话 (id = br-<project>-<slug>)
|
||||
├─ workdir <插件数据目录>/program/<hash>-<name>
|
||||
│ ★ hash = H(项目+分支) 是**唯一性来源**
|
||||
│ ⇒ **同一仓库的不同分支必然是不同目录**(否则两 agent 共用工作树、改动互相污染)
|
||||
│ ★ name = 分支名派生,**仅做可读性**,不承担唯一性
|
||||
│ (派生后可能撞:`feat/a` 与 `feat.a` 都成 `feat_a`,所以必须带 hash)
|
||||
│ 独立 clone(非 worktree、非 --reference);合入后**整目录删除**
|
||||
├─ session_id pi 会话(★ 续投的唯一依据)
|
||||
├─ runtime local(默认,内嵌) | ssh(远端同样建目录拉 pi)
|
||||
├─ host runtime=ssh 时的目标主机(**branch_open 时给**,见 §24.1)
|
||||
├─ agent_cli 执行后端(默认 pi;其他 CLI 需自备 sidecar,见 §24.2)
|
||||
└─ channels 插件为该工程注册的一对通道
|
||||
├─ inputch commondev/<id>
|
||||
└─ outputch commondev_<id>
|
||||
```
|
||||
|
||||
**一个仓库可并行开发**:每分支一份独立 clone,互不污染、可同时推进。
|
||||
但并行有前提——必须是**互不相干的模块**;有依赖的任务请先拆边界。
|
||||
|
||||
> **为何不能用 worktree / `--reference`**(实测):worktree 删后有
|
||||
> `.git/worktrees/` 残留(需 `prune`);`--reference` 有 alternates 外部依赖,
|
||||
> mirror 被清即损坏。**本地 clone 对象为硬链接**(实测 inode 相同)⇒ 省磁盘、
|
||||
> 无外部依赖、刪掉即彻底。代价:失去 worktree「同分支不可双挂」的保护
|
||||
> ⇒ 台账层必须补**唯一性闸**(同名未结分支响亮拒绝)。
|
||||
|
||||
### 23. ★ 必做:为每个工程对话新建驻留子,并把通道划给它
|
||||
|
||||
**★ 顺序很关键**:通道是**拉起 pi 拿到 session 之后**才注册的,所以你的动作是:
|
||||
|
||||
```
|
||||
建工程 ──► commondev_agent_up(拉起 pi,拿到 session_id)
|
||||
└─► 插件**此刻才**注册通道 inputch commondev/<id> + outputch commondev_<id>
|
||||
└─► ★ 你建驻留子并把这对通道划给它
|
||||
```
|
||||
|
||||
**为何必须先有 session**:通道是“与某个具体 pi 会话对话的入口”,没 session 就是空壳。
|
||||
顺序错了(先建子)会拿到一个指向不存在通道的 `output_send__…`。
|
||||
|
||||
**插件只能把通道登记出来;「划入(assign)」只有内核能做。** 所以这一步**必须由你(agent)
|
||||
自己调内核工具完成**,插件无法代劳。
|
||||
|
||||
```
|
||||
resident_agents create
|
||||
id = dev-<br-id>
|
||||
input_chs = commondev/<br-id> ← 划入该 inputch(可用斜杠)
|
||||
allowed_outputs = commondev_<br-id> ← 授权该 outputch(★ 下划线,见下)
|
||||
capacity = 64
|
||||
task_prompt = "你在 <workdir> 推进 <work>。
|
||||
下任务给自己用 output_send__commondev_<br-id>(payload=…),
|
||||
pi 的结果会回投到你的 commondev/<br-id>;
|
||||
有结论用 notify_parent 汇报。"
|
||||
```
|
||||
|
||||
**★ 下任务不需要新工具**:给 pi 发消息就是子调**内核原生的输出门**
|
||||
`output_send__commondev_<br-id>(payload="…")` ⇒ 打到插件的通道 handler ⇒ 插件
|
||||
查台账拿到 `session_id` 后 `sidecar.prompt()`。**插件不提供 `agent_send` 之类的工具**。
|
||||
|
||||
**⚠️ 通道命名(很容易踩)**:
|
||||
|
||||
| | 能用 `/` 吗 | 为什么 |
|
||||
|---|---|---|
|
||||
| **inputch** | ✅ | 内核自己就用 `child/<id>` |
|
||||
| **outputch** | ❌ **绝对不行** | 它会拼成 `output_send__<name>`,上游要求函数名 `^[a-zA-Z0-9_-]{1,64}$`;违反的后果不是“这个工具不可用”,而是**整条请求 400、agent 整体失语** |
|
||||
|
||||
⇒ 所以 inputch 叫 `commondev/<br-id>`、outputch 叫 `commondev_<br-id>`。
|
||||
`<br-id>` 本身也只能用 `[A-Za-z0-9_-]`。
|
||||
|
||||
**为什么必须这么做(不是可选的优化):**
|
||||
|
||||
1. inputch 是**最基本的输入路由单位**,路由按 `Owner` 在**进内核之前**完成。
|
||||
划给子之后,**该工程的输入只流向那个子**,根 agent 看不到它 —— 这正是
|
||||
多工程并行能做到"互不串台"的原因。
|
||||
2. **不划会的代价**(如果你偷懒):所有工程对话的输入都涌向**根 agent**,
|
||||
根成了瓶颈、上下文被 N 条工程的细节淹没,多工程并行**等于没做**。
|
||||
现场教训(`resident-subagent-design.md`):子挂着 `inputch=[timer]` 而 timer
|
||||
的输入打在父身上,**子的轮次永远是 0** —— 登记了不等于划了。
|
||||
|
||||
**落地判据**(怎么知道做对了):
|
||||
|
||||
- 投一条输入到 `commondev/<br-id>`,应只被 `dev-<br-id>` 处理;
|
||||
- 根 agent 的轮次**不**因这条输入增加;
|
||||
- `input_channels` 工具(内核自带,单工具多视图)用 `view=by_agent` 能看到划分总览。
|
||||
|
||||
### 24. 形态选择、自包含运行时与远端环境
|
||||
|
||||
**默认本地内嵌**(进程内 pi 实例);通过配置 `runtime=ssh` 切到**远程**,
|
||||
远端**同样创建目录并在该目录拉起 pi**。
|
||||
|
||||
**★ 自包含运行时**:插件**自带独立的 node 与独立的 pi**,都装在**插件工作目录**,
|
||||
**pi 的插件也装到这个目录** —— 不靠宿主机的 node/pi,也不污染用户的 `~/.pi`。
|
||||
**独立 node/pi 随包分发**(不首次下载、不依赖用户装)。
|
||||
|
||||
```
|
||||
<插件数据目录>/runtime/
|
||||
node/ pi/
|
||||
agentdir/ ← 每个内层 pi 必须传 PI_CODING_AGENT_DIR 指这里
|
||||
settings.json ← pi 自己的配置(偏好 / 默认 model / 全局包声明)
|
||||
models.json ← pi 自己的配置(★ LLM 源写这里)
|
||||
auth.json ← pi 自己的配置(key / OAuth,0600)
|
||||
mcp.json ← pi 自己的配置(MCP 服务器)
|
||||
skills/ prompts/ themes/ extensions/
|
||||
```
|
||||
|
||||
#### 24.1 远端(ssh)怎么开
|
||||
|
||||
两个轴**正交**,开线时一起给:
|
||||
|
||||
```
|
||||
commondev_branch_open(project="p", branch="feat/x",
|
||||
runtime="ssh", ssh_host="build@10.0.0.9")
|
||||
```
|
||||
|
||||
- `runtime=ssh` 而**没给 `ssh_host`** → 直接拒绝(不会拖到拉起时才报)
|
||||
- **未知 runtime**(比如 `k8s`)→ 响亮拒绝,**不静默降级为 local**
|
||||
(静默降级会让"我以为跑在远端"一路错下去)
|
||||
- 远端前置条件(**先配好,不要指望交互输密码**):
|
||||
- 从本机到目标机的**免密**(`BatchMode=yes`,卡在密码提示会超时)
|
||||
- 远端有 node(不在 PATH 里就配 `ssh_node_path`,例如 `/opt/node/bin/node`)
|
||||
- **必配两个根目录**(缺任一,远端线开不起来):
|
||||
- `ssh_workdir_root` — 远端放**工程工作目录**的根
|
||||
- `ssh_runtime_root` — 远端放 **sidecar 脚本**的目录
|
||||
- **★ 工作目录在远端准备**:本地 `clone --mirror` 出来的目录远端看不到,
|
||||
所以远端线走的是**远端建目录 → 远端共享 mirror(`<root>/.mirrors/`)→
|
||||
远端 clone → 远端切分支**。同一远端上的多条分支线共享一份 mirror。
|
||||
台账里的 `workdir` 会被改写成**远端路径**(不是本地路径)。
|
||||
远端拉取要求**远端自己能认证该仓库** —— 本插件**不把凭据推到远端**
|
||||
(凭据不进工作目录、不进 remote URL)。拉不动就响亮报错。
|
||||
- 每次拉起会 `scp` 把 sidecar 推过去(幂等)——**不推就会跑上一次的旧脚本**,
|
||||
症状是"改了没生效",极难归因。推送失败**响亮报错**,不静默用旧脚本。
|
||||
- 用 `commondev_agent_state` 看 `ssh_host` 确认打到哪台机。
|
||||
|
||||
#### 24.2 接其他 CLI(zcode 等)——**不改插件核心**
|
||||
|
||||
执行后端是**声明式**的:放一个目录就多一个后端。
|
||||
|
||||
```
|
||||
<插件数据目录>/runtime/<cli>/
|
||||
driver.json ← 描述:脚本名 / 参数 / 环境变量 / LLM 源策略
|
||||
sidecar.mjs ← 实现 sidecar 契约(ping / prompt / state / ...)
|
||||
```
|
||||
|
||||
`driver.json` 关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `script` | 脚本文件名(默认 `sidecar.mjs`) |
|
||||
| `node` | `auto` = 用插件自带的 node |
|
||||
| `args` | 参数模板,占位符 `{workdir} {sessionId} {sessionName} {dataDir} {pluginDir}` |
|
||||
| `env` | 环境变量模板(同样支持占位符) |
|
||||
| `llmConfig` | `pi` = 插件替它写 pi 的配置;**`none` = 它自管凭据,插件不碰** |
|
||||
| `sessionDirSuffix` | 会话目录后缀 |
|
||||
|
||||
**纪律**:
|
||||
|
||||
- **未知 `agent_cli` 响亮报错,绝不回退到 pi** —— 回退会用错后端跑任务而无提示。
|
||||
- `driver.json` **损坏也报错**(不猜默认),否则是"改了没生效"的经典来源。
|
||||
- 内置 pi **不需要** `driver.json`(有内建描述),老部署不受影响。
|
||||
- 现成例子看 `examples/custom-cli/zcode/`(**源码核实过**的 ZCode 契约)。
|
||||
|
||||
### 25. ★ 两种插件安装粒度(临时 vs 全局)
|
||||
|
||||
| 粒度 | 作用域 | 落到实处 | 用什么 |
|
||||
|---|---|---|---|
|
||||
| **临时** | 一个工程对话 | `<workdir>/.pi/settings.json` | `pi install -l` |
|
||||
| **全局** | 插件自带 pi 的**所有**工程 | `<agentdir>/settings.json` | `pi install` + `PI_CODING_AGENT_DIR` |
|
||||
|
||||
**支持的源**(实测):`npm:@scope/pkg@1.0.0`(带版本即钉住)/ `git:github.com/u/r@v1` /
|
||||
`https://github.com/u/r`(当 git 源)/ `ssh://…` / 本地路径(**不复制**)。
|
||||
|
||||
**三条纪律**:
|
||||
|
||||
1. **必须传 `PI_CODING_AGENT_DIR`**,否则「全局」安装会写进**用户的** `~/.pi/agent/settings.json`
|
||||
—— 那是对宿主的越界。
|
||||
2. **⚠ 安装是复制,不是引用**:`pi install ./pkg` 会把包**复制**到目标;改源目录
|
||||
**不影响已装的那份**(我曾因此得出「插件不生效」的错误结论)。要更新必须重装。
|
||||
3. **安装是显式一步**,不藏在 `agent_up` 里;来源(npm 包名 / 仓库 URL / 版本钉点)要可见。
|
||||
|
||||
### 26. ★ LLM 源与 pi 配置:直接写 pi 的配置文件
|
||||
|
||||
⛔ **不要自建第二套配置存储**(本稿前版曾提 `config_commondev`,**已废弃**)。
|
||||
**pi 本身就有完整的配置系统**,插件只是它的**受控写入入口**:
|
||||
|
||||
```
|
||||
<agentdir>/settings.json ← 偏好 / 默认 model / 「全局」包声明
|
||||
<agentdir>/models.json ← 兼容端点与模型(★ LLM 源写这里)
|
||||
<agentdir>/auth.json ← API key / OAuth(0600)
|
||||
<agentdir>/mcp.json ← MCP 服务器
|
||||
```
|
||||
|
||||
内核把 LLM 源存在 **`config` 表**(不是 `config_<plugin>`),键前缀 `core.llm.`:
|
||||
|
||||
```
|
||||
core.llm.provider = llmsproxy ← ★ 默认源就是这一项
|
||||
core.llm.sources.<n>.base_url / .api_key / .model / .adapter / .context_window …
|
||||
```
|
||||
|
||||
**导入时字段一对一对上**:`base_url`→`baseUrl`、`api_key`→`apiKey`、
|
||||
`adapter=openai`→`api="openai-completions"`、`model`→`models[].id`、
|
||||
`context_window`→`contextWindow`。
|
||||
|
||||
**工具面**(每个工具对应一处 pi 配置):
|
||||
|
||||
```
|
||||
commondev_pi_source_import 把内核的源批量导入内置 pi
|
||||
commondev_pi_source_add 添加源(写 models.json / auth.json)
|
||||
commondev_pi_source_list 列出(默认**不返回** api_key)
|
||||
commondev_pi_source_default 指定默认源(写 settings.json)
|
||||
commondev_pi_config_get/set 读写其他 pi 配置
|
||||
commondev_pi_mcp_add/remove/list
|
||||
```
|
||||
|
||||
**四条实现纪律**:
|
||||
|
||||
1. **写的就是 pi 的配置文件**,不造中间层;格式与 pi 自己写出的保持一致
|
||||
(否则 pi 重写文件时会把自定义字段抹掉)。
|
||||
2. **改前备份、改后回读校验**:json 写坏会让内层 pi 直接起不来;失败则回滚 + **响亮报错**。
|
||||
3. **key 不进日志/不进返回值**。
|
||||
4. **不改用户全局**:所有写入带 `PI_CODING_AGENT_DIR=<agentdir>` 或显式 scope,
|
||||
绡不碰 `~/.pi/agent/`。
|
||||
|
||||
**两条红线**:
|
||||
|
||||
1. **key 不能明文落盘到无保护位置**:`models.json`/`auth.json` 含明文 key ⇒ 目录 **0700**、
|
||||
文件 **0600**。内核那套是明文存 `config.db`(**644 全局可读**)—— **别把这套惯例带进来**。
|
||||
2. **不得静默回退到宿主凭据**:导入失败/源不可用就**报错**,不要偷偷用 `~/.pi/agent/auth.json`
|
||||
跑起来(那是宿主的凭据,用了就是越界)。
|
||||
|
||||
### 27. 远端环境缺失:问用户,不要自己装(硬纪律)
|
||||
|
||||
```
|
||||
ssh 不通 → 报错(配置问题,不是缺环境)
|
||||
无 pi / 无 node → ★ 注入 L3「远端 <host> 未装 X,是否安装?」→ **你(agent)去问用户**
|
||||
目录不可写 → 报错(权限问题,需人处理)
|
||||
```
|
||||
|
||||
- **SDK 没有“向用户提问”的 API**(插件是子进程,不能直接弹 UI)。现有机制是插件把
|
||||
决策消息注入给 agent(L1–L3),**由 agent 问用户**(先例:`cli` 的 `/stop` 用 L3)。
|
||||
- **不得擅自安装**:不 `curl | sh` 到别人的机器上。要先说清「装什么、装哪里、要什么权限」。
|
||||
- **不冒充成功**:环境缺失时 `agent_up` **响亮失败** + 留一条待决策项,
|
||||
**不静默降级到本地跑**(那会让人以为活派到了远端)。
|
||||
- **问完要能续**:安装动作本身是**可见的一步**(可查、可重跑),不是 `agent_up` 的隐式副作用。
|
||||
|
||||
### 28. 投递纪律(**这是最容易犯的错**)
|
||||
|
||||
**每个工程的投递必须落到同一个 `session_id`。** 实测反例(别重犯):
|
||||
|
||||
| 错法 | 后果 |
|
||||
|---|---|
|
||||
| 漏传 `--session-id` | `pi` **静默开新会话** —— 你以为在催上一次,其实是把活派给了陌生人 |
|
||||
| 两个进程**并发**同 `--session-id` | 会话文件被写坏:`user A`/`user B` 相邻,`assistant B` 在前、`assistant A` 落在最后 ⇒ B 那轮的模型上下文里**根本没有 A 的用户消息**,树链交错 |
|
||||
|
||||
⇒ 投递前先拿台账里的 `session_id`,投完**断言它没变**;一个 session **绝不并发投递**。
|
||||
这两条插件侧已做成结构性保证(单一投递队列 + 断言),你做主管时**不要绕过插件**直接
|
||||
去敲 `pi -p`。
|
||||
|
||||
**★ 另外两条实测硬经验(别再踩):**
|
||||
|
||||
3. **SDK 路径必须显式指定会话 id**。用 `createAgentSession({cwd})` 而不传
|
||||
`sessionManager` 时,pi **自己生成 UUID** 作会话 id ⇒ 与台账名字不一致 ⇒
|
||||
断言**永远失败**(实测:期望 `br-cdtest-feat_alpha`,实际 `01a13959-…`)。
|
||||
正确:`SessionManager.create(cwd, sessionDir, {id})` 再传给 `createAgentSession`。
|
||||
4. **工具返 `ok` ≠ 消息到了**。内核对成功回执刻意**极简**(防 output loop),
|
||||
所以 `ok` 只说 handler 成功了。必须去会话文件里看到 `user: <payload>` 才算数。
|
||||
(这条能防住“以为投了、其实没投”这类假成功。)
|
||||
|
||||
### 29. 三层审查的分工(别把自己变成贵的高级码农)
|
||||
|
||||
| 层 | 审什么 | 不审什么 |
|
||||
|---|---|---|
|
||||
| agent 自审 | 代码正确性、风格、实现细节 | — |
|
||||
| **你(主管)复审** | **这次的改动,判据够不够**:引入了哪些新失败方式?判据覆盖了吗?变异咬得住吗? | 逐行读 diff |
|
||||
| 插件复核 | 机械执行「正向 + 变异」 | 语义判断 |
|
||||
|
||||
### 30. ★ 不要把「测试通过」当作「改对了」
|
||||
|
||||
插件只能跑**你预先写下的判据**。判据没覆盖那条 bug,插件照样打勾 ——
|
||||
自动化**不会**发现假绿,**只会让假绿跑得更快、更可信**。
|
||||
|
||||
因此复核**必须两步**:
|
||||
|
||||
| 阶段 | 做什么 | 不通过的后果 |
|
||||
|---|---|---|
|
||||
| **正向** | 跑判据 → 期望通过 | 实现有 bug,驳回 |
|
||||
| **变异** ★ | 自动改坏被测代码 → 期望判据**报红** | **判据太松,驳回** |
|
||||
|
||||
**没有变异阶段,就是在给「测试通过」这个信号不断充值信用,直到它骗一次大的。**
|
||||
本仓实证:一条只查前缀的判据在变异测试下仍全绿,是变异把它咬出来的。
|
||||
|
||||
配套纪律:判据写「某标识符是否出现在函数体」时**必须先剔除注释再匹配**
|
||||
(曾因注释里含 `outputParent` 之类的词导致假绿)。
|
||||
|
||||
### 31. 工程的目录与生命周期(一个仓库并行开发)
|
||||
|
||||
```
|
||||
branch_open
|
||||
└─ <hash>-<name>/ (由 mirror **本地 clone** 派生)
|
||||
★ hash = H(项目+分支):**唯一性来源**(同仓库不同分支必不同目录)
|
||||
★ name = 分支名派生:仅可读性
|
||||
└─ 干活 → 提交 → push → PR
|
||||
└─ 合入(或废弃)
|
||||
└─ ★ **整个目录删除**(不保留、不归档)
|
||||
```
|
||||
|
||||
**要点**:
|
||||
|
||||
- **合入后直接删目录** —— 用户明确要求;不删则下次同名分支会与残留目录混淆。
|
||||
- **一个仓库可并行**:每分支一份独立 clone。
|
||||
- **★ 同仓库不同分支必然不同目录**:`<hash>` 含分支(`H(项目+分支)`),hash 是**唯一性来源**;
|
||||
`name` 只做可读性。实测:`feat/alpha` vs `feat/beta` → `b3426c01-feat_alpha` vs `0566c1f5-feat_beta`。
|
||||
- **⚠ 慢操作必须异步**:内核把**每次工具调用**硬限制在 **60 秒**(`toolcall.go:62`,
|
||||
硬编码无配置项)。`git clone --mirror` 同步做 ⇒ 工具报超时、**但 mirror 其实已建好**
|
||||
—— 调用方只看到失败、状态却是“做了一半”。正确做法:工具只登记 + 发起后台作业,
|
||||
用状态工具追进度(本插件:`commondev_job_status`)。
|
||||
- **为什么不是 worktree / `--reference`**(实测):worktree 删后有 `.git/worktrees/`
|
||||
残留(需 `prune`);`--reference` 有 alternates 外部依赖,mirror 被清即损坏。
|
||||
**本地 clone 对象为硬链接** ⇒ 省磁盘、无外部依赖、删掉即彻底。
|
||||
- **⚠️ 代价:唯一性闸必须补上**。worktree 天然禁止「同分支双挂」,独立 clone **没有**这条
|
||||
保护(两份 clone 都能在 `feat/x` 上各自 push,直到被拒才知道)。
|
||||
台账必须对「同名且未结(open/review)的分支」建唯一索引,重复 `branch_open`
|
||||
**响亮拒绝**,不静默复用。
|
||||
- **并行有前提**:并行的必须是**互不相干的模块**。有依赖的任务(A 改接口、B 用接口)
|
||||
两棵树互不可见,问题会集中到 merge 时爆发 ⇒ **先拆边界再谈并行**。
|
||||
- 与内核「子不得比父活得久、无残留」同一条纪律(见第 20 节)。
|
||||
|
||||
---
|
||||
|
||||
## Notes: 第三部分 hmap 插件开发与部署纪律
|
||||
|
||||
> 面向「要改 commondev」或「要写另一个插件」的场景。**所有条目都在本机实测过**,
|
||||
> 不是从文档抄的。
|
||||
|
||||
### 32. 先搞清三件事,别猜
|
||||
|
||||
```bash
|
||||
hmapdev version # 工具链版本 + SDK 模块 + 构建用 Go
|
||||
hmapdev sdk current # 当前活跃 SDK 版本(★ 动手前必看)
|
||||
hmapdev sdk list # 本机已装的所有 SDK 版本
|
||||
```
|
||||
|
||||
- 核心仓 `/home/program/TrueAgent`(Go module `gitcode.com/JianFeeeee/HomeAgent`)
|
||||
- **SDK 仓** `third_party/homeagent-sdk`(独立 git 仓、独立版本号)
|
||||
|
||||
### 33. 开工流程
|
||||
|
||||
```bash
|
||||
hmapdev init <name> # Go 脚手架(--lua / --type remotedevice 可选)
|
||||
cd <name> && vim plugin.go # 注册工具
|
||||
hmapdev debug . # 不想装就先跑一遍
|
||||
hmapdev build # 产出 dist/<name>_bundle.hmap
|
||||
```
|
||||
|
||||
### 34. ★ 五个必踩的坑(按踩中概率排序)
|
||||
|
||||
| # | 坑 | 事实 |
|
||||
|---|---|---|
|
||||
| 1 | **SDK 版本** | 插件编译时**校验 SDK 能力**,用旧接口的工程直接构建失败,报错形如「当前 SDK 缺少 sdk.InjectOptions.Priority」,并给出两条出路:`hmapdev sdk install <version>` 或 `hmapdev sdk install --from <本地源码>`。**核心仓有新能力但 SDK 未跟上时用 `--from`** |
|
||||
| 2 | **`hmapdev build`/`debug` 不支持 `--help`** | 不是打印帮助,而是去读当前目录的 `plg.json` ⇒ 你会看到 `error: read plg.json: no such file or directory`。**这不是故障**。要看 flag 用 `hmapdev --help` |
|
||||
| 3 | **产物是 ZIP,不是二进制** | `dist/<name>_bundle.hmap` 是 ZIP(魔数 `PK`),用 `unzip` 解,内含 `plugin.bin.linux.amd64` 等多平台二进制。**生产要的是解包后的单个二进制** `/home/newqqagent/plugins/<name>/plugin.bin`(不是 zip) |
|
||||
| 4 | **`plugin.json` 是安装标志** | 生产目录里没有它,插件不会被识别。`plg.json` 是**构建**标志,两者不同 |
|
||||
| 5 | **`plg.json` 的 `sdk` 字段必须完整 x.y.z** | 写 `"1.4"` 会被**直接拒绝**(SDK 版本纪律:跟随内核中版本、patch 位恒为 `.0`,一条内核线只有一个 SDK 版本) |
|
||||
|
||||
### 35. 注册工具的正确形状
|
||||
|
||||
```go
|
||||
s.RegisterTool(tp+"mytool", sdk.ToolDef{
|
||||
Name: tp + "mytool", // ← Name 必填,放结构体**首位**
|
||||
Description: "……",
|
||||
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||
Handler: h.mytool,
|
||||
NoMemory: true, // 声明字段一律放**末尾**
|
||||
}, s)
|
||||
```
|
||||
|
||||
**并发声明**(同一批多个 tool_call 时):
|
||||
|
||||
- `ParallelSafe: true` —— 可并发。**三个条件都满足才可声明**:handler 线程安全 /
|
||||
不与同批工具争抢同一资源(SQLite 写、同一设备、同一输出通道)/ 执行顺序无关。
|
||||
- `Serial: true` —— 必须串行,**优先级高于 `ParallelSafe`**。
|
||||
- **默认(都不写)= 整批串行**,是保守设计不是遗漏。
|
||||
|
||||
⇒ commondev 的**写操作**(凭据增删、台账写、`workdir_prepare`、`pr_open/merge`、
|
||||
`agent_send`)**一律 `Serial: true`**;**只读**(list/status)才 `ParallelSafe`。
|
||||
|
||||
### 36. ★ 外部插件的能力边界(选外部形态就必须接受)
|
||||
|
||||
**公开 SDK 里声明的符号,不等于外部插件拿得到。** 外部插件跑在独立进程里,能力由桥接注入:
|
||||
|
||||
| API | 外部插件的实际 | 该用什么 |
|
||||
|---|---|---|
|
||||
| `Events()` | **恒为 nil**(桥不注入 subscriber) | 桥接运行时按你的声明完成订阅 |
|
||||
| `UnregisterOutputChannel` | **静默无效**(只注入 registrar,不注入 unregistrar) | 注销需重载插件 |
|
||||
| `PriorityL4` | **被夹到 L3** | 用 L1–L3 |
|
||||
| `SocialAPI` 写操作 | 只有 6 个**只读**方法 | 读用 `GetPerson` 等 |
|
||||
| `PluginMgr()` | **只有 3 个方法**(`ReloadOne`/`ListLoadedPlugins`/`IsPluginDisabled`) | 就这 3 个 |
|
||||
| `resident_agents` 等内核工具 | **调不到**(proc 桥无对应方法) | 由 agent 侧调(见第 3 节) |
|
||||
|
||||
> **外部形态的 L4 天花板**:commondev 作为外部插件**永远拿不到 L4**。
|
||||
|
||||
> **⚠️ 不要用 `ToolAPI` 去调内核内置工具(如 `resident_agents`)**:`ToolAPI` 是
|
||||
> **单例**,per-agent 数据靠「最后注入者」,而创建驻留子也会注入 ⇒ 你可能打到
|
||||
> **某个驻留子的表上**。这是内核已自陈的局限,别踩。
|
||||
|
||||
**L4 归属表**(这块最容易搞错):
|
||||
|
||||
| agent | 它的"内核" | 谁能产生 L4 |
|
||||
|---|---|---|
|
||||
| 根 agent | 内核自身 + **内置**插件 | `raiseKernelInterrupt`、WebUI 终止按钮等 |
|
||||
| 驻留子 | **父 agent** | 父(父→子消息固定 L4) |
|
||||
| **外部插件(如 commondev)** | — | **永远不能**(被夹到 L3) |
|
||||
|
||||
⇒ 外部形态下,「立即取消整条分支」只能靠 **L3 打断主管 + 主管下令**。
|
||||
|
||||
### 37. 安装与验证
|
||||
|
||||
```bash
|
||||
# ① 从 bundle 取出本机平台二进制
|
||||
unzip -o dist/<name>_bundle.hmap 'plugin.bin.linux.amd64'
|
||||
# ② 放进插件目录(<name> 必须与 plg.json 的 name 一致)
|
||||
mkdir -p /home/newqqagent/plugins/<name>
|
||||
cp plugin.json /home/newqqagent/plugins/<name>/
|
||||
cp plugin.bin.linux.amd64 /home/newqqagent/plugins/<name>/plugin.bin
|
||||
chmod +x /home/newqqagent/plugins/<name>/plugin.bin
|
||||
# ③ 重启并确认工具**真的注册了**
|
||||
systemctl restart homeagent.service
|
||||
journalctl -u homeagent.service --since "-2 min" | grep "registering tool: <你的工具名>"
|
||||
```
|
||||
|
||||
**内置 vs 独立**(目录存在 ≠ 有 `plugin.bin`):
|
||||
|
||||
```bash
|
||||
for p in webui cmd seq qq; do
|
||||
printf "%-8s " $p
|
||||
ls /home/newqqagent/plugins/$p/plugin.bin >/dev/null 2>&1 \
|
||||
&& echo "独立(要单独构建部署)" || echo "内置(随 homed 部署)"
|
||||
done
|
||||
```
|
||||
|
||||
★ **改了内置插件的源码 ⇒ 必须重新构建并部署 homed**,重启服务不生效(`//go:embed` 编进二进制)。
|
||||
★ 判断线上跑的是哪次构建,看 `commit` 字段(**不要用 `strings`**):
|
||||
|
||||
```bash
|
||||
K=$(sqlite3 /home/newqqagent/config.db "select value from config_webui where key='api_key';")
|
||||
curl -s -H "X-API-Key: $K" http://127.0.0.1:8080/api/v1/status | grep -oE '"commit":"[^"]*"'
|
||||
```
|
||||
|
||||
⚠ 必须带 `X-API-Key`:无认证时返回 **200 + 登录页 HTML**,只看状态码会误判。
|
||||
|
||||
### 38. 排查「插件装了却不生效」
|
||||
|
||||
按顺序查,**别跳步**:
|
||||
|
||||
```bash
|
||||
ls /home/newqqagent/plugins/<name>/plugin.bin # 1) 内置还是独立
|
||||
journalctl -u homeagent.service --since "-5 min" | grep "registering tool:" | grep -i <name> # 2) 注册了吗
|
||||
journalctl -u homeagent.service --since "-5 min" | grep -E "loaded: <name>" # 3) 加载了吗
|
||||
journalctl -u homeagent.service --since "-5 min" | grep -iE "<name>.*(error|panic|failed)" # 4) 启动报错
|
||||
```
|
||||
|
||||
### 39. ★ 安全纪律(commondev 拿的是**能 push 用户所有仓库**的权限)
|
||||
|
||||
> **★ 架构决定(用户拍板 2026-10-14):敏感信息拦截下沉为内核安全模块。**
|
||||
>
|
||||
> 理由:**LLM 调用本身不可信**,而插件在**信任边界之外** ——
|
||||
> 插件注册的 stage 拦截是“自愿的”,不是强制的,也算不上边界。
|
||||
>
|
||||
> ★ **它不是单纯拦截,而是一个闭环**(用户纠正:“只是简单的拦截的话,
|
||||
> 绝大部分插件会失去原本的功能”):
|
||||
>
|
||||
> ```
|
||||
> ① 拦截 LLM 出向前扫到明文 → 换成 @{别名}
|
||||
> ② 加密暫存 明文 AES-GCM **落盘**存内核(不落明文)
|
||||
> ③ 别名 ★ **哈希键**:sec_ + HMAC-SHA256(masterKey, plain)[:8]
|
||||
> ⇒ **同一明文自动得同一个别名**(同变量合并,天然性质)
|
||||
> ⇒ 必须 HMAC 而非裸哈希(裸哈希可字典枚举);**不加 salt**(否则失去合并)
|
||||
> ④ 回填 插件调用前把 @{别名} 换回明文 ← 插件无感知
|
||||
> ```
|
||||
>
|
||||
> ★ **变量不自动淘汰**(跨轮/跨子 agent 随时要用);agent 可**主动**调用
|
||||
> 安全模块提供的**提交/删除**工具。泄漏风险靠主动删控制,不靠超时。
|
||||
>
|
||||
> ★ **因为内核零 IO(一切外部交互都从插件走),只需两根咽喉**:
|
||||
> · 入向 toolcall:`StageHost.ExecuteTool(name, args)`(`core/stages.go:140`)
|
||||
> · 入向 outputch:设备 `Execute`(`core/output.go:76`)
|
||||
> · 出向 LLM:provider 层的 `Chat(req)`(已核:构造 `CompletionRequest` 有 **7 处**,
|
||||
> task 层拦不全)
|
||||
>
|
||||
> ★ **变量系统是全局唯一的**(用户拍板):主 agent / 子 agent / 驻留子 agent
|
||||
> **同一套**别名表。已核依据:驻留子共享父的 `ProviderManager`(`resident.go:185`),
|
||||
> 而它**全进程只建一份**(`bootstrap.go:249`)⇒ 安全模块天然是进程级单例,
|
||||
> **不需要跨 agent 同步**。
|
||||
> ⚠ **变量共享 ≠ 授权共享**:能不能调某工具/通道仍看该 agent 自己的 `AllowedOutputs`
|
||||
> (与 `KernelSource` 一道是 per-agent 的)。两者**正交**。
|
||||
>
|
||||
> ★ **“插件无感知”是硬要求**:替换发生在路由到插件的**之前**;
|
||||
> 插件代码一行不改、不可分辨、无法绕过 ⇒ 存量插件自动获得该能力,
|
||||
> 且无需给 SDK 加参数(不破接口兼容)。
|
||||
>
|
||||
> **对本插件的直接影响(大幅简化)**:插件可以**把密钥明文直接投给 LLM**,
|
||||
> 由内核安全模块全权负责;`enc_*` 工具族与加密 shell 应**删减**,
|
||||
> 不再自带加密库/别名表(前稿那套作废)。
|
||||
>
|
||||
> 详见 `docs/zh/kernel-secret-firewall-design.md`(含硬约束 S1–S9)。
|
||||
>
|
||||
> **两条不可让步的约束**:
|
||||
> · **只能替代引用,永远不可取回明文**(用户明确)—— 读接口与用接口是两件事;
|
||||
> · **模式库宁漏勿误伤** —— 误报会搞坏正常任务。
|
||||
|
||||
**凭据:按名字引用 + 变量填充**(用户明确要求的能力)
|
||||
|
||||
```
|
||||
enc_store(alias="gitea-token", value="${GITEA_TOKEN}") ← ★ 引用,不是明文
|
||||
enc_list ← 只列名字,**绝不返回值**
|
||||
```
|
||||
|
||||
之后一切操作只带**别名**:`@{gitea-token}` —— 参数里永远不出现 secret。
|
||||
详见 **第二部分 §13**(那里是权威)。
|
||||
|
||||
**为何要 `${ENV_VAR}` 变量填充**:(①)明文不落盘;(②)dev/prod 换环境变量即可;
|
||||
(③)轮换凭据不需改插件数据。
|
||||
|
||||
#### ★★ 判据与复核:**插件 GREEN ≠ 改对了**
|
||||
|
||||
**这是本流水线最重要的一条纪律。** 插件只能跑你**预先写下**的判据;
|
||||
判据没覆盖那条 bug,插件照样打勾。自动化**不会**发现假绿,
|
||||
只会让假绿跑得更快、更可信。
|
||||
|
||||
**真实数据**(我用最小例子复刻过):
|
||||
|
||||
```go
|
||||
func Max(a,b int) int { if a>b { return a }; return b }
|
||||
```
|
||||
- 判据只测 `Max(5,3) == 5`(漏了 `b>a`)
|
||||
- 变异 `return b` → `return a`(**真 bug**)⇒ **判据仍全绿**
|
||||
- 补一条 `Max(3,5) == 5` ⇒ 同一变异**立刻报红**
|
||||
|
||||
⇒ 所以**复核必须两步**,你定义判据时**必须同时给变异规则**:
|
||||
|
||||
```
|
||||
commondev_test_define(
|
||||
project="MyProj", name="clamp 上下界", kind="unit",
|
||||
script="go test -count=1 ./...",
|
||||
expect="上下界都被限制",
|
||||
mutations=[{ id="M1", file="calc.go",
|
||||
old=" if v > hi {\n return hi\n }\n", new="",
|
||||
note="删掉上界分支(真 bug)——松判据看不见它" }]
|
||||
)
|
||||
commondev_review_submit(branch_id="br-...")
|
||||
commondev_review_result() ← 取结论
|
||||
```
|
||||
|
||||
**结论三档,注意第二档不是"通过"**:
|
||||
|
||||
| verdict | 含义 | 你该做什么 |
|
||||
|---|---|---|
|
||||
| `pass` | 正向全过 **且**全部变异被咬住 | 可以进下一步 |
|
||||
| **`pass_with_gaps`** | 正向过,但**有变异没被咬住**或**判据没变异规则** | **不能当通过** —— 去补判据/变异,再复核 |
|
||||
| `fail` | 有判据没过 | 打回执行 agent |
|
||||
|
||||
★ **`pass_with_gaps` 就是"你的判据有盲区"**。把它当绿色勾,
|
||||
就是在给自己攒一次大的意外。
|
||||
|
||||
**定义变异规则的三个要点**:
|
||||
|
||||
1. **`old` 必须在文件里恰好出现一次** ——
|
||||
出现 0 次 ⇒ 变异无效(却会被误报成"判据太松");
|
||||
出现多次 ⇒ 可能改错地方(却会被误报成"咬住了")。
|
||||
多出现时给出更长的上下文。
|
||||
2. **变异要"真坏"** —— 改的是逻辑,不是格式。改不坏就验不出判据的松紧。
|
||||
3. **`script` 里带 `-count=1`** —— Go 默认缓存测试结果;
|
||||
命中缓存时测试**根本没跑**,"变异后报红"就无从谈起。
|
||||
|
||||
#### 双凭据与分支保护:**已移到第二部分**(技能纪律 §15–§16)
|
||||
|
||||
原来这里用 `commondev_account_add(role=...)` + `commondev_protect_branch` 两个
|
||||
插件工具实现。**这些工具已移除**(平台知识不再进插件)。
|
||||
|
||||
现在这样做(命令见 **第二部分 §15**):
|
||||
|
||||
1. **在平台上建两个用户**:一个普通用户(执行侧,只能 push `feat/*`)、
|
||||
一个有管理权限的(主管侧,能审批与合并)。各自签发 token。
|
||||
2. **两个 token 都存进凭据库**:`enc_store(alias="gitea-exec", ...)` /
|
||||
`enc_store(alias="gitea-sup", ...)`。
|
||||
3. **给主干上锁**(第二部分 §15 的 curl 命令,四个开关都要)。
|
||||
4. 审批与合并用主管侧别名(§16)。
|
||||
|
||||
★ **执行侧必须用普通用户 token** —— 用 admin token 会**直接绕过分支保护**,
|
||||
机制失效且无任何报错。这是最容易搞错的一点。
|
||||
|
||||
★ **拦你的是平台,不是插件** —— 所以改插件代码也绕不过去。
|
||||
这才是「机制」与「流程约定」的区别。
|
||||
|
||||
**两条不可逆的红线**:
|
||||
|
||||
1. **变量解析不到值 ⇒ 响亮报错**,绝不静默当空串 —— 后者会退化成匿名访问,
|
||||
表现成 **401** 而非“凭据没配”,极难排查。
|
||||
2. **secret 绝不进返回值/日志**。
|
||||
|
||||
**平台形状差异**(不能一个认证头走天下):
|
||||
|
||||
| 平台 | API 前缀 | 认证头 |
|
||||
|---|---|---|
|
||||
| Gitea | `<base>/api/v1` | `Authorization: token <T>` ← **不是 Bearer** |
|
||||
| GitHub | `https://api.github.com` | `Authorization: Bearer <T>` |
|
||||
| GitCode | `https://api.gitcode.com/api/v5` | `Authorization: Bearer <T>` |
|
||||
|
||||
未知平台**必须报错**(不猜形状)。
|
||||
|
||||
**其他:**
|
||||
|
||||
1. **凭据不入 `config.db`**:现有惯例是明文存 `config_<plugin>` 表,而 `config.db` 是
|
||||
**644 全局可读**。commondev 的凭据性质完全不同 ⇒ 放独立文件
|
||||
`<DataDir>/credentials.json`,**0600,启动时校验权限,不对就拒绝启动**
|
||||
(本仓纪律:响亮失败,不静默降级)。
|
||||
2. **优先 SSH key 而非 PAT**;每平台独立、最小授权,**不要一个 token 通吃**。
|
||||
3. **token 不进 remote URL**:拼接仅用于**单次命令**(`pushURLWithToken`),
|
||||
clone 出来的 remote 不带凭据 —— 否则删掉临时目录也删不掉凭据。
|
||||
4. **不要硬编码 token**:走 `Settings()` 或宿主注入。
|
||||
5. **拉起 pi 时隔离端口**:内层 pi 会尝试监听 A2A/ACP 端口并打 `EADDRINUSE`,
|
||||
显式设 `PI_CODING_AGENT_SESSION_DIR` / `PI_A2A_PORT` / `PI_ACP_PORT`。
|
||||
|
||||
### 40. ★ 孤儿清理(插件有自己的硬约束)
|
||||
|
||||
内核对自己的驻留子有硬约束:**子不得比父活得久 / 无孤儿 goroutine / 无残留 temp**
|
||||
(`resident-subagent-design.md` §10,写成判据)。
|
||||
|
||||
**插件对自己拉起的进程必须同样硬**:`Stop()` 必须是**幂等的全清** ——
|
||||
pi 进程、sidecar、ssh 会话、workdir(含凭据残留检查)一个不留。
|
||||
写成判据:`TestStopKillsAllRuntimes`(变异:删掉某个清理 → 必须报红)。
|
||||
|
||||
### 41. 参考资料(**别凭记忆写 API**)
|
||||
|
||||
| 内容 | 路径 |
|
||||
|---|---|
|
||||
| SDK 源码 | `third_party/homeagent-sdk/sdk/` |
|
||||
| **可运行的示例插件** | `third_party/homeagent-sdk/example/`(a2a / acp / qq / plugindev…) |
|
||||
| 能力边界 | `third_party/homeagent-sdk/docs/guide/capability-boundary.md` |
|
||||
| 并发声明 | `third_party/homeagent-sdk/docs/guide/parallel-tool-declaration.md` |
|
||||
| 部署手册 | `docs/zh/deploy-runbook.md` |
|
||||
| sidecar 参考实现 | `internal/plugins/clawhubadapter/sidecar.go` |
|
||||
|
||||
★ **写插件前先翻 `example/`**:那里的代码是**编译通过**的,比文档可靠。
|
||||
**文档与示例冲突时以示例为准。** 注意 `example/` 是**外部插件**,
|
||||
能被它们编译通过的写法,外部就一定可用。
|
||||
|
||||
---
|
||||
|
||||
## Notes: 红牌(做了就是错)
|
||||
|
||||
1. **不划通道就当派了活** —— 登记 ≠ 划入;不划则所有输入涌向根 agent。
|
||||
2. **绕过插件直接敲 `pi -p`** —— 漏 `--session-id` 会静默开新会话。
|
||||
3. **并发投同一个 session** —— 会写坏会话(实测)。
|
||||
4. **没有变异阶段的复核** —— 等于给假绿充值。
|
||||
5. **凭据写进 `config.db` 或 remote URL**。
|
||||
6. **改了内置插件源码只重启服务** —— 必须重编 homed。
|
||||
7. **手改生成的文档**(`docs/api/*.md` 首行写着「请勿手改」)—— 改 `sdk/*.go` 注释再重新生成。
|
||||
8. **为了让插件生效去改 homed 的源码** —— 先确认它是不是内置插件。
|
||||
6
example/commondev/skill/commondev-discipline/skill.json
Normal file
6
example/commondev/skill/commondev-discipline/skill.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "commondev-discipline",
|
||||
"description": "commondev 主管式多仓库开发流水线的使用与开发纪律:为每个工程对话新建驻留子并划入其通道、投递必须落同一 session、复核必须含变异阶段;以及 hmap 插件开发纪律(plg.json、SDK 版本、产物形态、Serial/ParallelSafe、外部插件能力边界、凭据与孤儿清理)",
|
||||
"version": "1.0.0",
|
||||
"author": "HomeAgent"
|
||||
}
|
||||
Reference in New Issue
Block a user