openxiangda-cli 2.0.0-alpha.86 → 2.0.0-alpha.90

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 (78) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +5 -5
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +14 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/server/test/smoke.test.ts +3 -1
  20. package/template/apps/web/e2e/client-fixture.ts +5 -0
  21. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  22. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  23. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  24. package/template/apps/web/e2e/resources.spec.ts +20 -181
  25. package/template/apps/web/field-protocol.e2e.html +3 -0
  26. package/template/apps/web/package.json +2 -5
  27. package/template/apps/web/src/main.tsx +12 -60
  28. package/template/apps/web/test/contracts.test.ts +52 -104
  29. package/template/openxiangda.config.ts +2 -2
  30. package/template/package.json +1 -3
  31. package/template/packages/contracts/src/generated.ts +27 -0
  32. package/template/scripts/verify-template-budget.mjs +2 -2
  33. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  34. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  35. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  36. package/template/apps/web/src/Shell.tsx +0 -445
  37. package/template/apps/web/src/appearance.tsx +0 -147
  38. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  39. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  40. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  41. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  42. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  43. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  44. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  45. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  46. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  47. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  48. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  49. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  50. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  51. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  52. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  53. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  54. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  55. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  56. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  57. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  58. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  59. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  60. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  61. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  62. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  63. package/template/apps/web/src/data-provider.ts +0 -72
  64. package/template/apps/web/src/platform-client.ts +0 -708
  65. package/template/apps/web/src/runtime-meta.ts +0 -75
  66. package/template/apps/web/src/runtime.tsx +0 -227
  67. package/template/apps/web/src/styles.css +0 -1941
  68. package/template/apps/web/test/address-field.test.ts +0 -66
  69. package/template/apps/web/test/cascade-value.test.ts +0 -68
  70. package/template/apps/web/test/directory-value.test.ts +0 -55
  71. package/template/apps/web/test/field-bounds.test.ts +0 -17
  72. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  73. package/template/apps/web/test/location-field.test.ts +0 -83
  74. package/template/apps/web/test/resource-import.test.ts +0 -143
  75. package/template/apps/web/test/resource-query.test.ts +0 -144
  76. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  77. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  78. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,15 +1,21 @@
1
- import { ConfigProvider, Form, Typography } from 'antd';
2
- import type { DataFieldSurface, DataFileRef } from 'openxiangda-contracts/browser';
1
+ import { Button, ConfigProvider, Form, Typography } from 'antd';
2
+ import type {
3
+ DataAuditEntry,
4
+ DataFieldSurface,
5
+ DataFileRef,
6
+ } from 'openxiangda/core';
3
7
  import React from 'react';
4
8
  import ReactDOM from 'react-dom/client';
5
9
  import {
10
+ auditFieldChange,
6
11
  MobileSurfaceFieldControl,
12
+ SurfaceAuditFieldValue,
7
13
  SurfaceFieldControl,
8
14
  SurfaceFieldValue,
9
15
  type SurfaceField,
10
16
  type SurfaceFieldRenderers,
11
- } from '../src/components/resource/SurfaceFields';
12
- import '../src/styles.css';
17
+ } from 'openxiangda/field-kit';
18
+ import 'openxiangda/react/styles.css';
13
19
 
