@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,478 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const { loadCredentials } = require('../config');
|
|
6
|
+
const { authedRequest } = require('../api');
|
|
7
|
+
const { resolvePlatformRecord } = require('./platforms');
|
|
8
|
+
const { publishWithRetry, DEFAULT_MAX_RETRY } = require('./api-publish');
|
|
9
|
+
const {
|
|
10
|
+
buildParamTree,
|
|
11
|
+
buildResultTree,
|
|
12
|
+
buildSaveBody,
|
|
13
|
+
normalizeUrl,
|
|
14
|
+
normalizeLabels,
|
|
15
|
+
parseQueryKeys,
|
|
16
|
+
parseHeaderKeys,
|
|
17
|
+
parseContentType,
|
|
18
|
+
prettyExample,
|
|
19
|
+
pickOutboundApiId,
|
|
20
|
+
renderTree,
|
|
21
|
+
MAX_LABEL_LENGTH
|
|
22
|
+
} = require('../schema');
|
|
23
|
+
|
|
24
|
+
const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
25
|
+
|
|
26
|
+
const USAGE = `用法: wb api create --platform <platformId|名称> --name <api名称> --url </path> [选项]
|
|
27
|
+
|
|
28
|
+
必填:
|
|
29
|
+
--platform, -p <id|名称> 目标连接器应用
|
|
30
|
+
--name, -n <名称> API 资源名称
|
|
31
|
+
--url, -u <path> api 路径,必须以 / 开头,不带 host
|
|
32
|
+
路径参数写成 /a/{orderId}/b,会自动转成 /a/orderId/b 并把 orderId 放进 query 参数
|
|
33
|
+
|
|
34
|
+
可选:
|
|
35
|
+
--method, -m <方法> GET|POST|PUT|DELETE|PATCH,默认 POST
|
|
36
|
+
--body <json|@file> 请求体示例,用于自动生成 requestBody 结构树
|
|
37
|
+
--result <json|@file> 响应体示例,用于自动生成 responseBody 结构树
|
|
38
|
+
--query, -q "a=1&b=2" query 参数(只取参数名)
|
|
39
|
+
--header, -H "e: 2" 请求头(可重复;Content-Type 会被忽略,由 --content-type 表达)
|
|
40
|
+
--content-type <0-6> 0=none 1=json 2=soap+xml 3=xml 4=form 5=multipart 6=octet-stream
|
|
41
|
+
默认 GET 为 0,其余为 1
|
|
42
|
+
--required <a,b.d> 必填字段路径(body 用点号分隔,如 b.d)
|
|
43
|
+
--label "<路径>=<注释>" 字段描述,来自接口文档注释。可重复
|
|
44
|
+
可加作用域前缀:req:/res:/param:/header:,如 "res:data.id=主键"
|
|
45
|
+
不加前缀则同时作用于入参和出参
|
|
46
|
+
--labels <json|@file> 一次性给全部字段注释,结构:
|
|
47
|
+
{"request":{"b.d":"订单号"},"response":{"data.id":"主键"},
|
|
48
|
+
"param":{"pageNo":"页码"},"header":{"x-tenant":"租户"}}
|
|
49
|
+
也可给扁平 {"b.d":"订单号"}(同时作用于入参和出参)
|
|
50
|
+
单条注释不得超过 ${MAX_LABEL_LENGTH} 字符,超了请先自行总结
|
|
51
|
+
--remark, -r <描述> api 描述,默认与名称相同
|
|
52
|
+
--host-id <platformHostId> 手工指定 platformHostId(默认取连接器 hostBOList[0])
|
|
53
|
+
--update <outboundApiId> 更新已有 API 资源而不是新建
|
|
54
|
+
--no-publish 只保存,不走发布
|
|
55
|
+
--max-retry <N> 发布路径冲突时自动改路径重试次数,默认 ${DEFAULT_MAX_RETRY}
|
|
56
|
+
--url-suffix <word> 路径冲突时使用的后缀单词(默认按 api 名称推导)
|
|
57
|
+
--dry-run 只打印将提交的 JSON,不发请求
|
|
58
|
+
--json 输出接口原始 JSON
|
|
59
|
+
|
|
60
|
+
示例:
|
|
61
|
+
wb api create --platform 泛微E10 --name 查询订单 --url /order/query \\
|
|
62
|
+
--method POST --body @body.json --result @result.json --labels @labels.json
|
|
63
|
+
|
|
64
|
+
wb api create --platform 泛微E10 --name getUser --url "/user/{userId}" \\
|
|
65
|
+
--method GET --query "withDetail=1" --dry-run`;
|
|
66
|
+
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
const opts = {
|
|
69
|
+
platform: '',
|
|
70
|
+
name: '',
|
|
71
|
+
url: '',
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: '',
|
|
74
|
+
result: '',
|
|
75
|
+
query: '',
|
|
76
|
+
headers: [],
|
|
77
|
+
contentType: '',
|
|
78
|
+
required: '',
|
|
79
|
+
labels: '',
|
|
80
|
+
labelList: [],
|
|
81
|
+
labelSpecs: [],
|
|
82
|
+
remark: '',
|
|
83
|
+
hostId: '',
|
|
84
|
+
update: '',
|
|
85
|
+
publish: true,
|
|
86
|
+
maxRetry: DEFAULT_MAX_RETRY,
|
|
87
|
+
urlSuffix: '',
|
|
88
|
+
dryRun: false,
|
|
89
|
+
json: false
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
93
|
+
const a = argv[i];
|
|
94
|
+
const next = () => {
|
|
95
|
+
const v = argv[i + 1];
|
|
96
|
+
i += 1;
|
|
97
|
+
return v === undefined ? '' : v;
|
|
98
|
+
};
|
|
99
|
+
if (a === '--platform' || a === '-p') opts.platform = next();
|
|
100
|
+
else if (a === '--name' || a === '-n') opts.name = next();
|
|
101
|
+
else if (a === '--url' || a === '-u') opts.url = next();
|
|
102
|
+
else if (a === '--method' || a === '-m') opts.method = next();
|
|
103
|
+
else if (a === '--body') opts.body = next();
|
|
104
|
+
else if (a === '--result') opts.result = next();
|
|
105
|
+
else if (a === '--query' || a === '-q') opts.query = next();
|
|
106
|
+
else if (a === '--header' || a === '-H') opts.headers.push(next());
|
|
107
|
+
else if (a === '--content-type') opts.contentType = next();
|
|
108
|
+
else if (a === '--required') opts.required = next();
|
|
109
|
+
else if (a === '--label') opts.labelSpecs.push(next());
|
|
110
|
+
else if (a === '--labels') opts.labels = next();
|
|
111
|
+
else if (a === '--remark' || a === '-r') opts.remark = next();
|
|
112
|
+
else if (a === '--host-id') opts.hostId = next();
|
|
113
|
+
else if (a === '--update') opts.update = next();
|
|
114
|
+
else if (a === '--no-publish') opts.publish = false;
|
|
115
|
+
else if (a === '--max-retry') {
|
|
116
|
+
const v = Number(next());
|
|
117
|
+
opts.maxRetry = Number.isFinite(v) && v > 0 ? Math.min(Math.floor(v), 10) : DEFAULT_MAX_RETRY;
|
|
118
|
+
} else if (a === '--url-suffix') opts.urlSuffix = next();
|
|
119
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
120
|
+
else if (a === '--json') opts.json = true;
|
|
121
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
122
|
+
else if (!a.startsWith('-')) {
|
|
123
|
+
if (!opts.platform) opts.platform = a;
|
|
124
|
+
else if (!opts.name) opts.name = a;
|
|
125
|
+
else if (!opts.url) opts.url = a;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return opts;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 读取"JSON 或文件"入参:支持 @file 前缀、裸文件路径、内联 JSON。
|
|
133
|
+
* @returns {{value:any, source:string}|null}
|
|
134
|
+
*/
|
|
135
|
+
function readJsonSource(raw, label) {
|
|
136
|
+
const v = String(raw == null ? '' : raw).trim();
|
|
137
|
+
if (!v) return null;
|
|
138
|
+
|
|
139
|
+
let filePath = '';
|
|
140
|
+
if (v.startsWith('@')) filePath = v.slice(1).trim();
|
|
141
|
+
else if (fs.existsSync(v) && fs.statSync(v).isFile()) filePath = v;
|
|
142
|
+
|
|
143
|
+
if (filePath) {
|
|
144
|
+
if (!fs.existsSync(filePath)) throw new Error(`${label} 文件不存在:${filePath}`);
|
|
145
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
146
|
+
try {
|
|
147
|
+
return { value: JSON.parse(text), source: filePath };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new Error(`${label} 文件不是合法 JSON(${filePath}):${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
return { value: JSON.parse(v), source: 'inline' };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`${label} 既不是合法 JSON 也不是可读文件:${err.message}\n` +
|
|
158
|
+
` 内联示例请用单引号包裹:--body '{"a":1}';文件请写 --body @body.json`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeMethod(raw) {
|
|
164
|
+
const m = String(raw == null ? '' : raw).trim().toUpperCase();
|
|
165
|
+
if (!m) return 'POST';
|
|
166
|
+
if (!METHODS.includes(m)) {
|
|
167
|
+
throw new Error(`--method "${raw}" 不合法:可选 ${METHODS.join(' / ')}`);
|
|
168
|
+
}
|
|
169
|
+
return m;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** --label 的作用域前缀别名 */
|
|
173
|
+
const LABEL_SCOPE_ALIASES = {
|
|
174
|
+
req: 'request',
|
|
175
|
+
request: 'request',
|
|
176
|
+
body: 'request',
|
|
177
|
+
res: 'response',
|
|
178
|
+
response: 'response',
|
|
179
|
+
result: 'response',
|
|
180
|
+
param: 'param',
|
|
181
|
+
query: 'param',
|
|
182
|
+
path: 'param',
|
|
183
|
+
header: 'header'
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const ALL_SCOPES = ['request', 'response', 'param', 'header'];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 汇总字段注释(labelName)来源,合成一个分域对象交给 schema.normalizeLabels。
|
|
190
|
+
*
|
|
191
|
+
* 优先级:--label 覆盖 --labels(更精确的写在后面)。
|
|
192
|
+
* 注释本该来自接口文档,CLI 不做任何推导,没给就留空。
|
|
193
|
+
*/
|
|
194
|
+
function buildLabelInput(opts) {
|
|
195
|
+
const merged = { request: {}, response: {}, param: {}, header: {} };
|
|
196
|
+
|
|
197
|
+
const push = (scope, key, text) => {
|
|
198
|
+
const k = String(key == null ? '' : key).trim();
|
|
199
|
+
if (!k) return;
|
|
200
|
+
merged[scope][k] = String(text == null ? '' : text);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// 1) --labels <json|@file>
|
|
204
|
+
const src = readJsonSource(opts.labels, '--labels 字段注释');
|
|
205
|
+
if (src) {
|
|
206
|
+
const v = src.value;
|
|
207
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'--labels 需要一个 JSON 对象,形如 {"request":{"b.d":"订单号"},"response":{"data.id":"主键"}},' +
|
|
210
|
+
'或扁平形式 {"b.d":"订单号"}'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const keys = Object.keys(v);
|
|
214
|
+
if (keys.some((k) => ALL_SCOPES.includes(k))) {
|
|
215
|
+
for (const scope of ALL_SCOPES) {
|
|
216
|
+
const part = v[scope];
|
|
217
|
+
if (!part || typeof part !== 'object' || Array.isArray(part)) continue;
|
|
218
|
+
for (const [k, t] of Object.entries(part)) push(scope, k, t);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
for (const [k, t] of Object.entries(v)) {
|
|
222
|
+
for (const scope of ALL_SCOPES) push(scope, k, t);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2) --label "path=注释" / "res:path=注释"
|
|
228
|
+
for (const spec of opts.labelSpecs || []) {
|
|
229
|
+
const raw = String(spec == null ? '' : spec);
|
|
230
|
+
const eq = raw.indexOf('=');
|
|
231
|
+
if (eq <= 0) {
|
|
232
|
+
throw new Error(`--label "${raw}" 格式不对:应为 "<字段路径>=<注释>",如 "b.d=订单号"`);
|
|
233
|
+
}
|
|
234
|
+
let left = raw.slice(0, eq).trim();
|
|
235
|
+
const text = raw.slice(eq + 1).trim();
|
|
236
|
+
|
|
237
|
+
let scope = null;
|
|
238
|
+
const colon = left.indexOf(':');
|
|
239
|
+
if (colon > 0) {
|
|
240
|
+
const prefix = left.slice(0, colon).trim().toLowerCase();
|
|
241
|
+
if (LABEL_SCOPE_ALIASES[prefix]) {
|
|
242
|
+
scope = LABEL_SCOPE_ALIASES[prefix];
|
|
243
|
+
left = left.slice(colon + 1).trim();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (scope) push(scope, left, text);
|
|
248
|
+
else for (const s of ALL_SCOPES) push(s, left, text);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return merged;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 收集并校验入参 */
|
|
255
|
+
function collect(opts) {
|
|
256
|
+
const missing = [];
|
|
257
|
+
if (!String(opts.platform).trim()) missing.push('--platform 目标连接器(id 或名称)');
|
|
258
|
+
if (!String(opts.name).trim()) missing.push('--name API 资源名称');
|
|
259
|
+
if (!String(opts.url).trim()) missing.push('--url api 路径,必须以 / 开头');
|
|
260
|
+
if (missing.length) {
|
|
261
|
+
throw new Error(`缺少必填入参:\n ${missing.join('\n ')}\n\n${USAGE}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const apiName = String(opts.name).trim();
|
|
265
|
+
const method = normalizeMethod(opts.method);
|
|
266
|
+
const urlInfo = normalizeUrl(opts.url);
|
|
267
|
+
|
|
268
|
+
const bodySrc = readJsonSource(opts.body, '--body 请求体示例');
|
|
269
|
+
const resultSrc = readJsonSource(opts.result, '--result 响应体示例');
|
|
270
|
+
|
|
271
|
+
// 字段注释只来自 --label / --labels(接口文档),CLI 不做任何推导
|
|
272
|
+
const labelInfo = normalizeLabels(buildLabelInput(opts));
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
apiName,
|
|
276
|
+
method,
|
|
277
|
+
url: urlInfo.url,
|
|
278
|
+
pathParams: urlInfo.pathParams,
|
|
279
|
+
bodyJson: bodySrc ? bodySrc.value : null,
|
|
280
|
+
bodySource: bodySrc ? bodySrc.source : '',
|
|
281
|
+
resultJson: resultSrc ? resultSrc.value : null,
|
|
282
|
+
resultSource: resultSrc ? resultSrc.source : '',
|
|
283
|
+
queryParams: parseQueryKeys(opts.query),
|
|
284
|
+
headerParams: parseHeaderKeys(opts.headers),
|
|
285
|
+
contentType: parseContentType(opts.contentType, method),
|
|
286
|
+
required: String(opts.required || '')
|
|
287
|
+
.split(',')
|
|
288
|
+
.map((s) => s.trim())
|
|
289
|
+
.filter(Boolean),
|
|
290
|
+
labels: labelInfo.labels,
|
|
291
|
+
labelClipped: labelInfo.clipped,
|
|
292
|
+
remark: String(opts.remark || '').trim() || apiName,
|
|
293
|
+
updateId: String(opts.update || '').trim()
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 组装保存请求体(纯函数部分都在 schema 里) */
|
|
298
|
+
function assemble(v, platform) {
|
|
299
|
+
const paramList = buildParamTree({
|
|
300
|
+
queryParams: v.queryParams,
|
|
301
|
+
pathParams: v.pathParams,
|
|
302
|
+
headerParams: v.headerParams,
|
|
303
|
+
bodyJson: v.bodyJson,
|
|
304
|
+
required: v.required,
|
|
305
|
+
labels: v.labels
|
|
306
|
+
});
|
|
307
|
+
const resultList = buildResultTree(v.resultJson, { labels: v.labels, required: v.required });
|
|
308
|
+
|
|
309
|
+
return buildSaveBody({
|
|
310
|
+
platformId: platform.platformId,
|
|
311
|
+
platformHostId: platform.platformHostId,
|
|
312
|
+
outboundApiId: v.updateId,
|
|
313
|
+
apiName: v.apiName,
|
|
314
|
+
url: v.url,
|
|
315
|
+
httpMethod: v.method,
|
|
316
|
+
remark: v.remark,
|
|
317
|
+
contentType: v.contentType,
|
|
318
|
+
outboundApiParamList: paramList,
|
|
319
|
+
outboundApiResultList: resultList,
|
|
320
|
+
paramExample: prettyExample(v.bodyJson),
|
|
321
|
+
resultExample: prettyExample(v.resultJson)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 把结构树打成人能看的形式 */
|
|
326
|
+
function printTree(title, nodes) {
|
|
327
|
+
process.stdout.write(` ${title}:\n`);
|
|
328
|
+
for (const line of renderTree(nodes, 3)) process.stdout.write(` ${line}\n`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 创建并发布 API 资源
|
|
333
|
+
* 编排:确认连接器 → 组装结构树 → 保存 → 发布准备 → 发布(含路径冲突重试)
|
|
334
|
+
*/
|
|
335
|
+
async function commandApiCreate(cfg, argv) {
|
|
336
|
+
const opts = parseArgs(argv);
|
|
337
|
+
if (opts.help) {
|
|
338
|
+
process.stdout.write(USAGE + '\n');
|
|
339
|
+
process.exitCode = 0;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const cred = loadCredentials();
|
|
344
|
+
if (!cred || !cred.tokenValue) {
|
|
345
|
+
throw new Error('未登录或授权已失效,请先执行 auth login');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 步骤 1/4:确认连接器应用
|
|
349
|
+
const platform = await resolvePlatformRecord(cfg, cred, opts.platform, process.stdout);
|
|
350
|
+
if (opts.hostId) platform.platformHostId = String(opts.hostId).trim();
|
|
351
|
+
|
|
352
|
+
process.stdout.write(`[1/4] 连接器: ${platform.platformName || '(未知名称)'}(${platform.platformId})\n`);
|
|
353
|
+
if (platform.host) process.stdout.write(` host = ${platform.host}\n`);
|
|
354
|
+
if (!platform.platformHostId) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`没有拿到 platformHostId(连接器 ${platform.platformId} 的 hostBOList 为空或未命中)。\n` +
|
|
357
|
+
` 请用 --host-id <platformHostId> 手工指定,或在平台上先为该连接器配置 host。`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
process.stdout.write(` platformHostId = ${platform.platformHostId}\n`);
|
|
361
|
+
|
|
362
|
+
// 步骤 2/4:组装结构树
|
|
363
|
+
const v = collect(opts);
|
|
364
|
+
const body = assemble(v, platform);
|
|
365
|
+
|
|
366
|
+
process.stdout.write(`[2/4] 结构组装完成\n`);
|
|
367
|
+
process.stdout.write(` apiName = ${v.apiName}\n`);
|
|
368
|
+
process.stdout.write(` url = ${v.url}${v.pathParams.length ? `(路径参数: ${v.pathParams.join(', ')})` : ''}\n`);
|
|
369
|
+
process.stdout.write(` method = ${v.method} contentType = ${v.contentType}\n`);
|
|
370
|
+
process.stdout.write(` remark = ${v.remark}\n`);
|
|
371
|
+
process.stdout.write(` query = ${v.queryParams.length ? v.queryParams.join(', ') : '(无)'}\n`);
|
|
372
|
+
process.stdout.write(` header = ${v.headerParams.length ? v.headerParams.join(', ') : '(无)'}\n`);
|
|
373
|
+
|
|
374
|
+
// 字段注释统计:注释应来自接口文档,没给就留空(平台侧后续做 MCP 工具时才有可读的入参说明)
|
|
375
|
+
const labelCount = ALL_SCOPES.reduce((n, s) => n + v.labels[s].size, 0);
|
|
376
|
+
process.stdout.write(
|
|
377
|
+
` labels = ${labelCount ? `${labelCount} 条字段注释` : '(无,全部留空;建议用 --labels 按接口文档补上)'}\n`
|
|
378
|
+
);
|
|
379
|
+
if (v.labelClipped.length) {
|
|
380
|
+
const uniq = [];
|
|
381
|
+
for (const c of v.labelClipped) {
|
|
382
|
+
if (!uniq.some((u) => u.path === c.path && u.length === c.length)) uniq.push(c);
|
|
383
|
+
}
|
|
384
|
+
process.stderr.write(
|
|
385
|
+
`[warn] 以下字段注释超过 ${MAX_LABEL_LENGTH} 字符,已截断:\n` +
|
|
386
|
+
uniq.map((c) => ` ${c.scope}:${c.path}(${c.length} 字)`).join('\n') +
|
|
387
|
+
`\n 请先自行总结到 ${MAX_LABEL_LENGTH} 字以内再提交\n`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
printTree('入参结构', body.outboundApiParamList);
|
|
392
|
+
printTree('出参结构', body.outboundApiResultList);
|
|
393
|
+
|
|
394
|
+
if (opts.dryRun) {
|
|
395
|
+
process.stdout.write(`[3/4] --dry-run,未提交。将发送:\n`);
|
|
396
|
+
process.stdout.write(`POST {baseUrl}/index/sys/api/v1/platforms/${platform.platformId}/papis/save\n`);
|
|
397
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
398
|
+
process.stdout.write(`\n(发布阶段还会调用 publish-record + publish,此处不展示)\n`);
|
|
399
|
+
process.exitCode = 0;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 步骤 3/4:保存
|
|
404
|
+
const saveRes = await authedRequest(
|
|
405
|
+
cfg,
|
|
406
|
+
cred,
|
|
407
|
+
'POST',
|
|
408
|
+
`/sys/api/v1/platforms/${encodeURIComponent(platform.platformId)}/papis/save`,
|
|
409
|
+
body
|
|
410
|
+
);
|
|
411
|
+
const saveJson = saveRes.json;
|
|
412
|
+
if (!saveJson || typeof saveJson !== 'object') {
|
|
413
|
+
throw new Error(`保存失败:接口返回异常(HTTP ${saveRes.status})${String(saveRes.text).slice(0, 200)}`);
|
|
414
|
+
}
|
|
415
|
+
if (saveJson.code !== 200) {
|
|
416
|
+
if (opts.json) process.stdout.write(JSON.stringify(saveJson, null, 2) + '\n');
|
|
417
|
+
throw new Error(`保存失败:code=${saveJson.code} msg=${saveJson.msg || '未知错误'}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const outboundApiId = pickOutboundApiId(saveJson.data);
|
|
421
|
+
if (!outboundApiId) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`保存接口未返回 outboundApiId:${JSON.stringify(saveJson.data)}\n` +
|
|
424
|
+
` 无法继续发布,请到平台页面确认该 API 资源是否已生成。`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
process.stdout.write(`[3/4] 保存成功 outboundApiId = ${outboundApiId}\n`);
|
|
428
|
+
|
|
429
|
+
if (!opts.publish) {
|
|
430
|
+
process.stdout.write(`[4/4] --no-publish,跳过发布\n`);
|
|
431
|
+
process.stdout.write(`\n后续发布:wb api publish --platform ${platform.platformId} --api ${outboundApiId}\n`);
|
|
432
|
+
if (opts.json) process.stdout.write(JSON.stringify(saveJson, null, 2) + '\n');
|
|
433
|
+
process.exitCode = 0;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 步骤 4/4:发布(含路径冲突重试)
|
|
438
|
+
const result = await publishWithRetry(
|
|
439
|
+
cfg,
|
|
440
|
+
cred,
|
|
441
|
+
{ platformId: platform.platformId, outboundApiId, apiName: v.apiName },
|
|
442
|
+
{
|
|
443
|
+
maxRetry: opts.maxRetry,
|
|
444
|
+
urlSuffix: opts.urlSuffix,
|
|
445
|
+
log: (s) => process.stdout.write(' ' + s + '\n')
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
process.stdout.write(`[4/4] 发布成功\n`);
|
|
450
|
+
process.stdout.write(` outboundApiId = ${outboundApiId}\n`);
|
|
451
|
+
process.stdout.write(` 发布路径 = ${result.url}\n`);
|
|
452
|
+
if (result.flowId) process.stdout.write(` flowId = ${result.flowId}\n`);
|
|
453
|
+
if (result.attempts.length > 1) {
|
|
454
|
+
process.stdout.write(` 共尝试 ${result.attempts.length} 次(前 ${result.attempts.length - 1} 次路径被占用)\n`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (opts.json) {
|
|
458
|
+
process.stdout.write(
|
|
459
|
+
JSON.stringify({ save: saveJson, publishRecord: result.prep.raw, publish: result.raw }, null, 2) + '\n'
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
process.stdout.write(`\n复核: wb api list --platform ${platform.platformId} --name ${v.apiName}\n`);
|
|
464
|
+
process.exitCode = 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
module.exports = {
|
|
468
|
+
commandApiCreate,
|
|
469
|
+
assemble,
|
|
470
|
+
collect,
|
|
471
|
+
parseArgs,
|
|
472
|
+
readJsonSource,
|
|
473
|
+
normalizeMethod,
|
|
474
|
+
buildLabelInput,
|
|
475
|
+
METHODS,
|
|
476
|
+
LABEL_SCOPE_ALIASES,
|
|
477
|
+
ALL_SCOPES
|
|
478
|
+
};
|