@xbongbong/xbbcli 2.2.0 → 2.4.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/README.md +66 -5
- package/auth-login.js +306 -0
- package/auth-logout.js +175 -0
- package/auth-status.js +105 -0
- package/{ai-call-add-task.js → hkh-call-add-task.js} +1 -1
- package/{ai-word-list.js → hkh-word-list.js} +1 -1
- package/package.json +2 -2
- package/token-set.js +4 -388
- package/xbb-browser.js +330 -0
- package/xbb-config.js +33 -0
- package/xbb-token-store.js +389 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getRegistry } from './xbb-registry.js';
|
|
5
|
+
import { persistEnvVars } from './xbb-config.js';
|
|
6
|
+
|
|
7
|
+
// token-set 与 auth-login 共用的凭证落盘逻辑:写 config.env、表单缓存、命令映射、部门/员工缓存、环境变量
|
|
8
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
|
|
9
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
|
|
10
|
+
export const FORMLIST_FILE_SUFFIX = '.formlist.json';
|
|
11
|
+
export const COMMAND_MAP_FILE_SUFFIX = '.command-map.md';
|
|
12
|
+
export const DEPARTMENT_USER_FILE_SUFFIX = '.department-user.json';
|
|
13
|
+
export const READ_ALL_PAGE_SIZE = 200;
|
|
14
|
+
export const READ_ALL_MAX_PAGES = 1000;
|
|
15
|
+
|
|
16
|
+
export function resolveBaseUrl(corpid) {
|
|
17
|
+
if (corpid.startsWith('ding') || corpid.includes('$$ding')) {
|
|
18
|
+
return 'https://proapi.xbongbong.com';
|
|
19
|
+
}
|
|
20
|
+
return 'https://appapi.xbongbong.com';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeArg(value) {
|
|
24
|
+
return String(value || '').trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getFormlistFile(corpid) {
|
|
28
|
+
return path.join(CONFIG_DIR, `${corpid}${FORMLIST_FILE_SUFFIX}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getCommandMapFile(corpid) {
|
|
32
|
+
return path.join(CONFIG_DIR, `${corpid}${COMMAND_MAP_FILE_SUFFIX}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getDepartmentUserFile(corpid) {
|
|
36
|
+
return path.join(CONFIG_DIR, `${corpid}${DEPARTMENT_USER_FILE_SUFFIX}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runCommandJson(name, kwargs) {
|
|
40
|
+
const command = getRegistry().get(`xbb/${name}`);
|
|
41
|
+
if (!command) throw new Error(`找不到命令:${name}`);
|
|
42
|
+
const rows = await command.func(kwargs);
|
|
43
|
+
if (!Array.isArray(rows)) throw new Error(`${name} 返回结果不是数组`);
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getFormlistRows(corpid, saasMark) {
|
|
48
|
+
const rows = await runCommandJson('form-list', { saasMark: String(saasMark) });
|
|
49
|
+
|
|
50
|
+
if (!Array.isArray(rows)) {
|
|
51
|
+
throw new Error(`form-list 返回结果不是数组:saasMark=${saasMark}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const errorRow = rows.find((item) => item && item.code);
|
|
55
|
+
if (errorRow) {
|
|
56
|
+
throw new Error(`form-list 获取失败(saasMark=${saasMark}):${errorRow.code} ${errorRow.msg || ''}`.trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return rows;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 先按 resetToken=0 查询是否已存在个人 token,返回空串再按 resetToken=1 重新生成
|
|
63
|
+
async function requestPersonalToken(corpid, token, userId, resetToken) {
|
|
64
|
+
const rows = await runCommandJson('token-generate', {
|
|
65
|
+
checkUserId: userId,
|
|
66
|
+
resetToken: String(resetToken),
|
|
67
|
+
token,
|
|
68
|
+
corpid,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!Array.isArray(rows)) {
|
|
72
|
+
throw new Error('token-generate 返回结果不是数组');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const errorRow = rows.find((item) => item && String(item.code ?? '') !== '' && Number(item.code) !== 1);
|
|
76
|
+
if (errorRow) {
|
|
77
|
+
throw new Error(`token-generate 获取个人 token 失败:${errorRow.code} ${errorRow.msg || ''}`.trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return String(rows[0]?.token || '').trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function getPersonalToken(corpid, token, userId) {
|
|
84
|
+
const existingToken = await requestPersonalToken(corpid, token, userId, 0);
|
|
85
|
+
|
|
86
|
+
if (existingToken) {
|
|
87
|
+
if (!existingToken.startsWith('user_')) {
|
|
88
|
+
throw new Error('token-generate 未返回有效的个人 token');
|
|
89
|
+
}
|
|
90
|
+
return existingToken;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const personalToken = await requestPersonalToken(corpid, token, userId, 1);
|
|
94
|
+
if (!personalToken.startsWith('user_')) {
|
|
95
|
+
throw new Error('token-generate 未返回有效的个人 token');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return personalToken;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function writeFormlistFile(corpid) {
|
|
102
|
+
const customForms = await getFormlistRows(corpid, 2);
|
|
103
|
+
const systemForms = await getFormlistRows(corpid, 1);
|
|
104
|
+
const formlistFile = getFormlistFile(corpid);
|
|
105
|
+
const mergedRows = [...customForms, ...systemForms];
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(formlistFile, JSON.stringify(mergedRows, null, 2) + '\n', 'utf8');
|
|
108
|
+
return formlistFile;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function fetchAllRows(name, pageSize) {
|
|
112
|
+
const rows = [];
|
|
113
|
+
for (let page = 1; page <= READ_ALL_MAX_PAGES; page += 1) {
|
|
114
|
+
const pageRows = await runCommandJson(name, { page: String(page), pageSize: String(pageSize) });
|
|
115
|
+
const errorRow = pageRows.find((item) => item && String(item.code ?? '') !== '');
|
|
116
|
+
if (errorRow) {
|
|
117
|
+
if (String(errorRow.code) === 'NO_DATA') {
|
|
118
|
+
return rows;
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`${name} 获取失败(page=${page}):${errorRow.code} ${errorRow.msg || ''}`.trim());
|
|
121
|
+
}
|
|
122
|
+
rows.push(...pageRows);
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`${name} 分页超过 ${READ_ALL_MAX_PAGES} 页,未获取到完整数据`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseDepartmentListField(value) {
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
const text = String(value ?? '').trim();
|
|
132
|
+
if (!text) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(text);
|
|
137
|
+
return Array.isArray(parsed) ? parsed : text;
|
|
138
|
+
} catch {
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toDepartmentEntry(row) {
|
|
144
|
+
return {
|
|
145
|
+
id: row.id ?? '',
|
|
146
|
+
name: row.name ?? '',
|
|
147
|
+
parentId: row.parentId ?? '',
|
|
148
|
+
depIdRouter: row.depIdRouter ?? '',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function toUserEntry(row) {
|
|
153
|
+
return {
|
|
154
|
+
userId: row.userId ?? '',
|
|
155
|
+
name: row.name ?? '',
|
|
156
|
+
position: row.position ?? '',
|
|
157
|
+
jobnumber: row.jobnumber ?? '',
|
|
158
|
+
departmentList: parseDepartmentListField(row.departmentList),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function writeDepartmentUserFile(corpid, userId) {
|
|
163
|
+
const departmentRows = await fetchAllRows('department-list', READ_ALL_PAGE_SIZE);
|
|
164
|
+
const userRows = await fetchAllRows('user-list', READ_ALL_PAGE_SIZE);
|
|
165
|
+
const payload = {
|
|
166
|
+
corpid,
|
|
167
|
+
updatedAt: new Date().toISOString(),
|
|
168
|
+
pageSize: READ_ALL_PAGE_SIZE,
|
|
169
|
+
departmentCount: departmentRows.length,
|
|
170
|
+
userCount: userRows.length,
|
|
171
|
+
departments: departmentRows.map(toDepartmentEntry),
|
|
172
|
+
users: userRows.map(toUserEntry),
|
|
173
|
+
};
|
|
174
|
+
const file = getDepartmentUserFile(corpid);
|
|
175
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
176
|
+
fs.writeFileSync(file, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
177
|
+
return { file, corpName: getCorpName(departmentRows), userName: getUserName(userRows, userId) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getCorpName(departmentRows) {
|
|
181
|
+
const root = departmentRows.find((row) => Number(row.id) === 1);
|
|
182
|
+
return root ? String(root.name || '').trim() : '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getUserName(userRows, userId) {
|
|
186
|
+
const target = String(userId || '').trim();
|
|
187
|
+
if (!target) {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
const exact = userRows.find((row) => String(row.userId || '').trim() === target);
|
|
191
|
+
const matched = exact || userRows.find((row) => String(row.userId || '').trim().toLowerCase() === target.toLowerCase());
|
|
192
|
+
return matched ? String(matched.name || '').trim() : '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function writeCompanyProfile(corpid, profile) {
|
|
196
|
+
const fields = Object.entries(profile).filter(([, value]) => String(value || '').trim() !== '');
|
|
197
|
+
if (!fields.length) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const patch = Object.fromEntries(fields);
|
|
201
|
+
const companies = readCompanies().map((item) => (
|
|
202
|
+
String(item.corpid || '').trim() === corpid ? { ...item, ...patch } : item
|
|
203
|
+
));
|
|
204
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(companies, null, 2) + '\n', 'utf8');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeBusinessType(value) {
|
|
208
|
+
return String(value).trim().replace(/[))\]】,,;;::。!?!?]+$/g, '');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getCommandBusinessType(command) {
|
|
212
|
+
if (command.businessType !== undefined && command.businessType !== null) {
|
|
213
|
+
return normalizeBusinessType(command.businessType);
|
|
214
|
+
}
|
|
215
|
+
const description = String(command.description || '');
|
|
216
|
+
const match = description.match(/businessType\s*:\s*(\d+)/i);
|
|
217
|
+
return match ? normalizeBusinessType(match[1]) : '';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function escapeCell(value) {
|
|
221
|
+
return String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function optionDescription(option) {
|
|
225
|
+
return `--${option.name}: ${option.help || ''}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function writeCommandMap(corpid) {
|
|
229
|
+
const formFile = getFormlistFile(corpid);
|
|
230
|
+
if (!fs.existsSync(formFile)) {
|
|
231
|
+
throw new Error(`Form cache not found: ${formFile}`);
|
|
232
|
+
}
|
|
233
|
+
const raw = fs.readFileSync(formFile, 'utf8').replace(/^\uFEFF/, '');
|
|
234
|
+
const data = JSON.parse(raw);
|
|
235
|
+
const forms = Array.isArray(data) ? data : data.data || [];
|
|
236
|
+
|
|
237
|
+
const commands = [...getRegistry().values()]
|
|
238
|
+
.filter((command) => command.site === 'xbb')
|
|
239
|
+
.map((command) => ({
|
|
240
|
+
...command,
|
|
241
|
+
command_options: command.args || [],
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const lines = [
|
|
245
|
+
'# XBB 命令映射',
|
|
246
|
+
'',
|
|
247
|
+
`来源:xbb --help;表单缓存:${path.basename(formFile)}`,
|
|
248
|
+
'',
|
|
249
|
+
'| 命令 | 命令名称 | formId | businessType | 字段说明 |',
|
|
250
|
+
'| --- | --- | --- | --- | --- |',
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
for (const command of commands) {
|
|
254
|
+
const commandBusinessType = getCommandBusinessType(command);
|
|
255
|
+
const matchedForms = forms.filter((form) => String(form.businessType ?? '').trim() === commandBusinessType);
|
|
256
|
+
const formIds = [...new Set(matchedForms.map((form) => form.formId).filter(Boolean))];
|
|
257
|
+
const businessTypes = [...new Set(matchedForms.map((form) => form.businessType).filter(Boolean))];
|
|
258
|
+
const options = (command.command_options || []).map(optionDescription).join('<br>');
|
|
259
|
+
lines.push(
|
|
260
|
+
`| ${escapeCell(command.name)} | ${escapeCell(command.description)} | ${escapeCell(formIds.join(', '))} | ${escapeCell(businessTypes.join(', ') || commandBusinessType)} | ${escapeCell(options)} |`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const commandMapFile = getCommandMapFile(corpid);
|
|
265
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
266
|
+
fs.writeFileSync(commandMapFile, `${lines.join('\n')}\n`, 'utf8');
|
|
267
|
+
return commandMapFile;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createResult(status, message, corpid, baseurl, userId, formlistFile = '', commandMapFile = '', enable = '', companyCount = '', departmentUserFile = '', corpName = '', userName = '', envStored = '', envFile = '') {
|
|
271
|
+
return [{
|
|
272
|
+
status,
|
|
273
|
+
message,
|
|
274
|
+
configFile: CONFIG_FILE,
|
|
275
|
+
corpid,
|
|
276
|
+
baseurl,
|
|
277
|
+
userId,
|
|
278
|
+
enable,
|
|
279
|
+
companyCount,
|
|
280
|
+
formlistFile,
|
|
281
|
+
commandMapFile,
|
|
282
|
+
departmentUserFile,
|
|
283
|
+
corpName,
|
|
284
|
+
userName,
|
|
285
|
+
envStored,
|
|
286
|
+
envFile,
|
|
287
|
+
}];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function readCompanies() {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
293
|
+
return normalizeCompanies(parsed);
|
|
294
|
+
} catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeCompanies(parsed) {
|
|
300
|
+
if (Array.isArray(parsed)) {
|
|
301
|
+
return parsed.filter((item) => item && typeof item === 'object');
|
|
302
|
+
}
|
|
303
|
+
if (parsed && Array.isArray(parsed.companies)) {
|
|
304
|
+
return parsed.companies.filter((item) => item && typeof item === 'object');
|
|
305
|
+
}
|
|
306
|
+
if (parsed && parsed.corpid) {
|
|
307
|
+
const { corpid, token, baseurl, userId } = parsed;
|
|
308
|
+
return [{ corpid, token, baseurl, userId, enable: true }];
|
|
309
|
+
}
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function upsertCompany(companies, entry) {
|
|
314
|
+
const index = companies.findIndex((item) => String(item.corpid || '').trim() === entry.corpid);
|
|
315
|
+
const next = companies.map((item) => ({ ...item, enable: false }));
|
|
316
|
+
if (index === -1) {
|
|
317
|
+
next.push({ ...entry, enable: true });
|
|
318
|
+
} else {
|
|
319
|
+
next[index] = { ...next[index], ...entry, enable: true };
|
|
320
|
+
}
|
|
321
|
+
return next;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// token-set / auth-login 共用:保存一家公司的凭证并刷新本地缓存,返回与 token-set 相同结构的行
|
|
325
|
+
export async function saveCompanyCredentials(kwargs) {
|
|
326
|
+
const corpid = normalizeArg(kwargs.corpid);
|
|
327
|
+
const token = normalizeArg(kwargs.token);
|
|
328
|
+
const userId = normalizeArg(kwargs.userId);
|
|
329
|
+
|
|
330
|
+
if (!corpid) {
|
|
331
|
+
return createResult('error', '缺少 --corpid', '', '', '');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!token) {
|
|
335
|
+
return createResult('error', '缺少 --token', corpid, '', '');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!userId) {
|
|
339
|
+
return createResult('error', '缺少 --userId', corpid, '', '');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let personalToken = token;
|
|
343
|
+
if (!token.startsWith('user_')) {
|
|
344
|
+
try {
|
|
345
|
+
personalToken = await getPersonalToken(corpid, token, userId);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return createResult('error', `生成个人 token 失败:${error.message}`, corpid, '', userId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const baseurl = resolveBaseUrl(corpid);
|
|
352
|
+
const companies = upsertCompany(readCompanies(), { corpid, token: personalToken, baseurl, userId });
|
|
353
|
+
|
|
354
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
355
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(companies, null, 2) + '\n', 'utf8');
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const formlistFile = await writeFormlistFile(corpid);
|
|
359
|
+
const commandMapFile = writeCommandMap(corpid);
|
|
360
|
+
const { file: departmentUserFile, corpName, userName } = await writeDepartmentUserFile(corpid, userId);
|
|
361
|
+
writeCompanyProfile(corpid, { corpName, userName });
|
|
362
|
+
let envStored = 'skipped';
|
|
363
|
+
let envFile = '';
|
|
364
|
+
if (!kwargs.noEnv) {
|
|
365
|
+
try {
|
|
366
|
+
const envResult = persistEnvVars({ corpid, token: personalToken, baseurl, userId, corpName, userName });
|
|
367
|
+
envStored = envResult.stored;
|
|
368
|
+
envFile = envResult.file;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
envStored = 'failed';
|
|
371
|
+
envFile = String(error.message || error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return createResult('ok', '已保存 corpid、token、baseurl、userId,并同步表单模板缓存、命令映射文件与部门/员工缓存', corpid, baseurl, userId, formlistFile, commandMapFile, true, companies.length, departmentUserFile, corpName, userName, envStored, envFile);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return createResult(
|
|
377
|
+
'partial',
|
|
378
|
+
`已保存 corpid、token、baseurl、userId,但同步缓存失败:${error.message}`,
|
|
379
|
+
corpid,
|
|
380
|
+
baseurl,
|
|
381
|
+
userId,
|
|
382
|
+
getFormlistFile(corpid),
|
|
383
|
+
'',
|
|
384
|
+
true,
|
|
385
|
+
companies.length,
|
|
386
|
+
'',
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|