openyida 2026.10.15 → 2026.10.16-2

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 (39) hide show
  1. package/README.md +4 -1
  2. package/lib/agent/cmd.js +1 -1
  3. package/lib/agent/frozen-bundle.js +4 -4
  4. package/lib/agent/private-directory.js +9 -2
  5. package/lib/agent/provider-preflight.js +10 -3
  6. package/lib/agent/runtime-catalog.json +15 -12
  7. package/lib/agent/stdio.js +7 -1
  8. package/lib/app/check-prd-completeness.js +63 -1
  9. package/lib/app/form-navigation.js +7 -3
  10. package/lib/app/update-app.js +3 -0
  11. package/lib/auth/token-auth.js +27 -1
  12. package/lib/auth/token-store.js +29 -3
  13. package/lib/core/command-manifest.js +8 -4
  14. package/lib/core/locales/en.js +17 -1
  15. package/lib/core/locales/zh.js +17 -1
  16. package/lib/core/process-approver-query.js +152 -0
  17. package/lib/core/query-data.js +19 -1
  18. package/lib/core/utils.js +5 -0
  19. package/lib/design-plan/design-plan.js +3 -3
  20. package/lib/design-plan/entry-navigation.js +29 -2
  21. package/lib/design-plan/materialize.js +30 -4
  22. package/lib/design-plan/patch.js +9 -2
  23. package/package.json +5 -5
  24. package/yida-skills/SKILL.md +5 -0
  25. package/yida-skills/references/local-agent-preflight.md +5 -2
  26. package/yida-skills/skills/yida-app/SKILL.md +4 -0
  27. package/yida-skills/skills/yida-app/references/entry-navigation.md +13 -0
  28. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +10 -0
  29. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +2 -0
  30. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +1 -1
  31. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +4 -2
  32. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +7 -1
  33. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -0
  34. package/yida-skills/skills/yida-data-management/SKILL.md +24 -2
  35. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  36. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +6 -0
  37. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +3 -3
  38. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/design.md +3 -2
  39. package/yida-skills/skills-index.json +6 -2
package/README.md CHANGED
@@ -245,10 +245,13 @@ openyida configure-process APP_XXX FORM_XXX .cache/openyida/process/process.json
245
245
  openyida process preview APP_XXX PROC_INST_XXX --output .cache/openyida/process/process.html
246
246
  openyida data query form APP_XXX FORM_XXX --page 1 --size 20
247
247
  openyida data query form APP_XXX FORM_XXX --dynamic-order '{"dateField_xxx":"-"}'
248
+ openyida data query process APP_XXX FORM_XXX --current-approver USER_ID --count
248
249
  openyida data create form APP_XXX FORM_XXX --expect-form-name 客户 --expect-form-type receipt --data-file .cache/openyida/data-import/record.json
249
250
  openyida get-permission APP_XXX FORM_XXX
250
251
  ```
251
252
 
253
+ `--current-approver` takes an exact userId, not a display name. It scans running instances and their current TODO records, counts each matching instance once, and respects the current login's server-authorized data scope. Omit `--count` to include matching instance data. An incomplete scan or permission failure is reported as an error; the result does not claim application-wide visibility.
254
+
252
255
  Append and forward permissions are configured in the process definition through `nodes[].actions.normalActions/appendActions`. Both `create-process` and `configure-process` compile these settings into matching designer and runtime properties. See [approval action configuration](yida-skills/skills/yida-process-rule/references/approval-actions.md) for the JSON example, defaults, and verification limits.
253
256
 
254
257
  `configure-process` 的流程 JSON 中,审批人可配置为发起人、指定成员、指定角色、部门主管或直属主管,例如:
@@ -476,7 +479,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
476
479
  | `openyida list-forms <appType> [--keyword <text>]` | List forms/pages in an app |
477
480
  | `openyida aggregate-table <list\|create-empty\|inspect\|preview\|save\|publish\|status> <appType> ...` | Manage aggregate tables (virtualView) |
478
481
  | `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
479
- | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check PRD page/resource count risk |
482
+ | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
480
483
  | `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
481
484
  | `openyida er <appType> [--format mermaid\|json] [--output file] [--include-system] [--include-pages]` | Export app entity relationship diagram |
482
485
  | `openyida create-page <appType> "<name>" [--mode dashboard] [--hide-nav] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a custom display page |
package/lib/agent/cmd.js CHANGED
@@ -285,7 +285,7 @@ async function run(argv = [], options = {}) {
285
285
  // Inventory includes signed-out CLIs. Runtime reports their auth status;
286
286
  // the control plane independently enforces task readiness.
287
287
  config.providers = providerCheck.providers.filter(p => p.registrable).map(p => ({
288
- profileId: 'openyida.' + p.provider, provider: p.provider, executable: p.executable,
288
+ profileId: 'openyida.' + p.provider, provider: p.provider, executable: p.executable, ...(p.version ? { version: p.version } : {}),
289
289
  }));
290
290
  }
