md-zh-translation-skill 1.2.0 → 1.2.2
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/dist/src/codex-exec.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ChildProcessByStdio } from "node:child_process";
|
|
2
|
+
import type { Readable, Writable } from "node:stream";
|
|
1
3
|
export type CodexUsage = {
|
|
2
4
|
inputTokens: number;
|
|
3
5
|
cachedInputTokens: number;
|
|
@@ -23,6 +25,14 @@ export type CodexExecOptions = {
|
|
|
23
25
|
export interface CodexExecutor {
|
|
24
26
|
execute(prompt: string, options: CodexExecOptions): Promise<CodexExecResult>;
|
|
25
27
|
}
|
|
28
|
+
type SpawnFn = (command: string, args: readonly string[], options: {
|
|
29
|
+
cwd: string;
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"];
|
|
31
|
+
}) => ChildProcessByStdio<Writable, Readable, Readable>;
|
|
26
32
|
export declare class DefaultCodexExecutor implements CodexExecutor {
|
|
33
|
+
private readonly spawnFn;
|
|
34
|
+
private readonly sleepFn;
|
|
35
|
+
constructor(spawnFn?: SpawnFn, sleepFn?: (milliseconds: number) => Promise<void>);
|
|
27
36
|
execute(prompt: string, options: CodexExecOptions): Promise<CodexExecResult>;
|
|
28
37
|
}
|
|
38
|
+
export {};
|
package/dist/src/codex-exec.js
CHANGED
|
@@ -3,6 +3,8 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { CodexExecutionError } from "./errors.js";
|
|
6
|
+
const MAX_RETRYABLE_EXEC_FAILURES = 2;
|
|
7
|
+
const RETRYABLE_EXEC_FAILURE_DELAY_MS = 250;
|
|
6
8
|
function parseUsage(jsonl) {
|
|
7
9
|
let latestUsage = null;
|
|
8
10
|
for (const line of jsonl.split("\n")) {
|
|
@@ -48,67 +50,105 @@ function parseThreadId(jsonl) {
|
|
|
48
50
|
}
|
|
49
51
|
return undefined;
|
|
50
52
|
}
|
|
53
|
+
function isRetryableCodexFailure(stderr) {
|
|
54
|
+
return (stderr.includes("codex_core::shell_snapshot") &&
|
|
55
|
+
stderr.includes("Failed to delete shell snapshot"));
|
|
56
|
+
}
|
|
57
|
+
function sleep(milliseconds) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
setTimeout(resolve, milliseconds);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
51
62
|
export class DefaultCodexExecutor {
|
|
63
|
+
spawnFn;
|
|
64
|
+
sleepFn;
|
|
65
|
+
constructor(spawnFn = spawn, sleepFn = sleep) {
|
|
66
|
+
this.spawnFn = spawnFn;
|
|
67
|
+
this.sleepFn = sleepFn;
|
|
68
|
+
}
|
|
52
69
|
async execute(prompt, options) {
|
|
53
70
|
const workingDir = options.cwd ?? process.cwd();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
let attempt = 0;
|
|
72
|
+
let lastError = null;
|
|
73
|
+
while (attempt <= MAX_RETRYABLE_EXEC_FAILURES) {
|
|
74
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "md-zh-translate-"));
|
|
75
|
+
const outputPath = path.join(tempDir, "last-message.txt");
|
|
76
|
+
const schemaPath = options.outputSchema ? path.join(tempDir, "output-schema.json") : null;
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let stderr = "";
|
|
79
|
+
try {
|
|
80
|
+
if (schemaPath) {
|
|
81
|
+
await writeFile(schemaPath, `${JSON.stringify(options.outputSchema, null, 2)}\n`, "utf8");
|
|
82
|
+
}
|
|
83
|
+
const args = options.threadId
|
|
84
|
+
? buildResumeArgs(options, outputPath)
|
|
85
|
+
: buildExecArgs(options, workingDir, outputPath);
|
|
86
|
+
if (schemaPath) {
|
|
87
|
+
args.push("--output-schema", schemaPath);
|
|
88
|
+
}
|
|
89
|
+
args.push("-");
|
|
90
|
+
const child = this.spawnFn("codex", args, {
|
|
91
|
+
cwd: workingDir,
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
93
|
+
});
|
|
94
|
+
child.stdout.on("data", (chunk) => {
|
|
95
|
+
stdout += String(chunk);
|
|
96
|
+
});
|
|
97
|
+
child.stderr.on("data", (chunk) => {
|
|
98
|
+
stderr += String(chunk);
|
|
99
|
+
});
|
|
100
|
+
child.stdin.write(prompt);
|
|
101
|
+
child.stdin.end();
|
|
102
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
103
|
+
child.once("error", reject);
|
|
104
|
+
child.once("close", (code) => resolve(code ?? 1));
|
|
105
|
+
});
|
|
106
|
+
if (exitCode !== 0) {
|
|
107
|
+
options.onStderr?.(stderr);
|
|
108
|
+
const error = new CodexExecutionError(stderr.trim() || stdout.trim() || `codex exec exited with ${exitCode}`);
|
|
109
|
+
if (isRetryableCodexFailure(stderr) && attempt < MAX_RETRYABLE_EXEC_FAILURES) {
|
|
110
|
+
lastError = error;
|
|
111
|
+
attempt += 1;
|
|
112
|
+
await this.sleepFn(RETRYABLE_EXEC_FAILURE_DELAY_MS * attempt);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const text = (await readFile(outputPath, "utf8")).trim();
|
|
118
|
+
if (!text) {
|
|
119
|
+
throw new CodexExecutionError("Codex returned an empty final message.");
|
|
120
|
+
}
|
|
121
|
+
const threadId = parseThreadId(stdout) ?? options.threadId;
|
|
122
|
+
return {
|
|
123
|
+
text,
|
|
124
|
+
stderr,
|
|
125
|
+
jsonl: stdout,
|
|
126
|
+
usage: parseUsage(stdout),
|
|
127
|
+
...(threadId ? { threadId } : {})
|
|
128
|
+
};
|
|
89
129
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isRetryableCodexFailure(stderr) && attempt < MAX_RETRYABLE_EXEC_FAILURES) {
|
|
132
|
+
const retryableError = error instanceof CodexExecutionError
|
|
133
|
+
? error
|
|
134
|
+
: new CodexExecutionError(error instanceof Error ? error.message : String(error));
|
|
135
|
+
lastError = retryableError;
|
|
136
|
+
attempt += 1;
|
|
137
|
+
await this.sleepFn(RETRYABLE_EXEC_FAILURE_DELAY_MS * attempt);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (error instanceof CodexExecutionError) {
|
|
141
|
+
lastError = error;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
lastError = new CodexExecutionError(error instanceof Error ? error.message : String(error));
|
|
145
|
+
throw lastError;
|
|
93
146
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
text,
|
|
97
|
-
stderr,
|
|
98
|
-
jsonl: stdout,
|
|
99
|
-
usage: parseUsage(stdout),
|
|
100
|
-
...(threadId ? { threadId } : {})
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
if (error instanceof CodexExecutionError) {
|
|
105
|
-
throw error;
|
|
147
|
+
finally {
|
|
148
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
106
149
|
}
|
|
107
|
-
throw new CodexExecutionError(error instanceof Error ? error.message : String(error));
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
111
150
|
}
|
|
151
|
+
throw lastError ?? new CodexExecutionError("codex exec failed after retry.");
|
|
112
152
|
}
|
|
113
153
|
}
|
|
114
154
|
function buildExecArgs(options, workingDir, outputPath) {
|
|
@@ -116,6 +156,10 @@ function buildExecArgs(options, workingDir, outputPath) {
|
|
|
116
156
|
"exec",
|
|
117
157
|
"--skip-git-repo-check",
|
|
118
158
|
"--json",
|
|
159
|
+
"--disable",
|
|
160
|
+
"plugins",
|
|
161
|
+
"--disable",
|
|
162
|
+
"shell_snapshot",
|
|
119
163
|
"--sandbox",
|
|
120
164
|
"read-only",
|
|
121
165
|
"--color",
|
|
@@ -147,6 +191,10 @@ function buildResumeArgs(options, outputPath) {
|
|
|
147
191
|
"resume",
|
|
148
192
|
"--skip-git-repo-check",
|
|
149
193
|
"--json",
|
|
194
|
+
"--disable",
|
|
195
|
+
"plugins",
|
|
196
|
+
"--disable",
|
|
197
|
+
"shell_snapshot",
|
|
150
198
|
"-m",
|
|
151
199
|
options.model,
|
|
152
200
|
"-o",
|
|
@@ -4,6 +4,7 @@ Markdown 结构要求:
|
|
|
4
4
|
2. 不要改写 fenced code blocks、indented code blocks、inline code、URL、链接目标、图片 URL 和原始 HTML 标签。
|
|
5
5
|
3. 链接或图片的可见文字如果是正文,可以翻译;目标地址必须保持不变。
|
|
6
6
|
4. 不要把代码块、命令行片段或配置键值误译成中文。
|
|
7
|
+
5. 如果原文正文使用了可翻译的 Markdown 强调结构(如 **加粗**、*斜体*)或命令/flag 写法(如 --flag),译文应保持等价结构;不要丢掉强调,也不要把普通命令/flag 误改成代码块、标题或其他 Markdown 结构。
|
|
7
8
|
`.trim();
|
|
8
9
|
export const INITIAL_TRANSLATION_PROMPT = `
|
|
9
10
|
你是一名科技与科普翻译编辑。请把下面的英文 Markdown 文章翻译成自然、准确、可读性高的中文 Markdown,但本次任务以“硬性项正确”为第一优先级。请严格遵守以下要求:
|
|
@@ -51,9 +51,7 @@ export function protectSegmentFormattingSpans(body, startIndex = 1) {
|
|
|
51
51
|
spans.push({ id, kind, raw });
|
|
52
52
|
return id;
|
|
53
53
|
};
|
|
54
|
-
|
|
55
|
-
protectedBody = protectInlineCodeSegments(protectedBody, register);
|
|
56
|
-
protectedBody = protectInlineStrongEmphasis(protectedBody, register);
|
|
54
|
+
const protectedBody = mapOutsideInlineCode(body, (text) => protectInlineMarkdownLinks(text, register));
|
|
57
55
|
return { protectedBody, spans };
|
|
58
56
|
}
|
|
59
57
|
function protectFencedCodeBlocks(input, register) {
|
package/dist/src/translate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildBundledGateAuditPrompt, buildInitialPrompt, buildRepairPrompt, buildStylePolishPrompt } from "./internal/prompts/scheme-h.js";
|
|
1
|
+
import { buildBundledGateAuditPrompt, buildGateAuditPrompt, buildInitialPrompt, buildRepairPrompt, buildStylePolishPrompt } from "./internal/prompts/scheme-h.js";
|
|
2
2
|
import { DefaultCodexExecutor } from "./codex-exec.js";
|
|
3
3
|
import { FormattingError, HardGateError } from "./errors.js";
|
|
4
4
|
import { formatTranslatedBody, reconstructMarkdown } from "./format.js";
|
|
@@ -279,9 +279,7 @@ async function translateProtectedChunk(chunk, plan, context) {
|
|
|
279
279
|
? `${chunkLabel}, segment ${segment.index + 1}/${segments.length}`
|
|
280
280
|
: chunkLabel;
|
|
281
281
|
const segmentResult = await translateProtectedSegment(segment, plan, context, segmentPromptContext, segmentLabel, nextLocalSpanIndex);
|
|
282
|
-
nextLocalSpanIndex += segmentResult.spans.filter((span) => span.kind === "
|
|
283
|
-
span.kind === "strong_emphasis" ||
|
|
284
|
-
span.kind === "inline_markdown_link").length;
|
|
282
|
+
nextLocalSpanIndex += segmentResult.spans.filter((span) => span.kind === "inline_markdown_link").length;
|
|
285
283
|
draftedSegments.push(segmentResult);
|
|
286
284
|
}
|
|
287
285
|
let bundledAudit = await runBundledGateAudit(draftedSegments, plan, context, chunkPromptContext, chunkLabel);
|
|
@@ -404,12 +402,45 @@ async function runBundledGateAudit(draftedSegments, plan, context, chunkPromptCo
|
|
|
404
402
|
reuseSession: true,
|
|
405
403
|
onStderr: (stderrChunk) => reportChunkProgress(context.options, "audit", chunkPromptContext.chunkIndex - 1, plan, chunkLabel, stderrChunk)
|
|
406
404
|
});
|
|
407
|
-
|
|
405
|
+
let bundledAudit;
|
|
406
|
+
try {
|
|
407
|
+
bundledAudit = parseBundledGateAudit(auditResult.text, segmentIndices);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (!(error instanceof HardGateError) ||
|
|
411
|
+
!error.message.includes("Bundled gate audit segment_index set mismatch")) {
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
report(context.options, "audit", `Chunk ${chunkPromptContext.chunkIndex}/${plan.chunks.length}${chunkLabel}: bundled audit returned incomplete segment results; falling back to per-segment audit.`);
|
|
415
|
+
bundledAudit = await runFallbackSegmentAudits(draftedSegments, plan, context, chunkPromptContext, chunkLabel);
|
|
416
|
+
}
|
|
408
417
|
for (const segmentAudit of bundledAudit.segments) {
|
|
409
418
|
validateStructuralGateChecks(segmentAudit);
|
|
410
419
|
}
|
|
411
420
|
return bundledAudit;
|
|
412
421
|
}
|
|
422
|
+
async function runFallbackSegmentAudits(draftedSegments, plan, context, chunkPromptContext, chunkLabel) {
|
|
423
|
+
const segments = [];
|
|
424
|
+
for (const draftedSegment of draftedSegments) {
|
|
425
|
+
const segmentLabel = draftedSegments.length > 1
|
|
426
|
+
? `${chunkLabel}, segment ${draftedSegment.segment.index + 1}/${draftedSegments.length}`
|
|
427
|
+
: chunkLabel;
|
|
428
|
+
const auditResult = await context.executor.execute(withChunkContext(buildGateAuditPrompt(draftedSegment.protectedSource, draftedSegment.protectedBody), draftedSegment.promptContext), {
|
|
429
|
+
cwd: context.cwd,
|
|
430
|
+
model: context.model,
|
|
431
|
+
reasoningEffort: AUDIT_REASONING_EFFORT,
|
|
432
|
+
outputSchema: GATE_AUDIT_SCHEMA,
|
|
433
|
+
reuseSession: true,
|
|
434
|
+
onStderr: (stderrChunk) => reportChunkProgress(context.options, "audit", chunkPromptContext.chunkIndex - 1, plan, segmentLabel, stderrChunk)
|
|
435
|
+
});
|
|
436
|
+
const audit = parseGateAudit(auditResult.text);
|
|
437
|
+
segments.push({
|
|
438
|
+
segment_index: draftedSegment.segment.index + 1,
|
|
439
|
+
...audit
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return { segments };
|
|
443
|
+
}
|
|
413
444
|
function rebuildChunkFromSegmentStates(segments, draftedSegments, key) {
|
|
414
445
|
return segments
|
|
415
446
|
.map((segment) => {
|
|
@@ -612,6 +643,9 @@ function extractSegmentSpecialNotes(source) {
|
|
|
612
643
|
if (containsToolNameExplanationBlock(source)) {
|
|
613
644
|
notes.push("当前分段包含工具名、命令名、包名、CLI 名称或产品名的列表项说明。对这类以英文原名作为标签的说明条目,允许保留英文原名,并在后面直接接中文解释;不要为了满足首现双语而强行改写成“中文(英文)”主译格式。", "对于 `kubectl - Kubernetes cluster access`、`docker - ...`、`npm install -g ...` 这类工具/命令/产品说明,只要英文原名保留且中文解释清楚,就可视为合格的首现锚定;不要把“英文名(中文解释)”误判为必须修复。");
|
|
614
645
|
}
|
|
646
|
+
if (containsTranslatableMarkdownStructure(source)) {
|
|
647
|
+
notes.push("当前分段包含可翻译的 Markdown 强调结构或命令/flag 写法。翻译时必须保留等价结构:原文中的 **加粗**、*斜体* 等强调,不得无故去掉;像 --dangerously-skip-permissions 这类命令参数或 flag,应保留原始写法,不要改成代码块、标题、列表标签或其他 Markdown 结构。", "如果强调结构里的正文需要翻译,请翻译内容本身,但保留强调标记;如果命令、flag、配置键名或 CLI 参数本身是英文原名,请保留原名,只翻译周围解释。");
|
|
648
|
+
}
|
|
615
649
|
return notes;
|
|
616
650
|
}
|
|
617
651
|
function containsAttributionLikeBlock(source) {
|
|
@@ -645,6 +679,15 @@ function isToolNameExplanationLine(line) {
|
|
|
645
679
|
}
|
|
646
680
|
return /^(?:`[^`]+`|[@A-Za-z0-9._/+:-]+)\s*(?:-|—|:)\s+.+$/.test(body);
|
|
647
681
|
}
|
|
682
|
+
function containsTranslatableMarkdownStructure(source) {
|
|
683
|
+
return splitRawBlocks(source).some((block) => isTranslatableMarkdownStructureBlock(block.content));
|
|
684
|
+
}
|
|
685
|
+
function isTranslatableMarkdownStructureBlock(content) {
|
|
686
|
+
if (content.trim().length === 0) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
return /(\*\*[^*\n]+?\*\*|__[^_\n]+?__|\*[^*\n]+?\*|_[^_\n]+?_)/.test(content) || /\B--[A-Za-z0-9][A-Za-z0-9-]*/.test(content);
|
|
690
|
+
}
|
|
648
691
|
function buildChunkPromptContext(chunk, plan, sourcePathHint, establishedTerms) {
|
|
649
692
|
return {
|
|
650
693
|
documentTitle: plan.documentTitle,
|
package/package.json
CHANGED