weaver-work-cli 0.1.39 → 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.
Files changed (197) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-ENZYBBME.js → archive-J4I6MCKF.js} +2 -2
  3. package/dist/chunks/{archive-oa-YWLEC5V4.js → archive-oa-BRZKKTO4.js} +2 -2
  4. package/dist/chunks/{asset-OOV6DWPA.js → asset-L7D4FAOZ.js} +2 -2
  5. package/dist/chunks/{asset-idle-RE632MY5.js → asset-idle-DADLLV75.js} +2 -2
  6. package/dist/chunks/{attachment-content-RWN2FCLL.js → attachment-content-K4C7VLWQ.js} +3 -3
  7. package/dist/chunks/{blog-PEPOJAL7.js → blog-BVGQ7NZL.js} +2 -2
  8. package/dist/chunks/{caizhilian-WYIQDVPJ.js → caizhilian-SAADNFD6.js} +2 -2
  9. package/dist/chunks/{calendar-CIARBAOX.js → calendar-Q7NQY5Z2.js} +61 -23
  10. package/dist/chunks/{chunk-F2EQXUEE.js → chunk-3KTFS4IA.js} +397 -159
  11. package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
  12. package/dist/chunks/{chunk-HJYVHXUQ.js → chunk-4RAE6JXP.js} +1 -1
  13. package/dist/chunks/{chunk-CQHGNCK2.js → chunk-72CKAUAG.js} +41 -8
  14. package/dist/chunks/{chunk-K3RUEEOL.js → chunk-APVFJUQH.js} +2 -2
  15. package/dist/chunks/{chunk-BJGXMFGD.js → chunk-BBKDXQEC.js} +2 -2
  16. package/dist/chunks/chunk-BIPYLLVS.js +501 -0
  17. package/dist/chunks/{chunk-GVPJZXJ2.js → chunk-EKWAQSLU.js} +92 -50
  18. package/dist/chunks/{chunk-XXJOSZTQ.js → chunk-FOAA27U5.js} +381 -57
  19. package/dist/chunks/{chunk-I2HB737U.js → chunk-G4UR6NXN.js} +2 -2
  20. package/dist/chunks/{chunk-YS5DPN4T.js → chunk-GEWYMQME.js} +1 -1
  21. package/dist/chunks/{chunk-P2DAISYW.js → chunk-GHXCDPTM.js} +1 -1
  22. package/dist/chunks/{chunk-L4N7B7ZD.js → chunk-IZ7S7QUS.js} +4 -4
  23. package/dist/chunks/{chunk-NLK2RZKG.js → chunk-JPRO6ER2.js} +122 -2214
  24. package/dist/chunks/{chunk-36DIFZAH.js → chunk-LADPYOWC.js} +2 -2
  25. package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
  26. package/dist/chunks/chunk-NKZRQXST.js +2546 -0
  27. package/dist/chunks/{chunk-EINRJHQ3.js → chunk-QKNKR4RH.js} +3 -3
  28. package/dist/chunks/{chunk-T7JDJJ7H.js → chunk-R4CHUSKC.js} +126 -19
  29. package/dist/chunks/{chunk-EZGFHJFW.js → chunk-SA6VBIHY.js} +88 -15
  30. package/dist/chunks/{chunk-ZXRNHZTE.js → chunk-TTPJJJC4.js} +2 -2
  31. package/dist/chunks/{chunk-KTCB5R5H.js → chunk-W7BEHOSR.js} +2 -2
  32. package/dist/chunks/{chunk-MUQUHKPG.js → chunk-YRSZQUD2.js} +2 -2
  33. package/dist/chunks/{conflict-DRMDOL56.js → conflict-F2OMZRTO.js} +2 -2
  34. package/dist/chunks/{contract-73MERZWN.js → contract-5BETHRSG.js} +2 -2
  35. package/dist/chunks/{cowork-HNGBPD7A.js → cowork-3MN6EQQO.js} +27 -10
  36. package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
  37. package/dist/chunks/{ebuilder-form-OY6DVNRK.js → ebuilder-form-SBHOYBUD.js} +3 -3
  38. package/dist/chunks/{ehr-RAQNEQO7.js → ehr-KMOJULKG.js} +432 -1234
  39. package/dist/chunks/env-LB7NBL2O.js +19 -0
  40. package/dist/chunks/{esb-6XV7C7YV.js → esb-W2PJCFCX.js} +4 -4
  41. package/dist/chunks/factory-6ZFQXQ46.js +30 -0
  42. package/dist/chunks/{fna-XTSRB7G7.js → fna-ZQPCUV4R.js} +2 -2
  43. package/dist/chunks/group-chat-2HHAJN46.js +20 -0
  44. package/dist/chunks/{host-YMBXWFWX.js → host-4CBHGOHT.js} +3 -3
  45. package/dist/chunks/{host-A7J6GDUP.js → host-ATKVNZAT.js} +3 -3
  46. package/dist/chunks/{host-3F5UM5GN.js → host-BQH22NTQ.js} +3 -3
  47. package/dist/chunks/{host-YAQAPYNP.js → host-X2CSLFM6.js} +3 -3
  48. package/dist/chunks/{host-VGPVP7YF.js → host-YTODKMSL.js} +3 -3
  49. package/dist/chunks/{hrm-TLHKC545.js → hrm-FFM4UJFS.js} +4 -4
  50. package/dist/chunks/{im-YOUCFMUK.js → im-VEHKDQPB.js} +8 -8
  51. package/dist/chunks/{invoice-WNBS2B4C.js → invoice-LVIKSIIF.js} +3 -3
  52. package/dist/chunks/{jiuchuanhui-XABWS5RN.js → jiuchuanhui-X6DINO5P.js} +2 -2
  53. package/dist/chunks/{lazy-dispatch-NLZ5UQQI.js → lazy-dispatch-KSWTN5L6.js} +4 -4
  54. package/dist/chunks/{mail-LRYMBFNL.js → mail-VDKRGUD3.js} +9 -4
  55. package/dist/chunks/{meeting-ESXL6MFI.js → meeting-CLC6HP3Z.js} +12 -10
  56. package/dist/chunks/{okr-EOV65SSF.js → okr-CHG4OO3U.js} +2 -2
  57. package/dist/chunks/{plan-YFPVKW6Y.js → plan-XWR75667.js} +2 -2
  58. package/dist/chunks/{project-MDVZUYQW.js → project-5BXOOKMG.js} +40 -23
  59. package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
  60. package/dist/chunks/{registry-VT5Y2HLA.js → registry-7FFOXIAW.js} +4 -4
  61. package/dist/chunks/{registry-QCLZEDAP.js → registry-BKBZPBAJ.js} +10 -8
  62. package/dist/chunks/{registry-YX4CSEMD.js → registry-RHMW6ANW.js} +2 -2
  63. package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
  64. package/dist/chunks/{room-recommend-L3MWH4JW.js → room-recommend-62H7ZQQ6.js} +4 -4
  65. package/dist/chunks/{schedule-UASAVDVD.js → schedule-FNXKH3DQ.js} +4 -4
  66. package/dist/chunks/{workflow-KDGIUCNW.js → workflow-ADHKCAJK.js} +4 -4
  67. package/dist/index.js +33 -28
  68. package/dist/internal/e10/auth/commands.js +110 -56
  69. package/dist/internal/e10/auth/messages.js +6 -0
  70. package/dist/internal/e10/auth/session.js +28 -1
  71. package/dist/internal/e10/auth/xiaoe.js +315 -48
  72. package/dist/internal/e10/auth-recovery.js +36 -5
  73. package/dist/internal/e10/client.js +13 -0
  74. package/dist/internal/e10/context.js +61 -9
  75. package/dist/internal/e10/index.js +1 -1
  76. package/dist/internal/e10/network-error.js +93 -0
  77. package/dist/internal/e10/request-runtime.js +15 -0
  78. package/dist/shortcuts/calendar/operations/read.js +31 -22
  79. package/dist/shortcuts/calendar/operations/write.js +45 -1
  80. package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
  81. package/dist/shortcuts/cowork/operations/item-write.js +5 -3
  82. package/dist/shortcuts/cowork/operations/member-write.js +3 -1
  83. package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
  84. package/dist/shortcuts/cowork/operations/shared.js +7 -0
  85. package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
  86. package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
  87. package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
  88. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
  89. package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
  90. package/dist/shortcuts/ehr/operations/recruit.js +10 -4
  91. package/dist/shortcuts/ehr/operations/shared.js +47 -0
  92. package/dist/shortcuts/hrm/operations/org.js +118 -0
  93. package/dist/shortcuts/index.js +5 -0
  94. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  95. package/dist/shortcuts/meeting/operations/eb/catalog.js +9 -2
  96. package/dist/shortcuts/meeting/operations/eb/create-flow.js +461 -127
  97. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  98. package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
  99. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  100. package/dist/shortcuts/meeting/operations/eb/group-chat.js +605 -0
  101. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  102. package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
  103. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +44 -6
  104. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  105. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  106. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  107. package/dist/shortcuts/project/operations/read.js +6 -25
  108. package/dist/shortcuts/project/operations/shared.js +22 -0
  109. package/dist/shortcuts/project/operations/write.js +28 -2
  110. package/dist/shortcuts/recruit-stage/context.js +119 -0
  111. package/dist/shortcuts/recruit-stage/continuation.js +73 -0
  112. package/dist/shortcuts/recruit-stage/errors.js +65 -0
  113. package/dist/shortcuts/recruit-stage/host.js +109 -0
  114. package/dist/shortcuts/recruit-stage/index.js +115 -0
  115. package/dist/shortcuts/recruit-stage/manifest.js +29 -0
  116. package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
  117. package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
  118. package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
  119. package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
  120. package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
  121. package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
  122. package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
  123. package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
  124. package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
  125. package/dist/shortcuts/recruit-stage/shared.js +608 -0
  126. package/dist/shortcuts/workflow/operations/form.js +21 -2
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +52 -1
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +19 -11
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +78 -10
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +3 -1
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +16 -40
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +52 -15
  138. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
  139. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
  140. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
  141. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +53 -16
  142. package/docs/e10-auth.md +22 -1
  143. package/docs/operation-manual.md +8 -0
  144. package/package.json +1 -1
  145. package/scripts/install-skill-to-xiaoe.mjs +105 -0
  146. package/scripts/mcp-meeting.mjs +234 -0
  147. package/scripts/package-skill.mjs +4 -2
  148. package/skill-template/business-info.json +24 -0
  149. package/skill-template/domains/recruit-stage.md +29 -0
  150. package/skills/weaver-e10-calendar/SKILL.md +2 -1
  151. package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
  152. package/skills/weaver-e10-cowork/SKILL.md +1 -1
  153. package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
  154. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  155. package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
  156. package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
  157. package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
  158. package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
  159. package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
  160. package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
  161. package/skills/weaver-e10-mail/SKILL.md +1 -1
  162. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  163. package/skills/weaver-e10-meeting/SKILL.md +119 -26
  164. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  165. package/skills/weaver-e10-meeting.zip +0 -0
  166. package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
  167. package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
  168. package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
  169. package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
  170. package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
  171. package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
  172. package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
  173. package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
  174. package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
  175. package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
  176. package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
  177. package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
  178. package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
  179. package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
  180. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
  181. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
  182. package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
  183. package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
  184. package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
  185. package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
  186. package/skills/weaver-e10-shared/SKILL.md +2 -2
  187. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
  188. package/skills/weaver-e10-shijingran/SKILL.md +3 -2
  189. package/skills/weaver-e10-shijingran/references/operations.md +3 -1
  190. package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
  191. package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
  192. package/skills/weaver-e10-workflow/SKILL.md +7 -1
  193. package/skills/weaver-e10-workflow/aigroup/default.json +23 -9
  194. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +37 -21
  195. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +135 -43
  196. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
  197. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
