weaver-work-cli 0.1.39 → 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 (197) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-ENZYBBME.js → archive-J4I6MCKF.js} +2 -2
  3. package/dist/chunks/{archive-oa-YWLEC5V4.js → archive-oa-BRZKKTO4.js} +2 -2
  4. package/dist/chunks/{asset-OOV6DWPA.js → asset-L7D4FAOZ.js} +2 -2
  5. package/dist/chunks/{asset-idle-RE632MY5.js → asset-idle-DADLLV75.js} +2 -2
  6. package/dist/chunks/{attachment-content-RWN2FCLL.js → attachment-content-K4C7VLWQ.js} +3 -3
  7. package/dist/chunks/{blog-PEPOJAL7.js → blog-BVGQ7NZL.js} +2 -2
  8. package/dist/chunks/{caizhilian-WYIQDVPJ.js → caizhilian-SAADNFD6.js} +2 -2
  9. package/dist/chunks/{calendar-CIARBAOX.js → calendar-Q7NQY5Z2.js} +61 -23
  10. package/dist/chunks/{chunk-F2EQXUEE.js → chunk-3KTFS4IA.js} +397 -159
  11. package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
  12. package/dist/chunks/{chunk-HJYVHXUQ.js → chunk-4RAE6JXP.js} +1 -1
  13. package/dist/chunks/{chunk-CQHGNCK2.js → chunk-72CKAUAG.js} +41 -8
  14. package/dist/chunks/{chunk-K3RUEEOL.js → chunk-APVFJUQH.js} +2 -2
  15. package/dist/chunks/{chunk-BJGXMFGD.js → chunk-BBKDXQEC.js} +2 -2
  16. package/dist/chunks/chunk-BIPYLLVS.js +501 -0
  17. package/dist/chunks/{chunk-GVPJZXJ2.js → chunk-EKWAQSLU.js} +92 -50
  18. package/dist/chunks/{chunk-XXJOSZTQ.js → chunk-FOAA27U5.js} +381 -57
  19. package/dist/chunks/{chunk-I2HB737U.js → chunk-G4UR6NXN.js} +2 -2
  20. package/dist/chunks/{chunk-YS5DPN4T.js → chunk-GEWYMQME.js} +1 -1
  21. package/dist/chunks/{chunk-P2DAISYW.js → chunk-GHXCDPTM.js} +1 -1
  22. package/dist/chunks/{chunk-L4N7B7ZD.js → chunk-IZ7S7QUS.js} +4 -4
  23. package/dist/chunks/{chunk-NLK2RZKG.js → chunk-JPRO6ER2.js} +122 -2214
  24. package/dist/chunks/{chunk-36DIFZAH.js → chunk-LADPYOWC.js} +2 -2
  25. package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
  26. package/dist/chunks/chunk-NKZRQXST.js +2546 -0
  27. package/dist/chunks/{chunk-EINRJHQ3.js → chunk-QKNKR4RH.js} +3 -3
  28. package/dist/chunks/{chunk-T7JDJJ7H.js → chunk-R4CHUSKC.js} +126 -19
  29. package/dist/chunks/{chunk-EZGFHJFW.js → chunk-SA6VBIHY.js} +88 -15
  30. package/dist/chunks/{chunk-ZXRNHZTE.js → chunk-TTPJJJC4.js} +2 -2
  31. package/dist/chunks/{chunk-KTCB5R5H.js → chunk-W7BEHOSR.js} +2 -2
  32. package/dist/chunks/{chunk-MUQUHKPG.js → chunk-YRSZQUD2.js} +2 -2
  33. package/dist/chunks/{conflict-DRMDOL56.js → conflict-F2OMZRTO.js} +2 -2
  34. package/dist/chunks/{contract-73MERZWN.js → contract-5BETHRSG.js} +2 -2
  35. package/dist/chunks/{cowork-HNGBPD7A.js → cowork-3MN6EQQO.js} +27 -10
  36. package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
  37. package/dist/chunks/{ebuilder-form-OY6DVNRK.js → ebuilder-form-SBHOYBUD.js} +3 -3
  38. package/dist/chunks/{ehr-RAQNEQO7.js → ehr-KMOJULKG.js} +432 -1234
  39. package/dist/chunks/env-LB7NBL2O.js +19 -0
  40. package/dist/chunks/{esb-6XV7C7YV.js → esb-W2PJCFCX.js} +4 -4
  41. package/dist/chunks/factory-6ZFQXQ46.js +30 -0
  42. package/dist/chunks/{fna-XTSRB7G7.js → fna-ZQPCUV4R.js} +2 -2
  43. package/dist/chunks/group-chat-2HHAJN46.js +20 -0
  44. package/dist/chunks/{host-YMBXWFWX.js → host-4CBHGOHT.js} +3 -3
  45. package/dist/chunks/{host-A7J6GDUP.js → host-ATKVNZAT.js} +3 -3
  46. package/dist/chunks/{host-3F5UM5GN.js → host-BQH22NTQ.js} +3 -3
  47. package/dist/chunks/{host-YAQAPYNP.js → host-X2CSLFM6.js} +3 -3
  48. package/dist/chunks/{host-VGPVP7YF.js → host-YTODKMSL.js} +3 -3
  49. package/dist/chunks/{hrm-TLHKC545.js → hrm-FFM4UJFS.js} +4 -4
  50. package/dist/chunks/{im-YOUCFMUK.js → im-VEHKDQPB.js} +8 -8
  51. package/dist/chunks/{invoice-WNBS2B4C.js → invoice-LVIKSIIF.js} +3 -3
  52. package/dist/chunks/{jiuchuanhui-XABWS5RN.js → jiuchuanhui-X6DINO5P.js} +2 -2
  53. package/dist/chunks/{lazy-dispatch-NLZ5UQQI.js → lazy-dispatch-KSWTN5L6.js} +4 -4
  54. package/dist/chunks/{mail-LRYMBFNL.js → mail-VDKRGUD3.js} +9 -4
  55. package/dist/chunks/{meeting-ESXL6MFI.js → meeting-CLC6HP3Z.js} +12 -10
  56. package/dist/chunks/{okr-EOV65SSF.js → okr-CHG4OO3U.js} +2 -2
  57. package/dist/chunks/{plan-YFPVKW6Y.js → plan-XWR75667.js} +2 -2
  58. package/dist/chunks/{project-MDVZUYQW.js → project-5BXOOKMG.js} +40 -23
  59. package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
  60. package/dist/chunks/{registry-VT5Y2HLA.js → registry-7FFOXIAW.js} +4 -4
  61. package/dist/chunks/{registry-QCLZEDAP.js → registry-BKBZPBAJ.js} +10 -8
  62. package/dist/chunks/{registry-YX4CSEMD.js → registry-RHMW6ANW.js} +2 -2
  63. package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
  64. package/dist/chunks/{room-recommend-L3MWH4JW.js → room-recommend-62H7ZQQ6.js} +4 -4
  65. package/dist/chunks/{schedule-UASAVDVD.js → schedule-FNXKH3DQ.js} +4 -4
  66. package/dist/chunks/{workflow-KDGIUCNW.js → workflow-ADHKCAJK.js} +4 -4
  67. package/dist/index.js +33 -28
  68. package/dist/internal/e10/auth/commands.js +110 -56
  69. package/dist/internal/e10/auth/messages.js +6 -0
  70. package/dist/internal/e10/auth/session.js +28 -1
  71. package/dist/internal/e10/auth/xiaoe.js +315 -48
  72. package/dist/internal/e10/auth-recovery.js +36 -5
  73. package/dist/internal/e10/client.js +13 -0
  74. package/dist/internal/e10/context.js +61 -9
  75. package/dist/internal/e10/index.js +1 -1
  76. package/dist/internal/e10/network-error.js +93 -0
  77. package/dist/internal/e10/request-runtime.js +15 -0
  78. package/dist/shortcuts/calendar/operations/read.js +31 -22
  79. package/dist/shortcuts/calendar/operations/write.js +45 -1
  80. package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
  81. package/dist/shortcuts/cowork/operations/item-write.js +5 -3
  82. package/dist/shortcuts/cowork/operations/member-write.js +3 -1
  83. package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
  84. package/dist/shortcuts/cowork/operations/shared.js +7 -0
  85. package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
  86. package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
  87. package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
  88. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
  89. package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
  90. package/dist/shortcuts/ehr/operations/recruit.js +10 -4
  91. package/dist/shortcuts/ehr/operations/shared.js +47 -0
  92. package/dist/shortcuts/hrm/operations/org.js +118 -0
  93. package/dist/shortcuts/index.js +5 -0
  94. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  95. package/dist/shortcuts/meeting/operations/eb/catalog.js +9 -2
  96. package/dist/shortcuts/meeting/operations/eb/create-flow.js +461 -127
  97. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  98. package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
  99. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  100. package/dist/shortcuts/meeting/operations/eb/group-chat.js +605 -0
  101. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  102. package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
  103. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +44 -6
  104. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  105. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  106. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  107. package/dist/shortcuts/project/operations/read.js +6 -25
  108. package/dist/shortcuts/project/operations/shared.js +22 -0
  109. package/dist/shortcuts/project/operations/write.js +28 -2
  110. package/dist/shortcuts/recruit-stage/context.js +119 -0
  111. package/dist/shortcuts/recruit-stage/continuation.js +73 -0
  112. package/dist/shortcuts/recruit-stage/errors.js +65 -0
  113. package/dist/shortcuts/recruit-stage/host.js +109 -0
  114. package/dist/shortcuts/recruit-stage/index.js +115 -0
  115. package/dist/shortcuts/recruit-stage/manifest.js +29 -0
  116. package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
  117. package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
  118. package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
  119. package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
  120. package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
  121. package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
  122. package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
  123. package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
  124. package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
  125. package/dist/shortcuts/recruit-stage/shared.js +608 -0
  126. package/dist/shortcuts/workflow/operations/form.js +21 -2
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +52 -1
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +19 -11
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +78 -10
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +3 -1
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +16 -40
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +52 -15
  138. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
  139. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
  140. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
  141. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +53 -16
  142. package/docs/e10-auth.md +22 -1
  143. package/docs/operation-manual.md +8 -0
  144. package/package.json +1 -1
  145. package/scripts/install-skill-to-xiaoe.mjs +105 -0
  146. package/scripts/mcp-meeting.mjs +234 -0
  147. package/scripts/package-skill.mjs +4 -2
  148. package/skill-template/business-info.json +24 -0
  149. package/skill-template/domains/recruit-stage.md +29 -0
  150. package/skills/weaver-e10-calendar/SKILL.md +2 -1
  151. package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
  152. package/skills/weaver-e10-cowork/SKILL.md +1 -1
  153. package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
  154. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  155. package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
  156. package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
  157. package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
  158. package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
  159. package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
  160. package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
  161. package/skills/weaver-e10-mail/SKILL.md +1 -1
  162. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  163. package/skills/weaver-e10-meeting/SKILL.md +119 -26
  164. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  165. package/skills/weaver-e10-meeting.zip +0 -0
  166. package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
  167. package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
  168. package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
  169. package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
  170. package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
  171. package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
  172. package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
  173. package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
  174. package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
  175. package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
  176. package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
  177. package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
  178. package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
  179. package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
  180. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
  181. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
  182. package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
  183. package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
  184. package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
  185. package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
  186. package/skills/weaver-e10-shared/SKILL.md +2 -2
  187. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
  188. package/skills/weaver-e10-shijingran/SKILL.md +3 -2
  189. package/skills/weaver-e10-shijingran/references/operations.md +3 -1
  190. package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
  191. package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
  192. package/skills/weaver-e10-workflow/SKILL.md +7 -1
  193. package/skills/weaver-e10-workflow/aigroup/default.json +23 -9
  194. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +37 -21
  195. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +135 -43
  196. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
  197. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
