21 Commits

Author SHA1 Message Date
0fe239f8cd docs(ci): 记录 cmd/gui 假告警的根因与本机补丁(手册 §6)
每轮编辑后 pi-lens 都会报 `FAIL ./cmd/gui [setup failed]`,看着像仓库有测试红了,
实为工具缺陷。本会话内重复触发 20+ 次,每次都需人工复核,故完整记录并修掉。

根因(两个缺陷叠加):
  1. pi-lens 的 runner 按**仓库根**检测(本仓有 go.mod ⇒ go),但被测文件可能
     在另一种语言的项目里;`cmd/gui/*.test.mjs` 命中通用测试命名 ⇒ 对纯 Electron
     目录生成 `go test -run . ./cmd/gui` ⇒ 必然 "no Go files" 失败。
  2. 该失败进入进程内 failedTestsByRunner,而 failed-first 策略此后**每次**编辑
     都优先重跑它(与当前编辑的文件无关);条目只在测试**通过**时移除 ⇒ 永不自愈。
     日志形态:turn_end: README.md → test go cmd/gui/sse-backoff.test.mjs (failed-first)

为什么不能靠配置关掉(都实测/读码确认):
  - `.pi-lens.json` 是项目级,只认 ignore/rules/maxProjectFiles/reviewGraph/trivy
    + 三个改动开关;`tests` 是全局级,写进去会被忽略并告警。
  - 全局 `{"tests":{"enabled":false}}` 会一起关掉**所有项目**的回合末测试反馈,
    为一个仓库的误报付全局代价 —— 不值。
  - `ignore` 也挡不住:它只作用于扫描,不参与测试目标选择。

修法:给 pi-lens 的 getTestRunTarget 加一道「该 runner 真能跑这个目标吗」的校验
(go:目标目录至少有一个 .go 文件),不能跑就拒并清掉那条不可运行的失败记录。
runTestFileAsync 只有这一个调用点,是唯一收口。

验证:node --check 过语法;/usr/bin/diff 核对为纯新增零删除;
真实路径模拟 6/6 符合预期(cmd/gui 的 .mjs → 拒;真 Go 测试文件 → 放行);
go test ./... 仍 43 包全绿。

注意:补丁在 ~/.pi/agent/npm/... 里,pi-lens 升级后会被覆盖(届时误报会回来,
不影响仓库,只是噪音)。备份 index.js.orig-*,回退方式写在本节。

本手册同时补上 §6.1 症状 / §6.2 根因 / §6.3 配置为何无效 / §6.4 修法与回退。
2026-09-29 21:24:09 +08:00
64c008501f docs: 主仓 README 补在线文档入口(介绍站 + SDK 文档站)
与 SDK 仓同一个问题:主仓 README 的「文档」章节只列了仓内 markdown
(assets/docs/...),没有任何**在线**地址 —— 而主仓 README 正是绝大多数人
的第一入口,从它走不到介绍站与 SDK 文档站。

改为两段并列:
  **在线文档**:介绍站 + SDK 文档站(含 llms.txt / llms-full.txt 的 agent 直读入口)
  **仓内文档**(随代码版本走):原有的 OVERVIEW / ARCHITECTURE / PLUGIN_DEV 等

分开标注是有意的:在线站与内核版本**不严格对齐**(站点按发布节奏更新),
而仓内文档跟代码走。写清楚归属,读者才知道该信哪个。

中英文两份同步;两处 README 的 7 个 URL 全部实测 200。
2026-09-29 20:52:32 +08:00
16b6288ea6 docs(ci): 记录 GITCODE_TOKEN 已配置及其验收方式
原 3.6 节只写「未配置时该 job 显式跳过」,读起来像是一直没配。补上实际状态:
两仓 secret 均已配置(值取自 ~/.git-credentials 的 gitcode 条目),
配置命令走 stdin(避免 token 出现在进程列表),以及三道等价验收 ——
因为 sync job 只在**新版本**发版时运行(prepare.outputs.exists == 'false'),
历史 tag 触发不了,无法用旧版本实跑,所以要用等价命令验:

  ① gh secret list 确认 secret 在
  ② /api/v5/user 确认 token 有效
  ③ 带 private-token 头读 release 确认 job 用的鉴权方式可用(两仓都测)

并记下一条待改进项:该 token 是**宽范围个人令牌**(可读 92 仓/48 私有、有写权限),
而 CI 只需这两个仓;更稳的是换一枚仅限这两仓的令牌,把 CI 泄漏的影响面收窄。
当前按用户 2026-09-29 的决定保持原状。
2026-09-29 17:42:46 +08:00
e9cac42d5d docs: 补上最后两处历史 SDK 链接的迁移(v1.1.0 tag)
README 里还剩 1 处、README_EN 里 1 处指向 gitcode.com 的 SDK v1.1.0 release 链接。
核查后确认:GitHub 的 SDK 仓**只有 tag 没有 release**(releases 数为 0),
所以不能照抄「releases/tag/...」的路径 —— 改指 tag 页 tree/v1.1.0,
实测 HTTP 200(gitcode 侧同样仍可用,两边都保留可达性)。

至此两份 README 的 gitcode 链接归零。
2026-09-29 17:30:56 +08:00
8072c2442d docs(deploy): 手册补「本机 diff 是坏的」—— 比对判据的硬前置
这条已骗过两次(openai.lua、两版 release.yml),今天又骗了第三次,且这次
后果最重:它把部署脚本的比对判据整个架空成假绿灯。

补进手册 0.1 节(放在拓扑之后、各部署章节之前,因为它是所有比对的前提):
  - 结论:一律 /usr/bin/diff 或 cmp,不要裸 diff
  - 三条可自证真伪的单行命令(含 cmp 未被污染这个事实)
  - 为什么比「偶尔报错」危险:部署判据假绿灯 ⇒ 站点永久不再更新
  - ★ 判据自检的通用写法(拿必然不同的输入验判据,不过就退出)
  - 同类信号表:diff 说无差异但 wc -c 说大小不同 / 报一致但线上明显是旧的 /
    测必然不同的样本却报相同
  - 远端 diff 健康,但嵌在 ssh "…" 里要分清本地还是远端执行
2026-09-29 17:25:51 +08:00
547d28d9fa fix(deploy): 站点比对判据被坏 diff 静默架空(假绿灯)
deploy-sdk-site.sh 的 7 处本地比对用的是**裸 `diff`**,而本机 PATH 首位是
/opt/huawei/harmonyos/ohos-sdk/linux/toolchains/diff —— 它对**任何**输入都
返回 0 且无输出。后果不是「偶尔报错」,而是:

    --check 永远打印「✓ 线上与本机源逐字节一致」
    ⇒ 永远判定「无需部署」⇒ 站点改完再也不会被更新,且完全看不出来

实测凭据(本次):site/index.html 本地 f8898cfc…、线上 cc615cfa…,
两边**确实不同**,而 --check 报「逐字节一致」;用两个必然不同的小文件
(A / B)验证,坏 diff 退出码 0、/usr/bin/diff 退出码 1。

修法(两层):
  1. 钉绝对路径 DIFF=/usr/bin/diff,7 处本地比对全部改用它;
  2. **启动自检**:拿两个必然不同的输入验证一次,若它仍报「无差异」
     就直接退出,而不是继续拿一个坏判据去做部署决定。
     (同源的教训:「探针不红先怀疑探针」——判据本身坏了,
       它给出的「没问题」毫无意义。)

远端那一处保持 `diff` 并在旁边写明理由:远端是健康的 /usr/bin/diff,
不在本机这个坏 PATH 的影响范围内。

顺带补两个选项:
  --introduce         只部署 introduce 站。改 site/ 时用 —— 原 do_deploy 会先把
                      SDK 文档站重建一遍(apidoc + mkdocs,106 文件),而 site/
                      的改动跟它毫无关系,既慢又把没必要碰的线上站卷进变更面。
  --rollback-introduce 对称的回滚入口(原先只有 sdk 站能回滚)。

验证:--check 现在能正确报出差异(附带两份 md5 对照);--introduce 走完
「先比对→打包→备份→原子替换→回验」,线上 md5 与本地一致,http 200。
2026-09-29 17:23:17 +08:00
8b8199592d chore: 排除 pi-lens 对 cmd/gui 的 Go 测试误报
cmd/gui 是纯 Electron/Node 目录:0 个 .go 文件、无 go.mod、`go list ./...`
匹配 0 个包,它的测试入口是 package.json 里的 `node *.test.mjs`。

