dsh-lite-memory 0.2.3 → 0.2.5
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/README.md +4 -4
- package/TRIGGERS.md +9 -9
- package/client.js +54 -27
- package/index.js +29 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Lightweight memory for the DeepSeek Harness. Nothing in the system prompt, nothi
|
|
|
16
16
|
| 场景 | 行为 |
|
|
17
17
|
| --- | --- |
|
|
18
18
|
| **L0** 你明确要求「记住这个」 | 立即写盘,一条确认行 |
|
|
19
|
-
| **L1** 它发现有跨会话价值的经验 | 一行灰字
|
|
19
|
+
| **L1** 它发现有跨会话价值的经验 | 一行灰字 `memory · keep? "X" — 要记的内容` + 一个 `keep` 按钮;标题加一句话,看得懂才点,不点就永远不写,48 小时内不重复 |
|
|
20
20
|
| **L2** 常识、一次性参数、凭据的值 | 永不写入 |
|
|
21
21
|
|
|
22
22
|
触发词:`记住`、`记一下`、`上次`、`规范`、`约定`、`路径`、`凭据怎么取`。
|
|
@@ -24,14 +24,14 @@ Lightweight memory for the DeepSeek Harness. Nothing in the system prompt, nothi
|
|
|
24
24
|
| Case | Behaviour |
|
|
25
25
|
| --- | --- |
|
|
26
26
|
| **L0** you ask it to remember | written immediately, one confirmation line |
|
|
27
|
-
| **L1** it spots something worth keeping | one muted line
|
|
27
|
+
| **L1** it spots something worth keeping | one muted line `memory · keep? "X" — what would be remembered` with a single `keep` button — readable before you click, nothing written unless you click, at most once per two days |
|
|
28
28
|
| **L2** common sense, one-off parameters, credential values | never written |
|
|
29
29
|
|
|
30
30
|
## 点一下就存 / One click, no turn
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
`keep` 是同源直写:不产生消息、不动输入框、不打断当前回复。按钮变 `kept` 就结束了。自动写入的行尾有个 `✕`,点它就把刚才那条移入归档。
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
`keep` writes straight to disk from the page: no message, no extra model turn, the input box untouched. The button turns into `kept` and that is the whole interaction. An auto-saved line ends with `✕`: click it and that entry moves to the archive.
|
|
35
35
|
|
|
36
36
|
## 为什么轻 / Why it stays light
|
|
37
37
|
|
package/TRIGGERS.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
| 档 | 谁发起 | 什么时候 | 存不存 | 用户看到什么 |
|
|
6
6
|
| --- | --- | --- | --- | --- |
|
|
7
|
-
| L0 | 用户 | 说「记住 / 以后 / 别用 / 下次」这类话 | 直接存 |
|
|
8
|
-
| L1 | 模型 | 用户没说,但这经验跨会话有用 | 点了才存 |
|
|
7
|
+
| L0 | 用户 | 说「记住 / 以后 / 别用 / 下次」这类话 | 直接存 | `memory · saved "X"` + `✕` |
|
|
8
|
+
| L1 | 模型 | 用户没说,但这经验跨会话有用 | 点了才存 | `memory · keep "X"?` + `keep` 按钮 |
|
|
9
9
|
| L2 | — | 常识、一次性参数、临时路径 | 不存 | 无 |
|
|
10
10
|
|
|
11
11
|
## 什么时候触发
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
每次调用只占一行、灰色、12px:
|
|
23
23
|
|
|
24
24
|
```
|
|
25
|
-
·
|
|
26
|
-
·
|
|
27
|
-
·
|
|
28
|
-
·
|
|
29
|
-
·
|
|
30
|
-
·
|
|
25
|
+
· memory · 2 hits · 96 chars
|
|
26
|
+
· memory · no hits
|
|
27
|
+
· memory · saved "插件市场条目字段" — 目录条目要写应用名、一句话作用、图标地址 ✕
|
|
28
|
+
· memory · keep? "发布前先跑文案检查" — 界面文字只留标签、值和动作,交付前跑脚本清零 [keep]
|
|
29
|
+
· memory · skipped
|
|
30
|
+
· memory · dropped "插件市场条目字段"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
`keep` 按钮只在建议行出现。点一下直接写盘:不产生消息、不动输入框、不占用下一轮对话,按钮变 `kept` 并禁用。同源写盘通道不通时才退回旧路——插入 `保留记忆「X」` 并排队,至少不丢这条建议。自动写入的行尾带 `✕`,点了把那一条移入归档(不删文件,可从归档取回)。不点就什么都不发生,那行过一会儿只是一段灰色历史。
|
|
34
34
|
|
|
35
35
|
## 频率与退让
|
|
36
36
|
|
package/client.js
CHANGED
|
@@ -6,47 +6,63 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
|
|
7
7
|
/** One muted line per memory call: a summary, never the payload. */
|
|
8
8
|
const RUNNING = {
|
|
9
|
-
memory_save: '
|
|
10
|
-
memory_suggest: '
|
|
11
|
-
memory_search: '
|
|
12
|
-
memory_read: '
|
|
13
|
-
memory_forget: '
|
|
9
|
+
memory_save: 'memory · saving',
|
|
10
|
+
memory_suggest: 'memory · checking',
|
|
11
|
+
memory_search: 'memory · searching',
|
|
12
|
+
memory_read: 'memory · reading',
|
|
13
|
+
memory_forget: 'memory · dropping',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const KEEP = { idle: 'keep', saving: 'saving', saved: 'kept', kept: 'kept' };
|
|
17
17
|
const ROW = { display: 'flex', alignItems: 'center', gap: '6px', padding: '1px 0', fontSize: '12px', lineHeight: '1.6', opacity: 0.7 };
|
|
18
18
|
const DOT = { flex: '0 0 auto', opacity: 0.8 };
|
|
19
19
|
const LABEL = { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
|
20
|
-
const
|
|
21
|
-
const KEEP_ON = { ...
|
|
22
|
-
const KEEP_OFF = { ...
|
|
20
|
+
const KEEP_STYLE = { flex: '0 0 auto', border: '1px solid currentColor', background: 'transparent', color: 'inherit', borderRadius: '999px', padding: '0 8px', fontSize: '12px', lineHeight: '18px' };
|
|
21
|
+
const KEEP_ON = { ...KEEP_STYLE, opacity: 0.9, cursor: 'pointer' };
|
|
22
|
+
const KEEP_OFF = { ...KEEP_STYLE, opacity: 0.45, cursor: 'default' };
|
|
23
|
+
const DROP = { flex: '0 0 auto', border: 'none', background: 'transparent', color: 'inherit', padding: '0 2px', fontSize: '12px', lineHeight: '18px', opacity: 0.55, cursor: 'pointer' };
|
|
23
24
|
|
|
24
|
-
/** 结果文本已经是中文,这里只取第一行。 */
|
|
25
25
|
const settledText = (block) => {
|
|
26
26
|
if (block === undefined || block === null || !('kind' in block)) return null;
|
|
27
27
|
const content = Array.isArray(block.content) ? block.content : [];
|
|
28
28
|
for (const item of content) {
|
|
29
29
|
if (item !== null && item.type === 'text' && typeof item.text === 'string' && item.text.length > 0) return item.text;
|
|
30
30
|
}
|
|
31
|
-
if (block.error !== undefined) return '
|
|
32
|
-
if (block.isError === true) return '
|
|
31
|
+
if (block.error !== undefined) return 'memory · stopped';
|
|
32
|
+
if (block.isError === true) return 'memory · failed';
|
|
33
33
|
return null;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const firstLine = (value) => (typeof value === 'string' ? value.split('\n')[0] : null);
|
|
37
37
|
|
|
38
|
+
/** 结果行里带标题时把它取出来,用于点按钮后原路回传。 */
|
|
39
|
+
const quoted = (value) => {
|
|
40
|
+
const match = typeof value === 'string' ? /"([^"]+)"/.exec(value) : null;
|
|
41
|
+
return match ? match[1] : null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const post = (payload) => {
|
|
45
|
+
if (typeof fetch !== 'function') return Promise.resolve(null);
|
|
46
|
+
return fetch('/lite-memory/keep', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'content-type': 'application/json' },
|
|
49
|
+
body: JSON.stringify(payload),
|
|
50
|
+
})
|
|
51
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
52
|
+
.catch(() => null);
|
|
53
|
+
};
|
|
54
|
+
|
|
38
55
|
const Row = (props) => {
|
|
39
56
|
const line = props.phase === 'preparing' ? null : firstLine(settledText(props.block));
|
|
40
|
-
return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, line ?? RUNNING[props.toolName] ?? '
|
|
57
|
+
return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, line ?? RUNNING[props.toolName] ?? 'memory'));
|
|
41
58
|
};
|
|
42
59
|
|
|
43
|
-
/**
|
|
60
|
+
/** 建议行:一个「keep」按钮,点了直接写盘。 */
|
|
44
61
|
const SuggestRow = (props) => {
|
|
45
62
|
const [state, setState] = React.useState('idle');
|
|
46
63
|
const text = props.phase === 'preparing' ? null : settledText(props.block);
|
|
47
|
-
const label = firstLine(text) ?? RUNNING.memory_suggest
|
|
48
|
-
const
|
|
49
|
-
const title = match ? match[1] : null;
|
|
64
|
+
const label = firstLine(text) ?? RUNNING.memory_suggest;
|
|
65
|
+
const title = quoted(text);
|
|
50
66
|
const cwd = typeof props.cwd === 'string' ? props.cwd : '';
|
|
51
67
|
const actions = props.inputActions;
|
|
52
68
|
const canKeep = Boolean(title) && Boolean(cwd) && state === 'idle';
|
|
@@ -67,28 +83,39 @@ window.__ModuleLoader__.load({
|
|
|
67
83
|
}
|
|
68
84
|
};
|
|
69
85
|
if (typeof fetch !== 'function') return fallback();
|
|
70
|
-
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: { 'content-type': 'application/json' },
|
|
73
|
-
body: JSON.stringify({ title, cwd }),
|
|
74
|
-
})
|
|
75
|
-
.then((response) => (response.ok ? response.json() : null))
|
|
76
|
-
.then((value) => setState(value && value.ok ? 'saved' : 'kept'))
|
|
77
|
-
.catch(() => setState('kept'));
|
|
86
|
+
post({ op: 'keep', title, cwd }).then((value) => setState(value && value.ok ? 'saved' : 'kept'));
|
|
78
87
|
};
|
|
79
88
|
const button = title
|
|
80
|
-
? h('button', { type: 'button', onClick: keep, disabled: !canKeep, 'aria-label': '保留这条记忆', style: canKeep ? KEEP_ON : KEEP_OFF },
|
|
89
|
+
? h('button', { type: 'button', onClick: keep, disabled: !canKeep, 'aria-label': '保留这条记忆', style: canKeep ? KEEP_ON : KEEP_OFF }, KEEP[state] ?? KEEP.idle)
|
|
81
90
|
: null;
|
|
82
91
|
return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, label), button);
|
|
83
92
|
};
|
|
84
93
|
|
|
94
|
+
/** 自动写入行:一个「✕」,点了把它移入归档。 */
|
|
95
|
+
const SavedRow = (props) => {
|
|
96
|
+
const [state, setState] = React.useState('idle');
|
|
97
|
+
const text = props.phase === 'preparing' ? null : settledText(props.block);
|
|
98
|
+
const title = quoted(text);
|
|
99
|
+
const cwd = typeof props.cwd === 'string' ? props.cwd : '';
|
|
100
|
+
const canDrop = Boolean(title) && Boolean(cwd) && state === 'idle';
|
|
101
|
+
const drop = () => {
|
|
102
|
+
if (!canDrop) return;
|
|
103
|
+
setState('saving');
|
|
104
|
+
post({ op: 'undo', title, cwd }).then((value) => setState(value && value.ok ? 'dropped' : 'idle'));
|
|
105
|
+
};
|
|
106
|
+
const label = state === 'dropped' ? 'memory · dropped' : firstLine(text) ?? RUNNING.memory_save;
|
|
107
|
+
const button = title && state !== 'dropped' ? h('button', { type: 'button', onClick: drop, disabled: !canDrop, 'aria-label': '删除这条记忆', style: DROP }, '✕') : null;
|
|
108
|
+
return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, label), button);
|
|
109
|
+
};
|
|
110
|
+
|
|
85
111
|
return {
|
|
86
112
|
inject: ['slots'],
|
|
87
113
|
apply(ctx) {
|
|
88
|
-
for (const name of ['
|
|
114
|
+
for (const name of ['memory_search', 'memory_read', 'memory_forget']) {
|
|
89
115
|
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: name }, Row));
|
|
90
116
|
}
|
|
91
117
|
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: 'memory_suggest' }, SuggestRow));
|
|
118
|
+
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: 'memory_save' }, SavedRow));
|
|
92
119
|
},
|
|
93
120
|
};
|
|
94
121
|
},
|
package/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function readJsonBody(req, limit = 8192) {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
69
|
+
/** 「保留」和「✕」的唯一入口:保留只认已经建议过的那条,删除只按标题找已有条目。 */
|
|
70
70
|
function keepHandler(webServer) {
|
|
71
71
|
return async (req, res) => {
|
|
72
72
|
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, reason: '只接受 POST' });
|
|
@@ -75,7 +75,8 @@ function keepHandler(webServer) {
|
|
|
75
75
|
return sendJson(res, 403, { ok: false, reason: '来源不符' });
|
|
76
76
|
}
|
|
77
77
|
const pathname = String(req.url || '').split('?')[0];
|
|
78
|
-
|
|
78
|
+
const known = pathname === REMOTE_PATH + '/keep' || pathname === REMOTE_PATH + '/undo';
|
|
79
|
+
if (!known) return sendJson(res, 404, { ok: false, reason: '未知操作' });
|
|
79
80
|
let body;
|
|
80
81
|
try {
|
|
81
82
|
body = await readJsonBody(req);
|
|
@@ -86,6 +87,16 @@ function keepHandler(webServer) {
|
|
|
86
87
|
const title = typeof body.title === 'string' ? body.title : '';
|
|
87
88
|
if (cwd.length === 0 || title.length === 0) return sendJson(res, 400, { ok: false, reason: '请求不完整' });
|
|
88
89
|
const scope = scopeOf(cwd);
|
|
90
|
+
if (body.op === 'undo' || pathname === REMOTE_PATH + '/undo') {
|
|
91
|
+
const record = loadIndex(scope).entries.find((item) => item.title === title);
|
|
92
|
+
if (!record) return sendJson(res, 404, { ok: false, reason: '没有这条记忆' });
|
|
93
|
+
try {
|
|
94
|
+
forget(scope, record.id);
|
|
95
|
+
return sendJson(res, 200, { ok: true, title: record.title, id: record.id });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return sendJson(res, 400, { ok: false, reason: error instanceof Error ? error.message : '删除失败' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
89
100
|
const draft = findSuggestion(scope, title);
|
|
90
101
|
if (!draft) return sendJson(res, 404, { ok: false, reason: '没有待保留的建议' });
|
|
91
102
|
try {
|
|
@@ -100,8 +111,8 @@ function keepHandler(webServer) {
|
|
|
100
111
|
const SAVE_PARAMS = {
|
|
101
112
|
type: 'object',
|
|
102
113
|
properties: {
|
|
103
|
-
title: { type: 'string', description:
|
|
104
|
-
summary: { type: 'string', description:
|
|
114
|
+
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字` },
|
|
115
|
+
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语,不超过 ${LIMITS.summaryChars} 字` },
|
|
105
116
|
body: { type: 'string', description: '正文 markdown。只写指针与要点,不抄录长文档' },
|
|
106
117
|
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型' },
|
|
107
118
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签' },
|
|
@@ -153,20 +164,21 @@ export function apply(ctx) {
|
|
|
153
164
|
properties: {
|
|
154
165
|
id: { type: 'string' },
|
|
155
166
|
title: { type: 'string' },
|
|
167
|
+
summary: { type: 'string' },
|
|
156
168
|
merged: { type: 'boolean' },
|
|
157
169
|
},
|
|
158
|
-
required: ['id', 'title', 'merged'],
|
|
170
|
+
required: ['id', 'title', 'summary', 'merged'],
|
|
159
171
|
additionalProperties: false,
|
|
160
172
|
},
|
|
161
173
|
render: (value) => [{
|
|
162
174
|
type: 'text',
|
|
163
|
-
text:
|
|
175
|
+
text: `${value.merged ? 'memory · merged' : 'memory · saved'} "${value.title}"${value.summary ? ` — ${value.summary}` : ''}`,
|
|
164
176
|
}],
|
|
165
177
|
},
|
|
166
178
|
execute: async (args, execution) => {
|
|
167
179
|
const scope = scopeOf(cwdOf(execution));
|
|
168
180
|
const saved = upsert(scope, args);
|
|
169
|
-
return { id: saved.record.id, title: saved.record.title, merged: saved.merged };
|
|
181
|
+
return { id: saved.record.id, title: saved.record.title, summary: saved.record.summary, merged: saved.merged };
|
|
170
182
|
},
|
|
171
183
|
}), 'lite-memory: memory_save');
|
|
172
184
|
|
|
@@ -180,22 +192,26 @@ export function apply(ctx) {
|
|
|
180
192
|
properties: {
|
|
181
193
|
asked: { type: 'boolean' },
|
|
182
194
|
title: { type: 'string' },
|
|
195
|
+
summary: { type: 'string' },
|
|
183
196
|
reason: { type: 'string' },
|
|
184
197
|
},
|
|
185
|
-
required: ['asked', 'title'],
|
|
198
|
+
required: ['asked', 'title', 'summary'],
|
|
186
199
|
additionalProperties: false,
|
|
187
200
|
},
|
|
188
201
|
render: (value) => [{
|
|
189
202
|
type: 'text',
|
|
190
|
-
text: value.asked
|
|
203
|
+
text: value.asked
|
|
204
|
+
? `memory · keep? "${value.title}"${value.summary ? ` — ${value.summary}` : ''}`
|
|
205
|
+
: 'memory · skipped',
|
|
191
206
|
}],
|
|
192
207
|
},
|
|
193
208
|
execute: async (args, execution) => {
|
|
194
209
|
const scope = scopeOf(cwdOf(execution));
|
|
195
210
|
const gate = suggestionGate(scope, args.title);
|
|
196
|
-
|
|
211
|
+
const summary = String(args.summary ?? '');
|
|
212
|
+
if (!gate.ok) return { asked: false, title: String(args.title ?? ''), summary, reason: gate.reason };
|
|
197
213
|
noteSuggestion(scope, args, 'asked');
|
|
198
|
-
return { asked: true, title: String(args.title ?? '') };
|
|
214
|
+
return { asked: true, title: String(args.title ?? ''), summary };
|
|
199
215
|
},
|
|
200
216
|
}), 'lite-memory: memory_suggest');
|
|
201
217
|
|
|
@@ -216,7 +232,7 @@ export function apply(ctx) {
|
|
|
216
232
|
},
|
|
217
233
|
render: (value) => [{
|
|
218
234
|
type: 'text',
|
|
219
|
-
text: value.count === 0 ? '
|
|
235
|
+
text: value.count === 0 ? 'memory · no hits' : `memory · ${value.count} hits · ${value.chars} chars\n${value.index}`,
|
|
220
236
|
}],
|
|
221
237
|
},
|
|
222
238
|
execute: async (args, execution) => {
|
|
@@ -264,7 +280,7 @@ export function apply(ctx) {
|
|
|
264
280
|
},
|
|
265
281
|
render: (value) => [{
|
|
266
282
|
type: 'text',
|
|
267
|
-
text:
|
|
283
|
+
text: `memory · dropped "${value.title}"`,
|
|
268
284
|
}],
|
|
269
285
|
},
|
|
270
286
|
execute: async (args, execution) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "On-demand memory for the DeepSeek Harness: nothing in the system prompt, one muted line only when something is saved, hit or suggested.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|