openyida 2026.9.11 → 2026.9.13
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 +7 -5
- package/bin/yida.js +1 -0
- package/lib/ai/ai.js +2 -0
- package/lib/app/canvas-compile.js +29 -26
- package/lib/app/compile.js +39 -1
- package/lib/app/services/canvas-page-compiler.js +3 -3
- package/lib/app/update-app.js +17 -2
- package/lib/asset/ai-image.js +37 -65
- package/lib/asset/asset-cmd.js +151 -110
- package/lib/asset/asset-plan.js +105 -0
- package/lib/asset/asset-resolve.js +343 -225
- package/lib/asset/asset-status.js +20 -31
- package/lib/asset/attachment-upload.js +29 -0
- package/lib/asset/host-capabilities.js +91 -0
- package/lib/asset/image-metadata.js +104 -0
- package/lib/asset/url-verify.js +90 -130
- package/lib/core/agent-capabilities.js +11 -0
- package/lib/core/command-manifest.js +25 -11
- package/lib/core/locales/en.js +6 -2
- package/lib/core/locales/zh.js +6 -2
- package/lib/design-plan/materialize.js +7 -9
- package/lib/design-plan/normalize.js +11 -1
- package/lib/process/services/process-actions.js +145 -0
- package/lib/process/services/process-compiler.js +4 -30
- package/lib/process/services/process-view-verifier.js +31 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/yida-skills/SKILL.md +3 -2
- package/yida-skills/skills/yida-app/SKILL.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +11 -1
- package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +21 -1
- package/yida-skills/skills/yida-app/workflow/step-5-seed-records.md +11 -7
- package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +4 -0
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -3
- package/yida-skills/skills/yida-canvas-custom-page/references/component-library-guide.md +1 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +3 -27
- package/yida-skills/skills/yida-canvas-table-form/SKILL.md +1 -1
- package/yida-skills/skills/yida-create-app/SKILL.md +4 -4
- package/yida-skills/skills/yida-create-process/SKILL.md +2 -0
- package/yida-skills/skills/yida-custom-page/references/assets-guide.md +9 -12
- package/yida-skills/skills/yida-design/SKILL.md +1 -0
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +4 -4
- package/yida-skills/skills/yida-design/references/asset-workflow.md +14 -53
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +21 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +1 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/validate_design_themes.py +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/README.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +1 -1
- package/yida-skills/skills/yida-design/workflow/output-design.md +4 -0
- package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
- package/yida-skills/skills/yida-design/workflow/step-5-visual-states.md +2 -1
- package/yida-skills/skills/yida-image-assets/SKILL.md +91 -0
- package/yida-skills/skills/yida-image-assets/references/manifest-contract.md +87 -0
- package/yida-skills/skills/yida-image-assets/references/source-policy.md +19 -0
- package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +1 -1
- package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
- package/yida-skills/skills/yida-process-rule/SKILL.md +6 -1
- package/yida-skills/skills/yida-process-rule/references/approval-actions.md +48 -0
- package/yida-skills/skills/yida-publish-page/SKILL.md +5 -4
- package/yida-skills/skills/yida-rechart/SKILL.md +1 -1
- package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +22 -13
- package/yida-skills/skills-index.json +18 -1
package/lib/asset/asset-cmd.js
CHANGED
|
@@ -1,82 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* asset-cmd.js - `openyida asset` 素材能力命令
|
|
4
|
-
*
|
|
5
|
-
* 素材工具的统一入口,把「检测素材能力 / 解析回填 / 素材来源引导」
|
|
6
|
-
* 暴露为可脚本化命令,供本地智能体在生成官网/落地页前调用,
|
|
7
|
-
* 从而做到:无 CDN 不谎称已上传、无可用素材不交付「最终版」。
|
|
8
|
-
*
|
|
9
|
-
* 子命令:
|
|
10
|
-
* status 检测当前素材能力(CDN / 生成 / 推荐策略)
|
|
11
|
-
* resolve --hero <..> [--product <..>] [--upload-assets] 解析并回填一组素材,输出 materialStatus/缺口
|
|
12
|
-
* generate 输出素材来源引导(免费素材库清单 + 纪律)
|
|
13
|
-
*
|
|
14
|
-
* 所有子命令均支持 --json 便于智能体解析。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
'use strict';
|
|
18
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readDesignAssetStrategy } = require('./asset-plan');
|
|
19
6
|
const { getAssetStatus } = require('./asset-status');
|
|
20
|
-
const { resolveAssets } = require('./asset-resolve');
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const { throwCommandError, throwStatus } = require('../core/command-errors');
|
|
7
|
+
const { resolveAssets, ASSET_SOURCES } = require('./asset-resolve');
|
|
8
|
+
const { detectImageGenerator, getMaterialSourcingGuidance } = require('./ai-image');
|
|
9
|
+
const { throwCommandError, throwStatus, throwUsage } = require('../core/command-errors');
|
|
10
|
+
|
|
11
|
+
const RESOLVE_USAGE = '用法: openyida asset resolve (--input <manifest-draft.json> | --slot <slotId>=<路径或URL> ...) [--manifest <asset-manifest.json>] [--design <design.md>] [--source search|user|generated] [--app-type <appType>] [--upload-assets] [--offline] [--json]';
|
|
26
12
|
|
|
27
13
|
function printHelp() {
|
|
28
14
|
console.log(`
|
|
29
15
|
用法:
|
|
30
16
|
openyida asset status [--offline] [--json]
|
|
31
|
-
openyida asset resolve --
|
|
32
|
-
openyida asset
|
|
17
|
+
openyida asset resolve (--input <manifest-draft.json> | --slot <slotId>=<路径或URL> ...) [--manifest <asset-manifest.json>] [--design <design.md>] [--source search|user|generated] [--app-type <appType>] [--upload-assets] [--offline] [--json]
|
|
18
|
+
openyida asset sources [--json]
|
|
33
19
|
|
|
34
20
|
子命令:
|
|
35
|
-
status
|
|
36
|
-
resolve
|
|
37
|
-
generate 输出素材来源引导(免费可商用素材库清单 + 使用纪律,供智能体检索真实图片)
|
|
21
|
+
status 检测素材能力
|
|
22
|
+
resolve 校验图片、默认上传宜搭图片附件并生成清单
|
|
38
23
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
图片不超过 20 MiB 时上传;超过限制的外链保留原地址。
|
|
25
|
+
--app-type 指定目标应用,省略时读取项目 config.json。
|
|
26
|
+
--upload-assets 仅兼容旧命令,默认已上传;--offline 不联网、不上传。
|
|
27
|
+
sources 输出 Unsplash/Pexels 使用规则
|
|
42
28
|
`);
|
|
43
29
|
}
|
|
44
30
|
|
|
31
|
+
function parseSlot(value) {
|
|
32
|
+
const index = String(value || '').indexOf('=');
|
|
33
|
+
if (index <= 0 || index === String(value).length - 1) {return null;}
|
|
34
|
+
const slotId = String(value).slice(0, index).trim();
|
|
35
|
+
const input = String(value).slice(index + 1).trim();
|
|
36
|
+
if (!slotId || !input) {return null;}
|
|
37
|
+
return {
|
|
38
|
+
slotId,
|
|
39
|
+
input,
|
|
40
|
+
required: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
45
44
|
function parseArgs(args) {
|
|
46
45
|
const parsed = {
|
|
47
46
|
subCommand: args[0],
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
slots: [],
|
|
48
|
+
input: '',
|
|
49
|
+
manifest: '',
|
|
50
|
+
design: '',
|
|
51
51
|
offline: false,
|
|
52
52
|
uploadAssets: false,
|
|
53
|
+
appType: '',
|
|
54
|
+
source: 'search',
|
|
53
55
|
timeout: 0,
|
|
54
56
|
json: false,
|
|
55
57
|
help: false,
|
|
58
|
+
errors: [],
|
|
59
|
+
usedOptions: [],
|
|
56
60
|
};
|
|
57
|
-
|
|
58
61
|
for (let i = 1; i < args.length; i++) {
|
|
59
62
|
const arg = args[i];
|
|
60
63
|
if (arg === '--help' || arg === '-h') {
|
|
61
64
|
parsed.help = true;
|
|
62
65
|
} else if (arg === '--json') {
|
|
66
|
+
parsed.usedOptions.push('--json');
|
|
63
67
|
parsed.json = true;
|
|
64
68
|
} else if (arg === '--offline') {
|
|
69
|
+
parsed.usedOptions.push('--offline');
|
|
65
70
|
parsed.offline = true;
|
|
66
|
-
} else if (arg === '--
|
|
67
|
-
parsed.
|
|
68
|
-
|
|
71
|
+
} else if (arg === '--app-type') {
|
|
72
|
+
parsed.usedOptions.push('--app-type');
|
|
73
|
+
parsed.appType = args[++i] || '';
|
|
74
|
+
if (!parsed.appType || parsed.appType.startsWith('--')) {parsed.errors.push('ASSET_APP_TYPE_REQUIRED');}
|
|
75
|
+
} else if (arg === '--upload-assets') {
|
|
76
|
+
parsed.usedOptions.push('--upload-assets');
|
|
69
77
|
parsed.uploadAssets = true;
|
|
70
|
-
} else if (arg === '--
|
|
71
|
-
parsed.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
} else if (arg === '--source') {
|
|
79
|
+
parsed.usedOptions.push('--source');
|
|
80
|
+
parsed.source = args[++i] || '';
|
|
81
|
+
} else if (arg === '--input') {
|
|
82
|
+
parsed.usedOptions.push('--input');
|
|
83
|
+
parsed.input = args[++i] || '';
|
|
84
|
+
} else if (arg === '--design') {
|
|
85
|
+
parsed.usedOptions.push('--design');
|
|
86
|
+
parsed.design = args[++i] || '';
|
|
87
|
+
if (!parsed.design || parsed.design.startsWith('--')) {parsed.errors.push('ASSET_DESIGN_REQUIRED');}
|
|
88
|
+
} else if (arg === '--manifest') {
|
|
89
|
+
parsed.usedOptions.push('--manifest');
|
|
90
|
+
parsed.manifest = args[++i] || '';
|
|
91
|
+
} else if (arg === '--slot') {
|
|
92
|
+
parsed.usedOptions.push('--slot');
|
|
93
|
+
const slot = parseSlot(args[++i]);
|
|
94
|
+
if (slot) {parsed.slots.push(slot);} else {parsed.errors.push('--slot 格式应为 <slotId>=<路径或URL>');}
|
|
75
95
|
} else if (arg === '--timeout') {
|
|
96
|
+
parsed.usedOptions.push('--timeout');
|
|
76
97
|
parsed.timeout = parseInt(args[++i], 10) || 0;
|
|
98
|
+
} else {
|
|
99
|
+
parsed.errors.push(`未知参数:${arg}`);
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
|
-
|
|
80
102
|
return parsed;
|
|
81
103
|
}
|
|
82
104
|
|
|
@@ -87,74 +109,88 @@ function runStatus(options) {
|
|
|
87
109
|
return;
|
|
88
110
|
}
|
|
89
111
|
console.log('素材能力检测');
|
|
90
|
-
console.log('
|
|
91
|
-
console.log('
|
|
92
|
-
console.log('
|
|
93
|
-
console.log('
|
|
94
|
-
console.log('
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
console.log(' 上传目标 : 宜搭图片附件(需要登录态和 appType)');
|
|
113
|
+
console.log(' 上传条件 : ' + (status.canUpload ? '联网已启用;实际上传时验证登录态和应用权限' : '离线,不上传'));
|
|
114
|
+
console.log(' 可生成图片 : ' + (status.canGenerate === true ? '是' : status.canGenerate === false ? '否' : '未确认'));
|
|
115
|
+
console.log(' 建议策略 : ' + status.recommendedStrategy);
|
|
116
|
+
status.reasons.forEach(reason => console.log(' - ' + reason));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readDraft(filePath) {
|
|
120
|
+
let draft;
|
|
121
|
+
try {
|
|
122
|
+
draft = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throwCommandError(`无法读取素材草稿:${error.message}`, { code: 'ASSET_INPUT_INVALID' });
|
|
97
125
|
}
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
if (!draft || !Array.isArray(draft.assets)) {
|
|
127
|
+
throwCommandError('素材草稿必须包含 assets 数组', { code: 'ASSET_INPUT_INVALID' });
|
|
128
|
+
}
|
|
129
|
+
return draft;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeManifest(filePath, manifest) {
|
|
133
|
+
const target = path.resolve(filePath);
|
|
134
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
135
|
+
fs.writeFileSync(target, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
136
|
+
return target;
|
|
100
137
|
}
|
|
101
138
|
|
|
102
139
|
async function runResolve(options) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
if (options.errors.length > 0) {
|
|
141
|
+
throwUsage(options.errors.join('\n'), RESOLVE_USAGE, { code: 'ASSET_INVALID_ARGUMENTS' });
|
|
142
|
+
}
|
|
143
|
+
if (options.input && options.slots.length > 0) {
|
|
144
|
+
throwUsage('--input 与 --slot 不能同时使用', RESOLVE_USAGE, { code: 'ASSET_INPUT_CONFLICT' });
|
|
145
|
+
}
|
|
146
|
+
if (!ASSET_SOURCES.includes(options.source)) {
|
|
147
|
+
throwUsage('--source 仅支持 search、user 或 generated', RESOLVE_USAGE, { code: 'ASSET_INVALID_SOURCE' });
|
|
148
|
+
}
|
|
149
|
+
const draft = options.input ? readDraft(options.input) : {};
|
|
150
|
+
const assetStrategy = options.design ? readDesignAssetStrategy(path.resolve(options.design)) : draft.assetStrategy;
|
|
151
|
+
const assets = [
|
|
152
|
+
...(draft.assets || []),
|
|
153
|
+
...options.slots.map(slot => ({ ...slot, source: options.source })),
|
|
154
|
+
];
|
|
155
|
+
if (assets.length === 0 && !options.input) {
|
|
156
|
+
throwUsage('至少提供一个 --slot 或 --input', RESOLVE_USAGE, { code: 'ASSET_INPUT_REQUIRED' });
|
|
157
|
+
}
|
|
158
|
+
|
|
107
159
|
const result = await resolveAssets(assets, {
|
|
108
|
-
|
|
160
|
+
assetStrategy,
|
|
109
161
|
online: !options.offline,
|
|
110
|
-
|
|
162
|
+
appType: options.appType,
|
|
163
|
+
assetSource: options.source,
|
|
111
164
|
timeout: options.timeout || undefined,
|
|
112
165
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
console.log('素材解析结果');
|
|
118
|
-
console.log(' materialStatus : ' + result.materialStatus +
|
|
119
|
-
(result.materialStatus === 'final' ? '(可交付最终版)'
|
|
120
|
-
: result.materialStatus === 'draft' ? '(仅可交付标注缺口的草稿)'
|
|
121
|
-
: '(无可用素材,只能低保真草稿)'));
|
|
122
|
-
console.log(' strategy : ' + result.strategy);
|
|
123
|
-
console.log(' Hero : ' + (result.assets.heroImage || '(无)'));
|
|
124
|
-
console.log(' 产品图 : ' + (result.assets.productImages.length
|
|
125
|
-
? result.assets.productImages.join(', ') : '(无)'));
|
|
126
|
-
if (result.materialGaps.length) {
|
|
127
|
-
console.log('素材缺口:');
|
|
128
|
-
result.materialGaps.forEach((g) => console.log(' - ' + g));
|
|
129
|
-
}
|
|
130
|
-
// 非 final 时以非零退出码提示调用方(生成链路可据此拦截「最终版」)
|
|
131
|
-
if (result.materialStatus !== 'final') {
|
|
166
|
+
const manifestPath = options.manifest ? writeManifest(options.manifest, result) : '';
|
|
167
|
+
if (result.materialStatus === 'draft') {
|
|
132
168
|
throwStatus('素材未达到 final 状态', 2, {
|
|
133
169
|
code: 'ASSET_MATERIAL_NOT_FINAL',
|
|
134
|
-
details: result,
|
|
170
|
+
details: { ...result, manifestPath },
|
|
135
171
|
});
|
|
136
172
|
}
|
|
173
|
+
if (options.json) {
|
|
174
|
+
console.log(JSON.stringify({ ...result, manifestPath }, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log('素材状态: ' + result.materialStatus);
|
|
178
|
+
console.log('素材数量: ' + result.assets.length);
|
|
179
|
+
if (manifestPath) {console.log('素材清单: ' + manifestPath);}
|
|
137
180
|
}
|
|
138
181
|
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
const
|
|
182
|
+
function runSources(options) {
|
|
183
|
+
const status = getAssetStatus();
|
|
184
|
+
const output = {
|
|
185
|
+
generator: detectImageGenerator({ hostCapabilities: status.hostCapabilities }),
|
|
186
|
+
guidance: getMaterialSourcingGuidance(),
|
|
187
|
+
};
|
|
142
188
|
if (options.json) {
|
|
143
|
-
console.log(JSON.stringify(
|
|
189
|
+
console.log(JSON.stringify(output, null, 2));
|
|
144
190
|
return;
|
|
145
191
|
}
|
|
146
|
-
console.log('
|
|
147
|
-
console.log(' ' +
|
|
148
|
-
console.log('\n素材来源步骤:');
|
|
149
|
-
guidance.steps.forEach((s) => console.log(' ' + s));
|
|
150
|
-
console.log('\n免费可商用素材库:');
|
|
151
|
-
guidance.libraries.forEach((lib) => {
|
|
152
|
-
console.log(' · ' + lib.name + ' (' + lib.site + ')');
|
|
153
|
-
console.log(' 授权: ' + lib.license);
|
|
154
|
-
console.log(' 适合: ' + lib.bestFor);
|
|
155
|
-
});
|
|
156
|
-
console.log('\n纪律:');
|
|
157
|
-
guidance.rules.forEach((r) => console.log(' - ' + r));
|
|
192
|
+
console.log('图片来源: 用户素材、Unsplash、Pexels 或宿主生成');
|
|
193
|
+
output.guidance.rules.forEach(rule => console.log(' - ' + rule));
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
async function run(args) {
|
|
@@ -163,22 +199,27 @@ async function run(args) {
|
|
|
163
199
|
printHelp();
|
|
164
200
|
return;
|
|
165
201
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
default:
|
|
177
|
-
printHelp();
|
|
178
|
-
throwCommandError('未知子命令: ' + options.subCommand, {
|
|
179
|
-
code: 'ASSET_UNKNOWN_SUBCOMMAND',
|
|
180
|
-
});
|
|
202
|
+
const allowedOptions = {
|
|
203
|
+
status: new Set(['--offline', '--json']),
|
|
204
|
+
sources: new Set(['--json']),
|
|
205
|
+
};
|
|
206
|
+
if (allowedOptions[options.subCommand]) {
|
|
207
|
+
const invalid = options.usedOptions.filter(option => !allowedOptions[options.subCommand].has(option));
|
|
208
|
+
const errors = [...options.errors, ...invalid.map(option => `参数不适用于 ${options.subCommand}:${option}`)];
|
|
209
|
+
if (errors.length > 0) {
|
|
210
|
+
throwCommandError(errors.join('\n'), { code: 'ASSET_INVALID_ARGUMENTS' });
|
|
211
|
+
}
|
|
181
212
|
}
|
|
213
|
+
if (options.subCommand === 'status') {runStatus(options); return;}
|
|
214
|
+
if (options.subCommand === 'resolve') {await runResolve(options); return;}
|
|
215
|
+
if (options.subCommand === 'sources') {runSources(options); return;}
|
|
216
|
+
throwCommandError(`未知子命令:${options.subCommand}`, { code: 'ASSET_UNKNOWN_SUBCOMMAND' });
|
|
182
217
|
}
|
|
183
218
|
|
|
184
|
-
module.exports = {
|
|
219
|
+
module.exports = {
|
|
220
|
+
run,
|
|
221
|
+
parseArgs,
|
|
222
|
+
parseSlot,
|
|
223
|
+
readDraft,
|
|
224
|
+
writeManifest,
|
|
225
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { CliError } = require('../core/cli-error');
|
|
5
|
+
|
|
6
|
+
function invalid(code, details = {}) {
|
|
7
|
+
throw new CliError(code, { code, details });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function object(value) {
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function nonempty(value) {
|
|
15
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function minimumSize(asset) {
|
|
19
|
+
const match = typeof asset.minSize === 'string' && asset.minSize.match(/^(\d+)x(\d+)$/i);
|
|
20
|
+
if (asset.minSize !== undefined && !match) {
|
|
21
|
+
invalid('ASSET_INVALID_SIZE', { slotId: asset.slotId, minSize: asset.minSize });
|
|
22
|
+
}
|
|
23
|
+
const values = [asset.minWidth ?? 0, asset.minHeight ?? 0];
|
|
24
|
+
const declared = match ? [Number(match[1]), Number(match[2])] : [0, 0];
|
|
25
|
+
if ([...values, ...declared].some(value => !Number.isSafeInteger(value) || value < 0) || (match && declared.some(value => !value))) {
|
|
26
|
+
invalid('ASSET_INVALID_SIZE', { slotId: asset.slotId });
|
|
27
|
+
}
|
|
28
|
+
return { minWidth: Math.max(values[0], declared[0]), minHeight: Math.max(values[1], declared[1]) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readDesignAssetStrategy(file) {
|
|
32
|
+
let markdown;
|
|
33
|
+
try { markdown = fs.readFileSync(file, 'utf8'); } catch (error) {
|
|
34
|
+
invalid('ASSET_DESIGN_INVALID', { file, reason: error.message });
|
|
35
|
+
}
|
|
36
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(markdown);
|
|
37
|
+
const lines = frontmatter ? frontmatter[1].split(/\r?\n/).filter(line => /^assetStrategy:/.test(line)) : [];
|
|
38
|
+
if (lines.length !== 1) {invalid('ASSET_DESIGN_INVALID', { file, field: 'assetStrategy', format: 'single-line JSON in frontmatter' });}
|
|
39
|
+
try { return JSON.parse(lines[0].slice('assetStrategy:'.length).trim()); } catch {
|
|
40
|
+
invalid('ASSET_DESIGN_INVALID', { file, field: 'assetStrategy', format: 'single-line JSON in frontmatter' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The design owns page membership, required slots and minimum dimensions.
|
|
45
|
+
function prepareAssetPlan(assets, assetStrategy) {
|
|
46
|
+
if (!Array.isArray(assets)) {invalid('ASSET_INPUT_INVALID', { field: 'assets' });}
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const entries = assets.map(asset => {
|
|
49
|
+
if (!object(asset) || !nonempty(asset.slotId)) {invalid('ASSET_INPUT_INVALID', { field: 'assets[].slotId' });}
|
|
50
|
+
const slotId = asset.slotId.trim();
|
|
51
|
+
if (seen.has(slotId)) {invalid('ASSET_DUPLICATE_SLOT', { slotId });}
|
|
52
|
+
seen.add(slotId);
|
|
53
|
+
minimumSize(asset);
|
|
54
|
+
return { ...asset, slotId };
|
|
55
|
+
});
|
|
56
|
+
if (assetStrategy === undefined) {return { entries, pages: [], assetStrategy: undefined };}
|
|
57
|
+
if (!object(assetStrategy) || !Array.isArray(assetStrategy.pages)) {
|
|
58
|
+
invalid('ASSET_DESIGN_INVALID', { field: 'assetStrategy.pages' });
|
|
59
|
+
}
|
|
60
|
+
const pageIds = new Set();
|
|
61
|
+
const slots = new Map();
|
|
62
|
+
const pages = assetStrategy.pages.map(page => {
|
|
63
|
+
if (!object(page) || !nonempty(page.pageId) || !['required', 'beneficial', 'none'].includes(page.imageNeed) || !Array.isArray(page.slots)) {
|
|
64
|
+
invalid('ASSET_DESIGN_INVALID', { field: 'assetStrategy.pages[]' });
|
|
65
|
+
}
|
|
66
|
+
const pageId = page.pageId.trim();
|
|
67
|
+
if (pageIds.has(pageId)) {invalid('ASSET_DUPLICATE_PAGE', { pageId });}
|
|
68
|
+
pageIds.add(pageId);
|
|
69
|
+
if (page.imageNeed === 'none' && page.slots.length) {invalid('ASSET_DESIGN_INVALID', { pageId, field: 'slots' });}
|
|
70
|
+
const slotIds = [];
|
|
71
|
+
page.slots.forEach(slot => {
|
|
72
|
+
if (!object(slot) || !nonempty(slot.slotId) || !nonempty(slot.usage)) {
|
|
73
|
+
invalid('ASSET_DESIGN_INVALID', { pageId, field: 'slots[].slotId/usage' });
|
|
74
|
+
}
|
|
75
|
+
const count = slot.count ?? 1;
|
|
76
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {invalid('ASSET_DESIGN_INVALID', { pageId, slotId: slot.slotId, field: 'count' });}
|
|
77
|
+
minimumSize(slot);
|
|
78
|
+
for (let index = 0; index < count; index++) {
|
|
79
|
+
const slotId = count === 1 ? slot.slotId.trim() : `${slot.slotId.trim()}[${index}]`;
|
|
80
|
+
if (slots.has(slotId)) {invalid('ASSET_DUPLICATE_SLOT', { slotId });}
|
|
81
|
+
slots.set(slotId, { ...slot, slotId, pageId, required: slot.required !== false });
|
|
82
|
+
slotIds.push(slotId);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return { pageId, imageNeed: page.imageNeed, slotIds };
|
|
86
|
+
});
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!slots.has(entry.slotId)) {invalid('ASSET_UNDECLARED_SLOT', { slotId: entry.slotId });}
|
|
89
|
+
}
|
|
90
|
+
const byId = new Map(entries.map(entry => [entry.slotId, entry]));
|
|
91
|
+
const planned = [...slots.values()].map(slot => {
|
|
92
|
+
const entry = byId.get(slot.slotId) || {};
|
|
93
|
+
const designSize = minimumSize(slot);
|
|
94
|
+
const inputSize = minimumSize(entry);
|
|
95
|
+
return {
|
|
96
|
+
...entry,
|
|
97
|
+
...slot,
|
|
98
|
+
minWidth: Math.max(designSize.minWidth, inputSize.minWidth),
|
|
99
|
+
minHeight: Math.max(designSize.minHeight, inputSize.minHeight),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
return { entries: planned, pages, assetStrategy };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { minimumSize, prepareAssetPlan, readDesignAssetStrategy };
|