dsh-memory-tiers 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +420 -2460
- package/README.md +140 -268
- package/client.mjs +701 -608
- package/index.mjs +939 -826
- package/{memory5.mjs → memory.mjs} +5280 -3403
- package/package.json +61 -63
- package/settings.mjs +335 -327
- package/silicon.mjs +24 -22
- package/memory6.mjs +0 -1553
- package/reindex.mjs +0 -162
package/reindex.mjs
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 重建向量库 —— 换 embedding 模型后必须跑一次
|
|
4
|
-
*
|
|
5
|
-
* 为什么需要它:
|
|
6
|
-
* 向量是模型输出的坐标,换模型 = 换坐标系。旧坐标在新坐标系里没有任何意义。
|
|
7
|
-
* 更麻烦的是常用的几个模型(Qwen3-Embedding-0.6B / bge-m3)**都是 1024 维**,
|
|
8
|
-
* 维度检查发现不了,cosine() 会照常算出数字——不报错,只是越来越不准。
|
|
9
|
-
*
|
|
10
|
-
* 用法:
|
|
11
|
-
* node reindex.mjs # 用默认目录 ~/.dsh/storages/memory-v5
|
|
12
|
-
* node reindex.mjs --dir D:/my/memory # 指定库目录
|
|
13
|
-
* node reindex.mjs --dry # 只体检:库里有几条、当前用什么模型、要嵌多少
|
|
14
|
-
* node reindex.mjs --force # 不备份旧向量(默认会先改名隔离)
|
|
15
|
-
*
|
|
16
|
-
* 跑完重启 dsh 生效。
|
|
17
|
-
*/
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, renameSync } from 'node:fs'
|
|
19
|
-
import { join } from 'node:path'
|
|
20
|
-
import { homedir } from 'node:os'
|
|
21
|
-
import { createSilicon } from './silicon.mjs'
|
|
22
|
-
|
|
23
|
-
const VEC_SHARD_SIZE = 5000
|
|
24
|
-
const VEC_SHARD_RE = /^vectors-part-(\d{4})\.json$/
|
|
25
|
-
const vecShardName = (i) => `vectors-part-${String(i).padStart(4, '0')}.json`
|
|
26
|
-
|
|
27
|
-
// 和 memory5.embedOne / embedMany 保持一致:单条最多 512 字。
|
|
28
|
-
// 口径不一致的话,重建出来的向量和线上新写入的向量不是同一个分布。
|
|
29
|
-
const TEXT_LIMIT = 512
|
|
30
|
-
|
|
31
|
-
function arg(name) {
|
|
32
|
-
const i = process.argv.indexOf(name)
|
|
33
|
-
return i >= 0 ? process.argv[i + 1] : null
|
|
34
|
-
}
|
|
35
|
-
const has = (name) => process.argv.includes(name)
|
|
36
|
-
|
|
37
|
-
const dir = arg('--dir') || join(homedir(), '.dsh', 'storages', 'memory-v5')
|
|
38
|
-
const dry = has('--dry')
|
|
39
|
-
const force = has('--force')
|
|
40
|
-
|
|
41
|
-
function die(msg) {
|
|
42
|
-
console.error(`\n✗ ${msg}\n`)
|
|
43
|
-
process.exit(1)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!existsSync(join(dir, 'store.json'))) die(`找不到库:${dir}/store.json(用 --dir 指定正确目录)`)
|
|
47
|
-
|
|
48
|
-
const cfgPath = join(dir, 'config.json')
|
|
49
|
-
const cfg = existsSync(cfgPath) ? JSON.parse(readFileSync(cfgPath, 'utf8')) : {}
|
|
50
|
-
const apiKey = cfg.apiKey || process.env.SILICONFLOW_API_KEY
|
|
51
|
-
const base = cfg.apiBase || 'https://api.siliconflow.cn/v1'
|
|
52
|
-
|
|
53
|
-
if (!apiKey) {
|
|
54
|
-
die(`没配 apiKey(${cfgPath} 的 "apiKey" 或环境变量 SILICONFLOW_API_KEY)。
|
|
55
|
-
没有 key 就没有 embedding 模型,重建无从谈起(v0.5.2 起已不再用本地哈希向量兜底)。`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let silicon
|
|
59
|
-
try {
|
|
60
|
-
silicon = createSilicon({ apiKey, base, models: cfg.models || {} })
|
|
61
|
-
} catch (err) {
|
|
62
|
-
die(`模型层初始化失败:${err?.message ?? err}`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const stage = silicon.resolveStage('embed')
|
|
66
|
-
const signature = `api:${stage.model}`
|
|
67
|
-
|
|
68
|
-
const store = JSON.parse(readFileSync(join(dir, 'store.json'), 'utf8'))
|
|
69
|
-
const items = (store.items || []).filter((it) => it && it.id && String(it.text || '').trim())
|
|
70
|
-
|
|
71
|
-
console.log('库目录 ', dir)
|
|
72
|
-
console.log('条目 ', items.length, '条有文本')
|
|
73
|
-
console.log('向量模型 ', stage.model, '@', stage.base)
|
|
74
|
-
console.log('指纹 ', signature)
|
|
75
|
-
|
|
76
|
-
// 现有向量的指纹(给「要不要重建」一个判断依据)
|
|
77
|
-
const metaPath = join(dir, 'vectors.meta.json')
|
|
78
|
-
let meta = null
|
|
79
|
-
try { meta = JSON.parse(readFileSync(metaPath, 'utf8')) } catch { /* 没有就是老库 */ }
|
|
80
|
-
if (meta) console.log('现有向量 ', `${meta.count} 条 / ${meta.dim} 维 / 来自 ${meta.sig || '(未知)'}`)
|
|
81
|
-
else console.log('现有向量 ', '(没有指纹文件,视为旧库)')
|
|
82
|
-
|
|
83
|
-
if (meta && meta.sig === signature && !has('--rebuild')) {
|
|
84
|
-
console.log('\n✓ 指纹已经一致,不需要重建(确实想重嵌加 --rebuild)')
|
|
85
|
-
process.exit(0)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (dry) {
|
|
89
|
-
console.log('\n--dry:不写任何文件。去掉 --dry 才会真正重嵌。')
|
|
90
|
-
process.exit(0)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── 备份旧向量(默认做,--force 跳过)──────────────────────────────
|
|
94
|
-
if (!force) {
|
|
95
|
-
const files = readdirSync(dir).filter((f) => VEC_SHARD_RE.test(f) || f === 'vectors.meta.json')
|
|
96
|
-
if (files.length) {
|
|
97
|
-
const bak = join(dir, `vectors.stale-${new Date().toISOString().replace(/[:.]/g, '-')}`)
|
|
98
|
-
mkdirSync(bak, { recursive: true })
|
|
99
|
-
for (const f of files) {
|
|
100
|
-
try { renameSync(join(dir, f), join(bak, f)) } catch { /* 单个失败不影响后续 */ }
|
|
101
|
-
}
|
|
102
|
-
console.log(`\n旧向量已备份到 ${bak}(${files.length} 个文件)`)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── 重嵌 ────────────────────────────────────────────────────────────
|
|
107
|
-
const t0 = Date.now()
|
|
108
|
-
const vectors = []
|
|
109
|
-
const BATCH = 32
|
|
110
|
-
let done = 0
|
|
111
|
-
let failed = 0
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < items.length; i += BATCH) {
|
|
114
|
-
const slice = items.slice(i, i + BATCH)
|
|
115
|
-
let vecs = null
|
|
116
|
-
try {
|
|
117
|
-
vecs = await silicon.embed(slice.map((it) => String(it.text).slice(0, TEXT_LIMIT)))
|
|
118
|
-
} catch (err) {
|
|
119
|
-
console.warn(` 批 ${i / BATCH + 1} 失败:${err?.message ?? err}`)
|
|
120
|
-
}
|
|
121
|
-
for (let j = 0; j < slice.length; j++) {
|
|
122
|
-
const v = vecs?.[j]
|
|
123
|
-
if (v && v.length) vectors.push([slice[j].id, Float32Array.from(v)])
|
|
124
|
-
else failed++
|
|
125
|
-
}
|
|
126
|
-
done += slice.length
|
|
127
|
-
process.stdout.write(`\r 已处理 ${done}/${items.length} 失败 ${failed}`)
|
|
128
|
-
}
|
|
129
|
-
process.stdout.write('\n')
|
|
130
|
-
|
|
131
|
-
if (!vectors.length) die('一条都没嵌成功,检查 key / 模型名 / 网络后重试')
|
|
132
|
-
|
|
133
|
-
// ── 写分片(格式与 memory5 一致:base64 的 Float32Array)──────────────
|
|
134
|
-
const shardCount = Math.ceil(vectors.length / VEC_SHARD_SIZE)
|
|
135
|
-
for (let i = 0; i < shardCount; i++) {
|
|
136
|
-
const out = {}
|
|
137
|
-
for (const [k, v] of vectors.slice(i * VEC_SHARD_SIZE, (i + 1) * VEC_SHARD_SIZE)) {
|
|
138
|
-
out[k] = Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('base64')
|
|
139
|
-
}
|
|
140
|
-
writeFileSync(join(dir, vecShardName(i)), JSON.stringify(out), 'utf8')
|
|
141
|
-
}
|
|
142
|
-
// 清掉多余的旧片(条目变少时)
|
|
143
|
-
for (const f of readdirSync(dir)) {
|
|
144
|
-
const m = f.match(VEC_SHARD_RE)
|
|
145
|
-
if (m && Number(m[1]) >= shardCount) {
|
|
146
|
-
try { renameSync(join(dir, f), join(dir, f + '.old')) } catch { /* 忽略 */ }
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
writeFileSync(metaPath, JSON.stringify({
|
|
151
|
-
sig: signature,
|
|
152
|
-
dim: vectors[0][1].length,
|
|
153
|
-
count: vectors.length,
|
|
154
|
-
at: new Date().toISOString(),
|
|
155
|
-
}, null, 1), 'utf8')
|
|
156
|
-
|
|
157
|
-
const secs = ((Date.now() - t0) / 1000).toFixed(1)
|
|
158
|
-
const usage = silicon.usage?.()
|
|
159
|
-
console.log(`\n✓ 重建完成:${vectors.length} 条 / ${vectors[0][1].length} 维 / 用时 ${secs}s`)
|
|
160
|
-
if (failed) console.log(` ⚠ ${failed} 条嵌入失败,这些条目暂时只能靠关键词命中,重跑一次通常会补上`)
|
|
161
|
-
if (usage?.tokens) console.log(` 用量约 ${usage.tokens} token`)
|
|
162
|
-
console.log('\n重启 dsh 生效。')
|