weaver-work-cli 0.1.40 → 0.1.42

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 (185) hide show
  1. package/dist/chunks/{archive-JHTONRST.js → archive-J4I6MCKF.js} +2 -2
  2. package/dist/chunks/{archive-oa-FCHLCPHC.js → archive-oa-BRZKKTO4.js} +2 -2
  3. package/dist/chunks/{asset-RLIHC7DB.js → asset-L7D4FAOZ.js} +2 -2
  4. package/dist/chunks/{asset-idle-KVOFTLCP.js → asset-idle-DADLLV75.js} +2 -2
  5. package/dist/chunks/{attachment-content-RZYDB3MB.js → attachment-content-K4C7VLWQ.js} +3 -3
  6. package/dist/chunks/{blog-42OZTJNP.js → blog-BVGQ7NZL.js} +2 -2
  7. package/dist/chunks/{caizhilian-KRK547K5.js → caizhilian-SAADNFD6.js} +2 -2
  8. package/dist/chunks/{calendar-RTWAMKHD.js → calendar-Q7NQY5Z2.js} +61 -23
  9. package/dist/chunks/{chunk-LFMSDBZN.js → chunk-3KTFS4IA.js} +308 -153
  10. package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
  11. package/dist/chunks/{chunk-T6EGVBKU.js → chunk-4RAE6JXP.js} +1 -1
  12. package/dist/chunks/{chunk-7BRU4EFH.js → chunk-72CKAUAG.js} +29 -2
  13. package/dist/chunks/{chunk-NETCCARD.js → chunk-APVFJUQH.js} +2 -2
  14. package/dist/chunks/{chunk-CAQNBBQA.js → chunk-BBKDXQEC.js} +2 -2
  15. package/dist/chunks/{chunk-57IECMWX.js → chunk-BIPYLLVS.js} +26 -12
  16. package/dist/chunks/{chunk-LN447PY2.js → chunk-EKWAQSLU.js} +3 -2
  17. package/dist/chunks/{chunk-4TEDNCON.js → chunk-FOAA27U5.js} +338 -52
  18. package/dist/chunks/{chunk-7U577Q7U.js → chunk-G4UR6NXN.js} +2 -2
  19. package/dist/chunks/{chunk-NJKREDA3.js → chunk-GEWYMQME.js} +1 -1
  20. package/dist/chunks/{chunk-I566ZYNF.js → chunk-GHXCDPTM.js} +1 -1
  21. package/dist/chunks/{chunk-4CTESSKI.js → chunk-IZ7S7QUS.js} +4 -4
  22. package/dist/chunks/{chunk-VNBU55SL.js → chunk-JPRO6ER2.js} +57 -2432
  23. package/dist/chunks/{chunk-IZSSVYAG.js → chunk-LADPYOWC.js} +2 -2
  24. package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
  25. package/dist/chunks/chunk-NKZRQXST.js +2546 -0
  26. package/dist/chunks/{chunk-JWRZM2H2.js → chunk-QKNKR4RH.js} +3 -3
  27. package/dist/chunks/{chunk-FLS4DPGP.js → chunk-R4CHUSKC.js} +28 -15
  28. package/dist/chunks/{chunk-WFFNM65B.js → chunk-SA6VBIHY.js} +73 -19
  29. package/dist/chunks/{chunk-SPRHHSVC.js → chunk-TTPJJJC4.js} +2 -2
  30. package/dist/chunks/{chunk-7E6MUOEK.js → chunk-W7BEHOSR.js} +2 -2
  31. package/dist/chunks/{chunk-CXRQWOZ5.js → chunk-YRSZQUD2.js} +2 -2
  32. package/dist/chunks/{conflict-VR55AOOO.js → conflict-F2OMZRTO.js} +2 -2
  33. package/dist/chunks/{contract-HI4FNPYE.js → contract-5BETHRSG.js} +2 -2
  34. package/dist/chunks/{cowork-HJLI353D.js → cowork-3MN6EQQO.js} +22 -12
  35. package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
  36. package/dist/chunks/{ebuilder-form-HTGI63UU.js → ebuilder-form-SBHOYBUD.js} +3 -3
  37. package/dist/chunks/{ehr-GYXIYH3A.js → ehr-KMOJULKG.js} +432 -1234
  38. package/dist/chunks/{env-DW3G4HUO.js → env-LB7NBL2O.js} +2 -2
  39. package/dist/chunks/{esb-I5INCEFE.js → esb-W2PJCFCX.js} +4 -4
  40. package/dist/chunks/{factory-TKXGXS6L.js → factory-6ZFQXQ46.js} +1 -1
  41. package/dist/chunks/{fna-K2MHTQ24.js → fna-ZQPCUV4R.js} +2 -2
  42. package/dist/chunks/{group-chat-VYBNMJLG.js → group-chat-2HHAJN46.js} +5 -3
  43. package/dist/chunks/{host-ZUQTJYYK.js → host-4CBHGOHT.js} +3 -3
  44. package/dist/chunks/{host-D5SERMAJ.js → host-ATKVNZAT.js} +3 -3
  45. package/dist/chunks/{host-JM4YVHFM.js → host-BQH22NTQ.js} +3 -3
  46. package/dist/chunks/{host-L3VLITMT.js → host-X2CSLFM6.js} +3 -3
  47. package/dist/chunks/{host-256GVCAH.js → host-YTODKMSL.js} +3 -3
  48. package/dist/chunks/{hrm-VLRBKTZB.js → hrm-FFM4UJFS.js} +4 -4
  49. package/dist/chunks/{im-QK3VTLXK.js → im-VEHKDQPB.js} +7 -7
  50. package/dist/chunks/{invoice-T5JTA32H.js → invoice-LVIKSIIF.js} +3 -3
  51. package/dist/chunks/{jiuchuanhui-P2FNEQVZ.js → jiuchuanhui-X6DINO5P.js} +2 -2
  52. package/dist/chunks/{lazy-dispatch-XRRI46M6.js → lazy-dispatch-KSWTN5L6.js} +4 -4
  53. package/dist/chunks/{mail-3Y7MJ6PS.js → mail-VDKRGUD3.js} +2 -2
  54. package/dist/chunks/{meeting-M7LTXGJE.js → meeting-CLC6HP3Z.js} +12 -11
  55. package/dist/chunks/{okr-OBC6S67L.js → okr-CHG4OO3U.js} +2 -2
  56. package/dist/chunks/{plan-5H6I53SK.js → plan-XWR75667.js} +2 -2
  57. package/dist/chunks/{project-L75QZV4S.js → project-5BXOOKMG.js} +40 -23
  58. package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
  59. package/dist/chunks/{registry-RXPN6BBT.js → registry-7FFOXIAW.js} +4 -4
  60. package/dist/chunks/{registry-KMHCPAAD.js → registry-BKBZPBAJ.js} +10 -9
  61. package/dist/chunks/{registry-QGHFNUWS.js → registry-OZSMDVPP.js} +2 -2
  62. package/dist/chunks/{registry-UGRBCBHO.js → registry-RHMW6ANW.js} +2 -2
  63. package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
  64. package/dist/chunks/{room-recommend-A3UDC3CA.js → room-recommend-62H7ZQQ6.js} +4 -4
  65. package/dist/chunks/{schedule-5GCACCK7.js → schedule-FNXKH3DQ.js} +4 -4
  66. package/dist/chunks/{workflow-M5ZJ7DWF.js → workflow-ADHKCAJK.js} +6 -6
  67. package/dist/index.js +33 -28
  68. package/dist/internal/e10/auth/commands.js +1 -0
  69. package/dist/internal/e10/auth/messages.js +6 -0
  70. package/dist/internal/e10/auth/session.js +17 -0
  71. package/dist/internal/e10/auth/xiaoe.js +286 -44
  72. package/dist/internal/e10/auth-recovery.js +36 -5
  73. package/dist/internal/e10/context.js +56 -12
  74. package/dist/internal/e10/index.js +1 -1
  75. package/dist/internal/e10/network-error.js +93 -0
  76. package/dist/internal/e10/request-runtime.js +6 -0
  77. package/dist/shortcuts/calendar/operations/read.js +31 -22
  78. package/dist/shortcuts/calendar/operations/write.js +45 -1
  79. package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
  80. package/dist/shortcuts/cowork/operations/item-write.js +1 -7
  81. package/dist/shortcuts/cowork/operations/member-write.js +3 -1
  82. package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
  83. package/dist/shortcuts/cowork/operations/shared.js +7 -0
  84. package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
  85. package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
  86. package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
  87. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
  88. package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
  89. package/dist/shortcuts/ehr/operations/recruit.js +10 -4
  90. package/dist/shortcuts/ehr/operations/shared.js +47 -0
  91. package/dist/shortcuts/hrm/operations/org.js +118 -0
  92. package/dist/shortcuts/index.js +5 -0
  93. package/dist/shortcuts/meeting/operations/eb/catalog.js +4 -1
  94. package/dist/shortcuts/meeting/operations/eb/create-flow.js +85 -28
  95. package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +25 -3
  97. package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +33 -11
  99. package/dist/shortcuts/project/operations/read.js +6 -25
  100. package/dist/shortcuts/project/operations/shared.js +22 -0
  101. package/dist/shortcuts/project/operations/write.js +28 -2
  102. package/dist/shortcuts/recruit-stage/context.js +119 -0
  103. package/dist/shortcuts/recruit-stage/continuation.js +73 -0
  104. package/dist/shortcuts/recruit-stage/errors.js +65 -0
  105. package/dist/shortcuts/recruit-stage/host.js +109 -0
  106. package/dist/shortcuts/recruit-stage/index.js +115 -0
  107. package/dist/shortcuts/recruit-stage/manifest.js +29 -0
  108. package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
  109. package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
  110. package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
  111. package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
  112. package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
  113. package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
  114. package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
  115. package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
  116. package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
  117. package/dist/shortcuts/recruit-stage/shared.js +608 -0
  118. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +13 -6
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
  122. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +50 -8
  123. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +2 -1
  124. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
  125. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +7 -39
  126. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +51 -15
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +39 -14
  132. package/docs/e10-auth.md +7 -1
  133. package/docs/operation-manual.md +8 -0
  134. package/package.json +1 -1
  135. package/scripts/install-skill-to-xiaoe.mjs +105 -0
  136. package/scripts/package-skill.mjs +4 -2
  137. package/skill-template/business-info.json +24 -0
  138. package/skill-template/domains/recruit-stage.md +29 -0
  139. package/skills/weaver-e10-calendar/SKILL.md +2 -1
  140. package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
  141. package/skills/weaver-e10-cowork/SKILL.md +1 -1
  142. package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
  143. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  144. package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
  145. package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
  146. package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
  147. package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
  148. package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
  149. package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
  150. package/skills/weaver-e10-mail/SKILL.md +1 -1
  151. package/skills/weaver-e10-meeting/SKILL.md +32 -2
  152. package/skills/weaver-e10-meeting.zip +0 -0
  153. package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
  154. package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
  155. package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
  156. package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
  157. package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
  158. package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
  159. package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
  160. package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
  161. package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
  162. package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
  163. package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
  164. package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
  165. package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
  166. package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
  167. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
  168. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
  169. package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
  170. package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
  171. package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
  172. package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
  173. package/skills/weaver-e10-shared/SKILL.md +1 -1
  174. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
  175. package/skills/weaver-e10-shijingran/SKILL.md +3 -2
  176. package/skills/weaver-e10-shijingran/references/operations.md +3 -1
  177. package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
  178. package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
  179. package/skills/weaver-e10-workflow/SKILL.md +4 -2
  180. package/skills/weaver-e10-workflow/aigroup/default.json +11 -9
  181. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +23 -13
  182. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +62 -20
  183. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
  184. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
  185. package/dist/chunks/{chunk-E7OK5WD5.js → chunk-2LV74IWP.js} +5 -5
