dsh-memory-palace 1.8.3-alpha.2 → 1.8.4-alpha.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  把 WorkBuddy 的文件式记忆系统移植进 [DeepSeek Harness](https://www.deepseek.com/harness/) —— 为 Harness 提供**跨会话持久化、人类可直接编辑的 Markdown 记忆**。
4
4
 
5
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com) [![npm](https://img.shields.io/npm/v/dsh-memory-palace.svg?label=npm&labelColor=000000&color=ff4b01)](https://www.npmjs.com/package/dsh-memory-palace) [![DeepSeek Harness:0.2.1-rc.2](https://img.shields.io/badge/DeepSeek%20Harness-0.2.1--rc.2-success.svg?labelColor=4D6BFE)](https://github.com/deepseek-ai/deepseek-harness) [![Desktop: supported](https://img.shields.io/badge/Desktop-supported-success.svg?labelColor=4D6BFE)](#安装)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com) [![npm](https://img.shields.io/npm/v/dsh-memory-palace.svg?label=npm&labelColor=000000&color=ff4b01)](https://www.npmjs.com/package/dsh-memory-palace) [![DeepSeek Harness:0.2.1-rc.5](https://img.shields.io/badge/DeepSeek%20Harness-0.2.1--rc.5-success.svg?labelColor=4D6BFE)](https://github.com/deepseek-ai/deepseek-harness) [![Desktop: supported](https://img.shields.io/badge/Desktop-supported-success.svg?labelColor=4D6BFE)](#安装)
6
6
 
7
7
  ## 特性
8
8
 
@@ -10,12 +10,11 @@
10
10
  - **双层记忆**:用户级(跨项目个人偏好)+ 工作区级(项目约定),互不干扰。
11
11
  - **WorkBuddy / CodeBuddy 桥接**:项目已有对应记忆目录时直接读写,无需重复维护。
12
12
  - **记忆工具**:AI 可主动写入、聚合读取、按内容删除(删除需人工确认),写入自带去重。
13
- - **会话命令重整记忆**:手打 `/memory_reorganize` 触发项目记忆全量重整(沿用「超出注入预算 且 过冷却期」双门禁 + 原生确认,原文件自动整档备份);
13
+ - **会话命令重整记忆**:手打 `/memory_reorganize`(中文可打 `/记忆重整`)触发项目记忆全量重整(沿用「超出注入预算 且 过冷却期」双门禁 + 原生确认,原文件自动整档备份);
14
14
  - **设置页集成**:全部配置均可在 DSH 设置面板中图形化调整(按板块折叠),无需改配置文件。
15
- - **自定义指令**:不适合写进记忆的特殊指令可在设置页配置,插件经系统提示词注入。
15
+ - **自定义指令**:不适合写进记忆的特殊指令写在 profile 的 `.memory-palace/user-commands.md`(设置页也能编辑),插件经系统提示词注入。
16
16
  - **记忆注入**:长期记忆与今日日志以「会话起始快照」常驻上下文、按文件身份各只注入一次 —— 既保证每个步骤都可见,又不破坏前缀缓存、不堆积冗余快照。
17
- - **记忆写入(v1.8.0 起唯一模式)**:**记忆子代理**每轮自动整理今日日志并去重(章节化、标删),**长期记忆 `MEMORY.md` 由主 AI 主动维护**,兼顾自动化与可控性。
18
- - v1.8.0 破坏性变更:原「主动记忆(插件模式)」与「智能模式」已整体移除。二者的能力已被覆盖——主动落档由分工指令引导主 AI 承担,逐轮摘要由记忆子代理写进日志承担。
17
+ - **记忆写入**:**记忆子代理**每轮自动整理今日日志并去重(章节化、标删),**长期记忆 `MEMORY.md` 由主 AI 主动维护**,兼顾自动化与可控性。
19
18
  - **标准插件包**:经官方插件机制一键安装,无需改动 harness。
20
19
 
21
20
  ## 记忆文件布局
@@ -35,6 +34,8 @@
35
34
  └── 2026-08-16.md # 每日工作日志
36
35
  ```
37
36
 
37
+ > 自定义指令**不放在记忆目录里**,而是 profile 私有文件:`$DSH_HOME/profiles/<profile>/.memory-palace/user-commands.md`(与排障日志同根)。它是个人偏好而非项目事实,跟着 profile 走、可被编辑器直接修改。
38
+
38
39
  ## 工作原理
39
40
 
40
41
  **读取(每轮对话)**——两条通道并行:
@@ -69,8 +70,6 @@
69
70
 
70
71
  ## 安装
71
72
 
72
- > 要求 DeepSeek Harness **`^0.2.1-alpha.1`**(由 `package.json` 的 `engines.dsh` 判定,0.2.0 线及以下会被插件管理器直接拒绝)。
73
-
74
73
  ### CLI 版(`web` profile)
75
74
 
76
75
  方式一:直接通过 GitHub 安装(推荐,`lib/` 构建产物已随仓库分发,装即用)
@@ -107,7 +106,6 @@ dsh plugin --profile web remove dsh-memory-palace
107
106
  ```
108
107
 
109
108
  ### Desktop 版
110
- - github: `github:lovezi0/dsh-memory-palace`
111
109
  - npm: `dsh-memory-palace`
112
110
 
113
111
  ## 配置
@@ -121,6 +119,11 @@ dsh plugin --profile web remove dsh-memory-palace
121
119
 
122
120
  ## 版本历史
123
121
 
122
+ - **1.8.4**
123
+ - **1.8.4-alpha.1**
124
+ - 💥用户自定义指令落盘位置迁移
125
+ - 💪memory_reorganize 指令支持中文
126
+ - 🐛memory_reorganize 指令无效问题修复
124
127
  - **1.8.3**
125
128
  - **1.8.3-alpha.2**
126
129
  - 💪超预算回显附重整门禁指引,避免 agent 多轮精简替换凑数
package/lib/api.mjs CHANGED
@@ -1,9 +1,10 @@
1
- // memory-palace 自有同源 route(/memory-palace/api):设置读写(真保存)。
2
- // 经 registerApi 注入 ctx/paths/设置面;在 index.mjs 装配时调用一次。
1
+ // memory-palace 自有同源 route(/memory-palace/api):设置读写(真保存)+ 自定义指令读写。
2
+ // 经 registerApi 注入 ctx/设置面/userCommands;在 index.mjs 装配时调用一次。
3
3
  // 机制:client 直接 fetch 本 route,handler 内走服务端 settings.replace → settings-file 持久化,
4
4
  // 绕开 settingsScope(非 loopback 下 set() no-op)与 apiproxy allowlist 两层限制(参考 dsh-better-sidebar)。
5
- // distill.project / distill.project.preview 两个 action 已随「蒸馏项目记忆」功能移除,
6
- // 本 route 现仅剩设置读写(settings.get / settings.update)。
5
+ // distill.project / distill.project.preview 两个 action 已随「蒸馏项目记忆」功能移除。
6
+ // userCommands.* 走文件而不是配置:真源 = <profile>/.memory-palace/user-commands.md
7
+ // (旧配置项 customInstructions 已摘除,本 route 不再读写它)。
7
8
  const API_MAX_BODY_BYTES = 1 << 20;
8
9
 
9
10
  function apiHeader(headers, name) {
@@ -113,9 +114,9 @@ export function applyDebugLogLinkage(section) {
113
114
  }
114
115
 
115
116
  /**
116
- * @param {{ ctx: object, getSettingsFace: () => object | null }} deps
117
+ * @param {{ ctx: object, getSettingsFace: () => object | null, userCommands: object }} deps
117
118
  */
118
- export function registerApi({ ctx, getSettingsFace }) {
119
+ export function registerApi({ ctx, getSettingsFace, userCommands }) {
119
120
  ctx.effect(
120
121
  () =>
121
122
  ctx.webServer.register({
@@ -162,6 +163,19 @@ export function registerApi({ ctx, getSettingsFace }) {
162
163
  apiWriteOk(res, await face.replace(section, expectedRevision));
163
164
  return;
164
165
  }
166
+ // 自定义指令:真源是 profile 目录下的 Markdown 文件,不经过配置节。
167
+ if (method === "userCommands.get") {
168
+ apiWriteOk(res, { path: userCommands.file(), text: await userCommands.read() });
169
+ return;
170
+ }
171
+ if (method === "userCommands.update") {
172
+ const text = payload?.text;
173
+ if (typeof text !== "string") {
174
+ throw Object.assign(new Error("text must be a string"), { code: "bad-request", status: 400 });
175
+ }
176
+ apiWriteOk(res, await userCommands.write(text));
177
+ return;
178
+ }
165
179
  apiWriteError(res, Object.assign(new Error(`unknown memory-palace API method "${method}"`), { code: "not-found", status: 404 }));
166
180
  } catch (error) {
167
181
  apiWriteError(res, error);
package/lib/client.js CHANGED
@@ -62,7 +62,12 @@ window.__ModuleLoader__.load({
62
62
  distillTimeoutHint: "单次记忆子代理 LLM 调用的超时上限(秒);超时视为失败并降级。默认 60 秒。",
63
63
  customLabel: "自定义指令内容",
64
64
  customHint: "非空时注入系统提示词,位置在记忆插件与分工说明之后。对全部会话生效;留空则不注入。",
65
- customPlaceholder: "例如:回答用中文;代码注释用英文;提交信息用祈使句…"
65
+ customPath: "落盘位置:",
66
+ customPlaceholder: "例如:回答用中文;代码注释用英文;提交信息用祈使句…",
67
+ reorgAliasLabel: "记忆重整",
68
+ reorgAliasDesc: "全量重整项目记忆 MEMORY.md(沿用双门禁与确认弹窗)",
69
+ commandFailed: "命令未执行:",
70
+ commandStarted: "命令已提交"
66
71
  };
67
72
 
68
73
  const en = {
@@ -116,7 +121,12 @@ window.__ModuleLoader__.load({
116
121
  distillTimeoutHint: "Timeout (in seconds) for a single memory sub-agent LLM call; on timeout it fails and degrades. Default 60s.",
117
122
  customLabel: "Custom instruction text",
118
123
  customHint: "When non-empty it is injected into the system prompt right after the memory-plugin and memory-roles instructions. Applies to every session; leave empty to inject nothing.",
119
- customPlaceholder: "e.g. Answer in Chinese; write code comments in English; use imperative commit subjects…"
124
+ customPath: "Stored at:",
125
+ customPlaceholder: "e.g. Answer in Chinese; write code comments in English; use imperative commit subjects…",
126
+ reorgAliasLabel: "Reorganize memory",
127
+ reorgAliasDesc: "Fully reorganize the project MEMORY.md (gated, with a confirmation dialog)",
128
+ commandFailed: "Command did not run:",
129
+ commandStarted: "Command submitted"
120
130
  };
121
131
 
122
132
 
@@ -138,8 +148,9 @@ window.__ModuleLoader__.load({
138
148
  summaryTimeoutMs: v.summaryTimeoutMs != null ? String(Math.round((Number(v.summaryTimeoutMs) || 0) / 1000)) : "60",
139
149
  summaryModel: v.summaryModel || "",
140
150
  reorgCooldownDays: v.reorgCooldownDays != null ? String(v.reorgCooldownDays) : "7",
141
- subagentLogBudget: v.subagentLogBudget != null ? String(v.subagentLogBudget) : "20000",
142
- customInstructions: v.customInstructions || ""
151
+ subagentLogBudget: v.subagentLogBudget != null ? String(v.subagentLogBudget) : "20000"
152
+ // customInstructions 已移出配置:真源是 profile 目录下的
153
+ // .memory-palace/user-commands.md,走 apiGetUserCommands / apiUpdateUserCommands。
143
154
  };
144
155
  }
145
156
 
@@ -178,6 +189,31 @@ window.__ModuleLoader__.load({
178
189
  return { value: v.value || {}, user: v.user || {}, revision: v.revision };
179
190
  }
180
191
 
192
+ // 自定义指令:不再属于配置节,真源是 <profile>/.memory-palace/user-commands.md。
193
+ async function apiGetUserCommands() {
194
+ const resp = await fetch("/memory-palace/api/userCommands.get", {
195
+ method: "POST",
196
+ headers: { "content-type": "application/json" },
197
+ body: "{}",
198
+ });
199
+ const parsed = await resp.json().catch(() => null);
200
+ if (!parsed || parsed.ok !== true) throw new Error(parsed?.error?.message ?? `HTTP ${resp.status}`);
201
+ const v = parsed.value || {};
202
+ return { path: typeof v.path === "string" ? v.path : "", text: typeof v.text === "string" ? v.text : "" };
203
+ }
204
+
205
+ async function apiUpdateUserCommands(text) {
206
+ const resp = await fetch("/memory-palace/api/userCommands.update", {
207
+ method: "POST",
208
+ headers: { "content-type": "application/json" },
209
+ body: JSON.stringify({ text }),
210
+ });
211
+ const parsed = await resp.json().catch(() => null);
212
+ if (!parsed || parsed.ok !== true) throw new Error(parsed?.error?.message ?? `HTTP ${resp.status}`);
213
+ const v = parsed.value || {};
214
+ return { path: typeof v.path === "string" ? v.path : "", text: typeof v.text === "string" ? v.text : "" };
215
+ }
216
+
181
217
 
182
218
  // 模型枚举改走 ctx.remote.session.modelCatalog()——ApiProxy 包已在
183
219
  // DSH 0.1.2-alpha.2 删除(旧 conn.api.llm.models 死了)。服务缺席 / 非 ok / 异常
@@ -206,6 +242,10 @@ window.__ModuleLoader__.load({
206
242
  const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
207
243
  const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
208
244
  const [dirty, setDirty] = react.useState(false);
245
+ // 自定义指令不属于配置节:独立状态,真源是 user-commands.md。
246
+ // saved = 磁盘上最后一次读到/写入的内容,供「放弃」还原。
247
+ const [commands, setCommands] = react.useState({ path: "", text: "", saved: "" });
248
+ const [cmdDirty, setCmdDirty] = react.useState(false);
209
249
  const [saving, setSaving] = react.useState(false);
210
250
  const [failed, setFailed] = react.useState(false);
211
251
  const [validation, setValidation] = react.useState(null);
@@ -214,15 +254,16 @@ window.__ModuleLoader__.load({
214
254
  // 的语义 —— 纯前端临时状态、**不持久化**:「用户打开哪张卡」是一次阅读手势,不是配置。
215
255
  const [openCards, setOpenCards] = react.useState({});
216
256
 
217
- // 初始加载(从自有 route 读真实值)。
257
+ // 初始加载(从自有 route 读真实值)。自定义指令走独立 action(文件真源),与配置并行拉取。
218
258
  react.useEffect(() => {
219
259
  let alive = true;
220
260
  (async () => {
221
261
  try {
222
- const full = await apiGetFull();
262
+ const [full, cmd] = await Promise.all([apiGetFull(), apiGetUserCommands()]);
223
263
  if (alive) {
224
264
  setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
225
265
  setDraft(projectDraft({ value: full.value }));
266
+ setCommands({ path: cmd.path, text: cmd.text, saved: cmd.text });
226
267
  }
227
268
  } catch (e) {
228
269
  if (alive) setRemote((r) => ({ ...r, loaded: true }));
@@ -272,10 +313,23 @@ window.__ModuleLoader__.load({
272
313
  if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
273
314
  }
274
315
 
316
+ // 自定义指令:独立于配置 draft,保存走 userCommands.update(写文件)。
317
+ function editCommands(text) {
318
+ setCommands((c) => ({ ...c, text }));
319
+ setCmdDirty(true);
320
+ setFailed(false);
321
+ }
322
+
275
323
  async function save() {
276
324
  setSaving(true);
277
325
  setFailed(false);
278
326
  try {
327
+ // 自定义指令先落盘:它写的是独立文件,没有 revision 冲突,失败即整体失败提示。
328
+ if (cmdDirty) {
329
+ const saved = await apiUpdateUserCommands(commands.text);
330
+ setCommands({ path: saved.path, text: saved.text, saved: saved.text });
331
+ setCmdDirty(false);
332
+ }
279
333
  // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
280
334
  const section = {};
281
335
  for (const field of Object.keys(draft)) {
@@ -312,6 +366,8 @@ window.__ModuleLoader__.load({
312
366
  function discard() {
313
367
  setDraft(projectDraft(remote));
314
368
  setDirty(false);
369
+ setCommands((c) => ({ ...c, text: c.saved }));
370
+ setCmdDirty(false);
315
371
  setFailed(false);
316
372
  setValidation(null);
317
373
  }
@@ -338,11 +394,15 @@ window.__ModuleLoader__.load({
338
394
  })
339
395
  ]);
340
396
 
341
- const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
397
+ // dirtyNow:该行自己的「未保存」判据(默认跟随整节 dirty)。
398
+ // 自定义指令写的是独立文件,与配置节的 dirty 不同步,故单独传。
399
+ const row = (label, hint, control, field, dirtyNow) => h("div", { style: fieldStyle }, [
342
400
  h("div", { style: headStyle }, [
343
401
  h("label", { style: labelStyle }, label),
344
- dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
345
- !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
402
+ (dirtyNow === undefined ? dirty : dirtyNow) && field
403
+ ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
404
+ !(dirtyNow === undefined ? dirty : dirtyNow) && field && isOverridden(field)
405
+ ? h("span", { style: badgeStyle }, t("overridden")) : null
346
406
  ]),
347
407
  control,
348
408
  hint ? h("p", { style: hintStyle }, hint) : null
@@ -602,12 +662,14 @@ window.__ModuleLoader__.load({
602
662
  onChange: (e) => edit("silentPresets", e.target.value)
603
663
  }), "silentPresets")
604
664
  ]),
605
- // 自定义指令。非空时经 system prompt 追加到「记忆分工说明」之后;
606
- // 它是恒定内容(用户配置一次不变),故放 section 而非 E 投影 —— 零缓存成本。
665
+ // 自定义指令。真源是 <profile>/.memory-palace/user-commands.md(不再进配置);
666
+ // 非空时经 system prompt 追加到「记忆分工说明」之后。
667
+ // 它是恒定内容(改文件才变),故放 section 而非 E 投影 —— 零缓存成本。
607
668
  card("custom", t("custom"), t("customDesc"), [
608
- row(t("customLabel"), t("customHint"),
669
+ row(t("customLabel"),
670
+ commands.path ? t("customHint") + " " + t("customPath") + commands.path : t("customHint"),
609
671
  h("textarea", {
610
- value: draft.customInstructions,
672
+ value: commands.text,
611
673
  disabled,
612
674
  rows: 6,
613
675
  placeholder: t("customPlaceholder"),
@@ -619,8 +681,8 @@ window.__ModuleLoader__.load({
619
681
  lineHeight: "1.6",
620
682
  resize: "vertical"
621
683
  },
622
- onChange: (e) => edit("customInstructions", e.target.value)
623
- }), "customInstructions")
684
+ onChange: (e) => editCommands(e.target.value)
685
+ }), "userCommands", cmdDirty)
624
686
  ]),
625
687
  card("smartSummary", t("smartSummary"), t("smartSummaryDesc"), [
626
688
  // ---- 本卡片内三处删除已落地——① 记忆写入总开关:写入恒定进行,停写只剩
@@ -719,9 +781,89 @@ window.__ModuleLoader__.load({
719
781
  }
720
782
 
721
783
 
784
+ // 中文会话命令:宿主 server 命令名限 ^[a-z][a-z0-9_-]*$,中文无法直接注册;
785
+ // 但 client 的 commandUi contribution 只查重名、不限字符集——把 name 写成中文,
786
+ // 斜杠菜单即可用中文检索,回车 / 点选都能精确认领(与宿主 /model 的菜单行为同源)。
787
+ // 执行链与英文命令完全一致:action 里转发 /memory_reorganize,服务端逻辑零分叉。
788
+ const REORG_COMMAND_NAME = "memory_reorganize";
789
+ const REORG_ALIAS_NAME = "记忆重整";
790
+
791
+ // 命令反馈:command/run 与 command/done 都是宿主 log-only 事件,
792
+ // 客户端要等下一次会话装载才随日志回放渲染——命令执行完当场"什么都没发生"。
793
+ // 故命令结算后一律经 composer notice 立即回显(成功 info / 失败 error),
794
+ // 不依赖会话流刷新,也不污染模型上下文。
795
+ // 通道不可用时静默(会话流里最终仍有命令行,不影响主流程)。
796
+ function notifyComposer(ctx, sessionId, level, text) {
797
+ try {
798
+ const sessions = typeof ctx.get === "function" ? ctx.get("sessions") : undefined;
799
+ const actx = sessions && typeof sessions.scope === "function" ? sessions.scope(sessionId) : undefined;
800
+ const conversation = actx && typeof actx.get === "function" ? actx.get("conversation") : undefined;
801
+ if (conversation && conversation.input && typeof conversation.input.for === "function") {
802
+ conversation.input.for(actx).notify(level, text);
803
+ }
804
+ } catch {
805
+ /* 提示通道不可用:忽略 */
806
+ }
807
+ }
808
+
809
+ function registerCommandAlias(ctx, t) {
810
+ // commandUi 是可选服务:无命令面的部署(或测试替身)缺席时静默跳过,绝不阻塞主体。
811
+ if (typeof ctx.inject !== "function") return;
812
+ ctx.inject(["commandUi"], (scope) => {
813
+ const commandUi = scope && scope.commandUi;
814
+ if (!commandUi || typeof commandUi.register !== "function") return;
815
+ const contribution = {
816
+ name: REORG_ALIAS_NAME,
817
+ label: () => t("reorgAliasLabel"),
818
+ description: () => t("reorgAliasDesc"),
819
+ available: () => true,
820
+ ui: {
821
+ kind: "action",
822
+ run: (session) => {
823
+ const sessionId = session && session.sessionId;
824
+ if (!sessionId) return;
825
+ void (async () => {
826
+ try {
827
+ // 成功/失败结果由下方的 command/executed 监听统一回显(转发同样会 emit)。
828
+ const response = await ctx.remote.commands.execute(sessionId, "/" + REORG_COMMAND_NAME, []);
829
+ if (!response || response.ok !== true) {
830
+ // RPC 本身失败(未进入 handler)→ 不会有 command/executed,自行提示。
831
+ const message = response && response.error && response.error.message
832
+ ? response.error.message
833
+ : "";
834
+ notifyComposer(ctx, sessionId, "error", t("commandFailed") + (message ? " " + message : ""));
835
+ }
836
+ } catch (error) {
837
+ const message = error && error.message ? error.message : String(error);
838
+ notifyComposer(ctx, sessionId, "error", t("commandFailed") + " " + message);
839
+ }
840
+ })();
841
+ }
842
+ }
843
+ };
844
+ if (typeof scope.effect === "function") {
845
+ scope.effect(() => commandUi.register(contribution), "memory-palace: 中文命令 alias");
846
+ } else {
847
+ commandUi.register(contribution);
848
+ }
849
+ });
850
+ // 英文命令与中文别名的结算都经过这里:log-only 事件不实时推送,必须主动回显。
851
+ if (typeof ctx.on === "function") {
852
+ ctx.on("command/executed", (sessionId, name, result) => {
853
+ if (name !== REORG_COMMAND_NAME || !result) return;
854
+ const failed = result.kind === "error";
855
+ const text = result.text || (failed ? t("commandFailed") : t("commandStarted"));
856
+ notifyComposer(ctx, sessionId, failed ? "error" : "info", text);
857
+ });
858
+ }
859
+ }
860
+
861
+
722
862
  function apply(ctx) {
723
863
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
724
864
  const t = ctx.locale.bind(NS);
865
+ // 中文会话命令(/记忆重整)+ 命令失败提示(composer notice)。
866
+ registerCommandAlias(ctx, t);
725
867
  // 设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
726
868
  // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
727
869
  const controller = new MemoryPalaceController(ctx);
@@ -0,0 +1,84 @@
1
+ // 用户自定义指令的唯一真源:<profileContext.dir>/.memory-palace/user-commands.md
2
+ //
3
+ // 旧版把自定义指令放在配置项 `customInstructions`(profile 的 cordis.patch.yml),
4
+ // 本模块【不读配置】——该字段已从 schema 摘除,存量值静默失效。
5
+ //
6
+ // 取径依据:profileContext.dir = $DSH_HOME/profiles/<profile>,由宿主 CLI 与 Desktop
7
+ // 经同一个 runProfile 提供(与 common/logger.mjs 的日志落点同根),故两端都可用。
8
+ // 拿不到 profileContext 时**绝不猜路径**:读取返回空串、写入抛错。
9
+ //
10
+ // 读路径是同步的(systemPrompt.section.text() 必须是同步函数,每步调用),
11
+ // 故按 mtimeMs 缓存:文件未变就复用上次内容,避免每步都读盘。
12
+ import { mkdir, rename, writeFile } from "node:fs/promises";
13
+ import { readFileSync, statSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+
16
+ /** profile 私有目录名(与日志落点共用)。 */
17
+ export const USER_COMMANDS_DIR = ".memory-palace";
18
+ /** 自定义指令文件名。 */
19
+ export const USER_COMMANDS_FILE = "user-commands.md";
20
+
21
+ /** 去掉 BOM,其余原样保留(用户排版由用户决定)。 */
22
+ function stripBom(text) {
23
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
24
+ }
25
+
26
+ /**
27
+ * @param {{ ctx: object }} deps
28
+ * @returns {{ file: () => string | null, readSync: () => string, read: () => Promise<string>, write: (text: string) => Promise<{ path: string, text: string }> }}
29
+ */
30
+ export function createUserCommands({ ctx }) {
31
+ // file → 上次读到的内容;mtimeMs = -1 表示强制重读。
32
+ let cache = { file: null, mtimeMs: -1, text: "" };
33
+
34
+ function file() {
35
+ const pc = typeof ctx?.get === "function" ? ctx.get("profileContext") : undefined;
36
+ const dir = pc?.dir;
37
+ return typeof dir === "string" && dir ? join(dir, USER_COMMANDS_DIR, USER_COMMANDS_FILE) : null;
38
+ }
39
+
40
+ // 同步读取(供 system prompt 每步调用)。任何异常都不外抛——section.text() 抛错会毁整轮对话。
41
+ function readSync() {
42
+ const target = file();
43
+ if (!target) {
44
+ cache = { file: null, mtimeMs: -1, text: "" };
45
+ return "";
46
+ }
47
+ try {
48
+ const info = statSync(target);
49
+ if (cache.file === target && cache.mtimeMs === info.mtimeMs) return cache.text;
50
+ const text = stripBom(readFileSync(target, "utf8"));
51
+ cache = { file: target, mtimeMs: info.mtimeMs, text };
52
+ return text;
53
+ } catch {
54
+ // 文件不存在 / 不可读:视为无自定义指令,并清掉旧缓存(防删除后仍注入)。
55
+ cache = { file: target, mtimeMs: -1, text: "" };
56
+ return "";
57
+ }
58
+ }
59
+
60
+ async function read() {
61
+ return readSync();
62
+ }
63
+
64
+ // 原子写:先写同目录临时文件再 rename,避免半截内容被注入。
65
+ async function write(text) {
66
+ const target = file();
67
+ if (!target) {
68
+ throw Object.assign(new Error("无法定位 user-commands.md:当前运行环境没有 profileContext.dir"), {
69
+ code: "no-profile",
70
+ status: 503,
71
+ });
72
+ }
73
+ const body = stripBom(String(text ?? ""));
74
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
75
+ const tmp = `${target}.tmp`;
76
+ await writeFile(tmp, body, { encoding: "utf8", mode: 0o600 });
77
+ await rename(tmp, target);
78
+ // 强制下次 readSync 重新 stat(同毫秒写入时 mtime 可能未变)。
79
+ cache = { file: target, mtimeMs: -1, text: body };
80
+ return { path: target, text: body };
81
+ }
82
+
83
+ return { file, readSync, read, write };
84
+ }
package/lib/index.js CHANGED
@@ -32,6 +32,7 @@ import Schema from "@deepseek-ai/schemastery";
32
32
  import { extractText, extractToolErrorText } from "./common/text.mjs";
33
33
  import { createPaths } from "./common/paths.mjs";
34
34
  import { createLogger } from "./common/logger.mjs";
35
+ import { createUserCommands } from "./common/user-commands.mjs";
35
36
  import { planModeOf } from "./common/planmode.mjs";
36
37
  import { REORG_COMMAND_PROMPT, REORG_MESSAGE_PREFIX } from "./common/prompts.mjs";
37
38
  import { registerTools } from "./tools.mjs";
@@ -92,9 +93,10 @@ export const Config = Schema.object({
92
93
  // ---- 记忆子代理配置 ----
93
94
  reorgCooldownDays: Schema.number().default(7).description("项目级 MEMORY.md 全量重整的冷却天数(距上次重整)。与「超出注入预算」(`workspaceBudgetChars`,即项目级 MEMORY.md 大于该值)双条件同时满足才允许 memory_reorganize;时间戳以 HTML 注释落在 MEMORY.md 文件尾。").volatile(),
94
95
  subagentLogBudget: Schema.number().default(20000).description("记忆子代理回喂今日工作日志的字符上限。超出时仅回喂章节目录,子代理用 log_read_section 按需读取章节。").volatile(),
95
- // ---- :自定义指令(用户自写,经 system prompt 注入到「记忆分工 prompt」之后) ----
96
- // 放 system 而非 E 投影:用户配置一次即恒定 → 零缓存成本(见下方 section 的分流原则)。
97
- customInstructions: Schema.string().default("").description("自定义指令:非空时追加到系统提示词「记忆分工说明」之后,用于承载不适合写进用户记忆的特殊指令。对全部会话生效;留空则不注入。").volatile(),
96
+ // ---- 原自定义指令配置项(customInstructions)已移除 ----
97
+ // 破坏性变更:自定义指令改由 <profile>/.memory-palace/user-commands.md 承载,
98
+ // 配置里不再有该字段,也不再读取存量值(老 profile 的旧键作为未知键透传、静默失效)。
99
+ // 理由:人类可读 Markdown 才是真源,指令不该埋在 profile patch 里。
98
100
  });
99
101
 
100
102
  // 适配 dsh 0.2.1-alpha.1——Web 面删除 webRuntime 服务,信任权威改由 webStartup 携带
@@ -268,9 +270,11 @@ export function apply(ctx, config) {
268
270
  // 落点 `<profileDir>/.memory-palace/logs/<sessionId>/{info,debug}.log`,统一受 distillDebugLog 门控。
269
271
  // 取径与分级依据见 src/common/logger.mjs 文件头。
270
272
  const logger = createLogger({ ctx, getConfig: () => source() });
273
+ // 自定义指令真源 = <profile>/.memory-palace/user-commands.md(不读配置;profileContext 缺失时读取为空)。
274
+ const userCommands = createUserCommands({ ctx });
271
275
  registerTools({ ctx, getConfig: () => source(), paths, state });
272
- // distill 句柄已随「蒸馏项目记忆」功能移除;route 现只剩设置读写。
273
- registerApi({ ctx, getSettingsFace });
276
+ // distill 句柄已随「蒸馏项目记忆」功能移除;route 现剩设置读写 + 自定义指令读写。
277
+ registerApi({ ctx, getSettingsFace, userCommands });
274
278
  // 记忆子代理模块(原 hybrid)。必须【无条件注册】——apply() 同步段执行时 settings 服务
275
279
  // 尚未挂载,此刻 source() 读的是 cordis 组合配置(未显式配置时即 schema 默认);
276
280
  // 条件注册会在热配置变化后导致工具/子代理从未注册(实装踩坑:配置写着启用但
@@ -633,8 +637,10 @@ export function apply(ctx, config) {
633
637
  const introFull = [lead, files, read, proactive, tail].join("\n\n") + planNote;
634
638
  // 自定义指令追加在「记忆插件 prompt + 记忆分工 prompt」之后 ——
635
639
  // 拼接顺序即 system prompt 里的实际排布(同一 section 内完成,order 不变)。
636
- // 它由用户在设置页配置,内容恒定 → 放 system 零缓存成本,不违反本节的分流原则。
637
- const custom = typeof cfg.customInstructions === "string" ? cfg.customInstructions.trim() : "";
640
+ // 真源是 <profile>/.memory-palace/user-commands.md(用户手改或设置页编辑);
641
+ // 内容恒定(改文件才变)→ 放 system 零缓存成本,不违反本节的分流原则。
642
+ // readSync 内部按 mtimeMs 缓存,文件未变时不读盘。
643
+ const custom = userCommands.readSync().trim();
638
644
  // 本 section 已无任何动态块(日志迁出 E 投影),直接返回常量指令。
639
645
  // 返回值仍非空串(即便自定义指令为空也含 intro),避免 harness 对空 section 的处理歧义。
640
646
  return custom ? `${introFull}\n\n[用户自定义指令] ${custom}` : introFull;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-palace",
3
- "version": "1.8.3-alpha.2",
3
+ "version": "1.8.4-alpha.1",
4
4
  "description": "把 WorkBuddy 的文件式记忆系统移植进 DeepSeek Harness:用户级 + 工作区的人类可读 Markdown 记忆(~/.deepseek-harness/MEMORY.md 与 <cwd>/.deepseek-harness/memory/)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -41,7 +41,7 @@
41
41
  }
42
42
  },
43
43
  "engines": {
44
- "dsh": "^0.2.1-alpha.1"
44
+ "dsh": "^0.2.1-rc.5"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@deepseek-ai/schemastery": "~3.18.5-alpha.1"