weaver-work-cli 0.1.32 → 0.1.36

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 (211) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-UMGX72K6.js → archive-XBVH7JU7.js} +2 -2
  3. package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-MZM3EKFQ.js} +2 -2
  4. package/dist/chunks/{asset-GLEGKFG5.js → asset-4XO7UOUW.js} +2 -2
  5. package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-VXSWANAC.js} +3 -2
  6. package/dist/chunks/{blog-E44OTVDA.js → blog-PGZKOPSC.js} +2 -2
  7. package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-BH4ZVGUN.js} +2 -2
  8. package/dist/chunks/{calendar-JKPIRKFD.js → calendar-MXEMTIO4.js} +2 -2
  9. package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
  10. package/dist/chunks/chunk-3SO6QL6U.js +73 -0
  11. package/dist/chunks/chunk-5DQEEGCC.js +229 -0
  12. package/dist/chunks/{chunk-G3QJULO2.js → chunk-6LLJFKJE.js} +1 -1
  13. package/dist/chunks/{chunk-C6R3B2ML.js → chunk-BPE37FPB.js} +35 -7
  14. package/dist/chunks/chunk-BYATLLEF.js +68 -0
  15. package/dist/chunks/chunk-BYBSCN4X.js +932 -0
  16. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-F746NKNF.js} +1 -1
  17. package/dist/chunks/{chunk-GCG2NMWR.js → chunk-HXJQI5NM.js} +4 -2
  18. package/dist/chunks/{chunk-YRVDW4WN.js → chunk-I6QGIBDM.js} +2 -2
  19. package/dist/chunks/{chunk-H7J4DC5R.js → chunk-JCSN3JGM.js} +2 -2
  20. package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-JNG42JKG.js} +30 -1
  21. package/dist/chunks/{chunk-DW6AUUKS.js → chunk-JUXVYKII.js} +9 -4
  22. package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-JYKABYCV.js} +2 -2
  23. package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-NCOVUAZK.js} +2 -2
  24. package/dist/chunks/{chunk-E4Z23GHM.js → chunk-QHFANTX7.js} +1114 -904
  25. package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-QYGO5EE2.js} +22 -1148
  26. package/dist/chunks/{chunk-IURUFGZK.js → chunk-T72N74ZT.js} +50 -33
  27. package/dist/chunks/{chunk-H6XSTSLM.js → chunk-TC6MZAO3.js} +137 -204
  28. package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-VPUTVEMC.js} +737 -341
  29. package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-VPXOPQJI.js} +2 -2
  30. package/dist/chunks/chunk-WXFPWCXG.js +774 -0
  31. package/dist/chunks/{chunk-4YC43QQ4.js → chunk-XMGSYD6S.js} +4 -4
  32. package/dist/chunks/conflict-JAB2LJEZ.js +21 -0
  33. package/dist/chunks/continuation-RN6PZN2M.js +11 -0
  34. package/dist/chunks/contract-XI2EEIZU.js +9110 -0
  35. package/dist/chunks/cowork-SCPGLBA4.js +2527 -0
  36. package/dist/chunks/ebuilder-form-EYLHNOTA.js +121 -0
  37. package/dist/chunks/{ehr-HMFY6VIL.js → ehr-EAP77IMT.js} +1344 -27
  38. package/dist/chunks/{esb-ZYSVB7SG.js → esb-2TGGXEE7.js} +4 -4
  39. package/dist/chunks/{fna-B7RKPAZD.js → fna-2X7ZE7BU.js} +13 -8
  40. package/dist/chunks/{host-OCH2BCCV.js → host-C6EL2RKL.js} +3 -3
  41. package/dist/chunks/{host-45WED2KK.js → host-I4NRK4MX.js} +3 -3
  42. package/dist/chunks/{host-I4Z6BFYX.js → host-Q4LZGCRU.js} +3 -3
  43. package/dist/chunks/host-UEI5FGDM.js +23 -0
  44. package/dist/chunks/{host-KNZ52LJZ.js → host-XCLDDXKQ.js} +3 -3
  45. package/dist/chunks/{hrm-2KAZGUHI.js → hrm-YXERJM76.js} +3 -3
  46. package/dist/chunks/{im-ACJJWH4U.js → im-A4VZN2TQ.js} +88 -12
  47. package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-2Z6YPMIS.js} +3 -3
  48. package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-AR5GMKZB.js} +2 -2
  49. package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-QXW7P4X3.js} +4 -4
  50. package/dist/chunks/{mail-S2HDDXO2.js → mail-7GZ6CPBJ.js} +45 -24
  51. package/dist/chunks/{meeting-QVN3I37L.js → meeting-3NMGA3MB.js} +278 -43
  52. package/dist/chunks/{okr-QNG4VC2Q.js → okr-BZ6XUOAG.js} +2 -2
  53. package/dist/chunks/operations-5QL3AECO.js +11 -0
  54. package/dist/chunks/{plan-A25EMW6U.js → plan-DR5CMUYP.js} +2 -2
  55. package/dist/chunks/{project-G6XV66DZ.js → project-6J63MK33.js} +2 -2
  56. package/dist/chunks/{registry-DC6KR3NH.js → registry-KDD7DHNK.js} +4 -4
  57. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  58. package/dist/chunks/{registry-7C5UMIPL.js → registry-TKX2E7C7.js} +2 -2
  59. package/dist/chunks/{registry-TI3CIUXF.js → registry-VRTNC6UR.js} +8 -6
  60. package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-XCWRMB5P.js} +4 -3
  61. package/dist/chunks/{schedule-KFIITGSK.js → schedule-IVBICP32.js} +4 -3
  62. package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-D74NFK3C.js} +179 -6
  63. package/dist/index.js +35 -25
  64. package/dist/internal/e10/auth-recovery.js +14 -4
  65. package/dist/internal/e10/fetch-timing.js +31 -0
  66. package/dist/internal/e10/request-runtime.js +5 -0
  67. package/dist/shortcuts/contract/host.js +34 -59
  68. package/dist/shortcuts/cowork/continuation.js +70 -0
  69. package/dist/shortcuts/cowork/errors.js +59 -0
  70. package/dist/shortcuts/cowork/host.js +180 -0
  71. package/dist/shortcuts/cowork/index.js +166 -0
  72. package/dist/shortcuts/cowork/manifest.js +65 -0
  73. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  74. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  75. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  76. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  77. package/dist/shortcuts/cowork/operations/read.js +423 -0
  78. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  79. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  80. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  81. package/dist/shortcuts/cowork/operations/types.js +39 -0
  82. package/dist/shortcuts/cowork/operations.js +19 -0
  83. package/dist/shortcuts/ehr/host.js +4 -1
  84. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  85. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  86. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  87. package/dist/shortcuts/ehr/operations/recruit-write.js +43 -16
  88. package/dist/shortcuts/ehr/operations/recruit.js +182 -10
  89. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  90. package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
  91. package/dist/shortcuts/im/diagnostics.js +97 -0
  92. package/dist/shortcuts/im/host.js +58 -31
  93. package/dist/shortcuts/im/index.js +7 -2
  94. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  95. package/dist/shortcuts/im/operations/file-user.js +10 -2
  96. package/dist/shortcuts/im/operations/group-read.js +26 -5
  97. package/dist/shortcuts/im/operations/group-write.js +35 -28
  98. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  99. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  100. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  101. package/dist/shortcuts/im/operations/session.js +1 -1
  102. package/dist/shortcuts/im/operations/shared.js +10 -0
  103. package/dist/shortcuts/im/operations/types.js +13 -0
  104. package/dist/shortcuts/index.js +10 -0
  105. package/dist/shortcuts/mail/host.js +12 -1
  106. package/dist/shortcuts/mail/operations/types.js +17 -2
  107. package/dist/shortcuts/meeting/host.js +27 -34
  108. package/dist/shortcuts/meeting/index.js +79 -1
  109. package/dist/shortcuts/meeting/manifest.js +10 -0
  110. package/dist/shortcuts/meeting/operations/create.js +4 -1
  111. package/dist/shortcuts/meeting/operations/eb/catalog.js +10 -0
  112. package/dist/shortcuts/meeting/operations/eb/conflict.js +84 -8
  113. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1195 -140
  114. package/dist/shortcuts/meeting/operations/eb/factory.js +170 -8
  115. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  116. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +50 -5
  117. package/dist/shortcuts/meeting/operations/eb-fallback.js +163 -0
  118. package/dist/shortcuts/meeting/operations/env.js +99 -46
  119. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  120. package/dist/shortcuts/meeting/operations/invitation.js +79 -20
  121. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  122. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  123. package/dist/shortcuts/meeting/operations.js +16 -0
  124. package/dist/shortcuts/workflow/endpoints.js +8 -3
  125. package/dist/shortcuts/workflow/index.js +110 -1
  126. package/dist/shortcuts/workflow/operations/form.js +204 -36
  127. package/dist/shortcuts/workflow/operations/read.js +104 -23
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  138. package/dist/shortcuts/workflow/operations.js +23 -1
  139. package/docs/e10-auth.md +1 -1
  140. package/docs/ehr.md +1 -1
  141. package/docs/fna.md +2 -1
  142. package/docs/im.md +8 -1
  143. package/docs/todo-smart-convert/README.md +1 -1
  144. package/docs/todo-smart-convert/data-contract.md +2 -2
  145. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  146. package/package.json +1 -1
  147. package/skill-template/business-info.json +18 -0
  148. package/skill-template/domains/cowork.md +14 -0
  149. package/skill-template/domains/yimiaoban.md +3 -3
  150. package/skill-template/skill-template.md +1 -1
  151. package/skills/weaver-e10-blog/SKILL.md +5 -5
  152. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  153. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  154. package/skills/weaver-e10-cowork/SKILL.md +101 -0
  155. package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
  156. package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
  157. package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
  158. package/skills/weaver-e10-esb/SKILL.md +5 -5
  159. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  160. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  161. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  162. package/skills/weaver-e10-jucailin/SKILL.md +38 -14
  163. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  164. package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
  165. package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
  166. package/skills/weaver-e10-jucailin/references/salary.md +15 -11
  167. package/skills/weaver-e10-mail/SKILL.md +17 -14
  168. package/skills/weaver-e10-meeting/SKILL.md +442 -46
  169. package/skills/weaver-e10-meeting/references/meeting-eb.md +135 -3
  170. package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
  171. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  172. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  173. package/skills/weaver-e10-meeting.zip +0 -0
  174. package/skills/weaver-e10-okr/SKILL.md +5 -5
  175. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  176. package/skills/weaver-e10-plan/SKILL.md +5 -5
  177. package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
  178. package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
  179. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
  180. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
  181. package/skills/weaver-e10-shared/SKILL.md +12 -12
  182. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  183. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  184. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  185. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  186. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  187. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  188. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  189. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  190. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  191. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  192. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  193. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  194. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  195. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  196. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  197. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  198. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  199. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  200. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  201. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  202. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  203. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  204. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  205. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  206. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  207. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  208. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  209. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  210. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  211. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -0,0 +1,757 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { basename } from 'node:path';
