|
|
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 |
|
|
|
4852d70d77
|
fix(qq): 输出工具不再受"当前会话身份"限制(修「可信 QQ 会话身份不完整」误拒)
现场(用户线上,驻留子联调回执原文):
被**子的中断**唤醒的一轮里,父 agent 调用 output_send__qq(meta 带齐 user_id)被拒:
「QQ 权限策略拒绝工具 output_send__qq:可信 QQ 会话身份不完整;请不要改用其他会话 ID 重试」
根因:`sessionToolArgsAllowed` 对**所有**工具都先要求"本轮能精确匹配可信 OneBot 事件"。
`onInputAuthContext` 在来源是 QQ 但匹配不到可信事件时会降权成
`auth = qqAuthContext{active: true}`(无 peer、非 owner)⇒ `currentPeer == 0`
⇒ 连**输出**也一并被拒 ✗。
但输出是 agent 的**主动调用**:发到哪个会话由它自己给的 meta(group_id / user_id)决定,
`handleChannelOutput` 已经强制要求该字段存在(缺了给明确报错)。再要求"当前会话身份"
是多余的门,而且会把合法发送一起挡掉 —— 设计上收到输入后可以往任意(已授权)通道
发任意多次。
改法:`output_send__qq` 在身份判据**之前**直接放行;「只能访问当前会话」这类限制
保留给**读取类**工具(get_history / mark_read / get_message)—— 那才真的不能跨会话读。
判据 `TestDowngradedAuthStillAllowsQQOutput`:
降权态下输出放行、读取类仍被当前会话限制挡住。
扰动验证:去掉放行分支 ⇒ 该判据报出与现场**一字不差**的那句拒绝。
线上实测:CLI 发起的轮次里 output_send__qq 返回 ok,插件日志 handleChannelOutput 确认送达。
|
2026-09-13 16:03:24 +08:00 |
|
|
|
63b6eafaf0
|
docs(sdk): 补 1.3.0 的新增能力与发版口径;main 路牌推到 1.4.0
用户指出:两个仓库的文档都没跟着更新。本仓的缺口:
1. `README.md` / `README_EN.md` 头部仍写「当前:SDK 1.2.0(需内核 1.2.0+)」,
版本表停在 1.2.0 —— 1.3.0 的两项新增能力(`InjectOptions.Priority`/`PriorityL1`–`PriorityL4`、
`UnregisterOutputChannel` 一族)在 README 里**一个字都没有**,而它们正是这一版
插件作者最需要知道的东西。
2. 发版口径没写清"**SDK 仓不发 patch tag**":核心 1.3.x 的后续 patch 不伴随 SDK 发版,
patch 位恒为 `.0`(§七.1)。这条以前只在规范里,README 没提,结果我自己在
2026-09-13 误发了 `v1.3.1`(已撤回);`v1.2.1` 是同一类历史遗留。
3. `meta/meta.go` 的版本语义注释还停在「现为 1.2.0:核心的 1.2.x 线正在发布中」,
与事实相反。1.3.0 既已随核心正式 tag 定版,该号归发布线所有 ⇒ main 推进到 **1.4.0**。
补写内容:1.3.0 能力小节(四级中断优先级、动态输出通道、通道名约束与那起
`device/<id>` 生产事故)、发版口径两段、版本表补 1.3.0 行、meta 注释与路牌。
|
2026-09-13 14:40:21 +08:00 |
|
|
|
21221f20c5
|
feat(example): plugindev 插件 —— 把 hmapdev 工具链封装成 Agent 可调用的工具
用户要求:把 SDK 的 hmapdev 额外封装为 HomeAgent 插件并装上。
## 为什么
hmapdev 原本是"给人/CI 用"的命令行。做成插件后,Agent 能自己完成
「新建插件 → 构建 → 安装 → 重载」全流程(配合已有的 plugin_install / plgreload):
plugindev_init → plugindev_build → plugin_install(path) → plgreload
## 工具面(5 个)
`plugindev_status`(可用性/版本/当前 SDK/工作区,排障首选)、
`plugindev_init`(脚手架,插件名约束 `[a-zA-Z0-9_-]{1,64}`)、
`plugindev_build`(在工程目录构建打包,返回产物路径与下一步提示)、
`plugindev_sdk`(SDK 版本 list/current/path/latest/install/use,`from` 支持本地源码)、
`plugindev_projects`(列出工作区已有工程与产物)。
## 安全边界(实现里落实)
- 只 exec **hmapdev 一个可执行文件**,不做 shell 拼接;
- `plugindev_build` 只接受含 `plg.json` 的目录 —— 这个工具不会变成"对任意目录跑构建";
- 子进程全部带超时;输出截断(6000 字符,保留首尾)后才返回,避免几百 KB 构建日志灌爆模型上下文;
- 工作区默认落在 `<data_dir>/plugindev`,配置可改。
## 现场验证
已在本机生产装上(`plugin_install` 带 path):5 个工具注册成功、
启动日志 `[plugindev] 就绪:hmapdev=/usr/local/bin/hmapdev 工作区=/home/newqqagent/plugindev`,
`hmapdev` 已装到 `/usr/local/bin/hmapdev`(版本 1.3.0)。
|
2026-09-13 14:16:46 +08:00 |
|
|
|
11303e3ee4
|
chore(example): a2a 1.3.0→1.3.1、acp 1.2.0→1.2.1(补 RegisterInputChannel 后升 patch)
上一提交(4cb3a0b)给这两个示例补了显式 `RegisterInputChannel`,但没有升版本号,
于是生产上"重装"出来的包与实际安装版本同名(1.3.0/1.2.0),既不好追溯也没法用版本比较升级。
本次只改版本号(`plg.json` 是仓库里的真源;`plugin.json` 由 `hmapdev build` 从它同步,不入库)。
实测:升级并 reload 后,内核启动日志里这两个插件的"只声明了出站通道"告警消失
(它们此前是真实缺口 —— 部署件是 9-11 的旧构建)。
|
2026-09-13 14:09:07 +08:00 |
|
|
|
f4f6968987
|
docs(sdk): 通道名会拼进 LLM 函数名,写明命名约束([A-Za-z0-9_-]{1,64})
生产事故(v1.3.0 部署后 agent 完全不应答)的根因之一就是这个约束没写清:
远程设备通道名 `device/<id>` 里的 `/` 让 `output_send__device/<id>` 无法通过上游的
函数名校验,上游对**整条请求**回 400(`Invalid 'tools[N].function.name'`),
网关 auto tier 全链条失败,内核只能报"所有 provider 都失败"。
这不是"某个工具不可用",而是**整个 agent 哑掉** —— 所以这条约束必须出现在
插件作者会看的地方(`RegisterOutputChannel` 文档 + README 的通道一节):
- 通道名只允许 `[A-Za-z0-9_-]`,且总长要留出 `output_send__`(13) 的余量;
- 名字若来自外部输入(设备自报 id 等),请在插件侧派生合规且唯一的名字。
内核侧**不做**净化/反解:通道名是插件自己的声明,就该由插件遵守契约。
|
2026-09-13 13:10:40 +08:00 |
|
|
|
da046b2520
|
feat(sdk): UnregisterOutputChannel —— 动态输出通道(远程设备)随资源生灭
背景:输出通道不止有"启动时注册一次"的静态通道。远程设备是动态的:
`device/<id>` 只在设备在线期间存在,设备掉线后必须注销 —— 不注销,
`output_list_channels` 会一直列着死通道,模型会往它发消息并拿到"发送已提交"式假回执。
- 新增 `OutputChannelUnregistrar` 类型 + `UnregisterOutputChannel(name)` +
`SetOutputChannelUnregistrar`(用 setter 而不是给公开的 `New(...)` 加参数,避免破坏调用方)。
- 与既有 `SetInputChannelRegistrar` 同一套注入模式:内核在装载插件时注入。
v1.3.0
|
2026-09-13 12:23:15 +08:00 |
|
|
|
4cb3a0bda4
|
feat(sdk): 通道方向契约落地 + 模板工程/示例插件显式登记 inputch + 生成器两处修正
## 背景:内核侧发现的真问题
在真实二进制压力测试里发现:插件只调 `RegisterOutputChannel("cli", ...)`,
却用同一个通道名 `InjectTextSync("cli", ...)` 注入输入 ⇒ 内核 inputch 登记表里
**没有**这个通道,"把 inputch 划给驻留子"直接失败(`划入 inputch cli: inputch 未注册`)。
根因是**契约没有落到插件与 SDK 面上**:inputch 是内核最基本的**输入路由单位**,
"谁会往这个通道注入输入"必须显式声明,而 SDK 文档没说清它与 RegisterOutputChannel
的分工,示例与模板工程也没有示范。
## SDK 面
- `RegisterInputChannel` / `RegisterOutputChannel` 的文档补齐**方向契约**:
入站(谁会注入)与出站(output_send__<name> 的回复发给谁)是分开登记的两件事;
凡是用 `InjectText*/InjectInput*/InjectInterrupt*(source, "<name>", ...)` 注入的
通道名都要 RegisterInputChannel。README 同步补了一段契约说明。
## 示例插件(全部补齐,之前只有 qq/weather 是对的)
`a2a`、`acp`、`browser`、`memo`:注入用 `p.name` ⇒ 登记 `p.name`;
`calendar`、`rss`:注入用字面量通道名 ⇒ 登记同名通道。
(这些插件此前是"能注入、但通道不在登记表里",与 cli 同类问题。)
## 模板工程(生成器 templates.go)
- `tmplPluginGo`:示范入站+出站两个方向(含 ChannelDef/NoMemory 说明与 `inputch 未注册` 的成因)。
- `tmplMainLua`:同样两个方向(`register_input_channel` / `register_output_channel`)。
- `tmplReadme`:新增 "Channels" 一节(方向对照表 + 兜底告警说明)。
- 实测:`hmapdev init` 生成的 Go/Lua 工程都含通道代码,Go 工程可构建打包出 `.hmap`;
`--lua` 工程同样生成通道代码。
## 生成器两处修正(都是实测踩出来的)
1. `sdk install --from <dir>`:install 原本只能从 Release 归档下载,而 SDK 开发期的新能力
(如 proc 桥要透传的 `InjectOptions.Priority`)还没发版 ⇒ 生成的工程必然编译失败
(`z_proc_gen.go: opts.Priority undefined`)。现在可用本地源码装一个版本并激活。
实测:`hmapdev sdk install --from <local sdk>` → 装成 v1.3.0 并激活 → 工程构建通过。
2. 构建前置校验 `sdkHasInjectPriority`:proc 桥模板需要 `InjectOptions.Priority`,
旧 SDK 没有时应给出**可执行**的报错(升级 SDK 或用 `--from`),
而不是把两条 `opts.Priority undefined` 编译错误甩给用户(那些错误指向生成物,
完全看不出是 SDK 版本问题)。实测:声明 sdk=1.2.0 的工程构建时正确命中该提示。
## 未决(发布期事项)
`InjectOptions.Priority` 属本特性线新增能力,**已发布的 SDK v1.2.0 不含它**;
发版时 SDK 版本需随之内含该能力(当前源码 meta 已是 1.3.0),否则外部开发者
按文档生成的工程会撞上上面那条守卫。
|
2026-09-13 11:37:36 +08:00 |
|
|
|
e50bffa34f
|
fix(deepsearch): 只关自己拉起的 SearXNG;条数改由插件侧截断
两个都由「真调用/真测试」暴露,且都会让线上搜索表现为「后端不可用」。
一、归属:接管不等于拥有(例:E2E 测试把生产后端带走)
旧实现只要探活成功就认领关闭责任 → 同机第二个实例(测试拉起的插件、另一个 daemon)
退出时就 docker compose stop 掉**线上正在用的**后端。实测:跑一次
`go test ./internal/plugins/ -run TestRealPlugin_DeepSearch`,teardown 即关停
127.0.0.1:8888,用户看到的就是「搜索后端起不来」。
修:只有真正执行过 `docker compose up -d` 的实例才算「我们起的」;探到已在运行只接管。
二、条数:SearXNG 不认 count/limit(count/max_results 形同虚设)
实测 ?count=3、?limit=3、不带参数返回**完全相同的 35 条**,所以截断必须在插件里做。
旧实现把 count 当 limit 参数发给 SearXNG 就以为生效了 → 模型每次吞 35~58 条带摘要结果,
还会把「命中 N 条」当成「拿到了 N 条」报给用户(实测发生过)。
修:新增 limitResults(默认取 max_results,上限 20);输出改成
「命中 N 条,返回前 M 条」;不再发无意义的 limit 参数。
验证:22 项单测全过、-race 干净、vet/gofmt 干净;两条归属测试做过扰动(把旧语义放回
去后必红,并如实打出它执行的 `docker compose stop -t 2`);内核 E2E 三条通过且
**跑完 healthz 仍 200、容器未重启**;线上 1.1.2 实测 count=3 → 「命中 40 条,返回前 3 条」。
版本 1.1.0 → 1.1.2。
|
2026-09-13 09:55:27 +08:00 |
|
|
|
934eb4da7d
|
feat(sdk): 导出 PriorityL4 —— 内核级插件的“立即打断”能力
L4 的归属此前写成“只有内核(panic/selfip)”,这是不完整的:**内核级插件**
(编译期内置插件,如 webui/cli/timer)也需要它来实现中断能力——最典型的例子
就是 WebUI 的终止按钮:用户按下时必须有一条能立刻打断当前任务的中断。
- 新增 `PriorityL4 = "L4"`,注释写明“仅内核级(内置)插件可用”。
- `InjectOptions.Priority` 的注释同步更正:取值 L1..L4,L4 属内核级插件,
外部插件声明 L4 会被内核夹到 L3。
为什么不能给外部插件:否则任何第三方插件都能随时打断用户的一切工作。
夹取有**两道闸**(纵深防御):
1. proc 桥(外部进程唯一入口)一律把 L4 夹到 L3——在这里夹是因为
`source` 是插件自报字段、可以冒名;
2. 内核侧再按 `IsBuiltinPlugin(source)` 判一次。
`source` 约定为 `插件名` 或 `插件名/实例`(webui/<deviceID>),判据取第一段。
兼容性:纯追加,零值仍等价于 L1。
|
2026-09-13 07:18:03 +08:00 |
|
|
|
4f4a03d368
|
feat(sdk): InjectOptions.Priority —— 插件声明自己中断的级别(L1-L3)
内核的输入调度器区分两类别:中断输入(可抢占)与排队输入(可被任何中断打断)。
中断的级别是“这项工作有多不能等”的声明,由插件在注入时给出:
p.sdk.InjectInterruptTextOpts(src, ch, text, sdk.InjectOptions{
NoMemory: true,
Priority: sdk.PriorityL2, // L1 完全可等 / L2 一般提醒 / L3 需及时
})
- 新增 `InjectOptions.Priority string` 与 `PriorityL1/L2/L3` 常量(纯追加)。
- 空/非法值一律降级为 L1(默认级)——拼写错误不会被静默当成别的级别。
- **L4 由内核独占**(panic 中断、内核事件中断 selfip),插件声明 L4 会被内核
夹到 L3,远端常量的取值域里也不提供 L4。
- 排队注入(InjectText*/InjectInputSync*)没有级别:它们本就是“不需及时处理”
的那一类,可被任何中断打断;传了 Priority 也不会生效。
- 贯通链路:sdk.InjectOptions -> proc RPC 参数(priority)-> 内核 payload;
tools/hmapdev 模板同步透传(三个注入的 6 个 Opts 变体共用 applyInjectOpts)。
- example/qq 显式声明 L1:QQ 消息既不是时钟那样的实时工作,也不是紧急工作。
兼容性:零值等价于旧行为(L1),既有插件无需改动。
|
2026-09-13 07:00:59 +08:00 |
|
|
|
4482235312
|
fix(vikunja): body 里的 ID 必须是 JSON 数字(建任务/指派在 v2 下必定 422)
线上真调用暴露:vikunja_task_create 把 project_id 当字符串发出 →
422 validation failed: expected integer at body.project_id。同类的还有指派,
而且 v2 的 assignees **根本不接受 username 字段**(422 unexpected property),
两个分支都是坏的。单测没盖到,因为从未发过真实请求体。
实测(vikunja v2.6.0,2026-09-12):
{"project_id":"1"} → 422 expected integer
{"user_id":"1"} → 422 expected integer
{"username":"jianf"} → 422 unexpected property
{"user_id":1} → 201 ✓
{"label_id":1} → 201 ✓(插件本来就 Atoi,无需改)
修法:
- taskBody:project_id 走 parseID(数字)
- 新增 resolveUserID:用户名 → 数字 id,查 GET /users?q=(v1 用 ?s=);
**只认精确匹配**,不做「只有一条就用它」的模糊兜底 —— 指派是写别人任务的动作,猜错人更贵
- assigneeBody:v2 只发 {"user_id":N},不再带 username
- task_assignees remove:路径也用解析后的数字 id
新增 5 项回归测试钉住请求体形状(数字 project_id / user_id、无 username 字段、
数字 ID 不查用户表、移除走数字路径、未知用户给可读错误)。
版本 1.0.0 → 1.0.1。线上复验:create(project_id=1, assignees=jianf) 不再报错、
add→list 显示 jianf、标签 add/remove 正常,测试数据已清理(任务/标签残留 0)。
|
2026-09-12 23:43:08 +08:00 |
|
|
|
4cf2df5be6
|
feat(vikunja): Vikunja 任务管理插件(28 工具)
token 走 password+Secret 配置项、每次调用前 ensure() 重读(换 token 无需重启);
v1/v2 差异在插件内处理(建任务 PUT/POST、改任务整对象/PATCH、搜索 ?s=/?q=、
标签对象/{label_id}、TimeEntry 无 seconds 语义);16 项单测 + 沙盒 homed 实测 28 工具全注册。
|
2026-09-12 23:16:33 +08:00 |
|
|
|
ebd700eaf9
|
fix(browser): browser_search 解析现代 Bing 版式,并把解析失败显式报错
旧实现三处叠加,模型只拿到「标题=来源行 URL 串、无摘要」,表现为反复换词重搜:
- www.bing.com 对程序化请求常回 302,拿不到结果块 → 改 cn.bing.com
- 块内第一个 <a> 当标题 → 抓到来源行 `deepin.orghttps://www.deepin.org`;改取 h2 > a,
并解开 /ck/a?...&u=a1<base64url> 跳转包装
- 摘要正则 <div class="b_caption">.*?<p> 对现代版式 0 命中(已迁到 p.b_lineclamp*)
- 分块不再用 (.*?)</li>:块内可能嵌套 <li>(deep links)会在错误位置截断
- 解析不出结果时明确报错,不再伪装成 "No results found."
夹具 testdata/bing_cn.html 为真实 cn.bing.com 响应裁剪;新增 5 项单测。
版本 2.4.0 → 2.4.1(2.4.0 = 交互式 timeout 改必填,此前已提交)。
验证:go test -race 全过、vet/gofmt 干净;线上实测返回真实标题+摘要+规整链接。
|
2026-09-12 23:16:23 +08:00 |
|
|
|
7c0b7a1fb0
|
feat(deepsearch): 联网检索插件 + SearXNG 生命周期托管
把原 websearch 示例改名为 deepsearch(目录/go.mod/plg.json/工具前缀/README 全量对齐,
工具名 websearch_* → deepsearch_*)。
新增 searxng.go:插件自己托管搜索后端
- 启动探 healthz:已在跑则直接接管(不重启),没跑就 docker compose up -d 并等就绪
- 关闭动作注册为 stop handler(幂等、限时 4s < 内核 5s 宽限期)
- 配置 manage_searxng / searxng_dir / stop_searxng_on_exit
- 崩溃/被 kill 时不关后端(下次启动接管):安全失败方向
- 可注入 cmdRunner + 时间预算,8 项单测离线覆盖接管/拉起/失败/幂等/保留
契约依据(internal/plugin/proc):停止插件 = plugin.stop → RunStopHandlers(LIFO、
幂等)→ Stop() → exit(0),宽限期 5s;stdin 关闭同路径。
验证:19 项单测(18 通过 + 1 联调跳过)、-race 干净、vet/gofmt 干净;内核 E2E 真调用
返回 58 条/37 条、58/58 带摘要;线上 daemon 实测 stop handler 与冷启动(约 3s)。
|
2026-09-12 23:16:23 +08:00 |
|
|
|
8c10b7ecc7
|
feat(browser): 交互式会话的 timeout 改为必填,并补参数校验与测试
此前 `timeout` 默认 10m:Agent 不传也能开会话,于是"忘记设时长"会静默拿到一个
10 分钟就自己消失的浏览器会话,排查起来像是浏览器不稳。
改为**必填**:
- 新增 `parseBrowserSessionTimeout`(空值 → "timeout is required;创建浏览器会话时必须
明确指定关闭时长,如 15m 或 2h";非法或 ≤0 → 明确报错),工具 schema 的
`required` 加上 `timeout` 并同步描述;
- 缺参时返回可读错误结果(而不是静默套默认值);
- 新增 `plugin_test.go`(62 行)钉住「不传 timeout 必须报错」等边界;
- 示例版本 2.3.0 → 2.4.0,顺带对齐结构体字段(gofmt)。
验证:`go vet ./...` 干净、`go test ./...` → ok(browser 模块自带 go.mod)。
|
2026-09-12 20:17:18 +08:00 |
|
|
|
fcb7490f63
|
feat(vscode): 插件工程调试扩展(plg.json 诊断 / SDK 版本 / 构建运行 / 内核日志跟随)
插件的真实形态是「独立子进程 + 内核侧握手」,所以插件问题几乎都在 IDE 之外发生:
编不出来(多半是没声明用哪版 SDK,工具链拿了存储里的 current)、编出来起不来
(产物与内核协议绑定)、起来了行为不对(真因只在内核日志里)。这个扩展把这三件事
拉进 IDE。
- `tools/vscode-hmapdev`:TypeScript 扩展(零运行时依赖,仅 devDeps: typescript + @types/vscode)
- **plg.json 诊断**:必需字段;`sdk` 必须是完整版本号(区间写法 `1.2` 报错并说明
「patch 位恒为 .0」,与工具链 ResolveSDKForProject 同一套规矩);声明的 SDK 未安装时
直接给 `hmapdev sdk install vX.Y.Z`;
- **状态栏**:`插件 · SDK <声明> · hmapdev <版本>`,工具链缺失/工程有错时变色,tooltip 列已装 SDK;
- **命令**:build / build --target all / clean / debug(解释执行)/ 工具链版本 / SDK 列表-安装-切换 /
跟随内核日志 / 停止跟随 / 刷新;
- **任务**:同一批动作注册为 `hmapdev` 任务 + Go 问题匹配器(编译错误进 Problems);
- **内核日志跟随**:读 `<dataDir>/log` 最新 `homed_*.log`,按插件名过滤持续输出;
- schema 校验 + plg.json 骨架片段。
- 诚实边界(写进 README):**不是源码级调试器**——没有断点/单步,没有 DAP 会话;
做的是构建、运行、看内核日志、清单校验。
验证:`tsc` 零错误;`node --test` 13/13(版本规则、诊断分级、SDK 列表解析、状态栏文本、
以及**反向核对抓到的真缺陷**:`hmapdev 未找到` 这类输出曾被解析成版本号 → 已要求版本
token 以数字开头,否则「工具链不在」会被显示成「工具链 <垃圾词>」并让 SDK 诊断失真);
与真实工具链输出的集成核对 PASS(`hmapdev version` / `sdk list` 的真输出解析正确)。
README 同时补两节:plg.json 的 `sdk` 字段语义(含「为什么必须有」与「为什么拒区间写法」)、
本扩展的用法与边界。
|
2026-09-12 16:07:34 +08:00 |
|
|
|
9206353858
|
feat(hmapdev): 项目声明 SDK 版本,工具链据此自动选(plg.json 的 sdk 字段)
此前项目里没有任何「我要哪版 SDK」的声明:go.mod 的 require 是个 Go 模块版本,
而工具链实际用的是存储里的 current——谁改过 current 就拿谁的版本编,出错时
表现为莫名其妙的编译错误(本轮就踩过:存储里只有陈旧的 v0.8.0,模板项目
首次构建报 undefined: sdk.InjectOptions)。
- `plg.json` 新增 `sdk` 字段:本插件针对的 SDK 版本。`hmapdev init` 生成时写入
**完整版本号**(如 "1.2.0")。
- `hmapdev build` 按声明的版本在本地 SDK 存储里定位:命中则用它并把 go.mod 的
require/replace 同步到该版本;未命中则报**可执行**的错误(列出已装版本 +
`hmapdev sdk install vX.Y.Z`),绝不静默退化成 current。
- **区间写法("1.2")被拒绝**并说明规矩:SDK 版本跟随内核中版本、patch 位恒为 .0,
一条内核线只有一个 SDK 版本(写区间会让人误以为同一条线里还能挑不同 SDK)。
- 产物 `plugin.json` 记录实际选中的版本(`sdk`),便于追溯「这个 .hmap 是哪版编的」。
- 显式 `--sdk-path` / `plg.json sdk_path` 优先(本机改 SDK 联调的路径),此路径下也尽力记录版本。
- 存量项目(plg.json 无 sdk 字段)行为不变,向后兼容。
顺带修一处自相矛盾:解析出 1.2.0 之外的版本时,原先只改 replace 而 require 保持旧版本,
一旦有人删掉 replace 就会静默用回旧 SDK 编译(`go list -m` 报的也是假版本)。
验证:单测 10 例(精确命中/带 v 前缀目录/区间写法被拒并说明规矩/未命中给可执行命令/
空存储给安装指引/非法值拒绝/杂项目录不干扰)+ 反向验证(把版本比较退化成字典序,
「1.2.x 取最新」用例立刻变红,证明判据能发现缺陷)。
E2E:init → plg.json `"sdk": "1.2.0"`;build → 精确解析、go.mod require/replace 一致、
产物 plugin.json 记录 sdk;声明不存在的版本 → 可执行报错;无 sdk 字段 → 照旧构建。
|
2026-09-12 15:51:23 +08:00 |
|
|
|
83a54f321e
|
fix(hmapdev): 工具链能报出版本(此前 -ldflags -X meta.Version 静默无效)
两个缺口叠加:既没有 `version` 子命令,构建时的
`-ldflags "-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=…"` 也因为
**meta 包根本没被工具链引用**而完全不生效(链接器不会保留未被引用的符号,
rodata 里连那个字符串都没有)——于是「手里是哪一版 hmapdev」无从判断,
而插件产物与内核是协议绑定的,这恰恰是最需要判断的一件事(v0.8.0 SDK 陈旧目录
导致模板编译失败那次就是靠猜)。
- main.go 引用 meta 包并新增 `version`(以及 `-v/--version`)子命令,
输出工具链版本 / SDK 模块 / 构建提交 / 构建时间 / 构建用 Go / 可执行文件路径;
- 未注入(源码默认值)时也照常报,unknown 字段不打印(避免噪声);
- usage 里补上 `hmapdev version`;
- 加测试钉住两件事:注入值必须出现在输出里(-X 一旦失效立刻变红)、
未注入时也要能报出源码默认版本。
验证:`go build` 默认输出 1.3.0(主干默认值);`-X …meta.Version=1.2.1
-X …meta.Commit=abc1234` 后输出 1.2.1 + 提交号,且二进制内精确匹配到该串
(说明注入真的进了镜像);`go test -run PrintVersion` 2/2 PASS。
|
2026-09-12 15:08:56 +08:00 |
|
|
|
b93fe6b878
|
chore(version): main 路牌推到 1.3.0(v1.2.0 已从 release/v1.2.x 发出)
按版本纪律:release/v1.2.x 停在 v1.2.0 的发布提交(140cd34),主干 meta.Version
永远是「下一个未发布中版本」。
|
2026-09-12 14:16:46 +08:00 |
|
|
|
140cd34b56
|
fix(package): 示例构建改用宿主工具链,跨平台不再 Exec format error
`build.sh all all` 此前必然失败:示例的跨平台是由 hmapdev 的 `--target` 完成的,
被执行的进程必须在当前机器上跑,而 build_examples 传的是**目标平台**那把工具链
→ darwin 目标下拿 darwin 二进制在 linux 上跑,报
"cannot execute binary file: Exec format error"(linux/amd64 之后的所有目标全灭)。
- 宿主平台在脚本顶部、**export GOOS/GOARCH 之前**取定(`go env GOOS` 在 export
之后会返回目标平台,这正是原 bug 的成因),并用 `env -u GOOS -u GOARCH` 兜底;
- `all` 的第一个目标可能不是宿主平台 → 缺宿主工具链时先补建一次;
- 宿主工具链仍缺失则**显式报错并给出该跑哪条命令**,不再静默退化成 Exec format error。
验证(VERSION=1.2.0 bash package/build.sh all all):
linux/amd64、linux/arm64、darwin/amd64、darwin/arm64 四个目标示例产物均 17/17 成功
(修复前 darwin 两个目标 0/17);windows 目标仍按设计显式拒绝
(协议 2 的统一共享内存区未移植 Windows,走 WSL)。
v1.2.0
|
2026-09-12 14:15:20 +08:00 |
|
|
|
b237787c90
|
refactor(toolchain)!: 工具链 plugindev 更名为 hmapdev,module path 改回 gitcode
- 目录 tools/plugindev → tools/hmapdev,可执行文件名/平台产物名同步
(hmapdev_linux_amd64 等;包格式仍叫 .hmap)
- module path github.com/JianFeeeee/homeagent-sdk/tools/... → gitcode.com/...
(与仓库实际托管一致;核心仓不依赖该 path,改动无外部影响)
- SDK 存储目录 ~/.homeagent/plugindev/sdk → ~/.homeagent/hmapdev/sdk
新目录不存在而旧目录存在时沿用旧目录 → 已装 SDK 版本不会丢失
- 命令表/usage/--help/生成项目 README/示例 README/NSIS 安装器/
package/build.sh/build-examples.sh 全部同步;PLUGINDEV 环境变量保留兼容
- sdk/ 目录零改动(公开接口不变)
验证:
- go build ./... ok;go test ./tools/hmapdev/ ok(含模板接线守卫 TestProcTemplate_CoversAllCoreMethods)
- bash -n package/{build,build-examples}.sh ok
- 端到端:hmapdev init demo && hmapdev build → dist/demo_bundle.hmap(linux+darwin)
- 本机安装 /usr/local/bin/hmapdev,旧名以软链保留;sdk list/current 正常
|
2026-09-12 12:30:20 +08:00 |
|
|
|
69ff3089a4
|
fix(mocksdk): 补上漏掉的 InjectInputSync,恢复与公共 SDK 的同构
mocksdk 自己的注释立了规矩:「插件在 yaegi 下调得通的方法,编成 plugin.bin 后
必须也调得通,否则调试期与真实运行行为不一致」。但这个 mock 一直没有旧的三参数
`InjectInputSync`(`git log -S` 可证并非本次引入),而那正是通道类插件
(qq / a2a)完成「收到入站 → agent 处理 → 回复取回」闭环要调的方法:
- 编译成 plugin.bin:能用(实现在模板里)
- 在 yaegi 下调试:方法根本不存在
§九 早就点过 mocksdk 是最容易悄悄漂的一处(它没有任何代码对着编译,编译器抓不到;
上次漂的是 `Triple.Predicate` vs 公共 SDK 的 `Relation`)。
本次做的是**机械比对**而不是凭印象:抽出公共 SDK `IOInjector` 的 14 个方法名
与 mock 的方法集求差,差异恰好只有 `InjectInputSync` 一个,补上后差集为空。
mock 仍可编译,plugindev 测试全绿。
|
2026-09-12 09:34:51 +08:00 |
|
|
|
e839eb8220
|
test(plugindev): 模板接线守卫区分「漏接线」与「刻意保留的兼容面」
TestProcTemplate_CoversAllCoreMethods 红了:它要求模板出现内核提供的**每一个**
method id,而注入标志位落地后模板不再发 "io.injectTextNoMem"。
这不是漏接线,是刻意的向后兼容面:
- **旧模板确实发过它**(可复查:ba49dfd 之前的 templates/proc_main.go.tmpl 里,
InjectTextNoMemory 拼的就是 "io.injectTextNoMem"),所以内核必须继续接受
那时编出的插件二进制;内核侧的注释也写明「旧 RPC 语义就是『不进记忆』」。
- 当前模板改成走 "io.injectText" + `InjectOptions{NoMemory: true}`,两条路径
语义等价,没有理由再发旧 id。
若把「内核有、模板就必须发」当不变量,这条守卫会常驻误报——常驻误报的守卫
迟早被习惯性忽略,那时**真**漏接线就没人看见了。故:
- 从 required 移出该 id,改为显式的 `deprecated` 表(每条写出保留原因);
- 加**反向保护**:allowlist 里的 id 一旦重新出现在模板里就报错,提示该条目已过期,
避免这个表退化成「永久豁免」的垃圾抽屉。
实测:模板接线测试由红转绿;把 id 临时塞回模板,反向保护如期报出
「条目已过期,请从 deprecated 移除」,恢复后再次全绿。
|
2026-09-12 09:33:00 +08:00 |
|
|
|
d893bfa76f
|
docs: README 更新到 SDK 1.2.0(注入行为 / ContextPolicy / 重编要求)
README 此前停在 SDK 1.1.0,而且 1.2.0 的新增接口**一处都没写**。本次补齐:
- 「版本与兼容性」:当前版本改为 1.2.0(需内核 1.2.0+),并新增
**1.1.x → 1.2.x 必须重编**的说明——接口是纯追加,但插件运行协议升到 2
(统一共享内存区 fd3 布局改变,不支持滚动升级),旧 plugin.bin 会因协议
版本不匹配被拒绝(错误明确提示用配套 plugindev 重编,不静默降级)。
这与 1.0.x→1.1.x「不需重编」形成对照,必须写清楚。
- 新增「注入行为与上下文裁剪(1.2.0)」章节:`InjectOptions{NoMemory,
ContextPolicy, CleanerName}`、六个 `*Opts` 变体、`ContextPolicyNone`/`Prune`
取值、以及三条要点(零值等价于旧三参数方法 / 裁剪必须显式声明 /
裁剪先经注册的 Cleaner)。签名逐个从 sdk/plugin.go 抄录,未凭记忆书写。
- 「示例插件」补一句:release 附带预编译示例 `.hmap` + SHA256SUMS/MANIFEST,
理由是插件二进制与内核协议绑定,只发工具链容易让人拿旧产物去装而握手失败。
中英双份同步更新。
|
2026-09-12 09:27:15 +08:00 |
|
|
|
93ab794a82
|
docs(license): SDK 采用 AGPL-3.0-only,并补许可章节
本仓此前**没有任何许可文件**,README 里也没有许可声明。现补:
- `LICENSE`:GNU Affero 通用公共许可证第 3 版官方全文(gnu.org 正本,
661 行 / 34523 字节 / sha256 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0)
- README.md / README_EN.md:新增许可章节
选 AGPL-3.0-only 的理由:GPL 家族里传染性最强的一档,且不允许选后续版本。
对插件开发者的实际含义已写进 README:SDK 随插件静态链接(源码进入插件二进制),
插件因此是本 SDK 的衍生作品,必须以相同许可发布;AGPL §13 也覆盖网络交互,
通过 HTTP/WebSocket 提供服务的插件同样要向使用者提供源码。
第三方(Go 依赖 go-sqlite3 / gojieba / bubbletea 等,MIT / BSD-3 / Apache-2.0)
保持各自许可;平台侧模型与运行时(Chinese-CLIP Apache-2.0、ONNX Runtime MIT)
不属于本 SDK。
|
2026-09-12 09:16:20 +08:00 |
|
|
|
12cabcb290
|
fix(meta): SDK main 的路牌回到 1.2.0 —— beta 不发 SDK
我上一提交(44bd915)把 SDK main 从 1.2.0 推到 1.3.0,判据用错了。
当时照搬的是 核心仓 e4be966 的先例(§七.4「两仓 main 都是下一个未发布中版本」),
但那个先例的前提是**核心那一版已经正式发布过**:当时 v1.1.0 / v1.1.1 都已打 tag,
main 才推进到 1.2.0。
而按 §七.2,**beta 不伴随 SDK 发版**:SDK 1.2.0 要等核心的**正式** tag 才定版、
建 release/v1.2.x、打 tag(§七.3)。在此之前 1.2.0 仍然是 SDK **尚未发布**的中版本,
所以「下一个未发布中版本」就是 1.2.0 —— 推到 1.3.0 等于宣称 1.2.0 已经存在。
核心 main 是 1.3.0 并没有错:核心切出 release/v1.2.x 后,1.2.0 就归发布线所有。
**两仓在这个阶段故意不对称**,已在 meta.go 注释里写明,避免再被「对齐」回去。
|
2026-09-12 09:00:19 +08:00 |
|
|
|
44bd915fbf
|
chore(meta): SDK main 的版本路牌推到 1.3.0
按 核心仓 docs/git-branching.md §七.4,两仓 main 都遵守 §2.1:meta.Version 是
**下一个未发布中版本**。核心 1.2.x 线已开(release/v1.2.x 承载 1.2.0),所以
两仓 main 一起指向 1.3.0——核心仓同一时刻也做了同样的推进(chore(meta))。
它标记「main 正在积攒 1.3 的东西」,不表示 1.3.0 已经存在。
SDK 1.2.0 的定版与 tag 不在现在做:按 §七.3,SDK 的 release/v1.2.x、meta.Version
定为 1.2.0、tag v1.2.0 都与核心的**正式** tag 一起执行(beta 阶段不发 SDK)。
|
2026-09-12 08:24:44 +08:00 |
|
|
|
ba49dfda44
|
feat(sdk): 注入行为的记忆/裁剪标志位(纯追加)+ plugindev 退出码修复 + 示例 hmap 随发版
## 1. 注入标志位(公开 API 纯追加,无签名变更)
给注入行为补上工具早已有的两类声明位,并让通道定义也带上:
- `InjectOptions{NoMemory, ContextPolicy, CleanerName}`
- `IOInjector` 新增六个 `*Opts` 变体(排队/中断/同步 × 纯文本/带媒体)
- `ChannelDef.ContextPolicy`,并**补上 JSON tag**(Cleaner 标 `json:"-"`)
零值 InjectOptions 与旧的三参数方法完全等价(记入记忆 + 不裁剪),
存量插件不需要改一行、也不需要重编;旧方法保留为转发到零值 opts 的语法糖。
三条设计要点:
- **默认不裁剪**:裁剪会归档丢弃低相关事件,必须显式声明(ContextPolicy=prune)。
- **中断也允许声明 prune**(已确认):中断同样携带内容进上下文。
- `CleanerName`:注入的 source 未必是注册过的输入通道名,而注入内容常带
ANSI/JSON 包装;允许显式指定用哪个已注册 cleaner 清洗。
顺带修掉一个易静默丢字段的坑:`ChannelDef` 原来没有 JSON tag,只能手写字段
白名单跨进程传(`{"NoMemory": ...}`),新增字段会被丢掉。现在模板整体传 `def`。
## 2. 示例调用点统一写明意图
rss/memo/calendar/qq 的中断注入显式 `NoMemory: true`(行为等价,写清语义)。
## 3. plugindev 出错却 exit 0(真缺陷)
`buildBundle`/`buildTarget` 遇错只 Printf 后 return,`cmdBuild` 返回 void,
于是**构建失败也退 0**。实测中一个示例的 windows 目标编译失败,批量脚本却报
「17/17 全绿」,并因此少产出 16 个 .hmap。现在累计 `buildFailed` 并以非零退出。
## 4. 平台策略:插件目标去掉 windows
homed 已放弃 Windows 原生(见核心仓 cmd/homed/platform_windows.go:插件体系依赖
fd 继承 + 统一共享内存区的段内偏移解引用,Windows 句柄模型无法表达),
插件只运行在 homed 能跑的平台上,故 `allBundleTargets` 去掉 windows,
并对 windows 目标给出**可执行的报错**(指引 WSL2),而不是让它死在一句
`undefined: attachUnifiedShm` 上。
## 5. 发版附带各示例插件的 .hmap
新增 `package/build-examples.sh` 并接入 `package/build.sh`(组件 all|plugindev|examples):
- 用**刚构建出来的**那把工具链构建示例,保证与本次发版同源
- 逐平台 `--no-bundle --target <os/arch>`(bundle 会连 windows 一起编)
- 判成功同时看**退出码 + 产物存在**
- 全部产物齐了才 `sha256sum`(边打边算会漏掉后生成的包)
- 有任一失败即整体失败,不生成 SHA256SUMS
## 6. 版本
SDK 仍为 1.2.0(main 是下一个未发布中版本);1.2.0 条目补记本次新增接口,
并注明新标志位需核心 1.2.0+(旧核心会忽略这些字段,不报错但不生效)。
|
2026-09-11 20:29:30 +08:00 |
|
|
|
b2eafdf885
|
refactor(sdk): 移除 MediaAttachment.Description,媒体不再以文本描述参与索引
描述式索引是把图片将就成文本的机制,已在内核侧彻底拆除:
- MediaAttachment 不再携带 Description:媒体只按自己的原生向量被
检索与召回,不生成、不检索、不持久化任何描述文本;
- InsertWithMedia 的语义随之收敛为「媒体成为文档直接持有的一等块」,
文档向量融合这些块的原生向量,图片按图本身被召回;
- 往返测试同步去掉描述字段,只断言 digest/MIME/name/data 不损坏。
注意:这是公开 SDK 的破坏性字段删除(有意为之,媒体描述链路整体废弃),
不是加法式变更。
|
2026-09-11 11:43:35 +08:00 |
|
|
|
8c397ecf65
|
feat(plugindev): doc/knowledge 大正文走共享内存 + 协议版本 bump 到 2(§13.13)
- doc.insert / doc.insertWithMedia:doc_ref / attachments_ref
- knowledge.add:content_ref
- procProtocolVersion 1 → 2
bump 的理由:这两处改了内核→插件的 payload 承载方式,两种错配都静默失效
(v1 插件遇 v2 内核拿到空参数;v2 插件发 blocks_ref 给 v1 内核被静默忽略)。
双方都是等值校验,bump 后 v1 插件遇上 v2 内核会在建链时显式报错并带出
“请用配套 plugindev 重编”。
配套内核侧 50ba4a6。
|
2026-09-10 23:26:21 +08:00 |
|
|
|
632f6743d3
|
feat(plugindev): 媒体块经共享内存传递(§13.13)
SetToolBlocks / InjectInputMedia / InjectInputMediaSync /
InjectInterruptMedia 原先把 blocks 内联在 RPC 报文里。本地生成的图/音频是
base64 data URL(一张图可达数 MB),内联时整份要在报文里再编码再拷贝一遍;
更关键的是内容本体不在共享段里,插件回调无法就地改写。
改为经 putValueInArena 传 blocks_ref,内核 resolveBlocks 读回;小 payload
仍内联。
同步调用用 mediaArgsOwned 返回的释放函数延迟释放:injectMediaSync 要等
应答,槽不能在应答到达前回收,否则内核读到已释放内存。
配套内核侧 15d912e(实现 setToolBlocks 桩 + resolveBlocks)。
|
2026-09-10 22:46:41 +08:00 |
|
|
|
9d930db4ea
|
feat(plugindev): output.invoke 模板从共享帧读参数(§13.6)
内核侧 OutputInvokeParams 新增 Frame/ArgsLen,payload 不再内联在 RPC 报文里。
模板必须跟着读帧,否则 frameInput 拿不到参数、输出通道收到的 payload 为空——
生产插件(如 qq)走的正是模板,模板不改这个改动就等于没做。
无帧时回退内联 args,保持对直连 RPC 调用方的兼容。
|
2026-09-10 21:28:56 +08:00 |
|
|
|
0a164fe4b9
|
feat(qq): qq_get_message 声明 ContextPolicy=prune(§13.8 验证项)
消息正文只在当轮需要(决定怎么回复),用完即裁剪。不裁的后果是每条 QQ
消息的完整正文都留在 L0 上下文里,长会话下持续挤占 token 预算。
内核侧 §13.8 早已就位:StageAfterToolcall 之后按 ToolDef.ContextPolicy
调 RelevanceContext.Prune(772a494),但 QQ 侧一直没声明,等于功能空转。
|
2026-09-10 21:18:16 +08:00 |
|
|
|
fd5a291df1
|
feat(qq): 权限门 + 单轮循环保险 + 极简发送回执 (v1.4.0)
回应 problem.md:核心把「发送成功」的富回执喂给模型,模型读成
「这步成功,继续下一步」而重复调用 output_send__qq。
- handleChannelOutput 成功只返回 "ok",不回传 NapCat 原始响应(含 message_id)
- 全局权限门 on_input/before_toolcall/post_action:工具白名单、私人资源边界、
高风险命令 confirm 校验;before_toolcall 的 Response 只用于拒绝单个工具,
post_action 负责清除以免被内核误当作结束推理的最终响应
- 单轮循环保险:max_qq_tool_calls=200 / max_qq_output_calls=20 /
max_duplicate_qq_send=1,只拦参数完全相同的重复调用,不误杀必需的多次调用
- plg.json 1.2.0 → 1.4.0
|
2026-09-10 20:36:43 +08:00 |
|
|
|
5175e7d6e0
|
refactor(plugindev): 模板适配 funccall 调用帧
工具调用/清洗由内核发起,内核标定一块内存帧交给插件(callee),
插件在帧内工作;只有结果超出内核预留预算时才向内核申请扩容块。
- 新增 frameInput/frameOutput/arenaPut:读写调用帧、按需扩容
- tool.invoke:从 frame[0,args_len) 读参数;结果优先写帧结果区,
放不下才 arena.alloc 扩容并打 sharedRefFlagExpand
- cleaner.invoke:同一帧模型(frame + input_len)
- SharedRef.Flags 语义位与内核对齐(JSON / EXPAND)
- 防漂移测试更新为 frame/args_len/result_ref
|
2026-09-10 18:54:40 +08:00 |
|
|
|
71e3325439
|
refactor(plugindev): 模板改用内核独占共享槽池 + ContextPolicy 字段
共享内存是内核内部实现,不对插件开发者暴露。模板不再维护任何分配
游标(历史上 bump 游标 / 模板内位图 CAS 两版都因把可变分配状态放在
共享内存里而出竞态),改为通过内核 RPC 申请/归还:
- 删除 arenaWrite 本地 bump 分配器与 arenaOff/arenaUsed 全局变量
- 新增 arenaAlloc/arenaFree:走 arena.alloc / arena.free RPC
- 新增 putInArena/callWithText:按 payload 大小自动选择共享槽或内联
JSON(inlinePayloadLimit=512),SDK 公开 API 仍是普通字符串/Map,
插件开发者无感
- cleaner.invoke 改为读 TextRef/内联 Text、写 RespRef/内联 Text;
插件不做任何分配(内核预分配请求槽 + 响应槽)
- 同步 handshake:不再解析 arena 槽池布局(偏移与大小由 arena.alloc
的应答下发)
测试:
- 新增 TestProcTemplate_UsesKernelArenaRPC:模板必须调用 arena.alloc/
arena.free,且不得再出现 arenaUsed/arenaWrite(回归保护)
- TestProcTemplate_RejectsVersionMismatch 更新为 §13.1 后的「统一区域
魔数不匹配」文案
- 修正模板与 sdk/plugin.go 的 gofmt 对齐(含补上 ContextPolicy 字段)
|
2026-09-10 17:58:06 +08:00 |
|
|
|
18fec9b003
|
feat(shm): Cleaner SharedRef + ContextPolicy (§13.4/§13.8)
- cleaner.invoke 协议: TextRef SharedRef 替代内联 text
- 插件侧 arenaWrite/SharedRef 读写辅助
- ToolDef.ContextPolicy 字段(默认 none / 可设 prune)
- 握手解析统一区域 SuperBlock + arena 元数据
|
2026-09-10 16:04:55 +08:00 |
|
|
|
fc236120e3
|
feat(shm): 统一共享内存区域(§13.1) — 子进程侧模板适配
- proc_shm_unix.go.tmpl: fd 3 = 统一区域,fd 4 = eventfd
- proc_main.go.tmpl: 握手解析 SuperBlock,从 ctxOff/evtOff 定位两段
- 新增 unified region 常量(magic/version/offset) + StageContext 内部布局常量
|
2026-09-10 11:46:12 +08:00 |
|
|
|
a66739e59b
|
docs: README 顶部补版本兼容性表与并发约定,下载链接升到 v1.1.0
两件事此前没写进 README,会让读者拿到错的现状:
## 版本兼容性表
README 开头没有版本号、没有兼容性说明,读者无从判断「我这个版本能不能用新接口」。
补一张「内核版本 ↔ SDK 版本」表,说清 patch 位恒为 .0 的语义,以及
1.0.x 升 1.1.x 不需要改代码也不需要重编(新增是「插件调用、内核实现」方向,
不调就不受影响;实测用 SDK 0.9.2 编的旧 plugin.bin 在新内核上直接建链通过)。
## 并发约定
PluginSDK 是被多个 goroutine 同时使用的共享对象,这一点此前没有写在明处。
列出 SDK 已保证的(访问器/注入/注册/handler 幂等)与开发者必须自己保证的
(StageContext 字段全导出,并发读写要自己持锁;Extra 的 map 并发写是直接 fatal)。
并顺手把下载链接从 v1.0.0 升到 v1.1.0——照旧链接去 release 页面会找不到
v1.1.0 的产物,因为那条 curl 用的是 v1.0.0。
|
2026-09-06 11:35:09 +08:00 |
|
|
|
da01af1ad7
|
chore(meta): main 的版本路牌推到 1.2.0
按 核心仓 docs/git-branching.md §2.1,两仓 main 的 meta.Version 都是
**下一个未发布中版本**。1.1.x 线正在发布中,所以 main 指向 1.2.0。
SDK 版本跟随核心的中版本(§七.1):release/v1.1.x 上定版 1.1.0,
整条核心 1.1.x 线共用它。
**此 commit 不 cherry-pick 到发布分支**(§五)。
|
2026-09-06 10:04:26 +08:00 |
|
|
|
741e284cd4
|
Merge branch 'feature/sdk-multimodal' — 多模态贯通插件边界
公开 SDK 新增媒体字段与媒体注入接口(全部新增,无签名变更),
修掉 PluginSDK 的 API 字段与 autoRestart 两处并发竞态,
工具链(proc 模板、mocksdk、方法清单断言)同步接线,README 中英双语补文档。
存量插件零改动零重编。
|
2026-09-06 10:04:26 +08:00 |
|
|
|
ce5bff9275
|
feat(sdk): 多模态贯通插件边界——媒体字段、媒体注入接口与并发修复
记忆系统在核心 1.1.0 支持了二进制多媒体节点,但那条链路只对**内核自己**开放:
插件把 Triple / Doc 交进来,媒体一律无处安放,且**不报错**。本版补上公开接口
侧缺失的表达能力。
## 一、类型与接口(全部新增,无签名变更)
- `Triple` += `SentenceText`、`MediaDigests`
- `Doc` += `MediaDigests`、`Attachments`;新增 `MediaAttachment`
- `TextEvent` += `Attachments`
- `DocMemoryAPI` += `InsertWithMedia`
- `IOInjector` += `InjectInputMedia` / `InjectInputMediaSync` / `InjectInterruptMedia`
- `PluginSDK` 补上一直缺失的 `SetToolBlocks` 包装(接口里有、便捷方法里没有,
插件只能自己去拿 injector)
`MediaAttachment` 一个类型服务两个方向:给 `Data`+`MIME` 是新内容(内核按字节
去重),只给 `Digest` 是引用已有内容。读路径**只回元数据不回字节**——一次检索
可能命中几十份媒体,把字节全塞回来会撑爆跨进程消息。
媒体注入为什么不能搭 `SetToolBlocks` 的车:那个方法只在工具处理函数内部可用,
且媒体要等**下一条** tool message 才到模型手上。插件主动发起一轮带媒体的对话、
以及中断注入,需要各自的签名,且媒体在**本轮**就随消息发出。
`Triple.MediaDigests` 非空而 `SentenceText` 为空时,内核会用媒体标记本身充当句子
——媒体引用挂在句子上,没有句子就无处挂起。插件只需填 digest,标记由内核拼:
要求调用方知道格式,等于让一个拼写错误静默切断引用绑定而全链路无人报错。
## 二、修掉两处并发竞态
`sdk/stress_test.go` 的 `-race` 实测报 11 处 DATA RACE,收敛到两个字段:
1. **`PluginSDK` 的 API 字段无锁**。写方是内核(加载/重载插件时依次注入
injector、memory、doc、llm…),读方是插件在 `Start()` 里起的后台 goroutine
——轮询、监听、定时器都要拿 injector 往管道注消息。生产表现是插件重载瞬间
偶发崩溃:读到半个接口值就 nil 解引用。
2. **`autoRestart` 标志无锁**。`SetAutoRestart` 的文档用法本身就是「外部连接建好
后再决定能否自动重启」,而连接建立通常在后台 goroutine;内核 registry 在另一个
goroutine 读 `AutoRestart()` 决定崩溃后重启策略。这对读写天然跨 goroutine。
加 `apiMu sync.RWMutex`。关键约定写进注释:**只在持锁期间取字段值,取完立刻
释放再调用**。持锁调用会把 `InjectInputSync`(阻塞到 agent 回复,可达数分钟)
与 `SetIOInjector` 串到一起,让插件重载卡死。
## 三、压测(sdk/stress_test.go,13 例)
SDK 是被多个 goroutine 同时使用的共享对象,单线程单测全绿不代表并发路径成立。
断言的是不变量而非吞吐:
- 媒体注入高并发不丢不串——每次调用带唯一 tag,逐条校验文本与图片 URL 配对。
「不串」是重点:若实现里出现任何共享中间状态(把 blocks 暂存到字段再读出),
高并发下会出现 A 的文本配 B 的图,而两者单独看都「成功」了;
- injector 热替换(含替换成 nil,即内核卸载 API 的真实状态);
- stop / onRemove handler 恰好一次——契约是「执行后清空,幂等」,执行两次的后果
从重复写文件到 close 已关闭 channel 直接 panic;
- `StageContext` 并发读改写无 lost update(媒体链路让 Extra 成为新热点,
而 map 并发写在 Go 里是直接 fatal,recover 接不住);
- `OwnTools` scope 不跨插件泄漏;
- 媒体类型 JSON 往返字节级一致(9 种长度,含 0/1/2/3 与 base64 分组边界)
——`[]byte` 在 JSON 里是 base64,往返不一致意味着图片静默损坏,
要到 CAS 校验 digest 时才发现,那时已无从追查;
- `omitempty` 真的生效(读路径不能出现 `"data"` 键);
- nil 依赖全部静默降级不 panic。
## 四、工具链同步
- `proc_main.go.tmpl`:`procIO` 三个媒体方法、`procDocMemory.InsertWithMedia`。
模板不跟上的后果是**每个外部插件都编不过**(接口未实现),是硬失败;
- `proc_runtime_test.go`:方法清单补 `io.injectMedia*` 与 `doc.insertWithMedia`。
漏接线时插件调 `InjectInputMedia` 会静默无效果——模板不发这个 RPC,内核也就
收不到,两边都不报错;
- `yaegi/mocksdk`:与公开 SDK 对齐。它此前漂移严重且**没有任何代码对着它编译**,
所以漂移不会被编译器抓到:`Triple` 用的是 `Predicate`,而公开 SDK 一直叫
`Relation` —— 插件在 yaegi 调试期写 `Relation:` 报未知字段,写 `Predicate:` 则
编成 plugin.bin 时报错,两边都不对。
- README 中英双语补媒体接口文档与用法示例。
## 兼容性
存量插件不需要改一行也不需要重编:新增方法由**插件调用、内核实现**,不调就不
受影响。17 个 example 插件源码零改动通过类型检查;用 SDK 0.9.2 编的旧 plugin.bin
在新内核上直接建链通过(握手校验的是 ProtocolVersion=1,不是 SDK 版本)。
媒体接口需要核心 1.1.1+(更早的核心没有对应 RPC,调用返回 unknown method)。
`CoreVersion` 保持 1.0.0:它是「SDK 能在其上运行」的下限,媒体是可选能力。
|
2026-09-06 10:03:36 +08:00 |
|
|
|
e256023399
|
docs+plugindev: README 同步子进程架构,scaffold 修 entry 与 go.sum 死路
三类问题,都会让新用户第一次上手就走错:
1. README 仍写 plugin.so
plugin.json 示例的 entry、entry 字段说明、.hmap 包格式三处描述的都是
已退场的 C ABI 产物。改为 plugin.bin,并补 bundle 模式下
plugin.bin.<goos>.<goarch> 的命名与安装时挑平台的行为。
2. cmd_init.go 的 entry := "plugin.so"
scaffold 出来的 plg.json 带着一个已退场的 entry 值。build 实际不看这个
值(只用它区分 Lua),但跟着模板走会误以为自己在做 C ABI 插件。
3. 生成的项目第一次 build 必定失败
go.mod 只 require 一个 gitcode 模块版本号且不生成 go.sum。gitcode 不在
proxy.golang.org 上,于是:
go build → missing go.sum entry
go mod tidy → 去公共 proxy 拉一个不存在的条目,超时
原来 cmdBuild 里那句 `go mod download <mod>` 走的正是这条死路,失败后
只打一行 warn 就继续编译,紧接着死在同一个错误上——用户看到两段无关报错。
修法分两处:
- 生成的 go.mod 直接写指向本机 SDK 的 replace(replace 到目录时 go 不
需要也不校验 go.sum)
- 新增 ensureSDKResolvable:三级策略(已有本地 replace → 探测本机 SDK
并写入 → 兜底 go mod tidy 带 -mod=mod,失败给可操作提示)。存量项目
go.mod 无 replace 时走第二级救回。
bin/ 5 个预编译二进制不再进仓库(改为 release 附件):
5 个平台各 26-28MB,每次重编都在 git 历史里再叠一份,而它们本质是可从源码
复现的产物。README 的下载说明同步改为 release 附件 URL + 从源码编译。
v1.0.0
|
2026-09-03 19:27:03 +08:00 |
|
|
|
092d8f4ab0
|
Merge branch 'feature/plugin-proc-migration' into main
SDK 侧配合内核子进程化迁移:plugindev 工具链产出 plugin.bin
(纯 Go 二进制,零 cgo),模板支持共享内存 stage 与事件环消费。
- entry 语义收敛到 plugin.bin,删除 C ABI 工具链
- stage 回传改 diff,只回传变更字段(修复 lost update)
- 模板支持事件环消费(evtConsumerLoop)
- Windows 共享内存适配(OpenFileMappingW / OpenEventW)
- meta.Version 升到 1.0.0,与内核对齐
公开 SDK 接口(sdk/ 目录)全程零改动——接口冻结不变量。
|
2026-09-03 13:54:21 +08:00 |
|
|
|
5ed8d65479
|
meta: 版本升到 1.0.0,删除 C ABI 时代的死常量
## 为何是主版本号
公开 SDK 接口(sdk/ 目录)本轮**零改动**,插件业务代码一行不用改。
但产物形态变了:plugin.so → plugin.bin。0.9.x 内核只会 dlopen `.so`,
本版工具链产出的 `plugin.bin` 在旧内核上根本不会被识别——
这是不可互操作的破坏性变化,故 CoreVersion 也升 1.0.0 作为**硬下限**
而非建议值。
## 删掉的死代码
ABIVersion / ABIVersionMin / CABINum / CABINumMin / 51 个 Core<Method>
整数 ID,全部无使用者:
grep -rn "CABINum" --include=*.go --include=*.tmpl . → 只有定义处
grep -rn "meta.Core[A-Z]" ... → 无
它们随 Part 6.2 删除 internal/plugin/cabi/ 就已失效:
- 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量)
- 版本协商改为握手帧里的 protocol 字段
留着有实际危害——下一个读这个文件的人会以为 C 层协商还在生效,
或者以为加 method 时要同步维护那张整数表。
## 协议版本与语义版本解耦
新注释写明:子进程 RPC 的 protocol 是独立小整数(当前 1),
只在帧格式或握手语义变化时升;语义版本变动频繁(修 bug、加字段)
不应牵动 wire 协议。这两者以前被 ABIVersion = CoreVersion 绑在一起,
现在分开。
验证:go build ./... 通过;git diff sdk/ 为空(接口冻结不变量)。
|
2026-09-02 22:40:15 +08:00 |
|
|
|
9f844123fe
|
plugindev: entry 语义收敛 + 删 C ABI 工具链 + Windows 共享内存适配(Part 6.1)
## entry 不再是通道开关 —— 外部插件零改动的关键
17 个存量插件的 plg.json 都写着 "entry": "plugin.so"。若把 entry 当通道
开关,迁移就得改 17 个文件,而「外部插件零改动」是本次迁移的硬约束。
改法:Go 插件一律产出 plugin.bin,不看 entry 值。isProcEntry 删除,
resolveBuild 去掉 proc 参数。entry 现在只剩区分 Lua(main.lua)一个用途。
实测:weather 的 plg.json 一行不改(仍写 plugin.so),plugindev build
直接产出三平台 plugin.bin。
## Windows 不再是能力退化的第三套实现(§9.2 的正解)
C ABI 时代 Windows 是独立的第三套 ABI:dynamic_dll_windows.go 的 stage
只下发 3 个字段(raw_message/user_id/phase)且完全没有写回,sanitizer
这类改写型插件在 Windows 上静默失效,且无任何运行时警告。
现在 Windows 与 Unix 共用同一份 RPC 逻辑与同一份共享段布局。平台差异
收敛到三个挂载函数:
- Unix(linux/darwin/freebsd):内核经 ExtraFiles 传继承 fd(3=StageContext
段,4=事件环段,5=eventfd/pipe)
- Windows:没有 fd 继承语义(os/exec 的 ExtraFiles 在 Windows 不支持),
改用命名内核对象——父进程 CreateFileMapping/CreateEvent 建带名字的对象,
子进程 OpenFileMappingW/OpenEventW 按同名打开。名字经环境变量传入而非
硬编码:多个 homed 实例并存时不能撞名。
Windows 绑定用 syscall.NewLazyDLL 而非 golang.org/x/sys/windows:
OpenFileMappingW/OpenEventW 未被标准库 syscall 导出,而引入 x/sys 会给
**每个插件的 go.mod** 加一个新依赖,违反「插件仅依赖公开 SDK」。
LazyDLL 属标准库,零新增依赖。
新增 evtWaiter 接口抽象等待语义:eventfd 是计数器(多事件合并成一次
唤醒),Windows Event 是二元信号。不影响正确性——消费者被唤醒后按
readSeq 追 writeSeq 批量 drain,一次唤醒能处理累积的全部事件。
模板拆成三个文件:
proc_main.go.tmpl 平台无关(RPC + 共享段布局 + stage + 事件环消费)
proc_shm_unix.go.tmpl 继承 fd 挂载
proc_shm_windows.go.tmpl 命名对象挂载
## 删除 C ABI 工具链
templates.go 1296 → 516 行:
- tmplBridge(Windows DLL bridge) -265 行
- tmplLinuxBridge(Linux c-shared) -457 行
- tmplPluginInitC(C 入口) -57 行
另删 generateBridge / detectWindowsCC(MinGW 探测)/ tmplCABIHeader /
InitData.CABIVersion+CABIHeader。
交叉编译不再需要目标平台 C 工具链——这是 -buildmode=c-shared 退场的
连带收益(§3.1)。
## 测试
15 项全过,新增 4 项守护迁移不变量:
- AllPlatformsProduceBin:6 个 GOOS/GOARCH 组合统一产出 plugin.bin
- LuaIsSeparatePath:Lua 仍走解释器路径
- UnsupportedOSErrors:不支持平台明确报错,不静默产出错误产物
- NoCABIResiduals:代码中不得再出现 c-shared / CGO_ENABLED=1 /
detectWindowsCC / tmplLinuxBridge / tmplPluginInitC(注释除外)
- IgnoresEntryForGoPlugins:isProcEntry 必须已删除
验证:go build/vet/test 全通过;三平台交叉编译产出 plugin.bin;
git diff sdk/ 为空(接口冻结)。
Ref: docs/zh/架构迁移评估.md §3.1/§9.2、docs/zh/plugin-migration-plan.md Part 6
|
2026-09-02 18:39:02 +08:00 |
|
|
|
ef0e58ee23
|
plugindev: 模板支持事件环消费(Part 5 子进程侧)
handleHandshake 额外挂载 fd 4(事件环段)+ fd 5(eventfd),启动
evtConsumerLoop goroutine 消费事件。
HandshakeParams 新增 evt_ring_size 字段(0 = 不支持事件环)。
events.subscribe:按类型列表在本地注册 handler,evtConsumerLoop 从
共享段读 slot 后按位索引分发。events.unsubscribe 清空全部 handler。
事件环布局常量与内核 internal/plugin/proc/evtring.go 一一对应。
验证:weather.bin 零改动编译通过;E2E 测试全通过。
Ref: docs/zh/架构迁移评估.md §3.6
|
2026-09-02 17:05:01 +08:00 |
|
|
|
09b64dcb53
|
plugindev: 支持子进程插件构建(entry=plugin.bin,零 cgo)
Part 3 工具链改造。插件业务代码零改动,只需把 plg.json 的 entry
从 plugin.so 换成 plugin.bin。
新增 templates/proc_main.go.tmpl(1113 行)——子进程运行时:
- 51 个 core method 的插件侧 RPC 实现(procIO/procMemory/procSettings/
procSocial/procLLM/procKnowledge/procDocMemory/procTextMemory/procPluginMgr)
- 共享段访问(fd 3 = 内核经 ExtraFiles 传入的 memfd)+ 16 字段
StageContext 编解码,布局常量与 internal/plugin/proc/shm.go 逐一对齐
- handleStageInvoke:拿锁 → 读段 → handler → **只写脏字段** → 放锁。
只读插件脏字段集为空 → 零写入 → 不可能覆盖他人改写
(对照 C ABI 副本模型实测 35.8~36.8% lost update)
- 主循环每请求独立 goroutine:handler 内会反向调用内核并等应答,
在读循环里同步处理会死锁
- plugin.start 后显式上报 AutoRestart:公开 SDK 的 SetAutoRestart 是纯
setter 无 hook,隔着进程边界内核读不到(内核侧 corehandler.go:145 已就绪)
模板选择真实 .go 源文件 + //go:embed 而非 raw string:900+ 行代码塞在
字符串里写错只能等生成插件时才炸,作为源文件可被 parser/gofmt/vet 检查。
cmd_build.go:resolveBuild(target, proc) 分派;proc 走 go build -trimpath
+ CGO_ENABLED=0,交叉编译不再需要目标平台 C 工具链。bundle 模式各平台
产物同名故 zip 内加平台后缀(plugin.bin.linux.amd64)。
proc_runtime.go 生成时清理残留 z_bridge_gen.go/z_entry.c——同目录两套
main 会编译冲突,这让 .so → .bin 切换无需人工清理。
proc_runtime_test.go 16 项静态检查,防内核/插件两侧漂移:
method 名清单、7 个内核调用、共享段常量与字段枚举顺序、stage 加锁顺序、
快照必须存序列化字符串(切片共享底层数组的坑在 11.3 已踩过)、
arena 不足须报错、日志走 stderr、版本不匹配须拒绝、零 cgo。
验证:真实 plugindev 构建 example/weather,plugin.go 逐字节未改,
产出静态链接 ELF;git diff sdk/ 为空(接口冻结)。
Ref: docs/zh/架构迁移评估.md §3、docs/zh/plugin-migration-plan.md Part 3
|
2026-09-02 12:13:45 +08:00 |
|