Commit Graph

921 Commits

Author SHA1 Message Date
db88c5efe5 fix(agentcli): 关闭终端后反复注入过期状态
## 现象

关闭终端后 agent 持续收到「[终端 term_6 读取结束: read /dev/ptmx:
input/output error]」「[终端 term_6 中的进程已退出]」,而 terminal_list
显示 0 个会话——它拿不到任何证据说明这些是历史遗留,只能反复解释
"这是旧回声,不用管",每条都唤醒一轮推理。

## 根因(不是一次回声,是每 30 秒一次)

    close 工具 → session.Close() → readLoop 从 <-t.stopCh 静默 return
               → 但 **sessions 条目没删**
    → cleanupLoop 每 30s 扫到它,发现 !terminalRunning
    → 又注入一条「进程已退出」

三条退出路径(主动关闭 / 进程自己结束 / 超时)里,主动关闭那条漏了删表。

## 修法

1. readLoop 的 t.stopCh 分支补 removeSession,三条路径统一走它。
2. 新增 isClosing:所有"终端结束"通知都必须区分两种成因——
   进程自己结束(是新闻,要告诉 agent)vs 我们主动关掉(是回声,不能注入)。
   旧实现只看 r.err / terminalRunning,而主动关闭恰好让两者都成立。

## 测试基础设施修正

mockTerm.Kill() 原先是空操作,于是"主动关闭"在测试里从不产生 EIO,
判据永远绿(假绿)。改为模拟真实 PTY:Kill 后 Read 立刻返
`read /dev/ptmx: input/output error`。

判据:关闭后 session 必须立刻从表里消失、关闭后不得新增注入
(连开 20 次防 select 随机性)、进程自己结束仍必须通知(不过度修复)。
变异:关闭时不删表 / isClosing 恒 false,均被抓。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 10:10:21 +08:00
50508c83b5 fix(core): 裁剪收归基础设施,contextfull 只作为触发信号
## 问题

同一件事(上下文装不下)有三套实现、三份防护差异:

    task.go 发 LLM 前   maybeHandleContextOverflow(有恢复预算)
    task.go 上游报错     handleUpstreamContextFull(**直调内部,绕过预算**)
    resident.go 轮首     checkContextFull(只对驻留子生效)

其中 handleUpstreamContextFull 是"另起炉灶":它自己又调一遍
handleContextOverflow,绕过 f.OverflowRecover 预算——而文档 §四.1 明确
「L4 遇 L4 不能抢占,反复触发只会排队堆积」。

## 改法

裁剪成为**基础设施**,contextfull 只是「判定已被外部短路」的一种入参:

    enforceContextBudget(f, query, force)
      force=false  本地水位:积累量 > ratio × 窗口
      force=true   外部已确认超页(provider 返回 ErrContextFull)

之后的一切(恢复预算、裁剪、raise L4)完全相同。handleUpstreamContextFull
删除,只剩 isUpstreamContextFull 这个纯判定。

## 任务恢复入口补裁剪

中断任务跑完会把它的输入/输出提交进 a.context,恢复时重建出的前缀可能
**比被挂起时更大**。不在这里裁,这份更大的前缀会被原样发出去。

恢复用的是专用入口 enforceResumeContextBudget,两处语义与通用入口不同:

1. **query 取本帧最新有效上下文**(resumeQueryFrom 从 f.Msgs 尾部往回找),
   不是 f.Input。被挂起的任务可能隔了很久,f.Input 早已与当前对话脱节;
   拿旧 Input 算相关性,会裁掉"对该任务重要、但字面上不匹配旧输入"的事件。

2. **保护范围是本帧自己的现场**(len(f.Msgs)-f.PrefixLen),不是全局末尾
   N 条。被挂起任务的现场可能是一整批工具结果,若落在保护范围外被归档,
   恢复后的任务引用的内容已不在上下文里,任务自相矛盾。

为此新增 pruneForResume + contextTopKForProtected——不能复用 contextTopK,
它内部会把 topK 抬到 effectiveProtectedCount()+1(全局保护),两套下限
打架会让恢复场景「裁不动」(实测 protected=8、20 条上下文时 topK 被抬到 20)。

## 不变的部分

声明式裁剪(业务策略)语义原样保留:
  · 中断/消息注入 —— pruneOnInput,判据 payload.context_policy / ChannelDef
  · 工具内容注入   —— stepToolAfter,判据 ToolDef.ContextPolicy
两者走 memoryPass → pruneByQuery,与容量保护共用底层,但职责正交。

判据:任务恢复入口必须裁、恢复查询取本帧最新、保护本帧现场、
恢复预算对所有入口生效。变异验证:恢复不裁/保护退回全局/上游绕过预算,
三种坏法均被抓。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 10:10:21 +08:00
f5a1366770 fix(proc): arena 扩容块泄漏 + 取消成为一等状态
## 1. arena 泄漏(线上 arena 被打满的根因)

插件经 arena.alloc 申请的扩容块,块头 owner 记的是**插件的** ownerID,
而内核三处归还全写着 Free(OwnerHost, ref)。归属校验必然失败,错误又被
`_ =` 吞掉 ⇒ 每返回一次大结果就永久泄漏一块。

线上表现(2026-10-25):重任务跑几轮后 4MB arena 满,之后连 64KB 调用帧
都分不出来,所有插件工具一起报「arena: 空间不足(请求 65779 字节)」。

修法:三处改用插件自己的 ownerID,且失败必须留日志(吞掉错误等于把
下一次「空间不足」的原因提前销毁)。

## 2. 取消成为一等状态

旧实现里「调用在进行中」只表达为 pending 表里有这个键:取消时直接
delete,状态的痕迹全部消失,插件随后回的应答只能走「未知 id,丢弃」。
那是副作用的正确,不是规则的正确。

改成每次调用带显式状态机(callstate.go):

    pending ──应答──→ done
       ├──取消/超时──→ canceled ──迟到应答──→ done
       └──进程退出──→ failed

迁移表集中在 legalTransition,非法组合一律拒绝(CAS 只校验 from,
不校验 to——实测曾把 done 改回 failed)。

## 3. 取消跨进程送达

内核超时到点后只停止「等」,插件侧的 handler 还在跑,占着连接/子进程/
文件句柄,结果却已被丢弃。现在 CallContext 取消时向插件发 call.cancel
通知(fire-and-forget),插件运行时按 **RPC id** 定位并 cancel 对应调用的 ctx。

不按工具名定位:同一工具可并发多次,按名字会误伤其它实例。

插件作者零改动:handler 签名不变,取消由模板运行时接住。

## 4. 取消超时判定插件不稳定

严格区分两类超时(用户裁定):

    业务超时 = 任务跑太久     → 正常,不作为不稳定依据
    取消超时 = 喊停之后还不停 → 异常,是插件失去响应的直接证据

取消宽限(cancelGrace=5s)到期仍无应答 ⇒ ① 强制回收该调用名下的扩容块
(插件已静默退出,永远不会归还)② 计数 ③ 连续两次触发既有崩溃重启链。
插件一旦响应取消即清零;重启后也清零(判据是"连续"不是"累计")。

判据:8 条新增单测 + 全部变异验证(不回收/不上报/不清零/取消不跨进程
任一处改坏,对应用例立刻变红)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 10:10:21 +08:00
90d1289b2d Merge pull request #28 from JianFeeeee/fix/webui-gzip-race
fix(webui): streamOpenAI 改为 w 单写者模型,修 gzipResponseWriter 真数据竞争
2026-10-24 13:28:52 +08:00
2d49c23cc6 Merge remote-tracking branch 'refs/remotes/gh/main' into fix/webui-gzip-race 2026-10-24 13:25:18 +08:00
7457b052a7 fix(webui): streamOpenAI 改为 w 单写者模型,修 gzipResponseWriter 真数据竞争
根因(-race 实测,非推测):
streamOpenAI 的 handler goroutine 在收尾处直接 fmt.Fprintf(w, finish chunk)、
fmt.Fprintf(w, "data: [DONE]")、flusher.Flush(),与增量 flush goroutine
(16ms 批量合并)**并发写同一个 http.ResponseWriter**。经 gzip 中间件时,
gzipResponseWriter 的 mode / wroteHeader / buf 三个字段无锁被两方同时读写:

  Read  gzipResponseWriter.Write    gzip.go:139(读 g.mode)
        └─ streamOpenAI handler goroutine        handler_openai.go:256
  Write gzipResponseWriter.commit   gzip.go:206(写 g.mode/wroteHeader)
        └─ streamOpenAI 写 goroutine             handler_openai.go:165

修法选择:改写并发模型(单写者),不给 gzipResponseWriter 加锁。
理由两条:
1. 加锁只哄住 race detector,是治标 —— 并发写同一个 ResponseWriter 这件事
   本身就该被结构禁止,而不是靠锁逐字段兜。
2. 持锁要跨 Write / Flush 这类网络 IO,一旦客户端慢,整条 SSE 会串行卡在
   锁上(锁内做 IO)。单写者模型从结构上消灭竞争,且零锁开销。

改动:
- 新增 finishCh(chan finishItem,buf=1)+ writerExit。终止帧(finish chunk
  或超时错误 + [DONE] + Flush)也交给那个唯一的写 goroutine 发。
- 新增 sendFinish(usage, timedOut):**先投递再等 writerExit**。
  顺序不能反 —— writer goroutine 只在收到 finishCh 或 done 关闭时退出,而
  done 在 defer 里关(函数返回之后),先等后发即死锁。select 里的 <-done
  是防御路径:done 已关时 writer 可能已不在,不该再等一个永不到来的接收者。
- 超时路径与正常收尾路径都改走 sendFinish,handler goroutine 不再碰 w。
- writerExit 用 safeclose.Safe 关闭,遵守本仓 close 纪律
  (internal/safeclose/TestNoBareCloseOutsideOutsideSafeclose 禁裸 close(chan))。
- 顺带修掉一个既有逻辑缺陷:旧写法下 [DONE] 可能先于 deltaCh 里尚未发完的
  尾部 delta 到达客户端;现在先 flushPending 再发终止帧。

验证(全部 -count=1,除注明者):
- 基线(f408448 原版)TestOpenAIStreamIsActuallyStreaming + -race:
  单跑 5/5 次各 8 条 DATA RACE 且 FAIL;-count=5 时 10 / 12 / 10 条。
- 修复后同一用例 + -race:单跑 6/6 次 races=0 PASS。
- 修复后整包 go test -race ./internal/plugins/webui/:3/3 次 races=0 PASS
  (63s / 69s / 64s)。
- go build ./... 通过;go vet ./... 通过;go test ./... 58 包 ok、0 FAIL、
  0 WARNING: DATA RACE。

判据:cr-HomeAgent-6c0d1e07(stress),带变异 M1「sendFinish 改回直写 w
(并吃掉 <-writerExit,避免只靠死锁超时报红)」——实测被咬住(判据报红)。
2026-10-24 12:35:31 +08:00
873a0c3f75 Merge pull request #26 from JianFeeeee/fix/webp-embed-and-migrate-log
fix(media): webp 解码支持 + 迁移失败可观测 + 补回遗漏的 module 路径统一
2026-10-23 15:56:07 +08:00
3f834794c5 refactor: SDK module 路径统一改为 hologramagentsdk(59 文件 + go.mod)
## 为什么必须做

SDK 仓(独立仓库)已把 module 从 `github.com/JianFeeeee/homeagentsdk`
改名为 `hologramagentsdk`。主仓通过 `replace` 直接引用它的**物理目录**:

    replace github.com/JianFeeeee/homeagentsdk => ./third_party/homeagent-sdk

一旦 SDK 侧改成新 module 名,主仓立刻炸:

    go: ./third_party/homeagent-sdk@ used for two different module paths
        (github.com/JianFeeeee/hologramagentsdk and github.com/JianFeeeee/homeagentsdk)

Go **不允许**同一目录被两个 module 路径 replace。所以不能"新旧并存",
只能把主仓 59 个文件的 import 全部改到新名,并删掉旧 replace。

## 一个被证伪的前提

我最初按"third_party/homeagent-sdk 是独立仓库,不能在此仓修改其源码"
来处理,并因此判断两个仓的改动可以隔离。实际上主仓是靠 `replace` 指向
该目录的物理副本 —— **两个仓的改动不可能真正隔离**:SDK 侧改 module 名,
主仓必然跟着改 import。

这个前提应该在做任何改动之前用 `go list -m` 验证,而不是假设。

## 连带修掉两处

| 缺陷 | 性质 |
|---|---|
| `TestLuaSDKMockSingleSource` 失败 | SDK 的 `sdk/lua/sdk.lua` 变了,主仓内嵌副本未同步(跑 `scripts/sync-lua-sdk.sh` 修复) |
| `sdkModuleNames` 分散两处 | 已在 SDK 仓合并为单一数据源(见 SDK 仓 725732b) |

