openxiangda-cli 2.0.0-alpha.131 → 2.0.0-alpha.134

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.131",
3
+ "version": "2.0.0-alpha.134",
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.65",
25
- "openxiangda-devkit-core": "2.0.0-alpha.85",
26
- "openxiangda-mcp": "2.0.0-alpha.85",
27
- "openxiangda-skill-kit": "2.0.0-alpha.106"
24
+ "openxiangda-contracts": "2.0.0-alpha.67",
25
+ "openxiangda-devkit-core": "2.0.0-alpha.87",
26
+ "openxiangda-mcp": "2.0.0-alpha.87",
27
+ "openxiangda-skill-kit": "2.0.0-alpha.108"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsx": "4.23.12",
@@ -5,6 +5,7 @@
5
5
  - Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
6
6
  - Bind every generated `appRoutes` entry to its local page with `defineApplicationContributions`; desktop `admin` routes stay inside the platform Shell, while `user` routes render without an admin Shell for independent mobile/user experiences. Generated resource CRUD routes are compiler-owned under `/admin/resources/<resourceCode>...` and `/m/admin/resources/<resourceCode>...`; never recreate root resource paths, aliases or redirects. Explicit routes that have the same canonical shape as another explicit or generated route fail compilation, even when dynamic parameter names differ. Use only the typed `toolbar`, `row` and `detail` resource slots for generated resource actions. Declare the complete editable admin menu with `defineAdminNavigation` and its page/group helpers; the Shell renders only generated `adminNavigation` references and permissions only filter them. Do not create another router, menu store, layout, identity provider, permission store or copied CRUD page; route/action access uses capability or `allOf`/`anyOf`, while Data/App API and Workflow authorization remain server-owned.
7
7
  - Application login is optional `frontend.authentication`: existing platform users only, registration rejected, exact desktop `/login` and mobile `/m/login`. Bind generated `authenticationSurfaces` to separate PC/mobile renderers through `defineApplicationContributions`; renderers own only brand visuals and call `ApplicationLoginSurfaceProps`. The platform alone owns passwords, providers, OAuth state/callbacks, secure cookies, current identity and authorization. Never put login in protected `appRoutes`, call a v1 auth route, store tokens, create users/roles, or add another Router/identity provider. Keep QA in generated `platformAuthManifest`, outside the protected route denominator.
8
+ - External users without platform accounts use only an exact static `surface: 'user'` route declared through `frontend.publicAccess`. Declare the one resource, bounded field sets, required operations, draft limits and named duplicate validations; consume only generated `anonymousPublicAccess` and `createAnonymousPublicClient`. `own.list`/`own.read` mean records submitted by the same platform-issued HttpOnly browser credential, not a verified natural person, and another browser or cleared cookie intentionally loses access. Never create a guest role/user, call the general Native Data API, expose an anonymous upload path, store identity locally or derive ownership from IP, user-agent or fingerprint.
8
9
  - Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional `mutationOwner`, generated/list/layout/data-policy settings. Each field owns type, label, required state, Surface flags, reference/file metadata and access. Resource codes are lower kebab-case. Use `native` for direct Data API mutations, `action`, `readonly` or `workflow` for non-Native ownership; never grant or generate Native mutation for a non-Native owner.
9
10
  - Never write resource-level `schemaVersion`, `appCode`, `schema`, `surface`, `capabilities`, `fieldPolicies` or `platform/data` modules. The compiler derives the strict DataResource, CRUD capabilities, Surface and AI Schema. The application manifest still starts with its one top-level `schemaVersion: 3`.
10
11
  - Ordinary list/get/create/update/delete, filters, export and batch operations use the platform Native Data API. Do not create Function CRUD or NestJS wrappers.
@@ -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.46",
18
+ "openxiangda": "2.0.0-alpha.49",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -95,6 +95,14 @@ const fields: DataResourceSurface['fields'] = {
95
95
  }),
96
96
  };
97
97
 
