openyida 2026.10.8 → 2026.10.9

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 CHANGED
@@ -404,6 +404,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
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
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 |
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) |
407
408
  | `openyida app-online <appType> [--to-ding-app-center] [--show-app-center]` | Enable a Yida app |
408
409
  | `openyida app-offline <appType> [--to-ding-app-center] [--show-app-center]` | Disable a Yida app |
409
410
  | `openyida nav-group <list\|create\|rename\|delete\|move\|order\|auto-order\|hide\|show> <appType> ...` | Manage app sidebar navigation groups |
package/bin/yida.js CHANGED
@@ -914,6 +914,11 @@ async function main() {
914
914
  break;
915
915
  }
916
916
 
917
+ case 'upgrade-app-theme': {
918
+ await require('../lib/app/upgrade-app-theme').run(args);
919
+ break;
920
+ }
921
+
917
922
  case 'app-online':
918
923
  case 'app-offline': {
919
924
  const { run: runAppLifecycle } = require('../lib/app/app-lifecycle');
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { fetchFormPageList } = require('./form-navigation');
7
+ const { fetchSchemaRecord, resolveCodeBundleSchema } = require('./get-schema');
8
+ const { throwCommandError } = require('../core/command-errors');
9
+ const { t } = require('../core/i18n');
10
+
11
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
12
+ const pointer = value => String(value).replace(/~/g, '~0').replace(/\//g, '~1');
13
+
14
+ // Findings are review candidates, never a license to delete business code or all styles.
15
+ function inspectUpgradeSchema(schema) {
16
+ const content = schema.content || schema;
17
+ const sources = [];
18
+ const styleCandidates = [];
19
+ const blockers = [];
20
+ let jsxCount = 0;
21
+ let canvasCount = 0;
22
+ function walk(value, at) {
23
+ if (typeof value === 'string') {
24
+ if (/\/(?:style|css|customStyle|customCss)(?:\/|$)/i.test(at) || /createElement\s*\(\s*['"]style['"]|insertRule\s*\(|(?:parent|top)\.document|contentDocument|formDetail|inject\w*(?:Theme|Style)|<style\b|--(?:pod-|color-brand)/i.test(value)) {
25
+ styleCandidates.push({ pointer: at, sha256: hash(value), reviewRequired: true });
26
+ }
27
+ return;
28
+ }
29
+ if (!value || typeof value !== 'object') { return; }
30
+ const isInstance = Boolean(value.id || value.props || value.children);
31
+ if (isInstance && value.componentName === 'Jsx') { jsxCount++; }
32
+ if (isInstance && value.componentName === 'YidaCodeCanvas') {
33
+ canvasCount++;
34
+ if (typeof value.props?.code === 'string' && value.props.code.trim()) {
35
+ sources.push({ kind: 'canvas', pointer: `${at}/props/code`, code: value.props.code });
36
+ } else { blockers.push({ code: 'CANVAS_SOURCE_MISSING', pointer: at }); }
37
+ }
38
+ for (const [key, child] of Object.entries(value)) { walk(child, `${at}/${pointer(key)}`); }
39
+ }
40
+ walk(content, '');
41
+ const moduleSource = content.actions?.module?.source;
42
+ if (typeof moduleSource === 'string' && moduleSource.trim()) {
43
+ sources.push({ kind: jsxCount ? 'legacy-jsx' : 'actions', pointer: '/actions/module/source', code: moduleSource });
44
+ } else if (jsxCount) { blockers.push({ code: 'LEGACY_JSX_SOURCE_MISSING', pointer: '/actions/module/source' }); }
45
+ // A multi-component page cannot safely be replaced by publish's single Canvas template.
46
+ if (canvasCount > 1 || jsxCount > 1 || (jsxCount && canvasCount)) {
47
+ blockers.push({ code: 'MIXED_PAGE_REQUIRES_SCHEMA_PRESERVING_MIGRATION', pointer: '/pages' });
48
+ }
49
+ return { sources, styleCandidates, blockers, jsxCount, canvasCount };
50
+ }
51
+
52
+ async function prepareAppThemeUpgrade(params, authRef) {
53
+ if (!params.outputDir) {
54
+ throwCommandError(t('upgrade_app_theme.prepare_output_required'), { code: 'APP_THEME_PREPARE_OUTPUT_REQUIRED' });
55
+ }
56
+ const outputDir = path.resolve(params.outputDir);
57
+ if (fs.existsSync(outputDir)) {
58
+ throwCommandError(t('upgrade_app_theme.prepare_exists'), { code: 'APP_THEME_PREPARE_OUTPUT_EXISTS' });
59
+ }
60
+ const forms = await fetchFormPageList(params.appType, authRef);
61
+ // Never overwrite a previous backup or trust resource names as filesystem paths.
62
+ fs.mkdirSync(path.dirname(outputDir), { recursive: true });
63
+ fs.mkdirSync(outputDir);
64
+ const records = [];
65
+ for (const [index, form] of forms.entries()) {
66
+ const dir = path.join(outputDir, `resource-${index + 1}`);
67
+ fs.mkdirSync(dir);
68
+ const resource = { ...form, sources: [], styleCandidates: [], blockers: [], status: 'pending-review' };
69
+ records.push(resource);
70
+ try {
71
+ const record = await fetchSchemaRecord(params.appType, form, authRef, 0, { resolveCodeBundles: false });
72
+ if (!record.success) { throw new Error(record.errorMsg); }
73
+ const raw = JSON.stringify(record.schema, null, 2);
74
+ fs.writeFileSync(path.join(dir, 'schema.original.json'), raw, { flag: 'wx' });
75
+ resource.schemaFile = path.join(dir, 'schema.original.json');
76
+ resource.schemaSha256 = hash(raw);
77
+ const resolved = await resolveCodeBundleSchema(JSON.parse(raw), params.appType, form.formUuid, authRef);
78
+ fs.writeFileSync(path.join(dir, 'schema.resolved.json'), JSON.stringify(resolved, null, 2), { flag: 'wx' });
79
+ const inspected = inspectUpgradeSchema(resolved);
80
+ Object.assign(resource, { styleCandidates: inspected.styleCandidates, blockers: inspected.blockers,
81
+ kind: inspected.jsxCount ? 'legacy-jsx' : inspected.canvasCount ? 'canvas' : 'native-form-or-page' });
82
+ for (const [sourceIndex, source] of inspected.sources.entries()) {
83
+ const ext = source.kind === 'canvas' ? 'canvas.jsx' : source.kind === 'legacy-jsx' ? 'oyd.jsx' : 'js';
84
+ const file = path.join(dir, `source-${sourceIndex + 1}.${ext}`);
85
+ fs.writeFileSync(file, source.code, { flag: 'wx' });
86
+ resource.sources.push({ kind: source.kind, pointer: source.pointer, file, sha256: hash(source.code) });
87
+ }
88
+ } catch (error) {
89
+ resource.blockers.push({ code: error.code || 'SCHEMA_OR_SOURCE_FETCH_FAILED', message: error.message });
90
+ }
91
+ if (resource.blockers.length) { resource.status = 'blocked'; }
92
+ }
93
+ const manifestFile = path.join(outputDir, 'upgrade-plan.json');
94
+ const blockedCount = records.filter(record => record.blockers.length).length;
95
+ const output = { success: blockedCount === 0, stage: blockedCount ? 'blocked' : 'prepared',
96
+ appType: params.appType, corpId: authRef.corpId, baseUrl: authRef.baseUrl,
97
+ createdAt: new Date().toISOString(), manifestFile, remoteModified: false, appUpgradeComplete: false,
98
+ inventorySource: 'form-navigation', inventoryCoverageVerified: false,
99
+ total: records.length, blockedCount, resources: records };
100
+ fs.writeFileSync(manifestFile, JSON.stringify(output, null, 2), { flag: 'wx' });
101
+ return output;
102
+ }
103
+
104
+ module.exports = { inspectUpgradeSchema, prepareAppThemeUpgrade };
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ const querystring = require('querystring');
4
+ const { httpGet, httpPost, requestWithAutoLogin } = require('../core/utils');
5
+ const { createAuthRef } = require('../core/yida-client');
6
+ const { throwCommandError, throwUsage } = require('../core/command-errors');
7
+ const { t } = require('../core/i18n');
8
+
9
+ function parseArgs(args = []) {
10
+ const params = { appType: null, explicitRequest: false, confirm: false, help: false };
11
+ for (let i = 0; i < args.length; i++) {
12
+ const arg = args[i];
13
+ if (arg === '--help' || arg === '-h') { params.help = true; }
14
+ else if (arg === '--prepare') { params.prepare = true; }
15
+ else if (arg === '--output-dir' && args[i + 1] && !args[i + 1].startsWith('-')) { params.outputDir = args[++i]; }
16
+ else if (arg === '--json') { params.json = true; }
17
+ else if (arg === '--explicit-request') { params.explicitRequest = true; }
18
+ else if (arg === '--confirm') { params.confirm = true; }
19
+ else if (!arg.startsWith('-') && !params.appType) { params.appType = arg; }
20
+ else { throwUsage(t('cli_argument.invalid', arg)); }
21
+ }
22
+ if (params.prepare && params.confirm) { throwUsage(t('upgrade_app_theme.prepare_output_required')); }
23
+ return params;
24
+ }
25
+
26
+ // Read only literal fields from the server-rendered admin configuration. Never execute HTML/JS.
27
+ function parseThemeContext(html) {
28
+ const fail = () => throwCommandError(t('upgrade_app_theme.context_failed'), { code: 'APP_THEME_CONTEXT_UNVERIFIED' });
29
+ if (typeof html !== 'string') { return fail(); }
30
+ const contexts = [];
31
+ for (const match of html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi)) {
32
+ if (!match[1].includes('window.pageConfig')) { continue; }
33
+ let ast;
34
+ try { ast = require('@babel/standalone').transform(match[1], { ast: true, code: false }).ast; }
35
+ catch (_) { return fail(); }
36
+ for (const statement of ast.program.body) {
37
+ const expr = statement.expression;
38
+ if (expr?.type !== 'AssignmentExpression' || expr.operator !== '=' ||
39
+ expr.left.type !== 'MemberExpression' || expr.left.computed ||
40
+ expr.left.object.name !== 'window' || expr.left.property.name !== 'pageConfig') { continue; }
41
+ if (expr.right.type !== 'ObjectExpression') { return fail(); }
42
+ const context = {};
43
+ const wanted = ['appType', 'corpId', 'agentAppType'];
44
+ for (const prop of expr.right.properties) {
45
+ if (prop.type === 'SpreadElement' || prop.computed) { return fail(); }
46
+ const key = prop.key.name || prop.key.value;
47
+ if (!wanted.includes(key)) { continue; }
48
+ if (Object.hasOwn(context, key) || prop.value?.type !== 'StringLiteral') { return fail(); }
49
+ context[key] = prop.value.value;
50
+ }
51
+ if (wanted.some(key => !Object.hasOwn(context, key))) { return fail(); }
52
+ contexts.push(context);
53
+ }
54
+ }
55
+ if (contexts.length !== 1) { return fail(); }
56
+ return contexts[0];
57
+ }
58
+
59
+ async function readThemeEligibility(appType, auth) {
60
+ const html = await requestWithAutoLogin(current => httpGet(current.baseUrl,
61
+ `/${encodeURIComponent(appType)}/admin`, { _stamp: Date.now() }, { responseType: 'text' }), auth);
62
+ const context = parseThemeContext(html);
63
+ if (context.appType !== appType || !auth.corpId || context.corpId !== auth.corpId) {
64
+ throwCommandError(t('upgrade_app_theme.context_failed'), { code: 'APP_THEME_CONTEXT_UNVERIFIED' });
65
+ }
66
+ // agentAppType is derived by Tianshu from FROM_BUILDER_AI and BUILDER_AI_SOURCE.
67
+ // aiApp/FROM_AI is a separate feature flag and must not be used as provenance.
68
+ if (!['local', 'cloud', 'qwenwork'].includes(context.agentAppType.trim().toLowerCase())) {
69
+ throwCommandError(t('upgrade_app_theme.ai_only'), { code: 'APP_THEME_AI_APP_REQUIRED' });
70
+ }
71
+ return context;
72
+ }
73
+
74
+ // Both attestations are required even for callers that bypass the CLI runner.
75
+ // They record the agent's authorization checks; they cannot prove a human reply.
76
+ async function upgradeAppTheme(params, authRef) {
77
+ if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType)) {
78
+ throwUsage(t('upgrade_app_theme.usage'));
79
+ }
80
+ if (params.explicitRequest !== true || params.confirm !== true) {
81
+ throwCommandError(t('upgrade_app_theme.confirm_required'), { code: 'APP_THEME_CONFIRMATION_REQUIRED' });
82
+ }
83
+ const auth = authRef || createAuthRef();
84
+ await readThemeEligibility(params.appType, auth);
85
+ const targets = { CREATED_WITH_MODERN_THEME: 'y', APP_THEME_MODE: 'modern' };
86
+ const request = (endpoint, fields) => requestWithAutoLogin((current) => httpPost(
87
+ current.baseUrl,
88
+ `/${encodeURIComponent(params.appType)}/query/app/${endpoint}.json`,
89
+ querystring.stringify({ appType: params.appType, ...fields, _csrf_token: current.csrfToken })
90
+ ), auth);
91
+ const readConfig = async (key) => {
92
+ const response = await request('getSingleConfig', { key });
93
+ const emptyContent = response?.content && typeof response.content === 'object' &&
94
+ !Array.isArray(response.content) && Object.keys(response.content).length === 0;
95
+ if (response?.success !== true || (response.content !== null && typeof response.content !== 'string' && !emptyContent)) {
96
+ throwCommandError(t('upgrade_app_theme.read_failed'), { code: 'APP_THEME_READ_FAILED', details: { key } });
97
+ }
98
+ return emptyContent ? null : response.content;
99
+ };
100
+ // Read both before writing anything. The two endpoints are not transactional.
101
+ const previous = {};
102
+ for (const key of Object.keys(targets)) { previous[key] = await readConfig(key); }
103
+ const verifiedWrites = [];
104
+ let attemptedKey;
105
+ try {
106
+ for (const [key, value] of Object.entries(targets)) {
107
+ if (previous[key] === value) { continue; }
108
+ attemptedKey = key;
109
+ const response = await request('updateSingleConfig', { key, value });
110
+ if (response?.success !== true || response.content !== true) {
111
+ throwCommandError(t('upgrade_app_theme.write_failed'), { code: 'APP_THEME_UPGRADE_FAILED' });
112
+ }
113
+ if (await readConfig(key) !== value) {
114
+ throwCommandError(t('upgrade_app_theme.verify_failed'), { code: 'APP_THEME_NOT_PERSISTED' });
115
+ }
116
+ verifiedWrites.push(key);
117
+ }
118
+ // Verify the pair again after writes; never claim both succeeded from only one response.
119
+ if (attemptedKey) {
120
+ for (const [key, value] of Object.entries(targets)) {
121
+ if (await readConfig(key) !== value) {
122
+ throwCommandError(t('upgrade_app_theme.verify_failed'), { code: 'APP_THEME_NOT_PERSISTED' });
123
+ }
124
+ }
125
+ }
126
+ } catch (error) {
127
+ throwCommandError(attemptedKey ? t('upgrade_app_theme.incomplete') : error.message, {
128
+ code: error.code || 'APP_THEME_UPGRADE_FAILED',
129
+ details: { appType: params.appType, previous, attemptedKey, verifiedWrites,
130
+ configVerified: false, writeOutcomeMayBePartial: Boolean(attemptedKey) },
131
+ });
132
+ }
133
+ return { success: true, appType: params.appType, changed: verifiedWrites.length > 0,
134
+ verified: true, verificationScope: 'config', runtimeVerified: false, appUpgradeComplete: false,
135
+ nextStep: 'migrate-pages-and-clean-form-styles-with-yida-upgrade-app-theme', previous,
136
+ createdWithModernTheme: 'y', appThemeMode: 'modern', updatedKeys: verifiedWrites };
137
+
138
+ }
139
+
140
+ async function run(args = []) {
141
+ const params = parseArgs(args);
142
+ if (params.help) {
143
+ const { usage } = require('../core/chalk');
144
+ usage(t('upgrade_app_theme.usage'), t('upgrade_app_theme.confirm_required'));
145
+ return;
146
+ }
147
+ if (params.prepare) {
148
+ if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType) || !params.explicitRequest || !params.outputDir) {
149
+ throwUsage(t('upgrade_app_theme.prepare_output_required'));
150
+ }
151
+ const auth = createAuthRef();
152
+ await readThemeEligibility(params.appType, auth);
153
+ const { prepareAppThemeUpgrade } = require('./prepare-app-theme-upgrade');
154
+ const prepared = await prepareAppThemeUpgrade(params, auth);
155
+ if (!prepared.success) {
156
+ throwCommandError(t('upgrade_app_theme.prepare_blocked'), {
157
+ code: 'APP_THEME_PREPARE_BLOCKED', details: { manifestFile: prepared.manifestFile, blockedCount: prepared.blockedCount },
158
+ });
159
+ }
160
+ console.log(JSON.stringify(prepared));
161
+ return prepared;
162
+ }
163
+ if (params.outputDir) { throwUsage(t('upgrade_app_theme.prepare_output_required')); }
164
+ const output = await upgradeAppTheme(params);
165
+ console.log(JSON.stringify(output));
166
+ return output;
167
+ }
168
+
169
+ module.exports = { parseArgs, parseThemeContext, readThemeEligibility, upgradeAppTheme, run };
@@ -71,7 +71,7 @@ const PERMISSION_SCHEMA = Object.freeze({
71
71
  },
72
72
  modes: {
73
73
  allow: 'Allowed by the current OpenYida agent policy without an extra OpenYida-specific confirmation.',
74
- ask: 'Requires user confirmation before agent execution; currently reserved for destructive/delete-like operations.',
74
+ ask: 'Requires user confirmation before agent execution; required for destructive operations and explicit-confirmation workflows such as legacy app theme upgrades.',
75
75
  deny: 'Should not be auto-executed by an agent.',
76
76
  },
77
77
  effects: {
@@ -367,6 +367,8 @@ function findCommandSuggestion(argv = []) {
367
367
  }
368
368
 
369
369
  const COMMAND_SIDE_EFFECTS = new Map([
370
+ ['upgrade-app-theme', sideEffect('mixed', { mutates_yida: true, mutates_local: true,
371
+ read_actions: ['--prepare'], mutating_actions: ['--confirm'] })],
370
372
  ['create-form.batch', sideEffect('remote_write', { mutates_local: true })],
371
373
  ...sideEffectEntries([
372
374
  'agent-capabilities',
@@ -748,6 +750,12 @@ const COMMAND_PERMISSIONS = new Map([
748
750
  reason: ALLOW_EXTERNAL_REASON,
749
751
  })),
750
752
 
753
+ ...permissionEntries(['upgrade-app-theme'], permission('ask', 'write', {
754
+ action_dependent: true, read_actions: ['--prepare'], ask_actions: ['--confirm'],
755
+ preauthorized_actions: [], preauthorized_patterns: [], ask_patterns: [], unknown_action_mode: 'ask',
756
+ reason: 'Only on an explicit user request to upgrade this existing app to the modern theme; require ask_human confirmation for the target app before execution. General beautification, app creation and blanket authorization do not qualify.',
757
+ })),
758
+
751
759
  ...permissionEntries([
752
760
  'app-offline',
753
761
  'connector.delete',
@@ -1155,6 +1163,18 @@ const COMMAND_GROUPS = [
1155
1163
  'Platform preset keys are used independently of --theme-file and --theme-color. Choose --hide-app-nav or --show-app-nav to set navigation visibility.',
1156
1164
  ],
1157
1165
  }),
1166
+ command('upgrade-app-theme', ['upgrade-app-theme'], 'upgrade-app-theme <appType> --explicit-request [--confirm | --prepare --output-dir <dir>] [--json]', 'help.cmd_upgrade_app_theme', {
1167
+ output: 'json',
1168
+ args: [
1169
+ { name: 'appType', type: 'string', required: true, source: 'positional', position: 0 },
1170
+ { name: 'explicitRequest', type: 'boolean', required: true, source: 'option', builder_options: ['--explicit-request'] },
1171
+ { name: 'prepare', type: 'boolean', source: 'option', builder_options: ['--prepare'] },
1172
+ { name: 'outputDir', type: 'string', source: 'option', builder_options: ['--output-dir'] },
1173
+ { name: 'json', type: 'boolean', source: 'option', builder_options: ['--json'] },
1174
+ { name: 'confirm', type: 'boolean', source: 'option', builder_options: ['--confirm'] },
1175
+ ],
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. Only agentAppType=local/cloud/qwenwork qualifies; verify target app/corp identity. Do not check V4, appThemeEnable, organization theme gray or upgrade gray. After ask_human approval, persist CREATED_WITH_MODERN_THEME=y and APP_THEME_MODE=modern, then read both back. 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
+ }),
1158
1178
  command('app-online', ['app-online'], 'app-online <appType> [--to-ding-app-center] [--show-app-center]', 'help.cmd_app_online'),
1159
1179
  command('app-offline', ['app-offline'], 'app-offline <appType> [--to-ding-app-center] [--show-app-center]', 'help.cmd_app_offline'),
1160
1180
  command('nav-group', ['nav-group'], 'nav-group <list|create|rename|delete|move|order|auto-order|hide|show> <appType> ...', 'help.cmd_nav_group', {
@@ -2162,3 +2162,19 @@ module.exports.sample_options = {
2162
2162
  'theme_help': 'app-theme: --output is a CSS file; --design-file reads a completed design. Omitting it resets CSS. Set theme values in the design.',
2163
2163
  'style_help': 'application-style: --style-id is required and accepts every complete application theme, never a separate navigation style. design.md owns visual tokens for navigation, shell, custom pages, forms and details; --output is a directory for three files. Existing bundle files cause an error.'
2164
2164
  };
2165
+
2166
+ module.exports.upgrade_app_theme = {
2167
+ prepare_output_required: 'Prepare source backups: upgrade-app-theme <appType> --explicit-request --prepare --output-dir <new-directory> [--json].',
2168
+ prepare_exists: 'Backup directory already exists. Choose a new directory to preserve original sources.',
2169
+ prepare_blocked: 'Some schemas or sources could not be retrieved or require manual migration. Inspect upgrade-plan.json; remote resources were not modified.',
2170
+
2171
+ context_failed: 'Cannot verify the server context for this app and organization. Upgrade stopped.',
2172
+ ai_only: 'This app does not currently support upgrading to the new theme.',
2173
+ usage: 'openyida upgrade-app-theme <appType> --explicit-request [--confirm | --prepare --output-dir <dir>] [--json]',
2174
+ confirm_required: 'Requires an explicit upgrade request and ask_human approval of the upgrade scope. Both --explicit-request and --confirm are required.',
2175
+ read_failed: 'Cannot read theme upgrade configuration. Complete upgrade status is unverified.',
2176
+ write_failed: 'Theme upgrade configuration write failed.',
2177
+ verify_failed: 'Could not verify the theme upgrade. Check the current app state.',
2178
+ incomplete: 'The theme upgrade is incomplete. Some changes may have taken effect; further operations have stopped.',
2179
+ };
2180
+ module.exports.help.cmd_upgrade_app_theme = 'Upgrade an AI-built legacy app theme (explicit request and ask_human confirmation required)';
@@ -2116,3 +2116,19 @@ module.exports.sample_options = {
2116
2116
  'theme_help': 'app-theme:--output 指向 CSS 文件;--design-file 读取已完成的设计。省略设计文件会重置 CSS,主题值通过设计填写。',
2117
2117
  'style_help': 'application-style:--style-id 必填,支持目录中全部整套应用主题,不单选导航风格;design.md 统一定义五类界面的视觉 token,--output 指向目录,输出三文件,有同名文件时报错。'
2118
2118
  };
2119
+
2120
+ module.exports.upgrade_app_theme = {
2121
+ prepare_output_required: '准备源码备份:upgrade-app-theme <appType> --explicit-request --prepare --output-dir <新目录> [--json]。',
2122
+ prepare_exists: '备份目录已存在,请选择新目录,避免覆盖原始源码。',
2123
+ prepare_blocked: '部分 Schema 或源码无法完整获取或迁移,请检查备份目录中的 upgrade-plan.json;未修改远端资源。',
2124
+
2125
+ context_failed: '无法验证目标应用及组织的服务端上下文,已停止升级。',
2126
+ ai_only: '此应用暂不支持升级新版主题。',
2127
+ usage: 'openyida upgrade-app-theme <appType> --explicit-request [--confirm | --prepare --output-dir <dir>] [--json]',
2128
+ confirm_required: '仅在用户明确要求升级新版主题,并通过 ask_human 确认升级范围后执行;必须同时传入 --explicit-request 和 --confirm。',
2129
+ read_failed: '读取主题升级配置失败,未确认完整升级状态。',
2130
+ write_failed: '主题升级配置写入失败。',
2131
+ verify_failed: '未能确认主题升级已生效,请检查应用当前状态。',
2132
+ incomplete: '主题升级未完成,部分操作可能已生效,后续操作已停止。',
2133
+ };
2134
+ module.exports.help.cmd_upgrade_app_theme = 'AI 老应用升级新版主题(明确要求并经 ask_human 确认)';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.10.8",
3
+ "version": "2026.10.9",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -310,7 +310,7 @@ openyida copy
310
310
  | 大类目录 | 第一层意图信号 | 子技能 |
311
311
  | --- | --- | --- |
312
312
  | \`yida-skills/context\` | 登录、退出、组织信息、Schema、fieldId、只读预检 | \`yida-login\`, \`yida-logout\`, \`yida-basic-info\`, \`yida-get-schema\`, \`yida-corp-efficiency\` |
313
- | \`yida-skills/app\` | 从零搭应用、导航、多语言 | \`yida-app\`, \`yida-create-app\`, \`yida-nav-group\`, \`yida-i18n\` |
313
+ | \`yida-skills/app\` | 从零搭应用、导航、多语言、AI 老应用主题升级 | \`yida-app\`, \`yida-create-app\`, \`yida-nav-group\`, \`yida-i18n\`, \`yida-upgrade-app-theme\`(仅明确要求升级,必须 ask_human 确认) |
314
314
  | \`yida-skills/design\` | 完整应用需求分析、产品 PRD、单页 UI 改造、主页面视觉设计、应用主题色、全局换肤、图片素材 | \`yida-requirement-analysis\`, \`yida-prd\`, \`yida-design\`, \`yida-image-assets\` |
315
315
  | \`yida-skills/form\` | 表单字段、公式、校验、业务规则、批量录入、数据记录 | \`yida-create-form-page\`, \`yida-formula\`, \`yida-formula-evaluate\`, \`yida-business-rule\`, \`yida-canvas-table-form\`, \`yida-table-form\`, \`yida-data-management\` |
316
316
  | \`yida-skills/process\` | 审批、流程表单、流程规则、代理人 | \`yida-create-process\`, \`yida-process-rule\`, \`yida-agent-center\` |
@@ -102,7 +102,7 @@ description: >
102
102
  | 大类目录 | 第一层意图信号 | 子技能 |
103
103
  | --- | --- | --- |
104
104
  | `yida-skills/context` | 登录、退出、切换组织、组织版本/容量、Schema、fieldId、执行前检查 | `yida-login`、`yida-logout`、`yida-basic-info`、`yida-get-schema`、`yida-corp-efficiency` |
105
- | `yida-skills/app` | 从零搭应用、完整系统、应用启停、应用导航、多语言 | `yida-app`、`yida-create-app`、`yida-app-lifecycle`、`yida-nav-group`、`yida-i18n` |
105
+ | `yida-skills/app` | 从零搭应用、完整系统、应用启停、应用导航、多语言、AI 老应用主题升级 | `yida-app`、`yida-create-app`、`yida-app-lifecycle`、`yida-nav-group`、`yida-i18n`、`yida-upgrade-app-theme`(仅明确升级要求并 ask_human 确认) |
106
106
  | `yida-skills/design` | 完整应用需求分析、PRD、视觉设计、单页 UI 改造、应用主题色、全局换肤、图片素材 | `yida-requirement-analysis`、`yida-prd`、`yida-design`、`yida-image-assets` |
107
107
  | `yida-skills/form` | 表单字段、公式、校验、业务关联规则、批量录入、数据记录 | `yida-create-form-page`、`yida-formula`、`yida-formula-evaluate`、`yida-business-rule`、`yida-canvas-table-form`、`yida-table-form`、`yida-data-management` |
108
108
  | `yida-skills/process` | 审批、流程表单、流程规则、节点/分支/字段权限、流程代理 | `yida-create-process`、`yida-process-rule`、`yida-agent-center` |
@@ -126,6 +126,7 @@ description: >
126
126
  | 用户给 taskUuid 并要求转 PRD | 先用 `yida-tingji` 读取听记内容,再把已有内容交给 `yida-flash-note-to-prd` 生成 PRD |
127
127
  | 已有会议纪要/闪记内容转 PRD | `yida-flash-note-to-prd`,只处理已有内容,不负责按 taskUuid 拉取听记 |
128
128
  | 只创建应用壳并拿 appType | `yida-create-app`;若随后继续完整搭建,已经确认的 `requirement-brief.json`、`prd.md` 与 `design.md` 保持不变,真实 `appType` 只写入 schema 或当前任务资源上下文 |
129
+ | 明确强制要求 AI 老应用升级成新版主题 | `yida-upgrade-app-theme`;必须先取得针对目标应用的 ask_human 肯定回复,普通美化/换色/建应用不得触发 |
129
130
  | 启用/上线或停用/下线已有应用 | `yida-app-lifecycle`;只有用户明确要求时执行,`app-offline` 执行前需再次确认目标应用 |
130
131
  | 创建自定义展示页资源 | `yida-create-page`,之后交给 `yida-canvas-custom-page` 编写页面源码,再交给 `yida-publish-page` 发布 |
131
132
  | 开发表单字段结构 / 增删改字段 | 使用 `yida-create-form-page` 落地字段结构 |
@@ -12,6 +12,10 @@ description: 将基于 JSX 组件 的自定义页面升级/迁移为使用 `Yida
12
12
  - 迁移前页面:OpenYida 平台 JSX 组件页面,通常是 `project/pages/src/*.oyd.jsx`、`export function renderJsx()`、`_customState`、`this.utils.yida.*`,`openyida publish` 发布为 `Jsx` 组件;用户称 `.oyb.jsx` 时先按平台 JSX 组件页面需求识别,再确认实际源码后缀。
13
13
  - 目标实现:页面 Schema 中承载 `YidaCodeCanvas` 组件,组件属性包含 `code`、`runtimeCode`、`importedModules`,运行时执行 `YidaComp`。
14
14
 
15
+ ## 从老应用主题升级进入
16
+
17
+ 从 `yida-upgrade-app-theme` 进入时,复用已备份的 Schema、源码和已确认的目标页面清单,逐页进行等价迁移。保持原业务逻辑、主要区块结构、字段 ID、请求参数、权限和跳转行为,不借迁移重做页面或更换业务模板。平台数据能力按当前 Canvas 数据桥规范适配,无法证明等价时停止发布,在应用升级清单记录未完成项。源码注入历史表单详情主题时,按已确认范围清理样式及专属监听,保留业务事件。
18
+
15
19
  ## 迁移前必须确认
16
20
 
17
21
  | 检查项 | 命令 / 做法 |
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: yida-upgrade-app-theme
3
+ description: 将老应用升级为新版主题,同步更新页面和表单样式。仅在用户明确要求升级时使用,执行前须通过 ask_human 确认;普通换色、美化不触发。
4
+ ---
5
+
6
+ # 老应用升级新版主题
7
+
8
+ 保持业务逻辑和主要布局,升级应用主题、自定义页面和表单样式。
9
+
10
+ ## 1. 准备
11
+
12
+ 仅在用户明确要求升级新版主题时使用。普通美化、换色或创建应用不触发。
13
+
14
+ 确定目标应用,运行 `openyida agent-capabilities --summary-json`,再将页面备份到新目录:
15
+
16
+ ```bash
17
+ openyida upgrade-app-theme <appType> --explicit-request --prepare --output-dir <projectRoot>/.cache/openyida/theme-upgrade/<appType>/<run-id> --json
18
+ ```
19
+
20
+ 准备命令不会修改远端页面。读取生成的 `upgrade-plan.json`,核对隐藏页、嵌入页和用户指定页面;遗漏项通过 `openyida get-schema <appType> <formUuid>` 补齐。无法获取源码的页面记录为未完成项。
21
+
22
+ ## 2. 判断应用风格
23
+
24
+ 先看原自定义页面的实际效果和源码,从主按钮、标题、链接、背景和卡片中判断主色、明暗及整体风格。优先遵循用户指定的风格;否则延续已有页面的品牌色。不要直接采用平台默认色,也不要把成功、警告等状态色当作主色。
25
+
26
+ 页面风格不一致或主色不明确时,用 **ask_human** 提供候选颜色和简短理由,确认后再改。将选定主色和判断依据记录在升级清单中,统一用于应用导航、页面和表单。
27
+
28
+ ## 3. 确认
29
+
30
+ 任何远端修改前,必须调用 **ask_human**,说明:
31
+
32
+ - 目标应用、组织及涉及的页面和表单。
33
+ - 拟采用的主色和风格,以及它与原页面的关系。
34
+ - 将升级主题、迁移旧自定义页面、统一样式并重新发布。
35
+ - 保留业务逻辑和主要布局。
36
+
37
+ 提供“确认升级”和“取消”,收到明确确认后继续。无回复、拒绝或工具不可用时停止写入,“直接做”不能替代确认。范围变更时只确认新增部分。
38
+
39
+ ## 4. 迁移并升级
40
+
41
+ 按 [页面和表单迁移](references/page-migration.md) 完成本地修改和验证:
42
+
43
+ - JSX 页面转为 CodeCanvas。
44
+ - 已有 CodeCanvas 页面更新主题样式。
45
+ - 表单和详情页移除旧主题注入,保留业务代码。
46
+
47
+ 保留原应用和页面 ID。任一必要页面无法保留业务或结构时,停止升级并说明原因。
48
+
49
+ 本地验证通过后执行:
50
+
51
+ ```bash
52
+ openyida upgrade-app-theme <appType> --explicit-request --confirm --json
53
+ ```
54
+
55
+ 成功后保存选定的应用主题,再按清单发布页面、保存表单并回读验证。命令失败时停止后续操作。向用户说明进度和结果,不展示内部检查、配置字段或诊断数据。
56
+
57
+ ## 5. 验收
58
+
59
+ 重新加载页面,验证业务操作、表单详情、主题样式和窄屏效果,逐页记录结果。编译或升级命令成功不能代替页面验收。
60
+
61
+ 全部目标页面和表单通过后,才能报告升级完成;否则列出已完成项和剩余问题。
@@ -0,0 +1,61 @@
1
+ # 页面和表单迁移
2
+
3
+ ## 1. 备份原页面
4
+
5
+ 使用准备命令生成的 Schema、源码和 `upgrade-plan.json`。在单独目录修改源码,保留原始备份。
6
+
7
+ 核对隐藏页、嵌入页和用户指定页面,补齐遗漏。根据 Schema 区分 JSX、CodeCanvas 和表单。缺少原源码时停止该页迁移,不根据编译产物猜写业务逻辑。
8
+
9
+ ## 2. 迁移自定义页面
10
+
11
+ - **JSX 页面**:按 [yida-canvas-upgrade](../../yida-canvas-upgrade/SKILL.md) 转为 CodeCanvas,保留业务逻辑和主要布局。
12
+ - **CodeCanvas 页面**:直接更新主题样式,保留原有状态和业务代码。
13
+ - **混合组件页面**:保留原组件树、数据源和动作。不能用单 Canvas 发布命令覆盖;无法保留时停止该页迁移。
14
+
15
+ 迁移时保留字段 ID、请求参数、数据映射、权限、路由、筛选分页、提交回调和生命周期清理。平台 API 和数据源按 [数据桥规范](../../yida-canvas-custom-page/references/data-bridge-guide.md) 适配,验证可用后再发布,不用静态数据替代。
16
+
17
+ 按升级清单中选定的主色和风格准备应用主题 CSS,保留原页面的布局、圆角和密度。页面使用同一套主题 token,Provider 只作用于页面内部;成功、警告、失败等业务语义色保持不变。
18
+
19
+ ## 3. 清理表单旧样式
20
+
21
+ 检查表单 Schema、动作、CSS,以及自定义页面向表单详情、弹窗或 iframe 注入样式的代码。
22
+
23
+ - 逐项确认 `styleCandidates`,只删除旧主题注入及其专属监听、定时器和清理代码。
24
+ - 保留业务事件、页面内部样式、详情跳转、弹窗通信和提交回调。样式与业务混在同一函数时,只移除样式部分。
25
+ - 表单使用 `create-form patch` 精确修改对应路径,保留字段、校验、公式、权限和流程。修改动作源码时同步更新编译内容。
26
+
27
+ 不能按关键词批量删除代码。无法确认影响范围或保存方式时,停止该项修改。`publish --fix-theme` 只辅助替换部分固定色,使用后检查差异,不能代替注入清理。
28
+
29
+ ## 4. 校验并发布
30
+
31
+ 先完成本地修改和编译,再执行升级 Skill 中的升级命令。发布前重新拉取 Schema;若有他人改动,合并并重新验证,避免覆盖。
32
+
33
+ 升级命令成功后,保存已确认的应用主题并核对回读结果,不能只让页面跟随默认主题:
34
+
35
+ ```bash
36
+ openyida update-app <appType> --theme-file <working/app-theme.css>
37
+ ```
38
+
39
+ 只更新主题,保留应用名称、导航结构、Logo 和访问设置。
40
+
41
+ 单 Canvas 页面发布到原 `appType` 和 `formUuid`:
42
+
43
+ ```bash
44
+ openyida compile <working/page.canvas.jsx> --json
45
+ openyida publish <working/page.canvas.jsx> <appType> <原formUuid> --json
46
+ openyida get-schema <appType> <原formUuid>
47
+ ```
48
+
49
+ 回读确认页面已保存为 CodeCanvas,源码、运行代码或 codeBundle 有效,业务绑定完整。表单保存后也要回读核对。
50
+
51
+ ## 5. 验收并记录
52
+
53
+ 重新加载页面,检查:
54
+
55
+ - 查询、筛选、分页、增删改、提交、权限和详情开关正常。
56
+ - 页面主要结构、图表和窄屏显示正常。
57
+ - 应用导航、主按钮、链接、页面和表单符合选定主色及风格。
58
+ - 表单录入、校验、公式和流程未变,页面与表单主题一致。
59
+ - 旧样式没有被监听器或定时器重新注入。
60
+
61
+ 在 `upgrade-plan.json` 逐页记录备份、修改、发布、回读和验收结果。失败项写明原因,不自动回滚整个应用。全部目标资源通过后才能报告升级完成;否则列出已完成和未完成项。
@@ -3,6 +3,34 @@
3
3
  "source": "openyida",
4
4
  "entry": "openyida",
5
5
  "skills": [
6
+ {
7
+ "name": "yida-upgrade-app-theme",
8
+ "path": "skills/yida-upgrade-app-theme/SKILL.md",
9
+ "display_name": "老应用升级新版主题",
10
+ "description": "将老应用升级为新版主题,同步更新页面和表单样式。仅在用户明确要求升级时使用,执行前须通过 ask_human 确认;普通换色、美化不触发。",
11
+ "category": "yida-skills/app",
12
+ "tags": [
13
+ "升级新版主题"
14
+ ],
15
+ "positive_signals": [
16
+ "将老应用升级成新版主题",
17
+ "把应用切换到新版主题"
18
+ ],
19
+ "negative_signals": [
20
+ "美化页面",
21
+ "换颜色",
22
+ "创建应用",
23
+ "升级 CLI"
24
+ ],
25
+ "command_ids": [
26
+ "upgrade-app-theme",
27
+ "update-app",
28
+ "get-schema",
29
+ "publish",
30
+ "create-form.patch"
31
+ ],
32
+ "requires_login": true
33
+ },
6
34
  {
7
35
  "name": "yida-agent-center",
8
36
  "path": "skills/yida-agent-center/SKILL.md",
@@ -1304,8 +1332,10 @@
1304
1332
  {
1305
1333
  "name": "yida-skills/app",
1306
1334
  "display_name": "应用架构与设置",
1307
- "description": "从零搭建应用、应用级导航树、多语言等应用整体架构和配置。",
1335
+ "description": "从零搭建应用、应用级导航树、多语言和已有 AI 应用主题版本升级等应用管理与配置。",
1308
1336
  "signals": [
1337
+ "升级新版主题",
1338
+ "把应用切换到新版主题",
1309
1339
  "创建应用",
1310
1340
  "完整应用",
1311
1341
  "管理系统",