@guozhi-fe/skills 0.2.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # 国智前端 Skills
2
2
 
3
- 面向 Codex 的微前端内存排查技能。需要 Node.js 18 或更高版本。在业务项目根目录运行 CLI。
3
+ 面向 Codex 的Figma 业务组件设计、前端业务逻辑溯源与微前端内存排查技能。需要 Node.js 18 或更高版本。在业务项目根目录运行 CLI。
4
4
 
5
5
  | 技能 | 适用场景 |
6
6
  | --- | --- |
7
+ | build-ai-coding-component | 将已有 Figma Frame 整理为业务组件、状态变体、流程与交付摘要,供人工审阅和 AI Coding 使用。 |
8
+ | business-logic-trace | 追查业务入口、按钮条件、接口调用与字段来源,输出带源码证据的调用链和条件表。 |
7
9
  | microfrontend-memory-assessment | 反复打开和关闭子应用,判断内存增长的程度,输出可交接的采样证据。 |
8
10
  | microfrontend-memory-lifecycle | 追踪保留引用链,实施修复,并用生产构建验收。 |
9
11
 
10
12
  ## 首次安装
11
13
 
12
- 可用 `npm i -g @guozhi-fe/skills` 安装已发布的 CLI,或通过 `npx --package=@guozhi-fe/skills gzfe-skills init` 直接接入。试用本地修改时,在本仓库运行 `npm ci` 和 `npm pack`,再运行 `npm i -g /绝对路径/guozhi-fe-skills-0.2.0.tgz`。
14
+ 可用 `npm i -g @guozhi-fe/skills` 安装已发布的 CLI,或通过 `npx --package=@guozhi-fe/skills gzfe-skills init` 直接接入。试用本地修改时,在本仓库运行 `npm ci` 和 `npm pack`,再运行 `npm i -g /绝对路径/guozhi-fe-skills-0.5.1.tgz`。
13
15
 
14
16
  ```sh
15
17
  gzfe-skills init
@@ -21,6 +23,8 @@ gzfe-skills list
21
23
  直接指定技能名可以跳过交互。脚本中可使用 `--all` 安装全部技能,无终端时需要显式指定技能名或 `--all`。
22
24
 
23
25
  ```sh
26
+ gzfe-skills init build-ai-coding-component
27
+ gzfe-skills init business-logic-trace
24
28
  gzfe-skills init microfrontend-memory-assessment
25
29
  gzfe-skills init microfrontend-memory-assessment microfrontend-memory-lifecycle
26
30
  gzfe-skills init --all
@@ -53,4 +57,25 @@ update 默认处理已安装的受管理技能,也可在命令后指定名称
53
57
 
54
58
  ## 维护
55
59
 
