weaver-work-cli 0.1.38 → 0.1.40

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 (166) hide show
  1. package/README.md +7 -3
  2. package/dist/chunks/{archive-PDV724YK.js → archive-JHTONRST.js} +556 -66
  3. package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-FCHLCPHC.js} +2 -2
  4. package/dist/chunks/{asset-PDL2QE2N.js → asset-RLIHC7DB.js} +2 -2
  5. package/dist/chunks/asset-idle-KVOFTLCP.js +670 -0
  6. package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RZYDB3MB.js} +3 -3
  7. package/dist/chunks/{blog-UYR3EUXW.js → blog-42OZTJNP.js} +2 -2
  8. package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-KRK547K5.js} +2 -2
  9. package/dist/chunks/{calendar-FG4BAZHE.js → calendar-RTWAMKHD.js} +2 -2
  10. package/dist/chunks/{chunk-J7USS3ED.js → chunk-4CTESSKI.js} +4 -4
  11. package/dist/chunks/{chunk-OJQUJZQF.js → chunk-4TEDNCON.js} +45 -5
  12. package/dist/chunks/chunk-57IECMWX.js +487 -0
  13. package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-7BRU4EFH.js} +13 -7
  14. package/dist/chunks/{chunk-6LTFIFLM.js → chunk-7E6MUOEK.js} +2 -2
  15. package/dist/chunks/{chunk-KKRL3REV.js → chunk-7U577Q7U.js} +2 -2
  16. package/dist/chunks/{chunk-UJJD4QWF.js → chunk-CAQNBBQA.js} +2 -2
  17. package/dist/chunks/{chunk-CORFRD7Q.js → chunk-CXRQWOZ5.js} +2 -2
  18. package/dist/chunks/{chunk-GGJFMNJM.js → chunk-FLS4DPGP.js} +110 -16
  19. package/dist/chunks/{chunk-JSMFPO43.js → chunk-I566ZYNF.js} +1 -1
  20. package/dist/chunks/{chunk-6FTTSA45.js → chunk-IZSSVYAG.js} +2 -2
  21. package/dist/chunks/{chunk-XJY7G4DD.js → chunk-JWRZM2H2.js} +3 -3
  22. package/dist/chunks/{chunk-NI6GIPIM.js → chunk-LFMSDBZN.js} +529 -395
  23. package/dist/chunks/{chunk-VRYHGG6O.js → chunk-LN447PY2.js} +92 -51
  24. package/dist/chunks/{chunk-NR6RKXPA.js → chunk-NETCCARD.js} +4 -2
  25. package/dist/chunks/{chunk-Z32KWY6G.js → chunk-NJKREDA3.js} +1 -1
  26. package/dist/chunks/{chunk-FUWVHMVV.js → chunk-SPRHHSVC.js} +2 -2
  27. package/dist/chunks/{chunk-IZUDA353.js → chunk-T6EGVBKU.js} +1 -1
  28. package/dist/chunks/{chunk-DMV2TOMH.js → chunk-VNBU55SL.js} +428 -145
  29. package/dist/chunks/{chunk-UZZBA6PV.js → chunk-WFFNM65B.js} +47 -6
  30. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  31. package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-VR55AOOO.js} +2 -2
  32. package/dist/chunks/{contract-XIRAFTEV.js → contract-HI4FNPYE.js} +2 -2
  33. package/dist/chunks/{cowork-BLG62BL2.js → cowork-HJLI353D.js} +12 -5
  34. package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-HTGI63UU.js} +3 -3
  35. package/dist/chunks/{ehr-2STW5ILH.js → ehr-GYXIYH3A.js} +2 -2
  36. package/dist/chunks/env-DW3G4HUO.js +19 -0
  37. package/dist/chunks/{esb-UUDQUNKY.js → esb-I5INCEFE.js} +4 -4
  38. package/dist/chunks/factory-TKXGXS6L.js +30 -0
  39. package/dist/chunks/{fna-6RCY4NU5.js → fna-K2MHTQ24.js} +2 -2
  40. package/dist/chunks/group-chat-VYBNMJLG.js +18 -0
  41. package/dist/chunks/{host-FWDOSRH5.js → host-256GVCAH.js} +3 -3
  42. package/dist/chunks/{host-EJZFROU3.js → host-D5SERMAJ.js} +3 -3
  43. package/dist/chunks/{host-RHM3NPXV.js → host-JM4YVHFM.js} +3 -3
  44. package/dist/chunks/{host-7RXEDMAH.js → host-L3VLITMT.js} +3 -3
  45. package/dist/chunks/{host-UFF33TWJ.js → host-ZUQTJYYK.js} +3 -3
  46. package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-VLRBKTZB.js} +3 -3
  47. package/dist/chunks/{im-YJAXS7CO.js → im-QK3VTLXK.js} +8 -8
  48. package/dist/chunks/{invoice-HWVSXGX6.js → invoice-T5JTA32H.js} +3 -3
  49. package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-P2FNEQVZ.js} +2 -2
  50. package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-XRRI46M6.js} +5 -5
  51. package/dist/chunks/{mail-B42RRU7G.js → mail-3Y7MJ6PS.js} +9 -4
  52. package/dist/chunks/{meeting-CFYVBJSS.js → meeting-M7LTXGJE.js} +11 -10
  53. package/dist/chunks/{okr-GCLF6V7W.js → okr-OBC6S67L.js} +2 -2
  54. package/dist/chunks/{plan-ZVL7GBRI.js → plan-5H6I53SK.js} +2 -2
  55. package/dist/chunks/{project-4YWAAE2K.js → project-L75QZV4S.js} +2 -2
  56. package/dist/chunks/{registry-EP4VTHZS.js → registry-KMHCPAAD.js} +9 -8
  57. package/dist/chunks/{registry-OZSMDVPP.js → registry-QGHFNUWS.js} +2 -2
  58. package/dist/chunks/{registry-FRJ6H7BT.js → registry-RXPN6BBT.js} +4 -4
  59. package/dist/chunks/{registry-SG2PGVCF.js → registry-UGRBCBHO.js} +3 -3
  60. package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-A3UDC3CA.js} +4 -4
  61. package/dist/chunks/{schedule-NIJBTYDO.js → schedule-5GCACCK7.js} +4 -4
  62. package/dist/chunks/{workflow-BB24DFBY.js → workflow-M5ZJ7DWF.js} +11 -8
  63. package/dist/core/output.js +16 -6
  64. package/dist/index.js +33 -28
  65. package/dist/internal/e10/auth/commands.js +109 -56
  66. package/dist/internal/e10/auth/messages.js +1 -0
  67. package/dist/internal/e10/auth/session.js +11 -1
  68. package/dist/internal/e10/auth/xiaoe.js +29 -4
  69. package/dist/internal/e10/auth-recovery.js +13 -6
  70. package/dist/internal/e10/client.js +13 -0
  71. package/dist/internal/e10/context.js +24 -3
  72. package/dist/internal/e10/request-runtime.js +12 -1
  73. package/dist/shortcuts/archive/host.js +2 -0
  74. package/dist/shortcuts/archive/manifest.js +1 -1
  75. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  76. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  77. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  78. package/dist/shortcuts/archive/operations/registry.js +2 -0
  79. package/dist/shortcuts/archive/operations/upload.js +1 -1
  80. package/dist/shortcuts/asset-idle/errors.js +44 -0
  81. package/dist/shortcuts/asset-idle/host.js +161 -0
  82. package/dist/shortcuts/asset-idle/index.js +114 -0
  83. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  84. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  85. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  86. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  87. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  88. package/dist/shortcuts/asset-idle/operations.js +15 -0
  89. package/dist/shortcuts/cowork/operations/item-write.js +10 -2
  90. package/dist/shortcuts/index.js +5 -0
  91. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  92. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -1
  93. package/dist/shortcuts/meeting/operations/eb/create-flow.js +381 -104
  94. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  95. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +583 -0
  97. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +19 -3
  99. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  100. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  101. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  102. package/dist/shortcuts/workflow/host.js +3 -0
  103. package/dist/shortcuts/workflow/index.js +1 -1
  104. package/dist/shortcuts/workflow/operations/form.js +65 -171
  105. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +129 -27
  106. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  107. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +60 -117
  108. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  109. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  110. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +48 -8
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +17 -9
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +5 -2
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +122 -20
  118. package/docs/_catalog.md +3 -0
  119. package/docs/asset-idle.md +45 -0
  120. package/docs/e10-auth.md +16 -1
  121. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  122. package/package.json +1 -1
  123. package/scripts/mcp-meeting.mjs +234 -0
  124. package/scripts/package-skill.mjs +2 -0
  125. package/skill-template/business-info.json +14 -1
  126. package/skill-template/domains/asset-idle.md +13 -0
  127. package/skill-template/domains/wenshuding.md +3 -0
  128. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  129. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  130. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  131. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  132. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  133. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  134. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  135. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  136. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  137. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  138. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  139. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  140. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  141. package/skills/weaver-e10-cowork/references/cowork-write.md +1 -1
  142. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  143. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  144. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  145. package/skills/weaver-e10-mail/SKILL.md +3 -0
  146. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  147. package/skills/weaver-e10-meeting/SKILL.md +88 -25
  148. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  149. package/skills/weaver-e10-meeting.zip +0 -0
  150. package/skills/weaver-e10-shared/SKILL.md +5 -5
  151. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  152. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  153. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  154. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  155. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  156. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  159. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  160. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  161. package/skills/weaver-e10-workflow/SKILL.md +9 -2
  162. package/skills/weaver-e10-workflow/aigroup/default.json +166 -19
  163. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +72 -34
  164. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  165. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +350 -50
  166. package/dist/chunks/{chunk-2LV74IWP.js → chunk-E7OK5WD5.js} +5 -5
