weaver-work-cli 0.1.20 → 0.1.22

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 (77) hide show
  1. package/README.md +19 -11
  2. package/dist/core/refs.js +39 -36
  3. package/dist/internal/e10/auth/commands.js +33 -18
  4. package/dist/internal/e10/auth/session.js +60 -2
  5. package/dist/internal/e10/auth/xiaoe.js +253 -53
  6. package/dist/internal/e10/context.js +18 -6
  7. package/dist/internal/e10/request-runtime.js +3 -1
  8. package/dist/shortcuts/caizhilian/operations/confirm.js +1 -1
  9. package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +901 -20
  10. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +55 -0
  11. package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +4 -4
  12. package/dist/shortcuts/caizhilian/operations/specs.js +75 -0
  13. package/dist/shortcuts/hrm/manifest.js +16 -9
  14. package/dist/shortcuts/hrm/operations/registry.js +2 -0
  15. package/dist/shortcuts/hrm/operations/resolve.js +449 -0
  16. package/dist/shortcuts/invoice/host.js +5 -4
  17. package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
  18. package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
  19. package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
  20. package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
  21. package/dist/shortcuts/meeting/operations/eb/decision.js +385 -0
  22. package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
  23. package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
  24. package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
  25. package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
  26. package/dist/shortcuts/meeting/operations/env.js +78 -61
  27. package/dist/shortcuts/meeting/operations/link.js +125 -0
  28. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  29. package/dist/shortcuts/meeting/operations/shared.js +13 -0
  30. package/dist/shortcuts/workflow/host.js +5 -1
  31. package/dist/shortcuts/workflow/operations/form.js +95 -11
  32. package/dist/shortcuts/workflow/operations/read.js +785 -26
  33. package/docs/e10-auth.md +8 -2
  34. package/docs/hrm.md +34 -2
  35. package/docs/invoice.md +3 -0
  36. package/package.json +2 -2
  37. package/scripts/xiaoe_env.py +118 -0
  38. package/skill-template/domains/hrm.md +2 -1
  39. package/skill-template/skill-template.md +1 -1
  40. package/skills/weaver-e10-blog/SKILL.md +1 -1
  41. package/skills/weaver-e10-caizhilian/SKILL.md +5 -4
  42. package/skills/weaver-e10-caizhilian/references/file-collection.md +37 -0
  43. package/skills/weaver-e10-caizhilian/references/folder.md +5 -1
  44. package/skills/weaver-e10-caizhilian/references/setting-attach.md +35 -5
  45. package/skills/weaver-e10-caizhilian/references/source-manifest.json +620 -5
  46. package/skills/weaver-e10-calendar/SKILL.md +1 -1
  47. package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
  48. package/skills/weaver-e10-esb/SKILL.md +1 -1
  49. package/skills/weaver-e10-hrm/SKILL.md +52 -5
  50. package/skills/weaver-e10-hrm/references/org.md +107 -1
  51. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  52. package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -1
  53. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  54. package/skills/weaver-e10-mail/SKILL.md +1 -1
  55. package/skills/weaver-e10-meeting/SKILL.md +27 -11
  56. package/skills/weaver-e10-meeting/references/meeting-eb.md +162 -7
  57. package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
  58. package/skills/weaver-e10-meeting.zip +0 -0
  59. package/skills/weaver-e10-okr/SKILL.md +1 -1
  60. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  61. package/skills/weaver-e10-plan/SKILL.md +1 -1
  62. package/skills/weaver-e10-qiyecheng/SKILL.md +1 -1
  63. package/skills/weaver-e10-shared/SKILL.md +10 -13
  64. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -17
  65. package/skills/weaver-e10-shared/references/non-interactive-environment.md +21 -14
  66. package/skills/weaver-e10-shijingran/SKILL.md +1 -1
  67. package/skills/weaver-e10-skill-maker/SKILL.md +1 -1
  68. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
  69. package/skills/weaver-e10-wenshuding/SKILL.md +1 -1
  70. package/skills/weaver-e10-workflow/SKILL.md +19 -19
  71. package/skills/weaver-e10-workflow/references/source-manifest.json +13 -13
  72. package/skills/weaver-e10-workflow/references/workflow-create.md +11 -7
  73. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -14
  74. package/skills/weaver-e10-yepiaotong/SKILL.md +1 -2
  75. package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
  76. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
  77. package/skills/weaver-e10-ziguanjia/SKILL.md +1 -1
