

















@@ -1,10 +1,12 @@
11import fs from "node:fs";
22import path from "node:path";
33import { fileURLToPath } from "node:url";
4+import ts from "typescript";
45import { TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "../src/agents/tool-display-config.js";
5667const scriptDir = path.dirname(fileURLToPath(import.meta.url));
78const repoRoot = path.resolve(scriptDir, "..");
9+const configPath = path.join(repoRoot, "src/agents/tool-display-config.ts");
810const outputPath = path.join(
911repoRoot,
1012"apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json",
@@ -16,48 +18,62 @@ const toolSources = [
1618path.join(repoRoot, "src/auto-reply/reply/acp-projector.ts"),
1719];
182019-const args = new Set(process.argv.slice(2));
20-const shouldCheck = args.has("--check");
21-const shouldWrite = args.has("--write");
21+type DuplicateToolKey = {
22+name: string;
23+lines: number[];
24+};
222523-if (!shouldCheck && !shouldWrite) {
24-console.error("Usage: node --import tsx scripts/tool-display.ts --check|--write");
25-process.exit(1);
26-}
26+export function main(argv = process.argv.slice(2)): number {
27+const args = new Set(argv);
28+const shouldCheck = args.has("--check");
29+ const shouldWrite = args.has("--write");
273028-const expected = serializeToolDisplayConfig();
29-ensureCoreToolCoverage();
31+if (!shouldCheck && !shouldWrite) {
32+console.error("Usage: node --import tsx scripts/tool-display.ts --check|--write");
33+return 1;
34+}
303531-if (shouldWrite) {
32-fs.mkdirSync(path.dirname(outputPath), { recursive: true });
33-fs.writeFileSync(outputPath, expected);
34-process.stdout.write(`wrote ${path.relative(repoRoot, outputPath)}\n`);
35-process.exit(0);
36-}
36+const duplicateErrors = collectToolDisplayDuplicateErrors({ includeSnapshot: shouldCheck });
37+if (duplicateErrors.length > 0) {
38+console.error(duplicateErrors.join("\n"));
39+return 1;
40+}
374138-if (!fs.existsSync(path.dirname(outputPath))) {
39-process.stdout.write(
40-`skip tool-display snapshot check; missing ${path.relative(repoRoot, path.dirname(outputPath))}\n`,
41-);
42-process.exit(0);
43-}
42+const expected = serializeToolDisplayConfig();
43+ensureCoreToolCoverage();
444445-if (!fs.existsSync(outputPath)) {
46-console.error(
47-`missing generated snapshot: ${path.relative(repoRoot, outputPath)}\nrun: pnpm tool-display:write`,
48-);
49-process.exit(1);
50-}
45+ if (shouldWrite) {
46+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
47+fs.writeFileSync(outputPath, expected);
48+ process.stdout.write(`wrote ${path.relative(repoRoot, outputPath)}\n`);
49+ return 0;
50+ }
515152-const actual = fs.readFileSync(outputPath, "utf8");
53-if (actual !== expected) {
54-console.error(
55-`tool-display snapshot is stale: ${path.relative(repoRoot, outputPath)}\nrun: pnpm tool-display:write`,
56-);
57-process.exit(1);
58-}
52+if (!fs.existsSync(path.dirname(outputPath))) {
53+process.stdout.write(
54+`skip tool-display snapshot check; missing ${path.relative(repoRoot, path.dirname(outputPath))}\n`,
55+);
56+return 0;
57+}
595860-process.stdout.write("tool-display snapshot is up to date\n");
59+if (!fs.existsSync(outputPath)) {
60+console.error(
61+`missing generated snapshot: ${path.relative(repoRoot, outputPath)}\nrun: pnpm tool-display:write`,
62+);
63+return 1;
64+}
65+66+const actual = fs.readFileSync(outputPath, "utf8");
67+if (actual !== expected) {
68+console.error(
69+`tool-display snapshot is stale: ${path.relative(repoRoot, outputPath)}\nrun: pnpm tool-display:write`,
70+);
71+return 1;
72+}
73+74+process.stdout.write("tool-display snapshot is up to date\n");
75+return 0;
76+}
61776278function ensureCoreToolCoverage() {
6379const toolNames = new Set<string>();
@@ -92,3 +108,131 @@ function collectToolNamesFromFile(sourcePath: string, names: Set<string>) {
92108function serializeToolDisplayConfig(config: ToolDisplayConfig = TOOL_DISPLAY_CONFIG): string {
93109return `${JSON.stringify(config, null, 2)}\n`;
94110}
111+112+function collectToolDisplayDuplicateErrors(options: { includeSnapshot: boolean }): string[] {
113+const duplicateErrors: string[] = [];
114+const configSource = fs.readFileSync(configPath, "utf8");
115+const configDuplicates = collectToolDisplayConfigDuplicateKeys(configSource, configPath);
116+if (configDuplicates.length > 0) {
117+duplicateErrors.push(
118+formatDuplicateToolKeyError(path.relative(repoRoot, configPath), configDuplicates),
119+);
120+}
121+122+if (options.includeSnapshot && fs.existsSync(outputPath)) {
123+const snapshotSource = fs.readFileSync(outputPath, "utf8");
124+const snapshotDuplicates = collectToolDisplaySnapshotDuplicateKeys(snapshotSource, outputPath);
125+if (snapshotDuplicates.length > 0) {
126+duplicateErrors.push(
127+formatDuplicateToolKeyError(path.relative(repoRoot, outputPath), snapshotDuplicates),
128+);
129+}
130+}
131+return duplicateErrors;
132+}
133+134+export function collectToolDisplayConfigDuplicateKeys(
135+source: string,
136+sourcePath = "src/agents/tool-display-config.ts",
137+): DuplicateToolKey[] {
138+const sourceFile = ts.createSourceFile(sourcePath, source, ts.ScriptTarget.Latest, true);
139+let toolsObject: ts.ObjectLiteralExpression | undefined;
140+visitToolDisplayConfig(sourceFile, (configObject) => {
141+toolsObject = findObjectProperty(configObject, "tools");
142+});
143+return toolsObject ? collectDuplicatePropertyKeys(toolsObject, sourceFile) : [];
144+}
145+146+export function collectToolDisplaySnapshotDuplicateKeys(
147+source: string,
148+sourcePath = "tool-display.json",
149+): DuplicateToolKey[] {
150+const sourceFile = ts.parseJsonText(sourcePath, source);
151+const statement = sourceFile.statements[0];
152+if (!statement || !ts.isExpressionStatement(statement)) {
153+return [];
154+}
155+const root = statement.expression;
156+if (!ts.isObjectLiteralExpression(root)) {
157+return [];
158+}
159+const toolsObject = findObjectProperty(root, "tools");
160+return toolsObject ? collectDuplicatePropertyKeys(toolsObject, sourceFile) : [];
161+}
162+163+export function formatDuplicateToolKeyError(
164+relativePath: string,
165+duplicates: DuplicateToolKey[],
166+): string {
167+const formatted = duplicates
168+.map((duplicate) => `${duplicate.name} at lines ${duplicate.lines.join(", ")}`)
169+.join("; ");
170+return `tool-display metadata has duplicate tool ids in ${relativePath}: ${formatted}`;
171+}
172+173+function visitToolDisplayConfig(
174+node: ts.Node,
175+onConfig: (configObject: ts.ObjectLiteralExpression) => void,
176+) {
177+if (
178+ts.isVariableDeclaration(node) &&
179+ts.isIdentifier(node.name) &&
180+node.name.text === "TOOL_DISPLAY_CONFIG" &&
181+node.initializer &&
182+ts.isObjectLiteralExpression(node.initializer)
183+) {
184+onConfig(node.initializer);
185+return;
186+}
187+ts.forEachChild(node, (child) => visitToolDisplayConfig(child, onConfig));
188+}
189+190+function findObjectProperty(
191+object: ts.ObjectLiteralExpression,
192+propertyName: string,
193+): ts.ObjectLiteralExpression | undefined {
194+for (const property of object.properties) {
195+if (
196+ts.isPropertyAssignment(property) &&
197+getPropertyNameText(property.name) === propertyName &&
198+ts.isObjectLiteralExpression(property.initializer)
199+) {
200+return property.initializer;
201+}
202+}
203+return undefined;
204+}
205+206+function collectDuplicatePropertyKeys(
207+object: ts.ObjectLiteralExpression,
208+sourceFile: ts.SourceFile,
209+): DuplicateToolKey[] {
210+const keyLines = new Map<string, number[]>();
211+for (const property of object.properties) {
212+if (!ts.isPropertyAssignment(property)) {
213+continue;
214+}
215+const name = getPropertyNameText(property.name);
216+if (!name) {
217+continue;
218+}
219+const line =
220+sourceFile.getLineAndCharacterOfPosition(property.name.getStart(sourceFile)).line + 1;
221+keyLines.set(name, [...(keyLines.get(name) ?? []), line]);
222+}
223+return [...keyLines.entries()]
224+.filter(([, lines]) => lines.length > 1)
225+.map(([name, lines]) => ({ name, lines }))
226+.toSorted((left, right) => left.name.localeCompare(right.name));
227+}
228+229+function getPropertyNameText(name: ts.PropertyName): string | undefined {
230+if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
231+return name.text;
232+}
233+return undefined;
234+}
235+236+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
237+process.exitCode = main();
238+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。