openyida 2026.9.21 → 2026.9.22
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 +2 -0
- package/bin/yida.js +6 -0
- package/lib/app/app-entry.js +118 -0
- package/lib/app/app-list.js +1 -0
- package/lib/app/canvas-theme-guard.js +78 -8
- package/lib/app/fix-theme.js +82 -0
- package/lib/app/publish.js +67 -3
- package/lib/core/command-manifest.js +10 -0
- package/lib/core/locales/en.js +19 -3
- package/lib/core/locales/zh.js +19 -3
- package/lib/design-plan/materialize.js +3 -13
- package/lib/design-plan/preview.js +4 -1
- package/lib/design-plan/rebase.js +13 -4
- package/lib/design-plan/validate.js +2 -2
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +20 -0
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +1 -0
- package/yida-skills/skills/yida-canvas-custom-page/references/waf-safe-authoring.md +45 -0
- package/yida-skills/skills/yida-publish-page/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -393,6 +393,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
393
393
|
|
|
394
394
|
| Command | Description |
|
|
395
395
|
|---------|-------------|
|
|
396
|
+
| `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
|
|
397
|
+
| `openyida app-entry set <appType> [--frontend <url>] [--management <url>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
|
|
396
398
|
| `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
|
|
397
399
|
| `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
|
|
398
400
|
| `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
|
package/bin/yida.js
CHANGED
|
@@ -733,6 +733,12 @@ async function main() {
|
|
|
733
733
|
break;
|
|
734
734
|
}
|
|
735
735
|
|
|
736
|
+
case 'app-entry': {
|
|
737
|
+
const { run } = require('../lib/app/app-entry');
|
|
738
|
+
await run(args);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
|
|
736
742
|
case 'app-list': {
|
|
737
743
|
const { run } = require('../lib/app/app-list');
|
|
738
744
|
await run(args);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createAuthRef, createYidaClient, unwrapYidaResponse } = require('../core/yida-client');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
const { throwUsage } = require('../core/command-errors');
|
|
6
|
+
|
|
7
|
+
/** Keep complete runtime URLs; the server verifies tenant and resource ownership. */
|
|
8
|
+
function normalizeUrl(value, appType) {
|
|
9
|
+
const input = String(value || '').trim();
|
|
10
|
+
if (!input) { throw new Error(t('app_entry.invalid_url')); }
|
|
11
|
+
// eslint-disable-next-line no-control-regex -- reject control characters in persisted URLs
|
|
12
|
+
if (input.length > 2048 || /[\\\s\x00-\x1f\x7f]/.test(input)) {
|
|
13
|
+
throw new Error(t('app_entry.invalid_url'));
|
|
14
|
+
}
|
|
15
|
+
const match = /^https?:\/\/[^/?#]+(\/[^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i.exec(input);
|
|
16
|
+
if (!match) { throw new Error(t('app_entry.invalid_url')); }
|
|
17
|
+
const url = new URL(input);
|
|
18
|
+
const route = match[1];
|
|
19
|
+
const prefix = `/${appType}/`;
|
|
20
|
+
const parts = route.startsWith(prefix) ? route.slice(prefix.length).split('/') : [];
|
|
21
|
+
const short = /^\/(?:s\/[A-Za-z0-9_-]+|o\/[A-Za-z0-9_-]+(?:\/FORM-[A-Za-z0-9_-]+)?)$/.test(route);
|
|
22
|
+
const runtime = ['custom', 'workbench', 'submission', 'manage'].includes(parts[0]) &&
|
|
23
|
+
((parts.length === 1 && parts[0] === 'workbench') ||
|
|
24
|
+
(parts.length === 2 && /^[A-Za-z0-9_-]+$/.test(parts[1])));
|
|
25
|
+
if (url.username || url.password || url.pathname !== route || (!short && !runtime)) {
|
|
26
|
+
throw new Error(t('app_entry.invalid_url'));
|
|
27
|
+
}
|
|
28
|
+
if (match[2] !== undefined) {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const parameter of match[2].split('&')) {
|
|
31
|
+
const pair = /^(corpid|locale|viewUuid|processCode|hideLeftNav|noShowTopBottom)=([A-Za-z0-9_-]+)$/.exec(parameter);
|
|
32
|
+
if (!pair || seen.has(pair[1]) ||
|
|
33
|
+
(['hideLeftNav', 'noShowTopBottom'].includes(pair[1]) && !/^(true|false|y|n|0|1)$/.test(pair[2]))) {
|
|
34
|
+
throw new Error(t('app_entry.invalid_url'));
|
|
35
|
+
}
|
|
36
|
+
seen.add(pair[1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (match[3] !== undefined && !/^[A-Za-z0-9_/-]+$/.test(match[3])) {
|
|
40
|
+
throw new Error(t('app_entry.invalid_url'));
|
|
41
|
+
}
|
|
42
|
+
return input;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Reject duplicate or ambiguous flags; omissions never clear stored entries. */
|
|
46
|
+
function parseArgs(args) {
|
|
47
|
+
const [action, appType, ...flags] = args;
|
|
48
|
+
if (!['get', 'set'].includes(action) || !/^[A-Za-z0-9_-]+$/.test(appType || '')) {
|
|
49
|
+
throwUsage(t('app_entry.usage'));
|
|
50
|
+
}
|
|
51
|
+
const values = {};
|
|
52
|
+
for (let index = 0; index < flags.length; index++) {
|
|
53
|
+
const flag = flags[index];
|
|
54
|
+
if (flag === '--json') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const match = /^--(clear-)?(frontend|management)$/.exec(flag);
|
|
58
|
+
if (action !== 'set' || !match || Object.prototype.hasOwnProperty.call(values, match[2])) {
|
|
59
|
+
throwUsage(t('app_entry.usage'));
|
|
60
|
+
}
|
|
61
|
+
const value = match[1] ? null : flags[++index];
|
|
62
|
+
if (value !== null && (!value || value.startsWith('--'))) {
|
|
63
|
+
throwUsage(t('app_entry.usage'));
|
|
64
|
+
}
|
|
65
|
+
values[match[2]] = value;
|
|
66
|
+
}
|
|
67
|
+
if (action === 'set' && !Object.keys(values).length) {
|
|
68
|
+
throwUsage(t('app_entry.usage'));
|
|
69
|
+
}
|
|
70
|
+
return { action, appType, values };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read the revision used for a conditional partial write. */
|
|
74
|
+
async function readEntries(client, appType) {
|
|
75
|
+
const result = unwrapYidaResponse(await client.get(`/${appType}/query/app/getAccessEntries.json`));
|
|
76
|
+
if (!result || typeof result.revision !== 'string' || !result.revision ||
|
|
77
|
+
!result.accessEntries || typeof result.accessEntries !== 'object' || Array.isArray(result.accessEntries)) {
|
|
78
|
+
throw new Error(t('app_entry.invalid_response'));
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Persist only supplied entries and verify them through a separate read. */
|
|
84
|
+
async function run(args = []) {
|
|
85
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
86
|
+
console.log(t('app_entry.usage'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { action, appType, values } = parseArgs(args);
|
|
90
|
+
const authRef = createAuthRef();
|
|
91
|
+
const client = createYidaClient({ authRef });
|
|
92
|
+
const patch = {};
|
|
93
|
+
for (const [key, value] of Object.entries(values)) {
|
|
94
|
+
patch[key] = value === null ? null : { url: normalizeUrl(value, appType) };
|
|
95
|
+
}
|
|
96
|
+
let result = await readEntries(client, appType);
|
|
97
|
+
if (action === 'set') {
|
|
98
|
+
unwrapYidaResponse(await client.postForm(`/${appType}/query/app/saveAccessEntries.json`, {
|
|
99
|
+
accessEntries: JSON.stringify(patch), revision: result.revision,
|
|
100
|
+
}));
|
|
101
|
+
result = await readEntries(client, appType);
|
|
102
|
+
for (const [key, entry] of Object.entries(patch)) {
|
|
103
|
+
if ((result.accessEntries[key]?.url || '') !== (entry?.url || '')) {
|
|
104
|
+
throw new Error(t('app_entry.readback_failed'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const urls = {};
|
|
109
|
+
for (const key of ['frontend', 'management']) {
|
|
110
|
+
if (result.accessEntries[key]?.url) {
|
|
111
|
+
urls[key] = normalizeUrl(result.accessEntries[key].url, appType);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log(JSON.stringify({ appType, ...result, urls }, null, 2));
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { run, parseArgs, normalizeUrl };
|
package/lib/app/app-list.js
CHANGED
|
@@ -84,6 +84,7 @@ function formatApp(app, baseUrl) {
|
|
|
84
84
|
appName: (app.appName && app.appName.zh_CN) || '',
|
|
85
85
|
appType: app.appType || '',
|
|
86
86
|
systemLink: app.systemLink || '',
|
|
87
|
+
...(app.accessEntries ? { accessEntries: app.accessEntries } : {}),
|
|
87
88
|
...buildApplicationEntryUrls({ appType: app.appType, systemLink: app.systemLink, baseUrl }),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
@@ -2,8 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
const Babel = require('@babel/standalone');
|
|
4
4
|
const { CliError } = require('../core/cli-error');
|
|
5
|
+
const { warn } = require('../core/chalk');
|
|
5
6
|
const { t } = require('../core/i18n');
|
|
6
7
|
|
|
8
|
+
// Whether provably fixed brand overrides should be downgraded from a hard
|
|
9
|
+
// failure to a warning. Mirrors OPENYIDA_CANVAS_ALLOW_UNSUPPORTED_IMPORTS so
|
|
10
|
+
// legacy pages can ship first and migrate to canvas-theme later.
|
|
11
|
+
function isTruthy(value) {
|
|
12
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function allowFixedBrand(options = {}, env = process.env) {
|
|
16
|
+
return options.allowFixedBrand === true || isTruthy(env.OPENYIDA_CANVAS_ALLOW_FIXED_BRAND);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Whether fixed brand overrides should hard-block publishing. Off by default so
|
|
20
|
+
// users are never stuck: the guard warns and lets the app theme apply. Teams
|
|
21
|
+
// that want CI enforcement opt in via --strict-theme / OPENYIDA_CANVAS_STRICT_THEME.
|
|
22
|
+
function strictFixedBrand(options = {}, env = process.env) {
|
|
23
|
+
return options.strictBrand === true || isTruthy(env.OPENYIDA_CANVAS_STRICT_THEME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function emitThemeWarning(options, message) {
|
|
27
|
+
if (typeof options.onThemeWarning === 'function') { options.onThemeWarning(message); return; }
|
|
28
|
+
warn(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
7
32
|
// Check the maintained theme contract without executing business code.
|
|
8
33
|
// Dynamic component factories and runtime branches still need browser verification.
|
|
9
34
|
function assertCanvasThemeStructure(source, options = {}) {
|
|
@@ -123,9 +148,12 @@ function assertCanvasThemeStructure(source, options = {}) {
|
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
150
|
|
|
126
|
-
//
|
|
127
|
-
// Semantic colors, layout tokens and dynamic theme resolvers
|
|
128
|
-
|
|
151
|
+
// Collect every provably fixed brand color passed to antd's ConfigProvider.
|
|
152
|
+
// Semantic colors, layout tokens and dynamic theme resolvers are ignored.
|
|
153
|
+
// Each entry keeps the offending property's char offsets so the theme codemod
|
|
154
|
+
// can surgically remove it while the guard reuses the same detection to fail.
|
|
155
|
+
function collectBrandOverrides(ast, traverse) {
|
|
156
|
+
const violations = [];
|
|
129
157
|
const resolve = (p, seen = new Set()) => {
|
|
130
158
|
if (!p?.node || seen.has(p.node)) { return null; }
|
|
131
159
|
seen.add(p.node);
|
|
@@ -147,10 +175,12 @@ function assertBrandOverrides(ast, traverse, options) {
|
|
|
147
175
|
const color = val.isStringLiteral() ? val.node.value : val.node.quasis[0].value.cooked;
|
|
148
176
|
if (!color) { return; }
|
|
149
177
|
const line = val.node.loc?.start.line || 1;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
178
|
+
// Remove the override at its ConfigProvider usage site, not the resolved literal.
|
|
179
|
+
const property = p?.parentPath?.isObjectProperty() ? p.parentPath.node : null;
|
|
180
|
+
violations.push({
|
|
181
|
+
field, color, line,
|
|
182
|
+
start: property ? property.start : val.node.start,
|
|
183
|
+
end: property ? property.end : val.node.end,
|
|
154
184
|
});
|
|
155
185
|
};
|
|
156
186
|
const tokens = p => properties(p).forEach(item => {
|
|
@@ -179,6 +209,46 @@ function assertBrandOverrides(ast, traverse, options) {
|
|
|
179
209
|
}
|
|
180
210
|
},
|
|
181
211
|
});
|
|
212
|
+
return violations;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Fixed brand colors passed to antd's ConfigProvider are relaxed by default:
|
|
216
|
+
// warn and continue so users are never blocked (the app theme still applies at
|
|
217
|
+
// runtime). --strict-theme / OPENYIDA_CANVAS_STRICT_THEME restores the hard fail
|
|
218
|
+
// for CI enforcement; --allow-fixed-brand keeps its explicit ship-as-is warning.
|
|
219
|
+
function assertBrandOverrides(ast, traverse, options) {
|
|
220
|
+
const explicitlyAllowed = allowFixedBrand(options);
|
|
221
|
+
const strict = strictFixedBrand(options);
|
|
222
|
+
for (const { field, color, line } of collectBrandOverrides(ast, traverse)) {
|
|
223
|
+
if (explicitlyAllowed) {
|
|
224
|
+
emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_allowed', line, field));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!strict) {
|
|
228
|
+
emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_relaxed', line, field));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
throw new CliError(t('publish.canvas_theme_fixed_brand', line), {
|
|
232
|
+
code: 'OPENYIDA_CANVAS_THEME_FIXED_BRAND',
|
|
233
|
+
details: { stage: 'canvas_compile', sourcePath: options.sourcePath || '', line, field, value: color,
|
|
234
|
+
retryable: false, retrySafe: true, sideEffectState: 'none',
|
|
235
|
+
nextAction: { type: 'edit_source_then_recheck' },
|
|
236
|
+
sample: 'openyida sample openyida-page-template canvas-theme' },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Parse a canvas source and return removable fixed-brand overrides (with char
|
|
242
|
+
// offsets). Returns [] when the source has no theme markers or fails to parse,
|
|
243
|
+
// mirroring the guard's own gate so the codemod never acts on unrelated code.
|
|
244
|
+
function collectFixedBrandViolations(source) {
|
|
245
|
+
if (!/CanvasThemeProvider|useCanvasThemeContext|CanvasThemeContext|@canvas-theme-provider|antd/.test(source)) { return []; }
|
|
246
|
+
let ast;
|
|
247
|
+
try {
|
|
248
|
+
ast = Babel.packages.parser.parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
|
|
249
|
+
} catch { return []; }
|
|
250
|
+
const traverse = Babel.packages.traverse.default || Babel.packages.traverse;
|
|
251
|
+
return collectBrandOverrides(ast, traverse);
|
|
182
252
|
}
|
|
183
253
|
|
|
184
|
-
module.exports = { assertCanvasThemeStructure };
|
|
254
|
+
module.exports = { assertCanvasThemeStructure, collectFixedBrandViolations };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fix-theme.js - Canvas 主题迁移 codemod(纯函数)
|
|
5
|
+
*
|
|
6
|
+
* 把写死在 antd `ConfigProvider` 里的品牌交互色覆盖(如 `colorPrimary: '#1677ff'`)
|
|
7
|
+
* 从源码中移除,交由外层应用主题(CanvasThemeProvider / 平台主题)解析。这是
|
|
8
|
+
* "un-pin" 迁移:移除硬编码覆盖后,品牌色由应用主题接管,正好满足主题守卫
|
|
9
|
+
* `OPENYIDA_CANVAS_THEME_FIXED_BRAND` 的要求。
|
|
10
|
+
*
|
|
11
|
+
* 本模块不做任何 IO,只接收源码字符串、返回改写后的源码,便于单测与复用。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { collectFixedBrandViolations } = require('./canvas-theme-guard');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 计算某个对象属性的删除区间:优先连同其后的逗号一起删;若是最后一个属性,
|
|
18
|
+
* 则回退删除其前的逗号。属性独占一行时顺带清理该行缩进与换行,避免留下空行。
|
|
19
|
+
* @param {string} source
|
|
20
|
+
* @param {number} start - 属性节点起始偏移
|
|
21
|
+
* @param {number} end - 属性节点结束偏移
|
|
22
|
+
* @returns {{ start: number, end: number }}
|
|
23
|
+
*/
|
|
24
|
+
function expandRemovalSpan(source, start, end) {
|
|
25
|
+
let removeStart = start;
|
|
26
|
+
let removeEnd = end;
|
|
27
|
+
const trailing = source.slice(removeEnd).match(/^[ \t]*,/);
|
|
28
|
+
if (trailing) {
|
|
29
|
+
removeEnd += trailing[0].length;
|
|
30
|
+
const lineStart = source.lastIndexOf('\n', removeStart - 1) + 1;
|
|
31
|
+
const indent = source.slice(lineStart, removeStart);
|
|
32
|
+
if (/^[ \t]*$/.test(indent)) {
|
|
33
|
+
removeStart = lineStart;
|
|
34
|
+
const newline = source.slice(removeEnd).match(/^[ \t]*\r?\n/);
|
|
35
|
+
if (newline) { removeEnd += newline[0].length; }
|
|
36
|
+
}
|
|
37
|
+
return { start: removeStart, end: removeEnd };
|
|
38
|
+
}
|
|
39
|
+
// Last property in its object: drop the preceding comma instead.
|
|
40
|
+
const leading = source.slice(0, removeStart).match(/,[ \t\r\n]*$/);
|
|
41
|
+
if (leading) { removeStart -= leading[0].length; }
|
|
42
|
+
return { start: removeStart, end: removeEnd };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 移除源码中写死的品牌色覆盖。改写按偏移从后往前应用,避免前面的偏移失效。
|
|
47
|
+
* @param {string} source - canvas 源码
|
|
48
|
+
* @returns {{ changed: boolean, output: string, violations: Array<{field:string,color:string,line:number}> }}
|
|
49
|
+
*/
|
|
50
|
+
function transformFixedBrandTheme(source) {
|
|
51
|
+
const violations = collectFixedBrandViolations(source)
|
|
52
|
+
.filter(item => Number.isInteger(item.start) && Number.isInteger(item.end) && item.end > item.start);
|
|
53
|
+
if (!violations.length) {
|
|
54
|
+
return { changed: false, output: source, violations: [] };
|
|
55
|
+
}
|
|
56
|
+
const spans = violations
|
|
57
|
+
.map(item => expandRemovalSpan(source, item.start, item.end))
|
|
58
|
+
.sort((a, b) => a.start - b.start);
|
|
59
|
+
|
|
60
|
+
// Adjacent overrides can claim the same separating comma (one as trailing,
|
|
61
|
+
// the next as leading), so merge overlapping spans instead of dropping one.
|
|
62
|
+
const merged = [];
|
|
63
|
+
for (const span of spans) {
|
|
64
|
+
const last = merged[merged.length - 1];
|
|
65
|
+
if (last && span.start <= last.end) { last.end = Math.max(last.end, span.end); }
|
|
66
|
+
else { merged.push({ ...span }); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let output = source;
|
|
70
|
+
// Apply from the back so earlier offsets stay valid.
|
|
71
|
+
for (let i = merged.length - 1; i >= 0; i -= 1) {
|
|
72
|
+
output = output.slice(0, merged[i].start) + output.slice(merged[i].end);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
changed: output !== source,
|
|
77
|
+
output,
|
|
78
|
+
violations: violations.map(({ field, color, line }) => ({ field, color, line })),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { transformFixedBrandTheme, expandRemovalSpan };
|
package/lib/app/publish.js
CHANGED
|
@@ -99,9 +99,12 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
99
99
|
const compat = args.includes('--compat') || args.includes('--modern');
|
|
100
100
|
const force = args.includes('--force');
|
|
101
101
|
const canvas = args.includes('--canvas');
|
|
102
|
+
const allowFixedBrand = args.includes('--allow-fixed-brand');
|
|
103
|
+
const fixTheme = args.includes('--fix-theme');
|
|
104
|
+
const strictTheme = args.includes('--strict-theme');
|
|
102
105
|
const autoNavOrder = args.includes('--auto-nav-order');
|
|
103
106
|
const help = args.includes('--help') || args.includes('-h');
|
|
104
|
-
const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force' && arg !== '--canvas' && arg !== '--auto-nav-order' && arg !== '--json' && arg !== '--help' && arg !== '-h');
|
|
107
|
+
const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force' && arg !== '--canvas' && arg !== '--allow-fixed-brand' && arg !== '--fix-theme' && arg !== '--strict-theme' && arg !== '--auto-nav-order' && arg !== '--json' && arg !== '--help' && arg !== '-h');
|
|
105
108
|
|
|
106
109
|
if (help) {
|
|
107
110
|
usage(t('publish.usage'), t('publish.example'));
|
|
@@ -122,6 +125,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
122
125
|
compat,
|
|
123
126
|
force,
|
|
124
127
|
canvas,
|
|
128
|
+
allowFixedBrand,
|
|
129
|
+
fixTheme,
|
|
130
|
+
strictTheme,
|
|
125
131
|
autoNavOrder,
|
|
126
132
|
browserOpenMode: openOption.mode,
|
|
127
133
|
};
|
|
@@ -146,6 +152,52 @@ function buildCanvasCompileCliError(errorObject, sourcePath) {
|
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
// Ask the human (via stdin) whether to migrate. Returns false unless both the
|
|
156
|
+
// input and the prompt's output stream are real TTYs, so agent/CI/piped runs
|
|
157
|
+
// never block on a question nobody can answer and must opt in with --fix-theme.
|
|
158
|
+
function canPromptInteractively() {
|
|
159
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function confirmFixedBrandMigration() {
|
|
163
|
+
if (!canPromptInteractively()) { return Promise.resolve(false); }
|
|
164
|
+
const readline = require('readline');
|
|
165
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
rl.question(`${t('publish.fix_theme_confirm')} `, (answer) => {
|
|
168
|
+
rl.close();
|
|
169
|
+
resolve(/^(y|yes)$/i.test(String(answer).trim()));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Offer to un-pin fixed brand colors so the app theme takes over. Confirms with
|
|
175
|
+
// the user (or auto-applies when --fix-theme is set), rewrites the source file
|
|
176
|
+
// in place, and returns the migrated source; returns null when nothing changed
|
|
177
|
+
// or the user declined. Callers continue publishing regardless unless strict.
|
|
178
|
+
async function offerFixedBrandThemeMigration(sourcePath, source, { autoConfirm, strict } = {}) {
|
|
179
|
+
const { transformFixedBrandTheme } = require('./fix-theme');
|
|
180
|
+
const result = transformFixedBrandTheme(source);
|
|
181
|
+
if (!result.changed) { return null; }
|
|
182
|
+
|
|
183
|
+
warn(t('publish.fix_theme_detected', result.violations.length));
|
|
184
|
+
result.violations.forEach((item) => hint(t('publish.fix_theme_item', item.line, item.field, item.color)));
|
|
185
|
+
|
|
186
|
+
const proceed = autoConfirm === true || await confirmFixedBrandMigration();
|
|
187
|
+
if (!proceed) {
|
|
188
|
+
// Only nudge harder in strict mode; the relaxed path prints its own note.
|
|
189
|
+
if (strict) {
|
|
190
|
+
if (!canPromptInteractively()) { info(t('publish.fix_theme_flag_hint')); }
|
|
191
|
+
else { info(t('publish.fix_theme_skipped')); }
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.writeFileSync(sourcePath, result.output);
|
|
197
|
+
success(t('publish.fix_theme_applied', sourcePath));
|
|
198
|
+
return result.output;
|
|
199
|
+
}
|
|
200
|
+
|
|
149
201
|
function enableQuietForJson(argv = []) {
|
|
150
202
|
if (!Array.isArray(argv) || !argv.includes('--json')) {
|
|
151
203
|
return () => {};
|
|
@@ -614,7 +666,7 @@ async function runMain(argv) {
|
|
|
614
666
|
if (options.help) {
|
|
615
667
|
return options;
|
|
616
668
|
}
|
|
617
|
-
const { appType, formUuid, sourceFile, skipLint, healthCheck, compat, force, canvas: canvasFlag, autoNavOrder, browserOpenMode } = options;
|
|
669
|
+
const { appType, formUuid, sourceFile, skipLint, healthCheck, compat, force, canvas: canvasFlag, allowFixedBrand, fixTheme, strictTheme, autoNavOrder, browserOpenMode } = options;
|
|
618
670
|
|
|
619
671
|
let sourcePath = path.resolve(sourceFile);
|
|
620
672
|
if (!fs.existsSync(sourcePath)) {
|
|
@@ -644,8 +696,20 @@ async function runMain(argv) {
|
|
|
644
696
|
// (JSX/TS → runtimeCode + importedModules),无需登录态,可在开浏览器前失败快。
|
|
645
697
|
step(1, t('publish.step_canvas_compile'));
|
|
646
698
|
info(t('publish.canvas_compiling'));
|
|
699
|
+
// Offer to un-pin fixed brand colors before compiling so the ask-human /
|
|
700
|
+
// --fix-theme migration still runs now that the guard no longer blocks by
|
|
701
|
+
// default. Skipped when --allow-fixed-brand explicitly ships as-is. In
|
|
702
|
+
// non-TTY runs the prompt declines instead of hanging, and the guard warns
|
|
703
|
+
// (or hard-fails under --strict-theme) rather than blocking the user.
|
|
704
|
+
if (!allowFixedBrand) {
|
|
705
|
+
const migratedSource = await offerFixedBrandThemeMigration(sourcePath, initialSourceCode, {
|
|
706
|
+
autoConfirm: fixTheme,
|
|
707
|
+
strict: strictTheme,
|
|
708
|
+
});
|
|
709
|
+
if (migratedSource !== null) { sourceCode = migratedSource; }
|
|
710
|
+
}
|
|
647
711
|
try {
|
|
648
|
-
canvasResult = await compileCanvas(
|
|
712
|
+
canvasResult = await compileCanvas(sourceCode, { sourcePath, allowFixedBrand, strictBrand: strictTheme });
|
|
649
713
|
} catch (canvasCompileError) {
|
|
650
714
|
throw buildCanvasCompileCliError(canvasCompileError, sourcePath);
|
|
651
715
|
}
|
|
@@ -403,6 +403,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
|
|
|
403
403
|
|
|
404
404
|
...sideEffectEntries([
|
|
405
405
|
'app-list',
|
|
406
|
+
'app-entry.get',
|
|
406
407
|
'connector.detail',
|
|
407
408
|
'connector.list',
|
|
408
409
|
'connector.list-actions',
|
|
@@ -473,6 +474,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
|
|
|
473
474
|
'save-permission',
|
|
474
475
|
'save-share-config',
|
|
475
476
|
'update-app',
|
|
477
|
+
'app-entry.set',
|
|
476
478
|
'update-form-config',
|
|
477
479
|
], sideEffect('remote_write')),
|
|
478
480
|
|
|
@@ -634,6 +636,7 @@ const COMMAND_PERMISSIONS = new Map([
|
|
|
634
636
|
...permissionEntries([
|
|
635
637
|
'agent-capabilities',
|
|
636
638
|
'app-list',
|
|
639
|
+
'app-entry.get',
|
|
637
640
|
'check-design',
|
|
638
641
|
'check-prd-completeness',
|
|
639
642
|
'check-page',
|
|
@@ -725,6 +728,7 @@ const COMMAND_PERMISSIONS = new Map([
|
|
|
725
728
|
'save-permission',
|
|
726
729
|
'save-share-config',
|
|
727
730
|
'update-app',
|
|
731
|
+
'app-entry.set',
|
|
728
732
|
'update-form-config',
|
|
729
733
|
], permission('allow', 'write', {
|
|
730
734
|
reason: ALLOW_WRITE_REASON,
|
|
@@ -1047,6 +1051,8 @@ const COMMAND_GROUPS = [
|
|
|
1047
1051
|
id: 'app',
|
|
1048
1052
|
titleKey: 'help.group_app',
|
|
1049
1053
|
commands: [
|
|
1054
|
+
command('app-entry.get', ['app-entry', 'get'], 'app-entry get <appType> [--json]', 'app_entry.desc_get'),
|
|
1055
|
+
command('app-entry.set', ['app-entry', 'set'], 'app-entry set <appType> [--frontend <url>] [--management <url>] [--clear-frontend] [--clear-management] [--json]', 'app_entry.desc_set'),
|
|
1050
1056
|
command('app-list', ['app-list'], 'app-list [--type managed|created] [--page N] [--size N]', 'help.cmd_app_list', {
|
|
1051
1057
|
notes: ['Successful results include appUrl/workbenchUrl for the business workspace and adminUrl for developer administration. Complete application delivery retains adminUrl regardless of auth runtime; route construction does not verify recipient access. Preserve the frontend URL from its own page result.'],
|
|
1052
1058
|
}),
|
|
@@ -1860,6 +1866,10 @@ function summarizeLocalizedCommands(commands) {
|
|
|
1860
1866
|
},
|
|
1861
1867
|
application_entry_policy: {
|
|
1862
1868
|
delivery_unit: 'single_application_entry_group',
|
|
1869
|
+
persistence: {
|
|
1870
|
+
command_ids: ['app-entry.get', 'app-entry.set'],
|
|
1871
|
+
policy: 'After successful application delivery, register only verified in-scope runtime entries; set performs partial merge and readback. Do not register admin/design URLs. Incremental resource-only work preserves existing entries. Clear only on explicit removal. Conflicts require reread and review, never blind retries.',
|
|
1872
|
+
},
|
|
1863
1873
|
workbench: {
|
|
1864
1874
|
include: 'when_workspace_in_scope',
|
|
1865
1875
|
url: '{base_url}/{appType}/workbench',
|
package/lib/core/locales/en.js
CHANGED
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
app_entry: {
|
|
8
|
+
desc_get: 'Read frontend and management runtime entries',
|
|
9
|
+
desc_set: 'Register application entries with partial update and readback',
|
|
10
|
+
usage: 'Usage: openyida app-entry get <appType> [--json]\n openyida app-entry set <appType> [--frontend <url>] [--management <url>] [--clear-frontend] [--clear-management] [--json]\nRegister only published, verified runtime URLs for this application. Omitted entries remain unchanged.',
|
|
11
|
+
invalid_url: 'Provide a complete http:// or https:// URL for a page, public page, or short link in the current application. Relative paths, designer URLs, and temporary authentication parameters are unsupported.',
|
|
12
|
+
invalid_response: 'Incomplete entry response. Read the current entries before retrying and check that the server supports the entry configuration API.',
|
|
13
|
+
readback_failed: 'Saved entries do not match the readback. Read and review the current entries; do not overwrite automatically.',
|
|
14
|
+
},
|
|
7
15
|
design_plan: {
|
|
8
16
|
rebase_required: 'Before first materialization, repair the matching facts in business.json/visual.json and preserve the source baseline. If the source changed, add --rebase-parts to the original materialize command to reconcile against the saved baseline. Do not invent a digest, delete drafts, or run init again.',
|
|
9
17
|
baseline_missing: 'No trusted baseline matches these parts. Preserve the plan and facts; restore .build-plan-base.json from the original workspace. If unavailable, report the blocker instead of inventing a digest or rebuilding.',
|
|
10
|
-
rebase_stage_invalid: '
|
|
18
|
+
rebase_stage_invalid: 'The plan is confirmed, so initial parts are no longer merged. Keep the current build-plan.json and adjust it through patch --materialize.',
|
|
11
19
|
rebase_conflict: 'The source and part changed the same field differently. Review conflicts and explicitly select the current or baseline value in the part before retrying; preserve other facts. No files were saved.',
|
|
12
20
|
repair_page_bindings: 'Repair pageApplications in visual.json to match customPageDetails in business.json one to one; exclude native forms. Each visualMemoryApplications must be an array; use [] when none apply. Keep the existing draft, fix the listed fields and retry the original command. Do not delete the directory or run init again.',
|
|
13
21
|
},
|
|
@@ -1336,7 +1344,15 @@ Examples:
|
|
|
1336
1344
|
},
|
|
1337
1345
|
publish: {
|
|
1338
1346
|
canvas_path_missing_app_type: 'Line {0}: navigation is missing appType. Use the canvas-navigation sample and verified IDs to build /{appType}/{pageType}/{formUuid}, not a bare /custom/ route.',
|
|
1339
|
-
canvas_theme_fixed_brand: 'Line {0}: ConfigProvider fixes a brand interaction color to a string. Use canvas-theme and resolved application colors; CSS var belongs in DOM styles, not unresolved antd color tokens.',
|
|
1347
|
+
canvas_theme_fixed_brand: 'Line {0}: ConfigProvider fixes a brand interaction color to a string. Use canvas-theme and resolved application colors; CSS var belongs in DOM styles, not unresolved antd color tokens. This is a local compile-time check and no network request has been made yet; pass --allow-fixed-brand or set OPENYIDA_CANVAS_ALLOW_FIXED_BRAND=1 to bypass temporarily.',
|
|
1348
|
+
canvas_theme_fixed_brand_allowed: 'Line {0}: ConfigProvider hardcodes a brand interaction color ({1}); allowed for now because --allow-fixed-brand / OPENYIDA_CANVAS_ALLOW_FIXED_BRAND is set. Migrate to canvas-theme (resolved application colors) soon; this bypass will be removed in a later release.',
|
|
1349
|
+
canvas_theme_fixed_brand_relaxed: 'Line {0}: ConfigProvider hardcodes a brand interaction color ({1}). Publishing is no longer blocked by default and continues as-is, but this color will not follow the application theme. Add --fix-theme to migrate automatically, or migrate to canvas-theme manually; use --strict-theme to hard-fail in CI.',
|
|
1350
|
+
fix_theme_detected: 'Found {0} hardcoded brand interaction color override(s) that can be auto-migrated: remove the hardcoded overrides and let the application theme resolve them.',
|
|
1351
|
+
fix_theme_item: ' · Line {0} {1}: {2}',
|
|
1352
|
+
fix_theme_confirm: 'Auto-migrate the theme now? This rewrites your source file (removes the hardcoded brand-color overrides). [y/N]',
|
|
1353
|
+
fix_theme_applied: 'Theme migration applied and written back to source: {0}',
|
|
1354
|
+
fix_theme_skipped: 'Skipped theme migration; source left unchanged. Migrate to canvas-theme manually, or pass --allow-fixed-brand to bypass temporarily.',
|
|
1355
|
+
fix_theme_flag_hint: 'Non-interactive environment: no auto-migration. Re-run with --fix-theme to rewrite the source automatically.',
|
|
1340
1356
|
canvas_navigation_local_platform: 'Line {0} filters local views through platform navigation. Use mode=local or local view state; bind real pages to formUuid/navUuid and continue checking declared access.',
|
|
1341
1357
|
canvas_navigation_document_flex: 'Line {0} uses a zero-basis workspace flex layout for document content. Refresh canvas-nav-content: document pages need natural height and block flow; fixed-height flex belongs to workspace.',
|
|
1342
1358
|
canvas_theme_not_assembled: 'The theme marker on line {0} is not assembled. Run build-canvas-theme.js and compile/publish its separate output file. Pages with an integrated Provider do not need this marker.',
|
|
@@ -1467,7 +1483,7 @@ Examples:
|
|
|
1467
1483
|
error: '\n❌ Publish error: {0}',
|
|
1468
1484
|
source_not_found: '❌ Source file not found: {0}',
|
|
1469
1485
|
source_path_hint: '💡 Try this source file path: {0}',
|
|
1470
|
-
usage: 'Usage: openyida publish <sourceFile> <appType> <formUuid> [--health-check] [--force] [--canvas] [--compat] [--skip-lint] [--auto-nav-order] [--open|--no-open] [--json]',
|
|
1486
|
+
usage: 'Usage: openyida publish <sourceFile> <appType> <formUuid> [--health-check] [--force] [--canvas] [--compat] [--skip-lint] [--fix-theme] [--strict-theme] [--auto-nav-order] [--open|--no-open] [--json]',
|
|
1471
1487
|
example: 'Example: openyida publish pages/src/xxx.js APP_XXX FORM-XXX --health-check --auto-nav-order',
|
|
1472
1488
|
},
|
|
1473
1489
|
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
app_entry: {
|
|
8
|
+
desc_get: '读取应用前台和业务工作台入口',
|
|
9
|
+
desc_set: '登记应用访问入口(局部更新并回读)',
|
|
10
|
+
usage: '用法:openyida app-entry get <appType> [--json]\n openyida app-entry set <appType> [--frontend <url>] [--management <url>] [--clear-frontend] [--clear-management] [--json]\n仅登记已发布并验证的当前应用访问态地址;未指定的入口保持不变。',
|
|
11
|
+
invalid_url: '请提供当前组织、当前应用的完整访问 URL(http:// 或 https://);支持页面地址、公开地址和短链接,不支持相对路径、开发后台和临时鉴权参数。',
|
|
12
|
+
invalid_response: '访问入口响应不完整,请重新读取后再决定是否重试,并确认服务端支持入口配置接口。',
|
|
13
|
+
readback_failed: '访问入口保存后的回读结果不一致,请重新读取并确认;不要自动覆盖。',
|
|
14
|
+
},
|
|
7
15
|
design_plan: {
|
|
8
16
|
rebase_required: '首次物化前请修正 business.json/visual.json 中的对应 facts,保留主计划基线。如果主计划已改变,在原 materialize 命令加 --rebase-parts,CLI 会根据保存的基线核对差异;不要手填 digest、删除草稿或重新 init。',
|
|
9
17
|
baseline_missing: '未找到与片段匹配的可信原始基线。保留当前计划与 facts;从原工作区恢复 .build-plan-base.json 后重试。无法恢复时明确告知用户阻塞,不伪造摘要或自动重建。',
|
|
10
|
-
rebase_stage_invalid: '
|
|
18
|
+
rebase_stage_invalid: '方案已确认,不再合并初始化片段。请沿用当前 build-plan.json,通过 patch --materialize 调整。',
|
|
11
19
|
rebase_conflict: '主计划和片段对同一字段有不同修改。按 conflicts 核对并在片段中明确采用当前值或原基线值后重试;其他 facts 保留。本次未保存文件。',
|
|
12
20
|
repair_page_bindings: '按 business.json 的 customPageDetails 逐一修复 visual.json 的 pageApplications;原生表单不加入绑定。每项 visualMemoryApplications 必须为数组,无适用项用 []。保留现有草稿,按列出的字段修复后重试原命令;不要删除目录或重新 init。',
|
|
13
21
|
},
|
|
@@ -1333,7 +1341,15 @@ openyida - 宜搭命令行工具
|
|
|
1333
1341
|
},
|
|
1334
1342
|
publish: {
|
|
1335
1343
|
canvas_path_missing_app_type: '第 {0} 行的跳转地址缺少 appType。请提取 canvas-navigation,使用真实 appType 与页面 ID 构造 /{appType}/{pageType}/{formUuid};不要直接跳转 /custom/ 等裸路径。',
|
|
1336
|
-
canvas_theme_fixed_brand: '第 {0} 行的 ConfigProvider 将品牌交互色固定为字符串。请提取 canvas-theme,使用应用主题解析值;CSS var 仅直接用于 DOM 样式,不能替代 antd
|
|
1344
|
+
canvas_theme_fixed_brand: '第 {0} 行的 ConfigProvider 将品牌交互色固定为字符串。请提取 canvas-theme,使用应用主题解析值;CSS var 仅直接用于 DOM 样式,不能替代 antd 色值解析。这是本地编译期校验,此时尚未发起任何网络请求;如需临时放行请加 --allow-fixed-brand 或设置 OPENYIDA_CANVAS_ALLOW_FIXED_BRAND=1。',
|
|
1345
|
+
canvas_theme_fixed_brand_allowed: '第 {0} 行的 ConfigProvider 写死了品牌交互色({1}),因已设置 --allow-fixed-brand / OPENYIDA_CANVAS_ALLOW_FIXED_BRAND 暂时放行。请尽快迁移到 canvas-theme(应用主题解析),此旁路将在后续版本移除。',
|
|
1346
|
+
canvas_theme_fixed_brand_relaxed: '第 {0} 行的 ConfigProvider 写死了品牌交互色({1})。默认不再阻断发布,已按原样继续;但该色值不会跟随应用主题。建议加 --fix-theme 自动迁移,或手动迁移到 canvas-theme;如需在 CI 强制拦截可加 --strict-theme。',
|
|
1347
|
+
fix_theme_detected: '检测到 {0} 处写死的品牌交互色覆盖,可自动迁移:移除这些硬编码覆盖,交由应用主题解析。',
|
|
1348
|
+
fix_theme_item: ' · 第 {0} 行 {1}:{2}',
|
|
1349
|
+
fix_theme_confirm: '是否自动迁移主题?此操作会改写你的源码文件(移除写死的品牌色覆盖)。[y/N]',
|
|
1350
|
+
fix_theme_applied: '已完成主题迁移并写回源码:{0}',
|
|
1351
|
+
fix_theme_skipped: '已跳过主题迁移,未改动源码;你可以手动迁移到 canvas-theme,或加 --allow-fixed-brand 临时放行。',
|
|
1352
|
+
fix_theme_flag_hint: '非交互环境未自动迁移;如需自动改写请重新执行并加 --fix-theme。',
|
|
1337
1353
|
canvas_navigation_local_platform: '第 {0} 行把页内菜单交给平台导航过滤。请使用 mode=local 或直接切换本地视图;真实页面绑定 formUuid/navUuid,声明的权限仍需校验。',
|
|
1338
1354
|
canvas_navigation_document_flex: '第 {0} 行的导航内容仍使用工作区 flex 零基准布局,会压缩 document 长页。请重新提取 canvas-nav-content:document 使用自然高度与块布局,workspace 才使用定高 flex。',
|
|
1339
1355
|
canvas_theme_not_assembled: '第 {0} 行的主题标记尚未装配。请运行 build-canvas-theme.js 生成独立页面并编译发布输出文件;已直接接好 Provider 的页面无需此标记。',
|
|
@@ -1464,7 +1480,7 @@ openyida - 宜搭命令行工具
|
|
|
1464
1480
|
error: '\n❌ 发布异常: {0}',
|
|
1465
1481
|
source_not_found: '❌ 源文件不存在:{0}',
|
|
1466
1482
|
source_path_hint: '💡 可尝试使用源文件路径:{0}',
|
|
1467
|
-
usage: '用法: openyida publish <源文件路径> <appType> <formUuid> [--health-check] [--force] [--canvas] [--compat] [--skip-lint] [--auto-nav-order] [--open|--no-open] [--json]',
|
|
1483
|
+
usage: '用法: openyida publish <源文件路径> <appType> <formUuid> [--health-check] [--force] [--canvas] [--compat] [--skip-lint] [--fix-theme] [--strict-theme] [--auto-nav-order] [--open|--no-open] [--json]',
|
|
1468
1484
|
example: '示例:openyida publish pages/src/xxx.js APP_XXX FORM-XXX --health-check --auto-nav-order',
|
|
1469
1485
|
},
|
|
1470
1486
|
|
|
@@ -827,6 +827,7 @@ function renderHtml(plan, sections) {
|
|
|
827
827
|
if (result.error || result.status !== 0) {
|
|
828
828
|
throw new CliError((result.stderr || result.error?.message || 'build-plan.html 生成失败').trim(), {
|
|
829
829
|
code: 'DESIGN_PLAN_VALIDATION_FAILED',
|
|
830
|
+
details: { executable, status: result.status, stderr: result.stderr, error: result.error?.message },
|
|
830
831
|
});
|
|
831
832
|
}
|
|
832
833
|
return result.stdout;
|
|
@@ -850,19 +851,13 @@ function renderDraftPrd(plan) {
|
|
|
850
851
|
|
|
851
852
|
function prepareArtifacts(sourcePlan, outputDir, themeSeed, options = {}) {
|
|
852
853
|
const timings = {};
|
|
853
|
-
let authoringIssues = [];
|
|
854
854
|
const measure = (name, action) => {
|
|
855
855
|
const start = performance.now();
|
|
856
|
-
try {return action();}
|
|
857
|
-
if (authoringIssues.length && !error.details?.issues) {
|
|
858
|
-
error.details = { ...error.details, issues: authoringIssues };
|
|
859
|
-
}
|
|
860
|
-
throw error;
|
|
861
|
-
} finally {timings[name] = Math.round((performance.now() - start) * 100) / 100;}
|
|
856
|
+
try {return action();} finally {timings[name] = Math.round((performance.now() - start) * 100) / 100;}
|
|
862
857
|
};
|
|
863
858
|
const plan = measure('validationAndNormalizationMs', () => {
|
|
864
859
|
const normalized = normalizePlan(sourcePlan);
|
|
865
|
-
|
|
860
|
+
validateAuthoring(normalized);
|
|
866
861
|
return normalized;
|
|
867
862
|
});
|
|
868
863
|
const designFile = path.resolve(outputDir, 'design.md');
|
|
@@ -880,11 +875,6 @@ function prepareArtifacts(sourcePlan, outputDir, themeSeed, options = {}) {
|
|
|
880
875
|
measure('designValidationMs', () => validateDesignDocument(design, { prdMarkdown: prd, designFile }));
|
|
881
876
|
// Validate the same public CSS pipeline used by both Fast and Plan before writing artifacts.
|
|
882
877
|
const baseTheme = measure('themeValidationMs', () => applyDesignTokens(fs.readFileSync(path.join(PACKAGE_ROOT, 'yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css'), 'utf8'), design));
|
|
883
|
-
if (options.renderHtml === false && authoringIssues.length) {
|
|
884
|
-
throw new CliError(t('design_document.invalid', 'build-plan.json', authoringIssues.map(issue => `${issue.path}: ${issue.message}`).join('; ')), {
|
|
885
|
-
code: 'DESIGN_PLAN_VALIDATION_FAILED', details: { issues: authoringIssues },
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
878
|
const html = options.renderHtml === false ? undefined : measure('htmlMs', () => renderHtml({ ...plan, execution }));
|
|
889
879
|
const outputs = {
|
|
890
880
|
prd: path.join(outputDir, 'prd.md'),
|
|
@@ -27,7 +27,10 @@ function readPart(file, base) {
|
|
|
27
27
|
if (part.base?.digest !== base.digest || part.base?.revision !== base.revision) { invalid('stale base'); }
|
|
28
28
|
if (!part.facts || typeof part.facts !== 'object' || Array.isArray(part.facts)) { invalid('facts'); }
|
|
29
29
|
const keys = Object.keys(part.facts);
|
|
30
|
-
|
|
30
|
+
const unsupported = keys.filter(key => !MODULES.includes(key));
|
|
31
|
+
if (!keys.length || unsupported.length) {
|
|
32
|
+
invalid(`facts modules: ${keys.length ? unsupported.join(', ') : 'none'}; allowed: ${MODULES.join(', ')}`);
|
|
33
|
+
}
|
|
31
34
|
for (const [key, value] of Object.entries(part.facts)) {
|
|
32
35
|
if (['dataModels', 'businessFlows'].includes(key) ? !Array.isArray(value) : !value || typeof value !== 'object' || Array.isArray(value)) { invalid(key); }
|
|
33
36
|
}
|
|
@@ -26,6 +26,13 @@ function reconcile(base, current, proposed, field, conflicts) {
|
|
|
26
26
|
return proposed;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Only part-proposed meta keys merge, so CLI-owned fields (revision, status, planState) stay untouched.
|
|
30
|
+
function reconcileMeta(base, current, proposed, conflicts) {
|
|
31
|
+
if (!object(proposed)) { return reconcile(base, current, proposed, 'meta', conflicts); }
|
|
32
|
+
return Object.fromEntries(Object.entries(proposed).map(([key, value]) => [key,
|
|
33
|
+
reconcile(base?.[key], current?.[key], value, `meta.${key}`, conflicts)]));
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
function rebaseParts(source, input, businessFile, visualFile) {
|
|
30
37
|
const { planBase } = require('./parallel');
|
|
31
38
|
const baselinePath = path.join(path.dirname(path.resolve(input)), BASE_FILE);
|
|
@@ -35,8 +42,9 @@ function rebaseParts(source, input, businessFile, visualFile) {
|
|
|
35
42
|
if (saved.schemaVersion !== 1 || !object(saved.plan) || !equal(saved.base, planBase(saved.plan))) {
|
|
36
43
|
fail('DESIGN_PLAN_BASELINE_INVALID', { nextStep: t('design_plan.baseline_missing') });
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
// Confirmed plans are final: later edits go through patch --materialize.
|
|
46
|
+
if (source.meta?.status === 'confirmed' || source.meta?.planState?.planConfirmed === true
|
|
47
|
+
|| source.meta?.planState?.confirmedRevision) {
|
|
40
48
|
fail('DESIGN_PLAN_REBASE_STAGE_INVALID', { nextStep: t('design_plan.rebase_stage_invalid') });
|
|
41
49
|
}
|
|
42
50
|
const parts = [businessFile, visualFile].map(file => readJson(file));
|
|
@@ -47,8 +55,9 @@ function rebaseParts(source, input, businessFile, visualFile) {
|
|
|
47
55
|
}
|
|
48
56
|
return parts.map((part, index) => {
|
|
49
57
|
const conflicts = [];
|
|
50
|
-
const facts = Object.fromEntries(Object.keys(part.facts).map(key => [key,
|
|
51
|
-
|
|
58
|
+
const facts = Object.fromEntries(Object.keys(part.facts).map(key => [key, key === 'meta'
|
|
59
|
+
? reconcileMeta(saved.plan.meta, source.meta, part.facts.meta, conflicts)
|
|
60
|
+
: reconcile(saved.plan[key], source[key], part.facts[key], key, conflicts),
|
|
52
61
|
]));
|
|
53
62
|
if (conflicts.length) {
|
|
54
63
|
fail('DESIGN_PLAN_REBASE_CONFLICT', { sourcePath: [businessFile, visualFile][index], conflicts, nextStep: t('design_plan.rebase_conflict') });
|
|
@@ -181,8 +181,8 @@ function collectIssues(plan) {
|
|
|
181
181
|
|
|
182
182
|
function validateAuthoring(plan) {
|
|
183
183
|
const issues = collectIssues(plan);
|
|
184
|
-
//
|
|
185
|
-
if (issues.length
|
|
184
|
+
// Report every gap here: stricter downstream validators would surface it with a different error shape.
|
|
185
|
+
if (issues.length) {
|
|
186
186
|
throw new CliError(`计划有 ${issues.length} 项待补充:\n${issues.map(issue => `${issue.path}: ${issue.message}`).join('\n')}`, {
|
|
187
187
|
code: 'DESIGN_PLAN_VALIDATION_FAILED', details: { issues },
|
|
188
188
|
});
|
package/package.json
CHANGED
|
@@ -63,4 +63,4 @@ build-plan.json(业务和视觉的源事实)
|
|
|
63
63
|
|
|
64
64
|
技能加载失败、限流或换模型后,先成功读取本工作流和当前步骤,再沿用已确认需求继续;未加载成功不得凭记忆生成 HTML 或确认参数。首次物化前只修 business.json/visual.json 对应 facts(菜单与执行规划在 business.facts.execution),不要直接修初始化主计划。字段错误按具体路径修正后重试。
|
|
65
65
|
|
|
66
|
-
如果已经改动主计划并出现 DESIGN_PLAN_STALE_PART,在原 materialize 命令增加 --rebase-parts。CLI 使用 init 保存的 .build-plan-base.json 做三方核对,保留已完成 facts;冲突按 details.conflicts 明确选择后重试。基线缺失或无法核实时告知阻塞并恢复可信文件,不手填 digest、不删除重建、不重新 init
|
|
66
|
+
如果已经改动主计划并出现 DESIGN_PLAN_STALE_PART,在原 materialize 命令增加 --rebase-parts。CLI 使用 init 保存的 .build-plan-base.json 做三方核对,保留已完成 facts;冲突按 details.conflicts 明确选择后重试。基线缺失或无法核实时告知阻塞并恢复可信文件,不手填 digest、不删除重建、不重新 init。方案确认后调整一律走 patch --materialize;确认前重复物化仍可用 --rebase-parts 与基线核对,但改动内容以当前 build-plan.json 为准。
|
|
@@ -44,6 +44,26 @@
|
|
|
44
44
|
|
|
45
45
|
需求含 `userTasks` 和 `entryRecommendation` 时,按最终 PRD 核对服务与管理任务的实际访问路径,沿用用户已确认的调整;入口建议不作为完成证据。只有相关页面及访问配置回读通过后才交付对应入口,不为双入口补造后台功能,不用 `/admin` 开发后台代替业务管理工作区。
|
|
46
46
|
|
|
47
|
+
## 登记应用访问入口
|
|
48
|
+
|
|
49
|
+
前台与业务工作台是两个独立可选的入口,不是完整应用必须填满的字段。完整应用搭建完成后,根据本轮实际业务场景、用户确认的交付范围和入口回读证据决定是否登记,使应用卡片可以直接访问;不能只因为 CLI 返回了 `appUrl` / `workbenchUrl` 就自动登记为业务工作台。
|
|
50
|
+
|
|
51
|
+
有需要登记的入口时,先执行 `openyida app-entry get <appType>` 查看已有配置,再按以下情况调用。命令内部执行版本校验、局部更新和保存后回读,输出 `urls` 供交付使用。
|
|
52
|
+
|
|
53
|
+
| 本轮实际场景 | 登记方式 |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| 只有已验证的前台入口,例如公开展示或用户提交页面 | `openyida app-entry set <appType> --frontend <已验证前台完整URL>` |
|
|
56
|
+
| 只有已验证的业务管理入口,例如内部管理工作区 | `openyida app-entry set <appType> --management <已验证业务管理完整URL>` |
|
|
57
|
+
| 两类入口均在交付范围内,且各自验证通过 | `openyida app-entry set <appType> --frontend <已验证前台完整URL> --management <已验证业务管理完整URL>` |
|
|
58
|
+
| 两类入口均不适用或尚未验证 | 不执行 `app-entry set`;不传空字符串、`null`、占位 URL,不为了补齐配置创建页面或拼接工作台链接 |
|
|
59
|
+
|
|
60
|
+
- 前台地址必须来自已发布页面的成功返回及回读;独立前台仍需核验 `isRenderNav=false`。
|
|
61
|
+
- 已验证的公开地址按完整 URL 原样登记,保留所选入口的域名、路径和有效参数;不能替换成需要登录的长链接,也不能为了登记而自动开启公开访问。
|
|
62
|
+
- 业务工作台使用已验证的访问态 URL,不能登记 `/admin` 或设计器地址。
|
|
63
|
+
- 单页、表单、流程等局部修改不自动覆盖应用入口。只搭前台时不传 `--management`;缺省值保留原配置。
|
|
64
|
+
- 仅在用户明确移除入口时使用 `--clear-frontend` 或 `--clear-management`。
|
|
65
|
+
- 版本冲突时重新读取并核对,不盲目重试覆盖。接口未部署、保存或回读失败时,明确说明入口尚未登记,不能宣称应用卡片已可访问。
|
|
66
|
+
|
|
47
67
|
## build-manifest 约定
|
|
48
68
|
|
|
49
69
|
完整搭建收尾前,从本轮真实创建、复用和发布结果写入 `prd/<项目名>/build-manifest.json`。它只是轻量事实源,不是严格 schema;只记录已经拿到的真实资源名、类型和 ID,用于让 `check-prd-completeness` 做一次 app 资源列表 readback 后判断页面/资源数量是否完整。
|
|
@@ -198,5 +198,6 @@ openyida get-schema <appType> <formUuid> --field-map-json
|
|
|
198
198
|
| [data-bridge-guide.md](references/data-bridge-guide.md) | 表单、平台连接器与自定义同源接口的数据桥 | 接入真实数据时阅读 |
|
|
199
199
|
| [canvas-theme-provider.md](references/canvas-theme-provider.md) | 统一主题接入、示例装配、预览与发布 | 编写或调整 antd 页面时必读 |
|
|
200
200
|
| [canvas-style-implementation-guide.md](references/canvas-style-implementation-guide.md) | 将 `design.md` 的 App 主题色、antd token、背景层、卡片边界、圆角密度、控件焦点/下拉 reset、图表配色落到 `YidaCodeCanvas` 组件 | MUST:新建页面或调整视觉前完整读取,见顶部编码前必读 |
|
|
201
|
+
| [waf-safe-authoring.md](references/waf-safe-authoring.md) | 接口加白前规避平台安全(WAF)误拦的临时写法:不写正则字面量、危险 HTML/脚本串、注入特征串,及二分法定位 | 仅当用户反馈发布反复失败、疑似 WAF 拦截时阅读;常规生成不套用 |
|
|
201
202
|
| [component-library-guide.md](references/component-library-guide.md) | 组件库推荐组合和页面选型建议 | 选择 UI/图表依赖时阅读 |
|
|
202
203
|
| [canvas-authoring-examples.md](references/canvas-authoring-examples.md) | 最小组件、hooks、副作用、图表示例 | 手写 `.canvas.jsx` 代码时阅读 |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# WAF 安全误拦规避写法(临时方案)
|
|
2
|
+
|
|
3
|
+
> **仅在用户反馈发布反复失败、且疑似被平台安全(WAF)拦截时才启用本文档。**
|
|
4
|
+
> 这是接口尚未加白前的临时兜底方案,不是常规编码约束;正常生成 Canvas 页面时不必刻意套用,以免牺牲可读性。加白完成后本文档可下线。
|
|
5
|
+
|
|
6
|
+
## 何时判定为 WAF 拦截
|
|
7
|
+
|
|
8
|
+
发布返回 **HTTP 200,但响应体不是 JSON**,且 body 中含 `waf_block` 字样,即为安全拦截(并非业务报错)。表现为「发布一直失败」但登录态、参数、源码语法都正常。
|
|
9
|
+
|
|
10
|
+
## 定位命中片段
|
|
11
|
+
|
|
12
|
+
用二分法定位被拦截的源码片段:
|
|
13
|
+
|
|
14
|
+
1. 注释/删除页面源码的一半,重发 `openyida publish`。
|
|
15
|
+
2. 若仍被拦截,命中片段在保留的一半;若通过,命中片段在删掉的一半。
|
|
16
|
+
3. 继续对半缩小范围,直到定位到触发拦截的具体写法。
|
|
17
|
+
4. 按下方规则替换该写法后,恢复其余代码并重新发布。
|
|
18
|
+
|
|
19
|
+
## 规避写法
|
|
20
|
+
|
|
21
|
+
### 1. 不写正则字面量
|
|
22
|
+
|
|
23
|
+
日期/字符串解析改用非正则方式:`String.split()`、`slice()`、`Number()`、`Date` 等。
|
|
24
|
+
|
|
25
|
+
确需正则时用 `new RegExp('...')` 动态构造,避免形如 `\d{4}` 这类字面量直接出现在源码里。
|
|
26
|
+
|
|
27
|
+
- 命中示例(避免):直接在源码里写 `/\d{4}-\d{2}-\d{2}/` 之类的正则字面量。
|
|
28
|
+
- 安全写法:`new RegExp('\\d{4}')`,或直接用 `str.split('-')` 取段、`Number(x)`、`new Date(str)` 解析。
|
|
29
|
+
|
|
30
|
+
### 2. 不出现危险 HTML / 脚本字符串字面量
|
|
31
|
+
|
|
32
|
+
源码中避免出现下列字符串字面量:`<script`、``eval(` ``、`innerHTML`、`javascript:`、`onerror=`、`<iframe` 等。
|
|
33
|
+
|
|
34
|
+
事件处理写成 JSX 的 `onClick={fn}`,不要拼成 HTML 属性字符串(如把 `onclick="..."` 拼进字符串再注入)。
|
|
35
|
+
|
|
36
|
+
### 3. 不出现注入特征串
|
|
37
|
+
|
|
38
|
+
源码中不要出现 `union select`、`or 1=1`、`../`、`$(...)`、`/etc/passwd` 等注入特征串,即使只是常量、注释或示例数据。
|
|
39
|
+
|
|
40
|
+
## 复发时的处理顺序
|
|
41
|
+
|
|
42
|
+
1. 确认是 WAF 拦截(HTTP 200 + 非 JSON + `waf_block`),排除登录态/参数/语法问题。
|
|
43
|
+
2. 二分法定位命中片段。
|
|
44
|
+
3. 按上述规则替换写法。
|
|
45
|
+
4. 恢复完整源码,重新发布并回读验收。
|
|
@@ -172,6 +172,7 @@ body { background-color: #f2f3f5; }
|
|
|
172
172
|
| 发布后页面空白 | `YidaCodeCanvas` 页面检查 `YidaComp` 是否正确导出和依赖是否可加载;平台 JSX 组件页面检查 `renderJsx` 是否正确导出;同时查看浏览器控制台报错 |
|
|
173
173
|
| 发布接口成功但页面坏了 | 重新执行 `openyida publish <源文件路径> <appType> <formUuid> --health-check` 先确认远端 Schema 已读回且内容匹配;首屏渲染、控制台报错和体验问题仍结合浏览器验证 |
|
|
174
174
|
| 发布后功能异常 | `YidaCodeCanvas` 页面优先查依赖白名单、`YidaComp` 导出、hooks 副作用清理;平台 JSX 组件页面检查 `forceUpdate is not a function` 等常见错误,参考 `yida-custom-page` 平台 JSX 组件页面规范 |
|
|
175
|
+
| 用户反馈发布反复失败、疑似安全(WAF)拦截 | 仅当发布返回 HTTP 200 但响应体非 JSON 且 body 含 `waf_block` 时判定为拦截;按 `yida-canvas-custom-page` 的 [waf-safe-authoring.md](../yida-canvas-custom-page/references/waf-safe-authoring.md) 用二分法定位命中片段并替换写法后重发。非常规场景不启用 |
|
|
175
176
|
|
|
176
177
|
## Agent 错误处理策略
|
|
177
178
|
|