dsh-memento 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +132 -0
- package/CHANGELOG.md +46 -0
- package/LICENSE +201 -0
- package/README.es.md +166 -0
- package/README.hi.md +166 -0
- package/README.md +166 -0
- package/README.pt.md +166 -0
- package/README.zh.md +166 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/client/client.d.ts +6 -0
- package/client/client.js +240 -0
- package/cordis.patch.yml +4 -0
- package/index.mjs +1849 -0
- package/lib/budget.mjs +110 -0
- package/lib/constants.mjs +58 -0
- package/lib/errors.mjs +143 -0
- package/lib/extract.mjs +54 -0
- package/lib/gate.mjs +131 -0
- package/lib/match.mjs +43 -0
- package/lib/snapshot.mjs +115 -0
- package/lib/store.mjs +654 -0
- package/lib/strings.mjs +43 -0
- package/lib/workspace.mjs +31 -0
- package/package.json +93 -0
- package/types.d.ts +191 -0
package/README.zh.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# dsh-memento
|
|
2
|
+
|
|
3
|
+
**给 DeepSeek Harness 补上有界、分层、带审批门、可审计的跨会话记忆。**
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/@deepseek-ai/dsh)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[]()
|
|
9
|
+
[]()
|
|
10
|
+
|
|
11
|
+
[English](README.md) · [中文](README.zh.md) · [Español](README.es.md) · [Português](README.pt.md) · [हिन्दी](README.hi.md)
|
|
12
|
+
|
|
13
|
+
> 别的记忆插件卖**仓库**,dsh-memento 卖**接缝**:类型安全的 `ctx.memory` 服务、模型绕不过去的写入审批门、能从会话日志重建的审计链。DeepSeek Harness 的原生第一方记忆——记忆协议 + 信任门 + 审计,零网络、零凭据。
|
|
14
|
+
|
|
15
|
+
## ✨ 为什么是 dsh-memento?
|
|
16
|
+
|
|
17
|
+
- **它是能力接缝,不是又一个 store。** Service Definition(`ctx.memory`)+ 本地 SQLite Provider(`node:sqlite`,WAL,`0600`)+ Consumer(`memory` 工具 + 冻结快照注入)。任何未来的插件——dsh-claude-move 的 seed 集成、桥接、面板——都通过**同一个门**读写**同一份** store。
|
|
18
|
+
- **审批门不可绕过。** 每条写路径(`add`/`replace`/`remove`/`seed`)都被强制经过审批 waterfall,且强制点在**服务内部**而非工具层。`writePolicy: ask | auto | off` 是模型看不见、改不了的配置;会话级 `never` 姿态依旧先于一切。
|
|
19
|
+
- **模型可见 ⟺ 落盘。** 注入的快照逐字进入 `request/header.system`;每次写都能从 `approval/asked`(完整载荷)+ `approval/decided`(结果)+ 插件自有审计表重建。
|
|
20
|
+
- **有界且诚实。** 每轨每层硬字符预算(默认 user 2000 / agent 4000)。写满**返回结构化错误**(用量 + 上限)——模型整合后重试。绝不截断、绝不自动压缩。
|
|
21
|
+
|
|
22
|
+
## ⚡ 30 秒上手
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
# 要求 Node ^22.19 || >=24、DSH 0.1.0-rc.6
|
|
26
|
+
dsh plugin --profile web add dsh-memento # 或 ./dsh-memento / tarball / GitHub 地址
|
|
27
|
+
dsh --profile web --dump-config # 应看到 "# == dsh-memento" 层,启动无 FAILED
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
然后在 Web UI 里:让模型记住一件事 → 批准这次写入 → 开一个**新会话**问它记得什么。演示到此结束。
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# 可选覆盖(写在 profile 的 cordis.patch.yml)
|
|
34
|
+
- id: memento
|
|
35
|
+
config:
|
|
36
|
+
writePolicy: ask # ask(默认)| auto | off —— 模型不可见
|
|
37
|
+
budgets:
|
|
38
|
+
user: { userGlobal: 4000, workspace: 2000 } # 中文记忆多:调大并在 PR 说明理由
|
|
39
|
+
agent: { userGlobal: 4000, workspace: 4000 }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 🧠 它提供什么
|
|
43
|
+
|
|
44
|
+
| | 组件 | 你得到什么 |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| 🧩 Service Definition | `ctx.memory` —— `add` / `replace` / `remove` / `query` / `seed` / `budgets()` | 类型化、声明合并的服务;写方法内部强制过门 |
|
|
47
|
+
| 💾 Provider | `lib/store.mjs` —— `node:sqlite` 单文件(`$DSH_HOME/dsh-memento/memory.db`,WAL) | 零依赖、零网络;条目表 + 审计表;唯一子串匹配 |
|
|
48
|
+
| 🛠 Consumers | `memory` 工具 · 冻结快照注入(systemPrompt 段,顺序 `-50`)· `memory_recall` 工具 · `/memory` 命令 · 只读 Web 面板 | 模型读写、带用量头的冻结快照、两段式召回、用户侧命令、浏览器抽屉 |
|
|
49
|
+
|
|
50
|
+
**双轨 × 双层 × per-agent 键。** `user` 轨 = 用户画像(偏好、沟通风格、雷区);`agent` 轨 = 环境事实、项目约定、教训。每轨分 `user-global`(跨工作区)与 `workspace`(按会话 cwd)两层——学 Codex 的合并分层,不学 Hermes 的纯全局。第三维按会话 `agentPreset` 隔离条目(per-agent 作用域);无 preset 的条目留在人人可见的共享层。
|
|
51
|
+
|
|
52
|
+
**冻结快照。** 快照在会话首个 prompt 组装时渲染一次(SQLite 同步读 + 按会话缓存),会话内不再变化——前缀缓存天然稳定。会话内变更只落盘 + 落审计。
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Consumer: memory 工具 Consumer: 冻结快照(systemPrompt 段,顺序 -50)
|
|
56
|
+
add/replace/remove/query 按会话冻结,带用量头
|
|
57
|
+
│ 写(agent+callId) │ 读(同步,session cwd)
|
|
58
|
+
▼ ▼
|
|
59
|
+
Service Definition: ctx.memory —— budgets/add/replace/remove/query/seed
|
|
60
|
+
每次写:预算预检 → ctx.approval.request(审批 waterfall)→ 预算复审 → 落盘 → 审计
|
|
61
|
+
│
|
|
62
|
+
▼
|
|
63
|
+
Provider: lib/store.mjs —— node:sqlite(WAL,0600),条目表+审计表,唯一子串匹配
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 🧰 安装与卸载
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
dsh plugin --profile <name> add ./dsh-memento # 本地 checkout(无构建步骤)
|
|
70
|
+
dsh plugin --profile <name> add git+https://github.com/PerryLink/dsh-memento.git # GitHub 安装;npm 首发后可用
|
|
71
|
+
dsh plugin --profile <name> remove dsh-memento # 卸载:库与会话日志保留
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
卸载后记忆库与记录过记忆活动的会话日志保留,旧会话仍可正常加载。
|
|
75
|
+
|
|
76
|
+
## ⚙️ 配置
|
|
77
|
+
|
|
78
|
+
所有字段都是经过校验的 Schemastery `Config`;非法值加载期响亮失败。在 cordis.yml 的 `memento` 行覆盖。
|
|
79
|
+
|
|
80
|
+
| 字段 | 默认 | 含义 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `enabled` | `true` | `false` 时服务/工具/快照/命令/面板/answerer 整体消失(不留半残状态) |
|
|
83
|
+
| `dbPath` | `''` → `$DSH_HOME/dsh-memento/memory.db` | 绝对路径,或相对 `$DSH_HOME` |
|
|
84
|
+
| `budgets.user.userGlobal` / `budgets.user.workspace` | `2000` / `2000` | user 轨每层硬字符预算 |
|
|
85
|
+
| `budgets.agent.userGlobal` / `budgets.agent.workspace` | `4000` / `4000` | agent 轨每层硬字符预算 |
|
|
86
|
+
| `writePolicy` | `'ask'` | `'ask'`=用户审批;`'auto'`=放行但记录审批来源;`'off'`=拒绝。模型不可见 |
|
|
87
|
+
| `writePolicies` | `{}` | 按 track/scope 或来源覆盖:键 `user/workspace`、`agent/user-global`、`source:claude` 等 → `ask`/`auto`/`off`;未命中回退 `writePolicy` |
|
|
88
|
+
| `language` | `'en'` | 模型可见文案与命令输出语言:`'en'`(默认)或 `'zh'`——工具描述、冻结快照、`/memory` 命令、Web 面板全部跟随 |
|
|
89
|
+
| `snapshotOrder` | `-50` | 快照段注入顺序:harness identity(`-100`) 之后、persona(`0`) 之前 |
|
|
90
|
+
| `maxEntriesPerQuery` | `20` | query 默认返回上限(显式 `limit` 可超出,硬钳 1000) |
|
|
91
|
+
| `commandListLimit` | `50` | `/memory list`/`query` 命令单次渲染条目上限 |
|
|
92
|
+
| `commandAuditLimit` | `10` | `/memory audit` 命令单次渲染审计行上限 |
|
|
93
|
+
| `recall.historyLimitDefault` / `recall.snippetCap` / `recall.snippetChars` / `recall.windowDays` | `8` / `5` / `300` / `30` | `memory_recall` 历史段默认值:扫描会话数 / 每会话片段数 / 片段字符数 / 回溯天数 |
|
|
94
|
+
| `panelEntriesLimit` | `200` | Web 面板条目页大小(兼钳制上限) |
|
|
95
|
+
| `panelAuditLimit` | `20` | Web 面板审计默认条数(天花板 200) |
|
|
96
|
+
| `auditRetentionDays` | `0` | 审计保留天数:0 = 永久,>0 = 打开库时裁剪更早的审计行 |
|
|
97
|
+
| `proposals.enabled` / `proposals.maxChars` / `proposals.maxPending` | `true` / `2000` / `8` | auto-capture:每次压缩成功后生成待审批记忆提案(截断、每会话一条);可关闭或调上限 |
|
|
98
|
+
|
|
99
|
+
## 🛠 工具与观察面
|
|
100
|
+
|
|
101
|
+
- **`memory`** —— add/replace/remove/consolidate/query,工具描述内嵌 Save/Skip 行为指引(存用户偏好、纠正、环境事实、项目约定、教训;跳过琐碎事实、可再查的百科知识、大数据转储、一次性路径)。写走审批门,读免费;replace/remove 用**唯一子串**定位(歧义时报候选清单);consolidate 一次审批 + 一次原子写把 1..20 条整合为一条。
|
|
102
|
+
- **`memory_recall`** —— 两段式召回:有界记忆匹配 **+** 经 `ctx.sessionQuery` 的近期会话历史匹配(服务缺失时优雅降级为纯记忆结果)。
|
|
103
|
+
- **`/memory`** —— 用户触发命令(非模型回合):`list` · `query <词>` · `add [--track=user|agent] [--scope=user-global|workspace] <文本>` · `remove [选项] <子串>` · `consolidate [选项] <子串...> => <新文本>` · `proposals [approve|dismiss <id>]` · `budgets` · `audit` · `export`。命令写走同一 waterfall 与策略;审计落插件审计表 + `command/done`。`export` 只读,把所有条目 + 预算导出为一份 JSON 文档(备份/迁移)。
|
|
104
|
+
- **Auto-capture 提案** —— 会话压缩成功后,摘要落为待审批记忆提案(`agent/workspace`);approve 经审批门写入记忆,dismiss 丢弃。待审批提案出现在冻结快照与面板中。
|
|
105
|
+
- **Web 面板** —— 零构建 `dsh.client` 抽屉:按轨/层浏览条目、搜索、预算用量条、审计尾。设计上只读:写与审批走 `memory` 工具与内置审批 UI。
|
|
106
|
+
|
|
107
|
+
## 🎓 向终端记忆们学到的优点
|
|
108
|
+
|
|
109
|
+
dsh-memento 不是 Claude Code、Codex 或 Hermes 的移植版——但它的设计刻意吸收了这三家各自做对的部分,也刻意拒绝了它们吃过的亏:
|
|
110
|
+
|
|
111
|
+
| 终端记忆 | 做对了什么 | dsh-memento 吸收了 |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| **Claude Code** —— `CLAUDE.md` | 分层**纯文本记忆文件**(用户级 → 项目级),人可读、人可改,每个会话自动合并——你能自己读、自己修的记忆 | 纯文本条目;每会话合并 `user-global` / `workspace` 层;可浏览、可 `export`、可审计的库——透明即特性 |
|
|
114
|
+
| **Codex** —— `AGENTS.md` | **按目录作用域的指令**自动发现、零摩擦注入——就近比堆量更重要,加载记忆无需任何工具调用 | `workspace` 层按会话 cwd 隔离(Windows 大小写不敏感);冻结快照会话启动时自动注入 |
|
|
115
|
+
| **Hermes** —— `memory.md` | **主动记忆保存**(存/改/删),以及 [issue #48181](https://github.com/NousResearch/hermes-agent/issues/48181) 的安全教训:只做在工具层的门会被迟到的工具注入绕过——应把门做在所有写路径的交汇处 | 内嵌 Save/Skip 指引的 `memory` 工具 + 走审批门的 auto-capture 提案;审批门做在 **`ctx.memory` 写方法内部**,不在工具层 |
|
|
116
|
+
|
|
117
|
+
出处:[Claude Code 记忆](https://code.claude.com/docs/en/memory) · [Codex AGENTS.md](https://developers.openai.com/codex/cli/agents-md) · [Hermes 记忆](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/memory.md) · [Hermes #48181](https://github.com/NousResearch/hermes-agent/issues/48181)。
|
|
118
|
+
|
|
119
|
+
刻意拒绝的部分:把自动摘要写进模型私有状态的隐藏记忆(本插件把压缩摘要变成**待审批提案**,等人 approve/dismiss)、仓库/向量库野心、以及任何缺少人可见审批或审计痕迹的写入。同时采纳 Hermes 文档的警告:两个进程共用一个 home 目录会写同一份记忆文件——见安全边界。
|
|
120
|
+
|
|
121
|
+
## 🆚 与其它记忆插件的差异
|
|
122
|
+
|
|
123
|
+
| 插件 | 是什么 | dsh-memento 的差异 |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| dsh-memory-evolve | 记忆仓库 / 演化循环 | 类型化服务接缝、审批门、会话日志审计;不碰仓库野心 |
|
|
126
|
+
| dsh-mnemon | 记忆存储助手 | 协议 + 门 + 审计,不是又一个 store |
|
|
127
|
+
| dsh-kb-sieve | 知识库筛选 | 不重造检索:小语料子串检索,跨会话回忆用 `session_search`/`sessionQuery` |
|
|
128
|
+
| dsh-tdai-memory | 任务驱动记忆工具 | 预算是每轨×每层硬约束且在服务层执行,不是尽力而为 |
|
|
129
|
+
| claude-bridge | Claude Code 桥接 | DSH 原生;未来的 `seed(source:'claude')` 让桥接插件喂同一个 store |
|
|
130
|
+
| dsh-external/Recall | 外部 agent 记忆 | 本地优先、零网络,走 DSH 自己的审批 seam |
|
|
131
|
+
| 官方 MCP 记忆示例 | 官方"记忆 = 外接 MCP"立场 | **原生第一方**补充:目标一致、无需外部服务,两者可共存 |
|
|
132
|
+
|
|
133
|
+
命名已定 **`dsh-memento`**(npm 与 GitHub 均空闲)。不用 `dsh-recall`(与 dsh-external/Recall 混淆),不用已删除的旧名 `dsh-memory`。
|
|
134
|
+
|
|
135
|
+
## 🔒 安全边界
|
|
136
|
+
|
|
137
|
+
- **只消费公开服务**(`tools`、`systemPrompt`、审批 seam)。不修改引擎 / agent-loop / apiproxy / 官方 UI 包。
|
|
138
|
+
- **零网络、零凭据。** 库在本地,POSIX 文件权限 `0600`。
|
|
139
|
+
- **失败大声。** 库损坏/版本过新加载期报错;写满与子串歧义报结构化错误。绝不静默吞、绝不静默截断。
|
|
140
|
+
- **单进程共库。** 单进程多会话共享 SQLite(串行写、每会话审计独立)。两个**进程**共用一个 `$DSH_HOME` 会写同一个库文件:SQLite 锁下"谁后写谁赢"——不要对同一 `$DSH_HOME` 跑两个 harness 实例(与 Hermes 项目文档的官方警告一致)。
|
|
141
|
+
|
|
142
|
+
## ⚠️ 已知局限
|
|
143
|
+
|
|
144
|
+
- **会话事件词汇已声明、rc.6 上暂不派发。** `memory/added|updated|removed|recalled|snapshot` 已在 `types.d.ts` 声明合并,但 rc.6 没有仓外插件事件类型的注册面(append 未注册类型会让持久化会话无法加载)。审计完整性由审批审计对 + 审计表承担;harness 收录这些类型后自动开启派发。见 [ARCHITECTURE.md](ARCHITECTURE.md) 决策 4。
|
|
145
|
+
- **`ask` 策略需要 answerer。** 没有 UI/ACP answerer 组合时写失败封闭(`unavailable`)——这是审批 seam 的失败封闭姿态,属设计行为。
|
|
146
|
+
- **不使用 FTS5 索引。** 子串检索走大小写不敏感 `instr`(对 CJK 正确);召回排序用逐条目命中计数。FTS5 的 trigram 分词器无法索引单字 CJK 字符,故不采用——见 [ARCHITECTURE.md](ARCHITECTURE.md) 决策 10。
|
|
147
|
+
|
|
148
|
+
## 🧪 开发
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
npm install
|
|
152
|
+
npm test # node --test:103 个测试——预算、唯一子串、审批策略、store、快照、mock ctx 集成(S2/S3 不变量)、V2 命令/召回/面板
|
|
153
|
+
npm run typecheck # tsc --checkJs 类型检查门(index.mjs / lib / scripts)
|
|
154
|
+
npm run check:coverage # 行覆盖率门:lib ≥90%、index.mjs ≥85%、全部 ≥90%
|
|
155
|
+
npm run check:readmes # 五语 README 一致性门
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`lib/` 零 DSH 依赖(仅 node: 内置模块);DSH 依赖只出现在 `index.mjs`。完整纪律见 [AGENTS.md](AGENTS.md);设计决策见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
159
|
+
|
|
160
|
+
## 🏷 话题
|
|
161
|
+
|
|
162
|
+
建议的 GitHub topics:`dsh` · `dsh-plugin` · `deepseek-harness` · `memory` · `agent-memory` · `approval` · `audit` · `sqlite` · `cordis` · `llm`
|
|
163
|
+
|
|
164
|
+
## 📄 许可证
|
|
165
|
+
|
|
166
|
+
Apache License 2.0——见 [LICENSE](LICENSE)。不分发任何第三方代码;见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
dsh-memento reuses no third-party source code.
|
|
4
|
+
|
|
5
|
+
Runtime dependencies are the Node.js built-in modules only (`node:sqlite`,
|
|
6
|
+
`node:fs`, `node:path`, `node:crypto`, `node:os`). The DeepSeek Harness
|
|
7
|
+
packages listed in `peerDependencies` (`@deepseek-ai/cordis`,
|
|
8
|
+
`@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-session`,
|
|
9
|
+
`@deepseek-ai/schemastery`) are provided by the harness installation itself
|
|
10
|
+
and are not bundled or redistributed here.
|
|
11
|
+
|
|
12
|
+
The memory Save/Skip guidance embedded in the `memory` tool description
|
|
13
|
+
follows the editorial structure of the public memory-usage guidance from the
|
|
14
|
+
Hermes project's MEMORY.md (used as a behavioral reference only; no code or
|
|
15
|
+
text was copied).
|
package/client/client.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// client/client.js — dsh-memento 浏览器观察面板(F9,零构建 vanilla)。
|
|
3
|
+
//
|
|
4
|
+
// host 端 dsh.client 扫描把本文件作为 classic script 注入 __DSH_BOOT__ 图,
|
|
5
|
+
// 执行时经 window.__ModuleLoader__.load 注册工厂;apply 挂载浮层抽屉面板。
|
|
6
|
+
// 面板只读:条目浏览/搜索/预算条/审计尾,全部走本插件自注册的
|
|
7
|
+
// /api/memento/* JSON 路由(只走公开 API)。写与审批在 DSH 内置审批 UI 完成,
|
|
8
|
+
// 面板不产生任何模型可见内容、不做任何审批决策。
|
|
9
|
+
// 面板文案随 Config.language(en/zh)切换,语言来自 entries 路由响应。
|
|
10
|
+
|
|
11
|
+
(function () {
|
|
12
|
+
'use strict'
|
|
13
|
+
if (typeof window === 'undefined' || !window.__ModuleLoader__ || !window.__ModuleLoader__.load) return
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: 'dsh-memento',
|
|
16
|
+
factory: function () {
|
|
17
|
+
return {
|
|
18
|
+
name: 'memento-panel',
|
|
19
|
+
inject: [],
|
|
20
|
+
apply: function () { installPanel() },
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
})()
|
|
25
|
+
|
|
26
|
+
const PANEL_ID = 'dsh-memento-panel'
|
|
27
|
+
|
|
28
|
+
/** 面板文案(en 源文 / zh 译文;语言来自 /api/memento/entries 响应的 language 字段,缺省 en)。 */
|
|
29
|
+
const STRINGS = {
|
|
30
|
+
en: {
|
|
31
|
+
open: '🧠 Memory',
|
|
32
|
+
title: 'dsh-memento memory',
|
|
33
|
+
refresh: 'Refresh',
|
|
34
|
+
close: 'Close',
|
|
35
|
+
filter: 'Filter entries by text…',
|
|
36
|
+
empty: 'Memory is empty (or nothing matches the filter). Use the memory tool to write (approval happens in the built-in approval UI).',
|
|
37
|
+
truncated: (shown, total) => `Showing the first ${shown} of ${total} entries — narrow the filter to see more.`,
|
|
38
|
+
groupCount: (n) => `(${n})`,
|
|
39
|
+
budgets: 'Budget usage',
|
|
40
|
+
audit: 'Recent audit',
|
|
41
|
+
loading: 'Loading…',
|
|
42
|
+
auditEmpty: 'Audit is empty',
|
|
43
|
+
proposals: 'Pending proposals',
|
|
44
|
+
proposalsEmpty: 'No pending proposals (generated after session compaction; decide via /memory proposals approve|dismiss)',
|
|
45
|
+
loadFailed: (message) => `Load failed: ${message} (panel is read-only; make sure the Web profile has dsh-memento loaded)`,
|
|
46
|
+
},
|
|
47
|
+
zh: {
|
|
48
|
+
open: '🧠 记忆',
|
|
49
|
+
title: 'dsh-memento 记忆',
|
|
50
|
+
refresh: '刷新',
|
|
51
|
+
close: '关闭',
|
|
52
|
+
filter: '按文本过滤条目…',
|
|
53
|
+
empty: '记忆为空(或没有匹配当前过滤)。写操作请用 memory 工具(审批在 DS 内置审批 UI 完成)。',
|
|
54
|
+
truncated: (shown, total) => `仅显示前 ${shown} 条,共 ${total} 条——用过滤框缩小范围。`,
|
|
55
|
+
groupCount: (n) => `(${n} 条)`,
|
|
56
|
+
budgets: '预算用量',
|
|
57
|
+
audit: '最近审计',
|
|
58
|
+
loading: '加载中…',
|
|
59
|
+
auditEmpty: '审计为空',
|
|
60
|
+
proposals: '待审批提案',
|
|
61
|
+
proposalsEmpty: '暂无待审批提案(会话压缩后自动生成;用 /memory proposals approve|dismiss 处理)',
|
|
62
|
+
loadFailed: (message) => `加载失败:${message}(面板只读;请确认 Web profile 已装载 dsh-memento)`,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installPanel() {
|
|
67
|
+
if (document.getElementById(PANEL_ID)) return
|
|
68
|
+
let S = STRINGS.en
|
|
69
|
+
|
|
70
|
+
const style = document.createElement('style')
|
|
71
|
+
style.textContent = `
|
|
72
|
+
#dsh-memento-panel { position: fixed; z-index: 2147483000; font: 13px/1.5 system-ui, "Segoe UI", sans-serif; }
|
|
73
|
+
#mem-open { position: fixed; right: 16px; bottom: 56px; z-index: 2147483000; padding: 8px 14px; border: 1px solid #3f6fae;
|
|
74
|
+
border-radius: 999px; background: #0d1526; color: #7db4ff; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,.35); }
|
|
75
|
+
#mem-open:hover { background: #16233c; }
|
|
76
|
+
#mem-drawer { position: fixed; right: 0; top: 0; bottom: 0; width: 460px; max-width: 92vw; display: flex; flex-direction: column;
|
|
77
|
+
background: #101827; color: #d7e2f2; border-left: 1px solid #24344d; box-shadow: -8px 0 24px rgba(0,0,0,.4); }
|
|
78
|
+
#mem-head { padding: 10px 12px; border-bottom: 1px solid #24344d; display: flex; gap: 8px; align-items: center; }
|
|
79
|
+
#mem-head b { flex: 1; }
|
|
80
|
+
#mem-head button { background: #1c2a42; color: #cfe1f7; border: 1px solid #2f4466; border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
81
|
+
#mem-head button:hover { background: #27395a; }
|
|
82
|
+
#mem-filter { margin: 8px 12px; padding: 6px 8px; background: #0b1120; color: #d7e2f2; border: 1px solid #24344d; border-radius: 6px; }
|
|
83
|
+
#mem-body { flex: 1; overflow: auto; padding: 0 12px 12px; }
|
|
84
|
+
.mem-group { margin: 10px 0 4px; color: #8fb4e8; }
|
|
85
|
+
.mem-entry { padding: 6px 8px; margin: 3px 0; border: 1px solid #24344d; border-radius: 8px; background: #131e33; }
|
|
86
|
+
.mem-entry .t { display: block; color: #e6eefb; }
|
|
87
|
+
.mem-entry .m { display: block; color: #9fb4d4; font-size: 11px; }
|
|
88
|
+
.mem-bar { height: 6px; margin: 6px 0 2px; background: #0b1120; border-radius: 3px; overflow: hidden; border: 1px solid #24344d; }
|
|
89
|
+
.mem-bar i { display: block; height: 100%; background: #3f6fae; }
|
|
90
|
+
.mem-row { margin: 6px 0; color: #9fb4d4; }
|
|
91
|
+
.mem-audit { margin: 6px 0; padding: 4px 8px; border-left: 2px solid #3f6fae; color: #9fb4d4; font-size: 12px; }
|
|
92
|
+
.mem-empty { color: #8fa8cc; margin: 10px 0; }
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
const openBtn = document.createElement('button')
|
|
96
|
+
openBtn.id = 'mem-open'
|
|
97
|
+
openBtn.textContent = S.open
|
|
98
|
+
|
|
99
|
+
const root = document.createElement('div')
|
|
100
|
+
root.id = PANEL_ID
|
|
101
|
+
const drawer = document.createElement('div')
|
|
102
|
+
drawer.id = 'mem-drawer'
|
|
103
|
+
drawer.style.display = 'none'
|
|
104
|
+
drawer.innerHTML = `
|
|
105
|
+
<div id="mem-head"><b id="mem-title">${S.title}</b>
|
|
106
|
+
<button id="mem-refresh" title="${S.refresh}">${S.refresh}</button>
|
|
107
|
+
<button id="mem-close" title="${S.close}">✕</button>
|
|
108
|
+
</div>
|
|
109
|
+
<input id="mem-filter" placeholder="${S.filter}" />
|
|
110
|
+
<div id="mem-body"></div>
|
|
111
|
+
`
|
|
112
|
+
root.appendChild(openBtn)
|
|
113
|
+
root.appendChild(drawer)
|
|
114
|
+
document.body.appendChild(root)
|
|
115
|
+
document.head.appendChild(style)
|
|
116
|
+
|
|
117
|
+
const body = document.getElementById('mem-body')
|
|
118
|
+
const filter = document.getElementById('mem-filter')
|
|
119
|
+
const titleLabel = document.getElementById('mem-title')
|
|
120
|
+
const refreshBtn = document.getElementById('mem-refresh')
|
|
121
|
+
const closeBtn = document.getElementById('mem-close')
|
|
122
|
+
const filterInput = document.getElementById('mem-filter')
|
|
123
|
+
|
|
124
|
+
/** 按服务端 language 切换文案并刷新静态标签(语言随配置,运行期不变)。 */
|
|
125
|
+
const applyLanguage = (language) => {
|
|
126
|
+
S = STRINGS[language] ?? STRINGS.en
|
|
127
|
+
openBtn.textContent = S.open
|
|
128
|
+
titleLabel.textContent = S.title
|
|
129
|
+
refreshBtn.title = S.refresh
|
|
130
|
+
refreshBtn.textContent = S.refresh
|
|
131
|
+
closeBtn.title = S.close
|
|
132
|
+
filterInput.placeholder = S.filter
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let entries = []
|
|
136
|
+
let lastFilter = ''
|
|
137
|
+
let lastTotal = 0
|
|
138
|
+
let lastTruncated = false
|
|
139
|
+
|
|
140
|
+
const render = () => {
|
|
141
|
+
const visible = entries.filter((entry) => entry.text.toLowerCase().includes(lastFilter.toLowerCase()))
|
|
142
|
+
const groups = new Map()
|
|
143
|
+
for (const entry of visible) {
|
|
144
|
+
const key = `${entry.track}/${entry.scope}`
|
|
145
|
+
const list = groups.get(key)
|
|
146
|
+
if (list === undefined) groups.set(key, [entry])
|
|
147
|
+
else list.push(entry)
|
|
148
|
+
}
|
|
149
|
+
if (visible.length === 0) {
|
|
150
|
+
body.innerHTML = `<div class="mem-empty">${S.empty}</div>`
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
let html = lastTruncated
|
|
154
|
+
? `<div class="mem-empty">${S.truncated(entries.length, lastTotal)}</div>`
|
|
155
|
+
: ''
|
|
156
|
+
for (const [key, list] of groups) {
|
|
157
|
+
html += `<div class="mem-group">${key}${S.groupCount(list.length)}</div>`
|
|
158
|
+
for (const entry of list) {
|
|
159
|
+
html += `<div class="mem-entry"><span class="t">${escapeHtml(entry.text)}</span><span class="m">${escapeHtml(entry.source)} · ${new Date(entry.createdAt).toLocaleString()}</span></div>`
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
body.innerHTML = html
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const renderBudget = (budgets) => {
|
|
166
|
+
if (!Array.isArray(budgets) || budgets.length === 0) return
|
|
167
|
+
let html = `<div class="mem-group">${S.budgets}</div>`
|
|
168
|
+
for (const row of budgets) {
|
|
169
|
+
const pct = row.limit > 0 ? Math.min(100, Math.round((row.used / row.limit) * 100)) : 0
|
|
170
|
+
html += `<div class="mem-row">${row.track}/${row.scope}: ${row.used}/${row.limit}<div class="mem-bar"><i style="width:${pct}%"></i></div></div>`
|
|
171
|
+
}
|
|
172
|
+
html += `<div class="mem-group">${S.audit}</div>`
|
|
173
|
+
html += `<div id="mem-audit-slot"><div class="mem-empty">${S.loading}</div></div>`
|
|
174
|
+
html += `<div class="mem-group">${S.proposals}</div>`
|
|
175
|
+
html += `<div id="mem-proposal-slot"><div class="mem-empty">${S.loading}</div></div>`
|
|
176
|
+
body.insertAdjacentHTML('beforeend', html)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const renderAudit = (rows) => {
|
|
180
|
+
const slot = document.getElementById('mem-audit-slot')
|
|
181
|
+
if (slot === null) return
|
|
182
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
183
|
+
slot.innerHTML = `<div class="mem-empty">${S.auditEmpty}</div>`
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
slot.innerHTML = rows.map((row) =>
|
|
187
|
+
`<div class="mem-audit">${new Date(row.ts).toLocaleString()} ${escapeHtml(row.action)}${row.track ? ` ${escapeHtml(row.track)}/${escapeHtml(row.scope)}` : ''} · ${escapeHtml(row.outcome ?? '')} · ${escapeHtml(row.source ?? '')}</div>`).join('')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const renderProposals = (proposals) => {
|
|
191
|
+
const slot = document.getElementById('mem-proposal-slot')
|
|
192
|
+
if (slot === null) return
|
|
193
|
+
if (!Array.isArray(proposals) || proposals.length === 0) {
|
|
194
|
+
slot.innerHTML = `<div class="mem-empty">${S.proposalsEmpty}</div>`
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
slot.innerHTML = proposals.map((proposal) =>
|
|
198
|
+
`<div class="mem-audit">[${escapeHtml(proposal.id)}] ${escapeHtml(proposal.track)}/${escapeHtml(proposal.scope)} · ${escapeHtml(proposal.text.length > 160 ? `${proposal.text.slice(0, 160)}…` : proposal.text)}</div>`).join('')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const refresh = async () => {
|
|
202
|
+
try {
|
|
203
|
+
const response = await fetch('/api/memento/entries?limit=200')
|
|
204
|
+
if (!response.ok) throw new Error(`entries ${response.status}`)
|
|
205
|
+
const data = await response.json()
|
|
206
|
+
if (data.error !== undefined) throw new Error(data.error)
|
|
207
|
+
applyLanguage(data.language)
|
|
208
|
+
entries = Array.isArray(data.entries) ? data.entries : []
|
|
209
|
+
lastTotal = Number.isInteger(data.total) ? data.total : entries.length
|
|
210
|
+
lastTruncated = data.truncated === true
|
|
211
|
+
render()
|
|
212
|
+
renderBudget(data.budgets)
|
|
213
|
+
void fetch('/api/memento/audit?limit=20')
|
|
214
|
+
.then((res) => res.json())
|
|
215
|
+
.then((audit) => renderAudit(audit.rows))
|
|
216
|
+
.catch(() => renderAudit([]))
|
|
217
|
+
void fetch('/api/memento/proposals')
|
|
218
|
+
.then((res) => res.json())
|
|
219
|
+
.then((data) => renderProposals(data.proposals))
|
|
220
|
+
.catch(() => renderProposals([]))
|
|
221
|
+
} catch (error) {
|
|
222
|
+
body.innerHTML = `<div class="mem-empty">${S.loadFailed(String(error && error.message ? error.message : error))}</div>`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
openBtn.addEventListener('click', () => {
|
|
227
|
+
drawer.style.display = drawer.style.display === 'none' ? 'flex' : 'none'
|
|
228
|
+
if (drawer.style.display !== 'none') void refresh()
|
|
229
|
+
})
|
|
230
|
+
closeBtn.addEventListener('click', () => { drawer.style.display = 'none' })
|
|
231
|
+
refreshBtn.addEventListener('click', () => void refresh())
|
|
232
|
+
filter.addEventListener('input', () => {
|
|
233
|
+
lastFilter = filter.value
|
|
234
|
+
render()
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function escapeHtml(value) {
|
|
239
|
+
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
|
240
|
+
}
|
package/cordis.patch.yml
ADDED