@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.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/CHANGELOG.md +42 -0
- package/LICENSE +661 -0
- package/README.en.md +110 -0
- package/README.md +102 -2
- package/dsh-memory-local/FORK-BASE.json +24 -0
- package/dsh-memory-local/README.md +113 -0
- package/dsh-memory-local/build.mjs +101 -0
- package/dsh-memory-local/cordis.patch.yml +20 -0
- package/dsh-memory-local/deploy.mjs +306 -0
- package/dsh-memory-local/entity-status.mjs +75 -0
- package/dsh-memory-local/lib/bridge.mjs +108 -0
- package/dsh-memory-local/lib/host-adapter.js +1171 -0
- package/dsh-memory-local/lib/index.d.ts +128 -0
- package/dsh-memory-local/lib/index.js +944 -0
- package/dsh-memory-local/lib/injector.mjs +400 -0
- package/dsh-memory-local/lib/memory-read.mjs +116 -0
- package/dsh-memory-local/lib/module-gate.mjs +68 -0
- package/dsh-memory-local/lib/pointer.mjs +152 -0
- package/dsh-memory-local/lib/prod-gate.mjs +121 -0
- package/dsh-memory-local/lib/recall-log.mjs +118 -0
- package/dsh-memory-local/lib/recall.mjs +192 -0
- package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
- package/dsh-memory-local/lib/user-message.mjs +32 -0
- package/dsh-memory-local/lib/vectors-read.mjs +110 -0
- package/dsh-memory-local/package.json +51 -0
- package/dsh-memory-local/restore.mjs +259 -0
- package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
- package/dsh-memory-local/src/bridge.mjs +108 -0
- package/dsh-memory-local/src/host-adapter.js +1171 -0
- package/dsh-memory-local/src/index.d.ts +128 -0
- package/dsh-memory-local/src/index.js +944 -0
- package/dsh-memory-local/src/injector.mjs +400 -0
- package/dsh-memory-local/src/memory-read.mjs +116 -0
- package/dsh-memory-local/src/module-gate.mjs +68 -0
- package/dsh-memory-local/src/pointer.mjs +152 -0
- package/dsh-memory-local/src/recall-log.mjs +118 -0
- package/dsh-memory-local/src/recall.mjs +192 -0
- package/dsh-memory-local/src/schema-migrate.mjs +365 -0
- package/dsh-memory-local/src/user-message.mjs +32 -0
- package/dsh-memory-local/src/vectors-read.mjs +110 -0
- package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
- package/dsh-memory-local/test/probe-entity.mjs +170 -0
- package/dsh-memory-local/test/regression.mjs +270 -0
- package/dsh-memory-local/test/s2b.test.mjs +568 -0
- package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
- package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
- package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
- package/dsh-memory-local/verify-after-restart.mjs +181 -0
- package/lib/deploy-guard.mjs +391 -0
- package/lib/guard-http.mjs +93 -0
- package/lib/pitfall-form-map.mjs +130 -0
- package/lib/pitfall-score.mjs +185 -0
- package/lib/session-file-name.mjs +160 -0
- package/lib/zstd-frames.mjs +78 -0
- package/memory-db-tools/README.md +68 -0
- package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
- package/memory-db-tools/audit-analyze.mjs +352 -0
- package/memory-db-tools/audit-dump-ids.mjs +245 -0
- package/memory-db-tools/audit-export.mjs +270 -0
- package/memory-db-tools/audit-hit-probe.mjs +227 -0
- package/memory-db-tools/audit-ledger-probe.mjs +236 -0
- package/memory-db-tools/audit-persona-overlap.mjs +277 -0
- package/memory-db-tools/audit-struct-probe.mjs +213 -0
- package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
- package/memory-db-tools/check-memory-db.mjs +8 -0
- package/memory-db-tools/dump-memory-db.mjs +12 -0
- package/memory-db-tools/lib/memory-scope.mjs +446 -0
- package/memory-db-tools/list-memory-summary.mjs +477 -0
- package/memory-db-tools/memory-identity-check.mjs +101 -0
- package/memory-db-tools/memory-schema-migrate.mjs +290 -0
- package/memory-db-tools/memory-scope-fixture.mjs +197 -0
- package/memory-db-tools/read-memories.mjs +26 -0
- package/memory-db-tools/read-memory-db.mjs +20 -0
- package/memory-db-tools/refine-memories.mjs +247 -0
- package/memory-db-tools/show-mem-times.mjs +22 -0
- package/memory-engine/README.md +70 -0
- package/memory-engine/bench/bench.mjs +221 -0
- package/memory-engine/cli/distill-rollback.mjs +86 -0
- package/memory-engine/cli/distill.mjs +156 -0
- package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
- package/memory-engine/cli/doc-index.mjs +48 -0
- package/memory-engine/cli/domain-tags.mjs +251 -0
- package/memory-engine/cli/pitfall-hook.mjs +91 -0
- package/memory-engine/cli/recall.mjs +132 -0
- package/memory-engine/cli/selfcheck.mjs +57 -0
- package/memory-engine/cli/serve-start.mjs +247 -0
- package/memory-engine/cli/sweep.mjs +58 -0
- package/memory-engine/cli/sync.mjs +69 -0
- package/memory-engine/lib/cli-util.mjs +197 -0
- package/memory-engine/lib/config.mjs +135 -0
- package/memory-engine/lib/dirty-ledger.mjs +174 -0
- package/memory-engine/lib/distill-rollback.mjs +178 -0
- package/memory-engine/lib/distill.mjs +898 -0
- package/memory-engine/lib/doc-index-store.mjs +325 -0
- package/memory-engine/lib/doc-index.mjs +529 -0
- package/memory-engine/lib/doc-scan.mjs +271 -0
- package/memory-engine/lib/doc-summarize.mjs +305 -0
- package/memory-engine/lib/domain-tags.mjs +429 -0
- package/memory-engine/lib/embedder.mjs +242 -0
- package/memory-engine/lib/hashes.mjs +12 -0
- package/memory-engine/lib/memory-source.mjs +127 -0
- package/memory-engine/lib/pitfall-source.mjs +170 -0
- package/memory-engine/lib/policy-source.mjs +371 -0
- package/memory-engine/lib/recall.mjs +331 -0
- package/memory-engine/lib/selfcheck.mjs +131 -0
- package/memory-engine/lib/sweep.mjs +159 -0
- package/memory-engine/lib/sync.mjs +180 -0
- package/memory-engine/lib/tick-hook.mjs +218 -0
- package/memory-engine/lib/vectors-store.mjs +292 -0
- package/memory-engine/package-lock.json +1086 -0
- package/memory-engine/package.json +22 -0
- package/memory-engine/serve.mjs +171 -0
- package/memory-engine/test/01-store.test.mjs +127 -0
- package/memory-engine/test/02-sync.test.mjs +196 -0
- package/memory-engine/test/03-sweep.test.mjs +145 -0
- package/memory-engine/test/04-recall.test.mjs +214 -0
- package/memory-engine/test/05-selfcheck.test.mjs +175 -0
- package/memory-engine/test/06-doc-index.test.mjs +928 -0
- package/memory-engine/test/07-domain-tags.test.mjs +298 -0
- package/memory-engine/test/08-distill.test.mjs +686 -0
- package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
- package/memory-engine/test/10-tick-hook.test.mjs +252 -0
- package/memory-engine/test/11-policy-source.test.mjs +224 -0
- package/memory-engine/test/_tickhook-worker.mjs +56 -0
- package/memory-engine/test/fixtures.mjs +205 -0
- package/memory-engine/test/harness.mjs +73 -0
- package/memory-engine/test/run.mjs +35 -0
- package/memory-engine/tools/bridge-smoke.mjs +80 -0
- package/memory-engine/tools/fetch-model.mjs +128 -0
- package/memory-engine/tools/smoke-recall.mjs +76 -0
- package/package.json +53 -4
- package/persona-memory-audit.mjs +217 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deploy.mjs —— 把自持 fork(dsh-memory-local)幂等部署进某个 DSH home 的 web profile,
|
|
3
|
+
* 替换上游 `dsh-memory` 实体(同一槽位:bundle 名 / patch insert name / .package-map.json
|
|
4
|
+
* 全部按 `dsh-memory` 解析,所以只换实体内容、不动 profile wiring)。
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ 安全口径(本卡红线):
|
|
7
|
+
* · `--target <DSH_HOME>` **必填** —— 没有默认值,写错路径不会静默落到生产;
|
|
8
|
+
* · 目标是本机默认 home(`~/.dsh`,即生产)时必须显式再加 `--allow-production`,
|
|
9
|
+
* 否则 fail-closed 拒绝(本卡 S2a 明令禁止部署生产);
|
|
10
|
+
* · 本脚本**绝不重启任何 DSH**、绝不动 `memory.db`、绝不写 profile package.json / package-map。
|
|
11
|
+
*
|
|
12
|
+
* 步骤:
|
|
13
|
+
* 0) gate0:参数白名单(fail-closed)+ 目标 home 形态校验(必须 <home>/profiles/<profile> 存在)
|
|
14
|
+
* 1) gate1:静态扫 lib/ 里「解析后逃出插件目录的相对 import」(tools\lib\deploy-guard.mjs)
|
|
15
|
+
* 2) 备份当前实体(**仅当它不是 fork 版**):整份拷进 _archive\g24-s2a-memory-deploy-<stamp>\official-entity\
|
|
16
|
+
* 3) 幂等写实体:lib/ + cordis.patch.yml + 部署态 package.json(name→dsh-memory、version→上游版本,
|
|
17
|
+
* 真身与基点记在 package.json 的 `fork` 段)
|
|
18
|
+
* 4) gate2:以**部署位置**为解析上下文冒烟 import 每个 lib 模块
|
|
19
|
+
* 5) gate3:mock ctx 加载探针 —— 断言三工具注册 + memory:recall 段注册
|
|
20
|
+
*
|
|
21
|
+
* 退出码:0 = 成功(含幂等无变化) / 1 = 门禁或部署失败 / 2 = 用法/参数错误。
|
|
22
|
+
*
|
|
23
|
+
* 用法:
|
|
24
|
+
* node deploy.mjs --target <DSH_HOME> [--profile web] [--dry-run] [--allow-production]
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs'
|
|
27
|
+
import path from 'node:path'
|
|
28
|
+
import os from 'node:os'
|
|
29
|
+
import crypto from 'node:crypto'
|
|
30
|
+
import { spawnSync } from 'node:child_process'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
import { parseArgs, describeUnknown, findEscapingImports, smokeImportAll, EXIT } from '../lib/deploy-guard.mjs'
|
|
33
|
+
|
|
34
|
+
const KNOWN_ARGS = [
|
|
35
|
+
'--dry-run',
|
|
36
|
+
'--allow-production',
|
|
37
|
+
{ flag: '--target', takesValue: true },
|
|
38
|
+
{ flag: '--profile', takesValue: true, alias: '--p' },
|
|
39
|
+
]
|
|
40
|
+
const pa = parseArgs(process.argv.slice(2), KNOWN_ARGS)
|
|
41
|
+
if (!pa.ok) {
|
|
42
|
+
console.error(`[deploy] ${describeUnknown(pa.unknown, KNOWN_ARGS)}`)
|
|
43
|
+
console.error('[deploy] 用法:node deploy.mjs --target <DSH_HOME> [--profile web] [--dry-run] [--allow-production]')
|
|
44
|
+
process.exit(EXIT.USAGE)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
48
|
+
const dry = !!pa.flags['--dry-run']
|
|
49
|
+
const allowProduction = !!pa.flags['--allow-production']
|
|
50
|
+
const profileName = pa.values['--profile'] ?? 'web'
|
|
51
|
+
|
|
52
|
+
// ── gate0:--target 必填 + fail-closed ────────────────────────────────────────
|
|
53
|
+
const rawTarget = pa.values['--target']
|
|
54
|
+
if (!rawTarget) {
|
|
55
|
+
console.error('[deploy] 缺少 --target <DSH_HOME>:本脚本没有默认目标(写死生产路径是明令禁止的)')
|
|
56
|
+
console.error('[deploy] 示例:node deploy.mjs --target <隔离实例的 DSH_HOME> --dry-run')
|
|
57
|
+
process.exit(EXIT.USAGE)
|
|
58
|
+
}
|
|
59
|
+
const home = path.resolve(rawTarget)
|
|
60
|
+
if (!fs.existsSync(home)) {
|
|
61
|
+
console.error(`[deploy] --target 不存在:${home}`)
|
|
62
|
+
process.exit(EXIT.USAGE)
|
|
63
|
+
}
|
|
64
|
+
const defaultHome = path.join(os.homedir(), '.dsh')
|
|
65
|
+
const isProduction = home.toLowerCase() === path.resolve(defaultHome).toLowerCase()
|
|
66
|
+
if (isProduction && !allowProduction) {
|
|
67
|
+
console.error(`[deploy] 目标 ${home} 是本机默认 home(= 生产)。`)
|
|
68
|
+
console.error('[deploy] 本卡(G24-S2a)明令禁止部署生产;确认要动生产请显式追加 --allow-production。')
|
|
69
|
+
process.exit(EXIT.GATE)
|
|
70
|
+
}
|
|
71
|
+
const profile = path.join(home, 'profiles', profileName)
|
|
72
|
+
if (!fs.existsSync(profile)) {
|
|
73
|
+
console.error(`[deploy] profile 不存在:${profile}`)
|
|
74
|
+
process.exit(EXIT.USAGE)
|
|
75
|
+
}
|
|
76
|
+
const target = path.join(profile, 'node_modules', 'dsh-memory')
|
|
77
|
+
const packageMapFile = path.join(profile, 'node_modules', '.package-map.json')
|
|
78
|
+
|
|
79
|
+
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
|
|
80
|
+
|
|
81
|
+
function readJson(file) {
|
|
82
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
83
|
+
}
|
|
84
|
+
function stamp() {
|
|
85
|
+
const d = new Date()
|
|
86
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
87
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
|
|
88
|
+
}
|
|
89
|
+
function listTree(root) {
|
|
90
|
+
const out = []
|
|
91
|
+
const walk = (dir) => {
|
|
92
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
const full = path.join(dir, e.name)
|
|
94
|
+
if (e.isDirectory()) walk(full)
|
|
95
|
+
else if (e.isFile()) out.push(path.relative(root, full).replaceAll('\\', '/'))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (fs.existsSync(root)) walk(root)
|
|
99
|
+
return out.sort()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── 产物齐备性 ────────────────────────────────────────────────────────────────
|
|
103
|
+
const libEntry = path.join(here, 'lib', 'index.js')
|
|
104
|
+
const buildInfoFile = path.join(here, 'lib', '.build-info.json')
|
|
105
|
+
if (!fs.existsSync(libEntry) || !fs.existsSync(buildInfoFile)) {
|
|
106
|
+
console.error('[deploy] 构建产物缺失:先跑 `node build.mjs`')
|
|
107
|
+
process.exit(EXIT.GATE)
|
|
108
|
+
}
|
|
109
|
+
const buildInfo = readJson(buildInfoFile)
|
|
110
|
+
const srcSha = sha256(fs.readFileSync(libEntry))
|
|
111
|
+
const pkg = readJson(path.join(here, 'package.json'))
|
|
112
|
+
const forkBase = readJson(path.join(here, 'FORK-BASE.json'))
|
|
113
|
+
|
|
114
|
+
// ── gate1:逃逸相对 import 静态扫(一切写操作之前) ─────────────────────────────
|
|
115
|
+
{
|
|
116
|
+
const esc = findEscapingImports(path.join(here, 'lib'), { root: here })
|
|
117
|
+
console.log(`[gate1] 静态扫 lib/:${esc.scanned} 文件 / ${esc.specs} 处相对 import`)
|
|
118
|
+
if (esc.bad.length) {
|
|
119
|
+
for (const b of esc.bad) console.error(` ✗ ${b.file}:${b.line} '${b.spec}' → ${b.resolved}`)
|
|
120
|
+
console.error('[gate1] 未通过 —— 已中止,未写任何文件')
|
|
121
|
+
process.exit(EXIT.GATE)
|
|
122
|
+
}
|
|
123
|
+
console.log('[gate1] OK:无逃逸相对 import')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── 现状判定(幂等 / 备份需求) ───────────────────────────────────────────────
|
|
127
|
+
const archiveRoot = process.env.DSH_ARCHIVE || path.join(process.cwd(), '_archive')
|
|
128
|
+
const entityPkgFile = path.join(target, 'package.json')
|
|
129
|
+
const entityEntryFile = path.join(target, 'lib', 'index.js')
|
|
130
|
+
const entityExists = fs.existsSync(target)
|
|
131
|
+
const entityPkg = entityExists && fs.existsSync(entityPkgFile) ? readJson(entityPkgFile) : null
|
|
132
|
+
const entitySha = fs.existsSync(entityEntryFile) ? sha256(fs.readFileSync(entityEntryFile)) : null
|
|
133
|
+
const alreadyFork = Boolean(entityPkg && entityPkg.fork && entitySha === srcSha)
|
|
134
|
+
const isUpstream = entityExists && !entityPkg?.fork
|
|
135
|
+
|
|
136
|
+
console.log(`source : ${here}`)
|
|
137
|
+
console.log(`target : ${target}${isProduction ? ' ⚠ PRODUCTION HOME' : ' [non-default home]'}`)
|
|
138
|
+
console.log(`entity : ${entityExists ? (alreadyFork ? `already fork (sha ${entitySha.slice(0, 16)}…)` : entityPkg?.fork ? 'fork, but different sha' : `upstream ${entityPkg?.name}@${entityPkg?.version}`) : 'absent'}`)
|
|
139
|
+
console.log(`lib sha : ${srcSha}`)
|
|
140
|
+
// gate0b:profile 可加载性 —— 0.1.x 世代用 node_modules/.package-map.json,
|
|
141
|
+
// 0.2.0-rc.x 起该文件已废弃(2026-10-28 实测:已装 @deepseek-ai 包 6,273 个 .js
|
|
142
|
+
// 里 `package-map` / `packageMap` / `modules.yaml` 零引用),现世代以 pnpm 的
|
|
143
|
+
// node_modules/.modules.yaml 为准 ⇒ 二者任一存在即视为可加载。
|
|
144
|
+
const modulesYamlFile = path.join(profile, 'node_modules', '.modules.yaml')
|
|
145
|
+
if (!fs.existsSync(packageMapFile) && !fs.existsSync(modulesYamlFile)) {
|
|
146
|
+
console.error(`[deploy] 找不到 ${packageMapFile} 或 ${modulesYamlFile} —— 这不是一个可加载的 DSH profile`)
|
|
147
|
+
process.exit(EXIT.USAGE)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 已有备份里是否有与该实体逐文件一致的「上游原物」快照(回滚→重部署时不重复归档)。 */
|
|
151
|
+
function findMatchingSnapshot(entityDir) {
|
|
152
|
+
if (!fs.existsSync(archiveRoot)) return null
|
|
153
|
+
const dirs = fs.readdirSync(archiveRoot)
|
|
154
|
+
.filter((n) => n.startsWith('g24-s2a-memory-deploy-'))
|
|
155
|
+
.map((n) => path.join(archiveRoot, n))
|
|
156
|
+
.sort()
|
|
157
|
+
.reverse()
|
|
158
|
+
for (const dir of dirs) {
|
|
159
|
+
const manifestFile = path.join(dir, 'OFFICIAL-ENTITY.json')
|
|
160
|
+
if (!fs.existsSync(manifestFile)) continue
|
|
161
|
+
try {
|
|
162
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'))
|
|
163
|
+
if (manifest.files.every((f) => {
|
|
164
|
+
const full = path.join(entityDir, f.file)
|
|
165
|
+
return fs.existsSync(full) && sha256(fs.readFileSync(full)) === f.sha256
|
|
166
|
+
})) return dir
|
|
167
|
+
} catch { /* malformed manifest -> skip */ }
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let backupDir = null
|
|
173
|
+
let reusedSnapshot = false
|
|
174
|
+
if (isUpstream) {
|
|
175
|
+
const existing = findMatchingSnapshot(target)
|
|
176
|
+
if (existing) { backupDir = existing; reusedSnapshot = true }
|
|
177
|
+
else backupDir = path.join(archiveRoot, `g24-s2a-memory-deploy-${stamp()}`)
|
|
178
|
+
} else if (!entityExists) {
|
|
179
|
+
// 目标 profile 里**根本没有** dsh-memory 实体 = 全新安装(2026-10-28 为小汐生活实例
|
|
180
|
+
// `--profile life` 补的分支):没有「上游原物」可归档,restore.mjs 也用不到还原源
|
|
181
|
+
// ⇒ 只建一个记录目录,DEPLOY-INFO 落这里。
|
|
182
|
+
backupDir = path.join(archiveRoot, `g24-s2a-memory-deploy-${stamp()}`)
|
|
183
|
+
console.log('[deploy] 目标 profile 无 dsh-memory 实体 = 全新安装,无需归档上游原物')
|
|
184
|
+
} else {
|
|
185
|
+
// 已在 fork 态:沿用最近一个「含上游原物」的备份目录(restore 的唯一还原源)
|
|
186
|
+
const candidates = fs.existsSync(archiveRoot)
|
|
187
|
+
? fs.readdirSync(archiveRoot)
|
|
188
|
+
.filter((n) => n.startsWith('g24-s2a-memory-deploy-'))
|
|
189
|
+
.map((n) => path.join(archiveRoot, n))
|
|
190
|
+
.filter((d) => fs.existsSync(path.join(d, 'official-entity', 'package.json')))
|
|
191
|
+
.sort()
|
|
192
|
+
: []
|
|
193
|
+
backupDir = candidates.at(-1) ?? null
|
|
194
|
+
if (backupDir === null) {
|
|
195
|
+
console.error('[deploy] 实体已是 fork 态,但 _archive 里找不到含上游原物的备份目录 ——')
|
|
196
|
+
console.error(' 继续部署会让 restore.mjs 失去还原源,已中止(先把上游 dsh-memory 原物归档再跑)。')
|
|
197
|
+
process.exit(EXIT.GATE)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.log(`backup : ${backupDir}${isUpstream ? ' (will capture the upstream entity)' : ' (existing upstream snapshot)'}`)
|
|
201
|
+
|
|
202
|
+
if (dry) {
|
|
203
|
+
console.log('[dry-run] 未写任何文件')
|
|
204
|
+
process.exit(EXIT.OK)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── 2) 备份上游实体(仅首次替换时;回滚→重部署复用同一份快照) ──────────────────
|
|
208
|
+
if (isUpstream && reusedSnapshot) {
|
|
209
|
+
console.log(`[backup] 复用已有上游快照:${backupDir}(与当前实体逐文件一致,不重复归档)`)
|
|
210
|
+
} else if (isUpstream) {
|
|
211
|
+
fs.mkdirSync(backupDir, { recursive: true })
|
|
212
|
+
fs.cpSync(target, path.join(backupDir, 'official-entity'), { recursive: true })
|
|
213
|
+
const files = listTree(path.join(backupDir, 'official-entity')).map((f) => {
|
|
214
|
+
const full = path.join(backupDir, 'official-entity', f)
|
|
215
|
+
return { file: f, bytes: fs.statSync(full).size, sha256: sha256(fs.readFileSync(full)) }
|
|
216
|
+
})
|
|
217
|
+
fs.writeFileSync(path.join(backupDir, 'OFFICIAL-ENTITY.json'), `${JSON.stringify({
|
|
218
|
+
capturedAt: new Date().toISOString(),
|
|
219
|
+
from: target,
|
|
220
|
+
package: { name: entityPkg.name, version: entityPkg.version },
|
|
221
|
+
files,
|
|
222
|
+
}, null, 2)}\n`, 'utf8')
|
|
223
|
+
console.log(`[backup] 上游实体已归档:${files.length} 个文件 → ${path.join(backupDir, 'official-entity')}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── 3) 写实体(幂等) ─────────────────────────────────────────────────────────
|
|
227
|
+
fs.rmSync(target, { recursive: true, force: true })
|
|
228
|
+
fs.mkdirSync(target, { recursive: true })
|
|
229
|
+
for (const entry of ['lib', 'cordis.patch.yml']) {
|
|
230
|
+
const from = path.join(here, entry)
|
|
231
|
+
if (!fs.existsSync(from)) continue
|
|
232
|
+
fs.cpSync(from, path.join(target, entry), { recursive: true })
|
|
233
|
+
}
|
|
234
|
+
// 部署态 package.json:占上游槽位名(name/version),真身与基点记在 `fork` 段
|
|
235
|
+
const deployed = {
|
|
236
|
+
...pkg,
|
|
237
|
+
name: forkBase.upstream.name,
|
|
238
|
+
version: forkBase.upstream.version,
|
|
239
|
+
fork: {
|
|
240
|
+
name: pkg.name,
|
|
241
|
+
sourceVersion: pkg.version,
|
|
242
|
+
base: `${forkBase.upstream.name}@${forkBase.upstream.version}`,
|
|
243
|
+
baseSha256: forkBase.source.sha256,
|
|
244
|
+
libSha256: srcSha,
|
|
245
|
+
deployedAt: new Date().toISOString(),
|
|
246
|
+
deployedFrom: here,
|
|
247
|
+
note: 'Deployed entity occupies the upstream slot: bundle name, patch insert name and .package-map entry all resolve `dsh-memory`.',
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
// lib/.build-info.json 不进实体(部署态只放运行时需要的三件)
|
|
251
|
+
fs.writeFileSync(path.join(target, 'package.json'), `${JSON.stringify(deployed, null, 2)}\n`, 'utf8')
|
|
252
|
+
fs.rmSync(path.join(target, 'lib', '.build-info.json'), { force: true })
|
|
253
|
+
console.log(`[deploy] 实体已写入:lib/ cordis.patch.yml package.json(deploy 态名=${deployed.name}@${deployed.version},fork 真身=${pkg.name}@${pkg.version})`)
|
|
254
|
+
|
|
255
|
+
// ── 4) gate2:部署位置冒烟 import ─────────────────────────────────────────────
|
|
256
|
+
{
|
|
257
|
+
const sm = smokeImportAll(target, 'lib')
|
|
258
|
+
if (sm.error) {
|
|
259
|
+
console.error(`[deploy] 门禁②无法执行:${sm.error}(回滚:node ${path.join(here, 'restore.mjs')} --target ${home})`)
|
|
260
|
+
process.exit(EXIT.GATE)
|
|
261
|
+
}
|
|
262
|
+
console.log(`[gate2] 部署位置冒烟 import:${sm.total} 个模块(模式 ${sm.mode})`)
|
|
263
|
+
for (const b of sm.bad) console.error(` ✗ ${b.file} → ${b.err}`)
|
|
264
|
+
if (sm.bad.length) {
|
|
265
|
+
console.error('[gate2] 未通过 —— 立即回滚:node restore.mjs --target ' + home)
|
|
266
|
+
process.exit(EXIT.GATE)
|
|
267
|
+
}
|
|
268
|
+
console.log('[gate2] OK:模块在部署位置可解析')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── 5) gate3:mock ctx 加载探针(三工具 + recall 段) ─────────────────────────
|
|
272
|
+
{
|
|
273
|
+
const probe = spawnSync(process.execPath, [path.join(here, 'test', 'probe-entity.mjs'), target], {
|
|
274
|
+
encoding: 'utf8', timeout: 60000, windowsHide: true,
|
|
275
|
+
})
|
|
276
|
+
const out = (probe.stdout ?? '').trim().split(/\r?\n/).filter(Boolean).at(-1) ?? ''
|
|
277
|
+
console.log(`[gate3] 加载探针:${out || (probe.stderr ?? '').trim().slice(0, 300) || 'no output'}`)
|
|
278
|
+
if (probe.status !== 0) {
|
|
279
|
+
console.error('[gate3] 未通过(插件在 DSH 加载期注册不出预期工具/段)—— 立即回滚:node restore.mjs --target ' + home)
|
|
280
|
+
process.exit(EXIT.GATE)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── 记录 ─────────────────────────────────────────────────────────────────────
|
|
285
|
+
const info = {
|
|
286
|
+
deployedAt: new Date().toISOString(),
|
|
287
|
+
target,
|
|
288
|
+
home,
|
|
289
|
+
profile: profileName,
|
|
290
|
+
productionHome: isProduction,
|
|
291
|
+
source: here,
|
|
292
|
+
fork: { name: pkg.name, version: pkg.version },
|
|
293
|
+
occupation: { name: deployed.name, version: deployed.version },
|
|
294
|
+
base: `${forkBase.upstream.name}@${forkBase.upstream.version}`,
|
|
295
|
+
baseSha256: forkBase.source.sha256,
|
|
296
|
+
libSha256: srcSha,
|
|
297
|
+
upstreamEntityCaptured: isUpstream,
|
|
298
|
+
upstreamSnapshotDir: backupDir,
|
|
299
|
+
rollback: `node ${path.join(here, 'restore.mjs')} --target ${home}${isProduction ? ' --allow-production' : ''}`,
|
|
300
|
+
}
|
|
301
|
+
const infoFile = path.join(backupDir, `DEPLOY-INFO-${stamp()}.json`)
|
|
302
|
+
fs.mkdirSync(backupDir, { recursive: true })
|
|
303
|
+
fs.writeFileSync(infoFile, `${JSON.stringify(info, null, 2)}\n`, 'utf8')
|
|
304
|
+
console.log(`\n[deploy] complete — 记录:${infoFile}`)
|
|
305
|
+
console.log('[deploy] 未重启任何 DSH(本脚本不做重启)。隔离实例请用测试环境冒烟脚本启动:')
|
|
306
|
+
console.log(' (隔离实例请用你自己的测试环境冒烟脚本启动,本仓不提供)')
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entity-status.mjs —— 只读:报告某个 DSH home 的 web profile 里 `dsh-memory` 实体当前是
|
|
3
|
+
* **上游官方原物**还是 **fork(dsh-memory-local)**,并给逐文件 sha / 体积 / 工具面摘要。
|
|
4
|
+
*
|
|
5
|
+
* 用途:一键回滚脚本(桌面 `记忆引擎回滚.cmd`)跑完后的判据、以及交付报告取证。
|
|
6
|
+
* **只读**:不改任何文件、不重启、不写库。**输出全 ASCII**(要被 GBK 控制台读)。
|
|
7
|
+
*
|
|
8
|
+
* 退出码:0 = fork 版 / 3 = 上游原物 / 1 = 实体缺失或不可判。
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* node entity-status.mjs # 默认本机 home(~/.dsh)
|
|
12
|
+
* node entity-status.mjs --target <DSH_HOME> [--profile web]
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import os from 'node:os'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import crypto from 'node:crypto'
|
|
18
|
+
|
|
19
|
+
const argv = process.argv.slice(2)
|
|
20
|
+
const val = (flag, dflt) => {
|
|
21
|
+
const i = argv.indexOf(flag)
|
|
22
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt
|
|
23
|
+
}
|
|
24
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
25
|
+
process.stdout.write('usage: node entity-status.mjs [--target <DSH_HOME>] [--profile web]\n')
|
|
26
|
+
process.exit(0)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const home = path.resolve(val('--target', path.join(os.homedir(), '.dsh')))
|
|
30
|
+
const profileName = val('--profile', 'web')
|
|
31
|
+
const entity = path.join(home, 'profiles', profileName, 'node_modules', 'dsh-memory')
|
|
32
|
+
|
|
33
|
+
const out = (s) => process.stdout.write(`${s}\n`)
|
|
34
|
+
const sha256 = (p) => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex')
|
|
35
|
+
|
|
36
|
+
out(`entity : ${entity}`)
|
|
37
|
+
if (!fs.existsSync(entity)) {
|
|
38
|
+
out('status : MISSING (no dsh-memory entity under this home/profile)')
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const walk = (dir, base = '') => {
|
|
43
|
+
const rows = []
|
|
44
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
45
|
+
const full = path.join(dir, e.name)
|
|
46
|
+
const rel = base ? `${base}/${e.name}` : e.name
|
|
47
|
+
if (e.isDirectory()) rows.push(...walk(full, rel))
|
|
48
|
+
else rows.push({ rel, bytes: fs.statSync(full).size, sha: sha256(full) })
|
|
49
|
+
}
|
|
50
|
+
return rows
|
|
51
|
+
}
|
|
52
|
+
const files = walk(entity)
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
out(` ${f.rel.padEnd(24)} ${String(f.bytes).padStart(7)} B ${f.sha}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pkgFile = path.join(entity, 'package.json')
|
|
58
|
+
const entryFile = path.join(entity, 'lib', 'index.js')
|
|
59
|
+
let pkg = null
|
|
60
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8')) } catch { /* ignore */ }
|
|
61
|
+
|
|
62
|
+
const mjsCount = files.filter((f) => f.rel.startsWith('lib/') && f.rel.endsWith('.mjs')).length
|
|
63
|
+
const entry = files.find((f) => f.rel === 'lib/index.js')
|
|
64
|
+
const fork = pkg && pkg.fork
|
|
65
|
+
|
|
66
|
+
out(`entrySha : ${entry ? entry.sha : '(none)'} ${entry ? `${entry.bytes} B` : ''}`)
|
|
67
|
+
out(`lib/*.mjs: ${mjsCount}`)
|
|
68
|
+
if (fork) {
|
|
69
|
+
out(`status : FORK (${fork.name}@${fork.sourceVersion}, base ${fork.base})`)
|
|
70
|
+
out(`forkLib : ${fork.libSha256}`)
|
|
71
|
+
out(`match : ${entry && entry.sha === fork.libSha256 ? 'OK (entry sha == fork record)' : 'MISMATCH (entry sha != fork record!)'}`)
|
|
72
|
+
process.exit(entry && entry.sha === fork.libSha256 ? 0 : 1)
|
|
73
|
+
}
|
|
74
|
+
out(`status : UPSTREAM (official ${pkg ? `${pkg.name}@${pkg.version}` : 'unknown'}, no fork record)`)
|
|
75
|
+
process.exit(3)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORK-EDIT(2026-09-21): 回环编码桥客户端(方案 §3.1 编码通道契约 / §3.4 fail-open)
|
|
3
|
+
*
|
|
4
|
+
* 契约(S1 已实测,见 S1 交付 §3.7):
|
|
5
|
+
* · 桥只绑 127.0.0.1,端口从 8201 起、被占则 +1 顺延(serve.mjs,最多 10 次)
|
|
6
|
+
* · GET /health → { ready, modelLoading, model, dim, port, ... }
|
|
7
|
+
* · POST /encode {"texts":[…]} → { vectors: [[…]], model, dim }
|
|
8
|
+
*
|
|
9
|
+
* 本模块的铁律:
|
|
10
|
+
* ① 桥不在 / 未就绪 / 超时 / 任何异常 ⇒ 抛错,由调用方 fail-open(绝不阻塞对话);
|
|
11
|
+
* ② **超时不排队**(方案 §3.4:发出即设 250 ms 超时并放弃,桥侧不做补偿)——
|
|
12
|
+
* 单飞守卫:上一发未回则本轮直接跳过,不堆叠请求;
|
|
13
|
+
* ③ 只允许回环地址(非 127.0.0.1 / localhost / ::1 一律拒,绝不给桥改绑非回环的机会);
|
|
14
|
+
* ④ 探活结果(含**失败**)按 TTL 缓存 ⇒ 桥不在时不会每轮都连打 10 个端口。
|
|
15
|
+
*/
|
|
16
|
+
const LOOPBACK = /^(127\.0\.0\.1|localhost|::1)$/;
|
|
17
|
+
|
|
18
|
+
export function createBridgeClient({
|
|
19
|
+
host = '127.0.0.1',
|
|
20
|
+
port = 8201,
|
|
21
|
+
portScanMax = 10,
|
|
22
|
+
encodeTimeoutMs = 250,
|
|
23
|
+
healthTimeoutMs = 120,
|
|
24
|
+
healthTtlMs = 10000,
|
|
25
|
+
} = {}) {
|
|
26
|
+
if (!LOOPBACK.test(String(host))) {
|
|
27
|
+
throw new Error(`编码桥只允许回环地址(方案 §3.1),收到 host=${host}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let cache = null; // { ok, port, dim, model, at, error }
|
|
31
|
+
let inFlight = false;
|
|
32
|
+
let encodeCalls = 0;
|
|
33
|
+
|
|
34
|
+
const fetchJson = async (url, init, ms) => {
|
|
35
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(ms) });
|
|
36
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
37
|
+
return await res.json();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** 探活:从 port 起顺延,找第一个 ready 的桥;成功与失败都缓存 TTL。 */
|
|
41
|
+
const probe = async (force = false) => {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (!force && cache && now - cache.at < healthTtlMs) return cache;
|
|
44
|
+
let lastErr = 'unknown';
|
|
45
|
+
for (let p = port; p <= port + portScanMax; p += 1) {
|
|
46
|
+
try {
|
|
47
|
+
const h = await fetchJson(`http://${host}:${p}/health`, {}, healthTimeoutMs);
|
|
48
|
+
cache = {
|
|
49
|
+
ok: Boolean(h && h.ready),
|
|
50
|
+
port: p,
|
|
51
|
+
dim: Number(h && h.dim) || 0,
|
|
52
|
+
model: (h && h.model) || null,
|
|
53
|
+
modelLoading: Boolean(h && h.modelLoading),
|
|
54
|
+
at: Date.now(),
|
|
55
|
+
error: h && h.ready ? null : 'model-not-ready',
|
|
56
|
+
};
|
|
57
|
+
return cache;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
lastErr = String(e && e.message ? e.message : e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
cache = { ok: false, port: null, dim: 0, model: null, at: Date.now(), error: lastErr };
|
|
63
|
+
return cache;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const encode = async (text) => {
|
|
67
|
+
const h = await probe();
|
|
68
|
+
if (!h || !h.ok) throw new Error(`编码桥不可用:${(h && h.error) || 'not ready'}`);
|
|
69
|
+
if (inFlight) throw new Error('编码桥上一发未返回(超时不排队,本轮跳过)');
|
|
70
|
+
inFlight = true;
|
|
71
|
+
try {
|
|
72
|
+
encodeCalls += 1;
|
|
73
|
+
const out = await fetchJson(
|
|
74
|
+
`http://${host}:${h.port}/encode`,
|
|
75
|
+
{
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'content-type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({ texts: [String(text ?? '')] }),
|
|
79
|
+
},
|
|
80
|
+
encodeTimeoutMs,
|
|
81
|
+
);
|
|
82
|
+
const vec = out && out.vectors && out.vectors[0];
|
|
83
|
+
if (!vec) throw new Error('编码桥返回空向量');
|
|
84
|
+
return Float32Array.from(vec);
|
|
85
|
+
} finally {
|
|
86
|
+
inFlight = false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
kind: 'bridge',
|
|
92
|
+
host,
|
|
93
|
+
port,
|
|
94
|
+
describe: () => ({ kind: 'bridge', host, port, encodeTimeoutMs, healthTtlMs, encodeCalls }),
|
|
95
|
+
probe,
|
|
96
|
+
health: () => probe(),
|
|
97
|
+
stats: () => ({ encodeCalls, inFlight }),
|
|
98
|
+
get dim() {
|
|
99
|
+
return cache ? cache.dim : 0;
|
|
100
|
+
},
|
|
101
|
+
get modelId() {
|
|
102
|
+
return cache ? cache.model : null;
|
|
103
|
+
},
|
|
104
|
+
encode,
|
|
105
|
+
// 桥一次一条(召回只需 1 条查询向量)
|
|
106
|
+
encodeBatch: async (texts) => Promise.all(texts.map((t) => encode(t))),
|
|
107
|
+
};
|
|
108
|
+
}
|