Files
HomeAgent/docs/zh/plan.md
root bc9ef15eb0 三层回退恢复机制(L0写前留档/L1恢复梯子/L2离线回滚)+ guard 父守护
- L0: files 插件写受保护系统路径(/etc 等)前自动留档,AbstractBeforeWrite 到 data/file_baseline
- L1: failback 受限 worker 执行恢复梯子 probe→还原DNS/proxy→还原LLM配置+ReloadFromConfig→probe,N轮有界
- L2: tracker changeset 持久化原文 blob,guard 离线 RollbackFromDisk 回滚 agentfs;SystemSnapshot 支撑
- guard 父守护: 心跳 IPC(PING/ACK unix socket, 文件心跳回退)、失败计数、退出码协议(42/43/44)、最后手段
- 发行版路径适配: system.protected_paths/network_paths 可注入,默认面向主流 Linux
- Windows 兼容: guard.go/failback.go 加 //go:build linux, guard_windows.go 提供 no-op 桩
- 修复: guard.yaml last_resort 键冲突、changeset Content 不落盘导致离线回滚丢原文

Build 全绿, vet 干净, system/recovery/ipc/tracker 单元测试全过
2026-08-05 16:00:08 +08:00

25 KiB
Raw Blame History

WebUI 布局与配置归位修复计划

一、背景

上一轮 SDK 接口化改造完成并部署后,用户指出三个问题:

  1. WebUI 窄屏布局损坏:顶部 <nav> 为桌面式横排(标题 + 6 tab + 连接指示器 + 语言 + 主题), 窄屏断点仅缩小字号不换行,body { overflow-x:hidden } 直接把溢出的 tab 裁掉不可点击。
  2. OpenClaw skills 目录被注册为核心配置:core.skills.path("OpenClaw 技能存储目录")注册在核心 配置表(internal/config/registry.go),但全仓无任何读取方(死配置);实际生效路径是 clawhubadapter 自己的 skills_dir 配置(config_clawhubadapter 表 + core.daemon.data_dir/skills 兜底)。 技能目录是 clawhubadapter 适配加载的领域,不应属于核心配置。
  3. clawhubadapter 加载的微信插件成为独立配置项:设置页出现 channels.wechat.*(核心表)、 plugin.wechat.*(config_wechat 表)、config_openclaw_weixin(空表)等多处微信配置, 全部为历史残留——当前代码零引用,OC 技能的配置实际在其自身 ~/.openclaw/openclaw.json。 设置页会把核心表全部键 + 全部插件表当作配置组展示,导致残留以"独立配置项"形态出现。

二、修复计划

# 动作 位置 风险
A 窄屏导航修复:<768px 下 nav 横向滚动、h1 缩写、连接指示器简化;body 溢出裁切改为 nav 内滚动 cmd/gui/renderer/style.css 无
B 删除 core.skills.path 核心配置注册(set + RegisterDef 两处) internal/config/registry.go 无(无读取方)
C 备份后清理残留配置:channels.wechat.* 键、config_wechat / config_openclaw_weixin / config_openclaw 表(含微信 token,先备份) 生产库 /home/newqqagent/config.db 低(当前代码不读)

三、实施记录

步骤 A:webui 窄屏导航修复(已完成)

  • 修复对象为 webui HTTP 服务真正前端 internal/plugins/webui/dashboard.html(go:embed 内嵌, 登录后 / 返回,104KB;cmd/gui 是独立 electron 客户端,非 webui 一部分)。
  • <768px 断点:nav { overflow-x:auto; scrollbar-width:none; flex-wrap:nowrap } + ::-webkit-scrollbar { display:none }; nav a { white-space:nowrap; flex-shrink:0 };nav h1 { font-size:0 }(保留 logo 图、隐藏文字,弥补窄屏空间); nav > div { flex-shrink:0 } 右侧语言/主题/退出按钮不压缩。
  • 顺带在 cmd/gui(electron 客户端)同步了窄屏样式与消息来源徽标(app.js/style.css,客户端窗口缩放同样受益; 客户端需另行构建 electron 应用才生效)。
  • 验证:部署后 / 返回的 dashboard 含 scrollbar-width:none/font-size:0/::-webkit-scrollbar 规则。

