openyida 2026.9.14 → 2026.9.16

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.
Files changed (115) hide show
  1. package/README.md +8 -3
  2. package/lib/app/canvas-compile.js +2 -0
  3. package/lib/app/canvas-theme-guard.js +117 -0
  4. package/lib/app/create-form/batch.js +54 -6
  5. package/lib/app/create-form.js +93 -15
  6. package/lib/app/services/form-compiler.js +4 -4
  7. package/lib/app/theme-from-design.js +8 -2
  8. package/lib/app/theme-presets.js +1 -1
  9. package/lib/asset/ai-image.js +40 -6
  10. package/lib/asset/asset-cmd.js +8 -2
  11. package/lib/asset/asset-plan.js +46 -4
  12. package/lib/asset/asset-resolve.js +7 -3
  13. package/lib/core/agent-capabilities.js +16 -8
  14. package/lib/core/command-manifest.js +113 -16
  15. package/lib/core/locales/en.js +10 -2
  16. package/lib/core/locales/zh.js +10 -2
  17. package/lib/core/redact.js +15 -10
  18. package/lib/core/sample.js +6 -10
  19. package/lib/design-plan/design-plan.js +7 -2
  20. package/lib/design-plan/entry-navigation.js +83 -0
  21. package/lib/design-plan/init.js +73 -10
  22. package/lib/design-plan/materialize.js +103 -67
  23. package/lib/design-plan/normalize.js +23 -1
  24. package/lib/design-plan/patch.js +24 -4
  25. package/lib/design-plan/validate.js +30 -5
  26. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +14 -85
  27. package/lib/samples/openyida-scaffold/canvas-nav/data.jsx +84 -17
  28. package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +9 -83
  29. package/lib/samples/yida-rechart/trend-combo.canvas.jsx +14 -87
  30. package/package.json +2 -1
  31. package/scripts/postinstall.js +3 -3
  32. package/yida-skills/SKILL.md +3 -3
  33. package/yida-skills/skills/yida-app/SKILL.md +18 -12
  34. package/yida-skills/skills/yida-app/references/entry-navigation.md +150 -0
  35. package/yida-skills/skills/yida-app/workflow/parallel-work.md +10 -1
  36. package/yida-skills/skills/yida-app/workflow/plan/step-1-understand.md +1 -1
  37. package/yida-skills/skills/yida-app/workflow/plan/step-2-confirm.md +1 -1
  38. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +6 -6
  39. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +10 -9
  40. package/yida-skills/skills/yida-app/workflow/step-1-resource-context.md +4 -2
  41. package/yida-skills/skills/yida-app/workflow/step-2-design.md +15 -5
  42. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +4 -2
  43. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +14 -3
  44. package/yida-skills/skills/yida-app/workflow/step-6-main-page.md +6 -4
  45. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +6 -4
  46. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +13 -4
  47. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +29 -19
  48. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +32 -42
  49. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +5 -5
  50. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +45 -50
  51. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +2 -0
  52. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +13 -12
  53. package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +19 -2
  54. package/yida-skills/skills/yida-create-form-page/SKILL.md +36 -14
  55. package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +4 -3
  56. package/yida-skills/skills/yida-design/SKILL.md +6 -2
  57. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +42 -55
  58. package/yida-skills/skills/yida-design/references/asset-workflow.md +4 -2
  59. package/yida-skills/skills/yida-design/references/design-mode.md +3 -3
  60. package/yida-skills/skills/yida-design/references/navigation-decision.md +42 -0
  61. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +2 -2
  62. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +4 -4
  63. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +16 -11
  64. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +24 -150
  65. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-design.md +6 -2
  66. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +17 -13
  67. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +25 -0
  68. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/validate_design_themes.py +29 -0
  69. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/README.md +13 -1
  70. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +86 -87
  71. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +84 -102
  72. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +73 -97
  73. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +91 -95
  74. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/basic-tokens.json +101 -0
  75. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +82 -110
  76. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +86 -94
  77. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +91 -98
  78. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +88 -99
  79. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +84 -109
  80. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +83 -88
  81. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +83 -91
  82. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +87 -85
  83. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +88 -95
  84. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +90 -101
  85. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +87 -99
  86. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +85 -91
  87. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +84 -92
  88. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +85 -87
  89. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +82 -85
  90. package/yida-skills/skills/yida-design/workflow/output-design.md +1 -1
  91. package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +1 -1
  92. package/yida-skills/skills/yida-image-assets/SKILL.md +48 -51
  93. package/yida-skills/skills/yida-image-assets/references/manifest-contract.md +50 -54
  94. package/yida-skills/skills/yida-image-assets/references/source-policy.md +14 -15
  95. package/yida-skills/skills/yida-nav-group/SKILL.md +4 -2
  96. package/yida-skills/skills/yida-nav-shell/SKILL.md +23 -6
  97. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +6 -6
  98. package/yida-skills/skills/yida-page-config/SKILL.md +3 -3
  99. package/yida-skills/skills/yida-prd/SKILL.md +2 -1
  100. package/yida-skills/skills/yida-prd/references/app/blueprint.md +1 -1
  101. package/yida-skills/skills/yida-prd/references/app/navigation-patterns.md +6 -3
  102. package/yida-skills/skills/yida-prd/references/plan-content.md +4 -0
  103. package/yida-skills/skills/yida-prd/workflow/output-prd.md +13 -7
  104. package/yida-skills/skills/yida-prd/workflow/plan-business.md +3 -3
  105. package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +7 -5
  106. package/yida-skills/skills/yida-publish-page/SKILL.md +1 -0
  107. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +10 -39
  108. package/yida-skills/skills/yida-requirement-analysis/references/experience-groups.md +23 -0
  109. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +64 -0
  110. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +32 -73
  111. package/yida-skills/skills-index.json +6 -6
  112. package/yida-skills/skills/yida-design/references/app/blueprint.md +0 -70
  113. package/yida-skills/skills/yida-design/references/app/navigation-patterns.md +0 -43
  114. package/yida-skills/skills/yida-design/references/app/role-journey.md +0 -30
  115. package/yida-skills/skills/yida-design/workflow/output-prd.md +0 -185