@@ -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,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,25 +1,77 @@
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
+ 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), {
47
+ type: 'authentication',
48
+ subtype: networkUnavailable ? 'xiaoe_network_unavailable' : 'xiaoe_credential_unavailable',
49
+ // 网络错误是可重试的;凭证缺失/被拒重试也一样失败,不能让调用方空转。
50
+ retryable: networkUnavailable,
51
+ stage: lastFailure?.stage || 'xiaoe_unified_login',
52
+ ...(details ? { details } : {}),
53
+ });
54
+ }
6
55
  export async function getE10Session(ctx, load = {}) {
7
56
  const options = ctx.getGlobalOptions();
8
57
  return E10AuthSession.fromOptions(options, ctx.cwd, load);
9
58
  }
10
59
  export async function requireE10Session(ctx, load = {}) {
11
60
  let session = await getE10Session(ctx, load);
61
+ if (session?.isExpired() && canAutomaticallyRecoverXiaoE(ctx, session)) {
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
+ }
67
+ }
12
68
  if (!session && canAutomaticallyRecoverXiaoE(ctx)) {
13
69
  // Read-only probes use getE10Session; only a command requiring a session
14
70
  // may establish one from the current XiaoE host's credentials.
15
- session = await recoverXiaoEAuthSession(ctx).catch(() => null);
71
+ const outcome = await recoverXiaoEAuthSessionWithDiagnostics(ctx).catch(() => null);
72
+ session = outcome?.session || null;
16
73
  if (!session) {
17
- throw Object.assign(new CliError(E10_XIAOE_RECOVERY_FAILED_MESSAGE, ExitCode.AuthError), {
18
- type: 'authentication',
19
- subtype: 'xiaoe_credential_unavailable',
20
- retryable: false,
21
- stage: 'xiaoe_unified_login',
22
- });
74
+ throw xiaoERecoveryFailedError(outcome?.diagnostics);
23
75
  }
24
76
  }
