weaver-work-cli 0.1.36 → 0.1.38

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 (93) hide show
  1. package/dist/chunks/{archive-XBVH7JU7.js → archive-PDV724YK.js} +5 -4
  2. package/dist/chunks/{archive-oa-MZM3EKFQ.js → archive-oa-4XE4MNXQ.js} +4 -3
  3. package/dist/chunks/{asset-4XO7UOUW.js → asset-PDL2QE2N.js} +8 -7
  4. package/dist/chunks/{attachment-content-VXSWANAC.js → attachment-content-MDDGOE5E.js} +3 -3
  5. package/dist/chunks/{blog-PGZKOPSC.js → blog-UYR3EUXW.js} +5 -4
  6. package/dist/chunks/{caizhilian-BH4ZVGUN.js → caizhilian-LG2X5NMF.js} +5 -4
  7. package/dist/chunks/{calendar-MXEMTIO4.js → calendar-FG4BAZHE.js} +4 -3
  8. package/dist/chunks/{chunk-NCOVUAZK.js → chunk-6FTTSA45.js} +4 -3
  9. package/dist/chunks/{chunk-HXJQI5NM.js → chunk-6LTFIFLM.js} +3 -2
  10. package/dist/chunks/{chunk-QYGO5EE2.js → chunk-CORFRD7Q.js} +4 -3
  11. package/dist/chunks/{chunk-QHFANTX7.js → chunk-DMV2TOMH.js} +1348 -90
  12. package/dist/chunks/{chunk-TC6MZAO3.js → chunk-ELYDWBZJ.js} +52 -2
  13. package/dist/chunks/{chunk-JCSN3JGM.js → chunk-FUWVHMVV.js} +7 -6
  14. package/dist/chunks/{chunk-BPE37FPB.js → chunk-GGJFMNJM.js} +3 -3
  15. package/dist/chunks/{chunk-6LLJFKJE.js → chunk-IZUDA353.js} +1 -1
  16. package/dist/chunks/{chunk-XMGSYD6S.js → chunk-J7USS3ED.js} +4 -4
  17. package/dist/chunks/{chunk-F746NKNF.js → chunk-JSMFPO43.js} +1 -1
  18. package/dist/chunks/{chunk-T72N74ZT.js → chunk-KKRL3REV.js} +5 -4
  19. package/dist/chunks/{chunk-VPUTVEMC.js → chunk-NI6GIPIM.js} +98 -3
  20. package/dist/chunks/{chunk-VPXOPQJI.js → chunk-NR6RKXPA.js} +2 -2
  21. package/dist/chunks/{chunk-JNG42JKG.js → chunk-OJQUJZQF.js} +4 -6
  22. package/dist/chunks/{chunk-I6QGIBDM.js → chunk-UJJD4QWF.js} +4 -3
  23. package/dist/chunks/{chunk-JUXVYKII.js → chunk-UZZBA6PV.js} +174 -14
  24. package/dist/chunks/{chunk-JYKABYCV.js → chunk-VRYHGG6O.js} +2 -2
  25. package/dist/chunks/{chunk-WXFPWCXG.js → chunk-XJY7G4DD.js} +3 -3
  26. package/dist/chunks/{chunk-5DQEEGCC.js → chunk-Z32KWY6G.js} +1 -1
  27. package/dist/chunks/{conflict-JAB2LJEZ.js → conflict-BQZEQ4SP.js} +2 -2
  28. package/dist/chunks/{contract-XI2EEIZU.js → contract-XIRAFTEV.js} +4 -3
  29. package/dist/chunks/{cowork-SCPGLBA4.js → cowork-BLG62BL2.js} +4 -3
  30. package/dist/chunks/{ebuilder-form-EYLHNOTA.js → ebuilder-form-NUKG5QW4.js} +3 -3
  31. package/dist/chunks/{ehr-EAP77IMT.js → ehr-2STW5ILH.js} +4 -3
  32. package/dist/chunks/{esb-2TGGXEE7.js → esb-UUDQUNKY.js} +4 -4
  33. package/dist/chunks/{fna-2X7ZE7BU.js → fna-6RCY4NU5.js} +4 -3
  34. package/dist/chunks/{host-C6EL2RKL.js → host-7RXEDMAH.js} +3 -3
  35. package/dist/chunks/{host-XCLDDXKQ.js → host-EJZFROU3.js} +3 -3
  36. package/dist/chunks/{host-I4NRK4MX.js → host-FWDOSRH5.js} +3 -3
  37. package/dist/chunks/{host-Q4LZGCRU.js → host-RHM3NPXV.js} +3 -3
  38. package/dist/chunks/{host-UEI5FGDM.js → host-UFF33TWJ.js} +3 -3
  39. package/dist/chunks/{hrm-YXERJM76.js → hrm-PKJ6ELSH.js} +3 -3
  40. package/dist/chunks/{im-A4VZN2TQ.js → im-YJAXS7CO.js} +3 -3
  41. package/dist/chunks/{invoice-2Z6YPMIS.js → invoice-HWVSXGX6.js} +3 -3
  42. package/dist/chunks/{jiuchuanhui-AR5GMKZB.js → jiuchuanhui-L7VKILZB.js} +4 -3
  43. package/dist/chunks/{lazy-dispatch-QXW7P4X3.js → lazy-dispatch-NMYEIHRH.js} +4 -4
  44. package/dist/chunks/{mail-7GZ6CPBJ.js → mail-B42RRU7G.js} +4 -3
  45. package/dist/chunks/{meeting-3NMGA3MB.js → meeting-CFYVBJSS.js} +13 -12
  46. package/dist/chunks/{okr-BZ6XUOAG.js → okr-GCLF6V7W.js} +4 -3
  47. package/dist/chunks/{plan-DR5CMUYP.js → plan-ZVL7GBRI.js} +4 -3
  48. package/dist/chunks/{project-6J63MK33.js → project-4YWAAE2K.js} +4 -3
  49. package/dist/chunks/{registry-VRTNC6UR.js → registry-EP4VTHZS.js} +8 -8
  50. package/dist/chunks/{registry-KDD7DHNK.js → registry-FRJ6H7BT.js} +4 -4
  51. package/dist/chunks/{registry-TKX2E7C7.js → registry-SG2PGVCF.js} +2 -2
  52. package/dist/chunks/{room-recommend-XCWRMB5P.js → room-recommend-R3JVUUAC.js} +4 -4
  53. package/dist/chunks/{schedule-IVBICP32.js → schedule-NIJBTYDO.js} +4 -4
  54. package/dist/chunks/{workflow-D74NFK3C.js → workflow-BB24DFBY.js} +4 -4
  55. package/dist/index.js +27 -27
  56. package/dist/internal/e10/auth-recovery.js +114 -1
  57. package/dist/internal/e10/client.js +16 -2
  58. package/dist/internal/e10/request-runtime.js +103 -7
  59. package/dist/shortcuts/archive/host.js +3 -2
  60. package/dist/shortcuts/archive-oa/host.js +2 -1
  61. package/dist/shortcuts/asset/host.js +6 -5
  62. package/dist/shortcuts/blog/host.js +3 -2
  63. package/dist/shortcuts/caizhilian/host.js +3 -2
  64. package/dist/shortcuts/calendar/host.js +2 -1
  65. package/dist/shortcuts/contract/host.js +2 -1
  66. package/dist/shortcuts/cowork/host.js +2 -1
  67. package/dist/shortcuts/ebuilder-form/host.js +3 -1
  68. package/dist/shortcuts/ehr/host.js +2 -1
  69. package/dist/shortcuts/esb/host.js +2 -1
  70. package/dist/shortcuts/fna/host.js +2 -1
  71. package/dist/shortcuts/hrm/host.js +2 -1
  72. package/dist/shortcuts/im/host.js +3 -2
  73. package/dist/shortcuts/invoice/host.js +5 -4
  74. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  75. package/dist/shortcuts/mail/host.js +2 -1
  76. package/dist/shortcuts/meeting/host.js +3 -2
  77. package/dist/shortcuts/meeting/invitation/text.js +71 -0
  78. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1073 -50
  79. package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
  80. package/dist/shortcuts/meeting/operations/eb/factory.js +60 -1
  81. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  82. package/dist/shortcuts/meeting/operations/invitation.js +122 -9
  83. package/dist/shortcuts/okr/host.js +2 -1
  84. package/dist/shortcuts/plan/host.js +2 -1
  85. package/dist/shortcuts/project/host.js +2 -1
  86. package/dist/shortcuts/workflow/operations/form.js +160 -2
  87. package/package.json +1 -1
  88. package/skills/weaver-e10-meeting/SKILL.md +250 -301
  89. package/skills/weaver-e10-meeting/references/meeting-eb.md +203 -0
  90. package/skills/weaver-e10-meeting/references/meeting-invitation.md +37 -5
  91. package/skills/weaver-e10-meeting.zip +0 -0
  92. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  93. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