package/README.md CHANGED
@@ -396,11 +396,12 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
396
396
  | `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
397
397
  | `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
398
398
  | `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
399
+ | `openyida design-plan catalog [--json]` | List available planning themes and page patterns |
399
400
  | `openyida design-plan init <requirement-brief.json> [--theme-id <id>] [--output-dir <dir>] [--json]` | Initialize a plan draft from confirmed requirements |
400
401
  | `openyida design-plan preview <build-plan.json> --part-file <module.json> [--json]` | Update plan drafts by module |
401
402
  | `openyida design-plan materialize <build-plan.json> [--from-preview \| --business-file <json> --visual-file <json>] [--output-dir <dir>] [--check] [--json]` | Generate and validate design-plan artifacts from build-plan.json |
402
- | `openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]` | Patch a design plan by field path and invalidate prior confirmation |
403
- | `openyida update-app <appType> [--name "..."] [--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 |
403
+ | `openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]` | Patch a design plan; asset progress preserves existing approval |
404
+ | `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 |
404
405
  | `openyida app-online <appType> [--to-ding-app-center] [--show-app-center]` | Enable a Yida app |
405
406
  | `openyida app-offline <appType> [--to-ding-app-center] [--show-app-center]` | Disable a Yida app |
406
407
  | `openyida nav-group <list\|create\|rename\|delete\|move\|order\|auto-order\|hide\|show> <appType> ...` | Manage app sidebar navigation groups |
@@ -649,10 +650,14 @@ npx clawhub@latest install nicky1108/yida-app
649
650
  ```bash
650
651
  git clone https://github.com/openyida/openyida.git
651
652
  cd openyida
652
- npm install
653
+ nvm install
654
+ nvm use
655
+ npm ci --ignore-scripts
653
656
  npm run check:ci
654
657
  ```
655
658
 
659
+ Use the Node.js version in `.nvmrc` to match CI validation. If you do not use nvm, install that version directly.
660
+
656
661
  Useful checks:
657
662
 
658
663
  | Command | Purpose |
@@ -50,6 +50,7 @@ const {
50
50
  } = require('./form-detail-link-guard');
51
51
  const { t } = require('../core/i18n');
52
52
  const { assertInlineCssStructure } = require('./inline-css-guard');
53
+ const { assertCanvasThemeStructure } = require('./canvas-theme-guard');
53
54
 
54
55
  /**
55
56
  * 依赖 → window 别名白名单,用于把 import 改写到运行时全局对象。
@@ -967,6 +968,7 @@ function compileCanvasLocal(source, options = {}) {
967
968
  assertNoBareDependencyGlobals(source, options);
968
969
  assertNoManualDependencyGlobals(source, options);
969
970
  assertInlineCssStructure(source, options);
971
+ assertCanvasThemeStructure(source, options);
970
972
 
971
973
  const importedModules = extractImportedModules(source);
972
974
 
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const Babel = require('@babel/standalone');
4
+ const { CliError } = require('../core/cli-error');
5
+ const { t } = require('../core/i18n');
6
+
7
+ // Check the maintained theme contract without executing business code.
8
+ // Dynamic component factories and runtime branches still need browser verification.
9
+ function assertCanvasThemeStructure(source, options = {}) {
10
+ if (!/CanvasThemeProvider|useCanvasThemeContext|CanvasThemeContext/.test(source)) { return; }
11
+ let ast;
12
+ try {
13
+ ast = Babel.packages.parser.parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
14
+ } catch { return; } // Let the compiler report syntax errors.
15
+ const traverse = Babel.packages.traverse.default || Babel.packages.traverse;
16
+ const functions = new Map();
17
+ let program;
18
+ let defaultExport;
19
+ const fail = (kind, node) => {
20
+ const line = node?.loc?.start.line || 1;
21
+ throw new CliError(t(`publish.canvas_theme_${kind}`, line), {
22
+ code: 'OPENYIDA_CANVAS_THEME_PROVIDER_INVALID',
23
+ details: { stage: 'canvas_compile', sourcePath: options.sourcePath || '', line, issueType: kind },
24
+ });
25
+ };
26
+
27
+ traverse(ast, {
28
+ Program(p) { program = p; },
29
+ ExportDefaultDeclaration(p) { defaultExport = p.get('declaration'); },
30
+ Function(p) {
31
+ const name = p.node.id?.name || (p.parentPath.isVariableDeclarator() ? p.parentPath.node.id.name : '');
32
+ functions.set(p.node, { name, calls: [], renders: [], hooks: [] });
33
+ },
34
+ VariableDeclarator(p) {
35
+ if (p.node.id.name === 'CanvasThemeContext' && p.getFunctionParent()) {
36
+ fail('context_scope', p.node);
37
+ }
38
+ },
39
+ });
40
+
41
+ function resolveFunction(p, seen = new Set()) {
42
+ if (!p?.node || seen.has(p.node)) { return null; }
43
+ seen.add(p.node);
44
+ if (p.isFunction()) { return p.node; }
45
+ if (p.isVariableDeclarator()) { return resolveFunction(p.get('init'), seen); }
46
+ if (p.isIdentifier() || p.isJSXIdentifier()) {
47
+ return resolveFunction(p.scope.getBinding(p.node.name)?.path, seen);
48
+ }
49
+ // React.memo/forwardRef retain the wrapped component's theme requirements.
50
+ if (p.isCallExpression()) {
51
+ const callee = p.node.callee;
52
+ const name = callee.type === 'Identifier' ? callee.name : callee.property?.name;
53
+ if (['memo', 'forwardRef'].includes(name)) { return resolveFunction(p.get('arguments.0'), seen); }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ traverse(ast, {
59
+ CallExpression(p) {
60
+ const owner = functions.get(p.getFunctionParent()?.node);
61
+ const target = resolveFunction(p.get('callee'));
62
+ const targetName = functions.get(target)?.name || p.node.callee.name;
63
+ if (targetName === 'useCanvasThemeContext') {
64
+ if (!owner) { fail('root_hook', p.node); }
65
+ owner.hooks.push(p.node);
66
+ }
67
+ if (!owner) { return; }
68
+ if (target) { owner.calls.push(target); }
69
+ const callee = p.node.callee;
70
+ if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property.name === 'createElement') {
71
+ const component = resolveFunction(p.get('arguments.0'));
72
+ if (component) { owner.renders.push(component); }
73
+ }
74
+ },
75
+ JSXOpeningElement(p) {
76
+ const owner = functions.get(p.getFunctionParent()?.node);
77
+ const component = resolveFunction(p.get('name'));
78
+ if (owner && component) { owner.renders.push(component); }
79
+ },
80
+ });
81
+
82
+ const entry = resolveFunction(program.scope.getBinding('YidaComp')?.path) || resolveFunction(defaultExport);
83
+ if (!entry) { return; }
84
+
85
+ // Calling a hook before returning a Provider cannot read that Provider.
86
+ const directSeen = new Set();
87
+ function checkEntryCalls(node) {
88
+ if (directSeen.has(node)) { return; }
89
+ directSeen.add(node);
90
+ const info = functions.get(node);
91
+ if (info.hooks.length) { fail('root_hook', info.hooks[0]); }
92
+ info.calls.forEach(checkEntryCalls);
93
+ }
94
+ checkEntryCalls(entry);
95
+
96
+ const seen = new Set();
97
+ let providerMounted = false;
98
+ let firstHook;
99
+ function visit(node) {
100
+ if (seen.has(node)) { return; }
101
+ seen.add(node);
102
+ const info = functions.get(node);
103
+ firstHook = firstHook || info.hooks[0];
104
+ info.renders.forEach(child => {
105
+ if (functions.get(child).name === 'CanvasThemeProvider') { providerMounted = true; }
106
+ visit(child);
107
+ });
108
+ info.calls.forEach(visit);
109
+ }
110
+ visit(entry);
111
+ const hasProvider = [...functions.values()].some(info => info.name === 'CanvasThemeProvider');
112
+ if (!providerMounted && (hasProvider || firstHook)) {
113
+ fail('provider_missing', firstHook || entry);
114
+ }
115
+ }
116
+
117
+ module.exports = { assertCanvasThemeStructure };
@@ -225,6 +225,25 @@ function readbackMismatch(formUuid, form, schema) {
225
225
  });
226
226
  }
227
227
 
228
+ function buildPartialFailureRecovery(results) {
229
+ const items = Object.values(results || {});
230
+ const hasUnknownWrite = items.some(item =>
231
+ item && (item.status === 'running' || (item.status === 'failed' && !item.formUuid))
232
+ );
233
+ const recoveryAction = hasUnknownWrite
234
+ ? 'inspect_unknown_write_then_reconcile'
235
+ : 'rerun_unchanged_plan';
236
+ return { recoveryAction, nextAction: recoveryAction };
237
+ }
238
+
239
+ function buildDeliveryUrls(baseUrl, appType, formUuid) {
240
+ const normalizedBaseUrl = String(baseUrl || '').replace(/\/+$/, '');
241
+ if (!normalizedBaseUrl || !appType || !formUuid) { return {}; }
242
+ const appUrl = `${normalizedBaseUrl}/${appType}/workbench`;
243
+ const formUrl = `${appUrl}/${formUuid}`;
244
+ return { url: formUrl, formUrl, appUrl };
245
+ }
246
+
228
247
  function execute(args, { execFile: execFileImpl = execFile } = {}) {
229
248
  return new Promise((resolve, reject) => {
230
249
  execFileImpl(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
@@ -317,13 +336,15 @@ async function run(args, dependencies = {}) {
317
336
  try {
318
337
  const state = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { fingerprint, appType: options.appType, results: {} };
319
338
  if (state.fingerprint !== fingerprint) { invalid('state belongs to a different plan; reconcile existing resources before preparing a new batch'); }
320
- await call(['login', '--check-only', '--json']);
339
+ const loginStatus = await call(['login', '--check-only', '--json']);
340
+ const baseUrl = loginStatus?.base_url || loginStatus?.baseUrl || '';
321
341
  // Read back completed resources before their IDs can be used by dependent forms.
322
342
  for (const form of forms.filter(item => state.results[item.key]?.status === 'success')) {
323
343
  const item = state.results[form.key];
324
344
  const schema = await call(['get-schema', options.appType, item.formUuid, '--field-map-json']);
325
345
  if (schema.formUuid !== item.formUuid || !Array.isArray(schema.fields)) { invalid(`schema: ${form.key}`); }
326
346
  item.fields = schema.fields;
347
+ Object.assign(item, buildDeliveryUrls(baseUrl, options.appType, item.formUuid));
327
348
  }
328
349
  const resolve = (key, field) => {
329
350
  const item = state.results[key];
@@ -336,6 +357,7 @@ async function run(args, dependencies = {}) {
336
357
  await schedule(forms, options.concurrency, state.results, async form => {
337
358
  let formUuid = form.formUuid || state.results[form.key]?.formUuid;
338
359
  let shouldResume = Boolean(!form.formUuid && formUuid);
360
+ let delivery = buildDeliveryUrls(baseUrl, options.appType, formUuid);
339
361
  const resolvedFields = mapReferences(form.fields, resolve);
340
362
  if (!formUuid) {
341
363
  const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(resolvedFields), '--no-open'];
@@ -344,6 +366,12 @@ async function run(args, dependencies = {}) {
344
366
  const created = await call(argv);
345
367
  if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
346
368
  formUuid = created.formUuid;
369
+ delivery = {
370
+ ...buildDeliveryUrls(baseUrl, options.appType, formUuid),
371
+ ...(created.url ? { url: created.url } : {}),
372
+ ...(created.formUrl ? { formUrl: created.formUrl } : {}),
373
+ ...(created.appUrl ? { appUrl: created.appUrl } : {}),
374
+ };
347
375
  } catch (error) {
348
376
  const createdFormUuid = error.output?.formUuid || error.output?.details?.formUuid;
349
377
  if (typeof createdFormUuid !== 'string' || !createdFormUuid.startsWith('FORM')) {
@@ -357,25 +385,43 @@ async function run(args, dependencies = {}) {
357
385
  save(state);
358
386
  let resumed = false;
359
387
  if (shouldResume) {
360
- await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
388
+ const resumeOutput = await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
389
+ delivery = {
390
+ ...buildDeliveryUrls(baseUrl, options.appType, formUuid),
391
+ ...(resumeOutput.url ? { url: resumeOutput.url } : {}),
392
+ ...(resumeOutput.formUrl ? { formUrl: resumeOutput.formUrl } : {}),
393
+ ...(resumeOutput.appUrl ? { appUrl: resumeOutput.appUrl } : {}),
394
+ };
361
395
  resumed = true;
362
396
  }
363
397
  let schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
364
398
  if (!readbackMatchesExpectedFields(schema, resolvedFields) && !resumed && !form.formUuid) {
365
- await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
399
+ const resumeOutput = await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
400
+ delivery = {
401
+ ...buildDeliveryUrls(baseUrl, options.appType, formUuid),
402
+ ...(resumeOutput.url ? { url: resumeOutput.url } : {}),
403
+ ...(resumeOutput.formUrl ? { formUrl: resumeOutput.formUrl } : {}),
404
+ ...(resumeOutput.appUrl ? { appUrl: resumeOutput.appUrl } : {}),
405
+ };
366
406
  resumed = true;
367
407
  schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
368
408
  }
369
409
  if (schema.formUuid !== formUuid || !readbackMatchesExpectedFields(schema, resolvedFields)) {
370
410
  throw readbackMismatch(formUuid, { ...form, fields: resolvedFields }, schema);
371
411
  }
372
- return { formUuid, fields: schema.fields };
412
+ return { formUuid, fields: schema.fields, ...delivery };
373
413
  }, () => save(state));
374
414
  const success = Object.values(state.results).every(item => item.status === 'success');
375
- const output = { success, groups, stateFile, results: state.results };
415
+ const output = {
416
+ success,
417
+ groups,
418
+ stateFile,
419
+ results: state.results,
420
+ ...((baseUrl && options.appType) ? { appUrl: `${String(baseUrl).replace(/\/+$/, '')}/${options.appType}/workbench` } : {}),
421
+ };
376
422
  if (!success) {
377
423
  output.errorCode = 'FORM_BATCH_PARTIAL_FAILURE';
378
- output.nextAction = 'Inspect the saved state and child error, then fix the batch input or recover known formUuid values. Do not fall back to create-form create.';
424
+ Object.assign(output, buildPartialFailureRecovery(state.results));
379
425
  }
380
426
  console.log(JSON.stringify(output));
381
427
  if (!output.success) { process.exitCode = 1; }
@@ -397,5 +443,7 @@ module.exports = {
397
443
  execute,
398
444
  expectedReadbackFields,
399
445
  readbackMatchesExpectedFields,
446
+ buildPartialFailureRecovery,
447
+ buildDeliveryUrls,
400
448
  validateStaticDefinition,
401
449
  };
@@ -1719,7 +1719,7 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1719
1719
  id: nextNodeId(),
1720
1720
  props: {
1721
1721
  contentBgColor: 'white',
1722
- pageStyle: { backgroundColor: '#f2f3f5' },
1722
+ pageStyle: { backgroundColor: 'transparent' },
1723
1723
  contentMargin: '20',
1724
1724
  contentPadding: '20',
1725
1725
  showTitle: false,
@@ -1742,7 +1742,7 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1742
1742
  labelWeightMobile: 'normal',
1743
1743
  },
1744
1744
  condition: true,
1745
- css: 'body{background-color:#f2f3f5}',
1745
+ css: 'body{background-color:transparent}',
1746
1746
  methods: {
1747
1747
  __initMethods__: {
1748
1748
  type: 'js',
@@ -1941,7 +1941,7 @@ function buildEmptyFormSchema() {
1941
1941
  id: nextNodeId(),
1942
1942
  props: {
1943
1943
  contentBgColor: 'white',
1944
- pageStyle: { backgroundColor: '#f2f3f5' },
1944
+ pageStyle: { backgroundColor: 'transparent' },
1945
1945
  contentMargin: '20',
1946
1946
  contentPadding: '20',
1947
1947
  showTitle: false,
@@ -1952,7 +1952,7 @@ function buildEmptyFormSchema() {
1952
1952
  contentBgColorMobile: 'white',
1953
1953
  },
1954
1954
  condition: true,
1955
- css: 'body{background-color:#f2f3f5}',
1955
+ css: 'body{background-color:transparent}',
1956
1956
  methods: {
1957
1957
  __initMethods__: {
1958
1958
  type: 'js',
@@ -4858,7 +4858,10 @@ async function saveFormSchema(authRef, appType, formUuid, schema, version, stepO
4858
4858
  formUuid,
4859
4859
  result: sanitizeFailureResult(saveResult),
4860
4860
  });
4861
- if (failureContext) {
4861
+ if (failureContext && failureContext.deferFailureOutput) {
4862
+ // Resume may safely resolve an HTTP 5xx by exact readback. Defer the
4863
+ // failure payload so a recovered command emits one authoritative result.
4864
+ } else if (failureContext) {
4862
4865
  emitCreateFormPostCreateFailure(Object.assign({}, failureContext, {
4863
4866
  stage: 'saveFormSchema',
4864
4867
  error: saveError,
@@ -4879,6 +4882,13 @@ async function saveFormSchema(authRef, appType, formUuid, schema, version, stepO
4879
4882
 
4880
4883
  // ── create 模式主流程 ─────────────────────────────────
4881
4884
 
4885
+ function buildFormDeliveryUrls(baseUrl, appType, formUuid) {
4886
+ return {
4887
+ appUrl: baseUrl + '/' + appType + '/workbench',
4888
+ formUrl: baseUrl + '/' + appType + '/workbench/' + formUuid,
4889
+ };
4890
+ }
4891
+
4882
4892
  async function mainValidateFields(parsedArgs) {
4883
4893
  const { fieldsJsonOrFile } = parsedArgs;
4884
4894
  assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
@@ -5034,13 +5044,13 @@ async function mainCreate(parsedArgs, authRef) {
5034
5044
  }
5035
5045
 
5036
5046
  // 输出结果
5037
- const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5047
+ const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
5038
5048
  result(true, t('create_form.create_success'), [
5039
5049
  ['Form UUID', formUuid],
5040
5050
  ['URL', formUrl],
5041
5051
  ]);
5042
5052
  console.log(JSON.stringify(withBrowserHandoff(
5043
- { success: true, formUuid, formTitle, appType, fieldCount, icon: formIcon, iconSource: iconResolution.source, url: formUrl },
5053
+ { success: true, formUuid, formTitle, appType, fieldCount, icon: formIcon, iconSource: iconResolution.source, url: formUrl, formUrl, appUrl },
5044
5054
  formUrl,
5045
5055
  { stage: 'create_form_success', title: formTitle },
5046
5056
  parsedArgs.browserOpenMode
@@ -5127,6 +5137,7 @@ async function createFormForLegacyProcessQuiet(context, input) {
5127
5137
  }
5128
5138
  const saveResult = await saveFormSchema(authRef, appType, formUuid, schema, serverRevision, 4);
5129
5139
  const navIconResult = await updateCreatedFormNavigationIcon(authRef, appType, formUuid, formIcon);
5140
+ const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
5130
5141
  return {
5131
5142
  success: true,
5132
5143
  appType,
@@ -5138,7 +5149,9 @@ async function createFormForLegacyProcessQuiet(context, input) {
5138
5149
  navIconResult,
5139
5150
  icon: formIcon,
5140
5151
  iconSource: iconResolution.source,
5141
- url: authRef.baseUrl + '/' + appType + '/workbench/' + formUuid,
5152
+ url: formUrl,
5153
+ formUrl,
5154
+ appUrl,
5142
5155
  };
5143
5156
  }
5144
5157
 
@@ -5319,7 +5332,7 @@ async function mainAddOption(parsedArgs, authRef) {
5319
5332
  // Step 4: 保存 Schema
5320
5333
  await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
5321
5334
 
5322
- const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5335
+ const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
5323
5336
  result(true, '选项追加成功', [
5324
5337
  ['Form UUID', formUuid],
5325
5338
  ['Field', fieldLabel],
@@ -5339,6 +5352,8 @@ async function mainAddOption(parsedArgs, authRef) {
5339
5352
  skipped: skippedOptions,
5340
5353
  totalOptions: existingDataSource.length,
5341
5354
  url: formUrl,
5355
+ formUrl,
5356
+ appUrl,
5342
5357
  }));
5343
5358
  }
5344
5359
 
@@ -5438,7 +5453,7 @@ async function mainBindDataSource(parsedArgs, authRef) {
5438
5453
 
5439
5454
  await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
5440
5455
 
5441
- const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5456
+ const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
5442
5457
  result(true, '字段数据源保存成功', [
5443
5458
  ['Form UUID', formUuid],
5444
5459
  ['Field', fieldLabel],
@@ -5459,6 +5474,8 @@ async function mainBindDataSource(parsedArgs, authRef) {
5459
5474
  options: normalized.options.length,
5460
5475
  filterLocal: targetComponent.props.filterLocal,
5461
5476
  pageUrl: formUrl,
5477
+ formUrl,
5478
+ appUrl,
5462
5479
  },
5463
5480
  formUrl,
5464
5481
  { stage: 'bind_datasource_success', title: formUuid },
@@ -6109,6 +6126,59 @@ async function readResumeSchema(authRef, appType, formUuid) {
6109
6126
  return { schemaResult, schema, version: extractSchemaServerRevision(schemaResult) };
6110
6127
  }
6111
6128
 
6129
+ function isRetryableResumeSaveServerFailure(errorObject) {
6130
+ const status = Number(errorObject?.details?.result?.__httpStatus);
6131
+ return errorObject?.code === 'CREATE_FORM_SAVE_SCHEMA_FAILED' &&
6132
+ Number.isInteger(status) && status >= 500 && status <= 599;
6133
+ }
6134
+
6135
+ async function saveResumeSchemaWithSafeRetry(authRef, appType, formUuid, schema, version, fields) {
6136
+ const save = (value, revision) => saveFormSchema(
6137
+ authRef, appType, formUuid, value, revision, 4, { deferFailureOutput: true }
6138
+ );
6139
+ try {
6140
+ await save(schema, version);
6141
+ return '';
6142
+ } catch (originalError) {
6143
+ if (!isRetryableResumeSaveServerFailure(originalError)) { throw originalError; }
6144
+
6145
+ let readback;
6146
+ try { readback = await readResumeSchema(authRef, appType, formUuid); } catch (_) { throw originalError; }
6147
+ const root = readback.schema.pages[0].componentsTree && readback.schema.pages[0].componentsTree[0];
6148
+ const container = root ? findFormContainer(root) : null;
6149
+ if (!container) { throw originalError; }
6150
+ const evidence = collectResumeFieldEvidence(container.children);
6151
+ const verified = buildResumeChanges(
6152
+ evidence,
6153
+ collectResumeDesiredFields(fields, { verifyNestedFields: true })
6154
+ );
6155
+ if (verified.conflicts.length > 0) { throw originalError; }
6156
+ if (verified.missing.length === 0) { return 'save_schema_recovered_by_readback'; }
6157
+ const retryPlan = buildResumeChanges(evidence, collectResumeDesiredFields(fields));
6158
+ if (retryPlan.conflicts.length > 0 || retryPlan.missing.length === 0) { throw originalError; }
6159
+ const applied = applyChangesToSchema(
6160
+ readback.schema,
6161
+ retryPlan.missing.map(function (field) { return { action: 'add', field }; }),
6162
+ { verbose: true }
6163
+ );
6164
+ if ((applied.diagnostics || []).length > 0) { throw originalError; }
6165
+ const updatedContainer = findFormContainer(readback.schema.pages[0].componentsTree[0]);
6166
+ fillSerialNumberFormulas(updatedContainer.children, resolveCorpId(authRef.authData), appType, formUuid);
6167
+
6168
+ try {
6169
+ await save(readback.schema, readback.version);
6170
+ } catch (retryError) {
6171
+ originalError.details = Object.assign({}, originalError.details, {
6172
+ safeRetryAttempted: true,
6173
+ retryErrorCode: retryError?.code || 'CREATE_FORM_SAVE_SCHEMA_FAILED',
6174
+ retryResult: sanitizeFailureResult(retryError?.details?.result),
6175
+ });
6176
+ throw originalError;
6177
+ }
6178
+ return 'save_schema_retried';
6179
+ }
6180
+ }
6181
+
6112
6182
  async function mainResume(parsedArgs, authRef) {
6113
6183
  const { appType, formUuid, fieldsJsonOrFile } = parsedArgs;
6114
6184
  assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
@@ -6139,8 +6209,11 @@ async function mainResume(parsedArgs, authRef) {
6139
6209
  if (resumeValidationRules.length > 0) {
6140
6210
  applySmartValidations(firstRead.schema, resumeValidationRules);
6141
6211
  }
6142
- await saveFormSchema(authRef, appType, formUuid, firstRead.schema, firstRead.version, 4);
6212
+ const saveRecoveryStage = await saveResumeSchemaWithSafeRetry(
6213
+ authRef, appType, formUuid, firstRead.schema, firstRead.version, fields
6214
+ );
6143
6215
  completedStages.push('rebuild_blank_schema', 'save_schema');
6216
+ if (saveRecoveryStage) { completedStages.push(saveRecoveryStage); }
6144
6217
  recoveredBlankShell = true;
6145
6218
  plan = {
6146
6219
  existing: [],
@@ -6189,8 +6262,11 @@ async function mainResume(parsedArgs, authRef) {
6189
6262
  const corpId = resolveCorpId(authRef.authData);
6190
6263
  const updatedContainer = findFormContainer(firstRead.schema.pages[0].componentsTree[0]);
6191
6264
  fillSerialNumberFormulas(updatedContainer.children, corpId, appType, formUuid);
6192
- await saveFormSchema(authRef, appType, formUuid, firstRead.schema, firstRead.version, 4);
6265
+ const saveRecoveryStage = await saveResumeSchemaWithSafeRetry(
6266
+ authRef, appType, formUuid, firstRead.schema, firstRead.version, fields
6267
+ );
6193
6268
  completedStages.push('add_missing_fields', 'save_schema');
6269
+ if (saveRecoveryStage) { completedStages.push(saveRecoveryStage); }
6194
6270
  }
6195
6271
 
6196
6272
  const finalRead = await readResumeSchema(authRef, appType, formUuid);
@@ -6211,7 +6287,7 @@ async function mainResume(parsedArgs, authRef) {
6211
6287
  });
6212
6288
  }
6213
6289
  completedStages.push('verify_final_schema');
6214
- const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
6290
+ const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
6215
6291
  const output = {
6216
6292
  success: true,
6217
6293
  appType,
@@ -6223,6 +6299,8 @@ async function mainResume(parsedArgs, authRef) {
6223
6299
  finalFieldCount: collectResumeFieldEvidence(finalContainer.children).length,
6224
6300
  recoveredBlankShell,
6225
6301
  url: formUrl,
6302
+ formUrl,
6303
+ appUrl,
6226
6304
  };
6227
6305
  console.log(JSON.stringify(withBrowserHandoff(
6228
6306
  output,
@@ -6384,14 +6462,14 @@ async function mainUpdate(parsedArgs, authRef) {
6384
6462
  await saveFormSchema(authRef, appType, formUuid, schema, version, 6);
6385
6463
 
6386
6464
  // 输出结果
6387
- const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
6465
+ const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
6388
6466
  result(true, t('create_form.update_success'), [
6389
6467
  ['Form UUID', formUuid],
6390
6468
  ['URL', formUrl],
6391
6469
  ['Changes', String(appliedChanges.length)],
6392
6470
  ]);
6393
6471
  console.log(JSON.stringify(withBrowserHandoff(
6394
- { success: true, formUuid, appType, changesApplied: appliedChanges.length, changes: appliedChanges, url: formUrl },
6472
+ { success: true, formUuid, appType, changesApplied: appliedChanges.length, changes: appliedChanges, url: formUrl, formUrl, appUrl },
6395
6473
  formUrl,
6396
6474
  { stage: 'update_form_success', title: formUuid },
6397
6475
  parsedArgs.browserOpenMode
@@ -1615,7 +1615,7 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1615
1615
  id: nextNodeId(),
1616
1616
  props: {
1617
1617
  contentBgColor: 'white',
1618
- pageStyle: { backgroundColor: '#f2f3f5' },
1618
+ pageStyle: { backgroundColor: 'transparent' },
1619
1619
  contentMargin: '20',
1620
1620
  contentPadding: '20',
1621
1621
  showTitle: false,
@@ -1638,7 +1638,7 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1638
1638
  labelWeightMobile: 'normal',
1639
1639
  },
1640
1640
  condition: true,
1641
- css: 'body{background-color:#f2f3f5}',
1641
+ css: 'body{background-color:transparent}',
1642
1642
  methods: {
1643
1643
  __initMethods__: {
1644
1644
  type: 'js',
@@ -1833,7 +1833,7 @@ function buildEmptyFormSchema() {
1833
1833
  id: nextNodeId(),
1834
1834
  props: {
1835
1835
  contentBgColor: 'white',
1836
- pageStyle: { backgroundColor: '#f2f3f5' },
1836
+ pageStyle: { backgroundColor: 'transparent' },
1837
1837
  contentMargin: '20',
1838
1838
  contentPadding: '20',
1839
1839
  showTitle: false,
@@ -1844,7 +1844,7 @@ function buildEmptyFormSchema() {
1844
1844
  contentBgColorMobile: 'white',
1845
1845
  },
1846
1846
  condition: true,
1847
- css: 'body{background-color:#f2f3f5}',
1847
+ css: 'body{background-color:transparent}',
1848
1848
  methods: {
1849
1849
  __initMethods__: {
1850
1850
  type: 'js',
@@ -76,9 +76,15 @@ function applyDesignTokens(template, markdown, previousMarkdown) {
76
76
  let body = root[1].replace(declarations, (line, name, value) => line.replace(value, () => changed[name] || replaceBrand(value)));
77
77
  const extra = Object.entries(changed).filter(([name]) => !(name in original));
78
78
  body += extra.map(([name, value]) => ` ${name}: ${value};\n`).join('');
79
- // Keep scoped nav/light/dark selectors and their variable relationships intact.
80
- const tail = template.slice(root.index + root[0].length).replace(declarations,
79
+ let tail = template.slice(root.index + root[0].length).replace(declarations,
81
80
  (line, name, value) => line.replace(value, () => replaceBrand(value)));
81
+ // Platform navigation modes bind independently; the root shell remains the custom-page default.
82
+ if (changed['--pod-shell-theme-bg-color']) {
83
+ tail = tail.replace(/(\.pod-premium\.nav-(light|dark)\s*\{)([^}]*)(\})/g,
84
+ (block, selector, tone, body, close) => selector + body.replace(declarations,
85
+ (line, name, value) => ['--pod-shell-theme-bg-color', '--pod-page-header-bg-color'].includes(name)
86
+ ? line.replace(value, `var(--color-brand1-${tone === 'dark' ? 5 : 3})`) : line) + close);
87
+ }
82
88
  const css = `${template.slice(0, root.index)}:root {${body}}${tail}`;
83
89
  validateThemeCssContent(css);
84
90
  extractThemeColor(css);
@@ -122,7 +122,7 @@ function assertPresetThemeKey(themeKey) {
122
122
  throw new Error(
123
123
  `Unsupported theme: ${themeKey}. Use one of: ${formatPresetThemeKeys()}. ` +
124
124
  'For custom colors or application theme token profiles that are not platform --theme keys, ' +
125
- 'upload an application theme CSS file with update-app --theme-file and pass --theme-color instead.'
125
+ 'upload an application theme CSS file with update-app --theme-file.'
126
126
  );
127
127
  }
128
128
 
@@ -73,18 +73,52 @@ function getFreeStockLibraries() {
73
73
  function getMaterialSourcingGuidance() {
74
74
  return {
75
75
  steps: [
76
- '1. 先用用户提供的素材。',
77
- '2. 再从 Unsplash / Pexels 搜图。',
78
- '3. 仍有缺口时使用宿主生图。',
79
- '4. 最后使用中性占位并保持 draft。',
80
- '5. 所有候选图通过 asset resolve 写入 manifest。',
76
+ '1. Plan 确认后、Fast 设计就绪后启动素材任务,同时推进应用、表单和无图页面;先复用用户图片和已完成位置。',
77
+ '2. 同一页面的图片位置也并发搜索,最多同时四个;每个位置选一张来源完整的图片并查看。',
78
+ '3. 第二轮只补第一轮失败的必需位置,换一张图片或使用设计允许的生成图。',
79
+ '4. 两轮结束后,可选位置采用设计允许的无图布局,必需位置保留缺口;已就绪页面直接继续。',
80
+ '5. 每页使用 asset resolve --page-id 并发下载上传,写入独立清单并交接该页。',
81
81
  ],
82
+ schedulingPolicy: {
83
+ owner: 'host_agent',
84
+ searchConcurrency: 4,
85
+ concurrencyScope: 'collection_run',
86
+ searchUnit: 'slot',
87
+ resumePolicy: 'reuse_final_slots_and_remaining_round_budget',
88
+ runAlongside: ['app_creation', 'form_creation', 'pages_without_images'],
89
+ waitAt: 'own_page_image_binding_and_acceptance',
90
+ resultWriter: 'one_per_page',
91
+ },
92
+ collectionPolicy: {
93
+ maxRoundsPerPage: 2,
94
+ imagesPerSlot: 1,
95
+ candidatesPerSlotPerRound: 1,
96
+ secondRound: 'failed_required_slots_only',
97
+ concurrency: 'independent_pages_and_assets',
98
+ roundOwner: 'host_agent',
99
+ pageSelector: '--page-id',
100
+ },
101
+ completionPolicy: {
102
+ resultSource: 'asset-manifests/<pageId>.json',
103
+ planApproval: 'reuse_existing_approval',
104
+ progressFields: ['visualStyle.forUser.assetStrategy.materialStatus', 'visualStyle.forUser.assetStrategy.missingAssets'],
105
+ nextStep: 'continue_ready_pages',
106
+ },
107
+ failurePolicy: {
108
+ attemptsPerCandidate: 1,
109
+ sourceUnavailable: 'switch_source_for_remaining_slots',
110
+ candidateFailed: 'replace_input',
111
+ exhausted: 'planned_optional_layout_or_required_gap',
112
+ retryCondition: 'within_two_rounds_or_explicit_request',
113
+ },
82
114
  libraries: getFreeStockLibraries(),
83
115
  rules: [
84
116
  '不编造图片 URL;按 manifest 的页面状态继续,只使用当前页 materialStatus=final 的图片。',
85
117
  '默认下载并上传宜搭图片附件;超过 20 MiB 保留原链接;不额外探测 URL,下载失败跳过并记录缺口。',
86
118
  '联网搜图仅使用 Unsplash / Pexels。',
87
- '来源不明时使用中性占位或宿主生成图。',
119
+ '401、403、429、反爬挑战或超时后,将该来源标记为本次采集不可用,其他页面和后续轮次使用其他来源。',
120
+ '每页最多两轮;每个位置每轮选一张,同一候选尝试一次,成功即结束。第二轮只补失败的必需位置;换来源、换工具或恢复上传仍计入本轮预算。',
121
+ '来源信息获取失败时换用信息齐全的候选;真实房源、商品和人员保持真实素材要求。',
88
122
  ],
89
123
  };
90
124
  }