25
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';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 网络错误的统一识别。
3
+ *
4
+ * Node/undici 的 `fetch` 只抛 `TypeError: fetch failed`,真实原因(拒绝连接、
5
+ * DNS 失败、TLS 失败、连接/响应超时)放在 `error.cause.code` 上。丢掉这条 code,
6
+ * 「连不上 E10」就会被误报成「登录态不可用」,把用户引导去重新登录。
7
+ */
8
+ /** 明确属于「网络层没打通」的系统错误码(Node 常见 errno + undici 内部码)。 */
9
+ const NETWORK_ERROR_CODES = new Set([
10
+ 'ECONNREFUSED',
11
+ 'ECONNRESET',
12
+ 'ECONNABORTED',
13
+ 'EHOSTUNREACH',
14
+ 'EHOSTDOWN',
15
+ 'ENETUNREACH',
16
+ 'ENETDOWN',
17
+ 'ENETRESET',
18
+ 'ENOTFOUND',
19
+ 'EAI_AGAIN',
20
+ 'EPIPE',
21
+ 'ETIMEDOUT',
22
+ 'ESOCKETTIMEDOUT',
23
+ 'ERR_SOCKET_CONNECTION_TIMEOUT',
24
+ 'UND_ERR_CONNECT_TIMEOUT',
25
+ 'UND_ERR_HEADERS_TIMEOUT',
26
+ 'UND_ERR_BODY_TIMEOUT',
27
+ 'UND_ERR_SOCKET',
28
+ 'UND_ERR_CLOSED',
29
+ 'UND_ERR_ABORTED',
30
+ 'CERT_HAS_EXPIRED',
31
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
32
+ 'SELF_SIGNED_CERT_IN_CHAIN',
33
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
34
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
35
+ 'ERR_SSL_WRONG_VERSION_NUMBER',
36
+ ]);
37
+ /** `AbortController.abort()` 抛出的 DOMException 没有 errno,统一用这个码表示「超时」。 */
38
+ export const ABORT_ERROR_CODE = 'ABORT_ERR';
39
+ function asRecord(value) {
40
+ return value && typeof value === 'object' ? value : null;
41
+ }
42
+ /**
43
+ * 取错误链上最内层的系统错误码(`error.cause.code`);`AbortError` 归一化成
44
+ * `ABORT_ERR`。用 `seen` 防环:Node 的 cause 链偶尔会指回上层。
45
+ */
46
+ export function readErrorCauseCode(error, maxDepth = 5) {
47
+ let current = error;
48
+ const seen = new Set();
49
+ for (let depth = 0; depth < maxDepth; depth += 1) {
50
+ if (current === undefined || current === null || seen.has(current))
51
+ return undefined;
52
+ seen.add(current);
53
+ if (current instanceof Error && current.name === 'AbortError')
54
+ return ABORT_ERROR_CODE;
55
+ const record = asRecord(current);
56
+ if (!record)
57
+ return undefined;
58
+ if (record.name === 'AbortError')
59
+ return ABORT_ERROR_CODE;
60
+ const code = record.code;
61
+ if (typeof code === 'string' && code)
62
+ return code;
63
+ const errno = record.errno;
64
+ if (typeof errno === 'string' && errno)
65
+ return errno;
66
+ if (typeof errno === 'number')
67
+ return String(errno);
68
+ current = record.cause;
69
+ }
70
+ return undefined;
71
+ }
72
+ export function isAbortError(error) {
73
+ if (error instanceof Error && error.name === 'AbortError')
74
+ return true;
75
+ return asRecord(error)?.name === 'AbortError';
76
+ }
77
+ export function isNetworkErrorCode(code) {
78
+ return typeof code === 'string' && NETWORK_ERROR_CODES.has(code.toUpperCase());
79
+ }
80
+ /** undici 把底层原因藏起来时只剩这句话,但语义已经很明确了。 */
81
+ const TRANSPORT_MESSAGE_PATTERN = /fetch failed|socket hang up|other side closed|network|connection (?:refused|reset|closed)|timed? ?out|timeout|certificate|getaddrinfo|dns lookup/i;
82
+ export function looksLikeTransportMessage(message) {
83
+ return TRANSPORT_MESSAGE_PATTERN.test(message);
84
+ }
85
+ /**
86
+ * 给一次 fetch(含读响应体)套上超时。调用方必须在 `finally` 里 `clear()`,
87
+ * 否则进程会被这一个定时器拖住。
88
+ */
89
+ export function createTimeoutSignal(timeoutMs) {
90
+ const controller = new AbortController();
91
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
92
+ return { signal: controller.signal, clear: () => clearTimeout(timer) };
93
+ }
@@ -1,6 +1,8 @@
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 { readErrorCauseCode } from './network-error.js';
5
+ import { E10_XIAOE_RECOVERY_FAILED_MESSAGE } from './auth/messages.js';
4
6
  const DEFAULT_TIMEOUT_MS = 30_000;
