openxiangda-cli 2.0.0-alpha.79 → 2.0.0-alpha.80

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.79",
3
+ "version": "2.0.0-alpha.80",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@oclif/core": "4.13.3",
26
- "openxiangda-contracts": "2.0.0-alpha.35",
27
- "openxiangda-devkit-core": "2.0.0-alpha.46",
28
- "openxiangda-mcp": "2.0.0-alpha.46"
26
+ "openxiangda-contracts": "2.0.0-alpha.36",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.47",
28
+ "openxiangda-mcp": "2.0.0-alpha.47"
29
29
  },
30
30
  "devDependencies": {
31
31
  "tsx": "4.23.12",
@@ -2,14 +2,16 @@
2
2
 
3
3
  - Use only the workspace-pinned CLI: `pnpm openxiangda <command>`. Never invoke a bare global `openxiangda` inside a 2.0 task.
4
4
  - Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
5
- - Declare each resource once in `openxiangda.config.ts`. Resource codes are lower kebab-case. The same declaration owns storage, Surface metadata, permissions and generated AI Schema.
5
+ - Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional 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.
6
+ - 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`.
6
7
  - 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.
7
8
  - Add NestJS only for a named business action that needs a cross-resource transaction, an invariant or an external side effect.
8
9
  - A NestJS backend declares only `enabled`, `isolation: 'shared' | 'dedicated'` and `resourceProfile: 'light' | 'standard'`. Never put raw Kubernetes resources, replicas, ports or environment maps in application metadata; the platform owns capacity and scaling.
9
10
  - Use the current logged-in user and the union of application roles. Do not select RoleSession, persist platform Token or implement a second authorization path.
10
- - Declare restricted field read, create and update permissions explicitly. There is no `write` fallback; generated update permission is denied unless explicitly granted.
11
+ - 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.
11
12
  - Desktop and mobile pages share values, validation and authorization, but use separate renderers. Members, departments, resources and attachments use platform-owned selectors and stable IDs.
12
13
  - A single directory user reference uses a `string` field; a single department or same-app resource reference uses `uuid`; every `multiple: true` reference uses `json`. Roles that consume directory-backed fields explicitly include `app:<app-code>:directory:read`.
14
+ - Derive role grants with `resourceCapabilityCodes(appCode, resourceCode)`. Declare current-user rows only with `currentUserDataPolicy(...)`; do not invent operators, values or alternate current-user spellings.
13
15
  - 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.
14
16
  - Run `pnpm openxiangda check` after contract changes. 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.
15
17
  - 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`, and require its `aiCatalog` and `aiCatalogDigest` to match the normal compiler output. This is a stdio transport mode, not a ninth CLI command. Never write an application MCP server, Catalog file, preview store or second authorization path.
@@ -8,11 +8,13 @@ pnpm openxiangda dev
8
8
 
9
9
  当前应用已经把 CLI 和全部 2.0 包精确锁定在 `package.json` 与 lockfile 中。创建另一个应用时使用当前安装的 OpenXiangda 2.0 Skill 给出的精确 CLI 版本,不使用 `latest`、`alpha` 或全局命令。
10
10
 
11
- 在 `platform/data` 中声明 `DataResource` 和字段即可生成标准后台列表、筛选、表单、详情、审计、文件上传和独立移动端页面。成员、部门和资源关系分别使用 `reference: { kind: 'directory-user' }`、`directory-department`、`resource`,只保存稳定 ID,不复制目录数据。单选成员字段使用 `string`,单选部门和资源引用使用 `uuid`,多选引用统一使用 `json`。需要目录选择器的业务角色必须显式拥有 `app:<app-code>:directory:read` 能力。
11
+ 在 `openxiangda.config.ts` 的 `data.resources` 中只声明资源 `code`、`name`、`fields` 和可选列表/布局/数据策略。每个字段一次写完 type、label、required、列表/筛选标记、reference/file access;不要再写 `schema`、`surface`、资源 capabilities、资源 fieldPolicies `platform/data` 模块。编译器据此生成标准后台列表、筛选、表单、详情、审计、文件上传、独立移动端页面和 AI Schema。
12
+
13
+ 成员、部门和资源关系分别使用 `reference: { kind: 'directory-user' }`、`directory-department`、`resource`,只保存稳定 ID,不复制目录数据。单选成员字段使用 `string`,单选部门和资源引用使用 `uuid`,多选引用统一使用 `json`。需要目录选择器的业务角色必须显式拥有 `app:<app-code>:directory:read` 能力。用 `resourceCapabilityCodes` 生成资源 CRUD 权限,用 `currentUserDataPolicy` 声明唯一的当前用户行规则。
12
14
 
13
15
  所有 CRUD 位于 `apps/web/src/platform-client.ts` 的 Data API adapter;Nest 只在需要复杂事务或外部集成时增加 App API。选择器显式携带当前环境、使用同源 Cookie,浏览器不读取 Token,也不发送 RoleSession header。
14
16
 
15
- 前端读取平台 current-user 的角色/能力并集进行展示保护,服务端始终权威。字段策略区分 create/update;无权字段在提交前从 payload 删除。
17
+ 前端读取平台 current-user 的角色/能力并集进行展示保护,服务端始终权威。字段默认继承资源 read/create/update;`access` 数组为 all-of,`false` 为明确拒绝,无权字段在提交前从 payload 删除。
16
18
 
17
19
  ```bash
18
20
  pnpm openxiangda check
@@ -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-nest": "2.0.0-alpha.44",
18
+ "openxiangda-nest": "2.0.0-alpha.45",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -16,7 +16,7 @@
16
16
  "antd": "6.4.3",
17
17
  "dayjs": "1.11.18",
18
18
  "docx-preview": "0.3.7",
19
- "openxiangda-contracts": "2.0.0-alpha.35",
19
+ "openxiangda-contracts": "2.0.0-alpha.36",
20
20
  "react": "19.2.8",
21
21
  "react-dom": "19.2.8",
22
22
  "react-router-dom": "7.8.2",
@@ -352,7 +352,7 @@ export function Shell({ children }: { children: ReactNode }) {
352
352
  export function EmptyApplicationPage() {
353
353
  return (
354
354
  <Shell>
355
- <Empty description="还没有声明数据资源;请在 platform/data 中添加 DataResource" />
355
+ <Empty description="还没有声明数据资源;请在 openxiangda.config.ts 中添加资源和字段" />
356
356
  </Shell>
357
357
  );
358
358
  }
@@ -87,7 +87,16 @@ function definitionFor(resourceCode: string) {
87
87
 
88
88
  function fieldFor(surface: DataResourceSurface, key: string): SurfaceField {
89
89
  const field = surface.fields[key];
90
- return field ? { key, ...field } : { key, label: key, widget: 'text' };
90
+ return field
91
+ ? { key, ...field }
92
+ : {
93
+ key,
94
+ label: key,
95
+ widget: 'text',
96
+ readCapabilities: [],
97
+ createCapabilities: [],
98
+ updateCapabilities: [],
99
+ };
91
100
  }
92
101
 
93
102
  function fieldsBySection(surface: DataResourceSurface) {
@@ -131,10 +140,13 @@ function normalizeFormValues(values: Record<string, unknown>, surface: DataResou
131
140
  }
132
141
 
133
142
  function fieldWritable(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
134
- const capability = mode === 'create' ? field.createCapability : field.updateCapability;
135
143
  if (isAppSuperAdmin) return true;
136
- if (capability) return hasCapability(capability);
137
- return mode === 'create';
144
+ const capabilities = mode === 'create' ? field.createCapabilities : field.updateCapabilities;
145
+ return capabilities.length > 0 && capabilities.every(hasCapability);
146
+ }
147
+
148
+ function fieldReadable(field: SurfaceField, hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
149
+ return isAppSuperAdmin || (field.readCapabilities.length > 0 && field.readCapabilities.every(hasCapability));
138
150
  }
139
151
 
140
152
  function auditOperationLabel(operation: DataAuditEntry['operation']) {
@@ -179,11 +191,11 @@ async function exportResourceRows(
179
191
  code: string,
180
192
  name: string,
181
193
  surface: DataResourceSurface,
194
+ fields: SurfaceField[],
182
195
  query: Partial<GenericResourceQuery>,
183
196
  sort: { field: string; order: 'asc' | 'desc' }
184
197
  ) {
185
198
  const client = createNativeResourceClient(code, surface);
186
- const fields = listSurfaceFields(surface.fields);
187
199
  const rows: ResourceRecord[] = [];
188
200
  const pageSize = 500;
189
201
  let page = 1;
@@ -215,8 +227,8 @@ async function exportResourceRows(
215
227
  return { count: rows.length, truncated: rows.length < total };
216
228
  }
217
229
 
218
- function AuditEntry({ entry, surface }: { entry: DataAuditEntry; surface: DataResourceSurface }) {
219
- const fields = auditChangeFields(entry, surface);
230
+ function AuditEntry({ entry, surface, readable }: { entry: DataAuditEntry; surface: DataResourceSurface; readable: (field: SurfaceField) => boolean }) {
231
+ const fields = auditChangeFields(entry, surface).filter(readable);
220
232
  const changes = entry.changes || entry.record || entry.before || {};
221
233
  return (
222
234
  <div className="oxa-audit-entry">
@@ -261,14 +273,21 @@ function AuditEntry({ entry, surface }: { entry: DataAuditEntry; surface: DataRe
261
273
  );
262
274
  }
263
275
 
264
- function useGeneratedList(definition: GeneratedDefinition) {
276
+ function useGeneratedList(definition: GeneratedDefinition, readableFieldCodes: Set<string>) {
265
277
  const { code, surface } = definition;
266
- const defaultSortField = surface.list?.defaultSort?.field || Object.keys(surface.fields)[0] || 'id';
278
+ const declaredDefaultSort = surface.list?.defaultSort?.field;
279
+ const defaultSortField =
280
+ (declaredDefaultSort && readableFieldCodes.has(declaredDefaultSort)
281
+ ? declaredDefaultSort
282
+ : Object.keys(surface.fields).find((field) => readableFieldCodes.has(field))) || 'id';
267
283
  const defaultSortOrder = surface.list?.defaultSort?.order || 'asc';
268
284
  const defaultPageSize = surface.list?.defaultPageSize || 20;
285
+ const defaultSearchField = (surface.list?.searchableFields || []).find((field) =>
286
+ readableFieldCodes.has(field)
287
+ );
269
288
  const [page, setPage] = useState(1);
270
289
  const [pageSize, setPageSize] = useState(defaultPageSize);
271
- const [draft, setDraft] = useState<Partial<GenericResourceQuery>>({});
290
+ const [draft, setDraft] = useState<Partial<GenericResourceQuery>>({ searchField: defaultSearchField });
272
291
  const [query, setQuery] = useState<Partial<GenericResourceQuery>>({});
273
292
  const [sort, setSort] = useState({
274
293
  field: defaultSortField,
@@ -277,13 +296,13 @@ function useGeneratedList(definition: GeneratedDefinition) {
277
296
  useEffect(() => {
278
297
  setPage(1);
279
298
  setPageSize(defaultPageSize);
280
- setDraft({});
299
+ setDraft({ searchField: defaultSearchField });
281
300
  setQuery({});
282
301
  setSort({
283
302
  field: defaultSortField,
284
303
  order: defaultSortOrder as 'asc' | 'desc',
285
304
  });
286
- }, [code, defaultPageSize, defaultSortField, defaultSortOrder]);
305
+ }, [code, defaultPageSize, defaultSearchField, defaultSortField, defaultSortOrder]);
287
306
  const list = useList<ResourceRecord>({
288
307
  resource: code,
289
308
  pagination: { currentPage: page, pageSize },
@@ -332,9 +351,16 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
332
351
  const navigate = useNavigate();
333
352
  const { message } = App.useApp();
334
353
  const { hasCapability, identity } = useRuntime();
335
- const state = useGeneratedList(definition);
336
354
  const { code, name, surface, capabilities } = definition;
337
- const fields = listSurfaceFields(surface.fields);
355
+ const fields = listSurfaceFields(surface.fields).filter((field) =>
356
+ fieldReadable(field, hasCapability, identity.isAppSuperAdmin)
357
+ );
358
+ const readableFieldCodes = new Set(
359
+ Object.entries(surface.fields)
360
+ .filter(([, field]) => fieldReadable({ key: '', ...field }, hasCapability, identity.isAppSuperAdmin))
361
+ .map(([key]) => key)
362
+ );
363
+ const state = useGeneratedList(definition, readableFieldCodes);
338
364
  const [exporting, setExporting] = useState(false);
339
365
  const [filtersExpanded, setFiltersExpanded] = useState(false);
340
366
  const [density, setDensity] = useState<'small' | 'middle' | 'large'>('middle');
@@ -447,7 +473,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
447
473
  const handleExport = async () => {
448
474
  setExporting(true);
449
475
  try {
450
- const result = await exportResourceRows(code, name, surface, state.query, state.sort);
476
+ const result = await exportResourceRows(code, name, surface, fields, state.query, state.sort);
451
477
  message.success(result.truncated ? `已导出 ${result.count} 条(最多导出 10000 条)` : `已导出 ${result.count} 条`);
452
478
  } catch (error) {
453
479
  message.error(error instanceof Error ? error.message : '导出失败');
@@ -455,7 +481,12 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
455
481
  setExporting(false);
456
482
  }
457
483
  };
458
- const filterFields = surface.list?.filterFields || [];
484
+ const filterFields = (surface.list?.filterFields || []).filter((field) =>
485
+ readableFieldCodes.has(field)
486
+ );
487
+ const searchableFields = (surface.list?.searchableFields || []).filter((field) =>
488
+ readableFieldCodes.has(field)
489
+ );
459
490
  const hasMoreFilters = filterFields.length > 2;
460
491
  const columnMenu: MenuProps = {
461
492
  items: fields.map((field) => ({ key: field.key, label: field.label })),
@@ -505,7 +536,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
505
536
  <div className="oxa-filter-item">
506
537
  <Select
507
538
  allowClear
508
- options={(surface.list?.searchableFields || []).map((key) => ({
539
+ options={searchableFields.map((key) => ({
509
540
  label: fieldFor(surface, key).label,
510
541
  value: key,
511
542
  }))}
@@ -679,17 +710,24 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
679
710
 
680
711
  function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }) {
681
712
  const navigate = useNavigate();
682
- const { hasCapability } = useRuntime();
713
+ const { hasCapability, identity } = useRuntime();
683
714
  const { message } = App.useApp();
684
715
  const { code, name, surface, capabilities } = definition;
685
- const state = useGeneratedList(definition);
716
+ const readableFieldCodes = new Set(
717
+ Object.entries(surface.fields)
718
+ .filter(([, field]) => fieldReadable({ key: '', ...field }, hasCapability, identity.isAppSuperAdmin))
719
+ .map(([key]) => key)
720
+ );
721
+ const state = useGeneratedList(definition, readableFieldCodes);
686
722
  const [exporting, setExporting] = useState(false);
687
723
  const rows = (state.list.result.data || []) as unknown as ResourceRecord[];
688
- const fields = listSurfaceFields(surface.fields).slice(0, 4);
724
+ const fields = listSurfaceFields(surface.fields)
725
+ .filter((field) => fieldReadable(field, hasCapability, identity.isAppSuperAdmin))
726
+ .slice(0, 4);
689
727
  const handleExport = async () => {
690
728
  setExporting(true);
691
729
  try {
692
- const result = await exportResourceRows(code, name, surface, state.query, state.sort);
730
+ const result = await exportResourceRows(code, name, surface, fields, state.query, state.sort);
693
731
  message.success(result.truncated ? `已导出 ${result.count} 条(最多导出 10000 条)` : `已导出 ${result.count} 条`);
694
732
  } catch (error) {
695
733
  message.error(error instanceof Error ? error.message : '导出失败');
@@ -833,7 +871,7 @@ function GeneratedResourceFormPage({
833
871
  Object.fromEntries(
834
872
  Object.entries(values).filter(([key]) =>
835
873
  fieldWritable(
836
- surface.fields[key] ? { key, ...surface.fields[key] } : { key, label: key, widget: 'text' },
874
+ fieldFor(surface, key),
837
875
  mode,
838
876
  hasCapability,
839
877
  identity.isAppSuperAdmin
@@ -878,7 +916,13 @@ function GeneratedResourceFormPage({
878
916
  onFinish={(values) => void submit(values as Record<string, unknown>)}
879
917
  >
880
918
  <div className="oxa-sections">
881
- {fieldsBySection(surface).map((group) => (
919
+ {fieldsBySection(surface).map((group) => ({
920
+ ...group,
921
+ fields: group.fields.filter((field) =>
922
+ fieldReadable(field, hasCapability, identity.isAppSuperAdmin) ||
923
+ fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)
924
+ ),
925
+ })).filter((group) => group.fields.length > 0).map((group) => (
882
926
  <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}>
883
927
  <div className="oxa-grid">
884
928
  {group.fields.map((field) => (
@@ -911,6 +955,7 @@ function GeneratedResourceFormPage({
911
955
  function GeneratedResourceDetailPage({ definition, variant }: { definition: GeneratedDefinition; variant: 'desktop' | 'mobile' }) {
912
956
  const { id = '' } = useParams();
913
957
  const navigate = useNavigate();
958
+ const { hasCapability, identity } = useRuntime();
914
959
  const query = useOne<ResourceRecord>({
915
960
  resource: definition.code,
916
961
  id,
@@ -970,14 +1015,26 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
970
1015
  <Spin size="small" /> 正在读取操作记录
971
1016
  </div>
972
1017
  ) : auditEntries.length ? (
973
- auditEntries.slice(0, 5).map((entry) => <AuditEntry entry={entry} key={entry.id} surface={definition.surface} />)
1018
+ auditEntries.slice(0, 5).map((entry) => (
1019
+ <AuditEntry
1020
+ entry={entry}
1021
+ key={entry.id}
1022
+ readable={(field) => fieldReadable(field, hasCapability, identity.isAppSuperAdmin)}
1023
+ surface={definition.surface}
1024
+ />
1025
+ ))
974
1026
  ) : (
975
1027
  <Typography.Text type="secondary">暂无变更记录</Typography.Text>
976
1028
  );
977
1029
  const content = record ? (
978
1030
  <div className="oxa-detail-layout">
979
1031
  <div className="oxa-detail-main">
980
- {fieldsBySection(definition.surface).map((group) => (
1032
+ {fieldsBySection(definition.surface).map((group) => ({
1033
+ ...group,
1034
+ fields: group.fields.filter((field) =>
1035
+ fieldReadable(field, hasCapability, identity.isAppSuperAdmin)
1036
+ ),
1037
+ })).filter((group) => group.fields.length > 0).map((group) => (
981
1038
  <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}>
982
1039
  <Descriptions
983
1040
  column={{ xs: 1, sm: 2, lg: 3 }}
@@ -22,9 +22,9 @@
22
22
  "build": "pnpm --recursive build"
23
23
  },
24
24
  "devDependencies": {
25
- "openxiangda-cli": "2.0.0-alpha.79",
26
- "openxiangda-contracts": "2.0.0-alpha.35",
27
- "openxiangda-devkit-core": "2.0.0-alpha.46",
25
+ "openxiangda-cli": "2.0.0-alpha.80",
26
+ "openxiangda-contracts": "2.0.0-alpha.36",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.47",
28
28
  "typescript": "5.9.3"
29
29
  }
30
30
  }