openyida 2026.8.20 → 2026.8.25

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { createAuthRef, createYidaClient, isAuthRefReady } = require('../core/yida-client');
4
4
  const { CliError } = require('../core/cli-error');
5
+ const { t } = require('../core/i18n');
5
6
  const { searchUsers } = require('../corp-manager/api');
6
7
 
7
8
  const ROLE_CONFIGS = {
@@ -140,6 +141,12 @@ function unique(values) {
140
141
  return [...new Set((values || []).map(value => String(value).trim()).filter(Boolean))];
141
142
  }
142
143
 
144
+ function sameUserIds(left, right) {
145
+ const leftIds = unique(left).sort();
146
+ const rightIds = unique(right).sort();
147
+ return JSON.stringify(leftIds) === JSON.stringify(rightIds);
148
+ }
149
+
143
150
  function toPositiveInt(value, defaultValue) {
144
151
  const parsed = Number.parseInt(value || `${defaultValue}`, 10);
145
152
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -310,11 +317,7 @@ async function updateRoleManagers(options = {}, authRef = getAuthRef()) {
310
317
  const action = options.action || 'set';
311
318
  const inputUserIds = unique(options.userIds || options.users || []);
312
319
 
313
- if (action === 'set') {
314
- return saveRoleManagers({ appType, roleType, userIds: inputUserIds }, authRef);
315
- }
316
-
317
- if (inputUserIds.length === 0) {
320
+ if (action !== 'set' && inputUserIds.length === 0) {
318
321
  throw new Error(`${action} 操作必须提供 --users`);
319
322
  }
320
323
 
@@ -323,7 +326,9 @@ async function updateRoleManagers(options = {}, authRef = getAuthRef()) {
323
326
  const previousUserIds = current.roles[roleKey].userIds;
324
327
  let nextUserIds;
325
328
 
326
- if (action === 'add') {
329
+ if (action === 'set') {
330
+ nextUserIds = inputUserIds;
331
+ } else if (action === 'add') {
327
332
  nextUserIds = unique(previousUserIds.concat(inputUserIds));
328
333
  } else if (action === 'remove') {
329
334
  const removeSet = new Set(inputUserIds);
@@ -387,9 +392,22 @@ async function runUpdate(action, positionals, options) {
387
392
  });
388
393
  const current = await getAppPermission(appType);
389
394
  const roleKey = ROLE_CONFIGS[saved.roleType].key;
395
+ if (!sameUserIds(saved.userIds, current.roles[roleKey].userIds)) {
396
+ throw new CliError(t('app_permission.verify_failed'), {
397
+ code: 'APP_PERMISSION_VERIFY_FAILED',
398
+ details: {
399
+ expected: saved.userIds,
400
+ actual: current.roles[roleKey].userIds,
401
+ },
402
+ });
403
+ }
390
404
 
391
405
  printJson({
392
406
  ...saved,
407
+ before: {
408
+ userIds: saved.previousUserIds,
409
+ },
410
+ after: current.roles[roleKey],
393
411
  currentRole: current.roles[roleKey],
394
412
  });
395
413
  }
@@ -420,6 +438,7 @@ module.exports = {
420
438
  USAGE,
421
439
  parseCliOptions,
422
440
  splitList,
441
+ sameUserIds,
423
442
  normalizeRole,
424
443
  normalizeText,
425
444
  normalizeMember,
@@ -402,6 +402,7 @@ Examples:
402
402
  create_example1: ' openyida integration create APP_XXX FORM-XXX "New record notice" --receivers user123 --publish',
403
403
  create_example2: ' openyida integration create APP_XXX FORM-XXX "Get self then notify" --get-self --publish',
404
404
  create_missing_args: 'Missing required arguments.',
405
+ create_flow_name_too_long: 'Logic-flow names cannot exceed {0} characters (received {1}).',
405
406
  create_invalid_events: 'No valid trigger event was recognized.',
406
407
  create_no_receivers: 'No notification receiver or user field specified; no message node will be generated.',
407
408
  create_title: 'Create integration automation flow',
@@ -906,6 +907,56 @@ Examples:
906
907
  },
907
908
 
908
909
  // ── lib/get-page-config.js ─────────────────────────
910
+ // ── Permission write safety verification ────────────────────
911
+ app_permission: {
912
+ verify_failed: 'App administrator save verification failed',
913
+ },
914
+ corp_manager: {
915
+ address_book_verify_failed: 'Address book permission save verification failed: expected={0}, actual={1}',
916
+ admin_verify_failed: 'Administrator save verification failed: {0} was not found in the {1} list',
917
+ sub_admin_scope_verify_failed: 'Sub-administrator scope save verification failed: expected={0}, actual={1}',
918
+ admin_remove_verify_failed: 'Administrator removal verification failed: {0} is still in the {1} list',
919
+ },
920
+ save_permission: {
921
+ confirm_member_replace_usage: 'Replacing composite members also requires --confirm-member-replace',
922
+ data_object_required: 'Data permission must be a JSON object',
923
+ data_rule_required: 'Data permission rule must not be empty',
924
+ data_rule_type_required: 'Every data permission rule item must include type',
925
+ data_rule_value_invalid: 'The value of data permission rule {0} must be y/n or boolean',
926
+ data_enabled_required: 'Data permission must enable at least one data range',
927
+ custom_department_ids_required: 'CUSTOM_DEPARTMENT requires non-empty customDepartmentData.departmentIds',
928
+ formula_data_required: 'FORMULA requires non-empty formulaData',
929
+ data_range_required: 'Data permission requires a non-empty dataRange or rule',
930
+ action_enabled_required: 'Action permission must contain at least one operation set to true',
931
+ action_value_boolean: 'Action permission {0} must be boolean',
932
+ field_range_invalid: 'Field permission fieldRange only supports FORM or CUSTOM',
933
+ field_status_required: 'CUSTOM field permission requires a non-empty fieldStatus array',
934
+ field_status_item_required: 'Every fieldStatus item must include label, fieldName, componentName, and value',
935
+ field_status_value_invalid: 'Invalid field permission value: {0}; valid values: {1}',
936
+ json_object_required: '{0} must be a JSON object',
937
+ parse_failed: 'Failed to parse {0}: {1}',
938
+ role_conflict: 'Permission arguments specify inconsistent roles: {0}',
939
+ matrix_role_only: '--matrix can only update a role=MATRIX permission group',
940
+ all_members_role_only: '--all-members can only update a role=DEFAULT permission group',
941
+ target_role_invalid: 'Invalid role: {0}; valid values: {1}',
942
+ unnamed_package: 'Unnamed',
943
+ missing_package_uuid: 'No UUID',
944
+ target_no_match: 'No permission group matched role={0}; aborted with zero writes to prevent an unintended update. Current permission groups:\n{2}',
945
+ target_ambiguous: 'role={0} matched {1} permission groups; aborted with zero writes to prevent an unintended update.\n{2}',
946
+ unknown_operate_keys: 'The current permission group contains operation keys unknown to the CLI, so action-permission cannot be changed: {0}. Other dimensions may still be changed and unknown keys will be preserved verbatim.',
947
+ matrix_role_value_required: 'MATRIX roleData.roleValue must be a non-empty array',
948
+ matrix_data_required: 'When members use MATRIX, the data permission rule must include MATRIX',
949
+ data_matrix_member_required: 'When data permission includes MATRIX, members must select a valid permission matrix',
950
+ members_all_conflict: '--members and --all-members are mutually exclusive',
951
+ invalid_arguments: 'Argument validation failed: {0}',
952
+ query_limit: 'The permission-group query reached the current 20-item limit, so global role uniqueness cannot be proven. Aborted with zero writes.\n{0}',
953
+ unique_target: ' ✅ Unique target: {0}',
954
+ member_before: ' Members before: {0}',
955
+ member_after: ' Members after: {0}',
956
+ member_removed: ' Member roles to remove: {0}',
957
+ member_replace_confirm: 'Member replacement will remove existing composite roles. Review before/after and add --confirm-member-replace. Entries to be lost: {0}',
958
+ },
959
+
909
960
  get_page_config: {
910
961
  usage: 'Usage: openyida get-page-config <appType> <formUuid>',
911
962
  example: 'Example: openyida get-page-config APP_XXX FORM-XXX',
@@ -946,7 +997,10 @@ Examples:
946
997
  err_open_auth_invalid: 'openAuth must be y or n, current value: {0}',
947
998
  err_open_url_required: 'openUrl is required when enabling public access',
948
999
  err_open_url_prefix: 'openUrl must start with /o/, current value: {0}',
1000
+ err_page_url_prefix: 'openUrl must start with /o/ or /s/, current value: {0}',
949
1001
  err_open_url_chars: 'openUrl path only supports a-z A-Z 0-9 _ - and / as a segment separator, current value: {0}',
1002
+ verify_failed: 'Post-save verification failed: {0}',
1003
+ current_state_incomplete: 'Save aborted: current public-access config lacks openPageAuthConfig, so safe preservation cannot be proven',
950
1004
  },
951
1005
 
952
1006
  // ── lib/update-app.js ─────────────────────────────
@@ -399,6 +399,7 @@ openyida - 宜搭命令行工具
399
399
  create_example1: ' openyida integration create APP_XXX FORM-XXX "新增通知" --receivers user123 --publish',
400
400
  create_example2: ' openyida integration create APP_XXX FORM-XXX "获取自身后通知" --get-self --publish',
401
401
  create_missing_args: '缺少必要参数。',
402
+ create_flow_name_too_long: '逻辑流名称不能超过 {0} 个字符(当前 {1} 个)。',
402
403
  create_invalid_events: '未识别到有效触发事件。',
403
404
  create_no_receivers: '未指定通知接收人或成员字段,将不会生成消息通知节点。',
404
405
  create_title: '创建集成自动化逻辑流',
@@ -834,6 +835,56 @@ openyida - 宜搭命令行工具
834
835
  org_usage: '用法: openyida org <list|switch> [--json] [--corp-id <corpId>]',
835
836
  org_example: '示例:\n openyida org list --json # 列出可访问的组织\n openyida org switch --corp-id dingXXX # 优先切换已有 profile;不存在时登录新增',
836
837
  title: ' openyida import - 宜搭应用导入工具',
838
+
839
+ // ── 权限写入安全验证 ───────────────────────────
840
+ app_permission: {
841
+ verify_failed: '应用管理员保存后验证失败',
842
+ },
843
+ corp_manager: {
844
+ address_book_verify_failed: '保存通讯录权限后验证失败:expected={0}, actual={1}',
845
+ admin_verify_failed: '管理员保存后验证失败:{0} 未出现在 {1} 列表中',
846
+ sub_admin_scope_verify_failed: '平台子管理员保存后范围验证失败:expected={0}, actual={1}',
847
+ admin_remove_verify_failed: '管理员移除后验证失败:{0} 仍在 {1} 列表中',
848
+ },
849
+ save_permission: {
850
+ confirm_member_replace_usage: '复合成员替换需要额外传入 --confirm-member-replace',
851
+ data_object_required: '数据权限必须是 JSON 对象',
852
+ data_rule_required: '数据权限 rule 不能为空',
853
+ data_rule_type_required: '数据权限 rule 每一项都必须包含 type',
854
+ data_rule_value_invalid: '数据权限 rule {0} 的 value 只支持 y/n 或 boolean',
855
+ data_enabled_required: '数据权限至少需要一个启用的数据范围',
856
+ custom_department_ids_required: 'CUSTOM_DEPARTMENT 必须提供非空 customDepartmentData.departmentIds',
857
+ formula_data_required: 'FORMULA 必须提供非空 formulaData',
858
+ data_range_required: '数据权限必须提供非空 dataRange 或 rule',
859
+ action_enabled_required: '操作权限至少需要一个值为 true 的操作',
860
+ action_value_boolean: '操作权限 {0} 的值必须是 boolean',
861
+ field_range_invalid: '字段权限 fieldRange 只支持 FORM 或 CUSTOM',
862
+ field_status_required: 'CUSTOM 字段权限必须包含非空 fieldStatus 数组',
863
+ field_status_item_required: 'fieldStatus 每一项必须包含 label、fieldName、componentName 和 value',
864
+ field_status_value_invalid: '无效字段权限值: {0},有效值: {1}',
865
+ json_object_required: '{0} 必须是 JSON 对象',
866
+ parse_failed: '{0} 解析失败: {1}',
867
+ role_conflict: '权限参数中的 role 不一致: {0}',
868
+ matrix_role_only: '--matrix 只能更新 role=MATRIX 的权限组',
869
+ all_members_role_only: '--all-members 只能更新 role=DEFAULT 的权限组',
870
+ target_role_invalid: '无效 role: {0},有效值: {1}',
871
+ unnamed_package: '未命名',
872
+ missing_package_uuid: '无 UUID',
873
+ target_no_match: 'role={0} 未匹配;为防止误更新已中止(零写入)。当前权限组:\n{2}',
874
+ target_ambiguous: 'role={0} 匹配 {1} 个权限组;为防止误更新已中止(零写入)。\n{2}',
875
+ unknown_operate_keys: '当前权限组包含 CLI 未识别的操作权限键,不能修改 action-permission: {0}。可只修改其他维度,未知键会原样保留。',
876
+ matrix_role_value_required: 'MATRIX roleData.roleValue 必须是非空数组',
877
+ matrix_data_required: '成员选择为 MATRIX 时,数据权限 rule 必须包含 MATRIX',
878
+ data_matrix_member_required: '数据权限包含 MATRIX 时,成员必须选择有效权限矩阵',
879
+ members_all_conflict: '--members 与 --all-members 互斥,请勿同时指定',
880
+ invalid_arguments: '参数验证失败: {0}',
881
+ query_limit: '权限组查询已达到当前 20 条上限,无法证明 role 目标全局唯一,已中止(零写入)。\n{0}',
882
+ unique_target: ' ✅ 唯一目标: {0}',
883
+ member_before: ' 成员 before: {0}',
884
+ member_after: ' 成员 after: {0}',
885
+ member_removed: ' 将移除成员角色: {0}',
886
+ member_replace_confirm: '成员替换会移除现有复合角色,请核对 before/after 后添加 --confirm-member-replace。会丢失: {0}',
887
+ },
837
888
  // ── lib/get-page-config.js ─────────────────────────
838
889
  get_page_config: {
839
890
  usage: '用法: openyida get-page-config <appType> <formUuid>',
@@ -875,7 +926,10 @@ openyida - 宜搭命令行工具
875
926
  err_open_auth_invalid: 'openAuth 必须为 y 或 n,当前值: {0}',
876
927
  err_open_url_required: '开启公开访问时,openUrl 不能为空',
877
928
  err_open_url_prefix: 'openUrl 必须以 /o/ 开头,当前值: {0}',
929
+ err_page_url_prefix: 'openUrl 必须以 /o/ 或 /s/ 开头,当前值: {0}',
878
930
  err_open_url_chars: 'openUrl 路径部分只支持 a-z A-Z 0-9 _ -,可用 / 分隔多级路径,当前值: {0}',
931
+ verify_failed: '保存后验证失败: {0}',
932
+ current_state_incomplete: '保存已中止:当前公开访问配置缺少 openPageAuthConfig,无法证明可安全保留',
879
933
  },
880
934
 
881
935
  // ── lib/update-app.js ─────────────────────────────
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { createAuthRef, createYidaClient, isAuthRefReady } = require('../core/yida-client');
4
+ const { t } = require('../core/i18n');
4
5
 
5
6
  const ROLE_ALIASES = {
6
7
  app: 'applicationCreateRole',
@@ -248,9 +249,9 @@ async function getAddressBookVisible(authRef = getAuthRef()) {
248
249
  }
249
250
 
250
251
  async function saveAddressBookVisible(options = {}, authRef = getAuthRef()) {
251
- const current = await getAddressBookVisible(authRef);
252
- const isAllVisible = options.isAllVisible || options.allVisible || current.isAllVisible;
253
- const isAdminVisible = options.isAdminVisible || options.adminVisible || current.isAdminVisible;
252
+ const before = await getAddressBookVisible(authRef);
253
+ const isAllVisible = options.isAllVisible || options.allVisible || before.isAllVisible;
254
+ const isAdminVisible = options.isAdminVisible || options.adminVisible || before.isAdminVisible;
254
255
 
255
256
  const result = await corpPost('/query/corpadmin/saveAddressBoolVisible.json', {
256
257
  isAllVisible,
@@ -258,10 +259,21 @@ async function saveAddressBookVisible(options = {}, authRef = getAuthRef()) {
258
259
  }, authRef);
259
260
  assertSuccess(result, '保存通讯录权限');
260
261
 
262
+ const after = await getAddressBookVisible(authRef);
263
+ if (after.isAllVisible !== isAllVisible || after.isAdminVisible !== isAdminVisible) {
264
+ throw new Error(t(
265
+ 'corp_manager.address_book_verify_failed',
266
+ JSON.stringify({ isAllVisible, isAdminVisible }),
267
+ JSON.stringify(after)
268
+ ));
269
+ }
270
+
261
271
  return {
262
272
  success: true,
263
273
  isAllVisible,
264
274
  isAdminVisible,
275
+ before,
276
+ after,
265
277
  content: result.content || {},
266
278
  };
267
279
  }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { throwUsage } = require('../core/command-errors');
4
+ const { t } = require('../core/i18n');
4
5
 
5
6
  const {
6
7
  buildSubAdminConfig,
@@ -65,6 +66,13 @@ function splitList(value) {
65
66
  return String(value).split(',').map(item => item.trim()).filter(Boolean);
66
67
  }
67
68
 
69
+ function sameStringSet(left, right) {
70
+ const normalize = values => [...new Set((Array.isArray(values) ? values : splitList(values))
71
+ .map(value => String(value).trim())
72
+ .filter(Boolean))].sort();
73
+ return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
74
+ }
75
+
68
76
  function toPositiveInt(value, defaultValue) {
69
77
  const parsed = Number.parseInt(value || `${defaultValue}`, 10);
70
78
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -130,13 +138,29 @@ async function runAdd(positionals, options) {
130
138
  fail('缺少 --user <userId>');
131
139
  }
132
140
 
141
+ const deptIds = splitList(options.dept_ids || options.department_ids);
142
+ const scenes = splitList(options.scenes || 'appManage,bulletinBoard');
143
+ const before = await listAdmins({ role, userId });
133
144
  const result = await saveAdmin({
134
145
  role,
135
146
  userId,
136
- deptIds: splitList(options.dept_ids || options.department_ids),
137
- scenes: splitList(options.scenes || 'appManage,bulletinBoard'),
147
+ deptIds,
148
+ scenes,
138
149
  });
139
- printJson(result);
150
+ const after = await listAdmins({ role, userId });
151
+ const savedAdmin = after.admins.find(admin => admin.userId === userId);
152
+ if (!savedAdmin) {
153
+ throw new Error(t('corp_manager.admin_verify_failed', userId, role));
154
+ }
155
+ if (result.roleType === 'subCorpAdminRole'
156
+ && (!sameStringSet(savedAdmin.manageDeptIds, deptIds) || !sameStringSet(savedAdmin.manageScene, scenes))) {
157
+ throw new Error(t(
158
+ 'corp_manager.sub_admin_scope_verify_failed',
159
+ JSON.stringify({ deptIds, scenes }),
160
+ JSON.stringify({ deptIds: savedAdmin.manageDeptIds, scenes: savedAdmin.manageScene })
161
+ ));
162
+ }
163
+ printJson({ ...result, before, after });
140
164
  }
141
165
 
142
166
  async function runRemove(positionals, options) {
@@ -149,8 +173,13 @@ async function runRemove(positionals, options) {
149
173
  fail('缺少 --user <userId>');
150
174
  }
151
175
 
176
+ const before = await listAdmins({ role, userId });
152
177
  const result = await removeAdmin({ role, userId });
153
- printJson(result);
178
+ const after = await listAdmins({ role, userId });
179
+ if (after.admins.some(admin => admin.userId === userId)) {
180
+ throw new Error(t('corp_manager.admin_remove_verify_failed', userId, role));
181
+ }
182
+ printJson({ ...result, before, after });
154
183
  }
155
184
 
156
185
  async function runAddressBook(positionals, options) {
@@ -204,6 +233,7 @@ module.exports = {
204
233
  USAGE,
205
234
  parseCliOptions,
206
235
  splitList,
236
+ sameStringSet,
207
237
  normalizeVisible,
208
238
  buildSubAdminConfig,
209
239
  run,
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { normalizeTypedLiteral } = require('./integration-process-builder');
5
6
 
6
7
  /**
7
8
  * connector-presets.js - 已知连接器 action 的真实 inputs/outputs schema 预设
@@ -150,9 +151,7 @@ function buildConnectorRulesFromInputs(inputsSchema, assignments) {
150
151
  innerBase.id = input.name;
151
152
  innerBase.parentId = '';
152
153
  innerBase.valueType = assign.valueType || 'processVar';
153
- innerBase.value = assign.valueType === 'literal' && !Number.isNaN(Number(assign.value))
154
- ? Number(assign.value)
155
- : assign.value;
154
+ innerBase.value = normalizeTypedLiteral(assign.valueType, assign.value, input.componentName);
156
155
  innerBase.ruleId = assign.ruleId || generateConnectorRuleId();
157
156
  innerBase.valueLabel = assign.valueLabel || input.label || input.name;
158
157
  base.rules = [innerBase];