@@ -1,8 +1,10 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
- import { isE10AuthenticationRequired, recoverXiaoEAuthSession, sameE10SessionIdentity, } from './auth-recovery.js';
2
+ import { classifyE10Redirect, isE10AuthenticationRequired, recoverXiaoEAuthSession, sameE10SessionIdentity, } from './auth-recovery.js';
3
3
  import { startFetchTiming } from './fetch-timing.js';
4
4
  const DEFAULT_TIMEOUT_MS = 30_000;
5
5
  const DEFAULT_BIG_INT_STRINGS = true;
6
+ /** 下载直传最多跟几跳:E10 -> 对象存储通常 1 跳,留一点余量但不允许成环。 */
7
+ const MAX_DOWNLOAD_REDIRECTS = 3;
6
8
  const RESERVED_HEADERS = [
7
9
  'authorization',
8
10
  'cookie',
@@ -63,16 +65,18 @@ export class E10RequestRuntime {
63
65
  }
64
66
  stripReservedHeaders(mutable.headers);
65
67
  const requestEteamsId = this.session.eteamsId;
66
- let parsed = await this.executeRequest(mutable, request.trustedHeaders);
68
+ let attempt = await this.executeRequest(mutable, request.trustedHeaders);
67
69
  if (!mutable.streamBody
68
- && isE10AuthenticationRequired(parsed)
70
+ // 已交接给文件服务的响应不是 E10 的接口回答,登录态不该由它判定。
71
+ && !attempt.fromFileRedirect
72
+ && isE10AuthenticationRequired(authenticationProbe(mutable, attempt.parsed))
69
73
  && await this.recoverExpiredSession(requestEteamsId)) {
70
74
  // Authentication rejection is a definitive pre-business failure. Retry
71
75
  // exactly once with the new session; never retry transport/partial errors.
72
- parsed = await this.executeRequest(mutable, request.trustedHeaders);
76
+ attempt = await this.executeRequest(mutable, request.trustedHeaders);
73
77
  }
74
- await this.runAfterResponse(mutable, this.sessionContext(), parsed);
75
- return parsed;
78
+ await this.runAfterResponse(mutable, this.sessionContext(), attempt.parsed);
79
+ return attempt.parsed;
76
80
  }