@@ -5,7 +5,8 @@ import { join } from 'node:path';
5
5
  import { CliError, ExitCode } from '../../../core/errors.js';
6
6
  import { packagePath } from '../../../core/package-paths.js';
7
7
  import { isXiaoERuntime, isXiaoERuntimeAgent } from '../../../core/runtime-agent.js';
8
- import { buildE10CookieHeader, E10AuthSession, extractRawSetCookie, extractSessionTicket, profileNameFromUrl, readTeamsCheckUserInfo, saveAuthFile, saveProfileConfig, setActiveProfile, } from './session.js';
8
+ import { createTimeoutSignal, isAbortError, isNetworkErrorCode, looksLikeTransportMessage, readErrorCauseCode, } from '../network-error.js';
9
+ import { buildE10CookieHeader, E10_TEAMS_CHECK_TIMEOUT_MS, E10AuthSession, extractRawSetCookie, extractSessionTicket, profileNameFromUrl, readTeamsCheckUserInfo, saveAuthFile, saveProfileConfig, setActiveProfile, } from './session.js';
9
10
  export const DEFAULT_E10_BASE_URL = 'https://www.e-cology.com.cn';
10
11
  export const XIAOE_AGENT_TYPE = 'XiaoE';
11
12
  // `XIAOE_` 是宿主/平台保留前缀(XIAOE_DOMAIN、XIAOE_USER_TOKEN、XIAOE_SKILL_* 等)。