@@ -17,6 +17,7 @@ const AGENT_RETURN_URL_ENV_KEY = 'WEAVER_AGENT_RETURN_URL';
17
17
  const AGENT_RETURN_PATH = '/agent-return';
18
18
  const AGENT_RETURN_TOKEN_HEADER = 'x-weaver-agent-return-token';
19
19
  const AGENT_RETURN_TIMEOUT_MS = 900_000;
20
+ const OAUTH_CALLBACK_PROTOCOL_MISMATCH_GRACE_MS = 30_000;
20
21
  const UNSAFE_AGENT_RETURN_PROTOCOLS = new Set([
21
22
  'about:',
22
23
  'blob:',
@@ -191,6 +192,7 @@ function formatAuthStatus(status) {
191
192
  ...('credentialSourceStep' in status && status.credentialSourceStep ? ['credentialSourceStep: ' + status.credentialSourceStep] : []),
192
193
  ...('credentialSourceType' in status && status.credentialSourceType ? ['credentialSourceType: ' + status.credentialSourceType] : []),
193
194
  ...('credentialSourceLabel' in status && status.credentialSourceLabel ? ['credentialSourceLabel: ' + status.credentialSourceLabel] : []),
195
+ ...('expiryTimes' in status && status.expiryTimes ? ['expiryTimes: ' + status.expiryTimes] : []),
194
196
  ];
195
197
  if ('valid' in status) {
196
198
  lines.push('valid: ' + String(status.valid));
@@ -210,6 +212,7 @@ function formatLoginResult(result) {
210
212
  ...(result.credentialSourceStep ? ['credentialSourceStep: ' + result.credentialSourceStep] : []),
211
213
  ...(result.credentialSourceType ? ['credentialSourceType: ' + result.credentialSourceType] : []),
212
214
  ...(result.credentialSourceLabel ? ['credentialSourceLabel: ' + result.credentialSourceLabel] : []),
215
+ ...(result.expiryTimes ? ['expiryTimes: ' + result.expiryTimes] : []),
213
216
  ].join('\n');
214
217
  }
215
218
  async function clearInvoiceTokenCache(profile, authData) {
@@ -300,6 +303,24 @@ export function buildE10AuthorizeUrl(baseUrl, redirectUri, agentType) {
300
303
  });
301
304
  return `${baseUrl}/papi/sso/oauth2.0/authorize?${authorizeParams.toString()}`;
302
305
  }
306
+ /** Node's HTTP parser exposes the first unread bytes when TLS is sent to an HTTP listener. */
307
+ export function isTlsClientHello(error) {
308
+ if (!error || typeof error !== 'object')
309
+ return false;
310
+ const rawPacket = error.rawPacket;
311
+ return Buffer.isBuffer(rawPacket)
312
+ && rawPacket.length >= 3
313
+ && rawPacket[0] === 0x16
314
+ && rawPacket[1] === 0x03;
315
+ }
316
+ export function createOAuthCallbackProtocolMismatchError(port) {
317
+ return Object.assign(new CliError(`OAuth callback protocol mismatch: the browser connected to https://127.0.0.1:${port}, but the CLI callback listener uses HTTP. Retry the login and, on the callback error page, change only the URL scheme to http:// before reloading; keep the path and query unchanged. The E10 authorization service should preserve HTTP loopback redirect URIs.`, ExitCode.AuthError), {
318
+ type: 'authentication',
319
+ subtype: 'oauth_callback_protocol_mismatch',
320
+ retryable: false,
321
+ stage: 'oauth_callback',
322
+ });
323
+ }
303
324
  async function readJsonObject(resp) {
304
325
  const data = await resp.json().catch(() => ({}));
305
326
  return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
@@ -488,7 +509,7 @@ function xiaoEUnifiedLoginUnavailableMessage(baseUrl, attempted) {
488
509
  'CLI 会优先使用 XiaoE 客户端内置 Python 执行 tokens.py;自动发现不可用时,可用 WEAVER_XIAOE_TOKEN_SCRIPT_PYTHON 指定兼容解释器。',
489
510
  ].join(' ');
490
511
  }
491
- async function runBrowserLogin(ctx, options, behavior = {}) {
512
+ async function runXiaoELogin(ctx, options) {
492
513
  const commandOpts = asOptions(options);
493
514
  const globalOptions = ctx.getGlobalOptions();
494
515
  const profileFromGlobal = globalOptions.profile;
@@ -499,59 +520,49 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
499
520
  profileBaseUrl: cfg.baseUrl,
500
521
  env: ctx.env,
501
522
  });
502
- const xiaoESources = behavior.tryXiaoEUnifiedLogin === true
503
- ? inspectXiaoEUnifiedLoginSources({
504
- env: ctx.env,
505
- baseUrl: preferredXiaoEBaseUrl,
506
- profileBaseUrl: cfg.baseUrl,
507
- allowTokenScript: behavior.allowXiaoETokenScript === true,
508
- })
509
- : {
510
- platformCredential: false,
511
- tokenScript: false,
512
- clientCredential: false,
513
- environmentCredential: false,
514
- any: false,
515
- };
516
- const useXiaoELogin = behavior.tryXiaoEUnifiedLogin === true && xiaoESources.any;
517
- const baseUrl = behavior.tryXiaoEUnifiedLogin
518
- ? preferredXiaoEBaseUrl
519
- : normalizeUrl(explicitBaseUrl || cfg.baseUrl || '');
523
+ const xiaoESources = inspectXiaoEUnifiedLoginSources({
524
+ env: ctx.env,
525
+ baseUrl: preferredXiaoEBaseUrl,
526
+ profileBaseUrl: cfg.baseUrl,
527
+ allowTokenScript: true,
528
+ });
529
+ const useXiaoELogin = xiaoESources.any;
530
+ const baseUrl = preferredXiaoEBaseUrl;
531
+ const tenantKey = optionString(commandOpts, 'tenantKey');
532
+ ctx.logger.info([
533
+ 'XiaoE unified login preflight:',
534
+ 'authMode=xiaoe',
535
+ `credentialSourceDetected=${String(xiaoESources.any)}`,
536
+ `attempted=${String(useXiaoELogin)}`,
537
+ `platformCredentialDetected=${String(xiaoESources.platformCredential)}`,
538
+ `tokenScriptDetected=${String(xiaoESources.tokenScript)}`,
539
+ `clientSettingsDetected=${String(xiaoESources.clientCredential)}`,
540
+ `environmentCredentialDetected=${String(xiaoESources.environmentCredential)}`,
541
+ `baseUrl=${baseUrl || '(unresolved)'}`,
542
+ ].join(' '));
543
+ if (useXiaoELogin) {
544
+ const xiaoEResult = await tryXiaoEUnifiedLogin(ctx, baseUrl, tenantKey, profileFromGlobal, true);
545
+ if (xiaoEResult)
546
+ return xiaoEResult;
547
+ }
548
+ ctx.logger.warn(xiaoEUnifiedLoginUnavailableMessage(baseUrl, useXiaoELogin));
549
+ throw createXiaoEUnifiedLoginUnavailableError(baseUrl);
550
+ }
551
+ async function runBrowserLogin(ctx, options) {
552
+ const commandOpts = asOptions(options);
553
+ const globalOptions = ctx.getGlobalOptions();
554
+ const profileFromGlobal = globalOptions.profile;
555
+ const cfg = readProfileConfig(profileFromGlobal || getActiveProfile());
556
+ const explicitBaseUrl = optionString(commandOpts, 'baseUrl') || globalOptions.baseUrl || '';
557
+ const baseUrl = normalizeUrl(explicitBaseUrl || cfg.baseUrl || '');
520
558
  const tenantKey = optionString(commandOpts, 'tenantKey');
521
559
  const portText = optionString(commandOpts, 'port') || '19800';
522
560
  const port = Number.parseInt(portText, 10);
523
561
  const explicitAgentType = optionString(commandOpts, 'agentType', 'agent_type');
524
- const agentType = useXiaoELogin
525
- ? optionString(commandOpts, 'agentType', 'agent_type') || XIAOE_AGENT_TYPE
526
- : resolveAgentType(commandOpts, ctx.env);
562
+ const agentType = resolveAgentType(commandOpts, ctx.env);
527
563
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
528
564
  throw new CliError('Invalid --port value', ExitCode.UsageError);
529
565
  }
