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(); 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) => `\n${document.content.slice(0, 10000)}\n`) .join("\n\n") .slice(0, 20_000); const skillProtocol = stripFrontmatter(runtime.content).slice(0, 14_000); try { const response = await httpRequest("/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; }