openxiangda 1.0.137 → 1.0.139

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 (27) hide show
  1. package/lib/cli.js +132 -2
  2. package/lib/design-gates.js +8 -3
  3. package/openxiangda-skills/references/component-guide.md +7 -1
  4. package/openxiangda-skills/references/permission-design-patterns.md +37 -6
  5. package/openxiangda-skills/references/permissions-settings.md +45 -6
  6. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +7 -3
  7. package/package.json +1 -1
  8. package/packages/sdk/dist/{ProcessPreview-Ci8_UsbN.d.mts → ProcessPreview-CZJP8n3e.d.mts} +6 -0
  9. package/packages/sdk/dist/{ProcessPreview-Ci8_UsbN.d.ts → ProcessPreview-CZJP8n3e.d.ts} +6 -0
  10. package/packages/sdk/dist/components/index.cjs +143 -16
  11. package/packages/sdk/dist/components/index.cjs.map +1 -1
  12. package/packages/sdk/dist/components/index.d.mts +8 -2
  13. package/packages/sdk/dist/components/index.d.ts +8 -2
  14. package/packages/sdk/dist/components/index.mjs +143 -16
  15. package/packages/sdk/dist/components/index.mjs.map +1 -1
  16. package/packages/sdk/dist/runtime/index.cjs +169 -25
  17. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  18. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  19. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  20. package/packages/sdk/dist/runtime/index.mjs +169 -25
  21. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  22. package/packages/sdk/dist/runtime/react.cjs +26 -9
  23. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  24. package/packages/sdk/dist/runtime/react.d.mts +10 -2
  25. package/packages/sdk/dist/runtime/react.d.ts +10 -2
  26. package/packages/sdk/dist/runtime/react.mjs +26 -9
  27. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
package/lib/cli.js CHANGED
@@ -6963,6 +6963,21 @@ function validateResourceItem(kind, item, errors, warnings) {
6963
6963
  } else {
6964
6964
  errors.push(`${label}: 不支持的 viewType: ${definition.viewType}`);
6965
6965
  }
6966
+ for (const [index, group] of (item.permissionGroups || []).entries()) {
6967
+ const actions = group.actions || group.operations;
6968
+ if (actions !== undefined) {
6969
+ if (!Array.isArray(actions)) {
6970
+ errors.push(`${label}.permissionGroups[${index}]: actions/operations 必须是数组`);
6971
+ } else {
6972
+ for (const action of actions) {
6973
+ const normalizedAction = normalizePermissionActionValue(action);
6974
+ if (!DATA_VIEW_PERMISSION_ACTIONS.has(normalizedAction)) {
6975
+ errors.push(`${label}.permissionGroups[${index}]: 不支持的数据视图 action "${action}"`);
6976
+ }
6977
+ }
6978
+ }
6979
+ }
6980
+ }
6966
6981
  validateDataViewPerformance(label, definition, errors, warnings, storageMode);
6967
6982
  }
