openxiangda-cli 2.0.0-alpha.91 → 2.0.0-alpha.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/run.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execute } from '@oclif/core';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
5
6
  const publicCommands = new Set([
@@ -58,10 +59,29 @@ if (args.includes('--mcp-stdio')) {
58
59
  process.exitCode = 2;
59
60
  } else {
60
61
  const root = fileURLToPath(new URL('../', import.meta.url));
62
+ const cliManifest = JSON.parse(
63
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8')
64
+ );
65
+ const distributionManifest = process.env.OPENXIANGDA_DISTRIBUTION_NAME
66
+ ? {
67
+ ...cliManifest,
68
+ name: process.env.OPENXIANGDA_DISTRIBUTION_NAME,
69
+ version:
70
+ process.env.OPENXIANGDA_DISTRIBUTION_VERSION ||
71
+ cliManifest.version,
72
+ }
73
+ : undefined;
61
74
  await execute({
62
75
  args,
63
76
  loadOptions: {
64
77
  root,
78
+ ...(distributionManifest ? { pjson: distributionManifest } : {}),
79
+ ...(process.env.OPENXIANGDA_DISTRIBUTION_NAME
80
+ ? { name: process.env.OPENXIANGDA_DISTRIBUTION_NAME }
81
+ : {}),
82
+ ...(process.env.OPENXIANGDA_DISTRIBUTION_VERSION
83
+ ? { version: process.env.OPENXIANGDA_DISTRIBUTION_VERSION }
84
+ : {}),
65
85
  userPlugins: false,
66
86
  devPlugins: false,
67
87
  jitPlugins: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.91",
3
+ "version": "2.0.0-alpha.96",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,10 +21,10 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "@oclif/core": "4.13.3",
24
- "openxiangda-contracts": "2.0.0-alpha.45",
25
- "openxiangda-devkit-core": "2.0.0-alpha.58",
26
- "openxiangda-mcp": "2.0.0-alpha.58",
27
- "openxiangda-skill-kit": "2.0.0-alpha.76"
24
+ "openxiangda-contracts": "2.0.0-alpha.46",
25
+ "openxiangda-devkit-core": "2.0.0-alpha.59",
26
+ "openxiangda-mcp": "2.0.0-alpha.59",
27
+ "openxiangda-skill-kit": "2.0.0-alpha.77"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsx": "4.23.12",
@@ -15,7 +15,7 @@
15
15
  "@nestjs/common": "11.1.29",
16
16
  "@nestjs/core": "11.1.29",
17
17
  "@nestjs/platform-fastify": "11.1.29",
18
- "openxiangda": "2.0.0-alpha.3",
18
+ "openxiangda": "2.0.0-alpha.8",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -96,6 +96,22 @@ async function mockPlatform(
96
96
  }),
97
97
  });
98
98
  }
99
+ const exportRequest = url.pathname.match(
100
+ /\/native\/data\/([^/]+)\/export$/
101
+ );
102
+ if (exportRequest) {
103
+ observed.push({ path: url.pathname, roleSessionId });
104
+ return route.fulfill({
105
+ status: 200,
106
+ contentType: 'text/csv; charset=utf-8',
107
+ headers: {
108
+ 'content-disposition': `attachment; filename="${decodeURIComponent(
109
+ exportRequest[1]
110
+ )}.csv"`,
111
+ },
112
+ body: 'id,name\nacceptance-1,OpenXiangda\n',
113
+ });
114
+ }
99
115
  const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
100
116
  if (query) {
101
117
  observed.push({ path: url.pathname, roleSessionId });
@@ -0,0 +1,329 @@
1
+ import { expect, test, type Page } from '@playwright/test';
2
+ import { appCode } from '../../../packages/contracts/src/generated.js';
3
+
4
+ const runtimeBase = `/view/${appCode}`;
5
+ const taskId = '11111111-1111-4111-8111-111111111111';
6
+ const instanceId = '22222222-2222-4222-8222-222222222222';
7
+ function envelope(data: unknown, code = 200) {
8
+ return { code, message: code === 200 ? 'success' : 'forbidden', data };
9
+ }
10
+
11
+ function task(completed = false) {
12
+ return {
13
+ schemaVersion: 'openxiangda.workflow-task/v2',
14
+ engineVersion: '2.0',
15
+ id: taskId,
16
+ instanceId,
17
+ workflowCode: 'purchase-approval',
18
+ businessKey: 'PO-2026-0825',
19
+ nodeId: 'manager-review',
20
+ title: '采购申请审批',
21
+ status: completed ? 'completed' : 'assigned',
22
+ approvalMode: 'single',
23
+ taskKind: 'normal',
24
+ assignedRoleSubjectKey: 'membership:manager',
25
+ activeParticipantId: 'participant-1',
26
+ participants: [],
27
+ originTaskId: null,
28
+ returnSessionId: null,
29
+ version: completed ? 2 : 1,
30
+ dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
31
+ createdAt: '2026-08-25T01:00:00.000Z',
32
+ updatedAt: '2026-08-25T01:10:00.000Z',
33
+ };
34
+ }
35
+
36
+ function surface(completed = false) {
37
+ return {
38
+ schemaVersion: 'openxiangda.workflow-surface/v2',
39
+ protocolVersion: 'workflow_surface_v2',
40
+ surfaceRevision: completed ? 'revision-2' : 'revision-1',
41
+ engineVersion: '2.0',
42
+ instanceSequence: completed ? 4 : 3,
43
+ navigationTarget: {
44
+ kind: 'resource_record',
45
+ appCode,
46
+ environmentKey: 'preproduction',
47
+ resourceCode: 'purchase-orders',
48
+ recordId: 'purchase-1',
49
+ desktopPath: '/purchase-orders/purchase-1',
50
+ mobilePath: '/m/purchase-orders/purchase-1',
51
+ },
52
+ instance: {
53
+ schemaVersion: 'openxiangda.workflow-instance/v2',
54
+ engineVersion: '2.0',
55
+ id: instanceId,
56
+ appCode,
57
+ environmentId: 'preproduction-id',
58
+ environmentKey: 'preproduction',
59
+ workflowCode: 'purchase-approval',
60
+ definitionVersion: 1,
61
+ bindingVersion: 1,
62
+ businessKey: 'PO-2026-0825',
63
+ generation: 1,
64
+ status: completed ? 'approved' : 'running',
65
+ initiatorUserId: 'applicant-user',
66
+ initiatorAuthorizationDigest: null,
67
+ dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
68
+ dataRevision: '8',
69
+ currentNodeId: completed ? null : 'manager-review',
70
+ outcome: completed ? 'approved' : null,
71
+ version: completed ? 3 : 2,
72
+ eventSequence: completed ? 4 : 3,
73
+ startedAt: '2026-08-25T01:00:00.000Z',
74
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
75
+ },
76
+ task: completed ? null : task(false),
77
+ presentation: {
78
+ status: { label: completed ? '已同意' : '待审批', tone: 'warning' },
79
+ layout: 'approval_detail',
80
+ businessData: {
81
+ 申请金额: '¥28,600',
82
+ 采购类型: '办公设备',
83
+ 期望到货: '2026-09-05',
84
+ 申请人: '张三',
85
+ },
86
+ },
87
+ fieldPolicy: { default: 'readonly', fields: {} },
88
+ operations: completed
89
+ ? []
90
+ : [
91
+ {
92
+ key: 'approve',
93
+ kind: 'workflow_command',
94
+ label: '同意',
95
+ placement: 'primary',
96
+ tone: 'primary',
97
+ visible: true,
98
+ enabled: true,
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ comment: {
103
+ type: 'string',
104
+ title: '审批意见',
105
+ format: 'textarea',
106
+ maxLength: 500,
107
+ },
108
+ },
109
+ },
110
+ uiSchema: {
111
+ confirmText: '确认同意',
112
+ confirmation: { description: '提交后流程继续流转。' },
113
+ },
114
+ execute: {
115
+ method: 'POST',
116
+ href: '/workflow/approve',
117
+ idempotencyRequired: true,
118
+ },
119
+ refresh: ['surface', 'timeline', 'work_center'],
120
+ },
121
+ ...['reject', 'return', 'transfer', 'add_assignee'].map((key) => ({
122
+ key,
123
+ kind: 'workflow_command',
124
+ label: {
125
+ reject: '拒绝',
126
+ return: '退回',
127
+ transfer: '转交',
128
+ add_assignee: '加签',
129
+ }[key],
130
+ placement: 'secondary',
131
+ tone: key === 'reject' ? 'danger' : 'neutral',
132
+ visible: true,
133
+ enabled: true,
134
+ inputSchema: { type: 'object', properties: {} },
135
+ uiSchema: { confirmText: `确认${key}` },
136
+ execute: {
137
+ method: 'POST',
138
+ href: `/workflow/${key}`,
139
+ idempotencyRequired: true,
140
+ },
141
+ refresh: ['surface', 'timeline', 'work_center'],
142
+ })),
143
+ ],
144
+ extensions: {},
145
+ };
146
+ }
147
+
148
+ function timeline(completed = false) {
149
+ return {
150
+ engineVersion: '2.0',
151
+ instanceId,
152
+ flow: [
153
+ {
154
+ key: 'start',
155
+ nodeId: null,
156
+ kind: 'start',
157
+ title: '张三',
158
+ status: 'completed',
159
+ startedAt: '2026-08-25T01:00:00.000Z',
160
+ completedAt: '2026-08-25T01:00:00.000Z',
161
+ assignees: [],
162
+ },
163
+ {
164
+ key: 'manager',
165
+ nodeId: 'manager-review',
166
+ kind: 'approval',
167
+ title: '部门负责人',
168
+ status: completed ? 'completed' : 'active',
169
+ startedAt: '2026-08-25T01:01:00.000Z',
170
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
171
+ assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
172
+ },
173
+ {
174
+ key: 'finance',
175
+ nodeId: 'finance-review',
176
+ kind: 'approval',
177
+ title: '财务负责人',
178
+ status: completed ? 'completed' : 'waiting',
179
+ startedAt: null,
180
+ completedAt: null,
181
+ assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
182
+ },
183
+ ],
184
+ items: [],
185
+ };
186
+ }
187
+
188
+ async function mockWorkflow(page: Page, options: { forbidden?: boolean } = {}) {
189
+ let completed = false;
190
+ await page.route(`**${runtimeBase}/**`, async (route) => {
191
+ if (route.request().resourceType() !== 'document') return route.continue();
192
+ const response = await route.fetch();
193
+ const body = (await response.text()).replace(
194
+ '<head>',
195
+ `<head><meta name="openxiangda-runtime-base" content="${runtimeBase}/" />` +
196
+ `<meta name="openxiangda-app-code" content="${appCode}" />` +
197
+ '<meta name="openxiangda-environment" content="preproduction" />',
198
+ );
199
+ await route.fulfill({ response, body });
200
+ });
201
+ await page.route('**/service/**', async (route) => {
202
+ const url = new URL(route.request().url());
203
+ const path = url.pathname;
204
+ if (path.endsWith('/native/authz/current')) {
205
+ return route.fulfill({
206
+ contentType: 'application/json',
207
+ body: JSON.stringify(
208
+ envelope({
209
+ schemaVersion: 'openxiangda.runtime-authorization/v2',
210
+ state: 'active',
211
+ environment: {
212
+ id: 'preproduction-id',
213
+ key: 'preproduction',
214
+ activeAppVersionId: 'version-1',
215
+ headRevision: 1,
216
+ authzRevisionId: 'authz-1',
217
+ authzVersion: 1,
218
+ scopeDataVersion: 'scope-1',
219
+ },
220
+ principal: {
221
+ type: 'user_union',
222
+ userId: 'acceptance-user',
223
+ roleCodes: ['department_manager'],
224
+ capabilityCodes: [],
225
+ isAppSuperAdmin: false,
226
+ identityScope:
227
+ 'user-union:preproduction-id:acceptance-user:authz-1',
228
+ },
229
+ }),
230
+ ),
231
+ });
232
+ }
233
+ if (path.includes('/directory/')) {
234
+ return route.fulfill({
235
+ contentType: 'application/json',
236
+ body: JSON.stringify(envelope({ items: [], nextCursor: null })),
237
+ });
238
+ }
239
+ if (options.forbidden && path.includes('/workflow/')) {
240
+ return route.fulfill({
241
+ status: 403,
242
+ contentType: 'application/json',
243
+ body: JSON.stringify(envelope(null, 403)),
244
+ });
245
+ }
246
+ if (path.endsWith('/workflow/work-center/items')) {
247
+ return route.fulfill({
248
+ contentType: 'application/json',
249
+ body: JSON.stringify(
250
+ envelope({
251
+ engineVersion: '2.0',
252
+ authorizationMode: 'current_user_role_union',
253
+ authorizationDigest: 'digest-1',
254
+ status: 'pending',
255
+ total: 1,
256
+ limit: 20,
257
+ offset: 0,
258
+ items: [task(completed)],
259
+ }),
260
+ ),
261
+ });
262
+ }
263
+ if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
264
+ return route.fulfill({
265
+ contentType: 'application/json',
266
+ body: JSON.stringify(envelope(surface(completed))),
267
+ });
268
+ }
269
+ if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
270
+ return route.fulfill({
271
+ contentType: 'application/json',
272
+ body: JSON.stringify(envelope(timeline(completed))),
273
+ });
274
+ }
275
+ if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
276
+ completed = true;
277
+ return route.fulfill({
278
+ contentType: 'application/json',
279
+ body: JSON.stringify(envelope({ status: 'completed' })),
280
+ });
281
+ }
282
+ return route.fulfill({
283
+ status: 404,
284
+ contentType: 'application/json',
285
+ body: JSON.stringify(envelope(null, 404)),
286
+ });
287
+ });
288
+ }
289
+
290
+ test('renders a paged desktop work center from the current-user role union', async ({
291
+ page,
292
+ }) => {
293
+ await mockWorkflow(page);
294
+ await page.goto(`${runtimeBase}/admin/work-center`);
295
+ await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
296
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
297
+ await expect(page.getByText('PO-2026-0825')).toBeVisible();
298
+ });
299
+
300
+ test('renders the independent mobile task and executes only a Surface operation', async ({
301
+ page,
302
+ }) => {
303
+ await mockWorkflow(page);
304
+ await page.setViewportSize({ width: 390, height: 844 });
305
+ await page.goto(`${runtimeBase}/m/tasks/${taskId}`);
306
+ await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
307
+ await expect(page.getByText('¥28,600')).toBeVisible();
308
+ await expect(page.getByText('财务负责人').first()).toBeVisible();
309
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
310
+ await page.getByRole('button', { name: /同\s*意/ }).click();
311
+ await page.getByLabel('审批意见').fill('同意采购');
312
+ await page.getByRole('button', { name: '确认同意' }).click();
313
+ await expect(page.getByText('同意已提交')).toBeVisible();
314
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
315
+ });
316
+
317
+ test('shows an explicit authorization error instead of an empty work center', async ({
318
+ page,
319
+ }) => {
320
+ test.setTimeout(45_000);
321
+ await mockWorkflow(page, { forbidden: true });
322
+ await page.goto(`${runtimeBase}/admin/work-center`, {
323
+ waitUntil: 'domcontentloaded',
324
+ });
325
+ await expect(page.getByRole('alert')).toContainText('HTTP_403: forbidden', {
326
+ timeout: 30_000,
327
+ });
328
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
329
+ });
@@ -14,7 +14,7 @@
14
14
  "@ant-design/icons": "6.2.3",
