byteplan-cli 1.0.2 → 1.2.2

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.
Files changed (66) hide show
  1. package/package.json +7 -3
  2. package/skills/byteplan-analysis/SKILL.md +1078 -0
  3. package/skills/byteplan-api/API_REFERENCE.md +249 -0
  4. package/skills/byteplan-api/SKILL.md +96 -0
  5. package/skills/byteplan-api/package.json +16 -0
  6. package/skills/byteplan-api/scripts/api.js +973 -0
  7. package/skills/byteplan-excel/SKILL.md +212 -0
  8. package/skills/byteplan-excel/examples/margin-analysis.json +40 -0
  9. package/skills/byteplan-excel/package.json +12 -0
  10. package/skills/byteplan-excel/pnpm-lock.yaml +68 -0
  11. package/skills/byteplan-excel/scripts/generate_excel.js +156 -0
  12. package/skills/byteplan-html/SKILL.md +490 -0
  13. package/skills/byteplan-html/examples/example-output.html +184 -0
  14. package/skills/byteplan-html/examples/generate-ppt-style-html.js +611 -0
  15. package/skills/byteplan-html/examples/margin-contribution-analysis.json +152 -0
  16. package/skills/byteplan-html/package.json +18 -0
  17. package/skills/byteplan-html/scripts/generate_html.js +517 -0
  18. package/skills/byteplan-ppt/SKILL.md +394 -0
  19. package/skills/byteplan-ppt/examples/margin-contribution-analysis.json +152 -0
  20. package/skills/byteplan-ppt/package.json +16 -0
  21. package/skills/byteplan-ppt/pnpm-lock.yaml +138 -0
  22. package/skills/byteplan-ppt/scripts/check_ppt_overlap.js +318 -0
  23. package/skills/byteplan-ppt/scripts/generate_ppt.js +680 -0
  24. package/skills/byteplan-video/SKILL.md +606 -0
  25. package/skills/byteplan-video/examples/sample-video-data.json +82 -0
  26. package/skills/byteplan-video/remotion-project/package.json +22 -0
  27. package/skills/byteplan-video/remotion-project/pnpm-lock.yaml +1646 -0
  28. package/skills/byteplan-video/remotion-project/remotion.config.ts +6 -0
  29. package/skills/byteplan-video/remotion-project/scene_durations.json +32 -0
  30. package/skills/byteplan-video/remotion-project/scripts/generate_audio.js +279 -0
  31. package/skills/byteplan-video/remotion-project/src/DynamicReport.tsx +172 -0
  32. package/skills/byteplan-video/remotion-project/src/Root.tsx +51 -0
  33. package/skills/byteplan-video/remotion-project/src/SalesReport.tsx +107 -0
  34. package/skills/byteplan-video/remotion-project/src/index.tsx +4 -0
  35. package/skills/byteplan-video/remotion-project/src/scenes/ChartSlide.tsx +201 -0
  36. package/skills/byteplan-video/remotion-project/src/scenes/CoverSlide.tsx +61 -0
  37. package/skills/byteplan-video/remotion-project/src/scenes/EndSlide.tsx +60 -0
  38. package/skills/byteplan-video/remotion-project/src/scenes/InsightSlide.tsx +101 -0
  39. package/skills/byteplan-video/remotion-project/src/scenes/KpiSlide.tsx +84 -0
  40. package/skills/byteplan-video/remotion-project/src/scenes/RecommendationSlide.tsx +100 -0
  41. package/skills/byteplan-video/remotion-project/tsconfig.json +13 -0
  42. package/skills/byteplan-video/remotion-project/video_data.json +76 -0
  43. package/skills/byteplan-video/scripts/generate_video.js +270 -0
  44. package/skills/byteplan-video/templates/package.json +31 -0
  45. package/skills/byteplan-video/templates/pnpm-lock.yaml +2200 -0
  46. package/skills/byteplan-video/templates/remotion.config.ts +9 -0
  47. package/skills/byteplan-video/templates/scripts/generate-audio.ts +55 -0
  48. package/skills/byteplan-video/templates/src/components/BarChartScene.tsx +153 -0
  49. package/skills/byteplan-video/templates/src/components/InsightScene.tsx +135 -0
  50. package/skills/byteplan-video/templates/src/components/LineChartScene.tsx +214 -0
  51. package/skills/byteplan-video/templates/src/components/SceneFactory.tsx +34 -0
  52. package/skills/byteplan-video/templates/src/components/SummaryScene.tsx +155 -0
  53. package/skills/byteplan-video/templates/src/components/TitleScene.tsx +130 -0
  54. package/skills/byteplan-video/templates/src/compositions/AnalysisVideo.tsx +39 -0
  55. package/skills/byteplan-video/templates/src/index.tsx +28 -0
  56. package/skills/byteplan-video/templates/src/register-root.tsx +4 -0
  57. package/skills/byteplan-video/templates/src/storyboard/types.ts +46 -0
  58. package/skills/byteplan-video/templates/tsconfig.json +17 -0
  59. package/skills/byteplan-video/templates/tsconfig.scripts.json +13 -0
  60. package/skills/byteplan-word/SKILL.md +233 -0
  61. package/skills/byteplan-word/package.json +12 -0
  62. package/skills/byteplan-word/pnpm-lock.yaml +120 -0
  63. package/skills/byteplan-word/scripts/generate_word.js +548 -0
  64. package/src/api.js +78 -22
  65. package/src/cli.js +11 -0
  66. package/src/commands/skills.js +279 -0
