openxiangda-cli 2.0.0-alpha.12 → 2.0.0-alpha.120

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 (271) hide show
  1. package/README.md +13 -14
  2. package/bin/run.js +79 -1
  3. package/dist/base.d.ts +40 -5
  4. package/dist/base.d.ts.map +1 -1
  5. package/dist/base.js +123 -10
  6. package/dist/base.js.map +1 -1
  7. package/dist/commands/accept.d.ts +25 -0
  8. package/dist/commands/accept.d.ts.map +1 -0
  9. package/dist/commands/accept.js +21 -0
  10. package/dist/commands/accept.js.map +1 -0
  11. package/dist/commands/cancel.d.ts +16 -1
  12. package/dist/commands/cancel.d.ts.map +1 -1
  13. package/dist/commands/cancel.js +1 -1
  14. package/dist/commands/cancel.js.map +1 -1
  15. package/dist/commands/check.d.ts +17 -1
  16. package/dist/commands/check.d.ts.map +1 -1
  17. package/dist/commands/check.js +15 -3
  18. package/dist/commands/check.js.map +1 -1
  19. package/dist/commands/create.d.ts +25 -0
  20. package/dist/commands/create.d.ts.map +1 -0
  21. package/dist/commands/create.js +101 -0
  22. package/dist/commands/create.js.map +1 -0
  23. package/dist/commands/deploy.d.ts +21 -8
  24. package/dist/commands/deploy.d.ts.map +1 -1
  25. package/dist/commands/deploy.js +29 -16
  26. package/dist/commands/deploy.js.map +1 -1
  27. package/dist/commands/dev.d.ts +18 -1
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +24 -3
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/login.d.ts +26 -0
  32. package/dist/commands/login.d.ts.map +1 -0
  33. package/dist/commands/login.js +73 -0
  34. package/dist/commands/login.js.map +1 -0
  35. package/dist/commands/logs.d.ts +17 -2
  36. package/dist/commands/logs.d.ts.map +1 -1
  37. package/dist/commands/logs.js +2 -2
  38. package/dist/commands/logs.js.map +1 -1
  39. package/dist/commands/retry.d.ts +16 -1
  40. package/dist/commands/retry.d.ts.map +1 -1
  41. package/dist/commands/retry.js +5 -2
  42. package/dist/commands/retry.js.map +1 -1
  43. package/dist/commands/rollback.d.ts +17 -4
  44. package/dist/commands/rollback.d.ts.map +1 -1
  45. package/dist/commands/rollback.js +11 -4
  46. package/dist/commands/rollback.js.map +1 -1
  47. package/dist/commands/skill.d.ts +28 -0
  48. package/dist/commands/skill.d.ts.map +1 -0
  49. package/dist/commands/skill.js +43 -0
  50. package/dist/commands/skill.js.map +1 -0
  51. package/dist/commands/spec.d.ts +36 -0
  52. package/dist/commands/spec.d.ts.map +1 -0
  53. package/dist/commands/spec.js +117 -0
  54. package/dist/commands/spec.js.map +1 -0
  55. package/dist/commands/start.d.ts +25 -0
  56. package/dist/commands/start.d.ts.map +1 -0
  57. package/dist/commands/start.js +18 -0
  58. package/dist/commands/start.js.map +1 -0
  59. package/dist/commands/status.d.ts +16 -1
  60. package/dist/commands/status.d.ts.map +1 -1
  61. package/dist/commands/status.js +1 -1
  62. package/dist/commands/status.js.map +1 -1
  63. package/dist/commands/stop.d.ts +25 -0
  64. package/dist/commands/stop.d.ts.map +1 -0
  65. package/dist/commands/stop.js +18 -0
  66. package/dist/commands/stop.js.map +1 -0
  67. package/dist/create-workspace.d.ts +31 -0
  68. package/dist/create-workspace.d.ts.map +1 -0
  69. package/dist/create-workspace.js +273 -0
  70. package/dist/create-workspace.js.map +1 -0
  71. package/dist/toolchain-capsule.d.ts +4 -0
  72. package/dist/toolchain-capsule.d.ts.map +1 -0
  73. package/dist/toolchain-capsule.js +116 -0
  74. package/dist/toolchain-capsule.js.map +1 -0
  75. package/package.json +16 -9
  76. package/template/.dockerignore +23 -0
  77. package/template/.env.example +3 -0
  78. package/template/AGENTS.md +25 -0
  79. package/template/README.md +43 -0
  80. package/template/_gitignore +14 -0
  81. package/template/apps/server/Dockerfile +20 -0
  82. package/template/apps/server/package.json +27 -0
  83. package/template/apps/server/src/app.module.ts +43 -0
  84. package/template/apps/server/src/main.ts +5 -0
  85. package/template/apps/server/test/smoke.test.ts +16 -0
  86. package/template/apps/server/tsconfig.json +9 -0
  87. package/template/apps/web/data-api-live.e2e.html +15 -0
  88. package/template/apps/web/e2e/client-fixture.ts +5 -0
  89. package/template/apps/web/e2e/data-api-live-fixture.tsx +162 -0
  90. package/template/apps/web/e2e/data-api-live.spec.ts +144 -0
  91. package/template/apps/web/e2e/field-protocol-fixture.tsx +347 -0
  92. package/template/apps/web/e2e/field-protocol.spec.ts +195 -0
  93. package/template/apps/web/e2e/resource-experience-fixture.tsx +380 -0
  94. package/template/apps/web/e2e/resources.spec.ts +478 -0
  95. package/template/apps/web/e2e/workflow-experience-fixture.tsx +144 -0
  96. package/template/apps/web/e2e/workflow.spec.ts +576 -0
  97. package/template/apps/web/field-protocol.e2e.html +15 -0
  98. package/template/apps/web/index.html +12 -0
  99. package/template/apps/web/package.json +32 -0
  100. package/template/apps/web/playwright.config.ts +47 -0
  101. package/template/apps/web/resource-experience.e2e.html +15 -0
  102. package/template/apps/web/scripts/check.mjs +43 -0
  103. package/template/apps/web/scripts/verify-build.mjs +6 -0
  104. package/template/apps/web/src/main.tsx +41 -0
  105. package/template/apps/web/test/contracts.test.ts +149 -0
  106. package/template/apps/web/test/loopback.test.ts +62 -0
  107. package/template/apps/web/tsconfig.json +12 -0
  108. package/template/apps/web/vite.config.ts +40 -0
  109. package/template/apps/web/workflow-experience.e2e.html +15 -0
  110. package/template/appspec/app.md +36 -0
  111. package/template/openxiangda.config.ts +34 -0
  112. package/template/package.json +28 -0
  113. package/template/packages/contracts/package.json +23 -0
  114. package/template/packages/contracts/src/generated.ts +135 -0
  115. package/template/packages/contracts/src/index.ts +1 -0
  116. package/template/packages/contracts/tsconfig.json +9 -0
  117. package/template/pnpm-workspace.yaml +7 -0
  118. package/template/scripts/verify-template-budget.mjs +49 -0
  119. package/template/tsconfig.base.json +13 -0
  120. package/dist/commands/app/create.d.ts +0 -15
  121. package/dist/commands/app/create.d.ts.map +0 -1
  122. package/dist/commands/app/create.js +0 -42
  123. package/dist/commands/app/create.js.map +0 -1
  124. package/dist/commands/app/info.d.ts +0 -9
  125. package/dist/commands/app/info.d.ts.map +0 -1
  126. package/dist/commands/app/info.js +0 -10
  127. package/dist/commands/app/info.js.map +0 -1
  128. package/dist/commands/app/link.d.ts +0 -10
  129. package/dist/commands/app/link.d.ts.map +0 -1
  130. package/dist/commands/app/link.js +0 -16
  131. package/dist/commands/app/link.js.map +0 -1
  132. package/dist/commands/app/provision.d.ts +0 -9
  133. package/dist/commands/app/provision.d.ts.map +0 -1
  134. package/dist/commands/app/provision.js +0 -10
  135. package/dist/commands/app/provision.js.map +0 -1
  136. package/dist/commands/auth/login.d.ts +0 -10
  137. package/dist/commands/auth/login.d.ts.map +0 -1
  138. package/dist/commands/auth/login.js +0 -34
  139. package/dist/commands/auth/login.js.map +0 -1
  140. package/dist/commands/auth/logout.d.ts +0 -6
  141. package/dist/commands/auth/logout.d.ts.map +0 -1
  142. package/dist/commands/auth/logout.js +0 -10
  143. package/dist/commands/auth/logout.js.map +0 -1
  144. package/dist/commands/auth/status.d.ts +0 -6
  145. package/dist/commands/auth/status.d.ts.map +0 -1
  146. package/dist/commands/auth/status.js +0 -19
  147. package/dist/commands/auth/status.js.map +0 -1
  148. package/dist/commands/build.d.ts +0 -11
  149. package/dist/commands/build.d.ts.map +0 -1
  150. package/dist/commands/build.js +0 -23
  151. package/dist/commands/build.js.map +0 -1
  152. package/dist/commands/doctor.d.ts +0 -9
  153. package/dist/commands/doctor.d.ts.map +0 -1
  154. package/dist/commands/doctor.js +0 -7
  155. package/dist/commands/doctor.js.map +0 -1
  156. package/dist/commands/event/delivery/list.d.ts +0 -10
  157. package/dist/commands/event/delivery/list.d.ts.map +0 -1
  158. package/dist/commands/event/delivery/list.js +0 -14
  159. package/dist/commands/event/delivery/list.js.map +0 -1
  160. package/dist/commands/event/delivery/replay.d.ts +0 -13
  161. package/dist/commands/event/delivery/replay.d.ts.map +0 -1
  162. package/dist/commands/event/delivery/replay.js +0 -16
  163. package/dist/commands/event/delivery/replay.js.map +0 -1
  164. package/dist/commands/event/subscription/list.d.ts +0 -9
  165. package/dist/commands/event/subscription/list.d.ts.map +0 -1
  166. package/dist/commands/event/subscription/list.js +0 -10
  167. package/dist/commands/event/subscription/list.js.map +0 -1
  168. package/dist/commands/event/subscription/rotate-secret.d.ts +0 -15
  169. package/dist/commands/event/subscription/rotate-secret.d.ts.map +0 -1
  170. package/dist/commands/event/subscription/rotate-secret.js +0 -33
  171. package/dist/commands/event/subscription/rotate-secret.js.map +0 -1
  172. package/dist/commands/event/subscription/status.d.ts +0 -14
  173. package/dist/commands/event/subscription/status.d.ts.map +0 -1
  174. package/dist/commands/event/subscription/status.js +0 -19
  175. package/dist/commands/event/subscription/status.js.map +0 -1
  176. package/dist/commands/event/timer/list.d.ts +0 -9
  177. package/dist/commands/event/timer/list.d.ts.map +0 -1
  178. package/dist/commands/event/timer/list.js +0 -10
  179. package/dist/commands/event/timer/list.js.map +0 -1
  180. package/dist/commands/event/timer/status.d.ts +0 -14
  181. package/dist/commands/event/timer/status.d.ts.map +0 -1
  182. package/dist/commands/event/timer/status.js +0 -19
  183. package/dist/commands/event/timer/status.js.map +0 -1
  184. package/dist/commands/generate.d.ts +0 -10
  185. package/dist/commands/generate.d.ts.map +0 -1
  186. package/dist/commands/generate.js +0 -8
  187. package/dist/commands/generate.js.map +0 -1
  188. package/dist/commands/oauth/audit/list.d.ts +0 -12
  189. package/dist/commands/oauth/audit/list.d.ts.map +0 -1
  190. package/dist/commands/oauth/audit/list.js +0 -20
  191. package/dist/commands/oauth/audit/list.js.map +0 -1
  192. package/dist/commands/oauth/client/create.d.ts +0 -13
  193. package/dist/commands/oauth/client/create.d.ts.map +0 -1
  194. package/dist/commands/oauth/client/create.js +0 -34
  195. package/dist/commands/oauth/client/create.js.map +0 -1
  196. package/dist/commands/oauth/client/list.d.ts +0 -9
  197. package/dist/commands/oauth/client/list.d.ts.map +0 -1
  198. package/dist/commands/oauth/client/list.js +0 -10
  199. package/dist/commands/oauth/client/list.js.map +0 -1
  200. package/dist/commands/oauth/client/revoke.d.ts +0 -12
  201. package/dist/commands/oauth/client/revoke.d.ts.map +0 -1
  202. package/dist/commands/oauth/client/revoke.js +0 -12
  203. package/dist/commands/oauth/client/revoke.js.map +0 -1
  204. package/dist/commands/oauth/client/rotate.d.ts +0 -13
  205. package/dist/commands/oauth/client/rotate.d.ts.map +0 -1
  206. package/dist/commands/oauth/client/rotate.js +0 -20
  207. package/dist/commands/oauth/client/rotate.js.map +0 -1
  208. package/dist/commands/oauth/client/update.d.ts +0 -15
  209. package/dist/commands/oauth/client/update.d.ts.map +0 -1
  210. package/dist/commands/oauth/client/update.js +0 -23
  211. package/dist/commands/oauth/client/update.js.map +0 -1
  212. package/dist/commands/oauth/runtime/rotate.d.ts +0 -12
  213. package/dist/commands/oauth/runtime/rotate.d.ts.map +0 -1
  214. package/dist/commands/oauth/runtime/rotate.js +0 -31
  215. package/dist/commands/oauth/runtime/rotate.js.map +0 -1
  216. package/dist/commands/oauth/runtime/status.d.ts +0 -10
  217. package/dist/commands/oauth/runtime/status.d.ts.map +0 -1
  218. package/dist/commands/oauth/runtime/status.js +0 -17
  219. package/dist/commands/oauth/runtime/status.js.map +0 -1
  220. package/dist/commands/promote.d.ts +0 -13
  221. package/dist/commands/promote.d.ts.map +0 -1
  222. package/dist/commands/promote.js +0 -9
  223. package/dist/commands/promote.js.map +0 -1
  224. package/dist/commands/secret/audit/list.d.ts +0 -14
  225. package/dist/commands/secret/audit/list.d.ts.map +0 -1
  226. package/dist/commands/secret/audit/list.js +0 -16
  227. package/dist/commands/secret/audit/list.js.map +0 -1
  228. package/dist/commands/secret/create.d.ts +0 -18
  229. package/dist/commands/secret/create.d.ts.map +0 -1
  230. package/dist/commands/secret/create.js +0 -33
  231. package/dist/commands/secret/create.js.map +0 -1
  232. package/dist/commands/secret/delete.d.ts +0 -15
  233. package/dist/commands/secret/delete.d.ts.map +0 -1
  234. package/dist/commands/secret/delete.js +0 -20
  235. package/dist/commands/secret/delete.js.map +0 -1
  236. package/dist/commands/secret/list.d.ts +0 -10
  237. package/dist/commands/secret/list.d.ts.map +0 -1
  238. package/dist/commands/secret/list.js +0 -14
  239. package/dist/commands/secret/list.js.map +0 -1
  240. package/dist/commands/secret/rotate.d.ts +0 -18
  241. package/dist/commands/secret/rotate.d.ts.map +0 -1
  242. package/dist/commands/secret/rotate.js +0 -30
  243. package/dist/commands/secret/rotate.js.map +0 -1
  244. package/dist/commands/secret/update.d.ts +0 -17
  245. package/dist/commands/secret/update.d.ts.map +0 -1
  246. package/dist/commands/secret/update.js +0 -37
  247. package/dist/commands/secret/update.js.map +0 -1
  248. package/dist/commands/skill/install.d.ts +0 -11
  249. package/dist/commands/skill/install.d.ts.map +0 -1
  250. package/dist/commands/skill/install.js +0 -52
  251. package/dist/commands/skill/install.js.map +0 -1
  252. package/dist/commands/skill/validate.d.ts +0 -9
  253. package/dist/commands/skill/validate.d.ts.map +0 -1
  254. package/dist/commands/skill/validate.js +0 -32
  255. package/dist/commands/skill/validate.js.map +0 -1
  256. package/dist/commands/test.d.ts +0 -9
  257. package/dist/commands/test.d.ts.map +0 -1
  258. package/dist/commands/test.js +0 -7
  259. package/dist/commands/test.js.map +0 -1
  260. package/dist/commands/workflow/provider/list.d.ts +0 -10
  261. package/dist/commands/workflow/provider/list.d.ts.map +0 -1
  262. package/dist/commands/workflow/provider/list.js +0 -14
  263. package/dist/commands/workflow/provider/list.js.map +0 -1
  264. package/dist/commands/workflow/provider/rotate.d.ts +0 -15
  265. package/dist/commands/workflow/provider/rotate.d.ts.map +0 -1
  266. package/dist/commands/workflow/provider/rotate.js +0 -24
  267. package/dist/commands/workflow/provider/rotate.js.map +0 -1
  268. package/dist/secret-value.d.ts +0 -5
  269. package/dist/secret-value.d.ts.map +0 -1
  270. package/dist/secret-value.js +0 -15
  271. package/dist/secret-value.js.map +0 -1