但 pi-lens 的 test runner 在写文件时按**仓库主语言**(Go)为所在目录跑测试,
而 cmd/gui/*.test.mjs 被识别成测试文件 ⇒ 它对一个纯 Node 目录执行
`go test ./cmd/gui`,必然得到 `no Go files ... [setup failed]`,
却被报成「测试失败」。本会话内重复触发 13 次,每次都要人工复核一遍。

排除触发源(而非关闭 tests.enabled —— 那是**全局**开关,会一起削弱所有真实
项目的测试网)。.mjs 的真实运行方式是 `npm test`,CI 的 GUI job 已在跑。

上游缺陷:runner 应先确认目录内存在该语言的源文件,再决定是否执行。
2026-09-29 17:09:02 +08:00
e20c177e88 chore(site): 介绍站同步 1.4.0,并迁移 gitcode → GitHub 链接
1) 链接迁移:33 处 gitcode.com → GitHub(20 处 SDK、13 处主仓),0 残留。

2) 补齐 1.4.0 特性:「真正的 AgentOS」四张卡(隔离/调度/通信/资源)之外,
   新增第五张「并行 —— 同轮的工具,一起跑」,把声明式并发安全与
   整批降级这两条关键语义写清楚(这是当前版本最重要的架构变化,
   而站点此前一个字都没提)。

3) 插件列表与实际 SDK 示例对齐:补 ai_image(v1.3.0) / files(v1.0.0) /
   luademo(v0.1.0) 三张卡与对应徽章(23 项)。版本与描述取自各自 plg.json。
   注意 mc / homeagent-mail-bridge **不是**过期项 —— 它们是真实插件,
   只是源码不在 SDK 仓,各有自己的链接。

已用共享浏览器(CDP)在 file:// 下实测:徽章 23 个、点 ai_image 能正确
切换出卡片(可见性检查通过)、第五张卡渲染正常、页面内 gitcode 残留为 0。
2026-09-29 17:09:02 +08:00
16379f9cb2 ci(pages): 介绍站的 GitHub Pages 镜像
主站仍自托管在 NAS(192.168.2.106),国内内网直连毫秒级;GitHub Pages 提供
海外可达性与灾备。两边同源(同一个 site/ 目录),不存在谁是「真身」。

发布前的完整性检查是必要的:Pages 是静态托管的,**缺文件不会让部署失败**
(照样 200 + 404 页),所以「引用存在但文件没提交」这类问题必须在这里拦住 ——
检查 index.html 引用的每个 assets/ 资源都实际存在。

三个 action 版本已逐个查 GitHub API 确认真实存在(checkout@v7 与仓库其余
workflow 保持一致;configure-pages@v5 / upload-pages-artifact@v4 / deploy-pages@v4)。
workflow 已过 actionlint,检查逻辑已在本机原样跑通(3 个文件、165778 字节、
引用清单 assets/logo.svg)。

注:Pages 需在仓库设置里把 Source 选为「GitHub Actions」后本流水线才会真正发布。
2026-09-29 17:09:02 +08:00
2aa6fe6b57 docs: README 对齐 1.4.0 现状,并移除看板娘
四处过时事实:
  1. 「v1.3.x 线(v1.3.1–v1.3.12,最新已发布)」→ 补上 v1.3.13,并按 main 的实际内容
     新增 v1.4.x 段落(同轮工具并行、seq 序列编排、结果只统计不裁剪、结构化错误契约、
     设备命令白名单、GUI 增量渲染)。这些是 MAIN 上已合入但 README 从未提及的特性。
  2. 「内置 18 个插件」×2 → 实际 20 个(漏了 seq / kbtree 等)。
  3. 6 处 gitcode 链接 → GitHub(仓库已迁至 github.com/JianFeeeee/HomeAgent,
     SDK 为 github.com/JianFeeeee/homeagentsdk)。gitcode 仍作国内镜像保留。
  4. 移除「看板娘 / Web Mascot」整段(中英文各一处)—— 按用户要求。

三张架构图按代码重画(此前图上缺了 1.4.0 的核心机制):
  - 图一「消息处理时序」:tool 分支改为**批**语义 —— batchRunnable 三条判据
    (批内 >1、全部 ParallelSafe、无同通道重复发送)、可并发/整批降级两条路、
    以及「按声明序收尾 + 同通道保序」。
  - 图二「Stage 管道」:④⑤ 拆成并发/串行两条边,并注明并行只影响执行时序,
    post_action 与 after_toolcall 的可见顺序不变。
  - 图三「三层记忆」:补上**场面识别(声明 + 涌现)**子系统 —— 指纹权重、
    归属/唤起阈值、origin=declared|emergent、用进废退衰减。

另加一段说明:agent 自己的回复也写回记忆(context.Append(Source:"agent")),
因此它能读到自己先前的结论并主动纠正 —— 这是记忆召回的自然结果,
内核里**没有**任何名为「反思」的机制(避免把涌现行为误读成已实现的特性)。
2026-09-29 17:09:02 +08:00
8da670ca14 chore: 用 .mailmap 归并虚拟贡献者身份
GitHub 的 Contributors 列表按**提交邮箱**归并身份,而本项目历史里同一个人的提交来自
多个邮箱(本地 root、个人 QQ、gitcode noreply、GitHub noreply、dev@local 等),
于是列表里冒出 5 个并不存在的「协作者」:
  root@qq.com(135) / root@qq.com(51) / 2198972886@qq.com(33+4)
  dev@local(14) / root@minecraft-server(10) / 109188060+JianFeeeee@...noreply(19)
加上真实身份,7 条里只有 1 条是人。

.mailmap 只影响**展示层**(git shortlog / git log --use-mailmap / GitHub Contributors),
不改写任何提交对象 —— 历史 SHA 全部不变。归并后:
  git log --use-mailmap → JianFeeeee(715) + HomeAgent Agent(19)

HomeAgent Agent <agent@homeagent.local> **刻意不归并**:agent 的自动提交保留独立身份,
便于区分人类提交与自动化提交。
2026-09-29 17:09:02 +08:00
6d6beed72a chore(watch): 下线站点漂移巡检脚本
用户指示「那个脚本直接去了吧」:site-drift-watch.sh 每半小时巡检一次两个文档站
的产物漂移,但实测 5 次告警**全是源码漂移误报**(判据只比时间戳,而产物生成后的
提交都不影响文档站),线上漂移与探活失败从未报过 —— 信噪比太差,去掉损失很小。

调用方核查:只有 root crontab 的 `7,37 * * * *` 一条,systemd 无 unit,
脚本/Makefile/.github 里零引用。crontab 已按行备份并只删该两行(其余三条任务保留),
脚本本体备份在 /root/backups/ 可随时还原。

本提交只删仓库内的脚本文件;crontab 与运行环境侧的调整已在部署时完成。
2026-09-29 17:09:02 +08:00
f339850ffb docs(ci): CI/CD 流水线手册 —— 用法、机制与 6 个踩过的坑
仓库迁 GitHub 后新增两条流水线(ci.yml / release.yml),但用法与机制
此前只存在于 workflow 的注释和提交信息里。发版是高频操作,写成手册。

内容:
- §1 CI 六个 job 与「明确不进 CI」的清单(真机/密钥/内网依赖)
- §2 发版标准流程、幂等闸门(tag 存在即跳过)、
     发版门(go build 硬门 + go test 可用 [skip-release-tests] 跳过)
- §2.5 构建资产(ci-assets-v1)的托管与升级方式
- §3 六个实测踩过的坑:
      3.1 workflow 文件必须存在于目标分支(否则推 release/** 不触发)
      3.2 runner 无 electron 缓存 ⇒ 必须 npm ci(且不能用 --production)
      3.3 管道里的 grep -q 因 SIGPIPE 误杀检测(800M 包必炸)
      3.4 gh 在非 git 目录要显式 --repo
      3.5 gitcode 上传的 ASSET_DIR + 裸名语义
      3.6 手工补发 gitcode 附件的流程
- §4 SDK 仓的差异(独立 module 测试要跑两处、CGO 不需要)
- §5 关于失败邮件的说明(可能是验证步骤自身 bug 的假警报)

markdownlint 全绿(两个产物清单代码块补了 text 语言标注)。
2026-09-29 15:03:05 +08:00
192e63cf52 ci(release): gh release download 需显式 --repo(非 git 目录无法推断)
## 症状(第三次试发布)

Build 全绿、tag 已建、release 已建、2.40GB 附件全部上传成功 ——
唯独最后一道「回读校验」失败,整个 workflow 因此标记为 failure:

    failed to run git: fatal: not a git repository (or any of the
    parent directories): .git
    ##[error]Process completed with exit code 1.

## 根因

回读校验为了"下一份干净副本"先 `cd /tmp/back`,那里不是 git 仓库。
而 `gh release download` 默认从**当前目录的 git 上下文**推断仓库与
host(GITHUB_REPOSITORY / GH_HOST 之类环境变量不足以让它跳过推断),
于是报 "not a git repository"。

## 修法

    gh release download "$TAG" --repo "$GITHUB_REPOSITORY"

两个仓的 workflow 都有同一处(主仓 + SDK),一并修。

## 顺带

`third_party/homeagent-sdk/.github/` 加入 .gitignore —— 与 skills/ 同类:
SDK 仓自己的 workflow 由 SDK 仓跟踪管理(那边已跟踪),本仓不参与
构建,不需要在这边重复一份。未加规则时它会出现在本仓的未跟踪列表里。
2026-09-29 14:39:08 +08:00
91fc5093d1 ci(release): 验证产物改用 >/dev/null 而非 grep -q —— SIGPIPE 误杀检测
## 问题(第二次试发布实测)

打包成功后,「验证产物」步骤失败:

    tar: stdout: write error
    dpkg-deb: error: tar subprocess returned error exit status 2

而三个 deb 的元数据其实已全部正确打印(Package/Version/Architecture)。

## 根因

`dpkg-deb -c <800M 的 full 包> | grep -q <模型文件>`:

grep -q 匹配到目标行后**立即退出**、关闭管道读端 ⇒ dpkg-deb 内部的
tar 继续写 stdout 时收到 EPIPE ⇒ pipefail 判整条 pipeline 失败。

⇒ 检测项本身是好的(模型确实在包里),却被检测手段误杀。

本地用 CI 上同一个 800M full 包复现:
    grep -q 版    → dpkg-deb: error: tar subprocess was killed by
                    signal (Broken pipe)
    >/dev/null 版 → 通过

client(80M)没炸、full(800M)炸 —— 包越大越容易触发(内容越多,
grep -q 提前退出的窗口越大)。这正是它没在本地小规模测试里暴露的原因。

## 改法

检测存在性时用 `grep <pattern> >/dev/null`(读完整个输入再退出),
不用 `grep -q`。顺带补了 server 包的模型在位检测(原来只测了 full)。
2026-09-29 14:19:29 +08:00
5afe8be432 ci(release): 修 gitcode 同步的路径拼法(原写法必然找不到文件)
upload_assets.py 的路径语义是 `os.path.join(ASSET_DIR, name)`,
而原写法先 `cd dist` 再传 `./*.deb` ⇒ 拼成 `dist/dist/...`,
必然 "资产目录不存在" 或逐个 skip。token 一配上就会炸,属隐患。

改为:cd 进资产目录 + `ASSET_DIR=.` + **不传文件名**(让它扫描当前目录,
.deb/.tar.gz/SHA256SUMS 都在它的产物白名单里)。

SDK 侧另有一处同类问题,但那里**必须显式列名** —— hmapdev 的产物多数
没有扩展名(只有 windows 那个是 .exe),自动扫描会静默地一个都不传。
已同步修在 SDK 仓的 workflow 里。
2026-09-29 14:09:19 +08:00
87f8fc1485 ci(release): [skip-release-tests] 标记改查发版提交,不再查 HEAD
原实现用 `git log -1 --pretty=%B`(即 HEAD)找标记。但发版提交之后
往往还会跟几个提交(同步 workflow、改文档、修脚本),HEAD 一移动,
标记就被顶掉 —— 跳过机制**静默失效**,流水线又回去撞旧线的红测试。

改为查**改动 internal/meta/meta.go 的那个提交**(语义上正是「发版提交」):
  REL_COMMIT=$(git log -1 --format=%H -- internal/meta/meta.go)

顺带把 grep -qF 换成 case 匹配,避免多层引号嵌套。

自测(本地 worktree):
  发版提交 472d908 → 命中标记 ✓
  HEAD      fa5b1b4 → 不命中(旧逻辑会在此静默失效)

actionlint 全绿。
2026-09-29 14:03:25 +08:00
1acc933dc8 ci(release): 打包前装 Electron,否则 GUI 被静默跳过
## 问题

打包脚本从两处找 Electron 运行时:
  1) ~/.cache/electron 里的 electron-v<ver>-linux-<arch>.zip
  2) cmd/gui/node_modules/electron/dist(目标架构 == host 时)

全新 GitHub runner **两处都没有** —— 脚本在都没有时只能跳过 GUI,
于是 client/full 包会**静默地不含界面**(正是脚本作者担心的「假包」)。
实测:本地移走缓存后 GUI 被跳过,包仍能产出。

## 改法

打包前在 cmd/gui 执行 `npm ci`,让 electron 落到 node_modules。
runner 是 amd64 == 目标架构,脚本便走第 2 条路径。

**用 npm ci 而不是 `npm install electron@<range>`**:后者是非确定性的
(range 会随上游漂移,也锁不住传递依赖),zizmor 也把它标为
adhoc-packages 风险。package-lock.json(lockfileVersion 3)已锁定
electron,ci 严格按 lock 安装 ⇒ 同一 commit 永远得到同一套依赖。

不能用 `npm install --production`:那会跳过 devDependencies,
而 electron 正是 devDependency(这正是脚本自己那条命令找不到它的原因)。

## 验证

- 无声 cache + 有 node_modules/electron 时,脚本走 host-arch 回退并
  成功构建 GUI(263M / x86-64)—— 已本地实测
- 两者都无时改为打印明确原因并跳过(配合上一个 commit 的 find 修复)
- npm ci --dry-run 通过;actionlint 全绿
2026-09-29 14:02:18 +08:00
ac087aedb7 fix(packaging): find 在 set -euo pipefail 下致命退出 —— 任何无 electron 缓存的机器都打不出包
## 症状

CI 发布在「打包」步骤失败,输出停在:

    >>> Building GUI directory for linux/amd64...
      npm install...
      electron 版本取自 package.json 依赖声明: 33.0.0(非精确)
    >>> Restoring original go.mod...
    ##[error]Process completed with exit code 1.

没有错误信息,看不出真因。build_go 之前已全部成功(homed 80M 带 onnxruntime)。

## 根因(本地精确复现 + bash -x 追踪)

    + zip=$(find "$HOME/.cache/electron" -name "electron-v33.0.0-linux-x64.zip" | head -1)
    + zip=
    + restore_all          ← 直接退出

脚本是 `set -euo pipefail`。`find` 对**不存在的目录**返回退出码 1,
pipefail 让 pipeline 返回该 1,而 `set -e` 对**赋值语句里的命令替换**同样生效
⇒ 整个脚本当场退出。

实测退出码对照:
    x=$(find /不存在 | head -1)               → 1(脚本死)
    x=$(find /不存在 | head -1 || true)       → 0(存活)
    x=$(find /不存在)                          → 1(无管道也死)

⇒ 任何**没有 ~/.cache/electron 的机器**(全新克隆、CI runner、其他开发机)
都会撞上。本机一直「能打包」只是碰巧有那份 646M 缓存。

## 修法

给 4 处 find 加 `|| true` 兜底(214/217 electron 缓存、623/625 rpm_deb)。
另修 206 行 `[ -n "$ever" ] && echo ...`:`ever` 为空时该列表返回 1,
在 set -e 下同样会杀死脚本 —— 改为 if 形式。

★ 注意 608 行(fpm 查找)**早就有** `|| true`,说明这个模式被意识到过,
只是漏了这几处。属同一类缺陷的补全,不是新引入的写法。

## 验证

- 复现:移走 ~/.cache/electron 后 `package-linux.sh amd64 build` → 修复前 exit=1
  (输出与 CI 逐行一致),修复后 exit=0 且打印明确原因:
  `WARNING: electron binary not found at ... GUI will be skipped.`
- 有 electron 时仍正常构建:GUI 263M / x86-64(走 host-arch 回退路径)
- bash -n 与 shellcheck -S error 均通过
2026-09-29 13:59:47 +08:00
9beb3b558e ci(release): go test 门可显式跳过(默认仍严格)
## 问题(试发布实测暴露)

把 CI/Release 带到 release/v1.3.x 后,CI 三个 job 全红,但**每个失败都是
该分支自身的旧状态,与改动无关**:

| job | 失败原因 | main 上 |
|---|---|---|
| GUI (node) | `npm error Missing script: "test"`(1.3.x 尚无该脚本)| 正常 |
| Go test | TestRealPlugin_DeepSearchKeepsSharedBackendOnStop | **通过** |
| C gates | exit 2(1.3.x 无 csrc 基础设施)| 正常 |

⇒ 给已存在的发布线补新流水线 = 用今天的门去量旧代码。硬门会让该历史
维护线**完全无法发版**,正是用户要的「推 rel 分支就出产物」被挡死。

## 改法

拆开两道门,语义不同:

- `go build ./...` —— **硬门**,不可跳过。产物不可能建立在编译失败的代码上。
- `go test ./...` —— 默认跑,但可跳过。两个来源:
  1. workflow_dispatch 的 `skip_tests` 输入
  2. **发版 commit 里写 `[skip-release-tests]`**

第二个来源是关键:决定落在**定义该次发版的那个 commit** 里,`git log` 可审计,
而不是一个随手勾的开关。跳过时输出 `::warning` 注释,让后果在 run 页可见。

## 未决(留给用户)

`release/v1.3.x` 的 deepsearch 测试失败属该线既存状态(main 已修)。是否把它
cherry-pick 回 1.3.x 属产品决策(1.3.x 是历史维护线,main 已是 1.4.0),
故本次不擅自拉回绿,只提供显式跳过通道。

actionlint 全绿。
2026-09-29 13:40:25 +08:00
8cdcbf70fb ci: 发布流水线 —— release/** 推送即发版(tag/打包/发布/镜像全自动化)
## 设计

版本号唯一事实源是 internal/meta/meta.go 的 Version(仓库纪律),
所以发版动作 = 在 release/vX.Y.x 上把 meta.Version 改成目标版本后推送:

  prepare      读版本号;tag 已存在则整轮跳过(幂等闸门,改文档不会重发)
  build-linux  go build + go test 过门 → 下载资产 → 打包 3 deb + 1 tar.gz
               → 平铺 → 验证(deb 元数据/模型在位/校验和自验)→ artifact
  publish      打 tag → gh release create 传附件 → 回读下载验证校验和
  sync-gitcode 有 GITCODE_TOKEN 时同步 tag+附件到 gitcode(无则跳过不阻断)

## 关键事实(全部本地实测过才写进 workflow)

1. **编译不需要 ONNX Runtime**:onnxruntime_go 是 dlopen 方式(运行期才
   加载 .so),本地在清空 ORT 相关环境变量的条件下带 -tags=onnxruntime
   编译通过(83M)。CI 只需在**打包**时有 ORT(要打进 deb)。
2. **构建资产托管在 release ci-assets-v1**(已上传):
   chinese-clip-vit-b16-onnx.tar 719MB + onnxruntime-linux-amd64-1.28.0.tar
   24MB + SHA256SUMS。模型内容不随版本变 ⇒ 一次上传反复复用,CI 打包前
   下载并 sha256sum -c 校验。上传实测 3.2MB/s,构建期下载同源更快。
3. **打包链路在干净 worktree 全程实跑通过**(release/v1.3.x + VERSION=1.3.13):
   full 800M / server 726M / client 80M / tar.gz 841M,SHA256SUMS 平铺自验
   4/4 OK,full 包内确认含 TextEncoder/VisionEncoder.onnx 与 libonnxruntime.so。
4. **SHA256SUMS 的坑**:脚本把校验和写成平铺名(./xxx.deb),而产物在
   deb/ tar/ 子目录 ⇒ 直接 -c 会全 FAILED。workflow 里显式平铺后再验。
   (呼应 git-branching.md §七「校验和必须覆盖全部附件、只传一次」。)
5. ORT 资产补齐了缺失的 LICENSE + ThirdPartyNotices.txt(取自
   microsoft/onnxruntime v1.28.0 tag,与本地 .so 的内嵌版本号一致)——
   打包脚本的 stage_multimodal_assets 对这两文件非空校验,缺失即失败。
6. actionlint 全绿(修掉了 shellcheck SC2012:ls 改 stat 循环)。

## 已知边界

- arm64 发布产物暂缺(package-linux.sh 支持,但 CI 未配 QEMU 交叉;待需要时加 matrix)。
- Windows 安装器未纳入(需 electron-builder win 打包,单独验证后接入)。
- sync-gitcode 依赖 secret GITCODE_TOKEN(待用户配置;未配置时该 job 显式跳过)。
2026-09-29 13:07:36 +08:00
13 changed files with 1173 additions and 323 deletions

85
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,85 @@
name: Pages Mirror
# 介绍站(site/)的 GitHub Pages 镜像。
#
# 为什么是「镜像」而不是主站:主站自托管在 NAS(192.168.2.106),
# 国内访问是内网直连(毫秒级);GitHub Pages 负责海外可达性与灾备。
# 两边内容同源(同一个 site/ 目录),不存在谁是「真身」的问题。
#
# 手动触发也留着:改完 site/ 想立刻发布、或主站回滚后要重新对齐时用。
on:
push:
branches: [main]
paths:
- 'site/**'
- '.github/workflows/pages.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# 同一时刻只跑一个 Pages 部署;排队中的旧任务直接取消
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
name: Build site artifact
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
# 静态站:没有构建步骤,site/ 就是要发布的内容。
# 这里只做**完整性检查**——少一个文件会让线上 404,而 404 不会让
# 部署失败(Pages 照样返回 200 + 404 页),所以必须在发布前拦住。
- name: Verify site completeness
run: |
set -euo pipefail
test -f site/index.html || { echo "缺少 site/index.html"; exit 1; }
test -f site/assets/logo.svg || {
echo "缺少 site/assets/logo.svg"; exit 1
}
# index.html 引用的本地资源必须都在(防「改了引用忘了加文件」)
missing=0
grep -oE '(src|href)="(assets|\./assets)/[^"]+"' site/index.html \
| sed -E 's/.*="(\.\/)?([^"]*)"/\2/' > /tmp/refs.txt
while read -r ref; do
[ -z "$ref" ] && continue
if [ ! -e "site/$ref" ]; then
echo "引用缺失: site/$ref"
missing=1
fi
done < /tmp/refs.txt
[ "$missing" -eq 0 ] || exit 1
echo "site/ 文件数: $(find site -type f | wc -l)"
echo "index.html 字节: $(wc -c < site/index.html)"
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: site
# Pages 产物必须保留;不设 retention 会被仓库默认值(30 天)回收——
# 与部署无关,但重跑 build 时会少掉旧产物对比。
retention-days: 7
deploy:
name: Deploy to Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

398
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,398 @@
# 发布流水线:release/** 分支推送即发版。
#
# 设计依据 docs/git-branching.md §七(发版产物清单)与 git-release-discipline
# skill。核心事实:**推 tag ≠ 完成发版** —— 完整发版是四件事:
# bump meta.Version → 打 tag → 打包产物 → 建 release 条目并上传附件。
# (v1.3.1–v1.3.6 曾只推了 tag,产物与 release 条目全缺,事后补做。)
#
# 版本号来源:internal/meta/meta.go 的 Version(唯一事实源)。
# 所以发版动作 = 在 release/vX.Y.x 上把 meta.Version 改成目标版本后推送。
# 版本未变的推送(如改文档)会因 tag 已存在而**整轮跳过**,不会重复发版。
#
# 发版前的 go test 门可以显式跳过(见下面 skip_tests 的说明)。
name: Release
on:
push:
branches: ['release/**']
workflow_dispatch:
inputs:
skip_tests:
description: '跳过发版前的 go test 门(仅用于已知红的历史维护线)'
type: boolean
default: false
# 发布必须能写仓库(打 tag、建 release、传附件)。
permissions:
contents: write
# 发布不允许并发/取消:半途中断会留下 tag 存在但附件不全的状态。
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
# gojieba 需要 cgo;onnxruntime 版本经 dlopen 加载,编译期无需装 ORT。
CGO_ENABLED: 1
GOFLAGS: -buildvcs=false
# CI 用的大资产(模型/运行库)存于这个 release。
ASSETS_TAG: ci-assets-v1
jobs:
# ── 读版本号并判断是否需要发版 ──
prepare:
name: Prepare
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
prerelease: ${{ steps.ver.outputs.prerelease }}
exists: ${{ steps.ver.outputs.exists }}
skip_tests: ${{ steps.ver.outputs.skip_tests }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
# 发版前的 go test 门为什么可以跳过:
#
# 新旧发布线的测试健康状况不同。实测 release/v1.3.x(历史维护线)上
# internal/plugins 的 TestRealPlugin_DeepSearchKeepsSharedBackendOnStop
# 失败、GUI 尚无 npm test 脚本、csrc 基础设施不存在 —— 而这三项在 main
# 上都正常。给旧线补新流水线等于用今天的门去量旧代码,硬门会让该线
# **完全无法发版**。
#
# 故:默认严格(测试必跑);发版人若确知该线测试是既存红的,可在
# 发版 commit 里写 [skip-release-tests] 显式跳过 —— 决定因此记录在
# **定义该次发版的那个 commit** 里,git 历史可审计。
# go build 仍是硬门(产物不可能建立在编译失败的代码上)。
- id: ver
name: 读取 meta.Version 并检查 tag
run: |
set -euo pipefail
V=$(sed -n 's/^[[:space:]]*Version = "\(.*\)"/\1/p' \
internal/meta/meta.go | head -1)
if [ -z "$V" ]; then
echo "ERROR: 无法从 internal/meta/meta.go 读出 Version"
exit 1
fi
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "tag=v$V" >> "$GITHUB_OUTPUT"
# SemVer 预发布(1.3.13-beta.1)⇒ release 标记为预发布
case "$V" in
*-*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;;
*) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;;
esac
# 幂等闸门:tag 已存在说明该版本发过了,整轮跳过。
if git ls-remote --exit-code --tags origin "refs/tags/v$V" \
>/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo " tag v$V 已存在 —— 跳过发版"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo " 将为 v$V 发版"
fi
# 是否跳过发版前的 go test 门(默认不跳)。
# 两个来源:手动触发的输入,或发版 commit 里的显式标记。
# 后者使决定落在定义该次发版的 commit 上,可以从 git 历史审计。
#
# 标记查在**改动 meta.Version 的那个提交**上,而不是 HEAD:
# 发版提交之后往往还会跟几个提交(如同步 workflow、改文档),
# 若只看 HEAD,标记就会被后续提交顶掉,静默失效。
SKIP="${{ inputs.skip_tests }}"
MARKER=0
REL_COMMIT=$(git log -1 --format=%H -- internal/meta/meta.go)
REL_MSG=$(git log -1 --pretty=%B "$REL_COMMIT")
case "$REL_MSG" in
*'[skip-release-tests]'*) MARKER=1 ;;
*) MARKER=0 ;;
esac
echo " 发版提交: ${REL_COMMIT:0:12}"
if [ "$SKIP" = "true" ] || [ "$MARKER" = "1" ]; then
echo "skip_tests=true" >> "$GITHUB_OUTPUT"
echo ""
echo " ⚠️ **已请求跳过发版前的 go test 门**"
echo " 来源:${SKIP} = true / commit 标记 = $MARKER"
echo " 后果:产物可能建立在单元测试失败的代码上。"
echo " 理由应当记录在发版 commit 的正文里。"
else
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
echo " 发版前会跑 go test 门(可用 [skip-release-tests] 标记跳过)"
fi
# ── 构建 Linux 产物(amd64)──
#
# 三个 deb + 一个 tar.gz,总约 2.4GB(server/full/tar 含 719MB 模型)。
# 编译不需要 ONNX Runtime —— onnxruntime_go 是 dlopen 方式,运行期才加载
# libonnxruntime.so;但**打包**需要它(要打进 deb),故从 ASSETS_TAG 下载。
build-linux:
name: Build linux/amd64
needs: prepare
if: needs.prepare.outputs.exists == 'false'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: 确认 cgo 工具链
run: |
gcc --version | head -1
g++ --version | head -1
# 发版前的门。
#
# go build 是**硬门**:产物不可能建立在编译失败的代码上。
# go test 默认也跑,但可在发版 commit 里写 [skip-release-tests] 跳过
# —— 历史维护线的既有红测试不应阻断该线的一切发版(详见 prepare job)。
- name: go build(硬门)
run: |
set -euo pipefail
go build ./...
- name: go test(发版前验证)
if: needs.prepare.outputs.skip_tests != 'true'
run: go test ./... -count=1 -timeout 20m
- name: go test 被跳过(显式声明的后果)
if: needs.prepare.outputs.skip_tests == 'true'
run: |
echo "::warning title=go test 门已跳过::本次发版未跑 go test,产物可能建立在单元测试失败的代码上。"
# GUI 依赖 Electron 运行时。打包脚本从两处找它:
# 1) ~/.cache/electron 里的 electron-v<ver>-linux-<arch>.zip
# 2) cmd/gui/node_modules/electron/dist(同架构时)
# 全新 runner 两处都没有 —— 而脚本在都没有时**只能跳过 GUI**,
# 于是 client/full 包会静默地不含界面(这正是脚本作者担心的“假包”)。
# 所以这里显式装一份:npm 会解析出 ^33.0.0 的实际版本并落到
# node_modules,脚本便走第 2 条路径(runner 是 amd64 == 目标架构)。
#
# ⚠️ 不能用 `npm install --production`(那会跳过 devDependencies,
# 而 electron 正是 devDependency)。
- uses: actions/setup-node@v7
with:
node-version: '22'
- name: 安装 Electron(GUI 打包需要)
working-directory: cmd/gui
run: |
set -euo pipefail
# 用 npm ci 而非 `npm install electron@<range>`:后者是非确定性的
# (range 会随上游发布漂到新版本),且锁不住传递依赖。
# package-lock.json 里已锁定 electron(lockfileVersion 3),
# ci 严格按 lock 安装,同一个 commit 永远得到同一套依赖。
npm ci --no-audit --no-fund
test -f node_modules/electron/dist/electron
echo " 已就绪:$(node_modules/electron/dist/electron --version)"
- name: 下载构建资产(模型 + ONNX Runtime)
run: |
set -euo pipefail
BASE="https://github.com/${GITHUB_REPOSITORY}/releases/download/${ASSETS_TAG}"
mkdir -p /tmp/assets/model /tmp/assets/ort
for f in chinese-clip-vit-b16-onnx.tar \
onnxruntime-linux-amd64-1.28.0.tar SHA256SUMS; do
echo " 下载 $f"
curl -sSL --retry 3 -o "/tmp/assets/$f" "$BASE/$f"
done
# 校验(资产是构建输入,损坏会打出坏包)
(cd /tmp/assets && sha256sum -c SHA256SUMS)
tar -xf /tmp/assets/chinese-clip-vit-b16-onnx.tar \
-C /tmp/assets/model
ORT_TAR=/tmp/assets/onnxruntime-linux-amd64-1.28.0.tar
tar -xf "$ORT_TAR" -C /tmp/assets/ort
echo " 模型文件:"
ls /tmp/assets/model/chinese-clip-vit-b16-onnx
echo " ORT 文件:"
ls /tmp/assets/ort
- name: 打包(tar.gz + full/server/client deb)
env:
VERSION: ${{ needs.prepare.outputs.version }}
CHINESECLIP_BUNDLE_DIR: /tmp/assets/model/chinese-clip-vit-b16-onnx
ONNXRUNTIME_ASSET_DIR: /tmp/assets/ort
run: |
set -euo pipefail
bash deploy/packaging/package-linux.sh amd64 all
- name: 平铺产物(附件必须同目录,SHA256SUMS 用平铺名)
run: |
set -euo pipefail
mkdir -p /tmp/out
cp dist/linux/deb/*.deb /tmp/out/
cp dist/linux/tar/*.tar.gz /tmp/out/
cp dist/linux/SHA256SUMS /tmp/out/
echo " 产物:"
for f in /tmp/out/*; do
printf " %8.1fMB %s\n" \
"$(stat -c %s "$f" | awk '{print $1/1048576}')" "$(basename "$f")"
done
- name: 验证产物(deb 元数据 + 校验和自验)
run: |
set -euo pipefail
cd /tmp/out
for f in *.deb; do
echo " $f"
dpkg-deb -f "$f" Package Version Architecture | sed 's/^/ /'
done
# full/server 必须真的带模型,否则是“默认启用但装完不能用”的假包。
#
# ★ 不能用 grep -q:它匹配到就退出,关闭管道读端,dpkg-deb 内部
# 的 tar 写 stdout 时收到 EPIPE(“stdout: write error”),
# 在 pipefail 下整条 pipeline 变成失败 —— 检测项本身是好的,
# 却被检测手段误杀(首次试发布就死在这里)。改用 >/dev/null,
# grep 会读完整个输入再退出,不产生 SIGPIPE。
dpkg-deb -c homeagent-full_*_amd64.deb \
| grep "chinese-clip-vit-b16-onnx/TextEncoder.onnx" >/dev/null
echo " ✓ full 包含模型"
dpkg-deb -c homeagent-full_*_amd64.deb \
| grep "libonnxruntime.so" >/dev/null
echo " ✓ full 包含 ONNX Runtime"
dpkg-deb -c homeagent-server_*_amd64.deb \
| grep "chinese-clip-vit-b16-onnx/TextEncoder.onnx" >/dev/null
echo " ✓ server 包含模型"
sha256sum -c SHA256SUMS
- uses: actions/upload-artifact@v7
with:
name: linux-amd64
path: /tmp/out/*
retention-days: 7
if-no-files-found: error
# ── 建 tag、建 release、上传附件 ──
publish:
name: Publish
needs: [prepare, build-linux]
if: needs.prepare.outputs.exists == 'false'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/download-artifact@v8
with:
name: linux-amd64
path: dist
- name: 打 tag(打在触发本次发版的 commit 上)
env:
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "$TAG"
git push origin "$TAG"
- name: 建 release 并上传附件
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
PRE: ${{ needs.prepare.outputs.prerelease }}
run: |
set -euo pipefail
cd dist
FLAGS=()
[ "$PRE" = "true" ] && FLAGS+=(--prerelease)
gh release create "$TAG" \
--title "$TAG" \
--notes "HomeAgent $VERSION
产物清单与校验见 SHA256SUMS。
- \`homeagent_${VERSION}_linux_amd64.tar.gz\` — 内核 + CLI + GUI 打包
- \`homeagent-client_${VERSION}_amd64.deb\` — 客户端
- \`homeagent-server_${VERSION}_amd64.deb\` — 服务端(含向量模型)
- \`homeagent-full_${VERSION}_amd64.deb\` — 全量" \
"${FLAGS[@]}" \
./*.deb ./*.tar.gz ./SHA256SUMS
echo "=== release 内容 ==="
gh release view "$TAG" --json assets \
--jq '.assets[] | " \(.name) \(.size) 字节"'
- name: 回读校验(下载回来验证附件可读且校验和成立)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
mkdir -p /tmp/back
cd /tmp/back
# ★ 必须显式 --repo:gh 默认从**当前目录的 git 上下文**推断仓库,
# 而 /tmp/back 不是 git 仓库 ⇒ 报
# "failed to run git: fatal: not a git repository"。
# 首次试发布就死在这里 —— 产物其实全部上传成功(tag 与
# release 已建、2.40GB 附件齐备),只是这道回读校验自己失败了。
gh release download "$TAG" --repo "$GITHUB_REPOSITORY"
for f in *; do
printf " %8.1fMB %s\n" \
"$(stat -c %s "$f" | awk '{print $1/1048576}')" "$f"
done
sha256sum -c SHA256SUMS
echo " ✓ 回读校验通过"
# ── 同步到 gitcode(国内镜像)──
#
# 需要仓库 secret GITCODE_TOKEN;未配置则跳过(不阻断 GitHub 侧发布)。
# gitcode 的 release 附件是"同名只写一次",故只在此处上传一次。
sync-gitcode:
name: Sync to gitcode
needs: [prepare, publish]
if: needs.prepare.outputs.exists == 'false'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- id: tok
name: 检查 gitcode 凭据
run: |
if [ -n "${{ secrets.GITCODE_TOKEN }}" ]; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo " 未配置 GITCODE_TOKEN —— 跳过 gitcode 同步"
fi
- uses: actions/download-artifact@v8
if: steps.tok.outputs.ok == 'true'
with:
name: linux-amd64
path: dist
- name: 推 tag 与附件到 gitcode
if: steps.tok.outputs.ok == 'true'
env:
GC_TOKEN: ${{ secrets.GITCODE_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
# 1) 推 tag(附件上传前 release 条目必须先存在)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "$TAG" 2>/dev/null || true
GC_URL="https://JianFeeeee:${GC_TOKEN}@gitcode.com"
git push "${GC_URL}/JianFeeeee/HomeAgent.git" "$TAG"
# 2) 建 release 条目
curl -sS --max-time 60 -X POST \
-H "private-token: ${GC_TOKEN}" \
-H "Content-Type: application/json" \
"https://gitcode.com/api/v5/repos/JianFeeeee/HomeAgent/releases" \
-d "{\"tag_name\":\"$TAG\",\"body\":\"同步自 GitHub\"}" \
-o /tmp/.gcrel -w " 建 release → %{http_code}\n"
# 3) 上传附件(用仓库既有脚本,它处理 OBS 预签名两步流程)
cd dist
# 脚本的路径语义是 os.path.join(ASSET_DIR, name)。
# 不传文件名时它扫描 ASSET_DIR 并按后缀识别发布产物 —— 这里正合适
# (.deb/.tar.gz/SHA256SUMS 都在白名单里)。
# 不要传 "./x" 或 "dist/x":那会拼成 dist/dist/x。
ASSET_DIR=. GITCODE_REPO=JianFeeeee/HomeAgent \
python3 ../deploy/scripts/upload_assets.py "$TAG" "$GC_TOKEN"

3
.gitignore vendored
View File

@ -36,6 +36,9 @@ third_party/homeagent-sdk/scripts/
# skills/ 是 SDK 仓的 skill 源(hmapdev skill install 的来源), # skills/ 是 SDK 仓的 skill 源(hmapdev skill install 的来源),
# 与 tools/、docs/ 同理属 SDK 仓自治范围,不进本仓。 # 与 tools/、docs/ 同理属 SDK 仓自治范围,不进本仓。
third_party/homeagent-sdk/skills/ third_party/homeagent-sdk/skills/
# .github/ 同理:SDK 仓的 workflow 由 SDK 仓自己管(它在那边被跟踪),
# 本仓不参与构建,不需要在这边重复一份。
third_party/homeagent-sdk/.github/
third_party/homeagent-sdk/.gitignore third_party/homeagent-sdk/.gitignore
third_party/homeagent-sdk/README* third_party/homeagent-sdk/README*
third_party/homeagent-sdk/example/ third_party/homeagent-sdk/example/

24
.mailmap Normal file
View File

@ -0,0 +1,24 @@
# .mailmap —— 提交身份归并
#
# 为什么需要:GitHub 的 Contributors 列表按 **提交邮箱** 归并身份,而本项目历史里
# 同一个人的提交来自多个邮箱(本地 root、个人 QQ、gitcode noreply、GitHub noreply),
# 于是列表里出现若干「虚拟贡献者」,看起来像有多个协作者,实际只有一个人。
#
# 本文件只影响 **展示层**(git shortlog / git log --use-mailmap / GitHub Contributors),
# 不改写任何提交对象,历史 SHA 全部保持不变。
#
# 格式:<归并到的姓名> <归并到的邮箱> <历史姓名> <历史邮箱>
#
# 注意:HomeAgent Agent <agent@homeagent.local> **不归并** —— agent 的自动提交
# 保留独立身份,便于区分人类提交与自动化提交。
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> <root@qq.com>
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> <2198972886@qq.com>
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> <jianfeeeee@homeagent.local>
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> <dev@local>
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> <root@minecraft-server>
JianFeeeee <JianFeeeee@users.noreply.gitcode.com> <109188060+JianFeeeee@users.noreply.github.com>
# 说明:上表用同一主身份覆盖所有历史邮箱;`git shortlog -sne --use-mailmap` 应只剩
# JianFeeeee 与 HomeAgent Agent 两条。

14
.pi-lens.json Normal file
View File

@ -0,0 +1,14 @@
{
"_comment": "pi-lens 仓库级配置。cmd/gui 的 test 排除原因见 docs/zh/ci-cd-runbook.md「诊断噪音」一节。",
"_why_cmd_gui_tests_excluded": "cmd/gui 是纯 Electron/Node 目录(0 个 .go 文件、无 go.mod)。pi-lens 的 test runner 在写文件时按仓库主语言(Go)为目录跑测试,而 cmd/gui/*.test.mjs 被识别成测试文件 ⇒ 它对一个纯 Node 目录执行 `go test ./cmd/gui`,必然得到 `no Go files ... [setup failed]`,却被报成「测试失败」。这些 .mjs 的真实运行方式是 `npm test`(CI 的 GUI job 已在跑),pi-lens 无法正确执行它们,故排除触发源。上游缺陷:runner 应先确认目录内有该语言的源文件。",
"ignore": [
"cmd/gui/node_modules/**",
"cmd/gui/**/*.test.mjs",
".npm-cache/**",
"build/**",
"dist/**",
"third_party/homeagent-sdk/site_build/**",
"third_party/homeagent-sdk/build/**",
"third_party/homeagent-sdk/.npm-cache/**"
]
}

