weaver-work-cli 0.1.1 → 0.1.4

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 (351) hide show
  1. package/README.md +187 -76
  2. package/dist/cmd/setup/index.js +5 -5
  3. package/dist/cmd/skills/index.js +1 -1
  4. package/dist/core/hooks.js +7 -0
  5. package/dist/index.js +0 -0
  6. package/dist/internal/e10/auth/commands.js +160 -55
  7. package/dist/internal/e10/auth/crypto.js +7 -22
  8. package/dist/internal/e10/auth/session.js +1 -1
  9. package/dist/internal/e10/auth/xiaoe.js +268 -0
  10. package/dist/internal/e10/context.js +16 -2
  11. package/dist/internal/e10/index.js +1 -0
  12. package/dist/internal/e10/request-runtime.js +344 -0
  13. package/dist/internal/skills/install.js +48 -6
  14. package/dist/internal/skills/reader.js +27 -0
  15. package/dist/shortcuts/archive/continuation.js +70 -0
  16. package/dist/shortcuts/archive/errors.js +53 -0
  17. package/dist/shortcuts/archive/host.js +197 -0
  18. package/dist/shortcuts/archive/index.js +363 -0
  19. package/dist/shortcuts/archive/manifest.js +45 -0
  20. package/dist/shortcuts/archive/operations/archive-info.js +55 -0
  21. package/dist/shortcuts/archive/operations/borrow-car.js +146 -0
  22. package/dist/shortcuts/archive/operations/borrow-list.js +200 -0
  23. package/dist/shortcuts/archive/operations/confirm.js +46 -0
  24. package/dist/shortcuts/archive/operations/download.js +238 -0
  25. package/dist/shortcuts/archive/operations/flow-url.js +240 -0
  26. package/dist/shortcuts/archive/operations/fonds.js +95 -0
  27. package/dist/shortcuts/archive/operations/getPageUrl.js +137 -0
  28. package/dist/shortcuts/archive/operations/registry.js +43 -0
  29. package/dist/shortcuts/archive/operations/search.js +88 -0
  30. package/dist/shortcuts/archive/operations/shared.js +120 -0
  31. package/dist/shortcuts/archive/operations/types.js +23 -0
  32. package/dist/shortcuts/archive/operations/upload.js +182 -0
  33. package/dist/shortcuts/archive/operations.js +13 -0
  34. package/dist/shortcuts/archive/render/search-format.js +276 -0
  35. package/dist/shortcuts/archive/render/search-view.js +125 -0
  36. package/dist/shortcuts/asset/continuation.js +70 -0
  37. package/dist/shortcuts/asset/errors.js +67 -0
  38. package/dist/shortcuts/asset/host.js +438 -0
  39. package/dist/shortcuts/asset/index.js +255 -0
  40. package/dist/shortcuts/asset/manifest.js +471 -0
  41. package/dist/shortcuts/asset/operations/query.js +249 -0
  42. package/dist/shortcuts/asset/operations/registry.js +85 -0
  43. package/dist/shortcuts/asset/operations/shared.js +100 -0
  44. package/dist/shortcuts/asset/operations/type-cache.js +49 -0
  45. package/dist/shortcuts/asset/operations/types.js +1 -0
  46. package/dist/shortcuts/asset/operations/viewlink.js +117 -0
  47. package/dist/shortcuts/asset/operations/write.js +435 -0
  48. package/dist/shortcuts/asset/operations.js +9 -0
  49. package/dist/shortcuts/calendar/continuation.js +70 -0
  50. package/dist/shortcuts/calendar/errors.js +53 -0
  51. package/dist/shortcuts/calendar/host.js +148 -0
  52. package/dist/shortcuts/calendar/index.js +125 -0
  53. package/dist/shortcuts/calendar/manifest.js +26 -0
  54. package/dist/shortcuts/calendar/operations/read.js +259 -0
  55. package/dist/shortcuts/calendar/operations/registry.js +31 -0
  56. package/dist/shortcuts/calendar/operations/shared.js +132 -0
  57. package/dist/shortcuts/calendar/operations/types.js +25 -0
  58. package/dist/shortcuts/calendar/operations/write.js +439 -0
  59. package/dist/shortcuts/calendar/operations.js +8 -0
  60. package/dist/shortcuts/ehr/continuation.js +70 -0
  61. package/dist/shortcuts/ehr/errors.js +53 -0
  62. package/dist/shortcuts/ehr/host.js +213 -0
  63. package/dist/shortcuts/ehr/index.js +205 -0
  64. package/dist/shortcuts/ehr/manifest.js +47 -0
  65. package/dist/shortcuts/ehr/operations/attend-read.js +373 -0
  66. package/dist/shortcuts/ehr/operations/attend-write.js +514 -0
  67. package/dist/shortcuts/ehr/operations/hr-contact.js +363 -0
  68. package/dist/shortcuts/ehr/operations/hr-ds.js +298 -0
  69. package/dist/shortcuts/ehr/operations/hr-report.js +386 -0
  70. package/dist/shortcuts/ehr/operations/recruit-write.js +1629 -0
  71. package/dist/shortcuts/ehr/operations/recruit.js +803 -0
  72. package/dist/shortcuts/ehr/operations/registry.js +43 -0
  73. package/dist/shortcuts/ehr/operations/salary.js +319 -0
  74. package/dist/shortcuts/ehr/operations/shared.js +246 -0
  75. package/dist/shortcuts/ehr/operations/types.js +25 -0
  76. package/dist/shortcuts/ehr/operations.js +25 -0
  77. package/dist/shortcuts/esb/continuation.js +70 -0
  78. package/dist/shortcuts/esb/errors.js +65 -0
  79. package/dist/shortcuts/esb/host.js +187 -0
  80. package/dist/shortcuts/esb/index.js +112 -0
  81. package/dist/shortcuts/esb/manifest.js +91 -0
  82. package/dist/shortcuts/esb/operations/input-format.js +31 -0
  83. package/dist/shortcuts/esb/operations/registry.js +48 -0
  84. package/dist/shortcuts/esb/operations/shared.js +152 -0
  85. package/dist/shortcuts/esb/operations/trigger.js +76 -0
  86. package/dist/shortcuts/esb/operations/types.js +21 -0
  87. package/dist/shortcuts/esb/operations.js +15 -0
  88. package/dist/shortcuts/fna/continuation.js +70 -0
  89. package/dist/shortcuts/fna/errors.js +59 -0
  90. package/dist/shortcuts/fna/host.js +146 -0
  91. package/dist/shortcuts/fna/index.js +111 -0
  92. package/dist/shortcuts/fna/manifest.js +41 -0
  93. package/dist/shortcuts/fna/operations/budget.js +124 -0
  94. package/dist/shortcuts/fna/operations/expense-center.js +148 -0
  95. package/dist/shortcuts/fna/operations/invoice-writeoff.js +212 -0
  96. package/dist/shortcuts/fna/operations/loan.js +195 -0
  97. package/dist/shortcuts/fna/operations/registry.js +35 -0
  98. package/dist/shortcuts/fna/operations/shared.js +210 -0
  99. package/dist/shortcuts/fna/operations/types.js +25 -0
  100. package/dist/shortcuts/fna/operations.js +19 -0
  101. package/dist/shortcuts/hrm/errors.js +38 -0
  102. package/dist/shortcuts/hrm/host.js +156 -0
  103. package/dist/shortcuts/hrm/index.js +111 -0
  104. package/dist/shortcuts/hrm/manifest.js +32 -0
  105. package/dist/shortcuts/hrm/operations/employee.js +338 -0
  106. package/dist/shortcuts/hrm/operations/org.js +110 -0
  107. package/dist/shortcuts/hrm/operations/position.js +176 -0
  108. package/dist/shortcuts/hrm/operations/registry.js +33 -0
  109. package/dist/shortcuts/hrm/operations/shared.js +143 -0
  110. package/dist/shortcuts/hrm/operations/types.js +23 -0
  111. package/dist/shortcuts/hrm/operations.js +19 -0
  112. package/dist/shortcuts/index.js +20 -0
  113. package/dist/shortcuts/invoice/host.js +99 -94
  114. package/dist/shortcuts/invoice/index.js +65 -61
  115. package/dist/shortcuts/invoice/manifest.js +222 -14
  116. package/dist/shortcuts/invoice/operations/browse-field.js +17 -0
  117. package/dist/shortcuts/invoice/operations/issuing.js +198 -0
  118. package/dist/shortcuts/invoice/operations/ocr-preview.js +6 -3
  119. package/dist/shortcuts/invoice/operations/registry.js +17 -0
  120. package/dist/shortcuts/invoice/operations/reim.js +401 -0
  121. package/dist/shortcuts/invoice/operations/shared.js +271 -6
  122. package/dist/shortcuts/invoice/operations/validate-preview.js +1 -1
  123. package/dist/shortcuts/jiuchuanhui/cache.js +441 -0
  124. package/dist/shortcuts/jiuchuanhui/continuation.js +70 -0
  125. package/dist/shortcuts/jiuchuanhui/errors.js +59 -0
  126. package/dist/shortcuts/jiuchuanhui/host.js +159 -0
  127. package/dist/shortcuts/jiuchuanhui/index.js +109 -0
  128. package/dist/shortcuts/jiuchuanhui/manifest.js +469 -0
  129. package/dist/shortcuts/jiuchuanhui/operations/datajson.js +214 -0
  130. package/dist/shortcuts/jiuchuanhui/operations/esb.js +157 -0
  131. package/dist/shortcuts/jiuchuanhui/operations/generic.js +146 -0
  132. package/dist/shortcuts/jiuchuanhui/operations/registry.js +133 -0
  133. package/dist/shortcuts/jiuchuanhui/operations/shared.js +158 -0
  134. package/dist/shortcuts/jiuchuanhui/operations/types.js +1 -0
  135. package/dist/shortcuts/jiuchuanhui/operations.js +8 -0
  136. package/dist/shortcuts/mail/continuation.js +70 -0
  137. package/dist/shortcuts/mail/errors.js +53 -0
  138. package/dist/shortcuts/mail/host.js +182 -0
  139. package/dist/shortcuts/mail/index.js +519 -0
  140. package/dist/shortcuts/mail/manifest.js +38 -0
  141. package/dist/shortcuts/mail/operations/mail-blacklist.js +79 -0
  142. package/dist/shortcuts/mail/operations/mail-contacts.js +195 -0
  143. package/dist/shortcuts/mail/operations/mail-manage.js +335 -0
  144. package/dist/shortcuts/mail/operations/mail-read.js +508 -0
  145. package/dist/shortcuts/mail/operations/mail-send.js +474 -0
  146. package/dist/shortcuts/mail/operations/mail-sign.js +203 -0
  147. package/dist/shortcuts/mail/operations/mail-template.js +170 -0
  148. package/dist/shortcuts/mail/operations/registry.js +41 -0
  149. package/dist/shortcuts/mail/operations/shared.js +64 -0
  150. package/dist/shortcuts/mail/operations/types.js +27 -0
  151. package/dist/shortcuts/mail/operations.js +19 -0
  152. package/dist/shortcuts/plan/continuation.js +70 -0
  153. package/dist/shortcuts/plan/errors.js +70 -0
  154. package/dist/shortcuts/plan/host.js +180 -0
  155. package/dist/shortcuts/plan/index.js +150 -0
  156. package/dist/shortcuts/plan/manifest.js +53 -0
  157. package/dist/shortcuts/plan/operations/link.js +256 -0
  158. package/dist/shortcuts/plan/operations/read.js +259 -0
  159. package/dist/shortcuts/plan/operations/registry.js +33 -0
  160. package/dist/shortcuts/plan/operations/shared.js +246 -0
  161. package/dist/shortcuts/plan/operations/types.js +49 -0
  162. package/dist/shortcuts/plan/operations/version.js +49 -0
  163. package/dist/shortcuts/plan/operations/write.js +710 -0
  164. package/dist/shortcuts/plan/operations.js +8 -0
  165. package/docs/SKILL.md +11 -7
  166. package/docs/_catalog.md +23 -5
  167. package/docs/agent-invoice.md +31 -10
  168. package/docs/agent-skill-install.md +211 -0
  169. package/docs/calendar.md +87 -0
  170. package/docs/e10-auth.md +28 -9
  171. package/docs/ehr.md +186 -0
  172. package/docs/esb.md +64 -0
  173. package/docs/fna.md +72 -0
  174. package/docs/hrm.md +89 -0
  175. package/docs/invoice.md +66 -20
  176. package/docs/jiuchuanhui.md +131 -0
  177. package/docs/operation-manual.md +443 -0
  178. package/docs/plan.md +82 -0
  179. package/docs/wenshuding.md +129 -0
  180. package/docs/ziguanjia.md +89 -0
  181. package/package.json +4 -4
  182. package/scripts/package-skill.mjs +127 -34
  183. package/scripts/postbuild.mjs +13 -0
  184. package/skill-template/business-info.json +13 -2
  185. package/skill-template/domains/calendar.md +15 -0
  186. package/skill-template/domains/esb.md +13 -0
  187. package/skill-template/domains/hrm.md +16 -0
  188. package/skill-template/domains/jiuchuanhui.md +23 -0
  189. package/skill-template/domains/jucailin.md +22 -0
  190. package/skill-template/domains/mail.md +21 -0
  191. package/skill-template/domains/plan.md +17 -0
  192. package/skill-template/domains/qiyecheng.md +17 -0
  193. package/skill-template/domains/shared.md +9 -3
  194. package/skill-template/domains/skill-maker.md +10 -0
  195. package/skill-template/domains/wenshuding.md +22 -0
  196. package/skill-template/domains/{invoice.md → yepiaotong.md} +4 -0
  197. package/skill-template/domains/ziguanjia.md +21 -0
  198. package/skill-template/master-skill-template.md +12 -6
  199. package/skill-template/skill-template.md +40 -9
  200. package/skills/weaver-e10-calendar/SKILL.md +101 -0
  201. package/skills/weaver-e10-calendar/product.json +8 -0
  202. package/skills/weaver-e10-calendar/references/calendar-app-check.md +65 -0
  203. package/skills/weaver-e10-calendar/references/calendar-dict.md +55 -0
  204. package/skills/weaver-e10-calendar/references/calendar-query.md +70 -0
  205. package/skills/weaver-e10-calendar/references/calendar-write.md +97 -0
  206. package/skills/weaver-e10-calendar/references/source-manifest.json +54 -0
  207. package/skills/weaver-e10-esb/SKILL.md +102 -0
  208. package/skills/weaver-e10-esb/product.json +8 -0
  209. package/skills/weaver-e10-esb/references/input-format.md +52 -0
  210. package/skills/weaver-e10-esb/references/source-manifest.json +242 -0
  211. package/skills/weaver-e10-esb/references/trigger.md +99 -0
  212. package/skills/weaver-e10-hrm/SKILL.md +87 -0
  213. package/skills/weaver-e10-hrm/product.json +8 -0
  214. package/skills/weaver-e10-hrm/references/employee.md +109 -0
  215. package/skills/weaver-e10-hrm/references/org.md +95 -0
  216. package/skills/weaver-e10-hrm/references/position.md +95 -0
  217. package/skills/weaver-e10-hrm/references/source-manifest.json +121 -0
  218. package/skills/weaver-e10-jiuchuanhui/SKILL.md +174 -0
  219. package/skills/weaver-e10-jiuchuanhui/product.json +8 -0
  220. package/skills/weaver-e10-jiuchuanhui/references/cache.md +110 -0
  221. package/skills/weaver-e10-jiuchuanhui/references/clue.md +61 -0
  222. package/skills/weaver-e10-jiuchuanhui/references/contact-plan.md +56 -0
  223. package/skills/weaver-e10-jiuchuanhui/references/contact-records.md +48 -0
  224. package/skills/weaver-e10-jiuchuanhui/references/contact.md +56 -0
  225. package/skills/weaver-e10-jiuchuanhui/references/customer.md +69 -0
  226. package/skills/weaver-e10-jiuchuanhui/references/disabled-capabilities.md +24 -0
  227. package/skills/weaver-e10-jiuchuanhui/references/discovery-config.md +60 -0
  228. package/skills/weaver-e10-jiuchuanhui/references/general-helpers.md +59 -0
  229. package/skills/weaver-e10-jiuchuanhui/references/jiuchuanhui-entry.md +60 -0
  230. package/skills/weaver-e10-jiuchuanhui/references/sale.md +72 -0
  231. package/skills/weaver-e10-jiuchuanhui/references/source-manifest.json +2632 -0
  232. package/skills/weaver-e10-jucailin/SKILL.md +101 -0
  233. package/skills/weaver-e10-jucailin/product.json +8 -0
  234. package/skills/weaver-e10-jucailin/references/attend-read.md +73 -0
  235. package/skills/weaver-e10-jucailin/references/attend-write.md +82 -0
  236. package/skills/weaver-e10-jucailin/references/hr-contact.md +92 -0
  237. package/skills/weaver-e10-jucailin/references/hr-datasource.md +104 -0
  238. package/skills/weaver-e10-jucailin/references/hr-report.md +76 -0
  239. package/skills/weaver-e10-jucailin/references/recruit-read.md +76 -0
  240. package/skills/weaver-e10-jucailin/references/recruit-write.md +70 -0
  241. package/skills/weaver-e10-jucailin/references/safety-boundaries.md +35 -0
  242. package/skills/weaver-e10-jucailin/references/salary.md +110 -0
  243. package/skills/weaver-e10-jucailin/references/source-manifest.json +152 -0
  244. package/skills/weaver-e10-mail/SKILL.md +151 -0
  245. package/skills/weaver-e10-mail/product.json +8 -0
  246. package/skills/weaver-e10-mail/references/mail-agent-entry.md +47 -0
  247. package/skills/weaver-e10-mail/references/mail-attachment.md +30 -0
  248. package/skills/weaver-e10-mail/references/mail-blacklist.md +27 -0
  249. package/skills/weaver-e10-mail/references/mail-contacts.md +33 -0
  250. package/skills/weaver-e10-mail/references/mail-manage.md +44 -0
  251. package/skills/weaver-e10-mail/references/mail-read.md +76 -0
  252. package/skills/weaver-e10-mail/references/mail-send.md +43 -0
  253. package/skills/weaver-e10-mail/references/mail-sign.md +33 -0
  254. package/skills/weaver-e10-mail/references/mail-template.md +31 -0
  255. package/skills/weaver-e10-mail/references/safety-boundaries.md +32 -0
  256. package/skills/weaver-e10-plan/SKILL.md +139 -0
  257. package/skills/weaver-e10-plan/product.json +8 -0
  258. package/skills/weaver-e10-plan/references/plan-collect-and-compose.md +123 -0
  259. package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +138 -0
  260. package/skills/weaver-e10-plan/references/plan-report-query.md +154 -0
  261. package/skills/weaver-e10-plan/references/plan-report-remind.md +104 -0
  262. package/skills/weaver-e10-plan/references/plan-report-write.md +162 -0
  263. package/skills/weaver-e10-plan/references/source-manifest.json +858 -0
  264. package/skills/weaver-e10-qiyecheng/SKILL.md +118 -0
  265. package/skills/weaver-e10-qiyecheng/product.json +8 -0
  266. package/skills/weaver-e10-qiyecheng/references/budget.md +65 -0
  267. package/skills/weaver-e10-qiyecheng/references/catalog.md +56 -0
  268. package/skills/weaver-e10-qiyecheng/references/expense-center.md +51 -0
  269. package/skills/weaver-e10-qiyecheng/references/invoice-writeoff.md +57 -0
  270. package/skills/weaver-e10-qiyecheng/references/loan-management.md +59 -0
  271. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +698 -0
  272. package/skills/weaver-e10-shared/SKILL.md +86 -0
  273. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +127 -0
  274. package/skills/weaver-e10-shared/references/json-output-contract.md +120 -0
  275. package/skills/weaver-e10-shared/references/weaver-e10-installation.md +110 -0
  276. package/skills/weaver-e10-skill-maker/SKILL.md +77 -0
  277. package/skills/weaver-e10-skill-maker/product.json +8 -0
  278. package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +124 -0
  279. package/skills/weaver-e10-skill-maker/references/detector-validation.md +101 -0
  280. package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +230 -0
  281. package/skills/weaver-e10-skill-maker/references/post-generation-install.md +105 -0
  282. package/skills/weaver-e10-skill-maker/references/source-update-detection.md +261 -0
  283. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +141 -0
  284. package/skills/weaver-e10-wenshuding/SKILL.md +157 -0
  285. package/skills/weaver-e10-wenshuding/product.json +8 -0
  286. package/skills/weaver-e10-wenshuding/references/archive-agent-entry.md +63 -0
  287. package/skills/weaver-e10-wenshuding/references/archive-borrow-car.md +52 -0
  288. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +52 -0
  289. package/skills/weaver-e10-wenshuding/references/archive-disabled-capabilities.md +22 -0
  290. package/skills/weaver-e10-wenshuding/references/archive-download.md +48 -0
  291. package/skills/weaver-e10-wenshuding/references/archive-flow.md +46 -0
  292. package/skills/weaver-e10-wenshuding/references/archive-fonds.md +34 -0
  293. package/skills/weaver-e10-wenshuding/references/archive-info.md +33 -0
  294. package/skills/weaver-e10-wenshuding/references/archive-search.md +52 -0
  295. package/skills/weaver-e10-wenshuding/references/archive-upload.md +41 -0
  296. package/skills/weaver-e10-wenshuding/references/source-manifest.json +517 -0
  297. package/skills/weaver-e10-yepiaotong/SKILL.md +184 -0
  298. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-add.md +1 -1
  299. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +47 -0
  300. package/skills/weaver-e10-yepiaotong/references/invoice-browse-field-data.md +204 -0
  301. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-delete.md +1 -1
  302. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-detail.md +13 -5
  303. package/skills/weaver-e10-yepiaotong/references/invoice-disabled-capabilities.md +28 -0
  304. package/skills/weaver-e10-yepiaotong/references/invoice-download.md +31 -0
  305. package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +45 -0
  306. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-file-upload.md +15 -4
  307. package/skills/weaver-e10-yepiaotong/references/invoice-import.md +45 -0
  308. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +120 -0
  309. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +161 -0
  310. package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +99 -0
  311. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-ocr-preview.md +14 -4
  312. package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +58 -0
  313. package/skills/weaver-e10-yepiaotong/references/invoice-red.md +261 -0
  314. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +97 -0
  315. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-update.md +1 -1
  316. package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/references/invoice-validation-preview.md +10 -3
  317. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +1340 -0
  318. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +845 -0
  319. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +94 -0
  320. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +449 -0
  321. package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +266 -0
  322. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +494 -0
  323. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +1103 -0
  324. package/skills/weaver-e10-ziguanjia/SKILL.md +162 -0
  325. package/skills/weaver-e10-ziguanjia/product.json +8 -0
  326. package/skills/weaver-e10-ziguanjia/references/source-manifest.json +147 -0
  327. package/skills/weaver-e10-ziguanjia/references/ziguanjia-agent-entry.md +65 -0
  328. package/skills/weaver-e10-ziguanjia/references/ziguanjia-create.md +45 -0
  329. package/skills/weaver-e10-ziguanjia/references/ziguanjia-depre.md +48 -0
  330. package/skills/weaver-e10-ziguanjia/references/ziguanjia-disabled-capabilities.md +22 -0
  331. package/skills/weaver-e10-ziguanjia/references/ziguanjia-purch.md +57 -0
  332. package/skills/weaver-e10-ziguanjia/references/ziguanjia-query.md +62 -0
  333. package/skills/weaver-e10-ziguanjia/references/ziguanjia-repair.md +83 -0
  334. package/skills/weaver-e10-ziguanjia/references/ziguanjia-resolve.md +49 -0
  335. package/skills/weaver-e10-ziguanjia/references/ziguanjia-return.md +70 -0
  336. package/skills/weaver-e10-ziguanjia/references/ziguanjia-update.md +45 -0
  337. package/skills/weaver-e10-ziguanjia/references/ziguanjia-use.md +60 -0
  338. package/skills/weaver-e10-ziguanjia/references/ziguanjia-viewlink.md +81 -0
  339. package/skills/weaver-work-cli-invoice/SKILL.md +0 -103
  340. package/skills/weaver-work-cli-invoice/references/invoice-agent-entry.md +0 -38
  341. package/skills/weaver-work-cli-invoice/references/invoice-disabled-capabilities.md +0 -26
  342. package/skills/weaver-work-cli-invoice/references/invoice-download.md +0 -22
  343. package/skills/weaver-work-cli-invoice/references/invoice-enterprise-list.md +0 -35
  344. package/skills/weaver-work-cli-invoice/references/invoice-import.md +0 -28
  345. package/skills/weaver-work-cli-invoice/references/invoice-personal-list.md +0 -47
  346. package/skills/weaver-work-cli-shared/SKILL.md +0 -53
  347. package/skills/weaver-work-cli-shared/references/e10-auth-and-session.md +0 -83
  348. package/skills/weaver-work-cli-shared/references/json-output-contract.md +0 -61
  349. package/skills/weaver-work-cli-shared/references/weaver-work-cli-installation.md +0 -61
  350. /package/skills/{weaver-work-cli-shared → weaver-e10-shared}/references/high-risk-write.md +0 -0
  351. /package/skills/{weaver-work-cli-invoice → weaver-e10-yepiaotong}/product.json +0 -0