4
+ import { ehrConfirmationError, ehrPartialError, ehrValidationError } from '../errors.js';
5
+ import { WRITE, ehrSuccess, schemaOf, } from './types.js';
6
+ import { checkDuplicateByMobile } from './recruit-write.js';
7
+ /**
8
+ * 简历导入建人才:主表 + 6 张明细 + 原件附件 + 人才批次 + 平台标签 + 双链接。
9
+ *
10
+ * 口径来源:ehr 技能 recruit-resume-import.mjs(真机验证)。关键实测事实:
11
+ * - **明细只能走数据集接口** `POST /api/ebuilder/form/dataset/v2/saveFormData`;
12
+ * `form/core/saveFormData` 的 dataDetails 不接收 DetailTable(9 种结构全被静默忽略)。
13
+ * - 明细节点名 `detail1..detailN` 由 `GET /api/ebuilder/form/dataset/v1/getTableMapping?objId=`
14
+ * 运行时解析,**顺序 ≠ 布局顺序**,子表 → 节点靠 subFormId 反查。
15
+ * - 提交键必须是 **config.dataKey**(用 name 会被静默忽略,响应仍成功)。
16
+ * - 业务失败信息在 `data.datajson.message`(外层 message.msg 恒为 success)。
17
+ * - 长文本(自我评价/职业技能)必须用 dataText,用 content 会被丢弃。
18
+ * 解析由大模型完成(AI 按 schema 整理结构化 JSON);CLI 只做确定性映射与落库,不调 OCR。
19
+ */
20
+ /** 明细表 dataKey → 入参键 → 列取值(列不存在则跳过,绝不臆造)。 */
21
+ const SUB_TABLES = [
22
+ {
23
+ table: 'rcrt_education_exp',
24
+ input: 'academicInformations',
25
+ cols: {
26
+ school: (r) => r.school,
27
+ edu_major: (r) => r.edu_major ?? r.major,
28
+ edu_education: (r) => r.edu_education ?? r.education,
29
+ degree: (r) => r.degree,
30
+ study_date: (r) => r.study_date ?? joinRange(r.start_date, r.end_date),
31
+ faculty: (r) => r.faculty,
32
+ research_direction: (r) => r.research_direction,
33
+ major_courses: (r) => r.major_courses,
34
+ },
35
+ },
36
+ {
37
+ table: 'rcrt_work_exp',
38
+ input: 'jobInformations',
39
+ cols: {
40
+ co_name: (r) => r.co_name ?? r.company,
41
+ work_position: (r) => r.work_position ?? r.position,
42
+ work_dept: (r) => r.work_dept ?? r.department,
43
+ work_desc: (r) => r.work_desc ?? r.job_description,
44
+ resign_cause: (r) => r.resign_cause,
45
+ start_end_date: (r) => r.start_end_date ?? joinRange(r.start_date, r.end_date) ?? r.work_date,
46
+ work_type: (r) => r.work_type,
47
+ },
48
+ },
49
+ {
50
+ table: 'rcrt_sxjl',
51
+ input: 'internshipExperiences',
52
+ cols: {
53
+ sx_co_name: (r) => r.sx_co_name ?? r.company,
54
+ position_job: (r) => r.position_job ?? r.position,
55
+ job_description: (r) => r.job_description,
56
+ entry_time: (r) => r.entry_time,
57
+ sx_work_type: (r) => r.sx_work_type,
58
+ },
59
+ },
60
+ {
61
+ table: 'rcrt_project_exp',
62
+ input: 'projectExperiences',
63
+ cols: {
64
+ pj_name: (r) => r.pj_name ?? r.project_name,
65
+ pj_role: (r) => r.pj_role,
66
+ pj_duty: (r) => r.pj_duty,
67
+ pj_introduction: (r) => r.pj_introduction ?? r.project_description,
68
+ pj_date: (r) => r.pj_date ?? joinRange(r.start_date, r.end_date),
69
+ },
70
+ },
71
+ {
72
+ table: 'rcrt_award_exp',
73
+ input: 'awardExperiences',
74
+ cols: {
75
+ award_name: (r) => r.award_name,
76
+ awarded: (r) => r.awarded,
77
+ credentials: (r) => r.credentials,
78
+ award_date: (r) => r.award_date,
79
+ },
80
+ },
81
+ {
82
+ table: 'rcrt_language_skill',
83
+ input: 'languageAbilities',
84
+ cols: {
85
+ language_type: (r) => r.language_type,
86
+ mastery: (r) => r.mastery,
87
+ ls_ability: (r) => r.ls_ability,
88
+ literacy: (r) => r.literacy,
89
+ },
90
+ },
91
+ ];
92
+ /** 明确给字典/选择类的列:值需按数据字典名称或 id 反查后写入 optionId。 */
93
+ const DICT_COLS = new Set([
94
+ 'edu_education', 'degree', 'work_type', 'sx_work_type', 'mastery', 'ls_ability', 'literacy',
95
+ ]);
96
+ function joinRange(start, end) {
97
+ const s = start === undefined || start === null ? '' : String(start).trim();
98
+ const e = end === undefined || end === null ? '' : String(end).trim();
99
+ if (!s && !e)
100
+ return undefined;
101
+ return `${s},${e}`;
102
+ }
103
+ async function resolveSubTableMeta(env, formId) {
104
+ let data = null;
105
+ try {
106
+ const r = (await env.host.request({
107
+ path: `/api/ebuilder/form/form/core/getFormLayout?formId=${encodeURIComponent(formId)}`,
108
+ method: 'GET',
109
+ operation: 'ehr.recruit.candidate.import.prepare',
110
+ }));
111
+ data = r?.data;
112
+ }
113
+ catch {
114
+ // 低基线 GET 返回 405 → 回退 POST(ehr 技能实测口径)
115
+ }
116
+ if (!data) {
117
+ const r = (await env.host.request({
118
+ path: '/api/ebuilder/form/form/core/getFormLayout',
119
+ method: 'POST',
120
+ body: { module: 'ebuilderform', formId, terminalType: 'PC' },
121
+ operation: 'ehr.recruit.candidate.import.prepare',
122
+ }));
123
+ data = r?.data;
124
+ }
125
+ const comps = data?.formLayout?.pageLayouts?.[0]?.comps || [];
126
+ const out = {};
127
+ for (const comp of comps) {
128
+ const cfg = comp?.config || {};
129
+ if (comp?.type !== 'DetailTable' || !cfg.dataKey || !cfg.subFormId)
130
+ continue;
131
+ out[String(cfg.dataKey)] = {
132
+ dataKey: String(cfg.dataKey),
133
+ subFormId: String(cfg.subFormId),
134
+ title: String(cfg.title || ''),
135
+ columns: {},
136
+ };
137
+ }
138
+ for (const comp of comps) {
139
+ const cfg = comp?.config || {};
140
+ const subFormId = String(cfg.subFormId || '');
141
+ if (!subFormId || !cfg.dataKey || !cfg.fieldId)
142
+ continue;
143
+ const target = Object.values(out).find((t) => t.subFormId === subFormId);
144
+ if (!target)
145
+ continue;
146
+ const column = {
147
+ id: String(cfg.fieldId),
148
+ type: String(comp?.type || ''),
149
+ title: String(cfg.title || ''),
150
+ };
151
+ if (cfg.start?.dataKey && cfg.end?.dataKey) {
152
+ column.startKey = String(cfg.start.dataKey);
153
+ column.endKey = String(cfg.end.dataKey);
154
+ }
155
+ target.columns[String(cfg.dataKey)] = column;
156
+ }
157
+ return out;
158
+ }
159
+ /** 字典名称/ID → optionId(唯一命中才写;歧义或缺失跳过并记 warning,绝不写脏值)。 */
160
+ function makeDictResolver(cache, warnings) {
161
+ const byId = new Map();
162
+ for (const row of cache.dict?.raw || []) {
163
+ if (row.option_id && row.selection_name)
164
+ byId.set(String(row.option_id), String(row.selection_name));
165
+ }
166
+ return (value, label) => {
167
+ const raw = value === undefined || value === null ? '' : String(value).trim();
168
+ if (!raw)
169
+ return '';
170
+ // 已是字典 id(长数字)→ 必须能在字典里找到对应名称才采用
171
+ if (/^\d{6,}$/.test(raw)) {
172
+ if (byId.has(raw))
173
+ return raw;
174
+ warnings.push(`${label}:字典 id ${raw} 未在本环境字典中,已跳过`);
175
+ return '';
176
+ }
177
+ const ids = [...new Set((cache.dict?.by_name?.[raw] || []).map(String))];
178
+ if (ids.length === 1)
179
+ return ids[0];
180
+ warnings.push(`${label}「${raw}」${ids.length === 0 ? '在字典中不存在' : '命中多个同名选项(歧义)'},已跳过`);
181
+ return '';
182
+ };
183
+ }
184
+ function buildSubDetails(structured, meta, nodeOfTable, resolveDict) {
185
+ const details = {};
186
+ const stats = {};
187
+ const nodes = {};
188
+ const warnings = [];
189
+ for (const spec of SUB_TABLES) {
190
+ const sub = meta[spec.table];
191
+ if (!sub) {
192
+ stats[spec.table] = 0;
193
+ continue;
194
+ }
195
+ const rows = Array.isArray(structured[spec.input]) ? structured[spec.input] : [];
196
+ const built = [];
197
+ for (const row of rows) {
198
+ if (!row || typeof row !== 'object')
199
+ continue;
200
+ const out = {};
201
+ for (const [colKey, pick] of Object.entries(spec.cols)) {
202
+ const column = sub.columns[colKey];
203
+ if (!column)
204
+ continue;
205
+ let value = pick(row);
206
+ if (value === undefined || value === null || String(value).trim() === '')
207
+ continue;
208
+ if (DICT_COLS.has(colKey)) {
209
+ value = resolveDict(value, `${sub.title}.${column.title || colKey}`);
210
+ if (!value)
211
+ continue;
212
+ }
213
+ // 日期区间:一个组件对应两个物理列,必须拆成 startKey / endKey 两键提交
214
+ if (column.startKey && column.endKey) {
215
+ const parts = Array.isArray(value) ? value : String(value).split(/[,~]/);
216
+ const start = String(parts[0] ?? '').trim();
217
+ const end = String(parts[1] ?? '').trim();
218
+ if (!start || !end)
219
+ continue;
220
+ out[column.startKey] = start;
221
+ out[column.endKey] = end;
222
+ continue;
223
+ }
224
+ out[colKey] = String(value);
225
+ }
226
+ if (Object.keys(out).length > 0)
227
+ built.push(out);
228
+ }
229
+ if (built.length === 0) {
230
+ stats[spec.table] = 0;
231
+ continue;
232
+ }
233
+ const node = nodeOfTable[sub.subFormId];
234
+ if (!node) {
235
+ warnings.push(`子表「${sub.title || spec.table}」在 getTableMapping 中无对应节点,本次不写入(${built.length} 行)`);
236
+ stats[spec.table] = 0;
237
+ continue;
238
+ }
239
+ details[node] = built;
240
+ nodes[spec.table] = node;
241
+ stats[spec.table] = built.length;
242
+ }
243
+ return { details, stats, nodes, warnings };
244
+ }
245
+ async function datasetSaveFormData(env, objId, mainTable, details) {
246
+ const record = { mainTable };
247
+ for (const [node, rows] of Object.entries(details)) {
248
+ if (rows.length)
249
+ record[node] = rows;
250
+ }
251
+ const response = (await env.host.request({
252
+ path: '/api/ebuilder/form/dataset/v2/saveFormData',
253
+ method: 'POST',
254
+ body: {
255
+ datajson: {
256
+ datas: [record],
257
+ header: { objId },
258
+ operationinfo: { fieldNoFindIgnore: 'true', doAction: 'false' },
259
+ },
260
+ },
261
+ operation: 'ehr.recruit.candidate.import.apply',
262
+ }));
263
+ const datajson = response?.data?.datajson || {};
264
+ const code = response?.message?.code;
265
+ const ok = (code === undefined || String(code) === '0') && datajson.status !== false;
266
+ if (!ok) {
267
+ // 业务失败信息在 datajson.message(外层 message.msg 恒为 success)
268
+ throw ehrValidationError('save_failed', `dataset saveFormData 失败:${datajson.message || response?.message?.msg || JSON.stringify(response).slice(0, 200)}`);
269
+ }
270
+ const ids = Array.isArray(datajson.dataIds) ? datajson.dataIds : [];
271
+ if (!ids.length) {
272
+ throw ehrValidationError('save_failed', `dataset saveFormData 未返回 dataIds:${JSON.stringify(response).slice(0, 200)}`);
273
+ }
274
+ return String(ids[0]);
275
+ }
276
+ /** 上传原件到人才表单的简历附件字段(两次 multipart:预上传检查 → 正式上传)。 */
277
+ async function uploadResumeFile(env, filePath, fieldId, formId) {
278
+ let buffer;
279
+ let mtimeMs = Date.now();
280
+ try {
281
+ buffer = await readFile(filePath);
282
+ mtimeMs = (await stat(filePath)).mtimeMs || Date.now();
283
+ }
284
+ catch {
285
+ throw ehrValidationError('resume_file_unreadable', `无法读取简历文件:${filePath}`);
286
+ }
287
+ const name = basename(filePath);
288
+ const size = buffer.length;
289
+ const baseForm = (withFile) => {
290
+ const fd = new FormData();
291
+ fd.append('compressImg', 'false');
292
+ fd.append('source', 'form');
293
+ fd.append('fieldId', fieldId);
294
+ fd.append('sourceRefId', formId);
295
+ fd.append('module', 'ebuilderform');
296
+ fd.append('authModule', 'ebuilderform');
297
+ fd.append('formId', formId);
298
+ fd.append('refId', formId);
299
+ fd.append('size', String(size));
300
+ fd.append('commonText', JSON.stringify({ fieldId, formId }));
301
+ if (withFile)
302
+ fd.append('file', new Blob([new Uint8Array(buffer)]), name);
303
+ return fd;
304
+ };
305
+ const pre = (await env.host.request({
306
+ path: '/api/file/preUploadCheck',
307
+ method: 'POST',
308
+ multipart: baseForm(true), // 预上传检查必须带文件,否则服务端 500
309
+ headers: { ebBusinessId: formId, langType: 'zh_CN', timeZoneOffset: '-480' },
310
+ operation: 'ehr.recruit.candidate.import.apply',
311
+ }));
312
+ const preUploadId = pre?.data?.preUploadId;
313
+ const uploadType = pre?.data?.uploadType || 'common';
314
+ if (!preUploadId) {
315
+ throw ehrValidationError('resume_preupload_failed', `简历预上传检查失败:${JSON.stringify(pre || {}).slice(0, 200)}`);
316
+ }
317
+ const fd = baseForm(false);
318
+ fd.append('name', name);
319
+ fd.append('MD5', createHash('md5').update(buffer).digest('hex'));
320
+ fd.append('lastModified', String(Math.round(mtimeMs)));
321
+ fd.append('lastModifiedDate', String(Math.round(mtimeMs)));
322
+ fd.append('preUploadId', String(preUploadId));
323
+ fd.append('uploadType', String(uploadType));
324
+ fd.append('file', new Blob([new Uint8Array(buffer)]), name);
325
+ const res = (await env.host.request({
326
+ path: '/api/file/module/upload',
327
+ method: 'POST',
328
+ multipart: fd,
329
+ headers: { ebBusinessId: formId, langType: 'zh_CN', timeZoneOffset: '-480' },
330
+ operation: 'ehr.recruit.candidate.import.apply',
331
+ }));
332
+ const fileid = res?.data?.fileid || res?.data?.uploadModuleDto?.fileObj?.fileId || res?.data?.fileObj?.fileId;
333
+ if (!fileid) {
334
+ throw ehrValidationError('resume_upload_failed', `简历上传未返回 fileid:${JSON.stringify(res || {}).slice(0, 200)}`);
335
+ }
336
+ return { fileid: String(fileid), name, size };
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({
343
+ path: '/api/my/tag/query',
344
+ method: 'POST',
345
+ body: { tagModules: ['ebuilderform'], current: 1, pageSize: 500 },
346
+ operation: 'ehr.recruit.candidate.import.apply',
347
+ }));
348
+ const rows = list?.data?.data || list?.data?.list || [];
349
+ 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 });
355
+ }
356
+ const tags = [];
357
+ const applied = [];
358
+ const skipped = [];
359
+ for (const name of tagNames) {
360
+ const hit = byName.get(name);
361
+ if (!hit) {
362
+ skipped.push(name);
363
+ continue;
364
+ }
365
+ tags.push({ id: hit.id, name: hit.name, type: 0 });
366
+ applied.push(name);
367
+ }
368
+ if (tags.length) {
369
+ await env.host.request({
370
+ path: '/api/my/tag/common/batchAddEntityTag',
371
+ method: 'POST',
372
+ body: { tags, batchDatas: [{ entityId: talentId, module: 'ebuilderform' }] },
373
+ operation: 'ehr.recruit.candidate.import.apply',
374
+ });
375
+ }
376
+ return { applied, skipped };
377
+ }
378
+ async function requireCache(env) {
379
+ if (!env.recruitContext)
380
+ throw ehrValidationError('recruit_context_unavailable', '招聘上下文加载器未初始化');
381
+ return env.recruitContext.load(env.host);
382
+ }
383
+ function str(value) {
384
+ if (value === undefined || value === null)
385
+ return '';
386
+ return String(value).trim();
387
+ }
388
+ async function buildImportPlan(env, cache, args) {
389
+ const warnings = [];
390
+ const talent = cache.forms.talent_pool;
391
+ const objId = talent?.objId;
392
+ const formId = talent?.formId || objId;
393
+ if (!objId || !formId) {
394
+ throw ehrValidationError('talent_form_unavailable', '当前环境未启用「人才库」功能(未解析到 talent_pool 表单)');
395
+ }
396
+ const fields = cache.fields[objId] || {};
397
+ const name = str(args.candidateName);
398
+ const mobile = str(args.telephone);
399
+ if (!name)
400
+ throw ehrValidationError('candidate_name_required', 'candidateName 必填');
401
+ if (!/^\d{6,}$/.test(mobile))
402
+ throw ehrValidationError('telephone_invalid', 'telephone 必须是 6 位以上数字(手机号)');
403
+ if (!fields.candidate_name || !fields.telephone) {
404
+ throw ehrValidationError('recruit_field_missing', '环境缓存缺人才姓名字段(candidate_name)或手机字段(telephone)');
405
+ }
406
+ const resolveDict = makeDictResolver(cache, warnings);
407
+ const main = {};
408
+ const putText = (key, value, longText = false) => {
409
+ if (!fields[key])
410
+ return;
411
+ const text = str(value);
412
+ if (!text)
413
+ return;
414
+ main[key] = text;
415
+ void longText; // 长文本在数据集接口同样按字符串提交,此处仅作语义标记
416
+ };
417
+ putText('candidate_name', name);
418
+ putText('telephone', mobile);
419
+ putText('email', args.email);
420
+ putText('birthday', args.birthday);
421
+ putText('age', args.age);
422
+ putText('college', args.college);
423
+ putText('graduate_date', args.graduateDate);
424
+ putText('joinworkdate', args.joinworkdate);
425
+ putText('intend_position', args.intendPosition);
426
+ putText('intended_workplace', args.intendedWorkplace);
427
+ putText('self_desc', args.selfDesc, true);
428
+ putText('work_ability', args.workAbility, true);
429
+ // 字典/枚举类主表字段:AI 给名称 → 唯一命中反查 optionId
430
+ for (const [key, inputKey, label] of [
431
+ ['gender', 'genderName', '性别'],
432
+ ['highest_degree', 'highestDegreeName', '最高学历'],
433
+ ]) {
434
+ if (!fields[key])
435
+ continue;
436
+ const raw = str(args[inputKey]);
437
+ if (!raw)
438
+ continue;
439
+ const id = resolveDict(raw, label);
440
+ if (id)
441
+ main[key] = id;
442
+ }
443
+ // 渠道(人才来源):按名称解析字典
444
+ const channel = str(args.channel);
445
+ if (channel) {
446
+ if (!fields.channel)
447
+ warnings.push('人才表单无渠道字段,channel 已忽略');
448
+ else {
449
+ const id = resolveDict(channel, '渠道');
450
+ if (id)
451
+ main.channel = id;
452
+ }
453
+ }
454
+ // 黑名单默认「否」(选项 id 运行时反查)
455
+ if (fields.is_blacklist) {
456
+ const noId = resolveDict('否', '黑名单');
457
+ if (noId)
458
+ main.is_blacklist = noId;
459
+ }
460
+ // 明细表:getTableMapping 解析 detailN,再按布局列构建行
461
+ let subStats = {};
462
+ let detailNodes = {};
463
+ let details = {};
464
+ try {
465
+ const mappingResponse = (await env.host.request({
466
+ path: `/api/ebuilder/form/dataset/v1/getTableMapping?objId=${encodeURIComponent(objId)}`,
467
+ method: 'GET',
468
+ operation: 'ehr.recruit.candidate.import.prepare',
469
+ }));
470
+ const mapping = mappingResponse?.data;
471
+ if (!mapping?.mainTable)
472
+ throw new Error('getTableMapping 未返回 mainTable');
473
+ const nodeOfTable = {};
474
+ for (const [node, tableId] of Object.entries(mapping)) {
475
+ if (node === 'mainTable')
476
+ continue;
477
+ nodeOfTable[String(tableId)] = node;
478
+ }
479
+ const meta = await resolveSubTableMeta(env, formId);
480
+ const built = buildSubDetails(args, meta, nodeOfTable, resolveDict);
481
+ details = built.details;
482
+ subStats = built.stats;
483
+ detailNodes = built.nodes;
484
+ warnings.push(...built.warnings);
485
+ }
486
+ catch (error) {
487
+ warnings.push(`明细表未写入:${error?.message || '未知错误'}(主表照常建档)`);
488
+ }
489
+ const subRowTotal = Object.values(subStats).reduce((sum, n) => sum + n, 0);
490
+ const summary = {
491
+ candidateName: name,
492
+ telephone: mobile,
493
+ email: str(args.email) || '(未填)',
494
+ channel: channel || '(未指定)',
495
+ mainFieldCount: Object.keys(main).length,
496
+ mainFields: Object.keys(main).sort(),
497
+ subTableRows: subStats,
498
+ subRowTotal,
499
+ detailNodes,
500
+ platformTags: Array.isArray(args.tags) ? args.tags : [],
501
+ resumeFile: str(args.resumeFile) || '(未提供原件)',
502
+ warnings,
503
+ };
504
+ return {
505
+ objId,
506
+ formId,
507
+ talentObjId: objId,
508
+ mainTable: main,
509
+ details,
510
+ subStats,
511
+ detailNodes,
512
+ warnings,
513
+ talentName: name,
514
+ mobile,
515
+ summary,
516
+ };
517
+ }
518
+ const candidateImportPrepare = {
519
+ name: 'ehr.recruit.candidate.import.prepare',
520
+ resource: 'recruit',
521
+ method: 'candidate.import.prepare',
522
+ action: 'ehr.recruit.candidate.import.prepare',
523
+ summary: '简历导入建人才的确认摘要:手机号查重 + 主表字段映射 + 6 张明细表行数 + 标签核验,不写入任何数据',
524
+ capability: 'recruit',
525
+ risk: WRITE,
526
+ requiresConfirmation: true,
527
+ inputSchema: schemaOf({
528
+ candidateName: { type: 'string', maxLength: 60, description: '姓名(必填)' },
529
+ telephone: { type: 'string', pattern: '^\\d{6,20}$', description: '手机号(必填,查重主键)' },
530
+ email: { type: 'string', maxLength: 120 },
531
+ genderName: { type: 'string', description: '性别名称(如 男/女,按本环境字典反查)' },
532
+ birthday: { type: 'string', description: '出生日期 YYYY-MM-DD' },
533
+ age: { type: 'string', description: '年龄' },
534
+ highestDegreeName: { type: 'string', description: '最高学历名称(如 本科/硕士)' },
535
+ college: { type: 'string', maxLength: 120, description: '毕业院校' },
536
+ graduateDate: { type: 'string', description: '毕业时间' },
537
+ joinworkdate: { type: 'string', description: '参加工作日期' },
538
+ intendPosition: { type: 'string', maxLength: 120, description: '意向岗位' },
539
+ intendedWorkplace: { type: 'string', maxLength: 120, description: '期望工作地点' },
540
+ selfDesc: { type: 'string', maxLength: 20000, description: '自我评价(长文本)' },
541
+ workAbility: { type: 'string', maxLength: 20000, description: '职业技能/技术栈(长文本)' },
542
+ channel: { type: 'string', maxLength: 60, description: '人才来源渠道名称(如 智联招聘/内推)' },
543
+ tags: { type: 'array', items: { type: 'string' }, maxItems: 20, description: '平台标签名称(须为本环境已有标签,同名优先公共;不自动创建)' },
544
+ resumeFile: { type: 'string', description: '简历原件本地路径(可选;提供则上传到原始简历附件字段)' },
545
+ academicInformations: {
546
+ type: 'array',
547
+ maxItems: 20,
548
+ description: '教育经历(school/edu_major/edu_education/degree/study_date/faculty/research_direction/major_courses)',
549
+ items: { type: 'object' },
550
+ },
551
+ jobInformations: {
552
+ type: 'array',
553
+ maxItems: 30,
554
+ description: '工作经历(co_name/work_position/work_dept/work_desc/resign_cause/start_date/end_date/work_type)',
555
+ items: { type: 'object' },
556
+ },
557
+ internshipExperiences: {
558
+ type: 'array',
559
+ maxItems: 20,
560
+ description: '实习经历(sx_co_name/position_job/job_description/entry_time/sx_work_type)',
561
+ items: { type: 'object' },
562
+ },
563
+ projectExperiences: {
564
+ type: 'array',
565
+ maxItems: 30,
566
+ description: '项目经历(pj_name/pj_role/pj_duty/pj_introduction/start_date/end_date)',
567
+ items: { type: 'object' },
568
+ },
569
+ awardExperiences: {
570
+ type: 'array',
571
+ maxItems: 20,
572
+ description: '获奖经历(award_name/awarded/credentials/award_date)',
573
+ items: { type: 'object' },
574
+ },
575
+ languageAbilities: {
576
+ type: 'array',
577
+ maxItems: 10,
578
+ description: '语言能力(language_type/mastery/ls_ability/literacy)',
579
+ items: { type: 'object' },
580
+ },
581
+ }, { required: ['candidateName', 'telephone'] }),
582
+ handler: async (args, env) => {
583
+ const cache = await requireCache(env);
584
+ const plan = await buildImportPlan(env, cache, args);
585
+ const dup = await checkDuplicateByMobile(env, cache, plan.mobile);
586
+ if (dup.length > 1) {
587
+ throw ehrValidationError('duplicate_ambiguous', `手机号 ${plan.mobile} 存在多条候选(数据异常),请人工核对后再决定,不自动新建`);
588
+ }
589
+ const dupRow = dup[0];
590
+ const session = await env.host.getSession();
591
+ const continuation = await env.continuation.issue({
592
+ operation: 'ehr.recruit.candidate.import.apply',
593
+ baseUrl: session.baseUrl,
594
+ profile: session.profile,
595
+ userId: session.userId,
596
+ payload: {
597
+ objId: plan.objId,
598
+ formId: plan.formId,
599
+ mainTable: plan.mainTable,
600
+ details: plan.details,
601
+ tags: Array.isArray(args.tags) ? args.tags.map(String) : [],
602
+ resumeFile: str(args.resumeFile),
603
+ subStats: plan.subStats,
604
+ warnings: plan.warnings,
605
+ candidateName: plan.talentName,
606
+ telephone: plan.mobile,
607
+ },
608
+ }, 10 * 60 * 1000);
609
+ return ehrSuccess('ehr.recruit.candidate.import.prepare', {
610
+ status: 'AWAITING_CONFIRMATION',
611
+ preview: {
612
+ action: '简历导入建人才(dataset/v2/saveFormData 主表+明细 → 批次 → 智能标签 → 平台标签)',
613
+ ...plan.summary,
614
+ duplicate: dupRow
615
+ ? `已存在同手机号人才「${dupRow.candidate_name || dupRow.name || ''}」(id=${dupRow.form_data_id || dupRow.id})→ apply 将阻断`
616
+ : '无重复',
617
+ },
618
+ willSubmit: {
619
+ objId: plan.objId,
620
+ mainTable: plan.mainTable,
621
+ details: plan.details,
622
+ tags: Array.isArray(args.tags) ? args.tags : [],
623
+ },
624
+ continuation,
625
+ expiresInSeconds: 600,
626
+ });
627
+ },
628
+ };
629
+ const candidateImportApply = {
630
+ name: 'ehr.recruit.candidate.import.apply',
631
+ resource: 'recruit',
632
+ method: 'candidate.import.apply',
633
+ action: 'ehr.recruit.candidate.import.apply',
634
+ summary: '执行简历导入建人才(主表+明细 → 批次 → 智能标签 → 平台标签 → 双链接);必须先 prepare 并经用户明确确认',
635
+ capability: 'recruit',
636
+ risk: WRITE,
637
+ requiresConfirmation: true,
638
+ inputSchema: schemaOf({
639
+ candidateName: { type: 'string', maxLength: 60 },
640
+ telephone: { type: 'string', pattern: '^\\d{6,20}$' },
641
+ resumeFile: { type: 'string' },
642
+ tags: { type: 'array', items: { type: 'string' }, maxItems: 20 },
643
+ confirm: { const: true },
644
+ continuation: { type: 'string', minLength: 1 },
645
+ }, { required: ['candidateName', 'telephone', 'confirm', 'continuation'] }),
646
+ handler: async (args, env) => {
647
+ if (args.confirm !== true) {
648
+ throw ehrConfirmationError('ehr.recruit.candidate.import.apply', {
649
+ hint: '重新调用时传入 confirm=true 与 prepare 返回的 continuation',
650
+ });
651
+ }
652
+ const lease = await env.continuation.verify(args.continuation, 'ehr.recruit.candidate.import.apply');
653
+ const session = await env.host.getSession();
654
+ if (lease.baseUrl !== session.baseUrl || lease.profile !== session.profile || lease.userId !== session.userId) {
655
+ throw ehrValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 prepare');
656
+ }
657
+ const payload = lease.payload || {};
658
+ if (payload.candidateName !== str(args.candidateName) || payload.telephone !== str(args.telephone)) {
659
+ throw ehrValidationError('target_changed', '姓名或手机号与 prepare 时不一致,请重新执行 prepare');
660
+ }
661
+ const cache = await requireCache(env);
662
+ const objId = String(payload.objId);
663
+ const formId = String(payload.formId);
664
+ const fields = cache.fields[objId] || {};
665
+ // ① 原件附件(可选):先上传拿 fileid,再并入主表
666
+ const mainTable = { ...(payload.mainTable || {}) };
667
+ const resumeFieldKey = 'original_resume';
668
+ const resumeFile = str(payload.resumeFile);
669
+ let uploaded = null;
670
+ if (resumeFile) {
671
+ if (!fields[resumeFieldKey]) {
672
+ throw ehrValidationError('resume_field_missing', '人才表单无「原始简历」附件字段(original_resume),无法上传原件');
673
+ }
674
+ uploaded = await uploadResumeFile(env, resumeFile, fields[resumeFieldKey], formId);
675
+ mainTable[resumeFieldKey] = uploaded.fileid;
676
+ }
677
+ // ② 主表 + 明细一次写入(写请求发出后的不确定结果一律 partial,禁止自动重试)
678
+ let talentId;
679
+ try {
680
+ talentId = await datasetSaveFormData(env, objId, mainTable, payload.details || {});
681
+ }
682
+ catch (error) {
683
+ if (error?.subtype === 'save_failed')
684
+ throw error;
685
+ throw ehrPartialError(`简历建档写入结果不确定(${error?.message || '未知错误'}):请先用人才库搜索手机号 ${payload.telephone} 核对是否已建档,禁止直接重试`, { candidateName: payload.candidateName, telephone: payload.telephone });
686
+ }
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
+ }
724
+ // ⑥ 产物链接:人才卡片单页 + 人才库列表定位(自用系统追加 recruitType=useSelf)
725
+ const baseUrl = session.baseUrl.replace(/\/+$/, '');
726
+ const extra = String(cache.selfUse) === '1' ? '&recruitType=useSelf' : '';
727
+ const talentCardUrl = cache.ebdDetailPageId
728
+ ? `${baseUrl}/sp/ebdpage/view/${cache.ebdDetailPageId}?talentId=${talentId}${extra}`
729
+ : null;
730
+ const talentListId = cache.lists.talent?.listId;
731
+ const talentListLocateUrl = talentListId
732
+ ? `${baseUrl}/sp/ebdfpage/list/${talentListId}?talentId=${talentId}${extra}`
733
+ : null;
734
+ const warnings = [...(payload.warnings || [])];
735
+ if (batchNote)
736
+ warnings.push(batchNote);
737
+ return ehrSuccess('ehr.recruit.candidate.import.apply', {
738
+ result: 'success',
739
+ talentId,
740
+ candidateName: payload.candidateName,
741
+ telephone: payload.telephone,
742
+ mainTableFields: Object.keys(mainTable).length,
743
+ detailRows: payload.subStats || undefined,
744
+ attachment: uploaded ? { name: uploaded.name, size: uploaded.size } : null,
745
+ batch: { ok: batchOk, note: batchNote },
746
+ labelMatch,
747
+ tagsApplied: tagResult.applied,
748
+ tagsSkipped: tagResult.skipped,
749
+ talentCardUrl,
750
+ talentListLocateUrl,
751
+ }, warnings.length ? { warnings } : undefined);
752
+ },
753
+ };
754
+ export const recruitCandidateImportOperations = [
755
+ candidateImportPrepare,
756
+ candidateImportApply,
757
+ ];