15
15
  "@refinedev/core": "5.0.12",
16
16
  "antd": "6.4.3",
17
- "openxiangda": "2.0.0-alpha.3",
17
+ "openxiangda": "2.0.0-alpha.8",
18
18
  "react": "19.2.8",
19
19
  "react-dom": "19.2.8",
20
20
  "react-router-dom": "7.8.2"
@@ -6,6 +6,7 @@ const baseURL =
6
6
 
7
7
  export default defineConfig({
8
8
  testDir: './e2e',
9
+ workers: 2,
9
10
  use: { baseURL, trace: 'retain-on-failure' },
10
11
  webServer: process.env.OPENXIANGDA_E2E_BASE_URL
11
12
  ? undefined
@@ -1,32 +1,6 @@
1
- import { readFileSync, readdirSync, statSync } from 'node:fs';
2
- import { gzipSync } from 'node:zlib';
3
- import { join, resolve } from 'node:path';
1
+ import { verifyOpenXiangdaWebBuild } from 'openxiangda/testing';
4
2
 
5
- const dist = resolve(import.meta.dirname, '../dist');
6
- const assets = join(dist, 'assets');
7
- const names = readdirSync(assets);
8
- const files = names.map(name => join(assets, name));
9
- const total = files.reduce((bytes, file) => bytes + statSync(file).size, 0);
10
- if (total > 2_500_000) throw new Error(`WEB_DIST_BUDGET_EXCEEDED:${total}>2500000`);
11
- const javascript = files.filter(file => file.endsWith('.js'));
12
- const gzip = javascript.reduce(
13
- (bytes, file) => bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength,
14
- 0
15
- );
16
- if (gzip > 800_000) throw new Error(`WEB_JS_GZIP_BUDGET_EXCEEDED:${gzip}>800000`);
17
- if (names.some(name => name.endsWith('.map'))) throw new Error('WEB_SOURCE_MAP_FORBIDDEN');
18
- const html = readFileSync(join(dist, 'index.html'), 'utf8');
19
- if (!html.includes('<div id="root"></div>')) throw new Error('WEB_ROOT_MISSING');
20
- const initialNames = [
21
- ...html.matchAll(/(?:src|href)="\.\/assets\/([^"]+\.js)"/g),
22
- ].map(match => match[1]);
23
- const initialGzip = [...new Set(initialNames)].reduce((bytes, name) => {
24
- const file = join(assets, name);
25
- return bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength;
26
- }, 0);
27
- if (initialGzip > 600_000) {
28
- throw new Error(`WEB_INITIAL_JS_GZIP_BUDGET_EXCEEDED:${initialGzip}>600000`);
29
- }
3
+ const result = verifyOpenXiangdaWebBuild(new URL('../dist', import.meta.url));
30
4
  process.stdout.write(
31
- `Verified Vite build: ${total} bytes, ${gzip} total gzip bytes, ${initialGzip} initial gzip bytes.\n`
5
+ `Verified Vite build: ${result.totalBytes} bytes, ${result.totalJavaScriptGzipBytes} total gzip bytes, ${result.initialJavaScriptGzipBytes} initial gzip bytes.\n`
32
6
  );
@@ -5,6 +5,7 @@ import {
5
5
  appCode,
6
6
  appName,
7
7
  resourceDefinitions,
8
+ workflowCodes,
8
9
  } from '../../../packages/contracts/src/generated.js';
9
10
  import 'openxiangda/react/styles.css';
10
11
 
@@ -14,6 +15,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
14
15
  appCode={appCode}
15
16
  appName={appName}
16
17
  resourceDefinitions={resourceDefinitions}
18
+ workflows={workflowCodes}
17
19
  />
18
20
  </React.StrictMode>,
19
21
  );
