@kira905/dsh-memory-toolkit 0.1.2 → 0.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +130 -1
  2. package/README.en.md +225 -2
  3. package/README.md +208 -2
  4. package/cordis.patch.yml +32 -0
  5. package/lib/deploy-guard.mjs +463 -275
  6. package/lib/guard-http.mjs +59 -62
  7. package/lib/pitfall-form-map.mjs +94 -84
  8. package/lib/pitfall-score.mjs +262 -185
  9. package/lib/session-file-name.mjs +116 -100
  10. package/lib/zstd-frames.mjs +88 -67
  11. package/memory-db-tools/README.md +29 -1
  12. package/memory-db-tools/audit-analyze.mjs +1 -1
  13. package/memory-db-tools/audit-export.mjs +60 -16
  14. package/memory-db-tools/audit-struct-probe.mjs +29 -8
  15. package/memory-db-tools/lib/known-devices.mjs +137 -0
  16. package/memory-engine/README.md +58 -4
  17. package/memory-engine/bench/bench.mjs +1 -1
  18. package/memory-engine/cli/band-sentinel.mjs +164 -0
  19. package/memory-engine/cli/distill-rollback.mjs +112 -44
  20. package/memory-engine/cli/distill-sessions.mjs +237 -0
  21. package/memory-engine/cli/distill.mjs +196 -95
  22. package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
  23. package/memory-engine/cli/doc-index.mjs +47 -38
  24. package/memory-engine/cli/domain-tags.mjs +246 -162
  25. package/memory-engine/cli/layer-migrate.mjs +176 -0
  26. package/memory-engine/cli/layer-move.mjs +189 -0
  27. package/memory-engine/cli/pitfall-hook.mjs +136 -70
  28. package/memory-engine/cli/recall.mjs +243 -110
  29. package/memory-engine/cli/selfcheck.mjs +93 -44
  30. package/memory-engine/cli/serve-start.mjs +221 -140
  31. package/memory-engine/cli/sweep.mjs +83 -44
  32. package/memory-engine/cli/sync.mjs +93 -57
  33. package/memory-engine/config/audit-devices.json +6 -0
  34. package/memory-engine/config/bands.json +7 -0
  35. package/memory-engine/config/layers.json +7 -0
  36. package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
  37. package/memory-engine/dict/lexicon-config.json +11 -0
  38. package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
  39. package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
  40. package/memory-engine/lib/band-sentinel.mjs +417 -0
  41. package/memory-engine/lib/cli-strict.mjs +128 -0
  42. package/memory-engine/lib/cli-util.mjs +147 -125
  43. package/memory-engine/lib/config.mjs +83 -96
  44. package/memory-engine/lib/dirty-ledger.mjs +127 -94
  45. package/memory-engine/lib/distill-rollback.mjs +173 -133
  46. package/memory-engine/lib/distill.mjs +878 -585
  47. package/memory-engine/lib/doc-index-store.mjs +208 -235
  48. package/memory-engine/lib/doc-index.mjs +351 -350
  49. package/memory-engine/lib/doc-scan.mjs +178 -156
  50. package/memory-engine/lib/doc-summarize.mjs +220 -205
  51. package/memory-engine/lib/domain-tags.mjs +358 -277
  52. package/memory-engine/lib/embedder.mjs +175 -194
  53. package/memory-engine/lib/hashes.mjs +22 -7
  54. package/memory-engine/lib/layer-migrate.mjs +330 -0
  55. package/memory-engine/lib/layer-move.mjs +307 -0
  56. package/memory-engine/lib/layers.mjs +191 -0
  57. package/memory-engine/lib/lexicon-recall.mjs +473 -0
  58. package/memory-engine/lib/memory-source.mjs +78 -80
  59. package/memory-engine/lib/pitfall-source.mjs +115 -114
  60. package/memory-engine/lib/policy-source.mjs +279 -261
  61. package/memory-engine/lib/recall-log.mjs +285 -0
  62. package/memory-engine/lib/recall.mjs +565 -240
  63. package/memory-engine/lib/selfcheck.mjs +142 -106
  64. package/memory-engine/lib/session-distill.mjs +603 -0
  65. package/memory-engine/lib/session-source.mjs +421 -0
  66. package/memory-engine/lib/sweep.mjs +107 -130
  67. package/memory-engine/lib/sync.mjs +110 -140
  68. package/memory-engine/lib/tick-hook.mjs +123 -148
  69. package/memory-engine/lib/vectors-store.mjs +213 -188
  70. package/memory-engine/test/12-layering.test.mjs +539 -0
  71. package/memory-engine/test/13-session-distill.test.mjs +616 -0
  72. package/memory-engine/tools/bridge-smoke.mjs +94 -55
  73. package/memory-engine/tools/fetch-model.mjs +167 -87
  74. package/memory-engine/tools/smoke-recall.mjs +94 -38
  75. package/memory-engine/tools/verify-recall-chain.mjs +413 -0
  76. package/package.json +11 -1
