@agile-team/wl-skills-kit 2.7.2 → 2.8.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/CHANGELOG.md +37 -0
- package/README.md +40 -12
- package/bin/wl-skills.js +147 -3
- package/files/.github/copilot-instructions.md +18 -0
- package/files/.github/guides/architecture.md +6 -4
- package/files/.github/skills/_best-practices.md +230 -220
- package/files/.github/skills/core/page-codegen/SKILL.md +5 -5
- package/files/.github/skills/sync/_mcp-guardrail.md +109 -109
- package/files/.github/skills/sync/dict-sync/SKILL.md +208 -208
- package/files/.github/skills/sync/permission-sync/SKILL.md +240 -275
- package/files/docs/mock-architecture.md +321 -0
- package/files/mock/_utils.ts +35 -0
- package/mcp/api/client.js +83 -83
- package/mcp/tools/dictSync.js +178 -173
- package/mcp/tools/menuSync.js +11 -0
- package/package.json +2 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Mock 架构规范
|
|
2
|
+
|
|
3
|
+
> **适用范围**:所有基于 wl-skills-kit 的 Vue 3 业务子应用
|
|
4
|
+
> **技术依赖**:`vite-plugin-mock` + `mockjs`
|
|
5
|
+
> **源码参考**:wl-mdata 项目实践(v2.7.x 基线)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、设计目标
|
|
10
|
+
|
|
11
|
+
| 目标 | 实现方式 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **与页面代码完全解耦** | Mock 文件独立放在 `mock/` 目录,不在 `src/views` 中 import 任何 mock |
|
|
14
|
+
| **开关零污染** | `ENV_MOCK=false` → vite-plugin-mock 整体不挂载,不拦截任何请求 |
|
|
15
|
+
| **按业务模块自治** | 每个模块一个 mock 文件,删除模块只需删除对应 mock 文件 |
|
|
16
|
+
| **共享工具复用** | `_utils.ts` 提供标准响应构造器,子模块 import 而非重复定义 |
|
|
17
|
+
| **会话内数据可见** | `let STORE = [...]` 可变数组,CRUD 操作直接修改内存,下次查询可见 |
|
|
18
|
+
| **跨会话重置** | dev server 重启时模块重新加载,STORE 恢复初始状态 |
|
|
19
|
+
| **真实接口无缝切换** | `API_CONFIG` 保持真实路径,mock 端点带 `/dev-api` 前缀,关闭 mock 后无需改代码 |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 二、目录结构
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
项目根目录/
|
|
27
|
+
├── .env.dev ← ENV_MOCK=true(Mock 总开关)
|
|
28
|
+
├── vite.config.ts ← viteMockServe({ mockPath: "./mock", enable: ... })
|
|
29
|
+
├── mock/
|
|
30
|
+
│ ├── _utils.ts ← 共享工具(pageResult / ok / paginate / nowStr / pick)
|
|
31
|
+
│ └── [业务域]/ ← 镜像 src/views 第一级目录
|
|
32
|
+
│ ├── [模块].ts ← 一个模块可覆盖多个相关页面
|
|
33
|
+
│ ├── [模块].ts
|
|
34
|
+
│ └── ...
|
|
35
|
+
└── src/
|
|
36
|
+
└── views/
|
|
37
|
+
└── [业务域]/
|
|
38
|
+
└── [模块]/
|
|
39
|
+
└── [页面]/
|
|
40
|
+
├── index.vue
|
|
41
|
+
├── data.ts ← API_CONFIG 保持真实路径
|
|
42
|
+
└── api.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**命名约定**:
|
|
46
|
+
|
|
47
|
+
| src/views 路径 | mock 文件路径 | 说明 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `src/views/mdata/model/` | `mock/mdata/model.ts` | 一个模块一个文件 |
|
|
50
|
+
| `src/views/mdata/management/` | `mock/mdata/master.ts` | 可按业务语义命名 |
|
|
51
|
+
| `src/views/sale/customer/` | `mock/sale/customer.ts` | 镜像第一级域 |
|
|
52
|
+
| `src/views/sale/contract/` | `mock/sale/contract.ts` | 同域不同模块 |
|
|
53
|
+
|
|
54
|
+
> **一个 mock 文件可覆盖同模块下多个页面的接口**,只要它们属于同一业务实体(如"客户管理"和"客户申请"可共用 `mock/sale/customer.ts`)。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 三、`_utils.ts` 共享工具
|
|
59
|
+
|
|
60
|
+
> 该文件由 `wl-skills init` 自动写入 `mock/_utils.ts`。无 `export default`,vite-plugin-mock 会安全跳过。
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import Mock from "mockjs";
|
|
64
|
+
|
|
65
|
+
export const Random = Mock.Random;
|
|
66
|
+
export const pick = <T>(arr: T[]): T => Random.pick(arr) as T;
|
|
67
|
+
export const bool = () => Random.boolean();
|
|
68
|
+
|
|
69
|
+
/** 标准分页响应 */
|
|
70
|
+
export function pageResult(records: any[], total: number) {
|
|
71
|
+
return { code: 2000, message: "操作成功", data: { records, total } };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 标准成功响应 */
|
|
75
|
+
export function ok(data: any = null) {
|
|
76
|
+
return { code: 2000, message: "操作成功", data };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 通用分页截取(兼容 pageNo/current、pageSize/size) */
|
|
80
|
+
export function paginate(pool: any[], query: any) {
|
|
81
|
+
const current = Number(query?.current || query?.pageNo) || 1;
|
|
82
|
+
const size = Number(query?.size || query?.pageSize) || 10;
|
|
83
|
+
const start = (current - 1) * size;
|
|
84
|
+
return pageResult(pool.slice(start, start + size), pool.length);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 当前时间字符串 */
|
|
88
|
+
export function nowStr() {
|
|
89
|
+
return new Date().toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-");
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
所有 mock 模块文件统一通过 `import { paginate, ok, pick, nowStr } from "../_utils"` 引入。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 四、Mock 文件编写规范
|
|
98
|
+
|
|
99
|
+
### 4.1 文件头注释
|
|
100
|
+
|
|
101
|
+
每个 mock 文件顶部必须声明覆盖的业务模块和接口:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
/**
|
|
105
|
+
* [业务模块名] Mock
|
|
106
|
+
* 覆盖接口:[接口前缀1] / [接口前缀2]
|
|
107
|
+
*/
|
|
108
|
+
import type { MockMethod } from "vite-plugin-mock";
|
|
109
|
+
import Mock from "mockjs";
|
|
110
|
+
import { paginate, ok, pick, nowStr } from "../_utils";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 4.2 STORE 模式
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// ─── 数据生成器 ──────────────────────────────────────────────────
|
|
117
|
+
function genRecord() {
|
|
118
|
+
return {
|
|
119
|
+
id: Mock.Random.id(),
|
|
120
|
+
code: `MDM${Mock.Random.string("number", 8)}`,
|
|
121
|
+
name: Mock.Random.cword(4, 10),
|
|
122
|
+
status: pick(["ACTIVE", "DRAFT", "FROZEN"]),
|
|
123
|
+
createBy: Mock.Random.cname(),
|
|
124
|
+
createTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── 可变存储(dev server 会话内持久化)──────────────────────────
|
|
129
|
+
let STORE: any[] = Array.from({ length: 50 }, genRecord);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**核心规则**:
|
|
133
|
+
- 用 `let`(非 `const`)声明 STORE,允许 splice/unshift/assign
|
|
134
|
+
- 生成器函数 `genRecord()` 封装单条数据的随机逻辑
|
|
135
|
+
- 初始数据量建议 20-100 条,支持分页验证
|
|
136
|
+
|
|
137
|
+
### 4.3 端点编写
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
export default [
|
|
141
|
+
// ── 列表(分页)──
|
|
142
|
+
{
|
|
143
|
+
url: "/dev-api/[服务]/[资源]/list",
|
|
144
|
+
method: "get",
|
|
145
|
+
response: ({ query }: any) => paginate(STORE, query),
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// ── 新增 ──
|
|
149
|
+
{
|
|
150
|
+
url: "/dev-api/[服务]/[资源]/save",
|
|
151
|
+
method: "post",
|
|
152
|
+
response: ({ body }: any) => {
|
|
153
|
+
STORE.unshift({ ...genRecord(), ...body, id: Mock.Random.id(), createTime: nowStr() });
|
|
154
|
+
return ok(null);
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// ── 编辑 ──
|
|
159
|
+
{
|
|
160
|
+
url: "/dev-api/[服务]/[资源]/update",
|
|
161
|
+
method: "post",
|
|
162
|
+
response: ({ body }: any) => {
|
|
163
|
+
const idx = STORE.findIndex((r) => r.id === body?.id);
|
|
164
|
+
if (idx >= 0) Object.assign(STORE[idx], body, { updateTime: nowStr() });
|
|
165
|
+
return ok(null);
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// ── 删除 ──
|
|
170
|
+
{
|
|
171
|
+
url: "/dev-api/[服务]/[资源]/remove",
|
|
172
|
+
method: "post",
|
|
173
|
+
response: ({ body }: any) => {
|
|
174
|
+
const ids = Array.isArray(body?.ids) ? body.ids : [body?.id];
|
|
175
|
+
ids.forEach((id: string) => {
|
|
176
|
+
const idx = STORE.findIndex((r) => r.id === id);
|
|
177
|
+
if (idx >= 0) STORE.splice(idx, 1);
|
|
178
|
+
});
|
|
179
|
+
return ok(null);
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// ── 详情 ──
|
|
184
|
+
{
|
|
185
|
+
url: "/dev-api/[服务]/[资源]/getById",
|
|
186
|
+
method: "get",
|
|
187
|
+
response: ({ query }: any) => ok(STORE.find((r) => r.id === query?.id) || null),
|
|
188
|
+
},
|
|
189
|
+
] as MockMethod[];
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 4.4 端点覆盖检查
|
|
193
|
+
|
|
194
|
+
**每个 `API_CONFIG` 的 key 都必须在 mock 文件中有对应端点**,零遗漏。
|
|
195
|
+
|
|
196
|
+
| 操作 | STORE 修改方式 |
|
|
197
|
+
|---|---|
|
|
198
|
+
| 删除 | `STORE.splice(idx, 1)` |
|
|
199
|
+
| 新增 | `STORE.unshift({ ...genRecord(), ...body, id })` |
|
|
200
|
+
| 编辑 | `Object.assign(STORE[idx], body)` |
|
|
201
|
+
| 启用/停用 | 修改 `item.status` |
|
|
202
|
+
| 提交/审批 | 修改 `item.approvalStatus` |
|
|
203
|
+
| 作废 | `STORE.splice(idx, 1)` 或修改状态 |
|
|
204
|
+
|
|
205
|
+
> ❌ 禁止端点只返回 `{ code: 2000 }` 不修改 STORE — 否则 `select()` 刷新后看不到变化。
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 五、开关机制
|
|
210
|
+
|
|
211
|
+
### 5.1 环境变量
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# .env.dev
|
|
215
|
+
ENV_MOCK=true # true = 启用 mock,false = 关闭 mock 走真实接口
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 5.2 Vite 配置
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// vite.config.ts
|
|
222
|
+
import { viteMockServe } from "vite-plugin-mock";
|
|
223
|
+
|
|
224
|
+
plugins: [
|
|
225
|
+
viteMockServe({
|
|
226
|
+
mockPath: "./mock",
|
|
227
|
+
enable: command === "serve" && config.ENV_MOCK !== "false",
|
|
228
|
+
logger: true,
|
|
229
|
+
}),
|
|
230
|
+
]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**关键**:`enable` 判断只在 `serve` 模式且 `ENV_MOCK` 不为 `"false"` 时挂载。生产构建永远不包含 mock。
|
|
234
|
+
|
|
235
|
+
### 5.3 切换流程
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
开发阶段(Mock):ENV_MOCK=true → 所有接口走 mock,Network 可见
|
|
239
|
+
联调阶段(真实):ENV_MOCK=false → mock 插件不加载,接口走 proxy → 后端
|
|
240
|
+
部分联调: ENV_MOCK=false + 仅保留需要 mock 的端点(手动注释其余)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
> 切换时**无需修改任何页面代码**。`API_CONFIG` 始终保持真实路径,mock 端点的 `/dev-api` 前缀由 Vite proxy 统一处理。
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 六、一键清理
|
|
248
|
+
|
|
249
|
+
CLI 提供 `mock-clean` 命令,支持按模块清理或全量清理:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# 清理指定业务域的 mock
|
|
253
|
+
npx @agile-team/wl-skills-kit mock-clean --domain mdata
|
|
254
|
+
|
|
255
|
+
# 清理全部 mock(保留 _utils.ts)
|
|
256
|
+
npx @agile-team/wl-skills-kit mock-clean --all
|
|
257
|
+
|
|
258
|
+
# 预览将要删除的文件(dry-run)
|
|
259
|
+
npx @agile-team/wl-skills-kit mock-clean --all --dry-run
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
清理后建议:
|
|
263
|
+
1. 将 `.env.dev` 中 `ENV_MOCK` 改为 `false`
|
|
264
|
+
2. 确认 `vite.config.ts` 中 proxy 已配置正确的后端地址
|
|
265
|
+
3. 运行 `wl-skills validate` 确认页面无 mock 依赖残留
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 七、validate 检查项
|
|
270
|
+
|
|
271
|
+
`wl-skills validate` 对 mock 的检查(自动执行):
|
|
272
|
+
|
|
273
|
+
| 检查项 | 级别 | 说明 |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `mock/_utils.ts` 是否存在 | warn | 缺失则提示 `wl-skills init` 补充 |
|
|
276
|
+
| API_CONFIG URL 是否有对应 mock 端点 | warn | 每个 URL 在 mock 文件中必须有 `/dev-api` 前缀的端点 |
|
|
277
|
+
| mock 文件是否 import `_utils` | info | 未使用共享工具则提示 |
|
|
278
|
+
| mock 文件是否按域分目录 | info | 扁平放在 `mock/` 根目录则提示迁移 |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 八、常见问题
|
|
283
|
+
|
|
284
|
+
### Q: mock 和真实接口返回格式不一致怎么办?
|
|
285
|
+
|
|
286
|
+
确保 `_utils.ts` 中 `pageResult` / `ok` 的返回格式与后端统一:
|
|
287
|
+
```typescript
|
|
288
|
+
// 如果后端用 code: 200
|
|
289
|
+
export function ok(data: any = null) {
|
|
290
|
+
return { code: 200, message: "操作成功", data };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 如果后端用 code: 2000(平台默认)
|
|
294
|
+
export function ok(data: any = null) {
|
|
295
|
+
return { code: 2000, message: "操作成功", data };
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
> 在 `_utils.ts` 中统一修改一处即可,无需改每个 mock 文件。
|
|
300
|
+
|
|
301
|
+
### Q: 多个页面共享同一套数据怎么办?
|
|
302
|
+
|
|
303
|
+
同一个 mock 模块文件中声明一个 STORE,多个端点共享。例如"客户列表"和"客户申请"可共享 `CUSTOMER_STORE`。
|
|
304
|
+
|
|
305
|
+
### Q: 某个接口需要走真实后端,其余走 mock?
|
|
306
|
+
|
|
307
|
+
方案一:删除该接口在 mock 文件中的端点,vite-plugin-mock 找不到匹配就会放行到 proxy。
|
|
308
|
+
方案二:保持 `ENV_MOCK=true`,在 vite proxy 中对特定路径单独配置 target。
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 九、架构要点速查
|
|
313
|
+
|
|
314
|
+
| 能力 | 实现方式 |
|
|
315
|
+
|---|---|
|
|
316
|
+
| **新增数据可见** | 每个模块维护 `let STORE = [...]` 可变数组,save 用 `unshift` 置顶,下次 list 查询即可看到 |
|
|
317
|
+
| **关闭 mock 零污染** | `ENV_MOCK=false` → vite-plugin-mock 整体不挂载,不会有任何 mock handler 拦截真实请求 |
|
|
318
|
+
| **按模块独立** | 每个文件自治,未来删某业务模块 mock 只删对应文件 |
|
|
319
|
+
| **共享工具复用** | `_utils.ts` 提供 `pageResult` / `ok` / `paginate` / `nowStr` / `pick`,子模块 import 而非重复定义 |
|
|
320
|
+
| **跨会话重置** | dev server 重启时内存清空,mock 数据恢复初始状态(符合预期) |
|
|
321
|
+
| **URL 零修改切换** | `API_CONFIG` 保持真实路径,mock URL 带 `/dev-api` 前缀,关闭 mock 后业务代码无需任何改动 |
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock 公共工具函数
|
|
3
|
+
* 由 wl-skills-kit 提供,所有 mock 模块文件统一 import 此文件。
|
|
4
|
+
* 注意:本文件无 export default,vite-plugin-mock 会安全跳过,不会报错。
|
|
5
|
+
*/
|
|
6
|
+
import Mock from "mockjs";
|
|
7
|
+
|
|
8
|
+
export const Random = Mock.Random;
|
|
9
|
+
export const pick = <T>(arr: T[]): T => Random.pick(arr) as T;
|
|
10
|
+
export const bool = () => Random.boolean();
|
|
11
|
+
|
|
12
|
+
/** 标准分页响应 */
|
|
13
|
+
export function pageResult(records: any[], total: number) {
|
|
14
|
+
return { code: 2000, message: "操作成功", data: { records, total } };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 标准成功响应 */
|
|
18
|
+
export function ok(data: any = null) {
|
|
19
|
+
return { code: 2000, message: "操作成功", data };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 通用分页截取(兼容 pageNo/current、pageSize/size 两种参数名) */
|
|
23
|
+
export function paginate(pool: any[], query: any) {
|
|
24
|
+
const current = Number(query?.current || query?.pageNo) || 1;
|
|
25
|
+
const size = Number(query?.size || query?.pageSize) || 10;
|
|
26
|
+
const start = (current - 1) * size;
|
|
27
|
+
return pageResult(pool.slice(start, start + size), pool.length);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 当前时间字符串 */
|
|
31
|
+
export function nowStr() {
|
|
32
|
+
return new Date()
|
|
33
|
+
.toLocaleString("zh-CN", { hour12: false })
|
|
34
|
+
.replace(/\//g, "-");
|
|
35
|
+
}
|
package/mcp/api/client.js
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const https = require('https')
|
|
4
|
-
const http = require('http')
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 带鉴权的 HTTP 客户端(兼容 Node 16+,无额外依赖)
|
|
8
|
-
*
|
|
9
|
-
* @param {string} urlPath - 接口路径(以 / 开头),拼接到 config.gatewayPath 后
|
|
10
|
-
* @param {{ method?: string, body?: unknown }} options
|
|
11
|
-
* @param {{ gatewayPath: string, token: string }} config
|
|
12
|
-
* @returns {Promise<{ ok: boolean, data: any, error?: string, code?: number }>}
|
|
13
|
-
*/
|
|
14
|
-
function wlsFetch(urlPath, options, config) {
|
|
15
|
-
const fullUrl = config.gatewayPath + urlPath
|
|
16
|
-
const isHttps = fullUrl.startsWith('https')
|
|
17
|
-
const lib = isHttps ? https : http
|
|
18
|
-
const bodyStr = options.body ? JSON.stringify(options.body) : null
|
|
19
|
-
|
|
20
|
-
let urlObj
|
|
21
|
-
try {
|
|
22
|
-
urlObj = new URL(fullUrl)
|
|
23
|
-
} catch (e) {
|
|
24
|
-
return Promise.reject(new Error(`无效的 URL: ${fullUrl}`))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const reqOptions = {
|
|
28
|
-
hostname: urlObj.hostname,
|
|
29
|
-
port: urlObj.port || (isHttps ? 443 : 80),
|
|
30
|
-
path: urlObj.pathname + urlObj.search,
|
|
31
|
-
method: options.method || 'GET',
|
|
32
|
-
headers: {
|
|
33
|
-
'Content-Type': 'application/json',
|
|
34
|
-
'Authorization': `Bearer ${config.token}`,
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (bodyStr) {
|
|
39
|
-
reqOptions.headers['Content-Length'] = Buffer.byteLength(bodyStr)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return new Promise((resolve, reject) => {
|
|
43
|
-
const req = lib.request(reqOptions, (res) => {
|
|
44
|
-
let data = ''
|
|
45
|
-
res.on('data', (chunk) => { data += chunk })
|
|
46
|
-
res.on('end', () => {
|
|
47
|
-
try {
|
|
48
|
-
const json = JSON.parse(data)
|
|
49
|
-
if (json.code === 2000) {
|
|
50
|
-
resolve({ ok: true, data: json.data, code: json.code })
|
|
51
|
-
} else {
|
|
52
|
-
// 友好提示:401/4004 等典型错误附带排查指引
|
|
53
|
-
let hint = ''
|
|
54
|
-
if (json.code === 401 || /token|未登录|鉴权/i.test(json.message || '')) {
|
|
55
|
-
hint = '(Token 可能已过期,请重新登录后更新 env.local.json 的 token 字段,仅填纯 JWT 不含 bearer 前缀)'
|
|
56
|
-
} else if (json.code === 4004 || /url:|not\s*found/i.test(json.message || '')) {
|
|
57
|
-
hint = '(接口未找到。可能是 gatewayPath 配置缺失前缀或后端环境差异;请检查 env.local.json 的 gatewayPath。不要让 AI 绕开 MCP 自己拼 HTTP)'
|
|
58
|
-
}
|
|
59
|
-
resolve({
|
|
60
|
-
ok: false,
|
|
61
|
-
data: null,
|
|
62
|
-
error: (json.message || `服务端返回 code=${json.code}`) + hint,
|
|
63
|
-
code: json.code,
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
reject(new Error(`响应解析失败: ${e.message},原始内容: ${data.slice(0, 200)}`))
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
req.on('error', (e) => reject(new Error(`请求失败: ${e.message}`)))
|
|
73
|
-
req.setTimeout(15000, () => {
|
|
74
|
-
req.destroy()
|
|
75
|
-
reject(new Error('请求超时(15s)'))
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
if (bodyStr) req.write(bodyStr)
|
|
79
|
-
req.end()
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = { wlsFetch }
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const https = require('https')
|
|
4
|
+
const http = require('http')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 带鉴权的 HTTP 客户端(兼容 Node 16+,无额外依赖)
|
|
8
|
+
*
|
|
9
|
+
* @param {string} urlPath - 接口路径(以 / 开头),拼接到 config.gatewayPath 后
|
|
10
|
+
* @param {{ method?: string, body?: unknown }} options
|
|
11
|
+
* @param {{ gatewayPath: string, token: string }} config
|
|
12
|
+
* @returns {Promise<{ ok: boolean, data: any, error?: string, code?: number }>}
|
|
13
|
+
*/
|
|
14
|
+
function wlsFetch(urlPath, options, config) {
|
|
15
|
+
const fullUrl = config.gatewayPath + urlPath
|
|
16
|
+
const isHttps = fullUrl.startsWith('https')
|
|
17
|
+
const lib = isHttps ? https : http
|
|
18
|
+
const bodyStr = options.body ? JSON.stringify(options.body) : null
|
|
19
|
+
|
|
20
|
+
let urlObj
|
|
21
|
+
try {
|
|
22
|
+
urlObj = new URL(fullUrl)
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return Promise.reject(new Error(`无效的 URL: ${fullUrl}`))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const reqOptions = {
|
|
28
|
+
hostname: urlObj.hostname,
|
|
29
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
30
|
+
path: urlObj.pathname + urlObj.search,
|
|
31
|
+
method: options.method || 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Authorization': `Bearer ${config.token}`,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (bodyStr) {
|
|
39
|
+
reqOptions.headers['Content-Length'] = Buffer.byteLength(bodyStr)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const req = lib.request(reqOptions, (res) => {
|
|
44
|
+
let data = ''
|
|
45
|
+
res.on('data', (chunk) => { data += chunk })
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.parse(data)
|
|
49
|
+
if (json.code === 2000) {
|
|
50
|
+
resolve({ ok: true, data: json.data, code: json.code })
|
|
51
|
+
} else {
|
|
52
|
+
// 友好提示:401/4004 等典型错误附带排查指引
|
|
53
|
+
let hint = ''
|
|
54
|
+
if (json.code === 401 || /token|未登录|鉴权/i.test(json.message || '')) {
|
|
55
|
+
hint = '(Token 可能已过期,请重新登录后更新 env.local.json 的 token 字段,仅填纯 JWT 不含 bearer 前缀)'
|
|
56
|
+
} else if (json.code === 4004 || /url:|not\s*found/i.test(json.message || '')) {
|
|
57
|
+
hint = '(接口未找到。可能是 gatewayPath 配置缺失前缀或后端环境差异;请检查 env.local.json 的 gatewayPath。不要让 AI 绕开 MCP 自己拼 HTTP)'
|
|
58
|
+
}
|
|
59
|
+
resolve({
|
|
60
|
+
ok: false,
|
|
61
|
+
data: null,
|
|
62
|
+
error: (json.message || `服务端返回 code=${json.code}`) + hint,
|
|
63
|
+
code: json.code,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
reject(new Error(`响应解析失败: ${e.message},原始内容: ${data.slice(0, 200)}`))
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
req.on('error', (e) => reject(new Error(`请求失败: ${e.message}`)))
|
|
73
|
+
req.setTimeout(15000, () => {
|
|
74
|
+
req.destroy()
|
|
75
|
+
reject(new Error('请求超时(15s)'))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (bodyStr) req.write(bodyStr)
|
|
79
|
+
req.end()
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { wlsFetch }
|