@@ -70,3 +70,30 @@ test('runtime metadata stays mount-scoped', () => {
70
70
  );
71
71
  assert.equal(resolveApplicationBasename(mount), '/view/example-app');
72
72
  });
73
+
74
+ test('resource E2E harness owns the standard export download response', () => {
75
+ const source = readFileSync(
76
+ new URL('../e2e/resources.spec.ts', import.meta.url),
77
+ 'utf8'
78
+ );
79
+ assert.match(source, /\/native\\\/data\\\/\(\[\^\/\]\+\)\\\/export\$/);
80
+ assert.match(source, /content-disposition/);
81
+ assert.match(source, /text\/csv/);
82
+ });
83
+
84
+ test('published E2E harnesses do not write evidence into the platform source repository', () => {
85
+ const workflowSource = readFileSync(
86
+ new URL('../e2e/workflow.spec.ts', import.meta.url),
87
+ 'utf8'
88
+ );
89
+ assert.doesNotMatch(workflowSource, /docs\/architecture|evidenceDirectory/);
90
+ });
91
+
92
+ test('the platform package owns Web build verification policy', () => {
93
+ const verifier = readFileSync(
94
+ new URL('../scripts/verify-build.mjs', import.meta.url),
95
+ 'utf8'
96
+ );
97
+ assert.match(verifier, /verifyOpenXiangdaWebBuild/);
98
+ assert.doesNotMatch(verifier, /2_650_000|800_000|600_000/);
99
+ });
@@ -22,7 +22,7 @@
22
22
  "build": "pnpm --recursive build"
23
23
  },
24
24
  "devDependencies": {
25
- "openxiangda": "2.0.0-alpha.3",
25
+ "openxiangda": "2.0.0-alpha.8",
26
26
  "typescript": "5.9.3"
27
27
  }
28
28
  }