feat(canvas-agent): grant audited project tools and pause video authorization
This commit is contained in:
parent
3ad7fd6b45
commit
b619005c07
@ -1,3 +1,4 @@
|
||||
import { PROJECT_AGENT_OPERATIONS, projectOperationAuthorized } from "@/lib/ai/canvas-agent-runtime/project-operation-catalog";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
@ -28,6 +29,8 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
workspace: project.workspace,
|
||||
projectServices: PROJECT_AGENT_OPERATIONS.map(item => ({ ...item, authorized: projectOperationAuthorized(item) })),
|
||||
utilityTools: ["getWeather", "createDocument", "updateDocument", "requestSuggestions", "loadProjectSkill"],
|
||||
capabilities: getCanvasAgentAccessManifest(config).filter(
|
||||
(item) => item.workspace === (project.workspace === "video" ? "video" : "canvas"),
|
||||
),
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { registerProjectAgentTools } from "@/lib/ai/canvas-agent-runtime/project-tools";
|
||||
import { isZcodeWorkspaceAuthorized } from "@/lib/ai/canvas-agent-runtime/authorization";
|
||||
import {
|
||||
createUIMessageStream,
|
||||
createUIMessageStreamResponse,
|
||||
@ -79,8 +81,10 @@ export async function POST(request: Request) {
|
||||
const [ownedProject] = await db.select({ userId: canvasProject.userId, workspaceType: canvasProject.workspaceType })
|
||||
.from(canvasProject).where(eq(canvasProject.id, projectId)).limit(1);
|
||||
if (!ownedProject || ownedProject.userId !== session.user.id) throw new AgentRunError("画布不存在", 404);
|
||||
if (!isZcodeWorkspaceAuthorized(ownedProject.workspaceType)) throw new AgentRunError("视频画布暂未授权给 ZCode Agent,请使用手工功能。", 403);
|
||||
const isVideo = ownedProject.workspaceType === "video";
|
||||
const preset = body.agentPresetId ? await getAccessibleAgentPresetById({ id: body.agentPresetId, userId: session.user.id }) : null;
|
||||
if (preset && String(preset.type) === "video") throw new AgentRunError("视频技能暂未授权给 ZCode Agent", 403);
|
||||
if (body.agentPresetId && (!preset || preset.status !== "active")) throw new AgentRunError("此 Agent 预设不可用", 403);
|
||||
const objective = body.message.parts
|
||||
.filter((part) => part.type === "text")
|
||||
@ -306,6 +310,8 @@ export async function POST(request: Request) {
|
||||
),
|
||||
emit,
|
||||
});
|
||||
await registerProjectAgentTools({ session, projectId, request, objective: snapshot.objective,
|
||||
definitions: tools.definitions, executable: tools.executable, emit });
|
||||
const gateway = await createZcodeModelGateway(configured);
|
||||
activeModelController = new AbortController();
|
||||
const status = await runZcodeCanvasTask({
|
||||
|
||||
@ -65,6 +65,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ZCODE_VIDEO_CANVAS_AUTHORIZED } from "@/lib/ai/canvas-agent-runtime/authorization";
|
||||
import { CanvasAgentUiListener } from "./chat-panel/canvas-agent-ui-listener";
|
||||
import type { CanvasAgentUiCommand } from "@/lib/ai/canvas-agent-runtime/ui-command";
|
||||
import { VideoCanvasAgentPanel } from "./chat-panel/video-canvas-agent-panel";
|
||||
@ -11556,7 +11557,7 @@ export function LiblibVideoCanvasReplica({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isVideoAgentOpen ? (
|
||||
{ZCODE_VIDEO_CANVAS_AUTHORIZED && (isVideoAgentOpen ? (
|
||||
<VideoCanvasAgentPanel
|
||||
key={canvasId}
|
||||
projectId={canvasId}
|
||||
@ -11577,7 +11578,7 @@ export function LiblibVideoCanvasReplica({
|
||||
>
|
||||
<FileText className="h-4 w-4" />视频 Agent
|
||||
</button>
|
||||
)}
|
||||
))}
|
||||
|
||||
<TopBar
|
||||
draftTitle={draftTitle}
|
||||
|
||||
@ -35,3 +35,5 @@ CanvasAgentRun 保存 ZCode sessionId、sandbox 类型/构建、调用记录、
|
||||
- 每项 registry/config 功能有对应可调用工具或明确真实交互入口,缺后端明确报错。
|
||||
- 图片/视频 Agent 走新入口,Claw 路由不变;服务端阻止旧 Agent 请求静默回退自研循环。
|
||||
- 先本地与 48 的私有测试实例验证,再切换生产;保留旧构建回滚。
|
||||
|
||||
Current authorization: video canvas and storyboard Agent access is paused by the user. Image canvas and non-video project tools remain authorized; the real project workspace is checked server-side, so forged client mode cannot enable video. Full per-entry audit: authorization-audit.json / authorization-audit.html.
|
||||
|
||||
1
docs/specs/zcode-canvas/authorization-audit.html
Normal file
1
docs/specs/zcode-canvas/authorization-audit.html
Normal file
File diff suppressed because one or more lines are too long
3327
docs/specs/zcode-canvas/authorization-audit.json
Normal file
3327
docs/specs/zcode-canvas/authorization-audit.json
Normal file
File diff suppressed because it is too large
Load Diff
5
lib/ai/canvas-agent-runtime/authorization.ts
Normal file
5
lib/ai/canvas-agent-runtime/authorization.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/** User decision: video canvas authorization is paused; manual video features are unchanged. */
|
||||
export const ZCODE_VIDEO_CANVAS_AUTHORIZED = false;
|
||||
export function isZcodeWorkspaceAuthorized(workspace: string) {
|
||||
return workspace !== "video" || ZCODE_VIDEO_CANVAS_AUTHORIZED;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { isZcodeWorkspaceAuthorized } from "./authorization";
|
||||
import { CANVAS_CAPABILITY_REGISTRY } from "@/lib/canvas/capabilities/registry";
|
||||
import { DEFAULT_CANVAS_TOOLBAR_ITEMS } from "@/lib/canvas-toolbar-config";
|
||||
import {
|
||||
@ -9,7 +10,7 @@ export type AgentCapabilityAccess = {
|
||||
id: string;
|
||||
workspace: "canvas" | "video";
|
||||
title: string;
|
||||
authorized: true;
|
||||
authorized: boolean;
|
||||
previouslyExposed: boolean;
|
||||
execution: "server" | "interactive" | "backend_required";
|
||||
entry: string;
|
||||
@ -19,12 +20,13 @@ export type AgentCapabilityAccess = {
|
||||
// The full audit manifest is for the report/API. Repeating audit metadata and UI
|
||||
// descriptions in model history can exhaust context before the first edit.
|
||||
export function summarizeAgentCapabilities(items: AgentCapabilityAccess[]) {
|
||||
return items.map(({ id, title, execution, entry, note }) => ({
|
||||
return items.map(({ id, title, execution, entry, note, authorized }) => ({
|
||||
id,
|
||||
title,
|
||||
authorized,
|
||||
execution,
|
||||
...(execution === "server" ? { entry } : {}),
|
||||
...(execution === "backend_required" ? { note } : {}),
|
||||
...(authorized && execution === "server" ? { entry } : {}),
|
||||
...(execution === "backend_required" || !authorized ? { note } : {}),
|
||||
}));
|
||||
}
|
||||
export const VIDEO_CANVAS_EXTRA_ACTIONS = [
|
||||
@ -260,5 +262,6 @@ export function getCanvasAgentAccessManifest(
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
];
|
||||
].map(item => ({ ...item, authorized: isZcodeWorkspaceAuthorized(item.workspace),
|
||||
...(!isZcodeWorkspaceAuthorized(item.workspace) ? { note: ["用户暂未授权视频画布能力", item.note].filter(Boolean).join(";") } : {}) }));
|
||||
}
|
||||
|
||||
89
lib/ai/canvas-agent-runtime/project-operation-catalog.ts
Normal file
89
lib/ai/canvas-agent-runtime/project-operation-catalog.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/** Fixed business operations, never an arbitrary URL or administrator API proxy. */
|
||||
export type ProjectAgentOperation = {
|
||||
id: string; title: string; group: string; path: string; method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
hint: string; readOnly: boolean; result: "json"; authorized: true;
|
||||
};
|
||||
export const PROJECT_AGENT_OPERATIONS: ProjectAgentOperation[] = [];
|
||||
function add(group: string, id: string, title: string, path: string, methods: string, hint: string) {
|
||||
for (const method of methods.split(",") as ProjectAgentOperation["method"][]) PROJECT_AGENT_OPERATIONS.push({
|
||||
id: `${id}.${method.toLowerCase()}`, title, group, path, method, hint, readOnly: method === "GET", result: "json", authorized: true,
|
||||
});
|
||||
}
|
||||
add("技能", "skills", "可访问技能列表", "/api/agents", "GET", "query: categoryId、search(可选)。执行技能前用 loadProjectSkill 读取真实提示词和参数,再由当前原生核心编排已有工具。");
|
||||
add("技能", "skill-categories", "技能分类", "/api/agent-categories", "GET", "无需参数");
|
||||
add("素材", "materials", "商品 SKU/SPU 素材检索", "/api/agent-workflow/material-search", "GET", "query: keyword必填,source=primary/secondary、queryType=sku/spu、requiredCount可选");
|
||||
add("素材", "references", "参考图检索", "/api/agent-workflow/reference-search", "GET", "query: keyword、source、queryType;以接口返回的实际素材为准");
|
||||
add("素材", "appearance", "素材外观识别", "/api/agent-workflow/material-appearance", "POST", "body: imageUrls等图片参数,接口验证服务配置与格式");
|
||||
add("素材", "third-party-materials", "第三方图片素材检索", "/api/third-party-image-materials/search", "GET", "query: keyword、page、pageSize;沿用站点数据权限");
|
||||
add("素材", "gallery", "图库素材", "/api/gallery", "GET", "query: page、pageSize、category、keyword可选");
|
||||
add("素材", "featured", "精选素材", "/api/featured", "GET", "query: category、page可选");
|
||||
add("素材", "ranking", "素材排行榜", "/api/featured/ranking", "GET", "query: type、limit可选");
|
||||
add("PSD", "psd-templates", "PSD 模板列表", "/api/psd-templates", "GET", "query: category、keyword可选");
|
||||
add("PSD", "psd-template", "PSD 模板图层信息", "/api/psd-templates/[id]", "GET", "pathParams: id=模板编号");
|
||||
add("PSD", "psd-render", "PSD 真正渲染与任务状态", "/api/psd-templates/render", "GET,POST", "POST body: templateId必填,imageAssignments/textAssignments/hiddenLayerIds/layerOrderIds/imagePlacements等;GET query: taskId。pending/running只代表已受理,必须轮询到succeeded且有result.url才可交付。");
|
||||
add("详情与设计", "detail-plan", "详情页与 UI 设计规划", "/api/canvas/detail-plan", "POST", "body: prompt、图片和设计上下文;遵循接口校验");
|
||||
add("详情与设计", "detail-review", "详情结果验收", "/api/canvas/detail-review", "POST", "body: prompt、imageUrls等真实设计图信息");
|
||||
add("详情与设计", "detail-subject", "详情主体分析", "/api/canvas/detail-subject-analyze", "POST", "body: 图片与产品信息");
|
||||
add("详情与设计", "ui-review", "UI 页面验收", "/api/canvas/ui-screen/review", "POST", "body: 真实页面设计文档与审核要求");
|
||||
add("详情与设计", "text-copy", "图片文案建议", "/api/canvas/text-copy-suggestions", "POST", "body: imageUrl、prompt等文案上下文");
|
||||
add("详情与设计", "detail-stitch", "详情图片拼接", "/api/agent-workflow/detail-stitch", "POST", "body: images或imageUrls等拼接参数");
|
||||
add("详情与设计", "detail-normalize", "详情图片归一化", "/api/agent-workflow/detail-image-normalize", "POST", "body: 图片链接与尺寸参数");
|
||||
add("详情与设计", "style-models", "风格化可用模型", "/api/style-pro/model-options", "GET", "无需参数,不返回密钥");
|
||||
add("详情与设计", "style-generate", "风格化 PRO 生成", "/api/style-pro/generate", "POST", "body: 模型、提示词与参考图,遵守用户指定模型;不改站点配置");
|
||||
add("详情与设计", "wash-models", "AI 图像处理可用模型", "/api/ai-wash/model-options", "GET", "无需参数");
|
||||
add("详情与设计", "wash-generate", "AI 图像处理", "/api/ai-wash/generate", "POST", "body: 用户图像与参数;结果未确认不能重提收费任务");
|
||||
add("商品策划", "planning-image", "商品图片分析", "/api/product-planning/analyze-image", "POST", "body: imageUrl与产品上下文;底层功能开关仍生效");
|
||||
add("商品策划", "planning-sku", "商品 SKU 分析", "/api/product-planning/analyze-sku", "POST", "body: sku与用户商品信息");
|
||||
add("商品策划", "planning-analysis", "评价与问大家分析", "/api/product-planning/analyze", "POST", "body: kind=review/qa,rows为真实评价或问答数组");
|
||||
add("商品策划", "planning-keywords", "关键词洞察", "/api/product-planning/keyword-insight", "GET", "query: keyword等检索参数");
|
||||
for (const platform of ["taobao", "jd"]) add("商品策划", `planning-${platform}-item`, `${platform} 商品详情`, `/api/product-planning/${platform}/item`, "GET", "query: itemId等商品编号");
|
||||
add("商品策划", "planning-reviews", "淘宝真实评价", "/api/product-planning/taobao/reviews", "GET", "query: itemId、page等");
|
||||
add("商品策划", "planning-sales", "淘宝月销量", "/api/product-planning/taobao/month-sale", "GET", "query: itemId");
|
||||
add("商品策划", "planning-qa", "淘宝问大家", "/api/product-planning/taobao/qa", "POST", "body: itemId等商品编号");
|
||||
add("商品策划", "planning-market-items", "平台商品检索", "/api/product-planning/marketplace/items", "POST", "body: platform、keyword等检索参数");
|
||||
add("商品策划", "planning-templates", "策划模板列表与保存", "/api/product-planning/templates", "GET,POST", "POST body: title与策划内容;只保存当前用户自己的模板");
|
||||
add("商品策划", "planning-template", "策划模板读取与删除", "/api/product-planning/templates/[id]", "GET,DELETE", "pathParams: id;删除需要用户明确指定此编号");
|
||||
add("流程白板", "flow-files", "白板文件列表与创建", "/api/flow-board/files", "GET,POST", "POST body: title、snapshot;snapshot必须为真实白板快照");
|
||||
add("流程白板", "flow-file", "白板文件读取、修改、删除", "/api/flow-board/files/[id]", "GET,PUT,DELETE", "pathParams: id;PUT body: title、snapshot;先读取原文件,保留无关节点和工作表;删除需明确指定编号");
|
||||
add("流程白板", "flow-draft", "白板草稿读取与保存", "/api/flow-board/draft", "GET,PUT", "PUT body: title、snapshot;只操作当前用户草稿");
|
||||
add("分镜任务", "video-capabilities", "视频模型能力与参数", "/api/video-model-capabilities", "GET", "query: modelId等;沿用用户模型");
|
||||
add("分镜任务", "video-tasks", "用户视频任务列表", "/api/video-tasks", "GET", "query: page、limit等;仅当前用户任务");
|
||||
add("分镜任务", "storyboard-task", "分镜任务与阶段推进", "/api/video/storyboard/tasks/[taskId]", "GET", "pathParams: taskId;读取实际状态后再按接口动作推进,不重启已有收费任务");
|
||||
add("分镜任务", "storyboard-confirm", "确认用户分镜参数", "/api/video/storyboard/tasks/[taskId]/confirm", "POST", "pathParams: taskId;body: 用户实际确认的镜头与参数,不虚构用户确认");
|
||||
add("分镜任务", "storyboard-cancel", "停止指定分镜任务", "/api/video/storyboard/tasks/[taskId]/cancel", "POST", "pathParams: taskId;需用户明确要求停止此编号");
|
||||
add("分镜任务", "storyboard-shot", "分镜镜头修订", "/api/video/storyboard/tasks/[taskId]/shots/[shotIndex]", "PATCH", "pathParams: taskId、shotIndex;body: 用户要求的镜头修改");
|
||||
add("分镜任务", "storyboard-hd", "指定镜头高清处理", "/api/video/storyboard/tasks/[taskId]/shots/[shotIndex]/regenerate-hd", "POST", "pathParams: taskId、shotIndex;会产生费用,只有用户要求高清才执行");
|
||||
add("项目", "agent-projects", "Agent 创作项目列表与创建", "/api/agent-projects", "GET,POST", "POST body: title等项目数据;不写入其他用户项目");
|
||||
add("项目", "agent-project", "Agent 创作项目读写", "/api/agent-projects/[id]", "GET,PUT,DELETE", "pathParams: id;修改先读当前内容,删除需用户明确指定编号");
|
||||
add("项目", "canvas-list", "用户画布列表与创建", "/api/canvas", "GET,POST", "POST body: title、workspaceType=canvas/video等");
|
||||
add("项目", "canvas-read", "读取用户画布", "/api/canvas/[id]", "GET", "pathParams: id;只读取当前用户有权限的画布,节点写入使用专用画布工具");
|
||||
add("项目", "detail-archives", "画布详情归档", "/api/canvas/[id]/detail-archives", "GET,POST,PATCH,DELETE", "pathParams: id=当前画布编号;body/query按详情归档接口,删除需明确授权");
|
||||
add("项目", "model-assets", "3D 资产归档", "/api/canvas/[id]/model3d-assets", "GET,POST,PATCH,DELETE", "pathParams: id=当前画布编号;只处理拥有权限的资产");
|
||||
add("3D", "3d-task", "3D 生成任务状态", "/api/canvas/3d/tasks/[taskId]", "GET", "pathParams: taskId;生成用已有3D专用能力,不重复提交");
|
||||
add("3D", "world-task", "场景生成任务状态", "/api/canvas/world/tasks/[taskId]", "GET", "pathParams: taskId;生成用已有场景专用能力");
|
||||
add("智能编辑", "smart-edit", "智能图层编辑能力", "/api/canvas/smart-edit/capabilities", "GET", "无需参数;仅当该部署版本已有此功能时可用");
|
||||
add("文档", "documents", "读取与保存用户文档", "/api/document", "GET,POST", "query: id=文档UUID;POST body: title、kind=text/code/sheet/image、content。更新前先读取原文档,不操作其他用户文档。");
|
||||
add("图像处理", "enhance-pro", "专业高清处理任务与查询", "/api/enhance/pro", "GET,POST", "POST body按接口实际模型与尺寸要求,GET query: taskId。仅任务成功后才能交付,不能重复付费提交。");
|
||||
export function projectOperationAuthorized(operation: ProjectAgentOperation) {
|
||||
return operation.group !== "分镜任务";
|
||||
}
|
||||
export function projectOperationUrl(operation: ProjectAgentOperation, pathParams: Record<string, string>, query: Record<string, unknown>) {
|
||||
const path = operation.path.replace(/\[([^\]]+)\]/g, (_all, key: string) => {
|
||||
const value = pathParams[key];
|
||||
if (!value || !/^[a-zA-Z0-9_-]{1,180}$/.test(value)) throw new Error(`缺少或无效的路径参数:${key}`);
|
||||
return encodeURIComponent(value);
|
||||
});
|
||||
const values = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (!/^[\w.-]{1,80}$/.test(key) || !["string", "number", "boolean"].includes(typeof value)) throw new Error("查询参数格式无效");
|
||||
values.set(key, String(value));
|
||||
}
|
||||
return path + (values.size ? `?${values}` : "");
|
||||
}
|
||||
export function sanitizeProjectToolOutput(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(sanitizeProjectToolOutput);
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value)
|
||||
.filter(([key]) => !/^(?:api[-_]?key|access[-_]?key|secret(?:[-_]?key)?|token|password(?:hash)?|ak|sk|accessKeyId|secretAccessKey|clientSecret|accessToken|refreshToken|authorization|cookie|headers|credentials|providerConfig)$/i.test(key))
|
||||
.map(([key, entry]) => [key, sanitizeProjectToolOutput(entry)]));
|
||||
return value;
|
||||
}
|
||||
873
lib/ai/canvas-agent-runtime/project-operation-contracts.json
Normal file
873
lib/ai/canvas-agent-runtime/project-operation-contracts.json
Normal file
@ -0,0 +1,873 @@
|
||||
{
|
||||
"skills.get": {
|
||||
"source": "app/api/agents/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"listQuerySchema": "z.object({\n scope: z.enum([\"all\", \"public\", \"private\"]).default(\"all\"),\n type: z.enum([\"image\", \"video\", \"text\"]).optional(),\n sectionId: z.string().trim().max(64).optional(),\n level1Id: z.string().trim().max(64).optional(),\n level2Id: z.string().trim().max(64).optional(),\n level3Id: z.string().trim().max(64).optional(),\n productCategory: z.string().trim().max(64).optional(),\n audienceTag: z.string().trim().max(64).optional(),\n usageScenario: z.string().trim().max(64).optional(),\n brand: z.string().trim().max(64).optional(),\n keyword: z.string().trim().max(128).optional(),\n sort: z.enum([\"latest\", \"hottest\", \"most_used\"]).default(\"latest\"),\n})",
|
||||
"slotSchema": "z.object({\n title: z.string().trim().max(128).optional(),\n promptTemplate: z.string().trim().min(1).max(8000),\n defaultParams: z.record(z.string(), z.any()).optional(),\n requireUpload: z.boolean().optional(),\n uploadLabel: z.string().trim().max(128).optional(),\n referenceAssetIds: z.array(z.string().uuid()).max(12).optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"scope",
|
||||
"type",
|
||||
"sectionId",
|
||||
"level1Id",
|
||||
"level2Id",
|
||||
"level3Id",
|
||||
"productCategory",
|
||||
"audienceTag",
|
||||
"usageScenario",
|
||||
"brand",
|
||||
"keyword",
|
||||
"sort"
|
||||
]
|
||||
},
|
||||
"skill-categories.get": {
|
||||
"source": "app/api/agent-categories/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"scopeSchema": "z.enum([\"all\", \"public\", \"private\"]).default(\"public\")",
|
||||
"createCategorySchema": "z.object({\n name: z.string().trim().min(1).max(64),\n scope: z.enum([\"public\", \"private\"]).default(\"public\"),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"scope"
|
||||
]
|
||||
},
|
||||
"materials.get": {
|
||||
"source": "app/api/agent-workflow/material-search/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {
|
||||
"querySchema": "z.object({\n keyword: z.string().trim().min(1).max(64),\n source: z.enum([\"primary\", \"secondary\"]).optional(),\n queryType: z.enum([\"sku\", \"spu\"]).optional(),\n requiredCount: z.coerce.number().int().min(1).max(4).optional(),\n appearanceRecognitionEnabled: z.enum([\"true\", \"false\", \"1\", \"0\"]).optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"keyword",
|
||||
"source",
|
||||
"queryType",
|
||||
"requiredCount",
|
||||
"appearanceRecognitionEnabled"
|
||||
]
|
||||
},
|
||||
"references.get": {
|
||||
"source": "app/api/agent-workflow/reference-search/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {
|
||||
"productDetailChannelSchema": "z.preprocess((value) => {\n const normalized = typeof value === \"string\" ? value.trim() : \"\";\n return normalized || undefined;\n}, z.enum([\"taobao\", \"1688\", \"douyin\", \"jd\"]).optional())",
|
||||
"querySchema": "z.object({\n keyword: z.string().trim().min(1).max(64),\n queryType: z.enum([\"sku\", \"spu\", \"productId\"]).default(\"spu\"),\n target: z.enum([\"main\", \"detail\", \"sku\"]),\n brandName: z.string().trim().max(64).optional(),\n channel: productDetailChannelSchema,\n strictQueryType: z.enum([\"true\", \"false\", \"1\", \"0\"]).optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"item_id",
|
||||
"channel",
|
||||
"keyword",
|
||||
"queryType",
|
||||
"target",
|
||||
"brandName",
|
||||
"strictQueryType"
|
||||
]
|
||||
},
|
||||
"appearance.post": {
|
||||
"source": "app/api/agent-workflow/material-appearance/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"payloadSchema": "z.object({\n imageUrl: z.string().trim().url().max(2000),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"third-party-materials.get": {
|
||||
"source": "app/api/third-party-image-materials/search/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {
|
||||
"querySchema": "z.object({\n resourceType: z.enum([\"white-background\", \"task-detail\"]).default(\"white-background\"),\n sku: z.string().trim().max(64).optional(),\n spu: z.string().trim().max(64).optional(),\n keyword: z.string().trim().max(64).optional(),\n queryType: z.enum([\"sku\", \"spu\"]).optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"resourceType",
|
||||
"sku",
|
||||
"spu",
|
||||
"keyword",
|
||||
"queryType"
|
||||
]
|
||||
},
|
||||
"gallery.get": {
|
||||
"source": "app/api/gallery/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"page",
|
||||
"pageSize"
|
||||
]
|
||||
},
|
||||
"featured.get": {
|
||||
"source": "app/api/featured/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"page",
|
||||
"pageSize"
|
||||
]
|
||||
},
|
||||
"ranking.get": {
|
||||
"source": "app/api/featured/ranking/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"psd-templates.get": {
|
||||
"source": "app/api/psd-templates/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"brand",
|
||||
"mode",
|
||||
"summary"
|
||||
]
|
||||
},
|
||||
"psd-template.get": {
|
||||
"source": "app/api/psd-templates/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"psd-render.get": {
|
||||
"source": "app/api/psd-templates/render/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"psd-render.post": {
|
||||
"source": "app/api/psd-templates/render/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"detail-plan.post": {
|
||||
"source": "app/api/canvas/detail-plan/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"detail-review.post": {
|
||||
"source": "app/api/canvas/detail-review/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"detail-subject.post": {
|
||||
"source": "app/api/canvas/detail-subject-analyze/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"ui-review.post": {
|
||||
"source": "app/api/canvas/ui-screen/review/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"text-copy.post": {
|
||||
"source": "app/api/canvas/text-copy-suggestions/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"requestSchema": "z.object({\n imageUrl: z.string().min(1, \"图片地址不能为空\"),\n originalText: z.string().min(1, \"原文不能为空\"),\n currentText: z.string().optional(),\n title: z.string().optional(),\n pageTexts: z.array(z.string()).optional(),\n audience: z.string().optional(),\n usageScenario: z.string().optional(),\n coreNeed: z.string().optional(),\n productName: z.string().optional(),\n workflowSummary: z.string().optional(),\n workflowPrompt: z.string().optional(),\n userPrompt: z.string().optional(),\n presetName: z.string().optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"detail-stitch.post": {
|
||||
"source": "app/api/agent-workflow/detail-stitch/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"detail-normalize.post": {
|
||||
"source": "app/api/agent-workflow/detail-image-normalize/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"style-models.get": {
|
||||
"source": "app/api/style-pro/model-options/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"style-generate.post": {
|
||||
"source": "app/api/style-pro/generate/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"generateRequestSchema": "z.object({\n canvasId: z.string().trim().min(1).max(160).optional(),\n requestId: z.string().uuid().optional(),\n sourceImageUrl: z.string().trim().min(1).max(4000),\n generationMode: z.enum([\"style\", \"realistic\"]).optional(),\n modelId: z.string().trim().min(1).max(160).optional(),\n selections: z.record(z.array(z.string().trim().min(1).max(120)).max(20)),\n customInputs: z\n .object({\n global: z.record(z.string().max(2000)).optional(),\n categories: z.record(z.record(z.string().max(2000))).optional(),\n })\n .optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"wash-models.get": {
|
||||
"source": "app/api/ai-wash/model-options/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"wash-generate.post": {
|
||||
"source": "app/api/ai-wash/generate/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"requestSchema": "z.object({\n prompt: z.string().min(1).max(5000),\n referenceImage: z.string().min(1).max(200000),\n selectedModelId: z.string().min(1).max(128),\n imageSize: z.string().min(1).max(16).optional(),\n promptExtend: z.boolean().optional(),\n aspectRatio: z.string().min(1).max(16).optional(),\n title: z.string().max(128).optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-image.post": {
|
||||
"source": "app/api/product-planning/analyze-image/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"requestSchema": "z.object({\n itemId: z.string().trim().min(1),\n imageUrl: z.string().trim().url(),\n title: z.string().trim().optional(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-sku.post": {
|
||||
"source": "app/api/product-planning/analyze-sku/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"requestSchema": "z.object({\n itemId: z.string().trim().min(1),\n title: z.string().trim().optional(),\n platform: z.enum([\"taobao\", \"jd\"]),\n skus: z\n .array(\n z.object({\n name: z.string().trim().min(1),\n price: z.number().nullable(),\n })\n )\n .min(1),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-analysis.post": {
|
||||
"source": "app/api/product-planning/analyze/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"requestSchema": "z.union([\n z.object({\n kind: z.literal(\"review\"),\n rows: z\n .array(\n z.object({\n itemId: z.string(),\n date: z.string(),\n content: z.string(),\n appendComment: z.string().optional(),\n sku: z.string().optional(),\n userId: z.string().optional(),\n images: z.array(z.string()),\n videoUrls: z.array(z.string()),\n })\n )\n .min(1),\n }),\n z.object({\n kind: z.literal(\"qa\"),\n rows: z\n .array(\n z.object({\n itemId: z.string(),\n question: z.string(),\n questionDetail: z.string().optional(),\n answer: z.string(),\n answerUser: z.string().optional(),\n answerDate: z.string().optional(),\n isOfficial: z.boolean(),\n })\n )\n .min(1),\n }),\n])"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-keywords.get": {
|
||||
"source": "app/api/product-planning/keyword-insight/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"q"
|
||||
]
|
||||
},
|
||||
"planning-taobao-item.get": {
|
||||
"source": "app/api/product-planning/taobao/item/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"planning-jd-item.get": {
|
||||
"source": "app/api/product-planning/jd/item/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"planning-reviews.get": {
|
||||
"source": "app/api/product-planning/taobao/reviews/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"id",
|
||||
"page"
|
||||
]
|
||||
},
|
||||
"planning-sales.get": {
|
||||
"source": "app/api/product-planning/taobao/month-sale/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"planning-qa.post": {
|
||||
"source": "app/api/product-planning/taobao/qa/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"requestSchema": "z.object({\n itemId: z.string().trim().min(1),\n page: z.number().int().min(1).max(20).default(1),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-market-items.post": {
|
||||
"source": "app/api/product-planning/marketplace/items/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-templates.get": {
|
||||
"source": "app/api/product-planning/templates/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-templates.post": {
|
||||
"source": "app/api/product-planning/templates/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-template.get": {
|
||||
"source": "app/api/product-planning/templates/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"planning-template.delete": {
|
||||
"source": "app/api/product-planning/templates/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-files.get": {
|
||||
"source": "app/api/flow-board/files/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-files.post": {
|
||||
"source": "app/api/flow-board/files/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-file.get": {
|
||||
"source": "app/api/flow-board/files/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-file.put": {
|
||||
"source": "app/api/flow-board/files/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-file.delete": {
|
||||
"source": "app/api/flow-board/files/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-draft.get": {
|
||||
"source": "app/api/flow-board/draft/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"flow-draft.put": {
|
||||
"source": "app/api/flow-board/draft/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"video-capabilities.get": {
|
||||
"source": "app/(chat)/api/video-model-capabilities/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"modelId"
|
||||
]
|
||||
},
|
||||
"video-tasks.get": {
|
||||
"source": "app/(chat)/api/video-tasks/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"chatId"
|
||||
]
|
||||
},
|
||||
"storyboard-task.get": {
|
||||
"source": "app/(chat)/api/video/storyboard/tasks/[taskId]/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {
|
||||
"paramsSchema": "z.object({\n taskId: z.string().uuid(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"storyboard-confirm.post": {
|
||||
"source": "app/(chat)/api/video/storyboard/tasks/[taskId]/confirm/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"paramsSchema": "z.object({\n taskId: z.string().uuid(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"storyboard-cancel.post": {
|
||||
"source": "app/(chat)/api/video/storyboard/tasks/[taskId]/cancel/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"paramsSchema": "z.object({\n taskId: z.string().uuid(),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"storyboard-shot.patch": {
|
||||
"source": "app/(chat)/api/video/storyboard/tasks/[taskId]/shots/[shotIndex]/route.ts",
|
||||
"methods": [
|
||||
"PATCH"
|
||||
],
|
||||
"schemas": {
|
||||
"paramsSchema": "z.object({\n taskId: z.string().uuid(),\n shotIndex: z.coerce.number().int().min(1).max(99),\n})",
|
||||
"bodySchema": "z.object({\n prompt: z.string().trim().min(1).max(4000),\n regenerateHd: z.boolean().optional().default(true),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"storyboard-hd.post": {
|
||||
"source": "app/(chat)/api/video/storyboard/tasks/[taskId]/shots/[shotIndex]/regenerate-hd/route.ts",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"schemas": {
|
||||
"paramsSchema": "z.object({\n taskId: z.string().uuid(),\n shotIndex: z.coerce.number().int().min(1).max(99),\n})"
|
||||
},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"agent-projects.get": {
|
||||
"source": "app/api/agent-projects/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"agent-projects.post": {
|
||||
"source": "app/api/agent-projects/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"agent-project.get": {
|
||||
"source": "app/api/agent-projects/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"agent-project.put": {
|
||||
"source": "app/api/agent-projects/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"agent-project.delete": {
|
||||
"source": "app/api/agent-projects/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"canvas-list.get": {
|
||||
"source": "app/api/canvas/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"workspaceType",
|
||||
"limit",
|
||||
"page"
|
||||
]
|
||||
},
|
||||
"canvas-list.post": {
|
||||
"source": "app/api/canvas/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"workspaceType",
|
||||
"limit",
|
||||
"page"
|
||||
]
|
||||
},
|
||||
"canvas-read.get": {
|
||||
"source": "app/api/canvas/[id]/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": []
|
||||
},
|
||||
"detail-archives.get": {
|
||||
"source": "app/api/canvas/[id]/detail-archives/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"PATCH",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"archiveId"
|
||||
]
|
||||
},
|
||||
"detail-archives.post": {
|
||||
"source": "app/api/canvas/[id]/detail-archives/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"PATCH",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"archiveId"
|
||||
]
|
||||
},
|
||||
"detail-archives.patch": {
|
||||
"source": "app/api/canvas/[id]/detail-archives/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"PATCH",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"archiveId"
|
||||
]
|
||||
},
|
||||
"detail-archives.delete": {
|
||||
"source": "app/api/canvas/[id]/detail-archives/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"PATCH",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"archiveId"
|
||||
]
|
||||
},
|
||||
"model-assets.get": {
|
||||
"source": "app/api/canvas/[id]/model3d-assets/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE",
|
||||
"PATCH"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"model-assets.post": {
|
||||
"source": "app/api/canvas/[id]/model3d-assets/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE",
|
||||
"PATCH"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"model-assets.patch": {
|
||||
"source": "app/api/canvas/[id]/model3d-assets/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE",
|
||||
"PATCH"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"model-assets.delete": {
|
||||
"source": "app/api/canvas/[id]/model3d-assets/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE",
|
||||
"PATCH"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"3d-task.get": {
|
||||
"source": "app/api/canvas/3d/tasks/[taskId]/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"providerId"
|
||||
]
|
||||
},
|
||||
"world-task.get": {
|
||||
"source": "app/api/canvas/world/tasks/[taskId]/route.ts",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"providerId"
|
||||
]
|
||||
},
|
||||
"documents.get": {
|
||||
"source": "app/(chat)/api/document/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"id",
|
||||
"timestamp"
|
||||
]
|
||||
},
|
||||
"documents.post": {
|
||||
"source": "app/(chat)/api/document/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"id",
|
||||
"timestamp"
|
||||
]
|
||||
},
|
||||
"enhance-pro.get": {
|
||||
"source": "app/api/enhance/pro/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"enhance-pro.post": {
|
||||
"source": "app/api/enhance/pro/route.ts",
|
||||
"methods": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"schemas": {},
|
||||
"interfaces": {},
|
||||
"queryKeys": [
|
||||
"taskId"
|
||||
]
|
||||
}
|
||||
}
|
||||
94
lib/ai/canvas-agent-runtime/project-tools.ts
Normal file
94
lib/ai/canvas-agent-runtime/project-tools.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import "server-only";
|
||||
import { tool, type ToolSet } from "ai";
|
||||
import type { Session } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import contracts from "./project-operation-contracts.json";
|
||||
import { getAccessibleAgentPresetById, getDocumentById } from "@/lib/db/queries";
|
||||
import { getEnabledTools } from "@/lib/ai/tools-registry";
|
||||
import { getWeather } from "@/lib/ai/tools/get-weather";
|
||||
import { createDocument } from "@/lib/ai/tools/create-document";
|
||||
import { updateDocument } from "@/lib/ai/tools/update-document";
|
||||
import { requestSuggestions } from "@/lib/ai/tools/request-suggestions";
|
||||
import { PROJECT_AGENT_OPERATIONS, projectOperationAuthorized, projectOperationUrl, sanitizeProjectToolOutput } from "./project-operation-catalog";
|
||||
import { isExplicitDestructiveIntent } from "./capability-access";
|
||||
import type { CanvasAgentTool } from "./loop";
|
||||
|
||||
export async function registerProjectAgentTools(params: {
|
||||
session: Session; projectId: string; request: Request; objective: string;
|
||||
definitions: ToolSet; executable: Record<string, CanvasAgentTool>;
|
||||
emit: (type: string, data: unknown) => void;
|
||||
}) {
|
||||
const userId = params.session.user!.id!;
|
||||
function register(name: string, description: string, schema: z.ZodTypeAny, execute: CanvasAgentTool["execute"], readOnly = true) {
|
||||
params.definitions[name] = tool({ description, inputSchema: schema });
|
||||
params.executable[name] = { title: description.slice(0, 32), readOnly, concurrentSafe: readOnly, execute };
|
||||
}
|
||||
register("listProjectServices", "列出图片画布之外的全部项目业务工具、真实接口及输入提示。可按分组筛选;authorized=false的分镜/视频功能暂不授权。", z.object({ group: z.string().max(100).optional(), operationId: z.string().max(120).optional() }),
|
||||
async input => ({ services: PROJECT_AGENT_OPERATIONS.filter(item => (!input.group || item.group === input.group) && (!input.operationId || item.id === input.operationId))
|
||||
.map(item => ({ ...item, authorized: projectOperationAuthorized(item), ...(input.operationId ? { requestContract: (contracts as Record<string, unknown>)[item.id] } : {}) })), legacyTools: ["getWeather", "createDocument", "updateDocument", "requestSuggestions"],
|
||||
limits: "仅当前用户权限;无后台管理、密钥、支付、系统任务和Claw控制权限;文件选择用浏览器入口;异步任务必须轮询真实状态。" }));
|
||||
const invokeSchema = z.object({ operationId: z.string().min(1).max(120), pathParams: z.record(z.string().max(180)).default({}),
|
||||
query: z.record(z.union([z.string().max(4000), z.number(), z.boolean()])).default({}), body: z.record(z.unknown()).optional() });
|
||||
async function invoke(input: Record<string, unknown>, callId: string, readOnly: boolean): Promise<Record<string, unknown>> {
|
||||
const args = invokeSchema.parse(input);
|
||||
const operation = PROJECT_AGENT_OPERATIONS.find(item => item.id === args.operationId);
|
||||
if (!operation || operation.readOnly !== readOnly) throw new Error("请使用清单中对应读写类别的固定业务工具");
|
||||
if (!projectOperationAuthorized(operation)) throw new Error("用户暂未授权视频画布和分镜工具");
|
||||
if (JSON.stringify(args.body || {}).length > 2 * 1024 * 1024) throw new Error("业务工具输入超过大小限制");
|
||||
if (args.body?.workspaceType === "video" || args.query.workspaceType === "video" || args.body?.type === "video") throw new Error("视频画布暂未授权");
|
||||
if (operation.path.startsWith("/api/canvas/[id]")) {
|
||||
if (args.pathParams.id !== params.projectId) throw new Error("画布业务接口仅授权当前画布,不能跨画布读取或写入");
|
||||
}
|
||||
if (operation.method === "DELETE" && (!isExplicitDestructiveIntent(params.objective) || !Object.values(args.pathParams).some(id => params.objective.includes(id))))
|
||||
throw new Error("删除项目文件需要用户明确提出删除,并在要求中指定文件编号;模型不能自行授权");
|
||||
if (operation.id === "canvas-list.post" && !/新建|创建|新增/.test(params.objective)) throw new Error("请先确认需要新建画布");
|
||||
if (args.body?.userId && args.body.userId !== userId) throw new Error("不能指定其他用户身份");
|
||||
const route = projectOperationUrl(operation, args.pathParams, args.query);
|
||||
const incoming = new URL(params.request.url);
|
||||
const port = incoming.port || process.env.PORT || "3125";
|
||||
if (!/^\d{2,5}$/.test(port) || Number(port) > 65535) throw new Error("业务网关端口未配置");
|
||||
const headers = new Headers({ "Content-Type": "application/json", "x-canvas-agent-call-id": callId });
|
||||
// Auth is inherited from the original request; models cannot supply headers,
|
||||
// cookies, another host, URL, user identity or provider credentials.
|
||||
for (const key of ["cookie", "host", "x-forwarded-host", "x-forwarded-proto"])
|
||||
if (params.request.headers.has(key)) headers.set(key, params.request.headers.get(key)!);
|
||||
const response = await fetch(`http://127.0.0.1:${port}${route}`, { method: operation.method, headers, redirect: "error", cache: "no-store",
|
||||
...(operation.method !== "GET" ? { body: JSON.stringify(args.body || {}) } : {}), signal: AbortSignal.timeout(300000) });
|
||||
if (!response.headers.get("content-type")?.includes("application/json")) { await response.body?.cancel(); throw new Error(`此工具未返回可核验JSON结果(${response.status});请使用真实界面处理文件或流式操作`); }
|
||||
const raw = await response.text(); if (raw.length > 2 * 1024 * 1024) throw new Error("业务返回过大,请缩小查询范围");
|
||||
const payload = JSON.parse(raw);
|
||||
if (!response.ok || payload?.success === false || payload?.error) throw new Error(`业务工具 ${operation.title} 未成功(${response.status}):${typeof payload?.error === "string" ? payload.error.slice(0, 200) : "请核对输入参数或底层服务权限"}`);
|
||||
if (operation.id === "canvas-list.get" && Array.isArray(payload?.data)) payload.data = payload.data.filter((row: { workspaceType?: string }) => row.workspaceType !== "video");
|
||||
const status = payload?.processing || ["pending", "running", "queued"].includes(payload?.status) ? "pending" : payload?.status === "failed" ? "failed" : "succeeded";
|
||||
return { projectService: true, operationId: operation.id, status, taskId: payload?.taskId,
|
||||
result: sanitizeProjectToolOutput(payload), message: status === "pending" ? "仅已受理,必须轮询状态确认实际结果;禁止重复创建任务。" : "已获得真实业务接口结果" };
|
||||
}
|
||||
register("readProjectService", "读取项目业务数据。先 listProjectServices 查看 operationId、pathParams和query要求;仅执行已授权GET接口。", invokeSchema, (input, id) => invoke(input, id, true));
|
||||
register("runProjectService", "执行固定项目业务接口。先查清单和真实数据、制定计划;仅当前用户权限。pending表示任务已受理,不能声称完成或重复收费。", invokeSchema, (input, id) => invoke(input, id, false), false);
|
||||
register("loadProjectSkill", "读取用户可访问的技能完整提示词、槽位和默认参数,由当前ZCode核心执行;不会调用另一个旧Agent或自动生成收费内容。", z.object({ presetId: z.string().uuid() }), async input => {
|
||||
const preset = await getAccessibleAgentPresetById({ id: String(input.presetId), userId });
|
||||
if (!preset || preset.status !== "active") throw new Error("此技能不可用或无权访问");
|
||||
if (preset.type === "video") throw new Error("视频技能暂未授权给 ZCode Agent");
|
||||
return { id: preset.id, name: preset.name, type: preset.type, promptTemplate: preset.promptTemplate, defaultModelId: preset.defaultModelId,
|
||||
defaultParams: sanitizeProjectToolOutput(preset.defaultParams), slots: preset.slots.map(slot => ({ title: slot.title, promptTemplate: slot.promptTemplate, defaultParams: sanitizeProjectToolOutput(slot.defaultParams) })),
|
||||
message: "请按用户当前任务采用技能指令,并使用真实已授权工具执行;不得虚构结果。" };
|
||||
});
|
||||
const enabled = new Set(await getEnabledTools());
|
||||
const dataStream = { write: (part: { type: string; data?: unknown }) => params.emit(part.type, part.data) };
|
||||
const legacy: ToolSet = { getWeather, createDocument: createDocument({ session: params.session, dataStream: dataStream as never }),
|
||||
updateDocument: updateDocument({ session: params.session, dataStream: dataStream as never }), requestSuggestions: requestSuggestions({ session: params.session, dataStream: dataStream as never }) };
|
||||
for (const [name, definition] of Object.entries(legacy)) {
|
||||
params.definitions[name] = { ...definition, execute: undefined };
|
||||
params.executable[name] = { title: name, readOnly: name === "getWeather", concurrentSafe: name === "getWeather", execute: async (input, callId) => {
|
||||
if (!enabled.has(name as never)) throw new Error("管理员尚未启用此工具;授权清单不绕过底层服务开关");
|
||||
const id = input.id || input.documentId;
|
||||
if (name !== "getWeather" && name !== "createDocument") { const doc = await getDocumentById({ id: String(id) });
|
||||
if (!doc || doc.userId !== userId) throw new Error("文档不存在或不属于当前用户"); }
|
||||
const output = await definition.execute!(input as never, { toolCallId: callId, messages: [] });
|
||||
if (!output || typeof output !== "object" || "error" in output) throw new Error("业务工具未返回有效结果");
|
||||
if (name !== "getWeather") { const doc = await getDocumentById({ id: String((output as { id?: string }).id) });
|
||||
if (!doc || doc.userId !== userId || !doc.content) throw new Error("文档尚未保存,不能声称完成"); }
|
||||
return { projectService: true, status: "succeeded", ...sanitizeProjectToolOutput(output) as Record<string, unknown> };
|
||||
} };
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
import { isZcodeWorkspaceAuthorized } from "./authorization";
|
||||
/** Agent/Auto Agent/presets always use native ZCode. Claw and direct media modes retain their routes. */
|
||||
export function shouldUseCanvasAgentRuntime(body: {
|
||||
composerMode?: string;
|
||||
workspaceVariant?: string;
|
||||
agentPresetId?: string;
|
||||
resumeRunId?: string;
|
||||
contextPayload?: { source?: string; projectId?: string };
|
||||
message?: { parts?: Array<{ type?: string; text?: string }> };
|
||||
}) {
|
||||
return body.composerMode === "agent" && body.contextPayload?.source === "canvas" && Boolean(body.contextPayload.projectId);
|
||||
return body.composerMode === "agent" && body.contextPayload?.source === "canvas" && Boolean(body.contextPayload.projectId) && isZcodeWorkspaceAuthorized(body.workspaceVariant || "canvas");
|
||||
}
|
||||
|
||||
@ -45,9 +45,10 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
let question = "";
|
||||
const tools = Object.keys(ports.tools);
|
||||
const instructions = [ports.instructions,
|
||||
"你正在独立沙箱内运行原生 ZCode 核心。本站图片/视频/节点/界面能力已通过 canvas MCP 全部授权;先读实际画布,不猜测节点 ID。",
|
||||
"你正在独立沙箱内运行原生 ZCode 核心。本站已授权的图片、节点、界面及项目业务能力通过 canvas MCP 提供;视频画布暂未授权,不得绕过限制;先读实际画布,不猜测节点 ID。",
|
||||
`可用业务工具:${tools.join("、")}。工具名称带 mcp__canvas__ 前缀。`,
|
||||
"原生文件、Bash、记忆及子任务能力仅作用于此任务的私有工作区,不能通过 shell 直接访问模型服务商或本站后台;业务操作使用 MCP。",
|
||||
"查询项目级能力先用 listProjectServices;技能先 loadProjectSkill。项目工具的任务受理、文件选择与后台缺失不等于处理完成。",
|
||||
"需要澄清时调用 requestCanvasUserInput;不要使用终端交互。工具返回的 userUpdates 是用户的新补充,必须从下一步纳入。",
|
||||
"工具错误涉及结果未确认时停止收费动作,不通过更换调用 ID 或另一个工具重复生成。用户明确要求修改的目标才能删除或覆盖。",
|
||||
"只有真实工具返回成功才可声称完成;打开面板、等待用户选文件、后台缺少处理服务都不能声称媒体已处理。",
|
||||
@ -123,6 +124,10 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
await exclusive(async () => {
|
||||
const call = snapshot.calls.find(call => call.id === callId)!;
|
||||
call.status = "succeeded"; call.output = output;
|
||||
if (output.projectService && output.status === "succeeded" && typeof output.taskId === "string") {
|
||||
for (const accepted of snapshot.calls) if (accepted.output?.projectService && accepted.output.taskId === output.taskId && accepted.output.status === "pending")
|
||||
accepted.output = { ...accepted.output, status: "succeeded", verifiedResult: output.result };
|
||||
}
|
||||
if (name === "planCanvasTask") snapshot.requirements = { ...snapshot.requirements,
|
||||
...(typeof output.expectedImageCount === "number" ? { expectedImageCount: output.expectedImageCount } : {}),
|
||||
...(typeof output.aspectRatio === "string" ? { aspectRatio: output.aspectRatio } : {}) };
|
||||
@ -156,6 +161,12 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
return await save(question || "请在已打开的工具面板中完成操作,然后继续任务。", "needs_review");
|
||||
const supplements = snapshot.inputs.filter(input => !startedInputs.has(input.id));
|
||||
if (supplements.length) { prompt = supplements.map(input => input.text).join("\n"); continue; }
|
||||
const projectWrites = snapshot.calls.filter(call => !ports.tools[call.name]?.readOnly && call.output?.projectService);
|
||||
const canvasWrites = snapshot.calls.filter(call => !ports.tools[call.name]?.readOnly && !call.output?.projectService);
|
||||
if (projectWrites.length && !canvasWrites.length && !snapshot.requirements?.expectedImageCount) {
|
||||
const pending = projectWrites.some(call => call.output?.status !== "succeeded");
|
||||
return await save(pending ? "业务任务已受理或等待核对,请读取实际任务状态,保留原任务编号继续。" : result.response || "项目业务操作已完成。", pending ? "needs_review" : "completed");
|
||||
}
|
||||
// Text/analysis tasks have no generated media to inspect. Mutating tasks
|
||||
// still use the existing independent checks against actual saved nodes.
|
||||
const mutated = snapshot.calls.some(call => !ports.tools[call.name]?.readOnly);
|
||||
|
||||
@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/routes",
|
||||
testMatch: /(?:canvas-agent-runtime(?:-store|-review|-video)?|agent-capability-access)\.test\.ts/,
|
||||
testMatch: /(?:canvas-agent-runtime(?:-store|-review|-video)?|canvas-agent-project-tools|agent-capability-access)\.test\.ts/,
|
||||
fullyParallel: true,
|
||||
retries: 0,
|
||||
workers: 2,
|
||||
|
||||
41
scripts/zcode-agent/audit-project-tools.mjs
Normal file
41
scripts/zcode-agent/audit-project-tools.mjs
Normal file
@ -0,0 +1,41 @@
|
||||
import ts from 'typescript';
|
||||
import { readFile, writeFile, access } from 'node:fs/promises';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
const catalog = JSON.parse(execFileSync(process.execPath, ['node_modules/tsx/dist/cli.mjs','-e', "import {PROJECT_AGENT_OPERATIONS} from './lib/ai/canvas-agent-runtime/project-operation-catalog';console.log(JSON.stringify(PROJECT_AGENT_OPERATIONS))"], { encoding: 'utf8' }));
|
||||
const contracts = {}; const mismatch = [];
|
||||
for (const operation of catalog) {
|
||||
let file;
|
||||
for (const root of ['app/api','app/(chat)/api']) {
|
||||
const path = `${root}/${operation.path.slice('/api/'.length)}/route.ts`;
|
||||
try { await access(path); file = path; break; } catch {}
|
||||
}
|
||||
if (!file) { if (operation.id !== 'smart-edit.get') mismatch.push(`${operation.id}: missing route`); continue; }
|
||||
const source = await readFile(file,'utf8');
|
||||
const methods = [...source.matchAll(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)/g)].map(match => match[1]);
|
||||
if (!methods.includes(operation.method)) mismatch.push(`${operation.id}: method not implemented`);
|
||||
const ast = ts.createSourceFile(file,source,ts.ScriptTarget.Latest,true); const schemas = {}, interfaces = {};
|
||||
function walk(node) {
|
||||
if (ts.isVariableDeclaration(node) && node.initializer && /schema$/i.test(node.name.getText(ast)) && /^z\./.test(node.initializer.getText(ast)))
|
||||
schemas[node.name.getText(ast)] = node.initializer.getText(ast).slice(0,6000);
|
||||
if (ts.isInterfaceDeclaration(node) && /request|payload|body/i.test(node.name.getText(ast))) interfaces[node.name.getText(ast)] = node.getText(ast).slice(0,6000);
|
||||
ts.forEachChild(node,walk);
|
||||
}
|
||||
walk(ast);
|
||||
contracts[operation.id] = { source: file, methods, schemas, interfaces,
|
||||
queryKeys: [...new Set([...source.matchAll(/(?:searchParams|params)\.get\(["']([^"']+)["']\)/g)].map(match=>match[1]))] };
|
||||
}
|
||||
if (mismatch.length) throw new Error(mismatch.join('\n'));
|
||||
await writeFile('lib/ai/canvas-agent-runtime/project-operation-contracts.json',JSON.stringify(contracts,null,2)+'\n');
|
||||
console.log(JSON.stringify({ operations: catalog.length, routeContracts: Object.keys(contracts).length, deploymentOnly: 'smart-edit.get', unsupportedMethods: mismatch.length }));
|
||||
|
||||
const canvas = JSON.parse(execFileSync(process.execPath, ['node_modules/tsx/dist/cli.mjs','-e', "import {getCanvasAgentAccessManifest} from './lib/ai/canvas-agent-runtime/capability-access';console.log(JSON.stringify(getCanvasAgentAccessManifest()))"], { encoding: 'utf8' }));
|
||||
const project = catalog.map(item=>({...item, authorized:item.group !== '分镜任务', previouslyExposed:false, execution:'server'}));
|
||||
const utilities = ['getWeather','createDocument','updateDocument','requestSuggestions','loadProjectSkill'].map(id=>({id,title:id,group:'文档、天气与技能',authorized:true,previouslyExposed:false,entry:id,execution:'server'}));
|
||||
const report = { policy:'图片画布及非视频项目业务已授权;视频画布与分镜暂不授权;底层用户权限与服务开关仍生效。管理、密钥、支付、身份认证、系统任务和Claw不属于创作Agent工具。',
|
||||
upstreamCommit:'0a1fc56d279fd1001f78387bd81aec1c26767411', rows:[...canvas,...project,...utilities],
|
||||
verifiedRoutes:Object.keys(contracts).length, deploymentOnly:['smart-edit.get'] };
|
||||
await writeFile('docs/specs/zcode-canvas/authorization-audit.json',JSON.stringify(report,null,2)+'\n');
|
||||
const esc=value=>String(value??'').replace(/[&<>"']/g,char=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
|
||||
const rows=report.rows.map(row=>`<tr><td>${esc(row.workspace||row.group||'项目')}</td><td>${esc(row.title)}</td><td>${row.authorized?'已授权':'暂未授权'}</td><td>${esc(row.entry||row.id)}</td><td>${esc(row.note||row.hint||'')}</td></tr>`).join('');
|
||||
await writeFile('docs/specs/zcode-canvas/authorization-audit.html',`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>ZCode Agent 工具授权清单</title><style>body{font:14px system-ui;margin:32px;color:#222}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:9px;text-align:left}th{background:#f4f4f4;position:sticky;top:0}tr:nth-child(even){background:#fafafa}</style><h1>ZCode Agent 工具授权清单</h1><p>${esc(report.policy)}</p><p>共 ${report.rows.length} 项能力;每一项均保留实际执行方式与限制。接口鉴权不等于业务结果已完成。</p><table><thead><tr><th>范围</th><th>能力</th><th>授权</th><th>工具 / 操作编号</th><th>执行限制</th></tr></thead><tbody>${rows}</tbody></table></html>`);
|
||||
console.log(JSON.stringify({auditRows:report.rows.length, authorized:report.rows.filter(row=>row.authorized).length, paused:report.rows.filter(row=>!row.authorized).length, addedProjectOperations:project.filter(row=>row.authorized).length}));
|
||||
@ -60,3 +60,5 @@ uncertain results. These fixtures do not prove a configured real provider is
|
||||
healthy. A release also needs a real-provider test on a private image and video
|
||||
canvas, authentication/ownership probes and static-resource checks before
|
||||
switching the active Next build.
|
||||
|
||||
Current authorization: video canvas and storyboard Agent access is paused by the user. Image canvas and non-video project tools remain authorized; the real project workspace is checked server-side, so forged client mode cannot enable video. Full per-entry audit: authorization-audit.json / authorization-audit.html.
|
||||
|
||||
@ -111,7 +111,8 @@ test("every registered image capability is authorized to the Agent including loc
|
||||
test("access manifest covers all configured video functions and node/UI actions without granting fake backends", () => {
|
||||
const manifest = getCanvasAgentAccessManifest();
|
||||
expect(new Set(manifest.map((item) => item.id)).size).toBe(manifest.length);
|
||||
expect(manifest.every((item) => item.authorized)).toBe(true);
|
||||
expect(manifest.filter(item => item.workspace === "canvas").every(item => item.authorized)).toBe(true);
|
||||
expect(manifest.filter(item => item.workspace === "video").every(item => !item.authorized)).toBe(true);
|
||||
for (const key of Object.keys(defaultVideoCanvasFunctionConfig.functions))
|
||||
expect(manifest.some((item) => item.id === `video.function.${key}`)).toBe(true);
|
||||
for (const [key] of VIDEO_CANVAS_EXTRA_ACTIONS)
|
||||
@ -119,7 +120,7 @@ test("access manifest covers all configured video functions and node/UI actions
|
||||
for (const [key] of VIDEO_CANVAS_INTERACTIVE_ACTIONS)
|
||||
expect(manifest.some((item) => item.id === `video.ui.${key}`)).toBe(true);
|
||||
expect(manifest.find((item) => item.id === "video.function.audio-to-video")).toMatchObject({
|
||||
authorized: true,
|
||||
authorized: false,
|
||||
execution: "backend_required",
|
||||
});
|
||||
expect(manifest.find((item) => item.id === "video.ui.subtitle-erase")?.note).toContain("模拟");
|
||||
|
||||
32
tests/routes/canvas-agent-project-tools.test.ts
Normal file
32
tests/routes/canvas-agent-project-tools.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { PROJECT_AGENT_OPERATIONS, projectOperationAuthorized, projectOperationUrl, sanitizeProjectToolOutput } from "@/lib/ai/canvas-agent-runtime/project-operation-catalog";
|
||||
import { shouldUseCanvasAgentRuntime } from "@/lib/ai/canvas-agent-runtime/routing";
|
||||
import { getCanvasAgentAccessManifest } from "@/lib/ai/canvas-agent-runtime/capability-access";
|
||||
|
||||
test("business grant uses only fixed implemented route/method pairs and no management or secret endpoints", () => {
|
||||
expect(new Set(PROJECT_AGENT_OPERATIONS.map(item=>item.id)).size).toBe(PROJECT_AGENT_OPERATIONS.length);
|
||||
for (const op of PROJECT_AGENT_OPERATIONS) {
|
||||
expect(op.path).not.toMatch(/\/admin\/|\/internal\/|\/jobs\/|\/auth\/|\/payment\/|access-keys|clawd-chat|agent-chat/);
|
||||
const relative=op.path.slice('/api/'.length)+'/route.ts';
|
||||
const path=['app/api/','app/(chat)/api/'].map(base=>base+relative).find(existsSync);
|
||||
if (!path) { expect(op.id).toBe('smart-edit.get'); continue; }
|
||||
expect(readFileSync(path,'utf8')).toMatch(new RegExp(`export\\s+(?:async\\s+)?function\\s+${op.method}\\b`));
|
||||
}
|
||||
});
|
||||
test("video canvas and storyboard permissions are explicitly paused and never admitted through a forged mode", () => {
|
||||
expect(getCanvasAgentAccessManifest().filter(item=>item.workspace==='video').every(item=>!item.authorized)).toBe(true);
|
||||
expect(PROJECT_AGENT_OPERATIONS.filter(item=>item.group==='分镜任务').every(item=>!projectOperationAuthorized(item))).toBe(true);
|
||||
expect(shouldUseCanvasAgentRuntime({ composerMode:'agent', workspaceVariant:'video', contextPayload:{ source:'canvas',projectId:'p' }})).toBe(false);
|
||||
const route=readFileSync('app/api/canvas/agent-chat/route.ts','utf8');
|
||||
expect(route).toContain('isZcodeWorkspaceAuthorized(ownedProject.workspaceType)');
|
||||
});
|
||||
test("operation URL substitution rejects path escapes and keeps query values encoded", () => {
|
||||
const op=PROJECT_AGENT_OPERATIONS.find(item=>item.id==='flow-file.get')!;
|
||||
expect(()=>projectOperationUrl(op,{id:'../../admin/keys'},{})).toThrow();
|
||||
expect(()=>projectOperationUrl(op,{id:'a%2Fadmin'},{})).toThrow();
|
||||
expect(projectOperationUrl(op,{id:'safe-id'},{keyword:'a&x=http://evil'})).toBe('/api/flow-board/files/safe-id?keyword=a%26x%3Dhttp%3A%2F%2Fevil');
|
||||
});
|
||||
test("nested model/storage secrets cannot be returned to the native model", () => {
|
||||
expect(sanitizeProjectToolOutput({ data:[{ id:'m',apiKey:'secret', provider:{ SK:'hidden',AK:'hidden',secretAccessKey:'hidden',baseUrl:'https://safe' } }], headers:{ authorization:'secret' } })).toEqual({data:[{id:'m',provider:{baseUrl:'https://safe'}}]});
|
||||
});
|
||||
@ -102,10 +102,10 @@ const plan = () => step([call("plan", "planCanvasTask")]);
|
||||
|
||||
test("all image and video canvas Agent requests use native runtime; Claw and direct modes remain separate", () => {
|
||||
const body = { composerMode: "agent", contextPayload: { source: "canvas", projectId: "owned-project" } };
|
||||
for (const override of [{}, { agentPresetId: "preset" }, { workspaceVariant: "video" },
|
||||
for (const override of [{}, { agentPresetId: "preset" },
|
||||
{ message: { parts: [{ type: "text", text: "只分析不执行" }] } }, { resumeRunId: "run" }])
|
||||
expect(shouldUseCanvasAgentRuntime({ ...body, ...override })).toBe(true);
|
||||
for (const override of [{ composerMode: "clawd" }, { composerMode: "image" }, { composerMode: "video" },
|
||||
for (const override of [{ workspaceVariant: "video" }, { composerMode: "clawd" }, { composerMode: "image" }, { composerMode: "video" },
|
||||
{ contextPayload: { source: "canvas" } }, { contextPayload: { source: "chat", projectId: "p" } }])
|
||||
expect(shouldUseCanvasAgentRuntime({ ...body, ...override })).toBe(false);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user