@@ -1,29 +1,29 @@
1
1
  #!/usr/bin/env node
2
- // 记忆引擎 v2 · S3c —— 提炼 CLI(`cli/distill.mjs`)
2
+ // 提炼 CLI:把窗口内的记忆条目聚类,逐组交给模型压成一条「提炼产物」再写回库。
3
3
  //
4
- // 用法:
5
- // node cli/distill.mjs --db <memory.db> [选项] # 默认 **dry-run**(只打印不写库)
6
- // node cli/distill.mjs --db <memory.db> --apply # 真跑:调模型 + 写库 + 写 state
4
+ // 用法(对外契约:参数名 / 默认值 / 退出码都不变):
5
+ // node cli/distill.mjs --db <memory.db> [选项] 缺省是 **dry-run**:只算、只打印,不动库
6
+ // node cli/distill.mjs --db <memory.db> --apply 真跑:调模型 → 写库 → 落 state → 写日报段
7
7
  //
8
- // 选项:
9
- // --db <path> 记忆库(**必填**,无默认 —— 写死生产路径是明令禁止的)
10
- // --vectors-db <path> 向量库(默认 <home>/memory-vectors/vectors.db;home = db 的上上级)
11
- // --state <path> 提炼 state(默认 <home>/butler-distill-state.json)
12
- // --owner-user <id> 本机 owner 的 userid(缺省由身份表解析;解析不出 ⇒ **本轮 0 调用**)
13
- // --window-hours <n> 增量窗口(默认 24)
14
- // --limit <n> 输入条数上限(默认 0 = 不限)
15
- // --max-calls / --max-tokens 成本闸(默认 20 次 / 150000 token 每日)
16
- // --model / --base-url 模型档(默认 deepseek-flash @ 本鱼链;2026-09-21 主人拍板,不许改回第三方)
17
- // --sim-threshold <f> 聚类余弦阈值(默认 0.60)
18
- // --min-cluster-chars <n> 整组正文合计低于此值不调模型(默认 120)
19
- // --min-cluster-size <n> 组内少于 N 条不提炼(默认 **2**:单条自成一组不算聚类,且少写几条以护住 memory:recall 段)
20
- // --max-clusters <n> 单轮最多处理几组(默认 20)
21
- // --now <iso> 以某个时刻为"现在"(演练/三天不自我指涉模拟用)
22
- // --snapshot-dir <path> 写库前 `VACUUM INTO` 快照目录
23
- // --no-dirty-probe 不跑全表 hash 比对(不读 memories_dirty 补充源)
24
- // --daily-report <file> 追加/替换日报段(默认:今天的 <workspace>/docs/管家报告/<日期>-<设备>.md)
25
- // --no-daily 不写日报段
26
- // --json 机器可读(stdout 只出 JSON,日志走 stderr)
8
+ // 开关:
9
+ // --db <path> 必填(无默认:写死生产路径是明令禁止的)
10
+ // --vectors-db <path> 向量库(缺省 <home>/memory-vectors/vectors.db;home = 库文件的上一级目录)
11
+ // --state <path> 提炼台账(缺省 <home>/butler-distill-state.json)
12
+ // --owner-user <id> 本机 owner 的 userid(缺省查身份表;查不出 ⇒ 归属从严,本轮一次模型都不调)
13
+ // --window-hours <n> 增量窗口(24)
14
+ // --limit <n> 输入条数上限(0 = 不限)
15
+ // --max-calls / --max-tokens 每日成本闸(20 次 / 150000 token)
16
+ // --model / --base-url 模型档(缺省取库里的 DEFAULT_LLM)
17
+ // --sim-threshold <f> 聚类余弦阈值(0.60)
18
+ // --min-cluster-chars <n> 整组正文合计不到这个字数就不值得花一次调用(120)
19
+ // --min-cluster-size <n> 组内少于 N 条不提炼(2:单条不成聚类,也为了别把 recall 段挤掉)
20
+ // --max-clusters <n> 单轮最多处理几组(20)
21
+ // --now <iso> 把某个时刻当"现在"(演练 / 模拟用)
22
+ // --snapshot-dir <path> 写库前先 VACUUM INTO 到该目录
23
+ // --no-dirty-probe 不做全表 hash 比对(不读 memories_dirty 补充源)
24
+ // --daily-report <file> 覆盖日报落点(缺省是今天的管家报告)
25
+ // --no-daily 不写日报段
26
+ // --json stdout 只出 JSON,日志走 stderr
27
27
  // --quiet / --help
