weaver-work-cli 0.1.32 → 0.1.33
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 +2 -2
- package/dist/chunks/{archive-UMGX72K6.js → archive-FVFY6GZN.js} +2 -2
- package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-NW3OD4UV.js} +2 -2
- package/dist/chunks/{asset-GLEGKFG5.js → asset-7QJKMIDV.js} +2 -2
- package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-OSAVA6K3.js} +3 -2
- package/dist/chunks/{blog-E44OTVDA.js → blog-I7SDPNW4.js} +2 -2
- package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-O37ZIQNY.js} +2 -2
- package/dist/chunks/{calendar-JKPIRKFD.js → calendar-7KXE4ED3.js} +2 -2
- package/dist/chunks/{chunk-4YC43QQ4.js → chunk-2FLWXFVS.js} +4 -4
- package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
- package/dist/chunks/chunk-3SO6QL6U.js +73 -0
- package/dist/chunks/{chunk-DW6AUUKS.js → chunk-6ATGWRZM.js} +6 -4
- package/dist/chunks/chunk-BYATLLEF.js +68 -0
- package/dist/chunks/chunk-BYBSCN4X.js +932 -0
- package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-D223PTXU.js} +1 -0
- package/dist/chunks/{chunk-H7J4DC5R.js → chunk-EEWPQE3O.js} +2 -2
- package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-GFPXQIY2.js} +2 -2
- package/dist/chunks/{chunk-C6R3B2ML.js → chunk-HEL5XTYI.js} +12 -6
- package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
- package/dist/chunks/{chunk-YRVDW4WN.js → chunk-ILMBNVSG.js} +2 -2
- package/dist/chunks/{chunk-G3QJULO2.js → chunk-KAS45KNR.js} +1 -1
- package/dist/chunks/{chunk-H6XSTSLM.js → chunk-MEEFGWTM.js} +54 -196
- package/dist/chunks/{chunk-GCG2NMWR.js → chunk-MQ2I5AA3.js} +4 -2
- package/dist/chunks/chunk-NHA62QD7.js +774 -0
- package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-OCU4RLHC.js} +641 -309
- package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-PRHJGPMA.js} +2 -2
- package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-Q6LWB5HX.js} +2 -2
- package/dist/chunks/{chunk-IURUFGZK.js → chunk-RBGOMK4Y.js} +50 -33
- package/dist/chunks/chunk-VBCFFFGT.js +226 -0
- package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-WRRJ4GG2.js} +22 -1148
- package/dist/chunks/{chunk-E4Z23GHM.js → chunk-WY74UBJ6.js} +264 -750
- package/dist/chunks/conflict-SFNEVJY5.js +21 -0
- package/dist/chunks/continuation-RN6PZN2M.js +11 -0
- package/dist/chunks/cowork-CKWYHXYB.js +2508 -0
- package/dist/chunks/ebuilder-form-KOLJRVZD.js +121 -0
- package/dist/chunks/{ehr-HMFY6VIL.js → ehr-WFOC3B4D.js} +1174 -20
- package/dist/chunks/{esb-ZYSVB7SG.js → esb-LJZTN663.js} +4 -4
- package/dist/chunks/{fna-B7RKPAZD.js → fna-F7QRIIZC.js} +2 -2
- package/dist/chunks/{host-45WED2KK.js → host-BCGUVEWX.js} +3 -3
- package/dist/chunks/host-BQBRHVR3.js +23 -0
- package/dist/chunks/{host-I4Z6BFYX.js → host-DGE2TZD4.js} +3 -3
- package/dist/chunks/{host-KNZ52LJZ.js → host-KXPTEWFS.js} +3 -3
- package/dist/chunks/{host-OCH2BCCV.js → host-VQ6JJXGQ.js} +3 -3
- package/dist/chunks/{hrm-2KAZGUHI.js → hrm-NI6XAKW5.js} +3 -3
- package/dist/chunks/{im-ACJJWH4U.js → im-7TRTF3KT.js} +88 -12
- package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-AOXWJKYT.js} +3 -3
- package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-7NQWEZAW.js} +2 -2
- package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-RNS4EE4S.js} +4 -4
- package/dist/chunks/{mail-S2HDDXO2.js → mail-VDHJAB4O.js} +3 -3
- package/dist/chunks/{meeting-QVN3I37L.js → meeting-GA7XCNNP.js} +214 -43
- package/dist/chunks/{okr-QNG4VC2Q.js → okr-TQIRERPQ.js} +2 -2
- package/dist/chunks/operations-5QL3AECO.js +11 -0
- package/dist/chunks/{plan-A25EMW6U.js → plan-4RY4LN2P.js} +2 -2
- package/dist/chunks/{project-G6XV66DZ.js → project-D6XT2OC5.js} +2 -2
- package/dist/chunks/{registry-DC6KR3NH.js → registry-JHRIDQ3A.js} +4 -4
- package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-TI3CIUXF.js → registry-QKMI43Q6.js} +8 -6
- package/dist/chunks/{registry-7C5UMIPL.js → registry-VOGKFFTR.js} +2 -2
- package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-D4EUOOWY.js} +4 -3
- package/dist/chunks/{schedule-KFIITGSK.js → schedule-XPFBHPCV.js} +4 -3
- package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-HLBZRWIY.js} +172 -6
- package/dist/index.js +30 -25
- package/dist/internal/e10/auth-recovery.js +14 -4
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/cowork/continuation.js +70 -0
- package/dist/shortcuts/cowork/errors.js +59 -0
- package/dist/shortcuts/cowork/host.js +172 -0
- package/dist/shortcuts/cowork/index.js +166 -0
- package/dist/shortcuts/cowork/manifest.js +65 -0
- package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
- package/dist/shortcuts/cowork/operations/item-write.js +380 -0
- package/dist/shortcuts/cowork/operations/member-write.js +283 -0
- package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
- package/dist/shortcuts/cowork/operations/read.js +423 -0
- package/dist/shortcuts/cowork/operations/registry.js +39 -0
- package/dist/shortcuts/cowork/operations/review-write.js +157 -0
- package/dist/shortcuts/cowork/operations/shared.js +215 -0
- package/dist/shortcuts/cowork/operations/types.js +24 -0
- package/dist/shortcuts/cowork/operations.js +19 -0
- package/dist/shortcuts/ehr/host.js +4 -1
- package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
- package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
- package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
- package/dist/shortcuts/ehr/operations/recruit-write.js +8 -16
- package/dist/shortcuts/ehr/operations/recruit.js +35 -10
- package/dist/shortcuts/ehr/operations/registry.js +6 -0
- package/dist/shortcuts/im/diagnostics.js +97 -0
- package/dist/shortcuts/im/host.js +58 -31
- package/dist/shortcuts/im/index.js +7 -2
- package/dist/shortcuts/im/operations/at-mention.js +127 -0
- package/dist/shortcuts/im/operations/file-user.js +10 -2
- package/dist/shortcuts/im/operations/group-read.js +26 -5
- package/dist/shortcuts/im/operations/group-write.js +35 -28
- package/dist/shortcuts/im/operations/msg-sync.js +7 -2
- package/dist/shortcuts/im/operations/msg-write.js +76 -10
- package/dist/shortcuts/im/operations/quote-mention.js +119 -0
- package/dist/shortcuts/im/operations/session.js +1 -1
- package/dist/shortcuts/im/operations/shared.js +10 -0
- package/dist/shortcuts/im/operations/types.js +13 -0
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/mail/host.js +4 -1
- package/dist/shortcuts/meeting/host.js +27 -34
- package/dist/shortcuts/meeting/operations/create.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -7
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +168 -26
- package/dist/shortcuts/meeting/operations/eb/factory.js +69 -1
- package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +14 -3
- package/dist/shortcuts/meeting/operations/eb-fallback.js +159 -0
- package/dist/shortcuts/meeting/operations/env.js +93 -46
- package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
- package/dist/shortcuts/meeting/operations.js +16 -0
- package/dist/shortcuts/workflow/index.js +110 -1
- package/dist/shortcuts/workflow/operations/form.js +24 -3
- package/dist/shortcuts/workflow/operations/read.js +104 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
- package/docs/e10-auth.md +1 -1
- package/docs/im.md +8 -1
- package/docs/todo-smart-convert/README.md +1 -1
- package/docs/todo-smart-convert/data-contract.md +2 -2
- package/docs/todo-smart-convert/subgroup-rules.md +6 -0
- package/package.json +1 -1
- package/skill-template/domains/yimiaoban.md +3 -3
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +5 -5
- package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
- package/skills/weaver-e10-calendar/SKILL.md +5 -5
- package/skills/weaver-e10-esb/SKILL.md +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +5 -5
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
- package/skills/weaver-e10-jucailin/SKILL.md +18 -12
- package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
- package/skills/weaver-e10-mail/SKILL.md +5 -5
- package/skills/weaver-e10-meeting/SKILL.md +171 -25
- package/skills/weaver-e10-meeting/references/meeting-eb.md +4 -3
- package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
- package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/SKILL.md +5 -5
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +5 -5
- package/skills/weaver-e10-shared/SKILL.md +12 -12
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
- package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
- package/skills/weaver-e10-workflow/SKILL.md +29 -5
- package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
- package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
- package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
- package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
- package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
- package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
- package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
- package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ehrConfirmationError, ehrValidationError } from '../errors.js';
|
|
3
|
+
import { WRITE, SCHEMA_ID, ehrSuccess, schemaOf, } from './types.js';
|
|
4
|
+
import { buildListBody, fetchListRows, } from './recruit.js';
|
|
5
|
+
import { getDsOptions, getFormDataRecord, resolvePersonId, saveFormData, toSubmitDetails, } from './recruit-write.js';
|
|
6
|
+
/**
|
|
7
|
+
* 职位 JD 建职位(口述 → JD 文本 → 建到招聘职位列表)。
|
|
8
|
+
*
|
|
9
|
+
* 口径来源:ehr 技能 recruit-generate-jd.mjs(真机验证)。两步提交与页面「保存」等价:
|
|
10
|
+
* ① saveFormData(职位管理, dataDetails) → formDataId
|
|
11
|
+
* ② POST /api/recruit/position/v2/addPosition?formDataId= → positionPostId
|
|
12
|
+
*
|
|
13
|
+
* 字段 id 全部运行时从环境缓存 fields[objId] 取(dataKey → fieldId),不写死;
|
|
14
|
+
* 单行文本用 content、多行文本用 dataText、关联/枚举用 dataOptions。
|
|
15
|
+
*
|
|
16
|
+
* 默认值策略(使必填校验通过,且跨环境稳定):
|
|
17
|
+
* - 招聘状态 status = 「招聘中」(ds/options 按名称解析)
|
|
18
|
+
* - 招聘人数 recruit_count=1、薪酬月份数 number_of_pay_months=12
|
|
19
|
+
* - 薪酬类型 salary_type=月薪、招聘方式 recruit_type=社会招聘(ds/options)
|
|
20
|
+
* - 招聘负责人 / 用人经理 = 当前登录用户
|
|
21
|
+
* - 职位属性/序列/学历/工作年限/工作地点/薪酬上下限/招聘流程 = **取环境内已有职位记录的值**
|
|
22
|
+
* ("默认值 = 系统现成值",保证引用有效;取不到则跳过并在摘要中标注)
|
|
23
|
+
*/
|
|
24
|
+
const TEXT_KEYS = ['position_name'];
|
|
25
|
+
const MTEXT_KEYS = ['position_duty', 'work_request'];
|
|
26
|
+
const ASSOC_KEYS = ['demand'];
|
|
27
|
+
const PERSON_DEFAULT_KEYS = ['recr_principal', 'employ_manager'];
|
|
28
|
+
const REF_COPY_KEYS = [
|
|
29
|
+
'position_nature',
|
|
30
|
+
'job_category',
|
|
31
|
+
'education',
|
|
32
|
+
'work_years',
|
|
33
|
+
'work_location',
|
|
34
|
+
'salary_min',
|
|
35
|
+
'salary_max',
|
|
36
|
+
'recruit_flow',
|
|
37
|
+
];
|
|
38
|
+
const TEXT_DEFAULTS = { recruit_count: '1', number_of_pay_months: '12' };
|
|
39
|
+
const OPTION_DEFAULTS = { salary_type: '月薪', recruit_type: '社会招聘', status: '招聘中' };
|
|
40
|
+
/** 显式入参 → 职位字段 dataKey 与取值类别(逐字段声明,避免用通用 payload 包裹)。 */
|
|
41
|
+
const SET_FIELDS = [
|
|
42
|
+
{ arg: 'education', key: 'education', kind: 'dict', label: '学历要求(按数据字典名称)' },
|
|
43
|
+
{ arg: 'workYears', key: 'work_years', kind: 'dict', label: '工作年限(按数据字典名称)' },
|
|
44
|
+
{ arg: 'jobCategory', key: 'job_category', kind: 'dict', label: '职位类别(按数据字典名称)' },
|
|
45
|
+
{ arg: 'positionNature', key: 'position_nature', kind: 'dict', label: '职位属性(按数据字典名称)' },
|
|
46
|
+
{ arg: 'salaryType', key: 'salary_type', kind: 'option', label: '薪酬类型(月薪/年薪等)' },
|
|
47
|
+
{ arg: 'recruitType', key: 'recruit_type', kind: 'option', label: '招聘方式(社会招聘/校园招聘等)' },
|
|
48
|
+
{ arg: 'recruitCount', key: 'recruit_count', kind: 'number', label: '招聘人数' },
|
|
49
|
+
{ arg: 'salaryMin', key: 'salary_min', kind: 'number', label: '薪酬下限' },
|
|
50
|
+
{ arg: 'salaryMax', key: 'salary_max', kind: 'number', label: '薪酬上限' },
|
|
51
|
+
{ arg: 'numberOfPayMonths', key: 'number_of_pay_months', kind: 'number', label: '薪酬月数' },
|
|
52
|
+
{ arg: 'recruitPrincipal', key: 'recr_principal', kind: 'person', label: '招聘负责人(按姓名)' },
|
|
53
|
+
{ arg: 'employManager', key: 'employ_manager', kind: 'person', label: '用人经理(按姓名)' },
|
|
54
|
+
{ arg: 'interviewer', key: 'interviewer', kind: 'person', label: '面试官(按姓名)' },
|
|
55
|
+
{ arg: 'recruitHelper', key: 'recr_helper', kind: 'person', label: '招聘协助人(按姓名)' },
|
|
56
|
+
];
|
|
57
|
+
const SET_SCHEMA = Object.fromEntries(SET_FIELDS.map((f) => [f.arg, f.kind === 'number'
|
|
58
|
+
? { type: 'string', description: `${f.label}(数字,按字符串传)` }
|
|
59
|
+
: { type: 'string', description: f.label }]));
|
|
60
|
+
function fingerprint(value) {
|
|
61
|
+
return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex').slice(0, 32);
|
|
62
|
+
}
|
|
63
|
+
function requireConfirm(args, action) {
|
|
64
|
+
if (args.confirm !== true) {
|
|
65
|
+
throw ehrConfirmationError(action, { hint: '重新调用时传入 confirm=true 与 prepare 返回的 continuation' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function requireCache(env) {
|
|
69
|
+
if (!env.recruitContext)
|
|
70
|
+
throw ehrValidationError('recruit_context_unavailable', '招聘上下文加载器未初始化');
|
|
71
|
+
return env.recruitContext.load(env.host);
|
|
72
|
+
}
|
|
73
|
+
/** 取一条已有职位记录,作为「关联表单型字段」的默认值来源(取不到就返回空,不编造 id)。 */
|
|
74
|
+
async function fetchReferenceDetails(env, cache, objId, formId) {
|
|
75
|
+
const list = cache.lists.position;
|
|
76
|
+
if (!list?.listId)
|
|
77
|
+
return [];
|
|
78
|
+
try {
|
|
79
|
+
const body = buildListBody({
|
|
80
|
+
listId: list.listId,
|
|
81
|
+
cusMenuId: list.cusMenuId,
|
|
82
|
+
menuName: list.menuName || '招聘职位',
|
|
83
|
+
title: '招聘职位',
|
|
84
|
+
pageSize: 1,
|
|
85
|
+
});
|
|
86
|
+
const page = await fetchListRows(env.host, objId, list.listId, body);
|
|
87
|
+
const row = page.rows[0];
|
|
88
|
+
const recordId = row?.form_data_id || row?.id;
|
|
89
|
+
if (!recordId)
|
|
90
|
+
return [];
|
|
91
|
+
const record = await getFormDataRecord(env.host, formId, String(recordId), objId);
|
|
92
|
+
return toSubmitDetails(record.dataDetails);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function buildPositionPlan(env, cache, args) {
|
|
99
|
+
const form = cache.forms.position_manage;
|
|
100
|
+
const objId = form?.objId;
|
|
101
|
+
const formId = form?.formId || objId;
|
|
102
|
+
if (!objId || !formId) {
|
|
103
|
+
throw ehrValidationError('position_form_unavailable', '当前环境未启用「招聘职位」功能(未解析到 position_manage 表单),无法建职位');
|
|
104
|
+
}
|
|
105
|
+
const fields = cache.fields[objId] || {};
|
|
106
|
+
const fid = (key) => fields[key];
|
|
107
|
+
for (const key of [...TEXT_KEYS, ...MTEXT_KEYS]) {
|
|
108
|
+
if (!fid(key)) {
|
|
109
|
+
throw ehrValidationError('recruit_field_missing', `职位管理缺少字段 ${key}(ds/fields 解析不到),无法建职位`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const positionName = String(args.positionName ?? '').trim();
|
|
113
|
+
if (!positionName)
|
|
114
|
+
throw ehrValidationError('position_name_required', 'positionName 必填');
|
|
115
|
+
const data = new Map();
|
|
116
|
+
const put = (entry) => {
|
|
117
|
+
if (entry && entry.formField?.id)
|
|
118
|
+
data.set(String(entry.formField.id), entry);
|
|
119
|
+
};
|
|
120
|
+
const has = (key) => {
|
|
121
|
+
const id = fid(key);
|
|
122
|
+
return !!(id && data.has(String(id)));
|
|
123
|
+
};
|
|
124
|
+
// ① 核心字段
|
|
125
|
+
put({ formField: { id: fid('position_name') }, content: positionName });
|
|
126
|
+
const duty = typeof args.duty === 'string' ? args.duty : '';
|
|
127
|
+
const requirement = typeof args.requirement === 'string' ? args.requirement : '';
|
|
128
|
+
if (duty && fid('position_duty'))
|
|
129
|
+
put({ formField: { id: fid('position_duty') }, dataText: { content: duty } });
|
|
130
|
+
if (requirement && fid('work_request'))
|
|
131
|
+
put({ formField: { id: fid('work_request') }, dataText: { content: requirement } });
|
|
132
|
+
// ② 关联招聘需求(可选)
|
|
133
|
+
const applied = [];
|
|
134
|
+
const skipped = [];
|
|
135
|
+
if (args.demandId !== undefined && args.demandId !== null && args.demandId !== '') {
|
|
136
|
+
if (!fid('demand'))
|
|
137
|
+
throw ehrValidationError('recruit_field_missing', '职位管理缺少字段 demand,无法关联招聘需求');
|
|
138
|
+
const demandId = String(args.demandId).replace(/[^\d]/g, '');
|
|
139
|
+
if (!demandId)
|
|
140
|
+
throw ehrValidationError('demand_id_invalid', 'demandId 必须是数字 ID');
|
|
141
|
+
put({
|
|
142
|
+
formField: { id: fid('demand') },
|
|
143
|
+
dataOptions: [{ optionId: demandId, content: typeof args.demandName === 'string' && args.demandName ? args.demandName : demandId }],
|
|
144
|
+
});
|
|
145
|
+
applied.push(`demand=${demandId}`);
|
|
146
|
+
}
|
|
147
|
+
// ③ 显式入参覆盖(优先于默认值)
|
|
148
|
+
for (const field of SET_FIELDS) {
|
|
149
|
+
const raw = args[field.arg];
|
|
150
|
+
if (raw === undefined || raw === null || String(raw).trim() === '')
|
|
151
|
+
continue;
|
|
152
|
+
const value = String(raw).trim();
|
|
153
|
+
const fieldId = fid(field.key);
|
|
154
|
+
if (!fieldId) {
|
|
155
|
+
skipped.push(`${field.key}(职位表单无该字段)`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (field.kind === 'text' || field.kind === 'number') {
|
|
159
|
+
put({ formField: { id: fieldId }, content: value });
|
|
160
|
+
applied.push(`${field.key}=${value}`);
|
|
161
|
+
}
|
|
162
|
+
else if (field.kind === 'option') {
|
|
163
|
+
const options = await getDsOptions(env.host, objId, fieldId, cache.appId);
|
|
164
|
+
const optionId = options[value];
|
|
165
|
+
if (!optionId) {
|
|
166
|
+
skipped.push(`${field.key}=${value}(选项未匹配:${Object.keys(options).slice(0, 8).join('/')}…)`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
put({ formField: { id: fieldId }, dataOptions: [{ optionId: String(optionId), content: value }] });
|
|
170
|
+
applied.push(`${field.key}=${value}`);
|
|
171
|
+
}
|
|
172
|
+
else if (field.kind === 'dict') {
|
|
173
|
+
const ids = cache.dict.by_name[value] || [];
|
|
174
|
+
const unique = [...new Set(ids.map(String))];
|
|
175
|
+
if (unique.length !== 1) {
|
|
176
|
+
skipped.push(`${field.key}=${value}(字典${unique.length === 0 ? '无此项' : '同名多项,歧义'},未写入)`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
put({ formField: { id: fieldId }, dataOptions: [{ optionId: unique[0], content: value }] });
|
|
180
|
+
applied.push(`${field.key}=${value}`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const personId = await resolvePersonId(env.host, fieldId, value);
|
|
184
|
+
if (!personId) {
|
|
185
|
+
skipped.push(`${field.key}=${value}(人员未匹配)`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
put({ formField: { id: fieldId }, dataOptions: [{ optionId: String(personId), content: value }] });
|
|
189
|
+
applied.push(`${field.key}=${value}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ④ 默认值:枚举 / 数值 / 当前用户
|
|
193
|
+
for (const [key, name] of Object.entries(OPTION_DEFAULTS)) {
|
|
194
|
+
const fieldId = fid(key);
|
|
195
|
+
if (!fieldId || has(key))
|
|
196
|
+
continue;
|
|
197
|
+
const options = await getDsOptions(env.host, objId, fieldId, cache.appId);
|
|
198
|
+
const optionId = options[name];
|
|
199
|
+
if (optionId)
|
|
200
|
+
put({ formField: { id: fieldId }, dataOptions: [{ optionId: String(optionId), content: name }] });
|
|
201
|
+
}
|
|
202
|
+
for (const [key, value] of Object.entries(TEXT_DEFAULTS)) {
|
|
203
|
+
const fieldId = fid(key);
|
|
204
|
+
if (fieldId && !has(key))
|
|
205
|
+
put({ formField: { id: fieldId }, content: value });
|
|
206
|
+
}
|
|
207
|
+
const session = await env.host.getSession();
|
|
208
|
+
for (const key of PERSON_DEFAULT_KEYS) {
|
|
209
|
+
const fieldId = fid(key);
|
|
210
|
+
if (fieldId && !has(key) && session.userId) {
|
|
211
|
+
put({ formField: { id: fieldId }, dataOptions: [{ optionId: session.userId, content: '当前用户' }] });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ⑤ 关联表单型字段:沿用在册职位的现值(引用有效性由系统保证)
|
|
215
|
+
const refDetails = await fetchReferenceDetails(env, cache, objId, formId);
|
|
216
|
+
const refMap = new Map();
|
|
217
|
+
for (const detail of refDetails) {
|
|
218
|
+
const id = detail?.formField?.id;
|
|
219
|
+
if (id)
|
|
220
|
+
refMap.set(String(id), detail);
|
|
221
|
+
}
|
|
222
|
+
const inherited = [];
|
|
223
|
+
const missingRef = [];
|
|
224
|
+
for (const key of REF_COPY_KEYS) {
|
|
225
|
+
const fieldId = fid(key);
|
|
226
|
+
if (!fieldId || has(key))
|
|
227
|
+
continue;
|
|
228
|
+
const ref = refMap.get(String(fieldId));
|
|
229
|
+
if (ref) {
|
|
230
|
+
put(ref);
|
|
231
|
+
inherited.push(key);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
missingRef.push(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const inverse = {};
|
|
238
|
+
for (const [dataKey, fieldId] of Object.entries(fields))
|
|
239
|
+
inverse[String(fieldId)] = dataKey;
|
|
240
|
+
return {
|
|
241
|
+
objId,
|
|
242
|
+
formId,
|
|
243
|
+
dataDetails: [...data.values()],
|
|
244
|
+
summary: {
|
|
245
|
+
positionName,
|
|
246
|
+
dutyLength: duty.length,
|
|
247
|
+
requirementLength: requirement.length,
|
|
248
|
+
fieldCount: data.size,
|
|
249
|
+
filledFields: [...data.keys()].map((id) => inverse[id] || id).sort(),
|
|
250
|
+
explicitApplied: applied,
|
|
251
|
+
explicitSkipped: skipped,
|
|
252
|
+
inheritedFromExistingPosition: inherited,
|
|
253
|
+
noReferenceValue: missingRef,
|
|
254
|
+
principalDefault: session.userId ? '当前登录用户' : '未解析到当前用户',
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const positionCreatePrepare = {
|
|
259
|
+
name: 'ehr.recruit.position.create.prepare',
|
|
260
|
+
resource: 'recruit',
|
|
261
|
+
method: 'position.create.prepare',
|
|
262
|
+
action: 'ehr.recruit.position.create.prepare',
|
|
263
|
+
summary: '生成职位并建到招聘职位列表前的确认摘要:解析职位表单字段、套用默认值(状态/流程/负责人/关联字段),不写入任何数据',
|
|
264
|
+
capability: 'recruit',
|
|
265
|
+
risk: WRITE,
|
|
266
|
+
requiresConfirmation: true,
|
|
267
|
+
inputSchema: schemaOf({
|
|
268
|
+
positionName: { type: 'string', maxLength: 120, description: '职位名称(必填)' },
|
|
269
|
+
duty: { type: 'string', maxLength: 20000, description: '岗位职责 / JD 正文(写入职位职责多行文本)' },
|
|
270
|
+
requirement: { type: 'string', maxLength: 20000, description: '任职要求(写入工作任职要求多行文本)' },
|
|
271
|
+
demandId: { ...SCHEMA_ID, description: '关联招聘需求 id(可选)' },
|
|
272
|
+
demandName: { type: 'string', maxLength: 200, description: '关联需求名称(仅用于摘要展示)' },
|
|
273
|
+
...SET_SCHEMA,
|
|
274
|
+
}, { required: ['positionName'] }),
|
|
275
|
+
handler: async (args, env) => {
|
|
276
|
+
const cache = await requireCache(env);
|
|
277
|
+
const plan = await buildPositionPlan(env, cache, args);
|
|
278
|
+
const session = await env.host.getSession();
|
|
279
|
+
const continuation = await env.continuation.issue({
|
|
280
|
+
operation: 'ehr.recruit.position.create.apply',
|
|
281
|
+
baseUrl: session.baseUrl,
|
|
282
|
+
profile: session.profile,
|
|
283
|
+
userId: session.userId,
|
|
284
|
+
payload: { objId: plan.objId, formId: plan.formId, positionName: String(args.positionName).trim(), dataDetails: plan.dataDetails },
|
|
285
|
+
fingerprint: fingerprint({ objId: plan.objId, dataDetails: plan.dataDetails }),
|
|
286
|
+
}, 10 * 60 * 1000);
|
|
287
|
+
return ehrSuccess('ehr.recruit.position.create.prepare', {
|
|
288
|
+
status: 'AWAITING_CONFIRMATION',
|
|
289
|
+
preview: { action: '生成职位JD并建职位(saveFormData → addPosition)', ...plan.summary },
|
|
290
|
+
willSubmit: { objId: plan.objId, formId: plan.formId, dataDetails: plan.dataDetails },
|
|
291
|
+
continuation,
|
|
292
|
+
expiresInSeconds: 600,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
const positionCreateApply = {
|
|
297
|
+
name: 'ehr.recruit.position.create.apply',
|
|
298
|
+
resource: 'recruit',
|
|
299
|
+
method: 'position.create.apply',
|
|
300
|
+
action: 'ehr.recruit.position.create.apply',
|
|
301
|
+
summary: '执行建职位(saveFormData → addPosition);必须先 prepare 并经用户明确确认',
|
|
302
|
+
capability: 'recruit',
|
|
303
|
+
risk: WRITE,
|
|
304
|
+
requiresConfirmation: true,
|
|
305
|
+
inputSchema: schemaOf({
|
|
306
|
+
positionName: { type: 'string', maxLength: 120 },
|
|
307
|
+
confirm: { const: true },
|
|
308
|
+
continuation: { type: 'string', minLength: 1 },
|
|
309
|
+
}, { required: ['positionName', 'confirm', 'continuation'] }),
|
|
310
|
+
handler: async (args, env) => {
|
|
311
|
+
requireConfirm(args, 'ehr.recruit.position.create.apply');
|
|
312
|
+
const lease = await env.continuation.verify(args.continuation, 'ehr.recruit.position.create.apply');
|
|
313
|
+
const session = await env.host.getSession();
|
|
314
|
+
if (lease.baseUrl !== session.baseUrl || lease.profile !== session.profile || lease.userId !== session.userId) {
|
|
315
|
+
throw ehrValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 prepare');
|
|
316
|
+
}
|
|
317
|
+
const positionName = String(args.positionName ?? '').trim();
|
|
318
|
+
const payload = lease.payload || {};
|
|
319
|
+
if (payload.positionName !== positionName) {
|
|
320
|
+
throw ehrValidationError('target_changed', '职位名称与 prepare 时不一致,请重新执行 prepare');
|
|
321
|
+
}
|
|
322
|
+
const cache = await requireCache(env);
|
|
323
|
+
// ① 保存职位表单
|
|
324
|
+
const formDataId = await saveFormData(env.host, {
|
|
325
|
+
appId: cache.appId,
|
|
326
|
+
formId: String(payload.formId),
|
|
327
|
+
objId: String(payload.objId),
|
|
328
|
+
name: `职位管理-${positionName}`,
|
|
329
|
+
dataDetails: payload.dataDetails || [],
|
|
330
|
+
});
|
|
331
|
+
if (!formDataId) {
|
|
332
|
+
throw ehrValidationError('save_failed', 'saveFormData 未返回 formDataId,职位未创建');
|
|
333
|
+
}
|
|
334
|
+
// ② 提交生成职位(与页面「保存」的第二步等价)
|
|
335
|
+
const response = (await env.host.request({
|
|
336
|
+
path: `/api/recruit/position/v2/addPosition?formDataId=${encodeURIComponent(formDataId)}`,
|
|
337
|
+
method: 'POST',
|
|
338
|
+
operation: 'ehr.recruit.position.create.apply',
|
|
339
|
+
}));
|
|
340
|
+
const data = response?.data || {};
|
|
341
|
+
const positionPostId = data.positionPostId ? String(data.positionPostId) : '';
|
|
342
|
+
const created = response?.status !== false && !!positionPostId;
|
|
343
|
+
const list = cache.lists.position;
|
|
344
|
+
const baseUrl = session.baseUrl.replace(/\/+$/, '');
|
|
345
|
+
const cardUrl = `${baseUrl}/sp/ebdfpage/card/0/${payload.objId}/${formDataId}`;
|
|
346
|
+
const listLocateUrl = list?.listId
|
|
347
|
+
? `${baseUrl}/sp/ebdfpage/list/${list.listId}/card/0/${payload.objId}/${formDataId}/${list.listId}?id=${formDataId}&openSource=field&openSourceId=${formDataId}`
|
|
348
|
+
: null;
|
|
349
|
+
if (!created) {
|
|
350
|
+
throw ehrValidationError('position_create_failed', `职位表单已保存(formDataId=${formDataId}),但 addPosition 未返回职位 id:${response?.msg || '未知原因'};请到职位管理核对后再决定是否重试`);
|
|
351
|
+
}
|
|
352
|
+
return ehrSuccess('ehr.recruit.position.create.apply', {
|
|
353
|
+
result: 'success',
|
|
354
|
+
formDataId,
|
|
355
|
+
positionPostId,
|
|
356
|
+
rpaChecked: data.checkRpa === undefined ? null : !!data.checkRpa,
|
|
357
|
+
rpaValidMsg: data.rpaValidMsg || null,
|
|
358
|
+
positionName,
|
|
359
|
+
cardUrl,
|
|
360
|
+
listLocateUrl,
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
export const recruitPositionOperations = [
|
|
365
|
+
positionCreatePrepare,
|
|
366
|
+
positionCreateApply,
|
|
367
|
+
];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ehrValidationError } from '../errors.js';
|
|
2
|
+
import { READ, ehrSuccess, schemaOf, } from './types.js';
|
|
3
|
+
import { buildListBody, fetchListRows, } from './recruit.js';
|
|
4
|
+
/**
|
|
5
|
+
* 招聘工作统计:各业务模块记录数(基于稳定列表接口聚合)。
|
|
6
|
+
*
|
|
7
|
+
* 口径来源:ehr 技能 recruit-stats.mjs(真机验证)。
|
|
8
|
+
* 计数降级三态(绝不输出假值):
|
|
9
|
+
* ① 计数接口 getListDataCount 可用 → total 精确;
|
|
10
|
+
* ② 计数不可用、但分页翻到末页(页内行数 < pageSize)→ total = 累计行数(精确);
|
|
11
|
+
* ③ 计数不可用、且达扫描上限仍满页 → total = null(总数不可知),附 note。
|
|
12
|
+
* 明确不做:nchart 漏斗看板与 EDC 报表(需页面 compId/sheetId 配置,跨环境不稳定)。
|
|
13
|
+
*/
|
|
14
|
+
const STATS_MODULES = {
|
|
15
|
+
talent: { listKey: 'talent', formType: 'talent_pool', title: '人才库' },
|
|
16
|
+
demand: { listKey: 'demand', formType: 'demand_manage', title: '招聘需求' },
|
|
17
|
+
position: { listKey: 'position', formType: 'position_manage', title: '招聘职位' },
|
|
18
|
+
entry: { listKey: 'entry', formType: 'entry_manage', title: '入职管理' },
|
|
19
|
+
offer: { listKey: 'offer', formType: 'offer', title: '录用管理' },
|
|
20
|
+
interview: {
|
|
21
|
+
listKey: 'interview',
|
|
22
|
+
formType: 'interview_plan',
|
|
23
|
+
title: '面试安排',
|
|
24
|
+
note: '面试管理列表可能按页面配置只显示近期记录,计数仅供量级参考',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const DEFAULT_MODULES = ['talent', 'demand', 'position', 'entry', 'offer'];
|
|
28
|
+
async function countModule(env, cache, key, pageSize, maxPages) {
|
|
29
|
+
const cfg = STATS_MODULES[key];
|
|
30
|
+
const list = cache.lists[cfg.listKey];
|
|
31
|
+
const objId = cache.forms[cfg.formType]?.objId;
|
|
32
|
+
if (!list?.listId || !objId) {
|
|
33
|
+
return {
|
|
34
|
+
key,
|
|
35
|
+
module: cfg.title,
|
|
36
|
+
available: false,
|
|
37
|
+
total: null,
|
|
38
|
+
totalExact: false,
|
|
39
|
+
scannedRows: 0,
|
|
40
|
+
note: '当前环境未启用该功能菜单(未找到列表或表单),已跳过',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const baseBody = buildListBody({
|
|
44
|
+
listId: list.listId,
|
|
45
|
+
cusMenuId: list.cusMenuId,
|
|
46
|
+
menuName: list.menuName || cfg.title,
|
|
47
|
+
title: cfg.title,
|
|
48
|
+
pageSize,
|
|
49
|
+
});
|
|
50
|
+
let scanned = 0;
|
|
51
|
+
let total = null;
|
|
52
|
+
let totalExact = false;
|
|
53
|
+
let note = cfg.note;
|
|
54
|
+
for (let current = 1; current <= maxPages; current++) {
|
|
55
|
+
const page = await fetchListRows(env.host, objId, list.listId, { ...baseBody, current, pageSize });
|
|
56
|
+
// ① 计数接口可用(total 为数字且精确)→ 直接用,不必翻页
|
|
57
|
+
if (current === 1 && page.total !== null && page.totalExact) {
|
|
58
|
+
total = page.total;
|
|
59
|
+
totalExact = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
scanned += page.rows.length;
|
|
63
|
+
// ② 页内行数 < pageSize 即已到末页 → 累计行数就是总数
|
|
64
|
+
if (page.rows.length < pageSize) {
|
|
65
|
+
total = scanned;
|
|
66
|
+
totalExact = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ③ 达上限仍未取完 → 总数不可知(不编造 0 或估计值)
|
|
71
|
+
if (total === null) {
|
|
72
|
+
totalExact = false;
|
|
73
|
+
note = `计数接口不可用且达扫描上限(${maxPages} 页 × ${pageSize} 行)仍未取完,总数不可知;可调大 pageSize/maxPages 重试`;
|
|
74
|
+
}
|
|
75
|
+
return { key, module: cfg.title, available: true, total, totalExact, scannedRows: scanned, ...(note ? { note } : {}) };
|
|
76
|
+
}
|
|
77
|
+
const recruitStats = {
|
|
78
|
+
name: 'ehr.recruit.stats',
|
|
79
|
+
resource: 'recruit',
|
|
80
|
+
method: 'stats',
|
|
81
|
+
action: 'ehr.recruit.stats',
|
|
82
|
+
summary: '招聘工作统计:按模块统计记录数(人才库/招聘需求/招聘职位/入职管理/录用管理/面试安排),计数不可用时如实标"总数不可知"',
|
|
83
|
+
capability: 'recruit',
|
|
84
|
+
risk: READ,
|
|
85
|
+
inputSchema: schemaOf({
|
|
86
|
+
modules: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
items: { type: 'string', enum: Object.keys(STATS_MODULES) },
|
|
89
|
+
uniqueItems: true,
|
|
90
|
+
description: `要统计的模块,默认 ${DEFAULT_MODULES.join(',')}`,
|
|
91
|
+
},
|
|
92
|
+
pageSize: { type: 'integer', minimum: 1, maximum: 200, description: '单页行数,默认 100(计数接口不可用时用于分页实探)' },
|
|
93
|
+
maxPages: { type: 'integer', minimum: 1, maximum: 20, description: '最多翻页数,默认 5' },
|
|
94
|
+
}),
|
|
95
|
+
handler: async (args, env) => {
|
|
96
|
+
if (!env.recruitContext) {
|
|
97
|
+
throw ehrValidationError('recruit_context_unavailable', '招聘上下文加载器未初始化');
|
|
98
|
+
}
|
|
99
|
+
const requested = args.modules === undefined
|
|
100
|
+
? DEFAULT_MODULES
|
|
101
|
+
: (Array.isArray(args.modules) ? args.modules.map(String) : []);
|
|
102
|
+
if (!requested.length) {
|
|
103
|
+
throw ehrValidationError('modules_required', `modules 必须是非空数组,可选值:${Object.keys(STATS_MODULES).join(' / ')}`);
|
|
104
|
+
}
|
|
105
|
+
for (const key of requested) {
|
|
106
|
+
if (!STATS_MODULES[key]) {
|
|
107
|
+
throw ehrValidationError('enum_invalid', `modules 含不支持的值:${key}(可选 ${Object.keys(STATS_MODULES).join(' / ')})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const pageSize = Number.isInteger(args.pageSize) ? Number(args.pageSize) : 100;
|
|
111
|
+
const maxPages = Number.isInteger(args.maxPages) ? Number(args.maxPages) : 5;
|
|
112
|
+
const cache = await env.recruitContext.load(env.host);
|
|
113
|
+
// 模块间并发;每个模块内部翻页串行(服务端计数不可信,只能靠分页递进)
|
|
114
|
+
const modules = await Promise.all(requested.map((key) => countModule(env, cache, key, pageSize, maxPages)));
|
|
115
|
+
const known = modules.filter((m) => m.available && m.total !== null);
|
|
116
|
+
const unknown = modules.filter((m) => m.available && m.total === null);
|
|
117
|
+
const unavailable = modules.filter((m) => !m.available);
|
|
118
|
+
return ehrSuccess('ehr.recruit.stats', {
|
|
119
|
+
modules,
|
|
120
|
+
summary: {
|
|
121
|
+
knownTotal: known.reduce((sum, m) => sum + m.total, 0),
|
|
122
|
+
knownModules: known.map((m) => m.key),
|
|
123
|
+
unknownModules: unknown.map((m) => m.key),
|
|
124
|
+
unavailableModules: unavailable.map((m) => m.key),
|
|
125
|
+
note: unknown.length
|
|
126
|
+
? '存在总数不可知的模块(见对应 note),未计入 knownTotal'
|
|
127
|
+
: undefined,
|
|
128
|
+
},
|
|
129
|
+
}, { pageSize, maxPages });
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
export const recruitStatsOperations = [recruitStats];
|
|
133
|
+
export { STATS_MODULES as RECRUIT_STATS_MODULES };
|
|
@@ -29,7 +29,7 @@ async function requireRecruitCache(env) {
|
|
|
29
29
|
return env.recruitContext.load(env.host);
|
|
30
30
|
}
|
|
31
31
|
/** 建前查重:按手机号搜人才库(total 恒 0 平台缺陷,以 data.data[] 非空为准)。 */
|
|
32
|
-
async function checkDuplicateByMobile(env, cache, mobile) {
|
|
32
|
+
export async function checkDuplicateByMobile(env, cache, mobile) {
|
|
33
33
|
const talent = cache.lists.talent;
|
|
34
34
|
const objId = cache.forms.talent_pool?.objId;
|
|
35
35
|
if (!talent?.listId || !objId) {
|
|
@@ -158,7 +158,7 @@ async function runEsb(host, objId, dataIds, esbActionIds) {
|
|
|
158
158
|
return response?.data || {};
|
|
159
159
|
}
|
|
160
160
|
/** ds/options 运行时解析枚举(面试方式/轮次/结果等):name → optionId */
|
|
161
|
-
async function getDsOptions(host, objId, fieldId, groupId) {
|
|
161
|
+
export async function getDsOptions(host, objId, fieldId, groupId) {
|
|
162
162
|
const response = (await host.request({
|
|
163
163
|
path: '/api/ebuilder/common/ds/options',
|
|
164
164
|
query: { fieldName: fieldId, objId, sourceType: 'FORM', groupId },
|
|
@@ -171,7 +171,7 @@ async function getDsOptions(host, objId, fieldId, groupId) {
|
|
|
171
171
|
return map;
|
|
172
172
|
}
|
|
173
173
|
/** 人员名称 → userId(门户字段 browser/complete/resource) */
|
|
174
|
-
async function resolvePersonId(host, fieldId, keyword) {
|
|
174
|
+
export async function resolvePersonId(host, fieldId, keyword) {
|
|
175
175
|
const response = (await host.request({
|
|
176
176
|
path: '/api/ebuilder/form/common/browser/complete/resource',
|
|
177
177
|
body: { formParam: { fieldId, searchValue: keyword }, 'formParam.fieldId': fieldId, 'formParam.searchValue': keyword },
|
|
@@ -180,15 +180,8 @@ async function resolvePersonId(host, fieldId, keyword) {
|
|
|
180
180
|
const first = list[0];
|
|
181
181
|
return first ? String(first.id || first.value || '') || null : null;
|
|
182
182
|
}
|
|
183
|
-
/** 安排面试专用:面试计划门户字段 id 映射(talent/candidate_manage/interviewer/position...) */
|
|
184
|
-
async function getInterviewPortalFieldIds(host) {
|
|
185
|
-
const response = (await host.request({
|
|
186
|
-
path: '/api/recruit/candidate/getFieldIdsForInterview',
|
|
187
|
-
}));
|
|
188
|
-
return response?.data || {};
|
|
189
|
-
}
|
|
190
183
|
/** saveFormData:新建(rightType 0)或更新(rightType 2,带 formDataId/updateTimeStamp/layoutId) */
|
|
191
|
-
async function saveFormData(host, params) {
|
|
184
|
+
export async function saveFormData(host, params) {
|
|
192
185
|
const layoutId = params.layoutId || params.formId; // getFormLayout GET 405 不可靠,新建回退 formId(与 mjs 一致)
|
|
193
186
|
const customParam = {
|
|
194
187
|
objId: params.objId,
|
|
@@ -222,7 +215,7 @@ async function saveFormData(host, params) {
|
|
|
222
215
|
return String(response?.data?.id || '');
|
|
223
216
|
}
|
|
224
217
|
/** 取记录当前数据(updateTimeStamp 乐观锁 + dataDetails 全字段 + 编辑布局 id) */
|
|
225
|
-
async function getFormDataRecord(host, formId, dataId, objId) {
|
|
218
|
+
export async function getFormDataRecord(host, formId, dataId, objId) {
|
|
226
219
|
const response = (await host.request({
|
|
227
220
|
path: `/api/ebuilder/form/form/core/getFormDataAndLayout?formId=${formId}&dataId=${dataId}`,
|
|
228
221
|
method: 'POST',
|
|
@@ -242,7 +235,7 @@ async function getFormDataRecord(host, formId, dataId, objId) {
|
|
|
242
235
|
};
|
|
243
236
|
}
|
|
244
237
|
/** getFormDataAndLayout 返回的 dataDetails → saveFormData 可提交结构 */
|
|
245
|
-
function toSubmitDetails(details) {
|
|
238
|
+
export function toSubmitDetails(details) {
|
|
246
239
|
const out = [];
|
|
247
240
|
for (const item of details || []) {
|
|
248
241
|
const fieldId = item?.fieldId || item?.formField?.id;
|
|
@@ -546,11 +539,10 @@ async function buildInterviewPlan(env, cache, args) {
|
|
|
546
539
|
const objId = cacheObjId(cache, 'interview_plan', '面试计划表单');
|
|
547
540
|
const formId = cacheFormId(cache, 'interview_plan');
|
|
548
541
|
const fields = cache.fields[objId] || {};
|
|
549
|
-
const
|
|
550
|
-
const fieldIdOf = (key) => fields[key] || portalFields[key] || '';
|
|
542
|
+
const fieldIdOf = (key) => fields[key] || '';
|
|
551
543
|
for (const key of INTERVIEW_DATAKEYS) {
|
|
552
544
|
if (!fieldIdOf(key)) {
|
|
553
|
-
throw ehrValidationError('recruit_field_missing', `面试计划表单缺少字段 ${key}
|
|
545
|
+
throw ehrValidationError('recruit_field_missing', `面试计划表单缺少字段 ${key}(环境缓存 ds/fields 未解析到,请重建环境缓存后重试)`);
|
|
554
546
|
}
|
|
555
547
|
}
|
|
556
548
|
const typeMap = await getDsOptions(host, objId, fieldIdOf('interview_type'), cache.appId);
|