feat: add differ adapter — wraps docx-diff CLI, handles exit code 0/1/2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jiayiyan
2026-05-18 16:06:32 +08:00
parent deacad57fe
commit 9a07418d89
2 changed files with 213 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import { stat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
import * as z from "zod/v4";
export type DifferAdapterConfig = {
command: string | null;
timeout: number | null;
};
type SpawnCliFn = typeof spawnCli;
export function createDifferAdapter(
config: DifferAdapterConfig,
spawnCliFn: SpawnCliFn = spawnCli,
): AdapterFn {
const command = config.command ?? "docx-diff";
return <T>(_systemPrompt: string, schema: z.ZodType<T>) =>
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
const officeStep = ctx.steps.find((s) => s.role === "office");
if (officeStep === undefined) {
throw new Error("differ: office step not found in ctx.steps (invariant violation)");
}
const officeMeta = officeStep.meta as { mode: string; outputDocx: string; sourceDocx: string | null };
if (officeMeta.mode !== "edit" || officeMeta.sourceDocx === null) {
throw new Error("differ: office step is not in edit mode");
}
const { outputDocx, sourceDocx } = officeMeta;
const reportPath = join(dirname(outputDocx), "diff_report.html");
const result = await spawnCliFn(
command,
[sourceDocx, outputDocx, "-o", "html", "--out-file", reportPath],
{ cwd: null, timeoutMs: config.timeout },
);
if (!result.ok) {
const e = result.error;
if (e.kind === "non_zero_exit" && e.exitCode === 1) {
// exit 1 means "documents differ" — the normal success case for docx-diff
} else if (e.kind === "non_zero_exit") {
throw new Error(`differ: docx-diff failed (exit ${e.exitCode}): ${e.stderr}`);
} else if (e.kind === "timeout") {
throw new Error("differ: timed out");
} else {
throw new Error(`differ: spawn failed: ${(e as { message: string }).message}`);
}
}
try {
await stat(reportPath);
} catch {
throw new Error(`differ: report file not found: ${reportPath}`);
}
const raw = JSON.stringify({ outputDocx, sourceDocx, diffReport: reportPath });
const meta = schema.parse(JSON.parse(raw)) as T;
return { meta, childThread: null };
};
}