weaver-work-cli 0.1.40 → 0.1.44
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.
- package/README.md +17 -0
- package/dist/chunks/{archive-JHTONRST.js → archive-P6TBQGKL.js} +13 -5
- package/dist/chunks/{archive-oa-FCHLCPHC.js → archive-oa-7TQ2NXOH.js} +12 -5
- package/dist/chunks/{asset-RLIHC7DB.js → asset-3ELRG37G.js} +11 -4
- package/dist/chunks/{asset-idle-KVOFTLCP.js → asset-idle-WIGMUFMD.js} +7 -3
- package/dist/chunks/{attachment-content-RZYDB3MB.js → attachment-content-E3276UOV.js} +5 -4
- package/dist/chunks/{blog-42OZTJNP.js → blog-C5NMZAAC.js} +11 -4
- package/dist/chunks/{caizhilian-KRK547K5.js → caizhilian-5XSM2DFP.js} +13 -6
- package/dist/chunks/{calendar-RTWAMKHD.js → calendar-FI2WNSBO.js} +71 -26
- package/dist/chunks/{chunk-CXRQWOZ5.js → chunk-3YBDYOC4.js} +3 -3
- package/dist/chunks/{chunk-OOUTBLZ4.js → chunk-56EZERK4.js} +1 -1
- package/dist/chunks/chunk-6N4JPNFL.js +76 -0
- package/dist/chunks/{chunk-ZUX3P6BQ.js → chunk-727FH2NI.js} +1 -1
- package/dist/chunks/chunk-7BDPM5ST.js +11 -0
- package/dist/chunks/{chunk-LN447PY2.js → chunk-BCVYBYYT.js} +432 -2247
- package/dist/chunks/{chunk-7E6MUOEK.js → chunk-CWYGFU5B.js} +2 -2
- package/dist/chunks/{chunk-NOODWMCL.js → chunk-D4ADSZ7H.js} +1 -1
- package/dist/chunks/{chunk-WFFNM65B.js → chunk-ETNJSKMM.js} +281 -29
- package/dist/chunks/{chunk-CAQNBBQA.js → chunk-F4E4EM35.js} +3 -3
- package/dist/chunks/{chunk-4CTESSKI.js → chunk-FAZTGX7S.js} +6 -6
- package/dist/chunks/chunk-FRZKBMSW.js +2562 -0
- package/dist/chunks/{chunk-BYBSCN4X.js → chunk-G7PQWD4L.js} +6 -3
- package/dist/chunks/{chunk-NJKREDA3.js → chunk-GAMZLAG2.js} +1 -1
- package/dist/chunks/{chunk-FLS4DPGP.js → chunk-GM6J7Q4B.js} +28 -15
- package/dist/chunks/{chunk-NETCCARD.js → chunk-GRDRG6HP.js} +3 -3
- package/dist/chunks/chunk-HIT4Y6OI.js +1238 -0
- package/dist/chunks/{chunk-EZALDOQH.js → chunk-HP7JIYMT.js} +5 -2
- package/dist/chunks/{chunk-IZSSVYAG.js → chunk-IAZ3UPY6.js} +3 -3
- package/dist/chunks/{chunk-ZEDWSH32.js → chunk-IC62NFM5.js} +1 -1
- package/dist/chunks/{chunk-P26ZWSKU.js → chunk-KIHNAS25.js} +5 -2
- package/dist/chunks/{chunk-57IECMWX.js → chunk-N4M2DDFE.js} +29 -15
- package/dist/chunks/{chunk-JWRZM2H2.js → chunk-OF4S23UB.js} +4 -4
- package/dist/chunks/{chunk-VNBU55SL.js → chunk-PC7VKEB5.js} +80 -2458
- package/dist/chunks/{chunk-G2JSIUWF.js → chunk-PUQS5MWL.js} +91 -2
- package/dist/chunks/{chunk-E7OK5WD5.js → chunk-Q4OYAQYH.js} +5 -5
- package/dist/chunks/{chunk-7BRU4EFH.js → chunk-QVSWRLLU.js} +35 -6
- package/dist/chunks/{chunk-T6EGVBKU.js → chunk-TA7FXXJD.js} +6 -3
- package/dist/chunks/{chunk-GPMFD4RD.js → chunk-TXUG266F.js} +1 -1
- package/dist/chunks/{chunk-4TEDNCON.js → chunk-UFIT2UU3.js} +354 -56
- package/dist/chunks/{chunk-LUSPKZKY.js → chunk-UG5G54VL.js} +1 -0
- package/dist/chunks/{chunk-NIAJ5ZCG.js → chunk-UL5XUQ25.js} +5 -2
- package/dist/chunks/{chunk-FMPJJYYI.js → chunk-WRXSAVKO.js} +1 -1
- package/dist/chunks/{chunk-7U577Q7U.js → chunk-WSF4NNMR.js} +3 -3
- package/dist/chunks/chunk-XHK4MDR5.js +2497 -0
- package/dist/chunks/{chunk-LFMSDBZN.js → chunk-XNIK66I4.js} +326 -161
- package/dist/chunks/{chunk-3SO6QL6U.js → chunk-YWX377BY.js} +5 -2
- package/dist/chunks/{chunk-BYATLLEF.js → chunk-Z3GDFVJR.js} +1 -1
- package/dist/chunks/{chunk-I566ZYNF.js → chunk-Z7VYWPZV.js} +1 -1
- package/dist/chunks/{conflict-VR55AOOO.js → conflict-ARLSZJ6V.js} +4 -3
- package/dist/chunks/{continuation-2CKSBLA6.js → continuation-3S37AUSH.js} +4 -3
- package/dist/chunks/{continuation-RN6PZN2M.js → continuation-3XLYYTKV.js} +4 -3
- package/dist/chunks/{continuation-HZIZRZBX.js → continuation-HFW2YT5S.js} +4 -3
- package/dist/chunks/{continuation-NLECEJPX.js → continuation-KI7DRRZN.js} +4 -3
- package/dist/chunks/{contract-HI4FNPYE.js → contract-DNIIW5VJ.js} +13 -5
- package/dist/chunks/{cowork-HJLI353D.js → cowork-GMLRGVIW.js} +31 -14
- package/dist/chunks/create-flow-J22BO65E.js +56 -0
- package/dist/chunks/{ebuilder-form-HTGI63UU.js → ebuilder-form-ANV2FHYV.js} +12 -7
- package/dist/chunks/{ehr-GYXIYH3A.js → ehr-GSTGAIY3.js} +695 -1255
- package/dist/chunks/{employee-SAK2QYXX.js → employee-22BGJ3QS.js} +3 -3
- package/dist/chunks/{env-DW3G4HUO.js → env-BX3VRFS7.js} +4 -3
- package/dist/chunks/{esb-I5INCEFE.js → esb-ATAIASX2.js} +12 -7
- package/dist/chunks/{factory-TKXGXS6L.js → factory-PETTFME6.js} +3 -2
- package/dist/chunks/{fna-K2MHTQ24.js → fna-YEKBSFSX.js} +14 -8
- package/dist/chunks/{group-chat-VYBNMJLG.js → group-chat-BFVXAYFX.js} +7 -4
- package/dist/chunks/{host-D5SERMAJ.js → host-HRBXEPDQ.js} +5 -5
- package/dist/chunks/{host-JM4YVHFM.js → host-LR55CTHP.js} +5 -5
- package/dist/chunks/{host-L3VLITMT.js → host-SUOY6VYQ.js} +5 -5
- package/dist/chunks/{host-ZUQTJYYK.js → host-SZGV4UNT.js} +5 -5
- package/dist/chunks/{host-256GVCAH.js → host-WIKN2EJ5.js} +5 -5
- package/dist/chunks/{hrm-VLRBKTZB.js → hrm-NRDO4W5G.js} +11 -7
- package/dist/chunks/{im-QK3VTLXK.js → im-IQ2HN23S.js} +11 -10
- package/dist/chunks/{invoice-T5JTA32H.js → invoice-MNRW76PP.js} +636 -1768
- package/dist/chunks/{jiuchuanhui-P2FNEQVZ.js → jiuchuanhui-DTRBAX4N.js} +14 -6
- package/dist/chunks/{lazy-dispatch-XRRI46M6.js → lazy-dispatch-ZVWAI25B.js} +7 -6
- package/dist/chunks/{mail-3Y7MJ6PS.js → mail-ZVAUXOJA.js} +16 -14
- package/dist/chunks/{meeting-M7LTXGJE.js → meeting-FEVPZ67S.js} +21 -13
- package/dist/chunks/{okr-OBC6S67L.js → okr-SDH3E5RA.js} +12 -5
- package/dist/chunks/{operations-5QL3AECO.js → operations-62N6EP3C.js} +4 -3
- package/dist/chunks/{plan-5H6I53SK.js → plan-E35JFDI7.js} +12 -5
- package/dist/chunks/{project-L75QZV4S.js → project-73QXBAB2.js} +49 -25
- package/dist/chunks/reception-UWZVACBN.js +1899 -0
- package/dist/chunks/recruit-stage-4MAOCKFY.js +4770 -0
- package/dist/chunks/registry-IM5G23LS.js +24 -0
- package/dist/chunks/{registry-UGRBCBHO.js → registry-JDUS4E7D.js} +7 -6
- package/dist/chunks/{registry-RXPN6BBT.js → registry-KNKSCJNT.js} +7 -6
- package/dist/chunks/{registry-NYPYMZXG.js → registry-PO7SI256.js} +4 -4
- package/dist/chunks/{registry-QGHFNUWS.js → registry-PVUZQDDQ.js} +3 -3
- package/dist/chunks/{room-recommend-A3UDC3CA.js → room-recommend-H7KLXA5R.js} +6 -5
- package/dist/chunks/{schedule-5GCACCK7.js → schedule-ESZIBAUO.js} +6 -5
- package/dist/chunks/{workflow-M5ZJ7DWF.js → workflow-CWJFJLQU.js} +15 -10
- package/dist/cmd/action.js +6 -0
- package/dist/cmd/identity/index.js +45 -0
- package/dist/cmd/index.js +2 -0
- package/dist/core/continuation-ttl.js +18 -0
- package/dist/core/error-codes.js +85 -0
- package/dist/core/logger.js +11 -0
- package/dist/index.js +41 -30
- package/dist/internal/e10/auth/commands.js +164 -8
- package/dist/internal/e10/auth/error-log.js +182 -0
- package/dist/internal/e10/auth/messages.js +6 -0
- package/dist/internal/e10/auth/session.js +34 -3
- package/dist/internal/e10/auth/xiaoe.js +286 -44
- package/dist/internal/e10/auth-recovery.js +72 -14
- package/dist/internal/e10/context.js +72 -13
- package/dist/internal/e10/index.js +1 -1
- package/dist/internal/e10/network-error.js +93 -0
- package/dist/internal/e10/request-runtime.js +6 -0
- package/dist/internal/skills/install.js +9 -1
- package/dist/shortcuts/archive/continuation.js +2 -1
- package/dist/shortcuts/archive/index.js +3 -0
- package/dist/shortcuts/archive/operations/confirm.js +2 -1
- package/dist/shortcuts/archive-oa/continuation.js +2 -1
- package/dist/shortcuts/archive-oa/index.js +2 -0
- package/dist/shortcuts/archive-oa/operations/confirm.js +2 -1
- package/dist/shortcuts/asset/continuation.js +2 -1
- package/dist/shortcuts/asset/index.js +2 -0
- package/dist/shortcuts/asset-idle/index.js +2 -0
- package/dist/shortcuts/blog/continuation.js +2 -1
- package/dist/shortcuts/blog/index.js +2 -0
- package/dist/shortcuts/caizhilian/continuation.js +2 -1
- package/dist/shortcuts/caizhilian/index.js +2 -0
- package/dist/shortcuts/caizhilian/operations/confirm.js +3 -2
- package/dist/shortcuts/calendar/continuation.js +2 -1
- package/dist/shortcuts/calendar/index.js +2 -0
- package/dist/shortcuts/calendar/operations/read.js +31 -22
- package/dist/shortcuts/calendar/operations/write.js +46 -2
- package/dist/shortcuts/contract/continuation.js +2 -1
- package/dist/shortcuts/contract/index.js +2 -0
- package/dist/shortcuts/contract/operations/write.js +2 -1
- package/dist/shortcuts/cowork/continuation.js +2 -1
- package/dist/shortcuts/cowork/index.js +2 -0
- package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
- package/dist/shortcuts/cowork/operations/item-write.js +1 -7
- package/dist/shortcuts/cowork/operations/member-write.js +3 -1
- package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
- package/dist/shortcuts/cowork/operations/shared.js +7 -0
- package/dist/shortcuts/ebuilder-form/continuation.js +2 -1
- package/dist/shortcuts/ebuilder-form/index.js +2 -0
- package/dist/shortcuts/ebuilder-form/operations/write.js +3 -2
- package/dist/shortcuts/ehr/continuation.js +2 -1
- package/dist/shortcuts/ehr/index.js +2 -0
- package/dist/shortcuts/ehr/operations/attend-write.js +44 -2
- package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
- package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
- package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +317 -63
- package/dist/shortcuts/ehr/operations/recruit-position.js +134 -2
- package/dist/shortcuts/ehr/operations/recruit-write.js +263 -18
- package/dist/shortcuts/ehr/operations/recruit.js +10 -4
- package/dist/shortcuts/ehr/operations/shared.js +47 -0
- package/dist/shortcuts/esb/continuation.js +2 -1
- package/dist/shortcuts/esb/index.js +2 -0
- package/dist/shortcuts/esb/operations/trigger.js +2 -1
- package/dist/shortcuts/fna/continuation.js +2 -1
- package/dist/shortcuts/fna/index.js +2 -0
- package/dist/shortcuts/fna/operations/invoice-writeoff.js +1 -1
- package/dist/shortcuts/fna/operations/loan.js +1 -1
- package/dist/shortcuts/hrm/index.js +2 -0
- package/dist/shortcuts/hrm/operations/org.js +118 -0
- package/dist/shortcuts/im/continuation.js +2 -1
- package/dist/shortcuts/index.js +10 -0
- package/dist/shortcuts/invoice/continuation.js +2 -1
- package/dist/shortcuts/invoice/index.js +14 -15
- package/dist/shortcuts/invoice/invoice-record.js +93 -0
- package/dist/shortcuts/invoice/manifest.js +190 -393
- package/dist/shortcuts/invoice/operations/add.js +2 -1
- package/dist/shortcuts/invoice/operations/delete.js +2 -1
- package/dist/shortcuts/invoice/operations/import.js +2 -1
- package/dist/shortcuts/invoice/operations/issuing-contract-detail.js +86 -0
- package/dist/shortcuts/invoice/operations/issuing-contracts.js +210 -0
- package/dist/shortcuts/invoice/operations/registry.js +17 -17
- package/dist/shortcuts/invoice/operations/reim-auto.js +4 -569
- package/dist/shortcuts/invoice/operations/reim.js +52 -341
- package/dist/shortcuts/invoice/operations/shared.js +21 -4
- package/dist/shortcuts/invoice/operations/update.js +2 -1
- package/dist/shortcuts/invoice/projection.js +10 -62
- package/dist/shortcuts/jiuchuanhui/continuation.js +2 -1
- package/dist/shortcuts/jiuchuanhui/index.js +2 -0
- package/dist/shortcuts/jiuchuanhui/operations/datajson.js +2 -1
- package/dist/shortcuts/jiuchuanhui/operations/esb.js +2 -1
- package/dist/shortcuts/mail/continuation.js +2 -1
- package/dist/shortcuts/mail/index.js +2 -0
- package/dist/shortcuts/mail/operations/mail-blacklist.js +1 -1
- package/dist/shortcuts/mail/operations/mail-contacts.js +1 -1
- package/dist/shortcuts/mail/operations/mail-send.js +1 -1
- package/dist/shortcuts/mail/operations/mail-sign.js +1 -1
- package/dist/shortcuts/mail/operations/mail-template.js +1 -1
- package/dist/shortcuts/meeting/continuation.js +2 -1
- package/dist/shortcuts/meeting/index.js +2 -0
- package/dist/shortcuts/meeting/operations/change.js +1 -1
- package/dist/shortcuts/meeting/operations/create.js +1 -1
- package/dist/shortcuts/meeting/operations/eb/catalog.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +101 -35
- package/dist/shortcuts/meeting/operations/eb/decision.js +1 -1
- package/dist/shortcuts/meeting/operations/eb/factory.js +34 -1
- package/dist/shortcuts/meeting/operations/eb/group-chat.js +25 -3
- package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +33 -11
- package/dist/shortcuts/meeting/operations/lifecycle.js +1 -1
- package/dist/shortcuts/meeting/operations/receipts.js +1 -1
- package/dist/shortcuts/meeting/operations/signs.js +1 -1
- package/dist/shortcuts/okr/continuation.js +2 -1
- package/dist/shortcuts/okr/index.js +2 -0
- package/dist/shortcuts/okr/operations/write.js +1 -1
- package/dist/shortcuts/plan/continuation.js +2 -1
- package/dist/shortcuts/plan/index.js +2 -0
- package/dist/shortcuts/plan/operations/write.js +1 -1
- package/dist/shortcuts/project/continuation.js +2 -1
- package/dist/shortcuts/project/index.js +2 -0
- package/dist/shortcuts/project/operations/read.js +6 -25
- package/dist/shortcuts/project/operations/shared.js +22 -0
- package/dist/shortcuts/project/operations/write.js +28 -2
- package/dist/shortcuts/reception/constants.js +53 -0
- package/dist/shortcuts/reception/errors.js +54 -0
- package/dist/shortcuts/reception/host.js +125 -0
- package/dist/shortcuts/reception/index.js +198 -0
- package/dist/shortcuts/reception/manifest.js +177 -0
- package/dist/shortcuts/reception/operations/ask.js +71 -0
- package/dist/shortcuts/reception/operations/browsertool.js +58 -0
- package/dist/shortcuts/reception/operations/compose.js +129 -0
- package/dist/shortcuts/reception/operations/customparams.js +74 -0
- package/dist/shortcuts/reception/operations/doc.js +238 -0
- package/dist/shortcuts/reception/operations/docrender.js +36 -0
- package/dist/shortcuts/reception/operations/panels.js +179 -0
- package/dist/shortcuts/reception/operations/registry.js +26 -0
- package/dist/shortcuts/reception/operations/render-assets.js +184 -0
- package/dist/shortcuts/reception/operations/render.js +123 -0
- package/dist/shortcuts/reception/operations/runs.js +38 -0
- package/dist/shortcuts/reception/operations/schedule.js +127 -0
- package/dist/shortcuts/reception/operations/shared.js +196 -0
- package/dist/shortcuts/reception/operations/snapshot.js +68 -0
- package/dist/shortcuts/reception/operations/types.js +1 -0
- package/dist/shortcuts/reception/operations.js +33 -0
- package/dist/shortcuts/reception/upload.js +218 -0
- package/dist/shortcuts/recruit-stage/context.js +231 -0
- package/dist/shortcuts/recruit-stage/continuation.js +74 -0
- package/dist/shortcuts/recruit-stage/errors.js +65 -0
- package/dist/shortcuts/recruit-stage/host.js +109 -0
- package/dist/shortcuts/recruit-stage/index.js +115 -0
- package/dist/shortcuts/recruit-stage/manifest.js +29 -0
- package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
- package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
- package/dist/shortcuts/recruit-stage/operations/jd-create.js +719 -0
- package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
- package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
- package/dist/shortcuts/recruit-stage/operations/resume-import.js +1354 -0
- package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
- package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
- package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
- package/dist/shortcuts/recruit-stage/shared.js +608 -0
- package/dist/shortcuts/workflow/continuation.js +2 -1
- package/dist/shortcuts/workflow/index.js +2 -0
- package/dist/shortcuts/workflow/operations/form.js +32 -8
- package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +13 -6
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +50 -8
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +2 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +7 -39
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +51 -15
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +39 -14
- package/dist/shortcuts/workflow/operations/write.js +1 -1
- package/docs/_catalog.md +3 -0
- package/docs/agent-invoice.md +1 -1
- package/docs/archive-oa.md +1 -1
- package/docs/caizhilian.md +1 -1
- package/docs/calendar.md +1 -1
- package/docs/contract.md +1 -1
- package/docs/e10-auth.md +23 -1
- package/docs/ehr.md +2 -2
- package/docs/esb.md +1 -1
- package/docs/im.md +1 -1
- package/docs/invoice.md +8 -12
- package/docs/okr.md +1 -1
- package/docs/operation-manual.md +8 -0
- package/docs/reception.md +119 -0
- package/package.json +1 -1
- package/scripts/clean.mjs +43 -2
- package/scripts/install-skill-to-xiaoe.mjs +105 -0
- package/scripts/package-skill.mjs +4 -2
- package/skill-template/business-info.json +42 -2
- package/skill-template/domains/caizhilian.md +1 -1
- package/skill-template/domains/reception-flow.md +18 -0
- package/skill-template/domains/recruit-stage.md +29 -0
- package/skill-template/domains/yepiaotong.md +4 -7
- package/skills/weaver-e10-archive-oa/references/source-manifest.json +14 -14
- package/skills/weaver-e10-blog/references/blog-publish.md +1 -1
- package/skills/weaver-e10-blog/references/source-manifest.json +7 -7
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +21 -21
- package/skills/weaver-e10-caizhilian/references/write-protocol.md +2 -2
- package/skills/weaver-e10-calendar/SKILL.md +2 -1
- package/skills/weaver-e10-calendar/references/calendar-write.md +55 -1
- package/skills/weaver-e10-cowork/SKILL.md +1 -1
- package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
- package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
- package/skills/weaver-e10-ebuilder-form/references/custom-api.md +1 -1
- package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +1 -1
- package/skills/weaver-e10-esb/SKILL.md +2 -2
- package/skills/weaver-e10-esb/references/source-manifest.json +22 -22
- package/skills/weaver-e10-esb/references/trigger.md +2 -2
- package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +1 -1
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +1 -1
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +1 -1
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +29 -29
- package/skills/weaver-e10-jucailin/SKILL.md +1 -1
- package/skills/weaver-e10-jucailin/references/attend-write.md +10 -2
- package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
- package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
- package/skills/weaver-e10-jucailin/references/recruit-create.md +14 -5
- package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
- package/skills/weaver-e10-jucailin/references/recruit-write.md +5 -4
- package/skills/weaver-e10-mail/SKILL.md +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +39 -7
- package/skills/weaver-e10-meeting/references/meeting-change.md +1 -1
- package/skills/weaver-e10-meeting/references/meeting-create.md +1 -1
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +1 -1
- package/skills/weaver-e10-meeting/references/meeting-verification.md +1 -1
- package/skills/weaver-e10-okr/references/okr-align.md +1 -1
- package/skills/weaver-e10-okr/references/okr-keyresult.md +1 -1
- package/skills/weaver-e10-okr/references/okr-write.md +1 -1
- package/skills/weaver-e10-okr/references/source-manifest.json +8 -8
- package/skills/weaver-e10-plan/references/plan-report-write.md +1 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +20 -20
- package/skills/weaver-e10-qiyecheng/references/loan-management.md +1 -1
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +18 -18
- package/skills/weaver-e10-reception-flow/SKILL.md +173 -0
- package/skills/weaver-e10-reception-flow/assets//350/241/214/347/250/213/345/215/225/346/250/241/346/235/277-/346/263/233/345/276/256.docx +0 -0
- package/skills/weaver-e10-reception-flow/data/fields-snapshot.json +213 -0
- package/skills/weaver-e10-reception-flow/references/browsertool.md +68 -0
- package/skills/weaver-e10-reception-flow/references/env-pitfalls.md +51 -0
- package/skills/weaver-e10-reception-flow/references/fields-snapshot.md +227 -0
- package/skills/weaver-e10-reception-flow/references/guide.md +179 -0
- package/skills/weaver-e10-reception-flow/scripts/locate.sh +14 -0
- package/skills/weaver-e10-reception-flow/scripts/render.py +724 -0
- package/skills/weaver-e10-recruit-stage/SKILL.md +230 -0
- package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
- package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
- package/skills/weaver-e10-recruit-stage/lib/xiaoe-session.mjs +66 -0
- package/skills/weaver-e10-recruit-stage/references/position-templates.md +202 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +95 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +451 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
- package/skills/weaver-e10-shared/SKILL.md +1 -1
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +10 -2
- package/skills/weaver-e10-shijingran/SKILL.md +7 -6
- package/skills/weaver-e10-shijingran/references/operations.md +3 -1
- package/skills/weaver-e10-shijingran/references/source-manifest.json +30 -28
- package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
- package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +2 -0
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
- package/skills/weaver-e10-workflow/SKILL.md +4 -2
- package/skills/weaver-e10-workflow/aigroup/default.json +11 -9
- package/skills/weaver-e10-workflow/references/source-manifest.json +32 -32
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +23 -13
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +62 -20
- package/skills/weaver-e10-yepiaotong/SKILL.md +28 -36
- package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
- package/skills/weaver-e10-yepiaotong/references/invoice-detail.md +13 -10
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +2 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +194 -155
- package/skills/weaver-e10-yepiaotong/references/invoice-output-view.md +28 -32
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +5 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +8 -10
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +211 -616
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +149 -117
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +17 -20
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +20 -39
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +55 -22
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -17
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +88 -92
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +144 -354
- package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +7 -7
- package/dist/chunks/chunk-SPRHHSVC.js +0 -465
- package/dist/chunks/registry-KMHCPAAD.js +0 -22
- package/dist/shortcuts/invoice/markdown.js +0 -172
- package/dist/shortcuts/invoice/operations/issuing-qys.js +0 -424
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +0 -131
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +0 -179
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recruit-stage.resume.import.* —— 简历导入建人才(自 recruit-resume-import.mjs 1:1 移植)
|
|
3
|
+
*
|
|
4
|
+
* 三种输入(优先级:structured > fileId > resumeFile):
|
|
5
|
+
* - structured:大模型按 §13 schema 整理好的结构化简历对象(解析主通道;字典类字段给名称时
|
|
6
|
+
* 按数据字典唯一命中反查 optionId,歧义/缺失跳过,绝不写脏值)
|
|
7
|
+
* - fileId:已上传到 E10 的简历附件 id(原件仍会写进附件字段)
|
|
8
|
+
* - resumeFile:本地简历路径(两步 multipart 上传后同上)
|
|
9
|
+
* OCR 兜底(useOcr:true)仅显式开启时调用(闸门 = checkResumeExtraction);checkRpaValid 已废弃。
|
|
10
|
+
*
|
|
11
|
+
* 链路:查重(两级:getCandidateByPhone 页面同款 + checkUnique 表单唯一性,含软删除占用)
|
|
12
|
+
* → 建人才(首选 dataset/v2 主表+明细一次写入;被拦时回退 form/core 主表 + dataset update 补明细)
|
|
13
|
+
* → initTalentBatch 初始化批次 → matchTalentLabel 触发规则匹配 → 平台标签写入(含学校归属自动推导)
|
|
14
|
+
* → 读回校验明细行数 → 人才卡片/人才库定位双链接。
|
|
15
|
+
*
|
|
16
|
+
* 更新已有人才(updateTalentId):主表全量覆盖 + 明细整表覆盖(仅覆盖解析出数据的表),
|
|
17
|
+
* 受保护字段(is_blacklist/recruit_status/candidate_status/data_status)不覆盖,prepare 先出差异。
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import schoolTiers from '../school-tiers.json';
|
|
23
|
+
import { rsSuccess, schemaOf } from './types.js';
|
|
24
|
+
import { rsFail, rsValidationError, rsConfirmationError } from '../errors.js';
|
|
25
|
+
import { objIdOf, formIdOf, saveFormData, getFormLayoutMeta, runSaveButtonEsbActions, getTableMapping, invertTableMapping, datasetSaveFormData, datasetUpdateFormData, checkUnique, getCandidateByPhone, buildCardUrl, buildTalentViewUrl, talentLinkExtra, findMenuNode, randHex32, } from '../shared.js';
|
|
26
|
+
// 更新时不允许简历覆盖的字段(人工判定/流程状态,简历无权改写)
|
|
27
|
+
const PROTECTED_UPDATE_KEYS = ['is_blacklist', 'recruit_status', 'candidate_status', 'data_status'];
|
|
28
|
+
// ── 上传简历文件(两步:preUploadCheck 取 preUploadId → module/upload 带 file)→ fileid ──
|
|
29
|
+
// ⚠️ 预上传检查必须带文件,否则服务端 500;正式上传必带 name/MD5/lastModified/lastModifiedDate。
|
|
30
|
+
async function uploadResume(host, baseUrl, filePath, fieldId, formId) {
|
|
31
|
+
const buf = fs.readFileSync(filePath);
|
|
32
|
+
const filename = path.basename(filePath);
|
|
33
|
+
const baseForm = () => {
|
|
34
|
+
const fd = new FormData();
|
|
35
|
+
fd.append('compressImg', 'false');
|
|
36
|
+
fd.append('source', 'form');
|
|
37
|
+
fd.append('fieldId', fieldId);
|
|
38
|
+
fd.append('sourceRefId', formId);
|
|
39
|
+
fd.append('module', 'ebuilderform');
|
|
40
|
+
fd.append('authModule', 'ebuilderform');
|
|
41
|
+
fd.append('formId', formId);
|
|
42
|
+
fd.append('refId', formId);
|
|
43
|
+
fd.append('size', String(buf.length));
|
|
44
|
+
fd.append('commonText', JSON.stringify({ fieldId, formId }));
|
|
45
|
+
return fd;
|
|
46
|
+
};
|
|
47
|
+
const headers = {
|
|
48
|
+
langType: 'zh_CN', timeZoneOffset: '-480', ebBusinessId: formId,
|
|
49
|
+
Accept: 'application/json, text/plain, */*',
|
|
50
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
51
|
+
};
|
|
52
|
+
const withFile = () => { const fd = baseForm(); fd.append('file', new Blob([buf]), filename); return fd; };
|
|
53
|
+
const c = await host.request({ path: '/api/file/preUploadCheck', method: 'POST', multipart: withFile(), headers });
|
|
54
|
+
const preUploadId = c?.data?.preUploadId;
|
|
55
|
+
const uploadType = c?.data?.uploadType || 'common';
|
|
56
|
+
if (!preUploadId)
|
|
57
|
+
throw rsFail('简历预上传检查失败:' + JSON.stringify(c || {}).slice(0, 200));
|
|
58
|
+
const fd = baseForm();
|
|
59
|
+
const md5 = createHash('md5').update(buf).digest('hex');
|
|
60
|
+
const lm = String(Math.round(fs.statSync(filePath).mtimeMs || Date.now()));
|
|
61
|
+
fd.append('name', filename);
|
|
62
|
+
fd.append('MD5', md5);
|
|
63
|
+
fd.append('lastModified', lm);
|
|
64
|
+
fd.append('lastModifiedDate', lm);
|
|
65
|
+
fd.append('preUploadId', String(preUploadId));
|
|
66
|
+
fd.append('uploadType', String(uploadType));
|
|
67
|
+
fd.append('file', new Blob([buf]), filename);
|
|
68
|
+
const j = await host.request({ path: '/api/file/module/upload', method: 'POST', multipart: fd, headers });
|
|
69
|
+
if (!j?.data?.fileid)
|
|
70
|
+
throw rsFail('简历上传失败:' + JSON.stringify(j || {}).slice(0, 200));
|
|
71
|
+
return { fileid: String(j.data.fileid), name: j.data.name || filename };
|
|
72
|
+
}
|
|
73
|
+
// OCR 兜底解析(仅显式 useOcr 才调用):唯一闸门 = checkResumeExtraction
|
|
74
|
+
async function extractResume(host, fileid) {
|
|
75
|
+
const b = await host.request({ path: '/api/recruit/ocr/checkResumeExtraction', method: 'POST', body: {} });
|
|
76
|
+
if (b?.data !== true)
|
|
77
|
+
throw rsFail('当前环境未开启简历抽取(checkResumeExtraction=false):OCR 兜底不可用,请由大模型解析后走 structured 输入');
|
|
78
|
+
const r = await host.request({ path: '/api/recruit/ocr/resumeExtraction?fileId=' + encodeURIComponent(fileid), method: 'POST' });
|
|
79
|
+
const d = r?.data?.data;
|
|
80
|
+
if (!d || typeof d !== 'object')
|
|
81
|
+
throw rsFail('未从文件中解析出简历信息(可能是非简历文件)');
|
|
82
|
+
return d;
|
|
83
|
+
}
|
|
84
|
+
// 解析结果 → 人才基本信息 dataDetails(文本 content / 字典 dataOptions / 长文本 dataText)
|
|
85
|
+
function mapToTalent(fields, s) {
|
|
86
|
+
const out = [];
|
|
87
|
+
const put = (k, val, opts) => {
|
|
88
|
+
if (val === undefined || val === null || val === '')
|
|
89
|
+
return;
|
|
90
|
+
const id = fields[k];
|
|
91
|
+
if (!id)
|
|
92
|
+
return; // 环境无该字段则跳过,不臆造
|
|
93
|
+
out.push(opts ? { formField: { id }, dataOptions: opts } : { formField: { id }, content: String(val) });
|
|
94
|
+
};
|
|
95
|
+
const putLong = (k, val) => {
|
|
96
|
+
if (val === undefined || val === null || val === '')
|
|
97
|
+
return;
|
|
98
|
+
const id = fields[k];
|
|
99
|
+
if (!id)
|
|
100
|
+
return;
|
|
101
|
+
out.push({ formField: { id }, dataText: { content: String(val) } });
|
|
102
|
+
};
|
|
103
|
+
put('candidate_name', s.candidateName);
|
|
104
|
+
put('telephone', s.telephone);
|
|
105
|
+
put('email', s.email);
|
|
106
|
+
if (s.gender)
|
|
107
|
+
put('gender', s.genderName || s.gender, [{ optionId: String(s.gender), content: s.genderName || String(s.gender) }]);
|
|
108
|
+
put('birthday', s.birthday);
|
|
109
|
+
put('age', s.age);
|
|
110
|
+
if (s.highest_degree)
|
|
111
|
+
put('highest_degree', s.highest_degree_name || s.highest_degree, [{ optionId: String(s.highest_degree), content: s.highest_degree_name || String(s.highest_degree) }]);
|
|
112
|
+
// highest_degree_new 解析常给「序号」(如 3),不是 optionId → 只接受长数字 id,避免写脏值
|
|
113
|
+
if (/^\d{6,}$/.test(String(s.highest_degree_new || '')))
|
|
114
|
+
put('highest_degree_new', s.highest_degree_new);
|
|
115
|
+
put('college', s.college);
|
|
116
|
+
put('joinworkdate', s.joinworkdate);
|
|
117
|
+
put('graduate_date', s.graduate_date);
|
|
118
|
+
put('intend_position', s.intend_position);
|
|
119
|
+
put('intended_workplace', s.intended_workplace);
|
|
120
|
+
putLong('self_desc', s.self_desc);
|
|
121
|
+
putLong('work_ability', s.work_ability);
|
|
122
|
+
const job0 = Array.isArray(s.jobInformations) ? s.jobInformations[0] : null;
|
|
123
|
+
if (job0) {
|
|
124
|
+
put('current_position', job0.work_position);
|
|
125
|
+
put('current_industry', job0.co_name);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
// dataDetails(formField.id 口径)→ 数据集接口 mainTable(dataKey 口径;键必须用 config.dataKey)
|
|
130
|
+
function toDatasetMainTable(dataDetails, fields) {
|
|
131
|
+
const rev = {};
|
|
132
|
+
for (const [dk, fid] of Object.entries(fields || {}))
|
|
133
|
+
rev[String(fid)] = dk;
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const d of dataDetails || []) {
|
|
136
|
+
const dk = rev[String(d?.formField?.id || '')];
|
|
137
|
+
if (!dk)
|
|
138
|
+
continue;
|
|
139
|
+
let v;
|
|
140
|
+
if (Array.isArray(d.dataOptions)) {
|
|
141
|
+
const o = d.dataOptions[0] || {};
|
|
142
|
+
v = o.optionId !== undefined && o.optionId !== '' ? String(o.optionId) : String(o.content || '');
|
|
143
|
+
}
|
|
144
|
+
else if (d.dataText) {
|
|
145
|
+
v = String(d.dataText.content ?? '');
|
|
146
|
+
}
|
|
147
|
+
else if (d.content !== undefined) {
|
|
148
|
+
v = String(d.content);
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
continue;
|
|
152
|
+
if (v === '')
|
|
153
|
+
continue;
|
|
154
|
+
out[dk] = v;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
// 明细表元数据:全部从表单布局运行时解析(DetailTable config.subFormId + 列组件 config.fieldId),不写死
|
|
159
|
+
async function resolveSubTableMeta(ctx) {
|
|
160
|
+
const formId = formIdOf(ctx.cache, 'talent_pool');
|
|
161
|
+
const meta = await getFormLayoutMeta(ctx.host, formId);
|
|
162
|
+
const comps = meta?.formLayout?.pageLayouts?.[0]?.comps || [];
|
|
163
|
+
const tables = [];
|
|
164
|
+
for (const c of comps) {
|
|
165
|
+
const cfg = c.config || {};
|
|
166
|
+
if (c.type === 'DetailTable' && cfg.dataKey && cfg.subFormId) {
|
|
167
|
+
tables.push({ dataKey: String(cfg.dataKey), subFormId: String(cfg.subFormId), title: String(cfg.title || '') });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const out = {};
|
|
171
|
+
for (const t of tables) {
|
|
172
|
+
const cols = {};
|
|
173
|
+
for (const c of comps) {
|
|
174
|
+
const cfg = c.config || {};
|
|
175
|
+
if (String(cfg.subFormId) !== t.subFormId)
|
|
176
|
+
continue;
|
|
177
|
+
if (!cfg.dataKey || !cfg.fieldId)
|
|
178
|
+
continue;
|
|
179
|
+
const col = { id: String(cfg.fieldId), type: String(c.type || ''), title: String(cfg.title || '') };
|
|
180
|
+
// 日期区间(DateRange/DateInterval):物理上是两个子字段,各有独立 dataKey;不写死命名规律,按布局取
|
|
181
|
+
if (cfg.start && cfg.end && cfg.start.dataKey && cfg.end.dataKey) {
|
|
182
|
+
col.startKey = String(cfg.start.dataKey);
|
|
183
|
+
col.endKey = String(cfg.end.dataKey);
|
|
184
|
+
}
|
|
185
|
+
cols[String(cfg.dataKey)] = col;
|
|
186
|
+
}
|
|
187
|
+
out[t.dataKey] = { ...t, columns: cols };
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
// 字典 id → 名称
|
|
192
|
+
function makeDictLookup(cache) {
|
|
193
|
+
const byId = new Map();
|
|
194
|
+
for (const x of cache.dict?.raw || []) {
|
|
195
|
+
if (x.option_id && x.selection_name)
|
|
196
|
+
byId.set(String(x.option_id), x.selection_name);
|
|
197
|
+
}
|
|
198
|
+
return (id) => byId.get(String(id)) || '';
|
|
199
|
+
}
|
|
200
|
+
// 结构化简历 → 明细表行(值一律标量;关联/选择类解析给字典 id → 反查名称确认存在,查不到跳过不写脏值)
|
|
201
|
+
function buildSubDetails(structured, subs, cache) {
|
|
202
|
+
const byTable = {};
|
|
203
|
+
const stats = {};
|
|
204
|
+
const dictName = makeDictLookup(cache);
|
|
205
|
+
const addRows = (dataKey, rows, fieldMap) => {
|
|
206
|
+
const sub = subs[dataKey];
|
|
207
|
+
if (!sub || !Array.isArray(rows) || !rows.length)
|
|
208
|
+
return 0;
|
|
209
|
+
const list = [];
|
|
210
|
+
for (const r of rows) {
|
|
211
|
+
if (!r || typeof r !== 'object')
|
|
212
|
+
continue;
|
|
213
|
+
const row = {};
|
|
214
|
+
for (const [dk, conv] of Object.entries(fieldMap)) {
|
|
215
|
+
const col = sub.columns[dk];
|
|
216
|
+
if (!col)
|
|
217
|
+
continue;
|
|
218
|
+
const v = conv(r);
|
|
219
|
+
if (v === undefined || v === null || v === '')
|
|
220
|
+
continue;
|
|
221
|
+
// 日期区间:一个组件 = 两个物理字段(start/end 各有 dataKey),必须拆成两键提交
|
|
222
|
+
if (col.startKey && col.endKey) {
|
|
223
|
+
const parts = Array.isArray(v) ? v : String(v).split(/[,~]/);
|
|
224
|
+
const s = String(parts[0] || '').trim();
|
|
225
|
+
const e = String(parts[1] || '').trim();
|
|
226
|
+
// 「至今」的岗位/项目只有开始日期(实测 2026-11-05:原逻辑一旦缺 end 就整行跳过,
|
|
227
|
+
// 导致在职经历的起始日期丢失)。有哪个写哪个,两者都空才跳过。
|
|
228
|
+
if (s)
|
|
229
|
+
row[col.startKey] = s;
|
|
230
|
+
if (e)
|
|
231
|
+
row[col.endKey] = e;
|
|
232
|
+
if (!s && !e)
|
|
233
|
+
continue;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const scalar = typeof v === 'object' ? String(v.optionId ?? v.content ?? '') : String(v);
|
|
237
|
+
if (scalar === '')
|
|
238
|
+
continue;
|
|
239
|
+
row[dk] = scalar;
|
|
240
|
+
}
|
|
241
|
+
if (Object.keys(row).length)
|
|
242
|
+
list.push(row);
|
|
243
|
+
}
|
|
244
|
+
if (list.length)
|
|
245
|
+
byTable[dataKey] = list;
|
|
246
|
+
return list.length;
|
|
247
|
+
};
|
|
248
|
+
const dictOpt = (id) => {
|
|
249
|
+
if (id === undefined || id === null || id === '')
|
|
250
|
+
return '';
|
|
251
|
+
const s = String(id).trim();
|
|
252
|
+
return dictName(s) ? s : '';
|
|
253
|
+
};
|
|
254
|
+
const selOpt = (v) => {
|
|
255
|
+
if (!v)
|
|
256
|
+
return '';
|
|
257
|
+
const s = String(v).trim();
|
|
258
|
+
return /^\d{6,}$/.test(s) ? dictOpt(s) : '';
|
|
259
|
+
};
|
|
260
|
+
stats.rcrt_education_exp = addRows('rcrt_education_exp', structured.academicInformations, {
|
|
261
|
+
school: (r) => r.school,
|
|
262
|
+
edu_major: (r) => r.edu_major,
|
|
263
|
+
edu_education: (r) => dictOpt(r.edu_education),
|
|
264
|
+
degree: (r) => selOpt(r.degree),
|
|
265
|
+
study_date: (r) => r.study_date,
|
|
266
|
+
faculty: (r) => r.faculty,
|
|
267
|
+
research_direction: (r) => r.research_direction,
|
|
268
|
+
major_courses: (r) => r.major_courses,
|
|
269
|
+
});
|
|
270
|
+
stats.rcrt_work_exp = addRows('rcrt_work_exp', structured.jobInformations || structured.workExperiences, {
|
|
271
|
+
co_name: (r) => r.co_name || r.company,
|
|
272
|
+
work_position: (r) => r.work_position || r.position,
|
|
273
|
+
work_dept: (r) => r.work_dept || r.department,
|
|
274
|
+
work_desc: (r) => r.job_description || r.work_desc,
|
|
275
|
+
resign_cause: (r) => r.resign_cause,
|
|
276
|
+
start_end_date: (r) => [r.start_date, r.end_date].filter(Boolean).join(',') || r.work_date,
|
|
277
|
+
work_type: (r) => selOpt(r.work_type),
|
|
278
|
+
});
|
|
279
|
+
stats.rcrt_sxjl = addRows('rcrt_sxjl', structured.internshipExperiences, {
|
|
280
|
+
sx_co_name: (r) => r.sx_co_name,
|
|
281
|
+
position_job: (r) => r.position_job,
|
|
282
|
+
job_description: (r) => r.job_description,
|
|
283
|
+
entry_time: (r) => r.entry_time,
|
|
284
|
+
sx_work_type: (r) => selOpt(r.sx_work_type),
|
|
285
|
+
});
|
|
286
|
+
stats.rcrt_project_exp = addRows('rcrt_project_exp', structured.projectExperiences, {
|
|
287
|
+
pj_name: (r) => r.pj_name || r.project_name,
|
|
288
|
+
pj_role: (r) => r.pj_role,
|
|
289
|
+
pj_duty: (r) => r.pj_duty,
|
|
290
|
+
pj_introduction: (r) => r.pj_introduction || r.project_description,
|
|
291
|
+
pj_date: (r) => [r.start_date, r.end_date].filter(Boolean).join(','),
|
|
292
|
+
});
|
|
293
|
+
stats.rcrt_award_exp = addRows('rcrt_award_exp', structured.awardExperiences, {
|
|
294
|
+
award_name: (r) => r.award_name,
|
|
295
|
+
awarded: (r) => r.awarded,
|
|
296
|
+
credentials: (r) => r.credentials,
|
|
297
|
+
award_date: (r) => r.award_date,
|
|
298
|
+
});
|
|
299
|
+
stats.rcrt_language_skill = addRows('rcrt_language_skill', structured.languageAbilities, {
|
|
300
|
+
language_type: (r) => r.language_type,
|
|
301
|
+
mastery: (r) => selOpt(r.mastery),
|
|
302
|
+
ls_ability: (r) => selOpt(r.ls_ability),
|
|
303
|
+
literacy: (r) => selOpt(r.literacy),
|
|
304
|
+
});
|
|
305
|
+
return { byTable, stats };
|
|
306
|
+
}
|
|
307
|
+
// 读记录简要(查重结果展示用):主表 name/mobile/email/college
|
|
308
|
+
function makeBriefReader(ctx, formId, objId, fields) {
|
|
309
|
+
const rev = {};
|
|
310
|
+
for (const [dk, fid] of Object.entries(fields || {}))
|
|
311
|
+
rev[String(fid)] = dk;
|
|
312
|
+
return async (dataId) => {
|
|
313
|
+
try {
|
|
314
|
+
const r = await ctx.host.request({
|
|
315
|
+
path: '/api/ebuilder/form/form/core/getFormDataAndLayout', method: 'POST',
|
|
316
|
+
body: {
|
|
317
|
+
terminalType: 'PC', dataId, formId, module: 'ebuilderform', newLayout: true,
|
|
318
|
+
dataCrossLayout: false, permissionId: '', allReadOnly: true, initialDatas: [],
|
|
319
|
+
dynamicData: false, cost: false, customParam: { objId, dataId },
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const fd = r?.data?.formData;
|
|
323
|
+
if (!fd)
|
|
324
|
+
return null;
|
|
325
|
+
const dets = fd.dataDetails || [];
|
|
326
|
+
const get = (dk) => {
|
|
327
|
+
const d = dets.find((x) => rev[String(x.fieldId)] === dk);
|
|
328
|
+
if (!d)
|
|
329
|
+
return '';
|
|
330
|
+
return String(d.content ?? (d.dataText || {}).content ?? (d.dataOptions || []).map((o) => o.content).join(',') ?? '');
|
|
331
|
+
};
|
|
332
|
+
return { name: get('candidate_name'), mobile: get('telephone'), email: get('email'), college: get('college') };
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// 人才卡片 URL(环境缓存 ebdDetailPageId;自用系统追加 recruitType=useSelf)
|
|
340
|
+
function resolveTalentCardUrl(ctx, talentId) {
|
|
341
|
+
const pid = ctx.cache.ebdDetailPageId;
|
|
342
|
+
if (!pid)
|
|
343
|
+
return null;
|
|
344
|
+
return `${ctx.baseUrl}/sp/ebdpage/view/${pid}?talentId=${talentId}${talentLinkExtra(ctx.cache)}`;
|
|
345
|
+
}
|
|
346
|
+
// 人才库列表并打开指定人才:{baseUrl}/sp/ebdfpage/list/{listId}?talentId={id}&recruitType=useSelf
|
|
347
|
+
function resolveTalentListLocateUrl(ctx, talentId) {
|
|
348
|
+
const listId = ctx.cache.lists?.talent?.listId;
|
|
349
|
+
if (!listId || !talentId)
|
|
350
|
+
return null;
|
|
351
|
+
return `${ctx.baseUrl}/sp/ebdfpage/list/${listId}?talentId=${talentId}${talentLinkExtra(ctx.cache)}`;
|
|
352
|
+
}
|
|
353
|
+
// 读记录快照(更新前算 diff):主表 {dataKey: 展示值} + 各子表行数/格数
|
|
354
|
+
async function readTalentSnapshot(ctx, opts) {
|
|
355
|
+
const { host, formId, objId, fields, subMeta, dataId } = opts;
|
|
356
|
+
const L = await getFormLayoutMeta(ctx.host, formId);
|
|
357
|
+
const r = await ctx.host.request({
|
|
358
|
+
path: '/api/ebuilder/form/form/core/getFormDataAndLayout', method: 'POST',
|
|
359
|
+
body: {
|
|
360
|
+
terminalType: 'PC', dataId, formId, module: 'ebuilderform', newLayout: true,
|
|
361
|
+
layoutId: L?.formLayout?.id, layoutMultiId: L?.formLayoutMultiple?.id, dataCrossLayout: false,
|
|
362
|
+
permissionId: '', allReadOnly: true, initialDatas: [], dynamicData: false, cost: false,
|
|
363
|
+
customParam: { objId, dataId },
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
const dets = r?.data?.formData?.dataDetails || [];
|
|
367
|
+
const rev = {};
|
|
368
|
+
for (const [dk, fid] of Object.entries(fields || {}))
|
|
369
|
+
rev[String(fid)] = dk;
|
|
370
|
+
// 取值顺序:dataOptions.content > dataText.content > content(关联/选择字段 content 常为空,先取会误判)
|
|
371
|
+
const valOf = (d) => {
|
|
372
|
+
const opt = (d.dataOptions || []).map((o) => o.content).filter(Boolean).join(',');
|
|
373
|
+
const txt = (d.dataText || {}).content || '';
|
|
374
|
+
return opt || txt || (d.content ?? '');
|
|
375
|
+
};
|
|
376
|
+
const main = {};
|
|
377
|
+
const subRows = {};
|
|
378
|
+
const subCells = {};
|
|
379
|
+
const subIdToKey = {};
|
|
380
|
+
for (const [dk, sub] of Object.entries(subMeta || {}))
|
|
381
|
+
subIdToKey[String(sub.subFormId)] = dk;
|
|
382
|
+
const seen = {};
|
|
383
|
+
for (const d of dets) {
|
|
384
|
+
const subId = d.subFormId ? String(d.subFormId) : '';
|
|
385
|
+
if (subId) {
|
|
386
|
+
const dk = subIdToKey[subId] || subId;
|
|
387
|
+
(seen[dk] = seen[dk] || new Set()).add(String(d.dataIndex));
|
|
388
|
+
subCells[dk] = (subCells[dk] || 0) + 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const dk = rev[String(d.fieldId)];
|
|
392
|
+
if (dk)
|
|
393
|
+
main[dk] = String(valOf(d));
|
|
394
|
+
}
|
|
395
|
+
for (const [dk, s] of Object.entries(seen))
|
|
396
|
+
subRows[dk] = s.size;
|
|
397
|
+
return { main, subRows, subCells };
|
|
398
|
+
}
|
|
399
|
+
// ── 标签链路 ──
|
|
400
|
+
// a) 触发后台「人才自动标签」规则匹配(talentId 走 query,body 为空)
|
|
401
|
+
async function matchTalentLabel(host, talentId) {
|
|
402
|
+
const r = await host.request({ path: '/api/recruit/label/matchTalentLabel?talentId=' + encodeURIComponent(talentId), method: 'POST', body: {} });
|
|
403
|
+
return r?.data || null;
|
|
404
|
+
}
|
|
405
|
+
// b) 平台标签清单(公共 type0 / 个人 type1),运行时分页拉全——标签 id 各环境不同,绝不写死
|
|
406
|
+
// ── 平台标签清单:磁盘缓存(TTL 24h)+ 首轮取总页数后并发翻页 ──
|
|
407
|
+
// 背景(2026-11-04 实测 www.e-cology.com.cn):平台标签 **1924 个**,原实现「每类逐页串行 + 跨 type 累计
|
|
408
|
+
// total 判定」需 20+ 次串行请求 ≈ 20s,是简历链路最大瓶颈(不带标签 7.77s → 带标签 28.09s)。
|
|
409
|
+
// 标签属低频变更数据 → 磁盘缓存(24h)+ 并发翻页(限 5)后:首次 ~1-2s、后续 0 请求。
|
|
410
|
+
// ── 平台标签解析:按名精确查询优先,服务端未过滤时回退全量并发(不落盘)──
|
|
411
|
+
// 背景(2026-11-04 实测 www.e-cology.com.cn):平台标签 1924 个、绝大部分与招聘无关;
|
|
412
|
+
// 原「全量分页 + 磁盘缓存」在网页版小E 沙箱(容器不持久、盘可能只读)无收益,且缓存无用户维度
|
|
413
|
+
// (个人标签存在跨用户读取风险)。现改为:按名逐个并发查询(N 个标签 → 1 轮请求),
|
|
414
|
+
// 仅当确认服务端未按名过滤时才回退「全量并发翻页」,**不做磁盘缓存**。
|
|
415
|
+
/** 单名查询:并行 type0/type1;filtered=false 表示该 type 返回满页且 total 很大(疑似未按名过滤) */
|
|
416
|
+
async function queryTagByName(host, name) {
|
|
417
|
+
const pageSize = 100;
|
|
418
|
+
const bodyOf = (type) => ({ type, name, order: 8, pageSize, pageNo: 1, search: 1, tagGroup: 0, tagModules: ['ebuilderform'] });
|
|
419
|
+
const rs = await Promise.all([0, 1].map((type) => host.request({ path: '/api/my/tag/query', method: 'POST', body: bodyOf(type) }).catch(() => null)));
|
|
420
|
+
const items = [];
|
|
421
|
+
let filtered = true;
|
|
422
|
+
for (const r of rs) {
|
|
423
|
+
const d = r?.data || {};
|
|
424
|
+
const list = Array.isArray(d.data) ? d.data : [];
|
|
425
|
+
const total = Number(d.total || 0);
|
|
426
|
+
if (list.length >= pageSize && total >= pageSize)
|
|
427
|
+
filtered = false;
|
|
428
|
+
items.push(...list);
|
|
429
|
+
}
|
|
430
|
+
return { items, filtered };
|
|
431
|
+
}
|
|
432
|
+
// 单个 type 的全量标签:第 1 页拿 total → 剩余页按批并发(每批 5,上限 40 页防失控)
|
|
433
|
+
async function fetchTagsOfType(host, type) {
|
|
434
|
+
const pageSize = 100;
|
|
435
|
+
const bodyOf = (pageNo) => ({ type, name: '', order: 8, pageSize, pageNo, search: 1, tagGroup: 0, tagModules: ['ebuilderform'] });
|
|
436
|
+
const first = await host.request({ path: '/api/my/tag/query', method: 'POST', body: bodyOf(1) }).catch(() => null);
|
|
437
|
+
const d = first?.data || {};
|
|
438
|
+
const rows = Array.isArray(d.data) ? [...d.data] : [];
|
|
439
|
+
const total = Number(d.total || 0);
|
|
440
|
+
const pages = Math.min(40, Math.max(1, Math.ceil(total / pageSize)));
|
|
441
|
+
if (pages > 1 && rows.length) {
|
|
442
|
+
const rest = Array.from({ length: pages - 1 }, (_, i) => i + 2);
|
|
443
|
+
for (let i = 0; i < rest.length; i += 5) {
|
|
444
|
+
const batch = rest.slice(i, i + 5);
|
|
445
|
+
const rs = await Promise.all(batch.map((p) => host.request({ path: '/api/my/tag/query', method: 'POST', body: bodyOf(p) }).catch(() => null)));
|
|
446
|
+
for (const r of rs) {
|
|
447
|
+
const list = r?.data?.data;
|
|
448
|
+
if (Array.isArray(list))
|
|
449
|
+
rows.push(...list);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return rows;
|
|
454
|
+
}
|
|
455
|
+
async function listPlatformTags(host) {
|
|
456
|
+
const all = [];
|
|
457
|
+
for (const type of [0, 1]) {
|
|
458
|
+
try {
|
|
459
|
+
all.push(...await fetchTagsOfType(host, type));
|
|
460
|
+
}
|
|
461
|
+
catch { /* 单类型失败不影响另一类 */ }
|
|
462
|
+
}
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
return all.filter((t) => t && t.tagId && !seen.has(String(t.tagId)) && seen.add(String(t.tagId)));
|
|
465
|
+
}
|
|
466
|
+
// 按名解析 → tagId(不存在进 missing,不臆造;同名优先公共标签)
|
|
467
|
+
// 默认场景(简历导入)names 通常只有 1~4 个学校标签 → 1 轮并发按名查询即可;用户显式要求时才扩到其他标签。
|
|
468
|
+
async function resolveTags(host, names) {
|
|
469
|
+
const norm = (s) => String(s == null ? '' : s).trim().toLowerCase();
|
|
470
|
+
const byName = new Map();
|
|
471
|
+
const uniqNames = [...new Set((names || []).map((n) => String(n)).filter(Boolean))];
|
|
472
|
+
let availableCount;
|
|
473
|
+
if (!uniqNames.length)
|
|
474
|
+
return { resolved: [], found: [], missing: [], available: [], availableCount };
|
|
475
|
+
// ① 按名并发查询(N 个名字 → 1 轮)
|
|
476
|
+
const probes = await Promise.all(uniqNames.map((n) => queryTagByName(host, n).catch(() => ({ items: [], filtered: true }))));
|
|
477
|
+
const nameQueryUsable = probes.every((p) => p.filtered);
|
|
478
|
+
if (nameQueryUsable) {
|
|
479
|
+
for (const p of probes) {
|
|
480
|
+
for (const t of p.items) {
|
|
481
|
+
if (!t || !t.tagId)
|
|
482
|
+
continue;
|
|
483
|
+
const k = norm(t.name);
|
|
484
|
+
const prev = byName.get(k);
|
|
485
|
+
if (!prev)
|
|
486
|
+
byName.set(k, t);
|
|
487
|
+
else if ((prev.type ?? 0) !== 0 && t.type === 0)
|
|
488
|
+
byName.set(k, t);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// ② 回退:服务端疑似未按名过滤 → 全量并发(不落盘)
|
|
494
|
+
const dict = await listPlatformTags(host);
|
|
495
|
+
availableCount = dict.length;
|
|
496
|
+
for (const t of dict) {
|
|
497
|
+
const k = norm(t.name);
|
|
498
|
+
const prev = byName.get(k);
|
|
499
|
+
if (!prev)
|
|
500
|
+
byName.set(k, t);
|
|
501
|
+
else if ((prev.type ?? 0) !== 0 && t.type === 0)
|
|
502
|
+
byName.set(k, t);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const resolved = [];
|
|
506
|
+
const found = [];
|
|
507
|
+
const missing = [];
|
|
508
|
+
for (const n of names || []) {
|
|
509
|
+
const t = byName.get(norm(n));
|
|
510
|
+
if (!t) {
|
|
511
|
+
missing.push(n);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
resolved.push({ id: String(t.tagId), name: t.name, type: t.type ?? 0 });
|
|
515
|
+
found.push(t.name);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
resolved, found, missing, availableCount,
|
|
519
|
+
available: [...byName.values()].map((t) => ({ tagId: String(t.tagId), name: t.name, type: t.type ?? 0 })),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
// c) 写入实体标签(先确认存在再写;不存在不写)
|
|
523
|
+
async function addEntityTags(host, talentId, names) {
|
|
524
|
+
if (!names || !names.length)
|
|
525
|
+
return { applied: [], skipped: [] };
|
|
526
|
+
const { resolved, found, missing } = await resolveTags(host, names);
|
|
527
|
+
if (!resolved.length)
|
|
528
|
+
return { applied: [], skipped: missing };
|
|
529
|
+
const r = await host.request({
|
|
530
|
+
path: '/api/my/tag/common/batchAddEntityTag', method: 'POST',
|
|
531
|
+
body: { tags: resolved, batchDatas: [{ entityId: String(talentId), module: 'ebuilderform' }] },
|
|
532
|
+
});
|
|
533
|
+
if (r?.status === false)
|
|
534
|
+
return { applied: [], skipped: [...missing, ...found], error: r?.msg };
|
|
535
|
+
return { applied: found, skipped: missing };
|
|
536
|
+
}
|
|
537
|
+
// d) 读回某人才当前标签(data[].content = 标签名)
|
|
538
|
+
async function queryEntityTags(host, talentId) {
|
|
539
|
+
const out = [];
|
|
540
|
+
for (const type of [0, 1]) {
|
|
541
|
+
try {
|
|
542
|
+
const r = await host.request({ path: '/api/my/tag/common/queryEntityTag', method: 'POST', body: { entityId: String(talentId), module: 'ebuilderform', type } });
|
|
543
|
+
const list = r?.data;
|
|
544
|
+
if (Array.isArray(list))
|
|
545
|
+
for (const t of list)
|
|
546
|
+
out.push(t.content || t.name || t.labelName || String(t.id || ''));
|
|
547
|
+
}
|
|
548
|
+
catch { /* 读标签失败不影响主流程 */ }
|
|
549
|
+
}
|
|
550
|
+
return [...new Set(out.filter(Boolean))];
|
|
551
|
+
}
|
|
552
|
+
// ── 学校归属标签推导(985/211/双一流):内置全国名单(school-tiers.json 随包分发)──
|
|
553
|
+
// 由解析出的毕业院校直接推导候选,再走「环境标签清单按名求交」落库(清单里没有的自动 skip)。
|
|
554
|
+
function deriveSchoolTags(structured) {
|
|
555
|
+
// 名单随 bundle 内联(resolveJsonModule):不能用 fs 读相对路径——bundle 后 import.meta.url
|
|
556
|
+
// 指向 dist/chunks,找不到 school-tiers.json(2026-11-04 实测:学校标签推导曾因此静默失效)。
|
|
557
|
+
const cfg = schoolTiers;
|
|
558
|
+
const norm = (x) => String(x || '')
|
|
559
|
+
.replace(/[\s ]/g, '')
|
|
560
|
+
.replace(/[((][^))]*[))]/g, '')
|
|
561
|
+
.replace(/^(.*?(?:大学|学院|学校)).*(?:分校|校区)$/, '$1');
|
|
562
|
+
const canon = (x) => String(cfg.aliases?.[norm(x)] || norm(x));
|
|
563
|
+
const tiers = {
|
|
564
|
+
'985': new Set(cfg['985'].map(canon)),
|
|
565
|
+
'211': new Set(cfg['211'].map(canon)),
|
|
566
|
+
'双一流': new Set(cfg['双一流'].map(canon)),
|
|
567
|
+
};
|
|
568
|
+
const schools = [...new Set(((structured || {}).academicInformations || [])
|
|
569
|
+
.map((r) => String(r.school || '').trim()).filter(Boolean))];
|
|
570
|
+
const candidates = [];
|
|
571
|
+
const detail = [];
|
|
572
|
+
const tierNames = ['985', '211', '双一流'];
|
|
573
|
+
for (const sch of schools) {
|
|
574
|
+
const c = canon(sch);
|
|
575
|
+
const hit = tierNames.filter((t) => tiers[t].has(c));
|
|
576
|
+
detail.push({ school: sch, tiers: hit });
|
|
577
|
+
for (const t of hit)
|
|
578
|
+
if (!candidates.includes(t))
|
|
579
|
+
candidates.push(t);
|
|
580
|
+
if (sch && !candidates.includes(sch))
|
|
581
|
+
candidates.push(sch); // 校名本身可能也是环境标签(不存在则自动 skip)
|
|
582
|
+
}
|
|
583
|
+
return { candidates, detail, schools };
|
|
584
|
+
}
|
|
585
|
+
// 文本直建:字典名称 → optionId 归一(唯一命中才写;歧义/缺失跳过并记 warning,宁缺勿错)
|
|
586
|
+
function normalizeTextStructured(cache, s, warnings) {
|
|
587
|
+
const raw = cache.dict?.raw || [];
|
|
588
|
+
const byName = cache.dict?.by_name || {};
|
|
589
|
+
const dictIdByName = (name) => {
|
|
590
|
+
const k = String(name || '').trim();
|
|
591
|
+
if (!k)
|
|
592
|
+
return '';
|
|
593
|
+
let ids = byName[k];
|
|
594
|
+
if (!ids || !ids.length)
|
|
595
|
+
ids = raw.filter((x) => x.selection_name === k).map((x) => x.option_id);
|
|
596
|
+
const uniq = [...new Set((ids || []).map(String))];
|
|
597
|
+
if (!uniq.length) {
|
|
598
|
+
warnings.push('字典未找到「' + k + '」→ 该字段跳过');
|
|
599
|
+
return '';
|
|
600
|
+
}
|
|
601
|
+
if (uniq.length > 1) {
|
|
602
|
+
warnings.push('字典「' + k + '」命中 ' + uniq.length + ' 个同名选项(歧义)→ 该字段跳过');
|
|
603
|
+
return '';
|
|
604
|
+
}
|
|
605
|
+
return uniq[0];
|
|
606
|
+
};
|
|
607
|
+
const fixRows = (rows, keys) => {
|
|
608
|
+
for (const r of rows || []) {
|
|
609
|
+
if (!r || typeof r !== 'object')
|
|
610
|
+
continue;
|
|
611
|
+
for (const k of keys) {
|
|
612
|
+
const v = String(r[k] || '').trim();
|
|
613
|
+
if (!v || /^\d{6,}$/.test(v))
|
|
614
|
+
continue;
|
|
615
|
+
const id = dictIdByName(v);
|
|
616
|
+
if (id)
|
|
617
|
+
r[k] = id;
|
|
618
|
+
else
|
|
619
|
+
delete r[k];
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
if (!s.gender && s.genderName) {
|
|
624
|
+
const id = dictIdByName(s.genderName);
|
|
625
|
+
if (id)
|
|
626
|
+
s.gender = id;
|
|
627
|
+
}
|
|
628
|
+
if (!s.highest_degree && s.highest_degree_name) {
|
|
629
|
+
const id = dictIdByName(s.highest_degree_name);
|
|
630
|
+
if (id)
|
|
631
|
+
s.highest_degree = id;
|
|
632
|
+
}
|
|
633
|
+
fixRows(s.academicInformations, ['edu_education', 'degree']);
|
|
634
|
+
fixRows(s.jobInformations || s.workExperiences, ['work_type']);
|
|
635
|
+
fixRows(s.internshipExperiences, ['sx_work_type']);
|
|
636
|
+
fixRows(s.languageAbilities, ['mastery', 'ls_ability', 'literacy']);
|
|
637
|
+
return s;
|
|
638
|
+
}
|
|
639
|
+
// ── 日期格式预检与归一(prepare 阶段前置,避免格式引发连锁失败)──
|
|
640
|
+
// 实测(2026-11-05):人才表单接口只接受 yyyy-MM-dd。模型常给「2020.11」「2008-08」「2004.09-2008.07」,
|
|
641
|
+
// 会让 dataset 新增整批校验失败 → 回退 form/core 建主表 → 回退补明细又被同一格式拦下 → 明细全丢却报 success,
|
|
642
|
+
// 且实测多花 3.4s(5523ms vs 2105ms)。此处统一归一:
|
|
643
|
+
// · 只有年月 → 开始补 -01、结束补当月最后一天(语义约定,写入 warnings 明示)
|
|
644
|
+
// · 「至今/现在」→ 空(表示至今,不伪造结束日)
|
|
645
|
+
// · 区间统一成 "start,end"(comma 分隔,供 DateRange 拆两键)
|
|
646
|
+
// · 无法识别 → 跳过该字段并告警(宁缺勿整批失败),绝不静默改写
|
|
647
|
+
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
648
|
+
const ONGOING_RE = /至今|现在|今|now|present/i;
|
|
649
|
+
const RE_RANGE_FULL = /^\s*(\d{4}[.\-/年]\d{1,2}[.\-/月]\d{1,2}\s*日?)\s*[-–—~至,,]\s*(\d{4}[.\-/年]\d{1,2}[.\-/月]\d{1,2}\s*日?|至今|现在|今)\s*$/;
|
|
650
|
+
const RE_RANGE_YM = /^\s*(\d{4}[.\-/年]\d{1,2})\s*[-–—~至,,]\s*(\d{4}[.\-/年]\d{1,2}|至今|现在|今)\s*$/;
|
|
651
|
+
/** 单日期片段 → yyyy-MM-dd;isEnd=true 且缺日时补该月最后一天。无法解析返回 ''。 */
|
|
652
|
+
function normDatePart(raw, isEnd) {
|
|
653
|
+
const s = String(raw ?? '').trim();
|
|
654
|
+
if (!s)
|
|
655
|
+
return '';
|
|
656
|
+
if (ONGOING_RE.test(s))
|
|
657
|
+
return '';
|
|
658
|
+
if (DATE_ONLY_RE.test(s))
|
|
659
|
+
return s;
|
|
660
|
+
const m = s.match(/^(\d{4})\s*[.\-/年]?\s*(\d{1,2})?\s*[.\-/月]?\s*(\d{1,2})?\s*日?$/);
|
|
661
|
+
if (!m)
|
|
662
|
+
return '';
|
|
663
|
+
const y = Number(m[1]);
|
|
664
|
+
const mo = m[2] ? Number(m[2]) : 1;
|
|
665
|
+
if (!y || mo < 1 || mo > 12)
|
|
666
|
+
return '';
|
|
667
|
+
const lastDay = new Date(y, mo, 0).getDate();
|
|
668
|
+
let d;
|
|
669
|
+
if (m[3]) {
|
|
670
|
+
d = Number(m[3]);
|
|
671
|
+
if (d < 1 || d > lastDay)
|
|
672
|
+
return '';
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
d = isEnd ? lastDay : 1;
|
|
676
|
+
}
|
|
677
|
+
return y + '-' + String(mo).padStart(2, '0') + '-' + String(d).padStart(2, '0');
|
|
678
|
+
}
|
|
679
|
+
/** 日期区间 → "start,end";只有单值时返回单值。 */
|
|
680
|
+
function normDateRange(raw) {
|
|
681
|
+
const s = Array.isArray(raw)
|
|
682
|
+
? raw.map((x) => String(x ?? '').trim()).filter(Boolean).join(',')
|
|
683
|
+
: String(raw ?? '').trim();
|
|
684
|
+
if (!s)
|
|
685
|
+
return '';
|
|
686
|
+
const m = s.match(RE_RANGE_FULL) || s.match(RE_RANGE_YM);
|
|
687
|
+
if (m) {
|
|
688
|
+
const a = normDatePart(m[1], false);
|
|
689
|
+
if (!a)
|
|
690
|
+
return '';
|
|
691
|
+
const b = normDatePart(m[2], true);
|
|
692
|
+
return b ? a + ',' + b : a;
|
|
693
|
+
}
|
|
694
|
+
return normDatePart(s, false);
|
|
695
|
+
}
|
|
696
|
+
/** 就地归一结构化简历里的全部日期字段,把每处改动写进 warnings(不静默改写)。 */
|
|
697
|
+
function normalizeDates(s, warnings) {
|
|
698
|
+
if (!s || typeof s !== 'object')
|
|
699
|
+
return;
|
|
700
|
+
const one = (label, val, isEnd = false) => {
|
|
701
|
+
const before = String(val ?? '').trim();
|
|
702
|
+
if (!before)
|
|
703
|
+
return '';
|
|
704
|
+
if (ONGOING_RE.test(before))
|
|
705
|
+
return '';
|
|
706
|
+
const after = normDatePart(before, isEnd);
|
|
707
|
+
if (!after) {
|
|
708
|
+
warnings.push('日期无法识别,已跳过该字段(避免接口整批校验失败):' + label + ' = 「' + before + '」');
|
|
709
|
+
return '';
|
|
710
|
+
}
|
|
711
|
+
if (after !== before)
|
|
712
|
+
warnings.push('日期已规范化:' + label + ' 「' + before + '」→「' + after + '」');
|
|
713
|
+
return after;
|
|
714
|
+
};
|
|
715
|
+
// 主表单值日期
|
|
716
|
+
for (const k of ['joinworkdate', 'graduate_date', 'birthday']) {
|
|
717
|
+
if (s[k] === undefined || s[k] === null)
|
|
718
|
+
continue;
|
|
719
|
+
s[k] = one(k, s[k], false);
|
|
720
|
+
}
|
|
721
|
+
// 教育经历:区间(一个组件 = 起止两个物理字段,必须 comma 分隔)
|
|
722
|
+
if (Array.isArray(s.academicInformations)) {
|
|
723
|
+
for (let i = 0; i < s.academicInformations.length; i++) {
|
|
724
|
+
const r = s.academicInformations[i];
|
|
725
|
+
if (!r || typeof r !== 'object' || r.study_date === undefined)
|
|
726
|
+
continue;
|
|
727
|
+
const before = String(r.study_date ?? '').trim();
|
|
728
|
+
if (!before)
|
|
729
|
+
continue;
|
|
730
|
+
const after = normDateRange(before);
|
|
731
|
+
if (!after) {
|
|
732
|
+
warnings.push('日期区间无法识别,已跳过(避免接口整批校验失败):academicInformations[' + i + '].study_date = 「' + before + '」');
|
|
733
|
+
r.study_date = '';
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
if (after !== before)
|
|
737
|
+
warnings.push('日期已规范化:academicInformations[' + i + '].study_date 「' + before + '」→「' + after + '」');
|
|
738
|
+
r.study_date = after;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// 工作/项目经历:起止为两个独立字段,需分别归一后由 buildSubDetails 拼成区间
|
|
743
|
+
for (const key of ['jobInformations', 'workExperiences', 'projectExperiences']) {
|
|
744
|
+
const arr = s[key];
|
|
745
|
+
if (!Array.isArray(arr))
|
|
746
|
+
continue;
|
|
747
|
+
for (let i = 0; i < arr.length; i++) {
|
|
748
|
+
const r = arr[i];
|
|
749
|
+
if (!r || typeof r !== 'object')
|
|
750
|
+
continue;
|
|
751
|
+
if (r.start_date !== undefined && r.start_date !== null) {
|
|
752
|
+
r.start_date = one(key + '[' + i + '].start_date', r.start_date, false);
|
|
753
|
+
}
|
|
754
|
+
if (r.end_date !== undefined && r.end_date !== null) {
|
|
755
|
+
r.end_date = one(key + '[' + i + '].end_date', r.end_date, true);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// 实习入职时间 / 获奖日期:单值日期
|
|
760
|
+
for (const [key, field] of [['internshipExperiences', 'entry_time'], ['awardExperiences', 'award_date']]) {
|
|
761
|
+
const arr = s[key];
|
|
762
|
+
if (!Array.isArray(arr))
|
|
763
|
+
continue;
|
|
764
|
+
for (let i = 0; i < arr.length; i++) {
|
|
765
|
+
const r = arr[i];
|
|
766
|
+
if (!r || typeof r !== 'object')
|
|
767
|
+
continue;
|
|
768
|
+
if (r[field] !== undefined && r[field] !== null)
|
|
769
|
+
r[field] = one(key + '[' + i + '].' + field, r[field], false);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// 读回校验:统计各子表实际落库行数(不能只看写入接口返回,必须回读)
|
|
774
|
+
async function verifySubRows(ctx, formId, objId, dataId) {
|
|
775
|
+
const L = await getFormLayoutMeta(ctx.host, formId);
|
|
776
|
+
const r = await ctx.host.request({
|
|
777
|
+
path: '/api/ebuilder/form/form/core/getFormDataAndLayout', method: 'POST',
|
|
778
|
+
body: {
|
|
779
|
+
terminalType: 'PC', dataId, formId, module: 'ebuilderform', newLayout: true,
|
|
780
|
+
layoutId: L?.formLayout?.id, layoutMultiId: L?.formLayoutMultiple?.id, dataCrossLayout: false,
|
|
781
|
+
permissionId: '', allReadOnly: true, initialDatas: [], dynamicData: false, cost: false,
|
|
782
|
+
customParam: { objId, dataId },
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
const dets = r?.data?.formData?.dataDetails || [];
|
|
786
|
+
const seen = {};
|
|
787
|
+
for (const d of dets) {
|
|
788
|
+
if (!d.subFormId)
|
|
789
|
+
continue;
|
|
790
|
+
const k = String(d.subFormId);
|
|
791
|
+
(seen[k] = seen[k] || new Set()).add(String(d.dataIndex));
|
|
792
|
+
}
|
|
793
|
+
const rowsBySubFormId = {};
|
|
794
|
+
for (const [k, s] of Object.entries(seen))
|
|
795
|
+
rowsBySubFormId[k] = s.size;
|
|
796
|
+
return {
|
|
797
|
+
cells: dets.filter((d) => d.subFormId).length,
|
|
798
|
+
rowsBySubFormId,
|
|
799
|
+
rowTotal: Object.values(rowsBySubFormId).reduce((a, b) => a + b, 0),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async function loadStructured(ctx, args) {
|
|
803
|
+
const warnings = [];
|
|
804
|
+
if (args.structured !== undefined) {
|
|
805
|
+
const s = typeof args.structured === 'string' ? JSON.parse(args.structured) : args.structured;
|
|
806
|
+
if (!s || typeof s !== 'object')
|
|
807
|
+
throw rsValidationError('structured_invalid', 'structured 必须是 JSON 对象');
|
|
808
|
+
if (!String(s.telephone || '').trim() && !String(s.candidateName || '').trim()) {
|
|
809
|
+
throw rsValidationError('structured_invalid', '结构化简历缺少 telephone 与 candidateName(至少要有手机号或姓名,否则无法查重建档)');
|
|
810
|
+
}
|
|
811
|
+
const normalizedInline = normalizeTextStructured(ctx.cache, s, warnings);
|
|
812
|
+
normalizeDates(normalizedInline, warnings);
|
|
813
|
+
return { structured: normalizedInline, textMode: true, warnings };
|
|
814
|
+
}
|
|
815
|
+
if (args.structuredFile) {
|
|
816
|
+
const p = path.resolve(String(args.structuredFile));
|
|
817
|
+
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
818
|
+
if (!s || typeof s !== 'object')
|
|
819
|
+
throw rsValidationError('structured_invalid', 'structuredFile 内容不是合法 JSON 对象');
|
|
820
|
+
if (!String(s.telephone || '').trim() && !String(s.candidateName || '').trim()) {
|
|
821
|
+
throw rsValidationError('structured_invalid', '结构化简历缺少 telephone 与 candidateName');
|
|
822
|
+
}
|
|
823
|
+
const normalizedFile = normalizeTextStructured(ctx.cache, s, warnings);
|
|
824
|
+
normalizeDates(normalizedFile, warnings);
|
|
825
|
+
return { structured: normalizedFile, textMode: true, warnings };
|
|
826
|
+
}
|
|
827
|
+
return { structured: null, textMode: false, warnings };
|
|
828
|
+
}
|
|
829
|
+
async function buildImport(ctx, args) {
|
|
830
|
+
const { host, baseUrl, appId, cache } = ctx;
|
|
831
|
+
const objId = objIdOf(cache, 'talent_pool');
|
|
832
|
+
const formId = formIdOf(cache, 'talent_pool');
|
|
833
|
+
const fields = cache.fields[objId] || {};
|
|
834
|
+
if (!fields.candidate_name || !fields.telephone)
|
|
835
|
+
throw rsFail('环境缓存缺人才表单字段 candidate_name/telephone');
|
|
836
|
+
const resumeFieldId = fields.original_resume || '937153853968539861';
|
|
837
|
+
const { structured: loaded, textMode, warnings: structuredTextWarnings } = await loadStructured(ctx, args);
|
|
838
|
+
let structured = loaded;
|
|
839
|
+
// 上传:有本地文件时把原件传进附件字段
|
|
840
|
+
let fileid = args.fileId ? String(args.fileId) : '';
|
|
841
|
+
let fileName = null;
|
|
842
|
+
if (!fileid && args.resumeFile) {
|
|
843
|
+
const up = await uploadResume(host, baseUrl, path.resolve(String(args.resumeFile)), resumeFieldId, formId);
|
|
844
|
+
fileid = up.fileid;
|
|
845
|
+
fileName = up.name;
|
|
846
|
+
}
|
|
847
|
+
if (!textMode) {
|
|
848
|
+
if (!fileid)
|
|
849
|
+
throw rsFail('需要 resumeFile 或 fileId(附件必须上传);解析请由大模型按 schema 整理后走 structured 输入');
|
|
850
|
+
if (!args.useOcr) {
|
|
851
|
+
throw rsFail('解析默认由大模型完成(structured 输入),不自动调 OCR。确需 OCR 兜底(如扫描件)请显式 useOcr:true 重试(闸门=checkResumeExtraction)。');
|
|
852
|
+
}
|
|
853
|
+
structured = await extractResume(host, fileid);
|
|
854
|
+
}
|
|
855
|
+
// 学校归属标签推导(并入标签候选)
|
|
856
|
+
const schoolTagInfo = args.noSchoolTags ? { candidates: [], detail: [], schools: [] } : deriveSchoolTags(structured);
|
|
857
|
+
const tagNames = [...(args.tags || [])];
|
|
858
|
+
for (const t of schoolTagInfo.candidates)
|
|
859
|
+
if (!tagNames.includes(t))
|
|
860
|
+
tagNames.push(t);
|
|
861
|
+
// 字段映射 + 附件
|
|
862
|
+
const dataDetails = mapToTalent(fields, structured);
|
|
863
|
+
if (fileid) {
|
|
864
|
+
dataDetails.push({ formField: { id: resumeFieldId }, dataOptions: [{ optionId: String(fileid), content: fileName || '简历附件', type: 'file' }] });
|
|
865
|
+
}
|
|
866
|
+
// 渠道:显式 channel 优先;未指定时固定「手动录入」
|
|
867
|
+
let channelLabel = null;
|
|
868
|
+
if (args.channel) {
|
|
869
|
+
let channelOption;
|
|
870
|
+
for (const x of cache.dict?.raw || [])
|
|
871
|
+
if (x.selection_name === args.channel)
|
|
872
|
+
channelOption = x.option_id;
|
|
873
|
+
if (!channelOption)
|
|
874
|
+
throw rsFail('渠道「' + args.channel + '」未在字典中找到(不得臆造 optionId)');
|
|
875
|
+
dataDetails.push({ formField: { id: fields.channel }, dataOptions: [{ optionId: channelOption, content: args.channel }] });
|
|
876
|
+
channelLabel = args.channel;
|
|
877
|
+
}
|
|
878
|
+
else if (fields.channel) {
|
|
879
|
+
const byName = cache.dict?.by_name || {};
|
|
880
|
+
const manualId = (byName['手动录入'] || byName['手工录入'] || [])[0];
|
|
881
|
+
if (!manualId)
|
|
882
|
+
throw rsFail('数据字典中未找到「手动录入」,无法固定简历来源(不得臆造 optionId)');
|
|
883
|
+
dataDetails.push({ formField: { id: fields.channel }, dataOptions: [{ optionId: String(manualId), content: '手动录入' }] });
|
|
884
|
+
channelLabel = '手动录入(固定)';
|
|
885
|
+
}
|
|
886
|
+
// 是否黑名单:固定「否」(选项 id 运行时按名称反查)
|
|
887
|
+
if (fields.is_blacklist) {
|
|
888
|
+
const opt = (cache.field_options?.[objId] || {})[fields.is_blacklist] || {};
|
|
889
|
+
const noId = Object.keys(opt).find((k) => opt[k] === '否');
|
|
890
|
+
if (noId)
|
|
891
|
+
dataDetails.push({ formField: { id: fields.is_blacklist }, dataOptions: [{ optionId: String(noId), content: '否' }] });
|
|
892
|
+
}
|
|
893
|
+
// 明细表:数据集通道;节点名 detail1..N 由 getTableMapping 运行时解析(顺序≠布局顺序)
|
|
894
|
+
let subBuild = { byTable: {}, stats: {} };
|
|
895
|
+
let subMeta = {};
|
|
896
|
+
let subError = null;
|
|
897
|
+
const detailsPayload = {};
|
|
898
|
+
const detailNodes = {};
|
|
899
|
+
const unmappedTables = [];
|
|
900
|
+
try {
|
|
901
|
+
subMeta = await resolveSubTableMeta(ctx);
|
|
902
|
+
subBuild = buildSubDetails(structured, subMeta, cache);
|
|
903
|
+
const mapping = await getTableMapping(host, objId);
|
|
904
|
+
const nodeOf = invertTableMapping(mapping);
|
|
905
|
+
for (const [dk, rows] of Object.entries(subBuild.byTable)) {
|
|
906
|
+
const sub = subMeta[dk];
|
|
907
|
+
const node = sub ? nodeOf[String(sub.subFormId)] : null;
|
|
908
|
+
if (!node) {
|
|
909
|
+
unmappedTables.push(dk);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
detailsPayload[node] = rows;
|
|
913
|
+
detailNodes[dk] = node;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch (e) {
|
|
917
|
+
subError = e.message;
|
|
918
|
+
}
|
|
919
|
+
const subRowTotal = Object.values(subBuild.byTable).reduce((n, rows) => n + rows.length, 0);
|
|
920
|
+
return {
|
|
921
|
+
objId, formId, fields, fileid, fileName, structured, textMode, structuredTextWarnings,
|
|
922
|
+
dataDetails, detailsPayload, detailNodes, unmappedTables, subError, subBuild, subRowTotal,
|
|
923
|
+
subMeta, tagNames, schoolTagInfo, channelLabel, resumeFieldId,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
// 两级查重:① getCandidateByPhone(页面同款,命中即「已绑定人才」)② checkUnique(表单唯一性,捕捉无批次记录与软删除占用)
|
|
927
|
+
async function findExistingTalents(ctx, opts) {
|
|
928
|
+
const { objId, formId, fields, mobile } = opts;
|
|
929
|
+
const blank = { checked: false, unique: true, byPhone: null, byPhoneError: undefined, formConflicts: { existing: [], unreadable: [] }, ids: [] };
|
|
930
|
+
if (!mobile)
|
|
931
|
+
return blank;
|
|
932
|
+
// 两级查重并行:getCandidateByPhone(页面同款)与 checkUnique(表单唯一性)互不依赖 → 并行省 1 RTT。
|
|
933
|
+
// 只并行「查询层」:后续 readBrief 过滤依赖 byPhone 结果,保持串行。两者均为只读 POST,无副作用;
|
|
934
|
+
// 包装成永不 reject 的 task(失败仅记录),错误语义与串行版一致(byPhone 记录 error、checkUnique 忽略)。
|
|
935
|
+
const phoneTask = getCandidateByPhone(ctx.host, mobile).then((b) => ({ ok: true, b }), (e) => ({ ok: false, e }));
|
|
936
|
+
const uniqueTask = fields.telephone
|
|
937
|
+
? checkUnique(ctx.host, { objId, fieldId: fields.telephone, value: mobile }).then((r) => ({ ok: true, r }), () => ({ ok: false, r: null }))
|
|
938
|
+
: Promise.resolve({ ok: false, r: null });
|
|
939
|
+
const [phoneRes, uniqueRes] = await Promise.all([phoneTask, uniqueTask]);
|
|
940
|
+
let byPhone = null;
|
|
941
|
+
if (phoneRes.ok) {
|
|
942
|
+
const b = phoneRes.b;
|
|
943
|
+
if (b.candidateId || b.talentId) {
|
|
944
|
+
byPhone = {
|
|
945
|
+
talentId: b.talentId,
|
|
946
|
+
candidateId: b.candidateId,
|
|
947
|
+
talentViewUrl: b.candidateId ? buildTalentViewUrl(ctx.baseUrl, b.candidateId) : null,
|
|
948
|
+
talentCardUrl: b.talentId ? resolveTalentCardUrl(ctx, b.talentId) : null,
|
|
949
|
+
talentListLocateUrl: b.talentId ? resolveTalentListLocateUrl(ctx, b.talentId) : null,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
byPhone = { error: phoneRes.e?.message };
|
|
955
|
+
}
|
|
956
|
+
const formConflicts = { existing: [], unreadable: [] };
|
|
957
|
+
let ids = [];
|
|
958
|
+
if (uniqueRes.ok && uniqueRes.r) {
|
|
959
|
+
const r = uniqueRes.r;
|
|
960
|
+
ids = r.formDataIds;
|
|
961
|
+
const readBrief = makeBriefReader(ctx, formId, objId, fields);
|
|
962
|
+
for (const id of ids) {
|
|
963
|
+
if (byPhone?.talentId === id)
|
|
964
|
+
continue;
|
|
965
|
+
const rec = await readBrief(id);
|
|
966
|
+
if (rec) {
|
|
967
|
+
formConflicts.existing.push({
|
|
968
|
+
talentId: id, ...rec,
|
|
969
|
+
talentCardUrl: resolveTalentCardUrl(ctx, id),
|
|
970
|
+
talentListLocateUrl: resolveTalentListLocateUrl(ctx, id),
|
|
971
|
+
recordCardUrl: buildCardUrl(ctx.baseUrl, objId, id),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
else
|
|
975
|
+
formConflicts.unreadable.push(id);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const hit = Boolean(byPhone?.candidateId || byPhone?.talentId) || formConflicts.existing.length > 0;
|
|
979
|
+
return { checked: true, unique: !hit, byPhone: byPhone && !byPhone.error ? byPhone : null, byPhoneError: byPhone?.error, formConflicts, ids };
|
|
980
|
+
}
|
|
981
|
+
function fingerprint(value) {
|
|
982
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* 指纹用主表:剔除附件字段(original_resume)。
|
|
986
|
+
* 附件每次上传都会生成新的 fileid,属 volatile 字段;若参与指纹,prepare 与 apply 必然不一致
|
|
987
|
+
* (2026-11-04 实测:同一 PDF 两次上传 fileid 不同 → update 模式必报 fingerprint_mismatch)。
|
|
988
|
+
* 附件内容由 fileid 指向,用户确认时看的是文件名/份数,剔除不影响确认语义。
|
|
989
|
+
*/
|
|
990
|
+
function fingerprintMainTable(mainTable) {
|
|
991
|
+
const copy = { ...mainTable };
|
|
992
|
+
delete copy.original_resume;
|
|
993
|
+
return copy;
|
|
994
|
+
}
|
|
995
|
+
const baseInput = {
|
|
996
|
+
structured: { type: 'object', description: '大模型按 schema 整理的结构化简历对象(解析主通道;与 structuredFile 二选一)' },
|
|
997
|
+
structuredFile: { type: 'string', description: '结构化简历 JSON 文件路径(兼容脚本版用法)' },
|
|
998
|
+
fileId: { type: 'string', description: '已上传到 E10 的简历附件 id(原件写进附件字段)' },
|
|
999
|
+
resumeFile: { type: 'string', description: '本地简历文件路径(两步 multipart 上传)' },
|
|
1000
|
+
channel: { type: 'string', description: '简历渠道(须在数据字典中存在;缺省固定「手动录入」)' },
|
|
1001
|
+
tags: { type: 'array', items: { type: 'string' }, maxItems: 20, description: '平台标签名(按名命中环境清单,不存在跳过;学校归属自动推导另算)' },
|
|
1002
|
+
useOcr: { type: 'boolean', description: '显式开启 OCR 兜底解析(闸门=checkResumeExtraction;默认永不上调)' },
|
|
1003
|
+
noSchoolTags: { type: 'boolean', description: '关闭学校归属标签自动推导' },
|
|
1004
|
+
updateTalentId: { type: 'string', description: '按简历更新已有人才(主表全量覆盖+明细整表覆盖;先 prepare 看差异)' },
|
|
1005
|
+
};
|
|
1006
|
+
async function prepare(args, env) {
|
|
1007
|
+
const host = env.host;
|
|
1008
|
+
const session = await host.getSession();
|
|
1009
|
+
const ctx = { host, baseUrl: session.baseUrl.replace(/\/+$/, ''), appId: '', cache: await env.recruitContext() };
|
|
1010
|
+
ctx.appId = ctx.cache.appId;
|
|
1011
|
+
const argsT = args;
|
|
1012
|
+
if (!argsT.structured && !argsT.structuredFile && !argsT.fileId && !argsT.resumeFile) {
|
|
1013
|
+
throw rsValidationError('input_required', '需要 structured/structuredFile/fileId/resumeFile 之一');
|
|
1014
|
+
}
|
|
1015
|
+
const built = await buildImport(ctx, argsT);
|
|
1016
|
+
// 更新模式:出差异摘要
|
|
1017
|
+
if (argsT.updateTalentId) {
|
|
1018
|
+
const updateTalentId = String(argsT.updateTalentId);
|
|
1019
|
+
const snapshot = await readTalentSnapshot(ctx, { formId: built.formId, objId: built.objId, fields: built.fields, subMeta: built.subMeta, dataId: updateTalentId });
|
|
1020
|
+
const upMain = toDatasetMainTable(built.dataDetails, built.fields);
|
|
1021
|
+
const protectedSkipped = [];
|
|
1022
|
+
for (const k of PROTECTED_UPDATE_KEYS) {
|
|
1023
|
+
if (k in upMain) {
|
|
1024
|
+
delete upMain[k];
|
|
1025
|
+
protectedSkipped.push(k);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// 口径统一:快照是展示值,待写是 optionId —— 待写值反查成展示值再比,避免把"没变"误报成"覆盖"
|
|
1029
|
+
const dictName = makeDictLookup(ctx.cache);
|
|
1030
|
+
const fo = (ctx.cache.field_options || {})[built.objId] || {};
|
|
1031
|
+
const displayOf = (key, v) => {
|
|
1032
|
+
const s = String(v ?? '');
|
|
1033
|
+
if (key === 'original_resume')
|
|
1034
|
+
return s ? '(附件)' : '';
|
|
1035
|
+
if (!/^\d{6,}$/.test(s))
|
|
1036
|
+
return s;
|
|
1037
|
+
const dn = dictName(s);
|
|
1038
|
+
if (dn)
|
|
1039
|
+
return dn;
|
|
1040
|
+
const map = built.fields[key] ? fo[built.fields[key]] : null;
|
|
1041
|
+
if (map && map[s])
|
|
1042
|
+
return map[s];
|
|
1043
|
+
return s;
|
|
1044
|
+
};
|
|
1045
|
+
const fieldDiff = Object.entries(upMain).map(([k, v]) => {
|
|
1046
|
+
const old = snapshot.main[k] ?? '';
|
|
1047
|
+
const newDisplay = displayOf(k, v);
|
|
1048
|
+
const same = String(old) === newDisplay;
|
|
1049
|
+
return {
|
|
1050
|
+
field: k, old, new: newDisplay, rawValue: String(v),
|
|
1051
|
+
change: same ? 'same' : (k === 'original_resume' ? 'replace-attachment' : (old === '' ? 'fill' : 'overwrite')),
|
|
1052
|
+
};
|
|
1053
|
+
});
|
|
1054
|
+
const changed = fieldDiff.filter((x) => x.change !== 'same');
|
|
1055
|
+
const detailDiff = Object.entries(built.detailsPayload).map(([node, rows]) => {
|
|
1056
|
+
const dk = Object.keys(built.detailNodes).find((k) => built.detailNodes[k] === node) || node;
|
|
1057
|
+
return { table: dk, node, oldRows: snapshot.subRows[dk] || 0, newRows: rows.length, action: 'cover' };
|
|
1058
|
+
});
|
|
1059
|
+
const coveredKeys = new Set(Object.keys(built.detailNodes));
|
|
1060
|
+
const keptTables = Object.keys(built.subMeta).filter((k) => !coveredKeys.has(k) || !(built.detailNodes[k] in built.detailsPayload));
|
|
1061
|
+
let viewUrl = null;
|
|
1062
|
+
try {
|
|
1063
|
+
const b = await getCandidateByPhone(ctx.host, built.structured.telephone || '');
|
|
1064
|
+
if (b.candidateId)
|
|
1065
|
+
viewUrl = buildTalentViewUrl(ctx.baseUrl, b.candidateId);
|
|
1066
|
+
}
|
|
1067
|
+
catch { /* 查不到批次不影响更新 */ }
|
|
1068
|
+
const willSubmit = { updateTalentId, mainTable: fingerprintMainTable(upMain), details: built.detailsPayload };
|
|
1069
|
+
const fp = fingerprint(willSubmit);
|
|
1070
|
+
const continuation = await env.continuation.issue({ operation: 'recruit-stage.resume.import.apply', fingerprint: fp });
|
|
1071
|
+
return rsSuccess('recruit-stage.resume.import.prepare', {
|
|
1072
|
+
mode: 'prepare-update',
|
|
1073
|
+
action: '按简历更新已有人才',
|
|
1074
|
+
talentId: updateTalentId,
|
|
1075
|
+
strategy: { main: 'B 全量覆盖(简历解析出的字段全部写入)', details: '整表覆盖(仅覆盖简历解析出数据的表)' },
|
|
1076
|
+
changes: changed,
|
|
1077
|
+
unchanged: fieldDiff.filter((x) => x.change === 'same').map((x) => x.field),
|
|
1078
|
+
details: detailDiff,
|
|
1079
|
+
detailPreview: Object.fromEntries(Object.entries(built.detailsPayload).map(([n, rows]) => [n, rows[0]])),
|
|
1080
|
+
keptTables,
|
|
1081
|
+
protectedSkipped,
|
|
1082
|
+
talentViewUrl: viewUrl,
|
|
1083
|
+
note: '以上为将写入的差异。确认后 apply 执行;全量覆盖会改掉人工修正过的值',
|
|
1084
|
+
}, { continuation, fingerprint: fp, confirmHint: '用户明确确认后,携带 confirm:true 与 continuation 调用 apply' });
|
|
1085
|
+
}
|
|
1086
|
+
// 查重与标签预检互不依赖 → 并行(标签清单分页通常多 RTT,与查重力求重叠)。
|
|
1087
|
+
// tagCheckTask 内部自带 try/catch,永不 reject,避免查重先抛错时产生 unhandledRejection。
|
|
1088
|
+
const tagCheckTask = (async () => {
|
|
1089
|
+
if (!built.tagNames.length)
|
|
1090
|
+
return null;
|
|
1091
|
+
try {
|
|
1092
|
+
const rt = await resolveTags(ctx.host, built.tagNames);
|
|
1093
|
+
return { requested: built.tagNames, found: rt.found, missing: rt.missing, ...(typeof rt.availableCount === 'number' ? { availableCount: rt.availableCount } : {}) };
|
|
1094
|
+
}
|
|
1095
|
+
catch (e) {
|
|
1096
|
+
return { requested: built.tagNames, error: e.message };
|
|
1097
|
+
}
|
|
1098
|
+
})();
|
|
1099
|
+
const [dupInfo, tagCheck] = await Promise.all([
|
|
1100
|
+
findExistingTalents(ctx, { objId: built.objId, formId: built.formId, fields: built.fields, mobile: built.structured.telephone }),
|
|
1101
|
+
tagCheckTask,
|
|
1102
|
+
]);
|
|
1103
|
+
const summary = {
|
|
1104
|
+
action: '简历导入建人才',
|
|
1105
|
+
resumeFile: built.fileName || ('fileid=' + built.fileid),
|
|
1106
|
+
// 供 apply 复用:传 fileId 即可跳过二次上传(附件已在本环境,省一次 multipart)
|
|
1107
|
+
resumeFileId: built.fileid || undefined,
|
|
1108
|
+
parsed: {
|
|
1109
|
+
name: built.structured.candidateName, mobile: built.structured.telephone, email: built.structured.email || '(空)',
|
|
1110
|
+
gender: built.structured.genderName, birthday: built.structured.birthday, age: built.structured.age,
|
|
1111
|
+
degree: built.structured.highest_degree_name, college: built.structured.college,
|
|
1112
|
+
joinworkdate: built.structured.joinworkdate, intendPosition: built.structured.intend_position,
|
|
1113
|
+
jobCount: (built.structured.jobInformations || []).length, projectCount: (built.structured.projectExperiences || []).length,
|
|
1114
|
+
},
|
|
1115
|
+
mappedFieldCount: built.dataDetails.length,
|
|
1116
|
+
channel: built.channelLabel || '(未指定)',
|
|
1117
|
+
subTables: built.subBuild.stats,
|
|
1118
|
+
subTableRows: built.subRowTotal,
|
|
1119
|
+
subTableNodes: built.detailNodes,
|
|
1120
|
+
subTableUnmapped: built.unmappedTables.length ? built.unmappedTables : undefined,
|
|
1121
|
+
subTableError: built.subError || undefined,
|
|
1122
|
+
structuredWarnings: built.structuredTextWarnings.length ? built.structuredTextWarnings : undefined,
|
|
1123
|
+
duplicate: {
|
|
1124
|
+
checked: dupInfo.checked,
|
|
1125
|
+
unique: dupInfo.unique,
|
|
1126
|
+
bound: dupInfo.byPhone,
|
|
1127
|
+
formConflicts: dupInfo.formConflicts.existing,
|
|
1128
|
+
unreadableIds: dupInfo.formConflicts.unreadable,
|
|
1129
|
+
byPhoneError: dupInfo.byPhoneError,
|
|
1130
|
+
note: dupInfo.byPhone
|
|
1131
|
+
? '手机号已绑定人才(页面同款 getCandidateByPhone 命中)→ 不新建,给出人才查看页地址'
|
|
1132
|
+
: (dupInfo.formConflicts.existing.length
|
|
1133
|
+
? '手机号在人才库已存在(表单唯一性命中,但无应聘批次)→ 不新建'
|
|
1134
|
+
: (dupInfo.formConflicts.unreadable.length
|
|
1135
|
+
? '手机号被已删除记录占用(平台仍会拦新建):需先在回收站清理,或改为更新已有人才'
|
|
1136
|
+
: '无重复')),
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
const willSubmit = { formId: built.formId, objId: built.objId, mainTable: fingerprintMainTable(toDatasetMainTable(built.dataDetails, built.fields)), details: built.detailsPayload };
|
|
1140
|
+
const fp = fingerprint(willSubmit);
|
|
1141
|
+
const continuation = await env.continuation.issue({ operation: 'recruit-stage.resume.import.apply', fingerprint: fp });
|
|
1142
|
+
return rsSuccess('recruit-stage.resume.import.prepare', {
|
|
1143
|
+
mode: 'prepare', ...summary,
|
|
1144
|
+
willSubmit,
|
|
1145
|
+
tagCheck,
|
|
1146
|
+
schoolTags: built.schoolTagInfo,
|
|
1147
|
+
onCommit: '提交后将:① dataset/v2/saveFormData 建人才(主表+明细一次写入)② 初始化人才批次 ③ 触发人才自动标签规则匹配 ④ 按标签(含学校归属自动推导)写入平台标签 ⑤ 输出人才卡片/人才库定位链接',
|
|
1148
|
+
}, { continuation, fingerprint: fp, confirmHint: '用户明确确认后,携带 confirm:true 与 continuation 调用 apply' });
|
|
1149
|
+
}
|
|
1150
|
+
async function apply(args, env) {
|
|
1151
|
+
if (args.confirm !== true)
|
|
1152
|
+
throw rsConfirmationError('recruit-stage.resume.import.apply', { hint: '写操作必须显式 confirm:true' });
|
|
1153
|
+
if (!args.continuation)
|
|
1154
|
+
throw rsValidationError('continuation_required', '缺少 continuation(来自 prepare 的 meta)');
|
|
1155
|
+
const payload = await env.continuation.verify(String(args.continuation), 'recruit-stage.resume.import.apply');
|
|
1156
|
+
const host = env.host;
|
|
1157
|
+
const session = await host.getSession();
|
|
1158
|
+
const ctx = { host, baseUrl: session.baseUrl.replace(/\/+$/, ''), appId: '', cache: await env.recruitContext() };
|
|
1159
|
+
ctx.appId = ctx.cache.appId;
|
|
1160
|
+
const argsT = args;
|
|
1161
|
+
const built = await buildImport(ctx, argsT);
|
|
1162
|
+
// 更新模式
|
|
1163
|
+
if (argsT.updateTalentId) {
|
|
1164
|
+
const updateTalentId = String(argsT.updateTalentId);
|
|
1165
|
+
const upMain = toDatasetMainTable(built.dataDetails, built.fields);
|
|
1166
|
+
for (const k of PROTECTED_UPDATE_KEYS)
|
|
1167
|
+
delete upMain[k];
|
|
1168
|
+
const willSubmit = { updateTalentId, mainTable: fingerprintMainTable(upMain), details: built.detailsPayload };
|
|
1169
|
+
if (payload.fingerprint !== fingerprint(willSubmit)) {
|
|
1170
|
+
throw rsValidationError('fingerprint_mismatch', '待提交内容与 prepare 确认摘要不一致,请重新 prepare 确认后再 apply');
|
|
1171
|
+
}
|
|
1172
|
+
await datasetUpdateFormData(host, {
|
|
1173
|
+
objId: built.objId, dataId: updateTalentId, mainTable: upMain,
|
|
1174
|
+
details: built.detailsPayload, coverDetails: Object.keys(built.detailsPayload),
|
|
1175
|
+
});
|
|
1176
|
+
// 自检:行数与格数都要对得上;落库有短暂延迟,首读不符等 600ms 重读一次再判定
|
|
1177
|
+
const checkAfter = (snap) => {
|
|
1178
|
+
const out = [];
|
|
1179
|
+
for (const [node, rows] of Object.entries(built.detailsPayload)) {
|
|
1180
|
+
const dk = Object.keys(built.detailNodes).find((k) => built.detailNodes[k] === node) || node;
|
|
1181
|
+
const wantCells = rows.reduce((n, r) => n + Object.keys(r).length, 0);
|
|
1182
|
+
const gotRows = snap.subRows[dk] || 0;
|
|
1183
|
+
const gotCells = snap.subCells[dk] || 0;
|
|
1184
|
+
if (gotRows !== rows.length || gotCells !== wantCells) {
|
|
1185
|
+
out.push(`${dk}:期望 ${rows.length} 行/${wantCells} 格,读回 ${gotRows} 行/${gotCells} 格`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return out;
|
|
1189
|
+
};
|
|
1190
|
+
let after = await readTalentSnapshot(ctx, { formId: built.formId, objId: built.objId, fields: built.fields, subMeta: built.subMeta, dataId: updateTalentId });
|
|
1191
|
+
let verifyWarnings = checkAfter(after);
|
|
1192
|
+
if (verifyWarnings.length) {
|
|
1193
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
1194
|
+
after = await readTalentSnapshot(ctx, { formId: built.formId, objId: built.objId, fields: built.fields, subMeta: built.subMeta, dataId: updateTalentId });
|
|
1195
|
+
verifyWarnings = checkAfter(after);
|
|
1196
|
+
}
|
|
1197
|
+
return rsSuccess('recruit-stage.resume.import.apply', {
|
|
1198
|
+
// 更新模式同理:明细行/格数对不上时只能报 partial,不能因主表写入成功就报 success
|
|
1199
|
+
mode: 'update', result: verifyWarnings.length ? 'partial' : 'success',
|
|
1200
|
+
detailsState: verifyWarnings.length ? 'missing' : 'complete',
|
|
1201
|
+
talentId: updateTalentId,
|
|
1202
|
+
afterSummary: { mainFieldCount: Object.keys(after.main).length, subRows: after.subRows, subCells: after.subCells },
|
|
1203
|
+
verifyWarnings: verifyWarnings.length ? verifyWarnings : undefined,
|
|
1204
|
+
talentCardUrl: resolveTalentCardUrl(ctx, updateTalentId),
|
|
1205
|
+
talentListLocateUrl: resolveTalentListLocateUrl(ctx, updateTalentId),
|
|
1206
|
+
parsed: { name: built.structured.candidateName, mobile: built.structured.telephone, email: built.structured.email || '(空)' },
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
// 新建模式:查重阻断
|
|
1210
|
+
const dupInfo = await findExistingTalents(ctx, { objId: built.objId, formId: built.formId, fields: built.fields, mobile: built.structured.telephone });
|
|
1211
|
+
if (dupInfo.byPhone?.candidateId || dupInfo.byPhone?.talentId) {
|
|
1212
|
+
const b = dupInfo.byPhone;
|
|
1213
|
+
return rsSuccess('recruit-stage.resume.import.apply', {
|
|
1214
|
+
mode: 'apply', result: 'duplicate',
|
|
1215
|
+
message: '当前手机号已绑定人才信息,按页面规则不新建(未写入任何数据)',
|
|
1216
|
+
existingTalent: { talentId: b.talentId, candidateId: b.candidateId, talentViewUrl: b.talentViewUrl, talentCardUrl: b.talentCardUrl },
|
|
1217
|
+
talentViewUrl: b.talentViewUrl,
|
|
1218
|
+
otherRecords: dupInfo.formConflicts.existing.length ? dupInfo.formConflicts.existing : undefined,
|
|
1219
|
+
parsed: { name: built.structured.candidateName, mobile: built.structured.telephone },
|
|
1220
|
+
nextStep: '如需按这份简历更新该人才,明确说明即可(updateTalentId 走更新通道)',
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
if (dupInfo.formConflicts.existing.length) {
|
|
1224
|
+
return rsSuccess('recruit-stage.resume.import.apply', {
|
|
1225
|
+
mode: 'apply', result: 'duplicate',
|
|
1226
|
+
message: '手机号在人才库已存在(表单唯一性命中,但无应聘批次),不新建(未写入任何数据)',
|
|
1227
|
+
formConflicts: dupInfo.formConflicts.existing,
|
|
1228
|
+
parsed: { name: built.structured.candidateName, mobile: built.structured.telephone },
|
|
1229
|
+
nextStep: '如需按这份简历更新该人才,明确说明即可(updateTalentId 走更新通道)',
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
if (!dupInfo.unique && dupInfo.formConflicts.unreadable.length) {
|
|
1233
|
+
throw rsFail('手机号被 ' + dupInfo.formConflicts.unreadable.length + ' 条已删除记录占用(平台唯一性校验仍拦新建,id=' + dupInfo.formConflicts.unreadable.join(',') + '):请先在回收站清理,或改为更新已有人才');
|
|
1234
|
+
}
|
|
1235
|
+
// 建人才:① 首选数据集通道(主表+明细一次写入);② 被拦时回退页面级主表 + 数据集 update 补明细
|
|
1236
|
+
const datasetMain = toDatasetMainTable(built.dataDetails, built.fields);
|
|
1237
|
+
let newId = '';
|
|
1238
|
+
let writeChannel = '';
|
|
1239
|
+
const writeWarnings = [];
|
|
1240
|
+
try {
|
|
1241
|
+
const created = await datasetSaveFormData(host, { objId: built.objId, mainTable: datasetMain, details: built.detailsPayload });
|
|
1242
|
+
newId = created.dataId;
|
|
1243
|
+
writeChannel = 'dataset/v2/saveFormData(主表 + 明细一次写入)';
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
writeWarnings.push('数据集新增未通过(' + e.message + '),改为建主表后用数据集 update 补明细');
|
|
1247
|
+
newId = await saveFormData(host, { appId: ctx.appId, formId: built.formId, objId: built.objId, name: built.structured.candidateName || '简历导入', dataDetails: built.dataDetails });
|
|
1248
|
+
writeChannel = 'form/core/saveFormData(主表)+ dataset/v2/updateFormData(明细)';
|
|
1249
|
+
if (Object.keys(built.detailsPayload).length) {
|
|
1250
|
+
try {
|
|
1251
|
+
// 新建记录的明细必然为空 → 走「追加」即可。不用 cover:实测 cover 会重建整行,把 EBuilder(关联)列清空。
|
|
1252
|
+
await datasetUpdateFormData(host, { objId: built.objId, dataId: newId, mainTable: {}, details: built.detailsPayload });
|
|
1253
|
+
}
|
|
1254
|
+
catch (e2) {
|
|
1255
|
+
writeWarnings.push('回退后补明细失败:' + e2.message);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const r2 = await host.request({ path: '/api/recruit/candidate/initTalentBatch?formDataId=' + newId, method: 'POST' });
|
|
1260
|
+
const talentId = String(newId);
|
|
1261
|
+
// 写后 4 件事并行(都只依赖 talentId、彼此无依赖):
|
|
1262
|
+
// ① verifySubRows 回读明细校验(读)
|
|
1263
|
+
// ② runSaveButtonEsbActions 执行「保存」按钮 esb 动作流(写副作用:建日程/通知等)
|
|
1264
|
+
// ③ matchTalentLabel 触发后台智能标签规则(写:规则匹配,与平台标签属不同体系)
|
|
1265
|
+
// ④ addEntityTags 写平台标签(内部「按名解析 → 写入」两步)
|
|
1266
|
+
// initTalentBatch 保持在前串行:它是批次初始化,失败应阻断后续(与原语义一致)。
|
|
1267
|
+
// 每个 task 用 .then 包装错误,Promise.all 消费全部 → 各自失败互不影响(与原 try/catch 等价)。
|
|
1268
|
+
const [verifyRes, esbRes, labelRes, tagRes] = await Promise.all([
|
|
1269
|
+
verifySubRows(ctx, built.formId, built.objId, newId).then((v) => ({ ok: true, v }), (e) => ({ ok: false, e })),
|
|
1270
|
+
runSaveButtonEsbActions(host, { objId: built.objId, formId: built.formId, dataId: newId }).then((v) => ({ ok: true, v }), (e) => ({ ok: false, e })),
|
|
1271
|
+
matchTalentLabel(host, talentId).then((v) => ({ ok: true, v }), (e) => ({ ok: false, e })),
|
|
1272
|
+
addEntityTags(host, talentId, built.tagNames).then((v) => ({ ok: true, v }), (e) => ({ ok: false, e })),
|
|
1273
|
+
]);
|
|
1274
|
+
const subTableVerify = verifyRes.ok ? verifyRes.v : { error: verifyRes.e?.message };
|
|
1275
|
+
// 明细完整性判定:主表建成 ≠ 建档成功。实测(2026-11-05)出现过主表 19 字段写入成功、
|
|
1276
|
+
// 18 行明细 0 行落库,而返回仍是 result=success —— 只看 writeWarnings 才会发现问题。
|
|
1277
|
+
// 规则:期望行数 > 0 时读回行数必须 >= 期望,否则 result 只能报 partial;回读失败则标 unverified。
|
|
1278
|
+
let detailsState = 'complete';
|
|
1279
|
+
if (!verifyRes.ok) {
|
|
1280
|
+
detailsState = 'unverified';
|
|
1281
|
+
writeWarnings.push('明细读回校验失败(无法确认明细是否落库):' + (verifyRes.e?.message || ''));
|
|
1282
|
+
}
|
|
1283
|
+
else if (built.subRowTotal > 0) {
|
|
1284
|
+
const gotRows = Number(subTableVerify?.rowTotal || 0);
|
|
1285
|
+
if (gotRows < built.subRowTotal) {
|
|
1286
|
+
detailsState = 'missing';
|
|
1287
|
+
writeWarnings.push('明细行未完整落库(期望 ' + built.subRowTotal + ' 行,读回 ' + gotRows + ' 行):主表已写入,请按 writeWarnings 修正后走 updateTalentId 补明细');
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const saveEsbActions = esbRes.ok ? esbRes.v : { error: esbRes.e?.message };
|
|
1291
|
+
const labelMatch = labelRes.ok ? labelRes.v : 'ERR:' + (labelRes.e?.message || '');
|
|
1292
|
+
const tagResult = tagRes.ok
|
|
1293
|
+
? tagRes.v
|
|
1294
|
+
: { applied: [], skipped: built.tagNames, error: tagRes.e?.message };
|
|
1295
|
+
// 标签回读依赖写入结果,放在并行组之后(写操作可验证优先)
|
|
1296
|
+
const tags = await queryEntityTags(host, talentId).catch(() => []);
|
|
1297
|
+
const talentCardUrl = resolveTalentCardUrl(ctx, talentId);
|
|
1298
|
+
const talentListLocateUrl = resolveTalentListLocateUrl(ctx, talentId);
|
|
1299
|
+
return rsSuccess('recruit-stage.resume.import.apply', {
|
|
1300
|
+
mode: 'apply', result: detailsState === 'complete' ? 'success' : 'partial',
|
|
1301
|
+
detailsState,
|
|
1302
|
+
newCandidateFormDataId: newId,
|
|
1303
|
+
talentId,
|
|
1304
|
+
writeChannel,
|
|
1305
|
+
writeWarnings: writeWarnings.length ? writeWarnings : undefined,
|
|
1306
|
+
subTablesExpected: built.subBuild.stats,
|
|
1307
|
+
subTableVerify,
|
|
1308
|
+
initTalentBatch: r2?.data?.msg || '',
|
|
1309
|
+
saveEsbActions,
|
|
1310
|
+
schoolTags: built.schoolTagInfo,
|
|
1311
|
+
labelMatch,
|
|
1312
|
+
tagsApplied: tagResult.applied,
|
|
1313
|
+
tagsSkipped: tagResult.skipped,
|
|
1314
|
+
tags,
|
|
1315
|
+
talentCardUrl,
|
|
1316
|
+
talentListLocateUrl,
|
|
1317
|
+
// 产物链接(Agent 原样搬运:① 人才卡片 ② 人才库定位;缺失哪个只给另一个)
|
|
1318
|
+
viewLink: talentCardUrl ? `[查看 ${built.structured.candidateName || '新人才'} 的人才卡片](${talentCardUrl})` : null,
|
|
1319
|
+
listLocateLink: talentListLocateUrl ? `[在人才库中打开](${talentListLocateUrl})` : null,
|
|
1320
|
+
agentNote: '正文给两个 Markdown 链接——① [查看 <姓名> 的人才卡片](talentCardUrl) ② [在人才库中打开](talentListLocateUrl);缺失哪个就只给另一个,不输出半截。不要用 artifact / browsertool。',
|
|
1321
|
+
parsed: {
|
|
1322
|
+
name: built.structured.candidateName, mobile: built.structured.telephone, email: built.structured.email || '(空)',
|
|
1323
|
+
gender: built.structured.genderName, degree: built.structured.highest_degree_name, college: built.structured.college,
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
export const resumeImportOperations = [
|
|
1328
|
+
{
|
|
1329
|
+
name: 'recruit-stage.resume.import.prepare',
|
|
1330
|
+
resource: 'recruit.resume',
|
|
1331
|
+
method: 'prepare',
|
|
1332
|
+
action: 'resume.import.prepare',
|
|
1333
|
+
summary: '简历导入确认摘要:解析结果+字段映射+两级查重(页面同款)+标签预核(含学校归属自动推导);更新模式出字段/明细差异;不写入',
|
|
1334
|
+
capability: 'recruit.resume.import',
|
|
1335
|
+
risk: 'write',
|
|
1336
|
+
requiresConfirmation: true,
|
|
1337
|
+
inputSchema: schemaOf(baseInput, {}),
|
|
1338
|
+
handler: prepare,
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
name: 'recruit-stage.resume.import.apply',
|
|
1342
|
+
resource: 'recruit.resume',
|
|
1343
|
+
method: 'apply',
|
|
1344
|
+
action: 'resume.import.apply',
|
|
1345
|
+
summary: '用户确认后建档:数据集通道主表+明细一次写入(被拦回退)、初始化批次、触发规则标签、写平台标签、读回校验;返回人才卡片/人才库双链接',
|
|
1346
|
+
capability: 'recruit.resume.import',
|
|
1347
|
+
risk: 'write',
|
|
1348
|
+
requiresConfirmation: true,
|
|
1349
|
+
inputSchema: schemaOf({ ...baseInput, confirm: { type: 'boolean' }, continuation: { type: 'string' } }, {}),
|
|
1350
|
+
handler: apply,
|
|
1351
|
+
},
|
|
1352
|
+
];
|
|
1353
|
+
void findMenuNode;
|
|
1354
|
+
void randHex32;
|