98
+ const routeManifest = {
99
+ schemaVersion: 'openxiangda.application-route-manifest/v2',
100
+ appCode: 'openxiangda-application',
101
+ routes: [],
102
+ digest:
103
+ '88831760f6e672a77babbf039b35a6febca7852092f1579c2721c99596a265c4',
104
+ } as const;
105
+
98
106
  const surface: DataResourceSurface = {
99
107
  mutationOwner: 'native',
100
108
  generated: {
@@ -426,6 +434,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
426
434
  ]}
427
435
  contributions={applicationContributions}
428
436
  resourceDefinitions={resourceDefinitions}
437
+ routeManifest={routeManifest}
429
438
  />
430
439
  </React.StrictMode>
431
440
  );
@@ -128,6 +128,105 @@ const adminPages = [
128
128
  },
129
129
  ] as const satisfies AdminPagesInput;
130
130
 
131
+ const routeManifest = {
132
+ schemaVersion: 'openxiangda.application-route-manifest/v2',
133
+ appCode,
134
+ routes: [
135
+ {
136
+ code: 'application:todo-center',
137
+ kind: 'application-todo-center',
138
+ desktop: {
139
+ routeCode: 'application.todo-center.desktop',
140
+ path: '/admin/todos',
141
+ surface: 'admin',
142
+ pathParams: [],
143
+ requiresAuthentication: true,
144
+ },
145
+ mobile: {
146
+ routeCode: 'application.todo-center.mobile',
147
+ path: '/m/todos',
148
+ surface: 'user',
149
+ pathParams: [],
150
+ requiresAuthentication: true,
151
+ },
152
+ },
153
+ {
154
+ code: 'workflow:work-center',
155
+ kind: 'workflow-work-center',
156
+ desktop: {
157
+ routeCode: 'workflow.work.center.desktop',
158
+ path: '/admin/work-center',
159
+ surface: 'admin',
160
+ pathParams: [],
161
+ requiresAuthentication: true,
162
+ },
163
+ mobile: {
164
+ routeCode: 'workflow.work.center.mobile',
165
+ path: '/m/work-center',
166
+ surface: 'user',
167
+ pathParams: [],
168
+ requiresAuthentication: true,
169
+ },
170
+ },
171
+ {
172
+ code: 'workflow:task',
173
+ kind: 'workflow-task',
174
+ desktop: {
175
+ routeCode: 'workflow.task.desktop',
176
+ path: '/admin/tasks/:taskId',
177
+ surface: 'admin',
178
+ pathParams: ['taskId'],
179
+ requiresAuthentication: true,
180
+ },
181
+ mobile: {
182
+ routeCode: 'workflow.task.mobile',
183
+ path: '/m/tasks/:taskId',
184
+ surface: 'user',
185
+ pathParams: ['taskId'],
186
+ requiresAuthentication: true,
187
+ },
188
+ },
189
+ {
190
+ code: 'workflow:instance',
191
+ kind: 'workflow-instance',
192
+ desktop: {
193
+ routeCode: 'workflow.instance.desktop',
194
+ path: '/admin/workflows/:instanceId',
195
+ surface: 'admin',
196
+ pathParams: ['instanceId'],
197
+ requiresAuthentication: true,
198
+ },
199
+ mobile: {
200
+ routeCode: 'workflow.instance.mobile',
201
+ path: '/m/workflows/:instanceId',
202
+ surface: 'user',
203
+ pathParams: ['instanceId'],
204
+ requiresAuthentication: true,
205
+ },
206
+ },
207
+ {
208
+ code: 'workflow:purchase-approval:launch',
209
+ kind: 'workflow-launch',
210
+ workflowCode: 'purchase-approval',
211
+ desktop: {
212
+ routeCode: 'workflow.purchase-approval.launch.desktop',
213
+ path: '/admin/workflows/purchase-approval/new',
214
+ surface: 'admin',
215
+ pathParams: [],
216
+ requiresAuthentication: true,
217
+ },
218
+ mobile: {
219
+ routeCode: 'workflow.purchase-approval.launch.mobile',
220
+ path: '/m/workflows/:workflowCode/start',
221
+ surface: 'user',
222
+ pathParams: ['workflowCode'],
223
+ requiresAuthentication: true,
224
+ },
225
+ },
226
+ ],
227
+ digest: '34cc08a02cd187a07d9e2d2f99c1080b5b86f3a4512f9e0f43ff4cb05b7195fa',
228
+ } as const;
229
+
131
230
  const adminNavigation = [
132
231
  {
133
232
  code: 'workflow-center',
@@ -146,11 +245,32 @@ const adminNavigation = [
146
245
  },
147
246
  ] as const satisfies AdminNavigationInput;
148
247
 
149
- const initialPath =
150
- new URLSearchParams(window.location.search).get('initial') ||
151
- '/admin/work-center';
248
+ const fixtureParams = new URLSearchParams(window.location.search);
249
+ const initialPath = fixtureParams.get('initial') || '/admin/work-center';
250
+ const runtimeBase = fixtureParams.get('base')?.trim();
251
+ if (runtimeBase) {
252
+ for (const [name, content] of [
253
+ ['openxiangda-runtime-base', runtimeBase],
254
+ ['openxiangda-app-code', appCode],
255
+ ['openxiangda-environment', 'preproduction'],
256
+ ] as const) {
257
+ const meta =
258
+ document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`) ||
259
+ document.createElement('meta');
260
+ meta.name = name;
261
+ meta.content = content;
262
+ if (!meta.parentElement) document.head.appendChild(meta);
263
+ }
264
+ }
152
265
  if (location.pathname.endsWith('/workflow-experience.e2e.html')) {
153
- history.replaceState({}, '', initialPath);
266
+ const stateToken = fixtureParams.get('state');
267
+ history.replaceState(
268
+ stateToken
269
+ ? { usr: { routeNegotiationState: stateToken }, key: 'fixture-state' }
270
+ : {},
271
+ '',
272
+ initialPath,
273
+ );
154
274
  }
155
275
 
156
276
  ReactDOM.createRoot(document.getElementById('root')!).render(
@@ -160,6 +280,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
160
280
  appName="流程体验验收"
161
281
  adminNavigation={adminNavigation}
162
282
  adminPages={adminPages}
283
+ routeManifest={routeManifest}
163
284
  contributions={applicationContributions}
164
285
  resourceDefinitions={resourceDefinitions}
165
286
  workflows={workflowDefinitions}
@@ -22,6 +22,16 @@ function detailNavigation(custom = false) {
22
22
  };
23
23
  }
24
24
 
25
+ function workflowFixtureUrl(
26
+ initialPath: string,
27
+ options: { base?: string; state?: string } = {},
28
+ ) {
29
+ const params = new URLSearchParams({ initial: initialPath });
30
+ if (options.base) params.set('base', options.base);
31
+ if (options.state) params.set('state', options.state);
32
+ return `/workflow-experience.e2e.html?${params.toString()}`;
33
+ }
34
+
25
35
  function task(completed = false, customDetail = false) {
26
36
  return {
27
37
  schemaVersion: 'openxiangda.workflow-task/v2',
@@ -673,3 +683,108 @@ test('shows an explicit authorization error instead of an empty work center', as
673
683
  });
674
684
  await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
675
685
  });
686
+
687
+ test('negotiates direct standard routes after BrowserRouter listener setup', async ({
688
+ page,
689
+ }) => {
690
+ await mockWorkflow(page);
691
+ await page.setViewportSize({ width: 360, height: 844 });
692
+ await page.goto(
693
+ workflowFixtureUrl('/admin/todos?view=pending#unread', {
694
+ state: 'deep-link',
695
+ }),
696
+ );
697
+
698
+ // The initial desktop URL is replaced by the mobile pair and the rendered
699
+ // surface follows the Router update. The fixture is StrictMode-mounted.
700
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
701
+ await expect(page.locator('.oxa-todo-center-desktop')).toHaveCount(0);
702
+ await expect(page).toHaveURL(/\/m\/todos\?view=pending#unread$/);
703
+ const historyLength = await page.evaluate(() => window.history.length);
704
+ const negotiatedUrl = page.url();
705
+ await expect
706
+ .poll(() => page.url(), { timeout: 1_000 })
707
+ .toBe(negotiatedUrl);
708
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
709
+ routeNegotiationState: 'deep-link',
710
+ });
711
+
712
+ // 768px remains mobile; crossing to 769px switches the same manifest pair
713
+ // back to desktop without adding a browser-history entry.
714
+ await page.setViewportSize({ width: 768, height: 844 });
715
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
716
+ await page.setViewportSize({ width: 769, height: 844 });
717
+ await expect(page.locator('.oxa-todo-center-desktop')).toBeVisible();
718
+ await expect(page).toHaveURL(/\/admin\/todos\?view=pending#unread$/);
719
+ expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
720
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
721
+ routeNegotiationState: 'deep-link',
722
+ });
723
+ await page.goBack();
724
+ await expect(page).not.toHaveURL(/\/admin\/todos\?view=pending#unread$/);
725
+
726
+ // Direct task and instance URLs carry their dynamic parameter and deep-link
727
+ // context to the independent mobile renderer at the narrow width.
728
+ await page.setViewportSize({ width: 360, height: 844 });
729
+ await page.goto(
730
+ workflowFixtureUrl(`/admin/tasks/${taskId}?from=todo#approval`),
731
+ );
732
+ await expect(page.locator('.oxa-workflow-detail-mobile')).toBeVisible();
733
+ await expect(page).toHaveURL(
734
+ new RegExp(`/m/tasks/${taskId}\\?from=todo#approval$`),
735
+ );
736
+
737
+ await page.goto(
738
+ workflowFixtureUrl(`/admin/workflows/${instanceId}?from=todo#timeline`),
739
+ );
740
+ await expect(page.locator('.oxa-workflow-detail-mobile')).toBeVisible();
741
+ await expect(page).toHaveURL(
742
+ new RegExp(`/m/workflows/${instanceId}\\?from=todo#timeline$`),
743
+ );
744
+ });
745
+
746
+ test('negotiates launch pairs in both directions and preserves the mount basename', async ({
747
+ page,
748
+ }) => {
749
+ await mockWorkflow(page);
750
+ const base = '/runtime/route-negotiation';
751
+ await page.setViewportSize({ width: 360, height: 844 });
752
+ await page.goto(
753
+ workflowFixtureUrl(
754
+ `${base}/admin/workflows/purchase-approval/new?source=todo#launch`,
755
+ { base },
756
+ ),
757
+ );
758
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
759
+ await expect(page).toHaveURL(
760
+ new RegExp(
761
+ `${base}/m/workflows/purchase-approval/start\\?source=todo#launch$`,
762
+ ),
763
+ );
764
+
765
+ await page.setViewportSize({ width: 1_440, height: 900 });
766
+ await expect(page.locator('.oxa-workflow-submission-desktop')).toBeVisible();
767
+ await expect(page).toHaveURL(
768
+ new RegExp(
769
+ `${base}/admin/workflows/purchase-approval/new\\?source=todo#launch$`,
770
+ ),
771
+ );
772
+ });
773
+
774
+ test('does not negotiate non-standard application contribution routes', async ({
775
+ page,
776
+ }) => {
777
+ await mockWorkflow(page);
778
+ const base = '/runtime/route-negotiation';
779
+ await page.setViewportSize({ width: 360, height: 844 });
780
+ const customPath = `${base}/admin/operations/purchases/${instanceId}?taskId=${taskId}`;
781
+ await page.goto(workflowFixtureUrl(customPath, { base }));
782
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
783
+ `${instanceId} / ${taskId}`,
784
+ );
785
+ const customUrl = page.url();
786
+ await expect(page).toHaveURL(new RegExp(`${base}/admin/operations/`));
787
+ await expect
788
+ .poll(() => page.url(), { timeout: 1_000 })
789
+ .toBe(customUrl);
790
+ });
@@ -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.46",
17
+ "openxiangda": "2.0.0-alpha.49",
18
18
  "react": "19.2.8",