14
20
  const options = [
15
21
  { label: '启用', value: 'enabled' },
@@ -98,7 +104,7 @@ const fields = [
98
104
  field('业务签名', 'signature', 'signature'),
99
105
  field('地址', 'address', 'address'),
100
106
  field('精确定位', 'location', 'location'),
101
- field('UUID', 'uuid', 'readonly'),
107
+ field('UUID', 'uuid', 'readonly', { requiredHint: true }),
102
108
  field('JSON', 'json', 'json'),
103
109
  field('流水号', 'serial-number', 'readonly'),
104
110
  field('子表', 'subtable', 'subtable'),
@@ -117,17 +123,28 @@ const reference = {
117
123
  snapshot: { code: 'M-001' },
118
124
  };
119
125
  const fileRef: DataFileRef = {
126
+ schemaVersion: 'openxiangda.data-file-ref/v2',
120
127
  id: '11111111-1111-4111-8111-111111111111',
121
128
  name: '验收附件.pdf',
122
129
  size: 1280,
123
130
  contentType: 'application/pdf',
124
131
  };
132
+ const imageRef: DataFileRef = {
133
+ ...fileRef,
134
+ id: '22222222-2222-4222-8222-222222222222',
135
+ name: '验收图片.png',
136
+ contentType: 'image/png',
137
+ width: 640,
138
+ height: 480,
139
+ previewUrl: '/service/openxiangda-api/v2/applications/field-protocol-fixture/native/data/field-records/files/22222222-2222-4222-8222-222222222222/content?disposition=inline',
140
+ thumbnailUrl: '/service/openxiangda-api/v2/applications/field-protocol-fixture/native/data/field-records/files/22222222-2222-4222-8222-222222222222/content?variant=thumbnail&disposition=inline',
141
+ };
125
142
  const values: Record<string, unknown> = {
126
143
  单行文本: '仪器验收记录',
127
144
  邮箱: 'user@example.com',
128
145
  电话: '13800000000',
129
146
  多行文本: '第一行\n第二行',
130
- 富文本: '<p>已保存的<strong>富文本</strong></p>',
147
+ 富文本: `<p>已保存的<strong>富文本</strong></p><img src="${imageRef.previewUrl}" alt="富文本图片">`,
131
148
  整数: -7,
132
149
  小数: 12.34,
133
150
  金额: 5600.5,
@@ -164,16 +181,7 @@ const values: Record<string, unknown> = {
164
181
  资源单选: reference,
165
182
  资源多选: [reference],
166
183
  附件: [fileRef],
167
- 图片: [
168
- {
169
- ...fileRef,
170
- id: '22222222-2222-4222-8222-222222222222',
171
- name: '验收图片.png',
172
- contentType: 'image/png',
173
- width: 640,
174
- height: 480,
175
- },
176
- ],
184
+ 图片: [imageRef],
177
185
  业务签名: {
178
186
  file: {
179
187
  ...fileRef,
@@ -201,15 +209,38 @@ const values: Record<string, unknown> = {
201
209
  accuracy: 5,
202
210
  capturedAt: '2026-08-24T02:20:30.000Z',
203
211
  },
204
- UUID: '44444444-4444-4444-8444-444444444444',
205
212
  JSON: { status: 'ok', nested: { count: 1 } },
206
213
  流水号: 'FP-001000',
207
214
  子表: [{ item_name: '第一行' }, { item_name: '第二行' }],
208
215
  };
209
216
 
217
+ const auditEntry: DataAuditEntry = {
218
+ id: 'field-protocol-event',
219
+ operation: 'created',
220
+ recordId: 'field-protocol-record',
221
+ revision: 1,
222
+ actor: { principalType: 'user', subjectId: 'user-1' },
223
+ correlation: {
224
+ eventId: 'field-protocol-event',
225
+ requestId: null,
226
+ traceId: null,
227
+ environmentKey: 'preproduction',
228
+ appVersionId: 'field-protocol-version',
229
+ environmentHeadRevision: 1,
230
+ capturePlanRevision: 1,
231
+ cause: { eventId: null, subscriptionCode: null, depth: 0 },
232
+ },
233
+ changes: Object.fromEntries(
234
+ fields.map(item => [item.key, { after: values[item.key] ?? null }])
235
+ ),
236
+ projection: {},
237
+ occurredAt: '2026-08-25T00:00:00.000Z',
238
+ };
239
+
210
240
  const renderers: SurfaceFieldRenderers = {
211
241
  signer: user,
212
242
  upload: async (_field, file) => ({
243
+ schemaVersion: 'openxiangda.data-file-ref/v2',
213
244
  id: crypto.randomUUID(),
214
245
  name: file.name,
215
246
  size: file.size,
@@ -239,12 +270,12 @@ function AcceptancePage() {
239
270
  </Typography.Text>
240
271
  </header>
241
272
  <section aria-label={mobile ? '移动编辑字段' : '桌面编辑字段'} className="field-protocol-edit">
242
- <Form initialValues={values} layout="vertical">
273
+ <Form initialValues={values} layout="vertical" onFinish={() => undefined}>
243
274
  <div className="field-protocol-grid">
244
275
  {fields.map(item => (
245
276
  <div data-field-code={item.key} key={item.key}>
246
277
  <Control
247
- disabled={false}
278
+ disabled={item.widget === 'readonly'}
248
279
  field={item}
249
280
  operation="create"
250
281
  renderers={renderers}
@@ -253,6 +284,7 @@ function AcceptancePage() {
253
284
  </div>
254
285
  ))}
255
286
  </div>
287
+ <Button htmlType="submit">验证表单</Button>
256
288
  </Form>
257
289
  </section>
258
290
  {!mobile && (
@@ -262,9 +294,44 @@ function AcceptancePage() {
262
294
  {fields.map(item => (
263
295
  <div data-read-field={item.key} key={item.key}>
264
296
  <strong>{item.label}</strong>
265
- <SurfaceFieldValue field={item} renderers={renderers} value={values[item.key]} />
297
+ <SurfaceFieldValue
298
+ field={item}
299
+ renderers={renderers}
300
+ resourceCode="field-records"
301
+ value={values[item.key]}
302
+ />
303
+ </div>
304
+ ))}
305
+ </div>
306
+ </section>
307
+ )}
308
+ {!mobile && (
309
+ <section aria-label="当前协议字段变更记录" className="field-protocol-readonly">
310
+ <Typography.Title level={3}>当前协议字段变更记录</Typography.Title>
311
+ <div className="field-protocol-read-grid">
312
+ {fields.map(item => (
313
+ <div data-audit-field={item.key} key={item.key}>
314
+ <strong>{item.label}</strong>
315
+ <SurfaceAuditFieldValue
316
+ field={item}
317
+ resourceCode="field-records"
318
+ value={auditFieldChange(auditEntry, item.key)?.after}
319
+ />
266
320
  </div>
267
321
  ))}
322
+ <div data-audit-digest="rich-text">
323
+ <strong>富文本摘要</strong>
324
+ <SurfaceAuditFieldValue
325
+ field={fields[4]}
326
+ value={{
327
+ kind: 'digest',
328
+ truncated: true,
329
+ valueType: 'text.rich',
330
+ bytes: 65_537,
331
+ sha256: 'a'.repeat(64),
332
+ }}
333
+ />
334
+ </div>
268
335
  </div>
269
336
  </section>
270
337
  )}
@@ -23,13 +23,32 @@ test.describe('Field Kit protocol acceptance', () => {
23
23
  },
24
24
  });
25
25
  });
26
+ await page.route('**/files/*/content?*', async route => {
27
+ await route.fulfill({
28
+ body: Buffer.from(
29
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
30
+ 'base64'
31
+ ),
32
+ contentType: 'image/png',
33
+ status: 200,
34
+ });
35
+ });
26
36
  });
27
37
 
28
38
  test('renders and operates every desktop edit/read component', async ({ page }) => {
39
+ const pageErrors: string[] = [];
40
+ const addressRequests: string[] = [];
41
+ page.on('pageerror', error => pageErrors.push(error.message));
42
+ page.on('request', request => {
43
+ if (request.url().includes('/china-divisions/')) {
44
+ addressRequests.push(request.url());
45
+ }
46
+ });
29
47
  await page.setViewportSize({ width: 1440, height: 1100 });
30
48
  await page.goto('/field-protocol.e2e.html');
31
49
  await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
32
50
  await expect(page.locator('[data-read-field]')).toHaveCount(fieldCount);
51
+ await expect(page.locator('[data-audit-field]')).toHaveCount(fieldCount);
33
52
  await expect(page.locator('[data-field-code="静态单选下拉"] .ant-select')).toBeVisible();
34
53
  await expect(page.locator('[data-field-code="单选按钮"] .ant-radio-group')).toBeVisible();
35
54
  await expect(page.locator('[data-field-code="复选框"] .ant-checkbox-group')).toBeVisible();
@@ -37,6 +56,22 @@ test.describe('Field Kit protocol acceptance', () => {
37
56
  await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
38
57
  await expect(page.locator('[data-field-code="精确定位"]')).not.toContainText('手工');
39
58
  await expect(page.locator('.ant-alert-error')).toHaveCount(0);
59
+ await expect.poll(() => addressRequests.length).toBeGreaterThan(0);
60
+ expect(addressRequests.every(url => new URL(url).pathname.startsWith('/service/china-divisions/'))).toBe(true);
61
+
62
+ await page.getByRole('button', { name: '验证表单' }).click();
63
+ await expect(page.locator('[data-field-code="UUID"] .ant-form-item-has-error')).toHaveCount(0);
64
+
65
+ await expect(page.locator('[data-read-field="图片"] img.oxa-file-thumbnail')).toHaveAttribute('src', /blob:/);
66
+ await expect(page.locator('[data-read-field="富文本"] img')).toHaveAttribute('src', /blob:/);
67
+ expect(await page.locator('[data-read-field="图片"] img.oxa-file-thumbnail').evaluate(
68
+ image => (image as HTMLImageElement).naturalWidth
69
+ )).toBeGreaterThan(0);
70
+ expect(await page.locator('[data-read-field="富文本"] img').evaluate(
71
+ image => (image as HTMLImageElement).naturalWidth
72
+ )).toBeGreaterThan(0);
73
+ await expect(page.locator('[aria-label="当前协议字段变更记录"]')).not.toContainText('[object Object]');
74
+ await expect(page.locator('[data-audit-digest="rich-text"]')).toContainText('大值摘要');
40
75
 
41
76
  await page.locator('[data-field-code="精确定位"]').getByRole('button', { name: '浏览器定位' }).click();
42
77
  await expect(page.locator('[data-field-code="精确定位"]')).toContainText('120.123456');
@@ -78,12 +113,63 @@ test.describe('Field Kit protocol acceptance', () => {
78
113
  await saveSignature.click();
79
114
  await expect(page.locator('[data-field-code="业务签名"]')).not.toContainText('尚未签名');
80
115
 
116
+ expect(pageErrors).toEqual([]);
117
+
81
118
  expect(
82
119
  await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
83
120
  ).toBe(true);
84
121
  await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-desktop.png' });
85
122
  });
86
123
 
124
+ test('resource selectors query once when selection changes', async ({ page }) => {
125
+ const requestCounts = new Map<string, number>();
126
+ await page.route('**/source/query', async route => {
127
+ const match = new URL(route.request().url()).pathname.match(/\/fields\/([^/]+)\/source\/query$/);
128
+ const fieldCode = decodeURIComponent(match?.[1] || 'unknown');
129
+ requestCounts.set(fieldCode, (requestCounts.get(fieldCode) || 0) + 1);
130
+ const body = route.request().postDataJSON() as { operation?: 'create' | 'update' };
131
+ await route.fulfill({
132
+ contentType: 'application/json',
133
+ json: {
134
+ schemaVersion: 'openxiangda.data-field-source-page/v2',
135
+ environment: {
136
+ id: 'field-protocol-environment',
137
+ key: 'preproduction',
138
+ activeAppVersionId: 'field-protocol-version',
139
+ headRevision: 1,
140
+ authzRevisionId: 'field-protocol-authz',
141
+ authzVersion: 1,
142
+ scopeDataVersion: 'field-protocol-scope',
143
+ },
144
+ resourceCode: 'field-records',
145
+ fieldCode,
146
+ sourceResourceCode: 'field-lookups',
147
+ operation: body.operation || 'create',
148
+ items: [{
149
+ label: `候选-${fieldCode}`,
150
+ value: `candidate-${fieldCode}`,
151
+ resourceCode: 'field-lookups',
152
+ snapshot: { code: `code-${fieldCode}` },
153
+ }],
154
+ nextCursor: null,
155
+ },
156
+ });
157
+ });
158
+
159
+ await page.goto('/field-protocol.e2e.html');
160
+ for (const fieldCode of ['资源单选', '资源多选']) {
161
+ await page.locator(`[data-field-code="${fieldCode}"]`).getByRole('combobox').click();
162
+ await expect.poll(() => requestCounts.get(fieldCode) || 0).toBe(1);
163
+ await page
164
+ .locator('.ant-select-dropdown:visible .oxa-selector-option')
165
+ .filter({ hasText: `候选-${fieldCode}` })
166
+ .click();
167
+ await page.waitForTimeout(500);
168
+ expect(requestCounts.get(fieldCode)).toBe(1);
169
+ await page.keyboard.press('Escape');
170
+ }
171
+ });
172
+
87
173
  test('renders every mobile control without horizontal overflow', async ({ page }) => {
88
174
  await page.setViewportSize({ width: 390, height: 844 });
89
175
  await page.goto('/field-protocol.e2e.html?mode=mobile');
@@ -21,119 +21,26 @@ const definitions = resourceDefinitions as Record<string, DeclaredResource>;
21
21
  const allCapabilities = [...capabilities] as string[];
22
22
  const runtimeBase = `/view/${appCode}`;
23
23
 
24
- const roleSubjects = [
25
- {
26
- subjectKey: 'membership:college-admin',
27
- subjectKind: 'membership',
28
- role: {
29
- code: 'college_admin',
30
- name: '学院管理员',
31
- source: 'package',
32
- description: '管理本学院业务数据',
33
- },
34
- revision: 1,
35
- scopeDimensionCount: 1,
36
- scopeSummary: [],
37
- scopeSummaryTruncated: false,
38
- validFrom: null,
39
- validTo: null,
40
- },
41
- {
42
- subjectKey: 'membership:instrument-admin',
43
- subjectKind: 'membership',
44
- role: {
45
- code: 'instrument_admin',
46
- name: '仪器管理员',
47
- source: 'package',
48
- description: '管理分配的仪器',
49
- },
50
- revision: 1,
51
- scopeDimensionCount: 0,
52
- scopeSummary: [],
53
- scopeSummaryTruncated: false,
54
- validFrom: null,
55
- validTo: null,
56
- },
57
- ] as const;
58
-
59
- function roleContext(subjectKey: string, authorized: boolean) {
60
- const subject = roleSubjects.find(item => item.subjectKey === subjectKey)!;
61
- const roleSessionId = `role-session-${subject.role.code}`;
24
+ function runtimeAuthorization(authorized: boolean) {
62
25
  return {
63
- schemaVersion: 'openxiangda.role-session-context/v2',
26
+ schemaVersion: 'openxiangda.runtime-authorization/v2',
64
27
  state: 'active',
65
28
  environment: {
66
29
  id: 'preproduction-id',
67
30
  key: 'preproduction',
68
- kind: 'preproduction',
69
31
  activeAppVersionId: 'app-version-1',
70
32
  headRevision: 1,
33
+ authzRevisionId: 'authz-1',
71
34
  authzVersion: 1,
72
- roleSubjectSetVersion: 'subjects-1',
73
- },
74
- identityScope: `preproduction-id:${subjectKey}:1`,
75
- subjectProfile: {
76
- schemaVersion: 'openxiangda.subject-profile/v2',
77
- userId: 'acceptance-user',
78
- displayName: '验收用户',
79
- avatarUrl: null,
80
- jobNumber: null,
81
- affiliatedDepartment: { id: 'department-1', name: '实验中心' },
82
- },
83
- currentRoleSubject: subject,
84
- roleSubjectPage: {
85
- schemaVersion: 'openxiangda.role-subject-page/v2',
86
- items: roleSubjects,
87
- total: roleSubjects.length,
88
- nextCursor: null,
89
- roleSubjectSetVersion: 'subjects-1',
90
- },
91
- roleSession: {
92
- schemaVersion: 'openxiangda.native-role-session/v2',
93
- id: roleSessionId,
94
- environmentId: 'preproduction-id',
95
- environmentKey: 'preproduction',
96
- loginSessionId: 'login-session-1',
97
- userId: 'acceptance-user',
98
- selectionKey: subjectKey,
99
- subjectKind: 'membership',
100
- activeRoleCode: subject.role.code,
101
- activeRoleSource: 'package',
102
- activeRoleMembershipId: subjectKey.slice('membership:'.length),
103
- activeAuthzRevisionId: 'authz-1',
104
- activeAuthzVersion: 1,
105
- activeAppVersionId: 'app-version-1',
106
- activeEnvironmentHeadRevision: 1,
107
- activeMembershipRevision: 1,
108
- status: 'active',
109
- issuedAt: '2026-08-23T00:00:00.000Z',
110
- expiresAt: '2099-08-23T00:00:00.000Z',
111
- lastUsedAt: '2026-08-23T00:00:00.000Z',
35
+ scopeDataVersion: 'scope-1',
112
36
  },
113
37
  principal: {
114
- schemaVersion: 'openxiangda.native-principal/v2',
115
- tenantId: 'tenant-1',
116
- appCode,
117
- environmentId: 'preproduction-id',
118
- environmentKey: 'preproduction',
119
- activeAppVersionId: 'app-version-1',
120
- environmentHeadRevision: 1,
121
- principalType: 'user',
122
- subjectId: 'acceptance-user',
38
+ type: 'user_union',
123
39
  userId: 'acceptance-user',
124
- loginSessionId: 'login-session-1',
125
- roleSessionId,
126
- subjectKind: 'membership',
127
- activeRoleCode: subject.role.code,
128
- activeRoleSource: 'package',
129
- activeRoleMembershipId: subjectKey.slice('membership:'.length),
130
- activeMembershipRevision: 1,
131
- authzRevisionId: 'authz-1',
132
- authzVersion: 1,
133
- scopeDataVersion: 'scope-1',
40
+ roleCodes: ['college_admin', 'instrument_admin'],
41
+ capabilityCodes: authorized ? allCapabilities : [],
134
42
  isAppSuperAdmin: false,
135
- capabilities: authorized ? allCapabilities : [],
136
- expiresAt: null,
43
+ identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
137
44
  },
138
45
  };
139
46
  }
@@ -143,10 +50,8 @@ async function mockPlatform(
143
50
  authorized = true,
144
51
  targetRuntimeBase = runtimeBase,
145
52
  ) {
146
- let activeSubjectKey = roleSubjects[0].subjectKey;
147
53
  let contextReads = 0;
148
54
  const observed: Array<{ path: string; roleSessionId: string }> = [];
149
- const switches: Array<Record<string, unknown>> = [];
150
55
  await page.route(`**${targetRuntimeBase}/**`, async route => {
151
56
  if (route.request().resourceType() !== 'document') return route.continue();
152
57
  const response = await route.fetch();
@@ -162,42 +67,13 @@ async function mockPlatform(
162
67
  const url = new URL(route.request().url());
163
68
  const request = route.request();
164
69
  const roleSessionId = request.headers()['x-openxiangda-role-session-id'] || '';
165
- if (url.pathname.endsWith('/native/authz/role-session/context/role-subjects')) {
166
- return route.fulfill({
167
- contentType: 'application/json',
168
- body: JSON.stringify({
169
- code: 200,
170
- data: {
171
- schemaVersion: 'openxiangda.role-subject-page/v2',
172
- items: roleSubjects,
173
- total: roleSubjects.length,
174
- nextCursor: null,
175
- roleSubjectSetVersion: 'subjects-1',
176
- },
177
- }),
178
- });
179
- }
180
- if (url.pathname.endsWith('/native/authz/role-session/context/switch')) {
181
- const body = request.postDataJSON() as Record<string, unknown>;
182
- switches.push(body);
183
- if (roleSubjects.some(item => item.subjectKey === body.targetRoleSubjectKey)) {
184
- activeSubjectKey = String(body.targetRoleSubjectKey) as typeof activeSubjectKey;
185
- }
186
- return route.fulfill({
187
- contentType: 'application/json',
188
- body: JSON.stringify({
189
- code: 200,
190
- data: roleContext(activeSubjectKey, authorized),
191
- }),
192
- });
193
- }
194
- if (url.pathname.endsWith('/native/authz/role-session/context')) {
70
+ if (url.pathname.endsWith('/native/authz/current')) {
195
71
  contextReads += 1;
196
72
  return route.fulfill({
197
73
  contentType: 'application/json',
198
74
  body: JSON.stringify({
199
75
  code: 200,
200
- data: roleContext(activeSubjectKey, authorized),
76
+ data: runtimeAuthorization(authorized),
201
77
  }),
202
78
  });
203
79
  }
@@ -262,7 +138,6 @@ async function mockPlatform(
262
138
  });
263
139
  return {
264
140
  observed,
265
- switches,
266
141
  get contextReads() {
267
142
  return contextReads;
268
143
  },
@@ -314,7 +189,7 @@ test('opens the stable admin entry under the preproduction gateway mount', async
314
189
  await expect(page).toHaveURL(`${devRuntimeBase}/${codes[0]}`);
315
190
  });
316
191
 
317
- test('binds Data, Directory, App API, and role switching to one active RoleSession', async ({ page }) => {
192
+ test('uses the current user role union without exposing a RoleSession header', async ({ page }) => {
318
193
  const platform = await mockPlatform(page);
319
194
  await page.goto(`${runtimeBase}/admin`);
320
195
  if (codes.length) {
@@ -324,7 +199,7 @@ test('binds Data, Directory, App API, and role switching to one active RoleSessi
324
199
  }
325
200
 
326
201
  await page.evaluate(async () => {
327
- const client = await import('/src/platform-client.ts');
202
+ const client = await import('/e2e/client-fixture.ts');
328
203
  await client.searchResource('acceptance-resource', 'name', {
329
204
  keyword: '显微镜',
330
205
  });
@@ -332,55 +207,19 @@ test('binds Data, Directory, App API, and role switching to one active RoleSessi
332
207
  await client.requestApplicationApi('/health/current-user');
333
208
  });
334
209
 
335
- const initialSession = 'role-session-college_admin';
336
- expect(
337
- platform.observed.filter(item =>
338
- /\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
339
- item.path
340
- )
210
+ const protectedRequests = platform.observed.filter(item =>
211
+ /\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
212
+ item.path
341
213
  )
342
- ).toEqual(
343
- expect.arrayContaining([
344
- expect.objectContaining({ roleSessionId: initialSession }),
345
- expect.objectContaining({ roleSessionId: initialSession }),
346
- expect.objectContaining({ roleSessionId: initialSession }),
347
- ])
348
214
  );
349
-
350
- await page.locator('.oxa-current-user').click();
351
- await page.getByText('切换应用角色', { exact: true }).click();
352
- await page.getByRole('combobox', { name: '选择应用角色' }).click();
353
- await page.getByText('仪器管理员', { exact: true }).click();
354
- await page.getByRole('button', { name: '切换角色' }).click();
355
-
356
- await expect.poll(() => platform.switches.length).toBe(1);
357
- expect(platform.switches[0]).toMatchObject({
358
- targetRoleSubjectKey: 'membership:instrument-admin',
359
- expectedRoleSessionId: initialSession,
360
- environmentKey: 'preproduction',
361
- });
362
- await expect.poll(() => platform.contextReads).toBeGreaterThan(1);
215
+ expect(protectedRequests.length).toBeGreaterThanOrEqual(3);
216
+ expect(protectedRequests.every(item => item.roleSessionId === '')).toBe(true);
217
+ expect(platform.contextReads).toBe(1);
363
218
  await page.locator('.oxa-current-user').click();
364
219
  await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
365
- '仪器管理员'
220
+ 'college_admin、instrument_admin'
366
221
  );
367
- await page.keyboard.press('Escape');
368
- await page.evaluate(async () => {
369
- const client = await import('/src/platform-client.ts');
370
- await client.searchResource('acceptance-resource', 'name', {
371
- keyword: '光谱仪',
372
- });
373
- });
374
- await expect
375
- .poll(
376
- () =>
377
- platform.observed.filter(
378
- item =>
379
- item.path.includes('/native/data-resources/') &&
380
- item.roleSessionId === 'role-session-instrument_admin'
381
- ).length
382
- )
383
- .toBeGreaterThan(0);
222
+ await expect(page.getByText('切换应用角色', { exact: true })).toHaveCount(0);
384
223
  });
385
224
 
386
225
  test('renders platform mobile controls from declared field widgets', async ({ page }) => {
@@ -3,6 +3,9 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
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" />
6
9
  <title>Field protocol acceptance</title>
7
10
  </head>
8
11
  <body>
@@ -14,13 +14,10 @@
14
14
  "@ant-design/icons": "6.2.3",
15
15
  "@refinedev/core": "5.0.12",
16
16
  "antd": "6.4.3",
17
- "dayjs": "1.11.18",
18
- "docx-preview": "0.3.7",
19
- "openxiangda-contracts": "2.0.0-alpha.40",
17
+ "openxiangda": "2.0.0-alpha.2",
20
18
  "react": "19.2.8",
21
19
  "react-dom": "19.2.8",
22
- "react-router-dom": "7.8.2",
23
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
20
+ "react-router-dom": "7.8.2"
24
21
  },
25
22
  "devDependencies": {
26
23
  "@playwright/test": "1.62.1",