openxiangda-cli 2.0.0-alpha.126 → 2.0.0-alpha.130

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/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # openxiangda-cli
2
2
 
3
- OpenXiangda 2.0 installs one developer CLI with eight application lifecycle commands—`create`, `dev`, `check`, `deploy`, `status`, `logs`, `rollback`, and `login`—plus `skill install` for installing the exact AI guidance bundled with the same release.
3
+ OpenXiangda 2.0 installs one developer CLI for application creation, connected development, checks, deployment status and logs, explicit retry/cancel, environment start/stop, rollback, login, AppSpec and Skill installation.
4
+
5
+ Deployment recovery is server-authoritative. `status` exposes the root and
6
+ latest failures, candidate identity/state, recovery decision and append-only
7
+ attempt ledger; `logs` preserves those fields. The CLI follows only the
8
+ published recovery command and never advertises cancel after activation.
4
9
 
5
10
  The CLI disables user, development and JIT plugin loading. A clean build removes orphaned command output before TypeScript compilation. `--json` emits one stable result envelope and never mixes progress logs into machine output.
6
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.126",
3
+ "version": "2.0.0-alpha.130",
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.62",
25
- "openxiangda-devkit-core": "2.0.0-alpha.80",
26
- "openxiangda-mcp": "2.0.0-alpha.80",
27
- "openxiangda-skill-kit": "2.0.0-alpha.101"
24
+ "openxiangda-contracts": "2.0.0-alpha.65",
25
+ "openxiangda-devkit-core": "2.0.0-alpha.84",
26
+ "openxiangda-mcp": "2.0.0-alpha.84",
27
+ "openxiangda-skill-kit": "2.0.0-alpha.105"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsx": "4.23.12",
@@ -14,6 +14,7 @@
14
14
  - Use the current logged-in user and the union of that user's application roles. An optional declared Perspective projects only read visibility (pages, rows and fields); create/update/delete, workflows and custom actions continue to authorize against the complete union. Standard Data API calls inherit `X-OpenXiangda-Perspective` automatically. Use `@CurrentPerspective()` only when custom Nest code reads outside the standard Data API, and apply an equivalent read projection explicitly. Business code must not persist a platform Token or authorization result and must not implement a second identity path.
15
15
  - Fields inherit the resource read/create/update capabilities. Use field `access` only to tighten them; arrays are all-of and `false` is explicit deny. There is no `write` fallback.
16
16
  - Field `type` is semantic, never a hand-authored database type. The supported catalog is exactly `text.short`, `text.long`, `text.rich`, `number.integer`, `number.decimal`, `boolean`, `date`, `time`, `datetime`, `date-range`, `datetime-range`, `option.single`, `option.multiple`, `cascade.single`, `cascade.multiple`, `user.single`, `user.multiple`, `department.single`, `department.multiple`, `resource-ref.single`, `resource-ref.multiple`, `file`, `image`, `signature`, `address`, `location`, `json`, `serial-number`, `uuid` and `subtable`. The compiler alone derives PostgreSQL columns, constraints and indexes.
17
+ - Every `date-range` and `datetime-range` field explicitly declares `rangeBoundary: 'closed' | 'half-open'`. Values remain `{ start, end }`; callers never send or override the boundary. Closed ranges allow `start <= end` and use PostgreSQL `[]`; half-open ranges require `start < end` and use `[)`.
17
18
  - Declare every custom operation capability in `authz.capabilities` with `kind: 'backend'`, then reference that same code from the operation and its allowed roles. Generated resource CRUD capabilities do not go in this catalog.
18
19
  - Build App Operation request/response schemas with `resourceRecordSchema`, `schemaRef`, `composeJsonSchema` and `composeAppOperationSchemas` from `openxiangda/config`. They project the same Resource declaration and canonical field value protocols; never import a physical toolchain package or repeat user, department, resource-reference or managed-file schemas by hand.
