@guozhi-fe/skills 0.1.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 +52 -5
- package/bin/gzfe-skills.js +76 -12
- package/catalog.json +36 -0
- package/lib/browse.js +111 -0
- package/lib/catalog.js +26 -0
- package/lib/markdown.js +45 -0
- package/package.json +29 -5
- package/site/app.js +61 -0
- package/site/document.css +1 -0
- package/site/index.html +27 -0
- package/site/styles.css +1 -0
- package/skills/build-ai-coding-component/SKILL.md +120 -0
- package/skills/build-ai-coding-component/agents/openai.yaml +4 -0
- package/skills/business-logic-trace/SKILL.md +40 -0
- package/skills/business-logic-trace/agents/openai.yaml +4 -0
package/README.md
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
# 国智前端 Skills
|
|
2
2
|
|
|
3
|
-
面向 Codex
|
|
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
|
-
|
|
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
|
-
gzfe-skills init
|
|
17
|
+
gzfe-skills init
|
|
16
18
|
gzfe-skills list
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
从 0.2.0 开始,省略技能名会打开交互列表,逐项显示技能名及 `SKILL.md` 中的简介。使用上下键移动、空格勾选、回车确认;勾选一项安装一个技能,勾选多项批量安装。按 `a` 全选或取消全选,按 `Ctrl+C` 取消。未勾选任何技能时按回车会取消安装。
|
|
22
|
+
|
|
23
|
+
直接指定技能名可以跳过交互。脚本中可使用 `--all` 安装全部技能,无终端时需要显式指定技能名或 `--all`。
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
gzfe-skills init build-ai-coding-component
|
|
27
|
+
gzfe-skills init business-logic-trace
|
|
28
|
+
gzfe-skills init microfrontend-memory-assessment
|
|
29
|
+
gzfe-skills init microfrontend-memory-assessment microfrontend-memory-lifecycle
|
|
30
|
+
gzfe-skills init --all
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
文件和版本记录位于当前项目的 `.agents/skills/<技能名>/`,建议一起提交到项目 Git 仓库。重复 init 会保留已安装版本,未勾选的已安装技能也会保留。
|
|
34
|
+
|
|
35
|
+
## 卸载
|
|
36
|
+
|
|
37
|
+
不带参数运行 `uninstall` 会列出本工具管理的已安装技能,使用与 init 相同的勾选方式。也可直接指定技能名,或使用 `--all` 批量卸载。
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
gzfe-skills uninstall
|
|
41
|
+
gzfe-skills uninstall microfrontend-memory-assessment
|
|
42
|
+
gzfe-skills uninstall --all
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
卸载会移除所选技能目录及其安装记录。如果检测到本地文件有改动,会列出文件并停止。全量卸载只处理本工具管理的技能。卸载后可再次运行 `gzfe-skills init` 安装。
|
|
20
46
|
|
|
21
47
|
## 更新
|
|
22
48
|
|
|
@@ -31,4 +57,25 @@ update 默认处理已安装的受管理技能,也可在命令后指定名称
|
|
|
31
57
|
|
|
32
58
|
## 维护
|
|
33
59
|
|
|
34
|
-
编辑 skills 下的技能,变更分发内容时同步提升 package.json
|
|
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` 服务。
|
package/bin/gzfe-skills.js
CHANGED
|
@@ -3,10 +3,16 @@ import fs from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
|
+
import checkbox from '@inquirer/checkbox'
|
|
7
|
+
import { browse } from '../lib/browse.js'
|
|
8
|
+
import { readSkills } from '../lib/catalog.js'
|
|
9
|
+
import colors from 'yoctocolors-cjs'
|
|
10
|
+
import wrapAnsi from 'wrap-ansi'
|
|
6
11
|
|
|
7
12
|
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
8
13
|
const source = path.join(root, 'skills')
|
|
9
|
-
const
|
|
14
|
+
const skillMetadata = readSkills(root)
|
|
15
|
+
const catalog = skillMetadata.map(skill => skill.id)
|
|
10
16
|
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'))).version
|
|
11
17
|
const dest = path.resolve('.agents/skills')
|
|
12
18
|
const marker = '.gzfe-skills.json'
|
|
@@ -51,15 +57,58 @@ function stage(name, dir) {
|
|
|
51
57
|
fs.writeFileSync(path.join(dir, marker), JSON.stringify({ name, version, files: manifest(dir) }, null, 2) + '\n')
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
function
|
|
60
|
+
async function selectSkills(command, names) {
|
|
61
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
62
|
+
throw Error(`交互选择需要终端,请指定技能名,或使用 gzfe-skills ${command} --all。`)
|
|
63
|
+
}
|
|
64
|
+
const choices = names.map(name => {
|
|
65
|
+
const description = skillMetadata.find(skill => skill.id === name).description
|
|
66
|
+
const summary = description.match(/^[^。!?]+[。!?]?/u)?.[0] || description
|
|
67
|
+
const width = Math.max(16, Math.min(88, (process.stdout.columns || 80) - 4))
|
|
68
|
+
const lines = wrapAnsi(summary, width, { hard: true }).split('\n')
|
|
69
|
+
const detail = lines.map(line => ' ' + colors.dim(line)).join('\n')
|
|
70
|
+
return { name: `${colors.bold(name)}\n${detail}\n`, short: name, value: name }
|
|
71
|
+
})
|
|
72
|
+
return checkbox({
|
|
73
|
+
message: `选择要${command === 'init' ? '安装' : '卸载'}的技能(可单选或多选)`,
|
|
74
|
+
choices,
|
|
75
|
+
pageSize: 12,
|
|
76
|
+
theme: {
|
|
77
|
+
style: {
|
|
78
|
+
highlight: text => {
|
|
79
|
+
const [title, ...detail] = text.split('\n')
|
|
80
|
+
return [colors.cyan(title), ...detail].join('\n')
|
|
81
|
+
},
|
|
82
|
+
keysHelpTip: () => '↑↓ 移动 · 空格 勾选 · a 全选/取消全选 · 回车 确认 · Ctrl+C 取消',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function run([command, ...args]) {
|
|
55
89
|
if (!command || ['--help', '-h'].includes(command)) {
|
|
56
|
-
console.log(
|
|
90
|
+
console.log([
|
|
91
|
+
'gzfe-skills browse [--port 4173] [--no-open] 本地浏览技能目录',
|
|
92
|
+
'gzfe-skills init 交互选择技能,支持单选和多选',
|
|
93
|
+
'gzfe-skills init <技能名 ...> 直接安装指定技能',
|
|
94
|
+
'gzfe-skills init --all 安装全部技能,可用于脚本',
|
|
95
|
+
'gzfe-skills list 查看项目安装状态',
|
|
96
|
+
'gzfe-skills update [技能名 ...] [--dry-run]',
|
|
97
|
+
'gzfe-skills uninstall 交互选择要卸载的已安装技能',
|
|
98
|
+
'gzfe-skills uninstall <技能名 ...> 卸载指定技能',
|
|
99
|
+
'gzfe-skills uninstall --all 卸载本工具管理的全部技能',
|
|
100
|
+
'',
|
|
101
|
+
...catalog,
|
|
102
|
+
].join('\n'))
|
|
57
103
|
return
|
|
58
104
|
}
|
|
105
|
+
if (command === 'browse') return browse(root, args)
|
|
59
106
|
if (command === '--version') return console.log(version)
|
|
60
|
-
if (!['init', 'update', 'list'].includes(command)) throw Error('未知命令:' + command)
|
|
107
|
+
if (!['init', 'update', 'list', 'uninstall'].includes(command)) throw Error('未知命令:' + command)
|
|
61
108
|
const dry = command === 'update' && args.includes('--dry-run')
|
|
62
|
-
const
|
|
109
|
+
const all = ['init', 'uninstall'].includes(command) && args.includes('--all')
|
|
110
|
+
let names = [...new Set(args.filter(x => !(dry && x === '--dry-run') && !(all && x === '--all')))]
|
|
111
|
+
if (all && names.length) throw Error('--all 不能与技能名同时使用')
|
|
63
112
|
if (names.some(x => !catalog.includes(x)) || (command === 'list' && names.length)) throw Error('参数无效,请查看 --help')
|
|
64
113
|
for (const dir of [path.dirname(dest), dest]) {
|
|
65
114
|
const entry = fs.lstatSync(dir, { throwIfNoEntry: false })
|
|
@@ -69,12 +118,18 @@ function run([command, ...args]) {
|
|
|
69
118
|
for (const name of catalog) console.log(name, status(name).label)
|
|
70
119
|
return
|
|
71
120
|
}
|
|
121
|
+
if (['init', 'uninstall'].includes(command) && !names.length) {
|
|
122
|
+
const available = command === 'uninstall' ? catalog.filter(name => status(name).saved) : catalog
|
|
123
|
+
if (!available.length) return console.log('当前项目没有由 gzfe-skills 管理的已安装技能')
|
|
124
|
+
names = all ? available : await selectSkills(command, available)
|
|
125
|
+
if (!names.length) return console.log('未选择技能,操作已取消')
|
|
126
|
+
}
|
|
72
127
|
const plan = () => (names.length ? names : catalog).filter(name => {
|
|
73
128
|
const state = status(name)
|
|
74
129
|
if (command === 'update' && !names.length && !state.saved) return false
|
|
75
|
-
if (!state.saved && (command
|
|
130
|
+
if (!state.saved && (command !== 'init' || state.label !== '未安装')) throw Error(name + ':' + state.label)
|
|
76
131
|
if (state.changes?.length) throw Error(name + ' 有本地修改:\n' + state.changes.join('\n'))
|
|
77
|
-
return !state.saved || (command === 'update' && state.saved.version !== version)
|
|
132
|
+
return command === 'uninstall' || !state.saved || (command === 'update' && state.saved.version !== version)
|
|
78
133
|
})
|
|
79
134
|
if (dry) {
|
|
80
135
|
const selected = plan()
|
|
@@ -87,17 +142,21 @@ function run([command, ...args]) {
|
|
|
87
142
|
let cleanup = true
|
|
88
143
|
try {
|
|
89
144
|
const selected = plan()
|
|
90
|
-
|
|
145
|
+
if (command !== 'uninstall') {
|
|
146
|
+
for (const name of selected) stage(name, path.join(work, name))
|
|
147
|
+
}
|
|
91
148
|
for (const name of selected) {
|
|
92
149
|
const target = path.join(dest, name)
|
|
93
150
|
const backup = path.join(work, name + '.old')
|
|
94
151
|
const existed = fs.existsSync(target)
|
|
95
152
|
if (existed) fs.renameSync(target, backup)
|
|
96
153
|
applied.push({ target, backup, existed, installed: false })
|
|
154
|
+
if (command === 'uninstall') continue
|
|
97
155
|
fs.renameSync(path.join(work, name), target)
|
|
98
156
|
applied.at(-1).installed = true
|
|
99
157
|
}
|
|
100
|
-
|
|
158
|
+
const result = command === 'uninstall' ? '已卸载:' + selected.join(', ') : selected.join(', ') + ' → ' + version
|
|
159
|
+
console.log(selected.length ? result : '无需变更')
|
|
101
160
|
} catch (error) {
|
|
102
161
|
cleanup = false
|
|
103
162
|
for (const item of applied.reverse()) {
|
|
@@ -110,7 +169,12 @@ function run([command, ...args]) {
|
|
|
110
169
|
if (cleanup) fs.rmSync(work, { recursive: true })
|
|
111
170
|
}
|
|
112
171
|
}
|
|
113
|
-
try { run(process.argv.slice(2)) } catch (error) {
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
try { await run(process.argv.slice(2)) } catch (error) {
|
|
173
|
+
if (error.name === 'ExitPromptError') {
|
|
174
|
+
console.log('操作已取消')
|
|
175
|
+
process.exitCode = 130
|
|
176
|
+
} else {
|
|
177
|
+
console.error(error.message)
|
|
178
|
+
process.exitCode = 1
|
|
179
|
+
}
|
|
116
180
|
}
|
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
|
+
}
|
package/lib/markdown.js
ADDED
|
@@ -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,10 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guozhi-fe/skills",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "国智前端项目级 Codex 技能集",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
6
|
+
"bin": {
|
|
7
|
+
"gzfe-skills": "bin/gzfe-skills.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skills",
|
|
12
|
+
"lib",
|
|
13
|
+
"catalog.json",
|
|
14
|
+
"site"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"repository": "github:soleilLulu/gz-fe-skills",
|
|
20
|
+
"scripts": {
|
|
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"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@inquirer/checkbox": "^4.3.2",
|
|
29
|
+
"markdown-it": "^14.3.3",
|
|
30
|
+
"wrap-ansi": "^6.2.0",
|
|
31
|
+
"yaml": "^2.9.1",
|
|
32
|
+
"yoctocolors-cjs": "^2.1.3"
|
|
33
|
+
}
|
|
10
34
|
}
|
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}}
|
package/site/index.html
ADDED
|
@@ -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>
|
package/site/styles.css
ADDED
|
@@ -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
|
+
关键条件均有来源,相关上下游已闭合,或剩余缺口已准确说明时结束。只为核对不确定行为运行必要的既有测试;无需为解释代码执行全库测试、迁移或写入知识库。
|