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
@@ -66,11 +66,15 @@ function normalizeRoom(row) {
66
66
  place: toText(main.place).trim(),
67
67
  equipments: toText(main.equipments).trim(),
68
68
  mrType: toText(main.mr_type).trim(),
69
+ // ⛔ 2026-11-04 真机发现:以前没映射 room_desc,`roomDesc` 永远是 undefined,
70
+ // 于是「带客户优先接待类」里**按描述文案判定**那一半从来没生效过(只有名称含「接待」的能命中)。
71
+ // 例:广州会议室四 的描述「用来接待重要来宾…」明明写着,却被当普通房间。
72
+ roomDesc: toText(main.room_desc).trim(),
69
73
  };
70
74
  }
71
75
  /** 全量会议室:优先缓存(24h),`refresh:true` 强制重拉。 */
72
76
  export async function loadRooms(env, options = {}) {
73
- const key = `rooms-v2-${await cacheScopeSuffix(env)}`;
77
+ const key = `rooms-v3-${await cacheScopeSuffix(env)}`;
74
78
  const warnings = [];
75
79
  if (!options.refresh) {
76
80
  const cached = await readCache(env, key, { ttlMs: ROOMS_TTL_MS });
@@ -124,10 +128,19 @@ export async function loadFrequentRooms(env, options = {}) {
124
128
  try {
125
129
  const { getMeetingOperation } = await import('../registry.js');
126
130
  const listOp = getMeetingOperation('meeting.eb.meeting.list');
127
- for (let pageNo = 1; pageNo <= 5; pageNo += 1) {
128
- const page = await listOp.handler({ creator: userId, begindatetime: sinceText, pageNo, pageSize: PAGE_SIZE }, env);
131
+ // ⛔ 2026-11-04 实测:会议列表接口**每页固定 10 条**(pageSize=200 也无效),
132
+ // 以前只扫 5 页 = 最近 50 场,而用户 180 天有 130+ 场 —— 统计被截断,
133
+ // 结果全被最近的测试会议带偏(「测试会议室1」排第一就是这么来的)。
134
+ // 现在按返回的 total 翻到底(封顶 40 页 / 400 场,避免异常数据打爆)。
135
+ let seen = 0;
136
+ let total = 0;
137
+ for (let pageNo = 1; pageNo <= 40; pageNo += 1) {
138
+ const page = await listOp.handler({ creator: userId, begindatetime: sinceText, pageNo, pageSize: 200 }, env);
129
139
  requests += 1;
130
140
  const rows = (page?.data?.rows ?? []);
141
+ if (rows.length === 0)
142
+ break;
143
+ total = Number(page?.data?.total ?? total) || total;
131
144
  for (const row of rows) {
132
145
  const main = row?.mainTable ?? row ?? {};
133
146
  const name = toText(main.address).trim();
@@ -135,7 +148,8 @@ export async function loadFrequentRooms(env, options = {}) {
135
148
  continue;
136
149
  counts.set(name, (counts.get(name) ?? 0) + 1);
137
150
  }
138
- if (rows.length < PAGE_SIZE)
151
+ seen += rows.length;
152
+ if (total > 0 && seen >= total)
139
153
  break;
140
154
  }
141
155
  }
@@ -200,6 +214,10 @@ export async function recommendRooms(env, input) {
200
214
  });
201
215
  const memberCount = input.memberCount && input.memberCount > 0 ? input.memberCount : null;
202
216
  const requirement = input.requirement ?? {};
217
+ // 兼容顶层 hasCustomer(建会链路以前就是传顶层,打分却只读 requirement.hasCustomer,
218
+ // 导致「带客户优先接待类」从来没生效过 —— 2026-11-04 用户实测 3 个候选全是普通会议室)。
219
+ if (input.hasCustomer === true)
220
+ requirement.hasCustomer = true;
203
221
  const usable = roomData.rooms.filter((room) => room.status === '0');
204
222
  let busyCount = 0;
205
223
  const candidates = [];
@@ -258,14 +276,16 @@ export async function recommendRooms(env, input) {
258
276
  reasons.push(`设备含「${requirement.equipmentsKeyword}」`);
259
277
  score += 10;
260
278
  }
261
- // 带客户时优先接待类:会议室描述(room_desc)或名称里带「接待/招待/客户/商务/贵宾」的加分。
262
- // 描述原文会一起返回,用户能自己判断为什么推这间。
279
+ // 带客户时优先接待类:会议室描述(room_desc)或名称里带「接待/招待/贵宾/商务」的加分。
280
+ // ⛔ 不要把「客户」单独当关键词:`e-message(建议客户培训)` 这种培训室会被误判成接待类,
281
+ // 直接加 35 分排到第一(2026-11-04 真机实测)。真正的接待房名称/描述里一定带「接待」。
263
282
  if (requirement.hasCustomer === true) {
264
- const hostText = `${room.roomDesc ?? ''} ${room.name}`;
265
- const hostHit = hostText.match(/(接待|招待|客户|商务|贵宾|vip|VIP)/u);
283
+ const desc = String(room.roomDesc ?? '').trim();
284
+ const nameHit = room.name.match(/(接待|招待|贵宾|商务|vip)/iu);
285
+ const descHit = desc.match(/(接待|招待|贵宾|商务|vip)/iu);
286
+ const hostHit = nameHit ?? descHit;
266
287
  if (hostHit) {
267
- const desc = String(room.roomDesc ?? '').trim();
268
- reasons.push(`带客户优先:${desc !== '' ? `描述含「${desc.slice(0, 30)}」` : `名称含「${hostHit[1]}」`}`);
288
+ reasons.push(`带客户优先:${descHit ? `描述含「${desc.slice(0, 30)}」` : `名称含「${hostHit[1]}」`}`);
269
289
  score += 35;
270
290
  }
271
291
  }
@@ -390,6 +410,8 @@ export const ebRoomRecommendOperation = {
390
410
  ...(args.mrType ? { mrType: String(args.mrType) } : {}),
391
411
  ...(args.roomAttr ? { roomAttr: String(args.roomAttr) } : {}),
392
412
  ...(args.equipmentsKeyword ? { equipmentsKeyword: String(args.equipmentsKeyword) } : {}),
413
+ // 2026-11-04 修:以前漏了它 —— 用户传了 hasCustomer,打分却读 requirement.hasCustomer,永远 false
414
+ ...(args.hasCustomer === true ? { hasCustomer: true } : {}),
393
415
  },
394
416
  ...(args.topN !== undefined ? { topN: Number(args.topN) } : {}),
395
417
  ...(args.refresh === true ? { refreshRooms: true, refreshFrequent: true } : {}),
@@ -417,7 +439,7 @@ export async function rememberFrequentRoom(env, roomId) {
417
439
  if (!id)
418
440
  return false;
419
441
  const scope = await cacheScopeSuffix(env);
420
- const roomsCached = await readCache(env, `rooms-v2-${scope}`, { ttlMs: ROOMS_TTL_MS });
442
+ const roomsCached = await readCache(env, `rooms-v3-${scope}`, { ttlMs: ROOMS_TTL_MS });
421
443
  const name = roomsCached?.data?.find((room) => room.id === id)?.name ?? '';
422
444
  if (!name)
423
445
  return false;
@@ -1,15 +1,9 @@
1
1
  import { assertDatajsonSuccess, assertWeaverSuccess, } from '../host.js';
2
2
  import { projectValidationError } from '../errors.js';
3
3
  import { SCHEMA_FILTERS, SCHEMA_FIELDS, SCHEMA_ID_STRING, SCHEMA_PAGE_NO, SCHEMA_PAGE_SIZE, projectSuccess, schemaOf, } from './types.js';
4
- import { compactFilters, idString, operatorId, positiveInt, queryText, } from './shared.js';
4
+ import { buildCardUrl, compactFilters, idString, operatorId, positiveInt, queryText, resolveObjectId, } from './shared.js';
5
5
  const APP_TAG = 'weaver-project-eb';
6
- const DEFAULT_OBJ_IDS = { task: '1020270486210527232', project: '1020275562853482532' };
7
6
  const MIN_VERSION = [20, 6, 11];
8
- function buildCardUrl(baseUrl, objId, dataId) {
9
- if (!baseUrl || !objId || !dataId || /[\s/\\?#]/u.test(dataId))
10
- return null;
11
- return `${baseUrl.replace(/\/+$/u, '')}/sp/ebdfpage/card/0/${objId}/${dataId}`;
12
- }
13
7
  const CURRENT_USER_FILTERS = new Set([
14
8
  'manager',
15
9
  'participant',
@@ -114,7 +108,7 @@ async function decorateDetailUrls(host, operation, datajson) {
114
108
  if (targets.length === 0)
115
109
  return datajson;
116
110
  try {
117
- const objId = await getObject(host, operationTable(operation));
111
+ const objId = await resolveObjectId(host, operationTable(operation));
118
112
  const session = await host.getSession();
119
113
  for (const row of targets) {
120
114
  const url = buildCardUrl(session.baseUrl, objId, String(row.id));
@@ -161,19 +155,6 @@ async function getAppId(host) {
161
155
  throw new Error('当前环境未安装事井然项目管理应用');
162
156
  return appId;
163
157
  }
164
- async function getObject(host, value) {
165
- const response = await host.request({
166
- method: 'GET',
167
- path: '/api/ebproject/backendConsole/getCurrentObjId',
168
- query: { objId: DEFAULT_OBJ_IDS[value] },
169
- operation: 'project.object.id',
170
- });
171
- assertWeaverSuccess(response, 'project.object.id');
172
- const object = String(response?.data ?? '');
173
- if (!object)
174
- throw new Error('接口未返回当前环境对象 ID');
175
- return object;
176
- }
177
158
  async function datajsonRequest(host, operation, path, payload, query) {
178
159
  const response = await host.request({
179
160
  method: 'POST',
@@ -383,7 +364,7 @@ export const projectReadOperations = [
383
364
  inputSchema: schemaOf({ table: { type: 'string', enum: ['task', 'project'] } }, { required: ['table'] }),
384
365
  handler: async (args, { host }) => {
385
366
  const value = table(args.table);
386
- return projectSuccess('project.object.id', { table: value, objId: await getObject(host, value) });
367
+ return projectSuccess('project.object.id', { table: value, objId: await resolveObjectId(host, value) });
387
368
  },
388
369
  },
389
370
  {
@@ -417,7 +398,7 @@ export const projectReadOperations = [
417
398
  handler: async (args, { host }) => {
418
399
  const value = table(args.table);
419
400
  const groupId = args.groupId ? String(args.groupId) : await getAppId(host);
420
- const sourceId = args.sourceId ? String(args.sourceId) : await getObject(host, value);
401
+ const sourceId = args.sourceId ? String(args.sourceId) : await resolveObjectId(host, value);
421
402
  const response = await host.request({
422
403
  method: 'POST',
423
404
  path: '/api/ebuilder/common/ds/fields',
@@ -456,7 +437,7 @@ export const projectReadOperations = [
456
437
  handler: async (args, { host }) => {
457
438
  const value = table(args.table);
458
439
  const groupId = args.groupId ? String(args.groupId) : await getAppId(host);
459
- const sourceId = args.sourceId ? String(args.sourceId) : await getObject(host, value);
440
+ const sourceId = args.sourceId ? String(args.sourceId) : await resolveObjectId(host, value);
460
441
  const response = await host.request({
461
442
  method: 'GET',
462
443
  path: '/api/ebuilder/common/ds/options',
@@ -549,4 +530,4 @@ export const projectReadOperations = [
549
530
  hrmOperation('project.department.resolve', '/api/hrm/common/browser/data/department', 'organization', '按名称精确或模糊解析部门 ID'),
550
531
  hrmOperation('project.subcompany.resolve', '/api/hrm/common/browser/data/subcompany', 'organization', '按名称精确或模糊解析分部 ID'),
551
532
  ];
552
- export { getAppId, getObject, SCHEMA_FIELDS };
533
+ export { getAppId, resolveObjectId as getObject, SCHEMA_FIELDS };
@@ -1,4 +1,5 @@
1
1
  import { projectValidationError } from '../errors.js';
2
+ import { assertWeaverSuccess } from '../host.js';
2
3
  export function asObject(value, field = 'input') {
3
4
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
4
5
  throw projectValidationError('field_invalid', `${field} 必须是 JSON 对象`);
@@ -66,3 +67,24 @@ export function stableFingerprint(value) {
66
67
  }
67
68
  return `${text.length}:${hash.toString(16)}`;
68
69
  }
70
+ export const DEFAULT_OBJ_IDS = { task: '1020270486210527232', project: '1020275562853482532' };
71
+ /** 详情卡片地址:`{baseUrl}/sp/ebdfpage/card/0/{objId}/{dataId}`。参数不全或 dataId 含分隔符时返回 null。 */
72
+ export function buildCardUrl(baseUrl, objId, dataId) {
73
+ if (!baseUrl || !objId || !dataId || /[\s/\\?#]/u.test(dataId))
74
+ return null;
75
+ return `${baseUrl.replace(/\/+$/u, '')}/sp/ebdfpage/card/0/${objId}/${dataId}`;
76
+ }
77
+ /** 解析当前环境下的表单对象 ID(列表/详情/新建共用)。 */
78
+ export async function resolveObjectId(host, value) {
79
+ const response = await host.request({
80
+ method: 'GET',
81
+ path: '/api/ebproject/backendConsole/getCurrentObjId',
82
+ query: { objId: DEFAULT_OBJ_IDS[value] },
83
+ operation: 'project.object.id',
84
+ });
85
+ assertWeaverSuccess(response, 'project.object.id');
86
+ const object = String(response?.data ?? '');
87
+ if (!object)
88
+ throw new Error('接口未返回当前环境对象 ID');
89
+ return object;
90
+ }
@@ -1,7 +1,7 @@
1
1
  import { projectPartialError, projectValidationError } from '../errors.js';
2
2
  import { assertDatajsonSuccess } from '../host.js';
3
3
  import { SCHEMA_CONFIRM, SCHEMA_CONTINUATION, SCHEMA_FIELDS, SCHEMA_ID_STRING, SCHEMA_RECORDS, projectSuccess, schemaOf, } from './types.js';
4
- import { asObject, idString, operatorId, stableFingerprint } from './shared.js';
4
+ import { asObject, buildCardUrl, idString, operatorId, resolveObjectId, stableFingerprint } from './shared.js';
5
5
  const WRITE_TARGETS = {
6
6
  taskCreate: { entity: 'task', action: 'create', listPath: '', detailPath: '/api/ebuilder/form/formdata/v2/getFormDataByPk/queryTaskDetail', writePath: '/api/ebuilder/form/formdata/v2/saveFormData/createTask' },
7
7
  taskUpdate: { entity: 'task', action: 'update', listPath: '', detailPath: '/api/ebuilder/form/formdata/v2/getFormDataByPk/queryTaskDetail', writePath: '/api/ebuilder/form/formdata/v2/updateFormData/updateTask' },
@@ -62,6 +62,31 @@ async function sendWrite(host, operation, path, datajsonObject) {
62
62
  assertDatajsonSuccess(response, operation);
63
63
  return response?.datajson ?? null;
64
64
  }
65
+ /**
66
+ * 新建成功后,把返回的 dataIds 映射成详情页地址,供展示层把记录名称渲染为可点击链接。
67
+ * 链接解析失败不影响创建结果(写入已成功),只附带 detail_url_warning。
68
+ */
69
+ async function decorateCreatedRecords(host, entity, data) {
70
+ if (!data || typeof data !== 'object')
71
+ return data;
72
+ const value = data;
73
+ const ids = Array.isArray(value.dataIds)
74
+ ? value.dataIds.map((item) => String(item)).filter((item) => item !== '')
75
+ : [];
76
+ if (ids.length === 0)
77
+ return data;
78
+ try {
79
+ const [objId, session] = await Promise.all([resolveObjectId(host, entity), host.getSession()]);
80
+ const urls = ids.map((id) => buildCardUrl(session.baseUrl, objId, id));
81
+ return urls.some((url) => url !== null) ? { ...value, detail_urls: urls } : data;
82
+ }
83
+ catch (error) {
84
+ return {
85
+ ...value,
86
+ detail_url_warning: `详情链接暂不可用:${error instanceof Error ? error.message : String(error)};创建已成功,不要因此重试。`,
87
+ };
88
+ }
89
+ }
65
90
  async function verifySession(env, continuation) {
66
91
  if (continuation.userId !== await operatorId(env.host)) {
67
92
  throw projectValidationError('continuation_mismatch', 'continuation 与当前登录会话不匹配');
@@ -119,7 +144,8 @@ function createOperations(namePrefix, target) {
119
144
  await verifySession(env, continuation);
120
145
  const datajson = asObject(continuation.datajson, 'continuation.datajson');
121
146
  const response = await sendWrite(env.host, applyName, target.writePath, datajson);
122
- return projectSuccess(applyName, response, { targetType: target.entity, writeAction: 'create' });
147
+ const data = await decorateCreatedRecords(env.host, target.entity, response);
148
+ return projectSuccess(applyName, data, { targetType: target.entity, writeAction: 'create' });
123
149
  },
124
150
  },
125
151
  ];
@@ -0,0 +1,119 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { createRecruitContextLoader, } from '../ehr/operations/recruit.js';
6
+ import { rsFail } from './errors.js';
7
+ const flowMemo = new Map();
8
+ const digitalMenuMemo = new Map();
9
+ function cacheDirFor(baseUrl) {
10
+ const url = new URL(baseUrl);
11
+ const host = url.hostname + (url.port ? `_${url.port}` : '');
12
+ const envRoot = process.env.E10_SKILL_CACHE_DIR;
13
+ if (envRoot)
14
+ return join(envRoot, host);
15
+ const home = process.env.WEAVER_WORK_CLI_HOME || process.env.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
16
+ return join(home, 'env-cache', 'recruit-ops', host);
17
+ }
18
+ /**
19
+ * 磁盘缓存兼容:脚本版(recruit-stage 技能)播种的缓存里若已带 defaultRecruitFlow /
20
+ * lists.digital_human,直接复用(同环境免请求);CLI 侧则运行时解析一次并 memo。
21
+ */
22
+ async function readLegacyCacheExtra(baseUrl) {
23
+ try {
24
+ const dir = cacheDirFor(baseUrl);
25
+ if (!existsSync(dir))
26
+ return { defaultRecruitFlow: null, digitalHumanMenu: null };
27
+ const files = (await import('node:fs/promises')).readdir(dir);
28
+ const jsonFiles = (await files).filter((f) => f.endsWith('.json'));
29
+ if (jsonFiles.length !== 1)
30
+ return { defaultRecruitFlow: null, digitalHumanMenu: null };
31
+ const raw = JSON.parse(await readFile(join(dir, jsonFiles[0]), 'utf8'));
32
+ const flow = raw?.defaultRecruitFlow && raw.defaultRecruitFlow.id
33
+ ? { id: String(raw.defaultRecruitFlow.id), name: String(raw.defaultRecruitFlow.name || '') }
34
+ : null;
35
+ const dh = raw?.lists?.digital_human?.listId
36
+ ? {
37
+ menuName: String(raw.lists.digital_human.menuName || '面试数字人配置'),
38
+ listId: String(raw.lists.digital_human.listId),
39
+ cusMenuId: String(raw.lists.digital_human.cusMenuId || ''),
40
+ }
41
+ : null;
42
+ return { defaultRecruitFlow: flow, digitalHumanMenu: dh };
43
+ }
44
+ catch {
45
+ return { defaultRecruitFlow: null, digitalHumanMenu: null };
46
+ }
47
+ }
48
+ /** 默认招聘流程解析:招聘设置/流程列表中「默认启用」的那条(运行时解析,跨环境不写死)。 */
49
+ async function resolveDefaultRecruitFlow(host, baseUrl, appId) {
50
+ const memoKey = baseUrl;
51
+ if (flowMemo.has(memoKey))
52
+ return flowMemo.get(memoKey) ?? null;
53
+ const legacy = await readLegacyCacheExtra(baseUrl);
54
+ if (legacy.defaultRecruitFlow) {
55
+ flowMemo.set(memoKey, legacy.defaultRecruitFlow);
56
+ return legacy.defaultRecruitFlow;
57
+ }
58
+ let result = null;
59
+ try {
60
+ // 招聘流程配置( hire structures ):优先业务配置通道;接口不可用或为空时保持 null(由摘要标注"未解析到默认流程")
61
+ const r = await host.request({
62
+ path: '/api/recruit/position/recruitFlow/list',
63
+ method: 'POST',
64
+ body: { appId, current: 1, pageSize: 50 },
65
+ operation: 'context.defaultRecruitFlow',
66
+ });
67
+ const rows = r?.data?.list || r?.data?.rows || r?.data || [];
68
+ const list = Array.isArray(rows) ? rows : [];
69
+ const enabled = list.find((x) => x && (x.isDefault === true || Number(x.isDefault) === 1 || x.default_flow === '1'))
70
+ || list.find((x) => x && x.id);
71
+ if (enabled?.id) {
72
+ result = { id: String(enabled.id), name: String(enabled.name || enabled.flow_name || '') };
73
+ }
74
+ }
75
+ catch { /* 接口形态不符/未开放:保持 null,不阻塞建职位 */ }
76
+ flowMemo.set(memoKey, result);
77
+ return result;
78
+ }
79
+ async function resolveDigitalHumanMenu(host, baseUrl, cache) {
80
+ const memoKey = baseUrl;
81
+ if (digitalMenuMemo.has(memoKey))
82
+ return digitalMenuMemo.get(memoKey) ?? null;
83
+ const legacy = await readLegacyCacheExtra(baseUrl);
84
+ if (legacy.digitalHumanMenu) {
85
+ digitalMenuMemo.set(memoKey, legacy.digitalHumanMenu);
86
+ return legacy.digitalHumanMenu;
87
+ }
88
+ // 从 all_search 菜单里按别名找(跨基线菜单名不同:面试数字人配置/数字人配置)
89
+ const allSearch = (cache.lists?.all_search || []);
90
+ const candidates = allSearch.filter((m) => m.listId);
91
+ let hit = (candidates.find((m) => (m.menuName || '').includes('面试数字人'))
92
+ || candidates.find((m) => (m.menuName || '').includes('数字人配置'))) ?? null;
93
+ digitalMenuMemo.set(memoKey, hit);
94
+ return hit;
95
+ }
96
+ const loaders = new Map();
97
+ export function createRecruitStageContext() {
98
+ return async (host) => {
99
+ const loaderKey = 'shared';
100
+ if (!loaders.has(loaderKey))
101
+ loaders.set(loaderKey, createRecruitContextLoader());
102
+ const cache = await loaders.get(loaderKey).load(host);
103
+ const baseUrl = (await host.getSession()).baseUrl.replace(/\/+$/, '');
104
+ const [defaultRecruitFlow, digitalHumanMenu] = await Promise.all([
105
+ resolveDefaultRecruitFlow(host, baseUrl, cache.appId),
106
+ resolveDigitalHumanMenu(host, baseUrl, cache),
107
+ ]);
108
+ return { ...cache, defaultRecruitFlow, digitalHumanMenu };
109
+ };
110
+ }
111
+ /** 供 operation 内直接取上下文的辅助(失败统一转 rs 错误)。 */
112
+ export async function loadContext(host) {
113
+ try {
114
+ return await createRecruitStageContext()(host);
115
+ }
116
+ catch (e) {
117
+ throw rsFail(e?.message || '招聘环境上下文解析失败', { stage: 'context' });
118
+ }
119
+ }
@@ -0,0 +1,73 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+ import { rsValidationError } from './errors.js';
7
+ function stateRoot(ctx) {
8
+ const configured = ctx?.config?.path();
9
+ if (configured)
10
+ return resolve(configured, '..');
11
+ return process.env.WEAVER_WORK_CLI_HOME || process.env.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
12
+ }
13
+ async function loadKey(ctx) {
14
+ const keysDir = resolve(stateRoot(ctx), 'keys');
15
+ const filename = resolve(keysDir, 'recruit-stage-continuation.key');
16
+ await mkdir(keysDir, { recursive: true, mode: 0o700 });
17
+ if (existsSync(filename))
18
+ return readFile(filename);
19
+ const key = randomBytes(32);
20
+ try {
21
+ await writeFile(filename, key, { flag: 'wx', mode: 0o600 });
22
+ if (process.platform !== 'win32')
23
+ await chmod(filename, 0o600).catch(() => undefined);
24
+ return key;
25
+ }
26
+ catch {
27
+ return readFile(filename);
28
+ }
29
+ }
30
+ function encode(value) {
31
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
32
+ }
33
+ function signature(payload, key) {
34
+ return createHmac('sha256', key).update(payload).digest('base64url');
35
+ }
36
+ export function createRecruitStageContinuationSigner(ctx) {
37
+ return {
38
+ async issue(payload, ttlMs = 10 * 60_000) {
39
+ const issuedAt = Date.now();
40
+ const body = encode({ ...payload, issuedAt, expiresAt: issuedAt + ttlMs });
41
+ const sig = signature(body, await loadKey(ctx));
42
+ return `${body}.${sig}`;
43
+ },
44
+ async verify(token, expectedOperation) {
45
+ if (typeof token !== 'string' || !token.includes('.')) {
46
+ throw rsValidationError('continuation_invalid', 'continuation 无效');
47
+ }
48
+ const [body, provided] = token.split('.');
49
+ const expected = signature(body, await loadKey(ctx));
50
+ const valid = provided.length === expected.length
51
+ && timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
52
+ if (!valid)
53
+ throw rsValidationError('continuation_invalid', 'continuation 签名校验失败');
54
+ let payload;
55
+ try {
56
+ payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
57
+ }
58
+ catch {
59
+ throw rsValidationError('continuation_invalid', 'continuation 载荷解析失败');
60
+ }
61
+ if (!payload || typeof payload !== 'object') {
62
+ throw rsValidationError('continuation_invalid', 'continuation 载荷必须是对象');
63
+ }
64
+ if (typeof payload.expiresAt === 'number' && Date.now() > payload.expiresAt) {
65
+ throw rsValidationError('continuation_expired', 'continuation 已过期,请重新 prepare');
66
+ }
67
+ if (payload.operation && String(payload.operation) !== String(expectedOperation)) {
68
+ throw rsValidationError('continuation_operation_mismatch', `continuation 属于操作 ${String(payload.operation)},不能用于 ${expectedOperation}`);
69
+ }
70
+ return payload;
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,65 @@
1
+ import { CliError, ExitCode } from '../../core/errors.js';
2
+ /**
3
+ * recruit-stage 模块错误类型。
4
+ * 形态与 ehr 模块一致(type/subtype/details/partialData),
5
+ * 前缀 rs 避免与 ehr 的错误类型混淆。
6
+ */
7
+ export class RecruitStageCliError extends CliError {
8
+ type;
9
+ subtype;
10
+ code;
11
+ retryable;
12
+ stage;
13
+ details;
14
+ partialData;
15
+ constructor(type, subtype, message, options = {}) {
16
+ super(message, options.exitCode ?? ExitCode.InternalError);
17
+ this.name = 'RecruitStageCliError';
18
+ this.type = type;
19
+ this.subtype = subtype;
20
+ this.code = options.code;
21
+ this.retryable = options.retryable ?? false;
22
+ this.stage = options.stage;
23
+ this.details = options.details;
24
+ this.partialData = options.partialData;
25
+ }
26
+ }
27
+ export function rsValidationError(subtype, message, details) {
28
+ return new RecruitStageCliError('validation', subtype, message, {
29
+ details,
30
+ exitCode: ExitCode.UsageError,
31
+ });
32
+ }
33
+ export function rsAuthError(subtype, message, details) {
34
+ return new RecruitStageCliError('authentication', subtype, message, {
35
+ details,
36
+ exitCode: ExitCode.AuthError,
37
+ });
38
+ }
39
+ export function rsNetworkError(subtype, message, options = {}) {
40
+ return new RecruitStageCliError('network', subtype, message, {
41
+ ...options,
42
+ retryable: options.retryable ?? true,
43
+ exitCode: ExitCode.NetworkError,
44
+ });
45
+ }
46
+ export function rsConfirmationError(action, details) {
47
+ return new RecruitStageCliError('confirmation', 'required', '该操作需要明确确认', {
48
+ details: { action, ...details },
49
+ exitCode: ExitCode.ConfirmationRequired,
50
+ });
51
+ }
52
+ /** 业务失败(对应脚本版 throw new Error(...) 的主通道)。 */
53
+ export function rsFail(message, details) {
54
+ return new RecruitStageCliError('api', 'business_error', message, {
55
+ details,
56
+ exitCode: ExitCode.GeneralError,
57
+ });
58
+ }
59
+ export function rsPartialError(message, partialData, details) {
60
+ return new RecruitStageCliError('partial', 'write_uncertain', message, {
61
+ details,
62
+ partialData,
63
+ exitCode: ExitCode.Partial,
64
+ });
65
+ }
@@ -0,0 +1,109 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
2
+ import { requireE10Session } from '../../internal/e10/context.js';
3
+ import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
+ import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
6
+ import { rsAuthError, rsNetworkError, RecruitStageCliError, rsValidationError } from './errors.js';
7
+ function inferProfile(ctx) {
8
+ const options = ctx.getGlobalOptions();
9
+ return options.profile || getActiveProfile();
10
+ }
11
+ export function assertPath(requestPath) {
12
+ if (typeof requestPath !== 'string' || !requestPath.startsWith('/') || requestPath.startsWith('//')) {
13
+ throw rsValidationError('path_invalid', '请求路径必须是相对绝对路径');
14
+ }
15
+ if (/^https?:/i.test(requestPath) || /[\r\n]/.test(requestPath) || requestPath.includes('/../')) {
16
+ throw rsValidationError('path_invalid', '请求路径不允许跨域或路径逃逸');
17
+ }
18
+ if (!requestPath.startsWith('/api/')) {
19
+ throw rsValidationError('path_not_allowed', '只能调用 /api/ 路径');
20
+ }
21
+ }
22
+ /**
23
+ * 与 ehr host 的刻意差异:**不做全局 isBusinessSuccess 强判定**。
24
+ * 原因:EB 表单引擎各接口响应契约不一(getListDatas 返回 {data:{...}} 未必带 status/success/code),
25
+ * 脚本版(write-op/e10-request.mjs)也只在 saveFormData 等个别接口做业务判定——
26
+ * 业务成功判定留给各 operation 按接口语义自行处理,宿主只判 HTTP 状态与会话失效。
27
+ */
28
+ export function createRecruitStageHost(ctx, dependencies = {}) {
29
+ let e10Session = null;
30
+ let sessionContext = null;
31
+ const requireSession = dependencies.requireSession
32
+ || ((innerCtx) => requireE10Session(innerCtx, { skipProfileEnrichment: true }));
33
+ async function ensureSession() {
34
+ if (!e10Session) {
35
+ e10Session = await requireSession(ctx);
36
+ sessionContext = {
37
+ baseUrl: e10Session.getBaseUrl(),
38
+ profile: inferProfile(ctx),
39
+ userId: e10Session.userId,
40
+ tenantKey: e10Session.tenantKey,
41
+ eteamsId: e10Session.eteamsId,
42
+ };
43
+ }
44
+ return e10Session;
45
+ }
46
+ async function getSession() {
47
+ await ensureSession();
48
+ return sessionContext;
49
+ }
50
+ async function runtime() {
51
+ return createE10RequestRuntime(ctx, await ensureSession(), {
52
+ interceptors: dependencies.interceptors,
53
+ fetch: dependencies.fetch,
54
+ });
55
+ }
56
+ async function request(request) {
57
+ const { method, path, query, body, operation, multipart, headers: extraHeaders = {}, timeoutMs = 30_000, } = request;
58
+ assertPath(path);
59
+ const resolvedMethod = method || (body === undefined && multipart === undefined ? 'GET' : 'POST');
60
+ let response;
61
+ try {
62
+ response = await (await runtime()).requestJson({
63
+ module: 'recruit-stage',
64
+ operation,
65
+ method: resolvedMethod,
66
+ path,
67
+ query,
68
+ body: multipart === undefined ? body : undefined,
69
+ formData: multipart,
70
+ headers: { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders },
71
+ timeoutMs,
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (error instanceof E10ApiError && error.type === 'network') {
76
+ throw rsNetworkError(error.subtype, error.message, {
77
+ retryable: error.retryable,
78
+ details: error.details,
79
+ });
80
+ }
81
+ throw error;
82
+ }
83
+ if (isE10SessionExpiredResponse(response)) {
84
+ throw rsAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
85
+ }
86
+ if (response.parseError) {
87
+ if (response.status < 200 || response.status >= 300) {
88
+ throw new RecruitStageCliError('api', 'http_error', `泛微接口 HTTP ${response.status}`, {
89
+ code: response.status,
90
+ details: { status: response.status },
91
+ exitCode: 1,
92
+ });
93
+ }
94
+ throw rsNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
95
+ }
96
+ if (response.status < 200 || response.status >= 300) {
97
+ throw new RecruitStageCliError('api', 'http_error', `泛微接口 HTTP ${response.status}`, {
98
+ code: response.status,
99
+ details: { status: response.status, message: response.value?.msg },
100
+ exitCode: 1,
101
+ });
102
+ }
103
+ // 注意:运行时对响应做了 parseJsonPreservingBigInts(16+ 位整数加引号保精度),
104
+ // 业务代码拿到的 18 位 id 已是字符串形态——脚本版「从原文提取 formDataIds 防丢精度」的
105
+ // 特殊处理在 CLI 侧不再需要。
106
+ return response.value;
107
+ }
108
+ return { request, getSession };
109
+ }