openyida 2026.10.19 → 2026.10.20-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 (100) hide show
  1. package/README.md +43 -2
  2. package/bin/yida.js +28 -3
  3. package/lib/app/agent-app-type.js +12 -0
  4. package/lib/app/app-settings.js +38 -0
  5. package/lib/app/application-entry-urls.js +9 -1
  6. package/lib/app/cool-card.js +207 -0
  7. package/lib/app/create-form.js +19 -46
  8. package/lib/app/custom-button.js +372 -0
  9. package/lib/app/data-card-builder.js +188 -0
  10. package/lib/app/data-card-release.js +191 -0
  11. package/lib/app/data-card.js +251 -0
  12. package/lib/app/form-open-guard.js +2 -2
  13. package/lib/app/services/canvas-page-schema-builder.js +6 -1
  14. package/lib/app/services/form-compiler.js +3 -0
  15. package/lib/app/upgrade-app-theme.js +2 -1
  16. package/lib/core/command-manifest.js +169 -10
  17. package/lib/core/locales/en.js +138 -1
  18. package/lib/core/locales/zh.js +138 -1
  19. package/lib/core/query-data.js +30 -20
  20. package/lib/core/sample.js +3 -0
  21. package/lib/core/task-center.js +68 -4
  22. package/lib/dws/dws-wrapper.js +7 -56
  23. package/lib/formula/catalog.json +1 -0
  24. package/lib/formula/cli-contract.js +46 -0
  25. package/lib/formula/commands.js +165 -0
  26. package/lib/formula/config.js +68 -0
  27. package/lib/formula/contracts.js +26 -0
  28. package/lib/formula/evaluate.js +47 -67
  29. package/lib/formula/metadata.js +44 -0
  30. package/lib/formula/runtime.js +212 -0
  31. package/lib/formula/syntax.js +162 -0
  32. package/lib/integration/integration-create.js +2 -1
  33. package/lib/integration/integration-data-retrieve.js +12 -5
  34. package/lib/integration/integration-process-builder.js +10 -4
  35. package/lib/integration/integration-spec-builder.js +45 -11
  36. package/lib/report/chart-builder.js +7 -1
  37. package/lib/report/configure.js +151 -0
  38. package/lib/report/i18n-messages.js +22 -0
  39. package/lib/report/inspect.js +7 -0
  40. package/lib/report/validation.js +3 -0
  41. package/lib/report/zero-values.js +148 -0
  42. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +14 -0
  43. package/lib/samples/yida-cool-card/project-brief.json +5 -0
  44. package/lib/samples/yida-task-center/center.canvas.jsx +169 -0
  45. package/lib/samples/yida-task-center/filters.jsx +59 -0
  46. package/lib/samples/yida-task-center/progress.canvas.jsx +91 -0
  47. package/lib/task-center/contract.js +249 -0
  48. package/lib/task-center/loader.js +63 -0
  49. package/lib/task-center/lookups.js +82 -0
  50. package/lib/task-center/scaffold.js +178 -0
  51. package/lib/task-center/workflow-loader.js +72 -0
  52. package/lib/task-center/workflow.js +118 -0
  53. package/package.json +2 -1
  54. package/scripts/postinstall.js +5 -2
  55. package/yida-skills/SKILL.md +21 -7
  56. package/yida-skills/references/formula-functions.md +151 -315
  57. package/yida-skills/references/routing-supplement.md +4 -0
  58. package/yida-skills/skills/yida-app/SKILL.md +2 -0
  59. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +2 -0
  60. package/yida-skills/skills/yida-app/workflow/step-2-design.md +2 -0
  61. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +3 -1
  62. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +2 -0
  63. package/yida-skills/skills/yida-business-rule/SKILL.md +2 -0
  64. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +8 -0
  65. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +11 -1
  66. package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +79 -0
  67. package/yida-skills/skills/yida-cool-card/SKILL.md +66 -0
  68. package/yida-skills/skills/yida-cool-card/references/configuration.md +80 -0
  69. package/yida-skills/skills/yida-cool-card/references/design.md +90 -0
  70. package/yida-skills/skills/yida-create-form-page/SKILL.md +4 -0
  71. package/yida-skills/skills/yida-create-form-page/references/advanced-form-modes.md +6 -0
  72. package/yida-skills/skills/yida-custom-button/SKILL.md +71 -0
  73. package/yida-skills/skills/yida-custom-page/SKILL.md +2 -0
  74. package/yida-skills/skills/yida-data-card/SKILL.md +94 -0
  75. package/yida-skills/skills/yida-data-card/references/chart-config.md +73 -0
  76. package/yida-skills/skills/yida-data-card/references/release.md +77 -0
  77. package/yida-skills/skills/yida-data-management/SKILL.md +7 -1
  78. package/yida-skills/skills/yida-design/SKILL.md +2 -0
  79. package/yida-skills/skills/yida-flow-node-config/SKILL.md +1 -0
  80. package/yida-skills/skills/yida-flow-node-config/references/conditions-variables.md +6 -1
  81. package/yida-skills/skills/yida-flow-node-config/references/control.md +6 -0
  82. package/yida-skills/skills/yida-flow-node-config/references/data.md +8 -0
  83. package/yida-skills/skills/yida-formula/SKILL.md +152 -373
  84. package/yida-skills/skills/yida-formula/references/automation-formulas.md +100 -0
  85. package/yida-skills/skills/yida-formula/references/examples.md +3 -3
  86. package/yida-skills/skills/yida-formula/references/expression-formats.md +85 -0
  87. package/yida-skills/skills/yida-formula/references/field-values.md +205 -0
  88. package/yida-skills/skills/yida-formula/references/nested-examples.md +189 -0
  89. package/yida-skills/skills/yida-formula/references/node-examples.md +243 -0
  90. package/yida-skills/skills/yida-formula/references/runtime-examples.md +131 -0
  91. package/yida-skills/skills/yida-formula-evaluate/SKILL.md +137 -39
  92. package/yida-skills/skills/yida-integration/SKILL.md +4 -2
  93. package/yida-skills/skills/yida-integration/references/integration-node-schemas.md +4 -2
  94. package/yida-skills/skills/yida-integration/references/spec-authoring.md +2 -0
  95. package/yida-skills/skills/yida-process-rule/SKILL.md +2 -0
  96. package/yida-skills/skills/yida-report/SKILL.md +37 -1
  97. package/yida-skills/skills/yida-report/references/zero-values.md +59 -0
  98. package/yida-skills/skills/yida-task-center/SKILL.md +23 -0
  99. package/yida-skills/skills/yida-task-center/references/task-filters.md +128 -0
  100. package/yida-skills/skills-index.json +315 -32