@@ -59,6 +60,109 @@ export const XIAOE_USER_TOKEN_SOURCE = 'XIAOE_USER_TOKEN';
59
60
  export const XIAOE_BUNDLED_ENV_SCRIPT_SOURCE = 'bundled scripts/xiaoe_env.py';
60
61
  /** 与 XiaoE `tokens.py` 保持一致:换票成功后默认有效 2 小时。 */
61
62
  export const XIAOE_ETEAMID_TTL_MS = 7_200_000;
63
+ const XIAOE_DIAGNOSTIC_MAX_LENGTH = 300;
64
+ /** 换票失败原因可能夹带服务端回显,落进结构化错误前先抹掉已知密钥。 */
65
+ function redactXiaoEDiagnostic(message, secrets) {
66
+ let text = message;
67
+ for (const secret of secrets) {
68
+ if (secret && secret.length >= 8 && text.includes(secret)) {
69
+ text = text.split(secret).join('<redacted>');
70
+ }
71
+ }
72
+ const normalized = text.replace(/\s+/gu, ' ').trim();
73
+ return normalized.length > XIAOE_DIAGNOSTIC_MAX_LENGTH
74
+ ? `${normalized.slice(0, XIAOE_DIAGNOSTIC_MAX_LENGTH)}…`
75
+ : normalized;
76
+ }
77
+ function noteXiaoEStep(diagnostics, step) {
78
+ diagnostics?.attemptedSteps.push(step);
79
+ }
80
+ function tagXiaoEError(message, tags) {
81
+ return Object.assign(new CliError(message, ExitCode.AuthError), tags);
82
+ }
83
+ function readTag(error, key, type) {
84
+ if (!error || typeof error !== 'object')
85
+ return undefined;
86
+ const value = error[key];
87
+ return typeof value === type ? value : undefined;
88
+ }
89
+ /** 401/403/3xx = 登录态被拒;其余 4xx/5xx 才算服务端 HTTP 异常。 */
90
+ function classifyXiaoEHttpStatus(status) {
91
+ if (status === 401 || status === 403)
92
+ return 'credential';
93
+ if (status >= 300 && status < 400)
94
+ return 'credential';
95
+ return status >= 400 ? 'http' : 'credential';
96
+ }
97
+ /**
98
+ * 把任意异常归成三类。已经带标签的错误直接沿用;原始 fetch 异常则顺着
99
+ * `error.cause.code`(Node 把真实原因藏在这一层)区分网络错误与协议错误。
100
+ */
101
+ function classifyXiaoEError(error) {
102
+ const taggedKind = readTag(error, 'errorKind', 'string');
103
+ const taggedStatus = readTag(error, 'status', 'number');
104
+ const taggedCause = readTag(error, 'causeCode', 'string');
105
+ const causeCode = taggedCause || readErrorCauseCode(error);
106
+ const withCause = causeCode ? { causeCode } : {};
107
+ const withStatus = taggedStatus === undefined ? {} : { status: taggedStatus };
108
+ if (taggedKind)
109
+ return { kind: taggedKind, ...withCause, ...withStatus };
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ if (isAbortError(error))
112
+ return { kind: 'network', causeCode: causeCode || 'ETIMEDOUT' };
113
+ if (taggedStatus !== undefined) {
114
+ return { kind: classifyXiaoEHttpStatus(taggedStatus), ...withCause, status: taggedStatus };
115
+ }
116
+ if (causeCode && isNetworkErrorCode(causeCode))
117
+ return { kind: 'network', causeCode };
118
+ const httpMatch = /\bHTTP (\d{3})\b/u.exec(message);
119
+ if (httpMatch) {
120
+ const status = Number(httpMatch[1]);
121
+ return { kind: classifyXiaoEHttpStatus(status), ...withCause, status };
122
+ }
123
+ if (looksLikeTransportMessage(message))
124
+ return { kind: 'network', ...withCause };
125
+ return { kind: 'credential', ...withCause };
126
+ }
127
+ function noteXiaoEError(diagnostics, error, secrets = [], stage = 'e10_session') {
128
+ if (!diagnostics)
129
+ return;
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ const redacted = redactXiaoEDiagnostic(message, secrets);
132
+ if (redacted)
133
+ diagnostics.lastError = redacted;
134
+ const { kind, causeCode, status } = classifyXiaoEError(error);
135
+ const failure = {
136
+ stage: readTag(error, 'stage', 'string') ?? stage,
137
+ kind,
138
+ message: redacted,
139
+ ...(causeCode ? { causeCode } : {}),
140
+ ...(status === undefined ? {} : { httpStatus: status }),
141
+ };
142
+ if (!diagnostics.failures)
143
+ diagnostics.failures = [];
144
+ diagnostics.failures.push(failure);
145
+ }
146
+ /**
147
+ * 一次恢复失败的整体归因:只要出现过明确的凭证拒绝,就按凭证问题报(用户该去
148
+ * 重新登录);否则 HTTP 异常优先;只有**所有**失败都是网络错误时,才判定为
149
+ * 「暂时连不上 E10」,避免把网络抖动说成登录失效。
150
+ */
151
+ export function summarizeXiaoEFailureKind(diagnostics) {
152
+ const failures = diagnostics?.failures || [];
153
+ if (!failures.length)
154
+ return undefined;
155
+ if (failures.some((failure) => failure.kind === 'credential'))
156
+ return 'credential';
157
+ if (failures.some((failure) => failure.kind === 'http'))
158
+ return 'http';
159
+ return failures.every((failure) => failure.kind === 'network') ? 'network' : undefined;
160
+ }
161
+ /** 最后一条失败记录,用于把结构化错误的 `stage` 指向真正卡住的那一步。 */
162
+ export function lastXiaoEFailure(diagnostics) {
163
+ const failures = diagnostics?.failures || [];
164
+ return failures.length ? failures[failures.length - 1] : undefined;
165
+ }
62
166
  function envString(env, ...keys) {
63
167
  for (const key of keys) {
64
168
  const value = env[key];
@@ -91,6 +195,18 @@ export function hasXiaoEEnv(env = process.env, options = {}) {
91
195
  const explicitAgentType = envString(env, 'WEAVER_AGENT_TYPE');
92
196
  return explicitAgentType ? isXiaoERuntimeAgent(explicitAgentType) : false;
93
197
  }
198
+ export function resolveXiaoEClientType(env = process.env) {
199
+ const raw = envString(env, 'XIAOE_CLIENT_TYPE').toLowerCase();
200
+ if (raw === 'client' || raw === 'desktop' || raw === 'pc')
201
+ return 'client';
202
+ if (raw === 'cloud' || raw === 'web' || raw === 'browser')
203
+ return 'cloud';
204
+ return undefined;
205
+ }
206
+ /** 网页端运行时没有桌面凭据通道,只有平台票据可换。 */
207
+ export function isXiaoECloudRuntime(env = process.env) {
208
+ return resolveXiaoEClientType(env) === 'cloud';
209
+ }
94
210
  export function describeXiaoECredentialSource(source) {
95
211
  // Step 1 is the platform-injected credential (XIAOE_USER_TOKEN + XIAOE_DOMAIN):
96
212
  // it is available in every sandbox run and needs no local desktop client.
@@ -706,15 +822,22 @@ export function inspectXiaoEUnifiedLoginSources(options = {}) {
706
822
  });
707
823
  const platformCredential = hasXiaoEPlatformCredential(env);
708
824
  const allowLocalDiscovery = options.allowLocalDiscovery !== false;
709
- const tokenScript = options.allowTokenScript !== false
825
+ const clientType = resolveXiaoEClientType(env);
826
+ // 网页端/沙箱不存在桌面凭据通道,跳过探测也就不会被机器上别人的
827
+ // 客户端登录态误命中。
828
+ const allowDesktopChannels = clientType !== 'cloud';
829
+ const tokenScript = allowDesktopChannels
830
+ && options.allowTokenScript !== false
710
831
  && hasXiaoETokenScript(env, allowLocalDiscovery);
711
- const clientCredential = hasXiaoEClientCredential(env, allowLocalDiscovery);
832
+ const clientCredential = allowDesktopChannels
833
+ && hasXiaoEClientCredential(env, allowLocalDiscovery);
712
834
  const environmentCredential = Boolean(baseUrl && resolveXiaoEUnifiedLoginCredential(baseUrl, env));
713
835
  return {
714
836
  platformCredential,
715
837
  tokenScript,
716
838
  clientCredential,
717
839
  environmentCredential,
840
+ clientType,
718
841
  any: platformCredential || tokenScript || clientCredential || environmentCredential,
719
842
  };
720
843
  }
@@ -741,11 +864,16 @@ async function fetchXiaoEClientEteamId(credential, domain) {
741
864
  if (resp.status < 300 || resp.status >= 400)
742
865
  break;
743
866
  if (redirects === 5) {
744
- throw new CliError('XiaoE client token endpoint redirected too many times.', ExitCode.AuthError);
867
+ throw tagXiaoEError('XiaoE client token endpoint redirected too many times.', {
868
+ errorKind: 'http',
869
+ });
745
870
  }
746
871
  const location = resp.headers.get('location');
747
872
  if (!location) {
748
- throw new CliError('XiaoE client token endpoint returned a redirect without Location.', ExitCode.AuthError);
873
+ throw tagXiaoEError('XiaoE client token endpoint returned a redirect without Location.', {
874
+ errorKind: 'http',
875
+ status: resp.status,
876
+ });
749
877
  }
750
878
  const next = new URL(location, endpoint);
751
879
  const sameOrigin = next.origin === endpoint.origin;
@@ -754,17 +882,32 @@ async function fetchXiaoEClientEteamId(credential, domain) {
754
882
  && next.hostname === endpoint.hostname;
755
883
  const safeProtocol = next.protocol === 'http:' || next.protocol === 'https:';
756
884
  if (!safeProtocol || next.username || next.password || (!sameOrigin && !sameHostHttpsUpgrade)) {
757
- throw new CliError('XiaoE client token endpoint attempted an unsafe cross-origin redirect.', ExitCode.AuthError);
885
+ throw tagXiaoEError('XiaoE client token endpoint attempted an unsafe cross-origin redirect.', {
886
+ errorKind: 'http',
887
+ status: resp.status,
888
+ });
758
889
  }
759
890
  await resp.body?.cancel().catch(() => undefined);
760
891
  endpoint = next;
761
892
  }
762
893
  if (!resp?.ok) {
763
- throw new CliError(`XiaoE client token endpoint returned HTTP ${resp?.status || 0}.`, ExitCode.AuthError);
894
+ const status = resp?.status || 0;
895
+ throw tagXiaoEError(`XiaoE client token endpoint returned HTTP ${status}.`, {
896
+ errorKind: classifyXiaoEHttpStatus(status),
897
+ status,
898
+ });
764
899
  }
765
- const data = await resp.json().catch(() => null);
900
+ const data = await resp.json().catch((error) => {
901
+ // 读响应体时被超时打断属于网络错误,不能当成「返回了非法 JSON」。
902
+ if (isAbortError(error))
903
+ throw error;
904
+ return null;
905
+ });
766
906
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
767
- throw new CliError('XiaoE client token endpoint returned invalid JSON.', ExitCode.AuthError);
907
+ throw tagXiaoEError('XiaoE client token endpoint returned invalid JSON.', {
908
+ errorKind: 'http',
909
+ status: resp.status,
910
+ });
768
911
  }
769
912
  const payload = data;
770
913
  const businessCode = payload.code;
@@ -773,10 +916,23 @@ async function fetchXiaoEClientEteamId(credential, domain) {
773
916
  : {};
774
917
  if ((businessCode !== '0000' && businessCode !== 0 && businessCode !== 200) || !readString(businessData, 'eteamId')) {
775
918
  const codeText = businessCode === undefined ? 'missing' : String(businessCode);
776
- throw new CliError(`XiaoE client token exchange failed with business code ${codeText}.`, ExitCode.AuthError);
919
+ throw tagXiaoEError(`XiaoE client token exchange failed with business code ${codeText}.`, {
920
+ errorKind: 'credential',
921
+ });
777
922
  }
778
923
  return payload;
779
924
  }
925
+ catch (error) {
926
+ if (isAbortError(error)) {
927
+ throw tagXiaoEError(`XiaoE client token endpoint timed out after ${XIAOE_CLIENT_TOKEN_TIMEOUT_MS} ms.`, { errorKind: 'network', causeCode: 'ETIMEDOUT' });
928
+ }
929
+ // 已经带语义的 HTTP / 业务码错误原样上抛,别再包一层网络错误。
930
+ if (error instanceof CliError)
931
+ throw error;
932
+ const causeCode = readErrorCauseCode(error);
933
+ const message = error instanceof Error ? error.message : String(error);
934
+ throw tagXiaoEError(`XiaoE client token endpoint request failed${causeCode ? ` (${causeCode})` : ''}: ${message}`, { errorKind: 'network', ...(causeCode ? { causeCode } : {}) });
935
+ }
780
936
  finally {
781
937
  clearTimeout(timer);
782
938
  }
@@ -831,26 +987,62 @@ export function createXiaoEUnifiedLoginUnavailableError(baseUrl = '') {
831
987
  });
