@steedos-labs/plugin-workflow 3.0.39 → 3.0.40

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 (30) hide show
  1. package/designer/dist/amis-renderer/amis-renderer.css +1 -1
  2. package/designer/dist/amis-renderer/amis-renderer.js +1 -1
  3. package/designer/dist/assets/index-BIbXABqz.css +1 -0
  4. package/designer/dist/assets/index-DRUi3eGk.js +943 -0
  5. package/designer/dist/index.html +2 -2
  6. package/main/default/manager/instance_number_rules.js +1 -1
  7. package/main/default/manager/uuflow_manager.js +20 -2
  8. package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
  9. package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -1
  10. package/main/default/objects/instances/listviews/completed.listview.yml +1 -1
  11. package/main/default/objects/instances/listviews/draft.listview.yml +1 -1
  12. package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
  13. package/main/default/objects/instances/listviews/pending.listview.yml +1 -1
  14. package/main/default/pages/flow_selector.page.amis.json +2 -2
  15. package/main/default/pages/flow_selector_mobile.page.amis.json +2 -2
  16. package/main/default/pages/page_instance_print.page.amis.json +11 -1
  17. package/main/default/routes/am.router.js +3 -1
  18. package/main/default/routes/api_auto_number.router.js +166 -23
  19. package/main/default/routes/api_workflow_ai_form_design.router.js +116 -16
  20. package/main/default/routes/api_workflow_ai_form_design_stream.router.js +115 -17
  21. package/main/default/routes/api_workflow_box_filter.router.js +2 -2
  22. package/main/default/services/flows.service.js +20 -24
  23. package/main/default/services/instance.service.js +6 -4
  24. package/package.json +1 -1
  25. package/package.service.js +14 -0
  26. package/public/amis-renderer/amis-renderer.css +1 -1
  27. package/public/amis-renderer/amis-renderer.js +1 -1
  28. package/public/workflow/index.css +10 -3
  29. package/designer/dist/assets/index-CxYuhf9v.js +0 -757
  30. package/designer/dist/assets/index-Dve-EwQO.css +0 -1
@@ -75,6 +75,7 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
75
75
  - "org" — 组织(选择单个部门)
76
76
  - "orgMulti" — 组织(多选,选择多个部门)
77
77
  - "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
78
+ - "richtext" — 富文本(支持格式化文本、颜色、链接等)
78
79
  - required: 布尔值,是否必填
79
80
  - colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
80
81
  - description: 字符串,字段说明/提示文字,支持 HTML
@@ -168,6 +169,19 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
168
169
  - pickerMultiple: 布尔值,是否允许多选,默认 false
169
170
  - lookupFilters: 字符串,OData 过滤表达式,如 "status eq 'active'"
170
171
  - lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
172
+ - lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
173
+ - 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
174
+
175
+ - member 类型(成员单选)填充规则:
176
+ - pickerFillRules: 数组,选择人员后将人员字段值自动填充到表单其他字段。源对象为 space_users。格式 [{ "sourceField": "源字段API名", "targetField": "当前表单中的目标字段name" }]
177
+ - sourceField 支持点号表示法,可访问关联对象的子属性。成员的 organization 字段引用了 organizations 对象,因此可以使用 "organization.xxx" 访问部门属性
178
+ - 可用的 organization 子属性示例:organization.name(部门名称)、organization.fullname(部门全称)等
179
+ - 示例:选择成员后自动填充其所属部门名称到文本字段:
180
+ pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }, { "sourceField": "name", "targetField": "user_name" }]
181
+
182
+ - org 类型(部门单选)填充规则:
183
+ - pickerFillRules: 数组,选择部门后将部门字段值自动填充到表单其他字段。源对象为 organizations。格式同 member 的 pickerFillRules
184
+ - 示例:选择部门后自动填充部门全称到文本字段:pickerFillRules: [{ "sourceField": "fullname", "targetField": "org_fullname" }]
171
185
 
172
186
  - reference 类型(对象选择):
173
187
  - reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
@@ -193,16 +207,22 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
193
207
  ## 事件脚本
194
208
 
195
209
  events 对象包含三个可选的 JavaScript 脚本:
196
- - onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据)
210
+ - onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据), currentStep (当前审批步骤对象)
197
211
  - form.setFieldValue(fieldName, value) — 设置字段值
198
212
  - form.getFieldValue(fieldName) — 获取字段值
199
213
  - form.setFieldOptions(fieldName, options) — 设置下拉选项
200
214
  - form.setFieldHidden(fieldName, hidden) — 显隐字段
201
215
  - form.setFieldRequired(fieldName, required) — 设为必填
