weaver-e9-assistant-cli 1.1.4 → 1.1.11

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/wfcreate.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createApiFromProfile } from './client.js';
2
+ import { getCachedWorkflowList, cacheWorkflowList, findWorkflowByNameInCache } from './memory.js';
2
3
  const MENU_IDS = '1,12';
3
4
  /**
4
5
  * 取"新建流程"列表。GET createreq/wfinfo,解析 datas[].wfbeans[]。
@@ -91,12 +92,39 @@ export function buildRequestFormUrl(baseUrl, requestId, workflowId, now = Date.n
91
92
  /**
92
93
  * 按流程名在"新建流程"列表里查找 id(模糊匹配,返回全部命中项)。
93
94
  * 用于"发起 XX 流程"时把名字解析成 workflowid。
95
+ *
96
+ * ★ selfId 提供时优先读缓存(1天TTL),省掉 getCreateWfList HTTP 调用。
94
97
  */
95
- export async function findCreateWfByName(cfg, name) {
98
+ export async function findCreateWfByName(cfg, name, selfId) {
99
+ // 缓存优先
100
+ if (selfId) {
101
+ const cached = getCachedWorkflowList(cfg, selfId, '1');
102
+ if (cached) {
103
+ const hits = findWorkflowByNameInCache(cached, name);
104
+ if (hits) return hits;
105
+ // 缓存命中但没匹配到——可能是新流程,强制刷新
106
+ }
107
+ }
108
+
96
109
  const r = await getCreateWfList(cfg, { keyword: name });
97
110
  const hits = [];
98
111
  for (const c of r.categories)
99
112
  for (const w of c.workflows)
100
113
  hits.push(w);
114
+
115
+ // 写入缓存(全量 categories,后续不同关键词也可复用)
116
+ if (selfId && r.categories.length > 0) {
117
+ try {
118
+ cacheWorkflowList(cfg, selfId, '1', r.categories.map(c => ({
119
+ id: c.id,
120
+ name: c.name,
121
+ workflows: c.workflows.map(w => ({
122
+ id: w.id, name: w.name, typeId: w.typeId,
123
+ letter: w.letter, spell: w.spell,
124
+ })),
125
+ })));
126
+ } catch { /* 缓存写失败不阻断 */ }
127
+ }
128
+
101
129
  return hits;
102
130
  }
package/dist/wfform.js CHANGED
@@ -1,9 +1,72 @@
1
1
  import { createApiFromProfile } from './client.js';
2
- import { AuthTokenSession } from './auth-token.js';
3
2
  import { fetchCubeTreeBrowserOptions } from './cube.js';
4
- export const VIEWATTR_LABEL = { 1: '可编辑', 2: '只读', 3: '隐藏' };
3
+ import { getCachedBrowseOptions, cacheBrowseOptions } from './memory.js';
4
+ /**
5
+ * `viewattr`(loadForm fieldinfomap)取值——**以 E9 前端 bundle `getFieldCurViewAttr` 为准**
6
+ * (2026-10-21 从 `/spa/workflow/static4form/index.js` 逆向确认):
7
+ * **0=隐藏 / 1=只读 / 2=可编辑 / 3=必填(可编辑)**
8
+ * 依据:
9
+ * - `viewattr>0` 才在模板里(0 = 隐藏/不在模板);
10
+ * - 前端 `fieldViewOnly = (viewattr===1)`;只读场景(isonlyshow=1 / 只读模式 / 明细已存在行)一律强制成 1;
11
+ * - 必填校验用 `getFieldCurViewAttr(...) > 1`(即 2/3 才可填),必填范围按 `viewattr===3` 收集。
12
+ * 历史错误:先记成「1可编辑/2只读/3隐藏」,又改成「1编辑/2必填/3只读」——都把 1/3 弄反了,
13
+ * 导致可编辑的必填字段(va=3)被误判只读、只读字段(va=1)被放行。
14
+ */
15
+ export const VIEWATTR_LABEL = { 0: '隐藏', 1: '只读', 2: '可编辑', 3: '必填' };
16
+
17
+ /**
18
+ * 纯函数:字段是否禁止填写。返回原因字符串(null=可填)。
19
+ *
20
+ * 判据(按优先级):
21
+ * 1. 权威权限位(workflow_nodeform 表结构):
22
+ * - `isonlyshow === 1` → 禁止手工编辑(`-1`/`0`/缺失均视为未限制)
23
+ * - `isedit === 0` → 当前节点不可编辑(接口未返回则不拦)
24
+ * - `isview === 0` → 当前节点不可见(接口未返回则不拦)
25
+ * 2. `viewattr` 的真实映射(以 E9 前端 `getFieldCurViewAttr` 为准):
26
+ * - **0 = 隐藏**(不在模板/发起不展示,如 532 开票公司/税号/实际报销金额)
27
+ * - **1 = 只读**(展示但不允许填,如申请人/申请部门等自动带出项)
28
+ * - **2 = 可编辑**(允许填,非必填)
29
+ * - **3 = 必填(可编辑)**(允许填;前端必填校验按 viewattr===3 收集)
30
+ * 故 0/1 禁止填写;2/3 放行。
31
+ * 历史错误:旧实现把 1/3 的含义弄反(曾把 3 当只读),导致 va=3 的可编辑必填字段被误判只读。
32
+ */
33
+ export function fieldNonEditableReason(fld) {
34
+ const n = (v) => (v === undefined || v === null || v === '' ? undefined : Number(v));
35
+ if (n(fld?.isonlyshow) === 1)
36
+ return '禁止手工编辑';
37
+ if (n(fld?.isedit) === 0)
38
+ return '当前节点不可编辑';
39
+ if (n(fld?.isview) === 0)
40
+ return '当前节点不可见';
41
+ const va = n(fld?.viewattr);
42
+ if (va === 0)
43
+ return '隐藏';
44
+ if (va === 1)
45
+ return '只读';
46
+ return null; // 2=可编辑、3=必填(可编辑)、缺失=不拦
47
+ }
48
+ /**
49
+ * 展示用状态文案:区分「可编辑」「默认值自动带出」「禁止填写」。
50
+ * ⚠️ 带默认值的只读/隐藏字段(服务端 maindata 已有值)应显示「默认=xxx(自动带出,无需填写)」,
51
+ * 而不是裸「只读」——否则 agent 会把"系统已带出"误报成"用户不能填"(2026-10-21 用户反馈)。
52
+ * ⚠️ 可编辑字段带默认值时显示「可编辑 · 默认=xxx(可修改)」——默认值只是预填,**仍允许用户覆盖**。
53
+ */
54
+ export function fieldStatusLabel(fld) {
55
+ const reason = fieldNonEditableReason(fld);
56
+ const hasVal = fld?.currentValue !== undefined && fld?.currentValue !== null && String(fld.currentValue) !== '';
57
+ if (!reason) {
58
+ // 2=可编辑;3=必填(可编辑)。有默认值时仍允许用户修改。
59
+ const base = Number(fld?.viewattr) === 3 ? '可编辑(必填)' : '可编辑';
60
+ return hasVal ? `${base} · 默认=${fld.currentValue}(可修改)` : base;
61
+ }
62
+ if (hasVal)
63
+ return `默认=${fld.currentValue}(自动带出,无需填写)`;
64
+ return `🚫${reason}`;
65
+ }
66
+
5
67
  export const HTMLTYPE_LABEL = {
6
- 1: '单行文本', 2: '多行文本', 3: '浏览选择', 4: '附件', 5: '选择框', 6: '复选框', 7: '特殊',
68
+ // E9 fieldhtmltype:4=复选框(check框),6=附件上传(fileattr)。旧映射把 4/6 写反了。
69
+ 1: '单行文本', 2: '多行文本', 3: '浏览选择', 4: '复选框', 5: '选择框', 6: '附件', 7: '特殊',
7
70
  };
8
71
  /**
9
72
  * 取流程发起表单的字段定义。POST loadForm(aiAgentToken 鉴权)→ 解析 tableInfo。
@@ -23,19 +86,35 @@ export async function describeFormFields(cfg, workflowId) {
23
86
  const fmap = ti[key]?.fieldinfomap;
24
87
  if (!fmap || typeof fmap !== 'object')
25
88
  continue;
89
+ const maindata = j.maindata ?? {};
26
90
  const fields = Object.values(fmap).map((f) => {
27
91
  const va = num(f?.viewattr);
28
92
  const ht = num(f?.htmltype);
93
+ const reason = fieldNonEditableReason(f);
94
+ const id = str(f?.fieldid) ?? key;
95
+ // 服务端已带出的默认值/当前值(maindata)。只读但有默认值 ≠ "用户不能填",
96
+ // 而是"系统自动带出、无需填写"——展示层要区分,避免 agent 误报成只读不可填。
97
+ const cell = maindata[`field${id}`];
98
+ const rawVal = cell?.value;
99
+ const currentValue = (rawVal === undefined || rawVal === null || rawVal === '') ? undefined : String(rawVal);
29
100
  return {
30
- id: str(f?.fieldid) ?? key,
101
+ id,
31
102
  label: str(f?.fieldlabel),
32
103
  name: str(f?.fieldname),
104
+ currentValue,
105
+ // viewattr:0隐藏 / 1只读 / 2可编辑 / 3必填(参与可编辑性判断,见 fieldNonEditableReason)
33
106
  viewattr: va,
34
107
  viewattrLabel: va != null ? VIEWATTR_LABEL[va] ?? `va=${va}` : undefined,
108
+ // 字段权限(权威):isonlyshow=1 禁止手工编辑 / isedit=0 不可编辑 / isview=0 不可见
109
+ isonlyshow: num(f?.isonlyshow),
110
+ isedit: num(f?.isedit),
111
+ isview: num(f?.isview),
112
+ ismandatory: num(f?.ismandatory),
113
+ editable: reason == null,
114
+ nonEditableReason: reason,
35
115
  htmltype: ht,
36
116
  htmltypeLabel: ht != null ? HTMLTYPE_LABEL[ht] ?? `ht=${ht}` : undefined,
37
117
  isdetail: f?.isdetail,
38
- isonlyshow: f?.isonlyshow,
39
118
  table: key,
40
119
  raw: f,
41
120
  };
@@ -66,6 +145,44 @@ function num(v) {
66
145
  const n = Number(v);
67
146
  return Number.isFinite(n) ? n : undefined;
68
147
  }
148
+ /**
149
+ * 自定义浏览框 detailtype:161=单选 / 162=多选。
150
+ * E9 前端 generateFormData 实证:仅这两类会写 field<id>name,
151
+ * 且多选名称分隔符是 `##~~@@~~##`(与普通浏览框的 `##~~@@##` 不同)。
152
+ */
153
+ export const CUSTOM_BROWSER_DETAILTYPES = new Set(['161', '162']);
154
+ export function isCustomBrowserField(field) {
155
+ return Number(field?.htmltype) === 3 && CUSTOM_BROWSER_DETAILTYPES.has(String(field?.detailtype ?? ''));
156
+ }
157
+ /** 自定义浏览框(161/162)用 `##~~@@~~##`,其余沿用 `##~~@@##`。 */
158
+ export function browserNameSeparator(field) {
159
+ return isCustomBrowserField(field) ? '##~~@@~~##' : '##~~@@##';
160
+ }
161
+
162
+ /**
163
+ * 选择框(htmltype=5):把显示名/值统一规范化为 selectvalue(逗号分隔多选)。
164
+ * 传显示名但不转值时,服务端回显匹配不到选项 → 下拉框显示为空。
165
+ * 任一值无法唯一匹配则返回 undefined(调用方保留原值,不猜)。
166
+ */
167
+ export function normalizeSelectValue(field, value) {
168
+ const items = field?.raw?.selectattr?.selectitemlist ?? field?.selectattr?.selectitemlist;
169
+ if (!Array.isArray(items) || items.length === 0)
170
+ return undefined;
171
+ const rawValues = String(value ?? '').split(',').map((x) => x.trim()).filter(Boolean);
172
+ if (rawValues.length === 0)
173
+ return undefined;
174
+ const out = [];
175
+ for (const v of rawValues) {
176
+ const byValue = items.find((it) => String(it.selectvalue) === v);
177
+ const sameName = items.filter((it) => String(it.selectname) === v);
178
+ const hit = byValue ?? (sameName.length === 1 ? sameName[0] : null);
179
+ if (!hit)
180
+ return undefined;
181
+ out.push(String(hit.selectvalue));
182
+ }
183
+ return out.join(',');
184
+ }
185
+
69
186
  /**
70
187
  * 取自定义浏览框(browser.xxx)的选项(前 N 条)。通用接口(用户抓包确认):
71
188
  * GET /api/public/browser/data/<detailtype>?pageSize=<N>&current=1&min=1&max=<N>&type=<fielddbtype>&
@@ -73,26 +190,101 @@ function num(v) {
73
190
  * aiAgentToken 鉴权。返回 {total, columns, datas}:每行 id=选项值,显示名=columns 里 showType:1 的列值。
74
191
  *
75
192
  * detailtype/fielddbtype/formid 等从 loadForm fieldinfomap + params 取。pageSize 默认 10(前 10)。
193
+ *
194
+ * ★ 浏览框选项缓存:selfId 提供时优先读缓存(24小时 TTL,比表单结构短——选项变动更频繁),缓存命中省一次 HTTP API。
76
195
  */
77
- export async function getBrowserOptions(cfg, workflowId, fieldId, pageSize = 10) {
78
- const f = await describeFormFields(cfg, workflowId);
79
- // 字段可能在主表(main)或明细表(detail_<n>)的 fieldinfomap,遍历所有表查找
80
- let fm;
81
- const ti = f.raw?.tableInfo ?? {};
82
- for (const key of Object.keys(ti)) {
83
- const m = ti[key]?.fieldinfomap;
84
- if (m && m[fieldId]) {
85
- fm = m[fieldId];
86
- break;
196
+ export async function getBrowserOptions(cfg, workflowId, fieldId, pageSize = 10, selfId, relateValues = {}, formData, metaOverride = null) {
197
+ // ★ 联动浏览框(browserattr.relateFieldid):父字段值未填时选项为空,必须等值就绪再取
198
+ // 例:请假类型(8874) relateFieldid=["8872_0"] → 需带 field8872=<当前用户id> 才有选项
199
+ // relateValues:调用方已填的字段值映射(fieldId→value),优先;其次看 maindata 服务端带出值。
200
+ const _relateParentParams = (fm, f) => {
201
+ const rel = fm?.browserattr?.relateFieldid;
202
+ const out = {};
203
+ if (Array.isArray(rel)) {
204
+ for (const item of rel) {
205
+ const m = /^(\d+)_(\d+)$/.exec(String(item ?? ''));
206
+ if (!m)
207
+ continue;
208
+ const parentId = m[1];
209
+ const rowIdx = Number(m[2]);
210
+ let val = relateValues[parentId];
211
+ if ((val === undefined || val === null || val === '') && f.maindata?.[`field${parentId}`]?.value != null)
212
+ val = f.maindata[`field${parentId}`].value;
213
+ if (val === undefined || val === null || String(val) === '')
214
+ continue;
215
+ // 主表行(rowIdx=0)用 field<id>;明细行用 field<id>_<rownum>(与提交体一致)
216
+ out[rowIdx === 0 ? `field${parentId}` : `field${parentId}_${rowIdx}`] = String(val);
217
+ }
87
218
  }
219
+ return out;
220
+ };
221
+ // ★ 缓存命中:省掉 describeFormFields + API 两次 HTTP(~2.3s + ~1.2s)
222
+ // ⚠️ 联动浏览框的选项依赖父字段值,不能用普通缓存(父值一变选项就变)——
223
+ // 凡是调用方传了 relateValues(可能联动),一律跳过缓存读,走全量新取。
224
+ if (selfId && Object.keys(relateValues).length === 0) {
225
+ try {
226
+ const cached = getCachedBrowseOptions(cfg, selfId, workflowId, fieldId);
227
+ if (cached && cached.length >= pageSize) {
228
+ return cached.slice(0, pageSize);
229
+ }
230
+ if (cached && cached.length > 0) {
231
+ // 缓存的条数不够(如上次只查了10条,这次要50),走完整流程取更多
232
+ }
233
+ } catch { /* 缓存读失败走正常流程 */ }
234
+ }
235
+
236
+ let detailtype;
237
+ let fielddbtype;
238
+ let p;
239
+ let relateParams = {};
240
+ if (metaOverride) {
241
+ // 调用方已从 loadForm(iscreate=0) 拿到字段元数据/流程参数:
242
+ // 跳过 describeFormFields(省一次 HTTP,且处理任务的人未必有“发起”权限)
243
+ // metaOverride 路径无 fieldinfomap → 无联动父值可注入
244
+ detailtype = String(metaOverride.detailtype ?? '');
245
+ fielddbtype = String(metaOverride.fielddbtype ?? '');
246
+ p = metaOverride.params ?? {};
247
+ }
248
+ else {
249
+ // ★ formData 传入时复用(避免重复 describeFormFields ~2.3s)——调用方 prepareCreateSubmission 已取过。
250
+ // 缓存来源的 formData 也已由 getStartableFields 重建 raw.tableInfo(rebuildTableInfo),可复用。
251
+ let f = (formData && formData.raw?.tableInfo) ? formData : await describeFormFields(cfg, workflowId);
252
+ // 字段可能在主表(main)或明细表(detail_<n>)的 fieldinfomap,遍历所有表查找
253
+ let fm;
254
+ const ti = f.raw?.tableInfo ?? {};
255
+ for (const key of Object.keys(ti)) {
256
+ const m = ti[key]?.fieldinfomap;
257
+ if (m && m[fieldId]) {
258
+ fm = m[fieldId];
259
+ break;
260
+ }
261
+ }
262
+ if (!fm)
263
+ throw new Error(`字段 ${fieldId} 不在 fieldinfomap(可能不是浏览框)`);
264
+ // ⚠️ 联动浏览框:父字段值可能来自服务端 maindata(如"请假类型"联动当前用户)。
265
+ // 缓存 formData 重建自 tables,maindata 缺失——父值打不进请求 → 联动选项为空。
266
+ // 此时重新拉一次 loadForm(正确性优先于省这一次 HTTP),仅限联动浏览框 + 选项未命中缓存时。
267
+ if (f.maindata === undefined && Array.isArray(fm?.browserattr?.relateFieldid)) {
268
+ const fresh = await describeFormFields(cfg, workflowId);
269
+ const ti2 = fresh.raw?.tableInfo ?? {};
270
+ let fm2 = null;
271
+ for (const key of Object.keys(ti2)) {
272
+ const m = ti2[key]?.fieldinfomap;
273
+ if (m && m[fieldId]) { fm2 = m[fieldId]; break; }
274
+ }
275
+ if (fm2) {
276
+ fm = fm2;
277
+ f = fresh;
278
+ }
279
+ }
280
+ detailtype = String(fm.detailtype ?? '');
281
+ fielddbtype = String(fm.fielddbtype ?? '');
282
+ p = f.params ?? {};
283
+ // ★ 联动浏览框:父字段值(relateValues 或 maindata 服务端带出值)打进请求
284
+ relateParams = _relateParentParams(fm, f);
88
285
  }
89
- if (!fm)
90
- throw new Error(`字段 ${fieldId} 不在 fieldinfomap(可能不是浏览框)`);
91
- const detailtype = String(fm.detailtype ?? '');
92
- const fielddbtype = String(fm.fielddbtype ?? '');
93
286
  if (!detailtype || !fielddbtype)
94
287
  throw new Error(`字段 ${fieldId} 无 detailtype/fielddbtype,非浏览框`);
95
- const p = f.params ?? {};
96
288
  // ★ cube 树浏览框(detailtype=256):走递归取树,treeid = fielddbtype
97
289
  if (detailtype === '256') {
98
290
  const apiW = await createApiFromProfile(cfg);
@@ -105,14 +297,21 @@ export async function getBrowserOptions(cfg, workflowId, fieldId, pageSize = 10)
105
297
  wf_isagent: '0', wf_beagenter: '0', wfTestStr: '', wfCreater: String(p.wfCreater ?? '1'),
106
298
  disabledConditionCache: 'true', secretLevel: '4', companyId: '1', checkStrictly: 'false',
107
299
  treeid: fielddbtype, cube_treeid: fielddbtype, treetype: '256',
300
+ ...relateParams,
108
301
  };
109
302
  const items = await fetchCubeTreeBrowserOptions(apiW, base, { pageSize, maxNodes: 300 });
110
- return items.map((o) => ({ id: o.id, name: o.name, path: o.path }));
303
+ const result = items.map((o) => ({ id: o.id, name: o.name, path: o.path }));
304
+ // ⚠️ 联动字段不写缓存(选项依赖父值,换了父值选项就不同)
305
+ if (selfId && result.length > 0 && Object.keys(relateParams).length === 0) {
306
+ try { cacheBrowseOptions(cfg, selfId, workflowId, fieldId, result); } catch { /* 缓存写失败不阻断 */ }
307
+ }
308
+ return result;
111
309
  }
112
- const sess = AuthTokenSession.fromProfile(cfg);
113
- const base = cfg.baseUrl.replace(/\/+$/, '');
114
- const q = new URLSearchParams({
310
+ const api = await createApiFromProfile(cfg);
311
+ const q = {
115
312
  pageSize: String(pageSize), current: '1', min: '1', max: String(pageSize),
313
+ // 文档实证参数:type=fielddbtype(自定义浏览框 161/162 依赖它定位数据源)
314
+ type: fielddbtype,
116
315
  alllevel: '1', virtualtype: '1', status: '8',
117
316
  wfid: String(workflowId),
118
317
  billid: String(p.billid || p.formid || ''), isbill: String(p.isbill ?? '1'),
@@ -125,51 +324,71 @@ export async function getBrowserOptions(cfg, workflowId, fieldId, pageSize = 10)
125
324
  defaultSearch: 'false', secretLevel: '4',
126
325
  cmd: 'newly',
127
326
  __random__: String(Date.now()),
128
- });
129
- const r = await fetch(`${base}/api/public/browser/data/${detailtype}?${q.toString()}`, {
130
- headers: { aiAgentToken: sess.token, 'X-Requested-With': 'XMLHttpRequest' },
131
- signal: AbortSignal.timeout(15000),
132
- });
133
- const t = await r.text();
134
- let j = null;
135
- try {
136
- j = JSON.parse(t);
137
- }
138
- catch { /* non-json */ }
139
- // 响应可能是数组(HR 等)或树形对象(部门等),统一展平
140
- let flatDatas = [];
141
- if (Array.isArray(j?.datas)) {
142
- flatDatas = j.datas;
143
- }
144
- else if (j?.datas && typeof j.datas === 'object') {
145
- // 树形结构:递归展平 subs,只取可点击的叶子节点(canClick=true 或 type=2)
146
- flatDatas = flattenTree(j.datas);
147
- }
148
- const cols = Array.isArray(j?.columns) ? j.columns : [];
149
- // 有 columns 时按旧逻辑解析;无 columns 时(如 HR/部门浏览框)按 name/lastname 解析
150
- let nameKey, pkKey;
151
- if (cols.length > 0) {
152
- const showCol = cols.find((c) => String(c.showType) === '1')
153
- ?? cols.find((c) => c.isInputCol === 'true' || c.isInputCol === true)
154
- ?? cols.find((c) => /名称|名字|name/i.test(String(c.title ?? '')))
155
- ?? cols.find((c) => c.display === 'true' && c.hide !== 'true');
156
- nameKey = showCol?.dataIndex ?? 'mc';
157
- const pkCol = cols.find((c) => c.isPrimarykey === 'true' || c.isPrimarykey === true);
158
- pkKey = pkCol?.dataIndex;
327
+ ...relateParams,
328
+ };
329
+ // ★ 走 E9Api(token 优先、cookie 回退):裸 fetch 只带 aiAgentToken,
330
+ // cookie-only 环境会拿到空数据(e9pc/10.x 手工导入 cookie 的场景)。
331
+ const path = `/api/public/browser/data/${detailtype}`;
332
+ const parseBrowserData = (j) => {
333
+ // 响应可能是数组(HR 等)或树形对象(部门等),统一展平
334
+ let flatDatas = [];
335
+ if (Array.isArray(j?.datas)) {
336
+ flatDatas = j.datas;
337
+ }
338
+ else if (j?.datas && typeof j.datas === 'object') {
339
+ // 树形结构:递归展平 subs,只取可点击的叶子节点(canClick=true 或 type=2)
340
+ flatDatas = flattenTree(j.datas);
341
+ }
342
+ const cols = Array.isArray(j?.columns) ? j.columns : [];
343
+ // 有 columns 时按旧逻辑解析;无 columns 时(如 HR/部门浏览框)按 name/lastname 解析
344
+ let nameKey, pkKey;
345
+ if (cols.length > 0) {
346
+ const showCol = cols.find((c) => String(c.showType) === '1')
347
+ ?? cols.find((c) => c.isInputCol === 'true' || c.isInputCol === true)
348
+ ?? cols.find((c) => /名称|名字|name/i.test(String(c.title ?? '')))
349
+ ?? cols.find((c) => c.display === 'true' && c.hide !== 'true');
350
+ nameKey = showCol?.dataIndex ?? 'mc';
351
+ const pkCol = cols.find((c) => c.isPrimarykey === 'true' || c.isPrimarykey === true);
352
+ pkKey = pkCol?.dataIndex;
353
+ }
354
+ else {
355
+ // 无 columns:优先 lastname,回退 name;部分浏览框返回 key+showname(如假期类型)
356
+ const first = flatDatas.length > 0 ? flatDatas[0] : null;
357
+ if (first && first.key != null && first.showname != null) {
358
+ pkKey = 'key';
359
+ nameKey = 'showname';
360
+ }
361
+ else {
362
+ pkKey = null;
363
+ nameKey = first?.lastname != null ? 'lastname' : 'name';
364
+ }
365
+ }
366
+ return flatDatas
367
+ // 自定义浏览框(161/162)的主键列是 ids(不是 id);无 columns 的接口才用 d.id/d.key
368
+ .filter((d) => d && (d.id != null || d.key != null || (pkKey && d[pkKey] != null)))
369
+ .map((d) => {
370
+ const rawPk = pkKey && d[pkKey] != null && String(d[pkKey]) !== '' ? d[pkKey] : (d.id ?? d.key);
371
+ const pk = String(rawPk);
372
+ const name = stripHtml(String(d[nameKey] ?? d.mc ?? d.name ?? d.showname ?? rawPk));
373
+ return { id: pk, name: name || pk };
374
+ })
375
+ .filter((o) => o.id);
376
+ };
377
+ let result = parseBrowserData((await api.get(path, q))?.json ?? null);
378
+ // 兼容 token 通道 200 空包(no-op):cookie 可用时强制 cookie 再取一次
379
+ if (result.length === 0 && cfg.cookie) {
380
+ try {
381
+ result = parseBrowserData((await api.get(path, q, { forceCookie: true }))?.json ?? null);
382
+ }
383
+ catch { /* cookie 兜底失败:保持空结果,不阻断 */ }
159
384
  }
160
- else {
161
- // 无 columns:优先 lastname,回退 name
162
- nameKey = flatDatas.length > 0 && flatDatas[0].lastname != null ? 'lastname' : 'name';
163
- pkKey = null;
385
+
386
+ // ★ 缓存浏览框选项:下次同字段查询省掉一次 HTTP(⚠️ 联动字段不写,父值一变选项就变)
387
+ if (selfId && result.length > 0 && Object.keys(relateParams).length === 0) {
388
+ try { cacheBrowseOptions(cfg, selfId, workflowId, fieldId, result); } catch { /* 缓存写失败不阻断 */ }
164
389
  }
165
- return flatDatas
166
- .filter((d) => d && d.id != null)
167
- .map((d) => {
168
- const pk = pkKey && d[pkKey] != null && String(d[pkKey]) !== '' ? String(d[pkKey]) : String(d.id);
169
- const name = stripHtml(String(d[nameKey] ?? d.mc ?? d.name ?? d.id));
170
- return { id: pk, name: name || String(d.id) };
171
- })
172
- .filter((o) => o.id);
390
+
391
+ return result;
173
392
  }
174
393
  /** 递归展平树形浏览数据(部门组织架构等),只取可点击节点 */
175
394
  function flattenTree(node) {