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
@@ -335,45 +335,122 @@ async function uploadResumeFile(env, filePath, fieldId, formId) {
335
335
  }
336
336
  return { fileid: String(fileid), name, size };
337
337
  }
338
- /** 平台标签:按名称在本环境标签清单里解析 id(同名优先公共),不自动创建。 */
339
- async function applyPlatformTags(env, talentId, tagNames) {
340
- if (!tagNames.length)
341
- return { applied: [], skipped: [] };
342
- const list = (await env.host.request({
338
+ /** 按名查标签(并行 type=0 公共 / type=1 个人)。服务端若忽略 name 会返回满页 → filtered=false,由调用方回退全量。
339
+ * 🔴 2026-11-04 修:原实现只取第 1 页 500 条,环境标签超 500 时后续标签永远匹配不到(静默跳过)。 */
340
+ async function queryTagByName(env, name) {
341
+ const pageSize = 100;
342
+ const bodyOf = (type) => ({ type, name, order: 8, pageSize, pageNo: 1, search: 1, tagGroup: 0, tagModules: ['ebuilderform'] });
343
+ const rs = await Promise.all([0, 1].map((type) => env.host
344
+ .request({
343
345
  path: '/api/my/tag/query',
344
346
  method: 'POST',
345
- body: { tagModules: ['ebuilderform'], current: 1, pageSize: 500 },
347
+ body: bodyOf(type),
346
348
  operation: 'ehr.recruit.candidate.import.apply',
347
- }));
348
- const rows = list?.data?.data || list?.data?.list || [];
349
+ })
350
+ .catch(() => null)));
351
+ const items = [];
352
+ let filtered = true;
353
+ for (const r of rs) {
354
+ const d = r?.data || {};
355
+ const list = Array.isArray(d.data) ? d.data : [];
356
+ const total = Number(d.total || 0);
357
+ if (list.length >= pageSize && total >= pageSize)
358
+ filtered = false;
359
+ items.push(...list);
360
+ }
361
+ return { items, filtered };
362
+ }
363
+ /** 单类型的全量标签(首轮取 total 后并发翻页,限并发 5、上限 40 页)——仅在按名查询不可用时回退 */
364
+ async function fetchTagsOfType(env, type) {
365
+ const pageSize = 100;
366
+ const bodyOf = (pageNo) => ({ type, name: '', order: 8, pageSize, pageNo, search: 1, tagGroup: 0, tagModules: ['ebuilderform'] });
367
+ const requestPage = (pageNo) => env.host
368
+ .request({
369
+ path: '/api/my/tag/query',
370
+ method: 'POST',
371
+ body: bodyOf(pageNo),
372
+ operation: 'ehr.recruit.candidate.import.apply',
373
+ })
374
+ .catch(() => null);
375
+ const first = (await requestPage(1));
376
+ const d = first?.data || {};
377
+ const rows = Array.isArray(d.data) ? [...d.data] : [];
378
+ const total = Number(d.total || 0);
379
+ const pages = Math.min(40, Math.max(1, Math.ceil(total / pageSize)));
380
+ if (pages > 1 && rows.length) {
381
+ const rest = Array.from({ length: pages - 1 }, (_, i) => i + 2);
382
+ for (let i = 0; i < rest.length; i += 5) {
383
+ const batch = rest.slice(i, i + 5);
384
+ const rs = await Promise.all(batch.map((p) => requestPage(p)));
385
+ for (const r of rs) {
386
+ const list = r?.data?.data;
387
+ if (Array.isArray(list))
388
+ rows.push(...list);
389
+ }
390
+ }
391
+ }
392
+ return rows;
393
+ }
394
+ /** 解析标签名 → id(同名优先公共),不创建标签。
395
+ * ① 按名精确查询(N 个名字 → 1 轮请求);② 服务端未按名过滤时才回退全量(不落盘、不缓存)。 */
396
+ async function resolveTagIds(env, tagNames) {
397
+ const norm = (s) => String(s == null ? '' : s).trim().toLowerCase();
398
+ const uniqNames = [...new Set(tagNames.map((n) => String(n)).filter(Boolean))];
349
399
  const byName = new Map();
350
- for (const row of rows) {
351
- const name = String(row.name ?? row.tagName ?? '').trim();
352
- const id = row.tagId ?? row.id;
353
- if (name && id !== undefined && !byName.has(name))
354
- byName.set(name, { id: String(id), name });
400
+ const absorb = (row) => {
401
+ const name = String(row?.name ?? row?.tagName ?? '').trim();
402
+ const id = row?.tagId ?? row?.id;
403
+ if (!name || id === undefined)
404
+ return;
405
+ const key = norm(name);
406
+ const prev = byName.get(key);
407
+ if (!prev)
408
+ byName.set(key, { id: String(id), name });
409
+ else if ((row?.type ?? 0) === 0)
410
+ byName.set(key, { id: String(id), name });
411
+ };
412
+ const probes = await Promise.all(uniqNames.map((n) => queryTagByName(env, n)));
413
+ const nameQueryUsable = probes.every((p) => p.filtered);
414
+ if (nameQueryUsable) {
415
+ for (const p of probes)
416
+ for (const item of p.items)
417
+ absorb(item);
355
418
  }
356
- const tags = [];
357
- const applied = [];
419
+ else {
420
+ for (const type of [0, 1]) {
421
+ const rows = await fetchTagsOfType(env, type).catch(() => []);
422
+ for (const item of rows)
423
+ absorb(item);
424
+ }
425
+ }
426
+ const resolved = [];
427
+ const found = [];
358
428
  const skipped = [];
359
429
  for (const name of tagNames) {
360
- const hit = byName.get(name);
430
+ const hit = byName.get(norm(name));
361
431
  if (!hit) {
362
432
  skipped.push(name);
363
433
  continue;
364
434
  }
365
- tags.push({ id: hit.id, name: hit.name, type: 0 });
366
- applied.push(name);
435
+ resolved.push({ id: hit.id, name: hit.name, type: 0 });
436
+ found.push(name);
367
437
  }
368
- if (tags.length) {
438
+ return { resolved, found, skipped };
439
+ }
440
+ /** 写入平台标签(解析 + batchAddEntityTag)。解析结果不存在时静默跳过,不自动创建标签。 */
441
+ async function applyPlatformTags(env, talentId, tagNames) {
442
+ if (!tagNames.length)
443
+ return { applied: [], skipped: [] };
444
+ const r = await resolveTagIds(env, tagNames);
445
+ if (r.resolved.length) {
369
446
  await env.host.request({
370
447
  path: '/api/my/tag/common/batchAddEntityTag',
371
448
  method: 'POST',
372
- body: { tags, batchDatas: [{ entityId: talentId, module: 'ebuilderform' }] },
449
+ body: { tags: r.resolved, batchDatas: [{ entityId: talentId, module: 'ebuilderform' }] },
373
450
  operation: 'ehr.recruit.candidate.import.apply',
374
451
  });
375
452
  }
376
- return { applied, skipped };
453
+ return { applied: r.found, skipped: r.skipped };
377
454
  }
378
455
  async function requireCache(env) {
379
456
  if (!env.recruitContext)
@@ -582,7 +659,16 @@ const candidateImportPrepare = {
582
659
  handler: async (args, env) => {
583
660
  const cache = await requireCache(env);
584
661
  const plan = await buildImportPlan(env, cache, args);
585
- const dup = await checkDuplicateByMobile(env, cache, plan.mobile);
662
+ // 标签预检(只读)+ 查重并行:让用户在确认摘要里就能看到哪些标签在本环境不存在(apply 会跳过它们)
663
+ const tagNames = Array.isArray(args.tags) ? args.tags.map(String).filter(Boolean) : [];
664
+ const [dup, tagCheck] = await Promise.all([
665
+ checkDuplicateByMobile(env, cache, plan.mobile),
666
+ tagNames.length
667
+ ? resolveTagIds(env, tagNames)
668
+ .then((rt) => ({ requested: tagNames, found: rt.found, missing: rt.skipped }))
669
+ .catch((error) => ({ requested: tagNames, missing: [], error: error?.message || '标签预检失败' }))
670
+ : Promise.resolve(null),
671
+ ]);
586
672
  if (dup.length > 1) {
587
673
  throw ehrValidationError('duplicate_ambiguous', `手机号 ${plan.mobile} 存在多条候选(数据异常),请人工核对后再决定,不自动新建`);
588
674
  }
@@ -621,6 +707,7 @@ const candidateImportPrepare = {
621
707
  details: plan.details,
622
708
  tags: Array.isArray(args.tags) ? args.tags : [],
623
709
  },
710
+ ...(tagCheck ? { tagCheck } : {}),
624
711
  continuation,
625
712
  expiresInSeconds: 600,
626
713
  });
@@ -684,43 +771,31 @@ const candidateImportApply = {
684
771
  throw error;
685
772
  throw ehrPartialError(`简历建档写入结果不确定(${error?.message || '未知错误'}):请先用人才库搜索手机号 ${payload.telephone} 核对是否已建档,禁止直接重试`, { candidateName: payload.candidateName, telephone: payload.telephone });
686
773
  }
687
- // ③ 初始化人才批次(失败不回滚人才,仅记 warning)
688
- let batchOk = null;
689
- let batchNote;
690
- try {
691
- await env.host.request({
692
- path: `/api/recruit/candidate/initTalentBatch?formDataId=${encodeURIComponent(talentId)}`,
693
- method: 'POST',
694
- operation: 'ehr.recruit.candidate.import.apply',
695
- });
696
- batchOk = true;
697
- }
698
- catch (error) {
699
- batchOk = false;
700
- batchNote = `人才已建档,但初始化人才批次失败:${error?.message || '未知错误'}(可在人才卡片手动处理)`;
701
- }
702
- // ④ 触发后台智能标签规则(规则未配置时不产生标签,属正常)
703
- let labelMatch = null;
704
- try {
705
- const r = (await env.host.request({
706
- path: `/api/recruit/label/matchTalentLabel?talentId=${encodeURIComponent(talentId)}`,
707
- method: 'POST',
708
- body: {},
709
- operation: 'ehr.recruit.candidate.import.apply',
710
- }));
711
- labelMatch = r?.data === undefined ? 'TRIGGERED' : String(r.data);
712
- }
713
- catch {
714
- labelMatch = 'FAILED(不影响建档,可稍后在人才卡片手动触发)';
715
- }
716
- // ⑤ 平台标签(按名命中清单写入,不存在则跳过)
717
- let tagResult = { applied: [], skipped: [] };
718
- try {
719
- tagResult = await applyPlatformTags(env, talentId, Array.isArray(payload.tags) ? payload.tags.map(String) : []);
720
- }
721
- catch (error) {
722
- tagResult = { applied: [], skipped: Array.isArray(payload.tags) ? payload.tags.map(String) : [] };
723
- }
774
+ // ③④⑤ 初始化批次 / 触发电商后台标签规则 / 平台标签:三者只依赖 talentId,并行执行(各自 catch 等价原语义)
775
+ const batchTask = env.host
776
+ .request({
777
+ path: `/api/recruit/candidate/initTalentBatch?formDataId=${encodeURIComponent(talentId)}`,
778
+ method: 'POST',
779
+ operation: 'ehr.recruit.candidate.import.apply',
780
+ })
781
+ .then(() => ({ ok: true, note: undefined }))
782
+ .catch((error) => ({
783
+ ok: false,
784
+ note: `人才已建档,但初始化人才批次失败:${error?.message || '未知错误'}(可在人才卡片手动处理)`,
785
+ }));
786
+ const labelTask = env.host
787
+ .request({
788
+ path: `/api/recruit/label/matchTalentLabel?talentId=${encodeURIComponent(talentId)}`,
789
+ method: 'POST',
790
+ body: {},
791
+ operation: 'ehr.recruit.candidate.import.apply',
792
+ })
793
+ .then((r) => (r?.data === undefined ? 'TRIGGERED' : String(r.data)))
794
+ .catch(() => 'FAILED(不影响建档,可稍后在人才卡片手动触发)');
795
+ const tagTask = applyPlatformTags(env, talentId, Array.isArray(payload.tags) ? payload.tags.map(String) : []).catch(() => ({ applied: [], skipped: Array.isArray(payload.tags) ? payload.tags.map(String) : [] }));
796
+ const [batchOutcome, labelMatch, tagResult] = await Promise.all([batchTask, labelTask, tagTask]);
797
+ const batchOk = batchOutcome.ok;
798
+ const batchNote = batchOutcome.note;
724
799
  // ⑥ 产物链接:人才卡片单页 + 人才库列表定位(自用系统追加 recruitType=useSelf)
725
800
  const baseUrl = session.baseUrl.replace(/\/+$/, '');
726
801
  const extra = String(cache.selfUse) === '1' ? '&recruitType=useSelf' : '';
@@ -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'] }),