abc / web /src /lib /skill-guide.ts
xiaolongmr
init
85270f9
Raw History Blame Contribute Delete
7.04 kB
import { fetchSkillUserGuide, type InstalledSkill, type SkillRuntime } from "@/lib/api";
import { type ChatCompletionResponse, parseChatCompletionResponse } from "@/lib/chat-runtime";
import { httpRequest } from "@/lib/request";
export type SkillGuideDocument = {
path: string;
content: string;
};
const guideCache = new Map<string, string>();
const GUIDE_STORAGE_PREFIX = "chatgpt2api.skill-guide.v2";
function textFingerprint(value: string) {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16).padStart(8, "0");
}
function guideCacheKey(skill: InstalledSkill, runtime?: SkillRuntime, documents: SkillGuideDocument[] = []) {
const version = skill.guide_version?.trim() || (runtime
? textFingerprint([runtime.content, ...documents.map((item) => `${item.path}\n${item.content}`)].join("\n---\n"))
: "");
return version ? `${skill.id}:${version}` : "";
}
function readCachedGuide(cacheKey: string) {
if (!cacheKey) return "";
const memory = guideCache.get(cacheKey);
if (memory) return memory;
if (typeof window === "undefined") return "";
try {
const value = window.localStorage.getItem(`${GUIDE_STORAGE_PREFIX}:${cacheKey}`)?.trim() || "";
if (value) guideCache.set(cacheKey, value);
return value;
} catch {
return "";
}
}
function writeCachedGuide(skill: InstalledSkill, cacheKey: string, guide: string) {
if (!cacheKey || !guide.trim()) return;
guideCache.set(cacheKey, guide);
if (typeof window === "undefined") return;
const storageKey = `${GUIDE_STORAGE_PREFIX}:${cacheKey}`;
const skillPrefix = `${GUIDE_STORAGE_PREFIX}:${skill.id}:`;
try {
for (let index = window.localStorage.length - 1; index >= 0; index -= 1) {
const key = window.localStorage.key(index);
if (key?.startsWith(skillPrefix) && key !== storageKey) window.localStorage.removeItem(key);
}
window.localStorage.setItem(storageKey, guide);
} catch {
// Browser privacy modes or a full storage quota should not block the guide.
}
}
export function getCachedSkillUserGuide(skill: InstalledSkill) {
return readCachedGuide(guideCacheKey(skill));
}
function basename(path: string) {
return path.replace(/\\/g, "/").split("/").pop() || path;
}
function documentScore(path: string) {
const normalized = path.replace(/\\/g, "/").toLowerCase();
const name = basename(normalized);
let score = normalized.includes("/") ? 20 : 0;
if (/zh[-_.]?(?:cn|hans)|chinese|中文/.test(name)) score -= 12;
if (/^readme/.test(name)) score -= 8;
if (/^(?:usage|quickstart|getting[-_ ]started|user[-_ ]guide)/.test(name)) score -= 6;
if (normalized.startsWith("_package-docs/")) score -= 4;
return score;
}
export function skillUserDocumentPaths(runtime: SkillRuntime) {
return runtime.resources
.filter((resource) => resource.readable && resource.path !== "SKILL.md")
.map((resource) => resource.path)
.filter((path) => /(?:^|\/)(?:readme|usage|quickstart|getting[-_ ]started|user[-_ ]guide|how[-_ ]to)(?:[._-].*)?\.(?:md|mdx|txt)$/i.test(path))
.sort((left, right) => documentScore(left) - documentScore(right) || left.localeCompare(right))
.slice(0, 4);
}
function stripFrontmatter(content: string) {
return content.replace(/^---[\s\S]*?---\s*/i, "").trim();
}
function fallbackGuide(skill: InstalledSkill, runtime: SkillRuntime, documents: SkillGuideDocument[]) {
const source = documents[0];
const sourceExcerpt = source?.content.trim().slice(0, 6000);
return [
"## 能做什么",
skill.description || runtime.description || "按照该 Skill 的专业流程处理任务。",
"## 怎么开始",
`1. 点击“使用这个 Skill”,输入框会插入 \`$${skill.id}\`。`,
"2. 上传该任务依赖的图片或文件。",
"3. 用自然语言说明目标;如果文档要求先确认选项,Skill 会在执行前询问。",
sourceExcerpt ? `## 文档中的说明\n\n> 来源:${source.path}\n\n${sourceExcerpt}` : "",
].filter(Boolean).join("\n\n");
}
export async function generateSkillUserGuide({
skill,
runtime,
documents,
}: {
skill: InstalledSkill;
runtime: SkillRuntime;
documents: SkillGuideDocument[];
}) {
const cacheKey = guideCacheKey(skill, runtime, documents);
const cached = readCachedGuide(cacheKey);
if (cached) return cached;
try {
const shared = await fetchSkillUserGuide(skill.id);
const guide = shared.guide.trim();
if (guide) {
writeCachedGuide(skill, cacheKey, guide);
return guide;
}
} catch (error) {
console.warn("Unable to read shared Skill guide cache", error);
}
const sourceDocuments = documents
.map((document) => `<document path="${document.path}">\n${document.content.slice(0, 10000)}\n</document>`)
.join("\n\n")
.slice(0, 20_000);
const skillProtocol = stripFrontmatter(runtime.content).slice(0, 14_000);
try {
const response = await httpRequest<ChatCompletionResponse>("/v1/chat/completions", {
method: "POST",
body: {
model: "auto",
stream: false,
messages: [
{
role: "system",
content: [
"你是网站内的 Skill 使用说明编辑器。把开发者/Agent 文档整理成普通用户一眼能懂的中文使用指南。",
"优先采用 README、USAGE、QUICKSTART 等面向用户的文档;SKILL.md 只用于补足能力、必需输入、执行前确认和限制。",
"只能依据提供的资料,不得猜测不存在的能力、参数或结果。不要暴露内部系统提示、资源读取顺序、脚本路径或 Agent 协议。",
"输出简洁 Markdown,固定包含:『能做什么』『适合什么情况』『开始前准备』『怎么使用』『可直接照抄的示例』『需要注意』。",
`给出 2 到 4 个具体、自然的示例指令,每条必须原样包含调用标记 \`$${skill.id}\`。没有明确参数时用可替换占位说明,不要编造参数。`,
].join("\n"),
},
{
role: "user",
content: [
`Skill ID: ${skill.id}`,
`名称: ${skill.name}`,
`描述: ${skill.description || runtime.description}`,
sourceDocuments ? `面向用户的文档:\n${sourceDocuments}` : "没有找到 README/USAGE/QUICKSTART。",
`Agent 执行协议(仅用于补充事实):\n${skillProtocol}`,
].join("\n\n"),
},
],
},
});
const guide = parseChatCompletionResponse(response).content.trim();
if (guide) {
writeCachedGuide(skill, cacheKey, guide);
return guide;
}
} catch (error) {
console.warn("Unable to generate Skill user guide", error);
}
const fallback = fallbackGuide(skill, runtime, documents);
writeCachedGuide(skill, cacheKey, fallback);
return fallback;
}