Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dsh-mneme 是 DSH 宿主的记忆插件(蒸馏 / 注入 / 检索 / 巩固 / sc
| 配置 | `src/config.js`(schema + lightMode)、`src/settings.js`(feature flags 白名单) | 一切行为开关的家 |
| API 面 | `src/api.js`(宿主内 /api/dsh-mneme/*)、`src/api-standalone.js`(Bearer 数据面)、`bin/dsh-mneme-mcp.mjs`(MCP stdio)、`src/serve.js` + `bin/dsh-mneme-serve.mjs`(独立服务 daemon,无 LLM 数据面) | 对外四张脸 |
| 面板 | `lib/client.js` | 面板侧产物,无 src 对应物 |
| 手机面板 | `src/dsh-go/**` + `package.json` 的 `dshGo` | dsh-go 原生面板:进程内派发同一批路由,只有宿主提供 `dshGo` 服务时才加载([docs/DSH-GO.md](dsh-mneme/docs/DSH-GO.md)) |
| 运行时 | `src/runtime/*` | 模型下载(断点续传)/ 校验 / adopt |
| 命令 | `src/commands.js` | 斜杠命令注册与派发 |
| 双语 | `src/lang.js` | 文案与语言解析 |
Expand Down
8 changes: 8 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
- **验证**:全量 `node --test test/*.test.js` 2941 用例 / 2940 通过 / 0 失败 / 1 跳过;check-sync 99 一致。新增 `test/kb-collections.test.js`(8)、`test/kb-routes.test.js`(7)、`test/kb-api-wiring.test.js`(4)、`test/wiki-t1-jobs.test.js`(状态机,注入桩);`test/wiki-api.test.js` 13→18 条、`test/client.test.js` 109 条(座位数 2→3 已同步)、`test/api.test.js` 旗标计数锁 114→117。SECURITY.md 两侧路由清单已按 policy 声明补齐(`test/security-doc.test.js` 双向比对绿)。
- **已知边界**:T1 的测试全程走注入桩,**真 T1 一次没跑**(花钱的不进 CI);后端七条 KB 路由与七条 choro 路由已用真装配(createApi + createStore + createKb + createChoroQueries)发真 HTTP 逐个冒烟过,但**面板本身没在真机宿主里点过**——渲染与事件绑定仍只有代码级验证;真机上若 `/kb/docs` 回执字段有出入,只需改 `loadDocs`/`docRows` 一处。本机目前只登记 `dsh-baleen` 一个仓库且 `claims=0`(T0 骨架有、T1 语义正文为空),所以打开面板第一眼看到的仍是「骨架、无语义」的地志。

- **dsh-go 原生记忆库面板:手机 App 里也能看、能改这份记忆库**:`dsh-go`(DSH 的 Android 客户端,[仓库](https://github.com/ccch1mneyyy/dsh-Go))是 Flutter 原生界面,看不到 Web 面板。新增 `src/dsh-go/` 域随插件一起发布——不另开包,也不另起端口:面板在引擎进程内直接派发 `src/api.js` 已注册的路由 handler,读写与 Web 面板同一批口径(「两张皮只许剩鉴权与路径不同」)。

- **按宿主能力挂载**:`package.json` 增 `dshGo` 清单(一个面板 `mneme`,placement `session.tab` / `features`);`src/index.js` 在 webServer 块内 `ctx.inject(["dshGo"], …)` 里动态导入 `src/dsh-go/panel.js`,没有桥的宿主(桌面 / headless)整块不加载。`cfg.apiToken` 一并交给面板:进程内写请求带 `x-dsh-mneme-token`,不给自家路由开白名单。
- **视图**:v2 视图协议,顶层是 App 原生 `tabs`(记忆 / 实体 / 设置);设置页内两格 `segmented` 切「设置 | 状态」——电脑端是四个页签,但 App 页签条每格有 80dp 下限,四格会折行。文案取自 Web 面板的 zh i18n,不自造解释性文字。记忆 / 实体页的树不随数据变(宿主的 `view()` 早于 `data()`,按数据分支的地方首屏会定形、之后只填绑定),空状态提示改成绑定文案;翻页用一行 `chips`(原生按钮组每列至少 136dp×字号,两个按钮在窄屏会折成上下两行),页码兼作 chips 的标签。
- **写回不覆盖别人的写入**:编辑页记住打开时的快照,只提交改动过的字段;`updated_at` 对不上就中止并重新载入。删除确认不携带参数(`confirm` 节点只有 id 与按钮文案),所以按面板 scope 记下「这个面板打开了哪条记忆」,确认时核对。
- **为面板补了两处读面**:`/entities` 支持 `?q=`(名字过滤下推到 store,`%` / `_` 转义);`/external-api` 支持 `?token=omit`(只读配置,不生成也不落库 Bearer——默认 GET 会顺手铸造一个)。
- **验证**:`test/dsh-go-panel.test.js`(19 屏视图预算 + 页签 / 开关 / 文案锁)、`test/dsh-go-panel-actions.test.js`(点按之后发给宿主的请求、scope 核对、只发差集、视图切换)、`test/dsh-go-host.test.js`(请求体 flush、异步响应、令牌、401、超时);`test/api-routes-gaps.test.js` 补 `q` 与 `token=omit` 两例。

- **「Agent 眼中的你」按段化第一期:注入渲染函数产出段边界(`segments`),面板从此能按段读(#600)**。`renderUserSettingsBlock` 原先只回一整串 `text`,面板那张卡只能整段只读显示——想改注入文本里的某一句,得走「整段提取 → 复制 → 粘进画像框」的剪贴板中转,而画像段是手写原文、人格段是结构化卡、事实段是逐条维护的行,三者写回语义完全不同。段边界是这三条路能分开走的前提。

- **纯加法,注入字节零变化**:返回值多一个 `segments`,每行带 `kind` 与写回定位信息;`text` 仍是原来的 `escapePromptVars(config, lines.join("\n"))`。段文本存**未转义**原文(面板要显示给人看),转义只发生在 `text` 这一层——转义只动连续花括号、换行不参与,逐段转义与整串转义等价,所以注入行为与改前逐字节相同,老用户零变化。
Expand Down
97 changes: 97 additions & 0 deletions dsh-mneme/docs/DSH-GO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# dsh-go 原生面板

> 客户端仓库:[ccch1mneyyy/dsh-Go](https://github.com/ccch1mneyyy/dsh-Go)

> dsh-mneme 一直是「Web 面板 + 11 个记忆工具 + 自动注入」。手机 App **[dsh-go](https://github.com/ccch1mneyyy/dsh-Go)**(DSH 的 Android 客户端:Flutter 原生壳,手机本机跑引擎,也能遥控电脑)是 Flutter 原生界面,看不到 Web 面板——这个域就是给它的原生面板:与电脑端同一套功能划分(记忆 / 实体 / 设置 / 状态)、同一批后端路由。
>
> 记忆库是**按宿主独立存放**的:面板读写的是它所在那台宿主的 `memory.db`,与另一台设备上的库不互通。目前没有在线同步,只能通过分享包 `mneme.share/v1` 手动迁移,且不是实时同步。

## 职责边界

- **只做展示与转发**:取数、写回都走 `src/api.js` 已注册的 HTTP 路由 handler(进程内派发),不新增路由、不直连 SQLite、不写第二套口径。
- **不碰记忆本体**:面板不参与蒸馏、注入、巩固、冲突判定;它只是这些能力的一个操作面(和 Web 面板同级)。
- **可选能力**:桌面 / headless 宿主没有 [dsh-go](https://github.com/ccch1mneyyy/dsh-Go) 桥,这个域整块不加载(`ctx.inject(["dshGo"], …)` 的回调不会被调度)。
- **不重复 Web 面板**:`lib/client.js`(浏览器 bundle)照旧是 Web 面板的唯一实现;两边共享的是**后端**,不是视图代码。

## 对外接口

### 1. 包清单

`package.json` 的 `dshGo` 字段([dsh-go](https://github.com/ccch1mneyyy/dsh-Go) 桥扫描已启用插件的 package.json,见 `dsh-go-bridge/lib/native/manifest.js`):

```json
"dshGo": { "v": 1, "title": {...}, "icon": "book", "contributes": { "panels": [{ "id": "mneme", ... }] } }
```

面板的 `pkg` 由桥按「注册调用所在 fiber 所属的 loader entry」反查,所以**不需要独立包**:在 mneme 自己的插件上下文里调 `ctx.dshGo.panel("mneme", …)`,归谁一目了然。

### 2. 挂载点

`src/index.js` 在 webServer 块内、`createApi` 之后:

```js
ctx.inject(["dshGo"], (dshCtx) => {
import("./dsh-go/panel.js")
.then((panel) => panel.mountDshGoPanel(dshCtx, { apiToken: cfg.apiToken }))
.catch((error) => ctx.logger?.warn?.(`[dsh-mneme] dsh-go 面板未加载:${error?.message ?? error}`));
});
```

两处细节都是刻意的:

- **动态 import**:静态导入会在插件加载时就把面板模块跑一遍,「没有 dsh-go 就不加载这个域」这句承诺就不成立了。
- **把 `cfg.apiToken` 交给面板**:`createApi` 给 token / token-write 路由包了鉴权,进程内调用不带令牌会被自家路由拦成 401。令牌以 `x-dsh-mneme-token` 头随请求走(`src/dsh-go/host.js`),不给进程内调用开白名单。

### 3. 视图协议(v2)

`buildView(state)` 返回 `{v:2, type:"stack", accent:"app", children:[…]}`,顶层是 App 原生 `tabs`:记忆 / 实体 / 设置。设置页顶部两格 `segmented`(设置 | 状态)——电脑端是四个页签,但 App 的页签条每格有 80dp 下限,四格在手机上会折行,所以放三格。

### 4. 0.11.0 补上的两块面板面

mneme 0.11.0 把「Agent 眼中的你」自动事实(`profile_facts` 三表)和 chorography 地志搬上了 Web 面板。手机面板跟着补,仍走同一批路由:

| 面板位置 | 路由 | 说明 |
|---|---|---|
| 设置 › 个性化 › Agent 眼中的你 | `GET /profile-facts?agent=` | 一次拿到 `cards` / `facts`(含归档)/ `journal` / `runs`;flag 关着也照读,历史上攒下的事实要能看见 |
| 同上(增 / 改 / 归档) | `POST /profile-facts/apply` | body `{agent, ops}`,op 形状与自动轮共用:`add` 必须带 `content`,`update`/`archive` 必须带 `id` |
| 同上(立即更新) | `POST /profile-facts/run` | 一次 LLM 维护轮,慢且串行,面板按 pending 处理(60s);`409` = 功能关着,`503` = 没配模型,`502` = 模型答得不成形 |
| 设置 › 记忆引擎 › 代码库地志 | `GET /choro/repos`、`/choro/modules?repo=`、`/choro/page?repo=&slug=` | 仓库 → 模块骨架 → 页正文;都受 `chorographyEnabled` 闸门(关着回 `409 disabled`) |
| 同上(重建骨架) | `POST /choro/run` `{repo}` | 只跑 T0 apply(秒级);T1 富化仍需命令行 `--t1`,所以重建会把已富化的语义段退回骨架 |

功能开关这块也换了一半口径:

- 枚举型旗标(`chorographyInject` 的 `light/full/off`)用下拉,`PUT /features` 收字符串值——同一个动作 id 既服务开关也服务下拉。
- 新增「其它」分组:把 `/features` 的 `effective` 里**我们还没给中文名**的布尔项动态列成数据行(`list` + `items` 绑定),点一行翻转一项。上游每加一个开关,手机面板不会再静默漏掉它;数值/字符串键(模型名、预算)不放这里,它们要输入框。

## 内部文件

| 文件 | 一句话 |
|---|---|
| `src/dsh-go/panel.js` | 状态机、取数、动作(`mountDshGoPanel`)。动作跑完自己把新数据推给所有打开的 feed |
| `src/dsh-go/host.js` | 进程内派发:`ctx.get("webServer").match(path)` 拿到 mneme 自己的 handler,伪造 req/res 调用 |
| `src/dsh-go/text.js` | 类型标签、开关分组、时间与 scope 格式化 |
| `src/dsh-go/views/root.js` | `buildView`:顶层页签组合 |
| `src/dsh-go/views/{common,memory,entity,settings,status}.js` | 各分区视图;`common.js` 是小组件构造器 |

文案取自 Web 面板的 zh i18n(`lib/client.js` 里的 `memory.*` 键),面板不再自造解释性文案。

## 已知坑

- **feed 是推送式的**:`data()` 每个面板实例只跑一次,`refresh` 效果只让 App 重画视图、不会重跑 `data()`。所以每个动作结尾都要 `broadcast()`(向所有 emit 推新 payload),否则点了按钮界面还是旧数据。推送节流 4/s。
- **不能走 HTTP**:[dsh-go](https://github.com/ccch1mneyyy/dsh-Go) 桥的路由守卫(`installFallbackGuard`)对没有合法签名的请求直接 401(`x-dsh-go-guard: reject`),浏览器 cookie 是宿主签的、进程内也拿不到。进程内 `webServer.match()` 绕开这一层,且复用同一批 handler。
- **`/external-api` 的 GET 有副作用**:不带参数时会顺手生成 Bearer token 并落库。面板读配置用 `?token=omit`(只回 `enabled/port/host`,不铸造也不落库),不读令牌。
- **进程内调用也要带令牌**:宿主配了 `apiToken` 时,`/update`、`/delete`、`/features` 这些 token 路由对进程内请求同样校验;令牌走 `x-dsh-mneme-token` 头。
- **两种响应节奏都要等**:多数路由在返回的 promise 里 `res.end`,少数(`/version-check`)立即 `return` 再在回调里写响应。`host.js` 同时等 handler 的返回值和响应结束,只 race 的话后者会被当成空结果。
- **`/search` 不认识 `type`**:它是全库语义检索,面板在拿到结果后再按所选类型收一遍。
- **`/entities` 的 `q`**:名字过滤必须下推到 store(`LIKE`,`%`/`_` 转义)。面板若自己取 limit 条再本地过滤,排在 limit 之外的实体永远搜不到。
- **删除只能按状态认人**:`confirm` 节点没有参数位(只有 id 与两个按钮文案),所以「删哪一条」只能靠状态;状态是所有面板共用的,因此面板按 `scope` 记下自己打开的那条,确认时与本打开的条目核对,不一致就拒绝。
- **`/update` 不接受 `type`**:编辑已有记忆时类型只读展示,新建才给选择器。
- **保存只发差集**:`/update` 没有版本号校验,整份提交会把别处(工具、Web 面板)刚写进去的新值盖掉。面板在编辑页记住打开时的快照,只提交改动过的字段,`updated_at` 对不上就中止并让用户再看一眼。
- **`/create` 不接受 scope**:归属 Agent / 工作区两个输入只在编辑已有记忆时出现。
- **节点预算**:面板 120 节点 / 8 层 / 32 KiB,`data` 32 KiB。列表行只下发 `summary`(`content` 截 120 字),全文留在详情取,否则条目一多就超预算。
- **地志与事实都有闸门**:`/choro/*` 在 `chorographyEnabled` 关闭时回 `409 {error:"disabled"}`,`/profile-facts/run` 在 `profileFactsEnabled` 关闭时同样 `409`;两个读口(`/profile-facts`、`/choro/repos` 之外的模块/页)不受开关影响,面板要能展示历史数据。
- **事实的 op 形状是硬约定**:`add` 缺 `content` 会把 `"undefined"` 写进事实正文并从此每帧注入;面板在本地就拦「正文不能空」,不把脏 op 递过去。
- **重建骨架只到 T0**:`POST /choro/run` 是同步秒级回执,不带 T1 富化;面板文案要说明「语义段退回骨架」,否则用户会以为富化丢了。
- **`refresh` 会重建视图树,页签选择会丢**:顶层 `tabs` 的选中项是 App 的本地状态(视图协议没有绑定口),App 收到 `refresh` 重新拉一遍视图,树一重建就回到第一个页签。所以「只改一个绑定值」的动作(开关、档位下拉、外部访问开关、事实增改、地志翻页)不要申请重画,只推数据;只有真的改变了「哪些节点存在」的动作(进子页、换分区、换分组、翻页、搜索)才要 refresh。
- **`nestFlags` 不能把值压成布尔**:枚举型旗标(`chorographyInject` = light/full/off)以前被写成 `value === true`,下拉永远只拿到 true/false。视图绑定要原样透传。
- **不要用 `metric.caption` 放单位**:`caption` 固定渲染在数值下方一行;v2 的 metric 整串数值同一字号,所以单位拼进 value(`57 条`),App 侧只有自带模型卡才会拆小字单位。
Loading
Loading