File size: 7,037 Bytes
4f03ac7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | 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;
}
|