pi-firecode 0.8.1 → 1.0.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.
Files changed (79) hide show
  1. package/README.md +58 -24
  2. package/README.zh-CN.md +74 -0
  3. package/config.example.jsonc +47 -57
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11464 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/dist/master/prompts/master.zh.md +22 -0
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/master/prompts/worker.zh.md +1 -0
  10. package/{review → dist/review}/prompts/review.en.md +7 -6
  11. package/{review → dist/review}/prompts/review.zh.md +7 -6
  12. package/dist/watcher/prompts/watch.en.md +43 -0
  13. package/{watcher → dist/watcher}/prompts/watch.zh.md +1 -1
  14. package/package.json +10 -27
  15. package/config.ts +0 -474
  16. package/deliver.ts +0 -32
  17. package/flame-frames.ts +0 -460
  18. package/format.ts +0 -80
  19. package/header.ts +0 -92
  20. package/herdr-client.ts +0 -60
  21. package/index.ts +0 -62
  22. package/jsonc.ts +0 -34
  23. package/master/event-card.ts +0 -106
  24. package/master/event-format.ts +0 -52
  25. package/master/index.ts +0 -1200
  26. package/master/prompt.ts +0 -26
  27. package/master/prompts/master.zh.md +0 -17
  28. package/master/prompts/worker.zh.md +0 -1
  29. package/master/role.ts +0 -13
  30. package/master/spawn.ts +0 -193
  31. package/master/state.ts +0 -201
  32. package/provider/claude-sub.ts +0 -129
  33. package/provider/openai-native/index.ts +0 -8
  34. package/provider/openai-native/src/compact-client.ts +0 -362
  35. package/provider/openai-native/src/config.ts +0 -297
  36. package/provider/openai-native/src/extension.ts +0 -93
  37. package/provider/openai-native/src/native-compaction.ts +0 -151
  38. package/provider/openai-native/src/native-details.ts +0 -157
  39. package/provider/openai-native/src/native-replay.ts +0 -253
  40. package/provider/openai-native/src/native-runtime.ts +0 -144
  41. package/provider/openai-native/src/options.ts +0 -85
  42. package/provider/openai-native/src/request-pipeline.ts +0 -21
  43. package/provider/openai-native/src/responses-input.ts +0 -433
  44. package/review/advisor.ts +0 -112
  45. package/review/card.ts +0 -385
  46. package/review/checkpoint.ts +0 -384
  47. package/review/evidence.ts +0 -232
  48. package/review/index.ts +0 -1246
  49. package/review/outcome.ts +0 -74
  50. package/review/progress.ts +0 -324
  51. package/review/prompt.ts +0 -210
  52. package/review/reviewer.ts +0 -356
  53. package/review/session.ts +0 -129
  54. package/review/state.ts +0 -905
  55. package/review/ui.ts +0 -528
  56. package/session/bark.ts +0 -157
  57. package/session/herdr-display.ts +0 -87
  58. package/session/presets.ts +0 -257
  59. package/session/rename.ts +0 -46
  60. package/session/stats.ts +0 -310
  61. package/session/working-flame.ts +0 -116
  62. package/statusbar/index.ts +0 -118
  63. package/statusbar/quota-cache.ts +0 -60
  64. package/statusbar/quota-parse.ts +0 -85
  65. package/statusbar/quota.ts +0 -203
  66. package/statusbar/render.ts +0 -181
  67. package/statusbar/tps.ts +0 -87
  68. package/theme.ts +0 -84
  69. package/tools/grouping.ts +0 -233
  70. package/tools/index.ts +0 -183
  71. package/tools/line.ts +0 -194
  72. package/tools/parts.ts +0 -143
  73. package/tools/timing.ts +0 -22
  74. package/watcher/card.ts +0 -85
  75. package/watcher/index.ts +0 -204
  76. package/watcher/observer.ts +0 -75
  77. package/watcher/transcript.ts +0 -64
  78. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  79. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