832
988
  }
833
989
  async function resolveUserInfo(baseUrl, eteamsId, rawCookie = '', agentType = '') {
834
- const resp = await fetch(`${baseUrl}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(baseUrl)}`, {
835
- method: 'POST',
836
- headers: {
837
- accept: 'application/json',
838
- 'content-type': 'application/json',
839
- eteamsid: eteamsId,
840
- cookie: buildE10CookieHeader(rawCookie, eteamsId, agentType),
841
- 'user-agent': agentType ? `AgentType=${agentType},IsAgent=true` : 'weaver-work-cli',
842
- origin: baseUrl,
843
- },
844
- redirect: 'manual',
845
- });
846
- const data = await resp.json().catch(() => ({}));
847
- return { userInfo: readTeamsCheckUserInfo(resp, data), setCookie: extractRawSetCookie(resp) };
990
+ // teamsCheck 是恢复流程的必经往返:没有超时的话,服务端半开连接会把整条命令挂死。
991
+ const { signal, clear } = createTimeoutSignal(E10_TEAMS_CHECK_TIMEOUT_MS);
992
+ try {
993
+ const resp = await fetch(`${baseUrl}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(baseUrl)}`, {
994
+ method: 'POST',
995
+ headers: {
996
+ accept: 'application/json',
997
+ 'content-type': 'application/json',
998
+ eteamsid: eteamsId,
999
+ cookie: buildE10CookieHeader(rawCookie, eteamsId, agentType),
1000
+ 'user-agent': agentType ? `AgentType=${agentType},IsAgent=true` : 'weaver-work-cli',
1001
+ origin: baseUrl,
1002
+ },
1003
+ redirect: 'manual',
1004
+ signal,
1005
+ });
1006
+ // 读响应体同样在超时窗口内;中途被 abort 不能当成「空响应」吞掉。
1007
+ const data = await resp.json().catch((error) => {
1008
+ if (isAbortError(error))
1009
+ throw error;
1010
+ return {};
1011
+ });
1012
+ return { userInfo: readTeamsCheckUserInfo(resp, data), setCookie: extractRawSetCookie(resp), status: resp.status };
1013
+ }
1014
+ catch (error) {
1015
+ if (isAbortError(error)) {
1016
+ throw tagXiaoEError(`E10 teamsCheck timed out after ${E10_TEAMS_CHECK_TIMEOUT_MS} ms.`, { errorKind: 'network', stage: 'teams_check', causeCode: 'ETIMEDOUT' });
1017
+ }
1018
+ if (error instanceof CliError)
1019
+ throw error;
1020
+ const causeCode = readErrorCauseCode(error);
1021
+ const message = error instanceof Error ? error.message : String(error);
1022
+ throw tagXiaoEError(`E10 teamsCheck request failed${causeCode ? ` (${causeCode})` : ''}: ${message}`, { errorKind: 'network', stage: 'teams_check', ...(causeCode ? { causeCode } : {}) });
1023
+ }
1024
+ finally {
1025
+ clear();
1026
+ }
848
1027
  }