19
20
  - Visitor duplicate protection uses `createVisitorReservation({ duplicateMatch: { fieldCode: submittedValue }, ... })`. `duplicateMatch` is a non-empty value map, never a field-name array, and no mutable pre-read is allowed.
@@ -21,6 +22,6 @@
21
22
  - `option.*`, `user.*`, `department.*` and `resource-ref.*` values never collapse to scalar IDs. Single values store one labeled object and multiple values store object arrays. `resource-ref.*` JSON is not a foreign key, trusted target snapshot or automatically refreshed copy; current target state is read by `resourceCode` plus `value`. A resource source `labelField` must be `text.short` or `text.long`; `serial-number` is allowed in source search, description and snapshot fields, but not as the label. `location` accepts only exact WGS84 coordinates captured by DingTalk or browser geolocation; it has no manual input or `manual` source. Roles that consume directory-backed fields explicitly include `app:<app-code>:directory:read`.
22
23
  - Derive role grants with `resourceCapabilityCodes(appCode, resourceCode)`. Declare current-user rows only with `currentUserDataPolicy(...)`; do not invent operators, values or alternate current-user spellings.
23
24
  - Do not add compatibility aliases, migration branches or silent fallbacks for an earlier 2.0 alpha contract. Replace an incorrect contract and regenerate the application.
24
- - Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through the canonical `openxiangda.config.ts` declarations and generated clients; declare each Workflow launch as `standalone`, `custom-page`, `hidden-handoff` or `work-center-only`, put only self-contained standalone launch pages in navigation, and bind durable save handlers with `defineWorkflowLaunchContributions` without repeating generated titles. Desktop and mobile standard launch pages load the same canonical Workflow launch Surface. To replace Workflow detail, declare both desktop and mobile `detailRouteCode` values whose routes contain exactly `:instanceId`; Work Center, the application todo center and notification channels consume the platform-resolved paths, while every Surface and command remains server-authorized. Add the standard current-user todo page only with `adminApplicationTodoCenterPage()`; never query Notification Hub management APIs from a user page. Never depend on temporary router location state or copy 1.x workflow/message code, tables, APIs, templates or callbacks into this workspace. Keep ordinary CRUD and application-specific state machines independent of them.
25
+ - Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through canonical `openxiangda.config.ts` declarations and generated clients. `standalone`/`hidden-handoff` use the compiler-owned process operation and Durable Business Process Command; never add a browser save callback or call Workflow prepare/start directly. Custom pages launch only through a verified Named Action using `OpenXiangdaBusinessProcessService`. PC and mobile render the same `ProcessCommandSurface` independently and recover only by `commandId`. To replace Workflow detail, declare both desktop and mobile `detailRouteCode` values whose routes contain exactly `:instanceId`. Add the current-user todo page only with `adminApplicationTodoCenterPage()`; never query Notification Hub management APIs from a user page.
25
26
  - Run `pnpm openxiangda check --json` after contract changes and read `data.sealedArtifact`; check never seals, and an older `.openxiangda/build/app-package.json` is not the current check result. Use `pnpm openxiangda accept --plan <file>` only for optional real preproduction identity acceptance; it never blocks delivery. Deploy with `pnpm openxiangda deploy`, inspect with `pnpm openxiangda status` and `pnpm openxiangda logs`, and use the platform rollback command rather than mutating K3s directly.
26
27
  - AI-native clients start the workspace protocol through the same pinned binary: `pnpm exec openxiangda --mcp-stdio --cwd <workspace>`. Read `openxiangda://workspace/contracts` or call `contract_describe`; for first-time menu authoring, copy `data.adminNavigationAuthoring.suggestion.expression` once into `frontend.admin.navigation` with its listed `openxiangda/config` imports, then edit that application-owned declaration. Never treat the proposal as runtime discovery. When AppSpec is enabled, also read `openxiangda://workspace/appspec` or call `appspec_context`. Require `aiCatalog` and `aiCatalogDigest` to match the normal compiler output. This is a stdio transport mode, not another application-owned server. Never write an application MCP server, Catalog file, preview store or second authorization path.
