pi-firecode 0.8.1 → 1.0.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 +35 -15
- package/activity.ts +92 -0
- package/busy.ts +188 -0
- package/config.example.jsonc +38 -47
- package/config.ts +198 -172
- package/deliver.ts +100 -13
- package/flame.ts +149 -0
- package/format.ts +84 -31
- package/header.ts +208 -82
- package/index.ts +17 -11
- package/master/actions.ts +293 -0
- package/master/activity-list.ts +281 -0
- package/master/event-card.ts +18 -37
- package/master/event-format.ts +71 -40
- package/master/guard.ts +46 -0
- package/master/index.ts +88 -1036
- package/master/list-view.ts +129 -0
- package/master/outbox.ts +188 -0
- package/master/prompts/master.zh.md +17 -12
- package/master/prompts/worker.zh.md +1 -1
- package/master/role.ts +6 -1
- package/master/run.ts +315 -0
- package/master/runtime.ts +259 -0
- package/master/spawn.ts +79 -38
- package/master/state.ts +30 -6
- package/master/worker-view.ts +517 -0
- package/package.json +1 -1
- package/provider/claude-sub.ts +35 -13
- package/provider/openai-native/src/compact-client.ts +2 -14
- package/provider/openai-native/src/options.ts +4 -2
- package/review/advisor.ts +0 -2
- package/review/card.ts +36 -23
- package/review/checkpoint.ts +2 -8
- package/review/evidence.ts +42 -44
- package/review/index.ts +271 -525
- package/review/occupancy.ts +25 -0
- package/review/outcome.ts +31 -7
- package/review/prompt.ts +7 -8
- package/review/prompts/review.en.md +7 -6
- package/review/prompts/review.zh.md +7 -6
- package/review/reviewer.ts +2 -5
- package/review/session.ts +11 -30
- package/review/state.ts +32 -60
- package/review/ui.ts +30 -469
- package/round-recorder.ts +18 -0
- package/session/herdr-display.ts +1 -1
- package/session/presets.ts +130 -87
- package/session/quota.ts +123 -0
- package/session/stats.ts +4 -1
- package/statusbar/index.ts +242 -104
- package/statusbar/render.ts +95 -163
- package/theme.ts +2 -41
- package/tools/actions.ts +53 -0
- package/tools/assistant-view.ts +64 -0
- package/tools/click-anchor.ts +56 -0
- package/tools/group-view.ts +420 -0
- package/tools/grouping.ts +146 -221
- package/tools/host.ts +369 -0
- package/tools/index.ts +108 -105
- package/tools/line.ts +95 -58
- package/tools/machine.ts +78 -0
- package/tools/parts.ts +2 -2
- package/tools/round.ts +95 -0
- package/tools/turn-clock.ts +41 -0
- package/tools/turn-summary.ts +109 -0
- package/truncated-write.ts +18 -0
- package/watcher/card.ts +28 -22
- package/watcher/index.ts +16 -44
- package/watcher/observer.ts +3 -3
- package/watcher/prompts/watch.zh.md +1 -1
- package/watcher/transcript.ts +4 -12
- package/flame-frames.ts +0 -460
- package/review/progress.ts +0 -324
- package/session/bark.ts +0 -157
- package/session/working-flame.ts +0 -116
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/tps.ts +0 -87
package/review/card.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { getMarkdownTheme, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
13
13
|
import type { Language } from "../config.js";
|
|
14
|
+
import { HEAT_COLORS, paint } from "../flame.js";
|
|
14
15
|
import { formatDuration } from "../format.js";
|
|
15
16
|
import type { CardData, StopReason } from "./state.js";
|
|
16
17
|
|
|
@@ -30,6 +31,28 @@ const CARD_KINDS = new Set([
|
|
|
30
31
|
]);
|
|
31
32
|
const CARD_TONES = new Set(["success", "warning", "error", "neutral", "accent"]);
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 卡标题前的单色字形与它的语义色,按卡种类一处定义:构建时写进 details.icon,渲染时按种类上色。
|
|
36
|
+
* 字形与全局体系一致(✓ ✗ ◌ ‖ 与审查的盲文点阵),颜色只表达结论:通过绿、失败与未完成红、取消灰,
|
|
37
|
+
* 审查过程中的中性卡(开始、顾问指引)用审查的金色——品牌火焰不用于中性状态。
|
|
38
|
+
*/
|
|
39
|
+
const MARKS = {
|
|
40
|
+
start: { glyph: "⠿", color: "review" },
|
|
41
|
+
advisor: { glyph: "⠿", color: "review" },
|
|
42
|
+
pass: { glyph: "✓", color: "success" },
|
|
43
|
+
fail: { glyph: "✗", color: "error" },
|
|
44
|
+
stop: { glyph: "✗", color: "error" },
|
|
45
|
+
timeout: { glyph: "◌", color: "error" },
|
|
46
|
+
error: { glyph: "◌", color: "error" },
|
|
47
|
+
cancel: { glyph: "‖", color: "muted" },
|
|
48
|
+
} as const satisfies Record<CardData["kind"], { glyph: string; color: "review" | "success" | "error" | "muted" }>;
|
|
49
|
+
|
|
50
|
+
function paintMark(details: CardDetails, theme: Theme): string {
|
|
51
|
+
const mark = MARKS[details.kind as CardData["kind"]];
|
|
52
|
+
if (!mark) return details.icon;
|
|
53
|
+
return mark.color === "review" ? paint(HEAT_COLORS.gold, details.icon) : theme.fg(mark.color, details.icon);
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
export type CardDetails = {
|
|
34
57
|
version: typeof VERSION;
|
|
35
58
|
kind: CardData["kind"];
|
|
@@ -101,7 +124,7 @@ class ReviewCard implements Component {
|
|
|
101
124
|
function nativeCard(details: CardDetails, theme: Theme): Component {
|
|
102
125
|
// 全家卡统一:无垂直内边距;消息间距由宿主 CustomMessageComponent 提供,不再叠加。
|
|
103
126
|
const box = new Box(1, 0, (text) => theme.bg(backgroundFor(details.tone), text));
|
|
104
|
-
box.addChild(new Text(`${details
|
|
127
|
+
box.addChild(new Text(`${paintMark(details, theme)} ${details.title}`, 0, 0));
|
|
105
128
|
box.addChild(new Spacer(1));
|
|
106
129
|
box.addChild(new Markdown(details.lines.join("\n"), 0, 0, getMarkdownTheme()));
|
|
107
130
|
return box;
|
|
@@ -157,7 +180,7 @@ function started(card: Extract<CardData, { kind: "start" }>, language: Language)
|
|
|
157
180
|
? `Models: ${card.models.map(shortModel).join(", ")}`
|
|
158
181
|
: `模型:${card.models.map(shortModel).join("、")}`,
|
|
159
182
|
];
|
|
160
|
-
return spec(
|
|
183
|
+
return spec("start", title, lines, "neutral");
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
function shortModel(model: string) {
|
|
@@ -169,7 +192,7 @@ function passed(card: Extract<CardData, { kind: "pass" }>, language: Language):
|
|
|
169
192
|
const lines = withFooter(formatReviewResultLines(card.summary), [
|
|
170
193
|
elapsedLine(card.elapsedMs, card.totalElapsedMs, card.round > 1, language),
|
|
171
194
|
]);
|
|
172
|
-
return spec(
|
|
195
|
+
return spec("pass", title, lines, "success");
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
function failed(card: Extract<CardData, { kind: "fail" }>, language: Language): BuiltCard {
|
|
@@ -183,12 +206,10 @@ function failed(card: Extract<CardData, { kind: "fail" }>, language: Language):
|
|
|
183
206
|
: [elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)]),
|
|
184
207
|
];
|
|
185
208
|
return spec(
|
|
186
|
-
language,
|
|
187
209
|
"fail",
|
|
188
210
|
title,
|
|
189
211
|
withFooter(formatReviewResultLines(card.details), footer),
|
|
190
212
|
"warning",
|
|
191
|
-
"❌",
|
|
192
213
|
);
|
|
193
214
|
}
|
|
194
215
|
|
|
@@ -203,16 +224,16 @@ function stopped(card: Extract<CardData, { kind: "stop" }>, language: Language):
|
|
|
203
224
|
language,
|
|
204
225
|
);
|
|
205
226
|
const body = [advisorModelLine(card.advisorModel, language), "", ...adviceLines(card.advisor.advice)];
|
|
206
|
-
return spec(
|
|
227
|
+
return spec("stop", title, withFooter(body, footer), "warning");
|
|
207
228
|
}
|
|
208
229
|
const title = qualityTitle(card.round, language === "en" ? "Review failed" : "审查未通过", language);
|
|
209
230
|
const body = formatReviewResultLines(card.details || stopReason(card.reason, language));
|
|
210
|
-
return spec(
|
|
231
|
+
return spec("stop", title, withFooter(body, footer), "warning");
|
|
211
232
|
}
|
|
212
233
|
|
|
213
234
|
function cancelled(card: Extract<CardData, { kind: "cancel" }>, language: Language): BuiltCard {
|
|
214
235
|
const title = language === "en" ? "Review cancelled" : "审查已取消";
|
|
215
|
-
return spec(
|
|
236
|
+
return spec("cancel", title, [reasonText(card.reason, language)], "neutral");
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
function timedOut(_card: Extract<CardData, { kind: "timeout" }>, language: Language): BuiltCard {
|
|
@@ -221,7 +242,7 @@ function timedOut(_card: Extract<CardData, { kind: "timeout" }>, language: Langu
|
|
|
221
242
|
language === "en" ? "Blocker: review timed out" : "卡点:审查超时",
|
|
222
243
|
language === "en" ? "Reason: overall time limit exceeded" : "原因:超过总体时限",
|
|
223
244
|
];
|
|
224
|
-
return spec(
|
|
245
|
+
return spec("timeout", title, lines, "warning");
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
/** 终止原因的展示文案(reducer 只出枚举,这里本地化)。 */
|
|
@@ -250,7 +271,7 @@ function errored(card: Extract<CardData, { kind: "error" }>, language: Language)
|
|
|
250
271
|
? []
|
|
251
272
|
: ["", elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)]),
|
|
252
273
|
];
|
|
253
|
-
return spec(
|
|
274
|
+
return spec("error", title, lines, "warning");
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
/** 顾问卡与审查结果卡同构:裁决进标题,正文用粗体模型分节行开头。 */
|
|
@@ -259,10 +280,10 @@ function advisorCard(card: Extract<CardData, { kind: "advisor" }>, language: Lan
|
|
|
259
280
|
const title = language === "en" ? `Advisor guidance · ${decision}` : `顾问指引 · ${decision}`;
|
|
260
281
|
const body = [advisorModelLine(card.advisorModel, language), "", ...adviceLines(card.advisor.advice)];
|
|
261
282
|
const footer = card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs, undefined, false, language)];
|
|
262
|
-
return spec(
|
|
283
|
+
return spec("advisor", title, withFooter(body, footer), "neutral");
|
|
263
284
|
}
|
|
264
285
|
|
|
265
|
-
/**
|
|
286
|
+
/** 裁决词→人话文案的唯一映射:卡标题与审查活动行共用,防两处文案漂移。 */
|
|
266
287
|
export function decisionText(verdict: "continue" | "narrow" | "stop", language: Language) {
|
|
267
288
|
return language === "en"
|
|
268
289
|
? { continue: "Continue fixing", narrow: "Narrow scope", stop: "Stop fixing" }[verdict]
|
|
@@ -304,7 +325,7 @@ function elapsedLine(
|
|
|
304
325
|
const elapsed = showTotal && totalMs !== undefined
|
|
305
326
|
? `${formatDuration(ms)} / ${language === "en" ? "total" : "总"} ${formatDuration(totalMs)}`
|
|
306
327
|
: formatDuration(ms);
|
|
307
|
-
return language === "en" ?
|
|
328
|
+
return language === "en" ? `Elapsed: ${elapsed}` : `用时:${elapsed}`;
|
|
308
329
|
}
|
|
309
330
|
|
|
310
331
|
function formatReviewResultLines(review: string) {
|
|
@@ -365,21 +386,13 @@ const REDUNDANT_REVIEW_LINES = new Set([
|
|
|
365
386
|
]);
|
|
366
387
|
|
|
367
388
|
function spec(
|
|
368
|
-
|
|
369
|
-
kind: CardDetails["kind"],
|
|
389
|
+
kind: CardData["kind"],
|
|
370
390
|
title: string,
|
|
371
391
|
lines: string[],
|
|
372
392
|
tone: CardDetails["tone"],
|
|
373
|
-
icon: string,
|
|
374
393
|
): BuiltCard {
|
|
375
|
-
const localized = localize(lines, language);
|
|
376
394
|
return {
|
|
377
395
|
content: `${title}\n${lines.join("\n")}`,
|
|
378
|
-
details: { version: VERSION, kind, title, lines
|
|
396
|
+
details: { version: VERSION, kind, title, lines, tone, icon: MARKS[kind].glyph },
|
|
379
397
|
};
|
|
380
398
|
}
|
|
381
|
-
|
|
382
|
-
/** details 行已本地化成品;content 里除标题外都是事实,不做二次翻译。 */
|
|
383
|
-
function localize(lines: string[], language: Language) {
|
|
384
|
-
return lines;
|
|
385
|
-
}
|
package/review/checkpoint.ts
CHANGED
|
@@ -40,10 +40,7 @@ export class CheckpointConflictError extends Error {
|
|
|
40
40
|
|
|
41
41
|
/** 只读会话访问(checkpoint 读/写前的 CAS 读取)。 */
|
|
42
42
|
export interface CheckpointReadContext {
|
|
43
|
-
sessionManager
|
|
44
|
-
getBranch?: () => unknown[];
|
|
45
|
-
getEntries?: () => unknown[];
|
|
46
|
-
};
|
|
43
|
+
sessionManager: { getBranch(): unknown[] };
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
const PHASES = new Set([
|
|
@@ -319,10 +316,7 @@ export function readCheckpoint(ctx: CheckpointReadContext): ReviewState | undefi
|
|
|
319
316
|
}
|
|
320
317
|
|
|
321
318
|
function latestEntry(ctx: CheckpointReadContext): Record<string, unknown> | undefined {
|
|
322
|
-
const entries =
|
|
323
|
-
ctx.sessionManager?.getBranch?.() ??
|
|
324
|
-
ctx.sessionManager?.getEntries?.() ??
|
|
325
|
-
[];
|
|
319
|
+
const entries = ctx.sessionManager.getBranch();
|
|
326
320
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
327
321
|
const entry = entries[index];
|
|
328
322
|
if (!isRecord(entry) || entry.type !== "custom") continue;
|
package/review/evidence.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* toolResult 正文仍跳过(输出体积大且非一手证据——审查者应自行重跑验证命令)。
|
|
11
11
|
*/
|
|
12
12
|
import type { Language } from "../config.js";
|
|
13
|
+
import { textOf } from "../format.js";
|
|
13
14
|
|
|
14
15
|
export const DEFAULT_EVIDENCE_TOKENS = 24_000;
|
|
15
16
|
/** 单条消息渲染上限,防单条超长消息撑爆预算。 */
|
|
@@ -21,13 +22,20 @@ export interface Evidence {
|
|
|
21
22
|
omitted: number;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
interface Render {
|
|
26
|
+
language: Language;
|
|
27
|
+
/** 会话文件:截断标记据此告诉审查者完整原文在哪里。内存会话没有。 */
|
|
28
|
+
sessionFile?: string;
|
|
29
|
+
failedCalls: ReadonlySet<string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export function buildEvidence(
|
|
25
33
|
entries: readonly unknown[],
|
|
26
34
|
language: Language,
|
|
27
|
-
budgetTokens = DEFAULT_EVIDENCE_TOKENS,
|
|
35
|
+
{ budgetTokens = DEFAULT_EVIDENCE_TOKENS, sessionFile }: { budgetTokens?: number; sessionFile?: string } = {},
|
|
28
36
|
): Evidence {
|
|
29
|
-
const
|
|
30
|
-
const blocks = entries.flatMap((entry) => renderEntry(entry,
|
|
37
|
+
const render: Render = { language, ...(sessionFile ? { sessionFile } : {}), failedCalls: collectFailedCalls(entries) };
|
|
38
|
+
const blocks = entries.flatMap((entry) => renderEntry(entry, render));
|
|
31
39
|
if (blocks.length === 0) return { text: "", omitted: 0 };
|
|
32
40
|
// 锚点必须是首条用户消息(原始需求):它之前可能排着其他扩展的可显示消息,
|
|
33
41
|
// 盲取第一块会把真正的需求锚点让进预算竞争、在长会话里被裁掉。
|
|
@@ -78,11 +86,8 @@ function collectFailedCalls(entries: readonly unknown[]): ReadonlySet<string> {
|
|
|
78
86
|
return failed;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
function renderEntry(
|
|
82
|
-
|
|
83
|
-
language: Language,
|
|
84
|
-
failedCalls: ReadonlySet<string>,
|
|
85
|
-
): EvidenceBlock[] {
|
|
89
|
+
function renderEntry(entry: unknown, render: Render): EvidenceBlock[] {
|
|
90
|
+
const { language } = render;
|
|
86
91
|
if (!isRecord(entry)) return [];
|
|
87
92
|
switch (entry.type) {
|
|
88
93
|
case "message": {
|
|
@@ -91,25 +96,25 @@ function renderEntry(
|
|
|
91
96
|
if (message.role === "user")
|
|
92
97
|
return [
|
|
93
98
|
{
|
|
94
|
-
text: `## ${userLabel(language)}\n${clip(
|
|
99
|
+
text: `## ${userLabel(language)}\n${clip(textOf(message.content), render)}`,
|
|
95
100
|
role: "user" as const,
|
|
96
101
|
},
|
|
97
102
|
];
|
|
98
103
|
if (message.role === "assistant")
|
|
99
|
-
return [{ text: `## ${assistantLabel(language)}\n${assistantBody(message.content,
|
|
104
|
+
return [{ text: `## ${assistantLabel(language)}\n${assistantBody(message.content, render)}` }];
|
|
100
105
|
return [];
|
|
101
106
|
}
|
|
102
107
|
case "custom_message": {
|
|
103
108
|
if (entry.display !== true) return [];
|
|
104
|
-
return [{ text: `## ${customLabel(language, String(entry.customType ?? ""))}\n${clip(
|
|
109
|
+
return [{ text: `## ${customLabel(language, String(entry.customType ?? ""))}\n${clip(textOf(entry.content), render)}` }];
|
|
105
110
|
}
|
|
106
111
|
case "compaction":
|
|
107
112
|
return typeof entry.summary === "string" && entry.summary
|
|
108
|
-
? [{ text: `## ${summaryLabel(language)}\n${clip(entry.summary)}` }]
|
|
113
|
+
? [{ text: `## ${summaryLabel(language)}\n${clip(entry.summary, render)}` }]
|
|
109
114
|
: [];
|
|
110
115
|
case "branch_summary":
|
|
111
116
|
return typeof entry.summary === "string" && entry.summary
|
|
112
|
-
? [{ text: `## ${branchSummaryLabel(language)}\n${clip(entry.summary)}` }]
|
|
117
|
+
? [{ text: `## ${branchSummaryLabel(language)}\n${clip(entry.summary, render)}` }]
|
|
113
118
|
: [];
|
|
114
119
|
default:
|
|
115
120
|
return [];
|
|
@@ -137,26 +142,19 @@ function branchSummaryLabel(language: Language) {
|
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
/** assistant 正文 = 文本段 + 工具调用轨迹;纯工具回合也因此留下编辑记录。 */
|
|
140
|
-
function assistantBody(
|
|
141
|
-
content
|
|
142
|
-
language: Language,
|
|
143
|
-
failedCalls: ReadonlySet<string>,
|
|
144
|
-
): string {
|
|
145
|
-
if (typeof content === "string") return clip(content);
|
|
145
|
+
function assistantBody(content: unknown, render: Render): string {
|
|
146
|
+
if (typeof content === "string") return clip(content, render);
|
|
146
147
|
if (!Array.isArray(content)) return "";
|
|
147
148
|
const trail = content
|
|
148
|
-
.map((part) => toolCallLine(asRecord(part),
|
|
149
|
+
.map((part) => toolCallLine(asRecord(part), render))
|
|
149
150
|
.filter(Boolean)
|
|
150
151
|
.join("\n");
|
|
151
|
-
const body = clip(
|
|
152
|
+
const body = clip(textOf(content), render);
|
|
152
153
|
return [body, trail].filter(Boolean).join("\n");
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
function toolCallLine(
|
|
156
|
-
|
|
157
|
-
language: Language,
|
|
158
|
-
failedCalls: ReadonlySet<string>,
|
|
159
|
-
): string {
|
|
156
|
+
function toolCallLine(part: Record<string, unknown> | undefined, render: Render): string {
|
|
157
|
+
const { language, failedCalls } = render;
|
|
160
158
|
if (part?.type !== "toolCall" || typeof part.name !== "string" || !part.name) return "";
|
|
161
159
|
const args = asRecord(part.arguments);
|
|
162
160
|
const target =
|
|
@@ -171,33 +169,33 @@ function toolCallLine(
|
|
|
171
169
|
? " (failed)"
|
|
172
170
|
: "(失败)"
|
|
173
171
|
: "";
|
|
174
|
-
return `${`[${part.name}] ${clipLine(target)}`.trimEnd()}${failed}`;
|
|
172
|
+
return `${`[${part.name}] ${clipLine(target, render)}`.trimEnd()}${failed}`;
|
|
175
173
|
}
|
|
176
174
|
|
|
177
175
|
/** 单行轨迹上限:防超长 bash 命令撑大证据块;路径不受影响。 */
|
|
178
176
|
const TOOL_LINE_MAX_CHARS = 200;
|
|
179
177
|
|
|
180
|
-
function clipLine(text: string) {
|
|
178
|
+
function clipLine(text: string, { language, sessionFile }: Render) {
|
|
181
179
|
const single = text.replace(/\s+/gu, " ").trim();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return content
|
|
189
|
-
.map((part) => {
|
|
190
|
-
const record = asRecord(part);
|
|
191
|
-
return record?.type === "text" && typeof record.text === "string"
|
|
192
|
-
? record.text
|
|
193
|
-
: "";
|
|
194
|
-
})
|
|
195
|
-
.join("\n");
|
|
180
|
+
if (single.length <= TOOL_LINE_MAX_CHARS) return single;
|
|
181
|
+
const where = sessionFile ?? (language === "en" ? "the session file" : "会话文件");
|
|
182
|
+
const note = language === "en"
|
|
183
|
+
? `…[truncated, ${single.length} chars; full original in ${where}]`
|
|
184
|
+
: `…[截断,原文 ${single.length} 字,完整原文在 ${where}]`;
|
|
185
|
+
return `${single.slice(0, TOOL_LINE_MAX_CHARS)}${note}`;
|
|
196
186
|
}
|
|
197
187
|
|
|
198
|
-
|
|
188
|
+
/**
|
|
189
|
+
* 截断处必须写明是证据组装的截断:裸省略号会被审查者当成回复本身没写完(长交付物曾因此连判 FAIL)。
|
|
190
|
+
* 完整原文在会话文件里,审查者能用 read 自行核对。
|
|
191
|
+
*/
|
|
192
|
+
function clip(text: string, { language, sessionFile }: Render) {
|
|
199
193
|
if (text.length <= MESSAGE_MAX_CHARS) return text.trim();
|
|
200
|
-
|
|
194
|
+
const where = sessionFile ?? (language === "en" ? "the session file" : "会话文件");
|
|
195
|
+
const note = language === "en"
|
|
196
|
+
? `[evidence truncated: this message has ${text.length} characters, only the first ${MESSAGE_MAX_CHARS} are shown; the full original is in ${where} — read it if you need to verify]`
|
|
197
|
+
: `[证据截断:本条消息原文 ${text.length} 字,此处只给出前 ${MESSAGE_MAX_CHARS} 字;完整原文在 ${where},需要核对时用 read 查看]`;
|
|
198
|
+
return `${text.slice(0, MESSAGE_MAX_CHARS).trim()}\n${note}`;
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
/**
|