849
1028
  async function saveXiaoEUnifiedLogin(input) {
850
1029
  const baseUrl = normalizeUrl(input.baseUrl);
851
- const { userInfo, setCookie } = await resolveUserInfo(baseUrl, input.eteamsId, input.rawCookie || '', XIAOE_AGENT_TYPE);
1030
+ const { userInfo, setCookie, status } = await resolveUserInfo(baseUrl, input.eteamsId, input.rawCookie || '', XIAOE_AGENT_TYPE);
852
1031
  if (!userInfo.userId) {
853
- throw new CliError('Could not resolve userId. Check whether XiaoE unified login is valid for this E10 domain.', ExitCode.AuthError);
1032
+ // 401/403/3xx 是「票不被接受」,用户该去恢复登录;5xx 则是 E10 侧故障,
1033
+ // 报成凭证问题会把人引向无用的重新登录。
1034
+ const statusText = status || 0;
1035
+ throw classifyXiaoEHttpStatus(statusText) === 'http'
1036
+ ? tagXiaoEError(`E10 teamsCheck returned HTTP ${statusText}.`, {
1037
+ errorKind: 'http',
1038
+ stage: 'teams_check',
1039
+ status: statusText,
1040
+ })
1041
+ : tagXiaoEError('Could not resolve userId. Check whether XiaoE unified login is valid for this E10 domain.', {
1042
+ errorKind: 'credential',
1043
+ stage: 'teams_check',
1044
+ status: statusText,
1045
+ });
854
1046
  }
855
1047
  const tenantKey = userInfo.tenantKey || input.tenantKeyFallback || '';
856
1048
  const profile = input.profileHint || profileNameFromUrl(baseUrl);
@@ -908,19 +1100,32 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
908
1100
  profileBaseUrl: options.profileBaseUrl,
909
1101
  env,
910
1102
  });
911
- const mayUseTokenScript = options.allowTokenScript === true;
912
- if (!baseUrl && !mayUseTokenScript)
1103
+ // 网页端没有桌面凭据通道:`XIAOE_CLIENT_TYPE=cloud` 时不尝试 tokens.py 与
1104
+ // 本地客户端会话,避免在共享机器上误用他人的客户端登录态。
1105
+ const mayUseTokenScript = options.allowTokenScript === true && !isXiaoECloudRuntime(env);
1106
+ const diagnostics = options.diagnostics;
1107
+ const platformCredentialDetected = hasXiaoEPlatformCredential(env);
1108
+ const secrets = [envString(env, 'XIAOE_USER_TOKEN')];
1109
+ if (diagnostics) {
1110
+ diagnostics.baseUrl = baseUrl;
1111
+ diagnostics.platformCredentialDetected = platformCredentialDetected;
1112
+ diagnostics.clientType = resolveXiaoEClientType(env);
1113
+ }
1114
+ if (!baseUrl && !mayUseTokenScript) {
1115
+ noteXiaoEStep(diagnostics, 'base_url:missing');
913
1116
  return null;
1117
+ }
914
1118
  let credential = null;