package/README.md CHANGED
@@ -95,6 +95,22 @@ Local terminals and AI tools share the same policy. Managed cloud Agent runtimes
95
95
 
96
96
  If Codex is already installed, OpenYida also imports a local Codex plugin during postinstall. Restart Codex after installation, then type `@宜搭` or `@openyida` in the composer to attach the OpenYida context.
97
97
 
98
+ <details>
99
+ <summary>Optional: install DingTalk Workspace CLI (dws)</summary>
100
+
101
+ For DingTalk A2UI authoring, validation or messaging, install the official [DingTalk Workspace CLI](https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli#installation) separately:
102
+
103
+ ```bash
104
+ npm install -g dingtalk-workspace-cli
105
+ dws version
106
+ ```
107
+
108
+ Alternatively, `openyida dws install` displays official instructions for your platform without running an installer. Once installed, `openyida dws <command> [args]` forwards arguments to your system's dws.
109
+
110
+ OpenYida does not bundle or automatically install dws. Dws manages its own version, updates and login. Use `dws auth login` for DingTalk business access and verify the target organization and account. Native YiDA cool-card creation, reads and editor links work without dws; updating a chat card through dws does not save a YiDA template. See the official instructions for other installation methods and check the installed command's `--help` before use.
111
+
112
+ </details>
113
+
98
114
  ### 2. Check Your Environment
99
115
 
100
116
  Run this from the AI coding workspace where you want OpenYida to operate:
@@ -447,6 +463,9 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
447
463
 
448
464
  | Command | Description |
449
465
  |---------|-------------|
466
+ | `openyida app-settings <appType> [--section theme\|basic\|data-card\|cool-card] [--json]` | Read theme, frontend/backend, native data card and cool card management links |
467
+ | `openyida cool-card <list\|templates\|create\|get> <appType> [cardPageCode] [--type im\|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]` | Create and read native YiDA cool cards (interactive and pinned) |
468
+ | `openyida data-card <list\|create\|get\|save\|publish\|probe\|release-get\|release-save\|release-check\|release-test\|refs> <appType> [cardCode] [--name <title>] [--description <text>] [--schema <file>\|--config <file>] [--expected-hash <hash>] [--backup <file>] [--out <file>] [--published\|--version <version>] [--page <n>] [--size <n>] [--search <text>] [--allow-send] [--json]` | Create, configure, publish and distribute native YiDA data cards |
450
469
  | `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
451
470
  | `openyida app-entry set <appType> [--frontend <url>\|--frontend-page <formUuid>] [--management <url>\|--management-page <formUuid>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
452
471
  | `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
@@ -491,6 +510,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
491
510
  | `openyida list-forms <appType> [--keyword <text>]` | List forms/pages in an app |
492
511
  | `openyida aggregate-table <list\|create-empty\|inspect\|preview\|save\|publish\|status> <appType> ...` | Manage aggregate tables (virtualView) |
493
512
  | `openyida form-view <list\|inspect\|save\|create\|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]` | Native form views: list, inspect and configure columns, widths, frozen columns and filters |
513
+ | `openyida custom-button <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native custom buttons: inspect and configure form edits, automation flows and page links |
494
514
  | `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
495
515
  | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
496
516
  | `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
@@ -508,7 +528,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
508
528
  | Command | Description |
509
529
  |---------|-------------|
510
530
  | `openyida data <query\|get\|create\|update> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]` | Unified data management (form/process/task/subform) |
511
- | `openyida task-center <type> [options]` | Global task center (todo/processed/cc etc.) |
531
+ | `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
512
532
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
513
533
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
514
534
  | `openyida read-dingtalk-tingji <taskUuid> [--json]` | Fetch DingTalk Tingji details by task UUID |
@@ -545,6 +565,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
545
565
  | `openyida create-report <appType> "<name>" ... [--json] [--open\|--no-open]` | Create a Yida report |
546
566
  | `openyida append-chart <appType> <reportId> ... [--json] [--open\|--no-open]` | Append chart to existing report |
547
567
  | `openyida report inspect <appType> <reportId> --json` | Inspect report runtime bindings (read-only) |
568
+ | `openyida report configure <appType> <reportId> <cid> --hide-zero-values <true\|false> [--hide-zero-categories <true\|false>] [--hide-zero-legends <true\|false>] [--dry-run] [--json]` | Configure dynamic zero-value hiding on a native bar chart (explicit opt-in) |
548
569
 
549
570
  ### Connectors
550
571
 
@@ -598,7 +619,13 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
598
619
  | `openyida sample [--list] [<skill> <name>] [--output <file-or-directory>] [--var KEY=VALUE ...] [--design-file <design.md>] [--style-id <themeId\|free-creative>] [--theme-ids <id,...>] [--pop-art-context <explicit-request\|showcase>]` | Output code samples/templates |
599
620
  | `openyida doctor [--fix]` | Environment diagnostics & auto-fix |
600
621
  | `openyida db-seq-fix [--fix]` | Detect and repair PostgreSQL sequence drift |
601
- | `openyida formula evaluate <formula\|file> [--schema file]` | Static-check Yida formula syntax and field refs |
622
+ | `openyida formula evaluate <formula\|file> [--schema file] [--contract field-value\|validation-rule\|business-action\|node-expression] [--context form\|validation\|automation\|business] [--target-type NumberField] [--metadata file] [--strict] [--json]` | Static-check Yida formula syntax and field refs |
623
+ | `openyida formula check <formula\|file> [--schema file] [--contract field-value\|validation-rule\|business-action\|node-expression] [--context form\|validation\|automation\|business] [--target-type NumberField] [--metadata file] [--strict] [--json]` | Check formula syntax, fields, arguments and types |
624
+ | `openyida formula contracts [--json]` | Show formula formats and checker coverage |
625
+ | `openyida formula functions <appType> [--name IF] [--context form\|validation\|automation\|business] [--output file] [--json]` | Read Yida formula functions and signatures |
626
+ | `openyida formula types <appType> [--component NumberField] [--json]` | Query formula component value types |
627
+ | `openyida formula inspect <appType> <formUuid> [--field fieldId] [--json]` | Inspect configured form field formulas |
628
+ | `openyida formula test <appType> <formUuid> [formula\|file] [--data-file file \| --cases file] [--field fieldId] [--timezone-offset minutes] [--context form\|validation] [--binding platform\|numeric-literals] [--expect JSON] [--json]` | Evaluate a non-writing formula in Yida and compare its result |
602
629
  | `openyida update` | Check and update to latest version |
603
630
  | `openyida export-conversation [options]` | Export AI conversation records |
604
631
  | `openyida feedback <report\|setup\|url\|dismiss\|status> [options]` | Prepare local issue reports, configure feedback forms and reminders |
@@ -612,6 +639,20 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
612
639
 
613
640
  <!-- OPENYIDA_COMMANDS_END -->
614
641
 
642
+ ### Personal tasks and process progress
643
+
644
+ Query application tasks with `openyida data query tasks <appType> --type todo --normalized`; `created`, `done`, and `cc` select the other personal task lists. Use `openyida task-center capabilities --json` for supported filters and the page configuration example.
645
+
646
+ The default `app-v1` transport preserves the existing queries. Select `--transport app-query` for additional server filters while keeping the query fixed to the target application: `--originators '["USER_ID"]'` accepts one originator, `--process-codes '["PROCESS_CODE"]'` selects processes, and date bounds use Unix milliseconds. Available fields vary by task kind; unsupported fields fail before a request. `openyida task-center options <appType> --type originators|processes|forms --keyword TEXT --json` returns readable lookup options. The CLI never silently falls back to an unfiltered query.
647
+
648
+ After explicitly choosing a custom task page, run `openyida task-center scaffold --spec <file.json> --output <new-file.canvas.jsx>`. The configuration requires the choice record and the application's existing design file. Generated pages follow the application theme, read the current visitor's tasks, and open native pages for processing. `embedded` mode provides the same task section for an existing workbench. Compile and publish the page using the usual commands.
649
+
650
+ Page configurations can explicitly set `transport: "app-query"` and per-kind initial filters, for example `filters: { "todo": { "originators": ["USER_ID"] } }`. Generated pages include the same `filterSchema`, `normalizeFilters`, task loader, and option lookup helpers as the CLI.
651
+
652
+ Use `task-center count <appType> --type todo --transport app-query --filters-json '{"keyword":"purchase"}' --json` for a one-record query with a matching server total; an unknown total remains `null`. `task-center inspect <appType> --process-inst-id <id> [--form-uuid <id>] [--history] [--task-id <id>] --json` reads an authorized linked process and optional operation history. Personal task outcomes, selected-task outcomes and final process results remain separate. Read commands do not create custom pages or require a custom-page choice.
653
+
654
+ Scaffolds select only the needed task kinds, `fields` and `filterFields`; the default is a single todo list. `view: "progress", mode: "embedded"` provides a progress component for an existing business page, using its real linked `processInstanceId`. History and task-row expansion are opt-in. See the [workflow capability reference](yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md) for configuration and integration examples.
655
+
615
656
  ### CLI Notes
616
657
 
617
658
  #### Connector Safety and Real E2E
package/bin/yida.js CHANGED
@@ -761,6 +761,23 @@ async function main() {
761
761
  break;
762
762
  }
763
763
 
764
+ case 'cool-card': {
765
+ const { run } = require('../lib/app/cool-card');
766
+ await run(args);
767
+ break;
768
+ }
769
+ case 'data-card': {
770
+ const { run } = require('../lib/app/data-card');
771
+ await run(args);
772
+ break;
773
+ }
774
+
775
+ case 'app-settings': {
776
+ const { run } = require('../lib/app/app-settings');
777
+ await run(args);
778
+ break;
779
+ }
780
+
764
781
  case 'app-entry': {
765
782
  const { run } = require('../lib/app/app-entry');
766
783
  await run(args);
@@ -815,6 +832,11 @@ async function main() {
815
832
  break;
816
833
  }
817
834
 
835
+ case 'custom-button': {
836
+ const { run } = require('../lib/app/custom-button');
837
+ await run(args);
838
+ break;
839
+ }
818
840
  case 'form-view': {
819
841
  const { run } = require('../lib/app/form-view');
820
842
  await run(args);
@@ -856,6 +878,8 @@ async function main() {
856
878
  if (subCommand === 'evaluate' || subCommand === 'check') {
857
879
  const { run } = require('../lib/formula/evaluate');
858
880
  await run(subArgs);
881
+ } else if (['contracts', 'functions', 'types', 'inspect', 'test'].includes(subCommand)) {
882
+ await require('../lib/formula/commands').run(subCommand, subArgs);
859
883
  } else {
860
884
  throwCliUsage(t('cli.formula_usage'), t('cli.formula_example'));
861
885
  }
@@ -1115,10 +1139,11 @@ async function main() {
1115
1139
 
1116
1140
  case 'report': {
1117
1141
  const subCommand = args[0];
1118
- if (subCommand !== 'inspect') {
1119
- throwCliUsage('用法: openyida report inspect <appType> <reportId> --json');
1142
+ if (!['inspect', 'configure'].includes(subCommand)) {
1143
+ throwCliUsage('用法: openyida report <inspect|configure> ... --json');
1120
1144
  }
1121
- const { run } = require('../lib/report/inspect');
1145
+ const { run } = subCommand === 'inspect'
1146
+ ? require('../lib/report/inspect') : require('../lib/report/configure');
1122
1147
  await run(args.slice(1));
1123
1148
  break;
1124
1149
  }
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ // Server-derived provenance: channels may grow; only absence or "none" means non-AI.
4
+ function isAgentAppType(value) {
5
+ if (typeof value !== 'string') {
6
+ return false;
7
+ }
8
+ const type = value.trim().toLowerCase();
9
+ return type !== '' && type !== 'none';
10
+ }
11
+
12
+ module.exports = { isAgentAppType };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../core/cli-error');
4
+ const { t } = require('../core/i18n');
5
+ const { createAuthRef, createYidaClient } = require('../core/yida-client');
6
+ const { buildApplicationSettingsUrls } = require('./application-entry-urls');
7
+
8
+ function parseArgs(args = []) {
9
+ if (args.includes('--help') || args.includes('-h')) { return { help: true }; }
10
+ const [appType, ...rest] = args;
11
+ let section;
12
+ let json = false;
13
+ for (let i = 0; i < rest.length; i++) {
14
+ if (rest[i] === '--json' && !json) { json = true; }
15
+ else if (rest[i] === '--section' && section === undefined) { section = rest[++i]; }
16
+ else { throw new CliError(t('app_settings.usage'), { code: 'CLI_USAGE_ERROR' }); }
17
+ }
18
+ if (!/^APP_[A-Za-z0-9_-]+$/.test(appType || '') || (rest.includes('--section') && !['theme', 'basic', 'data-card', 'cool-card'].includes(section))) {
19
+ throw new CliError(t('app_settings.usage'), { code: 'CLI_USAGE_ERROR' });
20
+ }
21
+ return { appType, section };
22
+ }
23
+ async function execute({ appType, section }, client = createYidaClient({ authRef: createAuthRef() })) {
24
+ const response = await client.get(`/${appType}/query/app/getAppIncludingAecpInfo.json`, { appKey: appType });
25
+ if (response?.success !== true || response.__needLogin || response.__csrfExpired || !response.content
26
+ || response.content.appType !== appType) {
27
+ throw new CliError(t('app_settings.failed', response?.errorMsg || appType), { code: 'APP_SETTINGS_READ_FAILED' });
28
+ }
29
+ const urls = buildApplicationSettingsUrls({ appType, baseUrl: client.getAuthRef().baseUrl, systemLink: response.content.systemLink });
30
+ const key = { theme: 'themeSettingsUrl', basic: 'basicSettingsUrl', 'data-card': 'dataCardManageUrl', 'cool-card': 'coolCardManageUrl' }[section];
31
+ return { success: true, appType, urls, ...(key ? { url: urls[key] } : {}), runtimeVerified: false };
32
+ }
33
+ async function run(args = []) {
34
+ const parsed = parseArgs(args);
35
+ if (parsed.help) { console.log(t('app_settings.usage')); return; }
36
+ console.log(JSON.stringify(await execute(parsed), null, 2));
37
+ }
38
+ module.exports = { run, execute, parseArgs };
@@ -36,6 +36,14 @@ function readRenderNav(config) {
36
36
  return null;
37
37
  }
38
38
 
39
+ function buildApplicationSettingsUrls(options) {
40
+ const { adminUrl } = buildApplicationEntryUrls(options);
41
+ if (!adminUrl) { return {}; }
42
+ const url = new URL(adminUrl);
43
+ const settings = suffix => `${url.origin}${url.pathname}/appSetting/${suffix}${url.search}`;
44
+ return { themeSettingsUrl: settings('themeSetting'), basicSettingsUrl: settings('basicSetting'), dataCardManageUrl: settings('dataCardManage'), coolCardManageUrl: settings('coolCardManage') };
45
+ }
46
+
39
47
  function buildDisplayPageEntryUrls({ appType, formUuid, baseUrl, config, formType = config?.formType }) {
40
48
  if (formType !== 'display' || !/^FORM[-_][A-Za-z0-9_-]+$/.test(formUuid || '')) {return {};}
41
49
  if (config?.formType && config.formType !== 'display') {return {};}
@@ -54,4 +62,4 @@ function buildDisplayPageEntryUrls({ appType, formUuid, baseUrl, config, formTyp
54
62
  };
55
63
  }
56
64
 
57
- module.exports = { buildApplicationEntryUrls, buildDisplayPageEntryUrls, readRenderNav };
65
+ module.exports = { buildApplicationEntryUrls, buildApplicationSettingsUrls, buildDisplayPageEntryUrls, readRenderNav };
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { CliError } = require('../core/cli-error');
7
+ const { t } = require('../core/i18n');
8
+ const { createAuthRef, createYidaClient } = require('../core/yida-client');
9
+ const { buildApplicationSettingsUrls } = require('./application-entry-urls');
10
+
11
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
12
+ const validApp = value => /^APP_[A-Za-z0-9_-]+$/.test(value || '');
13
+ const validCard = value => /^form[A-Za-z0-9_-]+$/.test(value || '');
14
+ const validType = value => ['im', 'onebox'].includes(value);
15
+ const capabilities = Object.freeze({ create: true, read: true, templates: true, save: false, publish: false, send: false });
16
+
17
+ function fail(code, message, details) {
18
+ throw new CliError(t('cool_card.failed', message), { code: `COOL_CARD_${code}`, details });
19
+ }
20
+ function parseArgs(args = []) {
21
+ const positional = [], flags = {};
22
+ const allowed = {
23
+ list: ['--type', '--page', '--size', '--search'],
24
+ templates: ['--type', '--page', '--size', '--search'],
25
+ create: ['--type', '--name', '--out'], get: ['--out'],
26
+ };
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ if (['--help', '-h'].includes(arg)) { return { help: true }; }
30
+ if (arg === '--json' || ['--type', '--name', '--out', '--page', '--size', '--search'].includes(arg)) {
31
+ if (Object.hasOwn(flags, arg)) { fail('USAGE', t('cool_card.usage')); }
32
+ if (arg === '--json') { flags[arg] = true; }
33
+ else {
34
+ if (!args[i + 1] || args[i + 1].startsWith('-')) { fail('USAGE', t('cool_card.usage')); }
35
+ flags[arg] = args[++i];
36
+ }
37
+ } else if (arg.startsWith('-')) { fail('USAGE', t('cool_card.usage')); }
38
+ else { positional.push(arg); }
39
+ }
40
+ const [action, appType, cardPageCode] = positional;
41
+ if (!allowed[action] || !validApp(appType) || positional.length !== (action === 'get' ? 3 : 2)
42
+ || (cardPageCode && !validCard(cardPageCode))
43
+ || Object.keys(flags).some(key => key !== '--json' && !allowed[action].includes(key))
44
+ || (flags['--type'] !== undefined && !validType(flags['--type']))
45
+ || (['create', 'templates'].includes(action) && !validType(flags['--type']))
46
+ || (action === 'create' && !flags['--name']?.trim())) { fail('USAGE', t('cool_card.usage')); }
47
+ for (const [flag, max] of [['--page', 1000], ['--size', 100]]) {
48
+ if (flags[flag] !== undefined && (!/^\d+$/.test(flags[flag]) || +flags[flag] < 1 || +flags[flag] > max)) {
49
+ fail('USAGE', t('cool_card.usage'));
50
+ }
51
+ }
52
+ return { action, appType, cardPageCode, flags };
53
+ }
54
+ function unwrap(response, endpoint) {
55
+ if (response?.success !== true || response.__needLogin || response.__csrfExpired) {
56
+ fail('API_ERROR', response?.errorMsg || response?.message || endpoint, { endpoint, platformErrorCode: response?.errorCode });
57
+ }
58
+ return response.content;
59
+ }
60
+ function parseObject(value, label) {
61
+ let result = value;
62
+ if (typeof value === 'string') {
63
+ try { result = JSON.parse(value); } catch { fail('INVALID_RESPONSE', label); }
64
+ }
65
+ if (!object(result)) { fail('INVALID_RESPONSE', label); }
66
+ return result;
67
+ }
68
+ function validateCard(card, appType, cardPageCode) {
69
+ if (!object(card) || card.appId !== appType || !validCard(card.cardPageCode)
70
+ || (cardPageCode && card.cardPageCode !== cardPageCode) || !validType(card.cardType)) {
71
+ fail('TARGET_MISMATCH', cardPageCode || appType);
72
+ }
73
+ return card;
74
+ }
75
+ function decodeCard(card) {
76
+ // New blank resources can have no template yet. Do not call that a completed configuration.
77
+ const template = card.cardSchema ? parseObject(card.cardSchema, 'cardSchema') : null;
78
+ const editorData = template?.editorData ? parseObject(template.editorData, 'editorData') : null;
79
+ if (editorData?.schema?.componentsTree !== undefined && !Array.isArray(editorData.schema.componentsTree)) {
80
+ fail('INVALID_RESPONSE', 'componentsTree');
81
+ }
82
+ const components = new Set();
83
+ const visit = node => {
84
+ if (!object(node)) { return; }
85
+ if (typeof node.componentName === 'string') { components.add(node.componentName); }
86
+ if (Array.isArray(node.children)) { node.children.forEach(visit); }
87
+ };
88
+ (editorData?.schema?.componentsTree || []).forEach(visit);
89
+ return { template, editorData, components: [...components].sort(), variableList: editorData?.variableList || [],
90
+ templateIdsMatch: template?.templateId ? template.templateId === card.cardTemplateId : null,
91
+ publishStatus: template?.publishStatus || null };
92
+ }
93
+ function cardUrls(appType, card, baseUrl) {
94
+ const settings = buildApplicationSettingsUrls({ appType, baseUrl });
95
+ if (!settings.coolCardManageUrl) { fail('INVALID_TARGET', 'baseUrl'); }
96
+ // Link to the official editor; this command does not establish its DingTalk session.
97
+ const templateId = card.cardTemplateId;
98
+ const editorUrl = /(^|\.)aliwork\.com$/.test(new URL(baseUrl).hostname) && /^[A-Za-z0-9-]+$/.test(templateId || '')
99
+ ? `https://editor.dingtalk.com/editor?templateId=${encodeURIComponent(templateId)}&scene=yida_card&bizId=${encodeURIComponent(`${appType}.${card.cardPageCode}`)}#/` : null;
100
+ return { coolCardManageUrl: settings.coolCardManageUrl, editorUrl };
101
+ }
102
+ function userHandoff(action, appType, card, urls) {
103
+ // Existing cards enter through management so the platform can prepare the editable version.
104
+ const directEditor = action === 'create' && urls.editorUrl;
105
+ return { actor: 'user', action: action === 'create' ? 'configure-and-save' : 'review-or-edit',
106
+ target: directEditor ? 'official-editor' : 'cool-card-management',
107
+ url: directEditor || urls.coolCardManageUrl,
108
+ verificationCommand: `openyida cool-card get ${appType} ${card.cardPageCode} --json` };
109
+ }
110
+ function exportFile(filename, snapshot) {
111
+ const output = path.resolve(filename);
112
+ fs.mkdirSync(path.dirname(output), { recursive: true });
113
+ fs.writeFileSync(output, JSON.stringify(snapshot, null, 2), { mode: 0o600, flag: 'wx' });
114
+ return output;
115
+ }
116
+ async function execute(parsed, client) {
117
+ // Revalidate programmatic callers before auth or any network operation.
118
+ const flags = parsed.flags || {};
119
+ const argv = [parsed.action, parsed.appType, ...(parsed.cardPageCode ? [parsed.cardPageCode] : [])];
120
+ for (const [key, value] of Object.entries(flags)) { argv.push(key, ...(value === true ? [] : [value])); }
121
+ const checked = parseArgs(argv);
122
+ if (checked.help) { fail('USAGE', t('cool_card.usage')); }
123
+ const { action, appType } = checked;
124
+ let { cardPageCode } = checked;
125
+ if (flags['--out'] && fs.existsSync(path.resolve(flags['--out']))) { fail('OUTPUT_EXISTS', flags['--out']); }
126
+ client = client || createYidaClient({ authRef: createAuthRef() });
127
+ const prefix = `/${appType}/query/cardDesign/`;
128
+ const get = async (name, params) => unwrap(await client.get(prefix + name + '.json', params), name);
129
+ const base = { success: true, action, appType, resourceType: 'native-cool-card', capabilities };
130
+ if (['list', 'templates'].includes(action)) {
131
+ const content = await get(action === 'list' ? 'listCards' : 'listTpls', {
132
+ cardType: flags['--type'] || '', currentPage: +(flags['--page'] || 1), pageSize: +(flags['--size'] || 50),
133
+ searchName: flags['--search'] || '', ...(action === 'list' ? { excludeOfficialCard: false } : {}),
134
+ });
135
+ if (!object(content) || !Array.isArray(content.data) || !Number.isFinite(+content.totalCount) || +content.totalCount < 0) {
136
+ fail('INVALID_RESPONSE', action);
137
+ }
138
+ // Template catalog entries may be owned by a different application; listing does not grant mutation rights.
139
+ const data = content.data.map(card => {
140
+ if (action === 'list') { validateCard(card, appType); }
141
+ return Object.fromEntries(Object.entries(card).filter(([key]) => !['cardSchema', 'variableConfig'].includes(key)));
142
+ });
143
+ return { ...content, ...base, data };
144
+ }
145
+ let created;
146
+ let listedCard;
147
+ if (action === 'create') {
148
+ // Allowlist the complete write body. No entitlements, app tags, official-card switches or send options.
149
+ created = unwrap(await client.postFormOnce(prefix + 'createCard.json', {
150
+ cardName: flags['--name'], cardType: flags['--type'],
151
+ }), 'createCard');
152
+ try { validateCard(created, appType); }
153
+ catch (error) {
154
+ error.details = { ...error.details, mutationAccepted: true, inspectBeforeRetry: true, appType,
155
+ ...(validCard(created?.cardPageCode) ? { cardPageCode: created.cardPageCode } : {}) };
156
+ throw error;
157
+ }
158
+ cardPageCode = created.cardPageCode;
159
+ try {
160
+ const list = await get('listCards', { cardType: flags['--type'], currentPage: 1, pageSize: 100,
161
+ searchName: flags['--name'], excludeOfficialCard: false });
162
+ listedCard = Array.isArray(list?.data) ? list.data.find(item => item?.cardPageCode === cardPageCode) : null;
163
+ validateCard(listedCard, appType, cardPageCode);
164
+ if (listedCard.cardName !== flags['--name'] || listedCard.cardType !== flags['--type']) { fail('READBACK_MISMATCH', cardPageCode); }
165
+ } catch (error) {
166
+ error.details = { ...error.details, appType, cardPageCode, resourceCreated: true, inspectBeforeRetry: true };
167
+ throw error;
168
+ }
169
+ }
170
+ let card;
171
+ try {
172
+ card = validateCard(await get('getCardSchema', { cardAppId: appType, cardPageCode }), appType, cardPageCode);
173
+ } catch (error) {
174
+ if (!created) { throw error; }
175
+ // A real newly-created resource may have a template ID before that template exists.
176
+ // Verify its resource metadata independently; retain the failed schema read rather than inventing an empty schema.
177
+ const urls = cardUrls(appType, listedCard, client.getAuthRef().baseUrl);
178
+ const result = { ...base, cardPageCode, card: listedCard, completion: 'resource-only', readbackVerified: true,
179
+ schemaReadbackVerified: false, configurationVerified: false, deliveryVerified: false,
180
+ schemaReadbackError: { code: error.code || 'READ_FAILED', message: error.message, details: error.details },
181
+ urls, nextAction: userHandoff(action, appType, listedCard, urls) };
182
+ if (flags['--out']) { result.snapshotFile = exportFile(flags['--out'], { ...result }); }
183
+ return result;
184
+ }
185
+ try {
186
+ if (created && (card.cardType !== flags['--type'] || card.cardName !== flags['--name'])) { fail('READBACK_MISMATCH', cardPageCode); }
187
+ const decoded = decodeCard(card);
188
+ const snapshot = { resourceType: base.resourceType, appType, cardPageCode, card };
189
+ const configHash = crypto.createHash('sha256').update(JSON.stringify(snapshot)).digest('hex');
190
+ const urls = cardUrls(appType, card, client.getAuthRef().baseUrl);
191
+ const result = { ...base, cardPageCode, card, ...decoded, configHash,
192
+ readbackVerified: true, schemaReadbackVerified: decoded.template !== null,
193
+ ...(decoded.template ? {} : { completion: 'resource-only' }), configurationVerified: false, deliveryVerified: false,
194
+ urls, nextAction: userHandoff(action, appType, card, urls) };
195
+ if (flags['--out']) { result.snapshotFile = exportFile(flags['--out'], snapshot); }
196
+ return result;
197
+ } catch (error) {
198
+ if (created) { error.details = { ...error.details, appType, cardPageCode, resourceCreated: true, inspectBeforeRetry: true }; }
199
+ throw error;
200
+ }
201
+ }
202
+ async function run(args = []) {
203
+ const parsed = parseArgs(args);
204
+ if (parsed.help) { console.log(t('cool_card.usage')); return; }
205
+ console.log(JSON.stringify(await execute(parsed), null, 2));
206
+ }
207
+ module.exports = { parseArgs, execute, run, decodeCard, capabilities };
@@ -78,6 +78,7 @@ const { banner, step, label, success, fail, warn, info, error, result, usage, hi
78
78
  const { parseOpenOption: parseBrowserOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
79
79
  const { default: babelTransform } = require('../core/babel-transform');
80
80
  const { normalizeFormulaFieldRefs } = require('../formula/evaluate');
81
+ const { applyFieldFormula, formulaFields, validateSchemaFormulas } = require('../formula/config');
81
82
  const { createParseArgs } = require('./create-form/args');
82
83
  const { createDefinitionReaders } = require('./create-form/definition-reader');
83
84
  const { dispatchCreateFormCommand } = require('./create-form/commands');
@@ -458,6 +459,14 @@ function validateFormFieldDefinitions(fields, rootPath) {
458
459
  rootPath: rootPath || 'fields',
459
460
  });
460
461
  throwFieldValidationDiagnostics(fields, diagnostics, rootPath || 'fields');
462
+ function validateFormulas(items) {
463
+ for (const field of items || []) {
464
+ applyFieldFormula({}, field, normalizeFormDefinitionType(field));
465
+ if (Array.isArray(field.children)) { validateFormulas(field.children); }
466
+ }
467
+ }
468
+ validateFormulas(fields);
469
+ if (!rootPath) { validateSchemaFormulas(buildFormNodeComponents(fields)); }
461
470
  }
