dsh-tdai-memory 0.2.10 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +130 -130
- package/README.zh.md +72 -72
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/client.js +349 -349
- package/cordis.patch.yml +8 -8
- package/index.js +421 -421
- package/package.json +5 -2
- package/recall-inject.js +86 -86
- package/vendor/dsh-settings-expose.js +75 -75
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
- package/vendor/tdai/config.js +363 -363
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +640 -640
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
- package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
- package/vendor/tdai/core/prompts/persona-generation.js +35 -35
- package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +57 -57
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/seed-runtime.js +314 -314
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1632 -1632
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tdai-core.js +446 -446
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
package/client.js
CHANGED
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-tdai-memory — browser half.
|
|
3
|
-
*
|
|
4
|
-
* A "记忆" section inside the Web UI settings page: edits the `tdai-memory`
|
|
5
|
-
* settings namespace (data dir, extraction LLM, embeddings, capture/extract/
|
|
6
|
-
* recall switches, tools) through the settings scope transport plus nested
|
|
7
|
-
* `settings.mutate` ops. TdaiCore is built at startup, so changes apply
|
|
8
|
-
* after a restart (noted in the UI).
|
|
9
|
-
*
|
|
10
|
-
* Hand-written ModuleLoader bundle — no build step required.
|
|
11
|
-
*/
|
|
12
|
-
window.__ModuleLoader__.load({
|
|
13
|
-
id: "dsh-tdai-memory",
|
|
14
|
-
factory: (require) => {
|
|
15
|
-
var module = { exports: {} };
|
|
16
|
-
var exports = module.exports;
|
|
17
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
18
|
-
var react = require("react");
|
|
19
|
-
var h = react.createElement;
|
|
20
|
-
|
|
21
|
-
// ── CSS (theme tokens) ────────────────────────────────────────────────
|
|
22
|
-
var CSS = ".__tm_root{max-width:640px;display:flex;flex-direction:column;gap:10px}" +
|
|
23
|
-
".__tm_group{font-size:13px;font-weight:700;color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px;margin:6px 0 2px}" +
|
|
24
|
-
".__tm_field{display:flex;flex-direction:column;gap:4px}" +
|
|
25
|
-
".__tm_label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px}" +
|
|
26
|
-
".__tm_override{font-size:10px;color:var(--dsw-alias-state-business-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:0 4px}" +
|
|
27
|
-
".__tm_hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
28
|
-
".__tm_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px;box-sizing:border-box;width:100%}" +
|
|
29
|
-
".__tm_row{display:flex;align-items:center;gap:8px}" +
|
|
30
|
-
".__tm_check{accent-color:var(--dsw-alias-state-business-primary)}" +
|
|
31
|
-
".__tm_actions{display:flex;gap:8px;align-items:center;margin-top:4px}" +
|
|
32
|
-
".__tm_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer}" +
|
|
33
|
-
".__tm_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
|
|
34
|
-
".__tm_btn:disabled{opacity:.5;cursor:default}" +
|
|
35
|
-
".__tm_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #3964fe);background:var(--dsw-alias-state-business-primary, #3964fe);color:#fff}" +
|
|
36
|
-
".__tm_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
|
|
37
|
-
".__tm_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
|
|
38
|
-
".__tm_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
|
|
39
|
-
var tagId = "dsh-tdai-memory/main.css";
|
|
40
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
41
|
-
var tag = document.createElement("style");
|
|
42
|
-
tag.dataset.plugin = "dsh-tdai-memory";
|
|
43
|
-
tag.dataset.pluginCss = tagId;
|
|
44
|
-
tag.textContent = CSS;
|
|
45
|
-
document.head.appendChild(tag);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ── locale ────────────────────────────────────────────────────────────
|
|
49
|
-
var NS = "tdaiMemory";
|
|
50
|
-
var inject = ["slots", "locale", "settingsScope", "connection"];
|
|
51
|
-
var zh = {
|
|
52
|
-
nav: "记忆",
|
|
53
|
-
intro: "TDAI 记忆配置:L0 捕获 → L1 结构化提取 → 召回注入。密钥只写不读。TdaiCore 在启动时构建,修改后需重启生效。",
|
|
54
|
-
groupData: "数据",
|
|
55
|
-
groupLlm: "提取 LLM(L1/L2/L3)",
|
|
56
|
-
groupEmbedding: "向量 Embedding",
|
|
57
|
-
groupCapture: "捕获",
|
|
58
|
-
groupExtraction: "提取",
|
|
59
|
-
groupRecall: "召回",
|
|
60
|
-
groupTools: "工具",
|
|
61
|
-
fieldDataDir: "数据目录(空 = ~/.memory-tencentdb/memory-tdai)",
|
|
62
|
-
fieldLlmBaseUrl: "Base URL",
|
|
63
|
-
fieldLlmApiKey: "API Key",
|
|
64
|
-
fieldLlmModel: "模型",
|
|
65
|
-
fieldLlmMaxTokens: "最大输出 Tokens",
|
|
66
|
-
fieldLlmTimeoutMs: "超时(毫秒)",
|
|
67
|
-
fieldEmbeddingBaseUrl: "Base URL",
|
|
68
|
-
fieldEmbeddingApiKey: "API Key",
|
|
69
|
-
fieldEmbeddingModel: "模型",
|
|
70
|
-
fieldEmbeddingDimensions: "向量维度",
|
|
71
|
-
fieldEmbeddingSendDimensions: "请求带 dimensions 参数",
|
|
72
|
-
fieldCaptureEnabled: "捕获对话(L0)",
|
|
73
|
-
fieldExtractionEnabled: "结构化提取(L1)",
|
|
74
|
-
fieldExtractionEnableDedup: "冲突检测(额外 LLM 调用)",
|
|
75
|
-
fieldRecallEnabled: "召回注入",
|
|
76
|
-
fieldRecallMaxResults: "最大召回条数",
|
|
77
|
-
fieldRecallScoreThreshold: "相似度阈值",
|
|
78
|
-
fieldRecallTimeoutMs: "召回超时(毫秒)",
|
|
79
|
-
fieldToolsEnabled: "注册搜索工具(tdai_memory_search / tdai_conversation_search)",
|
|
80
|
-
secretHint: "留空保持当前密钥。",
|
|
81
|
-
save: "保存",
|
|
82
|
-
reset: "恢复默认",
|
|
83
|
-
saved: "已保存(重启后生效)",
|
|
84
|
-
saving: "保存中…",
|
|
85
|
-
error: "保存失败",
|
|
86
|
-
unavailable: "设置命名空间不可用(服务端未注册 tdai-memory 命名空间?)",
|
|
87
|
-
overridden: "已覆盖",
|
|
88
|
-
loading: "加载中…"
|
|
89
|
-
};
|
|
90
|
-
var en = {
|
|
91
|
-
nav: "Memory",
|
|
92
|
-
intro: "TDAI memory config: L0 capture → L1 extraction → recall injection. Keys are write-only. TdaiCore is built at startup; changes apply after a restart.",
|
|
93
|
-
groupData: "Data",
|
|
94
|
-
groupLlm: "Extraction LLM (L1/L2/L3)",
|
|
95
|
-
groupEmbedding: "Embeddings",
|
|
96
|
-
groupCapture: "Capture",
|
|
97
|
-
groupExtraction: "Extraction",
|
|
98
|
-
groupRecall: "Recall",
|
|
99
|
-
groupTools: "Tools",
|
|
100
|
-
fieldDataDir: "Data dir (empty = ~/.memory-tencentdb/memory-tdai)",
|
|
101
|
-
fieldLlmBaseUrl: "Base URL",
|
|
102
|
-
fieldLlmApiKey: "API Key",
|
|
103
|
-
fieldLlmModel: "Model",
|
|
104
|
-
fieldLlmMaxTokens: "Max output tokens",
|
|
105
|
-
fieldLlmTimeoutMs: "Timeout (ms)",
|
|
106
|
-
fieldEmbeddingBaseUrl: "Base URL",
|
|
107
|
-
fieldEmbeddingApiKey: "API Key",
|
|
108
|
-
fieldEmbeddingModel: "Model",
|
|
109
|
-
fieldEmbeddingDimensions: "Vector dimensions",
|
|
110
|
-
fieldEmbeddingSendDimensions: "Send dimensions with requests",
|
|
111
|
-
fieldCaptureEnabled: "Capture conversations (L0)",
|
|
112
|
-
fieldExtractionEnabled: "Structured extraction (L1)",
|
|
113
|
-
fieldExtractionEnableDedup: "Conflict detection (extra LLM call)",
|
|
114
|
-
fieldRecallEnabled: "Recall injection",
|
|
115
|
-
fieldRecallMaxResults: "Max recall results",
|
|
116
|
-
fieldRecallScoreThreshold: "Similarity threshold",
|
|
117
|
-
fieldRecallTimeoutMs: "Recall timeout (ms)",
|
|
118
|
-
fieldToolsEnabled: "Register search tools (tdai_memory_search / tdai_conversation_search)",
|
|
119
|
-
secretHint: "Leave blank to keep the current key.",
|
|
120
|
-
save: "Save",
|
|
121
|
-
reset: "Reset",
|
|
122
|
-
saved: "Saved (applies after restart)",
|
|
123
|
-
saving: "Saving…",
|
|
124
|
-
error: "Save failed",
|
|
125
|
-
unavailable: "Settings namespace unavailable (tdai-memory namespace not registered server-side?)",
|
|
126
|
-
overridden: "overridden",
|
|
127
|
-
loading: "Loading…"
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// ── field spec: dotted path + type + group ─────────────────────────────
|
|
131
|
-
var FIELDS = [
|
|
132
|
-
{ path: ["dataDir"], label: "fieldDataDir", type: "text", group: "groupData" },
|
|
133
|
-
{ path: ["llm", "baseUrl"], label: "fieldLlmBaseUrl", type: "text", group: "groupLlm" },
|
|
134
|
-
{ path: ["llm", "apiKey"], label: "fieldLlmApiKey", type: "password", secret: true, group: "groupLlm" },
|
|
135
|
-
{ path: ["llm", "model"], label: "fieldLlmModel", type: "text", group: "groupLlm" },
|
|
136
|
-
{ path: ["llm", "maxTokens"], label: "fieldLlmMaxTokens", type: "number", group: "groupLlm" },
|
|
137
|
-
{ path: ["llm", "timeoutMs"], label: "fieldLlmTimeoutMs", type: "number", group: "groupLlm" },
|
|
138
|
-
{ path: ["embedding", "baseUrl"], label: "fieldEmbeddingBaseUrl", type: "text", group: "groupEmbedding" },
|
|
139
|
-
{ path: ["embedding", "apiKey"], label: "fieldEmbeddingApiKey", type: "password", secret: true, group: "groupEmbedding" },
|
|
140
|
-
{ path: ["embedding", "model"], label: "fieldEmbeddingModel", type: "text", group: "groupEmbedding" },
|
|
141
|
-
{ path: ["embedding", "dimensions"], label: "fieldEmbeddingDimensions", type: "number", group: "groupEmbedding" },
|
|
142
|
-
{ path: ["embedding", "sendDimensions"], label: "fieldEmbeddingSendDimensions", type: "checkbox", group: "groupEmbedding" },
|
|
143
|
-
{ path: ["captureEnabled"], label: "fieldCaptureEnabled", type: "checkbox", group: "groupCapture" },
|
|
144
|
-
{ path: ["extraction", "enabled"], label: "fieldExtractionEnabled", type: "checkbox", group: "groupExtraction" },
|
|
145
|
-
{ path: ["extraction", "enableDedup"], label: "fieldExtractionEnableDedup", type: "checkbox", group: "groupExtraction" },
|
|
146
|
-
{ path: ["recall", "enabled"], label: "fieldRecallEnabled", type: "checkbox", group: "groupRecall" },
|
|
147
|
-
{ path: ["recall", "maxResults"], label: "fieldRecallMaxResults", type: "number", group: "groupRecall" },
|
|
148
|
-
{ path: ["recall", "scoreThreshold"], label: "fieldRecallScoreThreshold", type: "number", group: "groupRecall" },
|
|
149
|
-
{ path: ["recall", "timeoutMs"], label: "fieldRecallTimeoutMs", type: "number", group: "groupRecall" },
|
|
150
|
-
{ path: ["toolsEnabled"], label: "fieldToolsEnabled", type: "checkbox", group: "groupTools" }
|
|
151
|
-
];
|
|
152
|
-
FIELDS.forEach(function (f) { f.key = f.path.join("."); });
|
|
153
|
-
|
|
154
|
-
function getPath(obj, path) {
|
|
155
|
-
var cur = obj;
|
|
156
|
-
for (var i = 0; i < path.length; i += 1) {
|
|
157
|
-
if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
|
|
158
|
-
cur = cur[path[i]];
|
|
159
|
-
}
|
|
160
|
-
return cur;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function MemorySection(props) {
|
|
164
|
-
var t = props.t;
|
|
165
|
-
var scope = props.scope;
|
|
166
|
-
var api = props.api;
|
|
167
|
-
var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
|
|
168
|
-
var ready = snapshot.status === "ready" && snapshot.value !== void 0;
|
|
169
|
-
var [draft, setDraft] = react.useState({});
|
|
170
|
-
var [busy, setBusy] = react.useState(false);
|
|
171
|
-
var [notice, setNotice] = react.useState(null);
|
|
172
|
-
var [error, setError] = react.useState(null);
|
|
173
|
-
|
|
174
|
-
react.useEffect(function () {
|
|
175
|
-
scope.load();
|
|
176
|
-
var alive = true;
|
|
177
|
-
var sync = function () { if (alive) setSnapshot(scope.getSnapshot()); };
|
|
178
|
-
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
179
|
-
return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
|
|
180
|
-
}, [scope]);
|
|
181
|
-
// Initialize the draft ONLY when the snapshot becomes ready — never on
|
|
182
|
-
// value churn. settingsScope.getSnapshot() returns a fresh object per
|
|
183
|
-
// call, so depending on snapshot.value would reset user input on every
|
|
184
|
-
// render (typing appears dead).
|
|
185
|
-
react.useEffect(function () {
|
|
186
|
-
if (ready) setDraft(Object.assign({}, valueToDraft(snapshot.value)));
|
|
187
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
|
-
}, [ready]);
|
|
189
|
-
|
|
190
|
-
if (snapshot.status === "unavailable") {
|
|
191
|
-
return h("p", { className: "__tm_unavailable" }, t("unavailable"));
|
|
192
|
-
}
|
|
193
|
-
if (!ready) return h("p", { className: "__tm_status" }, t("loading"));
|
|
194
|
-
|
|
195
|
-
var value = snapshot.value;
|
|
196
|
-
var user = snapshot.user || {};
|
|
197
|
-
|
|
198
|
-
function fieldDraft(f) {
|
|
199
|
-
if (f.type === "checkbox") return draft[f.key] !== void 0 ? draft[f.key] : Boolean(getPath(value, f.path));
|
|
200
|
-
return draft[f.key] !== void 0 ? draft[f.key] : String(getPath(value, f.path) ?? "");
|
|
201
|
-
}
|
|
202
|
-
function setField(f, v) {
|
|
203
|
-
setDraft(function (prev) { var next = Object.assign({}, prev); next[f.key] = v; return next; });
|
|
204
|
-
setNotice(null);
|
|
205
|
-
setError(null);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function onSave() {
|
|
209
|
-
setBusy(true); setNotice(null); setError(null);
|
|
210
|
-
var ops = [];
|
|
211
|
-
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
212
|
-
var f = FIELDS[i];
|
|
213
|
-
var d = fieldDraft(f);
|
|
214
|
-
var current = getPath(value, f.path);
|
|
215
|
-
if (f.type === "password") {
|
|
216
|
-
if (!d) continue; // blank keeps the current key
|
|
217
|
-
if (d === String(current ?? "")) continue;
|
|
218
|
-
ops.push({ op: "set", path: f.path, value: d });
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (f.type === "checkbox") {
|
|
222
|
-
if (Boolean(d) === Boolean(current)) continue;
|
|
223
|
-
ops.push(Boolean(d) ? { op: "set", path: f.path, value: true } : { op: "unset", path: f.path });
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (String(d) === String(current ?? "")) continue;
|
|
227
|
-
if (String(d).trim() === "" && getPath(user, f.path) === void 0) continue;
|
|
228
|
-
ops.push(String(d).trim() === "" ? { op: "unset", path: f.path } : { op: "set", path: f.path, value: f.type === "number" ? Number(d) : d });
|
|
229
|
-
}
|
|
230
|
-
if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
|
|
231
|
-
api.settings.mutate({
|
|
232
|
-
ns: "tdai-memory",
|
|
233
|
-
ops: ops,
|
|
234
|
-
...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
|
|
235
|
-
}).then(function (response) {
|
|
236
|
-
setBusy(false);
|
|
237
|
-
if (!response.result.ok) {
|
|
238
|
-
var detail = response.result.error || {};
|
|
239
|
-
setError(t("error") + ": " + String(detail.message || detail.code || "unknown"));
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
setNotice(t("saved"));
|
|
243
|
-
if (response.result.value) setDraft(Object.assign({}, valueToDraft(response.result.value)));
|
|
244
|
-
scope.load();
|
|
245
|
-
}).catch(function (e) {
|
|
246
|
-
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function onReset() {
|
|
251
|
-
setBusy(true); setNotice(null); setError(null);
|
|
252
|
-
api.settings.mutate({
|
|
253
|
-
ns: "tdai-memory",
|
|
254
|
-
ops: FIELDS.map(function (f) { return { op: "unset", path: f.path }; }),
|
|
255
|
-
...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
|
|
256
|
-
}).then(function (response) {
|
|
257
|
-
setBusy(false);
|
|
258
|
-
if (!response.result.ok) { setError(t("error")); return; }
|
|
259
|
-
setNotice(t("saved"));
|
|
260
|
-
if (response.result.value) setDraft(Object.assign({}, valueToDraft(response.result.value)));
|
|
261
|
-
scope.load();
|
|
262
|
-
}).catch(function (e) {
|
|
263
|
-
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
var nodes = [];
|
|
268
|
-
var lastGroup = null;
|
|
269
|
-
// forEach callback gives each handler its own `f` — a `for (var i)`
|
|
270
|
-
// loop would share one `f` across every onChange closure, so typing
|
|
271
|
-
// updated the LAST field's draft and the input appeared dead.
|
|
272
|
-
FIELDS.forEach(function (f) {
|
|
273
|
-
if (f.group !== lastGroup) {
|
|
274
|
-
lastGroup = f.group;
|
|
275
|
-
nodes.push(h("div", { key: "g" + f.group, className: "__tm_group" }, t(f.group)));
|
|
276
|
-
}
|
|
277
|
-
var overridden = getPath(user, f.path) !== void 0;
|
|
278
|
-
if (f.type === "checkbox") {
|
|
279
|
-
nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
|
|
280
|
-
h("span", { className: "__tm_row" },
|
|
281
|
-
h("input", { className: "__tm_check", type: "checkbox", checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
|
|
282
|
-
h("span", { className: "__tm_label" }, t(f.label)),
|
|
283
|
-
overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
|
|
284
|
-
)
|
|
285
|
-
));
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
|
|
289
|
-
h("span", { className: "__tm_label" },
|
|
290
|
-
t(f.label),
|
|
291
|
-
overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
|
|
292
|
-
),
|
|
293
|
-
h("input", {
|
|
294
|
-
className: "__tm_input",
|
|
295
|
-
type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
|
|
296
|
-
value: fieldDraft(f),
|
|
297
|
-
placeholder: f.type === "password" ? (overridden ? "••••••••" : t("secretHint")) : "",
|
|
298
|
-
onChange: function (e) { setField(f, e.target.value); }
|
|
299
|
-
}),
|
|
300
|
-
f.type === "password" ? h("span", { className: "__tm_hint" }, t("secretHint")) : null
|
|
301
|
-
));
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
return h("div", { className: "__tm_root" },
|
|
305
|
-
h("p", { className: "__tm_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
306
|
-
nodes,
|
|
307
|
-
h("div", { className: "__tm_actions" },
|
|
308
|
-
h("button", { type: "button", className: "__tm_btn __tm_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
|
|
309
|
-
h("button", { type: "button", className: "__tm_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
|
|
310
|
-
notice ? h("span", { className: "__tm_status" }, notice) : null,
|
|
311
|
-
busy ? h("span", { className: "__tm_status" }, t("saving")) : null,
|
|
312
|
-
error ? h("span", { className: "__tm_error" }, error) : null
|
|
313
|
-
)
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function valueToDraft(value) {
|
|
318
|
-
var out = {};
|
|
319
|
-
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
320
|
-
var f = FIELDS[i];
|
|
321
|
-
out[f.key] = f.type === "checkbox" ? Boolean(getPath(value, f.path)) : String(getPath(value, f.path) ?? "");
|
|
322
|
-
}
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ── plugin ────────────────────────────────────────────────────────────
|
|
327
|
-
function apply(ctx) {
|
|
328
|
-
var t = ctx.locale.bind(NS);
|
|
329
|
-
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tdai-memory: dictionaries");
|
|
330
|
-
var scope = ctx.settingsScope.bind({ namespace: "tdai-memory" });
|
|
331
|
-
var api = ctx.connection.api;
|
|
332
|
-
ctx.slots.inject("settings.section", function () {
|
|
333
|
-
return ctx.slots.register({
|
|
334
|
-
name: "settings.section",
|
|
335
|
-
id: "tdai-memory",
|
|
336
|
-
order: 26,
|
|
337
|
-
label: function () { return t("nav"); },
|
|
338
|
-
locale: NS
|
|
339
|
-
}, function (props) {
|
|
340
|
-
return h(MemorySection, Object.assign({}, props, { scope: scope, api: api }));
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
exports.apply = apply;
|
|
346
|
-
exports.inject = inject;
|
|
347
|
-
return module.exports;
|
|
348
|
-
}
|
|
349
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tdai-memory — browser half.
|
|
3
|
+
*
|
|
4
|
+
* A "记忆" section inside the Web UI settings page: edits the `tdai-memory`
|
|
5
|
+
* settings namespace (data dir, extraction LLM, embeddings, capture/extract/
|
|
6
|
+
* recall switches, tools) through the settings scope transport plus nested
|
|
7
|
+
* `settings.mutate` ops. TdaiCore is built at startup, so changes apply
|
|
8
|
+
* after a restart (noted in the UI).
|
|
9
|
+
*
|
|
10
|
+
* Hand-written ModuleLoader bundle — no build step required.
|
|
11
|
+
*/
|
|
12
|
+
window.__ModuleLoader__.load({
|
|
13
|
+
id: "dsh-tdai-memory",
|
|
14
|
+
factory: (require) => {
|
|
15
|
+
var module = { exports: {} };
|
|
16
|
+
var exports = module.exports;
|
|
17
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
18
|
+
var react = require("react");
|
|
19
|
+
var h = react.createElement;
|
|
20
|
+
|
|
21
|
+
// ── CSS (theme tokens) ────────────────────────────────────────────────
|
|
22
|
+
var CSS = ".__tm_root{max-width:640px;display:flex;flex-direction:column;gap:10px}" +
|
|
23
|
+
".__tm_group{font-size:13px;font-weight:700;color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px;margin:6px 0 2px}" +
|
|
24
|
+
".__tm_field{display:flex;flex-direction:column;gap:4px}" +
|
|
25
|
+
".__tm_label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px}" +
|
|
26
|
+
".__tm_override{font-size:10px;color:var(--dsw-alias-state-business-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:0 4px}" +
|
|
27
|
+
".__tm_hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
28
|
+
".__tm_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px;box-sizing:border-box;width:100%}" +
|
|
29
|
+
".__tm_row{display:flex;align-items:center;gap:8px}" +
|
|
30
|
+
".__tm_check{accent-color:var(--dsw-alias-state-business-primary)}" +
|
|
31
|
+
".__tm_actions{display:flex;gap:8px;align-items:center;margin-top:4px}" +
|
|
32
|
+
".__tm_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer}" +
|
|
33
|
+
".__tm_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
|
|
34
|
+
".__tm_btn:disabled{opacity:.5;cursor:default}" +
|
|
35
|
+
".__tm_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #3964fe);background:var(--dsw-alias-state-business-primary, #3964fe);color:#fff}" +
|
|
36
|
+
".__tm_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
|
|
37
|
+
".__tm_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
|
|
38
|
+
".__tm_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
|
|
39
|
+
var tagId = "dsh-tdai-memory/main.css";
|
|
40
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
41
|
+
var tag = document.createElement("style");
|
|
42
|
+
tag.dataset.plugin = "dsh-tdai-memory";
|
|
43
|
+
tag.dataset.pluginCss = tagId;
|
|
44
|
+
tag.textContent = CSS;
|
|
45
|
+
document.head.appendChild(tag);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── locale ────────────────────────────────────────────────────────────
|
|
49
|
+
var NS = "tdaiMemory";
|
|
50
|
+
var inject = ["slots", "locale", "settingsScope", "connection"];
|
|
51
|
+
var zh = {
|
|
52
|
+
nav: "记忆",
|
|
53
|
+
intro: "TDAI 记忆配置:L0 捕获 → L1 结构化提取 → 召回注入。密钥只写不读。TdaiCore 在启动时构建,修改后需重启生效。",
|
|
54
|
+
groupData: "数据",
|
|
55
|
+
groupLlm: "提取 LLM(L1/L2/L3)",
|
|
56
|
+
groupEmbedding: "向量 Embedding",
|
|
57
|
+
groupCapture: "捕获",
|
|
58
|
+
groupExtraction: "提取",
|
|
59
|
+
groupRecall: "召回",
|
|
60
|
+
groupTools: "工具",
|
|
61
|
+
fieldDataDir: "数据目录(空 = ~/.memory-tencentdb/memory-tdai)",
|
|
62
|
+
fieldLlmBaseUrl: "Base URL",
|
|
63
|
+
fieldLlmApiKey: "API Key",
|
|
64
|
+
fieldLlmModel: "模型",
|
|
65
|
+
fieldLlmMaxTokens: "最大输出 Tokens",
|
|
66
|
+
fieldLlmTimeoutMs: "超时(毫秒)",
|
|
67
|
+
fieldEmbeddingBaseUrl: "Base URL",
|
|
68
|
+
fieldEmbeddingApiKey: "API Key",
|
|
69
|
+
fieldEmbeddingModel: "模型",
|
|
70
|
+
fieldEmbeddingDimensions: "向量维度",
|
|
71
|
+
fieldEmbeddingSendDimensions: "请求带 dimensions 参数",
|
|
72
|
+
fieldCaptureEnabled: "捕获对话(L0)",
|
|
73
|
+
fieldExtractionEnabled: "结构化提取(L1)",
|
|
74
|
+
fieldExtractionEnableDedup: "冲突检测(额外 LLM 调用)",
|
|
75
|
+
fieldRecallEnabled: "召回注入",
|
|
76
|
+
fieldRecallMaxResults: "最大召回条数",
|
|
77
|
+
fieldRecallScoreThreshold: "相似度阈值",
|
|
78
|
+
fieldRecallTimeoutMs: "召回超时(毫秒)",
|
|
79
|
+
fieldToolsEnabled: "注册搜索工具(tdai_memory_search / tdai_conversation_search)",
|
|
80
|
+
secretHint: "留空保持当前密钥。",
|
|
81
|
+
save: "保存",
|
|
82
|
+
reset: "恢复默认",
|
|
83
|
+
saved: "已保存(重启后生效)",
|
|
84
|
+
saving: "保存中…",
|
|
85
|
+
error: "保存失败",
|
|
86
|
+
unavailable: "设置命名空间不可用(服务端未注册 tdai-memory 命名空间?)",
|
|
87
|
+
overridden: "已覆盖",
|
|
88
|
+
loading: "加载中…"
|
|
89
|
+
};
|
|
90
|
+
var en = {
|
|
91
|
+
nav: "Memory",
|
|
92
|
+
intro: "TDAI memory config: L0 capture → L1 extraction → recall injection. Keys are write-only. TdaiCore is built at startup; changes apply after a restart.",
|
|
93
|
+
groupData: "Data",
|
|
94
|
+
groupLlm: "Extraction LLM (L1/L2/L3)",
|
|
95
|
+
groupEmbedding: "Embeddings",
|
|
96
|
+
groupCapture: "Capture",
|
|
97
|
+
groupExtraction: "Extraction",
|
|
98
|
+
groupRecall: "Recall",
|
|
99
|
+
groupTools: "Tools",
|
|
100
|
+
fieldDataDir: "Data dir (empty = ~/.memory-tencentdb/memory-tdai)",
|
|
101
|
+
fieldLlmBaseUrl: "Base URL",
|
|
102
|
+
fieldLlmApiKey: "API Key",
|
|
103
|
+
fieldLlmModel: "Model",
|
|
104
|
+
fieldLlmMaxTokens: "Max output tokens",
|
|
105
|
+
fieldLlmTimeoutMs: "Timeout (ms)",
|
|
106
|
+
fieldEmbeddingBaseUrl: "Base URL",
|
|
107
|
+
fieldEmbeddingApiKey: "API Key",
|
|
108
|
+
fieldEmbeddingModel: "Model",
|
|
109
|
+
fieldEmbeddingDimensions: "Vector dimensions",
|
|
110
|
+
fieldEmbeddingSendDimensions: "Send dimensions with requests",
|
|
111
|
+
fieldCaptureEnabled: "Capture conversations (L0)",
|
|
112
|
+
fieldExtractionEnabled: "Structured extraction (L1)",
|
|
113
|
+
fieldExtractionEnableDedup: "Conflict detection (extra LLM call)",
|
|
114
|
+
fieldRecallEnabled: "Recall injection",
|
|
115
|
+
fieldRecallMaxResults: "Max recall results",
|
|
116
|
+
fieldRecallScoreThreshold: "Similarity threshold",
|
|
117
|
+
fieldRecallTimeoutMs: "Recall timeout (ms)",
|
|
118
|
+
fieldToolsEnabled: "Register search tools (tdai_memory_search / tdai_conversation_search)",
|
|
119
|
+
secretHint: "Leave blank to keep the current key.",
|
|
120
|
+
save: "Save",
|
|
121
|
+
reset: "Reset",
|
|
122
|
+
saved: "Saved (applies after restart)",
|
|
123
|
+
saving: "Saving…",
|
|
124
|
+
error: "Save failed",
|
|
125
|
+
unavailable: "Settings namespace unavailable (tdai-memory namespace not registered server-side?)",
|
|
126
|
+
overridden: "overridden",
|
|
127
|
+
loading: "Loading…"
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ── field spec: dotted path + type + group ─────────────────────────────
|
|
131
|
+
var FIELDS = [
|
|
132
|
+
{ path: ["dataDir"], label: "fieldDataDir", type: "text", group: "groupData" },
|
|
133
|
+
{ path: ["llm", "baseUrl"], label: "fieldLlmBaseUrl", type: "text", group: "groupLlm" },
|
|
134
|
+
{ path: ["llm", "apiKey"], label: "fieldLlmApiKey", type: "password", secret: true, group: "groupLlm" },
|
|
135
|
+
{ path: ["llm", "model"], label: "fieldLlmModel", type: "text", group: "groupLlm" },
|
|
136
|
+
{ path: ["llm", "maxTokens"], label: "fieldLlmMaxTokens", type: "number", group: "groupLlm" },
|
|
137
|
+
{ path: ["llm", "timeoutMs"], label: "fieldLlmTimeoutMs", type: "number", group: "groupLlm" },
|
|
138
|
+
{ path: ["embedding", "baseUrl"], label: "fieldEmbeddingBaseUrl", type: "text", group: "groupEmbedding" },
|
|
139
|
+
{ path: ["embedding", "apiKey"], label: "fieldEmbeddingApiKey", type: "password", secret: true, group: "groupEmbedding" },
|
|
140
|
+
{ path: ["embedding", "model"], label: "fieldEmbeddingModel", type: "text", group: "groupEmbedding" },
|
|
141
|
+
{ path: ["embedding", "dimensions"], label: "fieldEmbeddingDimensions", type: "number", group: "groupEmbedding" },
|
|
142
|
+
{ path: ["embedding", "sendDimensions"], label: "fieldEmbeddingSendDimensions", type: "checkbox", group: "groupEmbedding" },
|
|
143
|
+
{ path: ["captureEnabled"], label: "fieldCaptureEnabled", type: "checkbox", group: "groupCapture" },
|
|
144
|
+
{ path: ["extraction", "enabled"], label: "fieldExtractionEnabled", type: "checkbox", group: "groupExtraction" },
|
|
145
|
+
{ path: ["extraction", "enableDedup"], label: "fieldExtractionEnableDedup", type: "checkbox", group: "groupExtraction" },
|
|
146
|
+
{ path: ["recall", "enabled"], label: "fieldRecallEnabled", type: "checkbox", group: "groupRecall" },
|
|
147
|
+
{ path: ["recall", "maxResults"], label: "fieldRecallMaxResults", type: "number", group: "groupRecall" },
|
|
148
|
+
{ path: ["recall", "scoreThreshold"], label: "fieldRecallScoreThreshold", type: "number", group: "groupRecall" },
|
|
149
|
+
{ path: ["recall", "timeoutMs"], label: "fieldRecallTimeoutMs", type: "number", group: "groupRecall" },
|
|
150
|
+
{ path: ["toolsEnabled"], label: "fieldToolsEnabled", type: "checkbox", group: "groupTools" }
|
|
151
|
+
];
|
|
152
|
+
FIELDS.forEach(function (f) { f.key = f.path.join("."); });
|
|
153
|
+
|
|
154
|
+
function getPath(obj, path) {
|
|
155
|
+
var cur = obj;
|
|
156
|
+
for (var i = 0; i < path.length; i += 1) {
|
|
157
|
+
if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
|
|
158
|
+
cur = cur[path[i]];
|
|
159
|
+
}
|
|
160
|
+
return cur;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function MemorySection(props) {
|
|
164
|
+
var t = props.t;
|
|
165
|
+
var scope = props.scope;
|
|
166
|
+
var api = props.api;
|
|
167
|
+
var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
|
|
168
|
+
var ready = snapshot.status === "ready" && snapshot.value !== void 0;
|
|
169
|
+
var [draft, setDraft] = react.useState({});
|
|
170
|
+
var [busy, setBusy] = react.useState(false);
|
|
171
|
+
var [notice, setNotice] = react.useState(null);
|
|
172
|
+
var [error, setError] = react.useState(null);
|
|
173
|
+
|
|
174
|
+
react.useEffect(function () {
|
|
175
|
+
if (typeof scope.load === "function") scope.load();
|
|
176
|
+
var alive = true;
|
|
177
|
+
var sync = function () { if (alive) setSnapshot(scope.getSnapshot()); };
|
|
178
|
+
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
179
|
+
return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
|
|
180
|
+
}, [scope]);
|
|
181
|
+
// Initialize the draft ONLY when the snapshot becomes ready — never on
|
|
182
|
+
// value churn. settingsScope.getSnapshot() returns a fresh object per
|
|
183
|
+
// call, so depending on snapshot.value would reset user input on every
|
|
184
|
+
// render (typing appears dead).
|
|
185
|
+
react.useEffect(function () {
|
|
186
|
+
if (ready) setDraft(Object.assign({}, valueToDraft(snapshot.value)));
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
|
+
}, [ready]);
|
|
189
|
+
|
|
190
|
+
if (snapshot.status === "unavailable") {
|
|
191
|
+
return h("p", { className: "__tm_unavailable" }, t("unavailable"));
|
|
192
|
+
}
|
|
193
|
+
if (!ready) return h("p", { className: "__tm_status" }, t("loading"));
|
|
194
|
+
|
|
195
|
+
var value = snapshot.value;
|
|
196
|
+
var user = snapshot.user || {};
|
|
197
|
+
|
|
198
|
+
function fieldDraft(f) {
|
|
199
|
+
if (f.type === "checkbox") return draft[f.key] !== void 0 ? draft[f.key] : Boolean(getPath(value, f.path));
|
|
200
|
+
return draft[f.key] !== void 0 ? draft[f.key] : String(getPath(value, f.path) ?? "");
|
|
201
|
+
}
|
|
202
|
+
function setField(f, v) {
|
|
203
|
+
setDraft(function (prev) { var next = Object.assign({}, prev); next[f.key] = v; return next; });
|
|
204
|
+
setNotice(null);
|
|
205
|
+
setError(null);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onSave() {
|
|
209
|
+
setBusy(true); setNotice(null); setError(null);
|
|
210
|
+
var ops = [];
|
|
211
|
+
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
212
|
+
var f = FIELDS[i];
|
|
213
|
+
var d = fieldDraft(f);
|
|
214
|
+
var current = getPath(value, f.path);
|
|
215
|
+
if (f.type === "password") {
|
|
216
|
+
if (!d) continue; // blank keeps the current key
|
|
217
|
+
if (d === String(current ?? "")) continue;
|
|
218
|
+
ops.push({ op: "set", path: f.path, value: d });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (f.type === "checkbox") {
|
|
222
|
+
if (Boolean(d) === Boolean(current)) continue;
|
|
223
|
+
ops.push(Boolean(d) ? { op: "set", path: f.path, value: true } : { op: "unset", path: f.path });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (String(d) === String(current ?? "")) continue;
|
|
227
|
+
if (String(d).trim() === "" && getPath(user, f.path) === void 0) continue;
|
|
228
|
+
ops.push(String(d).trim() === "" ? { op: "unset", path: f.path } : { op: "set", path: f.path, value: f.type === "number" ? Number(d) : d });
|
|
229
|
+
}
|
|
230
|
+
if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
|
|
231
|
+
api.settings.mutate({
|
|
232
|
+
ns: "tdai-memory",
|
|
233
|
+
ops: ops,
|
|
234
|
+
...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
|
|
235
|
+
}).then(function (response) {
|
|
236
|
+
setBusy(false);
|
|
237
|
+
if (!response.result.ok) {
|
|
238
|
+
var detail = response.result.error || {};
|
|
239
|
+
setError(t("error") + ": " + String(detail.message || detail.code || "unknown"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
setNotice(t("saved"));
|
|
243
|
+
if (response.result.value) setDraft(Object.assign({}, valueToDraft(response.result.value)));
|
|
244
|
+
if (typeof scope.load === "function") scope.load();
|
|
245
|
+
}).catch(function (e) {
|
|
246
|
+
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function onReset() {
|
|
251
|
+
setBusy(true); setNotice(null); setError(null);
|
|
252
|
+
api.settings.mutate({
|
|
253
|
+
ns: "tdai-memory",
|
|
254
|
+
ops: FIELDS.map(function (f) { return { op: "unset", path: f.path }; }),
|
|
255
|
+
...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
|
|
256
|
+
}).then(function (response) {
|
|
257
|
+
setBusy(false);
|
|
258
|
+
if (!response.result.ok) { setError(t("error")); return; }
|
|
259
|
+
setNotice(t("saved"));
|
|
260
|
+
if (response.result.value) setDraft(Object.assign({}, valueToDraft(response.result.value)));
|
|
261
|
+
if (typeof scope.load === "function") scope.load();
|
|
262
|
+
}).catch(function (e) {
|
|
263
|
+
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var nodes = [];
|
|
268
|
+
var lastGroup = null;
|
|
269
|
+
// forEach callback gives each handler its own `f` — a `for (var i)`
|
|
270
|
+
// loop would share one `f` across every onChange closure, so typing
|
|
271
|
+
// updated the LAST field's draft and the input appeared dead.
|
|
272
|
+
FIELDS.forEach(function (f) {
|
|
273
|
+
if (f.group !== lastGroup) {
|
|
274
|
+
lastGroup = f.group;
|
|
275
|
+
nodes.push(h("div", { key: "g" + f.group, className: "__tm_group" }, t(f.group)));
|
|
276
|
+
}
|
|
277
|
+
var overridden = getPath(user, f.path) !== void 0;
|
|
278
|
+
if (f.type === "checkbox") {
|
|
279
|
+
nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
|
|
280
|
+
h("span", { className: "__tm_row" },
|
|
281
|
+
h("input", { className: "__tm_check", type: "checkbox", checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
|
|
282
|
+
h("span", { className: "__tm_label" }, t(f.label)),
|
|
283
|
+
overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
|
|
284
|
+
)
|
|
285
|
+
));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
|
|
289
|
+
h("span", { className: "__tm_label" },
|
|
290
|
+
t(f.label),
|
|
291
|
+
overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
|
|
292
|
+
),
|
|
293
|
+
h("input", {
|
|
294
|
+
className: "__tm_input",
|
|
295
|
+
type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
|
|
296
|
+
value: fieldDraft(f),
|
|
297
|
+
placeholder: f.type === "password" ? (overridden ? "••••••••" : t("secretHint")) : "",
|
|
298
|
+
onChange: function (e) { setField(f, e.target.value); }
|
|
299
|
+
}),
|
|
300
|
+
f.type === "password" ? h("span", { className: "__tm_hint" }, t("secretHint")) : null
|
|
301
|
+
));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return h("div", { className: "__tm_root" },
|
|
305
|
+
h("p", { className: "__tm_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
306
|
+
nodes,
|
|
307
|
+
h("div", { className: "__tm_actions" },
|
|
308
|
+
h("button", { type: "button", className: "__tm_btn __tm_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
|
|
309
|
+
h("button", { type: "button", className: "__tm_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
|
|
310
|
+
notice ? h("span", { className: "__tm_status" }, notice) : null,
|
|
311
|
+
busy ? h("span", { className: "__tm_status" }, t("saving")) : null,
|
|
312
|
+
error ? h("span", { className: "__tm_error" }, error) : null
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function valueToDraft(value) {
|
|
318
|
+
var out = {};
|
|
319
|
+
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
320
|
+
var f = FIELDS[i];
|
|
321
|
+
out[f.key] = f.type === "checkbox" ? Boolean(getPath(value, f.path)) : String(getPath(value, f.path) ?? "");
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── plugin ────────────────────────────────────────────────────────────
|
|
327
|
+
function apply(ctx) {
|
|
328
|
+
var t = ctx.locale.bind(NS);
|
|
329
|
+
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tdai-memory: dictionaries");
|
|
330
|
+
var scope = ctx.settingsScope.bind({ namespace: "tdai-memory" });
|
|
331
|
+
var api = ctx.connection.api;
|
|
332
|
+
ctx.slots.inject("settings.section", function () {
|
|
333
|
+
return ctx.slots.register({
|
|
334
|
+
name: "settings.section",
|
|
335
|
+
id: "tdai-memory",
|
|
336
|
+
order: 26,
|
|
337
|
+
label: function () { return t("nav"); },
|
|
338
|
+
locale: NS
|
|
339
|
+
}, function (props) {
|
|
340
|
+
return h(MemorySection, Object.assign({}, props, { scope: scope, api: api }));
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
exports.apply = apply;
|
|
346
|
+
exports.inject = inject;
|
|
347
|
+
return module.exports;
|
|
348
|
+
}
|
|
349
|
+
});
|