19
19
  "react-dom": "19.2.8",
20
20
  "react-router-dom": "7.8.2"
@@ -12,7 +12,9 @@ import {
12
12
  appName,
13
13
  appPerspectives,
14
14
  appRoutes,
15
+ routeManifest,
15
16
  authenticationSurfaces,
17
+ anonymousPublicAccess,
16
18
  adminNavigation,
17
19
  adminPages,
18
20
  resourceDefinitions,
@@ -49,7 +51,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
49
51
  appName={appName}
50
52
  adminNavigation={adminNavigation}
51
53
  adminPages={adminPages}
54
+ routeManifest={routeManifest}
52
55
  contributions={applicationContributions}
56
+ publicAccess={anonymousPublicAccess}
53
57
  perspectives={appPerspectives}
54
58
  resourceDefinitions={resourceDefinitions}
55
59
  workflows={workflowDefinitions}
@@ -35,6 +35,7 @@ test('clean template contains only application declarations and one React entry'
35
35
  assert.match(source, /DefaultDesktopApplicationLoginSurface/);
36
36
  assert.match(source, /DefaultMobileApplicationLoginSurface/);
37
37
  assert.match(source, /contributions=\{applicationContributions\}/);
38
+ assert.match(source, /publicAccess=\{anonymousPublicAccess\}/);
38
39
  assert.match(source, /from 'openxiangda\/react'/);
39
40
  assert.match(source, /openxiangda\/react\/styles\.css/);
40
41
  const legacyMarkers = [
@@ -23,7 +23,7 @@
23
23
  "build": "pnpm --recursive build"
24
24
  },
25
25
  "devDependencies": {
26
- "openxiangda": "2.0.0-alpha.46",
26
+ "openxiangda": "2.0.0-alpha.49",
27
27
  "typescript": "5.9.3"
28
28
  }
29
29
  }
@@ -84,6 +84,7 @@ export const applicationAuthentication = {
84
84
  }
85
85
  }
