@mingfuyun/star-allinone 0.1.9 → 0.3.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/README.md +21 -9
- package/examples/AGENTS.md +29 -0
- package/examples/projects/starlink-frontend.star-allinone.json +7 -0
- package/examples/projects/starscope-frontend.star-allinone.json +7 -0
- package/examples/skills/custom-device-service/SKILL.md +18 -0
- package/package.json +5 -3
- package/scripts/cli.js +44 -11
- package/scripts/gen/fixtures/run.js +104 -0
- package/scripts/gen/fixtures/widgets.openapi.json +194 -0
- package/scripts/gen/names.js +117 -0
- package/scripts/gen/page.js +483 -0
- package/scripts/gen/project.js +154 -0
- package/scripts/gen/service.js +264 -0
- package/scripts/gen/spec.js +220 -0
- package/scripts/gen/ts.js +121 -0
- package/skills/README.md +32 -3
- package/skills/shared/ai-review/SKILL.md +8 -1
- package/skills/shared/api-mock/SKILL.md +8 -1
- package/skills/shared/api-to-service/SKILL.md +20 -8
- package/skills/shared/commit-current-changes/SKILL.md +8 -1
- package/skills/shared/design-to-page/SKILL.md +9 -1
- package/skills/shared/i-have-adhd/SKILL.md +142 -0
- package/skills/shared/impact-analysis/SKILL.md +8 -1
- package/skills/shared/page-recipe/SKILL.md +72 -0
- package/skills/shared/page-recipe/references/list-page-template.md +44 -0
- package/skills/shared/project-conventions/SKILL.md +8 -1
- package/skills/shared/project-conventions/references/architecture.md +3 -0
- package/skills/shared/publish-test/SKILL.md +7 -1
- package/skills/shared/review-and-submit/SKILL.md +7 -1
- package/skills/shared/verify/SKILL.md +9 -2
package/README.md
CHANGED
|
@@ -17,9 +17,10 @@ pnpm add @mingfuyun/star-allinone
|
|
|
17
17
|
公共 skill 由本仓库维护,随 `@mingfuyun/star-allinone` 发布。业务项目用 CLI 同步到自己的 `.agents/skills/shared-*` 并提交。Codex / Cursor 直接从业务仓库发现 skill,不写入 `~/.codex/skills`,团队成员也不需要本仓库源码。
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
|
+
AGENTS.md ← 常驻约定,不要加 .cursor/rules
|
|
20
21
|
.agents/skills/
|
|
21
|
-
├── shared-*
|
|
22
|
-
└── custom-*
|
|
22
|
+
├── shared-* ← npm 包同步,不能在业务项目直接改
|
|
23
|
+
└── custom-* ← 业务项目自己维护
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
完整约定见 [skills/README.md](skills/README.md)。
|
|
@@ -32,9 +33,13 @@ pnpm add @mingfuyun/star-allinone
|
|
|
32
33
|
pnpm exec star-allinone skills list
|
|
33
34
|
pnpm exec star-allinone skills sync --dry-run
|
|
34
35
|
pnpm exec star-allinone skills sync
|
|
36
|
+
pnpm exec star-allinone gen service --operation "GET /api/v1/devices/"
|
|
37
|
+
pnpm exec star-allinone gen page --feature Widgets --route /widgets --title 组件管理 --list-operation "GET /api/v1/widgets/"
|
|
35
38
|
```
|
|
36
39
|
|
|
37
|
-
`
|
|
40
|
+
`gen service` 按 OpenAPI/Swagger 写一条 `MFRequest` 方法和对应类型。`gen page` 生成标准列表页骨架,并可先生成相关 Service。
|
|
41
|
+
|
|
42
|
+
`list` 查看包内公共 skill。`sync` 把它们写入当前项目的 `.agents/skills/shared-*`,会整体替换过期的 `shared-*`,但不会改 `custom-*`。仓库根没有 `AGENTS.md` 时,会从 `examples/AGENTS.md` 补一份,已有文件不覆盖。
|
|
38
43
|
|
|
39
44
|
### 维护者:改公共 skill
|
|
40
45
|
|
|
@@ -72,25 +77,32 @@ StarLink 示例见 `examples/projects/`。
|
|
|
72
77
|
pnpm exec star-allinone skills sync
|
|
73
78
|
```
|
|
74
79
|
|
|
75
|
-
4. 把真正的业务特例放到 `.agents/skills/custom
|
|
80
|
+
4. 把真正的业务特例放到 `.agents/skills/custom-*`。审查、提交、发布、单接口 Mock/Service、标准列表生成不要做成 `custom-*`。全量契约重建(若需要)必须在 custom 里写明「只用于全量」。
|
|
81
|
+
|
|
82
|
+
5. 仓库根放 `AGENTS.md`(可从包内 `examples/AGENTS.md` 复制)。`skills sync` 只在该文件缺失时补一份,不会覆盖已有内容。常驻约定只写这一份,不要加 `.cursor/rules`。
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
6. 提交配置和 skill:
|
|
78
85
|
|
|
79
86
|
```bash
|
|
80
|
-
git add .star-allinone.json .agents/skills
|
|
87
|
+
git add .star-allinone.json AGENTS.md .agents/skills
|
|
81
88
|
git commit -m "chore(skill): 接入公共 skill"
|
|
82
89
|
```
|
|
83
90
|
|
|
84
91
|
### 业务项目:日常调用
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
只使用下面这些**用户入口**。不要直接 `$shared-verify`、`$shared-api-mock`、`$shared-commit-current-changes` 等内部 skill。
|
|
87
94
|
|
|
88
95
|
```text
|
|
89
96
|
使用 $shared-api-to-service,为 GET /api/v1/devices/ 生成类型、Service 和 JSON Mock。
|
|
90
97
|
```
|
|
91
98
|
|
|
92
99
|
```text
|
|
93
|
-
使用 $shared-
|
|
100
|
+
使用 $shared-page-recipe,生成 /widgets 标准列表页。
|
|
101
|
+
列表接口 GET /api/v1/widgets/。
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
使用 $shared-design-to-page,根据我上传的设计稿生成非标准页面。
|
|
94
106
|
缺少的 Service、类型和 Mock 一并生成。
|
|
95
107
|
```
|
|
96
108
|
|
|
@@ -103,7 +115,7 @@ git commit -m "chore(skill): 接入公共 skill"
|
|
|
103
115
|
使用 $shared-publish-test,把当前分支发布到测试环境。
|
|
104
116
|
```
|
|
105
117
|
|
|
106
|
-
公共流程用 `shared-*`,项目专属规则用 `custom
|
|
118
|
+
公共流程用 `shared-*`,项目专属规则用 `custom-*`。`custom-*` 不能再实现一遍 Service/页面生成;星链全量 swagger 重建除外。业务定制 skill 需要复用公共流程时,在 `custom-*` 的 `SKILL.md` 里写明先读取对应的 `.agents/skills/shared-*/SKILL.md`。
|
|
107
119
|
|
|
108
120
|
### 业务项目:更新公共 skill
|
|
109
121
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Agent instructions
|
|
2
|
+
|
|
3
|
+
This is a Umi Max + Ant Design 5 PC admin app. Shared skills live in `.agents/skills/shared-*` and are synced from `@mingfuyun/star-allinone`. Do not edit `shared-*`. Put project-only rules in `.agents/skills/custom-*` or `.agents/project/`.
|
|
4
|
+
|
|
5
|
+
Always-on instructions are this file. Do not add `.cursor/rules`.
|
|
6
|
+
|
|
7
|
+
## Default workflow
|
|
8
|
+
|
|
9
|
+
User-facing shared entries only:
|
|
10
|
+
|
|
11
|
+
- One endpoint: `pnpm exec star-allinone gen service --operation "METHOD /path/"` or `$shared-api-to-service`.
|
|
12
|
+
- Standard list: `$shared-page-recipe` / `pnpm exec star-allinone gen page`.
|
|
13
|
+
- Non-standard UI: `$shared-design-to-page`.
|
|
14
|
+
- Review and commit: `$shared-review-and-submit`. Wait for confirmation.
|
|
15
|
+
- Test publish: `$shared-publish-test` after explicit pipeline confirmation.
|
|
16
|
+
|
|
17
|
+
Do not invoke `shared-api-mock`, `shared-verify`, `shared-impact-analysis`, `shared-ai-review`, `shared-commit-current-changes`, or `shared-project-conventions` directly.
|
|
18
|
+
|
|
19
|
+
## Hard rules
|
|
20
|
+
|
|
21
|
+
- For non-trivial changes, read `.agents/skills/shared-project-conventions` (do not `$` invoke it).
|
|
22
|
+
- Pages use `import Service from '@/services'`. Do not call axios or `src/services/request.tsx` from pages.
|
|
23
|
+
- Lists: `MFProTable`. Forms: `ProForm`. Overlays: `CustomDrawer`, `BaseModal`, `HocModal` from `@mingfuyun/star-allinone`.
|
|
24
|
+
- Keep columns and actions in `const.tsx`. Empty cells render as `-`.
|
|
25
|
+
- Keep DTOs in the domain `typings.d.ts` namespace. Preserve backend snake_case.
|
|
26
|
+
- Do not edit `src/services/request.tsx` for a single endpoint.
|
|
27
|
+
- Every `await` uses try/catch. Do not use `Modal.confirm`.
|
|
28
|
+
- After TypeScript changes run `pnpm typecheck`.
|
|
29
|
+
- If `.agents/project/conventions.md` or `.agents/project/review-rules.md` exists, apply them as additions only.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: custom-device-service
|
|
3
|
+
description: Generate StarScope device service code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
本任务需要结合公共 skill `shared-api-to-service` 执行。
|
|
7
|
+
|
|
8
|
+
先读取并遵循:
|
|
9
|
+
|
|
10
|
+
`.agents/skills/shared-api-to-service/SKILL.md`
|
|
11
|
+
|
|
12
|
+
再读取当前项目的:
|
|
13
|
+
|
|
14
|
+
- OpenAPI 文件
|
|
15
|
+
- Service 约定
|
|
16
|
+
- 项目类型和权限规则
|
|
17
|
+
|
|
18
|
+
只生成符合当前项目结构的设备相关 service。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mingfuyun/star-allinone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "星系运维前端能力包",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"dist",
|
|
31
31
|
"README.md",
|
|
32
32
|
"scripts/cli.js",
|
|
33
|
-
"
|
|
33
|
+
"scripts/gen",
|
|
34
|
+
"skills",
|
|
35
|
+
"examples"
|
|
34
36
|
],
|
|
35
37
|
"scripts": {
|
|
36
38
|
"build": "webpack --env format=umd --mode=none && webpack --env format=esm --mode=none",
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"skills:sync": "node scripts/cli.js skills sync",
|
|
40
42
|
"skills:list": "node scripts/cli.js skills list",
|
|
41
43
|
"prepublishOnly": "npm run build",
|
|
42
|
-
"test": "
|
|
44
|
+
"test": "node scripts/gen/fixtures/run.js",
|
|
43
45
|
"dev": "webpack --mode=none --watch",
|
|
44
46
|
"clean": "rm -rf dist"
|
|
45
47
|
},
|
package/scripts/cli.js
CHANGED
|
@@ -17,11 +17,17 @@ function usage() {
|
|
|
17
17
|
console.log(`Usage:
|
|
18
18
|
star-allinone skills list
|
|
19
19
|
star-allinone skills sync [--target <dir>] [--dry-run]
|
|
20
|
+
star-allinone gen service --operation "GET /api/v1/devices/" [--spec <file>] [--domain Devices] [--mock-file] [--dry-run]
|
|
21
|
+
star-allinone gen page --feature Widgets --route /widgets --title 组件管理 --list-operation "GET /api/v1/widgets/" [--create-operation ...] [--update-operation ...] [--delete-operation ...] [--dry-run]
|
|
20
22
|
|
|
21
|
-
Run
|
|
23
|
+
Run from a business project containing .star-allinone.json.
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
It never writes ~/.codex/skills and never modifies custom-* skills
|
|
25
|
+
skills sync writes package skills/shared/* to <project>/.agents/skills/shared-*.
|
|
26
|
+
It never writes ~/.codex/skills and never modifies custom-* skills.
|
|
27
|
+
If <project>/AGENTS.md is missing, it copies examples/AGENTS.md once.
|
|
28
|
+
|
|
29
|
+
gen service writes one MFRequest method and types from OpenAPI/Swagger.
|
|
30
|
+
gen page scaffolds a list + drawer + confirm page and can generate services first.`);
|
|
25
31
|
process.exitCode = 1;
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -50,8 +56,12 @@ function parseArgs(args) {
|
|
|
50
56
|
for (let index = 0; index < args.length; index += 1) {
|
|
51
57
|
const arg = args[index];
|
|
52
58
|
if (arg === '--dry-run') options.dryRun = true;
|
|
53
|
-
else if (arg === '--
|
|
54
|
-
else
|
|
59
|
+
else if (arg === '--mock-file') options.mockFile = true;
|
|
60
|
+
else if (arg === '--force') options.force = true;
|
|
61
|
+
else if (arg.startsWith('--')) {
|
|
62
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
63
|
+
options[key] = args[++index];
|
|
64
|
+
} else positional.push(arg);
|
|
55
65
|
}
|
|
56
66
|
return { options, positional };
|
|
57
67
|
}
|
|
@@ -158,17 +168,40 @@ function syncProject(options) {
|
|
|
158
168
|
copySkill(path.join(skillsRoot, skillName), destination, skillName, prefix, variables, options);
|
|
159
169
|
}
|
|
160
170
|
pruneStaleShared(destination, prefix, syncedNames, options);
|
|
171
|
+
seedAgentsMarkdown(target, options);
|
|
161
172
|
console.log(`${options.dryRun ? 'Would sync' : 'Synced'} shared skills -> ${destination}`);
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
function seedAgentsMarkdown(target, options) {
|
|
176
|
+
const dest = path.join(target, 'AGENTS.md');
|
|
177
|
+
const source = path.join(root, 'examples', 'AGENTS.md');
|
|
178
|
+
if (fs.existsSync(dest) || !fs.existsSync(source)) return;
|
|
179
|
+
console.log(`${options.dryRun ? '[dry-run] ' : ''}AGENTS.md`);
|
|
180
|
+
if (!options.dryRun) fs.copyFileSync(source, dest);
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
function main() {
|
|
165
184
|
const [group, command, ...rest] = process.argv.slice(2);
|
|
166
|
-
if (group
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
if (group === 'skills') {
|
|
186
|
+
if (command === 'list') return listSkills();
|
|
187
|
+
if (command !== 'sync') return usage();
|
|
188
|
+
const { options, positional } = parseArgs(rest);
|
|
189
|
+
if (positional.length) return usage();
|
|
190
|
+
return syncProject(options);
|
|
191
|
+
}
|
|
192
|
+
if (group === 'gen') {
|
|
193
|
+
const { options, positional } = parseArgs(rest);
|
|
194
|
+
if (positional.length) return usage();
|
|
195
|
+
if (command === 'service') {
|
|
196
|
+
if (!options.operation) return usage();
|
|
197
|
+
return require('./gen/service').generateService(process.cwd(), options);
|
|
198
|
+
}
|
|
199
|
+
if (command === 'page') {
|
|
200
|
+
if (!options.feature) return usage();
|
|
201
|
+
return require('./gen/page').generatePage(process.cwd(), options);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return usage();
|
|
172
205
|
}
|
|
173
206
|
|
|
174
207
|
try {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { generateService } = require('../service');
|
|
5
|
+
const { generatePage } = require('../page');
|
|
6
|
+
|
|
7
|
+
function assert(condition, message) {
|
|
8
|
+
if (!condition) throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeProject() {
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'star-allinone-gen-'));
|
|
13
|
+
fs.writeFileSync(
|
|
14
|
+
path.join(root, '.star-allinone.json'),
|
|
15
|
+
JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
project: 'fixture',
|
|
18
|
+
serviceDir: 'src/services',
|
|
19
|
+
openapi: 'openapi.json',
|
|
20
|
+
},
|
|
21
|
+
null,
|
|
22
|
+
2,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
fs.copyFileSync(
|
|
26
|
+
path.join(__dirname, 'widgets.openapi.json'),
|
|
27
|
+
path.join(root, 'openapi.json'),
|
|
28
|
+
);
|
|
29
|
+
fs.mkdirSync(path.join(root, 'src/services'), { recursive: true });
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(root, 'src/services/request.tsx'),
|
|
32
|
+
'const MFRequest = { get() {}, post() {}, patch() {}, delete() {} };\nexport default MFRequest;\n',
|
|
33
|
+
);
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
path.join(root, 'src/services/index.ts'),
|
|
36
|
+
'export default {};\n',
|
|
37
|
+
);
|
|
38
|
+
fs.mkdirSync(path.join(root, 'config'), { recursive: true });
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(root, 'config/route.ts'),
|
|
41
|
+
`export default [
|
|
42
|
+
{ path: '/', redirect: '/home' },
|
|
43
|
+
{ path: '/*', component: '@/pages/404', name: '404' },
|
|
44
|
+
];
|
|
45
|
+
`,
|
|
46
|
+
);
|
|
47
|
+
fs.mkdirSync(path.join(root, 'src/components/CommonTable'), { recursive: true });
|
|
48
|
+
fs.mkdirSync(path.join(root, 'src/pages'), { recursive: true });
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function main() {
|
|
53
|
+
const root = makeProject();
|
|
54
|
+
const first = generateService(root, {
|
|
55
|
+
operation: 'GET /api/v1/widgets/',
|
|
56
|
+
mockFile: true,
|
|
57
|
+
});
|
|
58
|
+
assert(first.fn === 'widgetsList', `fn ${first.fn}`);
|
|
59
|
+
assert(first.domain === 'Widgets', `domain ${first.domain}`);
|
|
60
|
+
const service = fs.readFileSync(first.servicePath, 'utf8');
|
|
61
|
+
assert(service.includes("MFRequest.get<WIDGETS_API.WidgetList>('/api/v1/widgets/', params)"), service);
|
|
62
|
+
const typings = fs.readFileSync(first.typingsPath, 'utf8');
|
|
63
|
+
assert(typings.includes('type Widget ='), typings);
|
|
64
|
+
assert(typings.includes('type WidgetList ='), typings);
|
|
65
|
+
assert(typings.includes('results?: Widget[]'), typings);
|
|
66
|
+
assert(typings.includes('status?: string | null'), typings);
|
|
67
|
+
assert(typings.includes('type WidgetsListQuery'), typings);
|
|
68
|
+
const mockFile = path.join(root, 'mock/api/v1/widgets/get.json');
|
|
69
|
+
assert(fs.existsSync(mockFile), 'missing mock file');
|
|
70
|
+
const index = fs.readFileSync(path.join(root, 'src/services/index.ts'), 'utf8');
|
|
71
|
+
assert(index.includes("from './Widgets/widgets'"), index);
|
|
72
|
+
|
|
73
|
+
const reused = generateService(root, { operation: 'GET /api/v1/widgets/' });
|
|
74
|
+
assert(reused.status === 'reuse', 'expected reuse on second run');
|
|
75
|
+
|
|
76
|
+
generateService(root, { operation: 'PATCH /api/v1/widgets/{id}/' });
|
|
77
|
+
const patched = fs.readFileSync(first.servicePath, 'utf8');
|
|
78
|
+
assert(patched.includes('widgetsPartialUpdate'), patched);
|
|
79
|
+
assert(patched.includes('MFRequest.patch<WIDGETS_API.Widget>'), patched);
|
|
80
|
+
|
|
81
|
+
generatePage(root, {
|
|
82
|
+
feature: 'Widgets',
|
|
83
|
+
route: '/widgets',
|
|
84
|
+
title: '组件管理',
|
|
85
|
+
listOperation: 'GET /api/v1/widgets/',
|
|
86
|
+
createOperation: 'POST /api/v1/widgets/',
|
|
87
|
+
updateOperation: 'PATCH /api/v1/widgets/{id}/',
|
|
88
|
+
deleteOperation: 'DELETE /api/v1/widgets/{id}/',
|
|
89
|
+
});
|
|
90
|
+
const page = fs.readFileSync(path.join(root, 'src/pages/Widgets/index.tsx'), 'utf8');
|
|
91
|
+
assert(page.includes('Service.widgetsList'), page);
|
|
92
|
+
assert(page.includes("rowKey=\"id\""), page);
|
|
93
|
+
const route = fs.readFileSync(path.join(root, 'config/route.ts'), 'utf8');
|
|
94
|
+
assert(route.includes("path: '/widgets'"), route);
|
|
95
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
96
|
+
console.log('codegen fixtures passed');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
main();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(error.stack || error.message);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "codegen fixture",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/api/v1/widgets/": {
|
|
9
|
+
"get": {
|
|
10
|
+
"operationId": "widgets_list",
|
|
11
|
+
"tags": ["Widgets"],
|
|
12
|
+
"summary": "获取组件列表",
|
|
13
|
+
"parameters": [
|
|
14
|
+
{
|
|
15
|
+
"in": "query",
|
|
16
|
+
"name": "page",
|
|
17
|
+
"required": true,
|
|
18
|
+
"schema": { "type": "integer" }
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"in": "query",
|
|
22
|
+
"name": "page_size",
|
|
23
|
+
"required": true,
|
|
24
|
+
"schema": { "type": "integer" }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"in": "query",
|
|
28
|
+
"name": "keyword",
|
|
29
|
+
"schema": { "type": "string" }
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"responses": {
|
|
33
|
+
"200": {
|
|
34
|
+
"description": "ok",
|
|
35
|
+
"content": {
|
|
36
|
+
"application/json": {
|
|
37
|
+
"schema": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"code": { "type": "integer" },
|
|
41
|
+
"message": { "type": "string" },
|
|
42
|
+
"data": {
|
|
43
|
+
"$ref": "#/components/schemas/WidgetList"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"post": {
|
|
53
|
+
"operationId": "widgets_create",
|
|
54
|
+
"tags": ["Widgets"],
|
|
55
|
+
"summary": "创建组件",
|
|
56
|
+
"requestBody": {
|
|
57
|
+
"content": {
|
|
58
|
+
"application/json": {
|
|
59
|
+
"schema": {
|
|
60
|
+
"$ref": "#/components/schemas/WidgetBody"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"responses": {
|
|
66
|
+
"201": {
|
|
67
|
+
"description": "created",
|
|
68
|
+
"content": {
|
|
69
|
+
"application/json": {
|
|
70
|
+
"schema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"code": { "type": "integer" },
|
|
74
|
+
"message": { "type": "string" },
|
|
75
|
+
"data": {
|
|
76
|
+
"$ref": "#/components/schemas/Widget"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"/api/v1/widgets/{id}/": {
|
|
87
|
+
"patch": {
|
|
88
|
+
"operationId": "widgets_partial_update",
|
|
89
|
+
"tags": ["Widgets"],
|
|
90
|
+
"summary": "更新组件",
|
|
91
|
+
"parameters": [
|
|
92
|
+
{
|
|
93
|
+
"in": "path",
|
|
94
|
+
"name": "id",
|
|
95
|
+
"required": true,
|
|
96
|
+
"schema": { "type": "string" }
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"requestBody": {
|
|
100
|
+
"content": {
|
|
101
|
+
"application/json": {
|
|
102
|
+
"schema": {
|
|
103
|
+
"$ref": "#/components/schemas/WidgetBody"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"responses": {
|
|
109
|
+
"200": {
|
|
110
|
+
"description": "ok",
|
|
111
|
+
"content": {
|
|
112
|
+
"application/json": {
|
|
113
|
+
"schema": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": {
|
|
116
|
+
"code": { "type": "integer" },
|
|
117
|
+
"message": { "type": "string" },
|
|
118
|
+
"data": {
|
|
119
|
+
"$ref": "#/components/schemas/Widget"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"delete": {
|
|
129
|
+
"operationId": "widgets_destroy",
|
|
130
|
+
"tags": ["Widgets"],
|
|
131
|
+
"summary": "删除组件",
|
|
132
|
+
"parameters": [
|
|
133
|
+
{
|
|
134
|
+
"in": "path",
|
|
135
|
+
"name": "id",
|
|
136
|
+
"required": true,
|
|
137
|
+
"schema": { "type": "string" }
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"responses": {
|
|
141
|
+
"200": {
|
|
142
|
+
"description": "ok",
|
|
143
|
+
"content": {
|
|
144
|
+
"application/json": {
|
|
145
|
+
"schema": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"code": { "type": "integer" },
|
|
149
|
+
"message": { "type": "string" },
|
|
150
|
+
"data": { "type": "object" }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"components": {
|
|
161
|
+
"schemas": {
|
|
162
|
+
"Widget": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"required": ["id", "name"],
|
|
165
|
+
"properties": {
|
|
166
|
+
"id": { "type": "string" },
|
|
167
|
+
"name": { "type": "string" },
|
|
168
|
+
"status": { "type": "string", "nullable": true },
|
|
169
|
+
"count": { "type": "integer" }
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"WidgetBody": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"required": ["name"],
|
|
175
|
+
"properties": {
|
|
176
|
+
"name": { "type": "string" },
|
|
177
|
+
"status": { "type": "string" }
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"WidgetList": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"properties": {
|
|
183
|
+
"results": {
|
|
184
|
+
"type": "array",
|
|
185
|
+
"items": {
|
|
186
|
+
"$ref": "#/components/schemas/Widget"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"count": { "type": "integer" }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
function toPascal(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/[^\w]+/g, ' ')
|
|
4
|
+
.split(/[\s_-]+/)
|
|
5
|
+
.filter(Boolean)
|
|
6
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toCamel(value) {
|
|
11
|
+
const pascal = toPascal(value);
|
|
12
|
+
return pascal ? pascal.charAt(0).toLowerCase() + pascal.slice(1) : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toNamespace(domain) {
|
|
16
|
+
return `${String(domain)
|
|
17
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
18
|
+
.replace(/[^\w]+/g, '_')
|
|
19
|
+
.replace(/_+/g, '_')
|
|
20
|
+
.toUpperCase()}_API`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toServiceFile(domain) {
|
|
24
|
+
return toCamel(domain);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePath(apiPath) {
|
|
28
|
+
if (!apiPath) return '/';
|
|
29
|
+
const withSlash = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
30
|
+
return withSlash.replace(/\/{2,}/g, '/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pathVariants(apiPath) {
|
|
34
|
+
const normalized = normalizePath(apiPath);
|
|
35
|
+
const trimmed = normalized.replace(/\/+$/, '') || '/';
|
|
36
|
+
const withSlash = trimmed === '/' ? '/' : `${trimmed}/`;
|
|
37
|
+
return [...new Set([normalized, trimmed, withSlash])];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pathParamsFromPath(apiPath) {
|
|
41
|
+
return [...String(apiPath).matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pathToTemplate(apiPath) {
|
|
45
|
+
const replaced = normalizePath(apiPath).replace(/\{([^}]+)\}/g, '${$1}');
|
|
46
|
+
return replaced.includes('${') ? `\`${replaced}\`` : `'${replaced}'`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseOperationSelector(raw) {
|
|
50
|
+
const value = String(raw || '').trim();
|
|
51
|
+
if (!value) throw new Error('Missing --operation. Example: GET /api/v1/devices/');
|
|
52
|
+
const operationId = value.match(/^operationId\s*:\s*(.+)$/i);
|
|
53
|
+
if (operationId) return { operationId: operationId[1].trim() };
|
|
54
|
+
const spaced = value.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/\S+)/i);
|
|
55
|
+
if (spaced) {
|
|
56
|
+
return { method: spaced[1].toLowerCase(), path: spaced[2] };
|
|
57
|
+
}
|
|
58
|
+
if (value.startsWith('/')) return { path: value };
|
|
59
|
+
return { operationId: value };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function functionName(operation) {
|
|
63
|
+
if (operation.operationId) return toCamel(operation.operationId);
|
|
64
|
+
const params = pathParamsFromPath(operation.path);
|
|
65
|
+
const segments = normalizePath(operation.path)
|
|
66
|
+
.replace(/\{[^}]+\}/g, '')
|
|
67
|
+
.split('/')
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
const resource = toPascal(segments[segments.length - 1] || 'Resource');
|
|
70
|
+
const method = operation.method.toLowerCase();
|
|
71
|
+
if (method === 'get' && params.length) return `get${resource}Detail`;
|
|
72
|
+
if (method === 'get') return `list${resource}`;
|
|
73
|
+
if (method === 'post') return `create${resource}`;
|
|
74
|
+
if (method === 'patch' || method === 'put') return `update${resource}`;
|
|
75
|
+
if (method === 'delete') return `delete${resource}`;
|
|
76
|
+
return toCamel(`${method}_${resource}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requestHelper(operation) {
|
|
80
|
+
const method = operation.method.toLowerCase();
|
|
81
|
+
const contentType = operation.contentType || '';
|
|
82
|
+
if (method === 'get' && /export|download/.test(operation.path)) return 'export';
|
|
83
|
+
if (contentType.includes('multipart')) return 'import';
|
|
84
|
+
if (method === 'get') return 'get';
|
|
85
|
+
if (method === 'post') return 'post';
|
|
86
|
+
if (method === 'patch') return 'patch';
|
|
87
|
+
if (method === 'put') return 'put';
|
|
88
|
+
if (method === 'delete') return 'delete';
|
|
89
|
+
return method;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function domainFromPath(apiPath) {
|
|
93
|
+
const segments = normalizePath(apiPath).split('/').filter(Boolean);
|
|
94
|
+
const afterApi = segments[0] === 'api' ? segments.slice(2) : segments;
|
|
95
|
+
return toPascal(afterApi[0] || 'Common');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function typeName(prefix, suffix, useIPrefix) {
|
|
99
|
+
const name = `${toPascal(prefix)}${toPascal(suffix)}`;
|
|
100
|
+
return useIPrefix ? `I${name}` : name;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
toPascal,
|
|
105
|
+
toCamel,
|
|
106
|
+
toNamespace,
|
|
107
|
+
toServiceFile,
|
|
108
|
+
normalizePath,
|
|
109
|
+
pathVariants,
|
|
110
|
+
pathParamsFromPath,
|
|
111
|
+
pathToTemplate,
|
|
112
|
+
parseOperationSelector,
|
|
113
|
+
functionName,
|
|
114
|
+
requestHelper,
|
|
115
|
+
domainFromPath,
|
|
116
|
+
typeName,
|
|
117
|
+
};
|