openyida 2026.8.19 → 2026.8.20

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/bin/yida.js CHANGED
@@ -670,15 +670,13 @@ async function main() {
670
670
 
671
671
  case 'login': {
672
672
  const loginArgs = applyLoginEnvironmentFlags(args, { inferTargetUrl: true });
673
- const { getAuthStatus, isEnvAuthMode } = require('../lib/core/utils');
673
+ const { getAuthStatus } = require('../lib/core/utils');
674
674
  if (hasHelpFlag(loginArgs)) {
675
675
  printLoginHelp();
676
676
  } else {
677
677
  assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
678
678
  if (loginArgs.includes('--check-only')) {
679
679
  console.log(JSON.stringify(getAuthStatus(buildTokenLoginOptions(loginArgs)), null, 2));
680
- } else if (isEnvAuthMode()) {
681
- printLoginResult(getAuthStatus(buildTokenLoginOptions(loginArgs)));
682
680
  } else {
683
681
  const { tokenLogin } = require('../lib/auth/token-auth');
684
682
  const result = await tokenLogin(buildTokenLoginOptions(loginArgs));
@@ -697,7 +695,7 @@ async function main() {
697
695
  case 'auth': {
698
696
  const subCommand = args[0];
699
697
  const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
700
- const { getAuthStatus, isEnvAuthMode } = require('../lib/core/utils');
698
+ const { getAuthStatus } = require('../lib/core/utils');
701
699
  const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
702
700
  if (!subCommand || subCommand === '--help' || subCommand === '-h') {
703
701
  printAuthHelp();
@@ -720,9 +718,7 @@ async function main() {
720
718
  printLoginHelp();
721
719
  } else {
722
720
  assertNoUnsupportedLegacyLoginFlags(rawArgs.slice(1), authArgs);
723
- const result = isEnvAuthMode()
724
- ? getAuthStatus(buildTokenLoginOptions(authArgs))
725
- : await tokenLogin(buildTokenLoginOptions(authArgs));
721
+ const result = await tokenLogin(buildTokenLoginOptions(authArgs));
726
722
  printLoginResult(result);
727
723
  }
728
724
  } else if (subCommand === 'refresh') {
package/lib/auth/org.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  loadTokenSession,
14
14
  normalizeCorpName,
15
15
  resolveTokenSession,
16
- isHostInjectedTokenMode,
16
+ isEnvTokenAuthMode,
17
17
  saveAuthProfilePointer,
18
18
  saveProjectLegacyTokenSession,
19
19
  saveTokenSession,
@@ -70,15 +70,15 @@ function createProfileRequiredError(targetCorpId, resolution = {}) {
70
70
  return error;
71
71
  }
72
72
 
73
- function createHostInjectedSwitchError(targetCorpId, currentSession, resolution = {}) {
73
+ function createEnvTokenSwitchError(targetCorpId, currentSession, resolution = {}) {
74
74
  const actualCorpId = currentSession && currentSession.corp_id;
75
- const error = new Error(`当前为宿主注入登录态,不能通过 OAuth 切换组织:target=${targetCorpId}, actual=${actualCorpId || 'unknown'}`);
76
- error.code = 'ORG_SWITCH_HOST_INJECTED_MISMATCH';
77
- error.status = 'host_injected_token_mismatch';
75
+ const error = new Error(`当前为 env token 登录态,不能通过 OAuth 切换组织:target=${targetCorpId}, actual=${actualCorpId || 'unknown'}`);
76
+ error.code = 'ORG_SWITCH_ENV_TOKEN_MISMATCH';
77
+ error.status = 'env_token_mismatch';
78
78
  error.targetCorpId = targetCorpId;
79
79
  error.actualCorpId = actualCorpId;
80
80
  error.auth_source = resolution.auth_source || (currentSession && currentSession.auth_source) || 'env';
81
- error.auth_store = resolution.auth_store || (currentSession && currentSession.auth_store) || 'host_injected';
81
+ error.auth_store = resolution.auth_store || (currentSession && currentSession.auth_store) || 'env';
82
82
  error.can_auto_use = false;
83
83
  return error;
84
84
  }
@@ -220,8 +220,8 @@ async function switchOrganization(targetCorpId, options = {}) {
220
220
  if (targetResolution.status === 'profile_required') {
221
221
  throw createProfileRequiredError(targetCorpId, targetResolution);
222
222
  }
223
- if (isHostInjectedTokenMode(projectOptions.env)) {
224
- throw createHostInjectedSwitchError(targetCorpId, currentSession, targetResolution);
223
+ if (isEnvTokenAuthMode(projectOptions.env)) {
224
+ throw createEnvTokenSwitchError(targetCorpId, currentSession, targetResolution);
225
225
  }
226
226
 
227
227
  const loginOptions = {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { CliError } = require('../core/cli-error');
4
4
  const {
5
- isHostInjectedTokenMode,
5
+ isEnvTokenAuthMode,
6
6
  listUserAuthProfiles,
7
7
  loadAuthProfilePointer,
8
8
  loadTokenSession,
@@ -129,14 +129,14 @@ function resolveSwitchTarget(target, options = {}) {
129
129
  }
130
130
 
131
131
  function switchAuthProfile(target, options = {}) {
132
- if (isHostInjectedTokenMode(options.env || process.env)) {
132
+ if (isEnvTokenAuthMode(options.env || process.env)) {
133
133
  throw new CliError(
134
- 'host-injected token mode cannot switch local auth profiles',
134
+ 'env token mode cannot switch local auth profiles',
135
135
  {
136
- code: 'AUTH_PROFILE_SWITCH_HOST_INJECTED',
136
+ code: 'AUTH_PROFILE_SWITCH_ENV_TOKEN',
137
137
  details: {
138
- nextStep: 'Ask the host runtime to inject the target organization token instead of switching local profiles.',
139
- next_step: 'Ask the host runtime to inject the target organization token instead of switching local profiles.',
138
+ nextStep: 'Ask the runtime to inject the target organization token instead of switching local profiles.',
139
+ next_step: 'Ask the runtime to inject the target organization token instead of switching local profiles.',
140
140
  },
141
141
  }
142
142
  );
@@ -15,14 +15,13 @@ const {
15
15
  clearAllUserAuthProfiles,
16
16
  clearTokenSession,
17
17
  deleteUserAuthProfile,
18
- isHostInjectedTokenMode,
18
+ isEnvTokenAuthMode,
19
19
  listUserAuthProfiles,
20
20
  loadLocalTokenSession,
21
21
  loadTokenSession,
22
22
  loadUserProfileFile,
23
23
  maskToken,
24
24
  normalizeCorpName,
25
- normalizeTokenSession,
26
25
  resolveTokenSession,
27
26
  saveBusinessContext,
28
27
  saveTokenSession,
@@ -274,6 +273,32 @@ function isRefreshAuthRequired(value) {
274
273
  }
275
274
 
276
275
  async function tokenLogin(options = {}) {
276
+ const env = options.env || process.env;
277
+ if (isEnvTokenAuthMode(env)) {
278
+ const status = tokenStatus(options);
279
+ if (status && status.can_auto_use) {
280
+ return {
281
+ ...status,
282
+ ok: true,
283
+ status: 'ok',
284
+ can_auto_use: true,
285
+ message: 'env token bootstrap credentials are already available; OAuth login was skipped',
286
+ };
287
+ }
288
+ return {
289
+ ...status,
290
+ ok: false,
291
+ auth_mode: 'token',
292
+ auth_source: status && status.auth_source ? status.auth_source : 'env',
293
+ auth_store: status && status.auth_store ? status.auth_store : 'env',
294
+ status: status && status.status ? status.status : 'not_logged_in',
295
+ can_auto_use: false,
296
+ message: status && status.message
297
+ ? status.message
298
+ : 'env token bootstrap credential is missing. Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.',
299
+ };
300
+ }
301
+
277
302
  const baseUrl = resolveTokenBaseUrl(options);
278
303
  const authBaseUrl = appendPath(baseUrl, DEFAULT_AUTH_PATH_PREFIX);
279
304
  const clientId = options.clientId || process.env.OPENYIDA_DINGTALK_CLIENT_ID || DINGTALK_OAUTH_CLIENT_ID;
@@ -315,21 +340,21 @@ function tokenStatus(options = {}) {
315
340
  const session = resolution.session;
316
341
  if (!session || (!session.access_token && !session.refresh_token)) {
317
342
  const env = options.env || process.env;
318
- const hostInjected = isHostInjectedTokenMode(env);
343
+ const envTokenMode = isEnvTokenAuthMode(env);
319
344
  const status = {
320
345
  ok: false,
321
346
  auth_mode: 'token',
322
- auth_source: resolution.auth_source || (hostInjected ? 'env' : undefined),
347
+ auth_source: resolution.auth_source || (envTokenMode ? 'env' : undefined),
323
348
  auth_store: resolution.auth_store,
324
349
  auth_profile: resolution.auth_profile,
325
350
  status: resolution.status === 'profile_required' ? 'profile_required' : 'not_logged_in',
326
351
  can_auto_use: false,
327
- failure_reason: hostInjected ? 'env_token_missing' : resolution.status,
328
- message: resolution.message || (hostInjected
329
- ? 'host-injected token is missing. Ask the host to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.'
352
+ failure_reason: envTokenMode ? 'env_token_missing' : resolution.status,
353
+ message: resolution.message || (envTokenMode
354
+ ? 'env token bootstrap credential is missing. Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.'
330
355
  : undefined),
331
- next_step: resolution.next_step || (hostInjected
332
- ? 'Ask the host runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN; do not run OAuth login in host-injected token mode.'
356
+ next_step: resolution.next_step || (envTokenMode
357
+ ? 'Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN; stop until credentials are available.'
333
358
  : 'Run openyida login to add an auth profile.'),
334
359
  next_step_commands: resolution.next_step_commands,
335
360
  candidate_count: resolution.candidate_count,
@@ -338,7 +363,7 @@ function tokenStatus(options = {}) {
338
363
  persistence_scope: resolution.persistence_scope,
339
364
  warning: resolution.warning,
340
365
  };
341
- if (!hostInjected) {
366
+ if (!envTokenMode) {
342
367
  status.token_file = require('./token-store').getTokenFilePath(options);
343
368
  }
344
369
  return status;
@@ -353,7 +378,7 @@ function tokenStatus(options = {}) {
353
378
  persistence_scope: session.persistence_scope,
354
379
  user_auth_store_writable: session.user_auth_store_writable,
355
380
  warning: session.warning,
356
- status: 'refresh_required',
381
+ status: 'ok',
357
382
  can_auto_use: true,
358
383
  refresh_token: maskToken(session.refresh_token),
359
384
  base_url: session.base_url,
@@ -446,7 +471,7 @@ async function tokenRefresh(options = {}) {
446
471
  }
447
472
  }
448
473
  const env = options.env || process.env;
449
- if (isHostInjectedTokenMode(env) && session.auth_source === 'env') {
474
+ if (isEnvTokenAuthMode(env) && ['env', 'project_legacy'].includes(session.auth_source)) {
450
475
  env.OPENYIDA_ACCESS_TOKEN = normalized.access_token;
451
476
  if (normalized.refresh_token) {
452
477
  env.OPENYIDA_REFRESH_TOKEN = normalized.refresh_token;
@@ -454,10 +479,10 @@ async function tokenRefresh(options = {}) {
454
479
  if (normalized.expires_at) {
455
480
  env.OPENYIDA_ACCESS_TOKEN_EXPIRES_AT = String(normalized.expires_at);
456
481
  }
457
- return normalizeTokenSession({
482
+ return saveTokenSession({
458
483
  ...normalized,
459
484
  auth_source: 'env',
460
- });
485
+ }, options);
461
486
  }
462
487
  return saveTokenSession(normalized, options);
463
488
  }
@@ -496,20 +521,6 @@ function toLogoutProfileSummary(session = {}) {
496
521
  }
497
522
 
498
523
  async function tokenLogout(options = {}) {
499
- const env = options.env || process.env;
500
- if (isHostInjectedTokenMode(env)) {
501
- return {
502
- ok: true,
503
- auth_mode: 'token',
504
- status: 'host_injected_noop',
505
- can_auto_use: false,
506
- auth_source: 'env',
507
- auth_store: 'host_injected',
508
- persistence_scope: 'host',
509
- message: 'host-injected token mode does not write or delete local auth profiles',
510
- };
511
- }
512
-
513
524
  const authProfile = options.authProfile || options.profile;
514
525
  const deleteAllProfiles = options.allProfiles === true || options.all === true;
515
526
  if (deleteAllProfiles) {
@@ -394,6 +394,7 @@ function buildAuthProfileNextStep(status) {
394
394
 
395
395
  function getAuthProfileSelector(options = {}) {
396
396
  const env = options.env || process.env;
397
+ const envTokenMode = isEnvTokenAuthMode(env);
397
398
  return {
398
399
  authProfile: sanitizeProfileName(
399
400
  options.authProfile ||
@@ -401,8 +402,18 @@ function getAuthProfileSelector(options = {}) {
401
402
  env.OPENYIDA_AUTH_PROFILE ||
402
403
  ''
403
404
  ),
404
- corpId: String(options.corpId || env.OPENYIDA_AUTH_CORP_ID || '').trim(),
405
- userId: String(options.userId || env.OPENYIDA_AUTH_USER_ID || '').trim(),
405
+ corpId: String(
406
+ options.corpId ||
407
+ env.OPENYIDA_AUTH_CORP_ID ||
408
+ (envTokenMode ? env.OPENYIDA_TOKEN_CORP_ID : '') ||
409
+ ''
410
+ ).trim(),
411
+ userId: String(
412
+ options.userId ||
413
+ env.OPENYIDA_AUTH_USER_ID ||
414
+ (envTokenMode ? env.OPENYIDA_TOKEN_USER_ID : '') ||
415
+ ''
416
+ ).trim(),
406
417
  };
407
418
  }
408
419
 
@@ -585,13 +596,8 @@ function saveUserTokenSession(session, options = {}) {
585
596
  function saveTokenSession(session, options = {}) {
586
597
  const env = options.env || process.env;
587
598
  const normalized = normalizeTokenSession(session);
588
- if (isHostInjectedTokenMode(env) && normalized.auth_source === 'env') {
589
- return attachAuthStoreMetadata(normalized, {
590
- auth_source: 'env',
591
- auth_store: 'host_injected',
592
- persistence_scope: 'host',
593
- user_auth_store_writable: null,
594
- });
599
+ if (isEnvTokenAuthMode(env) && normalized.auth_source === 'env') {
600
+ return saveProjectLegacyTokenSession(normalized, options);
595
601
  }
596
602
 
597
603
  try {
@@ -616,9 +622,15 @@ function saveTokenSession(session, options = {}) {
616
622
  }
617
623
  }
618
624
 
619
- function isHostInjectedTokenMode(env = process.env) {
620
- const authEnabled = String(env.YIDA_AUTH_ENABLED || '').trim().toLowerCase();
621
- return ['1', 'true', 'yes', 'on'].includes(authEnabled);
625
+ function isEnvTokenAuthMode(env = process.env) {
626
+ return String(env.OPENYIDA_AUTH_MODE || '').trim().toLowerCase() === 'token';
627
+ }
628
+
629
+ function hasEnvTokenCredential(env = process.env) {
630
+ return !!(
631
+ String(env.OPENYIDA_ACCESS_TOKEN || '').trim() ||
632
+ String(env.OPENYIDA_REFRESH_TOKEN || '').trim()
633
+ );
622
634
  }
623
635
 
624
636
  function loadEnvTokenSession(env = process.env) {
@@ -773,29 +785,40 @@ function resolveTokenSession(options = {}) {
773
785
  const env = options.env || process.env;
774
786
  const selector = getAuthProfileSelector(options);
775
787
  const envSession = applyBusinessContextToEnvSession(loadEnvTokenSession(env), options);
776
- if (
777
- isHostInjectedTokenMode(env) &&
778
- envSession &&
779
- (envSession.access_token || envSession.refresh_token) &&
780
- sessionMatchesSelector(envSession, selector, { allowMissingIdentity: true })
781
- ) {
782
- return {
783
- session: attachAuthStoreMetadata(envSession, {
784
- auth_source: 'env',
785
- auth_store: 'host_injected',
786
- persistence_scope: 'host',
787
- user_auth_store_writable: null,
788
- }),
789
- status: 'ok',
790
- };
791
- }
792
- if (isHostInjectedTokenMode(env)) {
788
+ if (isEnvTokenAuthMode(env)) {
789
+ const localSession = loadLocalTokenSession(options);
790
+ if (
791
+ localSession &&
792
+ (localSession.access_token || localSession.refresh_token) &&
793
+ sessionMatchesSelector(localSession, selector)
794
+ ) {
795
+ return {
796
+ session: localSession,
797
+ status: 'ok',
798
+ };
799
+ }
800
+ if (
801
+ hasEnvTokenCredential(env) &&
802
+ envSession &&
803
+ (envSession.access_token || envSession.refresh_token) &&
804
+ sessionMatchesSelector(envSession, selector, { allowMissingIdentity: true })
805
+ ) {
806
+ return {
807
+ session: attachAuthStoreMetadata(envSession, {
808
+ auth_source: 'env',
809
+ auth_store: 'env',
810
+ persistence_scope: 'process',
811
+ user_auth_store_writable: null,
812
+ }),
813
+ status: 'ok',
814
+ };
815
+ }
793
816
  return {
794
817
  session: null,
795
818
  status: 'env_token_missing',
796
819
  auth_source: 'env',
797
- auth_store: 'host_injected',
798
- persistence_scope: 'host',
820
+ auth_store: 'env',
821
+ persistence_scope: 'process',
799
822
  user_auth_store_writable: null,
800
823
  };
801
824
  }
@@ -807,7 +830,7 @@ function resolveTokenSession(options = {}) {
807
830
  return {
808
831
  session: attachAuthStoreMetadata(envSession, {
809
832
  auth_source: 'env',
810
- auth_store: 'host_injected',
833
+ auth_store: 'env',
811
834
  persistence_scope: 'process',
812
835
  user_auth_store_writable: getUserAuthStoreWritable(options),
813
836
  }),
@@ -895,7 +918,7 @@ function resolveTokenSession(options = {}) {
895
918
  return {
896
919
  session: attachAuthStoreMetadata(envSession, {
897
920
  auth_source: 'env',
898
- auth_store: 'host_injected',
921
+ auth_store: 'env',
899
922
  persistence_scope: 'process',
900
923
  user_auth_store_writable: getUserAuthStoreWritable(options),
901
924
  }),
@@ -975,7 +998,8 @@ module.exports = {
975
998
  loadEnvTokenSession,
976
999
  loadLocalTokenSession,
977
1000
  saveProjectLegacyTokenSession,
978
- isHostInjectedTokenMode,
1001
+ isEnvTokenAuthMode,
1002
+ hasEnvTokenCredential,
979
1003
  isAccessTokenUsable,
980
1004
  maskToken,
981
1005
  resolveEnvName,
@@ -105,38 +105,6 @@ function compactSkillsDiagnostics(skillsDiagnostics) {
105
105
  };
106
106
  }
107
107
 
108
- function isTruthyEnv(value) {
109
- return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
110
- }
111
-
112
- function hasEnvTokenCredential(env = process.env) {
113
- return !!(
114
- String(env.OPENYIDA_ACCESS_TOKEN || '').trim() ||
115
- String(env.OPENYIDA_REFRESH_TOKEN || '').trim()
116
- );
117
- }
118
-
119
- function hasRuntimeProvisionedAccessToken(env = process.env) {
120
- return isTruthyEnv(env.YIDA_AUTH_ENABLED) &&
121
- !!String(env.OPENYIDA_ACCESS_TOKEN || '').trim();
122
- }
123
-
124
- function buildRuntimeProvisionedLoginStatus(env = process.env) {
125
- return {
126
- ok: true,
127
- auth_mode: 'token',
128
- auth_source: 'env',
129
- auth_store: 'host_injected',
130
- corp_id: env.OPENYIDA_TOKEN_CORP_ID,
131
- corp_name: env.OPENYIDA_TOKEN_CORP_NAME,
132
- user_id: env.OPENYIDA_TOKEN_USER_ID,
133
- user_auth_store_writable: null,
134
- persistence_scope: 'host',
135
- status: 'ok',
136
- can_auto_use: true,
137
- };
138
- }
139
-
140
108
  function buildRuntimeSnapshot(projectRoot) {
141
109
  const detected = typeof coreUtils.detectRuntimeCapabilities === 'function'
142
110
  ? coreUtils.detectRuntimeCapabilities()
@@ -204,18 +172,12 @@ function forbiddenAliasBriefs(manifest) {
204
172
  }
205
173
 
206
174
  function buildAuthPath(login, env = process.env) {
207
- const hostInjectedTokenMode = isTruthyEnv(env.YIDA_AUTH_ENABLED);
208
- const envTokenPresent = hasEnvTokenCredential(env);
209
- const authSource = login.auth_source || (hostInjectedTokenMode || envTokenPresent ? 'env' : 'token_session');
210
- const hostTokenEnvDetected = hostInjectedTokenMode || envTokenPresent || authSource === 'env';
211
- const authStore = login.auth_store || (hostTokenEnvDetected ? 'host_injected' : undefined);
212
- const runtimeAuthProvisioned = hasRuntimeProvisionedAccessToken(env) &&
213
- authSource === 'env' &&
214
- login.status === 'ok';
175
+ const authSource = login.auth_source || 'token_session';
176
+ const envTokenSession = authSource === 'env' || login.failure_reason === 'env_token_missing';
215
177
  const authPath = {
216
178
  mode: login.auth_mode || 'token',
217
179
  source: authSource,
218
- store: authStore,
180
+ store: login.auth_store,
219
181
  auth_profile: login.auth_profile,
220
182
  corp_id: login.corp_id,
221
183
  corp_name: login.corp_name,
@@ -227,12 +189,9 @@ function buildAuthPath(login, env = process.env) {
227
189
  next_step: login.next_step,
228
190
  next_step_commands: login.next_step_commands,
229
191
  can_auto_use: login.can_auto_use === true,
230
- host_injected_token_mode: hostInjectedTokenMode,
231
- host_token_env_detected: hostTokenEnvDetected,
232
- env_token_present: envTokenPresent,
233
- interactive_login_allowed: !hostTokenEnvDetected,
192
+ interactive_login_allowed: !envTokenSession,
234
193
  browser_session_auth_allowed: false,
235
- auth_runtime: 'token_oauth_session',
194
+ auth_runtime: envTokenSession ? 'env_token_bootstrap' : 'token_oauth_session',
236
195
  cookie_auth_supported: false,
237
196
  cookie_check_required: false,
238
197
  playwright_cookie_check_required: false,
@@ -243,13 +202,10 @@ function buildAuthPath(login, env = process.env) {
243
202
  'qr_login',
244
203
  'cookie_cache',
245
204
  ],
246
- missing_token_action: hostInjectedTokenMode
247
- ? 'STOP_AND_REQUEST_HOST_TOKEN'
205
+ missing_token_action: envTokenSession
206
+ ? 'STOP_AND_REQUEST_ENV_TOKEN'
248
207
  : 'RUN_OPENYIDA_LOGIN_IF_USER_APPROVES',
249
208
  };
250
- if (runtimeAuthProvisioned) {
251
- authPath.runtime_auth_provisioned = true;
252
- }
253
209
  return authPath;
254
210
  }
255
211
 
@@ -261,14 +217,14 @@ function buildInteractiveLogin(auth, runtime) {
261
217
  playwright_required: false,
262
218
  };
263
219
 
264
- if (auth.host_token_env_detected) {
220
+ if (!auth.interactive_login_allowed) {
265
221
  return {
266
222
  mode: 'not_required',
267
223
  browser_default: 'not_required',
268
224
  browser_owner: 'none',
269
225
  recommended_command: null,
270
226
  agent_action: 'do_not_run_oauth_login',
271
- reason: auth.env_token_present ? 'host_token_env_detected' : 'host_token_required',
227
+ reason: auth.source === 'env' ? 'env_token_bootstrap' : 'login_not_required',
272
228
  ...base,
273
229
  };
274
230
  }
@@ -306,7 +262,7 @@ function buildInteractiveLogin(auth, runtime) {
306
262
  browser_default: 'unsupported',
307
263
  browser_owner: 'none',
308
264
  recommended_command: null,
309
- agent_action: 'ask_user_for_browser_or_host_token',
265
+ agent_action: 'ask_user_for_browser_access',
310
266
  reason: 'no_desktop_shell_or_agent_browser_detected',
311
267
  ...base,
312
268
  };
@@ -314,7 +270,7 @@ function buildInteractiveLogin(auth, runtime) {
314
270
 
315
271
  function buildBuilderPath(login, projectRoot, manifest, env = process.env, runtimeSnapshot = null) {
316
272
  const auth = buildAuthPath(login, env);
317
- const canTrustSummaryPreflight = auth.can_auto_use === true || auth.host_injected_token_mode === true;
273
+ const canTrustSummaryPreflight = auth.can_auto_use === true;
318
274
  const runtime = runtimeSnapshot || buildRuntimeSnapshot(projectRoot);
319
275
 
320
276
  return {
@@ -352,10 +308,10 @@ function buildBuilderPath(login, projectRoot, manifest, env = process.env, runti
352
308
  skip_help_discovery_default: true,
353
309
  skip_env_noise_default: true,
354
310
  skip_login_check_only_default: canTrustSummaryPreflight,
355
- skip_browser_login_default: auth.host_token_env_detected,
311
+ skip_browser_login_default: !auth.interactive_login_allowed,
356
312
  skip_cookie_or_playwright_checks_default: true,
357
313
  default_app_list_policy: 'skip_when_bound_app_type_unique',
358
- stop_when_host_token_missing: auth.host_injected_token_mode && !auth.env_token_present,
314
+ stop_when_env_token_missing: auth.missing_token_action === 'STOP_AND_REQUEST_ENV_TOKEN' && !auth.can_auto_use,
359
315
  },
360
316
  command_contract: {
361
317
  command_prefix: manifest.command_prefix,
@@ -437,9 +393,6 @@ function compactBuilderPath(builderPath) {
437
393
  next_step: builderPath.auth.next_step,
438
394
  next_step_commands: builderPath.auth.next_step_commands,
439
395
  can_auto_use: builderPath.auth.can_auto_use,
440
- host_injected_token_mode: builderPath.auth.host_injected_token_mode,
441
- host_token_env_detected: builderPath.auth.host_token_env_detected,
442
- env_token_present: builderPath.auth.env_token_present,
443
396
  interactive_login_allowed: builderPath.auth.interactive_login_allowed,
444
397
  browser_session_auth_allowed: builderPath.auth.browser_session_auth_allowed,
445
398
  auth_runtime: builderPath.auth.auth_runtime,
@@ -449,10 +402,6 @@ function compactBuilderPath(builderPath) {
449
402
  qr_login_required: builderPath.auth.qr_login_required,
450
403
  missing_token_action: builderPath.auth.missing_token_action,
451
404
  };
452
- if (builderPath.auth.runtime_auth_provisioned === true) {
453
- auth.runtime_auth_provisioned = true;
454
- }
455
-
456
405
  return {
457
406
  schema_version: builderPath.schema_version,
458
407
  preflight: builderPath.preflight,
@@ -465,7 +414,7 @@ function compactBuilderPath(builderPath) {
465
414
  skip_login_check_only_default: environment.skip_login_check_only_default,
466
415
  skip_browser_login_default: environment.skip_browser_login_default,
467
416
  skip_cookie_or_playwright_checks_default: environment.skip_cookie_or_playwright_checks_default,
468
- stop_when_host_token_missing: environment.stop_when_host_token_missing,
417
+ stop_when_env_token_missing: environment.stop_when_env_token_missing,
469
418
  default_app_list_policy: environment.default_app_list_policy,
470
419
  },
471
420
  command_contract: {
@@ -541,43 +490,6 @@ function buildCommandManifestDigest(manifest) {
541
490
 
542
491
  function buildAgentCapabilitiesSummary() {
543
492
  const manifest = buildCommandManifest({ t, version });
544
-
545
- if (hasRuntimeProvisionedAccessToken(process.env)) {
546
- const projectResolution = resolveProjectRootCompat();
547
- const projectRoot = projectResolution.projectRoot;
548
- const skillsDiagnostics = compactSkillsDiagnostics(buildSkillsDiagnosticsCompat(projectResolution));
549
- const loginStatus = buildRuntimeProvisionedLoginStatus();
550
- const runtime = buildRuntimeSnapshot(projectRoot);
551
- const builderPath = compactBuilderPath(
552
- buildBuilderPath(loginStatus, projectRoot, manifest, process.env, runtime)
553
- );
554
-
555
- return {
556
- schema_version: 1,
557
- name: 'openyida-agent-capabilities-summary',
558
- version,
559
- login: compactLogin(loginStatus),
560
- precheck: {
561
- skipped: true,
562
- reason: 'runtime_auth_provisioned',
563
- },
564
- workdir: projectRoot,
565
- workdir_exists: fs.existsSync(projectRoot),
566
- workdir_source: projectResolution.source,
567
- workdir_reason: projectResolution.reason,
568
- project_root: compactProjectRootResolution(projectResolution),
569
- cache_dir: path.join(projectRoot, '.cache'),
570
- openyida_task_cache_dir: path.join(projectRoot, '.cache', 'openyida'),
571
- skills: skillsDiagnostics,
572
- command_manifest_digest: buildCommandManifestDigest(manifest),
573
- command_manifest_digest_algorithm: 'sha256',
574
- command_count: manifest.summary.command_count,
575
- full_capabilities_command: 'openyida agent-capabilities --json',
576
- runtime,
577
- builder_path: builderPath,
578
- };
579
- }
580
-
581
493
  const envSnapshot = buildEnvironmentSnapshot();
582
494
  const projectResolution = resolveProjectRootCompat({
583
495
  projectRoot: envSnapshot.active.projectRoot,
@@ -306,7 +306,7 @@ Examples:
306
306
  exec_failed: '\n❌ Execution failed: {0}',
307
307
  login_usage: 'Usage: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
308
308
  login_example: 'Examples:\n openyida login # Automatically open the browser via OAuth loopback login\n openyida login --no-browser # Let the caller handle the authorization URL\n openyida login --check-only --json # Check token auth status only\n openyida login --intl # Login against the international environment\n OPENYIDA_NO_BROWSER=1 openyida login # Suppress auto-opening the browser with an environment variable\n openyida auth login # Login alias',
309
- login_unsupported_option: 'Removed legacy login option is no longer supported: {0}. Use token/OAuth login; runtime-environment injected mode only checks the injected token.',
309
+ login_unsupported_option: 'Removed legacy login option is no longer supported: {0}. Use token/OAuth login; env token mode reads only OPENYIDA_* tokens.',
310
310
  auth_usage: 'Usage: openyida auth <status|login|refresh|logout|profiles|profile switch>',
311
311
  auth_example: 'Examples:\n openyida auth status # View login status\n openyida auth profiles # List existing login profiles\n openyida auth profile switch <auth_profile> # Switch current project to an existing profile\n openyida auth login # Add a profile when the target does not exist\n openyida auth refresh # Refresh login session\n openyida auth logout # Unbind current project auth\n openyida auth logout --profile <auth_profile> # Delete a shared profile explicitly',
312
312
  org_usage: 'Usage: openyida org <list|switch> [--json] [--corp-id <corpId>]',
@@ -307,7 +307,7 @@ openyida - 宜搭命令行工具
307
307
  exec_failed: '\n❌ 执行失败: {0}',
308
308
  login_usage: '用法: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
309
309
  login_example: '示例:\n openyida login # 通过 OAuth loopback 自动打开浏览器登录\n openyida login --no-browser # 不自动打开浏览器,由调用方接管授权链接\n openyida login --check-only --json # 只检查 token 登录态\n openyida login --intl # 使用国际站环境登录\n OPENYIDA_NO_BROWSER=1 openyida login # 通过环境变量抑制自动打开浏览器\n openyida auth login # 登录入口别名',
310
- login_unsupported_option: '已删除的旧登录参数不再支持: {0}。请使用 token/OAuth 登录;运行环境注入模式下只检查运行环境注入的 token。',
310
+ login_unsupported_option: '已删除的旧登录参数不再支持: {0}。请使用 token/OAuth 登录;env token 模式下只读取 OPENYIDA_* token。',
311
311
  first_run_title: ' 🤖 OpenYida - AI 问答模式已开启! ',
312
312
  first_run_welcome: ' {0}欢迎首次使用 OpenYida!{1} 以下是快速上手指南:',
313
313
  first_run_way1_title: ' 📝 方式一:直接描述需求',
@@ -18,6 +18,7 @@ const { CliError } = require('./cli-error');
18
18
  const { createAuthRef, createYidaClient, isAuthRefReady } = require('./yida-client');
19
19
 
20
20
  const { buildComponentAliasMaps } = require('../app/get-schema');
21
+ const { getProcessCodeFromFormBinding } = require('../app/services/form-mode-service');
21
22
 
22
23
  const USAGE = `openyida data - Unified Yida data CLI
23
24
 
@@ -25,6 +26,7 @@ Usage:
25
26
  openyida data query form <appType> <formUuid> [--page N] [--size N] [--all] [--max-pages N] [--search-json JSON|--search-file .cache/openyida/search.json] [--resolve-aliases] [--inst-id ID] [--no-hydrate-subforms]
26
27
  openyida data get form <appType> --inst-id <formInstId> [--form-uuid <formUuid>] [--no-hydrate-subforms]
27
28
  openyida data create form <appType> <formUuid> (--data-json <JSON>|--data-file .cache/openyida/data.json) [--dept-id ID] [--resolve-aliases]
29
+ 说明:若目标表单为流程表单,会自动使用 /v1/process/startInstance.json 发起流程。
28
30
  openyida data update form <appType> --inst-id <formInstId> (--data-json <JSON>|--data-file .cache/openyida/data.json) [--form-uuid <formUuid>] [--use-latest-version y] [--resolve-aliases]
29
31
  openyida data query subform <appType> <formUuid> --inst-id <formInstId> --table-field-id <fieldId|alias> [--page N] [--size N] [--resolve-aliases]
30
32
 
@@ -592,6 +594,15 @@ async function getForm(positionals, options, session) {
592
594
  }));
593
595
  }
594
596
 
597
+ async function resolveProcessCode(session, appType, formUuid) {
598
+ try {
599
+ return await getProcessCodeFromFormBinding(session, appType, formUuid);
600
+ } catch (err) {
601
+ console.warn(`⚠️ 无法判断表单 ${formUuid} 是否为流程表单,将使用 saveFormData 提交:${err.message}`);
602
+ return null;
603
+ }
604
+ }
605
+
595
606
  async function createForm(positionals, options, session) {
596
607
  requirePositionals(positionals, 2, ['appType', 'formUuid']);
597
608
  const dataJson = requireJsonOption(options, 'data_json', 'data_file', '数据');
@@ -603,6 +614,16 @@ async function createForm(positionals, options, session) {
603
614
  formDataJson: translateJsonWithAliases(dataJson, aliasContext, translateFormDataObject),
604
615
  };
605
616
  if (options.dept_id) {params.deptId = options.dept_id;}
617
+
618
+ const processCode = await resolveProcessCode(session, appType, formUuid);
619
+ if (processCode) {
620
+ printResult(await sendPost(session, appType, `/dingtalk/web/${appType}/v1/process/startInstance.json`, {
621
+ ...params,
622
+ processCode,
623
+ }));
624
+ return;
625
+ }
626
+
606
627
  printResult(await sendPost(session, appType, `/dingtalk/web/${appType}/v1/form/saveFormData.json`, params));
607
628
  }
608
629
 
package/lib/core/utils.js CHANGED
@@ -454,13 +454,9 @@ function getNodeExecutable() {
454
454
  return 'node';
455
455
  }
456
456
 
457
- function isInjectedAuthMode(env = process.env) {
458
- const authEnabled = String(env.YIDA_AUTH_ENABLED || '').trim().toLowerCase();
459
- return ['1', 'true', 'yes', 'on'].includes(authEnabled);
460
- }
461
-
462
457
  function isEnvAuthMode(env = process.env) {
463
- return isInjectedAuthMode(env);
458
+ const { isEnvTokenAuthMode } = require('../auth/token-store');
459
+ return isEnvTokenAuthMode(env);
464
460
  }
465
461
 
466
462
  function isTokenAuthMode(_env = process.env) {
@@ -942,7 +938,7 @@ function loadCookieData(projectRoot, defaultBaseUrl) {
942
938
 
943
939
  /**
944
940
  * 读取当前默认登录态。
945
- * 默认使用 OAuth token session;YIDA_AUTH_ENABLED=true 时仅使用运行环境注入 token。
941
+ * 默认使用 OAuth token session;env token mode 时仅使用项目缓存或运行环境注入 token。
946
942
  * @param {string} [projectRoot]
947
943
  * @param {string} [defaultBaseUrl]
948
944
  * @returns {object|null}
@@ -961,7 +957,7 @@ function getAuthStatus(options = {}) {
961
957
  // ── 登录触发 ──────────────────────────────────────────
962
958
 
963
959
  /**
964
- * 校验默认登录态;host mode 下不会触发 OAuth,本地模式提示用户执行 openyida login。
960
+ * 校验默认登录态;env token mode 下不会触发 OAuth,本地模式提示用户执行 openyida login。
965
961
  * @param {object} [options]
966
962
  * @param {boolean} [options.force=false] - 是否跳过本地缓存,强制重新登录
967
963
  * @returns {object} loginResult
@@ -977,11 +973,12 @@ function triggerLogin(options = {}) {
977
973
  : 'env_token_missing';
978
974
  const { CliError } = require('./cli-error');
979
975
  throw new CliError(
980
- `not_logged_in: host-injected token is unavailable (${reason}). Ask the host to inject an OpenYida token.`,
976
+ `not_logged_in: env token bootstrap is unavailable (${reason}). Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.`,
981
977
  {
982
- code: 'INJECTED_AUTH_REQUIRED',
978
+ code: 'ENV_TOKEN_AUTH_REQUIRED',
983
979
  details: {
984
980
  authMode: 'token',
981
+ authSource: 'env',
985
982
  failure_reason: reason,
986
983
  },
987
984
  }
@@ -1462,6 +1459,14 @@ function normalizeRefreshedTokenAuthData(refreshResult, fallbackBaseUrl) {
1462
1459
  }
1463
1460
 
1464
1461
  function tokenAuthRequiredResult() {
1462
+ if (isEnvAuthMode()) {
1463
+ return {
1464
+ success: false,
1465
+ __needLogin: true,
1466
+ errorCode: 'TOKEN_AUTH_REQUIRED',
1467
+ errorMsg: 'not_logged_in: env token bootstrap is unavailable. Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.',
1468
+ };
1469
+ }
1465
1470
  return {
1466
1471
  success: false,
1467
1472
  __needLogin: true,
@@ -1502,7 +1507,6 @@ module.exports = {
1502
1507
  getNpmExecutable,
1503
1508
  getNodeExecutable,
1504
1509
  resolveWukongWorkspaceRoot,
1505
- isInjectedAuthMode,
1506
1510
  isEnvAuthMode,
1507
1511
  isTokenAuthMode,
1508
1512
  };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * matrix-service.js - 宜搭权限矩阵查询服务
3
+ *
4
+ * 提供权限矩阵列表查询与单个矩阵详情查询,供 save-permission 等命令使用。
5
+ */
6
+ 'use strict';
7
+
8
+ const { createAuthRef, createYidaClient } = require('../core/yida-client');
9
+
10
+ /**
11
+ * 查询权限矩阵列表
12
+ *
13
+ * @param {object} authRef - 认证引用对象
14
+ * @param {object} options - 查询选项
15
+ * @param {string} [options.keyword] - 搜索关键词
16
+ * @param {number} [options.page=1] - 页码
17
+ * @param {number} [options.limit=10] - 每页条数
18
+ * @returns {Promise<Array>} 权限矩阵列表
19
+ */
20
+ async function getMatrixList(authRef, options = {}) {
21
+ const ref = authRef || createAuthRef();
22
+ const { keyword = '', page = 1, limit = 10 } = options;
23
+ const client = createYidaClient({ authRef: ref });
24
+ const result = await client.getContent('/query/matrix/getMatrixList.json', {
25
+ keyword,
26
+ page,
27
+ limit,
28
+ }, {
29
+ action: 'getMatrixList',
30
+ failMessage: '获取权限矩阵列表失败',
31
+ });
32
+ return (result && result.data) || [];
33
+ }
34
+
35
+ /**
36
+ * 根据 ID 查询单个权限矩阵详情
37
+ *
38
+ * @param {object} authRef - 认证引用对象
39
+ * @param {string} matrixId - 权限矩阵 ID
40
+ * @returns {Promise<object>} 权限矩阵详情
41
+ */
42
+ async function getMatrixById(authRef, matrixId) {
43
+ const ref = authRef || createAuthRef();
44
+ const client = createYidaClient({ authRef: ref });
45
+ return client.getContent('/query/matrix/getMatrixById.json', {
46
+ matrixId,
47
+ }, {
48
+ action: 'getMatrixById',
49
+ failMessage: `获取权限矩阵 ${matrixId} 详情失败`,
50
+ });
51
+ }
52
+
53
+ module.exports = {
54
+ getMatrixList,
55
+ getMatrixById,
56
+ };
@@ -6,6 +6,7 @@
6
6
  * openyida save-permission <appType> <formUuid> --action-permission <json>
7
7
  * openyida save-permission <appType> <formUuid> --field-permission <json>
8
8
  * openyida save-permission <appType> <formUuid> --members <userIds> --data-permission <json>
9
+ * openyida save-permission <appType> <formUuid> --matrix <json> --data-permission <json>
9
10
  *
10
11
  * 用法(新增权限组):
11
12
  * openyida save-permission <appType> <formUuid> --create --name <权限组名称> [--members <userIds>] [--data-permission <json>] [--action-permission <json>] [--field-permission <json>]
@@ -13,6 +14,9 @@
13
14
  * --members 参数:指定权限组成员,多个钉钉 userId 用逗号分隔
14
15
  * 示例:--members "54255850977641,12345678901234"
15
16
  * 不传则保持原有成员配置不变(更新模式)或仅包含管理员(新增模式)
17
+ * --all-members 参数:新增/更新权限组时设置为「全员可见」(roleType=DEFAULT, roleValue=ALL)
18
+ * --matrix 参数:使用权限矩阵作为权限成员,JSON 格式 {"matrixId":"MATRIX-XXX","columnId":"column_YYY"}
19
+ * 与 --members / --all-members 互斥
16
20
  *
17
21
  * 注意:--field-permission 透传宜搭 fieldPermit 原始 JSON,使用前建议先通过 get-permission 查看现有结构。
18
22
  */
@@ -39,6 +43,7 @@ const DATA_RANGE_TO_PERMIT_TYPE = {
39
43
  FREE_LOGIN: 'FREE_LOGIN',
40
44
  CUSTOM_DEPARTMENT: 'CUSTOM_DEPARTMENT',
41
45
  FORMULA: 'FORMULA',
46
+ MATRIX: 'MATRIX',
42
47
  };
43
48
 
44
49
  // 所有支持的操作权限 key
@@ -62,9 +67,11 @@ const VALID_OPERATE_KEYS = [
62
67
  function parseArgs(args) {
63
68
  if (args.length < 2) {
64
69
  throw new CliError([
65
- '用法: openyida save-permission <appType> <formUuid> [--create --name <名称>] [--data-permission <json>] [--action-permission <json>] [--field-permission <json>] [--members <userIds>]',
70
+ '用法: openyida save-permission <appType> <formUuid> [--create --name <名称>] [--data-permission <json>] [--action-permission <json>] [--field-permission <json>] [--members <userIds>] [--all-members] [--matrix <json>]',
66
71
  '示例(更新): openyida save-permission APP_XXX FORM-XXX --data-permission \'{"role":"DEFAULT","dataRange":"SELF"}\'',
67
- '示例(新增): openyida save-permission APP_XXX FORM-XXX --create --name "只读权限组" --members "54255850977641"',
72
+ '示例(新增全员): openyida save-permission APP_XXX FORM-XXX --create --name "全部人员看全部数据" --all-members --data-permission \'{"dataRange":"ALL"}\'',
73
+ '示例(新增指定人员): openyida save-permission APP_XXX FORM-XXX --create --name "只读权限组" --members "54255850977641"',
74
+ '示例(新增矩阵): openyida save-permission APP_XXX FORM-XXX --create --name "矩阵权限组" --matrix \'{"matrixId":"MATRIX-XXX","columnId":"column_YYY"}\' --data-permission \'{"rule":[{"type":"ORIGINATOR","value":"y"},{"type":"MATRIX","value":"y"}]}\'',
68
75
  ].join('\n'), {
69
76
  code: 'SAVE_PERMISSION_INVALID_ARGUMENTS',
70
77
  });
@@ -76,6 +83,8 @@ function parseArgs(args) {
76
83
  let actionPermission = null;
77
84
  let fieldPermission = null;
78
85
  let members = null;
86
+ let allMembers = false;
87
+ let matrix = null;
79
88
  let createMode = false;
80
89
  let groupName = null;
81
90
 
@@ -116,6 +125,17 @@ function parseArgs(args) {
116
125
  // 多个钉钉 userId 用逗号分隔,如 "54255850977641,12345678901234"
117
126
  members = args[index + 1].split(',').map((id) => id.trim()).filter(Boolean);
118
127
  index++;
128
+ } else if (args[index] === '--all-members') {
129
+ allMembers = true;
130
+ } else if (args[index] === '--matrix' && args[index + 1]) {
131
+ try {
132
+ matrix = JSON.parse(args[index + 1]);
133
+ } catch {
134
+ throw new CliError(`--matrix 参数 JSON 解析失败: ${args[index + 1]},格式: {"matrixId":"MATRIX-XXX","columnId":"column_YYY"}`, {
135
+ code: 'SAVE_PERMISSION_INVALID_ARGUMENTS',
136
+ });
137
+ }
138
+ index++;
119
139
  }
120
140
  }
121
141
 
@@ -125,16 +145,39 @@ function parseArgs(args) {
125
145
  });
126
146
  }
127
147
 
128
- if (!createMode && !dataPermission && !actionPermission && !fieldPermission && !members) {
129
- throw new CliError('请至少提供 --data-permission、--action-permission、--field-permission 或 --members 参数之一', {
148
+ if (!createMode && !dataPermission && !actionPermission && !fieldPermission && !members && !matrix) {
149
+ throw new CliError('请至少提供 --data-permission、--action-permission、--field-permission、--members 或 --matrix 参数之一', {
130
150
  code: 'SAVE_PERMISSION_INVALID_ARGUMENTS',
131
151
  });
132
152
  }
133
153
 
134
- return { appType, formUuid, dataPermission, actionPermission, fieldPermission, members, createMode, groupName };
154
+ return { appType, formUuid, dataPermission, actionPermission, fieldPermission, members, allMembers, matrix, createMode, groupName };
155
+ }
156
+
157
+ function buildDataPermit(dataPermission) {
158
+ if (dataPermission && Array.isArray(dataPermission.rule)) {
159
+ return JSON.stringify(dataPermission);
160
+ }
161
+ const dataRange = (dataPermission && dataPermission.dataRange) || 'ALL';
162
+ const permitType = DATA_RANGE_TO_PERMIT_TYPE[dataRange] || dataRange;
163
+ return JSON.stringify({ rule: [{ type: permitType, value: 'y' }] });
135
164
  }
136
165
 
137
166
  function validateDataPermission(dataPermission) {
167
+ if (dataPermission && Array.isArray(dataPermission.rule)) {
168
+ const validTypes = new Set([
169
+ ...Object.keys(DATA_RANGE_TO_PERMIT_TYPE),
170
+ ...Object.values(DATA_RANGE_TO_PERMIT_TYPE),
171
+ ]);
172
+ for (const item of dataPermission.rule) {
173
+ if (item.type && !validTypes.has(item.type)) {
174
+ throw new Error(
175
+ `无效的 rule type: ${item.type},有效值: ${Array.from(validTypes).join(', ')}`
176
+ );
177
+ }
178
+ }
179
+ return;
180
+ }
138
181
  const validRanges = Object.keys(DATA_RANGE_TO_PERMIT_TYPE);
139
182
  if (dataPermission.dataRange && !validRanges.includes(dataPermission.dataRange)) {
140
183
  throw new Error(
@@ -143,6 +186,18 @@ function validateDataPermission(dataPermission) {
143
186
  }
144
187
  }
145
188
 
189
+ function validateMatrix(matrix) {
190
+ if (!matrix || typeof matrix !== 'object') {
191
+ throw new Error('--matrix 参数必须是 JSON 对象');
192
+ }
193
+ if (!matrix.matrixId || typeof matrix.matrixId !== 'string') {
194
+ throw new Error('--matrix 参数必须包含 matrixId 字符串');
195
+ }
196
+ if (!matrix.columnId || typeof matrix.columnId !== 'string') {
197
+ throw new Error('--matrix 参数必须包含 columnId 字符串');
198
+ }
199
+ }
200
+
146
201
  function validateActionPermission(actionPermission) {
147
202
  if (!actionPermission.operations || typeof actionPermission.operations !== 'object') {
148
203
  throw new Error(
@@ -280,7 +335,7 @@ function savePermitPackage(appType, formUuid, permitPackage, overrideMembers, au
280
335
  }
281
336
 
282
337
  async function run(args) {
283
- const { appType, formUuid, dataPermission, actionPermission, fieldPermission, members, createMode, groupName } = parseArgs(args);
338
+ const { appType, formUuid, dataPermission, actionPermission, fieldPermission, members, allMembers, matrix, createMode, groupName } = parseArgs(args);
284
339
 
285
340
  warn(SEP);
286
341
  warn(' save-permission - 宜搭表单权限配置保存');
@@ -296,10 +351,17 @@ async function run(args) {
296
351
  // Step 0: 参数校验
297
352
  warn('\n📋 Step 0: 验证参数');
298
353
  try {
354
+ if (matrix && (members || allMembers)) {
355
+ throw new Error('--matrix 与 --members / --all-members 互斥,请勿同时指定');
356
+ }
299
357
  if (dataPermission) {
300
358
  validateDataPermission(dataPermission);
301
359
  warn(` ✅ 数据权限验证通过(dataRange: ${dataPermission.dataRange || 'ALL'})`);
302
360
  }
361
+ if (matrix) {
362
+ validateMatrix(matrix);
363
+ warn(` ✅ 权限矩阵验证通过(matrixId: ${matrix.matrixId}, columnId: ${matrix.columnId})`);
364
+ }
303
365
  if (actionPermission) {
304
366
  validateActionPermission(actionPermission);
305
367
  warn(' ✅ 操作权限验证通过');
@@ -335,6 +397,7 @@ async function run(args) {
335
397
  // 构建新权限组数据
336
398
  const dataRange = (dataPermission && dataPermission.dataRange) || 'ALL';
337
399
  const permitType = DATA_RANGE_TO_PERMIT_TYPE[dataRange] || dataRange;
400
+ const dataPermitStr = buildDataPermit(dataPermission);
338
401
 
339
402
  const newOperatePermit = {};
340
403
  if (actionPermission) {
@@ -346,10 +409,19 @@ async function run(args) {
346
409
  newOperatePermit['OPERATE_VIEW'] = 'y';
347
410
  }
348
411
 
349
- // 构建 roleData:默认包含管理员,如果指定了 members 则追加 PERSONS 条目
350
- const roleInclude = [{ roleType: 'MANAGER', roleValue: 'appMainAdminRole,corpAdminRole' }];
351
- if (members && members.length > 0) {
352
- roleInclude.push({ roleType: 'PERSONS', roleValue: members.join(',') });
412
+ // 构建 roleData:--matrix / --all-members / --members / 默认管理员 四选一
413
+ let roleInclude;
414
+ if (matrix) {
415
+ roleInclude = [{ roleType: 'MATRIX', roleValue: [{ matrixId: matrix.matrixId, columnId: matrix.columnId }] }];
416
+ } else if (allMembers) {
417
+ roleInclude = [{ roleType: 'DEFAULT', roleValue: 'ALL' }];
418
+ } else if (members && members.length > 0) {
419
+ roleInclude = [
420
+ { roleType: 'MANAGER', roleValue: 'appMainAdminRole,corpAdminRole' },
421
+ { roleType: 'PERSONS', roleValue: members.join(',') },
422
+ ];
423
+ } else {
424
+ roleInclude = [{ roleType: 'MANAGER', roleValue: 'appMainAdminRole,corpAdminRole' }];
353
425
  }
354
426
 
355
427
  const newPkg = {
@@ -357,7 +429,7 @@ async function run(args) {
357
429
  packageName: { zh_CN: groupName, en_US: groupName, type: 'i18n' },
358
430
  description: { zh_CN: groupName, en_US: groupName, type: 'i18n' },
359
431
  roleData: JSON.stringify({ include: roleInclude }),
360
- dataPermit: JSON.stringify({ rule: [{ type: permitType, value: 'y' }] }),
432
+ dataPermit: dataPermitStr,
361
433
  operatePermit: JSON.stringify(newOperatePermit),
362
434
  customButtonPermit: '[]',
363
435
  fieldPermit: JSON.stringify(normalizedFieldPermission || { fieldRange: 'FORM' }),
@@ -366,10 +438,18 @@ async function run(args) {
366
438
  };
367
439
 
368
440
  warn(` → 权限组名称: ${groupName}`);
369
- warn(` → 数据范围: ${dataRange} → ${permitType}`);
441
+ if (dataPermission && Array.isArray(dataPermission.rule)) {
442
+ warn(` → 数据范围: 自定义规则(${dataPermission.rule.length} 条)`);
443
+ } else {
444
+ warn(` → 数据范围: ${dataRange} → ${permitType}`);
445
+ }
370
446
  warn(` → 操作权限: ${Object.keys(newOperatePermit).join(', ') || '(无)'}`);
371
447
  if (normalizedFieldPermission) {warn(' → 字段权限: 自定义 fieldPermit');}
372
- if (members) {warn(` → 成员: ${members.join(', ')}`);}
448
+ if (matrix) {
449
+ warn(` → 权限矩阵: ${matrix.matrixId} / ${matrix.columnId}`);
450
+ } else if (members) {
451
+ warn(` → 成员: ${members.join(', ')}`);
452
+ }
373
453
 
374
454
  const createResult = await savePermitPackage(appType, formUuid, newPkg, null, authRef);
375
455
 
@@ -378,15 +458,28 @@ async function run(args) {
378
458
  const newPackageUuid = createResult.content || '';
379
459
  warn(' ✅ 权限组新增成功!');
380
460
  warn(SEP);
461
+ const dataPermissionSummary = (dataPermission && Array.isArray(dataPermission.rule))
462
+ ? `数据范围: 自定义规则(${dataPermission.rule.length} 条)`
463
+ : `数据范围: ${dataRange}`;
464
+ let membersSummary;
465
+ if (matrix) {
466
+ membersSummary = `权限矩阵: ${matrix.matrixId} / ${matrix.columnId}`;
467
+ } else if (allMembers) {
468
+ membersSummary = '成员: 全员';
469
+ } else if (members) {
470
+ membersSummary = `成员: ${members.join(', ')}`;
471
+ } else {
472
+ membersSummary = '仅管理员';
473
+ }
381
474
  console.log(JSON.stringify({
382
475
  success: true,
383
476
  packageUuid: newPackageUuid,
384
477
  summary: {
385
478
  name: groupName,
386
- dataPermission: `数据范围: ${dataRange}`,
479
+ dataPermission: dataPermissionSummary,
387
480
  actionPermission: `操作权限: ${Object.keys(newOperatePermit).join(', ') || '(无)'}`,
388
481
  fieldPermission: normalizedFieldPermission ? '自定义 fieldPermit' : '全部字段',
389
- members: members ? `成员: ${members.join(', ')}` : '仅管理员',
482
+ members: membersSummary,
390
483
  },
391
484
  message: '权限组已新增',
392
485
  }, null, 2));
@@ -424,7 +517,10 @@ async function run(args) {
424
517
  warn(` ✅ 获取到 ${packages.length} 个权限组`);
425
518
 
426
519
  // 根据 role 筛选要更新的权限组
427
- const targetRole = (dataPermission || actionPermission || fieldPermission || {}).role || 'DEFAULT';
520
+ let targetRole = (dataPermission || actionPermission || fieldPermission || {}).role || 'DEFAULT';
521
+ if (matrix) {
522
+ targetRole = 'MATRIX';
523
+ }
428
524
  const packagesToUpdate = packages.filter((pkg) => {
429
525
  if (targetRole === 'DEFAULT') {
430
526
  return pkg.roleMembers && pkg.roleMembers.some((rm) => rm.roleType === 'DEFAULT');
@@ -432,6 +528,9 @@ async function run(args) {
432
528
  if (targetRole === 'MANAGER') {
433
529
  return pkg.roleMembers && pkg.roleMembers.some((rm) => rm.roleType === 'MANAGER');
434
530
  }
531
+ if (targetRole === 'MATRIX') {
532
+ return pkg.roleMembers && pkg.roleMembers.some((rm) => rm.roleType === 'MATRIX');
533
+ }
435
534
  return true;
436
535
  });
437
536
 
@@ -447,8 +546,12 @@ async function run(args) {
447
546
  let permitType = null;
448
547
  const stepParts = [];
449
548
  if (dataPermission) {
450
- permitType = DATA_RANGE_TO_PERMIT_TYPE[dataPermission.dataRange] || dataPermission.dataRange;
451
- stepParts.push(`数据权限: ${dataPermission.dataRange} → ${permitType}`);
549
+ if (Array.isArray(dataPermission.rule)) {
550
+ stepParts.push(`数据权限: 自定义规则(${dataPermission.rule.length} 条)`);
551
+ } else {
552
+ permitType = DATA_RANGE_TO_PERMIT_TYPE[dataPermission.dataRange] || dataPermission.dataRange;
553
+ stepParts.push(`数据权限: ${dataPermission.dataRange} → ${permitType}`);
554
+ }
452
555
  }
453
556
  if (actionPermission) {
454
557
  stepParts.push('操作权限: 同步更新');
@@ -456,6 +559,9 @@ async function run(args) {
456
559
  if (normalizedFieldPermission) {
457
560
  stepParts.push('字段权限: 同步更新');
458
561
  }
562
+ if (matrix) {
563
+ stepParts.push(`权限矩阵: ${matrix.matrixId} / ${matrix.columnId}`);
564
+ }
459
565
  if (members) {
460
566
  stepParts.push(`成员: ${members.join(', ')}`);
461
567
  }
@@ -469,7 +575,7 @@ async function run(args) {
469
575
  const updatedPkg = { ...pkg };
470
576
 
471
577
  if (dataPermission) {
472
- updatedPkg.dataPermit = JSON.stringify({ rule: [{ type: permitType, value: 'y' }] });
578
+ updatedPkg.dataPermit = buildDataPermit(dataPermission);
473
579
  }
474
580
 
475
581
  if (actionPermission) {
@@ -487,6 +593,18 @@ async function run(args) {
487
593
  updatedPkg.fieldPermit = JSON.stringify(normalizedFieldPermission);
488
594
  }
489
595
 
596
+ // --all-members 时强制将权限组改为全员可见
597
+ if (allMembers) {
598
+ updatedPkg.roleData = JSON.stringify({ include: [{ roleType: 'DEFAULT', roleValue: 'ALL' }] });
599
+ }
600
+
601
+ // --matrix 时强制将权限组改为权限矩阵
602
+ if (matrix) {
603
+ updatedPkg.roleData = JSON.stringify({
604
+ include: [{ roleType: 'MATRIX', roleValue: [{ matrixId: matrix.matrixId, columnId: matrix.columnId }] }],
605
+ });
606
+ }
607
+
490
608
  // members 参数传给 savePermitPackage,null 表示不修改成员
491
609
  const overrideMembers = members || null;
492
610
 
@@ -524,7 +642,9 @@ module.exports = {
524
642
  parseArgs,
525
643
  validateDataPermission,
526
644
  validateActionPermission,
645
+ validateMatrix,
527
646
  normalizeFieldPermission,
647
+ buildDataPermit,
528
648
  fetchPermitPackages,
529
649
  buildRoleData,
530
650
  savePermitPackage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.8.19",
3
+ "version": "2026.8.20",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -110,6 +110,7 @@ openyida data query form <appType> <formUuid> [--page 1 --size 20] [--search-jso
110
110
  openyida data get form <appType> --inst-id <formInstId>
111
111
  openyida data create form <appType> <formUuid> --data-json '<json>' [--resolve-aliases]
112
112
  openyida data create form <appType> <formUuid> --data-file .cache/openyida/<项目名或任务名>/data-import/record.json [--resolve-aliases]
113
+ > `create form` 会自动探测表单类型;当目标表单为流程表单时,会改用 `/v1/process/startInstance.json` 发起流程。若已知 `processCode`,仍推荐显式使用 `create process`。
113
114
  openyida data update form <appType> --inst-id <formInstId> --form-uuid <formUuid> --data-json '<json>' [--resolve-aliases]
114
115
  openyida data update form <appType> --inst-id <formInstId> --form-uuid <formUuid> --data-file .cache/openyida/<项目名或任务名>/data-import/patch.json [--resolve-aliases]
115
116
  openyida data query subform <appType> <formUuid> --inst-id <formInstId> --table-field-id <fieldId|alias> [--page 1 --size 100] [--resolve-aliases]
@@ -55,6 +55,8 @@ openyida save-permission <appType> <formUuid> [选项]
55
55
  | `--action-permission <json>` | 修改操作权限(完全替换,只保留 true 的项) |
56
56
  | `--field-permission <json>` | 修改字段权限,传入宜搭 `fieldPermit` 对象或 `{ "role": "DEFAULT", "fieldPermit": {...} }` |
57
57
  | `--members <userIds>` | 修改成员,多个 userId 逗号分隔 |
58
+ | `--all-members` | 设置权限组为「全员可见」(`roleData.include` 为 `DEFAULT/ALL`) |
59
+ | `--matrix <json>` | 使用权限矩阵作为权限成员,JSON 格式 `{"matrixId":"MATRIX-XXX","columnId":"column_YYY"}`,与 `--members` / `--all-members` 互斥 |
58
60
 
59
61
  ### 数据权限 `dataRange` 可选值
60
62
 
@@ -65,6 +67,42 @@ openyida save-permission <appType> <formUuid> [选项]
65
67
  | `DEPARTMENT` / `ORIGINATOR_DEPARTMENT` | 本部门提交 |
66
68
  | `SAME_LEVEL_DEPARTMENT` | 同级部门 |
67
69
  | `SUBORDINATE_DEPARTMENT` | 下级部门 |
70
+ | `MATRIX` | 权限矩阵条件 |
71
+
72
+ > 如需同时设置多个数据范围、自定义部门或自定义过滤条件,可直接传入宜搭完整的 `dataPermit` JSON(必须包含 `rule` 数组)。例如截图中的「本人提交 + 本部门 + 同级部门 + 下级部门 + 免登 + 自定义部门 + 自定义过滤条件」可表示为:
73
+ >
74
+ > ```json
75
+ > {
76
+ > "rule": [
77
+ > { "type": "ORIGINATOR", "value": "y" },
78
+ > { "type": "ORIGINATOR_DEPARTMENT", "value": "y" },
79
+ > { "type": "SAME_LEVEL_DEPARTMENT", "value": "y" },
80
+ > { "type": "SUBORDINATE_DEPARTMENT", "value": "y" },
81
+ > { "type": "FREE_LOGIN", "value": "y" },
82
+ > { "type": "CUSTOM_DEPARTMENT", "value": "y" },
83
+ > { "type": "FORMULA", "value": "y" }
84
+ > ],
85
+ > "customDepartmentData": {
86
+ > "departmentIds": ["637215248"],
87
+ > "drillDown": "n"
88
+ > },
89
+ > "formulaData": {
90
+ > "condition": "OR",
91
+ > "ruleId": "group-xxx",
92
+ > "rules": []
93
+ > }
94
+ > }
95
+ > ```
96
+ >
97
+ > 命令示例:
98
+ >
99
+ > ```bash
100
+ > openyida save-permission APP_XXX FORM_XXX \
101
+ > --create --name "全部成员可查看本人提交数据" \
102
+ > --all-members \
103
+ > --data-permission '{"rule":[{"type":"ORIGINATOR","value":"y"},{"type":"ORIGINATOR_DEPARTMENT","value":"y"},{"type":"SAME_LEVEL_DEPARTMENT","value":"y"},{"type":"SUBORDINATE_DEPARTMENT","value":"y"},{"type":"FREE_LOGIN","value":"y"},{"type":"CUSTOM_DEPARTMENT","value":"y"},{"type":"FORMULA","value":"y"}],"customDepartmentData":{"departmentIds":["637215248"],"drillDown":"n"},"formulaData":{"condition":"OR","ruleId":"group-xxx","rules":[]}}' \
104
+ > --action-permission '{"operations":{"OPERATE_VIEW":true,"OPERATE_EDIT":true,"OPERATE_DELETE":true,"OPERATE_HISTORY":true,"OPERATE_COMMENT":true,"OPERATE_PRINT":true}}'
105
+ > ```
68
106
 
69
107
  ### 操作权限 key
70
108
 
@@ -79,7 +117,7 @@ openyida save-permission <appType> <formUuid> --create --name <名称> [选项]
79
117
  示例:
80
118
 
81
119
  ```bash
82
- openyida save-permission APP_XXX FORM-XXX \
120
+ openyida save-permission APP_XXX FORM_XXX \
83
121
  --create --name "部门数据查看组" \
84
122
  --members "54255850977641" \
85
123
  --data-permission '{"dataRange":"ORIGINATOR_DEPARTMENT"}' \
@@ -87,6 +125,30 @@ openyida save-permission APP_XXX FORM-XXX \
87
125
  --field-permission '{"fieldRange":"FORM"}'
88
126
  ```
89
127
 
128
+ > 设置「全部人员看全部数据」时,必须加上 `--all-members`,确保 `roleData.include` 为 `DEFAULT/ALL`:
129
+ >
130
+ > ```bash
131
+ > openyida save-permission APP_XXX FORM_XXX \
132
+ > --create --name "全部人员看全部数据" \
133
+ > --all-members \
134
+ > --data-permission '{"dataRange":"ALL"}' \
135
+ > --action-permission '{"operations":{"OPERATE_VIEW":true}}'
136
+ > ```
137
+ >
138
+ > 若目标表单已存在 DEFAULT 权限组,也可直接用 `--all-members --data-permission '{"dataRange":"ALL"}'` 更新该组。
139
+ >
140
+ > ### 使用权限矩阵
141
+ > 1. 先在宜搭后台「权限矩阵」中获取目标矩阵 ID 与结果列 columnId;或调用底层服务 `/query/matrix/getMatrixList.json` / `/query/matrix/getMatrixById.json` 查询。
142
+ > 2. 创建/更新权限组时指定 `--matrix '{"matrixId":"MATRIX-XXX","columnId":"column_YYY"}'`,并配合 `--data-permission` 设置包含 `MATRIX` 的数据范围。
143
+ >
144
+ > ```bash
145
+ > openyida save-permission APP_XXX FORM_XXX \
146
+ > --create --name "使用权限矩阵的权限组" \
147
+ > --matrix '{"matrixId":"MATRIX-XNCVJYB60YW7L0HPY9HE","columnId":"column_1767839664612"}' \
148
+ > --data-permission '{"rule":[{"type":"ORIGINATOR","value":"y"},{"type":"MATRIX","value":"y"}]}' \
149
+ > --action-permission '{"operations":{"OPERATE_VIEW":true,"OPERATE_EDIT":true,"OPERATE_DELETE":true,"OPERATE_HISTORY":true,"OPERATE_COMMENT":true,"OPERATE_PRINT":true}}'
150
+ > ```
151
+
90
152
  ## 字段权限
91
153
 
92
154
  - 默认结构通常是 `{ "fieldRange": "FORM" }`,表示继承表单设计中组件状态。