77
81
  catch (error) {
78
82
  await this.runOnError(mutable, this.sessionContext(), error);
@@ -84,7 +88,74 @@ export class E10RequestRuntime {
84
88
  const headers = this.composeHeaders(request, trustedHeaders);
85
89
  const init = this.buildFetchInit(request, headers);
86
90
  const response = await this.fetchWithTimeout(url, init, request.timeoutMs);
87
- return this.readResponse(response, request.responseType, request.bigIntStrings, request.maxResponseBytes, request.timeoutMs);
91
+ // 二进制下载:E10 常以 302 把文件交接给文件服务/对象存储。这里直接跟过去
92
+ // 取字节,既不会把「文件流」当成登录失效,也不会触发无谓的重新登录。
93
+ const followed = request.responseType === 'binary'
94
+ ? await this.followDownloadRedirect(response, request, url)
95
+ : { response, followed: false };
96
+ if (followed.followed && (followed.response.status === 401 || followed.response.status === 403)) {
97
+ // 这是文件服务对直链的回答(签名过期/无权限),与 E10 登录态无关:
98
+ // 报清楚,别让调用方误以为要重新登录。
99
+ throw new E10ApiError('network', 'download_file_service_denied', `文件服务拒绝下载(HTTP ${followed.response.status}),直链可能已过期,请重试下载`, {
100
+ retryable: true,
101
+ exitCode: ExitCode.NetworkError,
102
+ details: { status: followed.response.status },
103
+ });
104
+ }
105
+ return {
106
+ parsed: await this.readResponse(followed.response, request.responseType, request.bigIntStrings, request.maxResponseBytes, request.timeoutMs),
107
+ fromFileRedirect: followed.followed,
108
+ };
109
+ }
110
+ /**
111
+ * 跟随下载直传的 3xx。仅当跳转目标是文件服务(非登录页)时才跟随,最多
112
+ * `MAX_DOWNLOAD_REDIRECTS` 跳;登录类跳转原样返回,交给上层认证恢复处理。
113
+ * 凭证规则:同源才继续带 Cookie/eteamsid,跨域(对象存储)不带任何鉴权头。
114
+ */
115
+ async followDownloadRedirect(response, request, requestUrl) {
116
+ const baseOrigin = safeOrigin(this.session.getBaseUrl());
117
+ let current = response;
118
+ let currentUrl = requestUrl;
119
+ let followed = false;
120
+ for (let hop = 0; hop < MAX_DOWNLOAD_REDIRECTS; hop += 1) {
121
+ const kind = classifyE10Redirect({
122
+ status: current.status,
123
+ headers: current.headers,
124
+ requestPath: request.path,
125
+ });
126
+ if (kind !== 'file')
127
+ return { response: current, followed };
128
+ const location = current.headers.get('location');
129
+ if (!location)
130
+ return { response: current, followed };
131
+ let next;
132
+ try {
133
+ next = new URL(location, currentUrl);
134
+ }
135
+ catch {
136
+ return { response: current, followed };
137
+ }
138
+ if (!/^https?:$/u.test(next.protocol))
139
+ return { response: current, followed };
140
+ const sameOrigin = baseOrigin !== '' && safeOrigin(next.toString()) === baseOrigin;
141
+ current = await this.fetchWithTimeout(next, this.buildDownloadRedirectInit(sameOrigin), request.timeoutMs);
142
+ currentUrl = next;
143
+ followed = true;
144
+ }
145
+ return { response: current, followed };
146
+ }
147
+ buildDownloadRedirectInit(sameOrigin) {
148
+ const headers = {
149
+ Accept: 'application/octet-stream, */*',
150
+ 'User-Agent': this.session.uaHeader(),
151
+ };
152
+ if (sameOrigin) {
153
+ headers.Cookie = this.attachSessionTicket
154
+ ? this.session.cookieHeaderWithSessionTicket()
155
+ : this.session.cookieHeader();
156
+ headers.eteamsid = this.session.eteamsId;
157
+ }
158
+ return { method: 'GET', headers, redirect: 'manual' };
88
159
  }
89
160
  async recoverExpiredSession(requestEteamsId) {
90
161
  // Another concurrent request may already have refreshed the shared session
@@ -276,6 +347,31 @@ export class E10RequestRuntime {
276
347
  export function parseJsonPreservingBigInts(text) {
277
348
  return JSON.parse(quoteLargeJsonIntegers(text));
278
349
  }
350
+ /**
351
+ * 组装认证探测入参。二进制响应不能把文件字节交给关键词扫描(乱码里出现
352
+ * 「login」会误判成登录失效),只保留状态码与跳转目标;同时带上请求路径,
353
+ * 让「下载接口 -> 文件服务」的 3xx 能被识别为交接而不是失效。
354
+ */
355
+ function authenticationProbe(request, response) {
356
+ if ('content' in response) {
357
+ return { status: response.status, headers: response.headers, requestPath: request.path };
358
+ }
359
+ return {
360
+ status: response.status,
361
+ headers: response.headers,
362
+ requestPath: request.path,
363
+ text: response.text,
364
+ value: response.value,
365
+ };
366
+ }
367
+ function safeOrigin(rawUrl) {
368
+ try {
369
+ return new URL(rawUrl).origin.toLowerCase();
370
+ }
371
+ catch {
372
+ return '';
373
+ }
374
+ }
279
375
  const LONG_INTEGER_MIN_DIGITS = 16;
280
376
  // 一趟扫描把 JSON 拆成「字符串字面量」与「数字」两类 token:字符串整体跳过,
281
377
  // 数字只在它是纯整数且位数够长时补引号。逐字符拼串在 MB 级响应上要 100 ms 以上,
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { ArchiveCliError, archiveAuthError, archiveNetworkError, archiveValidationError, } from './errors.js';
6
7
  function inferProfile(ctx) {
7
8
  const options = ctx.getGlobalOptions();
@@ -151,7 +152,7 @@ export function createArchiveHost(ctx, dependencies = {}) {
151
152
  }
152
153
  async function request(request) {
153
154
  const response = await doRequest(request, false).catch((error) => mapRuntimeNetworkError(error));
154
- if (response.status === 401 || response.status === 302) {
155
+ if (isE10SessionExpiredResponse(response)) {
155
156
  throw archiveAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
156
157
  }
157
158
  if (response.parseError) {
@@ -182,7 +183,7 @@ export function createArchiveHost(ctx, dependencies = {}) {
182
183
  }
183
184
  async function requestBinary(request) {
184
185
  const response = await doRequest(request, true).catch((error) => mapRuntimeNetworkError(error));
185
- if (response.status === 401 || response.status === 302) {
186
+ if (isE10SessionExpiredResponse(response)) {
186
187
  throw archiveAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
187
188
  }
188
189
  if (response.status < 200 || response.status >= 300) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { ArchiveOaCliError, archiveOaAuthError, archiveOaNetworkError, archiveOaValidationError, } from './errors.js';
6
7
  /** 本模块允许调用的路径前缀:ebuilder 业务接口与 OpenESB 开放服务。 */
7
8
  const ALLOWED_PATH_PREFIXES = ['/api/', '/openserver/'];
@@ -122,7 +123,7 @@ export function createArchiveOaHost(ctx, dependencies = {}) {
122
123
  headers: { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders },
123
124
  timeoutMs,
124
125
  }).catch((error) => mapRuntimeNetworkError(error));
125
- if (response.status === 401 || response.status === 302) {
126
+ if (isE10SessionExpiredResponse(response)) {
126
127
  throw archiveOaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
127
128
  }
128
129
  if (response.parseError) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { ziguanjiaAuthError, ziguanjiaBusinessError, ziguanjiaNetworkError, ziguanjiaValidationError, } from './errors.js';
6
7
  const EBUILDER_PREFIX = process.env.WEAVER_EBUILDER_PREFIX || '/api/ebuilder';
7
8
  const ESB_TRIGGER_PATH = '/api/esb/server/event/triggerActionFlow';
@@ -176,7 +177,7 @@ export function createZiguanjiaHost(ctx) {
176
177
  query,
177
178
  bigIntStrings: true,
178
179
  });
179
- if (response.status === 401 || response.status === 302) {
180
+ if (isE10SessionExpiredResponse(response)) {
180
181
  throw ziguanjiaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
181
182
  }
182
183
  if (response.parseError) {
@@ -242,7 +243,7 @@ export function createZiguanjiaHost(ctx) {
242
243
  formData: fd,
243
244
  bigIntStrings: false,
244
245
  });
245
- if (response.status === 401 || response.status === 302) {
246
+ if (isE10SessionExpiredResponse(response)) {
246
247
  throw ziguanjiaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
247
248
  }
248
249
  if (response.parseError) {
@@ -315,7 +316,7 @@ export function createZiguanjiaHost(ctx) {
315
316
  },
316
317
  bigIntStrings: true,
317
318
  });
318
- if (response.status === 401 || response.status === 302) {
319
+ if (isE10SessionExpiredResponse(response)) {
319
320
  throw ziguanjiaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
320
321
  }
321
322
  if (response.parseError) {
@@ -363,7 +364,7 @@ export function createZiguanjiaHost(ctx) {
363
364
  body: fullBody,
364
365
  bigIntStrings: true,
365
366
  });
366
- if (response.status === 401 || response.status === 302) {
367
+ if (isE10SessionExpiredResponse(response)) {
367
368
  throw ziguanjiaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
368
369
  }
369
370
  if (response.parseError) {
@@ -393,7 +394,7 @@ export function createZiguanjiaHost(ctx) {
393
394
  body: payload,
394
395
  bigIntStrings: true,
395
396
  });
396
- if (response.status === 401 || response.status === 302) {
397
+ if (isE10SessionExpiredResponse(response)) {
397
398
  throw ziguanjiaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
398
399
  }
399
400
  if (response.parseError) {
@@ -3,6 +3,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
3
3
  import { requireE10Session } from '../../internal/e10/context.js';
4
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
5
5
  import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
6
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
6
7
  import { BlogCliError, blogAuthError, blogNetworkError, blogValidationError, } from './errors.js';
7
8
  const MAX_EXPORT_BYTES = 50 * 1024 * 1024;
8
9
  /** E10 登录失效在业务响应体里的 code(实测 -2 既可能是数字也可能是字符串) */
@@ -91,7 +92,7 @@ export function createBlogHost(ctx, dependencies = {}) {
91
92
  body,
92
93
  timeoutMs,
93
94
  });
94
- if (response.status === 401 || response.status === 302) {
95
+ if (isE10SessionExpiredResponse(response)) {
95
96
  throw blogAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
96
97
  }
97
98
  if (response.parseError) {
@@ -134,7 +135,7 @@ export function createBlogHost(ctx, dependencies = {}) {
134
135
  timeoutMs,
135
136
  headers: { Accept: 'application/octet-stream, */*' },
136
137
  });
137
- if (response.status === 401 || response.status === 302) {
138
+ if (isE10SessionExpiredResponse(response)) {
138
139
  throw blogAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
139
140
  }
140
141
  const contentType = response.contentType;
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { CaizhilianCliError, caizhilianAuthError, caizhilianNetworkError, caizhilianValidationError, } from './errors.js';
6
7
  /** 业务请求默认超时(源 assets/config.json:http.timeout = 12 秒)。 */
7
8
  export const CAIZHILAN_TIMEOUT_MS = 12_000;
@@ -152,7 +153,7 @@ export function createCaizhilianHost(ctx, dependencies = {}) {
152
153
  }
153
154
  async function request(request) {
154
155
  const response = await doRequest(request, false).catch((error) => mapRuntimeNetworkError(error));
155
- if (response.status === 401 || response.status === 302) {
156
+ if (isE10SessionExpiredResponse(response)) {
156
157
  throw caizhilianAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
157
158
  }
158
159
  if (response.parseError) {
@@ -183,7 +184,7 @@ export function createCaizhilianHost(ctx, dependencies = {}) {
183
184
  }
184
185
  async function requestBinary(request) {
185
186
  const response = await doRequest(request, true).catch((error) => mapRuntimeNetworkError(error));
186
- if (response.status === 401 || response.status === 302) {
187
+ if (isE10SessionExpiredResponse(response)) {
187
188
  throw caizhilianAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
188
189
  }
189
190
  if (response.status < 200 || response.status >= 300) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { calendarAuthError, calendarNetworkError, CalendarCliError, calendarValidationError } from './errors.js';
6
7
  function inferProfile(ctx) {
7
8
  const options = ctx.getGlobalOptions();
@@ -110,7 +111,7 @@ export function createCalendarHost(ctx, dependencies = {}) {
110
111
  catch (error) {
111
112
  mapRuntimeNetworkError(error);
112
113
  }
113
- if (response.status === 401 || response.status === 302) {
114
+ if (isE10SessionExpiredResponse(response)) {
114
115
  throw calendarAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
115
116
  }
116
117
  if (response.parseError) {
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { basename } from 'node:path';
3
3
  import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
4
+ import { isE10SessionExpiredResponse, } from '../../internal/e10/auth-recovery.js';
4
5
  import { requireE10Session } from '../../internal/e10/context.js';
5
6
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
6
7
  import { E10ApiError, createE10RequestRuntime, } from '../../internal/e10/request-runtime.js';
@@ -179,7 +180,7 @@ export function createContractHost(ctx, dependencies = {}) {
179
180
  }
180
181
  throw error;
181
182
  }
182
- if (response.status === 401 || response.status === 302) {
183
+ if (isE10SessionExpiredResponse(response)) {
183
184
  throw contractAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status, path });
184
185
  }
185
186
  const { text, value } = response;
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { CoworkCliError, coworkAuthError, coworkNetworkError, coworkValidationError } from './errors.js';
6
7
  import { coworkPerf } from './operations/types.js';
7
8
  function inferProfile(ctx) {
@@ -147,7 +148,7 @@ export function createCoworkHost(ctx, dependencies = {}) {
147
148
  coworkPerf.requestCount += 1;
148
149
  coworkPerf.requestMs += Date.now() - reqStart;
149
150
  }
150
- if (response.status === 401 || response.status === 302) {
151
+ if (isE10SessionExpiredResponse(response)) {
151
152
  throw coworkAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
152
153
  }
153
154
  if (response.parseError) {
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
3
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
5
  import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
6
  import { ebuilderAuthError, ebuilderBusinessError, ebuilderNetworkError, ebuilderValidationError, } from './errors.js';
@@ -231,7 +232,8 @@ export function createEbuilderFormHost(ctx) {
231
232
  headers: options.headers,
232
233
  bigIntStrings: options.bigIntStrings ?? true,
233
234
  });
234
- if ([302, 401, 403].includes(response.status)) {
235
+ // 403 在 eBuilder 这里偏「无权」,3xx 只有跳到登录页才算登录失效(文件服务交接不算)。
236
+ if (isE10SessionExpiredResponse(response) || response.status === 403) {
235
237
  throw ebuilderAuthError('session_expired', 'E10 登录态已失效或当前账号无权访问该 eBuilder 数据', { httpStatus: response.status });
236
238
  }
237
239
  if (response.parseError) {
@@ -3,6 +3,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
3
3
  import { requireE10Session } from '../../internal/e10/context.js';
4
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
5
5
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
6
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
6
7
  import { ehrAuthError, ehrNetworkError, EhrCliError, ehrValidationError } from './errors.js';
7
8
  function inferProfile(ctx) {
8
9
  const options = ctx.getGlobalOptions();
@@ -165,7 +166,7 @@ export function createEhrHost(ctx, dependencies = {}) {
165
166
  catch (error) {
166
167
  mapRuntimeNetworkError(error);
167
168
  }
168
- if (response.status === 401 || response.status === 302) {
169
+ if (isE10SessionExpiredResponse(response)) {
169
170
  throw ehrAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
170
171
  }
171
172
  if (response.parseError) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { EsbCliError, esbApiError, esbAuthError, esbNetworkError, esbPermissionError, esbValidationError, esbWriteUncertain, } from './errors.js';
6
7
  /** 源接口文档给出的去域名路径;域名与认证由 E10 会话层托管。 */
7
8
  export const ESB_PATHS = {
@@ -117,7 +118,7 @@ export function createEsbHost(ctx, dependencies = {}) {
117
118
  catch (error) {
118
119
  mapRuntimeNetworkError(error);
119
120
  }
120
- if (response.status === 401 || response.status === 302) {
121
+ if (isE10SessionExpiredResponse(response)) {
121
122
  throw esbAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
122
123
  }
123
124
  if (response.parseError) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { FnaCliError, fnaAuthError, fnaNetworkError, fnaValidationError } from './errors.js';
6
7
  function inferProfile(ctx) {
7
8
  const options = ctx.getGlobalOptions();
@@ -116,7 +117,7 @@ export function createFnaHost(ctx, dependencies = {}) {
116
117
  catch (error) {
117
118
  mapRuntimeNetworkError(error);
118
119
  }
119
- if (response.status === 401 || response.status === 302) {
120
+ if (isE10SessionExpiredResponse(response)) {
120
121
  throw fnaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
121
122
  }
122
123
  if (response.parseError) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { HrmCliError, hrmAuthError, hrmNetworkError, hrmValidationError } from './errors.js';
6
7
  function inferProfile(ctx) {
7
8
  const options = ctx.getGlobalOptions();
@@ -114,7 +115,7 @@ export function createHrmHost(ctx, dependencies = {}) {
114
115
  catch (error) {
115
116
  mapRuntimeNetworkError(error);
116
117
  }
117
- if (response.status === 401 || response.status === 302) {
118
+ if (isE10SessionExpiredResponse(response)) {
118
119
  throw hrmAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
119
120
  }
120
121
  if (response.parseError) {
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, extname, resolve } from 'node:path';
5
5
  import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
6
+ import { isE10SessionExpiredResponse, } from '../../internal/e10/auth-recovery.js';
6
7
  import { requireE10Session } from '../../internal/e10/context.js';
7
8
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
8
9
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -121,7 +122,7 @@ export function createImHost(ctx, dependencies = {}) {
121
122
  catch (error) {
122
123
  mapRuntimeNetworkError(error);
123
124
  }
124
- if (response.status === 401 || response.status === 302) {
125
+ if (isE10SessionExpiredResponse(response)) {
125
126
  throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
126
127
  }
127
128
  if (response.parseError) {
@@ -182,7 +183,7 @@ export function createImHost(ctx, dependencies = {}) {
182
183
  catch (error) {
183
184
  mapRuntimeNetworkError(error);
184
185
  }
185
- if (response.status === 401 || response.status === 302) {
186
+ if (isE10SessionExpiredResponse(response)) {
186
187
  throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE);
187
188
  }
188
189
  if (response.status !== 200) {
@@ -4,6 +4,7 @@ import { E10_REAUTH_GUIDANCE, E10_SESSION_EXPIRED_MESSAGE } from '../../internal
4
4
  import { requireE10Session } from '../../internal/e10/context.js';
5
5
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
6
6
  import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
7
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
7
8
  import { InvoiceCliError, invoiceAuthError, invoiceNetworkError, invoiceValidationError, } from './errors.js';
8
9
  const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
9
10
  const TOKEN_SERVICE = 'cn.weaver.weaver-work-cli.invoice-token';
@@ -199,7 +200,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
199
200
  timeoutMs,
200
201
  trustedHeaders: trustedInvoiceHeaders(token),
201
202
  });
202
- if (response.status === 401 || response.status === 302) {
203
+ if (isE10SessionExpiredResponse(response)) {
203
204
  throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
204
205
  }
205
206
  if (response.parseError) {
@@ -258,7 +259,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
258
259
  timeoutMs,
259
260
  trustedHeaders: trustedInvoiceHeaders(token),
260
261
  });
261
- if (response.status === 401 || response.status === 302) {
262
+ if (isE10SessionExpiredResponse(response)) {
262
263
  throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
263
264
  }
264
265
  if (response.parseError) {
@@ -293,7 +294,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
293
294
  timeoutMs,
294
295
  trustedHeaders: trustedInvoiceHeaders(token),
295
296
  });
296
- if (response.status === 401 || response.status === 302) {
297
+ if (isE10SessionExpiredResponse(response)) {
297
298
  throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
298
299
  }
299
300
  if (response.parseError) {
@@ -320,7 +321,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
320
321
  timeoutMs,
321
322
  trustedHeaders: trustedInvoiceHeaders(token),
322
323
  });
323
- if (response.status === 401 || response.status === 302) {
324
+ if (isE10SessionExpiredResponse(response)) {
324
325
  throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
325
326
  }
326
327
  const contentType = response.contentType;
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { JiuchuanhuiCliError, jiuchuanhuiAuthError, jiuchuanhuiNetworkError, jiuchuanhuiValidationError, } from './errors.js';
6
7
  function inferProfile(ctx) {
7
8
  const options = ctx.getGlobalOptions();
@@ -127,7 +128,7 @@ export function createJiuchuanhuiHost(ctx, dependencies = {}) {
127
128
  catch (error) {
128
129
  mapRuntimeNetworkError(error);
129
130
  }
130
- if (response.status === 401 || response.status === 302) {
131
+ if (isE10SessionExpiredResponse(response)) {
131
132
  throw jiuchuanhuiAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
132
133
  }
133
134
  if (response.parseError) {
@@ -3,6 +3,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
3
3
  import { requireE10Session } from '../../internal/e10/context.js';
4
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
5
5
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
6
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
6
7
  import { mailAuthError, mailNetworkError, MailCliError, mailValidationError } from './errors.js';
7
8
  import { mailPerf } from './operations/types.js';
8
9
  function inferProfile(ctx) {
@@ -160,7 +161,7 @@ export function createMailHost(ctx, dependencies = {}) {
160
161
  mailPerf.requestCount += 1;
161
162
  mailPerf.requestMs += Date.now() - reqStart;
162
163
  }
163
- if (response.status === 401 || response.status === 302) {
164
+ if (isE10SessionExpiredResponse(response)) {
164
165
  throw mailAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
165
166
  }
166
167
  if (response.parseError) {
@@ -1,4 +1,5 @@
1
1
  import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
2
+ import { isE10SessionExpiredResponse, } from '../../internal/e10/auth-recovery.js';
2
3
  import { requireE10Session } from '../../internal/e10/context.js';
3
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
5
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -127,7 +128,7 @@ export function createMeetingHost(ctx, dependencies = {}) {
127
128
  catch (error) {
128
129
  mapRuntimeNetworkError(error);
129
130
  }
130
- if (response.status === 401 || response.status === 302) {
131
+ if (isE10SessionExpiredResponse(response)) {
131
132
  throw meetingAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
132
133
  }
133
134
  if (response.status !== 200) {
@@ -177,7 +178,7 @@ export function createMeetingHost(ctx, dependencies = {}) {
177
178
  catch (error) {
178
179
  mapRuntimeNetworkError(error);
179
180
  }
180
- if (response.status === 401 || response.status === 302) {
181
+ if (isE10SessionExpiredResponse(response)) {
181
182
  throw meetingAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
182
183
  }
183
184
  if (response.parseError) {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 邀请函的**对话内文本版**(Markdown),与 docx 版同源同字段。
3
+ *
4
+ * 为什么要有这一份(2026-10-31 用户明确要求):
5
+ * 「邀请函不是在对话里给个文件让我自己下载,而是**直接展示在对话里**」。
6
+ * docx 是给人打印/发出去的成品,但小E 会话里用户看不到 docx 内容 ——
7
+ * 只回一个 filePath,用户还得去本地打开才知道写了什么。
8
+ * 所以渲染 docx 的同时产出一份纯文本,模型可以把正文直接贴进回复。
9
+ *
10
+ * 版式与 `templates/meeting/invitation.docx` 一致:标题 / 称呼 / 邀请语 /
11
+ * 会议要素 / 议程表 / 落款 / 备注。取不到的字段统一写【待补充】,与 docx 的黄色高亮同义。
12
+ */
13
+ import { PENDING_MARK } from './docx.js';
14
+ /** 单元格里的 `|` 会破坏 Markdown 表格,换行会破坏行结构 —— 统一折成空格。 */
15
+ function cell(value) {
16
+ return String(value ?? '').replace(/\|/g, '/').replace(/\s*\r?\n\s*/g, ' ').trim();
17
+ }
18
+ /**
19
+ * 生成邀请函的对话文本版。
20
+ *
21
+ * @param resolver 与 docx 共用的占位符解析器(保证两版内容一字不差)
22
+ * @param agendaRows 议程行(来自会议议题 / extra.agenda / 模板示例行)
23
+ * @param options.pendingHint 是否在末尾附「【待补充】需人工补」的提示(默认附)
24
+ */
25
+ export function renderInvitationMarkdown(resolver, agendaRows, options = {}) {
26
+ const pending = new Set();
27
+ const val = (key) => {
28
+ const rendered = resolver(key);
29
+ if (rendered.pending) {
30
+ pending.add(key);
31
+ return PENDING_MARK;
32
+ }
33
+ return String(rendered.text ?? '').trim();
34
+ };
35
+ const lines = [];
36
+ lines.push('# 邀请函');
37
+ lines.push('');
38
+ lines.push(`**尊敬的 ${val('guest_org')}:**`);
39
+ lines.push('');
40
+ lines.push(`${val('caller')} 诚邀您参加「${val('mt_name')}」,现将会议安排通知如下:`);
41
+ lines.push('');
42
+ lines.push(`- **会议名称**:${val('mt_name')}`);
43
+ lines.push(`- **会议时间**:${val('begin_datetime')}~${val('end_datetime')}`);
44
+ lines.push(`- **会议地点**:${val('address')}`);
45
+ lines.push(`- **参会人员**:${val('hrm_members')}`);
46
+ lines.push(`- **会议内容**:${val('mt_desc')}`);
47
+ lines.push('');
48
+ lines.push(`**${val('begin_date')}会议议程安排:**`);
49
+ lines.push('');
50
+ lines.push('| 议程安排 | 议程内容 | 议程时间 |');
51
+ lines.push('| --- | --- | --- |');
52
+ if (agendaRows.length === 0) {
53
+ lines.push('| — | 暂无议程 | — |');
54
+ }
55
+ else {
56
+ for (const [name, content, timeRange] of agendaRows) {
57
+ lines.push(`| ${cell(name) || '—'} | ${cell(content) || '—'} | ${cell(timeRange) || '—'} |`);
58
+ }
59
+ }
60
+ lines.push('');
61
+ lines.push(val('company_name'));
62
+ lines.push('');
63
+ lines.push(val('issue_date'));
64
+ lines.push('');
65
+ lines.push('> 注:以上为拟定会议安排,具体会议时间与议程需根据实际情况确认。');
66
+ if (options.pendingHint !== false && pending.size > 0) {
67
+ lines.push('');
68
+ lines.push(`> ⚠️ 标 ${PENDING_MARK} 的字段会议里没有值,需要人工补;补完可用 mtId 重新生成。`);
69
+ }
70
+ return { markdown: lines.join('\n'), pendingFields: [...pending] };
71
+ }