291
291
  if (managedConnections && args.command === 'connect') {
@@ -285,11 +285,11 @@ function materializeBundle(runtime, config, options = {}) {
285
285
  catch (error) {
286
286
  if (!['EEXIST', 'ENOTEMPTY', 'EACCES', 'EPERM'].includes(error.code) || !fs.existsSync(root)) {throw error;}
287
287
  }
288
- } else {
289
- // Never trust a version or mtime cache. Check source stability and every
290
- // byte of the existing bundle, including unexpected entries, on reuse.
291
- verifySources();
292
288
  }
289
+ // On reuse, source bytes were hashed to compute this exact manifest ID.
290
+ // We execute only the existing bundle, whose complete inventory and bytes
291
+ // are verified below. Reopening the source snapshot cannot make that bundle
292
+ // more trustworthy. New publication still checks source stability above.
293
293
  if (published) {fs.chmodSync(root, 0o500);}
294
294
  verifyBundle(root, id);
295
295
  const bundle = { schemaVersion: 1, id, root, manifestPath: path.join(root, 'bundle-manifest.json'), manifestSha256: id,
@@ -51,8 +51,15 @@ foreach ($sid in @($identity.Value, 'S-1-5-18', 'S-1-5-32-544')) {
51
51
  foreach ($directory in ($reader.ReadToEnd() | ConvertFrom-Json)) {
52
52
  # Concurrent launchers may create the same parent. Never rewrite a winner;
53
53
  # accept it only when its actual owner and grants meet the same private policy.
54
- [System.IO.Directory]::CreateDirectory($directory, $acl) | Out-Null
55
- $actual = (New-Object System.IO.DirectoryInfo($directory)).GetAccessControl()
54
+ # Extended-length paths avoid the .NET Framework MAX_PATH limit. Paths
55
+ # are already absolute and validated by the Node caller.
56
+ $nativePath = $directory
57
+ if (-not $directory.StartsWith('\\\\?\\')) {
58
+ if ($directory.StartsWith('\\\\')) { $nativePath = '\\\\?\\UNC\\' + $directory.Substring(2) }
59
+ else { $nativePath = '\\\\?\\' + $directory }
60
+ }
61
+ [System.IO.Directory]::CreateDirectory($nativePath, $acl) | Out-Null
62
+ $actual = (New-Object System.IO.DirectoryInfo($nativePath)).GetAccessControl()
56
63
  if ($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $identity.Value) {throw 'Private directory owner differs'}
57
64
  foreach ($rule in $actual.Access) {
58
65
  $sid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
@@ -186,7 +186,14 @@ async function checkProvider(provider, executable, options) {
186
186
  if (version.status !== 'passed') { return { status: version.status, cliDetected: false }; }
187
187
  // Installation evidence is independent of authentication/protocol readiness.
188
188
  // Apply this consistently to every enabled provider, including future adapters.
189
- const detected = { cliDetected: true };
189
+ // Reuse only a bounded, recognizable version line from this exact probe.
190
+ // Arbitrary provider banners/stderr never become launch metadata.
191
+ const versionLine = String(version.output || '').split(/\r?\n/).map(line => line.trim()).find(line =>
192
+ line.length <= 64 && /^(?:(?:codex(?:-cli)?|qoder(?:cli| cli)?|opencode)\s+)?v?\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)?(?:\s+\([0-9a-z ._-]+\))?$/i.test(line));
193
+ const detected = { cliDetected: true, ...(versionLine ? { version: versionLine } : {}) };
194
+ // Connect/run only establish installation inventory. The runtime discovers live
195
+ // protocol, login and models for EVERY registered provider; doctor probes them here.
196
+ if (options.inventoryOnly) { return { ...detected, status: 'cli_detected', authKnown: false, protocolProbe: 'not_run' }; }
190
197
  if (provider === 'opencode') { return { ...detected, status: 'available', authKnown: false, protocolProbe: 'not_run' }; }
191
198
  const protocol = await execute(executable, provider === 'qoder' ? ['--acp'] : ['app-server', '--listen', 'stdio://'], provider, options);
192
199
  if (protocol.status === 'login_required') {
@@ -230,9 +237,9 @@ async function preflight(args, options = {}) {
230
237
  };
231
238
  for (const candidate of entry.candidates.slice(0, 4)) {
232
239
  executable = candidate;
233
- result = await checkProvider(entry.provider, executable, { ...options, probe: boundedProbe });
240
+ result = await checkProvider(entry.provider, executable, { ...options, inventoryOnly: ['connect', 'run'].includes(args.command), probe: boundedProbe });
234
241
  if (result.cliDetected && !discovered) { discovered = { result, executable }; }
235
- if (Date.now() >= deadline || ['ready', 'available', 'login_required', 'probe_cancelled'].includes(result.status)) { break; }
242
+ if (Date.now() >= deadline || ['ready', 'available', 'cli_detected', 'login_required', 'probe_cancelled'].includes(result.status)) { break; }
236
243
  }
237
244
  if (!result.cliDetected && result.status !== 'probe_cancelled' && discovered) {
238
245
  ({ result, executable } = discovered);
@@ -3,6 +3,9 @@
3
3
  "keys": {
4
4
  "openyida-runtime-public-beta-20260923-1": {
5
5
  "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEApl2Q+64rX997KxEQUY2yU8Xnc7sM3NRQ8Nf0bAi9Hck=\n-----END PUBLIC KEY-----\n"
6
+ },
7
+ "openyida-runtime-public-beta-20261012-1": {
8
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAKaA5RBJXWVhQa+9ehmR3x7WQCAlVGxbJULG9T17qmRc=\n-----END PUBLIC KEY-----\n"
6
9
  }
7
10
  },
8
11
  "releases": [
@@ -10,33 +13,33 @@
10
13
  "platform": "darwin",
11
14
  "arch": "arm64",
12
15
  "packageName": "@openyida/local-agent-runtime-darwin-arm64",
13
- "packageVersion": "0.1.2",
14
- "runtimeVersion": "0.1.2",
15
- "manifestSha256": "98909c5b049544aa287abb781796a6b2261948eae8a4eb4ee6a345edc3ebb8b4"
16
+ "packageVersion": "0.1.4",
17
+ "runtimeVersion": "0.1.4",
18
+ "manifestSha256": "48dc2a4473422c663e3330840c4f96e6646b2ff70ed61452fef8f1e68bf4a258"
16
19
  },
17
20
  {
18
21
  "platform": "darwin",
19
22
  "arch": "x64",
20
23
  "packageName": "@openyida/local-agent-runtime-darwin-x64",
21
- "packageVersion": "0.1.2",
22
- "runtimeVersion": "0.1.2",
23
- "manifestSha256": "29d95d796890fcf6047e79e1a101d68f70506bcd53a5e813d19bf7b833ddb899"
24
+ "packageVersion": "0.1.4",
25
+ "runtimeVersion": "0.1.4",
26
+ "manifestSha256": "1c93114b7fc9f2af7de1088d801ccbd3eaf32aabd967b5bd56917e8c1ec0661b"
24
27
  },
25
28
  {
26
29
  "platform": "win32",
27
30
  "arch": "x64",
28
31
  "packageName": "@openyida/local-agent-runtime-win32-x64",
29
- "packageVersion": "0.1.2",
30
- "runtimeVersion": "0.1.2",
31
- "manifestSha256": "44aff218711c120d4d24e3c34ce2867a34d6f78bab5c1682a4808b4b9c3cfd56"
32
+ "packageVersion": "0.1.4",
33
+ "runtimeVersion": "0.1.4",
34
+ "manifestSha256": "13a7fc58b87a7f3f2592ba5239992c415463ab8b3fbf32312944db8c611fb813"
32
35
  },
33
36
  {
34
37
  "platform": "win32",
35
38
  "arch": "arm64",
36
39
  "packageName": "@openyida/local-agent-runtime-win32-arm64",
37
- "packageVersion": "0.1.2",
38
- "runtimeVersion": "0.1.2",
39
- "manifestSha256": "742e21e97f7c72ca78030d43f10d42e7470da2aabc0af6a8536dde6767390680"
40
+ "packageVersion": "0.1.4",
41
+ "runtimeVersion": "0.1.4",
42
+ "manifestSha256": "49e63f81730e1ee495863a48b9e90f687e9529e700788665196c169a630ed094"
40
43
  }
41
44
  ]
42
45
  }
@@ -127,7 +127,13 @@ function launchRuntime(runtime, config, options = {}) {
127
127
  diagnostic('error', 'runtime_action_failed', {
128
128
  command: config.command, component: 'node', errorCode: code, retryable: event.retryable === true,
129
129
  });
130
- failure = new CliError(t('agent.runtime_failed'), { code, details: { retryable: event.retryable === true } });
130
+ const enrollmentMessages = {
131
+ ENROLLMENT_EXPIRED: 'agent.enrollment_expired',
132
+ ENROLLMENT_REJECTED: 'agent.enrollment_rejected',
133
+ ENROLLMENT_CONFLICT: 'agent.enrollment_conflict',
134
+ ENROLLMENT_REQUEST_FAILED: 'agent.enrollment_request_failed',
135
+ };
136
+ failure = new CliError(t(enrollmentMessages[code] || 'agent.runtime_failed'), { code, details: { retryable: event.retryable === true } });
131
137
  stop('SIGTERM');
132
138
  return;
133
139
  }
@@ -268,8 +268,19 @@ function extractResources(markdown) {
268
268
  }
269
269
 
270
270
  function parsePrdMarkdown(markdown) {
271
+ const navigationVisibility = [];
272
+ let applicationTheme;
273
+ for (const match of String(markdown || '').matchAll(/```json\s*\n([\s\S]*?)\n```/g)) {
274
+ try {
275
+ const handoff = JSON.parse(match[1]);
276
+ if (Array.isArray(handoff.navigationVisibility)) {navigationVisibility.push(...handoff.navigationVisibility);}
277
+ if (Object.prototype.hasOwnProperty.call(handoff, 'applicationTheme')) {applicationTheme = handoff.applicationTheme;}
278
+ } catch { /* Other fenced examples are not the structured execution handoff. */ }
279
+ }
271
280
  return {
272
281
  resources: extractResources(markdown),
282
+ navigationVisibility,
283
+ ...(applicationTheme !== undefined ? { applicationTheme } : {}),
273
284
  };
274
285
  }
275
286
 
@@ -451,6 +462,7 @@ async function evaluatePrdCompleteness(markdown, options = {}, deps = {}) {
451
462
  const parsedManifest = parseBuildManifest(options.buildManifest);
452
463
  const items = [];
453
464
  const assetExecution = auditAssetExecution(options.buildManifest?.assetExecution, options.buildManifest?.assetTasks);
465
+ let themeVerification;
454
466
 
455
467
  function addItem(id, source, status, severity, message, details = {}) {
456
468
  items.push(makeRiskItem(id, source, status, severity, message, details));
@@ -465,6 +477,7 @@ async function evaluatePrdCompleteness(markdown, options = {}, deps = {}) {
465
477
  verdict,
466
478
  mode: 'delivery_risk_radar',
467
479
  assetExecution,
480
+ ...(themeVerification ? { themeVerification } : {}),
468
481
  appType,
469
482
  prdPath: options.prdPath || '',
470
483
  buildManifestPath: options.buildManifestPath || '',
@@ -510,7 +523,7 @@ async function evaluatePrdCompleteness(markdown, options = {}, deps = {}) {
510
523
  const fetchForms = deps.fetchForms || fetchFormPageList;
511
524
  let forms = [];
512
525
  try {
513
- forms = await fetchForms(appType, authRef);
526
+ forms = await fetchForms(appType, authRef, { includeNavigation: true });
514
527
  } catch (error) {
515
528
  addItem('app_resource_list_read_failed', 'remote.resource_list', 'fail', 'blocker', '目标应用资源列表读取失败,无法执行数量完整性检查', {
516
529
  expected: { appType, readable: true },
@@ -522,6 +535,55 @@ async function evaluatePrdCompleteness(markdown, options = {}, deps = {}) {
522
535
  const actualResources = forms.map(makeActualResource);
523
536
  const manifestResources = parsedManifest.resources.filter(resource => resource.required);
524
537
 
538
+ if (parsedPrd.applicationTheme !== undefined) {
539
+ const expected = parsedPrd.applicationTheme;
540
+ const { normalizeCssColorToHex } = require('./custom-theme');
541
+ const expectedColor = normalizeCssColorToHex(expected?.themeColor);
542
+ themeVerification = { configurationReadback: false, verified: false, runtimeVerified: false };
543
+ try {
544
+ if (!expectedColor || !['light', 'dark'].includes(expected?.navTheme)) {throw new Error('INVALID_THEME_EXPECTATION');}
545
+ const { fetchCurrentApp, getThemeSettings, getNavigationSettings } = require('./update-app');
546
+ const app = await (deps.fetchApp || fetchCurrentApp)(appType, authRef);
547
+ if (!app || typeof app !== 'object' || Array.isArray(app) || !Object.keys(app).length) {throw new Error('APP_SETTINGS_UNAVAILABLE');}
548
+ const settings = getThemeSettings(app);
549
+ const rawNav = getNavigationSettings(app, ['navTheme']).navTheme;
550
+ const actual = {
551
+ colour: settings.colour,
552
+ themeColor: normalizeCssColorToHex(settings.themeColor),
553
+ navTheme: ['white', 'gray'].includes(rawNav) ? 'light' : rawNav,
554
+ customThemeEnabled: settings.customThemeStyle?.enabled,
555
+ hasCssUrl: typeof settings.customThemeStyle?.cssUrl === 'string' && !!settings.customThemeStyle.cssUrl.trim(),
556
+ };
557
+ const matched = actual.colour === 'custom' && actual.themeColor === expectedColor
558
+ && actual.navTheme === expected.navTheme && actual.customThemeEnabled === true && actual.hasCssUrl;
559
+ themeVerification = { ...themeVerification, configurationReadback: true, verified: matched, expected, actual };
560
+ if (!matched) {
561
+ addItem('application_theme_mismatch', 'remote.application_theme', 'fail', 'blocker',
562
+ t('check_prd_completeness.application_theme_mismatch'), { expected, actual });
563
+ }
564
+ } catch {
565
+ addItem('application_theme_not_checked', 'remote.application_theme', 'not_checked', 'warning',
566
+ t('check_prd_completeness.application_theme_not_checked'), { expected });
567
+ }
568
+ }
569
+
570
+ // Reuse the resource-list readback. Page renderNav and menu visibility are
571
+ // independent; a successful publication cannot prove the latter.
572
+ for (const expected of parsedPrd.navigationVisibility) {
573
+ const matches = parsedManifest.resources.filter(resource => resource.name === expected?.name && resource.kind === 'display-page');
574
+ const ids = [...new Set(matches.map(resource => resource.formUuid).filter(Boolean))];
575
+ const actual = ids.length === 1 ? forms.find(form => form.formUuid === ids[0]) : undefined;
576
+ const known = value => typeof value === 'boolean';
577
+ const valid = expected?.hidden === true && expected?.mobileHidden === true;
578
+ if (valid && actual && (actual.hidden === false || actual.mobileHidden === false)) {
579
+ addItem('frontend_navigation_visible', 'remote.navigation', 'fail', 'blocker',
580
+ t('check_prd_completeness.frontend_navigation_visible', expected.name), { expected, actual });
581
+ } else if (!valid || !actual || !known(actual.hidden) || !known(actual.mobileHidden)) {
582
+ addItem('frontend_navigation_not_checked', 'prd.navigationVisibility', 'not_checked', 'warning',
583
+ t('check_prd_completeness.frontend_navigation_not_checked', expected?.name || ''), { expected, actual: actual || null });
584
+ }
585
+ }
586
+
525
587
  if (!options.buildManifest) {
526
588
  addItem('build_manifest_missing', 'build_manifest', 'not_checked', 'info', '未提供 build-manifest.json,只能基于 PRD 资源 hints 做风险提示', {
527
589
  expected: '--build-manifest <file>',
@@ -8,7 +8,7 @@ function resolveLocalizedText(value, fallback = '') {
8
8
  return parseYidaI18n(value, fallback, { parseJson: false });
9
9
  }
10
10
 
11
- function normalizeFormNavigationNode(node) {
11
+ function normalizeFormNavigationNode(node, options = {}) {
12
12
  if (!node || node.navType === 'SYSTEM' || !node.formUuid) {
13
13
  return null;
14
14
  }
@@ -21,10 +21,14 @@ function normalizeFormNavigationNode(node) {
21
21
  ),
22
22
  formType: node.formType || '',
23
23
  pathName: node.pathName || '',
24
+ ...(options.includeNavigation ? {
25
+ hidden: ['y', true].includes(node.hidden) ? true : ['n', false].includes(node.hidden) ? false : null,
26
+ mobileHidden: ['y', true].includes(node.mobileHidden) ? true : ['n', false].includes(node.mobileHidden) ? false : null,
27
+ } : {}),
24
28
  };
25
29
  }
26
30
 
27
- async function fetchFormPageList(appType, authRef) {
31
+ async function fetchFormPageList(appType, authRef, options = {}) {
28
32
  const result = await createYidaClient({ authRef }).get(
29
33
  `/dingtalk/web/${appType}/query/formnav/getFormNavigationListByOrder.json`,
30
34
  { _api: 'Nav.queryList', _mock: false }
@@ -39,7 +43,7 @@ async function fetchFormPageList(appType, authRef) {
39
43
 
40
44
  const items = Array.isArray(result.content) ? result.content : [];
41
45
  return items
42
- .map(normalizeFormNavigationNode)
46
+ .map(node => normalizeFormNavigationNode(node, options))
43
47
  .filter(Boolean);
44
48
  }
45
49
 
@@ -819,6 +819,9 @@ async function run(args) {
819
819
  }
820
820
 
821
821
  module.exports = {
822
+ fetchCurrentApp,
823
+ getThemeSettings,
824
+ getNavigationSettings,
822
825
  run,
823
826
  parseArgs,
824
827
  hasShellUpdate,
@@ -229,6 +229,26 @@ function createTokenAuthRequiredError(reason, sourceError) {
229
229
  );
230
230
  }
231
231
 
232
+ function createAuthProfileSelectionError(status) {
233
+ const reason = status.failure_reason || status.status;
234
+ if (!['profile_required', 'profile_not_found'].includes(reason)) {return null;}
235
+ const { CliError } = require('../core/cli-error');
236
+ const { t } = require('../core/i18n');
237
+ return new CliError(t(`common.auth_${reason}`), {
238
+ code: reason === 'profile_required' ? 'AUTH_PROFILE_REQUIRED' : 'AUTH_PROFILE_NOT_FOUND',
239
+ details: {
240
+ authMode: 'token',
241
+ status: reason,
242
+ failure_reason: reason,
243
+ auth_profile: status.auth_profile,
244
+ candidate_count: status.candidate_count,
245
+ candidates: status.candidates,
246
+ next_step: status.next_step,
247
+ next_step_commands: status.next_step_commands,
248
+ },
249
+ });
250
+ }
251
+
232
252
  function createTokenRefreshFailedError(reason, sourceError) {
233
253
  const { CliError } = require('../core/cli-error');
234
254
  return new CliError(
@@ -630,7 +650,12 @@ async function tokenLogout(options = {}) {
630
650
  }
631
651
 
632
652
  async function getAccessToken(options = {}) {
633
- let session = loadTokenSession(options);
653
+ const resolution = resolveTokenSession(options);
654
+ let session = resolution.session;
655
+ if (!session) {
656
+ const selectionError = createAuthProfileSelectionError(resolution);
657
+ if (selectionError) {throw selectionError;}
658
+ }
634
659
  // Do not reject a token from local expiry metadata. The business endpoint is
635
660
  // the authority: requestWithAutoLogin refreshes only after it rejects the
636
661
  // access token, then retries with the newly persisted local session.
@@ -675,4 +700,5 @@ module.exports = {
675
700
  resolveTokenBaseUrl,
676
701
  isRefreshAuthRequired,
677
702
  createTokenRefreshFailedError,
703
+ createAuthProfileSelectionError,
678
704
  };
@@ -472,12 +472,31 @@ function saveAuthProfilePointer(session, options = {}) {
472
472
  return pointer;
473
473
  }
474
474
 
475
- function loadAuthProfilePointer(options = {}) {
476
- const pointerFile = getAuthProfilePointerFilePath(options);
477
- if (!fs.existsSync(pointerFile)) {
475
+ function getWorkspaceAuthProfilePointerFilePath(options = {}) {
476
+ const root = path.resolve(resolveProjectRoot(options));
477
+ const workspace = path.dirname(root);
478
+ if (isEnvTokenAuthMode(options.env || process.env) || path.basename(root) !== 'project' ||
479
+ fs.existsSync(path.join(workspace, 'config.json'))) {
478
480
  return null;
479
481
  }
480
482
  try {
483
+ // Only bridge the conventional workspace -> workspace/project transition.
484
+ // A symlinked project belongs to its own workspace, not its lexical parent.
485
+ if (fs.realpathSync(root) !== path.join(fs.realpathSync(workspace), 'project')) {return null;}
486
+ return getAuthProfilePointerFilePath({ ...options, projectRoot: workspace, envName: resolveEnvName(options) });
487
+ } catch {return null;}
488
+ }
489
+
490
+ function loadAuthProfilePointer(options = {}) {
491
+ try {
492
+ let pointerFile = getAuthProfilePointerFilePath(options);
493
+ // An explicit child selection (including an unbound/corrupt pointer) wins.
494
+ // Reading inherited metadata never copies credentials or writes a pointer.
495
+ if (!fs.existsSync(pointerFile)) {
496
+ if (fs.existsSync(getTokenFilePath(options))) {return null;}
497
+ pointerFile = getWorkspaceAuthProfilePointerFilePath(options);
498
+ }
499
+ if (!pointerFile || !fs.existsSync(pointerFile)) {return null;}
481
500
  const pointer = JSON.parse(fs.readFileSync(pointerFile, 'utf8'));
482
501
  if (!pointer || typeof pointer !== 'object' || !pointer.auth_profile) {
483
502
  return null;
@@ -491,6 +510,13 @@ function loadAuthProfilePointer(options = {}) {
491
510
  function clearAuthProfilePointer(options = {}) {
492
511
  try {
493
512
  const pointerFile = getAuthProfilePointerFilePath(options);
513
+ const workspacePointerFile = getWorkspaceAuthProfilePointerFilePath(options);
514
+ if (workspacePointerFile && fs.existsSync(workspacePointerFile)) {
515
+ // Detaching this project must not revive its inherited workspace choice
516
+ // on the next command, or clear another project's workspace selection.
517
+ writeJsonFileAtomic(pointerFile, { version: AUTH_PROFILE_VERSION, detached: true });
518
+ return;
519
+ }
494
520
  if (fs.existsSync(pointerFile)) {
495
521
  fs.unlinkSync(pointerFile);
496
522
  }
@@ -1165,7 +1165,8 @@ const COMMAND_GROUPS = [
1165
1165
  output: 'text|json',
1166
1166
  args: [
1167
1167
  { name: 'buildPlan', type: 'file', required: true, source: 'positional', position: 0, builder_options: ['--build-plan'] },
1168
- { name: 'set', type: 'string', required: true, source: 'option', builder_options: ['--set'], repeatable: true, description: 'Field assignment path=value; repeat --set for multiple changes. CSS token overrides use visualStyle.tokens.<token>=<value>.' },
1168
+ { name: 'setFile', type: 'file', source: 'option', builder_options: ['--set-file'], description: 'JSON object mapping field paths to typed values; use for Windows arrays, objects and quoted text. Requires --set or --set-file.' },
1169
+ { name: 'set', type: 'string', source: 'option', builder_options: ['--set'], repeatable: true, description: 'Field assignment path=value; repeat --set for multiple changes. CSS token overrides use visualStyle.tokens.<token>=<value>.' },
1169
1170
  { name: 'materialize', type: 'boolean', source: 'option', builder_options: ['--materialize'] },
1170
1171
  { name: 'outputDir', type: 'string', source: 'option', builder_options: ['--output-dir'] },
1171
1172
  { name: 'json', type: 'boolean', source: 'option', builder_options: ['--json'] },
@@ -1492,7 +1493,7 @@ const COMMAND_GROUPS = [
1492
1493
  }),
1493
1494
  command('check-prd-completeness', ['check-prd-completeness'], 'check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]', 'help.cmd_check_prd_completeness', { managedRun: { scope: 'app', appParser: 'prd-completeness' },
1494
1495
  output: 'json',
1495
- notes: ['Checks resource counts and optional build-manifest.assetExecution recorded intervals. Include materialize.assetTasks and taskState records plus businessWork startedAt/endedAt; missing dispatch proof, missing overlap or assets before business in synchronous fallback produces needs_review. This is record consistency, not independent verification of host events.'],
1496
+ notes: ['Checks resource counts and optional build-manifest.assetExecution recorded intervals. Include materialize.assetTasks and taskState records plus businessWork startedAt/endedAt; missing dispatch proof, missing overlap or assets before business in synchronous fallback produces needs_review. This is record consistency, not independent verification of host events.', 'When the PRD execution handoff contains navigationVisibility, resolves frontend display pages through build-manifest resource IDs and checks PC/mobile menu hiding using the same remote resource-list read. A visible menu fails; unknown visibility or missing ID binding needs review. Checks PRD applicationTheme against live application settings: selected brand color, custom CSS enabled with a resource URL, and navigation tone. Mismatch fails; unreadable settings or invalid expectations need review. Reuses update-app readback field handling. Does not verify page renderNav, actual CSS content/loading, default routes or browser appearance.'],
1496
1497
  }),
1497
1498
  command('check-design', ['check-design'], 'check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]', 'help.cmd_check_design', { managedRun: { scope: 'local' },
1498
1499
  requiresLogin: false,
@@ -1624,13 +1625,14 @@ const COMMAND_GROUPS = [
1624
1625
  } },
1625
1626
  } },
1626
1627
  output: 'json',
1627
- noteKeys: ['help.data_notes'],
1628
+ noteKeys: ['help.data_notes', 'query_data.approver_query_help'],
1628
1629
  notes: [
1629
1630
  'Each condition uses key (field ID), componentName, type, operator and value. Query types: TextField/TextareaField/TableField TEXT; NumberField/RateField/DateField/CascadeDateField DOUBLE; RadioField/SelectField/CheckboxField/MultiSelectField/DepartmentSelectField ARRAY; EmployeeField/CascadeSelectField/CitySelectField STRING; ImageField/AttachmentField TEXT.',
1630
1631
  'Ordinary form searchFormDatas accepts AND condition arrays via --search-json/--search-file. DateField: type DOUBLE, is/is_not, value ""; zero is filled. Query range endpoints as DateField with .start/.end: both empty means unfilled, both filled means complete.',
1631
1632
  'Use synchronized NumberField counts (eq 0/gt 0) for any filled endpoint, multi-choice options or subform rows; maintain every write entry and backfill history. --resolve-aliases supports rangeAlias.start/.end and parentId aliases. Use server pagination and dynamicOrder. Guide: yida-skills/references/query-condition-guide.md; sample: yida-data-management empty-query.',
1632
1633
  ],
1633
1634
  examples: [
1635
+ 'openyida data query process APP_XXX FORM_XXX --current-approver USER_ID --count',
1634
1636
  'openyida data query form APP_XXX FORM_XXX --page 1 --size 20 --search-json \'[{"key":"dateField_due","componentName":"DateField","type":"DOUBLE","operator":"is","value":""}]\'',
1635
1637
  'openyida data create form APP_XXX FORM_XXX --expect-form-name 客户 --expect-form-type receipt --data-json \'{"textField_name":"客户 A","dateField_followUp":1787932800000}\'',
1636
1638
  'openyida data delete form APP_XXX FORM_XXX --inst-id FINST_XXX --expect-form-name 客户 --expect-form-type receipt --confirm --json',
@@ -2083,7 +2085,9 @@ function summarizeLocalizedCommands(commands) {
2083
2085
  derived_summary: 'execution.entryModeSummary is generated for PRD and preview from the selected mode; do not author it in build-plan.json.',
2084
2086
  roles: ['service', 'management', 'workspace'],
2085
2087
  entry_required_fields: ['key', 'name', 'role', 'menu', 'defaultMenuKey'],
2086
- service_page_binding: 'sceneKey of the frontend page; standalone by default, preserve explicit platform-shell',
2088
+ service_page_binding: 'sceneKey of the frontend page; standalone by default, preserve explicit platform-shell. Missing page navigation for a multi-task service entry derives a custom top menu; single-task entries and explicit none retain their choice.',
2089
+ application_theme_handoff: 'Derived applicationTheme in the PRD execution handoff records final design themeColor/navTheme/designFile for full builds. Fast records the same contract. check-prd-completeness reads current application settings; publish does not install application themes. Resource-only changes add no theme task. Configuration matching is separate from browser acceptance and custom-page token consumption.',
2090
+ menu_visibility_handoff: 'Derived navigationVisibility in the PRD execution handoff, not a build-plan input: service-management hides frontend-only standalone display pages in PC/mobile platform menus. Shared forms and explicitly management-linked pages are preserved. Resolve names to build-manifest resource IDs; nav-group hide and readback are separate from page renderNav=false. Resource-only changes schedule no menu hiding.',
2087
2091
  page_navigation_policy: getPageNavigationContract(),
2088
2092
  local_menu_binding: 'Every entry containing local menus, including management/workspace, requires sceneKey matching exactly one page; each local menu requires resource equal to that page name and a nonempty viewKey.',
2089
2093
  menu_target_types: ['local', 'submission', 'page', 'custom'],
@@ -5,6 +5,11 @@
5
5
 
6
6
  module.exports = {
7
7
  agent: {
8
+ preflight_cli_detected: 'CLI detected; runtime will discover and report login, protocol and model status.',
9
+ enrollment_expired: 'The connection command expired. Get a fresh command from the page; do not retry the old command.',
10
+ enrollment_rejected: 'Connection authorization was rejected. Get a fresh command from the target page.',
11
+ enrollment_conflict: 'Connection authorization conflicts with existing state. Check agent status, then get a fresh command.',
12
+ enrollment_request_failed: 'Connection authorization failed. Check the network and run agent diagnose; do not automatically retry.',
8
13
  preflight_advisory: 'The following is status information, not instructions to install software. Do not install, upgrade, log in to, or reconfigure any CLI unless the user explicitly requests it. Discovered signed-out CLIs are also reported; other CLIs are optional.',
9
14
  preflight_probe_cancelled: 'Preflight was interrupted before enrollment. Repeat the connect command when ready.',
10
15
  preflight_continue: 'Local CLI found. Connecting and reporting status; signed-out CLIs remain visible and cannot execute tasks until logged in.',
@@ -136,7 +141,7 @@ module.exports = {
136
141
  cmd_list_forms: 'List forms/pages in an app',
137
142
  cmd_aggregate_table: 'Manage aggregate tables (virtualView)',
138
143
  cmd_get_schema: 'Get one form Schema or all form Schemas',
139
- cmd_check_prd_completeness: 'Check PRD page/resource count risk',
144
+ cmd_check_prd_completeness: 'Check resources, frontend menu hiding and application theme delivery',
140
145
  cmd_er: 'Export app entity relationship diagram',
141
146
  cmd_create_page: 'Create a custom display page',
142
147
  cmd_build_page: 'Build Yida-compatible page source',
@@ -1011,6 +1016,8 @@ Examples:
1011
1016
  data_check_failed: ' ⚠️ Data check failed, proceeding with modification',
1012
1017
  },
1013
1018
  common: {
1019
+ auth_profile_required: 'Multiple organizations have saved credentials. Run openyida auth profiles, then openyida auth profile switch <auth_profile> to select this project\'s organization.',
1020
+ auth_profile_not_found: 'The requested organization credentials were not found. Run openyida auth profiles to inspect saved organizations and select the correct profile.',
1014
1021
  auth_identity_changed: 'Authentication identity changed during preflight; no write was issued.',
1015
1022
  http_status: ' HTTP status: {0}',
1016
1023
  http_response: ' HTTP response: {0}',
@@ -1548,6 +1555,10 @@ Examples:
1548
1555
  },
1549
1556
 
1550
1557
  check_prd_completeness: {
1558
+ application_theme_mismatch: 'The application theme does not match the selected project theme.',
1559
+ application_theme_not_checked: 'Application theme settings could not be verified; theme delivery remains unverified.',
1560
+ frontend_navigation_not_checked: 'Frontend menu hiding has not been verified: {0}',
1561
+ frontend_navigation_visible: 'Frontend page remains visible in the desktop or mobile management menu: {0}',
1551
1562
  usage: 'Usage: openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]',
1552
1563
  example: 'Example: openyida check-prd-completeness prd/order-management/prd.md --app-type APP_XXX --build-manifest prd/order-management/build-manifest.json --json',
1553
1564
  prd_not_found: 'PRD file not found: {0}',
@@ -2036,6 +2047,11 @@ Options:
2036
2047
  };
2037
2048
 
2038
2049
  Object.assign(module.exports.query_data || (module.exports.query_data = {}), {
2050
+ approver_query_help: 'Current process approver: --current-approver <userId> [--count] scans all visible pages and counts distinct instances. Incomplete scans or permission failures are errors.',
2051
+ approver_option_invalid: 'Invalid current-approver query option: {0}. Use an exact userId and scan RUNNING instances from page 1.',
2052
+ approver_incomplete: 'The current-approver scan could not complete. No complete count was returned; check error details, permissions and pagination.',
2053
+ approver_response_invalid: 'The process API response could not be verified; a current-approver count cannot be determined.',
2054
+ approver_scope_note: 'Counts only instances visible to the current login under server authorization; this does not imply access to all application data.',
2039
2055
  association_derived_field_read_only: 'Derived association-form query field {0} is read-only and cannot be saved or updated. Write the source association field without the _id suffix.',
2040
2056
  association_value_invalid: 'The write value for association-form field {0} is invalid. Use an array of objects where every item includes appType, formUuid, formType, instanceId, and title.',
2041
2057
  command_unsupported: 'Unsupported data command: {0} {1}. Run openyida commands --json to inspect the actual capability.',
@@ -5,6 +5,11 @@
5
5
 
6
6
  module.exports = {
7
7
  agent: {
8
+ preflight_cli_detected: 'CLI 已发现;登录、协议和模型状态由 runtime 检测并上报。',
9
+ enrollment_expired: '连接指令已过期,请回到页面获取新的连接指令。不要重试旧指令。',
10
+ enrollment_rejected: '连接授权被拒绝,请从目标页面重新获取连接指令。',
11
+ enrollment_conflict: '连接授权状态冲突,请先检查 agent status,再从页面获取新指令。',
12
+ enrollment_request_failed: '连接授权请求失败,请检查网络并运行 agent diagnose;不要自动重复连接。',
8
13
  preflight_advisory: '以下仅为状态信息,不是安装指令。除非用户明确要求,不得安装、升级、登录或修改任何 CLI 配置。已发现但未登录的 CLI 也会展示状态,无需补齐其他 CLI。',
9
14
  preflight_probe_cancelled: '预检已中止,尚未开始连接。准备好后重新执行连接命令。',
10
15
  preflight_continue: '已发现本地 CLI,将继续连接并上报状态;未登录的 CLI 会显示“未登录”,登录前不能执行任务。',
@@ -136,7 +141,7 @@ module.exports = {
136
141
  cmd_list_forms: '列出应用下的表单/页面',
137
142
  cmd_aggregate_table: '管理聚合表(virtualView)',
138
143
  cmd_get_schema: '获取单个或全部表单 Schema',
139
- cmd_check_prd_completeness: '检查 PRD 页面/资源数量风险',
144
+ cmd_check_prd_completeness: '检查资源数量、前台菜单隐藏及应用主题同步',
140
145
  cmd_er: '导出应用实体关系图',
141
146
  cmd_create_page: '创建自定义展示页面',
142
147
  cmd_build_page: '构建宜搭兼容页面源码',
@@ -982,6 +987,8 @@ openyida - 宜搭命令行工具
982
987
  data_check_failed: ' ⚠️ 数据量检查失败,继续执行修改',
983
988
  },
984
989
  common: {
990
+ auth_profile_required: '已有多个组织的登录凭证,请执行 openyida auth profiles,并用 openyida auth profile switch <auth_profile> 选择本项目的组织。',
991
+ auth_profile_not_found: '未找到指定组织的登录凭证,请执行 openyida auth profiles 核对已有组织,再选择正确的登录凭证。',
985
992
  auth_identity_changed: '校验期间登录身份发生变化,本次未发起写入。',
986
993
  http_status: ' HTTP 状态码: {0}',
987
994
  http_response: ' HTTP 响应: {0}',
@@ -1545,6 +1552,10 @@ openyida - 宜搭命令行工具
1545
1552
  },
1546
1553
 
1547
1554
  check_prd_completeness: {
1555
+ application_theme_mismatch: '应用主题与已选项目主题不一致,尚未完成统一换肤。',
1556
+ application_theme_not_checked: '应用主题配置未能核验,不能确认已完成统一换肤。',
1557
+ frontend_navigation_not_checked: '前台页面的后台菜单隐藏尚未核验:{0}',
1558
+ frontend_navigation_visible: '前台页面仍出现在后台 PC 或移动端菜单:{0}',
1548
1559
  usage: '用法: openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]',
1549
1560
  example: '示例: openyida check-prd-completeness prd/订单管理/prd.md --app-type APP_XXX --build-manifest prd/订单管理/build-manifest.json --json',
1550
1561
  prd_not_found: 'PRD 文件不存在:{0}',
@@ -1990,6 +2001,11 @@ openyida - 宜搭命令行工具
1990
2001
  };
1991
2002
 
1992
2003
  Object.assign(module.exports.query_data || (module.exports.query_data = {}), {
2004
+ approver_query_help: '流程当前审批人:--current-approver <userId> [--count],完整分页并按实例去重;结果仅限当前登录人的可见范围,不完整或无权限会报错。',
2005
+ approver_option_invalid: '当前审批人查询参数无效:{0}。请使用真实 userId,并从第1页查询运行中实例。',
2006
+ approver_incomplete: '无法完成当前审批人统计,未返回完整条数。请检查错误详情、权限及分页范围。',
2007
+ approver_response_invalid: '流程接口返回结构无法确认,不能据此统计当前审批人条数。',
2008
+ approver_scope_note: '仅统计当前登录人经平台服务端授权可见的实例;不代表拥有全应用数据权限。',
1993
2009
  association_derived_field_read_only: '关联表单查询派生字段 {0} 只读,不能用于保存或更新。请写入不带 _id 后缀的原始关联表单字段。',
1994
2010
  association_value_invalid: '关联表单字段 {0} 的写入值无效。必须使用对象数组,每项完整包含 appType、formUuid、formType、instanceId 和 title。',
1995
2011
  command_unsupported: '不支持的数据命令:{0} {1}。请使用 openyida commands --json 查询真实能力。',