dowafu 0.4.0 → 0.5.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/README.md +236 -66
- package/README_zh-tw.md +216 -66
- package/dist/adapters/anthropic-messages.js +5 -3
- package/dist/adapters/gemini-native.js +13 -5
- package/dist/adapters/responses.js +5 -1
- package/dist/api-token.js +54 -0
- package/dist/cli-args.js +45 -3
- package/dist/cli.js +680 -101
- package/dist/cost.js +24 -0
- package/dist/daemon.js +209 -0
- package/dist/db.js +219 -0
- package/dist/dispatch-home.js +8 -19
- package/dist/doctor.js +105 -45
- package/dist/gate.js +9 -4
- package/dist/job.js +97 -0
- package/dist/liveness.js +39 -0
- package/dist/mcp/http.js +178 -0
- package/dist/mcp/protocol.js +308 -0
- package/dist/mcp/stdio.js +36 -0
- package/dist/messages.js +338 -52
- package/dist/output.js +91 -104
- package/dist/progress.js +36 -0
- package/dist/provider-key.js +84 -0
- package/dist/providers-store.js +165 -0
- package/dist/providers.js +18 -6
- package/dist/report.js +38 -14
- package/dist/runner.js +18 -0
- package/dist/ticket-store.js +195 -0
- package/dist/ticket.js +22 -1
- package/dist/validate.js +58 -21
- package/package.json +1 -2
- package/providers.json +12 -10
package/dist/messages.js
CHANGED
|
@@ -25,13 +25,13 @@ const MESSAGES = {
|
|
|
25
25
|
unknownOption: (arg, helpText) => `未知選項:${arg}\n\n${helpText}`,
|
|
26
26
|
missingProviders: (path) => `找不到 providers.json:${path}`,
|
|
27
27
|
dryRunNotice: () => "--dry-run:僅解析/驗證/估算/印報表,未呼叫任何 API。",
|
|
28
|
-
helpText: (cmd) => `用法:${cmd} <ticket-
|
|
28
|
+
helpText: (cmd) => `用法:${cmd} <ticket-id> [options]
|
|
29
29
|
|
|
30
30
|
--lang <en|zh-tw> CLI 輸出與 spoke prompt 的語言,預設 en
|
|
31
31
|
--repo-root <dir> 白名單邊界與 .claude/agents 的根,預設 cwd
|
|
32
|
-
--
|
|
32
|
+
--db <path> SQLite 工單與產物資料庫,預設 DISPATCH_HOME/dowafu.db
|
|
33
|
+
--http-port <port> serve 模式的 HTTP binding 埠,僅綁 127.0.0.1,預設 7391
|
|
33
34
|
--json stdout 只印結果 JSON,其餘輸出改走 stderr
|
|
34
|
-
--out <dir> 落檔目錄,預設 tmp/spoke/
|
|
35
35
|
--concurrency <n> 同時執行的 spoke 數,預設 2
|
|
36
36
|
--max-tokens <n> 呼叫前估算閘門(各 spoke 初始 prompt 總和),預設 200000
|
|
37
37
|
--max-spoke-tokens <n> 單一 spoke 執行期累積上限(實際 usage),預設 400000
|
|
@@ -42,12 +42,23 @@ const MESSAGES = {
|
|
|
42
42
|
--max-round-reasoning-tokens <n> 單輪推理 token 上限,預設 null(不檢查)
|
|
43
43
|
--rate-limit-retries <n> 429 專用重試次數,預設 5(不計入 --retries)
|
|
44
44
|
--max-rate-wait <sec> 單次 429 等待上限,預設 30
|
|
45
|
-
--max-tool-calls <n> 單一 spoke 的 read_file 呼叫上限,預設
|
|
45
|
+
--max-tool-calls <n> 單一 spoke 的 read_file 呼叫上限,預設 20
|
|
46
46
|
--dry-run 解析、驗證、估算、印報表,不呼叫 API
|
|
47
47
|
--yes 略過派工確認。非互動環境(stdin 不是 TTY)沒帶就中止
|
|
48
48
|
--doctor 印出設定自檢(不呼叫 API、不花錢)後結束(exit 0)
|
|
49
49
|
--help, -h 印本說明後結束(exit 0)
|
|
50
|
-
--version, -V 印版本號後結束(exit 0
|
|
50
|
+
--version, -V 印版本號後結束(exit 0)
|
|
51
|
+
|
|
52
|
+
子指令(各自有自己的 --help/用法訊息):
|
|
53
|
+
${cmd} ticket ... 建立、匯入、檢視工單
|
|
54
|
+
${cmd} result <id> 印出一次派工的結果(各 spoke 原文與稽核表)
|
|
55
|
+
${cmd} key 設定 provider API key(互動選單)
|
|
56
|
+
${cmd} providers ... 型號白名單與啟用狀態
|
|
57
|
+
${cmd} token ... 入站 HTTP 憑證的發/列/撤銷
|
|
58
|
+
${cmd} approve [jobId] 核准佇列中等待的派工
|
|
59
|
+
${cmd} serve 啟動常駐 daemon(含 MCP 的 HTTP binding)
|
|
60
|
+
${cmd} serve --stop [--yes] 停止常駐 daemon;有執行中的 job 時會先確認
|
|
61
|
+
${cmd} mcp 以 stdio 提供 MCP server`,
|
|
51
62
|
availableLangValues: () => "en、zh-tw、zh",
|
|
52
63
|
availableValuesSuffix: (values) => `(可用值:${values})`,
|
|
53
64
|
numberFlagInvalid: (name, value) => `--${name} 需要數字,收到:${value}`,
|
|
@@ -70,16 +81,112 @@ const MESSAGES = {
|
|
|
70
81
|
outDirNotWritable: (outDir) => `落檔目錄不可寫:${outDir}`,
|
|
71
82
|
// 護欄:舊產物是花過錢的東西,覆蓋掉之前沒有人會被問到。換一個 ticket-id 零成本,
|
|
72
83
|
// 所以這裡不提供 --overwrite 之類的旗標——有旗標就會有人直接加上去。
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
resultsExistAbort: (resultId) => `這個 id 已經有派工結果,未派工、未呼叫任何 API:${resultId}\n` +
|
|
85
|
+
`那是上一次跑出來的東西,覆蓋掉就沒了。先看它:dowafu result ${resultId}\n` +
|
|
86
|
+
`兩條路:\n` +
|
|
75
87
|
` 1. 換一個沒用過的 ticket-id 再派(建議,零成本)\n` +
|
|
76
|
-
` 2.
|
|
77
|
-
` *
|
|
78
|
-
` 換 id
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
` 2. 由使用者自行把那筆結果從資料庫刪掉之後重跑——這是他的決定,不是你的\n` +
|
|
89
|
+
` * 上一次若是失敗收場(全部 failed、token 0),那筆沒有花過錢的東西,\n` +
|
|
90
|
+
` 換 id 或請使用者刪掉都行——但仍然由使用者決定要不要刪`,
|
|
91
|
+
resultsExistDryRunWarning: (resultId) => `⚠ 這個 id 已經有派工結果:${resultId}\n 乾跑不受影響,但實跑會被擋下。換一個 ticket-id 即可;要先看上一次的結果用 dowafu result ${resultId}。`,
|
|
92
|
+
resultsSaved: (resultId) => `結果已存入資料庫。讀回來:dowafu result ${resultId}`,
|
|
81
93
|
outDirFallbackStderr: () => "落檔目錄不可寫,完整報告已改印於 stderr:",
|
|
82
94
|
stdoutSummaryLine: (agent, status, model, tokens, costLabel, latencyMs) => `${agent}: ${status} model=${model} token=${tokens} cost=${costLabel} 耗時=${latencyMs}ms`,
|
|
95
|
+
dbResultPersistenceFailed: (agent, maskedErr) => `DB 結果寫入失敗(${agent}):${maskedErr}`,
|
|
96
|
+
dbSchemaTooNew: (found, supported) => `這個資料庫是較新版本的 dowafu 建立的(schema 版本 ${found},本版最高支援 ${supported})。請升級 dowafu,不要用舊版開啟它。`,
|
|
97
|
+
approveHelp: (cmd) => `用法:${cmd} approve [jobId 前綴] [--db <path>]`,
|
|
98
|
+
resultHelp: (cmd) => `用法:${cmd} result <ticket-id 或 jobId/前綴> [--db <path>]`,
|
|
99
|
+
resultNotFound: (id) => `找不到 "${id}" 的結果。派工尚未完成,或該 ticket-id/jobId 不存在。`,
|
|
100
|
+
resultPrefixAmbiguous: (prefix, count) => `前綴 ${prefix} 命中 ${count} 張 job;請提供更長的 id。`,
|
|
101
|
+
resultCandidate: (shortId, ticket, status) => ` ${shortId} ${ticket} ${status}`,
|
|
102
|
+
approveNoPending: () => "目前沒有待核准的 job。",
|
|
103
|
+
approveMultiplePending: (count) => `有 ${count} 張待核准 job;請指定其中一個短 id。`,
|
|
104
|
+
approvePrefixAmbiguous: (prefix, count) => `前綴 ${prefix} 命中 ${count} 張 job;請提供更長的 id。`,
|
|
105
|
+
approvePendingCandidate: (shortId, ticket, createdAt) => ` ${shortId} ${ticket} ${createdAt}`,
|
|
106
|
+
jobNotFound: (jobId) => `找不到 job:${jobId}`,
|
|
107
|
+
mcpJobPrefixAmbiguous: (prefix, count) => `前綴 ${prefix} 命中 ${count} 個 job,請改用完整 id:`,
|
|
108
|
+
jobNotApprovable: (jobId, status) => `job ${jobId} 目前是 ${status},不能核准`,
|
|
109
|
+
jobApproved: (jobId) => `已核准 job:${jobId}`,
|
|
110
|
+
daemonStarted: () => "daemon 已啟動。收到 SIGINT 時會乾淨退出;進行中的 job 下次啟動會被回收。",
|
|
111
|
+
daemonStartFailed: (detail) => `daemon 無法啟動:${detail}`,
|
|
112
|
+
daemonWorkerSpawnFailed: (detail) => `worker 無法啟動:${detail}`,
|
|
113
|
+
daemonWorkerExited: (exitCode) => `worker 以 exit code ${exitCode} 結束`,
|
|
114
|
+
daemonWorkerExitedDetail: (exitCode, detail) => `worker 以 exit code ${exitCode} 結束:${detail}`,
|
|
115
|
+
daemonWorkerTerminated: (signal) => `worker 被訊號 ${signal} 終止`,
|
|
116
|
+
daemonWorkerTerminatedDetail: (signal, detail) => `worker 被訊號 ${signal} 終止:${detail}`,
|
|
117
|
+
stopRequiresServe: () => "--stop 只能與 serve 一起使用。",
|
|
118
|
+
daemonStopRunningWarning: (count) => `目前有 ${count} 張 job 正在執行;停止 daemon 不會停止正在跑的 spoke,它們會繼續跑完,之後結果與 API 成本仍會被認列。`,
|
|
119
|
+
daemonStopConfirmPrompt: () => "確定要停止 daemon 嗎?[y/N] ",
|
|
120
|
+
daemonStopCancelled: () => "已取消,daemon 仍在執行。",
|
|
121
|
+
daemonStopNotRunning: (reason) => `daemon 未在執行(${reason})。`,
|
|
122
|
+
daemonStopPidNotDowafu: (pid) => `pid ${pid} 的行程不是 dowafu,為避免誤殺未送出訊號。`,
|
|
123
|
+
daemonStopSignalFailed: (pid) => `已向 pid ${pid} 送出停止訊號,但它沒有停止。`,
|
|
124
|
+
daemonStopSucceeded: (pid) => `daemon(pid ${pid})已停止。`,
|
|
125
|
+
daemonStopRestarted: (pid) => `daemon(原 pid ${pid})停止後又恢復運行;看起來有 OS 層服務管理在重拉它。要真的停下來請用 launchctl 或 systemctl。`,
|
|
126
|
+
doctorDaemonLine: (value) => ` daemon ${value}`,
|
|
127
|
+
doctorDaemonAlive: (pid) => `運行中(pid ${pid})`,
|
|
128
|
+
doctorDaemonMissing: (reason) => `未運行(${reason})`,
|
|
129
|
+
daemonOfflineWarning: (reason) => `⚠ worker daemon 未運行(${reason});job 已排入但不會執行。請先執行 dowafu serve。`,
|
|
130
|
+
mcpTicketsEmpty: () => "沒有可派的 DB 工單。",
|
|
131
|
+
mcpTicketsTitle: () => "可派工單:",
|
|
132
|
+
mcpTicketsRow: (ticket, spokeCount) => `- ${ticket}:${spokeCount} 個 spoke`,
|
|
133
|
+
mcpTicketsSpoke: (agent, provider, model, allowCount) => ` ${agent}:${provider}/${model};允許清單檔 ${allowCount} 個`,
|
|
134
|
+
mcpKeysTitle: () => "API key 狀態:",
|
|
135
|
+
mcpKeysRow: (provider, status) => ` ${provider}:${status}`,
|
|
136
|
+
mcpEnabledModelsTitle: () => "已啟用型號白名單:",
|
|
137
|
+
mcpEnabledModelRow: (provider, model) => ` ${provider}/${model}`,
|
|
138
|
+
mcpSubmitQueued: (jobId) => `已排入佇列,job id = ${jobId},狀態 pending_approval。尚未執行,須由人在終端機核准。`,
|
|
139
|
+
mcpApproveCommand: (command) => `請在終端機執行:${command}`,
|
|
140
|
+
mcpJobStarted: (at) => `開始於 ${at}`,
|
|
141
|
+
mcpJobFinished: (at) => `結束於 ${at}`,
|
|
142
|
+
mcpJobError: (detail) => `錯誤:${detail}`,
|
|
143
|
+
mcpJobProgress: (agent, lastRound, tokensIn, tokensOut, lastAt) => ` ${agent}:第 ${lastRound} 輪,累計 in ${tokensIn}/out ${tokensOut} tokens,最後更新 ${lastAt}`,
|
|
144
|
+
mcpJobNoProgressYet: () => " (還沒有任何一輪完成)",
|
|
145
|
+
mcpJobHeartbeatStale: (seconds) => `⚠ worker 的心跳已經 ${seconds} 秒沒有更新,這支 job 很可能已經死了。` +
|
|
146
|
+
`下一次 daemon 跑 reap 時會把它標成 interrupted;daemon 沒開的話它會一直停在 running。`,
|
|
147
|
+
mcpJobOutcome: (succeeded, failed, failedAgentsSuffix) => `spoke 結果:成功 ${succeeded}/失敗 ${failed}${failedAgentsSuffix}`,
|
|
148
|
+
mcpJobFailedAgentsSuffix: (agents) => `(失敗:${agents})`,
|
|
149
|
+
mcpResultPending: (jobId, status) => `job ${jobId} 目前是 ${status},還沒有結果。`,
|
|
150
|
+
mcpResultComplete: (jobId) => `job ${jobId} 結果:`,
|
|
151
|
+
mcpResultEmpty: () => "(沒有儲存的 spoke 結果)",
|
|
152
|
+
mcpApproveAmountMismatch: (jobId, providedUsd, expectedUsd) => `核准遭拒:金額不符。job ${jobId} 的硬上限是 $${expectedUsd},你給的是 ${providedUsd}。未核准,未呼叫任何 API。`,
|
|
153
|
+
mcpApproveNoPricing: (jobId) => `核准遭拒:job ${jobId} 對應的工單缺少價目資料,無法核對金額,未核准。`,
|
|
154
|
+
httpServerStarted: (port) => `HTTP binding 已啟動:127.0.0.1:${port}/mcp(僅本機,公開交給 tunnel)`,
|
|
155
|
+
httpNoTokensWarning: () => "⚠ 尚未發過任何 token(dowafu token issue)。端點已經活著,但沒有任何請求進得來——不是壞掉,是還沒發 token。",
|
|
156
|
+
tokenHelp: (cmd) => `${cmd} token issue [--label <說明>]\n${cmd} token list\n${cmd} token revoke <id>`,
|
|
157
|
+
tokenIssued: (id, plaintext) => `已發出 token,id = ${id}\n明文(只顯示這一次,之後拿不回來):${plaintext}`,
|
|
158
|
+
tokenListEmpty: () => "尚未發過任何 token。",
|
|
159
|
+
tokenListRow: (id, label, createdAt, revoked) => `${id}\t${label}\t${createdAt}\t${revoked ? "已撤銷" : "有效"}`,
|
|
160
|
+
tokenRevoked: (id) => `已撤銷 token:${id}`,
|
|
161
|
+
tokenNotFoundOrRevoked: (id) => `找不到 token 或已經撤銷過了:${id}`,
|
|
162
|
+
keyHelp: (cmd) => `${cmd} key 互動式設定(選單選 provider,貼上 key,不吃命令列參數)\n${cmd} key list 列出各 provider 目前生效的來源(不含明文)\n${cmd} key rm <provider> 移除 DB 那把\n${cmd} key test <provider> 呼叫一次真實 API 驗證這把 key(會花錢)`,
|
|
163
|
+
keyInteractiveRequiresTty: () => "dowafu key 需要互動式終端機(stdin 不是 TTY),不會掛住等輸入、也不會讀管道內容。請在互動式終端機執行 dowafu key 設定 API key。",
|
|
164
|
+
keyMenuHeader: () => " provider 狀態",
|
|
165
|
+
keyMenuRow: (num, providerPadded, status) => ` ${num}) ${providerPadded} ${status}`,
|
|
166
|
+
keyMenuPrompt: (max) => `選擇要設定的 provider(1-${max},q 離開):`,
|
|
167
|
+
keyMenuInvalidChoice: () => "無效的選擇。",
|
|
168
|
+
keyMenuQuit: () => "已離開,未變更任何設定。",
|
|
169
|
+
keyPastePrompt: (provider) => `貼上 ${provider} 的 API key(不會顯示):`,
|
|
170
|
+
keyPasteEmpty: () => "沒有貼到任何內容,已取消。",
|
|
171
|
+
keySavedTail: (tail) => `✓ 已存入 DB(${tail})。`,
|
|
172
|
+
keyVerifyPrompt: (provider) => `要現在驗證這把 key 嗎?會呼叫一次 ${provider} 的 API,會花錢(y/N):`,
|
|
173
|
+
keyListRow: (provider, status) => `${provider}\t${status}`,
|
|
174
|
+
keyRemoved: (provider) => `已從 DB 移除 ${provider} 的 key。`,
|
|
175
|
+
keyRemovedNotFound: (provider) => `DB 沒有 ${provider} 的 key,無需移除。`,
|
|
176
|
+
keyUnknownProvider: (provider, list) => `不認得的 provider:${provider}(已知:${list})`,
|
|
177
|
+
keyStatusDb: (tail, date) => `✓ 已設定(${tail},DB,${date})`,
|
|
178
|
+
keyStatusMissing: () => "✗ 未設定",
|
|
179
|
+
keyStatusUntested: () => ";未測",
|
|
180
|
+
keyStatusTestSucceeded: (model, at) => `;最近驗證成功(${model},${at})`,
|
|
181
|
+
keyStatusTestFailed: (model, at) => `;最近驗證失敗(${model},${at})`,
|
|
182
|
+
keyTestHelp: (cmd) => `用法:${cmd} key test <provider>`,
|
|
183
|
+
keyTestKeyMissing: (provider) => `${provider} 尚未設定任何 API key(DB/環境變數皆無),先用 \`dowafu key\` 設定。`,
|
|
184
|
+
keyTestNoEnabledModel: (provider) => `${provider} 沒有任何已啟用的型號,無法驗證(見 \`dowafu providers enable\`)。`,
|
|
185
|
+
keyTestCostWarning: (provider) => `即將呼叫一次 ${provider} 的 API 做最小驗證,會花錢。`,
|
|
186
|
+
keyTestConfirmPrompt: () => "確定要繼續嗎?[y/N] ",
|
|
187
|
+
keyTestCancelled: () => "已取消,未呼叫任何 API。",
|
|
188
|
+
keyTestSucceeded: (provider, model) => `✓ ${provider}/${model} 驗證成功,這把 key 可用。`,
|
|
189
|
+
keyTestFailed: (provider, detail) => `✗ ${provider} 驗證失敗:${detail}`,
|
|
83
190
|
formatMarkerMismatch: (marker, got) => `_dispatch.md 首行須為 ${marker},實際為:${got}`,
|
|
84
191
|
blankPlaceholder: () => "(空白)",
|
|
85
192
|
dispatchTableMissingHeader: () => "_dispatch.md 找不到派工表(缺 | agent | ... | 表頭或分隔列)",
|
|
@@ -98,9 +205,22 @@ const MESSAGES = {
|
|
|
98
205
|
missingQuestionsSection: () => '<agent>.md 缺「# 具體問題」(或英文工單的「# Questions」)或內容為空',
|
|
99
206
|
fileNotFound: (path) => `找不到 ${path}`,
|
|
100
207
|
agentFileNotFound: (agentPath, agent) => `找不到 ${agentPath}(_dispatch.md 列了 agent "${agent}")`,
|
|
208
|
+
ticketNotFound: (name) => `找不到 DB 工單:${name}`,
|
|
209
|
+
ticketCorrupt: (name) => `DB 工單內容損毀:${name}`,
|
|
210
|
+
ticketNameInvalid: (name) => `工單名稱無效:${name}`,
|
|
211
|
+
ticketAlreadyExists: (name) => `DB 工單已存在:${name}`,
|
|
212
|
+
ticketSpokeNotFound: (name, agent) => `DB 工單 "${name}" 沒有 agent "${agent}"`,
|
|
213
|
+
ticketShowHeader: (name) => `DB 工單 ${name}:`,
|
|
214
|
+
ticketEstimateTokens: (tokens) => `估算 ${tokens} tokens`,
|
|
215
|
+
ticketCompletedRuns: (runs, total, last) => `已完成 ${runs} 次;累計 $${total};最近一次 ${last}`,
|
|
216
|
+
ticketCompletedRunsNoPricing: (runs, last) => `已完成 ${runs} 次;累計無價目資料;最近一次 ${last}`,
|
|
217
|
+
ticketHelp: (cmd) => `${cmd} ticket create <name> <shared-file>\n${cmd} ticket add-spoke <name> <agent> <provider> <model> <body-file> [effort]\n${cmd} ticket add-allow <name> <agent> <repo-relative-path> [--repo-root <dir>]\n${cmd} ticket import <existing-ticket-dir> [name] [--repo-root <dir>]\n${cmd} ticket show <name>\n <shared-file>/<body-file> 可用 - 代表從 stdin 讀取本文(stdin 須為管道/重導向,不得是互動式終端機)`,
|
|
218
|
+
ticketStdinRequiresPipe: () => `此指令用 - 讀取 stdin 本文,但目前 stdin 是互動式終端機,沒有東西被導進來。` +
|
|
219
|
+
`請改用管道/重導向提供內容(例如 echo "..." | dowafu ticket create <name> -),或改用一般檔案路徑。`,
|
|
101
220
|
agentDefNotFound: (path, agent) => `找不到 agent 定義檔 ${path}(_dispatch.md 列了 "${agent}")`,
|
|
102
221
|
providerUndefinedInRow: (agent, provider) => `_dispatch.md 的 "${agent}" 列引用了未定義於 providers.json 的 provider "${provider}"`,
|
|
103
|
-
|
|
222
|
+
apiKeyNotConfigured: (agent, provider, cmd) => `尚未設定 provider "${provider}" 的 API key("${agent}" 列需要它)。\n`
|
|
223
|
+
+ ` 本版的 API key 已併入資料庫設定,不再讀取 .env 或環境變數;請執行 \`${cmd} key\` 設定。`,
|
|
104
224
|
modelNotWhitelisted: (agent, model, provider, list) => `"${agent}" 列的 model "${model}" 不在 provider "${provider}" 的 models 白名單內(允許:${list})`,
|
|
105
225
|
effortNotAllowed: (agent, effort, provider, list) => `"${agent}" 列的 effort "${effort}" 不在 provider "${provider}" 的允許值域內(允許值:${list})`,
|
|
106
226
|
emptyAllowedNote: () => "(空——尚未驗證,任何值皆拒絕)",
|
|
@@ -121,11 +241,24 @@ const MESSAGES = {
|
|
|
121
241
|
providerStoreTrue: (name) => `providers.json: ${name}.store 為 true,違反設計原則 6(零留存)。載入即中止,不得依賴伺服器端狀態。`,
|
|
122
242
|
providerBaseUrlMissing: (name) => `providers.json: ${name}.baseURL 缺失`,
|
|
123
243
|
providerCharsPerTokenInvalid: (name) => `providers.json: ${name}.charsPerToken 須為正數`,
|
|
244
|
+
providerModelsRequired: (name) => `providers.json: ${name}.models 必須列出至少一個型號`,
|
|
245
|
+
pricingModelNotWhitelisted: (providerName, model) => `providers.json: ${providerName}.pricing.${model} 不在 models 白名單內`,
|
|
246
|
+
providersDbCorrupt: (name) => `providers DB 損毀:${name} 缺少型號資料或 JSON 無法解析`,
|
|
247
|
+
modelDisabled: (model, cmd) => `型號 "${model}" 已停用;請先執行 ${cmd} providers enable ${model}`,
|
|
248
|
+
providerModelNotFound: (model) => `找不到型號:${model}`,
|
|
249
|
+
providerModelAmbiguous: (model, providers) => `型號 "${model}" 同時存在於:${providers}。請用 ${"dowafu"} providers enable <provider> ${model} 指定目標;可先看 providers list。`,
|
|
250
|
+
providersHelp: (cmd) => `${cmd} providers list [--db <path>]\n${cmd} providers enable <model> [--db <path>]\n${cmd} providers enable <provider> <model> [--db <path>]\n${cmd} providers disable <model> [--db <path>]\n${cmd} providers disable <provider> <model> [--db <path>]\n${cmd} providers import <path> [--db <path>]`,
|
|
251
|
+
providersListEmpty: () => "providers DB 尚無型號;請明確執行 providers import。",
|
|
252
|
+
providersListRow: (provider, model, input, output, enabled) => `${provider}\t${model}\tinput $${input}/M\toutput $${output}/M\t${enabled ? "enabled" : "disabled"}`,
|
|
253
|
+
providersImported: (path) => `已合流匯入 providers:${path}`,
|
|
254
|
+
providersSeeded: (path) => `providers 表原本是空的,已自動從出貨檔播種:${path}(貴的型號預設關閉,用 providers list 查看)`,
|
|
255
|
+
providersDbEmpty: (cmd) => `providers 表沒有任何型號,所以沒有型號可用。這通常表示出貨的 providers.json 讀不到;執行 \`${cmd} providers import <出貨的 providers.json>\` 匯入,或用 \`${cmd} --doctor\` 查看來源。`,
|
|
256
|
+
providersEnabled: (model) => `已啟用型號:${model}`,
|
|
257
|
+
providersDisabled: (model) => `已停用型號:${model}`,
|
|
124
258
|
providersFileNotObject: () => "providers.json 格式不是物件",
|
|
125
|
-
providersFormatVersionMismatch: (want, got) => `providers.json: formatVersion 不符(預期 ${want},實際 ${got}
|
|
259
|
+
providersFormatVersionMismatch: (want, got) => `providers.json: formatVersion 不符(預期 ${want},實際 ${got})。這通常代表匯入了舊版或不相容的檔案。`,
|
|
126
260
|
providersFileInvalidJson: (msg) => `providers.json 不是合法 JSON:${msg}`,
|
|
127
261
|
providerUndefined: (name) => `providers.json 未定義 provider "${name}"`,
|
|
128
|
-
runLogWriteFailed: (maskedErr) => `run.jsonl 寫入失敗:${maskedErr}`,
|
|
129
262
|
noFullReportAvailable: (status) => `(無法取得完整回報,執行狀態:${status})`,
|
|
130
263
|
persistTextFailed: (agent, maskedErr) => `落檔失敗(${agent}.md):${maskedErr}`,
|
|
131
264
|
persistRawFailed: (agent, maskedErr) => `落檔失敗(${agent} raw/):${maskedErr}`,
|
|
@@ -134,6 +267,7 @@ const MESSAGES = {
|
|
|
134
267
|
noneLabel: () => "無",
|
|
135
268
|
unknownUsageKeysWarning: (provider, keys) => `⚠ 未知 usage 欄位:${provider} ${keys}`,
|
|
136
269
|
zeroSourceReadWarning: (n) => `⚠ 零原始碼讀取(允許 ${n} 檔)`,
|
|
270
|
+
finishReasonWarningCell: (reason) => `provider 回報截斷:${reason}`,
|
|
137
271
|
toolCallStats: (total, allowed, rejected) => `工具呼叫:${total}(允許 ${allowed}/拒絕 ${rejected})`,
|
|
138
272
|
closingLineCell: (passFail) => `收尾句:${passFail}`,
|
|
139
273
|
observationCountCell: (display) => `觀察:${display}`,
|
|
@@ -146,14 +280,17 @@ const MESSAGES = {
|
|
|
146
280
|
|
|
147
281
|
| agent | provider | api | model(請求) | model(回傳) | effort | store | status | 耗時 | token | 估算成本 | 稽核 |
|
|
148
282
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`,
|
|
149
|
-
|
|
150
|
-
|
|
283
|
+
// 熱修補(票 C 手測):原本寫「明確 providers import 合流」。那在寫下的當時是準的
|
|
284
|
+
// ——當時填 DB 的唯一途徑就是明確 import。票 A 的熱修補加了空表自動播種之後這句就變成
|
|
285
|
+
// 假的,而且它假的地方正好是「這些數字哪來的」,是這個欄位存在的唯一理由。
|
|
286
|
+
// 播種與 import 目前沒有分開記錄,所以這裡只講查得證的那半:來源是 DB。
|
|
287
|
+
providersDatabase: () => "SQLite DB",
|
|
151
288
|
strayHeadingsWarning: (reviewChars, strayNames) => ` ⚠ _shared.md 的「# 待審段落」只有 ${reviewChars} 字,另有 ${strayNames.length} 個頂層章節:` +
|
|
152
289
|
`${strayNames.map((s) => `「# ${s}」`).join("、")}` +
|
|
153
290
|
`——待審段落可能被它們切斷了。工單以 \`#\` 切分區塊,內嵌文件若自帶 \`#\` 標題請降成 \`##\`,` +
|
|
154
291
|
`或用 \`\`\` 圍籬包起來。確認過是刻意的就忽略本行`,
|
|
155
|
-
gitignoreNotIgnored: (
|
|
156
|
-
gitignoreUnknown: (
|
|
292
|
+
gitignoreNotIgnored: (dbPath) => ` ⚠ 資料庫檔案 ${dbPath} 未被其所在的 git repo 忽略`,
|
|
293
|
+
gitignoreUnknown: (dbPath) => ` ℹ 無法判定資料庫檔案 ${dbPath} 是否被 .gitignore 涵蓋(它不在目前目錄所屬的 git 工作樹內、目前目錄非 git repo,或 git 不可用)`,
|
|
157
294
|
aboutToDispatch: (ticketId) => `即將派工 ${ticketId}:`,
|
|
158
295
|
initialPromptEstimate: (totalEst, maxTokens) => ` 初始 prompt 估算 ${totalEst} tokens(僅 system prompt+首則訊息,不含工單與允許清單;本閘門的估算上限 ${maxTokens})`,
|
|
159
296
|
allowlistTotalEstimate: (tokens, files) => ` 允許清單總量估算 ${tokens} tokens(${files} 檔)`,
|
|
@@ -166,13 +303,19 @@ const MESSAGES = {
|
|
|
166
303
|
concurrencyLine: (n) => ` 並行度 ${n}`,
|
|
167
304
|
tpmPeakLine: (provider, limit, peak) => ` ${provider} tpmLimit ${limit},靜態估算峰值 ${peak}`,
|
|
168
305
|
tpmPeakCaveat: () => " └ 僅為靜態指標,不預測執行中的 TPM 曲線(429 等待會改變實際並行數)",
|
|
169
|
-
allowedReadsSummary: (n
|
|
306
|
+
allowedReadsSummary: (n) => ` 允許讀取 ${n} 個檔案,結果存入資料庫`,
|
|
307
|
+
allowedReadsListEntry: (path) => ` ${path}`,
|
|
308
|
+
allowedReadsListFolded: (n) => ` …另 ${n} 個`,
|
|
309
|
+
allowedReadsListEmpty: () => " (允許清單為空)",
|
|
170
310
|
lensClosingLineZh: (agent) => `ℹ ${agent} 的 lens 收尾句符合中文版固定收尾句`,
|
|
171
311
|
lensClosingLineEn: (agent) => `ℹ ${agent} 的 lens 收尾句符合英文版固定收尾句`,
|
|
172
312
|
modelPricing: (input, output, cachedSuffix, asOfSuffix) => ` └ 單價 每 M token:input $${input}/output $${output}${cachedSuffix}${asOfSuffix}`,
|
|
173
313
|
modelPricingCachedSuffix: (cached) => `/cached input $${cached}`,
|
|
174
314
|
modelPricingAsOfSuffix: (asOf) => `;價目查證日 ${asOf}`,
|
|
175
|
-
modelPricingMissing: (model) => ` └ ⚠
|
|
315
|
+
modelPricingMissing: (model) => ` └ ⚠ DB 沒有 "${model}" 的價目資料,本型號無法估算成本`,
|
|
316
|
+
hardCapLine: (hardCapUsd) => `硬上限 $${hardCapUsd}(絕對上界,非預測)`,
|
|
317
|
+
hardCapWithTypicalLine: (typicalUsd, runs, hardCapUsd) => `典型 $${typicalUsd}(過去 ${runs} 次)/硬上限 $${hardCapUsd}(絕對上界,非預測)`,
|
|
318
|
+
hardCapMissingPricing: () => "硬上限:無價目資料",
|
|
176
319
|
rawIntegrityCheckFailed: (msg) => `raw 完整性檢查失敗(實作缺陷,不重試):${msg}`,
|
|
177
320
|
rateLimitRetriesExceeded: (n) => `429 撞牆次數超過 --rate-limit-retries (${n})`,
|
|
178
321
|
rateLimitWaitExceeded: (s, cap) => `429 要求等待 ${s}s,超過 --max-rate-wait ${cap}s`,
|
|
@@ -189,14 +332,16 @@ const MESSAGES = {
|
|
|
189
332
|
doctorConfigDirSourceXdgConfigHome: (dir) => `${dir}(來源:XDG_CONFIG_HOME)`,
|
|
190
333
|
doctorConfigDirSourceDefault: (dir) => `${dir}(來源:預設;DISPATCH_HOME 與 XDG_CONFIG_HOME 都沒設)`,
|
|
191
334
|
doctorConfigDirUnresolved: () => "無法解析(沒有 HOME)",
|
|
335
|
+
doctorDbLine: (dbPath) => ` 資料庫 ${dbPath}`,
|
|
192
336
|
doctorEnvLine: (value) => ` .env ${value}`,
|
|
193
|
-
doctorEnvPresentValue: () => "
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
337
|
+
doctorEnvPresentValue: () => "有,但本版不讀取(API key 請用 `dowafu key` 設定進 DB)",
|
|
338
|
+
doctorApiKeyLine: (value) => ` API key ${value}`,
|
|
339
|
+
doctorApiKeyRow: (provider, status) => `${provider} ${status}`,
|
|
340
|
+
doctorApiKeySourceDb: (tail, date) => `DB(${tail},${date})`,
|
|
341
|
+
doctorApiKeySourceMissing: () => "未設定",
|
|
197
342
|
doctorProviderCountItem: (name, count) => `${name} ${count} 個`,
|
|
198
343
|
doctorModelListLine: (value) => ` 型號白名單 ${value}`,
|
|
199
|
-
doctorModelListValue: (
|
|
344
|
+
doctorModelListValue: (enabled, total, mostExpensive) => `啟用 ${enabled}/${total};最貴啟用型號 ${mostExpensive};來源 SQLite DB`,
|
|
200
345
|
doctorModelListLoadFailedValue: (reason) => `無法載入:${reason}`,
|
|
201
346
|
doctorLensLine: (value) => ` lens 定義 ${value}`,
|
|
202
347
|
doctorLensFoundValue: (dirPath, total, closingCount, closingNames, noClosingSuffix) => `${dirPath} 找到 ${total} 個定義檔,其中 ${closingCount} 個具備固定收尾句:\n${" ".repeat(14)}${closingNames}${noClosingSuffix}`,
|
|
@@ -208,13 +353,13 @@ const MESSAGES = {
|
|
|
208
353
|
unknownOption: (arg, helpText) => `Unknown option: ${arg}\n\n${helpText}`,
|
|
209
354
|
missingProviders: (path) => `providers.json not found: ${path}`,
|
|
210
355
|
dryRunNotice: () => "--dry-run: parsed, validated, estimated, and reported only; no API calls were made.",
|
|
211
|
-
helpText: (cmd) => `Usage: ${cmd} <ticket-
|
|
356
|
+
helpText: (cmd) => `Usage: ${cmd} <ticket-id> [options]
|
|
212
357
|
|
|
213
358
|
--lang <en|zh-tw> Language for CLI output and spoke prompts, default en
|
|
214
359
|
--repo-root <dir> Root for the allowlist boundary and .claude/agents, default cwd
|
|
215
|
-
--
|
|
360
|
+
--db <path> SQLite ticket/result database, default DISPATCH_HOME/dowafu.db
|
|
361
|
+
--http-port <port> HTTP binding port for serve mode, loopback (127.0.0.1) only, default 7391
|
|
216
362
|
--json stdout prints only the result JSON; all other output goes to stderr
|
|
217
|
-
--out <dir> Output directory, default tmp/spoke/
|
|
218
363
|
--concurrency <n> Number of spokes to run concurrently, default 2
|
|
219
364
|
--max-tokens <n> Pre-call estimation gate (sum of each spoke's initial prompt), default 200000
|
|
220
365
|
--max-spoke-tokens <n> Per-spoke runtime cumulative cap (actual usage), default 400000
|
|
@@ -225,12 +370,23 @@ const MESSAGES = {
|
|
|
225
370
|
--max-round-reasoning-tokens <n> Per-round reasoning-token cap, default null (no check)
|
|
226
371
|
--rate-limit-retries <n> Retries dedicated to 429s, default 5 (not counted in --retries)
|
|
227
372
|
--max-rate-wait <sec> Max wait for a single 429, default 30
|
|
228
|
-
--max-tool-calls <n> Per-spoke read_file call cap, default
|
|
373
|
+
--max-tool-calls <n> Per-spoke read_file call cap, default 20
|
|
229
374
|
--dry-run Parse, validate, estimate, and print the report only; no API calls
|
|
230
375
|
--yes Skip the dispatch confirmation. Aborts in non-interactive environments (stdin not a TTY) unless given
|
|
231
376
|
--doctor Print the configuration self-check (no API call, no cost) and exit (exit 0)
|
|
232
377
|
--help, -h Print this help and exit (exit 0)
|
|
233
|
-
--version, -V Print the version and exit (exit 0)
|
|
378
|
+
--version, -V Print the version and exit (exit 0)
|
|
379
|
+
|
|
380
|
+
Subcommands (each has its own usage message):
|
|
381
|
+
${cmd} ticket ... Create, import and inspect tickets
|
|
382
|
+
${cmd} result <id> Print the results of one dispatch (spoke output and audit table)
|
|
383
|
+
${cmd} key Set provider API keys (interactive menu)
|
|
384
|
+
${cmd} providers ... Model whitelist and enabled state
|
|
385
|
+
${cmd} token ... Issue, list and revoke inbound HTTP tokens
|
|
386
|
+
${cmd} approve [jobId] Approve a dispatch waiting in the queue
|
|
387
|
+
${cmd} serve Run the daemon (includes the MCP HTTP binding)
|
|
388
|
+
${cmd} serve --stop [--yes] Stop the daemon; asks first if jobs are running
|
|
389
|
+
${cmd} mcp Serve MCP over stdio`,
|
|
234
390
|
availableLangValues: () => "en, zh-tw, zh",
|
|
235
391
|
availableValuesSuffix: (values) => ` (available: ${values})`,
|
|
236
392
|
numberFlagInvalid: (name, value) => `--${name} requires a number, got: ${value}`,
|
|
@@ -251,16 +407,112 @@ const MESSAGES = {
|
|
|
251
407
|
cancelledInteractive: () => "Cancelled; no API calls were made.",
|
|
252
408
|
cancelledNonInteractive: () => "Non-interactive environment (stdin is not a TTY); nobody could confirm. Cancelled; no API calls were made. To dispatch in this environment, pass --yes explicitly.",
|
|
253
409
|
outDirNotWritable: (outDir) => `Output directory is not writable: ${outDir}`,
|
|
254
|
-
|
|
255
|
-
`Those came from a previous run, and overwriting them loses them.
|
|
410
|
+
resultsExistAbort: (resultId) => `This id already has results. Nothing was dispatched; no API was called: ${resultId}\n` +
|
|
411
|
+
`Those came from a previous run, and overwriting them loses them. Read them first: dowafu result ${resultId}\n` +
|
|
412
|
+
`Two ways forward:\n` +
|
|
256
413
|
` 1. Pick a ticket-id you have not used and dispatch under that (recommended; it costs nothing)\n` +
|
|
257
|
-
` 2. Have the user
|
|
258
|
-
` * If the previous run ended in failure (all failed, zero tokens), nothing
|
|
259
|
-
`
|
|
260
|
-
|
|
261
|
-
|
|
414
|
+
` 2. Have the user delete those results from the database themselves, then rerun — that call is theirs, not yours\n` +
|
|
415
|
+
` * If the previous run ended in failure (all failed, zero tokens), nothing there was paid for\n` +
|
|
416
|
+
` — a new id or the user deleting it are both fine, but it is still their call to delete`,
|
|
417
|
+
resultsExistDryRunWarning: (resultId) => `⚠ This id already has results: ${resultId}\n The dry run is unaffected, but the real run will be stopped. Pick a different ticket-id; to read the previous run first, use dowafu result ${resultId}.`,
|
|
418
|
+
resultsSaved: (resultId) => `Results saved to the database. Read them back with: dowafu result ${resultId}`,
|
|
262
419
|
outDirFallbackStderr: () => "Output directory is not writable; the full report was printed to stderr instead:",
|
|
263
420
|
stdoutSummaryLine: (agent, status, model, tokens, costLabel, latencyMs) => `${agent}: ${status} model=${model} token=${tokens} cost=${costLabel} elapsed=${latencyMs}ms`,
|
|
421
|
+
dbResultPersistenceFailed: (agent, maskedErr) => `DB result persistence failed (${agent}): ${maskedErr}`,
|
|
422
|
+
dbSchemaTooNew: (found, supported) => `This database was created by a newer dowafu (schema version ${found}; this build supports up to ${supported}). Upgrade dowafu instead of opening it with an older build.`,
|
|
423
|
+
approveHelp: (cmd) => `Usage: ${cmd} approve [jobId prefix] [--db <path>]`,
|
|
424
|
+
resultHelp: (cmd) => `Usage: ${cmd} result <ticket-id or jobId/prefix> [--db <path>]`,
|
|
425
|
+
resultNotFound: (id) => `No results found for "${id}". The dispatch has not finished, or that ticket-id/jobId does not exist.`,
|
|
426
|
+
resultPrefixAmbiguous: (prefix, count) => `Prefix ${prefix} matches ${count} jobs; provide a longer id.`,
|
|
427
|
+
resultCandidate: (shortId, ticket, status) => ` ${shortId} ${ticket} ${status}`,
|
|
428
|
+
approveNoPending: () => "There are no jobs awaiting approval.",
|
|
429
|
+
approveMultiplePending: (count) => `${count} pending approvals found; specify one short id.`,
|
|
430
|
+
approvePrefixAmbiguous: (prefix, count) => `Prefix ${prefix} matches ${count} jobs; provide a longer id.`,
|
|
431
|
+
approvePendingCandidate: (shortId, ticket, createdAt) => ` ${shortId} ${ticket} ${createdAt}`,
|
|
432
|
+
jobNotFound: (jobId) => `Job not found: ${jobId}`,
|
|
433
|
+
mcpJobPrefixAmbiguous: (prefix, count) => `Prefix ${prefix} matches ${count} jobs; use the full id:`,
|
|
434
|
+
jobNotApprovable: (jobId, status) => `Job ${jobId} is ${status} and cannot be approved`,
|
|
435
|
+
jobApproved: (jobId) => `Approved job: ${jobId}`,
|
|
436
|
+
daemonStarted: () => "Daemon started. SIGINT exits cleanly; running jobs are reaped on the next start.",
|
|
437
|
+
daemonStartFailed: (detail) => `Could not start daemon: ${detail}`,
|
|
438
|
+
daemonWorkerSpawnFailed: (detail) => `Could not start worker: ${detail}`,
|
|
439
|
+
daemonWorkerExited: (exitCode) => `worker exited ${exitCode}`,
|
|
440
|
+
daemonWorkerExitedDetail: (exitCode, detail) => `worker exited ${exitCode}: ${detail}`,
|
|
441
|
+
daemonWorkerTerminated: (signal) => `worker terminated by ${signal}`,
|
|
442
|
+
daemonWorkerTerminatedDetail: (signal, detail) => `worker terminated by ${signal}: ${detail}`,
|
|
443
|
+
stopRequiresServe: () => "--stop can only be used with serve.",
|
|
444
|
+
daemonStopRunningWarning: (count) => `${count} job(s) are running; stopping the daemon does not stop their active spokes, which continue to completion and will still have their results and API costs recorded.`,
|
|
445
|
+
daemonStopConfirmPrompt: () => "Stop the daemon? [y/N] ",
|
|
446
|
+
daemonStopCancelled: () => "Cancelled; the daemon is still running.",
|
|
447
|
+
daemonStopNotRunning: (reason) => `Daemon is not running (${reason}).`,
|
|
448
|
+
daemonStopPidNotDowafu: (pid) => `pid ${pid} is not a dowafu process; no signal was sent to avoid killing the wrong process.`,
|
|
449
|
+
daemonStopSignalFailed: (pid) => `A stop signal was sent to pid ${pid}, but it did not stop.`,
|
|
450
|
+
daemonStopSucceeded: (pid) => `Daemon (pid ${pid}) stopped.`,
|
|
451
|
+
daemonStopRestarted: (pid) => `Daemon (previous pid ${pid}) came back after stopping; an OS-level service manager appears to be restarting it. Use launchctl or systemctl to stop it permanently.`,
|
|
452
|
+
doctorDaemonLine: (value) => ` Daemon ${value}`,
|
|
453
|
+
doctorDaemonAlive: (pid) => `running (pid ${pid})`,
|
|
454
|
+
doctorDaemonMissing: (reason) => `not running (${reason})`,
|
|
455
|
+
daemonOfflineWarning: (reason) => `⚠ The worker daemon is not running (${reason}); the job is queued but will not execute. Start it with dowafu serve.`,
|
|
456
|
+
mcpTicketsEmpty: () => "No dispatchable DB tickets.",
|
|
457
|
+
mcpTicketsTitle: () => "Dispatchable tickets:",
|
|
458
|
+
mcpTicketsRow: (ticket, spokeCount) => `- ${ticket}: ${spokeCount} spoke(s)`,
|
|
459
|
+
mcpTicketsSpoke: (agent, provider, model, allowCount) => ` ${agent}: ${provider}/${model}; allowlist files=${allowCount}`,
|
|
460
|
+
mcpKeysTitle: () => "API key status:",
|
|
461
|
+
mcpKeysRow: (provider, status) => ` ${provider}: ${status}`,
|
|
462
|
+
mcpEnabledModelsTitle: () => "Enabled model allowlist:",
|
|
463
|
+
mcpEnabledModelRow: (provider, model) => ` ${provider}/${model}`,
|
|
464
|
+
mcpSubmitQueued: (jobId) => `Job queued: ${jobId}; status pending_approval. It has not started and needs terminal approval by a human.`,
|
|
465
|
+
mcpApproveCommand: (command) => `Run this in a terminal: ${command}`,
|
|
466
|
+
mcpJobStarted: (at) => `Started at ${at}`,
|
|
467
|
+
mcpJobFinished: (at) => `Finished at ${at}`,
|
|
468
|
+
mcpJobError: (detail) => `Error: ${detail}`,
|
|
469
|
+
mcpJobProgress: (agent, lastRound, tokensIn, tokensOut, lastAt) => ` ${agent}: round ${lastRound}, ${tokensIn} in / ${tokensOut} out tokens so far, last updated ${lastAt}`,
|
|
470
|
+
mcpJobNoProgressYet: () => " (no round has completed yet)",
|
|
471
|
+
mcpJobHeartbeatStale: (seconds) => `⚠ The worker's heartbeat has not moved for ${seconds}s; this job is most likely dead. ` +
|
|
472
|
+
`The next reap will mark it interrupted; with no daemon running it stays at running indefinitely.`,
|
|
473
|
+
mcpJobOutcome: (succeeded, failed, failedAgentsSuffix) => `Spoke outcomes: ${succeeded} succeeded / ${failed} failed${failedAgentsSuffix}`,
|
|
474
|
+
mcpJobFailedAgentsSuffix: (agents) => ` (failed: ${agents})`,
|
|
475
|
+
mcpResultPending: (jobId, status) => `Job ${jobId} is ${status}; no result is available yet.`,
|
|
476
|
+
mcpResultComplete: (jobId) => `Job ${jobId} result:`,
|
|
477
|
+
mcpResultEmpty: () => "(No stored spoke results.)",
|
|
478
|
+
mcpApproveAmountMismatch: (jobId, providedUsd, expectedUsd) => `Approval rejected: amount mismatch. Job ${jobId}'s hard cap is $${expectedUsd}; you gave ${providedUsd}. Not approved; no API was called.`,
|
|
479
|
+
mcpApproveNoPricing: (jobId) => `Approval rejected: the ticket behind job ${jobId} lacks pricing data, so the amount could not be verified. Not approved.`,
|
|
480
|
+
httpServerStarted: (port) => `HTTP binding started: 127.0.0.1:${port}/mcp (loopback only; exposure is the tunnel's job)`,
|
|
481
|
+
httpNoTokensWarning: () => "⚠ No token has been issued yet (dowafu token issue). The endpoint is alive, but no request can get in — it isn't broken, it just has no token yet.",
|
|
482
|
+
tokenHelp: (cmd) => `${cmd} token issue [--label <text>]\n${cmd} token list\n${cmd} token revoke <id>`,
|
|
483
|
+
tokenIssued: (id, plaintext) => `Token issued, id = ${id}\nPlaintext (shown only this once, unrecoverable afterward): ${plaintext}`,
|
|
484
|
+
tokenListEmpty: () => "No tokens have been issued yet.",
|
|
485
|
+
tokenListRow: (id, label, createdAt, revoked) => `${id}\t${label}\t${createdAt}\t${revoked ? "revoked" : "active"}`,
|
|
486
|
+
tokenRevoked: (id) => `Revoked token: ${id}`,
|
|
487
|
+
tokenNotFoundOrRevoked: (id) => `Token not found or already revoked: ${id}`,
|
|
488
|
+
keyHelp: (cmd) => `${cmd} key Interactive setup (pick a provider from a menu, paste the key; takes no CLI args)\n${cmd} key list List each provider's currently effective source (no plaintext)\n${cmd} key rm <provider> Remove that provider's key from the DB\n${cmd} key test <provider> Call the real API once to verify the key (costs money)`,
|
|
489
|
+
keyInteractiveRequiresTty: () => "dowafu key needs an interactive terminal (stdin is not a TTY); it will not hang waiting for input or read from a pipe. Run dowafu key in an interactive terminal to set the API key.",
|
|
490
|
+
keyMenuHeader: () => " provider status",
|
|
491
|
+
keyMenuRow: (num, providerPadded, status) => ` ${num}) ${providerPadded} ${status}`,
|
|
492
|
+
keyMenuPrompt: (max) => `Choose a provider to set (1-${max}, q to quit): `,
|
|
493
|
+
keyMenuInvalidChoice: () => "Invalid choice.",
|
|
494
|
+
keyMenuQuit: () => "Left without changing anything.",
|
|
495
|
+
keyPastePrompt: (provider) => `Paste the API key for ${provider} (will not be echoed): `,
|
|
496
|
+
keyPasteEmpty: () => "Nothing was pasted; cancelled.",
|
|
497
|
+
keySavedTail: (tail) => `✓ Saved to the DB (${tail}).`,
|
|
498
|
+
keyVerifyPrompt: (provider) => `Verify this key now? This calls ${provider}'s API once and costs money (y/N): `,
|
|
499
|
+
keyListRow: (provider, status) => `${provider}\t${status}`,
|
|
500
|
+
keyRemoved: (provider) => `Removed ${provider}'s key from the DB.`,
|
|
501
|
+
keyRemovedNotFound: (provider) => `The DB has no key for ${provider}; nothing to remove.`,
|
|
502
|
+
keyUnknownProvider: (provider, list) => `Unknown provider: ${provider} (known: ${list})`,
|
|
503
|
+
keyStatusDb: (tail, date) => `✓ set (${tail}, DB, ${date})`,
|
|
504
|
+
keyStatusMissing: () => "✗ not set",
|
|
505
|
+
keyStatusUntested: () => "; never tested",
|
|
506
|
+
keyStatusTestSucceeded: (model, at) => `; last test passed (${model}, ${at})`,
|
|
507
|
+
keyStatusTestFailed: (model, at) => `; last test failed (${model}, ${at})`,
|
|
508
|
+
keyTestHelp: (cmd) => `Usage: ${cmd} key test <provider>`,
|
|
509
|
+
keyTestKeyMissing: (provider) => `${provider} has no API key set yet (neither DB nor environment) — set one with \`dowafu key\` first.`,
|
|
510
|
+
keyTestNoEnabledModel: (provider) => `${provider} has no enabled model to test with (see \`dowafu providers enable\`).`,
|
|
511
|
+
keyTestCostWarning: (provider) => `About to call ${provider}'s API once as a minimal verification. This costs money.`,
|
|
512
|
+
keyTestConfirmPrompt: () => "Proceed? [y/N] ",
|
|
513
|
+
keyTestCancelled: () => "Cancelled; no API call was made.",
|
|
514
|
+
keyTestSucceeded: (provider, model) => `✓ ${provider}/${model} verified; this key works.`,
|
|
515
|
+
keyTestFailed: (provider, detail) => `✗ ${provider} verification failed: ${detail}`,
|
|
264
516
|
formatMarkerMismatch: (marker, got) => `_dispatch.md's first line must be ${marker}, got: ${got}`,
|
|
265
517
|
blankPlaceholder: () => "(blank)",
|
|
266
518
|
dispatchTableMissingHeader: () => "_dispatch.md: dispatch table not found (missing | agent | ... | header or separator row)",
|
|
@@ -282,9 +534,22 @@ const MESSAGES = {
|
|
|
282
534
|
missingQuestionsSection: () => '<agent>.md is missing "# Questions" (or "# 具體問題" in a Chinese-language ticket) or its content is empty',
|
|
283
535
|
fileNotFound: (path) => `Not found: ${path}`,
|
|
284
536
|
agentFileNotFound: (agentPath, agent) => `Not found: ${agentPath} (_dispatch.md lists agent "${agent}")`,
|
|
537
|
+
ticketNotFound: (name) => `DB ticket not found: ${name}`,
|
|
538
|
+
ticketCorrupt: (name) => `DB ticket is corrupt: ${name}`,
|
|
539
|
+
ticketNameInvalid: (name) => `Invalid ticket name: ${name}`,
|
|
540
|
+
ticketAlreadyExists: (name) => `DB ticket already exists: ${name}`,
|
|
541
|
+
ticketSpokeNotFound: (name, agent) => `DB ticket "${name}" has no agent "${agent}"`,
|
|
542
|
+
ticketShowHeader: (name) => `DB ticket ${name}:`,
|
|
543
|
+
ticketEstimateTokens: (tokens) => `est. ${tokens} tokens`,
|
|
544
|
+
ticketCompletedRuns: (runs, total, last) => `Completed ${runs} times; total $${total}; last ${last}`,
|
|
545
|
+
ticketCompletedRunsNoPricing: (runs, last) => `Completed ${runs} times; total No pricing data; last ${last}`,
|
|
546
|
+
ticketHelp: (cmd) => `${cmd} ticket create <name> <shared-file>\n${cmd} ticket add-spoke <name> <agent> <provider> <model> <body-file> [effort]\n${cmd} ticket add-allow <name> <agent> <repo-relative-path> [--repo-root <dir>]\n${cmd} ticket import <existing-ticket-dir> [name] [--repo-root <dir>]\n${cmd} ticket show <name>\n <shared-file>/<body-file> may be - to read the body from stdin (stdin must be piped/redirected, not an interactive terminal)`,
|
|
547
|
+
ticketStdinRequiresPipe: () => "This command reads the body from stdin via -, but stdin is an interactive terminal right now; nothing is being piped in. " +
|
|
548
|
+
'Pipe or redirect content instead (for example, echo "..." | dowafu ticket create <name> -), or use a regular file path.',
|
|
285
549
|
agentDefNotFound: (path, agent) => `Agent definition file not found: ${path} (_dispatch.md lists "${agent}")`,
|
|
286
550
|
providerUndefinedInRow: (agent, provider) => `_dispatch.md's "${agent}" row references provider "${provider}", which is not defined in providers.json`,
|
|
287
|
-
|
|
551
|
+
apiKeyNotConfigured: (agent, provider, cmd) => `No API key configured for provider "${provider}" (the "${agent}" row needs one).\n`
|
|
552
|
+
+ ` This version keeps API keys in the database and no longer reads .env or environment variables; run \`${cmd} key\` to set one.`,
|
|
288
553
|
modelNotWhitelisted: (agent, model, provider, list) => `The "${agent}" row's model "${model}" is not in provider "${provider}"'s models whitelist (allowed: ${list})`,
|
|
289
554
|
effortNotAllowed: (agent, effort, provider, list) => `The "${agent}" row's effort "${effort}" is not in provider "${provider}"'s allowed range (allowed: ${list})`,
|
|
290
555
|
emptyAllowedNote: () => "(empty — not yet verified, any value is rejected)",
|
|
@@ -308,12 +573,25 @@ const MESSAGES = {
|
|
|
308
573
|
`Aborting on load; server-side state must not be relied upon.`,
|
|
309
574
|
providerBaseUrlMissing: (name) => `providers.json: ${name}.baseURL is missing`,
|
|
310
575
|
providerCharsPerTokenInvalid: (name) => `providers.json: ${name}.charsPerToken must be a positive number`,
|
|
576
|
+
providerModelsRequired: (name) => `providers.json: ${name}.models must list at least one model`,
|
|
577
|
+
pricingModelNotWhitelisted: (providerName, model) => `providers.json: ${providerName}.pricing.${model} is not in the models whitelist`,
|
|
578
|
+
providersDbCorrupt: (name) => `providers DB is corrupt: ${name} has no model data or invalid JSON`,
|
|
579
|
+
modelDisabled: (model, cmd) => `Model "${model}" is disabled; first run ${cmd} providers enable ${model}`,
|
|
580
|
+
providerModelNotFound: (model) => `Model not found: ${model}`,
|
|
581
|
+
providerModelAmbiguous: (model, providers) => `Model "${model}" exists in: ${providers}. Use dowafu providers enable <provider> ${model}; see providers list.`,
|
|
582
|
+
providersHelp: (cmd) => `${cmd} providers list [--db <path>]\n${cmd} providers enable <model> [--db <path>]\n${cmd} providers enable <provider> <model> [--db <path>]\n${cmd} providers disable <model> [--db <path>]\n${cmd} providers disable <provider> <model> [--db <path>]\n${cmd} providers import <path> [--db <path>]`,
|
|
583
|
+
providersListEmpty: () => "The providers DB has no models; explicitly run providers import.",
|
|
584
|
+
providersListRow: (provider, model, input, output, enabled) => `${provider}\t${model}\tinput $${input}/M\toutput $${output}/M\t${enabled ? "enabled" : "disabled"}`,
|
|
585
|
+
providersImported: (path) => `Providers merged from: ${path}`,
|
|
586
|
+
providersSeeded: (path) => `The providers table was empty and has been seeded from the bundled file: ${path} (expensive models start disabled; see providers list)`,
|
|
587
|
+
providersDbEmpty: (cmd) => `The providers table holds no models, so no model is usable. That usually means the bundled providers.json could not be read; run \`${cmd} providers import <bundled providers.json>\` to load it, or \`${cmd} --doctor\` to see the source.`,
|
|
588
|
+
providersEnabled: (model) => `Enabled model: ${model}`,
|
|
589
|
+
providersDisabled: (model) => `Disabled model: ${model}`,
|
|
311
590
|
providersFileNotObject: () => "providers.json format is not an object",
|
|
312
591
|
providersFormatVersionMismatch: (want, got) => `providers.json: formatVersion mismatch (expected ${want}, got ${got}). ` +
|
|
313
|
-
`This usually means
|
|
592
|
+
`This usually means an old or incompatible file was imported.`,
|
|
314
593
|
providersFileInvalidJson: (msg) => `providers.json is not valid JSON: ${msg}`,
|
|
315
594
|
providerUndefined: (name) => `providers.json does not define provider "${name}"`,
|
|
316
|
-
runLogWriteFailed: (maskedErr) => `Failed to write run.jsonl: ${maskedErr}`,
|
|
317
595
|
noFullReportAvailable: (status) => `(Full report unavailable; execution status: ${status})`,
|
|
318
596
|
persistTextFailed: (agent, maskedErr) => `Failed to write file (${agent}.md): ${maskedErr}`,
|
|
319
597
|
persistRawFailed: (agent, maskedErr) => `Failed to write file (${agent} raw/): ${maskedErr}`,
|
|
@@ -322,6 +600,7 @@ const MESSAGES = {
|
|
|
322
600
|
noneLabel: () => "none",
|
|
323
601
|
unknownUsageKeysWarning: (provider, keys) => `⚠ Unknown usage field(s): ${provider} ${keys}`,
|
|
324
602
|
zeroSourceReadWarning: (n) => `⚠ Zero source reads (allowed ${n} file(s))`,
|
|
603
|
+
finishReasonWarningCell: (reason) => `Provider-reported truncation:${reason}`,
|
|
325
604
|
toolCallStats: (total, allowed, rejected) => `Tool calls:${total} (allowed ${allowed} / rejected ${rejected})`,
|
|
326
605
|
closingLineCell: (passFail) => `Closing line:${passFail}`,
|
|
327
606
|
observationCountCell: (display) => `Observations:${display}`,
|
|
@@ -334,14 +613,13 @@ const MESSAGES = {
|
|
|
334
613
|
|
|
335
614
|
| agent | provider | api | model(requested) | model(returned) | effort | store | status | latency | token | est. cost | audit |
|
|
336
615
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`,
|
|
337
|
-
|
|
338
|
-
providersExplicit: (path, formatVersion) => `external file ${path} (formatVersion ${formatVersion})`,
|
|
616
|
+
providersDatabase: () => "SQLite DB",
|
|
339
617
|
strayHeadingsWarning: (reviewChars, strayNames) => ` ⚠ _shared.md's "# Under review" holds only ${reviewChars} characters, and ${strayNames.length} other ` +
|
|
340
618
|
`top-level heading(s) follow: ${strayNames.map((s) => `"# ${s}"`).join(", ")} — the section under review ` +
|
|
341
619
|
`may have been cut off by them. The ticket splits into sections by \`#\`; if an embedded document has its ` +
|
|
342
620
|
`own \`#\` headings, demote them to \`##\` or wrap the block in \`\`\` fences. Ignore this line if it is intended`,
|
|
343
|
-
gitignoreNotIgnored: (
|
|
344
|
-
gitignoreUnknown: (
|
|
621
|
+
gitignoreNotIgnored: (dbPath) => ` ⚠ Database file ${dbPath} is not ignored by the git repo it lives in`,
|
|
622
|
+
gitignoreUnknown: (dbPath) => ` ℹ Cannot determine whether database file ${dbPath} is covered by .gitignore (it is outside the git work tree of the current directory, the current directory is not a git repo, or git is unavailable)`,
|
|
345
623
|
aboutToDispatch: (ticketId) => `About to dispatch ${ticketId}:`,
|
|
346
624
|
initialPromptEstimate: (totalEst, maxTokens) => ` Initial prompt estimate ${totalEst} tokens (system prompt + first message only; excludes the ticket and allowlist; this gate's cap is ${maxTokens})`,
|
|
347
625
|
allowlistTotalEstimate: (tokens, files) => ` Allowlist total estimate ${tokens} tokens (${files} file(s))`,
|
|
@@ -354,13 +632,19 @@ const MESSAGES = {
|
|
|
354
632
|
concurrencyLine: (n) => ` Concurrency ${n}`,
|
|
355
633
|
tpmPeakLine: (provider, limit, peak) => ` ${provider} tpmLimit ${limit}, statically estimated peak ${peak}`,
|
|
356
634
|
tpmPeakCaveat: () => " └ Static indicator only; does not predict the in-flight TPM curve (429 waits change actual concurrency)",
|
|
357
|
-
allowedReadsSummary: (n
|
|
635
|
+
allowedReadsSummary: (n) => ` Allowed reads: ${n} file(s); results go to the database`,
|
|
636
|
+
allowedReadsListEntry: (path) => ` ${path}`,
|
|
637
|
+
allowedReadsListFolded: (n) => ` …and ${n} more`,
|
|
638
|
+
allowedReadsListEmpty: () => " (Allowlist is empty)",
|
|
358
639
|
lensClosingLineZh: (agent) => `ℹ ${agent}'s lens closing line matches the Chinese fixed closing line`,
|
|
359
640
|
lensClosingLineEn: (agent) => `ℹ ${agent}'s lens closing line matches the English fixed closing line`,
|
|
360
641
|
modelPricing: (input, output, cachedSuffix, asOfSuffix) => ` └ Price per M tokens: input $${input} / output $${output}${cachedSuffix}${asOfSuffix}`,
|
|
361
642
|
modelPricingCachedSuffix: (cached) => ` / cached input $${cached}`,
|
|
362
643
|
modelPricingAsOfSuffix: (asOf) => `; priced as of ${asOf}`,
|
|
363
|
-
modelPricingMissing: (model) => ` └ ⚠
|
|
644
|
+
modelPricingMissing: (model) => ` └ ⚠ The database has no pricing data for "${model}"; cost cannot be estimated for it`,
|
|
645
|
+
hardCapLine: (hardCapUsd) => `Hard cap $${hardCapUsd} (absolute upper bound, not a prediction)`,
|
|
646
|
+
hardCapWithTypicalLine: (typicalUsd, runs, hardCapUsd) => `Typical $${typicalUsd} (last ${runs} runs) / hard cap $${hardCapUsd} (absolute upper bound, not a prediction)`,
|
|
647
|
+
hardCapMissingPricing: () => "Hard cap: no pricing data",
|
|
364
648
|
rawIntegrityCheckFailed: (msg) => `Raw integrity check failed (implementation defect, not retried): ${msg}`,
|
|
365
649
|
rateLimitRetriesExceeded: (n) => `429 retry count exceeded --rate-limit-retries (${n})`,
|
|
366
650
|
rateLimitWaitExceeded: (s, cap) => `429 requested a ${s}s wait, exceeding --max-rate-wait ${cap}s`,
|
|
@@ -377,14 +661,16 @@ const MESSAGES = {
|
|
|
377
661
|
doctorConfigDirSourceXdgConfigHome: (dir) => `${dir} (source: XDG_CONFIG_HOME)`,
|
|
378
662
|
doctorConfigDirSourceDefault: (dir) => `${dir} (source: default; neither DISPATCH_HOME nor XDG_CONFIG_HOME is set)`,
|
|
379
663
|
doctorConfigDirUnresolved: () => "could not resolve (no HOME)",
|
|
664
|
+
doctorDbLine: (dbPath) => ` Database ${dbPath}`,
|
|
380
665
|
doctorEnvLine: (value) => ` .env ${value}`,
|
|
381
|
-
doctorEnvPresentValue: () => "present
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
666
|
+
doctorEnvPresentValue: () => "present, but this version does not read it (set API keys with `dowafu key`)",
|
|
667
|
+
doctorApiKeyLine: (value) => ` API keys ${value}`,
|
|
668
|
+
doctorApiKeyRow: (provider, status) => `${provider} ${status}`,
|
|
669
|
+
doctorApiKeySourceDb: (tail, date) => `DB (${tail}, ${date})`,
|
|
670
|
+
doctorApiKeySourceMissing: () => "not set",
|
|
385
671
|
doctorProviderCountItem: (name, count) => `${name} ${count}`,
|
|
386
672
|
doctorModelListLine: (value) => ` Model list ${value}`,
|
|
387
|
-
doctorModelListValue: (
|
|
673
|
+
doctorModelListValue: (enabled, total, mostExpensive) => `enabled ${enabled}/${total}; most expensive enabled ${mostExpensive}; source SQLite DB`,
|
|
388
674
|
doctorModelListLoadFailedValue: (reason) => `failed to load: ${reason}`,
|
|
389
675
|
doctorLensLine: (value) => ` Lens defs ${value}`,
|
|
390
676
|
doctorLensFoundValue: (dirPath, total, closingCount, closingNames, noClosingSuffix) => `${dirPath} holds ${total} definition file(s), ${closingCount} with a fixed closing line:\n${" ".repeat(15)}${closingNames}${noClosingSuffix}`,
|