chore: commit workspace updates
This commit is contained in:
@@ -1,17 +1,13 @@
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import type { ResponseFunctionTool } from "@/services/api/image";
|
||||
import {
|
||||
getSkillBundle,
|
||||
getSkillFile,
|
||||
listSkillFiles,
|
||||
searchSkillFiles,
|
||||
type Skill,
|
||||
type SkillPackageBundle,
|
||||
type SkillPackageFile,
|
||||
} from "@/services/api/skills";
|
||||
|
||||
export type SkillRuntimeProfile = "canvas" | "creation" | "shortDrama" | "director" | "onlineAgent" | "localAgent";
|
||||
export type SkillRuntimeDelivery = "linked-context" | "progressive-tools" | "native-package";
|
||||
export type SkillRuntimeProfile = "canvas" | "creation" | "shortDrama" | "director";
|
||||
export type SkillRuntimeDelivery = "linked-context";
|
||||
|
||||
type SkillRuntimeProfileConfig = {
|
||||
delivery: SkillRuntimeDelivery;
|
||||
@@ -25,8 +21,6 @@ export const SKILL_RUNTIME_PROFILES: Record<SkillRuntimeProfile, SkillRuntimePro
|
||||
creation: { delivery: "linked-context", maxSkills: 4, maxContextChars: 32_000, maxLinkedFilesPerSkill: 3 },
|
||||
shortDrama: { delivery: "linked-context", maxSkills: 4, maxContextChars: 32_000, maxLinkedFilesPerSkill: 3 },
|
||||
director: { delivery: "linked-context", maxSkills: 4, maxContextChars: 32_000, maxLinkedFilesPerSkill: 3 },
|
||||
onlineAgent: { delivery: "progressive-tools", maxSkills: 0, maxContextChars: 0, maxLinkedFilesPerSkill: 0 },
|
||||
localAgent: { delivery: "native-package", maxSkills: 4, maxContextChars: 0, maxLinkedFilesPerSkill: 0 },
|
||||
};
|
||||
|
||||
export type SkillRuntimeVersion = {
|
||||
@@ -61,29 +55,11 @@ export type LinkedSkillRuntimeResult = {
|
||||
metadata: SkillRuntimeMetadata;
|
||||
};
|
||||
|
||||
export type NativeSkillPackage = {
|
||||
skillId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
files: Array<{ path: string; mimeType: string; contentBase64: string }>;
|
||||
};
|
||||
|
||||
export type NativeSkillRuntimeResult = {
|
||||
delivery: "native-package";
|
||||
prompt: string;
|
||||
selectedSkills: Skill[];
|
||||
skills: NativeSkillPackage[];
|
||||
provenance: SkillRuntimeProvenance;
|
||||
metadata: SkillRuntimeMetadata;
|
||||
};
|
||||
|
||||
type SkillRuntimeResultByProfile = {
|
||||
canvas: LinkedSkillRuntimeResult;
|
||||
creation: LinkedSkillRuntimeResult;
|
||||
shortDrama: LinkedSkillRuntimeResult;
|
||||
director: LinkedSkillRuntimeResult;
|
||||
localAgent: NativeSkillRuntimeResult;
|
||||
};
|
||||
|
||||
export type PrepareSkillRuntimeInput<P extends keyof SkillRuntimeResultByProfile> = {
|
||||
@@ -98,8 +74,6 @@ export type SkillRuntimeToolResult = { ok: true; message: string; data?: unknown
|
||||
type SkillRuntimeDependencies = {
|
||||
getFile: typeof getSkillFile;
|
||||
listFiles: typeof listSkillFiles;
|
||||
searchFiles: typeof searchSkillFiles;
|
||||
getBundle: typeof getSkillBundle;
|
||||
};
|
||||
|
||||
type PreparedSkillInput = {
|
||||
@@ -112,31 +86,10 @@ type SkillDeliveryAdapter<TResult> = {
|
||||
prepare: (input: PreparedSkillInput) => Promise<TResult>;
|
||||
};
|
||||
|
||||
type SkillToolAdapter = {
|
||||
tools: ResponseFunctionTool[];
|
||||
toolNames: ReadonlySet<string>;
|
||||
executeTool: (name: string, args: Record<string, unknown>, skills: Skill[]) => Promise<SkillRuntimeToolResult | null>;
|
||||
};
|
||||
|
||||
const SKILL_REF_PATTERN = /@\[skill:([^\]]+)\]/g;
|
||||
const TEXT_FILE_EXTENSIONS = new Set([".md", ".mdx", ".txt", ".json", ".yaml", ".yml", ".toml", ".csv"]);
|
||||
const EMPTY_PROVENANCE: SkillRuntimeProvenance = { skillIds: [], skillVersions: [], skillFiles: [] };
|
||||
|
||||
export const SKILL_RUNTIME_AGENT_GUIDANCE =
|
||||
"技能采用渐进式读取:canvas_list_skills 只发现元数据;canvas_get_skill 只读取入口 SKILL.md;入口引用其他文件时,再调用 canvas_list_skill_files、canvas_search_skill_files 或 canvas_read_skill_file。禁止一次性读取整个技能包。";
|
||||
|
||||
function functionTool(name: string, description: string, properties: Record<string, unknown>, required: string[] = []): ResponseFunctionTool {
|
||||
return { type: "function", function: { name, description, parameters: { type: "object", properties, required, additionalProperties: false }, strict: false } };
|
||||
}
|
||||
|
||||
const PROGRESSIVE_SKILL_TOOLS: ResponseFunctionTool[] = [
|
||||
functionTool("canvas_list_skills", "自动发现当前用户已加入的技能;可按 query 检索名称、描述或标签,offset/limit 分页;只返回元数据。", { query: { type: "string" }, offset: { type: "number" }, limit: { type: "number" } }),
|
||||
functionTool("canvas_get_skill", "按 skillId 或名称读取技能元数据和入口 SKILL.md;不会加载 references、scripts、assets 等其他文件。", { skillId: { type: "string" }, name: { type: "string" } }),
|
||||
functionTool("canvas_list_skill_files", "列出一个技能包的文件路径、类型和大小,不读取文件正文。", { skillId: { type: "string" }, name: { type: "string" } }),
|
||||
functionTool("canvas_read_skill_file", "按路径读取技能包中的一个文本文件。先从入口引用或文件清单确定路径,禁止猜测路径。", { skillId: { type: "string" }, name: { type: "string" }, path: { type: "string" } }, ["path"]),
|
||||
functionTool("canvas_search_skill_files", "在一个技能包的文本文件中搜索关键词,返回路径、行号和短片段,不返回整包正文。", { skillId: { type: "string" }, name: { type: "string" }, query: { type: "string" } }, ["query"]),
|
||||
];
|
||||
|
||||
export function resolveSkillMentions(prompt: string, skills: Skill[], selectedSkillIds?: string[]) {
|
||||
const activeSkills = skills.filter((skill) => skill.isAdded);
|
||||
if (!activeSkills.length) return [];
|
||||
@@ -183,22 +136,12 @@ export function skillRuntimeMetadata(provenance: SkillRuntimeProvenance): SkillR
|
||||
export function createSkillRuntime(dependencies: SkillRuntimeDependencies = {
|
||||
getFile: getSkillFile,
|
||||
listFiles: listSkillFiles,
|
||||
searchFiles: searchSkillFiles,
|
||||
getBundle: getSkillBundle,
|
||||
}) {
|
||||
const linkedContextAdapter: SkillDeliveryAdapter<LinkedSkillRuntimeResult> = {
|
||||
prepare: (input) => prepareLinkedContext(input, dependencies),
|
||||
};
|
||||
const nativePackageAdapter: SkillDeliveryAdapter<NativeSkillRuntimeResult> = {
|
||||
prepare: (input) => prepareNativePackages(input, dependencies),
|
||||
};
|
||||
const progressiveToolsAdapter = createProgressiveToolsAdapter(dependencies);
|
||||
const deliveryAdapters: Record<Exclude<SkillRuntimeDelivery, "progressive-tools">, SkillDeliveryAdapter<LinkedSkillRuntimeResult | NativeSkillRuntimeResult>> = {
|
||||
const deliveryAdapters: Record<SkillRuntimeDelivery, SkillDeliveryAdapter<LinkedSkillRuntimeResult>> = {
|
||||
"linked-context": linkedContextAdapter,
|
||||
"native-package": nativePackageAdapter,
|
||||
};
|
||||
const toolAdapters: Partial<Record<SkillRuntimeDelivery, SkillToolAdapter>> = {
|
||||
"progressive-tools": progressiveToolsAdapter,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -209,16 +152,6 @@ export function createSkillRuntime(dependencies: SkillRuntimeDependencies = {
|
||||
if (!adapter) throw new Error(`技能运行模式 ${config.delivery} 不支持直接准备上下文`);
|
||||
return adapter.prepare({ prompt: normalizeSkillTokens(input.prompt, input.skills), selectedSkills, config }) as Promise<SkillRuntimeResultByProfile[P]>;
|
||||
},
|
||||
agentTools(profile: SkillRuntimeProfile) {
|
||||
return toolAdapters[SKILL_RUNTIME_PROFILES[profile].delivery]?.tools || [];
|
||||
},
|
||||
agentToolNames(profile: SkillRuntimeProfile) {
|
||||
return toolAdapters[SKILL_RUNTIME_PROFILES[profile].delivery]?.toolNames || new Set<string>();
|
||||
},
|
||||
executeAgentTool(profile: SkillRuntimeProfile, name: string, args: Record<string, unknown>, skills: Skill[]) {
|
||||
const adapter = toolAdapters[SKILL_RUNTIME_PROFILES[profile].delivery];
|
||||
return adapter ? adapter.executeTool(name, args, skills) : Promise.resolve(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -320,100 +253,6 @@ function linkedResult(prompt: string, selectedSkills: Skill[], provenance: Skill
|
||||
return { delivery: "linked-context", prompt, selectedSkills, provenance, metadata: skillRuntimeMetadata(provenance) };
|
||||
}
|
||||
|
||||
async function prepareNativePackages(input: PreparedSkillInput, dependencies: SkillRuntimeDependencies): Promise<NativeSkillRuntimeResult> {
|
||||
const bundles = await Promise.all(input.selectedSkills.map((skill) => dependencies.getBundle(skill.skillId)));
|
||||
const skills = bundles.map((result, index) => nativePackage(input.selectedSkills[index], result.bundle));
|
||||
const provenance = {
|
||||
skillIds: input.selectedSkills.map((skill) => skill.skillId),
|
||||
skillVersions: input.selectedSkills.map((skill, index) => ({ skillId: skill.skillId, versionId: bundles[index].bundle.versionId, version: bundles[index].bundle.version })),
|
||||
skillFiles: bundles.flatMap((result, index) => result.bundle.files.map((file) => ({ skillId: input.selectedSkills[index].skillId, path: file.path }))),
|
||||
};
|
||||
return { delivery: "native-package", prompt: input.prompt, selectedSkills: input.selectedSkills, skills, provenance, metadata: skillRuntimeMetadata(provenance) };
|
||||
}
|
||||
|
||||
function nativePackage(skill: Skill, bundle: SkillPackageBundle): NativeSkillPackage {
|
||||
return {
|
||||
skillId: skill.skillId,
|
||||
name: skill.skillName,
|
||||
description: skill.description,
|
||||
version: bundle.version,
|
||||
files: bundle.files.map((file) => ({ path: file.path, mimeType: file.mimeType, contentBase64: file.contentBase64 })),
|
||||
};
|
||||
}
|
||||
|
||||
function createProgressiveToolsAdapter(dependencies: SkillRuntimeDependencies): SkillToolAdapter {
|
||||
const handlers: Record<string, (args: Record<string, unknown>, skills: Skill[]) => Promise<SkillRuntimeToolResult>> = {
|
||||
canvas_list_skills: async (args, skills) => {
|
||||
const terms = typeof args.query === "string"
|
||||
? args.query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)
|
||||
: [];
|
||||
const matches = skills.filter((skill) => skill.isAdded && terms.every((term) =>
|
||||
`${skill.skillName} ${skill.description} ${skill.tag}`.toLocaleLowerCase().includes(term),
|
||||
));
|
||||
const offset = typeof args.offset === "number" && Number.isFinite(args.offset)
|
||||
? Math.max(0, Math.floor(args.offset))
|
||||
: 0;
|
||||
const limit = typeof args.limit === "number" && Number.isFinite(args.limit)
|
||||
? Math.max(1, Math.min(100, Math.floor(args.limit)))
|
||||
: 40;
|
||||
const items = matches.slice(offset, offset + limit).map((skill) => ({
|
||||
skillId: skill.skillId,
|
||||
name: skill.skillName,
|
||||
description: skill.description,
|
||||
tag: skill.tag,
|
||||
version: skill.version,
|
||||
fileCount: skill.fileCount,
|
||||
sourceType: skill.sourceType,
|
||||
}));
|
||||
return {
|
||||
ok: true,
|
||||
message: items.length ? "已发现相关技能,请按需读取入口;无匹配时可缩短关键词或查询完整目录。" : "没有匹配技能,可调整关键词。",
|
||||
data: {
|
||||
items,
|
||||
total: matches.length,
|
||||
nextOffset: offset + items.length < matches.length ? offset + items.length : null,
|
||||
},
|
||||
};
|
||||
},
|
||||
canvas_get_skill: async (args, skills) => {
|
||||
const skill = requireAddedSkill(skills, args);
|
||||
const entry = await dependencies.getFile(skill.skillId, "SKILL.md");
|
||||
return { ok: true, message: `已读取技能「${skill.skillName}」的入口 SKILL.md;如入口引用其他文件,请继续按需读取。`, data: { skillId: skill.skillId, name: skill.skillName, description: skill.description, version: skill.version, fileCount: skill.fileCount, sourceType: skill.sourceType, entry: entry.file } };
|
||||
},
|
||||
canvas_list_skill_files: async (args, skills) => {
|
||||
const skill = requireAddedSkill(skills, args);
|
||||
const result = await dependencies.listFiles(skill.skillId);
|
||||
return { ok: true, message: `已列出技能「${skill.skillName}」的 ${result.files.length} 个文件,仅含元数据。`, data: { skillId: skill.skillId, files: result.files } };
|
||||
},
|
||||
canvas_read_skill_file: async (args, skills) => {
|
||||
const skill = requireAddedSkill(skills, args);
|
||||
const path = requireString(args.path, "path");
|
||||
const result = await dependencies.getFile(skill.skillId, path);
|
||||
if (result.file.binary) return { ok: false, message: `文件 ${path} 不是可读取的文本文件。` };
|
||||
return { ok: true, message: `已按需读取 ${path}。`, data: { skillId: skill.skillId, file: result.file } };
|
||||
},
|
||||
canvas_search_skill_files: async (args, skills) => {
|
||||
const skill = requireAddedSkill(skills, args);
|
||||
const query = requireString(args.query, "query");
|
||||
const result = await dependencies.searchFiles(skill.skillId, query);
|
||||
return { ok: true, message: result.results.length ? `在技能「${skill.skillName}」中找到 ${result.results.length} 处匹配。` : "技能包中没有匹配内容。", data: { skillId: skill.skillId, query, results: result.results } };
|
||||
},
|
||||
};
|
||||
return {
|
||||
tools: PROGRESSIVE_SKILL_TOOLS,
|
||||
toolNames: new Set(Object.keys(handlers)),
|
||||
executeTool: async (name, args, skills) => {
|
||||
const handler = handlers[name];
|
||||
if (!handler) return null;
|
||||
try {
|
||||
return await handler(args, skills);
|
||||
} catch (error) {
|
||||
return { ok: false, message: error instanceof Error ? error.message : "技能工具执行失败" };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function provenanceFromLoaded(loaded: Array<{ skill: Skill; files: Array<{ path: string; sha256?: string }> }>): SkillRuntimeProvenance {
|
||||
return {
|
||||
skillIds: loaded.map((item) => item.skill.skillId),
|
||||
@@ -431,14 +270,16 @@ function normalizeSkillTokens(prompt: string, skills: Skill[]) {
|
||||
}
|
||||
|
||||
function containsNaturalSkillMention(value: string, name: string) {
|
||||
const token = `@${name}`;
|
||||
let index = 0;
|
||||
while (index < value.length) {
|
||||
const found = value.indexOf(token, index);
|
||||
if (found < 0) return false;
|
||||
const after = found + token.length;
|
||||
if (hasMentionBoundary(value, after)) return true;
|
||||
index = after;
|
||||
for (const prefix of ["@", "/"]) {
|
||||
const token = `${prefix}${name}`;
|
||||
let index = 0;
|
||||
while (index < value.length) {
|
||||
const found = value.indexOf(token, index);
|
||||
if (found < 0) break;
|
||||
const after = found + token.length;
|
||||
if (hasMentionBoundary(value, after)) return true;
|
||||
index = after;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -455,16 +296,3 @@ function normalizePackagePath(path: string) {
|
||||
function escapeAttribute(value: string) {
|
||||
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function requireAddedSkill(skills: Skill[], args: Record<string, unknown>) {
|
||||
const skillId = typeof args.skillId === "string" ? args.skillId.trim() : "";
|
||||
const name = typeof args.name === "string" ? args.name.trim().toLocaleLowerCase() : "";
|
||||
const skill = skills.find((item) => item.isAdded && (item.skillId === skillId || (name && item.skillName.toLocaleLowerCase() === name)));
|
||||
if (!skill) throw new Error("未找到已加入的技能,请先调用 canvas_list_skills。");
|
||||
return skill;
|
||||
}
|
||||
|
||||
function requireString(value: unknown, field: string) {
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} 必须是非空字符串`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user