步骤 B:删除 core.skills.path 核心配置(已完成)

  • 删除 internal/config/registry.go 两处:set("core.skills.path", ...)(SeedDefaults)与 reg(ConfigDef{Key:"core.skills.path", ...})(定义注册)。
  • 理由:该键全仓无读取方(grep 仅命中注册处),实际生效路径是 clawhubadapter 的 skills_dir (config_clawhubadapter 表 + core.daemon.data_dir/skills 兜底)。技能目录属 clawhubadapter 适配领域。
  • 验证:grep -rn "skills.path" --include="*.go" 零命中;部署后设置页无 core.skills.path。

步骤 C:清理生产库残留配置(已完成)

  • 操作前 sqlite3 .backup /tmp/opencode/config.db.pre-clean.bak(含微信 token 数据)。
  • 删除:config 表 channels.wechat.* 3 键 + core.skills.path 键;DROP TABLE config_wechat / config_openclaw_weixin / config_openclaw(三者均为历史残留:当前代码零引用,clawhubadapter 实际 使用 config_clawhubadapter 表;OC 技能配置在其自身 ~/.openclaw/openclaw.json)。
  • 验证:设置页总键数 138→129,无 wechat/weixin/skills.path 残留,plugin.clawhubadapter.skills_dir / simulator_dir 正常;服务 healthcheck ready、clawhubadapter "OC plugin manager started"。

SDK Stop 注册接口(RegisterStopHandler)计划

一、背景

2026-08-01 20:00 起生产 homeagent 进入崩溃循环(fatal error: thread exhaustion, systemd 重启计数 61+)。排查定位为 SDK 示例插件 calendar(示例源码在 SDK 仓库 example/calendar,生产以 plugin.so 形态加载)三个缺陷叠加:

  1. 农历引擎 3 个 bug(daysInLunarYear 位循环 i > 0 应 i > 0x8、缺闰月天数、 lunarToSolar 内层重复加闰月)→ lunarToSolar(2026,4,12) 返回 2062-11-16(偏移 36 年), 农历重复事件(lunar_yearly)的 next 被生成到遥远错误日期。
  2. cleanupPastEvents 保留过时重复事件 → 每 30s ticker 对已到点的重复事件再生成一份 next, 事件从 7 个爆炸到 45612 个(15MB events.json)。
  3. 无提醒投递保护:15018 份同时到点的事件一次性 go sdk.InjectInterruptText(...) 投递 → interrupt 风暴 → goroutine/线程耗尽。

处置:修复农历引擎 3 处 + next 去重 + 清理过时重复事件,用新版 SDK 仓库 + 新版 plugindev 工具链 重建 calendar_linux_amd64.hmap,经 webui POST /api/v1/plugins(透明代理到 pluginmgr 安装接口) 重装,重启验证收敛(事件 4 个、next 正确生成 2027-05-17、0 崩溃)。

过程中暴露的能力缺口:SDK 只有 Plugin 接口的 Name/Start/Stop,没有 stop 注册接口 (RegisterStopHandler/OnStop 均不存在,SDK v0.7.2/v0.8.0/master 一致)。插件停止时只能在自己的 Stop() 里写清理逻辑,SDK 层无法统一执行"停止时清理"回调;calendar 的 Stop() { p.saveEvents() } 还会用陈旧内存把已清理的数据写回磁盘(曾导致删除的重复事件复活)。

二、计划

