@max-null/dsh-memory 0.22.1 → 0.22.2

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
@@ -148,9 +148,19 @@ DSH 存储层的写入是「读—改—写全量覆盖」,且官方两个后
148
148
 
149
149
  把本机记忆镜像到一个 git 仓库,多个环境之间共享同一份记忆。
150
150
 
151
- > **默认不启用。** 不配 `memory.sync.remote` 时,`memory_sync` 与 `memory_conflicts`
152
- > **根本不注册**(模型看不到,也就不会去调),自述里的工具数也停在 14。只配 `mirrorDir`
153
- > 不配 `remote` 同样不启用——本地镜像不解决任何问题。这是实验性功能,不该影响别人的安装。
151
+ > **默认不启用,而且是显式开关。** `memory.sync.enabled` 为 `true` 时才注册
152
+ > `memory_sync` 与 `memory_conflicts`——关着时**根本不注册**(模型看不到,也就不会去调),
153
+ > 自述里的工具数停在 14。
154
+ >
155
+ > 判据**不是「配了 remote」**(2026-11-12 改):填了目标地址不等于打开了功能,那会让一次
156
+ > 配置试验变成意外开机。开关与目标解耦后,还多出一档「只开本地镜像、暂不推送」
157
+ > ——此时 `remote` 留空即可,push 会明确回「未配置远端地址 —— 已完成本地提交」。
158
+ >
159
+ > 开关在插件页「记忆」详情里点(与日记开关同一张卡片)。值写进 **profile 的
160
+ > `cordis.patch.yml`**(`memory.config.sync.enabled`,不是独立的设置文件):该 profile 开了
161
+ > `patchReload: live` 时即时生效,否则**重启实例后生效**——2026-11-12 在 dev 实例里实测,
162
+ > 关掉热重载时点了开关、工具不会立刻出现。`remote` / `mirrorDir` 那几项决定连接目标,
163
+ > 无论哪种情况都要重启。
154
164
 
155
165
  **为什么是「一条记忆一个文件」。** 整份 `memory.json` 放进 git 会撞:2026-11-06 实测,两端各新增一条记忆后 merge,`git` 报 `CONFLICT (content)`——两条记录挤在同一个插入点,按「取一边」解决会**静默丢掉另一端那条**,而 git 仍报「合并成功」。改成一条一个文件后同一场景是 `Merge made by the 'ort' strategy`,0 冲突。
156
166
 
@@ -159,8 +169,9 @@ DSH 存储层的写入是「读—改—写全量覆盖」,且官方两个后
159
169
  ```yaml
160
170
  memory:
161
171
  sync:
172
+ enabled: true # 总开关;关着两个工具根本不注册
162
173
  mirrorDir: '' # 缺省 <DSH_HOME>/memory-sync
163
- remote: '' # https 远端;缺省只做本地提交
174
+ remote: '' # https 远端;留空只做本地提交
164
175
  branch: main
165
176
  envName: '' # 本环境短名,冲突留证时用作后缀
166
177
  ```
package/client.js CHANGED
@@ -104,51 +104,17 @@ function brainIcon() {
104
104
  strokeLinejoin: "round"
105
105
  }, BRAIN_PATHS.map((d, index) => (0, import_react.createElement)("path", { key: index, d })));
106
106
  }
