@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,279 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadCredentials } = require('../config');
|
|
4
|
+
const { authedRequest } = require('../api');
|
|
5
|
+
const { fetchGroups } = require('./groups');
|
|
6
|
+
|
|
7
|
+
// 模板中的固定字段,逐字对齐平台前端提交格式
|
|
8
|
+
const FIXED = {
|
|
9
|
+
source: 0,
|
|
10
|
+
businessTypeList: [0],
|
|
11
|
+
digiwinBO: {
|
|
12
|
+
restUrl: '',
|
|
13
|
+
callUrl: '',
|
|
14
|
+
ver: '',
|
|
15
|
+
prod: 'yipaas',
|
|
16
|
+
pid: '',
|
|
17
|
+
uid: '',
|
|
18
|
+
ip: '',
|
|
19
|
+
timezone: null
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_ICON = '30002';
|
|
24
|
+
const DEFAULT_TYPE = 5;
|
|
25
|
+
|
|
26
|
+
// platformType = 系统分类。数字为平台值,名称仅作命令行别名(方便 AI/人直接写 --type ERP)
|
|
27
|
+
const TYPE_NAMES = {
|
|
28
|
+
1: 'OA',
|
|
29
|
+
2: 'CRM',
|
|
30
|
+
3: 'WMS',
|
|
31
|
+
4: 'ERP',
|
|
32
|
+
5: '其他'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 分组 code 实测允许大小写(如 Weaver、TemplateStudio),故不强制小写
|
|
36
|
+
const CODE_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
37
|
+
|
|
38
|
+
const USAGE = `用法: wb platform create --name <名称> --group <分组code> --host <http://ip:port> [选项]
|
|
39
|
+
|
|
40
|
+
必填:
|
|
41
|
+
--name, -n <名称> 应用连接器名称
|
|
42
|
+
--group, -g <code> 分组 code,需全局唯一(先用 groups 查重)
|
|
43
|
+
--host, -H <url> 目标系统地址,形如 http://192.168.0.88:6001
|
|
44
|
+
|
|
45
|
+
可选:
|
|
46
|
+
--remark, -r <描述> 系统描述,默认与名称相同
|
|
47
|
+
--type, -t <分类> 系统分类,默认 ${DEFAULT_TYPE}(其他)
|
|
48
|
+
可填数字 1-5 或名称:1=OA 2=CRM 3=WMS 4=ERP 5=其他
|
|
49
|
+
--icon <id> 图标,默认 ${DEFAULT_ICON}
|
|
50
|
+
--dry-run 只打印将提交的 JSON,不发请求
|
|
51
|
+
--force 跳过分组 code 重复预检(有风险)`;
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const opts = {
|
|
55
|
+
name: '',
|
|
56
|
+
group: '',
|
|
57
|
+
host: '',
|
|
58
|
+
remark: '',
|
|
59
|
+
type: '',
|
|
60
|
+
icon: DEFAULT_ICON,
|
|
61
|
+
dryRun: false,
|
|
62
|
+
force: false
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
const next = () => {
|
|
68
|
+
const v = argv[i + 1];
|
|
69
|
+
i += 1;
|
|
70
|
+
return v === undefined ? '' : v;
|
|
71
|
+
};
|
|
72
|
+
if (a === '--name' || a === '-n') opts.name = next();
|
|
73
|
+
else if (a === '--group' || a === '-g') opts.group = next();
|
|
74
|
+
else if (a === '--host' || a === '-H') opts.host = next();
|
|
75
|
+
else if (a === '--remark' || a === '-r') opts.remark = next();
|
|
76
|
+
else if (a === '--type' || a === '-t') opts.type = next();
|
|
77
|
+
else if (a === '--icon') opts.icon = next();
|
|
78
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
79
|
+
else if (a === '--force') opts.force = true;
|
|
80
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
81
|
+
else if (!a.startsWith('-')) {
|
|
82
|
+
// 位置参数依次当作 name / group / host
|
|
83
|
+
if (!opts.name) opts.name = a;
|
|
84
|
+
else if (!opts.group) opts.group = a;
|
|
85
|
+
else if (!opts.host) opts.host = a;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return opts;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 解析 platformType:支持数字 1-5,也支持名称 OA/CRM/WMS/ERP/其他(大小写不敏感)。
|
|
93
|
+
* 拿不准系统属于哪一类时,按平台约定归到 5(其他)。
|
|
94
|
+
*/
|
|
95
|
+
function parseType(raw) {
|
|
96
|
+
const v = String(raw === undefined || raw === null ? '' : raw).trim();
|
|
97
|
+
if (!v) return DEFAULT_TYPE;
|
|
98
|
+
if (/^[1-5]$/.test(v)) return Number(v);
|
|
99
|
+
|
|
100
|
+
const upper = v.toUpperCase();
|
|
101
|
+
for (const [num, name] of Object.entries(TYPE_NAMES)) {
|
|
102
|
+
if (name.toUpperCase() === upper) return Number(num);
|
|
103
|
+
}
|
|
104
|
+
// 常见写法兜底
|
|
105
|
+
if (['其它', 'OTHER', 'OTHERS', 'UNKNOWN'].includes(upper)) return 5;
|
|
106
|
+
|
|
107
|
+
throw new Error(
|
|
108
|
+
`--type "${v}" 不合法:可选 1=OA / 2=CRM / 3=WMS / 4=ERP / 5=其他(也可直接写名称,或省略则默认 5)`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 第一步:收集并校验入参 */
|
|
113
|
+
function collect(opts) {
|
|
114
|
+
const missing = [];
|
|
115
|
+
if (!String(opts.name).trim()) missing.push('--name 应用连接器名称');
|
|
116
|
+
if (!String(opts.group).trim()) missing.push('--group 分组 code(全局唯一,可用 groups 命令查看现有值)');
|
|
117
|
+
if (!String(opts.host).trim()) missing.push('--host 目标系统地址,如 http://192.168.0.88:6001');
|
|
118
|
+
|
|
119
|
+
if (missing.length) {
|
|
120
|
+
throw new Error(`缺少必填入参:\n ${missing.join('\n ')}\n\n${USAGE}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const name = opts.name.trim();
|
|
124
|
+
const group = opts.group.trim();
|
|
125
|
+
const host = opts.host.trim();
|
|
126
|
+
|
|
127
|
+
if (!CODE_RE.test(group)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`分组 code "${group}" 不合法:只允许字母、数字、下划线、短横线,长度 1–64(实测大小写都可用)`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (!/^https?:\/\/.+/i.test(host)) {
|
|
133
|
+
throw new Error(`地址 "${host}" 不合法:必须以 http:// 或 https:// 开头,并带上端口,如 http://192.168.0.88:6001`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
name,
|
|
138
|
+
group,
|
|
139
|
+
host: host.replace(/\/+$/, ''),
|
|
140
|
+
remark: String(opts.remark || '').trim() || name,
|
|
141
|
+
type: parseType(opts.type),
|
|
142
|
+
icon: opts.icon || DEFAULT_ICON
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 第二步:分组 code 查重(同时比对分组表与已建连接器,防止遗漏) */
|
|
147
|
+
async function checkGroupCode(cfg, cred, group, opts) {
|
|
148
|
+
const { total, records } = await fetchGroups(cfg, cred);
|
|
149
|
+
|
|
150
|
+
// 大小写不同也算冲突:同一 code 仅差大小写会让人混淆,且不排除数据库层面不区分大小写
|
|
151
|
+
const hit = records.find(
|
|
152
|
+
(r) => String(r.groupPrefix || '').toLowerCase() === group.toLowerCase()
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (hit) {
|
|
156
|
+
if (opts.force) {
|
|
157
|
+
process.stderr.write(
|
|
158
|
+
`[warn] 分组 code "${group}" 已存在(${hit.groupName || '未命名'},groupId=${hit.groupId}),因 --force 继续\n`
|
|
159
|
+
);
|
|
160
|
+
return { total, records, conflict: hit, forced: true };
|
|
161
|
+
}
|
|
162
|
+
const similar = records
|
|
163
|
+
.filter((r) => String(r.groupPrefix || '').toLowerCase().includes(group.toLowerCase().slice(0, 2)))
|
|
164
|
+
.slice(0, 5)
|
|
165
|
+
.map((r) => r.groupPrefix);
|
|
166
|
+
throw new Error(
|
|
167
|
+
`分组 code "${group}" 已被占用:${hit.groupName || '未命名'}(groupId=${hit.groupId})\n` +
|
|
168
|
+
` 请换一个 code。${similar.length ? `相近的已有 code: ${similar.join(', ')}` : ''}\n` +
|
|
169
|
+
` 查看全部分组:wb groups`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { total, records, conflict: null, forced: false };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 第三步:拼装请求体(严格对齐平台前端格式) */
|
|
177
|
+
function buildBody(v) {
|
|
178
|
+
return {
|
|
179
|
+
source: FIXED.source,
|
|
180
|
+
platformName: v.name,
|
|
181
|
+
platformType: v.type,
|
|
182
|
+
businessTypeList: FIXED.businessTypeList,
|
|
183
|
+
digiwinBO: FIXED.digiwinBO,
|
|
184
|
+
groupPrefix: v.group,
|
|
185
|
+
remark: v.remark,
|
|
186
|
+
icon: v.icon,
|
|
187
|
+
hostList: [{ host: v.host, sortOrder: 1 }]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 创建成功的返回(已实测确认):
|
|
193
|
+
* {"code":200,"msg":"操作成功","data":"2118536154193555456"}
|
|
194
|
+
* data 就是新建的 platformId 字符串。
|
|
195
|
+
* 仍保留对象形态的兜底,防止平台后续改结构。
|
|
196
|
+
*/
|
|
197
|
+
function pickNewId(data) {
|
|
198
|
+
if (data == null || data === '') return '';
|
|
199
|
+
if (typeof data === 'string' || typeof data === 'number') return String(data);
|
|
200
|
+
if (typeof data === 'object') {
|
|
201
|
+
return String(data.platformId || data.id || (data.record && data.record.platformId) || '');
|
|
202
|
+
}
|
|
203
|
+
return '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 创建应用连接器
|
|
208
|
+
* 编排:收集入参 → 查全部 code 判重 → POST /index/sys/api/v1/platforms
|
|
209
|
+
*/
|
|
210
|
+
async function commandPlatformCreate(cfg, argv) {
|
|
211
|
+
const opts = parseArgs(argv);
|
|
212
|
+
if (opts.help) {
|
|
213
|
+
process.stdout.write(USAGE + '\n');
|
|
214
|
+
process.exitCode = 0;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const cred = loadCredentials();
|
|
219
|
+
if (!cred || !cred.tokenValue) {
|
|
220
|
+
throw new Error('未登录或授权已失效,请先执行 auth login');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 步骤 1/3:入参
|
|
224
|
+
const v = collect(opts);
|
|
225
|
+
process.stdout.write('[1/3] 入参收集完成\n');
|
|
226
|
+
process.stdout.write(` platformName = ${v.name}\n`);
|
|
227
|
+
process.stdout.write(` groupPrefix = ${v.group}\n`);
|
|
228
|
+
process.stdout.write(` platformType = ${v.type}(${TYPE_NAMES[v.type] || '未知'})\n`);
|
|
229
|
+
process.stdout.write(` host = ${v.host}\n`);
|
|
230
|
+
process.stdout.write(` remark = ${v.remark}\n`);
|
|
231
|
+
|
|
232
|
+
// 步骤 2/3:分组 code 查重
|
|
233
|
+
const { total } = await checkGroupCode(cfg, cred, v.group, opts);
|
|
234
|
+
process.stdout.write(`[2/3] 分组 code 检查通过(系统现有 ${total} 个分组)\n`);
|
|
235
|
+
|
|
236
|
+
const body = buildBody(v);
|
|
237
|
+
|
|
238
|
+
if (opts.dryRun) {
|
|
239
|
+
process.stdout.write('[3/3] --dry-run,未提交。将发送:\n');
|
|
240
|
+
process.stdout.write(`POST {baseUrl}/index/sys/api/v1/platforms\n`);
|
|
241
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
242
|
+
process.exitCode = 0;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 步骤 3/3:提交
|
|
247
|
+
const res = await authedRequest(cfg, cred, 'POST', '/sys/api/v1/platforms', body);
|
|
248
|
+
const json = res.json;
|
|
249
|
+
|
|
250
|
+
if (!json || typeof json !== 'object') {
|
|
251
|
+
throw new Error(`创建失败:接口返回异常(HTTP ${res.status})${String(res.text).slice(0, 200)}`);
|
|
252
|
+
}
|
|
253
|
+
if (json.code !== 200) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`创建失败:code=${json.code} msg=${json.msg || '未知错误'}\n` +
|
|
256
|
+
` 已提交的分组 code: ${v.group}(若平台已建出分组但连接器未建成,需在平台侧清理)`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const newId = pickNewId(json.data);
|
|
261
|
+
process.stdout.write('[3/3] 创建成功\n');
|
|
262
|
+
if (newId) process.stdout.write(` platformId = ${newId}\n`);
|
|
263
|
+
process.stdout.write(` platformName = ${v.name}\n`);
|
|
264
|
+
process.stdout.write(` groupPrefix = ${v.group}\n`);
|
|
265
|
+
process.stdout.write(`\n可用 platforms --name ${v.name} 复核\n`);
|
|
266
|
+
process.exitCode = 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
commandPlatformCreate,
|
|
271
|
+
buildBody,
|
|
272
|
+
pickNewId,
|
|
273
|
+
collect,
|
|
274
|
+
parseType,
|
|
275
|
+
FIXED,
|
|
276
|
+
TYPE_NAMES,
|
|
277
|
+
DEFAULT_ICON,
|
|
278
|
+
DEFAULT_TYPE
|
|
279
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadCredentials } = require('../config');
|
|
4
|
+
const { authedRequest } = require('../api');
|
|
5
|
+
const { renderTable } = require('../ui');
|
|
6
|
+
|
|
7
|
+
const MAX_PAGE_SIZE = 10000;
|
|
8
|
+
|
|
9
|
+
/** 终端显示宽度:CJK 字符占 2 列 */
|
|
10
|
+
function dispWidth(s) {
|
|
11
|
+
let w = 0;
|
|
12
|
+
for (const ch of String(s == null ? '' : s)) {
|
|
13
|
+
w += /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/.test(ch)
|
|
14
|
+
? 2
|
|
15
|
+
: 1;
|
|
16
|
+
}
|
|
17
|
+
return w;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pad(s, n) {
|
|
21
|
+
const str = String(s == null ? '' : s);
|
|
22
|
+
const w = dispWidth(str);
|
|
23
|
+
return w >= n ? str : str + ' '.repeat(n - w);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const opts = { name: '', page: 1, size: MAX_PAGE_SIZE, json: false };
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a === '--name' || a === '-n') {
|
|
32
|
+
opts.name = argv[i + 1] || '';
|
|
33
|
+
i += 1;
|
|
34
|
+
} else if (a === '--page') {
|
|
35
|
+
const v = Number(argv[i + 1]);
|
|
36
|
+
opts.page = Number.isFinite(v) && v > 0 ? v : 1;
|
|
37
|
+
i += 1;
|
|
38
|
+
} else if (a === '--size') {
|
|
39
|
+
const v = Number(argv[i + 1]);
|
|
40
|
+
opts.size = Number.isFinite(v) && v > 0 ? Math.min(v, MAX_PAGE_SIZE) : MAX_PAGE_SIZE;
|
|
41
|
+
i += 1;
|
|
42
|
+
} else if (a === '--all') {
|
|
43
|
+
opts.size = MAX_PAGE_SIZE;
|
|
44
|
+
} else if (a === '--json') {
|
|
45
|
+
opts.json = true;
|
|
46
|
+
} else if (!a.startsWith('-')) {
|
|
47
|
+
// 位置参数直接当作连接器名称关键词
|
|
48
|
+
opts.name = a;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return opts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 查询连接器应用列表(供其他命令复用)。
|
|
56
|
+
* 覆盖接口:GET /index/sys/api/v1/platforms?platformName=&pageNum=1&pageSize=10000
|
|
57
|
+
*/
|
|
58
|
+
async function fetchPlatforms(cfg, cred, opts) {
|
|
59
|
+
const o = Object.assign({ name: '', page: 1, size: MAX_PAGE_SIZE }, opts || {});
|
|
60
|
+
const qs =
|
|
61
|
+
`/sys/api/v1/platforms?platformName=${encodeURIComponent(o.name || '')}` +
|
|
62
|
+
`&pageNum=${o.page}&pageSize=${o.size}`;
|
|
63
|
+
|
|
64
|
+
const res = await authedRequest(cfg, cred, 'GET', qs);
|
|
65
|
+
const json = res.json;
|
|
66
|
+
|
|
67
|
+
if (!json || typeof json !== 'object') {
|
|
68
|
+
throw new Error(`接口返回异常(HTTP ${res.status}):${String(res.text).slice(0, 200)}`);
|
|
69
|
+
}
|
|
70
|
+
if (json.code !== 200) {
|
|
71
|
+
throw new Error(`查询连接器失败:code=${json.code} msg=${json.msg || '未知错误'}`);
|
|
72
|
+
}
|
|
73
|
+
const data = json.data || {};
|
|
74
|
+
const records = Array.isArray(data.records) ? data.records : [];
|
|
75
|
+
return {
|
|
76
|
+
total: typeof data.total === 'number' ? data.total : records.length,
|
|
77
|
+
records,
|
|
78
|
+
current: typeof data.current === 'number' ? data.current : o.page,
|
|
79
|
+
size: typeof data.size === 'number' ? data.size : o.size,
|
|
80
|
+
raw: json
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 把「platformId 或连接器名称」解析成完整记录(含 hostBOList)。
|
|
86
|
+
* 创建 API 资源需要 platformHostId,它只在 hostBOList[0].platformHostId 里,所以必须拿整条记录。
|
|
87
|
+
*
|
|
88
|
+
* @returns {{platformId:string, platformName:string, platformHostId:string, host:string, hostBOList:object[], raw:object|null}}
|
|
89
|
+
*/
|
|
90
|
+
async function resolvePlatformRecord(cfg, cred, input, out) {
|
|
91
|
+
const raw = String(input == null ? '' : input).trim();
|
|
92
|
+
if (!raw) throw new Error('缺少 --platform(连接器 platformId 或名称)');
|
|
93
|
+
|
|
94
|
+
const byId = /^\d+$/.test(raw);
|
|
95
|
+
const { total, records } = await fetchPlatforms(cfg, cred, {
|
|
96
|
+
name: byId ? '' : raw,
|
|
97
|
+
size: MAX_PAGE_SIZE
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let hit = null;
|
|
101
|
+
if (byId) {
|
|
102
|
+
hit = records.find((r) => String(r.platformId) === raw) || null;
|
|
103
|
+
if (!hit) {
|
|
104
|
+
// 全量里查不到也放行:可能刚好在分页外,交由调用方决定是否补 --host-id
|
|
105
|
+
return {
|
|
106
|
+
platformId: raw,
|
|
107
|
+
platformName: '',
|
|
108
|
+
platformHostId: '',
|
|
109
|
+
host: '',
|
|
110
|
+
hostBOList: [],
|
|
111
|
+
raw: null,
|
|
112
|
+
resolved: false
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
if (total === 0) {
|
|
117
|
+
throw new Error(`没有找到名称包含 "${raw}" 的连接器应用。\n 查看全部:wb platforms`);
|
|
118
|
+
}
|
|
119
|
+
if (total > 1) {
|
|
120
|
+
const rows = records.map((r, i) => [
|
|
121
|
+
String(i + 1),
|
|
122
|
+
String(r.platformName || ''),
|
|
123
|
+
String(r.platformId || ''),
|
|
124
|
+
String(r.platformType),
|
|
125
|
+
String(r.enabled)
|
|
126
|
+
]);
|
|
127
|
+
out.write(`名称 "${raw}" 命中 ${total} 个连接器,请指定其中一个:\n\n`);
|
|
128
|
+
out.write(renderTable(['#', 'platformName', 'platformId', 'type', 'enabled'], rows) + '\n\n');
|
|
129
|
+
throw new Error('请用 --platform <platformId> 指定具体连接器(或给更精确的名称)');
|
|
130
|
+
}
|
|
131
|
+
hit = records[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hostList = Array.isArray(hit.hostBOList) ? hit.hostBOList : [];
|
|
135
|
+
const first = hostList.find((h) => h && h.platformHostId) || hostList[0] || null;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
platformId: String(hit.platformId || ''),
|
|
139
|
+
platformName: String(hit.platformName || ''),
|
|
140
|
+
platformHostId: first ? String(first.platformHostId || '') : '',
|
|
141
|
+
host: first ? String(first.host || '') : '',
|
|
142
|
+
hostBOList: hostList,
|
|
143
|
+
raw: hit,
|
|
144
|
+
resolved: true
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 查询应用连接器(platform)列表。
|
|
150
|
+
* - platformName 为模糊查询,大小写不敏感
|
|
151
|
+
* - 全量取数用 pageSize=10000(实际连接器数量不可能超过此值)
|
|
152
|
+
*/
|
|
153
|
+
async function commandPlatforms(cfg, argv) {
|
|
154
|
+
const opts = parseArgs(argv);
|
|
155
|
+
|
|
156
|
+
const cred = loadCredentials();
|
|
157
|
+
if (!cred || !cred.tokenValue) {
|
|
158
|
+
throw new Error('未登录或授权已失效,请先执行 auth login');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { total, records, current, size, raw } = await fetchPlatforms(cfg, cred, {
|
|
162
|
+
name: opts.name,
|
|
163
|
+
page: opts.page,
|
|
164
|
+
size: opts.size
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (opts.json) {
|
|
168
|
+
process.stdout.write(JSON.stringify(raw, null, 2) + '\n');
|
|
169
|
+
process.exitCode = 0;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const keyword = opts.name ? `"${opts.name}"` : '(全部)';
|
|
174
|
+
process.stdout.write(`查询条件: platformName=${keyword} pageNum=${current} pageSize=${size}\n`);
|
|
175
|
+
process.stdout.write(`共 ${total} 个连接器,本次返回 ${records.length} 条\n\n`);
|
|
176
|
+
|
|
177
|
+
if (!records.length) {
|
|
178
|
+
process.stdout.write('(无匹配结果)\n');
|
|
179
|
+
process.exitCode = 0;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const header = ['#', 'platformName', 'platformId', 'enabled', 'auth', 'type', 'groupName'];
|
|
184
|
+
const rows = records.map((r, i) => [
|
|
185
|
+
String(i + 1),
|
|
186
|
+
String(r.platformName || ''),
|
|
187
|
+
String(r.platformId || ''),
|
|
188
|
+
String(r.enabled),
|
|
189
|
+
String(r.authStatus),
|
|
190
|
+
String(r.platformType),
|
|
191
|
+
String(r.groupName || '')
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const widths = header.map((h, ci) => Math.max(dispWidth(h), ...rows.map((row) => dispWidth(row[ci]))));
|
|
195
|
+
|
|
196
|
+
process.stdout.write(header.map((h, i) => pad(h, widths[i])).join(' ') + '\n');
|
|
197
|
+
process.stdout.write(widths.map((w) => '-'.repeat(w)).join(' ') + '\n');
|
|
198
|
+
for (const row of rows) {
|
|
199
|
+
process.stdout.write(row.map((c, i) => pad(c, widths[i])).join(' ') + '\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
process.stdout.write(`\n提示:加 --json 可输出完整字段(含 apiHostPort、remark、createdAt 等)\n`);
|
|
203
|
+
process.exitCode = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { commandPlatforms, fetchPlatforms, resolvePlatformRecord, MAX_PAGE_SIZE };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { loadCredentials } = require('../config');
|
|
5
|
+
const { authedRequest } = require('../api');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 通用带权请求:自动注入 Authorization: <tokenValue>
|
|
9
|
+
* 用法:
|
|
10
|
+
* wb request GET /sys/api/v1/user/list
|
|
11
|
+
* wb request POST /sys/api/v1/xxx --data '{"a":1}'
|
|
12
|
+
* wb request POST /sys/api/v1/xxx --data-file ./body.json
|
|
13
|
+
*/
|
|
14
|
+
async function commandRequest(cfg, argv) {
|
|
15
|
+
const args = argv.slice();
|
|
16
|
+
const method = (args[0] || 'GET').toUpperCase();
|
|
17
|
+
const pathname = args[1];
|
|
18
|
+
if (!pathname) {
|
|
19
|
+
throw new Error('用法: wb request <METHOD> <PATH> [--data <json>] [--data-file <file>] [--header k:v]');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cred = loadCredentials();
|
|
23
|
+
if (!cred || !cred.tokenValue) {
|
|
24
|
+
throw new Error('未登录或授权已失效,请先执行 auth login');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let body;
|
|
28
|
+
const extraHeaders = {};
|
|
29
|
+
|
|
30
|
+
for (let i = 2; i < args.length; i += 1) {
|
|
31
|
+
const flag = args[i];
|
|
32
|
+
if (flag === '--data') {
|
|
33
|
+
body = args[i + 1];
|
|
34
|
+
i += 1;
|
|
35
|
+
} else if (flag === '--data-file') {
|
|
36
|
+
body = fs.readFileSync(args[i + 1], 'utf8');
|
|
37
|
+
i += 1;
|
|
38
|
+
} else if (flag === '--header') {
|
|
39
|
+
const raw = args[i + 1] || '';
|
|
40
|
+
const idx = raw.indexOf(':');
|
|
41
|
+
if (idx > 0) extraHeaders[raw.slice(0, idx).trim()] = raw.slice(idx + 1).trim();
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const res = await authedRequest(cfg, cred, method, pathname, body, extraHeaders);
|
|
47
|
+
const out = res.json !== null ? JSON.stringify(res.json, null, 2) : res.text;
|
|
48
|
+
process.stdout.write(out + '\n');
|
|
49
|
+
|
|
50
|
+
if (res.status >= 400) {
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { commandRequest };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadCredentials, clearCredentials, credMatchesBaseUrl } = require('../config');
|
|
4
|
+
|
|
5
|
+
function maskToken(token) {
|
|
6
|
+
if (!token) return '';
|
|
7
|
+
if (token.length <= 8) return '****';
|
|
8
|
+
return token.slice(0, 4) + '****' + token.slice(-4);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 认证状态检测。契约(WorkBuddy 依此调度 auth):
|
|
13
|
+
* - exit 0 表示已认证,非 0 表示未认证
|
|
14
|
+
* - 无副作用、只读本地持久化状态、10 秒内返回
|
|
15
|
+
* - --json 输出 {"connected":true|false,...},供 cli.json 的 statusMatch 用 JSON 正则匹配;
|
|
16
|
+
* 相比文本匹配,它不会因文案改动而失效
|
|
17
|
+
*/
|
|
18
|
+
function authStatus(cfg, asJson) {
|
|
19
|
+
const cred = loadCredentials();
|
|
20
|
+
const hasCred = Boolean(cred && cred.tokenValue);
|
|
21
|
+
const currentBase = String(cfg.baseUrl || '').trim();
|
|
22
|
+
const connected = Boolean(hasCred && currentBase && credMatchesBaseUrl(cred, cfg));
|
|
23
|
+
|
|
24
|
+
if (connected) {
|
|
25
|
+
if (asJson) {
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
connected: true,
|
|
29
|
+
user: cred.nickName || cred.userName || 'unknown',
|
|
30
|
+
userName: cred.userName || '',
|
|
31
|
+
baseUrl: cred.baseUrl || cfg.baseUrl,
|
|
32
|
+
savedAt: cred.savedAt || ''
|
|
33
|
+
}) + '\n'
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
process.stdout.write('Logged in as ' + (cred.nickName || cred.userName || 'unknown') + '\n');
|
|
37
|
+
}
|
|
38
|
+
process.exitCode = 0;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 未连接时区分三种原因,便于给出不同的修复指引
|
|
43
|
+
let reason = 'no-credentials';
|
|
44
|
+
if (hasCred) reason = currentBase ? 'base-url-changed' : 'no-base-url';
|
|
45
|
+
|
|
46
|
+
if (reason === 'base-url-changed') {
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`平台地址已变更(凭证签发于 ${cred.baseUrl || '(空)'},当前 ${currentBase}),需重新登录\n`
|
|
49
|
+
);
|
|
50
|
+
} else if (reason === 'no-base-url') {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`平台地址未配置(凭证签发于 ${cred.baseUrl || '(空)'}),需重新登录并在登录页填写平台地址\n`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (asJson) {
|
|
57
|
+
process.stdout.write(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
connected: false,
|
|
60
|
+
reason,
|
|
61
|
+
baseUrl: currentBase
|
|
62
|
+
}) + '\n'
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
process.stdout.write('Not logged in\n');
|
|
66
|
+
}
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 登出。必须幂等:未登录时也返回 0。
|
|
72
|
+
*/
|
|
73
|
+
function authLogout() {
|
|
74
|
+
const existed = Boolean(loadCredentials());
|
|
75
|
+
clearCredentials();
|
|
76
|
+
process.stdout.write(existed ? 'Logged out\n' : 'Already logged out\n');
|
|
77
|
+
process.exitCode = 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function whoami(cfg, asJson) {
|
|
81
|
+
const cred = loadCredentials();
|
|
82
|
+
if (!cred || !cred.tokenValue) {
|
|
83
|
+
process.stderr.write('Not logged in. 请先执行 wb auth login\n');
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!credMatchesBaseUrl(cred, cfg)) {
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`平台地址已变更(凭证签发于 ${cred.baseUrl},当前 ${cfg.baseUrl}),需重新登录后才有有效授权\n`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const view = {
|
|
94
|
+
userName: cred.userName || '',
|
|
95
|
+
nickName: cred.nickName || '',
|
|
96
|
+
userId: cred.userId || '',
|
|
97
|
+
deptId: cred.deptId || '',
|
|
98
|
+
deptCode: cred.deptCode || '',
|
|
99
|
+
roles: cred.roles || [],
|
|
100
|
+
tenant: cred.tenant || null,
|
|
101
|
+
baseUrl: cred.baseUrl || '',
|
|
102
|
+
tokenName: cred.tokenName || 'Authorization',
|
|
103
|
+
tokenValue: maskToken(cred.tokenValue),
|
|
104
|
+
savedAt: cred.savedAt || ''
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (asJson) {
|
|
108
|
+
process.stdout.write(JSON.stringify(view, null, 2) + '\n');
|
|
109
|
+
} else {
|
|
110
|
+
process.stdout.write(`用户: ${view.nickName} (${view.userName})\n`);
|
|
111
|
+
process.stdout.write(`用户ID: ${view.userId}\n`);
|
|
112
|
+
process.stdout.write(`角色: ${(view.roles || []).join(', ') || '-'}\n`);
|
|
113
|
+
process.stdout.write(`Token: ${view.tokenValue}\n`);
|
|
114
|
+
process.stdout.write(`登录时间: ${view.savedAt}\n`);
|
|
115
|
+
}
|
|
116
|
+
process.exitCode = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { authStatus, authLogout, whoami, maskToken };
|