@steedos-labs/plugin-workflow 3.0.27 → 3.0.28

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.
@@ -35,22 +35,28 @@ nav_schema: {
35
35
  "click": {
36
36
  "actions": [
37
37
  {
38
- "actionType": "setValue",
39
- "componentId": "u:instanceNav",
40
- "args": {
41
- "value": {
42
- "nav_reload_token": "${_|now|date:x}"
43
- }
44
- }
45
- },
46
- {
47
- "actionType": "rebuild",
38
+ "actionType": "reload",
48
39
  "componentId": "u:instanceNav"
49
40
  }
50
41
  ]
51
42
  }
52
43
  }
53
44
  },
45
+ {
46
+ "type": "button",
47
+ "label": "刷新树",
48
+ "className": "instance-tree-reload hidden",
49
+ "onEvent": {
50
+ "click": {
51
+ "actions": [
52
+ {
53
+ "actionType": "custom",
54
+ "script": "try{ var opts = window.__latestNavOptions; var data = window.__latestNavData; if(!Array.isArray(opts)){ console.log('[tree-reload] no global options'); return; } opts = JSON.parse(JSON.stringify(opts)); var tree = window.__navTreeComponent; if(!tree || !tree.props || !tree.props.formItem){ console.log('[tree-reload] no saved tree ref, trying amisScoped'); var s = window.amisScoped || (document.querySelector('[data-amisScoped]') && document.querySelector('[data-amisScoped]').__amisScoped); if(s){ tree = s.getComponentById('u:instanceNavTree'); } } if(tree && tree.props && tree.props.formItem){ tree.props.formItem.setOptions(opts, undefined, data || {}); console.log('[tree-reload] setOptions done, count:', opts.length); if(tree.forceUpdate){ tree.forceUpdate(); console.log('[tree-reload] forceUpdate called'); } } else { console.log('[tree-reload] FAIL: cannot find tree component'); } }catch(e){ console.warn('[tree-reload error]', e); }"
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ },
54
60
  {
55
61
  "type": "service",
56
62
  "id": "u:instanceNav",
@@ -62,19 +68,114 @@ nav_schema: {
62
68
  "actionType": "reload"
63
69
  }
64
70
  ]
71
+ },
72
+ "fetchInited": {
73
+ "actions": [
74
+ {
75
+ "actionType": "custom",
76
+ "script": "try{ console.log('[fetchInited] fired, options:', event.data && event.data.options ? event.data.options.length : 'N/A'); var s = event.context && event.context.scoped; if(!s){ var amisScoped = window.amisScoped || (document.querySelector('[data-amisScoped]') && document.querySelector('[data-amisScoped]').__amisScoped); if(amisScoped){ s = amisScoped; } } if(s){ var tree = s.getComponentById('u:instanceNavTree'); if(tree && tree.props && tree.props.formItem){ window.__navTreeComponent = tree; console.log('[fetchInited] saved tree ref to global'); var opts = event.data && event.data.options; if(Array.isArray(opts)){ opts = JSON.parse(JSON.stringify(opts)); tree.props.formItem.setOptions(opts, undefined, event.data); console.log('[fetchInited] setOptions done, opts count:', opts.length); if(tree.forceUpdate){ setTimeout(function(){ tree.forceUpdate(); console.log('[fetchInited] forceUpdate called'); }, 100); } } } else { console.log('[fetchInited] tree found but no formItem, trying reload'); } } else { console.log('[fetchInited] no scoped context found'); } }catch(e){ console.warn('[fetchInited error]', e); }"
77
+ }
78
+ ]
65
79
  }
66
80
  },
67
81
  "body": [
82
+ {
83
+ "type": "input-tree",
84
+ "name": "instanceNavTree",
85
+ "treeContainerClassName": "h-full bg-white",
86
+ "className": "instance-box-tree h-full w-full p-0 bg-white",
87
+ "id": "u:instanceNavTree",
88
+ "stacked": true,
89
+ "multiple": false,
90
+ "enableNodePath": false,
91
+ "hideRoot": true,
92
+ "showIcon": true,
93
+ "initiallyOpen": false,
94
+ "virtualThreshold": 100000,
95
+ "value": "${value}",
96
+ "size": "md",
97
+ "onEvent": {
98
+ "change": {
99
+ "actions": [
100
+ {
101
+ "actionType": "setValue",
102
+ "componentId": "instances_list_service",
103
+ "args": {
104
+ "value": {
105
+ "isFlowDataDone": false
106
+ }
107
+ }
108
+ },
109
+ {
110
+ "actionType": "custom",
111
+ "script": "//获取上一次的flowId和categoryId\nconst lastFlowId = event.data.flowId;\nconst lastCategoryId = event.data.categoryId;\n//从value中获取最新的flowId\nvar flowIdRegex = /&flowId=([^&]+)/;\nvar flowIdMatch = event.data.value.match(flowIdRegex);\nconst flowId = flowIdMatch && flowIdMatch.length > 0 ? flowIdMatch[1] : \"\";\n//从value中获取最新的categoryId\nvar categoryIdRegex = /&categoryId=([^&]+)/;\nvar categoryIdMatch = event.data.value.match(categoryIdRegex);\nconst categoryId = categoryIdMatch && categoryIdMatch.length > 0 ? categoryIdMatch[1] : \"\";\n//获取上一次的listname和最新的listname\nconst lastListName = event.data.listName;\nconst listName = event.data.value.split('?')[0].split('instances/grid/')[1];\n//切换流程时清除过滤条件\nif (lastListName == \"monitor\" && listName == \"monitor\" && (flowId != lastFlowId || categoryId != lastCategoryId)) {\n listViewPropsStoreKey = window.location.pathname + \"/crud\";\n sessionStorage.removeItem(listViewPropsStoreKey);\n sessionStorage.removeItem(listViewPropsStoreKey + \"/query\");\n}"
112
+ },
113
+ {
114
+ "actionType": "setValue",
115
+ "componentId": "instances_list_service",
116
+ "args": {
117
+ "value": {
118
+ "additionalFilters": [
119
+ "${event.data.options.name}",
120
+ "=",
121
+ "${event.data.options.value}"
122
+ ]
123
+ }
124
+ },
125
+ "expression": "${event.data.options.level>=10}"
126
+ },
127
+ {
128
+ "args": {
129
+ "link": "${event.data.value}",
130
+ "blank": false
131
+ },
132
+ "actionType": "link"
133
+ }
134
+ ]
135
+ }
136
+ },
137
+ "menuTpl": {
138
+ "type": "wrapper",
139
+ "className": "flex items-center py-1.5 px-3 m-0 rounded-md transition-colors duration-150",
140
+ "body": [
141
+ {
142
+ "type": "tpl",
143
+ "className": "flex-1 leading-6 truncate instance-menu-label",
144
+ "tpl": "${label}",
145
+ "id": "u:9dee51f00db4"
146
+ },
147
+ {
148
+ "type": "tpl",
149
+ "className": "ml-auto",
150
+ "tpl": "",
151
+ "badge": {
152
+ "className": "h-0",
153
+ "offset": [
154
+ -5,
155
+ 0
156
+ ],
157
+ "mode": "text",
158
+ "text": "${tag | toInt}",
159
+ "overflowCount": 999
160
+ },
161
+ "id": "u:2329cd1fecc2"
162
+ }
163
+ ],
164
+ "id": "u:545154bcc334"
165
+ },
166
+ "unfoldedLevel": 2,
167
+ "source": "${options}"
168
+ }
68
169
  ],
69
- "schemaApi": {
170
+ "api": {
70
171
  "method": "get",
71
- "url": "${context.rootUrl}/api/${appId}/workflow/nav?reload=${nav_reload_token}",
172
+ "url": "${context.rootUrl}/api/${appId}/workflow/nav",
72
173
  "headers": {
73
174
  "Authorization": "Bearer ${context.tenantId},${context.authToken}"
74
175
  },
176
+ "trackExpression": "false",
75
177
  "messages": {},
76
- "adaptor": "payload.data.value = window.location.pathname + decodeURIComponent(window.location.search);\n// return payload;\n\n\nreturn {\n \"type\": \"service\",\n \"data\": payload.data,\n \"body\": [{\n \"type\": \"input-tree\",\n \"name\": \"tree_\" + new Date().getTime(),\n \"treeContainerClassName\": \"h-full bg-white\",\n \"className\": \"instance-box-tree h-full w-full p-0 bg-white\",\n \"id\": \"u:9f3dd961ca12_\" + new Date().getTime(),\n \"stacked\": true,\n \"multiple\": false,\n \"enableNodePath\": false,\n \"hideRoot\": true,\n \"showIcon\": true,\n \"initiallyOpen\": false,\n \"virtualThreshold\": 100000,\n \"value\": \"${value}\",\n \"size\": \"md\",\n \"onEvent\": {\n \"change\": {\n \"actions\": [\n {\n \"actionType\": \"setValue\",\n \"componentId\": \"instances_list_service\",\n \"args\": {\n \"value\": {\n \"isFlowDataDone\": false\n }\n }\n },\n {\n \"actionType\": \"custom\",\n \"script\": \"//获取上一次的flowId和categoryId\\nconst lastFlowId = event.data.flowId;\\nconst lastCategoryId = event.data.categoryId;\\n//从value中获取最新的flowId\\nvar flowIdRegex = /&flowId=([^&]+)/;\\nvar flowIdMatch = event.data.value.match(flowIdRegex);\\nconst flowId = flowIdMatch && flowIdMatch.length > 0 ? flowIdMatch[1] : \\\"\\\";\\n//从value中获取最新的categoryId\\nvar categoryIdRegex = /&categoryId=([^&]+)/;\\nvar categoryIdMatch = event.data.value.match(categoryIdRegex);\\nconst categoryId = categoryIdMatch && categoryIdMatch.length > 0 ? categoryIdMatch[1] : \\\"\\\";\\n//获取上一次的listname和最新的listname\\nconst lastListName = event.data.listName;\\nconst listName = event.data.value.split('?')[0].split('instances/grid/')[1];\\n//切换流程时清除过滤条件\\nif (lastListName == \\\"monitor\\\" && listName == \\\"monitor\\\" && (flowId != lastFlowId || categoryId != lastCategoryId)) {\\n listViewPropsStoreKey = window.location.pathname + \\\"/crud\\\";\\n sessionStorage.removeItem(listViewPropsStoreKey);\\n sessionStorage.removeItem(listViewPropsStoreKey + \\\"/query\\\");\\n}\"\n },\n {\n \"actionType\": \"setValue\",\n \"componentId\": \"instances_list_service\",\n \"args\": {\n \"value\": {\n \"additionalFilters\": [\n \"${event.data.options.name}\",\n \"=\",\n \"${event.data.options.value}\"\n ]\n }\n },\n \"expression\": \"${event.data.options.level>=10}\"\n },\n {\n \"args\": {\n \"link\": \"${event.data.value}\",\n \"blank\": false\n },\n \"actionType\": \"link\"\n }\n ]\n }\n },\n \"menuTpl\": {\n \"type\": \"wrapper\",\n \"className\": \"flex items-center py-1.5 px-3 m-0 rounded-md transition-colors duration-150\",\n \"body\": [\n {\n \"type\": \"tpl\",\n \"className\": \"flex-1 leading-6 truncate instance-menu-label\",\n \"tpl\": \"${label}\",\n \"id\": \"u:9dee51f00db4\"\n },\n {\n \"type\": \"tpl\",\n \"className\": \"ml-auto\",\n \"tpl\": \"\",\n \"badge\": {\n \"className\": \"h-0\",\n \"offset\": [\n -5,\n 0\n ],\n \"mode\": \"text\",\n \"text\": \"${tag | toInt}\",\n \"overflowCount\": 999\n },\n \"id\": \"u:2329cd1fecc2\"\n }\n ],\n \"id\": \"u:545154bcc334\"\n },\n \"unfoldedLevel\": 2,\n \"options\": payload.data.options\n }]\n}"
77
- # "adaptor": "payload.data.value = window.location.pathname + decodeURIComponent(window.location.search); return payload;"
178
+ "adaptor": "payload.data.value = window.location.pathname + decodeURIComponent(window.location.search); payload.data._ts = Date.now(); if(payload.data.options){ payload.data.options = JSON.parse(JSON.stringify(payload.data.options)); window.__latestNavOptions = payload.data.options; window.__latestNavData = payload.data; console.log('[adaptor] saved options to global, count:', payload.data.options.length); } return payload;"
78
179
  },
79
180
  "messages": {},
80
181
  "dsType": "api"
@@ -45,7 +45,11 @@ window.waitForThing(window, 'socket').then(()=>{
45
45
  };
46
46
  if (isFirstRun === false && shouldReloadView()) {
47
47
  window.$(".list-view-btn-reload").click()
48
- window.$(".instance-nav-reload").click()
48
+ clearTimeout(window.__navReloadTimer)
49
+ clearTimeout(window.__navRetryTimer)
50
+ window.__navReloadTimer = setTimeout(function(){ window.$(".instance-nav-reload").click() }, 500)
51
+ // Retry: reload tree only (no API request) to pick up data already in Service store
52
+ window.__navRetryTimer = setTimeout(function(){ window.$(".instance-tree-reload").click() }, 2000)
49
53
  }
50
54
  isFirstRun = false;
51
55
  });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * 审批中心角标更新测试 - draft(草稿)角标专项
3
+ *
4
+ * 在浏览器控制台运行:
5
+ * 复制粘贴后回车,自动执行
6
+ *
7
+ * draft 角标位于 options[3].children[0].tag
8
+ */
9
+ (async function testDraftBadge() {
10
+ 'use strict';
11
+ const ROUNDS = 5;
12
+ const origLog = console.log;
13
+
14
+ // ---- 前置检查 ----
15
+ const navBtn = document.querySelector('.instance-nav-reload');
16
+ const treeBtn = document.querySelector('.instance-tree-reload');
17
+ const treeEl = document.querySelector('.instance-box-tree');
18
+ if (!navBtn || !treeBtn || !treeEl) {
19
+ console.error('缺少必要 DOM 元素'); return;
20
+ }
21
+
22
+ let baseDraft = null;
23
+ let baseInbox = null;
24
+ let round = 0;
25
+
26
+ // ---- 篡改函数 ----
27
+ function tamper(json) {
28
+ if (!(json && json.data && Array.isArray(json.data.options))) return null;
29
+ var opts = json.data.options;
30
+ // inbox = opts[0]
31
+ if (opts[0] && opts[0].hasOwnProperty('tag')) {
32
+ if (baseInbox === null) baseInbox = opts[0].tag;
33
+ opts[0].tag = baseInbox + round;
34
+ }
35
+ // draft = opts[3].children[0]
36
+ if (opts[3] && opts[3].children && opts[3].children[0]) {
37
+ if (baseDraft === null) baseDraft = opts[3].children[0].tag || 0;
38
+ opts[3].children[0].tag = baseDraft + round;
39
+ }
40
+ return json;
41
+ }
42
+
43
+ // ---- XHR 拦截 ----
44
+ const XHR = XMLHttpRequest.prototype;
45
+ const _open = XHR.open, _send = XHR.send;
46
+ const _rtDesc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
47
+ const _rDesc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response');
48
+ const urlMap = new WeakMap();
49
+
50
+ XHR.open = function(m, u) { urlMap.set(this, String(u)); return _open.apply(this, arguments); };
51
+ XHR.send = function() {
52
+ var url = urlMap.get(this);
53
+ if (url && url.includes('/workflow/nav')) {
54
+ var xhr = this, done = false, txt = null;
55
+ Object.defineProperty(xhr, 'responseText', {
56
+ get: function() {
57
+ var real = _rtDesc.get.call(this);
58
+ if (this.readyState === 4 && !done && real) {
59
+ done = true;
60
+ try { var j = JSON.parse(real); if (tamper(j)) txt = JSON.stringify(j); } catch(e) {}
61
+ }
62
+ return txt || real;
63
+ }, configurable: true
64
+ });
65
+ Object.defineProperty(xhr, 'response', {
66
+ get: function() {
67
+ var t = xhr.responseText;
68
+ if (xhr.responseType === '' || xhr.responseType === 'text') return t;
69
+ if (xhr.responseType === 'json' && txt) try { return JSON.parse(txt); } catch(e) {}
70
+ return _rDesc.get.call(this);
71
+ }, configurable: true
72
+ });
73
+ }
74
+ return _send.apply(this, arguments);
75
+ };
76
+
77
+ // ---- fetch 拦截 ----
78
+ const _fetch = window.fetch;
79
+ window.fetch = async function(...a) {
80
+ var r = await _fetch.apply(this, a);
81
+ var u = typeof a[0] === 'string' ? a[0] : (a[0] && a[0].url) || '';
82
+ if (u.includes('/workflow/nav')) {
83
+ try {
84
+ var c = r.clone(), j = await c.json();
85
+ if (tamper(j)) return new Response(JSON.stringify(j), { status: r.status, headers: r.headers });
86
+ } catch(e) {}
87
+ }
88
+ return r;
89
+ };
90
+
91
+ // ---- DOM 读取 ----
92
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
93
+
94
+ function getAllBadges() {
95
+ var sels = ['[class*="Badge-text"]', '.antd-Badge-text', '.cxd-Badge-text'];
96
+ for (var s of sels) { var e = treeEl.querySelectorAll(s); if (e.length) return e; }
97
+ return [];
98
+ }
99
+
100
+ function findBadgeByLabel(label) {
101
+ var items = treeEl.querySelectorAll('.cxd-Tree-itemLabel, .antd-Tree-itemLabel');
102
+ for (var item of items) {
103
+ if (item.textContent.includes(label)) {
104
+ var badge = item.querySelector('[class*="Badge-text"]');
105
+ if (badge) return parseInt(badge.textContent.trim(), 10);
106
+ }
107
+ }
108
+ // fallback: 搜索所有tree item
109
+ var allItems = treeEl.querySelectorAll('.cxd-Tree-item, .antd-Tree-item');
110
+ for (var ai of allItems) {
111
+ var labelEl = ai.querySelector('.instance-menu-label');
112
+ if (labelEl && labelEl.textContent.includes(label)) {
113
+ var b = ai.querySelector('[class*="Badge-text"]');
114
+ if (b) return parseInt(b.textContent.trim(), 10);
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ // ---- 初始化 ----
121
+ origLog.call(console, '%c=== Draft 角标测试 ===', 'color:blue;font-size:14px;font-weight:bold');
122
+ round = 0;
123
+ navBtn.click();
124
+ await sleep(4000);
125
+
126
+ if (baseDraft === null) {
127
+ console.error('未能获取基准 draft 值,API 未被拦截');
128
+ window.fetch = _fetch; XHR.open = _open; XHR.send = _send; return;
129
+ }
130
+ origLog.call(console, '基准值: inbox=' + baseInbox + ', draft=' + baseDraft);
131
+
132
+ // ---- 测试轮 ----
133
+ var results = [];
134
+ for (var i = 1; i <= ROUNDS; i++) {
135
+ round = i;
136
+ var expInbox = baseInbox + i;
137
+ var expDraft = baseDraft + i;
138
+ origLog.call(console, '--- 第' + i + '轮: 期望 inbox=' + expInbox + ', draft=' + expDraft);
139
+
140
+ navBtn.click();
141
+ await sleep(1500);
142
+ treeBtn.click();
143
+ await sleep(1500);
144
+
145
+ var domInbox = getAllBadges()[0] ? parseInt(getAllBadges()[0].textContent.trim(), 10) : null;
146
+ var domDraft = findBadgeByLabel('草稿') || findBadgeByLabel('Draft') || findBadgeByLabel('draft');
147
+
148
+ var inboxOk = domInbox === expInbox;
149
+ var draftOk = domDraft === expDraft;
150
+ results.push({ round: i, expInbox, domInbox, inboxOk, expDraft, domDraft, draftOk, pass: inboxOk && draftOk });
151
+
152
+ var icon = (inboxOk && draftOk) ? '✅' : '❌';
153
+ origLog.call(console, icon + ' inbox:' + domInbox + '/' + expInbox + (inboxOk?' OK':' FAIL') +
154
+ ' | draft:' + domDraft + '/' + expDraft + (draftOk?' OK':' FAIL'));
155
+
156
+ if (!(inboxOk && draftOk)) {
157
+ origLog.call(console, '全部badges:', Array.from(getAllBadges()).map(e => e.textContent.trim()));
158
+ break;
159
+ }
160
+ await sleep(500);
161
+ }
162
+
163
+ // ---- 恢复 ----
164
+ window.fetch = _fetch; XHR.open = _open; XHR.send = _send;
165
+
166
+ // ---- 报告 ----
167
+ var p = results.filter(r => r.pass).length;
168
+ origLog.call(console, '%c=== 结果: ' + p + '/' + results.length + ' 通过 ===',
169
+ p === results.length ? 'color:green;font-size:14px' : 'color:red;font-size:14px');
170
+ console.table(results);
171
+ })();
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 审批中心角标自动更新 - 模拟数据变化测试脚本
3
+ *
4
+ * 使用方法:
5
+ * 1. 打开审批中心页面(/app/approve_workflow/instances/grid/inbox 等)
6
+ * 2. 打开浏览器控制台 (F12 → Console)
7
+ * 3. 复制粘贴本脚本内容到控制台并回车
8
+ * 4. 等待测试完成,查看报告
9
+ *
10
+ * 测试原理:
11
+ * - 拦截 /api/.../workflow/nav 的 API 响应
12
+ * - 每轮将 inbox 角标值篡改为递增值(模拟收到新审批单 +1)
13
+ * - 模拟完整 socket 流程:先 Service reload,再 Tree reload(与 socket.client.js 一致)
14
+ * - 等待 UI 更新后,读取 DOM 中角标显示值
15
+ * - 第一轮失败即停止(快速定位问题)
16
+ */
17
+ (async function testBadgeUpdate() {
18
+ 'use strict';
19
+
20
+ const TOTAL_ROUNDS = 10; // 总测试轮数
21
+ const STOP_ON_FAIL = true; // 第一次失败即停止
22
+
23
+ console.log('%c=== 角标更新自动化测试(模拟数据变化)===', 'color: blue; font-size: 16px; font-weight: bold;');
24
+ console.log(`总测试轮数: ${TOTAL_ROUNDS}, 失败即停: ${STOP_ON_FAIL}`);
25
+ console.log('');
26
+
27
+ // 检查前置条件
28
+ const navReloadBtn = document.querySelector('.instance-nav-reload');
29
+ const treeReloadBtn = document.querySelector('.instance-tree-reload');
30
+ const treeEl = document.querySelector('.instance-box-tree');
31
+
32
+ if (!navReloadBtn) { console.error('❌ 未找到 .instance-nav-reload 按钮'); return; }
33
+ if (!treeReloadBtn) { console.error('❌ 未找到 .instance-tree-reload 按钮'); return; }
34
+ if (!treeEl) { console.error('❌ 未找到 .instance-box-tree 树组件'); return; }
35
+ console.log('✅ 前置条件检查通过: nav-reload, tree-reload, tree DOM 均存在');
36
+
37
+ // 状态
38
+ let roundCounter = 0;
39
+ let baseInboxTag = null;
40
+ let expectedInboxTag = null;
41
+ let actualApiInboxTag = null;
42
+ let apiCallCount = 0;
43
+ let fetchInitedCount = 0;
44
+
45
+ // ======== 监听 fetchInited 日志 ========
46
+ const origConsoleLog = console.log;
47
+ const fetchInitedLogs = [];
48
+ console.log = function() {
49
+ if (arguments[0] && typeof arguments[0] === 'string' && arguments[0].includes('[fetchInited]')) {
50
+ fetchInitedCount++;
51
+ fetchInitedLogs.push(Array.from(arguments).join(' '));
52
+ }
53
+ return origConsoleLog.apply(console, arguments);
54
+ };
55
+
56
+ // ======== 篡改 options 的通用函数 ========
57
+ function tamperOptions(json, source) {
58
+ if (!(json && json.data && Array.isArray(json.data.options))) return null;
59
+ const inboxIdx = json.data.options.findIndex(opt =>
60
+ opt.value && opt.value.includes('inbox') && opt.hasOwnProperty('tag')
61
+ );
62
+ if (inboxIdx < 0) return null;
63
+
64
+ if (baseInboxTag === null) {
65
+ baseInboxTag = json.data.options[inboxIdx].tag;
66
+ origConsoleLog.call(console, `%c[${source}拦截] 基准 inbox 角标: ${baseInboxTag}`, 'color: orange;');
67
+ }
68
+
69
+ const newTag = baseInboxTag + roundCounter;
70
+ json.data.options[inboxIdx].tag = newTag;
71
+
72
+ if (Array.isArray(json.data.options[inboxIdx].children)) {
73
+ json.data.options[inboxIdx].children.forEach(child => {
74
+ if (child.hasOwnProperty('tag')) {
75
+ child.tag = (child.tag || 0) + roundCounter;
76
+ }
77
+ });
78
+ }
79
+
80
+ actualApiInboxTag = newTag;
81
+ expectedInboxTag = newTag;
82
+ origConsoleLog.call(console, `%c[${source}拦截] 第${roundCounter}轮: 篡改 inbox tag → ${newTag}`, 'color: orange;');
83
+ return json;
84
+ }
85
+
86
+ // ======== 拦截 fetch,篡改 API 响应 ========
87
+ const originalFetch = window.fetch;
88
+ window.fetch = async function (...args) {
89
+ const response = await originalFetch.apply(this, args);
90
+ const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
91
+
92
+ if (url.includes('/workflow/nav')) {
93
+ apiCallCount++;
94
+ try {
95
+ const cloned = response.clone();
96
+ const json = await cloned.json();
97
+ const tampered = tamperOptions(json, 'fetch');
98
+ if (tampered) {
99
+ return new Response(JSON.stringify(tampered), {
100
+ status: response.status, statusText: response.statusText, headers: response.headers
101
+ });
102
+ }
103
+ } catch (e) {
104
+ origConsoleLog.call(console, '%c[fetch拦截] 解析失败:', 'color: red;', e);
105
+ }
106
+ }
107
+ return response;
108
+ };
109
+
110
+ // ======== 拦截 XMLHttpRequest,用 getter 篡改响应(在 axios 读取前拦截)========
111
+ const XHR = XMLHttpRequest.prototype;
112
+ const origXHROpen = XHR.open;
113
+ const origXHRSend = XHR.send;
114
+ const xhrUrlMap = new WeakMap();
115
+ const origResponseTextDesc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
116
+ const origResponseDesc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response');
117
+
118
+ XHR.open = function (method, url) {
119
+ xhrUrlMap.set(this, { url: String(url) });
120
+ return origXHROpen.apply(this, arguments);
121
+ };
122
+
123
+ XHR.send = function () {
124
+ const info = xhrUrlMap.get(this);
125
+ if (info && info.url && info.url.includes('/workflow/nav')) {
126
+ const xhr = this;
127
+ let _tamperedText = null;
128
+ let _processed = false;
129
+
130
+ // 用 getter 拦截 responseText —— 在 axios 读取时触发篡改
131
+ Object.defineProperty(xhr, 'responseText', {
132
+ get: function () {
133
+ const realText = origResponseTextDesc.get.call(this);
134
+ if (this.readyState === 4 && !_processed && realText) {
135
+ _processed = true;
136
+ apiCallCount++;
137
+ try {
138
+ const json = JSON.parse(realText);
139
+ const result = tamperOptions(json, 'XHR');
140
+ if (result) {
141
+ _tamperedText = JSON.stringify(result);
142
+ }
143
+ } catch (e) {
144
+ origConsoleLog.call(console, '%c[XHR拦截] 解析失败:', 'color: red;', e);
145
+ }
146
+ }
147
+ return _tamperedText || realText;
148
+ },
149
+ configurable: true
150
+ });
151
+
152
+ // 同步拦截 response 属性
153
+ Object.defineProperty(xhr, 'response', {
154
+ get: function () {
155
+ // 触发 responseText 的篡改逻辑
156
+ const text = xhr.responseText;
157
+ if (xhr.responseType === '' || xhr.responseType === 'text') {
158
+ return text;
159
+ }
160
+ if (xhr.responseType === 'json' && _tamperedText) {
161
+ try { return JSON.parse(_tamperedText); } catch (e) { }
162
+ }
163
+ return origResponseDesc.get.call(this);
164
+ },
165
+ configurable: true
166
+ });
167
+ }
168
+ return origXHRSend.apply(this, arguments);
169
+ };
170
+
171
+ // ======== DOM 读取 ========
172
+ function findBadgeElements() {
173
+ const selectors = ['[class*="Badge-text"]', '.antd-Badge-text', '.cxd-Badge-text'];
174
+ for (const sel of selectors) {
175
+ const els = treeEl.querySelectorAll(sel);
176
+ if (els.length > 0) return els;
177
+ }
178
+ return [];
179
+ }
180
+
181
+ function getInboxBadgeFromDOM() {
182
+ const badgeEls = findBadgeElements();
183
+ if (badgeEls.length === 0) return null;
184
+ const text = badgeEls[0].textContent.trim();
185
+ const num = parseInt(text, 10);
186
+ return isNaN(num) ? null : num;
187
+ }
188
+
189
+ function sleep(ms) {
190
+ return new Promise(resolve => setTimeout(resolve, ms));
191
+ }
192
+
193
+ // ======== 执行测试 ========
194
+ const results = [];
195
+
196
+ // 初始化:获取基准值(模拟完整 socket 流程:nav reload + tree reload)
197
+ origConsoleLog.call(console, '%c[初始化] 获取基准角标值...', 'color: orange;');
198
+ roundCounter = 0;
199
+ navReloadBtn.click();
200
+ await sleep(2000);
201
+ treeReloadBtn.click();
202
+ await sleep(2000);
203
+ const initDom = getInboxBadgeFromDOM();
204
+ origConsoleLog.call(console, `%c[初始化] 基准值: API=${baseInboxTag}, DOM=${initDom}, fetchInited=${fetchInitedCount}次`, 'color: orange;');
205
+
206
+ if (baseInboxTag === null) {
207
+ console.error('❌ 未能获取基准值,API 请求未被拦截(fetch 和 XHR 均未捕获)');
208
+ window.fetch = originalFetch;
209
+ XHR.open = origXHROpen;
210
+ XHR.send = origXHRSend;
211
+ console.log = origConsoleLog;
212
+ return;
213
+ }
214
+ origConsoleLog.call(console, '');
215
+
216
+ let stopped = false;
217
+ for (let round = 1; round <= TOTAL_ROUNDS; round++) {
218
+ roundCounter = round;
219
+ actualApiInboxTag = null;
220
+ expectedInboxTag = baseInboxTag + round;
221
+ const apiCountBefore = apiCallCount;
222
+ const fetchInitedBefore = fetchInitedCount;
223
+
224
+ origConsoleLog.call(console, `%c[第 ${round}/${TOTAL_ROUNDS} 轮]`, 'color: #888; font-weight: bold;',
225
+ `期望角标: ${baseInboxTag} + ${round} = ${expectedInboxTag}`);
226
+
227
+ // 模拟完整 socket 流程:
228
+ // 1. Service reload(获取 API 数据)
229
+ navReloadBtn.click();
230
+ await sleep(1500); // 等待 API 返回 + fetchInited 事件处理
231
+
232
+ // 2. Tree reload(从 Service 数据域读取 options)
233
+ treeReloadBtn.click();
234
+ await sleep(1500); // 等待 Tree reloadOptions 完成
235
+
236
+ // 3. 读取结果
237
+ const apiCalls = apiCallCount - apiCountBefore;
238
+ const fetchInitedFired = fetchInitedCount - fetchInitedBefore;
239
+ const domValue = getInboxBadgeFromDOM();
240
+ const pass = domValue === expectedInboxTag;
241
+
242
+ const result = {
243
+ round,
244
+ apiCalls,
245
+ fetchInited: fetchInitedFired,
246
+ expected: expectedInboxTag,
247
+ apiActual: actualApiInboxTag,
248
+ dom: domValue,
249
+ pass
250
+ };
251
+ results.push(result);
252
+
253
+ const icon = pass ? '✅' : '❌';
254
+ const color = pass ? 'color: green;' : 'color: red; font-weight: bold;';
255
+ origConsoleLog.call(console,
256
+ `${icon} %c第${round}轮:`,
257
+ color,
258
+ `API=${apiCalls}次`,
259
+ `fetchInited=${fetchInitedFired}次`,
260
+ `期望=${expectedInboxTag}`,
261
+ `DOM=${domValue}`,
262
+ pass ? '通过' : `失败(差${expectedInboxTag - domValue})`
263
+ );
264
+
265
+ if (!pass && STOP_ON_FAIL) {
266
+ origConsoleLog.call(console, '%c⛔ 首次失败,停止测试', 'color: red; font-size: 14px; font-weight: bold;');
267
+ origConsoleLog.call(console, '诊断信息:');
268
+ origConsoleLog.call(console, ' fetchInited事件日志:', fetchInitedLogs.length > 0 ? fetchInitedLogs : '无(事件可能未触发)');
269
+ origConsoleLog.call(console, ' fetchInited累计次数:', fetchInitedCount);
270
+ origConsoleLog.call(console, ' API篡改后值:', actualApiInboxTag);
271
+ origConsoleLog.call(console, ' DOM当前值:', domValue);
272
+ origConsoleLog.call(console, ' DOM全部角标:', Array.from(findBadgeElements()).map(el => el.textContent.trim()));
273
+ // 检查 tree-reload 增强是否生效
274
+ origConsoleLog.call(console, ' 检查 tree-reload 增强...');
275
+ try {
276
+ var s = window.amisScoped || (document.querySelector('[data-amisScoped]') && document.querySelector('[data-amisScoped]').__amisScoped);
277
+ if (s) {
278
+ var svc = s.getComponentById('u:instanceNav');
279
+ var tr = s.getComponentById('u:instanceNavTree');
280
+ origConsoleLog.call(console, ' Service组件:', svc ? '存在' : '不存在');
281
+ origConsoleLog.call(console, ' Tree组件:', tr ? '存在' : '不存在');
282
+ if (svc && svc.props && svc.props.data) {
283
+ var svcOpts = svc.props.data.options;
284
+ origConsoleLog.call(console, ' Service.data.options[0].tag:', svcOpts && svcOpts[0] ? svcOpts[0].tag : 'N/A');
285
+ }
286
+ if (tr && tr.props && tr.props.formItem) {
287
+ var treeOpts = tr.props.formItem.getOptions ? tr.props.formItem.getOptions() : (tr.props.options || []);
288
+ origConsoleLog.call(console, ' Tree.options[0].tag:', treeOpts && treeOpts[0] ? treeOpts[0].tag : 'N/A');
289
+ }
290
+ }
291
+ } catch(e) { origConsoleLog.call(console, ' 诊断出错:', e); }
292
+ stopped = true;
293
+ break;
294
+ }
295
+
296
+ if (round < TOTAL_ROUNDS) {
297
+ await sleep(500);
298
+ }
299
+ }
300
+
301
+ // 恢复
302
+ window.fetch = originalFetch;
303
+ XHR.open = origXHROpen;
304
+ XHR.send = origXHRSend;
305
+ console.log = origConsoleLog;
306
+
307
+ // 报告
308
+ const passed = results.filter(r => r.pass).length;
309
+ const failed = results.filter(r => !r.pass).length;
310
+
311
+ console.log('');
312
+ console.log('%c=== 测试报告(模拟数据变化)===', 'color: blue; font-size: 16px; font-weight: bold;');
313
+ console.log(`完成轮数: ${results.length}/${TOTAL_ROUNDS}${stopped ? ' (提前停止)' : ''}`);
314
+ console.log(`基准角标: ${baseInboxTag}`);
315
+ console.log(`通过: ${passed} ✅`);
316
+ console.log(`失败: ${failed} ❌`);
317
+ console.log(`%c成功率: ${(passed / results.length * 100).toFixed(1)}%`,
318
+ failed === 0 ? 'color: green; font-size: 14px; font-weight: bold;' : 'color: red; font-size: 14px; font-weight: bold;');
319
+
320
+ console.log('');
321
+ console.log('%c详细结果:', 'font-weight: bold;');
322
+ console.table(results.map(r => ({
323
+ '轮次': r.round,
324
+ 'API次数': r.apiCalls,
325
+ 'fetchInited': r.fetchInited,
326
+ '期望角标': r.expected,
327
+ 'DOM角标': r.dom,
328
+ '结果': r.pass ? '✅通过' : '❌失败'
329
+ })));
330
+
331
+ return { passed, failed, total: results.length, rate: (passed / results.length * 100).toFixed(1) + '%' };
332
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.27",
3
+ "version": "3.0.28",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "files": [