|
|
c6c39aa2a8
|
feat(qq): Qzone 说说、群发硬拦截、@全体不算@bot、状态语义修正
## 1. Qzone 说说(send_qzone_msg / delete_qzone_msg)
一次真实的误判,值得记下来:我曾断言「生产 NapCat 不支持发说说」,
依据是探测返回「不支持的Api SendQzoneMsg」——**是我的调用名错了**。
router.ts 里声明的是下划线式 `send_qzone_msg`,我用的是源码**类名**
`SendQzoneMsg`。换对名字后一次成功。
教训:「API 不存在」这个结论必须用真实 action 名验证;源码里的
类名/文件名/方法名都不是调用名。判据 TestQzoneActionNamesAreSnakeCase
把这条钉住了。
本地校验 ugc_right 白名单(1/4/16/64/128)与 16/128 必带 target_uins,
省一次跨进程往返、给模型更直接的反馈。
## 2. 内部委派不得向群聊发送(现场事故)
offload 子 agent 的「分诊汇报」把本该私聊给用户的进度播报**发到了群里**:
17:15:41 executing tool: output_send__qq
17:15:41 handleChannelOutput meta={"group_id": 979911915}
17:15:42 群友质问「又没@你,你就这么确定是找你的?」
根因:子 agent 被授予全部输出通道(AllowedOutputs=nil,用户明确要求
"持有全部输出通道"),于是它能自己选目标——而它只从上下文里看到一个
群号,就选了群。
修法:`beforeToolcall` 在 internal 放行前调用 outputSendGroupTarget,
识别出群目标就写 denial。为什么不放在 handleChannelOutput:ToolHandler
只收 args,拿不到帧上的 comefrom(SDK 契约如此),只有 stage 帧能看到。
## 3. @全体成员不算 @bot
原实现 `if v == botIDStr || v == "all" { return true }` —— 任何人
@全体都会拉起 bot。那不是"在叫 bot",是"在叫所有人":一个是针对性请求,
一个是广播,语义完全不同。
群里误响应的代价不是"多回一句",而是**在未被邀请的场合发言**。
宁可漏响应(用户会再 @ 一次),不能误响应。
## 4. 状态语义按用户定义重排
学习中 = 等待 LLM 搬砖中 = 执行 toolcall 在线 = 空闲
pre_action → 学习中(stepLLM 之前,唯一能提前标记"要等模型"处)
before_toolcall → 搬砖中
after_toolcall → 学习中(工具回来了,又要等 LLM)
before_output → 不改(LLM 已回、即将输出,不是等模型)
after_output → 在线(force,绕防抖)
原先「学习中」挂在 before_output 是错的:那个时刻 LLM 已经返回。
## 5. 「对方正在输入中」按轮驱动
起点从 HTTP 入口(消息刚到)改到 before_output(每轮只触发一次),
终点从 output_send(任何发送都停,含群聊/发给别人)改到 after_output。
身份由 InjectOptions.Payload 携带 `qq_uid`,内核透传到 stageCtx.Extra。
不用全局 p.auth:并发私聊会串号(A 的指示打到 B 身上)。
## 6. 启动日志不再硬编码工具数
`log.Printf("... (%d tools)", p.name, 15)` 硬编码了 15,工具加到 24 后
仍报 15 —— 排查"qzone 装上没"时被误导了好几轮。改成真实计数 p.toolCount。
## 判据(新增 13 条,变异 8/8 咬)
Qzone 5 条、群发拦截 5 条、@门 4 条、状态映射 7 条、打字 7 条。
★ 两个判据自身缺陷被变异抓出并修掉:
· @门判据用字符串找 `}\n\t}` 截分支,而 joined 已 strip 缩进 ⇒ 找不到
⇒ 分支延伸到文件末尾 ⇒ "文件里任何别处的 return"都让它通过。
改用括号计数 ifBlockAt。
· 状态判据只查常量名,`_ = qqStatusStudy` 就蒙混过关。改查真实调用
applyStatus(qqStatusStudy)。
|
2026-10-18 17:39:40 +08:00 |
|
|
|
4a9705ca41
|
feat(hmapdev): 构建期告警「同一 stage 被重复注册」+ qq 合并写法
## 用户指出的关键:告警要给**开发者**看,所以必须在构建期
我第一版把告警放进了工具链生成的 proc_main 模板(插件**运行期**打日志)。
用户直接指出:「本身工具链这个编译器警告就是给开发者看的」——运行期日志
是给运维/用户看的,开发者看不到。已撤销,改为构建期静态检查。
## 缺陷(真实事故,2026-10-18)
插件侧把 stage handler 存在 `map[string]sdk.StageHandler`(stage 名为键)⇒
**同一 stage 注册两次,后者静默覆盖前者**。
QQ 插件当时写成:
RegisterStage(BeforeToolcall, 权限门) ← 被下一条覆盖
RegisterStage(BeforeToolcall, 切状态) ← 实际生效
后果不只是状态不切换 —— **权限门被静默关掉了**(那几版部署里任何人
发消息都能调任何工具)。编译器不报、运行时不报错、内核日志还显示
`2 handlers [qq qq]`(看着完全正常)。只有静态检查能发现。
## 实现
· `stage_dup_check.go`:扫构建目录下的 .go(排除 _test.go 与 z_proc*),
统计每个 stage 常量被 RegisterStage 引用几次,>1 就告警
· 接入 `buildTarget` 开头 ⇒ 所有 target(含 lua)都检查
· **只告警不中断**:存量插件里重复注册可能是本意,直接 fail 会让它们
升 SDK 后无法构建
· `example/qq`:三个重复 stage 合并成单 handler(权限门 + 状态切换)
## 判据(4 条)
· 同 stage 两次 ⇒ 报出来
· 不同 stage 各一次 ⇒ 不误报
· **注释里的示例注册不计入**
· 测试文件 / 工具链生成文件不计入
★ 「注释」这条是这个检查器**第一版自己栽的坑**:直接对全文跑正则,
把插件里「当初写错了、留在注释里当教训」的那几行也统计进去,于是
修好之后照样报「注册了 3 次」(实测)。现在逐行跳 `//` 与跨行 `/* */`。
本项目已多次绊倒在「注释里有、代码里没有」上,这次轮到检查器自己。
## 实测
qq 修好后构建: packaged qq_linux_amd64.hmap(无告警)
故意加一条重复: ⚠️ 警告: stage StageBeforeOutput 注册了 2 次
建议:把多个功能合并进同一个 handler
|
2026-10-18 15:38:52 +08:00 |
|
|
|
678b9cf90c
|
feat(hmapdev): 同 stage 多 handler 默认合并 + 注册期告警
## 修的是真实事故(2026-10-18)
模板里原来是:
stageHandlers[string(stage)] = handler // stage 名为键,后者覆盖前者
有人写了两次同 stage 注册:
RegisterStage(BeforeToolcall, 权限门) ← 被下一条覆盖
RegisterStage(BeforeToolcall, 切状态) ← 实际生效
后果不只是状态不切换 —— **权限门被静默关掉了**(那几版 QQ 插件里
任何人发消息都能调任何工具)。而内核侧日志显示 `2 handlers [qq qq]`,
看起来完全正常 —— 这是最难查的一类 bug,我为它白烧了十几轮。
## 处置(按用户要求:告警 + 默认合并)
禁止重复 → 存量插件可能直接起不来(它们的重复常是本意)
静默覆盖 → 就是当前 bug
默认合并 → 两件事都做,且告警让开发者知道该优化写法 ← 选这个
合并语义三条:
1. **按注册顺序链式执行**(先注册的先跑)
2. **prev 返回 error 就中止**(与内核 RunStage 一致:权限门拒了
就不该再切状态)
3. **留痕**:注册期打 log.Printf 告警,开发者在插件启动时就能看见
★ 重复注册**不再**上报 stage.register:内核已经登记过一条,再报一条
会让它记两条 handler 而实际只派发一次 —— 那正是
`2 handlers [qq qq]` 假象的来源。
## 为什么改模板而不是 SDK
用户明确要求「工具链对同stage多handler注册给出告警」。模板是生成
proc_main 的地方,也就是 map 覆盖实际发生的地方;在源头拦,告警
直接出现在**插件启动日志**里。
## 判据(5 条,变异 3/3 咬)
· 有"已存在"判断(不是无条件赋值)
· 链式调用 prev(合并后旧 handler 不丢)
· prev 出错中止
· **告警是代码行**(跳过注释再找)+ 给出行动建议
· `if dup` 分支内不含 stage.register
## 判据第一版是假绿
裸 `strings.Contains(body, "重复注册")` 把**注释里的解释**也算进去了,
于是删掉 log.Printf 后判据照样通过(变异不咬,实测)。
改成逐行跳过注释再找 log.Printf。这不是第一次栽在
「注释里有、代码里没有」上。
|
2026-10-18 15:28:07 +08:00 |
|
|
|
61a871cf88
|
feat(qq): 在线状态随内核阶段自动切换 + 手动设置工具
## 用户要的两件事
1. 「在不同内核工作阶段自动切换 QQ 状态,比如 toolcall 阶段切忙碌中、
生成文字阶段切思考中」
2. 失败不阻塞、不重试、只写日志(完全非致命错误)
## 取值以 NapCat 源码为准(不是我猜的)
我最初拿 `status=11` 试探,NapCat 返回 retcode=0 **但那是无效状态**
(它不校验)。拉了源码 `SetOnlineStatus.ts` 的 actionDescription 才拿到
权威取值表:
在线 status=10 ext=0 忙碌 status=50 ext=0
离开 status=30 ext=0 请勿打扰 status=70 ext=0
隐身 status=40 ext=0 搬砖中 status=10 ext=2023
学习中 status=10 ext=1018 听歌中 status=10 ext=1028
## 阶段映射
StageBeforeToolcall → 搬砖中(status=10, ext=2023)
StageBeforeOutput → 学习中(status=10, ext=1018)★ QQ 无"思考中"预置项
StageAfterOutput → 在线 (status=10, ext=0)
`after_toolcall` 不切:工具刚结束还要继续,频繁切换只会闪。
## 失败处理(用户明确要求的设计)
三个钩子**永远 return nil**,不重试:
· 不返回 error ⇒ 内核不会因状态失败而中断工具调用或改写回复
· 不重试 ⇒ NapCat 掉线时不会每轮打 HTTP + 打日志
· 失败只在 applyStatus 记日志(首次 + 每 10 次),不刷屏
## 其它
· 默认**关闭**(`status_auto`):改 QQ 状态对外可见,属于"内核替用户
做决定",与 offload 同类的显式特权。
· 手动工具两个:`qq_set_status`(预置状态,收中英文名)、
`qq_set_diy_status`(自定义 face_id + wording)。
· 防抖:同状态跳过、最短间隔 2s —— stage 每轮都触发,不防抖会打满 NapCat。
· `Stop()` 里恢复在线:否则停机时停在"搬砖中",用户看到一个永久错位
且不知道是谁改的状态。★ 恢复判据用**实际应用过什么**(applied),
不能用 status_auto —— 手动工具设的状态在开关关闭时也要能恢复。
## 判据(5 条,变异 2/2 咬)
· 指向必拒绝的地址时三个钩子仍返回 nil(不阻塞)
· 开关关闭时完全不发请求
· 状态名归一化(含中文别名)
· 阶段映射用**权威取值**(忙碌必须是 50,不是 11)
· 未知状态名被拒且不发请求
## 实机验证(生产 NapCat 192.168.2.106:25570)
qq_set_status busy → map[ext_status:0 status:忙碌 status_num:50]
qq_set_status working → map[ext_status:2023 status:搬砖中 status_num:10]
qq_set_status online → map[ext_status:0 status:在线 status_num:10]
qq_set_status 乱写的 → 未知状态名: 乱写的(拒绝,未发请求)
注:`get_status` 只返回 online/good,不回显具体状态 —— 那是该接口的
特性,不是设置失败。
|
2026-10-18 14:34:17 +08:00 |
|
|
|
74e2c654ed
|
feat(sdk): 22 个示例插件配齐 skill + 三态启动声明 + 打包期校验
## 用户的三条要求
1. 「所有插件都默认自启会占启动时间,还在 llms 上下文塞大量工具」
⇒ 纯工具型改为按需加载,靠 skill 让 agent 知道要先拉起。
2. 「会向 agent 注入输入的部分插件不能这样做」
⇒ 9 个必须常驻,其余 13 个按需。
3. 「sdk 是不是没有暴露插件声明自身行为的方案」
⇒ 对:auto_start 只在 internal/plugin/manifest.go 有,SDK 零命中。
## 定级依据(代码证据,不是猜描述)
注入 / 钩子 ⇒ 必须常驻(9 个):
qq HTTP 监听 NapCat 推送,消息到达要立刻注入
rss 轮询订阅源,新文章主动推送
calendar 30s ticker 检查提醒
memo 5min ticker 检查待办到期
a2a TCP 监听外部 Agent 请求
acp TCP 监听 ACP 客户端
browser ticker + 长命令完成回调
sanitizer 注册 stage 钩子(无工具,要每轮生效)
luademo 注册 stage 钩子做示范
纯工具型 ⇒ 按需(13 个):
weather ocr music bili ai_image files editdoc deepsearch
plugindev recoverydiag vanblog vikunja commondev
★ commondev 值得单独说:它虽然有 6 处 Inject 调用,但**注入源是自己的
sidecar 完成回投**,而 sidecar 只在 commondev_agent_up 时才拉起
(Start() 里不启 sidecar,通道也在 agent_up 时才注册)。所以没有 agent
调用它时,它不需要做任何事 —— 可以按需。44 个工具 ≈726 tokens 省下来可观。
## 改动
· 22 个插件各配一个 skill,写明:何时用、先 plugin_start、
工具清单、**用 plugin_stop 而不是 plugin_disable**
· 13 个 plg.json 加 `auto_start: false`
· `sdk/autostart.go`:把三态语义与"按需插件必须有 skill"的理由写进公开 SDK
· hmapdev 打包期校验:`auto_start=false` 但无 skill/ 目录 ⇒ 直接 exit 1
(理由:缺 skill 的按需插件**等价于没装**,比没装更糟 —— 占着磁盘让人以为可用)
· 修 Lua 打包路径:它**完全不走** rawfile/skill 收集,luademo 的 skill 永远打不进去
· 修 build-examples.sh:原来 `[ ! -x ]` 判"二进制在不在",改了 hmapdev 源码后
重新打包**仍用旧工具链**(实测:luademo 的 skill 一直是 0)
· 补 mocksdk.ToolDef.Timeout(上一轮加 ToolDef.Timeout 时漏了镜像)
## 判据
· TestHasSkillDirRequiresRealSkillMd(空目录/.gitkeep/无 SKILL.md 都不算)
· TestBuildRejectsOnDemandWithoutSkill(打包期必须真拦住 + exit 1)
· TestLuaPackagingCollectsSkillAndRawfiles(**验代码行**,不验注释 —— 第一版
只查符号存在,把 createHmapWithRaw 换回 createHmap 判据照样通过)
## 踩过的坑
· Lua 分支的结束边界原是 `// Resolve build config`,但那个注释在文件里
**出现多次**(Go 路径里也有),用它截取会把 Go 分支一起包进来 ⇒
变异测不出。改成截到 Lua 分支自己的 `return` + `}`。
|
2026-10-18 13:21:47 +08:00 |
|
|
|
e1b458a710
|
fix(commondev): interrupt 的"是否真中断"判定改用 pi 的 isIdle + 真机 e2e
## 找不到的缺陷(变异测出来的)
静态判据全绿、真机也报 interrupted=true —— 但我把 sidecar 里的
`await session.abort()` **删掉**后,判据照样通过。原因:
const hadInflight = inflight > 0;
...
interrupted: hadInflight, // ← 只看"中断前有没有在跑"
任务还在跑 ⇒ inflight 仍 >0 ⇒ 照样回报"已中断"。**那正是最坏的情形:
用户以为停了,任务还在改代码、推分支。**
## 第二版又反向错了
改用"中断前后 inflight 的变化":
interrupted: before > 0 && inflight < before
实测 before=1 / inflight=1 ⇒ 误报"未中断",而任务其实已经停了。
原因:inflight 是**包装层**计数,只在后台 promise 的 finally 里递减,
而 abort() 返回时那个 promise 还没收尾。
反向的谎报同样有害:用户以为没停,重复发一遍任务。
## 最终:用 pi 自己的 isIdle
session.isIdle 是 pi 的权威视图("当前没有在跑的任务")
interrupted: !wasIdle && session.isIdle
实测应答:{"ok":true,"interrupted":true,"was_idle":false,"now_idle":true}
## 真机 e2e(3 条,HA_PI_E2E / HA_ZCODE_E2E 控制)
· TestRealPiInterruptStopsRunningTask
真 pi 会话跑 60 步任务,12s 后中断 → interrupted=true,且会话仍可用
· TestRealPiSameTaskWithoutInterruptStillRunning ★对照组
同样任务不中断,15s 时仍在跑 ⇒ 证明"已中断"不是"任务碰巧跑完了"
实测不中断时 >100s 仍未完成
· TestRealZcodeInterruptKillsChildAndClearsHandle
假 CLI(持续输出的 node 脚本)→ interrupt 真的 killed_pid,
第二次 interrupt 回 no_running_turn(句柄已清,不误杀下一轮)
## 变异(这次全咬)
删掉 await session.abort() → 咬(now_idle=false 暴露任务仍在跑)
把 interrupted 改回 hadInflight(代码行) → 咬
★ 静态判据里查 hadInflight 时**只查代码行、跳过注释** —— 全文 Contains
会把解释性注释当成缺陷(实测误报一次)。
## 顺带
两份 pi sidecar(工程根 = 单测/SSH e2e 用,rawfile/ = 部署用)必须
逐字一致:既有判据 TestSidecarScriptShipsWithPackage 会比对。
|
2026-10-18 10:52:02 +08:00 |
|
|
|
330c0c9246
|
test(commondev): 守住 rawfile/ 不进库却决定包能否运行的隐患
## 背景
`example/commondev/rawfile/.gitignore` 写着 `*`:rawfile 里是随包资源
(可能是上百兆 node/pi 二进制),进版本库会让克隆变慢、每次升级产生
巨型 diff。**这是设计意图**,不是疏忽。
但代价很具体:
本地打包 → 包里 571 个 runtime 条目(工作区已备好资源)→ 冒烟通过
CI 打包 → 包里 0 个 runtime 条目(干净克隆没有资源) → 插件起不来
而两次构建的**日志都写着"成功"**。这类"本地绿、别处坏"我们吃过一次:
TestHmapdevTemplateImplementsAllPluginMgrMethods 读了 gitignore 的文件,
本地绿、CI 红。
本次尤其危险:interrupt 的实现**同时**落在 sidecar.mjs(已进库)和
rawfile/runtime/*/sidecar.mjs(未进库)两处。只改前者,判据全绿、
包里的 sidecar 却还是旧的 —— 取消能力在发布版里根本不存在。
## 判据做什么
不能要求 rawfile/ 必须有内容(干净克隆本来就没有)。但可以:
· 有内容时,每个 CLI 目录必须有 sidecar.mjs 或 driver.json
· pi/zcode 的 sidecar **必须**支持 interrupt
· 没有内容时,用 t.Logf **大声说明**"此状态下打出的包不含 sidecar",
让这件事出现在测试输出里,而不是静默
## 变异
删掉 rawfile/runtime/pi/sidecar.mjs 的 interrupt case →
判据报"打包后 commondev_agent_interrupt 会失效(源码改了但资源没同步)"
|
2026-10-18 10:18:40 +08:00 |
|
|
|
28c162cf81
|
feat(commondev): 支持取消正在运行的任务(commondev_agent_interrupt)
## 缺陷(用户实测反馈)
「commondev 无法取消正在运行的 session,agent 反馈它无法取消错误的
pi session,只能等它跑完。」
日志佐证:agent 在 09:07 收到 interrupt 请求后只能如实回答无法取消。
## 根因不是"没实现取消",而是取消能力从来没被暴露
pi 的 AgentSession **本身就有** abort()(agent-session.d.ts:598,
"Abort current operation and wait for agent to become idle")。
但 commondev 注册的 30 个工具里一个 cancel/stop 都没有,
sidecar 也只有 ping/prompt/state/close 四个 method。
这是异步投递(2026-10-17)带来的新缺口:改动之前 prompt 是同步的,
调用方阻塞到任务结束,没有"中途要停"这个需求;改成异步投递后,
任务在后台长跑,用户却失去了停止它的手段。
## 改动
**sidecar 三个平台各有各的取消方式:**
pi → session.abort()(协作式中断,必须 await:不 await 会立刻
回"已中断"而实际还在跑 = 谎报成功)
zcode → 整组杀掉当前回合的子进程(没有 abort API)
spawn 时 detached=true 建了独立进程组,killTree 整组杀
**interrupt 语义如实回报:**
interrupted=true 确实有任务在跑并被终止
interrupted=false 当时没有在跑的任务(不算错误)
ok=false + error 该 CLI 没实现 / 调用失败
**三个收尾分支都清 currentChild**(timeout / error / close):
不清的话 interrupt 会拿到已死的子进程去杀,或更糟 —— 误杀下一轮。
## 判据(4 条 + 「不谎报」)
· pi 的两个 sidecar 都有 interrupt 且用 await session.abort()
· zcode 的 interrupt 真的 killTree 且用 currentChild
· 三个 sidecar 无重复声明(我写的时候真撞了:zcode 已有 currentChild)
· **不谎报成功**:sidecar 没跑 / CLI 没实现时必须 interrupted=false
## 变异验证
pi 删 interrupt case → 咬
pi 去掉 await → 咬("会谎报已中断")
zcode 不 killTree → 咬("只回 ok 不真杀")
## 顺带
既有判据 TestEveryRegisteredToolHasHandler 立刻抓到我加了 RegisterTool
但没在 handlerFor 映射里登记 —— 它比我先发现漏了接线。
|
2026-10-18 10:17:08 +08:00 |
|
|
|
e82d21d633
|
fix(ci): 发版前校验分支名与 meta.Version 一致(防开发阶段误发版)
## 事故(2026-10-17)
`release.yml` 的触发条件是 `push: branches: ['release/**']`,
而 tag 名 = `v` + `meta.Version`。于是:
2026-10-17 08:35 github-actions[bot] 打出 v1.4.0 并发布 Release
而 1.4.0 当时是**开发阶段**。起因只是有人(我)为了走 PR 流程建了一个
release/v1.4.x 分支并推送 —— 那一刻就自动发版了。
更麻烦的是它钉在旧提交 c2474f5 上:之后又合了三个修复
(异步投递、台账回填、示例 go.mod 修复),已发布的 1.4.0 里**没有**
这些修复,而 Release 页面看起来是"最新"。已按要求撤掉该 tag 与 Release。
## 改动
`prepare` 阶段在读出 meta.Version 之后,校验分支名:
release/vX.Y.x → meta 必须是 X.Y.* (中版本长跑线)
release/vX.Y.Z → meta 必须恰好 X.Y.Z
不一致直接 exit 1,并提示"开发阶段请勿推 release/**"。
## 判据(2 条)
· `TestReleaseWorkflowGuardsVersionMismatch`:逐形态断言两个分支形态
都有拒绝逻辑 + 有 exit 1
· `TestNoStaleReleaseTagForDevVersion`:记录当前 meta.Version 是开发版本
★ 判据写砸过一次并修好:第一版用
`if !contains(A) && !contains(B) { fail }`
—— 删掉其中任意一处描述文案,另一处还在,判据仍通过 ⇒ 变异不咬。
改为**逐形态断言**后,两个变异都准确报出是哪个形态缺保护。
|
2026-10-18 08:05:32 +08:00 |
|
|
|
c9b7a6534c
|
fix(commondev): 完成通知回填投递台账(原先永远停在 accepted)
## 缺陷(2026-10-18 本机实例日志实测)
异步投递上线后,台账里的记录全是 accepted、result 永远为空:
07:39:09 accepted result=None
07:40:09 accepted result=None
07:41:18 accepted result=None ← 13 条全是
而回投本身**是正常的**([plugin] 日志有 5 次 `回投任务结果…event=prompt_done`)。
缺的是:完成通知到达后没人把它写回台账。
实测后果(同一天日志,agent 自己起的轮询):
[timer] firing: 【轮询轮次 2/6】…检查是否有新 result 回填…
agent 为此安排了 6 轮 timer 去等一个**永远不会出现**的回填,白等 30 分钟。
更根本的是:看板与任何查询方都拿不到任务结果。
## 修法
`completeDelivery`:把完成/失败结果回填到该 branch 下**最新一条**
未回填的 accepted。`recordDelivery` 改为返回 key(追溯用)。
匹配用"最新未完成"而不是按 key:完成通知里没有 key(sidecar 不知道它),
而同一 branch 的投递是串行投给同一个 pi 会话的,最新未完成那条就是它。
## 判据(6 条)
回填最新 / 不串 branch / 不覆盖已回填 / 无匹配不 panic / 返回 key /
**完成通知真的调用了回填**(接线判据)
## ★ 写判据时踩的两个坑(都记在注释里)
1. **判据没覆盖接线 → 变异不咬**:前 5 条测的是 completeDelivery 自身行为。
把 sidecar.go 里**那一行调用删掉**,它们依然全绿 —— 而"函数写得对但
没人调"正是原始缺陷的形态。补了第 6 条读源码验接线。
2. **偏移错位导致假绿**:分段用 `seg := block[k:]` 后找 "case ",会命中
k 自己(n=0)⇒ seg 不截断、含整个函数剩余部分 ⇒ done 分支删掉回填也照样
"找到" error 分支那一行。改成跳过 `case "x":` 本身的长度再找。
|
2026-10-18 07:53:45 +08:00 |
|
|
|
4387e12ca8
|
fix(commondev): 投递任务改成真异步,不再同步等 pi 跑完
## 缺陷(用户质疑"投递不就是发一条消息"后实测确认)
`deliverToPi` 的注释写着"把给 pi 发消息包装一下",但实际是:
await s.prompt(text) // 同步等 pi 把**整个任务**跑完
而 pi/zcode 开工程线干活(克隆/改代码/跑测试)要几十秒到 20 分钟。
生产日志实测两次:
23:04:03 executing tool: output_send__commondev_br-...
23:05:03 ★ timed out after 60s
通道的语义是"**投递**任务"(见 outputch 描述),不是"等完成"。
把它实现成同步等待,就必然撞上内核的 60s 工具超时。
★ 我一开始的诊断是错的:以为"超时值太小",给 output_send 声明了 3m
—— 那是掩盖症状。投递本该瞬时返回,慢是 bug 不是需求。已撤回。
## 改动
投递 → 立即回 status=accepted(瞬时)
→ pi 在后台跑
→ 跑完推**主动通知**(id=0)
→ Go 侧 onNotify 经 inputch 注入回 agent
`sidecar.go` 的 readLoop 里早就留好了这个扩展位:
if resp.ID == 0 { // 主动通知(暂未使用,保留扩展位)
现在接上了 `makePromptNotifier`。
pi 与 zcode 两份 sidecar.mjs 都改了(rawfile/ 里那两份同步更新)。
## 判据抓到我三次(都是真问题)
1. `TestSidecarScriptShipsWithPackage`:rawfile/ 与工程根不一致
—— 我只改了根那份
2. `TestZcodeMissingCLIErrorIsActionable`:`inflight is not defined`
—— zcode 版用了未声明变量
3. 同一条判据随后**卡死 2 分钟**:CLI 不存在本该立刻报,异步后错误
被吞进后台通知 ⇒ 用户先收到"已受理"、几秒后才知道配置错。
改成**受理阶段就校验 CLI**(配置错是立刻可判定的,不该推迟)。
判据还逼我保住语义:原 `TestZcodeSidecarArgvBehavior` 断言首轮拿到
真实 `sess_` id,异步后要到完成通知里才有 —— 断言保留、改从通知取,
没有削弱。
## 真机 e2e(三条全过,都要求真改工程文件)
TestZcodeFullPipeline 42.1s
TestZcodeOverSSH 72.4s
TestZcodeOverSSHWithPerHostEndpoints 75.1s
受理现在是瞬时的 ⇒ 内核工具不会再 60s 超时。
|
2026-10-18 06:39:24 +08:00 |
|
|
|
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 |
|
|
|
65d2bd9e61
|
fix(qq): NapCat 确认超时提示「不要盲目重发」,版本 1.4.2 → 1.4.3
生产实测(2026-10-13):一次回复发出 4 条相同消息,日志中 3 次
output_send__qq 的调用 id 各不相同 —— 是模型看到「发送失败」后自己重试。
根因(在 NapCat 运行代码 /app/napcat/napcat.mjs 中确认):
sendMsg 把消息交给 QQ 内核后,等 onMsgInfoListUpdate 事件匹配
guildId + KSEND_STATUS_SUCCESS,内部固定等待 10s(附件更长)。
主机 IO 饱和(iowait 94–96%)时主线程被 10–30s 长任务阻塞,回调排不上队,
于是 NapCat 抛 "Timeout: NTEvent ... sendMsg" → 非零 retcode(实测 200)。
但消息**已被 QQ 送达**:同一窗口内确有 16 条「发送 -> 私聊」成功记录。
这与已修的 HTTP 客户端超时是两条不同路径:
- HTTP 超时:client.Post 返回 err(bd26c2a 已告警)
- NapCat 超时:HTTP 200 + 响应体带 NTEvent 字样(本次补)
只报「失败」不说「可能已送出」,模型必重发。
改动:
- napcat() 对发送类动作(napcatSendActions)的 NTEvent 超时包装
errNapcatTimeout,并附「很可能已送出、不要盲目重发」的告警;
查询类动作保持普通失败(可安全重试)。
- 不按 retcode 判定:该超时来自 callNormalEventV2 的普通 Error,
retcode 本身不携带「超时」语义。
- 不以 serviceAndMethod == sendMsg 为条件:调用方已限定发送动作,
该收紧既无生产证据支持,又会把同类超时退回普通失败重新诱发重发。
测试(全部穿过 napcat() 真实调用链):
- TestNapcatSendTimeoutWarnsAgainstBlindResend(生产原文 retcode=200)
- TestNapcatQueryTimeoutIsNotSendTimeout
- TestNapcatSendActionTimeoutRegardlessOfServiceName
变异验证(3 项,均按预期失败后退回):
① 删告警分支 → 发送用例失败 ② 加回 sendMsg 限定 → 不限服务名用例失败
③ 去掉发送动作门禁 → 查询用例失败
|
2026-10-13 12:52:55 +08:00 |
|
|
|
b73a95bb31
|
chore(qq): 版本 1.4.1 → 1.4.2(长超时客户端 + OneBot retcode 校验)
对应插件侧修复 bd26c2a:发送类端点用 120s 超时客户端并校验 OneBot
retcode/status,消除「消息其实已送达、却报发送失败」进而导致模型重复发送。
|
2026-10-13 10:22:34 +08:00 |
|
|
|
b2872f3702
|
refactor(sdk)!: 废除自定协议号,版本身份只由版本号本身承载
兼容性判据原有三处自造版本号,全部删除:
- procProtocolVersion(模板侧握手常量)
- shmVersion / shmOffVersion(共享段 header)
- unifiedVersion(SuperBlock)
为什么废除:语义版本变动频繁(修 bug、加字段),再要求人去升一个额外的号,
那个号必然漏升而腐烂。实证:ProtocolVersion 常量在多次布局变更里只升过一次,
而对应的守卫测试(TestProcTemplate_ShmLayoutMatchesKernel)长期处于失败状态
无人察觉 —— 它当时期望 shmStageFieldCount=18/shmVersion=1,实际已是 19/2。
取而代之的两个判据都是**结构的派生事实**,不需要谁记得去同步:
1. 主版本号:内核 meta.Version ↔ 插件 sdk.SDKVersion 的主版本段比对。
主版本跃迁正是破坏性变更的定义(1.0.0 从 C ABI 改子进程即属此列),
所以它天然是正确分界。
2. field_count:StageContext 描述符个数,决定标志位基址。
顺带修正了 shmFlagsOff:原实现用本插件的编译期常量推算标志位基址,
内核加字段时会静默错位。现改为从 header 读取内核下发的描述符个数。
SuperBlock 的 version 槽位保留(不动其余偏移),值不再写入也不再校验。
判据:TestProcTemplate_RejectsIncompatiblePeer(含 banned 列表,
防这些号被重新引入)。
|
2026-10-13 09:39:15 +08:00 |
|
|
|
bd26c2a402
|
fix(qq): 发送类端点用长超时客户端 + 校验 OneBot retcode,消除「已发出却报失败」
## 真实故障(2026-10-13,生产日志实证)
从 `<data>/log/week_2026-W41.tar.gz` 解出的历史日志 + `memory/context.json`
拿到完整错误文本:
napcat send_private_msg: Post "http://192.168.2.106:25570/send_private_msg":
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
对应日志时序(~30s 恰好撞上客户端超时):
17:50:07 executing tool: output_send__qq
17:50:47 result: 通过 [qq] 通道发送失败: ... Post "...": ... ← 40s
21:47:15 → 21:47:47 ← 32s
08:17:39 → 08:18:09 ← 30s
**消息其实发出去了**,证据有二:
① agent 自己在上下文里总结(memory/context.json 原文):
「其余几次 send 是 napcat 的 25570 接口超时重试,用户只收到一份,不会刷屏」
「napcat 这会儿接口有点不稳(好几次 context deadline exceeded),
不过关键的那一发是成功的」
② 同一内容重试后用户只收到一份 —— 那是 qq 循环保险拦住的,属侥幸而非正确行为。
根因:`httpClient.Timeout = 30s` 被**查询与发送共用**。而 QQ 发送要经
NapCat 的 `NodeIKernelMsgService/sendMsg`,慢时响应头 30s 回不来 ⇒
把已送达的消息报成失败 ⇒ 模型重试。
## 另一个独立缺陷:不校验 OneBot retcode
`napcat()` 原来只看 HTTP 通不通、JSON 能不能解,**从不看响应体里的
成败字段**。而 OneBot v11 的成败在 body 里:
成功 {"status":"ok","retcode":0,"data":{"message_id":...}}
失败 {"status":"failed","retcode":100,"message":"..."}
或 {"data":null,"message":"Timeout: NTEvent ...sendMsg","status":"failed"}
历史日志里实测到 NapCat 返回 `data:null` + `sendMsg Timeout`(真失败)
却被当作发送成功。两个方向都错,必须靠 retcode 判定。
## 改动
- 新增 `sendClient`(120s + Transport 显式配 `IdleConnTimeout`/`ResponseHeaderTimeout`)。
发送类端点(send_private_msg / send_group_msg / send_msg)走它;查询类
保持 30s(幂等,快速失败更利于排查)。
- `Transport.IdleConnTimeout=30s`:默认 Transport 的空闲连接无上限等待,
复用对端已单向关闭的 keep-alive 连接会得到"请求已写入/对端已处理,
本地读响应 EOF"——又一种「已发送却报失败」。
- `napcat()` 校验 OneBot `retcode`/`status`,失败转 error。
- 超时单独包 `errNapcatTimeout`,错误文案明写「**可能已送达并已发出,
不要盲目重发**」——只说"失败"会诱发模型重发 → 重复消息。
## 判据(4 条,两条经变异测试修正)
- `TestOneBotError_FailedResponses`:7 组 retcode/status 组合的判定
- `TestNapcatConvertsOneBotFailureToError`:**端到端**——napcat() 必须把
OneBot 失败转 error。★ 变异(删掉 napcat() 里的 oneBotError 调用)
实测发现:只测 oneBotError 的判据照样全绿(判据没穿过调用链)
⇒ 补这条才咬住。
- `TestNapcatSendUsesSendClient`:**真的验证路由**(用 Transport 重写 host
观测哪个 client 实际发了请求)。★ 变异(删掉路由分支)实测发现:
只断言两个 client 的 Timeout 字段值照样全绿 ⇒ 重写为观测实际请求。
- `TestNapcatTimeoutErrorIsActionable`:超时错误必须含「可能已送达」
与「不要盲目重发」
27 条判据全绿(含既有 23 条)。
|
2026-10-13 08:02:45 +08:00 |
|
|
|
777c3a0ad3
|
fix(proc,qq)!: shm 模板 v2 —— fExtraBag 透传插件自建 Extra 键;qq 身份可序列化绑帧
内核 shm 桥 v2 前(见主仓同步提交)只透传 4 个具名 Extra 键,插件自建键
(qq 的 qq_auth 绑帧身份)在 stage 之间静默丢失 → before_toolcall 永远回落
可变全局身份 → 权限门绑帧在 proc 插件下整体失效(owner 被误拒/非 owner
可能被误放,2026-10-12 生产实证)。
模板(proc_main.go.tmpl):
- shmStageFieldCount 18→19、shmVersion 1→2(与内核镜像同步)
- fExtraBag:具名 4 键之外的自建键整体 JSON 装袋,read/snapshot/dirty 全链接入
- ReadInto:bag 先打平、具名键后覆盖(具名键永远赢,防御袋污染)
qq 插件:
- qqAuthContext 字段导出 + json tag:非导出字段过 JSON 桥会静默丢空
(原根因链一环)
- authOnFrame 双形态:值形态(进程内/单测)+ map 形态(proc 桥往返,
数字 float64);半截身份拒绝 —— 宁回落全局也不猜(owner 误判即提权)
- authFromMap 按 json tag 重建,逐字段校验类型
判据(plugin_test.go):往返无损/降权保持/半截拒绝(逐键)/帧身份优先于
全局(含 owner 被误拒的原始故障场景反向)。变异 Q1-Q4 全杀。
|
2026-10-12 08:28:41 +08:00 |
|
|
|
0b497f4876
|
fix(browser)!: 升级 chromedp v0.9.5→v0.20.1,修视口截图必挂 (#1)
用户报告截图不对(此前已改为落盘返回路径)。查下去发现**两个叠加的问题**,
其中一个是既有的、与上次改动无关的严重缺陷。
## ① 协议解析失败:chromedp v0.9.5 不认 Chrome 154
```
ERROR: could not unmarshal event: parse error: expected string near offset 411 of 'cookiePart...'
```
cdproto 把 `partitionKey` 定义为 `string`,而 Chrome 154 已改成对象
(`cookiePartitionKey`)。新版 cdproto 定义了 `CookiePartitionKey` 结构体,
且 `UnmarshalJSON` **双向兼容**(字符串 → 结构体,旧浏览器也不坏)。
副作用:`Navigate` 从 2.5s 降到 85ms(解析错误消失,不再反复重试)。
## ② 视口截图在 headless 下**永远挂住**(既有缺陷,不是升级引入)
`Screenshot(sel)` 内部是 `QueryAfter` + `NodeVisible`,而 NodeVisible 在
headless Chromium 上永远不满足 —— 两步单独看都成功,但它一直等下去:
```
Do(Query body) ✓ Do(WaitReady body) ✓
Do(WaitVisible body) ✗挂住 Do(WaitVisible html) ✗挂住
Run(Screenshot html) ✗挂住 Run(FullScreenshot) ✓
Run(CaptureScreenshot) ✓(70KB)
```
**决定性对照:旧版 chromedp v0.9.5 在同一个后端上同样挂。**
所以这不是升级引入的。生产日志也佐证:
- 成功的 8 次 —— agent 都传了 `full:true`(走 FullScreenshot)
- 超时的 5 次 —— 都是默认分支(走 Screenshot("body"))
且 `NodeVisible` 的两步实测都正常(`dom.GetBoxModel` 300µs 成功、
`visibleJS` 返回 true / offsetWidth=480),就是等待机制本身不返回。
没有深挖到 chromedp 内部的根因,但**换用 `CaptureScreenshot` 完全绕开这条链**,
且语义更对:「不传 full 就只截当前视口」本来就是 API 文档的含义。
## 迁移方式:兼容层收敛,业务代码最小改动
v0.20 是**泛型重写**(`Action[T]` 返回值而不填指针),25 处调用点受影响。
新增 `chromedp_compat.go` 把新 API 收敛成几个小助手
(`chromedpDo` / `newRemoteAllocator` / `chromedpCaptureScreenshot` …),
业务代码保持旧写法。
为何不逐处改:20+ 处机械改写会把 diff 淹没,真正要审的
「行为变了没有」反而看不清。
`NewRemoteAllocator` 在 v0.20 移到了独立子模块 `remote`(核心模块现在
只用标准库 + cdproto,需要 websocket 的东西全部搬出去)。
## 顺带修掉 go.mod 的重复 replace
`go mod tidy` 清掉了 4 条重复的 `replace ... => ../../`(hmapdev 往已有
go.mod 追加却不去重导致的)。Go 容忍它,但 `go mod tidy` 会报错。
## 判据(可杀变异,实测)
`chromedp_compat_test.go`:
- 视口截图必须走 `CaptureScreenshot`,不得退回 `Screenshot(sel)`
(改回挂住的写法 → FAIL ×2)
- 兼容层关键助手都在(去掉 CaptureScreenshot → build FAIL)
- 无旧式「传指针接收截图」残留
全部 20 个 example `go vet` 通过;SDK 根 module build/test 通过。
生产实测:agent 做视口截图返回
`/home/newqqagent/browser_screenshots/shot-20261011-104225.610-browser_1.png`
(1280×713 PNG / 70518 字节),不再超时。
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
|
2026-10-11 10:45:41 +08:00 |
|
|
|
25bd243662
|
fix(browser)!: 截图落盘返回路径,不再回 base64
用户报告:截图直接返回二进制(base64)对 agent 十分不友好。确认属实,且
生产日志里有 agent 的**绕行实证**(2026-10-10):
browser_screenshot → cmd_run(自己 base64 解码写盘)→ describe_image
那一轮跑了 920 秒。
## 为什么 base64 是错的
工具结果是 JSON 文本,base64 PNG 会**整段进模型上下文**,而模型看不见
这些字符里的图 —— 纯粹浪费:
视口截图 100-500KB ⇒ base64 后 137-685KB(约 3-17 万 token)
更关键的是它把「解码 + 写文件」这件重复劳动推给了每个消费方,而模型做不到。
内核早有对应约定:`describe_image` 的 path 参数描述写着
「★处理设备/工具回传的媒体时必须传(screensee / camerasue 会回传 file 路径)」。
本插件返回路径才是合上这个约定。
## 改动
- 截图落盘到 `<data_dir>/browser_screenshots/`,返回**绝对路径**
- 结果里带 `hint` 直接告诉模型「调 describe_image(path=...)」——
工具描述模型可能漏看,结果里的提示更近
- `inline=true` 才额外带 base64/data_uri(只有确实拿不到文件系统的场景才用)
- 落盘失败**明确报错**,不静默退回 base64:那会让「上下文被灌爆」
在磁盘满/权限错时重新出现且没有迹象
- 文件名带毫秒时间戳 + 会话 id:同一轮内多次截图是常态
(生产日志里一轮截了 3 次),固定名会静默覆盖
- 会话 id 过滤路径分隔符(它来自模型且被拼进路径,否则 `../` 能写出目录外)
- 未注入 data_dir 时降级到临时目录,而不是让截图整条失败
## 判据(5 条,可杀变异,逐个实测)
`screenshot_test.go`:
- 落盘 + 返回绝对路径 + 内容逐字节一致
- 连续截图不互相覆盖(文件名去时间戳 → FAIL)
- 会话 id 路径穿越被挡(`../../etc/evil` 仍在目录内)
- 未注入目录时降级而非报错
- 工具描述必须提 path 与 describe_image(描述退回 base64 → FAIL)
- handleScreenshot 返回结构必须是 path 而非 base64
(整段替换为旧实现 → FAIL;保留 path 但无条件加 base64 → FAIL)
**判据本身修过两次(都是变异测试发现的)**:
1. 第一版只看 `res["base64"] = b64`,把 base64 写成 map 字面量的键就绕过了
2. 第二版按整段文本查 "inline",而**注释里也有 "inline"** ⇒ 把保护开关
改成 `if false` 仍能通过。改为先 `stripLineComments` 再查真实代码行
全 20 个 example 编译通过;browser 插件 `go test` + plugin 模式构建通过。
|
2026-10-10 23:26:19 +08:00 |
|
|
|
e0ee0c43bb
|
fix(hmapdev): SDK 模块识别要认新旧两个 module 名(迁移漏改的逻辑判定)
上一提交(b6a6168)只替换了 import 路径,**漏了用旧 module 名做逻辑判定
的五处**。CI 当场抓到一处(`TestPrintVersionReportsInjectedVersion`),
但那是测试断言;另外三处是**产品逻辑**,CI 抓不到,是静默失效。
## 漏改的地方
| 位置 | 原判定 | 失效后果 |
|---|---|---|
| `cmd_build.go` | `Contains(mod,"homeagent-sdk")` | 认不出 SDK 依赖 ⇒ **不写 replace** |
| `cmd_skill.go` | `Contains(gm,"homeagent-sdk")` | 认不出 SDK 仓 ⇒ 不安装 skills |
| `yaegi/interp.go` ×2 | `Contains(line,"homeagent-sdk")` | `findSDKGoPath` 找不到 SDK ⇒ 退化到兜底 |
迁移后这些 `Contains` **恒为假**(新名是 `homeagentsdk`,没有连字符)。
三者都**不报错**,只是行为悄悄变成另一条分支——比直接崩更难发现。
## 关键:不能只认新名
本机实测存量插件工作区的 go.mod **全部仍是旧名**:
```
/home/program/qq-plugin (无 go.mod)
/home/program/vanblog-plugin require gitcode.com/JianFeeeee/homeagent-sdk
/home/program/mcquery-plugin require gitcode.com/JianFeeeee/homeagent-sdk
/home/program/fileproc-plugin require gitcode.com/JianFeeeee/homeagent-sdk
/home/program/agentmail/plugins/ require gitcode.com/JianFeeeee/homeagent-sdk
```
用户已创建的项目不会因为我们改仓就自动更新。所以判定必须
**两种都接受**,而不是把旧名换成新名(那只是把 bug 从"认不出新"换成"认不出旧")。
## 改动
- `meta` 补 `SDKModule` / `LegacySDKModule` 常量(原注释引用了 `SDKModule`
但常量并不存在——注释先于实现)
- 新增 `tools/hmapdev/sdkmodule.go`:`isSDKModule` / `sdkModulePath` /
`containsSDKModule`
- 新增 `tools/hmapdev/yaegi/sdkmodule.go`:同语义的最小副本
(yaegi 是独立子包,不能 import 父包)
- 三处逻辑判定改用上述助手
## 判据(可杀变异,逐个实测)
`sdkmodule_test.go` + `yaegi/sdkmodule_test.go`:覆盖新名/旧名/带版本号的
旧名/空串/核心仓(`HomeAgent`,不是 SDK)/无关模块。
这三个判定点**此前零测试覆盖**——这正是它们能静默失效的原因。
变异验证:
- 只认新名 → FAIL「旧名漏认会让存量插件静默走错分支」
- `containsSDKModule` 只认新名 → FAIL「必须认旧名」
## 验证
- SDK 根 module + hmapdev module:`go build` / `go test` 全通过
- 20 个 example `go vet` 全通过
- `go mod verify` → all modules verified
|
2026-10-09 07:32:00 +08:00 |
|
|
|
d685789988
|
docs: 文档与站点链接随托管地迁移到 GitHub
go module 路径已迁到 `github.com/JianFeeeee/homeagentsdk`(见上一提交),
文档里的示例 import、Releases 下载地址、模块路径引用同步更新。
涉及:README.md、README_EN.md、docs/guide/{getting-started,first-plugin,
capability-boundary}.md、mkdocs.yml。
## 有意保留的 gitcode 提及
**不是漏改**,删了反而让文档说谎:
- README.md:「二进制的实际分发地址目前是 gitcode」——两个仓的 release
附件不同步,只有二进制暂时还得去 gitcode 取
- mkdocs.yml:gitcode 镜像链接(国内直连更快,海外看 GitHub)
- docs/guide/getting-started.md:注明「gitcode 仅作国内镜像」
判据:**能照做的指引**优先于「链接好看」。读者照 README 下载 hmapdev
时,走 gitcode 是真能拿到东西的那条路。
|
2026-10-09 07:25:35 +08:00 |
|
|
|
b6a6168e65
|
chore(module)!: module 路径随托管地迁移 gitcode → github
SDK 仓已迁到 GitHub 作为权威源,go module 路径同步:
`gitcode.com/JianFeeeee/homeagent-sdk` → `github.com/JianFeeeee/homeagentsdk`
## 范围
74 个文件:71 个 Go 源码 + go.mod/go.sum + hmapdev 模板 + CI 与打包脚本。
主仓(HomeAgent)侧对应改动是 6d38a164。本仓这些文件当时**不在工作树上**
(见下),所以那次的批量替换扫不到它们——这里是补偿。
## 为何这批文件当时不在工作树上(事故与恢复)
10-08 07:58 主仓做 checkout/cherry-pick 序列时,把本仓工作树的 20 个文件
误清理了(`example/*/plugin.go` + `plg.json`,10 个示例)。
根因是**结构误判**:本仓是「一目录两仓库」结构下主仓 `third_party/` 里的
**独立 git 仓库**,而主仓 .gitignore 忽略了 `example/`,于是它在主仓
`git status` 里不可见 —— 看着像「不受跟踪的构建产物」,实际是本仓受跟踪的源码。
已全部恢复(`git checkout -- <D 文件>`,只恢复删除、不动迁移改动),
20 个文件与 HEAD 逐字节一致。
**破坏的连锁后果**:恢复出来的 `plugin.go` 仍写着旧 module 路径,
因为迁移时它们已不在工作树上。所以这次补偿是必须的,不是可选优化。
## 保留 gitcode 引用
以下位置的 `gitcode` **有意保留**,不要"顺手清掉":
- README.md / docs 里「二进制实际分发地址目前是 gitcode」的说明
- mkdocs.yml 的 gitcode 镜像链接(国内直连更快)
- `tools/hmapdev/templates.go` 里「SDK 已从 gitcode 迁到 GitHub」的注释
gitcode 仍是**只读镜像**(源码同步;release 附件暂时仍在那边)。
## 验证
- 本仓根 module:`go build ./...` + `go test ./...` 全通过
- 全部 20 个 example:`go vet` 通过(补偿前 10 个因旧 module 路径失败)
- example/qq:`go test` 通过,且能从源码 build 出可用 plugin 产物
|
2026-10-09 07:25:25 +08:00 |
|
|
|
c7e5d2a446
|
docs: README 补文档站入口,并把「SDK 源码」链接迁到 GitHub
SDK 仓此前**没有任何地方指向文档站** —— README 不写、GitHub 仓的
homepage 字段也是空的,于是从仓首页根本走不到 sdk.homeagent.jianfgit.xyz。
文档站本身一直在线(106 个文件的 mkdocs 产物),只是没人从仓里指过去。
本次补上:
- README.md / README_EN.md 顶部加文档站入口(快速开始 / API 参考 / 能力边界
/ llms.txt + llms-full.txt 的 agent 直读入口),并在末尾加「文档」章节
列出全部页面的直链。
- mkdocs.yml 的社交链接由 gitcode 改指 GitHub,并**新增** gitcode 镜像入口与
介绍站入口(国内直连 gitcode 更快,海外/权威源看 GitHub,两个都留)。
- docs/guide/getting-started.md 的 `git clone` 地址改为 GitHub 仓名
(homeagentsdk),并写明「release 附件暂时仍在 gitcode」这个事实。
★ 刻意**不**改的东西:Go 模块路径 `gitcode.com/JianFeeeee/homeagent-sdk`。
改模块路径会让所有现有插件的 go.mod 全面失效,属于破坏性变更,
与「仓库托管位置迁移」是两件事。
关于 GitHub 侧没有 release 条目(因此不能写 releases/latest/download/...,
那会 404):已在 README 里如实写明 —— 二进制目前只在 gitcode 的 release 附件里,
GitHub 从下一个 SDK 版本(v1.4.0)起才会同步发布。
验证:两份 README 里 28 个 URL 全部实测 200;文档站重建(106 文件)并部署,
线上 `git clone` 已是 GitHub 地址、旧 gitcode clone 地址 0 次、
三个社交入口名称正确。
|
2026-09-29 20:43:27 +08:00 |
|
|
|
c16411a77d
|
ci: 修 CI 自身的三处错误(本地能过 ≠ CI 能过)
首次跑出来的 8 个失败全是本 workflow 自己的问题,不是代码缺陷。
三条都属同一类:**命令本地验证过,但环境前提不同**。
## 1. hmapdev 交叉编译:在错误的目录跑(5 个平台全红)
no Go files in /home/runner/work/homeagentsdk/homeagentsdk
`go build .` 缺 `working-directory: tools/hmapdev`,于是在仓库根执行 ——
根目录没有 Go 文件(包在 sdk/ 子目录)。加 working-directory 后本地实测
linux/arm64、darwin/arm64、windows/amd64 均产出 27–30MB 二进制。
## 2. Examples:装了 Go 1.21,而 examples 要求 1.25
go: go.mod requires go >= 1.25.0 (running go 1.21.0; GOTOOLCHAIN=local)
`go-version-file: go.mod` 读的是**根** go.mod —— 它写 `go 1.21.0`,
而 20 个 `example/*/go.mod` 都要求 `go 1.25.0`(只有根与 tools/hmapdev 是 1.21)。
于是 CI 装 1.21,examples 构建必失败。
★ 为什么本地测不出来:本机 Go 1.27 且 GOTOOLCHAIN 默认可自动取更高工具链,
更高版本能满足 1.25 的下限,所以一路通过。而 GitHub runner 上
**GOTOOLCHAIN=local**,Go 拒绝自动下载工具链,低版本直接报错。
本地「能过」在这里完全不构成证据。
改法:SDK CI 全部钉 `go-version: '1.25'`(同时满足 1.21 的下限与
examples 的 1.25 要求)。
## 3. 技能检查:hmapdev 建到了 /tmp
error: no active SDK version set
`resolveSkillsSource()` 先用**可执行文件位置**向上找仓库
(tools/hmapdev → ../../skills),落空才回退到 SDK store 的活跃版本。
我把二进制建到 /tmp/hmapdev ⇒ 推出仓库失败 ⇒ 回退 ⇒ CI 里没有 store ⇒ 报错。
改法:建到仓库内 `<repo>/build/hmapdev_ci`。本地实测(构建在仓库内时,
即便从 /tmp 调用也能正确识别仓库源——解析依据是 exe 位置而非 cwd)。
## 附带记录
- 根 go.mod(1.21) 与 example/*/go.mod(1.25) 的版本不一致本身值得关注:
用 Go 1.22–1.24 的用户按示例走会失败。是否统一属产品决策,本次不动。
- actionlint 全绿。
|
2026-09-29 15:29:16 +08:00 |
|
|
|
5759e58bad
|
ci: 建立 CI —— 此前 main 的推送与 PR 完全没有检查
## 缺口
本仓此前**只有 release.yml**(只在 release/** 推送时跑)。也就是说
推 main、开 PR 一律无检查,而 SDK 正是外部插件开发者直接依赖的契约面
(sdk/plugin.go 接口一破,所有外部插件编译失败)。
## 四个 job(每条命令都本地实测过)
| job | 内容 | 实测耗时 |
|---|---|---|
| go | 根模块 build + vet + test | <1s |
| hmapdev | 嵌套 module 测试 + 五平台交叉编译(矩阵) | ~2s + 编译 |
| examples | 构建全部 20 个示例(linux/amd64、darwin/arm64) | 9s / 21s |
| consistency | Lua SDK 三副本一致 + skills 可识别 + mkdocs --strict | ~2s |
## 三处「不能想当然」的地方(都有实测依据)
1. **示例不能用 `go build` 验**。它们是插件(只有 plugin.go、没有 func main),
必须由 hmapdev 注入 main 包装;直接 go build 得到
"function main is undeclared in the main package" —— 那不是缺陷,是方式不对。
故与发版走**同一个脚本**(package/build-examples.sh),避免 CI 与发版路径分叉。
2. **tools/hmapdev 是独立 module**,根模块的 `go test ./...` 不会进入它 ——
必须单独跑,否则它的测试永远不在 CI 里执行(Go 的模块边界)。
3. **不能用 `yaml.safe_load` 校验 mkdocs.yml**:它含 mkdocs-material 的
`!!python/name:` 标签(配置 emoji 的官方写法),safe_load 报
ConstructorError —— 是校验方式不对。改用 `mkdocs build --strict`
(本地实测 2s、0 warning)。
## 明确不进 CI
- `hmapdev skill install` —— 它会写开发者本机的 ~/.claude、~/.codex 等目录
- 需要内核仓在场的检查(本仓独立可测)
- 任何网络/真机依赖
CGO_ENABLED=0(SDK 纯 Go,与主仓相反)。
actionlint 全绿(修掉一处 shellcheck SC2012)。
|
2026-09-29 15:18:58 +08:00 |
|
|
|
275a054287
|
fix(packaging): build-examples.sh 用新变量名 HMAPDEV 会 unbound variable
## 症状
按注释声称的「兼容」用法传新名,脚本立刻死:
HMAPDEV=... bash package/build-examples.sh linux/amd64 /tmp/out
→ package/build-examples.sh: line 72: PLUGINDEV: unbound variable (exit 1)
而传旧名一切正常:
PLUGINDEV=... bash package/build-examples.sh linux/amd64 /tmp/out
→ 成功 20 / 失败 0
## 根因
第 71 行写的是:
HMAPDEV="${HMAPDEV:-${PLUGINDEV:-$SDK_ROOT/build/hmapdev}}"
这**只定义了 HMAPDEV**;PLUGINDEV 若未设置就仍是未定义变量。
而紧接着的 72/77/100 三行读的却是 `$PLUGINDEV`:
if [ ! -x "$PLUGINDEV" ]; then # ← 未定义 → set -u 下致命
out=$( cd "$dir" && "$PLUGINDEV" build ... )
于是只有当调用方**恰好传旧名**时 PLUGINDEV 才有值、才工作。
注释说「旧变量名 PLUGINDEV 仍兼容」——兼容的方向反了:是旧名恰好能跑,
新名不能。
## 为何一直没暴露
发版路径传的正是旧名(build.sh:92 `PLUGINDEV="$dev" ...`),
所以 release 流程一路是绿的。任何人按新名(或根本不传)调用都会撞上。
## 修法
三处改读 `$HMAPDEV`(第 71 行刚解析出的规范名)。
## 验证(三种调用方式都要能工作)
A) HMAPDEV=... → exit 0,20/20,20 个 .hmap(修复前 exit 1)
B) PLUGINDEV=... → exit 0,20/20(向后兼容保持)
C) 都不传 → 自动先构建 hmapdev,再 20/20
跨平台也验过:linux/arm64 20/20(19s)、darwin/amd64 20/20(21s);
windows 被有意拒绝并给出明确原因(协议 2 的统一共享内存区未移植)。
|
2026-09-29 15:18:43 +08:00 |
|
|
|
56c694cd7c
|
ci: gh release download 需显式 --repo
回读校验先 cd /tmp/back(非 git 目录),而 gh 默认从当前目录的
git 上下文推断仓库,于是报 "not a git repository" —— 发布本身
成功(tag/release/附件齐全),却被这道校验误判为失败。
改为 gh release download "$TAG" --repo "$GITHUB_REPOSITORY"。
|
2026-09-29 14:39:12 +08:00 |
|
|
|
dc9e7d0495
|
ci: SDK 仓发布流水线 —— release/** 推送即出 hmapdev 五平台产物
与主仓 Release 流水线同构,差异只在产物与打包命令:
prepare 读 meta.Version;tag 已存在则整轮跳过;go test 门可显式跳过
(改 meta 的提交里写 [skip-release-tests],查该提交而非 HEAD)
build go build(硬门)→ go test → build.sh all hmapdev → 验证 5 平台齐全
→ 生成 SHA256SUMS → artifact
publish 打 tag → gh release create 传附件 → 回读校验
sync-gitcode 有 GITCODE_TOKEN 时同步(无则跳过)
产物清单不是猜的,依 gitcode 上 v1.2.0/v1.3.0 的实际附件(各 6 个):
hmapdev_{linux,darwin}_{amd64,arm64} + hmapdev_windows_amd64.exe + SHA256SUMS
两个易错点都有实测依据:
1. **测试要分两处跑**:tools/hmapdev 是**独立 module**,根模块的
`go test ./...` 不会进入它(Go 的模块边界,不是配置问题)。
2. **gitcode 上传必须显式列文件名**:上传脚本的路径语义是
`os.path.join(ASSET_DIR, name)`,所以要 cd 进目录 + `ASSET_DIR=.` + 裸名;
而它按扩展名识别产物的默认扫描对 hmapdev **无效** —— 五个产物里只有
windows 那个有扩展名,自动扫描会静默地一个都不传。
故把主仓的 upload_assets.py 一并纳入本仓 scripts/(两仓各自独立可取)。
本地已验证:`VERSION=1.4.0 bash package/build.sh all hmapdev` 产出 5 个
二进制(各 27–29M),根模块 go test 通过,actionlint 全绿。
|
2026-09-29 14:09:35 +08:00 |
|
|
|
29c61f0e07
|
feat(hmapdev): skill 子命令 —— 插件开发知识随 SDK 分发
## 为什么需要
HomeAgent 插件开发的知识(hmapdev 工具链、SDK 版本坑、plg.json、
plugin.bin 部署、内置 vs 独立二进制)此前只存在于**某个 agent 的对话历史**里。
本机四个 agent(pi / claude / codex / .agents)都不会开发插件,
因为它们**没有任何渠道**拿到这些知识。
知识属于 SDK(与工具链同源、随 SDK 版本走),所以:
- 源:`skills/`(随 SDK 仓分发)
- 装:`hmapdev skill install`
## 命令
hmapdev skill list 列出活跃 SDK 里的 skills
hmapdev skill install [name...] 装到各 agent 的 skills 目录
hmapdev skill path 显示源目录与来源
安装目标(**只装这些**,不认识的目录不创建):
~/.pi/agent/skills ~/.claude/skills
~/.codex/skills ~/.agents/skills
## 两条策略,与别处**故意相反**
1. **总是覆盖**。skill 是**工具生成物**,不是用户数据。保留用户改过的版本
会让它与工具链脱节 —— 而工具链的命令面会随版本变。
(对比:`hmapdev build` 写出的适配器更新会**保护**用户修改,
因为那是运行时代码;skill 只是说明文档。)
2. **不扫 glob 自动发现**。不认识的目录建出来也没用,
还会让用户以为装上了。
## 源目录:仓库优先,store 回退
store(`~/.homeagent/hmapdev/sdk/<v>/`)是 `sdk install` 复制的**副本**。
而 skill 是纯文档、加它不需要动 SDK 的编译产物 ——
「改了 skill 却要重装 SDK 才能生效」对日常维护不合理。
故优先用 hmapdev **自身所在仓库**的 `skills/`(靠可执行文件位置反推,
并校验 go.mod 的 module 是 homeagent-sdk),落空才回退 store。
## 判据 9 条(cmd_skill_test.go)
覆盖:目标无重复/不逃出 home、**真实仓库**里 skills/ 的布局合规、
隐藏目录与普通文件不算 skill、幂等、覆盖用户修改、跳过未知目标、
真实入口 `cmdSkillInstall` 覆盖用户修改、未知 skill 名不静默。
### 写判据时踩的三个坑
1. **`TestSkillSourceHasValidLayout` 原用 `t.TempDir()`** ⇒ 那个目录是空的,
判据永远红,且红得毫无意义("文件不存在"是真的,但真实文件在 SDK 仓里)。
改为指向**真实仓库**。
2. **fixture 造错**:我 `MkdirAll` 出一个叫 `README.md` 的**目录**,
于是实现"正确地"把它当 skill,判据却报「把 README.md 当成了 skill」。
是 fixture 错,不是实现错。
3. ★ **`TestSkillInstallOverwritesUserEdit` 只验 `copyDir`、没走真实入口**
⇒ 我把 `cmdSkillInstall` 里的 copyDir 换成「已存在就跳过」,
**判据依然全绿**。变异测试抓出来的。补 `TestCmdSkillInstallOverwritesUserEdit`
走真实入口后,变异立刻变红。
## 实测
删掉三处 skill 后 `hmapdev skill install` 一条命令装回四处,
四处 md5 与源一致,二次安装结果不变(幂等)。
## 门禁
- `go test ./...`(hmapdev 独立 module):ok,0 FAIL
- `go build ./...`:ok
- 9 条判据全通过,变异测试确认能抓回归
|
2026-09-28 11:23:58 +08:00 |
|
|
|
82e8d9dbac
|
docs: 补两篇指南 —— 工具并发声明、流式多 tool_call
`ParallelSafe` / `Serial` / `stream_index` 三个新能力此前**零文档**:
README 提到 `Serial` 的那处是 UART 串口,与并发声明无关。插件作者只能
读源码注释才能知道这些字段存在及其优先级。
## docs/guide/parallel-tool-declaration.md
- 保守 opt-in 的理由:存量插件不改一行就得串行,不会被升级意外并发
- 声明 `ParallelSafe` 的三个条件(线程安全 / 不争抢资源 / 顺序无关)
- `Serial` 存在的意义:让"我确认过**必须**串行"与"我没想过"可区分
- **`Serial` 胜出**,不允许被 `ParallelSafe` 或默认值覆盖
- 声明字段放在 `ToolDef` 末尾,遵循既有 `NoMemory` 风格
- 压测效果 N=2/4/8 → 1.41×/2.22×/3.88×,并附"必须同时统计实际执行数"
的理由(旧适配器耗时更短但实际处理 0 个工具)
## docs/guide/stream-tool-call-index.md
面向写 Lua 适配器的人:
- 上游 `index` 字段的用途:分桶累积 `id`/`name`/`arguments`
- **键名是 `stream_index` 不是 `index`** —— 写错会被 Go 解码器静默丢弃
- 不透传的实际后果:name 互相覆盖、args 碎片混拼、工具被当空参数调用
- 顺带记两个易踩点:不能按 name 过滤分片;扁平结构的协议族同样要带
- 自检命令;并注明 `gemini.lua` 不涉及(协议是 `functionCall`)
## 其它
- `mkdocs.yml` nav 登记两篇 —— 之前它们会被 mkdocs 明确警告
"not included in the nav configuration",等于在站点里不可达
- 重跑 `tools/apidoc/build.sh` 同步 `docs/api/*`、`llms.txt`(生成物)
核实过的事实,避免臆造:
- 内置工具声明是**核心仓**的 `toolDefOptions`/`parallelOpts()`
(`internal/agent/core/tooldefs.go`),不是 `sdk.BuiltinToolDef` —— 初稿写错
- `gemini.lua` 对 `functionCall|tool_calls` 匹配数为 0,确认无流式实现
- `server/kimicode/anthropic/ollama` 四个适配器确有 `stream_index`(2~5 处)
- 跨仓相对链接不可解析,故改为纯文本路径指路
`docs/api/tools.md` 属生成物(首行注明"请勿手改"),其 `ToolDef` 签名被截断、
不展示字段 —— 这是生成器既有行为,本次 diff 只是行号漂移,未改它。
|
2026-09-27 22:25:38 +08:00 |
|
|
|
1b8218afa9
|
fix(tools): 审计器补齐 recv/selfCalls 索引与只读白名单;标注器定位修正
## audit_parallel 的四个误判来源
**① indexMethods 没设 recv 和 selfCalls ⇒ 递归跟进空转**
`scanWritesDeep` 靠 recv 组候选键、靠 selfCalls 才知道跟进谁,缺任一个
就静默漏判。handleRestart 只调 p.stopServer/startServer 两个方法,
因此被判成"只读" —— 而 stopServer 里有 `p.server = nil`。
(我先补了闭包那条分支的 selfCalls,误以为问题在闭包上,其实是
indexMethods 整体缺字段。)
**② writePatterns 写了 ".Set(" 而 callString 收集的是方法名**
`p.sdk.Settings().Set(...)` 是三段链,callString 返回 "Settings.Set",
永远匹配不上 ".Set("。漏判比误判更难发现:结果看着仍然合理。
→ 模式改为不带括号。
**③ looksLikeExternal 用黑名单 ⇒ 纯读标准库调用全被判"可能写"**
Marshal / ReadAll / NewRequest / NewReader 都不带写操作迹象,却被判写,
120 个工具里 119 个判成 SERIAL —— 等于工具没在工作,却**看上去在工作**
(保守方向不会引起怀疑)。
→ 改为 knownReadOnlyCalls 白名单。判定原则:**默认怀疑,明确信任** ——
写不动的东西要逐个列出来。
**④ 工具名提取打印 AST 内部结构**
`fmt.Sprintf("%v", a.Y)` 输出 `tp&{10500 10515 STRING "manage_social"}`,
名字里混着指针地址,人没法核对。
→ 改用 BasicLit.Value + 归一化去掉运行期前缀。
## annotate_parallel 的插入点定位
改文本匹配改结构体字面量这条路走了三次弯:
- 正则找"最后一个顶层字段"被嵌套 map 里的同形文本骗到,823 处重排改坏文件
- 变量前缀匹配没要求 RegisterTool( 在**同一行**,命中函数体里散落的字面量,
起点错到 switch case 中间,报错指向一处看起来完全无辜的分支
- 只按行末**净**深度判断:Parameters 写在单行时(进出同一行,净变化 0)
永远察觉不到曾进入深度 3,追踪一路跑到 1305 行才"收敛"
→ 最终按**行内峰值深度** + 记录进入深度 3 的行号判定。
## 幂等
未标注过的工具重复跑会插入第二份 ParallelSafe(duplicate field 编译错误)。
→ 加 blockHasDecl 预检。
|
2026-09-27 16:17:04 +08:00 |
|
|
|
deeda22650
|
tools: 工具并发安全审计器(audit_parallel + annotate_parallel)
两个工具,都是为了让「这个工具能不能并发」有**可复现的依据**,而不是靠人眼扫。
## tools/audit_parallel.go —— 审计
为什么需要它:正则扫 `ToolDef` 字面量**不可靠**。用它审计主仓 27 个工具时,
把 `config_set` 判成"无共享写",而它的 handler 其实在 `p.handleSet` 里且无锁 ——
原因:RegisterTool 第三个参数是方法名/闭包,正则看不到执行体。
本工具用 go/ast 从**注册点跟进到 handler 实现**,递归 3 层(带环检测),
输出三态:SAFE / SERIAL / UNKNOWN。
★ UNKNOWN 一律不声明并发安全 —— 追不到实现就不能声称安全。
### 开发它时踩的坑(都写在代码注释里)
- **通用 HTTP 包装器**:vanblog 的 manage_social / manage_settings 全走
`p.do("GET"|"POST"|"PUT", ...)`,方法名毫无写操作迹象,纯靠名字匹配
**全判成 SAFE** —— 而它们明确含 POST/PUT。审计工具给出与代码相反的结论,
比不给结论更危险(人会信它)。改为按 HTTP 动词判定后,
统计从 72/37 变为 46/63。
- **链式调用丢方法**:`p.sdk.Settings().Set(...)` 是三段链,只看最外层只得到
"Settings",`.Set` 整个丢失 ⇒ handleConfigure(写配置 + 启停服务)被判只读。
改为收集整条链的所有方法名。
- **变量名 ≠ 类型名**:调用点写 `p.handleRead()`,定义处是
`func (p *Plugin) handleRead()`。拿变量名去查类型索引**永远匹配不上**,
143 个工具全报 UNKNOWN。须建"变量名 → 接收者类型"索引。
- **方法接收者不是局部变量**:example 里根本没有 `p := &Plugin{}`,
p 是 Start 的接收者,局部变量索引全空。须把接收者变量名也纳入索引。
- **枚举被字符串污染**:verdict = "Serial:true" 而 report 只认三个枚举值
⇒ 全部落进 UNKNOWN,输出"共 13:UNKNOWN 13",看着像工具没在工作。
- **作者声明必须优先**:我把"已声明"当 finding 记录后照常跑写入检测并
**覆盖** verdict,于是 plugin_install(已标 Serial:true)被判 SAFE。
## tools/annotate_parallel/ —— 标注
按 SDK 风格插入声明项:Name 在首位,声明项在末尾(Parameters 之后、
handler 之前),不打散 gofmt 对齐。
插入点必须用**括号深度 + 记录进入深度 3 的行号**定位:
- `RegisterTool( =1, ToolDef{ =2, Parameters{ =3`
- 只判 depth==2 会在 `Name:` 行就返回(那行本来就是深度 2),
插入点跑到 RegisterTool 之前,编译报 "expected 1 expression"
- 空 `properties: map[string]interface{}{}` 让深度**在同一行**进出平衡,
所以"曾触及深度 3"也不能作门控,必须记行号
- 试过用正则找"最后一个顶层字段",被嵌套 map 里的同形文本骗到,
823 处错误重排把文件改坏 —— 文本匹配改结构体字面量就是这条路
用法:go build -o /tmp/annotate ./tools/annotate_parallel
/tmp/annotate <file> <tool:parallel|serial:说明> ...
|
2026-09-27 16:17:04 +08:00 |
|
|
|
1d7c330cfc
|
fix(hmapdev): mocksdk 补齐 ToolDef 声明项,并加字段/类型一致性判据
## 问题
mocksdk(yaegi 解释执行时的替身 SDK)的 ToolDef 只有 6 个字段,
公共 SDK 已有 10 个 —— 缺 RecallPolicy / ParallelSafe / Serial。
★ 危害不在编译期,而在**调试期**:插件作者用 hmapdev 在本地解释执行时,
写了 ParallelSafe:true 照常跑、不报错;直到编译安装后才发现声明根本没
被内核读到。这类不一致不会让任何现有测试失败。
这正是"黑名单不会自动跟上新执行能力"的又一次复现:主 SDK 每加一个声明项,
替身不会自动跟上。
## 修复
① mocksdk/plugin.go 补齐 RecallPolicy / ParallelSafe / Serial 三个字段,
注释写明"主 SDK 先加、mocksdk 没跟上"这段历史,避免后人再漂移。
② 新增 yaegi/mocksdk/parity_test.go:
- TestMockSDKToolDefMatchesSDK 比对**字段名**集合,双向都查
(少字段 = 声明静默失效;多字段 = 替身比本体还多,必有一方理解错了)
- TestMockSDKToolDefTypesMatch 比对**字段类型**,逐个断言
★ 归一化必须抹掉所有空白:gofmt 打印 "interface{}" 而反射给
"interface {}",这是打印格式差异。我第一版没抹空白,
结果每个复合类型都被误报成"类型不符" —— 判据自己制造假警报。
判据放在 tools/hmapdev 模块内(它是独立 module,主模块不包含它),
跑法:cd tools/hmapdev && go test ./yaegi/mocksdk/
|
2026-09-27 16:17:04 +08:00 |
|
|
|
bd73a9b241
|
feat(sdk): 通用反代声明项(DeclareProxy)+ ToolDef.Serial 串行标记 + 场面策略文档
本次一并提交工作区此前累积的改动(均已验证),并接入工具并发调度所需的
声明项。
把「谁来反代谁」从内核硬编码变成插件可声明。设备网关(remotedevice)
这类**编译进内核、没有独立插件目录与 plugin.json** 的服务,静态扫描扫不到,
此前只能靠约定。新增 DeclareProxy 让它们能自己声明反代路由。
ParallelSafe 的**反向**声明项。判据优先级:Serial 胜出,显式声明不允许被
ParallelSafe 或任何默认值覆盖。
为什么需要它:ParallelSafe 零值 false 已表达「安全/串行」,插件无法区分
「我没想过」和「我确认过必须串行」。没有这个区分,工具作者只能靠命名约定
传递意图,那不是契约。
ParallelSafe 本身也补齐了注释,明确其零值语义(默认串行、保守)与理由
(新语义下并发会改变工具的行为前提,让存量插件意外并发比慢一点危险得多)。
配套 ScenePolicy 声明项的使用说明。
remotedevice/ 整目录(C 实现的设备网关,已由 Go 侧 DeclareProxy 路径取代)。
- sdk/knowledge.go:随场面策略配套调整
- docs/api/*、docs/assets/api-index.json、docs/llms.txt、mkdocs.yml:
由 tools/apidoc/build.sh 从源码重新生成(行号随 plugin.go 变动漂移)
|
2026-09-27 16:17:04 +08:00 |
|
|
|
e417c69fc8
|
fix(example/bili): 进程组隔离 + 可取消的下载 —— 修拖死内核关停
## 现象
线上关停必超时:systemd 报 `State 'stop-sigterm' timed out. Killing.`,
其中只有 bili 报 `[proc] bili SIGKILL 后 2s 仍未被收割`,之后近 90 秒无日志。
## 根因
本插件用 `exec.Command("yt-dlp", ...)` + `cmd.Run()`:
- 无 CommandContext ⇒ Stop 无法取消
- 无 Setpgid ⇒ yt-dlp 与本插件同进程组
- Stop() 是 `return nil` ⇒ 内核 Kill 插件本体时,yt-dlp 变孤儿
yt-dlp 还会再 fork ffmpeg,**孙进程继承本插件的 stdout 管道写端**。
插件被 SIGKILL 后孙进程仍持有写端 ⇒ 内核 readLoop 永远等不到 EOF
⇒ `Kill()` 末尾的 readerWG.Wait 永不返回 ⇒ 整个关停挂死。
## 改法
1. 两处 `exec.Command` → `exec.CommandContext`,Stop 里 cancel 能掐掉
2. `setPgid` 让命令自成进程组:yt-dlp 拉起的 ffmpeg 也在组内,
kill(-pgid) 能一次带走整棵子进程树,不留孤儿
3. `Stop()` 不再是空实现:cancel 之后**必须 wait**。
只 cancel 不 wait 的话内核会先释放共享段,而 yt-dlp 还在写 stdout ——
那正是内核 readLoop 挂死的成因。`runWG` 等它真正退出。
顺带:取消时返回明确文案("已取消(插件停止)")而不是含糊的 exec 错误。
内核侧的三处修法(Setpgid / 杀进程组 / readerWG 超时)在主仓 ceeef0b,
对所有 8 个同样 exec.Command 且无进程组隔离的插件都有效。
验证:hmapdev build 通过;主仓部署后实测关停 90s → 1s、
`[homed] stopped` 打出、孙进程无残留。
|
2026-09-26 17:08:34 +08:00 |
|
|
|
a176cc3e20
|
feat(sdk): 反代声明(ProxyDef / RegisterProxy)—— 插件声明服务,HomeAgent 反代出去
配套核心仓「webui 通用反向代理」。SDK 1.4.0 尚未发布,接口未冻结,
本次按开发期自由变更处理(正式发版时并入版本号推进)。
## 声明契约
plugin.json 的 proxies 字段(声明式,静态可发现)或 RegisterProxy
(运行期,供没有 plugin.json 的内置插件用):
{"name":"ui","host":"myapp","path":"/p/myapp","strip_path":true,
"target":"127.0.0.1:12100","auth":"homeagent"}
命名与既有能力对齐(ToolDef / ChannelDef / ConfigDef / RegisterTool /
ToolRegistrar)——第一版写成 ProxyDecl / DeclareProxy / ProxyDeclarer
被评审指出「跟 SDK 其他接口不是一个风格」,已全面改名。
## strip_path:Path 的两种语义
Path 不能一刀切成「原样保留」,真实需求有两种且**不能自动判定**
(同一个 path 在两种语义下都说得通,猜错即全部 404 且像上游故障):
strip_path 缺省/false(别名模式)—— path 是上游真实路径的一部分
/api/v1/device/ws + path=/api/v1/device → 上游收到原样
适用:客户端**已硬编码**路径的机器接口(设备网关即如此)
strip_path=true(前缀模式)—— path 只是门户上的挂载点
/p/myapp/api/status + path=/p/myapp → 上游收到 /api/status
适用:自带 UI 的服务(前端用相对路径)
非法组合(strip_path 而无 path)被 ValidateProxyDef 挡住。
## 单一入口原则(契约级要求)
一个声明 = 一个入口。两种挂载形态对「根路径」处理截然不同:
Host 形态下根路径是插件的根(fetch('/api/x') 天然正确);
Path 形态下根路径**属于门户**,同样代码会打到门户自己身上
(静默错路由:页面能开、功能全坏)。
故被反代的插件必须**一律使用相对路径**,绝不硬编码以 / 开头的绝对路径。
这样同一份前端在两种形态下都正确,插件不必知道自己被挂在哪,
反代层也能按外部条件(子域是否有证书/放行)自由选择形态。
## 判据
sdk/proxy_test.go:两种语义的映射、非法组合、单一入口原则的契约存在性。
hmapdev proxy_config_test.go:schema 漂移保护(新增字段忘了同步就判红)、
非法声明在**打包时**就被拒(不必装到 HomeAgent 才看到)。
两模块 go test 全绿;文档站已重新生成(ProxyDef 与单一入口原则进入
docs/api/misc.md 与 llms-full.txt)。
## 顺带修回的一处(此前随工作树丢失)
writePluginJSON 漏写 proxies 键 —— 漏写的话插件装得上、启动正常、
就是不出现,没有任何报错。该 bug 曾在核心仓侧出现过(判据抓到过),
这次移植时由 TestWritePluginJSONPreservesProxies 再次判红并修复。
|
2026-09-26 14:08:30 +08:00 |
|
|
|
255d6479ad
|
feat(docs): 换品牌图标 + 补备案号 + 给 agent 的直读入口
三件事,都是「站点上线后看出来的」问题。
## ① 图标用的是 Material 默认,不是我们的
站上 favicon 是 mkdocs-material 自带的那张(`assets/images/favicon.png`),
与主站 introduce 不一致。改为引入主站同一份 logo:
- `docs/assets/logo.svg` —— 原样(浅色底,适合 favicon)
- `docs/assets/logo-mark.svg` —— 去掉底色矩形(否则在靛蓝页头上是个白方块)
`theme.favicon` / `theme.logo` 分别指向它们。实测子页面路径也正确
(mkdocs 生成 `../../assets/logo.svg`,不是错误的相对路径)。
## ② 缺备案号
`introduce` 底部有 ICP + 公安备案,文档站没有。Material 的 footer 只渲染
`config.copyright` 一个字符串,塞不进「两条带链接的备案」——所以覆盖了
`overrides/partials/footer.html`(`theme.custom_dir`),并顺带把许可也写进底部,
读者在任何一页都能看到,不必翻到首页。
## ③ 没有给 agent 的入口
站点是给人看的(HTML + 主题 + JS 搜索),但越来越多读者是 agent。让它们爬
HTML 既浪费 token(样板占大头)又容易漏内容。
新增 `tools/apidoc/gensite/agent.go`,随构建产出:
| 路径 | 内容 |
|---|---|
| `/llms.txt` | 站点目录:每页一行,带 URL 与一句话说明(4.3KB)|
| `/llms-full.txt` | 全部文档正文拼成一份,可一次读完(115KB)|
| `/<page>.md` | 每个页面的 Markdown 原文,含生成的 API 页(text/markdown)|
沿用 llms.txt 社区约定:`llms.txt` 读目录、`llms-full.txt` 一次读全。
**一个必须处理的坑**:mkdocs 只把 `.md` **渲染**成 HTML,不会把 Markdown
放进产物目录 —— 那样 `llms.txt` 里的链接会全部 404。所以 `build.sh` 增加了
第 4 步 `copy_agent_files`,在构建后把 23 个 Markdown 复制进 `site_build/`。
`docs/llms-full.txt` 已 gitignore:它是派生件,改任何一页都会整份重写,
进版本库只产生噪声(`llms.txt` 索引小且稳定,仍提交)。
验证:`mkdocs build --strict` 零告警;favicon/logo 可取(200,naturalWidth=400);
三页底部均含两个备案号;4 个 agent 入口均可访问且 content-type 正确。
|
2026-09-24 13:27:58 +08:00 |
|
|
|
9b6abe1b73
|
fix(docs): 中文搜不到英文注释的 API —— 补关键词层
## 问题(实测)
SDK 里 100 个有摘要的符号中 **66 个是英文注释**,例如:
RegisterTool registers a tool that the LLM can call.
于是搜「注册工具」——中文受众最自然的问法——**RegisterTool 得分 0,一条都搜不到**。
更糟的是逐字匹配把噪声顶上来了:搜「注册工具」返回 24 条,排第一的是
`SetToolBlocks`(描述里有「工具」二字),`RegisterTool` 根本不在列表里。
## 修法
不改源码注释(那会让代码与文档脱节),而是在检索索引上加一层**人工标注的
中文功能词**:`tools/apidoc/keywords.json`。
- `rules`:按符号名前缀/子串批量覆盖(`Register*` 全带「注册」,`*Memory*` 带「记忆」)
- `symbols`:逐符号补充(重点 API、或规则覆盖不到的)
词只进 `api-index.json` 的 `g` 字段,**不影响页面展示**;检索结果里会显示
(「为何命中」),读者能理解排序依据。
同时把中文逐字匹配从主信号降为**弱信号**(要求 60% 以上字符命中)——
它正是噪声来源:凡是含「工具」二字的说明都会被「注册工具」匹上。
## 结果
| 查询 | 修改前 | 修改后 |
|---|---|---|
| 注册工具 | 24 条,RegisterTool 缺席 | **7 条,RegisterTool 第一** |
| 崩溃 | 1 条 | 2 条(SetAutoRestart + AutoRestart)|
| InjectText | 7 条(含重复)| 6 条 |
| memory.recall | 1 条 | 1 条(不变)|
顺带修掉索引重复:接口会同时作为 `type` 符号与接口本身被加两次
(`MemoryAPI` 等 11 个各重复一条)。现在接口只走接口那条路径,索引 200 → 189 条。
keywords.json 是**可选**的:读不到只警告不中断,检索退化为原行为。
|
2026-09-24 13:00:32 +08:00 |
|
|
|
0a6e2b7dc4
|
docs: 插件 SDK 文档站(API 参考从源码生成 + 自建检索)
为插件作者建一个文档站,重点是**能按描述搜到 API**,以及**明确能力边界**。
## 为什么 API 参考要生成而不是手写
公开 API 面有 115 个符号、11 个接口。手抄必然与代码漂移——这是文档站最常见的
死法(本仓 README 里已经有过几处「文档说一套、代码是另一套」)。
所以 `tools/apidoc` 直接从 `sdk/*.go` 提取签名、文档注释与代码块示例,渲染成
`docs/api/*.md`。发现文档不对时改的是**源码注释**,不是生成物。生成页首行带
「勿手改」标记,防止有人改了下次构建白改。
- `extract.go`:go/ast + go/doc 提取(只用标准库,离线可跑,不引入依赖)
- `gensite/`:渲染 Markdown + 检索索引
- `gensite/usages.go`:从 `example/` 21 个示例插件里反查**真实调用点**,
贴在每个 API 下(源码注释里几乎没有可运行示例,但示例插件都是能编译跑的真代码)
## 能力边界:写这个站时查出的三处文档错误
这是本次最有价值的部分。原以为「公开 SDK 里有的 API 外部插件都能用」,
实测对照桥接模板后发现三处不符,站内已更正:
1. **`PluginMgr()` 被写成「仅内置可用」——错的。** 桥接模板第 692 行显式
`base.SetPluginMgrAPI(procPluginMgr{})`,公开 `PluginMgrAPI` 注释也写「外部插件可调用」。
真正的区别是**方法数**:公开面 3 个(ReloadOne/ListLoadedPlugins/IsPluginDisabled),
内部面 9 个。容易混淆是因为两个包里有同名但不同的接口。
2. **`Events()` 外部插件恒为 nil。** `SetEventSubscriber` 全仓只有定义、无调用点,
故 subscriber 从未被注入。外部插件的事件订阅实际由生成的运行时走
`events.subscribe` RPC 完成——旧文档把它当成可用入口,会让人写出必然失效的代码。
3. **`UnregisterOutputChannel` 是静默无效,不是报错。** 桥接只注入 registrar、
不注入 unregistrar,于是 `regOutputUnreg == nil`,函数命中 else 分支**直接返回 nil**
(sdk/plugin.go:539-549)——不报错、通道也没注销。
每条裁定的依据写进 `tools/apidoc/tiers.json`(文件:行号 或 grep 结论),
站上以告警框呈现,读者可自行核对。判断依据三源:桥接模板的 `base.Set*` 注入点、
`internal/sdk` 完整面、`internal/plugin/proc/protocol.go` 的 RPC 表。
## 检索(用户的核心诉求)
两套互补:
- **MkDocs 内置搜索**:全文,中文走 jieba 分词。
- **自建 API 检索**(`docs/javascripts/api-search.js` + `assets/api-index.json`):
支持四类查询——按名称、**按功能描述**(「注册工具」→ RegisterTool、
「崩溃」→ SetAutoRestart)、按 `限定符.方法`(`memory.recall` → MemoryAPI.Recall)、
按签名片段(`(string) error`)。并标出「仅内置」,避免外部插件作者踩空。
自建的理由:Material 内置搜索按整页文本索引,搜 `InjectText` 会列出所有提到它的
页面,但分不清哪条是它的定义;而且它要等 mkdocs build 才更新。
## 文档结构
- `docs/guide/`:快速开始、Go/Lua 首个插件、能力边界、打包发布、多平台、受限 SDK 与安全
- `docs/api/`:10 个按「你想做什么」划分的章节(工具/阶段/记忆/通道/配置/生命周期/
事件/LLM/常量/桥接)+ 仅内置汇总页
- `docs/versions.md`:SDK 版本语义(跟随内核中版本、patch 恒为 .0)、
1.0.0 是唯一破坏性变更、RPC 协议版本
## 验证
- `mkdocs build --strict` 零告警
- 23 个页面的全部站内链接与锚点可达(自动校验)
- 1440 / 768 / 390px 三视口:无横向溢出、无控制台错误
- 四种检索模式实测有结果且跳转锚点正确
- 构建产物 `site_build/` 已 gitignore
用法:`tools/apidoc/build.sh`(生成+构建)、`tools/apidoc/build.sh serve`(预览)。
|
2026-09-24 12:08:37 +08:00 |
|
|
|
e97cafc8de
|
license: SDK 改用 MIT —— 让第三方插件不被 AGPL 传染
原 LICENSE 是 2026-09-12(93ab794)引入的 AGPL-3.0-only 全文,README 据此
声明「插件静态链接 SDK,故必须以相同许可发布,闭源只能另取商业授权」。
这堵死了闭源插件,与第三方插件生态的目标相反。
## 为什么 MIT 才自洽
SDK 会随插件一起静态链接(源码进入插件二进制)。用传染许可,插件作者就被
强制开源;用 MIT,插件作者可自由选择许可(闭源 / 商业 / 私有均可),无需
回馈、无需任何例外或商业授权。这是刻意的宽松,也是插件生态安全的基础。
MIT 在这里**不会与任何许可冲突**:SDK 完全自包含 —— go.mod 零外部依赖,
sdk/ 只 import Go 标准库(sync),不引用核心仓任何代码(已实测确认)。
## 改动
- LICENSE:AGPL-3.0-only 全文(661 行)→ MIT 正文(21 行,版权人 JianFeeeee)
- README.md / README_EN.md:许可章节重写,把「必须同许可」改为「可自选许可」,
并写明自包含这一前提
内核仓仍为 AGPL-3.0-only(其 README 的对应论述与本提交同批更新)。
验证:go build ./sdk/... ./meta/... 通过。
|
2026-09-24 10:55:17 +08:00 |
|
|
|
5af2a86816
|
docs(sdk): 补全自动重启的真实约束(退避 / 上限 / 非无感)
原文只说「插件崩溃时平台自动拉起,保障服务可用性」,读起来像
无感瞬时恢复。实测与源码都不是这样:
## 补齐的默认参数(内核 internal/plugin/registry.go)
| 参数 | 值 | 含义 |
|---|---|---|
| procRestartBackoff | 1s | 第 n 次重启前等 n × 1s(线性退避) |
| procMaxRestarts | 3 | 窗口内重启次数上限 |
| procCrashWindow | 5min | 窗口内无新崩溃则计数归零 |
即实际序列 1s → 2s → 3s;同一 5 分钟窗口内第 4 次崩溃(n > 3)不再
自动拉起,交人工介入。首次重启就要等 1s,期间该插件的工具是缺席的、
调用会报错 —— 需要秒级就位的插件应在 OnStart 里自建重连与状态重建。
## 顺带改准一处混淆
`SetAutoRestart` 的文档写「崩溃后自动重载」—— 把「重启」说成了
「重载」。重载是换 plugin.bin 后重新加载那条路径(ReloadOne),
与崩溃自愈不是一回事。已在文档里写清,并附上退避与上限。
这个混淆与内核侧 HEAD 修正的 README/官网是同一处(源码注释里
「足够快到用户感知不到工具缺席」也是同一类无实测支撑的主观断言,
已在主仓同批改掉)。
中英双版同步更新。
|
2026-09-21 10:34:27 +08:00 |
|
|
|
7717bf5ca5
|
feat(sdk): RecallPolicy —— 声明工具输出是否触发记忆召回
与 ContextPolicy **正交**,但默认值刻意相反:
| | 管什么 | 默认 |
|---|---|---|
| ContextPolicy | **裁剪**:把低相关 L0 事件归档 | 关(剪裁是破坏性的,须显式声明) |
| RecallPolicy | **召回**:把 L2/L3 相关记忆注入本轮 | 输入/注入 `auto`,**工具 `none`** |
工具默认 none 的理由:多数工具输出是噪声,据它召回会把无关记忆拉进来
(源码注释原文)。需要「取回真实内容后据它召回」的工具才显式声明 auto。
## qq 的落地(本改动想解决的具体问题)
qq 通道到达的是**中断通知(meta)**而不是用户正文。原先用这条 meta 文本
去触发召回 —— 那是无关词,召不回真正相关的东西。改为:
- 通道声明 `RecallPolicy: none`(meta 不该据它召回)
- `qq_get_message` 声明 `auto`:消息正文取回后**由正文**触发召回
- `qq_get_history` 同样 `auto` + `prune`:拉回的历史消息既用完即裁、
又据正文召回(否则有「记忆里有、但拉历史时不注入」的盲区)
注:本实现已先随主仓 vendored 副本进入 main(两份经 diff 校验字节一致,
0 行差异),此处是把 SDK 仓自身补齐,使两仓 HEAD 对齐。
|
2026-09-20 09:15:34 +08:00 |
|
|
|
db207fd9b7
|
fix(scripts): 默认产物目录落到内核仓 dist/plugins
原默认 `$SDK_DIR/../dist/plugins`,在 SDK 仓位于 third_party/homeagent-sdk 时
会解析成 **third_party/dist/plugins** —— 既不在内核仓的发布产物目录
(upload_assets.py 认 dist/release 那套),也不在 SDK 仓内,等于丢在夹缝里,
必须每次显式传 OUT 才不会错。
改为向上找「含 go.mod 与 internal/ 的目录」即内核仓根,取其 dist/plugins。
不在内核仓内(SDK 被单独 clone)时回退到 SDK 仓自己的 dist/plugins。
不写死 ../../ 的理由:SDK 仓既可作 submodule 位于主仓内,也可被单独 clone,
写死相对路径会把产物丢到仓外。
|
2026-09-20 09:07:22 +08:00 |
|
|
|
953fbb2f50
|
feat(scripts): 批量打插件包(.hmap)供 release 发布
## 问题
release 此前**只发 homed/waiter 二进制与 hmapdev 工具链,不发插件包**(主仓 20 个
release、本仓的 release 都核实过,0 个 .hmap)。用户要用任何一个插件,都得:
1. 装 Go 1.25 + 网络拉依赖
2. 装 hmapdev 工具链
3. 进 example/<插件>/ 逐个 `hmapdev build`
而 `hmapdev build` **不是可选项**:5 个 example(deepsearch / luademo / vanblog /
vikunja / weather)连 `main.go` 都没有,直接 `go build` 会死在
「function main is undeclared in the main package」——入口是 hmapdev 现生成的。
所以「开箱可用」名不副实。本脚本把这一步前置到发布流程里。
## 用法
```bash
./scripts/build_plugin_bundles.sh # 全部 21 个
./scripts/build_plugin_bundles.sh weather qq # 指定
OUT=../dist/plugins ./scripts/build_plugin_bundles.sh
```
产出 `.hmap` + `SHA256SUMS.plugins`,可直接作为 release 附件。
## 实现要点(三处是踩过才写对的)
1. **产物有三种形态**,不能只认 `_bundle.hmap`:
- `<name>_bundle.hmap` 多平台 bundle(`plg.json` 的 `bundle: true`)
- `<name>_<os>_<arch>.hmap` 单平台(**qq** 的 plg.json 是 `bundle: false`,
与其余 20 个不一致)
- `<name>_lua.hmap` Lua 插件(**luademo**,不编译 Go)
脚本按形态标注,发布时能一眼看出是哪种。
2. **`set -e` 下不能用 `[ -z "$x" ] && x=$(ls ...)` 做兜底**:
一次 `ls` 无匹配(退出码 2)就会让整个子 shell 直接退出,后面的兜底根本走不到。
实测表现是「只有 qq 和 luademo 失败」——因为正是它俩没有 `_bundle.hmap`。
改成 `for` 循环 + `|| true`。
3. **参数拼错要报错,不能静默跳过**(否则以为打了实际没打)。
## 验证
- 21/21 构建成功,合计约 150 MB(bundle 含 linux/amd64 + darwin/amd64;
不含 windows 是**策略**——homed 已放弃 Windows 原生,源码注释有说明)
- 每个包内部结构核对:含 plugin.json、含内核认得的入口、当前平台可得
|
2026-09-20 09:02:31 +08:00 |
|
|
|
7ef9bc2ad3
|
docs(example): 为每个插件补 README
此前 example/ 下 21 个插件里,13 个完全没有 README,另 4 个是
`hmapdev init` 生成的脚手架样板(`# <name>` + `plugin build` + `Install` 三行,
等于从没被写过)。只有 deepsearch / vikunja / plugindev / luademo 是真实文档。
本次为 **17 个**插件写了真文档(13 个缺失 + 4 个样板),现在 21 个全部有内容。
## 写法
每个 README 覆盖:能力一句话 → 为什么需要 → 工具表 → 配置项表 →
通道与钩子(有才写)→ 构建 → 已知边界。
**事实全部从源码读出来,不推测**:
- 工具名核对到注册点(含 `tp+"x"` / `p.name+"_x"` 前缀拼接,展开成最终名)
- 配置键与默认值取自 `RegisterDef` / getStr 默认值
- 通道名、钩子名、依赖命令逐条 grep 确认
- 版本号与已部署实例交叉核对,17 个里 16 个一致
## 几处按源码写、与直觉不同的点
- **rss**:订阅时会把抓到的历史条目一次性标为 seen,所以订阅一个源
**不会**把历史文章全推一遍 —— 这是避免刷屏的关键,写进了文档。
- **files**:路径校验是**两道**(规范化后判断 + 解析符号链接后再判断),
只做前者的话沙箱里的软链接就能逃逸。两种情况报错文案不同。
- **qq**:身份必须**绑帧**而非存插件全局,源码注释记录了由此产生的两个真实故障
(中断抢占恢复后权限门整体失效、运行中到达的消息改写正在跑那一轮的身份)。
多来源合并时权限取**交集**。硬私有工具按前缀一律拒绝。这些是安全关键,
单独成节写清楚。
- **memo**:待办与备忘录**刻意分两类**(一提醒一不提醒),提醒注入带 NoMemory。
- **sanitizer**:不注册任何工具,只挂三个阶段钩子;依赖 ABI v2 的 stage 写回能力。
- **editdoc**:本目录是 v1.0.0(单工具),而线上跑 v2.0.0(全能版,源码未公开)——
在文档开头显式标注,**不按 v2 描述**,避免读者以为这里就是线上那份。
## 验证
- 21/21 文件非空且非样板(最小 913B,最大 6845B)
- 逐个核对 README 中出现的工具名能在源码找到依据;5 处报警经复核**全是误报**
(`ai_image_generate`/`music_*` 前缀来自 metadata 的 name,`on_input` 等是钩子不是工具)
- README 版本号 vs 线上 plugin.json:16/17 一致,editdoc 的差异已显式说明
注:本仓既有未提交改动(example/qq/plugin.go、sdk/plugin.go)**未纳入本次提交**。
|
2026-09-20 00:43:06 +08:00 |
|
|
|
cfa72df3e9
|
fix(qq): 权限身份改为绑帧,修中断抢占/运行中到达导致的串权与失效
问题(都是插件全局 p.auth 一份状态引起):
- 中断抢占当前轮并把现场压栈,中断轮收尾 afterOutput 清空全局身份;外层
恢复(resumeTask 复用同帧、不重跑 StageOnInput)后 auth.active=false,
beforeToolcall 在 !active 处直接返回 —— 该轮剩余工具调用**完全不受门**。
- 运行中到达的新消息会调 activateAuthContext 改写全局身份,把正在跑的那一轮
换成另一方的身份:换高即越权,换低即误拒。
改法:身份在 StageOnInput 绑定到本帧的 StageContext.Extra 上,beforeToolcall
以帧上身份为准(无绑定时才回退插件全局,兼容单测)。帧随中断栈一起压栈/恢复,
身份自然跟着走。
顺带:合并中断正文里的整批 message_id 现在全部消费(原来只清第一个,其余要等
generation 回收),新增 qqMessageIDsRe 支持 message_id=100,101,102 连写。
新增 4 条测试覆盖:中断恢复、运行中到达、整批 id 消费、非 QQ 轮不受门。
|
2026-09-14 16:45:09 +08:00 |
|
|
|
fe1c4cdb09
|
feat(qq): Bot 所有者消息升到 L2;普通消息保持 L1
jianf:所有者的话不该被普通人的消息打断/挤到队尾。
- 新增 interruptLevel(owner):owner → PriorityL2(一般提醒),其余 → PriorityL1(后台)。
一批里只要有一条来自 owner,整批按 L2 投递。
- 为什么不是 L3:L3 是时钟/终端那类"实时",QQ 是异步消息,抬到 L3 会打断真正实时的工作。
- 新增测试 TestOwnerMessagesGetHigherInterruptLevel 钉住 owner=L2 / 普通人=L1。
|
2026-09-14 16:31:27 +08:00 |
|
|
|
a01fe21ab1
|
feat(qq): 同一会话连续消息合并为一次中断 + 示例 SDK 指回仓库源码
需求(jianf):同一个人连发的数条消息应打包成一次中断,别逐条唤醒 Agent。
- debounce 合并:同一会话 + 同一发送者(群聊按 群号+QQ、私聊按 QQ)在
batch_window_ms(默认 1500)内的连续消息合成一批,每来一条重置计时;
整批不超过 batch_max_ms(默认 30000),避免对方持续刷屏时一直不投。
- n>1 时中断说明「短时间连续发来 N 条」并列出 message_id,建议一次
get_history 拿全上下文;n==1 沿用原文,行为与合并前逐字一致。
- 可配置 batch_window_ms / batch_max_ms,0 = 关闭合并(逐条投递)。
- Stop 时 flush 未到点批次,别把对方消息吞掉。
- 新增 4 条测试(同发送者合并 / 不同发送者不合并 / 窗口 0 逐条 / 单条沿用原文)。
顺带:deepsearch / vikunja 的 go.mod 与 plg.json 此前指向本地安装的 SDK 1.2.0,
导致无法用当前 SDK 重编(缺 InjectOptions.Priority)。改回 ../../ 仓库源码,
与其余示例一致。
|
2026-09-14 16:09:45 +08:00 |
|
|
|
f09891f054
|
fix(lua-sdk): 同步注入在 Lua 中明确标记为不可用(避免自锁)
sdk.inject_input_sync / *_sync_opts / inject_input_media_sync* 要等本轮回复,
而 Lua 代码只在持有插件锁的回调里执行 ⇒ 必然自锁。mock 不再假装返回
(reply,nil),改为与内核一致的明确错误,避免离线测试误以为可用。
|
2026-09-13 21:58:58 +08:00 |
|
|
|
efb396d7b3
|
feat(lua): Lua SDK 全量对齐 1.3.0 + hmapdev 单一 mock 源/版本标记/语法预检
内核侧 Lua 桥此前停在 v0.8.0 时代能力面,而 1.1/1.2/1.3 新增的
媒体、注入标志位、中断优先级、事件订阅、动态通道注销只在 Go 侧存在,
文档却宣称『能力完全对齐』——属于静默漂移。
本仓(事实源):
- 新增 sdk/lua/sdk.lua:Lua mock 的单一事实源,补齐全部新 API
(*_opts / inject_input_sync / inject_*_media / set_tool_blocks /
unregister_output_channel / events / plugin_mgr / insert_with_media /
sentence_text+media_digests / attachments / context_policy)。
- scripts/sync-lua-sdk.sh:把事实源同步到 hmapdev assets、luademo、
以及被 vendored 时的内核副本;三份 sdk.lua 不再各自漂移。
- hmapdev init --lua:优先从激活 SDK 拷权威 mock,内嵌模板降级为
assets/sdk.lua 回退,不再内联手写副本。
- hmapdev build(Lua):plugin.json 写入 SDK 版本(能力可追溯),
打包前用 luac -p / lua loadfile 做语法预检,失败以非零码退出。
- hmapdev debug --lua:优先用激活 SDK 的权威 mock(HMAPDEV_SDK_LUA)。
- luademo 升级为全能力示例(新增 luademo_probe_v2)。
|
2026-09-13 19:56:12 +08:00 |
|