@milian/linker-cli 0.0.0-stage → 0.17.3
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 +117 -2
- package/bin/linker-cli.js +24 -0
- package/package.json +19 -4
- package/src/api.js +154 -0
- package/src/cli.js +222 -0
- package/src/commands/api-create.js +478 -0
- package/src/commands/api-list.js +247 -0
- package/src/commands/api-publish.js +288 -0
- package/src/commands/auth.js +242 -0
- package/src/commands/groups.js +109 -0
- package/src/commands/mcp.js +485 -0
- package/src/commands/platform-create.js +279 -0
- package/src/commands/platforms.js +206 -0
- package/src/commands/request.js +55 -0
- package/src/commands/session.js +119 -0
- package/src/config.js +227 -0
- package/src/crypto.js +175 -0
- package/src/login-page.js +175 -0
- package/src/schema.js +582 -0
- package/src/ui.js +57 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadCredentials } = require('../config');
|
|
4
|
+
const { authedRequest } = require('../api');
|
|
5
|
+
const { resolvePlatformRecord } = require('./platforms');
|
|
6
|
+
const { renderTable, truncate } = require('../ui');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MCP 工具来源类型(业务方定义)
|
|
10
|
+
* 1 = 连接器资源(api) 2 = 连接流资源(flow)
|
|
11
|
+
*/
|
|
12
|
+
const SOURCE_TYPE_NAMES = { 1: '连接器资源', 2: '连接流资源' };
|
|
13
|
+
|
|
14
|
+
/** 来源查询接口的 pageNum/pageSize 由平台约定写死 */
|
|
15
|
+
const SOURCE_PAGE_NUM = 1;
|
|
16
|
+
const SOURCE_PAGE_SIZE = 1000;
|
|
17
|
+
|
|
18
|
+
// 表格显示上限:来源可能两三百个,全量倒给 AI 既慢又费上下文
|
|
19
|
+
const DISPLAY_LIMIT = 20;
|
|
20
|
+
const NAME_MAX_WIDTH = 40;
|
|
21
|
+
const GROUP_MAX_WIDTH = 16;
|
|
22
|
+
|
|
23
|
+
const SOURCES_USAGE = `用法: wb mcp sources [选项]
|
|
24
|
+
|
|
25
|
+
查询可创建 MCP 工具的来源列表(连接器资源 / 连接流资源)。
|
|
26
|
+
|
|
27
|
+
可选:
|
|
28
|
+
--type, -t <1|2> 1=连接器资源(默认) 2=连接流资源;也可写名称:api / flow / 连接器 / 连接流
|
|
29
|
+
--name, -n <关键词> 工具名,模糊查询
|
|
30
|
+
--platform, -p <id|名称> 限定某个连接器应用(等价于传该连接器的 groupId)
|
|
31
|
+
仅 --type 1 有意义;不给则查全部连接器
|
|
32
|
+
--all 打印全部结果(默认只显示前 ${DISPLAY_LIMIT} 条)
|
|
33
|
+
--json 输出接口原始 JSON
|
|
34
|
+
|
|
35
|
+
示例:
|
|
36
|
+
wb mcp sources # 全部连接器资源来源
|
|
37
|
+
wb mcp sources --platform 泛微E10 --name 应收
|
|
38
|
+
wb mcp sources --type 2 # 连接流资源来源`;
|
|
39
|
+
|
|
40
|
+
const CREATE_USAGE = `用法: wb mcp create --source <sourceId|工具名> [选项]
|
|
41
|
+
|
|
42
|
+
把来源封装成 MCP 工具(支持批量)。已创建过(exists=true)的来源会被拦下。
|
|
43
|
+
|
|
44
|
+
必填:
|
|
45
|
+
--source, -s <id|名称> 来源。可给 sourceId,也可给工具名(模糊唯一匹配)。
|
|
46
|
+
可重复,或逗号分隔多个
|
|
47
|
+
|
|
48
|
+
可选:
|
|
49
|
+
--type, -t <1|2> 1=连接器资源(默认) 2=连接流资源
|
|
50
|
+
--platform, -p <id|名称> 限定某个连接器应用(缩小来源检索范围)
|
|
51
|
+
--skip-exists 批量中遇到"已创建过"的自动跳过,只创建其余(默认整体中止)
|
|
52
|
+
--dry-run 只打印将提交的 JSON,不发请求
|
|
53
|
+
--json 输出接口原始 JSON
|
|
54
|
+
|
|
55
|
+
示例:
|
|
56
|
+
wb mcp create --source 2034441472166580229 --dry-run
|
|
57
|
+
wb mcp create --platform E10 --source "E10-取得应收单查询列表资料"
|
|
58
|
+
wb mcp create --type 2 --source 数组套数组出来
|
|
59
|
+
wb mcp create --source a,b,c --skip-exists`;
|
|
60
|
+
|
|
61
|
+
function parseCommon(argv) {
|
|
62
|
+
const opts = { type: '', name: '', platform: '', all: false, json: false, help: false };
|
|
63
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
64
|
+
const a = argv[i];
|
|
65
|
+
const next = () => {
|
|
66
|
+
const v = argv[i + 1];
|
|
67
|
+
i += 1;
|
|
68
|
+
return v === undefined ? '' : v;
|
|
69
|
+
};
|
|
70
|
+
if (a === '--type' || a === '-t') opts.type = next();
|
|
71
|
+
else if (a === '--name' || a === '-n') opts.name = next();
|
|
72
|
+
else if (a === '--platform' || a === '-p') opts.platform = next();
|
|
73
|
+
// --source 的取值不能被当成位置参数(create 时单独解析,这里只是吃掉)
|
|
74
|
+
else if (a === '--source' || a === '-s') next();
|
|
75
|
+
else if (a === '--all') opts.all = true;
|
|
76
|
+
else if (a === '--json') opts.json = true;
|
|
77
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
78
|
+
else if (!a.startsWith('-')) {
|
|
79
|
+
if (!opts.name) opts.name = a;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return opts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 解析来源类型:支持数字 1/2,也支持名称 */
|
|
86
|
+
function parseSourceType(raw) {
|
|
87
|
+
const v = String(raw === undefined || raw === null ? '' : raw).trim();
|
|
88
|
+
if (!v) return 1;
|
|
89
|
+
if (/^[12]$/.test(v)) return Number(v);
|
|
90
|
+
|
|
91
|
+
const s = v.toUpperCase();
|
|
92
|
+
if (['API', 'CONNECTOR', '连接器', '连接器资源'].includes(s)) return 1;
|
|
93
|
+
if (['FLOW', 'PROCESS', '连接流', '连接流资源'].includes(s)) return 2;
|
|
94
|
+
|
|
95
|
+
throw new Error(
|
|
96
|
+
`--type "${v}" 不合法:可选 1=连接器资源 / 2=连接流资源(也可写 api / flow / 连接器 / 连接流)`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sourceTypeLabel(n) {
|
|
101
|
+
const v = Number(n);
|
|
102
|
+
return SOURCE_TYPE_NAMES[v] ? `${v} ${SOURCE_TYPE_NAMES[v]}` : String(n);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 查询 MCP 工具来源
|
|
107
|
+
* POST /index/sys/api/v1/mcp/getMcpSourceList
|
|
108
|
+
* 入参 pageNum/pageSize 平台约定写死 1 / 1000
|
|
109
|
+
*/
|
|
110
|
+
async function fetchMcpSources(cfg, cred, opts) {
|
|
111
|
+
const o = Object.assign({ name: '', groupId: null, sourceType: 1 }, opts || {});
|
|
112
|
+
const body = {
|
|
113
|
+
name: String(o.name || ''),
|
|
114
|
+
groupId: o.groupId === undefined ? null : o.groupId,
|
|
115
|
+
sourceType: Number(o.sourceType) || 1,
|
|
116
|
+
pageNum: SOURCE_PAGE_NUM,
|
|
117
|
+
pageSize: SOURCE_PAGE_SIZE
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const res = await authedRequest(cfg, cred, 'POST', '/sys/api/v1/mcp/getMcpSourceList', body);
|
|
121
|
+
const json = res.json;
|
|
122
|
+
|
|
123
|
+
if (!json || typeof json !== 'object') {
|
|
124
|
+
throw new Error(`查询 MCP 来源失败(HTTP ${res.status}):${String(res.text).slice(0, 200)}`);
|
|
125
|
+
}
|
|
126
|
+
if (json.code !== 200) {
|
|
127
|
+
throw new Error(`查询 MCP 来源失败:code=${json.code} msg=${json.msg || '未知错误'}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data = json.data || {};
|
|
131
|
+
const records = Array.isArray(data.records) ? data.records : [];
|
|
132
|
+
return {
|
|
133
|
+
total: typeof data.total === 'number' ? data.total : records.length,
|
|
134
|
+
records,
|
|
135
|
+
raw: json
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 把用户给的 --source 解析成来源记录(纯函数)
|
|
141
|
+
* - 纯数字 → 按 sourceId 精确匹配
|
|
142
|
+
* - 其他 → 按 name 模糊匹配,命中多条不猜
|
|
143
|
+
*
|
|
144
|
+
* @returns {{picked:object[], missing:object[], ambiguous:object[]}}
|
|
145
|
+
*/
|
|
146
|
+
function pickSources(records, inputs) {
|
|
147
|
+
const picked = [];
|
|
148
|
+
const missing = [];
|
|
149
|
+
const ambiguous = [];
|
|
150
|
+
|
|
151
|
+
for (const raw of inputs || []) {
|
|
152
|
+
const key = String(raw === null || raw === undefined ? '' : raw).trim();
|
|
153
|
+
if (!key) continue;
|
|
154
|
+
|
|
155
|
+
let hit = null;
|
|
156
|
+
if (/^\d+$/.test(key)) {
|
|
157
|
+
hit = (records || []).find((r) => String(r.sourceId) === key) || null;
|
|
158
|
+
if (!hit) {
|
|
159
|
+
missing.push({ input: key, reason: 'sourceId 不在来源列表中(可能类型不对,或超出 1000 条)' });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
const matched = (records || []).filter((r) => String(r.name || '').includes(key));
|
|
164
|
+
if (matched.length === 0) {
|
|
165
|
+
missing.push({ input: key, reason: '工具名无匹配' });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (matched.length > 1) {
|
|
169
|
+
ambiguous.push({ input: key, matched });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
hit = matched[0];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!picked.some((p) => String(p.sourceId) === String(hit.sourceId))) picked.push(hit);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { picked, missing, ambiguous };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 统计来源可用情况(纯函数) */
|
|
182
|
+
function summarizeSources(records) {
|
|
183
|
+
const list = records || [];
|
|
184
|
+
const created = list.filter((r) => r.exists === true);
|
|
185
|
+
const creatable = list.filter((r) => r.exists !== true);
|
|
186
|
+
return { total: list.length, created, creatable };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 组装批量创建请求体(纯函数)
|
|
191
|
+
* 四个字段一律**原样透传**来源接口的返回值:
|
|
192
|
+
* - sourceType=1 时 groupId 是字符串,sourceType=2 时是字符串 "0"、groupName 是"连接流"
|
|
193
|
+
* 自己拼 groupId 会错,所以不转换。
|
|
194
|
+
*/
|
|
195
|
+
function buildAddBody(records) {
|
|
196
|
+
return {
|
|
197
|
+
list: (records || []).map((r) => ({
|
|
198
|
+
sourceType: r.sourceType === undefined ? null : r.sourceType,
|
|
199
|
+
sourceId: r.sourceId === undefined ? null : r.sourceId,
|
|
200
|
+
groupId: r.groupId === undefined ? null : r.groupId,
|
|
201
|
+
groupName: r.groupName === undefined ? null : r.groupName
|
|
202
|
+
}))
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 把 --platform 解析成该连接器的 groupId(连接器资源来源检索用) */
|
|
207
|
+
async function resolvePlatformGroupId(cfg, cred, platformInput, out) {
|
|
208
|
+
const target = await resolvePlatformRecord(cfg, cred, platformInput, out);
|
|
209
|
+
const groupId = target.raw && target.raw.groupId !== undefined ? target.raw.groupId : null;
|
|
210
|
+
if (groupId === null || groupId === undefined || groupId === '') {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`连接器 ${target.platformName || target.platformId} 没有返回 groupId,无法按连接器过滤来源。\n` +
|
|
213
|
+
` 可去掉 --platform 查全部连接器的来源。`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
platformId: target.platformId,
|
|
218
|
+
platformName: target.platformName,
|
|
219
|
+
groupId,
|
|
220
|
+
groupName: target.raw && target.raw.groupName ? String(target.raw.groupName) : ''
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** mcp sources */
|
|
225
|
+
async function commandMcpSources(cfg, argv) {
|
|
226
|
+
const opts = parseCommon(argv);
|
|
227
|
+
if (opts.help) {
|
|
228
|
+
process.stdout.write(SOURCES_USAGE + '\n');
|
|
229
|
+
process.exitCode = 0;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const cred = loadCredentials();
|
|
234
|
+
if (!cred || !cred.tokenValue) {
|
|
235
|
+
throw new Error('未登录或授权已失效,请先执行 auth login');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let sourceType = parseSourceType(opts.type);
|
|
239
|
+
let groupId = null;
|
|
240
|
+
let platLabel = '';
|
|
241
|
+
|
|
242
|
+
if (opts.platform) {
|
|
243
|
+
if (sourceType !== 1) {
|
|
244
|
+
process.stderr.write('[warn] --platform 只对连接器资源(--type 1)有效,已忽略\n');
|
|
245
|
+
} else {
|
|
246
|
+
const p = await resolvePlatformGroupId(cfg, cred, opts.platform, process.stdout);
|
|
247
|
+
groupId = p.groupId;
|
|
248
|
+
platLabel = p.platformName || p.platformId;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const { total, records, raw } = await fetchMcpSources(cfg, cred, {
|
|
253
|
+
name: opts.name,
|
|
254
|
+
groupId,
|
|
255
|
+
sourceType
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (opts.json) {
|
|
259
|
+
process.stdout.write(JSON.stringify(raw, null, 2) + '\n');
|
|
260
|
+
process.exitCode = 0;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
process.stdout.write(`来源类型: ${sourceTypeLabel(sourceType)}\n`);
|
|
265
|
+
process.stdout.write(`连接器 : ${platLabel ? `${platLabel}(groupId=${groupId})` : '(全部)'}\n`);
|
|
266
|
+
process.stdout.write(`查询条件: name=${opts.name ? `"${opts.name}"` : '(全部)'} pageNum=${SOURCE_PAGE_NUM} pageSize=${SOURCE_PAGE_SIZE}\n`);
|
|
267
|
+
|
|
268
|
+
const sum = summarizeSources(records);
|
|
269
|
+
process.stdout.write(`共 ${total} 个来源,本次返回 ${records.length} 个;可创建 ${sum.creatable.length} 个、已创建 ${sum.created.length} 个\n\n`);
|
|
270
|
+
|
|
271
|
+
if (!records.length) {
|
|
272
|
+
process.stdout.write('(无匹配结果)\n');
|
|
273
|
+
if (opts.name) process.stdout.write('换个关键词试试,或去掉 --name\n');
|
|
274
|
+
else process.stdout.write(`当前没有可用的${SOURCE_TYPE_NAMES[sourceType] || ''}来源\n`);
|
|
275
|
+
process.exitCode = 0;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const shown = opts.all ? records : records.slice(0, DISPLAY_LIMIT);
|
|
280
|
+
const rows = shown.map((r, i) => [
|
|
281
|
+
String(i + 1),
|
|
282
|
+
truncate(r.name || '', NAME_MAX_WIDTH),
|
|
283
|
+
String(r.sourceId || ''),
|
|
284
|
+
truncate(r.groupName || '', GROUP_MAX_WIDTH),
|
|
285
|
+
r.exists === true ? '已创建' : '未创建'
|
|
286
|
+
]);
|
|
287
|
+
process.stdout.write(renderTable(['#', 'name', 'sourceId', 'groupName', 'created'], rows) + '\n');
|
|
288
|
+
|
|
289
|
+
if (sum.created.length) {
|
|
290
|
+
process.stdout.write(`\n⚠ 其中 ${sum.created.length} 个已创建过 MCP 工具,不能再创建(created=已创建)\n`);
|
|
291
|
+
}
|
|
292
|
+
process.stdout.write(`\n创建:wb mcp create --type ${sourceType} --source <sourceId>\n`);
|
|
293
|
+
|
|
294
|
+
if (shown.length < records.length) {
|
|
295
|
+
process.stdout.write(
|
|
296
|
+
`\n仅显示前 ${shown.length} 条(共 ${records.length} 条)。` +
|
|
297
|
+
`用 --name / --platform 缩小范围,或加 --all 查看全部(名称列已按 ${NAME_MAX_WIDTH} 列截断,完整值用 --json)\n`
|
|
298
|
+
);
|
|
299
|
+
} else if (total > records.length) {
|
|
300
|
+
process.stdout.write(
|
|
301
|
+
`\n注意:只有 ${total - records.length} 个结果没显示(接口 pageSize 固定 ${SOURCE_PAGE_SIZE})。\n` +
|
|
302
|
+
` 请用 --name 或 --platform 缩小范围后再查。\n`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
process.exitCode = 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** mcp create */
|
|
309
|
+
async function commandMcpCreate(cfg, argv) {
|
|
310
|
+
const opts = parseCommon(argv);
|
|
311
|
+
opts.sources = [];
|
|
312
|
+
opts.skipExists = false;
|
|
313
|
+
opts.dryRun = false;
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
316
|
+
const a = argv[i];
|
|
317
|
+
const next = () => {
|
|
318
|
+
const v = argv[i + 1];
|
|
319
|
+
i += 1;
|
|
320
|
+
return v === undefined ? '' : v;
|
|
321
|
+
};
|
|
322
|
+
if (a === '--source' || a === '-s') {
|
|
323
|
+
for (const part of String(next()).split(',')) {
|
|
324
|
+
const s = part.trim();
|
|
325
|
+
if (s) opts.sources.push(s);
|
|
326
|
+
}
|
|
327
|
+
} else if (a === '--skip-exists') opts.skipExists = true;
|
|
328
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (opts.help) {
|
|
332
|
+
process.stdout.write(CREATE_USAGE + '\n');
|
|
333
|
+
process.exitCode = 0;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const cred = loadCredentials();
|
|
338
|
+
if (!cred || !cred.tokenValue) {
|
|
339
|
+
throw new Error('未登录或授权已失效,请先执行 auth login');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const sourceType = parseSourceType(opts.type);
|
|
343
|
+
|
|
344
|
+
if (!opts.sources.length) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`缺少 --source(来源 sourceId 或工具名)。\n` +
|
|
347
|
+
` 先查可用来源:wb mcp sources --type ${sourceType}\n\n${CREATE_USAGE}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 步骤 1/4:确定连接器(可选,用于缩小来源范围)
|
|
352
|
+
let groupId = null;
|
|
353
|
+
let platLabel = '';
|
|
354
|
+
if (opts.platform && sourceType === 1) {
|
|
355
|
+
const p = await resolvePlatformGroupId(cfg, cred, opts.platform, process.stdout);
|
|
356
|
+
groupId = p.groupId;
|
|
357
|
+
platLabel = `${p.platformName || p.platformId}(groupId=${groupId})`;
|
|
358
|
+
} else if (opts.platform && sourceType === 2) {
|
|
359
|
+
process.stderr.write('[warn] --platform 只对连接器资源(--type 1)有效,已忽略\n');
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write(`[1/4] 来源类型: ${sourceTypeLabel(sourceType)} 连接器: ${platLabel || '(全部)'}\n`);
|
|
362
|
+
|
|
363
|
+
// 步骤 2/4:拉来源列表并定位
|
|
364
|
+
const { total, records } = await fetchMcpSources(cfg, cred, { name: '', groupId, sourceType });
|
|
365
|
+
process.stdout.write(`[2/4] 拉取来源 ${records.length} 个(共 ${total} 个)\n`);
|
|
366
|
+
if (total > records.length) {
|
|
367
|
+
process.stderr.write(
|
|
368
|
+
`[warn] 接口只返回前 ${SOURCE_PAGE_SIZE} 个来源,可能漏掉目标。建议加 --platform 或改用 --name 缩小范围\n`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { picked, missing, ambiguous } = pickSources(records, opts.sources);
|
|
373
|
+
|
|
374
|
+
if (ambiguous.length) {
|
|
375
|
+
for (const amb of ambiguous) {
|
|
376
|
+
const rows = amb.matched.map((r, i) => [
|
|
377
|
+
String(i + 1),
|
|
378
|
+
String(r.name || ''),
|
|
379
|
+
String(r.sourceId || ''),
|
|
380
|
+
String(r.groupName || ''),
|
|
381
|
+
r.exists === true ? '已创建' : '未创建'
|
|
382
|
+
]);
|
|
383
|
+
process.stdout.write(`名称 "${amb.input}" 命中 ${amb.matched.length} 个来源,请指定其一:\n\n`);
|
|
384
|
+
process.stdout.write(renderTable(['#', 'name', 'sourceId', 'groupName', 'created'], rows) + '\n\n');
|
|
385
|
+
}
|
|
386
|
+
throw new Error('请用 --source <sourceId> 指定具体来源');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (missing.length) {
|
|
390
|
+
const detail = missing.map((m) => ` ${m.input}:${m.reason}`).join('\n');
|
|
391
|
+
throw new Error(
|
|
392
|
+
`以下来源没找到:\n${detail}\n` +
|
|
393
|
+
` 查看可用来源:wb mcp sources --type ${sourceType}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!picked.length) {
|
|
398
|
+
throw new Error(`没有解析出任何可用来源\n\n${CREATE_USAGE}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 步骤 3/4:exists=true 的不能创建
|
|
402
|
+
const created = picked.filter((r) => r.exists === true);
|
|
403
|
+
const toCreate = picked.filter((r) => r.exists !== true);
|
|
404
|
+
|
|
405
|
+
if (created.length) {
|
|
406
|
+
const rows = created.map((r, i) => [
|
|
407
|
+
String(i + 1),
|
|
408
|
+
String(r.name || ''),
|
|
409
|
+
String(r.sourceId || ''),
|
|
410
|
+
String(r.groupName || '')
|
|
411
|
+
]);
|
|
412
|
+
process.stdout.write(`\n以下来源已经创建过 MCP 工具,不能重复创建:\n\n`);
|
|
413
|
+
process.stdout.write(renderTable(['#', 'name', 'sourceId', 'groupName'], rows) + '\n\n');
|
|
414
|
+
|
|
415
|
+
if (!opts.skipExists) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`有 ${created.length} 个来源已创建过,已中止(未提交任何数据)。\n` +
|
|
418
|
+
` 去掉这些 --source 重试,或加 --skip-exists 只创建其余 ${toCreate.length} 个`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
process.stdout.write(`--skip-exists:跳过 ${created.length} 个,只创建其余 ${toCreate.length} 个\n`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!toCreate.length) {
|
|
425
|
+
process.stdout.write('[3/4] 没有需要创建的工具\n');
|
|
426
|
+
process.exitCode = 0;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const body = buildAddBody(toCreate);
|
|
431
|
+
|
|
432
|
+
process.stdout.write(`[3/4] 待创建 ${toCreate.length} 个:\n`);
|
|
433
|
+
for (const r of toCreate) {
|
|
434
|
+
process.stdout.write(
|
|
435
|
+
` ${r.name} sourceId=${r.sourceId} sourceType=${r.sourceType} groupId=${r.groupId} groupName=${r.groupName}\n`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (opts.dryRun) {
|
|
440
|
+
process.stdout.write(`[4/4] --dry-run,未提交。将发送:\n`);
|
|
441
|
+
process.stdout.write(`POST {baseUrl}/index/sys/api/v1/mcp/batch/add\n`);
|
|
442
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
443
|
+
process.exitCode = 0;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 步骤 4/4:批量创建
|
|
448
|
+
const res = await authedRequest(cfg, cred, 'POST', '/sys/api/v1/mcp/batch/add', body);
|
|
449
|
+
const json = res.json;
|
|
450
|
+
|
|
451
|
+
if (!json || typeof json !== 'object') {
|
|
452
|
+
throw new Error(`创建失败:接口返回异常(HTTP ${res.status})${String(res.text).slice(0, 200)}`);
|
|
453
|
+
}
|
|
454
|
+
if (json.code !== 200) {
|
|
455
|
+
if (opts.json) process.stdout.write(JSON.stringify(json, null, 2) + '\n');
|
|
456
|
+
throw new Error(`创建失败:code=${json.code} msg=${json.msg || '未知错误'}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
process.stdout.write(`[4/4] 创建成功,共 ${toCreate.length} 个 MCP 工具\n`);
|
|
460
|
+
for (const r of toCreate) {
|
|
461
|
+
process.stdout.write(` ${r.name}(sourceId=${r.sourceId})\n`);
|
|
462
|
+
}
|
|
463
|
+
if (opts.json) process.stdout.write(JSON.stringify(json, null, 2) + '\n');
|
|
464
|
+
|
|
465
|
+
process.stdout.write(`\n复核:wb mcp sources --type ${sourceType} --name <工具名>\n`);
|
|
466
|
+
process.exitCode = 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = {
|
|
470
|
+
commandMcpSources,
|
|
471
|
+
commandMcpCreate,
|
|
472
|
+
fetchMcpSources,
|
|
473
|
+
resolvePlatformGroupId,
|
|
474
|
+
pickSources,
|
|
475
|
+
summarizeSources,
|
|
476
|
+
buildAddBody,
|
|
477
|
+
parseSourceType,
|
|
478
|
+
sourceTypeLabel,
|
|
479
|
+
parseCommon,
|
|
480
|
+
SOURCE_TYPE_NAMES,
|
|
481
|
+
SOURCE_PAGE_NUM,
|
|
482
|
+
SOURCE_PAGE_SIZE,
|
|
483
|
+
DISPLAY_LIMIT,
|
|
484
|
+
NAME_MAX_WIDTH
|
|
485
|
+
};
|