109
README.md
View File

@ -26,6 +26,18 @@ homed(内核零 IO) ← PluginSDK → 插件(所有 IO 能力)
- **Document 层**:临时记忆,冷数据自动下沉,也支持用户主动提交 - **Document 层**:临时记忆,冷数据自动下沉,也支持用户主动提交
- **Graph 层**:SQLite 图数据库,持久化实体关系和语义记忆,支持蒸馏管道从原始对话中提取三元组 - **Graph 层**:SQLite 图数据库,持久化实体关系和语义记忆,支持蒸馏管道从原始对话中提取三元组
**场面识别:声明 + 涌现** — 记忆不按「会话」切,而按**可观察的场所指纹**切:
通道(`chan:*`)与对话对象最强(权重 1.0),工具 0.8、话题 0.4、时段最弱 0.2。
两条路同时走——**声明**(注入点/工具声明「这是哪个场面」)与**涌现**
(指纹同类重复 ≥2 次就自己长出场景,`origin=emergent`,无需标注、无需模型配合)。
相似度 ≥0.5 判归属、≥0.35 判唤起(**唤起比归属宽松**:想不起来是损失,多想起一条只是多几行上下文)。
场景每次重现强度 +1,久不重现按半衰期衰减。
> 这条机制有一个值得注意的副作用:**agent 自己的回复也写回记忆**
> (`task.go` 的 `context.Append(Source: "agent")`),所以它在同一场面上会**读到自己先前的结论**。
> 当输入已过期时,它会据此认出「这事上一轮已经办过」并**主动纠正自己先前的错误判断**——
> 表现为自我复盘。这是记忆召回的自然结果,内核里**没有**任何名为「反思」的机制。
**输入调度:两类别 + 四级中断** — 输入不直接进 LLM,先进调度器。 **输入调度:两类别 + 四级中断** — 输入不直接进 LLM,先进调度器。
排队(待办工作)与中断(按"有多不能等"分 L1~L4)两类;高级可抢占低级并保存现场 排队(待办工作)与中断(按"有多不能等"分 L1~L4)两类;高级可抢占低级并保存现场
(中断栈),同级不抢占。L4 只归内核与内核级插件(如 WebUI 终止按钮)。 (中断栈),同级不抢占。L4 只归内核与内核级插件(如 WebUI 终止按钮)。
@ -76,14 +88,22 @@ sequenceDiagram
LLM->>LLM: 安全点:中断求值/让位 LLM->>LLM: 安全点:中断求值/让位
LLM->>LLM: LLM Chat LLM->>LLM: LLM Chat
LLM->>ST: StagePostAction 插件可修改/短路 LLM->>ST: StagePostAction 插件可修改/短路
alt 无tool call alt 无 tool call
LLM-->>EV: 返回response LLM-->>EV: 返回response
else else 一批 N 个 tool_call
loop 每个tool Note over EV: batchRunnable 判据:批内 >1 且全部 ParallelSafe<br/>且无同通道重复发送(output_send__「通道」)
ST->>ST: StageBeforeToolcall 插件可拒绝 alt 可并发(三条全满足)
LLM->>LLM: executeToolCall par fan-out 并发执行
ST->>ST: StageAfterToolcall ST->>ST: StageBeforeToolcall ×N 插件可拒绝
LLM->>LLM: executeToolCall ×N
end
else 整批降级串行(任一个未声明 ParallelSafe)
loop 每个 tool 依次
ST->>ST: StageBeforeToolcall 插件可拒绝
LLM->>LLM: executeToolCall
end
end end
Note over EV: 顺序收尾:按**声明序** StageAfterToolcall → 落 tool 消息<br/>同通道输出严格保序
end end
end end
end end
@ -104,18 +124,26 @@ sequenceDiagram
flowchart LR flowchart LR
S1[① on_input] --> S2[② pre_action] S1[① on_input] --> S2[② pre_action]
S2 --> S3[③ post_action] S2 --> S3[③ post_action]
S3 --> Q{有tool?} S3 --> Q{有 tool_call?}
Q -->|是| S4[④ before_toolcall] Q -->|是,一批 N 个| PB{batchRunnable?<br/>全声明 ParallelSafe<br/>且无同通道重复发送}
S4 --> T[executeToolCall] PB -->|可并发| S45P[④⑤ 并发 ×N<br/>before_toolcall×N → 执行×N<br/>→ 按声明序 after_toolcall]
T --> S5[⑤ after_toolcall] PB -->|整批降级| S45S[④⑤ 依次 ×N<br/>before_toolcall → 执行<br/>→ after_toolcall]
S5 --> S3 S45P --> S3
S45S --> S3
Q -->|否| S6[⑥ before_output] Q -->|否| S6[⑥ before_output]
S6 --> S7[⑦ after_output] S6 --> S7[⑦ after_output]
style S1 fill:#e1f5fe style S1 fill:#e1f5fe
style S3 fill:#fff3e0 style S3 fill:#fff3e0
style S6 fill:#e8f5e9 style S6 fill:#e8f5e9
style S45P fill:#fce4ec
style S45S fill:#f5f5f5
``` ```
> ①–⑦ 七个 Stage 均由 `internal/sdk/plugin.go` 公开(`StageOnInput` … `StageAfterOutput`),
> 插件可注册挂钩。⚠️ 并行只影响 **④⑤ 的执行时序**:`post_action` 仍在本批工具
> 全部收尾后由下一轮触发,`after_toolcall` 也仍按**声明序**回调——
> 并发的是 IO 等待,不是插件契约的可见顺序。
### 三、三层记忆 ### 三、三层记忆
```mermaid ```mermaid
@ -138,6 +166,17 @@ flowchart TB
IDX[Indexer 向量+jieba→BFS depth=2] -->|【记忆索引】| SP IDX[Indexer 向量+jieba→BFS depth=2] -->|【记忆索引】| SP
MEM[memory_recall/commit/merge/purge/edit] MEM[memory_recall/commit/merge/purge/edit]
SOC[person_query/set_trait] SOC[person_query/set_trait]
subgraph SC[场面识别:声明 + 涌现]
FE[① 指纹 chan/peer/tool/topic/part<br/>权重 1.0/1.0/0.8/0.4/0.2]
EN{② EnterSceneWithHint<br/>相似度 ≥0.5 归属<br/>≥0.35 唤起}
FE --> EN
EN -->|插件已声明| DEC[origin=declared]
EN -->|重现 ≥2 次| EM[origin=emergent<br/>自动长出场景]
EM -->|每次重现 strength+1| STR[③ 用进废退<br/>久不重现按半衰期衰减]
DEC --> STR
end
STR -->|Primary| CARRY[本轮命中的场景<br/>挂载的记忆自动唤起]
CARRY -->|【场景记忆】| SP
end end
subgraph H[④ 心跳蒸馏] subgraph H[④ 心跳蒸馏]
REORG -->|Step3 冷文档| CD REORG -->|Step3 冷文档| CD
@ -151,13 +190,6 @@ flowchart TB
详细说明见 [`assets/docs/zh/ARCHITECTURE.md`](assets/docs/zh/ARCHITECTURE.md)。 详细说明见 [`assets/docs/zh/ARCHITECTURE.md`](assets/docs/zh/ARCHITECTURE.md)。
## 看板娘
<div align="center">
<img src="assets/branding/mascot-xiaozhai.webp" alt="HomeAgent 看板娘 小宅" width="200">
<p><strong>小宅</strong> — HomeAgent 看板娘</p>
</div>
## 快速体验 ## 快速体验
```bash ```bash
@ -203,17 +235,33 @@ internal/
├── memory/ 三层记忆:Graph(SQLite) / Document(JSON+TF-IDF) / Text(JSONL) + StaticEmbedder(预训练词嵌入/TF-IDF回退) + CleanTemplateText(去模版) ├── memory/ 三层记忆:Graph(SQLite) / Document(JSON+TF-IDF) / Text(JSONL) + StaticEmbedder(预训练词嵌入/TF-IDF回退) + CleanTemplateText(去模版)
├── knowledge/ 知识库(文件系统 + TF-IDF) ├── knowledge/ 知识库(文件系统 + TF-IDF)
├── plugin/ 插件注册表 + 子进程加载器(stdio RPC + 共享内存段 + 事件环) ├── plugin/ 插件注册表 + 子进程加载器(stdio RPC + 共享内存段 + 事件环)
├── plugins/ 内置 18 个插件(webui/cli/timer/cmd/mcp/files/cfgmgr/agentcli/healthcheck/pluginmgr/clawhubadapter/multimodal/remotedevice/ai_image/localuse/skillmgr/data 等) ├── plugins/ 内置 20 个插件(webui/cli/timer/cmd/mcp/files/cfgmgr/agentcli/healthcheck/pluginmgr/clawhubadapter/multimodal/remotedevice/ai_image/localuse/skillmgr/data/seq/kbtree 等)
├── sdk/ PluginSDK(Tool/Stage/Event 三通道) ├── sdk/ PluginSDK(Tool/Stage/Event 三通道)
├── config/ SQLite 配置中心 ├── config/ SQLite 配置中心
├── events/ 事件总线 ├── events/ 事件总线
└── internal/lua/adapters/ 10 个 LLM 协议适配器脚本 └── internal/lua/adapters/ 10 个 LLM 协议适配器脚本
外部插件开发见 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库,使用 `hmapdev` 工具链开发,参考 `example/` 目录下的 Go 和 Lua 示例 外部插件开发见 [homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) 仓库,使用 `hmapdev` 工具链开发,参考 `example/` 目录下的 Go 和 Lua 示例
``` ```
## 项目状态 ## 项目状态
**v1.3.x 线**(v1.3.1–v1.3.12,最新已发布)—— **驻留式子 agent** + **输入调度器重做**。 **v1.4.x 线**(进行中,`main`)—— **声明式并发安全** + **序列编排(seq)** + **工具结果诚实化**。
- **同轮工具并行执行**:同一轮里的多个 `tool_call` 默认并发执行,**但以「安全」为前置**——
工具必须在自己的 `ToolDef` 里显式声明 `ParallelSafe`(内置工具用 `toolDefOptions`),
未声明的一律串行;`Serial` 优先级更高。**批内只要有一个不安全,整批降级为串行**。
并发声明写在**工具自身**,内核不做硬编码工具名安全表。同通道输出**严格保序**,
结果与文本分离(`toolOutcome.Text` / `Raw`)。
- **`seq` 序列编排插件**:AST 解析 + 持久化、具名 group、变量槽、**组内并行 + 组间串行**、
条件调用(`seq_when_call`)、跨序列调用图与环检测、错误契约。
- **工具结果只统计不裁剪**:结果超出预算时**报告**统计量,不静默截断。
- **结构化工具错误契约**:参数按 schema **预校验**(分派前拦下)、
「工具不存在」与「执行失败」区分、`Success` 不再恒真。
- **设备命令白名单可配置**(waiter `device_cmd_allowlist`)、
**站点漂移巡检**、**GUI 增量渲染**(保留全部历史,按 `data-msgkey` 复用节点,
50/200/400 条实测 8.2/23.5/38.2ms)。
**v1.3.x 线**(v1.3.1–v1.3.13)—— **驻留式子 agent** + **输入调度器重做**。
- **驻留式子 agent**:内核可派驻轻量内核的子 agent(自己的调度器、自己的 temp 图记忆、 - **驻留式子 agent**:内核可派驻轻量内核的子 agent(自己的调度器、自己的 temp 图记忆、
共享通道登记表)。父经 `resident_agents`(list/create/send/inspect/compress/reclaim/destroy) 共享通道登记表)。父经 `resident_agents`(list/create/send/inspect/compress/reclaim/destroy)
@ -270,7 +318,7 @@ internal/
> inline/small **30.4µs**、frame/small 51.5µs、inline/large 767µs、frame/large 398µs。 > inline/small **30.4µs**、frame/small 51.5µs、inline/large 767µs、frame/large 398µs。
> 保留原文不修改,以免伪造历史。 > 保留原文不修改,以免伪造历史。
**v1.1.1** — 多模态贯通**插件边界**。v1.1.0 让记忆系统支持了二进制多媒体节点,但那条链路只对内核自己开放;本版打通到插件与模型。公开 SDK 新增媒体字段与三个媒体注入接口(配套 [SDK v1.1.0](https://gitcode.com/JianFeeeee/homeagent-sdk/releases/tag/v1.1.0),整条 1.1.x 线共用),内核实现对应四个 RPC。桥接层此前在**静默裁字段**:插件交进来的 `Confidence`/类型/`SentenceText` 全被丢弃、`Doc` 只留三个字段、`Remove` 不解引用(媒体永久算「被引用」,GC 收不掉)。`processTextInput`/`processMediaInput` 归一成一条 `processInput`,媒体路径由此获得它一直缺的去重、`no_memory`、通道 `Cleaner`、中断语义、`EventRawInput`。修掉三处真实缺陷:**用户发的图从来没出现在 WebUI 聊天记录里**(媒体路径发布 map 而订阅方断言 string)、**`memory_commit` 的 `sentence_text` 从未暴露给模型**(而它是媒体绑定链的必经环节)、**`PluginSDK` 两处并发竞态**(`-race` 实测 11 处,插件重载瞬间偶发 nil 解引用崩溃)。 **v1.1.1** — 多模态贯通**插件边界**。v1.1.0 让记忆系统支持了二进制多媒体节点,但那条链路只对内核自己开放;本版打通到插件与模型。公开 SDK 新增媒体字段与三个媒体注入接口(配套 [SDK v1.1.0](https://github.com/JianFeeeee/homeagentsdk/tree/v1.1.0),整条 1.1.x 线共用),内核实现对应四个 RPC。桥接层此前在**静默裁字段**:插件交进来的 `Confidence`/类型/`SentenceText` 全被丢弃、`Doc` 只留三个字段、`Remove` 不解引用(媒体永久算「被引用」,GC 收不掉)。`processTextInput`/`processMediaInput` 归一成一条 `processInput`,媒体路径由此获得它一直缺的去重、`no_memory`、通道 `Cleaner`、中断语义、`EventRawInput`。修掉三处真实缺陷:**用户发的图从来没出现在 WebUI 聊天记录里**(媒体路径发布 map 而订阅方断言 string)、**`memory_commit` 的 `sentence_text` 从未暴露给模型**(而它是媒体绑定链的必经环节)、**`PluginSDK` 两处并发竞态**(`-race` 实测 11 处,插件重载瞬间偶发 nil 解引用崩溃)。
**v1.1.0** — 记忆系统支持**二进制多媒体节点**。内容寻址媒体存储(CAS + SQLite 元数据 + 磁盘 blob,`Get` always 重校 digest),贯通 L0(上下文事件)/L2(文档)/L3(图谱句子)三层,引用计数式 GC(有引用者绝不删)。视觉模型生成的描述文本是持久语义记忆,blob 只是可被容量 GC 淘汰的缓存。 **v1.1.0** — 记忆系统支持**二进制多媒体节点**。内容寻址媒体存储(CAS + SQLite 元数据 + 磁盘 blob,`Get` always 重校 digest),贯通 L0(上下文事件)/L2(文档)/L3(图谱句子)三层,引用计数式 GC(有引用者绝不删)。视觉模型生成的描述文本是持久语义记忆,blob 只是可被容量 GC 淘汰的缓存。
@ -278,10 +326,19 @@ internal/
**v0.9.0** — C ABI v2:外部插件 Stage 回调支持写回(`invoke_stage` 增加 result 输出,插件可在 OnInput/AfterToolcall/PostAction 修改 RawMessage/LLMText/ToolResults 等并同步回内核),ABI 版本随内核 minor 对齐(v0.9.x → ABIVersion=2,`version_min=1` 向后兼容旧插件)。同步修复工具循环 zen 兼容补位误伤首轮 system 上下文的问题。配套 SDK 提供增强版 sanitizer 示例(坏 UTF-8/U+FFFD/ANSI 转义全链路清洗)。**该 ABI 已随 v1.0.0 退场。** **v0.9.0** — C ABI v2:外部插件 Stage 回调支持写回(`invoke_stage` 增加 result 输出,插件可在 OnInput/AfterToolcall/PostAction 修改 RawMessage/LLMText/ToolResults 等并同步回内核),ABI 版本随内核 minor 对齐(v0.9.x → ABIVersion=2,`version_min=1` 向后兼容旧插件)。同步修复工具循环 zen 兼容补位误伤首轮 system 上下文的问题。配套 SDK 提供增强版 sanitizer 示例(坏 UTF-8/U+FFFD/ANSI 转义全链路清洗)。**该 ABI 已随 v1.0.0 退场。**
**v0.8.0** — 核心可用,插件系统增强。内置 20+ 插件,外部插件开发见 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库。新增输入通道 `NoMemory`/`Cleaner`、`ChannelDef`、插件禁用/启用系统(CLI + WebUI),`plugindev` 工具链完成 C ABI `ChannelDef` 传递。 **v0.8.0** — 核心可用,插件系统增强。内置 20+ 插件,外部插件开发见 [homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) 仓库。新增输入通道 `NoMemory`/`Cleaner`、`ChannelDef`、插件禁用/启用系统(CLI + WebUI),`plugindev` 工具链完成 C ABI `ChannelDef` 传递。
## 文档 ## 文档
**在线文档**:
- 介绍站(项目总览):<https://introduce.homeagent.jianfgit.xyz/>
- 插件 SDK 文档(快速开始 / API 参考 / 指南 / 示例):<https://sdk.homeagent.jianfgit.xyz/>
—— 面向 agent 的纯文本入口:[`llms.txt`](https://sdk.homeagent.jianfgit.xyz/llms.txt) /
[`llms-full.txt`](https://sdk.homeagent.jianfgit.xyz/llms-full.txt)
**仓内文档**(随代码版本走):
- [项目概览](assets/docs/zh/OVERVIEW.md) | [English](assets/docs/en/OVERVIEW.md) - [项目概览](assets/docs/zh/OVERVIEW.md) | [English](assets/docs/en/OVERVIEW.md)
- [技术架构](assets/docs/zh/ARCHITECTURE.md) | [English](assets/docs/en/ARCHITECTURE.md) - [技术架构](assets/docs/zh/ARCHITECTURE.md) | [English](assets/docs/en/ARCHITECTURE.md)
- [插件开发指南](assets/docs/zh/PLUGIN_DEV.md) | [English](assets/docs/en/PLUGIN_DEV.md) - [插件开发指南](assets/docs/zh/PLUGIN_DEV.md) | [English](assets/docs/en/PLUGIN_DEV.md)
@ -290,7 +347,7 @@ internal/
## 下载 ## 下载
[Releases](https://gitcode.com/JianFeeeee/HomeAgent/releases) 提供三种变体: [Releases](https://github.com/JianFeeeee/HomeAgent/releases) 提供三种变体:
| 变体 | 内容 | 适用 | | 变体 | 内容 | 适用 |
| --- | --- | --- | | --- | --- | --- |
@ -328,12 +385,12 @@ make install # 安装到系统
**通过网络提供服务时也要向使用者提供源码**(§13 Remote Network Interaction)。 **通过网络提供服务时也要向使用者提供源码**(§13 Remote Network Interaction)。
即:任何人把改过的 HomeAgent 对外提供网络服务,都必须让该服务的使用者拿到改动后的源码。 即:任何人把改过的 HomeAgent 对外提供网络服务,都必须让该服务的使用者拿到改动后的源码。
插件与本项目通过公开 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 静态链接 插件与本项目通过公开 [homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) 静态链接
(SDK 源码会进入插件二进制),但那个仓**以 MIT 发布**——MIT 是宽松许可,拿到授权的代码不继承 (SDK 源码会进入插件二进制),但那个仓**以 MIT 发布**——MIT 是宽松许可,拿到授权的代码不继承
本项目的 AGPL。因此**外部插件不是本项目的衍生作品**,作者可自行选择许可(含闭源、商业、私有), 本项目的 AGPL。因此**外部插件不是本项目的衍生作品**,作者可自行选择许可(含闭源、商业、私有),
既不必同许可、也不受 §13 网络条款约束。第三方插件生态的安全与活跃正建立在这条之上。 既不必同许可、也不受 §13 网络条款约束。第三方插件生态的安全与活跃正建立在这条之上。
边界很清楚:**AGPL 覆盖内核与随包内置插件**(`homed`、`internal/`、`internal/plugins/` 下 18 个内置插件); 边界很清楚:**AGPL 覆盖内核与随包内置插件**(`homed`、`internal/`、`internal/plugins/` 下 20 个内置插件);
**MIT 覆盖公开 SDK**(`sdk/`,`go.mod` 零外部依赖、只依赖 Go 标准库,不引用内核任何代码)。 **MIT 覆盖公开 SDK**(`sdk/`,`go.mod` 零外部依赖、只依赖 Go 标准库,不引用内核任何代码)。
子进程隔离在这里不重要了——决定许可的是被链接的 SDK 代码本身,而它是 MIT。 子进程隔离在这里不重要了——决定许可的是被链接的 SDK 代码本身,而它是 MIT。

View File

@ -87,12 +87,20 @@ sequenceDiagram
LLM->>ST: StagePostAction Plugin can modify/short-circuit LLM->>ST: StagePostAction Plugin can modify/short-circuit
alt No tool call alt No tool call
LLM-->>EV: Returns response LLM-->>EV: Returns response
else else a batch of N tool_calls
loop Each tool Note over EV: batchRunnable criteria: batch >1 AND all declare ParallelSafe<br/>and no same-channel duplicate send (output_send__«channel»)
ST->>ST: StageBeforeToolcall Plugin can reject alt parallel (all three satisfied)
LLM->>LLM: executeToolCall par fan-out concurrent execution
ST->>ST: StageAfterToolcall ST->>ST: StageBeforeToolcall ×N Plugin can reject
LLM->>LLM: executeToolCall ×N
end
else whole batch degrades to serial (any undeclared)
loop Each tool in turn
ST->>ST: StageBeforeToolcall Plugin can reject
LLM->>LLM: executeToolCall
end
end end
Note over EV: ordered tail: StageAfterToolcall in **declaration order** → append tool msgs<br/>same-channel output strictly ordered
end end
end end
end end
@ -113,18 +121,27 @@ sequenceDiagram
flowchart LR flowchart LR
S1[① on_input] --> S2[② pre_action] S1[① on_input] --> S2[② pre_action]
S2 --> S3[③ post_action] S2 --> S3[③ post_action]
S3 --> Q{Has tool?} S3 --> Q{Any tool_call?}
Q -->|Yes| S4[④ before_toolcall] Q -->|Yes, batch of N| PB{batchRunnable?<br/>all declare ParallelSafe<br/>and no same-channel duplicate send}
S4 --> T[executeToolCall] PB -->|parallel| S45P[④⑤ concurrent ×N<br/>before_toolcall×N → execute×N<br/>→ after_toolcall in declaration order]
T --> S5[⑤ after_toolcall] PB -->|degrade to serial| S45S[④⑤ sequential ×N<br/>before_toolcall → execute<br/>→ after_toolcall]
S5 --> S3 S45P --> S3
S45S --> S3
Q -->|No| S6[⑥ before_output] Q -->|No| S6[⑥ before_output]
S6 --> S7[⑦ after_output] S6 --> S7[⑦ after_output]
style S1 fill:#e1f5fe style S1 fill:#e1f5fe
style S3 fill:#fff3e0 style S3 fill:#fff3e0
style S6 fill:#e8f5e9 style S6 fill:#e8f5e9
style S45P fill:#fce4ec
style S45S fill:#f5f5f5
``` ```
> All seven stages ①–⑦ are published by `internal/sdk/plugin.go` (`StageOnInput` …
> `StageAfterOutput`); plugins may register hooks. ⚠️ Parallelism affects only the
> **execution timing of ④⑤**: `post_action` still fires on the next round once the whole
> batch is collected, and `after_toolcall` still runs in **declaration order** —
> what runs concurrently is IO waiting, not the plugin-visible contract order.
### 3. Three-Layer Memory ### 3. Three-Layer Memory
```mermaid ```mermaid
@ -137,16 +154,27 @@ flowchart TB
end end
subgraph D[② Document File Memory] subgraph D[② Document File Memory]
DS[DocStore JSON+TF-IDF] DS[DocStore JSON+TF-IDF]
Q1[Query summary auto-inject] -->|[Related Memory Docs]| SP Q1[Query summary auto-inject] -->|Related Memory Docs| SP
Q2[doc_query LLM active recall] -->|Consume+delete source| DS Q2[doc_query LLM active recall] -->|Consume+delete source| DS
Q2 -->|Original timestamp write to context| RC Q2 -->|Original timestamp write to context| RC
CD[FindColdDocs 72h] -->|docToTriples| G CD[FindColdDocs 72h] -->|docToTriples| G
end end
subgraph G[③ Graph Database] subgraph G[③ Graph Database]
DB[(SQLite)] DB[(SQLite)]
IDX[Indexer vector+jieba→BFS depth=2] -->|[Memory Index]| SP IDX[Indexer vector+jieba→BFS depth=2] -->|Memory Index| SP
MEM[memory_recall/commit/merge/purge/edit] MEM[memory_recall/commit/merge/purge/edit]
SOC[person_query/set_trait] SOC[person_query/set_trait]
subgraph SC[Scene recognition: declared + emergent]
FE[① fingerprint chan/peer/tool/topic/part<br/>weights 1.0/1.0/0.8/0.4/0.2]
EN{② EnterSceneWithHint<br/>similarity ≥0.5 join<br/>≥0.35 recall}
FE --> EN
EN -->|plugin declared| DEC[origin=declared]
EN -->|seen ≥2 times| EM[origin=emergent<br/>the scene grows itself]
EM -->|each recurrence strength+1| STR[③ use-it-or-lose-it<br/>decay by half-life when unused]
DEC --> STR
end
STR -->|Primary| CARRY[scene hit this round<br/>its memories auto-recalled]
CARRY -->|Scene Memory| SP
end end
subgraph H[④ Heartbeat Distillation] subgraph H[④ Heartbeat Distillation]
REORG -->|Step3 Cold docs| CD REORG -->|Step3 Cold docs| CD
@ -160,13 +188,6 @@ flowchart TB
See [`assets/docs/en/ARCHITECTURE.md`](assets/docs/en/ARCHITECTURE.md) for details. See [`assets/docs/en/ARCHITECTURE.md`](assets/docs/en/ARCHITECTURE.md) for details.
## Web Mascot
<div align="center">
<img src="assets/branding/mascot-xiaozhai.webp" alt="HomeAgent Web Mascot Xiaozhai" width="200">
<p><strong>Xiaozhai</strong> — HomeAgent Web Mascot</p>
</div>
## Quick Start ## Quick Start
```bash ```bash
@ -195,17 +216,35 @@ internal/
├── memory/ Three-layer memory: Graph(SQLite) / Document(JSON+TF-IDF) / Text(JSONL) + StaticEmbedder(pretrained word embedding/TF-IDF fallback) + CleanTemplateText(de-template) ├── memory/ Three-layer memory: Graph(SQLite) / Document(JSON+TF-IDF) / Text(JSONL) + StaticEmbedder(pretrained word embedding/TF-IDF fallback) + CleanTemplateText(de-template)
├── knowledge/ Knowledge base (filesystem + TF-IDF) ├── knowledge/ Knowledge base (filesystem + TF-IDF)
├── plugin/ Plugin registry + subprocess loader (stdio RPC + shared memory segment + event ring) ├── plugin/ Plugin registry + subprocess loader (stdio RPC + shared memory segment + event ring)
├── plugins/ 18 built-in plugins (webui/cli/timer/cmd/mcp/files/cfgmgr/agentcli/healthcheck/pluginmgr/clawhubadapter/multimodal/remotedevice/ai_image/localuse/skillmgr/data, ...) ├── plugins/ 20 built-in plugins (webui/cli/timer/cmd/mcp/files/cfgmgr/agentcli/healthcheck/pluginmgr/clawhubadapter/multimodal/remotedevice/ai_image/localuse/skillmgr/data/seq/kbtree, ...)
├── sdk/ PluginSDK (Tool/Stage/Event three channels) ├── sdk/ PluginSDK (Tool/Stage/Event three channels)
├── config/ SQLite config center ├── config/ SQLite config center
├── events/ Event bus ├── events/ Event bus
└── internal/lua/adapters/ 10 LLM protocol adapter scripts └── internal/lua/adapters/ 10 LLM protocol adapter scripts
External plugin development: see [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repo, use `hmapdev` toolchain, refer to Go and Lua examples in `example/` External plugin development: see [homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) repo, use `hmapdev` toolchain, refer to Go and Lua examples in `example/`
``` ```
## Project Status ## Project Status
**v1.3.x line** (v1.3.1–v1.3.12, latest released) — **resident sub-agents** + **input scheduler rework**. **v1.4.x line** (in progress, `main`) — **declarative concurrency safety** + **sequence orchestration (seq)** + **honest tool results**.
- **Same-round parallel tool execution**: multiple `tool_call`s in one round run concurrently,
**but safety gates it** — a tool must explicitly declare `ParallelSafe` in its own `ToolDef`
(built-ins use `toolDefOptions`); anything undeclared runs serially, and `Serial` wins.
**A single unsafe tool degrades the whole batch to serial.** Declarations live **in the tool
itself**; the kernel keeps no hardcoded name table. Same-channel output stays **strictly ordered**,
and results are separated from text (`toolOutcome.Text` / `Raw`).
- **`seq` orchestration plugin**: AST parsing + persistence, named groups, variable slots,
**parallel within a group / serial between groups**, conditional calls (`seq_when_call`),
cross-sequence call graph with cycle detection, error contract.
- **Tool results are reported, never silently truncated** — over budget emits statistics.
- **Structured tool error contract**: arguments pre-validated against the schema (rejected before
dispatch), "tool not found" distinguished from "execution failed", `Success` no longer always true.
- **Configurable device command allowlist** (waiter `device_cmd_allowlist`),
**site drift watcher**, **incremental GUI rendering** (keeps all history, reuses nodes by
`data-msgkey`; 50/200/400 messages measured at 8.2/23.5/38.2ms).
**v1.3.x line** (v1.3.1–v1.3.13) — **resident sub-agents** + **input scheduler rework**.
- **Resident sub-agents**: the kernel can station lightweight-kernel child agents (their own - **Resident sub-agents**: the kernel can station lightweight-kernel child agents (their own
scheduler, their own temp graph memory, sharing the channel registry). The parent dispatches scheduler, their own temp graph memory, sharing the channel registry). The parent dispatches
@ -215,11 +254,11 @@ External plugin development: see [homeagent-sdk](https://gitcode.com/JianFeeeee/
(consistent across all three filter points) lets parent/child deliver to each other; (consistent across all three filter points) lets parent/child deliver to each other;
device capabilities became output channels too (one `device/<id>` per device). device capabilities became output channels too (one `device/<id>` per device).
- **Input scheduler**: two task classes (queued/interrupt) + four interrupt levels (L1–L4) - **Input scheduler**: two task classes (queued/interrupt) + four interrupt levels (L1–L4)
+ preempt/suspend/resume/interrupt-stack; same level never preempts same level, with a - preempt/suspend/resume/interrupt-stack; same level never preempts same level, with a
starvation guard and preemption cooldown. L4 belongs only to the kernel and kernel-level starvation guard and preemption cooldown. L4 belongs only to the kernel and kernel-level
plugins (e.g. the WebUI stop button). plugins (e.g. the WebUI stop button).
- **Lightweight kernel profile**: a child's memory surface narrows to "conventional context - **Lightweight kernel profile**: a child's memory surface narrows to "conventional context
+ graph memory" (narrow interface; the main graph opens as a query_only handle, writes go - graph memory" (narrow interface; the main graph opens as a query_only handle, writes go
to its own temp instance). to its own temp instance).
- **Backlog timely feedback** (later in the line): when the main agent is busy for a long time, - **Backlog timely feedback** (later in the line): when the main agent is busy for a long time,
the kernel hands queued input to a temporary **triage assistant** — simple items are handled the kernel hands queued input to a temporary **triage assistant** — simple items are handled
@ -284,7 +323,7 @@ External plugin development: see [homeagent-sdk](https://gitcode.com/JianFeeeee/
**v1.1.1** — Multimodal reaches the **plugin boundary**. v1.1.0 gave the memory system binary **v1.1.1** — Multimodal reaches the **plugin boundary**. v1.1.0 gave the memory system binary
multimedia nodes, but that path was open only to the kernel itself; this release opens it to multimedia nodes, but that path was open only to the kernel itself; this release opens it to
plugins and the model. The public SDK gains media fields and three media injection methods plugins and the model. The public SDK gains media fields and three media injection methods
(paired with [SDK v1.1.0](https://gitcode.com/JianFeeeee/homeagent-sdk/releases/tag/v1.1.0), (paired with [SDK v1.1.0](https://github.com/JianFeeeee/homeagentsdk/tree/v1.1.0),
shared by the whole 1.1.x line), and the kernel implements the four matching RPCs. The bridge shared by the whole 1.1.x line), and the kernel implements the four matching RPCs. The bridge
layer had been **silently dropping fields**: `Confidence`/types/`SentenceText` handed in by a layer had been **silently dropping fields**: `Confidence`/types/`SentenceText` handed in by a
plugin were discarded, `Doc` kept only three fields, and `Remove` never released references plugin were discarded, `Doc` kept only three fields, and `Remove` never released references
@ -307,10 +346,19 @@ semantic memory; the blob is only a cache that capacity GC may evict.
**v0.9.0** — C ABI v2: external plugin Stage callbacks can now write back (`invoke_stage` gained a result out-param; plugins may mutate RawMessage/LLMText/ToolResults etc. in OnInput/AfterToolcall/PostAction and have them synced to the core). ABI version now tracks core minor releases (v0.9.x → ABIVersion=2, `version_min=1` keeps old plugins loadable). Also fixes the tool-loop zen-compat placeholder that wrongly fired on first-turn system context tail. The SDK ships an enhanced sanitizer example (bad-UTF-8 / U+FFFD / ANSI-escape scrub across the whole pipeline). **This ABI retired with v1.0.0.** **v0.9.0** — C ABI v2: external plugin Stage callbacks can now write back (`invoke_stage` gained a result out-param; plugins may mutate RawMessage/LLMText/ToolResults etc. in OnInput/AfterToolcall/PostAction and have them synced to the core). ABI version now tracks core minor releases (v0.9.x → ABIVersion=2, `version_min=1` keeps old plugins loadable). Also fixes the tool-loop zen-compat placeholder that wrongly fired on first-turn system context tail. The SDK ships an enhanced sanitizer example (bad-UTF-8 / U+FFFD / ANSI-escape scrub across the whole pipeline). **This ABI retired with v1.0.0.**
**v0.8.0** — Core is functional, plugin system enhanced. 20+ built-in plugins. External plugin development via [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repo. Added input channel `NoMemory`/`Cleaner`, `ChannelDef`, plugin disable/enable system (CLI + WebUI), `plugindev` toolchain C ABI `ChannelDef` support. **v0.8.0** — Core is functional, plugin system enhanced. 20+ built-in plugins. External plugin development via [homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) repo. Added input channel `NoMemory`/`Cleaner`, `ChannelDef`, plugin disable/enable system (CLI + WebUI), `plugindev` toolchain C ABI `ChannelDef` support.
## Documentation ## Documentation
**Online docs**:
- Introduction site (project overview): <https://introduce.homeagent.jianfgit.xyz/>
- Plugin SDK docs (getting started / API reference / guides / examples): <https://sdk.homeagent.jianfgit.xyz/>
— agent-friendly plain text: [`llms.txt`](https://sdk.homeagent.jianfgit.xyz/llms.txt) /
[`llms-full.txt`](https://sdk.homeagent.jianfgit.xyz/llms-full.txt)
**In-repo docs** (versioned with the code):
- [Project Overview](assets/docs/en/OVERVIEW.md) | [中文](assets/docs/zh/OVERVIEW.md) - [Project Overview](assets/docs/en/OVERVIEW.md) | [中文](assets/docs/zh/OVERVIEW.md)
- [Technical Architecture](assets/docs/en/ARCHITECTURE.md) | [中文](assets/docs/zh/ARCHITECTURE.md) - [Technical Architecture](assets/docs/en/ARCHITECTURE.md) | [中文](assets/docs/zh/ARCHITECTURE.md)
- [Plugin Development Guide](assets/docs/en/PLUGIN_DEV.md) | [中文](assets/docs/zh/PLUGIN_DEV.md) - [Plugin Development Guide](assets/docs/en/PLUGIN_DEV.md) | [中文](assets/docs/zh/PLUGIN_DEV.md)
@ -319,10 +367,10 @@ semantic memory; the blob is only a cache that capacity GC may evict.
## Downloads ## Downloads
[Releases](https://gitcode.com/JianFeeeee/HomeAgent/releases) ship three variants: [Releases](https://github.com/JianFeeeee/HomeAgent/releases) ship three variants:
| Variant | Contents | For | | Variant | Contents | For |
|---|---|---| | --- | --- | --- |
| **full** | homed + waiter + desktop GUI + systemd unit | Single-machine, everything | | **full** | homed + waiter + desktop GUI + systemd unit | Single-machine, everything |
| **server** | homed + waiter + systemd unit | Servers (no desktop environment) | | **server** | homed + waiter + systemd unit | Servers (no desktop environment) |
| **client** | waiter + desktop GUI | Connecting to a remote HomeAgent | | **client** | waiter + desktop GUI | Connecting to a remote HomeAgent |
@ -360,7 +408,7 @@ with it over a network** (§13, Remote Network Interaction). Anyone running a mo
as a network service therefore has to make the modified source available to that service's users. as a network service therefore has to make the modified source available to that service's users.
Plugins are **statically linked** against this project through the public Plugins are **statically linked** against this project through the public
[homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) (the SDK source ends up inside the [homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) (the SDK source ends up inside the
plugin binary), but that repository is released under **MIT** — a permissive license, so code plugin binary), but that repository is released under **MIT** — a permissive license, so code
received under it does **not** inherit this project's AGPL. External plugins are therefore **not received under it does **not** inherit this project's AGPL. External plugins are therefore **not
derivative works of this project**: authors pick their own license (closed-source, commercial or derivative works of this project**: authors pick their own license (closed-source, commercial or
@ -368,7 +416,7 @@ private included), with no same-license obligation and no §13 network clause. T
vitality of the third-party plugin ecosystem rest on this. vitality of the third-party plugin ecosystem rest on this.
The boundary is clean: **AGPL covers the kernel and the bundled plugins** (`homed`, `internal/`, The boundary is clean: **AGPL covers the kernel and the bundled plugins** (`homed`, `internal/`,
the 18 built-in plugins under `internal/plugins/`); **MIT covers the public SDK** (`sdk/`, whose the 20 built-in plugins under `internal/plugins/`); **MIT covers the public SDK** (`sdk/`, whose
`go.mod` has zero external dependencies and imports only the Go standard library — it never `go.mod` has zero external dependencies and imports only the Go standard library — it never
references any kernel code). Process isolation is beside the point here — what decides the references any kernel code). Process isolation is beside the point here — what decides the
license is the linked SDK code itself, and that code is MIT. license is the linked SDK code itself, and that code is MIT.
@ -376,7 +424,7 @@ license is the linked SDK code itself, and that code is MIT.
### Third-party components shipped with the packages ### Third-party components shipped with the packages
| Component | License | Location | | Component | License | Location |
|---|---|---| | --- | --- | --- |
| Chinese-CLIP ViT-B/16 (ONNX artifacts) | Apache-2.0 | `/usr/lib/homeagent/models/chinese-clip-vit-b16-onnx/` | | Chinese-CLIP ViT-B/16 (ONNX artifacts) | Apache-2.0 | `/usr/lib/homeagent/models/chinese-clip-vit-b16-onnx/` |
| ONNX Runtime (`libonnxruntime.so`) | MIT | `/usr/lib/homeagent/onnxruntime/` | | ONNX Runtime (`libonnxruntime.so`) | MIT | `/usr/lib/homeagent/onnxruntime/` |
| jieba dictionary (embedded in the binary) | MIT | `internal/memory/jiebadict/` | | jieba dictionary (embedded in the binary) | MIT | `internal/memory/jiebadict/` |

View File

@ -22,9 +22,11 @@
# 结论(打不了包)不变但**理由已变**,别照旧文字理解。 # 结论(打不了包)不变但**理由已变**,别照旧文字理解。
# #
# 用法: # 用法:
# ./deploy-sdk-site.sh # 构建 + 部署 + 验证 # ./deploy-sdk-site.sh # 构建 + 部署 + 验证(两个站)
# ./deploy-sdk-site.sh --check # 只核对线上与本地产物差异,不动线上 # ./deploy-sdk-site.sh --check # 只核对线上与本地产物差异,不动线上
# ./deploy-sdk-site.sh --rollback <备份目录名> # 回滚 # ./deploy-sdk-site.sh --introduce # 只部署 introduce 站(改 site/ 时用,跳过 SDK 站重建)
# ./deploy-sdk-site.sh --rollback <备份目录名> # 回滚 sdk 站
# ./deploy-sdk-site.sh --rollback-introduce <备份目录名> # 回滚 introduce 站
set -euo pipefail set -euo pipefail
@ -40,6 +42,23 @@ PKG=/tmp/$PKG_NAME
say() { printf '\n\033[1m%s\033[0m\n' "$*"; } say() { printf '\n\033[1m%s\033[0m\n' "$*"; }
info() { printf ' %s\n' "$*"; } info() { printf ' %s\n' "$*"; }
# ── 比对工具:必须是**真能报出差异**的 diff ──
#
# 2026-09-29 实测踩到:本机 PATH 首位的 diff
# (/opt/huawei/harmonyos/ohos-sdk/linux/toolchains/diff)对**任何**输入都返回 0
# 且无输出。用它做判据的后果不是「偶尔报错」,而是**永久给出「逐字节一致」的假绿灯**——
# --check 永远说「无需部署」,站点改完再也不会被更新,而且看不出来。
#
# 所以:钉绝对路径 + 启动时自检一次。自检不过就直接退出,
# 而不是继续拿一个坏判据去做部署决定(「探针不红先怀疑探针」)。
DIFF=/usr/bin/diff
[ -x "$DIFF" ] || { printf '找不到可用的 %s\n' "$DIFF" >&2; exit 1; }
if "$DIFF" -q <(printf 'A\n') <(printf 'B\n') >/dev/null 2>&1; then
printf '★ 判据自检失败:%s 对两个不同输入报告「无差异」。\n' "$DIFF" >&2
printf ' 这个 diff 是坏的(PATH 首位那个工具链自带的可能有问题),拒绝据此做部署判断。\n' >&2
exit 1
fi
# ── introduce 站 ── # ── introduce 站 ──
# #
# 零构建:/home/program/TrueAgent/site/ 里就是成品(site/README.md 自称 # 零构建:/home/program/TrueAgent/site/ 里就是成品(site/README.md 自称
@ -67,12 +86,12 @@ do_check_introduce() {
info "本机源: $n 个文件(已排除 README.md)" info "本机源: $n 个文件(已排除 README.md)"
files=$($SSH "sudo -n find $SITES/introduce -type f 2>/dev/null | wc -l" 2>/dev/null) files=$($SSH "sudo -n find $SITES/introduce -type f 2>/dev/null | wc -l" 2>/dev/null)
info "线上站: $files 个文件" info "线上站: $files 个文件"
if diff -q <(intro_local_md5) <(intro_remote_md5) >/dev/null 2>&1; then if "$DIFF" -q <(intro_local_md5) <(intro_remote_md5) >/dev/null 2>&1; then
info "✓ introduce 线上与本机源逐字节一致" info "✓ introduce 线上与本机源逐字节一致"
return 0 return 0
fi fi
info "✗ introduce 有差异:" info "✗ introduce 有差异:"
diff <(intro_local_md5) <(intro_remote_md5) | head -20 || true "$DIFF" <(intro_local_md5) <(intro_remote_md5) | head -20 || true
return 1 return 1
} }
@ -104,10 +123,10 @@ do_deploy_introduce() {
local code local code
code=$(curl -s -o /dev/null -m 10 -w '%{http_code}' -k https://introduce.homeagent.jianfgit.xyz/) code=$(curl -s -o /dev/null -m 10 -w '%{http_code}' -k https://introduce.homeagent.jianfgit.xyz/)
info "首页 http=$code(introduce 配了 try_files 回落,404 才是异常)" info "首页 http=$code(introduce 配了 try_files 回落,404 才是异常)"
if diff -q <(intro_local_md5) <(intro_remote_md5) >/dev/null 2>&1; then if "$DIFF" -q <(intro_local_md5) <(intro_remote_md5) >/dev/null 2>&1; then
info "✓ introduce 线上与本机源逐字节一致" info "✓ introduce 线上与本机源逐字节一致"
else else
info "✗ introduce 仍有差异:"; diff <(intro_local_md5) <(intro_remote_md5) | head -20; return 1 info "✗ introduce 仍有差异:"; "$DIFF" <(intro_local_md5) <(intro_remote_md5) | head -20; return 1
fi fi
} }
@ -132,11 +151,11 @@ do_check() {
info "本地产物: $n 个文件" info "本地产物: $n 个文件"
files=$($SSH "sudo -n find $SITES/sdk -type f 2>/dev/null | wc -l" 2>/dev/null) files=$($SSH "sudo -n find $SITES/sdk -type f 2>/dev/null | wc -l" 2>/dev/null)
info "线上站: $files 个文件" info "线上站: $files 个文件"
if diff -q <(local_md5) <(remote_md5) >/dev/null 2>&1; then if "$DIFF" -q <(local_md5) <(remote_md5) >/dev/null 2>&1; then
info "✓ 线上与本地产物逐字节一致,无需部署" info "✓ 线上与本地产物逐字节一致,无需部署"
else else
info "✗ sdk 有差异,需部署。差异文件:" info "✗ sdk 有差异,需部署。差异文件:"
diff <(local_md5) <(remote_md5) | head -20 || true "$DIFF" <(local_md5) <(remote_md5) | head -20 || true
fi fi
# introduce 始终检查:两个站是独立的,sdk 一致不代表 introduce 也一致 # introduce 始终检查:两个站是独立的,sdk 一致不代表 introduce 也一致
@ -169,8 +188,8 @@ do_deploy() {
sudo -n mv $SITES/sdk $SITES/.sdk-old-$TS sudo -n mv $SITES/sdk $SITES/.sdk-old-$TS
sudo -n mv $SITES/.sdk-new-$TS $SITES/sdk sudo -n mv $SITES/.sdk-new-$TS $SITES/sdk
echo -n '现役站文件数: '; sudo -n find $SITES/sdk -type f | wc -l echo -n '现役站文件数: '; sudo -n find $SITES/sdk -type f | wc -l
# .sdk-old 与刚建的 .sdk-bak 内容必然相同(同一份旧站复制两次), # 注意:这里的 diff 是**远端**执行的(远端是健康的 /usr/bin/diff),
# 留一份就够,白占 11M。 # 不受本机 PATH 首位那个坏 diff 影响。
if sudo -n diff -r -q $SITES/.sdk-bak-$TS $SITES/.sdk-old-$TS >/dev/null 2>&1; then if sudo -n diff -r -q $SITES/.sdk-bak-$TS $SITES/.sdk-old-$TS >/dev/null 2>&1; then
sudo -n rm -rf $SITES/.sdk-old-$TS sudo -n rm -rf $SITES/.sdk-old-$TS
echo '已删重复的 .sdk-old(与 .sdk-bak 内容相同)' echo '已删重复的 .sdk-old(与 .sdk-bak 内容相同)'
@ -190,15 +209,30 @@ do_deploy() {
printf ' %-34s ' "$p" printf ' %-34s ' "$p"
curl -s -o /dev/null -m 10 -w 'http=%{http_code}\n' -k "https://sdk.homeagent.jianfgit.xyz/$p" curl -s -o /dev/null -m 10 -w 'http=%{http_code}\n' -k "https://sdk.homeagent.jianfgit.xyz/$p"
done done
if diff -q <(local_md5) <(remote_md5) >/dev/null 2>&1; then if "$DIFF" -q <(local_md5) <(remote_md5) >/dev/null 2>&1; then
info "✓ 线上与本地产物逐字节一致" info "✓ 线上与本地产物逐字节一致"
else else
info "✗ 仍有差异:"; diff <(local_md5) <(remote_md5) | head -20; return 1 info "✗ 仍有差异:"; "$DIFF" <(local_md5) <(remote_md5) | head -20; return 1
fi fi
do_deploy_introduce do_deploy_introduce
} }
# 只跑 introduce 站:改 site/ 时用。
#
# 为何需要:do_deploy 会先把 SDK 文档站重建一遍(要跑 apidoc + mkdocs,产物 106 个文件、
# 十几 MB),而 site/ 的改动跟 SDK 站毫无关系。只动介绍页却重建整个文档站,
# 既慢又把一个本来没必要碰的线上站也卷进变更面。
do_deploy_introduce_only() {
say "introduce 站:先比对再决定"
# 一致就不动线上(越少触碰越好);不一致才走「备份→原子替换→回验」
if do_check_introduce; then
info "✓ 线上已是最新,无需部署"
return 0
fi
do_deploy_introduce
}
do_rollback() { do_rollback() {
local bak=${1:?用法: --rollback <备份目录名,如 .sdk-bak-20260927-223242>} local bak=${1:?用法: --rollback <备份目录名,如 .sdk-bak-20260927-223242>}
say "回滚到 $bak" say "回滚到 $bak"
@ -213,9 +247,25 @@ do_rollback() {
info "已回滚,失败版本留在 $SITES/.sdk-failed-$TS" info "已回滚,失败版本留在 $SITES/.sdk-failed-$TS"
} }
do_rollback_introduce() {
local bak=${1:?用法: --rollback-introduce <备份目录名,如 .intro-bak-20260929-170000>}
say "回滚 introduce 到 $bak"
$SSH "
set -e
[ -d '$SITES/$bak' ] || { echo '找不到备份 $SITES/$bak'; exit 1; }
sudo -n cp -a $SITES/introduce $SITES/.intro-failed-$TS
sudo -n rm -rf $SITES/introduce
sudo -n cp -a $SITES/$bak $SITES/introduce
echo -n '回滚后文件数: '; sudo -n find $SITES/introduce -type f | wc -l
" 2>&1 | tail -3
info "已回滚,失败版本留在 $SITES/.intro-failed-$TS"
}
case "${1:-}" in case "${1:-}" in
--check) do_check ;; --check) do_check ;;
--rollback) do_rollback "${2:?用法: --rollback <备份目录名>}" ;; --introduce) do_deploy_introduce_only ;;
"") do_deploy ;; --rollback) do_rollback "${2:?用法: --rollback <备份目录名>}" ;;
*) echo "用法: $0 [--check | --rollback <备份目录名>]"; exit 2 ;; --rollback-introduce) do_rollback_introduce "${2:?用法: --rollback-introduce <备份目录名>}" ;;
"") do_deploy ;;
*) echo "用法: $0 [--check | --introduce | --rollback <备份目录名> | --rollback-introduce <备份目录名>]"; exit 2 ;;
esac esac

View File

@ -203,18 +203,28 @@ spec = (d.get('devDependencies', {}) or {}).get('electron') or (d.get('dependenc
m = re.search(r'(\\d+(?:\\.\\d+)*)', spec) m = re.search(r'(\\d+(?:\\.\\d+)*)', spec)
print(m.group(1) if m else '') print(m.group(1) if m else '')
" 2>/dev/null || true) " 2>/dev/null || true)
[ -n "$ever" ] && echo " electron 版本取自 package.json 依赖声明: $ever(非精确)" if [ -n "$ever" ]; then
echo " electron 版本取自 package.json 依赖声明: $ever(非精确)"
fi
fi fi
mkdir -p "$gui_out" mkdir -p "$gui_out"
# 优先:缓存里的目标架构 zip(~/.cache/electron/<hash>/electron-v<ver>-linux-<arch>.zip) # 优先:缓存里的目标架构 zip(~/.cache/electron/<hash>/electron-v<ver>-linux-<arch>.zip)
#
# ★ 这里必须 `|| true`:`find` 对**不存在的目录**返回退出码 1,而本脚本是
# `set -euo pipefail`,命令替换里的失败会让整个脚本当场退出。
# 后果:任何**没有 ~/.cache/electron 的机器**(全新克隆、CI runner、
# 其他开发机)跑到这里就死,且只留下一行「electron 版本取自 package.json」
# 作为最后的输出,看不出真因。实测(2026-09-29,GitHub runner 与本地
# 移走缓存后均复现):build_go 全部成功,然后卡在这里静默退出。
# 本机历史上之所以一直「能打包」,只是因为碰巧有那份缓存。
local zip="" local zip=""
if [ -n "$ever" ]; then if [ -n "$ever" ]; then
zip=$(find "$HOME/.cache/electron" -name "electron-v${ever}-linux-${ELECTRON_ARCH}.zip" 2>/dev/null | head -1) zip=$(find "$HOME/.cache/electron" -name "electron-v${ever}-linux-${ELECTRON_ARCH}.zip" 2>/dev/null | head -1 || true)
fi fi
if [ -z "$zip" ]; then if [ -z "$zip" ]; then
zip=$(find "$HOME/.cache/electron" -name "electron-v*-linux-${ELECTRON_ARCH}.zip" 2>/dev/null | head -1) zip=$(find "$HOME/.cache/electron" -name "electron-v*-linux-${ELECTRON_ARCH}.zip" 2>/dev/null | head -1 || true)
fi fi
if [ -n "$zip" ]; then if [ -n "$zip" ]; then
@ -620,9 +630,11 @@ build_rpm() {
if [ ! -f "$rpmbuild_dir/usr/bin/rpmbuild" ]; then if [ ! -f "$rpmbuild_dir/usr/bin/rpmbuild" ]; then
# try to extract from cached deb packages # try to extract from cached deb packages
local rpm_deb local rpm_deb
rpm_deb="$(find /tmp -name "rpm_*.deb" -type f 2>/dev/null | head -1)" # 同 build_gui:`find` 对不存在/无命中会返回 1,`set -euo pipefail` 下
# 会让脚本当场退出(`|| true` 是给命令替换兜底,不是忽视错误)。
rpm_deb="$(find /tmp -name "rpm_*.deb" -type f 2>/dev/null | head -1 || true)"
if [ -z "$rpm_deb" ]; then if [ -z "$rpm_deb" ]; then
rpm_deb="$(find "$PROJECT_ROOT" -name "rpm_*.deb" -type f 2>/dev/null | head -1)" rpm_deb="$(find "$PROJECT_ROOT" -name "rpm_*.deb" -type f 2>/dev/null | head -1 || true)"
fi fi
if [ -n "$rpm_deb" ]; then if [ -n "$rpm_deb" ]; then
mkdir -p "$rpmbuild_dir" mkdir -p "$rpmbuild_dir"

302
docs/zh/ci-cd-runbook.md Normal file
View File

@ -0,0 +1,302 @@
# CI/CD 流水线手册(GitHub Actions)
> 2026-09-29 随仓库迁移 gitcode → GitHub 而建。此前 AtomGit 停止对普通用户
> 提供流水线,本仓无任何自动化验证。本文记录两条流水线的用法、机制与边界。
> 分支模型见 `docs/git-branching.md`(本文与其 §七「发版产物清单」衔接)。
## 0. 仓库与流水线总览
| 仓 | 位置 | 流水线 | 触发 |
| --- | --- | --- | --- |
| 主仓 HomeAgent | `github.com/JianFeeeee/HomeAgent`(公开) | `ci.yml` + `release.yml` | push / PR、`release/**` push |
| SDK 仓 homeagentsdk | `github.com/JianFeeeee/homeagentsdk`(公开) | `release.yml` | `release/**` push |
| gitcode 镜像 | 同名仓库 | 无(由主仓 Release 的 sync job 同步) | — |
**分支保护**(main,公开仓免费):10 项必须检查(Go build/vet/test、Race、
cross×5、GUI、C gates、Docs)+ `strict`(必须与 main 同步)+ 禁 force push。
⇒ **feature 合入 main 前必须过流水线**(PR 的 checks 全绿才能 merge)。
**gitcode 的角色**:只读镜像(git push 同步)。Release 附件由 sync job 自动
补传,或手工跑 `deploy/scripts/upload_assets.py`(见 §3.4)。
## 1. CI(ci.yml)—— 每次推送到 main / release/** / PR 都跑
六个 job,**全部命令均本地实测过**(原则:不写"应该有用"的未验证步骤):
| job | 内容 | 时长 |
| --- | --- | --- |
| Go build / vet / test | `go build/vet/test ./...` + `make check-client-versions` | ~12m |
| Race detector | `go test -race`(core + waiter) | ~8m |
| Cross-compile ×5 | waiter/initconfig/mock-server 五平台(CGO=0) | ~5m |
| GUI (node) | `cd cmd/gui && npm test`(纯 Node,零依赖,秒级) | <1m |
| C infrastructure gates | `make check-csrc`(告警/ABI/ASan/跨架构) | ~6m |
| Docs build | mkdocs.yml 可解析性检查 | <1m |
**明确不进 CI**(依赖真机/密钥/内网,跑了只会变 flaky 噪音):
`deploy-*.sh`、waiter 真机(192.168.2.x)、`npm run test-live`(需真
Electron+Xvfb+真后端)、`scripts/kernel-stress/*`、需 `DEEPSEEK_API_KEY` 的
真实 LLM 测试(自带 t.Skip)。
### 已知边界(不是缺陷)
- **只有 waiter/initconfig/mock-server 能纯交叉编译**。homed/memgc/
homed-kb-migrate 依赖 cgo(gojieba/onnx),必须原生构建(见 Makefile)。
- **`cmd/gui` 是纯 Electron 目录**(0 个 .go、无 go.mod)。`go test ./...`
不会包含它(Go 的 `./...` 语义);只有显式 `go test ./cmd/gui` 才报
"no Go files"。前端测试走 `npm test`。
- **CGO 必须开**(CI 里 `CGO_ENABLED: 1`):gojieba 需要。
## 2. 发布(release.yml)—— release/** 推送即发版
### 2.1 标准发版流程(以 1.3.14 为例)
```bash
# 1. 在发布线的 worktree(干净)里 bump 版本
git worktree add /tmp/rel-v1.3.14 release/v1.3.x
cd /tmp/rel-v1.3.14
sed -i 's/Version = "1.3.13"/Version = "1.3.14"/' internal/meta/meta.go
git commit -am "release: 1.3.14"
git push origin release/v1.3.x
# 2. 流水线自动:读版本 → go build/test 门 → 下载资产 → 打包
# → 打 tag v1.3.14 → 建 GitHub release → 传附件 → 回读校验
# → (配了 GITCODE_TOKEN 时)同步 gitcode
# 3. 在 GitHub Actions 页看进度;全绿即发版完成
```
**版本号唯一事实源是 `internal/meta/meta.go` 的 Version**(SDK 仓是
`meta/meta.go`)。改它并推送 = 发版指令。
### 2.2 幂等闸门:tag 已存在 ⇒ 整轮跳过
Prepare 先查 `refs/tags/v<version>` 是否存在。已存在(比如改文档的推送、
cherry-pick 维护提交)则 Build/Publish/Sync 全部 skipped。**不会重复发版**。
### 2.3 发版门:go build 硬门 + go test 可显式跳过
- `go build ./...` —— **硬门**,不可跳过。
- `go test ./...` —— 默认跑。历史维护线若存在**既存红测试**(如
release/v1.3.x 的 deepsearch 测试,main 上已修),在**改动 meta 的那个
提交**的正文里写 `[skip-release-tests]` 即可跳过:
```bash
git commit -am "release: 1.3.14 [skip-release-tests]
(正文里写明跳过理由——为什么本线的红是既存的、与本次发版无关)"
```
跳过时 CI 输出 `::warning`,决定记录在发版 commit 里可审计。
★ 标记查在**改动 meta 的提交**上而非 HEAD:发版提交后常还会跟几个
维护提交,只看 HEAD 会让标记被顶掉、静默失效。
### 2.4 产物与验证
产物(v1.3.13 实测):
```text
homeagent-client_1.3.13_amd64.deb 83.5MB 客户端(不含模型)
homeagent-server_1.3.13_amd64.deb 730.1MB 服务端(含向量模型)
homeagent-full_1.3.13_amd64.deb 808.3MB 全量(模型+ORT+GUI)
homeagent_1.3.13_linux_amd64.tar.gz 839.2MB 内核+CLI+GUI 打包
SHA256SUMS 全量校验和
```
流水线内置的验证(不通过即不发版):
- deb 元数据(Package/Version/Architecture)逐包核对;
- full/server 包内**必须真的含** `TextEncoder.onnx` 与 `libonnxruntime.so`
(防"默认启用但装完不能用"的假包);
- `sha256sum -c SHA256SUMS`(产物先平铺再验 —— 脚本把校验和写成平铺名);
- 发布后**回读**:把附件下载回来再验一遍校验和。
### 2.5 构建资产(ci-assets-v1 release)
server/full 的打包需要 719MB 模型 + 24MB ONNX Runtime。它们**内容不随版本
变**,故作为 `ci-assets-v1` release 的附件一次性托管,每次发版由流水线下载
并 `sha256sum -c` 校验后使用:
```text
chinese-clip-vit-b16-onnx.tar 718.8MB Chinese-CLIP ViT-B/16 ONNX
onnxruntime-linux-amd64-1.28.0.tar 23.5MB ORT 1.28.0 + LICENSE + TPN
SHA256SUMS
```
模型或 ORT 升级时:改好本地 `CHINESECLIP_BUNDLE_DIR` /
`ONNXRUNTIME_ASSET_DIR` 指向的目录 → 重新打包 → 在 GitHub 上新建
`ci-assets-v2` release 并上传 → 同步更新 release.yml 里的 `ASSETS_TAG`。
## 3. 运维要点(都踩过坑)
### 3.1 workflow 文件必须存在于目标分支
GitHub 用**被推送 commit 里的** `.github/workflows/*.yml` 决定是否触发。
给旧发布线补流水线时,要把 workflow 文件本身 commit 到那条分支
(实测:只在 main 有时,推 release/** 什么都不触发)。
### 3.2 runner 上没有 electron 缓存
打包脚本从两处找 Electron:`~/.cache/electron` 的 zip,或
`cmd/gui/node_modules/electron/dist`。全新 runner 两处都没有 ⇒ release.yml
在打包前 `npm ci`(electron 已在 package-lock 锁定;**不能**
`npm install --production`,electron 是 devDependency 会被跳过)。
### 3.3 管道里的 `grep -q` 会因 SIGPIPE 误杀检测
`dpkg-deb -c <800M 包> | grep -q <目标>`:grep -q 匹配即退出、关闭读端 ⇒
tar 写 stdout 收到 EPIPE ⇒ pipefail 判失败。**包越大越易触发**
(80M 的 client 没事、800M 的 full 炸)。检测存在性一律
`grep <pat> >/dev/null`。
### 3.4 gh 在非 git 目录要显式 `--repo`
`gh release download` 在 `/tmp/back` 这类非 git 目录里会报
"not a git repository"(gh 从 cwd 的 git 上下文推断仓库)。必须
`gh release download "$TAG" --repo "$GITHUB_REPOSITORY"`。
### 3.5 gitcode 上传的路径语义
`upload_assets.py` 拼路径是 `os.path.join(ASSET_DIR, name)`:
要 `cd` 进资产目录、`ASSET_DIR=.`、传**裸文件名**。SDK 仓的产物多数无
扩展名,必须显式列名(自动扫描按后缀识别,会静默一个都不传)。
### 3.6 gitcode 凭据(`GITCODE_TOKEN`)
CI 的 sync job 需要仓库 secret `GITCODE_TOKEN`;**未配置时该 job 显式跳过**
(不阻断 GitHub 侧发布)。
**2026-09-29 已配置**:两仓(`JianFeeeee/HomeAgent`、`JianFeeeee/homeagentsdk`)
均已设同名 secret,取值自 `~/.git-credentials` 里那条 `https://JianFeeeee:<token>@gitcode.com`。
配置方式(经 stdin 传入,避免 token 出现在进程列表):
```bash
printf '%s' "$TOKEN" | gh secret set GITCODE_TOKEN --repo JianFeeeee/HomeAgent
printf '%s' "$TOKEN" | gh secret set GITCODE_TOKEN --repo JianFeeeee/homeagentsdk
```
验收:sync job 只在**新版本**发版时运行(`prepare.outputs.exists == 'false'`),
历史 tag 触发不了,所以无法用旧版本实跑。等价验证三道:
```bash
# ① secret 存在
gh secret list --repo JianFeeeee/HomeAgent | grep GITCODE_TOKEN
# ② token 有效
curl -s "https://gitcode.com/api/v5/user?access_token=$TOKEN"
# ③ job 用的 private-token 头可读 release(两仓都测)
curl -s -H "private-token: $TOKEN" \
"https://gitcode.com/api/v5/repos/JianFeeeee/HomeAgent/releases/tags/v1.3.13"
```
★ **token 是宽范围的个人令牌**(可读 92 仓/48 私有、有写权限),而 CI 只需要这两个仓。
更稳的做法是去 gitcode 建一枚**仅限这两仓**的令牌再替换 —— 这样 CI 泄漏时
影响面不扩到其他仓。当前未做(按用户 2026-09-29 的决定)。
手工补发的完整流程(下载 GitHub 产物 → 建 release 条目 → 上传):
```bash
# token 放 ~/.git-credentials(https://JianFeeeee:<token>@gitcode.com)
ASSET_DIR=<产物目录> GITCODE_REPO=JianFeeeee/HomeAgent \
python3 deploy/scripts/upload_assets.py <tag> <token>
# release 条目必须先存在(脚本向 releases/<tag>/upload_url 取 OBS 签名 URL)
```
## 4. SDK 仓(third_party/homeagent-sdk)
与主仓同构,差异:
| | 主仓 | SDK |
| --- | --- | --- |
| 版本源 | `internal/meta/meta.go` | `meta/meta.go` |
| 产物 | 3 deb + 1 tar.gz(2.4GB) | `hmapdev_*` 5 平台 + SHA256SUMS(~140MB) |
| CGO | 必须(gojieba) | 不需要(CGO_ENABLED=0 纯交叉) |
| 测试 | `go test ./...` | **两处**:根模块 + `tools/hmapdev`(独立 module,根的 ./... 不含它) |
| 门 | 同一机制 | 同一机制(`[skip-release-tests]`、幂等闸门) |
发版:改 `meta/meta.go` 的 Version 推 `release/vX.Y.x`。SDK 版本随核心的
中版本走、patch 恒为 `.0`(见 git-branching.md §七.1)。
## 5. 通知
Actions 失败会给仓库 owner 发邮件(GitHub 默认)。若嫌吵:
github.com/settings/notifications → Actions 关闭,或仓库页 Watch → Custom
取消 Actions。注意失败邮件也可能是"验证步骤自身 bug"的假警报 ——
先看是哪个 job/step 红了再判断(对照 §3 的坑)。
## 6. 开发环境的诊断噪音(`cmd/gui [setup failed]`)
### 6.1 症状
每轮改完文件,pi-lens 的回合末摘要里会冒一条:
```text
FAIL ./cmd/gui [setup failed]
```
它看着像仓库里有测试红了,**实际是工具缺陷**。真实状态:
```bash
go test ./... # 退出码 0,43 个包全过、0 FAIL
find cmd/gui -name '*.go' | wc -l # 0 —— 该目录根本没有 Go 代码
go test ./cmd/gui # “no Go files in .../cmd/gui”
cd cmd/gui && npm test # 这才是它的测试(node 的 .test.mjs),通过
```
### 6.2 根因(pi-lens 的两个缺陷叠加)
1. **runner 按仓库根选**,不按被跑的文件选。本仓根有 `go.mod` ⇒ 选中 go runner;
而 `cmd/gui/*.test.mjs` 命中通用测试命名(`detectFileRole` 与 runner 无关)
⇒ 对 `cmd/gui` 生成 `go test -run . ./cmd/gui` ⇒ 必失败。
2. **failed-first 把误报变成永久**:失败项进 `failedTestsByRunner`(进程内 Map),
此后**每次**编辑都优先重跑它(与当前编辑的文件无关);而该条目只在测试
**通过**时才移除 ⇒ 对这条永远失败的命令,永不自愈。
日志里的形态(`/root/.pi-lens/sessionstart.log`):
```text
turn_end: README.md → test go cmd/gui/sse-backoff.test.mjs (failed-first)
```
注意触发者是 `README.md` —— 目标是**与本次编辑无关**的陈旧失败项。
### 6.3 为什么不能用项目级配置关掉
`.pi-lens.json` 是**项目级**,只认一小排键
(`ignore` / `rules` / `maxProjectFiles` / `reviewGraph` / `trivy` + 三个改动开关)。
`tests` 是**全局级**键,写进项目文件会被忽略并告警:
```text
"tests" is a global-only pi-lens setting and is not honored in a project .pi-lens.json
```
而全局关掉(`~/.pi-lens/config.json` 的 `{"tests":{"enabled":false}}`)
会一起关掉**所有项目**的回合末测试反馈 —— 为一个仓库的误报付全局代价,不值。
另:`ignore` 也挡不住,因为它只作用于扫描,不参与测试目标选择(`failed-first`
的回退分支根本不看候选文件)。
### 6.4 修法:本机补丁(已打)
补丁位置:`~/.pi/agent/npm/node_modules/pi-lens/dist/index.js`。
在 `getTestRunTarget` 返回目标前加一道校验:
> 该 runner 是否**真能跑**这个目标?只有 go 做实质检查 ——
> 目标所在目录要有至少一个 `.go` 文件。不能跑就返回 null,
> 并顺手把这条不可运行的记录从 `failed-first` 集合里移除。
原方法体改名为 `selectTestRunTargetRaw`,外面套一层校验(`runTestFileAsync`
只有这一个调用点,所以这里是唯一收口)。补丁全文已用 `node --check` 验语法,
用 `/usr/bin/diff` 核对为**纯新增、零删除**。
**立即生效(不必重启会话)**:在会话里执行内置命令 **`/reload`**
(重载扩展且不重启 `pi-web-sessiond`);不手动重载则在**下个会话**自然生效。
**验证**:改一个仓库文件但先不提交,等回合结束,然后
`grep 'turn_end: .*→ test' /root/.pi-lens/sessionstart.log | tail -3`
—— 应不再出现 `test go cmd/gui/...`;而编辑一个真 Go 测试文件时仍应正常触发。
**会被覆盖**:pi-lens 升级/重装后补丁消失,误报会回来(不影响仓库,只是噪音)。
备份在同目录 `index.js.orig-*`,回退就是拷回去:
```bash
cd ~/.pi/agent/npm/node_modules/pi-lens/dist
cp -a index.js.orig-<时间戳> index.js # 然后 /reload
```
> 上游缺陷:runner 选择应先确认目标文件属于该语言(或至少确认目录内有该语言的源文件)。

View File

@ -38,6 +38,49 @@
--- ---
### 0.1 ★★ 本机 `diff` 是坏的 —— 所有比对判据的硬前置
**结论:本机一律用 `/usr/bin/diff` 或 `cmp`,不要用裸 `diff`。**
PATH 首位是 `/opt/huawei/harmonyos/ohos-sdk/linux/toolchains/diff`,
它**对任何输入都返回 0 且无输出**。用两个必然不同的小文件验证:
```bash
printf 'A\n' > /tmp/d1; printf 'B\n' > /tmp/d2
diff -q /tmp/d1 /tmp/d2; echo $? # ⇒ 0(错!应为 1)
/usr/bin/diff -q /tmp/d1 /tmp/d2; echo $? # ⇒ 1(对)
cmp -s /tmp/d1 /tmp/d2; echo $? # ⇒ 1(cmp 未被污染)
```
为什么这比“偶尔报错”危险得多:它让**部署判据变成假绿灯**。
2026-09-29 实测:`deploy-sdk-site.sh --check` 对着两份**确实不同**的
`index.html`(本地 `f8898cfc…` / 线上 `cc615cfa…`)报「✓ 逐字节一致」,
于是永远判定「无需部署」——站点改完再也不会被更新,而且看不出来。
已在 `547d28d` 修掉(钉绝对路径 + 启动自检)。
★ 判据自检的通用做法:
```bash
# 启动时用两个必然不同的输入验证判据真的能报差异;不通过就退出,
# 而不是继续拿一个坏判据去做决定。
if "$DIFF" -q <(printf 'A\n') <(printf 'B\n') >/dev/null 2>&1; then
echo '判据自检失败:这个 diff 认为两份不同内容“无差异”' >&2; exit 1
fi
```
同类信号(任一出现就立刻怀疑判据本身):
| 信号 | 含义 |
| --- | --- |
| `diff` 说无差异,但 `wc -c` / `stat -c %s` 说大小不同 | 判据坏了 |
| 部署脚本报「一致」但线上内容明显是旧的 | 判据坏了 |
| 测了两个**必然不同**的样本却报「相同」 | 判据坏了 |
远端(106 / 30)的 `diff` **是健康的**(`/usr/bin/diff`),不受影响;
但嵌在 `ssh "…"` 里的命令要分清楚是本地还是远端执行。
---
## 1. 部署 homed(本机) ## 1. 部署 homed(本机)
### 1.1 硬前置:必须 onnxruntime 构建 ### 1.1 硬前置:必须 onnxruntime 构建
@ -118,6 +161,7 @@ curl -s -H "X-API-Key: $K" http://127.0.0.1:8080/api/v1/status | grep -oE '"comm
(86811464),更掩盖了这个问题。 (86811464),更掩盖了这个问题。
⇒ 部署前的判据顺序: ⇒ 部署前的判据顺序:
1. `/api/v1/status` 的 `commit` —— 线上在跑什么 1. `/api/v1/status` 的 `commit` —— 线上在跑什么
2. `git log <commit>..HEAD` —— 差哪些提交 2. `git log <commit>..HEAD` —— 差哪些提交
3. 那些提交里**有无运行时改动**(`internal/`、`cmd/`)—— 只有它才需要部署 3. 那些提交里**有无运行时改动**(`internal/`、`cmd/`)—— 只有它才需要部署

View File

@ -1,211 +0,0 @@
#!/usr/bin/env bash
# 站点漂移巡检:有差异才提醒,绝不自动部署。
#
# 老大定的档位是「有差异就提醒」而不是「自动推」——文档站发错了是公开可见的,
# 宁可等人点一下。所以这个脚本**只读不动**:不构建、不上传、不碰线上任何文件。
#
# 判三类信号:
# 1. 线上漂移:.106 上的站 ≠ 本机产物/site 源(逐字节 md5 清单比对)
# 2. 源码漂移:git HEAD 比产物新 ⇒ 提交了但没重新构建部署
# 3. 探活失败:.106 或 NapCat 挂了(这类比文档漂移紧急,必须报)
#
# 为什么不直接调 deploy-sdk-site.sh --check 再 grep 输出:那脚本的输出是给人看的
# 彩色文本,拿来当机器判断依据太脆(改个文案就失效)。md5 清单逻辑很短,
# 这里复刻一份,注释指向 deploy-sdk-site.sh 保持同步。
#
# 去重:差异持续存在时,每轮都发会把老大刷屏。所以按「差异指纹」去重——
# 差异内容变了才发新的;差异没了发一条「已恢复」;一直没变就闭嘴。
#
# 定时:cron 7,37 * * * *(错开整点,避开 acme 等已排满 :00 的任务)
set -uo pipefail
SDK_DIR=/home/program/TrueAgent/third_party/homeagent-sdk
BUILD_DIR="$SDK_DIR/site_build"
INTRO_SRC=/home/program/TrueAgent/site
HOST=192.168.2.106
SSH="ssh -n -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no admin@$HOST"
SITES=/vol1/docker/navi-data/sites
NAPCAT=${NAPCAT_URL:-http://192.168.2.106:25570}
QQ_USER=${QQ_USER:-2198972886}
STATE_DIR=/home/program/TrueAgent/.drift-watch
STATE_FILE="$STATE_DIR/state"
LOCK="$STATE_DIR/lock"
LOG=/var/log/site-drift-watch.log
# 差异清单最多列这么行,剩下的折叠计数(QQ 消息不宜过长)
MAX_LINES=12
mkdir -p "$STATE_DIR"
exec 9>"$LOCK"
flock -n 9 || { echo "[$(date '+%F %T')] 上一轮还在跑,跳过" >>"$LOG"; exit 0; }
say() { printf '\033[1m%s\033[0m\n' "$*"; }
info() { printf ' %s\n' "$*"; }
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >>"$LOG"; }
# ── 通知(直连 NapCat,cron 不需要唤醒 agent)────────────────────────
#
# 为什么不走 agent:叫醒 agent 发消息要过 LLM 一趟,慢且烧 token;
# NapCat 的 HTTP 接口是现成的,直发即时且零成本。
notify() {
local msg="$1" payload resp code
payload=$(jq -nc --arg m "$msg" --argjson u "$QQ_USER" '{user_id:$u, message:$m}')
resp=$(curl -s -m 20 -X POST "$NAPCAT/send_private_msg" \
-H 'Content-Type: application/json' -d "$payload" 2>/dev/null)
code=$(printf '%s' "$resp" | jq -r '.retcode // -1' 2>/dev/null)
if [ "$code" = "0" ]; then
info "已通知老大($(printf '%s' "$resp" | jq -r '.data.message_id // "?"'))"
return 0
fi
info "✗ 通知发送失败:$(printf '%s' "$resp" | head -c 200)"
log "notify failed: $resp"
return 1
}
# ── md5 清单(与 deploy-sdk-site.sh 保持一致)─────────────────────────
norm_md5() { sed 's| \./| |'; }
list_md5() { # list_md5 <远端目录> <排除项...>
local dir="$1"; shift
local excl=()
local e
for e in "$@"; do excl+=(-not -path "$e"); done
$SSH "cd $dir && sudo -n find . -type f -print0 | sort -z | sudo -n xargs -0 md5sum" 2>/dev/null | norm_md5
}
sdk_diff() {
diff <( cd "$BUILD_DIR" && find . -type f -print0 | sort -z | xargs -0 md5sum | norm_md5 ) \
<( list_md5 "$SITES/sdk" ) 2>/dev/null
}
intro_diff() {
# README.md 是仓库说明不是站点资源,故意排除(见 deploy-sdk-site.sh 注释)
diff <( cd "$INTRO_SRC" && find index.html assets -type f -print0 | sort -z | xargs -0 md5sum | norm_md5 ) \
<( list_md5 "$SITES/introduce" -not -path './README.md' ) 2>/dev/null
}
# ── 探活 ────────────────────────────────────────────────────────────
probe() {
local url="$1"
curl -s -m 15 -X POST "$url" 2>/dev/null | jq -r '.retcode // -1' 2>/dev/null
}
# ── 主流程 ──────────────────────────────────────────────────────────
FORCE=0; DRY=0
for a in "$@"; do
case "$a" in
--test) FORCE=1 ;;
--dry-run) DRY=1 ;;
esac
done
say "巡检 $HOST 的两个站(只读,不部署)"
# 1) NapCat 探活(通知通路本身也得是活的,否则有差异也通知不到)
nap=off
if [ "$(probe "$NAPCAT/get_status")" = "0" ]; then
nap=on; info "✓ NapCat 通知通路在线"
else
info "✗ NapCat 不可达($NAPCAT)——有差异也发不出通知"
fi
# 2) 线上漂移
reasons=(); details=""
# .106 整体可达性:连不上是最高优先级故障
if ! $SSH "true" 2>/dev/null; then
info "✗ $HOST SSH 不可达(admin@,BatchMode)"
reasons+=("🖥 $HOST SSH 连不上,站点状态未知")
sdk_diff() { echo "__UNREACHABLE__"; }
fi
sdkout=$(sdk_diff)
if [ "$sdkout" = "__UNREACHABLE__" ]; then
:
elif [ -n "$sdkout" ]; then
n=$(printf '%s\n' "$sdkout" | wc -l)
reasons+=("📄 sdk 站与本地产物有差异($n 行)")
details+="【sdk 站】"$'\n'"$(printf '%s\n' "$sdkout" | head -$MAX_LINES)"
[ "$n" -gt "$MAX_LINES" ] && details+=$'\n'"…还有 $((n - MAX_LINES)) 行"
details+=$'\n\n'
info "✗ sdk 有差异:$n 行"
else
info "✓ sdk 站与本地产物逐字节一致"
fi
introout=$(intro_diff)
if [ "$introout" = "__UNREACHABLE__" ]; then
:
elif [ -n "$introout" ]; then
n=$(printf '%s\n' "$introout" | wc -l)
reasons+=("🎨 introduce 站与本机源有差异($n 行)")
details+="【introduce 站】"$'\n'"$(printf '%s\n' "$introout" | head -$MAX_LINES)"
[ "$n" -gt "$MAX_LINES" ] && details+=$'\n'"…还有 $((n - MAX_LINES)) 行"
details+=$'\n\n'
info "✗ introduce 有差异:$n 行"
else
info "✓ introduce 站与本机源逐字节一致"
fi
# 3) 源码漂移:提交了但没重新构建
head_ts=$( cd "$SDK_DIR" && git log -1 --format=%ct 2>/dev/null )
build_ts=0
[ -d "$BUILD_DIR" ] && build_ts=$(stat -c %Y "$BUILD_DIR/index.html" 2>/dev/null || echo 0)
if [ -n "$head_ts" ] && [ "$build_ts" -gt 0 ] && [ "$head_ts" -gt "$build_ts" ]; then
hs=$(cd "$SDK_DIR" && git log -1 --format='%h %s' 2>/dev/null)
reasons+=("🧱 sdk 源码已提交但产物没重新构建($hs)")
details+="【源码漂移】"$'\n'"HEAD $head_ts > 产物 $build_ts"$'\n'"$(cd "$SDK_DIR" && git log -1 --format='%h %ad %s' --date=iso 2>/dev/null)"$'\n\n'
info "✗ 源码比产物新,需重新构建"
else
info "✓ 源码与产物无漂移"
fi
# ── 去重与通知 ──────────────────────────────────────────────────────
fingerprint=$(printf '%s\n' "${reasons[@]:-}" "$details" | md5sum | cut -c1-16)
prev=""; prev_state=""
[ -f "$STATE_FILE" ] && { prev_state=$(jq -r '.state // ""' "$STATE_FILE" 2>/dev/null); prev=$(jq -r '.fingerprint // ""' "$STATE_FILE" 2>/dev/null); }
if [ "$FORCE" = "1" ]; then
info "--test:强制通知一次"
[ "$DRY" = "1" ] && { say "(dry-run,不实际发送)"; }
[ "$DRY" != "1" ] && notify "【站点巡检 · 测试】通知通路自检(当前差异条数:${#reasons[@]})——看到这条说明巡检能及时叫你。"
exit 0
fi
if [ "${#reasons[@]}" -eq 0 ]; then
if [ "$prev_state" = "alert" ]; then
msg="✅【站点巡检】已恢复正常:sdk 与 introduce 两站都和本机逐字节一致,源码也无漂移。刚才的差异自己好了(或你部署过了)。"
[ "$DRY" = "1" ] || notify "$msg"
log "recovered (was alert), notified=$([ "$DRY" = "1" ] && echo dry || echo yes)"
else
info "无差异,按约定保持安静(不打扰老大)"
log "clean"
fi
printf '{"state":"clean","fingerprint":"%s","ts":"%s"}\n' "$fingerprint" "$(date -Is)" >"$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
exit 0
fi
# 有差异
if [ "$fingerprint" = "$prev" ] && [ "$prev_state" = "alert" ]; then
info "差异与上次通知完全相同(指纹 $fingerprint),不重复打扰"
log "alert-unchanged fp=$fingerprint"
exit 0
fi
say "有差异,准备通知"
msg="🚨【站点巡检】发现 ${#reasons[@]} 处漂移(只提醒,未自动部署)"$'\n\n'
for r in "${reasons[@]}"; do msg+="· $r"$'\n'; done
msg+=$'\n'"$details"
msg+=$'\n'"—— 要部署就跑:/home/program/TrueAgent/deploy-sdk-site.sh"
msg+=$'\n'" 只看差异:/home/program/TrueAgent/deploy-sdk-site.sh --check"
msg+=$'\n'" 回滚:--rollback <备份目录名>"
if [ "$DRY" = "1" ]; then
say "(dry-run,以下内容本该发出)"; printf '%s\n' "$msg"
else
notify "$msg" && printf '{"state":"alert","fingerprint":"%s","ts":"%s"}\n' "$fingerprint" "$(date -Is)" >"$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
fi
log "alert fp=$fingerprint reasons=${#reasons[@]}"

View File

@ -2018,7 +2018,7 @@
</p> </p>
<div class="cta rise" style="--d:280ms"> <div class="cta rise" style="--d:280ms">
<a class="btn btn-primary" href="#quickstart">快速上手 →</a> <a class="btn btn-primary" href="#quickstart">快速上手 →</a>
<a class="btn btn-ghost" href="https://gitcode.com/JianFeeeee/HomeAgent">查看源码</a> <a class="btn btn-ghost" href="https://github.com/JianFeeeee/HomeAgent">查看源码</a>
</div> </div>
</div> </div>
</div> </div>
@ -2058,6 +2058,12 @@
<h3>记忆不会越用越肿</h3> <h3>记忆不会越用越肿</h3>
<p>上下文按相关性取舍,最近 10 条恒受保护。低分下沉成文档,冷文档再蒸馏成三元组。有效工作区间上限 60 万 token。</p> <p>上下文按相关性取舍,最近 10 条恒受保护。低分下沉成文档,冷文档再蒸馏成三元组。有效工作区间上限 60 万 token。</p>
</article> </article>
<article class="duty rise-zoom">
<span class="duty-k">并行</span>
<h3>同轮的工具,一起跑</h3>
<p>一轮里的多个 tool_call 默认并发,但<strong>安全性前置</strong>:工具必须在自己的定义里声明并发安全,未声明的一律串行。批内只要有一个不安全,<strong>整批降级</strong>——不做收益不抵风险的「部分并发」。同通道输出仍严格保序。</p>
</article>
</div> </div>
</div> </div>
</section> </section>
@ -2807,6 +2813,9 @@ echo <span class="s">"你好,记住我喜欢喝咖啡"</span> | ./build/waiter
<button class="badge" type="button" data-plugin="vanblog" style="--i:17">vanblog</button> <button class="badge" type="button" data-plugin="vanblog" style="--i:17">vanblog</button>
<button class="badge" type="button" data-plugin="vikunja" style="--i:18">vikunja</button> <button class="badge" type="button" data-plugin="vikunja" style="--i:18">vikunja</button>
<button class="badge" type="button" data-plugin="weather" style="--i:19">weather</button> <button class="badge" type="button" data-plugin="weather" style="--i:19">weather</button>
<button class="badge" type="button" data-plugin="ai_image" style="--i:20">ai_image</button>
<button class="badge" type="button" data-plugin="files" style="--i:21">files</button>
<button class="badge" type="button" data-plugin="luademo" style="--i:22">luademo</button>
<button class="badge muted" type="button" id="expandAll">展开全部</button> <button class="badge muted" type="button" id="expandAll">展开全部</button>
</div> </div>
<div class="pdetail" id="pdetail"> <div class="pdetail" id="pdetail">
@ -2814,37 +2823,37 @@ echo <span class="s">"你好,记住我喜欢喝咖啡"</span> | ./build/waiter
<div class="pcard" id="p-a2a"> <div class="pcard" id="p-a2a">
<div class="pcard-head"><code>a2a</code><span class="ver">v1.3.1</span></div> <div class="pcard-head"><code>a2a</code><span class="ver">v1.3.1</span></div>
<p>Agent 间通信。对外起 HTTP 服务暴露 /agent-card 与 /a2a(JSON-RPC)供他人发现与调用;对内提供工具去问别的 Agent。把监听地址设为空即只出站、不开端口。</p> <p>Agent 间通信。对外起 HTTP 服务暴露 /agent-card 与 /a2a(JSON-RPC)供他人发现与调用;对内提供工具去问别的 Agent。把监听地址设为空即只出站、不开端口。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/a2a" target="_blank" rel="noopener noreferrer" title="SDK example/a2a"><span class="psrc-r">SDK</span><span class="psrc-p">example/a2a</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/a2a" target="_blank" rel="noopener noreferrer" title="SDK example/a2a"><span class="psrc-r">SDK</span><span class="psrc-p">example/a2a</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-acp"> <div class="pcard" id="p-acp">
<div class="pcard-head"><code>acp</code><span class="ver">v1.2.1</span></div> <div class="pcard-head"><code>acp</code><span class="ver">v1.2.1</span></div>
<p>Agent Client Protocol 通信插件:充当 ACP 服务端接受其他 Agent 的任务请求,同时提供客户端工具向远程 ACP Agent(如 opencode)发起会话并读取回复</p> <p>Agent Client Protocol 通信插件:充当 ACP 服务端接受其他 Agent 的任务请求,同时提供客户端工具向远程 ACP Agent(如 opencode)发起会话并读取回复</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/acp" target="_blank" rel="noopener noreferrer" title="SDK example/acp"><span class="psrc-r">SDK</span><span class="psrc-p">example/acp</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/acp" target="_blank" rel="noopener noreferrer" title="SDK example/acp"><span class="psrc-r">SDK</span><span class="psrc-p">example/acp</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-bili"> <div class="pcard" id="p-bili">
<div class="pcard-head"><code>bili</code><span class="ver">v1.2.0</span></div> <div class="pcard-head"><code>bili</code><span class="ver">v1.2.0</span></div>
<p>B站视频下载。需系统装 yt-dlp。不指定格式时先返回可用清晰度列表,选定后再下载;输出目录有路径校验,拒绝写成系统目录。</p> <p>B站视频下载。需系统装 yt-dlp。不指定格式时先返回可用清晰度列表,选定后再下载;输出目录有路径校验,拒绝写成系统目录。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/bili" target="_blank" rel="noopener noreferrer" title="SDK example/bili"><span class="psrc-r">SDK</span><span class="psrc-p">example/bili</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/bili" target="_blank" rel="noopener noreferrer" title="SDK example/bili"><span class="psrc-r">SDK</span><span class="psrc-p">example/bili</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-browser"> <div class="pcard" id="p-browser">
<div class="pcard-head"><code>browser</code><span class="ver">v2.4.1</span></div> <div class="pcard-head"><code>browser</code><span class="ver">v2.4.1</span></div>
<p>统一浏览器插件:搜索、HTTP抓取(quick)、无头渲染(normal)、交互式浏览器(interactive/CDP)</p> <p>统一浏览器插件:搜索、HTTP抓取(quick)、无头渲染(normal)、交互式浏览器(interactive/CDP)</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/browser" target="_blank" rel="noopener noreferrer" title="SDK example/browser"><span class="psrc-r">SDK</span><span class="psrc-p">example/browser</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/browser" target="_blank" rel="noopener noreferrer" title="SDK example/browser"><span class="psrc-r">SDK</span><span class="psrc-p">example/browser</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-calendar"> <div class="pcard" id="p-calendar">
<div class="pcard-head"><code>calendar</code><span class="ver">v1.1.0</span></div> <div class="pcard-head"><code>calendar</code><span class="ver">v1.1.0</span></div>
<p>日历事件管理,支持提醒和重复事件</p> <p>日历事件管理,支持提醒和重复事件</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/calendar" target="_blank" rel="noopener noreferrer" title="SDK example/calendar"><span class="psrc-r">SDK</span><span class="psrc-p">example/calendar</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/calendar" target="_blank" rel="noopener noreferrer" title="SDK example/calendar"><span class="psrc-r">SDK</span><span class="psrc-p">example/calendar</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-deepsearch"> <div class="pcard" id="p-deepsearch">
<div class="pcard-head"><code>deepsearch</code><span class="ver">v1.1.2</span></div> <div class="pcard-head"><code>deepsearch</code><span class="ver">v1.1.2</span></div>
<p>为 agent 提供真正的联网信息检索:本地 SearXNG 聚合多引擎(返回标题/URL/摘要/时间),支持新闻、时间范围、指定引擎;并提供网页正文抽取与「搜索+读前K篇」的深检索</p> <p>为 agent 提供真正的联网信息检索:本地 SearXNG 聚合多引擎(返回标题/URL/摘要/时间),支持新闻、时间范围、指定引擎;并提供网页正文抽取与「搜索+读前K篇」的深检索</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/deepsearch" target="_blank" rel="noopener noreferrer" title="SDK example/deepsearch"><span class="psrc-r">SDK</span><span class="psrc-p">example/deepsearch</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/deepsearch" target="_blank" rel="noopener noreferrer" title="SDK example/deepsearch"><span class="psrc-r">SDK</span><span class="psrc-p">example/deepsearch</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-editdoc"> <div class="pcard" id="p-editdoc">
<div class="pcard-head"><code>editdoc</code><span class="ver">v2.0.0</span></div> <div class="pcard-head"><code>editdoc</code><span class="ver">v2.0.0</span></div>
<p>全能办公插件:新建/读取/编辑/转换 docx·xlsx·pptx·md·csv·txt(基于 python-docx / openpyxl / python-pptx / soffice / pandoc)</p> <p>全能办公插件:新建/读取/编辑/转换 docx·xlsx·pptx·md·csv·txt(基于 python-docx / openpyxl / python-pptx / soffice / pandoc)</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/editdoc" target="_blank" rel="noopener noreferrer" title="SDK example/editdoc"><span class="psrc-r">SDK</span><span class="psrc-p">example/editdoc</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/editdoc" target="_blank" rel="noopener noreferrer" title="SDK example/editdoc"><span class="psrc-r">SDK</span><span class="psrc-p">example/editdoc</span><span class="psrc-out">↗</span></a>
<p class="psrc-note">公开仓中的 example 仍是 v1.0.0;v2.0.0 全能版源码尚未公开。</p> <p class="psrc-note">公开仓中的 example 仍是 v1.0.0;v2.0.0 全能版源码尚未公开。</p>
</div> </div>
<div class="pcard" id="p-homeagent-mail-bridge"> <div class="pcard" id="p-homeagent-mail-bridge">
@ -2860,57 +2869,72 @@ echo <span class="s">"你好,记住我喜欢喝咖啡"</span> | ./build/waiter
<div class="pcard" id="p-memo"> <div class="pcard" id="p-memo">
<div class="pcard-head"><code>memo</code><span class="ver">v1.1.0</span></div> <div class="pcard-head"><code>memo</code><span class="ver">v1.1.0</span></div>
<p>待办与备忘录刻意分两类:待办每 5 分钟检查未完成数、有则注入提醒;备忘录纯记事不提醒。混成一类要么天天弹、要么被忘掉。</p> <p>待办与备忘录刻意分两类:待办每 5 分钟检查未完成数、有则注入提醒;备忘录纯记事不提醒。混成一类要么天天弹、要么被忘掉。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/memo" target="_blank" rel="noopener noreferrer" title="SDK example/memo"><span class="psrc-r">SDK</span><span class="psrc-p">example/memo</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/memo" target="_blank" rel="noopener noreferrer" title="SDK example/memo"><span class="psrc-r">SDK</span><span class="psrc-p">example/memo</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-music"> <div class="pcard" id="p-music">
<div class="pcard-head"><code>music</code><span class="ver">v0.1.0</span></div> <div class="pcard-head"><code>music</code><span class="ver">v0.1.0</span></div>
<p>搜歌与查歌词(网易云公开接口)。只读:不下载音频、不落文件。两段式用法 —— 先搜到歌曲 ID,再按 ID 取词。</p> <p>搜歌与查歌词(网易云公开接口)。只读:不下载音频、不落文件。两段式用法 —— 先搜到歌曲 ID,再按 ID 取词。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/music" target="_blank" rel="noopener noreferrer" title="SDK example/music"><span class="psrc-r">SDK</span><span class="psrc-p">example/music</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/music" target="_blank" rel="noopener noreferrer" title="SDK example/music"><span class="psrc-r">SDK</span><span class="psrc-p">example/music</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-ocr"> <div class="pcard" id="p-ocr">
<div class="pcard-head"><code>ocr</code><span class="ver">v1.0.0</span></div> <div class="pcard-head"><code>ocr</code><span class="ver">v1.0.0</span></div>
<p>图片文字识别。需系统装 tesseract(中文另需 chi_sim 语言包)。支持 URL 或 base64 输入,临时文件识别后自动清理。</p> <p>图片文字识别。需系统装 tesseract(中文另需 chi_sim 语言包)。支持 URL 或 base64 输入,临时文件识别后自动清理。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/ocr" target="_blank" rel="noopener noreferrer" title="SDK example/ocr"><span class="psrc-r">SDK</span><span class="psrc-p">example/ocr</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/ocr" target="_blank" rel="noopener noreferrer" title="SDK example/ocr"><span class="psrc-r">SDK</span><span class="psrc-p">example/ocr</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-plugindev"> <div class="pcard" id="p-plugindev">
<div class="pcard-head"><code>plugindev</code><span class="ver">v1.0.0</span></div> <div class="pcard-head"><code>plugindev</code><span class="ver">v1.0.0</span></div>
<p>把 hmapdev 工具链封装成 Agent 可调用的工具:脚手架生成插件工程、构建打包 .hmap、管理 SDK 版本。配合 plugin_install 即可让 Agent 自己做完「新建插件 → 构建 → 安装」全流程。</p> <p>把 hmapdev 工具链封装成 Agent 可调用的工具:脚手架生成插件工程、构建打包 .hmap、管理 SDK 版本。配合 plugin_install 即可让 Agent 自己做完「新建插件 → 构建 → 安装」全流程。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/plugindev" target="_blank" rel="noopener noreferrer" title="SDK example/plugindev"><span class="psrc-r">SDK</span><span class="psrc-p">example/plugindev</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/plugindev" target="_blank" rel="noopener noreferrer" title="SDK example/plugindev"><span class="psrc-r">SDK</span><span class="psrc-p">example/plugindev</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-qq"> <div class="pcard" id="p-qq">
<div class="pcard-head"><code>qq</code><span class="ver">v1.4.1</span></div> <div class="pcard-head"><code>qq</code><span class="ver">v1.4.1</span></div>
<p>经 NapCat 桥接 QQ:20 个工具覆盖消息、群/好友、文件传输。带权限模型 —— 身份绑在「帧」上而非插件全局(中断抢占恢复后不会丢身份),多来源合并取权限交集,敏感工具按前缀一律拒绝非所有者。</p> <p>经 NapCat 桥接 QQ:20 个工具覆盖消息、群/好友、文件传输。带权限模型 —— 身份绑在「帧」上而非插件全局(中断抢占恢复后不会丢身份),多来源合并取权限交集,敏感工具按前缀一律拒绝非所有者。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/qq" target="_blank" rel="noopener noreferrer" title="SDK example/qq"><span class="psrc-r">SDK</span><span class="psrc-p">example/qq</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/qq" target="_blank" rel="noopener noreferrer" title="SDK example/qq"><span class="psrc-r">SDK</span><span class="psrc-p">example/qq</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-recoverydiag"> <div class="pcard" id="p-recoverydiag">
<div class="pcard-head"><code>recoverydiag</code><span class="ver">v0.2.0</span></div> <div class="pcard-head"><code>recoverydiag</code><span class="ver">v0.2.0</span></div>
<p>快速检查/崩溃取证工具集:diag_triage(退出码/信号/存活粗分)、diag_db(config.db 完整性 + LLM 源解析校验)、diag_log_scan(日志签名命中)、diag_delta(last-good 快照 vs 现状 diff)、diag_loc(正交综合定位)。全部返回结论而非原文,确定性、不消耗 LLM token,供 guard / failback 恢复决策使用。</p> <p>快速检查/崩溃取证工具集:diag_triage(退出码/信号/存活粗分)、diag_db(config.db 完整性 + LLM 源解析校验)、diag_log_scan(日志签名命中)、diag_delta(last-good 快照 vs 现状 diff)、diag_loc(正交综合定位)。全部返回结论而非原文,确定性、不消耗 LLM token,供 guard / failback 恢复决策使用。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/recoverydiag" target="_blank" rel="noopener noreferrer" title="SDK example/recoverydiag"><span class="psrc-r">SDK</span><span class="psrc-p">example/recoverydiag</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/recoverydiag" target="_blank" rel="noopener noreferrer" title="SDK example/recoverydiag"><span class="psrc-r">SDK</span><span class="psrc-p">example/recoverydiag</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-rss"> <div class="pcard" id="p-rss">
<div class="pcard-head"><code>rss</code><span class="ver">v1.1.0</span></div> <div class="pcard-head"><code>rss</code><span class="ver">v1.1.0</span></div>
<p>RSS/Atom 订阅监控。按间隔轮询,发现新条目即以中断注入告知 agent;订阅时记下历史条目,所以订一个源不会把旧文章全推一遍。</p> <p>RSS/Atom 订阅监控。按间隔轮询,发现新条目即以中断注入告知 agent;订阅时记下历史条目,所以订一个源不会把旧文章全推一遍。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/rss" target="_blank" rel="noopener noreferrer" title="SDK example/rss"><span class="psrc-r">SDK</span><span class="psrc-p">example/rss</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/rss" target="_blank" rel="noopener noreferrer" title="SDK example/rss"><span class="psrc-r">SDK</span><span class="psrc-p">example/rss</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-sanitizer"> <div class="pcard" id="p-sanitizer">
<div class="pcard-head"><code>sanitizer</code><span class="ver">v0.1.0</span></div> <div class="pcard-head"><code>sanitizer</code><span class="ver">v0.1.0</span></div>
<p>在三个阶段清洗文本:坏 UTF-8 / U+FFFD / ANSI 转义(会被模型复读)与 LLM 输出里的工具调用残留。不注册工具,只挂钩子。</p> <p>在三个阶段清洗文本:坏 UTF-8 / U+FFFD / ANSI 转义(会被模型复读)与 LLM 输出里的工具调用残留。不注册工具,只挂钩子。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/sanitizer" target="_blank" rel="noopener noreferrer" title="SDK example/sanitizer"><span class="psrc-r">SDK</span><span class="psrc-p">example/sanitizer</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/sanitizer" target="_blank" rel="noopener noreferrer" title="SDK example/sanitizer"><span class="psrc-r">SDK</span><span class="psrc-p">example/sanitizer</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-vanblog"> <div class="pcard" id="p-vanblog">
<div class="pcard-head"><code>vanblog</code><span class="ver">v1.0.0</span></div> <div class="pcard-head"><code>vanblog</code><span class="ver">v1.0.0</span></div>
<p>管理 VanBlog 开源博客系统:文章的增删改查、分类标签管理、草稿发布、备份导出等</p> <p>管理 VanBlog 开源博客系统:文章的增删改查、分类标签管理、草稿发布、备份导出等</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/vanblog" target="_blank" rel="noopener noreferrer" title="SDK example/vanblog"><span class="psrc-r">SDK</span><span class="psrc-p">example/vanblog</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/vanblog" target="_blank" rel="noopener noreferrer" title="SDK example/vanblog"><span class="psrc-r">SDK</span><span class="psrc-p">example/vanblog</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-vikunja"> <div class="pcard" id="p-vikunja">
<div class="pcard-head"><code>vikunja</code><span class="ver">v1.0.1</span></div> <div class="pcard-head"><code>vikunja</code><span class="ver">v1.0.1</span></div>
<p>Vikunja 待办/任务管理:任务增删改查、项目与看板桶、标签、指派、评论、关联、附件、保存筛选器、团队与分享、通知、订阅、Webhook、时间跟踪、数据导入、实例管理;并附通用 API 直通工具兜底</p> <p>Vikunja 待办/任务管理:任务增删改查、项目与看板桶、标签、指派、评论、关联、附件、保存筛选器、团队与分享、通知、订阅、Webhook、时间跟踪、数据导入、实例管理;并附通用 API 直通工具兜底</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/vikunja" target="_blank" rel="noopener noreferrer" title="SDK example/vikunja"><span class="psrc-r">SDK</span><span class="psrc-p">example/vikunja</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/vikunja" target="_blank" rel="noopener noreferrer" title="SDK example/vikunja"><span class="psrc-r">SDK</span><span class="psrc-p">example/vikunja</span><span class="psrc-out">↗</span></a>
</div> </div>
<div class="pcard" id="p-weather"> <div class="pcard" id="p-weather">
<div class="pcard-head"><code>weather</code><span class="ver">v1.0.0</span></div> <div class="pcard-head"><code>weather</code><span class="ver">v1.0.0</span></div>
<p>查实时天气与预报(wttr.in,无需 API Key)。可设默认城市;结果标记为不进记忆计算 —— 天气是易变数据,反复写进记忆只会挤占预算。</p> <p>查实时天气与预报(wttr.in,无需 API Key)。可设默认城市;结果标记为不进记忆计算 —— 天气是易变数据,反复写进记忆只会挤占预算。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/weather" target="_blank" rel="noopener noreferrer" title="SDK example/weather"><span class="psrc-r">SDK</span><span class="psrc-p">example/weather</span><span class="psrc-out">↗</span></a> <a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/weather" target="_blank" rel="noopener noreferrer" title="SDK example/weather"><span class="psrc-r">SDK</span><span class="psrc-p">example/weather</span><span class="psrc-out">↗</span></a>
</div>
<div class="pcard" id="p-ai_image">
<div class="pcard-head"><code>ai_image</code><span class="ver">v1.3.0</span></div>
<p>文生图:按文字提示生成图片,下载到本地并返回<strong>文件路径</strong>(可直接被多模态链路读取)。</p>
<a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/ai_image" target="_blank" rel="noopener noreferrer" title="SDK example/ai_image"><span class="psrc-r">SDK</span><span class="psrc-p">example/ai_image</span><span class="psrc-out">↗</span></a>
</div>
<div class="pcard" id="p-files">
<div class="pcard-head"><code>files</code><span class="ver">v1.0.0</span></div>
<p>沙箱文件操作:读写与编辑文件,<strong>全部操作限制在沙箱目录内</strong>(读取/写入/编辑/列目录)。</p>
<a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/files" target="_blank" rel="noopener noreferrer" title="SDK example/files"><span class="psrc-r">SDK</span><span class="psrc-p">example/files</span><span class="psrc-out">↗</span></a>
</div>
<div class="pcard" id="p-luademo">
<div class="pcard-head"><code>luademo</code><span class="ver">v0.1.0</span></div>
<p>Lua 插件全功能示例:工具(no_memory / cleaner)、阶段钩子、通道、数据类 API —— 想用 Lua 写插件,从这个例子抄起。</p>
<a class="psrc" href="https://github.com/JianFeeeee/homeagentsdk/blob/main/example/luademo" target="_blank" rel="noopener noreferrer" title="SDK example/luademo"><span class="psrc-r">SDK</span><span class="psrc-p">example/luademo</span><span class="psrc-out">↗</span></a>
</div> </div>
</div> </div>
</div> </div>
@ -2986,35 +3010,35 @@ echo <span class="s">"你好,记住我喜欢喝咖啡"</span> | ./build/waiter
<div> <div>
<h4>文档</h4> <h4>文档</h4>
<ul> <ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/OVERVIEW.md">项目概览</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/OVERVIEW.md">项目概览</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/ARCHITECTURE.md">技术架构</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/ARCHITECTURE.md">技术架构</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/PLUGIN_DEV.md">插件开发</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/PLUGIN_DEV.md">插件开发</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/ADAPTER.md">Lua 适配器</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/ADAPTER.md">Lua 适配器</a></li>
</ul> </ul>
</div> </div>
<div> <div>
<h4>深入</h4> <h4>深入</h4>
<ul> <ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/docs/zh/input-scheduler-design.md">输入调度器设计</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/docs/zh/input-scheduler-design.md">输入调度器设计</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/docs/zh/resident-subagent-design.md">驻留式子 Agent</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/docs/zh/resident-subagent-design.md">驻留式子 Agent</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/docs/zh/multimodal-space.md">统一多模态空间</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/docs/zh/multimodal-space.md">统一多模态空间</a></li>
</ul> </ul>
</div> </div>
<div> <div>
<h4>项目</h4> <h4>项目</h4>
<ul> <ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent">源码仓库</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent">源码仓库</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/releases">发布下载</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/releases">发布下载</a></li>
<li><a href="https://gitcode.com/JianFeeeee/homeagent-sdk">插件 SDK</a></li> <li><a href="https://github.com/JianFeeeee/homeagentsdk">插件 SDK</a></li>
</ul> </ul>
</div> </div>
<div> <div>
<h4>状态</h4> <h4>状态</h4>
<ul> <ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/releases">最新发布:v1.3.x 线</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/releases">最新发布:v1.3.x 线</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/internal/meta/meta.go">main 在研:1.4.0</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/internal/meta/meta.go">main 在研:1.4.0</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/LICENSE">内核许可:AGPL-3.0-only</a></li> <li><a href="https://github.com/JianFeeeee/HomeAgent/blob/main/LICENSE">内核许可:AGPL-3.0-only</a></li>
<li><a href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/LICENSE">插件 SDK:MIT</a></li> <li><a href="https://github.com/JianFeeeee/homeagentsdk/blob/main/LICENSE">插件 SDK:MIT</a></li>
</ul> </ul>
</div> </div>
</div> </div>