weaver-work-cli 0.1.40 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{archive-JHTONRST.js → archive-J4I6MCKF.js} +2 -2
- package/dist/chunks/{archive-oa-FCHLCPHC.js → archive-oa-BRZKKTO4.js} +2 -2
- package/dist/chunks/{asset-RLIHC7DB.js → asset-L7D4FAOZ.js} +2 -2
- package/dist/chunks/{asset-idle-KVOFTLCP.js → asset-idle-DADLLV75.js} +2 -2
- package/dist/chunks/{attachment-content-RZYDB3MB.js → attachment-content-K4C7VLWQ.js} +3 -3
- package/dist/chunks/{blog-42OZTJNP.js → blog-BVGQ7NZL.js} +2 -2
- package/dist/chunks/{caizhilian-KRK547K5.js → caizhilian-SAADNFD6.js} +2 -2
- package/dist/chunks/{calendar-RTWAMKHD.js → calendar-Q7NQY5Z2.js} +61 -23
- package/dist/chunks/{chunk-LFMSDBZN.js → chunk-3KTFS4IA.js} +308 -153
- package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
- package/dist/chunks/{chunk-T6EGVBKU.js → chunk-4RAE6JXP.js} +1 -1
- package/dist/chunks/{chunk-7BRU4EFH.js → chunk-72CKAUAG.js} +29 -2
- package/dist/chunks/{chunk-NETCCARD.js → chunk-APVFJUQH.js} +2 -2
- package/dist/chunks/{chunk-CAQNBBQA.js → chunk-BBKDXQEC.js} +2 -2
- package/dist/chunks/{chunk-57IECMWX.js → chunk-BIPYLLVS.js} +26 -12
- package/dist/chunks/{chunk-LN447PY2.js → chunk-EKWAQSLU.js} +3 -2
- package/dist/chunks/{chunk-4TEDNCON.js → chunk-FOAA27U5.js} +338 -52
- package/dist/chunks/{chunk-7U577Q7U.js → chunk-G4UR6NXN.js} +2 -2
- package/dist/chunks/{chunk-NJKREDA3.js → chunk-GEWYMQME.js} +1 -1
- package/dist/chunks/{chunk-I566ZYNF.js → chunk-GHXCDPTM.js} +1 -1
- package/dist/chunks/{chunk-4CTESSKI.js → chunk-IZ7S7QUS.js} +4 -4
- package/dist/chunks/{chunk-VNBU55SL.js → chunk-JPRO6ER2.js} +57 -2432
- package/dist/chunks/{chunk-IZSSVYAG.js → chunk-LADPYOWC.js} +2 -2
- package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
- package/dist/chunks/chunk-NKZRQXST.js +2546 -0
- package/dist/chunks/{chunk-JWRZM2H2.js → chunk-QKNKR4RH.js} +3 -3
- package/dist/chunks/{chunk-FLS4DPGP.js → chunk-R4CHUSKC.js} +28 -15
- package/dist/chunks/{chunk-WFFNM65B.js → chunk-SA6VBIHY.js} +73 -19
- package/dist/chunks/{chunk-SPRHHSVC.js → chunk-TTPJJJC4.js} +2 -2
- package/dist/chunks/{chunk-7E6MUOEK.js → chunk-W7BEHOSR.js} +2 -2
- package/dist/chunks/{chunk-CXRQWOZ5.js → chunk-YRSZQUD2.js} +2 -2
- package/dist/chunks/{conflict-VR55AOOO.js → conflict-F2OMZRTO.js} +2 -2
- package/dist/chunks/{contract-HI4FNPYE.js → contract-5BETHRSG.js} +2 -2
- package/dist/chunks/{cowork-HJLI353D.js → cowork-3MN6EQQO.js} +22 -12
- package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
- package/dist/chunks/{ebuilder-form-HTGI63UU.js → ebuilder-form-SBHOYBUD.js} +3 -3
- package/dist/chunks/{ehr-GYXIYH3A.js → ehr-KMOJULKG.js} +432 -1234
- package/dist/chunks/{env-DW3G4HUO.js → env-LB7NBL2O.js} +2 -2
- package/dist/chunks/{esb-I5INCEFE.js → esb-W2PJCFCX.js} +4 -4
- package/dist/chunks/{factory-TKXGXS6L.js → factory-6ZFQXQ46.js} +1 -1
- package/dist/chunks/{fna-K2MHTQ24.js → fna-ZQPCUV4R.js} +2 -2
- package/dist/chunks/{group-chat-VYBNMJLG.js → group-chat-2HHAJN46.js} +5 -3
- package/dist/chunks/{host-ZUQTJYYK.js → host-4CBHGOHT.js} +3 -3
- package/dist/chunks/{host-D5SERMAJ.js → host-ATKVNZAT.js} +3 -3
- package/dist/chunks/{host-JM4YVHFM.js → host-BQH22NTQ.js} +3 -3
- package/dist/chunks/{host-L3VLITMT.js → host-X2CSLFM6.js} +3 -3
- package/dist/chunks/{host-256GVCAH.js → host-YTODKMSL.js} +3 -3
- package/dist/chunks/{hrm-VLRBKTZB.js → hrm-FFM4UJFS.js} +4 -4
- package/dist/chunks/{im-QK3VTLXK.js → im-VEHKDQPB.js} +7 -7
- package/dist/chunks/{invoice-T5JTA32H.js → invoice-LVIKSIIF.js} +3 -3
- package/dist/chunks/{jiuchuanhui-P2FNEQVZ.js → jiuchuanhui-X6DINO5P.js} +2 -2
- package/dist/chunks/{lazy-dispatch-XRRI46M6.js → lazy-dispatch-KSWTN5L6.js} +4 -4
- package/dist/chunks/{mail-3Y7MJ6PS.js → mail-VDKRGUD3.js} +2 -2
- package/dist/chunks/{meeting-M7LTXGJE.js → meeting-CLC6HP3Z.js} +12 -11
- package/dist/chunks/{okr-OBC6S67L.js → okr-CHG4OO3U.js} +2 -2
- package/dist/chunks/{plan-5H6I53SK.js → plan-XWR75667.js} +2 -2
- package/dist/chunks/{project-L75QZV4S.js → project-5BXOOKMG.js} +40 -23
- package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
- package/dist/chunks/{registry-RXPN6BBT.js → registry-7FFOXIAW.js} +4 -4
- package/dist/chunks/{registry-KMHCPAAD.js → registry-BKBZPBAJ.js} +10 -9
- package/dist/chunks/{registry-QGHFNUWS.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-UGRBCBHO.js → registry-RHMW6ANW.js} +2 -2
- package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
- package/dist/chunks/{room-recommend-A3UDC3CA.js → room-recommend-62H7ZQQ6.js} +4 -4
- package/dist/chunks/{schedule-5GCACCK7.js → schedule-FNXKH3DQ.js} +4 -4
- package/dist/chunks/{workflow-M5ZJ7DWF.js → workflow-ADHKCAJK.js} +6 -6
- package/dist/index.js +33 -28
- package/dist/internal/e10/auth/commands.js +1 -0
- package/dist/internal/e10/auth/messages.js +6 -0
- package/dist/internal/e10/auth/session.js +17 -0
- package/dist/internal/e10/auth/xiaoe.js +286 -44
- package/dist/internal/e10/auth-recovery.js +36 -5
- package/dist/internal/e10/context.js +56 -12
- 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/shortcuts/calendar/operations/read.js +31 -22
- package/dist/shortcuts/calendar/operations/write.js +45 -1
- 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/ehr/operations/attend-write.js +43 -1
- 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 +134 -59
- package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
- package/dist/shortcuts/ehr/operations/recruit.js +10 -4
- package/dist/shortcuts/ehr/operations/shared.js +47 -0
- package/dist/shortcuts/hrm/operations/org.js +118 -0
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +85 -28
- package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
- 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/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/recruit-stage/context.js +119 -0
- package/dist/shortcuts/recruit-stage/continuation.js +73 -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 +630 -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 +1198 -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/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/docs/e10-auth.md +7 -1
- package/docs/operation-manual.md +8 -0
- package/package.json +1 -1
- package/scripts/install-skill-to-xiaoe.mjs +105 -0
- package/scripts/package-skill.mjs +4 -2
- package/skill-template/business-info.json +24 -0
- package/skill-template/domains/recruit-stage.md +29 -0
- package/skills/weaver-e10-calendar/SKILL.md +2 -1
- package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
- package/skills/weaver-e10-cowork/SKILL.md +1 -1
- package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
- package/skills/weaver-e10-jucailin/SKILL.md +1 -1
- package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
- 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 +2 -2
- package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
- package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
- package/skills/weaver-e10-mail/SKILL.md +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +32 -2
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-recruit-stage/SKILL.md +226 -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/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 +73 -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 +438 -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 +4 -2
- package/skills/weaver-e10-shijingran/SKILL.md +3 -2
- package/skills/weaver-e10-shijingran/references/operations.md +3 -1
- package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
- package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
- 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/todo-smart-convert.md +23 -13
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +62 -20
- package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
- package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
- package/dist/chunks/{chunk-E7OK5WD5.js → chunk-2LV74IWP.js} +5 -5
|
@@ -335,45 +335,122 @@ async function uploadResumeFile(env, filePath, fieldId, formId) {
|
|
|
335
335
|
}
|
|
336
336
|
return { fileid: String(fileid), name, size };
|
|
337
337
|
}
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
338
|
+
/** 按名查标签(并行 type=0 公共 / type=1 个人)。服务端若忽略 name 会返回满页 → filtered=false,由调用方回退全量。
|
|
339
|
+
* 🔴 2026-11-04 修:原实现只取第 1 页 500 条,环境标签超 500 时后续标签永远匹配不到(静默跳过)。 */
|
|
340
|
+
async function queryTagByName(env, name) {
|
|
341
|
+
const pageSize = 100;
|
|
342
|
+
const bodyOf = (type) => ({ type, name, order: 8, pageSize, pageNo: 1, search: 1, tagGroup: 0, tagModules: ['ebuilderform'] });
|
|
343
|
+
const rs = await Promise.all([0, 1].map((type) => env.host
|
|
344
|
+
.request({
|
|
343
345
|
path: '/api/my/tag/query',
|
|
344
346
|
method: 'POST',
|
|
345
|
-
body:
|
|
347
|
+
body: bodyOf(type),
|
|
346
348
|
operation: 'ehr.recruit.candidate.import.apply',
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
+
})
|
|
350
|
+
.catch(() => null)));
|
|
351
|
+
const items = [];
|
|
352
|
+
let filtered = true;
|
|
353
|
+
for (const r of rs) {
|
|
354
|
+
const d = r?.data || {};
|
|
355
|
+
const list = Array.isArray(d.data) ? d.data : [];
|
|
356
|
+
const total = Number(d.total || 0);
|
|
357
|
+
if (list.length >= pageSize && total >= pageSize)
|
|
358
|
+
filtered = false;
|
|
359
|
+
items.push(...list);
|
|
360
|
+
}
|
|
361
|
+
return { items, filtered };
|
|
362
|
+
}
|
|
363
|
+
/** 单类型的全量标签(首轮取 total 后并发翻页,限并发 5、上限 40 页)——仅在按名查询不可用时回退 */
|
|
364
|
+
async function fetchTagsOfType(env, type) {
|
|
365
|
+
const pageSize = 100;
|
|
366
|
+
const bodyOf = (pageNo) => ({ type, name: '', order: 8, pageSize, pageNo, search: 1, tagGroup: 0, tagModules: ['ebuilderform'] });
|
|
367
|
+
const requestPage = (pageNo) => env.host
|
|
368
|
+
.request({
|
|
369
|
+
path: '/api/my/tag/query',
|
|
370
|
+
method: 'POST',
|
|
371
|
+
body: bodyOf(pageNo),
|
|
372
|
+
operation: 'ehr.recruit.candidate.import.apply',
|
|
373
|
+
})
|
|
374
|
+
.catch(() => null);
|
|
375
|
+
const first = (await requestPage(1));
|
|
376
|
+
const d = first?.data || {};
|
|
377
|
+
const rows = Array.isArray(d.data) ? [...d.data] : [];
|
|
378
|
+
const total = Number(d.total || 0);
|
|
379
|
+
const pages = Math.min(40, Math.max(1, Math.ceil(total / pageSize)));
|
|
380
|
+
if (pages > 1 && rows.length) {
|
|
381
|
+
const rest = Array.from({ length: pages - 1 }, (_, i) => i + 2);
|
|
382
|
+
for (let i = 0; i < rest.length; i += 5) {
|
|
383
|
+
const batch = rest.slice(i, i + 5);
|
|
384
|
+
const rs = await Promise.all(batch.map((p) => requestPage(p)));
|
|
385
|
+
for (const r of rs) {
|
|
386
|
+
const list = r?.data?.data;
|
|
387
|
+
if (Array.isArray(list))
|
|
388
|
+
rows.push(...list);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return rows;
|
|
393
|
+
}
|
|
394
|
+
/** 解析标签名 → id(同名优先公共),不创建标签。
|
|
395
|
+
* ① 按名精确查询(N 个名字 → 1 轮请求);② 服务端未按名过滤时才回退全量(不落盘、不缓存)。 */
|
|
396
|
+
async function resolveTagIds(env, tagNames) {
|
|
397
|
+
const norm = (s) => String(s == null ? '' : s).trim().toLowerCase();
|
|
398
|
+
const uniqNames = [...new Set(tagNames.map((n) => String(n)).filter(Boolean))];
|
|
349
399
|
const byName = new Map();
|
|
350
|
-
|
|
351
|
-
const name = String(row
|
|
352
|
-
const id = row
|
|
353
|
-
if (name
|
|
354
|
-
|
|
400
|
+
const absorb = (row) => {
|
|
401
|
+
const name = String(row?.name ?? row?.tagName ?? '').trim();
|
|
402
|
+
const id = row?.tagId ?? row?.id;
|
|
403
|
+
if (!name || id === undefined)
|
|
404
|
+
return;
|
|
405
|
+
const key = norm(name);
|
|
406
|
+
const prev = byName.get(key);
|
|
407
|
+
if (!prev)
|
|
408
|
+
byName.set(key, { id: String(id), name });
|
|
409
|
+
else if ((row?.type ?? 0) === 0)
|
|
410
|
+
byName.set(key, { id: String(id), name });
|
|
411
|
+
};
|
|
412
|
+
const probes = await Promise.all(uniqNames.map((n) => queryTagByName(env, n)));
|
|
413
|
+
const nameQueryUsable = probes.every((p) => p.filtered);
|
|
414
|
+
if (nameQueryUsable) {
|
|
415
|
+
for (const p of probes)
|
|
416
|
+
for (const item of p.items)
|
|
417
|
+
absorb(item);
|
|
355
418
|
}
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
else {
|
|
420
|
+
for (const type of [0, 1]) {
|
|
421
|
+
const rows = await fetchTagsOfType(env, type).catch(() => []);
|
|
422
|
+
for (const item of rows)
|
|
423
|
+
absorb(item);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const resolved = [];
|
|
427
|
+
const found = [];
|
|
358
428
|
const skipped = [];
|
|
359
429
|
for (const name of tagNames) {
|
|
360
|
-
const hit = byName.get(name);
|
|
430
|
+
const hit = byName.get(norm(name));
|
|
361
431
|
if (!hit) {
|
|
362
432
|
skipped.push(name);
|
|
363
433
|
continue;
|
|
364
434
|
}
|
|
365
|
-
|
|
366
|
-
|
|
435
|
+
resolved.push({ id: hit.id, name: hit.name, type: 0 });
|
|
436
|
+
found.push(name);
|
|
367
437
|
}
|
|
368
|
-
|
|
438
|
+
return { resolved, found, skipped };
|
|
439
|
+
}
|
|
440
|
+
/** 写入平台标签(解析 + batchAddEntityTag)。解析结果不存在时静默跳过,不自动创建标签。 */
|
|
441
|
+
async function applyPlatformTags(env, talentId, tagNames) {
|
|
442
|
+
if (!tagNames.length)
|
|
443
|
+
return { applied: [], skipped: [] };
|
|
444
|
+
const r = await resolveTagIds(env, tagNames);
|
|
445
|
+
if (r.resolved.length) {
|
|
369
446
|
await env.host.request({
|
|
370
447
|
path: '/api/my/tag/common/batchAddEntityTag',
|
|
371
448
|
method: 'POST',
|
|
372
|
-
body: { tags, batchDatas: [{ entityId: talentId, module: 'ebuilderform' }] },
|
|
449
|
+
body: { tags: r.resolved, batchDatas: [{ entityId: talentId, module: 'ebuilderform' }] },
|
|
373
450
|
operation: 'ehr.recruit.candidate.import.apply',
|
|
374
451
|
});
|
|
375
452
|
}
|
|
376
|
-
return { applied, skipped };
|
|
453
|
+
return { applied: r.found, skipped: r.skipped };
|
|
377
454
|
}
|
|
378
455
|
async function requireCache(env) {
|
|
379
456
|
if (!env.recruitContext)
|
|
@@ -582,7 +659,16 @@ const candidateImportPrepare = {
|
|
|
582
659
|
handler: async (args, env) => {
|
|
583
660
|
const cache = await requireCache(env);
|
|
584
661
|
const plan = await buildImportPlan(env, cache, args);
|
|
585
|
-
|
|
662
|
+
// 标签预检(只读)+ 查重并行:让用户在确认摘要里就能看到哪些标签在本环境不存在(apply 会跳过它们)
|
|
663
|
+
const tagNames = Array.isArray(args.tags) ? args.tags.map(String).filter(Boolean) : [];
|
|
664
|
+
const [dup, tagCheck] = await Promise.all([
|
|
665
|
+
checkDuplicateByMobile(env, cache, plan.mobile),
|
|
666
|
+
tagNames.length
|
|
667
|
+
? resolveTagIds(env, tagNames)
|
|
668
|
+
.then((rt) => ({ requested: tagNames, found: rt.found, missing: rt.skipped }))
|
|
669
|
+
.catch((error) => ({ requested: tagNames, missing: [], error: error?.message || '标签预检失败' }))
|
|
670
|
+
: Promise.resolve(null),
|
|
671
|
+
]);
|
|
586
672
|
if (dup.length > 1) {
|
|
587
673
|
throw ehrValidationError('duplicate_ambiguous', `手机号 ${plan.mobile} 存在多条候选(数据异常),请人工核对后再决定,不自动新建`);
|
|
588
674
|
}
|
|
@@ -621,6 +707,7 @@ const candidateImportPrepare = {
|
|
|
621
707
|
details: plan.details,
|
|
622
708
|
tags: Array.isArray(args.tags) ? args.tags : [],
|
|
623
709
|
},
|
|
710
|
+
...(tagCheck ? { tagCheck } : {}),
|
|
624
711
|
continuation,
|
|
625
712
|
expiresInSeconds: 600,
|
|
626
713
|
});
|
|
@@ -684,43 +771,31 @@ const candidateImportApply = {
|
|
|
684
771
|
throw error;
|
|
685
772
|
throw ehrPartialError(`简历建档写入结果不确定(${error?.message || '未知错误'}):请先用人才库搜索手机号 ${payload.telephone} 核对是否已建档,禁止直接重试`, { candidateName: payload.candidateName, telephone: payload.telephone });
|
|
686
773
|
}
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
catch {
|
|
714
|
-
labelMatch = 'FAILED(不影响建档,可稍后在人才卡片手动触发)';
|
|
715
|
-
}
|
|
716
|
-
// ⑤ 平台标签(按名命中清单写入,不存在则跳过)
|
|
717
|
-
let tagResult = { applied: [], skipped: [] };
|
|
718
|
-
try {
|
|
719
|
-
tagResult = await applyPlatformTags(env, talentId, Array.isArray(payload.tags) ? payload.tags.map(String) : []);
|
|
720
|
-
}
|
|
721
|
-
catch (error) {
|
|
722
|
-
tagResult = { applied: [], skipped: Array.isArray(payload.tags) ? payload.tags.map(String) : [] };
|
|
723
|
-
}
|
|
774
|
+
// ③④⑤ 初始化批次 / 触发电商后台标签规则 / 平台标签:三者只依赖 talentId,并行执行(各自 catch 等价原语义)
|
|
775
|
+
const batchTask = env.host
|
|
776
|
+
.request({
|
|
777
|
+
path: `/api/recruit/candidate/initTalentBatch?formDataId=${encodeURIComponent(talentId)}`,
|
|
778
|
+
method: 'POST',
|
|
779
|
+
operation: 'ehr.recruit.candidate.import.apply',
|
|
780
|
+
})
|
|
781
|
+
.then(() => ({ ok: true, note: undefined }))
|
|
782
|
+
.catch((error) => ({
|
|
783
|
+
ok: false,
|
|
784
|
+
note: `人才已建档,但初始化人才批次失败:${error?.message || '未知错误'}(可在人才卡片手动处理)`,
|
|
785
|
+
}));
|
|
786
|
+
const labelTask = env.host
|
|
787
|
+
.request({
|
|
788
|
+
path: `/api/recruit/label/matchTalentLabel?talentId=${encodeURIComponent(talentId)}`,
|
|
789
|
+
method: 'POST',
|
|
790
|
+
body: {},
|
|
791
|
+
operation: 'ehr.recruit.candidate.import.apply',
|
|
792
|
+
})
|
|
793
|
+
.then((r) => (r?.data === undefined ? 'TRIGGERED' : String(r.data)))
|
|
794
|
+
.catch(() => 'FAILED(不影响建档,可稍后在人才卡片手动触发)');
|
|
795
|
+
const tagTask = applyPlatformTags(env, talentId, Array.isArray(payload.tags) ? payload.tags.map(String) : []).catch(() => ({ applied: [], skipped: Array.isArray(payload.tags) ? payload.tags.map(String) : [] }));
|
|
796
|
+
const [batchOutcome, labelMatch, tagResult] = await Promise.all([batchTask, labelTask, tagTask]);
|
|
797
|
+
const batchOk = batchOutcome.ok;
|
|
798
|
+
const batchNote = batchOutcome.note;
|
|
724
799
|
// ⑥ 产物链接:人才卡片单页 + 人才库列表定位(自用系统追加 recruitType=useSelf)
|
|
725
800
|
const baseUrl = session.baseUrl.replace(/\/+$/, '');
|
|
726
801
|
const extra = String(cache.selfUse) === '1' ? '&recruitType=useSelf' : '';
|
|
@@ -301,21 +301,28 @@ async function resolveCandidateForWrite(host, cache, talentId) {
|
|
|
301
301
|
throw ehrValidationError('recruit_batch_id_missing', '未取到 batchId(prepareQuery 返回异常),无法定位候选人应聘批次');
|
|
302
302
|
}
|
|
303
303
|
const v = randomHex6();
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
304
|
+
// otherInfo 必成功(保持原语义);baseInfo 并行取(姓名兜底 + AI 面试条件必填的邮箱/手机),失败不阻断
|
|
305
|
+
const [otherResponse, baseResponse] = await Promise.all([
|
|
306
|
+
host.request({
|
|
307
|
+
path: `/api/recruit/candidate/getCandidateCardOtherInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_ab=ab`,
|
|
308
|
+
headers: extra,
|
|
309
|
+
}),
|
|
310
|
+
host
|
|
311
|
+
.request({
|
|
312
312
|
path: `/api/recruit/candidate/getCandidateCardBaseInfo?talentId=${talentId}&batchId=${batchId}&v=${v}&randomkey_cd=cd`,
|
|
313
313
|
headers: extra,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
})
|
|
315
|
+
.catch(() => null),
|
|
316
|
+
]);
|
|
317
|
+
const batch = otherResponse?.data?.currentCandidateBatch || {};
|
|
318
|
+
const talentBase = baseResponse?.data?.talent || {};
|
|
319
|
+
const name = batch?.talent?.name || talentBase?.hrm?.name || talentBase?.name || '';
|
|
320
|
+
const email = String(talentBase.email || talentBase.hrm?.email || '').trim();
|
|
321
|
+
const mobile = String(talentBase.telephone || talentBase.mobile || '').trim();
|
|
317
322
|
return {
|
|
318
323
|
name,
|
|
324
|
+
email,
|
|
325
|
+
mobile,
|
|
319
326
|
candidateManageId: batch?.id || batch?.candidateManageId || null,
|
|
320
327
|
positionId: batch?.position?.id || null,
|
|
321
328
|
positionName: batch?.position?.name || null,
|
|
@@ -535,6 +542,8 @@ const candidateCreateApply = {
|
|
|
535
542
|
// 安排面试 operation
|
|
536
543
|
// ---------------------------------------------------------------------------
|
|
537
544
|
const INTERVIEW_TYPES = ['现场面试', '视频面试', '电话面试', 'AI面试'];
|
|
545
|
+
/** AI 面试方案默认值(用户口径:默认「大模型面试」)。若环境无此方案,运行时列出候选让用户选,绝不自动取第一条。 */
|
|
546
|
+
const AI_DEFAULT_PLAN_NAME = '大模型面试';
|
|
538
547
|
const INTERVIEW_DATAKEYS = ['subject', 'start_time', 'end_time', 'interview_type', 'interview_session', 'interviewer',
|
|
539
548
|
'position_manage_id', 'talent_id', 'candidate_manage_id', 'mobile', 'email', 'interview_location', 'notice_mould'];
|
|
540
549
|
function defaultInterviewWindow() {
|
|
@@ -544,16 +553,181 @@ function defaultInterviewWindow() {
|
|
|
544
553
|
const end = new Date(now.getTime() + 60 * 60 * 1000);
|
|
545
554
|
return { start: fmt(now), end: fmt(end) };
|
|
546
555
|
}
|
|
556
|
+
/** 当前登录人(teamsCheck → currentUser.id/username):面试官未指定时的默认值。
|
|
557
|
+
* teamsCheck 响应无 status 字段、不被 isBusinessSuccess 认可,故直连 fetch 绕开业务判定(同 recruit.ts 口径)。 */
|
|
558
|
+
async function resolveCurrentUserForWrite(host) {
|
|
559
|
+
try {
|
|
560
|
+
const session = await host.getSession();
|
|
561
|
+
const baseUrl = String(session?.baseUrl || '');
|
|
562
|
+
if (!baseUrl)
|
|
563
|
+
return null;
|
|
564
|
+
const resp = await fetch(`${baseUrl}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(baseUrl)}`, {
|
|
565
|
+
method: 'POST',
|
|
566
|
+
headers: {
|
|
567
|
+
accept: 'application/json',
|
|
568
|
+
'content-type': 'application/json',
|
|
569
|
+
eteamsid: String(session?.eteamsId || ''),
|
|
570
|
+
cookie: typeof session?.cookieHeader === 'function' ? session.cookieHeader() : '',
|
|
571
|
+
'user-agent': typeof session?.uaHeader === 'function' ? session.uaHeader() : 'weaver-work-cli',
|
|
572
|
+
origin: baseUrl,
|
|
573
|
+
},
|
|
574
|
+
redirect: 'manual',
|
|
575
|
+
});
|
|
576
|
+
const data = await resp.json().catch(() => ({}));
|
|
577
|
+
const cu = data?.currentUser ?? data?.data?.currentUser ?? null;
|
|
578
|
+
if (!cu?.id)
|
|
579
|
+
return null;
|
|
580
|
+
return { id: String(cu.id), name: String(cu.username || cu.id) };
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/** 取表单布局 id(GET 在低基线返 405 → 回退 POST),用于 AI 面试方案的关联字段候选 */
|
|
587
|
+
async function fetchLayoutIdForForm(host, formId) {
|
|
588
|
+
try {
|
|
589
|
+
const r = (await host.request({
|
|
590
|
+
path: `/api/ebuilder/form/form/core/getFormLayout?formId=${encodeURIComponent(formId)}`,
|
|
591
|
+
method: 'GET',
|
|
592
|
+
}));
|
|
593
|
+
const id = r?.data?.formLayout?.id;
|
|
594
|
+
if (id)
|
|
595
|
+
return String(id);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
// 低基线 GET 405 → POST
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const r = (await host.request({
|
|
602
|
+
path: '/api/ebuilder/form/form/core/getFormLayout',
|
|
603
|
+
method: 'POST',
|
|
604
|
+
body: { module: 'ebuilderform', formId, terminalType: 'PC' },
|
|
605
|
+
}));
|
|
606
|
+
const id = r?.data?.formLayout?.id;
|
|
607
|
+
if (id)
|
|
608
|
+
return String(id);
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
// 布局取不到时回退 formId(页面保存本身也回退 formId)
|
|
612
|
+
}
|
|
613
|
+
return formId;
|
|
614
|
+
}
|
|
615
|
+
const DIGITAL_PLAN_NAME_KEYS = ['msfa_name', 'digital_name', 'plan_name', 'scheme_name'];
|
|
616
|
+
/** AI 面试方案(面试数字人)候选:① 关联字段候选接口 → ② 「面试数字人配置」列表兜底。
|
|
617
|
+
* 🔴 只返回候选,绝不自动取第一条(方案选错=面试由错误的数字人执行)。 */
|
|
618
|
+
async function listDigitalPlansForWrite(host, cache, formId, layoutId, fieldId) {
|
|
619
|
+
try {
|
|
620
|
+
const r = (await host.request({
|
|
621
|
+
path: '/api/ebuilder/form/common/browser/complete/ebuilder',
|
|
622
|
+
body: {
|
|
623
|
+
browserMultiple: false,
|
|
624
|
+
newly: 1,
|
|
625
|
+
searchValue: '',
|
|
626
|
+
formParam: JSON.stringify({ formId, layoutId, fieldId, filterItems: [], module: 'ebuilderform', dataDetails: [] }),
|
|
627
|
+
openScroll: true,
|
|
628
|
+
controlId: fieldId,
|
|
629
|
+
businessId: formId,
|
|
630
|
+
variableMap: {},
|
|
631
|
+
},
|
|
632
|
+
}));
|
|
633
|
+
if (r?.status !== false && r?.data) {
|
|
634
|
+
const list = r.data.data || [];
|
|
635
|
+
const items = list
|
|
636
|
+
.filter((x) => x && x.id)
|
|
637
|
+
.map((x) => ({ id: String(x.id), name: String(x.name || '').trim() }));
|
|
638
|
+
if (items.length)
|
|
639
|
+
return { available: true, items };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
// 回退列表
|
|
644
|
+
}
|
|
645
|
+
const menus = (cache.lists?.all_search || []);
|
|
646
|
+
const menu = menus.find((m) => (m.menuName || '').includes('面试数字人'))
|
|
647
|
+
|| menus.find((m) => (m.menuName || '').includes('数字人配置'));
|
|
648
|
+
if (!menu?.listId) {
|
|
649
|
+
return { available: false, items: [], reason: '关联字段候选接口不可用,且当前环境未配置「面试数字人配置」列表菜单' };
|
|
650
|
+
}
|
|
651
|
+
let objId = '';
|
|
652
|
+
try {
|
|
653
|
+
const r = (await host.request({
|
|
654
|
+
path: `/api/ebuilder/form/obj/getObjsByTag?appId=${encodeURIComponent(String(cache.appId || ''))}&tag=uf_rcrt_digital_human`,
|
|
655
|
+
method: 'GET',
|
|
656
|
+
}));
|
|
657
|
+
const d = r?.data;
|
|
658
|
+
if (Array.isArray(d) && d.length)
|
|
659
|
+
objId = String(d[0]?.id || '');
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// 取不到对象 id
|
|
663
|
+
}
|
|
664
|
+
if (!objId)
|
|
665
|
+
return { available: false, items: [], reason: '未能解析「面试数字人配置」对象 id' };
|
|
666
|
+
const rows = [];
|
|
667
|
+
for (let cur = 1; cur <= 20; cur++) {
|
|
668
|
+
const r = (await host.request({
|
|
669
|
+
path: `/api/ebuilder/form/list/getListDatas?passid=${objId}&cid=${objId}`,
|
|
670
|
+
body: {
|
|
671
|
+
current: cur,
|
|
672
|
+
pageSize: 100,
|
|
673
|
+
tableuid: randomHex32(),
|
|
674
|
+
listId: menu.listId,
|
|
675
|
+
searchParamData: { searchType: '3', type: 'and' },
|
|
676
|
+
terminal: 'PC',
|
|
677
|
+
browser: 'Chrome',
|
|
678
|
+
requiredMatch: 1,
|
|
679
|
+
mapResult: 1,
|
|
680
|
+
recheckQuery: 1,
|
|
681
|
+
isFollow: 0,
|
|
682
|
+
isShareToMe: 0,
|
|
683
|
+
filterCondition: {},
|
|
684
|
+
cusMenuId: menu.cusMenuId,
|
|
685
|
+
urlPageTitle: menu.menuName,
|
|
686
|
+
menuName: menu.menuName,
|
|
687
|
+
},
|
|
688
|
+
headers: { ebBusinessId: String(menu.listId) },
|
|
689
|
+
}));
|
|
690
|
+
const page = r?.data?.data || [];
|
|
691
|
+
rows.push(...page);
|
|
692
|
+
if (page.length < 100)
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
const items = rows
|
|
696
|
+
.map((x) => {
|
|
697
|
+
const nk = DIGITAL_PLAN_NAME_KEYS.find((k) => String(x[k] || '').trim());
|
|
698
|
+
return { id: String(x.form_data_id || x.id || ''), name: String(nk ? x[nk] : '').trim() };
|
|
699
|
+
})
|
|
700
|
+
.filter((x) => x.id);
|
|
701
|
+
if (!items.length)
|
|
702
|
+
return { available: false, items: [], reason: '「面试数字人配置」暂无可选方案' };
|
|
703
|
+
return { available: true, items };
|
|
704
|
+
}
|
|
705
|
+
/** 方案匹配:id 全等 → 名称全等 → 名称包含(唯一才算命中,否则回歧义列表) */
|
|
706
|
+
function pickDigitalPlanForWrite(items, key) {
|
|
707
|
+
const k = String(key || '').trim();
|
|
708
|
+
if (!k)
|
|
709
|
+
return { plan: null, ambiguous: [] };
|
|
710
|
+
const byId = items.filter((x) => x.id === k);
|
|
711
|
+
if (byId.length === 1)
|
|
712
|
+
return { plan: byId[0], ambiguous: [] };
|
|
713
|
+
const byExact = items.filter((x) => x.name === k);
|
|
714
|
+
if (byExact.length === 1)
|
|
715
|
+
return { plan: byExact[0], ambiguous: [] };
|
|
716
|
+
const byLike = items.filter((x) => x.name.includes(k));
|
|
717
|
+
if (byLike.length === 1)
|
|
718
|
+
return { plan: byLike[0], ambiguous: [] };
|
|
719
|
+
return { plan: null, ambiguous: byLike };
|
|
720
|
+
}
|
|
547
721
|
async function buildInterviewPlan(env, cache, args) {
|
|
548
722
|
const host = env.host;
|
|
549
723
|
const talentId = requiredString(args, 'talentId', 'talent_id_required', '人才 ID');
|
|
550
724
|
if (!/^\d+$/.test(talentId))
|
|
551
725
|
throw ehrValidationError('talent_id_invalid', 'talentId 必须是数字 ID');
|
|
552
|
-
const interviewer = requiredString(args, 'interviewer', 'interviewer_required', '面试官姓名');
|
|
553
726
|
const type = typeof args.type === 'string' && args.type.trim() ? args.type.trim() : '现场面试';
|
|
554
727
|
if (!INTERVIEW_TYPES.includes(type)) {
|
|
555
728
|
throw ehrValidationError('enum_invalid', `type(面试方式)取值必须是 ${INTERVIEW_TYPES.join(' / ')} 之一`);
|
|
556
729
|
}
|
|
730
|
+
const isAI = type === 'AI面试';
|
|
557
731
|
const session = typeof args.session === 'string' && args.session.trim() ? args.session.trim() : '初试';
|
|
558
732
|
const candidate = await resolveCandidateForWrite(host, cache, talentId);
|
|
559
733
|
const positionId = args.positionId ? String(args.positionId) : candidate.positionId;
|
|
@@ -582,9 +756,63 @@ async function buildInterviewPlan(env, cache, args) {
|
|
|
582
756
|
if (!sessionOptionId) {
|
|
583
757
|
throw ehrValidationError('enum_invalid', `面试轮次「${session}」不在枚举中(${Object.keys(sessionMap).join(' / ')})`);
|
|
584
758
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
759
|
+
// 面试官:显式指定 → 按名解析;未指定 → 默认当前登录人(少问一次,与页面待办默认口径一致)
|
|
760
|
+
const interviewerInput = typeof args.interviewer === 'string' ? args.interviewer.trim() : '';
|
|
761
|
+
let interviewerId = null;
|
|
762
|
+
let interviewer = interviewerInput;
|
|
763
|
+
if (interviewerInput) {
|
|
764
|
+
interviewerId = await resolvePersonId(host, fieldIdOf('interviewer'), interviewerInput);
|
|
765
|
+
if (!interviewerId) {
|
|
766
|
+
throw ehrValidationError('interviewer_not_found', `面试官「${interviewerInput}」解析不到 userId(browser/complete/resource 无匹配)`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
const me = await resolveCurrentUserForWrite(host);
|
|
771
|
+
if (!me) {
|
|
772
|
+
throw ehrValidationError('interviewer_unresolved', '未指定面试官,且无法解析当前登录人(teamsCheck 失败);请用 interviewer 参数指定');
|
|
773
|
+
}
|
|
774
|
+
// 🔴 teamsCheck 的 currentUser.id 与人员字段 optionId 语义未必相同(实测两者 id 不同),
|
|
775
|
+
// 故用与显式指定完全相同的解析通道(browser/complete/resource)把姓名换成 userId。
|
|
776
|
+
interviewerId = await resolvePersonId(host, fieldIdOf('interviewer'), me.name);
|
|
777
|
+
if (!interviewerId) {
|
|
778
|
+
throw ehrValidationError('interviewer_unresolved', `未指定面试官,当前登录人「${me.name}」在人员字段中解析不到;请用 interviewer 参数指定`);
|
|
779
|
+
}
|
|
780
|
+
interviewer = me.name;
|
|
781
|
+
}
|
|
782
|
+
// AI 面试:方案(digital_id)与邮箱都是条件必填,不填保存必失败
|
|
783
|
+
let aiPlan = null;
|
|
784
|
+
let aiPlans = [];
|
|
785
|
+
let email = String(args.email || '').trim() || candidate.email;
|
|
786
|
+
const mobile = candidate.mobile;
|
|
787
|
+
if (isAI) {
|
|
788
|
+
const digitalFid = fieldIdOf('digital_id');
|
|
789
|
+
if (!digitalFid) {
|
|
790
|
+
throw ehrValidationError('recruit_field_missing', '面试计划表单缺少「AI面试方案」字段(digital_id),无法安排 AI 面试');
|
|
791
|
+
}
|
|
792
|
+
const layoutId = await fetchLayoutIdForForm(host, formId);
|
|
793
|
+
const plans = await listDigitalPlansForWrite(host, cache, formId, layoutId, digitalFid);
|
|
794
|
+
aiPlans = plans.items;
|
|
795
|
+
if (!plans.available) {
|
|
796
|
+
throw ehrValidationError('ai_plan_unavailable', `AI 面试必须选择「AI面试方案」,但${plans.reason || '暂无可选方案'};请先在该环境维护「面试数字人配置」`);
|
|
797
|
+
}
|
|
798
|
+
// 默认「大模型面试」(用户口径);环境无此方案时列出候选,绝不自动取第一条
|
|
799
|
+
const wanted = typeof args.aiPlan === 'string' && args.aiPlan.trim() ? args.aiPlan.trim() : AI_DEFAULT_PLAN_NAME;
|
|
800
|
+
const picked = pickDigitalPlanForWrite(aiPlans, wanted);
|
|
801
|
+
if (picked.plan) {
|
|
802
|
+
aiPlan = picked.plan;
|
|
803
|
+
}
|
|
804
|
+
else if (picked.ambiguous.length > 1) {
|
|
805
|
+
throw ehrValidationError('ai_plan_ambiguous', `AI面试方案「${wanted}」命中多个:${picked.ambiguous.map((x) => x.name).join('、')},请用 aiPlan 指明具体方案`, { candidates: picked.ambiguous });
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
throw ehrValidationError('ai_plan_not_found', `未找到 AI面试方案「${wanted}」,请用 aiPlan 从候选中指定(候选:${aiPlans.map((x) => x.name).join('、')})`, { candidates: aiPlans });
|
|
809
|
+
}
|
|
810
|
+
if (!email) {
|
|
811
|
+
throw ehrValidationError('email_required', 'AI 面试要求填写候选人邮箱:人才资料里没有邮箱,请用 email 参数提供');
|
|
812
|
+
}
|
|
813
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
814
|
+
throw ehrValidationError('email_invalid', `候选人邮箱格式不正确:${email}`);
|
|
815
|
+
}
|
|
588
816
|
}
|
|
589
817
|
const window = defaultInterviewWindow();
|
|
590
818
|
const start = typeof args.startTime === 'string' && args.startTime.trim() ? args.startTime.trim() : window.start;
|
|
@@ -603,6 +831,9 @@ async function buildInterviewPlan(env, cache, args) {
|
|
|
603
831
|
{ formField: { id: fieldIdOf('position_manage_id') }, dataOptions: [{ optionId: positionId, content: candidate.positionName }] },
|
|
604
832
|
{ formField: { id: fieldIdOf('talent_id') }, dataOptions: [{ optionId: talentId, content: candidate.name }] },
|
|
605
833
|
{ formField: { id: fieldIdOf('candidate_manage_id') }, dataOptions: [{ optionId: candidate.candidateManageId, content: candidate.candidateManageId }] },
|
|
834
|
+
...(mobile ? [{ formField: { id: fieldIdOf('mobile') }, content: mobile }] : []),
|
|
835
|
+
...(email ? [{ formField: { id: fieldIdOf('email') }, content: email }] : []),
|
|
836
|
+
...(aiPlan ? [{ formField: { id: fieldIdOf('digital_id') }, dataOptions: [{ optionId: aiPlan.id, content: aiPlan.name }] }] : []),
|
|
606
837
|
];
|
|
607
838
|
return {
|
|
608
839
|
formId, objId, dataDetails,
|
|
@@ -610,12 +841,19 @@ async function buildInterviewPlan(env, cache, args) {
|
|
|
610
841
|
positionName: candidate.positionName,
|
|
611
842
|
interviewTime: `${start} ~ ${end}`,
|
|
612
843
|
interviewer,
|
|
844
|
+
email: email || undefined,
|
|
845
|
+
mobile: mobile || undefined,
|
|
846
|
+
isAIInterview: isAI,
|
|
847
|
+
aiPlan,
|
|
848
|
+
aiPlans: isAI ? aiPlans : undefined,
|
|
613
849
|
};
|
|
614
850
|
}
|
|
615
851
|
const interviewArrangeSchema = {
|
|
616
852
|
talentId: { type: 'string', pattern: '^\\d+$', description: '人才 ID(candidate.search 返回的 talentId)' },
|
|
617
|
-
interviewer: { type: 'string', description: '
|
|
853
|
+
interviewer: { type: 'string', description: '面试官姓名(可选;不填默认当前登录人)' },
|
|
618
854
|
type: { type: 'string', enum: [...INTERVIEW_TYPES], description: '面试方式,默认现场面试' },
|
|
855
|
+
aiPlan: { type: 'string', description: 'AI 面试方案(方案名或 id;type=AI面试 时生效,默认「大模型面试」)' },
|
|
856
|
+
email: { type: 'string', description: '候选人邮箱(AI 面试必填;不填则取人才资料里的邮箱)' },
|
|
619
857
|
session: { type: 'string', description: '面试轮次(如 初试/复试),默认初试;须在枚举内' },
|
|
620
858
|
positionId: { type: 'string', description: '应聘职位 ID;不填时从人才当前应聘批次继承' },
|
|
621
859
|
startTime: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$', description: '开始时间 YYYY-MM-DD HH:mm,默认当前时间' },
|
|
@@ -631,7 +869,7 @@ const interviewArrangePrepare = {
|
|
|
631
869
|
capability: 'recruit',
|
|
632
870
|
risk: WRITE,
|
|
633
871
|
requiresConfirmation: true,
|
|
634
|
-
inputSchema: schemaOf({ ...interviewArrangeSchema }, { required: ['talentId'
|
|
872
|
+
inputSchema: schemaOf({ ...interviewArrangeSchema }, { required: ['talentId'] }),
|
|
635
873
|
handler: async (args, env) => {
|
|
636
874
|
const cache = await requireRecruitCache(env);
|
|
637
875
|
const plan = await buildInterviewPlan(env, cache, args);
|
|
@@ -656,6 +894,13 @@ const interviewArrangePrepare = {
|
|
|
656
894
|
session: args.session || '初试',
|
|
657
895
|
time: plan.interviewTime,
|
|
658
896
|
interviewer: plan.interviewer,
|
|
897
|
+
...(plan.isAIInterview
|
|
898
|
+
? {
|
|
899
|
+
aiPlan: plan.aiPlan ? `${plan.aiPlan.name}(id=${plan.aiPlan.id})` : '(未指定)',
|
|
900
|
+
email: plan.email || '(缺失)',
|
|
901
|
+
mobile: plan.mobile || '(人才资料无手机号)',
|
|
902
|
+
}
|
|
903
|
+
: {}),
|
|
659
904
|
notify: notify ? '将触发「新建日程」面试通知动作流(外部通知)' : '面试通知默认不触发',
|
|
660
905
|
},
|
|
661
906
|
willSubmit: { formId: plan.formId, objId: plan.objId, dataDetails: plan.dataDetails },
|
|
@@ -15,6 +15,8 @@ import { compact, normalizePaged, toNumber, toPageQuery, toPositiveInt } from '.
|
|
|
15
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
const RECRUIT_APP_TAG = 'weaver-recruit-service';
|
|
17
17
|
/** 逻辑名 -> objTag(权威映射,与 _init_env_cache.mjs 一致) */
|
|
18
|
+
/** 环境缓存结构版本:新增/变更解析字段(如 2026-11-04 补 candidate 应聘者列表)时 +1,旧缓存自动失效重建。 */
|
|
19
|
+
const RECRUIT_CACHE_VERSION = 1;
|
|
18
20
|
const RECRUIT_TAGS = {
|
|
19
21
|
talent_pool: 'uf_rcrt_talent_pool',
|
|
20
22
|
interview_plan: 'uf_rcrt_interview_plan',
|
|
@@ -38,7 +40,8 @@ const LIST_KEYWORDS = {
|
|
|
38
40
|
position: ['招聘职位', '职位列表', '职位管理'],
|
|
39
41
|
entry: ['入职管理'],
|
|
40
42
|
offer: ['录用管理', 'offer管理', 'offer'],
|
|
41
|
-
interview: ['面试管理'],
|
|
43
|
+
interview: ['面试管理', '面试安排列表', '面试安排'],
|
|
44
|
+
candidate: ['应聘者', '候选人', '应聘批次', '应聘管理'],
|
|
42
45
|
biz_config: ['业务配置映射表'],
|
|
43
46
|
};
|
|
44
47
|
/** 列表类型 -> 缓存键 + 表单逻辑名(candidate.list 四列表映射,同 recruit-list.mjs TYPES) */
|
|
@@ -47,6 +50,8 @@ export const LIST_TYPES = {
|
|
|
47
50
|
position: { listKey: 'position', formType: 'position_manage', title: '招聘职位' },
|
|
48
51
|
entry: { listKey: 'entry', formType: 'entry_manage', title: '入职管理' },
|
|
49
52
|
offer: { listKey: 'offer', formType: 'offer', title: '录用管理' },
|
|
53
|
+
interview: { listKey: 'interview', formType: 'interview_plan', title: '面试管理' },
|
|
54
|
+
candidate: { listKey: 'candidate', formType: 'candidate_manage', title: '应聘者' },
|
|
50
55
|
};
|
|
51
56
|
/** 剔除与展示无关的内部键(同 recruit-list.mjs) */
|
|
52
57
|
const INTERNAL_KEYS = new Set([
|
|
@@ -336,7 +341,7 @@ async function resolveRecruitEnv(host, appId) {
|
|
|
336
341
|
}
|
|
337
342
|
const lists = {
|
|
338
343
|
talent: null, demand: null, position: null, entry: null, offer: null,
|
|
339
|
-
interview: null, dict: dictMenu, biz_config: bizConfigMenu,
|
|
344
|
+
interview: null, candidate: null, dict: dictMenu, biz_config: bizConfigMenu,
|
|
340
345
|
all_search: searchMenus, pages,
|
|
341
346
|
};
|
|
342
347
|
for (const [biz, keywords] of Object.entries(LIST_KEYWORDS)) {
|
|
@@ -384,7 +389,7 @@ export function createRecruitContextLoader() {
|
|
|
384
389
|
}
|
|
385
390
|
}
|
|
386
391
|
catch { /* 缓存读取失败回退闸门链路 */ }
|
|
387
|
-
if (diskCache) {
|
|
392
|
+
if (diskCache && diskCache.cacheVersion === RECRUIT_CACHE_VERSION) {
|
|
388
393
|
memo.set(memoKey, diskCache);
|
|
389
394
|
return diskCache;
|
|
390
395
|
}
|
|
@@ -409,6 +414,7 @@ export function createRecruitContextLoader() {
|
|
|
409
414
|
note: 'recruit-ops 环境参数缓存(weaver-work-cli 运行时解析产物,不含认证字段)',
|
|
410
415
|
},
|
|
411
416
|
envKey: `${dir.split(/[\\/]/).pop()}|${appId}`,
|
|
417
|
+
cacheVersion: RECRUIT_CACHE_VERSION,
|
|
412
418
|
...resolved,
|
|
413
419
|
}, null, 2), 'utf8');
|
|
414
420
|
}
|
|
@@ -627,7 +633,7 @@ const candidateList = {
|
|
|
627
633
|
capability: 'recruit',
|
|
628
634
|
risk: READ,
|
|
629
635
|
inputSchema: schemaOf({
|
|
630
|
-
type: { type: 'string', enum: ['demand', 'position', 'entry', 'offer'], description: '列表类型' },
|
|
636
|
+
type: { type: 'string', enum: ['demand', 'position', 'entry', 'offer', 'interview', 'candidate'], description: '列表类型' },
|
|
631
637
|
current: SCHEMA_ID,
|
|
632
638
|
pageSize: SCHEMA_ID,
|
|
633
639
|
}, { required: ['type'] }),
|