@@ -0,0 +1,973 @@
1
+ import 'dotenv/config';
2
+ import crypto from 'crypto';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+ import { execSync, spawn } from 'child_process';
7
+
8
+ // CLI 配置
9
+ const CLI_PACKAGE = 'byteplan-cli';
10
+ const CLI_BIN_NAME = 'byteplan';
11
+
12
+ // 环境配置
13
+ const ENVIRONMENTS = {
14
+ uat: 'https://uatapp.byteplan.com',
15
+ };
16
+
17
+ const BASIC_AUTH = 'Basic UEM6bkxDbndkSWhpeldieWtIeXVaTTZUcFFEZDdLd0s5SVhESzhMR3NhN1NPVw==';
18
+
19
+ // 当前环境(仅支持 UAT)
20
+ let currentEnv = 'uat';
21
+
22
+ // 是否使用 CLI 模式(默认优先使用 CLI)
23
+ let useCliMode = true;
24
+
25
+ /**
26
+ * 检查 byteplan-cli 是否已安装
27
+ * @returns {boolean}
28
+ */
29
+ function isCliInstalled() {
30
+ try {
31
+ execSync(`${CLI_BIN_NAME} --version`, { stdio: 'pipe' });
32
+ return true;
33
+ } catch (e) {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 安装 byteplan-cli
40
+ * @returns {Promise<boolean>}
41
+ */
42
+ async function installCli() {
43
+ console.log(`📦 正在安装 ${CLI_PACKAGE}...`);
44
+ try {
45
+ execSync(`npm i -g ${CLI_PACKAGE}`, { stdio: 'inherit' });
46
+ console.log(`✅ ${CLI_PACKAGE} 安装成功`);
47
+ return true;
48
+ } catch (e) {
49
+ console.error(`❌ ${CLI_PACKAGE} 安装失败: ${e.message}`);
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 确保 CLI 已安装,如果没有则自动安装
56
+ * @returns {Promise<boolean>}
57
+ */
58
+ async function ensureCliInstalled() {
59
+ if (isCliInstalled()) {
60
+ return true;
61
+ }
62
+ console.log(`⚠️ 未检测到 ${CLI_PACKAGE}, 正在自动安装...`);
63
+ return await installCli();
64
+ }
65
+
66
+ /**
67
+ * 执行 CLI 命令并返回 JSON 结果
68
+ * @param {string[]} args - CLI 参数
69
+ * @returns {Promise<any>}
70
+ */
71
+ async function execCli(args) {
72
+ // 确保 CLI 已安装
73
+ await ensureCliInstalled();
74
+
75
+ const fullCmd = `${CLI_BIN_NAME} ${args.join(' ')}`;
76
+
77
+ try {
78
+ const result = execSync(fullCmd, {
79
+ encoding: 'utf-8',
80
+ stdio: ['pipe', 'pipe', 'pipe'],
81
+ env: { ...process.env }
82
+ });
83
+
84
+ // 解析 JSON 输出
85
+ const jsonStr = result.trim();
86
+ if (jsonStr) {
87
+ return JSON.parse(jsonStr);
88
+ }
89
+ return null;
90
+ } catch (e) {
91
+ // 如果 CLI 执行失败,解析错误输出
92
+ const stderr = e.stderr?.toString() || '';
93
+ if (stderr.includes('error') || stderr.includes('Error')) {
94
+ try {
95
+ const errorJson = JSON.parse(stderr);
96
+ throw new Error(errorJson.message || stderr);
97
+ } catch {
98
+ throw new Error(`CLI 执行失败: ${stderr || e.message}`);
99
+ }
100
+ }
101
+ throw new Error(`CLI 执行失败: ${e.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * 设置是否使用 CLI 模式
107
+ * @param {boolean} useCli - 是否使用 CLI
108
+ */
109
+ export function setCliMode(useCli) {
110
+ useCliMode = useCli;
111
+ }
112
+
113
+ /**
114
+ * 获取当前模式
115
+ * @returns {boolean}
116
+ */
117
+ export function isUsingCli() {
118
+ return useCliMode && isCliInstalled();
119
+ }
120
+
121
+ /**
122
+ * 获取 .env 文件路径
123
+ * @returns {string}
124
+ */
125
+ function getEnvPath() {
126
+ // 优先使用调用方的 .env(当前工作目录),其次使用项目根目录
127
+ const cwd = process.cwd();
128
+ const projectEnvPath = path.join(cwd, '.env');
129
+ if (fs.existsSync(projectEnvPath)) {
130
+ return projectEnvPath;
131
+ }
132
+ // 回退到 skill 目录的 .env
133
+ return path.join(getDirname(), '..', '..', '.env');
134
+ }
135
+
136
+ /**
137
+ * 读取 .env 文件内容
138
+ * @returns {string}
139
+ */
140
+ function readEnvContent() {
141
+ const envPath = getEnvPath();
142
+ if (fs.existsSync(envPath)) {
143
+ return fs.readFileSync(envPath, 'utf-8');
144
+ }
145
+ return '';
146
+ }
147
+
148
+ /**
149
+ * 保存凭证到 .env 文件
150
+ * @param {Object} credentials - 凭证对象
151
+ * @param {string} credentials.username - 用户名
152
+ * @param {string} credentials.password - 密码
153
+ * @param {string} credentials.access_token - 访问令牌
154
+ * @param {string} credentials.refresh_token - 刷新令牌
155
+ * @param {number} credentials.expires_in - 过期时间(秒)
156
+ */
157
+ function saveCredentials(credentials) {
158
+ const { username, password, access_token, refresh_token, expires_in } = credentials;
159
+
160
+ // 读取现有 .env 内容
161
+ let envContent = readEnvContent();
162
+ const lines = envContent.split('\n');
163
+
164
+ // 需要更新的配置项(每次登录都更新)
165
+ const keysToUpdate = ['BP_ENV', 'BP_USER', 'BP_PASSWORD', 'USER_NAME', 'PASSWORD', 'ACCESS_TOKEN', 'REFRESH_TOKEN', 'TOKEN_EXPIRES_IN'];
166
+
167
+ // 保留非配置项的行(注释、空行、其他配置)
168
+ const preservedLines = lines.filter(line => {
169
+ const trimmed = line.trim();
170
+ if (!trimmed || trimmed.startsWith('#')) return true;
171
+ const key = trimmed.split('=')[0]?.trim();
172
+ // 保留不属于 keysToUpdate 的配置
173
+ return !keysToUpdate.includes(key);
174
+ });
175
+
176
+ // 构建新的配置内容(总是写入最新值)
177
+ const configLines = [];
178
+
179
+ // 写入环境配置
180
+ configLines.push(`BP_ENV=${currentEnv}`);
181
+
182
+ // 写入用户名
183
+ configLines.push(`BP_USER=${username}`);
184
+
185
+ // 写入密码(用引号包裹以保护特殊字符)
186
+ configLines.push(`BP_PASSWORD="${password}"`);
187
+
188
+ // 写入 access_token
189
+ configLines.push(`ACCESS_TOKEN=${access_token}`);
190
+
191
+ // 写入 refresh_token
192
+ if (refresh_token) {
193
+ configLines.push(`REFRESH_TOKEN=${refresh_token}`);
194
+ }
195
+
196
+ // 写入过期时间
197
+ if (expires_in) {
198
+ configLines.push(`TOKEN_EXPIRES_IN=${Date.now() + expires_in * 1000}`);
199
+ }
200
+
201
+ // 合并内容:保留的行 + 新配置
202
+ const finalLines = [...preservedLines, ...configLines];
203
+ const finalContent = finalLines.join('\n');
204
+
205
+ // 写入文件
206
+ const envPath = getEnvPath();
207
+ fs.writeFileSync(envPath, finalContent, 'utf-8');
208
+ }
209
+
210
+ /**
211
+ * 检查 token 是否过期
212
+ * @returns {boolean}
213
+ */
214
+ function isTokenExpired() {
215
+ const expiresIn = process.env.TOKEN_EXPIRES_IN;
216
+ if (!expiresIn) {
217
+ return true; // 没有过期时间,认为已过期,需要重新登录
218
+ }
219
+ const expiresAt = parseInt(expiresIn, 10);
220
+ // 提前 5 分钟认为过期
221
+ return Date.now() >= expiresAt - 5 * 60 * 1000;
222
+ }
223
+
224
+ /**
225
+ * 重新加载 .env 文件(用于 CLI 登录后同步 token)
226
+ */
227
+ function reloadEnv() {
228
+ const envPath = getEnvPath();
229
+ if (fs.existsSync(envPath)) {
230
+ const envContent = fs.readFileSync(envPath, 'utf-8');
231
+ const lines = envContent.split('\n');
232
+ for (const line of lines) {
233
+ const trimmed = line.trim();
234
+ if (!trimmed || trimmed.startsWith('#')) continue;
235
+ const [key, ...valueParts] = trimmed.split('=');
236
+ if (key && valueParts.length > 0) {
237
+ const value = valueParts.join('=').replace(/^["']|["']$/g, '');
238
+ process.env[key.trim()] = value;
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 设置登录环境
246
+ * @param {'uat'} env - 环境名称
247
+ */
248
+ export function setEnvironment(env) {
249
+ if (ENVIRONMENTS[env]) {
250
+ currentEnv = env;
251
+ } else {
252
+ throw new Error(`未知环境: ${env},仅支持: uat`);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 获取当前环境
258
+ * @returns {string}
259
+ */
260
+ export function getEnvironment() {
261
+ return currentEnv;
262
+ }
263
+
264
+ /**
265
+ * 获取当前环境的基础 URL
266
+ * @returns {string}
267
+ */
268
+ export function getBaseUrl() {
269
+ return ENVIRONMENTS[currentEnv];
270
+ }
271
+
272
+ /**
273
+ * 获取 RSA 公钥(UAT 环境)
274
+ * @returns {Promise<{key: string, id: string}>}
275
+ */
276
+ async function getPublicKey() {
277
+ const baseUrl = getBaseUrl();
278
+ const response = await fetch(`${baseUrl}/base/util/get/publicKey?t=${Date.now()}`, {
279
+ method: 'GET',
280
+ headers: {
281
+ 'accept': 'application/json',
282
+ },
283
+ });
284
+
285
+ const data = await response.json();
286
+
287
+ // 兼容多种响应格式
288
+ const key = data.data || data.publicKey || data.key || '';
289
+ const id = data.publicKeyId || data.id || '';
290
+
291
+ return { key, id };
292
+ }
293
+
294
+ /**
295
+ * RSA 加密密码(UAT 环境)
296
+ * @param {string} password - 明文密码
297
+ * @param {string} publicKey - RSA 公钥
298
+ * @returns {string} - Base64 编码的加密密码
299
+ */
300
+ function encryptPassword(password, publicKey) {
301
+ if (!publicKey) {
302
+ throw new Error('公钥为空,无法加密密码');
303
+ }
304
+
305
+ // 清理公钥格式
306
+ let cleanKey = publicKey
307
+ .replace(/-----BEGIN PUBLIC KEY-----/g, '')
308
+ .replace(/-----END PUBLIC KEY-----/g, '')
309
+ .replace(/\s/g, '');
310
+
311
+ // 格式化为 PEM
312
+ const formattedKeyBody = cleanKey.match(/.{1,64}/g)?.join('\n') || cleanKey;
313
+ const formattedKey = `-----BEGIN PUBLIC KEY-----\n${formattedKeyBody}\n-----END PUBLIC KEY-----`;
314
+
315
+ // RSA 加密
316
+ const encrypted = crypto.publicEncrypt(
317
+ {
318
+ key: formattedKey,
319
+ padding: crypto.constants.RSA_PKCS1_PADDING,
320
+ },
321
+ Buffer.from(password, 'utf-8'),
322
+ );
323
+
324
+ return encrypted.toString('base64');
325
+ }
326
+
327
+ /**
328
+ * 登录获取 token
329
+ * @param {string} username - 用户名(手机号)
330
+ * @param {string} password - 密码
331
+ * @param {'uat'} [env] - 环境,默认为 UAT
332
+ * @returns {Promise<{access_token: string, refresh_token: string, expires_in: number}>}
333
+ */
334
+ export async function login(username, password, env) {
335
+ // 如果指定了环境,使用指定环境
336
+ if (env && ENVIRONMENTS[env]) {
337
+ setEnvironment(env);
338
+ }
339
+
340
+ // CLI 模式
341
+ if (useCliMode) {
342
+ await ensureCliInstalled();
343
+ try {
344
+ const result = await execCli(['login', '-u', username, '-p', password, '-e', currentEnv]);
345
+ if (result.error) {
346
+ throw new Error(result.message || '登录失败');
347
+ }
348
+ // CLI 登录成功后,token 已自动保存到 ~/.byteplan/.env
349
+ // 需要重新加载 .env 以获取 token
350
+ reloadEnv();
351
+ return {
352
+ access_token: process.env.ACCESS_TOKEN,
353
+ refresh_token: process.env.REFRESH_TOKEN,
354
+ expires_in: result.expiresIn,
355
+ };
356
+ } catch (e) {
357
+ // CLI 失败时,回退到直接 API 调用
358
+ console.warn(`⚠️ CLI 登录失败,回退到直接 API: ${e.message}`);
359
+ useCliMode = false;
360
+ }
361
+ }
362
+
363
+ // 直接 API 调用(回退模式)
364
+ const baseUrl = getBaseUrl();
365
+ const t = Date.now();
366
+ const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
367
+
368
+ let bodyParts = [
369
+ `--${boundary}`,
370
+ 'Content-Disposition: form-data; name="scope"',
371
+ '',
372
+ 'read write',
373
+ `--${boundary}`,
374
+ 'Content-Disposition: form-data; name="grant_type"',
375
+ '',
376
+ 'password',
377
+ `--${boundary}`,
378
+ 'Content-Disposition: form-data; name="username"',
379
+ '',
380
+ `+86${username}`,
381
+ ];
382
+
383
+ if (currentEnv === 'uat') {
384
+ // UAT 环境:获取公钥并加密密码
385
+ const { key: publicKey, id: publicKeyId } = await getPublicKey();
386
+ const encryptedPassword = encryptPassword(password, publicKey);
387
+
388
+ bodyParts.push(
389
+ `--${boundary}`,
390
+ `Content-Disposition: form-data; name="publicKeyId"`,
391
+ '',
392
+ publicKeyId,
393
+ );
394
+ bodyParts.push(
395
+ `--${boundary}`,
396
+ 'Content-Disposition: form-data; name="password"',
397
+ '',
398
+ encryptedPassword,
399
+ );
400
+ } else {
401
+ // DEV 环境:明文密码
402
+ bodyParts.push(
403
+ `--${boundary}`,
404
+ 'Content-Disposition: form-data; name="password"',
405
+ '',
406
+ password,
407
+ );
408
+ }
409
+
410
+ bodyParts.push(`--${boundary}--`);
411
+
412
+ const response = await fetch(`${baseUrl}/base/login?t=${t}`, {
413
+ method: 'POST',
414
+ headers: {
415
+ 'accept': 'application/json',
416
+ 'authorization': BASIC_AUTH,
417
+ 'content-type': `multipart/form-data; boundary=${boundary}`,
418
+ },
419
+ body: bodyParts.join('\r\n'),
420
+ });
421
+
422
+ const data = await response.json();
423
+
424
+ if (data.error) {
425
+ throw new Error(`登录失败: ${data.message || data.error_description || JSON.stringify(data)}`);
426
+ }
427
+
428
+ // 保存凭证到 .env
429
+ try {
430
+ saveCredentials({
431
+ username,
432
+ password,
433
+ access_token: data.access_token,
434
+ refresh_token: data.refresh_token,
435
+ expires_in: data.expires_in
436
+ });
437
+ } catch (e) {
438
+ console.warn(`⚠️ Token 持久化失败: ${e.message}`);
439
+ }
440
+
441
+ return data;
442
+ }
443
+
444
+ /**
445
+ * 使用 .env 中的账号密码登录
446
+ * @param {'uat'} [env] - 环境,默认为 UAT
447
+ * @param {boolean} [forceReLogin=false] - 是否强制重新登录
448
+ * @returns {Promise<{access_token: string, refresh_token: string, expires_in: number}>}
449
+ */
450
+ export async function loginWithEnv(env, forceReLogin = false) {
451
+ // 重新加载 .env 以获取最新的凭证
452
+ reloadEnv();
453
+
454
+ const username = process.env.BP_USER || process.env.USER_NAME;
455
+ const password = process.env.BP_PASSWORD || process.env.PASSWORD;
456
+
457
+ if (!username || !password) {
458
+ throw new Error('请在 .env 中配置 BP_USER 和 BP_PASSWORD(或 USER_NAME 和 PASSWORD)');
459
+ }
460
+
461
+ // 如果指定了环境,使用指定环境;否则默认使用 UAT
462
+ if (env && ENVIRONMENTS[env]) {
463
+ setEnvironment(env);
464
+ } else {
465
+ setEnvironment('uat');
466
+ }
467
+
468
+ // 如果没有强制重新登录,先检查是否有可用的缓存 token
469
+ if (!forceReLogin) {
470
+ const cachedToken = process.env.ACCESS_TOKEN;
471
+ if (cachedToken && !isTokenExpired()) {
472
+ return {
473
+ access_token: cachedToken,
474
+ refresh_token: process.env.REFRESH_TOKEN || null,
475
+ expires_in: null,
476
+ _cached: true // 标记为缓存的 token
477
+ };
478
+ }
479
+ }
480
+
481
+ // 缓存无效,重新登录
482
+ return login(username, password);
483
+ }
484
+
485
+ /**
486
+ * 获取当前 token
487
+ * @returns {string|null}
488
+ */
489
+ export function getToken() {
490
+ return process.env.ACCESS_TOKEN || null;
491
+ }
492
+
493
+ /**
494
+ * 获取用户信息
495
+ * @param {string} token - access token
496
+ * @returns {Promise<{user: object, tenantList: array}>}
497
+ */
498
+ export async function getUserInfo(token) {
499
+ // CLI 模式
500
+ if (useCliMode && !token) {
501
+ await ensureCliInstalled();
502
+ try {
503
+ const result = await execCli(['user', 'info']);
504
+ if (result.error) {
505
+ throw new Error(result.message || '获取用户信息失败');
506
+ }
507
+ // 将 CLI 结果转换为原有格式
508
+ return {
509
+ user: {
510
+ id: result.user?.userId,
511
+ userName: result.user?.userName,
512
+ mobile: result.user?.userPhone,
513
+ email: result.user?.email,
514
+ iconUrl: result.user?.avatar,
515
+ tenantId: result.currentTenant?.tenantId,
516
+ tenantName: result.currentTenant?.tenantName,
517
+ tenantCode: result.currentTenant?.tenantCode,
518
+ },
519
+ tenantList: [], // CLI user info 不返回 tenantList
520
+ };
521
+ } catch (e) {
522
+ console.warn(`⚠️ CLI 获取用户信息失败,回退到直接 API: ${e.message}`);
523
+ useCliMode = false;
524
+ // 回退到直接 API,需要先获取 token
525
+ await loginWithEnv();
526
+ token = getToken();
527
+ }
528
+ }
529
+
530
+ // 直接 API 调用
531
+ const baseUrl = getBaseUrl();
532
+ const t = Date.now();
533
+ const response = await fetch(`${baseUrl}/base/api/home?pageAuthFlag=true&t=${t}`, {
534
+ method: 'GET',
535
+ headers: {
536
+ 'accept': 'application/json, text/plain, */*',
537
+ 'authorization': `Bearer ${token}`,
538
+ },
539
+ });
540
+
541
+ return response.json();
542
+ }
543
+
544
+ /**
545
+ * 切换租户
546
+ * @param {string} token - access token
547
+ * @param {string} tenantId - 租户ID
548
+ * @returns {Promise<object>}
549
+ */
550
+ export async function switchTenant(token, tenantId) {
551
+ const baseUrl = getBaseUrl();
552
+ const t = Date.now();
553
+ const response = await fetch(`${baseUrl}/base/api/user/tenant/switch?enabled=1&tenantId=${tenantId}&t=${t}`, {
554
+ method: 'PUT',
555
+ headers: {
556
+ 'accept': 'application/json, text/plain, */*',
557
+ 'authorization': `Bearer ${token}`,
558
+ },
559
+ });
560
+
561
+ return response.json();
562
+ }
563
+
564
+ /**
565
+ * 默认 menu headers(AI_REPORT 菜单配置)
566
+ */
567
+ const DEFAULT_MENU_HEADERS = {
568
+ 'x-menu-code': 'AI_REPORT',
569
+ 'x-menu-id': '2008425412219936770',
570
+ 'x-menu-params': 'null',
571
+ 'x-page-code': 'ai_report',
572
+ };
573
+
574
+ /**
575
+ * 查询模型列表
576
+ * @param {string} token - access token
577
+ * @param {object} options - 可选参数
578
+ * @param {number} options.page - 页码,默认 0
579
+ * @param {number} options.size - 每页数量,默认 100
580
+ * @param {object} options.menuHeaders - 自定义 menu headers
581
+ * @returns {Promise<array>}
582
+ */
583
+ export async function queryModels(token, options = {}) {
584
+ const { page = 0, size = 100, menuHeaders = {} } = options;
585
+
586
+ // CLI 模式
587
+ if (useCliMode && !token) {
588
+ await ensureCliInstalled();
589
+ try {
590
+ const result = await execCli(['model', 'list', '-p', String(page), '-s', String(size)]);
591
+ if (result.error) {
592
+ throw new Error(result.message || '查询模型失败');
593
+ }
594
+ // 将 CLI 结果转换为原有格式(数组)
595
+ return result.models || [];
596
+ } catch (e) {
597
+ console.warn(`⚠️ CLI 查询模型失败,回退到直接 API: ${e.message}`);
598
+ useCliMode = false;
599
+ await loginWithEnv();
600
+ token = getToken();
601
+ }
602
+ }
603
+
604
+ // 直接 API 调用
605
+ const baseUrl = getBaseUrl();
606
+
607
+ const headers = {
608
+ 'accept': 'application/json, text/plain, */*',
609
+ 'authorization': `Bearer ${token}`,
610
+ ...DEFAULT_MENU_HEADERS,
611
+ ...menuHeaders,
612
+ };
613
+
614
+ const response = await fetch(`${baseUrl}/data/api/model/query?page=${page}&size=${size}`, {
615
+ method: 'GET',
616
+ headers,
617
+ });
618
+
619
+ return response.json();
620
+ }
621
+
622
+ /**
623
+ * 根据模型编码查询模型数据
624
+ * @param {string} token - access token
625
+ * @param {string} modelCode - 模型编码
626
+ * @param {Object} params - 查询参数
627
+ * @param {Object} params.customQuery - 自定义查询条件
628
+ * @param {string[]} params.groupFields - 分组字段
629
+ * @param {Array} params.functions - 聚合函数
630
+ * @param {object} options - 可选配置
631
+ * @param {object} options.menuHeaders - 自定义 menu headers
632
+ * @returns {Promise<any>}
633
+ */
634
+ export async function getModelData(token, modelCode, params = {}, options = {}) {
635
+ const { customQuery, groupFields, functions } = params;
636
+ const { menuHeaders = {} } = options;
637
+
638
+ // CLI 模式(仅支持简单查询)
639
+ if (useCliMode && !token && !customQuery && !groupFields && !functions) {
640
+ await ensureCliInstalled();
641
+ const pageNum = params.pageNum || 0;
642
+ const pageSize = params.pageSize || 100;
643
+ try {
644
+ const result = await execCli(['data', 'query', modelCode, '-p', String(pageNum), '-s', String(pageSize)]);
645
+ if (result.error) {
646
+ throw new Error(result.message || '查询数据失败');
647
+ }
648
+ // 将 CLI 结果转换为原有格式
649
+ return {
650
+ data: {
651
+ headers: result.headers || [],
652
+ data: result.data || [],
653
+ pageNum: result.pagination?.pageNum || pageNum,
654
+ pageSize: result.pagination?.pageSize || pageSize,
655
+ total: result.pagination?.total || 0,
656
+ }
657
+ };
658
+ } catch (e) {
659
+ console.warn(`⚠️ CLI 查询数据失败,回退到直接 API: ${e.message}`);
660
+ useCliMode = false;
661
+ await loginWithEnv();
662
+ token = getToken();
663
+ }
664
+ }
665
+
666
+ // 直接 API 调用
667
+ const baseUrl = getBaseUrl();
668
+
669
+ const headers = {
670
+ 'accept': 'application/json, text/plain, */*',
671
+ 'authorization': `Bearer ${token}`,
672
+ 'content-type': 'application/json',
673
+ ...DEFAULT_MENU_HEADERS,
674
+ ...menuHeaders,
675
+ };
676
+
677
+ const body = {
678
+ modelCode,
679
+ params: {},
680
+ };
681
+
682
+ if (customQuery) {
683
+ body.params.customQuery = customQuery;
684
+ }
685
+
686
+ if (groupFields && groupFields.length > 0) {
687
+ body.params.groupFields = groupFields;
688
+ }
689
+
690
+ if (functions && functions.length > 0) {
691
+ body.params.functions = functions;
692
+ }
693
+
694
+ const response = await fetch(`${baseUrl}/foresight/api/bi/multdim/anls/ai/query`, {
695
+ method: 'POST',
696
+ headers,
697
+ body: JSON.stringify(body),
698
+ });
699
+
700
+ return response.json();
701
+ }
702
+
703
+ /**
704
+ * 获取模型字段详情
705
+ * @param {string} token - access token
706
+ * @param {string[]} modelCodes - 模型编码数组
707
+ * @returns {Promise<Object>}
708
+ */
709
+ export async function getModelColumns(token, modelCodes) {
710
+ // CLI 模式
711
+ if (useCliMode && !token) {
712
+ await ensureCliInstalled();
713
+ try {
714
+ const result = await execCli(['model', 'columns', ...modelCodes]);
715
+ if (result.error) {
716
+ throw new Error(result.message || '获取模型字段失败');
717
+ }
718
+ // 将 CLI 结果转换为原有格式
719
+ const columnsMap = {};
720
+ for (const modelCode of modelCodes) {
721
+ columnsMap[modelCode] = result[modelCode]?.columns || [];
722
+ }
723
+ return { data: columnsMap };
724
+ } catch (e) {
725
+ console.warn(`⚠️ CLI 获取模型字段失败,回退到直接 API: ${e.message}`);
726
+ useCliMode = false;
727
+ await loginWithEnv();
728
+ token = getToken();
729
+ }
730
+ }
731
+
732
+ // 直接 API 调用
733
+ const baseUrl = getBaseUrl();
734
+ const response = await fetch(`${baseUrl}/data/api/model/col/get/model/col/by/codes`, {
735
+ method: 'POST',
736
+ headers: {
737
+ 'accept': 'application/json, text/plain, */*',
738
+ 'authorization': `Bearer ${token}`,
739
+ 'content-type': 'application/json',
740
+ ...DEFAULT_MENU_HEADERS,
741
+ },
742
+ body: JSON.stringify(modelCodes),
743
+ });
744
+
745
+ return response.json();
746
+ }
747
+
748
+ /**
749
+ * 获取维度值列表
750
+ * @param {string} token - access token
751
+ * @param {string} dimCode - 维度代码
752
+ * @param {Object} options - 可选参数
753
+ * @returns {Promise<Array>}
754
+ */
755
+ export async function getDimValues(token, dimCode, options = {}) {
756
+ const { modelCode, colName, page = 0, size = 100, keywords = null } = options;
757
+
758
+ // CLI 模式
759
+ if (useCliMode && !token) {
760
+ await ensureCliInstalled();
761
+ try {
762
+ const args = ['dim', dimCode];
763
+ if (keywords) {
764
+ args.push('-k', keywords);
765
+ }
766
+ const result = await execCli(args);
767
+ if (result.error) {
768
+ throw new Error(result.message || '获取维度值失败');
769
+ }
770
+ // 将 CLI 结果转换为原有格式
771
+ return {
772
+ data: {
773
+ content: result.values || [],
774
+ }
775
+ };
776
+ } catch (e) {
777
+ console.warn(`⚠️ CLI 获取维度值失败,回退到直接 API: ${e.message}`);
778
+ useCliMode = false;
779
+ await loginWithEnv();
780
+ token = getToken();
781
+ }
782
+ }
783
+
784
+ // 直接 API 调用
785
+ const baseUrl = getBaseUrl();
786
+
787
+ const response = await fetch(`${baseUrl}/data/api/online/lov/DATA_DIM_VALUE_LOV?dimCode=${dimCode}&page=${page}&size=${size}`, {
788
+ method: 'POST',
789
+ headers: {
790
+ 'accept': 'application/json, text/plain, */*',
791
+ 'authorization': `Bearer ${token}`,
792
+ 'content-type': 'application/json',
793
+ },
794
+ body: JSON.stringify({
795
+ dimCode,
796
+ keywords,
797
+ page,
798
+ size,
799
+ modelCode: modelCode || '',
800
+ colName: colName || '',
801
+ filterMap: {},
802
+ writeOnly: false,
803
+ }),
804
+ });
805
+
806
+ return response.json();
807
+ }
808
+
809
+ /**
810
+ * 获取列表值(LIST类型字段的可选值)
811
+ * @param {string} token - access token
812
+ * @param {string} listCode - 列表代码
813
+ * @param {Object} options - 可选参数
814
+ * @returns {Promise<Array>}
815
+ */
816
+ export async function getListValues(token, listCode, options = {}) {
817
+ const { modelCode, colName, page = 0, size = 999 } = options;
818
+
819
+ // CLI 模式(暂不支持 LIST,使用直接 API)
820
+ if (useCliMode && !token) {
821
+ await loginWithEnv();
822
+ token = getToken();
823
+ }
824
+
825
+ // 直接 API 调用
826
+ const baseUrl = getBaseUrl();
827
+
828
+ const response = await fetch(`${baseUrl}/data/api/permission/dimension/query`, {
829
+ method: 'POST',
830
+ headers: {
831
+ 'accept': 'application/json, text/plain, */*',
832
+ 'authorization': `Bearer ${token}`,
833
+ 'content-type': 'application/json',
834
+ },
835
+ body: JSON.stringify({
836
+ total: false,
837
+ page,
838
+ size,
839
+ dimensionType: 'LIST',
840
+ modelColName: colName,
841
+ dimensionCode: listCode,
842
+ modelCode,
843
+ filterMap: {},
844
+ writeOnly: false,
845
+ }),
846
+ });
847
+
848
+ return response.json();
849
+ }
850
+
851
+ /**
852
+ * 获取LOV值(LOV类型字段的可选值)
853
+ * @param {string} token - access token
854
+ * @param {string} lovCode - LOV代码
855
+ * @param {Object} options - 可选参数
856
+ * @returns {Promise<Array>}
857
+ */
858
+ export async function getLovValues(token, lovCode, options = {}) {
859
+ const { modelCode, colName, page = 0, size = 100, keywords = null } = options;
860
+
861
+ // CLI 模式
862
+ if (useCliMode && !token) {
863
+ await ensureCliInstalled();
864
+ try {
865
+ const args = ['lov', lovCode];
866
+ if (keywords) {
867
+ args.push('-k', keywords);
868
+ }
869
+ const result = await execCli(args);
870
+ if (result.error) {
871
+ throw new Error(result.message || '获取LOV值失败');
872
+ }
873
+ // 将 CLI 结果转换为原有格式
874
+ return {
875
+ data: {
876
+ content: result.values || [],
877
+ }
878
+ };
879
+ } catch (e) {
880
+ console.warn(`⚠️ CLI 获取LOV值失败,回退到直接 API: ${e.message}`);
881
+ useCliMode = false;
882
+ await loginWithEnv();
883
+ token = getToken();
884
+ }
885
+ }
886
+
887
+ // 直接 API 调用
888
+ const baseUrl = getBaseUrl();
889
+
890
+ const response = await fetch(`${baseUrl}/data/api/online/lov/${lovCode}?page=${page}&size=${size}`, {
891
+ method: 'POST',
892
+ headers: {
893
+ 'accept': 'application/json, text/plain, */*',
894
+ 'authorization': `Bearer ${token}`,
895
+ 'content-type': 'application/json',
896
+ },
897
+ body: JSON.stringify({
898
+ keywords,
899
+ page,
900
+ size,
901
+ modelCode: modelCode || '',
902
+ colName: colName || '',
903
+ filterMap: {},
904
+ writeOnly: false,
905
+ }),
906
+ });
907
+
908
+ return response.json();
909
+ }
910
+
911
+ /**
912
+ * 获取层级值(LEVEL类型字段的可选值)
913
+ * @param {string} token - access token
914
+ * @param {string} levelCode - 层级代码
915
+ * @param {Object} options - 可选参数
916
+ * @returns {Promise<Array>}
917
+ */
918
+ export async function getLevelValues(token, levelCode, options = {}) {
919
+ const { modelCode, colName, page = 0, size = 999 } = options;
920
+
921
+ // CLI 模式(暂不支持 LEVEL,使用直接 API)
922
+ if (useCliMode && !token) {
923
+ await loginWithEnv();
924
+ token = getToken();
925
+ }
926
+
927
+ // 直接 API 调用
928
+ const baseUrl = getBaseUrl();
929
+
930
+ const response = await fetch(`${baseUrl}/data/api/permission/dimension/query`, {
931
+ method: 'POST',
932
+ headers: {
933
+ 'accept': 'application/json, text/plain, */*',
934
+ 'authorization': `Bearer ${token}`,
935
+ 'content-type': 'application/json',
936
+ },
937
+ body: JSON.stringify({
938
+ total: false,
939
+ page,
940
+ size,
941
+ dimensionType: 'DIM_HIERARCHY',
942
+ modelColName: colName,
943
+ dimensionCode: levelCode,
944
+ modelCode,
945
+ filterMap: {},
946
+ writeOnly: false,
947
+ }),
948
+ });
949
+
950
+ return response.json();
951
+ }
952
+
953
+ /**
954
+ * 获取 api.js 的绝对路径(供其他 skill 引用)
955
+ * @returns {string}
956
+ */
957
+ export function getApiPath() {
958
+ return path.resolve(getDirname(), 'api.js');
959
+ }
960
+
961
+ /**
962
+ * 获取当前模块所在目录
963
+ */
964
+ function getDirname() {
965
+ try {
966
+ return path.dirname(fileURLToPath(import.meta.url));
967
+ } catch {
968
+ return path.dirname(new URL(import.meta.url).pathname);
969
+ }
970
+ }
971
+
972
+ // 导出 DEFAULT_MENU_HEADERS 供外部使用
973
+ export { DEFAULT_MENU_HEADERS };