f3232000f4
feat(scheduler): L4 也归内核级插件 —— WebUI 终止按钮可用“立即打断”
...
上一提交把 L4 写成“内核独占(panic / selfip)”,漏了内核级插件这类来源。
用户澄清:**内核级插件应当能声明 L4,用于实现中断能力**,例如 WebUI 的终止按钮。
判据(两道闸,纵深防御):
1. proc 桥(外部进程唯一入口)一律把 L4 夹到 L3。在这里夹而不是只按 source 判,
是因为 source 是插件自报字段、可以冒名;本函数所在位置能确知“来自外部进程”。
2. core:isKernelLevelSource(source) 查 pluginReg.IsBuiltinPlugin,只有编译期内置
插件(init() 自注册的工厂)才承认 L4。
source 约定 `插件名` 或 `插件名/实例`(webui/<deviceID>),判据取第一段——
否则带设备身份的 WebUI 来源会被误判成外部插件而拿不到 L4。
改动:
- core: interruptLevel(evt, privileged bool);新增 isKernelLevelSource;
requestPreempt 不再夹取(级别已由 interruptLevel 解析,否则内核级插件的 L4 被削掉)。
- eventloop: 传入 a.isKernelLevelSource(evt.Source)。
- proc 桥: 新增 clampExternalPriority,pubSdkInjectOpts 一律夹取。
- internal/sdk: 再导出 PriorityL1..L4(内置插件用 sdk.PriorityL4)。
- webui handleChatInterrupt(终止按钮)声明 PriorityL4。
- timer 声明 PriorityL3:定时器是“时钟那种实时工作”,比 QQ 那类可无限等待的
异步消息高(L1)——这是对用户“它不是时钟那种实时工作”的直接推论,可改。
- 测试: L4 特权矩阵(非特权夹取 / 特权承认)、source 判据(内置、内置/实例、
外部、空、前缀不误匹配)、内核级插件 L4 一路到达调度器、proc 夹取两条。
- 设计稿 §2/§3.2/§11.1/§14/§15 按“L4 = 内核 + 内核级插件”更正。
验收:go build/vet 干净;go test ./... 37 包 ok 0 FAIL;-race 全绿(含 webui/timer)。
2026-09-13 07:18:03 +08:00
86a702b3b0
fix(scheduler)!: 中断栈语义(嵌套抢占 LIFO),并修掉抢占空转
...
用户指正:存在**中断被中断**的场景,所以被打断的现场要进**中断栈**。
我此前把 suspendPool 明确写成“不是栈、按优先级取”,是错的。
改动:
- suspendPool 改名 suspendStack,恢复纪律改为**严格 LIFO(只比栈顶)**;
栈内不做优先级重排——嵌套抢占天然使栈自底向上基础级递增,
且“后被打断的先恢复”才是栈语义。取出即弹栈。
- 修掉一个由此暴露的真 bug(抢占空转):一次抢占生效后,被挂起的原任务
会因饥饿防护提升有效级,与抢占者同级;此时若按“先到先服务”,原任务
(入队更早)会被立刻选回,抢占者永远排不到 —— 抢占等于没发生。
现在**同级时 pendingInterrupts 优先于其它两类**,保证抢占必然生效。
- 状态 DTO:SuspendPool/suspend_pool → SuspendStack/suspend_stack
- 设计稿:§2 用语更正(它**就是**中断栈)、§4.1 选择函数(候选只含栈顶 +
pending 同级优先,并说明为何必需)、§6.2/§6.3/§9/§11 用例同步
测试新增 scheduler_stack_test.go 3 项:
- 嵌套 L1→L2→L3,恢复严格 LIFO(B 先于 A)
- 只比栈顶:人为构造“栈底 L3、栈顶 L2”,必须取栈顶(区分两种实现)
- 嵌套下的深度上限
验收:agent 全量 + -race;全仓 build/vet 通过
2026-09-13 06:30:23 +08:00
f11de37bf2
feat(scheduler): M7 可观测性 + 压力测试 + 端到端测试
...
设计依据 docs/zh/input-scheduler-design.md §11.5(O1/O2)、§11.6(E1/E2)。
- 可观测性:KernelStatus 新增 Scheduler 段(running/三集合深度/计数/
深度上限),由 GetKernelStatus 从 DumpScheduler 原子快照填充;
新增 events.EventScheduler,挂起/恢复各发一条(action/task/level)
- TaskKind.String() 便于日志与状态输出
- 新增 scheduler_e2e_test.go 3 项:
· 压力:200 排队输入 + 50 中断全部经真实 loop 执行,结束时三集合排空、
LLM 调用数精确等于输入数、无 Rejected
· 可观测性:挂起/恢复事件齐备,状态快照计数一致
· 端到端:完整启动 schedulerLoop+interceptLoop,经真实 channel 投递
L1 任务与 L4 中断,验证「LLM 流式中断 → 挂起 → 中断先完成 → 原任务恢复」
整条链路(LLM 调用数 = 丢弃1+中断1+恢复1+常规1)
- 验收:agent 全量 + -race;全仓 build/vet 通过
2026-09-13 00:45:28 +08:00
5aaae93367
feat(healthcheck): 内核状态快照报出内核版本号与 ONNX 模型启用状态
...
healthcheck_kernel 此前没有任何「ONNX 模型是否在用」的信息,只报「向量可用/不可用」,
分不清「统一多模态空间已加载」与「退回到词嵌入/TF-IDF 路径」;人格卡要求
「版本以运行时快照为准」,也缺一个可查字段(build.version 早就在,但没人知道)。
- KernelStatus 新增 onnx 段:enabled / provider / dim / fingerprint / modalities / reason。
判据取 Loaded()(provider 真正打开且元数据合法),**不是**「配置里写了 provider」
—— 后者在模型缺失 / 运行时缺失时也为真,报出去就是假绿。
- 未启用时 reason 给**具体原因**:未配置(说明会走回退路径)/ 打开失败的具体错误。
homed 把「配置的 provider 名」与「打开失败原因」透传给 Agent,仅供状态报告。
- ProviderAdapter 新增 Modalities()(可选能力,按接口断言取用,不改公开契约)。
- healthcheck_kernel 的工具描述同步说明它回答这两件事。
**顺带修一个真实 panic**:collectKernelStatus 的 knowledge 是**接口**参数,
(*knowledge.Store)(nil) 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic,
而 healthcheck_kernel 正是走这条路径(panic 发生在工具 goroutine 里)。
GetKernelStatus 改为先按具体指针判空、再赋给接口;并加刻画测试钉住这个成因
(一旦不再 panic 说明参数形状已变,守卫与该测试应同步删除)。
验证:单测 4 例(已启用 / 打开失败 / 未配置 / 未加载)+ 刻画测试;
隔离实例 E2E 7/7:正例 provider=chineseclip → enabled=true、dim=512、模态 2;
反例 provider=nonexistent → enabled=false 且 reason 含具体错误与 provider 名,
真实对话仍通。
2026-09-12 14:56:25 +08:00
dc80855540
feat(status): 暴露构建源码地址,WebUI 状态页给出 AGPL §13 的源码入口
...
选了 AGPL-3.0-only 之后,§13(Remote Network Interaction)就不只是声明问题:
把修改过的版本作为网络服务提供出去时,必须给使用者取得 Corresponding Source 的机会。
只在仓库里放 LICENSE 并不自动满足这一条——**使用者拿到的是服务,不是仓库**。
所以把它做进产品里,而不是写进文档就算完:
- `internal/meta.SourceURL`:本次构建对应的源码地址,默认指向本仓库。
注释里写明「修改后对外部署的分支必须改指向自己的仓库」,并给出 -ldflags 覆盖方式
(`-X .../internal/meta.SourceURL=<你的仓库>`),不需要改源码。
- `BuildStatus.SourceURL`(`json:"source_url,omitempty"`)+ 内核状态填充:
走已有的 `/api/v1/kernel` 构建身份链路,不新增端点。
- WebUI 状态页在「版本」行下渲染「源码 / Source」链接(URL 经 escHtml 转义后进属性;
`target="_blank" rel="noopener noreferrer"`)。
- 用 `omitempty`:未注入该值的旧构建不会在 JSON 里多出一个空字段。
验证:
- `internal/plugins/webui/dashboard.html` 的两个内联 script 块 `node --check` 均通过;
- 全仓库 `go build ./...` 通过,`internal/meta/meta.go` gofmt 干净
(`internal/agent/core/status.go` 有**既有**的 gofmt 差异,与本改动无关,按纪律未整体重排);
- 端到端:本地构建 homed → 冷启动 → `GET /api/v1/kernel` 的 `build.source_url`
实测为 `https://gitcode.com/JianFeeeee/HomeAgent `。
2026-09-12 09:41:39 +08:00
d1959cbe80
feat(core): 注入行为的记忆/裁剪标志位落地 + jieba 词库内嵌 + Windows 改走 WSL
...
配套 SDK 提交:homeagent-sdk ba49dfd(公开 API 纯追加,无签名变更)。
本仓第三方的库镜像同步至该版本,以保证全新 clone 能编译。
## 1. 注入标志位(内核侧)
- 7 条注入路径(排队/中断/同步 × 纯文本/带媒体 + 旧 NoMem 变体)解析并转发
no_memory / context_policy / cleaner_name;策略在入口**校验**,
非法值报错而不是静默降级成 none(降级会让调用方以为自己声明的裁剪在生效)。
- 新增 validateContextPolicy(与 tool.register 同一套规则)与 pubSdkInjectOpts。
- input.register 不再手写字段白名单重建 ChannelDef,改为整体传递 + 补 ContextPolicy。
- io 层:applyInjectOpts 把标志位写进事件 payload,仅非零时写
(零值与旧 payload 逐字节一致,事件订阅方与旧内核都不受影响)。
- ioAdapter / procCore / internal-sdk 别名补齐六个 *Opts 实现。
## 2. 修掉「输入无条件裁剪」这个真缺陷
eventloop 此前对**每条非中断输入**都调 `context.Prune(...)`:破坏性(低相关事件被
归档移出上下文)且无法从调用点看出是谁触发的。改为 pruneOnInput/pruneDeclared:
优先级:注入点声明(payload.context_policy)> 通道声明(ChannelDef.ContextPolicy)
> 默认**不裁剪**
查询向量仍取清洗后的内容;新增 cleanInputFor 解析清洗文本,优先级为
注入点声明的 cleaner(cleaner_name)> 按 source 查到的通道 cleaner > 原文,
名字查不到时**记日志再回退**(注入是 fire-and-forget,插件看不到错误,
至少要在内核日志留下「你声明的清洗没生效」的痕迹)。
## 3. jieba 词库内嵌(修「猜 GOMODCACHE → 静默失效」)
原 jiebaDictDir() 去猜 GOMODCACHE/GOPATH/~/go/pkg/mod,部署机上通常没有 Go 模块
缓存 → GetJieba() 返回 nil → 分词/关键词提取/NLP 依存解析(进而 doc→graph 三元组
抽取)/静态词向量 tokenizer **一律静默返回空列表**,只有一行日志。本机看起来正常
只因开发机与生产机重合、恰好有那份缓存。
现在词库随二进制分发:internal/memory/jiebadict/ 5 文件约 11.6MB + go:embed,
按**内容哈希**命名缓存目录落盘(词库升级不复用旧文件),已齐全则跳过写入。
模块缓存降为兜底。homed 体积 32MB。
顺带确认(并有测试佐证):gojieba 的 Tag() 不需要 pos_dict/ 目录——
cppjieba 的 PosTagger 从主词典每行的词性列取 tag。
## 4. homed 放弃 Windows 原生,改走 WSL2
插件体系依赖「继承的 fd」+「统一共享内存区的段内偏移解引用」,Windows 既无 fd
继承语义,其句柄模型也无法表达后者;强行适配等于再维护一套平台专属 ABI
(C ABI 时代三套 ABI 并存曾导致改写型插件在某平台静默失效)。
- cmd/homed/platform_{windows,other}.go:原生 Windows 启动即拒绝并打印 WSL2 指引。
- internal/plugin/proc/shmalloc_windows.go:allocShm 直接返回「请用 WSL2」,
**不返回半可用的段**(与 shmalloc_other.go 同风格:未支持平台显式报错);
procEnvForShm 返回 nil。顺手修掉两处长期编译错误
(cryptorand→rand、h.evData→h.unified.evtData),使 GOOS=windows 至少能编译。
注:homed 本就编不出 Windows——internal/memory 依赖 cgo-only 的 gojieba。
- deploy/packaging/installer.nsi:不再安装 homed.exe/initconfig.exe,改为携带
**linux payload** 并调用新的 install-via-wsl.ps1;退出码 20/21 表示
「需先装 WSL/发行版」,走指引而非报错。
- deploy/packaging/windows/install-via-wsl.ps1(新):检测 WSL → 引导安装 →
确保 WSL2 → 送包进发行版 → 在 WSL 内按 Linux 方式安装。**复用 Linux 包与
linux/setup.sh**,不另写一套安装逻辑;落点与 deb 布局统一
(/usr/bin/homed + /usr/lib/homeagent/setup.sh)。
- deploy/packaging/linux/setup.sh:API Key 允许 HOMEAGENT_API_KEY 覆盖
(否则安装器界面显示一份、config.db 里另一份 → 登录不上)。
- deploy/packaging/build.sh:windows 目标只构建 waiter + gui,并新增
stage_linux_payload 把 Linux 包暂存给安装器;homed/initconfig 在 windows
目标下明确拒绝。
## 5. 插件调用点统一写明意图
- webui 的 OpenAI 兼容端点(固定提示词模板)→ InjectTextSyncNoMemory。
- agentcli 的 5 处纯状态通知(已启动/超时/执行结束/进程退出/读取结束)→ NoMemory;
**带输出**的 2 处(定时反馈、有新输出)刻意保留记忆并注明理由。
- timer 的定时提醒 → NoMemory(中断本来也隐含 NoMemory,这里是写明意图)。
## 6. 版本
meta.Version 仍为 1.2.0(main 是下一个未发布中版本);
SDKCompatibleVersion 1.1.0 → **1.2.0**(本内核已实现 SDK 1.2.0 全部新增方法)。
## 测试
- core:默认不裁剪(无声明/none/空)、通道 opt-in、注入点双向覆盖通道、
nil context/io 安全、cleaner 优先级与未知名回退。
- io:零值 opts 与历史 payload 逐键相同;text/中断/媒体三类注入标志位都落到
payload;旧方法仍生效。
- proc:validateContextPolicy 只接受 ""/none/prune,报错含位置与实际值;
**跨进程** e2e——testdata 插件经 io.injectText 送出三个标志位,断言它们穿过 RPC
到达内核。
- memory:模块缓存不可见时内嵌词库仍可用(分词与 POS 内容词均非空)、
落盘幂等、内容哈希稳定。
验证:go build ./... / go vet ./... / go vet -tags onnxruntime ./...
go test -short ./internal/memory/... ./internal/nlp/... ./internal/plugin/...
./internal/agent/{core,io}/... ./pkg/...
2026-09-11 20:31:50 +08:00
5836c2ce5c
refactor(memory): 拆除描述式媒体索引,媒体成为一等块并按原生向量融合
...
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、
再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个
致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上
只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。
本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆:
一、描述链彻底删除(无残留、无兼容分支)
- media.Item 去掉 Description/DescribedBy 与对应列;
- 删除 Store.Describe / Store.Search / Store.Pending;
- 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项
core.memory.media.describe_on_ingest;
- SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。
二、marker 机制删除,媒体归属改为结构化块边
- 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/
extractMediaDigests/sentenceWithMediaMarkers/docMediaContext;
- memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样,
不再被 marker 污染;
- 块以 sentence --contains--> block / document --contains--> block 结构边
挂到承载节点(新增 documents 表与 document 节点种类);
- 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。
三、旧数据迁移(幂等)
- 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest
还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行;
- CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子
连同块边一起删掉。
四、向量融合:媒体按图本身被召回
- 新增 vector.FuseVectors(逐维求和 + L2 归一化);
- Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合),
新增 Doc.DenseFP,指纹变化触发重算;
- ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在
同一统一空间内比稠密余弦;
- 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再
直接充当记忆检索结果。
五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建)
- internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧
(qwen/embedder.go、image.go、model_input.go)、CLIP 移除、
sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。
验证:go build ./... 、go vet ./...(含 -tags medialive)均通过;
在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/...
全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造
无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
2026-09-11 11:45:24 +08:00
dae01f9c06
refactor(memory): 移除 media_refs/引用计数,媒体成为一等记忆块
...
媒体此前是"文本块 + digest 引用 + owner 账本 + 独立 GC":ContextEvent.Media
记 digest,media_refs 表用 owner_kind/owner_id 保活,ref_count 决定 GC 能否清。
这与文本记忆块的管理方式不一致,也是本次一并纠正的核心偏差。
改为与文本块完全一致的生命周期:
1. 一等记忆块直接由所在层持有
- ContextEvent.Blocks / Doc.Blocks / GraphDB memory_blocks
- 块带 modality/digest/MIME/size/vector/fingerprint,文本、图片、视频同构
- Context→Document→Graph 迁移的是块本身(ID 不变),迁移后清空源容器,
同一块不同时存在于两层
2. 删除平行生命周期账本
- media.Store 去掉 media_refs 表、OwnerKind 常量、RefCount 字段、
AddRef/DropRef/DropOwner/Refs、ref_count 列与索引
- 删除 mediaGCLoop、GC(keep,minAge)、容量上限与 media.gc_* / media.max_mb 配置
- 媒体内容在块被永久删除时一并删除(media.Store.Delete + forgetPayloads),
与"删除文本块即删除内容"同一语义
3. L3 原生结构
- memory_blocks / memory_block_edges(contains/depicts/derived_from)
- 边端点必须是真实图节点,不再用 owner 字符串伪装关系
- BlocksForNode 支持 sentence --contains--> block 反查
4. SDK 与检索同步
- 插件附件/标记直接变成块,不再 AddRef
- 跨模态检索改用 QueryMediaScored(CAS 内不再有孤儿缓存需要过滤)
测试全部改写为块语义:删除 refcount/media_refs/GC 断言,新增块迁移、
单层不变量、Delete 语义与并发删除回归。
注:cmd/homed/main.go 同时携带工作区中既有的 CLIP→Qwen 模型目录接线改动。
2026-09-11 10:57:22 +08:00
687e5655bc
feat(sdk): 多模态贯通插件边界——公开接口、内核桥接与统一输入主干
...
记忆系统在 1.1.0 支持了二进制多媒体节点,但那条链路只对**内核自己**开放:
用户在 qq 发图能落进 CAS、能被记忆引用,而插件调 Commit / DocMemory().Insert
交进来的媒体一律无处安放。原因是三层都断着,且**每一层都不报错**。
## 一、公开 SDK:补上媒体的表达能力(全部新增,无签名变更)
- `Triple` += `SentenceText`、`MediaDigests`
- `Doc` += `MediaDigests`、`Attachments`;新增 `MediaAttachment`
- `TextEvent` += `Attachments`
- `DocMemoryAPI` += `InsertWithMedia`
- `IOInjector` += `InjectInputMedia` / `InjectInputMediaSync` / `InjectInterruptMedia`
- `PluginSDK` 补上一直缺失的 `SetToolBlocks` 包装(接口里有、便捷方法里没有)
`MediaAttachment` 一个类型服务两个方向:给 `Data`+`MIME` 是新内容(CAS 按字节
去重),只给 `Digest` 是引用已有内容。读路径**只回元数据不回字节**——一次检索
可能命中几十份媒体,全塞回去会把跨进程消息撑爆。
媒体注入不能搭 `SetToolBlocks` 的车:那个方法只在工具处理函数内部可用,且媒体
要等下一条 tool message 才到模型手上。插件主动发起一轮带媒体的对话、以及中断
注入,需要自己的签名,且媒体在**本轮**就送到模型。
## 二、内核桥接层:原先在静默裁字段
`internal/sdk/memory_impl.go` 此前只搬自己认识的几个字段,其余丢弃且返回 nil:
- 图记忆丢 `Confidence`/`SubjectType`/`ObjectType`/`SentenceText`,又走 `Commit`
而非 `CommitWithMedia`(不回 sentenceIDs)→ 媒体绑定链 `SentenceText → sentences
→ sentence_id → media_refs` 一步都走不通,插件即便按格式写好标记也永远挂不上;
- 知识库 `Query` 只回 ID/Title/Content,`Insert` 只写这三个;`Remove` 不解引用,
于是那些媒体永久处于「被引用」状态,GC 收不掉、磁盘只增不减
(内核的归档路径 `releaseDocMedia` 做了这一步,插件路径漏了同一步)。
规则改为:**内部结构有的字段一律透传**。标记格式处理作为包级私有辅助留在桥接
层自己手里,但必须与内核 `mediaSummaryForEvent` 字节兼容——两边要能互读对方
写下的标记。
标记插入必须在 `ds.Insert` **之前**(向量索引取 `Summary + " " + Content`,
之后补的标记检索不到),引用绑定必须在**之后**(owner_id 是 Insert 生成的 ID)。
## 三、跨进程链路:不接线就是全体外部插件编译失败
`go test` 直接把这一层拍出来了——`procIO does not implement sdk.IOInjector`。
公开接口加方法后,生成模板不跟上,**每个外部插件都编不过**,是硬失败不是软降级。
六处接线:`protocol.go` 四个 method 常量、`capability.go` 能力归属、
`corehandler.go` 四个分派分支、`proc_core.go` 委托、`proc_main.go.tmpl` 模板侧
实现、以及三个测试替身。
## 四、统一输入主干:把模态从「函数选择」降级为「字段」
`processTextInput` / `processMediaInput` 合并为 `processInput`。这个分叉是历史
产物而非设计:`processTextInput` 本来就处理媒体(`bindEventMedia` +
`mediaSummaryForEvent`,与媒体路径尾部完全相同),`process()` 只看
`stageCtx.Extra["media_blocks"]`、根本不认识 `evt.Type`。模态是输入的**属性**,
不是输入的**种类**。
媒体路径由此获得它一直缺的六项:去重、`no_memory`、通道 `Cleaner`、中断语义、
`_consolidation_` 路由、正确的 `EventRawInput`。
最后一项是个真 bug:媒体路径发布 `"content": evt.Payload`(一个 map),而
`webui/handler.go` 断言 `.(string)` → 断言失败、`content == ""`、提前返回。
**用户发的图从来没出现在 WebUI 聊天记录里。**
`media_blocks` 同时接受 `[]agentAPI.ContentBlock` 与 `[]pubsdk.ContentBlock`:
字段一致但 Go 不自动转换,只认一种的后果是另一种被静默丢弃。
## 五、模型可调用的三个工具
`memory_commit` 的 `sentence_text` **从未暴露给模型**,而它是绑定链上的必经环节;
连同 `media_digests` 一起补进 JSON schema 与工具文档。`doc_commit` 加
`media_digests`。`doc_query` 把关联媒体单独一行附在结果末尾(正文按 2000 字截断,
标记通常就在尾部)。
标记由**内核**生成而非插件/模型拼装:要求调用方知道格式,等于让一个拼写错误
静默切断引用绑定,而全链路无人报错。
## 六、WebUI 上传走真实媒体链路
图片/音频读回字节拼 data URL 注入 `media_blocks`(8MB 上限,超限退回按路径处理)。
此前只注入一句「文件已保存到 <路径>」,指望模型自己调 `files_read`——但那返回
文本,图片字节对模型永远不可见。附件类型识别扩展到 audio 并在缺 Content-Type
时按扩展名兜底(判错不只是卡片样式问题,图片被当普通文件就进不了视觉链路)。
## 测试
- `internal/sdk/memory_impl_test.go`(12 例,此前该包**没有任何测试文件**)
- `internal/agent/core/inputunify_test.go`(统一主干 + 双静态类型 + 三工具媒体)
- `third_party/homeagent-sdk/sdk/stress_test.go`(13 例并发压测)
压测抓到两处**真**竞态(不是理论风险):`PluginSDK` 的 API 字段与 `autoRestart`
无锁,而写方(内核注入 API、插件 `SetAutoRestart`)与读方(插件后台 goroutine
注入、内核 registry 读 `AutoRestart`)天然跨 goroutine。加 `apiMu` 修掉;约定
只在持锁期间取字段值,取完即释放再调用——持锁调用会把 `InjectInputSync` 这类
阻塞到 agent 回复(可达数分钟)的方法与 `SetIOInjector` 串起来,让插件重载卡死。
测试还抓出两个自身缺陷:`bindDocMedia` 把同一份媒体数两次(`AddRef` 幂等所以表
是对的,但日志说「绑定 2 个」而实际 1 条——误导后续排查),以及用单字符实体名
时 `validEntityName` 静默跳过、`Commit` 返回 nil 却什么都没写。
存量插件不需要改一行也不需要重编:新增方法由插件调用、内核实现,不调就不受影响。
17 个 example 插件源码零改动通过类型检查。
2026-09-06 09:51:31 +08:00
09071dc235
fix(multimodal): 修多模态假成功 + 落地视觉回退链 + see_video 帧数语义
...
## 起因
生产盲测:模型调 multimodal_see_picture 后声称看到了图,实际一个字
都没收到。工具却返回「[已将图片注入后续对话]」。
链路:core.llm.model=AUTO → llmsproxy 按优先级选 big-pickle(prio=100)
→ 转 opencode zen。llmsproxy 的 opencode.lua 明写着:
-- zen 上游 schema 只接受 text content part(无视觉/音频能力)
if part.type ~= nil and part.type ~= "text" then -- 丢弃
判据:256x256 纯红 PNG,带图与不带图的 prompt_tokens 都是 256。
图片贡献零 token,即根本没进上游。
内核序列化与注入链本身是对的(Message.MarshalJSON 正确产出 content
数组,SetToolBlocks → IOManager → ConsumeToolBlocks → toolMsg.Blocks
全通)。缺的是「主模型能否消费这些块」这一判断——内核此前完全没有
多模态能力的概念(grep supportsVision|multimodal 在 agent/ 零命中)。
这与 v1.0.0 修的 output_send 假成功同类:告诉调用方成功而实际未送达。
## 1. 能力声明
新增 core.llm.sources.<name>.vision / .audio(走既有 sourceFieldDefs,
WebUI 配置页自动出现),types.LLMSource 与 api.BaseConfig 同步加字段。
新增 agentAPI.ModalProvider 接口 + ProviderSupportsVision/Audio 判定:
未实现该接口的 provider 一律按不支持处理。保守侧是刻意的——宁可多走
一次文字回退,也不能把图默默扔给会剥掉它的上游。
为何是声明而非探测:探测需额外真实调用且结果不稳定(取决于 AUTO 当次
路由到哪);而 200 响应 + 相同 token 数从响应侧无法区分「看到了但没
内容」和「被剥掉了」。
## 2. 回退链(modalfallback.go)
实现了 config/registry.go 里注册但从未被读取的 image/audio
fallback_provider + fallback_model(此前 0 处读取点)。
prepareToolBlocks 在 process.go 注入前判定:能直视就原样透传;不能就
调声明了该能力的源转写成文字,带 [由 X 转写,非当前模型直接感知] 标注。
几处刻意的设计:
- 逐模态判定,不一刀切。很多视觉模型能看图但听不到音频,全部降级会
白白把可直视的图变成二手描述
- 混合场景下转写文字作为 text 块并入 native,两部分同时到达模型
- 配置指向未声明能力的源时拒绝并继续找——照用只会重演静默剥离
- 未配 fallback_provider 但某源声明了 vision 时自动扫出来用;静默失败
比多找一个能用的源更糟
- 空回复算失败。上游剥掉媒体后模型往往回「我没看到图片」或空串,两种
都说明回退链也没真看到
- 多媒体块按模态合包为一次请求(见下)
## 3. 批量合包(生产实测驱动的返工)
首版逐块调用,生产 see_video 6 帧实测:4 帧里 3 帧超时,整轮 363 秒。
改为按模态合包一次请求后同一用例 131 秒、6/6 成功。
顺带把 modalFallbackTimeout 从 90s 提到 180s:生产经网关转
claude-opus-5 看一张 400x400 图要 ~81s,90s 贴着上限。
多张时 detail 默认 low 控体积,单张用 high 看细节;插件显式给了
detail 则尊重它。
## 4. see_video 帧数语义
fps=1/N 是频率(每 N 秒一帧)不是数量。20s 视频实测:
frames=4 → 5 帧、frames=10 → 2 帧、frames=1 → 20 帧,要得越多拿得越少;
长视频下 frames=4 会产出 时长/4 帧,靠 i>=9 的 break 兜着才没炸上下文,
而那个 break 用的是 ReadDir 索引,跳过条目后与实际帧数错位。
改为 ffprobe 取时长 → fps=N/时长 + -frames:v N 硬封顶。
0.4s/3s/20s/120s × frames=1/2/4/7/10 全部精确。
极短视频的坑:fps=1 在 0.4s 素材上产出 0 帧(不足一秒抽不出),所以
时长探测失败时不能退化成 fps=1,改为不传 -vf 只靠 -frames:v。
## 验证
- modalfallback_test.go 14 例:直视透传 / 回退转写 / 无源如实报告 /
未实现接口按不支持 / 混合模态拆分 / 空回复算失败 / 块数上限 /
多图合一次调用 / detail 策略 / 拒绝未声明能力的源 / 未配置时自动扫源
- go test ./... 全绿,go vet 无警告
- 生产盲测(答案预先封存、生成时不读):随机三色带 → 模型答
「紫、蓝、红」,与封存答案完全一致
- 负向验证:拿掉回退源后模型如实回答「没看到图片内容」并引用工具返回
的配置提示,且主动纠正了上一轮的答案
- 生产 see_video 6 帧:单次转写,模型正确描述测试图卡的计数器递增与
彩虹带滚动
2026-09-04 06:25:51 +08:00
f91b27aedb
fix: WebUI 版本显示 + Windows 交叉编译 + 发布脚本三处回归
...
## WebUI 版本链路修复
问题:handler.go:949 报的是 sdk.SDKVersion,那条链最终指向
SDK 仓 meta.Version 的硬编码值,与 -ldflags 注入的内核版本
完全不相交。构建时间、commit hash 全部丢失。
dashboard.html 兜底值是 '0.1.0'——碰巧版本号相等时不显眼,
一旦不等就报错。
修复:
- KernelStatus 新增 BuildStatus 字段(Version/Commit/BuildTime/SDKCompatible/KernelName)
取自 internal/meta(-ldflags 注入点),与接口冻结无关(KernelStatus 只在 internal/sdk)
- /api/v1/status 改用 meta.Version,另加 sdk_version 字段暴露 SDK 版本
- dashboard.html 概览卡显示 HomeAgent vX.Y.Z + commit/日期/SDK 兼容版本,
去掉 || '0.1.0' 误导性兜底
## Windows 交叉编译修复
问题:internal/plugin/dynamic_proc_windows.go(Part 1 的桩,610e9d0)只定义了
tryLoadProc,但平台中立的 registry.go 还在调 loadProc / closeProcHost——这两个
函数只在 dynamic_proc_unix.go 里。Windows 下整个 homed 从 Part 1 起编译不过。
plan.md §12.5 声称「Windows 只做了交叉编译,无真机验证」——实际是连编译都没通过。
修复:dynamic_proc_unix.go / dynamic_proc_windows.go 合并为平台中立的
dynamic_proc.go(文件内无任何平台专属调用,proc 包内部通过
shmalloc_* / evtfd_* / shmpass_* / procattr_* 各自带构建标签处理差异)。
## 发布脚本三处回归修复
deploy/packaging/build.sh(从 package/build.sh 移到 deploy/packaging/ 后):
1. PROJECT_ROOT 少一层目录(.. → ../..),产物落进 deploy/build/ 而非根目录
2. initconfig 从未被构建,但 installer.nsi 和 package-linux.sh 都引用它
3. GO 兜底路径指向 /home/jianf/go1.26.5(陈旧硬编码)改为 command -v go
4. electron-builder --config package.json 校验整个文件导致 devDependencies 被判为 unknown
property,去掉 --config 让它从 build 键读配置
deploy/packaging/package-linux.sh:
1. build_go() 补上 initconfig 构建步骤
2. GO 兜底路径同步修复
知识库 3 条重写 + 1 条新增:
- homeagent_identity:v0.9.0 C ABI → v1.0.0 子进程
- homeagent_architecture:全篇重写为子进程架构(三面通信、Supervisor 台账、
崩溃自愈、权限三道闸)
- homeagent_recent_updates:在 v0.9.0 前插入 v1.0.0 主线摘要
- changelog_v1.0.0(新建):6 类缺陷消除、架构、实测、已知限制、迁移指引
2026-09-03 15:38:31 +08:00
02cc74ce11
fix(proc): 子进程崩溃自愈 + 集中台账 + 注册面摘除
...
根因:子进程插件被 kill 后,内核只发了一个无人订阅的事件,
工具/stage handler/IO 通道全留在注册表里指向死进程,
模型继续调用只吃 ErrProcessExited,没有任何路径把插件拉回来。
## 四层修复
### 1. 专职 waitLoop(进程收割)
- 每个子进程配一根 waitLoop goroutine,是 cmd.Wait() 的唯一调用点
- 不再依赖 stdout EOF 判定死亡(孙子进程继承 stdout 时 EOF 永不到来)
- 手工 os.Pipe 替代 cmd.StdinPipe/StdoutPipe,避免 waitLoop 与
os/exec 的内部关闭竞争
- host.go: Host.Supervisor(),Host.Close() 先 StopAll 再拆段
### 2. 集中台账 Supervisor
- proc/supervisor.go: 插件 Spawn 握手成功即 track,进程退出即 untrack
- StopAll: 并发发 plugin.stop 走优雅路径,到期仍在的一律 Kill
- 关停后才完成握手的进程被立即结束,不会活过内核
- 消除「孤儿进程持共享段映射 → SIGBUS」的隐患
### 3. 注册面摘除(detachPlugin)
- 新增 StageHost.UnregisterPluginStages:摘除指定插件的全部 stage handler
- 新增 Registry.pluginChannels 台账:记录每个插件注册的 IO 通道
- 三条路径统一走 detachPlugin:Disable / ReloadOne / RemovePlugin
- StopAndUnload 漏了 IO 通道也一并补上
### 4. 自动重启
- onProcCrash 从「只发事件」改为「摘注册面 → 从注册表移除 → 异步排重启」
- scheduleProcRestart: 窗口 5 分钟内最多 3 次,线性退避 1s/2s/3s
- 超限停手留日志;重启前复核是否已被 Disable 或被其他路径加载
- 崩溃计数窗口过期自动归零
### 5. 主动停止 vs 崩溃的区分
- proc.Plugin 新增 stopping 标志:Stop()/Close() 里 Set(true)
- handleExit 读 stopping 标志,主动停止不上报 onCrash
- 防止重载/禁用/卸载被误判为崩溃触发多余重启
### 6. Linux Pdeathsig 兜底
- procattr_linux.go: SysProcAttr.Pdeathsig = SIGKILL
- 兜 homed 自身被 SIGKILL/OOM 时子进程变孤儿的场景
- macOS/Windows 无等价物,空实现
### 7. pluginmgr 升级
- PluginManager 接口新增 PluginRuntime / ListPluginRuntimes
- plugin_list 输出运行态:loaded / alive / pid / crash_count / channel
- 新增 plugin_status: 全量运行期快照 + dead/unhealthy 汇总
- 新增 plugin_restart: 无条件重启单个插件(plgreload 不动未改二进制的插件)
### 测试
- process_test.go: 3 例(grandchild stdout 感知 / Supervisor track-untrack /
StopAll 无孤儿)
- crash_recovery_test.go: 8 例(detach 三项齐全 / 通道重注册 / 崩溃不阻塞 /
退避阈值 / 窗口过期 / 关停中跳过 / PluginRuntime 通道识别)
- stages_plugin_test.go: 4 例(stage 按插件摘除 / 空 stage 清理 / 空名 no-op /
工具+stage 双摘后可重新注册同名)
2026-09-03 12:37:58 +08:00
b777322b95
feat(multimodal): 内置多模态感知插件 + process.go 原生支持 tool message 多模态块
...
【新插件 internal/plugins/multimodal】
- see_picture(path): 读取本地图片/URL,base64 注入 image_url block,
模型在下一轮 LLM 请求的 tool message 里直接看到图(1024×1024 图约 8500 token)。
自动识别 MIME,限 3MB 防爆 context。
- see_video(path, frames): ffmpeg 提取关键帧,多帧作为 image_url block 注入。
默认 4 帧,最大 10 帧,每帧限 2MB。
- listen(path): 读取音频文件,转为 audio_url block 注入,支持 mp3/wav/ogg/m4a。
限 5MB。
【内核多模态 tool message 支持】
- agent/api 新增 ToolOutput 类型(为后续 handler 直接返回 blocks 预留)
- SDK 公共层新增 ContentBlock/ImageURL/AudioURL(OpenAI 多模态格式)
- IOManager 新增 SetToolBlocks/ConsumeToolBlocks(interface{} 避免循环依赖)
- PluginSDK.SetToolBlocks(blocks) 插件工具调用后注入 blocks
- ioAdapter 桥接 IOInjector.SetToolBlocks
- process.go 工具执行后消费 pending blocks → 追加到 tool message 的 Blocks 字段
→ MarshalJSON 输出 content 数组格式 → LLM 看到图/音频
【验证】
multimodal_see_picture 注入 1024×1024 PNG 后 llmsproxy 统计:
prompt_tokens=44407(含 ~8500 image token),模型正确描述了图片内容。
2026-08-27 08:39:21 +08:00
f0cdbdb030
feat(sdk): SettingsAPI.DataDir() 插件专属数据目录 + ai_image 本地交付
...
【SDK DataDir API】
- SettingsAPI 新增 DataDir() string:返回插件专属数据目录
<data>/plugin_data/<name>(内核保证存在),解决此前插件只能
靠 GetCore("daemon.data_dir") 手工解析的缺陷
- settingsImpl 新增 dataDir 字段 + SetDataDir;Registry buildSDK
注入(<data>/plugin_data/<name> 并 MkdirAll);main.go 接线
- cabi 新增 CORE_SETTINGS_DATA_DIR (id 51);plugindev dispatchSettings
模板补 DataDir() 实现
【ai_image 交付本地路径】
- 生成后下载临时 S3 URL 到插件数据目录,返回本地文件路径(永久不
过期),而非 1 小时过期的 S3 URL。带 UA 规避图床对无 UA 客户端拦截
(此前 agent 裸 curl 验证被拒导致误报失败)
- 返回 local_paths 字段 + 提示用 output_send(type=image) 展示
端到端:ai_image_generate → plugin_data/ai_image/*.png 有效 PNG(1024²),
经 llmsproxy→siliconflow 生成。
2026-08-26 21:40:29 +08:00
ae42e486de
feat(pluginmgr): 插件更新接口(upgrade/downgrade 保留配置)+ skill_install overwrite
...
内核 Registry 拆出 StopAndUnload:
- 停止并从注册表移除插件但保留 config_<name> 表
- 不触发 onRemove 回调(那是删除专用语义)
- RemovePlugin 改为追加清理配置表示清除,更新场景调 StopAndUnload
pluginmgr:
- installFromData/installFromURL/installFromPath 加 overwrite 参数
- 已存在+overwrite=true:StopAndUnload→备份旧目录→解压新包→失败回滚→
返回 action=upgraded/downgraded/reinstalled+previous_version+config_kept
- 已存在+overwrite=false:返回 error+hint(指向 overwrite 用法)
- cmpVersion 点分版本号数字比较(非字典序)
- 测试覆盖:首次安装→重装拒绝→升级保留配置→降级→失败回滚
skill_install 加 overwrite 参数:
- 同名技能存在时先卸载旧实例+删除目录再安装新包
SDK PluginMgr 接口同步加 StopAndUnload(name string) error
工具链 plugindev 已重建到 /usr/local/bin(7/29→8/25 版本)
QQ 插件诊断日志版(webhook recv 到达+isAtBot 失败日志)已打包并
通过 upgrade 接口热更新部署,配置保留验证通过。
2026-08-25 22:02:17 +08:00
061d2ae320
feat(streaming): token-level delta events + interrupt for CLI/WebUI/GUI
...
Expose the LLM token-level streaming deltas (EventReasoningDelta /
EventContentDelta) to every client channel and add user-initiated
interrupt (cancel generation / send interrupt message) to all three
frontends, preserving the existing interrupt-injection semantics.
SDK/events:
- EventReasoningDelta, EventContentDelta constants exported in the
public/internal SDK event alias tables.
CLI plugin:
- handleChat subscribes to both delta events and forwards
reasoning_delta / content_delta JSON frames (channel-filtered);
aggregated reasoning/tool_call/response frames still fire as before.
- New /stop (alias /interrupt) builtin injects an interrupt via
InjectInterrupt(cliSource, cliChannel) - matches interceptLoop
semantics: cancels an active stream and re-injects the message as
a [中断消息] for a restarted turn; with no active LLM it behaves
as a plain input.
Waiter client (line mode + TUI):
- streamRender accumulates delta chunks and redraws the current line;
a reset frame (stream abandoned, e.g. user interrupt) flushes the
partial buffer so the next turn does not concatenate onto stale
content. Aggregated frames terminate the delta line and render the
final text (old servers without deltas behave exactly as before).
- TUI merges content_delta into the in-flight agent message and seals
it (final flag) on response/tool_call/error so subsequent deltas
never append to a finished message.
WebUI:
- SSE handler subscribes to the two delta events but does NOT record
them into the replay ring - reconnection replays only aggregated
events (the final truth), avoiding duplicate delta accumulation.
- POST /api/v1/chat/interrupt calls InjectInterrupt(webui, webui)
with optional message; fronted by a Stop button shown only while
a generation is in flight.
dashboard.html / GUI app.js:
- Stop button next to Send (hidden until chatLoading); interruptChat
POSTs /chat/interrupt. Delta listeners append incrementally;
agent_output (aggregated) now replaces (not appends) the in-flight
content and marks _final; reset frames finalize the partial message.
process.go:
- chatStreamWithFallback preserves the context.Canceled/
DeadlineExceeded contract: a user interrupt returns the canceled
error (never a partial-content success) so the existing continue
branch restarts the turn with the [中断消息]. A reset
EventContentDelta is published so connected clients drop stale
partial renderings before the new turn begins.
Verified: /stop 'msg' via waiter triggers 'interrupt from cli/cli' in
interceptLoop; unit TestChatStreamCancelPreservesInterrupt confirms the
canceled error propagates instead of being swallowed.
2026-08-25 10:50:37 +08:00
ba5785036a
feat: 设备鉴权迁移至客户端 + 插件卸载保护
...
安全修复(客户端鉴权):
- remotedevice 服务端移除授权状态存储(authorized map/SetAuthorized/handleDeviceAuth)
- DeviceMeta.Authorized 改为设备 hello 自报,服务端仅透传展示
- device_ctl_* 工具移除服务端授权检查,无条件转发,设备端自行决定是否执行
- 共享设备桥库 Bridge 新增本地 authorized 状态,未授权收到 cmd 直接拒绝
- waiter: --device-authorized / device_authorized 配置控制本地授权
- GUI: 授权存 gui-prefs 本地文件;设备页仅本机可切换开关
- webui /device/auth 旧路径返回 410 Gone
- 根因:agent 可经 config_set 篡改服务端授权配置自行授权设备
插件管理强化:
- 内置插件禁止卸载(IsBuiltinPlugin + 409),外部插件卸载即时生效
- 卸载不存在插件返回 404;移除误导性 reload_required 提示
- webui 插件路由:名称白名单校验防路径穿越、保留字路径保护
2026-08-24 19:26:11 +08:00
5b0cd45093
feat: screensee 工具 — agent 查看远程设备屏幕内容
...
与 screensue(向用户屏幕显示)配对: screensue 是给用户看, screensee 是 agent 看。
服务端实现:
1. remotedevice 新增 screensee 工具:
- 下发 homeagent-screensee 命令 → 设备截屏回传 jpeg base64
- seeHandler 回调(agent 核心注入)用视觉模型自动描述屏幕内容
- 未授权/离线/超时完整错误路径; 结果留档 cmdresult
2. SDK LLMMessage 扩展多模态 Blocks(text/image_url):
- llm_impl 转换为 agentAPI.ContentBlock, 视觉模型可看图
3. describeScreen: 默认提示词描述窗口/文字/界面状态;
provider 参数可指定视觉源(临时切换后恢复)
GUI 端需配套(已发群): onDeviceMsg 加 case "screensee",
desktopCapturer 截屏 → jpeg base64 data URL 回执(同 camerasue 抓拍模式)。
测试: 端到端模拟设备截屏回传+视觉回调验证; 全项目 go test 通过
2026-08-21 11:31:45 +08:00
da327ac3e1
feat: 导出单插件重载到 SDK (CORE_PLUGIN_RELOAD_ONE/48 + LIST/49 + IS_DISABLED/50)
...
- PluginManager 接口新增 ReloadOne(name) error
- cabi dispatch: case 48 单插件重载, 49 列已加载, 50 查询禁用
- 外部 SDK 新增 PluginMgrAPI(ReloadOne/ListLoadedPlugins/IsPluginDisabled) + bridge dispatchPluginMgr 注入
2026-08-16 18:27:25 +08:00
147d0baaf9
fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持
...
- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对
- agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式
- agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch
- webui: server 输出通道适配器(保留 reasoning_content/disable_thinking)
- GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
2026-08-14 00:48:40 +08:00
9d5a914941
Phase 0.1/1/3/6: 核心生产问题修复
...
Healthcheck 隔离 (Phase 0.1):
- 新增 internal/sdk/selftest.go: VirtualInstance 完全隔离自检空间
- PluginSDK.Selftest()/SelftestReset() 暴露隔离实例 (含 mutex)
- LLM 自检只读白名单 isSafeReadonlyTool 防写类工具污染生产
- 单测验证: healthcheck 后生产实例内容不变 + 无残留
- 存量清理: 删除 gotest/luatest 残留目录
GraphDB 去重 (Phase 1):
- migrateRelationUnique: 启动自动重建 relations 表加 UNIQUE 约束并去重
- Commit 改为存在性检查, 重复三元组仅刷新 confidence 不重复插入
- 3 个 dedup 单测全绿
配置时长解析 (Phase 3):
- parseDurationExtended 支持 2d/1w/3h 等人类可读单位
- GetDuration 全局生效, 防 2d 静默回退 30m
Agentcli 通知风暴治理 (Phase 6):
- 语义通知: 累积 notify_bytes(2KB) 或间隔 notify_interval(2s) 触发
- 生命周期即时通知: 启动/进程退出/EOF 立即通知
- 可配置 settings, 保留通知机制保证 agent 感知终端存在
- 运维止血: 已杀掉幽灵 PID 3716282 (bash git sparse clone 运行 16h)
Plan.md: 新增设计意图备忘(插件即App/分层记忆), 更新各 Phase 进度
2026-08-12 13:51:40 +08:00
f960fde785
agent: 更智能的 LLM provider 调度(byModel 精确路由 + AUTO 优先级链)
...
吸收 llmsproxy 的调度思想适配 HomeAgent“一源一模型”结构:
- RoutableProvider{Model,Priority} 次级接口(不破坏既有 Provider 实现)
- ProviderManager.OrderedProviders 改为按 (优先级 desc, 可用, 默认优先) 稳定排序,
AUTO/空模型走该优先级链
- 新增 ProviderManager.ResolveForModel:精确模型名路由到归属源,找不到回落 AUTO 链
- LuaAdaptedProvider 不再无条件覆写 req.Model;显式模型名原样转发
- LLMSource.Priority + core.llm.sources.<name>.priority 配置项
- process.go: 显式模型走 ResolveForModel,AUTO 走 OrderedProviders
- 新增路由单测(优先级排序 + byModel 解析)
验证: go test ./... 27 包 0 失败;Windows 交叉编译通过;部署后服务健康
2026-08-10 11:54:45 +08:00
f0dacef281
lua: 吸收 llmsproxy 适配器高级特性(worker 池/静态预提取/动态签名钩子)
...
- 适配器 worker 池化:单 LState+全局锁(串行瓶颈)→ 每 adapter 一个 gopher-lua
LState 池,按使用该 adapter 的源并发上限求和配置池大小,并发 transform 互不阻塞
- staticInfo 预提取:name/version/endpoint/headers 加载期编译缓存,Endpoint/Headers
读缓存不占 worker;加载即预编译首个 worker
- build_headers 动态钩子 + hmac/sha256/base64/tohex 全局:签名型上游(kimicode 等)可接入
- provider applyAdapterHeaders 接入动态头(url/method/body/api_key/timestamp/source 元数据),
未定义时回落静态 headers,缺省补 Authorization
- LLMSource.MaxConcurrent + core.llm.sources.<name>.max_concurrent,注册时汇总
VM.ConfigureConcurrency
- 新增 Lua VM 测试(load/transform/build_headers/并发)
验证: go test ./... 27 包 0 失败;Windows 交叉编译通过;部署后 9 adapter 全部预加载
2026-08-10 11:29:57 +08:00
8dcce5a3a9
webui 删除彻底化:清理残留 config/defs/provider 悬空
...
审计发现 webui 多处删除只清一处、留下鬼影:
- RemovePlugin: 仅 DROP config_<name> 与 defs,config 表 plugin.<name>.* 键永不删 →
补 DELETE config LIKE 'plugin.<name>.%'
- ConfigRegistry.Delete: 删 core.llm.sources.<name>.* 后遗留 ConfigDef、且删 core.llm.*
不更新 guard 快照(重启/failback 会把已删源复活)→ 同步清 source defs + 写 llm 快照
- llm_impl.ReloadFromConfig: 删除默认源后 core.llm.provider 悬空,SetDefault 指向不存在源
→ 仅当源仍注册时才 SetDefault
- clawhubadapter 测试 mockSettings 补 RemoveCore/RemovePlugin
- mcp 服务器删除仍为进程内需 reload 生效(符合既有 removeServerHandler 语义)
验证: go test ./... 26 包 0 失败;本机部署 sources=3、27 插件加载、对话正常
2026-08-10 09:51:30 +08:00
cd2a27a8cc
webui: 修复 '删除源/服务器' 只置 <nil> 不真删的问题
...
前端 deleteSource/deleteMCPServer 通过 PUT value:null 删除,但后端 handleSettings
PUT 只 SetCore/SetPlugin(fmt.Sprint(nil) → 字面 '<nil>'),导致 core.llm.sources.<name>.*
等键残留不可达的 <nil> 行,污染 LLM 源与探活。
- SettingsAPI 新增 RemoveCore/RemovePlugin;settingsImpl 接入 ConfigRegistry.Delete
- webui handleSettings PUT:body.Value==null 时改走删除分支(核心表/插件表均适配)
- 现状验证:mocktest.* 残留已从本机 config.db 清除,sources=3,无无效源
2026-08-10 09:35:29 +08:00
171e6f233b
llm: 统一源接入层修复(对照 llmsproxy)
...
- provider: 新增 OpenAI-compatible 响应/流兜底解析,Lua adapter 异常时也能解析
choices/message/tool_calls/usage(含 function.arguments 缺失、对象/字符串参数)
- 过滤无效 LLM 源(<nil>/空/缺 http(s) scheme),main 与 ReloadFromConfig 均跳过,
避免 mocktest 等坏源污染 fallback 与 healthcheck
- adapter(openai/deepseek/groq/mistral/github/kimicode): 修 tool_calls 对
nil function 的崩溃,兼容扁平/嵌套结构;openai 流透传 reasoning/tool_calls
- config: ToConfig 探活端点过滤无效 base_url,修复 supervisor 误报 LLM unreachable
2026-08-09 20:39:54 +08:00
09faa2874a
插件删除回调:onRemove 生命周期(仅卸载触发,重载不触发)
...
- 公共 SDK(third_party/homeagent-sdk):RegisterOnRemoveHandler/RunOnRemoveHandlers
(后注册先执行、幂等);内部 SDK PluginManager 接口新增 RemovePlugin
- registry.RemovePlugin:runStopHandlers → Stop → 清理 plugins/sdkRefs/instances
→ runOnRemoveHandlers → toolCleaner.UnregisterPluginTools → cfgReg.RemoveDisabledPlugin
- pluginmgr.removePlugin 先调 Registry.RemovePlugin 再删目录
- 配置清理:PluginSettings.Remove(key)(内部 SettingsAPI + settingsImpl)
- 演示:timer 插件 onRemove 删 max_duration 键;plugindev 模板同步 onRemove 示例;
SDK example/calendar cleanupData 删 events.json;manager plugins/uninstall 先停通道
2026-08-02 13:32:44 +08:00
76ef49e9a6
clawhubadapter: OpenClaw 通道插件兼容修复(gateway 生命周期桥 + channelRuntime + deliver 事件式输出)
...
- manager/main.js:makeChannelRuntime(dispatchReplyWithBufferedBlockDispatcher 入站 + deliver 出站
绑定 + typingCallbacks)、startChannels/stopChannels(gateway.startAccount fire-and-forget 生命周期
桥、listAccountIds/resolveAccount 规范签名 cfg 传参)、tools/call 通道分支无 outbound 走 deliver
事件式发送、SIGTERM 优雅停靠、console 输出重定向 stderr 防 JSON-RPC 流污染
- plugin.go:channel_input 改 InjectInputSync 同步注入取回复并经 CallTool 回发通道(修复
InjectInterruptText 无 ResponseCh 致回复静默丢弃);channel_status/channel_output 通知接入
- registry.go:channelStatus 状态缓存
- SDK:公共 IOInjector 增加 InjectInputSync(source, channel, text) string + ioAdapter 实现
- 生产验证:微信发消息 → pollLoop → dispatchReply → mock LLM 回文本 → deliver →
ilink/bot/sendmessage status=200 送达
2026-08-02 13:03:49 +08:00
c7ee45d6e1
refactor: remove core skill direct loading, skills owned by clawhubadapter only
...
- Drop skill.NewManager from homed bootstrap; skills dir no longer core-managed
- Remove GetInjectedPrompt system-prompt injection (skills are not first-class)
- Delete internal/skill package, SkillAPI, webui /api/v1/skills, status skills block
- ConfigRegistry: plugin config tables now created only via RegisterDef; arbitrary
scope Set/Get no longer implicitly creates config_<name> tables (fixes stray
config_today_task table from SKILL directory name being used as a scope)
2026-08-02 11:40:13 +08:00
dbbd73b930
refactor: migrate built-in plugins to SDK-only interface
...
- Six-phase plan complete: webui/cli/healthcheck/pluginmgr/clawhubadapter
now interact with the kernel exclusively via internal/sdk interfaces;
all Configure() calls and package-level global injection removed
- buildSDK in internal/plugin/registry.go is the single assembly point
- Add internal/sdk/events.go exporting event types/constants
- Fix ProviderManager cooldown sharing: LuaAdaptedProvider.Name() now
returns the source name instead of lua_<adapter>, so multiple sources
sharing an adapter (single script load via shared VM AdapterCache) no
longer share failure-cooldown state
- Verified: build/vet/tests green, deployed to homeagent.service with
full plugin capability testing via local OpenAI-compatible mock
2026-08-01 12:17:17 +08:00
796a48dae5
plugin disable system: kernel→SDK PluginManager + WebUI/CLI
...
- New disabled_plugins table (name, disabled_at, disabled_by)
- SDK.PluginManager interface: DisablePlugin/EnablePlugin/ListDisabledPlugins
- Registry implements PluginManager, wired into PluginSDK
- WebUI: POST /api/v1/plugins/<name>/disable|enable + plugins page with toggle
- Disabling webui shows confirmation dialog
- CLI: /plugin disable <name> / /plugin enable <name>
- pluginmgr removePlugin sync-cleanup from disabled_plugins table
2026-07-29 15:07:32 +08:00
a899d777c3
sdk: embed non-toolchain SDK in third_party, add NoMemory/Cleaner support
...
- Embed sdk/, example/, meta/, go.mod from homeagent-sdk (no .git)
- Core .gitignore excludes SDK toolchain: bin/, tools/, package/
- RegisterInputChannel + ChannelDef(NoMemory, Cleaner) in SDK
- IOManager input channel registry with GetInputChannelDef
- eventloop: apply channel Cleaner/NoMemory to interrupt text
- context engine: channelDefLookup applied in textForVector
- document store: ChannelCleaner param for archive functions
- All callers/adapters updated with ChannelDef{} default
2026-07-29 14:48:23 +08:00
e992e1ff84
v0.7.2: fix cfgmgr plugin list and core config key prefix
2026-07-25 12:18:35 +08:00
c9e67d3d55
docs: 修正全部文档使其与源码实现一致
...
主仓库:
- 修复 4 份英文文档语言切换链接指向错误 (../zh/ → ../en/)
- ARCHITECTURE.md 标题 "三种加载方式" → "四种加载方式" (实际表格4行)
- PLUGIN_DEV.md 示例表: 添加 webfetch, 移除不存在的 luaplugintest/testlua
- PLUGIN_DEV.md 代码示例: InjectInput/InjectInterrupt → InjectText/InjectInterruptText
- PLUGIN_DEV.md 代码示例: Memory/Knowledge/LLM/Events 接口签名修正
- PLUGIN_DEV.md .hmap 内容统一, plugindev 编译去除 .exe 后缀
SDK 仓库:
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化
修正前一次会话中的 QQ/Bili 插件问题:
- qq napcat() 超时, fetchBotInfo 竞态, handleWebhook 同步阻塞
- bili CDN 直连失败, 添加 HTTP_PROXY 代理
2026-07-18 20:46:58 +08:00
3cad4b635f
RegisterStage: add scope parameter, remove RegisterStageOwnTools
...
- SDK: RegisterStage(stage, handler, scope...) with StageScopeGlobal/StageScopeOwnTools
- Delete RegisterStageOwnTools, migrate cmd and qq plugins
- Internal SDK: add StageScope alias
- Docs: update all zh/en docs for C ABI buildmode and stage scope
- C ABI: fix nil errorOut in Handle.Start/Handle.Stop (SIGSEGV fix)
- C ABI header: add dispatch IDs 26-45 for Settings/Doc/Knowledge/LLM/Social/TextMemory
2026-07-17 11:25:54 +08:00
7f28b997e6
feat: output channel redesign - per-channel output gates, LLM chain events, SDKConfig
...
- Output channels generate per-channel tools: output_send__{name} (type=output) + output_send__{name}_help
- content is JSON string transparently passed to plugin handler for routing
- EventAgentLLMChain: full LLM response forwarded after each turn for webui/logs
- sdk.New refactored to SDKConfig struct (no more 13 positional args)
- RegisterOutputChannel adds desc param for JSON format documentation
- channelDevice simplified (no Tools method), desc field added
- Child agent permission updated for output_send__ prefix
- System prompt: output gates, multi-call, long messages split
- WebUI: subscribes to EventAgentLLMChain in SSE, no output channel
- Tests updated for new naming convention
2026-07-16 12:11:16 +08:00
1f1233b823
refactor: P0-P3 fixes, C1 cleanup, architecture diagrams, go.work upgrade
...
- P0-1: ProviderError type + ReportStatus for precise 401/403 detection
- P0-2: Remove -config flag from deploy/homeagent.service
- P2-1: 5s debounce on context.go Save()
- P2-2→C1: Delete output_set_channel entirely
- P2-3: Extract mediaDataURL/mediaChat helpers
- P2-4: Dedup defaultSources var
- P3: Delete dead packages (embed/tokenizer/container/snapshot)
- P3: Delete dead functions (messagesToMap, RunStageAll)
- CL: Update .gitignore, docs, Makefile, gojieba removal
- Config: Delete config/config.yaml, update docs
- Arch: Remove EmitOutputTo from emitResponse
- CL-1: go.work 1.19→1.21
- Docs: Add Mermaid architecture diagrams to README
- Docs: Add kernel-rebuild requires plugin-rebuild note to PLUGIN_DEV.md
2026-07-12 11:42:56 +08:00
50e5dec745
feat: converge dynamic plugins onto canonical homeagent-sdk
...
- Separate built-in plugin interface from external plugin interface
- Route dynamic plugin loading through homeagent-sdk/sdk using reflection
- Turn internal/sdk into an enhanced wrapper over canonical SDK types
- Vendor SDK repo snapshot under third_party/homeagent-sdk for stable builds
- Keep internal constructors/adapters for memory, knowledge, llm, settings
- Align dynamic QQ loading with canonical SDK chain
2026-07-06 19:27:51 +08:00
a7e06fba77
feat: expose tool owner plugin in stage context
...
- add Plugin field to ToolDef, ToolCall, ToolResult
- track tool owner in StageHost
- annotate before/after_toolcall stage context with tool plugin
- add RegisterStageOwnTools() for plugin-scoped tool listeners
- keep interrupt input source/output channel context in stage messages
2026-07-06 17:25:18 +08:00
8cec92d947
feat: 路径配置化 + 裸二进制启动 + pluginmgr 内置插件
...
- types.go: 新增 PluginDirConfig 结构体嵌入 Config
- config/registry.go: 新增7个路径配置项 (core.plugin.dir 等) + ConfigDef 元数据
- cmd/homed/main.go: -data 默认自动检测二进制同级目录,使用 cfg.Plugin.Dir
- internal/plugins/pluginmgr/: 内置插件实现 (4工具 + HTTP API + 包校验)
- all.go: 注册 pluginmgr
- manifest.go: 扩展 PluginManifest 字段
- sdk/settings.go: RegisterDef / Defs 接口
- webui: 设置页自动发现 ConfigDef 元数据
- config/config.go, config/config.yaml: 清理 YAML 死代码
- sdk/plugin.go: IO 通道泛型化支持非文本类型
- waiter: CLI 支持 socket 发现和交互模式
2026-07-04 16:56:32 +08:00
c3816dc699
IO 抽象层增强:非文本输入支持 + waiter CLI 重写
...
PluginSDK:
- 添加 InjectInput / InjectInputSync / InjectInterrupt 泛型接口
- 插件现在可注入 image/audio/file 等任意类型输入
Provider:
- 添加 ContentBlock / ImageURL / AudioURL 类型
- Message 增加 Blocks 字段,Content 在非空 Blocks 时序列化为数组(多模态格式)
Agent:
- handleInput 新增 image/audio 类型分发 → processMediaInput
- processMediaInput 将媒体数据附着到对话上下文,LLM 自主决策处理策略
- 新增内置工具:describe_image / transcribe_audio / ocr_image(pendingMedia 驱动)
- 工具仅当有未处理媒体数据时注册,通过 Provider 直接调用多模态模型
Config:
- 新增 InputProcessingConfig(image/audio 处理配置)
- 含 fallback_provider / describe_prompt / ocr_enabled 等选项
Waiter CLI 重写:
- 配置文件 ~/.config/homeagent/cli.yaml(自动发现 socket)
- 原始终端行编辑 + 命令历史持久化 + 彩色输出
- 内置命令:/help /reconnect /connect /remote /local /prompt
- 断线自动重连
2026-07-04 15:01:35 +08:00
fab58e709a
feat: complete P0/P1/P2 — WebUI SPA, OpenClaw sidecar+simulator, healthcheck auto-sched+perf
...
P0: WebUI重构
- 完整 SPA 仪表盘 (7标签页), //go:embed dashboard.html
P1: OpenClaw兼容 (三通道: SKILL.md / sidecar / simulator)
- Node.js 模拟进程统一加载任意 OpenClaw 插件
- JSON-RPC 2.0 over stdio 协议, go:embed 内嵌
P2: Healthcheck 优化
- 定时自动执行 (startAutoCheck, 30min)
- healthcheck_perf 性能监控工具
其他: agentcli/cmd 插件, integration_test, status.go,
test_deepseek 清理, 多项 bug 修复
2026-07-04 12:51:32 +08:00
d4956c23f0
feat: OpenAI 兼容端点完整实现 + reasoning_content 输出通道
...
- StageContext 新增 ReasoningContent + TokenUsage 字段
- emitResponse 将 reasoning_content / usage 传入 Payload
- /v1/chat/completions:
- 非流式返回 reasoning_content + token_usage
- 流式 (stream=true) SSE 分块返回 reasoning/content/finish chunk
- token_usage 来自精确 LLM 回报而非估算
2026-07-03 20:49:43 +08:00
2d314b3e9c
重构: 插件自注册 + .so 动态加载 + 中断打断机制
...
- 所有内置插件 init() 自注册 (plugin.RegisterFactory), 移除 main.go 硬编码
- 新增 .so 动态加载器 (internal/plugin/dynamic.go), 插件可编译为 plugin.so
- 新增 plugin.json 元数据 (internal/plugin/manifest.go)
- 新增 interceptLoop 独立 goroutine:
(a) cancelLLM() 取消进行中的 HTTP 请求
(b) interceptCh → drainInterrupt() 注入 [打断消息] 到 LLM 上下文
(c) InjectInput 空闲时触发新处理循环
- 新增 internal/plugins/all.go 空白导入触发所有内置插件 init()
- internal/sdk/ 作为 PluginSDK 正式 Go API
- internal/api/ → internal/plugins/webui/ 迁移
- 删除旧 cmd/cli/, 使用 cmd/waiter/ 替代
- 更新 PLAN.md / ARCHITECTURE.md / README.md 文档
2026-07-03 16:53:34 +08:00