openyida 2026.10.30 → 2026.10.31

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 (34) hide show
  1. package/lib/app/app-theme-state.js +80 -0
  2. package/lib/app/create-app.js +31 -0
  3. package/lib/app/update-app.js +51 -47
  4. package/lib/auth/qwenwork-desktop.js +2 -1
  5. package/lib/auth/qwenwork-login.js +40 -5
  6. package/lib/core/agent-capabilities.js +48 -10
  7. package/lib/core/command-manifest.js +7 -3
  8. package/lib/core/form-edit-permissions.js +210 -0
  9. package/lib/core/locales/en.js +9 -1
  10. package/lib/core/locales/zh.js +9 -1
  11. package/lib/core/query-data.js +12 -1
  12. package/lib/core/qwenwork-login-guidance.js +4 -1
  13. package/lib/core/utils.js +15 -10
  14. package/lib/permission/permission-scope.js +27 -8
  15. package/lib/permission/save-permission.js +6 -4
  16. package/package.json +1 -1
  17. package/yida-skills/SKILL.md +6 -0
  18. package/yida-skills/references/execution-rules.md +11 -0
  19. package/yida-skills/references/qwenwork-cloud-login.md +2 -0
  20. package/yida-skills/references/qwenwork-desktop-login.md +2 -0
  21. package/yida-skills/references/setup-and-env.md +22 -6
  22. package/yida-skills/references/user-facing-summary.md +23 -3
  23. package/yida-skills/skills/yida-app/SKILL.md +6 -0
  24. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +3 -3
  25. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +1 -1
  26. package/yida-skills/skills/yida-create-app/SKILL.md +7 -1
  27. package/yida-skills/skills/yida-data-management/SKILL.md +4 -1
  28. package/yida-skills/skills/yida-data-management/references/api-matrix.md +4 -0
  29. package/yida-skills/skills/yida-design/references/application-style-library.md +4 -2
  30. package/yida-skills/skills/yida-design/workflow/output-design.md +1 -1
  31. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -1
  32. package/yida-skills/skills/yida-form-permission/SKILL.md +3 -22
  33. package/yida-skills/skills/yida-login/SKILL.md +11 -4
  34. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +2 -0
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const { httpGet, requestWithAutoLogin } = require('../core/utils');
4
+ const { throwCommandError } = require('../core/command-errors');
5
+ const { t } = require('../core/i18n');
6
+ const { isAgentAppType } = require('./agent-app-type');
7
+
8
+ function themeContextFailure(reason) {
9
+ throwCommandError(t('upgrade_app_theme.context_failed'), {
10
+ code: 'APP_THEME_CONTEXT_UNVERIFIED', details: { reason },
11
+ });
12
+ }
13
+
14
+ // The application API exposes persisted mode/provenance. These establish the
15
+ // configured theme, not proof that a browser loaded the stylesheet.
16
+ function resolveAppThemeState(app = {}) {
17
+ const config = app.config && typeof app.config === 'object' && !Array.isArray(app.config) ? app.config : {};
18
+ const mode = app.appThemeMode ?? config.APP_THEME_MODE;
19
+ const enabled = app.appThemeEnable;
20
+ const createdModern = config.CREATED_WITH_MODERN_THEME;
21
+ if (enabled === 'n' || mode === 'legacy' || createdModern === 'n') {
22
+ return { mode: 'legacy', source: 'server_settings' };
23
+ }
24
+ if (enabled === 'y' && mode === 'modern') {
25
+ return { mode: 'modern', source: 'server_theme_state' };
26
+ }
27
+ if (enabled !== undefined && enabled !== null && enabled !== 'y') {
28
+ return { mode: 'unknown', source: 'server_settings' };
29
+ }
30
+ if (createdModern === 'y' &&
31
+ (mode === 'modern' || ((mode === undefined || mode === null || mode === '') && isAgentAppType(app.agentAppType)))) {
32
+ return { mode: 'modern', source: 'server_creation_config' };
33
+ }
34
+ return { mode: 'unknown', source: 'server_settings' };
35
+ }
36
+
37
+ async function fetchCurrentApp(appType, authRef, verifyIdentity = false) {
38
+ const target = { baseUrl: authRef.baseUrl, corpId: authRef.corpId, userId: authRef.userId };
39
+ if (verifyIdentity && (typeof appType !== 'string' || !/^[A-Za-z0-9_-]+$/.test(appType) ||
40
+ typeof target.corpId !== 'string' || !target.corpId || !target.baseUrl)) {
41
+ themeContextFailure('invalid_target');
42
+ }
43
+ const assertIdentity = auth => {
44
+ if (verifyIdentity && Object.keys(target).some(key => auth[key] !== target[key])) {
45
+ themeContextFailure('identity_changed');
46
+ }
47
+ };
48
+ const response = await requestWithAutoLogin((auth) => {
49
+ assertIdentity(auth);
50
+ return httpGet(
51
+ auth.baseUrl,
52
+ `/${encodeURIComponent(appType)}/query/app/getAppIncludingAecpInfo.json`,
53
+ {
54
+ _api: 'nattyFetch',
55
+ _mock: 'false',
56
+ appKey: appType,
57
+ _csrf_token: auth.csrfToken,
58
+ _stamp: Date.now(),
59
+ }
60
+ );
61
+ }, authRef);
62
+ assertIdentity(authRef);
63
+
64
+ if (verifyIdentity) {
65
+ const app = response?.content;
66
+ if (response?.success !== true || !app || typeof app !== 'object' || Array.isArray(app)) {
67
+ themeContextFailure('app_query_failed');
68
+ }
69
+ if (app.appType !== appType || app.corpId !== target.corpId) {
70
+ themeContextFailure('app_identity_mismatch');
71
+ }
72
+ }
73
+
74
+ if (!response || !response.success) {
75
+ throw new Error(response ? response.errorMsg || '查询应用详情失败' : '查询应用详情失败');
76
+ }
77
+ return response.content || {};
78
+ }
79
+
80
+ module.exports = { fetchCurrentApp, resolveAppThemeState };
@@ -22,6 +22,7 @@ const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff
22
22
  const { throwCommandError } = require('../core/command-errors');
