@customize-agent/server 4.60.31 → 4.60.32
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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +32 -29
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.json +1 -1
- package/.next/server/chunks/1243.js +1 -1
- package/.next/server/chunks/1936.js +8 -8
- package/.next/server/chunks/2328.js +1 -1
- package/.next/server/chunks/6822.js +1 -1
- package/.next/server/chunks/{4722.js → 7848.js} +13 -13
- package/.next/server/chunks/8204.js +1 -1
- package/.next/server/chunks/9666.js +51 -50
- package/.next/server/instrumentation.js +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages/api/config/embedding/healthCheck.js +2 -1
- package/.next/server/pages/api/config/embedding.js +2 -1
- package/.next/server/pages/api/config/healthCheck.js +2 -1
- package/.next/server/pages/api/config/models.js +2 -1
- package/.next/server/pages/api/config/providers/[provider].js +2 -1
- package/.next/server/pages/api/config/providers.js +2 -1
- package/.next/server/pages/api/config/reset.js +3 -0
- package/.next/server/pages/api/config/reset.js.nft.json +1 -0
- package/.next/server/pages/api/config/web-access.js +2 -1
- package/.next/server/pages/api/documents/export.js +1 -1
- package/.next/server/pages/api/documents/generate/preview.js +1 -1
- package/.next/server/pages/api/documents/generate.js +1 -1
- package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/confirm.js +1 -1
- package/.next/server/pages/api/health.js +1 -1
- package/.next/server/pages/api/kb/features.js +2 -1
- package/.next/server/pages/api/prompt/chat.js +3 -2
- package/.next/server/pages/api/system/reset.js +1 -1
- package/.next/server/pages/canvas/[id].html +1 -1
- package/.next/server/pages/canvas.html +1 -1
- package/.next/server/pages/documents.html +1 -1
- package/.next/server/pages/guide.html +1 -1
- package/.next/server/pages/index.html +1 -1
- package/.next/server/pages/jobs.html +1 -1
- package/.next/server/pages/knowledge/file-detail.html +1 -1
- package/.next/server/pages/knowledge/files.html +1 -1
- package/.next/server/pages/knowledge/manage.html +1 -1
- package/.next/server/pages/knowledge/source-preview.html +1 -1
- package/.next/server/pages/knowledge.html +1 -1
- package/.next/server/pages/models.html +1 -1
- package/.next/server/pages/overview.html +1 -1
- package/.next/server/pages/prompt.html +1 -1
- package/.next/server/pages/settings.html +1 -1
- package/.next/server/pages/skills/[id]/edit.html +1 -1
- package/.next/server/pages/skills/[id]/edit.js.nft.json +1 -1
- package/.next/server/pages/skills/[id].html +1 -1
- package/.next/server/pages/skills/[id].js.nft.json +1 -1
- package/.next/server/pages/skills/new.html +1 -1
- package/.next/server/pages/skills/new.js.nft.json +1 -1
- package/.next/server/pages/skills.html +1 -1
- package/.next/server/pages/system/logs.html +1 -1
- package/.next/server/pages-manifest.json +37 -36
- package/.next/static/279fd5e41d98af82751903f8ea0267af84a75c794a35915a6e294777024190f8/_buildManifest.js +1 -0
- package/.next/static/chunks/1141-d420a7685bc3623c.js +1 -0
- package/.next/static/chunks/2864-b040f2bffa08c139.js +6 -0
- package/.next/static/chunks/3456-70bb03a39473f693.js +1 -0
- package/.next/static/chunks/3980-73841c40d55ddfa1.js +1 -0
- package/.next/static/chunks/4337-a670a1a94ab505d9.js +1 -0
- package/.next/static/chunks/6506-ac0ba7338cc84b7d.js +1 -0
- package/.next/static/chunks/6748-07b0b05bf03c72f0.js +1 -0
- package/.next/static/chunks/9937-e4b2deadd24e3962.js +1 -0
- package/.next/static/chunks/pages/{_app-73e5b40b24a7fe7d.js → _app-646f249ed0cc363f.js} +13 -13
- package/.next/static/chunks/pages/canvas/{[id]-45da860b862e0743.js → [id]-18865f1b7e3a4696.js} +1 -1
- package/.next/static/chunks/pages/canvas-47a86f1679c159af.js +1 -0
- package/.next/static/chunks/pages/knowledge/file-detail-3d1ffd43f4f0e47b.js +1 -0
- package/.next/static/chunks/pages/knowledge/{source-preview-40ff4936b832787e.js → source-preview-24936b9a746ed07f.js} +1 -1
- package/.next/static/chunks/pages/models-e679c45d71631078.js +1 -0
- package/.next/static/chunks/pages/skills/[id]/{edit-35892def57c4975f.js → edit-99e616d5019e80b1.js} +1 -1
- package/.next/static/chunks/pages/skills/[id]-71f696be252bbf0d.js +1 -0
- package/.next/static/chunks/pages/skills/{new-a279e6ca2f7aded7.js → new-00886ea4c676f927.js} +1 -1
- package/.next/static/css/808682ca3875966b.css +1 -0
- package/messages/en-US.json +11 -0
- package/messages/zh-CN.json +11 -0
- package/package.json +3 -3
- package/scripts/brand-moon-fit.cjs +0 -47
- package/scripts/generate-brand-assets.cjs +0 -41
- package/scripts/half-built-scan.allow.json +16 -6
- package/scripts/half-built-scan.baseline.json +2 -17
- package/scripts/half-built-scan.mjs +1 -84
- package/.next/server/chunks/6021.js +0 -1
- package/.next/static/0fdab21c22b65a3042c22ee4ac8e49fbfd22e365df64d5e9b709ccb05aecf02e/_buildManifest.js +0 -1
- package/.next/static/chunks/2378-c6d25abe02815666.js +0 -1
- package/.next/static/chunks/2864-67ba808c5092c2f9.js +0 -6
- package/.next/static/chunks/3456-8b609f8475ae5030.js +0 -1
- package/.next/static/chunks/5046-abead1022185a99a.js +0 -1
- package/.next/static/chunks/5257-915621e439b56580.js +0 -1
- package/.next/static/chunks/6505-012e90937e1e8dae.js +0 -1
- package/.next/static/chunks/6748-04b5429330e1451e.js +0 -1
- package/.next/static/chunks/7955-3a7ee8cda4f857b9.js +0 -1
- package/.next/static/chunks/pages/canvas-a15773d02c97b7a6.js +0 -1
- package/.next/static/chunks/pages/knowledge/file-detail-5859f0c25af34ffe.js +0 -1
- package/.next/static/chunks/pages/models-0aaff5bfadc7b62f.js +0 -1
- package/.next/static/chunks/pages/skills/[id]-19c352e150e05c27.js +0 -1
- package/.next/static/css/837ac5def44e2eba.css +0 -1
- package/scripts/capture-baseline.mjs +0 -353
- /package/.next/static/{0fdab21c22b65a3042c22ee4ac8e49fbfd22e365df64d5e9b709ccb05aecf02e → 279fd5e41d98af82751903f8ea0267af84a75c794a35915a6e294777024190f8}/_ssgManifest.js +0 -0
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* capture-baseline:验收基线采集(方案 P8.5 / Z5 的 A 组)。
|
|
4
|
-
*
|
|
5
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
-
* ## 为什么需要这个脚本(方案 14.2 的原话:归档作为评估基线的成色)
|
|
7
|
-
*
|
|
8
|
-
* `~/.customize-agent/projects/<id>/generatedDocuments/` 里有 288 份 draft、281 份 assets、
|
|
9
|
-
* 46 份交付报告。**够**判据行为回归;**不够**做「改造是否让文档更好」的判定,因为归档里缺两样:
|
|
10
|
-
*
|
|
11
|
-
* - ❌ **无 `blockLedger`**(块级目标/实际/失败类别)——没有它就答不出「篇幅失守是写作侧还是修复侧」;
|
|
12
|
-
* - ❌ **无逐调用 token/缓存遥测**——成本与缓存命中率**当前不可重建**。
|
|
13
|
-
*
|
|
14
|
-
* 于是「改了之后是不是退化了」在数据上无从判断(S4.2)。本脚本把这两样**在生成结束的那一刻**
|
|
15
|
-
* 抄下来,与七维评价回执一起冻结成一份可 diff 的基线。
|
|
16
|
-
*
|
|
17
|
-
* ## 为什么是「采集归档」而不是「另跑一遍管线」
|
|
18
|
-
*
|
|
19
|
-
* 生成只发生一次,**产物就是归档**。脚本不重跑任何东西——它读归档、按 baseline schema 抽取、
|
|
20
|
-
* 落成一份 `*.baseline.json`。这样:
|
|
21
|
-
* 1. 采集本身**零 LLM 成本**(跑一次真实文档 ~50 分钟,重跑一遍纯属浪费);
|
|
22
|
-
* 2. 基线与被评的那份文档**逐字段同源**(不会出现「基线是另一轮跑的」这种不可比);
|
|
23
|
-
*
|
|
24
|
-
* ## 用法
|
|
25
|
-
*
|
|
26
|
-
* ```bash
|
|
27
|
-
* # 采集一份既有归档(docId 见 generatedDocuments/index.json)
|
|
28
|
-
* node apps/server/scripts/capture-baseline.mjs --project-root . --doc doc-1790407778481-d7a36837
|
|
29
|
-
* # 采集最近 N 份
|
|
30
|
-
* node apps/server/scripts/capture-baseline.mjs --project-root . --latest 2
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* 落盘:`~/.customize-agent/baselines/<时间戳>-<label>.baseline.json`(**append-only**,
|
|
34
|
-
* 不覆盖历史基线——「同代码重跑波动 ±7」这件事只能靠多份基线看出来)。
|
|
35
|
-
*
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import crypto from 'node:crypto';
|
|
39
|
-
import fs from 'node:fs';
|
|
40
|
-
import os from 'node:os';
|
|
41
|
-
import path from 'node:path';
|
|
42
|
-
|
|
43
|
-
const DEFAULT_BASE_URL = process.env.BASELINE_BASE_URL || 'http://localhost:17321';
|
|
44
|
-
|
|
45
|
-
function parseArgs(argv) {
|
|
46
|
-
const args = { projectRoot: process.cwd(), docs: [], corpora: [], latest: 0, label: '', baseUrl: DEFAULT_BASE_URL, timeoutMinutes: 90 };
|
|
47
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
48
|
-
const token = argv[index];
|
|
49
|
-
const next = () => argv[++index];
|
|
50
|
-
if (token === '--project-root') args.projectRoot = path.resolve(next());
|
|
51
|
-
else if (token === '--doc') args.docs.push(next());
|
|
52
|
-
else if (token === '--corpus') args.corpora.push(next());
|
|
53
|
-
else if (token === '--latest') args.latest = Number(next());
|
|
54
|
-
else if (token === '--label') args.label = next();
|
|
55
|
-
else if (token === '--base-url') args.baseUrl = next();
|
|
56
|
-
else if (token === '--timeout-minutes') args.timeoutMinutes = Number(next());
|
|
57
|
-
else if (token === '--help' || token === '-h') { printHelp(); process.exit(0); }
|
|
58
|
-
else throw new Error(`未知参数:${token}(--help 看用法)`);
|
|
59
|
-
}
|
|
60
|
-
return args;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function printHelp() {
|
|
64
|
-
process.stdout.write([
|
|
65
|
-
'capture-baseline:把既有归档抽成可 diff 的验收基线(方案 P8.5)。',
|
|
66
|
-
'',
|
|
67
|
-
' --project-root <dir> 文档所属项目根(默认当前目录;决定归档目录)',
|
|
68
|
-
' --doc <docId> 采集指定文档(可重复)',
|
|
69
|
-
' --latest <N> 采集最近 N 份已完成文档',
|
|
70
|
-
' --corpus <关键词> **按既有归档的输入原样重跑一份**再采集(可重复):从归档里找最近一份',
|
|
71
|
-
' 匹配该关键词(材料组名/要求文本)的记录,用它的 promptIds/requirement/',
|
|
72
|
-
' title/materialRoot 重跑——这是"改了代码之后同一份语料还能不能达到同样质量"',
|
|
73
|
-
' 的取数方式(含巢湖/合工大这类真实语料)',
|
|
74
|
-
' --label <name> 基线标签(默认 baseline)',
|
|
75
|
-
' --base-url <url> 服务地址(默认 http://localhost:17321)',
|
|
76
|
-
' --timeout-minutes <N> 单份生成等待上限(默认 90)',
|
|
77
|
-
'',
|
|
78
|
-
'',
|
|
79
|
-
].join('\n'));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ── 归档读取(与 generatedDocumentService 的落盘布局同源:~/.customize-agent/projects/<projectId>/generatedDocuments)──
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 项目 id(`sha256(绝对路径)` 前 12 位,与 `@customize-agent/knowledge` 的 `computeProjectId` 逐字同构)。
|
|
86
|
-
* **只用于定位归档目录**,不参与任何评分口径——口径单源在服务端,本脚本只读。
|
|
87
|
-
*
|
|
88
|
-
* ⚠️ 刻意不 import 那个包:脚本要在**服务未启动 / 依赖未构建**时也能跑(验收现场最需要它的时候,
|
|
89
|
-
* 往往正是服务刚崩的时候)。同构关系由下面这条自检守住——算法一旦漂移,脚本会立刻找不到归档,
|
|
90
|
-
* 而不是悄悄采到空基线。
|
|
91
|
-
*/
|
|
92
|
-
function projectIdOf(projectRoot) {
|
|
93
|
-
return crypto.createHash('sha256').update(path.resolve(projectRoot)).digest('hex').slice(0, 12);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function archiveDir(projectRoot) {
|
|
97
|
-
return path.join(os.homedir(), '.customize-agent', 'projects', projectIdOf(projectRoot), 'generatedDocuments');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── 抽取:只抽「归档里已有、但没被结构化保存」的那些量 ─────────────────────
|
|
101
|
-
|
|
102
|
-
function num(value) {
|
|
103
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 冻结合同七维回执与阻断数,直接读取当前正文的实际测量。
|
|
108
|
-
* 换算会让基线与被评文档的差异混入「脚本口径」这个第三变量。
|
|
109
|
-
*/
|
|
110
|
-
function scoresOf(record) {
|
|
111
|
-
const report = record?.reviewMetadata?.contractQuality;
|
|
112
|
-
const score = report?.score;
|
|
113
|
-
return {
|
|
114
|
-
contractId: report?.contractId ?? null,
|
|
115
|
-
documentHash: report?.documentHash ?? null,
|
|
116
|
-
dimensions: score?.dimensions ?? null,
|
|
117
|
-
deliveryReady: score?.deliveryReady ?? null,
|
|
118
|
-
verificationComplete: score?.verificationComplete ?? null,
|
|
119
|
-
qualityRated: score?.qualityRated ?? null,
|
|
120
|
-
qualityGaps: score?.qualityGaps ?? null,
|
|
121
|
-
measuredCriteria: Array.isArray(report?.findings) ? report.findings.length : null,
|
|
122
|
-
// 导出门禁保留实际阻断数;缺少评价回执不能当作零分或通过。
|
|
123
|
-
blockingCount: Array.isArray(record?.draft?.exportGate?.blockingIssues) ? record.draft.exportGate.blockingIssues.length : null,
|
|
124
|
-
exportGatePassed: record?.draft?.exportGate?.passed ?? null,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** 块级篇幅账(方案 14.2 点名的缺口之一) */
|
|
129
|
-
function blockLedgerOf(record) {
|
|
130
|
-
const ledger = record?.reviewMetadata?.diagnostics?.blockLedger || record?.draft?.reviewMetadata?.diagnostics?.blockLedger || [];
|
|
131
|
-
const rows = Array.isArray(ledger) ? ledger : [];
|
|
132
|
-
const primary = rows.filter(row => !row.isolatedRetry);
|
|
133
|
-
return {
|
|
134
|
-
rows: primary.length,
|
|
135
|
-
targetChars: primary.reduce((sum, row) => sum + (num(row.target) || 0), 0),
|
|
136
|
-
actualChars: primary.reduce((sum, row) => sum + (num(row.actual) || 0), 0),
|
|
137
|
-
firstPassRate: primary.length > 0 ? primary.filter(row => row.passed && (num(row.attempt) || 0) === 0).length / primary.length : null,
|
|
138
|
-
failureKinds: primary.flatMap(row => row.failureKinds || []).reduce((acc, kind) => { acc[kind] = (acc[kind] || 0) + 1; return acc; }, {}),
|
|
139
|
-
ledger, // 明细整段落盘:缺口归因要靠它,不要在脚本里预先聚合掉信息
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** 逐调用 token/缓存遥测(方案 14.2 点名的缺口之二) */
|
|
144
|
-
function llmTelemetryOf(record) {
|
|
145
|
-
const diagnostics = record?.reviewMetadata?.diagnostics || record?.draft?.reviewMetadata?.diagnostics || {};
|
|
146
|
-
const llm = diagnostics.llm || {};
|
|
147
|
-
return {
|
|
148
|
-
calls: num(llm.calls), failures: num(llm.failures), retries: num(llm.retries), schemaFailures: num(llm.schemaFailures),
|
|
149
|
-
maxActive: num(llm.maxActive),
|
|
150
|
-
inputTokens: num(llm.inputTokens), outputTokens: num(llm.outputTokens),
|
|
151
|
-
promptCacheHitTokens: num(llm.promptCacheHitTokens), promptCacheMissTokens: num(llm.promptCacheMissTokens),
|
|
152
|
-
inputChars: num(llm.inputChars),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 修复链净增 + 逐轮明细(Z7 R1:退役决策的输入)与关链状态(Z5) */
|
|
157
|
-
function repairChainOf(record) {
|
|
158
|
-
const diagnostics = record?.reviewMetadata?.diagnostics || record?.draft?.reviewMetadata?.diagnostics || {};
|
|
159
|
-
const telemetry = record?.reviewMetadata?.telemetry || {};
|
|
160
|
-
return {
|
|
161
|
-
netChars: diagnostics.repairChainNetChars || telemetry.repairChainNetChars || null,
|
|
162
|
-
switchState: diagnostics.repairChain || null,
|
|
163
|
-
repairHeat: telemetry.repairHeat || null,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function baselineOf(record, options) {
|
|
168
|
-
const diagnostics = record?.reviewMetadata?.diagnostics || {};
|
|
169
|
-
const startedAt = num(record.createdAt);
|
|
170
|
-
const endedAt = num(record.completedAt);
|
|
171
|
-
const elapsedMs = num(record.elapsedMs) ?? (startedAt !== null && endedAt !== null && endedAt >= startedAt ? endedAt - startedAt : null);
|
|
172
|
-
return {
|
|
173
|
-
schema: 'capture-baseline/v1',
|
|
174
|
-
capturedAt: new Date().toISOString(),
|
|
175
|
-
recordContentHash: crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex'),
|
|
176
|
-
runId: record.runId ?? null,
|
|
177
|
-
workflowVersion: record.workflowVersion ?? null,
|
|
178
|
-
// 阶段墙钟不能相加当成等待时间;并发阶段与调用耗时可能重叠。
|
|
179
|
-
metrics: diagnostics.metrics ?? record.reviewMetadata?.metrics ?? record.draft?.reviewMetadata?.diagnostics?.metrics ?? null,
|
|
180
|
-
doc: {
|
|
181
|
-
id: record.id, title: record.title, templateId: record.templateId, templateName: record.templateName,
|
|
182
|
-
status: record.status, elapsedMs, wordCount: num(record.wordCount),
|
|
183
|
-
markdownChars: typeof record.markdown === 'string' ? record.markdown.length : null,
|
|
184
|
-
completedAt: record.completedAt ?? null,
|
|
185
|
-
},
|
|
186
|
-
scores: scoresOf(record),
|
|
187
|
-
blockLedger: blockLedgerOf(record),
|
|
188
|
-
llm: llmTelemetryOf(record),
|
|
189
|
-
repairChain: repairChainOf(record),
|
|
190
|
-
note: options.note || '',
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function baselinesDir() { return path.join(os.homedir(), '.customize-agent', 'baselines'); }
|
|
195
|
-
|
|
196
|
-
async function postJson(url, body, timeoutMs = 60000) {
|
|
197
|
-
const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs) });
|
|
198
|
-
const text = await response.text();
|
|
199
|
-
let parsed = null;
|
|
200
|
-
try { parsed = JSON.parse(text); } catch { /* 非 JSON 响应原样带出 */ }
|
|
201
|
-
return { status: response.status, body: parsed ?? text };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* ★ 实测修正(2026-09-30,A3 腿):轮询超时 60s → **180s**,且**轮询失败不再终止采集**。
|
|
206
|
-
*
|
|
207
|
-
* 现场:生成中的服务端被 LLM 调用打满时,列表端点可能 >60s 才回答,`AbortSignal.timeout(60000)`
|
|
208
|
-
* 直接抛 `The operation was aborted due to timeout` ⇒ **采集脚本死在轮询上,而生成还在跑**
|
|
209
|
-
* (本轮 A3 腿就是这么丢的:脚本 05:29 报失败、文档仍在 generating)。
|
|
210
|
-
* 采集器的职责是"等",不是"因为服务忙就放弃"——超时只该由 `deadline`(`--timeout-minutes`)决定。
|
|
211
|
-
*/
|
|
212
|
-
async function getJson(url, timeoutMs = 180000) {
|
|
213
|
-
try {
|
|
214
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
215
|
-
const text = await response.text();
|
|
216
|
-
try { return { status: response.status, body: JSON.parse(text) }; } catch { return { status: response.status, body: text }; }
|
|
217
|
-
} catch (error) {
|
|
218
|
-
// 轮询失败 ⇒ 返回空体(调用方继续等);**不 throw**(服务忙不等于生成失败)
|
|
219
|
-
process.stdout.write(`[baseline] 轮询失败(继续等,不影响生成):${error instanceof Error ? error.message : String(error)}\n`);
|
|
220
|
-
return { status: 0, body: undefined };
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const TERMINAL = new Set(['completed', 'completed_with_issues', 'warning', 'failed', 'aborted']);
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* ★ 2026-09-28:触发入参改成 **§12.1 三字段 + 语料四件套**(原实现传的是已退役的 `templateId` 单字段)。
|
|
228
|
-
* `corpus` 给出时,用它的 `promptIds/requirement/title/materialRoot` **原样重跑**——
|
|
229
|
-
* 语料可复现是基线可比的前提(换了提示词或材料组就不是同一份文档了)。
|
|
230
|
-
*/
|
|
231
|
-
async function triggerAndWait({ baseUrl, projectRoot, corpus, timeoutMinutes }) {
|
|
232
|
-
const started = await postJson(`${baseUrl}/api/documents/generate`,
|
|
233
|
-
{ promptIds: corpus.promptIds, requirement: corpus.requirement || '', title: corpus.title, materialRoot: corpus.materialRoot, projectRoot }, 120000);
|
|
234
|
-
if (started.status !== 202 && started.status !== 200) {
|
|
235
|
-
throw new Error(`触发失败(HTTP ${started.status}):${typeof started.body === 'string' ? started.body : JSON.stringify(started.body)}`);
|
|
236
|
-
}
|
|
237
|
-
const documentId = started.body?.documentId || started.body?.record?.id || started.body?.id;
|
|
238
|
-
if (!documentId) throw new Error(`触发返回里没有 documentId:${JSON.stringify(started.body).slice(0, 400)}`);
|
|
239
|
-
process.stdout.write(`[baseline] 已触发 ${corpus.title}(语料:${corpus.materialRoot}) → ${documentId}(等待生成,通常 40–60 分钟)\n`);
|
|
240
|
-
const deadline = Date.now() + timeoutMinutes * 60000;
|
|
241
|
-
let lastLog = 0;
|
|
242
|
-
for (;;) {
|
|
243
|
-
if (Date.now() > deadline) throw new Error(`等待超时(${timeoutMinutes} 分钟):${documentId}`);
|
|
244
|
-
await new Promise(resolve => setTimeout(resolve, 20000));
|
|
245
|
-
const { body } = await getJson(`${baseUrl}/api/documents/generated?id=${encodeURIComponent(documentId)}&projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
246
|
-
const record = body?.document;
|
|
247
|
-
if (!record) continue;
|
|
248
|
-
if (Date.now() - lastLog > 300000) {
|
|
249
|
-
lastLog = Date.now();
|
|
250
|
-
process.stdout.write(`[baseline] ${documentId} 状态=${record.status} 字数=${record.wordCount ?? '?'}\n`);
|
|
251
|
-
}
|
|
252
|
-
if (TERMINAL.has(record.status)) return record;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function loadArchivedRecord({ projectRoot, docId }) {
|
|
257
|
-
const file = path.join(archiveDir(projectRoot), 'drafts', `${docId}.json`);
|
|
258
|
-
if (!fs.existsSync(file)) throw new Error(`归档不存在:${file}`);
|
|
259
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* ★ 2026-09-28:按关键词找**最近的语料记录**并抽出重跑所需的输入(四件套 + 项目根)。
|
|
264
|
-
* 匹配面:`materialRoot`(材料组名)与 `requirement`(要求文本)——真实语料通常两者都带关键词。
|
|
265
|
-
*/
|
|
266
|
-
function findCorpus({ projectRoot, keyword }) {
|
|
267
|
-
const dir = path.join(archiveDir(projectRoot), 'drafts');
|
|
268
|
-
if (!fs.existsSync(dir)) throw new Error(`归档目录不存在:${dir}`);
|
|
269
|
-
const candidates = fs.readdirSync(dir)
|
|
270
|
-
.filter(name => name.endsWith('.json') && !name.endsWith('.meta.json'))
|
|
271
|
-
.map(name => {
|
|
272
|
-
const full = path.join(dir, name);
|
|
273
|
-
try { return { full, stat: fs.statSync(full), record: JSON.parse(fs.readFileSync(full, 'utf8')) }; } catch { return undefined; }
|
|
274
|
-
})
|
|
275
|
-
.filter(Boolean)
|
|
276
|
-
.filter(item => `${item.record.materialRoot || ''} ${item.record.requirement || ''} ${item.record.title || ''}`.includes(keyword))
|
|
277
|
-
.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs);
|
|
278
|
-
if (candidates.length === 0) throw new Error(`归档里找不到含「${keyword}」的记录(材料组名/要求文本/标题都没命中):${dir}`);
|
|
279
|
-
const newest = candidates[0].record;
|
|
280
|
-
if (!newest.promptIds?.length) throw new Error(`「${keyword}」最近一份记录没有 promptIds(旧记录),无法原样重跑`);
|
|
281
|
-
return {
|
|
282
|
-
sourceDocId: newest.id,
|
|
283
|
-
promptIds: newest.promptIds,
|
|
284
|
-
requirement: newest.requirement || '',
|
|
285
|
-
title: newest.templateName || newest.title || '',
|
|
286
|
-
materialRoot: newest.materialRoot || '',
|
|
287
|
-
projectRoot: newest.projectRoot || projectRoot,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function latestDocIds({ projectRoot, count }) {
|
|
292
|
-
const indexFile = path.join(archiveDir(projectRoot), 'index.json');
|
|
293
|
-
if (!fs.existsSync(indexFile)) throw new Error(`索引不存在:${indexFile}`);
|
|
294
|
-
const entries = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
295
|
-
return entries
|
|
296
|
-
.filter(entry => entry.completedAt && String(entry.status).startsWith('completed'))
|
|
297
|
-
.sort((left, right) => (right.completedAt || 0) - (left.completedAt || 0))
|
|
298
|
-
.slice(0, count)
|
|
299
|
-
.map(entry => entry.id);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── 主流程 ─────────────────────────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
async function main() {
|
|
305
|
-
const args = parseArgs(process.argv.slice(2));
|
|
306
|
-
const records = [];
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* ★ 2026-09-28:**按既有归档的输入原样重跑**(真实语料基线:巢湖 / 合工大这类)。
|
|
310
|
-
* 每个关键词解析出一份语料四件套,逐份触发 + 等待 + 采集——同一份语料在改动前后各跑一次,
|
|
311
|
-
* 两次的 `*.baseline.json` 可直接 diff(脚本的产物是可 diff 的基线,判定仍不在这里)。
|
|
312
|
-
*/
|
|
313
|
-
for (const keyword of args.corpora) {
|
|
314
|
-
const corpus = findCorpus({ projectRoot: args.projectRoot, keyword });
|
|
315
|
-
process.stdout.write(`[baseline] 语料「${keyword}」命中 ${corpus.sourceDocId}:标题「${corpus.title}」材料组「${corpus.materialRoot}」\n`);
|
|
316
|
-
records.push(await triggerAndWait({ baseUrl: args.baseUrl, projectRoot: args.projectRoot, corpus, timeoutMinutes: args.timeoutMinutes }));
|
|
317
|
-
}
|
|
318
|
-
if (args.latest > 0) {
|
|
319
|
-
for (const docId of latestDocIds({ projectRoot: args.projectRoot, count: args.latest })) {
|
|
320
|
-
records.push(loadArchivedRecord({ projectRoot: args.projectRoot, docId }));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
for (const docId of args.docs) records.push(loadArchivedRecord({ projectRoot: args.projectRoot, docId }));
|
|
324
|
-
if (records.length === 0) { printHelp(); process.exit(1); }
|
|
325
|
-
|
|
326
|
-
const baselines = records.map(record => baselineOf(record, args));
|
|
327
|
-
const stamp = new Date().toISOString().replace(/[:.]/gu, '-');
|
|
328
|
-
const label = args.label || 'baseline';
|
|
329
|
-
const outDir = baselinesDir();
|
|
330
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
331
|
-
const outFile = path.join(outDir, `${stamp}-${label}.baseline.json`);
|
|
332
|
-
fs.writeFileSync(outFile, `${JSON.stringify({ capturedAt: new Date().toISOString(), label, projectRoot: path.resolve(args.projectRoot), baselines }, null, 2)}\n`);
|
|
333
|
-
|
|
334
|
-
process.stdout.write('\n[baseline] 采集完成\n');
|
|
335
|
-
for (const item of baselines) {
|
|
336
|
-
const dims = item.scores.dimensions ? JSON.stringify(item.scores.dimensions) : '(无 dimensions)';
|
|
337
|
-
process.stdout.write(
|
|
338
|
-
` ${item.doc.id} ${item.doc.title} 状态=${item.doc.status} ${item.doc.elapsedMs === null ? '耗时未知' : Math.round(item.doc.elapsedMs / 60000) + 'min'}\n`
|
|
339
|
-
+ ` 综合区间=${item.scores.overallInterval.lower ?? '?'}~${item.scores.overallInterval.upper ?? '?'} 阻断=${item.scores.blockingCount ?? '?'} 七维=${dims}\n`
|
|
340
|
-
+ ` 块账=${item.blockLedger.rows} 块 Σ目标=${item.blockLedger.targetChars} Σ实际=${item.blockLedger.actualChars}\n`
|
|
341
|
-
+ ` LLM 调用=${item.llm.calls ?? '?'} 缓存命中/未命中=${item.llm.promptCacheHitTokens ?? '?'}/${item.llm.promptCacheMissTokens ?? '?'}\n`
|
|
342
|
-
+ ` 修复链净增=${item.repairChain.netChars?.delta ?? '?'}(${item.repairChain.netChars ? (item.repairChain.netChars.ratio * 100).toFixed(1) + '%' : '?'})\n`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
process.stdout.write(`[baseline] 落盘:${outFile}\n`);
|
|
346
|
-
process.stdout.write('[baseline] 基线只抄写归档已有值,未测项保留为空。\n');
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
main().catch(error => {
|
|
351
|
-
process.stderr.write(`[baseline] 失败:${error instanceof Error ? error.message : String(error)}\n`);
|
|
352
|
-
process.exit(1);
|
|
353
|
-
});
|