28
28
  //
29
29
  // 退出码:0 = 正常(含"没活干")/ 1 = 失败 / 2 = 用法错 / 3 = 环境不可用(库不在)
@@ -32,87 +32,203 @@ import path from 'node:path';
32
32
  import { isMainEntry, parseArgs, makeLogger, num } from '../lib/cli-util.mjs';
33
33
  import {
34
34
  runDistill, readState, renderDailyLine, updateDailySection, dailyReportPath,
35
- COST_LIMITS, DEFAULT_LLM, DEFAULT_WINDOW_HOURS, CLUSTER_DEFAULTS, STATE_SCHEMA,
35
+ COST_LIMITS, DEFAULT_LLM, DEFAULT_WINDOW_HOURS, CLUSTER_DEFAULTS, STATE_SCHEMA as LIB_STATE_SCHEMA,
36
36
  } from '../lib/distill.mjs';
37
37
 
38
- const USAGE = 'node cli/distill.mjs --db <memory.db> [--apply] [--window-hours 24] [--limit N] '
39
- + '[--max-calls 20] [--max-tokens 150000] [--max-clusters 20] [--min-cluster-size 2] [--min-cluster-chars 120] '
40
- + '[--model deepseek-flash] [--now <iso>] [--snapshot-dir <dir>] [--daily-report <file>] [--no-dirty-probe] '
41
- + '[--no-daily] [--json] [--help]';
38
+ const USAGE = [
39
+ 'node cli/distill.mjs',
40
+ '--db <memory.db>',
41
+ '[--apply]',
42
+ '[--window-hours 24]',
43
+ '[--limit N]',
44
+ '[--max-calls 20]',
45
+ '[--max-tokens 150000]',
46
+ '[--max-clusters 20]',
47
+ '[--min-cluster-size 2]',
48
+ '[--min-cluster-chars 120]',
49
+ '[--model deepseek-flash]',
50
+ '[--now <iso>]',
51
+ '[--snapshot-dir <dir>]',
52
+ '[--daily-report <file>]',
53
+ '[--no-dirty-probe]',
54
+ '[--no-daily]',
55
+ '[--json]',
56
+ '[--help]',
57
+ ].join(' ');
42
58
 
