@steedos-labs/plugin-workflow 3.0.45 → 3.0.47

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 (38) 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-CH5Rzd1r.css +1 -0
  4. package/designer/dist/assets/index-Z3wTJs2h.js +952 -0
  5. package/designer/dist/index.html +2 -2
  6. package/main/default/manager/handlers_manager.js +4 -4
  7. package/main/default/manager/import.js +20 -2
  8. package/main/default/manager/uuflow_manager.js +1 -1
  9. package/main/default/objects/instances/buttons/instance_delete.button.yml +1 -1
  10. package/main/default/objects/instances/buttons/instance_related.button.yml +4 -1
  11. package/main/default/objects/instances/buttons/instance_terminate.button.yml +1 -1
  12. package/main/default/routes/am.router.js +36 -1
  13. package/main/default/routes/amis_form_design.router.js +1 -1
  14. package/main/default/routes/api_workflow_ai_form_design.router.js +43 -24
  15. package/main/default/routes/api_workflow_ai_form_design_stream.router.js +15 -24
  16. package/main/default/routes/api_workflow_instance_forward.router.js +2 -2
  17. package/main/default/routes/api_workflow_next_step_users.router.js +18 -1
  18. package/main/default/routes/flow_form_design.ejs +13 -1
  19. package/main/default/routes/flow_form_design.router.js +4 -3
  20. package/main/default/test/FORMULA_SCAN_GUIDE.md +258 -0
  21. package/main/default/test/prompt-formula-analyze.md +59 -0
  22. package/main/default/test/prompt-formula-fix.md +75 -0
  23. package/main/default/test/reports/formula-scan/.gitkeep +0 -0
  24. package/main/default/test/reports/formula-scan/SCAN_HISTORY.md +165 -0
  25. package/main/default/test/reports/formula-scan/scan-result-v6-20260324.json +31846 -0
  26. package/main/default/test/reports/formula-scan/scan-result-v7-20260324.json +31543 -0
  27. package/main/default/test/run_approval_comments_upgrade.js +135 -0
  28. package/main/default/test/scan_production_formulas.js +573 -0
  29. package/main/default/test/test_formula_compat.js +52 -2
  30. package/main/default/utils/designerManager.js +2 -1
  31. package/main/default/utils/formula-compat.js +15 -1
  32. package/package.json +1 -1
  33. package/public/amis-renderer/amis-renderer.css +1 -1
  34. package/public/amis-renderer/amis-renderer.js +1 -1
  35. package/public/workflow/index.css +4 -0
  36. package/src/schema/steedos_form_schema.amis.js +3 -1
  37. package/designer/dist/assets/index-B5zmn1Wt.css +0 -1
  38. package/designer/dist/assets/index-D7W5yEF4.js +0 -943
@@ -5,8 +5,8 @@
5
5
  <link rel="shortcut icon" type="image/svg+xml" href="/images/logo.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>designer</title>
8
- <script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-D7W5yEF4.js"></script>
9
- <link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-B5zmn1Wt.css">
8
+ <script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-Z3wTJs2h.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-CH5Rzd1r.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -285,7 +285,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
285
285
  let field_code = null;
286
286
 
