openyida 2026.10.30-beta.1 → 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.
- package/README.md +3 -16
- package/lib/app/app-theme-state.js +80 -0
- package/lib/app/create-app.js +31 -0
- package/lib/app/custom-theme.js +21 -1
- package/lib/app/services/canvas-page-schema-builder.js +4 -0
- package/lib/app/update-app.js +97 -65
- package/lib/auth/qwenwork-desktop.js +2 -1
- package/lib/auth/qwenwork-login.js +40 -5
- package/lib/core/agent-capabilities.js +50 -10
- package/lib/core/browser-handoff.js +18 -4
- package/lib/core/command-manifest.js +12 -6
- package/lib/core/form-edit-permissions.js +210 -0
- package/lib/core/locales/en.js +9 -1
- package/lib/core/locales/zh.js +9 -1
- package/lib/core/query-data.js +12 -1
- package/lib/core/qwenwork-login-guidance.js +5 -2
- package/lib/core/utils.js +15 -10
- package/lib/design-plan/visual-policy.js +1 -1
- package/lib/permission/permission-scope.js +27 -8
- package/lib/permission/save-permission.js +6 -4
- package/package.json +1 -1
- package/yida-skills/SKILL.md +9 -3
- package/yida-skills/references/execution-rules.md +28 -0
- package/yida-skills/references/qwenwork-cloud-login.md +2 -0
- package/yida-skills/references/qwenwork-desktop-login.md +2 -0
- package/yida-skills/references/setup-and-env.md +26 -8
- package/yida-skills/references/user-facing-summary.md +26 -6
- package/yida-skills/skills/yida-app/SKILL.md +6 -0
- package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +4 -4
- package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +2 -0
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +13 -11
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +1 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +25 -22
- package/yida-skills/skills/yida-canvas-custom-page/references/waf-safe-authoring.md +14 -37
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +3 -17
- package/yida-skills/skills/yida-create-app/SKILL.md +14 -6
- package/yida-skills/skills/yida-data-management/SKILL.md +4 -1
- package/yida-skills/skills/yida-data-management/references/api-matrix.md +4 -0
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/references/application-style-library.md +7 -3
- package/yida-skills/skills/yida-design/references/application-theme-consistency.md +4 -4
- package/yida-skills/skills/yida-design/references/theme-selection.md +7 -6
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +10 -9
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/design.md +6 -3
- package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
- package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +4 -4
- package/yida-skills/skills/yida-dingtalk-openapi/references/connector-mapping.md +7 -3
- package/yida-skills/skills/yida-form-permission/SKILL.md +3 -22
- package/yida-skills/skills/yida-login/SKILL.md +13 -6
- package/yida-skills/skills/yida-publish-page/SKILL.md +4 -2
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +2 -0
package/README.md
CHANGED
|
@@ -143,9 +143,9 @@ openyida login --check-only --json
|
|
|
143
143
|
openyida auth status
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
In QwenWork web, `openyida login --qwenwork --json` (or ordinary `login`)
|
|
146
|
+
In QwenWork web, `openyida login --qwenwork --json` (or ordinary `login`) reuses the sandbox identity. If cloud authentication fails, the same command falls back to OAuth; follow its authorization URL and wait for completion. Desktop and other hosts keep their existing login paths.
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Login supports DingTalk and QwenWork organizations, ODS and personal spaces. Sessions preserve server-provided identity classification, business domain and refresh credentials. Historical sessions default to DingTalk organizations. JSON errors include bounded diagnostics without credentials or response bodies.
|
|
149
149
|
|
|
150
150
|
For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
|
|
151
151
|
|
|
@@ -153,20 +153,7 @@ For QwenWork cloud identity diagnostics, run inside the actual cloud execution e
|
|
|
153
153
|
openyida auth qwenwork user-info --json
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
This diagnostic
|
|
157
|
-
|
|
158
|
-
This command exchanges the injected `QWENWORK_AGENT_TOKEN` (legacy: `MULE_AGENT_TOKEN` / `AGENT_TOKEN`) at `QWENWORK_GATEWAY_URL` (legacy: `MULE_GATEWAY_URL`), then queries `/user/info` on the same origin. It reports identity and scope metadata without printing or persisting either credential. It does not create an OpenYida login session, and QwenWork organization IDs are not YiDA `corpId` values. Existing OAuth/profile behavior is unchanged.
|
|
159
|
-
|
|
160
|
-
Programmatic callers can use `exchangeQwenWorkAgentToken`, `getQwenWorkUserInfo` and `getQwenWorkIdentity` from `lib/auth/qwenwork-client.js`. The exchange function returns an in-memory `access_token` credential for the identity query or a future server-side login exchange; callers must not log or persist it.
|
|
161
|
-
|
|
162
|
-
```js
|
|
163
|
-
const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('openyida/lib/auth/qwenwork-client');
|
|
164
|
-
async function readCloudUser() {
|
|
165
|
-
const credential = await exchangeQwenWorkAgentToken();
|
|
166
|
-
// The result contains QwenWork identifiers, not YiDA IDs.
|
|
167
|
-
return getQwenWorkUserInfo(credential);
|
|
168
|
-
}
|
|
169
|
-
```
|
|
156
|
+
This legacy diagnostic reads the injected host credentials and reports identity and scope without printing or saving tokens. It does not create or validate a YiDA session; QwenWork organization IDs are not YiDA `corpId` values. Check the YiDA session with `openyida login --check-only --json`. See [QwenWork login guidance](yida-skills/references/qwenwork-cloud-login.md) for authentication behavior.
|
|
170
157
|
|
|
171
158
|
To reuse an existing shared login profile for the current project, switch first instead of starting a new OAuth login:
|
|
172
159
|
|
|
@@ -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 };
|
package/lib/app/create-app.js
CHANGED
|
@@ -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
|
},
|
package/lib/app/custom-theme.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { createHash } = require('crypto');
|
|
5
6
|
const { resolveBearerAuthHeaders } = require('../core/utils');
|
|
6
7
|
const { readManagedContext, managedError } = require('../agent/managed-context');
|
|
7
8
|
const { CliError } = require('../core/cli-error');
|
|
@@ -306,7 +307,26 @@ async function uploadCustomThemeFile(appType, filePath, authRef, options = {}) {
|
|
|
306
307
|
if (!response.ok) {
|
|
307
308
|
throw new Error((payload && (payload.errorMsg || payload.message)) || `主题文件上传失败: HTTP ${response.status}`);
|
|
308
309
|
}
|
|
309
|
-
|
|
310
|
+
// Hash multipart bytes, not reread files or server-supplied receipt fields.
|
|
311
|
+
if (payload?.success === false) {
|
|
312
|
+
return { ...payload, themeUpload: { requested: true, status: 'rejected', uploadedThisRun: false, appType } };
|
|
313
|
+
}
|
|
314
|
+
const upload = unwrapUploadResponse(payload);
|
|
315
|
+
return { ...payload, themeUpload: {
|
|
316
|
+
requested: true,
|
|
317
|
+
status: 'uploaded',
|
|
318
|
+
uploadedThisRun: true,
|
|
319
|
+
appType,
|
|
320
|
+
baseUrl: new URL(authRef.baseUrl).origin,
|
|
321
|
+
corpId: authRef.corpId || null,
|
|
322
|
+
sourceFileName: file.name,
|
|
323
|
+
sourceBytes: file.content.length,
|
|
324
|
+
sourceSha256: createHash('sha256').update(file.content).digest('hex'),
|
|
325
|
+
themeColor: file.themeColor,
|
|
326
|
+
cssUrl: upload.downloadUrl || upload.url,
|
|
327
|
+
cssFileName: upload.name || '',
|
|
328
|
+
resourceContentVerified: false,
|
|
329
|
+
} };
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
module.exports = {
|
|
@@ -81,6 +81,10 @@ var OPENYIDA_UTIL_API_METHODS = [
|
|
|
81
81
|
function openyidaYidaApiBridgeTargets() {
|
|
82
82
|
var targets = [];
|
|
83
83
|
function pushTarget(target) {
|
|
84
|
+
if (target !== window) {
|
|
85
|
+
var origin = window.location.origin;
|
|
86
|
+
if (!origin || origin === 'null' || target.location.origin !== origin) { return; }
|
|
87
|
+
}
|
|
84
88
|
if (target && targets.indexOf(target) === -1) {
|
|
85
89
|
targets.push(target);
|
|
86
90
|
}
|
package/lib/app/update-app.js
CHANGED
|
@@ -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,
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
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,
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
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;
|
|
@@ -566,6 +536,51 @@ function themeSettingsMatch(expected, actual) {
|
|
|
566
536
|
['cssUrl', 'cssFileName'].every((key) => (expectedStyle[key] || '') === (actualStyle[key] || ''));
|
|
567
537
|
}
|
|
568
538
|
|
|
539
|
+
function themeReadbackEvidence(params, details) {
|
|
540
|
+
const operation = params.disableCustomTheme ? 'disable'
|
|
541
|
+
: params.customThemeStyle ? 'bind_resource'
|
|
542
|
+
: params.colour || params.themeColor ? 'update_theme' : 'preserve';
|
|
543
|
+
return { ...details, scope: 'configuration_readback', operation, runtimeVerified: false };
|
|
544
|
+
}
|
|
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
|
+
|
|
572
|
+
function noThemeUpload(appType) {
|
|
573
|
+
return { requested: false, status: 'not_requested', uploadedThisRun: false, appType };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function themeFailureEvidence(themeUpload) {
|
|
577
|
+
const uncertain = themeUpload.uploadedThisRun === true || themeUpload.status === 'unknown';
|
|
578
|
+
return { themeUpload, runtimeVerification: { verified: false, status: 'not_checked' },
|
|
579
|
+
...(uncertain ? { target: { appType: themeUpload.appType }, retrySafe: false,
|
|
580
|
+
sideEffectState: 'unknown', readbackAllowed: true } : {}),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
569
584
|
function getRequestedNavigationSettings(params, payload) {
|
|
570
585
|
return Object.fromEntries(['navTheme', 'layoutDirection', 'hideAppNav', 'logoSource']
|
|
571
586
|
.filter((name) => params[name] !== undefined && params[name] !== null && params[name] !== '')
|
|
@@ -614,6 +629,7 @@ async function saveAppSettings(params, authRef) {
|
|
|
614
629
|
const navigationFields = Object.keys(expectedNavigation);
|
|
615
630
|
if (!verifyTheme && !navigationFields.length) {return response;}
|
|
616
631
|
let actual;
|
|
632
|
+
let actualApp = {};
|
|
617
633
|
let actualNavigation;
|
|
618
634
|
let readbackError;
|
|
619
635
|
let themeVerified = !verifyTheme;
|
|
@@ -623,15 +639,17 @@ async function saveAppSettings(params, authRef) {
|
|
|
623
639
|
if (attempt) { await new Promise((resolve) => setTimeout(resolve, attempt * 250)); }
|
|
624
640
|
try {
|
|
625
641
|
const app = await fetchCurrentApp(params.appType, authRef);
|
|
642
|
+
actualApp = app;
|
|
626
643
|
actual = getThemeSettings(app);
|
|
627
644
|
actualNavigation = getNavigationSettings(app, navigationFields, currentApp, params.disableCustomTheme);
|
|
628
645
|
readbackError = undefined;
|
|
629
646
|
themeVerified = !verifyTheme || themeSettingsMatch(expected, actual);
|
|
630
647
|
navigationVerified = navigationFields.every((name) => expectedNavigation[name] === actualNavigation[name]);
|
|
631
|
-
|
|
632
|
-
|
|
648
|
+
const themeCompletion = getThemeCompletion(params, currentApp, app, themeVerified, navigationVerified);
|
|
649
|
+
if (themeVerified && navigationVerified && (!params.customThemeStyle || themeCompletion.complete)) {
|
|
650
|
+
return { ...response, themeCompletion,
|
|
633
651
|
...(submittedFields ? { submittedFields } : {}),
|
|
634
|
-
...(verifyTheme ? { themeVerification: { verified: true, ...actual } } : {}),
|
|
652
|
+
...(verifyTheme ? { themeVerification: themeReadbackEvidence(params, { verified: true, ...actual }) } : {}),
|
|
635
653
|
...(navigationFields.length ? { navigationVerification: { verified: true, ...actualNavigation } } : {}),
|
|
636
654
|
};
|
|
637
655
|
}
|
|
@@ -643,11 +661,14 @@ async function saveAppSettings(params, authRef) {
|
|
|
643
661
|
}
|
|
644
662
|
return {
|
|
645
663
|
success: false,
|
|
664
|
+
themeCompletion: getThemeCompletion(params, currentApp, actualApp, themeVerified, navigationVerified),
|
|
646
665
|
...(submittedFields ? { submittedFields } : {}),
|
|
647
|
-
errorCode: themeVerified ? '
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
?
|
|
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'),
|
|
670
|
+
...(verifyTheme ? { themeVerification: themeReadbackEvidence(params, themeVerified
|
|
671
|
+
? { verified: true, ...actual } : { verified: false, expected, actual, readbackError }) } : {}),
|
|
651
672
|
...(navigationFields.length ? { navigationVerification: navigationVerified
|
|
652
673
|
? { verified: true, ...actualNavigation }
|
|
653
674
|
: { verified: false, expected: expectedNavigation, actual: actualNavigation, readbackError } } : {}),
|
|
@@ -663,22 +684,25 @@ async function applyCustomThemeUpdate(appType, themeParams, authRef) {
|
|
|
663
684
|
assertNavTheme(params.navTheme);
|
|
664
685
|
assertLogoSource(params.logoSource);
|
|
665
686
|
|
|
666
|
-
|
|
667
|
-
await checkAppStyleInjection(params, authRef);
|
|
668
|
-
const themeFile = readThemeCssFile(params.themeFile);
|
|
669
|
-
params.themeColor = themeFile.themeColor;
|
|
670
|
-
const uploadResponse = await requestWithAutoLogin(
|
|
671
|
-
(auth) => uploadCustomThemeFile(appType, params.themeFile, auth),
|
|
672
|
-
authRef
|
|
673
|
-
);
|
|
674
|
-
params.customThemeStyle = buildCustomThemeStyle(uploadResponse);
|
|
675
|
-
|
|
687
|
+
let themeUpload = { requested: true, status: 'not_started', uploadedThisRun: false, appType };
|
|
676
688
|
try {
|
|
677
|
-
|
|
689
|
+
assertCustomThemeOptions(params);
|
|
690
|
+
await checkAppStyleInjection(params, authRef);
|
|
691
|
+
readThemeCssFile(params.themeFile);
|
|
692
|
+
// Lost responses leave upload state unknown.
|
|
693
|
+
themeUpload = { ...themeUpload, status: 'unknown', uploadedThisRun: null };
|
|
694
|
+
const uploadResponse = await requestWithAutoLogin(
|
|
695
|
+
(auth) => uploadCustomThemeFile(appType, params.themeFile, auth),
|
|
696
|
+
authRef
|
|
697
|
+
);
|
|
698
|
+
if (uploadResponse.themeUpload) {themeUpload = uploadResponse.themeUpload;}
|
|
699
|
+
params.customThemeStyle = buildCustomThemeStyle(uploadResponse);
|
|
700
|
+
params.themeColor = themeUpload.themeColor;
|
|
701
|
+
return { ...await saveAppSettings(params, authRef), themeUpload };
|
|
678
702
|
} catch (error) {
|
|
679
|
-
error.
|
|
680
|
-
|
|
681
|
-
|
|
703
|
+
throwCommandError(error.message, { code: error.code || 'APP_THEME_UPDATE_FAILED', exitCode: error.exitCode,
|
|
704
|
+
details: { ...error.details, appType, uploadedResource: params.customThemeStyle,
|
|
705
|
+
...themeFailureEvidence(themeUpload), applicationSettingsVerified: false } });
|
|
682
706
|
}
|
|
683
707
|
}
|
|
684
708
|
|
|
@@ -767,6 +791,8 @@ async function run(args) {
|
|
|
767
791
|
postData
|
|
768
792
|
);
|
|
769
793
|
}, authRef);
|
|
794
|
+
const themeUpload = params.themeFile ? response?.themeUpload : noThemeUpload(params.appType);
|
|
795
|
+
if (themeUpload?.uploadedThisRun) {params.themeColor = themeUpload.themeColor;}
|
|
770
796
|
|
|
771
797
|
// Verify the persisted resource before exposing success or a mutation receipt.
|
|
772
798
|
let nameVerification;
|
|
@@ -789,9 +815,11 @@ async function run(args) {
|
|
|
789
815
|
success: true,
|
|
790
816
|
appType: params.appType,
|
|
791
817
|
themeVerification: response.themeVerification,
|
|
818
|
+
themeCompletion: response.themeCompletion,
|
|
819
|
+
themeUpload,
|
|
792
820
|
nameVerification,
|
|
793
821
|
navigationVerification: response.navigationVerification,
|
|
794
|
-
|
|
822
|
+
runtimeVerification: { verified: false, status: 'not_checked' },
|
|
795
823
|
updatedFields: {
|
|
796
824
|
name: params.name || undefined,
|
|
797
825
|
desc: params.desc || undefined,
|
|
@@ -810,11 +838,15 @@ async function run(args) {
|
|
|
810
838
|
const errorMsg = response ? response.errorMsg || t('common.unknown_error') : t('common.request_failed');
|
|
811
839
|
chalkResult(false, t('update_app.failed', errorMsg));
|
|
812
840
|
console.log(JSON.stringify({ success: false, appType: params.appType, error: errorMsg,
|
|
841
|
+
themeUpload,
|
|
842
|
+
runtimeVerification: { verified: false, status: 'not_checked' },
|
|
813
843
|
submittedFields: response && response.submittedFields,
|
|
844
|
+
themeCompletion: response && response.themeCompletion,
|
|
814
845
|
errorCode: response && response.errorCode, themeVerification: response && response.themeVerification,
|
|
815
846
|
navigationVerification: response && response.navigationVerification }));
|
|
816
847
|
throwCommandError(errorMsg, { code: response && response.errorCode,
|
|
817
|
-
details: response && (response.errorCode === 'APP_NAVIGATION_NOT_PERSISTED' ? response.navigationVerification : response.themeVerification)
|
|
848
|
+
details: response && { ...(response.errorCode === 'APP_NAVIGATION_NOT_PERSISTED' ? response.navigationVerification : response.themeVerification),
|
|
849
|
+
themeCompletion: response.themeCompletion, ...themeFailureEvidence(themeUpload) } });
|
|
818
850
|
}
|
|
819
851
|
}
|
|
820
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: {
|
|
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)
|
|
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
|
|
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') {
|
|
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') {
|