memory-eternal 0.10.0 → 0.10.3
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/.claude-plugin/plugin.json +2 -16
- package/.codex-plugin/plugin.json +2 -16
- package/.cursor-plugin/plugin.json +2 -16
- package/.mcp.json +9 -9
- package/LICENSE +21 -21
- package/README.en.md +19 -5
- package/README.md +23 -5
- package/bin/dsh-memory.mjs +343 -188
- package/examples/mcp-memory/memory-eternal.cordis.yml +59 -59
- package/hooks/capture.mjs +77 -77
- package/hooks/claude-session.js +56 -56
- package/hooks/hooks.claude.json +12 -12
- package/hooks/hooks.codex.json +15 -15
- package/hooks/hooks.cursor.json +11 -11
- package/index.js +177 -30
- package/lib/api.js +48 -14
- package/lib/capture-log.js +65 -59
- package/lib/capture.js +112 -7
- package/lib/client.js +13 -13
- package/lib/config-sync.js +161 -90
- package/lib/db.js +359 -359
- package/lib/feedback.js +214 -205
- package/lib/llm-openai.js +84 -84
- package/lib/mcp.js +207 -166
- package/lib/node-bin.js +99 -99
- package/lib/setup.js +470 -470
- package/lib/sse.js +51 -51
- package/lib/stall.js +30 -30
- package/lib/sweep.js +56 -56
- package/lib/vault-resolve.js +84 -84
- package/lib/vault.js +23 -6
- package/lib/watchdog.js +527 -392
- package/lib/web-assets.js +41 -0
- package/lib/web.js +316 -176
- package/package.json +12 -8
- package/scripts/release.mjs +204 -0
- package/skills/memory-recall/SKILL.md +14 -14
- package/web/app.js +12 -12
- package/web/index.html +46 -46
package/lib/feedback.js
CHANGED
|
@@ -1,205 +1,214 @@
|
|
|
1
|
-
// 记忆核心 · 「反馈异常」支撑层(纯函数,便于单测)
|
|
2
|
-
//
|
|
3
|
-
// 为什么不做「点一下直接提交」:
|
|
4
|
-
// 1. GitHub 不允许匿名创建 issue(REST 必须带认证);
|
|
5
|
-
// 2. 插件跑在用户机器上,内置 token 等于把 token 公开(asar/JS 反编译即可提取);
|
|
6
|
-
// 3. 走 OAuth 或自建中转需要 client secret / 服务器,成本与风险都不合算。
|
|
7
|
-
// 因此提供两条可用路径:
|
|
8
|
-
// A. 预填 issue 链接:标题/正文/标签/诊断都填好,用户点一次 Submit(等价于一点提交);
|
|
9
|
-
// B. 给 AI 的提示词:用户贴进对话框,让 AI 用 gh 建 issue 并回报链接。
|
|
10
|
-
//
|
|
11
|
-
// 另一件必须做的事:脱敏。issue 是公开的,绝不能把 home 目录、API key、token 带出去。
|
|
12
|
-
|
|
13
|
-
const SECRET_PATTERNS = [
|
|
14
|
-
/sk-[A-Za-z0-9_-]{12,}/g,
|
|
15
|
-
/ghp_[A-Za-z0-9]{20,}/g,
|
|
16
|
-
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
17
|
-
/(api[_-]?key|token|password|secret|authorization)\s*[:=]\s*["']?[^\s"',}]{8,}/gi,
|
|
18
|
-
/\b[A-Za-z0-9+/]{40,}={0,2}\b/g,
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 脱敏:home 目录 → ~,各类 key/token → ***。
|
|
23
|
-
* @param {string} input
|
|
24
|
-
* @param {string} [home]
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
export function redactText(input, home = '') {
|
|
28
|
-
let s = String(input ?? '')
|
|
29
|
-
if (home && home.length > 3) {
|
|
30
|
-
const variants = [home, home.split('\\').join('/'), home.split('/').join('\\')]
|
|
31
|
-
for (const v of variants) if (v && v.length > 3) s = s.split(v).join('~')
|
|
32
|
-
}
|
|
33
|
-
s = s.replace(/[A-Za-z]:\\Users\\[^\\\s"']+/g, '~')
|
|
34
|
-
s = s.replace(/(\/home\/|\/Users\/)[^/\s"']+/g, '~')
|
|
35
|
-
for (const re of SECRET_PATTERNS) s = s.replace(re, '***')
|
|
36
|
-
return s
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 组装诊断信息(markdown,已脱敏、已限长)。
|
|
41
|
-
* @param {object} input
|
|
42
|
-
* @param {string} [home]
|
|
43
|
-
* @returns {string}
|
|
44
|
-
*/
|
|
45
|
-
export function buildDiagnostics(input = {}, home = '') {
|
|
46
|
-
const rows = []
|
|
47
|
-
const add = (k, v) => {
|
|
48
|
-
if (v === undefined || v === null) return
|
|
49
|
-
const s = String(v).trim()
|
|
50
|
-
if (!s) return
|
|
51
|
-
rows.push('- ' + k + ': ' + redactText(s, home))
|
|
52
|
-
}
|
|
53
|
-
add('memory-eternal', input.version)
|
|
54
|
-
add('DSH', input.dshVersion)
|
|
55
|
-
add('Host', input.host)
|
|
56
|
-
add('OS', input.os)
|
|
57
|
-
add('Node', input.node)
|
|
58
|
-
add('Vault', input.vaultDir)
|
|
59
|
-
add('Vault source', input.vaultSource)
|
|
60
|
-
add('Workspace', input.workspace)
|
|
61
|
-
add('Cards', input.cards)
|
|
62
|
-
add('Graph', input.graph)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
body =
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'##
|
|
134
|
-
'',
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'',
|
|
138
|
-
|
|
139
|
-
'',
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
''
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'',
|
|
162
|
-
'
|
|
163
|
-
'',
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'',
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
'
|
|
173
|
-
'',
|
|
174
|
-
'
|
|
175
|
-
'',
|
|
176
|
-
'
|
|
177
|
-
'',
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
'
|
|
182
|
-
'',
|
|
183
|
-
'
|
|
184
|
-
'',
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'',
|
|
188
|
-
'Diagnostics
|
|
189
|
-
'
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1
|
+
// 记忆核心 · 「反馈异常」支撑层(纯函数,便于单测)
|
|
2
|
+
//
|
|
3
|
+
// 为什么不做「点一下直接提交」:
|
|
4
|
+
// 1. GitHub 不允许匿名创建 issue(REST 必须带认证);
|
|
5
|
+
// 2. 插件跑在用户机器上,内置 token 等于把 token 公开(asar/JS 反编译即可提取);
|
|
6
|
+
// 3. 走 OAuth 或自建中转需要 client secret / 服务器,成本与风险都不合算。
|
|
7
|
+
// 因此提供两条可用路径:
|
|
8
|
+
// A. 预填 issue 链接:标题/正文/标签/诊断都填好,用户点一次 Submit(等价于一点提交);
|
|
9
|
+
// B. 给 AI 的提示词:用户贴进对话框,让 AI 用 gh 建 issue 并回报链接。
|
|
10
|
+
//
|
|
11
|
+
// 另一件必须做的事:脱敏。issue 是公开的,绝不能把 home 目录、API key、token 带出去。
|
|
12
|
+
|
|
13
|
+
const SECRET_PATTERNS = [
|
|
14
|
+
/sk-[A-Za-z0-9_-]{12,}/g,
|
|
15
|
+
/ghp_[A-Za-z0-9]{20,}/g,
|
|
16
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
17
|
+
/(api[_-]?key|token|password|secret|authorization)\s*[:=]\s*["']?[^\s"',}]{8,}/gi,
|
|
18
|
+
/\b[A-Za-z0-9+/]{40,}={0,2}\b/g,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 脱敏:home 目录 → ~,各类 key/token → ***。
|
|
23
|
+
* @param {string} input
|
|
24
|
+
* @param {string} [home]
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function redactText(input, home = '') {
|
|
28
|
+
let s = String(input ?? '')
|
|
29
|
+
if (home && home.length > 3) {
|
|
30
|
+
const variants = [home, home.split('\\').join('/'), home.split('/').join('\\')]
|
|
31
|
+
for (const v of variants) if (v && v.length > 3) s = s.split(v).join('~')
|
|
32
|
+
}
|
|
33
|
+
s = s.replace(/[A-Za-z]:\\Users\\[^\\\s"']+/g, '~')
|
|
34
|
+
s = s.replace(/(\/home\/|\/Users\/)[^/\s"']+/g, '~')
|
|
35
|
+
for (const re of SECRET_PATTERNS) s = s.replace(re, '***')
|
|
36
|
+
return s
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 组装诊断信息(markdown,已脱敏、已限长)。
|
|
41
|
+
* @param {object} input
|
|
42
|
+
* @param {string} [home]
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function buildDiagnostics(input = {}, home = '') {
|
|
46
|
+
const rows = []
|
|
47
|
+
const add = (k, v) => {
|
|
48
|
+
if (v === undefined || v === null) return
|
|
49
|
+
const s = String(v).trim()
|
|
50
|
+
if (!s) return
|
|
51
|
+
rows.push('- ' + k + ': ' + redactText(s, home))
|
|
52
|
+
}
|
|
53
|
+
add('memory-eternal', input.version)
|
|
54
|
+
add('DSH', input.dshVersion)
|
|
55
|
+
add('Host', input.host)
|
|
56
|
+
add('OS', input.os)
|
|
57
|
+
add('Node', input.node)
|
|
58
|
+
add('Vault', input.vaultDir)
|
|
59
|
+
add('Vault source', input.vaultSource)
|
|
60
|
+
add('Workspace', input.workspace)
|
|
61
|
+
add('Cards', input.cards)
|
|
62
|
+
add('Graph', input.graph)
|
|
63
|
+
// 待应用配置的同步状态(#16):独立 Web 端保存 → pending 文件 → 宿主 drain。
|
|
64
|
+
// 失败必须能被「反馈异常」一键带出,否则用户只能人肉盯 tries 字段。
|
|
65
|
+
if (input.configSync && typeof input.configSync === 'object') {
|
|
66
|
+
const s = input.configSync
|
|
67
|
+
const bits = ['tries=' + Number(s.tries || 0)]
|
|
68
|
+
if (s.dropped) bits.push('DROPPED(改动已放弃但文件保留)')
|
|
69
|
+
if (s.lastError) bits.push('最近错误:' + String(s.lastError))
|
|
70
|
+
add('Config sync', bits.join(' · '))
|
|
71
|
+
}
|
|
72
|
+
if (input.health && input.health.ok === false) add('Health', 'FAIL: ' + (input.health.reason || ''))
|
|
73
|
+
const log = Array.isArray(input.captureLog) ? input.captureLog.slice(-10) : []
|
|
74
|
+
const out = ['### 诊断信息', '', ...rows]
|
|
75
|
+
if (log.length) {
|
|
76
|
+
out.push('', '最近沉淀日志(最多 10 条):', '`')
|
|
77
|
+
for (const e of log) out.push(redactText(typeof e === 'string' ? e : JSON.stringify(e), home))
|
|
78
|
+
out.push('`')
|
|
79
|
+
}
|
|
80
|
+
let text = out.join('\n')
|
|
81
|
+
if (text.length > 2400) text = text.slice(0, 2400) + '\n…(诊断信息过长已截断)'
|
|
82
|
+
return text
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 生成 GitHub 预填 issue 链接(带长度保护:URL 过长会被 GitHub/浏览器直接拒绝)。
|
|
87
|
+
* @param {{repo?:string, title?:string, body?:string, labels?:string[], maxLength?:number}} input
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
export function buildIssueUrl(input = {}) {
|
|
91
|
+
const repo = input.repo || 'EternalNight996/memory-eternal'
|
|
92
|
+
const title = String(input.title || '[Bug] ').slice(0, 120)
|
|
93
|
+
const labels = Array.isArray(input.labels) ? input.labels : ['bug']
|
|
94
|
+
const maxLength = Number(input.maxLength) > 500 ? Number(input.maxLength) : 7000
|
|
95
|
+
const build = (body) => {
|
|
96
|
+
const q = new URLSearchParams()
|
|
97
|
+
q.set('title', title)
|
|
98
|
+
q.set('body', body)
|
|
99
|
+
if (labels.length) q.set('labels', labels.join(','))
|
|
100
|
+
return 'https://github.com/' + repo + '/issues/new?' + q.toString()
|
|
101
|
+
}
|
|
102
|
+
let body = String(input.body || '')
|
|
103
|
+
let url = build(body)
|
|
104
|
+
if (url.length > maxLength) {
|
|
105
|
+
// URLSearchParams 会把正文按 percent-encoding 膨胀(中文约 9 倍),用「长度估算」截断会不准。
|
|
106
|
+
// 直接二分:找最大的 k 使 build(body.slice(0,k) + note).length <= maxLength —— 结果可证明在阈值内。
|
|
107
|
+
const note = '\n\n> 内容过长已截断:完整诊断请在插件里点「复制诊断信息」,再补到 issue 评论里。'
|
|
108
|
+
const withNote = (k) => build(body.slice(0, k) + note)
|
|
109
|
+
if (withNote(0).length > maxLength) {
|
|
110
|
+
body = note
|
|
111
|
+
} else {
|
|
112
|
+
let lo = 0
|
|
113
|
+
let hi = body.length
|
|
114
|
+
while (lo < hi) {
|
|
115
|
+
const mid = Math.ceil((lo + hi) / 2)
|
|
116
|
+
if (withNote(mid).length <= maxLength) lo = mid
|
|
117
|
+
else hi = mid - 1
|
|
118
|
+
}
|
|
119
|
+
body = body.slice(0, lo) + note
|
|
120
|
+
}
|
|
121
|
+
url = build(body)
|
|
122
|
+
}
|
|
123
|
+
return url
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 反馈正文模板(预填 issue 与提示词共用)。
|
|
128
|
+
* @param {{description?:string, diagnostics?:string, version?:string}} [input]
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function buildIssueBody(input = {}) {
|
|
132
|
+
return [
|
|
133
|
+
'## 现象',
|
|
134
|
+
String(input.description || '').trim() || '(请描述你遇到的问题)',
|
|
135
|
+
'',
|
|
136
|
+
'## 复现步骤',
|
|
137
|
+
'1. ',
|
|
138
|
+
'2. ',
|
|
139
|
+
'',
|
|
140
|
+
'## 期望行为',
|
|
141
|
+
'',
|
|
142
|
+
'## 实际行为',
|
|
143
|
+
'',
|
|
144
|
+
'## 环境',
|
|
145
|
+
'- 插件版本: ' + (input.version || '(见诊断信息)'),
|
|
146
|
+
'',
|
|
147
|
+
input.diagnostics || '',
|
|
148
|
+
'',
|
|
149
|
+
'---',
|
|
150
|
+
'> 由 memory-eternal 插件内「反馈异常」生成(诊断信息已自动脱敏:home 目录与各类 key/token 已替换)。',
|
|
151
|
+
].join('\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const REPO = 'https://github.com/EternalNight996/memory-eternal'
|
|
155
|
+
|
|
156
|
+
const PROMPT_ZH = [
|
|
157
|
+
'我在使用 memory-eternal(DeepSeek Harness 的记忆插件)时遇到了问题,请你帮我把这个问题提交到 GitHub。',
|
|
158
|
+
'',
|
|
159
|
+
'仓库:' + REPO,
|
|
160
|
+
'',
|
|
161
|
+
'请按这个流程做:',
|
|
162
|
+
'',
|
|
163
|
+
'1. 先跑 `gh issue list --repo EternalNight996/memory-eternal --state open --limit 50`,看是否已有相同问题:',
|
|
164
|
+
' - 已有 → 用 `gh issue comment <编号> --body-file <文件>` 补充现象,不要重复开新 issue;',
|
|
165
|
+
' - 没有 → 继续第 2 步。',
|
|
166
|
+
'2. 把正文写成文件,然后 `gh issue create --repo EternalNight996/memory-eternal --title "[Bug] <一句话概括>" --body-file <文件>`;正文用这个结构:',
|
|
167
|
+
' 现象 / 复现步骤 / 期望行为 / 实际行为 / 环境(插件版本 / DSH 版本 / 操作系统 / Node)/ 诊断信息。',
|
|
168
|
+
'3. 如果 `gh` 没登录或不可用,不要尝试提交,改为输出一条「标题与正文都已预填」的 GitHub 新建 issue 链接给我,我自己点提交。',
|
|
169
|
+
'4. 提交成功后,把 issue 链接发给我。',
|
|
170
|
+
'',
|
|
171
|
+
'注意:我的问题描述请原样照录,不要替我美化或省略细节;诊断信息原样放进「诊断信息」一节。',
|
|
172
|
+
'',
|
|
173
|
+
'我的问题描述:',
|
|
174
|
+
'<在这里写你的问题>',
|
|
175
|
+
'',
|
|
176
|
+
'诊断信息:',
|
|
177
|
+
'<把「反馈异常」弹窗里复制的诊断信息粘到这里>',
|
|
178
|
+
].join('\n')
|
|
179
|
+
|
|
180
|
+
const PROMPT_EN = [
|
|
181
|
+
'I hit a problem while using memory-eternal (a DeepSeek Harness memory plugin). Please file it on GitHub for me.',
|
|
182
|
+
'',
|
|
183
|
+
'Repo: ' + REPO,
|
|
184
|
+
'',
|
|
185
|
+
'Workflow:',
|
|
186
|
+
'',
|
|
187
|
+
'1. Run `gh issue list --repo EternalNight996/memory-eternal --state open --limit 50` first: if a matching issue exists, add a comment with `gh issue comment <n> --body-file <file>` instead of opening a duplicate.',
|
|
188
|
+
'2. Otherwise create it with `gh issue create --repo EternalNight996/memory-eternal --title "[Bug] <one-line summary>" --body-file <file>`, using sections: Symptoms / Steps to reproduce / Expected / Actual / Environment (plugin, DSH, OS, Node) / Diagnostics.',
|
|
189
|
+
'3. If `gh` is not authenticated or unavailable, do NOT try to submit: output a GitHub new-issue link with the title and body pre-filled instead, and I will click submit myself.',
|
|
190
|
+
'4. Report the resulting issue URL back to me.',
|
|
191
|
+
'',
|
|
192
|
+
'Keep my description verbatim - do not rewrite or summarise it. Put the diagnostics under the Diagnostics section as-is.',
|
|
193
|
+
'',
|
|
194
|
+
'My description:',
|
|
195
|
+
'<write your problem here>',
|
|
196
|
+
'',
|
|
197
|
+
'Diagnostics:',
|
|
198
|
+
'<paste the diagnostics copied from the in-app Report-a-bug dialog>',
|
|
199
|
+
].join('\n')
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 给 AI 的提示词(用户贴进对话框)。
|
|
203
|
+
* @param {{description?:string, diagnostics?:string, repo?:string, lang?:'zh'|'en'}} [input]
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
export function buildAgentPrompt(input = {}) {
|
|
207
|
+
const lang = input.lang === 'en' ? 'en' : 'zh'
|
|
208
|
+
let text = lang === 'en' ? PROMPT_EN : PROMPT_ZH
|
|
209
|
+
const repo = input.repo || 'EternalNight996/memory-eternal'
|
|
210
|
+
text = text.split(REPO).join('https://github.com/' + repo)
|
|
211
|
+
if (input.description) text = text.replace(lang === 'en' ? '<write your problem here>' : '<在这里写你的问题>', String(input.description).trim())
|
|
212
|
+
if (input.diagnostics) text = text.replace(lang === 'en' ? '<paste the diagnostics copied from the in-app Report-a-bug dialog>' : '<把「反馈异常」弹窗里复制的诊断信息粘到这里>', String(input.diagnostics).trim())
|
|
213
|
+
return text
|
|
214
|
+
}
|
package/lib/llm-openai.js
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
// 记忆核心 · OpenAI 兼容 LLM 适配器(独立进程用)。
|
|
2
|
-
//
|
|
3
|
-
// 在没有 DSH llm 服务的环境(MCP server / CLI / web / hooks)里,为
|
|
4
|
-
// lib/capture.js 的 summarizeTurn 提供同形状的 llm 对象:
|
|
5
|
-
// - listProviders() / listModels() → 供 resolveRoute 解析路由
|
|
6
|
-
// - stream(options) → async iterable,yield 纯文本 chunk(由 capture.js
|
|
7
|
-
// fallback BlockAssembler 拼装)
|
|
8
|
-
//
|
|
9
|
-
// 配置(环境变量,或显式传参覆盖):
|
|
10
|
-
// MEMORY_LLM_BASE_URL OpenAI 兼容端点,如 https://api.deepseek.com、
|
|
11
|
-
// http://127.0.0.1:11434/v1(Ollama)等
|
|
12
|
-
// MEMORY_LLM_KEY API key(本地端点可留空)
|
|
13
|
-
// MEMORY_LLM_MODEL 模型名,如 deepseek-chat
|
|
14
|
-
// 未配置时 createLlmFromEnv() 返回 null:调用方应降级(recall 不受影响,
|
|
15
|
-
// capture 退化为存原文卡)。
|
|
16
|
-
|
|
17
|
-
function flatContent(content) {
|
|
18
|
-
if (typeof content === 'string') return content
|
|
19
|
-
if (Array.isArray(content)) {
|
|
20
|
-
return content
|
|
21
|
-
.map((c) => (typeof c === 'string' ? c : c?.text ?? ''))
|
|
22
|
-
.filter(Boolean)
|
|
23
|
-
.join('\n')
|
|
24
|
-
}
|
|
25
|
-
return String(content ?? '')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createOpenAiLlm({ baseUrl, apiKey = '', model, purpose = 'memory-capture' } = {}) {
|
|
29
|
-
if (!baseUrl || !model) return null
|
|
30
|
-
const endpoint = baseUrl.replace(/\/+$/, '') + '/chat/completions'
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
purpose,
|
|
34
|
-
|
|
35
|
-
listProviders() {
|
|
36
|
-
return [{ id: 'openai-compat' }]
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
async listModels() {
|
|
40
|
-
return [{ id: model }]
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 非流式调用,整体作为单个 chunk yield(capture 的蒸馏一次性输出 JSON,
|
|
45
|
-
* 无需逐 token 流式)。signal 超时/中断会向上抛出。
|
|
46
|
-
*/
|
|
47
|
-
async *stream(options = {}) {
|
|
48
|
-
const messages = []
|
|
49
|
-
if (options.system) messages.push({ role: 'system', content: options.system })
|
|
50
|
-
for (const m of options.messages ?? []) {
|
|
51
|
-
messages.push({ role: m?.role ?? 'user', content: flatContent(m?.content) })
|
|
52
|
-
}
|
|
53
|
-
const body = {
|
|
54
|
-
model: options.model || model,
|
|
55
|
-
messages,
|
|
56
|
-
max_tokens: options.maxTokens ?? 900,
|
|
57
|
-
stream: false,
|
|
58
|
-
}
|
|
59
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
60
|
-
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
61
|
-
const res = await fetch(endpoint, {
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers,
|
|
64
|
-
body: JSON.stringify(body),
|
|
65
|
-
signal: options.signal ?? undefined,
|
|
66
|
-
})
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const detail = await res.text().catch(() => '')
|
|
69
|
-
throw new Error(`[memory-eternal] LLM ${res.status}: ${detail.slice(0, 300)}`)
|
|
70
|
-
}
|
|
71
|
-
const data = await res.json()
|
|
72
|
-
const text = data?.choices?.[0]?.message?.content ?? ''
|
|
73
|
-
if (text) yield text
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function createLlmFromEnv(env = process.env) {
|
|
79
|
-
const baseUrl = env.MEMORY_LLM_BASE_URL || env.OPENAI_BASE_URL || ''
|
|
80
|
-
const apiKey = env.MEMORY_LLM_KEY || env.OPENAI_API_KEY || ''
|
|
81
|
-
const model = env.MEMORY_LLM_MODEL || ''
|
|
82
|
-
if (!baseUrl || !model) return null
|
|
83
|
-
return createOpenAiLlm({ baseUrl, apiKey, model })
|
|
84
|
-
}
|
|
1
|
+
// 记忆核心 · OpenAI 兼容 LLM 适配器(独立进程用)。
|
|
2
|
+
//
|
|
3
|
+
// 在没有 DSH llm 服务的环境(MCP server / CLI / web / hooks)里,为
|
|
4
|
+
// lib/capture.js 的 summarizeTurn 提供同形状的 llm 对象:
|
|
5
|
+
// - listProviders() / listModels() → 供 resolveRoute 解析路由
|
|
6
|
+
// - stream(options) → async iterable,yield 纯文本 chunk(由 capture.js
|
|
7
|
+
// fallback BlockAssembler 拼装)
|
|
8
|
+
//
|
|
9
|
+
// 配置(环境变量,或显式传参覆盖):
|
|
10
|
+
// MEMORY_LLM_BASE_URL OpenAI 兼容端点,如 https://api.deepseek.com、
|
|
11
|
+
// http://127.0.0.1:11434/v1(Ollama)等
|
|
12
|
+
// MEMORY_LLM_KEY API key(本地端点可留空)
|
|
13
|
+
// MEMORY_LLM_MODEL 模型名,如 deepseek-chat
|
|
14
|
+
// 未配置时 createLlmFromEnv() 返回 null:调用方应降级(recall 不受影响,
|
|
15
|
+
// capture 退化为存原文卡)。
|
|
16
|
+
|
|
17
|
+
function flatContent(content) {
|
|
18
|
+
if (typeof content === 'string') return content
|
|
19
|
+
if (Array.isArray(content)) {
|
|
20
|
+
return content
|
|
21
|
+
.map((c) => (typeof c === 'string' ? c : c?.text ?? ''))
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join('\n')
|
|
24
|
+
}
|
|
25
|
+
return String(content ?? '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createOpenAiLlm({ baseUrl, apiKey = '', model, purpose = 'memory-capture' } = {}) {
|
|
29
|
+
if (!baseUrl || !model) return null
|
|
30
|
+
const endpoint = baseUrl.replace(/\/+$/, '') + '/chat/completions'
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
purpose,
|
|
34
|
+
|
|
35
|
+
listProviders() {
|
|
36
|
+
return [{ id: 'openai-compat' }]
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async listModels() {
|
|
40
|
+
return [{ id: model }]
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 非流式调用,整体作为单个 chunk yield(capture 的蒸馏一次性输出 JSON,
|
|
45
|
+
* 无需逐 token 流式)。signal 超时/中断会向上抛出。
|
|
46
|
+
*/
|
|
47
|
+
async *stream(options = {}) {
|
|
48
|
+
const messages = []
|
|
49
|
+
if (options.system) messages.push({ role: 'system', content: options.system })
|
|
50
|
+
for (const m of options.messages ?? []) {
|
|
51
|
+
messages.push({ role: m?.role ?? 'user', content: flatContent(m?.content) })
|
|
52
|
+
}
|
|
53
|
+
const body = {
|
|
54
|
+
model: options.model || model,
|
|
55
|
+
messages,
|
|
56
|
+
max_tokens: options.maxTokens ?? 900,
|
|
57
|
+
stream: false,
|
|
58
|
+
}
|
|
59
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
60
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
61
|
+
const res = await fetch(endpoint, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers,
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
signal: options.signal ?? undefined,
|
|
66
|
+
})
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const detail = await res.text().catch(() => '')
|
|
69
|
+
throw new Error(`[memory-eternal] LLM ${res.status}: ${detail.slice(0, 300)}`)
|
|
70
|
+
}
|
|
71
|
+
const data = await res.json()
|
|
72
|
+
const text = data?.choices?.[0]?.message?.content ?? ''
|
|
73
|
+
if (text) yield text
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createLlmFromEnv(env = process.env) {
|
|
79
|
+
const baseUrl = env.MEMORY_LLM_BASE_URL || env.OPENAI_BASE_URL || ''
|
|
80
|
+
const apiKey = env.MEMORY_LLM_KEY || env.OPENAI_API_KEY || ''
|
|
81
|
+
const model = env.MEMORY_LLM_MODEL || ''
|
|
82
|
+
if (!baseUrl || !model) return null
|
|
83
|
+
return createOpenAiLlm({ baseUrl, apiKey, model })
|
|
84
|
+
}
|