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.
Files changed (185) hide show
  1. package/dist/chunks/{archive-JHTONRST.js → archive-J4I6MCKF.js} +2 -2
  2. package/dist/chunks/{archive-oa-FCHLCPHC.js → archive-oa-BRZKKTO4.js} +2 -2
  3. package/dist/chunks/{asset-RLIHC7DB.js → asset-L7D4FAOZ.js} +2 -2
  4. package/dist/chunks/{asset-idle-KVOFTLCP.js → asset-idle-DADLLV75.js} +2 -2
  5. package/dist/chunks/{attachment-content-RZYDB3MB.js → attachment-content-K4C7VLWQ.js} +3 -3
  6. package/dist/chunks/{blog-42OZTJNP.js → blog-BVGQ7NZL.js} +2 -2
  7. package/dist/chunks/{caizhilian-KRK547K5.js → caizhilian-SAADNFD6.js} +2 -2
  8. package/dist/chunks/{calendar-RTWAMKHD.js → calendar-Q7NQY5Z2.js} +61 -23
  9. package/dist/chunks/{chunk-LFMSDBZN.js → chunk-3KTFS4IA.js} +308 -153
  10. package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
  11. package/dist/chunks/{chunk-T6EGVBKU.js → chunk-4RAE6JXP.js} +1 -1
  12. package/dist/chunks/{chunk-7BRU4EFH.js → chunk-72CKAUAG.js} +29 -2
  13. package/dist/chunks/{chunk-NETCCARD.js → chunk-APVFJUQH.js} +2 -2
  14. package/dist/chunks/{chunk-CAQNBBQA.js → chunk-BBKDXQEC.js} +2 -2
  15. package/dist/chunks/{chunk-57IECMWX.js → chunk-BIPYLLVS.js} +26 -12
  16. package/dist/chunks/{chunk-LN447PY2.js → chunk-EKWAQSLU.js} +3 -2
  17. package/dist/chunks/{chunk-4TEDNCON.js → chunk-FOAA27U5.js} +338 -52
  18. package/dist/chunks/{chunk-7U577Q7U.js → chunk-G4UR6NXN.js} +2 -2
  19. package/dist/chunks/{chunk-NJKREDA3.js → chunk-GEWYMQME.js} +1 -1
  20. package/dist/chunks/{chunk-I566ZYNF.js → chunk-GHXCDPTM.js} +1 -1
  21. package/dist/chunks/{chunk-4CTESSKI.js → chunk-IZ7S7QUS.js} +4 -4
  22. package/dist/chunks/{chunk-VNBU55SL.js → chunk-JPRO6ER2.js} +57 -2432
  23. package/dist/chunks/{chunk-IZSSVYAG.js → chunk-LADPYOWC.js} +2 -2
  24. package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
  25. package/dist/chunks/chunk-NKZRQXST.js +2546 -0
  26. package/dist/chunks/{chunk-JWRZM2H2.js → chunk-QKNKR4RH.js} +3 -3
  27. package/dist/chunks/{chunk-FLS4DPGP.js → chunk-R4CHUSKC.js} +28 -15
  28. package/dist/chunks/{chunk-WFFNM65B.js → chunk-SA6VBIHY.js} +73 -19
  29. package/dist/chunks/{chunk-SPRHHSVC.js → chunk-TTPJJJC4.js} +2 -2
  30. package/dist/chunks/{chunk-7E6MUOEK.js → chunk-W7BEHOSR.js} +2 -2
  31. package/dist/chunks/{chunk-CXRQWOZ5.js → chunk-YRSZQUD2.js} +2 -2
  32. package/dist/chunks/{conflict-VR55AOOO.js → conflict-F2OMZRTO.js} +2 -2
  33. package/dist/chunks/{contract-HI4FNPYE.js → contract-5BETHRSG.js} +2 -2
  34. package/dist/chunks/{cowork-HJLI353D.js → cowork-3MN6EQQO.js} +22 -12
  35. package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
  36. package/dist/chunks/{ebuilder-form-HTGI63UU.js → ebuilder-form-SBHOYBUD.js} +3 -3
  37. package/dist/chunks/{ehr-GYXIYH3A.js → ehr-KMOJULKG.js} +432 -1234
  38. package/dist/chunks/{env-DW3G4HUO.js → env-LB7NBL2O.js} +2 -2
  39. package/dist/chunks/{esb-I5INCEFE.js → esb-W2PJCFCX.js} +4 -4
  40. package/dist/chunks/{factory-TKXGXS6L.js → factory-6ZFQXQ46.js} +1 -1
  41. package/dist/chunks/{fna-K2MHTQ24.js → fna-ZQPCUV4R.js} +2 -2
  42. package/dist/chunks/{group-chat-VYBNMJLG.js → group-chat-2HHAJN46.js} +5 -3
  43. package/dist/chunks/{host-ZUQTJYYK.js → host-4CBHGOHT.js} +3 -3
  44. package/dist/chunks/{host-D5SERMAJ.js → host-ATKVNZAT.js} +3 -3
  45. package/dist/chunks/{host-JM4YVHFM.js → host-BQH22NTQ.js} +3 -3
  46. package/dist/chunks/{host-L3VLITMT.js → host-X2CSLFM6.js} +3 -3
  47. package/dist/chunks/{host-256GVCAH.js → host-YTODKMSL.js} +3 -3
  48. package/dist/chunks/{hrm-VLRBKTZB.js → hrm-FFM4UJFS.js} +4 -4
  49. package/dist/chunks/{im-QK3VTLXK.js → im-VEHKDQPB.js} +7 -7
  50. package/dist/chunks/{invoice-T5JTA32H.js → invoice-LVIKSIIF.js} +3 -3
  51. package/dist/chunks/{jiuchuanhui-P2FNEQVZ.js → jiuchuanhui-X6DINO5P.js} +2 -2
  52. package/dist/chunks/{lazy-dispatch-XRRI46M6.js → lazy-dispatch-KSWTN5L6.js} +4 -4
  53. package/dist/chunks/{mail-3Y7MJ6PS.js → mail-VDKRGUD3.js} +2 -2
  54. package/dist/chunks/{meeting-M7LTXGJE.js → meeting-CLC6HP3Z.js} +12 -11
  55. package/dist/chunks/{okr-OBC6S67L.js → okr-CHG4OO3U.js} +2 -2
  56. package/dist/chunks/{plan-5H6I53SK.js → plan-XWR75667.js} +2 -2
  57. package/dist/chunks/{project-L75QZV4S.js → project-5BXOOKMG.js} +40 -23
  58. package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
  59. package/dist/chunks/{registry-RXPN6BBT.js → registry-7FFOXIAW.js} +4 -4
  60. package/dist/chunks/{registry-KMHCPAAD.js → registry-BKBZPBAJ.js} +10 -9
  61. package/dist/chunks/{registry-QGHFNUWS.js → registry-OZSMDVPP.js} +2 -2
  62. package/dist/chunks/{registry-UGRBCBHO.js → registry-RHMW6ANW.js} +2 -2
  63. package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
  64. package/dist/chunks/{room-recommend-A3UDC3CA.js → room-recommend-62H7ZQQ6.js} +4 -4
  65. package/dist/chunks/{schedule-5GCACCK7.js → schedule-FNXKH3DQ.js} +4 -4
  66. package/dist/chunks/{workflow-M5ZJ7DWF.js → workflow-ADHKCAJK.js} +6 -6
  67. package/dist/index.js +33 -28
  68. package/dist/internal/e10/auth/commands.js +1 -0
  69. package/dist/internal/e10/auth/messages.js +6 -0
  70. package/dist/internal/e10/auth/session.js +17 -0
  71. package/dist/internal/e10/auth/xiaoe.js +286 -44
  72. package/dist/internal/e10/auth-recovery.js +36 -5
  73. package/dist/internal/e10/context.js +56 -12
  74. package/dist/internal/e10/index.js +1 -1
  75. package/dist/internal/e10/network-error.js +93 -0
  76. package/dist/internal/e10/request-runtime.js +6 -0
  77. package/dist/shortcuts/calendar/operations/read.js +31 -22
  78. package/dist/shortcuts/calendar/operations/write.js +45 -1
  79. package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
  80. package/dist/shortcuts/cowork/operations/item-write.js +1 -7
  81. package/dist/shortcuts/cowork/operations/member-write.js +3 -1
  82. package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
  83. package/dist/shortcuts/cowork/operations/shared.js +7 -0
  84. package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
  85. package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
  86. package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
  87. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
  88. package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
  89. package/dist/shortcuts/ehr/operations/recruit.js +10 -4
  90. package/dist/shortcuts/ehr/operations/shared.js +47 -0
  91. package/dist/shortcuts/hrm/operations/org.js +118 -0
  92. package/dist/shortcuts/index.js +5 -0
  93. package/dist/shortcuts/meeting/operations/eb/catalog.js +4 -1
  94. package/dist/shortcuts/meeting/operations/eb/create-flow.js +85 -28
  95. package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +25 -3
  97. package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +33 -11
  99. package/dist/shortcuts/project/operations/read.js +6 -25
  100. package/dist/shortcuts/project/operations/shared.js +22 -0
  101. package/dist/shortcuts/project/operations/write.js +28 -2
  102. package/dist/shortcuts/recruit-stage/context.js +119 -0
  103. package/dist/shortcuts/recruit-stage/continuation.js +73 -0
  104. package/dist/shortcuts/recruit-stage/errors.js +65 -0
  105. package/dist/shortcuts/recruit-stage/host.js +109 -0
  106. package/dist/shortcuts/recruit-stage/index.js +115 -0
  107. package/dist/shortcuts/recruit-stage/manifest.js +29 -0
  108. package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
  109. package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
  110. package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
  111. package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
  112. package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
  113. package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
  114. package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
  115. package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
  116. package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
  117. package/dist/shortcuts/recruit-stage/shared.js +608 -0
  118. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +13 -6
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
  122. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +50 -8
  123. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +2 -1
  124. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
  125. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +7 -39
  126. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +51 -15
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +39 -14
  132. package/docs/e10-auth.md +7 -1
  133. package/docs/operation-manual.md +8 -0
  134. package/package.json +1 -1
  135. package/scripts/install-skill-to-xiaoe.mjs +105 -0
  136. package/scripts/package-skill.mjs +4 -2
  137. package/skill-template/business-info.json +24 -0
  138. package/skill-template/domains/recruit-stage.md +29 -0
  139. package/skills/weaver-e10-calendar/SKILL.md +2 -1
  140. package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
  141. package/skills/weaver-e10-cowork/SKILL.md +1 -1
  142. package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
  143. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  144. package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
  145. package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
  146. package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
  147. package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
  148. package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
  149. package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
  150. package/skills/weaver-e10-mail/SKILL.md +1 -1
  151. package/skills/weaver-e10-meeting/SKILL.md +32 -2
  152. package/skills/weaver-e10-meeting.zip +0 -0
  153. package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
  154. package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
  155. package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
  156. package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
  157. package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
  158. package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
  159. package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
  160. package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
  161. package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
  162. package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
  163. package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
  164. package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
  165. package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
  166. package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
  167. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
  168. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
  169. package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
  170. package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
  171. package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
  172. package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
  173. package/skills/weaver-e10-shared/SKILL.md +1 -1
  174. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
  175. package/skills/weaver-e10-shijingran/SKILL.md +3 -2
  176. package/skills/weaver-e10-shijingran/references/operations.md +3 -1
  177. package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
  178. package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
  179. package/skills/weaver-e10-workflow/SKILL.md +4 -2
  180. package/skills/weaver-e10-workflow/aigroup/default.json +11 -9
  181. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +23 -13
  182. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +62 -20
  183. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
  184. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
  185. package/dist/chunks/{chunk-E7OK5WD5.js → chunk-2LV74IWP.js} +5 -5