915
1119
  // Step 1 — platform-injected credential (XIAOE_USER_TOKEN + XIAOE_DOMAIN).
916
1120
  // Works in sandboxes: no desktop client, no Python, no developer-machine paths.
917
- if (hasXiaoEPlatformCredential(env)) {
1121
+ if (platformCredentialDetected) {
918
1122
  options.logger?.info('Trying XiaoE platform credential from XIAOE_USER_TOKEN...');
919
1123
  try {
920
1124
  credential = await resolveXiaoEPlatformCredential(baseUrl || DEFAULT_E10_BASE_URL, env);
921
1125
  }
922
1126
  catch (error) {
923
1127
  const message = error instanceof Error ? error.message : String(error);
1128
+ noteXiaoEError(diagnostics, error, secrets, 'platform_token');
924
1129
  options.logger?.warn('XiaoE platform credential failed. ' + message);
925
1130
  }
926
1131
  if (!credential) {
@@ -929,25 +1134,38 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
929
1134
  : 'XiaoE platform credential did not return usable ETEAMSID; will try xiaoeUserData/tokens.py next.');
930
1135
  }
931
1136
  }
1137
+ noteXiaoEStep(diagnostics, credential ? 'platform_token:ok' : `platform_token:${platformCredentialDetected ? 'failed' : 'missing'}`);
932
1138
  // Step 1b — compatibility path used by the reference login package. The
933
1139
  // script is shipped with the CLI npm package (never with a Skill) and its
934
1140
  // stdout stays inside this process so credentials are not exposed to agents.