202
- - onValueChange: 字段值变化时执行。可用参数:field (变化的字段), value (新值), oldValue (旧值), form (表单实例), values (所有字段值)
203
- - onSubmit: 表单提交前执行。可用参数:form (表单实例), values (提交数据)。返回 false 可阻止提交
216
+ - form.currentStep 当前审批步骤对象,包含 name(步骤名)、step_type(步骤类型: start/submit/sign/counterSign/condition)、_id 等属性
217
+ - onValueChange: 字段值变化时执行。可用参数:field (变化的字段), value (新值), oldValue (旧值), form (表单实例), values (所有字段值), currentStep (当前审批步骤对象)
218
+ - onSubmit: 表单提交前执行。可用参数:form (表单实例), values (提交数据), currentStep (当前审批步骤对象)。返回 false 可阻止提交
219
+
220
+ 所有脚本支持 async/await。
221
+ ⚠️ 脚本内容直接写代码体,参数(form、field、value 等)由运行时自动注入,**禁止用 function(...){} 包裹**。
222
+ 正确:"if (field.name === 'x') { ... }"
223
+ 错误:"function(field, value, oldValue, form, values) { ... }"
204
224
 
205
- 所有脚本支持 async/await,示例:
225
+ 示例:
206
226
  \`\`\`javascript
207
227
  // onValueChange 示例
208
228
  if (field.name === 'quantity' || field.name === 'unit_price') {
@@ -227,14 +247,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
227
247
  新版表单已**不再支持** instance_template(审批单模板)、form_script(表单脚本)、flow_events(流程事件),这些是旧版的遗留功能。当用户提供这些数据时,你需要:
228
248
 
229
249
  1. **分析其中的业务逻辑**,理解它们实现了什么功能
230
- 2. **将业务逻辑迁移到新版机制中**:
231
- - 字段显隐控制 转为字段的 visibilityRules
232
- - 动态必填控制转为字段的 requiredRules
233
- - 字段值自动计算转为 formula 类型字段,或写入 events.onValueChange
250
+ 2. **⭐ 声明式字段配置优先,脚本兜底**。迁移旧版脚本时,按以下优先级选择实现方式——能用字段属性声明式解决的,绝不写事件脚本:
251
+ - 数值范围校验(如金额上限/下限)→ number 字段的 **min / max** 属性(而非 onSubmit 脚本校验)
252
+ - 字段显隐控制字段的 **visibilityRules**(而非 onValueChange 中 form.setFieldHidden())
253
+ - 动态必填控制字段的 **requiredRules**(而非 onValueChange 中 form.setFieldRequired())
254
+ - 字段值自动计算 → **formula** 类型字段(而非 onValueChange 中 form.setFieldValue()),仅当公式语法无法表达时才退化到 onValueChange
255
+ - 跨对象属性引用(如旧公式 {applicant.organization.name})→ 成员字段的 **pickerFillRules**,sourceField 使用点号表示法如 "organization.name",targetField 指向目标文本字段
256
+ - 字段选项设置 → 字段的 **options** 属性(静态选项),仅动态/异步加载选项时才用 events.onInit / events.onValueChange 中 form.setFieldOptions()
234
257
  - 初始化赋值/选项加载 → 写入 events.onInit
235
- - 值联动/级联逻辑 写入 events.onValueChange
236
- - 提交前校验 写入 events.onSubmit
237
- - 字段选项设置 → 转为字段的 options 属性,或通过 events.onInit / events.onValueChange 中 form.setFieldOptions() 设置
258
+ - 值联动/级联逻辑(无法用 formula 表达时)→ 写入 events.onValueChange
259
+ - 提交前复杂校验(无法用 min/max/required 等表达时)→ 写入 events.onSubmit
238
260
  3. **instance_template 仍可用于推断字段 label、colspan(列宽)和 rowspan(行高)**
239
261
  4. **不要在输出中返回** instance_template、form_script、flow_events 字段
240
262
 
@@ -313,8 +335,54 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
313
335
  - 新版事件脚本使用 form.getFieldValue() / form.setFieldValue() / form.setFieldOptions() / form.setFieldHidden() / form.setFieldRequired() 等 API
314
336
 
315
337
  ### 流程事件 (flow_events)
316
- - 旧版流程级别的事件脚本
317
- - 分析其中的业务逻辑,合并迁移到新版的 events
338
+ - 旧版流程级别的事件脚本,通常绑定在 jQuery 事件上(如 \`instance-before-submit\`、\`instance-before-save\` 等)
339
+ - 分析其中的业务逻辑,**优先用字段声明式配置实现,无法声明式表达时才写入 events 脚本**
340
+ - 常见迁移模式:
341
+ - \`instance-before-submit\` 中的数值范围校验(如 \`money >= 5000000\` 则阻止提交)→ **直接设置 number 字段的 max 属性**,如 \`"max": 4999999.99\`,无需写 onSubmit 脚本
342
+ - \`instance-before-submit\` 中的必填校验 → 设置字段 \`required: true\` 或 \`requiredRules\`
343
+ - \`instance-before-submit\` 中的复杂跨字段校验(如"结束日期必须大于开始日期"且无法用单字段属性表达)→ 写入 events.onSubmit,用 \`form.getFieldValue()\` 获取值,返回 \`false\` 阻止提交
344
+ - \`instance-before-save\` 中的自动赋值 → 写入 events.onInit 或 events.onValueChange
345
+ - \`WorkflowManager.getInstance().state\` 等实例状态判断 → 新版 onSubmit 仅在提交时触发,草稿保存不触发,通常可省略状态判断
346
+ - \`e.preventDefault()\` + \`toastr.error(msg)\` → 新版用 \`form.showError(msg); return false;\` 替代
347
+ - 注意:旧版脚本中的字段引用(如 \`formValues['中文字段名']\`)需替换为新版英文字段 name
348
+ - 注意:旧版脚本是 JavaScript 语法,其中被注释掉的代码(// 单行注释 或 /* */ 块注释)属于废弃逻辑,不需要识别和迁移,直接忽略即可
349
+ - 迁移示例:
350
+ 旧版 flow_events:
351
+ \`\`\`javascript
352
+ var money = parseFloat(formValues['申请增加额度']);
353
+ if (money >= 5000000) { e.preventDefault(); toastr.error('金额不能超过500万'); }
354
+ \`\`\`
355
+ 新版迁移方式:**不写脚本**,直接在字段上配置:
356
+ \`\`\`json
357
+ { "name": "apply_increase_amount", "type": "number", "max": 4999999.99, "precision": 2 }
358
+ \`\`\`
359
+
360
+ ### 旧版 HTML 字段类型迁移
361
+ - 旧版表单中存在 type 为 \"html\" 的字段,这是旧版的 HTML 编辑器字段,允许用户输入和编辑 HTML 格式的富文本内容
362
+ - 新版表单中 **不再支持 \"html\" 类型**,已被 **\"richtext\"(富文本)** 类型完全替代
363
+ - **迁移规则**:当遇到 type 为 \"html\" 的字段时,必须将其 type 改为 \"richtext\",其余属性(name、label、required、colspan、defaultValue 等)保持不变
364
+ - richtext 类型支持格式化文本、颜色、链接等富文本编辑功能,功能上完全覆盖旧版 html 类型
365
+ - 迁移示例:
366
+ 旧版字段:{ "name": "content", "label": "内容", "type": "html" }
367
+ 新版字段:{ "name": "content", "label": "内容", "type": "richtext" }
368
+ - migrationLog 记录格式:"字段 <name>: type 从 html 转换为 richtext"
369
+
370
+ ### 旧公式字段跨对象引用迁移
371
+ - 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
372
+ - 迁移方式:
373
+ 1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
374
+ 2. 在成员字段(type: "member")上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
375
+ 3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
376
+ - 迁移示例:
377
+ 旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
378
+ 新版迁移方式:
379
+ 1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
380
+ 2. 在成员字段上配置 pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }]
381
+ - 常见映射:
382
+ - \`{applicant.organization.name}\` → sourceField: "organization.name"
383
+ - \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
384
+ - \`{applicant.name}\` → sourceField: "name"
385
+ - \`{applicant.mobile}\` → sourceField: "mobile"
318
386
 
319
387
  ## 输出格式
320
388
  返回一个 JSON 对象,格式如下(不要有任何其他文字、解释、markdown 标记):
@@ -325,8 +393,32 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
325
393
  "onInit": "脚本代码或空字符串",
326
394
  "onValueChange": "脚本代码或空字符串",
327
395
  "onSubmit": "脚本代码或空字符串"
328
- }
329
- }`;
396
+ },
397
+ "migrationLog": ["迁移记录,仅升级场景需要"]
398
+ }
399
+
400
+ ### migrationLog(迁移记录)
401
+ 当用户提供了 flow_events、form_script、instance_template 中包含业务逻辑(如校验、赋值、显隐控制等脚本代码)时,必须在 migrationLog 数组中为每条被迁移的逻辑记录一条说明,格式:
402
+ "<来源>: <原逻辑简述> → <迁移到的具体位置>"
403
+
404
+ 迁移目标可以是以下任意一种:
405
+ - 字段属性:如 "字段 apply_increase_amount 的 max:4999999.99"
406
+ - 字段规则:如 "字段 remark 的 visibilityRules"、"字段 attachment_desc 的 requiredRules"
407
+ - formula 字段:如 "新增 formula 字段 total_amount"
408
+ - 事件脚本:如 "events.onSubmit"、"events.onValueChange"
409
+ - 已省略(含原因):如 "已省略(旧版 state=='draft' 判断,新版 onSubmit 仅提交时触发,无需判断)"
410
+
411
+ 示例:
412
+ "migrationLog": [
413
+ "flow_events: 申请增加额度>=500万阻止提交 → 字段 apply_increase_amount 的 max:4999999.99",
414
+ "flow_events: ins.state!='draft' 状态判断 → 已省略(新版 onSubmit 仅提交时触发)",
415
+ "form_script: 根据类型字段显隐备注 → 字段 remark 的 visibilityRules",
416
+ "form_script: 选择部门后级联加载人员选项 → events.onValueChange",
417
+ "instance_template: {applicant.organization.name} 跨对象引用 → 成员字段 applicant 的 pickerFillRules (organization.name → dept_name)"
418
+ ]
419
+
420
+ ⚠️ 如果 flow_events 或 form_script 不为空但 migrationLog 为空数组,视为错误。每条旧版逻辑都必须有对应的迁移记录,不允许静默丢弃。
421
+ 如果没有提供 flow_events、form_script,或它们为空,则 migrationLog 可省略或为空数组。`;
330
422
 
331
423
  // Build current fields description
332
424
  const fieldsJson = JSON.stringify(fields || [], null, 2);
@@ -439,7 +531,7 @@ ${userRequest}
439
531
  'text', 'textarea', 'number', 'date', 'datetime', 'time',
440
532
  'select', 'multiSelect', 'checkbox', 'radio', 'file', 'image',
441
533
  'lookup', 'section', 'table', 'grid', 'formula',
442
- 'member', 'memberMulti', 'org', 'orgMulti', 'reference'
534
+ 'member', 'memberMulti', 'org', 'orgMulti', 'reference', 'richtext'
443
535
  ];
444
536
  const jsIdentifierRe = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
445
537
  function sanitizeName(name) {
@@ -458,6 +550,8 @@ ${userRequest}
458
550
  if (field.type === 'groupPicker') field.type = field.pickerMultiple ? 'orgMulti' : 'org';
459
551
  // Backward compat: map legacy select + pickerMultiple to multiSelect
460
552
  if (field.type === 'select' && field.pickerMultiple) field.type = 'multiSelect';
553
+ // Backward compat: map legacy html type to richtext
554
+ if (field.type === 'html') field.type = 'richtext';
461
555
  if (field.type && !validTypes.includes(field.type)) {
462
556
  field.type = 'text'; // fallback to text for unknown types
463
557
  }
@@ -481,6 +575,8 @@ ${userRequest}
481
575
  if (child.type === 'userPicker') child.type = child.pickerMultiple ? 'memberMulti' : 'member';
482
576
  if (child.type === 'groupPicker') child.type = child.pickerMultiple ? 'orgMulti' : 'org';
483
577
  if (child.type === 'select' && child.pickerMultiple) child.type = 'multiSelect';
578
+ // Backward compat: map legacy html type to richtext
579
+ if (child.type === 'html') child.type = 'richtext';
484
580
  if (child.type && !validTypes.includes(child.type)) {
485
581
  child.type = 'text';
486
582
  }
@@ -512,6 +608,10 @@ ${userRequest}
512
608
  onSubmit: result.events.onSubmit || '',
513
609
  },
514
610
  };
611
+ // Pass through migrationLog if present (for upgrade audit trail)
612
+ if (Array.isArray(result.migrationLog) && result.migrationLog.length > 0) {
613
+ responseData.migrationLog = result.migrationLog;
614
+ }
515
615
  return res.json(responseData);
516
616
 
517
617
  } catch (error) {
@@ -302,7 +302,7 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
302
302
  'text', 'textarea', 'number', 'date', 'datetime', 'time',
303
303
  'select', 'multiSelect', 'checkbox', 'radio', 'file', 'image',
304
304
  'lookup', 'section', 'table', 'grid', 'formula',
305
- 'member', 'memberMulti', 'org', 'orgMulti', 'reference'
305
+ 'member', 'memberMulti', 'org', 'orgMulti', 'reference', 'richtext'
306
306
  ];
307
307
  const jsIdentifierRe = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
308
308
  function sanitizeName(name) {
@@ -321,6 +321,8 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
321
321
  if (field.type === 'groupPicker') field.type = field.pickerMultiple ? 'orgMulti' : 'org';
322
322
  // Backward compat: map legacy select + pickerMultiple to multiSelect
323
323
  if (field.type === 'select' && field.pickerMultiple) field.type = 'multiSelect';
324
+ // Backward compat: map legacy html type to richtext
325
+ if (field.type === 'html') field.type = 'richtext';
324
326
  if (field.type && !validTypes.includes(field.type)) {
325
327
  field.type = 'text';
326
328
  }
@@ -342,6 +344,8 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
342
344
  if (child.type === 'userPicker') child.type = child.pickerMultiple ? 'memberMulti' : 'member';
343
345
  if (child.type === 'groupPicker') child.type = child.pickerMultiple ? 'orgMulti' : 'org';
344
346
  if (child.type === 'select' && child.pickerMultiple) child.type = 'multiSelect';
347
+ // Backward compat: map legacy html type to richtext
348
+ if (child.type === 'html') child.type = 'richtext';
345
349
  if (child.type && !validTypes.includes(child.type)) {
346
350
  child.type = 'text';
347
351
  }
@@ -370,6 +374,10 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
370
374
  onSubmit: result.events.onSubmit || '',
371
375
  },
372
376
  };
377
+ // Pass through migrationLog if present (for upgrade audit trail)
378
+ if (Array.isArray(result.migrationLog) && result.migrationLog.length > 0) {
379
+ responseData.migrationLog = result.migrationLog;
380
+ }
373
381
 
374
382
  const fieldCount = (responseData.fields || []).length;
375
383
  sendEvent('progress', { stage: 'complete', message: `转换完成!共 ${fieldCount} 个字段` });
@@ -429,6 +437,7 @@ function buildSystemPrompt() {
429
437
  - "org" — 组织(选择单个部门)
430
438
  - "orgMulti" — 组织(多选,选择多个部门)
431
439
  - "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
440
+ - "richtext" — 富文本(支持格式化文本、颜色、链接等)
432
441
  - required: 布尔值,是否必填
433
442
  - colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
434
443
  - description: 字符串,字段说明/提示文字,支持 HTML
@@ -522,6 +531,19 @@ function buildSystemPrompt() {
522
531
  - pickerMultiple: 布尔值,是否允许多选,默认 false
523
532
  - lookupFilters: 字符串,OData 过滤表达式,如 "status eq 'active'"
524
533
  - lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
534
+ - lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
535
+ - 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
536
+
537
+ - member 类型(成员单选)填充规则:
538
+ - pickerFillRules: 数组,选择人员后将人员字段值自动填充到表单其他字段。源对象为 space_users。格式 [{ "sourceField": "源字段API名", "targetField": "当前表单中的目标字段name" }]
539
+ - sourceField 支持点号表示法,可访问关联对象的子属性。成员的 organization 字段引用了 organizations 对象,因此可以使用 "organization.xxx" 访问部门属性
540
+ - 可用的 organization 子属性示例:organization.name(部门名称)、organization.fullname(部门全称)等
541
+ - 示例:选择成员后自动填充其所属部门名称到文本字段:
542
+ pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }, { "sourceField": "name", "targetField": "user_name" }]
543
+
544
+ - org 类型(部门单选)填充规则:
545
+ - pickerFillRules: 数组,选择部门后将部门字段值自动填充到表单其他字段。源对象为 organizations。格式同 member 的 pickerFillRules
546
+ - 示例:选择部门后自动填充部门全称到文本字段:pickerFillRules: [{ "sourceField": "fullname", "targetField": "org_fullname" }]
525
547
 
526
548
  - reference 类型(对象选择):
527
549
  - reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
@@ -547,16 +569,22 @@ function buildSystemPrompt() {
547
569
  ## 事件脚本
548
570
 
549
571
  events 对象包含三个可选的 JavaScript 脚本:
550
- - onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据)
572
+ - onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据), currentStep (当前审批步骤对象)
551
573
  - form.setFieldValue(fieldName, value) — 设置字段值
552
574
  - form.getFieldValue(fieldName) — 获取字段值
553
575
  - form.setFieldOptions(fieldName, options) — 设置下拉选项
554
576
  - form.setFieldHidden(fieldName, hidden) — 显隐字段
555
577
  - form.setFieldRequired(fieldName, required) — 设为必填
556
- - onValueChange: 字段值变化时执行。可用参数:field (变化的字段), value (新值), oldValue (旧值), form (表单实例), values (所有字段值)
557
- - onSubmit: 表单提交前执行。可用参数:form (表单实例), values (提交数据)。返回 false 可阻止提交
578
+ - form.currentStep 当前审批步骤对象,包含 name(步骤名)、step_type(步骤类型: start/submit/sign/counterSign/condition)、_id 等属性
579
+ - onValueChange: 字段值变化时执行。可用参数:field (变化的字段), value (新值), oldValue (旧值), form (表单实例), values (所有字段值), currentStep (当前审批步骤对象)
580
+ - onSubmit: 表单提交前执行。可用参数:form (表单实例), values (提交数据), currentStep (当前审批步骤对象)。返回 false 可阻止提交
558
581
 
559
- 所有脚本支持 async/await,示例:
582
+ 所有脚本支持 async/await
583
+ ⚠️ 脚本内容直接写代码体,参数(form、field、value 等)由运行时自动注入,**禁止用 function(...){} 包裹**。
584
+ 正确:"if (field.name === 'x') { ... }"
585
+ 错误:"function(field, value, oldValue, form, values) { ... }"
586
+
587
+ 示例:
560
588
  \`\`\`javascript
561
589
  // onValueChange 示例
562
590
  if (field.name === 'quantity' || field.name === 'unit_price') {
@@ -581,14 +609,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
581
609
  新版表单已**不再支持** instance_template(审批单模板)、form_script(表单脚本)、flow_events(流程事件),这些是旧版的遗留功能。当用户提供这些数据时,你需要:
582
610
 
583
611
  1. **分析其中的业务逻辑**,理解它们实现了什么功能
584
- 2. **将业务逻辑迁移到新版机制中**:
585
- - 字段显隐控制 转为字段的 visibilityRules
586
- - 动态必填控制转为字段的 requiredRules
587
- - 字段值自动计算转为 formula 类型字段,或写入 events.onValueChange
612
+ 2. **⭐ 声明式字段配置优先,脚本兜底**。迁移旧版脚本时,按以下优先级选择实现方式——能用字段属性声明式解决的,绝不写事件脚本:
613
+ - 数值范围校验(如金额上限/下限)→ number 字段的 **min / max** 属性(而非 onSubmit 脚本校验)
614
+ - 字段显隐控制字段的 **visibilityRules**(而非 onValueChange 中 form.setFieldHidden())
615
+ - 动态必填控制字段的 **requiredRules**(而非 onValueChange 中 form.setFieldRequired())
616
+ - 字段值自动计算 → **formula** 类型字段(而非 onValueChange 中 form.setFieldValue()),仅当公式语法无法表达时才退化到 onValueChange
617
+ - 跨对象属性引用(如旧公式 {applicant.organization.name})→ 成员字段的 **pickerFillRules**,sourceField 使用点号表示法如 "organization.name",targetField 指向目标文本字段
618
+ - 字段选项设置 → 字段的 **options** 属性(静态选项),仅动态/异步加载选项时才用 events.onInit / events.onValueChange 中 form.setFieldOptions()
588
619
  - 初始化赋值/选项加载 → 写入 events.onInit
589
- - 值联动/级联逻辑 写入 events.onValueChange
590
- - 提交前校验 写入 events.onSubmit
591
- - 字段选项设置 → 转为字段的 options 属性,或通过 events.onInit / events.onValueChange 中 form.setFieldOptions() 设置
620
+ - 值联动/级联逻辑(无法用 formula 表达时)→ 写入 events.onValueChange
621
+ - 提交前复杂校验(无法用 min/max/required 等表达时)→ 写入 events.onSubmit
592
622
  3. **instance_template 仍可用于推断字段 label、colspan(列宽)和 rowspan(行高)**
593
623
  4. **不要在输出中返回** instance_template、form_script、flow_events 字段
594
624
 
@@ -667,9 +697,53 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
667
697
  - 新版事件脚本使用 form.getFieldValue() / form.setFieldValue() / form.setFieldOptions() / form.setFieldHidden() / form.setFieldRequired() 等 API
668
698
 
669
699
  ### 流程事件 (flow_events)
670
- - 旧版流程级别的事件脚本
671
- - 分析其中的业务逻辑,合并迁移到新版的 events
672
-
700
+ - 旧版流程级别的事件脚本,通常绑定在 jQuery 事件上(如 \`instance-before-submit\`、\`instance-before-save\` 等)
701
+ - 分析其中的业务逻辑,**优先用字段声明式配置实现,无法声明式表达时才写入 events 脚本**
702
+ - 常见迁移模式:
703
+ - \`instance-before-submit\` 中的数值范围校验(如 \`money >= 5000000\` 则阻止提交)→ **直接设置 number 字段的 max 属性**,如 \`"max": 4999999.99\`,无需写 onSubmit 脚本
704
+ - \`instance-before-submit\` 中的必填校验 → 设置字段 \`required: true\` 或 \`requiredRules\`
705
+ - \`instance-before-submit\` 中的复杂跨字段校验(如"结束日期必须大于开始日期"且无法用单字段属性表达)→ 写入 events.onSubmit,用 \`form.getFieldValue()\` 获取值,返回 \`false\` 阻止提交
706
+ - \`instance-before-save\` 中的自动赋值 → 写入 events.onInit 或 events.onValueChange
707
+ - \`WorkflowManager.getInstance().state\` 等实例状态判断 → 新版 onSubmit 仅在提交时触发,草稿保存不触发,通常可省略状态判断
708
+ - \`e.preventDefault()\` + \`toastr.error(msg)\` → 新版用 \`form.showError(msg); return false;\` 替代
709
+ - 注意:旧版脚本中的字段引用(如 \`formValues['中文字段名']\`)需替换为新版英文字段 name
710
+ - 注意:旧版脚本是 JavaScript 语法,其中被注释掉的代码(// 单行注释 或 /* */ 块注释)属于废弃逻辑,不需要识别和迁移,直接忽略即可
711
+ - 迁移示例:
712
+ 旧版 flow_events:
713
+ \`\`\`javascript
714
+ var money = parseFloat(formValues['申请增加额度']);
715
+ if (money >= 5000000) { e.preventDefault(); toastr.error('金额不能超过500万'); }
716
+ \`\`\`
717
+ 新版迁移方式:**不写脚本**,直接在字段上配置:
718
+ \`\`\`json
719
+ { "name": "apply_increase_amount", "type": "number", "max": 4999999.99, "precision": 2 }
720
+ \`\`\`
721
+ ### 旧版 HTML 字段类型迁移
722
+ - 旧版表单中存在 type 为 \"html\" 的字段,这是旧版的 HTML 编辑器字段,允许用户输入和编辑 HTML 格式的富文本内容
723
+ - 新版表单中 **不再支持 \"html\" 类型**,已被 **\"richtext\"(富文本)** 类型完全替代
724
+ - **迁移规则**:当遇到 type 为 \"html\" 的字段时,必须将其 type 改为 \"richtext\",其余属性(name、label、required、colspan、defaultValue 等)保持不变
725
+ - richtext 类型支持格式化文本、颜色、链接等富文本编辑功能,功能上完全覆盖旧版 html 类型
726
+ - 迁移示例:
727
+ 旧版字段:{ "name": "content", "label": "内容", "type": "html" }
728
+ 新版字段:{ "name": "content", "label": "内容", "type": "richtext" }
729
+ - migrationLog 记录格式:"字段 <name>: type 从 html 转换为 richtext"
730
+
731
+ ### 旧公式字段跨对象引用迁移
732
+ - 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
733
+ - 迁移方式:
734
+ 1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
735
+ 2. 在成员字段(type: "member")上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
736
+ 3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
737
+ - 迁移示例:
738
+ 旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
739
+ 新版迁移方式:
740
+ 1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
741
+ 2. 在成员字段上配置 pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }]
742
+ - 常见映射:
743
+ - \`{applicant.organization.name}\` → sourceField: "organization.name"
744
+ - \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
745
+ - \`{applicant.name}\` → sourceField: "name"
746
+ - \`{applicant.mobile}\` → sourceField: "mobile"
673
747
  ## 输出格式
674
748
  返回一个 JSON 对象,格式如下(不要有任何其他文字、解释、markdown 标记):
675
749
  {
@@ -679,8 +753,32 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
679
753
  "onInit": "脚本代码或空字符串",
680
754
  "onValueChange": "脚本代码或空字符串",
681
755
  "onSubmit": "脚本代码或空字符串"
682
- }
683
- }`;
756
+ },
757
+ "migrationLog": ["迁移记录,仅升级场景需要"]
758
+ }
759
+
760
+ ### migrationLog(迁移记录)
761
+ 当用户提供了 flow_events、form_script、instance_template 中包含业务逻辑(如校验、赋值、显隐控制等脚本代码)时,必须在 migrationLog 数组中为每条被迁移的逻辑记录一条说明,格式:
762
+ "<来源>: <原逻辑简述> → <迁移到的具体位置>"
763
+
764
+ 迁移目标可以是以下任意一种:
765
+ - 字段属性:如 "字段 apply_increase_amount 的 max:4999999.99"
766
+ - 字段规则:如 "字段 remark 的 visibilityRules"、"字段 attachment_desc 的 requiredRules"
767
+ - formula 字段:如 "新增 formula 字段 total_amount"
768
+ - 事件脚本:如 "events.onSubmit"、"events.onValueChange"
769
+ - 已省略(含原因):如 "已省略(旧版 state=='draft' 判断,新版 onSubmit 仅提交时触发,无需判断)"
770
+
771
+ 示例:
772
+ "migrationLog": [
773
+ "flow_events: 申请增加额度>=500万阻止提交 → 字段 apply_increase_amount 的 max:4999999.99",
774
+ "flow_events: ins.state!='draft' 状态判断 → 已省略(新版 onSubmit 仅提交时触发)",
775
+ "form_script: 根据类型字段显隐备注 → 字段 remark 的 visibilityRules",
776
+ "form_script: 选择部门后级联加载人员选项 → events.onValueChange",
777
+ "instance_template: {applicant.organization.name} 跨对象引用 → 成员字段 applicant 的 pickerFillRules (organization.name → dept_name)"
778
+ ]
779
+
780
+ ⚠️ 如果 flow_events 或 form_script 不为空但 migrationLog 为空数组,视为错误。每条旧版逻辑都必须有对应的迁移记录,不允许静默丢弃。
781
+ 如果没有提供 flow_events、form_script,或它们为空,则 migrationLog 可省略或为空数组。`;
684
782
  }
685
783
 
686
784
  exports.default = router;
@@ -14,10 +14,10 @@ router.get('/api/workflow/v2/:box/filter', requireAuthentication, async function
14
14
  const userSession = req.user;
15
15
  const { userId, is_space_admin, spaceId } = userSession;
16
16
  // TODO 按应用分类显示
17
- const { app, flowId } = req.query;
17
+ const { app, flowId, additionalFilters } = req.query;
18
18
  const { box } = req.params;
19
19
  const filter = await objectql.getSteedosSchema().broker.call("instance.getBoxFilters", {
20
- box, appId: app, flowId, userId, is_space_admin, spaceId
20
+ box, appId: app, flowId, additionalFilters, userId, is_space_admin, spaceId
21
21
  })
22
22
  return res.send({
23
23
  filter
@@ -144,37 +144,33 @@ module.exports = {
144
144
  }
145
145
  } else {
146
146
  var categories = await this.getSpaceCategories(categoriesIds, userSession);
147
+ const allFilters = [
148
+ ["category", "in", _.map(categories, '_id')],
149
+ ["state", "=", "enabled"],
150
+ ];
151
+ if (action === "new") allFilters.push(["forbid_initiate_instance", "!=", true]);
152
+ if (keywordsFilter) allFilters.push(keywordsFilter);
153
+ const allFlows = await objectql.getObject("flows").find({
154
+ filters: allFilters,
155
+ fields: ['_id', 'name', 'sort_no', 'category', 'perms'],
156
+ });
157
+ const flowsByCategory = _.groupBy(allFlows, 'category');
147
158
  for (const category of categories) {
148
- const filters = [
149
- ["category", "=", category._id],
150
- ["state", "=", "enabled"],
151
- ];
152
- if(action === "new"){
153
- filters.push(["forbid_initiate_instance", "!=", true]);
154
- }
155
- if (keywordsFilter) {
156
- filters.push(keywordsFilter)
157
- }
158
- // console.log(`filters`, filters)
159
- const categoryFlows = await objectql.getObject("flows").find({
160
- filters: filters,
161
- fields: ['_id', 'name', 'sort_no', 'category', 'perms'],
162
- sort: "sort_no desc,name",
163
- });
159
+ const catFlows = flowsByCategory[category._id] || [];
164
160
  category.flows = [];
165
- for (const flow of categoryFlows) {
166
- if(allData){
161
+ for (const flow of catFlows) {
162
+ if (allData) {
163
+ category.flows.push(flow);
164
+ } else if (this.canAdd(flow, userSession)) {
167
165
  category.flows.push(flow);
168
- }else{
169
- if (this.canAdd(flow, userSession)) {
166
+ } else if (action === 'query') {
167
+ if (is_space_admin || this.canMonitor(flow, userSession) || this.canAdmin(flow, userSession)) {
170
168
  category.flows.push(flow);
171
- } else if (action == 'query') {
172
- if (is_space_admin || this.canMonitor(flow, userSession) || this.canAdmin(flow, userSession)) {
173
- category.flows.push(flow);
174
- }
175
169
  }
176
170
  }
177
171
  }
172
+ // Sort per-category in memory to avoid MongoDB large-dataset sort RAM limit
173
+ category.flows = _.orderBy(category.flows, ['sort_no', 'name'], ['desc', 'asc']);
178
174
  }
179
175
  data = categories;
180
176
  }
@@ -134,8 +134,7 @@ module.exports = {
134
134
  },
135
135
  getBoxFilters: {
136
136
  async handler(ctx){
137
- const { appId, box, flowId, userId, is_space_admin, spaceId } = ctx.params;
138
- const categoriesIds = await this.getAppCategoriesIds(appId);
137
+ const { appId, box, flowId, userId, is_space_admin, spaceId, additionalFilters } = ctx.params;
139
138
  const filter = [];
140
139
  switch (box) {
141
140
  case 'inbox':
@@ -182,8 +181,11 @@ module.exports = {
182
181
  break;
183
182
  }
184
183
 
185
- if(categoriesIds && categoriesIds.length > 0){
186
- filter.push(['category', 'in', categoriesIds])
184
+ if(!additionalFilters || additionalFilters.indexOf('category') < 0){
185
+ const categoriesIds = await this.getAppCategoriesIds(appId);
186
+ if(categoriesIds && categoriesIds.length > 0){
187
+ filter.push(['category', 'in', categoriesIds])
188
+ }
187
189
  }
188
190
 
189
191
  return filter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.39",
3
+ "version": "3.0.40",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -51,6 +51,20 @@ module.exports = {
51
51
  }
52
52
  }
53
53
  },
54
+ getUser: {
55
+ rest: {
56
+ method: 'GET',
57
+ fullPath: '/api/workflow/getCurrentUser'
58
+ },
59
+ async handler(ctx) {
60
+ try {
61
+ return ctx.meta.user;
62
+ } catch (error) {
63
+ console.error(error);
64
+ return false;
65
+ }
66
+ }
67
+ },
54
68
  send_badge_to_user: {
55
69
  params: {
56
70
  send_from: { type: 'string', optional: false },