5
7
  const DEFAULT_BIG_INT_STRINGS = true;
6
8
  /** 下载直传最多跟几跳:E10 -> 对象存储通常 1 跳,留一点余量但不允许成环。 */
@@ -53,6 +55,12 @@ export class E10RequestRuntime {
53
55
  return this.request({ ...request, responseType: 'binary' });
54
56
  }
55
57
  async request(request) {
58
+ if (this.session.isExpired?.()) {
59
+ const recovered = await this.recoverExpiredSession(this.session.eteamsId);
60
+ if (!recovered || this.session.isExpired?.()) {
61
+ throw new E10ApiError('authentication', 'session_expired', E10_XIAOE_RECOVERY_FAILED_MESSAGE, { exitCode: ExitCode.AuthError, retryable: false });
62
+ }
63
+ }
56
64
  const mutable = this.prepareRequest(request);
57
65
  try {
58
66
  await this.runBeforeRequest(mutable, this.sessionContext());
@@ -171,6 +179,8 @@ export class E10RequestRuntime {
171
179
  }
172
180
  if (!recovered)
173
181
  return false;
182
+ if (recovered.isExpired?.())
183
+ return false;
174
184
  if (!sameE10SessionIdentity(this.session, recovered)) {
175
185
  throw new E10ApiError('authentication', 'session_context_changed', 'XiaoE 自动恢复后的登录身份或 E10 环境发生变化,本次请求未重试', { exitCode: ExitCode.AuthError, retryable: false });
176
186
  }
@@ -305,12 +315,17 @@ export class E10RequestRuntime {
305
315
  throw new E10ApiError('network', 'timeout', `请求超时(${timeoutMs}ms)`, {
306
316
  retryable: false,
307
317
  exitCode: ExitCode.NetworkError,
318
+ details: { timeoutMs },
308
319
  });
309
320
  }
310
321
  const message = error instanceof Error ? error.message : String(error);
322
+ // Node 的 `fetch failed` 不带原因,真正的 errno(ECONNREFUSED/ENOTFOUND…)
323
+ // 在 `error.cause.code` 上:保留下来才能区分「断网」与「服务端报错」。
324
+ const causeCode = readErrorCauseCode(error);
311
325
  throw new E10ApiError('network', 'transport', `请求失败:${message}`, {
312
326
  retryable: true,
313
327
  exitCode: ExitCode.NetworkError,
328
+ ...(causeCode ? { details: { causeCode } } : {}),
314
329
  });
315
330
  }
316
331
  finally {
@@ -184,6 +184,36 @@ function firstId(value) {
184
184
  return String(value);
185
185
  return undefined;
186
186
  }
187
+ /**
188
+ * 取「日程」应用 appId 与日程表单 formId(用于详情卡片页链接)。
189
+ * 偶发空返回时 getObjsByTag 原样重试最多 3 次。任一获取失败返回 null。
190
+ * 创建日程后需用 formId 拼卡片页链接,故抽为可复用函数。
191
+ */
192
+ export async function fetchAgendaAppAndForm(host) {
193
+ const appIdsResponse = await host.request({
194
+ method: 'GET',
195
+ path: '/api/bs/ebuilder/app/tags/getAppIds',
196
+ operation: 'calendar.app.check',
197
+ query: { tag: AGENDA_APP_TAG },
198
+ }).catch(() => undefined);
199
+ const appId = firstId(appIdsResponse?.data);
200
+ let formId;
201
+ if (appId) {
202
+ for (let attempt = 0; attempt < 3 && !formId; attempt++) {
203
+ const formResponse = await host.request({
204
+ method: 'GET',
205
+ path: '/api/ebuilder/form/obj/getObjsByTag',
206
+ operation: 'calendar.app.check',
207
+ query: { tag: AGENDA_FORM_TAG, appId },
208
+ }).catch(() => undefined);
209
+ formId = firstId(formResponse?.data);
210
+ if (!formId && attempt < 2) {
211
+ await new Promise((resolve) => setTimeout(resolve, 800));
212
+ }
213
+ }
214
+ }
215
+ return { appId: appId ?? null, formId: formId ?? null };
216
+ }
187
217
  const calendarAppCheck = {
188
218
  name: 'calendar.app.check',
189
219
  resource: 'calendar',
@@ -194,14 +224,7 @@ const calendarAppCheck = {
194
224
  risk: 'read',
195
225
  inputSchema: schemaOf({}),
196
226
  handler: async (_args, { host }) => {
197
- // 1. getAppIds 取当前环境 appId
198
- const appIdsResponse = await host.request({
199
- method: 'GET',
200
- path: '/api/bs/ebuilder/app/tags/getAppIds',
201
- operation: 'calendar.app.check',
202
- query: { tag: AGENDA_APP_TAG },
203
- }).catch(() => undefined);
204
- const appId = firstId(appIdsResponse?.data);
227
+ const { appId, formId } = await fetchAgendaAppAndForm(host);
205
228
  if (!appId) {
206
229
  return calendarSuccess('calendar.app.check', {
207
230
  installed: false,
@@ -217,20 +240,6 @@ const calendarAppCheck = {
217
240
  query: { appId },
218
241
  }).catch(() => undefined);
219
242
  const commitVersion = versionResponse?.data?.commitVersion ?? versionResponse?.commitVersion;
220
- // 3. 查询日程表单 ID(偶发空返回,参数原样重试最多 3 次)
221
- let formId;
222
- for (let attempt = 0; attempt < 3 && !formId; attempt++) {
223
- const formResponse = await host.request({
224
- method: 'GET',
225
- path: '/api/ebuilder/form/obj/getObjsByTag',
226
- operation: 'calendar.app.check',
227
- query: { tag: AGENDA_FORM_TAG, appId },
228
- }).catch(() => undefined);
229
- formId = firstId(formResponse?.data);
230
- if (!formId && attempt < 2) {
231
- await new Promise((resolve) => setTimeout(resolve, 800));
232
- }
233
- }
234
243
  const hasVersion = typeof commitVersion === 'string' && commitVersion.trim() !== '';
235
244
  const installed = hasVersion && commitVersion !== null && commitVersion !== undefined;
236
245
  const versionOk = installed && compareVersion(commitVersion, MIN_APP_VERSION) >= 0;
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { calendarConfirmationError, calendarPartialError, calendarValidationError, CalendarCliError } from '../errors.js';
3
3
  import { calendarSuccess, schemaOf, SCHEMA_ID, SCHEMA_DATETIME_MINUTE, SCHEMA_SWITCH, SCHEMA_DATE } from './types.js';
4
4
  import { assertDateTimeMinute, defaultEndTime, optionId, participantIds, pickDefined, } from './shared.js';
5
+ import { fetchAgendaAppAndForm } from './read.js';
5
6
  const CONTINUATION_TTL_SECONDS = 600;
6
7
  // 日程 mainTable 可选业务字段(创建/更新共用)
7
8
  const OPTIONAL_FIELDS = [
@@ -81,6 +82,37 @@ async function verifyLease(args, action, env) {
81
82
  function isNetworkError(error) {
82
83
  return error instanceof CalendarCliError && error.type === 'network';
83
84
  }
85
+ /**
86
+ * 从 saveFormData/createAgenda 返回体提取新建日程 ID。
87
+ * 兼容两种后端形态:
88
+ * - 顶层 `datajson.dataIds`(字符串数组)
89
+ * - `datajson.datas[].mainTable.dataIds` / `datajson.datas[].mainTable.id`
90
+ */
91
+ function extractCreatedIds(response) {
92
+ const datajson = response?.datajson;
93
+ if (!datajson || typeof datajson !== 'object')
94
+ return [];
95
+ if (Array.isArray(datajson.dataIds)) {
96
+ return datajson.dataIds.map((v) => String(v)).filter(Boolean);
97
+ }
98
+ const datas = Array.isArray(datajson.datas) ? datajson.datas : [];
99
+ const ids = [];
100
+ for (const d of datas) {
101
+ const mt = d?.mainTable;
102
+ if (!mt)
103
+ continue;
104
+ if (Array.isArray(mt.dataIds))
105
+ ids.push(...mt.dataIds.map((v) => String(v)));
106
+ else if (mt.id !== undefined && mt.id !== null && mt.id !== '')
107
+ ids.push(String(mt.id));
108
+ }
109
+ return ids;
110
+ }
111
+ /** 拼日程详情卡片页链接(baseUrl 可能带协议头,统一归一成 https://host/...)。 */
112
+ function buildCardUrl(baseUrl, formId, id) {
113
+ const hostPart = String(baseUrl || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
114
+ return `https://${hostPart}/sp/ebdfpage/card/0/${formId}/${id}`;
115
+ }
84
116
  /** 构造写入接口 datajson 请求体(datas[].mainTable 结构)。 */
85
117
  async function buildWriteDatajson(host, mainTableList) {
86
118
  const session = await host.getSession();
@@ -195,9 +227,21 @@ const calendarCreateApply = {
195
227
  }
196
228
  throw error;
197
229
  }
230
+ const createdIds = extractCreatedIds(response);
231
+ const createdId = createdIds[0];
232
+ const session = await env.host.getSession();
233
+ let accessUrl = null;
234
+ if (createdId) {
235
+ const { formId } = await fetchAgendaAppAndForm(env.host);
236
+ if (formId) {
237
+ accessUrl = buildCardUrl(session.baseUrl, formId, createdId);
238
+ }
239
+ }
198
240
  return calendarSuccess('calendar.create.apply', {
199
241
  status: 'COMPLETE',
200
- result: response,
242
+ createdId: createdId ?? null,
243
+ createdIds,
244
+ accessUrl,
201
245
  });
202
246
  },
203
247
  };
@@ -1,6 +1,6 @@
1
1
  import { CoworkCliError, coworkNotFoundError, coworkPartialError, coworkValidationError } from '../errors.js';
2
2
  import { READ_BEFORE_WRITE, WRITE, SCHEMA_ID, coworkSuccess, schemaOf, } from './types.js';
3
- import { assertEnum, assertString, compact, normalizeList, requireConfirm, toBase64Html, toId, toPositiveInt, unwrapData, verifyLease, } from './shared.js';
3
+ import { assertEnum, assertString, compact, coworkViewUrl, normalizeList, requireConfirm, toBase64Html, toId, toPositiveInt, unwrapData, verifyLease, } from './shared.js';
4
4
  function isNetworkError(error) {
5
5
  return error instanceof CoworkCliError && error.type === 'network';
6
6
  }
@@ -132,6 +132,8 @@ const discussPublishApply = {
132
132
  coworkId,
133
133
  sectionId,
134
134
  status: data.status ?? root?.status ?? null,
135
+ viewUrl: coworkViewUrl(lease.baseUrl, coworkId),
136
+ mobileViewUrl: coworkViewUrl(lease.baseUrl, coworkId, true),
135
137
  });
136
138
  },
137
139
  };
@@ -266,6 +268,8 @@ const discussActionApply = {
266
268
  coworkId,
267
269
  discussId,
268
270
  result: unwrapData(response) ?? null,
271
+ viewUrl: coworkViewUrl(lease.baseUrl, coworkId),
272
+ mobileViewUrl: coworkViewUrl(lease.baseUrl, coworkId, true),
269
273
  });
270
274
  },
271
275
  };
@@ -1,6 +1,6 @@
1
1
  import { CoworkCliError, coworkNotFoundError, coworkPartialError, coworkValidationError } from '../errors.js';
2
2
  import { READ_BEFORE_WRITE, WRITE, SCHEMA_ID, SCHEMA_DATETIME, coworkSuccess, schemaOf, } from './types.js';
3
- import { assertEnum, assertString, requireConfirm, toBase64Html, toId, unwrapData, verifyLease, } from './shared.js';
3
+ import { assertEnum, assertString, coworkViewUrl, requireConfirm, toBase64Html, toId, unwrapData, verifyLease, } from './shared.js';
4
4
  function isNetworkError(error) {
5
5
  return error instanceof CoworkCliError && error.type === 'network';
6
6
  }
@@ -203,6 +203,8 @@ const itemCreateApply = {
203
203
  sectionId,
204
204
  principal,
205
205
  endTime,
206
+ viewUrl: coworkViewUrl(lease.baseUrl, data.id),
207
+ mobileViewUrl: coworkViewUrl(lease.baseUrl, data.id, true),
206
208
  });
207
209
  },