23
23
  const { isPermissionGuidanceError } = require('../core/permission-guidance');
24
24
  const { resolveBuilderAiSource } = require('./builder-ai-source');
25
+ const { fetchCurrentApp, resolveAppThemeState } = require('./app-theme-state');
25
26
 
26
27
  const DEFAULT_APP_OPTIONS = {
27
28
  iconColor: '#0089FF',
@@ -376,6 +377,35 @@ async function run(args) {
376
377
  const appUrl = entryUrls.appUrl;
377
378
  const corpId = authRef.corpId || (authRef.authData && authRef.authData.corp_id) || '';
378
379
 
380
+ let themeVerification;
381
+ try {
382
+ const app = await fetchCurrentApp(appType, authRef, true);
383
+ const theme = resolveAppThemeState(app);
384
+ if (theme.mode !== 'modern') {
385
+ throwCommandError(t('create_app.modern_theme_unverified'), {
386
+ code: 'APP_THEME_NOT_MODERN', details: { themeMode: theme.mode },
387
+ });
388
+ }
389
+ themeVerification = { verified: true, appThemeMode: 'modern', source: theme.source,
390
+ scope: 'configuration_readback', runtimeVerified: false };
391
+ } catch (error) {
392
+ const created = { appCreated: true, appType, corpId, url: appUrl, retryCreate: false,
393
+ partial: true, residual: { appType, corpId }, retrySafe: false, mutationPerformed: true,
394
+ sideEffectState: 'applied', readbackAllowed: true };
395
+ if (isPermissionGuidanceError(error)) {
396
+ error.details = { ...error.details, ...created };
397
+ throw error;
398
+ }
399
+ throwCommandError(t('create_app.modern_theme_unverified'), {
400
+ code: 'APP_CREATED_THEME_UNVERIFIED', details: {
401
+ ...created,
402
+ causeCode: error.code || 'APP_THEME_READ_FAILED',
403
+ ...(error.details?.themeMode ? { themeMode: error.details.themeMode } : {}),
404
+ ...(error.details?.reason ? { reason: error.details.reason } : {}),
405
+ },
406
+ });
407
+ }
408
+
379
409
  chalkResult(true, t('create_app.success'), [
380
410
  ['appType', appType],
381
411
  ['Corp ID', corpId || t('common.unknown_error')],
@@ -391,6 +421,7 @@ async function run(args) {
391
421
  appType,
392
422
  appName,
393
423
  corpId,
424
+ themeVerification,
394
425
  url: appUrl,
395
426
  ...entryUrls,
396
427
  },
@@ -7,8 +7,8 @@
7
7
  'use strict';
8
8
 
9
9
  const querystring = require('querystring');
10
+ const { fetchCurrentApp, resolveAppThemeState } = require('./app-theme-state');
10
11
  const {
11
- httpGet,
12
12
  httpPost,
13
13
  requestWithAutoLogin,
14
14
  } = require('../core/utils');
@@ -376,35 +376,24 @@ function preserveBasicSettings(payload, app) {
376
376
  }
377
377
  }
378
378
 
379
- function requireAppStyleConfirmation(params, context) {
380
- const modern = pickAppField(context, 'appThemeEnable', 'APP_THEME_ENABLE') === 'y' &&
381
- pickAppField(context, 'appThemeMode', 'APP_THEME_MODE') === 'modern';
382
- if (!modern && params.confirmLegacyAppStyle !== true) {
379
+ function requireAppStyleConfirmation(params, app) {
380
+ const theme = resolveAppThemeState(app);
381
+ if (theme.mode !== 'modern' && params.confirmLegacyAppStyle !== true) {
382
+ const storedMode = pickAppField(app, 'appThemeMode', 'APP_THEME_MODE');
383
383
  throwCommandError(t('update_app.legacy_style_confirm_required'), {
384
384
  code: 'LEGACY_APP_CUSTOM_THEME_CONFIRMATION_REQUIRED',
385
- details: { appType: params.appType, applicationSettingsWritten: false, nextAction: 'ask_user_for_application_style_injection' },
385
+ details: { appType: params.appType, applicationSettingsWritten: false,
386
+ themeCapability: theme.mode, storedThemeMode: typeof storedMode === 'string' ? storedMode : null,
387
+ nextAction: 'ask_user_for_application_style_injection' },
386
388
  });
387
389
  }
388
390
  }
389
391
 
390
- // Runtime capability is resolved by the server. Stored APP_THEME_MODE alone is insufficient.
391
392
  async function checkAppStyleInjection(params, authRef) {
392
- const html = await requestWithAutoLogin(auth => httpGet(auth.baseUrl,
393
- `/${encodeURIComponent(params.appType)}/admin`, { _stamp: Date.now() }, { responseType: 'text' }), authRef);
394
- const { parseThemeContext } = require('./upgrade-app-theme');
395
- // Even human approval cannot waive target application and organization identity.
396
- const identity = parseThemeContext(html, ['appType', 'corpId']);
397
- if (identity.appType !== params.appType || !authRef.corpId || identity.corpId !== authRef.corpId) {
398
- throwCommandError(t('upgrade_app_theme.context_failed'), { code: 'APP_THEME_CONTEXT_UNVERIFIED' });
399
- }
400
- let context;
401
- try {
402
- context = parseThemeContext(html, ['appType', 'corpId', 'appThemeEnable', 'appThemeMode']);
403
- } catch (error) {
404
- if (error.code !== 'APP_THEME_CONTEXT_UNVERIFIED') {throw error;}
405
- return requireAppStyleConfirmation(params, {});
406
- }
407
- requireAppStyleConfirmation(params, context);
393
+ // AppRpc#getAppIncludingAecpInfo returns the application's own identity.
394
+ // Never open /admin or follow a browser login redirect to validate an API write.
395
+ const app = await fetchCurrentApp(params.appType, authRef, true);
396
+ requireAppStyleConfirmation(params, app);
408
397
  }
409
398
 
410
399
  function buildUpdateAppPostData(params, currentApp = {}, authRef) {
@@ -520,25 +509,6 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
520
509
  return postDataObj;
521
510
  }
522
511
 
523
- async function fetchCurrentApp(appType, authRef) {
524
- const response = await requestWithAutoLogin((auth) => httpGet(
525
- auth.baseUrl,
526
- `/${appType}/query/app/getAppIncludingAecpInfo.json`,
527
- {
528
- _api: 'nattyFetch',
529
- _mock: 'false',
530
- appKey: appType,
531
- _csrf_token: auth.csrfToken,
532
- _stamp: Date.now(),
533
- }
534
- ), authRef);
535
-
536
- if (!response || !response.success) {
537
- throw new Error(response ? response.errorMsg || '查询应用详情失败' : '查询应用详情失败');
538
- }
539
- return response.content || {};
540
- }
541
-
542
512
  function getThemeSettings(app) {
543
513
  const rawStyle = pickAppField(app, 'customThemeStyle', 'CUSTOM_THEME_STYLE');
544
514
  let customThemeStyle = rawStyle || null;
@@ -573,6 +543,32 @@ function themeReadbackEvidence(params, details) {
573
543
  return { ...details, scope: 'configuration_readback', operation, runtimeVerified: false };
574
544
  }
575
545
 
546
+ /** Configuration completion is separate from rendering. */
547
+ function getThemeCompletion(params, currentApp, app, themeVerified, navigationVerified) {
548
+ const before = resolveAppThemeState(currentApp);
549
+ const theme = resolveAppThemeState(app);
550
+ const operation = params.customThemeStyle ? 'application_css'
551
+ : params.disableCustomTheme ? 'disable'
552
+ : params.colour && params.colour !== 'custom' ? 'preset' : 'partial_update';
553
+ const actual = getThemeSettings(app);
554
+ const checks = {
555
+ themeModeVerified: theme.mode !== 'unknown' && theme.mode === before.mode,
556
+ settingsVerified: themeVerified === true,
557
+ navigationVerified: navigationVerified === true,
558
+ };
559
+ if (operation === 'application_css') {
560
+ checks.resourceBound = actual.colour === 'custom' && !!normalizeCssColorToHex(actual.themeColor) &&
561
+ actual.customThemeStyle?.enabled === true &&
562
+ typeof actual.customThemeStyle?.cssUrl === 'string' && !!actual.customThemeStyle.cssUrl.trim();
563
+ // Legacy overlays require scope approval.
564
+ if (theme.mode === 'legacy') {checks.legacyScopeConfirmed = params.confirmLegacyAppStyle === true;}
565
+ }
566
+ const complete = operation !== 'partial_update' && Object.values(checks).every(value => value === true);
567
+ return { scope: 'configuration_readback', operation, mode: theme.mode, modeSource: theme.source,
568
+ complete, status: complete ? 'complete' : operation === 'partial_update' ? 'partial' : 'incomplete',
569
+ checks, missing: Object.keys(checks).filter(key => !checks[key]), runtimeVerified: false };
570
+ }
571
+
576
572
  function noThemeUpload(appType) {
577
573
  return { requested: false, status: 'not_requested', uploadedThisRun: false, appType };
578
574
  }
@@ -633,6 +629,7 @@ async function saveAppSettings(params, authRef) {
633
629
  const navigationFields = Object.keys(expectedNavigation);
634
630
  if (!verifyTheme && !navigationFields.length) {return response;}
635
631
  let actual;
632
+ let actualApp = {};
636
633
  let actualNavigation;
637
634
  let readbackError;
638
635
  let themeVerified = !verifyTheme;
@@ -642,13 +639,15 @@ async function saveAppSettings(params, authRef) {
642
639
  if (attempt) { await new Promise((resolve) => setTimeout(resolve, attempt * 250)); }
643
640
  try {
644
641
  const app = await fetchCurrentApp(params.appType, authRef);
642
+ actualApp = app;
645
643
  actual = getThemeSettings(app);
646
644
  actualNavigation = getNavigationSettings(app, navigationFields, currentApp, params.disableCustomTheme);
647
645
  readbackError = undefined;
648
646
  themeVerified = !verifyTheme || themeSettingsMatch(expected, actual);
649
647
  navigationVerified = navigationFields.every((name) => expectedNavigation[name] === actualNavigation[name]);
650
- if (themeVerified && navigationVerified) {
651
- return { ...response,
648
+ const themeCompletion = getThemeCompletion(params, currentApp, app, themeVerified, navigationVerified);
649
+ if (themeVerified && navigationVerified && (!params.customThemeStyle || themeCompletion.complete)) {
650
+ return { ...response, themeCompletion,
652
651
  ...(submittedFields ? { submittedFields } : {}),
653
652
  ...(verifyTheme ? { themeVerification: themeReadbackEvidence(params, { verified: true, ...actual }) } : {}),
654
653
  ...(navigationFields.length ? { navigationVerification: { verified: true, ...actualNavigation } } : {}),
@@ -662,9 +661,12 @@ async function saveAppSettings(params, authRef) {
662
661
  }
663
662
  return {
664
663
  success: false,
664
+ themeCompletion: getThemeCompletion(params, currentApp, actualApp, themeVerified, navigationVerified),
665
665
  ...(submittedFields ? { submittedFields } : {}),
666
- errorCode: themeVerified ? 'APP_NAVIGATION_NOT_PERSISTED' : 'APP_THEME_NOT_PERSISTED',
667
- errorMsg: t(themeVerified ? 'update_app.navigation_not_persisted' : 'update_app.theme_not_persisted'),
666
+ errorCode: !themeVerified ? 'APP_THEME_NOT_PERSISTED' : !navigationVerified
667
+ ? 'APP_NAVIGATION_NOT_PERSISTED' : 'APP_THEME_CONFIGURATION_INCOMPLETE',
668
+ errorMsg: t(!themeVerified ? 'update_app.theme_not_persisted' : !navigationVerified
669
+ ? 'update_app.navigation_not_persisted' : 'update_app.theme_configuration_incomplete'),
668
670
  ...(verifyTheme ? { themeVerification: themeReadbackEvidence(params, themeVerified
669
671
  ? { verified: true, ...actual } : { verified: false, expected, actual, readbackError }) } : {}),
670
672
  ...(navigationFields.length ? { navigationVerification: navigationVerified
@@ -813,6 +815,7 @@ async function run(args) {
813
815
  success: true,
814
816
  appType: params.appType,
815
817
  themeVerification: response.themeVerification,
818
+ themeCompletion: response.themeCompletion,
816
819
  themeUpload,
817
820
  nameVerification,
818
821
  navigationVerification: response.navigationVerification,
@@ -838,11 +841,12 @@ async function run(args) {
838
841
  themeUpload,
839
842
  runtimeVerification: { verified: false, status: 'not_checked' },
840
843
  submittedFields: response && response.submittedFields,
844
+ themeCompletion: response && response.themeCompletion,
841
845
  errorCode: response && response.errorCode, themeVerification: response && response.themeVerification,
842
846
  navigationVerification: response && response.navigationVerification }));
843
847
  throwCommandError(errorMsg, { code: response && response.errorCode,
844
848
  details: response && { ...(response.errorCode === 'APP_NAVIGATION_NOT_PERSISTED' ? response.navigationVerification : response.themeVerification),
845
- ...themeFailureEvidence(themeUpload) } });
849
+ themeCompletion: response.themeCompletion, ...themeFailureEvidence(themeUpload) } });
846
850
  }
847
851
  }
848
852
 
@@ -17,7 +17,8 @@ const MAX_RESPONSE_BYTES = 65536;
17
17
  function fail(code) {
18
18
  return new CliError(t('qwenwork.desktop_login_failed'), {
19
19
  code: `QWENWORK_DESKTOP_${code}`,
20
- details: { invocation: buildQwenWorkLoginInvocation() },
20
+ details: { stage: 'authentication', owner: 'openyida', retryable: false,
21
+ nextAction: 'retry_after_host_state_changes', output_visibility: 'internal', invocation: buildQwenWorkLoginInvocation() },
21
22
  });
22
23
  }
23
24
 
@@ -9,7 +9,13 @@ const { exchangeQwenWorkCloudSession } = require('./qwenwork-cloud');
9
9
  const { tokenLogin, tokenRefresh, isRefreshAuthRequired } = require('./token-auth');
10
10
 
11
11
  function failure(code) {
12
- return new CliError(t('qwenwork.login_check_failed'), { code });
12
+ return new CliError(t('qwenwork.login_check_failed'), { code, details: {
13
+ stage: 'authentication', owner: 'openyida',
14
+ retryable: false,
15
+ nextAction: code === 'QWENWORK_LOGIN_IDENTITY_MISMATCH'
16
+ ? 'select_target_organization_in_host_then_retry' : 'retry_after_host_state_changes',
17
+ output_visibility: 'internal',
18
+ } });
13
19
  }
14
20
  function summary(ok, session = {}) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok, ...(ok ? sessionIdentity(session) : {}) }; }
15
21
 
@@ -72,7 +78,8 @@ async function verify(session, options = {}) {
72
78
  throw failure('QWENWORK_LOGIN_CHECK_FAILED');
73
79
  }
74
80
  if (value.corp_id !== session.corp_id || value.user_id !== session.user_id ||
75
- (options.corpId && value.corp_id !== options.corpId)) { throw failure('QWENWORK_LOGIN_IDENTITY_MISMATCH'); }
81
+ (options.corpId && value.corp_id !== options.corpId) ||
82
+ (options.userId && value.user_id !== options.userId)) { throw failure('QWENWORK_LOGIN_IDENTITY_MISMATCH'); }
76
83
  return true;
77
84
  } catch (error) {
78
85
  if (error instanceof CliError) { throw error; }
@@ -81,7 +88,7 @@ async function verify(session, options = {}) {
81
88
  }
82
89
 
83
90
  /** 网页态优先消费宿主凭据,认证失败回退普通 OAuth;其他宿主保持原流程。 */
84
- async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
91
+ async function performQwenWorkLogin(options = {}, dependencies = {}) {
85
92
  const env = options.env || process.env;
86
93
  const runtime = options.runtime || detectRuntimeCapabilities({ env });
87
94
  if (isQwenWorkWebRuntime(runtime) && !isEnvTokenAuthMode(env) && !hasEnvTokenCredential(env)) {
@@ -110,12 +117,26 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
110
117
  return summary(true, session);
111
118
  }
112
119
  const resolution = resolveTokenSession(options);
113
- if (resolution.status === 'profile_required') { throw failure('AUTH_PROFILE_REQUIRED'); }
120
+ if (resolution.status === 'profile_required') {
121
+ // 受管桌面的身份由客户端绑定;历史 profile 歧义不能让状态检查跳过宿主签票。
122
+ // 只返回未就绪,不选择凭据、发起 OAuth 或放宽显式身份约束。
123
+ const hostCheck = options.checkOnly && runtime.tool === 'qwenwork' && runtime.subtype === 'qwenwork_desktop' &&
124
+ /^[a-f0-9]{64}$/.test(env.QWENWORK_CLIENT_OPENYIDA_IDENTITY || '') &&
125
+ ![options.authProfile, options.profile, options.corpId, options.userId,
126
+ env.OPENYIDA_AUTH_PROFILE, env.OPENYIDA_AUTH_CORP_ID, env.OPENYIDA_AUTH_USER_ID].some(Boolean);
127
+ if (hostCheck) { return summary(false); }
128
+ throw failure('AUTH_PROFILE_REQUIRED');
129
+ }
130
+ if (runtime.tool === 'qwenwork' && resolution.status === 'profile_not_found' &&
131
+ (options.corpId || options.userId || env.OPENYIDA_AUTH_CORP_ID || env.OPENYIDA_AUTH_USER_ID)) {
132
+ throw failure('QWENWORK_LOGIN_IDENTITY_MISMATCH');
133
+ }
114
134
  let session = loadTokenSession(options);
115
135
  if (session && options.endpoint && new URL(session.base_url).origin !== new URL(options.endpoint).origin) {
116
136
  session = null;
117
137
  }
118
138
  if (await verify(session, options)) { return summary(true, session); }
139
+ if (options.checkOnly) { return summary(false); }
119
140
  if (session?.refresh_token) {
120
141
  try {
121
142
  const refreshed = await (dependencies.refresh || tokenRefresh)(options);
@@ -129,7 +150,6 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
129
150
  }
130
151
  }
131
152
  }
132
- if (options.checkOnly) { return summary(false); }
133
153
  if (isEnvTokenAuthMode(env) || isQwenWorkWebRuntime(runtime)) { throw failure('QWENWORK_LOGIN_FAILED'); }
134
154
  const result = await (dependencies.login || tokenLogin)(options);
135
155
  if (!result || result.ok === false || !result.access_token || !await verify(result, options)) {
@@ -138,6 +158,21 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
138
158
  return summary(true, result);
139
159
  }
140
160
 
161
+ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
162
+ try {
163
+ const result = await performQwenWorkLogin(options, dependencies);
164
+ return result.ok && (options.corpId || options.userId) ? { ...result, target_match: 'matched' } : result;
165
+ } catch (error) {
166
+ const terminal = failure(error instanceof CliError ? error.code : 'QWENWORK_LOGIN_FAILED');
167
+ if (error instanceof CliError) {
168
+ error.details = { ...terminal.details, ...error.details,
169
+ retryable: false, nextAction: terminal.details.nextAction, output_visibility: 'internal' };
170
+ throw error;
171
+ }
172
+ throw terminal;
173
+ }
174
+ }
175
+
141
176
  async function run(args, options = {}) {
142
177
  const result = await ensureQwenWorkLogin({ ...options, checkOnly: args.includes('--check-only') });
143
178
  if (args.includes('--json') && !args.includes('--quiet') && process.env.YIDA_QUIET !== '1') {
@@ -3,7 +3,7 @@
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { version, openyidaLocalBeta: localBeta } = require('../../package.json');
6
+ const { version } = require('../../package.json');
7
7
  const { t } = require('./i18n');
8
8
  const { buildCommandManifest, buildEntryDeliveryPolicy } = require('./command-manifest');
9
9
  const { buildEnvironmentSnapshot } = require('./env');
@@ -231,25 +231,26 @@ function buildAuthPath(login, env = process.env, runtime = null) {
231
231
  ? 'STOP_AND_REQUEST_ENV_TOKEN'
232
232
  : 'RUN_OPENYIDA_LOGIN_IF_USER_APPROVES',
233
233
  };
234
+ if (runtime?.tool === 'qwenwork') {
235
+ for (const key of ['auth_profile', 'corp_id', 'corp_name', 'user_id', 'user_name', 'store', 'persistence_scope', 'warning']) { delete authPath[key]; }
236
+ }
234
237
  if (isQwenWorkWebRuntime(runtime) && !envTokenSession) {
235
238
  const invocation = buildQwenWorkLoginInvocation({ subtype: 'qwenwork_web' });
236
239
  authPath.auth_runtime = 'qwenwork_cloud_exchange';
237
240
  authPath.interactive_login_allowed = true;
238
241
  authPath.missing_token_action = 'RUN_OPENYIDA_LOGIN_QWENWORK';
239
- for (const key of ['auth_profile', 'corp_id', 'corp_name', 'user_id', 'user_name', 'store', 'persistence_scope', 'warning']) { delete authPath[key]; }
240
242
  authPath.next_step = authPath.can_auto_use
241
243
  ? 'Silently continue the requested business operation and report its business result.'
242
244
  : 'Run the recommended login command and wait. The CLI first tries cloud credentials, then uses normal OAuth if authentication fails. If it prints an authorization URL, follow oauth_fallback browser guidance without starting another login process. Never inspect or display credentials.';
243
245
  authPath.next_step_commands = authPath.can_auto_use ? [] : [invocation.command];
244
246
  return authPath;
245
247
  }
246
- // 桌面换票仅由当前活跃宿主和发行配置共同启用,不影响其他宿主的认证路径。
247
- if (localBeta?.desktopTicketLogin && runtime?.tool === 'qwenwork' && runtime?.subtype === 'qwenwork_desktop' && !envTokenSession) {
248
+ // 公开入口已随正式包提供;宿主处理票据,模型只执行能力快照的命令。
249
+ if (runtime?.tool === 'qwenwork' && runtime?.subtype === 'qwenwork_desktop' && !envTokenSession) {
248
250
  const invocation = buildQwenWorkLoginInvocation();
249
251
  authPath.auth_runtime = 'qwenwork_desktop_exchange_ticket';
250
252
  authPath.interactive_login_allowed = true;
251
253
  authPath.missing_token_action = 'RUN_OPENYIDA_LOGIN_QWENWORK';
252
- for (const key of ['auth_profile', 'corp_id', 'corp_name', 'user_id', 'user_name', 'store', 'persistence_scope', 'warning']) { delete authPath[key]; }
253
254
  authPath.next_step = authPath.can_auto_use
254
255
  ? 'Silently continue the requested business operation and report its business result.'
255
256
  : `Run the recommended login command as returned: ${invocation.command}. No extra PATH or output setup is needed. After success silently continue the requested business operation. If it fails, follow the returned invocation guidance.`;
@@ -277,7 +278,7 @@ function buildInteractiveLogin(auth, runtime) {
277
278
  invocation: buildQwenWorkLoginInvocation(),
278
279
  agent_action: auth.can_auto_use ? 'continue_business_command' : 'run_login_qwenwork_and_wait',
279
280
  user_visible: false, success_action: 'continue_business_command_without_message',
280
- reason: 'qwenwork_desktop_pre_beta', completion_signal: 'process_exit_and_final_json', playwright_required: false,
281
+ reason: 'qwenwork_desktop_host', completion_signal: 'process_exit_and_final_json', playwright_required: false,
281
282
  };
282
283
  }
283
284
  const base = {
@@ -358,12 +359,44 @@ function buildBuilderPath(login, projectRoot, manifest, env = process.env, runti
358
359
  runtime,
359
360
  browser_verification: require('./browser-handoff').browserVerificationPolicy(runtime, env),
360
361
  interactive_login: buildInteractiveLogin(auth, runtime),
361
- communication: isQwenWorkWebRuntime(runtime) || (runtime.tool === 'qwenwork' && runtime.subtype === 'qwenwork_desktop') ? {
362
+ communication: {
362
363
  preparation_visibility: 'silent',
364
+ authentication_narration: 'forbidden_in_messages_progress_todos_cards_and_tool_titles',
365
+ readiness_announcement: 'never_announce_environment_login_or_organization_readiness',
366
+ preparation_success_action: 'continue_business_operation_without_transition_message',
367
+ tool_title_content: 'current_business_stage_only_preserve_raw_command_in_execution_record',
368
+ app_build_task: {
369
+ title_template: '在{organization_name}组织下搭建宜搭应用',
370
+ applies_to: 'controllable_task_title_and_start_message_conversation_title_when_host_supports_update',
371
+ organization_source: 'verified_current_host_organization_display_name_never_environment_label_domain_id_or_history',
372
+ unknown_organization_title: '在当前组织下搭建宜搭应用',
373
+ update_title_after_organization_is_verified: true,
374
+ append_authentication_or_preparation_status: false,
375
+ },
376
+ app_creation_notice: {
377
+ template: '当前在{organization_name}组织下为您创建{app_name}应用',
378
+ when: 'once_immediately_before_creating_new_app_not_preparation_retries_or_resume',
379
+ organization_source: 'verified_current_host_organization_display_name_never_environment_label_domain_id_or_history',
380
+ app_name_source: 'confirmed_app_name',
381
+ unknown_organization: '当前组织',
382
+ append_authentication_or_preparation_status: false,
383
+ },
363
384
  completion_content: 'business_result_and_business_link_only',
364
385
  disclose_authentication_identity_environment: false,
365
- report_preparation_only_when_user_action_is_required: true,
386
+ disclose_implementation_details: false,
387
+ progress_content: 'business_stage_only_never_authentication_or_internal_repair',
388
+ user_action_content: 'minimal_action_and_business_impact_without_technical_explanation',
389
+ report_preparation_only_when_user_action_is_required: false,
366
390
  bundled_skill_path: path.resolve(__dirname, '../../yida-skills'),
391
+ },
392
+ host_context: runtime.tool === 'qwenwork' ? {
393
+ default_organization: 'current_host_organization',
394
+ ask_organization_by_default: false,
395
+ explicit_target_policy: 'preserve_user_target_and_stop_if_unmatched',
396
+ organization_switch: 'host_supported_flow_only_never_copy_profiles_or_rewrite_environment',
397
+ recovery: auth.auth_runtime === 'env_token_bootstrap'
398
+ ? { max_attempts: 0, failure_next_step: 'stop_and_request_host_preparation' }
399
+ : buildQwenWorkLoginInvocation({ subtype: runtime.subtype }),
367
400
  } : undefined,
368
401
  environment_check_simplification: {
369
402
  contract_role: 'machine_readable_builder_runtime_not_yida_agent_business_logic',
@@ -488,6 +521,7 @@ function compactBuilderPath(builderPath) {
488
521
  browser_verification: builderPath.browser_verification,
489
522
  interactive_login: builderPath.interactive_login,
490
523
  communication: builderPath.communication,
524
+ host_context: builderPath.host_context,
491
525
  environment_check_simplification: {
492
526
  minimal_probe_commands: environment.minimal_probe_commands,
493
527
  can_skip_default_exploration_when_summary_ok: environment.can_skip_default_exploration_when_summary_ok,
@@ -657,8 +691,12 @@ function buildAgentCapabilitiesSummary() {
657
691
  schema_version: 1,
658
692
  name: 'openyida-agent-capabilities-summary',
659
693
  version,
660
- login: ['qwenwork_desktop_exchange_ticket', 'qwenwork_cloud_exchange'].includes(builderPath.auth.auth_runtime)
661
- ? { status: loginStatus.status, can_auto_use: loginStatus.can_auto_use === true }
694
+ login: runtime.tool === 'qwenwork'
695
+ ? { status: loginStatus.status, can_auto_use: loginStatus.can_auto_use === true,
696
+ ...(builderPath.auth.auth_runtime === 'env_token_bootstrap' ? {
697
+ auth_mode: loginStatus.auth_mode, auth_source: loginStatus.auth_source,
698
+ failure_reason: loginStatus.failure_reason,
699
+ } : {}) }
662
700
  : compactLogin(loginStatus),
663
701
  workdir: projectRoot,
664
702
  workdir_exists: !!projectResolution.exists,
@@ -1344,7 +1344,7 @@ const COMMAND_GROUPS = [
1344
1344
  output: 'json',
1345
1345
  }),
1346
1346
  command('create-app', ['create-app'], 'create-app "<name>"|--name <name> [options] [--locale zh_CN|en_US|ja_JP] [--open|--no-open]', 'help.cmd_create_app', { managedRun: { scope: 'denied', reason: 'scope_not_audited' },
1347
- notes: ['Successful results include appUrl/workbenchUrl for the business workspace and adminUrl for developer administration. Complete application delivery retains adminUrl regardless of auth runtime; route construction does not verify recipient access. Preserve the frontend URL from its own page result.', 'In QwenWork, permission failures return a nonzero exit and structured status=permission_required. YIDA_PLATFORM_ACCESS_REQUIRED and YIDA_APP_CREATE_PERMISSION_REQUIRED include details.permissionLabel/permissionUrl and nextAction.label/url; YIDA_PERMISSION_REQUIRED preserves an unclassified permission reason without inventing a link. Explain the missing permission, display the semantic label as a Markdown link to its paired URL (or reuse nextStep), and pause resource writes; do not automatically open it, send an application, switch spaces, log in again or retry creation. Resume only after the user requests continuation and the host identity, organization and permissions are verified.'],
1347
+ notes: ['Creation always requests createWithModernTheme=y and verifies the new app identity and modern configuration through getAppIncludingAecpInfo.json before reporting success. APP_CREATED_THEME_UNVERIFIED preserves details.appCreated/appType/retryCreate=false; inspect that app and never recreate or silently downgrade it. themeVerification is configuration readback, not browser rendering proof.', 'Successful results include appUrl/workbenchUrl for the business workspace and adminUrl for developer administration. Complete application delivery retains adminUrl regardless of auth runtime; route construction does not verify recipient access. Preserve the frontend URL from its own page result.', 'In QwenWork, permission failures return a nonzero exit and structured status=permission_required. YIDA_PLATFORM_ACCESS_REQUIRED and YIDA_APP_CREATE_PERMISSION_REQUIRED include details.permissionLabel/permissionUrl and nextAction.label/url; YIDA_PERMISSION_REQUIRED preserves an unclassified permission reason without inventing a link. Explain the missing permission, display the semantic label as a Markdown link to its paired URL (or reuse nextStep), and pause resource writes; do not automatically open it, send an application, switch spaces, log in again or retry creation. Resume only after the user requests continuation and the host identity, organization and permissions are verified.'],
1348
1348
  }),
1349
1349
  command('design-plan.catalog', ['design-plan', 'catalog'], 'design-plan catalog [--json]', 'help.cmd_design_plan_catalog', { managedRun: { scope: 'local' },
1350
1350
  requiresLogin: false, output: 'json',
@@ -1419,12 +1419,13 @@ const COMMAND_GROUPS = [
1419
1419
  { name: 'showAppNav', type: 'boolean', source: 'option', builder_options: ['--show-app-nav'] },
1420
1420
  ],
1421
1421
  notes: [
1422
- 'Before uploading or binding CSS, read server-resolved appThemeEnable/appThemeMode and verify app/corp identity. Only y/modern allows automatic app-level injection. Legacy or unknown capability requires asking the human about this exact application and reviewed stylesheet scope before --confirm-legacy-app-style; unavailable confirmation means stop before uploading. Prefer page-scoped styles for legacy applications. --disable-custom-theme retains the existing resource URL/name and sets enabled/iframePropagation to boolean false. Never submit an empty style or remove the resource URL. Inspect strict configuration readback. Checking that link#yida-app-custom-theme is absent and CSS is not requested follows builder_path.browser_verification; HTTP success alone is insufficient.',
1422
+ 'Before uploading or binding CSS, verify app/corp identity through getAppIncludingAecpInfo.json, never an admin HTML route. Recognize modern when server appThemeEnable=y and appThemeMode=modern, or when CREATED_WITH_MODERN_THEME=y with explicit modern mode or an unset mode and server-derived AI provenance (including qwenwork). Explicit disabled/legacy facts take precedence. A mode name or host alone is insufficient; missing evidence remains unknown. Configuration readback never proves browser CSS rendering. Legacy or unknown capability requires asking the human about this exact application and reviewed stylesheet scope before --confirm-legacy-app-style; unavailable confirmation means stop before uploading. Prefer page-scoped styles for legacy applications. --disable-custom-theme retains the existing resource URL/name and sets enabled/iframePropagation to boolean false. Never submit an empty style or remove the resource URL. Inspect strict configuration readback. Checking that link#yida-app-custom-theme is absent and CSS is not requested follows builder_path.browser_verification; HTTP success alone is insufficient.',
1423
1423
  'Upload application CSS with --theme-file; the primary color is read from CSS and saved with the theme resource. Edit navigation radius, three border states and selected shadow in design.md, regenerate CSS, then upload; these tokens are not separate update-app flags. Unknown options, missing values and combined --hide-app-nav/--show-app-nav are rejected.',
1424
1424
  'themeUpload records this invocation: target app/environment, source name/bytes/SHA-256, themeColor and resource URL. resourceContentVerified=false means no remote byte comparison. Without --theme-file it is not_requested/uploadedThisRun=false. Unknown upload or failed binding needs readback before another upload; accepted uploads retain their receipt.',
1425
1425
  'One design document and theme CSS cover navigation, application shell, native forms, custom pages and record details. tokens.application-global.appearance.navigation defines navigation surfaces, text and interaction states, logo, groups, search, popups, borders, radius and density. Platform-supported tokens are applied through the active navigation mode; page content consumes the same application theme.',
1426
1426
  'Applications with appThemeEnable=n or appThemeMode=legacy persist side/top/l_shape as ver/hoz/ver and map unsupported white/gray navigation colors to legacy light. Existing legacy layouts and navType are preserved. Unknown theme capability keeps the existing save protocol.',
1427
1427
  'navigationVerification checks requested navTheme/layoutDirection/hideAppNav/logoSource; mismatch is APP_NAVIGATION_NOT_PERSISTED, nonzero, with read-only retries. themeVerification.scope=configuration_readback; operation is bind_resource/update_theme/disable/preserve. preserve is not upload evidence. New themes need an uploaded receipt and matching enabled resource/color. runtimeVerification stays false/not_checked. Browser checks follow builder_path.browser_verification; only a local agent on the user machine may run them. Cloud/sandbox/remote/unknown keep not_checked internal and deliver confirmed results and entry URLs, without default user verification tasks.',
1428
+ 'themeCompletion.complete requires stable proven modern mode, or legacy mode with scope confirmation, plus matching CSS/color and requested navigation. Unknown/changed mode fails with APP_THEME_CONFIGURATION_INCOMPLETE after read-only retries. Color/navigation-only is partial; preset/disable are separate operations. Never replace requested CSS with colors after a historical redirect. sample is optional; configuration is not rendering proof.',
1428
1429
  'For generated design artifacts, pass themeProfile.navTheme from design.md to --nav-theme. Do not infer it from the business brief or omit it expecting a light default.',
1429
1430
  'Platform preset keys are used independently of --theme-file and --theme-color. Choose --hide-app-nav or --show-app-nav to set navigation visibility.',
1430
1431
  ],
@@ -1899,6 +1900,7 @@ const COMMAND_GROUPS = [
1899
1900
  output: 'json',
1900
1901
  noteKeys: ['help.data_notes', 'query_data.approver_query_help'],
1901
1902
  notes: [
1903
+ 'Ordinary form updates read record edit permission and native edit-schema field behaviors before writing. Only verified NORMAL fields are accepted; readonly, hidden, disabled, unknown or dynamically unverifiable fields reject the entire update. Restricted subform replacements require the native editor. This CLI preflight does not replace server-side authorization.',
1902
1904
  'Application tasks: data query tasks <appType> --type todo|created|done|cc [--normalized] [--transport app-v1|app-query]. submitted remains an alias of created. --normalized returns items/total/page/pageSize with native detail targets; an unknown total is null. app-v1 is the compatible default; only cc adds --process-codes and --instance-status. Explicit app-query fixes appKeys to the target app and supports the per-kind filters in task-center capabilities: --originators, --process-codes, --instance-create-from/to, --task-create-from/to, --task-finish-from/to, --carbon-create-from/to. Date values are Unix milliseconds. Unsupported filters and caller scope/identity overrides are rejected; transports never silently fall back.',
1903
1905
  'Each condition uses key (field ID), componentName, type, operator and value. Query types: TextField/TextareaField/TableField TEXT; NumberField/RateField/DateField/CascadeDateField DOUBLE; RadioField/SelectField/CheckboxField/MultiSelectField/DepartmentSelectField ARRAY; EmployeeField/CascadeSelectField/CitySelectField STRING; ImageField/AttachmentField TEXT.',
1904
1906
  'Ordinary form searchFormDatas accepts AND condition arrays via --search-json/--search-file. DateField: type DOUBLE, is/is_not, value ""; zero is filled. Query range endpoints as DateField with .start/.end: both empty means unfilled, both filled means complete.',
@@ -2332,7 +2334,9 @@ function summarizeLocalizedCommands(commands) {
2332
2334
  reference: 'yida-skills/skills/yida-design/references/ask-human-interaction-contract.md',
2333
2335
  audience: 'nontechnical_user',
2334
2336
  wording: 'Use everyday language, concrete business names and short sentences. Lead with the result or action; explain necessary technical terms on first use.',
2335
- progress: 'Say what is happening and what happens next. Report completed work from verified results.',
2337
+ progress: 'Report only the actual business stage and verified business results. Never announce environment, login or current-organization readiness.',
2338
+ preparation: 'Strictly internal: installation, environment checks, authentication, ticket exchange, refresh and recovery must never appear in messages, todos, cards or tool-call titles/descriptions. On success continue the business operation without a transition message. If user action is essential, state only the minimal action and business impact.',
2339
+ app_creation_notice: 'Immediately before creating a new app, say once: 当前在{组织名称}组织下为您创建{应用名称}应用. Use the verified current host organization display name and confirmed app name; never substitute an environment label, domain, organization ID or historical account. If the display name is unavailable, say 当前组织. Do not repeat on retries or existing-app continuation, or append authentication/preparation status.',
2336
2340
  image_progress: 'Use 素材采集&应用创建 as the progress description while image collection and app creation are both running; otherwise use the active stage name. Keep the todo items separate. Record image counts, slots, subtasks and execution details internally.',
2337
2341
  todo: {
2338
2342
  templates: 'yida-skills/skills/yida-design/references/ask-human-interaction-contract.md#步骤列表与进度',