index.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /**
  2. * skill-core-guide · Browser bundle(无 Node 依赖)
  3. * ---------------------------------------------------------------------------
  4. * 本文件**不得** import 任何 node: 内置模块 —— 只使用 Web 标准 API
  5. * (fetch / TextEncoder / URL / crypto.subtle 等),可直接被
  6. * <script type="module"> 加载或在浏览器打包器中消费。
  7. *
  8. * 导出能力:
  9. * - 平台常量与端点真值表(纯数据)
  10. * - 校验器(纯函数:package.json / manifest / frontmatter / 技能名)
  11. * - 浏览器可跑的质检子集(network 类检查)
  12. *
  13. * 不导出:依赖 fs / child_process 的检查项(functional / sop / loop /
  14. * multiRuntime 的本地执行部分)—— 那些只能在 Node 侧运行。
  15. *
  16. * @example
  17. * <script type="module">
  18. * import { PLATFORM, checkApiConnectivity, validatePackageJson } from './browser/index.mjs';
  19. * const r = await checkApiConnectivity();
  20. * document.body.textContent = r.status + ' — ' + r.detail;
  21. * </script>
  22. */
  23. // ============================================================
  24. // 平台常量(内联,保持浏览器 bundle 零依赖、可独立分发)
  25. // ============================================================
  26. export const VERSION = '1.0.3';
  27. export const SKILL_NAME = 'skill-core-guide';
  28. export const PLATFORM = {
  29. name: 'Fmode Harness',
  30. version: '1.0.0',
  31. apiBase: 'https://api.fmode.cn',
  32. gatewayBase: 'https://server.fmode.cn',
  33. cdnBase: 'https://fmode.cn',
  34. obsBase: 'https://fmode-s3.obs.cn-north-4.myhuaweicloud.com',
  35. gogsBase: 'https://git.fmode.cn',
  36. gogsOrg: 'fmode',
  37. githubOrg: 'fmodecn',
  38. npmRegistry: 'https://registry.npmjs.org',
  39. skillhub: {
  40. host: 'https://api.skillhub.cn',
  41. team: 'fmode',
  42. orgId: 'org-m8z913un',
  43. },
  44. };
  45. export const ENDPOINTS = {
  46. llmChat: {
  47. id: 'llmChat',
  48. method: 'POST',
  49. url: 'https://api.fmode.cn/v1/chat/completions',
  50. status: 'live',
  51. auth: 'Bearer <fmodeApiToken>',
  52. purpose: 'LLM 对话补全(OpenAI 兼容)',
  53. },
  54. imageGenerate: {
  55. id: 'imageGenerate',
  56. method: 'POST',
  57. url: 'https://api.fmode.cn/v1/images/generations',
  58. status: 'live',
  59. auth: 'Bearer <fmodeApiToken>',
  60. purpose: '图像生成',
  61. },
  62. listenTranscribe: {
  63. id: 'listenTranscribe',
  64. method: 'POST',
  65. url: 'https://server.fmode.cn/api/listen/transcribe',
  66. status: 'live',
  67. auth: 'Bearer <fmodeApiToken>',
  68. purpose: '录音转写(讯飞 LFASR)',
  69. },
  70. vocSkillBootstrap: {
  71. id: 'vocSkillBootstrap',
  72. method: 'POST',
  73. url: 'https://server.fmode.cn/api/fmode/voc-skill/install-prompt',
  74. status: 'live',
  75. auth: 'x-parse-session-token: <sessionToken>',
  76. purpose: 'sessionToken → fmode API token 自举',
  77. },
  78. deploySts: {
  79. id: 'deploySts',
  80. method: 'POST',
  81. url: 'https://server.fmode.cn/api/apig/deploy/huaweicloud',
  82. status: 'live',
  83. auth: 'Bearer <sessionToken>',
  84. purpose: '签发项目隔离 OBS STS',
  85. },
  86. verifyCode: {
  87. id: 'verifyCode',
  88. method: 'POST',
  89. url: 'https://server.fmode.cn/api/fmode/verifycode',
  90. status: 'planned',
  91. auth: '无',
  92. purpose: '手机号验证码(未上线)',
  93. },
  94. };
  95. export const RUNTIMES = {
  96. cli: { key: 'cli', label: 'CLI', entry: 'bin/<name>.mjs', usage: 'npx --yes <skill>@latest <command>', supported: true },
  97. sdk: { key: 'sdk', label: 'SDK (Node ESM)', entry: 'lib/index.mjs', usage: "import { ... } from '<skill>'", supported: true },
  98. browser: {
  99. key: 'browser',
  100. label: 'Browser',
  101. entry: 'browser/index.mjs',
  102. usage: '<script type="module" src="...">',
  103. supported: true,
  104. constraint: '禁止 import 任何 node: 内置模块',
  105. },
  106. server: {
  107. key: 'server',
  108. label: 'Server (CJS require)',
  109. entry: null,
  110. usage: "require('<skill>')",
  111. supported: false,
  112. constraint: 'ESM only —— 不提供 CJS 入口',
  113. },
  114. };
  115. export const TIERS = {
  116. system: { key: 'system', label: '系统层 / Infrastructure', desc: '平台基础设施与 Agent 运行时治理' },
  117. service: { key: 'service', label: '服务层 / Platform Services', desc: 'Fmode 基础服务封装' },
  118. application: { key: 'application', label: '应用层 / Business Applications', desc: '面向业务场景的端到端技能' },
  119. };
  120. export const CHANNELS = {
  121. gogs: { key: 'gogs', label: 'Gogs(主仓)', url: (n) => `https://git.fmode.cn/fmode/${n}` },
  122. github: { key: 'github', label: 'GitHub(镜像)', url: (n) => `https://github.com/fmodecn/${n}` },
  123. npm: { key: 'npm', label: 'npm', url: (n) => `https://www.npmjs.com/package/${n}` },
  124. skillhub: { key: 'skillhub', label: 'skillhub.cn', url: (s) => `https://skillhub.cn/skill/${s}` },
  125. };
  126. export const CREDENTIAL_CHAIN = [
  127. { level: 0, source: 'sessionToken 自举', detail: 'FMODE_SESSION_TOKEN / ~/.fmode/config.json → voc-skill/install-prompt' },
  128. { level: 1, source: '环境变量', detail: 'FMODE_API_TOKEN' },
  129. { level: 2, source: '用户级 config', detail: '~/.fmode/config.json → fmodeApiToken' },
  130. { level: 3, source: '项目级 config', detail: '<cwd>/.fmode/config.json → fmodeApiToken' },
  131. { level: 4, source: 'Claude Code settings', detail: '~/.claude/settings.json → env.ANTHROPIC_AUTH_TOKEN' },
  132. ];
  133. // ============================================================
  134. // 纯函数校验器(与 lib/index.mjs 同源逻辑,此处内联以保持零依赖)
  135. // ============================================================
  136. export const NAMING = {
  137. prefix: 'skill-',
  138. altPrefix: 'fmode-',
  139. pattern: /^(skill|fmode)-[a-z0-9]+(-[a-z0-9]+)*$/,
  140. slugPattern: /^fmode-skill-[a-z0-9]+(-[a-z0-9]+)*$/,
  141. };
  142. /** 校验技能名 */
  143. export function validateName(name) {
  144. if (!name || typeof name !== 'string') return { ok: false, reason: '技能名不能为空' };
  145. if (!NAMING.pattern.test(name)) {
  146. return { ok: false, reason: `"${name}" 不符合 skill-<kebab-case> 规范` };
  147. }
  148. return { ok: true, slug: `fmode-${name}` };
  149. }
  150. /** 校验 package.json(纯函数) */
  151. export function validatePackageJson(pkg) {
  152. const errors = [];
  153. const warnings = [];
  154. if (!pkg || typeof pkg !== 'object') return { ok: false, errors: ['不是对象'], warnings };
  155. for (const f of ['name', 'version', 'description', 'type', 'main', 'exports', 'bin', 'files', 'license']) {
  156. if (pkg[f] === undefined || pkg[f] === null || pkg[f] === '') errors.push(`缺少必需字段:${f}`);
  157. }
  158. if (pkg.type !== 'module') errors.push(`"type" 必须是 "module"(当前 ${JSON.stringify(pkg.type)})`);
  159. if (pkg.main !== './lib/index.mjs') errors.push(`"main" 必须是 "./lib/index.mjs"`);
  160. const dot = pkg.exports && pkg.exports['.'];
  161. if (!dot) errors.push('"exports" 缺少 "." 入口');
  162. else if (typeof dot === 'string') warnings.push('建议 exports["."] 写成 { import, default }');
  163. else for (const cond of ['import', 'default']) if (!dot[cond]) errors.push(`exports["."] 缺少 "${cond}"`);
  164. if (pkg.require !== undefined) errors.push('不应出现 "require" 字段 —— ESM only');
  165. if (!pkg.license) errors.push('缺少 license(平台统一 MIT)');
  166. if (pkg.name && !NAMING.pattern.test(pkg.name)) warnings.push(`技能名 "${pkg.name}" 建议用 skill-/fmode- 前缀`);
  167. return { ok: errors.length === 0, errors, warnings };
  168. }
  169. /** 校验 skill-package-manifest.json(纯函数) */
  170. export function validateManifest(manifest) {
  171. const errors = [];
  172. const warnings = [];
  173. if (!manifest || typeof manifest !== 'object') return { ok: false, errors: ['不是对象'], warnings };
  174. for (const f of ['name', 'version', 'description', 'skills']) {
  175. if (!manifest[f]) errors.push(`缺少必需字段:${f}`);
  176. }
  177. if (manifest.skills !== undefined && (!Array.isArray(manifest.skills) || !manifest.skills.length)) {
  178. errors.push('"skills" 必须是非空数组');
  179. }
  180. if (!manifest.install) warnings.push('建议声明 "install" 字段');
  181. return { ok: errors.length === 0, errors, warnings };
  182. }
  183. /** 极简 YAML frontmatter 解析(纯函数,无依赖) */
  184. export function parseFrontmatter(text) {
  185. if (typeof text !== 'string') return { data: {}, body: '', raw: null };
  186. const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
  187. if (!m) return { data: {}, body: text, raw: null };
  188. const data = {};
  189. for (const line of m[1].split(/\r?\n/)) {
  190. const t = line.trim();
  191. if (!t || t.startsWith('#')) continue;
  192. const i = t.indexOf(':');
  193. if (i <= 0) continue;
  194. const k = t.slice(0, i).trim();
  195. let v = t.slice(i + 1).trim();
  196. if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
  197. data[k] = v.slice(1, -1);
  198. } else if (v.startsWith('[') && v.endsWith(']')) {
  199. data[k] = v.slice(1, -1).split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
  200. } else {
  201. data[k] = v;
  202. }
  203. }
  204. return { data, body: text.slice(m[0].length), raw: m[1] };
  205. }
  206. export const FRONTMATTER_SCHEMAS = {
  207. hermes: { key: 'hermes', label: 'Hermes 本地技能格式', required: ['name', 'description', 'version'], recommended: ['tags', 'license', 'author'] },
  208. skillhub: { key: 'skillhub', label: 'skillhub.cn 格式', required: ['slug', 'displayName', 'version', 'summary', 'license'], recommended: ['tags'] },
  209. };
  210. /** 校验 frontmatter */
  211. export function validateFrontmatter(text, format = 'hermes') {
  212. const schema = FRONTMATTER_SCHEMAS[format];
  213. const errors = [];
  214. const warnings = [];
  215. if (!schema) return { ok: false, data: {}, errors: [`未知格式:${format}`], warnings };
  216. const { data, raw } = parseFrontmatter(text);
  217. if (raw === null) return { ok: false, data: {}, errors: ['未找到 YAML frontmatter'], warnings };
  218. for (const f of schema.required) if (data[f] === undefined || data[f] === '') errors.push(`缺少必需字段:${f}`);
  219. for (const f of schema.recommended) if (data[f] === undefined) warnings.push(`建议补充:${f}`);
  220. return { ok: errors.length === 0, data, errors, warnings };
  221. }
  222. // ============================================================
  223. // 浏览器可跑的质检子集
  224. // ============================================================
  225. /**
  226. * 浏览器版 API 联通检查(只用 fetch,无 Node 依赖)。
  227. * @param {{token?: string, timeoutMs?: number}} [opts]
  228. * @returns {Promise<{id: string, title: string, status: 'pass'|'fail'|'skip', detail: string, evidence: string[]}>}
  229. */
  230. export async function checkApiConnectivity(opts = {}) {
  231. const evidence = [];
  232. const headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};
  233. const probe = async (url, method = 'GET') => {
  234. try {
  235. const res = await fetch(url, {
  236. method,
  237. headers: { 'Content-Type': 'application/json', ...headers },
  238. body: method === 'POST' ? '{}' : undefined,
  239. signal: AbortSignal.timeout(opts.timeoutMs || 12000),
  240. });
  241. return { ok: true, status: res.status };
  242. } catch (err) {
  243. return { ok: false, status: 0, error: err.message };
  244. }
  245. };
  246. const llm = await probe(ENDPOINTS.llmChat.url, 'POST');
  247. evidence.push(`POST ${ENDPOINTS.llmChat.url} → HTTP ${llm.status}`);
  248. if (!llm.ok) {
  249. return { id: 'apiConnectivity', title: 'Fmode API 联通', status: 'skip', detail: '网络不可达(可能是 CORS 或离线)', evidence };
  250. }
  251. if (llm.status !== 200 && llm.status !== 401) {
  252. return { id: 'apiConnectivity', title: 'Fmode API 联通', status: 'fail', detail: `LLM 网关返回 ${llm.status}`, evidence };
  253. }
  254. const gw = await probe(ENDPOINTS.listenTranscribe.url, 'POST');
  255. evidence.push(`POST ${ENDPOINTS.listenTranscribe.url} → HTTP ${gw.status}`);
  256. if (!gw.ok) {
  257. return { id: 'apiConnectivity', title: 'Fmode API 联通', status: 'skip', detail: '业务网关不可达(浏览器端可能被 CORS 拦截,属正常)', evidence };
  258. }
  259. return {
  260. id: 'apiConnectivity',
  261. title: 'Fmode API 联通',
  262. status: 'pass',
  263. detail: 'LLM 网关与业务网关均可达(401=端点存在需鉴权)',
  264. evidence,
  265. };
  266. }
  267. /**
  268. * 浏览器可跑的全量检查(当前仅网络类)。
  269. * @param {object} [opts]
  270. */
  271. export async function runBrowserChecks(opts = {}) {
  272. const results = [await checkApiConnectivity(opts)];
  273. const pass = results.filter((r) => r.status === 'pass').length;
  274. const fail = results.filter((r) => r.status === 'fail').length;
  275. const skip = results.filter((r) => r.status === 'skip').length;
  276. return {
  277. results,
  278. summary: { total: results.length, pass, fail, skip, ok: fail === 0 && skip === 0, partial: fail === 0 && skip > 0 },
  279. };
  280. }
  281. /** 供浏览器端展示的技能清单摘要(不依赖 Node) */
  282. export const INVENTORY_SUMMARY = {
  283. total: 16,
  284. byTier: { system: 7, service: 6, application: 3 },
  285. byPlatform: { gogs: 11, github: 12, npm: 8, skillhub: 1 },
  286. note: '完整清单见仓库 inventory.md 或 lib/inventory.mjs;skillhub 渠道企业 key 失效待补发',
  287. };
  288. export default {
  289. VERSION,
  290. PLATFORM,
  291. ENDPOINTS,
  292. RUNTIMES,
  293. TIERS,
  294. CHANNELS,
  295. CREDENTIAL_CHAIN,
  296. validateName,
  297. validatePackageJson,
  298. validateManifest,
  299. validateFrontmatter,
  300. parseFrontmatter,
  301. checkApiConnectivity,
  302. runBrowserChecks,
  303. };