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;
}