This commit is contained in:
2026-08-14 13:51:19 +08:00
parent 5fbcf2c0b8
commit e3b3558350
2 changed files with 91 additions and 12 deletions
@@ -295,12 +295,27 @@ test("text-to-cad build123d guidance is available from the original skill", asyn
});
test("text-to-cad CAD-SkillX references are available through documentation lookup", async () => {
const result = await readTextToCadDocumentation("references/cad-skillx/planning/mounting-plate.planning.md");
const result = await readTextToCadDocumentation("references/cad-skillx/planning/mounting-plate.planning.md", {
cadSkillxEnabled: true,
});
assert.equal(result.ok, true);
if (!result.ok) return;
assert.equal(result.content.includes("Mounting Plate Planning Skill"), true);
});
test("text-to-cad CAD-SkillX references are disabled by the code switch", async () => {
const result = await readTextToCadDocumentation("references/cad-skillx/planning/mounting-plate.planning.md");
assert.equal(result.ok, false);
if (result.ok) return;
assert.equal(result.error.includes("disabled"), true);
const skill = await readTextToCadDocumentation("skill");
assert.equal(skill.ok, true);
if (!skill.ok) return;
assert.equal(skill.content.includes("skill-pack-curated-20260807"), false);
assert.equal(skill.content.includes("CAD-SkillX generated optimization references are disabled"), true);
});
test("text-to-cad documentation lookup rejects paths outside CAD-SkillX references", async () => {
const result = await readTextToCadDocumentation("references/cad-skillx/../build123d-modeling.md");
assert.equal(result.ok, false);
@@ -317,7 +332,7 @@ test("new build123d generation is bound to the router token and exact API docs",
const source = fs.readFileSync(chatSourcePath, "utf8");
assert.equal(source.includes("read_text_to_cad_docs: tool"), true);
assert.equal(source.includes("read_build123d_api: tool"), true);
assert.equal(source.includes("loadRequiredTextToCadDocumentation()"), true);
assert.equal(source.includes("loadRequiredTextToCadDocumentation({ cadSkillxEnabled: useCadSkillx })"), true);
assert.equal(source.includes("Studio-loaded text-to-cad/build123d base documentation"), true);
assert.equal(source.includes("build123d generation requires text-to-cad docs"), false);
assert.equal(source.includes("routeToken: z.string().optional()"), true);
@@ -326,6 +341,19 @@ test("new build123d generation is bound to the router token and exact API docs",
assert.equal(source.includes("服务端已自动补读缺失 API"), true);
});
test("CAD-SkillX code switch is off and not exposed as a UI toggle", () => {
const uiSource = fs.readFileSync(path.join(process.cwd(), "src", "components", "agent-studio.tsx"), "utf8");
const routeSource = fs.readFileSync(path.join(process.cwd(), "src", "app", "api", "chat", "route.ts"), "utf8");
const chatSource = fs.readFileSync(chatSourcePath, "utf8");
assert.equal(uiSource.includes("cad-agent-studio.cad-skillx-enabled"), false);
assert.equal(uiSource.includes("cadSkillxEnabled"), false);
assert.equal(routeSource.includes("cadSkillxEnabled"), false);
assert.equal(chatSource.includes("const CAD_SKILLX_ENABLED = false;"), true);
assert.equal(chatSource.includes("const useCadSkillx = CAD_SKILLX_ENABLED;"), true);
assert.equal(chatSource.includes("CAD-SkillX generated optimization references are disabled"), true);
assert.equal(chatSource.includes("readTextToCadDocumentation(document, { cadSkillxEnabled: useCadSkillx })"), true);
});
test("build123d API preflight ignores non-build123d imported helpers", () => {
const candidates = build123dCallCandidates([
"from pathlib import Path",
+61 -10
View File
@@ -386,6 +386,7 @@ const SIMPLECADAPI_REFERENCE_ROOT = path.join(SIMPLECADAPI_SKILL_ROOT, "referenc
const SIMPLECADAPI_REQUIRED_DOCS = ["skill", "api/README.md", "stdlib/README.md"];
const TEXT_TO_CAD_SKILL_ROOT = path.join(process.cwd(), "..", "text-to-cad", "skills", "cad");
const TEXT_TO_CAD_REQUIRED_DOCS = ["skill", "references/build123d-modeling.md", "references/step-generation.md"];
const CAD_SKILLX_ENABLED = false;
type SimpleCadApiDocumentationResult =
| {
@@ -512,7 +513,21 @@ type TextToCadDocumentationResult =
| { ok: true; document: string; path: string; content: string }
| { ok: false; document: string; error: string; suggestedDocuments: string[] };
export async function readTextToCadDocumentation(document: string): Promise<TextToCadDocumentationResult> {
type TextToCadDocumentationOptions = {
cadSkillxEnabled?: boolean;
};
function stripCadSkillxReferences(content: string) {
return content
.replace(/\n?<!-- cad-skillx:start:[\s\S]*?<!-- cad-skillx:end:[^>]*-->\n?/g, "\n\n")
.trimEnd();
}
export async function readTextToCadDocumentation(
document: string,
options: TextToCadDocumentationOptions = {},
): Promise<TextToCadDocumentationResult> {
const cadSkillxEnabled = options.cadSkillxEnabled ?? CAD_SKILLX_ENABLED;
const normalized = normalizeTextToCadDocument(document);
const fixedDocuments: Record<string, string> = {
skill: path.join(TEXT_TO_CAD_SKILL_ROOT, "SKILL.md"),
@@ -535,6 +550,17 @@ export async function readTextToCadDocumentation(document: string): Promise<Text
&& !path.isAbsolute(cadSkillxRelative),
);
const target = fixedDocuments[normalized || ""] || (cadSkillxDocument ? cadSkillxTarget : "");
if (cadSkillxDocument && !cadSkillxEnabled) {
return {
ok: false,
document: normalized || String(document || ""),
error: "CAD-SkillX optimization references are disabled for this request.",
suggestedDocuments: [
"skill",
...Object.keys(fixedDocuments).filter((key) => key !== "skill"),
],
};
}
if (!normalized || !target) {
return {
ok: false,
@@ -550,11 +576,19 @@ export async function readTextToCadDocumentation(document: string): Promise<Text
};
}
try {
const content = await fs.readFile(target, "utf8");
return {
ok: true,
document: normalized,
path: target,
content: await fs.readFile(target, "utf8"),
content: normalized === "skill" && !cadSkillxEnabled
? [
stripCadSkillxReferences(content),
"",
"## CAD-SkillX references",
"CAD-SkillX generated optimization references are disabled for this request. Do not read or use `references/cad-skillx/` documents.",
].join("\n")
: content,
};
} catch (error) {
return {
@@ -566,8 +600,10 @@ export async function readTextToCadDocumentation(document: string): Promise<Text
}
}
async function loadRequiredTextToCadDocumentation() {
const results = await Promise.all(TEXT_TO_CAD_REQUIRED_DOCS.map(readTextToCadDocumentation));
async function loadRequiredTextToCadDocumentation(options: TextToCadDocumentationOptions = {}) {
const results = await Promise.all(
TEXT_TO_CAD_REQUIRED_DOCS.map((document) => readTextToCadDocumentation(document, options)),
);
const unavailable = results.filter((result) => !result.ok);
if (unavailable.length) {
throw new Error(`Studio could not load required text-to-cad documentation: ${unavailable.map((result) => result.document).join(", ")}.`);
@@ -667,7 +703,16 @@ export function build123dCallCandidates(source: string) {
return [...new Set([...candidates, ...memberReferences])].slice(0, 64);
}
function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], taskContext = "") {
function buildSystemPrompt(
viewerContext: unknown[],
attachments: unknown[],
taskContext = "",
options: { cadSkillxEnabled?: boolean } = {},
) {
const cadSkillxEnabled = options.cadSkillxEnabled ?? CAD_SKILLX_ENABLED;
const build123dDocumentationInstruction = cadSkillxEnabled
? "When route_cad_request returns build123d_python, Studio automatically loads the text-to-cad skill, build123d-modeling, and step-generation documents into the route result. Do not repeat those reads. If the request matches a CAD-SkillX entry listed there, use read_text_to_cad_docs only for the relevant Planning/Functional/Atomic reference. Before writing source, call read_build123d_api once with every build123d symbol you plan to use. It reads the active build123d runtime's actual signature and docstring. Follow those signatures exactly: do not guess argument order, namespaces, or keyword names. Use build123d enum members exactly, for example Align.CENTER and Mode.SUBTRACT, not strings such as \"CENTER\"."
: "When route_cad_request returns build123d_python, Studio automatically loads the text-to-cad skill, build123d-modeling, and step-generation documents into the route result. Do not repeat those reads. CAD-SkillX generated optimization references are disabled for this request: do not call read_text_to_cad_docs for references/cad-skillx/... and do not use CAD-SkillX Planning/Functional/Atomic guidance. Before writing source, call read_build123d_api once with every build123d symbol you plan to use. It reads the active build123d runtime's actual signature and docstring. Follow those signatures exactly: do not guess argument order, namespaces, or keyword names. Use build123d enum members exactly, for example Align.CENTER and Mode.SUBTRACT, not strings such as \"CENTER\".";
return [
"You are the CAD Agent Studio assistant.",
"You help create and modify CAD models through a real server-side CAD generation tool.",
@@ -676,7 +721,7 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
"For every new text/image CAD model, call route_cad_request once before writing native source. CAD Router selects SimpleCADAPI only for catalogued standard primary parts; every other new part is text-to-cad/build123d. Use its returned sourceKind in generate_cad. Do not choose the backend yourself and do not read backend documentation for routing.",
"When route_cad_request returns simplecadapi_python, first read the original SimpleCADAPI docs through read_simplecadapi_docs: skill, api/README.md, and stdlib/README.md. Then read the exact original API or stdlib Markdown page for every SimpleCADAPI function used in the source. Follow the documented signatures literally; do not invent namespaces or API names. Documentation paths come from the README links, not Python namespaces: for example simplecadapi.ql.value is documented as api/value.md, not ql/value.md. The server rejects source that calls a nonexistent SimpleCADAPI attribute.",
"When route_cad_request returns build123d_python, Studio automatically loads the text-to-cad skill, build123d-modeling, and step-generation documents into the route result. Do not repeat those reads. If the request matches a CAD-SkillX entry listed there, use read_text_to_cad_docs only for the relevant Planning/Functional/Atomic reference. Before writing source, call read_build123d_api once with every build123d symbol you plan to use. It reads the active build123d runtime's actual signature and docstring. Follow those signatures exactly: do not guess argument order, namespaces, or keyword names. Use build123d enum members exactly, for example Align.CENTER and Mode.SUBTRACT, not strings such as \"CENTER\".",
build123dDocumentationInstruction,
"When STEP files are attached, the source is teacher and acceptance truth only. Use reconstruct_uploaded_step; never import the teacher as model geometry.",
"All uploaded files are available to you. Images are attached directly to the latest user message when the selected provider/model supports vision. STEP/STP and binary files are available through inspect_uploaded_file.",
"When the user asks to model from an uploaded image, inspect the image directly from the image part. If the selected provider/model cannot process images, say that clearly and ask the user to switch to a vision-capable OpenAI-compatible model.",
@@ -936,6 +981,7 @@ export async function streamAgentResponse({
selectedTaskId?: string;
conversationId?: string;
}) {
const useCadSkillx = CAD_SKILLX_ENABLED;
const requestedConversationId = String(conversationId || "").trim();
const normalizedConversationId = requestedConversationId ? safeConversationId(requestedConversationId) : "";
const requestMessages = messages;
@@ -1058,7 +1104,7 @@ export async function streamAgentResponse({
build123dApiSymbolsRead.clear();
routeDecisions.clear();
const build123dBaseDocs = decision.selectedBackend === "build123d"
? await loadRequiredTextToCadDocumentation()
? await loadRequiredTextToCadDocumentation({ cadSkillxEnabled: useCadSkillx })
: [];
for (const document of build123dBaseDocs) {
textToCadDocsRead.add(document.document);
@@ -1077,6 +1123,7 @@ export async function streamAgentResponse({
...(build123dBaseDocs.length ? {
backendContext: {
source: "Studio-loaded text-to-cad/build123d base documentation",
cadSkillxEnabled: useCadSkillx,
documents: build123dBaseDocs.map(({ document, content }) => ({ document, content })),
},
} : {}),
@@ -1102,13 +1149,15 @@ export async function streamAgentResponse({
description: [
"Read an additional original text-to-cad/build123d reference when the Studio-loaded base documentation identifies a need.",
"The base skill, build123d modeling, and STEP generation references are already returned by route_cad_request for build123d routes.",
"Use this for relevant Markdown files below references/cad-skillx/ or optional positioning/inspection references.",
useCadSkillx
? "Use this for relevant Markdown files below references/cad-skillx/ or optional positioning/inspection references."
: "CAD-SkillX optimization references are disabled; use this only for optional non-cad-skillx references such as positioning or inspection.",
].join(" "),
inputSchema: z.object({
document: z.string().describe("Listed text-to-cad skill document key or references/cad-skillx/... Markdown path."),
}),
execute: async ({ document }) => {
const result = await readTextToCadDocumentation(document);
const result = await readTextToCadDocumentation(document, { cadSkillxEnabled: useCadSkillx });
if (result.ok) textToCadDocsRead.add(result.document);
return result;
},
@@ -1600,7 +1649,9 @@ export async function streamAgentResponse({
});
const result = streamText({
model: languageModel,
system: buildSystemPrompt(viewerContext, uploadedAttachments, taskContext),
system: buildSystemPrompt(viewerContext, uploadedAttachments, taskContext, {
cadSkillxEnabled: useCadSkillx,
}),
tools: cadTools,
stopWhen: isStepCount(16),
messages: modelMessages,