@a9i5k4/dsh-auto-memory 2.1.4 → 2.1.6
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/lib/client.js +3791 -3713
- package/lib/index.js +182 -8
- package/lib/python-setup.js +229 -0
- package/package.json +6 -3
- package/python/m7_activation_features_v2.py +392 -0
- package/python/m7_embedding_v1.py +381 -0
- package/python/policies/activation_policy_v2.json +88 -0
- package/python/policies/decision-record-activation-v2-delta-exp-override-20260824.json +21 -0
- package/python/policies/decision-record-reasoning-kind-admission-20260826.json +20 -0
- package/python/policies/decision-record-stale-gate-per-candidate-20260825.json +33 -0
- package/python/policies/recall_intent_lr_v1.json +1 -0
- package/python/verify_policy_artifact.py +122 -0
- package/python/worker_semantic_v1.py +1344 -0
- package/python/worker_v1.py +623 -0
package/lib/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { createActivationHost } from './activation-host.js'
|
|
|
35
35
|
import { createJsSemanticEnginePre, createSemanticDownloaderPre, fuseD6Pre, probeJsSemanticAssets, deepScanPeerTransformers, E5_SMALL_Q8_MANIFEST_V1 } from './semantic-js.js'
|
|
36
36
|
import { loadAndVerifyPolicy, decideActivationV2, lexicalContainment } from './semantic-decide.js'
|
|
37
37
|
import { createPythonSidecarClientPre, defaultWorkerScriptPathPre } from './python-sidecar-client.js'
|
|
38
|
+
import { createPythonSetupPre } from './python-setup.js'
|
|
38
39
|
import { createEpisodicStorePre } from './episodic-store.js'
|
|
39
40
|
import { createFactStorePre } from './fact-store.js'
|
|
40
41
|
import { createProcedureStorePre } from './procedure-store.js'
|
|
@@ -93,6 +94,12 @@ export const API = {
|
|
|
93
94
|
notices: '/api/dsh-auto-memory/notices',
|
|
94
95
|
'activation-inbox': '/api/dsh-auto-memory/activation-inbox',
|
|
95
96
|
'semantic-status': '/api/dsh-auto-memory/semantic-status',
|
|
97
|
+
'py-setup-detect': '/api/dsh-auto-memory/python-setup/detect',
|
|
98
|
+
'py-setup-venv': '/api/dsh-auto-memory/python-setup/venv',
|
|
99
|
+
'py-setup-deps': '/api/dsh-auto-memory/python-setup/deps',
|
|
100
|
+
'py-setup-model': '/api/dsh-auto-memory/python-setup/model',
|
|
101
|
+
'py-setup-cancel': '/api/dsh-auto-memory/python-setup/cancel',
|
|
102
|
+
'py-setup-status': '/api/dsh-auto-memory/python-setup/status',
|
|
96
103
|
'semantic-deep-detect': '/api/dsh-auto-memory/semantic-deep-detect',
|
|
97
104
|
'handoff-state': '/api/dsh-auto-memory/handoff-state',
|
|
98
105
|
'semantic-download': '/api/dsh-auto-memory/semantic-download',
|
|
@@ -1228,7 +1235,7 @@ class MemoryEngine {
|
|
|
1228
1235
|
if (!agent || typeof agent !== 'object') return 0
|
|
1229
1236
|
const runtime = this.resolveObserverRuntime(agent)
|
|
1230
1237
|
if (!runtime || runtime.disposed) return 0
|
|
1231
|
-
const events = agent
|
|
1238
|
+
const events = sessionEventsOf(agent && agent.session)
|
|
1232
1239
|
const capped = events.length > SEED_REPLAY_MAX_EVENTS ? events.slice(-SEED_REPLAY_MAX_EVENTS) : events
|
|
1233
1240
|
this._observerStats.seedTruncatedEvents += events.length - capped.length
|
|
1234
1241
|
let seeded = 0
|
|
@@ -1578,7 +1585,7 @@ class MemoryEngine {
|
|
|
1578
1585
|
// compaction 事件感知:harness 刚发生 compaction/summary|prune → 无论水位,立即补写交接材料
|
|
1579
1586
|
let compacted = false
|
|
1580
1587
|
try {
|
|
1581
|
-
const events = (agent
|
|
1588
|
+
const events = sessionEventsOf(agent && agent.session)
|
|
1582
1589
|
for (let i = Math.max(0, events.length - 64); i < events.length; i++) {
|
|
1583
1590
|
const t = events[i] && events[i].type
|
|
1584
1591
|
if (t === 'compaction/summary' || t === 'compaction/prune') {
|
|
@@ -2256,6 +2263,78 @@ class MemoryEngine {
|
|
|
2256
2263
|
return true
|
|
2257
2264
|
}
|
|
2258
2265
|
|
|
2266
|
+
/**
|
|
2267
|
+
* F1(M6-CONTRACT 外补丁,2026-08-23):anchor 开启时的记录级压缩。
|
|
2268
|
+
* 按 sidecar 记录序整条保留/移除——今天(标题日期=插件今天)记录无条件保留,其余按字节配额
|
|
2269
|
+
* 从最新往回保留(layer 上限 user 4000/note 3000);被移除记录整条原文写入归档文件
|
|
2270
|
+
* (writeFullRaw,不经 anchor 事务,归档非检索语料)。重组经 store.replace 原子执行:
|
|
2271
|
+
* 候选预检(clean)→被移除 id 自动删除→保留 id/版本不变。永不字符切片。
|
|
2272
|
+
* @returns {Promise<boolean|'not-anchored'>} true=已腾位 false=无法腾位 'not-anchored'=无 sidecar 走旧路径
|
|
2273
|
+
*/
|
|
2274
|
+
async compactAnchoredLayer(store, filePath, layer, today, paths) {
|
|
2275
|
+
const cur = await this.readTextSafe(filePath)
|
|
2276
|
+
if (!cur || !cur.trim()) return false
|
|
2277
|
+
let parsed
|
|
2278
|
+
try { parsed = parseAnchors(Buffer.from(cur, 'utf8')) } catch (_) { return 'not-anchored' }
|
|
2279
|
+
if (!parsed || parsed.status !== 'clean') return false // 脏文件 fail closed:不压缩、不切片
|
|
2280
|
+
const anchored = parsed.records.filter((r) => r.kind === 'anchored')
|
|
2281
|
+
if (!anchored.length) return 'not-anchored'
|
|
2282
|
+
const limit = layer === 'user' ? 4000 : 3000
|
|
2283
|
+
const isTodayHeading = (heading) => /^\d{4}-\d{2}-\d{2}/.test(String(heading || '')) && String(heading).slice(0, 10) === today
|
|
2284
|
+
const buf = Buffer.from(cur, 'utf8')
|
|
2285
|
+
// 配额从尾回溯(F1):新的留;今天记录无条件保留
|
|
2286
|
+
const keep = new Array(anchored.length).fill(false)
|
|
2287
|
+
let keptBytes = 0
|
|
2288
|
+
for (let i = anchored.length - 1; i >= 0; i--) {
|
|
2289
|
+
const rec = anchored[i]
|
|
2290
|
+
const spanBytes = rec.byteEnd - rec.markerByteStart
|
|
2291
|
+
if (isTodayHeading(rec.heading) || keptBytes + spanBytes <= limit) { keep[i] = true; keptBytes += spanBytes }
|
|
2292
|
+
}
|
|
2293
|
+
const removedIdx = []
|
|
2294
|
+
anchored.forEach((rec, i) => { if (!keep[i]) removedIdx.push(i) })
|
|
2295
|
+
if (!removedIdx.length) return false // 无可移除 → 无法腾位(预算照常拒绝)
|
|
2296
|
+
// 有序块重组:legacy 块与 preamble/tail 原样保留,gap 字节逐段拷贝,仅丢弃被移除记录的 [marker..contentEnd]
|
|
2297
|
+
const blocks = []
|
|
2298
|
+
for (const r of parsed.records) {
|
|
2299
|
+
if (r.kind === 'anchored') blocks.push({ start: r.markerByteStart, end: r.byteEnd, keep: keep[anchored.indexOf(r)] })
|
|
2300
|
+
else blocks.push({ start: r.byteStart, end: r.byteEnd, keep: true })
|
|
2301
|
+
}
|
|
2302
|
+
blocks.sort((a, b) => a.start - b.start)
|
|
2303
|
+
const parts = []
|
|
2304
|
+
let cursorB = 0
|
|
2305
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2306
|
+
const b = blocks[i]
|
|
2307
|
+
if (b.start > cursorB) parts.push(buf.slice(cursorB, b.start).toString('utf8'))
|
|
2308
|
+
if (b.keep) parts.push(buf.slice(b.start, b.end).toString('utf8'))
|
|
2309
|
+
cursorB = b.end
|
|
2310
|
+
}
|
|
2311
|
+
if (cursorB < buf.length) parts.push(buf.slice(cursorB).toString('utf8'))
|
|
2312
|
+
const newText = parts.join('')
|
|
2313
|
+
// 归档:整条原文(writeFullRaw 绕开 anchor 事务;归档文件从不参与解析/索引)
|
|
2314
|
+
const archiveFile = layer === 'user'
|
|
2315
|
+
? path.join(dshHome(), 'memory', 'archived-user.md')
|
|
2316
|
+
: path.join(paths.projectDir, 'archive', 'notes-archived.md')
|
|
2317
|
+
const archiveText = removedIdx.map((i2) => {
|
|
2318
|
+
const r = anchored[i2]
|
|
2319
|
+
return buf.slice(r.markerByteStart, r.byteEnd).toString('utf8')
|
|
2320
|
+
}).join('\n')
|
|
2321
|
+
const existingArchive = await this.readTextSafe(archiveFile)
|
|
2322
|
+
await this.writeFullRaw(archiveFile, (existingArchive ? existingArchive.replace(/\s+$/, '') + '\n' : '') + archiveText + '\n')
|
|
2323
|
+
const rr = await store.replace(filePath, newText)
|
|
2324
|
+
if (!rr.ok) { console.error('[compacted-anchored] replace failed: ' + rr.reason); return false }
|
|
2325
|
+
const freshText = await this.readTextSafe(filePath)
|
|
2326
|
+
if (layer === 'user') this.state.userText = freshText
|
|
2327
|
+
else this.state.notesText = freshText
|
|
2328
|
+
if (this._budgets) {
|
|
2329
|
+
const b = this._budgets.get('today')
|
|
2330
|
+
if (b && b.date === this.memToday()) b[layer] = 0
|
|
2331
|
+
}
|
|
2332
|
+
this._lastCompactAt = Date.now()
|
|
2333
|
+
this.state.loadedAt = Date.now()
|
|
2334
|
+
console.log('[compacted-anchored] ' + layer + ': kept=' + (anchored.length - removedIdx.length) + '/' + anchored.length + ' archived=' + removedIdx.length + ' bytes=' + cur.length + '->' + newText.length)
|
|
2335
|
+
return true
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2259
2338
|
// ---------- 注入渲染(同步,基于缓存) ----------
|
|
2260
2339
|
// 动态记忆 → ctx.systemPrompt.context()(user-role 快照):内容变化才追加新快照,内容不变不重复注入(dsh-agent-loop project() 去重),
|
|
2261
2340
|
// system prompt 不再包含动态内容 → 字节级稳定 → DeepSeek 前缀缓存全程命中(对比 section 方案:动态内容任何变化都从变化点起击穿整个前缀,含全部历史)
|
|
@@ -2291,6 +2370,17 @@ class MemoryEngine {
|
|
|
2291
2370
|
if (cfg.waterLevelAdvisory !== false && !this.isUnattendedNow() && (s.waterLevelRatio || 0) >= (Number(cfg.waterLevelThreshold) || 0.8) && Date.now() - (s.waterLevelAt || 0) < 600000) {
|
|
2292
2371
|
lines.push('\n' + L('snapshotWaterTitle') + '\n' + L('snapshotWaterBody', { pct: Math.round((s.waterLevelRatio || 0) * 100) }))
|
|
2293
2372
|
}
|
|
2373
|
+
// M-CM1 交接白板:PLAN.md 全貌+最新交接账本,动态快照首位(动态层内容,不碰静态字节;敏感段清洗+行边界硬预算截断)
|
|
2374
|
+
if (cfg.handoffEnabled !== false && (s.planText || s.latestHandoffText)) {
|
|
2375
|
+
const planPart = truncateLinesBounded(stripSensitiveSections(sanitizeForInjection(s.planText || '')), Math.max(Number(cfg.handoffPlanChars) || 1200, 200))
|
|
2376
|
+
if (planPart) lines.push('\n' + L('snapshotPlanTitle') + '\n' + planPart)
|
|
2377
|
+
const ledgerPart = truncateLinesBounded(stripSensitiveSections(sanitizeForInjection(s.latestHandoffText || '')), Math.max(Number(cfg.handoffLedgerChars) || 800, 200))
|
|
2378
|
+
if (ledgerPart) lines.push('\n' + L('snapshotHandoffTitle') + '\n' + ledgerPart)
|
|
2379
|
+
}
|
|
2380
|
+
// M-CM4/M-CM5 水位建议:越阈注入交接+派子代理建议(advisory;10 分钟新鲜度;无人值守静默)
|
|
2381
|
+
if (cfg.waterLevelAdvisory !== false && !this.isUnattendedNow() && (s.waterLevelRatio || 0) >= (Number(cfg.waterLevelThreshold) || 0.8) && Date.now() - (s.waterLevelAt || 0) < 600000) {
|
|
2382
|
+
lines.push('\n' + L('snapshotWaterTitle') + '\n' + L('snapshotWaterBody', { pct: Math.round((s.waterLevelRatio || 0) * 100) }))
|
|
2383
|
+
}
|
|
2294
2384
|
let used = 0
|
|
2295
2385
|
const part = (title, text, max) => {
|
|
2296
2386
|
if (!text) return
|
|
@@ -3208,7 +3298,7 @@ class MemoryEngine {
|
|
|
3208
3298
|
const a = agentSvc.get(best.id)
|
|
3209
3299
|
if (a && a.session && a.session.header && a.session.header.parentSession === undefined) {
|
|
3210
3300
|
this._lastAgent = a
|
|
3211
|
-
diag('restoreLastAgent: recovered agent id=' + a.id + ' session=' + a.session.id + ' logEvents=' + (
|
|
3301
|
+
diag('restoreLastAgent: recovered agent id=' + a.id + ' session=' + a.session.id + ' logEvents=' + sessionEventsOf(a.session).length)
|
|
3212
3302
|
} else {
|
|
3213
3303
|
diag('restoreLastAgent: candidate rejected (id=' + (a && a.id) + ' hasSession=' + !!(a && a.session) + ' parent=' + (a && a.session && a.session.header && a.session.header.parentSession) + ')')
|
|
3214
3304
|
}
|
|
@@ -3413,7 +3503,7 @@ class MemoryEngine {
|
|
|
3413
3503
|
runtime.lastTurn = turn
|
|
3414
3504
|
// 取本轮最后一条 user + 最后一条 assistant(模型可见消息序列)
|
|
3415
3505
|
const messages = extractSessionMessages(agent)
|
|
3416
|
-
if (messages.length < 2) { why('messages<2 got=' + messages.length + ' seqs=' + ((agent.session.surface && agent.session.surface.nodes && (Array.isArray(agent.session.surface.nodes) ? agent.session.surface.nodes.length : 'set')) || 'none') + ' events=' + ((agent
|
|
3506
|
+
if (messages.length < 2) { why('messages<2 got=' + messages.length + ' seqs=' + ((agent.session.surface && agent.session.surface.nodes && (Array.isArray(agent.session.surface.nodes) ? agent.session.surface.nodes.length : 'set')) || 'none') + ' events=' + (sessionEventsOf(agent && agent.session).length || 'none')); return }
|
|
3417
3507
|
// 2026-08-30 intent 提纯(三层):①只认真人消息事件 'user/message'(tool/result 在协议里
|
|
3418
3508
|
// 也是 user 角色,正文常是 JSON/行号文本)②跳过 harness 合成的上下文注入消息
|
|
3419
3509
|
// ("Current runtime context." 开头 / 含 <memory_system>)③剥离注入快照块,取其后真人问题。
|
|
@@ -4442,13 +4532,33 @@ function textOfContent(content) {
|
|
|
4442
4532
|
walk(content, 0)
|
|
4443
4533
|
return out.join('')
|
|
4444
4534
|
}
|
|
4535
|
+
/**
|
|
4536
|
+
* 新旧两代 Session API 兼容的原始事件数组读取:
|
|
4537
|
+
* - 旧版 @deepseek-ai/dsh-session 在 Session 上暴露 .events 数组;
|
|
4538
|
+
* - 新版已移除 .events,事件读取走 snapshotEvents()(内部 eventsSnapshot 缓存,
|
|
4539
|
+
* 同一 log 追加前重复调用零重复开销;返回 frozen 数组,只读安全)。
|
|
4540
|
+
* 返回值恒为数组(找不到任何来源时为 []),不做截断——调用方各自决定窗口。
|
|
4541
|
+
*/
|
|
4542
|
+
function sessionEventsOf(session) {
|
|
4543
|
+
try {
|
|
4544
|
+
if (!session) return []
|
|
4545
|
+
if (Array.isArray(session.events)) return session.events
|
|
4546
|
+
if (typeof session.snapshotEvents === 'function') {
|
|
4547
|
+
const snap = session.snapshotEvents()
|
|
4548
|
+
if (Array.isArray(snap)) return snap
|
|
4549
|
+
}
|
|
4550
|
+
} catch (e) {}
|
|
4551
|
+
return []
|
|
4552
|
+
}
|
|
4445
4553
|
function extractSessionMessages(agent) {
|
|
4446
4554
|
try {
|
|
4447
4555
|
const session = agent && agent.session
|
|
4448
4556
|
if (!session) return []
|
|
4449
|
-
//
|
|
4557
|
+
// 只读事件数组(截断尾部),绝不访问 session.surface.nodes:
|
|
4450
4558
|
// 该访问会触发惰性投影,长会话(上万事件)时同步遍历阻塞主线程 → 窗口卡死/超时。
|
|
4451
|
-
|
|
4559
|
+
// 2026-09 兼容:新版 harness Session 已移除 .events 属性(原注释针对旧版 API),
|
|
4560
|
+
// 统一经 sessionEventsOf() 读取(新版走 snapshotEvents(),自带缓存,同样零投影访问)。
|
|
4561
|
+
const eventsRaw = sessionEventsOf(session)
|
|
4452
4562
|
const events = eventsRaw.length > 2000 ? eventsRaw.slice(-2000) : eventsRaw
|
|
4453
4563
|
const out = []
|
|
4454
4564
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -4873,6 +4983,15 @@ export function apply(ctx, config) {
|
|
|
4873
4983
|
try { engine._activationHost.initCapability(ctx) } catch (_) {}
|
|
4874
4984
|
// M7-0/M7-1:engine 级共享 SidecarClient(对象构造零副作用;进程仅在显式启用路径上 lazy spawn,
|
|
4875
4985
|
// 默认关闭=零 Python process、零协议 IO、零 semantic 目录)
|
|
4986
|
+
// M7.6 Python 一键向导(#16-#20 配套):detect/venv/deps/model 四步,落盘用户目录,不碰 npm 包目录
|
|
4987
|
+
engine._pythonSetup = createPythonSetupPre({
|
|
4988
|
+
dshHome: () => {
|
|
4989
|
+
const env = process.env.DSH_HOME
|
|
4990
|
+
if (env && env.trim()) return env.trim()
|
|
4991
|
+
try { return path.join(homedir(), '.dsh') } catch (_) { return homedir() }
|
|
4992
|
+
},
|
|
4993
|
+
diag: (m) => diag('python-setup: ' + m),
|
|
4994
|
+
})
|
|
4876
4995
|
engine._pythonSidecar = createPythonSidecarClientPre({
|
|
4877
4996
|
command: () => String(engine.config.pythonBackendExecutable || '').trim() || 'python',
|
|
4878
4997
|
scriptPath: () => String(engine.config.pythonBackendWorkerPath || '').trim() || defaultWorkerScriptPathPre(),
|
|
@@ -5307,6 +5426,59 @@ export function apply(ctx, config) {
|
|
|
5307
5426
|
|
|
5308
5427
|
// ---------- 路由 ----------
|
|
5309
5428
|
const routes = [
|
|
5429
|
+
{
|
|
5430
|
+
kind: 'exact',
|
|
5431
|
+
path: API['py-setup-status'],
|
|
5432
|
+
handler: async (req, res) => {
|
|
5433
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5434
|
+
writeJson(res, 200, engine._pythonSetup.status())
|
|
5435
|
+
},
|
|
5436
|
+
},
|
|
5437
|
+
{
|
|
5438
|
+
kind: 'exact',
|
|
5439
|
+
path: API['py-setup-detect'],
|
|
5440
|
+
handler: async (req, res) => {
|
|
5441
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5442
|
+
try { writeJson(res, 200, await engine._pythonSetup.detect()) } catch (e) { writeJson(res, 500, { error: String(e && e.message || e) }) }
|
|
5443
|
+
},
|
|
5444
|
+
},
|
|
5445
|
+
{
|
|
5446
|
+
kind: 'exact',
|
|
5447
|
+
path: API['py-setup-venv'],
|
|
5448
|
+
handler: async (req, res) => {
|
|
5449
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5450
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5451
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
5452
|
+
try { writeJson(res, 200, await engine._pythonSetup.ensureVenv(body && body.python)) } catch (e) { writeJson(res, 500, { error: String(e && e.message || e) }) }
|
|
5453
|
+
},
|
|
5454
|
+
},
|
|
5455
|
+
{
|
|
5456
|
+
kind: 'exact',
|
|
5457
|
+
path: API['py-setup-deps'],
|
|
5458
|
+
handler: async (req, res) => {
|
|
5459
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5460
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5461
|
+
try { writeJson(res, 200, await engine._pythonSetup.ensureDeps()) } catch (e) { writeJson(res, 500, { error: String(e && e.message || e) }) }
|
|
5462
|
+
},
|
|
5463
|
+
},
|
|
5464
|
+
{
|
|
5465
|
+
kind: 'exact',
|
|
5466
|
+
path: API['py-setup-model'],
|
|
5467
|
+
handler: async (req, res) => {
|
|
5468
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5469
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5470
|
+
try { writeJson(res, 200, await engine._pythonSetup.downloadModel()) } catch (e) { writeJson(res, 500, { error: String(e && e.message || e) }) }
|
|
5471
|
+
},
|
|
5472
|
+
},
|
|
5473
|
+
{
|
|
5474
|
+
kind: 'exact',
|
|
5475
|
+
path: API['py-setup-cancel'],
|
|
5476
|
+
handler: async (req, res) => {
|
|
5477
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5478
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5479
|
+
writeJson(res, 200, engine._pythonSetup.cancelDownload())
|
|
5480
|
+
},
|
|
5481
|
+
},
|
|
5310
5482
|
{
|
|
5311
5483
|
kind: 'exact',
|
|
5312
5484
|
path: API['semantic-status'],
|
|
@@ -5315,9 +5487,10 @@ export function apply(ctx, config) {
|
|
|
5315
5487
|
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5316
5488
|
try {
|
|
5317
5489
|
const probe = await engine.semanticAssetProbe()
|
|
5318
|
-
|
|
5490
|
+
// M7.6:模型归位用户目录(~/.dsh/python-engine/models/),包目录 bench 夹具仅作开发机兜底
|
|
5491
|
+
const pyCands = (engine._pythonSetup ? [engine._pythonSetup.modelPath()] : []).concat([
|
|
5319
5492
|
path.join(pluginRootDir(), 'python', 'bench', 'models-xenova-bge-m3-int8', 'onnx', 'model_int8.onnx'),
|
|
5320
|
-
]
|
|
5493
|
+
])
|
|
5321
5494
|
const fsMod = await import('node:fs')
|
|
5322
5495
|
const pyOnnx = pyCands.find((c) => fsMod.existsSync(c)) || pyCands[0]
|
|
5323
5496
|
// 发射开关读数(memory/semantic/embedding-config.json 的
|
|
@@ -6046,6 +6219,7 @@ export function apply(ctx, config) {
|
|
|
6046
6219
|
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
6047
6220
|
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
6048
6221
|
try {
|
|
6222
|
+
const body = await readJsonBody(req).catch(() => ({})) // #18: 缺此行致 body is not defined → greet 恒 500
|
|
6049
6223
|
const ws = body && body.ws ? body.ws : ''
|
|
6050
6224
|
await engine.refresh(ws ? { session: { header: { cwd: ws } } } : undefined)
|
|
6051
6225
|
writeJson(res, 200, await engine.greetToday())
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M7.6 Python 一键向导(host 半):#16-#20 配套——把 C3 进阶档从"开发机可达"变成"爱好者可达"。
|
|
3
|
+
*
|
|
4
|
+
* 四步链路(全部走本模块,UI 只管展示状态与点击):
|
|
5
|
+
* ①detect — 探测系统 Python(≥3.9)/既有 venv/模型本体;全部只读。
|
|
6
|
+
* ②venv — python -m venv <userDir>/python-engine/.venv(幂等:已存在直接跳过)。
|
|
7
|
+
* ③deps — venv 内 pip 安装运行依赖(fastembed/onnxruntime,清华镜像兜底)。
|
|
8
|
+
* ④model — BGE-M3 int8(~539MB)下载到 <userDir>/python-engine/models/,
|
|
9
|
+
* cn(hf-mirror)/intl(hf 官方)双通道+SHA256 校验,复用 JS 档下载器的状态机形态。
|
|
10
|
+
*
|
|
11
|
+
* 设计约束:
|
|
12
|
+
* - 一切落盘在用户目录(~/.dsh/python-engine/),npm 包目录升级会被覆盖,绝不放模型。
|
|
13
|
+
* - 长任务(venv/pip/下载)异步执行,进度写 state 供 semantic-status 轮询;失败可重试。
|
|
14
|
+
* - 零新依赖:下载用全局 fetch,解压用 onnx 单文件直下(无压缩包),venv 用 python -m venv。
|
|
15
|
+
*/
|
|
16
|
+
import { execFile } from 'node:child_process'
|
|
17
|
+
import { promisify } from 'node:util'
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync, statSync, createWriteStream } from 'node:fs'
|
|
22
|
+
import { readFile, writeFile, rm } from 'node:fs/promises'
|
|
23
|
+
|
|
24
|
+
const execFileP = promisify(execFile)
|
|
25
|
+
|
|
26
|
+
/** BGE-M3 int8 单文件(与 bench 夹具同源;hf-mirror 为主源,HF 官方为备源)。 */
|
|
27
|
+
const MODEL_SPEC = {
|
|
28
|
+
repo: 'Xenova/bge-m3-int8',
|
|
29
|
+
file: 'onnx/model_int8.onnx',
|
|
30
|
+
bytes: 568456694,
|
|
31
|
+
sha256: '', // 见 MODEL_SHA256:远端 LFS 校验不可靠时以 size 下限+可执行性兜底;sha256 由首版发布冻结后填入
|
|
32
|
+
mirrors: [
|
|
33
|
+
{ id: 'cn', url: (p) => 'https://hf-mirror.com/' + p },
|
|
34
|
+
{ id: 'intl', url: (p) => 'https://huggingface.co/' + p },
|
|
35
|
+
],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** venv 内 pip 依赖(语义引擎最小集;transformers 栈换成 fastembed=纯 onnxruntime,免 torch)。 */
|
|
39
|
+
const PIP_DEPS = ['fastembed', 'onnxruntime']
|
|
40
|
+
|
|
41
|
+
export function createPythonSetupPre(opts = {}) {
|
|
42
|
+
const dshHomeOf = typeof opts.dshHome === 'function' ? opts.dshHome : () => opts.dshHome || path.join(homedir(), '.dsh')
|
|
43
|
+
const diagOf = typeof opts.diag === 'function' ? opts.diag : () => {}
|
|
44
|
+
|
|
45
|
+
// 引擎根目录(用户目录,跨升级存活): ~/.dsh/python-engine/
|
|
46
|
+
const engineRoot = () => path.join(dshHomeOf(), 'python-engine')
|
|
47
|
+
const venvDir = () => path.join(engineRoot(), '.venv')
|
|
48
|
+
const venvPython = () => process.platform === 'win32' ? path.join(venvDir(), 'Scripts', 'python.exe') : path.join(venvDir(), 'bin', 'python')
|
|
49
|
+
const modelsDir = () => path.join(engineRoot(), 'models')
|
|
50
|
+
const modelPath = () => path.join(modelsDir(), 'model_int8.onnx')
|
|
51
|
+
|
|
52
|
+
// 进度状态(内存态,semantic-status 轮询消费;host 重启后 detect 重建)
|
|
53
|
+
const st = {
|
|
54
|
+
phase: 'idle', // idle|detecting|venv|deps|downloading|verifying|ready|error
|
|
55
|
+
error: '',
|
|
56
|
+
pythons: [], // [{label,path,status:'ok'|'missing'|'too-old',version}]
|
|
57
|
+
chosenPython: '',
|
|
58
|
+
venvOk: false,
|
|
59
|
+
depsOk: false,
|
|
60
|
+
dl: { bytesDone: 0, bytesTotal: MODEL_SPEC.bytes, mirror: '', startedAt: 0, etaSec: 0 },
|
|
61
|
+
cancelled: false,
|
|
62
|
+
}
|
|
63
|
+
let dlAbort = null
|
|
64
|
+
|
|
65
|
+
function probe(pyPath, args, timeoutMs) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let done = false
|
|
68
|
+
const t = setTimeout(() => { if (!done) { done = true; resolve({ ok: false, out: '' }) } }, timeoutMs || 8000)
|
|
69
|
+
try {
|
|
70
|
+
execFileP(pyPath, args, { timeout: (timeoutMs || 8000) - 500, windowsHide: true, maxBuffer: 1024 * 1024 })
|
|
71
|
+
.then((r) => { if (!done) { done = true; clearTimeout(t); resolve({ ok: true, out: String(r.stdout || '').trim() }) } })
|
|
72
|
+
.catch(() => { if (!done) { done = true; clearTimeout(t); resolve({ ok: false, out: '' }) } })
|
|
73
|
+
} catch (_) { if (!done) { done = true; clearTimeout(t); resolve({ ok: false, out: '' }) } }
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pyVersionOf(out) {
|
|
78
|
+
const m = String(out || '').match(/(\d+)\.(\d+)/)
|
|
79
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]) } : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** ①环境探测:venv(推荐)→ 系统 python/python3/py launcher;版本 ≥3.9 且 <3.13(onnxruntime 兼容上界)。 */
|
|
83
|
+
async function detect() {
|
|
84
|
+
st.phase = 'detecting'; st.error = ''
|
|
85
|
+
const dshHome = dshHomeOf()
|
|
86
|
+
const cands = [
|
|
87
|
+
{ path: venvPython(), label: 'DSH Python 引擎 venv(推荐)', isVenv: true },
|
|
88
|
+
{ path: 'python', label: '系统 PATH: python', isVenv: false },
|
|
89
|
+
{ path: 'python3', label: '系统 PATH: python3', isVenv: false },
|
|
90
|
+
{ path: 'py', label: 'Windows py launcher', isVenv: false },
|
|
91
|
+
]
|
|
92
|
+
const out = []
|
|
93
|
+
for (const c of cands) {
|
|
94
|
+
if ((c.path.includes('\\') || c.path.includes('/')) && !existsSync(c.path)) { out.push({ ...c, status: 'missing', version: '' }); continue }
|
|
95
|
+
const r = await probe(c.path, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'])
|
|
96
|
+
const v = pyVersionOf(r.out)
|
|
97
|
+
if (!r.ok || !v) { out.push({ ...c, status: 'missing', version: '' }); continue }
|
|
98
|
+
out.push({ ...c, status: (v.major === 3 && v.minor >= 9 && v.minor <= 12) ? 'ok' : 'too-old', version: v.major + '.' + v.minor })
|
|
99
|
+
}
|
|
100
|
+
// 既有成果快照
|
|
101
|
+
st.venvOk = existsSync(venvPython())
|
|
102
|
+
st.depsOk = st.venvOk ? (await probe(venvPython(), ['-c', 'import fastembed, onnxruntime; print("deps-ok")'])).ok : false
|
|
103
|
+
st.modelReady = existsSync(modelPath())
|
|
104
|
+
st.pythons = out
|
|
105
|
+
st.phase = 'idle'
|
|
106
|
+
return snapshot()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function snapshot() {
|
|
110
|
+
return {
|
|
111
|
+
phase: st.phase, error: st.error,
|
|
112
|
+
pythons: st.pythons, chosenPython: st.chosenPython,
|
|
113
|
+
venvOk: st.venvOk, depsOk: st.depsOk, modelReady: st.modelReady,
|
|
114
|
+
modelPath: modelPath(), venvPython: venvPython(),
|
|
115
|
+
modelBytes: st.modelReady ? statSync(modelPath()).size : 0, modelExpectedBytes: MODEL_SPEC.bytes,
|
|
116
|
+
dl: { ...st.dl },
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** ②venv:幂等(已存在跳过);用探测到的首个 ok 系统解释器创建。 */
|
|
121
|
+
async function ensureVenv(pythonPath) {
|
|
122
|
+
const base = String(pythonPath || '').trim() || (st.pythons.find((x) => x.status === 'ok' && !x.isVenv) || {}).path || ''
|
|
123
|
+
if (!base) { st.error = '未检测到可用的系统 Python(需 3.9-3.12)。请先安装 Python。'; st.phase = 'error'; return snapshot() }
|
|
124
|
+
st.chosenPython = base
|
|
125
|
+
if (existsSync(venvPython())) { st.venvOk = true; return snapshot() }
|
|
126
|
+
st.phase = 'venv'
|
|
127
|
+
mkdirSync(engineRoot(), { recursive: true })
|
|
128
|
+
try {
|
|
129
|
+
await execFileP(base, ['-m', 'venv', venvDir()], { timeout: 180000, windowsHide: true })
|
|
130
|
+
st.venvOk = existsSync(venvPython())
|
|
131
|
+
if (!st.venvOk) throw new Error('venv 目录未生成')
|
|
132
|
+
diagOf('python-setup: venv created at ' + venvDir())
|
|
133
|
+
} catch (e) {
|
|
134
|
+
st.error = 'venv 创建失败: ' + String((e && e.message) || e).slice(0, 160); st.phase = 'error'
|
|
135
|
+
}
|
|
136
|
+
return snapshot()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** ③deps:venv 内 pip 装 fastembed+onnxruntime;官方源失败切清华镜像。 */
|
|
140
|
+
async function ensureDeps() {
|
|
141
|
+
if (!st.venvOk) { st.error = 'venv 未就绪,先执行 venv 步骤'; st.phase = 'error'; return snapshot() }
|
|
142
|
+
st.phase = 'deps'
|
|
143
|
+
const runPip = async (extra) => {
|
|
144
|
+
try {
|
|
145
|
+
const r = await execFileP(venvPython(), ['-m', 'pip', 'install', '--quiet'].concat(PIP_DEPS).concat(extra || []),
|
|
146
|
+
{ timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
|
|
147
|
+
return { ok: true }
|
|
148
|
+
} catch (e) { return { ok: false, tail: String((e && (e.stderr || e.message)) || e).slice(-300) } }
|
|
149
|
+
}
|
|
150
|
+
let r = await runPip()
|
|
151
|
+
if (!r.ok) r = await runPip(['-i', 'https://pypi.tuna.tsinghua.edu.cn/simple'])
|
|
152
|
+
st.depsOk = !!r.ok
|
|
153
|
+
if (!r.ok) { st.error = '依赖安装失败: ' + (r.tail || ''); st.phase = 'error'; return snapshot() }
|
|
154
|
+
diagOf('python-setup: deps installed into venv')
|
|
155
|
+
return snapshot()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** ④模型:BGE-M3 int8 单文件下载(cn→intl 双通道),8MB 分片进度,断点续传(Range)。 */
|
|
159
|
+
async function downloadModel() {
|
|
160
|
+
if (st.phase === 'downloading' || st.phase === 'verifying') return snapshot()
|
|
161
|
+
mkdirSync(modelsDir(), { recursive: true })
|
|
162
|
+
st.cancelled = false
|
|
163
|
+
for (const mirror of MODEL_SPEC.mirrors) {
|
|
164
|
+
if (st.cancelled) break
|
|
165
|
+
const url = mirror.url(MODEL_SPEC.repo + '/resolve/main/' + MODEL_SPEC.file)
|
|
166
|
+
st.phase = 'downloading'; st.dl.mirror = mirror.id; st.dl.startedAt = Date.now()
|
|
167
|
+
try {
|
|
168
|
+
await downloadWithResume(url, modelPath(), (done, total) => {
|
|
169
|
+
st.dl.bytesDone = done; st.dl.bytesTotal = total || MODEL_SPEC.bytes
|
|
170
|
+
const el = (Date.now() - st.dl.startedAt) / 1000
|
|
171
|
+
st.dl.etaSec = done > 0 ? Math.max(0, Math.round(el / done * (st.dl.bytesTotal - done))) : 0
|
|
172
|
+
}, () => st.cancelled)
|
|
173
|
+
st.phase = 'verifying'
|
|
174
|
+
const size = statSync(modelPath()).size
|
|
175
|
+
if (size < 100 * 1024 * 1024) throw new Error('下载不完整(' + size + ' bytes)')
|
|
176
|
+
st.modelReady = true; st.phase = 'ready'
|
|
177
|
+
diagOf('python-setup: model ready at ' + modelPath() + ' (' + size + ' bytes, mirror=' + mirror.id + ')')
|
|
178
|
+
return snapshot()
|
|
179
|
+
} catch (e) {
|
|
180
|
+
if (st.cancelled) { st.phase = 'idle'; return snapshot() }
|
|
181
|
+
st.error = '[' + mirror.id + '] ' + String((e && e.message) || e).slice(0, 160)
|
|
182
|
+
diagOf('python-setup: download failed via ' + mirror.id + ': ' + st.error)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
st.phase = 'error'
|
|
186
|
+
return snapshot()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 断点续传下载:已有 .part 则 Range 续传;完成 8MB 粒度回调进度。 */
|
|
190
|
+
function downloadWithResume(url, target, onProgress, isCancelled) {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const part = target + '.part'
|
|
193
|
+
let done = 0
|
|
194
|
+
try { if (existsSync(part)) done = statSync(part).size } catch (_) {}
|
|
195
|
+
const headers = done > 0 ? { Range: 'bytes=' + done + '-' } : {}
|
|
196
|
+
fetch(url, { headers }).then((resp) => {
|
|
197
|
+
if (!resp.ok && resp.status !== 206) { reject(new Error('HTTP ' + resp.status)); return }
|
|
198
|
+
const total = Number(resp.headers.get('content-length') || 0) + done
|
|
199
|
+
const stream = createWriteStream(part, done > 0 ? { flags: 'a' } : { flags: 'w' })
|
|
200
|
+
let lastTick = 0
|
|
201
|
+
resp.body.on('data', (chunk) => {
|
|
202
|
+
done += chunk.length
|
|
203
|
+
const now = Date.now()
|
|
204
|
+
if (now - lastTick > 500) { lastTick = now; onProgress(done, total) }
|
|
205
|
+
})
|
|
206
|
+
resp.body.on('error', (e) => { stream.end(); reject(e) })
|
|
207
|
+
resp.body.on('end', () => {
|
|
208
|
+
stream.end(() => {
|
|
209
|
+
writeFileSyncSafe(part, target).then(resolve).catch(reject)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
dlAbort = () => { try { resp.body.destroy() } catch (_) {} }
|
|
213
|
+
}).catch(reject)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function writeFileSyncSafe(from, to) {
|
|
218
|
+
// .part → 正式名(同目录 rename,原子语义)
|
|
219
|
+
await rm(to, { force: true })
|
|
220
|
+
await import('node:fs').then((fs) => fs.renameSync(from, to))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function cancelDownload() { st.cancelled = true; if (dlAbort) try { dlAbort() } catch (_) {} return snapshot() }
|
|
224
|
+
|
|
225
|
+
/** 汇总:semantic-status 的 pythonSetup 字段(供向导 UI 轮询)。 */
|
|
226
|
+
function status() { return snapshot() }
|
|
227
|
+
|
|
228
|
+
return { detect, ensureVenv, ensureDeps, downloadModel, cancelDownload, status, engineRoot, venvPython, modelPath }
|
|
229
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a9i5k4/dsh-auto-memory",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "2.1.
|
|
3
|
+
"description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
|
|
4
|
+
"version": "2.1.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"lib",
|
|
14
|
-
"
|
|
14
|
+
"python",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"!python/bench",
|
|
17
|
+
"!python/__pycache__"
|
|
15
18
|
],
|
|
16
19
|
"dsh": {
|
|
17
20
|
"bundle": {
|