dsh-tdai-memory 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -31
- package/README.zh.md +68 -0
- package/client.js +303 -0
- package/index.js +49 -16
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -1,56 +1,96 @@
|
|
|
1
1
|
# dsh-tdai-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](README.zh.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**GitHub**: [Scorp1o117/dsh-tdai-memory](https://github.com/Scorp1o117/dsh-tdai-memory) · **npm**: [dsh-tdai-memory](https://www.npmjs.com/package/dsh-tdai-memory)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **L2 场景 / L3 画像**:场景块与用户画像生成(管线自动调度)
|
|
10
|
-
- **自动召回注入**:每次组装提示词时,按当前用户消息检索相关记忆 + 画像,作为动态上下文注入(模型"凭空想起")
|
|
11
|
-
- **工具**:`tdai_memory_search`(L1 结构化检索)、`tdai_conversation_search`(L0 原文检索)
|
|
7
|
+
A port of **TencentDB Agent Memory** (Tencent Cloud's open-source four-layer
|
|
8
|
+
memory system, originally an OpenClaw plugin) into DeepSeek Harness.
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
## Features
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
- **L0 conversation capture**: every turn (turn end, request boundary) is
|
|
13
|
+
written to raw conversation storage (JSONL + SQLite + FTS + vectors)
|
|
14
|
+
- **L1 structured memory**: a background pipeline uses an LLM to extract
|
|
15
|
+
facts / preferences / events (persona / episodic / instruction) from
|
|
16
|
+
conversations, stored in `records/` + SQLite + FTS + vectors
|
|
17
|
+
- **L2 scenes / L3 persona**: scene blocks and user profile generation
|
|
18
|
+
(pipeline-scheduled)
|
|
19
|
+
- **Automatic recall injection**: on every prompt assembly, relevant memories
|
|
20
|
+
and the user profile are retrieved by the current user message and injected
|
|
21
|
+
as dynamic context (the model "just remembers")
|
|
22
|
+
- **Tools**: `tdai_memory_search` (L1 structured search),
|
|
23
|
+
`tdai_conversation_search` (L0 raw-text search)
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
The data directory reuses the existing `~/.memory-tencentdb/memory-tdai`, so
|
|
26
|
+
**previously accumulated memories carry over seamlessly**.
|
|
27
|
+
|
|
28
|
+
## Architecture (porting approach)
|
|
29
|
+
|
|
30
|
+
| Layer | Content |
|
|
18
31
|
|---|---|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
| dsh
|
|
22
|
-
|
|
|
32
|
+
| Core | The host-neutral core of `tdai-memory-openclaw-plugin` (`src/core`, `src/utils`), tsc-compiled to ESM (`dist-dsh/`), zero changes |
|
|
33
|
+
| Host adapter | `StandaloneHostAdapter` (official standalone mode, direct OpenAI-compatible calls) |
|
|
34
|
+
| dsh shell | `index.js`: config mapping, `session/event` + `session/flush` capture, `system-prompt/assemble` recall injection on `agent.ctx`, tool registration, lifecycle |
|
|
35
|
+
| Fallback | `recall-inject.js`: preset-row recall injection (used when mounted inside an agent preset) |
|
|
36
|
+
|
|
37
|
+
Hard-won wiring details:
|
|
38
|
+
|
|
39
|
+
- **Capture**: `session/flush` listener (await semantics; must complete before
|
|
40
|
+
headless exits); `turn/start` timestamps as the L0 cursor floor; turn-id dedup
|
|
41
|
+
- **Headless one-shot runs**: wait for `core.handleSessionEnd()` inside flush
|
|
42
|
+
(L1 extraction finishes before exit; otherwise the 5s shutdown timeout kills it)
|
|
43
|
+
- **Recall injection**: must be registered on **`agent.ctx`** (assembly runs in
|
|
44
|
+
the agent scope; root listeners never see it); attach one tick after
|
|
45
|
+
`session/created` by resolving the agent from the `agents` service
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
## Configuration (profile patch + settings)
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
49
|
+
Configuration is **settings-namespace driven**: the profile patch is the base
|
|
50
|
+
layer, and the `tdai-memory:` section of `$DSH_HOME/settings.yaml` overrides it
|
|
51
|
+
(LLM/embedding keys live in settings.yaml). The **Web UI Settings → 记忆**
|
|
52
|
+
section edits every field (v0.2.0, write-only keys); TdaiCore is built at
|
|
53
|
+
startup, so changes apply **after a restart**.
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
```yaml
|
|
56
|
+
# $DSH_HOME/settings.yaml
|
|
57
|
+
tdai-memory:
|
|
58
|
+
llm:
|
|
59
|
+
apiKey: 'sk-...'
|
|
60
|
+
embedding:
|
|
61
|
+
apiKey: 'local-no-key'
|
|
62
|
+
```
|
|
31
63
|
|
|
32
64
|
```yaml
|
|
65
|
+
# profile patch (base layer)
|
|
33
66
|
- id: tdai-memory
|
|
34
|
-
name: '
|
|
67
|
+
name: 'dsh-tdai-memory'
|
|
35
68
|
config:
|
|
36
69
|
extraction:
|
|
37
70
|
enabled: true
|
|
38
|
-
enableDedup: false # dedup
|
|
39
|
-
llm: # L1/L2/L3
|
|
71
|
+
enableDedup: false # dedup LLM output parsing is flaky; off by default
|
|
72
|
+
llm: # L1/L2/L3 extraction model (OpenAI-compatible)
|
|
40
73
|
baseUrl: 'https://opencode.ai/zen/go/v1'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
embedding: # 向量(OpenAI 兼容 /v1/embeddings)
|
|
74
|
+
model: 'mimo-v2.5' # deepseek-v4-flash produces invalid extraction JSON
|
|
75
|
+
embedding: # vectors (OpenAI-compatible /v1/embeddings)
|
|
44
76
|
baseUrl: 'http://127.0.0.1:8088/v1'
|
|
45
|
-
apiKey: 'local-no-key' # parseConfig 强制 apiKey 非空,本地端点可占位
|
|
46
77
|
model: 'Qwen3-Embedding-0.6B'
|
|
47
78
|
dimensions: 1024
|
|
48
79
|
sendDimensions: false
|
|
49
80
|
```
|
|
50
81
|
|
|
51
|
-
##
|
|
82
|
+
## Known trade-offs
|
|
83
|
+
|
|
84
|
+
- **Extraction model**: `mimo-v2.5` extracts correctly but takes 20-30s per
|
|
85
|
+
call (background execution, does not block the conversation);
|
|
86
|
+
`deepseek-v4-flash` is fast but its JSON output is non-compliant (extracts 0)
|
|
87
|
+
- **dedup**: LLM conflict-detection output parsing is unstable (once caused
|
|
88
|
+
stored=0); off by default; enable only with a more reliable model
|
|
89
|
+
- **L1 memory vectors**: written with storage (8088 embedding is fast); L0
|
|
90
|
+
vectors run as a background task, drained by `destroy()` on headless exit
|
|
91
|
+
- **Upgrades**: after pulling new upstream code, rerun
|
|
92
|
+
`npx tsc -p dsh-tsconfig.json` in the tdai project dir (output in `dist-dsh/`)
|
|
93
|
+
|
|
94
|
+
## License
|
|
52
95
|
|
|
53
|
-
|
|
54
|
-
- **dedup**:LLM 冲突检测输出解析不稳(曾导致 stored=0),默认关闭;开启需换更稳的模型
|
|
55
|
-
- **L1 记忆向量**:随存储写入(8088 embedding 快);L0 向量走后台任务,headless 退出时由 `destroy()` drain
|
|
56
|
-
- **升级**:上游拉新代码后,在 tdai 项目目录重跑 `npx tsc -p dsh-tsconfig.json`(产物在 `dist-dsh/`)
|
|
96
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# dsh-tdai-memory
|
|
2
|
+
|
|
3
|
+
**GitHub**: [Scorp1o117/dsh-tdai-memory](https://github.com/Scorp1o117/dsh-tdai-memory) · **npm**: [dsh-tdai-memory](https://www.npmjs.com/package/dsh-tdai-memory) · [English](README.md)
|
|
4
|
+
|
|
5
|
+
把 **TencentDB Agent Memory**(腾讯云开源的四层记忆系统,原为 OpenClaw 插件)移植进 DeepSeek Harness。
|
|
6
|
+
|
|
7
|
+
## 能力
|
|
8
|
+
|
|
9
|
+
- **L0 对话捕获**:每轮对话(turn 结束、请求边界)自动写入原始对话(JSONL + SQLite + FTS + 向量)
|
|
10
|
+
- **L1 结构化记忆**:后台管线用 LLM 从对话中提取事实/偏好/事件(persona / episodic / instruction),写入 `records/` + SQLite + FTS + 向量
|
|
11
|
+
- **L2 场景 / L3 画像**:场景块与用户画像生成(管线自动调度)
|
|
12
|
+
- **自动召回注入**:每次组装提示词时,按当前用户消息检索相关记忆 + 画像,作为动态上下文注入(模型"凭空想起")
|
|
13
|
+
- **工具**:`tdai_memory_search`(L1 结构化检索)、`tdai_conversation_search`(L0 原文检索)
|
|
14
|
+
|
|
15
|
+
数据目录复用现有 `~/.memory-tencentdb/memory-tdai`,**旧记忆无缝继承**。
|
|
16
|
+
|
|
17
|
+
## 架构(移植方式)
|
|
18
|
+
|
|
19
|
+
| 层 | 内容 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 核心 | `tdai-memory-openclaw-plugin` 的 host-neutral 核心(`src/core`、`src/utils`),tsc 编译为 ESM(`dist-dsh/`),零改动 |
|
|
22
|
+
| 宿主适配 | `StandaloneHostAdapter`(官方 standalone 模式,OpenAI 兼容直调) |
|
|
23
|
+
| dsh 壳 | `index.js`:配置映射、`session/event`+`session/flush` 捕获、`agent.ctx` 上的 `system-prompt/assemble` waterfall 召回注入、工具注册、生命周期 |
|
|
24
|
+
| 备用 | `recall-inject.js`:preset 行形态的召回注入(挂在 agent preset 内时用) |
|
|
25
|
+
|
|
26
|
+
关键接线(都是踩坑换来的):
|
|
27
|
+
|
|
28
|
+
- **捕获**:`session/flush` 监听器(await 语义,headless 退出前必完成);`turn/start` 时间戳做 L0 cursor 下限;turn id 去重
|
|
29
|
+
- **headless 一次性任务**:flush 内再等 `core.handleSessionEnd()`(L1 提取跑完才退出;否则 5s 关闭超时会杀掉提取)
|
|
30
|
+
- **召回注入**:必须注册在 **`agent.ctx`** 上(组装在 agent 作用域进行,root 监听器收不到);`session/created` 后延迟一 tick 从 `agents` 服务拿 agent
|
|
31
|
+
|
|
32
|
+
## 配置(profile patch + settings)
|
|
33
|
+
|
|
34
|
+
配置以 **settings 命名空间**驱动:profile patch 作为 base 层,`$DSH_HOME/settings.yaml` 的 `tdai-memory:` 节覆盖(LLM/embedding 密钥已迁到 settings.yaml)。Web UI 设置 → 记忆 可编辑全部字段(v0.2.0,含写-only 密钥);TdaiCore 启动时构建,改动**重启后生效**。
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
# $DSH_HOME/settings.yaml
|
|
38
|
+
tdai-memory:
|
|
39
|
+
llm:
|
|
40
|
+
apiKey: 'sk-...'
|
|
41
|
+
embedding:
|
|
42
|
+
apiKey: 'local-no-key'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
# profile patch(base 层)
|
|
47
|
+
- id: tdai-memory
|
|
48
|
+
name: 'dsh-tdai-memory'
|
|
49
|
+
config:
|
|
50
|
+
extraction:
|
|
51
|
+
enabled: true
|
|
52
|
+
enableDedup: false # dedup 的 LLM 输出解析不稳,先关
|
|
53
|
+
llm: # L1/L2/L3 提取模型(OpenAI 兼容)
|
|
54
|
+
baseUrl: 'https://opencode.ai/zen/go/v1'
|
|
55
|
+
model: 'mimo-v2.5' # deepseek-v4-flash 对提取 JSON 输出不合格
|
|
56
|
+
embedding: # 向量(OpenAI 兼容 /v1/embeddings)
|
|
57
|
+
baseUrl: 'http://127.0.0.1:8088/v1'
|
|
58
|
+
model: 'Qwen3-Embedding-0.6B'
|
|
59
|
+
dimensions: 1024
|
|
60
|
+
sendDimensions: false
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 已知取舍
|
|
64
|
+
|
|
65
|
+
- **提取模型**:`mimo-v2.5` 提取正确但单次 20-30s(后台执行,不阻塞对话);`deepseek-v4-flash` 快但 JSON 输出不合规(提取 0 条)
|
|
66
|
+
- **dedup**:LLM 冲突检测输出解析不稳(曾导致 stored=0),默认关闭;开启需换更稳的模型
|
|
67
|
+
- **L1 记忆向量**:随存储写入(8088 embedding 快);L0 向量走后台任务,headless 退出时由 `destroy()` drain
|
|
68
|
+
- **升级**:上游拉新代码后,在 tdai 项目目录重跑 `npx tsc -p dsh-tsconfig.json`(产物在 `dist-dsh/`)
|
package/client.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
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);background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-on-accent)}" +
|
|
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
|
+
secretHint: "留空保持当前密钥。",
|
|
62
|
+
save: "保存",
|
|
63
|
+
reset: "恢复默认",
|
|
64
|
+
saved: "已保存(重启后生效)",
|
|
65
|
+
saving: "保存中…",
|
|
66
|
+
error: "保存失败",
|
|
67
|
+
unavailable: "设置命名空间不可用(服务端未注册 tdai-memory 命名空间?)",
|
|
68
|
+
overridden: "已覆盖",
|
|
69
|
+
loading: "加载中…"
|
|
70
|
+
};
|
|
71
|
+
var en = {
|
|
72
|
+
nav: "Memory",
|
|
73
|
+
intro: "TDAI memory config: L0 capture → L1 extraction → recall injection. Keys are write-only. TdaiCore is built at startup; changes apply after a restart.",
|
|
74
|
+
groupData: "Data",
|
|
75
|
+
groupLlm: "Extraction LLM (L1/L2/L3)",
|
|
76
|
+
groupEmbedding: "Embeddings",
|
|
77
|
+
groupCapture: "Capture",
|
|
78
|
+
groupExtraction: "Extraction",
|
|
79
|
+
groupRecall: "Recall",
|
|
80
|
+
groupTools: "Tools",
|
|
81
|
+
secretHint: "Leave blank to keep the current key.",
|
|
82
|
+
save: "Save",
|
|
83
|
+
reset: "Reset",
|
|
84
|
+
saved: "Saved (applies after restart)",
|
|
85
|
+
saving: "Saving…",
|
|
86
|
+
error: "Save failed",
|
|
87
|
+
unavailable: "Settings namespace unavailable (tdai-memory namespace not registered server-side?)",
|
|
88
|
+
overridden: "overridden",
|
|
89
|
+
loading: "Loading…"
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ── field spec: dotted path + type + group ─────────────────────────────
|
|
93
|
+
var FIELDS = [
|
|
94
|
+
{ path: ["dataDir"], label: "数据目录(空 = ~/.memory-tencentdb/memory-tdai)", type: "text", group: "groupData" },
|
|
95
|
+
{ path: ["llm", "baseUrl"], label: "Base URL", type: "text", group: "groupLlm" },
|
|
96
|
+
{ path: ["llm", "apiKey"], label: "API Key", type: "password", secret: true, group: "groupLlm" },
|
|
97
|
+
{ path: ["llm", "model"], label: "模型", type: "text", group: "groupLlm" },
|
|
98
|
+
{ path: ["llm", "maxTokens"], label: "最大输出 Tokens", type: "number", group: "groupLlm" },
|
|
99
|
+
{ path: ["llm", "timeoutMs"], label: "超时(毫秒)", type: "number", group: "groupLlm" },
|
|
100
|
+
{ path: ["embedding", "baseUrl"], label: "Base URL", type: "text", group: "groupEmbedding" },
|
|
101
|
+
{ path: ["embedding", "apiKey"], label: "API Key", type: "password", secret: true, group: "groupEmbedding" },
|
|
102
|
+
{ path: ["embedding", "model"], label: "模型", type: "text", group: "groupEmbedding" },
|
|
103
|
+
{ path: ["embedding", "dimensions"], label: "向量维度", type: "number", group: "groupEmbedding" },
|
|
104
|
+
{ path: ["embedding", "sendDimensions"], label: "请求带 dimensions 参数", type: "checkbox", group: "groupEmbedding" },
|
|
105
|
+
{ path: ["captureEnabled"], label: "捕获对话(L0)", type: "checkbox", group: "groupCapture" },
|
|
106
|
+
{ path: ["extraction", "enabled"], label: "结构化提取(L1)", type: "checkbox", group: "groupExtraction" },
|
|
107
|
+
{ path: ["extraction", "enableDedup"], label: "冲突检测(额外 LLM 调用)", type: "checkbox", group: "groupExtraction" },
|
|
108
|
+
{ path: ["recall", "enabled"], label: "召回注入", type: "checkbox", group: "groupRecall" },
|
|
109
|
+
{ path: ["recall", "maxResults"], label: "最大召回条数", type: "number", group: "groupRecall" },
|
|
110
|
+
{ path: ["recall", "scoreThreshold"], label: "相似度阈值", type: "number", group: "groupRecall" },
|
|
111
|
+
{ path: ["recall", "timeoutMs"], label: "召回超时(毫秒)", type: "number", group: "groupRecall" },
|
|
112
|
+
{ path: ["toolsEnabled"], label: "注册搜索工具(tdai_memory_search / tdai_conversation_search)", type: "checkbox", group: "groupTools" }
|
|
113
|
+
];
|
|
114
|
+
FIELDS.forEach(function (f) { f.key = f.path.join("."); });
|
|
115
|
+
|
|
116
|
+
function getPath(obj, path) {
|
|
117
|
+
var cur = obj;
|
|
118
|
+
for (var i = 0; i < path.length; i += 1) {
|
|
119
|
+
if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
|
|
120
|
+
cur = cur[path[i]];
|
|
121
|
+
}
|
|
122
|
+
return cur;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function MemorySection(props) {
|
|
126
|
+
var t = props.t;
|
|
127
|
+
var scope = props.scope;
|
|
128
|
+
var api = props.api;
|
|
129
|
+
var snapshot = react.useSyncExternalStore(
|
|
130
|
+
function (cb) { return scope.subscribe(cb); },
|
|
131
|
+
function () { return scope.getSnapshot(); }
|
|
132
|
+
);
|
|
133
|
+
var ready = snapshot.status === "ready" && snapshot.value !== void 0;
|
|
134
|
+
var [draft, setDraft] = react.useState({});
|
|
135
|
+
var [busy, setBusy] = react.useState(false);
|
|
136
|
+
var [notice, setNotice] = react.useState(null);
|
|
137
|
+
var [error, setError] = react.useState(null);
|
|
138
|
+
|
|
139
|
+
react.useEffect(function () {
|
|
140
|
+
scope.load();
|
|
141
|
+
return function () { scope.dispose(); };
|
|
142
|
+
}, [scope]);
|
|
143
|
+
react.useEffect(function () {
|
|
144
|
+
if (ready) setDraft(Object.assign({}, valueToDraft(snapshot.value)));
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
}, [ready, snapshot.value]);
|
|
147
|
+
|
|
148
|
+
if (snapshot.status === "unavailable") {
|
|
149
|
+
return h("p", { className: "__tm_unavailable" }, t("unavailable"));
|
|
150
|
+
}
|
|
151
|
+
if (!ready) return h("p", { className: "__tm_status" }, t("loading"));
|
|
152
|
+
|
|
153
|
+
var value = snapshot.value;
|
|
154
|
+
var user = snapshot.user || {};
|
|
155
|
+
|
|
156
|
+
function fieldDraft(f) {
|
|
157
|
+
if (f.type === "checkbox") return draft[f.key] !== void 0 ? draft[f.key] : Boolean(getPath(value, f.path));
|
|
158
|
+
return draft[f.key] !== void 0 ? draft[f.key] : String(getPath(value, f.path) ?? "");
|
|
159
|
+
}
|
|
160
|
+
function setField(f, v) {
|
|
161
|
+
setDraft(function (prev) { var next = Object.assign({}, prev); next[f.key] = v; return next; });
|
|
162
|
+
setNotice(null);
|
|
163
|
+
setError(null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function onSave() {
|
|
167
|
+
setBusy(true); setNotice(null); setError(null);
|
|
168
|
+
var ops = [];
|
|
169
|
+
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
170
|
+
var f = FIELDS[i];
|
|
171
|
+
var d = fieldDraft(f);
|
|
172
|
+
var current = getPath(value, f.path);
|
|
173
|
+
if (f.type === "password") {
|
|
174
|
+
if (!d) continue; // blank keeps the current key
|
|
175
|
+
if (d === String(current ?? "")) continue;
|
|
176
|
+
ops.push({ op: "set", path: f.path, value: d });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (f.type === "checkbox") {
|
|
180
|
+
if (Boolean(d) === Boolean(current)) continue;
|
|
181
|
+
ops.push(Boolean(d) ? { op: "set", path: f.path, value: true } : { op: "unset", path: f.path });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (String(d) === String(current ?? "")) continue;
|
|
185
|
+
if (String(d).trim() === "" && getPath(user, f.path) === void 0) continue;
|
|
186
|
+
ops.push(String(d).trim() === "" ? { op: "unset", path: f.path } : { op: "set", path: f.path, value: f.type === "number" ? Number(d) : d });
|
|
187
|
+
}
|
|
188
|
+
if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
|
|
189
|
+
api.settings.mutate({
|
|
190
|
+
ns: "tdai-memory",
|
|
191
|
+
ops: ops,
|
|
192
|
+
...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
|
|
193
|
+
}).then(function (response) {
|
|
194
|
+
setBusy(false);
|
|
195
|
+
if (!response.result.ok) {
|
|
196
|
+
var detail = response.result.error || {};
|
|
197
|
+
setError(t("error") + ": " + String(detail.message || detail.code || "unknown"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
setNotice(t("saved"));
|
|
201
|
+
scope.load();
|
|
202
|
+
}).catch(function (e) {
|
|
203
|
+
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function onReset() {
|
|
208
|
+
setBusy(true); setNotice(null); setError(null);
|
|
209
|
+
api.settings.mutate({
|
|
210
|
+
ns: "tdai-memory",
|
|
211
|
+
ops: FIELDS.map(function (f) { return { op: "unset", path: f.path }; }),
|
|
212
|
+
...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
|
|
213
|
+
}).then(function (response) {
|
|
214
|
+
setBusy(false);
|
|
215
|
+
if (!response.result.ok) { setError(t("error")); return; }
|
|
216
|
+
setNotice(t("saved"));
|
|
217
|
+
scope.load();
|
|
218
|
+
}).catch(function (e) {
|
|
219
|
+
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
var nodes = [];
|
|
224
|
+
var lastGroup = null;
|
|
225
|
+
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
226
|
+
var f = FIELDS[i];
|
|
227
|
+
if (f.group !== lastGroup) {
|
|
228
|
+
lastGroup = f.group;
|
|
229
|
+
nodes.push(h("div", { key: "g" + f.group, className: "__tm_group" }, t(f.group)));
|
|
230
|
+
}
|
|
231
|
+
var overridden = getPath(user, f.path) !== void 0;
|
|
232
|
+
if (f.type === "checkbox") {
|
|
233
|
+
nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
|
|
234
|
+
h("span", { className: "__tm_row" },
|
|
235
|
+
h("input", { className: "__tm_check", type: "checkbox", checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
|
|
236
|
+
h("span", { className: "__tm_label" }, f.label),
|
|
237
|
+
overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
|
|
238
|
+
)
|
|
239
|
+
));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
|
|
243
|
+
h("span", { className: "__tm_label" },
|
|
244
|
+
f.label,
|
|
245
|
+
overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
|
|
246
|
+
),
|
|
247
|
+
h("input", {
|
|
248
|
+
className: "__tm_input",
|
|
249
|
+
type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
|
|
250
|
+
value: fieldDraft(f),
|
|
251
|
+
placeholder: f.type === "password" ? (overridden ? "••••••••" : t("secretHint")) : "",
|
|
252
|
+
onChange: function (e) { setField(f, e.target.value); }
|
|
253
|
+
}),
|
|
254
|
+
f.type === "password" ? h("span", { className: "__tm_hint" }, t("secretHint")) : null
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return h("div", { className: "__tm_root" },
|
|
259
|
+
h("p", { className: "__tm_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
260
|
+
nodes,
|
|
261
|
+
h("div", { className: "__tm_actions" },
|
|
262
|
+
h("button", { type: "button", className: "__tm_btn __tm_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
|
|
263
|
+
h("button", { type: "button", className: "__tm_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
|
|
264
|
+
notice ? h("span", { className: "__tm_status" }, notice) : null,
|
|
265
|
+
busy ? h("span", { className: "__tm_status" }, t("saving")) : null,
|
|
266
|
+
error ? h("span", { className: "__tm_error" }, error) : null
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function valueToDraft(value) {
|
|
272
|
+
var out = {};
|
|
273
|
+
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
274
|
+
var f = FIELDS[i];
|
|
275
|
+
out[f.key] = f.type === "checkbox" ? Boolean(getPath(value, f.path)) : String(getPath(value, f.path) ?? "");
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── plugin ────────────────────────────────────────────────────────────
|
|
281
|
+
function apply(ctx) {
|
|
282
|
+
var t = ctx.locale.bind(NS);
|
|
283
|
+
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tdai-memory: dictionaries");
|
|
284
|
+
var scope = ctx.settingsScope.bind({ namespace: "tdai-memory" });
|
|
285
|
+
var api = ctx.connection.api;
|
|
286
|
+
ctx.slots.inject("settings.section", function () {
|
|
287
|
+
return ctx.slots.register({
|
|
288
|
+
name: "settings.section",
|
|
289
|
+
id: "tdai-memory",
|
|
290
|
+
order: 26,
|
|
291
|
+
label: function () { return t("nav"); },
|
|
292
|
+
locale: NS
|
|
293
|
+
}, function (props) {
|
|
294
|
+
return h(MemorySection, Object.assign({}, props, { scope: scope, api: api }));
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
exports.apply = apply;
|
|
300
|
+
exports.inject = inject;
|
|
301
|
+
return module.exports;
|
|
302
|
+
}
|
|
303
|
+
});
|
package/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import os from "node:os";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import z from "@deepseek-ai/schemastery";
|
|
23
23
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
24
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
24
25
|
import { TdaiCore } from "./vendor/tdai/core/tdai-core.js";
|
|
25
26
|
import { parseConfig } from "./vendor/tdai/config.js";
|
|
26
27
|
import { StandaloneHostAdapter } from "./vendor/tdai/adapters/standalone/host-adapter.js";
|
|
@@ -29,6 +30,8 @@ import { StandaloneHostAdapter } from "./vendor/tdai/adapters/standalone/host-ad
|
|
|
29
30
|
const name = "tdai-memory";
|
|
30
31
|
/** Services this row injects. */
|
|
31
32
|
const inject = ["sessions", "systemPrompt", "tools"];
|
|
33
|
+
/** Settings namespace owned by this plugin (Web UI settings section). */
|
|
34
|
+
const NS = settingsNamespace("tdai-memory");
|
|
32
35
|
|
|
33
36
|
/** Runtime schema for the tdai-memory row. */
|
|
34
37
|
const Config = z.object({
|
|
@@ -37,7 +40,7 @@ const Config = z.object({
|
|
|
37
40
|
/** LLM used for L1/L2/L3 extraction (OpenAI-compatible). Falls back to TDAI_LLM_* env. */
|
|
38
41
|
llm: z.object({
|
|
39
42
|
baseUrl: z.string().default(""),
|
|
40
|
-
apiKey: z.string().default(""),
|
|
43
|
+
apiKey: z.string().default("").role("secret"),
|
|
41
44
|
model: z.string().default(""),
|
|
42
45
|
maxTokens: z.number().default(4096),
|
|
43
46
|
timeoutMs: z.number().default(120000),
|
|
@@ -45,7 +48,7 @@ const Config = z.object({
|
|
|
45
48
|
/** Embedding endpoint (OpenAI-compatible /v1/embeddings). */
|
|
46
49
|
embedding: z.object({
|
|
47
50
|
baseUrl: z.string().default("http://127.0.0.1:8088/v1"),
|
|
48
|
-
apiKey: z.string().default(""),
|
|
51
|
+
apiKey: z.string().default("").role("secret"),
|
|
49
52
|
model: z.string().default("Qwen3-Embedding-0.6B"),
|
|
50
53
|
dimensions: z.number().default(1024),
|
|
51
54
|
sendDimensions: z.boolean().default(false),
|
|
@@ -81,31 +84,41 @@ function extractText(content) {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function apply(ctx, config) {
|
|
84
|
-
|
|
87
|
+
// ── settings-backed configuration ─────────────────────────────────────────
|
|
88
|
+
// TdaiCore is built once from the RESOLVED settings value (composition
|
|
89
|
+
// entry as base, `tdai-memory` settings section / settings.yaml as user
|
|
90
|
+
// layer), so keys stored in settings.yaml are honored. Changes apply after
|
|
91
|
+
// a restart; the Web UI notes this. If no settings service ever mounts,
|
|
92
|
+
// the composition entry builds the core unchanged.
|
|
93
|
+
let started = false;
|
|
94
|
+
const build = (cfg) => {
|
|
95
|
+
if (started) return;
|
|
96
|
+
started = true;
|
|
97
|
+
const dataDir = cfg.dataDir || join(os.homedir(), ".memory-tencentdb", "memory-tdai");
|
|
85
98
|
|
|
86
99
|
const tdaiConfig = parseConfig({
|
|
87
100
|
timezone: "system",
|
|
88
101
|
storeBackend: "sqlite",
|
|
89
|
-
capture: { enabled:
|
|
90
|
-
extraction: { enabled:
|
|
91
|
-
recall: { ...
|
|
102
|
+
capture: { enabled: cfg.captureEnabled },
|
|
103
|
+
extraction: { enabled: cfg.extraction.enabled, enableDedup: cfg.extraction.enableDedup },
|
|
104
|
+
recall: { ...cfg.recall },
|
|
92
105
|
embedding: {
|
|
93
106
|
enabled: true,
|
|
94
107
|
provider: "openai",
|
|
95
|
-
baseUrl:
|
|
96
|
-
apiKey:
|
|
97
|
-
model:
|
|
98
|
-
dimensions:
|
|
99
|
-
sendDimensions:
|
|
108
|
+
baseUrl: cfg.embedding.baseUrl,
|
|
109
|
+
apiKey: cfg.embedding.apiKey,
|
|
110
|
+
model: cfg.embedding.model,
|
|
111
|
+
dimensions: cfg.embedding.dimensions,
|
|
112
|
+
sendDimensions: cfg.embedding.sendDimensions,
|
|
100
113
|
timeoutMs: 10000,
|
|
101
114
|
},
|
|
102
115
|
llm: {
|
|
103
116
|
enabled: true,
|
|
104
|
-
baseUrl:
|
|
105
|
-
apiKey:
|
|
106
|
-
model:
|
|
107
|
-
maxTokens:
|
|
108
|
-
timeoutMs:
|
|
117
|
+
baseUrl: cfg.llm.baseUrl || process.env.TDAI_LLM_BASE_URL || "",
|
|
118
|
+
apiKey: cfg.llm.apiKey || process.env.TDAI_LLM_API_KEY || "",
|
|
119
|
+
model: cfg.llm.model || process.env.TDAI_LLM_MODEL || "deepseek-v4-flash",
|
|
120
|
+
maxTokens: cfg.llm.maxTokens,
|
|
121
|
+
timeoutMs: cfg.llm.timeoutMs,
|
|
109
122
|
},
|
|
110
123
|
});
|
|
111
124
|
|
|
@@ -304,6 +317,20 @@ function apply(ctx, config) {
|
|
|
304
317
|
}));
|
|
305
318
|
}
|
|
306
319
|
|
|
320
|
+
// ── settings-backed configuration ─────────────────────────────────────────
|
|
321
|
+
// The composition entry stays the base layer; a registered `tdai-memory`
|
|
322
|
+
// settings section overlays it (settings.yaml). TdaiCore is built once at
|
|
323
|
+
// startup, so changes apply after a restart — the namespace registration is
|
|
324
|
+
// what lets the Web UI surface and store them.
|
|
325
|
+
installSettingsSection(ctx, NS, Config, config, {
|
|
326
|
+
setSource: (source) => {
|
|
327
|
+
build(source);
|
|
328
|
+
},
|
|
329
|
+
onChange: () => {
|
|
330
|
+
if (started) ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
307
334
|
// ── lifecycle ────────────────────────────────────────────────────────────
|
|
308
335
|
ctx.effect(async () => {
|
|
309
336
|
await core.initialize();
|
|
@@ -323,6 +350,12 @@ function apply(ctx, config) {
|
|
|
323
350
|
});
|
|
324
351
|
};
|
|
325
352
|
}, "tdai-memory.initialize()");
|
|
353
|
+
}; // end build(cfg)
|
|
354
|
+
|
|
355
|
+
// Fallback when no settings service ever mounts: build from the entry config.
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
if (!started) build(config);
|
|
358
|
+
}, 0);
|
|
326
359
|
}
|
|
327
360
|
|
|
328
361
|
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"name": "dsh-tdai-memory",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "TencentDB Agent Memory
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "DeepSeek Harness 的 TencentDB Agent Memory 移植:L0 对话捕获 → L1 结构化记忆提取 → L2 场景/L3 画像,自动召回注入 + 记忆/对话搜索工具;复用现有 ~/.memory-tencentdb/memory-tdai 数据;附 Web UI 设置栏。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./index.js"
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
9
11
|
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"client": {
|
|
14
|
+
"platform": "web"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/Scorp1o117/dsh-tdai-memory",
|
|
10
18
|
"files": [
|
|
11
19
|
"index.js",
|
|
12
20
|
"recall-inject.js",
|
|
21
|
+
"client.js",
|
|
13
22
|
"vendor",
|
|
14
23
|
"README.md",
|
|
15
24
|
"README.zh.md",
|
|
@@ -40,6 +49,7 @@
|
|
|
40
49
|
},
|
|
41
50
|
"dependencies": {
|
|
42
51
|
"@ai-sdk/openai": "^3.0.53",
|
|
52
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
43
53
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
44
54
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
45
55
|
"@node-rs/jieba": "^2.0.1",
|
|
@@ -50,4 +60,4 @@
|
|
|
50
60
|
"undici": "^8.1.0"
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
|
-
|
|
63
|
+
|