dsh-memu-memory 1.1.2 → 1.2.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/AGENTS.md +8 -6
- package/README.md +50 -5
- package/README.zh.md +49 -5
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +6 -4
- package/docs/emotion-model-spec.md +281 -0
- package/docs/env-and-commands.md +13 -0
- package/docs/host-api.md +35 -3
- package/docs/memory-semantics.md +40 -11
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +395 -366
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +635 -0
- package/lib/client.js +728 -58
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +115 -0
- package/lib/curator.js +81 -37
- package/lib/cycle.js +43 -6
- package/lib/embedding-catalog.js +763 -0
- package/lib/embedding-download.js +407 -0
- package/lib/embedding-runtime.js +710 -0
- package/lib/engine.js +2 -2
- package/lib/index.js +36 -0
- package/lib/inject.js +232 -103
- package/lib/local-embedding-entry.js +145 -0
- package/lib/local-embedding-manager.js +267 -0
- package/lib/local-embedding-protocol.js +173 -0
- package/lib/local-embedding-worker.js +652 -0
- package/lib/local-vector-service.js +283 -0
- package/lib/memory.js +599 -57
- package/lib/memu-config.js +10 -22
- package/lib/memu.js +53 -19
- package/lib/mood.js +268 -39
- package/lib/paths.js +24 -0
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +450 -29
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +54 -5
- package/lib/state.js +137 -172
- package/lib/tools.js +58 -7
- package/lib/vector-migration.js +878 -0
- package/lib/vector-profile.js +968 -0
- package/package.json +4 -2
- package/scripts/acceptance-local.mjs +7 -5
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +537 -0
- package/scripts/check-all.mjs +29 -1
- package/scripts/check-doc-drift.mjs +353 -0
- package/scripts/diagnose-bindings.mjs +176 -0
- package/scripts/local-runtime-install.mjs +142 -0
- package/scripts/local-runtime-verify.mjs +56 -0
- package/scripts/local-vector-verify.mjs +93 -0
- package/scripts/recall-bench.mjs +813 -0
- package/scripts/repetition-ab-scenarios.mjs +93 -0
- package/scripts/repetition-detector-snapshot.mjs +314 -0
- package/scripts/repetition-eval-live.mjs +360 -0
- package/scripts/repetition-eval.mjs +702 -0
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +212 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +320 -0
- package/tests/consolidation-fork.test.mjs +357 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +321 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +77 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +92 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +536 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +208 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +733 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +164 -0
- package/tests/windows-env.test.mjs +89 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
// §14.4 模型目录:只读、无副作用。目录里的每个事实都带来源;没有核实过的制品一律
|
|
4
|
+
// availability: 'pending' 且 files: []——绝不写猜测的 bytes/SHA-256(§14.4/§14.12)。
|
|
5
|
+
// 这里不做下载、不做安装、不读不写任何 profile/active 状态(那是 embedding-download 与
|
|
6
|
+
// 后续迁移模块的事)。
|
|
7
|
+
|
|
8
|
+
export const CATALOG_SCHEMA_VERSION = 1
|
|
9
|
+
|
|
10
|
+
// 默认来源顺序;只有 manifest 里显式写了 repo+revision 的来源才会生成 URL。
|
|
11
|
+
export const PROVIDER_ORDER = Object.freeze(['modelscope', 'hf-mirror', 'huggingface'])
|
|
12
|
+
|
|
13
|
+
export const PROVIDERS = Object.freeze({
|
|
14
|
+
modelscope: Object.freeze({
|
|
15
|
+
id: 'modelscope',
|
|
16
|
+
label: 'ModelScope',
|
|
17
|
+
url: ({ repo, revision, path }) => `https://modelscope.cn/api/v1/models/${repo}/repo?Revision=${revision}&FilePath=${encodeURIComponent(path)}`,
|
|
18
|
+
}),
|
|
19
|
+
'hf-mirror': Object.freeze({
|
|
20
|
+
id: 'hf-mirror',
|
|
21
|
+
label: 'hf-mirror',
|
|
22
|
+
url: ({ repo, revision, path }) => `https://hf-mirror.com/${repo}/resolve/${revision}/${encodePath(path)}`,
|
|
23
|
+
}),
|
|
24
|
+
huggingface: Object.freeze({
|
|
25
|
+
id: 'huggingface',
|
|
26
|
+
label: 'Hugging Face',
|
|
27
|
+
url: ({ repo, revision, path }) => `https://huggingface.co/${repo}/resolve/${revision}/${encodePath(path)}`,
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export const FILE_ROLES = Object.freeze(['graph', 'config', 'tokenizer', 'tokenizer-config', 'special-tokens', 'vocab', 'quantization', 'pooling-config', 'notice'])
|
|
32
|
+
|
|
33
|
+
const ONNX_DTYPES = Object.freeze({
|
|
34
|
+
1: 'float32', 2: 'uint8', 3: 'int8', 4: 'uint16', 5: 'int16', 6: 'int32', 7: 'int64',
|
|
35
|
+
8: 'string', 9: 'bool', 10: 'float16', 11: 'double', 12: 'uint32', 13: 'uint64',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function encodePath(filePath) {
|
|
39
|
+
return String(filePath).split('/').map(encodeURIComponent).join('/')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sha256Of(value) {
|
|
43
|
+
return crypto.createHash('sha256').update(value).digest('hex')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canonical(value) {
|
|
47
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
|
|
48
|
+
if (value && typeof value === 'object') {
|
|
49
|
+
return `{${Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(value ?? null)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deepFreeze(value) {
|
|
55
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
56
|
+
Object.freeze(value)
|
|
57
|
+
for (const key of Object.keys(value)) deepFreeze(value[key])
|
|
58
|
+
}
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 核实证据(2026-10-30):ModelScope 可直连;huggingface.co 与 hf-mirror.com 在本机
|
|
63
|
+
// fetch failed。逐文件 SHA-256 取自 ModelScope 文件清单,并与实际下载的字节复核一致,
|
|
64
|
+
// 证据留在 .tmp-local-vector-20261030/probe/。
|
|
65
|
+
const BGE_REPO = 'Xenova/bge-small-zh-v1.5'
|
|
66
|
+
const BGE_REVISION = '313386ad7ffad360a4870fd1910f8ecaf1a05151'
|
|
67
|
+
const BGE_COMMIT_TIME = '2025-07-22T11:41:37.000Z'
|
|
68
|
+
const BAAI_REPO = 'BAAI/bge-small-zh-v1.5'
|
|
69
|
+
const BAAI_POOLING_REVISION = '8399f11f8da998fe932df2684586c92024219d05'
|
|
70
|
+
|
|
71
|
+
const rawCatalog = [
|
|
72
|
+
{
|
|
73
|
+
id: 'bge-small-zh-v1.5-int8',
|
|
74
|
+
displayName: 'BGE small zh v1.5 (int8 ONNX)',
|
|
75
|
+
summary: '中文优先的检索小模型;BGE v1.5 检索无需 instruction,CLS pooling + L2 归一化,512 维。',
|
|
76
|
+
languages: ['zh', 'en'],
|
|
77
|
+
tier: 'preferred',
|
|
78
|
+
availability: 'available',
|
|
79
|
+
pendingReasons: [],
|
|
80
|
+
parameters: 24000000,
|
|
81
|
+
parameterSource: 'BAAI 官方模型页 24M(docs/local-vector-construction.md §14.4 引用)',
|
|
82
|
+
dimension: 512,
|
|
83
|
+
maxTokens: 512,
|
|
84
|
+
pooling: 'cls',
|
|
85
|
+
normalization: 'l2',
|
|
86
|
+
prefixes: { query: '', document: '' },
|
|
87
|
+
quantization: {
|
|
88
|
+
weights: 'qint8',
|
|
89
|
+
scheme: 'dynamic-per-channel',
|
|
90
|
+
reduceRange: true,
|
|
91
|
+
source: 'quantize_config.json(per_channel=true, reduce_range=true, weight_type=QInt8) + 图内 MatMulInteger/DynamicQuantizeLinear',
|
|
92
|
+
},
|
|
93
|
+
runtime: {
|
|
94
|
+
engine: 'onnxruntime-node',
|
|
95
|
+
version: '1.31.1',
|
|
96
|
+
abi: 'napi-v6',
|
|
97
|
+
os: 'win32',
|
|
98
|
+
arch: 'x64',
|
|
99
|
+
verified: true,
|
|
100
|
+
tokenizer: { name: '@huggingface/tokenizers', version: '0.2.0', license: 'Apache-2.0', sha256: 'f6b9849b58de625c4fd79583242dd4506914b92082f9af17719d39416658ef24' },
|
|
101
|
+
packages: [
|
|
102
|
+
{ name: 'onnxruntime-node', version: '1.31.1', license: 'MIT', sha256: '41aedb3e877786245ab4dc0f533cda63f7d971b5c5fec171eafca9fb9572a8d5' },
|
|
103
|
+
{ name: 'onnxruntime-common', version: '1.31.1', license: 'MIT', sha256: 'c8e59240260cd3d230f1417765e0dbe098323ef65a5785991059de1bd5d7cde0' },
|
|
104
|
+
],
|
|
105
|
+
note: '隔离目录真实 ORT CPU 加载/推理 smoke 已核实;仅允许固定版本,不隐式下载。',
|
|
106
|
+
// 受管安装事实(2026-10-30):三个 npm tarball 本轮真实下载并核对 sha256/bytes;
|
|
107
|
+
// 解包后的最小运行时树跑通真实 /v1/embeddings 推理(512 维、worker ready ≈559ms)。
|
|
108
|
+
// 只允许从这些 URL 下载并按下面的 hash 校验;没有 hash 或没有该平台原生库一律拒绝安装。
|
|
109
|
+
install: {
|
|
110
|
+
scope: 'per-user-managed',
|
|
111
|
+
layout: 'node_modules',
|
|
112
|
+
postinstall: 'never-run',
|
|
113
|
+
verifiedAt: '2026-10-30T03:45:00.000Z',
|
|
114
|
+
verifiedOn: 'win32-x64',
|
|
115
|
+
platforms: {
|
|
116
|
+
'win32-x64': 'smoke-verified',
|
|
117
|
+
'win32-arm64': 'artifact-pinned',
|
|
118
|
+
'linux-x64': 'artifact-pinned',
|
|
119
|
+
'linux-arm64': 'artifact-pinned',
|
|
120
|
+
'darwin-arm64': 'artifact-pinned',
|
|
121
|
+
},
|
|
122
|
+
evidence: 'npm tarball sha256 与 registry integrity 双向一致;解包最小树真实推理通过;原生库逐文件 sha256 取自同一棵已核实树',
|
|
123
|
+
},
|
|
124
|
+
artifacts: [
|
|
125
|
+
{
|
|
126
|
+
name: 'onnxruntime-node', version: '1.31.1', license: 'MIT',
|
|
127
|
+
url: 'https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.31.1.tgz',
|
|
128
|
+
bytes: 115391311,
|
|
129
|
+
sha256: 'd7a3de2b1bba92614abc74af909cc0ba4f4dd5347735418794edeb58cfef2d22',
|
|
130
|
+
integrity: 'sha512-ijnSghbDHVWaQ2nDPmVccPlCHI8kH1Mx1aJ4a/qzRP16bBcVs4E3EqZQKZCkhWnbqN1tuQWxagimFtDpbPYYQw==',
|
|
131
|
+
extract: ['package.json', 'README.md', 'dist', 'lib', 'bin/napi-v6/<platform>'],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'onnxruntime-common', version: '1.31.1', license: 'MIT',
|
|
135
|
+
url: 'https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.31.1.tgz',
|
|
136
|
+
bytes: 72322,
|
|
137
|
+
sha256: '967acac0c2a22122461e526aeda07305da7289d94f276920d8a6919483f07bfc',
|
|
138
|
+
integrity: 'sha512-F+nnYNiQtmNJ95V6R/isBJqiVpdANCrsMEg1OaF+Bkbl3GAtzWLR7Ng17Ld0QtYtYJyotOHPJzOQn1Y6PLZjpg==',
|
|
139
|
+
extract: ['package.json', 'README.md', 'dist', 'lib'],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: '@huggingface/tokenizers', version: '0.2.0', license: 'Apache-2.0',
|
|
143
|
+
url: 'https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.2.0.tgz',
|
|
144
|
+
bytes: 87011,
|
|
145
|
+
sha256: '2800014c83b8186b2e4210c20672f1fb65a12cef85d7e95e0f2003e022611b4a',
|
|
146
|
+
integrity: 'sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg==',
|
|
147
|
+
extract: ['package.json', 'README.md', 'LICENSE', 'dist', 'types'],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
// 解包后的 package.json 事实(字节数 + sha256):用于确认解出来的就是 pin 住的版本。
|
|
151
|
+
packageFiles: [
|
|
152
|
+
{ package: 'onnxruntime-node', bytes: 1593, sha256: '41aedb3e877786245ab4dc0f533cda63f7d971b5c5fec171eafca9fb9572a8d5' },
|
|
153
|
+
{ package: 'onnxruntime-common', bytes: 977, sha256: 'c8e59240260cd3d230f1417765e0dbe098323ef65a5785991059de1bd5d7cde0' },
|
|
154
|
+
{ package: '@huggingface/tokenizers', bytes: 1965, sha256: 'f6b9849b58de625c4fd79583242dd4506914b92082f9af17719d39416658ef24' },
|
|
155
|
+
],
|
|
156
|
+
// 判分/加载必需的原生库逐文件 hash。没有列出原生库的平台不得安装(不允许猜)。
|
|
157
|
+
native: {
|
|
158
|
+
'win32-x64': [
|
|
159
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/x64/onnxruntime_binding.node', bytes: 358200, sha256: '8d9f996ff7dcc60c4899aa87aa7abe07d806f9c314882f44a001d0fd8a23d16f' },
|
|
160
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/x64/onnxruntime.dll', bytes: 31736672, sha256: 'beb6e1edba0e74c132453fe4df113da2ea2b99ef8ea7dfc25696a872efd11c45' },
|
|
161
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/x64/DirectML.dll', bytes: 18527544, sha256: 'd16be72fa002dd610cca93e376f0ef44865dfce43de4b7bf9738af22534f7a9d' },
|
|
162
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/x64/dxcompiler.dll', bytes: 17986400, sha256: '430eed027979399769a325b60319aca4e4afa4daebe475d3c763dcf2a0190a72' },
|
|
163
|
+
],
|
|
164
|
+
'win32-arm64': [
|
|
165
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/arm64/onnxruntime_binding.node', bytes: 504160, sha256: '6f34d4635793371c85e5260dbdc0864c2ef62ccfcb1f439afa3501ba6f8d530d' },
|
|
166
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/arm64/onnxruntime.dll', bytes: 32835936, sha256: '14d1cd398fa5bc8b5c61bdfba899f17b40550012ec2884955ce656a5d8a8ff77' },
|
|
167
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/arm64/DirectML.dll', bytes: 18444600, sha256: '4bb0639eb23a02de2c5f1eddd117f13330819f55b556e8c75068b68ad1a3516e' },
|
|
168
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/arm64/dxcompiler.dll', bytes: 22285624, sha256: '4a77bcadf4d437ad50d10517bb0b4a5a4c2fc84b1fde4d1cf5a5abe9e2dce431' },
|
|
169
|
+
],
|
|
170
|
+
'linux-x64': [
|
|
171
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/linux/x64/onnxruntime_binding.node', bytes: 519712, sha256: 'e3a108803b8bd33c8e9691b52921c37bede7a6886251aa41219f053d268ee231' },
|
|
172
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/linux/x64/libonnxruntime.so.1', bytes: 47552704, sha256: 'f19a4da48c4c65e0cdccff82354482f657fa91a3455b66174195b94ca55b0e6f' },
|
|
173
|
+
],
|
|
174
|
+
'linux-arm64': [
|
|
175
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/linux/arm64/onnxruntime_binding.node', bytes: 551752, sha256: '5d943f6bf8f65c40213bac636f19bcb227ee1d188417c4b6a6f70062579cfb84' },
|
|
176
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/linux/arm64/libonnxruntime.so.1', bytes: 25266408, sha256: '152fb7d2f4f06acc5a38627fbca3e56ba2a9b984cb060d2ac500ac272afbb9ba' },
|
|
177
|
+
],
|
|
178
|
+
'darwin-arm64': [
|
|
179
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/darwin/arm64/onnxruntime_binding.node', bytes: 323512, sha256: '95ab5698fbcb34d035ea98266156ad5a06b76d0ee739ea42d660df6901df390a' },
|
|
180
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/darwin/arm64/libonnxruntime.1.dylib', bytes: 45331560, sha256: '9a362365243b56da98a2ad65128a32d6f8066d1f39ce697e0fce997adf0586dc' },
|
|
181
|
+
{ path: 'node_modules/onnxruntime-node/bin/napi-v6/darwin/arm64/libonnxruntime.1.31.1.dylib', bytes: 45331560, sha256: '9a362365243b56da98a2ad65128a32d6f8066d1f39ce697e0fce997adf0586dc' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
license: {
|
|
186
|
+
base: { id: 'mit', source: 'ModelScope BAAI/bge-small-zh-v1.5 的 License 字段 = mit(2026-10-30 只读核对)' },
|
|
187
|
+
onnxExport: { id: 'undeclared', source: 'ModelScope Xenova/bge-small-zh-v1.5 的 License 字段为空,ONNX 导出未单独声明许可(2026-10-30 核对)' },
|
|
188
|
+
},
|
|
189
|
+
verification: {
|
|
190
|
+
status: 'verified',
|
|
191
|
+
method: 'sha256+onnx-header',
|
|
192
|
+
checkedAt: '2026-10-30T00:20:00.000Z',
|
|
193
|
+
scope: '逐文件 bytes/sha256 复核 + ONNX 图头(opset/输入输出/dtype/形状/自包含)+ config/tokenizer 解析',
|
|
194
|
+
evidence: '.tmp-local-vector-20261030/probe/ 下的实测文件,sha256 与 ModelScope 清单逐字节一致',
|
|
195
|
+
notVerified: ['huggingface/hf-mirror 上的对应 commit'],
|
|
196
|
+
},
|
|
197
|
+
sources: {
|
|
198
|
+
modelscope: { repo: BGE_REPO, revision: BGE_REVISION, revisionVerified: true, commitTime: BGE_COMMIT_TIME },
|
|
199
|
+
'hf-mirror': { repo: BGE_REPO, revision: null, revisionVerified: false, note: 'hf-mirror.com 本机不可达,commit 未核实;不生成 URL' },
|
|
200
|
+
huggingface: { repo: BGE_REPO, revision: null, revisionVerified: false, note: 'huggingface.co 本机不可达,commit 未核实;不生成 URL' },
|
|
201
|
+
},
|
|
202
|
+
witness: {
|
|
203
|
+
provider: 'modelscope',
|
|
204
|
+
repo: BAAI_REPO,
|
|
205
|
+
revision: BAAI_POOLING_REVISION,
|
|
206
|
+
path: '1_Pooling/config.json',
|
|
207
|
+
bytes: 190,
|
|
208
|
+
sha256: 'aaa8861589f80c961a03cc86c7eeaef7605c1676b9ab55329d33a304738769c6',
|
|
209
|
+
purpose: 'pooling 事实来源:pooling_mode_cls_token=true、word_embedding_dimension=512',
|
|
210
|
+
},
|
|
211
|
+
graph: {
|
|
212
|
+
path: 'onnx/model_quantized.onnx',
|
|
213
|
+
opset: 11,
|
|
214
|
+
producer: 'onnx.quantize',
|
|
215
|
+
producerVersion: '0.1.0',
|
|
216
|
+
selfContained: true,
|
|
217
|
+
externalData: [],
|
|
218
|
+
inputs: [
|
|
219
|
+
{ name: 'input_ids', dtype: 'int64', shape: ['batch_size', 'sequence_length'] },
|
|
220
|
+
{ name: 'attention_mask', dtype: 'int64', shape: ['batch_size', 'sequence_length'] },
|
|
221
|
+
{ name: 'token_type_ids', dtype: 'int64', shape: ['batch_size', 'sequence_length'] },
|
|
222
|
+
],
|
|
223
|
+
outputs: [{ name: 'last_hidden_state', dtype: 'float32', shape: ['batch_size', 'sequence_length', 512] }],
|
|
224
|
+
},
|
|
225
|
+
files: [
|
|
226
|
+
{ path: 'config.json', role: 'config', bytes: 716, sha256: 'd4193ead3a810fd694fa8a31d7fc72fbaebc0668b603e398734bf2f6538ff42f' },
|
|
227
|
+
{ path: 'tokenizer.json', role: 'tokenizer', bytes: 439125, sha256: '48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26' },
|
|
228
|
+
{ path: 'tokenizer_config.json', role: 'tokenizer-config', bytes: 367, sha256: 'e6f3b96db926a37d4039995fbf5ad17de158dfb8f6343d607e4dbaad18d75f5a' },
|
|
229
|
+
{ path: 'special_tokens_map.json', role: 'special-tokens', bytes: 125, sha256: 'b6d346be366a7d1d48332dbc9fdf3bf8960b5d879522b7799ddba59e76237ee3' },
|
|
230
|
+
{ path: 'vocab.txt', role: 'vocab', bytes: 109540, sha256: '45bbac6b341c319adc98a532532882e91a9cefc0329aa57bac9ae761c27b291c' },
|
|
231
|
+
{ path: 'onnx/model_quantized.onnx', role: 'graph', bytes: 24010842, sha256: '15b717c382bcb518ba457b93ea6850ede7f4f1cd8937454aa06972366cd19bcc' },
|
|
232
|
+
{ path: 'quantize_config.json', role: 'quantization', bytes: 674, sha256: '2cc488b20fa05fe86aba2fdc2be44d24827e11e2b7c7a0753d1427da6797b46f' },
|
|
233
|
+
{ path: 'README.md', role: 'notice', bytes: 1057, sha256: 'cb22054df1a98f7ee9d5b281d023e67c27cc316e1733baf03fc994d95c3048ab' },
|
|
234
|
+
{
|
|
235
|
+
path: '1_Pooling/config.json',
|
|
236
|
+
role: 'pooling-config',
|
|
237
|
+
bytes: 190,
|
|
238
|
+
sha256: 'aaa8861589f80c961a03cc86c7eeaef7605c1676b9ab55329d33a304738769c6',
|
|
239
|
+
origin: { provider: 'modelscope', repo: BAAI_REPO, revision: BAAI_POOLING_REVISION },
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'all-minilm-l6-v2-int8',
|
|
245
|
+
displayName: 'all-MiniLM-L6-v2 (int8 ONNX)',
|
|
246
|
+
summary: '英文句子相似度小模型;不是中文模型的等价替代(§14.4)。',
|
|
247
|
+
languages: ['en'],
|
|
248
|
+
tier: 'optional',
|
|
249
|
+
availability: 'pending',
|
|
250
|
+
pendingReasons: [
|
|
251
|
+
'ONNX 制品尺寸/hash 未核实(本包只核实首个 BGE 制品)',
|
|
252
|
+
'官方 wrapper 常用上限 256,不能按底座 512 推定,需在 config/tokenizer/实际输出三处确认',
|
|
253
|
+
'huggingface/hf-mirror 在本机不可达,未取得固定 commit',
|
|
254
|
+
],
|
|
255
|
+
parameters: 22000000,
|
|
256
|
+
parameterSource: '约 22M(§14.4 候选值,未复核)',
|
|
257
|
+
dimension: 384,
|
|
258
|
+
maxTokens: null,
|
|
259
|
+
pooling: 'mean',
|
|
260
|
+
normalization: 'l2',
|
|
261
|
+
prefixes: { query: '', document: '' },
|
|
262
|
+
quantization: { weights: 'qint8', scheme: 'unknown', reduceRange: null, source: '未核实' },
|
|
263
|
+
runtime: { engine: 'onnxruntime-node', version: null, abi: 'napi-v6', os: 'win32', arch: 'x64', verified: false, note: '同 BGE:ORT 未实测' },
|
|
264
|
+
license: {
|
|
265
|
+
base: { id: 'apache-2.0', source: 'ModelScope Xenova/all-MiniLM-L6-v2 的 License = apache-2.0(2026-10-30 只读核对)' },
|
|
266
|
+
onnxExport: { id: 'undeclared', source: 'ONNX 导出未单独声明许可' },
|
|
267
|
+
},
|
|
268
|
+
verification: { status: 'unverified', method: null, checkedAt: null, scope: '仅候选条目;没有可下载制品', evidence: null, notVerified: ['全部制品'] },
|
|
269
|
+
sources: {
|
|
270
|
+
modelscope: { repo: 'Xenova/all-MiniLM-L6-v2', revision: null, revisionVerified: false, note: '候选来源,未固定 commit' },
|
|
271
|
+
'hf-mirror': { repo: 'Xenova/all-MiniLM-L6-v2', revision: null, revisionVerified: false, note: '未核实' },
|
|
272
|
+
huggingface: { repo: 'sentence-transformers/all-MiniLM-L6-v2', revision: null, revisionVerified: false, note: '未核实' },
|
|
273
|
+
},
|
|
274
|
+
graph: null,
|
|
275
|
+
files: [],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'multilingual-e5-small',
|
|
279
|
+
displayName: 'multilingual-e5-small(待适配,初版不启用)',
|
|
280
|
+
summary: '多语言(约 118M),需要 query/passage 前缀;当前请求通道无 role 字段,不能靠文本猜角色(§14.4)。',
|
|
281
|
+
languages: ['zh', 'en', 'multilingual'],
|
|
282
|
+
tier: 'deferred',
|
|
283
|
+
availability: 'pending',
|
|
284
|
+
pendingReasons: [
|
|
285
|
+
'需要 query/passage 前缀,当前请求通道无 role 字段,不能安全启用',
|
|
286
|
+
'长度/量化/许可证制品均未核实',
|
|
287
|
+
'约 118M,int8 理论约 118 MB,不是 22–24M 级',
|
|
288
|
+
],
|
|
289
|
+
parameters: 118000000,
|
|
290
|
+
parameterSource: '§14.4 E5 作者模型表',
|
|
291
|
+
dimension: 384,
|
|
292
|
+
maxTokens: null,
|
|
293
|
+
pooling: 'mean',
|
|
294
|
+
normalization: 'l2',
|
|
295
|
+
prefixes: { query: 'query: ', document: 'passage: ' },
|
|
296
|
+
quantization: { weights: 'qint8', scheme: 'unknown', reduceRange: null, source: '未核实' },
|
|
297
|
+
runtime: { engine: 'onnxruntime-node', version: null, abi: 'napi-v6', os: 'win32', arch: 'x64', verified: false, note: '同 BGE:ORT 未实测' },
|
|
298
|
+
license: {
|
|
299
|
+
base: { id: 'unverified', source: '模型卡许可未核实(§14.4/§14.12)' },
|
|
300
|
+
onnxExport: { id: 'undeclared', source: '无已核实导出' },
|
|
301
|
+
},
|
|
302
|
+
verification: { status: 'unverified', method: null, checkedAt: null, scope: '仅目录占位;初版不启用', evidence: null, notVerified: ['全部制品'] },
|
|
303
|
+
sources: {
|
|
304
|
+
modelscope: { repo: 'intfloat/multilingual-e5-small', revision: null, revisionVerified: false, note: '候选来源,未固定 commit' },
|
|
305
|
+
'hf-mirror': { repo: 'intfloat/multilingual-e5-small', revision: null, revisionVerified: false, note: '未核实' },
|
|
306
|
+
huggingface: { repo: 'intfloat/multilingual-e5-small', revision: null, revisionVerified: false, note: '未核实' },
|
|
307
|
+
},
|
|
308
|
+
graph: null,
|
|
309
|
+
files: [],
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* profile fingerprint:任何会改变向量空间的字段(模型/revision/量化/pooling/tokenizer/
|
|
315
|
+
* 前缀/维度/分窗)都必须进指纹(§14.8)。
|
|
316
|
+
*/
|
|
317
|
+
export function profileFingerprint(entry) {
|
|
318
|
+
const files = Array.isArray(entry?.files) ? entry.files : []
|
|
319
|
+
const graphFile = files.find(file => file.role === 'graph') || null
|
|
320
|
+
const tokenizerFile = files.find(file => file.role === 'tokenizer') || null
|
|
321
|
+
const sources = entry?.sources && typeof entry.sources === 'object' ? entry.sources : {}
|
|
322
|
+
const primary = PROVIDER_ORDER.map(id => sources[id]).find(source => source && typeof source.revision === 'string') || null
|
|
323
|
+
return sha256Of(canonical({
|
|
324
|
+
schema: CATALOG_SCHEMA_VERSION,
|
|
325
|
+
id: entry?.id ?? null,
|
|
326
|
+
repo: primary?.repo ?? null,
|
|
327
|
+
revision: primary?.revision ?? null,
|
|
328
|
+
dimension: entry?.dimension ?? null,
|
|
329
|
+
maxTokens: entry?.maxTokens ?? null,
|
|
330
|
+
pooling: entry?.pooling ?? null,
|
|
331
|
+
normalization: entry?.normalization ?? null,
|
|
332
|
+
prefixes: entry?.prefixes ?? null,
|
|
333
|
+
quantization: entry?.quantization ? { weights: entry.quantization.weights, scheme: entry.quantization.scheme } : null,
|
|
334
|
+
graph: graphFile ? graphFile.sha256 : null,
|
|
335
|
+
tokenizer: tokenizerFile ? tokenizerFile.sha256 : null,
|
|
336
|
+
runtime: entry?.runtime ? { engine: entry.runtime.engine, abi: entry.runtime.abi, version: entry.runtime.version, os: entry.runtime.os, arch: entry.runtime.arch } : null,
|
|
337
|
+
files: files.map(({ path, role, bytes, sha256 }) => ({ path, role, bytes, sha256 })).sort((a, b) => a.path.localeCompare(b.path)),
|
|
338
|
+
graphProtocol: entry?.graph ?? null,
|
|
339
|
+
tokenizerProtocol: entry?.tokenizer ?? null,
|
|
340
|
+
preprocessing: entry?.preprocessing ?? null,
|
|
341
|
+
windowing: entry?.windowing ?? null,
|
|
342
|
+
aggregation: entry?.aggregation ?? null,
|
|
343
|
+
protocol: entry?.protocol ?? 'dsh-local-embedding-v1',
|
|
344
|
+
}))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const catalog = deepFreeze(rawCatalog.map(entry => ({
|
|
348
|
+
...entry,
|
|
349
|
+
schema: CATALOG_SCHEMA_VERSION,
|
|
350
|
+
totalBytes: (entry.files || []).reduce((sum, file) => sum + (Number.isFinite(file.bytes) ? file.bytes : 0), 0),
|
|
351
|
+
profileFingerprint: profileFingerprint(entry),
|
|
352
|
+
})))
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 把目录条目映射成"向量 profile 描述符"(供 lib/vector-profile.js 之类模块计算/比较指纹)。
|
|
356
|
+
* 目的:一处是模型事实(catalog),另一处是激活状态(profile),两边必须用同一份指纹,
|
|
357
|
+
* 否则安装/迁移会拿两套不可比的 hash 互相判等。这里只做映射,不读写任何 active 状态。
|
|
358
|
+
*/
|
|
359
|
+
export function toProfileDescriptor(entry) {
|
|
360
|
+
const files = Array.isArray(entry?.files) ? entry.files : []
|
|
361
|
+
const tokenizerFile = files.find(file => file.role === "tokenizer") || null
|
|
362
|
+
const sources = entry?.sources && typeof entry.sources === "object" ? entry.sources : {}
|
|
363
|
+
const primary = PROVIDER_ORDER.map(id => sources[id]).find(source => source && typeof source.revision === "string") || null
|
|
364
|
+
const runtime = entry?.runtime || {}
|
|
365
|
+
return {
|
|
366
|
+
id: entry?.id ?? "",
|
|
367
|
+
model: entry?.id ?? "",
|
|
368
|
+
repo: primary?.repo ?? "",
|
|
369
|
+
revision: primary?.revision ?? "",
|
|
370
|
+
runtime: runtime.version ? `${runtime.engine}@${runtime.version}` : String(runtime.engine || ""),
|
|
371
|
+
tokenizer: tokenizerFile ? tokenizerFile.sha256 : null,
|
|
372
|
+
preprocessing: {
|
|
373
|
+
pooling: entry?.pooling ?? null,
|
|
374
|
+
normalization: entry?.normalization ?? null,
|
|
375
|
+
prefixes: entry?.prefixes ?? null,
|
|
376
|
+
maxTokens: entry?.maxTokens ?? null,
|
|
377
|
+
truncation: "keep-head",
|
|
378
|
+
},
|
|
379
|
+
pooling: entry?.pooling ?? "",
|
|
380
|
+
normalization: entry?.normalization ?? "",
|
|
381
|
+
prefixes: entry?.prefixes ?? null,
|
|
382
|
+
maxTokens: entry?.maxTokens ?? null,
|
|
383
|
+
dimension: entry?.dimension ?? null,
|
|
384
|
+
protocol: "dsh-local-embedding-v1",
|
|
385
|
+
fingerprint: entry?.profileFingerprint ?? null,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** 只读目录快照;返回冻结数组,调用方改不到目录本体。 */
|
|
390
|
+
export function getEmbeddingCatalog() {
|
|
391
|
+
return catalog
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 按稳定 id 取单个模型;未知 id 返回 null(不猜测、不回退默认模型)。 */
|
|
395
|
+
export function getEmbeddingModel(id) {
|
|
396
|
+
if (typeof id !== 'string') return null
|
|
397
|
+
return catalog.find(entry => entry.id === id) || null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ---------------------------------------------------------------- ONNX 图头
|
|
401
|
+
|
|
402
|
+
function readVarint(buffer, offset) {
|
|
403
|
+
let result = 0
|
|
404
|
+
let shift = 0
|
|
405
|
+
let position = offset
|
|
406
|
+
for (;;) {
|
|
407
|
+
if (position >= buffer.length) throw new Error('truncated varint')
|
|
408
|
+
const byte = buffer[position++]
|
|
409
|
+
result += (byte & 0x7f) * Math.pow(2, shift)
|
|
410
|
+
shift += 7
|
|
411
|
+
if ((byte & 0x80) === 0) break
|
|
412
|
+
if (shift > 56) throw new Error('varint too long')
|
|
413
|
+
}
|
|
414
|
+
return [result, position]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function readFields(buffer) {
|
|
418
|
+
const fields = []
|
|
419
|
+
let position = 0
|
|
420
|
+
while (position < buffer.length) {
|
|
421
|
+
let key
|
|
422
|
+
const keyRead = readVarint(buffer, position)
|
|
423
|
+
key = keyRead[0]
|
|
424
|
+
position = keyRead[1]
|
|
425
|
+
const number = key >>> 3
|
|
426
|
+
const wire = key & 7
|
|
427
|
+
if (number === 0) throw new Error('invalid field number')
|
|
428
|
+
if (wire === 0) {
|
|
429
|
+
const valueRead = readVarint(buffer, position)
|
|
430
|
+
position = valueRead[1]
|
|
431
|
+
fields.push({ number, wire, value: valueRead[0] })
|
|
432
|
+
} else if (wire === 2) {
|
|
433
|
+
const lengthRead = readVarint(buffer, position)
|
|
434
|
+
position = lengthRead[1]
|
|
435
|
+
const length = lengthRead[0]
|
|
436
|
+
if (position + length > buffer.length) throw new Error('length-prefixed field overruns buffer')
|
|
437
|
+
fields.push({ number, wire, value: buffer.subarray(position, position + length) })
|
|
438
|
+
position += length
|
|
439
|
+
} else if (wire === 5) {
|
|
440
|
+
if (position + 4 > buffer.length) throw new Error('truncated fixed32')
|
|
441
|
+
fields.push({ number, wire, value: buffer.subarray(position, position + 4) })
|
|
442
|
+
position += 4
|
|
443
|
+
} else if (wire === 1) {
|
|
444
|
+
if (position + 8 > buffer.length) throw new Error('truncated fixed64')
|
|
445
|
+
fields.push({ number, wire, value: buffer.subarray(position, position + 8) })
|
|
446
|
+
position += 8
|
|
447
|
+
} else {
|
|
448
|
+
throw new Error(`unsupported wire type ${wire}`)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return fields
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function textOf(field) {
|
|
455
|
+
return field && Buffer.isBuffer(field.value) ? field.value.toString('utf8') : ''
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function valueInfo(buffer) {
|
|
459
|
+
const info = readFields(buffer)
|
|
460
|
+
const name = textOf(info.find(field => field.number === 1))
|
|
461
|
+
const typeField = info.find(field => field.number === 2)
|
|
462
|
+
let dtype = null
|
|
463
|
+
let shape = null
|
|
464
|
+
if (typeField) {
|
|
465
|
+
const tensorType = readFields(typeField.value).find(field => field.number === 1)
|
|
466
|
+
if (tensorType) {
|
|
467
|
+
const tensor = readFields(tensorType.value)
|
|
468
|
+
const elemType = tensor.find(field => field.number === 1)
|
|
469
|
+
if (elemType) dtype = ONNX_DTYPES[elemType.value] || `elem_type_${elemType.value}`
|
|
470
|
+
const shapeField = tensor.find(field => field.number === 2)
|
|
471
|
+
if (shapeField) {
|
|
472
|
+
shape = readFields(shapeField.value)
|
|
473
|
+
.filter(field => field.number === 1)
|
|
474
|
+
.map(dimField => {
|
|
475
|
+
const dim = readFields(dimField.value)
|
|
476
|
+
const dimValue = dim.find(field => field.number === 1)
|
|
477
|
+
if (dimValue) return dimValue.value
|
|
478
|
+
const dimParam = dim.find(field => field.number === 2)
|
|
479
|
+
return dimParam ? textOf(dimParam) : '?'
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { name, dtype, shape }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 只读解析 ONNX 图头:opset、生产者、输入/输出名与 dtype/形状、initializer 体积、
|
|
489
|
+
* 是否引用 external_data。不执行图、不需要 onnxruntime(§14.4 第 1/2 条)。
|
|
490
|
+
*/
|
|
491
|
+
export function readOnnxGraphHeader(buffer) {
|
|
492
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 8) {
|
|
493
|
+
const error = new Error('ONNX artifact is empty or too small to be a graph')
|
|
494
|
+
error.code = 'EMBEDDING_ONNX_INVALID'
|
|
495
|
+
throw error
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const model = readFields(buffer)
|
|
499
|
+
const graphField = model.find(field => field.number === 7)
|
|
500
|
+
if (!graphField) {
|
|
501
|
+
const error = new Error('ONNX artifact has no graph field')
|
|
502
|
+
error.code = 'EMBEDDING_ONNX_INVALID'
|
|
503
|
+
throw error
|
|
504
|
+
}
|
|
505
|
+
const graph = readFields(graphField.value)
|
|
506
|
+
const initializers = graph.filter(field => field.number === 5)
|
|
507
|
+
let initializerBytes = 0
|
|
508
|
+
let externalDataEntries = 0
|
|
509
|
+
for (const initializer of initializers) {
|
|
510
|
+
const tensor = readFields(initializer.value)
|
|
511
|
+
const raw = tensor.find(field => field.number === 9)
|
|
512
|
+
if (raw) initializerBytes += raw.value.length
|
|
513
|
+
externalDataEntries += tensor.filter(field => field.number === 13).length
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
irVersion: model.find(field => field.number === 1)?.value ?? null,
|
|
517
|
+
producer: textOf(model.find(field => field.number === 2)) || null,
|
|
518
|
+
producerVersion: textOf(model.find(field => field.number === 3)) || null,
|
|
519
|
+
opsets: model.filter(field => field.number === 8).map(field => {
|
|
520
|
+
const opset = readFields(field.value)
|
|
521
|
+
return { domain: textOf(opset.find(item => item.number === 1)), version: opset.find(item => item.number === 2)?.value ?? null }
|
|
522
|
+
}),
|
|
523
|
+
inputs: graph.filter(field => field.number === 11).map(field => valueInfo(field.value)),
|
|
524
|
+
outputs: graph.filter(field => field.number === 12).map(field => valueInfo(field.value)),
|
|
525
|
+
initializerCount: initializers.length,
|
|
526
|
+
initializerBytes,
|
|
527
|
+
externalDataEntries,
|
|
528
|
+
selfContained: externalDataEntries === 0 && initializerBytes > 0,
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error?.code === 'EMBEDDING_ONNX_INVALID') throw error
|
|
532
|
+
const wrapped = new Error(`cannot parse ONNX graph header: ${error?.message || error}`)
|
|
533
|
+
wrapped.code = 'EMBEDDING_ONNX_INVALID'
|
|
534
|
+
throw wrapped
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ---------------------------------------------------------------- manifest 校验
|
|
539
|
+
|
|
540
|
+
function isPlainObject(value) {
|
|
541
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isSafeRelativePath(filePath) {
|
|
545
|
+
if (typeof filePath !== 'string' || !filePath || filePath.length > 240) return false
|
|
546
|
+
if (filePath.includes('\\') || filePath.includes('\u0000')) return false
|
|
547
|
+
if (filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)) return false
|
|
548
|
+
return filePath.split('/').every(segment => segment && segment !== '.' && segment !== '..')
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 校验 manifest/目录条目。返回 { ok, errors, warnings }:errors 是"不可下载/不可安装",
|
|
553
|
+
* warnings 是"可以下载但必须如实展示"(例如 ONNX 导出未单独声明许可、ORT 版本未实测)。
|
|
554
|
+
*/
|
|
555
|
+
export function validateEmbeddingManifest(manifest) {
|
|
556
|
+
const errors = []
|
|
557
|
+
const warnings = []
|
|
558
|
+
const error = (code, message) => errors.push({ code, message })
|
|
559
|
+
const warn = (code, message) => warnings.push({ code, message })
|
|
560
|
+
|
|
561
|
+
if (!isPlainObject(manifest)) {
|
|
562
|
+
error('MANIFEST_NOT_OBJECT', 'manifest must be a plain object')
|
|
563
|
+
return { ok: false, errors, warnings }
|
|
564
|
+
}
|
|
565
|
+
if (!/^[a-z0-9][a-z0-9._-]{2,63}$/.test(String(manifest.id || ''))) error('ID_INVALID', 'id must be a stable lowercase slug')
|
|
566
|
+
if (typeof manifest.displayName !== 'string' || !manifest.displayName.trim()) error('DISPLAY_NAME_MISSING', 'displayName is required')
|
|
567
|
+
if (!Array.isArray(manifest.languages) || !manifest.languages.length) error('LANGUAGES_INVALID', 'languages must be a non-empty array')
|
|
568
|
+
|
|
569
|
+
const license = manifest.license
|
|
570
|
+
if (!isPlainObject(license) || !isPlainObject(license.base) || typeof license.base.id !== 'string' || !license.base.id) {
|
|
571
|
+
error('LICENSE_MISSING', 'license.base.id and its source are required (§14.4)')
|
|
572
|
+
} else if (license.base.id === 'unverified' && manifest.availability === 'available') {
|
|
573
|
+
error('LICENSE_UNVERIFIED', 'an available artifact must have a verified license')
|
|
574
|
+
}
|
|
575
|
+
if (isPlainObject(license) && isPlainObject(license.onnxExport) && !['declared', 'mit', 'apache-2.0'].includes(license.onnxExport.id)) {
|
|
576
|
+
warn('LICENSE_DERIVATIVE_UNDECLARED', 'ONNX export license is not separately declared; show it as undeclared instead of inheriting the code licence')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!Number.isFinite(manifest.parameters) || manifest.parameters <= 0) error('PARAMETERS_INVALID', 'parameters must be a positive number')
|
|
580
|
+
if (!Number.isInteger(manifest.dimension) || manifest.dimension < 1 || manifest.dimension > 8192) error('DIMENSION_INVALID', 'dimension must be an integer in 1..8192')
|
|
581
|
+
if (!(manifest.maxTokens === null || (Number.isInteger(manifest.maxTokens) && manifest.maxTokens >= 32 && manifest.maxTokens <= 8192))) {
|
|
582
|
+
error('MAX_TOKENS_INVALID', 'maxTokens must be null (unverified) or an integer in 32..8192')
|
|
583
|
+
}
|
|
584
|
+
if (!['cls', 'mean', 'max'].includes(manifest.pooling)) error('POOLING_INVALID', 'pooling must be cls/mean/max')
|
|
585
|
+
if (!['l2', 'none'].includes(manifest.normalization)) error('NORMALIZATION_INVALID', 'normalization must be l2/none')
|
|
586
|
+
if (!isPlainObject(manifest.prefixes) || typeof manifest.prefixes.query !== 'string' || typeof manifest.prefixes.document !== 'string') {
|
|
587
|
+
error('PREFIXES_INVALID', 'prefixes.query/document must be strings (empty is allowed)')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const sources = manifest.sources
|
|
591
|
+
if (!isPlainObject(sources) || !Object.keys(sources).length) error('SOURCES_MISSING', 'sources must map providers to explicit repo/revision')
|
|
592
|
+
let pinnedSources = 0
|
|
593
|
+
for (const provider of Object.keys(sources || {})) {
|
|
594
|
+
const source = sources[provider]
|
|
595
|
+
if (!PROVIDERS[provider]) { error('SOURCE_PROVIDER_UNKNOWN', `unknown provider ${provider}`); continue }
|
|
596
|
+
if (!isPlainObject(source)) { error('SOURCE_INVALID', `source ${provider} must be an object`); continue }
|
|
597
|
+
if (typeof source.repo !== 'string' || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(source.repo)) {
|
|
598
|
+
error('SOURCE_REPO_INVALID', `source ${provider}.repo must be owner/name`)
|
|
599
|
+
}
|
|
600
|
+
if (source.revision === null) {
|
|
601
|
+
warn('SOURCE_REVISION_UNPINNED', `source ${provider} has no pinned revision; no URL may be derived from it`)
|
|
602
|
+
} else if (typeof source.revision !== 'string' || !/^[0-9a-f]{40}$/.test(source.revision)) {
|
|
603
|
+
error('SOURCE_REVISION_INVALID', `source ${provider}.revision must be a 40-char lowercase commit sha`)
|
|
604
|
+
} else {
|
|
605
|
+
pinnedSources++
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (manifest.availability === 'available') {
|
|
610
|
+
if (!pinnedSources) error('SOURCE_NOT_PINNED', 'an available artifact needs at least one provider with an explicit pinned revision')
|
|
611
|
+
if (!isPlainObject(manifest.verification) || manifest.verification.status !== 'verified') {
|
|
612
|
+
error('VERIFICATION_MISSING', 'an available artifact must carry verification.status = verified')
|
|
613
|
+
} else {
|
|
614
|
+
if (typeof manifest.verification.method !== 'string' || !manifest.verification.method) error('VERIFICATION_METHOD_MISSING', 'verification.method is required')
|
|
615
|
+
if (typeof manifest.verification.checkedAt !== 'string' || Number.isNaN(Date.parse(manifest.verification.checkedAt))) {
|
|
616
|
+
error('VERIFICATION_TIME_INVALID', 'verification.checkedAt must be an ISO timestamp')
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else if (!Array.isArray(manifest.pendingReasons) || !manifest.pendingReasons.length) {
|
|
620
|
+
error('PENDING_REASON_MISSING', 'a non-available model must state why it is pending')
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const runtime = manifest.runtime
|
|
624
|
+
if (!isPlainObject(runtime) || typeof runtime.engine !== 'string' || typeof runtime.abi !== 'string' || typeof runtime.os !== 'string' || typeof runtime.arch !== 'string') {
|
|
625
|
+
error('RUNTIME_INVALID', 'runtime.engine/abi/os/arch are required (§14.4)')
|
|
626
|
+
} else if (runtime.version === null) {
|
|
627
|
+
warn('RUNTIME_VERSION_UNPINNED', 'runtime version is not pinned yet; enabling the profile must stay blocked until ORT is verified')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const files = manifest.files
|
|
631
|
+
if (!Array.isArray(files)) {
|
|
632
|
+
error('FILES_INVALID', 'files must be an array')
|
|
633
|
+
} else {
|
|
634
|
+
if (manifest.availability === 'available' && !files.length) error('FILES_MISSING', 'an available artifact must list its files')
|
|
635
|
+
const seen = new Set()
|
|
636
|
+
for (const file of files) {
|
|
637
|
+
if (!isPlainObject(file)) { error('FILE_INVALID', 'each file must be an object'); continue }
|
|
638
|
+
if (!isSafeRelativePath(file.path)) error('FILE_PATH_UNSAFE', `unsafe file path: ${JSON.stringify(file.path)}`)
|
|
639
|
+
if (seen.has(file.path)) error('FILE_DUPLICATE', `duplicate file path ${file.path}`)
|
|
640
|
+
seen.add(file.path)
|
|
641
|
+
if (!Number.isInteger(file.bytes) || file.bytes < 0) error('FILE_BYTES_INVALID', `file ${file.path} needs exact bytes`)
|
|
642
|
+
if (typeof file.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(file.sha256)) error('FILE_SHA256_INVALID', `file ${file.path} needs a lowercase sha256`)
|
|
643
|
+
if (!FILE_ROLES.includes(file.role)) error('FILE_ROLE_INVALID', `file ${file.path} has unknown role ${file.role}`)
|
|
644
|
+
if (file.origin !== undefined) {
|
|
645
|
+
const origin = file.origin
|
|
646
|
+
if (!isPlainObject(origin) || !PROVIDERS[origin.provider] || typeof origin.repo !== 'string' || !/^[0-9a-f]{40}$/.test(String(origin.revision))) {
|
|
647
|
+
error('FILE_ORIGIN_INVALID', `file ${file.path} origin must pin provider/repo/revision`)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const graphs = files.filter(file => file.role === 'graph')
|
|
652
|
+
if (manifest.availability === 'available' && graphs.length !== 1) error('GRAPH_MISSING', 'exactly one file must have role graph')
|
|
653
|
+
for (const graphFile of graphs) if (!String(graphFile.path || '').endsWith('.onnx')) error('GRAPH_NOT_ONNX', `graph file must be .onnx: ${graphFile.path}`)
|
|
654
|
+
|
|
655
|
+
if (manifest.totalBytes !== undefined) {
|
|
656
|
+
const actual = files.reduce((sum, file) => sum + (Number.isFinite(file.bytes) ? file.bytes : 0), 0)
|
|
657
|
+
if (manifest.totalBytes !== actual) error('TOTAL_BYTES_MISMATCH', `totalBytes ${manifest.totalBytes} does not equal the sum of files ${actual}`)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const graph = manifest.graph
|
|
662
|
+
if (manifest.availability === 'available') {
|
|
663
|
+
if (!isPlainObject(graph)) {
|
|
664
|
+
error('GRAPH_DESCRIPTOR_MISSING', 'graph descriptor (inputs/outputs/dtype) is required')
|
|
665
|
+
} else {
|
|
666
|
+
if (!Array.isArray(graph.inputs) || !graph.inputs.length || !Array.isArray(graph.outputs) || !graph.outputs.length) {
|
|
667
|
+
error('GRAPH_IO_INVALID', 'graph.inputs/outputs must be non-empty arrays')
|
|
668
|
+
}
|
|
669
|
+
for (const io of [...(graph.inputs || []), ...(graph.outputs || [])]) {
|
|
670
|
+
if (!isPlainObject(io) || typeof io.name !== 'string' || !io.name || typeof io.dtype !== 'string' || !io.dtype) {
|
|
671
|
+
error('GRAPH_IO_INVALID', 'each graph input/output needs name and dtype')
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const output = (graph.outputs || [])[0]
|
|
675
|
+
const last = Array.isArray(output?.shape) ? output.shape[output.shape.length - 1] : null
|
|
676
|
+
if (Number.isInteger(manifest.dimension) && last !== manifest.dimension) {
|
|
677
|
+
error('GRAPH_DIMENSION_MISMATCH', `graph output last dim ${last} does not match dimension ${manifest.dimension}`)
|
|
678
|
+
}
|
|
679
|
+
if (Array.isArray(graph.externalData) && graph.externalData.length && graph.selfContained !== false) {
|
|
680
|
+
error('GRAPH_EXTERNAL_DATA_UNDECLARED', 'external data references must be listed and selfContained=false')
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const graphFileInFiles = Array.isArray(files) ? files.find(file => file.role === 'graph') : null
|
|
686
|
+
if (isPlainObject(graph) && graphFileInFiles && graph.path !== graphFileInFiles.path) {
|
|
687
|
+
error('GRAPH_PATH_MISMATCH', `graph.path ${graph.path} must equal the graph file ${graphFileInFiles.path}`)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (manifest.profileFingerprint !== undefined) {
|
|
691
|
+
const expected = profileFingerprint(manifest)
|
|
692
|
+
if (manifest.profileFingerprint !== expected) error('FINGERPRINT_MISMATCH', `profileFingerprint ${manifest.profileFingerprint} does not match the profile ${expected}`)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 只按 manifest 里显式写明的 repo+revision 生成 URL(§14.4)。默认顺序
|
|
700
|
+
* modelscope → hf-mirror → huggingface;没有固定 revision 的来源不生成任何 URL,
|
|
701
|
+
* 而是列进 skipped 并写明原因。
|
|
702
|
+
*/
|
|
703
|
+
export function resolveEmbeddingSources(idOrEntry, options = {}) {
|
|
704
|
+
const entry = typeof idOrEntry === 'string' ? getEmbeddingModel(idOrEntry) : idOrEntry
|
|
705
|
+
if (!isPlainObject(entry)) {
|
|
706
|
+
const error = new Error(`unknown embedding model: ${String(idOrEntry)}`)
|
|
707
|
+
error.code = 'EMBEDDING_MODEL_UNKNOWN'
|
|
708
|
+
throw error
|
|
709
|
+
}
|
|
710
|
+
const providers = Array.isArray(options.providers) && options.providers.length ? options.providers : PROVIDER_ORDER
|
|
711
|
+
const files = Array.isArray(entry.files) ? entry.files : []
|
|
712
|
+
const skipped = []
|
|
713
|
+
const sources = []
|
|
714
|
+
|
|
715
|
+
for (const providerId of providers) {
|
|
716
|
+
if (!PROVIDERS[providerId]) {
|
|
717
|
+
skipped.push({ provider: providerId, reason: 'provider-unknown' })
|
|
718
|
+
continue
|
|
719
|
+
}
|
|
720
|
+
const source = isPlainObject(entry.sources) ? entry.sources[providerId] : null
|
|
721
|
+
if (!isPlainObject(source)) { skipped.push({ provider: providerId, reason: 'not-mapped' }); continue }
|
|
722
|
+
if (typeof source.revision !== 'string' || !/^[0-9a-f]{40}$/.test(source.revision)) {
|
|
723
|
+
skipped.push({ provider: providerId, reason: 'revision-unpinned', repo: source.repo || null })
|
|
724
|
+
continue
|
|
725
|
+
}
|
|
726
|
+
if (typeof source.repo !== 'string' || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(source.repo)) {
|
|
727
|
+
skipped.push({ provider: providerId, reason: 'repo-invalid' })
|
|
728
|
+
continue
|
|
729
|
+
}
|
|
730
|
+
const fileUrls = []
|
|
731
|
+
let blocked = null
|
|
732
|
+
for (const file of files) {
|
|
733
|
+
const origin = file.origin
|
|
734
|
+
if (origin && origin.provider !== providerId) {
|
|
735
|
+
blocked = { provider: providerId, reason: 'origin-provider-mismatch', path: file.path, originProvider: origin.provider }
|
|
736
|
+
break
|
|
737
|
+
}
|
|
738
|
+
const repo = origin?.repo || source.repo
|
|
739
|
+
const revision = origin?.revision || source.revision
|
|
740
|
+
fileUrls.push({
|
|
741
|
+
path: file.path,
|
|
742
|
+
role: file.role,
|
|
743
|
+
bytes: file.bytes,
|
|
744
|
+
sha256: file.sha256,
|
|
745
|
+
repo,
|
|
746
|
+
revision,
|
|
747
|
+
url: PROVIDERS[providerId].url({ repo, revision, path: file.path }),
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
if (blocked) { skipped.push(blocked); continue }
|
|
751
|
+
if (!fileUrls.length) { skipped.push({ provider: providerId, reason: 'no-files' }); continue }
|
|
752
|
+
sources.push({ provider: providerId, label: PROVIDERS[providerId].label, repo: source.repo, revision: source.revision, ready: true, files: fileUrls })
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
id: entry.id,
|
|
757
|
+
availability: entry.availability,
|
|
758
|
+
order: [...providers],
|
|
759
|
+
sources,
|
|
760
|
+
skipped,
|
|
761
|
+
primary: sources[0] || null,
|
|
762
|
+
}
|
|
763
|
+
}
|