File size: 24,967 Bytes
85270f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 | import {
fetchAvailableSkills,
fetchSkillRuntime,
readSkillResource,
runSkillScript,
type InstalledSkill,
type SkillRuntime,
type SkillScriptArtifact,
} from "@/lib/api";
import { type ChatCompletionResponse, type ChatToolPart } from "@/lib/chat-runtime";
import { httpRequest } from "@/lib/request";
export type SkillCapability = "chat" | "web_search" | "image_generation" | "image_edit";
export type SkillImageWorkflow = {
version: 1 | 2;
summary: string;
tasks: Array<{
id: string;
title?: string;
operation?: "source_or_generate" | "generate" | "edit";
reference?: string;
prompt: string;
size?: string;
}>;
postprocess?: {
script: string;
args: string[];
title?: string;
inputs?: string[];
};
};
export type PreparedSkillInvocation = {
skill: InstalledSkill;
runtime: SkillRuntime;
request: string;
capability: SkillCapability;
prompt: string;
summary: string;
toolParts: ChatToolPart[];
artifacts: SkillScriptArtifact[];
plannerContext: string;
imageWorkflow?: SkillImageWorkflow;
awaitingInput?: {
missing: Array<{ id: string; label: string; type: string }>;
question: string;
};
};
type SkillAgentDecision = {
action: "read_resource" | "run_script" | "complete";
path?: string;
script?: string;
args?: string[];
capability?: SkillCapability;
prompt?: string;
summary?: string;
};
type MediaParserPayload = {
success?: boolean;
summary?: Record<string, unknown>;
results?: Array<Record<string, unknown>>;
};
function assistantText(response: ChatCompletionResponse) {
const value = response.choices?.[0]?.message?.content;
if (typeof value === "string") return value.trim();
if (!Array.isArray(value)) return "";
return value.flatMap((item) => {
if (typeof item === "string") return [item];
if (!item || typeof item !== "object") return [];
const record = item as { text?: unknown; content?: unknown };
if (typeof record.text === "string") return [record.text];
if (typeof record.content === "string") return [record.content];
return [];
}).join("\n").trim();
}
function jsonDecision(value: string): SkillAgentDecision {
const clean = value.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
const start = clean.indexOf("{");
const end = clean.lastIndexOf("}");
if (start < 0 || end <= start) {
return { action: "complete", capability: "chat", prompt: clean, summary: "Skill 已完成处理" };
}
try {
return JSON.parse(clean.slice(start, end + 1)) as SkillAgentDecision;
} catch {
return { action: "complete", capability: "chat", prompt: clean, summary: "Skill 已完成处理" };
}
}
function invocationId(prefix: string, index: number) {
return `${prefix}-${Date.now()}-${index}`;
}
function requiredResourcePaths(runtime: SkillRuntime) {
const known = runtime.resources
.filter((item) => item.readable && item.path !== "SKILL.md")
.map((item) => item.path);
const trigger = /\b(?:read|load|consult|review)\b|(?:读取|阅读|加载|查阅)/i;
const required = new Set<string>();
for (const line of runtime.content.split(/\r?\n/)) {
if (!trigger.test(line)) continue;
for (const path of known) {
if (line.includes(path)) required.add(path);
}
}
return [...required];
}
function markdownEnumeration(path: string, content: string) {
const block = content.match(/(?:^|\n)##\s+Contents\s*\n([\s\S]*?)(?=\n##\s|$)/i)?.[1] || "";
const items = [...block.matchAll(/^\s*[-*]\s+(.+?)\s*$/gm)]
.map((match) => match[1].replace(/\s+[-–—].*$/, "").trim())
.filter((item) => item && !/(?:blending|mixing)\s+rules?|混合规则/i.test(item));
if (items.length < 2) return "";
return `<skill_variants source="${path}">${JSON.stringify(items)}</skill_variants>`;
}
function buildPlannerContext(runtime: SkillRuntime, readContents: Map<string, string>) {
const sources = new Map<string, string>([["SKILL.md", runtime.content], ...readContents.entries()]);
const prioritized = [...sources.entries()].sort(([left], [right]) => {
const score = (path: string) => /director-grammars|reference-gallery|color-signatures/i.test(path) ? 0 : 1;
return score(left) - score(right) || left.localeCompare(right);
});
const variants = prioritized.map(([path, content]) => markdownEnumeration(path, content)).filter(Boolean);
const resources = prioritized.map(([path, content]) => (
`<skill_resource path="${path}">\n${content}\n</skill_resource>`
)).join("\n\n").slice(0, 60_000);
return [...variants, resources].filter(Boolean).join("\n\n");
}
function parseImageWorkflow(value: string): SkillImageWorkflow | undefined {
try {
const raw = JSON.parse(value) as Partial<SkillImageWorkflow>;
if (![1, 2].includes(Number(raw.version)) || !Array.isArray(raw.tasks) || !raw.tasks.length || raw.tasks.length > 32) return undefined;
const tasks = raw.tasks.flatMap((task) => {
const id = String(task?.id || "").trim();
const prompt = String(task?.prompt || "").trim();
if (!id || !prompt) return [];
const operation = String(task?.operation || "").trim();
if (operation && !["source_or_generate", "generate", "edit"].includes(operation)) return [];
return [{
id,
prompt,
...(task.title ? { title: String(task.title) } : {}),
...(operation ? { operation: operation as "source_or_generate" | "generate" | "edit" } : {}),
...(task.reference ? { reference: String(task.reference).trim() } : {}),
...(task.size ? { size: String(task.size) } : {}),
}];
});
if (tasks.length !== raw.tasks.length || new Set(tasks.map((task) => task.id)).size !== tasks.length) return undefined;
const taskIds = new Set(tasks.map((task) => task.id));
if (tasks.some((task) => task.operation === "edit" && (!task.reference || !taskIds.has(task.reference)))) return undefined;
const postprocess = raw.postprocess && typeof raw.postprocess === "object"
? {
script: String(raw.postprocess.script || "").trim(),
args: Array.isArray(raw.postprocess.args) ? raw.postprocess.args.map(String) : [],
...(raw.postprocess.title ? { title: String(raw.postprocess.title) } : {}),
...(Array.isArray(raw.postprocess.inputs)
? { inputs: raw.postprocess.inputs.map(String).map((item) => item.trim()).filter(Boolean) }
: {}),
}
: undefined;
if (postprocess && (!postprocess.script.startsWith("scripts/") || !postprocess.args.length)) return undefined;
if (postprocess?.inputs?.some((input) => !taskIds.has(input))) return undefined;
return {
version: Number(raw.version) as 1 | 2,
summary: String(raw.summary || "按 Skill 工作流生成独立图片并组装最终成品").trim(),
tasks,
...(postprocess ? { postprocess } : {}),
};
} catch {
return undefined;
}
}
export async function loadSkillPlannerContext(skillId: string) {
const runtime = await fetchSkillRuntime(skillId);
const contents = new Map<string, string>();
const paths = requiredResourcePaths(runtime);
const results = await Promise.allSettled(paths.map(async (path) => {
const resource = await readSkillResource(skillId, path);
return [path, resource.content] as const;
}));
for (const result of results) {
if (result.status === "fulfilled") contents.set(result.value[0], result.value[1]);
}
return buildPlannerContext(runtime, contents);
}
export async function resolveInstalledSkill(prompt: string, forcedSkillId = "") {
const skills = (await fetchAvailableSkills()).items;
const mentions = [...prompt.matchAll(/\$([a-z0-9._-]+)/gi)];
const match = forcedSkillId
? null
: mentions.find((item) => !["search", "chat", "image", "image-generation"].includes(item[1].toLowerCase()));
const requested = (forcedSkillId || match?.[1] || "").toLowerCase();
if (!requested) return null;
const skill = skills.find((item) => item.id.toLowerCase() === requested || item.name.toLowerCase() === requested);
if (!skill) return null;
const request = match && match.index !== undefined
? `${prompt.slice(0, match.index)}${prompt.slice(match.index + match[0].length)}`.replace(/\s{2,}/g, " ").trim()
: prompt.trim();
return { skill, request };
}
export async function prepareInstalledSkill({
prompt,
model,
attachmentFiles,
attachmentSummary,
invocationKey,
forcedSkillId = "",
conversationContext = "",
onProgress,
}: {
prompt: string;
model: string;
attachmentFiles: File[];
attachmentSummary: Array<{ name: string; type: string; size: number }>;
invocationKey: string;
forcedSkillId?: string;
conversationContext?: string;
onProgress?: (parts: ChatToolPart[]) => void | Promise<void>;
}): Promise<PreparedSkillInvocation | null> {
const resolved = await resolveInstalledSkill(prompt, forcedSkillId);
if (!resolved) return null;
const runtime = await fetchSkillRuntime(resolved.skill.id);
const toolParts: ChatToolPart[] = [];
const artifacts: SkillScriptArtifact[] = [];
const requiredInputs = runtime.presentation.inputs.filter((input) => input.required);
const missingInputs = requiredInputs.filter((input) => {
const type = input.type.toLowerCase();
if (["image", "photo"].includes(type)) return !attachmentFiles.some((file) => file.type.startsWith("image/"));
if (["file", "document", "pdf", "archive", "video", "audio"].includes(type)) return attachmentFiles.length === 0;
if (["text", "prompt", "request"].includes(type)) return !resolved.request.trim();
return false;
});
if (missingInputs.length) {
const labels = missingInputs.map((input) => input.label);
const question = `开始使用「${runtime.name}」前,还需要:${labels.join("、")}。请补充或上传后继续。`;
return {
skill: resolved.skill,
runtime,
request: resolved.request,
capability: "chat",
prompt: question,
summary: question,
toolParts,
artifacts,
plannerContext: "",
awaitingInput: {
missing: missingInputs.map((input) => ({ id: input.id, label: input.label, type: input.type })),
question,
},
};
}
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [
{
role: "system",
content: [
"You are a Skill runtime controller. Follow the installed SKILL.md exactly and prepare one executable action for the host application.",
"You can request one tool action at a time. Return JSON only, with no Markdown fences.",
'Read tool: {"action":"read_resource","path":"relative/path"}. Read every resource the Skill explicitly requires before execution.',
'Script tool: {"action":"run_script","script":"scripts/name.py","args":["..."]}. Only choose a listed, allowed script. Use {{input:0}}, {{output:name.ext}}, {{workspace}}, {{outputs}}, or {{skill}} for paths.',
'Complete action: {"action":"complete","capability":"chat|web_search|image_generation|image_edit","prompt":"fully prepared downstream instruction","summary":"short user-facing summary"}.',
"The host has native chat, web search, image generation, and image editing. Prefer those native capabilities when the Skill says to prefer a built-in capability; do not run a provider fallback script in that case.",
"Use image_edit when an uploaded image must be transformed. Use image_generation only without a source image.",
"Never claim a resource was read or a script ran until its tool result is supplied. Do not expose credentials, private environment variables, or internal authentication.",
"Do not silently invent unresolved options when the Skill requires user choice. In that case complete with capability chat and a concise question in prompt.",
].join("\n"),
},
{
role: "user",
content: JSON.stringify({
skill: {
id: runtime.id,
name: runtime.name,
description: runtime.description,
instructions: runtime.content,
resources: runtime.resources.map((item) => ({ path: item.path, size: item.size, readable: item.readable })),
scripts: runtime.scripts.map((item) => ({ path: item.path, kind: item.kind, allowed: item.allowed })),
},
user_request: resolved.request,
conversation_context: conversationContext || undefined,
attachments: attachmentSummary,
}),
},
];
const readPaths = new Set<string>();
const readContents = new Map<string, string>();
const scriptEvidence: string[] = [];
let imageWorkflow: SkillImageWorkflow | undefined;
let scriptRuns = 0;
let lastDecision: SkillAgentDecision | null = null;
const readResource = async (path: string, key: string) => {
const tool: ChatToolPart = {
type: "tool",
toolId: `skill_read_resource_${key}`,
invocationId: invocationId("skill-resource", toolParts.length),
status: "running",
title: `正在读取 Skill 资源 · ${path}`,
input: { skill: runtime.id, path },
};
toolParts.push(tool);
await onProgress?.([...toolParts]);
try {
const resource = await readSkillResource(runtime.id, path);
readPaths.add(path);
readContents.set(path, resource.content);
Object.assign(tool, { status: "success", title: `已读取 Skill 资源 · ${path}`, output: { path, size: resource.size } });
messages.push({ role: "user", content: `<tool_result tool="read_resource" path="${path}">\n${resource.content}\n</tool_result>` });
} catch (error) {
Object.assign(tool, { status: "error", error: error instanceof Error ? error.message : "读取失败" });
await onProgress?.([...toolParts]);
throw error;
}
await onProgress?.([...toolParts]);
};
const workflowPath = runtime.resources.find((item) => item.path.toLowerCase() === "skill-workflow.json" && item.readable)?.path;
if (workflowPath) {
await readResource(workflowPath, "host_workflow");
imageWorkflow = parseImageWorkflow(readContents.get(workflowPath) || "");
if (imageWorkflow) {
// A host workflow is already an executable, validated plan. Returning it
// directly avoids both the Skill-controller LLM and the image-planner LLM,
// which add minutes of latency and can accidentally discard postprocess.
return {
skill: resolved.skill,
runtime,
request: resolved.request,
capability: attachmentFiles.length ? "image_edit" : "image_generation",
prompt: imageWorkflow.summary,
summary: imageWorkflow.summary,
toolParts,
artifacts,
plannerContext: buildPlannerContext(runtime, readContents),
imageWorkflow,
};
}
}
const mandatoryResources = requiredResourcePaths(runtime).filter((path) => !readPaths.has(path));
for (let index = 0; index < mandatoryResources.length; index += 1) {
await readResource(mandatoryResources[index], `required_${index}`);
}
// Media parsing is an execution Skill, not a prompt-writing Skill. Route
// it deterministically through its approved CLI so the model cannot replace
// a real download with a narrated/fabricated JSON response.
if (runtime.id.toLowerCase() === "media-parser") {
const script = runtime.scripts.find((item) => (
item.allowed && item.path.replace(/\\/g, "/").endsWith("/media_parser_cli.py")
));
if (!script) throw new Error("media-parser 的执行入口尚未获得管理员授权");
const noDownload = /(?:不|不要|无需|仅|只).{0,12}下载|只.{0,12}(?:直链|元数据)|(?:直链|元数据).{0,12}(?:不|不要)下载/i.test(resolved.request);
// Always parse remote media URLs without saving the payload to this server.
// The backend turns those signed CDN URLs into expiring streamed artifacts,
// forwarding platform-required headers (notably Bilibili Referer/User-Agent).
const args = [resolved.request, "--no-download", "--pretty"];
const tool: ChatToolPart = {
type: "tool",
toolId: "skill_run_script_media_parser",
invocationId: invocationId("skill-script", toolParts.length),
status: "running",
title: noDownload ? "正在解析媒体链接" : "正在解析可下载直链",
input: { skill: runtime.id, script: script.path, args, attachmentCount: attachmentFiles.length },
};
toolParts.push(tool);
await onProgress?.([...toolParts]);
try {
const result = await runSkillScript(runtime.id, script.path, args, attachmentFiles, `${invocationKey}:media-parser`);
let parsed: MediaParserPayload | undefined;
try {
parsed = JSON.parse(result.stdout) as MediaParserPayload;
} catch {
parsed = undefined;
}
const parserErrors = Array.isArray(parsed?.results)
? parsed.results.map((item) => String(item.error || "").trim()).filter(Boolean)
: [];
if (result.status !== "success" || parsed?.success === false) {
throw new Error(parserErrors.join(";") || result.stderr || result.stdout || "媒体解析失败");
}
artifacts.push(...result.artifacts);
Object.assign(tool, {
status: "success",
title: noDownload ? "媒体链接解析完成" : `媒体直链已就绪 · ${result.artifacts.length} 个文件`,
output: {
exitCode: result.exit_code,
durationMs: result.duration_ms,
summary: parsed?.summary,
results: parsed?.results,
artifacts: result.artifacts,
},
});
await onProgress?.([...toolParts]);
const summary = noDownload
? "媒体链接已真实解析完成"
: `媒体解析已完成,共生成 ${result.artifacts.length} 个可流式下载文件`;
return {
skill: resolved.skill,
runtime,
request: resolved.request,
capability: "chat",
prompt: [
"media-parser 已由系统真实执行完成。请依据下面的结构化结果简洁汇总标题、作者和成功/失败数量。",
noDownload
? "用户要求只解析,不要声称已经下载文件。"
: "可下载媒体已经作为本条回复的临时流式附件提供;明确告诉用户可点击附件上的下载按钮直接从源站保存,不要声称服务器已经保存完整媒体。",
`解析结果:\n${result.stdout}`,
result.artifacts.length ? `真实下载附件:\n${JSON.stringify(result.artifacts)}` : "",
].filter(Boolean).join("\n\n"),
summary,
toolParts,
artifacts,
plannerContext: buildPlannerContext(runtime, readContents),
};
} catch (error) {
Object.assign(tool, { status: "error", error: error instanceof Error ? error.message : "媒体解析失败" });
await onProgress?.([...toolParts]);
throw error;
}
}
for (let step = 0; step < 12; step += 1) {
const response = await httpRequest<ChatCompletionResponse>("/v1/chat/completions", {
method: "POST",
body: { model: model || "auto", messages, stream: false },
});
const raw = assistantText(response);
const decision = jsonDecision(raw);
lastDecision = decision;
messages.push({ role: "assistant", content: JSON.stringify(decision) });
if (decision.action === "read_resource") {
const path = String(decision.path || "").trim();
if (!path) throw new Error("Skill 请求读取资源,但没有提供路径");
if (readPaths.has(path)) {
messages.push({
role: "user",
content: `<tool_result tool="read_resource" path="${path}" cached="true">\n${readContents.get(path) || ""}\n</tool_result>`,
});
continue;
}
await readResource(path, `agent_${step}`);
continue;
}
if (decision.action === "run_script") {
const script = String(decision.script || "").trim();
if (!script) throw new Error("Skill 请求执行脚本,但没有提供脚本路径");
if (scriptRuns >= 3) throw new Error("单轮 Skill 最多执行 3 次脚本");
scriptRuns += 1;
const args = Array.isArray(decision.args) ? decision.args.map(String) : [];
const tool: ChatToolPart = {
type: "tool",
toolId: `skill_run_script_${step}`,
invocationId: invocationId("skill-script", step),
status: "running",
title: `正在执行 Skill 脚本 · ${script}`,
input: { skill: runtime.id, script, args, attachmentCount: attachmentFiles.length },
};
toolParts.push(tool);
await onProgress?.([...toolParts]);
try {
const result = await runSkillScript(runtime.id, script, args, attachmentFiles, `${invocationKey}:${step}`);
artifacts.push(...result.artifacts);
scriptEvidence.push([
`脚本:${script}`,
`状态:${result.status}(退出码 ${result.exit_code})`,
result.stdout ? `标准输出:\n${result.stdout}` : "",
result.stderr ? `标准错误:\n${result.stderr}` : "",
result.artifacts.length ? `产物:${JSON.stringify(result.artifacts)}` : "",
].filter(Boolean).join("\n"));
Object.assign(tool, {
status: result.status === "success" ? "success" : "error",
title: result.status === "success" ? `Skill 脚本已完成 · ${script}` : `Skill 脚本执行失败 · ${script}`,
output: {
exitCode: result.exit_code,
durationMs: result.duration_ms,
stdout: result.stdout,
stderr: result.stderr,
artifacts: result.artifacts,
},
...(result.status === "error" ? { error: result.stderr || `脚本退出码 ${result.exit_code}` } : {}),
});
messages.push({ role: "user", content: `<tool_result tool="run_script" script="${script}">\n${JSON.stringify(result)}\n</tool_result>` });
} catch (error) {
Object.assign(tool, { status: "error", error: error instanceof Error ? error.message : "脚本执行失败" });
await onProgress?.([...toolParts]);
throw error;
}
await onProgress?.([...toolParts]);
continue;
}
if (decision.action === "complete") {
const capability: SkillCapability = ["chat", "web_search", "image_generation", "image_edit"].includes(String(decision.capability))
? decision.capability as SkillCapability
: attachmentFiles.length ? "image_edit" : "chat";
const preparedPrompt = String(decision.prompt || resolved.request).trim();
const resourceEvidence = [...readContents.entries()]
.map(([path, content]) => `资源:${path}\n${content}`)
.join("\n\n---\n\n")
.slice(0, 18_000);
const promptForHost = capability === "chat"
? [
"你是已安装 Skill 的最终执行模型。下面的 Skill 说明和资源已经由系统真实读取并提供给你;不要声称无法访问、无法加载或要求用户换环境。",
"请直接完成原始用户请求,不要只复述计划、征询下一步或把工具结果说成是用户提供的。只有当 Skill 明确要求必须先由用户选择且原请求没有给出选择时,才提出一个必要问题。",
`已安装 Skill「${runtime.name}」的 SKILL.md:\n${runtime.content.slice(0, 20_000)}`,
resourceEvidence ? `已读取的 Skill 资源:\n${resourceEvidence}` : "",
conversationContext ? `与本次 Skill 延续相关的对话上下文:\n${conversationContext}` : "",
`原始用户请求:\n${resolved.request}`,
`Skill 控制器给出的执行指令:\n${preparedPrompt}`,
scriptEvidence.length
? `已真实执行的 Skill 脚本结果(这是工具证据,不是用户输入):\n${scriptEvidence.join("\n\n---\n\n")}`
: "",
"基于以上内容给出结构清晰、可直接使用的最终交付。用户要求文件时必须创建真实文件附件;若兼容接口不能创建文件,至少返回完整的 fenced SVG/HTML 源码,系统会自动转成可预览附件。若工具已返回具体数值、颜色、字体、布局或文件,必须在答案中呈现。",
].filter(Boolean).join("\n\n")
: preparedPrompt;
return {
skill: resolved.skill,
runtime,
request: resolved.request,
capability,
prompt: promptForHost,
summary: String(decision.summary || `已按 Skill「${runtime.name}」准备执行`).trim(),
toolParts,
artifacts,
plannerContext: buildPlannerContext(runtime, readContents),
imageWorkflow,
};
}
}
throw new Error(`Skill 执行超过最大步骤数${lastDecision ? `(最后动作:${lastDecision.action})` : ""}`);
}
|