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.
@@ -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 {};
@@ -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
- const tempDir = await mkdtemp(path.join(tmpdir(), "md-zh-translate-"));
55
- const outputPath = path.join(tempDir, "last-message.txt");
56
- const schemaPath = options.outputSchema ? path.join(tempDir, "output-schema.json") : null;
57
- if (schemaPath) {
58
- await writeFile(schemaPath, `${JSON.stringify(options.outputSchema, null, 2)}\n`, "utf8");
59
- }
60
- const args = options.threadId
61
- ? buildResumeArgs(options, outputPath)
62
- : buildExecArgs(options, workingDir, outputPath);
63
- if (schemaPath) {
64
- args.push("--output-schema", schemaPath);
65
- }
66
- args.push("-");
67
- const child = spawn("codex", args, {
68
- cwd: workingDir,
69
- stdio: ["pipe", "pipe", "pipe"]
70
- });
71
- let stdout = "";
72
- let stderr = "";
73
- child.stdout.on("data", (chunk) => {
74
- stdout += String(chunk);
75
- });
76
- child.stderr.on("data", (chunk) => {
77
- stderr += String(chunk);
78
- });
79
- child.stdin.write(prompt);
80
- child.stdin.end();
81
- const exitCode = await new Promise((resolve, reject) => {
82
- child.once("error", reject);
83
- child.once("close", (code) => resolve(code ?? 1));
84
- });
85
- try {
86
- if (exitCode !== 0) {
87
- options.onStderr?.(stderr);
88
- throw new CodexExecutionError(stderr.trim() || stdout.trim() || `codex exec exited with ${exitCode}`);
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
- const text = (await readFile(outputPath, "utf8")).trim();
91
- if (!text) {
92
- throw new CodexExecutionError("Codex returned an empty final message.");
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
- const threadId = parseThreadId(stdout) ?? options.threadId;
95
- return {
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
- let protectedBody = mapOutsideInlineCode(body, (text) => protectInlineMarkdownLinks(text, register));
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) {
@@ -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 === "inline_code" ||
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
- const bundledAudit = parseBundledGateAudit(auditResult.text, segmentIndices);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-zh-translation-skill",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "CLI skill for translating English Markdown articles into polished Chinese Markdown with a hidden gated pipeline.",
5
5
  "type": "module",
6
6
  "bin": {