@szc-ft/mcp-szcd-client 0.23.1 → 0.25.0
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/agents/build.js +22 -12
- package/agents/opencode-extension/agents/szcd-component-expert.md +94 -50
- package/agents/qwen-extension/agents/szcd-component-expert.md +94 -50
- package/agents/src/szcd-component-expert.md +94 -50
- package/agents/src/tools.json +1 -1
- package/agents/szcd-component-expert.md +94 -50
- package/agents/szcd-component-expert.qoder.md +95 -51
- package/agents/szcd-component-expert.trae.md +94 -50
- package/opencode-extension/agents/szcd-component-expert.md +94 -50
- package/opencode-extension/skills/local-api-tool/SKILL.md +164 -60
- package/opencode-extension/skills/local-api-tool/scripts/extract_swagger.py +477 -0
- package/opencode-extension/skills/szcd-component-helper/SKILL.md +22 -17
- package/opencode-extension/skills/szcd-design-to-code/SKILL.md +141 -31
- package/package.json +1 -1
- package/qwen-extension/agents/szcd-component-expert.md +94 -50
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-api-tool/SKILL.md +164 -60
- package/qwen-extension/skills/local-api-tool/scripts/extract_swagger.py +477 -0
- package/qwen-extension/skills/szcd-component-helper/SKILL.md +22 -17
- package/qwen-extension/skills/szcd-design-to-code/SKILL.md +141 -31
- package/scripts/lib/claude-code.js +1 -1
- package/scripts/lib/common.js +44 -0
- package/scripts/lib/opencode.js +1 -1
- package/scripts/lib/qoder.js +1 -1
- package/scripts/lib/trae-cli.js +1 -1
- package/scripts/lib/trae-ide.js +1 -1
- package/scripts/postinstall.js +1 -0
- package/standard-skill/local-api-tool/SKILL.md +164 -60
- package/standard-skill/local-api-tool/scripts/extract_swagger.py +477 -0
- package/standard-skill/szcd-component-helper/SKILL.md +22 -17
- package/standard-skill/szcd-design-to-code/SKILL.md +141 -31
|
@@ -55,50 +55,146 @@ Ant Design → ProComponents → Cover 层 → Wrapper 层 → ProPackages →
|
|
|
55
55
|
|
|
56
56
|
### 步骤2.5:Sketch 文件解析(有 .sketch 文件时执行)
|
|
57
57
|
|
|
58
|
-
**核心原则**:写代码之前必须拿到所有 `text
|
|
58
|
+
**核心原则**:写代码之前必须拿到所有 `text`、`rectangle`、`group` 节点的完整样式 Token + 父子层级,禁止凭节点名猜测颜色/字号/背景色,禁止用 x 坐标平面判定元素归属。`queryNodes.type` 接受数组、`getMultipleNodeInfo` 内部自动分片,比逐个 `getNodeInfo` 快 10 倍以上。
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
**工具调用顺序(sketch-mcp-server ≥ v1.5.0,4 次主调用)**:
|
|
61
61
|
|
|
62
|
-
1. `mcp__sketch-mcp-server__loadSketchByPath` —
|
|
62
|
+
1. `mcp__sketch-mcp-server__loadSketchByPath` — 加载文件(内部自动解压并保存目录,供后续 extractBitmaps 使用)
|
|
63
63
|
2. `mcp__sketch-mcp-server__listPages` — 获取页面列表
|
|
64
|
-
3. `mcp__sketch-mcp-
|
|
65
|
-
4. `mcp__sketch-mcp-server__getPageStructure`(pageId
|
|
66
|
-
|
|
67
|
-
- `
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
3. `mcp__sketch-mcp-server__queryNodes`(pageId, type="artboard")— 获取画板列表,确定目标画板
|
|
65
|
+
4. `mcp__sketch-mcp-server__getPageStructure`(pageId, mode="tree")— **先看完整层级关系**,识别顶层 group 分区(如 LeftTree 区/Query+Table 区/信息栏区/标题栏区)
|
|
66
|
+
- 再用 `mode="ids"` 拿到按类型分组的全量 ID:`{ text: [...], rectangle: [...], group: [...], bitmap: [...], shapePath: [...] }`
|
|
67
|
+
- 同时返回 `summary`(各类型统计)和 `totalIds`(总数)
|
|
68
|
+
5. `mcp__sketch-mcp-server__getMultipleNodeInfo`([**所有 text + rectangle + group ID 同批提交**])— **支持任意数量 ID,内部自动分批处理**
|
|
69
|
+
- 关键字段:`style.fills[0].color.hex`(背景色)、`text.color`(文字色)、`style.fontSize`、`style.fontWeight`、`style.borders[0]`、`style.shadows`
|
|
70
|
+
- **`parent: { id, name, type }`**(v1.5.0+ 新增)— 元素归属判定的核心字段,**始终用 parent 走 group 父子关系,禁止用 x/y 坐标判定归属**
|
|
71
|
+
- **禁止只取 text 类型**:会遗漏 rectangle/group 容器的渐变背景、边框、阴影;信息栏/卡片类区块必须连容器一起拉
|
|
72
|
+
- 无需手动分批,传入全部 ID 即可(内部按 100 个一批自动处理)
|
|
73
|
+
|
|
74
|
+
**替代方案(更短,3 次主调用)**:当只关心特定类型节点时,可用 `queryNodes` 一次性拿数组类型:
|
|
75
|
+
```
|
|
76
|
+
queryNodes({ pageId, type: ["text", "rectangle", "group"], limit: 500 })
|
|
77
|
+
→ 返回这三类节点的 ID 列表,跳过 getPageStructure(mode="ids") 步骤
|
|
78
|
+
```
|
|
79
|
+
适用场景:已通过 `getPageStructure(mode="tree")` 看清分区,只想批量获取关键 token 节点时。
|
|
79
80
|
|
|
80
81
|
**步骤 2.5 产出物**(**只产出数据,不做组件映射**):
|
|
81
|
-
-
|
|
82
|
-
- text/rectangle 节点的完整样式 Token
|
|
83
|
-
-
|
|
82
|
+
- 按类型分组的全量节点 ID 列表
|
|
83
|
+
- text/rectangle/group 节点的完整样式 Token + parent 父子关系
|
|
84
|
+
- bitmap ID 列表(传给步骤 2.5.1)
|
|
85
|
+
- shapePath 节点名列表(传给步骤 2.5.1)
|
|
84
86
|
|
|
85
87
|
组件候选列表推理**不**在步骤 2.5 内部完成,**统一在主流程中结合步骤 1 的 `templatePatterns` 推理**(详见主流程步骤 4 前置说明)。
|
|
86
88
|
|
|
89
|
+
### 步骤2.5.1:图标与位图提取(有 bitmap/shapePath 节点时执行)
|
|
90
|
+
|
|
91
|
+
**输入**:步骤 2.5 产出的 bitmap ID 列表 + shapePath 节点名列表
|
|
92
|
+
|
|
93
|
+
**工具调用**:
|
|
94
|
+
|
|
95
|
+
1. `mcp__sketch-mcp-server__extractBitmaps`(bitmapIds)— 从 .sketch 解压目录提取 bitmap PNG 资源
|
|
96
|
+
- 返回 `{ bitmaps: [{ nodeId, name, base64, mimeType, size, fileName }] }`
|
|
97
|
+
- 位图资源用于 CSS `background-image` 引用
|
|
98
|
+
2. `mcp__sketch-mcp-server__matchIconFromName`(nodeNames, library="newFont")— 匹配 iconfont 图标库(v1.5.0+ 评分制)
|
|
99
|
+
- 输入 shapePath 节点名列表(如 `["表 icon", "新建文件夹", "编组11备份"]`)
|
|
100
|
+
- **评分机制**:compoundIcons 强映射 +100、keyword 精确 +20、keyword 包含 +10、semanticTag +3
|
|
101
|
+
- 每个 nodeName 返回 **Top-3 候选**(按分数降序),让你做最终选择
|
|
102
|
+
- 返回格式:
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"results": [
|
|
106
|
+
{
|
|
107
|
+
"nodeName": "新建文件夹",
|
|
108
|
+
"top3": [
|
|
109
|
+
{ "source": "compound", "score": 104, "antdIcon": "FolderAddOutlined", "matchedBy": "文件夹|目录|folder + 新增|添加|add|plus|新建|create", "rationale": "..." },
|
|
110
|
+
{ "source": "compound", "score": 104, "antdIcon": "FileAddOutlined" },
|
|
111
|
+
{ "source": "compound", "score": 102, "antdIcon": "FolderOutlined" }
|
|
112
|
+
],
|
|
113
|
+
"bestMatch": { "source": "compound", "antdIcon": "FolderAddOutlined" }
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"matchedIcons": [...], // Top-1 兼容旧字段
|
|
117
|
+
"unmatched": ["编组11备份"], // 无关键词信号的节点
|
|
118
|
+
"recommendFallback": { "编组11备份": "antd QuestionOutlined" },
|
|
119
|
+
"matchRate": "66.7%"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**⚠️ 图标语义优先级(v1.5.0 已内置 compoundIcons,多数场景自动命中)**:
|
|
124
|
+
|
|
125
|
+
实战教训:Sketch 中"编组11备份"实际是 folder-plus 图标,旧版被误判为通用 PlusOutlined(优先命中"新增"关键词吃掉了 folder 语义)。v1.5.0 评分制 + compoundIcons 强映射已修复该类问题。
|
|
126
|
+
|
|
127
|
+
**判定流程**:
|
|
128
|
+
1. **优先看 `bestMatch.source`**:`compound` 表示触发了复合词强映射,可直接采用 `antdIcon`
|
|
129
|
+
2. **看 Top-3 是否有合适的候选**:分数接近的候选都值得考虑
|
|
130
|
+
3. **`unmatched` 节点(如"编组11备份"无关键词信号)**:必须看节点 `parent.name` 和兄弟节点上下文兜底
|
|
131
|
+
- 例:parent 是 LeftTree 标题区 → 多半是文件夹/分类操作图标 → `FolderAddOutlined` / `FolderOpenOutlined`
|
|
132
|
+
- 例:parent 是 Query 操作栏 → 多半是表单动作图标 → `SearchOutlined` / `PlusOutlined`
|
|
133
|
+
|
|
134
|
+
**参考表(内置 compoundIcons 已覆盖;列出供 unmatched 兜底参考)**:
|
|
135
|
+
|
|
136
|
+
| Sketch 节点上下文 | 期望 antd 图标 | 反例(不要选) |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| 文件夹+新增/添加 | `FolderAddOutlined` | PlusOutlined |
|
|
139
|
+
| 文件+新增 | `FileAddOutlined` | PlusOutlined |
|
|
140
|
+
| 用户+新增 | `UserAddOutlined` | PlusOutlined |
|
|
141
|
+
| 文件夹+打开 | `FolderOpenOutlined` | FolderOutlined |
|
|
142
|
+
| 编辑+笔 | `EditOutlined` | FormOutlined |
|
|
143
|
+
| 设置+齿轮 | `SettingOutlined` | ToolOutlined |
|
|
144
|
+
|
|
145
|
+
**操作流程**:matchIconFromName 返回 unmatched 或 fallback 为通用 `PlusOutlined/EditOutlined` 时,**先看节点 parent group 的名称/兄弟节点**,按上表选语义更精确的复合图标。
|
|
146
|
+
|
|
147
|
+
**步骤 2.5.1 产出物**:
|
|
148
|
+
- bitmap base64 资源列表 → 用于代码中的 CSS background-image
|
|
149
|
+
- IconfontWapper 代码片段(`<IconfontWapper assets="newFont" type="icon-biao" />`)
|
|
150
|
+
- 未匹配节点的 fallback 建议(antd 图标替代)
|
|
151
|
+
|
|
152
|
+
### 步骤2.5.2:批量样式提取(合并入步骤 2.5)
|
|
153
|
+
|
|
154
|
+
此步骤已合并入步骤 2.5 第 5 步,`getMultipleNodeInfo` 现在支持自动分片,无需手动分批调用。
|
|
155
|
+
|
|
87
156
|
### 步骤2.5 实战经验沉淀(重要)
|
|
88
157
|
|
|
89
|
-
**经验1
|
|
90
|
-
-
|
|
91
|
-
- 正确做法:`getPageStructure(pageId)`
|
|
158
|
+
**经验1:用 `getPageStructure(mode="ids")` 一步拿全量 ID,避免 getPageStructure 截断**
|
|
159
|
+
- 错误做法:用 `getPageStructure(pageId)` 拿节点树 → 大文件截断 → 需要 agent 解析
|
|
160
|
+
- 正确做法:`getPageStructure(pageId, mode="ids")` 轻量返回 `{ text: [ids], rectangle: [ids], bitmap: [ids] }` → 无截断风险
|
|
161
|
+
- 如需节点层级关系,用 `getPageStructure(pageId, fields=["id","name","type"])` 过滤字段
|
|
162
|
+
|
|
163
|
+
**经验2:`getMultipleNodeInfo` 支持任意数量,无需手动分批**
|
|
164
|
+
- 旧做法:手动把 ID 分成每批 ≤ 100 个 → 多次调用
|
|
165
|
+
- 新做法:直接传入全部 ID(如 200 个)→ 内部自动按 100 个一批处理 → 返回合并结果
|
|
166
|
+
- 返回结构不变:`{ nodes: [...], total, requested, batches }`
|
|
167
|
+
|
|
168
|
+
**经验3:元素归属必须走 group 父子关系,禁止 x 坐标平面判定**
|
|
92
169
|
|
|
93
|
-
|
|
170
|
+
实战教训:`'主题'text(x=272,y=149)` 被错误识别为内容区块标题,实际是左侧树标题(与树节点同 x=272 列)。同列坐标不代表同区域。
|
|
171
|
+
|
|
172
|
+
正确做法:
|
|
173
|
+
- 步骤 2.5 第 4 步先 `mode="tree"` 看顶层 group 分区(通常 2-5 个,对应 LeftTree区/Query+Table区/信息栏区/标题栏区)
|
|
174
|
+
- `getMultipleNodeInfo`/`getNodeInfo` v1.5.0+ **自动返回** `parent: { id, name, type }` 字段,按 parent 归属到具体分区即可
|
|
175
|
+
- "主题"这类标题文本,先看 `parent.name` 是否带 "LeftTree" / "树" 等容器关键字,不要看 x 坐标
|
|
176
|
+
- 坐标仅作为同 parent 内子元素排序兜底使用
|
|
177
|
+
|
|
178
|
+
**经验4:信息栏/卡片容器必须先取 group 再取子元素**
|
|
179
|
+
|
|
180
|
+
实战教训:步骤 2.5 仅提取 text 节点导致信息栏容器样式遗漏(渐变背景 #f3faff→#f8fdff、边框 #e9f5ff、图标编组)。
|
|
181
|
+
|
|
182
|
+
正确做法:
|
|
183
|
+
- 看到画板里有 `编组N`/`infoCard`/`statBox` 等 group 节点,必须连 group 自身和内部 rectangle 一起拉 `getMultipleNodeInfo`
|
|
184
|
+
- group 节点的 `style.fills` 才是渐变背景、`style.borders` 才是边框
|
|
185
|
+
- 子图标若为 group(rectangle + shapePath),rectangle 是圆形/方形背景,shapePath 是矢量图标
|
|
186
|
+
|
|
187
|
+
**经验5:text 节点是颜色/DOM 映射的两级索引**
|
|
94
188
|
每个 text 节点同时关联两种上下文,**必须同批拉取**:
|
|
95
189
|
- 自身样式:`fontSize`、`fontWeight`、`text.color`
|
|
96
190
|
- 所属 group 父级 rectangle 样式:`bg`、`border`、`radius`、`shadow`
|
|
97
191
|
|
|
98
192
|
错误做法:先查 text 拿自身样式 → 再根据 parent ID 单独查 rectangle(多走一步)
|
|
99
|
-
|
|
193
|
+
正确做法:
|
|
194
|
+
- 用 `queryNodes({ pageId, type: ["text", "rectangle", "group"], limit: 500 })` 一次拿三类节点的 ID
|
|
195
|
+
- 再用 `getMultipleNodeInfo([...全部ID])` 同批提交,每个节点返回的 `parent` 字段直接告诉你归属关系,无需二次查询
|
|
100
196
|
|
|
101
|
-
**经验
|
|
197
|
+
**经验6:图表区样式提取有规律可循**
|
|
102
198
|
|
|
103
199
|
| 图表子元素 | 对应 Sketch 节点类型 | 关键样式字段 |
|
|
104
200
|
|---|---|---|
|
|
@@ -113,10 +209,11 @@ Ant Design → ProComponents → Cover 层 → Wrapper 层 → ProPackages →
|
|
|
113
209
|
|
|
114
210
|
识别出图表后,**图例色块**应归入 ECharts `legend` 配置(不要用 div 硬编码),**柱体色**映射到 `series.itemStyle.color`。
|
|
115
211
|
|
|
116
|
-
**经验
|
|
117
|
-
- `shapePath` / `shapeGroup` 在 sketch 节点中占比可能高达 46.8
|
|
118
|
-
-
|
|
119
|
-
-
|
|
212
|
+
**经验7:矢量图形用 matchIconFromName 自动匹配,无需手动解压**
|
|
213
|
+
- `shapePath` / `shapeGroup` 在 sketch 节点中占比可能高达 46.8%,但 `renderNodeAsBase64` 不支持渲染
|
|
214
|
+
- **新方案**:`matchIconFromName(shapePathNames)` 自动查询 iconfont 库 → 返回匹配结果
|
|
215
|
+
- bitmap 用 `extractBitmaps(bitmapIds)` 自动提取 PNG 资源
|
|
216
|
+
- 兜底:未匹配的节点用 antd `@ant-design/icons` 同语义图标替代,在代码注释中标注“⚠️ 矢量图标用 antd 图标替代,原始 Sketch 为 shapePath”
|
|
120
217
|
|
|
121
218
|
### 步骤3:分析设计稿图片(有图片时执行)
|
|
122
219
|
|
|
@@ -167,6 +264,19 @@ Ant Design → ProComponents → Cover 层 → Wrapper 层 → ProPackages →
|
|
|
167
264
|
| 柱状对比图 | `Bar` |
|
|
168
265
|
| 饼图/环形图 | `Pie` |
|
|
169
266
|
|
|
267
|
+
### ⚠️ TemplateMode 槽位传参铁律(高频踩坑)
|
|
268
|
+
|
|
269
|
+
**LeftContent / Query / PageContent 三个槽位都接收单个 ReactNode,禁止传数组**:
|
|
270
|
+
|
|
271
|
+
| ❌ 错误 | ✅ 正确 | 后果 |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `LeftContent={[<LeftTree/>]}` | `LeftContent={<LeftTree/>}` | 数组会触发多余 tab 渲染 |
|
|
274
|
+
| `LeftContent={[<LeftTree/>, <Filter/>]}` | `LeftContent={<><LeftTree/><Filter/></>}` 或包一层 div | 同上 |
|
|
275
|
+
| `Query={[<Query/>]}` | `Query={<Query/>}` | 同上 |
|
|
276
|
+
| `PageContent={[<TableOrList/>]}` | `PageContent={<TableOrList/>}` | 同上 |
|
|
277
|
+
|
|
278
|
+
**判定方法**:Sketch 中只要某个分区是"单棵树/单个查询面板/单个表格",对应槽位就传单个组件实例。需要多个并列时用 Fragment `<>...</>` 或容器 div 包裹,**不要用数组**。
|
|
279
|
+
|
|
170
280
|
**长尾场景走语义搜索**(不进文档,避免膨胀):
|
|
171
281
|
- 数据可视化长尾:`Radar` / `Gauge` / `Funnel`(用 `Line/Bar/Pie` 之外的图表)
|
|
172
282
|
- 表格子类型长尾:`DragSortTable`(拖拽排序)、`steps` 步骤条表单
|
|
@@ -152,7 +152,7 @@ function copySkillToClaudeCode(deps, isProjectLevel = false) {
|
|
|
152
152
|
try {
|
|
153
153
|
deps.ensureDirectory(skillDir);
|
|
154
154
|
const skillDest = path.join(skillDir, "SKILL.md");
|
|
155
|
-
deps.
|
|
155
|
+
deps.copySkill(skill.sourcePath, skillDest);
|
|
156
156
|
console.log(`✓ Copied skill "${skill.name}" to Claude Code ${isProjectLevel ? 'project' : 'user'} directory: ${skillDir}`);
|
|
157
157
|
} catch (error) {
|
|
158
158
|
console.log(`⚠️ Failed to copy skill "${skill.name}" to Claude Code ${isProjectLevel ? 'project' : 'user'} directory: ${error.message}`);
|
package/scripts/lib/common.js
CHANGED
|
@@ -140,6 +140,50 @@ export function copyFile(source, destination) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 递归复制目录(用于 skill 目录这种含子文件/子目录的场景)
|
|
145
|
+
* 不存在的源直接抛错;目标会被创建。
|
|
146
|
+
*/
|
|
147
|
+
export function copyDirRecursive(source, destination) {
|
|
148
|
+
if (!fs.existsSync(source)) {
|
|
149
|
+
throw new Error(`Source directory not found: ${source}`);
|
|
150
|
+
}
|
|
151
|
+
if (!fs.existsSync(destination)) {
|
|
152
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
155
|
+
const src = path.join(source, entry.name);
|
|
156
|
+
const dest = path.join(destination, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
copyDirRecursive(src, dest);
|
|
159
|
+
} else {
|
|
160
|
+
fs.copyFileSync(src, dest);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 复制 skill:以 SKILL.md 所在目录为单位,递归复制整个目录到目标 skill 目录。
|
|
167
|
+
* 这样 standard-skill/<skill>/scripts/ 等附属内容会一并带走。
|
|
168
|
+
*
|
|
169
|
+
* @param {string} skillSourceFile - SKILL.md 的绝对路径(discoverSkills() 返回的 sourcePath)
|
|
170
|
+
* @param {string} destSkillFile - 目标 SKILL.md 的绝对路径(保持现有调用方习惯)
|
|
171
|
+
*/
|
|
172
|
+
export function copySkill(skillSourceFile, destSkillFile) {
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.existsSync(skillSourceFile)) {
|
|
175
|
+
throw new Error(`Skill source file not found: ${skillSourceFile}`);
|
|
176
|
+
}
|
|
177
|
+
const srcDir = path.dirname(skillSourceFile);
|
|
178
|
+
const destDir = path.dirname(destSkillFile);
|
|
179
|
+
copyDirRecursive(srcDir, destDir);
|
|
180
|
+
console.log(`✓ Copied skill: ${srcDir} → ${destDir}`);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`❌ Failed to copy skill: ${error.message}`);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
143
187
|
export function writeFile(filePath, content) {
|
|
144
188
|
try {
|
|
145
189
|
fs.writeFileSync(filePath, content);
|
package/scripts/lib/opencode.js
CHANGED
|
@@ -295,7 +295,7 @@ function copySkillToOpenCode(deps, isProjectLevel = false) {
|
|
|
295
295
|
try {
|
|
296
296
|
deps.ensureDirectory(skillDir);
|
|
297
297
|
const skillDest = path.join(skillDir, "SKILL.md");
|
|
298
|
-
deps.
|
|
298
|
+
deps.copySkill(skill.sourcePath, skillDest);
|
|
299
299
|
console.log(`✓ Copied skill "${skill.name}" to OpenCode ${isProjectLevel ? 'project' : 'user'} directory: ${skillDir}`);
|
|
300
300
|
} catch (error) {
|
|
301
301
|
console.log(`⚠️ Failed to copy skill "${skill.name}" to OpenCode ${isProjectLevel ? 'project' : 'user'} directory: ${error.message}`);
|
package/scripts/lib/qoder.js
CHANGED
|
@@ -192,7 +192,7 @@ function copySkillToQoder(deps, isProjectLevel = false) {
|
|
|
192
192
|
try {
|
|
193
193
|
deps.ensureDirectory(skillDir);
|
|
194
194
|
const skillDest = path.join(skillDir, "SKILL.md");
|
|
195
|
-
deps.
|
|
195
|
+
deps.copySkill(skill.sourcePath, skillDest);
|
|
196
196
|
console.log(`✓ Copied skill "${skill.name}" to Qoder ${isProjectLevel ? "project" : "user"} directory: ${skillDir}`);
|
|
197
197
|
} catch (error) {
|
|
198
198
|
console.log(`⚠️ Failed to copy skill "${skill.name}" to Qoder ${isProjectLevel ? "project" : "user"} directory: ${error.message}`);
|
package/scripts/lib/trae-cli.js
CHANGED
|
@@ -226,7 +226,7 @@ export function copySkillToTraeCli(deps, isProjectLevel = false) {
|
|
|
226
226
|
try {
|
|
227
227
|
deps.ensureDirectory(skillDir);
|
|
228
228
|
const skillDest = path.join(skillDir, "SKILL.md");
|
|
229
|
-
deps.
|
|
229
|
+
deps.copySkill(skill.sourcePath, skillDest);
|
|
230
230
|
console.log(`✓ Copied skill "${skill.name}" to Trae CLI ${isProjectLevel ? 'project' : 'user'} directory: ${skillDir}`);
|
|
231
231
|
} catch (error) {
|
|
232
232
|
console.log(`⚠️ Failed to copy skill "${skill.name}" to Trae CLI ${isProjectLevel ? 'project' : 'user'} directory: ${error.message}`);
|
package/scripts/lib/trae-ide.js
CHANGED
|
@@ -207,7 +207,7 @@ export function copySkillToTrae(deps, isProjectLevel = false) {
|
|
|
207
207
|
try {
|
|
208
208
|
deps.ensureDirectory(skillDir);
|
|
209
209
|
const skillDest = path.join(skillDir, "SKILL.md");
|
|
210
|
-
deps.
|
|
210
|
+
deps.copySkill(skill.sourcePath, skillDest);
|
|
211
211
|
console.log(`✓ Copied skill "${skill.name}" to Trae ${isProjectLevel ? 'project' : 'user'} directory: ${skillDir}`);
|
|
212
212
|
} catch (error) {
|
|
213
213
|
console.log(`⚠️ Failed to copy skill "${skill.name}" to Trae ${isProjectLevel ? 'project' : 'user'} directory: ${error.message}`);
|
package/scripts/postinstall.js
CHANGED
|
@@ -39,6 +39,7 @@ const deps = {
|
|
|
39
39
|
isCommandAvailable: common.isCommandAvailable,
|
|
40
40
|
ensureDirectory: common.ensureDirectory,
|
|
41
41
|
copyFile: common.copyFile,
|
|
42
|
+
copySkill: common.copySkill,
|
|
42
43
|
writeFile: common.writeFile,
|
|
43
44
|
discoverSkills: common.discoverSkills,
|
|
44
45
|
SKILL_SOURCE: common.SKILL_SOURCE,
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: local-api-tool
|
|
3
|
-
description: 本地网络 API 工具,专门处理 10.x.x.x 网段的 Swagger/YApi
|
|
3
|
+
description: 本地网络 API 工具,专门处理 10.x.x.x 网段的 Swagger/YApi 文档拉取和联调测试。内置 scripts/extract_swagger.py 一步完成"下载→解析→按 tag 过滤→关联 DTO definitions→输出",与服务端 parseApiDocs 一比一对齐(递归深度 5、两层 definitions、传递闭包裁剪),避免 curl 管道截断和手写花括号平衡脚本的弯路。仅当脚本不可用时回退到服务端 parse_swagger_json 兜底。**仅限 10.x.x.x 网段**,172.16-31.x.x 和 192.168.x.x 服务端可直连,不要使用本技能。
|
|
4
4
|
compatibility:
|
|
5
5
|
tools:
|
|
6
6
|
- run_command
|
|
7
7
|
- web_search
|
|
8
8
|
- web_fetch
|
|
9
|
+
runtime:
|
|
10
|
+
- python3 >= 3.7
|
|
11
|
+
- curl
|
|
9
12
|
---
|
|
10
13
|
|
|
11
14
|
# 本地网络 API 工具(local-api-tool)
|
|
@@ -37,25 +40,50 @@ compatibility:
|
|
|
37
40
|
|
|
38
41
|
### 流程 A:拉取 Swagger API 文档(fetch)
|
|
39
42
|
|
|
43
|
+
> **优先使用内置脚本 `scripts/extract_swagger.py`**,一步完成"下载 → 解析 → 过滤 → 关联 DTO → 输出",避免 curl 管道截断、临时脚本花括号平衡等弯路。脚本与服务端 `parseApiDocs` 一比一对齐(递归深度 5、两层 definitions、传递闭包裁剪)。
|
|
44
|
+
> 仅当脚本不可用(无 python3 / 文件权限缺失)时才回退到「手工 curl + 服务端 parse_swagger_json」兜底流程。
|
|
45
|
+
|
|
46
|
+
#### A1. 内置脚本路径(首选)
|
|
47
|
+
|
|
40
48
|
```
|
|
41
49
|
用户请求 fetch Swagger URL (10.x.x.x)
|
|
42
50
|
│
|
|
43
51
|
▼
|
|
44
|
-
|
|
52
|
+
AI 识别 10.x.x.x 网段(或服务端返回 LOCAL_NETWORK_UNREACHABLE)
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
通过 run_command 在本地执行:
|
|
56
|
+
bash $SKILL_DIR/scripts/extract_swagger.py \
|
|
57
|
+
--url <apiDocsUrl> [--cookie '...'] [--filter <关键词>] [--out /tmp/parsed.json]
|
|
45
58
|
│
|
|
46
59
|
▼
|
|
47
|
-
|
|
60
|
+
脚本内部:curl 下载 → 解析 → 过滤 paths → 自动展开被引用 DTO → 输出 JSON
|
|
61
|
+
│
|
|
62
|
+
▼
|
|
63
|
+
AI 读取脚本输出(或 --out 指定的文件),结构与 api_tool(parse_swagger_json) 完全一致:
|
|
64
|
+
{ info, basePath, tags, apis, definitions, definitionsDetail, _filter? }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**$SKILL_DIR 是本 skill 的绝对目录**。在 qwen/qoder 中 skill 加载时通常通过环境变量或上下文提供,若不确定可用:
|
|
68
|
+
- qwen-extension: `~/.qwen/extensions/szcd-component-helper/skills/local-api-tool/scripts/extract_swagger.py`
|
|
69
|
+
- opencode-extension: `~/.config/opencode/extensions/szcd-component-helper/skills/local-api-tool/scripts/extract_swagger.py`
|
|
70
|
+
|
|
71
|
+
#### A2. 服务端兜底(脚本不可用时)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
调用 api_tool(action="fetch", url=...)
|
|
48
75
|
│
|
|
49
76
|
▼
|
|
50
|
-
|
|
77
|
+
服务端返回 { reason: "LOCAL_NETWORK_UNREACHABLE", swaggerEndpoints, authInfo }
|
|
51
78
|
│
|
|
52
79
|
▼
|
|
53
|
-
AI 通过 run_command
|
|
80
|
+
AI 通过 run_command 在本地 curl 写到临时文件(禁止管道截断!):
|
|
54
81
|
步骤1: 鉴权获取 Cookie(如 authInfo.hasPassword 为 true)
|
|
55
|
-
步骤2:
|
|
82
|
+
步骤2: curl -o /tmp/swagger.json <apiDocsUrl> ← 必须写文件
|
|
56
83
|
│
|
|
57
84
|
▼
|
|
58
|
-
AI
|
|
85
|
+
AI 读取整个文件传给服务端:
|
|
86
|
+
api_tool(action="parse_swagger_json", swaggerJson=<完整 JSON 字符串>, apiFilter=<可选>)
|
|
59
87
|
│
|
|
60
88
|
▼
|
|
61
89
|
返回解析后的 API 文档给用户
|
|
@@ -84,7 +112,97 @@ AI 通过 run_command 在本地执行对应 curl 命令
|
|
|
84
112
|
|
|
85
113
|
## 详细执行步骤
|
|
86
114
|
|
|
87
|
-
### 一、拉取 Swagger
|
|
115
|
+
### 一、拉取 Swagger 文档(推荐路径:extract_swagger.py)
|
|
116
|
+
|
|
117
|
+
**前提**:本地有 python3(≥ 3.7)和 curl。脚本只依赖标准库,无第三方依赖。
|
|
118
|
+
|
|
119
|
+
#### 1.1 直接通过 URL 下载并解析(推荐)
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# 无鉴权
|
|
123
|
+
python3 ~/.qwen/extensions/szcd-component-helper/skills/local-api-tool/scripts/extract_swagger.py \
|
|
124
|
+
--url 'http://10.2.7.60:30194/ikmp-knowledge/v2/api-docs' \
|
|
125
|
+
--out /tmp/parsed.json
|
|
126
|
+
|
|
127
|
+
# 有鉴权(先单独 curl 拿 Cookie,参考下方 1.3)
|
|
128
|
+
python3 .../extract_swagger.py \
|
|
129
|
+
--url 'http://10.2.7.60:30194/ikmp-knowledge/v2/api-docs' \
|
|
130
|
+
--cookie 'JSESSIONID=xxx' \
|
|
131
|
+
--out /tmp/parsed.json
|
|
132
|
+
|
|
133
|
+
# 按关键词过滤 + 自动裁剪 definitionsDetail(仅保留被引用的 VO 及传递闭包)
|
|
134
|
+
python3 .../extract_swagger.py \
|
|
135
|
+
--url '<apiDocsUrl>' \
|
|
136
|
+
--filter tag --filter user \
|
|
137
|
+
--out /tmp/parsed.json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### 1.2 读取已下载的本地 JSON 文件
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 第一步:curl 写文件(务必用 -o 写文件,不要用管道,避免大 JSON 被截断)
|
|
144
|
+
curl -sS --connect-timeout 10 --max-time 30 \
|
|
145
|
+
-H 'Accept: application/json' \
|
|
146
|
+
-H 'Cookie: <Cookie>' \
|
|
147
|
+
-o /tmp/swagger-raw.json \
|
|
148
|
+
'http://10.2.7.60:30194/ikmp-knowledge/v2/api-docs'
|
|
149
|
+
|
|
150
|
+
# 第二步:交给脚本解析
|
|
151
|
+
python3 .../extract_swagger.py --file /tmp/swagger-raw.json --filter tag --out /tmp/parsed.json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### 1.3 鉴权(如需 Cookie)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# 取 Cookie(注意 -D - 把响应头打到 stdout)
|
|
158
|
+
curl -sS -D - -X GET 'http://10.2.7.60:30194/ikmp-knowledge/swagger-resources/configuration/ui' \
|
|
159
|
+
-H 'Authorization: Basic <base64(username:password)>' \
|
|
160
|
+
-H 'Accept: application/json' \
|
|
161
|
+
-o /dev/null \
|
|
162
|
+
| grep -i '^set-cookie:' | head -1 | sed 's/.*: //; s/;.*//'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
将得到的 `JSESSIONID=xxx` 传给 `--cookie` 参数。
|
|
166
|
+
|
|
167
|
+
#### 1.4 单独查询某个 VO/DTO 完整定义(兜底)
|
|
168
|
+
|
|
169
|
+
如果脚本已经解析过、但 AI 后续发现某个 `definitionsDetail` 之外的 VO 也需要展开,用:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
python3 .../extract_swagger.py --file /tmp/swagger-raw.json --definition KnowledgeOperLogVO
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
找不到时会自动给出模糊匹配建议(最多 10 个)。
|
|
176
|
+
|
|
177
|
+
#### 1.5 输出结构(与服务端 `parse_swagger_json` 一致)
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"info": { "title": "...", "version": "...", "description": "..." },
|
|
182
|
+
"basePath": "/svc",
|
|
183
|
+
"tags": [{ "name": "tag", "description": "..." }],
|
|
184
|
+
"apis": [
|
|
185
|
+
{
|
|
186
|
+
"url": "/tag/pageList",
|
|
187
|
+
"method": "POST",
|
|
188
|
+
"tag": "tag",
|
|
189
|
+
"summary": "标签分页查询",
|
|
190
|
+
"operationId": "tagPageList",
|
|
191
|
+
"parameters": [
|
|
192
|
+
{ "name": "body", "in": "body", "required": true,
|
|
193
|
+
"dtoRef": "TagQueryDTO",
|
|
194
|
+
"dtoProperties": [ /* 递归展开,required 字段已标注 */ ] }
|
|
195
|
+
],
|
|
196
|
+
"response": { "voName": "ResponseTagPage", "properties": [ /* 递归展开 */ ] }
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
"definitions": [ /* 摘要:所有 VO/DTO 的 name + type + description */ ],
|
|
200
|
+
"definitionsDetail": [ /* 详情:被 apis 引用的 VO/DTO 及传递闭包,深度5 */ ],
|
|
201
|
+
"_filter": { "keywords": ["tag"], "total": 50, "matched": 12 } // 仅 --filter 生效时出现
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 二、服务端兜底路径(脚本不可用时)
|
|
88
206
|
|
|
89
207
|
**步骤 1:调用服务端 api_tool**
|
|
90
208
|
|
|
@@ -111,41 +229,38 @@ api_tool(action="fetch", url="http://10.2.7.60:30194/ikmp-knowledge/swagger-ui/i
|
|
|
111
229
|
}
|
|
112
230
|
```
|
|
113
231
|
|
|
114
|
-
**步骤 3
|
|
115
|
-
|
|
116
|
-
根据返回数据中的 `authInfo` 和 `swaggerEndpoints`,AI 自行构造 curl 命令并通过 `run_command` 在本地执行:
|
|
117
|
-
|
|
118
|
-
3a. **鉴权(如 authInfo.hasPassword 为 true)**:
|
|
232
|
+
**步骤 3:本地 curl 写入临时文件(禁止管道截断!)**
|
|
119
233
|
|
|
120
234
|
```bash
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
-H '
|
|
124
|
-
|
|
235
|
+
# 鉴权(如需要)
|
|
236
|
+
curl -sS -D - -X GET '<authUrl>' \
|
|
237
|
+
-H 'Authorization: Basic <base64(user:pass)>' \
|
|
238
|
+
-o /dev/null | grep -i '^set-cookie:' | head -1 | sed 's/.*: //; s/;.*//'
|
|
125
239
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
3b. **获取 API 文档 JSON**:直接使用 `swaggerEndpoints.apiDocsUrl`:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
curl -s -X GET 'http://10.2.7.60:30194/ikmp-knowledge/v2/api-docs' \
|
|
240
|
+
# 下载 API 文档(务必 -o 写文件,不要 | head / | python)
|
|
241
|
+
curl -sS --connect-timeout 10 --max-time 30 \
|
|
132
242
|
-H 'Accept: application/json' \
|
|
133
|
-
-H 'Cookie:
|
|
243
|
+
-H 'Cookie: <步骤上一步的 Cookie>' \
|
|
244
|
+
-o /tmp/swagger-raw.json \
|
|
245
|
+
'<apiDocsUrl>'
|
|
134
246
|
```
|
|
135
247
|
|
|
136
|
-
如果 authInfo.hasPassword 为 false,省略 Cookie 头。
|
|
137
|
-
|
|
138
248
|
**步骤 4:调用服务端解析**
|
|
139
249
|
|
|
140
250
|
```
|
|
141
|
-
api_tool(
|
|
251
|
+
api_tool(
|
|
252
|
+
action="parse_swagger_json",
|
|
253
|
+
swaggerJson=<读取 /tmp/swagger-raw.json 的完整内容>,
|
|
254
|
+
swaggerSourceUrl="http://10.2.7.60:30194/ikmp-knowledge/swagger-ui/index.html",
|
|
255
|
+
apiFilter=["tag"] // 可选,强烈推荐:按关键词过滤 + 自动裁剪 definitionsDetail
|
|
256
|
+
)
|
|
142
257
|
```
|
|
143
258
|
|
|
144
259
|
**步骤 5:返回解析结果**
|
|
145
260
|
|
|
146
|
-
|
|
261
|
+
将返回结果直接呈现给用户。
|
|
147
262
|
|
|
148
|
-
###
|
|
263
|
+
### 三、联调测试(action=test)
|
|
149
264
|
|
|
150
265
|
**步骤 1:调用服务端 api_tool**
|
|
151
266
|
|
|
@@ -196,13 +311,24 @@ curl -s -X POST 'http://10.2.7.60:30194/ikmp-knowledge/api/xxx' \
|
|
|
196
311
|
|
|
197
312
|
将 curl 的响应内容返回给用户。如果是 JSON 格式,尽量格式化输出。
|
|
198
313
|
|
|
314
|
+
## ⚠️ 避坑指南(来自历史反馈)
|
|
315
|
+
|
|
316
|
+
1. **禁止用 `curl | head/tail/jq/python` 对完整 Swagger JSON 做管道切片** —— curl 管道经常因下游 SIGPIPE 截断,导致 JSON 不闭合 / Python 解析失败 / 临时写花括号平衡脚本,是历史踩坑率最高的反模式。**必须用 `-o /tmp/xxx.json` 写文件**,再用脚本或 `parse_swagger_json` 处理。
|
|
317
|
+
2. **不要在本地手写 Python 切 Swagger** —— `extract_swagger.py` 已经做了和服务端 `parseApiDocs` 完全对齐的递归展开、传递闭包、required 标注、过滤裁剪。本地手写一个 mini 版只会丢字段或踩 `$ref`/`originalRef` 双字段陷阱。
|
|
318
|
+
3. **过滤优先用 `--filter`(脚本)或 `apiFilter`(服务端)** —— 不要先拿全量再让 AI 自己挑接口看,过滤后 `definitionsDetail` 会自动按传递闭包裁剪,输出量减少 70%+。
|
|
319
|
+
4. **单 VO 查询用 `--definition`(脚本)或 `get_definition`(服务端)** —— 不要回头再拉一次全量。
|
|
320
|
+
|
|
199
321
|
## 快捷方式:AI 直接识别 10 段地址
|
|
200
322
|
|
|
201
|
-
如果 AI 能识别出用户请求的 URL 中包含 `10.x.x.x` 主机地址,可以**直接跳过首次 api_tool
|
|
323
|
+
如果 AI 能识别出用户请求的 URL 中包含 `10.x.x.x` 主机地址,可以**直接跳过首次 api_tool 调用**,在本地执行脚本,减少一次无用的服务端请求:
|
|
202
324
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
325
|
+
```bash
|
|
326
|
+
python3 ~/.qwen/extensions/szcd-component-helper/skills/local-api-tool/scripts/extract_swagger.py \
|
|
327
|
+
--url 'http://10.x.x.x/<service>/v2/api-docs' \
|
|
328
|
+
[--cookie '<JSESSIONID=...>'] \
|
|
329
|
+
[--filter '<关键词>'] \
|
|
330
|
+
--out /tmp/parsed.json
|
|
331
|
+
```
|
|
206
332
|
|
|
207
333
|
### URL 推导规则
|
|
208
334
|
|
|
@@ -214,33 +340,11 @@ curl -s -X POST 'http://10.2.7.60:30194/ikmp-knowledge/api/xxx' \
|
|
|
214
340
|
| authUrl | `basePath + /swagger-resources/configuration/ui` | `http://10.2.7.60:30194/ikmp-knowledge/swagger-resources/configuration/ui` |
|
|
215
341
|
| apiDocsUrl | `basePath + /v2/api-docs` | `http://10.2.7.60:30194/ikmp-knowledge/v2/api-docs` |
|
|
216
342
|
|
|
217
|
-
### curl 执行模板
|
|
218
|
-
|
|
219
|
-
**鉴权(带密码时):**
|
|
220
|
-
```bash
|
|
221
|
-
curl -s -D - -X GET '<authUrl>' \
|
|
222
|
-
-H 'Authorization: Basic <base64(username:password)>' \
|
|
223
|
-
-H 'Accept: application/json'
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**获取文档(带 Cookie 时):**
|
|
227
|
-
```bash
|
|
228
|
-
curl -s -X GET '<apiDocsUrl>' \
|
|
229
|
-
-H 'Accept: application/json' \
|
|
230
|
-
-H 'Cookie: <鉴权获取的Cookie>'
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**获取文档(无密码时):**
|
|
234
|
-
```bash
|
|
235
|
-
curl -s -X GET '<apiDocsUrl>' \
|
|
236
|
-
-H 'Accept: application/json'
|
|
237
|
-
```
|
|
238
|
-
|
|
239
343
|
## 注意事项
|
|
240
344
|
|
|
241
|
-
1. **Cookie 传递**:鉴权步骤获取的 Cookie
|
|
345
|
+
1. **Cookie 传递**:鉴权步骤获取的 Cookie 通过 `--cookie` 传给脚本,或通过 `-H 'Cookie: xxx'` 传给 curl
|
|
242
346
|
2. **无密码场景**:如果用户未提供密码,跳过鉴权步骤,直接获取文档
|
|
243
|
-
3. **JSON
|
|
244
|
-
4.
|
|
245
|
-
5.
|
|
246
|
-
6. **自动执行**:AI 识别 `reason: "LOCAL_NETWORK_UNREACHABLE"` 后应**自动**通过 `run_command`
|
|
347
|
+
3. **JSON 格式**:脚本/curl 获取的 API 文档必须是有效的 Swagger JSON,否则 `extract_swagger.py` / `parse_swagger_json` 会报错(脚本会打印预览前 200 字符方便排查)
|
|
348
|
+
4. **超时处理**:脚本和 curl 都已内置 `--connect-timeout 10 --max-time 30`,可通过 `--timeout` 调整
|
|
349
|
+
5. **错误处理**:脚本退出码 1=参数错误 / 2=网络IO / 3=解析;如果某步骤失败,将错误信息返回给用户,不要继续后续步骤
|
|
350
|
+
6. **自动执行**:AI 识别 10.x.x.x 或 `reason: "LOCAL_NETWORK_UNREACHABLE"` 后应**自动**通过 `run_command` 执行脚本,无需让用户手动复制粘贴命令
|