@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.
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+
3
+ const { loadCredentials } = require('../config');
4
+ const { authedRequest } = require('../api');
5
+ const { fetchPlatforms, MAX_PAGE_SIZE } = require('./platforms');
6
+ const { renderTable } = require('../ui');
7
+
8
+ // 默认每页 10 条:API 资源可能很多,默认全量既慢又难读
9
+ const DEFAULT_PAGE_SIZE = 10;
10
+
11
+ // API 资源状态(平台权威定义)
12
+ const STATUS_NAMES = {
13
+ 10: '草稿',
14
+ 20: '待发布',
15
+ 30: '启用中',
16
+ 40: '停用中',
17
+ 50: '异常'
18
+ };
19
+
20
+ // 内容类型(平台权威定义)
21
+ const CONTENT_TYPE_NAMES = {
22
+ 0: 'none',
23
+ 1: 'json',
24
+ 2: 'soap+xml',
25
+ 3: 'xml',
26
+ 4: 'form',
27
+ 5: 'multipart',
28
+ 6: 'octet-stream'
29
+ };
30
+
31
+ function statusLabel(raw) {
32
+ const n = Number(raw);
33
+ return STATUS_NAMES[n] ? `${n} ${STATUS_NAMES[n]}` : String(raw);
34
+ }
35
+
36
+ function contentTypeLabel(raw) {
37
+ const n = Number(raw);
38
+ return CONTENT_TYPE_NAMES[n] ? `${n} ${CONTENT_TYPE_NAMES[n]}` : String(raw);
39
+ }
40
+
41
+ const USAGE = `用法: wb api list --platform <platformId|连接器名称> [--name <关键词>] [选项]
42
+
43
+ 必填:
44
+ --platform, -p <id|名称> 目标连接器应用。可给 platformId,也可给名称(自动模糊查后唯一定位)
45
+
46
+ 可选:
47
+ --name, -n <关键词> API 资源名称,模糊查询(结果多时强烈建议带上)
48
+ --page <N> 页码,默认 1
49
+ --size <N> 每页条数,默认 ${DEFAULT_PAGE_SIZE},上限 ${MAX_PAGE_SIZE}
50
+ --all 取全部(等价 --size ${MAX_PAGE_SIZE},不推荐,可能很慢)
51
+ --json 输出接口原始 JSON
52
+
53
+ 示例:
54
+ wb api list --platform 泛微E10 --name 用户
55
+ wb api list --platform 2034441470421749761 --all
56
+ wb api list # 不带 --platform 会列出所有连接器供选择`;
57
+
58
+ function parseArgs(argv) {
59
+ const opts = { platform: '', name: '', page: 1, size: DEFAULT_PAGE_SIZE, json: false };
60
+
61
+ for (let i = 0; i < argv.length; i += 1) {
62
+ const a = argv[i];
63
+ const next = () => {
64
+ const v = argv[i + 1];
65
+ i += 1;
66
+ return v === undefined ? '' : v;
67
+ };
68
+ if (a === '--platform' || a === '-p') opts.platform = next();
69
+ else if (a === '--name' || a === '-n') opts.name = next();
70
+ else if (a === '--page') {
71
+ const v = Number(next());
72
+ opts.page = Number.isFinite(v) && v > 0 ? v : 1;
73
+ } else if (a === '--size') {
74
+ const v = Number(next());
75
+ opts.size = Number.isFinite(v) && v > 0 ? Math.min(v, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
76
+ } else if (a === '--all') {
77
+ opts.size = MAX_PAGE_SIZE;
78
+ } else if (a === '--json') {
79
+ opts.json = true;
80
+ } else if (a === '--help' || a === '-h') {
81
+ opts.help = true;
82
+ } else if (!a.startsWith('-')) {
83
+ if (!opts.platform) opts.platform = a;
84
+ else if (!opts.name) opts.name = a;
85
+ }
86
+ }
87
+ return opts;
88
+ }
89
+
90
+ /** 把「platformId 或连接器名称」解析成确定的 platformId */
91
+ async function resolvePlatform(cfg, cred, input, out) {
92
+ const raw = String(input || '').trim();
93
+
94
+ // 纯数字视为 platformId,直接用,避免多打一次接口
95
+ if (/^\d+$/.test(raw)) return { platformId: raw, platformName: '' };
96
+
97
+ const { total, records } = await fetchPlatforms(cfg, cred, { name: raw, size: MAX_PAGE_SIZE });
98
+
99
+ if (total === 1) {
100
+ return { platformId: String(records[0].platformId), platformName: String(records[0].platformName || '') };
101
+ }
102
+
103
+ if (total === 0) {
104
+ throw new Error(
105
+ `没有找到名称包含 "${raw}" 的连接器应用。\n 查看全部:wb platforms`
106
+ );
107
+ }
108
+
109
+ // 命中多个:列出来让用户选,绝不猜
110
+ const rows = records.map((r, i) => [
111
+ String(i + 1),
112
+ String(r.platformName || ''),
113
+ String(r.platformId || ''),
114
+ String(r.platformType),
115
+ String(r.enabled)
116
+ ]);
117
+ out.write(`名称 "${raw}" 命中 ${total} 个连接器,请指定其中一个:\n\n`);
118
+ out.write(renderTable(['#', 'platformName', 'platformId', 'type', 'enabled'], rows) + '\n\n');
119
+ throw new Error('请用 --platform <platformId> 指定具体连接器(或给更精确的名称)');
120
+ }
121
+
122
+ /** 未指定 --platform 时,列出全部连接器供用户选择 */
123
+ async function printPlatformChoices(cfg, cred, out) {
124
+ const { total, records } = await fetchPlatforms(cfg, cred, { size: MAX_PAGE_SIZE });
125
+
126
+ out.write(`未指定连接器应用。系统现有 ${total} 个,请先选一个:\n\n`);
127
+ if (!records.length) {
128
+ out.write('(系统里还没有连接器,先用 platform create 创建一个)\n');
129
+ return;
130
+ }
131
+ const rows = records.map((r, i) => [
132
+ String(i + 1),
133
+ String(r.platformName || ''),
134
+ String(r.platformId || ''),
135
+ String(r.platformType),
136
+ String(r.enabled)
137
+ ]);
138
+ out.write(renderTable(['#', 'platformName', 'platformId', 'type', 'enabled'], rows) + '\n');
139
+ out.write(`\n再执行:wb api list --platform <platformId 或 名称> [--name <关键词>]\n`);
140
+ }
141
+
142
+ /**
143
+ * 查询某个连接器应用下的 API 资源列表
144
+ * 接口:GET /index/sys/api/v1/platforms/{platformId}/papis
145
+ * ?apiName=&platformId={platformId}&pageNum=1&pageSize=10
146
+ */
147
+ async function commandApiList(cfg, argv) {
148
+ const opts = parseArgs(argv);
149
+ if (opts.help) {
150
+ process.stdout.write(USAGE + '\n');
151
+ process.exitCode = 0;
152
+ return;
153
+ }
154
+
155
+ const cred = loadCredentials();
156
+ if (!cred || !cred.tokenValue) {
157
+ throw new Error('未登录或授权已失效,请先执行 auth login');
158
+ }
159
+
160
+ if (!opts.platform) {
161
+ await printPlatformChoices(cfg, cred, process.stdout);
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ const target = await resolvePlatform(cfg, cred, opts.platform, process.stdout);
167
+ const pid = target.platformId;
168
+
169
+ const qs =
170
+ `/sys/api/v1/platforms/${encodeURIComponent(pid)}/papis` +
171
+ `?apiName=${encodeURIComponent(opts.name || '')}&platformId=${encodeURIComponent(pid)}` +
172
+ `&pageNum=${opts.page}&pageSize=${opts.size}`;
173
+
174
+ const res = await authedRequest(cfg, cred, 'GET', qs);
175
+ const json = res.json;
176
+
177
+ if (opts.json) {
178
+ process.stdout.write(JSON.stringify(json, null, 2) + '\n');
179
+ process.exitCode = json && json.code === 200 ? 0 : 1;
180
+ return;
181
+ }
182
+
183
+ if (!json || typeof json !== 'object') {
184
+ throw new Error(`接口返回异常(HTTP ${res.status}):${String(res.text).slice(0, 200)}`);
185
+ }
186
+ if (json.code !== 200) {
187
+ process.stdout.write(JSON.stringify(json, null, 2) + '\n');
188
+ throw new Error(`查询失败:code=${json.code} msg=${json.msg || '未知错误'}`);
189
+ }
190
+
191
+ const data = json.data || {};
192
+ const records = Array.isArray(data.records) ? data.records : [];
193
+ const total = typeof data.total === 'number' ? data.total : records.length;
194
+ const current = typeof data.current === 'number' ? data.current : opts.page;
195
+ const size = typeof data.size === 'number' ? data.size : opts.size;
196
+
197
+ const platLabel = target.platformName ? `${target.platformName}(${pid})` : pid;
198
+ process.stdout.write(`连接器: ${platLabel}\n`);
199
+ process.stdout.write(
200
+ `查询条件: apiName=${opts.name ? `"${opts.name}"` : '(全部)'} pageNum=${current} pageSize=${size}\n`
201
+ );
202
+ process.stdout.write(`共 ${total} 个 API 资源,本次返回 ${records.length} 条\n\n`);
203
+
204
+ if (!records.length) {
205
+ process.stdout.write('(无匹配结果)\n');
206
+ if (opts.name) process.stdout.write('换个关键词试试,或去掉 --name 查看该连接器下全部 API 资源\n');
207
+ else process.stdout.write('该连接器下还没有 API 资源\n');
208
+ process.exitCode = 0;
209
+ return;
210
+ }
211
+
212
+ const header = ['#', 'apiName', 'outboundApiId', 'method', 'url', 'content', 'status'];
213
+ const rows = records.map((r, i) => [
214
+ String(i + 1),
215
+ String(r.apiName || ''),
216
+ String(r.outboundApiId || ''),
217
+ String(r.httpMethod || ''),
218
+ String(r.url || ''),
219
+ contentTypeLabel(r.contentType),
220
+ statusLabel(r.status)
221
+ ]);
222
+ process.stdout.write(renderTable(header, rows) + '\n');
223
+
224
+ // flowId 是发布后生成的发布 ID,有值即已走过发布流程
225
+ const published = records.filter((r) => r.flowId).length;
226
+ process.stdout.write(`\n本页已发布(有 flowId)${published} 条 / 共 ${records.length} 条;flowId 需 --json 查看\n`);
227
+ process.stdout.write('status: 10 草稿 / 20 待发布 / 30 启用中 / 40 停用中 / 50 异常\n');
228
+
229
+ if (!opts.name && total > records.length) {
230
+ process.stdout.write(
231
+ `\n结果较多(共 ${total} 条,本页 ${records.length} 条):建议加 --name <关键词> 过滤,或 --page 翻页\n`
232
+ );
233
+ }
234
+ process.stdout.write('\n提示:加 --json 可输出完整字段(含 contentType、paramExample、flowId 等)\n');
235
+ process.exitCode = 0;
236
+ }
237
+
238
+ module.exports = {
239
+ commandApiList,
240
+ parseArgs,
241
+ resolvePlatform,
242
+ statusLabel,
243
+ contentTypeLabel,
244
+ STATUS_NAMES,
245
+ CONTENT_TYPE_NAMES,
246
+ DEFAULT_PAGE_SIZE
247
+ };
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ const { loadCredentials } = require('../config');
4
+ const { authedRequest } = require('../api');
5
+ const { MAX_PAGE_SIZE, resolvePlatformRecord } = require('./platforms');
6
+ const { urlForAttempt } = require('../schema');
7
+ const { renderTable } = require('../ui');
8
+
9
+ /** 发布路径被占用的业务错误码(业务方给定) */
10
+ const PATH_CONFLICT_CODE = 40011037;
11
+
12
+ const DEFAULT_MAX_RETRY = 3;
13
+
14
+ const USAGE = `用法: wb api publish --platform <platformId|名称> --api <outboundApiId|api名称> [选项]
15
+
16
+ 说明: 已保存过的 API 资源重新走"发布准备 + 发布"。新资源请用 api create。
17
+
18
+ 可选:
19
+ --max-retry <N> 路径冲突时自动改路径重试次数,默认 ${DEFAULT_MAX_RETRY}
20
+ --url-suffix <word> 路径冲突时使用的后缀单词(默认按 api 名称推导)
21
+ --dry-run 只做发布准备,不真正发布
22
+ --json 输出接口原始 JSON
23
+
24
+ 示例:
25
+ wb api publish --platform 泛微E10 --api 2118594622711623680
26
+ wb api publish --platform 泛微E10 --api zw1 --dry-run`;
27
+
28
+ function parseArgs(argv) {
29
+ const opts = { platform: '', api: '', maxRetry: DEFAULT_MAX_RETRY, urlSuffix: '', dryRun: false, json: false };
30
+ for (let i = 0; i < argv.length; i += 1) {
31
+ const a = argv[i];
32
+ const next = () => {
33
+ const v = argv[i + 1];
34
+ i += 1;
35
+ return v === undefined ? '' : v;
36
+ };
37
+ if (a === '--platform' || a === '-p') opts.platform = next();
38
+ else if (a === '--api' || a === '-a') opts.api = next();
39
+ else if (a === '--max-retry') {
40
+ const v = Number(next());
41
+ opts.maxRetry = Number.isFinite(v) && v > 0 ? Math.min(Math.floor(v), 10) : DEFAULT_MAX_RETRY;
42
+ } else if (a === '--url-suffix') opts.urlSuffix = next();
43
+ else if (a === '--dry-run') opts.dryRun = true;
44
+ else if (a === '--json') opts.json = true;
45
+ else if (a === '--help' || a === '-h') opts.help = true;
46
+ }
47
+ return opts;
48
+ }
49
+
50
+ function papisPath(platformId, outboundApiId, tail) {
51
+ const base =
52
+ `/sys/api/v1/platforms/${encodeURIComponent(platformId)}/papis` +
53
+ (outboundApiId ? `/${encodeURIComponent(outboundApiId)}` : '');
54
+ return tail ? `${base}/${tail}` : base;
55
+ }
56
+
57
+ /**
58
+ * 在连接器下按 outboundApiId 或名称定位一条 API 资源
59
+ * @returns {{outboundApiId:string, apiName:string, url:string, httpMethod:string, status:number}}
60
+ */
61
+ async function findApi(cfg, cred, platformId, input, out) {
62
+ const raw = String(input == null ? '' : input).trim();
63
+ if (!raw) throw new Error('缺少 --api(outboundApiId 或 api 名称)');
64
+
65
+ const qs =
66
+ papisPath(platformId) +
67
+ `?apiName=${encodeURIComponent(/^\d+$/.test(raw) ? '' : raw)}` +
68
+ `&platformId=${encodeURIComponent(platformId)}&pageNum=1&pageSize=${MAX_PAGE_SIZE}`;
69
+
70
+ const res = await authedRequest(cfg, cred, 'GET', qs);
71
+ const json = res.json;
72
+ if (!json || typeof json !== 'object') {
73
+ throw new Error(`查询 API 资源失败(HTTP ${res.status}):${String(res.text).slice(0, 200)}`);
74
+ }
75
+ if (json.code !== 200) {
76
+ throw new Error(`查询 API 资源失败:code=${json.code} msg=${json.msg || '未知错误'}`);
77
+ }
78
+
79
+ const records = Array.isArray((json.data || {}).records) ? json.data.records : [];
80
+ let hit = null;
81
+ if (/^\d+$/.test(raw)) {
82
+ hit = records.find((r) => String(r.outboundApiId) === raw) || null;
83
+ if (!hit) {
84
+ // 列表可能被过滤掉,退化为直接用 id(apiName 拿不到,冲突重试后缀会用兜底词池)
85
+ return { outboundApiId: raw, apiName: '', url: '', httpMethod: '', status: null };
86
+ }
87
+ } else {
88
+ const matched = records.filter((r) => String(r.apiName || '').includes(raw));
89
+ if (matched.length === 1) hit = matched[0];
90
+ else if (matched.length === 0) {
91
+ throw new Error(
92
+ `连接器下没有找到名称包含 "${raw}" 的 API 资源。\n 查看列表:wb api list --platform ${platformId}`
93
+ );
94
+ } else {
95
+ const rows = matched.map((r, i) => [
96
+ String(i + 1),
97
+ String(r.apiName || ''),
98
+ String(r.outboundApiId || ''),
99
+ String(r.httpMethod || ''),
100
+ String(r.url || ''),
101
+ String(r.status)
102
+ ]);
103
+ out.write(`名称 "${raw}" 命中 ${matched.length} 条 API 资源,请指定其中一条:\n\n`);
104
+ out.write(renderTable(['#', 'apiName', 'outboundApiId', 'method', 'url', 'status'], rows) + '\n\n');
105
+ throw new Error('请用 --api <outboundApiId> 指定具体 API 资源');
106
+ }
107
+ }
108
+
109
+ return {
110
+ outboundApiId: String(hit.outboundApiId || ''),
111
+ apiName: String(hit.apiName || ''),
112
+ url: String(hit.url || ''),
113
+ httpMethod: String(hit.httpMethod || ''),
114
+ status: typeof hit.status === 'number' ? hit.status : null
115
+ };
116
+ }
117
+
118
+ /**
119
+ * 发布准备:返回 groupCode / 发布后 url / packageResponse / isPublished
120
+ * POST /index/sys/api/v1/platforms/{platformId}/papis/{outboundApiId}/publish-record
121
+ */
122
+ async function preparePublish(cfg, cred, platformId, outboundApiId) {
123
+ const res = await authedRequest(cfg, cred, 'POST', papisPath(platformId, outboundApiId, 'publish-record'), {
124
+ platformId,
125
+ outboundApiId
126
+ });
127
+ const json = res.json;
128
+ if (!json || typeof json !== 'object') {
129
+ throw new Error(`发布准备失败(HTTP ${res.status}):${String(res.text).slice(0, 200)}`);
130
+ }
131
+ if (json.code !== 200) {
132
+ throw new Error(`发布准备失败:code=${json.code} msg=${json.msg || '未知错误'}`);
133
+ }
134
+ const data = json.data || {};
135
+ return {
136
+ groupCode: String(data.groupCode || ''),
137
+ url: String(data.url || ''),
138
+ packageResponse: data.packageResponse === undefined ? 0 : data.packageResponse,
139
+ isPublished: Boolean(data.isPublished),
140
+ raw: json
141
+ };
142
+ }
143
+
144
+ /** 单次发布 PUT .../publish。overwrite 恒为 false:不覆盖已占用路径,冲突就改路径重试 */
145
+ async function publishOnce(cfg, cred, body) {
146
+ const res = await authedRequest(
147
+ cfg,
148
+ cred,
149
+ 'PUT',
150
+ papisPath(body.platformId, body.outboundApiId, 'publish'),
151
+ body
152
+ );
153
+ return { status: res.status, json: res.json, text: res.text, body };
154
+ }
155
+
156
+ /**
157
+ * 发布(含路径冲突自动重试)
158
+ * 冲突时在「原始发布 url」后追加语义化英文单词重试,不是层层叠加。
159
+ *
160
+ * @param {object} p {platformId, outboundApiId, apiName}
161
+ * @param {object} o {maxRetry, urlSuffix, log}
162
+ */
163
+ async function publishWithRetry(cfg, cred, p, o) {
164
+ const opts = o || {};
165
+ const log = opts.log || (() => {});
166
+ const maxRetry = Math.max(1, Number(opts.maxRetry) || DEFAULT_MAX_RETRY);
167
+
168
+ const prep = await preparePublish(cfg, cred, p.platformId, p.outboundApiId);
169
+ if (!prep.url) {
170
+ throw new Error('发布准备未返回 url,无法继续发布(请先在平台页面确认该 API 资源是否保存成功)');
171
+ }
172
+ log(`发布路径: ${prep.url}`);
173
+ log(`分组 code: ${prep.groupCode || '(空)'} 是否已发布过: ${prep.isPublished ? '是(再次发布)' : '否(首次发布)'}`);
174
+
175
+ const baseUrl = prep.url;
176
+ const attempts = [];
177
+
178
+ for (let attempt = 1; attempt <= maxRetry; attempt += 1) {
179
+ const url = urlForAttempt(baseUrl, p.apiName, attempt, opts.urlSuffix);
180
+ const body = {
181
+ platformId: p.platformId,
182
+ outboundApiId: p.outboundApiId,
183
+ url,
184
+ packageResponse: prep.packageResponse,
185
+ overwrite: false
186
+ };
187
+
188
+ const r = await publishOnce(cfg, cred, body);
189
+ const json = r.json;
190
+
191
+ if (!json || typeof json !== 'object') {
192
+ throw new Error(`发布失败(HTTP ${r.status}):${String(r.text).slice(0, 200)}`);
193
+ }
194
+
195
+ if (json.code === 200) {
196
+ attempts.push({ attempt, url, code: 200, ok: true });
197
+ const data = json.data || {};
198
+ return {
199
+ prep,
200
+ attempts,
201
+ url,
202
+ flowId: String(data.flowId || ''),
203
+ apiId: String(data.apiId || ''),
204
+ raw: json
205
+ };
206
+ }
207
+
208
+ if (json.code === PATH_CONFLICT_CODE) {
209
+ attempts.push({ attempt, url, code: json.code, ok: false, msg: json.msg || '路径已存在,请修改' });
210
+ log(` 第 ${attempt} 次被占用:${url}`);
211
+ continue;
212
+ }
213
+
214
+ attempts.push({ attempt, url, code: json.code, ok: false, msg: json.msg || '' });
215
+ throw new Error(
216
+ `发布失败:code=${json.code} msg=${json.msg || '未知错误'}\n 提交路径: ${url}`
217
+ );
218
+ }
219
+
220
+ const tried = attempts.map((a) => a.url).join('\n ');
221
+ throw new Error(
222
+ `发布失败:连续 ${maxRetry} 次路径冲突(${PATH_CONFLICT_CODE} 路径已存在)。\n` +
223
+ ` 已尝试: ${tried}\n` +
224
+ ` 可用 --url-suffix <单词> 指定后缀,或 --max-retry 加大次数`
225
+ );
226
+ }
227
+
228
+ /** api publish 命令 */
229
+ async function commandApiPublish(cfg, argv) {
230
+ const opts = parseArgs(argv);
231
+ if (opts.help) {
232
+ process.stdout.write(USAGE + '\n');
233
+ process.exitCode = 0;
234
+ return;
235
+ }
236
+
237
+ const cred = loadCredentials();
238
+ if (!cred || !cred.tokenValue) {
239
+ throw new Error('未登录或授权已失效,请先执行 auth login');
240
+ }
241
+ if (!opts.platform) throw new Error(`缺少 --platform\n\n${USAGE}`);
242
+ if (!opts.api) throw new Error(`缺少 --api\n\n${USAGE}`);
243
+
244
+ const target = await resolvePlatformRecord(cfg, cred, opts.platform, process.stdout);
245
+ process.stdout.write(`[1/3] 连接器: ${target.platformName || '(未知名称)'}(${target.platformId})\n`);
246
+
247
+ const api = await findApi(cfg, cred, target.platformId, opts.api, process.stdout);
248
+ process.stdout.write(`[2/3] API 资源: ${api.apiName || '(未知名称)'}(${api.outboundApiId})\n`);
249
+
250
+ if (opts.dryRun) {
251
+ const prep = await preparePublish(cfg, cred, target.platformId, api.outboundApiId);
252
+ process.stdout.write(`[3/3] --dry-run,未发布。发布准备返回:\n`);
253
+ process.stdout.write(JSON.stringify(prep.raw, null, 2) + '\n');
254
+ process.exitCode = 0;
255
+ return;
256
+ }
257
+
258
+ const result = await publishWithRetry(
259
+ cfg,
260
+ cred,
261
+ { platformId: target.platformId, outboundApiId: api.outboundApiId, apiName: api.apiName },
262
+ { maxRetry: opts.maxRetry, urlSuffix: opts.urlSuffix, log: (s) => process.stdout.write(' ' + s + '\n') }
263
+ );
264
+
265
+ if (opts.json) {
266
+ process.stdout.write(JSON.stringify(result.raw, null, 2) + '\n');
267
+ }
268
+
269
+ process.stdout.write(`[3/3] 发布成功\n`);
270
+ process.stdout.write(` 发布路径 = ${result.url}\n`);
271
+ if (result.flowId) process.stdout.write(` flowId = ${result.flowId}\n`);
272
+ if (result.attempts.length > 1) {
273
+ process.stdout.write(` 共尝试 ${result.attempts.length} 次(前 ${result.attempts.length - 1} 次路径被占用)\n`);
274
+ }
275
+ process.exitCode = 0;
276
+ }
277
+
278
+ module.exports = {
279
+ commandApiPublish,
280
+ preparePublish,
281
+ publishOnce,
282
+ publishWithRetry,
283
+ findApi,
284
+ papisPath,
285
+ parseArgs,
286
+ PATH_CONFLICT_CODE,
287
+ DEFAULT_MAX_RETRY
288
+ };