@@ -26,6 +26,13 @@ BrowserRouter、RuntimeBoundary、Refine、generated routes 和后台 Shell
26
26
  在显式后台导航组中加入 `adminApplicationTodoCenterPage()`;平台生成
27
27
  `/admin/todos` 与 `/m/todos`,只读取当前用户的消息/待办投影并统一解析目标页面。
28
28
 
29
+ 自定义后台页使用 `OpenXiangdaAdminPage` 作为页面根;Ant Design 组件自动继承平台
30
+ `ConfigProvider`,图表等 TypeScript 渲染值读取 `useOpenXiangdaTheme()`,自定义 CSS
31
+ 只使用公开的 `--oxa-color-*`、`--oxa-shadow-surface` 和
32
+ `--oxa-radius-surface` 语义变量。不要为后台页新建主题 Provider,也不要把白色、黑色、
33
+ 浅灰色写死为页面、卡片、边框或文字颜色。品牌色只能用于品牌和有明确语义的数据分类,
34
+ 不能替代结构色。
35
+
29
36
  登录声明位于 `frontend.authentication`,只允许现有平台用户并拒绝注册。桌面 `/login`
30
37
  与移动 `/m/login` 是独立生成的 `authenticationSurfaces`,不计入受保护的
31
38
  `appRoutes`。用 `defineApplicationContributions({ routes: appRoutes,
@@ -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.40",
18
+ "openxiangda": "2.0.0-alpha.45",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -11,27 +11,7 @@ import {
11
11
 
12
12
  @Module({
13
13
  imports: [
14
- OpenXiangdaModule.forRoot({
15
- appCode: process.env.OPENXIANGDA_APP_CODE || 'openxiangda-application',
16
- platformBaseUrl:
17
- process.env.OPENXIANGDA_PLATFORM_BASE_URL ||
18
- 'http://127.0.0.1:7001/service',
19
- environmentKey:
20
- process.env.OPENXIANGDA_ENVIRONMENT_KEY || 'preproduction',
21
- environmentId:
22
- process.env.OPENXIANGDA_ENVIRONMENT_ID || 'connected-development',
23
- appVersionId:
24
- process.env.OPENXIANGDA_APP_VERSION_ID || 'connected-development',
25
- deploymentRunId:
26
- process.env.OPENXIANGDA_DEPLOYMENT_RUN_ID || 'connected-development',
27
- environmentHeadRevision: Number(
28
- process.env.OPENXIANGDA_ENVIRONMENT_HEAD_REVISION || 1
29
- ),
30
- backendRevisionId:
31
- process.env.OPENXIANGDA_BACKEND_REVISION_ID || 'connected-development',
32
- version: process.env.OPENXIANGDA_APP_VERSION || '0.1.0-dev',
33
- connectedDevelopment:
34
- process.env.OPENXIANGDA_CONNECTED_DEV === 'true',
14
+ OpenXiangdaModule.forApplication({
35
15
  eventHandlerManifest,
36
16
  eventSchemas,
37
17
  eventSigningSecrets: eventSigningSecretsFromEnvironment(
@@ -5,7 +5,8 @@ import test from 'node:test';
5
5
  test('the clean server template only owns platform bootstrap', () => {
6
6
  const source = readFileSync(new URL('../src/app.module.ts', import.meta.url), 'utf8');
7
7
  const main = readFileSync(new URL('../src/main.ts', import.meta.url), 'utf8');
8
- assert.match(source, /OpenXiangdaModule\.forRoot/);
8
+ assert.match(source, /OpenXiangdaModule\.forApplication/);
9
+ assert.doesNotMatch(source, /OPENXIANGDA_(?:APP_CODE|ENVIRONMENT_ID|OAUTH_CLIENT_ID)/);
9
10
  assert.match(source, /eventHandlerManifest/);
10
11
  assert.match(source, /eventSigningSecretsFromEnvironment/);
11
12
  assert.match(main, /bootstrapOpenXiangdaApplication\(AppModule\)/);
@@ -42,6 +42,9 @@ const field = (
42
42
  readCapabilities: [],
43
43
  createCapabilities: [],
44
44
  updateCapabilities: [],
45
+ ...(type === 'date-range' || type === 'datetime-range'
46
+ ? { rangeBoundary: 'closed' as const }
47
+ : {}),
45
48
  ...extra,
46
49
  } as unknown as SurfaceField);
47
50
 
@@ -3,10 +3,13 @@ import ReactDOM from 'react-dom/client';
3
3
  import { Button, Typography } from 'antd';
4
4
  import {
5
5
  defineApplicationContributions,
6
+ OpenXiangdaAdminPage,
6
7
  OpenXiangdaApplication,
8
+ useOpenXiangdaTheme,
7
9
  } from 'openxiangda/react';
8
10
  import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
9
11
  import 'openxiangda/react/styles.css';
12
+ import './theme-contract-fixture.css';
10
13
 
11
14
  const field = (
12
15
  label: string,
@@ -177,7 +180,20 @@ const resourceDefinitions = Object.fromEntries(
177
180
  );
178
181
 
179
182
  function ContentOperationsPage() {
180
- return <Typography.Title level={2}>内容业务操作</Typography.Title>;
183
+ const { effectiveAppearance, tokens } = useOpenXiangdaTheme();
184
+ return (
185
+ <OpenXiangdaAdminPage
186
+ className="theme-contract-page"
187
+ data-primary={tokens.primary}
188
+ data-theme={effectiveAppearance}
189
+ data-testid="custom-admin-page"
190
+ >
191
+ <Typography.Title level={2}>内容业务操作</Typography.Title>
192
+ <div className="theme-contract-panel">
193
+ 自定义后台页语义主题面板
194
+ </div>
195
+ </OpenXiangdaAdminPage>
196
+ );
181
197
  }
182
198
 
183
199
  function ResourcePortalPage() {
@@ -71,7 +71,12 @@ function runtimeAuthorization(authorized: boolean, superAdmin = false) {
71
71
  type: 'user_union',
72
72
  userId: 'acceptance-user',
73
73
  roleCodes: ['college_admin', 'instrument_admin'],
74
- capabilityCodes: authorized ? allCapabilities : [],
74
+ capabilityCodes: authorized
75
+ ? [
76
+ ...allCapabilities,
77
+ 'app:openxiangda-application:operations:read',
78
+ ]
79
+ : [],
75
80
  isAppSuperAdmin: superAdmin,
76
81
  identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
77
82
  },
@@ -281,6 +286,32 @@ test('renders the compiled desktop resource contract and shared workbench', asyn
281
286
  await expect.poll(() => page.evaluate(() => localStorage.getItem('openxiangda.admin.appearance'))).toBe('dark');
282
287
  });
283
288
 
289
+ test('custom admin pages inherit tokens and follow live system appearance', async ({ page }) => {
290
+ await page.emulateMedia({ colorScheme: 'dark' });
291
+ await mockPlatform(page);
292
+ await page.goto('/resource-experience.e2e.html');
293
+ await page
294
+ .locator('.oxa-sider')
295
+ .getByText('内容业务操作', { exact: true })
296
+ .click();
297
+
298
+ const customPage = page.getByTestId('custom-admin-page');
299
+ const customPanel = customPage.locator('.theme-contract-panel');
300
+ await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'dark');
301
+ await expect(customPage).toHaveAttribute('data-theme', 'dark');
302
+ await expect(customPage).toHaveAttribute('data-primary', '#447bdc');
303
+ await expect(customPage).toHaveCSS('background-color', 'rgb(11, 21, 36)');
304
+ await expect(customPanel).toHaveCSS('background-color', 'rgb(17, 29, 46)');
305
+ await expect(customPanel).toHaveCSS('color', 'rgb(215, 224, 236)');
306
+ await expect(customPanel).toHaveCSS('border-color', 'rgb(38, 55, 79)');
307
+
308
+ await page.emulateMedia({ colorScheme: 'light' });
309
+ await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'light');
310
+ await expect(customPage).toHaveAttribute('data-theme', 'light');
311
+ await expect(customPage).toHaveCSS('background-color', 'rgb(245, 245, 245)');
312
+ await expect(customPanel).toHaveCSS('background-color', 'rgb(255, 255, 255)');
313
+ });
314
+
284
315
  test('opens the stable admin entry under the preproduction gateway mount', async ({ page }) => {
285
316
  test.skip(!codes.length, 'No resource route exists in an empty application.');
286
317
  const devRuntimeBase = `/dev/${appCode}`;
@@ -0,0 +1,15 @@
1
+ .theme-contract-page {
2
+ display: grid;
3
+ align-content: start;
4
+ gap: 16px;
5
+ padding: 24px;
6
+ }
7
+
8
+ .theme-contract-panel {
9
+ border: 1px solid var(--oxa-color-border);
10
+ border-radius: var(--oxa-radius-surface);
11
+ background: var(--oxa-color-surface);
12
+ padding: 24px;
13
+ color: var(--oxa-color-text);
14
+ box-shadow: var(--oxa-shadow-surface);
15
+ }
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import ReactDOM from 'react-dom/client';
3
3
  import {
4
4
  defineApplicationContributions,
5
- defineWorkflowLaunchContributions,
6
5
  OpenXiangdaApplication,
7
6
  type AdminNavigationInput,
8
7
  type AdminPagesInput,
@@ -16,9 +15,44 @@ const workflowDefinitions = [
16
15
  code: 'purchase-approval',
17
16
  title: '采购审批',
18
17
  launch: { mode: 'standalone' },
18
+ subject: {
19
+ resourceCode: 'purchase-orders',
20
+ factProjection: { amount: 'amount' },
21
+ },
22
+ processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
19
23
  },
20
24
  ] as const;
21
25
 
26
+ const resourceDefinitions = {
27
+ 'purchase-orders': {
28
+ code: 'purchase-orders',
29
+ name: '采购申请',
30
+ capabilities: {
31
+ read: `app:${appCode}:data:purchase-orders:read`,
32
+ create: `app:${appCode}:data:purchase-orders:create`,
33
+ update: `app:${appCode}:data:purchase-orders:update`,
34
+ delete: `app:${appCode}:data:purchase-orders:delete`,
35
+ },
36
+ surface: {
37
+ mutationOwner: 'native',
38
+ generated: { list: true, detail: true, create: true, update: true, delete: true },
39
+ fields: {
40
+ amount: {
41
+ label: '申请金额',
42
+ type: 'number.decimal',
43
+ widget: 'money',
44
+ requiredHint: true,
45
+ readCapabilities: [`app:${appCode}:data:purchase-orders:read`],
46
+ createCapabilities: [`app:${appCode}:data:purchase-orders:create`],
47
+ updateCapabilities: [`app:${appCode}:data:purchase-orders:update`],
48
+ },
49
+ },
50
+ form: { layout: 'flat', fieldOrder: ['amount'] },
51
+ mobile: { enabled: true },
52
+ },
53
+ },
54
+ } as const;
55
+
22
56
  const detailRoutes = {
23
57
  desktop: {
24
58
  code: 'purchase-detail',
@@ -127,18 +161,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
127
161
  adminNavigation={adminNavigation}
128
162
  adminPages={adminPages}
129
163
  contributions={applicationContributions}
130
- resourceDefinitions={{}}
131
- workflows={defineWorkflowLaunchContributions(workflowDefinitions, {
132
- 'purchase-approval': async () => ({
133
- businessKey: 'PO-2026-0825',
134
- dataRef: {
135
- resourceCode: 'purchase-orders',
136
- recordId: 'purchase-1',
137
- dataRevision: '8',
138
- },
139
- facts: { amount: 28600, applicant: '张三' },
140
- }),
141
- })}
164
+ resourceDefinitions={resourceDefinitions}
165
+ workflows={workflowDefinitions}
142
166
  />
143
167
  </React.StrictMode>
144
168
  );
@@ -3,6 +3,7 @@ import { appCode } from '../../../packages/contracts/src/generated.js';
3
3
 
4
4
  const taskId = '11111111-1111-4111-8111-111111111111';
5
5
  const instanceId = '22222222-2222-4222-8222-222222222222';
6
+ const commandId = '55555555-5555-4555-8555-555555555555';
6
7
  function envelope(data: unknown, code = 200) {
7
8
  return { code, message: code === 200 ? 'success' : 'forbidden', data };
8
9
  }
@@ -53,6 +54,8 @@ function surface(completed = false, customDetail = false) {
53
54
  protocolVersion: 'workflow_surface_v2',
54
55
  surfaceRevision: completed ? 'revision-2' : 'revision-1',
55
56
  engineVersion: '2.0',
57
+ commandToken: 'A'.repeat(43),
58
+ commandTokenExpiresAt: '2099-08-29T09:05:00.000Z',
56
59
  instanceSequence: completed ? 4 : 3,
57
60
  detailNavigation: detailNavigation(customDetail),
58
61
  navigationTarget: {
@@ -212,6 +215,11 @@ function launchSurface() {
212
215
  workflowCode: 'purchase-approval',
213
216
  title: '采购审批',
214
217
  launchMode: 'standalone',
218
+ processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
219
+ subject: {
220
+ resourceCode: 'purchase-orders',
221
+ factProjection: { amount: 'amount' },
222
+ },
215
223
  inputSchema: { type: 'object', properties: {} },
216
224
  head: {
217
225
  workflowRevision: 4,
@@ -224,18 +232,85 @@ function launchSurface() {
224
232
  desktop: '/admin/workflows/purchase-approval/new',
225
233
  mobile: '/m/workflows/purchase-approval/start',
226
234
  },
227
- prepare: {
228
- method: 'POST',
229
- href: '/workflow/definitions/purchase-approval/prepare-start',
230
- },
231
- start: {
235
+ commit: {
232
236
  method: 'POST',
233
- href: '/workflow/instances',
237
+ href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
234
238
  idempotencyRequired: true,
235
239
  },
236
240
  };
237
241
  }
238
242
 
243
+ function processCommand(status: 'accepted' | 'started' = 'started') {
244
+ return {
245
+ schemaVersion: 'openxiangda.business-process.command/v2',
246
+ id: commandId,
247
+ appCode,
248
+ environmentKey: 'preproduction',
249
+ operationCode: 'openxiangda.workflow.purchase-approval.submit',
250
+ idempotencyKey: 'process:purchase-approval:test',
251
+ workflowCode: 'purchase-approval',
252
+ status,
253
+ revision: status === 'started' ? 4 : 1,
254
+ subject: {
255
+ resourceCode: 'purchase-orders',
256
+ id: '66666666-6666-4666-8666-666666666666',
257
+ dataRevision: 1,
258
+ factDigest: 'a'.repeat(64),
259
+ },
260
+ definitionVersion: 1,
261
+ bindingVersion: 1,
262
+ requirements: [],
263
+ answers: {},
264
+ preview: {},
265
+ workflowInstanceId: status === 'started' ? instanceId : null,
266
+ attemptCount: 1,
267
+ lastError: null,
268
+ replayed: false,
269
+ createdAt: '2026-08-29T01:00:00.000Z',
270
+ updatedAt: '2026-08-29T01:00:01.000Z',
271
+ };
272
+ }
273
+
274
+ function processCommandSurface() {
275
+ const command = processCommand('started');
276
+ const base = `/openxiangda-api/v2/applications/${appCode}/business-process/commands/${commandId}`;
277
+ return {
278
+ schemaVersion: 'openxiangda.process-command-surface/v2',
279
+ surfaceRevision: 'b'.repeat(64),
280
+ command,
281
+ subject: {
282
+ resourceCode: 'purchase-orders',
283
+ recordId: command.subject.id,
284
+ dataRevision: 1,
285
+ form: {
286
+ kind: 'native-resource-form',
287
+ resourceCode: 'purchase-orders',
288
+ recordId: command.subject.id,
289
+ },
290
+ },
291
+ requirements: [],
292
+ resume: {
293
+ status: { method: 'GET', href: base },
294
+ surface: { method: 'GET', href: `${base}/surface` },
295
+ answer: null,
296
+ retry: null,
297
+ navigationTarget: {
298
+ kind: 'PLATFORM_ROUTE',
299
+ routeCode: 'resource.record',
300
+ appCode,
301
+ pathParams: {
302
+ resourceCode: 'purchase-orders',
303
+ recordId: command.subject.id,
304
+ },
305
+ query: { processCommandId: commandId },
306
+ access: 'AUTHENTICATED',
307
+ },
308
+ desktop: `/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
309
+ mobile: `/m/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
310
+ },
311
+ };
312
+ }
313
+
239
314
  function todoPage() {
240
315
  return {
241
316
  schemaVersion: 'openxiangda.application-todo-center/v2',
@@ -283,6 +358,12 @@ async function mockWorkflow(
283
358
  await page.route('**/service/**', async route => {
284
359
  const url = new URL(route.request().url());
285
360
  const path = url.pathname;
361
+ if (path.endsWith('/auth/surface')) {
362
+ return route.fulfill({
363
+ contentType: 'application/json',
364
+ body: JSON.stringify(envelope({ csrfToken: 'workflow-csrf-1' })),
365
+ });
366
+ }
286
367
  if (path.endsWith('/native/authz/current')) {
287
368
  return route.fulfill({
288
369
  contentType: 'application/json',
@@ -319,7 +400,7 @@ async function mockWorkflow(
319
400
  userId: 'acceptance-user',
320
401
  roleCodes: ['department_manager'],
321
402
  capabilityCodes: [],
322
- isAppSuperAdmin: false,
403
+ isAppSuperAdmin: true,
323
404
  identityScope:
324
405
  'user-union:preproduction-id:acceptance-user:authz-1',
325
406
  },
@@ -369,40 +450,36 @@ async function mockWorkflow(
369
450
  });
370
451
  }
371
452
  if (
372
- path.endsWith(
373
- '/workflow/definitions/purchase-approval/prepare-start'
374
- )
453
+ path.endsWith('/business-process/standard-commands') &&
454
+ route.request().method() === 'POST'
375
455
  ) {
456
+ const body = route.request().postDataJSON();
457
+ expect(body).toMatchObject({
458
+ schemaVersion: 'openxiangda.standard-process.commit/v2',
459
+ environmentKey: 'preproduction',
460
+ workflowCode: 'purchase-approval',
461
+ mutation: { kind: 'create', data: { amount: '28600' } },
462
+ });
463
+ for (const forbidden of [
464
+ 'facts',
465
+ 'dataRef',
466
+ 'preparationToken',
467
+ 'processOperationCode',
468
+ ]) {
469
+ expect(body).not.toHaveProperty(forbidden);
470
+ }
376
471
  return route.fulfill({
377
472
  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
- ),
473
+ body: JSON.stringify(envelope(processCommand('accepted'))),
392
474
  });
393
475
  }
394
476
  if (
395
- path.endsWith('/workflow/instances') &&
396
- route.request().method() === 'POST'
477
+ path.endsWith(`/business-process/commands/${commandId}/surface`) &&
478
+ route.request().method() === 'GET'
397
479
  ) {
398
480
  return route.fulfill({
399
481
  contentType: 'application/json',
400
- body: JSON.stringify(
401
- envelope({
402
- ...surface(false, options.customDetail).instance,
403
- id: instanceId,
404
- })
405
- ),
482
+ body: JSON.stringify(envelope(processCommandSurface())),
406
483
  });
407
484
  }
408
485
  if (path.endsWith('/workflow/work-center/items')) {
@@ -445,6 +522,19 @@ async function mockWorkflow(
445
522
  });
446
523
  }
447
524
  if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
525
+ expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
526
+ 'workflow-csrf-1'
527
+ );
528
+ expect(route.request().postDataJSON()).toMatchObject({
529
+ commandToken: 'A'.repeat(43),
530
+ input: { comment: '同意采购' },
531
+ });
532
+ expect(route.request().postDataJSON()).not.toHaveProperty(
533
+ 'expectedTaskVersion'
534
+ );
535
+ expect(route.request().postDataJSON()).not.toHaveProperty(
536
+ 'expectedInstanceVersion'
537
+ );
448
538
  completed = true;
449
539
  return route.fulfill({
450
540
  contentType: 'application/json',
@@ -507,8 +597,7 @@ test('launches the same workflow from the canonical mobile Surface and follows c
507
597
  );
508
598
  await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
509
599
  await expect(page.getByRole('heading', { name: '采购审批' })).toBeVisible();
510
- await page.getByRole('button', { name: '保存业务数据并预检' }).click();
511
- await expect(page.getByRole('button', { name: '提交审批' })).toBeVisible();
600
+ await page.getByRole('spinbutton', { name: '请输入申请金额' }).fill('28600');
512
601
  await page.getByRole('button', { name: '提交审批' }).click();
513
602
  await expect(page.getByTestId('custom-workflow-detail')).toContainText(
514
603
  `${instanceId} / ${taskId}`
@@ -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.40",
17
+ "openxiangda": "2.0.0-alpha.45",
18
18
  "react": "19.2.8",
19
19
  "react-dom": "19.2.8",
20
20
  "react-router-dom": "7.8.2"
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import ReactDOM from 'react-dom/client';
3
3
  import {
4
4
  defineApplicationContributions,
5
- defineWorkflowLaunchContributions,
6
5
  DefaultDesktopApplicationLoginSurface,
7
6
  DefaultMobileApplicationLoginSurface,
8
7
  OpenXiangdaApplication,
@@ -43,11 +42,6 @@ const applicationContributions = defineApplicationContributions(
43
42
  },
44
43
  },
45
44
  );
46
- const workflowContributions = defineWorkflowLaunchContributions(
47
- workflowDefinitions,
48
- {},
49
- );
50
-
51
45
  ReactDOM.createRoot(document.getElementById('root')!).render(
52
46
  <React.StrictMode>
53
47
  <OpenXiangdaApplication
@@ -58,7 +52,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
58
52
  contributions={applicationContributions}
59
53
  perspectives={appPerspectives}
60
54
  resourceDefinitions={resourceDefinitions}
61
- workflows={workflowContributions}
55
+ workflows={workflowDefinitions}
62
56
  />
63
57
  </React.StrictMode>,
64
58
  );
@@ -23,7 +23,7 @@
23
23
  "build": "pnpm --recursive build"
24
24
  },
25
25
  "devDependencies": {
26
- "openxiangda": "2.0.0-alpha.40",
26
+ "openxiangda": "2.0.0-alpha.45",
27
27
  "typescript": "5.9.3"
28
28
  }
29
29
  }
@@ -1,5 +1,5 @@
1
1
  // Generated by OpenXiangda 2.0 Native compiler. Do not edit by hand.
2
- export const compilerContractVersion = "native-2" as const;
2
+ export const compilerContractVersion = "native-3" as const;
3
3
  export const appCode = "openxiangda-application" as const;
4
4
  export const appName = "OpenXiangda 应用" as const;
5
5
  export const appPerspectives = [] as const;