208
210
  };
@@ -295,7 +297,7 @@ const itemActionApply = {
295
297
  }
296
298
  throw error;
297
299
  }
298
- return coworkSuccess('cowork.item.action.apply', { action, id, result: unwrapData(response) ?? null });
300
+ return coworkSuccess('cowork.item.action.apply', { action, id, result: unwrapData(response) ?? null, viewUrl: coworkViewUrl(lease.baseUrl, id) });
299
301
  },
300
302
  };
301
303
  const ITEM_FLAGS = ['hide', 'vote'];
@@ -367,7 +369,7 @@ const itemFlagApply = {
367
369
  form,
368
370
  operation: 'cowork.item.flag.apply',
369
371
  });
370
- return coworkSuccess('cowork.item.flag.apply', { action, id, result: unwrapData(response) ?? null });
372
+ return coworkSuccess('cowork.item.flag.apply', { action, id, result: unwrapData(response) ?? null, viewUrl: coworkViewUrl(lease.baseUrl, id) });
371
373
  },
372
374
  };
373
375
  export const coworkItemWriteOperations = [
@@ -1,6 +1,6 @@
1
1
  import { CoworkCliError, coworkNotFoundError, coworkPartialError, coworkValidationError } from '../errors.js';
2
2
  import { READ_BEFORE_WRITE, WRITE, SCHEMA_ID, coworkSuccess, schemaOf, } from './types.js';
3
- import { assertEnum, compact, normalizeList, requireConfirm, toId, toIdList, toPositiveInt, unwrapData, verifyLease, } from './shared.js';
3
+ import { assertEnum, compact, coworkViewUrl, normalizeList, requireConfirm, toId, toIdList, toPositiveInt, unwrapData, verifyLease, } from './shared.js';
4
4
  function isNetworkError(error) {
5
5
  return error instanceof CoworkCliError && error.type === 'network';
6
6
  }
@@ -153,6 +153,8 @@ const memberActionApply = {
153
153
  coworkId,
154
154
  userIds: action === 'join' ? undefined : String(form.ids).split(','),
155
155
  result: unwrapData(response) ?? null,
156
+ viewUrl: coworkViewUrl(lease.baseUrl, coworkId),
157
+ mobileViewUrl: coworkViewUrl(lease.baseUrl, coworkId, true),
156
158
  });