43
- /** 解析「本机 owner 的 userid」:权威实现优先,其次 fork 自检镜像,最后内置回落(与 domain-tags 同口径)。 */
44
- async function resolveOwnerUser(explicit, home) {
59
+ export const STATE_SCHEMA = LIB_STATE_SCHEMA;
60
+
61
+ /** 数值旋钮表:flag 名 → 归一化字段 → 缺省值。 */
62
+ const KNOBS = [
63
+ { flag: 'window-hours', field: 'windowHours', fallback: DEFAULT_WINDOW_HOURS },
64
+ { flag: 'limit', field: 'limit', fallback: 0 },
65
+ { flag: 'max-calls', field: 'callsPerDay', fallback: COST_LIMITS.callsPerDay },
66
+ { flag: 'max-tokens', field: 'tokensPerDay', fallback: COST_LIMITS.tokensPerDay },
67
+ { flag: 'sim-threshold', field: 'simThreshold', fallback: CLUSTER_DEFAULTS.simThreshold },
68
+ { flag: 'min-cluster-chars', field: 'minClusterChars', fallback: CLUSTER_DEFAULTS.minClusterChars },
69
+ { flag: 'min-cluster-size', field: 'minClusterSize', fallback: CLUSTER_DEFAULTS.minClusterSize },
70
+ { flag: 'max-clusters', field: 'maxClustersPerRun', fallback: CLUSTER_DEFAULTS.maxClustersPerRun },
71
+ ];
72
+
73
+ const OWNER_MODULES = ['../../memory-db-tools/lib/memory-scope.mjs', '../dsh-memory-local/src/schema-migrate.mjs'];
74
+
75
+ /** 本机 owner 的 userid:权威实现 → fork 自检镜像 → 内置回落(和域标签 CLI 同一口径)。 */
76
+ export async function resolveOwnerUser(explicit, home) {
45
77
  if (explicit) return { user: explicit, source: 'cli' };
46
- const cands = ['../../memory-db-tools/lib/memory-scope.mjs', '../dsh-memory-local/src/schema-migrate.mjs'];
47
- for (const rel of cands) {
48
- try {
49
- const mod = await import(new URL(rel, import.meta.url).href);
50
- const identity = mod.readIdentityTable ? mod.readIdentityTable(home) : null;
51
- const r = mod.resolveDefaultOwner ? mod.resolveDefaultOwner(identity) : null;
52
- if (r && r.ownerUser) return { user: r.ownerUser, source: `${rel} (${r.source})`, notes: r.notes ?? [] };
53
- } catch { /* 试下一个候选 */ }
78
+ for (const spec of OWNER_MODULES) {
79
+ const hit = await probeOwnerModule(spec, home);
80
+ if (hit) return hit;
54
81
  }
55
- return { user: null, source: '解析失败(两条权威源都不可用)', notes: ['⚠️ 本机 owner 解析不出来 ⇒ 按 D2 从严:本轮不调用任何第三方模型'] };
82
+ return {
83
+ user: null,
84
+ source: '解析失败(两条权威源都不可用)',
85
+ notes: ['⚠️ 本机 owner 解析不出来 ⇒ 归属从严:本轮不调用任何第三方模型'],
86
+ };
56
87
  }
57
88
 
58
- async function main() {
59
- const { flags } = parseArgs();
60
- const json = Boolean(flags.json);
61
- const log = makeLogger({ json, quiet: Boolean(flags.quiet) });
62
- if (flags.help) { process.stdout.write(`用法:${USAGE}\n`); return 0; }
89
+ async function probeOwnerModule(spec, home) {
90
+ try {
91
+ const mod = await import(new URL(spec, import.meta.url).href);
92
+ const table = mod.readIdentityTable ? mod.readIdentityTable(home) : null;
93
+ const picked = mod.resolveDefaultOwner ? mod.resolveDefaultOwner(table) : null;
94
+ if (!picked?.ownerUser) return null;
95
+ return { user: picked.ownerUser, source: `${spec} (${picked.source})`, notes: picked.notes ?? [] };
96
+ } catch {
97
+ /* 换下一个候选 */
98
+ }
99
+ return null;
100
+ }
63
101
 
102
+ function readKnobs(flags) {
103
+ const out = {};
104
+ for (const knob of KNOBS) out[knob.field] = num(flags[knob.flag], knob.fallback);
105
+ return out;
106
+ }
107
+
108
+ /** 参数校验:返回 {code} 表示当场收工,返回 {dbPath} 表示可以往下走。 */
109
+ function vetArgs(flags) {
64
110
  const dbPath = flags.db ? path.resolve(String(flags.db)) : null;
65
111
  if (!dbPath) {
66
112
  process.stderr.write('用法错:必须给 --db <库路径>(本脚本没有默认库路径)\n');
67
- return 2;
113
+ return { code: 2 };
68
114
  }
69
115
  if (!fs.existsSync(dbPath)) {
70
116
  process.stderr.write(`环境不可用:库不存在 ${dbPath}\n`);
71
- return 3;
117
+ return { code: 3 };
118
+ }
119
+ const now = flags.now ? Date.parse(String(flags.now)) : Date.now();
120
+ if (!Number.isFinite(now)) {
121
+ process.stderr.write(`用法错:--now 不是合法时间:${flags.now}\n`);
122
+ return { code: 2 };
123
+ }
124
+ return { dbPath, now };
125
+ }
126
+
127
+ function composePayload({ r, state, limits, dailyLine, daily, ctx, owner }) {
128
+ return {
129
+ ok: r.ok,
130
+ mode: r.mode,
131
+ at: new Date(ctx.now).toISOString(),
132
+ db: ctx.dbPath,
133
+ vectorsDb: ctx.vectorsDb,
134
+ stateFile: ctx.stateFile,
135
+ model: DEFAULT_LLM.model,
136
+ ownerUser: owner.user,
137
+ ownerSource: owner.source,
138
+ limits,
139
+ inputs: r.inputs,
140
+ clusterStats: r.clusterStats ?? null,
141
+ dirty: r.dirty ?? null,
142
+ planned: r.planned,
143
+ calls: r.calls,
144
+ written: r.written,
145
+ writtenIds: r.writtenIds,
146
+ cost: state.cost ?? null,
147
+ stopping: r.stopping,
148
+ warnings: r.warnings,
149
+ dailyLine,
150
+ daily,
151
+ snapshot: r.snapshot ?? null,
152
+ };
153
+ }
154
+
155
+ function printSummary({ r, state, limits, dailyLine, log }) {
156
+ log('');
157
+ log(`输入 ${r.inputs?.accepted ?? 0} 条(窗口 ${r.inputs?.windowRows ?? 0} / 台账 ${r.inputs?.dirtyRows ?? 0})⇒ 接受 ${r.inputs?.accepted ?? 0}`);
158
+ log(`聚类 ${r.clusterStats?.clusters ?? 0} 组(单条组 ${r.clusterStats?.singletonClusters ?? 0} · 无向量 ${r.clusterStats?.noVector ?? 0})`);
159
+ for (const step of r.planned ?? []) {
160
+ const verb = step.action === 'call' ? '→ 调模型' : '· 跳过 ';
161
+ const chars = step.chars ? ` (${step.chars} 字)` : '';
162
+ log(` ${verb} ${step.key} ids=[${step.ids.join(',')}] ${step.action}${chars}`);
163
+ }
164
+ for (const call of r.calls ?? []) {
165
+ const tail = call.error ? ` · ${call.error}` : '';
166
+ log(` ✦ ${call.key} 调用 ${call.ok ? 'OK' : '失败'} · ${call.tokens} token · 结论 ${call.conclusionChars} 字${tail}`);
167
+ }
168
+ log(`提炼条数 ${r.written}${r.writtenIds?.length ? `(新 id ${r.writtenIds.join(',')})` : ''}`);
169
+ if (r.stopping) log(`停机:${r.stopping}`);
170
+ const spent = Number(state.cost?.promptTokens ?? 0) + Number(state.cost?.completionTokens ?? 0);
171
+ log(`当日调用 ${state.cost?.calls ?? 0}/${limits.callsPerDay} · 当日 token ${spent}/${limits.tokensPerDay}`);
172
+ for (const warn of r.warnings ?? []) log(`⚠️ ${warn}`);
173
+ log(`日报行:${dailyLine}`);
174
+ }
175
+
176
+ export async function main() {
177
+ const { flags } = parseArgs();
178
+ const json = Boolean(flags.json);
179
+ const log = makeLogger({ json, quiet: Boolean(flags.quiet) });
180
+ if (flags.help) {
181
+ process.stdout.write(`用法:${USAGE}\n`);
182
+ return 0;
72
183
  }
73
184
 
185
+ const vetted = vetArgs(flags);
186
+ if (vetted.code !== undefined) return vetted.code;
187
+ const { dbPath, now } = vetted;
188
+
74
189
  const home = path.resolve(flags.home ? String(flags.home) : path.dirname(path.dirname(dbPath)));
75
190
  const vectorsDb = flags['vectors-db'] ? path.resolve(String(flags['vectors-db'])) : path.join(home, 'memory-vectors', 'vectors.db');
76
191
  const stateFile = flags.state ? path.resolve(String(flags.state)) : path.join(home, 'butler-distill-state.json');
77
192
  const apply = Boolean(flags.apply);
78
- const now = flags.now ? Date.parse(String(flags.now)) : Date.now();
79
- if (!Number.isFinite(now)) { process.stderr.write(`用法错:--now 不是合法时间:${flags.now}\n`); return 2; }
80
- const limits = {
81
- callsPerDay: num(flags['max-calls'], COST_LIMITS.callsPerDay),
82
- tokensPerDay: num(flags['max-tokens'], COST_LIMITS.tokensPerDay),
83
- };
193
+ const knobs = readKnobs(flags);
194
+ const limits = { callsPerDay: knobs.callsPerDay, tokensPerDay: knobs.tokensPerDay };
84
195
  const owner = await resolveOwnerUser(flags['owner-user'] ? String(flags['owner-user']) : null, home);
85
196
  const snapshotDir = flags['snapshot-dir'] ? path.resolve(String(flags['snapshot-dir'])) : null;
197
+ const model = flags.model ? String(flags.model) : DEFAULT_LLM.model;
86
198
 
87
199
  log(`库 : ${dbPath}`);
88
200
  log(`向量库 : ${vectorsDb}`);
89
201
  log(`state : ${stateFile}`);
90
202
  log(`本机 owner: ${owner.user ?? '(未解析)'}(来源 ${owner.source})`);
91
- log(`模型 : ${flags.model ? String(flags.model) : DEFAULT_LLM.model}(成本闸 ≤${limits.callsPerDay} 次 / ≤${limits.tokensPerDay} token 每日)`);
203
+ log(`模型 : ${model}(成本闸 ≤${limits.callsPerDay} 次 / ≤${limits.tokensPerDay} token 每日)`);
92
204
  log(`模式 : ${apply ? 'APPLY(写库)' : 'DRY-RUN(只打印不写库)'}`);
93
- for (const n of owner.notes ?? []) log(`⚠️ ${n}`);
205
+ for (const note of owner.notes ?? []) log(`⚠️ ${note}`);
94
206
 
95
- const r = await runDistill({
96
- memoryDb: dbPath, vectorsDb, stateFile, now,
97
- windowHours: num(flags['window-hours'], DEFAULT_WINDOW_HOURS),
207
+ const stamp = new Date(now).toISOString().replace(/[:.]/g, '-');
208
+ const run = await runDistill({
209
+ memoryDb: dbPath,
210
+ vectorsDb,
211
+ stateFile,
212
+ now,
213
+ windowHours: knobs.windowHours,
98
214
  apply,
99
215
  limits,
100
- model: flags.model ? String(flags.model) : DEFAULT_LLM.model,
216
+ model,
101
217
  baseUrl: flags['base-url'] ? String(flags['base-url']) : DEFAULT_LLM.baseUrl,
102
218
  localOwner: owner.user,
103
- limit: num(flags.limit, 0),
104
- simThreshold: num(flags['sim-threshold'], CLUSTER_DEFAULTS.simThreshold),
105
- minClusterChars: num(flags['min-cluster-chars'], CLUSTER_DEFAULTS.minClusterChars),
106
- minClusterSize: num(flags['min-cluster-size'], CLUSTER_DEFAULTS.minClusterSize),
107
- maxClustersPerRun: num(flags['max-clusters'], CLUSTER_DEFAULTS.maxClustersPerRun),
219
+ limit: knobs.limit,
220
+ simThreshold: knobs.simThreshold,
221
+ minClusterChars: knobs.minClusterChars,
222
+ minClusterSize: knobs.minClusterSize,
223
+ maxClustersPerRun: knobs.maxClustersPerRun,
108
224
  dirtyProbe: !flags['no-dirty-probe'],
109
- snapshotFile: snapshotDir ? path.join(snapshotDir, `memory-before-distill-${new Date(now).toISOString().replace(/[:.]/g, '-')}.db`) : null,
225
+ snapshotFile: snapshotDir ? path.join(snapshotDir, `memory-before-distill-${stamp}.db`) : null,
110
226
  log,
111
227
  });
112
228
 
113
- // 不调模型(dry-run / 环境问题)时也要留一行可查的"当日成本",方便日报段
114
- const state = r.state ?? readState(stateFile, dbPath, new Date(now).toISOString());
115
- const dailyLine = r.dailyLine ?? renderDailyLine({ mode: r.mode, run: r.run ?? {}, cost: state.cost, limits, stopping: r.stopping });
229
+ // 没花一分钱(dry-run / 环境问题)也要给一行当日成本,日报段才拼得出来
230
+ const state = run.state ?? readState(stateFile, dbPath, new Date(now).toISOString());
231
+ const dailyLine = run.dailyLine ?? renderDailyLine({ mode: run.mode, run: run.run ?? {}, cost: state.cost, limits, stopping: run.stopping });
116
232
 
117
233
  let daily = { ok: false, reason: '未写日报(--no-daily)' };
118
234
  if (!flags['no-daily'] && apply) {
@@ -122,35 +238,20 @@ async function main() {
122
238
  else log(`日报段未写:${daily.reason}(${file})`);
123
239
  }
124
240
 
125
- const out = {
126
- ok: r.ok, mode: r.mode, at: new Date(now).toISOString(),
127
- db: dbPath, vectorsDb, stateFile, model: DEFAULT_LLM.model, ownerUser: owner.user, ownerSource: owner.source,
128
- limits, inputs: r.inputs, clusterStats: r.clusterStats ?? null, dirty: r.dirty ?? null,
129
- planned: r.planned, calls: r.calls, written: r.written, writtenIds: r.writtenIds,
130
- cost: state.cost ?? null, stopping: r.stopping, warnings: r.warnings, dailyLine, daily,
131
- snapshot: r.snapshot ?? null,
132
- };
133
- if (json) process.stdout.write(JSON.stringify(out, null, 2) + '\n');
134
- else {
135
- log('');
136
- log(`输入 ${r.inputs?.accepted ?? 0} 条(窗口 ${r.inputs?.windowRows ?? 0} / 台账 ${r.inputs?.dirtyRows ?? 0})⇒ 接受 ${r.inputs?.accepted ?? 0}`);
137
- log(`聚类 ${r.clusterStats?.clusters ?? 0} 组(单条组 ${r.clusterStats?.singletonClusters ?? 0} · 无向量 ${r.clusterStats?.noVector ?? 0})`);
138
- for (const p of r.planned ?? []) log(` ${p.action === 'call' ? '→ 调模型' : '· 跳过 '} ${p.key} ids=[${p.ids.join(',')}] ${p.action}${p.chars ? ` (${p.chars} 字)` : ''}`);
139
- for (const c of r.calls ?? []) log(` ✦ ${c.key} 调用 ${c.ok ? 'OK' : '失败'} · ${c.tokens} token · 结论 ${c.conclusionChars} 字${c.error ? ` · ${c.error}` : ''}`);
140
- log(`提炼条数 ${r.written}${r.writtenIds?.length ? `(新 id ${r.writtenIds.join(',')})` : ''}`);
141
- if (r.stopping) log(`停机:${r.stopping}`);
142
- log(`当日调用 ${state.cost?.calls ?? 0}/${limits.callsPerDay} · 当日 token ${(Number(state.cost?.promptTokens ?? 0) + Number(state.cost?.completionTokens ?? 0))}/${limits.tokensPerDay}`);
143
- for (const w of r.warnings ?? []) log(`⚠️ ${w}`);
144
- log(`日报行:${dailyLine}`);
145
- }
146
- return r.ok ? 0 : (r.exitCode || 1);
241
+ const ctx = { dbPath, vectorsDb, stateFile, now };
242
+ const payload = composePayload({ r: run, state, limits, dailyLine, daily, ctx, owner });
243
+ if (json) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
244
+ else printSummary({ r: run, state, limits, dailyLine, log });
245
+ return run.ok ? 0 : (run.exitCode || 1);
147
246
  }
148
247
 
149
248
  if (isMainEntry(import.meta.url)) {
150
- main().then((code) => { process.exitCode = code; }).catch((e) => {
151
- console.error(`未捕获异常:${e && e.stack ? e.stack : e}`);
152
- process.exitCode = 1;
153
- });
249
+ main()
250
+ .then((code) => {
251
+ process.exitCode = code;
252
+ })
253
+ .catch((e) => {
254
+ console.error(`未捕获异常:${e?.stack ?? e}`);
255
+ process.exitCode = 1;
256
+ });
154
257
  }
155
-
156
- export { main, resolveOwnerUser, STATE_SCHEMA };