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
@@ -301,21 +301,28 @@ async function resolveCandidateForWrite(host, cache, talentId) {
301
301
  throw ehrValidationError('recruit_batch_id_missing', '未取到 batchId(prepareQuery 返回异常),无法定位候选人应聘批次');
302
302
  }
303
303
  const v = randomHex6();
304
- const otherResponse = (await host.request({
305
- path: `/api/recruit/candidate/getCandidateCardOtherInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_ab=ab`,
306
- headers: extra,
307
- }));
308
- const batch = otherResponse?.data?.currentCandidateBatch || {};
309
- let name = batch?.talent?.name || '';
310
- if (!name) {
311
- const baseResponse = (await host.request({
304
+ // otherInfo 必成功(保持原语义);baseInfo 并行取(姓名兜底 + AI 面试条件必填的邮箱/手机),失败不阻断
305
+ const [otherResponse, baseResponse] = await Promise.all([
306
+ host.request({
307
+ path: `/api/recruit/candidate/getCandidateCardOtherInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_ab=ab`,
308
+ headers: extra,
309
+ }),
310
+ host
311
+ .request({
312
312
  path: `/api/recruit/candidate/getCandidateCardBaseInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_cd=cd`,
313
313
  headers: extra,
314
- }));
315
- name = baseResponse?.data?.talent?.hrm?.name || '';
316
- }
314
+ })
315
+ .catch(() => null),
316
+ ]);
317
+ const batch = otherResponse?.data?.currentCandidateBatch || {};
318
+ const talentBase = baseResponse?.data?.talent || {};
319
+ const name = batch?.talent?.name || talentBase?.hrm?.name || talentBase?.name || '';
320
+ const email = String(talentBase.email || talentBase.hrm?.email || '').trim();
321
+ const mobile = String(talentBase.telephone || talentBase.mobile || '').trim();
317
322
  return {
318
323
  name,
324
+ email,
325
+ mobile,
319
326
  candidateManageId: batch?.id || batch?.candidateManageId || null,
320
327
  positionId: batch?.position?.id || null,
321
328
  positionName: batch?.position?.name || null,
@@ -535,6 +542,8 @@ const candidateCreateApply = {
535
542
  // 安排面试 operation
536
543
  // ---------------------------------------------------------------------------
537
544
  const INTERVIEW_TYPES = ['现场面试', '视频面试', '电话面试', 'AI面试'];
545
+ /** AI 面试方案默认值(用户口径:默认「大模型面试」)。若环境无此方案,运行时列出候选让用户选,绝不自动取第一条。 */
546
+ const AI_DEFAULT_PLAN_NAME = '大模型面试';
538
547
  const INTERVIEW_DATAKEYS = ['subject', 'start_time', 'end_time', 'interview_type', 'interview_session', 'interviewer',
539
548
  'position_manage_id', 'talent_id', 'candidate_manage_id', 'mobile', 'email', 'interview_location', 'notice_mould'];
540
549
  function defaultInterviewWindow() {
@@ -544,16 +553,181 @@ function defaultInterviewWindow() {
544
553
  const end = new Date(now.getTime() + 60 * 60 * 1000);
545
554
  return { start: fmt(now), end: fmt(end) };
546
555
  }
556
+ /** 当前登录人(teamsCheck → currentUser.id/username):面试官未指定时的默认值。
557
+ * teamsCheck 响应无 status 字段、不被 isBusinessSuccess 认可,故直连 fetch 绕开业务判定(同 recruit.ts 口径)。 */
558
+ async function resolveCurrentUserForWrite(host) {
559
+ try {
560
+ const session = await host.getSession();
561
+ const baseUrl = String(session?.baseUrl || '');
562
+ if (!baseUrl)
563
+ return null;
564
+ const resp = await fetch(`${baseUrl}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(baseUrl)}`, {
565
+ method: 'POST',
566
+ headers: {
567
+ accept: 'application/json',
568
+ 'content-type': 'application/json',
569
+ eteamsid: String(session?.eteamsId || ''),
570
+ cookie: typeof session?.cookieHeader === 'function' ? session.cookieHeader() : '',
571
+ 'user-agent': typeof session?.uaHeader === 'function' ? session.uaHeader() : 'weaver-work-cli',
572
+ origin: baseUrl,
573
+ },
574
+ redirect: 'manual',
575
+ });
576
+ const data = await resp.json().catch(() => ({}));
577
+ const cu = data?.currentUser ?? data?.data?.currentUser ?? null;
578
+ if (!cu?.id)
579
+ return null;
580
+ return { id: String(cu.id), name: String(cu.username || cu.id) };
581
+ }
582
+ catch {
583
+ return null;
584
+ }
585
+ }
586
+ /** 取表单布局 id(GET 在低基线返 405 → 回退 POST),用于 AI 面试方案的关联字段候选 */
587
+ async function fetchLayoutIdForForm(host, formId) {
588
+ try {
589
+ const r = (await host.request({
590
+ path: `/api/ebuilder/form/form/core/getFormLayout?formId=${encodeURIComponent(formId)}`,
591
+ method: 'GET',
592
+ }));
593
+ const id = r?.data?.formLayout?.id;
594
+ if (id)
595
+ return String(id);
596
+ }
597
+ catch {
598
+ // 低基线 GET 405 → POST
599
+ }
600
+ try {
601
+ const r = (await host.request({
602
+ path: '/api/ebuilder/form/form/core/getFormLayout',
603
+ method: 'POST',
604
+ body: { module: 'ebuilderform', formId, terminalType: 'PC' },
605
+ }));
606
+ const id = r?.data?.formLayout?.id;
607
+ if (id)
608
+ return String(id);
609
+ }
610
+ catch {
611
+ // 布局取不到时回退 formId(页面保存本身也回退 formId)
612
+ }
613
+ return formId;
614
+ }
615
+ const DIGITAL_PLAN_NAME_KEYS = ['msfa_name', 'digital_name', 'plan_name', 'scheme_name'];
616
+ /** AI 面试方案(面试数字人)候选:① 关联字段候选接口 → ② 「面试数字人配置」列表兜底。
617
+ * 🔴 只返回候选,绝不自动取第一条(方案选错=面试由错误的数字人执行)。 */
618
+ async function listDigitalPlansForWrite(host, cache, formId, layoutId, fieldId) {
619
+ try {
620
+ const r = (await host.request({
621
+ path: '/api/ebuilder/form/common/browser/complete/ebuilder',
622
+ body: {
623
+ browserMultiple: false,
624
+ newly: 1,
625
+ searchValue: '',
626
+ formParam: JSON.stringify({ formId, layoutId, fieldId, filterItems: [], module: 'ebuilderform', dataDetails: [] }),
627
+ openScroll: true,
628
+ controlId: fieldId,
629
+ businessId: formId,
630
+ variableMap: {},
631
+ },
632
+ }));
633
+ if (r?.status !== false && r?.data) {
634
+ const list = r.data.data || [];
635
+ const items = list
636
+ .filter((x) => x && x.id)
637
+ .map((x) => ({ id: String(x.id), name: String(x.name || '').trim() }));
638
+ if (items.length)
639
+ return { available: true, items };
640
+ }
641
+ }
642
+ catch {
643
+ // 回退列表
644
+ }
645
+ const menus = (cache.lists?.all_search || []);
646
+ const menu = menus.find((m) => (m.menuName || '').includes('面试数字人'))
647
+ || menus.find((m) => (m.menuName || '').includes('数字人配置'));
648
+ if (!menu?.listId) {
649
+ return { available: false, items: [], reason: '关联字段候选接口不可用,且当前环境未配置「面试数字人配置」列表菜单' };
650
+ }
651
+ let objId = '';
652
+ try {
653
+ const r = (await host.request({
654
+ path: `/api/ebuilder/form/obj/getObjsByTag?appId=${encodeURIComponent(String(cache.appId || ''))}&tag=uf_rcrt_digital_human`,
655
+ method: 'GET',
656
+ }));
657
+ const d = r?.data;
658
+ if (Array.isArray(d) && d.length)
659
+ objId = String(d[0]?.id || '');
660
+ }
661
+ catch {
662
+ // 取不到对象 id
663
+ }
664
+ if (!objId)
665
+ return { available: false, items: [], reason: '未能解析「面试数字人配置」对象 id' };
666
+ const rows = [];
667
+ for (let cur = 1; cur <= 20; cur++) {
668
+ const r = (await host.request({
669
+ path: `/api/ebuilder/form/list/getListDatas?passid=${objId}&cid=${objId}`,
670
+ body: {
671
+ current: cur,
672
+ pageSize: 100,
673
+ tableuid: randomHex32(),
674
+ listId: menu.listId,
675
+ searchParamData: { searchType: '3', type: 'and' },
676
+ terminal: 'PC',
677
+ browser: 'Chrome',
678
+ requiredMatch: 1,
679
+ mapResult: 1,
680
+ recheckQuery: 1,
681
+ isFollow: 0,
682
+ isShareToMe: 0,
683
+ filterCondition: {},
684
+ cusMenuId: menu.cusMenuId,
685
+ urlPageTitle: menu.menuName,
686
+ menuName: menu.menuName,
687
+ },
688
+ headers: { ebBusinessId: String(menu.listId) },
689
+ }));
690
+ const page = r?.data?.data || [];
691
+ rows.push(...page);
692
+ if (page.length < 100)
693
+ break;
694
+ }
695
+ const items = rows
696
+ .map((x) => {
697
+ const nk = DIGITAL_PLAN_NAME_KEYS.find((k) => String(x[k] || '').trim());
698
+ return { id: String(x.form_data_id || x.id || ''), name: String(nk ? x[nk] : '').trim() };
699
+ })
700
+ .filter((x) => x.id);
701
+ if (!items.length)
702
+ return { available: false, items: [], reason: '「面试数字人配置」暂无可选方案' };
703
+ return { available: true, items };
704
+ }
705
+ /** 方案匹配:id 全等 → 名称全等 → 名称包含(唯一才算命中,否则回歧义列表) */
706
+ function pickDigitalPlanForWrite(items, key) {
707
+ const k = String(key || '').trim();
708
+ if (!k)
709
+ return { plan: null, ambiguous: [] };
710
+ const byId = items.filter((x) => x.id === k);
711
+ if (byId.length === 1)
712
+ return { plan: byId[0], ambiguous: [] };
713
+ const byExact = items.filter((x) => x.name === k);
714
+ if (byExact.length === 1)
715
+ return { plan: byExact[0], ambiguous: [] };
716
+ const byLike = items.filter((x) => x.name.includes(k));
717
+ if (byLike.length === 1)
718
+ return { plan: byLike[0], ambiguous: [] };
719
+ return { plan: null, ambiguous: byLike };
720
+ }
547
721
  async function buildInterviewPlan(env, cache, args) {
548
722
  const host = env.host;
549
723
  const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
550
724
  if (!/^\d+$/.test(talentId))
551
725
  throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
552
- const interviewer = requiredString(args, 'interviewer', 'interviewer_required', '面试官姓名');
553
726
  const type = typeof args.type === 'string' && args.type.trim() ? args.type.trim() : '现场面试';
554
727
  if (!INTERVIEW_TYPES.includes(type)) {
555
728
  throw ehrValidationError('enum_invalid', `type(面试方式)取值必须是 ${INTERVIEW_TYPES.join(' / ')} 之一`);
556
729
  }
730
+ const isAI = type === 'AI面试';
557
731
  const session = typeof args.session === 'string' && args.session.trim() ? args.session.trim() : '初试';
558
732
  const candidate = await resolveCandidateForWrite(host, cache, talentId);
559
733
  const positionId = args.positionId ? String(args.positionId) : candidate.positionId;
@@ -582,9 +756,63 @@ async function buildInterviewPlan(env, cache, args) {
582
756
  if (!sessionOptionId) {
583
757
  throw ehrValidationError('enum_invalid', `面试轮次「${session}」不在枚举中(${Object.keys(sessionMap).join(' / ')})`);
584
758
  }
585
- const interviewerId = await resolvePersonId(host, fieldIdOf('interviewer'), interviewer);
586
- if (!interviewerId) {
587
- throw ehrValidationError('interviewer_not_found', `面试官「${interviewer}」解析不到 userId(browser/complete/resource 无匹配)`);
759
+ // 面试官:显式指定 → 按名解析;未指定 → 默认当前登录人(少问一次,与页面待办默认口径一致)
760
+ const interviewerInput = typeof args.interviewer === 'string' ? args.interviewer.trim() : '';
761
+ let interviewerId = null;
762
+ let interviewer = interviewerInput;
763
+ if (interviewerInput) {
764
+ interviewerId = await resolvePersonId(host, fieldIdOf('interviewer'), interviewerInput);
765
+ if (!interviewerId) {
766
+ throw ehrValidationError('interviewer_not_found', `面试官「${interviewerInput}」解析不到 userId(browser/complete/resource 无匹配)`);
767
+ }
768
+ }
769
+ else {
770
+ const me = await resolveCurrentUserForWrite(host);
771
+ if (!me) {
772
+ throw ehrValidationError('interviewer_unresolved', '未指定面试官,且无法解析当前登录人(teamsCheck 失败);请用 interviewer 参数指定');
773
+ }
774
+ // 🔴 teamsCheck 的 currentUser.id 与人员字段 optionId 语义未必相同(实测两者 id 不同),
775
+ // 故用与显式指定完全相同的解析通道(browser/complete/resource)把姓名换成 userId。
776
+ interviewerId = await resolvePersonId(host, fieldIdOf('interviewer'), me.name);
777
+ if (!interviewerId) {
778
+ throw ehrValidationError('interviewer_unresolved', `未指定面试官,当前登录人「${me.name}」在人员字段中解析不到;请用 interviewer 参数指定`);
779
+ }
780
+ interviewer = me.name;
781
+ }
782
+ // AI 面试:方案(digital_id)与邮箱都是条件必填,不填保存必失败
783
+ let aiPlan = null;
784
+ let aiPlans = [];
785
+ let email = String(args.email || '').trim() || candidate.email;
786
+ const mobile = candidate.mobile;
787
+ if (isAI) {
788
+ const digitalFid = fieldIdOf('digital_id');
789
+ if (!digitalFid) {
790
+ throw ehrValidationError('recruit_field_missing', '面试计划表单缺少「AI面试方案」字段(digital_id),无法安排 AI 面试');
791
+ }
792
+ const layoutId = await fetchLayoutIdForForm(host, formId);
793
+ const plans = await listDigitalPlansForWrite(host, cache, formId, layoutId, digitalFid);
794
+ aiPlans = plans.items;
795
+ if (!plans.available) {
796
+ throw ehrValidationError('ai_plan_unavailable', `AI 面试必须选择「AI面试方案」,但${plans.reason || '暂无可选方案'};请先在该环境维护「面试数字人配置」`);
797
+ }
798
+ // 默认「大模型面试」(用户口径);环境无此方案时列出候选,绝不自动取第一条
799
+ const wanted = typeof args.aiPlan === 'string' && args.aiPlan.trim() ? args.aiPlan.trim() : AI_DEFAULT_PLAN_NAME;
800
+ const picked = pickDigitalPlanForWrite(aiPlans, wanted);
801
+ if (picked.plan) {
802
+ aiPlan = picked.plan;
803
+ }
804
+ else if (picked.ambiguous.length > 1) {
805
+ throw ehrValidationError('ai_plan_ambiguous', `AI面试方案「${wanted}」命中多个:${picked.ambiguous.map((x) => x.name).join('、')},请用 aiPlan 指明具体方案`, { candidates: picked.ambiguous });
806
+ }
807
+ else {
808
+ throw ehrValidationError('ai_plan_not_found', `未找到 AI面试方案「${wanted}」,请用 aiPlan 从候选中指定(候选:${aiPlans.map((x) => x.name).join('、')})`, { candidates: aiPlans });
809
+ }
810
+ if (!email) {
811
+ throw ehrValidationError('email_required', 'AI 面试要求填写候选人邮箱:人才资料里没有邮箱,请用 email 参数提供');
812
+ }
813
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
814
+ throw ehrValidationError('email_invalid', `候选人邮箱格式不正确:${email}`);
815
+ }
588
816
  }
589
817
  const window = defaultInterviewWindow();
590
818
  const start = typeof args.startTime === 'string' && args.startTime.trim() ? args.startTime.trim() : window.start;
@@ -603,6 +831,9 @@ async function buildInterviewPlan(env, cache, args) {
603
831
  { formField: { id: fieldIdOf('position_manage_id') }, dataOptions: [{ optionId: positionId, content: candidate.positionName }] },
604
832
  { formField: { id: fieldIdOf('talent_id') }, dataOptions: [{ optionId: talentId, content: candidate.name }] },
605
833
  { formField: { id: fieldIdOf('candidate_manage_id') }, dataOptions: [{ optionId: candidate.candidateManageId, content: candidate.candidateManageId }] },
834
+ ...(mobile ? [{ formField: { id: fieldIdOf('mobile') }, content: mobile }] : []),
835
+ ...(email ? [{ formField: { id: fieldIdOf('email') }, content: email }] : []),
836
+ ...(aiPlan ? [{ formField: { id: fieldIdOf('digital_id') }, dataOptions: [{ optionId: aiPlan.id, content: aiPlan.name }] }] : []),
606
837
  ];
607
838
  return {
608
839
  formId, objId, dataDetails,
@@ -610,12 +841,19 @@ async function buildInterviewPlan(env, cache, args) {
610
841
  positionName: candidate.positionName,
611
842
  interviewTime: `${start} ~ ${end}`,
612
843
  interviewer,
844
+ email: email || undefined,
845
+ mobile: mobile || undefined,
846
+ isAIInterview: isAI,
847
+ aiPlan,
848
+ aiPlans: isAI ? aiPlans : undefined,
613
849
  };
614
850
  }
615
851
  const interviewArrangeSchema = {
616
852
  talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID(candidate.search 返回的 talentId)' },
617
- interviewer: { type: 'string', description: '面试官姓名(须可解析为系统人员)' },
853
+ interviewer: { type: 'string', description: '面试官姓名(可选;不填默认当前登录人)' },
618
854
  type: { type: 'string', enum: [...INTERVIEW_TYPES], description: '面试方式,默认现场面试' },
855
+ aiPlan: { type: 'string', description: 'AI 面试方案(方案名或 id;type=AI面试 时生效,默认「大模型面试」)' },
856
+ email: { type: 'string', description: '候选人邮箱(AI 面试必填;不填则取人才资料里的邮箱)' },
619
857
  session: { type: 'string', description: '面试轮次(如 初试/复试),默认初试;须在枚举内' },
620
858
  positionId: { type: 'string', description: '应聘职位 ID;不填时从人才当前应聘批次继承' },
621
859
  startTime: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$', description: '开始时间 YYYY-MM-DD HH:mm,默认当前时间' },
@@ -631,7 +869,7 @@ const interviewArrangePrepare = {
631
869
  capability: 'recruit',
632
870
  risk: WRITE,
633
871
  requiresConfirmation: true,
634
- inputSchema: schemaOf({ ...interviewArrangeSchema }, { required: ['talentId', 'interviewer'] }),
872
+ inputSchema: schemaOf({ ...interviewArrangeSchema }, { required: ['talentId'] }),
635
873
  handler: async (args, env) => {
636
874
  const cache = await requireRecruitCache(env);
637
875
  const plan = await buildInterviewPlan(env, cache, args);
@@ -656,6 +894,13 @@ const interviewArrangePrepare = {
656
894
  session: args.session || '初试',
657
895
  time: plan.interviewTime,
658
896
  interviewer: plan.interviewer,
897
+ ...(plan.isAIInterview
898
+ ? {
899
+ aiPlan: plan.aiPlan ? `${plan.aiPlan.name}(id=${plan.aiPlan.id})` : '(未指定)',
900
+ email: plan.email || '(缺失)',
901
+ mobile: plan.mobile || '(人才资料无手机号)',
902
+ }
903
+ : {}),
659
904
  notify: notify ? '将触发「新建日程」面试通知动作流(外部通知)' : '面试通知默认不触发',
660
905
  },
661
906
  willSubmit: { formId: plan.formId, objId: plan.objId, dataDetails: plan.dataDetails },
@@ -15,6 +15,8 @@ import { compact, normalizePaged, toNumber, toPageQuery, toPositiveInt } from '.
15
15
  // ─────────────────────────────────────────────────────────────────────────────
16
16
  const RECRUIT_APP_TAG = 'weaver-recruit-service';
17
17
  /** 逻辑名 -> objTag(权威映射,与 _init_env_cache.mjs 一致) */
18
+ /** 环境缓存结构版本:新增/变更解析字段(如 2026-11-04 补 candidate 应聘者列表)时 +1,旧缓存自动失效重建。 */
19
+ const RECRUIT_CACHE_VERSION = 1;
18
20
  const RECRUIT_TAGS = {
19
21
  talent_pool: 'uf_rcrt_talent_pool',
20
22
  interview_plan: 'uf_rcrt_interview_plan',
@@ -38,7 +40,8 @@ const LIST_KEYWORDS = {
38
40
  position: ['招聘职位', '职位列表', '职位管理'],
39
41
  entry: ['入职管理'],
40
42
  offer: ['录用管理', 'offer管理', 'offer'],
41
- interview: ['面试管理'],
43
+ interview: ['面试管理', '面试安排列表', '面试安排'],
44
+ candidate: ['应聘者', '候选人', '应聘批次', '应聘管理'],
42
45
  biz_config: ['业务配置映射表'],
43
46
  };
44
47
  /** 列表类型 -> 缓存键 + 表单逻辑名(candidate.list 四列表映射,同 recruit-list.mjs TYPES) */
@@ -47,6 +50,8 @@ export const LIST_TYPES = {
47
50
  position: { listKey: 'position', formType: 'position_manage', title: '招聘职位' },
48
51
  entry: { listKey: 'entry', formType: 'entry_manage', title: '入职管理' },
49
52
  offer: { listKey: 'offer', formType: 'offer', title: '录用管理' },
53
+ interview: { listKey: 'interview', formType: 'interview_plan', title: '面试管理' },
54
+ candidate: { listKey: 'candidate', formType: 'candidate_manage', title: '应聘者' },
50
55
  };
51
56
  /** 剔除与展示无关的内部键(同 recruit-list.mjs) */
52
57
  const INTERNAL_KEYS = new Set([
@@ -336,7 +341,7 @@ async function resolveRecruitEnv(host, appId) {
336
341
  }
337
342
  const lists = {
338
343
  talent: null, demand: null, position: null, entry: null, offer: null,
339
- interview: null, dict: dictMenu, biz_config: bizConfigMenu,
344
+ interview: null, candidate: null, dict: dictMenu, biz_config: bizConfigMenu,
340
345
  all_search: searchMenus, pages,
341
346
  };
342
347
  for (const [biz, keywords] of Object.entries(LIST_KEYWORDS)) {
@@ -384,7 +389,7 @@ export function createRecruitContextLoader() {
384
389
  }
385
390
  }
386
391
  catch { /* 缓存读取失败回退闸门链路 */ }
387
- if (diskCache) {
392
+ if (diskCache && diskCache.cacheVersion === RECRUIT_CACHE_VERSION) {
388
393
  memo.set(memoKey, diskCache);
389
394
  return diskCache;
390
395
  }
@@ -409,6 +414,7 @@ export function createRecruitContextLoader() {
409
414
  note: 'recruit-ops 环境参数缓存(weaver-work-cli 运行时解析产物,不含认证字段)',
410
415
  },
411
416
  envKey: `${dir.split(/[\\/]/).pop()}|${appId}`,
417
+ cacheVersion: RECRUIT_CACHE_VERSION,
412
418
  ...resolved,
413
419
  }, null, 2), 'utf8');
414
420
  }
@@ -627,7 +633,7 @@ const candidateList = {
627
633
  capability: 'recruit',
628
634
  risk: READ,
629
635
  inputSchema: schemaOf({
630
- type: { type: 'string', enum: ['demand', 'position', 'entry', 'offer'], description: '列表类型' },
636
+ type: { type: 'string', enum: ['demand', 'position', 'entry', 'offer', 'interview', 'candidate'], description: '列表类型' },
631
637
  current: SCHEMA_ID,
632
638
  pageSize: SCHEMA_ID,
633
639
  }, { required: ['type'] }),
@@ -244,3 +244,50 @@ export function assertEnumArray(value, allowed, field) {
244
244
  throw ehrValidationError('enum_invalid', `${field} 必须是数组`);
245
245
  return value.map((item) => assertEnum(item, allowed, field));
246
246
  }
247
+ /**
248
+ * EHR 页面路由表(**集中一处,禁止各 operation 自行拼写**)。
249
+ * 来源:weaver-interface/business-apis/ehr/references/attend-appeal-save.md、
250
+ * hr-all-module.md(2026-11-04 真实环境取证)。
251
+ * 🔴 未取证的路由不在此表、不得臆造;某环境打不开时以实际地址为准并反馈维护者。
252
+ */
253
+ export const EHR_PAGES = {
254
+ /** 考勤异常/申诉页面(创建的申诉在此可查) */
255
+ attendAbnormal: '/sp/attend/abnormal',
256
+ /** 在职员工名册(原通讯录) */
257
+ hrEmployeeManager: '/sp/hrm/hr/employeeManager',
258
+ /** 生日名单 */
259
+ hrBirthdayList: '/sp/hrm/hr/birthday/list',
260
+ /** 人员状态变更记录 */
261
+ hrPersonalStateChange: '/sp/hrm/hr/personalStateChange/record',
262
+ /** 时点员工名册 */
263
+ hrTpReportTp: '/sp/hrm/hr/tpReport/tp',
264
+ /** 历史曾在职名册 */
265
+ hrTpReportHw: '/sp/hrm/hr/tpReport/hw',
266
+ /** 离职员工名册 */
267
+ hrTpReportDismiss: '/sp/hrm/hr/tpReport/dismiss',
268
+ /** 人员流动分析表 */
269
+ hrTpReportTurn: '/sp/hrm/hr/tpReport/turn',
270
+ /** 基本状况分析 */
271
+ hrReportBasePerson: '/sp/hrm/hr/reportForm/basePerson',
272
+ /** 部门人数统计 */
273
+ hrReportDeptPerson: '/sp/hrm/hr/reportForm/deptPerson',
274
+ /** 岗位人数统计 */
275
+ hrReportPosiPerson: '/sp/hrm/hr/reportForm/posiPerson',
276
+ /** 职务状况统计 */
277
+ hrReportJobPerson: '/sp/hrm/hr/reportForm/jobPerson',
278
+ };
279
+ /** 拼接 EHR 页面地址(去尾斜杠;path 必须来自 EHR_PAGES)。 */
280
+ export function ehrPageUrl(baseUrl, path) {
281
+ return String(baseUrl || '').replace(/\/+$/, '') + path;
282
+ }
283
+ /** 人员详情卡片地址模板:替换 {人员id} 即为可点击地址(该接口无独立文档,查询到人员 id 后即附)。 */
284
+ export function hrProfileUrl(baseUrl, employeeId) {
285
+ return String(baseUrl || '').replace(/\/+$/, '') + '/sp/hrm/profileInfo/' + encodeURIComponent(String(employeeId));
286
+ }
287
+ /** 日期加减(YYYY-MM-DD,UTC 语义,避免时区跨天)。 */
288
+ export function addDays(date, delta) {
289
+ const [y, m, d] = String(date).split('-').map(Number);
290
+ const t = new Date(Date.UTC(y, m - 1, d + delta));
291
+ const f = (n) => String(n).padStart(2, '0');
292
+ return `${t.getUTCFullYear()}-${f(t.getUTCMonth() + 1)}-${f(t.getUTCDate())}`;
293
+ }
@@ -118,7 +118,125 @@ const orgDetail = {
118
118
  });
119
119
  },
120
120
  };
121
+ /**
122
+ * 组织**模糊查找**(部门 + 分部一次查完,按相似度排序)。
123
+ *
124
+ * 2026-11-04 用户反馈:说部门/分部时关键字常常不完整(「会议管理」vs「会议管理部门」),
125
+ * 模型只能一遍遍调 CLI 试,每次 2 秒。这个接口一次调用返回所有近似结果 + 完整路径,
126
+ * 让模型直接选,不再反复试错。
127
+ */
128
+ const orgLookup = {
129
+ name: 'hrm.org.lookup',
130
+ resource: 'org',
131
+ method: 'org.lookup',
132
+ action: 'hrm.org.lookup',
133
+ summary: '组织模糊查找:关键词**不用写全**,一次返回所有近似的部门/分部(含完整路径、按相似度排序)',
134
+ risk: READ,
135
+ inputSchema: schemaOf({
136
+ keyword: { type: 'string', minLength: 1, description: '部门/分部名称关键词(可只写一部分,如「会议管理」「研发」)' },
137
+ type: { type: 'string', enum: ['department', 'subcompany', 'both'], description: '默认 both(部门+分部都查)' },
138
+ limit: { type: 'number', minimum: 1, maximum: 50, description: '最多返回条数,默认 10' },
139
+ }, { required: ['keyword'] }),
140
+ handler: async (args, { host }) => {
141
+ const keyword = String(args.keyword ?? '').trim();
142
+ if (!keyword)
143
+ throw hrmValidationError('keyword_required', 'keyword 必填');
144
+ const scope = args.type === 'department' || args.type === 'subcompany' ? String(args.type) : 'both';
145
+ const limit = Math.max(1, Math.min(50, Number(args.limit) || 10));
146
+ const fetchOrg = async (type, text) => {
147
+ const body = {
148
+ ...browserBaseBody(),
149
+ virtualType: 'ALL',
150
+ cmd: 'list',
151
+ formDatas: { name: [{ nameCondition: { id: 'in', content: '包含', disabled: false }, name: text }] },
152
+ quickSearchDatas: {},
153
+ type: 'and',
154
+ stopDepartment: false,
155
+ current: 1,
156
+ pageSize: 200,
157
+ };
158
+ const path = type === 'subcompany'
159
+ ? '/api/hrm/common/browser/data/subcompany'
160
+ : '/api/hrm/common/browser/data/department';
161
+ const resp = await host.request({ method: 'POST', path, body });
162
+ const rows = (resp?.data?.data ?? []);
163
+ return rows.map((r) => ({
164
+ id: String(r.id ?? ''),
165
+ name: String(r.name ?? ''),
166
+ type,
167
+ fullPath: String(r.fullPath ?? r.path ?? ''),
168
+ ...(r.subcompanyName ? { subcompanyName: String(r.subcompanyName) } : {}),
169
+ ...(r.parentName ? { parentName: String(r.parentName) } : {}),
170
+ })).filter((r) => r.id);
171
+ };
172
+ const types = scope === 'both'
173
+ ? ['department', 'subcompany']
174
+ : [scope];
175
+ const collected = [];
176
+ let requests = 0;
177
+ const stripSuffix = (text) => text.replace(/(部门|事业部|中心|科室|组|部)$/u, '').trim();
178
+ for (const type of types) {
179
+ requests += 1;
180
+ collected.push(...await fetchOrg(type, keyword));
181
+ // 关键词带了「部门/中心/组/部」后缀但没命中 → 去掉后缀再查一次(用户常说全称,库里可能不带后缀)
182
+ const stripped = stripSuffix(keyword);
183
+ if (collected.length === 0 && stripped !== '' && stripped !== keyword) {
184
+ const raws = await fetchOrg(type, stripped);
185
+ // 二次命中把分数降一档(不是精确关键词命中)
186
+ collected.push(...raws.map((row) => ({ ...row, stripped: true })));
187
+ requests += 1;
188
+ }
189
+ // 仍然没有 → **逐字缩短**再试(「会议管理部门」库里叫「会议模块组」,只能靠「会议」命中)
190
+ if (collected.length === 0) {
191
+ for (let len = Math.min(keyword.length, 6) - 1; len >= 2; len -= 1) {
192
+ const short = keyword.slice(0, len);
193
+ const raws = await fetchOrg(type, short);
194
+ requests += 1;
195
+ if (raws.length > 0) {
196
+ collected.push(...raws.map((row) => ({ ...row, shortened: short })));
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ const scoreOf = (row) => {
203
+ const name = String(row.name);
204
+ const pathText = String(row.fullPath ?? '');
205
+ let score = 0;
206
+ if (name === keyword)
207
+ score = 100;
208
+ else if (name.startsWith(keyword))
209
+ score = 85;
210
+ else if (name.includes(keyword))
211
+ score = 70;
212
+ else if (pathText.includes(keyword))
213
+ score = 50;
214
+ else
215
+ score = 35; // 去后缀后命中
216
+ if (row.type === 'department')
217
+ score += 5; // 同分时部门优先(会议按部门找人的场景更多)
218
+ if (row.stripped)
219
+ score -= 5;
220
+ if (row.shortened)
221
+ score -= 15; // 逐字缩短命中的,可信度最低
222
+ return score;
223
+ };
224
+ const rows = collected
225
+ .map((row) => ({ ...row, score: scoreOf(row) }))
226
+ .sort((left, right) => Number(right.score) - Number(left.score)
227
+ || String(left.name).localeCompare(String(right.name), 'zh-Hans-CN'))
228
+ .slice(0, limit);
229
+ return hrmSuccess('hrm.org.lookup', {
230
+ rows,
231
+ total: rows.length,
232
+ ...(rows.length === 0
233
+ ? { hint: `没有名称含「${keyword}」的部门或分部。换更短的关键词(如两个字)再试,或用 hrm.org.query 按 parent/subcompany 逐级找。` }
234
+ : {}),
235
+ }, { requests, keyword });
236
+ },
237
+ };
121
238
  export const hrmOrgOperations = [
122
239
  orgQuery,
240
+ orgLookup,
123
241
  orgDetail,
124
242
  ];
@@ -35,6 +35,11 @@ export const builtinShortcuts = [
35
35
  description: '泛微E10聚才林(人事/出勤/薪酬)CLI',
36
36
  load: async () => (await import('./ehr/index.js')).ehrShortcut,
37
37
  }),
38
+ lazyShortcut({
39
+ name: 'recruit-stage',
40
+ description: '泛微E10招聘助手(JD生成/简历建档/AI面试邀约)CLI',
41
+ load: async () => (await import('./recruit-stage/index.js')).recruitStageShortcut,
42
+ }),
38
43
  lazyShortcut({
39
44
  name: 'cowork',
40
45
  description: '泛微E10协作区(协作事项/讨论/审批/共享权限)CLI',
@@ -5,6 +5,12 @@ const CONTINUATION_TTL_MS = 600_000;
5
5
  function b64(s) {
6
6
  return Buffer.from(s, 'utf8').toString('base64');
7
7
  }
8
+ function mailViewUrl(baseUrl, mailResourceId) {
9
+ if (!mailResourceId)
10
+ return null;
11
+ const base = String(baseUrl || '').replace(/\/+$/, '');
12
+ return `${base}/sp/mail/view?mailResourceId=${mailResourceId}`;
13
+ }
8
14
  function splitNames(s) {
9
15
  if (!s)
10
16
  return [];
@@ -466,9 +472,9 @@ const opSendApply = {
466
472
  const value = (await env.host.request({ path, method: 'POST', body }));
467
473
  const d = value?.data || {};
468
474
  if (draft) {
469
- return mailSuccess('mail.send.apply', { success: d.status === 1, draft: true, mailResourceId: d.mailResourceId, subject: lease.subject }, { savedDraft: true });
475
+ return mailSuccess('mail.send.apply', { success: d.status === 1, draft: true, mailResourceId: d.mailResourceId, subject: lease.subject, viewUrl: mailViewUrl(session.baseUrl, d.mailResourceId) }, { savedDraft: true });
470
476
  }
471
- return mailSuccess('mail.send.apply', { success: d.success === 1, msg: d.msg || '邮件已发送', subject: lease.subject }, { sent: true });
477
+ return mailSuccess('mail.send.apply', { success: d.success === 1, msg: d.msg || '邮件已发送', subject: lease.subject, mailResourceId: d.mailResourceId ?? null, viewUrl: mailViewUrl(session.baseUrl, d.mailResourceId) }, { sent: true });
472
478
  },
473
479
  };
474
480
  export const mailSendOperations = [opSendPrepare, opSendApply];
@@ -120,8 +120,12 @@ export const EB_ENDPOINTS = [
120
120
  queryParams: [
121
121
  'mt_id', 'creator', 'mt_name', 'mt_status', 'mt_type', 'caller', 'contacter',
122
122
  'enddatetime', 'begindatetime', 'address', 'hrmmember', 'attend_user', 'reporting_user',
123
- 'topic_presenter', 'request_id',
123
+ 'topic_presenter',
124
124
  ],
125
+ // 群 id 走 body `datajson.mainTable` 精确匹配(2026-11-02 实测:按它查回 1 条,准)。
126
+ // ⛔ `request_id` 不能当筛选条件:接口文档里它**不是入参**,URL query 传了被忽略,
127
+ // 出参是「按显示名称」(返回流程标题,不是单号)—— 想按单号找会议,只能取列表再逐条 detail 比对。
128
+ mainTableParams: ['imchat_groupid'],
125
129
  summary: 'EB 版会议列表(query_mtDetail_list + count):URL query 传筛选条件,body 带 datajson',
126
130
  notes: [
127
131
  'hrmmember 等人员参数必须传员工 id,不能传姓名文本。',
@@ -272,7 +276,10 @@ export const EB_ENDPOINTS = [
272
276
  interfacePk: 'query_roomusage_list',
273
277
  countPk: 'query_roomusage_count',
274
278
  queryParams: ['mt_id', 'address', 'enddatetime', 'begindatetime'],
275
- summary: 'EB 版会议室占用列表(query_roomusage_list + count)',
279
+ dateFilter: true,
280
+ paramAliases: { roomId: 'address', room_id: 'address', day: 'date' },
281
+ summary: 'EB 版会议室占用列表(query_roomusage_list + count)。按日期过滤传 date(yyyy-MM-dd)'
282
+ + '或 begindatetime/enddatetime;**不传任何时间条件 = 返回全量历史**,不是接口不支持过滤',
276
283
  },
277
284
  {
278
285
  domain: 'roomUsage',