157
159
  },
158
160
  };
@@ -1,6 +1,6 @@
1
1
  import { CoworkCliError, coworkNotFoundError, coworkPartialError, coworkValidationError } from '../errors.js';
2
2
  import { READ_BEFORE_WRITE, WRITE, SCHEMA_ID, coworkSuccess, schemaOf, } from './types.js';
3
- import { normalizeList, requireConfirm, toId, unwrapData, verifyLease, } from './shared.js';
3
+ import { coworkViewUrl, normalizeList, requireConfirm, toId, unwrapData, verifyLease, } from './shared.js';
4
4
  function isNetworkError(error) {
5
5
  return error instanceof CoworkCliError && error.type === 'network';
6
6
  }
@@ -163,6 +163,8 @@ const permAddApply = {
163
163
  userId,
164
164
  userName,
165
165
  result: unwrapData(response) ?? null,
166
+ viewUrl: coworkViewUrl(lease.baseUrl, coworkId),
167
+ mobileViewUrl: coworkViewUrl(lease.baseUrl, coworkId, true),
166
168
  });
167
169
  },
168
170
  };
@@ -275,6 +277,8 @@ const permRemoveApply = {
275
277
  removed: stillThere === false ? true : undefined,
276
278
  verified: stillThere !== null,
277
279
  result: unwrapData(response) ?? null,
280
+ viewUrl: coworkViewUrl(lease.baseUrl, coworkId),
281
+ mobileViewUrl: coworkViewUrl(lease.baseUrl, coworkId, true),
278
282
  });