86
86
  } as const;
87
+ export const anonymousPublicAccess = undefined;
87
88
  export const platformAuthManifest = {
88
89
  "schemaVersion": "openxiangda.platform-auth-manifest/v2",
89
90
  "appCode": "openxiangda-application",
@@ -112,6 +113,12 @@ export const platformAuthManifest = {
112
113
  }
113
114
  ]
114
115
  } as const;
116
+ export const routeManifest = {
117
+ "schemaVersion": "openxiangda.application-route-manifest/v2",
118
+ "appCode": "openxiangda-application",
119
+ "routes": [],
120
+ "digest": "88831760f6e672a77babbf039b35a6febca7852092f1579c2721c99596a265c4"
121
+ } as const;
115
122
  export const adminPages = [] as const;
116
123
  export const adminNavigation = [] as const;
117
124
  export const eventTypes = [] as const;
@@ -137,6 +144,8 @@ export type AppRouteCode = AppRoute["code"];
137
144
  export type ApplicationAuthenticationMethod = (typeof authenticationMethods)[number];
138
145
  export type ApplicationAuthenticationSurface = (typeof authenticationSurfaces)[keyof typeof authenticationSurfaces];
139
146
  export type ApplicationAuthenticationSurfaceCode = ApplicationAuthenticationSurface["routeCode"];
147
+ export type AnonymousPublicAccess = typeof anonymousPublicAccess;
148
+ export type RouteManifest = typeof routeManifest;
140
149
  export type AdminPage = (typeof adminPages)[number];
141
150
  export type AdminNavigationGroup = (typeof adminNavigation)[number];
142
151
  export type AppPerspective = (typeof appPerspectives)[number];