56
- 编辑 skills 下的技能,变更分发内容时同步提升 package.json 的版本。两个技能共享的证据协议维护在 lifecycle 的 references 目录;评估技能安装时会获得独立副本。
60
+ 编辑 skills 下的技能,变更分发内容时同步提升 package.json 的版本。两个内存排查技能共享的证据协议维护在 lifecycle 的 references 目录;评估技能安装时会获得独立副本。
61
+
62
+ ## 本地技能目录
63
+
64
+ ```sh
65
+ gzfe-skills browse
66
+ ```
67
+
68
+ 自动选择空闲端口并打开浏览器,仅监听 `127.0.0.1`;按 `Ctrl+C` 关闭服务。可在任意目录运行,不会向当前项目写入文件,不需要 Python、GitHub Pages 或公开仓库。
69
+
70
+ ```sh
71
+ gzfe-skills browse --port 4173
72
+ gzfe-skills browse --no-open
73
+ ```
74
+
75
+ 页面支持搜索、复制指定版本安装命令和使用提示词。技能说明直接读取包内的 `SKILL.md`,可离线查看;安装命令通过 npm 获取包,仍需相应 registry 的访问权限。更新 CLI 后重启 `browse`,即可查看新版目录。
76
+
77
+ 展示页面和 `catalog.json` 随 npm 包一起分发。发包前自动生成展示数据并检查与包内技能一致。源码预览可运行 `npm run build:catalog` 后执行 `npm run browse`。
78
+
79
+ 正式发布流程及首次认证配置见 [维护和发布 SOP](SOP.md#5-维护技能与发布本地目录)。
80
+
81
+ 技能说明以 Markdown 阅读页展示,支持标题、列表、表格、代码块和章节导航;参考文档也可本地打开。顶部可切换查看原文。更新代码或 CLI 后请重启 `browse` 服务。
@@ -4,13 +4,15 @@ import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { createHash } from 'node:crypto'
6
6
  import checkbox from '@inquirer/checkbox'
7
- import { parse } from 'yaml'
7
+ import { browse } from '../lib/browse.js'
8
+ import { readSkills } from '../lib/catalog.js'
8
9
  import colors from 'yoctocolors-cjs'
9
10
  import wrapAnsi from 'wrap-ansi'
10
11
 
11
12
  const root = fileURLToPath(new URL('../', import.meta.url))
12
13
  const source = path.join(root, 'skills')
13
- const catalog = fs.readdirSync(source).sort()
14
+ const skillMetadata = readSkills(root)
15
+ const catalog = skillMetadata.map(skill => skill.id)
14
16
  const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'))).version
15
17
  const dest = path.resolve('.agents/skills')
16
18
  const marker = '.gzfe-skills.json'
@@ -60,13 +62,7 @@ async function selectSkills(command, names) {
60
62
  throw Error(`交互选择需要终端,请指定技能名,或使用 gzfe-skills ${command} --all。`)
61
63
  }
62
64
  const choices = names.map(name => {
63
- const file = path.join(source, name, 'SKILL.md')
64
- const frontmatter = fs.readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
65
- const metadata = frontmatter && parse(frontmatter[1])
66
- if (typeof metadata?.description !== 'string' || !metadata.description.trim()) {
67
- throw Error('技能缺少有效简介:' + file)
68
- }
69
- const description = metadata.description.replace(/\s+/g, ' ').trim()
65
+ const description = skillMetadata.find(skill => skill.id === name).description
70
66
  const summary = description.match(/^[^。!?]+[。!?]?/u)?.[0] || description
71
67
  const width = Math.max(16, Math.min(88, (process.stdout.columns || 80) - 4))
72
68
  const lines = wrapAnsi(summary, width, { hard: true }).split('\n')
@@ -92,6 +88,7 @@ async function selectSkills(command, names) {
92
88
  async function run([command, ...args]) {
93
89
  if (!command || ['--help', '-h'].includes(command)) {
94
90
  console.log([
91
+ 'gzfe-skills browse [--port 4173] [--no-open] 本地浏览技能目录',
95
92
  'gzfe-skills init 交互选择技能,支持单选和多选',
96
93
  'gzfe-skills init <技能名 ...> 直接安装指定技能',
97
94
  'gzfe-skills init --all 安装全部技能,可用于脚本',
@@ -105,6 +102,7 @@ async function run([command, ...args]) {
105
102
  ].join('\n'))
106
103
  return
107
104
  }
105
+ if (command === 'browse') return browse(root, args)
108
106
  if (command === '--version') return console.log(version)
109
107
  if (!['init', 'update', 'list', 'uninstall'].includes(command)) throw Error('未知命令:' + command)
110
108
  const dry = command === 'update' && args.includes('--dry-run')
package/catalog.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "packageName": "@guozhi-fe/skills",
4
+ "version": "0.5.1",
5
+ "gitTag": "v0.5.1",
6
+ "skills": [
7
+ {
8
+ "id": "build-ai-coding-component",
9
+ "displayName": "Build AI-Coding Component",
10
+ "summary": "Build AI-readable Figma component specs for coding",
11
+ "description": "基于已有 Figma Frame 和用户提供的功能、流程、状态描述,生成或重构便于人工审阅与 AI Coding 的业务组件设计。用于创建主 Component Set、必要 Variants、带条件的主流程、关键 Scenario Frames 和精简 Handoff Summary;适合 React、Ant Design、AG Grid 业务组件。不要用于单纯视觉润色或直接生成前端代码。",
12
+ "defaultPrompt": "Use $build-ai-coding-component to turn this existing Figma frame and my business-flow description into a component set, state flow, and scenario frames ready for AI coding."
13
+ },
14
+ {
15
+ "id": "business-logic-trace",
16
+ "displayName": "业务逻辑溯源",
17
+ "summary": "追查业务入口、接口与字段流向,区分展示禁用并附上源码证据",
18
+ "description": "从前端仓库追查某个业务行为、按钮条件、接口调用时机或字段来源,输出带源码定位的调用链与条件表。适用于旧系统理解和迁移前调查;不用于仅解释通用语法、直接实施迁移或安全扫描。",
19
+ "defaultPrompt": "使用 $business-logic-trace 追查当前页面的按钮条件和接口字段来源。"
20
+ },
21
+ {
22
+ "id": "microfrontend-memory-assessment",
23
+ "displayName": "微前端内存评估",
24
+ "summary": "评估微前端内存增长趋势、业务影响和证据置信度,输出修复交接材料",
25
+ "description": "评估微前端子应用反复打开关闭后的内存增长是否异常、影响程度与证据置信度,生成可复用的采样证据和修复交接报告。适用于内存体检、多个子应用初筛或仅判断严重性;明确要求定位修复时使用 microfrontend-memory-lifecycle。",
26
+ "defaultPrompt": "使用 $microfrontend-memory-assessment 评估指定主子应用反复开关后的内存增长和严重程度,生成证据与修复交接报告。"
27
+ },
28
+ {
29
+ "id": "microfrontend-memory-lifecycle",
30
+ "displayName": "微前端内存修复与验收",
31
+ "summary": "定位主子应用反复开关的内存增长,并以生产构建堆证据验收修复",
32
+ "description": "对微前端反复开关的内存增长定位保留引用链、实施修复并完成生产浏览器验收。适用于明确要求分析修复或已有评估证据的任务;仅做体检、严重性判断时使用 microfrontend-memory-assessment。",
33
+ "defaultPrompt": "使用 $microfrontend-memory-lifecycle 排查当前主子应用反复打开关闭后的内存增长,完成证据驱动修复和生产浏览器验收。"
34
+ }
35
+ ]
36
+ }
package/lib/browse.js ADDED
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import http from 'node:http'
4
+ import { spawn } from 'node:child_process'
5
+ import { buildCatalog } from './catalog.js'
6
+ import { renderDocument } from './markdown.js'
7
+
8
+ export function browseOptions(args) {
9
+ const options = { port: 0, open: true }
10
+ for (let i = 0; i < args.length; i++) {
11
+ if (args[i] === '--no-open') options.open = false
12
+ else if (args[i] === '--port') {
13
+ const value = args[++i]
14
+ if (!/^\d+$/.test(value || '') || Number(value) < 1 || Number(value) > 65535) throw Error('--port 需要 1–65535 之间的整数')
15
+ options.port = Number(value)
16
+ } else throw Error(`未知 browse 参数:${args[i]}`)
17
+ }
18
+ return options
19
+ }
20
+
21
+ export async function createBrowseServer(root, port = 0) {
22
+ // Use the shipped catalog; source checkouts can preview without generating files.
23
+ const file = path.join(root, 'catalog.json')
24
+ const current = buildCatalog(root)
25
+ const catalog = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : current
26
+ if (JSON.stringify(catalog) !== JSON.stringify(current)) throw Error('展示数据与技能内容不一致,请运行 npm run build:catalog 后重试')
27
+ const routes = new Map([
28
+ ['/', ['site/index.html', 'text/html; charset=utf-8']],
29
+ ['/index.html', ['site/index.html', 'text/html; charset=utf-8']],
30
+ ['/styles.css', ['site/styles.css', 'text/css; charset=utf-8']],
31
+ ['/app.js', ['site/app.js', 'text/javascript; charset=utf-8']],
32
+ ['/document.css', ['site/document.css', 'text/css; charset=utf-8']],
33
+ ])
34
+ // Load only explicit public routes. Never serve the user's working directory.
35
+ const assets = new Map([...routes].map(([url, [relative, type]]) => [url, { type, body: fs.readFileSync(path.join(root, relative)) }]))
36
+ assets.set('/catalog.json', { type: 'application/json; charset=utf-8', body: JSON.stringify(catalog) })
37
+ const rawDocuments = new Map()
38
+ for (const skill of catalog.skills) {
39
+ const documents = new Map()
40
+ function collect(dir, prefix = '') {
41
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ const relative = prefix + entry.name
43
+ if (entry.isDirectory()) collect(path.join(dir, entry.name), relative + '/')
44
+ else if (entry.isFile() && entry.name.endsWith('.md')) documents.set(relative, fs.readFileSync(path.join(dir, entry.name), 'utf8'))
45
+ }
46
+ }
47
+ collect(path.join(root, 'skills', skill.id))
48
+ // Assessment uses the lifecycle references, just as its installed protocol does.
49
+ if (skill.id === 'microfrontend-memory-assessment') {
50
+ const shared = path.join(root, 'skills/microfrontend-memory-lifecycle/references')
51
+ for (const entry of fs.readdirSync(shared, { withFileTypes: true })) {
52
+ if (entry.isFile() && entry.name.endsWith('.md')) documents.set('references/' + entry.name, fs.readFileSync(path.join(shared, entry.name), 'utf8'))
53
+ }
54
+ }
55
+ for (const [relative, source] of documents) {
56
+ const pathname = `/skills/${skill.id}/${relative.split('/').map(encodeURIComponent).join('/')}`
57
+ assets.set(pathname, { type: 'text/html; charset=utf-8', body: renderDocument(source, { skill, version: catalog.version, pathname }) })
58
+ rawDocuments.set(pathname, { type: 'text/plain; charset=utf-8', body: source })
59
+ }
60
+ }
61
+ const server = http.createServer((request, response) => {
62
+ response.setHeader('X-Content-Type-Options', 'nosniff')
63
+ response.setHeader('Cache-Control', 'no-store')
64
+ if (!['GET', 'HEAD'].includes(request.method)) {
65
+ response.writeHead(405, { Allow: 'GET, HEAD' }).end()
66
+ return
67
+ }
68
+ const pathname = (request.url || '').split('?')[0]
69
+ const raw = new URLSearchParams((request.url || '').split('?')[1]).get('raw') === '1'
70
+ const asset = raw ? rawDocuments.get(pathname) : assets.get(pathname)
71
+ if (!asset) { response.writeHead(404).end('Not found'); return }
72
+ response.writeHead(200, { 'Content-Type': asset.type })
73
+ response.end(request.method === 'HEAD' ? undefined : asset.body)
74
+ })
75
+ await new Promise((resolve, reject) => {
76
+ server.once('error', reject)
77
+ server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject); resolve() })
78
+ })
79
+ return { server, url: `http://127.0.0.1:${server.address().port}/` }
80
+ }
81
+
82
+ function openBrowser(url) {
83
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open'
84
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
85
+ const child = spawn(command, args, { stdio: 'ignore' })
86
+ const fallback = () => console.error('无法自动打开浏览器,请手动打开上方地址。')
87
+ child.once('error', fallback)
88
+ child.once('exit', code => { if (code) fallback() })
89
+ child.unref()
90
+ }
91
+
92
+ export async function browse(root, args) {
93
+ const options = browseOptions(args)
94
+ let result
95
+ try { result = await createBrowseServer(root, options.port) }
96
+ catch (error) {
97
+ if (error.code === 'EADDRINUSE') throw Error(`端口 ${options.port} 已被占用,请换一个 --port 或省略以自动选择空闲端口`)
98
+ throw error
99
+ }
100
+ const { server, url } = result
101
+ console.log(`技能目录:${url}\n仅本机可访问,按 Ctrl+C 关闭。`)
102
+ const stop = () => {
103
+ process.removeListener('SIGINT', stop)
104
+ process.removeListener('SIGTERM', stop)
105
+ server.close()
106
+ server.closeAllConnections()
107
+ }
108
+ process.once('SIGINT', stop)
109
+ process.once('SIGTERM', stop)
110
+ if (options.open) openBrowser(url)
111
+ }
package/lib/catalog.js ADDED
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { parse } from 'yaml'
4
+
5
+ export function readSkills(root) {
6
+ const source = path.join(root, 'skills')
7
+ return fs.readdirSync(source, { withFileTypes: true }).filter(entry => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name, 'en')).map(({ name: id }) => {
8
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) throw Error(`无效技能 ID:${id}`)
9
+ const file = path.join(source, id, 'SKILL.md')
10
+ const frontmatter = fs.readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
11
+ const metadata = frontmatter && parse(frontmatter[1])
12
+ if (metadata?.name !== id || typeof metadata.description !== 'string' || !metadata.description.trim()) throw Error(`技能名称或简介无效:${file}`)
13
+ const agentFile = path.join(source, id, 'agents/openai.yaml')
14
+ const ui = fs.existsSync(agentFile) ? parse(fs.readFileSync(agentFile, 'utf8'))?.interface : undefined
15
+ for (const key of ['display_name', 'short_description', 'default_prompt']) {
16
+ if (ui?.[key] !== undefined && (typeof ui[key] !== 'string' || !ui[key].trim())) throw Error(`无效字段 ${key}:${agentFile}`)
17
+ }
18
+ const description = metadata.description.replace(/\s+/g, ' ').trim()
19
+ return { id, displayName: ui?.display_name?.trim() || id, summary: ui?.short_description?.trim() || description, description, ...(ui?.default_prompt ? { defaultPrompt: ui.default_prompt.trim() } : {}) }
20
+ })
21
+ }
22
+
23
+ export function buildCatalog(root) {
24
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
25
+ return { schemaVersion: 1, packageName: pkg.name, version: pkg.version, gitTag: `v${pkg.version}`, skills: readSkills(root) }
26
+ }
@@ -0,0 +1,45 @@
1
+ import MarkdownIt from 'markdown-it'
2
+
3
+ const markdown = new MarkdownIt({ html: false, linkify: true })
4
+ const escape = markdown.utils.escapeHtml
5
+ function slug(text) {
6
+ return text.toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, '').replace(/\s/g, '-') || 'section'
7
+ }
8
+
9
+ export function renderDocument(source, { skill, version, pathname }) {
10
+ const body = source.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, '')
11
+ const tokens = markdown.parse(body, {})
12
+ const sections = []
13
+ const used = new Set()
14
+ for (let i = 0; i < tokens.length; i++) {
15
+ const token = tokens[i]
16
+ if (token.type === 'heading_open') {
17
+ const inline = tokens[i + 1]
18
+ const title = (inline.children || []).filter(child => ['text', 'code_inline'].includes(child.type)).map(child => child.content).join('') || inline.content
19
+ const base = `section-${slug(title)}`
20
+ let id = base, suffix = 0
21
+ while (used.has(id)) id = `${base}-${++suffix}`
22
+ used.add(id)
23
+ token.attrSet('id', id)
24
+ if (['h2', 'h3'].includes(token.tag)) sections.push({ title, id, level: token.tag })
25
+ }
26
+ for (const child of token.children || []) {
27
+ if (child.type !== 'link_open') continue
28
+ const href = child.attrGet('href')
29
+ // Match the prefixed heading IDs on local documents, leaving external URLs intact.
30
+ if (href && !/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) && href.includes('#')) {
31
+ const index = href.indexOf('#')
32
+ const fragment = href.slice(index + 1)
33
+ if (fragment) child.attrSet('href', href.slice(0, index + 1) + 'section-' + fragment)
34
+ }
35
+ }
36
+ }
37
+ const content = markdown.renderer.render(tokens, markdown.options, {})
38
+ const navigation = sections.map(section => `<li class="${section.level}"><a href="#${escape(section.id)}">${escape(section.title)}</a></li>`).join('')
39
+ return `<!doctype html>
40
+ <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
41
+ <title>${escape(skill.displayName)} · GZFE Skills</title><link rel="stylesheet" href="/document.css"></head>
42
+ <body><header class="reader-header"><a class="brand" href="/">GZFE / Skills</a><nav aria-label="文档操作"><a href="/">返回技能目录</a><a href="${escape(pathname)}?raw=1">查看 Markdown 原文</a></nav></header>
43
+ <div class="reader-layout"><main id="content"><div class="document-meta"><span>${escape(skill.displayName)}</span><span>v${escape(version)}</span></div><article class="markdown-body">${content}</article><p class="document-end">文档来自本地技能包 · ${escape(skill.id)}</p></main>
44
+ ${sections.length ? `<nav class="toc" aria-label="章节导航"><details open><summary>本页目录</summary><ul>${navigation}</ul></details></nav>` : ''}</div></body></html>`
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guozhi-fe/skills",
3
- "version": "0.2.0",
3
+ "version": "0.5.1",
4
4
  "description": "国智前端项目级 Codex 技能集",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,17 +8,25 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "skills"
