openyida 2026.10.9 → 2026.10.10-1
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 +1 -1
- package/lib/app/update-app.js +157 -17
- package/lib/app/upgrade-app-theme.js +38 -11
- package/lib/core/command-manifest.js +9 -5
- package/lib/core/locales/en.js +9 -5
- package/lib/core/locales/zh.js +9 -5
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/workflow/parallel-work.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +7 -7
- package/yida-skills/skills/yida-app/workflow/step-5-seed-records.md +2 -1
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +1 -1
- package/yida-skills/skills/yida-data-management/SKILL.md +12 -5
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/references/application-style-library.md +16 -29
- package/yida-skills/skills/yida-design/references/application-theme-consistency.md +3 -0
- package/yida-skills/skills/yida-design/references/native-form-styles.md +3 -0
- package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +1 -2
- package/yida-skills/skills/yida-design/templates/design-themes/index.json +0 -252
- package/yida-skills/skills/yida-design/workflow/output-design.md +2 -0
- package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -1
- package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +3 -0
- package/yida-skills/skills/yida-upgrade-app-theme/SKILL.md +3 -3
- package/yida-skills/skills-index.json +1 -1
package/README.md
CHANGED
|
@@ -403,7 +403,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
403
403
|
| `openyida design-plan preview <build-plan.json> --part-file <module.json> [--json]` | Update plan drafts by module |
|
|
404
404
|
| `openyida design-plan materialize <build-plan.json> [--from-preview \| --business-file <json> --visual-file <json>] [--output-dir <dir>] [--rebase-parts] [--check] [--json]` | Generate or update plan documents and theme with consistency checks |
|
|
405
405
|
| `openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]` | Patch plan fields and optionally sync documents and theme |
|
|
406
|
-
| `openyida update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--nav-theme light\|dark\|white\|gray] [--logo-source appIcon\|customImage] [--layout side\|top\|l_shape] [--hide-app-nav\|--show-app-nav]` | Update app info |
|
|
406
|
+
| `openyida update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light\|dark\|white\|gray] [--logo-source appIcon\|customImage] [--layout side\|top\|l_shape] [--hide-app-nav\|--show-app-nav]` | Update app info |
|
|
407
407
|
| `openyida upgrade-app-theme <appType> --explicit-request [--confirm \| --prepare --output-dir <dir>] [--json]` | Upgrade an AI-built legacy app theme (explicit request and ask_human confirmation required) |
|
|
408
408
|
| `openyida app-online <appType> [--to-ding-app-center] [--show-app-center]` | Enable a Yida app |
|
|
409
409
|
| `openyida app-offline <appType> [--to-ding-app-center] [--show-app-center]` | Disable a Yida app |
|
package/lib/app/update-app.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
} = require('../core/utils');
|
|
15
15
|
const { createAuthRef } = require('../core/yida-client');
|
|
16
16
|
const { t } = require('../core/i18n');
|
|
17
|
+
const { redactSensitive } = require('../core/redact');
|
|
17
18
|
const { buildYidaI18n } = require('../core/yida-i18n');
|
|
18
19
|
const { throwCommandError, throwUsage } = require('../core/command-errors');
|
|
19
20
|
const { assertAppThemeKey } = require('./theme-presets');
|
|
@@ -81,6 +82,8 @@ function parseArgs(args) {
|
|
|
81
82
|
themeColor: null,
|
|
82
83
|
themeFile: null,
|
|
83
84
|
customThemeStyle: null,
|
|
85
|
+
confirmLegacyAppStyle: false,
|
|
86
|
+
disableCustomTheme: false,
|
|
84
87
|
navTheme: null,
|
|
85
88
|
logoSource: null,
|
|
86
89
|
layoutDirection: null,
|
|
@@ -100,7 +103,7 @@ function parseArgs(args) {
|
|
|
100
103
|
for (let i = 1; i < args.length; i++) {
|
|
101
104
|
const arg = args[i];
|
|
102
105
|
const nextArg = args[i + 1];
|
|
103
|
-
if (!['--hide-app-nav', '--show-app-nav'].includes(arg) &&
|
|
106
|
+
if (!['--hide-app-nav', '--show-app-nav', '--confirm-legacy-app-style', '--disable-custom-theme'].includes(arg) &&
|
|
104
107
|
(!nextArg || nextArg.startsWith('--') || ['-n', '-d'].includes(nextArg))) {
|
|
105
108
|
throwCommandError(t('cli_argument.invalid', arg), { code: 'CLI_INVALID_ARGUMENTS' });
|
|
106
109
|
}
|
|
@@ -175,6 +178,12 @@ function parseArgs(args) {
|
|
|
175
178
|
i++;
|
|
176
179
|
}
|
|
177
180
|
break;
|
|
181
|
+
case '--confirm-legacy-app-style':
|
|
182
|
+
result.confirmLegacyAppStyle = true;
|
|
183
|
+
break;
|
|
184
|
+
case '--disable-custom-theme':
|
|
185
|
+
result.disableCustomTheme = true;
|
|
186
|
+
break;
|
|
178
187
|
case '--hide-app-nav':
|
|
179
188
|
result.hideAppNav = 'y';
|
|
180
189
|
break;
|
|
@@ -186,6 +195,7 @@ function parseArgs(args) {
|
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
|
|
198
|
+
assertCustomThemeOptions(result);
|
|
189
199
|
return result;
|
|
190
200
|
}
|
|
191
201
|
|
|
@@ -204,6 +214,7 @@ function hasShellUpdate(params) {
|
|
|
204
214
|
params.colour ||
|
|
205
215
|
params.themeColor ||
|
|
206
216
|
params.themeFile ||
|
|
217
|
+
params.disableCustomTheme ||
|
|
207
218
|
params.navTheme ||
|
|
208
219
|
params.logoSource ||
|
|
209
220
|
params.layoutDirection ||
|
|
@@ -240,6 +251,27 @@ function normalizeStoredLayoutDirection(layoutDirection, navType) {
|
|
|
240
251
|
return 'side';
|
|
241
252
|
}
|
|
242
253
|
|
|
254
|
+
/** 保存协议跟随应用实际主题能力;缺失运行态字段时保留既有保存行为。 */
|
|
255
|
+
function isLegacyNavigation(app) {
|
|
256
|
+
return pickAppField(app, 'appThemeEnable', 'APP_THEME_ENABLE') === 'n' ||
|
|
257
|
+
pickAppField(app, 'appThemeMode', 'APP_THEME_MODE') === 'legacy';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 旧 Shell 的布局协议与新版布局不同;保留已有旧值,不反写 navType。 */
|
|
261
|
+
function resolveSavedLayoutDirection(layoutDirection, navType, app, explicit = false) {
|
|
262
|
+
if (!isLegacyNavigation(app)) {
|
|
263
|
+
return explicit ? normalizeLayoutDirection(layoutDirection) : normalizeStoredLayoutDirection(layoutDirection, navType);
|
|
264
|
+
}
|
|
265
|
+
if (['slide', 'ver', 'hoz'].includes(layoutDirection)) {return layoutDirection;}
|
|
266
|
+
const layout = explicit ? normalizeLayoutDirection(layoutDirection) : normalizeStoredLayoutDirection(layoutDirection, navType);
|
|
267
|
+
return { side: 'ver', top: 'hoz', l_shape: 'ver' }[layout];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 旧导航没有白/灰独立风格,降级到已有浅色;不改应用品牌色和自定义 CSS。 */
|
|
271
|
+
function resolveSavedNavTheme(navTheme, app) {
|
|
272
|
+
return isLegacyNavigation(app) && ['white', 'gray'].includes(navTheme) ? 'light' : navTheme;
|
|
273
|
+
}
|
|
274
|
+
|
|
243
275
|
function pickAppField(currentApp, fieldName, configName) {
|
|
244
276
|
if (currentApp[fieldName] !== undefined && currentApp[fieldName] !== null) {
|
|
245
277
|
return currentApp[fieldName];
|
|
@@ -302,7 +334,81 @@ function buildUpdateAppNamePostData(params, authRef) {
|
|
|
302
334
|
return postDataObj;
|
|
303
335
|
}
|
|
304
336
|
|
|
337
|
+
function assertCustomThemeOptions(params) {
|
|
338
|
+
const otherUpdates = ['name', 'desc', 'icon', 'iconColor', 'colour', 'themeColor', 'themeFile',
|
|
339
|
+
'customThemeStyle', 'navTheme', 'logoSource', 'layoutDirection'];
|
|
340
|
+
if (params.disableCustomTheme && (otherUpdates.some(key => params[key]) ||
|
|
341
|
+
(params.hideAppNav !== null && params.hideAppNav !== undefined))) {
|
|
342
|
+
throwCommandError(t('update_app.disable_theme_conflict'), { code: 'CLI_INVALID_ARGUMENTS' });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function disabledCustomThemeStyle(rawStyle) {
|
|
347
|
+
let style = rawStyle;
|
|
348
|
+
if (typeof style === 'string') {
|
|
349
|
+
try {style = JSON.parse(style);} catch (_) {style = null;}
|
|
350
|
+
}
|
|
351
|
+
if (!style || Array.isArray(style) || typeof style !== 'object' ||
|
|
352
|
+
typeof style.cssUrl !== 'string' || !style.cssUrl.trim()) {
|
|
353
|
+
throwCommandError(t('update_app.custom_theme_config_invalid'), { code: 'APP_CUSTOM_THEME_CONFIG_INVALID' });
|
|
354
|
+
}
|
|
355
|
+
return JSON.stringify({ ...style, enabled: false, iframePropagation: false });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// A disable operation preserves the exact latest basic settings, including empty values.
|
|
359
|
+
function preserveBasicSettings(payload, app) {
|
|
360
|
+
const configKeys = {
|
|
361
|
+
colour: 'COLOUR', themeColor: 'THEME_COLOR', mode: 'APPMODE', type: 'APPTYPE', navLayout: 'NAVLAYOUT',
|
|
362
|
+
layoutDirection: 'LAY_OUT_DIRECTION', navType: 'NAVTYPE', navTheme: 'NAV_THEME', homepageLogo: 'HOMEPAGELOGO',
|
|
363
|
+
logoSource: 'LOGO_SOURCE', logoLink: 'LOGOLINK', systemLink: 'SYSTEMLINK', showAppTitle: 'SHOWAPPTITLE',
|
|
364
|
+
showIcon: 'SHOWICON', showNav: 'SHOWNAV', showCrumb: 'SHOWCRUMB', showAppCenter: 'SHOWAPPCENTER',
|
|
365
|
+
deviceType: 'DEVICETYPE', detailLayout: 'DETAIL_LAYOUT', procOperateRecordLayout: 'PROC_OPERATERECORD_LAYOUT',
|
|
366
|
+
navigation: 'NAVIGATION', pageHeader: 'PAGEHEADER', pageFooter: 'PAGEFOOTER',
|
|
367
|
+
addWaterMark: 'ADDWATERMARK', sentryMode: 'SENTRY_MODE', hideAppNav: 'HIDE_APP_NAV',
|
|
368
|
+
};
|
|
369
|
+
for (const [field, key] of Object.entries(configKeys)) {
|
|
370
|
+
const value = pickAppField(app, field, key);
|
|
371
|
+
if (value !== undefined && value !== null) {payload[field] = value;}
|
|
372
|
+
else {delete payload[field];}
|
|
373
|
+
}
|
|
374
|
+
for (const field of ['icon', 'iconUrl']) {
|
|
375
|
+
if (app[field] !== undefined && app[field] !== null) {payload[field] = app[field];}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
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) {
|
|
383
|
+
throwCommandError(t('update_app.legacy_style_confirm_required'), {
|
|
384
|
+
code: 'LEGACY_APP_CUSTOM_THEME_CONFIRMATION_REQUIRED',
|
|
385
|
+
details: { appType: params.appType, applicationSettingsWritten: false, nextAction: 'ask_user_for_application_style_injection' },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Runtime capability is resolved by the server. Stored APP_THEME_MODE alone is insufficient.
|
|
391
|
+
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);
|
|
408
|
+
}
|
|
409
|
+
|
|
305
410
|
function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
411
|
+
assertCustomThemeOptions(params);
|
|
306
412
|
assertAppThemeKey(params.colour);
|
|
307
413
|
if (params.colour && params.colour !== 'custom' && (params.themeFile || params.customThemeStyle || params.themeColor)) {
|
|
308
414
|
throw new Error(t('update_app.theme_preset_conflict'));
|
|
@@ -341,9 +447,8 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
|
341
447
|
mode: currentApp.mode || (currentApp.config && currentApp.config.APPMODE) || 'normal',
|
|
342
448
|
type: currentApp.type || (currentApp.config && currentApp.config.APPTYPE) || 'single',
|
|
343
449
|
navLayout: currentApp.navLayout || (currentApp.config && currentApp.config.NAVLAYOUT) || 'auto',
|
|
344
|
-
layoutDirection:
|
|
345
|
-
|
|
346
|
-
: normalizeStoredLayoutDirection(currentLayoutDirection, currentNavType),
|
|
450
|
+
layoutDirection: resolveSavedLayoutDirection(
|
|
451
|
+
params.layoutDirection || currentLayoutDirection, currentNavType, currentApp, !!params.layoutDirection),
|
|
347
452
|
homepageLogo: currentApp.homepageLogo || (currentApp.config && currentApp.config.HOMEPAGELOGO) || '',
|
|
348
453
|
logoSource: params.logoSource || currentApp.logoSource || (currentApp.config && currentApp.config.LOGO_SOURCE) || 'appIcon',
|
|
349
454
|
logoLink: currentApp.logoLink || (currentApp.config && currentApp.config.LOGOLINK) || '',
|
|
@@ -370,7 +475,12 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
|
370
475
|
|
|
371
476
|
// ThemeNavSetting 保存现有 navType;新布局由 layoutDirection 控制,不反写或补造旧字段。
|
|
372
477
|
if (currentNavType) {postDataObj.navType = currentNavType;}
|
|
373
|
-
|
|
478
|
+
if (params.disableCustomTheme && !params.layoutDirection) {
|
|
479
|
+
if (currentLayoutDirection) {postDataObj.layoutDirection = currentLayoutDirection;}
|
|
480
|
+
else {delete postDataObj.layoutDirection;}
|
|
481
|
+
}
|
|
482
|
+
const navTheme = params.disableCustomTheme && !params.navTheme
|
|
483
|
+
? currentNavTheme : resolveSavedNavTheme(params.navTheme || currentNavTheme, currentApp);
|
|
374
484
|
if (navTheme) {postDataObj.navTheme = navTheme;}
|
|
375
485
|
if (addWaterMark !== undefined && addWaterMark !== null) {
|
|
376
486
|
postDataObj.addWaterMark = addWaterMark;
|
|
@@ -394,7 +504,10 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
|
394
504
|
postDataObj.themeColor = currentThemeColor;
|
|
395
505
|
}
|
|
396
506
|
|
|
397
|
-
if (params.
|
|
507
|
+
if (params.disableCustomTheme) {
|
|
508
|
+
preserveBasicSettings(postDataObj, currentApp);
|
|
509
|
+
postDataObj.customThemeStyle = disabledCustomThemeStyle(currentCustomThemeStyle);
|
|
510
|
+
} else if (params.colour && params.colour !== 'custom' && !params.customThemeStyle) {
|
|
398
511
|
// 切回平台预置主题时必须清空旧 CSS,避免 CUSTOM_THEME_STYLE 继续覆盖 colour。
|
|
399
512
|
postDataObj.customThemeStyle = '';
|
|
400
513
|
} else {
|
|
@@ -449,7 +562,7 @@ function themeSettingsMatch(expected, actual) {
|
|
|
449
562
|
if (!expectedStyle || !actualStyle || typeof expectedStyle !== 'object' || typeof actualStyle !== 'object') {
|
|
450
563
|
return expectedStyle === actualStyle;
|
|
451
564
|
}
|
|
452
|
-
return ['enabled', 'iframePropagation'].every((key) =>
|
|
565
|
+
return ['enabled', 'iframePropagation'].every((key) => expectedStyle[key] === actualStyle[key]) &&
|
|
453
566
|
['cssUrl', 'cssFileName'].every((key) => (expectedStyle[key] || '') === (actualStyle[key] || ''));
|
|
454
567
|
}
|
|
455
568
|
|
|
@@ -459,14 +572,16 @@ function getRequestedNavigationSettings(params, payload) {
|
|
|
459
572
|
.map((name) => [name, payload[name]]));
|
|
460
573
|
}
|
|
461
574
|
|
|
462
|
-
function getNavigationSettings(app, fields) {
|
|
463
|
-
const configKeys = { navTheme: 'NAV_THEME', layoutDirection: 'LAY_OUT_DIRECTION', hideAppNav: 'HIDE_APP_NAV', logoSource: 'LOGO_SOURCE' };
|
|
575
|
+
function getNavigationSettings(app, fields, currentApp = app, rawLayout = false) {
|
|
576
|
+
const configKeys = { navTheme: 'NAV_THEME', layoutDirection: 'LAY_OUT_DIRECTION', hideAppNav: 'HIDE_APP_NAV', logoSource: 'LOGO_SOURCE', navType: 'NAVTYPE', homepageLogo: 'HOMEPAGELOGO' };
|
|
464
577
|
return Object.fromEntries(fields.map((name) => {
|
|
465
578
|
let value = pickAppField(app, name, configKeys[name]);
|
|
466
|
-
if (name === 'layoutDirection') {
|
|
579
|
+
if (name === 'layoutDirection' && !rawLayout) {
|
|
467
580
|
const navType = pickAppField(app, 'navType', 'NAVTYPE');
|
|
468
581
|
// A missing response field is not evidence that the requested default was saved.
|
|
469
|
-
value =
|
|
582
|
+
value = isLegacyNavigation(currentApp)
|
|
583
|
+
? (value || null)
|
|
584
|
+
: (value || navType ? normalizeStoredLayoutDirection(value, navType) : null);
|
|
470
585
|
}
|
|
471
586
|
return [name, value === undefined ? null : value];
|
|
472
587
|
}));
|
|
@@ -474,18 +589,28 @@ function getNavigationSettings(app, fields) {
|
|
|
474
589
|
|
|
475
590
|
/** updateApp 整包保存;读取最新配置后合并,并核验主题资源和本次请求的导航设置。 */
|
|
476
591
|
async function saveAppSettings(params, authRef) {
|
|
592
|
+
assertCustomThemeOptions(params);
|
|
593
|
+
if (params.customThemeStyle) {await checkAppStyleInjection(params, authRef);}
|
|
477
594
|
const currentApp = await fetchCurrentApp(params.appType, authRef);
|
|
478
595
|
const payload = buildUpdateAppPostData(params, currentApp, authRef);
|
|
596
|
+
const submittedFields = params.disableCustomTheme ? redactSensitive({
|
|
597
|
+
customThemeStyle: JSON.parse(payload.customThemeStyle), colour: payload.colour, themeColor: payload.themeColor,
|
|
598
|
+
navType: payload.navType, navTheme: payload.navTheme, layoutDirection: payload.layoutDirection,
|
|
599
|
+
logoSource: payload.logoSource, homepageLogo: payload.homepageLogo,
|
|
600
|
+
}) : undefined;
|
|
479
601
|
const response = await requestWithAutoLogin((auth) => httpPost(
|
|
480
602
|
auth.baseUrl,
|
|
481
603
|
`/${params.appType}/query/app/updateApp.json?_api=Form.updateApp&_mock=false&_stamp=${Date.now()}`,
|
|
482
604
|
querystring.stringify({ ...payload, _csrf_token: auth.csrfToken })
|
|
483
605
|
), authRef);
|
|
484
|
-
if (!response || !response.success) { return response; }
|
|
606
|
+
if (!response || !response.success) { return submittedFields ? { ...response, submittedFields } : response; }
|
|
485
607
|
|
|
486
608
|
const expected = getThemeSettings(payload);
|
|
487
|
-
const verifyTheme = !!(params.colour || params.themeColor || params.customThemeStyle || expected.themeColor || expected.customThemeStyle);
|
|
488
|
-
const expectedNavigation =
|
|
609
|
+
const verifyTheme = !!(params.colour || params.themeColor || params.customThemeStyle || params.disableCustomTheme || expected.themeColor || expected.customThemeStyle);
|
|
610
|
+
const expectedNavigation = params.disableCustomTheme
|
|
611
|
+
? Object.fromEntries(['navType', 'navTheme', 'layoutDirection', 'hideAppNav', 'logoSource', 'homepageLogo']
|
|
612
|
+
.filter(field => Object.hasOwn(payload, field)).map(field => [field, payload[field]]))
|
|
613
|
+
: getRequestedNavigationSettings(params, payload);
|
|
489
614
|
const navigationFields = Object.keys(expectedNavigation);
|
|
490
615
|
if (!verifyTheme && !navigationFields.length) {return response;}
|
|
491
616
|
let actual;
|
|
@@ -499,12 +624,13 @@ async function saveAppSettings(params, authRef) {
|
|
|
499
624
|
try {
|
|
500
625
|
const app = await fetchCurrentApp(params.appType, authRef);
|
|
501
626
|
actual = getThemeSettings(app);
|
|
502
|
-
actualNavigation = getNavigationSettings(app, navigationFields);
|
|
627
|
+
actualNavigation = getNavigationSettings(app, navigationFields, currentApp, params.disableCustomTheme);
|
|
503
628
|
readbackError = undefined;
|
|
504
629
|
themeVerified = !verifyTheme || themeSettingsMatch(expected, actual);
|
|
505
630
|
navigationVerified = navigationFields.every((name) => expectedNavigation[name] === actualNavigation[name]);
|
|
506
631
|
if (themeVerified && navigationVerified) {
|
|
507
632
|
return { ...response,
|
|
633
|
+
...(submittedFields ? { submittedFields } : {}),
|
|
508
634
|
...(verifyTheme ? { themeVerification: { verified: true, ...actual } } : {}),
|
|
509
635
|
...(navigationFields.length ? { navigationVerification: { verified: true, ...actualNavigation } } : {}),
|
|
510
636
|
};
|
|
@@ -517,6 +643,7 @@ async function saveAppSettings(params, authRef) {
|
|
|
517
643
|
}
|
|
518
644
|
return {
|
|
519
645
|
success: false,
|
|
646
|
+
...(submittedFields ? { submittedFields } : {}),
|
|
520
647
|
errorCode: themeVerified ? 'APP_NAVIGATION_NOT_PERSISTED' : 'APP_THEME_NOT_PERSISTED',
|
|
521
648
|
errorMsg: t(themeVerified ? 'update_app.navigation_not_persisted' : 'update_app.theme_not_persisted'),
|
|
522
649
|
...(verifyTheme ? { themeVerification: themeVerified
|
|
@@ -536,6 +663,8 @@ async function applyCustomThemeUpdate(appType, themeParams, authRef) {
|
|
|
536
663
|
assertNavTheme(params.navTheme);
|
|
537
664
|
assertLogoSource(params.logoSource);
|
|
538
665
|
|
|
666
|
+
assertCustomThemeOptions(params);
|
|
667
|
+
await checkAppStyleInjection(params, authRef);
|
|
539
668
|
const themeFile = readThemeCssFile(params.themeFile);
|
|
540
669
|
params.themeColor = themeFile.themeColor;
|
|
541
670
|
const uploadResponse = await requestWithAutoLogin(
|
|
@@ -544,7 +673,13 @@ async function applyCustomThemeUpdate(appType, themeParams, authRef) {
|
|
|
544
673
|
);
|
|
545
674
|
params.customThemeStyle = buildCustomThemeStyle(uploadResponse);
|
|
546
675
|
|
|
547
|
-
|
|
676
|
+
try {
|
|
677
|
+
return await saveAppSettings(params, authRef);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
error.details = { ...error.details, appType, uploadedResource: params.customThemeStyle,
|
|
680
|
+
applicationSettingsVerified: false };
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
548
683
|
}
|
|
549
684
|
|
|
550
685
|
async function run(args) {
|
|
@@ -570,6 +705,7 @@ async function run(args) {
|
|
|
570
705
|
!params.colour &&
|
|
571
706
|
!params.themeColor &&
|
|
572
707
|
!params.themeFile &&
|
|
708
|
+
!params.disableCustomTheme &&
|
|
573
709
|
!params.navTheme &&
|
|
574
710
|
!params.logoSource &&
|
|
575
711
|
!params.layoutDirection &&
|
|
@@ -579,6 +715,7 @@ async function run(args) {
|
|
|
579
715
|
printUsage();
|
|
580
716
|
throwUsage(t('update_app.missing_update_field'), t('update_app.usage'));
|
|
581
717
|
}
|
|
718
|
+
assertCustomThemeOptions(params);
|
|
582
719
|
assertAppThemeKey(params.colour);
|
|
583
720
|
if (params.colour && params.colour !== 'custom' && (params.themeFile || params.themeColor)) {
|
|
584
721
|
throw new Error(t('update_app.theme_preset_conflict'));
|
|
@@ -635,13 +772,14 @@ async function run(args) {
|
|
|
635
772
|
if (response && response.success) {
|
|
636
773
|
const details = [['appType', params.appType]];
|
|
637
774
|
if (params.name) {details.push(['Name', params.name]);}
|
|
638
|
-
chalkResult(true, t('update_app.success'), details);
|
|
775
|
+
chalkResult(true, t(params.disableCustomTheme ? 'update_app.custom_theme_disabled' : 'update_app.success'), details);
|
|
639
776
|
|
|
640
777
|
console.log(JSON.stringify({
|
|
641
778
|
success: true,
|
|
642
779
|
appType: params.appType,
|
|
643
780
|
themeVerification: response.themeVerification,
|
|
644
781
|
navigationVerification: response.navigationVerification,
|
|
782
|
+
...(params.disableCustomTheme ? { runtimeVerification: { verified: false } } : {}),
|
|
645
783
|
updatedFields: {
|
|
646
784
|
name: params.name || undefined,
|
|
647
785
|
desc: params.desc || undefined,
|
|
@@ -649,6 +787,7 @@ async function run(args) {
|
|
|
649
787
|
colour: params.colour || undefined,
|
|
650
788
|
themeColor: params.themeColor || undefined,
|
|
651
789
|
themeFile: params.themeFile || undefined,
|
|
790
|
+
customThemeStyle: params.disableCustomTheme ? response.themeVerification.customThemeStyle : undefined,
|
|
652
791
|
navTheme: params.navTheme || undefined,
|
|
653
792
|
logoSource: params.logoSource || undefined,
|
|
654
793
|
layoutDirection: params.layoutDirection || undefined,
|
|
@@ -659,6 +798,7 @@ async function run(args) {
|
|
|
659
798
|
const errorMsg = response ? response.errorMsg || t('common.unknown_error') : t('common.request_failed');
|
|
660
799
|
chalkResult(false, t('update_app.failed', errorMsg));
|
|
661
800
|
console.log(JSON.stringify({ success: false, appType: params.appType, error: errorMsg,
|
|
801
|
+
submittedFields: response && response.submittedFields,
|
|
662
802
|
errorCode: response && response.errorCode, themeVerification: response && response.themeVerification,
|
|
663
803
|
navigationVerification: response && response.navigationVerification }));
|
|
664
804
|
throwCommandError(errorMsg, { code: response && response.errorCode,
|
|
@@ -24,7 +24,7 @@ function parseArgs(args = []) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Read only literal fields from the server-rendered admin configuration. Never execute HTML/JS.
|
|
27
|
-
function parseThemeContext(html) {
|
|
27
|
+
function parseThemeContext(html, wanted = ['appType', 'corpId', 'agentAppType']) {
|
|
28
28
|
const fail = () => throwCommandError(t('upgrade_app_theme.context_failed'), { code: 'APP_THEME_CONTEXT_UNVERIFIED' });
|
|
29
29
|
if (typeof html !== 'string') { return fail(); }
|
|
30
30
|
const contexts = [];
|
|
@@ -40,7 +40,6 @@ function parseThemeContext(html) {
|
|
|
40
40
|
expr.left.object.name !== 'window' || expr.left.property.name !== 'pageConfig') { continue; }
|
|
41
41
|
if (expr.right.type !== 'ObjectExpression') { return fail(); }
|
|
42
42
|
const context = {};
|
|
43
|
-
const wanted = ['appType', 'corpId', 'agentAppType'];
|
|
44
43
|
for (const prop of expr.right.properties) {
|
|
45
44
|
if (prop.type === 'SpreadElement' || prop.computed) { return fail(); }
|
|
46
45
|
const key = prop.key.name || prop.key.value;
|
|
@@ -71,17 +70,34 @@ async function readThemeEligibility(appType, auth) {
|
|
|
71
70
|
return context;
|
|
72
71
|
}
|
|
73
72
|
|
|
73
|
+
/** 升级时将历史导航协议转换为新版值,保留导航结构与已配置的新值。 */
|
|
74
|
+
function resolveModernNavigationConfig(previous) {
|
|
75
|
+
const layout = previous.LAY_OUT_DIRECTION;
|
|
76
|
+
const navType = previous.NAVTYPE;
|
|
77
|
+
const targets = {};
|
|
78
|
+
// 旧版 ver 默认是侧导;只有明确的 top_side 结构才恢复 L 型。
|
|
79
|
+
const layouts = { slide: 'side', ver: navType === 'top_side' ? 'l_shape' : 'side',
|
|
80
|
+
hoz: navType === 'top_side' ? 'l_shape' : 'top' };
|
|
81
|
+
if (Object.hasOwn(layouts, layout)) {targets.LAY_OUT_DIRECTION = layouts[layout];}
|
|
82
|
+
// APP_THEME_MODE=modern 的 light 已是新版浅品牌色,重复升级不能把它变成 white。
|
|
83
|
+
const wasLegacy = previous.APP_THEME_MODE !== 'modern';
|
|
84
|
+
const colors = { default: 'light', light: 'white', dark: 'dark', clear: 'white' };
|
|
85
|
+
if (wasLegacy && Object.hasOwn(colors, previous.NAV_THEME)) {targets.NAV_THEME = colors[previous.NAV_THEME];}
|
|
86
|
+
return targets;
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
// Both attestations are required even for callers that bypass the CLI runner.
|
|
75
90
|
// They record the agent's authorization checks; they cannot prove a human reply.
|
|
76
91
|
async function upgradeAppTheme(params, authRef) {
|
|
77
92
|
if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType)) {
|
|
78
93
|
throwUsage(t('upgrade_app_theme.usage'));
|
|
79
94
|
}
|
|
95
|
+
const auth = authRef || createAuthRef();
|
|
96
|
+
// Reject ineligible apps before offering upgrade confirmation; flags cannot bypass this gate.
|
|
97
|
+
await readThemeEligibility(params.appType, auth);
|
|
80
98
|
if (params.explicitRequest !== true || params.confirm !== true) {
|
|
81
99
|
throwCommandError(t('upgrade_app_theme.confirm_required'), { code: 'APP_THEME_CONFIRMATION_REQUIRED' });
|
|
82
100
|
}
|
|
83
|
-
const auth = authRef || createAuthRef();
|
|
84
|
-
await readThemeEligibility(params.appType, auth);
|
|
85
101
|
const targets = { CREATED_WITH_MODERN_THEME: 'y', APP_THEME_MODE: 'modern' };
|
|
86
102
|
const request = (endpoint, fields) => requestWithAutoLogin((current) => httpPost(
|
|
87
103
|
current.baseUrl,
|
|
@@ -97,13 +113,18 @@ async function upgradeAppTheme(params, authRef) {
|
|
|
97
113
|
}
|
|
98
114
|
return emptyContent ? null : response.content;
|
|
99
115
|
};
|
|
100
|
-
// Read
|
|
116
|
+
// Read theme and navigation before writing anything; these endpoints are not transactional.
|
|
101
117
|
const previous = {};
|
|
102
|
-
for (const key of Object.keys(targets)
|
|
118
|
+
for (const key of [...Object.keys(targets), 'LAY_OUT_DIRECTION', 'NAV_THEME', 'NAVTYPE']) {
|
|
119
|
+
previous[key] = await readConfig(key);
|
|
120
|
+
}
|
|
121
|
+
const navigationTargets = resolveModernNavigationConfig(previous);
|
|
122
|
+
// 先写导航,再启用主题,避免配色/布局迁移失败后留下已启用的新主题。
|
|
123
|
+
const allTargets = { ...navigationTargets, ...targets };
|
|
103
124
|
const verifiedWrites = [];
|
|
104
125
|
let attemptedKey;
|
|
105
126
|
try {
|
|
106
|
-
for (const [key, value] of Object.entries(
|
|
127
|
+
for (const [key, value] of Object.entries(allTargets)) {
|
|
107
128
|
if (previous[key] === value) { continue; }
|
|
108
129
|
attemptedKey = key;
|
|
109
130
|
const response = await request('updateSingleConfig', { key, value });
|
|
@@ -115,9 +136,9 @@ async function upgradeAppTheme(params, authRef) {
|
|
|
115
136
|
}
|
|
116
137
|
verifiedWrites.push(key);
|
|
117
138
|
}
|
|
118
|
-
// Verify
|
|
139
|
+
// Verify all migration targets again after writes; each value needs independent readback.
|
|
119
140
|
if (attemptedKey) {
|
|
120
|
-
for (const [key, value] of Object.entries(
|
|
141
|
+
for (const [key, value] of Object.entries(allTargets)) {
|
|
121
142
|
if (await readConfig(key) !== value) {
|
|
122
143
|
throwCommandError(t('upgrade_app_theme.verify_failed'), { code: 'APP_THEME_NOT_PERSISTED' });
|
|
123
144
|
}
|
|
@@ -133,7 +154,10 @@ async function upgradeAppTheme(params, authRef) {
|
|
|
133
154
|
return { success: true, appType: params.appType, changed: verifiedWrites.length > 0,
|
|
134
155
|
verified: true, verificationScope: 'config', runtimeVerified: false, appUpgradeComplete: false,
|
|
135
156
|
nextStep: 'migrate-pages-and-clean-form-styles-with-yida-upgrade-app-theme', previous,
|
|
136
|
-
createdWithModernTheme: 'y', appThemeMode: 'modern',
|
|
157
|
+
createdWithModernTheme: 'y', appThemeMode: 'modern', navigation: {
|
|
158
|
+
layoutDirection: navigationTargets.LAY_OUT_DIRECTION || previous.LAY_OUT_DIRECTION,
|
|
159
|
+
navTheme: navigationTargets.NAV_THEME || previous.NAV_THEME,
|
|
160
|
+
}, updatedKeys: verifiedWrites };
|
|
137
161
|
|
|
138
162
|
}
|
|
139
163
|
|
|
@@ -145,11 +169,14 @@ async function run(args = []) {
|
|
|
145
169
|
return;
|
|
146
170
|
}
|
|
147
171
|
if (params.prepare) {
|
|
148
|
-
if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType)
|
|
172
|
+
if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType)) {
|
|
149
173
|
throwUsage(t('upgrade_app_theme.prepare_output_required'));
|
|
150
174
|
}
|
|
151
175
|
const auth = createAuthRef();
|
|
152
176
|
await readThemeEligibility(params.appType, auth);
|
|
177
|
+
if (!params.explicitRequest || !params.outputDir) {
|
|
178
|
+
throwUsage(t('upgrade_app_theme.prepare_output_required'));
|
|
179
|
+
}
|
|
153
180
|
const { prepareAppThemeUpgrade } = require('./prepare-app-theme-upgrade');
|
|
154
181
|
const prepared = await prepareAppThemeUpgrade(params, auth);
|
|
155
182
|
if (!prepared.success) {
|
|
@@ -1139,7 +1139,7 @@ const COMMAND_GROUPS = [
|
|
|
1139
1139
|
{ name: 'json', type: 'boolean', source: 'option', builder_options: ['--json'] },
|
|
1140
1140
|
],
|
|
1141
1141
|
}),
|
|
1142
|
-
command('update-app', ['update-app'], 'update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape] [--hide-app-nav|--show-app-nav]', 'help.cmd_update_app', {
|
|
1142
|
+
command('update-app', ['update-app'], 'update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape] [--hide-app-nav|--show-app-nav]', 'help.cmd_update_app', {
|
|
1143
1143
|
args: [
|
|
1144
1144
|
{ name: 'appType', type: 'string', required: true, source: 'positional', position: 0 },
|
|
1145
1145
|
{ name: 'name', type: 'string', source: 'option', builder_options: ['--name', '-n'] },
|
|
@@ -1149,15 +1149,19 @@ const COMMAND_GROUPS = [
|
|
|
1149
1149
|
{ name: 'colour', type: 'string', source: 'option', builder_options: ['--colour', '--theme'], description: 'Platform theme key or custom; CSS colors belong in themeColor.' },
|
|
1150
1150
|
{ name: 'themeColor', type: 'string', source: 'option', builder_options: ['--theme-color', '--themeColor'], description: 'App primary color; themeFile supplies the effective color when both are provided.' },
|
|
1151
1151
|
{ name: 'themeFile', type: 'file', source: 'option', builder_options: ['--theme-file', '--custom-theme-file'], description: 'Completed application theme CSS, not a placeholder authoring bundle. Reads --color-brand1-6, uploads CSS and sets colour=custom. Supports app-theme.css or app_theme.css; use the actual generated path.' },
|
|
1152
|
-
{ name: '
|
|
1152
|
+
{ name: 'confirmLegacyAppStyle', type: 'boolean', source: 'option', builder_options: ['--confirm-legacy-app-style'], description: 'Attests to an affirmative human reply for this exact legacy/unknown app and reviewed CSS scope; never infer consent from a request to beautify a page.' },
|
|
1153
|
+
{ name: 'disableCustomTheme', type: 'boolean', source: 'option', builder_options: ['--disable-custom-theme'], description: 'Disable app-level CSS by serializing the existing customThemeStyle with boolean enabled=false and iframePropagation=false. Retain cssUrl/cssFileName and other basic settings. Cannot be combined with other updates.' },
|
|
1154
|
+
{ name: 'navTheme', type: 'string', source: 'option', builder_options: ['--nav-theme', '--navTheme'], description: 'Platform value: light, dark, white or gray; disabled/legacy apps persist white/gray as light. Generated design flows pass the selected template-derived light/dark value explicitly; this option has no implicit light default.' },
|
|
1153
1155
|
{ name: 'logoSource', type: 'string', source: 'option', builder_options: ['--logo-source', '--logoSource'], description: 'appIcon or customImage; customImage requires an existing homepageLogo.' },
|
|
1154
|
-
{ name: 'layoutDirection', type: 'string', source: 'option', builder_options: ['--layout', '--layout-direction', '--layoutDirection'], description: 'side, top or l_shape.' },
|
|
1156
|
+
{ name: 'layoutDirection', type: 'string', source: 'option', builder_options: ['--layout', '--layout-direction', '--layoutDirection'], description: 'side, top or l_shape. Disabled or legacy apps persist ver, hoz or ver respectively.' },
|
|
1155
1157
|
{ name: 'hideAppNav', type: 'boolean', source: 'option', builder_options: ['--hide-app-nav'] },
|
|
1156
1158
|
{ name: 'showAppNav', type: 'boolean', source: 'option', builder_options: ['--show-app-nav'] },
|
|
1157
1159
|
],
|
|
1158
1160
|
notes: [
|
|
1161
|
+
'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, then refresh the actual visit page to verify link#yida-app-custom-theme is absent and the CSS is not requested; HTTP success alone is insufficient.',
|
|
1159
1162
|
'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.',
|
|
1160
1163
|
'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.',
|
|
1164
|
+
'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.',
|
|
1161
1165
|
'navigationVerification reads back explicitly requested navTheme, layoutDirection, hideAppNav and logoSource. A mismatch returns APP_NAVIGATION_NOT_PERSISTED and a nonzero exit code; only readback is retried. themeVerification independently verifies the uploaded theme resource. Verify rendered navigation, form, custom page and record detail after both checks pass.',
|
|
1162
1166
|
'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.',
|
|
1163
1167
|
'Platform preset keys are used independently of --theme-file and --theme-color. Choose --hide-app-nav or --show-app-nav to set navigation visibility.',
|
|
@@ -1173,7 +1177,7 @@ const COMMAND_GROUPS = [
|
|
|
1173
1177
|
{ name: 'json', type: 'boolean', source: 'option', builder_options: ['--json'] },
|
|
1174
1178
|
{ name: 'confirm', type: 'boolean', source: 'option', builder_options: ['--confirm'] },
|
|
1175
1179
|
],
|
|
1176
|
-
notes: ['--prepare reads remote schemas/source bundles into a new local backup directory and produces a per-resource migration plan without remote writes; it requires explicit upgrade intent but no mutation confirmation. Before --confirm, use the independent upgrade Skill to prepare/review sources, migrate legacy JSX to Canvas without business/layout changes, and remove verified form-detail style injection. Only mutate after an explicit user request to upgrade an existing app to the modern theme AND an affirmative ask_human reply for that app. These flags attest to both checks; they do not replace human confirmation. If ask_human is unavailable, stop before mutation.
|
|
1180
|
+
notes: ['--prepare reads remote schemas/source bundles into a new local backup directory and produces a per-resource migration plan without remote writes; it requires explicit upgrade intent but no mutation confirmation. Before --confirm, use the independent upgrade Skill to prepare/review sources, migrate legacy JSX to Canvas without business/layout changes, and remove verified form-detail style injection. Only mutate after an explicit user request to upgrade an existing app to the modern theme AND an affirmative ask_human reply for that app. These flags attest to both checks; they do not replace human confirmation. If ask_human is unavailable, stop before mutation. The CLI itself checks server-derived agentAppType and app/corp identity before offering any confirmation or preparing backups. agentAppType comes from FROM_BUILDER_AI=y plus BUILDER_AI_SOURCE; source alone or FROM_AI does not qualify. Only local/cloud/qwenwork qualifies. Reject non-AI or unverified apps without offering an upgrade or exposing source fields. Skill guidance and confirmation flags cannot bypass this gate. Do not check V4, appThemeEnable, organization theme gray or upgrade gray. After ask_human approval, migrate legacy navigation layout and colors before persisting CREATED_WITH_MODERN_THEME=y and APP_THEME_MODE=modern, then read all changed settings back. Already modern navigation colors stay unchanged. Report partial/unknown writes on failure. Success verifies configuration only, not page migration or runtime rendering. appUpgradeComplete remains false until the Skill verifies all page/form migrations.'],
|
|
1177
1181
|
}),
|
|
1178
1182
|
command('app-online', ['app-online'], 'app-online <appType> [--to-ding-app-center] [--show-app-center]', 'help.cmd_app_online'),
|
|
1179
1183
|
command('app-offline', ['app-offline'], 'app-offline <appType> [--to-ding-app-center] [--show-app-center]', 'help.cmd_app_offline'),
|
|
@@ -1889,7 +1893,7 @@ function summarizeLocalizedCommands(commands) {
|
|
|
1889
1893
|
plan_command_ids: ['design-plan.catalog', 'design-plan.init', 'design-plan.preview', 'design-plan.materialize', 'design-plan.patch'],
|
|
1890
1894
|
theme_command_ids: ['sample', 'create-app', 'update-app'],
|
|
1891
1895
|
navigation_command_ids: { platform: ['update-app', 'nav-group'], custom: ['update-app', 'update-form-config', 'get-form-config'] },
|
|
1892
|
-
navigation_policy: 'Before PRD planning in Fast and Plan, the agent determines navigation ownership and layout from business context. Follow yida-design/references/navigation-decision.md for entry and layout decisions. Preserve explicit user requirements and existing navigation for local changes; resource-only requests operate on their specified resources and settings. Use native navigation by default for the application workspace, and custom navigation only for an explicitly justified application-level branded menu or special navigation interaction. A service frontend defaults to one coding page with its own menu and all selected business views; a single-step entry can use navigation.type=none. Choose backend native or coding pages by task efficiency. Backend coding pages in platform-shell are content-only: platform menus own cross-module navigation; local Tabs cover categories or states of the current task only. Never turn the management entry menu into a second page navbar, sidebar or module Tabs. Persistent filters/state alone do not justify custom application navigation. Plan handoff pages include derived navigationPolicy; Fast records the same boundary. Do not hide platform navigation to repair duplicate page menus. Store application navigation in navigationType and standalone entry menus in pageSpecHandoff.navigation. Configure a custom frontend menu at page scope and preserve workspace navigation. Record type/source/reason/variant using ai_default for agent ownership decisions and user_selected for explicit user requirements. Include navigation in the overall Plan confirmation; Fast implements the recorded decision. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, persisting layoutDirection and hideAppNav=n. Preserve stored navType.
|
|
1896
|
+
navigation_policy: 'Before PRD planning in Fast and Plan, the agent determines navigation ownership and layout from business context. Follow yida-design/references/navigation-decision.md for entry and layout decisions. Preserve explicit user requirements and existing navigation for local changes; resource-only requests operate on their specified resources and settings. Use native navigation by default for the application workspace, and custom navigation only for an explicitly justified application-level branded menu or special navigation interaction. A service frontend defaults to one coding page with its own menu and all selected business views; a single-step entry can use navigation.type=none. Choose backend native or coding pages by task efficiency. Backend coding pages in platform-shell are content-only: platform menus own cross-module navigation; local Tabs cover categories or states of the current task only. Never turn the management entry menu into a second page navbar, sidebar or module Tabs. Persistent filters/state alone do not justify custom application navigation. Plan handoff pages include derived navigationPolicy; Fast records the same boundary. Do not hide platform navigation to repair duplicate page menus. Store application navigation in navigationType and standalone entry menus in pageSpecHandoff.navigation. Configure a custom frontend menu at page scope and preserve workspace navigation. Record type/source/reason/variant using ai_default for agent ownership decisions and user_selected for explicit user requirements. Include navigation in the overall Plan confirmation; Fast implements the recorded decision. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, persisting layoutDirection and hideAppNav=n. Preserve stored navType. For appThemeEnable=n or appThemeMode=legacy, persist side/top/l_shape as ver/hoz/ver and white/gray as light; without --layout preserve stored slide/ver/hoz. For modern or unknown capability, normalize layoutDirection and navType using the existing application layout compatibility rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Apply shell top_fold/none overrides at page scope. Application-wide custom side/top/mixed/dock uses app hiding plus per-page hiding within scope; a standalone frontend in a platform-navigation app uses only page configuration. Read back layoutDirection, hideAppNav and each configured page, then verify the planned frontend and workspace links. Navigation structure is separate from tone. Navigation tone is derived from the final selected theme template, never guessed from the business brief or implicitly defaulted to light; apply that explicit value with update-app --nav-theme. Custom top navigation defaults to edge-to-edge, not floating. Over a hero image, start transparent, add a surface on scroll, restore transparency at the top; expanded menus stay readable. Follow yida-design/references/page-continuity.md; floating requires an explicit design.',
|
|
1893
1897
|
design_mode_policy: 'Analyze requirements first with yida-requirement-analysis: read only necessary sources before the first question, offer concrete core-function candidates, clarify key usage, and preserve explicit scope and valid answers. Initial intake focuses on core functions and actual usage. Record actual user replies; proceed with dependent planning after a reply or explicit delegation. First-time builds include new apps and existing apps without business pages. After intake, reuse explicit mode choices and detailed supplied plans; if the user delegates the mode or asks for no more questions, use Fast unless Plan is explicit; otherwise, if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. intake.confirmed records scope and key-usage clarification; the plan revision tracks plan approval separately. Fast and Plan share one three-direction visual generation rule. Fast compares exactly three directions internally and keeps the selected result. When Plan is explicit and the user has not supplied a complete visual direction, generate exactly three directions and use ask_human for the style choice before design-plan init; do not wait for the user to request a comparison. Planning preparation derives page scenes, selected theme mapping and navigation structure before init; mark AI suggestions and apply navigation_policy. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false keeps planning within the specified resources. When theme mapping is missing, query design-plan catalog once and reuse its themes and pagePatterns. Init returns authoring.pendingFields and context with typed examples; complete pending facts and review coverage before setting ready=true. Reuse one complete agent-capabilities summary per unchanged environment. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Present the current plan without revision numbers in user-visible titles, summaries, attachments or questions. Keep revision in internal confirmation fields, validate it before building, and execute its generated artifacts.',
|
|
1894
1898
|
product_design_policy: 'yida-requirement-analysis owns shared facts and first-time intake. yida-prd owns business planning; yida-design owns visual design. Reuse the same confirmed brief and supplied details. Prepare business and base visuals concurrently, then bind visuals to settled page tasks. yida-app merges and checks the artifacts before creating resources. Plan authors scope, data and rules, page organization, business interactions and acceptance once. Ordered blocks {name,purpose} supply default priority and content layers; explicit designs remain authoritative. Project-specific acceptance criteria are merged with generated resource checks. Plan uses the compact authoring contract and selected theme context; the CLI reads full templates and renders all artifacts. Fast, Plan and single-page design share yida-design/templates/design-themes and the same three-direction generation rule. Theme templates declare application-global variables from basic-tokens.json with fixed typography, spacing and Tooltip colors, plus scoped custom-page variables; component recipes consume those variables. Each selected template is the authority for navTheme, its six navigation color tokens, and its selected-item shadow token; free-creative authors its own navigation tone and tokens. Explicit project customization stays in visualStyle.tokens. The CLI declares each fixed application palette once in a shared :root and platform-mode selector block. The CLI preserves mode-independent navigation appearance tokens. Light, dark, white and gray mode classes consume the same project tokens; navTheme remains platform metadata, not a second palette. Legacy generated mode blocks are consolidated on update, while unrelated custom CSS is preserved.',
|
|
1895
1899
|
ui_guidance_policy: 'Page implementation consumes yida-prd prd.md for positioning, information architecture, page prototype, native form entry policy, material strategy, business-specific checks, resource creation order, page implementation delivery order, navigation order, and acceptance criteria; it consumes yida-design design.md for app custom theme CSS delivery, themeColor/navTheme, visual states, visualScaffold, surface material, rounded rules, density rules, components, state styling, and page-level imageNeed. When a page is imageNeed=required, or beneficial with declared image slots, yida-image-assets prepares a traceable asset manifest alongside page layout and interaction work; wait for final images only at that page\'s image binding and acceptance. imageNeed=none skips the branch. prd.md and design.md remain the only design sources of truth. Page implementation may extract page-spec.json as a derived implementation handoff from prd.md + design.md; conflicts are resolved by sending business conflicts to yida-prd and visual conflicts to yida-design before regenerating the spec. Core normal forms default to 1-3 business sample records before page implementation, followed by query readback. An explicit opt-out, configuration dictionary, sensitive data, or lack of safely constructible values requires a recorded skip reason. Screenshots, public sharing, data-source deep binding, and fine navigation grouping are optional after explicit user request or PRD acceptance criteria.',
|
package/lib/core/locales/en.js
CHANGED
|
@@ -1175,15 +1175,19 @@ Examples:
|
|
|
1175
1175
|
|
|
1176
1176
|
// ── lib/update-app.js ─────────────────────────────
|
|
1177
1177
|
update_app: {
|
|
1178
|
+
legacy_style_confirm_required: 'Application CSS is discouraged for legacy or unverified themes. Review CSS scope and ask the human to confirm injection into this exact application (navigation, forms and visit pages may be affected). Only then pass --confirm-legacy-app-style. Otherwise use page-scoped styles.',
|
|
1179
|
+
disable_theme_conflict: '--disable-custom-theme only disables CSS and cannot be combined with other application updates.',
|
|
1180
|
+
custom_theme_config_invalid: 'customThemeStyle is missing, malformed, or lacks a nonempty cssUrl. No disable request was submitted; inspect the latest app configuration.',
|
|
1181
|
+
custom_theme_disabled: 'Custom theme CSS is disabled and resource settings are retained; runtime page verification remains pending.',
|
|
1178
1182
|
theme_preset_conflict: 'A preset colour cannot be combined with custom CSS or themeColor. Use --colour custom or omit --colour.',
|
|
1179
1183
|
custom_theme_color_required: 'colour=custom requires a theme file or a valid themeColor. Pass --theme-file or --theme-color.',
|
|
1180
|
-
theme_not_persisted: 'App theme readback failed or did not match the saved settings.
|
|
1184
|
+
theme_not_persisted: 'App theme readback failed or did not match the saved settings. CSS binding or removal is unverified. Inspect expected/actual values and readback errors in themeVerification, then check current app settings before repeating an upload or recreating the app.',
|
|
1181
1185
|
navigation_not_persisted: 'App navigation readback failed or did not match the requested settings. Navigation changes remain unverified. Inspect the expected values, actual values and readback error in navigationVerification, then check the current app settings.',
|
|
1182
|
-
usage: 'openyida update-app <appType> [--name "New Name"] [--theme-file ./theme.css] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape]',
|
|
1186
|
+
usage: 'openyida update-app <appType> [--name "New Name"] [--theme-file ./theme.css] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape]',
|
|
1183
1187
|
example: 'openyida update-app APP_XXX --theme-file ./app-theme.css --nav-theme light --logo-source appIcon --layout side',
|
|
1184
|
-
options: 'Options:\n --name, -n App name (specify at least one update field)\n --desc, -d App description\n --icon Icon name (e.g. xian-yingyong)\n --icon-color Icon color (default: #0089FF)\n --colour, --theme podBlue | podGreen | podOrange | deepBlue | teal | black | custom ...\n --theme-color Update the app primary color; the CSS primary color takes precedence when a theme file is supplied\n --theme-file Custom app theme CSS file (up to 20 MB; must declare --color-brand1-6)\n --nav-theme Navigation theme: light, dark, white, or gray\n --logo-source Navigation logo source: appIcon or customImage\n --layout Modern navigation layout: side, top, or l_shape\n --hide-app-nav Hide app navigation in visit mode, only for custom pages with self-rendered top/side navigation\n --show-app-nav Restore app navigation in visit mode',
|
|
1188
|
+
options: 'Options:\n --name, -n App name (specify at least one update field)\n --desc, -d App description\n --icon Icon name (e.g. xian-yingyong)\n --icon-color Icon color (default: #0089FF)\n --colour, --theme podBlue | podGreen | podOrange | deepBlue | teal | black | custom ...\n --theme-color Update the app primary color; the CSS primary color takes precedence when a theme file is supplied\n --theme-file Custom app theme CSS file (up to 20 MB; must declare --color-brand1-6)\n --nav-theme Navigation theme: light, dark, white, or gray\n --logo-source Navigation logo source: appIcon or customImage\n --layout Modern navigation layout: side, top, or l_shape\n --hide-app-nav Hide app navigation in visit mode, only for custom pages with self-rendered top/side navigation\n --show-app-nav Restore app navigation in visit mode\n --confirm-legacy-app-style Allow legacy app CSS only after human approval\n --disable-custom-theme Disable application CSS, retain resource URL and primary color',
|
|
1185
1189
|
missing_app_type: 'Error: missing appType argument',
|
|
1186
|
-
missing_update_field: 'Error: at least one update field must be specified (--name, --desc, --icon, --theme, --theme-color, --theme-file, --nav-theme, --logo-source, --layout, --hide-app-nav, --show-app-nav)',
|
|
1190
|
+
missing_update_field: 'Error: at least one update field must be specified (--name, --desc, --icon, --theme, --theme-color, --theme-file, --nav-theme, --logo-source, --layout, --hide-app-nav, --show-app-nav, --disable-custom-theme)',
|
|
1187
1191
|
title: ' update-app - Yida App Info Update Tool',
|
|
1188
1192
|
app_type: '\n App ID: {0}',
|
|
1189
1193
|
new_name: ' New name: {0}',
|
|
@@ -2169,7 +2173,7 @@ module.exports.upgrade_app_theme = {
|
|
|
2169
2173
|
prepare_blocked: 'Some schemas or sources could not be retrieved or require manual migration. Inspect upgrade-plan.json; remote resources were not modified.',
|
|
2170
2174
|
|
|
2171
2175
|
context_failed: 'Cannot verify the server context for this app and organization. Upgrade stopped.',
|
|
2172
|
-
ai_only: 'This
|
|
2176
|
+
ai_only: 'This operation is unavailable for this app.',
|
|
2173
2177
|
usage: 'openyida upgrade-app-theme <appType> --explicit-request [--confirm | --prepare --output-dir <dir>] [--json]',
|
|
2174
2178
|
confirm_required: 'Requires an explicit upgrade request and ask_human approval of the upgrade scope. Both --explicit-request and --confirm are required.',
|
|
2175
2179
|
read_failed: 'Cannot read theme upgrade configuration. Complete upgrade status is unverified.',
|