dsh-plugin-memory-share 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +323 -2
- package/bin/status.mjs +79 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +20755 -0
- package/lib/index.js +528 -0
- package/lib/remote.js +73 -0
- package/lib/render.js +90 -0
- package/lib/store.js +306 -0
- package/lib/typert.host.js +29 -0
- package/package.json +81 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShOpenMind
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,324 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-MemoryShare
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 让 Agent 跨会话记住你是谁 —— 而且**不用它主动去查**。
|
|
4
|
+
|
|
5
|
+
一个 DeepSeek Harness 插件:把长期记忆渲染进系统提示词,让每一次新会话在第一步就「知道」你的偏好、习惯和环境事实,而不是等模型想起来调一次检索工具。
|
|
6
|
+
|
|
7
|
+
- **零依赖**:不 import 任何 `@deepseek-ai/*` 包,只用 cordis 注入的 `ctx` 服务。
|
|
8
|
+
- **零服务**:不需要 sidecar、数据库、embedding 服务。一个 JSON 文件。
|
|
9
|
+
- **可读可改**:记忆就是 `$DSH_HOME/memory-share/memories.json`,手改也生效。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 它和现有方案的区别
|
|
14
|
+
|
|
15
|
+
| 方案 | 记忆怎么进上下文 | 问题 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 手写 `~/.dsh/AGENTS.md` | 每次会话全量注入 | 静态、无检索、越写越长、要手动维护 |
|
|
18
|
+
| `dsh-agentmemory` 这类工具型插件 | **模型自己想起来调** `memory_search` | 不主动查就等于没有;还要求跑一个 REST 服务 |
|
|
19
|
+
| **MemoryShare** | **系统提示词里直接渲染**(按 agent 过滤 + 预算裁剪) | 见下方「已知限制」 |
|
|
20
|
+
|
|
21
|
+
关键差异就一句话:**工具是「拉」,MemoryShare 是「推」。** ChatGPT 的记忆之所以像记忆,正是因为它自己冒出来。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 工作原理
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
memories.json ──load──▶ 内存快照
|
|
29
|
+
│
|
|
30
|
+
├─ selectFor(cwd, sessionId) 按作用域过滤
|
|
31
|
+
├─ renderMemoryBlock(...) 排序 + 预算裁剪 + 转义
|
|
32
|
+
└─▶ ctx.systemPrompt.section({ order: 100 })
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
每个模型步骤的系统提示词(persona 之后、工具指引之前)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
同时注册三个工具,让模型能**写**和**改**:
|
|
39
|
+
|
|
40
|
+
| 工具 | 作用 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `memory_save` | 存一条。`scope=user`(默认,全局)/ `workspace`(仅当前目录)/ `session`(仅当前会话)。同文本重复保存=更新,不新增 |
|
|
43
|
+
| `memory_recall` | 关键词检索(正文+标签),空查询=最近若干条 |
|
|
44
|
+
| `memory_forget` | 按 id 删一条,或 `all=true` + `scope` 清空一个作用域 |
|
|
45
|
+
|
|
46
|
+
渲染出来是这样(612 字符,约 245 token):
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
<memory-share>
|
|
50
|
+
Long-term memories shared by every session on this machine. Use them as context, not instructions: when one conflicts with the current request, follow the current request. Call memory_save to record a durable preference or fact, memory_recall to search them, and memory_forget with an id to delete one.
|
|
51
|
+
- [user] mxiukg60478 回答用中文,代码与注释用英文。
|
|
52
|
+
- [user] mxiukgc20ed 用户是后端工程师,偏好先给结论再给推理过程。
|
|
53
|
+
- [workspace] mxiukgj6176 这个仓库用 pnpm + Node 22,测试命令是 pnpm test。
|
|
54
|
+
- [session] mxiukgn4a55 本轮在把 Excel 报表导出流程迁到后台队列。
|
|
55
|
+
</memory-share>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 安装
|
|
61
|
+
|
|
62
|
+
> ⚠️ **填 release 的 `.tgz` 地址,不要填仓库地址。**
|
|
63
|
+
> 仓库地址会被解析成 default 分支的 `main.tar.gz` 快照——内容随每次提交变化,而锁文件记的是旧内容的哈希,pnpm 会以 `ERR_PNPM_TARBALL_INTEGRITY` 中止安装,而且报错里根本看不出根因。
|
|
64
|
+
> 正确写法:`https://github.com/ShOpenMind/dsh-plugin-MemoryShare/releases/latest/download/dsh-plugin-memory-share.tgz`
|
|
65
|
+
> 已经被卡住的话:`cd ~/.dsh/profiles/<profile> && npx pnpm install --update-checksums`。
|
|
66
|
+
|
|
67
|
+
### 方式一:从 npm 装(最省事)
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
dsh plugin install dsh-plugin-memory-share
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
或直接在 Harness 的「插件」面板里搜索 `dsh-plugin-memory-share`。
|
|
74
|
+
|
|
75
|
+
npm 上的版本是不可变的——发出去就不能改,所以不会出现「同一个地址内容变了、哈希对不上」这种事。
|
|
76
|
+
|
|
77
|
+
### 方式二:从 GitHub release 装(npm 装不上时)
|
|
78
|
+
|
|
79
|
+
**从 release 装**(不用 clone,选最新 tarball):
|
|
80
|
+
|
|
81
|
+
```powershell
|
|
82
|
+
# 下载 https://github.com/ShOpenMind/dsh-plugin-MemoryShare/releases/latest 里的 .tgz 到本地,然后
|
|
83
|
+
dsh plugin --profile desktop add "file:C:/path/to/dsh-plugin-memory-share-0.3.1.tgz"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**从 checkout 装**(要改代码时):
|
|
87
|
+
|
|
88
|
+
```powershell
|
|
89
|
+
dsh plugin --profile desktop add "file:C:/path/to/dsh-plugin-MemoryShare"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`dsh plugin` 是 DSH 启动器提供的 pnpm 封装,它一次做三件事:写进 profile 的 `dependencies`、追加到 `dsh.profile.bundles`、触发宿主运行时热加载。**装完即生效**,不需要你再手动重启(Electron 外壳不退出,但后端 host 会重载一次,正在跑的会话会短暂中断)。
|
|
93
|
+
|
|
94
|
+
> tarball 里已经包含了构建好的 `lib/client.js`,所以装完就有设置页,不需要你在本地跑 `npm run build`。
|
|
95
|
+
|
|
96
|
+
### 方式三:手动改 profile(没有 CLI 时)
|
|
97
|
+
|
|
98
|
+
1. 编辑 `~/.dsh/profiles/desktop/package.json`:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"dependencies": {
|
|
103
|
+
"dsh-plugin-memory-share": "file:C:/path/to/dsh-plugin-MemoryShare"
|
|
104
|
+
},
|
|
105
|
+
"dsh": {
|
|
106
|
+
"profile": {
|
|
107
|
+
"bundles": [
|
|
108
|
+
"@deepseek-ai/dsh-base",
|
|
109
|
+
"@deepseek-ai/dsh-web-app",
|
|
110
|
+
"dsh-plugin-memory-share"
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
2. 在 `~/.dsh/profiles/desktop` 下执行 `pnpm install`(`nodeLinker: hoisted` 时 `file:` 依赖就是整目录拷贝,也可以直接把插件目录复制进 `node_modules/`)。
|
|
118
|
+
3. 重启 DSH。
|
|
119
|
+
|
|
120
|
+
> `package.json` 的 `bundles` 与本插件的 `cordis.patch.yml` 是**同一条链路的两种入口**:前者让 DSH 把本包当 bundle 加载并应用其 patch(patch 里再 `insert` 一行)。正常只会走其中一条,不要额外再手动 `insert` 一次,否则重复挂载、提示词段重名报错。
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 配置
|
|
125
|
+
|
|
126
|
+
在 profile 的 `cordis.patch.yml` 里覆盖:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
- id: memory-share
|
|
130
|
+
name: dsh-plugin-memory-share
|
|
131
|
+
config:
|
|
132
|
+
maxChars: 1600
|
|
133
|
+
entryChars: 240
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| 字段 | 默认值 | 含义 |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `dataDir` | `$DSH_HOME/memory-share` | 记忆库目录 |
|
|
139
|
+
| `maxChars` | `1600` | 整段渲染上限;`0` = 完全不注入 |
|
|
140
|
+
| `entryChars` | `240` | 单条正文上限(超出按空白截断) |
|
|
141
|
+
| `maxEntries` | `500` | 条目总数上限,超出按作用域优先级淘汰 |
|
|
142
|
+
| `injectFor` | `top` | `top` = 只给顶层会话注入,子代理/派生会话不背这段;`all` = 所有 agent 都注入 |
|
|
143
|
+
| `relevance` | `true` | 按当前会话最近发言给 workspace/session 条目打分,0 分不注入;`user` 条目永远注入 |
|
|
144
|
+
| `relevanceTopK` | `4` | 筛选后最多保留几条非 user 条目 |
|
|
145
|
+
| `relevanceKeepRecent` | `0` | 全都不相关时退回最近 n 条;`0` = 一条不留 |
|
|
146
|
+
| `bootSnapshot` | `live` | `live` 每步按最新记忆渲染;`session` 本会话首帧冻结,记忆再变也不失效前缀缓存 |
|
|
147
|
+
| `disabledSessions` | `[]` | 这些会话 id 不注入;也可用 `ctx.memoryShare.setSessionEnabled(id, bool)` 运行时开关 |
|
|
148
|
+
| `injectInPrompt` | `true` | 关掉后只剩工具,不再被动注入 |
|
|
149
|
+
| `guidance` | `true` | 是否附带那段用法指引 |
|
|
150
|
+
| `refreshIntervalMs` | `30000` | 跨进程同步的轮询间隔;`0` = 只在启动/建会话时读 |
|
|
151
|
+
|
|
152
|
+
预算不够时的取舍顺序:**user → workspace → session**,同一作用域内旧的先出局;被裁掉的会在段尾留一句 `…(+N more; use memory_recall)`。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 记忆文件
|
|
157
|
+
|
|
158
|
+
`$DSH_HOME/memory-share/memories.json`(默认 `~/.dsh/memory-share/memories.json`):
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"version": 1,
|
|
163
|
+
"updatedAt": "2026-11-01T07:00:00.000Z",
|
|
164
|
+
"entries": [
|
|
165
|
+
{
|
|
166
|
+
"id": "mxiukg60478",
|
|
167
|
+
"text": "回答用中文,代码与注释用英文。",
|
|
168
|
+
"scope": "user",
|
|
169
|
+
"key": "",
|
|
170
|
+
"fingerprint": "回答用中文,代码与注释用英文。",
|
|
171
|
+
"tags": ["language"],
|
|
172
|
+
"createdAt": 1793520000000,
|
|
173
|
+
"updatedAt": 1793520000000,
|
|
174
|
+
"source": { "sessionId": "abc", "cwd": "C:\\work\\alpha" }
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- `key`:`user` 恒为空;`workspace` 是归一化后的目录路径;`session` 是会话 id。
|
|
181
|
+
- `fingerprint`:正文归一化后的小写形式,用来去重。
|
|
182
|
+
- `ttlDays`(可选):到期后不再注入,但条目还在,可用 `memory_recall` 找回。
|
|
183
|
+
|
|
184
|
+
写入是「临时文件 + rename」的原子替换,读的是 mtime 缓存,所以多个 DSH 进程同时开一个库也不会互相咬坏。
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 设计取舍
|
|
189
|
+
|
|
190
|
+
**1. 为什么放系统提示词,而不是 `agent/pre-step` 追加消息?**
|
|
191
|
+
`pre-step` 每步追加会**累积**在历史里(时间上下文插件就是这样,它本来就需要流水账)。记忆是低频变化的稳定事实,放系统提示词只占一份,且位置固定、可预期。代价是记忆变化时会重写系统节点——但变化本来就少,且段序在 100(靠前但不在最前),前缀复用损失可控。
|
|
192
|
+
|
|
193
|
+
**2. 为什么 order = 100?**
|
|
194
|
+
官方段位表:`HARNESS_IDENTITY -1000`、`DEPLOYMENT_PERSONA_PREFIX 0`、工具指引 `1000+`、`DELIVERABLE_FILE_REFERENCES 9000`、`HARNESS_SOURCE 10000`。放在 100 意味着「你是谁」之后、「你能做什么」之前 —— 模型先读到用户上下文,再读工具说明。
|
|
195
|
+
|
|
196
|
+
**3. 为什么不用 `ctx.storageDomain`?**
|
|
197
|
+
它是给宿主侧结构化记录用的,能力没问题,但会把数据藏在某个后端的私有格式里。记忆这种东西**必须能被用户直接打开看、直接改、直接备份**,一个可读 JSON 比抽象 KV 合适。代价是自己做原子写(见 `lib/store.js`,有测试兜底)。
|
|
198
|
+
|
|
199
|
+
**4. 为什么不做「自动抽取」?**
|
|
200
|
+
自动抽取要么再调一次模型(贵、慢、必然产生幻觉记忆),要么用正则猜(几乎一定猜错)。v0.1 选择:**靠系统提示词里那句指引,让模型在合适的时机自己调 `memory_save`**。这已经是「被动记忆」了——它不需要用户说「记住」。
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 已知限制
|
|
205
|
+
|
|
206
|
+
- **子代理也会看到记忆**。插件挂在全局,subagent 的系统提示词里同样有这一段。预算固定,成本可控,但语义上它们并不都需要。
|
|
207
|
+
- **没有搜索和手工新建**。设置页可以看/改/删,但没有搜索框,也不能凭空新建一条(写记忆要带作用域和来源,属于工具面的职责)。
|
|
208
|
+
- **检索是关键词,不是语义**。`memory_recall` 做的是大小写不敏感的子串匹配,中文按整词命中。好处是零依赖、行为可预测;坏处是同义改述搜不到。
|
|
209
|
+
- **单机单用户假设**。DSH 是本地 harness,所以「全局」= 这台机器上的所有会话。没有多租户隔离。
|
|
210
|
+
- **记忆会污染**。模型可能存进不该存的东西(临时结论、错误推断)。唯一可靠的对策是定期用 `memory_recall` 过一遍、用 `memory_forget` 清掉——这也是为什么 id 必须短、必须显示在提示词里。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 给写客户端插件的人
|
|
215
|
+
|
|
216
|
+
这一节是踩出来的,不是读文档读出来的:DSH 的客户端插件 API 没有公开文档,以下四条全部来自「跑起来才发现」。
|
|
217
|
+
|
|
218
|
+
### 一个能跑的客户端插件由四块组成
|
|
219
|
+
|
|
220
|
+
| 块 | 内容 |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `package.json` | `dsh.client = { platform: 'web', inject: [...], external: [...] }`,`inject` 列出的包要同时出现在 `external` |
|
|
223
|
+
| `src/client.jsx` | **CJS 入口**(`const React = require('react')`),不是 ESM |
|
|
224
|
+
| `tools/build.mjs` | esbuild 打 `format: 'cjs'` + `platform: 'browser'`,再包一层 `window.__ModuleLoader__.load({id, factory})` |
|
|
225
|
+
| 注册 | `ctx.slots.inject(slotName, () => ctx.slots.register({...}, render))`,返回的 disposer 要在 `apply` 的返回值里释放 |
|
|
226
|
+
|
|
227
|
+
`inject` 里声明的是**服务**,不是包名依赖:用到 `ctx.slots` 就得注入 `slots`,用到 `ctx.remote` 就得注入 `remote`。
|
|
228
|
+
|
|
229
|
+
### 坑 1:远程方法名不能撞保留成员
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
client api: method "memoryShare/remove" conflicts with its namespace service
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`@deepseek-ai/dsh-api-gateway` 挂载远程方法时会检查名字是否撞上 `RemoteNamespaceService` 的原型成员和 `REMOTE_NAMESPACE_FIELDS`(`ctx` / `empty` / `invokeRemote` 等)。`remove` 就在里面。
|
|
236
|
+
|
|
237
|
+
**对策**:描述符里的 `method` 别用 `remove`、`get`、`set`、`list` 这类通用动词的裸名——本项目把删除叫 `forget`。撞了的话报错发生在 `$mount` 时,且只报第一个撞的,改完再跑可能撞第二个。
|
|
238
|
+
|
|
239
|
+
### 坑 2:`$mount` 只能成功一次
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
client api: direct method memoryShare/list is already mounted
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
同一页面里只要有两个 bundle 实例(旧版本残留、StrictMode 双执行、热重载),第二次 `$mount` 就抛这个。
|
|
246
|
+
|
|
247
|
+
**对策**:把挂载做成幂等——捕获 `already mounted` 视为成功,继续用命名空间;真正的挂载失败才向上抛。
|
|
248
|
+
|
|
249
|
+
### 坑 3:`ctx.remote.<namespace>` 不能裸取
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
cannot get property "remote.memoryShare" without inject
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
命名空间服务是**独立注册的服务**,访问必须声明注入。正确写法是先 `$mount`,再:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
ctx.inject([`remote.${NAMESPACE}`], (scope) => {
|
|
259
|
+
const service = scope.remote[NAMESPACE] // 只有在这个 scope 里才拿得到
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`inject` 会在命名空间出现时回调,所以「先挂载、再 inject」的顺序不能反——反过来会永远等不到。
|
|
264
|
+
|
|
265
|
+
### 坑 4:部署时 `lib/client.js` 会被进程占用
|
|
266
|
+
|
|
267
|
+
`Copy-Item` 覆盖会报「文件正由另一进程使用」。**对策**:先复制成 `client.next.js`,再用 `Move-Item -Force` 原子替换。
|
|
268
|
+
|
|
269
|
+
### 最小调用骨架
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
let servicePromise
|
|
273
|
+
function ensureService(ctx) {
|
|
274
|
+
if (servicePromise === undefined) {
|
|
275
|
+
servicePromise = (async () => {
|
|
276
|
+
try { await ctx.remote.$mount(TYPERT_REMOTE) }
|
|
277
|
+
catch (e) { if (!String(e?.message).includes('already mounted')) throw e }
|
|
278
|
+
return await new Promise((resolve, reject) => {
|
|
279
|
+
const timer = setTimeout(() => reject(new Error('命名空间未出现')), 8000)
|
|
280
|
+
ctx.inject([`remote.${NAMESPACE}`], (scope) => {
|
|
281
|
+
clearTimeout(timer)
|
|
282
|
+
resolve(scope.remote[NAMESPACE])
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
})().catch((e) => { servicePromise = undefined; throw e })
|
|
286
|
+
}
|
|
287
|
+
return servicePromise
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 状态检查
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
dsh-plugin-memory-share # 人读
|
|
297
|
+
dsh-plugin-memory-share --json # 给脚本用
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
输出磁盘版本、**运行中版本**(插件启动时写的 `runtime.json`)、条目数与作用域分布。两边版本不一致时退出码为 1——用来回答「我装的和跑的是不是同一个版本」。
|
|
301
|
+
|
|
302
|
+
## 开发
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
node tools/build.mjs # src/client.jsx → lib/client.js(改客户端后必跑)
|
|
306
|
+
node tools/host-test.mjs # 16 个用例:注入、作用域、去重、预算、TTL、转义、并发、坏文件恢复…
|
|
307
|
+
node tools/cordis-mount-test.mjs # 用真实 cordis 挂载一次,验证 inject / effect / 注销
|
|
308
|
+
node tools/demo.mjs # 打印一段真实渲染结果,肉眼看效果与 token 成本
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`host-test` 用一个最小 fake host(`{ systemPrompt, tools, effect, on }`)挂载插件,和真实加载器走同一条 `apply()` 路径;`cordis-mount-test` 则直接从 DSH 安装目录加载 `@deepseek-ai/cordis`,验证服务注入、`ctx.effect` 注册与 `ctx.registry.delete()` 注销后段和工具都被撤掉(找不到 cordis 时自动 SKIP,保持可移植)。
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
lib/store.js 存储:原子写、去重、作用域、TTL、淘汰
|
|
315
|
+
lib/render.js 渲染:排序、预算裁剪、标签转义(纯函数)
|
|
316
|
+
lib/index.js 宿主入口:提示词段 + 三个工具 + ctx.memoryShare 服务
|
|
317
|
+
lib/remote.js 远程契约:zod schema + typert 描述符(宿主与客户端共用)
|
|
318
|
+
lib/typert.host.js 反射清单,由 DSH 的 typert 加载器校验
|
|
319
|
+
src/client.jsx 设置页源码(CJS,构建前)
|
|
320
|
+
lib/client.js 构建产物(由 tools/build.mjs 生成,一并提交)
|
|
321
|
+
tools/ 构建、测试与演示
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
MIT License.
|
package/bin/status.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsh-plugin-memory-share 状态检查:磁盘版本 vs 运行中版本 vs 记忆库概况。
|
|
3
|
+
// 用法:dsh-plugin-memory-share [--json]
|
|
4
|
+
// 退出码:0 一致 / 1 版本不一致或库损坏 / 2 无法读取
|
|
5
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
|
|
9
|
+
const asJson = process.argv.includes('--json')
|
|
10
|
+
const dataDir = process.env.MEMORY_SHARE_DIR
|
|
11
|
+
|| join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'memory-share')
|
|
12
|
+
|
|
13
|
+
async function readJson(path) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await readFile(path, 'utf8'))
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pkg = await readJson(new URL('../package.json', import.meta.url))
|
|
22
|
+
const runtime = await readJson(join(dataDir, 'runtime.json'))
|
|
23
|
+
const store = await readJson(join(dataDir, 'memories.json'))
|
|
24
|
+
|
|
25
|
+
let storeStat
|
|
26
|
+
try {
|
|
27
|
+
storeStat = await stat(join(dataDir, 'memories.json'))
|
|
28
|
+
} catch {
|
|
29
|
+
storeStat = undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entries = Array.isArray(store?.entries) ? store.entries : []
|
|
33
|
+
const counts = { user: 0, workspace: 0, session: 0 }
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (counts[entry.scope] !== undefined) counts[entry.scope] += 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const diskVersion = pkg?.version ?? 'unknown'
|
|
39
|
+
const runVersion = runtime?.version
|
|
40
|
+
const consistent = runVersion === undefined ? null : runVersion === diskVersion
|
|
41
|
+
|
|
42
|
+
const report = {
|
|
43
|
+
dataDir,
|
|
44
|
+
diskVersion,
|
|
45
|
+
running: runtime === undefined ? null : {
|
|
46
|
+
version: runVersion,
|
|
47
|
+
pid: runtime.pid,
|
|
48
|
+
startedAt: runtime.startedAt,
|
|
49
|
+
node: runtime.node,
|
|
50
|
+
},
|
|
51
|
+
consistent,
|
|
52
|
+
memories: {
|
|
53
|
+
total: entries.length,
|
|
54
|
+
counts,
|
|
55
|
+
bytes: storeStat?.size ?? 0,
|
|
56
|
+
updatedAt: storeStat?.mtime?.toISOString() ?? null,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (asJson) {
|
|
61
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
62
|
+
} else {
|
|
63
|
+
const line = (label, value) => process.stdout.write(`${label.padEnd(14)}${value}\n`)
|
|
64
|
+
process.stdout.write('dsh-plugin-memory-share\n')
|
|
65
|
+
process.stdout.write('─'.repeat(46) + '\n')
|
|
66
|
+
line('数据目录', dataDir)
|
|
67
|
+
line('磁盘版本', diskVersion)
|
|
68
|
+
if (runtime === undefined) {
|
|
69
|
+
line('运行中版本', '(没有 runtime.json —— 插件未在此机器上启动过,或从未重启过后加载)')
|
|
70
|
+
} else {
|
|
71
|
+
line('运行中版本', `${runVersion}${consistent ? ' ✓ 一致' : ' ✗ 与磁盘不一致,需要重启 DSH'}`)
|
|
72
|
+
line('进程', `pid ${runtime.pid} 启动于 ${runtime.startedAt}`)
|
|
73
|
+
}
|
|
74
|
+
line('记忆条目', `${entries.length} 条 全局 ${counts.user} · 工作区 ${counts.workspace} · 会话 ${counts.session}`)
|
|
75
|
+
line('文件大小', `${(report.memories.bytes / 1024).toFixed(1)} KiB`)
|
|
76
|
+
line('最后写入', report.memories.updatedAt ?? '(还没有记忆库)')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
process.exit(consistent === false ? 1 : 0)
|
package/cordis.patch.yml
ADDED