第一条是**副本一致性判据抓到的**,不是我自己发现的 —— 这类判据的价值在此:
两个仓必然有副本,靠人记住"改了 A 要同步 B"是做不到的。

## 验证

- `go build ./...` / `go vet ./...` 干净
- `go test ./... -count=1`:**58 包 0 失败**
- `internal/plugin`(Lua SDK 副本一致性)+ `internal/sdk` 全绿
- 主仓 `third_party/homeagent-sdk/` 与 SDK 仓 `main` 无差异
2026-10-23 15:51:02 +08:00
6327578693 fix(media): 支持 webp 解码 + 向量迁移失败必须留下原因
## 生产现状(2026-10-23 排查)

启动日志只有一行,没有任何细节:

    [media] 启动向量迁移: 4 个图像块 → fp=cd2a495cf990 dim=512 workers=4
    [media] 向量迁移完成: 成功=0 失败=4 总计=4

反查 `memory/graph.db` 才找到根因:

| mime | 块数 | fingerprint |
|---|---|---|
| image/jpeg | 153 | 当前 fp |
| image/png | 394 | 当前 fp |
| **image/webp** | **4** | **空**(从未嵌入成功) |

而 `providers/chineseclip/image.go` 只注册了 gif/jpeg/png:

    _ "image/gif"
    _ "image/jpeg"
    _ "image/png"

⇒ `image.Decode` 对 webp 返回 `image: unknown format` ⇒ 这 4 个块
**永远**拿不到向量 ⇒ 语义检索永远搜不到它们,且没有任何报错。

## 两个独立缺陷

### ① 功能缺口:webp 不可解码

加 `_ "golang.org/x/image/webp"`。这是纯 Go 实现、无 cgo,不改变二进制形态。

代价:`x/image v0.25.0` 硬性要求 `x/text >= v0.23.0`,因此 `x/text` 从
`v0.3.8` 升到 `v0.23.0`。已实测 `providers/chineseclip` 全绿(分词器用
`x/text/unicode/norm` 做 NFC 归一化,升级未改变行为)。

### ② 可观测性缺口:失败被 `continue` 吞掉

原实现:

    if err := a.reembedOneBlock(b, fp); err != nil {
        atomic.AddInt64(&failed, 1)
        continue          ← 原因就此消失
    }

现在按 `mime | error` 聚合后打印(不逐条打,上千张图会刷屏):

    [media] 向量迁移完成: 成功=0 失败=0 不支持=4 总计=4 fp=cd2a495cf990
    [media] 失败原因分类(按 mime | error 聚合):
    [media]   4 个: image/webp | chineseclip: 解码图像: image: unknown format

同时区分 `ErrModalityUnsupported`(**永久**无向量,重试无意义)与临时失败,
单独计为 `不支持=N` —— 混在"失败"里会让人以为重试就能恢复。

## 判据(4 条,全部变异自证)

| 判据 | 覆盖 | 变异 |
|---|---|---|
| `TestWebpIsDecodable` | webp 能被 `image.Decode` 识别 | 注释掉 webp 注册 ⇒ 红并指名「image.go 里缺少 ...」 |
| `TestWebpPreprocessProducesTensor` | webp 能走完整预处理(缩放+归一化),不只解码 | 同上 ⇒ 红 |
| `TestPngStillDecodable` | 注册表改动没弄坏 png | — |
| `TestReembedStaleMedia_LogsFailureReasons` | 失败原因(mime + 错误原文 + 计数)必须进日志 | 删掉打印 ⇒ 红 |
| `TestReembedStaleMedia_SeparatesUnsupportedFromFailed` | 不支持模态单独计数 | — |

webp 夹具用 ffmpeg 生成的真实 2×2 webp(44 字节)。手写字节序列构造不出
合法容器——第一版手写就失败了,这是实测教训。

## 验证

- `providers/chineseclip` / `internal/agent/core` / `internal/memory/vector` 全绿
- `go build ./...` / `go vet ./...` 干净
- 全量回归:见提交后 CI
2026-10-23 15:50:59 +08:00
a2a190f663 chore(sdk): 同步 SDK 仓副本(rebase 到 main + module 名列表修复 + 契约值改名)
主仓 third_party/homeagent-sdk/ 是 SDK 仓的物理副本,随 SDK 仓 rebase 更新:
- go.mod module 名跟随上游改名
- sdkModuleNames 合并为单一数据源(修 CI 抓到的 isSDKModule 漏识别新名)
- proxy.go 契约值 ProxyAuthAgent/ProxyAuthAgentLegacy + 注释改名
- lua sdk 副本一致性保持
2026-10-23 15:50:46 +08:00
57321b5fd4 Merge pull request #25 from JianFeeeee/ci/harden-csrc-gate
ci: 加固 C 门禁 —— 卡死不再拖满超时,缺工具不再假装通过
2026-10-23 15:49:40 +08:00
369a202d6f fix(csrc): 用 -nostdinc 剃掉宿主头,修 clang 交叉的 stdint.h 冲突
## 上一版为什么还红

装了 `libc6-dev-arm64-cross`(1.7MB 头包)后,CI 报的错变成了:

    In file included from csrc/src/ha_codec.c:23:
    In file included from /usr/lib/llvm-18/lib/clang/18/include/stdint.h:52:
    /usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found

注意路径:它找的是 **`/usr/include/stdint.h`(宿主 amd64 的)**,不是
aarch64 的那份。宿主 stdint.h 会 `#include <bits/libc-header-start.h>`,
那是 amd64 的布局,aarch64 目标下必然找不到。

**只加 `-isystem` 指向 aarch64 头不够** —— 默认搜索路径里的 `/usr/include`
仍然优先。

## 修法:`-nostdinc` 剃掉全部默认路径,再按序加回

    ARM64_INC="-nostdinc -isystem $(clang -print-resource-dir)/include"
    for d in /usr/aarch64-linux-gnu/include /usr/include/aarch64-linux-gnu; do
        [ -d "$d" ] && ARM64_INC="$ARM64_INC -isystem $d"
    done

顺序:**clang 内建头 → aarch64 目标头**。`-nostdinc` 保证宿主 `/usr/include`
不再参与,宿主头泄漏这条路径被彻底断掉。

只对 clang-target 模式生效;explicit / cross-gcc 模式用编译器自带搜索路径,
不传这些(它们本来就知道自己的 sysroot 在哪)。

## 本地五条路径实测

| 路径 | 结果 |
|---|---|
| ① clang-target(默认) | ✅ 0 告警通过 |
| ② 显式 cross-gcc | ✅ 0 告警通过 |
| ③ 显式指向不存在的编译器 | ✅ FAIL(Error 1) |
| ④ **真的写坏 C 源码** | ✅ FAIL(Error 1) |
| ⑤ 恢复后 | ✅ 通过 |
| `make check-csrc` 全门禁 | ✅ 全部通过,0 SKIP |

③④ 是防止"假通过"的回归判据:③ 验 `command -v` 校验 + PIPESTATUS,
④ 验编译器真实退出码没被 `| head` 吞掉。
2026-10-23 00:04:19 +08:00
0e5077497e fix(csrc): clang 交叉需要 aarch64 libc 头 —— 只装 1.7MB 头包,不装 10MB 编译器
## CI 实测暴露的下一层问题

上一版让 clang 做交叉编译后,安装步骤成功且**完全跳过 apt**(cmake/clang 都在
镜像里),但 `make check-csrc` 红在:

    In file included from /usr/lib/llvm-18/lib/clang/18/include/stdint.h:52:
    /usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found

真因:`clang --target=aarch64-linux-gnu` 切换的是**目标架构**,但系统头
仍按**宿主**(amd64)找 —— `bits/libc-header-start.h` 是 amd64 的布局,
aarch64 目标下不该用。本地能过是本机装了完整交叉 GCC,带来了
`/usr/aarch64-linux-gnu/include`。

## 试过但走不通的两条路(记录以免重复踩)

| 尝试 | 结果 |
|---|---|
| `-nostdinc` + clang 内建头 | ❌ clang 内建**不含 `string.h`**(那是 libc 的) |
| `-ffreestanding -nostdlibinc` | ❌ 同上,代码实际用了 `strlen`/`memcpy` |

## 采用:只装 libc 头,不装编译器

    libc6-dev-arm64-cross     1.7 MB(+ libc6-arm64-cross、linux-libc-dev-arm64-cross)
    gcc-aarch64-linux-gnu     含 cpp-13(10.8MB)+ gcc-16 等,实测下载卡死

既然只需要**头文件**(不做链接、不跑代码),就不该拉整个交叉 GCC。
装上头后 clang 就能完成 `-fsyntax-only`。

### Makefile 配合

`csrc-cross` 的 clang-target 模式现在自动探测并注入 aarch64 头路径:

    for d in /usr/aarch64-linux-gnu/include /usr/include/aarch64-linux-gnu; do
        [ -d "$d" ] && ARM64_SYSINC="$ARM64_SYSINC -isystem $d"
    done

探测改用**真实头文件**(`#include <string.h>`)而非空 main,这样
"clang 版本支持 --target" 与 "aarch64 头是否齐备" 两个条件一起验;
缺头时明确 SKIP 并说清装哪个包(`libc6-dev-arm64-cross`)。

## 本地实测

| 验证 | 结果 |
|---|---|
| 有 aarch64 头时 | ✅ clang-target 通过(0 告警) |
| `make check-csrc` 全门禁 | ✅ 全部通过,0 SKIP |
| YAML 可解析 | ✅ |
| 上一版三条失败路径(编译器不存在/真写坏/正常) | ✅ 仍然正确(PIPESTATUS 修复未回退) |
2026-10-22 23:59:37 +08:00
7fdec0670f fix(csrc): 交叉门禁改用 clang --target,去掉会卡死的 apt 下载 + 修两处假通过
## 卡死真因(这次可观测了)

上一版加了 `timeout` 硬超时后,日志终于能看出卡在哪:

    15:47:33  Fetched 12.5 MB in 1s (9972 kB/s)      ← update 很快
    15:47:38  Get:2 gcc-13-aarch64-linux-gnu-base 51.9 kB
    15:47:39  Get:3 cpp-13-aarch64-linux-gnu 10.8 MB   ← 下到这儿卡住
    15:50:37  ##[error]apt-get install 超时/失败(180s)

**根因:为了跑一道 `-fsyntax-only` 的语法门禁,去下载 10MB+ 的交叉 GCC 工具链,
在 runner 上拉不动。**

## 改法:用 clang 自带的交叉前端

这道门禁只做 `-fsyntax-only`(不链接、不运行)。clang 支持
`--target=aarch64-linux-gnu`,会**真的切到 aarch64 目标**:

    $ clang -fsyntax-only arch2.c          → error: "not aarch64 target"
    $ clang --target=aarch64-linux-gnu ... → 通过(__aarch64__ 生效)

指针宽度、endianness、C 扩展支持都按目标架构算 —— 足以暴露本门禁要抓的
那类问题。而 clang 在 ubuntu-latest 镜像里**已预装**(实测确认,只列出
`gcc-aarch64-linux-gnu` 一项需要安装)。

优先级:`CC_ARM64`(显式)> `clang --target` > `aarch64-linux-gnu-gcc` > SKIP

## 顺带修掉两处**本来就存在**的假通过

这两处不是本轮引入的,是新写的存在性校验暴露出来的:

### ① `cmd | head` 吞掉编译器退出码

    for src in ...; do
        if ! $CC ... -fsyntax-only $src 2>&1 | head -20; then ok=0; fi
    done

管道退出码是 `head` 的(恒 0)。实测:`CC_ARM64=nonexistent-xyz make csrc-cross`
竟然输出「0 告警、编译通过 ✓」—— 编译器根本没执行。

修法:改用 `${PIPESTATUS[0]}`,取编译器的真实退出码。

### ② `PIPESTATUS` 需要 bash,Make 默认是 dash

`/bin/sh` 在 Debian/Ubuntu 上是 dash,不支持 `PIPESTATUS`(报 Bad substitution)。
必须显式 `SHELL := /bin/bash`,否则上面的修法静默失效。

### 风险自证(三条路径实测)

| 场景 | 结果 |
|---|---|
| 正常(clang-target) | ✅ 0 告警、编译通过 ✓ |
| `CC_ARM64` 指向不存在的命令 | ✅ `[FAIL] CC_ARM64=nonexistent-xyz 不存在` → Error 1 |
| 真的写坏 C 源码 | ✅ 编译器报错 + `[FAIL] arm64 交叉编译失败` → Error 1 |