287
287
  for (const form_field of form_fields) {
288
- if (form_field._id === approver_org_field) {
288
+ if (form_field._id === approver_org_field || form_field.code === approver_org_field || form_field.name === approver_org_field) {
289
289
  field_code = form_field.code;
290
290
  break;
291
291
  }
@@ -361,7 +361,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
361
361
  let field_code = null;
362
362
 
363
363
  for (const form_field of form_fields) {
364
- if (form_field._id === approver_org_field) {
364
+ if (form_field._id === approver_org_field || form_field.code === approver_org_field || form_field.name === approver_org_field) {
365
365
  field_code = form_field.code;
366
366
  break;
367
367
  }
@@ -466,7 +466,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
466
466
  let field_code = null;
467
467
 
468
468
  for (const form_field of form_fields) {
469
- if (form_field._id === approver_user_field) {
469
+ if (form_field._id === approver_user_field || form_field.code === approver_user_field || form_field.name === approver_user_field) {
470
470
  field_code = form_field.code;
471
471
  break;
472
472
  }
@@ -546,7 +546,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
546
546
  let field_code = null;
547
547
 
548
548
  for (const form_field of form_fields) {
549
- if (form_field._id === approver_user_field) {
549
+ if (form_field._id === approver_user_field || form_field.code === approver_user_field || form_field.name === approver_user_field) {
550
550
  field_code = form_field.code;
551
551
  break;
552
552
  }
@@ -94,7 +94,7 @@ async function upgradeForm(formId, form, currentUserId, spaceId) {
94
94
  pass = recordsCount > 0;
95
95
  }
96
96
 
97
- if (pass === true && ff["current"]["start_date"]) {
97
+ if (pass === true) {
98
98
  formUpdateObj.$push = { 'historys': ff["current"] };
99
99
  current._id = _makeNewID();
100
100
  current._rev = ff["current"]["_rev"] + 1;
@@ -124,6 +124,22 @@ async function upgradeForm(formId, form, currentUserId, spaceId) {
124
124
  current.form_script = form["current"]["form_script"];
125
125
  current.name_forumla = form["current"]["name_forumla"];
126
126
 
127
+ current.style = form["current"]["style"];
128
+ current.mode = form["current"]["mode"];
129
+ current.wizard_mode = form["current"]["wizard_mode"];
130
+ current.amis_schema = form["current"]["amis_schema"]
131
+
132
+ current.version = form["current"]["version"];
133
+ current.viewMode = form["current"]["viewMode"];
134
+ current.tableColumns = form["current"]["tableColumns"];
135
+
136
+ current.events = form["current"]["events"];
137
+
138
+ current.formTitle = form["current"]["formTitle"];
139
+ current.tableTitleColor = form["current"]["tableTitleColor"];
140
+ current.tableBorderColor = form["current"]["tableBorderColor"];
141
+ current.tableShowOuterBorder = form["current"]["tableShowOuterBorder"];
142
+
127
143
  formUpdateObj.$set = {
128
144
  'current': current,
129
145
  'name': form["name"],
@@ -232,6 +248,7 @@ async function upgradeFlow(flowCome, userId, flowId) {
232
248
  updateObj.$set.modified = now;
233
249
  updateObj.$set.modified_by = userId;
234
250
  updateObj.$set.events = flowCome['events'] || '';
251
+ updateObj.$set.upgraded = flowCome.upgraded || false;
235
252
 
236
253
  const form = await formCollection.findOne({ _id: flow.form }, { projection: { category: 1 } });
237
254
  updateObj.$set.category = form['category'];
@@ -409,9 +426,10 @@ async function workflow(uid, spaceId, form, enabled, company_id, options = {}) {
409
426
  form.modified_by = uid;
410
427
  form.historys = [];
411
428
 
412
- const fields = addFieldName(form.current?.fields || []);
429
+ const fields = form.current.version === 'v2' ? form.current?.fields || [] : addFieldName(form.current?.fields || []);
413
430
 
414
431
  form.current = {
432
+ ...form.current,
415
433
  _id: _makeNewID(),
416
434
  _rev: 1,
417
435
  form: form_id,
@@ -1191,7 +1191,7 @@ UUFlowManager.getInstanceName = async function (instance, vals) {
1191
1191
  if (form_v.fields) {
1192
1192
  for (const field of form_v.fields) {
1193
1193
  if (["select", "multiSelect", "radio"].includes(field.type)) {
1194
- const fieldOptions = field.options?.split("\n").map((n) => {
1194
+ const fieldOptions = _.isArray(field.options) ? field.options : field.options?.split("\n").map((n) => {
1195
1195
  const itemSplits = n.split(":");
1196
1196
  return {
1197
1197
  label: itemSplits[0],
@@ -64,4 +64,4 @@ label: 删除
64
64
  'on': record_only
65
65
  type: amis_button
66
66
  visible: true
67
- sort: 500
67
+ sort: 2500
@@ -35,6 +35,9 @@ amis_schema: |-
35
35
  "rowClassNameExpr": "<%= data.__selected === true ? 'hidden' : '' %>",
36
36
  "perPage": 20
37
37
  },
38
+ "searchable_default": {
39
+ "submit_date": "${[STARTOF(DATEMODIFY(NOW(), -180, 'day'), 'day'),ENDOF(NOW(), 'day')]}"
40
+ },
38
41
  "amis": {
39
42
  "id": "u:f0273e374d19",
40
43
  "embed": true,
@@ -47,7 +50,7 @@ amis_schema: |-
47
50
  "source": {
48
51
  "method": "post",
49
52
  "url": "${context.rootUrl}/graphql",
50
- "requestAdaptor": "var searchableFilter = SteedosUI.getSearchFilter(api.data.$self) || []; const { pageNo, pageSize, keywords = '' } = api.data;\nvar keywordsFilters = SteedosUI.getKeywordsSearchFilter(api.data.$self.__keywords_lookup__related_instances__to__instances, [\"name\", \"flow_name\",\"submitter\",\"submit_date\"]);\nif (keywordsFilters && keywordsFilters.length > 0) {\n searchableFilter.push(keywordsFilters);\n}\nconsole.log(\"===\", JSON.stringify(api.data));\napi.data = {\n query: `\n query{\n rows: instances__getRelatedInstances(keywords: \"${keywords}\", top: ${pageSize || 20}, skip: ${(pageNo - 1) * pageSize}, filters: ${JSON.stringify(searchableFilter)}){\n _id,\n name,\n flow_name,\n submit_date,\n submitter\n _display:_ui{\n submit_date,\n submitter\n }\n },\n count: instances__getRelatedInstances__count(filters: ${JSON.stringify(searchableFilter)}, keywords: \"${keywords}\")\n }\n `\n};\nreturn api;",
53
+ "requestAdaptor": "var searchableDefaultConfig = {\"submit_date\": \"${[STARTOF(DATEMODIFY(NOW(), -180, 'day'), 'day'),ENDOF(NOW(), 'day')]}\"};\nvar filterFormValues = JSON.parse(JSON.stringify(searchableDefaultConfig));\nif (_.isObject(filterFormValues)) {\n _.each(filterFormValues, function(v, k) {\n var isAmisFormulaValue = typeof v === \"string\" && v.indexOf(\"${\") > -1;\n if (isAmisFormulaValue) {\n filterFormValues[k] = AmisCore.evaluate(v, context);\n }\n });\n var fields = api.data.$self.uiSchema && api.data.$self.uiSchema.fields;\n filterFormValues = SteedosUI.getSearchFilterFormValues(filterFormValues, fields);\n}\nvar selfData = JSON.parse(JSON.stringify(api.data.$self));\nvar searchableFilter = SteedosUI.getSearchFilter(Object.assign({}, filterFormValues, selfData)) || []; const { pageNo, pageSize, keywords = '' } = api.data;\nvar keywordsFilters = SteedosUI.getKeywordsSearchFilter(api.data.$self.__keywords_lookup__related_instances__to__instances, [\"name\", \"flow_name\",\"submitter\",\"submit_date\"]);\nif (keywordsFilters && keywordsFilters.length > 0) {\n searchableFilter.push(keywordsFilters);\n}\nconsole.log(\"===\", JSON.stringify(api.data));\napi.data = {\n query: `\n query{\n rows: instances__getRelatedInstances(keywords: \"${keywords}\", top: ${pageSize || 20}, skip: ${(pageNo - 1) * pageSize}, filters: ${JSON.stringify(searchableFilter)}){\n _id,\n name,\n flow_name,\n submit_date,\n submitter\n _display:_ui{\n submit_date,\n submitter\n }\n },\n count: instances__getRelatedInstances__count(filters: ${JSON.stringify(searchableFilter)}, keywords: \"${keywords}\")\n }\n `\n};\nreturn api;",
51
54
  "headers": {
52
55
  "Authorization": "Bearer ${context.tenantId},${context.authToken}"
53
56
  },
@@ -64,7 +64,7 @@ amis_schema: |-
64
64
  "componentId": "",
65
65
  "args": {
66
66
  "blank": false,
67
- "url": "/app/${appId}/instances/grid/${side_listview_id}"
67
+ "url": "/app/${appId}/${side_listview_id === 'inbox' || side_listview_id === 'outbox' ? 'instance_tasks' : 'instances'}/grid/${side_listview_id}"
68
68
  },
69
69
  "actionType": "url"
70
70
  }
@@ -99,6 +99,26 @@ router.get('/am/designer/startup/v2', async function (req, res) {
99
99
 
100
100
  let form = await formCollection.findOne({ _id: flow.form }, { projection: { historys: 0 } });
101
101
 
102
+ // Resolve created_by / modified_by user names for display
103
+ let usersCollection = await getCollection('users');
104
+ const userIds = [flow.created_by, flow.modified_by].filter(Boolean);
105
+ if (userIds.length > 0) {
106
+ const userDocs = await usersCollection.find(
107
+ { _id: { $in: userIds } },
108
+ { projection: { name: 1 } }
109
+ ).toArray();
110
+ const userMap = {};
111
+ for (const u of userDocs) {
112
+ userMap[u._id] = u.name;
113
+ }
114
+ if (flow.created_by && userMap[flow.created_by]) {
115
+ flow.created_by_name = userMap[flow.created_by];
116
+ }
117
+ if (flow.modified_by && userMap[flow.modified_by]) {
118
+ flow.modified_by_name = userMap[flow.modified_by];
119
+ }
120
+ }
121
+
102
122
  res.send({
103
123
  flow,
104
124
  form,
@@ -464,6 +484,7 @@ router.put('/am/flows', async function (req, res) {
464
484
  let formId = flowCome["form"];
465
485
  let flowId = flowCome["id"];
466
486
  let upgraded = flowCome['upgraded'];
487
+ delete flowCome['decription'];
467
488
  let now = new Date();
468
489
 
469
490
  await designerManager.checkBeforeFlow(spaceId, formId);
@@ -565,7 +586,7 @@ router.put('/am/flows', async function (req, res) {
565
586
  updateObj.$set.is_valid = flowCome['is_valid'];
566
587
  updateObj.$set.flowtype = flowCome['flowtype'];
567
588
  updateObj.$set.help_text = flowCome['help_text'];
568
- updateObj.$set.decription = flowCome['descriptions'];
589
+ updateObj.$set.description = flowCome['descriptions'];
569
590
  updateObj.$set.error_message = flowCome['error_message'];
570
591
  updateObj.$set.modified = now;
571
592
  updateObj.$set.modified_by = userId;
@@ -584,6 +605,20 @@ router.put('/am/flows', async function (req, res) {
584
605
  updateObj.$set.perms = flowCome['perms'];
585
606
  }
586
607
 
608
+ // Save flow-level properties
609
+ if (flowCome['allow_select_step'] !== undefined) {
610
+ updateObj.$set.allow_select_step = flowCome['allow_select_step'];
611
+ }
612
+ if (flowCome['timeout_auto_submit'] !== undefined) {
613
+ updateObj.$set.timeout_auto_submit = flowCome['timeout_auto_submit'];
614
+ }
615
+ if (flowCome['auto_remind'] !== undefined) {
616
+ updateObj.$set.auto_remind = flowCome['auto_remind'];
617
+ }
618
+ if (flowCome['enable_distribute_instance_related'] !== undefined) {
619
+ updateObj.$set.enable_distribute_instance_related = flowCome['enable_distribute_instance_related'];
620
+ }
621
+
587
622
  // flow对象上添加categoryId
588
623
  let form = await formCollection.findOne({_id: flow.form}, {
589
624
  projection: {
@@ -35,7 +35,7 @@ router.get('/api/amisFormDesign', requireAuthentication, async function (req, re
35
35
 
36
36
  const retUrl = process.env.ROOT_URL + `/app/admin/flows/view/${flowId}`
37
37
  const steedosBuilderUrl = process.env.STEEDOS_BUILDER_URL || 'https://builder.steedos.cn';
38
- const builderHost = `${steedosBuilderUrl}/amis?${assetUrl}&retUrl=${retUrl}`;
38
+ const builderHost = `${steedosBuilderUrl}/amis?${assetUrl}&retUrl=${retUrl}&unpkgUrl=${process.env.STEEDOS_UNPKG_URL || 'https://unpkg.steedos.cn'}`;
39
39
 
40
40
  // let data = fs.readFileSync(__dirname+'/design.html', 'utf8');
41
41
  // res.send(data.replace('SteedosBuilderHost',steedosBuilderHost).replace('DataContext', JSON.stringify(dataContext)));
@@ -77,17 +77,18 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
77
77
  - "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
78
78
  - "richtext" — 富文本(支持格式化文本、颜色、链接等)
79
79
  - required: 布尔值,是否必填
80
- - colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
81
80
  - description: 字符串,字段说明/提示文字,支持 HTML
82
81
  - placeholder: 字符串,输入占位提示
83
82
  - defaultValue: 字符串,默认值
83
+ - readonly: 布尔值,是否只读(只读字段不可编辑,仅展示)
84
+ - showLabel: 布尔值,是否显示字段标签,默认 true。设为 false 时隐藏标签,只显示输入区域
84
85
 
85
86
  ### 特定类型的额外字段
86
87
 
87
88
  - select/multiSelect/radio 类型:
88
89
  - options: 数组,选项列表,格式 [{ "label": "显示文本", "value": "值" }]
89
90
 
90
- - number 类型:
91
+ - number 类型(纯手工输入的数字字段,**不含公式**;如果字段有公式/自动计算,必须用 formula 类型):
91
92
  - min: 数字,最小值
92
93
  - max: 数字,最大值
93
94
  - precision: 数字,小数位数
@@ -97,8 +98,10 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
97
98
  - maxLength: 数字,最大字符数
98
99
  - minLength: 数字,最小字符数
99
100
 
100
- - formula 类型:
101
+ - formula 类型(凡是有公式/自动计算的字段,都必须用此类型,禁止用 number+formula 组合):
101
102
  - formula: 字符串,公式表达式。语法:用 \${} 包裹表达式,内部直接使用字段名(不需要额外括号)。没有 \${} 的部分作为纯文本。
103
+ - valueType: 字符串,公式计算结果的值类型。可选值:"number"(数值,默认)、"string"(字符串,如 CONCAT/RMB/TEXT 等返回文本的公式)、"date"(日期)、"dateTime"(日期时间)、"boolean"(布尔)。根据公式计算结果的实际数据类型设置。示例:算术运算/SUM/ROUND 等→ "number";CONCAT/RMB/TEXT 等→ "string";DATE/TODAY 等→ "date"
104
+ - precision: 数字,小数位数(仅当 valueType 为 "number" 时有效,如金额合计字段设 precision: 2)
102
105
  - ⚠️ 严格限制:只能使用下方列出的函数,禁止使用任何 Excel 函数、JavaScript 函数或其他未列出的函数(如 NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、parseFloat、Math.floor 等一律不可用)。如果某个功能无法用已有函数实现,用 IF/AND/OR 的组合或算术运算代替,不要臆造函数名。
103
106
  - 支持的函数(完整列表,共 36 个,仅此列表内的函数可用):
104
107
  - 聚合:SUM(...) 求和,AVG/AVERAGE(...) 平均,MAX(...) 最大,MIN(...) 最小,COUNT(...) 计数(空/0不计)
@@ -130,6 +133,9 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
130
133
  - table 类型(子表):
131
134
  - children: 数组,子字段列表,每个子字段结构与普通字段相同(但不能嵌套 table/section/grid)
132
135
  - 子字段可用类型:text, textarea, number, date, datetime, time, select, multiSelect, checkbox, radio, file, image, member, memberMulti, org, orgMulti, formula
136
+ - showRowNumber: 布尔值,是否显示行号,默认 true
137
+ - minRows: 数字,最少行数(用户不能删减到少于此数)
138
+ - maxRows: 数字,最多行数(用户不能新增超过此数)
133
139
 
134
140
  - grid 类型(网格):
135
141
  - 用于复杂的自由表格布局,支持任意行列数、合并/拆分单元格、嵌入字段或静态文本
@@ -235,12 +241,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
235
241
  ## 规则
236
242
  1. 修改时尽可能保留用户已有字段的 name,只改需要改的部分
237
243
  2. 新增字段的 name 必须是合法的 JavaScript 变量名(字母/下划线开头,仅含字母、数字、下划线),要语义清晰
238
- 3. section 分组、table 子表、grid 网格的 colspan 必须等于 tableColumns(即独占整行)
244
+ 3. section 分组、table 子表、grid 网格始终独占整行,不需要设置 colspan
239
245
  4. formula 字段的公式表达式使用 \${} 语法,内部直接引用字段名,如 "\${unit_price * quantity}";公式内部不能再嵌套 \${},错误示例:"\${RMB(\${amount})}",正确示例:"\${RMB(amount)}"
240
246
  5. formula 字段只能使用上方"支持的函数(完整列表)"中列出的 36 个函数,禁止使用任何 Excel 专有函数(NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、IFERROR、EOMONTH 等)或 JavaScript 方法(Math.floor、parseFloat、toString 等),违反此规则会导致运行时报错
241
247
  6. 如果用户只要求修改脚本/事件,则 fields 保持原样不变
242
248
  7. 如果用户只要求修改字段,则 events 保持原样不变
243
249
  8. 返回完整的 fields 数组和 events 对象
250
+ 9. **grid 类型字段必须同时返回 gridRows、gridCols、gridData、children 四个属性**,缺少任何一个都会导致网格渲染为空白。gridData 中 cellType 为 "input" 的单元格必须有 fieldId,且该 fieldId 需在 children 数组中有对应的子字段
251
+ 10. 修改已有 grid 字段时,必须保留其完整的 gridData 和 children 数据,不要丢弃未被修改的子字段。如果用户未要求改动 grid 内容,应原样返回 grid 的所有属性
252
+ 11. 每个字段的类型特有属性必须完整返回:number 类型的 min/max/precision/thousandSeparator、text/textarea 的 maxLength/minLength、lookup 的 reference_to/lookupLabelField/lookupFillRules、member/org 的 pickerFillRules、reference 的 reference_to/displayFields 等。不要遗漏这些属性
253
+ 12. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "${a + b}" },正确示例:{ type: "formula", formula: "${a + b}", precision: 2 }。grid/table 的 children 子字段同样适用此规则
244
254
 
245
255
  ## 额外待转换的数据(升级场景)
246
256
 
@@ -266,7 +276,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
266
276
  - 分析模板时,首先统计 HTML 表格展开所有 colspan 后的**实际最大总列数**(包含标签列和数据列),然后选择布局方案:
267
277
  - **⭐ 优先使用方案二(grid 网格字段)**。只有当模板表格极其简单(总列数 ≤ 6 且无 rowspan/colspan)时才考虑方案一。如果你不确定该用哪个,请使用方案二。
268
278
 
269
- #### 方案一:简单布局(tableColumns + tableColspan
279
+ #### 方案一:简单布局(tableColumns)
270
280
  适用条件:模板 HTML 表格展开后总列数 ≤ 6(含标签列),每行最多 3 个输入字段,且无 rowspan/colspan 合并单元格。**如果模板中出现任何 rowspan 或 colspan,则不得使用方案一,必须使用方案二。**
271
281
 
272
282
  1. **确定 tableColumns**(每行字段数):
@@ -274,19 +284,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
274
284
  - 模板中每行有 2 个输入字段 → tableColumns=2
275
285
  - 模板中每行有 3 个输入字段 → tableColumns=3(最大值)
276
286
 
277
- 2. **字段 tableColspan**:基于 tableColumns 栅格设置,默认每个字段占 1
278
- - tableColumns=2 时:每个字段默认 tableColspan=1,独占一行的字段 tableColspan=2
279
- - tableColumns=3 时:每个字段默认 tableColspan=1,跨两列的 tableColspan=2,独占一行的 tableColspan=3
280
- - 注意:tableColspan 的最大值等于 tableColumns
287
+ 2. **字段排布**:每个字段默认占 1 列,按顺序自动流入行中
281
288
 
282
289
  3. **分组布局**:模板中的分类标题应转为 type="section" 字段
283
290
 
284
291
  4. **字段 label**:优先使用模板中该字段旁边的显示文本
285
292
 
286
293
  #### 方案二:复杂表格布局(grid 网格字段)⭐ 默认首选方案
287
- 适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3) + colspan 简单表达。**绝大多数审批单模板都应使用此方案。**
294
+ 适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3) 简单表达。**绝大多数审批单模板都应使用此方案。**
288
295
 
289
- **必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns + colspan 简化处理。如果模板有任何 rowspan/colspan 合并单元格,就必须使用 grid。**
296
+ **必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns 简化处理。如果模板有任何 rowspan/colspan 合并单元格,就必须使用 grid。**
290
297
 
291
298
  转换步骤:
292
299
  1. **分析 HTML 表格结构**:统计表格的总行数和总列数(考虑 colspan 展开后的最大列数),确定 gridRows 和 gridCols
@@ -296,7 +303,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
296
303
  - HTML 的 \`rowspan\`/\`colspan\` 属性直接映射到 gridData 单元格的 \`rowspan\`/\`colspan\`
297
304
  3. **构建 children 数组**:为每个 input 类型的单元格创建对应的子字段,通过 fieldId 关联
298
305
  4. **设置 gridColumnWidths**:根据 HTML 表格各列的实际宽度比例设置百分比数组
299
- 5. **grid 字段的 colspan 设为 tableColumns**(独占整行),tableColumns 可设为 1
306
+ 5. **grid 字段始终独占整行**,tableColumns 可设为 1
300
307
 
301
308
  转换示例 — 假设模板有一个 5 列表格(标题+4个数据列):
302
309
  HTML: \`<tr><td rowspan="3">工程名称</td><td>材料费</td><td>{{values.clf_bq}}</td><td>{{values.clf_lj}}</td><td>{{values.clf_bz}}</td></tr>\`
@@ -304,7 +311,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
304
311
  \`\`\`json
305
312
  {
306
313
  "type": "grid", "label": "费用明细", "name": "cost_grid",
307
- "gridRows": 10, "gridCols": 5, "colspan": 1,
314
+ "gridRows": 10, "gridCols": 5,
308
315
  "gridColumnWidths": [15, 15, 25, 25, 20],
309
316
  "gridData": [
310
317
  { "row": 0, "col": 0, "cellType": "label", "value": "工程名称", "rowspan": 3, "align": "center" },
@@ -541,8 +548,6 @@ ${userRequest}
541
548
  return s || 'field_' + Math.random().toString(36).slice(2, 6);
542
549
  }
543
550
 
544
- const tblCols = (typeof result.tableColumns === 'number' && result.tableColumns >= 1) ? Math.min(result.tableColumns, 3) : 2;
545
-
546
551
  if (result.fields) {
547
552
  for (const field of result.fields) {
548
553
  // Backward compat: map legacy userPicker/groupPicker to new types
@@ -559,15 +564,9 @@ ${userRequest}
559
564
  if (field.name && !jsIdentifierRe.test(field.name)) {
560
565
  field.name = sanitizeName(field.name);
561
566
  }
562
- // Convert AI colspan (1..tableColumns) to tableColspan + 12-grid colspan
563
- if (field.type === 'section' || field.type === 'table' || field.type === 'grid') {
564
- field.tableColspan = tblCols;
565
- field.colspan = 12;
566
- } else {
567
- const aiColspan = Math.max(1, Math.min(Number(field.colspan) || 1, tblCols));
568
- field.tableColspan = aiColspan;
569
- field.colspan = (aiColspan >= tblCols) ? 12 : Math.round(12 / tblCols * aiColspan);
570
- }
567
+ // Remove colspan/tableColspan from AI output layout is determined by field type defaults
568
+ delete field.colspan;
569
+ delete field.tableColspan;
571
570
  // Validate children types for table
572
571
  if (field.type === 'table' && Array.isArray(field.children)) {
573
572
  for (const child of field.children) {
@@ -590,6 +589,26 @@ ${userRequest}
590
589
  }
591
590
  }
592
591
  }
592
+ // Validate children types for grid
593
+ if (field.type === 'grid' && Array.isArray(field.children)) {
594
+ for (const child of field.children) {
595
+ // Backward compat: map legacy types in children
596
+ if (child.type === 'userPicker') child.type = child.pickerMultiple ? 'memberMulti' : 'member';
597
+ if (child.type === 'groupPicker') child.type = child.pickerMultiple ? 'orgMulti' : 'org';
598
+ if (child.type === 'select' && child.pickerMultiple) child.type = 'multiSelect';
599
+ if (child.type === 'html') child.type = 'richtext';
600
+ if (child.type && !validTypes.includes(child.type)) {
601
+ child.type = 'text';
602
+ }
603
+ if (child.name && !jsIdentifierRe.test(child.name)) {
604
+ child.name = sanitizeName(child.name);
605
+ }
606
+ // Grid children cannot be section or grid
607
+ if (child.type === 'section' || child.type === 'grid') {
608
+ child.type = 'text';
609
+ }
610
+ }
611
+ }
593
612
  }
594
613
  }
595
614
 
@@ -315,8 +315,6 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
315
315
  return s || 'field_' + Math.random().toString(36).slice(2, 6);
316
316
  }
317
317
 
318
- const tblCols = (typeof result.tableColumns === 'number' && result.tableColumns >= 1) ? Math.min(result.tableColumns, 3) : 2;
319
-
320
318
  if (result.fields) {
321
319
  for (const field of result.fields) {
322
320
  // Backward compat: map legacy userPicker/groupPicker to new types
@@ -332,15 +330,9 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
332
330
  if (field.name && !jsIdentifierRe.test(field.name)) {
333
331
  field.name = sanitizeName(field.name);
334
332
  }
335
- if (field.type === 'section' || field.type === 'table' || field.type === 'grid') {
336
- field.tableColspan = tblCols;
337
- field.colspan = 12;
338
- } else {
339
- // AI outputs colspan as 1..tableColumns; convert to tableColspan + 12-grid colspan
340
- const aiColspan = Math.max(1, Math.min(Number(field.colspan) || 1, tblCols));
341
- field.tableColspan = aiColspan;
342
- field.colspan = (aiColspan >= tblCols) ? 12 : Math.round(12 / tblCols * aiColspan);
343
- }
333
+ // Remove colspan/tableColspan from AI output layout is determined by field type defaults
334
+ delete field.colspan;
335
+ delete field.tableColspan;
344
336
  if (field.type === 'table' && Array.isArray(field.children)) {
345
337
  for (const child of field.children) {
346
338
  // Backward compat: map legacy types in children
@@ -443,7 +435,6 @@ function buildSystemPrompt() {
443
435
  - "richtext" — 富文本(支持格式化文本、颜色、链接等)
444
436
  - "autoNumber" — 自动编号(只读字段,自动生成带前缀、日期、序号的唯一编号)
445
437
  - required: 布尔值,是否必填
446
- - colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
447
438
  - description: 字符串,字段说明/提示文字,支持 HTML
448
439
  - placeholder: 字符串,输入占位提示
449
440
  - defaultValue: 字符串,默认值
@@ -453,7 +444,7 @@ function buildSystemPrompt() {
453
444
  - select/multiSelect/radio 类型:
454
445
  - options: 数组,选项列表,格式 [{ "label": "显示文本", "value": "值" }]
455
446
 
456
- - number 类型:
447
+ - number 类型(纯手工输入的数字字段,**不含公式**;如果字段有公式/自动计算,必须用 formula 类型):
457
448
  - min: 数字,最小值
458
449
  - max: 数字,最大值
459
450
  - precision: 数字,小数位数
@@ -463,8 +454,10 @@ function buildSystemPrompt() {
463
454
  - maxLength: 数字,最大字符数
464
455
  - minLength: 数字,最小字符数
465
456
 
466
- - formula 类型:
457
+ - formula 类型(凡是有公式/自动计算的字段,都必须用此类型,禁止用 number+formula 组合):
467
458
  - formula: 字符串,公式表达式。语法:用 \${} 包裹表达式,内部直接使用字段名(不需要额外括号)。没有 \${} 的部分作为纯文本。
459
+ - valueType: 字符串,公式计算结果的值类型。可选值:"number"(数值,默认)、"string"(字符串,如 CONCAT/RMB/TEXT 等返回文本的公式)、"date"(日期)、"dateTime"(日期时间)、"boolean"(布尔)。根据公式计算结果的实际数据类型设置。示例:算术运算/SUM/ROUND 等→ "number";CONCAT/RMB/TEXT 等→ "string";DATE/TODAY 等→ "date"
460
+ - precision: 数字,小数位数(仅当 valueType 为 "number" 时有效,如金额合计字段设 precision: 2)
468
461
  - ⚠️ 严格限制:只能使用下方列出的函数,禁止使用任何 Excel 函数、JavaScript 函数或其他未列出的函数(如 NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、parseFloat、Math.floor 等一律不可用)。如果某个功能无法用已有函数实现,用 IF/AND/OR 的组合或算术运算代替,不要臆造函数名。
469
462
  - 支持的函数(完整列表,共 36 个,仅此列表内的函数可用):
470
463
  - 聚合:SUM(...) 求和,AVG/AVERAGE(...) 平均,MAX(...) 最大,MIN(...) 最小,COUNT(...) 计数(空/0不计)
@@ -611,12 +604,13 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
611
604
  ## 规则
612
605
  1. 修改时尽可能保留用户已有字段的 name,只改需要改的部分
613
606
  2. 新增字段的 name 必须是合法的 JavaScript 变量名(字母/下划线开头,仅含字母、数字、下划线),要语义清晰
614
- 3. section 分组、table 子表、grid 网格的 colspan 必须等于 tableColumns(即独占整行)
607
+ 3. section 分组、table 子表、grid 网格始终独占整行,不需要设置 colspan
615
608
  4. formula 字段的公式表达式使用 \${} 语法,内部直接引用字段名,如 "\${unit_price * quantity}";公式内部不能再嵌套 \${},错误示例:"\${RMB(\${amount})}",正确示例:"\${RMB(amount)}"
616
609
  5. formula 字段只能使用上方"支持的函数(完整列表)"中列出的 36 个函数,禁止使用任何 Excel 专有函数(NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、IFERROR、EOMONTH 等)或 JavaScript 方法(Math.floor、parseFloat、toString 等),违反此规则会导致运行时报错
617
610
  6. 如果用户只要求修改脚本/事件,则 fields 保持原样不变
618
611
  7. 如果用户只要求修改字段,则 events 保持原样不变
619
612
  8. 返回完整的 fields 数组和 events 对象
613
+ 9. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "\${a + b}" },正确示例:{ type: "formula", formula: "\${a + b}", valueType: "number", precision: 2 }。grid/table 的 children 子字段同样适用此规则
620
614
 
621
615
  ## 额外待转换的数据(升级场景)
622
616
 
@@ -642,7 +636,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
642
636
  - 分析模板时,首先统计 HTML 表格展开所有 colspan 后的**实际最大总列数**(包含标签列和数据列),然后选择布局方案:
643
637
  - **⭐ 优先使用方案二(grid 网格字段)**。只有当模板表格极其简单(总列数 ≤ 6 且无 rowspan/colspan)时才考虑方案一。如果你不确定该用哪个,请使用方案二。
644
638
 
645
- #### 方案一:简单布局(tableColumns + tableColspan
639
+ #### 方案一:简单布局(tableColumns)
646
640
  适用条件:模板 HTML 表格展开后总列数 ≤ 6(含标签列),每行最多 3 个输入字段,且无 rowspan/colspan 合并单元格。**如果模板中出现任何 rowspan 或 colspan,则不得使用方案一,必须使用方案二。**
647
641
 
648
642
  1. **确定 tableColumns**(每行字段数):
@@ -650,19 +644,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
650
644
  - 模板中每行有 2 个输入字段 → tableColumns=2
651
645
  - 模板中每行有 3 个输入字段 → tableColumns=3(最大值)
652
646
 
653
- 2. **字段 tableColspan**:基于 tableColumns 栅格设置,默认每个字段占 1
654
- - tableColumns=2 时:每个字段默认 tableColspan=1,独占一行的字段 tableColspan=2
655
- - tableColumns=3 时:每个字段默认 tableColspan=1,跨两列的 tableColspan=2,独占一行的 tableColspan=3
656
- - 注意:tableColspan 的最大值等于 tableColumns
647
+ 2. **字段排布**:每个字段默认占 1 列,按顺序自动流入行中
657
648
 
658
649
  3. **分组布局**:模板中的分类标题应转为 type="section" 字段
659
650
 
660
651
  4. **字段 label**:优先使用模板中该字段旁边的显示文本
661
652
 
662
653
  #### 方案二:复杂表格布局(grid 网格字段)⭐ 默认首选方案
663
- 适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3) + colspan 简单表达。**绝大多数审批单模板都应使用此方案。**
654
+ 适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3) 简单表达。**绝大多数审批单模板都应使用此方案。**
664
655
 
665
- **必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns + colspan 简化处理。如果模板有任何 rowspan/colspan 合并单元格,就必须使用 grid。**
656
+ **必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns 简化处理。如果模板有任何 rowspan/colspan 合并单元格,就必须使用 grid。**
666
657
 
667
658
  转换步骤:
668
659
  1. **分析 HTML 表格结构**:统计表格的总行数和总列数(考虑 colspan 展开后的最大列数),确定 gridRows 和 gridCols
@@ -672,7 +663,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
672
663
  - HTML 的 \`rowspan\`/\`colspan\` 属性直接映射到 gridData 单元格的 \`rowspan\`/\`colspan\`
673
664
  3. **构建 children 数组**:为每个 input 类型的单元格创建对应的子字段,通过 fieldId 关联
674
665
  4. **设置 gridColumnWidths**:根据 HTML 表格各列的实际宽度比例设置百分比数组
675
- 5. **grid 字段的 colspan 设为 tableColumns**(独占整行),tableColumns 可设为 1
666
+ 5. **grid 字段始终独占整行**,tableColumns 可设为 1
676
667
 
677
668
  转换示例 — 假设模板有一个 5 列表格(标题+4个数据列):
678
669
  HTML: \`<tr><td rowspan="3">工程名称</td><td>材料费</td><td>{{values.clf_bq}}</td><td>{{values.clf_lj}}</td><td>{{values.clf_bz}}</td></tr>\`
@@ -680,7 +671,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
680
671
  \`\`\`json
681
672
  {
682
673
  "type": "grid", "label": "费用明细", "name": "cost_grid",
683
- "gridRows": 10, "gridCols": 5, "colspan": 1,
674
+ "gridRows": 10, "gridCols": 5,
684
675
  "gridColumnWidths": [15, 15, 25, 25, 20],
685
676
  "gridData": [
686
677
  { "row": 0, "col": 0, "cellType": "label", "value": "工程名称", "rowspan": 3, "align": "center" },
@@ -205,7 +205,7 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
205
205
  const key = f.code;
206
206
  let old_v = old_values[key];
207
207
  if (old_v) {
208
- const fieldOptions = f.options?.split("\n").map(n => {
208
+ const fieldOptions = _.isArray(f.options) ? f.options : f.options?.split("\n").map(n => {
209
209
  const itemSplits = n.split(":");
210
210
  return {
211
211
  label: itemSplits[0],
@@ -288,7 +288,7 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
288
288
  const key = field.code;
289
289
  let old_v = old_values[key];
290
290
  if (old_v) {
291
- const fieldOptions = field.options?.split("\n").map(n => {
291
+ const fieldOptions = _.isArray(field.options) ? field.options : field.options?.split("\n").map(n => {
292
292
  const itemSplits = n.split(":");
293
293
  return {
294
294
  label: itemSplits[0],
@@ -29,7 +29,24 @@ const getFieldValue = (values, code)=>{
29
29
 
30
30
  const getFormFieldValue = (fields, values, fieldId)=>{
31
31
  const code = getFieldName(fields, fieldId);
32
- return getFieldValue(values, code);
32
+ const _values = getFieldValue(values, code);
33
+
34
+ if(_.isArray(_values)){
35
+ return _.map(_values, (value)=>{
36
+ if(_.isObject(value) && value.id){
37
+ return value.id;
38
+ }else{
39
+ return value;
40
+ }
41
+ })
42
+ }else{
43
+ if(_.isObject(_values) && _values.id){
44
+ return _values.id;
45
+ }else{
46
+ return _values;
47
+ }
48
+ }
49
+
33
50
  }
34
51
 
35
52
  /**
@@ -70,6 +70,7 @@
70
70
  title: "<%=locale%>" === 'zh-CN' ? '流程表单设计器': 'Flow Form Designer',
71
71
  saveText: "<%=locale%>" === 'zh-CN' ? '保存': 'Save',
72
72
  deployText: "<%=locale%>" === 'zh-CN' ? '发布': 'Deploy',
73
+ unpkgUrl: "<%=unpkgUrl%>"
73
74
  };
74
75
 
75
76
  const getArgumentsList = (func)=>{
@@ -279,7 +280,7 @@
279
280
  * 保持同步。
280
281
  */
281
282
  const _getSafeCode = (code) => {
282
- return code.replace(/(/g, '_').replace(/)/g, '').replace(/\(/g, '_').replace(/\)/g, '').replace(/、/g, '_').replace(/,/g, '_').replace(/%/g, '_').replace(/=/g, '_');
283
+ return code.replace(/(/g, '_').replace(/)/g, '').replace(/\(/g, '_').replace(/\)/g, '').replace(/、/g, '_').replace(/,/g, '_').replace(/%/g, '_').replace(/=/g, '_').replace(/:/g, '_').replace(/\//g, '_').replace(/-/g, '_');
283
284
  };
284
285
 
285
286
  /**
@@ -295,6 +296,14 @@
295
296
  if (formula.trim() === '{now}') return '${NOW()}';
296
297
 
297
298
  let newFormula = formula;
299
+
300
+ // 预处理:修正生产数据中的非标准聚合函数语法
301
+ // B: 全角括号 sum({x}) → sum({x})
302
+ newFormula = newFormula.replace(/(sum|average|count|max|min|numToRMB)\s*(/ig, '$1(');
303
+ newFormula = newFormula.replace(/\})/g, '})');
304
+ // C: 缺失括号 sum{x} → sum({x})
305
+ newFormula = newFormula.replace(/(sum|average|count|max|min|numToRMB)\{([^{}]*)\}/ig, '$1({$2})');
306
+
298
307
  const isFunction = newFormula.match(/(sum|average|count|max|min|numToRMB)\s*\(/i);
299
308
  const hasFieldRef = newFormula.match(/\{[^{}]+\}/);
300
309
  const isOperator = newFormula.match(/[\+\-\*\/]/) && hasFieldRef && newFormula.indexOf("}.") < 0;
@@ -304,6 +313,9 @@
304
313
  const _isContextVariable = (code) => code === 'applicant' || code === 'approver';
305
314
 
306
315
  if (isFunction || isOperator || isObjectField || isDotField) {
316
+ // 将数学分组方括号 [] 转为圆括号 ()
317
+ newFormula = newFormula.replace(/\[/g, '(').replace(/\]/g, ')');
318
+
307
319
  if (isFunction) {
308
320
  newFormula = newFormula.replace(/sum\s*\(/ig, 'SUM(');
309
321
  newFormula = newFormula.replace(/average\s*\(/ig, 'AVG(');