530
- if (behavior.tryXiaoEUnifiedLogin) {
531
- ctx.logger.info([
532
- 'XiaoE unified login preflight:',
533
- 'authMode=xiaoe',
534
- `credentialSourceDetected=${String(xiaoESources.any)}`,
535
- `attempted=${String(useXiaoELogin)}`,
536
- `platformCredentialDetected=${String(xiaoESources.platformCredential)}`,
537
- `tokenScriptDetected=${String(xiaoESources.tokenScript)}`,
538
- `clientSettingsDetected=${String(xiaoESources.clientCredential)}`,
539
- `environmentCredentialDetected=${String(xiaoESources.environmentCredential)}`,
540
- `baseUrl=${baseUrl || '(unresolved)'}`,
541
- ].join(' '));
542
- }
543
- if (useXiaoELogin) {
544
- const xiaoEResult = await tryXiaoEUnifiedLogin(ctx, baseUrl, tenantKey, profileFromGlobal, behavior.allowXiaoETokenScript === true);
545
- if (xiaoEResult)
546
- return xiaoEResult;
547
- }
548
- const willOpenBrowser = behavior.browserFallback !== 'never';
549
- if (behavior.tryXiaoEUnifiedLogin && behavior.explainXiaoEFallback) {
550
- ctx.logger.warn(xiaoEUnifiedLoginUnavailableMessage(baseUrl, useXiaoELogin));
551
- }
552
- if (!willOpenBrowser) {
553
- throw createXiaoEUnifiedLoginUnavailableError(baseUrl);
554
- }
555
566
  if (!baseUrl)