## CI 侧

- 安装步骤不再要求 `gcc-aarch64-linux-gnu` ⇒ **依赖已预装,直接跳过 apt-get,完全不碰网络**
- 工具链确认步骤只校验 `cmake` + `clang`
- `[SKIP]` 拦截、`timeout` 硬超时、去 `-qq` 全部保留(对付将来别的原因导致的卡死)
2026-10-22 23:55:54 +08:00
16bb794a43 ci: 二次加固 —— 上一版对 apt 卡死无效,改用 timeout 硬超时并去掉 -qq
## 上一版为什么没解决

第一版加了 `Acquire::http::Timeout=60` + `Retries=3` + step 6 分钟超时。
CI 实测结果:

    15:39:48  需要安装: gcc-aarch64-linux-gnu
    15:46:01  ##[error]The action '安装 C 工具链(已装则跳过)' has timed out after 6 minutes.

**依然卡满 6 分钟**,而且日志里**一个 apt 输出都没有** —— 因为 `-qq` 把
所有输出静默了。两个结论:

1. `Acquire::*Timeout` 只管**单个连接**,不管 apt 整体 —— 源重试、DPKG 锁
   等待、依赖计算都不受它约束,所以它拦不住这类卡死
2. `-qq` 让卡死**完全不可观测**:既看不到卡在哪个源,也看不到在下载什么。
   不可观测正是它能悄悄拖满超时的原因

(顺带证明"已装则跳过"这条是有效的:cmake/clang 都在镜像里,只列出了
`gcc-aarch64-linux-gnu` 一项要装。)

## 这一版改法

| 改动 | 理由 |
|---|---|
| 去掉 `-qq`,保留可读进度 | 下次卡死必须能看出卡在哪,否则无从诊断 |
| `timeout 120 apt-get update` | **外层硬超时**,强制结束,不依赖 apt 自身行为 |
| `timeout 180 apt-get install` | 同上;120+180=300s 配 step 5 分钟正好 |
| 失败即 `exit 1` + `::error::`,不重试整段 | 宁可红一次让人看见,不要用重试掩盖真实问题 |
| `--no-install-recommends` 保留 | 减少下载量与依赖面 |

## 本地实测

| 验证 | 结果 |
|---|---|
| YAML 可解析 | ✅ |
| `timeout 3 sleep 60` 行为 | ✅ 3 秒强制结束,退出码 124,可被 `if !` 捕获 |
| "已装则跳过"分支 | ✅ 「✅ 跳过 apt-get」 |
| 上一版缺工具 `[SKIP] ... exit 0` 风险 | ✅ 已复现并拦截(见前一提交) |

## 仍待 CI 验证

这一版的真实效果只有在 runner 上才知。若这次仍然卡满,说明 `timeout` 对
sudo+apt 组合失效(可能性低),届时改用「换源」或「预装镜像」方案。
2026-10-22 23:47:15 +08:00
1aec7688a6 ci: 加固 C 门禁 —— 卡死不再拖满超时,且缺工具不再假装通过
## 两个独立问题

### ① 卡死:20 分钟全花在 apt-get,不是门禁

实测证据:本机 `make check-csrc` 五个子门禁**各自 0~1 秒**:

    csrc-lint 1s / csrc-abi 0s / csrc-headers 0s / csrc-sanitize 0s / csrc-cross 0s

而 CI 里同一个 job 两次跑到 20 分钟 job 级超时(其中一次直接 cancelled)。
卡死点只可能在 `apt-get update` —— ubuntu-latest runner 上偶发挂住不返回,
既不失败也没进展,只能等 job timeout,而报错信息毫无指向性。

### ② 假通过:缺工具时门禁 `[SKIP] ... exit 0`

这是更危险的一个。Makefile 里每道门禁在缺依赖时:

    if ! command -v $CC_ARM64 >/dev/null 2>&1; then
        echo "  [SKIP] $CC_ARM64 不存在(未装交叉工具链)"; exit 0
    fi

本机这样合理(开发者未必装交叉工具链),但在 CI 里意味着:apt-get 悄悄失败
⇒ 五道门禁全部跳过 ⇒ **job 绿** ⇒ 让人以为 C 侧有门禁,实际一个都没跑。

**静默降级比红更危险:红会有人修,绿没人看。**

实测确认(`CC_ARM64=nonexistent-gcc-xyz make csrc-cross`):

    == C 侧交叉编译(linux/arm64)==
      [SKIP] nonexistent-gcc-xyz 不存在(未装交叉工具链)
    --- 退出码: 0 ---        ← 这就是问题

## 加固内容

| 措施 | 作用 |
|---|---|
| 逐包检查是否已安装 | runner 镜像通常自带 cmake/clang ⇒ **多数情况完全不碰网络** |
| apt-get 自带 `Acquire::*Timeout=60` + `Retries=3` | 卡住时自身失败,而不是拖到 job 级超时(后者报错无指向性) |
| 装包 step `timeout-minutes: 6` | 双保险 |
| 新增「确认工具链齐备」步骤 | 缺任一工具 ⇒ 红,并 `::error::` 说明"缺工具时门禁会 SKIP 等于假装通过" |
| 跑门禁 step `timeout-minutes: 5` | 门禁实测 0~1 秒,超出即异常(不是慢) |
| `grep '\[SKIP\]'` 二次拦截 | 覆盖将来新增门禁时漏配依赖的情况 |
| job `timeout-minutes` 20 → 15 | 已经是纯 apt-get 余量 |

## 为什么要两道 SKIP 拦截

「确认工具链齐备」+「grep [SKIP]」看似重复,其实针对不同风险:

- 前者:**当前**三道门禁的依赖齐不齐
- 后者:**将来**新增门禁时,新依赖没写进安装步骤 —— 那时前者不知道要检查它,
  只有日志里的 `[SKIP]` 能抓到

## 本地实测(逐条验证,不是只改完就推)

| 验证 | 结果 |
|---|---|
| YAML 可解析 | ✅ |
| 三件套本机齐备 | ✅ cmake / clang / aarch64-linux-gnu-gcc |
| `make check-csrc` 无 SKIP | ✅ 五门禁全实际执行 |
| 已装则跳过 apt-get 分支 | ✅ 「三件套已存在,跳过 apt-get」 |
| 部分缺失时拼装需要安装列表 | ✅ 正确识别出 `clang` |
| **变异:交叉工具链缺失** | ✅ grep 拦截生效(若不加这层,CI 会绿) |
2026-10-22 23:39:21 +08:00
409f7d63bc Merge pull request #24 from JianFeeeee/feat/outputch-multisend
feat(outputch): 一次调用发多条/多类型 + 能力校验下放插件 + 契约值改名 Agent
2026-10-22 23:23:42 +08:00
d3590192e7 fix(outputch): 内核不再替插件判断载荷语义 —— 空 payload 是合法占位
## 实测踩到的真 bug(生产环境)

QQ 插件重装后首次实测 `sticker_index` 用法,直接失败:

    output_send__qq result: 参数有误,未发送任何内容:payload 不能为空

而插件侧明明有这个放行分支:

    if payload == "" && !(rawType == "sticker" && stickerIndex > 0) {
        return nil, fmt.Errorf("payload 不能为空")
    }

分支永远走不到 —— 内核先拒了。

## 根因:两处内核越权判断

| 位置 | 旧判断 | 问题 |
|---|---|---|
| `parseOutputItems` | `strings.TrimSpace(p) == ""` ⇒ 拒绝 | 内核不知道 payload 语义。`type=sticker` + `meta.sticker_index` 要求 payload 留空占位,真实 URL 由插件从收藏表情取 |
| `sendOneOutput` | `stageCtx.FinalText == ""` ⇒ "输出被插件清空" | 原本就空的占位被判成异常 |

**正确边界**:参数**是否存在**归内核(没传 `payload` 键 ⇒ 拒绝,结构性错误内核能判);参数**值是否合法**归插件(空串 ⇒ 交插件判断并返回 error)。

这与本轮 outputch 的另一条改动是同一条原则(`type` 的能力校验下放插件):
**内核只透传与对齐,不解释插件自定义字段的含义**。

## 连带修正:`stringOrArray("")` 不再返回 nil

原来 `payload: ""` 被当成"未提供",与"没传 payload 键"混为一谈。
现在空串**保留为一个元素**:前者是模型明确给的空占位(须走到插件),
后者才是真的没提供(由 `len(payloads) == 0` 拦下)。

## 判据调整(原有判据与正确设计冲突)

`TestOutputSend_RejectsEmptyEntries` 断言"含空条目必须整 call 拒绝" —— 那是
按旧的越权设计写的,**与正确边界冲突**。改写为三条分别钉住边界:

| 判据 | 断言 |
|---|---|
| `EmptyPayloadDeferredToPlugin` | 内核不拦空条目,交插件判断(2 条都要送到) |
| `EmptyStringIsNotMissing` | `payload: ""` 是占位而非缺失,必须走到插件且原样透传空串 |
| `MissingPayloadStillRejected` | 完全没传 payload 键仍拒绝(内核能判的结构性错误) |

变异验证(把 trim 判空加回 `parseOutputItems`)⇒ 两条同时变红:
- 「内核不该拦空条目,应交插件判断,实际只发 0 条」
- 「空字符串 payload 应交给插件(sticker_index 用法依赖它),实际 0 次调用」

## 验证

- `go test ./internal/agent/core/` 全绿(含新 3 条)
- 生产实测修复前后对照(journalctl 原文):

      21:54:35  output_send__qq result: 参数有误,未发送任何内容:payload 不能为空   ← 修复前
      21:56:34  output_send__qq result: ok                                          ← 修复后

- webui / agent-core 全绿
2026-10-22 22:42:02 +08:00
55dfa959aa ci: 触发重跑 —— 上一轮 C infrastructure gates 因 runner 卡在 apt-get 超时(本地同门禁全通过) 2026-10-22 21:40:48 +08:00
b20e32d81d feat(outputch): 提示词显式声明「必须调 outputch 才送达」+ 契约值改名 Agent
## ① 提示词:只有显式调用 outputch 才会送达用户(用户明确要求)

模型最常见的错误不是不会调工具,而是**以为返回文本就等于回复了** ——
在异步通道(qq/wechat/群聊)上那等于自言自语,用户永远收不到。

改动前【输出规则】开篇是「消息不会自动发送到对话来源通道,你必须自己决定
如何回复」,语气偏描述。改为开篇总述句:

    **只有显式调用 `output_send__{通道名}` 才会把内容送达用户。**
    返回纯文本只是在这次推理里说话,用户看不到 —— 除非是下面说的同步通道。

同时补上本次内核新增能力:payload 传数组可一次发多条。

## ② 契约值改名:ProxyAuthHomeAgent="homeagent" → ProxyAuthAgent="agent"

用户指示契约值一起改。常量名与字符串值同时改,但**保留旧值读取路径**:

    ProxyAuthAgent       = "agent"      ← 新值
    ProxyAuthAgentLegacy = "homeagent"  ← 只用于兼容读取
    ProxyAuthHomeAgent   = ProxyAuthAgent  (Deprecated 别名,存量源码可编译)

为什么必须保留:`Auth` 是**写进 plugin.json 的持久化声明值**。存量配置里
写着旧值 —— 本机实测 `/home/newqqagent/plugins/huawei_smarthome/plugin.json:19`
就是 `"auth": "homeagent"`。只认新值 ⇒ 该插件加载时被判为非法声明 ⇒
反代不生效,症状是「插件好好的但页面打不开」,且无任何报错指向改名。

### 归一化是安全关键

`EffectiveProxyAuth` 把**空串与旧值都归一化为 ProxyAuthAgent**。

判断点全都写成 `auth == ProxyAuthAgent`。若不归一化,就得每点写两遍比较
—— **漏一处就是鉴权绕过**:旧值被判为「非门户鉴权」⇒ 跳过 authorizeProxy
⇒ 请求直接透传给上游。所以兼容收在一个函数里,不散落在调用点。

### 顺带修掉两处隐患

1. `webui/proxy.go` 的裸字面量 `"homeagent"`(非法行占位)改为
   `sdk.ProxyAuthAgent` —— 裸字面量在改名时会被漏掉,届时它悄悄变成
   未知值,而未知值在旧代码里等于「无鉴权」。
2. 两处鉴权判断改用 `EffectiveProxyAuth(...)`,堵住旧值绕过。

## 判据

| 判据 | 覆盖 | 变异自证 |
|---|---|---|
| `TestSystemPromptRequiresExplicitOutputCall` | 总述句必须存在、必须说明「返回文本用户看不到」、同步通道是唯一例外、payload 可传数组 | 删掉总述句 ⇒ 红 |
| `TestProxyAuthLegacyValueStillAccepted` | 旧值仍被接受 + 必须归一化为新值 | 去掉归一化分支 ⇒ 「旧值应归一化为 "agent",实际 "homeagent"」红 |