@@ -0,0 +1,630 @@
1
+ /**
2
+ * recruit-stage.jd.create.* —— 生成职位 JD / 创建职位(写操作,自 recruit-generate-jd.mjs 1:1 移植)
3
+ *
4
+ * 链路:① saveFormData(职位管理 uf_rcrt_position_manage) → ② addPosition?formDataId 提交生成职位
5
+ *
6
+ * 三种操作:
7
+ * .prepare 只出确认摘要(不写库),签发 continuation(写操作红线:先确认后落库)
8
+ * .apply 用户确认后执行(confirm:true + continuation + 摘要指纹比对)
9
+ * .prefill 预填模式(人工保存):输出新建页 URL + WeFormSDK 注入脚本,不落库
10
+ *
11
+ * 字段 id 全部经环境缓存 fields[objId] 运行时取,不写死。
12
+ * 多行文本字段(position_duty/work_request)走 dataText;单行文本(position_name)走 content;
13
+ * 关联类字段(demand/status/recruit_flow)走 dataOptions。
14
+ *
15
+ * sets 覆盖("贴合 JD 型"):AI 读 JD 后按字段语义传值,按字段取值类别自动解析 optionId:
16
+ * 数值型→content;人员型→按姓名解析 userId;枚举型→ds/options;字典引用型→数据字典;
17
+ * 关联表单型 work_location→「办公地点」解析;其它→content。
18
+ *
19
+ * 默认值策略(跨环境运行时解析、不写死 id):状态=招聘中;流程=环境默认启用流程;
20
+ * 人数=1、薪酬月数=12;薪酬类型=月薪、招聘方式=社会招聘;
21
+ * 招聘负责人/用人经理=当前登录用户;其余关联表单型取环境内已有职位记录的值("默认值=系统现成值")。
22
+ */
23
+ import { createHash, randomBytes } from 'node:crypto';
24
+ import { readFileSync } from 'node:fs';
25
+ import { resolve as resolvePath, dirname } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { rsSuccess, schemaOf } from './types.js';
28
+ import { rsFail, rsValidationError, rsConfirmationError } from '../errors.js';
29
+ import { objIdOf, formIdOf, saveFormData, getDsOptions, cachedDsMap, getFormDataRecord, resolvePersonId, findMenuNode, resolveCurrentUser, buildCardUrl, buildListLocateUrl, randHex32, } from '../shared.js';
30
+ const SCHEMA_ID_STR = { type: 'string', description: '18 位记录 id(字符串)' };
31
+ // 职位管理表单字段 dataKey(缓存 fields 里取;不写死 fieldId)
32
+ const DATAKEYS_TEXT = ['position_name'];
33
+ const DATAKEYS_MTEXT = ['position_duty', 'work_request'];
34
+ // 必填字段默认值策略
35
+ const DEF_TEXT = { recruit_count: '1', number_of_pay_months: '12' };
36
+ const DEF_OPTION = { salary_type: '月薪', recruit_type: '社会招聘' };
37
+ const DEF_CURRENT_USER = ['recr_principal', 'employ_manager'];
38
+ const DEF_FROM_REF = [
39
+ 'position_nature', 'job_category', 'education', 'work_years', 'work_location',
40
+ 'salary_min', 'salary_max',
41
+ ];
42
+ const DEF_STATUS = '招聘中';
43
+ // sets 覆盖解析:字段取值类别
44
+ const NUM_KEYS = new Set(['recruit_count', 'number_of_pay_months', 'salary_min', 'salary_max', 'internship_duration', 'working_days']);
45
+ const PERSON_KEYS = new Set(['recr_principal', 'recr_helper', 'employ_manager', 'interviewer']);
46
+ const OPTION_KEYS = new Set(['salary_type', 'recruit_type', 'status']);
47
+ const DICT_KEYS = new Set(['position_nature', 'job_category', 'education', 'work_years']);
48
+ const MTEXT_KEYS = new Set(['position_duty', 'work_request']);
49
+ function fingerprint(value) {
50
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
51
+ }
52
+ function detailValueId(d) {
53
+ if (!d)
54
+ return '';
55
+ if (d.dataOptions && d.dataOptions[0])
56
+ return String(d.dataOptions[0].optionId || '');
57
+ if (d.dataText)
58
+ return String(d.dataText.content || '');
59
+ if (d.content !== undefined)
60
+ return String(d.content);
61
+ return '';
62
+ }
63
+ function dictGroupOf(cache, valueId) {
64
+ const row = (cache.dict?.raw || []).find((x) => String(x.option_id) === String(valueId));
65
+ return row ? String(row.dict_type) : '';
66
+ }
67
+ function dictIdByName(cache, name, group) {
68
+ const rows = (cache.dict?.raw || []).filter((x) => x.selection_name === name && (!group || String(x.dict_type) === group));
69
+ return rows.length ? String(rows[0].option_id) : '';
70
+ }
71
+ // 取环境内一条「字段较全」的职位记录的全部字段(ret: { fid: detail }),保证引用有效
72
+ async function fetchRefByFid(ctx) {
73
+ const { host, cache } = ctx;
74
+ const objId = objIdOf(cache, 'position_manage');
75
+ const formId = formIdOf(cache, 'position_manage');
76
+ const posMenu = cache.lists?.position || null;
77
+ if (!posMenu)
78
+ return {};
79
+ const body = {
80
+ current: 1, pageSize: 20, tableuid: randHex32(),
81
+ listId: posMenu.listId, searchParamData: { searchType: '3', type: 'and' },
82
+ terminal: 'PC', browser: 'Chrome', requiredMatch: 1, mapResult: 1,
83
+ recheckQuery: 1, isFollow: 0, isShareToMe: 0, filterCondition: {},
84
+ cusMenuId: posMenu.cusMenuId, urlPageTitle: '招聘职位', menuName: '招聘职位',
85
+ };
86
+ const r = await host.request({
87
+ path: `/api/ebuilder/form/list/getListDatas?passid=${objId}&cid=${objId}`,
88
+ body, headers: { ebBusinessId: posMenu.listId },
89
+ });
90
+ const rows = r?.data?.data || [];
91
+ const ref = rows.find((x) => x.form_data_id && x.position_nature);
92
+ if (!ref)
93
+ return {};
94
+ const rec = await getFormDataRecord(host, formId, String(ref.form_data_id), objId);
95
+ const byFid = {};
96
+ for (const d of rec.dataDetails || []) {
97
+ const f = String(d.fieldId || d.formField?.id || '');
98
+ if (f)
99
+ byFid[f] = d;
100
+ }
101
+ return byFid;
102
+ }
103
+ // ref 记录 detail → 提交 detail(保留 dataOptions/dataText/content 形态)
104
+ function refToSubmit(d) {
105
+ const o = { formField: { id: String(d.fieldId || d.formField?.id) } };
106
+ if (d.dataOptions)
107
+ o.dataOptions = d.dataOptions;
108
+ else if (d.dataText)
109
+ o.dataText = d.dataText;
110
+ else if (d.content !== undefined && d.content !== '')
111
+ o.content = d.content;
112
+ else if (d.value !== undefined && d.value !== '')
113
+ o.dataText = { content: String(d.value) };
114
+ else
115
+ return null;
116
+ return o;
117
+ }
118
+ // 工作地点:按名称/详细地址/城市解析「办公地点」记录 id(tag uf_rcrt_work_place → 表单 → 列表匹配)
119
+ async function resolveWorkLocation(ctx, value) {
120
+ const { host, appId, cache } = ctx;
121
+ const t = await host.request({ path: `/api/ebuilder/form/obj/getObjsByTag?appId=${appId}&tag=uf_rcrt_work_place`, method: 'GET' });
122
+ const d = t?.data;
123
+ const pObj = (Array.isArray(d) && d[0]) ? String(d[0].id || '') : '';
124
+ const menu = findMenuNode(cache.menus || [], ['办公地点', '工作地点'], (m) => m.listId && m.cusMenuId);
125
+ if (!pObj || !menu)
126
+ return null;
127
+ const body = {
128
+ current: 1, pageSize: 200, tableuid: randHex32(),
129
+ listId: menu.listId, searchParamData: { searchType: '3', type: 'and' },
130
+ terminal: 'PC', browser: 'Chrome', requiredMatch: 1, mapResult: 1,
131
+ recheckQuery: 1, isFollow: 0, isShareToMe: 0, filterCondition: {},
132
+ cusMenuId: menu.cusMenuId, urlPageTitle: menu.menuName, menuName: menu.menuName,
133
+ };
134
+ const r = await host.request({ path: `/api/ebuilder/form/list/getListDatas?passid=${pObj}&cid=${pObj}`, body, headers: { ebBusinessId: menu.listId } });
135
+ const rows = r?.data?.data || [];
136
+ const v = String(value).trim();
137
+ const hit = rows.find((x) => {
138
+ const name = String(x.name || ''), detail = String(x.detail_location || ''), area = String(x.area_text || '');
139
+ return name.includes(v) || detail.includes(v) || area.includes(v) || (detail && v.includes(detail));
140
+ });
141
+ return hit ? { id: String(hit.form_data_id || hit.id), name: String(hit.detail_location || hit.name || v) } : null;
142
+ }
143
+ // 解析 sets key=value → 提交 detail(按字段取值类别;解析不到返回 null)
144
+ async function resolveSetValue(ctx, key, value, refByFid) {
145
+ const { host, appId, cache } = ctx;
146
+ const objId = objIdOf(cache, 'position_manage');
147
+ const id = (cache.fields[objId] || {})[key];
148
+ if (!id)
149
+ return null;
150
+ if (NUM_KEYS.has(key))
151
+ return { formField: { id }, content: String(value) };
152
+ if (MTEXT_KEYS.has(key))
153
+ return { formField: { id }, dataText: { content: String(value) } };
154
+ if (key === 'position_name')
155
+ return { formField: { id }, content: String(value) };
156
+ if (PERSON_KEYS.has(key)) {
157
+ const uid = await resolvePersonId(host, id, value);
158
+ return uid ? { formField: { id }, dataOptions: [{ optionId: String(uid), content: value }] } : null;
159
+ }
160
+ if (OPTION_KEYS.has(key)) {
161
+ const m = cachedDsMap(cache, objId, key) || await getDsOptions(host, objId, id, appId);
162
+ const oid = m[value];
163
+ return oid ? { formField: { id }, dataOptions: [{ optionId: String(oid), content: value }] } : null;
164
+ }
165
+ if (DICT_KEYS.has(key)) {
166
+ const group = dictGroupOf(cache, detailValueId(refByFid[String(id)]));
167
+ const did = dictIdByName(cache, value, group) || dictIdByName(cache, value, '');
168
+ return did ? { formField: { id }, dataOptions: [{ optionId: String(did), content: value }] } : null;
169
+ }
170
+ if (key === 'work_location') {
171
+ const loc = await resolveWorkLocation(ctx, value);
172
+ return loc ? { formField: { id }, dataOptions: [{ optionId: loc.id, content: loc.name }] } : null;
173
+ }
174
+ return { formField: { id }, content: String(value) };
175
+ }
176
+ // ── 预填(人工保存)模式:dataDetails → WeFormSDK.changeFieldValue 的 (mark, info) ──
177
+ function toPrefillFields(dataDetails, fields) {
178
+ const inv = {};
179
+ for (const [k, v] of Object.entries(fields))
180
+ inv[String(v)] = k;
181
+ const personFids = new Set(Object.keys(fields).filter((k) => PERSON_KEYS.has(k)).map((k) => String(fields[k])));
182
+ const out = [];
183
+ for (const d of dataDetails) {
184
+ const fid = String(d.formField.id);
185
+ let info = null;
186
+ if (d.dataOptions)
187
+ info = { specialObj: d.dataOptions.map((o) => ({ id: String(o.optionId), name: o.content, ...(personFids.has(fid) ? { type: 'user' } : {}) })) };
188
+ else if (d.dataText)
189
+ info = { value: String(d.dataText.content) };
190
+ else if (d.content !== undefined)
191
+ info = { value: String(d.content) };
192
+ if (info)
193
+ out.push({ mark: 'field' + fid, info, label: inv[fid] || fid });
194
+ }
195
+ return out;
196
+ }
197
+ // 职位管理「新建页」URL:从 allMenus 解析职位列表菜单(别名匹配,跨环境不写死)
198
+ async function resolvePositionNewUrl(ctx) {
199
+ const { host, appId, baseUrl, cache } = ctx;
200
+ const cached = cache.menus?.find?.((m) => m.combinationId && m.id && m.pageId && ['招聘职位', '职位列表', '职位管理'].some((n) => (m.menuName || '').includes(n)));
201
+ let node = cached || null;
202
+ if (!node) {
203
+ const r = await host.request({ path: `/api/cusapp/app/allMenus?isFront=1&appId=${appId}&terminalType=PC`, method: 'GET' });
204
+ const flat = [];
205
+ (function walk(ns) { for (const n of (ns || []))
206
+ if (n && typeof n === 'object') {
207
+ flat.push(n);
208
+ walk(n.children || n.childs);
209
+ } })(r?.data?.cusAppMenus || r?.data);
210
+ node = findMenuNode(flat, ['招聘职位', '职位列表', '职位管理'], (n) => n.combinationId && n.id && n.pageId);
211
+ }
212
+ if (!node)
213
+ return null;
214
+ return `${baseUrl}/ebdapp/view/${appId}/combination/${node.combinationId}-${node.id}/search/${node.pageId}?terminal=PC&cusMenuId=${node.id}`;
215
+ }
216
+ // 招聘首页(PAGE 型)URL
217
+ async function resolveHomePageUrl(ctx) {
218
+ const { host, appId, baseUrl } = ctx;
219
+ const r = await host.request({ path: `/api/cusapp/app/allMenus?isFront=1&appId=${appId}&terminalType=PC`, method: 'GET' });
220
+ const flat = [];
221
+ (function walk(ns) { for (const n of (ns || []))
222
+ if (n && typeof n === 'object') {
223
+ flat.push(n);
224
+ walk(n.children || n.childs);
225
+ } })(r?.data?.cusAppMenus || r?.data);
226
+ const node = findMenuNode(flat, ['招聘首页', '招聘主页'], (n) => n.pageType === 'PAGE' && n.pageId && n.id)
227
+ || flat.find((n) => n.pageType === 'PAGE' && String(n.menuName || '').includes('首页'));
228
+ if (!node || !node.pageId || !node.id)
229
+ return null;
230
+ return `${baseUrl}/ebdapp/view/${appId}/PAGE/${node.pageId}-${node.id}?terminal=PC&cusMenuId=${node.id}&appId_${node.pageId}=${appId}`;
231
+ }
232
+ // 参考同类职位:抓最近一页职位列表(displayData 已是服务端翻译后的可读文本),按关键词客户端过滤
233
+ // 取前 5 条,再逐条拉记录详情取薪酬与职责/要求正文。供 AI 在确认摘要里给出「最近同类职位参考」。
234
+ const REF_DETAIL_KEYS = ['salary_type', 'salary_min', 'salary_max', 'number_of_pay_months', 'education',
235
+ 'work_years', 'recruit_count', 'recruit_type', 'position_nature', 'work_location', 'department', 'sub_company'];
236
+ const REF_TEXT_KEYS = ['position_duty', 'work_request'];
237
+ async function fetchRecentReferences(ctx, jd, keyword) {
238
+ const { host, cache } = ctx;
239
+ const listParam = cache.lists?.position || null;
240
+ if (!listParam?.listId)
241
+ return { available: false, reason: '环境缓存缺少职位列表参数', items: [] };
242
+ const r = await host.request({
243
+ path: '/api/ebuilder/form/list/getListDatas?passid=' + jd.objId + '&cid=' + jd.objId,
244
+ body: {
245
+ current: 1, pageSize: 30, tableuid: randHex32(), listId: listParam.listId,
246
+ searchParamData: { searchType: '3', type: 'and' },
247
+ terminal: 'PC', browser: 'Chrome', requiredMatch: 1, mapResult: 1, recheckQuery: 1,
248
+ isFollow: 0, isShareToMe: 0, filterCondition: {},
249
+ cusMenuId: listParam.cusMenuId, urlPageTitle: listParam.menuName, menuName: listParam.menuName,
250
+ },
251
+ headers: { ebBusinessId: listParam.listId },
252
+ });
253
+ const rows = r?.data?.displayData || r?.data?.data || [];
254
+ const kw = String(keyword || '').trim();
255
+ const tokens = [...new Set(kw.split(/[^A-Za-z0-9\u4e00-\u9fff]+/).concat(kw).map((t) => t.trim()).filter((t) => t.length >= 2))];
256
+ const match = (row) => {
257
+ if (!tokens.length)
258
+ return true;
259
+ const name = String(row.position_name || row.name || '');
260
+ const hay = name + ' ' + Object.values(row.displayData || {}).map(String).join(' ');
261
+ return tokens.some((t) => hay.toUpperCase().includes(t.toUpperCase()));
262
+ };
263
+ const fields = cache.fields[jd.objId] || {};
264
+ const rev = {};
265
+ for (const [k, v] of Object.entries(fields))
266
+ rev[String(v)] = k;
267
+ const picked = [];
268
+ for (const row of rows) {
269
+ if (!match(row) || picked.length >= 5)
270
+ continue;
271
+ const dataId = String(row.form_data_id || row.id || '');
272
+ if (!dataId || dataId === String(jd.dataId || ''))
273
+ continue;
274
+ const item = {
275
+ 职位: String(row.position_name || row.name || '').trim(),
276
+ 状态: String(row.status || row.data_status_value || '').trim(),
277
+ 创建时间: String(row.create_time || '').trim(),
278
+ };
279
+ try {
280
+ const rec = await getFormDataRecord(host, jd.formId, dataId, jd.objId, '');
281
+ for (const d of rec.dataDetails || []) {
282
+ const dk = rev[String(d.fieldId ?? d.formField?.id)] || '';
283
+ if (!dk)
284
+ continue;
285
+ let v = '';
286
+ if (Array.isArray(d.dataOptions) && d.dataOptions.length)
287
+ v = d.dataOptions.map((o) => o?.content || '').join(',');
288
+ else if (d.dataText?.content)
289
+ v = String(d.dataText.content);
290
+ else if (d.content !== undefined && d.content !== '')
291
+ v = String(d.content);
292
+ if (v === '')
293
+ continue;
294
+ if (REF_DETAIL_KEYS.includes(dk))
295
+ item[dk] = v.slice(0, 60);
296
+ else if (REF_TEXT_KEYS.includes(dk))
297
+ item[dk] = v.replace(/\s+/g, ' ').slice(0, 150);
298
+ }
299
+ }
300
+ catch { /* 单条详情失败不影响其余参考 */ }
301
+ if (Object.keys(item).length > 3)
302
+ picked.push(item);
303
+ }
304
+ return { available: true, keyword: kw, scanned: rows.length, items: picked };
305
+ }
306
+ async function buildJd(opts) {
307
+ const { ctx, positionName, duty, requestText, demandId, demandName, sets } = opts;
308
+ const { host, appId, cache } = ctx;
309
+ const objId = objIdOf(cache, 'position_manage');
310
+ const formId = formIdOf(cache, 'position_manage');
311
+ const fields = cache.fields[objId] || {};
312
+ const fid = (k) => fields[k];
313
+ for (const k of DATAKEYS_TEXT)
314
+ if (!fid(k))
315
+ throw rsFail('职位管理缺少字段 ' + k + '(ds/fields 解析不到)');
316
+ for (const k of DATAKEYS_MTEXT)
317
+ if (!fid(k))
318
+ throw rsFail('职位管理缺少字段 ' + k + '(ds/fields 解析不到)');
319
+ const refByFid = await fetchRefByFid(ctx);
320
+ const map = new Map();
321
+ const put = (d) => { if (d && d.formField && d.formField.id)
322
+ map.set(String(d.formField.id), d); };
323
+ const hasKey = (k) => { const id = fid(k); return !!(id && map.has(String(id))); };
324
+ // ① 用户提供的核心字段
325
+ put({ formField: { id: fid('position_name') }, content: positionName });
326
+ if (duty)
327
+ put({ formField: { id: fid('position_duty') }, dataText: { content: duty } });
328
+ if (requestText)
329
+ put({ formField: { id: fid('work_request') }, dataText: { content: requestText } });
330
+ if (demandId) {
331
+ if (!fid('demand'))
332
+ throw rsFail('职位管理缺少字段 demand(ds/fields 解析不到,关联需求无法设置)');
333
+ put({ formField: { id: fid('demand') }, dataOptions: [{ optionId: String(demandId), content: demandName || String(demandId) }] });
334
+ }
335
+ // ② sets 覆盖(AI 依 JD 指定;优先于自动默认值)
336
+ const appliedSets = [];
337
+ const skippedSets = [];
338
+ for (const [k, v] of sets) {
339
+ const d = await resolveSetValue(ctx, k, v, refByFid);
340
+ if (d) {
341
+ put(d);
342
+ appliedSets.push(k + '=' + v);
343
+ }
344
+ else {
345
+ skippedSets.push(k + '=' + v);
346
+ }
347
+ }
348
+ // ③ 默认值:招聘状态 = 招聘中
349
+ if (!hasKey('status')) {
350
+ if (!fid('status'))
351
+ throw rsFail('职位管理缺少字段 status(ds/fields 解析不到)');
352
+ const statusMap = cachedDsMap(cache, objId, 'status') || await getDsOptions(host, objId, fid('status'), appId);
353
+ const statusId = statusMap[DEF_STATUS];
354
+ if (!statusId)
355
+ throw rsFail('职位管理「招聘状态」未解析到「' + DEF_STATUS + '」选项(ds/options 返回为空)');
356
+ put({ formField: { id: fid('status') }, dataOptions: [{ optionId: String(statusId), content: DEF_STATUS }] });
357
+ }
358
+ // ④ 默认值:招聘流程 = 环境默认启用的流程
359
+ const defFlow = cache.defaultRecruitFlow;
360
+ if (!hasKey('recruit_flow') && fid('recruit_flow') && defFlow && defFlow.id) {
361
+ put({ formField: { id: fid('recruit_flow') }, dataOptions: [{ optionId: String(defFlow.id), content: defFlow.name }] });
362
+ }
363
+ // ⑤ 人员型默认 = 当前登录用户
364
+ const filled = [];
365
+ let currentUser = null;
366
+ if (DEF_CURRENT_USER.some((k) => fid(k) && !hasKey(k))) {
367
+ currentUser = await resolveCurrentUser(host);
368
+ if (currentUser && currentUser.id) {
369
+ for (const k of DEF_CURRENT_USER) {
370
+ if (fid(k) && !hasKey(k)) {
371
+ put({ formField: { id: fid(k) }, dataOptions: [{ optionId: String(currentUser.id), content: currentUser.name }] });
372
+ filled.push(k);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ // ⑥ 其余必填字段默认值
378
+ for (const [k, v] of Object.entries(DEF_TEXT)) {
379
+ if (fid(k) && !hasKey(k)) {
380
+ put({ formField: { id: fid(k) }, content: v });
381
+ filled.push(k);
382
+ }
383
+ }
384
+ for (const [k, name] of Object.entries(DEF_OPTION)) {
385
+ if (fid(k) && !hasKey(k)) {
386
+ const m = cachedDsMap(cache, objId, k) || await getDsOptions(host, objId, fid(k), appId);
387
+ const oid = m[name];
388
+ if (oid) {
389
+ put({ formField: { id: fid(k) }, dataOptions: [{ optionId: String(oid), content: name }] });
390
+ filled.push(k);
391
+ }
392
+ }
393
+ }
394
+ for (const k of DEF_FROM_REF) {
395
+ const id = fid(k);
396
+ if (id && !map.has(String(id)) && refByFid[String(id)]) {
397
+ const d = refToSubmit(refByFid[String(id)]);
398
+ if (d) {
399
+ put(d);
400
+ filled.push(k);
401
+ }
402
+ }
403
+ }
404
+ const inv = {};
405
+ for (const [k, v] of Object.entries(fields))
406
+ inv[String(v)] = k;
407
+ const requiredDefaultsFilled = [...map.keys()].map((f) => inv[f] || f).filter((k) => filled.includes(k));
408
+ return {
409
+ objId, formId, appId, dataDetails: [...map.values()],
410
+ summary: {
411
+ action: '生成职位JD(创建职位)',
412
+ positionName,
413
+ dutyLen: (duty || '').length,
414
+ requestLen: (requestText || '').length,
415
+ demand: demandId ? (demandName || demandId) : '(未关联需求)',
416
+ status: DEF_STATUS,
417
+ recruitFlow: (defFlow && defFlow.id) ? defFlow.name : '(未解析到默认流程)',
418
+ currentUser: currentUser ? currentUser.name : null,
419
+ setApplied: appliedSets,
420
+ setSkipped: skippedSets,
421
+ requiredDefaultsFilled,
422
+ },
423
+ };
424
+ }
425
+ function setsOf(args) {
426
+ const raw = args.sets;
427
+ if (Array.isArray(raw)) {
428
+ return raw.map((item) => {
429
+ if (typeof item === 'string') {
430
+ const j = item.indexOf('=');
431
+ if (j <= 0)
432
+ throw rsValidationError('set_invalid', `sets 项必须是 "字段=值" 形式:${item}`);
433
+ return [item.slice(0, j).trim().replace(/-/g, '_'), item.slice(j + 1)];
434
+ }
435
+ if (item && typeof item === 'object' && item.key !== undefined) {
436
+ return [String(item.key).trim().replace(/-/g, '_'), String(item.value ?? '')];
437
+ }
438
+ throw rsValidationError('set_invalid', 'sets 项必须是字符串 "字段=值" 或 {key,value} 对象');
439
+ });
440
+ }
441
+ return [];
442
+ }
443
+ async function prepare(args, env) {
444
+ const host = env.host;
445
+ const session = await host.getSession();
446
+ const ctx = { host, baseUrl: session.baseUrl.replace(/\/+$/, ''), appId: '', cache: await env.recruitContext() };
447
+ ctx.appId = ctx.cache.appId;
448
+ const positionName = String(args.positionName || '').trim();
449
+ if (!positionName)
450
+ throw rsValidationError('position_name_required', 'positionName 必填');
451
+ const jd = await buildJd({
452
+ ctx, positionName,
453
+ duty: args.positionDuty ? String(args.positionDuty) : undefined,
454
+ requestText: args.workRequest ? String(args.workRequest) : undefined,
455
+ demandId: args.demandId ? String(args.demandId) : undefined,
456
+ demandName: args.demandName ? String(args.demandName) : undefined,
457
+ sets: setsOf(args),
458
+ });
459
+ const refs = await fetchRecentReferences(ctx, jd, String(args.referenceKeyword || positionName))
460
+ .catch((e) => ({ available: false, reason: e.message, items: [] }));
461
+ const willSubmit = { formId: jd.formId, objId: jd.objId, dataDetails: jd.dataDetails };
462
+ const fp = fingerprint(willSubmit);
463
+ const continuation = await env.continuation.issue({ operation: 'recruit-stage.jd.create.apply', fingerprint: fp });
464
+ return rsSuccess('recruit-stage.jd.create.prepare', {
465
+ mode: 'prepare',
466
+ ...jd.summary,
467
+ recentReferences: refs.items,
468
+ recentReferencesNote: refs.available
469
+ ? `已按「${refs.keyword}」在当前环境职位列表中检索(扫描 ${refs.scanned} 条,命中 ${refs.items.length} 条)——请对标这些同类职位的薪酬/人数/要求给出推荐值,一并放进确认摘要,不要逐字段询问用户`
470
+ : ('参考检索不可用:' + (refs.reason || '')),
471
+ willSubmit,
472
+ }, {
473
+ continuation,
474
+ fingerprint: fp,
475
+ confirmHint: '用户明确确认后,携带 confirm:true 与本 continuation 调用 recruit-stage.jd.create.apply',
476
+ });
477
+ }
478
+ async function apply(args, env) {
479
+ if (args.confirm !== true)
480
+ throw rsConfirmationError('recruit-stage.jd.create.apply', { hint: '写操作必须显式 confirm:true' });
481
+ if (!args.continuation)
482
+ throw rsValidationError('continuation_required', '缺少 continuation(来自 prepare 的 meta)');
483
+ const payload = await env.continuation.verify(String(args.continuation), 'recruit-stage.jd.create.apply');
484
+ const host = env.host;
485
+ const session = await host.getSession();
486
+ const ctx = { host, baseUrl: session.baseUrl.replace(/\/+$/, ''), appId: '', cache: await env.recruitContext() };
487
+ ctx.appId = ctx.cache.appId;
488
+ const positionName = String(args.positionName || '').trim();
489
+ if (!positionName)
490
+ throw rsValidationError('position_name_required', 'positionName 必填');
491
+ const jd = await buildJd({
492
+ ctx, positionName,
493
+ duty: args.positionDuty ? String(args.positionDuty) : undefined,
494
+ requestText: args.workRequest ? String(args.workRequest) : undefined,
495
+ demandId: args.demandId ? String(args.demandId) : undefined,
496
+ demandName: args.demandName ? String(args.demandName) : undefined,
497
+ sets: setsOf(args),
498
+ });
499
+ const willSubmit = { formId: jd.formId, objId: jd.objId, dataDetails: jd.dataDetails };
500
+ if (payload.fingerprint !== fingerprint(willSubmit)) {
501
+ throw rsValidationError('fingerprint_mismatch', '待提交内容与 prepare 确认摘要不一致(字段值已变化),请重新 prepare 确认后再 apply');
502
+ }
503
+ // ① 保存职位管理表单 → ② addPosition 提交生成职位
504
+ const formDataId = await saveFormData(host, {
505
+ appId: jd.appId, formId: jd.formId, objId: jd.objId,
506
+ name: '职位管理-' + positionName, dataDetails: jd.dataDetails,
507
+ });
508
+ const r2 = await host.request({ path: '/api/recruit/position/v2/addPosition?formDataId=' + formDataId, method: 'POST' });
509
+ const data = r2?.data || {};
510
+ const positionPostId = data.positionPostId;
511
+ const checkRpa = data.checkRpa;
512
+ const rpaValidMsg = data.rpaValidMsg || null;
513
+ const created = r2?.status !== false && !!positionPostId;
514
+ const listUrl = await resolvePositionNewUrl(ctx).catch(() => null);
515
+ const cardUrl = buildCardUrl(ctx.baseUrl, jd.objId, formDataId);
516
+ const locateUrl = buildListLocateUrl(ctx.baseUrl, ctx.appId, (ctx.cache.lists?.position?.listId) || '', jd.objId, formDataId);
517
+ return rsSuccess('recruit-stage.jd.create.apply', {
518
+ mode: 'apply',
519
+ result: created ? 'success' : 'failed',
520
+ formDataId,
521
+ positionPostId: positionPostId || null,
522
+ rpaChecked: checkRpa === undefined ? null : !!checkRpa,
523
+ rpaValidMsg,
524
+ positionListUrl: listUrl || null,
525
+ positionCardUrl: cardUrl,
526
+ positionListLocateUrl: locateUrl,
527
+ // 产物链接(Agent 原样搬运成 Markdown 链接,按顺序:详情在前、列表在后)
528
+ viewLink: cardUrl ? `[查看 ${jd.summary.positionName || '新建职位'} 职位详情](${cardUrl})` : null,
529
+ listLocateLink: (locateUrl || listUrl) ? `[打开招聘岗位列表(已定位新建职位)](${locateUrl || listUrl})` : null,
530
+ agentNote: '查看入口 = 两个 Markdown 链接(平台实证可点击):按顺序原样搬运 viewLink 与 listLocateLink;不要包 browsertool 标签。链接只是查看入口,重复点击不会重复创建。',
531
+ ...jd.summary,
532
+ });
533
+ }
534
+ async function prefill(args, env) {
535
+ const host = env.host;
536
+ const session = await host.getSession();
537
+ const ctx = { host, baseUrl: session.baseUrl.replace(/\/+$/, ''), appId: '', cache: await env.recruitContext() };
538
+ ctx.appId = ctx.cache.appId;
539
+ const positionName = String(args.positionName || '').trim();
540
+ if (!positionName)
541
+ throw rsValidationError('position_name_required', 'positionName 必填');
542
+ const jd = await buildJd({
543
+ ctx, positionName,
544
+ duty: args.positionDuty ? String(args.positionDuty) : undefined,
545
+ requestText: args.workRequest ? String(args.workRequest) : undefined,
546
+ demandId: args.demandId ? String(args.demandId) : undefined,
547
+ demandName: args.demandName ? String(args.demandName) : undefined,
548
+ sets: setsOf(args),
549
+ });
550
+ const fields = toPrefillFields(jd.dataDetails, ctx.cache.fields[jd.objId] || {});
551
+ const listUrl = await resolvePositionNewUrl(ctx);
552
+ const homeUrl = await resolveHomePageUrl(ctx);
553
+ const jsExpr = `(() => { const W = window.WeFormSDK; const i = W && W.getWeFormInstance('ebuilderform', '${jd.formId}');`
554
+ + ` if (!i) return 'no-form-instance(请先打开职位管理新建表单)'; const fields = ${JSON.stringify(fields)};`
555
+ + ` let ok = 0, fail = []; for (const f of fields) { try { i.changeFieldValue(f.mark, f.info); ok++; } catch (e) { fail.push(f.label); } }`
556
+ + ` return 'prefilled ' + ok + '/' + fields.length + (fail.length ? ' fail=' + fail.join(',') : ''); })()`;
557
+ return rsSuccess('recruit-stage.jd.create.prefill', {
558
+ mode: 'prefill',
559
+ ...jd.summary,
560
+ formId: jd.formId,
561
+ fields,
562
+ jsExpr,
563
+ openMethods: {
564
+ homeFullscreen: { url: homeUrl, openAction: '点「快速入口 → 创建职位」打开表单,再点表单右上角「放大」按钮 → 全屏单页' },
565
+ listDialog: { url: listUrl, openAction: '点击页面上的「新建」' },
566
+ },
567
+ steps: [
568
+ '1) 浏览器打开 openMethods.homeFullscreen.url(或 listDialog.url),需已登录该环境',
569
+ '2) 打开新建表单:B = 首页点「创建职位」+ 点表单右上「放大」(全屏单页);A = 列表点「新建」(弹窗)',
570
+ '3) 在页面上下文执行 jsExpr(如 browser-skill: bsk evaluate "<jsExpr>")→ 返回 prefilled N/N 即完成',
571
+ '4) 由操作者核对内容后自行点「保存/保存并发布」;本模式不写任何数据',
572
+ ],
573
+ });
574
+ }
575
+ const baseInput = {
576
+ positionName: { type: 'string', description: '职位名称(必填)' },
577
+ positionDuty: { type: 'string', description: '职位职责/JD 正文(多行文本)' },
578
+ workRequest: { type: 'string', description: '任职要求(多行文本)' },
579
+ demandId: { ...SCHEMA_ID_STR, description: '关联招聘需求 id(可选)' },
580
+ demandName: { type: 'string', description: '关联招聘需求名称(可选,仅展示)' },
581
+ sets: {
582
+ type: 'array', maxItems: 30,
583
+ items: { oneOf: [{ type: 'string' }, { type: 'object', properties: { key: { type: 'string' }, value: { type: 'string' } } }] },
584
+ description: 'AI 依 JD 指定的字段值("字段dataKey=值" 或 {key,value})',
585
+ },
586
+ referenceKeyword: { type: 'string', description: '同类职位参考检索关键词(默认用职位名)' },
587
+ };
588
+ export const jdCreateOperations = [
589
+ {
590
+ name: 'recruit-stage.jd.create.prepare',
591
+ resource: 'recruit.jd',
592
+ method: 'prepare',
593
+ action: 'jd.create.prepare',
594
+ summary: '生成职位确认摘要:解析职位表单字段、套用默认值(状态/流程/负责人/关联字段取环境现成值)、检索最近同类职位参考,不写入任何数据',
595
+ capability: 'recruit.jd.create',
596
+ risk: 'write',
597
+ requiresConfirmation: true,
598
+ inputSchema: schemaOf(baseInput, { required: ['positionName'] }),
599
+ handler: prepare,
600
+ },
601
+ {
602
+ name: 'recruit-stage.jd.create.apply',
603
+ resource: 'recruit.jd',
604
+ method: 'apply',
605
+ action: 'jd.create.apply',
606
+ summary: '用户确认后创建职位:saveFormData(职位管理)→ addPosition 生成职位,与页面「保存」等价;返回职位详情与列表定位双链接',
607
+ capability: 'recruit.jd.create',
608
+ risk: 'write',
609
+ requiresConfirmation: true,
610
+ inputSchema: schemaOf({ ...baseInput, confirm: { type: 'boolean', description: '必须显式 true' }, continuation: { type: 'string', description: 'prepare 返回的 continuation' } }, { required: ['positionName', 'confirm', 'continuation'] }),
611
+ handler: apply,
612
+ },
613
+ {
614
+ name: 'recruit-stage.jd.create.prefill',
615
+ resource: 'recruit.jd',
616
+ method: 'prefill',
617
+ action: 'jd.create.prefill',
618
+ summary: '预填模式(人工保存):输出打开职位新建页的 URL + WeFormSDK 注入 jsExpr,不落库,由操作者核对后自行保存',
619
+ capability: 'recruit.jd.create',
620
+ risk: 'read',
621
+ inputSchema: schemaOf(baseInput, { required: ['positionName'] }),
622
+ handler: prefill,
623
+ },
624
+ ];
625
+ // 保留引用避免 tree-shake 误删(readFileSync/fileURLToPath 供 school-tiers 等后续扩展使用)
626
+ void readFileSync;
627
+ void resolvePath;
628
+ void dirname;
629
+ void fileURLToPath;
630
+ void randomBytes;