556
567
  throw new CliError('Missing --base-url', ExitCode.UsageError);
557
568
  const agentReturnTarget = resolveAgentReturnTarget(commandOpts, ctx.env, agentType);
@@ -564,6 +575,7 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
564
575
  let settled = false;
565
576
  let timer = null;
566
577
  let pendingLoginResult = null;
578
+ let tlsCallbackDetected = false;
567
579
  const finish = (error, result) => {
568
580
  if (settled)
569
581
  return;
@@ -647,6 +659,13 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
647
659
  finish(new CliError('No authorization code received.', ExitCode.AuthError));
648
660
  return;
649
661
  }
662
+ // The user followed the recovery instruction and reached the HTTP
663
+ // callback. Do not let the protocol-mismatch grace timer race the token
664
+ // exchange that is now legitimately in progress.
665
+ if (tlsCallbackDetected && timer) {
666
+ clearTimeout(timer);
667
+ timer = null;
668
+ }
650
669
  try {
651
670
  const result = await oidcExchange(code, redirectUri, baseUrl, tenantKey, redirectAgentType, profileFromGlobal);
652
671
  const responseHeaders = {
@@ -685,6 +704,26 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
685
704
  : error.message;
686
705
  finish(new CliError(message, ExitCode.UsageError));
687
706
  });