package/review/advisor.ts DELETED
@@ -1,112 +0,0 @@
1
- /** 顾问仲裁:连续 N 轮失败后经独立进程内会话返回三选一裁决。 */
2
- import type { Language } from "../config.js";
3
- import type { PromptLayers } from "./prompt.js";
4
- import type { AdvisorResult, AdvisorVerdict } from "./state.js";
5
- import type { ReviewSessionRunner } from "./session.js";
6
- import type { ReviewModelConfig } from "./reviewer.js";
7
-
8
- export interface RunAdvisorOptions {
9
- config: ReviewModelConfig;
10
- prompt: PromptLayers;
11
- cwd: string;
12
- language: Language;
13
- signal?: AbortSignal;
14
- runSession: ReviewSessionRunner;
15
- onEvent?: (event: Record<string, unknown>) => void;
16
- }
17
-
18
- export async function runAdvisor(options: RunAdvisorOptions): Promise<AdvisorResult> {
19
- const result = await options.runSession({
20
- role: "advisor",
21
- model: options.config.model,
22
- thinking: options.config.thinking,
23
- tools: options.config.tools,
24
- prompt: options.prompt,
25
- cwd: options.cwd,
26
- timeoutMs: options.config.timeoutMs,
27
- signal: options.signal,
28
- onEvent: options.onEvent,
29
- });
30
- if (result.kind !== "output") throw new Error(advisorProcessError(result, options.language));
31
- return parseAdvisorOutput(result.text, options.language);
32
- }
33
-
34
- const VERDICTS = new Set<AdvisorVerdict>(["continue", "stop", "narrow"]);
35
-
36
- function advisorProcessError(
37
- result: Exclude<Awaited<ReturnType<ReviewSessionRunner>>, { kind: "output" }>,
38
- language: Language,
39
- ): string {
40
- const prefix = language === "en" ? "advisor session unavailable" : "顾问会话不可用";
41
- if (result.kind === "aborted") return `${prefix}: aborted`;
42
- if (result.kind === "timeout") return `${prefix}: timeout`;
43
- if (result.kind === "empty") return `${prefix}: empty output`;
44
- return `${prefix}: ${result.message}`;
45
- }
46
-
47
- /** 首行应为裸裁决词;容忍模型前言,在前几个非空行内识别裁决行,
48
- * 其余行(含前言)并入 advice;完全识别不出才回落 continue。 */
49
- const VERDICT_SCAN_LINES = 8;
50
-
51
- export function parseAdvisorOutput(
52
- text: string,
53
- language: Language,
54
- ): AdvisorResult {
55
- const lines = unwrapCodeFence(text.trim().split(/\r?\n/));
56
- const firstLine = lines.find((line) => line.trim()) ?? "";
57
- let verdict: AdvisorVerdict | undefined;
58
- let verdictIndex = -1;
59
- let scanned = 0;
60
- for (let index = 0; index < lines.length && scanned < VERDICT_SCAN_LINES; index += 1) {
61
- if (!lines[index].trim()) continue;
62
- scanned += 1;
63
- const parsed = normalizeVerdict(lines[index]);
64
- if (parsed) {
65
- verdict = parsed;
66
- verdictIndex = index;
67
- break;
68
- }
69
- }
70
- const advice = lines
71
- .filter((_, index) => index !== verdictIndex)
72
- .join("\n")
73
- .trim();
74
- if (!verdict) {
75
- // 把首行原文带回去:顾问会话跑完即释放、原始输出不落盘,这是事后诊断解析失败的唯一线索。
76
- const sample = firstLine.slice(0, 80);
77
- return {
78
- verdict: "continue",
79
- advice:
80
- language === "en"
81
- ? `Advisor output was not parseable (first line: ${sample}); continuing with the current findings.`
82
- : `顾问输出无法解析(首行:${sample}),按继续处理当前发现。`,
83
- };
84
- }
85
- return {
86
- verdict,
87
- advice: advice || (language === "en" ? "(no advice)" : "(无建议)"),
88
- };
89
- }
90
-
91
- function unwrapCodeFence(lines: string[]) {
92
- const fenced =
93
- lines.length >= 2 &&
94
- /^```\w*\s*$/u.test(lines[0].trim()) &&
95
- /^```\s*$/u.test(lines.at(-1)?.trim() ?? "");
96
- return fenced ? lines.slice(1, -1) : lines;
97
- }
98
-
99
- function normalizeVerdict(line: string): AdvisorVerdict | undefined {
100
- const normalized = line
101
- .trim()
102
- .replace(/^\*{1,2}(.+?)\*{1,2}$/u, "$1")
103
- .replace(/^#{1,6}\s*/u, "")
104
- .replace(/^(?:(?:verdict|裁决|结论)\s*[::]\s*)/iu, "")
105
- // 提示词里裁决词本身带反引号展示,模型照抄 `continue` 很常见,剔掉包裹的反引号。
106
- .replace(/^`+(.+?)`+$/u, "$1")
107
- .trim()
108
- .toLowerCase();
109
- return VERDICTS.has(normalized as AdvisorVerdict)
110
- ? (normalized as AdvisorVerdict)
111
- : undefined;
112
- }
package/review/card.ts DELETED
@@ -1,385 +0,0 @@
1
- /**
2
- * 结果卡:渲染器 + payload 校验 + 各状态卡构建。
3
- *
4
- * 渲染器在 registerReview 顶层无条件注册(不懒加载、不挂 session_start),
5
- * 因此 live 与 reload 走同一个纯渲染路径,外观只有一种。渲染器永不抛异常:
6
- * details 校验不过就降级渲染 content 纯文本(pi 对抛异常的渲染器会静默回落默认框,
7
- * 与未注册表现相同,必须从源头避免)。
8
- *
9
- * payload 校验零外部依赖:纯函数一次性整体校验,不做字段级兼容。
10
- */
11
- import { getMarkdownTheme, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
12
- import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
13
- import type { Language } from "../config.js";
14
- import { formatDuration } from "../format.js";
15
- import type { CardData, StopReason } from "./state.js";
16
-
17
- export const CARD_TYPE = "firecode-review-card";
18
- const VERSION = 1;
19
-
20
- const CARD_KINDS = new Set([
21
- "queued",
22
- "start",
23
- "pass",
24
- "fail",
25
- "stop",
26
- "cancel",
27
- "timeout",
28
- "error",
29
- "advisor",
30
- ]);
31
- const CARD_TONES = new Set(["success", "warning", "error", "neutral", "accent"]);
32
-
33
- export type CardDetails = {
34
- version: typeof VERSION;
35
- kind: CardData["kind"];
36
- title: string;
37
- lines: string[];
38
- tone: "success" | "warning" | "error" | "neutral" | "accent";
39
- icon: string;
40
- };
41
-
42
- /** 一次性整体校验结果卡 payload;结构不符返回 false(渲染器降级 content 纯文本)。 */
43
- export function isValidCardDetails(value: unknown): value is CardDetails {
44
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
45
- const record = value as Record<string, unknown>;
46
- if (Object.keys(record).length !== 6) return false;
47
- if (record.version !== VERSION) return false;
48
- if (typeof record.kind !== "string" || !CARD_KINDS.has(record.kind)) return false;
49
- if (typeof record.title !== "string") return false;
50
- if (!Array.isArray(record.lines) || !record.lines.every((line) => typeof line === "string"))
51
- return false;
52
- if (typeof record.tone !== "string" || !CARD_TONES.has(record.tone)) return false;
53
- return typeof record.icon === "string";
54
- }
55
-
56
- export interface BuiltCard {
57
- content: string;
58
- details: CardDetails;
59
- }
60
-
61
- export function registerCardRenderer(pi: ExtensionAPI): void {
62
- pi.registerMessageRenderer<CardDetails>(
63
- CARD_TYPE,
64
- (message, _options, theme) => new ReviewCard(message.details, message.content, theme),
65
- );
66
- }
67
-
68
- class ReviewCard implements Component {
69
- private readonly card: Component | undefined;
70
- private readonly fallback: Component;
71
-
72
- constructor(details: CardDetails | undefined, content: string | (string | unknown)[], theme: Theme) {
73
- this.fallback = new Text(plainContent(content), 0, 0);
74
- let card: Component | undefined;
75
- try {
76
- card = isValidCardDetails(details) ? nativeCard(details, theme) : undefined;
77
- } catch {
78
- card = undefined;
79
- }
80
- this.card = card;
81
- }
82
-
83
- render(width: number): string[] {
84
- try {
85
- return (this.card ?? this.fallback).render(Math.max(1, width));
86
- } catch {
87
- try {
88
- return this.fallback.render(Math.max(1, width));
89
- } catch {
90
- return [];
91
- }
92
- }
93
- }
94
-
95
- invalidate(): void {
96
- this.card?.invalidate?.();
97
- this.fallback.invalidate?.();
98
- }
99
- }
100
-
101
- function nativeCard(details: CardDetails, theme: Theme): Component {
102
- // 全家卡统一:无垂直内边距;消息间距由宿主 CustomMessageComponent 提供,不再叠加。
103
- const box = new Box(1, 0, (text) => theme.bg(backgroundFor(details.tone), text));
104
- box.addChild(new Text(`${details.icon} ${details.title}`, 0, 0));
105
- box.addChild(new Spacer(1));
106
- box.addChild(new Markdown(details.lines.join("\n"), 0, 0, getMarkdownTheme()));
107
- return box;
108
- }
109
-
110
- function backgroundFor(tone: CardDetails["tone"]) {
111
- if (tone === "success") return "toolSuccessBg" as const;
112
- if (tone === "warning" || tone === "error") return "toolErrorBg" as const;
113
- return "customMessageBg" as const;
114
- }
115
-
116
- function plainContent(content: string | (string | unknown)[]): string {
117
- if (typeof content === "string") return content;
118
- if (!Array.isArray(content)) return "";
119
- return content.map(plainPart).filter(Boolean).join("\n");
120
- }
121
-
122
- function plainPart(part: unknown): string {
123
- return typeof part === "object" && part !== null && "text" in part
124
- ? String((part as { text: unknown }).text)
125
- : "";
126
- }
127
-
128
- // ---- 卡构建:content 给 LLM(纯文本事实),details 给渲染(本地化成品行)----
129
-
130
- export function buildCard(card: CardData, language: Language): BuiltCard {
131
- switch (card.kind) {
132
- case "start":
133
- return started(card, language);
134
- case "pass":
135
- return passed(card, language);
136
- case "fail":
137
- return failed(card, language);
138
- case "stop":
139
- return stopped(card, language);
140
- case "cancel":
141
- return cancelled(card, language);
142
- case "timeout":
143
- return timedOut(card, language);
144
- case "error":
145
- return errored(card, language);
146
- case "advisor":
147
- return advisorCard(card, language);
148
- }
149
- }
150
-
151
- // "queued" 仍留在 CARD_KINDS:旧会话的排队卡 reload 时要能继续按卡渲染,只是不再新发。
152
-
153
- function started(card: Extract<CardData, { kind: "start" }>, language: Language): BuiltCard {
154
- const title = language === "en" ? "Review started" : "审查开始";
155
- const lines = [
156
- language === "en"
157
- ? `Models: ${card.models.map(shortModel).join(", ")}`
158
- : `模型:${card.models.map(shortModel).join("、")}`,
159
- ];
160
- return spec(language, "start", title, lines, "neutral", "🔥");
161
- }
162
-
163
- function shortModel(model: string) {
164
- return model.split("/").at(-1) || model;
165
- }
166
-
167
- function passed(card: Extract<CardData, { kind: "pass" }>, language: Language): BuiltCard {
168
- const title = qualityTitle(card.round, language === "en" ? "Review passed" : "审查通过", language);
169
- const lines = withFooter(formatReviewResultLines(card.summary), [
170
- elapsedLine(card.elapsedMs, card.totalElapsedMs, card.round > 1, language),
171
- ]);
172
- return spec(language, "pass", title, lines, "success", "✅");
173
- }
174
-
175
- function failed(card: Extract<CardData, { kind: "fail" }>, language: Language): BuiltCard {
176
- const title = qualityTitle(card.round, language === "en" ? "Review failed" : "审查未通过", language);
177
- const footer = [
178
- ...(card.advisor?.advice
179
- ? [language === "en" ? "Advisor note" : "顾问建议", ...adviceLines(card.advisor.advice)]
180
- : []),
181
- ...(card.elapsedMs === undefined
182
- ? []
183
- : [elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)]),
184
- ];
185
- return spec(
186
- language,
187
- "fail",
188
- title,
189
- withFooter(formatReviewResultLines(card.details), footer),
190
- "warning",
191
- "❌",
192
- );
193
- }
194
-
195
- function stopped(card: Extract<CardData, { kind: "stop" }>, language: Language): BuiltCard {
196
- const footer = card.elapsedMs === undefined
197
- ? []
198
- : [elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)];
199
- if (card.reason === "advisor") {
200
- const title = qualityTitle(
201
- card.round,
202
- language === "en" ? "Review stopped by advisor" : "审查已由顾问终止",
203
- language,
204
- );
205
- const body = [advisorModelLine(card.advisorModel, language), "", ...adviceLines(card.advisor.advice)];
206
- return spec(language, "stop", title, withFooter(body, footer), "warning", "❌");
207
- }
208
- const title = qualityTitle(card.round, language === "en" ? "Review failed" : "审查未通过", language);
209
- const body = formatReviewResultLines(card.details || stopReason(card.reason, language));
210
- return spec(language, "stop", title, withFooter(body, footer), "warning", "❌");
211
- }
212
-
213
- function cancelled(card: Extract<CardData, { kind: "cancel" }>, language: Language): BuiltCard {
214
- const title = language === "en" ? "Review cancelled" : "审查已取消";
215
- return spec(language, "cancel", title, [reasonText(card.reason, language)], "neutral", "⏸");
216
- }
217
-
218
- function timedOut(_card: Extract<CardData, { kind: "timeout" }>, language: Language): BuiltCard {
219
- const title = language === "en" ? "Review incomplete" : "审查未完成";
220
- const lines = [
221
- language === "en" ? "Blocker: review timed out" : "卡点:审查超时",
222
- language === "en" ? "Reason: overall time limit exceeded" : "原因:超过总体时限",
223
- ];
224
- return spec(language, "timeout", title, lines, "warning", "🛑");
225
- }
226
-
227
- /** 终止原因的展示文案(reducer 只出枚举,这里本地化)。 */
228
- function reasonText(reason: StopReason, language: Language) {
229
- if (reason === "user")
230
- return language === "en" ? "Stopped by user" : "已按你的操作停止";
231
- if (reason === "shutdown")
232
- return language === "en" ? "Stopped when the session closed" : "会话关闭时停止";
233
- return language === "en" ? "Stopped" : "已停止";
234
- }
235
-
236
- function stopReason(reason: StopReason, language: Language) {
237
- if (reason === "advisor")
238
- return language === "en" ? "Advisor recommends stopping" : "顾问建议停止";
239
- if (reason === "max_rounds")
240
- return language === "en" ? "Maximum review rounds reached" : "已达到最大审查轮数";
241
- return reasonText(reason, language);
242
- }
243
-
244
- function errored(card: Extract<CardData, { kind: "error" }>, language: Language): BuiltCard {
245
- const title = language === "en" ? "Review incomplete" : "审查未完成";
246
- const lines = [
247
- language === "en" ? "Blocker: review did not complete" : "卡点:审查未完成",
248
- language === "en" ? `Reason: ${card.message}` : `原因:${card.message}`,
249
- ...(card.elapsedMs === undefined
250
- ? []
251
- : ["", elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)]),
252
- ];
253
- return spec(language, "error", title, lines, "warning", "🛑");
254
- }
255
-
256
- /** 顾问卡与审查结果卡同构:裁决进标题,正文用粗体模型分节行开头。 */
257
- function advisorCard(card: Extract<CardData, { kind: "advisor" }>, language: Language): BuiltCard {
258
- const decision = decisionText(card.advisor.verdict, language);
259
- const title = language === "en" ? `Advisor guidance · ${decision}` : `顾问指引 · ${decision}`;
260
- const body = [advisorModelLine(card.advisorModel, language), "", ...adviceLines(card.advisor.advice)];
261
- const footer = card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs, undefined, false, language)];
262
- return spec(language, "advisor", title, withFooter(body, footer), "neutral", "🧭");
263
- }
264
-
265
- /** 裁决词→人话文案的唯一映射:卡标题与活动条摘要共用,防两处文案漂移。 */
266
- export function decisionText(verdict: "continue" | "narrow" | "stop", language: Language) {
267
- return language === "en"
268
- ? { continue: "Continue fixing", narrow: "Narrow scope", stop: "Stop fixing" }[verdict]
269
- : { continue: "继续修复", narrow: "收窄范围", stop: "停止修复" }[verdict];
270
- }
271
-
272
- /** 与审查结果卡的「**模型 N · xxx**」分节行同款式。 */
273
- function advisorModelLine(model: string, language: Language) {
274
- return `**${language === "en" ? "Model" : "模型"} · ${shortModel(model)}**`;
275
- }
276
-
277
- /** 顾问建议排版:粗体段标题前补空行——Markdown 把单换行折进同段,不补行三段会糊成一块。 */
278
- function adviceLines(advice: string) {
279
- const output: string[] = [];
280
- for (const line of advice.split(/\r?\n/u)) {
281
- if (/^\*\*[^*]+\*\*\s*[::]/u.test(line.trim()) && output.length > 0 && output.at(-1) !== "")
282
- output.push("");
283
- output.push(line);
284
- }
285
- return output;
286
- }
287
-
288
- function qualityTitle(round: number, title: string, language: Language) {
289
- if (round <= 1) return title;
290
- return language === "en" ? `Round ${round} ${title}` : `第 ${round} 轮${title}`;
291
- }
292
-
293
- function withFooter(lines: string[], footer: string[]) {
294
- if (footer.length === 0) return lines;
295
- return [...lines, ...(lines.length > 0 ? ["", "---", ""] : []), ...footer];
296
- }
297
-
298
- function elapsedLine(
299
- ms: number,
300
- totalMs: number | undefined,
301
- showTotal: boolean,
302
- language: Language,
303
- ) {
304
- const elapsed = showTotal && totalMs !== undefined
305
- ? `${formatDuration(ms)} / ${language === "en" ? "total" : "总"} ${formatDuration(totalMs)}`
306
- : formatDuration(ms);
307
- return language === "en" ? `⏱ Elapsed: ${elapsed}` : `⏱ 用时:${elapsed}`;
308
- }
309
-
310
- function formatReviewResultLines(review: string) {
311
- const lines = review.split(/\r?\n/u);
312
- const sections: { title: string; body: string[] }[] = [];
313
- const preface: string[] = [];
314
- let current: { title: string; body: string[] } | undefined;
315
- for (const line of lines) {
316
- if (/^(?:模型|Model)\s+\d+\s+·\s+/iu.test(line.trim())) {
317
- if (current) sections.push(current);
318
- current = { title: line.trim(), body: [] };
319
- } else if (current) current.body.push(line);
320
- else preface.push(line);
321
- }
322
- if (current) sections.push(current);
323
- if (sections.length === 0) return normalizedReviewLines(review);
324
- return [
325
- ...normalizedReviewLines(preface.join("\n")),
326
- ...(preface.join("").trim() ? [""] : []),
327
- ...sections.flatMap((section, index) => [
328
- ...(index > 0 ? ["", "---", ""] : []),
329
- `**${section.title}**`,
330
- "",
331
- ...normalizedReviewLines(section.body.join("\n")),
332
- ]),
333
- ];
334
- }
335
-
336
- function normalizedReviewLines(review: string) {
337
- const lines = review
338
- .split(/\r?\n/u)
339
- .map((line) => line.trimEnd())
340
- .filter((line) => !REDUNDANT_REVIEW_LINES.has(line.trim()));
341
- const output: string[] = [];
342
- for (const line of lines) {
343
- if (line.trim() === "" && (output.length === 0 || output.at(-1) === "")) continue;
344
- // 证据/验证命令是取证区:与上面的结论区空一行分隔,密集长行不再糊成一片。
345
- if (
346
- /^[-*+]?\s*(?:\*\*)?(?:证据|Evidence)(?:\*\*)?\s*[::]/u.test(line.trim()) &&
347
- output.length > 0 &&
348
- output.at(-1) !== ""
349
- ) output.push("");
350
- output.push(line.trim() === "" ? "" : line);
351
- }
352
- while (output.at(-1) === "") output.pop();
353
- return output;
354
- }
355
-
356
- const REDUNDANT_REVIEW_LINES = new Set([
357
- "PASS",
358
- "FAIL",
359
- "通过",
360
- "未通过",
361
- "审查通过",
362
- "审查未通过",
363
- "Review passed",
364
- "Review failed",
365
- ]);
366
-
367
- function spec(
368
- language: Language,
369
- kind: CardDetails["kind"],
370
- title: string,
371
- lines: string[],
372
- tone: CardDetails["tone"],
373
- icon: string,
374
- ): BuiltCard {
375
- const localized = localize(lines, language);
376
- return {
377
- content: `${title}\n${lines.join("\n")}`,
378
- details: { version: VERSION, kind, title, lines: localized, tone, icon },
379
- };
380
- }
381
-
382
- /** details 行已本地化成品;content 里除标题外都是事实,不做二次翻译。 */
383
- function localize(lines: string[], language: Language) {
384
- return lines;
385
- }