@zoria-lind/dsh-ecolink-adapter 0.2.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/adapter/src/modules/memoryInject.js +189 -189
- package/package.json +4 -1
- package/service/README.md +10 -2
- package/service/package.json +37 -37
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
// dsh-ecolink-adapter v1(E1 通道分离):记忆注入模块。
|
|
2
|
-
// 与网页端扩展同源的核心逻辑(复用 ../extension/core 的选择器/提示词纯函数):
|
|
3
|
-
// - 直读 memory.json(只读,mtime 缓存——唯一写者是 ecolink-service)
|
|
4
|
-
// - 【指令通道,默认开】agent/pre-step 每轮幂等追加一条 source.kind='plugin' 的
|
|
5
|
-
// 快照消息承载 [dsh-ecolink 指令] 块(照抄内核 dsh-time-context 的成熟做法:
|
|
6
|
-
// prepend 注册 + await next() + 末尾 append createUserMessage;决策里的 messages
|
|
7
|
-
// 只喂本步请求、不写入会话事件)。不改写用户消息 → 不碰前缀缓存。
|
|
8
|
-
// - 【内容通道,默认关(contentInjectionEnabled 逃生舱)】旧行为:改写用户消息注入
|
|
9
|
-
// 记忆块(打分 + 预算 + #记忆名 + singleInjection 会话去重)。
|
|
10
|
-
// - 块标记 [dsh-ecolink 记忆] / [dsh-ecolink 指令]:幂等剥离(DSH 每 step 重发
|
|
11
|
-
// inbox 同一消息,与网页端同款防残留逻辑)
|
|
12
|
-
// 稳定性铁律:任何异常 → 静默放行,不注入。
|
|
13
|
-
// 内核事实(09 §A 已核实):agent/* 事件载荷由 agentEvents() 自动 {...payload, agent} 融合
|
|
14
|
-
// → pre-step 载荷直接有 payload.agent(v0 的 agent/status 跟踪 hack 据此移除)。
|
|
15
|
-
|
|
16
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
17
|
-
import { homedir } from 'node:os'
|
|
18
|
-
import { join } from 'node:path'
|
|
19
|
-
import { selectMemories, effectiveBudget, parseMemoryCommand } from '../../../extension/core/selector.mjs'
|
|
20
|
-
import { buildInjectedBlock, buildInstructionBlock, stripAllBlocks } from '../../../extension/core/prompt.mjs'
|
|
21
|
-
import { resolveKernelModule } from '../kernel.js'
|
|
22
|
-
|
|
23
|
-
const INSTRUCTION_SUMMARY = 'ecolink-instruction'
|
|
24
|
-
|
|
25
|
-
function expandHome(dir) {
|
|
26
|
-
if (typeof dir !== 'string' || dir.length === 0) return dir
|
|
27
|
-
if (dir === '~' || dir.startsWith('~/') || dir.startsWith('~\\')) return join(homedir(), dir.slice(2))
|
|
28
|
-
return dir
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createMemoryInjectModule(ctx, config, stats, deps = {}) {
|
|
32
|
-
if (!config?.enabled) return () => {}
|
|
33
|
-
if (!ctx || typeof ctx.on !== 'function') return () => {}
|
|
34
|
-
|
|
35
|
-
const poolPath = expandHome(deps.poolPath ?? config.poolPath)
|
|
36
|
-
|
|
37
|
-
// createUserMessage:测试经 deps 注入假实现(显式 null = 强制禁用,模拟解析失败);
|
|
38
|
-
// 生产从 @deepseek-ai/dsh-llm 解析(失败 → 指令通道静默禁用,警告一次)
|
|
39
|
-
let createUserMessage = null
|
|
40
|
-
if (deps.createUserMessage === null) {
|
|
41
|
-
// 强制禁用路径(测试用)
|
|
42
|
-
} else if (typeof deps.createUserMessage === 'function') {
|
|
43
|
-
createUserMessage = deps.createUserMessage
|
|
44
|
-
} else {
|
|
45
|
-
const llm = resolveKernelModule('@deepseek-ai/dsh-llm')
|
|
46
|
-
if (llm && typeof llm.createUserMessage === 'function') {
|
|
47
|
-
createUserMessage = llm.createUserMessage
|
|
48
|
-
} else {
|
|
49
|
-
console.warn('[dsh-ecolink-adapter] 未能解析 @deepseek-ai/dsh-llm,指令通道禁用(记忆注入不受影响)')
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 池读取:mtime 缓存(TTL 兜底 + 文件变化立即重读);失败返回 null → 不注入
|
|
54
|
-
let poolCache = { mtimeMs: 0, at: 0, data: null }
|
|
55
|
-
function readPool() {
|
|
56
|
-
try {
|
|
57
|
-
if (!existsSync(poolPath)) return null
|
|
58
|
-
const st = statSync(poolPath)
|
|
59
|
-
const now = Date.now()
|
|
60
|
-
if (poolCache.data && st.mtimeMs === poolCache.mtimeMs && now - poolCache.at < config.poolTtlMs) {
|
|
61
|
-
return poolCache.data
|
|
62
|
-
}
|
|
63
|
-
const data = JSON.parse(readFileSync(poolPath, 'utf8'))
|
|
64
|
-
poolCache = { mtimeMs: st.mtimeMs, at: now, data }
|
|
65
|
-
return data
|
|
66
|
-
} catch {
|
|
67
|
-
return poolCache.data // 读失败:用旧缓存或 null
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 指令消息识别(幂等剥旧用:summary 唯一属于本插件的指令快照)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
&& m?.source?.summary === INSTRUCTION_SUMMARY
|
|
75
|
-
|
|
76
|
-
// 指令快照消息(与内核 dsh-time-context 同款形状:form='snapshot' + sections)
|
|
77
|
-
// E9.1:指令块内追加"读路径"指引——旧版只教写不教读,模型不知道池可查,
|
|
78
|
-
// "你还记得吗"类问题零工具调用凭印象回答(2026-09-14 实测)。
|
|
79
|
-
// E9.2:从"建议"升级为"硬规则"——查池是回答记忆类问题的前提,查之前禁止
|
|
80
|
-
// 答"不知道/不记得"(模型常因"当前上下文没有"而诚实地说不知道,但池是另一回事)
|
|
81
|
-
const READ_HINT = '跨端记忆查询(强制规则):当用户询问或提及"之前记过什么/你还记得吗/我的偏好/日程安排/考试安排/习惯/计划"等任何可能与历史记忆有关的问题时,第一步必须先运行 /ecolink-diff 命令查询记忆池(本会话首次调用返回全量),需要细节时再用 read 工具读 ~/.dsh-memory/memory.json 核对。禁止在未查询记忆池的情况下回答"不知道/不记得/没有记录"——记忆池与本对话上下文是两回事,池里有而上下文没有是常态。'
|
|
82
|
-
function buildInstructionMessage() {
|
|
83
|
-
const text = buildInstructionBlock().replace('[/dsh-ecolink 指令]', READ_HINT + '\n[/dsh-ecolink 指令]')
|
|
84
|
-
return createUserMessage({
|
|
85
|
-
content: [{ type: 'text', text }],
|
|
86
|
-
source: { kind: 'plugin
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 内容通道(E1 逃生舱):旧行为的按消息改写注入
|
|
91
|
-
function injectContentIntoMessages(messages) {
|
|
92
|
-
const sk = String(currentAgent?.session?.id ?? currentAgent?.id ?? 'default')
|
|
93
|
-
if (config.singleInjection && injectedSessions.has(sk)) return { messages, changed: false }
|
|
94
|
-
|
|
95
|
-
const pool = readPool()
|
|
96
|
-
if (!pool) return { messages, changed: false }
|
|
97
|
-
const shared = pool.shared_pool ?? []
|
|
98
|
-
const sessionPools = pool.session_pools ?? {}
|
|
99
|
-
|
|
100
|
-
const out = []
|
|
101
|
-
let changed = false
|
|
102
|
-
for (const message of messages) {
|
|
103
|
-
const srcKind = message?.source?.kind
|
|
104
|
-
if (srcKind !== undefined && srcKind !== 'user') { out.push(message); continue }
|
|
105
|
-
const content = message?.content
|
|
106
|
-
const text = typeof content === 'string' ? content
|
|
107
|
-
: Array.isArray(content) ? content.filter((b) => b?.type === 'text').map((b) => b.text).join('\n')
|
|
108
|
-
: ''
|
|
109
|
-
if (!text || text.trim().length === 0) { out.push(message); continue }
|
|
110
|
-
|
|
111
|
-
// #记忆名 显式调用 → 只注入该会话记忆;否则共享池 + 全部会话池
|
|
112
|
-
const cmd = parseMemoryCommand(text, sessionPools)
|
|
113
|
-
let items
|
|
114
|
-
let sessionName = null
|
|
115
|
-
if (cmd) {
|
|
116
|
-
const sp = sessionPools[cmd]
|
|
117
|
-
items = (sp?.memories ?? []).map((m) => ({ ...m, identity: sp?.identity }))
|
|
118
|
-
sessionName = sp?.identity
|
|
119
|
-
} else {
|
|
120
|
-
items = [
|
|
121
|
-
...shared,
|
|
122
|
-
...Object.entries(sessionPools).flatMap(([, sp]) => (sp?.memories ?? []).map((m) => ({ ...m, identity: sp?.identity }))),
|
|
123
|
-
]
|
|
124
|
-
}
|
|
125
|
-
const budget = effectiveBudget(text, config.maxInjectionChars)
|
|
126
|
-
const { memories } = selectMemories(items, text, budget)
|
|
127
|
-
if (memories.length === 0) { out.push(message); continue }
|
|
128
|
-
|
|
129
|
-
const clean = stripAllBlocks(text)
|
|
130
|
-
const composed = buildInjectedBlock(memories, sessionName) + '\n\n' + clean
|
|
131
|
-
if (composed === text) { out.push(message); continue }
|
|
132
|
-
out.push({ ...message, content: typeof content === 'string' ? composed : [{ type: 'text', text: composed }] })
|
|
133
|
-
changed = true
|
|
134
|
-
stats?.bump('ecolinkAdapter.injected', 1)
|
|
135
|
-
}
|
|
136
|
-
if (changed) injectedSessions.add(sk)
|
|
137
|
-
return { messages: out, changed }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// 当前 agent(pre-step 载荷自带 agent,内核 agentEvents() 融合;保底用上次 running 状态)
|
|
141
|
-
let currentAgent
|
|
142
|
-
const onStatus = (payload) => {
|
|
143
|
-
if (payload?.status === 'running' && payload?.agent) currentAgent = payload.agent
|
|
144
|
-
}
|
|
145
|
-
ctx.on?.('agent/status', onStatus)
|
|
146
|
-
|
|
147
|
-
const injectedSessions = new Set()
|
|
148
|
-
|
|
149
|
-
const handler = async (payload, next) => {
|
|
150
|
-
const decision = await next()
|
|
151
|
-
try {
|
|
152
|
-
if (!decision || decision.kind !== 'enter') return decision
|
|
153
|
-
if (payload?.signal?.aborted) return decision
|
|
154
|
-
if (!config.injectEnabled) return decision
|
|
155
|
-
const messages = Array.isArray(decision.messages) ? decision.messages : []
|
|
156
|
-
if (messages.length === 0) return decision
|
|
157
|
-
|
|
158
|
-
currentAgent = payload?.agent ?? currentAgent
|
|
159
|
-
let out = messages
|
|
160
|
-
let changed = false
|
|
161
|
-
|
|
162
|
-
// ① 指令通道(默认开):剥旧指令快照 → 末尾追加新指令(每轮幂等,不改写用户消息)
|
|
163
|
-
if (config.instructionEnabled && createUserMessage) {
|
|
164
|
-
const kept = out.filter((m) => !isOwnInstruction(m))
|
|
165
|
-
out = [...kept, buildInstructionMessage()]
|
|
166
|
-
changed = true
|
|
167
|
-
stats?.bump('ecolinkAdapter.instructed', 1)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ② 内容通道(默认关):改写用户消息注入记忆块
|
|
171
|
-
if (config.contentInjectionEnabled) {
|
|
172
|
-
const r = injectContentIntoMessages(out)
|
|
173
|
-
out = r.messages
|
|
174
|
-
changed = changed || r.changed
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return changed ? { ...decision, messages: out } : decision
|
|
178
|
-
} catch (err) {
|
|
179
|
-
console.warn(`[dsh-ecolink-adapter] 注入失败,静默放行(${err?.message ?? err})`)
|
|
180
|
-
return decision
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
ctx.on('agent/pre-step', handler, { prepend: true })
|
|
185
|
-
return () => {
|
|
186
|
-
ctx.off('agent/pre-step', handler)
|
|
187
|
-
ctx.off?.('agent/status', onStatus)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
1
|
+
// dsh-ecolink-adapter v1(E1 通道分离):记忆注入模块。
|
|
2
|
+
// 与网页端扩展同源的核心逻辑(复用 ../extension/core 的选择器/提示词纯函数):
|
|
3
|
+
// - 直读 memory.json(只读,mtime 缓存——唯一写者是 ecolink-service)
|
|
4
|
+
// - 【指令通道,默认开】agent/pre-step 每轮幂等追加一条 source.kind='plugin' 的
|
|
5
|
+
// 快照消息承载 [dsh-ecolink 指令] 块(照抄内核 dsh-time-context 的成熟做法:
|
|
6
|
+
// prepend 注册 + await next() + 末尾 append createUserMessage;决策里的 messages
|
|
7
|
+
// 只喂本步请求、不写入会话事件)。不改写用户消息 → 不碰前缀缓存。
|
|
8
|
+
// - 【内容通道,默认关(contentInjectionEnabled 逃生舱)】旧行为:改写用户消息注入
|
|
9
|
+
// 记忆块(打分 + 预算 + #记忆名 + singleInjection 会话去重)。
|
|
10
|
+
// - 块标记 [dsh-ecolink 记忆] / [dsh-ecolink 指令]:幂等剥离(DSH 每 step 重发
|
|
11
|
+
// inbox 同一消息,与网页端同款防残留逻辑)
|
|
12
|
+
// 稳定性铁律:任何异常 → 静默放行,不注入。
|
|
13
|
+
// 内核事实(09 §A 已核实):agent/* 事件载荷由 agentEvents() 自动 {...payload, agent} 融合
|
|
14
|
+
// → pre-step 载荷直接有 payload.agent(v0 的 agent/status 跟踪 hack 据此移除)。
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { selectMemories, effectiveBudget, parseMemoryCommand } from '../../../extension/core/selector.mjs'
|
|
20
|
+
import { buildInjectedBlock, buildInstructionBlock, stripAllBlocks } from '../../../extension/core/prompt.mjs'
|
|
21
|
+
import { resolveKernelModule } from '../kernel.js'
|
|
22
|
+
|
|
23
|
+
const INSTRUCTION_SUMMARY = 'ecolink-instruction'
|
|
24
|
+
|
|
25
|
+
function expandHome(dir) {
|
|
26
|
+
if (typeof dir !== 'string' || dir.length === 0) return dir
|
|
27
|
+
if (dir === '~' || dir.startsWith('~/') || dir.startsWith('~\\')) return join(homedir(), dir.slice(2))
|
|
28
|
+
return dir
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createMemoryInjectModule(ctx, config, stats, deps = {}) {
|
|
32
|
+
if (!config?.enabled) return () => {}
|
|
33
|
+
if (!ctx || typeof ctx.on !== 'function') return () => {}
|
|
34
|
+
|
|
35
|
+
const poolPath = expandHome(deps.poolPath ?? config.poolPath)
|
|
36
|
+
|
|
37
|
+
// createUserMessage:测试经 deps 注入假实现(显式 null = 强制禁用,模拟解析失败);
|
|
38
|
+
// 生产从 @deepseek-ai/dsh-llm 解析(失败 → 指令通道静默禁用,警告一次)
|
|
39
|
+
let createUserMessage = null
|
|
40
|
+
if (deps.createUserMessage === null) {
|
|
41
|
+
// 强制禁用路径(测试用)
|
|
42
|
+
} else if (typeof deps.createUserMessage === 'function') {
|
|
43
|
+
createUserMessage = deps.createUserMessage
|
|
44
|
+
} else {
|
|
45
|
+
const llm = resolveKernelModule('@deepseek-ai/dsh-llm')
|
|
46
|
+
if (llm && typeof llm.createUserMessage === 'function') {
|
|
47
|
+
createUserMessage = llm.createUserMessage
|
|
48
|
+
} else {
|
|
49
|
+
console.warn('[dsh-ecolink-adapter] 未能解析 @deepseek-ai/dsh-llm,指令通道禁用(记忆注入不受影响)')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 池读取:mtime 缓存(TTL 兜底 + 文件变化立即重读);失败返回 null → 不注入
|
|
54
|
+
let poolCache = { mtimeMs: 0, at: 0, data: null }
|
|
55
|
+
function readPool() {
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(poolPath)) return null
|
|
58
|
+
const st = statSync(poolPath)
|
|
59
|
+
const now = Date.now()
|
|
60
|
+
if (poolCache.data && st.mtimeMs === poolCache.mtimeMs && now - poolCache.at < config.poolTtlMs) {
|
|
61
|
+
return poolCache.data
|
|
62
|
+
}
|
|
63
|
+
const data = JSON.parse(readFileSync(poolPath, 'utf8'))
|
|
64
|
+
poolCache = { mtimeMs: st.mtimeMs, at: now, data }
|
|
65
|
+
return data
|
|
66
|
+
} catch {
|
|
67
|
+
return poolCache.data // 读失败:用旧缓存或 null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 指令消息识别(幂等剥旧用:summary 唯一属于本插件的指令快照)
|
|
72
|
+
// v4 source 约定(2026-10-01):kind = 'plugin:<插件名>',不再有 plugin 字段
|
|
73
|
+
const isOwnInstruction = (m) => m?.source?.kind === 'plugin:ecolink-adapter'
|
|
74
|
+
&& m?.source?.summary === INSTRUCTION_SUMMARY
|
|
75
|
+
|
|
76
|
+
// 指令快照消息(与内核 dsh-time-context 同款形状:form='snapshot' + sections)
|
|
77
|
+
// E9.1:指令块内追加"读路径"指引——旧版只教写不教读,模型不知道池可查,
|
|
78
|
+
// "你还记得吗"类问题零工具调用凭印象回答(2026-09-14 实测)。
|
|
79
|
+
// E9.2:从"建议"升级为"硬规则"——查池是回答记忆类问题的前提,查之前禁止
|
|
80
|
+
// 答"不知道/不记得"(模型常因"当前上下文没有"而诚实地说不知道,但池是另一回事)
|
|
81
|
+
const READ_HINT = '跨端记忆查询(强制规则):当用户询问或提及"之前记过什么/你还记得吗/我的偏好/日程安排/考试安排/习惯/计划"等任何可能与历史记忆有关的问题时,第一步必须先运行 /ecolink-diff 命令查询记忆池(本会话首次调用返回全量),需要细节时再用 read 工具读 ~/.dsh-memory/memory.json 核对。禁止在未查询记忆池的情况下回答"不知道/不记得/没有记录"——记忆池与本对话上下文是两回事,池里有而上下文没有是常态。'
|
|
82
|
+
function buildInstructionMessage() {
|
|
83
|
+
const text = buildInstructionBlock().replace('[/dsh-ecolink 指令]', READ_HINT + '\n[/dsh-ecolink 指令]')
|
|
84
|
+
return createUserMessage({
|
|
85
|
+
content: [{ type: 'text', text }],
|
|
86
|
+
source: { kind: 'plugin:ecolink-adapter', form: 'snapshot', summary: INSTRUCTION_SUMMARY, sections: [{ name: 'ecolink-instruction', text }] },
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 内容通道(E1 逃生舱):旧行为的按消息改写注入
|
|
91
|
+
function injectContentIntoMessages(messages) {
|
|
92
|
+
const sk = String(currentAgent?.session?.id ?? currentAgent?.id ?? 'default')
|
|
93
|
+
if (config.singleInjection && injectedSessions.has(sk)) return { messages, changed: false }
|
|
94
|
+
|
|
95
|
+
const pool = readPool()
|
|
96
|
+
if (!pool) return { messages, changed: false }
|
|
97
|
+
const shared = pool.shared_pool ?? []
|
|
98
|
+
const sessionPools = pool.session_pools ?? {}
|
|
99
|
+
|
|
100
|
+
const out = []
|
|
101
|
+
let changed = false
|
|
102
|
+
for (const message of messages) {
|
|
103
|
+
const srcKind = message?.source?.kind
|
|
104
|
+
if (srcKind !== undefined && srcKind !== 'user') { out.push(message); continue }
|
|
105
|
+
const content = message?.content
|
|
106
|
+
const text = typeof content === 'string' ? content
|
|
107
|
+
: Array.isArray(content) ? content.filter((b) => b?.type === 'text').map((b) => b.text).join('\n')
|
|
108
|
+
: ''
|
|
109
|
+
if (!text || text.trim().length === 0) { out.push(message); continue }
|
|
110
|
+
|
|
111
|
+
// #记忆名 显式调用 → 只注入该会话记忆;否则共享池 + 全部会话池
|
|
112
|
+
const cmd = parseMemoryCommand(text, sessionPools)
|
|
113
|
+
let items
|
|
114
|
+
let sessionName = null
|
|
115
|
+
if (cmd) {
|
|
116
|
+
const sp = sessionPools[cmd]
|
|
117
|
+
items = (sp?.memories ?? []).map((m) => ({ ...m, identity: sp?.identity }))
|
|
118
|
+
sessionName = sp?.identity
|
|
119
|
+
} else {
|
|
120
|
+
items = [
|
|
121
|
+
...shared,
|
|
122
|
+
...Object.entries(sessionPools).flatMap(([, sp]) => (sp?.memories ?? []).map((m) => ({ ...m, identity: sp?.identity }))),
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
const budget = effectiveBudget(text, config.maxInjectionChars)
|
|
126
|
+
const { memories } = selectMemories(items, text, budget)
|
|
127
|
+
if (memories.length === 0) { out.push(message); continue }
|
|
128
|
+
|
|
129
|
+
const clean = stripAllBlocks(text)
|
|
130
|
+
const composed = buildInjectedBlock(memories, sessionName) + '\n\n' + clean
|
|
131
|
+
if (composed === text) { out.push(message); continue }
|
|
132
|
+
out.push({ ...message, content: typeof content === 'string' ? composed : [{ type: 'text', text: composed }] })
|
|
133
|
+
changed = true
|
|
134
|
+
stats?.bump('ecolinkAdapter.injected', 1)
|
|
135
|
+
}
|
|
136
|
+
if (changed) injectedSessions.add(sk)
|
|
137
|
+
return { messages: out, changed }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 当前 agent(pre-step 载荷自带 agent,内核 agentEvents() 融合;保底用上次 running 状态)
|
|
141
|
+
let currentAgent
|
|
142
|
+
const onStatus = (payload) => {
|
|
143
|
+
if (payload?.status === 'running' && payload?.agent) currentAgent = payload.agent
|
|
144
|
+
}
|
|
145
|
+
ctx.on?.('agent/status', onStatus)
|
|
146
|
+
|
|
147
|
+
const injectedSessions = new Set()
|
|
148
|
+
|
|
149
|
+
const handler = async (payload, next) => {
|
|
150
|
+
const decision = await next()
|
|
151
|
+
try {
|
|
152
|
+
if (!decision || decision.kind !== 'enter') return decision
|
|
153
|
+
if (payload?.signal?.aborted) return decision
|
|
154
|
+
if (!config.injectEnabled) return decision
|
|
155
|
+
const messages = Array.isArray(decision.messages) ? decision.messages : []
|
|
156
|
+
if (messages.length === 0) return decision
|
|
157
|
+
|
|
158
|
+
currentAgent = payload?.agent ?? currentAgent
|
|
159
|
+
let out = messages
|
|
160
|
+
let changed = false
|
|
161
|
+
|
|
162
|
+
// ① 指令通道(默认开):剥旧指令快照 → 末尾追加新指令(每轮幂等,不改写用户消息)
|
|
163
|
+
if (config.instructionEnabled && createUserMessage) {
|
|
164
|
+
const kept = out.filter((m) => !isOwnInstruction(m))
|
|
165
|
+
out = [...kept, buildInstructionMessage()]
|
|
166
|
+
changed = true
|
|
167
|
+
stats?.bump('ecolinkAdapter.instructed', 1)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ② 内容通道(默认关):改写用户消息注入记忆块
|
|
171
|
+
if (config.contentInjectionEnabled) {
|
|
172
|
+
const r = injectContentIntoMessages(out)
|
|
173
|
+
out = r.messages
|
|
174
|
+
changed = changed || r.changed
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return changed ? { ...decision, messages: out } : decision
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn(`[dsh-ecolink-adapter] 注入失败,静默放行(${err?.message ?? err})`)
|
|
180
|
+
return decision
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ctx.on('agent/pre-step', handler, { prepend: true })
|
|
185
|
+
return () => {
|
|
186
|
+
ctx.off('agent/pre-step', handler)
|
|
187
|
+
ctx.off?.('agent/status', onStatus)
|
|
188
|
+
}
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoria-lind/dsh-ecolink-adapter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "dsh-ecolink DSH 适配层:直读本地记忆池、pre-step 注入(打分+预算+会话去重)、#记忆名 过滤,打通网页端记忆生态",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"!service/test.mjs",
|
|
22
22
|
"!service/fix-pool.mjs"
|
|
23
23
|
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
24
27
|
"repository": {
|
|
25
28
|
"type": "git",
|
|
26
29
|
"url": "git+https://github.com/Zoria-Lind/dsh-ecolink.git"
|
package/service/README.md
CHANGED
|
@@ -61,9 +61,17 @@ node service/server.mjs
|
|
|
61
61
|
|
|
62
62
|
所有响应 JSON;写操作经内部队列串行化,落盘为 tmp+rename 原子写;文件损坏时自动备份并空池重启。
|
|
63
63
|
|
|
64
|
-
## 压缩(0.2.0
|
|
64
|
+
## 压缩(0.2.0 重构:**双池事务**)
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
两个池:主池(`memory.json`,日常读写)+ 暂存池(`staging.json`,压缩期间的托管区)。
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
开始 ──▶ 过时记忆:主池 ──▶ 暂存池(主池清空该批;旧记忆全程有落盘副本,可随时回滚)
|
|
70
|
+
压缩 ──▶ 模型输出新标签 ──▶ 普通收割/同步 ──▶ 主池
|
|
71
|
+
验收 ──▶ 达标:清空暂存池(提交) │ 不达标/失败:暂存池原样放回主池(回滚,零丢失)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
双模式(网页端免费模型 / API)共用同一套事务,压缩事故类(误删/清池)在机制上不可能发生。
|
|
67
75
|
|
|
68
76
|
1. **暂存(stage)**:过时记忆整体移入 staging.json,主池清空该批;压缩清单即暂存条目(每行 `[key] 内容`,供模型复用原 key 做同 key 覆盖);
|
|
69
77
|
2. **压缩**:模型逐条阅读清单,输出说明 `压缩完成(重复 N 条)`(N = 模型统计的完全重复/高度重叠条数)+ 完整属性标签(`<DSM:memory_write key="…" importance="always|called">内容</…>`);标签经普通收割/同步通道入主池;
|
package/service/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@zoria-lind/dsh-ecolink-service",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "dsh-ecolink 本地记忆服务:网页扩展与 DSH 共享记忆池的唯一写者(零依赖、127.0.0.1、token 鉴权)",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "server.mjs",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./server.mjs"
|
|
10
|
-
},
|
|
11
|
-
"bin": {
|
|
12
|
-
"ecolink-service": "server.mjs"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"server.mjs",
|
|
16
|
-
"pool.mjs",
|
|
17
|
-
"compress.mjs",
|
|
18
|
-
"config.example.json",
|
|
19
|
-
"README.md",
|
|
20
|
-
"LICENSE"
|
|
21
|
-
],
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=18.0.0"
|
|
24
|
-
},
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/Zoria-Lind/dsh-ecolink.git",
|
|
28
|
-
"directory": "service"
|
|
29
|
-
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"dsh",
|
|
32
|
-
"deepseek-harness",
|
|
33
|
-
"memory",
|
|
34
|
-
"ecolink",
|
|
35
|
-
"local-service"
|
|
36
|
-
]
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoria-lind/dsh-ecolink-service",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "dsh-ecolink 本地记忆服务:网页扩展与 DSH 共享记忆池的唯一写者(零依赖、127.0.0.1、token 鉴权)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "server.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./server.mjs"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"ecolink-service": "server.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"server.mjs",
|
|
16
|
+
"pool.mjs",
|
|
17
|
+
"compress.mjs",
|
|
18
|
+
"config.example.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Zoria-Lind/dsh-ecolink.git",
|
|
28
|
+
"directory": "service"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"dsh",
|
|
32
|
+
"deepseek-harness",
|
|
33
|
+
"memory",
|
|
34
|
+
"ecolink",
|
|
35
|
+
"local-service"
|
|
36
|
+
]
|
|
37
|
+
}
|