11
+ "skills",
12
+ "lib",
13
+ "catalog.json",
14
+ "site"
12
15
  ],
13
16
  "engines": {
14
17
  "node": ">=18"
15
18
  },
16
19
  "repository": "github:soleilLulu/gz-fe-skills",
17
20
  "scripts": {
18
- "test": "node --test"
21
+ "test": "node --test",
22
+ "prepack": "node scripts/build-catalog.js",
23
+ "build:catalog": "node scripts/build-catalog.js",
24
+ "verify:package": "node scripts/verify-package.js",
25
+ "browse": "node bin/gzfe-skills.js browse"
19
26
  },
20
27
  "dependencies": {
21
28
  "@inquirer/checkbox": "^4.3.2",
29
+ "markdown-it": "^14.3.3",
22
30
  "wrap-ansi": "^6.2.0",
23
31
  "yaml": "^2.9.1",
24
32
  "yoctocolors-cjs": "^2.1.3"
package/site/app.js ADDED
@@ -0,0 +1,61 @@
1
+ const $ = selector => document.querySelector(selector)
2
+ let noticeTimer
3
+ async function copy(text) {
4
+ try {
5
+ await navigator.clipboard.writeText(text)
6
+ $('#notice').textContent = '已复制'
7
+ clearTimeout(noticeTimer)
8
+ noticeTimer = setTimeout(() => { $('#notice').textContent = '' }, 2400)
9
+ } catch {
10
+ const dialog = $('#copy-fallback')
11
+ dialog.querySelector('textarea').value = text
12
+ dialog.showModal()
13
+ dialog.querySelector('textarea').select()
14
+ }
15
+ }
16
+ $('#close-dialog').addEventListener('click', () => $('#copy-fallback').close())
17
+ function element(tag, text, className) {
18
+ const node = document.createElement(tag)
19
+ node.textContent = text
20
+ if (className) node.className = className
21
+ return node
22
+ }
23
+ function copyButton(label, value, className) {
24
+ const button = element('button', label, className)
25
+ button.type = 'button'
26
+ button.addEventListener('click', () => copy(value))
27
+ return button
28
+ }
29
+ try {
30
+ const response = await fetch('./catalog.json')
31
+ if (!response.ok) throw Error('目录加载失败')
32
+ const catalog = await response.json()
33
+ if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.skills)) throw Error('目录格式错误')
34
+ $('#version').textContent = `v${catalog.version} · ${catalog.skills.length} 个技能`
35
+ function render() {
36
+ const query = $('#search').value.trim().toLocaleLowerCase()
37
+ const skills = catalog.skills.filter(skill => [skill.id, skill.displayName, skill.summary, skill.description].join(' ').toLocaleLowerCase().includes(query))
38
+ $('#skills').replaceChildren(...skills.map(skill => {
39
+ const card = element('article', '', 'card')
40
+ const actions = element('div', '', 'actions')
41
+ actions.append(copyButton('复制安装命令', `npx --package=${catalog.packageName}@${catalog.version} gzfe-skills init ${skill.id}`, 'primary'))
42
+ if (skill.defaultPrompt) actions.append(copyButton('复制提示词', skill.defaultPrompt))
43
+ const link = element('a', '查看说明 ↗')
44
+ link.href = `./skills/${encodeURIComponent(skill.id)}/SKILL.md`
45
+ link.target = '_blank'
46
+ link.rel = 'noopener'
47
+ actions.append(link)
48
+ card.append(element('h2', skill.displayName), element('p', skill.summary), element('code', skill.id), actions)
49
+ return card
50
+ }))
51
+ $('#count').textContent = query ? `找到 ${skills.length} / ${catalog.skills.length} 个技能` : `全部技能 · ${skills.length}`
52
+ $('#empty').hidden = skills.length !== 0
53
+ }
54
+ $('#search').disabled = false
55
+ $('#search').addEventListener('input', render)
56
+ render()
57
+ } catch (error) {
58
+ $('#error').hidden = false
59
+ $('#version').textContent = '目录暂不可用'
60
+ console.error(error)
61
+ }
@@ -0,0 +1 @@
1
+ :root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#25382e;background:#f5f6f1;line-height:1.8;color-scheme:light}*{box-sizing:border-box}body{margin:0}a{color:#286647;text-underline-offset:4px}a:focus-visible,summary:focus-visible{outline:3px solid #8cb59a;outline-offset:4px}.reader-header{max-width:1240px;margin:auto;min-height:80px;padding:18px 32px;display:flex;align-items:center;justify-content:space-between;gap:20px;border-bottom:1px solid #dce2d9}.brand{font-weight:750;font-size:19px;text-decoration:none}.reader-header nav{display:flex;gap:24px;font-size:14px}.reader-layout{max-width:1240px;margin:32px auto 64px;padding:0 32px;display:grid;grid-template-columns:minmax(0,860px) 230px;gap:36px;align-items:start}main{min-width:0;background:white;border:1px solid #dce2d9;border-radius:14px;padding:40px 48px}.document-meta{display:flex;gap:16px;justify-content:space-between;color:#6b796e;font-size:13px;border-bottom:1px solid #e4e9e2;padding-bottom:20px;margin-bottom:28px}.markdown-body{font-size:16px;overflow-wrap:anywhere}.markdown-body h1{font-size:32px;line-height:1.4;margin:0 0 28px;letter-spacing:-.5px}.markdown-body h2{font-size:23px;margin:40px 0 18px;padding-bottom:10px;border-bottom:1px solid #e4e9e2}.markdown-body h3{font-size:19px;margin:30px 0 14px}.markdown-body :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:24px}.markdown-body p{margin:16px 0}.markdown-body ul,.markdown-body ol{padding-left:26px}.markdown-body li{padding-left:4px;margin:10px 0}.markdown-body strong{color:#193f2b}.markdown-body code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.86em;border-radius:4px;background:#edf2ec;padding:3px 6px}.markdown-body pre{max-width:100%;overflow:auto;background:#f0f3ed;border:1px solid #dce2d9;border-radius:8px;padding:18px;line-height:1.65}.markdown-body pre code{padding:0;background:none;overflow-wrap:normal}.markdown-body blockquote{margin:20px 0;border-left:3px solid #87aa91;padding:1px 20px;color:#617366;background:#f5f7f2}.markdown-body table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse;font-size:14px;margin:24px 0}.markdown-body th,.markdown-body td{border:1px solid #dce2d9;padding:10px 14px;min-width:90px}.markdown-body th{background:#edf2ec;text-align:left}.markdown-body img{max-width:100%}.markdown-body hr{border:0;border-top:1px solid #dce2d9;margin:30px 0}.toc{position:sticky;top:28px;font-size:13px;padding:16px 0}.toc summary{font-weight:700;color:#314e3a;cursor:pointer}.toc ul{list-style:none;margin:16px 0;padding:0;border-left:1px solid #cfdacc}.toc li{margin:12px 0;padding-left:16px}.toc .h3{padding-left:28px}.toc a{text-decoration:none;color:#65776a}.toc a:hover{color:#245d46;text-decoration:underline}.document-end{border-top:1px solid #e4e9e2;margin:40px 0 0;padding-top:20px;font-size:12px;color:#6b796e;overflow-wrap:anywhere}@media(max-width:900px){.reader-layout{grid-template-columns:minmax(0,1fr);gap:20px}.toc{position:static;grid-row:1;padding:0 20px}.toc ul{columns:2}main{padding:32px}}@media(max-width:540px){.reader-header{padding:18px 20px;align-items:flex-start;flex-direction:column;gap:8px}.reader-header nav{font-size:13px;gap:20px}.reader-layout{padding:0 12px;margin-top:20px}main{padding:24px 20px}.markdown-body{font-size:15px}.markdown-body h1{font-size:26px}.markdown-body h2{font-size:21px}.toc ul{columns:1}.document-meta{flex-wrap:wrap;gap:6px}}
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="description" content="GZFE 团队技能目录。查找技能,复制安装命令,在项目中使用。">
7
+ <title>GZFE Skills · 技能目录</title>
8
+ <link rel="stylesheet" href="./styles.css">
9
+ <script type="module" src="./app.js"></script>
10
+ </head>
11
+ <body>
12
+ <header><a class="brand" href="./"><span class="mark">G</span> GZFE <span class="muted">/ Skills</span></a><a href="https://github.com/soleilLulu/gz-fe-skills">GitHub ↗</a></header>
13
+ <main>
14
+ <section class="intro"><p class="eyebrow">GUOZHI FRONTEND · SKILL LIBRARY</p><h1>找到技能,<br>开始解决问题。</h1><p class="lead">把团队积累的排查方法,带进你的下一个项目。</p><p id="version" class="version">正在读取技能目录…</p></section>
15
+ <section aria-label="查找技能">
16
+ <label for="search">搜索技能</label><input id="search" type="search" placeholder="试试「内存」「评估」或技能名称" autocomplete="off" disabled>
17
+ <p id="count" class="muted" role="status"></p>
18
+ <div id="skills" class="grid"></div><p id="empty" hidden>没有找到匹配的技能,换一个关键词试试。</p>
19
+ <p id="error" role="alert" hidden>目录加载失败,请刷新页面重试。</p>
20
+ </section>
21
+ <aside><strong>在业务项目目录中安装</strong><p>复制技能的安装命令,在终端执行;随后将使用提示词发送到 Codex。已经安装过的技能会保留原版本,请按仓库说明执行更新。</p></aside>
22
+ </main>
23
+ <footer><span>GZFE · 团队方法,随项目同行。</span><span>本地技能目录 · 随 CLI 版本分发</span></footer>
24
+ <dialog id="copy-fallback"><h2>复制内容</h2><p>自动复制不可用,请选中下方内容手动复制。</p><textarea readonly aria-label="待复制内容"></textarea><button id="close-dialog" type="button">关闭</button></dialog>
25
+ <p id="notice" class="notice" role="status"></p>
26
+ </body>
27
+ </html>
@@ -0,0 +1 @@
1
+ :root{color-scheme:light;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#20332c;background:#f5f6f1;line-height:1.65}*{box-sizing:border-box}body{margin:0}header,main,footer{max-width:1120px;margin:auto;padding:0 32px}header{height:104px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #dce2d9}a{color:inherit;text-underline-offset:4px}.brand{text-decoration:none;display:flex;align-items:center;gap:10px;font-weight:700;font-size:19px}.mark{display:grid;place-items:center;width:34px;height:34px;background:#245d46;color:white;border-radius:9px}.muted{color:#65736b}.intro{padding:64px 0 36px}.eyebrow{font-size:12px;letter-spacing:2px;color:#417156;font-weight:700}h1{font-size:clamp(36px,5vw,58px);letter-spacing:-2px;line-height:1.22;margin:18px 0 22px}.lead{font-size:17px;color:#65736b}.version{display:inline-block;font-size:12px;border:1px solid #cfdbd1;border-radius:20px;padding:3px 12px}label{display:block;font-size:14px;font-weight:600;margin-bottom:8px}input{width:100%;padding:17px 20px;font:inherit;color:inherit;border:1px solid #c7d2c8;border-radius:12px;background:white}input:focus,button:focus-visible,a:focus-visible{outline:3px solid #8cb59a;outline-offset:3px}#count{font-size:13px;margin:18px 0}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.card{display:flex;flex-direction:column;padding:28px;background:#fff;border:1px solid #dce2d9;border-radius:16px}.card h2{font-size:23px;margin:0 0 12px}.card p{margin:0 0 18px;color:#65736b}.card code{font-size:12px;overflow-wrap:anywhere;color:#68796c}.actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:auto;padding-top:28px}button{font:inherit;font-size:13px;border:1px solid #cdd8ce;border-radius:7px;padding:9px 12px;background:#fff;color:#254f3d;cursor:pointer}.primary{background:#245d46;border-color:#245d46;color:white}button:hover{filter:brightness(.95)}.actions a{font-size:13px;margin-left:auto}aside{margin:36px 0 64px;padding:24px;border-left:3px solid #89a98f;background:#ecf0e8}aside p{font-size:14px;color:#65736b;margin:8px 0 0}footer{display:flex;justify-content:space-between;gap:16px;font-size:12px;color:#65736b;border-top:1px solid #dce2d9;padding-top:24px;padding-bottom:28px}.notice:empty{display:none}.notice{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);padding:10px 20px;background:#203f30;color:white;border-radius:8px;box-shadow:0 4px 20px #0002}dialog{max-width:640px;width:90%;border:1px solid #cdd8ce;border-radius:14px;padding:24px}dialog::backdrop{background:#0005}textarea{display:block;width:100%;min-height:150px;font:inherit;font-size:14px;padding:10px;margin-bottom:16px}@media(max-width:700px){header,main,footer{padding-left:20px;padding-right:20px}header{height:80px}.intro{padding-top:40px}.grid{grid-template-columns:1fr}.card{padding:24px}footer{flex-direction:column}.actions a{margin-left:0}}
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: build-ai-coding-component
3
+ description: "基于已有 Figma Frame 和用户提供的功能、流程、状态描述,生成或重构便于人工审阅与 AI Coding 的业务组件设计。用于创建主 Component Set、必要 Variants、带条件的主流程、关键 Scenario Frames 和精简 Handoff Summary;适合 React、Ant Design、AG Grid 业务组件。不要用于单纯视觉润色或直接生成前端代码。"
4
+ ---
5
+
6
+ # 构建面向 AI Coding 的 Figma 业务组件
7
+
8
+ 将已有 Frame 转化为最小但完整的设计契约:一个业务主组件、一条明确主流程、少量关键场景和一张交付摘要。
9
+
10
+ ## 前置要求
11
+
12
+ 1. 写入 Figma 前使用 `figma-use`。
13
+ 2. 读取 Frame 链接、组件目标、状态、事件、条件、成功/失败路径及可用的数据或技术信息。
14
+ 3. 先检查源 Frame、现有组件、Variables 和 Styles;保留原稿并继承现有视觉规范。
15
+ 4. 不编造业务规则、接口或版本。只有影响组件边界或主流程的歧义才需要询问。
16
+
17
+ ## 选择写入模式
18
+
19
+ - **新建模式**:链接指向新的源 Frame,且文件中尚无与该业务对应的主 Component Set。必须在同一 Figma 文件中新建 Page `AI-CODING|<ComponentName>|Codex`,所有产出放入该 Page;不要放在源 Frame 附近。
20
+ - **续改模式**:链接指向已经包含对应主 Component Set,或已有 `ai-handoff` 交付结构的内容。必须在当前 Page 中复用并调整现有 Component Set、FLOW、SCENARIOS 和 HANDOFF;不要新建 Page 或重复创建主组件。
21
+ - 判断依据是“是否已有与目标业务对应的主 Component Set”,而不是当前 Page 是否存在其他无关组件。
22
+
23
+ ## 必须产出
24
+
25
+ 在所选目标 Page 中创建或复用一个可分享的顶层 Section:
26
+
27
+ ```text
28
+ SECTION|<domain>/<ComponentName>|ai-handoff
29
+ ├─ HANDOFF|summary
30
+ ├─ CMP|<ComponentName>
31
+ ├─ FLOW|<ComponentName>
32
+ └─ SCENARIOS|key-cases
33
+ ```
34
+
35
+ 不要默认创建额外页面、完整设计系统、冗长文档或重复界面。
36
+
37
+ ## 工作流程
38
+
39
+ ### 1. 审计并区分信息
40
+
41
+ - 识别源 Frame 的稳定结构、重复区域、已有状态和可复用设计资产。
42
+ - 优先使用 Library 组件、Variables 和 Styles;存在语义 Token 时不要硬编码。
43
+ - 将信息标为:
44
+ - `CONFIRMED`:已由用户、设计或代码确定,可直接实现。
45
+ - `REFERENCE`:以接口、类型或代码链接为权威来源。
46
+ - `TBD`:实现所需但尚未确定,禁止自行假设。
47
+
48
+ ### 2. 建立最小状态模型
49
+
50
+ - 建模 `State`(稳定 UI)、`Event`(动作)、`Guard`(条件)和关键 `Effect`(结果)。
51
+
52
+ - `Reset`、`Retry`、`Close` 默认是 Event,不是 State。
53
+ - 异步操作必须有 `loading/submitting` 状态。
54
+ - 只创建业务实际存在的状态;未知行为标记 `TBD`。
55
+
56
+ ### 3. 创建主 Component Set
57
+
58
+ - 新建模式从源 Frame 创建 `CMP|<ComponentName>`;续改模式复用并更新现有主 Component Set。
59
+ - 仅将改变结构或视觉反馈的维度做成 Variant,例如 `status`、`mode`。
60
+ - 只生成有效 Variant 组合,避免无效笛卡尔积。
61
+ - 使用 `BOOLEAN`、`TEXT`、`INSTANCE_SWAP` 表达可配置内容;数据和文案差异不做 Variant。
62
+ - FLOW 和 SCENARIOS 必须使用主组件 Instance,不得复制脱离主组件的完整界面。
63
+ - 使用 Auto Layout,并明确必要的尺寸、滚动和溢出规则。
64
+
65
+ ### 4. 绘制主流程
66
+
67
+ - 状态命名:`STATE|<id>|<status>|<mode>|guard=<condition>`。
68
+ - 连线命名:`EVENT|<event>|guard=<condition>|effect=<action>`。
69
+
70
+ - 包含起点、主要成功路径、失败路径和返回/结束路径。
71
+ - 每个状态使用主组件 Instance。
72
+ - 每条分支显示 Event 和 Guard;关键副作用显示 Effect。
73
+ - 使用 Decision 节点表达条件,并标明 `A XOR B|同一步骤的互斥状态`。
74
+ - 不依靠颜色、位置、线型或 Figma 评论承载关键语义。
75
+
76
+ ### 5. 创建关键 Scenario Frames
77
+
78
+ 使用 `SCENARIO|<caseId>|<short-name>` 命名,每个 Scenario 包含一个主组件 Instance,并标明 Variant、输入条件、示例数据、预期 UI 和下一事件。
79
+
80
+ 只覆盖用户明确描述的默认、loading、主要成功、主要失败和关键互斥场景;其他边界场景仅在业务需要时增加,不穷举数据组合。
81
+
82
+ ### 6. 添加精简 Handoff Summary
83
+
84
+ 在 Section 顶部创建小型 `HANDOFF|summary`:
85
+
86
+ ```text
87
+ Purpose:组件目标
88
+ States:主要 Variants 与互斥关系
89
+ Flow:主要 Events 与 Guards
90
+ Implementation:技术栈、AntD/AG Grid 映射和版本
91
+ Data:数据形态及接口/TypeScript 类型链接
92
+ Open Questions:TBD 列表
93
+ ```
94
+
95
+ 对每项使用 `CONFIRMED`、`REFERENCE` 或 `TBD` 标记;不要复述流程图和 Scenario 已表达的信息。
96
+
97
+ 若使用 Ant Design,只标明主要区域对应的组件及 loading、disabled、validation、close 等关键交互。若使用 AG Grid,只标明 rowData 方向、动态 columnDefs、稳定 colId、固定列/滚动、formatter/renderer 和 loading/empty/error。
98
+
99
+ ## 层级与命名
100
+
101
+ - Section 表示一个业务交付单位;Frame 表示说明、流程或场景;Component/Instance 表示可复用 UI。
102
+ - Group 只用于临时移动,不承载布局或业务语义。
103
+ - Variant 属性使用可映射代码的英文名和值,如 `status=partialError`、`mode=multiple`。
104
+ - 禁止使用 `Frame 123`、`Variant 1`、`Property 2`、`状态一` 等无语义名称。
105
+
106
+ ## 完成标准
107
+
108
+ - Component Set、Variants 和 Instance 关联正确。
109
+ - 主流程的 Event、Guard 和互斥关系清楚。
110
+ - 核心状态均有 Variant 或 Scenario。
111
+ - 节点名称、可见说明与属性语义一致;Handoff 中事实、外部来源和 TBD 清楚。
112
+ - Auto Layout、Token、字体、滚动和溢出无明显问题;截图无重叠、裁切或不可读文字。
113
+
114
+ 完成后返回 Section、Component Set 的链接或节点 ID,并简述 Variants、Scenarios、TBD 和验证结果。
115
+
116
+ ## 示例
117
+
118
+ 输入:“基于这个试定价弹窗 Frame 创建业务主组件;计算后有单笔成功、多笔成功和失败,Reset 返回未计算状态;前端使用 Ant Design 和 AG Grid。”
119
+
120
+ 输出:一个主 Component Set、必要 Variants、带 Event/Guard 的流程、关键 Scenario Instances,以及标明 `CONFIRMED/REFERENCE/TBD` 的简短 Handoff Summary。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Build AI-Coding Component"
3
+ short_description: "Build AI-readable Figma component specs for coding"
4
+ default_prompt: "Use $build-ai-coding-component to turn this existing Figma frame and my business-flow description into a component set, state flow, and scenario frames ready for AI coding."
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: business-logic-trace
3
+ description: 从前端仓库追查某个业务行为、按钮条件、接口调用时机或字段来源,输出带源码定位的调用链与条件表。适用于旧系统理解和迁移前调查;不用于仅解释通用语法、直接实施迁移或安全扫描。
4
+ ---
5
+
6
+ # 业务逻辑溯源
7
+
8
+ 把“当前代码到底怎样工作”收敛为一个有来源、可复查的答案。默认只读,不把历史会话中的结论当作当前源码事实。
9
+
10
+ ## 输入与边界
11
+
12
+ 使用用户指定的仓库、页面或组件,以及行为、接口路径、字段名或截图中的标签。可从当前项目推断仓库;多个候选会改变结论时,只询问缺少的定位信息。读取适用的 `AGENTS.md` 和规范索引。
13
+
14
+ 若同时提供新旧仓库,标明每条证据所属仓库。只有用户要求比较时才检查目标实现,不把目标设计当作旧实现,也不自动开始改代码。
15
+
16
+ ## 调查步骤
17
+
18
+ 1. 定位实际入口。用 `rg` 搜索可见文案、标识符或接口路径,再检查导入和调用方。区分真实页面、演示、测试、废弃实现,以及名字相似的不同入口。
19
+ 2. 沿最短相关链路核对:入口事件 → controller/hook/model → 请求函数与参数 → 数据转换或状态存储 → 最终 UI 消费。问题涉及轮询、订阅、重试或刷新时,再追踪对应触发点与清理条件。
20
+ 3. 根据问题核对以下容易混淆的细节,不机械扩展调查范围:
21
+ - 按钮是否出现、是否禁用、点击后是否被拦截、请求是否发出,是不同结论。
22
+ - 行权限、页面权限、选择数量、产品类型、状态、锁定和草稿标记是否分别参与判断。
23
+ - 多选条件是“全部满足”还是“任一满足”;未知状态是否因黑名单实现而放行。
24
+ - 字段原始来源、重命名、枚举映射和优先级;`??` 与 `||` 的空值语义不能混用。
25
+ - 接口路径是否被公共请求层添加前缀,标识符进入 URL、参数、请求头还是请求体。
26
+ - Mock、注释、类型和测试辅助解释源码,不能单独证明运行入口或后端实际行为。
27
+ 4. 阅读关键分支上下文,追到决定答案的来源和消费者。没有直接引用时检查注册表、动态配置或别名;仍未发现只能说“在已搜索范围未找到”,不能断言接口已废弃。
28
+
29
+ ## 输出与停止条件
30
+
31
+ 先直接回答,再按需要给出短链路或表格:
32
+
33
+ | 入口/条件 | 实际行为 | 参数或字段来源 | 源码证据 |
34
+ | --- | --- | --- | --- |
35
+
36
+ 关键事实附当前文件路径、行号和函数名。注明检查的仓库、日期;跨仓库或迁移对比时补充 HEAD 与相关未提交修改,避免把工作区结论归于某个提交。
37
+
38
+ 将结果区分为“源码确认”“文档定义”“推断/待确认”。静态阅读不声称已经验证线上请求、服务器权限或全部运行场景。用户问“现状”时保留代码事实;发现它与 PRD 冲突,单独列差异,不替任何一方改写事实。
39
+
40
+ 关键条件均有来源,相关上下游已闭合,或剩余缺口已准确说明时结束。只为核对不确定行为运行必要的既有测试;无需为解释代码执行全库测试、迁移或写入知识库。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "业务逻辑溯源"
3
+ short_description: "追查业务入口、接口与字段流向,区分展示禁用并附上源码证据"
4
+ default_prompt: "使用 $business-logic-trace 追查当前页面的按钮条件和接口字段来源。"