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
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
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
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
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
61f307be1a
feat(qq): msg_id→get_history 7天兜底 + list_chats/mark_read 会话列表 v1.2.0
...
## 问题
1. NapCat get_msg 的 message_id 是 QQ 服务端临时短号,约 3 天后失效。
实测 1306 条真实 webhook 消息,141 条(10.8%)现在查回报「消息不存在」,
全是 3 天前的旧消息;1 小时内的消息可正常查回。NapCat 侧无保留时长配置项
(napcat.json / onebot11_*.json / webui.json 均无),是 QQ 协议硬限制。
模型收到 not_found 后曾编造正文(虚构 message_id + 虚构需求),
已在核心 prompt 加事实性约束,此处从插件层根治取不到正文的问题。
2. 插件与真人客户端差距大:没有会话列表、消息不按到达先后排序、无未读提醒,
模型只能靠单条中断消息被动响应,导致消息处理不及时。
## 改动
### msg_id → get_history 兜底(不缓存正文)
- 新增 msgRef{peerID,isGroup,time}:只记 msg_id → (peer, 时间) 映射,7 天 TTL
(qqMsgTTL),超 2000 条时惰性清理过期项。不缓存消息正文。
- handleGetMessage 改为包装层:命中映射且 <7 天 → getMsgFromHistoryByTime()
按 peer 拉 get_history(count=50),取时间最接近的一条,
经 msgToGetMsgResult() 包装为与 get_msg 同构的结果(附 resolved_via:history);
未命中或超 7 天 → 回退原 NapCat get_msg(重命名为 getMsgFromNapcat)。
- 超 7 天的消息由 get_history 自行处理,插件不做长期缓存。
- not_found 文案改为明确引导改用 qq_get_history / qq_list_chats。
### 会话列表(对齐真人客户端)
- 新增 chatMeta:会话名、未读数、最新一条 ≤60 字摘要(qqLastSumLen)、最新时间。
只维护最新一条摘要,不存历史。
- 新增 qq_list_chats:按最新消息时间降序返回会话列表,每项含
peer_id/type/name/unread/last_text/last_nick/last_time。
- 新增 qq_mark_read:按 group_id/user_id 清零未读;get_history 拉取某会话后
自动标已读(看过=已读,与真人客户端一致)。
- webhook 记录时机前移:策略允许的消息(群/私聊、是否 @bot 均记)都进入映射与
会话状态,@bot 只决定是否发中断——与真人客户端一致能看到全部会话。
### 中断模板
- 补 fallback 路径与私聊 user_id(原模板只给 message_id,取不到正文时
模型没有 peer 信息可用于 get_history):
「先用 get_message 取正文;若取不到(已过期),改用 get_history(...) 按会话拉取,
或用 list_chats 查看未读会话。用 output_send__qq 回复」
## 验证
用重建的 plugindev build --target linux/amd64 产出 dist/qq_linux_amd64.hmap,
经内核 plugin_install(overwrite=true) 安装(action=reinstalled, config_kept=true),
重启 homed 后内核注册 20 个 qq_* 工具(原 18 + list_chats + mark_read)。
实测 qq_list_chats(count=8) 返回按时间排序的会话,unread=6 正确累积。
注意:cgo c-shared 插件带完整 Go runtime,dlclose 后引用计数不归零,
同路径 dlopen 复用旧映像,plgreload 无法热替换 .so,换 .so 必须重启 homed。
2026-08-30 17:11:57 +08:00
d57c5eaf3e
fix(examples): 全插件安全审查修复(qq/a2a/memo/calendar/rss/browser/bili/recoverydiag)
...
审查发现并修复 7 项问题:
- P1 qq: downloadURL 裸 http.Get 无超时 → 120s client
- P2 a2a: inbound http.Server 零超时 → Read 30s/Write 120s/Idle 60s
- P3 bili: output_dir 配置项零校验 → 系统目录黑名单(/、/etc、/usr、/var 等)
- P4 recoverydiag: db_path LLM 可控任意 sqlite → 强制限制 data 目录内
- P5 memo/calendar/rss: os.WriteFile 直写 → atomicWriteJSON (temp+rename)
- P6 qq: 3 处后台 goroutine(已读/rcon转发/下载)加 panic recover
- P7 browser: dump-dom failback Kill 后补 wait 回收僵尸进程
recoverydiag 此前被 .gitignore 排除,但其 db_path 安全修复
属生产代码,故取消忽略并入库。
全部经 plugindev 重打包升版安装验证 config_kept=true。
2026-08-26 17:04:41 +08:00
c91739d670
fix(qq): parseIDList 兼容科学计数法存库的历史坏值
...
配置里 QQ 号被 WebUI 以科学计数法(2.198972886e+09)存库时,
ParseInt 解析失败导致 adminIDs 为空、老大消息不被标记。
ParseInt 失败后回退 ParseFloat, 整数值转为 int64。
2026-08-15 17:24:21 +08:00
cca9fdce9c
example: 新增 acp(ACP 代理通信)、vanblog(VanBlog 博客管理) 插件; 多插件工具调用检测与清理改进; weather 移除 onRemove/文本记忆; plugindev 精简
2026-08-15 09:03:05 +08:00
84bf100a12
example/qq: add typing indicator on private messages
...
- setInputStatus helper wrapping NapCat set_input_status API
- startTyping/stopTyping with goroutine-based typing manager
(5s interval, 30s auto-timeout)
- start typing on webhook private message receipt
- stop typing when output channel sends a response
- fix plg.json UTF-8 BOM that broke plugindev parse
2026-07-30 09:24:20 +08:00
c5bcae9404
sdk: add RegisterInputChannel + NoMemory/Cleaner support
...
- ChannelDef struct: NoMemory bool + Cleaner func(string) string
- InputChannelRegistrar interface + RegisterInputChannel method
- RegisterOutputChannel now takes ChannelDef parameter
- QQ example registers input channel with NoMemory:true + Cleaner
2026-07-29 14:46:15 +08:00
cb7999ca71
feat: sdk NoMemory/Cleaner + example build fixes
...
- sdk/plugin.go: ToolDef adds NoMemory/Cleaner fields
- sdk/plugin_test.go: unit tests for NoMemory/Cleaner
- all example plugins: NoMemory/Cleaner annotated for each tool
- plugindev/templates.go: template shows NoMemory/Cleaner pattern
- plugindev/cmd_build.go: fix ensureGoMod, buildBundle/buildTarget sdkPath param, NewPluginFactory
- example/go.mod: add external dependency declarations (chromedp, gofeed)
- add main.go stubs for all example plugins
2026-07-25 11:17:21 +08:00
5e0c8d5a40
feat(qq): download task tracking with qq_get_download_tasks, sync re-added
2026-07-23 10:41:08 +08:00
e182b89983
feat(qq): async file download, get_private_file_url, mark-as-read, get_recent_contacts
2026-07-23 10:36:49 +08:00
e030a9b9f9
refactor(qq): remove local message cache, direct NapCat query, URL-based file download
2026-07-23 10:27:55 +08:00
ab3c0bfd2d
fix(qq): use file_id instead of file field for group file download
...
NapCat file messages have file_id in data["file_id"], not data["file"].
data["file"] is the filename, causing get_file API to return empty.
Also pass url field to downloadFile for URL-direct fallback.
2026-07-21 15:33:41 +08:00
1a3e72e899
fix: use short config keys in RegisterDef, remove generics helpers
...
- qq, a2a, bili, files, rss: RegisterDef keys changed from
namespaced (e.g. "plugin.qq.listen") to short flat keys ("listen")
- a2a, bili: Get() calls updated to match short keys
- rss: replaced readCfg generics with getSetting, added SetAutoRestart(true)
- files: already uses short key Get, only RegisterDef needed fixing
2026-07-21 13:20:13 +08:00
52dc22f86f
feat: add example plugins (ai_image, calendar, music, rss, weather), fix .gitignore, move gengskill to tools/
2026-07-21 12:45:17 +08:00
75ae2b4692
fix: napcat returns json.RawMessage to avoid double-quoting in C ABI bridge
...
go_invoke_tool does json.Marshal(r) on handler results. When r is a string,
the raw JSON gets wrapped in quotes and escapes, causing the core's
json.Unmarshal into map[string]interface{} to fail (displayed as map[]).
Changed return type to json.RawMessage (implements json.Marshaler, outputs
raw bytes inline). Added rawString() helper for callers that need the
string representation.
2026-07-20 17:51:36 +08:00
bf08764c29
refactor: qq output channel to payload/meta/type interface
...
- handleChannelOutput: read payload/meta/type instead of content JSON
- Route by type: text/voice/image/file
- buildOutputHelp: show new parameter format
- Remove fixJSON (no longer needed without JSON-within-JSON)
2026-07-19 10:29:22 +08:00
02997a0dca
fix: QQ interrupt text explicitly requires output_send__ reply
2026-07-18 21:29:00 +08:00
0856a9b2c3
feat: merge web/webfetch/bili into single browser plugin (search/fetch/render/video)
2026-07-18 21:06:56 +08:00
1762f0c34b
docs: 修正全部文档使其与源码实现一致
...
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化 (plugin.so + plugin.dll + main.lua)
- 添加 meta/ 包元数据文件
2026-07-18 20:47:17 +08:00