107
- var SETTINGS_NAV_MARKER = "data-dsh-memory-settings-nav";
108
- var BRAIN_MASK_SVG = `%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z'/%3E%3Cpath d='M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z'/%3E%3C/svg%3E`;
109
- var NAV_ICON_CSS = `
110
- [data-dsh-memory-settings-nav] > svg:first-child { display: none; }
111
- [data-dsh-memory-settings-nav]::before {
112
- content: '';
113
- flex: none;
114
- width: 16px;
115
- height: 16px;
116
- background: currentColor;
117
- -webkit-mask: url("data:image/svg+xml,${BRAIN_MASK_SVG}") center / contain no-repeat;
118
- mask: url("data:image/svg+xml,${BRAIN_MASK_SVG}") center / contain no-repeat;
119
- }
120
- `;
121
- var navCssInjected = false;
122
- function injectNavCss() {
123
- if (navCssInjected || typeof document === "undefined") return;
124
- navCssInjected = true;
125
- const style = document.createElement("style");
126
- style.setAttribute("data-plugin", "@max-null/dsh-memory");
127
- style.textContent = NAV_ICON_CSS;
128
- document.head.append(style);
129
- }
130
- function registerSettingsNavIcon(label) {
131
- let disposed = false;
132
- const sync = () => {
133
- if (disposed) return;
134
- const currentLabel = label().trim();
135
- const buttons = document.querySelectorAll('[role="dialog"] nav button');
136
- for (const button of buttons) {
137
- const matches = currentLabel.length > 0 && button.textContent?.trim() === currentLabel;
138
- if (matches) button.setAttribute(SETTINGS_NAV_MARKER, "");
139
- else button.removeAttribute(SETTINGS_NAV_MARKER);
140
- }
141
- };
142
- sync();
143
- const observer = new MutationObserver(sync);
144
- observer.observe(document.body, { childList: true, subtree: true, characterData: true });
145
- return () => {
146
- disposed = true;
147
- observer.disconnect();
148
- document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`).forEach((element) => {
149
- element.removeAttribute(SETTINGS_NAV_MARKER);
150
- });
151
- };
107
+ function navIcon(size) {
108
+ return (0, import_react.createElement)("svg", {
109
+ width: size,
110
+ height: size,
111
+ viewBox: "0 0 24 24",
112
+ fill: "none",
113
+ stroke: "currentColor",
114
+ strokeWidth: 2,
115
+ strokeLinecap: "round",
116
+ strokeLinejoin: "round"
117
+ }, BRAIN_PATHS.map((d, index) => (0, import_react.createElement)("path", { key: index, d })));
152
118
  }
153
119
  var STRINGS = {
154
120
  zh: {
@@ -1031,19 +997,8 @@ function SettingsMemoryView(props) {
1031
997
  cwd: currentSessionCwd(props.ctx)
1032
998
  });
1033
999
  }
1034
- function DiarySettingsCard(props) {
1035
- const { form } = props;
1036
- const [enabled, setEnabled] = (0, import_react.useState)(true);
1037
- (0, import_react.useEffect)(() => {
1038
- const read = () => {
1039
- setEnabled(form.getSnapshot()?.value?.diary?.enabled ?? true);
1040
- };
1041
- read();
1042
- const off = form.subscribe(read);
1043
- return () => {
1044
- if (typeof off === "function") off();
1045
- };
1046
- }, [form]);
1000
+ function SwitchRow(props) {
1001
+ const { label, hint, enabled, ariaLabel, onToggle, divided } = props;
1047
1002
  return (0, import_react.createElement)(
1048
1003
  "div",
1049
1004
  {
@@ -1053,18 +1008,14 @@ function DiarySettingsCard(props) {
1053
1008
  justifyContent: "space-between",
1054
1009
  gap: 12,
1055
1010
  padding: "12px 0",
1056
- borderTop: "1px solid var(--dsw-alias-border-l2, #1e2836)"
1011
+ ...divided ? { borderTop: "1px solid var(--dsw-alias-border-l2, #1e2836)" } : {}
1057
1012
  }
1058
1013
  },
1059
1014
  (0, import_react.createElement)(
1060
1015
  "div",
1061
1016
  { style: { minWidth: 0 } },
1062
- (0, import_react.createElement)("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-label-primary, #d8e0ea)" } }, "\u65E5\u8BB0"),
1063
- (0, import_react.createElement)(
1064
- "div",
1065
- { style: { fontSize: 11.5, color: "var(--dsw-alias-label-secondary, #67748a)", marginTop: 3, lineHeight: 1.5 } },
1066
- "\u672C\u673A\u5199\u65E5\u8BB0\u3002\u5173\u6389\u540E\u65E5\u8BB0\u5DE5\u5177\u62D2\u7EDD\u6267\u884C\u3001\u4E0D\u518D\u6CE8\u5165\u6307\u9488\uFF1B\u5DF2\u6709\u65E5\u8BB0\u6587\u4EF6\u539F\u6837\u4FDD\u7559\uFF0C\u4E0D\u4F1A\u88AB\u5220\u3002"
1067
- )
1017
+ (0, import_react.createElement)("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-label-primary, #d8e0ea)" } }, label),
1018
+ (0, import_react.createElement)("div", { style: { fontSize: 11.5, color: "var(--dsw-alias-label-secondary, #67748a)", marginTop: 3, lineHeight: 1.5 } }, hint)
1068
1019
  ),
1069
1020
  (0, import_react.createElement)(
1070
1021
  "button",
@@ -1072,17 +1023,8 @@ function DiarySettingsCard(props) {
1072
1023
  type: "button",
1073
1024
  role: "switch",
1074
1025
  "aria-checked": enabled,
1075
- "aria-label": "\u65E5\u8BB0\u5F00\u5173",
1076
- onClick: () => {
1077
- try {
1078
- const pending = form.mutate([{ op: "set", path: ["diary", "enabled"], value: !enabled }]);
1079
- void Promise.resolve(pending).catch((error) => {
1080
- console.warn("dsh-memory: \u65E5\u8BB0\u5F00\u5173\u5199\u5165\u5931\u8D25", error);
1081
- });
1082
- } catch (error) {
1083
- console.warn("dsh-memory: \u65E5\u8BB0\u5F00\u5173\u5199\u5165\u629B\u9519", error);
1084
- }
1085
- },
1026
+ "aria-label": ariaLabel,
1027
+ onClick: onToggle,
1086
1028
  style: {
1087
1029
  flex: "none",
1088
1030
  width: 40,
@@ -1110,6 +1052,52 @@ function DiarySettingsCard(props) {
1110
1052
  )
1111
1053
  );
1112
1054
  }
1055
+ function MemorySettingsCard(props) {
1056
+ const { form } = props;
1057
+ const [diary, setDiary] = (0, import_react.useState)(true);
1058
+ const [sync, setSync] = (0, import_react.useState)(false);
1059
+ (0, import_react.useEffect)(() => {
1060
+ const read = () => {
1061
+ setDiary(form.getSnapshot()?.value?.diary?.enabled ?? true);
1062
+ setSync(form.getSnapshot()?.value?.sync?.enabled ?? false);
1063
+ };
1064
+ read();
1065
+ const off = form.subscribe(read);
1066
+ return () => {
1067
+ if (typeof off === "function") off();
1068
+ };
1069
+ }, [form]);
1070
+ const write = (path, value, what) => {
1071
+ try {
1072
+ const pending = form.mutate([{ op: "set", path, value }]);
1073
+ void Promise.resolve(pending).catch((error) => {
1074
+ console.warn(`dsh-memory: ${what}\u5F00\u5173\u5199\u5165\u5931\u8D25`, error);
1075
+ });
1076
+ } catch (error) {
1077
+ console.warn(`dsh-memory: ${what}\u5F00\u5173\u5199\u5165\u629B\u9519`, error);
1078
+ }
1079
+ };
1080
+ return (0, import_react.createElement)(
1081
+ "div",
1082
+ null,
1083
+ (0, import_react.createElement)(SwitchRow, {
1084
+ label: "\u65E5\u8BB0",
1085
+ hint: "\u672C\u673A\u5199\u65E5\u8BB0\u3002\u5173\u6389\u540E\u65E5\u8BB0\u5DE5\u5177\u62D2\u7EDD\u6267\u884C\u3001\u4E0D\u518D\u6CE8\u5165\u6307\u9488\uFF1B\u5DF2\u6709\u65E5\u8BB0\u6587\u4EF6\u539F\u6837\u4FDD\u7559\uFF0C\u4E0D\u4F1A\u88AB\u5220\u3002",
1086
+ enabled: diary,
1087
+ ariaLabel: "\u65E5\u8BB0\u5F00\u5173",
1088
+ onToggle: () => write(["diary", "enabled"], !diary, "\u65E5\u8BB0"),
1089
+ divided: false
1090
+ }),
1091
+ (0, import_react.createElement)(SwitchRow, {
1092
+ label: "\u4E91\u540C\u6B65",
1093
+ hint: "\u628A\u672C\u673A\u8BB0\u5FC6\u955C\u50CF\u5230\u4E00\u4E2A git \u4ED3\u5E93\uFF0C\u591A\u4E2A\u73AF\u5883\u5171\u7528\u4E00\u4EFD\u8BB0\u5FC6\u3002\u5173\u7740\u65F6 memory_sync \u4E0E memory_conflicts \u6839\u672C\u4E0D\u6CE8\u518C\u3002\u6253\u5F00\u540E\u8FD8\u9700\u8981\u5728\u63D2\u4EF6\u914D\u7F6E\u91CC\u586B sync.remote\uFF08\u7559\u7A7A = \u53EA\u505A\u672C\u5730\u955C\u50CF\u4E0E\u63D0\u4EA4\uFF09\u3002\u5F00\u5173\u5199\u5165 profile \u7684 cordis.patch.yml\uFF1Aprofile \u5F00 patchReload: live \u65F6\u5373\u65F6\u751F\u6548\uFF0C\u5426\u5219\u91CD\u542F\u5B9E\u4F8B\u540E\u751F\u6548\u3002",
1094
+ enabled: sync,
1095
+ ariaLabel: "\u4E91\u540C\u6B65\u5F00\u5173",
1096
+ onToggle: () => write(["sync", "enabled"], !sync, "\u4E91\u540C\u6B65"),
1097
+ divided: true
1098
+ })
1099
+ );
1100
+ }
1113
1101
  function apply(ctx) {
1114
1102
  const face = ctx;
1115
1103
  const locale = face.get?.("locale");
@@ -1120,7 +1108,6 @@ function apply(ctx) {
1120
1108
  });
1121
1109
  const slots = ctx.slots;
1122
1110
  if (slots?.inject !== void 0) {
1123
- injectNavCss();
1124
1111
  slots.inject("settings.section", () => slots.register({
1125
1112
  name: "settings.section",
1126
1113
  id: "dsh-memory",
@@ -1130,7 +1117,12 @@ function apply(ctx) {
1130
1117
  }, () => (0, import_react.createElement)(SettingsMemoryView, {
1131
1118
  ctx
1132
1119
  })));
1133
- face.effect?.(() => registerSettingsNavIcon(() => STRINGS[localeId].tabMemory), "dsh-memory: settings navigation icon");
1120
+ slots.inject("settings.section.icon", () => slots.register({
1121
+ name: "settings.section.icon",
1122
+ id: "dsh-memory",
1123
+ order: 60,
1124
+ label: () => STRINGS[localeId].tabMemory
1125
+ }, (props) => navIcon(props?.size ?? 16)));
1134
1126
  }
1135
1127
  try {
1136
1128
  const configForms = face.get?.("configForms");
@@ -1140,10 +1132,10 @@ function apply(ctx) {
1140
1132
  name: "plugins.bundle.config",
1141
1133
  key: "@max-null/dsh-memory",
1142
1134
  inject: () => ({})
1143
- }, () => (0, import_react.createElement)(DiarySettingsCard, { form: settingsForm })));
1135
+ }, () => (0, import_react.createElement)(MemorySettingsCard, { form: settingsForm })));
1144
1136
  }
1145
1137
  } catch (error) {
1146
- console.warn("dsh-memory: \u65E5\u8BB0\u914D\u7F6E\u5361\u7247\u672A\u6CE8\u518C\uFF08\u4E0D\u5F71\u54CD\u8BB0\u5FC6\u4E0E\u65E5\u8BB0\u5DE5\u5177\uFF09", error);
1138
+ console.warn("dsh-memory: \u8BBE\u7F6E\u5361\u7247\u672A\u6CE8\u518C\uFF08\u4E0D\u5F71\u54CD\u8BB0\u5FC6\u4E0E\u65E5\u8BB0\u5DE5\u5177\uFF09", error);
1147
1139
  }
1148
1140
  const root = ctx;
1149
1141
  if (root.inject === void 0) return;
package/dist/engine.d.ts CHANGED
@@ -370,14 +370,16 @@ export interface MemoryConfig {
370
370
  */
371
371
  backup?: MemoryBackupConfig;
372
372
  /**
373
- * 云同步配置(2026-11-11):`{ mirrorDir, remote?, branch?, envName? }`。
373
+ * 云同步配置(2026-11-11;2026-11-12 改成显式开关):
374
+ * `{ enabled, mirrorDir, remote?, branch?, envName? }`。
374
375
  *
375
- * 缺省不配 = 不同步。设计见 `docs/设计/2026-11-06-记忆云同步-设计方案.md`。
376
+ * **开关是 `enabled`,不是「配了 remote 就算开」**——与日记同一个模式。缺省不配 = 不同步。
377
+ * 设计见 `docs/设计/2026-11-06-记忆云同步-设计方案.md`。
376
378
  */
377
379
  sync?: unknown;
378
380
  /**
379
- * 日记开关的**部署默认值**(2026-10-24)。运行中的选择在面板里改,写入
380
- * `$DSH_HOME/memory-settings.json`;两边都开才开——否则部署时关掉会被
381
+ * 日记开关的**部署默认值**(2026-10-24)。运行中的选择在面板里改,值写进
382
+ * profile 的 `cordis.patch.yml`(**不另设存储文件**,见设计文档 §四);两边都开才开——否则部署时关掉会被
381
383
  * 使用者那一侧缺省的 `true` 翻回来。
382
384
  *
383
385
  * 关掉时日记工具会拒绝写入、不注入指针、也不提醒;已有的 md 文件原样保留
package/dist/index.d.ts CHANGED
@@ -34,11 +34,31 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
34
34
  embeddings: z<any, any, "plain">;
35
35
  backup: z<any, any, "plain">;
36
36
  /**
37
- * 云同步配置(2026-11-11):`{ mirrorDir, remote?, branch?, envName? }`。
37
+ * 云同步配置(2026-11-11;2026-11-12 改成显式开关)。
38
38
  *
39
- * 缺省不配 = 不同步。设计见 `docs/设计/2026-11-06-记忆云同步-设计方案.md`。
39
+ * **用 `enabled` 开关,而不是「配了 remote 就算开」**:与日记同一个模式。语义清楚得多
40
+ * ——「我打开了这个功能」和「我碰巧填了一个字段」是两件事;而且开启后可以只配一部分
41
+ * (比如只做本地镜像、暂不推送)。
42
+ *
43
+ * `volatile()` 让 `enabled` 能在运行时改:设置面板把值写进 profile 的
44
+ * `cordis.patch.yml`,是否即时生效取决于该 profile 的 `patchReload`(`live` 才热重载)。
45
+ * 其余字段(remote / branch 等)改了要重启——它们决定的是连接目标,不是临时状态。
46
+ *
47
+ * 默认 **关闭**:实验性功能不该影响别人的安装。
40
48
  */
41
- sync: z<any, any, "plain">;
49
+ sync: z<NoInfer<Schemastery.ObjectS<NoInfer<{
50
+ enabled: z<boolean, boolean, "defined">;
51
+ mirrorDir: z<string, string, "defined">;
52
+ remote: z<string, string, "defined">;
53
+ branch: z<string, string, "defined">;
54
+ envName: z<string, string, "defined">;
55
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
56
+ enabled: z<boolean, boolean, "defined">;
57
+ mirrorDir: z<string, string, "defined">;
58
+ remote: z<string, string, "defined">;
59
+ branch: z<string, string, "defined">;
60
+ envName: z<string, string, "defined">;
61
+ }>>>, "volatile-defined">;
42
62
  /**
43
63
  * 日记开关。默认开——这是**功能默认,不是采集默认**。
44
64
  *
@@ -61,11 +81,31 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
61
81
  embeddings: z<any, any, "plain">;
62
82
  backup: z<any, any, "plain">;
63
83
  /**
64
- * 云同步配置(2026-11-11):`{ mirrorDir, remote?, branch?, envName? }`。
84
+ * 云同步配置(2026-11-11;2026-11-12 改成显式开关)。
65
85
  *
66
- * 缺省不配 = 不同步。设计见 `docs/设计/2026-11-06-记忆云同步-设计方案.md`。
86
+ * **用 `enabled` 开关,而不是「配了 remote 就算开」**:与日记同一个模式。语义清楚得多
87
+ * ——「我打开了这个功能」和「我碰巧填了一个字段」是两件事;而且开启后可以只配一部分
88
+ * (比如只做本地镜像、暂不推送)。
89
+ *
90
+ * `volatile()` 让 `enabled` 能在运行时改:设置面板把值写进 profile 的
91
+ * `cordis.patch.yml`,是否即时生效取决于该 profile 的 `patchReload`(`live` 才热重载)。
92
+ * 其余字段(remote / branch 等)改了要重启——它们决定的是连接目标,不是临时状态。
93
+ *
94
+ * 默认 **关闭**:实验性功能不该影响别人的安装。
67
95
  */
68
- sync: z<any, any, "plain">;
96
+ sync: z<NoInfer<Schemastery.ObjectS<NoInfer<{
97
+ enabled: z<boolean, boolean, "defined">;
98
+ mirrorDir: z<string, string, "defined">;
99
+ remote: z<string, string, "defined">;
100
+ branch: z<string, string, "defined">;
101
+ envName: z<string, string, "defined">;
102
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
103
+ enabled: z<boolean, boolean, "defined">;
104
+ mirrorDir: z<string, string, "defined">;
105
+ remote: z<string, string, "defined">;
106
+ branch: z<string, string, "defined">;
107
+ envName: z<string, string, "defined">;
108
+ }>>>, "volatile-defined">;
69
109
  /**
70
110
  * 日记开关。默认开——这是**功能默认,不是采集默认**。
71
111
  *
package/dist/index.js CHANGED
@@ -45,11 +45,25 @@ export const Config = z.object({
45
45
  embeddings: z.any(),
46
46
  backup: z.any(),
47
47
  /**
48
- * 云同步配置(2026-11-11):`{ mirrorDir, remote?, branch?, envName? }`。
48
+ * 云同步配置(2026-11-11;2026-11-12 改成显式开关)。
49
49
  *
50
- * 缺省不配 = 不同步。设计见 `docs/设计/2026-11-06-记忆云同步-设计方案.md`。
50
+ * **用 `enabled` 开关,而不是「配了 remote 就算开」**:与日记同一个模式。语义清楚得多
51
+ * ——「我打开了这个功能」和「我碰巧填了一个字段」是两件事;而且开启后可以只配一部分
52
+ * (比如只做本地镜像、暂不推送)。
53
+ *
54
+ * `volatile()` 让 `enabled` 能在运行时改:设置面板把值写进 profile 的
55
+ * `cordis.patch.yml`,是否即时生效取决于该 profile 的 `patchReload`(`live` 才热重载)。
56
+ * 其余字段(remote / branch 等)改了要重启——它们决定的是连接目标,不是临时状态。
57
+ *
58
+ * 默认 **关闭**:实验性功能不该影响别人的安装。
51
59
  */
52
- sync: z.any(),
60
+ sync: z.object({
61
+ enabled: z.boolean().default(false),
62
+ mirrorDir: z.string().default(''),
63
+ remote: z.string().default(''),
64
+ branch: z.string().default('main'),
65
+ envName: z.string().default(''),
66
+ }).default({ enabled: false }).volatile(),
53
67
  /**
54
68
  * 日记开关。默认开——这是**功能默认,不是采集默认**。
55
69
  *
@@ -93,6 +107,35 @@ function diaryOn(config) {
93
107
  : raw;
94
108
  return unwrapped?.enabled !== false;
95
109
  }
110
+ /**
111
+ * 解包 `config.sync`。
112
+ *
113
+ * `config.sync` 经 loader 解析后是 `Volatile<{...}>`(`settings` 侧的解包就是
114
+ * `isVolatile(v) → v.get()`)——**schema 声明了 `.volatile()`,所以挂载时一定会被包装**,
115
+ * 直接读字段拿到的是空的。裸对象只在直接调用 `apply` 的场合出现,一并兜住。
116
+ * 开关与字段读的是同一份解包结果,所以只在这里做一次。
117
+ * @param config - 插件配置。
118
+ * @returns 解包后的对象;没配就是空对象(字段全按「缺省」处理)。
119
+ */
120
+ function syncSettings(config) {
121
+ const raw = config?.sync;
122
+ if (raw === undefined || raw === null)
123
+ return {};
124
+ const unwrapped = typeof raw.get === 'function'
125
+ ? raw.get()
126
+ : raw;
127
+ return (unwrapped ?? {});
128
+ }
129
+ /**
130
+ * 读云同步开关。
131
+ *
132
+ * 判据是显式 `enabled`,不是「配了 remote」——与日记同一个模式(2026-11-12)。
133
+ * 与日记相反:**解不出来就是关**(`=== true` 才开)。默认值必须与 schema 的
134
+ * `default(false)` 一致——实验性功能不能因为读不到开关就自己跑起来。
135
+ */
136
+ function syncOn(config) {
137
+ return syncSettings(config).enabled === true;
138
+ }
96
139
  /** 日记提醒的冷却窗口(毫秒)。6 小时是拍出来的第一个值,实测后再调。 */
97
140
  const DIARY_REMIND_MS = 6 * 60 * 60 * 1000;
98
141
  /**
@@ -508,11 +551,11 @@ export async function apply(ctx, config) {
508
551
  order: 115,
509
552
  text: GUIDANCE,
510
553
  });
511
- // 云同步是**默认关闭**的(2026-11-11):实验性功能不该影响别人的安装。判据是
512
- // 「配了 remote」——只配 mirrorDir 没有意义(本地镜像不解决任何问题)。
513
- // 关闭时那两个工具**根本不注册**,而不是注册了再报错:模型看不到就不会去调。
514
- const syncRaw = (config?.sync ?? {});
515
- const syncEnabled = typeof syncRaw.remote === 'string' && syncRaw.remote !== '';
554
+ // 云同步**默认关闭**(2026-11-11):实验性功能不该影响别人的安装。判据是显式开关
555
+ // `enabled`(2026-11-12,与日记一致)——「我打开了这个功能」与「我碰巧填了一个
556
+ // remote」是两件事,后者会让一次配置试验变成意外开机。关闭时那两个工具**根本不
557
+ // 注册**,而不是注册了再报错:模型看不到就不会去调。
558
+ const syncEnabled = syncOn(config);
516
559
  // 0.3.2:记忆机制自述——常驻注入(不落用户存储),随发版更新。
517
560
  // order 49 < recall(50):机制说明在具体记忆内容之前。
518
561
  ctx.systemPrompt.context({
@@ -1024,10 +1067,12 @@ export async function apply(ctx, config) {
1024
1067
  // 决定。把 `git pull --rebase && git add && git commit && git push` 那一串交给模型,
1025
1068
  // 出错难排查,也容易越权。
1026
1069
  //
1027
- // 缺省不启用:没配 `memory.sync` 时这两个工具直接报「未配置」,而不是默默什么都不做——
1028
- // 静默不动作比报错更坏。
1070
+ // 缺省不启用:没打开 `memory.sync.enabled` 时这两个工具根本不注册(见上面的
1071
+ // `syncEnabled`);打开之后配置才可能不全——`remote` 为空就只做本地提交。
1029
1072
  const syncConfig = () => {
1030
- const raw = (config?.sync ?? {});
1073
+ // 走 {@link syncSettings} 解包:`.volatile()` 包装后直接读字段是空的,
1074
+ // 「开关打开了、配置读不出来」正是半成品状态会踩的坑。
1075
+ const raw = syncSettings(config);
1031
1076
  const mirrorDir = typeof raw.mirrorDir === 'string' && raw.mirrorDir !== ''
1032
1077
  ? raw.mirrorDir
1033
1078
  : join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'memory-sync');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-memory",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Cross-session plaintext memory plugin for the DeepSeek Harness — deterministic BM25 recall, writes take effect immediately, signal-driven injection, no embeddings",
5
5
  "license": "MIT",
6
6
  "publishConfig": {