707
+ server.on('clientError', (error, socket) => {
708
+ if (isTlsClientHello(error)) {
709
+ if (!tlsCallbackDetected) {
710
+ tlsCallbackDetected = true;
711
+ ctx.logger.warn([
712
+ `OAuth callback protocol mismatch: the browser used HTTPS for 127.0.0.1:${port}, while the CLI listener is HTTP.`,
713
+ '在 30 秒内把浏览器当前地址开头明确改为 http://,保留后面的完整路径和参数,然后回车重试。',
714
+ '不要把地址中的授权码复制到聊天或日志。',
715
+ ].join(' '));
716
+ if (timer)
717
+ clearTimeout(timer);
718
+ timer = setTimeout(() => {
719
+ finish(createOAuthCallbackProtocolMismatchError(port));
720
+ }, OAUTH_CALLBACK_PROTOCOL_MISMATCH_GRACE_MS);
721
+ }
722
+ socket.destroy();
723
+ return;
724
+ }
725
+ socket.destroy();
726
+ });
688
727
  server.listen(port, '127.0.0.1', () => {
689
728
  ctx.logger.info('Local callback server: ' + redirectUri);
690
729
  openUrlInBrowser(loginUrl, ctx);
@@ -728,7 +767,7 @@ function formatWhoami(title, data) {
728
767
  'User-Agent: ' + (data.userAgent || ''),
729
768
  ].join('\n');
730
769
  }
731
- function registerBrowserLoginCommand(auth, action, name, description, behavior = {}) {
770
+ function registerBrowserLoginCommand(auth, action, name, description) {
732
771
  const command = auth.command(name)
733
772
  .description(description)
734
773
  .option('--base-url <url>', 'E10 base URL')
@@ -738,7 +777,23 @@ function registerBrowserLoginCommand(auth, action, name, description, behavior =
738
777
  .option('--agent-type <value>', 'Alias of --agent_type')
739
778
  .option('--agent-return-url <url>', 'Desktop application URL linked by the login success page');
740
779
  command.action(action(async (ctx, options) => {
741
- const result = await runBrowserLogin(ctx, options, behavior);
780
+ const result = await runBrowserLogin(ctx, options);
781
+ ctx.output.write(result, { text: formatLoginResult(result) });
782
+ }));
783
+ }
784
+ function registerXiaoELoginCommand(auth, action) {
785
+ auth.command('xiaoe')
786
+ .description('Use XiaoE unified login without browser OAuth fallback')
787
+ .option('--base-url <url>', 'E10 base URL')
788
+ .option('--tenant-key <key>', 'Tenant key fallback after login')
789
+ // Keep previously accepted browser flags as no-ops so existing wrappers do
790
+ // not break. The dedicated handler never parses them or opens a callback.
791
+ .option('--port <n>', 'Deprecated compatibility option; ignored by XiaoE unified login', '19800')
792
+ .option('--agent_type <value>', 'Deprecated compatibility option; ignored by XiaoE unified login')
793
+ .option('--agent-type <value>', 'Alias of --agent_type')
794
+ .option('--agent-return-url <url>', 'Deprecated compatibility option; ignored by XiaoE unified login')
795
+ .action(action(async (ctx, options) => {
796
+ const result = await runXiaoELogin(ctx, options);
742
797
  ctx.output.write(result, { text: formatLoginResult(result) });
743
798
  }));
744
799
  }
@@ -758,7 +813,9 @@ export function registerE10AuthCommands(parent, action) {
758
813
  const commandOpts = asOptions(options);
759
814
  const authFile = findAuthFile(ctx.cwd, opts);
760
815
  const session = authFile ? E10AuthSession.fromFile(authFile, opts.baseUrl, opts.passportUrl) : null;
761
- const valid = session && commandOpts.check !== false ? await session.checkValid() : undefined;
816
+ const valid = session && commandOpts.check !== false
817
+ ? session.isExpired() ? false : await session.checkValid()
818
+ : undefined;
762
819
  const status = session
763
820
  ? {
764
821
  loggedIn: true,
@@ -779,6 +836,7 @@ export function registerE10AuthCommands(parent, action) {
779
836
  ...(session.credentialSourceStep ? { credentialSourceStep: session.credentialSourceStep } : {}),
780
837
  ...(session.credentialSourceType ? { credentialSourceType: session.credentialSourceType } : {}),
781
838
  ...(session.credentialSourceLabel ? { credentialSourceLabel: session.credentialSourceLabel } : {}),
839
+ ...(session.expiryTimes ? { expiryTimes: session.expiryTimes } : {}),
782
840
  valid,
783
841
  }
784
842
  : {
@@ -823,12 +881,7 @@ export function registerE10AuthCommands(parent, action) {
823
881
  }));
824
882
  registerBrowserLoginCommand(auth, action, 'login', 'Open browser OAuth2.0 login and save E10 auth');
825
883
  registerBrowserLoginCommand(auth, action, 'oidc', 'Alias of auth login for OAuth2.0 browser login');
826
- registerBrowserLoginCommand(auth, action, 'xiaoe', 'Use XiaoE unified login without browser OAuth fallback', {
827
- tryXiaoEUnifiedLogin: true,
828
- allowXiaoETokenScript: true,
829
- explainXiaoEFallback: true,
830
- browserFallback: 'never',
831
- });
884
+ registerXiaoELoginCommand(auth, action);
832
885
  auth.command('logout')
833
886
  .alias('clear')
834
887
  .description('Clear saved E10 auth for the current or selected profile')
@@ -1,3 +1,4 @@
1
1
  export const E10_REAUTH_GUIDANCE = '请结合当前宿主环境选择合适的登录方式,恢复登录后重试原请求。';
2
2
  export const E10_AUTH_REQUIRED_MESSAGE = `E10 登录态不可用。${E10_REAUTH_GUIDANCE}`;
3
+ export const E10_XIAOE_RECOVERY_FAILED_MESSAGE = 'E10 登录态不可用。已尝试小 e 自动重新获取登录信息,但未取得可用登录态。请在小 e 中恢复登录后重试。';
3
4
  export const E10_SESSION_EXPIRED_MESSAGE = `E10 登录态已失效,本次操作已停止。${E10_REAUTH_GUIDANCE}`;
@@ -337,6 +337,15 @@ export class E10AuthSession {
337
337
  get credentialSourceLabel() {
338
338
  return this.data.credentialSourceLabel || '';
339
339
  }
340
+ get expiryTimes() {
341
+ const value = Number(this.data.expiryTimes);
342
+ return Number.isFinite(value) && value > 0 ? value : undefined;
343
+ }
344
+ /** XiaoE 换取的 ETEAMSID 是否已经超过其明确的失效时间。 */
345
+ isExpired(now = Date.now()) {
346
+ const expiryTimes = this.expiryTimes;
347
+ return expiryTimes !== undefined && now >= expiryTimes;
348
+ }
340
349
  getBaseUrl() {
341
350
  return (this.data.baseUrl || '').replace(/\/$/, '');
342
351
  }
@@ -389,6 +398,7 @@ export class E10AuthSession {
389
398
  stTicket: this.data.stTicket,
390
399
  redirectUrl: this.data.redirectUrl,
391
400
  cookieSavedAt: this.data.cookieSavedAt,
401
+ expiryTimes: this.expiryTimes,
392
402
  mainUrl: this.data.mainUrl,
393
403
  ...sourceInfo,
394
404
  hasEteamsId: Boolean(this.eteamsId),
@@ -419,7 +429,7 @@ export class E10AuthSession {
419
429
  return null;
420
430
  const session = E10AuthSession.fromFile(authPath, baseUrl, passportUrl);
421
431
  // 富化只是"刷新/纠偏":登录时这些字段已落盘。调用方可显式跳过(见 E10SessionLoadOptions)。
422
- if (session && !(load.skipProfileEnrichment && hasUsableUserId(session))) {
432
+ if (session && !session.isExpired() && !(load.skipProfileEnrichment && hasUsableUserId(session))) {
423
433
  await session.fetchEmployeeId();
424
434
  }
425
435
  return session;
@@ -57,6 +57,8 @@ export const XIAOE_SKILL_ETEAMID_API_PATH = '/spec-apps/api/app/rpachat/skill/ex
57
57
  export const XIAOE_CLIENT_SETTINGS_SOURCE = 'XiaoE/Robots/user_settings.json';
58
58
  export const XIAOE_USER_TOKEN_SOURCE = 'XIAOE_USER_TOKEN';
59
59
  export const XIAOE_BUNDLED_ENV_SCRIPT_SOURCE = 'bundled scripts/xiaoe_env.py';
60
+ /** 与 XiaoE `tokens.py` 保持一致:换票成功后默认有效 2 小时。 */
61
+ export const XIAOE_ETEAMID_TTL_MS = 7_200_000;
60
62
  function envString(env, ...keys) {
61
63
  for (const key of keys) {
62
64
  const value = env[key];
@@ -134,6 +136,14 @@ function readString(data, ...keys) {
134
136
  }
135
137
  return '';
136
138
  }
139
+ function readExpiryTimes(data) {
140
+ for (const key of ['expiryTimes', 'expiryTime', 'expiresAt', 'expires_at']) {
141
+ const value = Number(data[key]);
142
+ if (Number.isFinite(value) && value > 0)
143
+ return Math.trunc(value);
144
+ }
145
+ return undefined;
146
+ }
137
147
  function cookieString(value) {
138
148
  if (!value)
139
149
  return '';
@@ -209,7 +219,19 @@ function readXiaoECredentialRecord(value, source, fallbackBaseUrl = '') {
209
219
  || cookieValue(rawCookie, 'ETEAMSID');
210
220
  if (!baseUrl || !eteamsId)
211
221
  return null;
212
- return { baseUrl: normalizeUrl(baseUrl), eteamsId, source, rawCookie };
222
+ const expiryTimes = candidates.map(readExpiryTimes).find((value) => value !== undefined);
223
+ return {
224
+ baseUrl: normalizeUrl(baseUrl),
225
+ eteamsId,
226
+ source,
227
+ rawCookie,
228
+ ...(expiryTimes !== undefined ? { expiryTimes } : {}),
229
+ };
230
+ }
231
+ function withDefaultXiaoEExpiry(credential) {
232
+ if (!credential || credential.expiryTimes !== undefined)
233
+ return credential;
234
+ return { ...credential, expiryTimes: Date.now() + XIAOE_ETEAMID_TTL_MS };
213
235
  }
214
236
  function parseXiaoEUnifiedLoginMap(raw, source) {
215
237
  const value = raw.trim();
@@ -533,7 +555,7 @@ async function resolveXiaoETokenScriptCredential(baseUrl, env = process.env) {
533
555
  const tokenData = await runXiaoETokenScript(baseUrl, env);
534
556
  if (!tokenData)
535
557
  return null;
536
- return readXiaoECredentialRecord(tokenData, 'xiaoeUserData/tokens.py', baseUrl);
558
+ return withDefaultXiaoEExpiry(readXiaoECredentialRecord(tokenData, 'xiaoeUserData/tokens.py', baseUrl));
537
559
  }
538
560
  async function resolveXiaoEBundledEnvScriptCredential(baseUrl, env = process.env) {
539
561
  const scriptPath = xiaoEBundledEnvScriptPath();
@@ -542,7 +564,7 @@ async function resolveXiaoEBundledEnvScriptCredential(baseUrl, env = process.env
542
564
  const tokenData = await runXiaoEPythonCredentialScript(scriptPath, baseUrl, env);
543
565
  if (!tokenData)
544
566
  return null;
545
- return readXiaoECredentialRecord(tokenData, XIAOE_BUNDLED_ENV_SCRIPT_SOURCE, baseUrl);
567
+ return withDefaultXiaoEExpiry(readXiaoECredentialRecord(tokenData, XIAOE_BUNDLED_ENV_SCRIPT_SOURCE, baseUrl));
546
568
  }
547
569
  export function xiaoEClientRobotsRoot(env = process.env) {
548
570
  if (process.platform === 'win32') {
@@ -766,7 +788,7 @@ async function resolveXiaoETokenCredential(credential, baseUrl) {
766
788
  // from the same origin as the token exchange. Newer 0000/200 responses keep
767
789
  // the XiaoE service origin separate and use the requested E10 target.
768
790
  const resolvedBaseUrl = payload.code === 0 ? credential.erpaUrl : targetBaseUrl;
769
- return readXiaoECredentialRecord(payload, credential.source, resolvedBaseUrl);
791
+ return withDefaultXiaoEExpiry(readXiaoECredentialRecord(payload, credential.source, resolvedBaseUrl));
770
792
  }
771
793
  async function resolveXiaoEPlatformCredential(baseUrl, env = process.env) {
772
794
  const credential = readXiaoEPlatformCredential(env);
@@ -850,6 +872,7 @@ async function saveXiaoEUnifiedLogin(input) {
850
872
  baseUrl,
851
873
  passportUrl: baseUrl,
852
874
  cookieSavedAt: Date.now(),
875
+ ...(input.expiryTimes !== undefined ? { expiryTimes: input.expiryTimes } : {}),
853
876
  loginSource: 'xiaoe-unified',
854
877
  credentialSourceStep: input.credentialSource.credentialSourceStep,
855
878
  credentialSourceType: input.credentialSource.credentialSourceType,
@@ -872,6 +895,7 @@ async function saveXiaoEUnifiedLogin(input) {
872
895
  agentType: XIAOE_AGENT_TYPE,
873
896
  version: '',
874
897
  loginSource: 'xiaoe-unified',
898
+ ...(input.expiryTimes !== undefined ? { expiryTimes: input.expiryTimes } : {}),
875
899
  ...input.credentialSource,
876
900
  },
877
901
  };
@@ -973,6 +997,7 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
973
997
  baseUrl: credential.baseUrl || baseUrl || DEFAULT_E10_BASE_URL,
974
998
  eteamsId: credential.eteamsId,
975
999
  rawCookie: credential.rawCookie,
1000
+ expiryTimes: credential.expiryTimes,
976
1001
  tenantKeyFallback: options.tenantKeyFallback,
977
1002
  profileHint: options.profileHint,
978
1003
  credentialSource: describeXiaoECredentialSource(credential.source),
@@ -1,6 +1,6 @@
1
1
  import { isXiaoERuntimeAgent } from '../../core/runtime-agent.js';
2
- import { getActiveProfile } from './auth/session.js';
3
- import { hasXiaoEEnv, tryXiaoEUnifiedLogin } from './auth/xiaoe.js';
2
+ import { getActiveProfile, readProfileConfig } from './auth/session.js';
3
+ import { hasXiaoEEnv, resolveXiaoEPreferredBaseUrl, tryXiaoEUnifiedLogin } from './auth/xiaoe.js';
4
4
  const AUTH_REQUIRED_CODES = new Set(['-2', '401']);
5
5
  const AUTH_REQUIRED_MARKERS = [
6
6
  '未登录',
@@ -225,8 +225,10 @@ export function canAutomaticallyRecoverXiaoE(ctx, currentSession) {
225
225
  || isXiaoERuntimeAgent(currentSession?.agentType);
226
226
  }
227
227
  /**
228
- * Silently refresh a stale XiaoE session. Concurrent requests for the same
229
- * profile/base URL share one credential exchange, so a burst of 401 responses
228
+ * Silently recover a stale or missing XiaoE session. Without a local session,
229
+ * current runtime evidence is required before obtaining credentials.
230
+ * Concurrent requests for the same profile/base URL share one credential
231
+ * exchange, so a burst of 401 responses
230
232
  * does not start a burst of login attempts.
231
233
  */
232
234
  export async function recoverXiaoEAuthSession(ctx, currentSession) {
@@ -234,7 +236,12 @@ export async function recoverXiaoEAuthSession(ctx, currentSession) {
234
236
  return null;
235
237
  const options = globalOptions(ctx);
236
238
  const profile = options.profile || getActiveProfile();
237
- const baseUrl = currentSession.getBaseUrl();
239
+ const config = readProfileConfig(profile);
240
+ const baseUrl = resolveXiaoEPreferredBaseUrl({
241
+ explicitBaseUrl: currentSession?.getBaseUrl() || options.baseUrl,
242
+ profileBaseUrl: config.baseUrl,
243
+ env: ctx.env,
244
+ });
238
245
  const key = `${profile}\n${baseUrl}`;
239
246
  const existing = recoveryInFlight.get(key);
240
247
  if (existing)
@@ -246,7 +253,7 @@ export async function recoverXiaoEAuthSession(ctx, currentSession) {
246
253
  profileBaseUrl: baseUrl,
247
254
  allowBundledEnvScript: true,
248
255
  allowTokenScript: true,
249
- tenantKeyFallback: currentSession.tenantKey,
256
+ tenantKeyFallback: currentSession?.tenantKey || config.tenantKey,
250
257
  profileHint: profile,
251
258
  // Intentionally no logger: transparent recovery must not leak an
252
259
  // intermediate "login" step into agent stdout/stderr or JSON output.
@@ -1,4 +1,5 @@
1
1
  import { isE10AuthenticationRequired, isE10LoginRedirect, isE10RedirectStatus, sameE10SessionIdentity, } from './auth-recovery.js';
2
+ import { E10_XIAOE_RECOVERY_FAILED_MESSAGE } from './auth/messages.js';
2
3
  class E10AuthenticationContextChangedError extends Error {
3
4
  }
4
5
  export class E10ApiClient {
@@ -62,6 +63,18 @@ export class E10ApiClient {
62
63
  async requestRaw(url, init) {
63
64
  let lastError = null;
64
65
  let authenticationRecoveryAttempted = false;
66
+ if (this.auth.isExpired?.()) {
67
+ authenticationRecoveryAttempted = true;
68
+ const recovered = await this.recoverAuthentication?.(this.auth) || null;
69
+ if (!recovered || recovered.isExpired?.()) {
70
+ throw new Error(E10_XIAOE_RECOVERY_FAILED_MESSAGE);
71
+ }
72
+ if (!sameE10SessionIdentity(this.auth, recovered)) {
73
+ throw new E10AuthenticationContextChangedError('XiaoE automatic authentication recovery changed the E10 login context');
74
+ }
75
+ this.auth.replaceWith(recovered);
76
+ refreshAuthenticationHeaders(init, this.auth);
77
+ }
65
78
  for (let attempt = 1; attempt <= this.retries; attempt++) {
66
79
  try {
67
80
  const resp = await fetch(url, { ...init, redirect: 'manual' });
@@ -1,14 +1,35 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
- import { E10_AUTH_REQUIRED_MESSAGE } from './auth/messages.js';
2
+ import { E10_AUTH_REQUIRED_MESSAGE, E10_XIAOE_RECOVERY_FAILED_MESSAGE } from './auth/messages.js';
3
3
  import { E10AuthSession } from './auth/session.js';
4
4
  import { E10ApiClient } from './client.js';
5
- import { recoverXiaoEAuthSession } from './auth-recovery.js';
5
+ import { canAutomaticallyRecoverXiaoE, recoverXiaoEAuthSession } from './auth-recovery.js';
6
+ function xiaoERecoveryFailedError() {
7
+ return Object.assign(new CliError(E10_XIAOE_RECOVERY_FAILED_MESSAGE, ExitCode.AuthError), {
8
+ type: 'authentication',
9
+ subtype: 'xiaoe_credential_unavailable',
10
+ retryable: false,
11
+ stage: 'xiaoe_unified_login',
12
+ });
13
+ }
6
14
  export async function getE10Session(ctx, load = {}) {
7
15
  const options = ctx.getGlobalOptions();
8
16
  return E10AuthSession.fromOptions(options, ctx.cwd, load);
9
17
  }
10
18
  export async function requireE10Session(ctx, load = {}) {
11
- const session = await getE10Session(ctx, load);
19
+ let session = await getE10Session(ctx, load);
20
+ if (session?.isExpired() && canAutomaticallyRecoverXiaoE(ctx, session)) {
21
+ session = await recoverXiaoEAuthSession(ctx, session).catch(() => null);
22
+ if (!session || session.isExpired())
23
+ throw xiaoERecoveryFailedError();
24
+ }
25
+ if (!session && canAutomaticallyRecoverXiaoE(ctx)) {
26
+ // Read-only probes use getE10Session; only a command requiring a session
27
+ // may establish one from the current XiaoE host's credentials.
28
+ session = await recoverXiaoEAuthSession(ctx).catch(() => null);
29
+ if (!session) {
30
+ throw xiaoERecoveryFailedError();
31
+ }
32
+ }
12
33
  if (!session) {
13
34
  throw new CliError(E10_AUTH_REQUIRED_MESSAGE, ExitCode.AuthError);
14
35
  }
@@ -1,6 +1,7 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
2
  import { classifyE10Redirect, isE10AuthenticationRequired, recoverXiaoEAuthSession, sameE10SessionIdentity, } from './auth-recovery.js';
3
3
  import { startFetchTiming } from './fetch-timing.js';
4
+ import { E10_XIAOE_RECOVERY_FAILED_MESSAGE } from './auth/messages.js';
4
5
  const DEFAULT_TIMEOUT_MS = 30_000;
5
6
  const DEFAULT_BIG_INT_STRINGS = true;
6
7
  /** 下载直传最多跟几跳:E10 -> 对象存储通常 1 跳,留一点余量但不允许成环。 */
@@ -53,6 +54,12 @@ export class E10RequestRuntime {
53
54
  return this.request({ ...request, responseType: 'binary' });
54
55
  }
55
56
  async request(request) {
57
+ if (this.session.isExpired?.()) {
58
+ const recovered = await this.recoverExpiredSession(this.session.eteamsId);
59
+ if (!recovered || this.session.isExpired?.()) {
60
+ throw new E10ApiError('authentication', 'session_expired', E10_XIAOE_RECOVERY_FAILED_MESSAGE, { exitCode: ExitCode.AuthError, retryable: false });
61
+ }
62
+ }
56
63
  const mutable = this.prepareRequest(request);
57
64
  try {
58
65
  await this.runBeforeRequest(mutable, this.sessionContext());
@@ -171,6 +178,8 @@ export class E10RequestRuntime {
171
178
  }
172
179
  if (!recovered)
173
180
  return false;
181
+ if (recovered.isExpired?.())
182
+ return false;
174
183
  if (!sameE10SessionIdentity(this.session, recovered)) {
175
184
  throw new E10ApiError('authentication', 'session_context_changed', 'XiaoE 自动恢复后的登录身份或 E10 环境发生变化,本次请求未重试', { exitCode: ExitCode.AuthError, retryable: false });
176
185
  }
@@ -201,6 +210,7 @@ export class E10RequestRuntime {
201
210
  timeoutMs: request.timeoutMs || DEFAULT_TIMEOUT_MS,
202
211
  responseType: request.responseType || 'json',
203
212
  bigIntStrings: request.bigIntStrings ?? DEFAULT_BIG_INT_STRINGS,
213
+ followRedirects: request.followRedirects === true,
204
214
  };
205
215
  }
206
216
  sessionContext() {
@@ -270,7 +280,8 @@ export class E10RequestRuntime {
270
280
  const init = {
271
281
  method: request.method,
272
282
  headers,
273
- redirect: 'manual',
283
+ // 文件下载允许跟随重定向(302 → 文件服务);其余请求保持 manual 以便识别登录跳转
284
+ redirect: request.followRedirects ? 'follow' : 'manual',
274
285
  };
275
286
  if (request.streamBody) {
276
287
  init.body = request.streamBody;
@@ -135,6 +135,8 @@ export function createArchiveHost(ctx, dependencies = {}) {
135
135
  formData: multipart,
136
136
  headers: { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders },
137
137
  timeoutMs,
138
+ // 下载接口常 302 重定向到文件服务/CDN,需跟随,避免被误判为登录失效
139
+ followRedirects: true,
138
140
  })
139
141
  : await rt.requestJson({
140
142
  module: 'archive',
@@ -25,7 +25,7 @@ export const archiveManifest = {
25
25
  rules: [
26
26
  '写操作(*.apply)必须先执行同名的 prepare 拿到 continuation,再带 confirm=true 执行 apply;CLI 用 --yes 表示确认。',
27
27
  '借阅单下载为异步任务:apply 会轮询打包进度,完成后把 zip 保存到当前用户的「下载」目录(可用 output 指定路径)。',
28
- '发起借阅/续借只返回流程创建页 URL,CLI 不自动打开浏览器,需由用户或上层在浏览器中打开。',
28
+ '发起借阅/续借只返回流程创建页 URL,CLI 不自动打开浏览器;应以 Markdown 超链接(a 标签)返回,由客户端决定打开方式。',
29
29
  '借阅车/借阅单列表优先自定义接口,接口未发布时自动回退 ebuilder 列表链路,回退时行的字段更少(提供 fields/display)。',
30
30
  ],
31
31
  operations: archiveOperations,
@@ -194,7 +194,19 @@ const borrowListDetail = {
194
194
  }
195
195
  const session = await host.getSession();
196
196
  const detail = await fetchBorrowDetail(host, session.userId, String(dataId));
197
- return archiveSuccess('archive.borrow-list.detail', detail, { count: detail.details.length });
197
+ // 档案题名补可点击链接:arcDangan.id 形如 {档案ID}_{表单ID},拼成与全文检索一致的 archiveView
198
+ const root = String(session?.baseUrl ?? '').replace(/\/+$/, '');
199
+ const details = detail.details.map((item) => ({
200
+ ...item,
201
+ arcDangan: item.arcDangan.map((arc) => {
202
+ const [arcId, arcFormId = ''] = String(arc.id ?? '').split('_');
203
+ if (!arcId || !arcFormId)
204
+ return arc;
205
+ const viewPath = `/sp/custom/archive/archiveView?arcId=${arcId}&arcFormId=${arcFormId}`;
206
+ return { ...arc, viewPath, url: root ? `${root}${viewPath}` : viewPath };
207
+ }),
208
+ }));
209
+ return archiveSuccess('archive.borrow-list.detail', { ...detail, details }, { count: details.length });
198
210
  },
199
211
  };
200
212
  export const borrowListOperations = [borrowListList, borrowListDetail];
@@ -67,7 +67,7 @@ function flowResult(action, baseUrl, workflowId, params, extra = {}) {
67
67
  workflowId,
68
68
  url: buildFlowUrl(baseUrl, workflowId, params),
69
69
  params,
70
- hint: 'CLI 不自动打开浏览器,请在浏览器中打开该 URL 完成后续填写与提交',
70
+ hint: 'CLI 不自动打开浏览器,也不要执行 open/start 自动打开;请把 url 以 Markdown 超链接(a 标签)返回,由客户端决定打开方式',
71
71
  }, { wrote: true });
72
72
  }
73
73
  const flowBorrowCarPrepare = {