dowafu 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -25
- package/README_zh-tw.md +197 -0
- package/dist/adapters/anthropic-messages.js +18 -11
- package/dist/adapters/gemini-native.js +19 -16
- package/dist/adapters/read-file-tool-description.js +5 -0
- package/dist/adapters/responses.js +26 -14
- package/dist/audit.js +17 -1
- package/dist/cli-args.js +103 -38
- package/dist/cli.js +78 -43
- package/dist/dispatch-home.js +24 -6
- package/dist/doctor.js +91 -0
- package/dist/error-classify.js +13 -4
- package/dist/gate.js +3 -2
- package/dist/mask.js +16 -3
- package/dist/messages.js +396 -0
- package/dist/output.js +60 -37
- package/dist/prompt.js +5 -3
- package/dist/providers.js +46 -34
- package/dist/raw-integrity.js +6 -5
- package/dist/report.js +76 -22
- package/dist/runner.js +21 -10
- package/dist/ticket.js +35 -25
- package/dist/validate.js +21 -20
- package/dist/whitelist.js +9 -1
- package/package.json +3 -2
- package/providers.json +8 -3
- package/publish/en/.agents/skills/find-holes-external/SKILL.md +450 -0
- package/publish/en/.agents/skills/preflight/SKILL.md +137 -0
- package/publish/en/.agents/skills/wrap/SKILL.md +64 -0
- package/publish/en/.claude/agents/explore-haiku.md +8 -0
- package/publish/en/.claude/agents/hole-finder-cost.md +15 -0
- package/publish/en/.claude/agents/hole-finder-feasibility.md +15 -0
- package/publish/en/.claude/agents/hole-finder-safety.md +15 -0
- package/publish/en/.claude/agents/hole-finder.md +14 -0
- package/publish/en/.claude/skills/find-holes/SKILL.md +114 -0
- package/publish/en/.claude/skills/find-holes-external/SKILL.md +463 -0
- package/publish/en/.claude/skills/preflight/SKILL.md +198 -0
- package/publish/en/.claude/skills/wrap/SKILL.md +61 -0
- package/publish/en/README.md +106 -0
- package/publish/en/workflow_spec.md +71 -0
- package/publish/{.agents → zh-tw/.agents}/skills/find-holes-external/SKILL.md +195 -20
- package/publish/{.agents → zh-tw/.agents}/skills/preflight/SKILL.md +49 -7
- package/publish/{.claude → zh-tw/.claude}/skills/find-holes/SKILL.md +32 -4
- package/publish/{.claude → zh-tw/.claude}/skills/find-holes-external/SKILL.md +194 -19
- package/publish/{.claude → zh-tw/.claude}/skills/preflight/SKILL.md +51 -6
- package/publish/{README.md → zh-tw/README.md} +16 -0
- /package/publish/{.agents → zh-tw/.agents}/skills/wrap/SKILL.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/explore-haiku.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-cost.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-feasibility.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-safety.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/skills/wrap/SKILL.md +0 -0
- /package/publish/{workflow_spec.md → zh-tw/workflow_spec.md} +0 -0
package/dist/output.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// plan_dispatch_v1.4.md §12/§16:落檔與 run.jsonl。寫入序列化——前一次 appendFile 未
|
|
2
2
|
// resolve 前不發下一次;呼叫平行,寫入序列。所有落檔內容在寫入前經 maskDeep 遮蔽(§12)。
|
|
3
|
-
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { maskDeep, maskString } from "./mask.js";
|
|
6
|
+
import { m } from "./messages.js";
|
|
6
7
|
export class RunLogWriter {
|
|
7
8
|
filePath;
|
|
9
|
+
lang;
|
|
8
10
|
queue = Promise.resolve();
|
|
9
|
-
constructor(filePath) {
|
|
11
|
+
constructor(filePath, lang) {
|
|
10
12
|
this.filePath = filePath;
|
|
13
|
+
this.lang = lang;
|
|
11
14
|
}
|
|
12
15
|
// plan_dispatch_v2.4.md §12(一):ts 語意為「append 被呼叫的當下」,即事件發生時刻,
|
|
13
16
|
// 故在此(方法本體)取時間,不得延後到 queue 的 then callback 內——落盤時間會被檔案
|
|
@@ -21,7 +24,7 @@ export class RunLogWriter {
|
|
|
21
24
|
// append 都不會執行,且 flush() 也會 rejected,等於一次瞬時寫入失敗讓該次派工
|
|
22
25
|
// 其餘所有事件全部遺失。失敗以 stderr 留痕,不靜默吞掉。
|
|
23
26
|
this.queue = this.queue.then(() => appendFile(this.filePath, line, "utf8")).catch((err) => {
|
|
24
|
-
console.error(
|
|
27
|
+
console.error(m(this.lang, "runLogWriteFailed", maskString(String(err))));
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
async flush() {
|
|
@@ -37,6 +40,20 @@ export async function ensureOutDir(outDir) {
|
|
|
37
40
|
// (issue_log_v2.0.md 2026-08-07:hub 驗收時據此數錯 9/22 vs 實際 7/20)。
|
|
38
41
|
await writeFile(path.join(outDir, "run.jsonl"), "", "utf8");
|
|
39
42
|
}
|
|
43
|
+
// 上面 §13 那條「重跑會覆蓋同名輸出目錄」的前提是**重跑的人知道自己在覆蓋什麼**。
|
|
44
|
+
// 四格 host 對測(2026-08-13)打掉了那個前提:hub 會在派工前自己 rm -rf 掉輸出目錄,
|
|
45
|
+
// 沒有人被問過,而那底下是已經付過錢的產物。覆蓋本身不改(此處語意維持不變),改成
|
|
46
|
+
// 由 cli.ts 在**還沒呼叫任何 API 的地方**擋下來——目錄非空就中止,並要求換 ticket-id。
|
|
47
|
+
// 讀不到目錄(不存在、或權限不足)一律回 false:不存在本來就該放行,權限不足則交給
|
|
48
|
+
// 後面的 ensureOutDir 報 outDirNotWritable,兩者的訊息各自比這裡準確。
|
|
49
|
+
export async function outDirHasArtifacts(outDir) {
|
|
50
|
+
try {
|
|
51
|
+
return (await readdir(outDir)).length > 0;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
40
57
|
// §13:raw 一律無條件覆寫。v2.4 實作期間曾加過「空 raw 不覆蓋磁碟上的非空內容」守衛
|
|
41
58
|
// (由 v2.4 §20 的一條測試要求逼出來),驗收時撤除:它破壞了 §13「同一工單重跑會覆蓋
|
|
42
59
|
// 同名輸出目錄」——重跑時該 spoke 若零 raw,上一次的內容會留著冒充本次證據,與
|
|
@@ -53,22 +70,25 @@ export async function writeRawFiles(outDir, agent, result) {
|
|
|
53
70
|
}
|
|
54
71
|
// §13:「原文產出」欄——failed 無產出;其餘(含 truncated:*)皆落檔,即使不完整,
|
|
55
72
|
// 因為那是已經付過錢的內容。
|
|
56
|
-
export async function writeSpokeText(outDir, agent, result) {
|
|
73
|
+
export async function writeSpokeText(outDir, agent, result, lang) {
|
|
57
74
|
if (result.status === "failed")
|
|
58
75
|
return;
|
|
59
|
-
|
|
76
|
+
// issue_log_v2.5.md 待修 #7:本函式一度是唯一沒有遮蔽的落檔路徑——`raw/` 與 `run.jsonl`
|
|
77
|
+
// 都經 maskDeep,只有這裡直寫 finalText。spoke 讀的是被審專案的原始碼,那裡若有硬編金鑰,
|
|
78
|
+
// 它可以照抄進「原文」欄(§16 模板要求逐字複製),於是秘密落到一個不經遮蔽的檔案裡。
|
|
79
|
+
const content = maskString(result.finalText ?? m(lang, "noFullReportAvailable", result.status));
|
|
60
80
|
await writeFile(path.join(outDir, `${agent}.md`), content, "utf8");
|
|
61
81
|
}
|
|
62
82
|
// plan_dispatch_v2.4.md §13(一):每支 spoke 完成即落檔的落地點。落檔例外不得外傳——
|
|
63
83
|
// 落檔失敗不得使呼叫端的 promise rejected、不得改變該 spoke 的 status(落檔是產物持久化,
|
|
64
84
|
// 不是執行結果),但必須留下痕跡:寫入 result.errors[] 並經 onError 回報。
|
|
65
85
|
// writeSpokeText、writeRawFiles 各自包住例外,其一失敗不影響另一個執行。
|
|
66
|
-
export async function persistSpokeResult(outDir, result, onError) {
|
|
86
|
+
export async function persistSpokeResult(outDir, result, onError, lang) {
|
|
67
87
|
try {
|
|
68
|
-
await writeSpokeText(outDir, result.agent, result);
|
|
88
|
+
await writeSpokeText(outDir, result.agent, result, lang);
|
|
69
89
|
}
|
|
70
90
|
catch (err) {
|
|
71
|
-
const msg =
|
|
91
|
+
const msg = m(lang, "persistTextFailed", result.agent, maskString(String(err)));
|
|
72
92
|
result.errors.push(msg);
|
|
73
93
|
onError(msg);
|
|
74
94
|
}
|
|
@@ -76,7 +96,7 @@ export async function persistSpokeResult(outDir, result, onError) {
|
|
|
76
96
|
await writeRawFiles(outDir, result.agent, result);
|
|
77
97
|
}
|
|
78
98
|
catch (err) {
|
|
79
|
-
const msg =
|
|
99
|
+
const msg = m(lang, "persistRawFailed", result.agent, maskString(String(err)));
|
|
80
100
|
result.errors.push(msg);
|
|
81
101
|
onError(msg);
|
|
82
102
|
}
|
|
@@ -84,35 +104,40 @@ export async function persistSpokeResult(outDir, result, onError) {
|
|
|
84
104
|
function formatStoreCell(store) {
|
|
85
105
|
return store === "false" ? "false" : store;
|
|
86
106
|
}
|
|
87
|
-
const BUDGET_TRIGGER_LABEL = {
|
|
88
|
-
total: "總量",
|
|
89
|
-
reasoning: "推理累積",
|
|
90
|
-
reasoning_round: "推理單輪尖峰",
|
|
91
|
-
};
|
|
92
107
|
// §14:truncated:budget 需顯示觸發來源;reasoning_round 是「模型在某一輪卡住」的異常訊號,
|
|
93
108
|
// 與正常的總量/累積推理超支後續動作不同(前者查 prompt 或換 effort,後者調門檻),
|
|
94
109
|
// 須顯眼區分,不能跟其他 truncated:budget 混在一起看起來像同一種情況。
|
|
95
|
-
function formatStatusCell(r) {
|
|
110
|
+
function formatStatusCell(r, lang) {
|
|
96
111
|
if (r.status !== "truncated:budget" || !r.budgetTrigger)
|
|
97
112
|
return r.status;
|
|
98
|
-
const label =
|
|
99
|
-
const flag = r.budgetTrigger === "reasoning_round" ?
|
|
113
|
+
const label = m(lang, "budgetTriggerLabel", r.budgetTrigger);
|
|
114
|
+
const flag = r.budgetTrigger === "reasoning_round" ? m(lang, "anomalySpikeFlag") : "";
|
|
100
115
|
return `${r.status}(${flag}${label})`;
|
|
101
116
|
}
|
|
102
117
|
// plan_dispatch_v2.0.md §15(二):清單外引用附出現章節與疑似縮寫來源,讓讀者不必自己去猜
|
|
103
118
|
// (issue_log_v2.0.md 2026-08-07:曾有 hub 因為裸字串誤判成稽核器的 bug,推錯了方向)。
|
|
104
|
-
function formatOutsideAllowlistCell(detail) {
|
|
119
|
+
function formatOutsideAllowlistCell(detail, lang) {
|
|
105
120
|
if (detail.length === 0)
|
|
106
|
-
return "
|
|
121
|
+
return m(lang, "noneLabel");
|
|
107
122
|
return detail
|
|
108
123
|
.map((d) => {
|
|
109
|
-
const sectionPart = d.section ?
|
|
110
|
-
const suffixPart = d.suffixOf ?
|
|
111
|
-
return
|
|
124
|
+
const sectionPart = d.section ? m(lang, "outsideAllowlistSection", d.section) : m(lang, "outsideAllowlistNoSection");
|
|
125
|
+
const suffixPart = d.suffixOf ? m(lang, "outsideAllowlistSuffixNote", d.suffixOf) : "";
|
|
126
|
+
return m(lang, "outsideAllowlistEntry", d.path, `${sectionPart}${suffixPart}`);
|
|
112
127
|
})
|
|
113
128
|
.join(",");
|
|
114
129
|
}
|
|
115
|
-
|
|
130
|
+
// plan_i18n_v1.2.md §4.1:SUSPECT_PHRASES 中英兩套並存比對,summary.md 分開標示是哪一套
|
|
131
|
+
// 命中——日後調整這份清單時才有資料可依據,不必再猜一次。
|
|
132
|
+
function formatSuspectPhrasesDetail(a, lang) {
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (a.suspectPhrasesZh.length > 0)
|
|
135
|
+
parts.push(`zh:${a.suspectPhrasesZh.join(",")}`);
|
|
136
|
+
if (a.suspectPhrasesEn.length > 0)
|
|
137
|
+
parts.push(`en:${a.suspectPhrasesEn.join(",")}`);
|
|
138
|
+
return parts.length > 0 ? parts.join(" / ") : m(lang, "noneLabel");
|
|
139
|
+
}
|
|
140
|
+
export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits, lang) {
|
|
116
141
|
const rows = results.map((r) => {
|
|
117
142
|
const a = audits.get(r.agent);
|
|
118
143
|
const t = toolCallAudits.get(r.agent);
|
|
@@ -120,33 +145,31 @@ export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits)
|
|
|
120
145
|
// plan_dispatch_v2.6.md §26 規格四:hub 會讀的地方才有用(「順序放大量」印進乾跑報表
|
|
121
146
|
// 後 hub 才真的重排清單,第 10 次派工,省 52%),放最前面顯眼標示。
|
|
122
147
|
if (r.unknownUsageKeys.length > 0) {
|
|
123
|
-
cells.push(
|
|
148
|
+
cells.push(m(lang, "unknownUsageKeysWarning", r.provider, r.unknownUsageKeys.join(", ")));
|
|
124
149
|
}
|
|
125
150
|
// §15(一):沒看程式碼就作答的簽名,放最前面顯眼標示——見 tool-call-audit.ts。
|
|
126
151
|
if (t?.zeroSourceRead) {
|
|
127
|
-
cells.push(
|
|
152
|
+
cells.push(m(lang, "zeroSourceReadWarning", t.allowedReadsCount));
|
|
128
153
|
}
|
|
129
154
|
if (t) {
|
|
130
|
-
cells.push(
|
|
155
|
+
cells.push(m(lang, "toolCallStats", t.total, t.allowed, t.rejected));
|
|
131
156
|
}
|
|
132
157
|
if (a) {
|
|
133
|
-
cells.push(
|
|
134
|
-
|
|
158
|
+
cells.push(m(lang, "closingLineCell", a.finalLinePass ? "pass" : "fail"),
|
|
159
|
+
// v1.9 §15:null(數不出來)與 0(明確為零)須可區分,不得混印
|
|
160
|
+
m(lang, "observationCountCell", a.observationCount !== null ? String(a.observationCount) : m(lang, "cannotCountObservations")), m(lang, "outsideAllowlistCell", formatOutsideAllowlistCell(a.citedPathsOutsideAllowlistDetail, lang)), m(lang, "cannotVerifySectionCell", a.cannotVerifySectionPresent ? "pass" : "fail"), m(lang, "suspectPhrasesCell", formatSuspectPhrasesDetail(a, lang)));
|
|
135
161
|
}
|
|
136
|
-
const auditCell = cells.length > 0 ? cells.join(" / ") : "
|
|
162
|
+
const auditCell = cells.length > 0 ? cells.join(" / ") : m(lang, "auditUnavailable");
|
|
137
163
|
// plan_fixes_v1.0.md §4:無價目資料須與「估出來是 $0」區分,不能印成空白或 0——
|
|
138
164
|
// 兩者對讀者的意義完全不同(沒資料 vs 免費)。
|
|
139
|
-
const costCell = r.costUsd === null ? "
|
|
140
|
-
return `| ${r.agent} | ${r.provider} | ${r.api} | ${r.modelRequested} | ${r.modelReturned ?? "—"} | ${r.effort ?? "—"} | ${formatStoreCell(r.store)} | ${formatStatusCell(r)} | ${r.latencyMs}ms | ${r.usage.totalTokens} | ${costCell} | ${auditCell} |`;
|
|
165
|
+
const costCell = r.costUsd === null ? m(lang, "noPricingData") : `$${r.costUsd.toFixed(4)}`;
|
|
166
|
+
return `| ${r.agent} | ${r.provider} | ${r.api} | ${r.modelRequested} | ${r.modelReturned ?? "—"} | ${r.effort ?? "—"} | ${formatStoreCell(r.store)} | ${formatStatusCell(r, lang)} | ${r.latencyMs}ms | ${r.usage.totalTokens} | ${costCell} | ${auditCell} |`;
|
|
141
167
|
});
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
| agent | provider | api | model(請求) | model(回傳) | effort | store | status | 耗時 | token | 估算成本 | 稽核 |
|
|
145
|
-
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
168
|
+
return `${m(lang, "summaryHeader", ticketId)}
|
|
146
169
|
${rows.join("\n")}
|
|
147
170
|
`;
|
|
148
171
|
}
|
|
149
|
-
export async function writeSummary(outDir, ticketId, results, audits, toolCallAudits) {
|
|
150
|
-
const md = buildSummaryMarkdown(ticketId, results, audits, toolCallAudits);
|
|
172
|
+
export async function writeSummary(outDir, ticketId, results, audits, toolCallAudits, lang) {
|
|
173
|
+
const md = buildSummaryMarkdown(ticketId, results, audits, toolCallAudits, lang);
|
|
151
174
|
await writeFile(path.join(outDir, "summary.md"), md, "utf8");
|
|
152
175
|
}
|
package/dist/prompt.js
CHANGED
|
@@ -39,7 +39,9 @@ const TOOL_NOTE_EN = "You have one tool, `read_file(path)`. Neither the ticket n
|
|
|
39
39
|
// plan_dispatch_v2.1.md §8(二):組裝順序改為 agent body → 工具說明 → 回報模板。原順序
|
|
40
40
|
// (agent body → 回報模板 → 工具說明)讓工具說明掉在回報模板的固定收尾句之後,在結構上
|
|
41
41
|
// 像附註——而那正是唯一說明「你有工具」的段落(provider log system-1.txt 全文證實)。
|
|
42
|
-
|
|
42
|
+
// plan_i18n_v1.3.md §一之2:不設預設值——不是安全網(九個既有呼叫端都已顯式傳值,
|
|
43
|
+
// 一個都不會紅),是防未來新增的呼叫端靜默拿到與產品預設(en)相反的值。
|
|
44
|
+
export function buildSystemPrompt(agentBody, lang) {
|
|
43
45
|
const toolNote = lang === "en" ? TOOL_NOTE_EN : TOOL_NOTE;
|
|
44
46
|
const template = lang === "en" ? REPORT_TEMPLATE_EN : REPORT_TEMPLATE;
|
|
45
47
|
return [agentBody.trim(), toolNote, template].join("\n\n");
|
|
@@ -76,7 +78,7 @@ function displayTicketDir(ticketDir, repoRoot) {
|
|
|
76
78
|
return ticketDir;
|
|
77
79
|
return rel;
|
|
78
80
|
}
|
|
79
|
-
export function buildFirstUserText(ticketDir, agent, allowedReadsRelative, repoRoot, lang
|
|
81
|
+
export function buildFirstUserText(ticketDir, agent, allowedReadsRelative, repoRoot, lang) {
|
|
80
82
|
const dir = displayTicketDir(ticketDir, repoRoot);
|
|
81
83
|
if (lang === "en") {
|
|
82
84
|
return `Do these in order:
|
|
@@ -98,7 +100,7 @@ ${buildStep3(allowedReadsRelative, lang)}
|
|
|
98
100
|
「允許讀取」清單內的路徑相對於 repo 根目錄,不是相對於工單目錄。
|
|
99
101
|
清單外的檔案會被拒絕。被拒時不要重試,在「無法驗證」欄記下缺什麼。`;
|
|
100
102
|
}
|
|
101
|
-
export function buildFinalizeUserText(lang
|
|
103
|
+
export function buildFinalizeUserText(lang) {
|
|
102
104
|
return lang === "en"
|
|
103
105
|
? "You have reached the execution limit. Produce your report now from what you already have; do not call any more tools."
|
|
104
106
|
: "已達執行上限,請依現有資訊直接產出目前的回報,不要再呼叫工具。";
|
package/dist/providers.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// plan_dispatch_v1.4.md §5:providers.json 載入與驗證。fail closed——api 欄缺失或
|
|
2
2
|
// store:true 即中止(exit 2),不套用預設值繼續跑(§10 步驟 3)。
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { DispatchError } from "./types.js";
|
|
5
|
-
|
|
4
|
+
import { DispatchError, } from "./types.js";
|
|
5
|
+
import { m } from "./messages.js";
|
|
6
|
+
function parseReasoning(raw, providerName, lang) {
|
|
6
7
|
if (!raw || typeof raw !== "object") {
|
|
7
8
|
return { style: null, allowed: [] };
|
|
8
9
|
}
|
|
9
10
|
const r = raw;
|
|
10
11
|
const style = r.style;
|
|
11
12
|
if (style !== undefined && style !== "openai" && style !== "deepseek" && style !== "gemini" && style !== "anthropic") {
|
|
12
|
-
throw new DispatchError(
|
|
13
|
+
throw new DispatchError(m(lang, "reasoningStyleInvalid", providerName, String(style)), 2);
|
|
13
14
|
}
|
|
14
15
|
const allowed = Array.isArray(r.allowed) ? r.allowed.filter((v) => typeof v === "string") : [];
|
|
15
16
|
const modelOverrides = r.modelOverrides && typeof r.modelOverrides === "object"
|
|
@@ -21,75 +22,86 @@ function parseReasoning(raw, providerName) {
|
|
|
21
22
|
let def;
|
|
22
23
|
if (r.default !== undefined) {
|
|
23
24
|
if (typeof r.default !== "string") {
|
|
24
|
-
throw new DispatchError(
|
|
25
|
+
throw new DispatchError(m(lang, "reasoningDefaultNotString", providerName), 2);
|
|
25
26
|
}
|
|
26
27
|
def = r.default;
|
|
27
28
|
}
|
|
28
29
|
if (allowed.length > 0 && def === undefined) {
|
|
29
|
-
throw new DispatchError(
|
|
30
|
+
throw new DispatchError(m(lang, "reasoningDefaultMissing", providerName), 2);
|
|
30
31
|
}
|
|
31
32
|
if (def !== undefined && !allowed.includes(def)) {
|
|
32
|
-
|
|
33
|
+
const list = allowed.length > 0 ? allowed.join(", ") : m(lang, "emptyList");
|
|
34
|
+
throw new DispatchError(m(lang, "reasoningDefaultNotAllowed", providerName, def, list), 2);
|
|
33
35
|
}
|
|
34
36
|
return { style: style ?? null, allowed, default: def, modelOverrides };
|
|
35
37
|
}
|
|
36
38
|
// plan_fixes_v1.0.md §4:pricing 為選填——缺席的 provider/模型無法估算成本(cost.ts
|
|
37
39
|
// 回傳 null,不是 0),不強制每個 provider 都要有價目。有填就驗完整,格式錯即中止
|
|
38
40
|
// (fail closed,與本檔其餘欄位一致),不悄悄忽略壞掉的價目導致成本估算安靜地錯。
|
|
39
|
-
function parsePositiveNumber(v, label) {
|
|
41
|
+
function parsePositiveNumber(v, label, lang) {
|
|
40
42
|
if (typeof v !== "number" || !(v > 0)) {
|
|
41
|
-
throw new DispatchError(
|
|
43
|
+
throw new DispatchError(m(lang, "positiveNumberRequired", label, JSON.stringify(v)), 2);
|
|
42
44
|
}
|
|
43
45
|
return v;
|
|
44
46
|
}
|
|
45
|
-
function parsePricing(raw, providerName) {
|
|
47
|
+
function parsePricing(raw, providerName, lang) {
|
|
46
48
|
if (raw === undefined)
|
|
47
49
|
return undefined;
|
|
48
50
|
if (!raw || typeof raw !== "object") {
|
|
49
|
-
throw new DispatchError(
|
|
51
|
+
throw new DispatchError(m(lang, "pricingNotObject", providerName), 2);
|
|
50
52
|
}
|
|
51
53
|
const out = {};
|
|
52
54
|
for (const [model, entry] of Object.entries(raw)) {
|
|
53
55
|
if (!entry || typeof entry !== "object") {
|
|
54
|
-
throw new DispatchError(
|
|
56
|
+
throw new DispatchError(m(lang, "pricingModelNotObject", providerName, model), 2);
|
|
55
57
|
}
|
|
56
58
|
const e = entry;
|
|
57
59
|
out[model] = {
|
|
58
|
-
inputPerM: parsePositiveNumber(e.inputPerM, `${providerName}.pricing.${model}.inputPerM
|
|
59
|
-
outputPerM: parsePositiveNumber(e.outputPerM, `${providerName}.pricing.${model}.outputPerM
|
|
60
|
+
inputPerM: parsePositiveNumber(e.inputPerM, `${providerName}.pricing.${model}.inputPerM`, lang),
|
|
61
|
+
outputPerM: parsePositiveNumber(e.outputPerM, `${providerName}.pricing.${model}.outputPerM`, lang),
|
|
60
62
|
...(e.cachedInputPerM !== undefined
|
|
61
|
-
? { cachedInputPerM: parsePositiveNumber(e.cachedInputPerM, `${providerName}.pricing.${model}.cachedInputPerM
|
|
63
|
+
? { cachedInputPerM: parsePositiveNumber(e.cachedInputPerM, `${providerName}.pricing.${model}.cachedInputPerM`, lang) }
|
|
62
64
|
: {}),
|
|
63
65
|
...(e.cacheWritePerM !== undefined
|
|
64
|
-
? { cacheWritePerM: parsePositiveNumber(e.cacheWritePerM, `${providerName}.pricing.${model}.cacheWritePerM
|
|
66
|
+
? { cacheWritePerM: parsePositiveNumber(e.cacheWritePerM, `${providerName}.pricing.${model}.cacheWritePerM`, lang) }
|
|
65
67
|
: {}),
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
return out;
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
// v0.2.0:只取 `pricingSource.asOf`,而且**刻意寬鬆**——與上面的 `parsePricing` 相反。
|
|
73
|
+
// 理由是兩者性質不同:`pricing` 是計費輸入,錯了成本會安靜地算錯,所以 fail closed;
|
|
74
|
+
// `asOf` 只是報表上的一個查證日期,壞掉的後果是「少印一個日期」。對它 fail closed 會讓
|
|
75
|
+
// 一份價目完全正確的 `--providers` 檔,因為 metadata 打錯字而整個跑不起來。
|
|
76
|
+
function parsePricingAsOf(raw) {
|
|
77
|
+
if (!raw || typeof raw !== "object")
|
|
78
|
+
return undefined;
|
|
79
|
+
const asOf = raw.asOf;
|
|
80
|
+
return typeof asOf === "string" && asOf.trim().length > 0 ? asOf.trim() : undefined;
|
|
81
|
+
}
|
|
82
|
+
function parseProviderConfig(name, raw, lang) {
|
|
71
83
|
if (!raw || typeof raw !== "object") {
|
|
72
|
-
throw new DispatchError(
|
|
84
|
+
throw new DispatchError(m(lang, "providerConfigNotObject", name), 2);
|
|
73
85
|
}
|
|
74
86
|
const r = raw;
|
|
75
87
|
// §5:api 欄無預設值,介面選擇不容猜測——缺失即中止。
|
|
76
88
|
if (r.api !== "responses" && r.api !== "gemini-native" && r.api !== "anthropic-messages") {
|
|
77
|
-
throw new DispatchError(
|
|
89
|
+
throw new DispatchError(m(lang, "providerApiInvalid", name, JSON.stringify(r.api)), 2);
|
|
78
90
|
}
|
|
79
91
|
// §5:store 不可為 true——違反設計原則 6(零留存)即中止。
|
|
80
92
|
if (r.store === true) {
|
|
81
|
-
throw new DispatchError(
|
|
93
|
+
throw new DispatchError(m(lang, "providerStoreTrue", name), 2);
|
|
82
94
|
}
|
|
83
95
|
const store = r.store === null ? null : false; // 未填視為 false
|
|
84
96
|
if (typeof r.baseURL !== "string" || r.baseURL.length === 0) {
|
|
85
|
-
throw new DispatchError(
|
|
97
|
+
throw new DispatchError(m(lang, "providerBaseUrlMissing", name), 2);
|
|
86
98
|
}
|
|
87
99
|
// §5:models 白名單。未填(或非陣列)視為空陣列 = 不做型號檢查。
|
|
88
100
|
const models = Array.isArray(r.models) ? r.models.filter((v) => typeof v === "string") : [];
|
|
89
101
|
// §5:charsPerToken。未填時用 CLI 全域值——保留 null,不在此處套 1.0(CLI 端才知道
|
|
90
102
|
// 使用者傳入的 --chars-per-token 是多少)。
|
|
91
103
|
if (r.charsPerToken !== undefined && (typeof r.charsPerToken !== "number" || !(r.charsPerToken > 0))) {
|
|
92
|
-
throw new DispatchError(
|
|
104
|
+
throw new DispatchError(m(lang, "providerCharsPerTokenInvalid", name), 2);
|
|
93
105
|
}
|
|
94
106
|
const charsPerToken = typeof r.charsPerToken === "number" ? r.charsPerToken : null;
|
|
95
107
|
return {
|
|
@@ -97,12 +109,13 @@ function parseProviderConfig(name, raw) {
|
|
|
97
109
|
api: r.api,
|
|
98
110
|
store,
|
|
99
111
|
toolCalling: r.toolCalling === true,
|
|
100
|
-
reasoning: parseReasoning(r.reasoning, name),
|
|
112
|
+
reasoning: parseReasoning(r.reasoning, name, lang),
|
|
101
113
|
models,
|
|
102
114
|
charsPerToken,
|
|
103
115
|
tpmLimit: typeof r.tpmLimit === "number" ? r.tpmLimit : null,
|
|
104
116
|
maxSpokeTokens: typeof r.maxSpokeTokens === "number" ? r.maxSpokeTokens : null,
|
|
105
|
-
pricing: parsePricing(r.pricing, name),
|
|
117
|
+
pricing: parsePricing(r.pricing, name, lang),
|
|
118
|
+
pricingAsOf: parsePricingAsOf(r.pricingSource),
|
|
106
119
|
};
|
|
107
120
|
}
|
|
108
121
|
// plan_dispatch_v1.10.md §24.3:providers.json 隨工具出貨,不可覆寫(方案 D)。
|
|
@@ -110,42 +123,41 @@ function parseProviderConfig(name, raw) {
|
|
|
110
123
|
// 在載入時做,不符即中止,防跨版本漂移(一份舊版 providers.json 寫著已證實不生效的
|
|
111
124
|
// 參數路徑,可能回顯、不報錯,行為完全不受控,facts_dispatch.md 2026-08-06 已有實例)。
|
|
112
125
|
export const PROVIDERS_FORMAT_VERSION = 1;
|
|
113
|
-
export function parseProvidersFile(json) {
|
|
126
|
+
export function parseProvidersFile(json, lang) {
|
|
114
127
|
if (!json || typeof json !== "object") {
|
|
115
|
-
throw new DispatchError(
|
|
128
|
+
throw new DispatchError(m(lang, "providersFileNotObject"), 2);
|
|
116
129
|
}
|
|
117
130
|
const { formatVersion, ...providerEntries } = json;
|
|
118
131
|
if (formatVersion !== PROVIDERS_FORMAT_VERSION) {
|
|
119
|
-
throw new DispatchError(
|
|
120
|
-
`這通常代表 --providers 指向了舊版或不相容的檔案。`, 2);
|
|
132
|
+
throw new DispatchError(m(lang, "providersFormatVersionMismatch", PROVIDERS_FORMAT_VERSION, JSON.stringify(formatVersion)), 2);
|
|
121
133
|
}
|
|
122
134
|
const out = {};
|
|
123
135
|
for (const [name, raw] of Object.entries(providerEntries)) {
|
|
124
|
-
out[name] = parseProviderConfig(name, raw);
|
|
136
|
+
out[name] = parseProviderConfig(name, raw, lang);
|
|
125
137
|
}
|
|
126
138
|
return out;
|
|
127
139
|
}
|
|
128
|
-
export async function loadProviders(filePath) {
|
|
140
|
+
export async function loadProviders(filePath, lang) {
|
|
129
141
|
let text;
|
|
130
142
|
try {
|
|
131
143
|
text = await readFile(filePath, "utf8");
|
|
132
144
|
}
|
|
133
145
|
catch {
|
|
134
|
-
throw new DispatchError(
|
|
146
|
+
throw new DispatchError(m(lang, "missingProviders", filePath), 2);
|
|
135
147
|
}
|
|
136
148
|
let json;
|
|
137
149
|
try {
|
|
138
150
|
json = JSON.parse(text);
|
|
139
151
|
}
|
|
140
152
|
catch (err) {
|
|
141
|
-
throw new DispatchError(
|
|
153
|
+
throw new DispatchError(m(lang, "providersFileInvalidJson", err.message), 2);
|
|
142
154
|
}
|
|
143
|
-
return parseProvidersFile(json);
|
|
155
|
+
return parseProvidersFile(json, lang);
|
|
144
156
|
}
|
|
145
|
-
export function getProviderConfig(providers, name) {
|
|
157
|
+
export function getProviderConfig(providers, name, lang) {
|
|
146
158
|
const config = providers[name];
|
|
147
159
|
if (!config) {
|
|
148
|
-
throw new DispatchError(
|
|
160
|
+
throw new DispatchError(m(lang, "providerUndefined", name), 2);
|
|
149
161
|
}
|
|
150
162
|
return config;
|
|
151
163
|
}
|
package/dist/raw-integrity.js
CHANGED
|
@@ -17,33 +17,34 @@
|
|
|
17
17
|
// 物件被傳過去」,而非「長得像的東西存在」,後者會掩蓋掉真正的 bug(例如重新建構出一個
|
|
18
18
|
// 內容相似但簽章欄位被序列化過程改寫的複製品)。若未來 adapter 需要對 raw 做任何正規化,
|
|
19
19
|
// 這個判準本身就需要重新設計——那是規格問題,不是這裡的實作可以自行決定的。
|
|
20
|
+
import { m } from "./messages.js";
|
|
20
21
|
export class RawIntegrityError extends Error {
|
|
21
22
|
constructor(message) {
|
|
22
23
|
super(message);
|
|
23
24
|
this.name = "RawIntegrityError";
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
export function checkRawArrayIntegrity(conv, builtInput) {
|
|
27
|
+
export function checkRawArrayIntegrity(conv, builtInput, lang) {
|
|
27
28
|
for (const turn of conv.turns) {
|
|
28
29
|
if (turn.role !== "assistant")
|
|
29
30
|
continue;
|
|
30
31
|
if (!Array.isArray(turn.raw)) {
|
|
31
|
-
throw new RawIntegrityError(
|
|
32
|
+
throw new RawIntegrityError(m(lang, "rawIntegrityNotArray"));
|
|
32
33
|
}
|
|
33
34
|
for (const item of turn.raw) {
|
|
34
35
|
if (!builtInput.includes(item)) {
|
|
35
36
|
const type = item?.type ?? "?";
|
|
36
|
-
throw new RawIntegrityError(
|
|
37
|
+
throw new RawIntegrityError(m(lang, "rawIntegrityItemNotFound", type));
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
export function checkRawObjectIntegrity(conv, builtContents) {
|
|
42
|
+
export function checkRawObjectIntegrity(conv, builtContents, lang) {
|
|
42
43
|
for (const turn of conv.turns) {
|
|
43
44
|
if (turn.role !== "assistant")
|
|
44
45
|
continue;
|
|
45
46
|
if (!builtContents.includes(turn.raw)) {
|
|
46
|
-
throw new RawIntegrityError(
|
|
47
|
+
throw new RawIntegrityError(m(lang, "rawIntegrityObjectNotFound"));
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
}
|
package/dist/report.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// plan_dispatch_v1.4.md §11:派工報表(呼叫前)。模型錯了、effort 不如預期、介面走錯、
|
|
2
2
|
// 成本量級不對,在送出前就看得到。不使用 ✓/✗ 標記——TPM 是滑動窗,429 等待會動態改變
|
|
3
3
|
// 實際並行數,靜態估算無法精確預測。
|
|
4
|
+
import { stripFrontmatter } from "./ticket.js";
|
|
5
|
+
import { FIXED_CLOSING_LINE, FIXED_CLOSING_LINE_EN } from "./audit.js";
|
|
6
|
+
import { m } from "./messages.js";
|
|
4
7
|
export function effectiveCap(spoke, cli) {
|
|
5
8
|
return spoke.providerConfig.maxSpokeTokens ?? cli.maxSpokeTokens;
|
|
6
9
|
}
|
|
7
10
|
export function effectiveCharsPerToken(spoke, cli) {
|
|
8
11
|
return spoke.providerConfig.charsPerToken ?? cli.charsPerToken;
|
|
9
12
|
}
|
|
10
|
-
function formatProvidersSource(src) {
|
|
13
|
+
function formatProvidersSource(src, lang) {
|
|
11
14
|
if (src.kind === "bundled")
|
|
12
|
-
return
|
|
13
|
-
return
|
|
15
|
+
return m(lang, "providersBundled", src.formatVersion);
|
|
16
|
+
return m(lang, "providersExplicit", src.path ?? "", src.formatVersion);
|
|
14
17
|
}
|
|
15
18
|
// v1.10 §10:三態不得合併——「未忽略」是需要處理的狀態,「無法判定」不是,措辭須區分。
|
|
16
19
|
//
|
|
@@ -18,22 +21,64 @@ function formatProvidersSource(src) {
|
|
|
18
21
|
// 已訂輸出目錄屬呼叫端 cwd,checkGitignore 檢查的也是 cwd 所屬的 repo——cwd 與
|
|
19
22
|
// --repo-root 指的目標專案是兩回事(v1.11 §10 修的正是這個基準不一致)。「目標專案」
|
|
20
23
|
// 這個詞在 cwd ≠ repoRoot 時會指錯對象,故改為不預設兩者相同的措辭。
|
|
21
|
-
function formatGitignoreWarning(status, outDir) {
|
|
24
|
+
function formatGitignoreWarning(status, outDir, lang) {
|
|
22
25
|
if (status === "ignored")
|
|
23
26
|
return null;
|
|
24
27
|
if (status === "not_ignored") {
|
|
25
|
-
return
|
|
28
|
+
return m(lang, "gitignoreNotIgnored", outDir);
|
|
26
29
|
}
|
|
27
|
-
return
|
|
30
|
+
return m(lang, "gitignoreUnknown", outDir);
|
|
28
31
|
}
|
|
29
|
-
|
|
32
|
+
// plan_i18n_v1.2.md §6.3:呼叫前提示 lens 收尾句落在哪一個 FIXED_CLOSING_LINE*——只提示不擋
|
|
33
|
+
// (同既有 gitignore 警告),讓 lens 檔案本身的收尾句指示(如「回報最後一行固定為:...」)
|
|
34
|
+
// 在真正付費呼叫之前先被人看到有沒有漂移。agentBody 已在 resolveSpokes 剝過 frontmatter,
|
|
35
|
+
// 這裡再跑一次 stripFrontmatter 是冪等的(不再以 --- 開頭,regex 不命中),不需重讀檔。
|
|
36
|
+
function lensClosingLineStatus(agentBody) {
|
|
37
|
+
const lines = stripFrontmatter(agentBody).split(/\r?\n/).map((l) => l.trim());
|
|
38
|
+
const lastNonEmpty = [...lines].reverse().find((l) => l.length > 0) ?? "";
|
|
39
|
+
if (lastNonEmpty.includes(FIXED_CLOSING_LINE))
|
|
40
|
+
return "zh";
|
|
41
|
+
if (lastNonEmpty.includes(FIXED_CLOSING_LINE_EN))
|
|
42
|
+
return "en";
|
|
43
|
+
return "none";
|
|
44
|
+
}
|
|
45
|
+
// 沒有收尾句的 lens(如 explore-haiku.md,不是 hole-finder 系列、不受這份收尾句約定拘束)
|
|
46
|
+
// 回傳 null、不印任何提示——只對「有收尾句、值得比對」的 lens 發聲,才不會對每一支
|
|
47
|
+
// 不相干的 lens 都噴一行「未偵測到」的常駐噪音。
|
|
48
|
+
function formatLensClosingLineNotice(spoke, lang) {
|
|
49
|
+
const status = lensClosingLineStatus(spoke.agentBody);
|
|
50
|
+
if (status === "zh")
|
|
51
|
+
return m(lang, "lensClosingLineZh", spoke.agent);
|
|
52
|
+
if (status === "en")
|
|
53
|
+
return m(lang, "lensClosingLineEn", spoke.agent);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// v0.2.0:把本次型號的單價印進乾跑報表。起因是 /publish-check 判斷清單第 4 項
|
|
57
|
+
// (「指向的機制在外部專案存在嗎」):skill 改成「價目查 providers.json,不要查官網」之後,
|
|
58
|
+
// 外部專案的 hub 其實找不到那個檔——它隨套件出貨,躺在 node_modules 或全域安裝目錄底下,
|
|
59
|
+
// 路徑帶版本號與相依雜湊,猜不到。找不到就會退回舊行為去查官網,等於那條規則只修了一半。
|
|
60
|
+
//
|
|
61
|
+
// **這一行讓那個檔不必被找到。** 順帶也解決 `--providers` 覆寫之後 hub 讀到錯的那一份。
|
|
62
|
+
// 缺價目時明講「無價目資料」而不是印 0——與 cost.ts 的 null 語意一致(§4:「無法估算」
|
|
63
|
+
// 與「估算出來是零」不是同一件事)。
|
|
64
|
+
function formatModelPricing(spoke, lang) {
|
|
65
|
+
const pricing = spoke.providerConfig.pricing?.[spoke.model];
|
|
66
|
+
if (!pricing)
|
|
67
|
+
return m(lang, "modelPricingMissing", spoke.model);
|
|
68
|
+
const cached = pricing.cachedInputPerM !== undefined ? m(lang, "modelPricingCachedSuffix", pricing.cachedInputPerM) : "";
|
|
69
|
+
const asOf = spoke.providerConfig.pricingAsOf
|
|
70
|
+
? m(lang, "modelPricingAsOfSuffix", spoke.providerConfig.pricingAsOf)
|
|
71
|
+
: "";
|
|
72
|
+
return m(lang, "modelPricing", pricing.inputPerM, pricing.outputPerM, cached, asOf);
|
|
73
|
+
}
|
|
74
|
+
export function buildReport(ticketId, spokes, estimates, allowlistEstimates, cli, outDir, meta, lang) {
|
|
30
75
|
const estByAgent = new Map(estimates.map((e) => [e.agent, e.estimatedTokens]));
|
|
31
|
-
const lines = [
|
|
76
|
+
const lines = [m(lang, "aboutToDispatch", ticketId)];
|
|
32
77
|
// v1.10 §11:repoRoot 明列於報表,與 store 同性質的可見性保證——白名單是整套隔離的
|
|
33
78
|
// 唯一支點(§7),其邊界由「從哪個目錄下指令」決定,這件事在報表上必須看得見,否則
|
|
34
79
|
// 使用者無從發現自己在錯的目錄下跑。
|
|
35
80
|
lines.push(` repoRoot ${meta.repoRoot}`);
|
|
36
|
-
lines.push(` providers ${formatProvidersSource(meta.providersSource)}`);
|
|
81
|
+
lines.push(` providers ${formatProvidersSource(meta.providersSource, lang)}`);
|
|
37
82
|
for (const spoke of spokes) {
|
|
38
83
|
const est = estByAgent.get(spoke.agent) ?? 0;
|
|
39
84
|
const cap = effectiveCap(spoke, cli);
|
|
@@ -41,6 +86,10 @@ export function buildReport(ticketId, spokes, estimates, allowlistEstimates, cli
|
|
|
41
86
|
`[${spoke.providerConfig.api}] effort=${spoke.effort ?? "—"} lang=${spoke.lang} ` +
|
|
42
87
|
`store=${spoke.providerConfig.store === false ? "false" : "n/a"} ` +
|
|
43
88
|
`est. ${est.toLocaleString()} cap ${cap.toLocaleString()}`);
|
|
89
|
+
lines.push(` ${formatModelPricing(spoke, lang)}`);
|
|
90
|
+
const closingLineNotice = formatLensClosingLineNotice(spoke, lang);
|
|
91
|
+
if (closingLineNotice)
|
|
92
|
+
lines.push(` ${closingLineNotice}`);
|
|
44
93
|
}
|
|
45
94
|
const totalEst = estimates.reduce((s, e) => s + e.estimatedTokens, 0);
|
|
46
95
|
const worstTotal = spokes.reduce((s, spoke) => s + effectiveCap(spoke, cli), 0);
|
|
@@ -48,37 +97,42 @@ export function buildReport(ticketId, spokes, estimates, allowlistEstimates, cli
|
|
|
48
97
|
// 這個數字只涵蓋 system prompt + buildFirstUserText() 產出的第一則訊息,不含工單與
|
|
49
98
|
// 允許清單本身(那些是 spoke 自己用 tool call 讀進去的)。實測差距 27 倍(2,593 vs
|
|
50
99
|
// 71,482)。標籤明講範圍,並緊接印出允許清單總量,讓兩個數字一起出現、不必自己去找。
|
|
51
|
-
lines.push(
|
|
100
|
+
lines.push(m(lang, "initialPromptEstimate", totalEst.toLocaleString(), cli.maxTokens.toLocaleString()));
|
|
52
101
|
// §14:獨立於閘門一之外,只呈現不設閘門——閘門一不含允許清單與工單內容(issue_log_v2.0.md
|
|
53
102
|
// 2026-08-07),此數字才是「若整個允許清單都被讀完」的量級參考,門檻待樣本累積後再定。
|
|
54
103
|
const totalAllowlistTokens = allowlistEstimates.reduce((s, e) => s + e.estimatedTokens, 0);
|
|
55
104
|
const totalAllowlistFiles = allowlistEstimates.reduce((s, e) => s + e.fileCount, 0);
|
|
56
|
-
lines.push(
|
|
105
|
+
lines.push(m(lang, "allowlistTotalEstimate", totalAllowlistTokens.toLocaleString(), totalAllowlistFiles));
|
|
57
106
|
// plan_dispatch_v2.4.md §14:口徑說明——避免此數字被誤讀為預期消耗。三件事:
|
|
58
107
|
// (1) 字元數的上限估計,不是預期消耗;(2) 不去重,同一檔出現在多支清單會重複計入
|
|
59
108
|
// (正確行為:兩支各讀一次就是兩份成本);(3) 實測程式碼素材約 3.5 字元/token,
|
|
60
109
|
// 故實際消耗通常遠低於此數(charsPerToken 假設 1.0,係數本身刻意不動,見 §14)。
|
|
61
|
-
lines.push(
|
|
110
|
+
lines.push(m(lang, "allowlistEstimateCaveat"));
|
|
62
111
|
// issue_log_v2.1.md(第 8、9 次):多數模型一輪只叫一個檔,每輪重送全部歷史,故清單靠前
|
|
63
112
|
// 的檔會被重複計費多次。這一行是「順序造成的放大量」,也是唯一 hub 改得動的成本槓桿——
|
|
64
113
|
// 只印排序能省的部分,不印預估總量:總量還受「批次讀 vs 逐個讀」影響(模型決定,我們
|
|
65
114
|
// 控制不了),印出來會被當成預期值。省下的百分比則不受 charsPerToken 偏差影響(分子分母
|
|
66
115
|
// 抵銷),是這裡唯一可信的絕對數字。
|
|
116
|
+
//
|
|
117
|
+
// real-run-same-lens §九之6 要求這一行標明「此為逐個讀假設下的上限」,訊息文字已補。
|
|
118
|
+
// real-run-i18n-lang(2026-08-12)另加一條:讀檔形態**不只隨型號變,也隨 prompt 語言變**
|
|
119
|
+
// ——同型號同 effort,只把語言從中文換成英文,luna 的 feasibility 就從 9 輪(逐個)
|
|
120
|
+
// 變 4 輪(批次)。所以「這個廠牌會不會批次」在乾跑階段更不可能預先判定。
|
|
67
121
|
const seqAsListed = allowlistEstimates.reduce((s, e) => s + e.sequential.asListed, 0);
|
|
68
122
|
const seqSorted = allowlistEstimates.reduce((s, e) => s + e.sequential.sorted, 0);
|
|
69
123
|
if (seqAsListed > 0) {
|
|
70
124
|
const savedPct = Math.round(((seqAsListed - seqSorted) / seqAsListed) * 100);
|
|
71
|
-
lines.push(
|
|
125
|
+
lines.push(m(lang, "sequentialReadAmplification", seqAsListed.toLocaleString()));
|
|
72
126
|
if (savedPct >= 5) {
|
|
73
|
-
lines.push(
|
|
127
|
+
lines.push(m(lang, "sequentialReadCanReduce", seqSorted.toLocaleString(), savedPct));
|
|
74
128
|
}
|
|
75
129
|
else {
|
|
76
|
-
lines.push(
|
|
130
|
+
lines.push(m(lang, "sequentialReadNearOptimal", savedPct));
|
|
77
131
|
}
|
|
78
|
-
lines.push(
|
|
132
|
+
lines.push(m(lang, "sequentialReadCostNote"));
|
|
79
133
|
}
|
|
80
|
-
lines.push(
|
|
81
|
-
lines.push(
|
|
134
|
+
lines.push(m(lang, "worstCaseTotal", worstTotal.toLocaleString()));
|
|
135
|
+
lines.push(m(lang, "concurrencyLine", cli.concurrency));
|
|
82
136
|
const byProvider = new Map();
|
|
83
137
|
for (const spoke of spokes) {
|
|
84
138
|
const est = estByAgent.get(spoke.agent) ?? 0;
|
|
@@ -89,12 +143,12 @@ export function buildReport(ticketId, spokes, estimates, allowlistEstimates, cli
|
|
|
89
143
|
for (const [provider, { tpmLimit, peak }] of byProvider) {
|
|
90
144
|
if (tpmLimit === null)
|
|
91
145
|
continue;
|
|
92
|
-
lines.push(
|
|
93
|
-
lines.push(
|
|
146
|
+
lines.push(m(lang, "tpmPeakLine", provider, tpmLimit.toLocaleString(), peak.toLocaleString()));
|
|
147
|
+
lines.push(m(lang, "tpmPeakCaveat"));
|
|
94
148
|
}
|
|
95
149
|
const allowedCount = spokes.reduce((s, spoke) => s + spoke.allowedReadsResolved.length, 0);
|
|
96
|
-
lines.push(
|
|
97
|
-
const gitignoreWarning = formatGitignoreWarning(meta.gitignoreStatus, outDir);
|
|
150
|
+
lines.push(m(lang, "allowedReadsSummary", allowedCount, outDir));
|
|
151
|
+
const gitignoreWarning = formatGitignoreWarning(meta.gitignoreStatus, outDir, lang);
|
|
98
152
|
if (gitignoreWarning)
|
|
99
153
|
lines.push(gitignoreWarning);
|
|
100
154
|
return lines.join("\n");
|