@@ -1,9 +1,10 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { accessSync, constants as fsConstants, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { CliError, ExitCode } from '../../../core/errors.js';
6
- import { buildE10CookieHeader, E10AuthSession, profileNameFromUrl, readTeamsCheckUserInfo, saveAuthFile, saveProfileConfig, setActiveProfile, } from './session.js';
7
+ import { buildE10CookieHeader, E10AuthSession, extractRawSetCookie, extractSessionTicket, profileNameFromUrl, readTeamsCheckUserInfo, saveAuthFile, saveProfileConfig, setActiveProfile, } from './session.js';
7
8
  export const DEFAULT_E10_BASE_URL = 'https://www.e-cology.com.cn';
8
9
  export const XIAOE_AGENT_TYPE = 'XiaoE';
9
10
  // `XIAOE_` 是宿主/平台保留前缀(XIAOE_DOMAIN、XIAOE_USER_TOKEN、XIAOE_SKILL_* 等)。
@@ -20,7 +21,6 @@ const XIAOE_UNIFIED_LOGIN_MAP_ENV_KEYS = [
20
21
  'XIAOE_UNIFIED_LOGIN_MAP',
21
22
  ];
22
23
  const XIAOE_SINGLE_DOMAIN_ENV_KEYS = [
23
- 'XIAOE_DOMAIN',
24
24
  'WEAVER_XIAOE_E10_DOMAIN',
25
25
  'WEAVER_XIAOE_BASE_URL',
26
26
  'WEAVER_XIAOE_E10_BASE_URL',
@@ -50,6 +50,7 @@ const XIAOE_CLIENT_TOKEN_TIMEOUT_MS = 15_000;
50
50
  export const XIAOE_SKILL_ETEAMID_API_PATH = '/spec-apps/api/app/rpachat/skill/extend/eteamId';
51
51
  export const XIAOE_CLIENT_SETTINGS_SOURCE = 'XiaoE/Robots/user_settings.json';
52
52
  export const XIAOE_USER_TOKEN_SOURCE = 'XIAOE_USER_TOKEN';
53
+ export const XIAOE_BUNDLED_ENV_SCRIPT_SOURCE = 'bundled scripts/xiaoe_env.py';
53
54
  function envString(env, ...keys) {
54
55
  for (const key of keys) {
55
56
  const value = env[key];
@@ -69,11 +70,13 @@ export function hasXiaoEEnv(env = process.env) {
69
70
  export function describeXiaoECredentialSource(source) {
70
71
  // Step 1 is the platform-injected credential (XIAOE_USER_TOKEN + XIAOE_DOMAIN):
71
72
  // it is available in every sandbox run and needs no local desktop client.
72
- if (source === XIAOE_USER_TOKEN_SOURCE) {
73
+ if (source === XIAOE_USER_TOKEN_SOURCE || source === XIAOE_BUNDLED_ENV_SCRIPT_SOURCE) {
73
74
  return {
74
75
  credentialSourceStep: 1,
75
76
  credentialSourceType: 'xiaoe_platform',
76
- credentialSourceLabel: 'XiaoE platform user token',
77
+ credentialSourceLabel: source === XIAOE_BUNDLED_ENV_SCRIPT_SOURCE
78
+ ? 'XiaoE platform user token (bundled Python fallback)'
79
+ : 'XiaoE platform user token',
77
80
  };
78
81
  }
79
82
  if (source === 'xiaoeUserData/tokens.py') {
@@ -256,18 +259,24 @@ function normalizeBaseUrlValue(value) {
256
259
  return '';
257
260
  return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
258
261
  }
259
- export function resolveXiaoEDefaultBaseUrl(env = process.env) {
262
+ function resolveXiaoEConfiguredBaseUrl(env) {
260
263
  const explicitDefault = envString(env, ...XIAOE_DEFAULT_DOMAIN_ENV_KEYS);
261
264
  if (explicitDefault)
262
265
  return normalizeBaseUrlValue(explicitDefault);
263
- // The injected XIAOE_DOMAIN (XIAOE_USER_TOKEN's target domain) is a fact
264
- // about the current host and must win over the CLI's built-in fallback.
266
+ // XIAOE_DOMAIN is the XiaoE/erpa service address used to exchange
267
+ // XIAOE_USER_TOKEN. Only dedicated E10 variables may select the target E10.
265
268
  const singleDomain = envString(env, ...XIAOE_SINGLE_DOMAIN_ENV_KEYS);
266
269
  if (singleDomain)
267
270
  return normalizeBaseUrlValue(singleDomain);
268
271
  const firstCredential = listXiaoEUnifiedLoginCredentials(env)[0];
269
272
  if (firstCredential?.baseUrl)
270
273
  return normalizeUrl(firstCredential.baseUrl);
274
+ return '';
275
+ }
276
+ export function resolveXiaoEDefaultBaseUrl(env = process.env) {
277
+ const configured = resolveXiaoEConfiguredBaseUrl(env);
278
+ if (configured)
279
+ return configured;
271
280
  return hasXiaoEEnv(env) ? DEFAULT_E10_BASE_URL : '';
272
281
  }
273
282
  function uniqueStrings(values) {
@@ -294,7 +303,7 @@ function xiaoETokenScriptCandidatePaths(env = process.env) {
294
303
  ]);
295
304
  }
296
305
  const envHome = envString(env, 'HOME');
297
- const nodeHome = homedir();
306
+ const nodeHome = envHome ? '' : homedir();
298
307
  return uniqueStrings([
299
308
  envHome ? join(envHome, 'Library', 'Application Support', 'xiaoeUserData', 'tokens.py') : '',
300
309
  nodeHome ? join(nodeHome, 'Library', 'Application Support', 'xiaoeUserData', 'tokens.py') : '',
@@ -303,6 +312,10 @@ function xiaoETokenScriptCandidatePaths(env = process.env) {
303
312
  function xiaoETokenScriptPath(env = process.env) {
304
313
  return xiaoETokenScriptCandidatePaths(env).find((path) => existsSync(path)) || '';
305
314
  }
315
+ export function xiaoEBundledEnvScriptPath() {
316
+ const scriptPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../scripts/xiaoe_env.py');
317
+ return existsSync(scriptPath) ? scriptPath : '';
318
+ }
306
319
  export function hasXiaoETokenScript(env = process.env) {
307
320
  const explicit = envString(env, XIAOE_TOKEN_SCRIPT_ENV_KEY, XIAOE_TOKEN_SCRIPT_LEGACY_ENV_KEY);
308
321
  if (explicit)
@@ -310,22 +323,114 @@ export function hasXiaoETokenScript(env = process.env) {
310
323
  const scriptPath = xiaoETokenScriptPath(env);
311
324
  return Boolean(scriptPath && existsSync(scriptPath));
312
325
  }
313
- function xiaoETokenScriptPythonCandidates(env = process.env) {
326
+ function isExecutableFile(path) {
327
+ try {
328
+ if (!statSync(path).isFile())
329
+ return false;
330
+ accessSync(path, fsConstants.X_OK);
331
+ return true;
332
+ }
333
+ catch {
334
+ return false;
335
+ }
336
+ }
337
+ function listXiaoEPythonRuntimeDirectories(resourcesRoot) {
338
+ if (!resourcesRoot || !existsSync(resourcesRoot))
339
+ return [];
340
+ try {
341
+ return readdirSync(resourcesRoot, { withFileTypes: true })
342
+ .filter((entry) => entry.isDirectory() && /^Python3(?:[-_.]\d[\d.]*)?$/iu.test(entry.name))
343
+ .map((entry) => join(resourcesRoot, entry.name))
344
+ .sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
345
+ }
346
+ catch {
347
+ return [];
348
+ }
349
+ }
350
+ function listExecutablePythonFiles(directory, platform) {
351
+ if (!directory || !existsSync(directory))
352
+ return [];
353
+ try {
354
+ const pattern = platform === 'win32' ? /^python(?:3(?:\.\d+)*)?\.exe$/iu : /^python3(?:\.\d+)*$/u;
355
+ return readdirSync(directory, { withFileTypes: true })
356
+ .filter((entry) => entry.isFile() && pattern.test(entry.name))
357
+ .map((entry) => join(directory, entry.name))
358
+ .filter(isExecutableFile)
359
+ .sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
360
+ }
361
+ catch {
362
+ return [];
363
+ }
364
+ }
365
+ /**
366
+ * Locate the Python runtime bundled with the desktop XiaoE client. The client
367
+ * ships the runtime expected by xiaoeUserData/tokens.py; the system Python may
368
+ * still be unavailable or incompatible with a host-provided script.
369
+ */
370
+ export function xiaoEClientPythonCandidates(env = process.env, platform = process.platform) {
371
+ const appRoots = [];
372
+ if (platform === 'darwin') {
373
+ const envHome = envString(env, 'HOME');
374
+ const nodeHome = homedir();
375
+ for (const home of uniqueStrings([envHome, nodeHome])) {
376
+ appRoots.push(join(home, 'Applications', '智能小e.app'));
377
+ }
378
+ appRoots.push('/Applications/智能小e.app');
379
+ }
380
+ else if (platform === 'win32') {
381
+ const localAppData = envString(env, 'LOCALAPPDATA');
382
+ const programFiles = uniqueStrings([
383
+ envString(env, 'ProgramW6432'),
384
+ envString(env, 'ProgramFiles'),
385
+ envString(env, 'ProgramFiles(x86)'),
386
+ ]);
387
+ const appNames = ['智能小e', 'XiaoE', 'XiaoEClient', 'XiaoEAI'];
388
+ if (localAppData) {
389
+ for (const appName of appNames) {
390
+ appRoots.push(join(localAppData, 'Programs', appName), join(localAppData, appName));
391
+ }
392
+ }
393
+ for (const programFilesRoot of programFiles) {
394
+ for (const appName of appNames)
395
+ appRoots.push(join(programFilesRoot, appName));
396
+ }
397
+ }
398
+ const candidates = [];
399
+ for (const appRoot of uniqueStrings(appRoots)) {
400
+ const resourcesRoots = platform === 'darwin'
401
+ ? [join(appRoot, 'Contents', 'Resources')]
402
+ : [join(appRoot, 'Resources'), appRoot];
403
+ for (const resourcesRoot of resourcesRoots) {
404
+ for (const runtimeRoot of listXiaoEPythonRuntimeDirectories(resourcesRoot)) {
405
+ const executableDirectories = platform === 'win32'
406
+ ? [runtimeRoot, join(runtimeRoot, 'bin')]
407
+ : [join(runtimeRoot, 'bin')];
408
+ for (const executableDirectory of executableDirectories) {
409
+ candidates.push(...listExecutablePythonFiles(executableDirectory, platform));
410
+ }
411
+ }
412
+ }
413
+ }
414
+ return uniqueStrings(candidates);
415
+ }
416
+ export function xiaoETokenScriptPythonCandidates(env = process.env, platform = process.platform) {
314
417
  const explicit = envString(env, XIAOE_TOKEN_SCRIPT_PYTHON_ENV_KEY, XIAOE_TOKEN_SCRIPT_PYTHON_LEGACY_ENV_KEY);
315
418
  if (explicit)
316
419
  return [explicit];
317
- return uniqueStrings(process.platform === 'win32'
318
- ? ['python', 'python3', 'py']
319
- : ['python3', '/usr/bin/python3', 'python']);
420
+ return uniqueStrings(platform === 'win32'
421
+ ? [...xiaoEClientPythonCandidates(env, platform), 'python', 'python3', 'py']
422
+ : [...xiaoEClientPythonCandidates(env, platform), 'python3', '/usr/bin/python3', 'python']);
320
423
  }
321
424
  function isXiaoETokenScriptEnvFailure(message) {
322
- return /No module named|ModuleNotFoundError|ENOENT|command not found|not recognized as an internal|cannot find the file/i
425
+ return /No module named|ModuleNotFoundError|ENOENT|EACCES|permission denied|command not found|not recognized as an internal|cannot find the file/i
323
426
  .test(message);
324
427
  }
325
- async function runXiaoETokenScriptOnce(pythonCommand, scriptPath, env) {
428
+ async function runXiaoETokenScriptOnce(pythonCommand, scriptPath, baseUrl, env) {
326
429
  const wrapper = [
327
430
  'import ast,contextlib,io,json,runpy,sys',
328
431
  `path = ${JSON.stringify(scriptPath)}`,
432
+ `base_url = ${JSON.stringify(baseUrl)}`,
433
+ 'sys.argv = [path, base_url]',
329
434
  'buf = io.StringIO()',
330
435
  'data = None',
331
436
  'with contextlib.redirect_stdout(buf):',
@@ -347,9 +452,9 @@ async function runXiaoETokenScriptOnce(pythonCommand, scriptPath, env) {
347
452
  ' except Exception as exc:',
348
453
  ' last_error = exc',
349
454
  ' if data is None:',
350
- ' raise SystemExit("tokens.py stdout is not valid JSON object")',
455
+ ' raise SystemExit("XiaoE credential script stdout is not valid JSON object")',
351
456
  'else:',
352
- ' raise SystemExit("tokens.py produced empty stdout")',
457
+ ' raise SystemExit("XiaoE credential script produced empty stdout")',
353
458
  'print(json.dumps(data, ensure_ascii=False))',
354
459
  ].join('\n');
355
460
  return await new Promise((resolve, reject) => {
@@ -360,12 +465,12 @@ async function runXiaoETokenScriptOnce(pythonCommand, scriptPath, env) {
360
465
  }, (error, stdout, stderr) => {
361
466
  if (error) {
362
467
  const detail = (stderr || error.message || 'unknown error').trim().split('\n').slice(-1)[0];
363
- reject(new CliError('XiaoE token script failed: ' + detail, ExitCode.AuthError));
468
+ reject(new CliError('XiaoE credential script failed: ' + detail, ExitCode.AuthError));
364
469
  return;
365
470
  }
366
471
  const text = stdout.trim();
367
472
  if (!text) {
368
- reject(new CliError('XiaoE token script returned empty stdout.', ExitCode.AuthError));
473
+ reject(new CliError('XiaoE credential script returned empty stdout.', ExitCode.AuthError));
369
474
  return;
370
475
  }
371
476
  try {
@@ -373,19 +478,18 @@ async function runXiaoETokenScriptOnce(pythonCommand, scriptPath, env) {
373
478
  resolve(parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null);
374
479
  }
375
480
  catch {
376
- reject(new CliError('XiaoE token script stdout is not valid JSON.', ExitCode.AuthError));
481
+ reject(new CliError('XiaoE credential script stdout is not valid JSON.', ExitCode.AuthError));
377
482
  }
378
483
  });
379
484
  });
380
485
  }
381
- async function runXiaoETokenScript(env = process.env) {
382
- const scriptPath = xiaoETokenScriptPath(env);
486
+ async function runXiaoEPythonCredentialScript(scriptPath, baseUrl, env) {
383
487
  if (!scriptPath || !existsSync(scriptPath))
384
488
  return null;
385
489
  let lastError = null;
386
490
  for (const pythonCommand of xiaoETokenScriptPythonCandidates(env)) {
387
491
  try {
388
- return await runXiaoETokenScriptOnce(pythonCommand, scriptPath, env);
492
+ return await runXiaoETokenScriptOnce(pythonCommand, scriptPath, baseUrl, env);
389
493
  }
390
494
  catch (error) {
391
495
  lastError = error;
@@ -396,14 +500,26 @@ async function runXiaoETokenScript(env = process.env) {
396
500
  break;
397
501
  }
398
502
  }
399
- throw lastError instanceof Error ? lastError : new CliError('XiaoE token script failed.', ExitCode.AuthError);
503
+ throw lastError instanceof Error ? lastError : new CliError('XiaoE credential script failed.', ExitCode.AuthError);
504
+ }
505
+ async function runXiaoETokenScript(baseUrl, env = process.env) {
506
+ return runXiaoEPythonCredentialScript(xiaoETokenScriptPath(env), baseUrl, env);
400
507
  }
401
508
  async function resolveXiaoETokenScriptCredential(baseUrl, env = process.env) {
402
- const tokenData = await runXiaoETokenScript(env);
509
+ const tokenData = await runXiaoETokenScript(baseUrl, env);
403
510
  if (!tokenData)
404
511
  return null;
405
512
  return readXiaoECredentialRecord(tokenData, 'xiaoeUserData/tokens.py', baseUrl);
406
513
  }
514
+ async function resolveXiaoEBundledEnvScriptCredential(baseUrl, env = process.env) {
515
+ const scriptPath = xiaoEBundledEnvScriptPath();
516
+ if (!scriptPath)
517
+ return null;
518
+ const tokenData = await runXiaoEPythonCredentialScript(scriptPath, baseUrl, env);
519
+ if (!tokenData)
520
+ return null;
521
+ return readXiaoECredentialRecord(tokenData, XIAOE_BUNDLED_ENV_SCRIPT_SOURCE, baseUrl);
522
+ }
407
523
  export function xiaoEClientRobotsRoot(env = process.env) {
408
524
  if (process.platform === 'win32') {
409
525
  const localAppData = envString(env, 'LOCALAPPDATA');
@@ -437,10 +553,6 @@ export function findXiaoEClientSettingsFile(env = process.env) {
437
553
  return '';
438
554
  return files.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)[0];
439
555
  }
440
- /**
441
- * Mirror of the host `xiaoeUserData/tokens.py` credential chain, implemented in
442
- * Node so the CLI does not depend on a Python runtime that has `requests`.
443
- */
444
556
  /**
445
557
  * Platform-injected credential: XIAOE_USER_TOKEN (+ the XiaoE service address).
446
558
  * This is the only source that works in a Linux sandbox with no desktop client
@@ -488,21 +600,57 @@ async function fetchXiaoEClientEteamId(credential, domain) {
488
600
  const controller = new AbortController();
489
601
  const timer = setTimeout(() => controller.abort(), XIAOE_CLIENT_TOKEN_TIMEOUT_MS);
490
602
  try {
491
- const resp = await fetch(`${credential.erpaUrl}${XIAOE_SKILL_ETEAMID_API_PATH}`, {
492
- method: 'POST',
493
- headers: {
494
- 'content-type': 'application/json',
495
- authorization: credential.authToken,
496
- },
497
- body: JSON.stringify({ domain: domain || DEFAULT_E10_BASE_URL }),
498
- redirect: 'manual',
499
- signal: controller.signal,
500
- });
501
- if (!resp.ok) {
502
- throw new CliError(`XiaoE client token endpoint returned HTTP ${resp.status}.`, ExitCode.AuthError);
603
+ let endpoint = new URL(`${credential.erpaUrl}${XIAOE_SKILL_ETEAMID_API_PATH}`);
604
+ let resp = null;
605
+ for (let redirects = 0; redirects <= 5; redirects += 1) {
606
+ resp = await fetch(endpoint, {
607
+ method: 'POST',
608
+ headers: {
609
+ 'content-type': 'application/json',
610
+ authorization: credential.authToken,
611
+ },
612
+ body: JSON.stringify({ domain: domain || DEFAULT_E10_BASE_URL }),
613
+ redirect: 'manual',
614
+ signal: controller.signal,
615
+ });
616
+ if (resp.status < 300 || resp.status >= 400)
617
+ break;
618
+ if (redirects === 5) {
619
+ throw new CliError('XiaoE client token endpoint redirected too many times.', ExitCode.AuthError);
620
+ }
621
+ const location = resp.headers.get('location');
622
+ if (!location) {
623
+ throw new CliError('XiaoE client token endpoint returned a redirect without Location.', ExitCode.AuthError);
624
+ }
625
+ const next = new URL(location, endpoint);
626
+ const sameOrigin = next.origin === endpoint.origin;
627
+ const sameHostHttpsUpgrade = endpoint.protocol === 'http:'
628
+ && next.protocol === 'https:'
629
+ && next.hostname === endpoint.hostname;
630
+ const safeProtocol = next.protocol === 'http:' || next.protocol === 'https:';
631
+ if (!safeProtocol || next.username || next.password || (!sameOrigin && !sameHostHttpsUpgrade)) {
632
+ throw new CliError('XiaoE client token endpoint attempted an unsafe cross-origin redirect.', ExitCode.AuthError);
633
+ }
634
+ await resp.body?.cancel().catch(() => undefined);
635
+ endpoint = next;
636
+ }
637
+ if (!resp?.ok) {
638
+ throw new CliError(`XiaoE client token endpoint returned HTTP ${resp?.status || 0}.`, ExitCode.AuthError);
503
639
  }
504
640
  const data = await resp.json().catch(() => null);
505
- return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
641
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
642
+ throw new CliError('XiaoE client token endpoint returned invalid JSON.', ExitCode.AuthError);
643
+ }
644
+ const payload = data;
645
+ const businessCode = payload.code;
646
+ const businessData = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
647
+ ? payload.data
648
+ : {};
649
+ if ((businessCode !== '0000' && businessCode !== 0 && businessCode !== 200) || !readString(businessData, 'eteamId')) {
650
+ const codeText = businessCode === undefined ? 'missing' : String(businessCode);
651
+ throw new CliError(`XiaoE client token exchange failed with business code ${codeText}.`, ExitCode.AuthError);
652
+ }
653
+ return payload;
506
654
  }
507
655
  finally {
508
656
  clearTimeout(timer);
@@ -511,7 +659,11 @@ async function fetchXiaoEClientEteamId(credential, domain) {
511
659
  async function resolveXiaoETokenCredential(credential, baseUrl) {
512
660
  const targetBaseUrl = baseUrl || DEFAULT_E10_BASE_URL;
513
661
  const payload = await fetchXiaoEClientEteamId(credential, targetBaseUrl);
514
- return readXiaoECredentialRecord(payload, credential.source, targetBaseUrl);
662
+ // The legacy gateway contract uses numeric code 0 and serves the E10 APIs
663
+ // from the same origin as the token exchange. Newer 0000/200 responses keep
664
+ // the XiaoE service origin separate and use the requested E10 target.
665
+ const resolvedBaseUrl = payload.code === 0 ? credential.erpaUrl : targetBaseUrl;
666
+ return readXiaoECredentialRecord(payload, credential.source, resolvedBaseUrl);
515
667
  }
516
668
  async function resolveXiaoEPlatformCredential(baseUrl, env = process.env) {
517
669
  const credential = readXiaoEPlatformCredential(env);
@@ -530,10 +682,31 @@ export function resolveXiaoEPreferredBaseUrl(options) {
530
682
  const explicit = normalizeUrl(options.explicitBaseUrl || '');
531
683
  if (explicit)
532
684
  return explicit;
533
- const xiaoEDefault = resolveXiaoEDefaultBaseUrl(env);
534
- if (hasXiaoEEnv(env))
535
- return xiaoEDefault || normalizeUrl(options.profileBaseUrl || '');
536
- return normalizeUrl(options.profileBaseUrl || '') || xiaoEDefault;
685
+ const configured = resolveXiaoEConfiguredBaseUrl(env);
686
+ if (configured)
687
+ return configured;
688
+ const profileBaseUrl = normalizeUrl(options.profileBaseUrl || '');
689
+ if (profileBaseUrl)
690
+ return profileBaseUrl;
691
+ return hasXiaoEEnv(env) ? DEFAULT_E10_BASE_URL : '';
692
+ }
693
+ export function shouldUseXiaoEBrowserFallback(platform = process.platform) {
694
+ return platform === 'darwin' || platform === 'win32';
695
+ }
696
+ export function createXiaoEUnifiedLoginUnavailableError(baseUrl = '') {
697
+ const target = normalizeUrl(baseUrl);
698
+ const message = [
699
+ 'XiaoE unified login did not return a usable E10 session.',
700
+ target ? `Resolved E10 baseUrl: ${target}.` : 'No E10 baseUrl could be resolved.',
701
+ 'This web/Linux host cannot complete a localhost browser OAuth callback.',
702
+ 'Refresh the XiaoE web session or restore host credential injection before retrying.',
703
+ ].join(' ');
704
+ return Object.assign(new CliError(message, ExitCode.AuthError), {
705
+ type: 'authentication',
706
+ subtype: 'xiaoe_web_credential_unavailable',
707
+ retryable: false,
708
+ stage: 'xiaoe_unified_login',
709
+ });
537
710
  }
538
711
  async function resolveUserInfo(baseUrl, eteamsId, rawCookie = '', agentType = '') {
539
712
  const resp = await fetch(`${baseUrl}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(baseUrl)}`, {
@@ -549,11 +722,11 @@ async function resolveUserInfo(baseUrl, eteamsId, rawCookie = '', agentType = ''
549
722
  redirect: 'manual',
550
723
  });
551
724
  const data = await resp.json().catch(() => ({}));
552
- return readTeamsCheckUserInfo(resp, data);
725
+ return { userInfo: readTeamsCheckUserInfo(resp, data), setCookie: extractRawSetCookie(resp) };
553
726
  }
554
727
  async function saveXiaoEUnifiedLogin(input) {
555
728
  const baseUrl = normalizeUrl(input.baseUrl);
556
- const userInfo = await resolveUserInfo(baseUrl, input.eteamsId, input.rawCookie || '', XIAOE_AGENT_TYPE);
729
+ const { userInfo, setCookie } = await resolveUserInfo(baseUrl, input.eteamsId, input.rawCookie || '', XIAOE_AGENT_TYPE);
557
730
  if (!userInfo.userId) {
558
731
  throw new CliError('Could not resolve userId. Check whether XiaoE unified login is valid for this E10 domain.', ExitCode.AuthError);
559
732
  }
@@ -563,6 +736,7 @@ async function saveXiaoEUnifiedLogin(input) {
563
736
  const cookies = { ETEAMSID: input.eteamsId };
564
737
  if (input.rawCookie)
565
738
  cookies.raw = input.rawCookie;
739
+ const ztSession = extractSessionTicket(setCookie, input.rawCookie || '');
566
740
  const authData = {
567
741
  cookies,
568
742
  userId: userInfo.userId,
@@ -580,6 +754,7 @@ async function saveXiaoEUnifiedLogin(input) {
580
754
  credentialSourceStep: input.credentialSource.credentialSourceStep,
581
755
  credentialSourceType: input.credentialSource.credentialSourceType,
582
756
  credentialSourceLabel: input.credentialSource.credentialSourceLabel,
757
+ ...(ztSession ? { ztSession } : {}),
583
758
  };
584
759
  const authFile = saveAuthFile(authData, profile);
585
760
  saveProfileConfig(baseUrl, baseUrl, profile, tenantKey);
@@ -606,6 +781,7 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
606
781
  const explicitBaseUrl = normalizeUrl(options.baseUrl || '');
607
782
  const baseUrl = resolveXiaoEPreferredBaseUrl({
608
783
  explicitBaseUrl,
784
+ profileBaseUrl: options.profileBaseUrl,
609
785
  env,
610
786
  });
611
787
  const mayUseTokenScript = options.allowTokenScript || hasXiaoEEnv(env);
@@ -624,7 +800,31 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
624
800
  options.logger?.warn('XiaoE platform credential failed. ' + message);
625
801
  }
626
802
  if (!credential) {
627
- options.logger?.warn('XiaoE platform credential did not return usable ETEAMSID; will try xiaoeUserData/tokens.py next.');
803
+ options.logger?.warn(options.allowBundledEnvScript
804
+ ? 'XiaoE platform credential did not return usable ETEAMSID; will try the CLI-bundled xiaoe_env.py compatibility path next.'
805
+ : 'XiaoE platform credential did not return usable ETEAMSID; will try xiaoeUserData/tokens.py next.');
806
+ }
807
+ }
808
+ // Step 1b — compatibility path used by the reference login package. The
809
+ // script is shipped with the CLI npm package (never with a Skill) and its
810
+ // stdout stays inside this process so credentials are not exposed to agents.
811
+ if (!credential && options.allowBundledEnvScript && hasXiaoEPlatformCredential(env)) {
812
+ const scriptPath = xiaoEBundledEnvScriptPath();
813
+ if (!scriptPath) {
814
+ options.logger?.warn('CLI-bundled xiaoe_env.py was not found; will try desktop XiaoE credential sources next.');
815
+ }
816
+ else {
817
+ options.logger?.info('Trying CLI-bundled xiaoe_env.py compatibility credential...');
818
+ try {
819
+ credential = await resolveXiaoEBundledEnvScriptCredential(baseUrl || DEFAULT_E10_BASE_URL, env);
820
+ }
821
+ catch (error) {
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ options.logger?.warn('CLI-bundled xiaoe_env.py failed. ' + message);
824
+ }
825
+ if (!credential) {
826
+ options.logger?.warn('CLI-bundled xiaoe_env.py did not return usable ETEAMSID; will try xiaoeUserData/tokens.py next.');
827
+ }
628
828
  }
629
829
  }
630
830
  // Step 2 — host tokens.py (desktop host only; Linux sandboxes have no such path).
@@ -647,8 +847,8 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
647
847
  }
648
848
  }
649
849
  // Step 3 — built-in Node equivalent of tokens.py: reads the local XiaoE
650
- // client session and exchanges it for an eteamId, so a Python runtime
651
- // without `requests` no longer blocks unified login.
850
+ // client session and exchanges it for an eteamId, so Python availability
851
+ // no longer blocks unified login.
652
852
  if (!credential && hasXiaoEClientCredential(env)) {
653
853
  try {
654
854
  credential = await resolveXiaoEClientCredential(baseUrl || DEFAULT_E10_BASE_URL, env);
@@ -1,16 +1,19 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
- import { E10AuthSession } from './auth/session.js';
3
- import { hasXiaoEEnv, hasXiaoETokenScript, tryXiaoEUnifiedLogin } from './auth/xiaoe.js';
2
+ import { E10AuthSession, getActiveProfile, readProfileConfig } from './auth/session.js';
3
+ import { createXiaoEUnifiedLoginUnavailableError, hasXiaoEEnv, hasXiaoETokenScript, resolveXiaoEPreferredBaseUrl, shouldUseXiaoEBrowserFallback, tryXiaoEUnifiedLogin, } from './auth/xiaoe.js';
4
4
  import { E10ApiClient } from './client.js';
5
5
  function shouldPreferXiaoEUnifiedLogin(options, env) {
6
6
  return !options.auth && !options.profile && (hasXiaoEEnv(env) || hasXiaoETokenScript(env));
7
7
  }
8
- export async function getE10Session(ctx) {
8
+ export async function getE10Session(ctx, load = {}) {
9
9
  const options = ctx.getGlobalOptions();
10
10
  if (shouldPreferXiaoEUnifiedLogin(options, ctx.env)) {
11
+ const profileBaseUrl = readProfileConfig(getActiveProfile()).baseUrl;
11
12
  const xiaoELogin = await tryXiaoEUnifiedLogin({
12
13
  env: ctx.env,
13
14
  baseUrl: options.baseUrl,
15
+ profileBaseUrl,
16
+ allowBundledEnvScript: true,
14
17
  allowTokenScript: true,
15
18
  logger: ctx.logger,
16
19
  });
@@ -18,11 +21,20 @@ export async function getE10Session(ctx) {
18
21
  return xiaoELogin.session;
19
22
  return null;
20
23
  }
21
- return E10AuthSession.fromOptions(options, ctx.cwd);
24
+ return E10AuthSession.fromOptions(options, ctx.cwd, load);
22
25
  }
23
- export async function requireE10Session(ctx) {
24
- const session = await getE10Session(ctx);
26
+ export async function requireE10Session(ctx, load = {}) {
27
+ const session = await getE10Session(ctx, load);
25
28
  if (!session) {
29
+ const options = ctx.getGlobalOptions();
30
+ if ((hasXiaoEEnv(ctx.env) || hasXiaoETokenScript(ctx.env)) && !shouldUseXiaoEBrowserFallback()) {
31
+ const baseUrl = resolveXiaoEPreferredBaseUrl({
32
+ explicitBaseUrl: options.baseUrl,
33
+ profileBaseUrl: readProfileConfig(getActiveProfile()).baseUrl,
34
+ env: ctx.env,
35
+ });
36
+ throw createXiaoEUnifiedLoginUnavailableError(baseUrl);
37
+ }
26
38
  throw new CliError('E10 auth not found. In XiaoE, run weaver-work-cli auth xiaoe first; if unified login is unavailable, ask the user for the E10 login domain (base URL), then run: weaver-work-cli auth login --base-url <url> --agent_type <agent_type>.', ExitCode.AuthError);
27
39
  }
28
40
  return session;
@@ -28,10 +28,12 @@ export class E10RequestRuntime {
28
28
  session;
29
29
  interceptors;
30
30
  fetchFn;
31
+ attachSessionTicket;
31
32
  constructor(ctx, session, dependencies = {}) {
32
33
  this.ctx = ctx;
33
34
  this.session = session;
34
35
  this.fetchFn = dependencies.fetch || globalThis.fetch;
36
+ this.attachSessionTicket = dependencies.attachSessionTicket === true;
35
37
  this.interceptors = [
36
38
  ...(this.ctx.hooks?.getE10RequestInterceptors?.() || []),
37
39
  ...(dependencies.interceptors || []),
@@ -137,7 +139,7 @@ export class E10RequestRuntime {
137
139
  langType: 'zh_CN',
138
140
  timeZoneOffset: String(new Date().getTimezoneOffset()),
139
141
  ...request.headers,
140
- Cookie: this.session.cookieHeader(),
142
+ Cookie: this.attachSessionTicket ? this.session.cookieHeaderWithSessionTicket() : this.session.cookieHeader(),
141
143
  eteamsid: this.session.eteamsId,
142
144
  'User-Agent': this.session.uaHeader(),
143
145
  Origin: this.session.getBaseUrl(),
@@ -30,7 +30,7 @@ export async function verifyLease(args, action, env) {
30
30
  }
31
31
  const lease = await env.continuation.verify(args.continuation.trim(), action);
32
32
  const session = await env.host.getSession();
33
- if (lease.baseUrl !== session.baseUrl || lease.userId !== session.userId) {
33
+ if ((lease.baseUrl && lease.baseUrl !== session.baseUrl) || (lease.userId && lease.userId !== session.userId)) {
34
34
  throw caizhilianValidationError('continuation_context_mismatch', '确认上下文已变化(环境或登录账号不同),请重新执行 prepare');
35
35
  }
36
36
  return lease;