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/runner.js
CHANGED
|
@@ -3,15 +3,23 @@
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildFinalizeUserText, buildFirstUserText, buildSystemPrompt } from "./prompt.js";
|
|
6
|
-
import {
|
|
6
|
+
import { allowlistRejectMessage, checkAllowlist } from "./whitelist.js";
|
|
7
7
|
import { findUnknownUsageKeys, sumUsage, usageProviderKeyFor } from "./usage.js";
|
|
8
8
|
import { estimateCostUsd } from "./cost.js";
|
|
9
9
|
import { describeError, ProviderHttpError } from "./mask.js";
|
|
10
10
|
import { parseRetryAfter } from "./rate-limit.js";
|
|
11
11
|
import { RawIntegrityError } from "./raw-integrity.js";
|
|
12
12
|
import { classifyError } from "./error-classify.js";
|
|
13
|
+
import { m } from "./messages.js";
|
|
13
14
|
const MAX_FILE_BYTES = 200 * 1024; // §13:單檔 200KB 上限,防「單輪讀入巨檔」異常
|
|
14
15
|
const MAX_ROUNDS = 60; // 安全上限,遠高於實務上的 --max-tool-calls,純防無窮迴圈 bug
|
|
16
|
+
// i18n_classification_t2.md §三之1:這兩則都成為 executeToolCall/runSpoke 的
|
|
17
|
+
// resultText,被 push 進 conv.turns 送往外部 API——消費者是 spoke 不是人類,歸 C 類,
|
|
18
|
+
// 不進 messages.ts,由 spoke.lang 直接選用(同 whitelist.ts 的 allowlistRejectMessage)。
|
|
19
|
+
const FILE_TRUNCATED_SUFFIX = "\n...(檔案超過 200KB 上限,內容已截斷)";
|
|
20
|
+
const FILE_TRUNCATED_SUFFIX_EN = "\n...(file exceeds the 200KB limit; content truncated)";
|
|
21
|
+
const TOOL_LIMIT_REACHED_MESSAGE = "已達 --max-tool-calls 上限,未執行";
|
|
22
|
+
const TOOL_LIMIT_REACHED_MESSAGE_EN = "The --max-tool-calls limit has been reached; not executed.";
|
|
15
23
|
function sleep(ms) {
|
|
16
24
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
25
|
}
|
|
@@ -21,7 +29,7 @@ async function executeToolCall(call, spoke, repoRoot) {
|
|
|
21
29
|
const check = checkAllowlist(spoke.allowSet, requestedPath, repoRoot);
|
|
22
30
|
if (!check.allowed) {
|
|
23
31
|
return {
|
|
24
|
-
resultText:
|
|
32
|
+
resultText: allowlistRejectMessage(spoke.lang),
|
|
25
33
|
log: { path: requestedPath, allowed: false, reason: check.reason, startedAt, durationMs: Date.now() - startedAt },
|
|
26
34
|
};
|
|
27
35
|
}
|
|
@@ -29,7 +37,8 @@ async function executeToolCall(call, spoke, repoRoot) {
|
|
|
29
37
|
const buf = await readFile(check.realPath);
|
|
30
38
|
let content = buf.toString("utf8");
|
|
31
39
|
if (buf.byteLength > MAX_FILE_BYTES) {
|
|
32
|
-
|
|
40
|
+
const suffix = spoke.lang === "en" ? FILE_TRUNCATED_SUFFIX_EN : FILE_TRUNCATED_SUFFIX;
|
|
41
|
+
content = buf.subarray(0, MAX_FILE_BYTES).toString("utf8") + suffix;
|
|
33
42
|
}
|
|
34
43
|
return {
|
|
35
44
|
resultText: content,
|
|
@@ -38,7 +47,7 @@ async function executeToolCall(call, spoke, repoRoot) {
|
|
|
38
47
|
}
|
|
39
48
|
catch {
|
|
40
49
|
return {
|
|
41
|
-
resultText:
|
|
50
|
+
resultText: allowlistRejectMessage(spoke.lang),
|
|
42
51
|
log: { path: requestedPath, allowed: false, reason: "not_found", startedAt, durationMs: Date.now() - startedAt },
|
|
43
52
|
};
|
|
44
53
|
}
|
|
@@ -68,19 +77,19 @@ async function sendWithResilience(adapter, conv, sendOpts, cfg, ctx) {
|
|
|
68
77
|
// §8:raw 完整性違反是實作缺陷,不是 API 錯誤——重試只會確定性地再次觸發同一個
|
|
69
78
|
// bug(下一輪的 conv 結構仍帶著同樣的漏洞),不消耗任何網路呼叫也不可能成功。
|
|
70
79
|
// 立即判定失敗,不進入一般失敗的重試計數。
|
|
71
|
-
recordError(err,
|
|
80
|
+
recordError(err, m(ctx.lang, "rawIntegrityCheckFailed", err.message), null);
|
|
72
81
|
return { ok: false, kind: "failed" };
|
|
73
82
|
}
|
|
74
83
|
const info = describeError(err);
|
|
75
84
|
if (info.is429) {
|
|
76
85
|
rateLimitAttempt++;
|
|
77
86
|
if (rateLimitAttempt > cfg.rateLimitRetries) {
|
|
78
|
-
recordError(err,
|
|
87
|
+
recordError(err, m(ctx.lang, "rateLimitRetriesExceeded", cfg.rateLimitRetries), info.status ?? 429, info.errorBody);
|
|
79
88
|
return { ok: false, kind: "rate_limited" };
|
|
80
89
|
}
|
|
81
90
|
const { seconds, source } = parseRetryAfter(info.retryAfterHeader, info.message, rateLimitAttempt - 1);
|
|
82
91
|
if (seconds > cfg.maxRateWaitSec) {
|
|
83
|
-
recordError(err,
|
|
92
|
+
recordError(err, m(ctx.lang, "rateLimitWaitExceeded", seconds, cfg.maxRateWaitSec), info.status ?? 429, info.errorBody);
|
|
84
93
|
return { ok: false, kind: "rate_limited" };
|
|
85
94
|
}
|
|
86
95
|
ctx.rateLimitHits.push({ at: Date.now(), waitSeconds: seconds, source });
|
|
@@ -157,6 +166,7 @@ export async function runSpoke(spoke, adapter, ticketDir, options) {
|
|
|
157
166
|
addWaitedMs: (ms) => {
|
|
158
167
|
waitedMs += ms;
|
|
159
168
|
},
|
|
169
|
+
lang: spoke.lang,
|
|
160
170
|
});
|
|
161
171
|
if (!outcome.ok) {
|
|
162
172
|
const hasContent = rawResponses.length > 0;
|
|
@@ -206,7 +216,7 @@ export async function runSpoke(spoke, adapter, ticketDir, options) {
|
|
|
206
216
|
conv.turns.push(result.turn);
|
|
207
217
|
if (!result.usage.available) {
|
|
208
218
|
status = "truncated:usage_unavailable";
|
|
209
|
-
errors.push(
|
|
219
|
+
errors.push(m(spoke.lang, "usageUnavailableRound", round));
|
|
210
220
|
finalText = result.meta.text ?? finalText;
|
|
211
221
|
if (toolCallsThisRound.length === 0 || finalizeMode)
|
|
212
222
|
break;
|
|
@@ -220,7 +230,7 @@ export async function runSpoke(spoke, adapter, ticketDir, options) {
|
|
|
220
230
|
}
|
|
221
231
|
if (finalizeMode) {
|
|
222
232
|
// 收束呼叫理論上不帶 tool,仍收到 tool call 屬異常,防禦性丟棄不執行
|
|
223
|
-
errors.push(
|
|
233
|
+
errors.push(m(spoke.lang, "finalizeToolCallIgnored", round));
|
|
224
234
|
finalText = result.meta.text;
|
|
225
235
|
break;
|
|
226
236
|
}
|
|
@@ -267,7 +277,8 @@ export async function runSpoke(spoke, adapter, ticketDir, options) {
|
|
|
267
277
|
};
|
|
268
278
|
toolCalls.push(log);
|
|
269
279
|
options.onEvent({ type: "tool_call", agent: spoke.agent, path: log.path, allowed: false, reason: log.reason });
|
|
270
|
-
|
|
280
|
+
const limitMessage = spoke.lang === "en" ? TOOL_LIMIT_REACHED_MESSAGE_EN : TOOL_LIMIT_REACHED_MESSAGE;
|
|
281
|
+
conv.turns.push({ role: "tool", callId: call.id, result: limitMessage });
|
|
271
282
|
continue;
|
|
272
283
|
}
|
|
273
284
|
const { resultText, log } = await executeToolCall(call, spoke, options.repoRoot);
|
package/dist/ticket.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { DispatchError } from "./types.js";
|
|
6
|
+
import { m } from "./messages.js";
|
|
6
7
|
const FORMAT_MARKER = "<!-- format: v1 -->";
|
|
7
8
|
function splitTableRow(line) {
|
|
8
9
|
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
@@ -12,15 +13,15 @@ function isSeparatorRow(cells) {
|
|
|
12
13
|
return cells.length > 0 && cells.every((c) => /^:?-{2,}:?$/.test(c));
|
|
13
14
|
}
|
|
14
15
|
// §4:`_dispatch.md`。model/provider/agent 必填,無預設值;留白或寫 "default" 視為缺失。
|
|
15
|
-
export function parseDispatchTable(markdown) {
|
|
16
|
+
export function parseDispatchTable(markdown, lang) {
|
|
16
17
|
const lines = markdown.split(/\r?\n/);
|
|
17
18
|
const firstNonBlank = lines.find((l) => l.trim().length > 0);
|
|
18
19
|
if (firstNonBlank?.trim() !== FORMAT_MARKER) {
|
|
19
|
-
throw new DispatchError(
|
|
20
|
+
throw new DispatchError(m(lang, "formatMarkerMismatch", FORMAT_MARKER, firstNonBlank?.trim() ?? m(lang, "blankPlaceholder")), 2);
|
|
20
21
|
}
|
|
21
22
|
const headerIndex = lines.findIndex((l) => /^\s*\|\s*agent\s*\|/i.test(l));
|
|
22
23
|
if (headerIndex === -1 || !isSeparatorRow(splitTableRow(lines[headerIndex + 1] ?? ""))) {
|
|
23
|
-
throw new DispatchError(
|
|
24
|
+
throw new DispatchError(m(lang, "dispatchTableMissingHeader"), 2);
|
|
24
25
|
}
|
|
25
26
|
const headerCells = splitTableRow(lines[headerIndex]).map((c) => c.toLowerCase());
|
|
26
27
|
const rows = [];
|
|
@@ -39,15 +40,29 @@ export function parseDispatchTable(markdown) {
|
|
|
39
40
|
const effort = get("effort");
|
|
40
41
|
const isMissing = (v) => v.length === 0 || v.toLowerCase() === "default";
|
|
41
42
|
if (isMissing(agent) || isMissing(provider) || isMissing(model)) {
|
|
42
|
-
throw new DispatchError(
|
|
43
|
+
throw new DispatchError(m(lang, "dispatchRowMissingFields", i + 1, line), 2);
|
|
43
44
|
}
|
|
44
45
|
rows.push({ agent, provider, model, effort: effort.length > 0 ? effort : undefined });
|
|
45
46
|
}
|
|
46
47
|
if (rows.length === 0) {
|
|
47
|
-
throw new DispatchError(
|
|
48
|
+
throw new DispatchError(m(lang, "dispatchTableEmpty"), 2);
|
|
49
|
+
}
|
|
50
|
+
const distinctAgents = new Set(rows.map((r) => r.agent));
|
|
51
|
+
if (distinctAgents.size !== rows.length) {
|
|
52
|
+
const counts = new Map();
|
|
53
|
+
for (const row of rows)
|
|
54
|
+
counts.set(row.agent, (counts.get(row.agent) ?? 0) + 1);
|
|
55
|
+
const [dupAgent, dupCount] = [...counts.entries()].find(([, count]) => count > 1);
|
|
56
|
+
throw new DispatchError(m(lang, "duplicateAgentInDispatchTable", dupAgent, dupCount), 2);
|
|
48
57
|
}
|
|
49
58
|
return rows;
|
|
50
59
|
}
|
|
60
|
+
// plan_i18n_v1.2.md §6.2:「讀檔 → 剝 frontmatter → trim」這個片段跨多處共用(原為
|
|
61
|
+
// validate.ts 私有),放這裡不產生循環——audit.ts 已經 import 本檔。
|
|
62
|
+
export function stripFrontmatter(markdown) {
|
|
63
|
+
const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
|
64
|
+
return (match ? match[1] : markdown).trim();
|
|
65
|
+
}
|
|
51
66
|
// 匯出供 audit.ts 重用(回報模板同樣是 `# 標題` 結構)。
|
|
52
67
|
export function splitTopLevelSections(markdown) {
|
|
53
68
|
const lines = markdown.split(/\r?\n/);
|
|
@@ -80,7 +95,7 @@ function parseBulletList(body) {
|
|
|
80
95
|
.filter((v) => Boolean(v && v.length > 0));
|
|
81
96
|
}
|
|
82
97
|
// §4:`_shared.md`。「待審段落」缺失或空即中止;「前提」缺失為警告(空前提合法)。
|
|
83
|
-
export function parseSharedDoc(markdown) {
|
|
98
|
+
export function parseSharedDoc(markdown, lang) {
|
|
84
99
|
const sections = splitTopLevelSections(markdown);
|
|
85
100
|
const reviewText = sections.get("待審段落") ?? sections.get("Under review");
|
|
86
101
|
if (!reviewText || reviewText.length === 0) {
|
|
@@ -91,37 +106,32 @@ export function parseSharedDoc(markdown) {
|
|
|
91
106
|
if (sections.has("待審段落") || sections.has("Under review")) {
|
|
92
107
|
const stray = [...sections.keys()].filter((k) => k !== "待審段落" && k !== "Under review" && !k.startsWith("前提") && k !== "Premises");
|
|
93
108
|
if (stray.length > 0) {
|
|
94
|
-
throw new DispatchError(
|
|
95
|
-
`${stray.map((s) => `「# ${s}」`).join("、")}。` +
|
|
96
|
-
`工單以 \`#\` 切分區塊,內嵌的規劃書若自帶 \`#\` 標題請降成 \`##\`。` +
|
|
97
|
-
`(注意:在「# 待審段落」下面補一行文字雖然能通過檢查,但規劃書本體仍會留在` +
|
|
98
|
-
`後面那個區塊裡,spoke 收到的待審段落等於是空的。)`, 2);
|
|
109
|
+
throw new DispatchError(m(lang, "strayHeadingsCutReviewSection", stray), 2);
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
|
-
throw new DispatchError(
|
|
112
|
+
throw new DispatchError(m(lang, "missingReviewSection"), 2);
|
|
102
113
|
}
|
|
103
114
|
const premisesBody = sections.get("前提(不受審)") ?? sections.get("前提") ?? sections.get("Premises");
|
|
104
115
|
const premises = premisesBody ? parseBulletList(premisesBody) : [];
|
|
105
116
|
return { premises, reviewText };
|
|
106
117
|
}
|
|
107
118
|
// §4:`<agent>.md`。「具體問題」缺失或空即中止;「允許讀取」缺失為警告(空清單合法)。
|
|
108
|
-
export function parseAgentTicket(markdown) {
|
|
119
|
+
export function parseAgentTicket(markdown, lang) {
|
|
109
120
|
const sections = splitTopLevelSections(markdown);
|
|
110
|
-
//
|
|
111
|
-
//
|
|
121
|
+
// 先中文後英文:命中哪一套只決定用哪一組欄位鍵去讀值——不再代表語言選擇,語言改由
|
|
122
|
+
// run-level `--lang` 決定(見上方型別註解、plan_i18n_v1.3.md §一之5)。
|
|
112
123
|
const zhQuestions = sections.get("具體問題");
|
|
113
124
|
const enQuestions = sections.get("Questions");
|
|
114
|
-
const lang = zhQuestions !== undefined ? "zh" : enQuestions !== undefined ? "en" : "zh";
|
|
115
125
|
const questions = zhQuestions ?? enQuestions;
|
|
116
126
|
if (!questions || questions.length === 0) {
|
|
117
|
-
throw new DispatchError(
|
|
127
|
+
throw new DispatchError(m(lang, "missingQuestionsSection"), 2);
|
|
118
128
|
}
|
|
119
129
|
const allowedBody = sections.get("允許讀取") ?? sections.get("Allowed reads");
|
|
120
130
|
const allowedReads = allowedBody ? parseBulletList(allowedBody) : [];
|
|
121
|
-
return { questions, allowedReads
|
|
131
|
+
return { questions, allowedReads };
|
|
122
132
|
}
|
|
123
133
|
// 檔案系統存取層:讀工單目錄、組出完整 Ticket。
|
|
124
|
-
export async function loadTicket(ticketDir) {
|
|
134
|
+
export async function loadTicket(ticketDir, lang) {
|
|
125
135
|
const dispatchPath = path.join(ticketDir, "_dispatch.md");
|
|
126
136
|
const sharedPath = path.join(ticketDir, "_shared.md");
|
|
127
137
|
let dispatchText;
|
|
@@ -129,17 +139,17 @@ export async function loadTicket(ticketDir) {
|
|
|
129
139
|
dispatchText = await readFile(dispatchPath, "utf8");
|
|
130
140
|
}
|
|
131
141
|
catch {
|
|
132
|
-
throw new DispatchError(
|
|
142
|
+
throw new DispatchError(m(lang, "fileNotFound", dispatchPath), 2);
|
|
133
143
|
}
|
|
134
144
|
let sharedText;
|
|
135
145
|
try {
|
|
136
146
|
sharedText = await readFile(sharedPath, "utf8");
|
|
137
147
|
}
|
|
138
148
|
catch {
|
|
139
|
-
throw new DispatchError(
|
|
149
|
+
throw new DispatchError(m(lang, "fileNotFound", sharedPath), 2);
|
|
140
150
|
}
|
|
141
|
-
const rows = parseDispatchTable(dispatchText);
|
|
142
|
-
const shared = parseSharedDoc(sharedText);
|
|
151
|
+
const rows = parseDispatchTable(dispatchText, lang);
|
|
152
|
+
const shared = parseSharedDoc(sharedText, lang);
|
|
143
153
|
const perAgent = new Map();
|
|
144
154
|
for (const row of rows) {
|
|
145
155
|
const agentPath = path.join(ticketDir, `${row.agent}.md`);
|
|
@@ -148,9 +158,9 @@ export async function loadTicket(ticketDir) {
|
|
|
148
158
|
agentText = await readFile(agentPath, "utf8");
|
|
149
159
|
}
|
|
150
160
|
catch {
|
|
151
|
-
throw new DispatchError(
|
|
161
|
+
throw new DispatchError(m(lang, "agentFileNotFound", agentPath, row.agent), 2);
|
|
152
162
|
}
|
|
153
|
-
perAgent.set(row.agent, parseAgentTicket(agentText));
|
|
163
|
+
perAgent.set(row.agent, parseAgentTicket(agentText, lang));
|
|
154
164
|
}
|
|
155
165
|
return { ticketDir, rows, shared, perAgent };
|
|
156
166
|
}
|
package/dist/validate.js
CHANGED
|
@@ -5,22 +5,20 @@ import fs from "node:fs";
|
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { DispatchError } from "./types.js";
|
|
8
|
+
import { stripFrontmatter } from "./ticket.js";
|
|
8
9
|
import { buildAllowSet } from "./whitelist.js";
|
|
10
|
+
import { m } from "./messages.js";
|
|
9
11
|
function apiKeyEnvFor(provider) {
|
|
10
12
|
return `${provider.toUpperCase()}_API_KEY`;
|
|
11
13
|
}
|
|
12
|
-
function
|
|
13
|
-
const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
|
14
|
-
return (match ? match[1] : markdown).trim();
|
|
15
|
-
}
|
|
16
|
-
async function readAgentBody(agentsDir, agent) {
|
|
14
|
+
async function readAgentBody(agentsDir, agent, lang) {
|
|
17
15
|
const agentDefPath = path.join(agentsDir, `${agent}.md`);
|
|
18
16
|
let text;
|
|
19
17
|
try {
|
|
20
18
|
text = await readFile(agentDefPath, "utf8");
|
|
21
19
|
}
|
|
22
20
|
catch {
|
|
23
|
-
throw new DispatchError(
|
|
21
|
+
throw new DispatchError(m(lang, "agentDefNotFound", agentDefPath, agent), 2);
|
|
24
22
|
}
|
|
25
23
|
return stripFrontmatter(text);
|
|
26
24
|
}
|
|
@@ -28,57 +26,60 @@ function isUnderDocsDir(repoRoot, resolved) {
|
|
|
28
26
|
const docsRoot = path.resolve(repoRoot, "_docs");
|
|
29
27
|
return resolved === docsRoot || resolved.startsWith(docsRoot + path.sep);
|
|
30
28
|
}
|
|
31
|
-
export async function resolveSpokes(ticket, providers, repoRoot,
|
|
29
|
+
export async function resolveSpokes(ticket, providers, repoRoot,
|
|
30
|
+
// plan_i18n_v1.3.md §一之3 第 3 點:必填、不得有預設值——有預設值的話呼叫端漏傳會
|
|
31
|
+
// 靜默回退成舊來源,連紅字都沒有,等同「傳了但傳的是舊來源」那種型別系統看不見的錯誤。
|
|
32
|
+
lang, agentsDir = path.join(repoRoot, ".claude", "agents")) {
|
|
32
33
|
const resolved = [];
|
|
33
34
|
for (const row of ticket.rows) {
|
|
34
35
|
const providerConfig = providers[row.provider];
|
|
35
36
|
if (!providerConfig) {
|
|
36
|
-
throw new DispatchError(
|
|
37
|
+
throw new DispatchError(m(lang, "providerUndefinedInRow", row.agent, row.provider), 2);
|
|
37
38
|
}
|
|
38
39
|
// §18:只檢查本次工單用到的 provider,缺即中止。
|
|
39
40
|
const envName = apiKeyEnvFor(row.provider);
|
|
40
41
|
if (!process.env[envName]) {
|
|
41
|
-
throw new DispatchError(
|
|
42
|
+
throw new DispatchError(m(lang, "missingEnvVar", envName, row.agent, row.provider), 2);
|
|
42
43
|
}
|
|
43
44
|
// §5:model 須在 providers.json 的 models 白名單內;白名單為空 = 不做型號檢查。
|
|
44
45
|
if (providerConfig.models.length > 0 && !providerConfig.models.includes(row.model)) {
|
|
45
|
-
throw new DispatchError(
|
|
46
|
-
`(允許:${providerConfig.models.join(", ")})`, 2);
|
|
46
|
+
throw new DispatchError(m(lang, "modelNotWhitelisted", row.agent, row.model, row.provider, providerConfig.models.join(", ")), 2);
|
|
47
47
|
}
|
|
48
48
|
// §4:effort 填了但不在該 provider 的 allowed 內即中止(含 allowed 為空陣列)。
|
|
49
49
|
if (row.effort !== undefined && !providerConfig.reasoning.allowed.includes(row.effort)) {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const list = providerConfig.reasoning.allowed.length > 0
|
|
51
|
+
? providerConfig.reasoning.allowed.join(", ")
|
|
52
|
+
: m(lang, "emptyAllowedNote");
|
|
53
|
+
throw new DispatchError(m(lang, "effortNotAllowed", row.agent, row.effort, row.provider, list), 2);
|
|
52
54
|
}
|
|
53
55
|
// §5:「留白」不再是「不送任何 reasoning 參數」,而是送 reasoning.default。
|
|
54
56
|
// allowed 為空時 default 不存在——該 provider 不可用,即使 effort 留白也中止。
|
|
55
57
|
const effectiveEffort = row.effort ?? providerConfig.reasoning.default;
|
|
56
58
|
if (effectiveEffort === undefined) {
|
|
57
|
-
throw new DispatchError(
|
|
58
|
-
`(allowed 為空 = 尚未驗證,該 provider 不可用)`, 2);
|
|
59
|
+
throw new DispatchError(m(lang, "effortBlankNoDefault", row.agent, row.provider), 2);
|
|
59
60
|
}
|
|
60
61
|
const agentTicket = ticket.perAgent.get(row.agent);
|
|
61
62
|
if (!agentTicket) {
|
|
62
63
|
// loadTicket() 已保證存在,此處為型別窄化與防禦
|
|
63
|
-
throw new DispatchError(
|
|
64
|
+
throw new DispatchError(m(lang, "internalErrorTicketContentMissing", row.agent), 2);
|
|
64
65
|
}
|
|
65
66
|
// §4「針對 hub 會寫錯」:_docs/ 一律拒絕;允許清單逐一驗證存在,任一不存在即中止並指名。
|
|
66
67
|
const allowedReadsResolved = [];
|
|
67
68
|
for (const rel of agentTicket.allowedReads) {
|
|
68
69
|
const resolvedPath = path.resolve(repoRoot, rel);
|
|
69
70
|
if (isUnderDocsDir(repoRoot, resolvedPath)) {
|
|
70
|
-
throw new DispatchError(
|
|
71
|
+
throw new DispatchError(m(lang, "allowedReadsUnderDocs", row.agent, rel), 2);
|
|
71
72
|
}
|
|
72
73
|
let real;
|
|
73
74
|
try {
|
|
74
75
|
real = fs.realpathSync(resolvedPath);
|
|
75
76
|
}
|
|
76
77
|
catch {
|
|
77
|
-
throw new DispatchError(
|
|
78
|
+
throw new DispatchError(m(lang, "allowedReadsPathNotFound", row.agent, rel), 2);
|
|
78
79
|
}
|
|
79
80
|
allowedReadsResolved.push(real);
|
|
80
81
|
}
|
|
81
|
-
const agentBody = await readAgentBody(agentsDir, row.agent);
|
|
82
|
+
const agentBody = await readAgentBody(agentsDir, row.agent, lang);
|
|
82
83
|
const sharedPath = path.join(ticket.ticketDir, "_shared.md");
|
|
83
84
|
const ownAgentPath = path.join(ticket.ticketDir, `${row.agent}.md`);
|
|
84
85
|
const allowSet = buildAllowSet([sharedPath, ownAgentPath, ...allowedReadsResolved]);
|
|
@@ -93,7 +94,7 @@ export async function resolveSpokes(ticket, providers, repoRoot, agentsDir = pat
|
|
|
93
94
|
allowSet,
|
|
94
95
|
allowedReadsResolved,
|
|
95
96
|
allowedReadsRelative: agentTicket.allowedReads,
|
|
96
|
-
lang
|
|
97
|
+
lang,
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
return resolved;
|
package/dist/whitelist.js
CHANGED
|
@@ -35,4 +35,12 @@ export function checkAllowlist(allowSet, requestedPath, repoRoot) {
|
|
|
35
35
|
return { allowed: false, reason: insideRepo ? "not_in_allowlist" : "outside_repo" };
|
|
36
36
|
}
|
|
37
37
|
// 統一回給 spoke 的訊息:不區分「不存在」與「不在允許範圍」。
|
|
38
|
-
|
|
38
|
+
//
|
|
39
|
+
// i18n_classification_t2.md §三之2:這則訊息經 executeToolCall 當 resultText 送回對話,
|
|
40
|
+
// 消費者是 spoke 不是人類——歸 C 類,不進 messages.ts,由 spoke.lang 直接選用兩套並存的
|
|
41
|
+
// 雙語常數(同 runner.ts 本地的 200KB 截斷/--max-tool-calls 上限兩則走同一模式)。
|
|
42
|
+
const ALLOWLIST_REJECT_MESSAGE = "不存在或不在允許範圍";
|
|
43
|
+
const ALLOWLIST_REJECT_MESSAGE_EN = "Not found or outside the allowed list";
|
|
44
|
+
export function allowlistRejectMessage(lang) {
|
|
45
|
+
return lang === "en" ? ALLOWLIST_REJECT_MESSAGE_EN : ALLOWLIST_REJECT_MESSAGE;
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dowafu",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Send a section of your design doc to external LLMs for review. They read only the files you whitelist, and nothing is billed until you confirm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
35
35
|
"providers.json",
|
|
36
|
-
"publish"
|
|
36
|
+
"publish",
|
|
37
|
+
"README_zh-tw.md"
|
|
37
38
|
],
|
|
38
39
|
"scripts": {
|
|
39
40
|
"verify:providers": "tsx scripts/verify-providers.ts",
|
package/providers.json
CHANGED
|
@@ -31,13 +31,18 @@
|
|
|
31
31
|
"allowed": ["low", "high", "max"],
|
|
32
32
|
"default": "high"
|
|
33
33
|
},
|
|
34
|
-
"models": ["deepseek-v4-flash"],
|
|
34
|
+
"models": ["deepseek-v4-flash", "deepseek-v4-pro"],
|
|
35
35
|
"charsPerToken": 1.0,
|
|
36
36
|
"tpmLimit": null,
|
|
37
37
|
"maxSpokeTokens": null,
|
|
38
|
-
"pricingSource": {
|
|
38
|
+
"pricingSource": {
|
|
39
|
+
"url": "https://api-docs.deepseek.com/quick_start/pricing/",
|
|
40
|
+
"asOf": "2026-08-14",
|
|
41
|
+
"note": "本表填的是 2026-08-16 16:00 UTC 起生效的尖峰價。在此之前官方仍收舊價(flash 0.0028/0.14/0.28、pro 0.003625/0.435/0.87),所以生效前本表偏高。離峰(尖峰時段 01:00-04:00 與 06:00-10:00 UTC 以外)為尖峰的一半;本檔一個型號只放得下一組價,取尖峰=取上限。"
|
|
42
|
+
},
|
|
39
43
|
"pricing": {
|
|
40
|
-
"deepseek-v4-flash": { "inputPerM": 0.
|
|
44
|
+
"deepseek-v4-flash": { "inputPerM": 0.44, "cachedInputPerM": 0.014, "outputPerM": 1.32 },
|
|
45
|
+
"deepseek-v4-pro": { "inputPerM": 1.32, "cachedInputPerM": 0.044, "outputPerM": 3.96 }
|
|
41
46
|
}
|
|
42
47
|
},
|
|
43
48
|
"gemini": {
|