@zoria-lind/dsh-ecolink-adapter 0.2.2 → 0.3.0
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 +45 -86
- package/adapter/README.md +45 -86
- package/adapter/cordis.patch.yml +2 -2
- package/adapter/src/client.js +117 -0
- package/adapter/src/config.js +5 -5
- package/adapter/src/index.js +9 -28
- package/adapter/src/modules/memoryAccess.js +27 -0
- package/adapter/src/modules/memoryDiff.js +31 -116
- package/adapter/src/modules/memoryInject.js +80 -189
- package/adapter/src/modules/memoryPanel.js +156 -0
- package/adapter/src/modules/memoryReader.js +87 -0
- package/adapter/src/modules/memoryWrite.js +3 -2
- package/adapter/src/modules/serviceGuard.js +22 -12
- package/extension/core/artifact.mjs +208 -0
- package/extension/core/entry.mjs +7 -0
- package/extension/core/queue.mjs +10 -9
- package/extension/core/selector.mjs +89 -88
- package/package.json +11 -8
- package/service/README.md +2 -0
- package/service/package.json +1 -1
- package/service/pool.mjs +58 -8
- package/service/server.mjs +9 -0
|
@@ -1,88 +1,89 @@
|
|
|
1
|
-
// 注入选择器(PLAN §3.5):打分排序 + token 预算 + #记忆名 显式调用。
|
|
2
|
-
// 打分:关键词重叠(内容 2-gram 命中 ×5)+ 会话名匹配 ×15 + 时间衰减 + 访问计数 + pinned。
|
|
3
|
-
// 预算:maxInjectionChars(默认 3000 字符),长提示词自动缩减。
|
|
4
|
-
// ⚠ 双份维护:inject.js 有本文件的经典脚本内联镜像(页面世界无法 import ESM)。
|
|
5
|
-
// 本文件是规范与单测基准;改逻辑时必须同步 inject.js 的 selectMemories/effectiveBudget/parseMemoryCommand。
|
|
6
|
-
|
|
7
|
-
export const SCOPE_PREFIX = '[dsh-ecolink 记忆]'
|
|
8
|
-
|
|
9
|
-
// 中文友好的大块匹配:内容 2-gram 与提示词 2-gram 的重叠命中数
|
|
10
|
-
export function bigrams(s) {
|
|
11
|
-
const t = String(s ?? '')
|
|
12
|
-
if (t.length < 2) return new Set()
|
|
13
|
-
const set = new Set()
|
|
14
|
-
for (let i = 0; i < t.length - 1; i++) set.add(t.slice(i, i + 2))
|
|
15
|
-
return set
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function keywordHits(content, promptBigrams, promptLower) {
|
|
19
|
-
const c = String(content ?? '')
|
|
20
|
-
const cb = bigrams(c)
|
|
21
|
-
let hits = 0
|
|
22
|
-
for (const g of cb) if (promptBigrams.has(g)) hits += 1
|
|
23
|
-
// 整词附加分(英文/数字词)
|
|
24
|
-
const words = promptLower.split(/[^\w一-龥]+/).filter((w) => w.length >= 2)
|
|
25
|
-
const cl = c.toLowerCase()
|
|
26
|
-
for (const w of words) if (cl.includes(w)) hits += 2
|
|
27
|
-
return hits
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function scoreItem(item, prompt, now = Date.now()) {
|
|
31
|
-
const promptLower = String(prompt ?? '').toLowerCase()
|
|
32
|
-
const pb = bigrams(promptLower)
|
|
33
|
-
const content = item?.content ?? ''
|
|
34
|
-
let score = Math.min(20, keywordHits(content, pb, promptLower) * 5)
|
|
35
|
-
// 会话名匹配(identity 即"名字",权重 ×15)
|
|
36
|
-
const identity = item?.identity
|
|
37
|
-
if (identity && promptLower.includes(String(identity).toLowerCase())) score += 15
|
|
38
|
-
// 时间衰减:越新越相关(半衰 ~7 天)
|
|
39
|
-
const ts = new Date(item?.timestamp).getTime()
|
|
40
|
-
if (Number.isFinite(ts)) {
|
|
41
|
-
const ageDays = Math.max(0, (now - ts) / 86400e3)
|
|
42
|
-
score += 10 / (1 + ageDays / 7)
|
|
43
|
-
}
|
|
44
|
-
// 访问过的记忆加权(选择器自举:用过的更可能再用)
|
|
45
|
-
if (item?.last_accessed) score += 5
|
|
46
|
-
if (item?.pinned) score += 10
|
|
47
|
-
return score
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 在预算内精选:按分数排序取前 N,单条截断到 500 字符
|
|
51
|
-
export function selectMemories(items, prompt, budgetChars = 3000, now = Date.now()) {
|
|
52
|
-
const scored = items
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (len <=
|
|
74
|
-
return Math.floor(maxInjectionChars * 0.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
// 注入选择器(PLAN §3.5):打分排序 + token 预算 + #记忆名 显式调用。
|
|
2
|
+
// 打分:关键词重叠(内容 2-gram 命中 ×5)+ 会话名匹配 ×15 + 时间衰减 + 访问计数 + pinned。
|
|
3
|
+
// 预算:maxInjectionChars(默认 3000 字符),长提示词自动缩减。
|
|
4
|
+
// ⚠ 双份维护:inject.js 有本文件的经典脚本内联镜像(页面世界无法 import ESM)。
|
|
5
|
+
// 本文件是规范与单测基准;改逻辑时必须同步 inject.js 的 selectMemories/effectiveBudget/parseMemoryCommand。
|
|
6
|
+
|
|
7
|
+
export const SCOPE_PREFIX = '[dsh-ecolink 记忆]'
|
|
8
|
+
|
|
9
|
+
// 中文友好的大块匹配:内容 2-gram 与提示词 2-gram 的重叠命中数
|
|
10
|
+
export function bigrams(s) {
|
|
11
|
+
const t = String(s ?? '')
|
|
12
|
+
if (t.length < 2) return new Set()
|
|
13
|
+
const set = new Set()
|
|
14
|
+
for (let i = 0; i < t.length - 1; i++) set.add(t.slice(i, i + 2))
|
|
15
|
+
return set
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function keywordHits(content, promptBigrams, promptLower) {
|
|
19
|
+
const c = String(content ?? '')
|
|
20
|
+
const cb = bigrams(c)
|
|
21
|
+
let hits = 0
|
|
22
|
+
for (const g of cb) if (promptBigrams.has(g)) hits += 1
|
|
23
|
+
// 整词附加分(英文/数字词)
|
|
24
|
+
const words = promptLower.split(/[^\w一-龥]+/).filter((w) => w.length >= 2)
|
|
25
|
+
const cl = c.toLowerCase()
|
|
26
|
+
for (const w of words) if (cl.includes(w)) hits += 2
|
|
27
|
+
return hits
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function scoreItem(item, prompt, now = Date.now()) {
|
|
31
|
+
const promptLower = String(prompt ?? '').toLowerCase()
|
|
32
|
+
const pb = bigrams(promptLower)
|
|
33
|
+
const content = item?.content ?? ''
|
|
34
|
+
let score = Math.min(20, keywordHits(content, pb, promptLower) * 5)
|
|
35
|
+
// 会话名匹配(identity 即"名字",权重 ×15)
|
|
36
|
+
const identity = item?.identity
|
|
37
|
+
if (identity && promptLower.includes(String(identity).toLowerCase())) score += 15
|
|
38
|
+
// 时间衰减:越新越相关(半衰 ~7 天)
|
|
39
|
+
const ts = new Date(item?.timestamp).getTime()
|
|
40
|
+
if (Number.isFinite(ts)) {
|
|
41
|
+
const ageDays = Math.max(0, (now - ts) / 86400e3)
|
|
42
|
+
score += 10 / (1 + ageDays / 7)
|
|
43
|
+
}
|
|
44
|
+
// 访问过的记忆加权(选择器自举:用过的更可能再用)
|
|
45
|
+
if (item?.last_accessed) score += 5
|
|
46
|
+
if (item?.pinned) score += 10
|
|
47
|
+
return score
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 在预算内精选:按分数排序取前 N,单条截断到 500 字符
|
|
51
|
+
export function selectMemories(items, prompt, budgetChars = 3000, now = Date.now()) {
|
|
52
|
+
const scored = items
|
|
53
|
+
.filter(item => item?.kind !== 'artifact')
|
|
54
|
+
.map((item) => ({ item, score: scoreItem(item, prompt, now) }))
|
|
55
|
+
.sort((a, b) => b.score - a.score || (b.item?.timestamp ?? '').localeCompare(a.item?.timestamp ?? ''))
|
|
56
|
+
const chosen = []
|
|
57
|
+
let used = 0
|
|
58
|
+
for (const { item } of scored) {
|
|
59
|
+
const content = String(item?.content ?? '').trim()
|
|
60
|
+
if (!content) continue
|
|
61
|
+
const capped = content.length > 500 ? content.slice(0, 500) + '…' : content
|
|
62
|
+
const cost = capped.length + 24 // 条目头部格式开销
|
|
63
|
+
if (used + cost > budgetChars) break
|
|
64
|
+
chosen.push({ ...item, content: capped })
|
|
65
|
+
used += cost
|
|
66
|
+
}
|
|
67
|
+
return { memories: chosen, chars: used }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 长提示词自动缩减预算:提示词本身越长,留给记忆的空间越小
|
|
71
|
+
export function effectiveBudget(prompt, maxInjectionChars = 3000) {
|
|
72
|
+
const len = String(prompt ?? '').length
|
|
73
|
+
if (len <= 5000) return maxInjectionChars
|
|
74
|
+
if (len <= 12000) return Math.floor(maxInjectionChars * 0.5)
|
|
75
|
+
return Math.floor(maxInjectionChars * 0.25)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// #记忆名 显式调用(PLAN §3.5,WebTool parseMemoryCommand 模式):
|
|
79
|
+
// 提示词里的 "#<会话名>" 命中会话池 identity → 只注入该会话的记忆,返回会话 id
|
|
80
|
+
export function parseMemoryCommand(prompt, sessionPools) {
|
|
81
|
+
const m = /#([^\s#,,。.!??、#]{1,40})/.exec(String(prompt ?? ''))
|
|
82
|
+
if (!m) return null
|
|
83
|
+
const name = m[1].trim()
|
|
84
|
+
if (!name) return null
|
|
85
|
+
for (const [sid, pool] of Object.entries(sessionPools ?? {})) {
|
|
86
|
+
if (pool?.identity && String(pool.identity).toLowerCase() === name.toLowerCase()) return sid
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoria-lind/dsh-ecolink-adapter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "DSH 跨端记忆:指定全文读取、侧栏浏览与注入、通用/非通用分类",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "adapter/src/index.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./adapter/src/index.js"
|
|
9
|
+
".": "./adapter/src/index.js",
|
|
10
|
+
"./client": "./adapter/src/client.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"adapter/src",
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"!service/config.json.bak-*",
|
|
20
21
|
"!service/*.log",
|
|
21
22
|
"!service/test.mjs",
|
|
23
|
+
"!service/*.test.mjs",
|
|
22
24
|
"!service/fix-pool.mjs"
|
|
23
25
|
],
|
|
24
26
|
"engines": {
|
|
@@ -33,13 +35,14 @@
|
|
|
33
35
|
"patch": "./adapter/cordis.patch.yml"
|
|
34
36
|
},
|
|
35
37
|
"compatibility": {
|
|
36
|
-
"dsh": "
|
|
38
|
+
"dsh": "0.2.1-rc.2",
|
|
37
39
|
"dshReleases": {
|
|
38
|
-
"0.1
|
|
39
|
-
"0.1.5-rc.2": "compatible",
|
|
40
|
-
"0.1.7-rc.1": "compatible",
|
|
41
|
-
"0.2.0-rc.2": "compatible"
|
|
40
|
+
"0.2.1-rc.2": "compatible"
|
|
42
41
|
}
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"inject": [],
|
|
45
|
+
"platform": "web"
|
|
43
46
|
}
|
|
44
47
|
},
|
|
45
48
|
"keywords": [
|
package/service/README.md
CHANGED
package/service/package.json
CHANGED
package/service/pool.mjs
CHANGED
|
@@ -39,6 +39,11 @@ export function renderTimestamp(isoTs, precision = null) {
|
|
|
39
39
|
return isoTs.slice(0, 16) // minute
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Same wire fingerprint as extension/core/entry.mjs (covered by contract tests).
|
|
43
|
+
export function entryVersion(item) {
|
|
44
|
+
return createHash('sha256').update(JSON.stringify([item?.id ?? null,item?.key ?? null,item?.content ?? '',
|
|
45
|
+
item?.injectionPolicy === 'universal' ? 'universal' : 'manual',item?.kind ?? 'fact',item?.revision ?? null])).digest('hex')
|
|
46
|
+
}
|
|
42
47
|
const EMPTY_POOL = { version: 1, shared_pool: [], session_pools: {} }
|
|
43
48
|
|
|
44
49
|
// ---- 快照与 diff(E3:变更角标/同步的基础;此前服务侧完全没有 hash/快照能力) ----
|
|
@@ -164,7 +169,7 @@ export function createPool(options = {}) {
|
|
|
164
169
|
|
|
165
170
|
// 串行执行变更(所有写操作经此排队,防并发交错)
|
|
166
171
|
function mutate(fn) {
|
|
167
|
-
const run = queue.then(() => { load(); const r = fn(); save(); return r })
|
|
172
|
+
const run = queue.then(() => { load(); const before = structuredClone(state); try { const r = fn(); save(); return r } catch (error) { state = before; throw error } })
|
|
168
173
|
queue = run.catch(() => {})
|
|
169
174
|
return run
|
|
170
175
|
}
|
|
@@ -175,7 +180,7 @@ export function createPool(options = {}) {
|
|
|
175
180
|
const cutoff = now - retentionDays * 86400e3
|
|
176
181
|
const archived = []
|
|
177
182
|
const keep = (item) => {
|
|
178
|
-
if (item.pinned) return true
|
|
183
|
+
if (item.pinned || item.kind === 'artifact' || item.injectionPolicy === 'universal') return true
|
|
179
184
|
const ts = new Date(item.timestamp).getTime()
|
|
180
185
|
const acc = item.last_accessed ? new Date(item.last_accessed).getTime() : 0
|
|
181
186
|
// <= 而非 <:条目时间戳可能与 cutoff 同毫秒(实测抖动),同刻即视为到期
|
|
@@ -213,6 +218,7 @@ export function createPool(options = {}) {
|
|
|
213
218
|
importance,
|
|
214
219
|
pinned: !!pinned,
|
|
215
220
|
last_accessed: null,
|
|
221
|
+
injectionPolicy: 'manual',
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
224
|
|
|
@@ -246,7 +252,7 @@ export function createPool(options = {}) {
|
|
|
246
252
|
if (isBlocked(m.content)) { blocked += 1; continue }
|
|
247
253
|
if (m.action === 'replace' && typeof m.id === 'string') {
|
|
248
254
|
const target = findItem(m.id)
|
|
249
|
-
if (target) {
|
|
255
|
+
if (target && target.kind !== 'artifact') {
|
|
250
256
|
target.content = m.content.trim()
|
|
251
257
|
target.timestamp = new Date().toISOString()
|
|
252
258
|
target.source = m.source ?? target.source
|
|
@@ -259,7 +265,7 @@ export function createPool(options = {}) {
|
|
|
259
265
|
const key = typeof m.key === 'string' && m.key.trim().length > 0 ? m.key.trim() : null
|
|
260
266
|
if (key) {
|
|
261
267
|
const poolArr = sessionId ? (state.session_pools[sessionId]?.memories ?? []) : state.shared_pool
|
|
262
|
-
const existing = poolArr.find((it) => it.key === key)
|
|
268
|
+
const existing = poolArr.find((it) => it.key === key && it.kind !== 'artifact')
|
|
263
269
|
if (existing) {
|
|
264
270
|
// DSM 同款:同 key 同值跳过写入(防时间戳无意义翻新与重复计数)
|
|
265
271
|
if (existing.content === m.content.trim()) continue
|
|
@@ -279,7 +285,7 @@ export function createPool(options = {}) {
|
|
|
279
285
|
{
|
|
280
286
|
const trimmed = m.content.trim()
|
|
281
287
|
const allPools = [...state.shared_pool, ...Object.values(state.session_pools ?? {}).flatMap((sp) => sp.memories ?? [])]
|
|
282
|
-
if (allPools.some((it) => it.content === trimmed)) { deduped += 1; continue }
|
|
288
|
+
if (allPools.some((it) => it.kind !== 'artifact' && it.content === trimmed)) { deduped += 1; continue }
|
|
283
289
|
}
|
|
284
290
|
// 无 key 条目按内容去重:相同内容已存在则跳过
|
|
285
291
|
// (模型回显示例会同一内容反复吐,池层是最后一道闸)
|
|
@@ -309,6 +315,49 @@ export function createPool(options = {}) {
|
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
return {
|
|
318
|
+
|
|
319
|
+
// Explicit metadata mutation is independent from text upsert/deduplication.
|
|
320
|
+
metadata({id,poolId,version,injectionPolicy}={}) {
|
|
321
|
+
return mutate(()=>{
|
|
322
|
+
const arr=poolId==='shared'?state.shared_pool:state.session_pools[poolId]?.memories
|
|
323
|
+
const found=(arr??[]).filter(it=>it.id===id)
|
|
324
|
+
if(found.length!==1) return {ok:false,error:'条目不存在或不唯一'}
|
|
325
|
+
const item=found[0]
|
|
326
|
+
if(!version || entryVersion(item)!==version) return {ok:false,conflict:true,error:'条目已变化,请刷新后重试'}
|
|
327
|
+
if(!['manual','universal'].includes(injectionPolicy)) return {ok:false,error:'类型无效'}
|
|
328
|
+
item.injectionPolicy=injectionPolicy
|
|
329
|
+
return {ok:true,id,version:entryVersion(item),injectionPolicy}
|
|
330
|
+
})
|
|
331
|
+
},
|
|
332
|
+
// Explicit artifact save bypasses short-fact trimming/harvest/blacklist/compression.
|
|
333
|
+
// requestId is stable across retry; same name requires the exact previewed version.
|
|
334
|
+
saveArtifact({name,content,requestId,expectedVersion}={}) {
|
|
335
|
+
return mutate(()=>{
|
|
336
|
+
if(typeof name!=='string'||!name.trim()||name.trim().length>200) return {ok:false,error:'名称须为1–200字符'}
|
|
337
|
+
if(typeof content!=='string'||!content.trim()||Buffer.byteLength(content,'utf8')>1500000) return {ok:false,error:'正文为空或超过1.5MB'}
|
|
338
|
+
if(typeof requestId!=='string'||!/^[a-zA-Z0-9_-]{8,100}$/.test(requestId)) return {ok:false,error:'缺少有效保存请求ID'}
|
|
339
|
+
const title=name.trim(), existing=state.shared_pool.find(it=>it.kind==='artifact'&&it.key===title)
|
|
340
|
+
const prior=existing&&[existing,...(existing.versions??[])].find(v=>v.requestId===requestId)
|
|
341
|
+
if(prior) {
|
|
342
|
+
if(prior.content!==content) return {ok:false,error:'保存请求ID已用于不同正文,请新建保存请求'}
|
|
343
|
+
return {ok:true,id:existing.id,revision:prior.revision,version:entryVersion(existing),duplicate:true}
|
|
344
|
+
}
|
|
345
|
+
if(existing&&expectedVersion!==entryVersion(existing))
|
|
346
|
+
return {ok:false,conflict:true,error:'已有同名完整成果;确认后保存新版本并保留旧版',id:existing.id,version:entryVersion(existing),revision:existing.revision}
|
|
347
|
+
const now=new Date().toISOString()
|
|
348
|
+
let item=existing
|
|
349
|
+
if(item) {
|
|
350
|
+
item.versions??=[]
|
|
351
|
+
item.versions.push({content:item.content,timestamp:item.timestamp,revision:item.revision,requestId:item.requestId})
|
|
352
|
+
item.content=content;item.timestamp=now;item.revision=(item.revision??1)+1;item.requestId=requestId
|
|
353
|
+
}else{
|
|
354
|
+
item={id:randomUUID(),key:title,content,timestamp:now,source:'web-artifact',kind:'artifact',revision:1,requestId,versions:[],
|
|
355
|
+
injectionPolicy:'manual',importance:'called',pinned:true,last_accessed:null}
|
|
356
|
+
state.shared_pool.push(item)
|
|
357
|
+
}
|
|
358
|
+
return {ok:true,id:item.id,revision:item.revision,version:entryVersion(item),retainedVersions:item.versions.length}
|
|
359
|
+
})
|
|
360
|
+
},
|
|
312
361
|
poolFile,
|
|
313
362
|
archiveFile,
|
|
314
363
|
dir,
|
|
@@ -483,7 +532,7 @@ export function createPool(options = {}) {
|
|
|
483
532
|
const push = (it, sessionId = null, identity = null) => {
|
|
484
533
|
const ts = new Date(it.timestamp).getTime()
|
|
485
534
|
// <= 与归档同语义:同毫秒即视为到期(days=0 时全部命中,测试用)
|
|
486
|
-
if (Number.isFinite(ts) && ts <= cutoff) out.push({ ...it, session_id: sessionId, identity })
|
|
535
|
+
if (it.kind !== 'artifact' && it.injectionPolicy !== 'universal' && Number.isFinite(ts) && ts <= cutoff) out.push({ ...it, session_id: sessionId, identity })
|
|
487
536
|
}
|
|
488
537
|
for (const it of state.shared_pool) push(it)
|
|
489
538
|
for (const [sid, sp] of Object.entries(state.session_pools)) {
|
|
@@ -518,12 +567,12 @@ export function createPool(options = {}) {
|
|
|
518
567
|
const pull = (arr, sessionId = null) => {
|
|
519
568
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
520
569
|
const ts = new Date(arr[i].timestamp).getTime()
|
|
521
|
-
if (Number.isFinite(ts) && ts <= cutoff) staged.push({ ...arr[i], session_id: sessionId })
|
|
570
|
+
if (arr[i].kind !== 'artifact' && arr[i].injectionPolicy !== 'universal' && Number.isFinite(ts) && ts <= cutoff) staged.push({ ...arr[i], session_id: sessionId })
|
|
522
571
|
}
|
|
523
572
|
}
|
|
524
573
|
const keep = (arr, sessionId = null) => arr.filter((it) => {
|
|
525
574
|
const ts = new Date(it.timestamp).getTime()
|
|
526
|
-
return !(Number.isFinite(ts) && ts <= cutoff)
|
|
575
|
+
return it.kind === 'artifact' || it.injectionPolicy === 'universal' || !(Number.isFinite(ts) && ts <= cutoff)
|
|
527
576
|
})
|
|
528
577
|
pull(state.shared_pool)
|
|
529
578
|
state.shared_pool = keep(state.shared_pool)
|
|
@@ -560,6 +609,7 @@ export function createPool(options = {}) {
|
|
|
560
609
|
// 全池内容去重:同内容保留首条(暂存条目先放回,新标签若内容一致则被吸收)
|
|
561
610
|
const seen = new Set()
|
|
562
611
|
const dedupe = (arr) => arr.filter((it) => {
|
|
612
|
+
if (it.kind === 'artifact' || it.injectionPolicy === 'universal') return true
|
|
563
613
|
const c = String(it.content ?? '').trim()
|
|
564
614
|
if (!c || seen.has(c)) return false
|
|
565
615
|
seen.add(c)
|
package/service/server.mjs
CHANGED
|
@@ -169,6 +169,15 @@ const server = createServer(async (req, res) => {
|
|
|
169
169
|
|
|
170
170
|
try {
|
|
171
171
|
// ---- POST ----
|
|
172
|
+
if (path === '/memory/metadata' && method === 'POST') {
|
|
173
|
+
const result = await pool.metadata(await readBody(req))
|
|
174
|
+
return send(res, result.ok ? 200 : (result.conflict ? 409 : 400), result)
|
|
175
|
+
}
|
|
176
|
+
if (path === '/memory/artifact' && method === 'POST') {
|
|
177
|
+
const result = await pool.saveArtifact(await readBody(req))
|
|
178
|
+
return send(res, result.ok ? 200 : (result.conflict ? 409 : 400), result)
|
|
179
|
+
}
|
|
180
|
+
|
|
172
181
|
if (path === '/memory/sync' && method === 'POST') {
|
|
173
182
|
const body = await readBody(req)
|
|
174
183
|
const memories = Array.isArray(body.memories) ? body.memories : []
|