@@ -0,0 +1,608 @@
1
+ /**
2
+ * recruit-stage 共享写操作库(自技能版 scripts/write-op.mjs 1:1 移植)。
3
+ *
4
+ * 封装 EB 写操作公共能力:表单/布局解析、saveFormData、runEsb、按钮配置解析
5
+ * (getFormButtons/getListButtons)。全部运行时解析,不写死 esbActionIds/表单 id;
6
+ * 认证由 CLI 宿主注入(host.request),环境上下文经 recruit-stage/context.ts 加载。
7
+ *
8
+ * 与脚本版的机制差异(其余逻辑逐行对应):
9
+ * - 请求层:脚本版 request({path,body,...}) → host.request({path,body,...});
10
+ * 401 判定/续期由 CLI 运行时统一处理;响应大整数已被 parseJsonPreservingBigInts
11
+ * 加引号保精度(脚本版"从原文提取 formDataIds"的兜底不再需要,但保留兼容读取)。
12
+ * - 环境缓存:脚本版 ensureEnvCache(<appId>.json)→ 复用 ehr 模块 RecruitContextLoader
13
+ * (同构五层解析,磁盘缓存目录一致,可直接命中脚本版播种的缓存)。
14
+ */
15
+ import { randomUUID } from 'node:crypto';
16
+ import { rsFail } from './errors.js';
17
+ export function randHex32() {
18
+ return randomUUID().replace(/-/g, '');
19
+ }
20
+ // 表单 objId(tag 解析,缓存优先)
21
+ export function objIdOf(cache, key) {
22
+ const fo = cache.forms[key];
23
+ if (!fo?.objId)
24
+ throw rsFail('环境缓存缺少表单 ' + key + ' 的 objId(该表单可能未启用)');
25
+ return String(fo.objId);
26
+ }
27
+ export function formIdOf(cache, key) {
28
+ const fo = cache.forms[key];
29
+ if (!fo?.formId)
30
+ return objIdOf(cache, key);
31
+ return String(fo.formId);
32
+ }
33
+ // 表单布局完整数据:GET 优先,失败回退 POST(部分版本需 POST + module 参数,缺 module 会 500)
34
+ // 进程内 memo:同 formId 同进程只拉一次(getFormLayout/getFormLayoutMeta 共享);失败即剔除可重试
35
+ const __layoutMemo = new Map();
36
+ function fetchLayoutData(host, formId) {
37
+ if (!__layoutMemo.has(formId)) {
38
+ const p = fetchLayoutDataRaw(host, formId).catch((e) => { __layoutMemo.delete(formId); throw e; });
39
+ __layoutMemo.set(formId, p);
40
+ }
41
+ return __layoutMemo.get(formId);
42
+ }
43
+ async function fetchLayoutDataRaw(host, formId) {
44
+ try {
45
+ const r = await host.request({ path: '/api/ebuilder/form/form/core/getFormLayout?formId=' + formId, method: 'GET' });
46
+ if (r?.data)
47
+ return r.data;
48
+ }
49
+ catch { /* 回退 POST */ }
50
+ try {
51
+ const r = await host.request({
52
+ path: '/api/ebuilder/form/form/core/getFormLayout',
53
+ method: 'POST',
54
+ body: { module: 'ebuilderform', formId, terminalType: 'PC' },
55
+ });
56
+ if (r?.data)
57
+ return r.data;
58
+ }
59
+ catch { /* ignore */ }
60
+ return null;
61
+ }
62
+ // 表单布局 id:布局数据 formLayout.id;失败回退 formId
63
+ export async function getFormLayout(host, formId) {
64
+ const d = await fetchLayoutData(host, formId);
65
+ return d?.formLayout?.id || d?.id || formId;
66
+ }
67
+ // 表单布局完整元数据:viewRelateConfig(字段级 required/readOnly)+ viewLinkages(条件必填/显隐)
68
+ export async function getFormLayoutMeta(host, formId) {
69
+ return fetchLayoutData(host, formId);
70
+ }
71
+ // 表单类 esb 按钮解析:POST getFormButtons(body objId[, layoutId]) → 扁平 actions[](带 _btn=按钮名)
72
+ export async function getFormButtons(host, objId, layoutId) {
73
+ const body = {
74
+ layoutType: 'formLayout', objId, dataId: '', formDataId: '',
75
+ type: '1', fromRecycle: false, otherOperator: '', objectAppId: '',
76
+ browser: 'Chrome', isTamper: '0',
77
+ };
78
+ if (layoutId)
79
+ body.layoutId = layoutId;
80
+ const r = await host.request({ path: '/api/ebuilder/form/data/getFormButtons', body });
81
+ const btns = r?.data?.buttons || r?.data || [];
82
+ const actions = [];
83
+ for (const b of btns) {
84
+ for (const a of b?.actions || []) {
85
+ actions.push({ ...a, _btn: b.name, _buttonKey: b.buttonKey, _buttonId: b.id, _buttonOrder: b.showOrder });
86
+ }
87
+ }
88
+ return actions;
89
+ }
90
+ // 取「保存」按钮下的**已启用**动作链(按 showOrder 升序)——页面点「保存」时依次执行的就是这一串。
91
+ // 跨环境自适应:按钮 id / 动作 id / 动作数量 / 名称均运行时解析,不写死。
92
+ // enable=0 的动作页面不会执行(实测:人才表单 7 条动作里 3 条为禁用态),一律排除。
93
+ export function pickSaveButtonActions(actions, { includeDisabled = false } = {}) {
94
+ const all = actions || [];
95
+ let own = all.filter((a) => a._buttonKey === 'save');
96
+ if (!own.length) {
97
+ const names = new Set(all.filter((a) => a.actionType === 'system' && a._btn).map((a) => a._btn));
98
+ own = all.filter((a) => a._btn && names.has(a._btn));
99
+ }
100
+ const sorted = [...own].sort((x, y) => (x.showOrder ?? 0) - (y.showOrder ?? 0));
101
+ if (includeDisabled)
102
+ return sorted;
103
+ return sorted.filter((a) => a.enable === undefined || a.enable === null || Number(a.enable) !== 0);
104
+ }
105
+ // 列表类 esb 按钮解析(催评):GET getListButtons/{listId}?passid → buttons[].actions[]
106
+ export async function getListButtons(host, listId, passid) {
107
+ const r = await host.request({ path: '/api/ebuilder/form/list/getListButtons/' + listId + '?passid=' + passid, method: 'GET' });
108
+ const btns = r?.data?.buttons || r?.data || [];
109
+ const actions = [];
110
+ for (const b of btns)
111
+ for (const a of b?.actions || [])
112
+ actions.push({ ...a, _btn: b.name });
113
+ return actions;
114
+ }
115
+ // 从 actions 里挑 actionType=esb 且名称匹配(含关键词、排除词)的按钮 id
116
+ export function pickEsb(actions, include, exclude = []) {
117
+ const match = (name) => {
118
+ if (!name)
119
+ return false;
120
+ if (exclude.some((e) => name.includes(e)))
121
+ return false;
122
+ return include.some((i) => name.includes(i));
123
+ };
124
+ for (const a of actions) {
125
+ if (a.actionType === 'esb' && match(a.name || a._btn || a.text))
126
+ return a.id;
127
+ }
128
+ return null;
129
+ }
130
+ // saveFormData:新建(rightType 0)或更新(rightType 2,带 formDataId/updateTimeStamp)
131
+ export async function saveFormData(host, opts) {
132
+ const { appId, formId, objId, name, dataDetails, rightType = '0', formDataId, updateTimeStamp, layoutId, extraCustomParam = {} } = opts;
133
+ const finalLayout = layoutId || await getFormLayout(host, formId);
134
+ const customParam = {
135
+ objId, name: name || '', firstDataName: name || '',
136
+ isFlowEnd: false, otherOperator: '', dataStatus: 'submit', rightType,
137
+ appId, ...extraCustomParam,
138
+ };
139
+ const formData = {
140
+ module: 'ebuilderform', client: 'pc', dataStatus: 'submit',
141
+ form: { id: formId }, formLayout: { id: finalLayout },
142
+ };
143
+ if (formDataId)
144
+ formData.id = formDataId;
145
+ if (updateTimeStamp)
146
+ formData.updateTimeStamp = updateTimeStamp;
147
+ if (dataDetails)
148
+ formData.dataDetails = dataDetails;
149
+ const r = await host.request({
150
+ path: '/api/ebuilder/form/form/core/saveFormData?formId=' + formId,
151
+ body: { module: 'ebuilderform', appId, customParam, formData },
152
+ });
153
+ if (!r?.status && r?.code !== 200) {
154
+ throw rsFail('saveFormData 失败: ' + (r?.msg || JSON.stringify(r).slice(0, 200)));
155
+ }
156
+ return String(r?.data?.id);
157
+ }
158
+ // ── 招聘业务查重(页面新建人才时用的就是它)──────────────────────────────
159
+ // 页面代码块逻辑:手机号 → POST getCandidateByPhone → data.candidateId 有值即「已绑定人才」。
160
+ // 注意 candidateId 是**应聘批次 id**(= batchNo),不是 talentId;两者都返回。未命中时 data = {}。
161
+ export async function getCandidateByPhone(host, phone) {
162
+ const r = await host.request({ path: '/api/recruit/candidate/getCandidateByPhone', method: 'POST', body: { phone: String(phone) } });
163
+ const d = r?.data || {};
164
+ return {
165
+ talentId: d.talentId ? String(d.talentId) : '',
166
+ candidateId: d.candidateId ? String(d.candidateId) : '',
167
+ batchNo: d.batchNo ? String(d.batchNo) : '',
168
+ };
169
+ }
170
+ // 人才查看页(页面代码块弹窗跳转地址,recruitType=useSelf 表示租户人才库)
171
+ export function buildTalentViewUrl(baseUrl, candidateId) {
172
+ return `${baseUrl}/sp/recruit/talentView?batchId=${candidateId}&recruitType=useSelf`;
173
+ }
174
+ // 人才类链接的自用系统追加参数:环境缓存 selfUse(config_key=-9999)='1' → 追加 &recruitType=useSelf。
175
+ // 仅人才类链接需要;职位/面试等非人才链接不需要。
176
+ export function talentLinkExtra(cache) {
177
+ return String(cache && cache.selfUse) === '1' ? '&recruitType=useSelf' : '';
178
+ }
179
+ // ── 唯一性校验(页面新建/编辑时调用的查重接口)────────────────────────────────
180
+ // 返回 formDataIds = 已存在记录 id 列表;含已软删除记录(平台仍视为占用,会拦新建)。
181
+ // CLI 运行时已对 16+ 位整数保精度(字符串形态),此处仍保留正则兜底提取以兼容极端情况。
182
+ export async function checkUnique(host, opts) {
183
+ const { objId, fieldId, value, dataId } = opts;
184
+ const dataDetail = { content: String(value), formField: { id: String(fieldId) } };
185
+ const customParam = { objId: String(objId) };
186
+ if (dataId) {
187
+ dataDetail.formData = { id: String(dataId) };
188
+ customParam.dataId = String(dataId);
189
+ }
190
+ const r = await host.request({
191
+ path: '/api/ebuilder/form/form/core/checkUnique', method: 'POST',
192
+ body: { dataDetail, module: 'ebuilderform', customParam },
193
+ });
194
+ const d = r?.data || {};
195
+ let ids = [];
196
+ const rawList = d.formDataIds;
197
+ if (Array.isArray(rawList))
198
+ ids = rawList.map((x) => String(x)).filter((x) => /^\d{10,}$/.test(x));
199
+ if (!ids.length) {
200
+ // 兜底:宿主未保精度时从原文提取(与脚本版同构)
201
+ const text = typeof r?.__raw === 'string' ? r.__raw : '';
202
+ const m = text.match(/"formDataIds"\s*:\s*\[([^\]]*)\]/);
203
+ if (m)
204
+ ids = m[1].match(/\d{10,}/g) || [];
205
+ }
206
+ return { unique: d.unique !== false, formDataIds: [...new Set(ids)] };
207
+ }
208
+ // ── OpenAPI 数据集接口(唯一支持明细表的写入通道)────────────────────────────
209
+ // `form/core/saveFormData` 的 dataDetails 不接收 DetailTable(实测 9 种结构全被忽略),
210
+ // 明细表只能走 /api/ebuilder/form/dataset/v2/saveFormData(detail1..detailN)。
211
+ // 主表 ↔ 明细节点映射:{mainTable: mainObjId, detail1: subFormId, ...}
212
+ // 明细编号顺序 ≠ 布局顺序,必须运行时解析。
213
+ export async function getTableMapping(host, objId) {
214
+ const r = await host.request({ path: '/api/ebuilder/form/dataset/v1/getTableMapping?objId=' + encodeURIComponent(objId), method: 'GET' });
215
+ const d = r?.data;
216
+ if (!d || !d.mainTable)
217
+ throw rsFail('getTableMapping 未返回映射(objId=' + objId + '):' + JSON.stringify(r).slice(0, 200));
218
+ return d;
219
+ }
220
+ // 反向映射:subFormId → detailN
221
+ export function invertTableMapping(mapping) {
222
+ const out = {};
223
+ for (const [node, tableId] of Object.entries(mapping || {})) {
224
+ if (node === 'mainTable')
225
+ continue;
226
+ out[String(tableId)] = node;
227
+ }
228
+ return out;
229
+ }
230
+ // OpenAPI 新增(主表 + 明细一次写入)。键必须用 config.dataKey(name 可能等于 id,会被静默忽略)。
231
+ export async function datasetSaveFormData(host, opts) {
232
+ const { objId, mainTable, details, doAction = false, fieldNoFindIgnore = true } = opts;
233
+ const record = { mainTable };
234
+ for (const [node, rows] of Object.entries(details || {})) {
235
+ if (Array.isArray(rows) && rows.length)
236
+ record[node] = rows;
237
+ }
238
+ const r = await host.request({
239
+ path: '/api/ebuilder/form/dataset/v2/saveFormData',
240
+ method: 'POST',
241
+ body: {
242
+ datajson: {
243
+ datas: [record],
244
+ header: { objId },
245
+ operationinfo: {
246
+ fieldNoFindIgnore: fieldNoFindIgnore ? 'true' : 'false',
247
+ doAction: doAction ? 'true' : 'false',
248
+ },
249
+ },
250
+ },
251
+ });
252
+ const dj = r?.data?.datajson || {};
253
+ const code = r?.message?.code;
254
+ const ok = (code === undefined || String(code) === '0') && dj.status !== false;
255
+ if (!ok) {
256
+ // 业务失败信息在 data.datajson.message(如平台查重),外层 message.msg 恒为 success
257
+ throw rsFail('dataset saveFormData 失败:' + (dj.message || r?.message?.msg || JSON.stringify(r).slice(0, 300)));
258
+ }
259
+ const ids = Array.isArray(dj.dataIds) ? dj.dataIds.map(String) : [];
260
+ if (!ids.length)
261
+ throw rsFail('dataset saveFormData 未返回 dataIds:' + JSON.stringify(r).slice(0, 300));
262
+ return { dataId: ids[0], dataIds: ids };
263
+ }
264
+ // OpenAPI 修改(支持明细覆盖:operateType={detailN:'cover'})
265
+ export async function datasetUpdateFormData(host, opts) {
266
+ const { objId, dataId, mainTable, details, coverDetails = [] } = opts;
267
+ const record = { mainTable: { id: String(dataId), ...(mainTable || {}) } };
268
+ const operateType = {};
269
+ for (const [node, rows] of Object.entries(details || {})) {
270
+ if (!Array.isArray(rows))
271
+ continue;
272
+ record[node] = rows;
273
+ if (coverDetails.includes(node))
274
+ operateType[node] = 'cover';
275
+ }
276
+ const operationinfo = { updateType: 'ids', fieldNoFindIgnore: 'true', needAdd: 'false', doAction: 'false' };
277
+ if (Object.keys(operateType).length)
278
+ operationinfo.operateType = operateType;
279
+ const r = await host.request({
280
+ path: '/api/ebuilder/form/dataset/v2/updateFormData',
281
+ method: 'POST',
282
+ body: { datajson: { datas: [record], header: { objId }, operationinfo } },
283
+ });
284
+ const dj = r?.data?.datajson || {};
285
+ const ok = (r?.message?.code === undefined || String(r?.message?.code) === '0') && dj.status !== false;
286
+ if (!ok)
287
+ throw rsFail('dataset updateFormData 失败:' + (dj.message || r?.message?.msg || JSON.stringify(r).slice(0, 300)));
288
+ return { dataIds: (dj.dataIds || []).map(String) };
289
+ }
290
+ // runEsb:触发动作流(dataIds = 目标记录 id;esbActionIds = 按钮配置解析)
291
+ export async function runEsb(host, opts) {
292
+ const r = await host.request({
293
+ path: '/api/ebuilder/form/esb/runEsb?objId=' + opts.objId,
294
+ body: { dataIds: opts.dataIds, esbActionIds: opts.esbActionIds, objId: opts.objId },
295
+ });
296
+ return r?.data || {};
297
+ }
298
+ // 通用:执行「保存」按钮配置里的 esb 动作流。没有配 esb 时返回空数组(不报错)。
299
+ export async function runSaveButtonEsbActions(host, opts) {
300
+ const { objId, formId, dataId } = opts;
301
+ const layoutId = await getFormLayout(host, formId);
302
+ const acts = pickSaveButtonActions(await getFormButtons(host, objId, layoutId));
303
+ const done = [];
304
+ for (const a of acts) {
305
+ if (a.actionType !== 'esb')
306
+ continue;
307
+ const r = await runEsb(host, { objId, dataIds: dataId, esbActionIds: a.id });
308
+ const ok = r?.status !== false;
309
+ done.push({ order: a.showOrder, name: a.name, ok, msg: r?.successMsg || r?.failMsg || '' });
310
+ if (!ok)
311
+ throw rsFail('「保存」后续动作「' + (a.name || a.id) + '」执行失败:' + (r?.failMsg || '未知原因'));
312
+ }
313
+ return done;
314
+ }
315
+ /**
316
+ * 当前登录用户(跨环境取法:teamsCheck 返回 currentUser)。
317
+ * 用途:人员型字段的"默认 = 当前人"(招聘负责人/用人经理/面试官),
318
+ * 避免用"人员候选列表第一条"当默认(可能是任意人)。
319
+ */
320
+ export async function resolveCurrentUser(host) {
321
+ try {
322
+ const r = await host.request({ path: '/api/baseserver/layout/teamsCheck', method: 'POST', body: {} });
323
+ const cu = (r && (r.currentUser || (r.data && r.data.currentUser))) || null;
324
+ return cu && cu.id ? { id: String(cu.id), name: cu.username || String(cu.id) } : null;
325
+ }
326
+ catch {
327
+ return null;
328
+ }
329
+ }
330
+ // 人员名称 → userId(门户字段 browser/complete/resource)
331
+ export async function resolvePersonId(host, fieldId, keyword) {
332
+ const r = await host.request({
333
+ path: '/api/ebuilder/form/common/browser/complete/resource',
334
+ body: { formParam: { fieldId, searchValue: keyword }, 'formParam.fieldId': fieldId, 'formParam.searchValue': keyword },
335
+ });
336
+ const list = r?.data?.data || r?.data || [];
337
+ return list[0]?.id || list[0]?.value || null;
338
+ }
339
+ // ds/options 运行时解析枚举(面试方式/轮次/状态等):name → optionId
340
+ export async function getDsOptions(host, objId, fieldId, groupId) {
341
+ const r = await host.request({
342
+ path: '/api/ebuilder/common/ds/options?fieldName=' + fieldId + '&objId=' + objId + '&sourceType=FORM&groupId=' + groupId,
343
+ method: 'GET',
344
+ });
345
+ const list = r?.data || [];
346
+ const map = {};
347
+ for (const x of list)
348
+ map[x.name] = x.id;
349
+ return map;
350
+ }
351
+ // 环境缓存 field_options({optionId: 名称})反转成 getDsOptions 同款 {名称: optionId};未覆盖返回 null 由调用方回源
352
+ export function cachedDsMap(cache, objId, fieldKey) {
353
+ const fidv = (cache.fields[objId] || {})[fieldKey];
354
+ const opts = fidv && cache.field_options?.[objId] && cache.field_options[objId][fidv];
355
+ if (!opts || !Object.keys(opts).length)
356
+ return null;
357
+ const map = {};
358
+ for (const [id, name] of Object.entries(opts))
359
+ map[name] = id;
360
+ return map;
361
+ }
362
+ /* ============ 按钮动作智能解析(数据页按钮 → 粗定位 → 精核验 → 动作拆解) ============ */
363
+ // 数据页按钮:POST getFormButtons(type=2,带 dataId)→ 返回按钮数组(含 actions)
364
+ export async function getFormButtonsForData(host, objId, formId, dataId, layoutId) {
365
+ const body = {
366
+ layoutType: 'formLayout', objId, dataId, formDataId: dataId,
367
+ type: '2', fromRecycle: false, otherOperator: '', objectAppId: '',
368
+ browser: 'Chrome', isTamper: '0',
369
+ };
370
+ if (layoutId)
371
+ body.layoutId = layoutId;
372
+ const r = await host.request({ path: '/api/ebuilder/form/data/getFormButtons', body });
373
+ const data = r?.data;
374
+ const btns = Array.isArray(data) ? data : (data?.buttons || []);
375
+ return btns;
376
+ }
377
+ // 粗定位:找「带保存动作」的按钮(actionType=system 或 buttonKey=editSave)
378
+ export function findSubmitButton(btns) {
379
+ for (const b of btns || []) {
380
+ const acts = b?.actions || [];
381
+ if (acts.some((a) => a.actionType === 'system') || b?.buttonKey === 'editSave')
382
+ return b;
383
+ }
384
+ return null;
385
+ }
386
+ // 从 jsCode 的 ecode(内联 JS)静态提取后端 API 调用(url/method/data 参数名)
387
+ export function extractJsRequests(ecode) {
388
+ if (!ecode)
389
+ return [];
390
+ const calls = [];
391
+ const re = /(?:weappUtils\.request|\$\.ajax|axios\.\w+|fetch)\s*\(\s*\{[\s\S]*?\}/g;
392
+ let m;
393
+ while ((m = re.exec(ecode))) {
394
+ const block = m[0];
395
+ const url = (block.match(/url\s*:\s*["']([^"']+)["']/) || [])[1];
396
+ if (!url)
397
+ continue;
398
+ const method = (block.match(/method\s*:\s*["']([^"']+)["']/) || [])[1] || 'post';
399
+ const data = {};
400
+ const dm = block.match(/data\s*:\s*\{([\s\S]*?)\}/);
401
+ if (dm) {
402
+ for (const kv of dm[1].split(',')) {
403
+ const kk = kv.match(/["']?([A-Za-z_][A-Za-z0-9_]*)["']?\s*:/);
404
+ if (kk)
405
+ data[kk[1]] = kv.split(':').slice(1).join(':').trim().slice(0, 60);
406
+ }
407
+ }
408
+ calls.push({ url, method, data });
409
+ }
410
+ return calls;
411
+ }
412
+ // 精核验:分析按钮全部 actions → { system, esbIds, jsCalls, names }
413
+ export function analyzeButtonActions(btn) {
414
+ const acts = btn?.actions || [];
415
+ const esbIds = acts.filter((a) => a.actionType === 'esb').map((a) => String(a.id));
416
+ const jsCalls = [];
417
+ for (const a of acts) {
418
+ if (a.actionType === 'jsCode' && a.ecode)
419
+ jsCalls.push(...extractJsRequests(a.ecode));
420
+ }
421
+ return {
422
+ system: acts.some((a) => a.actionType === 'system'),
423
+ esbIds,
424
+ jsCalls,
425
+ names: acts.map((a) => (a.actionType === 'jsCode' ? `jsCode(${a.name})` : `${a.actionType}(${a.name})`)),
426
+ };
427
+ }
428
+ // 取数据页表单记录(含 updateTimeStamp 乐观锁 + dataDetails 全字段)
429
+ export async function getFormDataRecord(host, formId, dataId, objId, layoutId) {
430
+ const r = await host.request({
431
+ path: '/api/ebuilder/form/form/core/getFormDataAndLayout?formId=' + formId + '&dataId=' + dataId,
432
+ method: 'POST',
433
+ body: {
434
+ terminalType: 'PC', dataId, formId, module: 'ebuilderform', newLayout: true,
435
+ cacheUid: '', layoutMultiId: '', dataCrossLayout: false, permissionId: '',
436
+ allReadOnly: false, initialDatas: [], dynamicData: false, cost: false,
437
+ ...(layoutId ? { layoutId } : {}),
438
+ customParam: { objId },
439
+ },
440
+ });
441
+ const fd = r?.data?.formData || {};
442
+ const fl = r?.data?.formLayout || {};
443
+ return { id: fd.id, updateTimeStamp: fd.updateTimeStamp, dataDetails: fd.dataDetails || [], formLayoutId: fl.id };
444
+ }
445
+ // dataDetails 转换:getFormDataAndLayout 返回 {fieldId, dataOptions|dataText} → saveFormData 提交形态
446
+ export function toSubmitDetails(details) {
447
+ const out = [];
448
+ for (const x of details || []) {
449
+ const fid = x.fieldId || x.formField?.id;
450
+ if (!fid)
451
+ continue;
452
+ const o = { formField: { id: fid } };
453
+ if (x.dataOptions)
454
+ o.dataOptions = x.dataOptions;
455
+ if (x.dataText)
456
+ o.dataText = x.dataText;
457
+ if (x.value !== undefined)
458
+ o.dataText = { content: String(x.value) };
459
+ if (!o.dataOptions && !o.dataText)
460
+ continue;
461
+ out.push(o);
462
+ }
463
+ return out;
464
+ }
465
+ /**
466
+ * 跨环境菜单解析(同一功能在不同环境叫法不同)。
467
+ * 匹配顺序:① 精确名 ② 包含名;均可叠加 filter 校验。
468
+ * 未命中返回 null —— 调用方应视为「该环境无此菜单」并提示,不得臆造 URL。
469
+ */
470
+ export function findMenuNode(menus, aliases, filter) {
471
+ const list = Array.isArray(menus) ? menus : [];
472
+ const nameOf = (m) => String((m && m.menuName) || '').trim();
473
+ const ok = (m) => (typeof filter === 'function' ? !!filter(m) : true);
474
+ for (const a of aliases || []) {
475
+ const hit = list.find((m) => nameOf(m) === a && ok(m));
476
+ if (hit)
477
+ return hit;
478
+ }
479
+ for (const a of aliases || []) {
480
+ const hit = list.find((m) => nameOf(m).includes(a) && ok(m));
481
+ if (hit)
482
+ return hit;
483
+ }
484
+ return null;
485
+ }
486
+ /**
487
+ * 由表单布局元数据推导「当前取值下必填、但未提供且无默认值」的字段(纯函数)。
488
+ * - 静态必填:viewRelateConfig[].required
489
+ * - 条件必填:viewLinkages[] 中条件只涉及「面试方式」一维时,命中当前取值则 required 生效
490
+ * - 排除 defaultDetails 里带默认值的字段
491
+ *
492
+ * 枚举改名/换 id 的兼容(实测坑):
493
+ * ① 条件 optionId 在当前枚举里 → 严格按当前枚举名比较;
494
+ * ② 条件 optionId 不在当前枚举 → 按条件文本去括号后比较(AI面试(数字人) → AI面试)。
495
+ */
496
+ export function computeRequiredHint(opts) {
497
+ const { meta, fields, typeName, typeOptionId, typeOptions, dataDetails } = opts;
498
+ const rev = {};
499
+ for (const [dk, fid] of Object.entries(fields || {}))
500
+ rev[String(fid)] = dk;
501
+ const nm = (fid) => rev[String(fid)] || String(fid);
502
+ const curName = (id) => (typeOptions || {})[String(id)] || '';
503
+ const norm = (s) => String(s || '').replace(/[((][^))]*[))]/g, '').trim();
504
+ const matchTypeCondition = (cd) => {
505
+ if (!cd)
506
+ return false;
507
+ let optId = '';
508
+ let optText = '';
509
+ try {
510
+ const sj = JSON.parse(cd.sourceContent || '{}');
511
+ const o = (sj.dataOptions || [])[0];
512
+ if (o) {
513
+ optId = String(o.optionId || '');
514
+ optText = String(o.content || '');
515
+ }
516
+ if (!optText)
517
+ optText = String(sj.content || '');
518
+ }
519
+ catch { /* ignore */ }
520
+ const actual = curName(optId);
521
+ let eq;
522
+ if (actual) {
523
+ eq = actual === typeName;
524
+ }
525
+ else {
526
+ eq = !/[((]/.test(String(typeName || '')) && norm(optText) === norm(typeName);
527
+ }
528
+ if (cd.detailSymbol === 'UN_EQUAL')
529
+ return !eq;
530
+ if (cd.detailSymbol === 'IN') {
531
+ const names = optText.split(',').map((s) => norm(s));
532
+ return names.includes(norm(typeName)) || Boolean(typeOptionId && optId === String(typeOptionId));
533
+ }
534
+ return eq;
535
+ };
536
+ const required = new Set();
537
+ for (const x of meta?.viewRelateConfig || []) {
538
+ if (x.required)
539
+ required.add(nm(x.fieldId));
540
+ }
541
+ const typeFid = String((fields || {}).interview_type || '');
542
+ for (const lk of meta?.viewLinkages || []) {
543
+ const details = (lk.condition || {}).conditionDetails || [];
544
+ if (!details.length)
545
+ continue;
546
+ let hit = true;
547
+ for (const cd of details) {
548
+ if (!typeFid || String(cd.targetId) !== typeFid) {
549
+ hit = false;
550
+ break;
551
+ }
552
+ if (!matchTypeCondition(cd)) {
553
+ hit = false;
554
+ break;
555
+ }
556
+ }
557
+ if (!hit)
558
+ continue;
559
+ for (const ld of lk.linkageDetails || []) {
560
+ if (ld.linkageProp === 'required') {
561
+ for (const v of String(ld.linkageValue || '').split(','))
562
+ if (v.trim())
563
+ required.add(nm(v.trim()));
564
+ }
565
+ }
566
+ }
567
+ const filled = new Set((dataDetails || []).map((d) => nm((d.formField || {}).id)));
568
+ const defaults = new Set((meta?.defaultDetails || []).map((x) => nm(x.fieldId)));
569
+ const defaultValues = {};
570
+ for (const x of meta?.defaultDetails || []) {
571
+ const dk = nm(x.fieldId);
572
+ const o = (x.dataOptions || [])[0];
573
+ defaultValues[dk] = o
574
+ ? { optionId: String(o.optionId || ''), content: o.content || '' }
575
+ : { optionId: '', content: x.content || '' };
576
+ }
577
+ let missing = [...required].filter((k) => !filled.has(k) && !defaults.has(k));
578
+ // 业务裁定(2026-10-21 用户确认):「AI面试」= 数字人面试;「AI面试(小程序)」才是试卷+渠道形态。
579
+ // 对「AI面试」显式剔除这两项(required 原样保留以反映配置事实,只避免误报缺失)。
580
+ const DIGITAL_EXCLUDE = ['interview_paper_id', 'send_channel'];
581
+ const isDigitalHuman = String(typeName || '').trim() === 'AI面试';
582
+ let excludedByBusiness = [];
583
+ if (isDigitalHuman) {
584
+ excludedByBusiness = missing.filter((k) => DIGITAL_EXCLUDE.includes(k));
585
+ missing = missing.filter((k) => !DIGITAL_EXCLUDE.includes(k));
586
+ }
587
+ return { available: true, required: [...required], missing, hasDefault: [...defaults], defaultValues, excludedByBusiness };
588
+ }
589
+ /**
590
+ * EB 记录「卡片单页」URL:{baseUrl}/sp/ebdfpage/card/0/{objId}/{dataId}
591
+ * objId 来自环境缓存(跨环境不同,不得写死);dataId = saveFormData 返回的记录 id。
592
+ */
593
+ export function buildCardUrl(baseUrl, objId, dataId) {
594
+ if (!baseUrl || !objId || !dataId)
595
+ return null;
596
+ return String(baseUrl).replace(/\/+$/, '') + '/sp/ebdfpage/card/0/' + objId + '/' + dataId;
597
+ }
598
+ /**
599
+ * 「列表 + 滑出该记录卡片」组合地址(2026-10-21 实测可用,简化版无需
600
+ * EbdfListToCardKey/targetConditionId——它们是页面会话态/已存条件 id,运行时组不出来也不需要):
601
+ * {base}/sp/ebdfpage/list/{listId}/card/0/{objId}/{dataId}/{listId}?id={dataId}&openSource=field&openSourceId={dataId}&openSourceAppId={appId}
602
+ */
603
+ export function buildListLocateUrl(baseUrl, appId, listId, objId, dataId) {
604
+ if (!baseUrl || !listId || !objId || !dataId)
605
+ return null;
606
+ const base = String(baseUrl).replace(/\/+$/, '');
607
+ return `${base}/sp/ebdfpage/list/${listId}/card/0/${objId}/${dataId}/${listId}?id=${dataId}&openSource=field&openSourceId=${dataId}&openSourceAppId=${appId}`;
608
+ }
@@ -89,14 +89,16 @@ export function pickInternalMessageContentField(fields) {
89
89
  *
90
90
  * `留言内容` 为空 ⇒ 返回空数组:这是「表单没有业务内容」,不是「只有发起人」。
91
91
  */
92
- export function pickInternalMessageFieldPairs(fields, basicInfo = {}) {
92
+ export function pickInternalMessageFieldPairs(fields, basicInfo = {},
93
+ /** 兜底发起人(2026-11-03:汇总链路已不再保留 basic,改由列表行的 createrName 传入) */
94
+ fallbackCreatorName) {
93
95
  const content = pickInternalMessageContentField(fields);
94
96
  const contentValue = String(content?.value ?? '').trim();
95
97
  if (!contentValue)
96
98
  return [];
97
99
  const name = normalizeDetailFieldName(content?.fieldName) || INTERNAL_MESSAGE_CONTENT_LABEL;
98
100
  const pairs = [{ fieldName: name, value: contentValue }];
99
- const creatorName = String(basicInfo.creatorName ?? '').trim();
101
+ const creatorName = String(basicInfo.creatorName ?? fallbackCreatorName ?? '').trim();
100
102
  if (creatorName)
101
103
  pairs.push({ fieldName: INTERNAL_MESSAGE_INITIATOR_LABEL, value: creatorName });
102
104
  const createdAt = String(basicInfo.createdAt ?? '').trim();
@@ -125,16 +125,20 @@ export function promptTagOf(prompt) {
125
125
  /**
126
126
  * 缓存条目是否与当前口径/素材匹配。
127
127
  *
128
- * **失效条件(2026-11-02 用户定稿):只由 `requestSummaryPrompt`(default.json / 用户意图)调整触发**——
129
- * - `pk`:双方都存在时才比对;**条目缺 pk = 旧口径条目,兼容命中**(无法证明口径变过就不作废);
128
+ * **失效条件:由 `requestSummaryPrompt`(default.json / 用户意图)调整触发**——
129
+ * - `pk`:**严格比对**(2026-11-04 修正)。调用方只要传了 pk,条目缺 pk 或 pk 不等 ⇒ 一律失效重写。
130
+ * 旧口径(`entry.pk !== undefined` 才比对)等于「改 requestSummaryPrompt 对老条目永远不生效」:
131
+ * 条目是用旧口径写的,缺 pk 只能说明「口径未知」,不能证明「口径没变」。
132
+ * 空口径(`pk === ''`)会照常落库,因此「改空 / 改回非空」同样能触发失效,且空口径组不会抖动。
130
133
  * - `maxChars`:双方都存在时才比对,且调用方只在用户**显式**传字数时才传入
131
134
  * (`summary.ts` 的 `explicitMaxCharsFor`)——规则/默认字数变化不作废历史摘要;
132
135
  * 用户显式要求「10 字以内」时仍会重写(2026-10-22 实测踩坑的保护保留);
133
136
  * - `dv`:详情素材特征值仍比对——素材升级 / 排除清单调整后旧摘要缺信息(如安全工单缺行号),
134
- * 不能继续命中;条目缺 dv = 旧条目,兼容命中。
137
+ * 不能继续命中;条目缺 dv = 旧条目,兼容命中(素材侧无法核验,属另一条口径,未随本次收紧)。
135
138
  */
136
139
  function entryMatches(entry, maxChars, pk, dv) {
137
- if (pk !== undefined && entry.pk !== undefined && entry.pk !== pk)
140
+ // 调用方不传 pk(undefined)= 不做口径判定(保持对旧调用点的兼容);传了就必须逐字符相等。
141
+ if (pk !== undefined && entry.pk !== pk)
138
142
  return false;
139
143
  if (maxChars !== undefined && entry.maxChars !== undefined && entry.maxChars !== maxChars)
140
144
  return false;
@@ -151,7 +155,9 @@ function normalizeSummaryEntry(raw) {
151
155
  if (typeof entry.cachedAt !== 'number' || !Number.isFinite(entry.cachedAt))
152
156
  return null;
153
157
  const maxChars = typeof entry.maxChars === "number" && Number.isFinite(entry.maxChars) ? entry.maxChars : undefined;
154
- const pk = typeof entry.pk === 'string' && entry.pk !== '' ? entry.pk : undefined;
158
+ // 空串是有效口径(该组本来就没配 requestSummaryPrompt):保留下来,
159
+ // 否则「空口径条目」会被当成「缺 pk 的旧条目」,收紧后每次都 miss。
160
+ const pk = typeof entry.pk === 'string' ? entry.pk : undefined;
155
161
  const dv = typeof entry.dv === 'string' && entry.dv !== '' ? entry.dv : undefined;
156
162
  return {
157
163
  summary: entry.summary,
@@ -236,7 +242,8 @@ export async function openSummaryCache(ctx) {
236
242
  summary: text,
237
243
  cachedAt: Date.now(),
238
244
  ...(maxChars === undefined ? {} : { maxChars }),
239
- ...(pk === undefined || pk === '' ? {} : { pk }),
245
+ // 空串同样落库(见 entryMatches 注释):空口径与「缺 pk」必须能区分开。
246
+ ...(pk === undefined ? {} : { pk }),
240
247
  ...(dv === undefined || dv === '' ? {} : { dv }),
241
248
  });
242
249
  dirty = true;