## 验证

- `go build ./...` / `go vet ./...` 干净
- webui / internal-sdk / agent-core 全绿
- SDK 仓(独立提交 acd1166):`go build` + `go test ./...` 0 失败
2026-10-22 21:17:05 +08:00
f850d754c1 feat(outputch): 一 call 多发 + 多类型 —— 内核级通用能力(非某插件专属)
用户要求:内核 outputch 是通用基础,传入 meta/payload/type(三者均为**插件自定义
字段**)。要支持一次调用发多条、多种类型消息,通过 payload 数组 + type 数组实现。

## 内核原有形态的限制

output_send__<ch> 每次只能发一条单类型:
  payload: string / type: string / meta: string
带附件或多段内容的通道只能额外注册独立 tool(如 QQ 的 qq_send_file),
而这不该是插件的职责 —— "发一条消息"本来就是输出门的语义。

## 改法

### ① 工具 schema:oneOf [string, array]

payload / type / meta 三者的 schema 改为同时接受字符串与数组。
payload 是字符串时走单条路径,**行为与改动前逐字相同**(向后兼容,用户拍板)。

### ② 解析规则(parseOutputItems)

- payload 与 type 必须等长;type 省略或单值 ⇒ 广播
- meta 允许**单值广播**(用户拍板)
- 长度不符 ⇒ **整 call 拒绝,一条都不发**
- 含空条目 ⇒ 同样整 call 拒绝(不能静默发空消息)

### ③ 能力校验下放插件(用户拍板)

删掉内核里的 `switch rawType { case text/voice/image/file: 查 caps }`:

  · type 是**插件自定义枚举**,内核不认识它。写死四个值 = 内核在理解插件语义;
  · 且插件的自定义类型(如 QQ 的 sticker)会被内核无端拒绝。

内核只保留两件它确实该管的校验:**授权闸**(IsOutputAllowed)与
**通道存在性**。type 与能力的匹配由插件在 handler 里判断并**返回 error**。

### ④ 部分失败:继续发剩余的,如实逐项汇报

用户要求"部分失败向 agent 返回失败项"。实现选择"继续发完"而不是"中止":
第 2 条图片失败不该把第 3 条文字也丢掉,否则"一次发多条"失去意义。

返回形态:
  全部成功(多条)  已通过 [qq] 通道发送 3 条
  部分失败          已通过 [qq] 通道发送 2/3 条:
                      [1] text ok
                      [2] image 失败:<插件返回的错误原文>
                      [3] text ok
                    失败的条目请单独重发。

单条成功仍返回 "ok"(与旧版完全一致,避免富回执驱动回声效应 ——
output.go 里有实测依据)。

### ⑤ 拆出 sendOneOutput

before_output / 安全回填 / after_output 这套顺序对**每一条**都必须完整走一遍。
混在批量循环里极易漏环(例如只给第一条跑 stage),所以把单条路径独立成函数,
批量只是循环调用它。

## 判据(8 条,全部有变异自证)

| 判据 | 覆盖 |
|---|---|
| SingleValueUnchanged | 单值 payload 行为与旧版逐字相同(返回 "ok") |
| ArraySendsInOrder | 数组按顺序发 N 条,type 一一对应 |
| MetaBroadcast | meta 单值广播给所有条 |
| LengthMismatchRejectsWholeCall | 长度不符 ⇒ 一条都不发 |
| PartialFailureReportsAndContinues | 第 2 条失败仍尝试第 3 条 + 如实列出失败项与插件错误原文 |
| KernelDoesNotInterpretType | 自定义 type "sticker" 原样透传,内核不拦 |
| RejectsEmptyEntries | 含空条目整 call 拒绝 |
| UnauthorizedStillBlocked | 授权闸不被批量绕过 |

变异全部被抓:
- 第 2 条失败即中止 → 「实际只调用 2 次」
- 恢复内核 type 能力校验 → 「内核不该对 type 做能力判断」
- meta 不广播 → 「meta 未广播到」

## 验证

- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- 现有 output 相关测试(output_grant / output_rules / resident_output)全绿

## 未做(下一步)

- example 插件(qq)收回 qq_send_file / qq_send_sticker,改走 outputch
- outputch 说明迁入各插件 skill(保留 _help 机制)
- 插件侧补 type 能力自校验(校验已下放,需要它们真的做)
2026-10-22 21:05:17 +08:00
881f3c34df Merge pull request #23 from JianFeeeee/feat/rename-and-prompt-convergence
refactor: 全面改名 HologramAgent + 提示词技术内容下沉 + 修 21 处乱码 + 回填数据完整性
2026-10-22 21:04:34 +08:00
f256098c9f refactor: 全面改名为 HologramAgent + 提示词技术内容下沉到 skill/工具描述 + 修 21 处乱码
## ① 全面改名 HomeAgent → HologramAgent

仓库已改名,但代码与文档里仍有大量旧名。本次按"用户能看到的优先"全量替换:

- 用户可见:登录页标题/页脚、默认人格第一句、CLI 状态输出、启动日志、
  plugin_install 的工具描述、AI 图片插件的 User-Agent
