@max-null/dsh-memory 0.22.0 → 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 +17 -2
- package/client.js +72 -80
- package/dist/engine.d.ts +6 -4
- package/dist/index.d.ts +46 -6
- package/dist/index.js +109 -56
- package/dist/self.d.ts +3 -0
- package/dist/self.js +30 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,10 +144,24 @@ DSH 存储层的写入是「读—改—写全量覆盖」,且官方两个后
|
|
|
144
144
|
- 只读文件、只复制——**绝不复用引擎的写入路径**。写路径会先 `reload()` 再合并,磁盘缺失时它会把内存覆盖掉,正是这次事故的成因;备份不能走同一条路。
|
|
145
145
|
- 日记按**逐字节内容**判断是否已备份过,不按 mtime:复制与编辑都会动 mtime,内容才是事实。
|
|
146
146
|
|
|
147
|
-
## 云同步(2026-11-11
|
|
147
|
+
## 云同步(2026-11-11)· **默认关闭**
|
|
148
148
|
|
|
149
149
|
把本机记忆镜像到一个 git 仓库,多个环境之间共享同一份记忆。
|
|
150
150
|
|
|
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
|
+
> 无论哪种情况都要重启。
|
|
164
|
+
|
|
151
165
|
**为什么是「一条记忆一个文件」。** 整份 `memory.json` 放进 git 会撞:2026-11-06 实测,两端各新增一条记忆后 merge,`git` 报 `CONFLICT (content)`——两条记录挤在同一个插入点,按「取一边」解决会**静默丢掉另一端那条**,而 git 仍报「合并成功」。改成一条一个文件后同一场景是 `Merge made by the 'ort' strategy`,0 冲突。
|
|
152
166
|
|
|
153
167
|
**项目身份用项目名,不用路径。** 本机 `H:\MaxNull\WorkStation` 与另一台机器 `D:\WorkStation` 是同一个项目——目录名(统一小写)决定它在 `records/project/` 下落到哪。用绝对路径哈希的话,换个盘符就成了另一个项目,记忆会静默分裂成两份互不相认的。
|
|
@@ -155,8 +169,9 @@ DSH 存储层的写入是「读—改—写全量覆盖」,且官方两个后
|
|
|
155
169
|
```yaml
|
|
156
170
|
memory:
|
|
157
171
|
sync:
|
|
172
|
+
enabled: true # 总开关;关着两个工具根本不注册
|
|
158
173
|
mirrorDir: '' # 缺省 <DSH_HOME>/memory-sync
|
|
159
|
-
remote: '' # https
|
|
174
|
+
remote: '' # https 远端;留空只做本地提交
|
|
160
175
|
branch: main
|
|
161
176
|
envName: '' # 本环境短名,冲突留证时用作后缀
|
|
162
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
1035
|
-
const {
|
|
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)" } },
|
|
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":
|
|
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
|
-
|
|
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)(
|
|
1135
|
+
}, () => (0, import_react.createElement)(MemorySettingsCard, { form: settingsForm })));
|
|
1144
1136
|
}
|
|
1145
1137
|
} catch (error) {
|
|
1146
|
-
console.warn("dsh-memory: \
|
|
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
|
|
373
|
+
* 云同步配置(2026-11-11;2026-11-12 改成显式开关):
|
|
374
|
+
* `{ enabled, mirrorDir, remote?, branch?, envName? }`。
|
|
374
375
|
*
|
|
375
|
-
*
|
|
376
|
+
* **开关是 `enabled`,不是「配了 remote 就算开」**——与日记同一个模式。缺省不配 = 不同步。
|
|
377
|
+
* 设计见 `docs/设计/2026-11-06-记忆云同步-设计方案.md`。
|
|
376
378
|
*/
|
|
377
379
|
sync?: unknown;
|
|
378
380
|
/**
|
|
379
|
-
* 日记开关的**部署默认值**(2026-10-24
|
|
380
|
-
*
|
|
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
|
|
37
|
+
* 云同步配置(2026-11-11;2026-11-12 改成显式开关)。
|
|
38
38
|
*
|
|
39
|
-
*
|
|
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<
|
|
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
|
|
84
|
+
* 云同步配置(2026-11-11;2026-11-12 改成显式开关)。
|
|
65
85
|
*
|
|
66
|
-
*
|
|
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<
|
|
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
|
@@ -13,7 +13,7 @@ import { MemoryGateway } from "./remote.js";
|
|
|
13
13
|
import { runMemoryBackup } from "./backup.js";
|
|
14
14
|
import { ANCHOR_KINDS } from "./anchors.js";
|
|
15
15
|
import { mountMemoryApi } from "./routes.js";
|
|
16
|
-
import {
|
|
16
|
+
import { buildSelfDescription } from "./self.js";
|
|
17
17
|
import { appendEntry, diaryDir, lastWriteAt, pointerLine, readDay, recentDates } from "./diary.js";
|
|
18
18
|
import z from '@deepseek-ai/schemastery';
|
|
19
19
|
import { DEFAULT_INJECTION_BUDGET, DEFAULT_SUMMARY_CHARS, candidateNotice, indexNotice, neighborNotice, neutralizeBraces, omittedNotice, renderInjection, staleNotice } from "./injection.js";
|
|
@@ -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
|
|
48
|
+
* 云同步配置(2026-11-11;2026-11-12 改成显式开关)。
|
|
49
49
|
*
|
|
50
|
-
*
|
|
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.
|
|
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,12 +551,18 @@ export async function apply(ctx, config) {
|
|
|
508
551
|
order: 115,
|
|
509
552
|
text: GUIDANCE,
|
|
510
553
|
});
|
|
554
|
+
// 云同步**默认关闭**(2026-11-11):实验性功能不该影响别人的安装。判据是显式开关
|
|
555
|
+
// `enabled`(2026-11-12,与日记一致)——「我打开了这个功能」与「我碰巧填了一个
|
|
556
|
+
// remote」是两件事,后者会让一次配置试验变成意外开机。关闭时那两个工具**根本不
|
|
557
|
+
// 注册**,而不是注册了再报错:模型看不到就不会去调。
|
|
558
|
+
const syncEnabled = syncOn(config);
|
|
511
559
|
// 0.3.2:记忆机制自述——常驻注入(不落用户存储),随发版更新。
|
|
512
560
|
// order 49 < recall(50):机制说明在具体记忆内容之前。
|
|
513
561
|
ctx.systemPrompt.context({
|
|
514
562
|
name: 'memory:self',
|
|
515
563
|
order: 49,
|
|
516
|
-
|
|
564
|
+
// 自述里的工具清单必须与实际注册的一致:漏一个,模型就按一份不存在的工具面行事。
|
|
565
|
+
text: buildSelfDescription(syncEnabled),
|
|
517
566
|
});
|
|
518
567
|
const injectionBudget = config?.injectionBudget ?? DEFAULT_INJECTION_BUDGET;
|
|
519
568
|
const summaryChars = config?.summaryChars ?? DEFAULT_SUMMARY_CHARS;
|
|
@@ -1018,10 +1067,12 @@ export async function apply(ctx, config) {
|
|
|
1018
1067
|
// 决定。把 `git pull --rebase && git add && git commit && git push` 那一串交给模型,
|
|
1019
1068
|
// 出错难排查,也容易越权。
|
|
1020
1069
|
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1070
|
+
// 缺省不启用:没打开 `memory.sync.enabled` 时这两个工具根本不注册(见上面的
|
|
1071
|
+
// `syncEnabled`);打开之后配置才可能不全——`remote` 为空就只做本地提交。
|
|
1023
1072
|
const syncConfig = () => {
|
|
1024
|
-
|
|
1073
|
+
// 走 {@link syncSettings} 解包:`.volatile()` 包装后直接读字段是空的,
|
|
1074
|
+
// 「开关打开了、配置读不出来」正是半成品状态会踩的坑。
|
|
1075
|
+
const raw = syncSettings(config);
|
|
1025
1076
|
const mirrorDir = typeof raw.mirrorDir === 'string' && raw.mirrorDir !== ''
|
|
1026
1077
|
? raw.mirrorDir
|
|
1027
1078
|
: join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'memory-sync');
|
|
@@ -1032,56 +1083,58 @@ export async function apply(ctx, config) {
|
|
|
1032
1083
|
...typeof raw.envName === 'string' && raw.envName !== '' ? { envName: raw.envName } : {},
|
|
1033
1084
|
};
|
|
1034
1085
|
};
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1086
|
+
if (syncEnabled) {
|
|
1087
|
+
ctx.tools.register(defineTool({
|
|
1088
|
+
name: registered('memory_sync'),
|
|
1089
|
+
description: 'Push this machine\'s memories to the sync repo, pull other machines\' memories back, or inspect sync state. Conflicts are never auto-resolved — both versions are kept under conflicts/ for a human to settle. 把本机记忆推到同步仓库 / 拉回其他机器的记忆 / 查看同步状态。冲突不会自动裁决:两边版本都留在 conflicts/ 里等人处理。',
|
|
1090
|
+
parameters: {
|
|
1091
|
+
action: {
|
|
1092
|
+
type: 'string',
|
|
1093
|
+
required: true,
|
|
1094
|
+
enum: ['status', 'pull', 'push'],
|
|
1095
|
+
description: 'status = 只看状态(不读不写);pull = 拉取并导入;push = 镜像、提交并推送。',
|
|
1096
|
+
},
|
|
1044
1097
|
},
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
render: (_args, value) => renderJson(value),
|
|
1049
|
-
},
|
|
1050
|
-
async execute(args) {
|
|
1051
|
-
const cfg = syncConfig();
|
|
1052
|
-
const run = args.action === 'push'
|
|
1053
|
-
? await syncPush(memory, cfg)
|
|
1054
|
-
: args.action === 'pull'
|
|
1055
|
-
? await syncPull(memory, cfg)
|
|
1056
|
-
: await syncStatus(cfg);
|
|
1057
|
-
// 工具返回值要求 `Record<string, JsonValue>`;这几个结果都是普通可序列化对象,
|
|
1058
|
-
// 结构化类型对不上而已。走一遍 JSON 往返顺便保证它确实可序列化。
|
|
1059
|
-
return JSON.parse(JSON.stringify(run));
|
|
1060
|
-
},
|
|
1061
|
-
presentCall: args => present('Sync memory', 'other'),
|
|
1062
|
-
}));
|
|
1063
|
-
ctx.tools.register(defineTool({
|
|
1064
|
-
name: registered('memory_conflicts'),
|
|
1065
|
-
description: 'List memory conflicts left by sync. Sync never auto-resolves them: if two machines changed the same memory, both versions were kept. 列出同步留下的记忆冲突。同步不会自动裁决——两端改过同一条时,两个版本都留着。',
|
|
1066
|
-
parameters: {
|
|
1067
|
-
action: {
|
|
1068
|
-
type: 'string',
|
|
1069
|
-
required: true,
|
|
1070
|
-
enum: ['list'],
|
|
1071
|
-
description: 'list = 列出 conflicts/ 下的冲突文件。',
|
|
1098
|
+
output: {
|
|
1099
|
+
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
1100
|
+
render: (_args, value) => renderJson(value),
|
|
1072
1101
|
},
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1102
|
+
async execute(args) {
|
|
1103
|
+
const cfg = syncConfig();
|
|
1104
|
+
const run = args.action === 'push'
|
|
1105
|
+
? await syncPush(memory, cfg)
|
|
1106
|
+
: args.action === 'pull'
|
|
1107
|
+
? await syncPull(memory, cfg)
|
|
1108
|
+
: await syncStatus(cfg);
|
|
1109
|
+
// 工具返回值要求 `Record<string, JsonValue>`;这几个结果都是普通可序列化对象,
|
|
1110
|
+
// 结构化类型对不上而已。走一遍 JSON 往返顺便保证它确实可序列化。
|
|
1111
|
+
return JSON.parse(JSON.stringify(run));
|
|
1112
|
+
},
|
|
1113
|
+
presentCall: args => present('Sync memory', 'other'),
|
|
1114
|
+
}));
|
|
1115
|
+
ctx.tools.register(defineTool({
|
|
1116
|
+
name: registered('memory_conflicts'),
|
|
1117
|
+
description: 'List memory conflicts left by sync. Sync never auto-resolves them: if two machines changed the same memory, both versions were kept. 列出同步留下的记忆冲突。同步不会自动裁决——两端改过同一条时,两个版本都留着。',
|
|
1118
|
+
parameters: {
|
|
1119
|
+
action: {
|
|
1120
|
+
type: 'string',
|
|
1121
|
+
required: true,
|
|
1122
|
+
enum: ['list'],
|
|
1123
|
+
description: 'list = 列出 conflicts/ 下的冲突文件。',
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
output: {
|
|
1127
|
+
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
1128
|
+
render: (_args, value) => renderJson(value),
|
|
1129
|
+
},
|
|
1130
|
+
async execute() {
|
|
1131
|
+
const cfg = syncConfig();
|
|
1132
|
+
const files = conflictFiles(cfg);
|
|
1133
|
+
return { count: files.length, files };
|
|
1134
|
+
},
|
|
1135
|
+
presentCall: () => present('List memory conflicts', 'other'),
|
|
1136
|
+
}));
|
|
1137
|
+
}
|
|
1085
1138
|
// 定时备份(2026-10-18 事故后新增):只读复制,绝不走引擎的写入路径。
|
|
1086
1139
|
// 首次延迟 1 分钟避开启动高峰;此后按 `backup.intervalMinutes` 重复;`unref` 不挡进程退出。
|
|
1087
1140
|
const backup = config?.backup;
|
package/dist/self.d.ts
CHANGED
package/dist/self.js
CHANGED
|
@@ -24,23 +24,33 @@ export const SELF_VERSION = (() => {
|
|
|
24
24
|
return 'unknown';
|
|
25
25
|
}
|
|
26
26
|
})();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
function buildDescription(syncEnabled) {
|
|
28
|
+
return (`[记忆系统自述] 本环境内置跨会话记忆服务(dsh-memory v${SELF_VERSION})。`
|
|
29
|
+
+ '你有 ' + (syncEnabled ? 16 : 14) + ' 个工具:**记忆工具** memory_save / memory_search / memory_list / memory_update / memory_confirm / memory_forget / memory_move / memory_sweep(`memory_confirm` 仅在用户明确要求放行隔离记录时用;`memory_move` 把放错了层的记忆移到 global、当前工作区或相对祖先;`memory_sweep` 是只读体检——失效清单、久未命中、候选与预算占用,不修改任何状态);**提示词模板工具** prompt_search / prompt_get / prompt_list / prompt_add(模板是 md 文件、**永不注入**,走这条通道,不会混进记忆检索);'
|
|
30
|
+
+ (syncEnabled
|
|
31
|
+
? '**同步工具** memory_sync / memory_conflicts:跨环境同步;`memory_sync` 的 `status` 只看状态、`pull` 拉取并导入、`push` 镜像并推送,`memory_conflicts` 列出**未被自动裁决**的冲突,同步**不会**自动决定哪一边对——两边都改过同一条时两个版本都留着等人处理,记忆被静默覆盖就没了;'
|
|
32
|
+
: '')
|
|
33
|
+
+ '**日记工具** diary_read / diary_write:日记是 md 文件,落在 `<DSH_HOME>/diary`,与记忆**分开存、不进检索、不占注入预算**。记的是经历——发生了什么(有细节)、我怎么想(可以有情绪、可以没结论)、关于他(他说了什么、我注意到了什么);不是任务清单(那是 git log),也不是结论(那是 memory_save)。读它不吃预算,需要时整篇读。'
|
|
34
|
+
+ '记忆分两层:`global` 跨项目(偏好 / 习惯 / 环境知识),`project` 跟随工作区(落在 `<cwd>/.dsh/storages/`,随 git 提交分享)。'
|
|
35
|
+
+ '**何时写入**(不必刻意找机会,遇到下面这些就动手):'
|
|
36
|
+
+ '① 用户表达长期偏好或约定(「以后都…」「记住…」「我们的规矩是…」);'
|
|
37
|
+
+ '② 用户纠正了你的做法或理解——把正确结论记下来;'
|
|
38
|
+
+ '③ 得出跨会话仍然有用的结论、路径或坑,而非本次任务的临时细节。'
|
|
39
|
+
+ '**不要记**:临时状态、未经验证的推测、能从代码或文档直接读到的事实(那类记「去哪查」即可)。'
|
|
40
|
+
+ '**怎么写**:一条一件事,并给多角度关键词(同义词、缩写、中英),否则将来检索不到它。'
|
|
41
|
+
+ '**锚点(可选)**:若这条记忆描述的是「随某个环境值变化的事实」(某工具版本下的行为、某环境变量决定的配置),用 anchor 参数把它绑到那个值上——值变了它会自动失效并标 stale,不会再被当成仍然正确;绑错了用 memory_update 改(当场重新判定,成立即复活),不要了就传 `anchor: null` 清除。当前锚点随记录一起返回。'
|
|
42
|
+
+ '写入即生效(`approved`),人工只在例外时介入;命中密钥/凭据规则的写入会被隔离(不进注入与检索)。'
|
|
43
|
+
+ '**常驻注入**:每轮注入「global + 当前会话工作区」里 approved 且开着常驻开关的记忆单行摘要(形如 `- [memory:id:namespace] 摘要`);全文要靠 memory_search 取。'
|
|
44
|
+
+ '常驻开关由人(或模型显式传 `injected`)决定:**自动改变注入状态的规则已全部停用**——命中次数不再自动升常驻(它只喂体检报告的候选提示),30 天未命中的自动撤下也已改为**只计量不生效**(撤下来就基本不会自己回去,那种动作不该由一个没有分布依据的阈值决定)。**给 memory_save / memory_update 传 `injected` 即显式接管**。'
|
|
45
|
+
+ '**钉的判据是「失效可察觉性」**:这条记忆若没被想起来,你会不会根本意识不到自己漏了它?会 → 钉常驻(规则 / 委托 / 判据);不会 → 留检索(事实 / 参考 / 案例)。'
|
|
46
|
+
+ '**注入有预算**(默认 1500 字符,约装 11 条),装不下的整条丢弃、按**最近更新**优先取舍(不是最近被检索——检索是「需要时才用」的行为,拿它决定「谁每轮在场」会让查得多的挤掉钉得牢的)——所以「钉了常驻」只等于「有资格排队」。'
|
|
47
|
+
+ '装不下时注入末尾会出现一行 `(另有 N 条常驻因预算未注入:…短摘要…)`——**那是「有常驻记忆并不在场」的唯一信号**,看到它就该逐条评估撤下。'
|
|
48
|
+
+ '发现某条记忆与实际不符时用 memory_update 修正(保持生效);确认没用的用 memory_forget。'
|
|
49
|
+
+ '(English: dsh-memory provides cross-session memory. Writes take effect immediately; save durable preferences, corrections and reusable conclusions — not transient state, guesses, or facts readable from the repo (record where to look instead). Always add multi-angle keywords. Pass `injected: true` to keep one resident every turn — reserve that for rules and standing agreements; facts stay search-only. Resident injection runs on a budget, so a pinned memory is only queued, never guaranteed to be in context.)');
|
|
50
|
+
}
|
|
51
|
+
/** 能力全貌(面板 / 远端视图用;同步未启用时也展示它的存在)。 */
|
|
52
|
+
export const SELF_DESCRIPTION = buildDescription(true);
|
|
53
|
+
/** 按当前是否启用同步生成自述——系统提示那一路用这个。 */
|
|
54
|
+
export function buildSelfDescription(syncEnabled) {
|
|
55
|
+
return buildDescription(syncEnabled);
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-memory",
|
|
3
|
-
"version": "0.22.
|
|
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": {
|