279
283
  },
280
284
  };
@@ -88,6 +88,13 @@ export function unwrapData(response) {
88
88
  return response;
89
89
  return response.data;
90
90
  }
91
+ /** 协作查看页地址:PC `/sp/cowork/view/{id}`、移动端 `/mobile/cowork/view/{id}`(真实环境取证形态,禁止改拼)。 */
92
+ export function coworkViewUrl(baseUrl, id, mobile = false) {
93
+ if (!id)
94
+ return null;
95
+ const base = String(baseUrl || '').replace(/\/+$/, '');
96
+ return `${base}/${mobile ? 'mobile' : 'sp'}/cowork/view/${id}`;
97
+ }
91
98
  /** 协作列表的 data 形如 { pageSize, total, current, pageUid, columns[], data[] }。 */
92
99
  export function normalizePaged(response) {
93
100
  const data = unwrapData(response);
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { ehrConfirmationError, ehrPartialError, ehrValidationError, EhrCliError } from '../errors.js';
3
3
  import { READ, SCHEMA_DATE, SCHEMA_ID, ehrSuccess, schemaOf, WRITE, } from './types.js';
4
- import { assertEnum, assertEnumArray, compact, pickDefined, toIdList } from './shared.js';
4
+ import { assertEnum, assertEnumArray, compact, pickDefined, toIdList, EHR_PAGES, ehrPageUrl, addDays } from './shared.js';
5
5
  const CONTINUATION_TTL_SECONDS = 600;
6
6
  const CARD_TYPES = ['CHECKIN', 'CHECKOUT', 'LEAVE', 'RETURN', 'CHECKIN_AND_RETURN', 'CHECKOUT_AND_LEAVE'];
7
7
  const APPEAL_TYPES = [
@@ -316,9 +316,51 @@ const ehrAttendAppealSaveApply = {
316
316
  }
317
317
  throw error;
318
318
  }
319
+ // 回执(按 references/attend-appeal-save.md 契约,2026-11-04):
320
+ // ① 回查申诉列表定位本次新建记录(日期前后各放宽 1 天,防跨天/时区边界漏查);
321
+ // ② 返回 appealListUrl(考勤异常/申诉页面,已取证路由)。
322
+ // 回查失败不阻塞提交结果(提交已成功),以 locateWarning 如实提示。
323
+ const session = await env.host.getSession();
324
+ const appealListUrl = ehrPageUrl(session.baseUrl, EHR_PAGES.attendAbnormal);
325
+ let appeal = null;
326
+ let locateWarning;
327
+ try {
328
+ const listResp = await env.host.request({
329
+ method: 'POST',
330
+ path: '/api/attend/web/appeal/getAppealList',
331
+ body: {
332
+ beginDate: addDays(String(args.attendDay), -1),
333
+ endDate: addDays(String(args.attendDay), 1),
334
+ current: 1,
335
+ pageSize: 50,
336
+ },
337
+ });
338
+ const payload = listResp?.data;
339
+ const rows = Array.isArray(payload?.data) ? payload.data
340
+ : Array.isArray(payload?.rows) ? payload.rows
341
+ : Array.isArray(payload) ? payload
342
+ : [];
343
+ const day = String(args.attendDay);
344
+ const desc = String(args.appealDesc ?? '');
345
+ const pick = rows.find((r) => String(r?.attendDay ?? r?.attend_date ?? r?.attendDate ?? '').startsWith(day))
346
+ || (desc ? rows.find((r) => String(r?.appealDesc ?? r?.appeal_desc ?? '').includes(desc)) : null)
347
+ || null;
348
+ if (pick)
349
+ appeal = pick;
350
+ else
351
+ locateWarning = '未在申诉列表中定位到本次记录(可能刚提交尚在同步),请稍后用 ehr.attend.appeal.list 复核';
352
+ }
353
+ catch (e) {
354
+ locateWarning = '申诉列表回查失败:' + (e?.message || '未知原因') + ';请稍后用 ehr.attend.appeal.list 复核';
355
+ }
319
356
  return ehrSuccess('ehr.attend.appeal.save.apply', {
320
357
  status: 'COMPLETE',
321
358
  result: response?.data,
359
+ /** 定位到的新建申诉记录(含申诉 ID 等,处理申诉时用其 id) */
360
+ appeal,
361
+ /** 考勤异常/申诉页面地址(已取证路由,禁止改拼) */
362
+ appealListUrl,
363
+ ...(locateWarning ? { locateWarning } : {}),
322
364
  verifyHint: '可通过 ehr.attend.appeal.list 回查申诉是否已生成',
323
365
  });
324
366
  },