- 文档与脚本:README、assets/docs、docs/zh、deploy/*.sh、.github/workflows
- 鸿蒙工程:cmd/ohos/HomeAgent → cmd/ohos/HologramAgent(git mv)
  目录内的 module/app 描述同步;**证书路径原样保留**
  (default_HomeAgent_xxx.cer/p7b/p12 是签名材料的实际文件名,改了会找不到证书)
- 插件元数据:plugin.json 的 author

★ 必须保持原样的两处(改了会坏):
  · legacySeededPromptSignature = "你是 HomeAgent 的看板娘「小宅」…"
    —— 那是识别历史播种人格卡的**原文锚点**,改了就识别不出旧数据
  · 公开 SDK 的 ProxyAuthHomeAgent = "homeagent"
    —— 那是 SDK 契约里的值,属独立仓库,本仓不改

## ② 修 21 处乱码(U+FFFD)

这些是历史编辑留下的编码破坏,全在注释里,例如:
  「赌注重下���遍」→「赌注重下一遍」
  「「不��滤」」  →「「不过滤」」
  「**待��退场**」→「**待退场**」

★ 保留 providers/chineseclip/tokenizer.go 里的 2 处:那是**故意**用
  U+FFFD 演示"非法 UTF-8 字节被归一成 U+FFFD",不是乱码。

## ③ 提示词技术内容下沉(用户:技术性内容应该收敛到 skill 与 -help 中)

实测系统提示词 2309 字,其中两段几乎全是**模型无法据以行动的实现细节**:

  【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、batchRunnable 判据…)
  【三层记忆】   1069 字(L1/L2/L3 编号、消费式删除、向量空间、指纹…)

收敛后 **2309 → 1723 字(-25%)**,保留的全是"模型据此做什么不同的事":

  工具顺序:默认并行 / 不要依赖先后 / 同通道保序 / 写类工具可能串行
  三层记忆:时序里能看最近 / doc_query 把历史事件按原始时间戳插回时序 /
           关系用 memory_recall / 成文知识用 knowledge_search

判断口径写进判据:新加一段提示词前先问"模型会据此做什么不同的事",
答不上来就该进 skill 或工具描述。

## 判据

prompt_memory_test.go 新增 2 条:
- TestSystemPromptHasNoImplementationJargon
  黑名单:ParallelSafe / batchRunnable / 消费式 / DenseCosine / fingerprint /
  向量空间 / 批内降级 / 自激
- TestSystemPromptStaysCompact:上限 2000 字(当前 1723)

变异:把 "ParallelSafe 声明、批内降级" 加回工具顺序段 ⇒ 第一条立刻红。

## 验证

- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- make check-client-versions:客户端与内核版本一致
2026-10-22 20:49:44 +08:00
6bea130639 fix(memory): 回填事件必须照常归档 —— 纠正 NoMemory 语义被错误扩大到存储层
用户指出:doc 召回部分需要回填记忆,因为 doc 召回后原文在 doc 层已经删除了。

## 我上一轮的错误(PR #22)

把「工具输出不进记忆」与「回填事件不归档」塞进了同一个 NoMemory 标记:

    doc_query → Consume(L2 副本删除)→ 回填 L1(标记 NoMemory)
              → L1 裁掉它 → Prune 跳过归档(错误实现)
              → 三层都不在 ⇒ 内容永久丢失

Consume 已经把 L2 副本删掉,**L1 是唯一副本**。裁掉时不归档 = 丢数据。
这不是自激防护,这是把数据丢掉当成防护。

## 正确区分(两件事不能共用一个标记)

| 对象 | 进计算层 | 进归档 | 理由 |
|---|---|---|---|
| doc_query 的**工具返回值**(正文摘录) | ❌ | ❌ | 它是事件的摘录副本,再向量化 = 重复注入 ⇒ 自激 |
| **回填的事件本身**(原文) | ❌ | ✅ **必须** | L2 副本已删,L1 是唯一副本;不归档即丢失 |

## 修法

ContextEvent.NoMemory 语义定为「**不参与计算,照常归档**」,
与 ToolDef.NoMemory 一致:只影响计算层,不影响存储层。

自激的入口在"重新计算向量"那一侧(textForVector 返回空),
不在"归档"这一侧 —— 删掉 Prune 里的跳过逻辑。

## 判据(改写了那条断言错误行为的判据)

TestSelfExcite_RestoredEventsNotReArchived 现在断言三件事:
1. 回填事件带 NoMemory 标记且未被向量化(不参与计算)
2. 前提成立:L2 里确实已无该事件(Consume 删掉了)
3. ★ 被裁掉后必须能落回 L2(否则内容永久丢失)

变异验证:
- Prune 重新跳过 NoMemory ⇒ 红,报「三层都不在,内容永久丢失」
- restoreDocEvents 不置 NoMemory ⇒ 红(会重复加权)

## 验证

- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
2026-10-22 20:37:34 +08:00
41921c0b6d Merge pull request #22 from JianFeeeee/fix/doc-query-self-excitation
fix(memory): 掐断 doc_query 的自激震荡 —— 工具输出与回填事件都不进记忆
2026-10-22 20:30:51 +08:00
71e2b83a76 fix(memory): 掐断 doc_query 的自激震荡 —— 工具输出与回填事件都不进记忆
用户指出(2026-10-22):doc_query 的结果走 NoMemory 路径,否则会对记忆块
产生自激震荡。核实后确认**两个入口都开着**。

## 回路

    doc_query
      ├─ ① 工具返回值含被召回事件的正文片段 → 作为 ToolResults 进 context
      │     → 参与向量化/关键词/蒸馏 → 同一内容被**重新注入**记忆
      ├─ ② 回填事件本身(带原 Source、完整 Blocks)进 L1
      │     → Prune 因相关而保留它们(挤掉真正的新事件)
      │     → 下一轮 Prune 又把它们归档回 L2
      └─ ⇒ 下次 doc_query 再召回同一批:块在 L1↔L2 之间反复搬迁

## ① 工具输出:doc_query 未声明 NoMemory

基础设施早就有(textForVector 里按 ToolDef.NoMemory 跳过工具输出),
但 doc_query 是**内置工具**,它的声明无处安放:
StageHost.ToolDef 只覆盖插件工具,内置工具由 executeToolCallInner 的
switch 分派,不经过注册。

修法:
- internal/sdk/tool.go:BuiltinToolDef 加 NoMemory 字段(与 ToolDef.NoMemory
  同义,两条路必须能声明同一件事)
- internal/agent/core/tooldefs.go:declareNoMemoryTool + noMemoryOf,
  由 init() 里**显式名单**登记 doc_query
- internal/agent/core/context.go:textForVector 同时查插件 ToolDef 与
  内置声明表(原来只查前者)

★ 为什么在 init 显式登记、不靠 toolDefWith 构造时登记:
  工具表是按需构造的(受 `if a.docStore != nil` 之类条件控制),
  而 textForVector 可能在构造之前被调用 ⇒ 声明会"时有时无"。
  实测踩到:构造前查 noMemoryOf("doc_query") 得到 false。

★ declareParallelTool 顺带改为**按字段合并**(原来整体覆盖):
  同一工具可能同时声明 parallel 与 noMemory,覆盖会让后登记的那项
  静默丢失 —— 声明丢一个是最难察觉的一类问题。

## ② 回填事件:加 ContextEvent.NoMemory

ContextEvent 新增 NoMemory 字段(此前只有 ToolDef/ChannelDef 有)。
restoreDocEvents 回填时置位(含存量降级路径),于是:

- textForVector 直接返回 "" ⇒ 不向量化、不参与关键词/蒸馏
- PruneWithProtected 归档前过滤掉它们 ⇒ 不再产生新的 L2 副本

语义与 ToolDef.NoMemory 一致:**原文仍在上下文可读**,只是不进计算、
不再产生新记忆。这是「回填是为了让模型读到原文」的本意。

## 判据(3 条,钉住两个入口 + 一个后果)

selfexcite_test.go:
- DocQueryDeclaresNoMemory:声明可查 + 工具输出不进 textForVector
- RestoredEventsNotReArchived:回填事件带 NoMemory 标记 + Prune 后
  L2 里不出现含回填内容的新文档(真跑 Prune 验证回路)
- RestoredEventsDontCrowdOutNewOnes:回填事件不被向量化(否则会
  参与相关性打分、挤掉新事件)

变异全部被抓住:
- Prune 不过滤 NoMemory → 「回填事件被 Prune 又归档回了 L2」
- restoreDocEvents 不置 NoMemory → 「没有 NoMemory 标记」+ 回路成立
- init 不登记 doc_query → 「未声明 NoMemory」

## 验证

- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- 公开 SDK(third_party/homeagent-sdk)未改动 —— 改的是 internal/sdk
2026-10-22 20:16:57 +08:00
9c668ceeb1 Merge pull request #21 from JianFeeeee/feature/memory-usability
feat(memory): doc 层去掉 jieba 摘要 + doc_query 返回真实内容 + 三层记忆提示词
2026-10-22 19:51:54 +08:00
e2cb6e70e0 feat(memory): doc 层去掉 jieba 摘要 + doc_query 返回真实内容 + 三层记忆提示词
上一轮(memory-blockflow-repair-plan)修好了 L1↔L2 结构,但功能不可用。
用生产数据实测:三个互不相关查询分数全挤在 0.75~0.79,区分度≈0,
命中内容与查询无关。本次修四层。

## ① 向量输入:Summary + Content → 事件正文

denseFor 原用 `VectorizeDense(doc.Summary + " " + doc.Content)`,而 Summary
由 summarizeEntries → memory.ExtractKeywords → TokenizeContentWords → jieba.Tag()
生成。同一来源的摘要逐字同质:

    来自 1 个来源的 1 条对话 (src0) 涉及: 用户, 提问
    来自 1 个来源的 1 条对话 (src1) 涉及: 用户, 提问

真实 chineseclip 实测(4 篇主题完全不同的短正文):两两余弦 0.85~0.95。
这与设计矛盾 —— ARCHITECTURE 写的是「统一多模态空间(主)」,而 Summary
是主路径的输入,却由最末一级的词法分词产出。

改为 documentTextForVector(doc):按事件顺序拼原文(块里的正文)。
存量无 Events 的文档退回 Content。

★ 媒体不单独生成摘要:媒体块与文本块在同一事件内
  (Doc.Events[i].Blocks),事件正文进入向量即覆盖其语义。
  为媒体单独写描述正是 CAS 时代「描述式索引」的错误,v1.2.0 已拆除。
  「按图搜图」是视觉路(RecallBlocks)的职责,已有独立通路。

## ② doc 层移除 jieba 语义生成

- summarizeEntries:jieba 词袋 → 取首条事件正文首行(真实内容,可读)
- 删除 extractTags / extractEntities(纯 jieba 词袋)
- 删除 Doc.Tags / Doc.Entities(前者唯一消费点是 doc_query 返回的
  「标签: map, content」噪声行;后者零消费点)
- doc_commit 删除 tags 参数(无处可存,留着会骗模型)

★ TF-IDF 回退路径**保留**(Store.tfidfEmb/tfidfIdx/ensureTFIDF/Reindex):
  它是「稠密空间没加载」时的兜底,按 ARCHITECTURE「统一多模态空间(主)
  → 词嵌入 → TF-IDF(回退)」必须保留。但它不能是主路径 —— 一旦稠密空间
  可用,jieba 就不参与。

## ③ doc_query 返回真实内容

旧返回:「已加载 3 篇文档记忆: #1(来自 1 个来源的 1 条对话 (agent) 涉及: ok)」
——全 jieba 词袋,模型无法判断有没有用(这是它几乎不调用 doc_query 的直接原因)。

新返回:
    已召回 3 篇历史记忆,原文已按各自**原始时间戳**插回对话时序(不是当前对话):

    [1] 2026-06-07 08:09 · webui · 2 条事件 · 含 1 份媒体
        配色方案怎么定?
        结论:紫蓝红三色带按六比三比一…

    共回填 N 条事件。要引用其中的细节,直接看对话时序里对应时间点即可。

每条给出「时间 · 来源 · 事件数 · 媒体数」+ 真实正文前几行。

## ④ 提示词:agent 终于能理解三层记忆

- doc_query 描述:一句话 → 覆盖「L2 是什么 / 何时用 / 调用后原文回填到时序 /
  与 memory_recall 分工 / 消费式副作用」
- doc_commit 描述:说清「适合写整段过程结论,不适合写单个事实」
- 系统提示词新增【三层记忆】段(内核运行时块):L1 对话时序 / L2 历史记忆 /
  L3 图记忆各自是什么、怎么取、如何选择
- 预填充文案(crossModalMarkdown):统计描述 → 时间+来源+正文开头,
  并在块尾明确引导「要完整原文用 doc_query」

## ⑤ 向量算法版本(否则修复对存量无效)

实测踩到的坑:改完 denseFor 后,1055 篇存量文档分数纹丝不动(0.78/0.79/0.76,
区分度 0.003)—— 因为 DenseFP 只记「哪个向量空间」,不记「用什么文本算的」,
指纹不变 ⇒ 全部命中跳过条件 ⇒ 永不重算。

修法:DenseFP = 空间指纹 + "|" + docVectorAlgoVersion(v2)。
算法一变,存量自动重算一次。

## 判据(每条都有变异自证)

document/retrieval_quality_test.go(6 条):
- Top1HitsCorrectDoc / DistinctQueriesHitDistinctDocs / ScoreGapIsSignificant
- VectorInputIsBodyNotSummary(正文哨兵必须进向量)
- SummaryNoiseMustNotPolluteVector(主题不同的短正文,两两余弦 <0.80)
- AlgoVersionChangeForcesRecompute(换算法必须触发存量重算)

core/recalldoc_test.go(+2):返回值必须含时间/来源/真实正文/时序说明
core/prompt_memory_test.go(5 条):工具描述五项覆盖、doc_commit 范围、
  三层记忆段落、tags 参数已删、预填充文案含内容与 doc_query 引导

变异全部被抓住(截断落库 / Summary+Content / 去掉算法版本 / 旧预填充文案 /
一句话工具描述 / 删三层记忆段)。

## 验证

- go build ./... 通过;go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- go test -race ./internal/memory/document/ ./internal/agent/core/:通过
- 真实数据(chineseclip 512 维):
  · 旧格式存量(1055 篇,无 Events):4/5 → 仍是旧分数,等自然冲刷
  · 新格式文档(走 ContextToDoc):4/5 top1 命中正确,
    区分度最高 0.18(旧实现 0.003)
2026-10-22 19:49:25 +08:00
b1f02a22b6 fix(memory): 向量输入改用事件正文 —— 摘要是 jieba 词袋,把全库拉成同一方向
## 实测缺陷(真实 chineseclip,4 篇主题完全不同的短正文)

    [1][3] 0.9478    [2][3] 0.9427
    [1][2] 0.9316    [0][3] 0.8614

两两余弦 0.85~0.95,几乎同向。查询分数 0.70~0.78 挤在一起,区分度 0.08。

根因:denseFor 用 `doc.Summary + " " + doc.Content`,而 Summary 由
summarizeEntries → memory.ExtractKeywords → TokenizeContentWords → jieba.Tag()
生成,同一来源的摘要**逐字同质**:

    来自 1 个来源的 1 条对话 (src0) 涉及: 用户, 提问
    来自 1 个来源的 1 条对话 (src1) 涉及: 用户, 提问

这与设计直接矛盾:ARCHITECTURE L131/L174 写的向量层级是
「统一多模态空间(主)→ StaticEmbedder → TF-IDF(回退)」,
而 Summary 是**主路径的输入**,却由**最末一级的词法分词**产出。

## 修法

denseFor 改用 documentTextForVector(doc):按事件顺序拼原文(来自块里的正文)。
存量无 Events 的文档退回 Content(渲染视图,但比 Summary 更接近内容)。

★ 绝不含 Summary —— 它是词袋,会把全库拉向同一方向。

## 媒体为什么不单独生成摘要

媒体块与文本块**在同一事件内**(Doc.Events[i].Blocks),事件正文进入向量
即覆盖其语义。为媒体单独生成描述正是 CAS 时代「描述式索引」的错误
(描述是二手信息),v1.2.0 已整体拆除,不重走。

「按图搜图」是视觉路(RecallBlocks 读块自己的向量)的职责,已有独立通路。

## 判据

- TestRetrieval_SummaryNoiseMustNotPolluteVector:主题不同的短正文文档,
  两两余弦必须 <0.80。修复前 0.82~0.86。
- TestRetrieval_VectorInputIsBodyNotSummary:正文里的唯一哨兵必须能检索到。

变异:denseFor 改回 Summary+Content ⇒ 前者立刻红(实测 0.8225~0.8573)。
2026-10-22 19:33:50 +08:00
1dcd3ced03 test(memory): 先写检索质量判据 —— 当前红,量化「检索不可用」
上一轮(memory-blockflow-repair-plan)写了 16 条结构判据、6 个变异全绿,
然后用生产数据实测发现检索**完全不可用**:

    查询 "doc 层召回是怎么设计的"  → 0.7834  内容无关
    查询 "hmap 插件安装验签"       → 0.7922  内容无关
    查询 "QQ 头像"                → 0.7601  内容无关

三个互不相关的查询分数全挤在 0.75~0.79,区分度≈0。

★ 教训:结构判据不能替代质量判据。
  「字段非空」「条数正确」「不 panic」在检索完全失效时依然全绿。

## 本文件

5 条判据,其中 2 条当前**红**(这就是要修的缺陷):

- Top1HitsCorrectDoc / DistinctQueriesHitDistinctDocs / ScoreGapIsSignificant
  → 用 fakeDenseSpace(按主题词投影),验证排序与区分度骨架
- **VectorInputIsBodyNotSummary**(红)→ 正文哨兵必须进向量
- **SummaryNoiseMustNotPolluteVector**(红)→ 主题不同的文档不得接近同向

## 第 5 条的实测依据(真实 chineseclip,4 篇短正文)

    [1][3] 0.9478    [2][3] 0.9427
    [1][2] 0.9316    [0][3] 0.8614

四篇主题完全不同的文档,两两余弦 0.85~0.95 —— 几乎同向。
根因:denseFor 用 doc.Summary + doc.Content,而 Summary 由 jieba 词袋生成,
同一来源的摘要逐字同质:

    来自 1 个来源的 1 条对话 (src0) 涉及: 用户, 提问
    来自 1 个来源的 1 条对话 (src1) 涉及: 用户, 提问

判据用 bigramSpace(字符 bigram 归一化直方图)模拟该行为,
不依赖模型文件,CI 可跑;阈值 0.80,实测缺陷态 0.82~0.86。

★ 正文必须**短**:正文越长 Summary 噪声占比越小、缺陷越隐蔽 ——
  这正是生产上只有短文档受害的原因。
2026-10-22 19:32:59 +08:00
37bece97cb fix(packaging): tar 包漏拷 depgraph.bin —— 装完 homed 一启动就死
内核启动时比对「磁盘上的启动依赖图 digest」与「二进制内嵌 digest」,
用于拦住「图文件完好但描述的是旧代码」这类静默顺序错(Makefile build 段注释)。

build_tar 只拷了二进制,没拷 $BUILD_DIR/depgraph.bin ⇒ 装 tar 包的机器
homed 读不到图,直接拒绝启动(退出码 1)。

修法:与 homed 同目录拷 depgraph.bin(/usr/bin/depgraph.bin);
缺图时**显式报错并 return 1**,而不是打出一个注定起不来的包 ——
静默产出坏包比打包失败更难查。
2026-10-22 19:30:05 +08:00
47c95efb80 feat(memory): L2 与 L1 同构 —— 事件列表承载原文,正文内联在块里
块化重构(memory-restructure-plan)换了底层数据结构,但 L1↔L2 的流动没跟着改,
导致这条链出现结构性失效。本次按设计原文修复。

## 断点(生产实测)

**① L2 只存渲染视图,事件本体丢失**

ContextToDoc 把每条事件拍平成一行:truncate(Response,100)、truncate(工具输出,200)。
生产库 1000 篇归档文档实测:
  - response 段恰好 103 字符(=100+"..."):311/2094 = 15.1%
  - 工具输出段恰好 203 字符:7593/13261
  - ToolsUsed 完全不存
⇒ 原文在落库那一刻就不存在了,召回再全也拿不回来。

**② L2 里没有文本块,与 L1 不同构**

L2 块分布 image 437 / audio 7 / text 0 —— 文本全靠 Content 这个视图。

**③ 召回造合成事件,时间戳用归档时刻**

doc_query 用 d.CreatedAt 造一条 cold_storage 事件、正文取 Content 再截 2000 字、
且完全不回填 Blocks。块自带 created_at(捕获时刻),实测与 doc.created_at
差近两小时,却没人用。

**④ 块在 Consume 时两层同时消失**

Consume 从内存与磁盘同时删除文档,而块的字节只存在于那个 .json 里。
生产 L2 有 444 个块,其中 396 个不在 L3(约 248MB)—— 一旦被命中即永久丢失。

## 修法(按设计,不是按我的推断)

设计前提(写进 ARCHITECTURE 与计划文档):
  · 记忆块是唯一物理载体,层是持有关系
  · L1 与 L2 结构同构,唯一区别是「是否直接投喂给模型」
  · 正文(含多媒体正文)内联在块里;事件只是块的容器
  · 任何一层都不压缩(视图不是记忆)
  · 内核不得硬编码任何插件行为

1. Doc 增加 Events []DocEvent(与 ContextEvent 同构),成为文档主体
2. 正文各自成块:Input 一块、Response 一块(BlockText,Text 即正文)
3. restoreDocEvents 逐条按原始时间戳迁回 L1,块随事件迁回
4. Content 保留为渲染视图(contentFromEvents 生成),不再是原文载体
5. 蒸馏 docToTriples 改用 documentOriginalText:从块读原文,不再用视图
6. 删掉 ARCHITECTURE 里「过滤 agentcli/terminal 来源」——那是内核硬编码
   插件名,改由插件经 NoMemory/Cleaner 声明

## 顺带修掉的真实缺陷

simpleHash 是 O(n²) 且哈希串长度正比于内容:
    for _, c := range s { h += fmt.Sprintf("%x", c) }
压力测试实测 1.25MB 正文归档耗时 **270 秒**;生产实测 content_hash
是 content 的 2.2 倍(52072 字 → 114491 字)。
换成 sha256 前 16 字节:**270s → 0.52s(520 倍)**,定长 32 hex。

## 存量

按拍板不迁移,等系统自然冲刷。降级路径:无 Events 的旧文档仍按旧行为召回
(整篇渲成一条事件),不伪造成「有原始事件」。生产副本实测 1104 篇旧文档
全部正常加载。

## 判据(16 条,全部有变异自证)

document/blockflow_test.go(11 条):正文完整性、文本块粒度、逐条保留、
视图定位、幂等、存量兼容、原文可用性、hash 线性、3 条压力
core/recalldoc_test.go(5 条):逐条回填、原始时间戳、块迁回、
回填不截断、存量降级、批量压力

变异全部被抓:
  · 落库 truncate(100)        → ArchivePreservesFullText(实测 len=303/原 10215)
  · Input/Response 合成一块    → InputAndResponseAreSeparateTextBlocks(实际 1)
  · 召回用 d.CreatedAt         → RestoresEachEventAtOriginalTimestamp(回填 1/期望 3)
  · 召回不回填 Blocks          → MigratesBlocksBackToContext
  · 召回再截 2000 字           → TextIsNotTruncatedOnTheWayBack(2003/原 18020)
  · 恢复 O(n²) hash            → HashIsLinearAndFixedLength(超时 >300s)

## 验证

- go build ./... 通过
- go test ./... -count=1:58 包 0 失败
- go test -race ./internal/memory/document/ ./internal/agent/core/:通过
- 生产数据副本实际启动:1104 篇文档加载正常、0 崩溃、干净退出
- 副本 e2e:新归档 2 事件 / 4 文本块、正文 3207 字完整回读、时间戳精确

## 不改

- 公开 SDK 契约(TextEvent / DocMemoryAPI / InsertWithMedia 已等价)
- 存量文档(等自然冲刷)
- 渲染层截断常数(独立议题,避免与本次修复互相掩盖)
2026-10-22 11:09:13 +08:00
9faae921ba Merge pull request #20 from JianFeeeee/refactor/rename-batch7-sdk-docs
docs(rename-batch7): SDK 仓名引用统一为 hologramagentsdk(module 路径按发布 tag 保留旧名)
2026-10-22 07:29:22 +08:00
8040586daa docs(rename-batch7): SDK 仓名引用统一为 hologramagentsdk,module 路径按发布 tag 保持旧名
SDK 仓已在 GitHub 改名为 JianFeeeee/hologramagentsdk(旧名 homeagentsdk
301 跳转,仓 id 与提交未变)。本批把仓内**指向该仓的引用**跟上:

- 仓库链接 / 克隆地址 / 示例目录链接:homeagentsdk → hologramagentsdk
  (README zh+en、PLUGIN_DEV zh+en、OVERVIEW zh+en、site/index.html)
- package-linux.sh 的 fallback clone URL 同步新名(仅 URL 与注释)
- ci-cd-runbook:仓库表与 secret 配置命令用当前名,并注明历史改名点

**刻意保留旧名的三处**,都是事实而非遗漏:
1. Go import / go.mod replace 与文档里的 import 示例——已发布 tag
   (v1.3.0 及更早)的 go.mod 仍是 module github.com/JianFeeeee/homeagentsdk,
   改了直接编译不过。PLUGIN_DEV 里加了显式注记说明这条边界。
2. gitcode 的 homeagentsdk → 404 实测行(那是实测事实表)
3. runbook 里「配置时还叫 homeagentsdk」的历史说明

实测(2026-10-22):github.com/JianFeeeee/hologramagentsdk 的 /tree/v1.1.0、
/tree/v1.3.0、/blob/main/LICENSE 均 200;example/* 为 301→tree 200
(GitHub 对目录自动从 blob 跳 tree,非死链);ls-remote 取到
refs/tags/v1.1.0 = 49f9957。

零代码改动:.go/.lua/go.mod/go.sum/third_party 变更数 0。
2026-10-22 07:25:53 +08:00
f408448c37 Merge pull request #18 from JianFeeeee/refactor/rename-batch6-site-docs
refactor(rename-batch6): 文档/静态站旧仓名清理 + gitcode 镜像仓名对齐 HologramAgent
2026-10-22 07:20:29 +08:00
75b4c50fa2 Merge main into refactor/rename-batch6-site-docs:吸收前置检查重试脚本,GC_REPO 收为 job 级单一定义点
PR #19 合入后 main 上 ci.yml / release.yml 的前置检查换成了公共脚本
.github/scripts/check-gitcode-repo.sh,与本批「GC_REPO 收为单一定义点」
改了同一段代码,故冲突。解决口径:

- 前置检查主体取 main 的重试脚本版本(不保留本批内联的那份长 case,
  重试/退避/notfound-优先于-403 的能力保留)
- GC_REPO 仍收为 job 级 env 单一定义处:main 上 ci.yml 两处、release.yml
  三处字面量(含 upload_assets 调用里那处)全部改为走 ${GC_REPO}
- 结果:每个文件内部只有 1 个定义点,注释里的实测清单同步为当前状态

零代码改动:.go/.lua/go.mod/go.sum/third_party 变更数 0。
YAML safe_load 两文件通过;check-gitcode-repo.sh bash -n 通过。
2026-10-22 07:17:57 +08:00
f65408dadb Merge pull request #19 from JianFeeeee/fix/gitcode-precheck-retry
fix(ci): gitcode 目标仓前置检查加有界重试,区分网络抖动与仓不存在
2026-10-22 07:16:00 +08:00
777f2f810d fix(ci): gitcode 目标仓前置检查加有界重试(网络抖动不再误判)
2026-10-21 的 main 流水线(dfc4e16)里,「前置检查 gitcode 目标仓是否存在」
首次执行时 gitcode 没有给出预期响应,一次 ls-remote 判失败 ⇒ sync job 红;
重跑立刻全绿。把一次网络抖动翻译成「镜像同步坏了」,是纯噪声。

改法:只对**网络层**失败重试,其余立即失败。
- 网络层(DNS/连接重置/TLS/超时/EOF)→ 最多 3 次,退避 5s、10s
- 仓不存在(gitcode 实测:could not be found + **403**,不是 404)
  → 立即失败,不重试。重试只会把「去建仓」拖成十几秒空等
- 凭据问题(401/403 但不带 not-found 特征)→ 立即失败,不重试
  (token 错了等 15 秒也不会变对)
- 分类顺序上 notfound 必须先于 403:gitcode 的「仓不存在」响应里
  **同时**含 could not be found 与 error: 403,先判 403 会把真问题贴上
  「凭据问题」的错误标签
- 每次尝试打一行「第 N/M 次探测失败(类别):摘要」;重试提示打到 stderr
  (⏳ … 秒后重试),Actions 日志里一眼看得出发生过重试
- 耗尽重试时整段回放每次尝试的摘要,并明确写「判定:网络问题」
  —— 与「仓不存在」/「凭据问题」的判定措辞互不相同,可直接 grep 区分

同口径:**抽成 .github/scripts/check-gitcode-repo.sh,两个 workflow 共用同一个
文件**,而不是两边各写一份「逐字一致」的脚本 —— 后者迟早会漂。ci.yml 的
sync-gitcode-main 与 release.yml 的 sync-gitcode 各留 1 行 bash 调用。
(抽得出公共片段,故没有采用「两边逐字改」的方案。)

token 安全:GC_URL 含 token,永不 echo;所有输出先过 redact()。脱敏两道:
先按字面量遮 token(BRE 元字符先转义),再按 URL 结构遮 userinfo@。
结构式的 password 段**不能排除 `/`** —— 真实 token 可以含 `/`,第一版写成
[^@/[:space:]]* 时 `https://user:tok/en@host` 匹配不上,只剩字面量一道兜住
(判据 C6 首次就是红的)。

判据与变异(9 组正向 / 8 条变异,全部咬住):
判据脚本与变异脚本见 commondev 台账 criteria/b7-gitcode-precheck-retry{,.mutations}.sh。
正向覆盖:重试次数上限(恰好 3 次探测)、仓不存在时 1 次即失败无重试痕迹、
首次成功无重试噪声、抖动后第 2 次成功、凭据问题不重试、token 不进日志、
默认退避真的是 5s/10s(用假 sleep 记录入参,不真等 15 秒)、
两处 workflow 都调同一脚本事 yaml 可解析 + bash -n。
变异 8/8 咬住:默认次数 3→1、退避→0、仓不存在也重试、去掉上限、
去掉重试提示、清空 notfound 特征串、去掉结构式脱敏、ci.yml 退回内联 ls-remote。

过程中修掉三处自己判据的盲区(都不是改产物去迁就判据,而是补判据):
1. M2(退避→0/0)首次未被咬住 ⇒ 补 C7「默认退避 = 5 10」(假 sleep 记录入参)
2. M7(删结构式脱敏)首次未被咬住 ⇒ 补 C7b:userinfo 密码用**别的** secret
   且**含 `/`**。C6 用当前 token 的字面量做输入,字面量替换本身就能抹掉,
   于是结构式规则删了也照样绿;且密码不含 `/` 时兜底的裸 user@ 规则会顶上,
   同样验不出来
3. C7b 第一版断言 grep 'JianFeeeee' 会误报 —— 脚本正常输出里就有
   gitcode.com/JianFeeeee/HologramAgent(那是路径不是凭据),改为断言
   userinfo 形态(://JianFeeeee[:@])

真实网络实测(不是只跑假 git):
- 仓存在:目标仓可访问,rc=0
- 仓不存在:1 次探测即失败,日志含 could not be found + 403,rc=1
- 网络失败:3 次探测 + 2 次退避,总耗时 5.7s(退避调成 1s/1s),rc=1

yaml.safe_load 三个 workflow 全部可解析;bash -n 通过。

未改动(留待裁决):deploy/scripts/upload_assets.py docstring 里
「SDK 仓传 JianFeeeee/homeagent-sdk」指 gitcode 侧、与实测一致
(gitcode: homeagent-sdk=200 / homeagentsdk=404;GitHub 相反),保持原样。
2026-10-21 23:46:01 +08:00
a3ab6e5cfa docs(deploy): SDK 镜像仓示例统一为 hologramagentsdk
upload_assets.py docstring 与 docs/git-branching.md 的上传示例里,SDK 仓
仍写 homeagent-sdk;按已定口径(SDK 仓名 hologramagentsdk,gitcode/GitHub
两侧均 200 实测)统一,避免与 sdk-docs 线合并后出现两种说法。

纯注释/文档改动,零代码逻辑变更。
2026-10-21 20:18:30 +08:00
ffe9bc2cff refactor(ci,docs): gitcode 镜像仓改名 HomeAgent→HologramAgent,定义点取值对齐
gitcode 侧镜像仓已完成改名,旧名 HomeAgent 变成 301 跳转,真仓是
HologramAgent。dbb787a 里三处 GC_REPO 定义点仍取旧名(命中 301),本批只
改取值与说明,**不改变 dbb787a 已建立的「每个文件恰一个定义点」结构**,
不引入任何新字面量。

实测(2026-10-21 20:1x,直连不带代理,非推测):
  $ curl -s -o /dev/null -w '%{http_code}' https://gitcode.com/JianFeeeee/<repo>

  HologramAgent     → 200  ← 当前取值
  HomeAgent         → 301  ← 旧名,跳转到 HologramAgent
  homeagent-sdk     → 200(SDK 仓,另一对象)
  hologramagentsdk  → 200(SDK 仓,另一对象)
  homeagentsdk      → 404
  HologramAgent-sdk → 404

「301 不是另一个仓,就是同一仓改名」的判定依据:两个地址 `git ls-remote`
得到的 refs 列表 md5 相同(同一组分支与 SHA),且 curl 页面 title 一致。

三个定义点(各文件恰好 1 处,消费点全部走变量,本次只改这三行的取值):
1. .github/workflows/ci.yml   job sync-gitcode-main 的 env.GC_REPO
2. .github/workflows/release.yml job sync-gitcode   的 env.GC_REPO
3. deploy/scripts/upload_assets.py 的 os.environ.get("GITCODE_REPO", ...) 默认值
消费点未动:ci.yml 2 处、release.yml 4 处命令行,仍全部是 ${GC_REPO}。

注释同步改准(原来把 301 的仓写成「200 ← 当前用这个」,是错的):
- 两个 workflow 的取值依据清单换成上面六个仓的现状,注明实测时间与 curl 命令
- 明确 HomeAgent = 301 跳转、HologramAgent = 当前取值
- 「以后再改名只改这三行」的说明保留

docs/zh/ci-cd-runbook.md 同步:
- 总览表 gitcode 镜像改为 HologramAgent(与主仓同名)
- 「当前取值」段与六仓实测表、301 判定依据
- API 侧口径也改准:/api/v5/repos/JianFeeeee/{HomeAgent,HologramAgent}
  不带 token 均 400(需鉴权,非仓不存在);/releases/tags/v1.3.13
  两名字均 200 且响应体一致。旧文档写「HologramAgent→404 是死链」已不成立。
- §3.3 curl 验活地址、§3.4 手工上传示例的 GITCODE_REPO 同步为新值

判据(正向 14 项 + 变异 11 条,全部实跑):
- 两个 workflow yaml.safe_load 解析通过;每个 env.GC_REPO 恰好 1 处
- 三处定义点取值完全一致(P4);消费点计数 ci=2/release=4(P6)
- P8 改为「不带 -L 的 200」:这正是本次误判的根因——之前用 curl -L 测,
  301 跟随后也是 200,把「旧名跳转」误读成「旧名是真仓、新名 404」
- 变异 M11「三处一致改回旧名 HomeAgent」只被 P8 抓住(P4 仍绿),
  证明该判据能单独区分「一致但指向 301 旧名」
- 所有 step run 块过 bash -n(30 个 run 块,0 错)
- gitcode 作 import host 残留 0;工作区无 .go/.lua/go.mod/go.sum 变更

零代码改动:packaging_test.go 里「禁止历史模块路径」的反向断言未动,
third_party 未动,systemd 相关行未动。
2026-10-21 20:15:41 +08:00
dbb787a658 refactor(ci,docs): gitcode 镜像仓名收为单一定义点,取值 HomeAgent(实测 200)
gitcode 镜像目标仓名此前在 3 个文件里各写死多份字面量,改名时要遍改且极
易漏(漏一处 = 一个静默死链,而且失败面在 release 里只有一格)。本批只做
「收成单点」,不改任何取值、不碰代码。

三个定义点(每个文件恰好 1 处,消费点全部走变量):
1. .github/workflows/ci.yml → job `sync-gitcode-main` 的 env.GC_REPO:232
   消费点 2 处命令行:git ls-remote 探测、git push main。
2. .github/workflows/release.yml → job `sync-gitcode` 的 env.GC_REPO:375
   消费点 4 处命令行:git ls-remote 探测、push tag、POST /releases、
   upload_assets.py 的 GITCODE_REPO 传入。
   (两个 workflow 是独立文件,不能共享 env,故各自一处定义。)
3. deploy/scripts/upload_assets.py → os.environ.get("GITCODE_REPO", ...) 默认值。
   手工跑脚本时的兜底,与两个 workflow 同值。

为什么取值 JianFeeeee/HomeAgent 而不是 HologramAgent:
GitHub 主仓已改名 HologramAgent,但 **gitcode 侧镜像仓没有跟着改名**。
curl 直连实测(2026-10-21):
  https://gitcode.com/JianFeeeee/HomeAgent      → 200   ← 当前取值
  https://gitcode.com/JianFeeeee/HologramAgent  → 404
指向 HologramAgent 会让每次发版/推 main 都在镜像同步这步红掉,且国内镜像
停止更新。

两个 job 的失败面不同(容易搞混,此处写明):
- release.yml 的 sync-gitcode:needs [prepare, publish]。探测 exit 1 ⇒
  **只红「镜像同步」这一格**;GitHub 侧 release 产物已经发出去了。
- ci.yml 的 sync-gitcode-main:needs [go, race, cross, gui, csrc, docs] 全绿
  后才跑。探测 exit 1 ⇒ **main 的整条 CI 红**(不是一格)。
所以仓名写错的真实后果是「必红一格 + 国内镜像不更新」,不是「发不出包」。

何时切回 HologramAgent:等 gitcode 上把镜像仓真正改名/新建成功、
https://gitcode.com/JianFeeeee/HologramAgent 返回 200 之后,
**只改上面这三行**即可,两个 workflow 的消费点与 runbook 均无需再动。

docs/zh/ci-cd-runbook.md 同步三节:单点化表格(定义处/消费点)、当前取值与
200/404 实测依据、两个 job 的失败面差异;并把 §3.4 的 curl 验活地址与
upload_assets 手工调用示例同步到 HomeAgent(原写法是已 404 的死链)。

判据(含变异,全部实跑):
- 两个 workflow 可被 yaml.safe_load 解析;每个 jobs[..].env.GC_REPO 恰好 1 处
- 每个 step 的 run 块抽出来喂 bash -n,全部无语法错
- 可执行形态里的 gitcode 目标仓名不含 HologramAgent(注释行豁免)
- 变异:把任一 GC_REPO 定义改回 HologramAgent、或把某消费点还原成字面量,
  判据均报红
- 零代码改动:git status 无 .go/.lua/go.mod/go.sum;go build ./... 通过
2026-10-21 19:55:59 +08:00
aafd971b7a fix(docs): PLUGIN_DEV 与 SKILL 的 module 路径 host 改为 github 2026-10-21 19:42:28 +08:00
ee2b7d7bac fix(docs,site): 回退上一批错误口径 —— SDK 仓名与站点域名改回实际活链
上一批(37b8c26 / 228fae2)把文档与站点里的 SDK 仓名统一改成了
`JianFeeeee/HologramAgent-sdk`、把站点域名改成了 `*.hologramagent.jianfgit.xyz`,
前提「本机无外网、无法验证」是错的。实测外网通畅,两处改动都是真实死链,本批全部回退。

## 一、SDK 仓名 HologramAgent-sdk → homeagentsdk(104 处)

| 文件 | 处数 |
|---|---|
| site/index.html | 23 |
| README.md | 7 |
| README_EN.md | 7 |
| assets/docs/zh/PLUGIN_DEV.md | 29 |
| assets/docs/en/PLUGIN_DEV.md | 29 |
| assets/docs/zh/OVERVIEW.md | 2 |
| assets/docs/en/OVERVIEW.md | 2 |
| docs/git-branching.md | 2 |
| docs/zh/ci-cd-runbook.md | 3 |
| **合计** | **104** |

含 `.git` 形态与 `cd homeagentsdk` 目录名。PLUGIN_DEV.md 里原本就是
`import "github.com/JianFeeeee/homeagentsdk/sdk"` 的两处示例(zh 253/448、en 255/455)
与 6a 前基线逐行 diff 无差异,未被改动成第三种形态。

## 二、站点域名 hologramagent.jianfgit.xyz → homeagent.jianfgit.xyz(15 处)

| 文件 | 处数 |
|---|---|
| README.md | 4 |
| README_EN.md | 4 |
| docs/zh/deploy-runbook.md | 2 |
| deploy/scripts/deploy-sdk-site.sh | 5 |
| **合计** | **15** |

deploy-sdk-site.sh 只改探活/注释里的域名,nginx 语义(SNI 透传、
server_name → root 的链路描述)未变。

## 三、明确未动

- go.mod 两行、third_party/homeagent-sdk 路径、internal/**/*.go 的 import —— 零改动
- deploy/scripts/upload_assets.py:23 默认值 `JianFeeeee/HologramAgent` 保留(主仓确已改名)
- cmd/gui/package.json homepage 保留 HologramAgent(GitHub 200)
- docs/git-branching.md:420 的 GITCODE_REPO 改为 `JianFeeeee/homeagent-sdk`:
  gitcode 上只有 homeagent-sdk(200) 与 HomeAgent(200),HologramAgent-sdk 与
  HologramAgent 在 gitcode 均为 404

