auto-gen-crud 0.1.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/API.md +130 -0
- package/README.md +234 -0
- package/dist/adapters/ant-design-vue.d.ts +8 -0
- package/dist/adapters/element-plus.d.ts +8 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/naive.d.ts +8 -0
- package/dist/adapters/types.d.ts +162 -0
- package/dist/components/AutoCrud.vue.d.ts +398 -0
- package/dist/components/CopyText.vue.d.ts +6 -0
- package/dist/components/CrudModal.vue.d.ts +47 -0
- package/dist/components/DecodeText.vue.d.ts +8 -0
- package/dist/components/DynamicForm.vue.d.ts +63 -0
- package/dist/components/DynamicFormItem.vue.d.ts +20 -0
- package/dist/components/DynamicTable.vue.d.ts +30 -0
- package/dist/context.d.ts +36 -0
- package/dist/core/date-format-utils.d.ts +8 -0
- package/dist/core/form-runtime-cache.d.ts +27 -0
- package/dist/core/form-utils.d.ts +51 -0
- package/dist/core/object-utils.d.ts +43 -0
- package/dist/core/table-utils.d.ts +19 -0
- package/dist/core/types.d.ts +443 -0
- package/dist/generator/fields.d.ts +27 -0
- package/dist/generator/index.d.ts +14 -0
- package/dist/generator/index.js +740 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/openapi.d.ts +10 -0
- package/dist/generator/operations.d.ts +8 -0
- package/dist/generator/schema.d.ts +32 -0
- package/dist/generator/string-utils.d.ts +11 -0
- package/dist/generator/templates.d.ts +6 -0
- package/dist/generator/types.d.ts +185 -0
- package/dist/generator/writer.d.ts +8 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useModalForm.d.ts +11 -0
- package/dist/hooks/useSearchForm.d.ts +10 -0
- package/dist/hooks/useTable.d.ts +11 -0
- package/dist/index-Dc-solHn.js +299 -0
- package/dist/index-Dc-solHn.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +985 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/style.css +1 -0
- package/dist/useTable-BwDSqD1q.js +214 -0
- package/dist/useTable-BwDSqD1q.js.map +1 -0
- package/dist/utils/clipboard.d.ts +16 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +532 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/message.d.ts +9 -0
- package/dist/utils/request.d.ts +8 -0
- package/dist/utils/sm4-core.d.ts +12 -0
- package/dist/utils/sm4.d.ts +29 -0
- package/docs/AI_AGENT_GUIDE.md +210 -0
- package/mcp/auto-crud-mcp.mjs +502 -0
- package/mcp/auto-gen-crud-mcp.mjs +586 -0
- package/package.json +93 -0
- package/skills/auto-gen-crud-agent/SKILL.md +114 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# auto-gen-crud AI Agent 使用指南
|
|
2
|
+
|
|
3
|
+
这份文档面向你的 VSCode AI agent 插件。目标不是让 agent 生成一个“看起来像 CRUD 的页面”,而是让它按 `auto-gen-crud` 的工程边界生成可维护、可二开的初始化页面。
|
|
4
|
+
|
|
5
|
+
## 推荐工作流
|
|
6
|
+
|
|
7
|
+
1. 先确认当前项目是否已安装并初始化 `auto-gen-crud`。
|
|
8
|
+
2. 优先读取项目已有页面、公共请求工具、路由规范和权限写法。
|
|
9
|
+
3. 如果有 OpenAPI / Apifox 文档,优先调用 `auto-gen-crud/generator` 生成页面骨架。
|
|
10
|
+
4. 如果没有接口文档,再用 `AutoCrud`、`useSearchForm`、`useTable`、`useModalForm` 手写页面。
|
|
11
|
+
5. 生成后只做必要二开,不把业务逻辑塞进组件内部。
|
|
12
|
+
6. 最后执行类型检查、构建或项目已有 lint 命令。
|
|
13
|
+
|
|
14
|
+
## Agent 必须遵守的边界
|
|
15
|
+
|
|
16
|
+
- 不要改 `node_modules`、`dist` 或发布产物来解决业务问题。
|
|
17
|
+
- 不要把 CRUD 封成一个更大的黑盒页面组件;优先生成清晰的页面目录。
|
|
18
|
+
- 页面目录默认拆成 `index.vue`、`api/`、`config/` 三层。
|
|
19
|
+
- `index.vue` 只负责页面编排、请求调用和跨模块动作。
|
|
20
|
+
- `config/searchForm.ts` 只描述搜索字段。
|
|
21
|
+
- `config/table.ts` 只描述表格列和操作入口。
|
|
22
|
+
- `config/modalForm.ts` 只描述新增、编辑、查看弹窗字段。
|
|
23
|
+
- `api/types.ts` 放当前页面的类型,不要硬编码跨项目相对路径。
|
|
24
|
+
- `pageContext` 默认保持空对象,只在业务需要权限、联动、缓存时补字段。
|
|
25
|
+
- `getDataList` 推荐直接展开 `...searchForm.data`,不要逐个生成查询字段。
|
|
26
|
+
- 枚举默认生成 `TypeTextMap`;只有明确需要数组选项时才打开 `generateTypeOptions`。
|
|
27
|
+
|
|
28
|
+
## OpenAPI 生成器
|
|
29
|
+
|
|
30
|
+
包内导出:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { generateCrudPage } from "auto-gen-crud/generator";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
最小用法:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
await generateCrudPage({
|
|
40
|
+
openapi: "http://127.0.0.1:4523/export/openapi/2?version=3.1",
|
|
41
|
+
outDir: "src/views/DictTypePage",
|
|
42
|
+
overwrite: false,
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
常用配置:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await generateCrudPage({
|
|
50
|
+
openapi,
|
|
51
|
+
outDir: "src/views/DictTypePage",
|
|
52
|
+
tag: "开发中心/字典管理",
|
|
53
|
+
pageTitle: "字典管理",
|
|
54
|
+
entityName: "DictType",
|
|
55
|
+
operationMap: {
|
|
56
|
+
list: "POST /web/dict/pageQueryDictType.json",
|
|
57
|
+
create: "POST /web/dict/createDictType.json",
|
|
58
|
+
update: "POST /web/dict/updateDictType.json",
|
|
59
|
+
},
|
|
60
|
+
requestAdapter: {
|
|
61
|
+
importCode: 'import request from "@/utils/request";',
|
|
62
|
+
renderCall(ctx) {
|
|
63
|
+
return `return request.${ctx.method}<${ctx.responseType}>({ url: ${JSON.stringify(ctx.path)}, data: ${ctx.payloadName} });`;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
generateTypeOptions: false,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 生成器输出约定
|
|
71
|
+
|
|
72
|
+
生成目录通常包含:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
src/views/ExamplePage/
|
|
76
|
+
├── index.vue
|
|
77
|
+
├── api/
|
|
78
|
+
│ ├── index.ts
|
|
79
|
+
│ └── types.ts
|
|
80
|
+
└── config/
|
|
81
|
+
├── enums.ts
|
|
82
|
+
├── searchForm.ts
|
|
83
|
+
├── table.ts
|
|
84
|
+
└── modalForm.ts
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`index.vue` 的核心查询逻辑应保持这种形态:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
async function getDataList() {
|
|
91
|
+
const { page, pageSize } = table.pagination;
|
|
92
|
+
|
|
93
|
+
const result = await queryExampleList({
|
|
94
|
+
currentPage: page,
|
|
95
|
+
pageSize,
|
|
96
|
+
...searchForm.data,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
table.data = result.result.records;
|
|
100
|
+
table.pagination.total = result.result.total;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 手写页面时的最小结构
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
import { AutoCrud, useModalForm, useSearchForm, useTable } from "auto-gen-crud";
|
|
109
|
+
|
|
110
|
+
interface RowType {
|
|
111
|
+
id?: number | string;
|
|
112
|
+
name?: string;
|
|
113
|
+
status?: string;
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const statusTextMap = {
|
|
118
|
+
enabled: "启用",
|
|
119
|
+
disabled: "禁用",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const searchForm = useSearchForm({
|
|
123
|
+
columns: [
|
|
124
|
+
{ label: "名称", prop: "name", component: "input" },
|
|
125
|
+
{ label: "状态", prop: "status", component: "select", options: statusTextMap },
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const modalForm = useModalForm<RowType>({
|
|
130
|
+
columns: [
|
|
131
|
+
{ label: "名称", prop: "name", component: "input", required: true },
|
|
132
|
+
{ label: "状态", prop: "status", component: "select", options: statusTextMap },
|
|
133
|
+
],
|
|
134
|
+
async onSubmit(data) {
|
|
135
|
+
console.log("提交", data);
|
|
136
|
+
modalForm.close();
|
|
137
|
+
await table.load();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const table = useTable<RowType>({
|
|
142
|
+
columns: [
|
|
143
|
+
{ title: "名称", field: "name" },
|
|
144
|
+
{ title: "状态", field: "status" },
|
|
145
|
+
{
|
|
146
|
+
title: "操作",
|
|
147
|
+
actions: [
|
|
148
|
+
{ key: "edit", label: "编辑", type: "primary", onClick: row => modalForm.openEdit(row) },
|
|
149
|
+
{ key: "view", label: "查看", type: "link", onClick: row => modalForm.openView(row) },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
async getDataList({ pagination }) {
|
|
154
|
+
console.log("查询", pagination, searchForm.data);
|
|
155
|
+
return { data: [], total: 0 };
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<AutoCrud
|
|
162
|
+
title="示例"
|
|
163
|
+
:search-form-config="searchForm"
|
|
164
|
+
:table-config="table"
|
|
165
|
+
:modal-form-config="modalForm"
|
|
166
|
+
/>
|
|
167
|
+
</template>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## MCP 接入
|
|
171
|
+
|
|
172
|
+
本包内置 stdio MCP:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
node node_modules/auto-gen-crud/mcp/auto-gen-crud-mcp.mjs
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
如果使用全局安装:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
auto-gen-crud-mcp
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
VSCode AI 插件可以注册:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"auto-gen-crud": {
|
|
190
|
+
"command": "node",
|
|
191
|
+
"args": ["node_modules/auto-gen-crud/mcp/auto-gen-crud-mcp.mjs"]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Agent 优先调用这些工具:
|
|
198
|
+
|
|
199
|
+
- `vac_get_agent_guide`:获取 agent 生成 CRUD 页面的规则。
|
|
200
|
+
- `vac_get_generator_reference`:获取 OpenAPI 生成器参数和示例。
|
|
201
|
+
- `vac_get_api_reference`:查询组件和 hook API。
|
|
202
|
+
- `vac_list_schema_fields`:查询表单、表格 schema。
|
|
203
|
+
- `vac_search_docs`:搜索 README、API、agent guide 和核心类型。
|
|
204
|
+
|
|
205
|
+
## Agent Prompt 建议
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
你是 auto-gen-crud 页面生成 agent。先读取项目已有页面、请求工具和路由规范;如果有 OpenAPI,优先使用 auto-gen-crud/generator 生成初始化页面;如果没有 OpenAPI,再手写 AutoCrud + hooks。保持 index.vue/api/config 分层,pageContext 默认空对象,getDataList 使用 ...searchForm.data,枚举默认使用 TypeTextMap。生成后执行类型检查或项目已有校验命令。
|
|
209
|
+
```
|
|
210
|
+
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
|
|
8
|
+
const docs = {
|
|
9
|
+
readme: readText("README.md"),
|
|
10
|
+
api: readText("API.md"),
|
|
11
|
+
coreTypes: readText("src/core/types.ts"),
|
|
12
|
+
tableUtils: readText("src/core/table-utils.ts"),
|
|
13
|
+
formUtils: readText("src/core/form-utils.ts"),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const formComponents = [
|
|
17
|
+
"input",
|
|
18
|
+
"textarea",
|
|
19
|
+
"select",
|
|
20
|
+
"multipleSelect",
|
|
21
|
+
"radio",
|
|
22
|
+
"checkbox",
|
|
23
|
+
"inputNumber",
|
|
24
|
+
"datePicker",
|
|
25
|
+
"timePicker",
|
|
26
|
+
"switch",
|
|
27
|
+
"cascader",
|
|
28
|
+
"slider",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const uiInstallMap = {
|
|
32
|
+
"element-plus": {
|
|
33
|
+
install: "npm install auto-gen-crud element-plus",
|
|
34
|
+
imports: 'import ElementPlus from "element-plus";\nimport "element-plus/dist/index.css";',
|
|
35
|
+
use: "app.use(ElementPlus).use(createAutoCrud());",
|
|
36
|
+
},
|
|
37
|
+
naive: {
|
|
38
|
+
install: "npm install auto-gen-crud naive-ui",
|
|
39
|
+
imports: 'import naive from "naive-ui";',
|
|
40
|
+
use: 'app.use(naive).use(createAutoCrud({ ui: "naive" }));',
|
|
41
|
+
},
|
|
42
|
+
"ant-design-vue": {
|
|
43
|
+
install: "npm install auto-gen-crud ant-design-vue",
|
|
44
|
+
imports: 'import Antd from "ant-design-vue";\nimport "ant-design-vue/dist/reset.css";',
|
|
45
|
+
use: 'app.use(Antd).use(createAutoCrud({ ui: "ant-design-vue" }));',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const tools = [
|
|
50
|
+
{
|
|
51
|
+
name: "vac_get_overview",
|
|
52
|
+
description: "获取 auto-gen-crud 的安装方式、核心设计、UI adapter 选择和最小示例摘要。",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
topic: {
|
|
57
|
+
type: "string",
|
|
58
|
+
enum: ["all", "install", "example", "adapter", "migration"],
|
|
59
|
+
description: "要聚焦的主题。默认 all。",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "vac_get_api_reference",
|
|
66
|
+
description: "查询 auto-gen-crud 的 API 文档片段,例如 createAutoCrud、useTable、CrudFormItem。",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
symbol: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "API 名称,如 createAutoCrud、useSearchForm、useTable、useModalForm、CrudFormItem、CrudTableColumn、Adapter。",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ["symbol"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "vac_list_schema_fields",
|
|
80
|
+
description: "列出表单字段、表格列、安装配置等 schema 支持的字段和组件类型。",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
schema: {
|
|
85
|
+
type: "string",
|
|
86
|
+
enum: ["form", "table", "install", "components"],
|
|
87
|
+
description: "要查询的 schema 类型。",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: ["schema"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "vac_generate_crud_page",
|
|
95
|
+
description: "根据实体名和字段生成一个可复制的 Vue3 + auto-gen-crud 页面示例。",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
entityName: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "实体中文名或业务名,例如 用户、订单。",
|
|
102
|
+
},
|
|
103
|
+
rowTypeName: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "TS 行数据类型名,默认 RowData。",
|
|
106
|
+
},
|
|
107
|
+
ui: {
|
|
108
|
+
type: "string",
|
|
109
|
+
enum: ["element-plus", "naive", "ant-design-vue"],
|
|
110
|
+
description: "目标 UI。默认 element-plus。",
|
|
111
|
+
},
|
|
112
|
+
fields: {
|
|
113
|
+
type: "array",
|
|
114
|
+
description: "字段列表。每项支持 label、prop、component、table、search、modal、required、copy、decode、dateTime。",
|
|
115
|
+
items: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
label: { type: "string" },
|
|
119
|
+
prop: { type: "string" },
|
|
120
|
+
component: { type: "string", enum: formComponents },
|
|
121
|
+
table: { type: "boolean" },
|
|
122
|
+
search: { type: "boolean" },
|
|
123
|
+
modal: { type: "boolean" },
|
|
124
|
+
required: { type: "boolean" },
|
|
125
|
+
copy: { type: "boolean" },
|
|
126
|
+
decode: { type: "boolean" },
|
|
127
|
+
dateTime: { oneOf: [{ type: "boolean" }, { type: "string" }] },
|
|
128
|
+
},
|
|
129
|
+
required: ["label", "prop"],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ["entityName", "fields"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "vac_generate_install_snippet",
|
|
138
|
+
description: "生成指定 UI 库下的 Vue app 初始化代码,包括依赖安装命令和 createAutoCrud 配置。",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
ui: {
|
|
143
|
+
type: "string",
|
|
144
|
+
enum: ["element-plus", "naive", "ant-design-vue"],
|
|
145
|
+
description: "目标 UI。默认 element-plus。",
|
|
146
|
+
},
|
|
147
|
+
withDecode: {
|
|
148
|
+
type: "boolean",
|
|
149
|
+
description: "是否包含敏感信息 decode/decodeLog 示例。",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "vac_search_docs",
|
|
156
|
+
description: "在 README、API 和核心类型文件中搜索关键词,返回相关上下文。",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
query: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "搜索关键词,如 pagination、decode、options。",
|
|
163
|
+
},
|
|
164
|
+
limit: {
|
|
165
|
+
type: "number",
|
|
166
|
+
description: "最多返回多少条,默认 8。",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ["query"],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
function readText(relativePath) {
|
|
175
|
+
return readFileSync(resolve(rootDir, relativePath), "utf8");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function jsonRpc(id, result) {
|
|
179
|
+
return { jsonrpc: "2.0", id, result };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function jsonRpcError(id, code, message, data) {
|
|
183
|
+
return { jsonrpc: "2.0", id, error: { code, message, data } };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function textContent(text) {
|
|
187
|
+
return { content: [{ type: "text", text }] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sectionByHeading(text, heading) {
|
|
191
|
+
const lines = text.split(/\r?\n/);
|
|
192
|
+
const index = lines.findIndex(line => line.replace(/^#+\s*/, "").trim().toLowerCase() === heading.toLowerCase());
|
|
193
|
+
if (index < 0) return "";
|
|
194
|
+
const level = (lines[index].match(/^#+/) || [""])[0].length;
|
|
195
|
+
const end = lines.findIndex((line, lineIndex) => lineIndex > index && /^#+\s+/.test(line) && (line.match(/^#+/) || [""])[0].length <= level);
|
|
196
|
+
return lines.slice(index, end < 0 ? undefined : end).join("\n").trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function compact(text, max = 3500) {
|
|
200
|
+
if (text.length <= max) return text;
|
|
201
|
+
return `${text.slice(0, max)}\n\n...内容已截断,可用 vac_search_docs 继续查询。`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleToolCall(name, args = {}) {
|
|
205
|
+
switch (name) {
|
|
206
|
+
case "vac_get_overview":
|
|
207
|
+
return textContent(getOverview(args.topic || "all"));
|
|
208
|
+
case "vac_get_api_reference":
|
|
209
|
+
return textContent(getApiReference(args.symbol));
|
|
210
|
+
case "vac_list_schema_fields":
|
|
211
|
+
return textContent(getSchemaFields(args.schema));
|
|
212
|
+
case "vac_generate_crud_page":
|
|
213
|
+
return textContent(generateCrudPage(args));
|
|
214
|
+
case "vac_generate_install_snippet":
|
|
215
|
+
return textContent(generateInstallSnippet(args.ui || "element-plus", Boolean(args.withDecode)));
|
|
216
|
+
case "vac_search_docs":
|
|
217
|
+
return textContent(searchDocs(args.query, args.limit || 8));
|
|
218
|
+
default:
|
|
219
|
+
throw new Error(`未知工具:${name}。可用工具:${tools.map(tool => tool.name).join(", ")}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getOverview(topic) {
|
|
224
|
+
const map = {
|
|
225
|
+
install: sectionByHeading(docs.readme, "安装和初始化"),
|
|
226
|
+
example: sectionByHeading(docs.readme, "最小示例"),
|
|
227
|
+
adapter: `${sectionByHeading(docs.readme, "为什么要用 Adapter")}\n\n${sectionByHeading(docs.api, "Adapter 学习重点")}`,
|
|
228
|
+
migration: sectionByHeading(docs.readme, "从旧 auto-gen-crud 迁移"),
|
|
229
|
+
};
|
|
230
|
+
if (topic !== "all") return compact(map[topic] || docs.readme);
|
|
231
|
+
return compact(
|
|
232
|
+
[
|
|
233
|
+
sectionByHeading(docs.readme, "设计特点"),
|
|
234
|
+
sectionByHeading(docs.readme, "安装和初始化"),
|
|
235
|
+
sectionByHeading(docs.readme, "最小示例"),
|
|
236
|
+
sectionByHeading(docs.readme, "为什么要用 Adapter"),
|
|
237
|
+
].join("\n\n")
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getApiReference(symbol) {
|
|
242
|
+
const normalized = String(symbol || "").trim();
|
|
243
|
+
const heading = normalized.toLowerCase();
|
|
244
|
+
const headingMap = {
|
|
245
|
+
createautocrud: "createAutoCrud",
|
|
246
|
+
usesearchform: "useSearchForm",
|
|
247
|
+
usetable: "useTable",
|
|
248
|
+
usemodalform: "useModalForm",
|
|
249
|
+
crudformitem: "CrudFormItem",
|
|
250
|
+
crudtablecolumn: "CrudTableColumn",
|
|
251
|
+
adapter: "Adapter 学习重点",
|
|
252
|
+
};
|
|
253
|
+
const section = sectionByHeading(docs.api, headingMap[heading] || normalized);
|
|
254
|
+
if (section) return section;
|
|
255
|
+
|
|
256
|
+
const typeContext = searchInText(docs.coreTypes, normalized, 6)
|
|
257
|
+
.map(item => `src/core/types.ts:${item.line}\n${item.context}`)
|
|
258
|
+
.join("\n\n");
|
|
259
|
+
if (typeContext) return typeContext;
|
|
260
|
+
|
|
261
|
+
return `没有找到 ${normalized}。建议查询:createAutoCrud、useSearchForm、useTable、useModalForm、CrudFormItem、CrudTableColumn、Adapter。`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getSchemaFields(schema) {
|
|
265
|
+
switch (schema) {
|
|
266
|
+
case "components":
|
|
267
|
+
return `CrudFormComponent 支持:\n${formComponents.map(name => `- ${name}`).join("\n")}`;
|
|
268
|
+
case "install":
|
|
269
|
+
return sectionByHeading(docs.api, "AutoCrudInstallOptions");
|
|
270
|
+
case "form":
|
|
271
|
+
return `${sectionByHeading(docs.api, "CrudFormItem")}\n\n常用 component:${formComponents.join(", ")}`;
|
|
272
|
+
case "table":
|
|
273
|
+
return sectionByHeading(docs.api, "CrudTableColumn");
|
|
274
|
+
default:
|
|
275
|
+
return "schema 必须是 form、table、install 或 components。";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function generateInstallSnippet(ui, withDecode) {
|
|
280
|
+
const config = uiInstallMap[ui] || uiInstallMap["element-plus"];
|
|
281
|
+
const options = ui === "element-plus" ? "" : `{ ui: "${ui}" }`;
|
|
282
|
+
const decodeOptions = withDecode
|
|
283
|
+
? `{
|
|
284
|
+
${ui === "element-plus" ? "" : `ui: "${ui}",\n `}decode: async value => String(value),
|
|
285
|
+
decodeLog: async (value, row) => {
|
|
286
|
+
console.log("记录查看日志", value, row);
|
|
287
|
+
},
|
|
288
|
+
}`
|
|
289
|
+
: options;
|
|
290
|
+
return `安装:\n\n\`\`\`bash\n${config.install}\n\`\`\`\n\nmain.ts:\n\n\`\`\`ts\nimport { createApp } from "vue";
|
|
291
|
+
import { createAutoCrud } from "auto-gen-crud";
|
|
292
|
+
import App from "./App.vue";
|
|
293
|
+
${config.imports}
|
|
294
|
+
import "auto-gen-crud/style.css";
|
|
295
|
+
|
|
296
|
+
createApp(App)
|
|
297
|
+
.use(${ui === "element-plus" ? "ElementPlus" : ui === "naive" ? "naive" : "Antd"})
|
|
298
|
+
.use(createAutoCrud(${decodeOptions}))
|
|
299
|
+
.mount("#app");
|
|
300
|
+
\`\`\``;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function generateCrudPage(args) {
|
|
304
|
+
const entityName = safeText(args.entityName || "数据");
|
|
305
|
+
const rowTypeName = safeIdentifier(args.rowTypeName || "RowData");
|
|
306
|
+
const fields = Array.isArray(args.fields) && args.fields.length > 0 ? args.fields : [{ label: "名称", prop: "name", component: "input", table: true, search: true, modal: true, required: true }];
|
|
307
|
+
const normalizedFields = fields.map(field => ({
|
|
308
|
+
label: safeText(field.label || field.prop),
|
|
309
|
+
prop: safeProp(field.prop),
|
|
310
|
+
component: formComponents.includes(field.component) ? field.component : "input",
|
|
311
|
+
table: field.table !== false,
|
|
312
|
+
search: Boolean(field.search),
|
|
313
|
+
modal: field.modal !== false,
|
|
314
|
+
required: Boolean(field.required),
|
|
315
|
+
copy: Boolean(field.copy),
|
|
316
|
+
decode: Boolean(field.decode),
|
|
317
|
+
dateTime: field.dateTime,
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
const typeFields = normalizedFields.map(field => ` ${field.prop}: ${field.component === "inputNumber" || field.component === "slider" ? "number" : field.component === "switch" ? "boolean" : "string"};`).join("\n");
|
|
321
|
+
const searchColumns = normalizedFields.filter(field => field.search).map(toFormItem).join(",\n ");
|
|
322
|
+
const modalColumns = normalizedFields.filter(field => field.modal).map(toFormItem).join(",\n ");
|
|
323
|
+
const tableColumns = normalizedFields.filter(field => field.table).map(toTableColumn).join(",\n ");
|
|
324
|
+
|
|
325
|
+
return `\`\`\`vue
|
|
326
|
+
<script setup lang="ts">
|
|
327
|
+
import { AutoCrud, useModalForm, useSearchForm, useTable } from "auto-gen-crud";
|
|
328
|
+
|
|
329
|
+
interface ${rowTypeName} extends Record<string, unknown> {
|
|
330
|
+
${typeFields}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const searchForm = useSearchForm({
|
|
334
|
+
columns: [
|
|
335
|
+
${searchColumns || toFormItem(normalizedFields[0])},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const modalForm = useModalForm<${rowTypeName}>({
|
|
340
|
+
columns: [
|
|
341
|
+
${modalColumns || toFormItem(normalizedFields[0])},
|
|
342
|
+
],
|
|
343
|
+
async onSubmit(data, mode) {
|
|
344
|
+
console.log("${entityName}提交", mode, data);
|
|
345
|
+
modalForm.close();
|
|
346
|
+
await table.load(searchForm.data);
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const table = useTable<${rowTypeName}>({
|
|
351
|
+
columns: [
|
|
352
|
+
${tableColumns || toTableColumn(normalizedFields[0])},
|
|
353
|
+
{
|
|
354
|
+
title: "操作",
|
|
355
|
+
actions: [
|
|
356
|
+
{ key: "edit", label: "编辑", type: "primary", onClick: row => modalForm.openEdit(row) },
|
|
357
|
+
{ key: "view", label: "查看", type: "link", onClick: row => modalForm.openView(row) },
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
async getDataList({ pagination, searchData }) {
|
|
362
|
+
console.log("${entityName}查询", pagination, searchData);
|
|
363
|
+
return { data: [], total: 0 };
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
</script>
|
|
367
|
+
|
|
368
|
+
<template>
|
|
369
|
+
<AutoCrud
|
|
370
|
+
title="${entityName}"
|
|
371
|
+
:search-form-config="searchForm"
|
|
372
|
+
:table-config="table"
|
|
373
|
+
:modal-form-config="modalForm"
|
|
374
|
+
/>
|
|
375
|
+
</template>
|
|
376
|
+
\`\`\``;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function toFormItem(field) {
|
|
380
|
+
const extra = field.required ? ", required: true" : "";
|
|
381
|
+
return `{ label: "${field.label}", prop: "${field.prop}", component: "${field.component}"${extra} }`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function toTableColumn(field) {
|
|
385
|
+
const attrs = [
|
|
386
|
+
`title: "${field.label}"`,
|
|
387
|
+
`field: "${field.prop}"`,
|
|
388
|
+
field.copy ? "copy: true" : "",
|
|
389
|
+
field.decode ? "decode: true" : "",
|
|
390
|
+
field.dateTime ? `dateTime: ${field.dateTime === true ? "true" : JSON.stringify(field.dateTime)}` : "",
|
|
391
|
+
].filter(Boolean);
|
|
392
|
+
return `{ ${attrs.join(", ")} }`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function safeIdentifier(value) {
|
|
396
|
+
const text = String(value).replace(/[^\w$]/g, "");
|
|
397
|
+
return text || "RowData";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function safeProp(value) {
|
|
401
|
+
return String(value || "name").replace(/[^a-zA-Z0-9_.$]/g, "") || "name";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function safeText(value) {
|
|
405
|
+
return String(value || "").replace(/["\\]/g, "");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function searchDocs(query, limit) {
|
|
409
|
+
const parts = [
|
|
410
|
+
["README.md", docs.readme],
|
|
411
|
+
["API.md", docs.api],
|
|
412
|
+
["src/core/types.ts", docs.coreTypes],
|
|
413
|
+
["src/core/form-utils.ts", docs.formUtils],
|
|
414
|
+
["src/core/table-utils.ts", docs.tableUtils],
|
|
415
|
+
];
|
|
416
|
+
const results = parts.flatMap(([file, text]) => searchInText(text, query, 3).map(item => ({ file, ...item }))).slice(0, limit);
|
|
417
|
+
if (results.length === 0) return `没有找到 "${query}"。`;
|
|
418
|
+
return results.map(item => `${item.file}:${item.line}\n${item.context}`).join("\n\n---\n\n");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function searchInText(text, query, radius) {
|
|
422
|
+
const keyword = String(query || "").toLowerCase();
|
|
423
|
+
if (!keyword) return [];
|
|
424
|
+
const lines = text.split(/\r?\n/);
|
|
425
|
+
const matches = [];
|
|
426
|
+
lines.forEach((line, index) => {
|
|
427
|
+
if (line.toLowerCase().includes(keyword)) {
|
|
428
|
+
const start = Math.max(0, index - radius);
|
|
429
|
+
const end = Math.min(lines.length, index + radius + 1);
|
|
430
|
+
matches.push({
|
|
431
|
+
line: index + 1,
|
|
432
|
+
context: lines.slice(start, end).join("\n"),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return matches;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function handleRequest(request) {
|
|
440
|
+
const { id, method, params = {} } = request;
|
|
441
|
+
if (method === "initialize") {
|
|
442
|
+
return jsonRpc(id, {
|
|
443
|
+
protocolVersion: params.protocolVersion || "2024-11-05",
|
|
444
|
+
capabilities: {
|
|
445
|
+
tools: {},
|
|
446
|
+
},
|
|
447
|
+
serverInfo: {
|
|
448
|
+
name: "auto-gen-crud-mcp",
|
|
449
|
+
version: "0.1.0",
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (method === "tools/list") {
|
|
455
|
+
return jsonRpc(id, { tools });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (method === "tools/call") {
|
|
459
|
+
try {
|
|
460
|
+
const result = handleToolCall(params.name, params.arguments || {});
|
|
461
|
+
return jsonRpc(id, result);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return jsonRpcError(id, -32000, error instanceof Error ? error.message : String(error));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (method === "notifications/initialized") return null;
|
|
468
|
+
|
|
469
|
+
return jsonRpcError(id, -32601, `不支持的方法:${method}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let buffer = "";
|
|
473
|
+
|
|
474
|
+
process.stdin.setEncoding("utf8");
|
|
475
|
+
process.stdin.on("data", chunk => {
|
|
476
|
+
buffer += chunk;
|
|
477
|
+
drainBuffer().catch(error => {
|
|
478
|
+
writeMessage(jsonRpcError(null, -32000, error instanceof Error ? error.message : String(error)));
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
async function drainBuffer() {
|
|
483
|
+
while (true) {
|
|
484
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
485
|
+
if (headerEnd < 0) return;
|
|
486
|
+
const header = buffer.slice(0, headerEnd);
|
|
487
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
488
|
+
if (!match) throw new Error("缺少 Content-Length header");
|
|
489
|
+
const length = Number(match[1]);
|
|
490
|
+
const bodyStart = headerEnd + 4;
|
|
491
|
+
if (buffer.length < bodyStart + length) return;
|
|
492
|
+
const body = buffer.slice(bodyStart, bodyStart + length);
|
|
493
|
+
buffer = buffer.slice(bodyStart + length);
|
|
494
|
+
const response = await handleRequest(JSON.parse(body));
|
|
495
|
+
if (response) writeMessage(response);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function writeMessage(message) {
|
|
500
|
+
const body = JSON.stringify(message);
|
|
501
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
|
|
502
|
+
}
|