935
- if (!credential && options.allowBundledEnvScript && hasXiaoEPlatformCredential(env)) {
936
- const scriptPath = xiaoEBundledEnvScriptPath();
937
- if (!scriptPath) {
938
- options.logger?.warn('CLI-bundled xiaoe_env.py was not found; will try desktop XiaoE credential sources next.');
1141
+ if (!credential) {
1142
+ if (!options.allowBundledEnvScript) {
1143
+ noteXiaoEStep(diagnostics, 'bundled_env_script:disabled');
1144
+ }
1145
+ else if (!platformCredentialDetected) {
1146
+ // 脚本本身也只读 XIAOE_USER_TOKEN/XIAOE_DOMAIN,前置缺失时重跑没有意义。
1147
+ noteXiaoEStep(diagnostics, 'bundled_env_script:missing');
939
1148
  }
940
1149
  else {
941
- options.logger?.info('Trying CLI-bundled xiaoe_env.py compatibility credential...');
942
- try {
943
- credential = await resolveXiaoEBundledEnvScriptCredential(baseUrl || DEFAULT_E10_BASE_URL, env);
1150
+ const scriptPath = xiaoEBundledEnvScriptPath();
1151
+ if (!scriptPath) {
1152
+ noteXiaoEStep(diagnostics, 'bundled_env_script:missing');
1153
+ options.logger?.warn('CLI-bundled xiaoe_env.py was not found; will try desktop XiaoE credential sources next.');
944
1154
  }
945
- catch (error) {
946
- const message = error instanceof Error ? error.message : String(error);
947
- options.logger?.warn('CLI-bundled xiaoe_env.py failed. ' + message);
948
- }
949
- if (!credential) {
950
- options.logger?.warn('CLI-bundled xiaoe_env.py did not return usable ETEAMSID; will try xiaoeUserData/tokens.py next.');
1155
+ else {
1156
+ options.logger?.info('Trying CLI-bundled xiaoe_env.py compatibility credential...');
1157
+ try {
1158
+ credential = await resolveXiaoEBundledEnvScriptCredential(baseUrl || DEFAULT_E10_BASE_URL, env);
1159
+ }
1160
+ catch (error) {
1161
+ const message = error instanceof Error ? error.message : String(error);
1162
+ noteXiaoEError(diagnostics, error, secrets, 'bundled_env_script');
1163
+ options.logger?.warn('CLI-bundled xiaoe_env.py failed. ' + message);
1164
+ }
1165
+ if (!credential) {
1166
+ options.logger?.warn('CLI-bundled xiaoe_env.py did not return usable ETEAMSID; will try xiaoeUserData/tokens.py next.');
1167
+ }
1168
+ noteXiaoEStep(diagnostics, `bundled_env_script:${credential ? 'ok' : 'failed'}`);
951
1169
  }
952
1170
  }
953
1171
  }
@@ -955,6 +1173,7 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
955
1173
  if (!credential && mayUseTokenScript) {
956
1174
  const tokenScriptPath = xiaoETokenScriptPath(env);
957
1175
  if (!tokenScriptPath) {
1176
+ noteXiaoEStep(diagnostics, 'token_script:missing');
958
1177
  options.logger?.warn('XiaoE token script not found. Expected xiaoeUserData/tokens.py; will try the local XiaoE client session next.');
959
1178
  }
960
1179
  else {
@@ -964,11 +1183,13 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
964
1183
  }
965
1184
  catch (error) {
966
1185
  const message = error instanceof Error ? error.message : String(error);
1186
+ noteXiaoEError(diagnostics, error, secrets, 'token_script');
967
1187
  options.logger?.warn('XiaoE token script failed. ' + message);
968
1188
  }
969
1189
  if (!credential) {
970
1190
  options.logger?.warn('XiaoE token script did not return usable ETEAMSID; will try the local XiaoE client session next.');
971
1191
  }
1192
+ noteXiaoEStep(diagnostics, `token_script:${credential ? 'ok' : 'failed'}`);
972
1193
  }
973
1194
  // Step 3 — built-in Node equivalent of tokens.py: reads the local XiaoE
974
1195
  // client session and exchanges it for an eteamId, so Python availability
@@ -980,20 +1201,32 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
980
1201
  }
981
1202
  catch (error) {
982
1203
  const message = error instanceof Error ? error.message : String(error);
1204
+ noteXiaoEError(diagnostics, error, secrets, 'client_session');
983
1205
  options.logger?.warn('Built-in XiaoE client credential failed. ' + message);
984
1206
  }
985
1207
  if (!credential) {
986
1208
  options.logger?.warn('Built-in XiaoE client credential did not return usable ETEAMSID; will try WEAVER_XIAOE_* credentials next.');
987
1209
  }
1210
+ noteXiaoEStep(diagnostics, `client_session:${credential ? 'ok' : 'failed'}`);
988
1211
  }
1212
+ else if (!credential) {
1213
+ noteXiaoEStep(diagnostics, 'client_session:missing');
1214
+ }
1215
+ }
1216
+ else if (!credential) {
1217
+ noteXiaoEStep(diagnostics, 'token_script:disabled');
1218
+ noteXiaoEStep(diagnostics, 'client_session:disabled');
989
1219
  }
990
1220
  if (!credential && baseUrl)
991
1221
  credential = resolveXiaoEUnifiedLoginCredential(baseUrl, env);
992
- if (!credential)
1222
+ if (!credential) {
1223
+ noteXiaoEStep(diagnostics, 'environment_map:empty');
993
1224
  return null;
1225
+ }
1226
+ noteXiaoEStep(diagnostics, `credential_source:${credential.source}`);
994
1227
  options.logger?.info('Detected XiaoE unified login for this E10 domain. Trying XiaoE auth...');
995
1228
  try {
996
- return await saveXiaoEUnifiedLogin({
1229
+ const login = await saveXiaoEUnifiedLogin({
997
1230
  baseUrl: credential.baseUrl || baseUrl || DEFAULT_E10_BASE_URL,
998
1231
  eteamsId: credential.eteamsId,
999
1232
  rawCookie: credential.rawCookie,
@@ -1002,9 +1235,18 @@ export async function tryXiaoEUnifiedLogin(options = {}) {
1002
1235
  profileHint: options.profileHint,
1003
1236
  credentialSource: describeXiaoECredentialSource(credential.source),
1004
1237
  });
1238
+ noteXiaoEStep(diagnostics, 'e10_session:ok');
1239
+ if (diagnostics) {
1240
+ // 成功换到登录态:前面压着的失败不该再让调用方误判整次恢复失败。
1241
+ delete diagnostics.lastError;
1242
+ delete diagnostics.failures;
1243
+ }
1244
+ return login;
1005
1245
  }
1006
1246
  catch (error) {
1007
1247
  const message = error instanceof Error ? error.message : String(error);
1248
+ noteXiaoEError(diagnostics, error, secrets, 'e10_session');
1249
+ noteXiaoEStep(diagnostics, 'e10_session:failed');
1008
1250
  options.logger?.warn('XiaoE unified login failed. ' + message);
1009
1251
  return null;
1010
1252
  }
@@ -1,6 +1,6 @@
1
1
  import { isXiaoERuntimeAgent } from '../../core/runtime-agent.js';
2
2
  import { getActiveProfile, readProfileConfig } from './auth/session.js';
3
- import { hasXiaoEEnv, resolveXiaoEPreferredBaseUrl, tryXiaoEUnifiedLogin } from './auth/xiaoe.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
  '未登录',
@@ -224,6 +224,24 @@ export function canAutomaticallyRecoverXiaoE(ctx, currentSession) {
224
224
  || currentSession?.loginSource === 'xiaoe-unified'
225
225
  || isXiaoERuntimeAgent(currentSession?.agentType);
226
226
  }
227
+ /**
228
+ * 透明恢复默认必须零输出:中间登录步骤绝不能漏进 Agent 的 stdout 或 JSON。
229
+ * 只有显式 `--verbose`(且非 `--json`/`--quiet`)时才把每一步的真实失败原因
230
+ * 经 debug 通道送到 stderr,供人排查。宿主 logger 若没有 debug(测试替身、
231
+ * 旧嵌入方)则继续完全静默。
232
+ */
233
+ function recoveryLogger(ctx) {
234
+ const options = globalOptions(ctx);
235
+ if (options.json || options.quiet || options.verbose !== true)
236
+ return undefined;
237
+ const adapter = ctx.logger;
238
+ if (!adapter || typeof adapter.debug !== 'function')
239
+ return undefined;
240
+ return {
241
+ info: (message) => adapter.debug(message),
242
+ warn: (message) => adapter.debug(message),
243
+ };
244
+ }
227
245
  /**
228
246
  * Silently recover a stale or missing XiaoE session. Without a local session,
229
247
  * current runtime evidence is required before obtaining credentials.
@@ -232,8 +250,20 @@ export function canAutomaticallyRecoverXiaoE(ctx, currentSession) {
232
250
  * does not start a burst of login attempts.
233
251
  */
234
252
  export async function recoverXiaoEAuthSession(ctx, currentSession) {
253
+ return (await recoverXiaoEAuthSessionWithDiagnostics(ctx, currentSession)).session;
254
+ }
255
+ /**
256
+ * `recoverXiaoEAuthSession` 的诊断版:失败时返回本次尝试评估过哪些凭证来源、
257
+ * 最后一条真实原因,供调用方挂到结构化错误上。并发恢复共享同一份诊断
258
+ * (同一个 in-flight Promise),因此每个等待者看到的原因一致。
259
+ */
260
+ export async function recoverXiaoEAuthSessionWithDiagnostics(ctx, currentSession) {
261
+ const emptyOutcome = () => ({
262
+ session: null,
263
+ diagnostics: { attemptedSteps: [] },
264
+ });
235
265
  if (!canAutomaticallyRecoverXiaoE(ctx, currentSession))
236
- return null;
266
+ return emptyOutcome();
237
267
  const options = globalOptions(ctx);
238
268
  const profile = options.profile || getActiveProfile();
239
269
  const config = readProfileConfig(profile);
@@ -246,6 +276,7 @@ export async function recoverXiaoEAuthSession(ctx, currentSession) {
246
276
  const existing = recoveryInFlight.get(key);
247
277
  if (existing)
248
278
  return existing;
279
+ const diagnostics = { attemptedSteps: [] };
249
280
  const recovery = (async () => {
250
281
  const login = await tryXiaoEUnifiedLogin({
251
282
  env: ctx.env,
@@ -255,10 +286,10 @@ export async function recoverXiaoEAuthSession(ctx, currentSession) {
255
286
  allowTokenScript: true,
256
287
  tenantKeyFallback: currentSession?.tenantKey || config.tenantKey,
257
288
  profileHint: profile,
258
- // Intentionally no logger: transparent recovery must not leak an
259
- // intermediate "login" step into agent stdout/stderr or JSON output.
289
+ diagnostics,
290
+ logger: recoveryLogger(ctx),
260
291
  });
261
- return login?.session || null;
292
+ return { session: login?.session || null, diagnostics };
262
293
  })();
263
294
  recoveryInFlight.set(key, recovery);
264
295
  try {
@@ -1,14 +1,55 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
- import { E10_AUTH_REQUIRED_MESSAGE, E10_XIAOE_RECOVERY_FAILED_MESSAGE } from './auth/messages.js';
2
+ import { E10_AUTH_REQUIRED_MESSAGE, E10_XIAOE_HOST_CREDENTIAL_MISSING_MESSAGE, E10_XIAOE_NETWORK_UNAVAILABLE_MESSAGE, E10_XIAOE_RECOVERY_FAILED_MESSAGE, } from './auth/messages.js';
3
+ import { lastXiaoEFailure, summarizeXiaoEFailureKind, } from './auth/xiaoe.js';
3
4
  import { E10AuthSession } from './auth/session.js';
4
5
  import { E10ApiClient } from './client.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), {
6
+ import { canAutomaticallyRecoverXiaoE, recoverXiaoEAuthSession, recoverXiaoEAuthSessionWithDiagnostics, } from './auth-recovery.js';
7
+ /**
8
+ * 把一次失败的静默恢复转成可定位的结构化错误。文案按归因分流:只有网络层
9
+ * 完全没打通时才提示「稍后重试」(此时登录态根本没被验证过,让人去重新登录
10
+ * 是误导);其余情况沿用原文案。诊断放进 `details`:`attemptedSteps` 说明
11
+ * 评估过哪些凭证通道及各自结果,`failures` 保留每一次失败的阶段、类别和
12
+ * `error.cause.code`,`lastError` 是最后一条真实原因(已脱敏截断),
13
+ * `resultSessionExpired` 表示票其实换到了、但被判为已过期。
14
+ */
15
+ function xiaoERecoveryFailedError(diagnostics, resultSessionExpired = false) {
16
+ const failureKind = summarizeXiaoEFailureKind(diagnostics);
17
+ const lastFailure = lastXiaoEFailure(diagnostics);
18
+ const networkUnavailable = failureKind === 'network';
19
+ // Web sandboxes only have the platform credential channel. If the runtime
20
+ // marker survived but the host credential pair did not, asking the user to
21
+ // log in again is misleading: the sandbox launcher dropped the injection.
22
+ const hostCredentialMissing = diagnostics?.clientType === 'cloud'
23
+ && diagnostics.platformCredentialDetected === false
24
+ && diagnostics.attemptedSteps.includes('platform_token:missing')
25
+ && diagnostics.attemptedSteps.includes('environment_map:empty');
26
+ const details = diagnostics
27
+ ? {
28
+ ...(diagnostics.baseUrl ? { baseUrl: diagnostics.baseUrl } : {}),
29
+ ...(diagnostics.clientType ? { clientType: diagnostics.clientType } : {}),
30
+ ...(diagnostics.platformCredentialDetected === undefined
31
+ ? {}
32
+ : { platformCredentialDetected: diagnostics.platformCredentialDetected }),
33
+ attemptedSteps: diagnostics.attemptedSteps,
34
+ ...(diagnostics.lastError ? { lastError: diagnostics.lastError } : {}),
35
+ ...(failureKind ? { errorKind: failureKind } : {}),
36
+ ...(lastFailure ? { failureStage: lastFailure.stage } : {}),
37
+ ...(diagnostics.failures?.length ? { failures: diagnostics.failures } : {}),
38
+ ...(hostCredentialMissing ? { reason: 'host_credential_not_injected' } : {}),
39
+ ...(resultSessionExpired ? { resultSessionExpired: true } : {}),
40
+ }
41
+ : undefined;
42
+ return Object.assign(new CliError(networkUnavailable
43
+ ? E10_XIAOE_NETWORK_UNAVAILABLE_MESSAGE
44
+ : hostCredentialMissing
45
+ ? E10_XIAOE_HOST_CREDENTIAL_MISSING_MESSAGE
46
+ : E10_XIAOE_RECOVERY_FAILED_MESSAGE, ExitCode.AuthError), {
8
47
  type: 'authentication',
9
- subtype: 'xiaoe_credential_unavailable',
10
- retryable: false,
11
- stage: 'xiaoe_unified_login',
48
+ subtype: networkUnavailable ? 'xiaoe_network_unavailable' : 'xiaoe_credential_unavailable',
49
+ // 网络错误是可重试的;凭证缺失/被拒重试也一样失败,不能让调用方空转。
50
+ retryable: networkUnavailable,
51
+ stage: lastFailure?.stage || 'xiaoe_unified_login',
52
+ ...(details ? { details } : {}),
12
53
  });
13
54
  }
14
55
  export async function getE10Session(ctx, load = {}) {
@@ -18,16 +59,19 @@ export async function getE10Session(ctx, load = {}) {
18
59
  export async function requireE10Session(ctx, load = {}) {
19
60
  let session = await getE10Session(ctx, load);
20
61
  if (session?.isExpired() && canAutomaticallyRecoverXiaoE(ctx, session)) {
21
- session = await recoverXiaoEAuthSession(ctx, session).catch(() => null);
22
- if (!session || session.isExpired())
23
- throw xiaoERecoveryFailedError();
62
+ const outcome = await recoverXiaoEAuthSessionWithDiagnostics(ctx, session).catch(() => null);
63
+ session = outcome?.session || null;
64
+ if (!session || session.isExpired()) {
65
+ throw xiaoERecoveryFailedError(outcome?.diagnostics, session?.isExpired() === true);
66
+ }
24
67
  }
25
68
  if (!session && canAutomaticallyRecoverXiaoE(ctx)) {
26
69
  // Read-only probes use getE10Session; only a command requiring a session
27
70
  // may establish one from the current XiaoE host's credentials.
28
- session = await recoverXiaoEAuthSession(ctx).catch(() => null);
71
+ const outcome = await recoverXiaoEAuthSessionWithDiagnostics(ctx).catch(() => null);
72
+ session = outcome?.session || null;
29
73
  if (!session) {
30
- throw xiaoERecoveryFailedError();
74
+ throw xiaoERecoveryFailedError(outcome?.diagnostics);
31
75
  }
32
76
  }
33
77
  if (!session) {
@@ -1,4 +1,4 @@
1
1
  export { E10ApiClient } from './client.js';
2
2
  export { getE10Client, getE10Session, requireE10Client, requireE10Session } from './context.js';
3
3
  export { E10ApiError, E10RequestRuntime, createE10RequestRuntime } from './request-runtime.js';
4
- export { canAutomaticallyRecoverXiaoE, isE10AuthenticationRequired, recoverXiaoEAuthSession, } from './auth-recovery.js';
4
+ export { canAutomaticallyRecoverXiaoE, isE10AuthenticationRequired, recoverXiaoEAuthSession, recoverXiaoEAuthSessionWithDiagnostics, } from './auth-recovery.js';