## 四、实测证据(curl -L -o /dev/null -m 25 -w '%{http_code}')

GitHub 侧(经 http://127.0.0.1:7890 出口):

    200  https://github.com/JianFeeeee/homeagentsdk          ← 回退目标,活链
    200  https://github.com/JianFeeeee/HologramAgent         ← 主仓,确已改名
    404  https://github.com/JianFeeeee/HologramAgent-sdk     ← 上一批写成这个,死链
    404  https://github.com/JianFeeeee/homeagent-sdk         ← 不是这个名字
    200  https://github.com/JianFeeeee/HomeAgent             ← 301 → HologramAgent

深链抽查(回退后全部 200):

    200  .../homeagentsdk/tree/main/example/weather
    200  .../homeagentsdk/tree/main/example/plugindev
    200  .../homeagentsdk/tree/main/example/luademo
    200  .../homeagentsdk/blob/main/LICENSE
    200  .../homeagentsdk/tree/v1.1.0

站点域名(直连,不走代理):

    200  https://sdk.homeagent.jianfgit.xyz/                ← 回退目标,已部署
    200  https://introduce.homeagent.jianfgit.xyz/
    200  https://sdk.homeagent.jianfgit.xyz/llms.txt
    200  https://sdk.homeagent.jianfgit.xyz/llms-full.txt
    404  https://sdk.hologramagent.jianfgit.xyz/            ← 上一批写成这个,未部署

新子域 404 的原因不是网络:证书是 CN=*.jianfgit.xyz 通配符可用,
但 nginx 里没有 hologramagent 的 server_name,站点根本没部署。

gitcode 侧(决定 §三 那行的连字符形态):

    200  https://gitcode.com/JianFeeeee/homeagent-sdk
    200  https://gitcode.com/JianFeeeee/HomeAgent
    404  https://gitcode.com/JianFeeeee/HologramAgent-sdk
    404  https://gitcode.com/JianFeeeee/HologramAgent

## 五、验收

1. 全仓 `HologramAgent-sdk` 计数 = 0(.md/.html/.py/.sh/.yml/.yaml/.txt/.json/.go/.mod/.sum)
2. 全仓 `hologramagent.jianfgit.xyz` 计数 = 0
3. site/index.html 23 条 SDK 链接全指向 homeagentsdk,0 条指向 HologramAgent-sdk
4. curl -L 三条必测均 200(见 §四)
5. `TMPDIR=/home/newqqagent/tmpbuild6 go build ./...` 退出码 0
   (/tmp 已 98% 满,用独立 TMPDIR 避开 No space left on device)
6. git status 干净;11 个改动文件全为 .md/.html/.sh,零 .go 改动
2026-10-21 19:36:53 +08:00
228fae252d fix(docs): 回退 PLUGIN_DEV 的 SDK import 示例,并修正 upload_assets/gui homepage 的仓名
上一批 37b8c26 把文档里 4 处 Go import 示例改成了
`github.com/JianFeeeee/HologramAgent-sdk/sdk`,但该 module 路径并不存在:
主仓 go.mod:16 仍是 `github.com/JianFeeeee/homeagentsdk v0.9.2`,:46 replace
目标未变,third_party/homeagent-sdk/go.mod 首行仍是
`module github.com/JianFeeeee/homeagentsdk`,GitHub 上 HologramAgent-sdk
与 hologramagentsdk 均 404。照着文档抄这行 import 会直接 module not found,
比不改更坏。本次回退成真实可编译的 homeagentsdk。

改动:
- assets/docs/{zh,en}/PLUGIN_DEV.md 各 2 处(zh:253、448;en:255、455)
  import 路径回退为 github.com/JianFeeeee/homeagentsdk/sdk。该行附近未加任何
  「已改名」说明——等 SDK 仓真改名、主仓 go.mod 与第三方 module 一起改时
  再统一处理,属另一批。全仓已无 HologramAgent-sdk/sdk 形态。
- deploy/scripts/upload_assets.py:8、:23 默认目标仓
  JianFeeeee/HomeAgent → JianFeeeee/HologramAgent(主仓确已改名,HomeAgent
  在 GitHub 上 301)。:8 括号里「SDK 仓传 JianFeeeee/homeagent-sdk」按要求保留。
- cmd/gui/package.json:5 homepage 元数据同步为 HologramAgent;该文件仅此
  1 行变更,name/author/description 等其它字段未动。

保留不动(已获确认属正确形态):
- go.mod 两行与 deploy/packaging/package-linux.sh 六处 homeagentsdk
  (必须与 go.mod 逐字一致)
- internal/**/*.go 的 74 处真实依赖 import
- deploy/packaging/packaging_test.go:99,101 对 gitcode.com/JianFeeeee/HomeAgent
  的反向断言(它断言 build.sh 不得使用历史路径)
- internal/plugins/webui 三处注释里的 homeagent.jianfgit.xyz(历史实测记录)
- cd HologramAgent-sdk / clone ...HologramAgent-sdk.git(操作指令目录名与 URL,
  非 import 路径)

判据:7 条正向(含「文档 import 路径必须与 go.mod 的 SDK module 同源」);
变异 4 次均按预期报红——分别还原 zh:253 import、upload_assets.py:23 默认值、
gui homepage、以及把 packaging_test.go 反向断言那两行也改名。
go build ./... 通过,go test ./deploy/packaging/ 通过。
2026-10-21 19:25:05 +08:00
37b8c26675 refactor(docs): 批6a 清理介绍站与文档的旧仓名/旧链接
改名 HomeAgent → HologramAgent 后,介绍站与仓内文档仍指向旧仓地址、旧站点
域名,点击即 404 / 落回旧站。本批只动文档与静态站,代码零改动。

映射(严格按主管给定的唯一权威口径):
- github.com/JianFeeeee/HomeAgent → .../HologramAgent(含 /blob /releases /tree)
- github.com/JianFeeeee/homeagentsdk → .../HologramAgent-sdk
- introduce.homeagent.jianfgit.xyz → introduce.hologramagent.jianfgit.xyz
- sdk.homeagent.jianfgit.xyz → sdk.hologramagent.jianfgit.xyz
- 品牌名/链接文本 homeagent-sdk → HologramAgent-sdk(含 `cd homeagent-sdk`
  克隆后的本地目录名,跟随新仓名)

改动面(12 个文件,138 行,纯替换无逻辑变更):
- site/index.html 36 处:13 处主仓链接 + 23 处 SDK 仓链接(22 个 example/
  与 LICENSE、仓库首页)
- README.md / README_EN.md 各 12 处(9 处 URL + 3 处链接文本)
- assets/docs/{zh,en}/PLUGIN_DEV.md 各 34 处、OVERVIEW.md 各 2 处
- docs/zh/ci-cd-runbook.md 9 处、docs/zh/deploy-runbook.md 2 处(站点域名)
- deploy/scripts/deploy-sdk-site.sh 5 处(注释里的 server_name + 3 处 curl
  探活地址,否则改完站脚本仍在验旧域名)
- docs/git-branching.md 2 处、assets/skills/commondev-discipline/SKILL.md 1 处
  (gitcode module 路径)

刻意不动(动了会坏):
- 真实目录 third_party/homeagent-sdk(只改 URL 里的 homeagentsdk)
- go.mod 的 module / replace、所有 .go import(module 仍是
  github.com/JianFeeeee/HologramAgent,SDK 仍 homeagentsdk)
- deploy/packaging/package-linux.sh 里的 `replace github.com/JianFeeeee/
  homeagentsdk` 与 clone URL —— 它必须与未改动的 go.mod 逐字一致,属代码非文档
- 数据目录 ~/.homeagent、二进制 homed、产物名 hologramagent_*、
  插件名 homeagent-mail-bridge
- systemd 相关行(homeagent.service / systemctl / journalctl)—— 待上级确认,
  本批不动,清单见汇报
- docs/api/*.md(首行「请勿手改」,生成型)

判据(含变异验证):
- 三条旧串 grep 全仓清零;third_party/homeagent-sdk 目录仍在
- site/index.html 标签配平(HEAD/WORK 双跑)+ section 数 13→13 未减
- 变异:分别还原 deploy-sdk-site.sh:206、site/index.html:3041、README.md:243、
  ci-cd-runbook.md:11 各一处,以及删一个 </footer>,判据均按预期报红
- go build ./... 通过(仅把 TMPDIR 指到 /var/tmp 规避 /tmp tmpfs 满,仓内文件无关)

未执行任何线上动作:不 push、不开 PR、不重命名 GitHub 仓。注意 SDK 仓在
GitHub 侧目前仍叫 homeagentsdk,本批把文档指向 HologramAgent-sdk 是「改名
在路上」,该仓改名完成前这批 SDK 链接会 404。
2026-10-21 19:22:02 +08:00
dfc4e16e65 Merge pull request #16 from JianFeeeee/refactor/rename-batch4b-ci-mirror
refactor(ci): 批4b 清理 CI/镜像链路旧仓名,并加 gitcode 目标仓前置检查
2026-10-21 19:09:45 +08:00
a7331251ab refactor(ci): 批4b 清理 CI/镜像链路旧仓名,并加 gitcode 目标仓前置检查
改名 HomeAgent → HologramAgent 后,CI 里仍写死旧仓库路径,且镜像链路
一旦目标仓缺失只会得到 git 的原始报错,看不出「仓没建」。

改动:
- ci.yml: sync-gitcode-main 推 main 目标改为 JianFeeeee/HologramAgent,
  并在 push 前新增「前置检查 gitcode 目标仓是否存在」独立 step。
- release.yml: sync-gitcode 的 push / releases API / GITCODE_REPO 统一
  走 GC_REPO=JianFeeeee/HologramAgent 变量;同样新增前置检查 step。

前置检查语义:
- GITCODE_TOKEN 未配置 → 沿用既有「跳过镜像同步」(可选项,不算错误);
- token 有但目标仓不存在 → 打印要建的仓与地址,exit 1 硬失败;
- 探测与 push 用同一 URL、同一凭据(git ls-remote),贴近真实失败面;
- 日志对 URL userinfo 做结构脱敏,避免 token 被 git 报错带出。

gitcode 报错文本按 2026-10 实测校准:目标仓不存在时返回
  remote: <CH...> The project you were looking for could not be found.
  fatal: ... The requested URL returned error: 403
是 "could not be found" + 403,不是 GitHub 那种 404 / Repository not found,
故分类同时匹配这两种形态(401/403 单列一支,提示仓未建或 token 无权限)。

未执行任何线上动作:不重命名 GitHub 仓、不建 gitcode 镜像仓、不 push/开 PR/打 tag。
2026-10-21 18:11:59 +08:00
5d7e26edf2 Merge pull request #15 from JianFeeeee/refactor/rename-batch5-docs-site
改名批 5-a:仓库内文档与站点改名 + 修 make install 真破损
2026-10-21 17:11:09 +08:00
9bc2687972 refactor(docs): 补正 git-branching 主仓名,接上批 5-a 改名
批 5-a 复核发现 docs/git-branching.md 两处仍以 TrueAgent 指代主仓,漏改。
只动这两行,`third_party/homeagent-sdk` 是 SDK 仓目录名,原样保留。

- 第 4 行:`本仓(TrueAgent/HomeAgent)` → `本仓(HologramAgent)`
  (整段收敛成一个名字,不再留 "旧目录名/旧展示名" 的复合写法)
- 第 183 行:`### 主仓(TrueAgent)` → `### 主仓(HologramAgent)`

TrueAgent 在本仓其余 6 个文件里都是磁盘上真实存在的路径 /home/program/TrueAgent
或本机工作副本名,一行未动:deploy/systemd/embed-sidecar.service、
assets/skills/commondev-discipline/SKILL.md、docs/zh/commondev-design.md、
docs/zh/commondev-agent-runtime-design.md、internal/security/store_test.go、site/README.md。

判据口径同步收紧(判据 2):
- 豁免只允许三类并逐条打印 —— A 仓库 URL(批 4-b)、B 本机真实路径、C 既有契约
  (HOMEAGENT_*、homeagent-sdk/homeagentsdk、homeagent.service、插件名);
  ~/.homeagent 单列为 D 遗留项(须与 cmd/ 侧同改)
- 原 `TrueAgent/HomeAgent` 常驻豁免已撤销(该串已不存在,留着会掩盖它复活)
- 新增 TrueAgent 守卫:git-branching 内 TrueAgent 必须 0;全仓 TrueAgent 逐行集合
  必须 == base e3584bb 集合减去这两行,命中文件数 7→6
- 新增安装路径断言:/usr/lib、/usr/share/doc、/etc、/var/lib 旧值必须 0

自证:docs/git-branching.md 里 HologramAgent 3 处、TrueAgent 0 处;全仓 TrueAgent
文件数 7→6,逐行与基线 diff 仅这两行。四条判据正向全绿,13 条变异全部被咬住。
2026-10-21 17:07:46 +08:00
931c803421 refactor(docs): 核心仓文档与介绍站展示名改名为 HologramAgent
只改仓库内文件的纯展示品牌名与已随批 2 落地的安装路径;运行时契约、仓库 URL、
域名、环境变量、可执行名、插件名一律不动。

一、纯展示名(HomeAgent → HologramAgent,共 44 处 / 18 个文件)
- README.md 4、README_EN.md 4(标题、client 变体说明、Windows 安装包名、AGPL 段)
- docs/ 8 个文件 14 处(git-branching 1、ci-cd-runbook 1、commondev-design 3、
  commondev-agent-runtime-design 4、deploy-runbook 1、legacy-issues 1、
  prod-migration-checklist 1、triple-extract-attention 2)
- assets/docs/ 6 个文件 14 处(zh/en 的 OVERVIEW 2+2、ARCHITECTURE 2+2、
  PLUGIN_DEV 3+3)
- assets/branding/mascot-xiaozhai-guide.md 2 处(身份行、标志名)
- site/index.html 6 处(title、meta description、og:title、logo alt、页头字、
  插件卡片文案 "HologramAgent 接入 AgentMail")

二、安装路径对齐批 2 产物(共 10 处)
- /usr/lib/homeagent → /usr/lib/hologram(7 处:README 2、README_EN 2、
  multimodal-space 3)
- /usr/share/doc/homeagent → /usr/share/doc/hologramagent(3 处:README 1、
  README_EN 1、multimodal-space 1;注意 Doc 路径带 agent 后缀)
- site/index.html 的 19 处 HomeAgent 里,13 处是 github.com/JianFeeeee/HomeAgent
  仓库 URL,属红线整串保留;只改 URL 之外的展示文案(其余 13 处零改动)

三、明确不改(红线 / 一致性冲突)
- site/index.html 的 23 个 github.com/JianFeeeee/homeagentsdk 外链(21 条
  example + 2 条纯仓库),SDK 仓改名属批 4-b,一个未动
- 核心仓仓库 URL、sdk./introduce.homeagent.jianfgit.xyz 域名、HOMEAGENT_ 前缀、
  homed/waiter/hmapdev、.hmap、homeagent-mail-bridge 等插件名、third_party/、
  internal/ 与 cmd/ 全部零改动
- ~/.homeagent 路径本批保持原样:cmd/waiter/main.go:109、daemon.go:290 硬编码
  仍为 .homeagent,全仓无 .hologram 命中;照规格改文档会指向程序不用的目录,
  需与代码侧那条线一起改(遗留项,已单独上报)

判据:文档区展示名残留归零(44 文件)、红线计数与 base e3584bb 逐项相同、
三条变异均被咬住且还原后复绿。
2026-10-21 17:00:48 +08:00