@@ -0,0 +1,576 @@
1
+ import { expect, test, type Page } from '@playwright/test';
2
+ import { appCode } from '../../../packages/contracts/src/generated.js';
3
+
4
+ const taskId = '11111111-1111-4111-8111-111111111111';
5
+ const instanceId = '22222222-2222-4222-8222-222222222222';
6
+ function envelope(data: unknown, code = 200) {
7
+ return { code, message: code === 200 ? 'success' : 'forbidden', data };
8
+ }
9
+
10
+ function detailNavigation(custom = false) {
11
+ return custom
12
+ ? {
13
+ custom: true,
14
+ desktopPath: `/admin/operations/purchases/${instanceId}?taskId=${taskId}`,
15
+ mobilePath: `/m/purchases/${instanceId}?taskId=${taskId}`,
16
+ }
17
+ : {
18
+ custom: false,
19
+ desktopPath: `/admin/tasks/${taskId}`,
20
+ mobilePath: `/m/tasks/${taskId}`,
21
+ };
22
+ }
23
+
24
+ function task(completed = false, customDetail = false) {
25
+ return {
26
+ schemaVersion: 'openxiangda.workflow-task/v2',
27
+ engineVersion: '2.0',
28
+ id: taskId,
29
+ instanceId,
30
+ workflowCode: 'purchase-approval',
31
+ businessKey: 'PO-2026-0825',
32
+ nodeId: 'manager-review',
33
+ title: '采购申请审批',
34
+ status: completed ? 'completed' : 'assigned',
35
+ approvalMode: 'single',
36
+ taskKind: 'normal',
37
+ assignedRoleSubjectKey: 'membership:manager',
38
+ activeParticipantId: 'participant-1',
39
+ participants: [],
40
+ originTaskId: null,
41
+ returnSessionId: null,
42
+ version: completed ? 2 : 1,
43
+ dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
44
+ createdAt: '2026-08-25T01:00:00.000Z',
45
+ updatedAt: '2026-08-25T01:10:00.000Z',
46
+ detailNavigation: detailNavigation(customDetail),
47
+ };
48
+ }
49
+
50
+ function surface(completed = false, customDetail = false) {
51
+ return {
52
+ schemaVersion: 'openxiangda.workflow-surface/v2',
53
+ protocolVersion: 'workflow_surface_v2',
54
+ surfaceRevision: completed ? 'revision-2' : 'revision-1',
55
+ engineVersion: '2.0',
56
+ instanceSequence: completed ? 4 : 3,
57
+ detailNavigation: detailNavigation(customDetail),
58
+ navigationTarget: {
59
+ kind: 'resource_record',
60
+ appCode,
61
+ environmentKey: 'preproduction',
62
+ resourceCode: 'purchase-orders',
63
+ recordId: 'purchase-1',
64
+ desktopPath: '/purchase-orders/purchase-1',
65
+ mobilePath: '/m/purchase-orders/purchase-1',
66
+ },
67
+ instance: {
68
+ schemaVersion: 'openxiangda.workflow-instance/v2',
69
+ engineVersion: '2.0',
70
+ id: instanceId,
71
+ appCode,
72
+ environmentId: 'preproduction-id',
73
+ environmentKey: 'preproduction',
74
+ workflowCode: 'purchase-approval',
75
+ definitionVersion: 1,
76
+ bindingVersion: 1,
77
+ businessKey: 'PO-2026-0825',
78
+ generation: 1,
79
+ status: completed ? 'approved' : 'running',
80
+ initiatorUserId: 'applicant-user',
81
+ initiatorAuthorizationDigest: null,
82
+ dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
83
+ dataRevision: '8',
84
+ currentNodeId: completed ? null : 'manager-review',
85
+ outcome: completed ? 'approved' : null,
86
+ version: completed ? 3 : 2,
87
+ eventSequence: completed ? 4 : 3,
88
+ startedAt: '2026-08-25T01:00:00.000Z',
89
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
90
+ },
91
+ task: completed ? null : task(false, customDetail),
92
+ presentation: {
93
+ status: { label: completed ? '已同意' : '待审批', tone: 'warning' },
94
+ layout: 'approval_detail',
95
+ businessData: {
96
+ 申请金额: '¥28,600',
97
+ 采购类型: '办公设备',
98
+ 期望到货: '2026-09-05',
99
+ 申请人: '张三',
100
+ },
101
+ },
102
+ fieldPolicy: { default: 'readonly', fields: {} },
103
+ operations: completed
104
+ ? []
105
+ : [
106
+ {
107
+ key: 'approve',
108
+ kind: 'workflow_command',
109
+ label: '同意',
110
+ placement: 'primary',
111
+ tone: 'primary',
112
+ visible: true,
113
+ enabled: true,
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ comment: {
118
+ type: 'string',
119
+ title: '审批意见',
120
+ format: 'textarea',
121
+ maxLength: 500,
122
+ },
123
+ },
124
+ },
125
+ uiSchema: {
126
+ confirmText: '确认同意',
127
+ confirmation: { description: '提交后流程继续流转。' },
128
+ },
129
+ execute: {
130
+ method: 'POST',
131
+ href: '/workflow/approve',
132
+ idempotencyRequired: true,
133
+ },
134
+ refresh: ['surface', 'timeline', 'work_center'],
135
+ },
136
+ ...['reject', 'return', 'transfer', 'add_assignee'].map(key => ({
137
+ key,
138
+ kind: 'workflow_command',
139
+ label: {
140
+ reject: '拒绝',
141
+ return: '退回',
142
+ transfer: '转交',
143
+ add_assignee: '加签',
144
+ }[key],
145
+ placement: 'secondary',
146
+ tone: key === 'reject' ? 'danger' : 'neutral',
147
+ visible: true,
148
+ enabled: true,
149
+ inputSchema: { type: 'object', properties: {} },
150
+ uiSchema: { confirmText: `确认${key}` },
151
+ execute: {
152
+ method: 'POST',
153
+ href: `/workflow/${key}`,
154
+ idempotencyRequired: true,
155
+ },
156
+ refresh: ['surface', 'timeline', 'work_center'],
157
+ })),
158
+ ],
159
+ extensions: {},
160
+ };
161
+ }
162
+
163
+ function timeline(completed = false) {
164
+ return {
165
+ engineVersion: '2.0',
166
+ instanceId,
167
+ flow: [
168
+ {
169
+ key: 'start',
170
+ nodeId: null,
171
+ kind: 'start',
172
+ title: '张三',
173
+ status: 'completed',
174
+ startedAt: '2026-08-25T01:00:00.000Z',
175
+ completedAt: '2026-08-25T01:00:00.000Z',
176
+ assignees: [],
177
+ },
178
+ {
179
+ key: 'manager',
180
+ nodeId: 'manager-review',
181
+ kind: 'approval',
182
+ title: '部门负责人',
183
+ status: completed ? 'completed' : 'active',
184
+ startedAt: '2026-08-25T01:01:00.000Z',
185
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
186
+ assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
187
+ },
188
+ {
189
+ key: 'finance',
190
+ nodeId: 'finance-review',
191
+ kind: 'approval',
192
+ title: '财务负责人',
193
+ status: completed ? 'completed' : 'waiting',
194
+ startedAt: null,
195
+ completedAt: null,
196
+ assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
197
+ },
198
+ ],
199
+ items: [],
200
+ };
201
+ }
202
+
203
+ function launchSurface() {
204
+ return {
205
+ schemaVersion: 'openxiangda.workflow-launch-surface/v2',
206
+ protocolVersion: 'workflow_launch_surface_v2',
207
+ surfaceRevision: 'launch-revision-1',
208
+ engineVersion: '2.0',
209
+ appCode,
210
+ environmentKey: 'preproduction',
211
+ environmentId: 'preproduction-id',
212
+ workflowCode: 'purchase-approval',
213
+ title: '采购审批',
214
+ launchMode: 'standalone',
215
+ inputSchema: { type: 'object', properties: {} },
216
+ head: {
217
+ workflowRevision: 4,
218
+ definitionVersion: 1,
219
+ bindingVersion: 1,
220
+ nativeRevision: 3,
221
+ contractRevisionId: 'contract-revision-1',
222
+ },
223
+ paths: {
224
+ desktop: '/admin/workflows/purchase-approval/new',
225
+ mobile: '/m/workflows/purchase-approval/start',
226
+ },
227
+ prepare: {
228
+ method: 'POST',
229
+ href: '/workflow/definitions/purchase-approval/prepare-start',
230
+ },
231
+ start: {
232
+ method: 'POST',
233
+ href: '/workflow/instances',
234
+ idempotencyRequired: true,
235
+ },
236
+ };
237
+ }
238
+
239
+ function todoPage() {
240
+ return {
241
+ schemaVersion: 'openxiangda.application-todo-center/v2',
242
+ appCode,
243
+ environmentKey: 'preproduction',
244
+ view: 'pending',
245
+ counts: { pending: 1, informational: 2, completed: 3, unread: 1 },
246
+ items: [
247
+ {
248
+ messageId: '33333333-3333-4333-8333-333333333333',
249
+ recipientId: '44444444-4444-4444-8444-444444444444',
250
+ state: 'action_required',
251
+ interactionState: 'unread',
252
+ title: '设备采购申请待审批',
253
+ summary: '张三提交了一笔办公设备采购申请',
254
+ fields: [
255
+ { code: 'amount', label: '申请金额', value: '¥28,600' },
256
+ { code: 'department', label: '申请部门', value: '理学院' },
257
+ ],
258
+ actions: [{ code: 'open', label: '去处理' }],
259
+ sourceKind: 'PLATFORM_EVENT',
260
+ sourceLabel: '流程中心',
261
+ occurredAt: '2026-08-28T01:00:00.000Z',
262
+ updatedAt: '2026-08-28T01:01:00.000Z',
263
+ navigation: {
264
+ desktopPath: `/admin/operations/purchases/${instanceId}?taskId=${taskId}`,
265
+ mobilePath: `/m/purchases/${instanceId}?taskId=${taskId}`,
266
+ external: false,
267
+ navigationUnavailable: false,
268
+ },
269
+ },
270
+ ],
271
+ total: 1,
272
+ limit: 12,
273
+ offset: 0,
274
+ nextOffset: null,
275
+ };
276
+ }
277
+
278
+ async function mockWorkflow(
279
+ page: Page,
280
+ options: { forbidden?: boolean; customDetail?: boolean } = {}
281
+ ) {
282
+ let completed = false;
283
+ await page.route('**/service/**', async route => {
284
+ const url = new URL(route.request().url());
285
+ const path = url.pathname;
286
+ if (path.endsWith('/native/authz/current')) {
287
+ return route.fulfill({
288
+ contentType: 'application/json',
289
+ body: JSON.stringify(
290
+ envelope({
291
+ schemaVersion: 'openxiangda.runtime-authorization/v2',
292
+ state: 'active',
293
+ environment: {
294
+ id: 'preproduction-id',
295
+ key: 'preproduction',
296
+ activeAppVersionId: 'version-1',
297
+ headRevision: 1,
298
+ authzRevisionId: 'authz-1',
299
+ authzVersion: 1,
300
+ scopeDataVersion: 'scope-1',
301
+ },
302
+ subjectProfile: {
303
+ schemaVersion: 'openxiangda.subject-profile/v2',
304
+ userId: 'acceptance-user',
305
+ displayName: '王老师',
306
+ avatarUrl: null,
307
+ jobNumber: 'T001',
308
+ affiliatedDepartment: { id: 'department-1', name: '理学院' },
309
+ },
310
+ roles: [
311
+ {
312
+ code: 'department_manager',
313
+ name: '部门管理员',
314
+ source: 'package',
315
+ },
316
+ ],
317
+ principal: {
318
+ type: 'user_union',
319
+ userId: 'acceptance-user',
320
+ roleCodes: ['department_manager'],
321
+ capabilityCodes: [],
322
+ isAppSuperAdmin: false,
323
+ identityScope:
324
+ 'user-union:preproduction-id:acceptance-user:authz-1',
325
+ },
326
+ })
327
+ ),
328
+ });
329
+ }
330
+ if (path.includes('/directory/')) {
331
+ return route.fulfill({
332
+ contentType: 'application/json',
333
+ body: JSON.stringify(envelope({ items: [], nextCursor: null })),
334
+ });
335
+ }
336
+ if (options.forbidden && path.includes('/workflow/')) {
337
+ return route.fulfill({
338
+ status: 403,
339
+ contentType: 'application/json',
340
+ body: JSON.stringify(envelope(null, 403)),
341
+ });
342
+ }
343
+ if (path.endsWith('/todos') && route.request().method() === 'GET') {
344
+ return route.fulfill({
345
+ contentType: 'application/json',
346
+ body: JSON.stringify(envelope(todoPage())),
347
+ });
348
+ }
349
+ if (path.includes('/todos/') && path.endsWith('/interactions')) {
350
+ return route.fulfill({
351
+ contentType: 'application/json',
352
+ body: JSON.stringify(
353
+ envelope({
354
+ accepted: true,
355
+ duplicate: false,
356
+ interactionState: 'clicked',
357
+ })
358
+ ),
359
+ });
360
+ }
361
+ if (
362
+ path.endsWith(
363
+ '/workflow/definitions/purchase-approval/launch-surface'
364
+ )
365
+ ) {
366
+ return route.fulfill({
367
+ contentType: 'application/json',
368
+ body: JSON.stringify(envelope(launchSurface())),
369
+ });
370
+ }
371
+ if (
372
+ path.endsWith(
373
+ '/workflow/definitions/purchase-approval/prepare-start'
374
+ )
375
+ ) {
376
+ return route.fulfill({
377
+ contentType: 'application/json',
378
+ body: JSON.stringify(
379
+ envelope({
380
+ schemaVersion: 'openxiangda.workflow-preparation/v2',
381
+ preparationId: 'preparation-1',
382
+ status: 'ready',
383
+ requirements: [],
384
+ preparationToken: 'preparation-token-1',
385
+ preview: { nodes: [] },
386
+ expiresAt: '2026-08-28T02:00:00.000Z',
387
+ definitionVersion: 1,
388
+ bindingVersion: 1,
389
+ inputHash: 'input-hash-1',
390
+ })
391
+ ),
392
+ });
393
+ }
394
+ if (
395
+ path.endsWith('/workflow/instances') &&
396
+ route.request().method() === 'POST'
397
+ ) {
398
+ return route.fulfill({
399
+ contentType: 'application/json',
400
+ body: JSON.stringify(
401
+ envelope({
402
+ ...surface(false, options.customDetail).instance,
403
+ id: instanceId,
404
+ })
405
+ ),
406
+ });
407
+ }
408
+ if (path.endsWith('/workflow/work-center/items')) {
409
+ return route.fulfill({
410
+ contentType: 'application/json',
411
+ body: JSON.stringify(
412
+ envelope({
413
+ engineVersion: '2.0',
414
+ authorizationMode: 'current_user_role_union',
415
+ authorizationDigest: 'digest-1',
416
+ status: 'pending',
417
+ total: 1,
418
+ limit: 20,
419
+ offset: 0,
420
+ items: [task(completed, options.customDetail)],
421
+ })
422
+ ),
423
+ });
424
+ }
425
+ if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
426
+ return route.fulfill({
427
+ contentType: 'application/json',
428
+ body: JSON.stringify(
429
+ envelope(surface(completed, options.customDetail))
430
+ ),
431
+ });
432
+ }
433
+ if (path.endsWith(`/workflow/instances/${instanceId}/surface`)) {
434
+ return route.fulfill({
435
+ contentType: 'application/json',
436
+ body: JSON.stringify(
437
+ envelope(surface(completed, options.customDetail))
438
+ ),
439
+ });
440
+ }
441
+ if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
442
+ return route.fulfill({
443
+ contentType: 'application/json',
444
+ body: JSON.stringify(envelope(timeline(completed))),
445
+ });
446
+ }
447
+ if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
448
+ completed = true;
449
+ return route.fulfill({
450
+ contentType: 'application/json',
451
+ body: JSON.stringify(envelope({ status: 'completed' })),
452
+ });
453
+ }
454
+ return route.fulfill({
455
+ status: 404,
456
+ contentType: 'application/json',
457
+ body: JSON.stringify(envelope(null, 404)),
458
+ });
459
+ });
460
+ }
461
+
462
+ test('renders a paged desktop work center from the current-user role union', async ({
463
+ page,
464
+ }) => {
465
+ await mockWorkflow(page);
466
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center');
467
+ await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
468
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
469
+ await expect(page.getByText('PO-2026-0825')).toBeVisible();
470
+ });
471
+
472
+ test('renders the generic desktop and mobile application todo center without a second router', async ({
473
+ page,
474
+ }) => {
475
+ await mockWorkflow(page);
476
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/todos');
477
+ await expect(page.getByRole('heading', { name: '待办中心' })).toBeVisible();
478
+ await expect(page.getByText('设备采购申请待审批').first()).toBeVisible();
479
+ await expect(page.getByText('¥28,600').first()).toBeVisible();
480
+
481
+ await page.setViewportSize({ width: 390, height: 844 });
482
+ await page.goto('/workflow-experience.e2e.html?initial=/m/todos');
483
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
484
+ await page.getByRole('button', { name: '去处理' }).click();
485
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
486
+ `${instanceId} / ${taskId}`
487
+ );
488
+ });
489
+
490
+ test('launches the same workflow from the canonical mobile Surface and follows custom detail routing', async ({
491
+ page,
492
+ }) => {
493
+ await mockWorkflow(page, { customDetail: true });
494
+ await page.setViewportSize({ width: 390, height: 844 });
495
+ await page.goto(
496
+ '/workflow-experience.e2e.html?initial=/m/workflows/purchase-approval/start'
497
+ );
498
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
499
+ await expect(page.getByRole('heading', { name: '采购审批' })).toBeVisible();
500
+ await page.getByRole('button', { name: '保存业务数据并预检' }).click();
501
+ await expect(page.getByRole('button', { name: '提交审批' })).toBeVisible();
502
+ await page.getByRole('button', { name: '提交审批' }).click();
503
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
504
+ `${instanceId} / ${taskId}`
505
+ );
506
+ await expect(page).toHaveURL(
507
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
508
+ );
509
+ });
510
+
511
+ test('routes desktop and mobile work-center entries to the declared custom detail pages', async ({
512
+ page,
513
+ }) => {
514
+ await mockWorkflow(page, { customDetail: true });
515
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center');
516
+ await page.getByRole('link', { name: '采购申请审批' }).click();
517
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
518
+ `${instanceId} / ${taskId}`
519
+ );
520
+ await expect(page).toHaveURL(
521
+ new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}$`)
522
+ );
523
+
524
+ await page.setViewportSize({ width: 390, height: 844 });
525
+ await page.goto('/workflow-experience.e2e.html?initial=/m/work-center');
526
+ await page.getByText('采购申请审批').click();
527
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
528
+ `${instanceId} / ${taskId}`
529
+ );
530
+ await expect(page).toHaveURL(
531
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
532
+ );
533
+ });
534
+
535
+ test('redirects a direct standard task URL to the declared custom detail page', async ({
536
+ page,
537
+ }) => {
538
+ await mockWorkflow(page, { customDetail: true });
539
+ await page.goto(
540
+ `/workflow-experience.e2e.html?initial=/admin/tasks/${taskId}`
541
+ );
542
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
543
+ `${instanceId} / ${taskId}`
544
+ );
545
+ });
546
+
547
+ test('renders the independent mobile task and executes only a Surface operation', async ({
548
+ page,
549
+ }) => {
550
+ await mockWorkflow(page);
551
+ await page.setViewportSize({ width: 390, height: 844 });
552
+ await page.goto(`/workflow-experience.e2e.html?initial=/m/tasks/${taskId}`);
553
+ await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
554
+ await expect(page.getByText('¥28,600')).toBeVisible();
555
+ await expect(page.getByText('财务负责人').first()).toBeVisible();
556
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
557
+ await page.getByRole('button', { name: /同\s*意/ }).click();
558
+ await page.getByLabel('审批意见').fill('同意采购');
559
+ await page.getByRole('button', { name: '确认同意' }).click();
560
+ await expect(page.getByText('同意已提交')).toBeVisible();
561
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
562
+ });
563
+
564
+ test('shows an explicit authorization error instead of an empty work center', async ({
565
+ page,
566
+ }) => {
567
+ test.setTimeout(45_000);
568
+ await mockWorkflow(page, { forbidden: true });
569
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center', {
570
+ waitUntil: 'domcontentloaded',
571
+ });
572
+ await expect(page.getByRole('alert')).toContainText('HTTP_403: forbidden', {
573
+ timeout: 30_000,
574
+ });
575
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
576
+ });
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="openxiangda-runtime-base" content="/" />
7
+ <meta name="openxiangda-app-code" content="field-protocol-fixture" />
8
+ <meta name="openxiangda-environment" content="preproduction" />
9
+ <title>Field protocol acceptance</title>
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/e2e/field-protocol-fixture.tsx"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenXiangda 应用</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@app/web",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "check": "tsc -p tsconfig.json --noEmit && node scripts/check.mjs",
9
+ "test": "tsx --test test/*.test.ts",
10
+ "test:e2e": "playwright test",
11
+ "build": "tsc -p tsconfig.json --noEmit && vite build && node scripts/verify-build.mjs"
12
+ },
13
+ "dependencies": {
14
+ "@ant-design/icons": "6.2.3",
15
+ "@refinedev/core": "5.0.12",
16
+ "antd": "6.4.3",
17
+ "openxiangda": "2.0.0-alpha.34",
18
+ "react": "19.2.8",
19
+ "react-dom": "19.2.8",
20
+ "react-router-dom": "7.8.2"
21
+ },
22
+ "devDependencies": {
23
+ "@playwright/test": "1.62.1",
24
+ "@types/node": "24.13.3",
25
+ "@types/react": "19.2.14",
26
+ "@types/react-dom": "19.2.3",
27
+ "@vitejs/plugin-react": "4.7.0",
28
+ "tsx": "4.23.12",
29
+ "typescript": "5.9.3",
30
+ "vite": "7.1.3"
31
+ }
32
+ }
@@ -0,0 +1,47 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ const e2ePort = resolveE2ePort();
4
+ const baseURL =
5
+ process.env.OPENXIANGDA_E2E_BASE_URL || `http://127.0.0.1:${e2ePort}`;
6
+
7
+ export default defineConfig({
8
+ testDir: './e2e',
9
+ // The development server owns one mutable dependency-optimizer generation.
10
+ // Serial browser entrypoints prevent a second HTML graph from invalidating
11
+ // an in-flight transformed module during release acceptance.
12
+ workers: 1,
13
+ use: { baseURL, trace: 'retain-on-failure' },
14
+ webServer: process.env.OPENXIANGDA_E2E_BASE_URL
15
+ ? undefined
16
+ : {
17
+ command: 'pnpm dev',
18
+ url: baseURL,
19
+ reuseExistingServer: false,
20
+ env: {
21
+ OPENXIANGDA_WEB_PORT: String(e2ePort),
22
+ OPENXIANGDA_ENVIRONMENT_KEY: 'production',
23
+ OPENXIANGDA_DEV_PROXY:
24
+ process.env.OPENXIANGDA_E2E_PLATFORM_URL ||
25
+ 'http://127.0.0.1:7001',
26
+ },
27
+ },
28
+ });
29
+
30
+ function resolveE2ePort() {
31
+ const configured = process.env.OPENXIANGDA_E2E_PORT;
32
+ if (configured !== undefined) {
33
+ const port = Number(configured);
34
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
35
+ throw new Error(
36
+ 'OPENXIANGDA_E2E_PORT must be an integer between 1024 and 65535'
37
+ );
38
+ }
39
+ return port;
40
+ }
41
+
42
+ let hash = 2166136261;
43
+ for (const character of process.cwd()) {
44
+ hash = Math.imul(hash ^ character.charCodeAt(0), 16777619);
45
+ }
46
+ return 30_000 + ((hash >>> 0) % 30_000);
47
+ }