@@ -0,0 +1,1629 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { ehrConfirmationError, ehrPartialError, ehrValidationError, EhrCliError } from '../errors.js';
3
+ import { WRITE, ehrSuccess, schemaOf, } from './types.js';
4
+ const CONTINUATION_TTL_SECONDS = 600;
5
+ function fingerprint(value) {
6
+ return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex').slice(0, 32);
7
+ }
8
+ function requireConfirm(args, action) {
9
+ if (args.confirm !== true) {
10
+ throw ehrConfirmationError(action, { hint: '重新调用时传入 confirm=true 与 prepare 返回的 continuation' });
11
+ }
12
+ }
13
+ /** 校验 continuation 与当前登录上下文一致,避免切换租户/账号后误执行写操作。 */
14
+ async function verifyLease(args, action, env) {
15
+ const lease = await env.continuation.verify(args.continuation, action);
16
+ const session = await env.host.getSession();
17
+ if (lease.baseUrl !== session.baseUrl || lease.profile !== session.profile || lease.userId !== session.userId) {
18
+ throw ehrValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 prepare');
19
+ }
20
+ return lease;
21
+ }
22
+ function isNetworkError(error) {
23
+ return error instanceof EhrCliError && error.type === 'network';
24
+ }
25
+ async function requireRecruitCache(env) {
26
+ if (!env.recruitContext) {
27
+ throw ehrValidationError('recruit_context_unavailable', '招聘上下文加载器未初始化');
28
+ }
29
+ return env.recruitContext.load(env.host);
30
+ }
31
+ /** 建前查重:按手机号搜人才库(total 恒 0 平台缺陷,以 data.data[] 非空为准)。 */
32
+ async function checkDuplicateByMobile(env, cache, mobile) {
33
+ const talent = cache.lists.talent;
34
+ const objId = cache.forms.talent_pool?.objId;
35
+ if (!talent?.listId || !objId) {
36
+ throw ehrValidationError('recruit_context_missing', '当前环境未解析到人才库菜单/表单,无法执行建前查重');
37
+ }
38
+ const fields = cache.fields[objId] || {};
39
+ if (!fields.telephone) {
40
+ throw ehrValidationError('recruit_field_missing', '环境缓存缺人才手机字段(telephone),无法查重');
41
+ }
42
+ const mobileField = `field_${fields.telephone}`;
43
+ const tableuid = randomHex32();
44
+ const response = (await env.host.request({
45
+ path: `/api/ebuilder/form/list/getListDatas?passid=${objId}&cid=${objId}`,
46
+ body: {
47
+ current: 1,
48
+ pageSize: 10,
49
+ tableuid,
50
+ listId: talent.listId,
51
+ searchParamData: {
52
+ searchType: '3',
53
+ commonSearchFields: { [mobileField]: [{ [mobileField]: mobile, [`${mobileField}_condition_type`]: 'like' }] },
54
+ type: 'and',
55
+ },
56
+ terminal: 'PC', browser: 'Chrome', requiredMatch: 1, mapResult: 1, recheckQuery: 1,
57
+ isFollow: 0, isShareToMe: 0, filterCondition: {},
58
+ cusMenuId: talent.cusMenuId, urlPageTitle: '人才库', menuName: talent.menuName || '人才管理',
59
+ },
60
+ headers: { ebBusinessId: talent.listId },
61
+ }));
62
+ return response?.data?.data || [];
63
+ }
64
+ /** 渠道名称 → 字典 optionId(dict.raw 精确匹配,找不到必须报错,禁止臆造 optionId)。 */
65
+ function channelOptionId(cache, name) {
66
+ for (const item of cache.dict?.raw || []) {
67
+ if (item.selection_name === name)
68
+ return item.option_id;
69
+ }
70
+ throw ehrValidationError('channel_not_found', `渠道「${name}」未在数据字典中找到,请从已知渠道选择(不得臆造 optionId)`);
71
+ }
72
+ function validateNameMobile(name, mobile) {
73
+ const nameText = typeof name === 'string' ? name.trim() : '';
74
+ const mobileText = typeof mobile === 'string' ? mobile.trim() : '';
75
+ if (!nameText)
76
+ throw ehrValidationError('name_required', 'name(姓名)必填');
77
+ if (!/^\d{6,20}$/.test(mobileText))
78
+ throw ehrValidationError('mobile_invalid', 'mobile 必须是 6-20 位数字手机号');
79
+ return { name: nameText, mobile: mobileText };
80
+ }
81
+ function randomHex32() {
82
+ const bytes = new Uint8Array(16);
83
+ globalThis.crypto.getRandomValues(bytes);
84
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
85
+ }
86
+ function randomHex6() {
87
+ const bytes = new Uint8Array(3);
88
+ globalThis.crypto.getRandomValues(bytes);
89
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
90
+ }
91
+ function cacheObjId(cache, logic, purpose) {
92
+ const objId = cache.forms[logic]?.objId;
93
+ if (!objId) {
94
+ throw ehrValidationError('recruit_form_missing', `环境缓存缺少表单 ${logic} 的 objId(${purpose}可能未启用)`);
95
+ }
96
+ return objId;
97
+ }
98
+ function cacheFormId(cache, logic) {
99
+ const entry = cache.forms[logic];
100
+ return entry?.formId || entry?.objId || '';
101
+ }
102
+ /** 表单类 esb 按钮解析(POST getFormButtons type=1)→ 扁平 actions[](带 _btn=按钮名) */
103
+ async function getFormActions(host, objId) {
104
+ const response = (await host.request({
105
+ path: '/api/ebuilder/form/data/getFormButtons',
106
+ body: {
107
+ layoutType: 'formLayout', objId, dataId: '', formDataId: '',
108
+ type: '1', fromRecycle: false, otherOperator: '', objectAppId: '',
109
+ browser: 'Chrome', isTamper: '0',
110
+ },
111
+ }));
112
+ const buttons = response?.data?.buttons || (Array.isArray(response?.data) ? response.data : []);
113
+ const actions = [];
114
+ for (const button of buttons) {
115
+ for (const action of button?.actions || [])
116
+ actions.push({ ...action, _btn: button.name });
117
+ }
118
+ return actions;
119
+ }
120
+ /** 列表类 esb 按钮解析(催评):GET getListButtons/{listId}?passid */
121
+ async function getListActions(host, listId, passId) {
122
+ const response = (await host.request({
123
+ path: `/api/ebuilder/form/list/getListButtons/${listId}?passid=${passId}`,
124
+ }));
125
+ const buttons = response?.data?.buttons || (Array.isArray(response?.data) ? response.data : []);
126
+ const actions = [];
127
+ for (const button of buttons) {
128
+ for (const action of button?.actions || [])
129
+ actions.push({ ...action, _btn: button.name });
130
+ }
131
+ return actions;
132
+ }
133
+ /** 从 actions 挑 actionType=esb 且名称匹配(含 include 关键词、排除 exclude)的 id */
134
+ function pickEsbActionId(actions, include, exclude = []) {
135
+ const match = (name) => {
136
+ if (!name)
137
+ return false;
138
+ if (exclude.some((word) => name.includes(word)))
139
+ return false;
140
+ return include.some((word) => name.includes(word));
141
+ };
142
+ for (const action of actions) {
143
+ if (action.actionType === 'esb' && match(action.name || action._btn || action.text))
144
+ return String(action.id);
145
+ }
146
+ return null;
147
+ }
148
+ /** 找不到 esb 按钮 → 明确阻断(不编造 esbActionIds) */
149
+ function requireEsbActionId(objId, include, exclude = []) {
150
+ throw ehrValidationError('esb_action_unresolved', `当前环境未取到动作流配置(按钮名称需含:${include.join('/')},objId=${objId}),请确认对应表单已部署或联系管理员`);
151
+ }
152
+ /** 触发动作流 runEsb(dataIds=目标记录 id,esbActionIds=按钮配置解析) */
153
+ async function runEsb(host, objId, dataIds, esbActionIds) {
154
+ const response = (await host.request({
155
+ path: `/api/ebuilder/form/esb/runEsb?objId=${objId}`,
156
+ body: { dataIds, esbActionIds, objId },
157
+ }));
158
+ return response?.data || {};
159
+ }
160
+ /** ds/options 运行时解析枚举(面试方式/轮次/结果等):name → optionId */
161
+ async function getDsOptions(host, objId, fieldId, groupId) {
162
+ const response = (await host.request({
163
+ path: '/api/ebuilder/common/ds/options',
164
+ query: { fieldName: fieldId, objId, sourceType: 'FORM', groupId },
165
+ }));
166
+ const map = {};
167
+ for (const option of response?.data || []) {
168
+ if (option?.name && option?.id)
169
+ map[option.name] = String(option.id);
170
+ }
171
+ return map;
172
+ }
173
+ /** 人员名称 → userId(门户字段 browser/complete/resource) */
174
+ async function resolvePersonId(host, fieldId, keyword) {
175
+ const response = (await host.request({
176
+ path: '/api/ebuilder/form/common/browser/complete/resource',
177
+ body: { formParam: { fieldId, searchValue: keyword }, 'formParam.fieldId': fieldId, 'formParam.searchValue': keyword },
178
+ }));
179
+ const list = response?.data?.data || response?.data || [];
180
+ const first = list[0];
181
+ return first ? String(first.id || first.value || '') || null : null;
182
+ }
183
+ /** 安排面试专用:面试计划门户字段 id 映射(talent/candidate_manage/interviewer/position...) */
184
+ async function getInterviewPortalFieldIds(host) {
185
+ const response = (await host.request({
186
+ path: '/api/recruit/candidate/getFieldIdsForInterview',
187
+ }));
188
+ return response?.data || {};
189
+ }
190
+ /** saveFormData:新建(rightType 0)或更新(rightType 2,带 formDataId/updateTimeStamp/layoutId) */
191
+ async function saveFormData(host, params) {
192
+ const layoutId = params.layoutId || params.formId; // getFormLayout GET 405 不可靠,新建回退 formId(与 mjs 一致)
193
+ const customParam = {
194
+ objId: params.objId,
195
+ name: params.name,
196
+ firstDataName: params.name,
197
+ isFlowEnd: false,
198
+ otherOperator: '',
199
+ dataStatus: 'submit',
200
+ rightType: params.rightType || '0',
201
+ appId: params.appId,
202
+ };
203
+ const formData = {
204
+ module: 'ebuilderform',
205
+ client: 'pc',
206
+ dataStatus: 'submit',
207
+ form: { id: params.formId },
208
+ formLayout: { id: layoutId },
209
+ dataDetails: params.dataDetails,
210
+ };
211
+ if (params.formDataId)
212
+ formData.id = params.formDataId;
213
+ if (params.updateTimeStamp)
214
+ formData.updateTimeStamp = params.updateTimeStamp;
215
+ const response = (await host.request({
216
+ path: `/api/ebuilder/form/form/core/saveFormData?formId=${params.formId}`,
217
+ body: { module: 'ebuilderform', appId: params.appId, customParam, formData },
218
+ }));
219
+ if (!response?.status && response?.code !== 200) {
220
+ throw ehrValidationError('save_failed', `saveFormData 失败: ${response?.msg || JSON.stringify(response).slice(0, 200)}`);
221
+ }
222
+ return String(response?.data?.id || '');
223
+ }
224
+ /** 取记录当前数据(updateTimeStamp 乐观锁 + dataDetails 全字段 + 编辑布局 id) */
225
+ async function getFormDataRecord(host, formId, dataId, objId) {
226
+ const response = (await host.request({
227
+ path: `/api/ebuilder/form/form/core/getFormDataAndLayout?formId=${formId}&dataId=${dataId}`,
228
+ method: 'POST',
229
+ body: {
230
+ terminalType: 'PC', dataId, formId, module: 'ebuilderform', newLayout: true,
231
+ cacheUid: '', layoutMultiId: '', dataCrossLayout: false, permissionId: '',
232
+ allReadOnly: false, initialDatas: [], dynamicData: false, cost: false,
233
+ customParam: { objId },
234
+ },
235
+ }));
236
+ const formData = response?.data?.formData || {};
237
+ return {
238
+ id: String(formData.id || ''),
239
+ updateTimeStamp: String(formData.updateTimeStamp || ''),
240
+ dataDetails: formData.dataDetails || [],
241
+ formLayoutId: String(response?.data?.formLayout?.id || ''),
242
+ };
243
+ }
244
+ /** getFormDataAndLayout 返回的 dataDetails → saveFormData 可提交结构 */
245
+ function toSubmitDetails(details) {
246
+ const out = [];
247
+ for (const item of details || []) {
248
+ const fieldId = item?.fieldId || item?.formField?.id;
249
+ if (!fieldId)
250
+ continue;
251
+ const entry = { formField: { id: fieldId } };
252
+ if (item.dataOptions)
253
+ entry.dataOptions = item.dataOptions;
254
+ if (item.dataText)
255
+ entry.dataText = item.dataText;
256
+ if (item.value !== undefined)
257
+ entry.dataText = { content: String(item.value) };
258
+ if (!entry.dataOptions && !entry.dataText)
259
+ continue;
260
+ out.push(entry);
261
+ }
262
+ return out;
263
+ }
264
+ /** 候选人定位(安排面试/催评):hasRights + prepareQuery → batchId,OtherInfo → 姓名/职位/应聘管理 id */
265
+ async function resolveCandidateForWrite(host, cache, talentId) {
266
+ const talentListId = cache.lists.talent?.listId || '';
267
+ const extra = talentListId ? { ebBusinessId: talentListId } : {};
268
+ const rightsResponse = (await host.request({
269
+ path: `/api/recruit/talent/hasRights?formDataId=${talentId}`,
270
+ method: 'POST',
271
+ headers: extra,
272
+ }));
273
+ if (!((rightsResponse?.data || []).includes('READ'))) {
274
+ throw ehrValidationError('recruit_rights_denied', '当前账号无该人才的 READ 权限,无法执行写操作');
275
+ }
276
+ const rk = randomHex6();
277
+ const prepareResponse = (await host.request({
278
+ path: `/api/recruit/candidate/getCandidateCardPrepareQuery?talentId=${talentId}&randomkey_${rk}=${rk}`,
279
+ headers: extra,
280
+ }));
281
+ const batchId = prepareResponse?.data?.batchId;
282
+ if (!batchId) {
283
+ throw ehrValidationError('recruit_batch_id_missing', '未取到 batchId(prepareQuery 返回异常),无法定位候选人应聘批次');
284
+ }
285
+ const v = randomHex6();
286
+ const otherResponse = (await host.request({
287
+ path: `/api/recruit/candidate/getCandidateCardOtherInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_ab=ab`,
288
+ headers: extra,
289
+ }));
290
+ const batch = otherResponse?.data?.currentCandidateBatch || {};
291
+ let name = batch?.talent?.name || '';
292
+ if (!name) {
293
+ const baseResponse = (await host.request({
294
+ path: `/api/recruit/candidate/getCandidateCardBaseInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_cd=cd`,
295
+ headers: extra,
296
+ }));
297
+ name = baseResponse?.data?.talent?.hrm?.name || '';
298
+ }
299
+ return {
300
+ name,
301
+ candidateManageId: batch?.id || batch?.candidateManageId || null,
302
+ positionId: batch?.position?.id || null,
303
+ positionName: batch?.position?.name || null,
304
+ batchId,
305
+ };
306
+ }
307
+ /** EB 列表查询体(写操作定位记录用,同 recruit-list.mjs baseBody) */
308
+ function writeListBody(params) {
309
+ return {
310
+ current: 1,
311
+ pageSize: params.pageSize,
312
+ tableuid: randomHex32(),
313
+ listId: params.listId,
314
+ searchParamData: { searchType: '3', type: 'and' },
315
+ terminal: 'PC',
316
+ browser: 'Chrome',
317
+ requiredMatch: 1,
318
+ mapResult: 1,
319
+ recheckQuery: 1,
320
+ isFollow: 0,
321
+ isShareToMe: 0,
322
+ filterCondition: {},
323
+ cusMenuId: params.cusMenuId,
324
+ urlPageTitle: params.title,
325
+ menuName: params.menuName,
326
+ };
327
+ }
328
+ function requiredString(args, key, subtype, label) {
329
+ const value = typeof args[key] === 'string' ? args[key].trim() : '';
330
+ if (!value)
331
+ throw ehrValidationError(subtype, `${key}(${label})必填`);
332
+ return value;
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // 安排面试:prepare(候选人定位+枚举/面试官解析+摘要)→ apply(saveFormData 面试计划 → assignedInterview)
336
+ // 迁移自 ehr 技能 recruit-arrange-interview.mjs。面试通知(runEsb 外部通知)默认不触发。
337
+ // ---------------------------------------------------------------------------
338
+ // ---------------------------------------------------------------------------
339
+ // 新建人才:prepare(查重/渠道解析/确认摘要 + continuation)→ apply(saveFormData + initTalentBatch)
340
+ // 迁移自 ehr 技能 recruit-new-candidate.mjs(真机已验证两步链路)。
341
+ // ---------------------------------------------------------------------------
342
+ const candidateCreateSchema = {
343
+ name: { type: 'string', minLength: 1, description: '人才姓名' },
344
+ mobile: { type: 'string', pattern: '^\\d{6,20}$', description: '手机号(建前查重依据)' },
345
+ channel: { type: 'string', description: '渠道名称(如 BOSS直聘),须为数据字典中存在的渠道' },
346
+ };
347
+ const candidateCreatePrepare = {
348
+ name: 'ehr.recruit.candidate.create.prepare',
349
+ resource: 'recruit',
350
+ method: 'candidate.create.prepare',
351
+ action: 'ehr.recruit.candidate.create.prepare',
352
+ summary: '准备新建应聘者:手机号查重 + 渠道解析 + 确认摘要,不写入任何数据',
353
+ capability: 'recruit',
354
+ risk: WRITE,
355
+ requiresConfirmation: true,
356
+ inputSchema: schemaOf({ ...candidateCreateSchema }, { required: ['name', 'mobile'] }),
357
+ handler: async (args, env) => {
358
+ const { name, mobile } = validateNameMobile(args.name, args.mobile);
359
+ const cache = await requireRecruitCache(env);
360
+ const talentObj = cache.forms.talent_pool;
361
+ const fields = (talentObj?.objId && cache.fields[talentObj.objId]) || {};
362
+ if (!fields.candidate_name || !fields.telephone) {
363
+ throw ehrValidationError('recruit_field_missing', '环境缓存缺人才姓名字段(candidate_name)或手机字段(telephone)');
364
+ }
365
+ // 建前查重
366
+ const dup = await checkDuplicateByMobile(env, cache, mobile);
367
+ if (dup.length > 1) {
368
+ throw ehrValidationError('duplicate_ambiguous', `手机号 ${mobile} 存在多条候选(数据异常),请人工核对后再决定,不自动新建`);
369
+ }
370
+ const dupRow = dup[0];
371
+ // 渠道解析(指定了渠道但字典没有 → 明确报错)
372
+ let channelOption = null;
373
+ if (args.channel !== undefined && args.channel !== null && String(args.channel).trim() !== '') {
374
+ channelOption = channelOptionId(cache, String(args.channel).trim());
375
+ }
376
+ const channelName = channelOption ? String(args.channel).trim() : null;
377
+ const dataDetails = [
378
+ { formField: { id: fields.candidate_name }, content: name },
379
+ { formField: { id: fields.telephone }, content: mobile },
380
+ ...(channelOption ? [{ formField: { id: fields.channel }, dataOptions: [{ optionId: channelOption, content: channelName }] }] : []),
381
+ ];
382
+ const preview = {
383
+ action: '新建应聘者',
384
+ name,
385
+ mobile,
386
+ channel: channelName || '(未指定)',
387
+ duplicate: dupRow
388
+ ? `已存在同名/同手机号人才「${dupRow.candidate_name || dupRow.name}」(talentId=${dupRow.form_data_id || dupRow.id})→ apply 将被阻断,不允许新建重复记录`
389
+ : '无重复',
390
+ baseUrl: (await env.host.getSession()).baseUrl,
391
+ };
392
+ const session = await env.host.getSession();
393
+ const token = await env.continuation.issue({
394
+ operation: 'ehr.recruit.candidate.create.apply',
395
+ baseUrl: session.baseUrl,
396
+ profile: session.profile,
397
+ userId: session.userId,
398
+ payload: { name, mobile, channel: channelName, formId: talentObj?.formId || talentObj?.objId, objId: talentObj?.objId, dataDetails },
399
+ fingerprint: fingerprint({ name, mobile, channel: channelName, dataDetails }),
400
+ }, CONTINUATION_TTL_SECONDS * 1000);
401
+ return ehrSuccess('ehr.recruit.candidate.create.prepare', {
402
+ status: 'AWAITING_CONFIRMATION',
403
+ preview,
404
+ willSubmit: { formId: talentObj?.formId || talentObj?.objId, objId: talentObj?.objId, dataDetails },
405
+ continuation: token,
406
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
407
+ });
408
+ },
409
+ };
410
+ const candidateCreateApply = {
411
+ name: 'ehr.recruit.candidate.create.apply',
412
+ resource: 'recruit',
413
+ method: 'candidate.create.apply',
414
+ action: 'ehr.recruit.candidate.create.apply',
415
+ summary: '执行新建应聘者(saveFormData → initTalentBatch);必须先 prepare 并经用户明确确认',
416
+ capability: 'recruit',
417
+ risk: WRITE,
418
+ requiresConfirmation: true,
419
+ inputSchema: schemaOf({
420
+ ...candidateCreateSchema,
421
+ confirm: { const: true },
422
+ continuation: { type: 'string', minLength: 1 },
423
+ }, { required: ['name', 'mobile', 'confirm', 'continuation'] }),
424
+ handler: async (args, env) => {
425
+ requireConfirm(args, 'ehr.recruit.candidate.create.apply');
426
+ const { name, mobile } = validateNameMobile(args.name, args.mobile);
427
+ const lease = await verifyLease(args, 'ehr.recruit.candidate.create.apply', env);
428
+ const payload = (lease.payload || {});
429
+ // 指纹校验:apply 入参必须与 prepare 快照一致(防确认后偷改目标)
430
+ const channelName = args.channel !== undefined && args.channel !== null && String(args.channel).trim() !== ''
431
+ ? String(args.channel).trim()
432
+ : null;
433
+ const currentFingerprint = fingerprint({ name, mobile, channel: channelName, dataDetails: payload.dataDetails });
434
+ if (currentFingerprint !== lease.fingerprint) {
435
+ throw ehrValidationError('target_changed', '输入与 prepare 时的快照不一致(姓名/手机号/渠道被修改),请重新执行 prepare');
436
+ }
437
+ // apply 阶段再查重一次:prepare 后数据可能已被他人写入
438
+ const cache = await requireRecruitCache(env);
439
+ const dup = await checkDuplicateByMobile(env, cache, mobile);
440
+ if (dup.length > 0) {
441
+ const row = dup[0];
442
+ throw ehrValidationError('duplicate_blocked', `查重命中:手机号已存在「${row.candidate_name || row.name}」(talentId=${row.form_data_id || row.id}),不允许新建重复记录。请改为修改该人才信息。`);
443
+ }
444
+ const formId = payload.formId;
445
+ const objId = payload.objId;
446
+ if (!formId || !objId || !Array.isArray(payload.dataDetails)) {
447
+ throw ehrValidationError('continuation_payload_invalid', 'continuation 缺少表单参数(formId/objId/dataDetails),请重新执行 prepare');
448
+ }
449
+ const session = await env.host.getSession();
450
+ const appId = (await requireRecruitCache(env)).appId;
451
+ const customParam = {
452
+ objId,
453
+ name,
454
+ firstDataName: name,
455
+ isFlowEnd: false,
456
+ otherOperator: '',
457
+ dataStatus: 'submit',
458
+ rightType: '0',
459
+ appId,
460
+ };
461
+ const formData = {
462
+ module: 'ebuilderform',
463
+ client: 'pc',
464
+ dataStatus: 'submit',
465
+ form: { id: formId },
466
+ formLayout: { id: formId },
467
+ dataDetails: payload.dataDetails,
468
+ };
469
+ let saveResponse;
470
+ try {
471
+ saveResponse = (await env.host.request({
472
+ path: `/api/ebuilder/form/form/core/saveFormData?formId=${formId}`,
473
+ body: { module: 'ebuilderform', appId, customParam, formData },
474
+ }));
475
+ }
476
+ catch (error) {
477
+ if (isNetworkError(error)) {
478
+ throw ehrPartialError('saveFormData 请求发送后连接中断,是否创建成功不确定,禁止自动重试;请用 candidate.search 按手机号回查确认', { attempted: { name, mobile } }, { stage: 'candidate.create.save' });
479
+ }
480
+ throw error;
481
+ }
482
+ if (!saveResponse?.status && saveResponse?.code !== 200) {
483
+ throw ehrValidationError('save_failed', `saveFormData 失败: ${saveResponse?.msg || JSON.stringify(saveResponse).slice(0, 200)}`);
484
+ }
485
+ const newId = saveResponse?.data?.id;
486
+ if (!newId) {
487
+ throw ehrPartialError('saveFormData 未返回新记录 id,创建结果不确定,禁止自动重试;请用 candidate.search 回查确认', { attempted: { name, mobile } }, { stage: 'candidate.create.save' });
488
+ }
489
+ // 第二步:初始化人才批次(失败不影响记录已创建,但必须如实报告 partial)
490
+ let initMsg = '';
491
+ let initOk = true;
492
+ try {
493
+ const initResponse = (await env.host.request({
494
+ path: `/api/recruit/candidate/initTalentBatch?formDataId=${newId}`,
495
+ method: 'POST',
496
+ }));
497
+ initMsg = initResponse?.data?.msg || initResponse?.msg || '';
498
+ }
499
+ catch (error) {
500
+ initOk = false;
501
+ initMsg = error instanceof Error ? error.message : String(error);
502
+ }
503
+ if (!initOk) {
504
+ throw ehrPartialError(`人才记录已创建(talentId=${newId}),但 initTalentBatch 初始化批次失败:${initMsg}。请人工在 E10 中确认批次状态,禁止自动重试。`, { talentId: newId, initTalentBatchError: initMsg }, { stage: 'candidate.create.init' });
505
+ }
506
+ return ehrSuccess('ehr.recruit.candidate.create.apply', {
507
+ status: 'COMPLETE',
508
+ talentId: newId,
509
+ initTalentBatch: initMsg,
510
+ searchHint: `可用 ehr.recruit.candidate.search 按 name=${name} 或 mobile=${mobile} 回查`,
511
+ });
512
+ },
513
+ };
514
+ // ---------------------------------------------------------------------------
515
+ // 安排面试 operation
516
+ // ---------------------------------------------------------------------------
517
+ const INTERVIEW_TYPES = ['现场面试', '视频面试', '电话面试', 'AI面试'];
518
+ const INTERVIEW_DATAKEYS = ['subject', 'start_time', 'end_time', 'interview_type', 'interview_session', 'interviewer',
519
+ 'position_manage_id', 'talent_id', 'candidate_manage_id', 'mobile', 'email', 'interview_location', 'notice_mould'];
520
+ function defaultInterviewWindow() {
521
+ const pad = (n) => String(n).padStart(2, '0');
522
+ const now = new Date();
523
+ const fmt = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
524
+ const end = new Date(now.getTime() + 60 * 60 * 1000);
525
+ return { start: fmt(now), end: fmt(end) };
526
+ }
527
+ async function buildInterviewPlan(env, cache, args) {
528
+ const host = env.host;
529
+ const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
530
+ if (!/^\d+$/.test(talentId))
531
+ throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
532
+ const interviewer = requiredString(args, 'interviewer', 'interviewer_required', '面试官姓名');
533
+ const type = typeof args.type === 'string' && args.type.trim() ? args.type.trim() : '现场面试';
534
+ if (!INTERVIEW_TYPES.includes(type)) {
535
+ throw ehrValidationError('enum_invalid', `type(面试方式)取值必须是 ${INTERVIEW_TYPES.join(' / ')} 之一`);
536
+ }
537
+ const session = typeof args.session === 'string' && args.session.trim() ? args.session.trim() : '初试';
538
+ const candidate = await resolveCandidateForWrite(host, cache, talentId);
539
+ const positionId = args.positionId ? String(args.positionId) : candidate.positionId;
540
+ if (!positionId) {
541
+ throw ehrValidationError('position_not_resolved', '未定位到应聘职位(positionId),请提供 positionId 参数');
542
+ }
543
+ if (!candidate.candidateManageId) {
544
+ throw ehrValidationError('candidate_manage_missing', '未定位到应聘管理记录(candidateManageId),请确认该人才已有应聘批次');
545
+ }
546
+ const objId = cacheObjId(cache, 'interview_plan', '面试计划表单');
547
+ const formId = cacheFormId(cache, 'interview_plan');
548
+ const fields = cache.fields[objId] || {};
549
+ const portalFields = await getInterviewPortalFieldIds(host);
550
+ const fieldIdOf = (key) => fields[key] || portalFields[key] || '';
551
+ for (const key of INTERVIEW_DATAKEYS) {
552
+ if (!fieldIdOf(key)) {
553
+ throw ehrValidationError('recruit_field_missing', `面试计划表单缺少字段 ${key}(ds/fields 与门户字段均解析不到)`);
554
+ }
555
+ }
556
+ const typeMap = await getDsOptions(host, objId, fieldIdOf('interview_type'), cache.appId);
557
+ const typeOptionId = typeMap[type];
558
+ if (!typeOptionId) {
559
+ throw ehrValidationError('enum_invalid', `面试方式「${type}」不在枚举中(${Object.keys(typeMap).join(' / ')})`);
560
+ }
561
+ const sessionMap = await getDsOptions(host, objId, fieldIdOf('interview_session'), cache.appId);
562
+ const sessionOptionId = sessionMap[session];
563
+ if (!sessionOptionId) {
564
+ throw ehrValidationError('enum_invalid', `面试轮次「${session}」不在枚举中(${Object.keys(sessionMap).join(' / ')})`);
565
+ }
566
+ const interviewerId = await resolvePersonId(host, fieldIdOf('interviewer'), interviewer);
567
+ if (!interviewerId) {
568
+ throw ehrValidationError('interviewer_not_found', `面试官「${interviewer}」解析不到 userId(browser/complete/resource 无匹配)`);
569
+ }
570
+ const window = defaultInterviewWindow();
571
+ const start = typeof args.startTime === 'string' && args.startTime.trim() ? args.startTime.trim() : window.start;
572
+ const end = typeof args.endTime === 'string' && args.endTime.trim() ? args.endTime.trim() : window.end;
573
+ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(start) || !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(end)) {
574
+ throw ehrValidationError('time_format_invalid', 'startTime/endTime 格式必须是 YYYY-MM-DD HH:mm');
575
+ }
576
+ const subject = (candidate.positionName ? `${candidate.positionName}-` : '') + candidate.name + '的面试';
577
+ const dataDetails = [
578
+ { formField: { id: fieldIdOf('subject') }, content: subject },
579
+ { formField: { id: fieldIdOf('start_time') }, content: start },
580
+ { formField: { id: fieldIdOf('end_time') }, content: end },
581
+ { formField: { id: fieldIdOf('interview_type') }, dataOptions: [{ optionId: typeOptionId, content: type }] },
582
+ { formField: { id: fieldIdOf('interview_session') }, dataOptions: [{ optionId: sessionOptionId, content: session }] },
583
+ { formField: { id: fieldIdOf('interviewer') }, dataOptions: [{ optionId: interviewerId, content: interviewer }] },
584
+ { formField: { id: fieldIdOf('position_manage_id') }, dataOptions: [{ optionId: positionId, content: candidate.positionName }] },
585
+ { formField: { id: fieldIdOf('talent_id') }, dataOptions: [{ optionId: talentId, content: candidate.name }] },
586
+ { formField: { id: fieldIdOf('candidate_manage_id') }, dataOptions: [{ optionId: candidate.candidateManageId, content: candidate.candidateManageId }] },
587
+ ];
588
+ return {
589
+ formId, objId, dataDetails,
590
+ candidateName: candidate.name,
591
+ positionName: candidate.positionName,
592
+ interviewTime: `${start} ~ ${end}`,
593
+ interviewer,
594
+ };
595
+ }
596
+ const interviewArrangeSchema = {
597
+ talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID(candidate.search 返回的 talentId)' },
598
+ interviewer: { type: 'string', description: '面试官姓名(须可解析为系统人员)' },
599
+ type: { type: 'string', enum: [...INTERVIEW_TYPES], description: '面试方式,默认现场面试' },
600
+ session: { type: 'string', description: '面试轮次(如 初试/复试),默认初试;须在枚举内' },
601
+ positionId: { type: 'string', description: '应聘职位 ID;不填时从人才当前应聘批次继承' },
602
+ startTime: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$', description: '开始时间 YYYY-MM-DD HH:mm,默认当前时间' },
603
+ endTime: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$', description: '结束时间,默认开始时间 +1 小时' },
604
+ notify: { type: 'boolean', description: '是否触发「新建日程」面试通知动作流(外部通知),默认 false' },
605
+ };
606
+ const interviewArrangePrepare = {
607
+ name: 'ehr.recruit.candidate.interview.arrange.prepare',
608
+ resource: 'recruit',
609
+ method: 'candidate.interview.arrange.prepare',
610
+ action: 'ehr.recruit.candidate.interview.arrange.prepare',
611
+ summary: '准备安排面试:候选人定位 + 面试方式/轮次/面试官解析 + 确认摘要,不写入任何数据',
612
+ capability: 'recruit',
613
+ risk: WRITE,
614
+ requiresConfirmation: true,
615
+ inputSchema: schemaOf({ ...interviewArrangeSchema }, { required: ['talentId', 'interviewer'] }),
616
+ handler: async (args, env) => {
617
+ const cache = await requireRecruitCache(env);
618
+ const plan = await buildInterviewPlan(env, cache, args);
619
+ const session = await env.host.getSession();
620
+ const notify = args.notify === true;
621
+ const payload = { ...args, formId: plan.formId, objId: plan.objId, dataDetails: plan.dataDetails, notify };
622
+ const token = await env.continuation.issue({
623
+ operation: 'ehr.recruit.candidate.interview.arrange.apply',
624
+ baseUrl: session.baseUrl,
625
+ profile: session.profile,
626
+ userId: session.userId,
627
+ payload,
628
+ fingerprint: fingerprint({ dataDetails: plan.dataDetails, notify }),
629
+ }, CONTINUATION_TTL_SECONDS * 1000);
630
+ return ehrSuccess('ehr.recruit.candidate.interview.arrange.prepare', {
631
+ status: 'AWAITING_CONFIRMATION',
632
+ preview: {
633
+ action: '安排面试',
634
+ candidate: `${plan.candidateName}(talentId=${args.talentId})`,
635
+ position: plan.positionName || '(未解析到)',
636
+ type: args.type || '现场面试',
637
+ session: args.session || '初试',
638
+ time: plan.interviewTime,
639
+ interviewer: plan.interviewer,
640
+ notify: notify ? '将触发「新建日程」面试通知动作流(外部通知)' : '面试通知默认不触发',
641
+ },
642
+ willSubmit: { formId: plan.formId, objId: plan.objId, dataDetails: plan.dataDetails },
643
+ continuation: token,
644
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
645
+ });
646
+ },
647
+ };
648
+ const interviewArrangeApply = {
649
+ name: 'ehr.recruit.candidate.interview.arrange.apply',
650
+ resource: 'recruit',
651
+ method: 'candidate.interview.arrange.apply',
652
+ action: 'ehr.recruit.candidate.interview.arrange.apply',
653
+ summary: '执行安排面试(saveFormData 面试计划 → assignedInterview 分配;notify=true 时先触发通知动作流);必须先 prepare 并经用户明确确认',
654
+ capability: 'recruit',
655
+ risk: WRITE,
656
+ requiresConfirmation: true,
657
+ inputSchema: schemaOf({ ...interviewArrangeSchema, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['talentId', 'interviewer', 'confirm', 'continuation'] }),
658
+ handler: async (args, env) => {
659
+ requireConfirm(args, 'ehr.recruit.candidate.interview.arrange.apply');
660
+ const lease = await verifyLease(args, 'ehr.recruit.candidate.interview.arrange.apply', env);
661
+ const payload = (lease.payload || {});
662
+ const cache = await requireRecruitCache(env);
663
+ const plan = await buildInterviewPlan(env, cache, args);
664
+ if (fingerprint({ dataDetails: plan.dataDetails, notify: args.notify === true }) !== lease.fingerprint) {
665
+ throw ehrValidationError('target_changed', '输入与 prepare 时的快照不一致(候选人/面试官/时间等被修改),请重新执行 prepare');
666
+ }
667
+ const formId = payload.formId || plan.formId;
668
+ const objId = payload.objId || plan.objId;
669
+ const notify = args.notify === true;
670
+ // ② 面试通知动作流(外部通知,仅在 notify=true 时触发)
671
+ let notifyResult = null;
672
+ if (notify) {
673
+ const actions = await getFormActions(env.host, objId);
674
+ const esbId = pickEsbActionId(actions, ['新建日程']);
675
+ if (!esbId)
676
+ requireEsbActionId(objId, ['新建日程']);
677
+ }
678
+ // ① 新建面试计划
679
+ let planFormId;
680
+ try {
681
+ planFormId = await saveFormData(env.host, {
682
+ appId: cache.appId, formId, objId,
683
+ name: `面试管理-${plan.candidateName} ${plan.interviewTime}`,
684
+ dataDetails: plan.dataDetails,
685
+ });
686
+ }
687
+ catch (error) {
688
+ if (isNetworkError(error)) {
689
+ throw ehrPartialError('saveFormData 请求发送后连接中断,面试计划是否创建不确定,禁止自动重试;请用 candidate.card 回查确认', { attempted: { talentId: args.talentId } }, { stage: 'interview.arrange.save' });
690
+ }
691
+ throw error;
692
+ }
693
+ if (!planFormId) {
694
+ throw ehrPartialError('saveFormData 未返回面试计划记录 id,创建结果不确定,禁止自动重试', { attempted: { talentId: args.talentId } }, { stage: 'interview.arrange.save' });
695
+ }
696
+ if (notify) {
697
+ const actions = await getFormActions(env.host, objId);
698
+ const esbId = pickEsbActionId(actions, ['新建日程']);
699
+ if (esbId)
700
+ notifyResult = await runEsb(env.host, objId, planFormId, esbId);
701
+ }
702
+ // ③ 分配面试(失败不影响计划已创建 → partial 如实报告)
703
+ let assigned;
704
+ try {
705
+ assigned = (await env.host.request({
706
+ path: `/api/recruit/candidate/assignedInterview?formDataId=${planFormId}`,
707
+ method: 'POST',
708
+ }));
709
+ }
710
+ catch (error) {
711
+ if (isNetworkError(error)) {
712
+ throw ehrPartialError(`面试计划已创建(id=${planFormId}),但 assignedInterview 请求连接中断,分配结果不确定,禁止自动重试;请人工在 E10 面试管理确认`, { interviewFormId: planFormId }, { stage: 'interview.arrange.assign' });
713
+ }
714
+ throw error;
715
+ }
716
+ return ehrSuccess('ehr.recruit.candidate.interview.arrange.apply', {
717
+ status: 'COMPLETE',
718
+ interviewFormId: planFormId,
719
+ notify: notifyResult,
720
+ assigned: assigned?.data || {},
721
+ candidate: `${plan.candidateName}(talentId=${args.talentId})`,
722
+ time: plan.interviewTime,
723
+ interviewer: plan.interviewer,
724
+ });
725
+ },
726
+ };
727
+ // ---------------------------------------------------------------------------
728
+ // 发送 Offer 邮件 operation(L3 外部通知,四步:estValidateOffer → saveFormData → sendOffer → runEsb)
729
+ // 迁移自 ehr 技能 recruit-send-offer.mjs。
730
+ // ---------------------------------------------------------------------------
731
+ async function locateOfferRow(host, cache, talentId) {
732
+ const offerList = cache.lists.offer;
733
+ if (!offerList?.listId) {
734
+ throw ehrValidationError('recruit_list_unavailable', '当前环境未启用「录用管理」(Offer 管理)功能菜单');
735
+ }
736
+ const offerObjId = cacheObjId(cache, 'offer', '录用管理');
737
+ const body = writeListBody({
738
+ listId: offerList.listId, cusMenuId: offerList.cusMenuId,
739
+ menuName: offerList.menuName || '录用管理', title: '录用管理', pageSize: 50,
740
+ });
741
+ const response = (await host.request({
742
+ path: `/api/ebuilder/form/list/getListDatas?passid=${offerObjId}&cid=${offerObjId}`,
743
+ body,
744
+ headers: { ebBusinessId: offerList.listId },
745
+ }));
746
+ const rows = response?.data?.data || [];
747
+ const row = rows.find((item) => String(item.talentId) === String(talentId));
748
+ if (!row) {
749
+ throw ehrValidationError('offer_not_found', `未在 Offer 管理列表找到 talentId=${talentId} 的 Offer 记录(offer 流程可能未走完,无法发 Offer)`);
750
+ }
751
+ if (!row.offer_id) {
752
+ throw ehrValidationError('offer_id_missing', '该候选人 Offer 记录缺少 offer_id,无法发送(offer 流程可能未走完)');
753
+ }
754
+ const status = Array.isArray(row.offer_status_select)
755
+ ? (row.offer_status_select[0]?.name || '')
756
+ : String(row.offer_status_select || '');
757
+ return { ...row, __status: status };
758
+ }
759
+ async function resolveOfferTemplate(host, cache, templateName) {
760
+ const response = (await host.request({
761
+ path: '/api/ebuilder/form/obj/getObjsByTag',
762
+ query: { appId: cache.appId, tag: 'uf_rcrt_notify_mould' },
763
+ }));
764
+ const mouldObjId = Array.isArray(response?.data) && response.data.length ? String(response.data[0].id) : '';
765
+ if (!mouldObjId) {
766
+ throw ehrValidationError('template_form_missing', '当前环境未取到邮件通知模板表单(uf_rcrt_notify_mould),无法解析 Offer 邮件模板');
767
+ }
768
+ const allData = (await host.request({
769
+ path: '/api/ebuilder/form/dataset/v2/getAllData',
770
+ body: { datajson: { header: { objId: mouldObjId }, mainTable: {}, pageInfo: { pageNo: 1, pageSize: 200 } } },
771
+ }));
772
+ const datas = allData?.data?.datajson?.datas || [];
773
+ const hit = datas.find((item) => {
774
+ const main = item.mainTable || {};
775
+ const name = main.mould_name?.fieldValue || '';
776
+ const type = Array.isArray(main.mould_type) ? (main.mould_type[0]?.fieldOptionName || '') : '';
777
+ return name === templateName && (type === '发送offer' || type === '');
778
+ });
779
+ if (!hit) {
780
+ const names = datas.map((item) => item.mainTable?.mould_name?.fieldValue).filter(Boolean);
781
+ throw ehrValidationError('template_not_found', `未找到邮件模板「${templateName}」(可选:${names.join('、')}),请确认模板名称`);
782
+ }
783
+ return {
784
+ id: String(hit.mainTable.id.fieldValue),
785
+ name: templateName,
786
+ subject: hit.mainTable.email_subject?.fieldValue || '',
787
+ };
788
+ }
789
+ const offerSendSchema = {
790
+ talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID' },
791
+ email: { type: 'string', description: '收件邮箱;不填时使用 Offer 记录上的邮箱' },
792
+ deadline: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$', description: 'Offer 失效日期 YYYY-MM-DD;不填时使用 Offer 记录的入职时间' },
793
+ days: { type: 'integer', minimum: 1, description: '确认有效天数,默认 5' },
794
+ template: { type: 'string', description: '邮件模板名称(uf_rcrt_notify_mould 表 mould_name),默认 offer通知' },
795
+ };
796
+ async function buildOfferPlan(env, cache, args) {
797
+ const host = env.host;
798
+ const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
799
+ if (!/^\d+$/.test(talentId))
800
+ throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
801
+ const row = await locateOfferRow(host, cache, talentId);
802
+ if (row.__status !== '未发送') {
803
+ throw ehrValidationError('offer_status_invalid', `该 Offer 当前状态为「${row.__status}」,不是「未发送」——再次发送会重复打扰候选人,请先向用户说明并获确认后再继续`);
804
+ }
805
+ const toEmail = typeof args.email === 'string' && args.email.trim() ? args.email.trim() : String(row.email || '');
806
+ if (!toEmail) {
807
+ throw ehrValidationError('email_required', 'Offer 记录无收件邮箱且对话未提供 email,无法发送(邮箱必填)');
808
+ }
809
+ const templateName = typeof args.template === 'string' && args.template.trim() ? args.template.trim() : 'offer通知';
810
+ const template = await resolveOfferTemplate(host, cache, templateName);
811
+ const deadline = typeof args.deadline === 'string' && args.deadline.trim() ? args.deadline.trim() : String(row.entry_time || '');
812
+ if (!deadline) {
813
+ throw ehrValidationError('deadline_required', '未提供 offer 失效日期(deadline YYYY-MM-DD),无法发送');
814
+ }
815
+ const days = typeof args.days === 'number' && Number.isFinite(args.days) ? Math.floor(args.days) : 5;
816
+ const fbObjId = cacheObjId(cache, 'offer_sending', 'offer反馈表单');
817
+ const fbFormId = cacheFormId(cache, 'offer_sending');
818
+ const fields = cache.fields[fbObjId] || {};
819
+ for (const key of ['talent_name', 'email', 'phone', 'offer_mod_id', 'offer_exp_date', 'send_msg', 'offer_id', 'exp_days', 'can_edit_exp']) {
820
+ if (!fields[key]) {
821
+ throw ehrValidationError('recruit_field_missing', `offer反馈 表单缺少字段映射 ${key}(环境缓存未含该表单字段)`);
822
+ }
823
+ }
824
+ // getDataName:取 Offer 记录标题(关联字段 content 用)
825
+ let offerName = 'Offer';
826
+ try {
827
+ const nameResponse = (await host.request({
828
+ path: '/api/ebuilder/form/data/getDataName',
829
+ body: { id: String(row.offer_id), objId: cacheObjId(cache, 'offer', '录用管理') },
830
+ }));
831
+ offerName = nameResponse?.data?.dataInfo?.dataName || 'Offer';
832
+ }
833
+ catch { /* 回退 'Offer' */ }
834
+ const dataDetails = [
835
+ { formField: { id: fields.talent_name }, content: row.candidate_name },
836
+ { formField: { id: fields.email }, content: toEmail },
837
+ { formField: { id: fields.send_msg }, dataOptions: [{ optionId: '0', content: '关闭' }] },
838
+ { formField: { id: fields.exp_days }, content: String(days) },
839
+ { formField: { id: fields.can_edit_exp }, dataOptions: [{ optionId: '0', content: '不可编辑' }] },
840
+ { formField: { id: fields.offer_exp_date }, content: deadline },
841
+ { formField: { id: fields.offer_mod_id }, dataOptions: [{ optionId: template.id, content: template.name }] },
842
+ { formField: { id: fields.offer_id }, dataOptions: [{ optionId: String(row.offer_id), content: offerName }] },
843
+ ];
844
+ return {
845
+ fbObjId, fbFormId, dataDetails,
846
+ offerId: String(row.offer_id), offerName, toEmail, deadline, days,
847
+ templateName, candidateName: String(row.candidate_name || row.name || ''),
848
+ };
849
+ }
850
+ const offerSendPrepare = {
851
+ name: 'ehr.recruit.offer.send.prepare',
852
+ resource: 'recruit',
853
+ method: 'offer.send.prepare',
854
+ action: 'ehr.recruit.offer.send.prepare',
855
+ summary: '准备发送 Offer 邮件(L3 外部通知):Offer 定位 + 状态/邮箱/模板/失效日期校验 + 确认摘要,不写入不发送',
856
+ capability: 'recruit',
857
+ risk: WRITE,
858
+ requiresConfirmation: true,
859
+ inputSchema: schemaOf({ ...offerSendSchema }, { required: ['talentId'] }),
860
+ handler: async (args, env) => {
861
+ const cache = await requireRecruitCache(env);
862
+ const plan = await buildOfferPlan(env, cache, args);
863
+ const session = await env.host.getSession();
864
+ const token = await env.continuation.issue({
865
+ operation: 'ehr.recruit.offer.send.apply',
866
+ baseUrl: session.baseUrl,
867
+ profile: session.profile,
868
+ userId: session.userId,
869
+ payload: { ...args, fbObjId: plan.fbObjId, fbFormId: plan.fbFormId, dataDetails: plan.dataDetails, offerId: plan.offerId },
870
+ fingerprint: fingerprint({ dataDetails: plan.dataDetails, offerId: plan.offerId }),
871
+ }, CONTINUATION_TTL_SECONDS * 1000);
872
+ return ehrSuccess('ehr.recruit.offer.send.prepare', {
873
+ status: 'AWAITING_CONFIRMATION',
874
+ preview: {
875
+ action: '发送 Offer 邮件(L3 外部通知)',
876
+ candidate: `${plan.candidateName}(talentId=${args.talentId})`,
877
+ offerId: plan.offerId,
878
+ toEmail: plan.toEmail,
879
+ template: plan.templateName,
880
+ deadline: plan.deadline,
881
+ days: plan.days,
882
+ note: '确认后才执行 estValidateOffer → saveFormData → sendOffer → 动作流四步;邮件会真实发送给候选人',
883
+ },
884
+ willSubmit: { formId: plan.fbFormId, objId: plan.fbObjId, dataDetails: plan.dataDetails },
885
+ continuation: token,
886
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
887
+ });
888
+ },
889
+ };
890
+ const offerSendApply = {
891
+ name: 'ehr.recruit.offer.send.apply',
892
+ resource: 'recruit',
893
+ method: 'offer.send.apply',
894
+ action: 'ehr.recruit.offer.send.apply',
895
+ summary: '执行发送 Offer 邮件(estValidateOffer → 新建 offer反馈 → sendOffer → 动作流);必须先 prepare 并经用户明确确认',
896
+ capability: 'recruit',
897
+ risk: WRITE,
898
+ requiresConfirmation: true,
899
+ inputSchema: schemaOf({ ...offerSendSchema, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['talentId', 'confirm', 'continuation'] }),
900
+ handler: async (args, env) => {
901
+ requireConfirm(args, 'ehr.recruit.offer.send.apply');
902
+ const lease = await verifyLease(args, 'ehr.recruit.offer.send.apply', env);
903
+ const payload = (lease.payload || {});
904
+ if (!payload.fbObjId || !payload.fbFormId || !Array.isArray(payload.dataDetails) || !payload.offerId) {
905
+ throw ehrValidationError('continuation_payload_invalid', 'continuation 缺少表单参数,请重新执行 prepare');
906
+ }
907
+ const cache = await requireRecruitCache(env);
908
+ // ① 服务端校验(estValidateOffer)
909
+ let validateResponse;
910
+ try {
911
+ validateResponse = (await env.host.request({
912
+ path: `/api/recruit/estPre/estValidateOffer?offerId=${payload.offerId}`,
913
+ method: 'POST',
914
+ }));
915
+ }
916
+ catch (error) {
917
+ if (isNetworkError(error)) {
918
+ throw ehrPartialError('estValidateOffer 请求连接中断,校验结果不确定,禁止自动重试', { offerId: payload.offerId }, { stage: 'offer.send.validate' });
919
+ }
920
+ throw error;
921
+ }
922
+ if (validateResponse?.data?.validateSuccess !== true) {
923
+ throw ehrValidationError('offer_validate_failed', `estValidateOffer 校验未通过:${validateResponse?.msg || '未知原因'}`);
924
+ }
925
+ // ② 新建 offer反馈 记录
926
+ let fbId;
927
+ try {
928
+ fbId = await saveFormData(env.host, {
929
+ appId: cache.appId, formId: payload.fbFormId, objId: payload.fbObjId,
930
+ name: `offer反馈-${payload.dataDetails[0]?.content || ''} ${new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-')}`,
931
+ dataDetails: payload.dataDetails,
932
+ });
933
+ }
934
+ catch (error) {
935
+ if (isNetworkError(error)) {
936
+ throw ehrPartialError('saveFormData(offer反馈)请求连接中断,是否写入不确定,禁止自动重试;请回查 Offer 状态', { offerId: payload.offerId }, { stage: 'offer.send.save' });
937
+ }
938
+ throw error;
939
+ }
940
+ if (!fbId) {
941
+ throw ehrPartialError('新建 offer反馈 记录失败(未返回 id),禁止自动重试', { offerId: payload.offerId }, { stage: 'offer.send.save' });
942
+ }
943
+ // ③ 发送
944
+ let sendResponse;
945
+ try {
946
+ sendResponse = (await env.host.request({
947
+ path: `/api/recruit/offer/sendOffer?formDataId=${fbId}`,
948
+ method: 'POST',
949
+ }));
950
+ }
951
+ catch (error) {
952
+ if (isNetworkError(error)) {
953
+ throw ehrPartialError(`offer反馈记录已创建(id=${fbId}),但 sendOffer 请求连接中断,邮件是否已发出不确定,禁止自动重试;请回查 offer_status_select`, { feedbackRecordId: fbId }, { stage: 'offer.send.sendOffer' });
954
+ }
955
+ throw error;
956
+ }
957
+ if (sendResponse?.status !== true) {
958
+ throw ehrPartialError(`sendOffer 失败:${sendResponse?.msg || JSON.stringify(sendResponse).slice(0, 200)}。offer反馈记录(id=${fbId})已存在,请人工核对后处理,禁止自动重试`, { feedbackRecordId: fbId }, { stage: 'offer.send.sendOffer' });
959
+ }
960
+ // ④ 触发发信动作流
961
+ const actions = await getFormActions(env.host, payload.fbObjId);
962
+ const esbId = pickEsbActionId(actions, ['更新offer发送信息']);
963
+ if (!esbId)
964
+ requireEsbActionId(payload.fbObjId, ['更新offer发送信息']);
965
+ const esbResult = await runEsb(env.host, payload.fbObjId, fbId, esbId);
966
+ return ehrSuccess('ehr.recruit.offer.send.apply', {
967
+ status: 'COMPLETE',
968
+ feedbackRecordId: fbId,
969
+ validateOffer: '通过',
970
+ sendOffer: 'SUCCESS',
971
+ runEsb: { successNum: esbResult.successNum, failNum: esbResult.failNum, msg: esbResult.successMsg },
972
+ nextStep: '回查 offer_status_select 应已变更为「已发送待反馈」',
973
+ });
974
+ },
975
+ };
976
+ // ---------------------------------------------------------------------------
977
+ // 办理入职 operation(路径 B:对已有待入职记录办理;落账号不可逆)
978
+ // 迁移自 ehr 技能 recruit-process-entry.mjs。
979
+ // ---------------------------------------------------------------------------
980
+ async function inheritFromOffer(host, cache, talentId) {
981
+ const out = {};
982
+ const offerList = cache.lists.offer;
983
+ if (!offerList?.listId)
984
+ return out;
985
+ const offerObjId = cache.forms.offer?.objId;
986
+ if (!offerObjId)
987
+ return out;
988
+ const body = writeListBody({
989
+ listId: offerList.listId, cusMenuId: offerList.cusMenuId,
990
+ menuName: offerList.menuName || '录用管理', title: '录用管理', pageSize: 100,
991
+ });
992
+ const response = (await host.request({
993
+ path: `/api/ebuilder/form/list/getListDatas?passid=${offerObjId}&cid=${offerObjId}`,
994
+ body,
995
+ headers: { ebBusinessId: offerList.listId },
996
+ }));
997
+ const row = (response?.data?.data || []).find((item) => String(item.talentId) === String(talentId));
998
+ if (!row)
999
+ return out;
1000
+ const sub = Array.isArray(row.subcompany) ? row.subcompany[0]?.id : (row.subcompany_id || row.subcompany);
1001
+ const pos = row.position_id || (Array.isArray(row.position) ? row.position[0]?.id : row.position);
1002
+ if (sub)
1003
+ out.subcompany = String(sub);
1004
+ if (pos)
1005
+ out.position = String(pos);
1006
+ if (row.entry_time)
1007
+ out.entryTime = String(row.entry_time);
1008
+ if (row.telephone)
1009
+ out.phone = String(row.telephone);
1010
+ return out;
1011
+ }
1012
+ async function locateEntryRow(host, cache, talentId, entryIdArg) {
1013
+ const entryList = cache.lists.entry;
1014
+ if (!entryList?.listId) {
1015
+ throw ehrValidationError('recruit_list_unavailable', '当前环境未启用「入职管理」功能菜单');
1016
+ }
1017
+ const entryObjId = cacheObjId(cache, 'entry_manage', '入职管理');
1018
+ const inherited = await inheritFromOffer(host, cache, talentId);
1019
+ const phone = inherited.phone;
1020
+ const body = writeListBody({
1021
+ listId: entryList.listId, cusMenuId: entryList.cusMenuId,
1022
+ menuName: entryList.menuName || '入职管理', title: '入职管理', pageSize: 100,
1023
+ });
1024
+ const response = (await host.request({
1025
+ path: `/api/ebuilder/form/list/getListDatas?passid=${entryObjId}&cid=${entryObjId}`,
1026
+ body,
1027
+ headers: { ebBusinessId: entryList.listId },
1028
+ }));
1029
+ const rows = response?.data?.data || [];
1030
+ let row = null;
1031
+ if (entryIdArg)
1032
+ row = rows.find((item) => String(item.form_data_id || item.id) === String(entryIdArg));
1033
+ if (!row && phone)
1034
+ row = rows.find((item) => String(item.telephone) === String(phone));
1035
+ if (!row) {
1036
+ throw ehrValidationError('entry_not_found', `未在入职管理列表定位到入职记录(entryId=${entryIdArg || '—'} / 手机=${phone || '—'}),请确认该候选人已有待入职记录`);
1037
+ }
1038
+ const entryId = String(row.form_data_id || row.id || '');
1039
+ if (!entryId)
1040
+ throw ehrValidationError('entry_id_missing', '入职记录缺少 form_data_id,无法办理');
1041
+ // transact_status:1=待办理 2=已办理
1042
+ const ts = Array.isArray(row.transact_status) ? (row.transact_status[0]?.name || '') : String(row.transact_status || '').replace(/"/g, '');
1043
+ return { entryId, name: String(row.xm || row.name || ''), transactStatus: ts };
1044
+ }
1045
+ const entryProcessSchema = {
1046
+ talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID' },
1047
+ entryId: { type: 'string', description: '入职记录 ID;不填时按 Offer 记录手机号在入职列表匹配' },
1048
+ subcompanyId: { type: 'string', description: '入职分部 ID;不填时从 Offer 记录继承' },
1049
+ departmentId: { type: 'string', description: '入职部门 ID(必填,Offer 记录无此字段)' },
1050
+ positionId: { type: 'string', description: '入职职位 ID;不填时从 Offer 记录继承' },
1051
+ positionRankId: { type: 'string', description: '职级 ID(必填)' },
1052
+ entryTime: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$', description: '入职日期 YYYY-MM-DD;不填时从 Offer 记录继承' },
1053
+ };
1054
+ async function buildEntryPlan(env, cache, args) {
1055
+ const host = env.host;
1056
+ const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
1057
+ if (!/^\d+$/.test(talentId))
1058
+ throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
1059
+ const department = requiredString(args, 'departmentId', 'department_required', '入职部门 ID');
1060
+ const positionRank = requiredString(args, 'positionRankId', 'position_rank_required', '职级 ID');
1061
+ const entry = await locateEntryRow(host, cache, talentId, typeof args.entryId === 'string' ? args.entryId.trim() : '');
1062
+ if (entry.transactStatus === '2') {
1063
+ throw ehrValidationError('entry_already_processed', '该入职记录已办理(transact_status=2),无需重复办理');
1064
+ }
1065
+ const inherited = await inheritFromOffer(host, cache, talentId);
1066
+ const subcompany = (typeof args.subcompanyId === 'string' && args.subcompanyId.trim()) || inherited.subcompany || '';
1067
+ const position = (typeof args.positionId === 'string' && args.positionId.trim()) || inherited.position || '';
1068
+ const entryTime = (typeof args.entryTime === 'string' && args.entryTime.trim()) || inherited.entryTime || '';
1069
+ if (!subcompany)
1070
+ throw ehrValidationError('subcompany_required', '缺少入职分部 subcompanyId(Offer 记录亦无,请显式提供)');
1071
+ if (!position)
1072
+ throw ehrValidationError('position_required', '缺少入职职位 positionId(Offer 记录亦无,请显式提供)');
1073
+ if (!entryTime)
1074
+ throw ehrValidationError('entry_time_required', '缺少入职日期 entryTime(YYYY-MM-DD,Offer 记录亦无)');
1075
+ // ① 服务端校验(estValidateEntry,带 entryId 路径 B)
1076
+ const query = `subcompany=${subcompany}&department=${department}&position=${position}&positionRank=${positionRank}&entryTime=${entryTime}&entryId=${entry.entryId}`;
1077
+ const validateResponse = (await host.request({
1078
+ path: `/api/recruit/estPre/estValidateEntry?${query}`,
1079
+ method: 'POST',
1080
+ }));
1081
+ if (validateResponse?.data?.validateSuccess !== true) {
1082
+ throw ehrValidationError('entry_validate_failed', `estValidateEntry 校验未通过:${validateResponse?.msg || '组织架构组合有误'}`);
1083
+ }
1084
+ // 读取表单现状(updateTimeStamp 乐观锁 / 编辑布局 / 现有字段值)
1085
+ const emObjId = cacheObjId(cache, 'entry_manage', '入职管理');
1086
+ const emFormId = cacheFormId(cache, 'entry_manage');
1087
+ const fields = cache.fields[emObjId] || {};
1088
+ for (const key of ['xm', 'telephone', 'email', 'subcompany', 'department', 'position', 'position_rank', 'entry_time', 'entry_status', 'info_collection']) {
1089
+ if (!fields[key])
1090
+ throw ehrValidationError('recruit_field_missing', `入职管理表单缺少字段映射 ${key}`);
1091
+ }
1092
+ const record = await getFormDataRecord(host, emFormId, entry.entryId, emObjId);
1093
+ if (!record.updateTimeStamp) {
1094
+ throw ehrValidationError('update_timestamp_missing', '未取到入职记录 updateTimeStamp(乐观锁),无法更新');
1095
+ }
1096
+ if (!record.formLayoutId) {
1097
+ throw ehrValidationError('edit_layout_missing', '未取到编辑布局 id,无法更新入职记录');
1098
+ }
1099
+ const existing = toSubmitDetails(record.dataDetails);
1100
+ const setField = (fieldId, detail) => {
1101
+ const index = existing.findIndex((item) => item.formField?.id === fieldId);
1102
+ if (index >= 0)
1103
+ existing[index] = detail;
1104
+ else
1105
+ existing.push(detail);
1106
+ };
1107
+ setField(fields.subcompany, { formField: { id: fields.subcompany }, dataOptions: [{ optionId: subcompany, content: '入职分部' }] });
1108
+ setField(fields.department, { formField: { id: fields.department }, dataOptions: [{ optionId: department, content: '入职部门' }] });
1109
+ setField(fields.position, { formField: { id: fields.position }, dataOptions: [{ optionId: position, content: '入职职位' }] });
1110
+ setField(fields.position_rank, { formField: { id: fields.position_rank }, dataOptions: [{ optionId: positionRank, content: '职级' }] });
1111
+ setField(fields.entry_time, { formField: { id: fields.entry_time }, content: entryTime });
1112
+ return {
1113
+ entryId: entry.entryId, candidateName: entry.name, transactStatus: entry.transactStatus,
1114
+ subcompany, department, position, positionRank, entryTime,
1115
+ updateTimeStamp: record.updateTimeStamp, editLayoutId: record.formLayoutId,
1116
+ emObjId, emFormId, dataDetails: existing,
1117
+ };
1118
+ }
1119
+ const entryProcessPrepare = {
1120
+ name: 'ehr.recruit.entry.process.prepare',
1121
+ resource: 'recruit',
1122
+ method: 'entry.process.prepare',
1123
+ action: 'ehr.recruit.entry.process.prepare',
1124
+ summary: '准备办理入职(路径 B,落账号不可逆):入职记录定位 + estValidateEntry 校验 + 组织架构确认摘要,不写入',
1125
+ capability: 'recruit',
1126
+ risk: WRITE,
1127
+ requiresConfirmation: true,
1128
+ inputSchema: schemaOf({ ...entryProcessSchema }, { required: ['talentId', 'departmentId', 'positionRankId'] }),
1129
+ handler: async (args, env) => {
1130
+ const cache = await requireRecruitCache(env);
1131
+ const plan = await buildEntryPlan(env, cache, args);
1132
+ const session = await env.host.getSession();
1133
+ const token = await env.continuation.issue({
1134
+ operation: 'ehr.recruit.entry.process.apply',
1135
+ baseUrl: session.baseUrl,
1136
+ profile: session.profile,
1137
+ userId: session.userId,
1138
+ payload: {
1139
+ entryId: plan.entryId, updateTimeStamp: plan.updateTimeStamp, editLayoutId: plan.editLayoutId,
1140
+ emObjId: plan.emObjId, emFormId: plan.emFormId, dataDetails: plan.dataDetails,
1141
+ candidateName: plan.candidateName,
1142
+ },
1143
+ fingerprint: fingerprint({ entryId: plan.entryId, dataDetails: plan.dataDetails }),
1144
+ }, CONTINUATION_TTL_SECONDS * 1000);
1145
+ return ehrSuccess('ehr.recruit.entry.process.prepare', {
1146
+ status: 'AWAITING_CONFIRMATION',
1147
+ preview: {
1148
+ action: '办理入职(路径 B:对已有待入职记录)',
1149
+ candidate: `${plan.candidateName}(talentId=${args.talentId})`,
1150
+ entryId: plan.entryId,
1151
+ transactStatus: plan.transactStatus || '待办理',
1152
+ subcompany: plan.subcompany,
1153
+ department: plan.department,
1154
+ position: plan.position,
1155
+ positionRank: plan.positionRank,
1156
+ entryTime: plan.entryTime,
1157
+ note: '提交后更新入职记录 → saveEstPreEntry 落人员账号(不可撤销)→ 触发「offer转入职」动作流;确认后才执行',
1158
+ },
1159
+ willSubmit: { formId: plan.emFormId, objId: plan.emObjId, updateTimeStamp: plan.updateTimeStamp, dataDetails: plan.dataDetails },
1160
+ continuation: token,
1161
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
1162
+ });
1163
+ },
1164
+ };
1165
+ const entryProcessApply = {
1166
+ name: 'ehr.recruit.entry.process.apply',
1167
+ resource: 'recruit',
1168
+ method: 'entry.process.apply',
1169
+ action: 'ehr.recruit.entry.process.apply',
1170
+ summary: '执行办理入职(更新入职记录 → saveEstPreEntry 落账号 → 动作流);落账号不可撤销,必须先 prepare 并经用户明确确认',
1171
+ capability: 'recruit',
1172
+ risk: WRITE,
1173
+ requiresConfirmation: true,
1174
+ inputSchema: schemaOf({ ...entryProcessSchema, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['talentId', 'departmentId', 'positionRankId', 'confirm', 'continuation'] }),
1175
+ handler: async (args, env) => {
1176
+ requireConfirm(args, 'ehr.recruit.entry.process.apply');
1177
+ const lease = await verifyLease(args, 'ehr.recruit.entry.process.apply', env);
1178
+ const payload = (lease.payload || {});
1179
+ if (!payload.entryId || !payload.updateTimeStamp || !payload.emObjId || !payload.emFormId || !Array.isArray(payload.dataDetails)) {
1180
+ throw ehrValidationError('continuation_payload_invalid', 'continuation 缺少入职记录参数,请重新执行 prepare');
1181
+ }
1182
+ const cache = await requireRecruitCache(env);
1183
+ // ③ 更新入职记录(rightType=2,带 id + updateTimeStamp + 编辑布局)
1184
+ let saveResponse;
1185
+ try {
1186
+ saveResponse = (await env.host.request({
1187
+ path: `/api/ebuilder/form/form/core/saveFormData?formId=${payload.emFormId}`,
1188
+ body: {
1189
+ module: 'ebuilderform',
1190
+ appId: cache.appId,
1191
+ customParam: {
1192
+ secretLevel: '', secretLevelValidity: '',
1193
+ objId: payload.emObjId,
1194
+ name: `入职管理-${payload.candidateName || ''}`,
1195
+ firstDataName: `入职管理-${payload.candidateName || ''}`,
1196
+ isFlowEnd: false, otherOperator: '', dataStatus: 'submit', rightType: '2', appId: cache.appId,
1197
+ },
1198
+ formData: {
1199
+ id: payload.entryId,
1200
+ updateTimeStamp: payload.updateTimeStamp,
1201
+ module: 'ebuilderform',
1202
+ dataDelRows: [],
1203
+ dataDetails: payload.dataDetails,
1204
+ form: { id: payload.emFormId },
1205
+ formLayout: { id: payload.editLayoutId },
1206
+ client: 'pc',
1207
+ dataStatus: 'submit',
1208
+ },
1209
+ },
1210
+ }));
1211
+ }
1212
+ catch (error) {
1213
+ if (isNetworkError(error)) {
1214
+ throw ehrPartialError('saveFormData(入职更新)请求连接中断,更新是否生效不确定,禁止自动重试;请回查入职记录', { entryId: payload.entryId }, { stage: 'entry.process.save' });
1215
+ }
1216
+ throw error;
1217
+ }
1218
+ if (!(saveResponse?.status === true && saveResponse?.code === 200)) {
1219
+ throw ehrValidationError('entry_save_failed', `更新入职记录失败:${saveResponse?.msg || JSON.stringify(saveResponse).slice(0, 200)}`);
1220
+ }
1221
+ // ④ 落账号(不可逆;以 code=200 && status=true && fail=false 为准)
1222
+ let entryAccountResponse;
1223
+ try {
1224
+ entryAccountResponse = (await env.host.request({
1225
+ path: `/api/recruit/estPre/saveEstPreEntry?entryId=${payload.entryId}`,
1226
+ method: 'POST',
1227
+ }));
1228
+ }
1229
+ catch (error) {
1230
+ if (isNetworkError(error)) {
1231
+ throw ehrPartialError(`入职记录已更新(entryId=${payload.entryId}),但 saveEstPreEntry(落账号)请求连接中断,账号是否已落不确定,禁止自动重试;请人工在 E10 人事模块核对`, { entryId: payload.entryId }, { stage: 'entry.process.account' });
1232
+ }
1233
+ throw error;
1234
+ }
1235
+ if (!(entryAccountResponse?.status === true && entryAccountResponse?.fail === false)) {
1236
+ throw ehrPartialError(`saveEstPreEntry(落账号)失败:${entryAccountResponse?.msg || '未知'}。入职记录已更新但账号未落,请人工核对,禁止自动重试`, { entryId: payload.entryId }, { stage: 'entry.process.account' });
1237
+ }
1238
+ // ⑤ 触发「offer转入职」动作流
1239
+ const actions = await getFormActions(env.host, payload.emObjId);
1240
+ const esbId = pickEsbActionId(actions, ['offer转入职']);
1241
+ if (!esbId)
1242
+ requireEsbActionId(payload.emObjId, ['offer转入职']);
1243
+ const esbResult = await runEsb(env.host, payload.emObjId, payload.entryId, esbId);
1244
+ return ehrSuccess('ehr.recruit.entry.process.apply', {
1245
+ status: 'COMPLETE',
1246
+ entryId: payload.entryId,
1247
+ validateEntry: '通过',
1248
+ saveFormData: '已更新',
1249
+ saveEstPreEntry: '已落账号',
1250
+ runEsb: { successNum: esbResult.successNum, failNum: esbResult.failNum, msg: esbResult.successMsg },
1251
+ nextStep: '回查入职列表 transact_status 应由 1 变为 2',
1252
+ });
1253
+ },
1254
+ };
1255
+ // ---------------------------------------------------------------------------
1256
+ // 面试反馈编辑 operation(提交/修改面试反馈,全字段集防清空 + 动作流 + 过程日志)
1257
+ // 迁移自 ehr 技能 recruit-edit-interview-feedback.mjs。
1258
+ // ---------------------------------------------------------------------------
1259
+ const INVALID_INTERVIEW_STATUS_ID = '841052191228968977'; // InterviewStatusEnum.INVALID(已失效不允许编辑反馈)
1260
+ async function listInterviewFeedback(host, talentId, batchId) {
1261
+ const response = (await host.request({
1262
+ path: '/api/recruit/candidate/getCandidateCardInterviewInfo',
1263
+ body: { talentId, batchId, current: 1, pageSize: 100 },
1264
+ }));
1265
+ const list = response?.data?.list || [];
1266
+ const out = [];
1267
+ for (let index = 0; index < list.length; index++) {
1268
+ const item = list[index];
1269
+ const feedbacks = item?.interviewFeedBack || [];
1270
+ if (feedbacks.length) {
1271
+ const fb = feedbacks[0];
1272
+ out.push({
1273
+ index,
1274
+ planName: fb.interviewPlanId?.[0]?.name || item.subject || '',
1275
+ interviewer: (fb.interviewer || []).map((x) => x.name).join('、') || '',
1276
+ feedbackId: String(fb.id || fb.formDataId || ''),
1277
+ result: (fb.result || []).map((x) => x.name).join('、') || '',
1278
+ judgement: String(fb.judgement || '').replace(/^<p>|<\/p>$/g, '').trim(),
1279
+ unionStatusId: item.unionStatus?.[0]?.id || '',
1280
+ unionStatusName: item.unionStatus?.[0]?.name || '',
1281
+ });
1282
+ }
1283
+ }
1284
+ return out;
1285
+ }
1286
+ const feedbackEditSchema = {
1287
+ talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID' },
1288
+ feedbackId: { type: 'string', description: '反馈记录 ID(最稳定位方式);不填时按 feedbackIndex 定位' },
1289
+ feedbackIndex: { type: 'integer', minimum: 0, description: '第几场含反馈的面试(默认第 0 场)' },
1290
+ result: { type: 'string', description: '面试结果(通过/不通过/待定),须在枚举内' },
1291
+ comment: { type: 'string', description: '面试评价文字;不填时保留原评价' },
1292
+ };
1293
+ async function buildFeedbackEditPlan(env, cache, args) {
1294
+ const host = env.host;
1295
+ const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
1296
+ if (!/^\d+$/.test(talentId))
1297
+ throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
1298
+ if (args.result === undefined && args.comment === undefined) {
1299
+ throw ehrValidationError('feedback_content_required', '请提供 result(面试结果)和/或 comment(评价)至少一项');
1300
+ }
1301
+ const candidate = await resolveCandidateForWrite(host, cache, talentId);
1302
+ const batchId = candidate.batchId;
1303
+ const list = await listInterviewFeedback(host, talentId, batchId);
1304
+ if (!list.length) {
1305
+ throw ehrValidationError('feedback_not_found', '该候选人没有可编辑的面试反馈记录');
1306
+ }
1307
+ let target;
1308
+ if (typeof args.feedbackId === 'string' && args.feedbackId.trim()) {
1309
+ target = list.find((item) => String(item.feedbackId) === args.feedbackId.trim());
1310
+ if (!target) {
1311
+ throw ehrValidationError('feedback_not_found', `未找到反馈记录 id=${args.feedbackId},可选:${list.map((item) => item.feedbackId).join(',')}`);
1312
+ }
1313
+ }
1314
+ else {
1315
+ const index = typeof args.feedbackIndex === 'number' ? args.feedbackIndex : 0;
1316
+ target = list[index];
1317
+ if (!target)
1318
+ throw ehrValidationError('feedback_index_invalid', `feedbackIndex=${index} 超出范围,共 ${list.length} 场含反馈`);
1319
+ }
1320
+ // 面试状态校验:已失效(INVALID)不允许填写/编辑反馈(与系统按钮条件一致)
1321
+ if (target.unionStatusId === INVALID_INTERVIEW_STATUS_ID) {
1322
+ throw ehrValidationError('interview_invalid_status', `该面试状态为「已失效」,不允许填写/编辑面试反馈。面试=${target.planName},反馈记录Id=${target.feedbackId}`);
1323
+ }
1324
+ const objId = cacheObjId(cache, 'interview_detail', '面试反馈表单');
1325
+ const formId = cacheFormId(cache, 'interview_detail');
1326
+ const fields = cache.fields[objId] || {};
1327
+ const resultFieldId = fields.result;
1328
+ const judgementFieldId = fields.judgement;
1329
+ if (!resultFieldId || !judgementFieldId) {
1330
+ throw ehrValidationError('recruit_field_missing', '环境缓存缺少面试反馈的 result/judgement 字段映射');
1331
+ }
1332
+ let newResultOptionId = null;
1333
+ let newResultName = String(target.result || '');
1334
+ if (typeof args.result === 'string' && args.result.trim()) {
1335
+ const opts = await getDsOptions(host, objId, resultFieldId, cache.appId);
1336
+ newResultOptionId = opts[args.result.trim()] || null;
1337
+ if (!newResultOptionId) {
1338
+ throw ehrValidationError('result_not_in_enum', `未识别的面试结果「${args.result}」,可选:${Object.keys(opts).join(' / ')}`);
1339
+ }
1340
+ newResultName = args.result.trim();
1341
+ }
1342
+ const newComment = args.comment !== undefined ? String(args.comment) : null;
1343
+ return {
1344
+ talentId, batchId, feedbackId: String(target.feedbackId), planName: String(target.planName || ''),
1345
+ interviewer: String(target.interviewer || ''),
1346
+ currentResult: String(target.result || ''), currentJudgement: String(target.judgement || ''),
1347
+ newResultName, newResultOptionId, newComment,
1348
+ objId, formId, resultFieldId, judgementFieldId,
1349
+ };
1350
+ }
1351
+ const feedbackEditPrepare = {
1352
+ name: 'ehr.recruit.candidate.interview.feedback.edit.prepare',
1353
+ resource: 'recruit',
1354
+ method: 'candidate.interview.feedback.edit.prepare',
1355
+ action: 'ehr.recruit.candidate.interview.feedback.edit.prepare',
1356
+ summary: '准备提交/修改面试反馈:反馈记录定位 + 已失效状态拦截 + 结果枚举解析 + 前后对照摘要,不写入',
1357
+ capability: 'recruit',
1358
+ risk: WRITE,
1359
+ requiresConfirmation: true,
1360
+ inputSchema: schemaOf({ ...feedbackEditSchema }, { required: ['talentId'] }),
1361
+ handler: async (args, env) => {
1362
+ const cache = await requireRecruitCache(env);
1363
+ const plan = await buildFeedbackEditPlan(env, cache, args);
1364
+ const session = await env.host.getSession();
1365
+ const token = await env.continuation.issue({
1366
+ operation: 'ehr.recruit.candidate.interview.feedback.edit.apply',
1367
+ baseUrl: session.baseUrl,
1368
+ profile: session.profile,
1369
+ userId: session.userId,
1370
+ payload: {
1371
+ talentId: plan.talentId, batchId: plan.batchId, feedbackId: plan.feedbackId,
1372
+ newResultOptionId: plan.newResultOptionId, newResultName: plan.newResultName, newComment: plan.newComment,
1373
+ objId: plan.objId, formId: plan.formId, resultFieldId: plan.resultFieldId, judgementFieldId: plan.judgementFieldId,
1374
+ },
1375
+ fingerprint: fingerprint({
1376
+ feedbackId: plan.feedbackId,
1377
+ result: plan.newResultOptionId,
1378
+ comment: plan.newComment,
1379
+ }),
1380
+ }, CONTINUATION_TTL_SECONDS * 1000);
1381
+ return ehrSuccess('ehr.recruit.candidate.interview.feedback.edit.prepare', {
1382
+ status: 'AWAITING_CONFIRMATION',
1383
+ preview: {
1384
+ action: '提交/修改面试反馈',
1385
+ talentId: plan.talentId,
1386
+ batchId: plan.batchId,
1387
+ interview: plan.planName,
1388
+ interviewer: plan.interviewer,
1389
+ feedbackId: plan.feedbackId,
1390
+ currentFeedback: `${plan.currentResult ? `结果 ${plan.currentResult}` : ''}${plan.currentJudgement ? `;评价 ${plan.currentJudgement}` : ''}` || '(无)',
1391
+ newFeedback: `${plan.newResultOptionId ? `结果 ${plan.newResultName}` : ''}${plan.newComment !== null ? `;评价 ${plan.newComment}` : ''}` || '(不变)',
1392
+ note: 'apply 将保存反馈表单(全字段集 + 乐观锁)并回查校验',
1393
+ },
1394
+ continuation: token,
1395
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
1396
+ });
1397
+ },
1398
+ };
1399
+ const feedbackEditApply = {
1400
+ name: 'ehr.recruit.candidate.interview.feedback.edit.apply',
1401
+ resource: 'recruit',
1402
+ method: 'candidate.interview.feedback.edit.apply',
1403
+ action: 'ehr.recruit.candidate.interview.feedback.edit.apply',
1404
+ summary: '执行提交/修改面试反馈(全字段集保存 + 动作流 + 过程日志 + 回查校验);必须先 prepare 并经用户明确确认',
1405
+ capability: 'recruit',
1406
+ risk: WRITE,
1407
+ requiresConfirmation: true,
1408
+ inputSchema: schemaOf({ ...feedbackEditSchema, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['talentId', 'confirm', 'continuation'] }),
1409
+ handler: async (args, env) => {
1410
+ requireConfirm(args, 'ehr.recruit.candidate.interview.feedback.edit.apply');
1411
+ const lease = await verifyLease(args, 'ehr.recruit.candidate.interview.feedback.edit.apply', env);
1412
+ const payload = (lease.payload || {});
1413
+ if (!payload.feedbackId || !payload.objId || !payload.formId) {
1414
+ throw ehrValidationError('continuation_payload_invalid', 'continuation 缺少反馈记录参数,请重新执行 prepare');
1415
+ }
1416
+ const cache = await requireRecruitCache(env);
1417
+ // 重新取最新记录(updateTimeStamp 乐观锁 + 全字段集防清空)
1418
+ const record = await getFormDataRecord(env.host, payload.formId, payload.feedbackId, payload.objId);
1419
+ if (!record.updateTimeStamp) {
1420
+ throw ehrValidationError('update_timestamp_missing', '未取到反馈记录 updateTimeStamp(乐观锁),无法保存');
1421
+ }
1422
+ const details = toSubmitDetails(record.dataDetails);
1423
+ let newDetails = details.map((item) => {
1424
+ const fieldId = item.formField?.id;
1425
+ if (fieldId === payload.resultFieldId && payload.newResultOptionId) {
1426
+ return { formField: { id: fieldId }, dataOptions: [{ optionId: payload.newResultOptionId, content: payload.newResultName }] };
1427
+ }
1428
+ if (fieldId === payload.judgementFieldId && payload.newComment !== null) {
1429
+ return { formField: { id: fieldId }, dataText: { content: `<p>${payload.newComment || ''}</p>` } };
1430
+ }
1431
+ return item;
1432
+ });
1433
+ if (payload.newResultOptionId && !newDetails.some((item) => item.formField?.id === payload.resultFieldId)) {
1434
+ newDetails.push({ formField: { id: payload.resultFieldId }, dataOptions: [{ optionId: payload.newResultOptionId, content: payload.newResultName }] });
1435
+ }
1436
+ if (payload.newComment !== null && !newDetails.some((item) => item.formField?.id === payload.judgementFieldId)) {
1437
+ newDetails.push({ formField: { id: payload.judgementFieldId }, dataText: { content: `<p>${payload.newComment || ''}</p>` } });
1438
+ }
1439
+ // 保存反馈表单
1440
+ let savedId;
1441
+ try {
1442
+ savedId = await saveFormData(env.host, {
1443
+ appId: cache.appId, formId: payload.formId, objId: payload.objId,
1444
+ name: '面试反馈-' + lease.payload?.planName || '面试反馈',
1445
+ dataDetails: newDetails,
1446
+ rightType: '2',
1447
+ formDataId: payload.feedbackId,
1448
+ updateTimeStamp: record.updateTimeStamp,
1449
+ layoutId: record.formLayoutId,
1450
+ });
1451
+ }
1452
+ catch (error) {
1453
+ if (isNetworkError(error)) {
1454
+ throw ehrPartialError('saveFormData(面试反馈)请求连接中断,保存是否生效不确定,禁止自动重试;请用 candidate.card 回查反馈', { feedbackId: payload.feedbackId }, { stage: 'feedback.edit.save' });
1455
+ }
1456
+ throw error;
1457
+ }
1458
+ if (!savedId)
1459
+ savedId = payload.feedbackId;
1460
+ // 回查校验
1461
+ const backList = await listInterviewFeedback(env.host, String(payload.talentId), String(payload.batchId));
1462
+ const backFeedback = backList.find((item) => String(item.feedbackId) === String(payload.feedbackId));
1463
+ const changed = Boolean(backFeedback)
1464
+ && (!payload.newResultOptionId || String(backFeedback?.result).includes(String(payload.newResultName)))
1465
+ && (payload.newComment === null || String(backFeedback?.judgement).includes(String(payload.newComment)));
1466
+ return ehrSuccess('ehr.recruit.candidate.interview.feedback.edit.apply', {
1467
+ status: changed ? 'COMPLETE' : 'UNCERTAIN',
1468
+ feedbackId: payload.feedbackId,
1469
+ saved: Boolean(savedId),
1470
+ verify: backFeedback
1471
+ ? { result: backFeedback.result, comment: backFeedback.judgement, changed }
1472
+ : '回查未取到该反馈',
1473
+ ...(changed ? {} : { warning: '回查未确认变更生效,请人工在 E10 中核对面试反馈,禁止自动重试' }),
1474
+ });
1475
+ },
1476
+ };
1477
+ // ---------------------------------------------------------------------------
1478
+ // 催促面试评价 operation(L3 外部通知:向面试官发系统通知)
1479
+ // 迁移自 ehr 技能 recruit-urge-feedback.mjs。
1480
+ // ---------------------------------------------------------------------------
1481
+ const feedbackUrgeSchema = {
1482
+ talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID' },
1483
+ };
1484
+ async function buildFeedbackUrgePlan(env, cache, args) {
1485
+ const host = env.host;
1486
+ const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
1487
+ if (!/^\d+$/.test(talentId))
1488
+ throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
1489
+ const candidate = await resolveCandidateForWrite(host, cache, talentId);
1490
+ const rk = randomHex6();
1491
+ const extra = cache.lists.talent?.listId ? { ebBusinessId: cache.lists.talent.listId } : {};
1492
+ const interviewResponse = (await host.request({
1493
+ path: '/api/recruit/candidate/getCandidateCardInterviewInfo',
1494
+ body: { talentId, batchId: candidate.batchId, current: 1, pageSize: 100 },
1495
+ headers: extra,
1496
+ }));
1497
+ const list = interviewResponse?.data?.list || [];
1498
+ // 取第一条「待反馈」记录(result 为空),没有则取第一条
1499
+ let target = null;
1500
+ for (const item of list) {
1501
+ for (const feedback of item.interviewFeedBack || []) {
1502
+ if (!feedback.result || !feedback.result.length) {
1503
+ target = { it: item, fb: feedback };
1504
+ break;
1505
+ }
1506
+ }
1507
+ if (target)
1508
+ break;
1509
+ }
1510
+ if (!target && list.length) {
1511
+ const item = list[0];
1512
+ const feedback = (item.interviewFeedBack || [])[0];
1513
+ if (feedback)
1514
+ target = { it: item, fb: feedback };
1515
+ }
1516
+ if (!target) {
1517
+ throw ehrValidationError('feedback_not_found', '未找到该候选人的面试反馈记录,无法催评');
1518
+ }
1519
+ const interviewer = target.fb.updater?.[0]?.name || target.it.interviewer?.[0]?.name || candidate.name;
1520
+ // esb 按钮:面试反馈列表 getListButtons
1521
+ const interviewList = cache.lists.interview;
1522
+ if (!interviewList?.listId) {
1523
+ throw ehrValidationError('recruit_list_unavailable', '当前环境未启用「面试管理」功能菜单,无法解析催评动作流按钮');
1524
+ }
1525
+ const interviewDetailObjId = cacheObjId(cache, 'interview_detail', '面试反馈表单');
1526
+ const actions = await getListActions(host, interviewList.listId, interviewDetailObjId);
1527
+ const esbActionId = pickEsbActionId(actions, ['面试安排催评'], ['批量催评']);
1528
+ if (!esbActionId)
1529
+ requireEsbActionId(interviewDetailObjId, ['面试安排催评'], ['批量催评']);
1530
+ return {
1531
+ talentId, name: candidate.name,
1532
+ subject: String(target.it.subject || ''),
1533
+ interviewer,
1534
+ feedbackRecordId: String(target.fb.id),
1535
+ esbActionId, interviewDetailObjId,
1536
+ };
1537
+ }
1538
+ const feedbackUrgePrepare = {
1539
+ name: 'ehr.recruit.candidate.interview.feedback.urge.prepare',
1540
+ resource: 'recruit',
1541
+ method: 'candidate.interview.feedback.urge.prepare',
1542
+ action: 'ehr.recruit.candidate.interview.feedback.urge.prepare',
1543
+ summary: '准备催促面试评价(L3 外部通知):待反馈记录定位 + 催评动作流按钮解析 + 确认摘要,不发送',
1544
+ capability: 'recruit',
1545
+ risk: WRITE,
1546
+ requiresConfirmation: true,
1547
+ inputSchema: schemaOf({ ...feedbackUrgeSchema }, { required: ['talentId'] }),
1548
+ handler: async (args, env) => {
1549
+ const cache = await requireRecruitCache(env);
1550
+ const plan = await buildFeedbackUrgePlan(env, cache, args);
1551
+ const session = await env.host.getSession();
1552
+ const token = await env.continuation.issue({
1553
+ operation: 'ehr.recruit.candidate.interview.feedback.urge.apply',
1554
+ baseUrl: session.baseUrl,
1555
+ profile: session.profile,
1556
+ userId: session.userId,
1557
+ payload: {
1558
+ talentId: plan.talentId, feedbackRecordId: plan.feedbackRecordId,
1559
+ esbActionId: plan.esbActionId, interviewDetailObjId: plan.interviewDetailObjId,
1560
+ },
1561
+ fingerprint: fingerprint({ talentId: plan.talentId, feedbackRecordId: plan.feedbackRecordId }),
1562
+ }, CONTINUATION_TTL_SECONDS * 1000);
1563
+ return ehrSuccess('ehr.recruit.candidate.interview.feedback.urge.prepare', {
1564
+ status: 'AWAITING_CONFIRMATION',
1565
+ preview: {
1566
+ action: '催促面试评价(外部通知)',
1567
+ talent: `${plan.name}(talentId=${plan.talentId})`,
1568
+ interview: plan.subject,
1569
+ interviewer: plan.interviewer,
1570
+ feedbackRecordId: plan.feedbackRecordId,
1571
+ note: '确认后将向面试官发送「请填写评价」系统通知(会打扰真人,请确保已经用户同意)',
1572
+ },
1573
+ continuation: token,
1574
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
1575
+ });
1576
+ },
1577
+ };
1578
+ const feedbackUrgeApply = {
1579
+ name: 'ehr.recruit.candidate.interview.feedback.urge.apply',
1580
+ resource: 'recruit',
1581
+ method: 'candidate.interview.feedback.urge.apply',
1582
+ action: 'ehr.recruit.candidate.interview.feedback.urge.apply',
1583
+ summary: '执行催促面试评价(runEsb 发送系统通知给面试官,L3 外部通知);必须先 prepare 并经用户明确确认',
1584
+ capability: 'recruit',
1585
+ risk: WRITE,
1586
+ requiresConfirmation: true,
1587
+ inputSchema: schemaOf({ ...feedbackUrgeSchema, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['talentId', 'confirm', 'continuation'] }),
1588
+ handler: async (args, env) => {
1589
+ requireConfirm(args, 'ehr.recruit.candidate.interview.feedback.urge.apply');
1590
+ const lease = await verifyLease(args, 'ehr.recruit.candidate.interview.feedback.urge.apply', env);
1591
+ const payload = (lease.payload || {});
1592
+ if (!payload.feedbackRecordId || !payload.esbActionId || !payload.interviewDetailObjId) {
1593
+ throw ehrValidationError('continuation_payload_invalid', 'continuation 缺少催评参数,请重新执行 prepare');
1594
+ }
1595
+ if (fingerprint({ talentId: payload.talentId, feedbackRecordId: payload.feedbackRecordId })
1596
+ !== lease.fingerprint) {
1597
+ throw ehrValidationError('target_changed', '输入与 prepare 时的快照不一致,请重新执行 prepare');
1598
+ }
1599
+ let esbResult;
1600
+ try {
1601
+ esbResult = await runEsb(env.host, payload.interviewDetailObjId, payload.feedbackRecordId, payload.esbActionId);
1602
+ }
1603
+ catch (error) {
1604
+ if (isNetworkError(error)) {
1605
+ throw ehrPartialError('runEsb(催评通知)请求连接中断,通知是否已发出不确定,禁止自动重试(重复发送会多次打扰面试官)', { feedbackRecordId: payload.feedbackRecordId }, { stage: 'feedback.urge.runEsb' });
1606
+ }
1607
+ throw error;
1608
+ }
1609
+ return ehrSuccess('ehr.recruit.candidate.interview.feedback.urge.apply', {
1610
+ status: 'COMPLETE',
1611
+ feedbackRecordId: payload.feedbackRecordId,
1612
+ runEsb: { successNum: esbResult.successNum, failNum: esbResult.failNum, msg: esbResult.successMsg },
1613
+ });
1614
+ },
1615
+ };
1616
+ export const recruitWriteOperations = [
1617
+ candidateCreatePrepare,
1618
+ candidateCreateApply,
1619
+ interviewArrangePrepare,
1620
+ interviewArrangeApply,
1621
+ offerSendPrepare,
1622
+ offerSendApply,
1623
+ entryProcessPrepare,
1624
+ entryProcessApply,
1625
+ feedbackEditPrepare,
1626
+ feedbackEditApply,
1627
+ feedbackUrgePrepare,
1628
+ feedbackUrgeApply,
1629
+ ];