462
471
 
463
472
  function throwFieldValidationDiagnostics(fields, diagnostics, rootPath) {
@@ -729,7 +738,7 @@ function buildFieldComponent(field) {
729
738
  props.__gridSpan = 1;
730
739
  props.tips = i18n('', '');
731
740
  props.linkage = '';
732
- props.precision = 0;
741
+ props.precision = field.precision === undefined ? 0 : field.precision;
733
742
  props.step = 1;
734
743
  props.thousandsSeparators = false;
735
744
  props.innerAfter = field.innerAfter || '';
@@ -1240,6 +1249,8 @@ function buildFieldComponent(field) {
1240
1249
  props.placeholder = i18n(field.placeholder);
1241
1250
  }
1242
1251
 
1252
+ applyFieldFormula(props, field, componentName);
1253
+
1243
1254
  const component = {
1244
1255
  componentName: componentName,
1245
1256
  id: nodeId,
@@ -1429,6 +1440,7 @@ function resolveFieldIdReferences(fieldComponents) {
1429
1440
  if (resolvedId) {
1430
1441
  info(t('create_form.filling_rule_resolved', targetLabel, resolvedId));
1431
1442
  rule.target = resolvedId;
1443
+ rule.targetFieldId = resolvedId;
1432
1444
  } else {
1433
1445
  warn(t('create_form.filling_rule_failed', targetLabel));
1434
1446
  }
@@ -1440,6 +1452,7 @@ function resolveFieldIdReferences(fieldComponents) {
1440
1452
  if (resolvedSourceId) {
1441
1453
  info(t('create_form.filling_rule_resolved', sourceLabel, resolvedSourceId));
1442
1454
  rule.source = resolvedSourceId;
1455
+ rule.sourceFieldId = resolvedSourceId;
1443
1456
  } else {
1444
1457
  warn(t('create_form.filling_rule_failed', sourceLabel));
1445
1458
  }
@@ -1465,6 +1478,7 @@ function resolveFieldIdReferences(fieldComponents) {
1465
1478
  if (resolvedId) {
1466
1479
  info(t('create_form.table_rule_resolved', ruleIndex + 1, targetLabel, resolvedId));
1467
1480
  rule.target = resolvedId;
1481
+ rule.targetFieldId = resolvedId;
1468
1482
  } else {
1469
1483
  warn(t('create_form.table_rule_failed', targetLabel));
1470
1484
  }
@@ -1650,6 +1664,7 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1650
1664
  // 解析主题配置
1651
1665
  const themeConfig = getThemeConfig(theme || 'default', labelAlign || 'top');
1652
1666
  const fieldComponents = buildFormNodeComponents(fields);
1667
+ validateSchemaFormulas(fieldComponents);
1653
1668
  const formContainerChildren = layoutConfig.groupFields
1654
1669
  ? buildGroupedFieldComponents(fields, layoutConfig.formLayout)
1655
1670
  : fieldComponents;
@@ -2038,15 +2053,6 @@ function findFormContainer(node) {
2038
2053
  return null;
2039
2054
  }
2040
2055
 
2041
- function findFieldIndexByLabel(fields, label) {
2042
- for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
2043
- if (extractLabelText(fields[fieldIndex]) === label) {
2044
- return fieldIndex;
2045
- }
2046
- }
2047
- return -1;
2048
- }
2049
-
2050
2056
  function findFieldByLabelDeep(fields, label) {
2051
2057
  if (!Array.isArray(fields)) {
2052
2058
  return null;
@@ -2166,30 +2172,6 @@ function collectFieldReferences(fields, options) {
2166
2172
  return refs;
2167
2173
  }
2168
2174
 
2169
- function collectFieldMatchesByLabelDeep(fields, label, tableAncestors, matches) {
2170
- const result = matches || [];
2171
- const ancestors = Array.isArray(tableAncestors) ? tableAncestors : [];
2172
- if (!Array.isArray(fields)) {
2173
- return result;
2174
- }
2175
- fields.forEach(function (field, fieldIndex) {
2176
- const labelText = extractLabelText(field);
2177
- if (labelText === label) {
2178
- result.push({
2179
- field,
2180
- fields,
2181
- index: fieldIndex,
2182
- evidence: buildFieldEvidence(field, ancestors),
2183
- });
2184
- }
2185
- const nextAncestors = field && field.componentName === 'TableField'
2186
- ? ancestors.concat(compactTableAncestorEvidence(field))
2187
- : ancestors;
2188
- collectFieldMatchesByLabelDeep(field && field.children, label, nextAncestors, result);
2189
- });
2190
- return result;
2191
- }
2192
-
2193
2175
  function collectFieldCandidates(fields, tableAncestors, query) {
2194
2176
  const candidates = [];
2195
2177
  const normalizedQuery = query ? String(query).trim() : '';
@@ -2424,18 +2406,6 @@ function resolveFieldReference(fields, input, options) {
2424
2406
  };
2425
2407
  }
2426
2408
 
2427
- function requireResolvedField(fields, input, options) {
2428
- const resolution = resolveFieldReference(fields, input, options);
2429
- if (resolution.ok) {
2430
- return resolution;
2431
- }
2432
- const err = createCreateFormError(t('create_form.field_resolution_failed'), 'CREATE_FORM_FIELD_RESOLUTION_FAILED', {
2433
- diagnostics: [resolution.diagnostic],
2434
- });
2435
- err.diagnostics = [resolution.diagnostic];
2436
- throw err;
2437
- }
2438
-
2439
2409
  function outputFieldResolutionFailure(appType, formUuid, diagnostics) {
2440
2410
  const fieldDiagnostics = Array.isArray(diagnostics) ? diagnostics : [];
2441
2411
  fail(t('create_form.field_resolution_failed'));
@@ -4517,6 +4487,7 @@ function applyFieldChanges(component, changes) {
4517
4487
  props[key] = changes[key];
4518
4488
  }
4519
4489
  });
4490
+ applyFieldFormula(props, changes, component.componentName);
4520
4491
  }
4521
4492
 
4522
4493
  function ensureComponentsMap(schema, componentName) {
@@ -4565,6 +4536,7 @@ function countDataFieldDefinitions(fields) {
4565
4536
  // ── 应用修改操作(update 模式) ─────────────────────
4566
4537
 
4567
4538
  function applyChangesToSchema(schema, changes, options) {
4539
+ const previousFormulas = formulaFields(schema);
4568
4540
  const verbose = Boolean(options && options.verbose);
4569
4541
  const componentsTree = schema.pages[0].componentsTree;
4570
4542
  if (!componentsTree || componentsTree.length === 0) {
@@ -4713,6 +4685,7 @@ function applyChangesToSchema(schema, changes, options) {
4713
4685
  resolveFieldIdReferences(formFields);
4714
4686
 
4715
4687
  formContainer.children = formFields;
4688
+ validateSchemaFormulas(schema, previousFormulas);
4716
4689
  appliedChanges.diagnostics = diagnostics;
4717
4690
  return appliedChanges;
4718
4691
  }