# 动作 位置 风险
1 公共 SDK PluginSDK 加 RegisterStopHandler(fn func()) + RunStopHandlers()(幂等、后注册先执行),两处同步 third_party/homeagent-sdk/sdk/plugin.go、SDK 仓库 sdk/plugin.go 低(纯新增,内置 SDK 内嵌透传)
2 内核 Registry 保存每插件 SDK 引用(sdkRefs),StopAll/ReloadOne/DisablePlugin 调 Stop() 前执行 RunStopHandlers internal/plugin/registry.go 中(生命周期路径,需回归 reload/disable)
3 工具链 plugindev:z_bridge 模板 bridgeState 存 SDK,StopPlugin 先 RunStopHandlers() 再 plugin.Stop();init 脚手架模板加演示 SDK 仓库 tools/plugindev/templates.go、templates/main.go.tmpl 低
4 内置示例插件演示(如 timer:ticker 停止改为 stop handler) internal/plugins/timer/plugin.go 低
5 外部示例插件同步(example/calendar 的 saveEvents 改由 stop handler 执行,验证 z_bridge 链路;其余 example 加演示) SDK 仓库 example/* 低
6 文档同步:SDK README 生命周期章节 + 主仓插件开发文档 SDK 仓库 README.md/README_EN.md 等 无

三、实施记录

  1. SDK 公共层(RegisterStopHandler + RunStopHandlers:后注册先执行、执行后清空幂等)已落地 third_party/homeagent-sdk/sdk/plugin.go,并同步到 SDK 仓库 /tmp/opencode/sdk-repo/sdk/plugin.go(两处一致)。
  2. 内核 Registry(internal/plugin/registry.go)新增 sdkRefs map[string]*sdk.PluginSDK + runStopHandlers, loadOne 注册、StopAll/ReloadOne/DisablePlugin 在 Stop() 前执行(共 4 处调用点)。
  3. 工具链 plugindev(SDK 仓库):linux tmplLinuxBridge 的 go_stop_plugin 先 RunStopHandlers() 再 plg.Stop(); windows tmplBridge 的 bridgeState 加 sdk 字段、StopPlugin 同链路;tmplPluginGo + main.go.tmpl 脚手架加 RegisterStopHandler 演示。plugindev 重新编译通过(GOPATH=/root/go)。
  4. 内置 timer 插件演示:close(p.stopCh) 移入 stop handler,Stop() 只 wg.Wait()。
  5. 外部示例:example/calendar 的 saveEvents 改为 s.RegisterStopHandler(p.saveEvents), Stop() 删除写盘调用(持久化交由 stop handler,避免陈旧内存复活已删事件)。
  6. 文档:SDK 仓库 README.md/README_EN.md 生命周期章节补充 RegisterStopHandler 说明。
  7. 构建测试:主仓 go build ./... + go test ./internal/sdk/... ./internal/plugin/... 全绿; SDK 仓库 go build ./... + go test ./sdk/... 全绿。
  8. 生产部署验证:新 plugindev(--no-bundle)重建 calendar_linux_amd64.hmap(md5 084e97c0…,strings 确认 go_stop_plugin → RunStopHandlers → saveEvents 编译进 plugin.so);webui API 删旧装新;重装新内核 homed(含 sdkRefs/runStopHandlers);两次重启事件稳定 3 个不复活、events.json mtime 与 stop 时刻吻合 (saveEvents 经 stop handler 真实执行)、0 次 thread exhaustion、服务 active。

clawhubadapter OpenClaw 通道插件兼容修复计划

一、背景

生产 core.llm.provider 已是 mock LLM 源(core.llm.sources.mocktest,base_url http://127.0.0.1:18080/v1、model mock-model、adapter openai),mock LLM 服务常驻运行。 借助 mock 通道插件(/tmp/opencode/mock-skills/mock-wechat/,完全复刻 openclaw-weixin 的 真实注册格式 register(api) → api.registerChannel({ plugin: ChannelPlugin }))放入生产 skills 目录 端到端复现,得出如下结论:

已验证可用链路:manager 加载 mock 插件 → Go 端识别 ocplugin mock-wechat handled by manager → 注册工具 mock-wechat_read_mock_wechat_input/mock-wechat_mock_echo → RegisterOutputChannel("mock-wechat") → mock 自推消息经 channel_input 通知 → [agent] interrupt from manager/mock-wechat → mock LLM 正常回复(195ms)。

复现的核心缺陷(真实通道插件 wechat/dingding"根本不可用"的根因):

  1. 输出断链:manager tools/call 通道分支只认 channelPlugin.outbound.sendText/sendMedia (旧格式),真实 ChannelPlugin(openclaw-weixin 等)无 outbound → tools/call mock-wechat → error: "channel mock-wechat has no output handler"。
  2. 生命周期静止:manager mock api 从不调用 gateway.startAccount/stopAccount,也无 api.runtime/channelRuntime → 通道插件加载后永不启动(不登录、不轮询、不收消息)。
  3. 输入依赖错位:真实插件把消息经 channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher 推送(manager 完全无此对象),而不是调 api.submitInput。
  4. stdout 污染:插件 console.log 直接进 JSON-RPC 流,Go 端 readLoop 跳过非 JSON 行,有丢通知风险。

wechat 通道的心跳机制(openclaw-weixin/dist/index.js pollLoop,448 行起):每账号一个常驻 pollLoop,循环 POST ilink/bot/getupdates(body {get_updates_buf},超时 35s)——长轮询即心跳: 服务器收到 poll 请求即知通道在线,新消息随 poll 响应 push 回来;超时视为空响应继续轮询,真错误延时 5s 重试。与 gateway 生命周期强绑定:

  • pollLoop 只由 gateway.startAccount(ctx) 启动;不被调用 → 心跳/收消息全断(服务器侧认为通道离线)。
  • startAccount 末尾 await new Promise(()=>{}) 永久挂起——OC gateway 靠它配合 health-monitor (startAccount 退出 → 判定账号崩溃 → 重启账号)。manager 调 startAccount 必须 fire-and-forget。
  • 停靠 gateway.stopAccount(ctx)(ctx.account.accountId 定位),停止时经 statusSinks 调 ctx.setStatus({running:false, connected:false, lastStopAt});启动即上报 ctx.getStatus()/ctx.setStatus({...running:true, connected:true, lastStartAt})——connected 是 gateway 判断账号存活的依据。
  • sendTyping(ilink/bot/sendtyping + typing_ticket)是打字指示,非心跳,无需支持。

二、修复计划

# 动作 位置 风险
A manager 提供 gateway 生命周期桥:channel 插件注册后自动 gateway.startAccount(ctx)(fire-and-forget,不等待挂起的 Promise),构造完整 ctx {account, cfg, channelRuntime, getStatus, setStatus};Go 端 channel_stop 通知 → stopAccount internal/plugins/clawhubadapter/manager/main.js 中
B 实现 channelRuntime mock:reply.dispatchReplyWithBufferedBlockDispatcher(deliver 回调 → channel_output 通知送 Go 端)、getPolls(OC 通用通道轮询输入)、call 透传 同上 中
C tools/call 通道分支改造:无 outbound 的 ChannelPlugin 改走 channelRuntime 事件式发送(agent 输出 → deliver),不再报 "no output handler" 同上 低
D 状态上报透传:setStatus 经 channel_status 通知 → Go 端可查;health-monitor 语义(startAccount 保持挂起) 同上 低
E stdout 卫生:插件 console.log 重定向 stderr(或 JSON-RPC 流感知封装),杜绝污染 同上 低
F Go 端:channel_output/channel_status 通知接入(事件分发),通道输出 handler 保持 sp.CallTool internal/plugins/clawhubadapter/registry.go、plugin.go 中
G 端到端验证:mock 通道插件 + mock LLM(生产环境,临时放入/移出 skills 目录)复跑全链路(登录启动→收消息→回复→出站→停止) 生产 低

三、实施记录

(逐步填写)

  1. manager/main.js — 通道运行时与生命周期桥(已完成,独立运行验证)

    • makeChannelRuntime(chName, ch):reply.dispatchReplyWithBufferedBlockDispatcher(opts) 提取 dispatcherOptions.deliver/typingCallbacks 按 ctx.AccountId 挂到 ch.deliverers,随后 notify('channel_input', {channel, payload:{content: BodyForAgent||Body, from, sessionKey, accountId, messageSid, chatType, raw}}) 入站;返回 dispatcher(sendNow/addToBuffer/sendBuffer/closeBuffer)。 chatPolls/getPolls 空转(防断连误判)、call 转发 channel_output 通知。
    • startChannels(name):channel 插件注册后自动枚举账号(config.listAccountIds→resolveAccount, 缺省 ['default']),构造完整 ctx {account, cfg, channelRuntime, getStatus, setStatus}, fire-and-forget 调 gateway.startAccount(真实插件会永久挂起,绝不等待);崩溃/状态变更经 channel_status 通知透传(health-monitor 语义:startAccount 不退出=账号存活)。
    • stopChannels(name):逐个账号 gateway.stopAccount;进程 SIGTERM/SIGINT 时统一执行优雅停靠。
    • tools/call 通道分支:保留 outbound(旧格式)→ 新增 deliver 事件式发送 (deliverItem 按 accountId 取 deliver + typingCallbacks.onReplyStart/onCleanup 包裹)→ 无 deliver 时降级 channel_output 通知 → 兜底报错。不再出现 "has no output handler"。
    • stdout 卫生:全局 console.log 重定向 stderr,JSON-RPC 流仅承载协议帧。
  2. mock 插件升级(/tmp/opencode/mock-skills/mock-wechat/index.js):完全复刻真实 weixin 行为—— gateway.startAccount 永久挂起 + setStatus 上报 + setInterval 心跳轮询 + 800ms 后经 dispatchReplyWithBufferedBlockDispatcher 推送入站(deliver 本地记录发送);stopAccount 停轮询+状态置否。

  3. manager 独立运行验证(通过):channel_status 启动上报(running=true connected=true); tools/call mock-wechat → {"status":"sent","via":"channelRuntime.deliver"},插件 deliver 收到 text="hello from agent" 且 typing onReplyStart/onCleanup 正确包裹;心跳 poll #1-4 常驻; SIGTERM → stopAccount called 退出码 0;插件 console 输出全部走 stderr(协议流零污染)。

  4. Go 端通知接入(plugin.go translateAndRegister + registry.go 状态缓存):channel_status 存 channelStatus map(可查)+ 日志;channel_output 降级事件日志。go build ./... 通过。

  5. 回复闭环修复(同步注入):channel_input → InjectInterruptText 的 InputEvent 不带 ResponseCh (internal/agent/io/channel.go:276),agent 回复在 emitResponse(eventloop.go:384)被静默丢弃。 改为 s.InjectInputSync(pluginName, channel, "text", payload)(内部 SDK 已有 4 参版本,返回 *OutputEvent)同步等待回复 → 提取 Payload["content"] → sp.CallTool(channel, {payload, meta}) → manager tools/call → deliver → 插件发送 → 微信送达。公共 SDK IOInjector 同步补 InjectInputSync(source, channel, text) string(ioAdapter 实现,供外部插件一致使用)。

  6. mock LLM 恒定文本化:/opt/llm-mock/mock_server.py decide() 删除工具调用分支,一律回文本 ("无论收到什么消息都通过微信插件发送"),保证每条入站消息回复必然走通道输出。

  7. 生产微信闭环验证(通过):用户微信发"你好..." → pollLoop 收到 → dispatchReply → InjectInputSync → mock LLM 回文本 → CallTool(wechat) → deliver → POST ilink/bot/sendmessage → status=200 message_id=7489545365740590088,微信收到"(mock)已收到消息,长度 324 字符。"

  8. 通用性审查(无硬编码):manager/plugin.go/registry.go 均无 weixin/wechat 特判,全部按 OC 规范 字段实现(gateway/config/capabilities/channelRuntime/deliver/typingCallbacks)。修正规范签名参数 约定:listAccountIds(cfg)、resolveAccount(cfg, accountId) 正确传 cfg。

  9. 已知边界(非硬编码,架构性):a) channelRuntime.getPolls/chatPolls 返回空 msgs——依赖 runtime 轮询输入的通用通道型插件收不到消息(weixin/dingding 类自带 pollLoop 的通道不受影响); b) gatewayMethods 登录流程(web.login.start/QR 扫码)未实现(CLI 有 stub),通道凭 token 配置直连;c) startAccount ctx 提供 account/channelRuntime/cfg/getStatus/setStatus 核心字段。

  10. 补充边界 a) getPolls/chatPolls 消息源(已完成):manager makeChannelRuntime 的 chatPolls/getPolls 改为读 ch.pollQueues(按 accountId 队列,poll 取走即消费);新增 channel/send RPC(Go 端注入 → 队列 → 插件轮询取走);Go 端 SendToChannel(channel, payload)

    • ChannelSender() 单例(Start 时置位)。验证:mock-poll 插件(纯 chatPolls 轮询型)—— channel/send → {"status":"queued"} → [mock-poll] poll got msg → dispatchReply → channel_input 入站完整(content/from/sessionKey/accountId/messageSid/chatType)。 微信链路回归正常(Polling started + channel_status running=true)。
  11. 边界 b) 登录流程核实(已解决,无需实现):真实登录机制是 SKILL 脚本旁路—— weixin-openclaw-login SKILL 的 scripts/get-login-url.js(ilink 二维码 URL)+ poll-login-status.py(轮询扫码状态)→ agent 经 exec 执行 → 拿 bot_token 写入 ~/.openclaw-weixin/account.json(2026-07-28 17:31 创建,token 有效)→ 插件启动 resolveAccountData 直读。不依赖 manager gatewayMethods(web.login.start 等 OC gateway 协议 stub 不影响真实使用)。

  12. clawhubadapter 全量管理接口(已完成并验证):向 agent 暴露完整插件/通道管理面——

    • 新工具:clawhubadapter_plugin_info(类型/工具/关联通道详情)、plugin_reload(reloadPlugin)、 channel_list(全部注册通道 + 实时状态)、channel_send(SendToChannel 投递)、 channel_start/channel_stop(manager channel/start、channel/stop RPC)。
    • manager 新增 RPC:plugins/channels(registeredChannels 摘要含 status/accounts)、 channel/start(fire-and-forget startChannels 恢复账号)、channel/stop(stopAccount, 按 channel 全停或按 accountId 单停,省略 channel 则全部停止)。
    • Go 侧 channelSummary(mgr) 合并 manager 注册信息与 channel_status 实时缓存(缓存优先)。
    • 端到端验证(生产实例,LLM 为本地 mock 源):微信发"你好通道..." → agent 执行 channel_list → - wechat | plugin=openclaw-weixin type=text running=true connected=true accounts=[default] → 回复送达;发"注入..." → 执行 channel_send → "消息已投递到通道 wechat" → 回复送达。standalone manager 另验证 channel/start(mock-wechat startAccount 重新执行、心跳恢复)与 channel/stop(stopAccount called)。
  13. 插件删除回调 onRemove 全套(已完成并验证):RegisterOnRemoveHandler(仅卸载触发、 重载/禁用不触发,与 stop handler 互补——stop 每次停止都执行)。registry.RemovePlugin 流程: stop handlers → Stop → runOnRemoveHandlers → 移除 plugins/sdkRefs/instances → UnregisterPluginTools → 配置清理。配置清理含两层:ConfigRegistry.RemovePlugin 删除 defs 中 plugin.<name>.* 配置项定义 + DROP config_<name> 插件配置表 (含用户设置值,ListPlugins 基于 config_% 表枚举故配置区完全消失);已用临时程序验证 (before: defs=1/plugins=[timer] → after: defs=0/plugins=[])。示例盘点(SDK 仓库 15 个): calendar(events.json)、memo(memos.json)、rss(订阅数据目录)、weather(缓存目录)已加; files(filesDir 为用户配置的访问根目录,默认 /)、bili/qq(用户下载资产)、 ocr(函数内 defer RemoveAll 自清理)按语义不加;plugindev 模板 main.go.tmpl + README.tmpl 含 onRemove 演示;SDK README/README_EN 生命周期文档补"删除清理(onRemove)"小节。

  14. [2026-08-03] SDK 工具链/打包/重装 + dlclose 修复:

    • 工具链源码位置澄清:SDK 仓完整内容位于 third_party/homeagent-sdk(主仓 .gitignore 仅跟踪 sdk/meta/go.mod,"两个远程仓库各取所需";/tmp/opencode/sdk-repo 为工作克隆,远程=gitcode)。
    • 工具链支持公共 IOInjector.InjectInputSync:CORE_INJECT_INPUT_SYNC=47(C 桥 dispatchIO callString 回传回复文本);主仓 cabi loader case 47 用内部 4 参版 InjectInputSync 取 OutputEvent.Payload["content"] setResult(meta.go ID 47 + loader.go,主仓 3f252ed)。
    • plugindev 构建环境:GOMODCACHE=/root/go/pkg/mod(yaegi 缓存所在)、GOPROXY=off。
    • 工具链打包 memo:plg.json BOM 去除、name_en "Memo/Notes"→"Memo"(toSnake 不处理斜杠, name_en 带 / 会使 hmap 名含子路径报错);bundle=true 时走全平台交叉编译(本机无 darwin 工具链),打包用 build --no-bundle --target linux/amd64;产物 dist/memo_linux_amd64.hmap。
    • 重装:pluginmgr HTTP API(127.0.0.1:9876)DELETE /plugins/memo 卸载(走内核 RemovePlugin
      • onRemove)→ POST /plugins binary body 传 hmap(返回 installed+checksum);生效用 webui POST /api/v1/plugins/reload(X-API-Key,生产 admin123)。
    • 关键 bug:Linux dlopen 同路径复用旧句柄——RemovePlugin/ReloadOne 只 Stop 不 dlclose, 插件二进制更新后重载仍执行旧代码(生产 memo 装新版仍注册旧 3 工具)。修复: cabiPlugin.Close()(handle.Close)+ Registry.closeDynamic 在卸载/重载时调用(主仓 649e312)。 生产 homed-new7 验证:memo 6 工具(memo_todo_add/complete/list + memo_memo_create/list/delete) 注册正常,wechat 通道 running。
    • SDK 仓推送 b6e30f9(工具链 47 + 重建 bin 二进制 + memo plg.json + sdk/plugin.go 注释精简)。
  15. [2026-08-03] 嵌套 git 恢复 + 工作区清理 + codegraph 索引修正:

    • 嵌套 git 恢复:third_party/homeagent-sdk 原本是"单仓库双提交"(目录内嵌套 .git 推 gitcode homeagent-sdk 仓,主仓 git 跟踪 sdk/meta/go.mod 推 HomeAgent 仓),嵌套 .git 此前被误删; 已从 /tmp/opencode/sdk-repo 复制 .git 恢复(remote=homeagent-sdk.git,HEAD=b6e30f9,工作区干净), 主仓 git 不受影响。以后 SDK 改动直接在 third_party 内 git commit+push(SDK 仓推送仍用带凭据 URL https://JianFeeeee:BCkb32xBuLxWD9P4MmU8ydZ5@gitcode.com/JianFeeeee/homeagent-sdk.git), 不再经 /tmp 中转。
    • /tmp 清理:删除 /tmp/opencode/sdk-repo、hasdk-fresh、plugindev-new、plugindev_new、mock-run、 lunartest(SDK 中转/临时目录);保留 homed-new*(生产二进制备份)、mock-skills 等非 SDK 内容。
    • replace 修正:go.mod 第 17 行已是 ./third_party/homeagent-sdk(正确);package-linux.sh prepare_gomod 优先用 $PROJECT_ROOT/third_party/homeagent-sdk,仅缺失时才 clone /tmp/homeagent-sdk 兜底(主仓 108faac)。
    • codegraph 索引修正:根目录 codegraph.json(PROJECT_CONFIG_FILENAME)配 includeIgnored+include: ["third_party/homeagent-sdk"],codegraph index 后 Files 179→233, third_party 文件 7→61,tools/plugindev 与 sdk/plugin.go(InjectInputSync 等)均可查询 (此前嵌套 SDK 仓被主仓 .gitignore 挡在索引外);codegraph sync 不感知配置变更,需 index 全量重建。