6968
6983
  if (kind === 'storageConfigs') {
@@ -7064,6 +7079,12 @@ function validateResourceItem(kind, item, errors, warnings) {
7064
7079
  if (kind === 'scopeGrantSources') {
7065
7080
  if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
7066
7081
  if (!item.sourceFormUuid && !item.formUuid) errors.push(`${label}: 缺少 sourceFormUuid`);
7082
+ if (
7083
+ item.syncMode !== undefined &&
7084
+ !['on_write', 'manual'].includes(String(item.syncMode))
7085
+ ) {
7086
+ errors.push(`${label}: syncMode 只能是 on_write 或 manual`);
7087
+ }
7067
7088
  const subjectMappings = item.subjectMappings || item.subjects;
7068
7089
  const dimensionMappings = item.dimensionMappings || item.dimensions;
7069
7090
  if (!Array.isArray(subjectMappings) || subjectMappings.length === 0) {
@@ -7104,6 +7125,19 @@ function validateResourceItem(kind, item, errors, warnings) {
7104
7125
  if (kind === 'formPermissionGroups') {
7105
7126
  if (!item.name) errors.push(`${label}: 缺少 name`);
7106
7127
  if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
7128
+ const actions = item.actions || item.operations;
7129
+ if (actions !== undefined) {
7130
+ if (!Array.isArray(actions)) {
7131
+ errors.push(`${label}: actions/operations 必须是数组`);
7132
+ } else {
7133
+ for (const action of actions) {
7134
+ const normalizedAction = normalizePermissionActionValue(action);
7135
+ if (!FORM_PERMISSION_ACTIONS.has(normalizedAction)) {
7136
+ errors.push(`${label}: 不支持的表单 action "${action}"`);
7137
+ }
7138
+ }
7139
+ }
7140
+ }
7107
7141
  if (item.dataPermission?.type === 'scope_policy' && !getScopePolicyCode(item.dataPermission)) {
7108
7142
  errors.push(`${label}: dataPermission.type=scope_policy 时必须声明 policyCode`);
7109
7143
  }
@@ -7131,14 +7165,43 @@ function validateScopePermissionResources(manifest, errors, warnings) {
7131
7165
  .map(String)
7132
7166
  );
7133
7167
  const scopePolicyRefs = [];
7168
+ for (const source of manifest.scopeGrantSources || []) {
7169
+ if (String(source.syncMode || '').trim() === 'manual') {
7170
+ warnings.push(`${resourceLabel('scopeGrantSources', source)}: syncMode=manual,运行时来源表单变更不会自动物化;请确认有显式 scope sync 作业`);
7171
+ }
7172
+ }
7134
7173
  const inspectPermissionGroup = (kind, item) => {
7135
7174
  const groups = kind === 'dataViews'
7136
7175
  ? (Array.isArray(item.permissionGroups) ? item.permissionGroups : [])
7137
7176
  : [item];
7138
7177
  for (const group of groups) {
7178
+ const actions = kind === 'dataViews'
7179
+ ? normalizeDataViewPermissionGroupActions(group)
7180
+ : normalizeFormPermissionGroupActions(group);
7139
7181
  const dataPermission = group?.dataPermission;
7182
+ if (
7183
+ kind === 'formPermissionGroups' &&
7184
+ actions.includes('create') &&
7185
+ (!dataPermission || dataPermission.type !== 'scope_policy')
7186
+ ) {
7187
+ warnings.push(`${resourceLabel(kind, item)}: 声明了 create,但未声明 create 对应的 scope_policy;若新增数据必须落在用户业务范围内,请补充 dataPermission.type=scope_policy`);
7188
+ }
7140
7189
  if (!dataPermission) continue;
7141
7190
  if (dataPermission.type === 'scope_policy') {
7191
+ if (kind === 'formPermissionGroups') {
7192
+ const missing = ['view', 'edit', 'delete', 'export']
7193
+ .filter(action => !actions.includes(action));
7194
+ if (missing.length) {
7195
+ warnings.push(`${resourceLabel(kind, item)}: 使用 scope_policy 但 action 覆盖不完整,缺少 ${missing.join(', ')};请确认每个角色的 action matrix`);
7196
+ }
7197
+ }
7198
+ if (kind === 'dataViews') {
7199
+ const missing = ['query', 'stats', 'export']
7200
+ .filter(action => !actions.includes(action));
7201
+ if (missing.length) {
7202
+ warnings.push(`${resourceLabel(kind, item)}: Data View 使用 scope_policy 但 action 覆盖不完整,缺少 ${missing.join(', ')}`);
7203
+ }
7204
+ }
7142
7205
  const policyCode = getScopePolicyCode(dataPermission);
7143
7206
  if (!policyCode) {
7144
7207
  errors.push(`${resourceLabel(kind, item)}: scope_policy 缺少 policyCode`);
@@ -10472,11 +10535,20 @@ function normalizeDataViewManifest(bound, dataView) {
10472
10535
  definition: normalizedDefinition,
10473
10536
  refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
10474
10537
  permissionGroups: Array.isArray(dataView.permissionGroups)
10475
- ? dataView.permissionGroups.map(group => stripUndefinedValues(withoutResourceMeta(group)))
10538
+ ? dataView.permissionGroups.map(group => normalizeDataViewPermissionGroupManifest(group))
10476
10539
  : undefined,
10477
10540
  });
10478
10541
  }
10479
10542
 
10543
+ function normalizeDataViewPermissionGroupManifest(group = {}) {
10544
+ const body = stripUndefinedValues({
10545
+ ...withoutResourceMeta(group),
10546
+ operations: normalizeDataViewPermissionGroupActions(group),
10547
+ });
10548
+ delete body.actions;
10549
+ return body;
10550
+ }
10551
+
10480
10552
  function normalizeRouteManifest(route) {
10481
10553
  const code = route.code || route.resourceCode;
10482
10554
  return stripUndefinedValues({
@@ -10549,10 +10621,20 @@ function normalizeScopeGrantSourceManifest(source, bound) {
10549
10621
  effectiveFromField: source.effectiveFromField,
10550
10622
  effectiveToField: source.effectiveToField,
10551
10623
  filterJson: source.filterJson || source.filter,
10624
+ syncMode: normalizeScopeGrantSourceSyncMode(source.syncMode, source.sync),
10552
10625
  enabled: source.enabled !== false,
10553
10626
  });
10554
10627
  }
10555
10628
 
10629
+ function normalizeScopeGrantSourceSyncMode(value, legacySync) {
10630
+ const raw = String(value || '').trim().toLowerCase();
10631
+ if (['manual', 'off', 'none', 'disabled', 'false'].includes(raw)) {
10632
+ return 'manual';
10633
+ }
10634
+ if (legacySync === false) return 'manual';
10635
+ return 'on_write';
10636
+ }
10637
+
10556
10638
  function normalizeDataScopePolicyManifest(policy) {
10557
10639
  const code = policy.code || policy.resourceCode;
10558
10640
  return stripUndefinedValues({
@@ -11161,10 +11243,12 @@ function normalizeFormPermissionGroupManifest(bound, group) {
11161
11243
  name: group.name || group.code,
11162
11244
  type: group.type || 'view',
11163
11245
  roles: group.roles || [],
11246
+ operations: normalizeFormPermissionGroupActions(group),
11164
11247
  };
11165
11248
  delete body.code;
11166
11249
  delete body.formCode;
11167
11250
  delete body.form;
11251
+ delete body.actions;
11168
11252
  return stripUndefinedValues(body);
11169
11253
  }
11170
11254
 
@@ -11186,6 +11270,52 @@ function sortStringValues(value = []) {
11186
11270
  return [...(value || [])].map(item => String(item)).sort();
11187
11271
  }
11188
11272
 
11273
+ const FORM_PERMISSION_ACTIONS = new Set([
11274
+ 'view',
11275
+ 'create',
11276
+ 'edit',
11277
+ 'delete',
11278
+ 'export',
11279
+ 'import',
11280
+ 'change_records',
11281
+ 'workflow',
11282
+ ]);
11283
+
11284
+ const DATA_VIEW_PERMISSION_ACTIONS = new Set([
11285
+ 'query',
11286
+ 'stats',
11287
+ 'export',
11288
+ 'refresh',
11289
+ ]);
11290
+
11291
+ function normalizePermissionActionValue(action) {
11292
+ const value = String(action || '').trim();
11293
+ if (!value) return '';
11294
+ if (value === 'submit') return 'create';
11295
+ return value;
11296
+ }
11297
+
11298
+ function normalizePermissionActionArray(value, allowed, fallback = []) {
11299
+ const source = Array.isArray(value) ? value : fallback;
11300
+ return unique(source.map(normalizePermissionActionValue).filter(action => allowed.has(action)));
11301
+ }
11302
+
11303
+ function normalizeFormPermissionGroupActions(group = {}) {
11304
+ if (group.actions !== undefined || group.operations !== undefined) {
11305
+ return normalizePermissionActionArray(group.actions || group.operations, FORM_PERMISSION_ACTIONS);
11306
+ }
11307
+ return group.type === 'submit'
11308
+ ? ['create']
11309
+ : ['view', 'edit', 'delete', 'change_records', 'workflow'];
11310
+ }
11311
+
11312
+ function normalizeDataViewPermissionGroupActions(group = {}) {
11313
+ if (group.actions !== undefined || group.operations !== undefined) {
11314
+ return normalizePermissionActionArray(group.actions || group.operations, DATA_VIEW_PERMISSION_ACTIONS);
11315
+ }
11316
+ return ['query'];
11317
+ }
11318
+
11189
11319
  function pickObjectKeys(source = {}, keys = []) {
11190
11320
  return keys.reduce((acc, key) => {
11191
11321
  acc[key] = source[key];
@@ -11407,7 +11537,7 @@ function normalizeDataViewPermissionGroupsForPlan(groups = []) {
11407
11537
  code,
11408
11538
  name: group.name || code || '默认权限组',
11409
11539
  roles: Array.isArray(group.roles) ? sortStringValues(group.roles) : [],
11410
- operations: Array.isArray(group.operations) ? sortStringValues(group.operations) : ['query'],
11540
+ operations: sortStringValues(normalizeDataViewPermissionGroupActions(group)),
11411
11541
  fieldPermissions: normalizeFieldPermissionsForPlan(group.fieldPermissions),
11412
11542
  dataPermission: group.dataPermission || undefined,
11413
11543
  });
@@ -124,15 +124,19 @@ const DESIGN_GATE_TOPICS = [
124
124
  '哪些角色可以设置应用角色、分配角色成员、给角色分配接口权限;这些角色是否需要 `app:role:manage`',
125
125
  '哪些角色可以维护页面权限组、表单权限组、组织账号;是否需要 `app:page-permission-group:manage`、`app:form-permission-group:manage`、`app:organization:manage`',
126
126
  '业务范围字段是什么,哪些可见字段需要派生隐藏 scalar key 供表单权限条件匹配',
127
- '页面/菜单/路由、表单 submit/view/manage/fieldAccessPolicy/dataScope 的权限矩阵如何落到 resources',
127
+ '每个角色的 action matrix 是什么:view/create/edit/delete/export/import/change_records/workflow 是否分别授权',
128
+ '导出/导入是否独立授权;导出范围是否与 view 范围不同,导入是否还需要逐行 create 范围校验',
129
+ 'create 是否需要 scope_policy 校验提交数据,防止新增到未授权业务范围',
130
+ '页面/菜单/路由、表单 actions/fieldAccessPolicy/dataPermission/scope_policy 的权限矩阵如何落到 resources',
128
131
  '查询参数是否参与授权;如果参与,只能做上下文、筛选或 ticket 输入,敏感数据必须由 public-access grant、角色、表单权限组或 App Function 校验',
129
132
  '验收时需要模拟哪些角色、账号状态、业务范围、查询参数篡改和拒绝场景',
130
133
  ],
131
134
  recommendedDefaults: [
132
135
  '正式后台优先选择 managed-platform-account;如果平台账号已存在,选择 existing-platform-user-assignment;固定内部门户选择 static-role-permission',
133
136
  'query-param-context 仅用于低风险页面上下文、筛选条件或公开 ticket 输入,不作为敏感数据授权依据',
134
- '角色写 src/resources/roles,页面组写 permissions/page-groups,表单组写 permissions/form-groups,显式声明 dataScope/operations/fieldAccessPolicy',
135
- '复杂业务范围优先写 permissions/scope-dimensions、permissions/scope-grant-sources、permissions/data-scope-policies,并在表单权限组或 data-view 权限组使用 dataPermission.type=scope_policy',
137
+ '角色写 src/resources/roles,页面组写 permissions/page-groups,表单组写 permissions/form-groups,显式声明 actions/dataPermission/fieldAccessPolicy;平台内部仍兼容存储为 operations',
138
+ '复杂业务范围优先写 permissions/scope-dimensions、permissions/scope-grant-sources、permissions/data-scope-policies,并在表单权限组或 data-view 权限组使用 dataPermission.type=scope_policy;授权来源默认 syncMode=on_write,由平台在来源表单写入后自动物化',
139
+ 'scope_policy 只表达数据范围,actions 表达操作能力;按钮隐藏只是 UX,后端 action check 才是权威',
136
140
  'scope_policy 默认语义是个人授权 + 当前应用角色授权;多维 rules 显式 AND;空授权集合拒绝;管理员绕过但可审计',
137
141
  '能管理角色或给别人分配角色的应用角色必须在 roles manifest 声明 apiPermissionCodes;至少包含 app:role:manage,按需加入 app:page-permission-group:manage、app:form-permission-group:manage、app:organization:manage',
138
142
  '由校区管理员等委托管理员创建的新角色,如果还具备继续管理账号/角色/权限的能力,也必须同步声明并发布对应 apiPermissionCodes',
@@ -144,6 +148,7 @@ const DESIGN_GATE_TOPICS = [
144
148
  '不能只做前端权限隐藏,必须有后端角色、表单权限组、public-access grant 或 App Function 校验',
145
149
  '查询参数不能作为敏感授权依据,只能作为上下文、筛选或 ticket 输入',
146
150
  '只配菜单可见,不配后端数据权限',
151
+ '只隐藏按钮但没有表单权限组 actions 或 App Function 服务端校验',
147
152
  '为了省事授予全部数据再在前端过滤',
148
153
  '为每个客户/项目/区域/学院/班级/门店创建一个角色或一个权限组',
149
154
  '把应用表单里的业务范围强行同步成平台部门、平台角色或大量权限组',
@@ -64,7 +64,7 @@ AI 在实现成熟交互前必须先判断是否已有可靠组件或库:
64
64
  | 富文本编辑 | `EditorField` | 含图片上传集成 | 完整工具栏、平台图床、粘贴清洗 |
65
65
  | 数字签名 | `DigitalSignatureField` | 平台级签名 | 签名采集、验证、归档 |
66
66
  | 地理位置 | `LocationField` | 接入地图服务 | 定位、地图选点、地址解析 |
67
- | 数据管理列表 | `DataManagementList` | 接入数据查询 API | 分页、筛选、导出、批量操作、字段权限 |
67
+ | 数据管理列表 | `DataManagementList` | 接入数据查询 API | 分页、筛选、导出、批量操作、字段权限、后端 action summary 按钮控制 |
68
68
 
69
69
  ### 示例
70
70
 
@@ -89,6 +89,12 @@ OSS 浏览器直传所需的 CORS 规则在 `src/resources/storage/<code>.json`
89
89
 
90
90
  业务应用只能从 `openxiangda`、`openxiangda/runtime`、`openxiangda/runtime/react` 等公开入口导入组件和 SDK。不要为了减小包体积去引用 `openxiangda/packages/sdk/dist/...` 内部文件;React SPA 模板已提供 vendor chunk 分组,SDK 会把 `antd-mobile`、`dayjs` 等大型 UI 依赖交给应用构建器按路由拆分。
91
91
 
92
+ `DataManagementList` 默认 `permissionMode="auto"`,进入页面会读取表单 action
93
+ summary,并用 `can.create/export/import/delete/workflow/change_records` 隐藏新增、
94
+ 导入、导出、删除、流程等按钮。`readonly=true` 仍会强制关闭新增、编辑、删除和
95
+ 导入。`actionOverrides` 只能关闭按钮,不能扩大后端权限;敏感授权必须依赖后端
96
+ form actions、`scope_policy` 或 App Function 服务端校验。
97
+
92
98
  > ✅ 一句话原则:**这些场景看到了,直接抄上表,不要自己写。**
93
99
 
94
100
  ---
@@ -27,8 +27,8 @@ Required design:
27
27
  scalar scope keys, platform role member sync state.
28
28
  - `src/resources/roles/*.json`: stable app role codes.
29
29
  - `src/resources/permissions/page-groups/*.json`: menu, route, and page access.
30
- - `src/resources/permissions/form-groups/*.json`: submit/view/manage, data
31
- scope, operations, and `fieldAccessPolicy`.
30
+ - `src/resources/permissions/form-groups/*.json`: `actions`, data scope or
31
+ `dataPermission.type=scope_policy`, and `fieldAccessPolicy`.
32
32
  - `apiPermissionCodes` on roles that are allowed to manage roles, role members,
33
33
  permission groups, or organization accounts.
34
34
  - Sync App Functions or trusted JS_CODE: sync organization units, accounts, and
@@ -142,8 +142,13 @@ The platform keeps RBAC and data range separate:
142
142
 
143
143
  - Roles decide who can enter pages, submit/view forms, use operations, and
144
144
  maintain permissions.
145
+ - Form `actions` decide what a role can do: `view`, `create`, `edit`, `delete`,
146
+ `export`, `import`, `change_records`, and `workflow`.
147
+ - Data View `actions` decide query capabilities: `query`, `stats`, `export`,
148
+ and `refresh`.
145
149
  - Field access remains `fieldAccessPolicy`.
146
- - Business scope policies decide which rows a matched role/user can read.
150
+ - Business scope policies decide which rows or submitted scope values a matched
151
+ role/user can access for the current action.
147
152
  - App Functions must still check role/scope server-side before sensitive
148
153
  writes.
149
154
 
@@ -157,21 +162,47 @@ Default semantics:
157
162
  - Authorization source rows should include an explicit status/enabled field and
158
163
  grant sources should use `filterJson` or `enabledField` so drafts, disabled
159
164
  rows, or rejected assignments are not materialized.
160
- - Source forms must have a sync path. `sync: true` on the manifest runs during
161
- publish; runtime changes need an automation/App Function or platform API call
162
- that resyncs the affected grant source after create/update/delete.
165
+ - Source authorization forms are synced by the platform after form
166
+ create/update/delete/import when the grant source uses the default
167
+ `syncMode: "on_write"`. Use `syncMode: "manual"` only for externally
168
+ maintained sources or unusual bulk jobs; `sync: true` still performs an
169
+ immediate full sync during resource publish.
163
170
  - Policy target fields should normally be hidden scalar keys. If targeting a
164
171
  JSONB option/person/department field directly, declare `valuePath: "value"` or
165
172
  `componentType`; multi-value fields should be normalized into a scalar key or
166
173
  a separate authorization row.
167
174
  - App Function form read helpers enforce view permission for real callers. Do
168
175
  not grant broad form read permission and then filter in function/page code.
176
+ - `DataManagementList` can hide buttons from the backend action summary, but it
177
+ is not the authority. Sensitive actions must be represented by form actions or
178
+ App Function server checks.
169
179
 
170
180
  Do not model a dynamic business object set by generating dozens or hundreds of
171
181
  roles or permission groups. Use a business authorization form and `scope_policy`
172
182
  when the object set is maintained by users, imported from another system, or
173
183
  expected to grow.
174
184
 
185
+ Recommended action pattern:
186
+
187
+ ```json
188
+ {
189
+ "code": "campus_profile_manage",
190
+ "formCode": "student_profile",
191
+ "name": "校区管理员档案权限",
192
+ "type": "view",
193
+ "roles": ["campus_admin"],
194
+ "actions": ["view", "edit", "export"],
195
+ "dataPermission": {
196
+ "type": "scope_policy",
197
+ "policyCode": "campus_row_scope"
198
+ },
199
+ "fieldAccessPolicy": {
200
+ "defaultAccess": "readonly",
201
+ "fields": [{ "fieldId": "remark", "access": "edit" }]
202
+ }
203
+ }
204
+ ```
205
+
175
206
  ## Role Setting Delegation
176
207
 
177
208
  Setting application roles is itself permission controlled. A user can be a
@@ -260,6 +260,7 @@ Declare scope resources:
260
260
  { "field": "status", "op": "eq", "value": "enabled" }
261
261
  ]
262
262
  },
263
+ "syncMode": "on_write",
263
264
  "sync": true
264
265
  }
265
266
  ```
@@ -289,7 +290,43 @@ Reference the policy from a form permission group:
289
290
  "name": "学院查看学生",
290
291
  "type": "view",
291
292
  "roles": ["college_admin"],
292
- "operations": ["view"],
293
+ "actions": ["view", "edit", "export"],
294
+ "dataPermission": {
295
+ "type": "scope_policy",
296
+ "policyCode": "college_data"
297
+ }
298
+ }
299
+ ```
300
+
301
+ Unified permission model:
302
+
303
+ - `scope_policy` means data range: which rows this user/current app role can
304
+ access.
305
+ - `actions` means operation ability: what the role can do to rows in that
306
+ range. Form actions are `view`, `create`, `edit`, `delete`, `export`,
307
+ `import`, `change_records`, and `workflow`.
308
+ - The platform still stores actions in the legacy `operations` field. Manifests
309
+ may use either `actions` or `operations`; prefer `actions` in new resources.
310
+ - `type: "submit"` is the legacy create group and maps to `actions:["create"]`.
311
+ - `export` is independent from `view` for new apps. Old apps may temporarily
312
+ rely on view-to-export compatibility, but `permission audit` should be used to
313
+ migrate to explicit `export`.
314
+ - `create` with `scope_policy` validates submitted scope fields; a campus admin
315
+ cannot create a row for another campus just because the create button is
316
+ visible.
317
+ - Frontend button hiding is UX only. The backend action check and action-specific
318
+ data range are authoritative.
319
+
320
+ Create permission with business scope:
321
+
322
+ ```json
323
+ {
324
+ "code": "college_student_create",
325
+ "formCode": "student",
326
+ "name": "学院管理员新增本学院学生",
327
+ "type": "submit",
328
+ "roles": ["college_admin"],
329
+ "actions": ["create"],
293
330
  "dataPermission": {
294
331
  "type": "scope_policy",
295
332
  "policyCode": "college_data"
@@ -298,7 +335,8 @@ Reference the policy from a form permission group:
298
335
  ```
299
336
 
300
337
  For Data View permission groups, use the same `dataPermission` shape inside the
301
- view's `permissionGroups`.
338
+ view's `permissionGroups`. Data View actions are `query`, `stats`, `export`,
339
+ and `refresh`.
302
340
 
303
341
  Default runtime semantics:
304
342
 
@@ -317,10 +355,11 @@ Default runtime semantics:
317
355
  - `filterJson` filters authorization source rows before materializing grants.
318
356
  Use it for enabled/approved/effective authorization rows; unsupported filter
319
357
  operations do not match.
320
- - Source authorization forms must be synced after changes. For formal apps,
321
- create an automation/App Function that runs `openxiangda scope sync` or the
322
- equivalent platform API after authorization rows are created, updated, or
323
- deleted. Publishing with `"sync": true` only syncs during resource publish.
358
+ - Source authorization forms are synced by the platform after create/update/
359
+ delete/import when the grant source uses the default `syncMode: "on_write"`.
360
+ Use `syncMode: "manual"` only for externally maintained sources or unusual
361
+ bulk jobs that explicitly call `openxiangda scope sync`. Publishing with
362
+ `"sync": true` still performs an immediate full sync during resource publish.
324
363
  - App Function `ctx.form.queryOne/queryMany/getById` enforces the caller's view
325
364
  permission when there is a real current user. No matching view permission
326
365
  group means deny; backend system automations can still use trusted internal
@@ -334,6 +334,7 @@ Grant source:
334
334
  { "field": "status", "op": "eq", "value": "enabled" }
335
335
  ]
336
336
  },
337
+ "syncMode": "on_write",
337
338
  "sync": true
338
339
  }
339
340
  ```
@@ -373,9 +374,12 @@ Data View permission groups use the same `dataPermission` object inside
373
374
  default; large grant sets use DB `EXISTS` instead of huge `IN` SQL. Prefer
374
375
  hidden scalar target fields such as `collegeCode`; if the target is a JSONB
375
376
  option/person/department field, set `valuePath: "value"` or `componentType`.
376
- Runtime changes to the authorization source form need an automation/App
377
- Function or explicit platform API call to resync the grant source; manifest
378
- `sync: true` only runs during resource publish.
377
+ Authorization source form changes are synced by the platform after
378
+ create/update/delete/import when the grant source uses the default
379
+ `syncMode: "on_write"`. Use `syncMode: "manual"` only for externally maintained
380
+ sources or special bulk jobs that explicitly call `openxiangda scope sync`.
381
+ Manifest `sync: true` still performs an immediate full sync during resource
382
+ publish.
379
383
 
380
384
  ## 3. Connector — `src/resources/connectors/<code>.json`
381
385
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.137",
3
+ "version": "1.0.139",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -1021,6 +1021,12 @@ interface ProcessDefinition {
1021
1021
  interface ViewPermissionSummary {
1022
1022
  fieldPermissions: Record<string, 'FORM_FILED_HIDDEN' | 'FORM_FILED_VIEW' | 'FORM_FILED_EDIT'>;
1023
1023
  operations: string[];
1024
+ actions?: string[];
1025
+ can?: Record<string, boolean>;
1026
+ fieldAccessPolicy?: any;
1027
+ hasFullAccess?: boolean;
1028
+ resourceType?: string;
1029
+ matchedGroupCodes?: string[];
1024
1030
  }
1025
1031
  /** 表单实例数据 */
1026
1032
  interface FormInstanceData {
@@ -1021,6 +1021,12 @@ interface ProcessDefinition {
1021
1021
  interface ViewPermissionSummary {
1022
1022
  fieldPermissions: Record<string, 'FORM_FILED_HIDDEN' | 'FORM_FILED_VIEW' | 'FORM_FILED_EDIT'>;
1023
1023
  operations: string[];
1024
+ actions?: string[];
1025
+ can?: Record<string, boolean>;
1026
+ fieldAccessPolicy?: any;
1027
+ hasFullAccess?: boolean;
1028
+ resourceType?: string;
1029
+ matchedGroupCodes?: string[];
1024
1030
  }
1025
1031
  /** 表单实例数据 */
1026
1032
  interface FormInstanceData {