openyida 2026.10.20-2 → 2026.10.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/yida.js +9 -0
- package/lib/app/cool-card.js +5 -0
- package/lib/core/command-manifest.js +9 -4
- package/lib/core/locales/en.js +12 -1
- package/lib/core/locales/zh.js +12 -1
- package/lib/core/sample.js +1 -0
- package/lib/core/update.js +1 -1
- package/lib/feedback/feedback.js +1 -0
- package/lib/feedback/github.js +130 -0
- package/lib/print-template/builder.js +232 -0
- package/lib/print-template/codes.js +46 -0
- package/lib/print-template/contract.js +31 -0
- package/lib/print-template/detail.js +36 -0
- package/lib/print-template/model.js +97 -0
- package/lib/print-template/print-template.js +302 -0
- package/lib/print-template/scenes.js +29 -0
- package/lib/samples/yida-print-template/approval.json +14 -0
- package/lib/samples/yida-print-template/codes.json +12 -0
- package/lib/samples/yida-print-template/delivery.json +14 -0
- package/lib/samples/yida-print-template/order.json +14 -0
- package/lib/samples/yida-print-template/quotation.json +13 -0
- package/lib/samples/yida-print-template/receipt.json +13 -0
- package/package.json +1 -1
- package/yida-skills/SKILL.md +3 -2
- package/yida-skills/skills/yida-cool-card/SKILL.md +3 -1
- package/yida-skills/skills/yida-feedback/SKILL.md +1 -1
- package/yida-skills/skills/yida-feedback/references/github-contribution.md +19 -2
- package/yida-skills/skills/yida-print-template/SKILL.md +134 -0
- package/yida-skills/skills/yida-print-template/references/detail.md +40 -0
- package/yida-skills/skills/yida-print-template/references/guide.md +35 -0
- package/yida-skills/skills/yida-print-template/references/scenarios.md +72 -0
- package/yida-skills/skills-index.json +14 -1
package/README.md
CHANGED
|
@@ -446,6 +446,8 @@ openyida feedback report --input summary.json --output-dir feedback-draft-001
|
|
|
446
446
|
|
|
447
447
|
Your AI prepares the summary from observed evidence and selected reproduction screenshots. PRs include before/after test comparisons. Before anything is published, review the final text and images, confirm sensitive information, and remove anything you do not want to share. See the [report input format](yida-skills/skills/yida-feedback/references/report-input.md). Review business information before sharing. With a GitHub account, authorize your AI to submit an Issue or contribute a verified public OpenYida fix through your personal Fork. Without an account, create a GitHub account first. If that is not possible, share the report in the AI开发者群 (AI developer group).
|
|
448
448
|
|
|
449
|
+
When you choose to submit an OpenYida Issue, the feedback Skill can check your signed-in GitHub account with `openyida feedback github status --json`. Star and a personal Fork are optional: `feedback github star --confirm` and `feedback github fork --confirm` run only after you choose them. Existing actions are reused. This check is separate from installation and automatic updates; neither action is required to submit an Issue.
|
|
450
|
+
|
|
449
451
|
<!-- OPENYIDA_COMMANDS_START -->
|
|
450
452
|
<!-- This section is generated by `npm run docs:commands`. Do not edit command rows by hand. -->
|
|
451
453
|
|
|
@@ -509,6 +511,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
|
|
|
509
511
|
| `openyida create-form add-option <appType> <formUuid> <fieldLabel> <option1> [option2] ...` | Update a form page |
|
|
510
512
|
| `openyida list-forms <appType> [--keyword <text>]` | List forms/pages in an app |
|
|
511
513
|
| `openyida aggregate-table <list\|create-empty\|inspect\|preview\|save\|publish\|status> <appType> ...` | Manage aggregate tables (virtualView) |
|
|
514
|
+
| `openyida print-template <list\|inspect\|fields\|build\|create\|copy\|save\|restore\|settings\|status\|order\|render\|files> <appType> <formUuid> [templateId] [--config <file>] [--scene <order\|quotation\|delivery\|receipt\|approval\|codes>] [--bindings <file>] [--name <title>] [--expected-hash <hash>] [--backup <file>] [--out <file>] [--status START\|STOP] [--instance <id>] [--page <n>] [--size <n>] [--allow-permission-change] [--json] \| print-template <list\|render> --detail-url <url> [--template-id <id>] [--json]` | Manage native YiDA print templates, generate document layouts, back up, restore and request PDFs |
|
|
512
515
|
| `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
516
|
| `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 |
|
|
514
517
|
| `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
|
|
@@ -628,7 +631,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
|
|
|
628
631
|
| `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 |
|
|
629
632
|
| `openyida update` | Check and update to latest version |
|
|
630
633
|
| `openyida export-conversation [options]` | Export AI conversation records |
|
|
631
|
-
| `openyida feedback <report\|setup\|url\|dismiss\|status> [options]` | Prepare local issue reports, configure feedback forms and reminders |
|
|
634
|
+
| `openyida feedback <report\|github\|setup\|url\|dismiss\|status> [options]` | Prepare local issue reports, configure feedback forms and reminders |
|
|
632
635
|
| `openyida batch <file>\|--commands "cmd1 ; cmd2" [--stop-on-error] [--json]` | Run OpenYida commands in batch |
|
|
633
636
|
| `openyida flash-to-prd --file <path> --name "<project>"` | Convert flash notes or meeting notes to a PRD prompt |
|
|
634
637
|
| `openyida ai <text\|image> [options]` | Call Yida AI text and image recognition APIs |
|
package/bin/yida.js
CHANGED
|
@@ -837,6 +837,11 @@ async function main() {
|
|
|
837
837
|
await run(args);
|
|
838
838
|
break;
|
|
839
839
|
}
|
|
840
|
+
case 'print-template': {
|
|
841
|
+
const { run } = require('../lib/print-template/print-template');
|
|
842
|
+
await run(args);
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
840
845
|
case 'form-view': {
|
|
841
846
|
const { run } = require('../lib/app/form-view');
|
|
842
847
|
await run(args);
|
|
@@ -1333,6 +1338,10 @@ async function main() {
|
|
|
1333
1338
|
}
|
|
1334
1339
|
|
|
1335
1340
|
case 'feedback': {
|
|
1341
|
+
if (args[0] === 'github') {
|
|
1342
|
+
await require('../lib/feedback/github').run(args.slice(1));
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1336
1345
|
const { run: runFeedback } = require('../lib/feedback/feedback');
|
|
1337
1346
|
await runFeedback(args);
|
|
1338
1347
|
break;
|
package/lib/app/cool-card.js
CHANGED
|
@@ -6,6 +6,7 @@ const crypto = require('crypto');
|
|
|
6
6
|
const { CliError } = require('../core/cli-error');
|
|
7
7
|
const { t } = require('../core/i18n');
|
|
8
8
|
const { createAuthRef, createYidaClient } = require('../core/yida-client');
|
|
9
|
+
const { inferEnvironmentNameFromUrl } = require('../core/env-manager');
|
|
9
10
|
const { buildApplicationSettingsUrls } = require('./application-entry-urls');
|
|
10
11
|
|
|
11
12
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -124,6 +125,10 @@ async function execute(parsed, client) {
|
|
|
124
125
|
let { cardPageCode } = checked;
|
|
125
126
|
if (flags['--out'] && fs.existsSync(path.resolve(flags['--out']))) { fail('OUTPUT_EXISTS', flags['--out']); }
|
|
126
127
|
client = client || createYidaClient({ authRef: createAuthRef() });
|
|
128
|
+
// Use the authenticated target, not a global environment preference, before creating any resource.
|
|
129
|
+
if (action === 'create' && inferEnvironmentNameFromUrl(client.getAuthRef().baseUrl) === 'alibaba') {
|
|
130
|
+
fail('UNSUPPORTED_ENVIRONMENT', t('cool_card.unsupported_environment'), { environment: 'alibaba' });
|
|
131
|
+
}
|
|
127
132
|
const prefix = `/${appType}/query/cardDesign/`;
|
|
128
133
|
const get = async (name, params) => unwrap(await client.get(prefix + name + '.json', params), name);
|
|
129
134
|
const base = { success: true, action, appType, resourceType: 'native-cool-card', capabilities };
|
|
@@ -8,6 +8,7 @@ const { getPageNavigationContract } = require('../design-plan/navigation-policy'
|
|
|
8
8
|
const { ENTRY_MODES } = require('../design-plan/entry-navigation');
|
|
9
9
|
const { DIVIDER_TYPES, DEFAULT_DIVIDER_TYPE } = require('../app/form-field-validator');
|
|
10
10
|
const { FORMULA_COMMANDS } = require('../formula/cli-contract');
|
|
11
|
+
const { printTemplateCommand, printTemplateEffects, printTemplatePermission } = require('../print-template/contract');
|
|
11
12
|
|
|
12
13
|
const SIDE_EFFECT_BASES = Object.freeze({
|
|
13
14
|
local_read: Object.freeze({
|
|
@@ -550,6 +551,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
|
|
|
550
551
|
})],
|
|
551
552
|
['cool-card', sideEffect('mixed', { mutates_yida: true, mutates_local: true, read_actions: ['list', 'templates', 'get'], mutating_actions: ['create'] })],
|
|
552
553
|
['data-card', sideEffect('mixed', { mutates_yida: true, mutates_local: true, read_actions: ['list', 'get', 'probe', 'release-check', 'refs'], mutating_actions: ['create', 'save', 'publish', 'release-get', 'release-save', 'release-test'], note: 'release-get reads broadcast settings but the native backend may initialize the application cool app; classify it conservatively as a possible mutation.' })],
|
|
554
|
+
['print-template', sideEffect('mixed', printTemplateEffects)],
|
|
553
555
|
['form-view', sideEffect('mixed', {
|
|
554
556
|
mutates_yida: true,
|
|
555
557
|
mutates_local: true,
|
|
@@ -645,8 +647,8 @@ const COMMAND_SIDE_EFFECTS = new Map([
|
|
|
645
647
|
['feedback', sideEffect('mixed', {
|
|
646
648
|
mutates_yida: true,
|
|
647
649
|
mutates_local: true,
|
|
648
|
-
read_actions: ['url', 'status'],
|
|
649
|
-
mutating_actions: ['setup', 'dismiss', 'report'],
|
|
650
|
+
read_actions: ['url', 'status', 'github status'],
|
|
651
|
+
mutating_actions: ['setup', 'dismiss', 'report', 'github star', 'github fork'],
|
|
650
652
|
})],
|
|
651
653
|
['i18n', sideEffect('mixed', {
|
|
652
654
|
mutates_yida: true,
|
|
@@ -845,6 +847,7 @@ const COMMAND_PERMISSIONS = new Map([
|
|
|
845
847
|
ask_actions: ['connect', 'run', 'disconnect', 'logout'],
|
|
846
848
|
})),
|
|
847
849
|
|
|
850
|
+
...permissionEntries(['print-template'], actionDependentPermission(printTemplatePermission)),
|
|
848
851
|
...permissionEntries(['cool-card'], actionDependentPermission({ preauthorized_actions: ['create'] })),
|
|
849
852
|
...permissionEntries(['data-card'], actionDependentPermission({ preauthorized_actions: ['create', 'save', 'publish', 'release-get'], ask_actions: ['release-save', 'release-test'] })),
|
|
850
853
|
...permissionEntries([
|
|
@@ -1609,6 +1612,7 @@ const COMMAND_GROUPS = [
|
|
|
1609
1612
|
},
|
|
1610
1613
|
output: 'json',
|
|
1611
1614
|
}),
|
|
1615
|
+
command(...printTemplateCommand),
|
|
1612
1616
|
command('form-view', ['form-view'], 'form-view <list|inspect|save|create|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]', 'help.cmd_form_view', {
|
|
1613
1617
|
managedRun: { scope: 'actions', actions: {
|
|
1614
1618
|
list: { scope: 'app', appPosition: 1 },
|
|
@@ -2047,11 +2051,12 @@ const COMMAND_GROUPS = [
|
|
|
2047
2051
|
command('export-conversation', ['export-conversation'], 'export-conversation [options]', 'help.cmd_export_conversation', { managedRun: { scope: 'denied', reason: 'scope_not_audited' },
|
|
2048
2052
|
requiresLogin: false,
|
|
2049
2053
|
}),
|
|
2050
|
-
command('feedback', ['feedback'], 'feedback <report|setup|url|dismiss|status> [options]', 'help.cmd_feedback', { managedRun: { scope: 'actions', actions: { report: { scope: 'local' } } },
|
|
2054
|
+
command('feedback', ['feedback'], 'feedback <report|github|setup|url|dismiss|status> [options]', 'help.cmd_feedback', { managedRun: { scope: 'actions', actions: { report: { scope: 'local' } } },
|
|
2055
|
+
permission: actionDependentPermission({ preauthorized_actions: ['report', 'url', 'status', 'dismiss', 'github status'], ask_actions: ['setup', 'github star', 'github fork'] }),
|
|
2051
2056
|
requiresLogin: false,
|
|
2052
2057
|
output: 'text|json',
|
|
2053
2058
|
examples: ['openyida feedback report --input summary.json --output-dir feedback-draft-001'],
|
|
2054
|
-
notes: ['report prepares local review drafts only; no login, source checkout, update check or network submission.'],
|
|
2059
|
+
notes: ['report prepares local review drafts only; no login, source checkout, update check or network submission. github status [--json] checks the authenticated github.com user, Star and personal Fork through an existing gh installation; it performs no writes. github star|fork --confirm [--json] requires the user to explicitly choose that account action. Missing gh/login, unknown status and declined actions never block Issue submission. Already applied actions are reused; writes are single-attempt with readback. No auto-update/install hook, Issue submission, clone or git remote modification. Managed runs permit only offline report preparation.'],
|
|
2055
2060
|
}),
|
|
2056
2061
|
command('batch', ['batch'], 'batch <file>|--commands "cmd1 ; cmd2" [--stop-on-error] [--json]', 'help.cmd_batch', { managedRun: { scope: 'denied', reason: 'scope_not_audited' },
|
|
2057
2062
|
output: 'text|json',
|
package/lib/core/locales/en.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
print_template: {
|
|
8
|
+
'description': 'Manage native YiDA print templates, generate document layouts, back up, restore and request PDFs',
|
|
9
|
+
'error': 'Print template operation failed: {0}',
|
|
10
|
+
'usage': 'Usage: openyida print-template <list|inspect|fields|build|create|copy|save|restore|settings|status|order|render|files> <appType> <formUuid> [templateId]. Use --config <file>, build --scene order|quotation|delivery|receipt|approval|codes --bindings <file>, create --config <file>, --name <title>, --expected-hash <inspect-hash>, --backup <file>, --out <file>, --status START|STOP, --instance <id>, --page <n>, --size <n>, --allow-permission-change, --json as documented. For native record links: list|render --detail-url <url> [--template-id <id>].'
|
|
11
|
+
},
|
|
7
12
|
custom_button: {
|
|
8
13
|
'usage': 'Usage: openyida custom-button <list|inspect|options|create|update|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]',
|
|
9
14
|
'invalid_config': 'Invalid configuration or argument: {0}',
|
|
@@ -15,7 +20,7 @@ module.exports = {
|
|
|
15
20
|
'readback_mismatch': 'Saved custom button does not match readback. Keep the backup and inspect remote state.'
|
|
16
21
|
},
|
|
17
22
|
dws_external: {'note': 'dws is an independent external tool. OpenYida only shows installation instructions; it does not download or install it.', 'install_guide': 'Show official DingTalk CLI installation instructions'},
|
|
18
|
-
cool_card: {'description': 'Create and read native YiDA cool cards (interactive and pinned)', 'usage': 'Usage: openyida cool-card <list|templates|create|get> <appType> [cardPageCode] [--type im|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]', 'failed': 'Native cool card operation stopped: {0}'},
|
|
23
|
+
cool_card: {'unsupported_environment': 'The Alibaba Group edition does not support building cool cards.', 'description': 'Create and read native YiDA cool cards (interactive and pinned)', 'usage': 'Usage: openyida cool-card <list|templates|create|get> <appType> [cardPageCode] [--type im|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]', 'failed': 'Native cool card operation stopped: {0}'},
|
|
19
24
|
data_card: {'description':'Create, configure, publish and distribute native YiDA data cards','usage':'Usage: 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]','failed':'Native data card operation stopped: {0}','conflict':'The draft changed. Read it again before saving or publishing.','mismatch':'Readback does not match the expected schema or published state. Keep the backup and inspect the card before retrying.'},
|
|
20
25
|
app_settings: {'description':'Read theme, frontend/backend, native data card and cool card management links','usage':'Usage: openyida app-settings <appType> [--section theme|basic|data-card|cool-card] [--json]','failed':'Application settings could not be read: {0}'},
|
|
21
26
|
form_view: {
|
|
@@ -2507,3 +2512,9 @@ module.exports.task_center = {
|
|
|
2507
2512
|
'filter_carbonCreateFrom': 'Copied from',
|
|
2508
2513
|
'filter_carbonCreateTo': 'Copied to'
|
|
2509
2514
|
};
|
|
2515
|
+
|
|
2516
|
+
module.exports.feedback_github = {
|
|
2517
|
+
usage: 'Usage: openyida feedback github <status|star|fork> [--confirm] [--json]',
|
|
2518
|
+
confirmation: 'Star or Fork requires the user to explicitly choose that action before --confirm. Neither is required to submit an Issue.',
|
|
2519
|
+
failed: 'The GitHub action could not be verified. Inspect its status before retrying; this does not block Issue submission.',
|
|
2520
|
+
};
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
print_template: {
|
|
8
|
+
'description': '管理宜搭原生打印模板、按字段生成多种单据、备份恢复与生成PDF',
|
|
9
|
+
'error': '打印模板操作失败:{0}',
|
|
10
|
+
'usage': 'Usage: openyida print-template <list|inspect|fields|build|create|copy|save|restore|settings|status|order|render|files> <appType> <formUuid> [templateId]. Use --config <file>, build --scene order|quotation|delivery|receipt|approval|codes --bindings <file>, create --config <file>, --name <title>, --expected-hash <inspect-hash>, --backup <file>, --out <file>, --status START|STOP, --instance <id>, --page <n>, --size <n>, --allow-permission-change, --json as documented. For native record links: list|render --detail-url <url> [--template-id <id>].'
|
|
11
|
+
},
|
|
7
12
|
custom_button: {
|
|
8
13
|
'usage': '用法:openyida custom-button <list|inspect|options|create|update|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]',
|
|
9
14
|
'invalid_config': '配置或参数无效:{0}',
|
|
@@ -15,7 +20,7 @@ module.exports = {
|
|
|
15
20
|
'readback_mismatch': '保存后的配置与回读不一致,请保留备份并检查远端状态。'
|
|
16
21
|
},
|
|
17
22
|
dws_external: {'note': 'dws 是独立的外部工具;OpenYida 仅提供安装指引,不下载或安装它。', 'install_guide': '显示钉钉 CLI 官方安装指引'},
|
|
18
|
-
cool_card: {'description': '创建和读取宜搭原生酷卡片(交互卡片与置顶卡片)', 'usage': '用法: openyida cool-card <list|templates|create|get> <appType> [cardPageCode] [--type im|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]', 'failed': '原生酷卡片操作已停止:{0}'},
|
|
23
|
+
cool_card: {'unsupported_environment': '集团版不支持酷卡片搭建。', 'description': '创建和读取宜搭原生酷卡片(交互卡片与置顶卡片)', 'usage': '用法: openyida cool-card <list|templates|create|get> <appType> [cardPageCode] [--type im|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]', 'failed': '原生酷卡片操作已停止:{0}'},
|
|
19
24
|
data_card: {'description':'创建、配置、发布原生数据卡片并管理投放播报','usage':'用法: 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]','failed':'原生数据卡片操作已停止:{0}','conflict':'卡片草稿已发生变化,请重新读取后再保存或发布。','mismatch':'回读配置或发布状态与预期不一致,请保留备份并检查卡片后再重试。'},
|
|
20
25
|
app_settings: {'description':'获取主题、业务前后台、原生数据卡片和酷卡片管理入口','usage':'用法: openyida app-settings <appType> [--section theme|basic|data-card|cool-card] [--json]','failed':'无法读取应用设置:{0}'},
|
|
21
26
|
form_view: {
|
|
@@ -2461,3 +2466,9 @@ module.exports.task_center = {
|
|
|
2461
2466
|
'filter_carbonCreateFrom': '抄送时间起',
|
|
2462
2467
|
'filter_carbonCreateTo': '抄送时间止'
|
|
2463
2468
|
};
|
|
2469
|
+
|
|
2470
|
+
module.exports.feedback_github = {
|
|
2471
|
+
usage: '用法:openyida feedback github <status|star|fork> [--confirm] [--json]',
|
|
2472
|
+
confirmation: 'Star 或 Fork 需要用户明确选择后再传入 --confirm;提交 Issue 不要求这两项操作。',
|
|
2473
|
+
failed: 'GitHub 操作未能核实完成,请先查询状态;这不影响提交 Issue。',
|
|
2474
|
+
};
|
package/lib/core/sample.js
CHANGED
|
@@ -23,6 +23,7 @@ const { t } = require('./i18n');
|
|
|
23
23
|
// ── Sample 索引表 ─────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
const SAMPLES = {
|
|
26
|
+
'yida-print-template': Object.fromEntries(require('../print-template/scenes').names.map(name => [name, `${name}.json`])),
|
|
26
27
|
'yida-chart': {
|
|
27
28
|
'line-trend': 'line-trend.js',
|
|
28
29
|
'multi-bar-compare': 'multi-bar-compare.js',
|
package/lib/core/update.js
CHANGED
|
@@ -159,7 +159,7 @@ function getAutoUpdateIntervalMs(env = process.env) {
|
|
|
159
159
|
|
|
160
160
|
function shouldSkipAutoUpdateCommand(command, args = []) {
|
|
161
161
|
// Report preparation must work offline even in a global npm installation.
|
|
162
|
-
if (command === 'feedback' && args[0]
|
|
162
|
+
if (command === 'feedback' && ['report', 'github'].includes(args[0])) {return true;}
|
|
163
163
|
if (!command || ['--help', '-h', '--version', '-v'].includes(command)) {
|
|
164
164
|
return true;
|
|
165
165
|
}
|
package/lib/feedback/feedback.js
CHANGED
|
@@ -51,6 +51,7 @@ function printUsage() {
|
|
|
51
51
|
openyida feedback setup <appType> [--name "OpenYida 体验反馈"] [--path /o/openyida-feedback] [--open|--no-open]
|
|
52
52
|
openyida feedback setup --create-app [--app-name "OpenYida 体验反馈"] [--name "OpenYida 体验反馈"]
|
|
53
53
|
openyida feedback report --input <summary.json> --output-dir <new-directory>
|
|
54
|
+
openyida feedback github <status|star|fork> [--confirm] [--json]
|
|
54
55
|
openyida feedback url [--tool Codex] [--model gpt-5] [--command publish] [--diagnostics "..."]
|
|
55
56
|
openyida feedback dismiss --today|--forever|--reset
|
|
56
57
|
openyida feedback status
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const { CliError } = require('../core/cli-error');
|
|
5
|
+
const { t } = require('../core/i18n');
|
|
6
|
+
|
|
7
|
+
const REPOSITORY = 'openyida/openyida';
|
|
8
|
+
const links = Object.freeze({ repository: `https://github.com/${REPOSITORY}`,
|
|
9
|
+
issue: `https://github.com/${REPOSITORY}/issues/new`, fork: `https://github.com/${REPOSITORY}/fork` });
|
|
10
|
+
|
|
11
|
+
/** Parse explicit account actions; submitting an Issue never implies Star or Fork consent. */
|
|
12
|
+
function parseArgs(args = []) {
|
|
13
|
+
const [action, ...flags] = args;
|
|
14
|
+
if (!['status', 'star', 'fork'].includes(action) || new Set(flags).size !== flags.length
|
|
15
|
+
|| flags.some(flag => !['--json', '--confirm'].includes(flag))
|
|
16
|
+
|| (action === 'status' && flags.includes('--confirm'))) {
|
|
17
|
+
throw new CliError(t('feedback_github.usage'), { code: 'FEEDBACK_GITHUB_USAGE' });
|
|
18
|
+
}
|
|
19
|
+
if (action !== 'status' && !flags.includes('--confirm')) {
|
|
20
|
+
throw new CliError(t('feedback_github.confirmation'), { code: 'FEEDBACK_GITHUB_CONFIRMATION_REQUIRED' });
|
|
21
|
+
}
|
|
22
|
+
return { action, confirm: flags.includes('--confirm') };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Run gh against github.com only. Never expose its raw output or credential diagnostics. */
|
|
26
|
+
function request(method, endpoint, options, fields = []) {
|
|
27
|
+
const spawn = options.spawnSyncFn || spawnSync;
|
|
28
|
+
const result = spawn('gh', ['api', '--hostname', 'github.com', '--include', '--method', method, endpoint, ...fields], {
|
|
29
|
+
encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024,
|
|
30
|
+
env: { ...(options.env || process.env), GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1', GIT_TERMINAL_PROMPT: '0' },
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
});
|
|
33
|
+
const output = String(result.stdout || '');
|
|
34
|
+
const status = Number(output.match(/^HTTP\/[\d.]+\s+(\d{3})\b/)?.[1]) || null;
|
|
35
|
+
const separator = output.match(/\r?\n\r?\n/);
|
|
36
|
+
let data = null;
|
|
37
|
+
try { data = separator ? JSON.parse(output.slice(separator.index + separator[0].length)) : null; }
|
|
38
|
+
catch { /* Empty responses and invalid JSON remain unverified. */ }
|
|
39
|
+
return { status, data, ok: result.status === 0 && status >= 200 && status < 300,
|
|
40
|
+
unavailable: result.error?.code === 'ENOENT' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Verify ownership and upstream before offering an existing personal Fork for contributions. */
|
|
44
|
+
function readFork(response, login) {
|
|
45
|
+
if (response.status === 404) { return { state: 'absent', url: null }; }
|
|
46
|
+
if (!response.ok) { return { state: 'unknown', url: null }; }
|
|
47
|
+
const repo = response.data;
|
|
48
|
+
if (repo?.fork === true && repo.parent?.full_name?.toLowerCase() === REPOSITORY
|
|
49
|
+
&& repo.full_name?.toLowerCase() === `${login}/openyida`.toLowerCase()
|
|
50
|
+
&& repo.owner?.login?.toLowerCase() === login.toLowerCase()) {
|
|
51
|
+
return { state: 'ready', url: `https://github.com/${login}/openyida` };
|
|
52
|
+
}
|
|
53
|
+
return { state: 'conflict', url: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Include renamed personal Forks; never infer absence from an incomplete or failed query. */
|
|
57
|
+
function inspectFork(login, options) {
|
|
58
|
+
const query = 'query { repository(owner:"openyida", name:"openyida") { forks(first:100, affiliations:[OWNER]) { nodes { nameWithOwner isFork owner { login } parent { nameWithOwner } } pageInfo { hasNextPage } } } }';
|
|
59
|
+
const response = request('POST', 'graphql', options, ['--raw-field', `query=${query}`]);
|
|
60
|
+
const connection = response.data?.data?.repository?.forks;
|
|
61
|
+
if (!response.ok || response.data?.errors || !Array.isArray(connection?.nodes)
|
|
62
|
+
|| typeof connection.pageInfo?.hasNextPage !== 'boolean') { return { state: 'unknown', url: null }; }
|
|
63
|
+
for (const repo of connection.nodes) {
|
|
64
|
+
const [owner, name, extra] = String(repo?.nameWithOwner || '').split('/');
|
|
65
|
+
if (repo?.isFork === true && repo.parent?.nameWithOwner?.toLowerCase() === REPOSITORY
|
|
66
|
+
&& owner.toLowerCase() === login.toLowerCase() && repo.owner?.login?.toLowerCase() === login.toLowerCase()
|
|
67
|
+
&& /^[A-Za-z0-9_.-]+$/.test(name || '') && !extra) {
|
|
68
|
+
return { state: 'ready', url: `https://github.com/${owner}/${name}` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (connection.nodes.length || connection.pageInfo.hasNextPage) { return { state: 'unknown', url: null }; }
|
|
72
|
+
return readFork(request('GET', `repos/${login}/openyida`, options), login);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Read account, Star and personal Fork state without changing it; unknown is never treated as absent. */
|
|
76
|
+
function inspect(options) {
|
|
77
|
+
const account = request('GET', 'user', options);
|
|
78
|
+
const login = account.data?.login;
|
|
79
|
+
const base = { success: true, repository: REPOSITORY, links, mutationPerformed: false, issueRequiresStarOrFork: false };
|
|
80
|
+
if (!account.ok || account.data?.type !== 'User' || !/^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(login || '')) {
|
|
81
|
+
return { ...base, authenticated: false, reason: account.unavailable ? 'gh_unavailable' : 'account_unverified',
|
|
82
|
+
login: null, starred: null, fork: { state: 'unknown', url: null }, suggestedActions: [] };
|
|
83
|
+
}
|
|
84
|
+
const star = request('GET', `user/starred/${REPOSITORY}`, options);
|
|
85
|
+
const starred = star.ok && star.status === 204 ? true : star.status === 404 ? false : null;
|
|
86
|
+
const fork = inspectFork(login, options);
|
|
87
|
+
return { ...base, authenticated: true, login, starred, fork,
|
|
88
|
+
suggestedActions: [...(starred === false ? ['star'] : []), ...(fork.state === 'absent' ? ['fork'] : [])] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Execute one explicitly authorized action, then read it back. No retry, clone, remote change or Issue submission. */
|
|
92
|
+
function execute(parsed, options = {}) {
|
|
93
|
+
const { action } = parseArgs([parsed.action, ...(parsed.confirm === true ? ['--confirm'] : [])]);
|
|
94
|
+
if ((options.env || process.env).OPENYIDA_MANAGED_RUN === '1') {
|
|
95
|
+
throw new CliError(t('feedback_github.failed'), { code: 'FEEDBACK_GITHUB_MANAGED_DENIED' });
|
|
96
|
+
}
|
|
97
|
+
const result = inspect(options);
|
|
98
|
+
if (action === 'status') { return result; }
|
|
99
|
+
const fail = (stage, mutationPerformed = false) => {
|
|
100
|
+
throw new CliError(t('feedback_github.failed'), { code: 'FEEDBACK_GITHUB_FAILED',
|
|
101
|
+
details: { stage, action, mutationPerformed, inspectBeforeRetry: true } });
|
|
102
|
+
};
|
|
103
|
+
if (!result.authenticated) { fail('account'); }
|
|
104
|
+
if (action === 'star') {
|
|
105
|
+
if (result.starred === true) { return { ...result, action, alreadyApplied: true }; }
|
|
106
|
+
if (result.starred !== false) { fail('star-status'); }
|
|
107
|
+
const written = request('PUT', `user/starred/${REPOSITORY}`, options);
|
|
108
|
+
if (!written.ok) { fail('star-write', 'unknown'); }
|
|
109
|
+
const verified = request('GET', `user/starred/${REPOSITORY}`, options);
|
|
110
|
+
if (!verified.ok || verified.status !== 204) { fail('star-readback', true); }
|
|
111
|
+
return { ...result, action, starred: true, mutationPerformed: true, readbackVerified: true,
|
|
112
|
+
suggestedActions: result.suggestedActions.filter(item => item !== 'star') };
|
|
113
|
+
}
|
|
114
|
+
if (result.fork.state === 'ready') { return { ...result, action, alreadyApplied: true }; }
|
|
115
|
+
if (result.fork.state !== 'absent') { fail('fork-status'); }
|
|
116
|
+
const created = request('POST', `repos/${REPOSITORY}/forks`, options, ['--field', 'default_branch_only=true']);
|
|
117
|
+
if (!created.ok) { fail('fork-write', 'unknown'); }
|
|
118
|
+
const fork = readFork(request('GET', `repos/${result.login}/openyida`, options), result.login);
|
|
119
|
+
return { ...result, action, fork, mutationPerformed: true, readbackVerified: fork.state === 'ready',
|
|
120
|
+
completion: fork.state === 'ready' ? 'verified' : 'pending-verification',
|
|
121
|
+
suggestedActions: result.suggestedActions.filter(item => item !== 'fork') };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** CLI entrypoint. The confirmation flag attests to the user's explicit choice of this action. */
|
|
125
|
+
function run(args = []) {
|
|
126
|
+
const parsed = parseArgs(args);
|
|
127
|
+
console.log(JSON.stringify(execute(parsed), null, 2));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { parseArgs, execute, run };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { object, fail, json, safe, label, content } = require('./model');
|
|
4
|
+
const codes = require('./codes');
|
|
5
|
+
const TYPES = new Set(['value', 'date', 'img', 'table', 'commonCheckbox', 'yidaApproveNode']);
|
|
6
|
+
const FONTS = ['宋体', 'Arial', '新宋', '微软雅黑', '黑体'];
|
|
7
|
+
// The native PDF renderer recognizes these editor values, not localized font labels.
|
|
8
|
+
const FONT_VALUES = { 宋体: 'SimSun', Arial: 'Arial Unicode MS', 新宋: 'NSimSun', 微软雅黑: 'Microsoft YaHei', 黑体: 'SimHei' };
|
|
9
|
+
|
|
10
|
+
/** Escape literal values in XHTML and native dynamic-field attributes. */
|
|
11
|
+
function escape(value) {
|
|
12
|
+
return String(value ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Fail on unrecognized configuration instead of silently dropping requested features. */
|
|
16
|
+
function keys(value, allowed, name) {
|
|
17
|
+
if (!object(value) || Object.keys(value).some(key => !allowed.includes(key))) { fail('INVALID_CONFIG', name); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Build printable field groups from the same dictionaries consumed by the native editor. */
|
|
21
|
+
function fields(raw, codeCapability = { enabled: false, reason: 'capability_unverified' }) {
|
|
22
|
+
if (!object(raw) || !Array.isArray(raw.formField) && !Array.isArray(raw.forms) || !Array.isArray(raw.systemField)) {
|
|
23
|
+
fail('INVALID_RESPONSE', 'print dictionary');
|
|
24
|
+
}
|
|
25
|
+
const normalize = (items, group, parent) => (items || []).map(item => ({ ...item, label: label(item.description), group,
|
|
26
|
+
...(codes.eligible(item) ? { codeFormats: codeCapability.enabled ? [...codes.FORMATS] : [],
|
|
27
|
+
codeUnavailableReason: codeCapability.enabled ? null : codeCapability.reason } : {}),
|
|
28
|
+
parentKey: parent || null, children: normalize(item.children, group, item.key) }));
|
|
29
|
+
return [...normalize(raw.formField, 'form'), ...normalize(raw.systemField, 'system'),
|
|
30
|
+
...normalize(raw.remarkRecord ? [raw.remarkRecord] : [], 'system'), ...normalize(raw.approvedNode, 'approval'),
|
|
31
|
+
...(raw.forms || []).flatMap(form => normalize(form.formField, 'form', form.id).map(item => ({ ...item, formKey: form.id })))];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Resolve exact IDs or unambiguous names, scoped to a parent table when supplied. */
|
|
35
|
+
function resolve(items, ref) {
|
|
36
|
+
if (typeof ref !== 'string' || !ref) { fail('INVALID_CONFIG', 'field reference'); }
|
|
37
|
+
const byId = items.filter(item => item.key === ref);
|
|
38
|
+
const matches = byId.length ? byId : items.filter(item => item.label === ref);
|
|
39
|
+
if (matches.length !== 1) { fail('FIELD_REFERENCE', ref, { candidates: matches.map(item => ({ key: item.key, label: item.label })) }); }
|
|
40
|
+
return matches[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reject executable HTML/Velocity input; native macros remain data references only. */
|
|
44
|
+
function html(value) {
|
|
45
|
+
if (typeof value !== 'string' || /<\s*\/?\s*(?:script|iframe|object|embed|form|link|meta|style)\b|\bon\w+\s*=|javascript\s*:|data\s*:|#\s*(?:set|evaluate|parse|include|foreach|if|macro)\b|\$!?\{/i.test(value)) {
|
|
46
|
+
fail('INVALID_CONFIG', 'static XHTML');
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Validate a complete native body while preserving advanced native attributes verbatim. */
|
|
52
|
+
function validateNative(raw, dictionary) {
|
|
53
|
+
const result = content(raw);
|
|
54
|
+
safe(result);
|
|
55
|
+
const attrs = result.termJsonAttr;
|
|
56
|
+
const entries = result.dictionaryList;
|
|
57
|
+
const ids = attrs.map(item => item?.id);
|
|
58
|
+
if (ids.some(id => typeof id !== 'string' || !/^[\w-]+$/.test(id)) || new Set(ids).size !== ids.length
|
|
59
|
+
|| entries.length !== attrs.length || new Set(entries.map(item => item.id)).size !== entries.length) {
|
|
60
|
+
fail('INVALID_CONFIG', 'native component IDs');
|
|
61
|
+
}
|
|
62
|
+
const all = [];
|
|
63
|
+
const visit = items => items.forEach(item => { all.push(item); visit(item.children || []); });
|
|
64
|
+
visit(dictionary);
|
|
65
|
+
for (const attr of attrs) {
|
|
66
|
+
const entry = entries.find(item => item.id === attr.id);
|
|
67
|
+
const code = entry && codes.resolveNative(entry.key, all);
|
|
68
|
+
const labelKey = typeof attr.content === 'string' && /exttype="label"/.test(attr.content)
|
|
69
|
+
? /fieldkey="([^"]+)"/.exec(attr.content)?.[1] : null;
|
|
70
|
+
if (!TYPES.has(attr.type) || !entry || entry.type !== attr.type || (code ? attr.type !== 'img' : !all.some(field => field.key === (labelKey || entry.key)))
|
|
71
|
+
|| typeof attr.content !== 'string' || !result.termContents.includes(` #macro_${attr.id} `)) {
|
|
72
|
+
fail('INVALID_CONFIG', `native component: ${attr.id}`);
|
|
73
|
+
}
|
|
74
|
+
// Native placeholders use ${label} in disabled input values. They are never evaluated locally.
|
|
75
|
+
html(attr.content.replace(/\$!?\{[^}]*\}/g, 'FIELD'));
|
|
76
|
+
// Repeating tables keep child image keys inside the parent macro, outside dictionaryList.
|
|
77
|
+
for (const [input] of attr.content.matchAll(/<input\b[^>]*>/gi)) {
|
|
78
|
+
const key = /\bkey\s*=\s*(["'])(.*?)\1/i.exec(input)?.[2];
|
|
79
|
+
if (codes.resolveNative(key, all) && !/\bkeytype\s*=\s*(["'])img\1/i.test(input)) {
|
|
80
|
+
fail('INVALID_CONFIG', 'native code image');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const macros = [...result.termContents.matchAll(/#macro_([\w-]+)/g)].map(match => match[1]);
|
|
85
|
+
if (macros.some(id => !ids.includes(id))) { fail('INVALID_CONFIG', 'unresolved macro'); }
|
|
86
|
+
html(result.termContents.replace(/ #macro_[\w-]+ /g, 'FIELD'));
|
|
87
|
+
return { termContents: result.termContents, termJsonAttr: attrs, dictionaryList: entries };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Compile named sections into the existing editor's native macros, never browser-only placeholders. */
|
|
91
|
+
function build(config, dictionary) {
|
|
92
|
+
keys(config, ['title', 'style', 'font', 'fontSize', 'width', 'blocks'], 'document');
|
|
93
|
+
const theme = config.style || 'document';
|
|
94
|
+
if (!['document', 'compact', 'formal'].includes(theme) || config.font !== undefined && !FONTS.includes(config.font)
|
|
95
|
+
|| config.title !== undefined && typeof config.title !== 'string'
|
|
96
|
+
|| config.fontSize !== undefined && (!Number.isFinite(config.fontSize) || config.fontSize < 8 || config.fontSize > 72)
|
|
97
|
+
|| config.width !== undefined && (!Number.isFinite(config.width) || config.width < 160 || config.width > 2000)
|
|
98
|
+
|| !Array.isArray(config.blocks) || !config.blocks.length) { fail('INVALID_CONFIG', 'document style / blocks'); }
|
|
99
|
+
const padding = theme === 'compact' ? '3px 5px' : '7px 9px';
|
|
100
|
+
const border = theme === 'formal' ? '#333333' : '#777777';
|
|
101
|
+
const cellStyle = `border:1px solid ${border};padding:${padding};vertical-align:top;`;
|
|
102
|
+
const tableStyle = `border-collapse:collapse;width:100%;border:1px solid ${border};`;
|
|
103
|
+
const termJsonAttr = [];
|
|
104
|
+
const dictionaryList = [];
|
|
105
|
+
let counter = 0;
|
|
106
|
+
const next = type => `${type}_${++counter}`;
|
|
107
|
+
const scalar = (field, options = {}, nested = false) => {
|
|
108
|
+
if (options.render !== undefined) { field = codes.transform(field, options.render); }
|
|
109
|
+
if (field.type === 'commonCheckbox') {
|
|
110
|
+
if (nested || !Array.isArray(field.children) || !field.children.length) { fail('UNSUPPORTED', `checkbox: ${field.label}`); }
|
|
111
|
+
const id = next('commonCheckbox');
|
|
112
|
+
const body = `<span id="${id}" key="${escape(field.key)}" contenteditable="false" class="render-commonCheckbox">${field.children.map(child => `<label key="${escape(child.key)}" keytype="${escape(child.type)}" keydescription="${escape(child.label)}">☐\${${escape(child.label)}}</label>`).join(' ')}</span>`;
|
|
113
|
+
termJsonAttr.push({ id, type: 'commonCheckbox', content: body });
|
|
114
|
+
dictionaryList.push({ id, type: 'commonCheckbox', key: field.key, description: field.label });
|
|
115
|
+
return ` #macro_${id} `;
|
|
116
|
+
}
|
|
117
|
+
if (!['value', 'date', 'img'].includes(field.type)) { fail('UNSUPPORTED', `${field.label}: ${field.type}`); }
|
|
118
|
+
const id = next(field.type);
|
|
119
|
+
let extra = '';
|
|
120
|
+
const width = options.width ?? (field.codeFormat === 'qrcode' ? 128 : field.codeFormat === 'barcode' ? 240 : undefined);
|
|
121
|
+
const height = options.height ?? (field.codeFormat === 'qrcode' ? 128 : field.codeFormat === 'barcode' ? 80 : undefined);
|
|
122
|
+
if (field.type === 'date') {
|
|
123
|
+
const format = options.format || 'yyyy-MM-dd';
|
|
124
|
+
if (!(field.children || []).some(child => child.key === format)) { fail('INVALID_CONFIG', `date format: ${format}`); }
|
|
125
|
+
extra += ` dateformat="${escape(format)}"`;
|
|
126
|
+
} else if (options.format !== undefined) { fail('INVALID_CONFIG', `format: ${field.label}`); }
|
|
127
|
+
if (width !== undefined || height !== undefined) {
|
|
128
|
+
if (field.type !== 'img' || [width, height].filter(v => v !== undefined).some(v => !Number.isFinite(v) || v <= 0 || v > 2000)) {
|
|
129
|
+
fail('INVALID_CONFIG', 'image dimensions');
|
|
130
|
+
}
|
|
131
|
+
extra += width ? ` width="${width}" td-width="${Number.isInteger(width) ? `${width}.0` : width}"` : '';
|
|
132
|
+
extra += height ? ` height="${height}" td-height="${Number.isInteger(height) ? `${height}.0` : height}"` : '';
|
|
133
|
+
}
|
|
134
|
+
const input = `<input id="${id}" key="${escape(field.key)}" keytype="${field.type}" type="text" disabled="disabled" class="render-${field.type}" style="border:0;background-color:transparent;color:inherit;font:inherit;" title="${escape(field.label)}(${escape(field.key)})" value="\${${escape(field.label)}}"${extra}/>`;
|
|
135
|
+
if (nested) { return input; }
|
|
136
|
+
termJsonAttr.push({ id, type: field.type, content: input });
|
|
137
|
+
dictionaryList.push({ id, type: field.type, key: field.key, description: field.label });
|
|
138
|
+
return ` #macro_${id} `;
|
|
139
|
+
};
|
|
140
|
+
const dynamic = (ref, options = {}) => {
|
|
141
|
+
const field = resolve(dictionary, ref);
|
|
142
|
+
if (field.type === 'table' || field.group === 'approval') {
|
|
143
|
+
if (['format', 'width', 'height', 'render'].some(key => options[key] !== undefined)) { fail('INVALID_CONFIG', 'table options'); }
|
|
144
|
+
const id = next(field.group === 'approval' ? 'yidaApproveNode' : 'table');
|
|
145
|
+
const selected = options.columns || field.children.map(child => child.key);
|
|
146
|
+
if (!Array.isArray(selected) || !selected.length) { fail('INVALID_CONFIG', 'table columns'); }
|
|
147
|
+
const columns = selected.map(col => {
|
|
148
|
+
if (typeof col === 'string') { col = { field: col }; }
|
|
149
|
+
keys(col, ['field', 'label', 'format', 'width', 'height', 'render'], 'table column');
|
|
150
|
+
return { ...col, definition: resolve(field.children, col.field) };
|
|
151
|
+
});
|
|
152
|
+
const row = `<tr class="render-table row">${columns.map(col => `<td style="${cellStyle}">${scalar(col.definition, col, true)}</td>`).join('')}</tr>`;
|
|
153
|
+
const head = `<tr class="render-table head">${columns.map(col => `<td style="${cellStyle}">${escape(col.label ?? col.definition.label)}</td>`).join('')}</tr>`;
|
|
154
|
+
const type = field.group === 'approval' ? 'yidaApproveNode' : 'table';
|
|
155
|
+
const body = type === 'table'
|
|
156
|
+
? `<table id="${id}" key="${escape(field.key)}" class="render-table" style="${tableStyle}"><tbody>${head}${row}</tbody></table>`
|
|
157
|
+
: `<div id="${id}" key="${escape(field.key)}" class="render-yidaApproveNode"><div>${columns.map(col => `<p>${escape(col.label ?? col.definition.label)}:${scalar(col.definition, col, true)}</p>`).join('')}</div></div>`;
|
|
158
|
+
termJsonAttr.push({ id, type, content: body });
|
|
159
|
+
dictionaryList.push({ id, type, key: field.key, description: field.label });
|
|
160
|
+
return ` #macro_${id} `;
|
|
161
|
+
}
|
|
162
|
+
return scalar(field, options);
|
|
163
|
+
};
|
|
164
|
+
const blocks = config.blocks.map(block => {
|
|
165
|
+
if (!object(block)) { fail('INVALID_CONFIG', 'block'); }
|
|
166
|
+
if (block.type === 'heading' || block.type === 'text') {
|
|
167
|
+
keys(block, ['type', 'text', 'align'], block.type);
|
|
168
|
+
if (typeof block.text !== 'string' || block.align && !['left', 'center', 'right'].includes(block.align)) { fail('INVALID_CONFIG', block.type); }
|
|
169
|
+
const tag = block.type === 'heading' ? 'h2' : 'p';
|
|
170
|
+
return `<${tag} style="text-align:${block.align || 'left'};">${escape(block.text)}</${tag}>`;
|
|
171
|
+
}
|
|
172
|
+
if (block.type === 'fields') {
|
|
173
|
+
keys(block, ['type', 'fields', 'columns'], 'fields block');
|
|
174
|
+
const count = block.columns ?? 1;
|
|
175
|
+
if (![1, 2, 3, 4].includes(count) || !Array.isArray(block.fields) || !block.fields.length) { fail('INVALID_CONFIG', 'fields block'); }
|
|
176
|
+
const cells = block.fields.map(ref => {
|
|
177
|
+
const item = typeof ref === 'string' ? { field: ref } : ref;
|
|
178
|
+
keys(item, ['field', 'label', 'format', 'width', 'height', 'render'], 'field');
|
|
179
|
+
const field = resolve(dictionary, item.field);
|
|
180
|
+
if (field.type === 'table' || field.group === 'approval') { fail('INVALID_CONFIG', 'use table block'); }
|
|
181
|
+
return `<td style="${cellStyle}width:${100 / count}%;">${escape(item.label ?? field.label)}:${dynamic(item.field, item)}</td>`;
|
|
182
|
+
});
|
|
183
|
+
const rows = [];
|
|
184
|
+
for (let i = 0; i < cells.length; i += count) { rows.push(`<tr>${cells.slice(i, i + count).join('')}${`<td style="${cellStyle}"></td>`.repeat(Math.max(0, count - cells.slice(i, i + count).length))}</tr>`); }
|
|
185
|
+
return `<table style="${tableStyle}"><tbody>${rows.join('')}</tbody></table>`;
|
|
186
|
+
}
|
|
187
|
+
if (block.type === 'table' || block.type === 'field') {
|
|
188
|
+
keys(block, ['type', 'field', 'columns', 'format', 'width', 'height', 'render'], block.type);
|
|
189
|
+
const field = resolve(dictionary, block.field);
|
|
190
|
+
if (block.type === 'table' && field.type !== 'table' && field.group !== 'approval') { fail('INVALID_CONFIG', 'table field'); }
|
|
191
|
+
if (block.columns !== undefined && field.type !== 'table' && field.group !== 'approval') { fail('INVALID_CONFIG', 'columns require table'); }
|
|
192
|
+
return dynamic(block.field, block);
|
|
193
|
+
}
|
|
194
|
+
if (codes.FORMATS.includes(block.type)) {
|
|
195
|
+
keys(block, ['type', 'field', 'width', 'height'], block.type);
|
|
196
|
+
const width = block.width ?? (block.type === 'qrcode' ? 128 : 240);
|
|
197
|
+
const height = block.height ?? (block.type === 'qrcode' ? 128 : 80);
|
|
198
|
+
const macro = dynamic(block.field, { render: block.type, width, height });
|
|
199
|
+
return `<table style="border-collapse:collapse;width:${width}px;"><tbody><tr><td style="width:${width}px;height:${height}px;padding:0;border:0;">${macro}</td></tr></tbody></table>`;
|
|
200
|
+
}
|
|
201
|
+
if (block.type === 'html') {
|
|
202
|
+
keys(block, ['type', 'html'], 'html block');
|
|
203
|
+
// Static XHTML supports arbitrary arrangements; dynamic tokens must resolve to live fields.
|
|
204
|
+
const result = html(block.html).replace(/\{\{([^{}]+)\}\}/g, (_, ref) => dynamic(ref.trim()));
|
|
205
|
+
if (/\{\{|\}\}/.test(result)) { fail('INVALID_CONFIG', 'unclosed field token'); }
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
if (block.type === 'image') {
|
|
209
|
+
keys(block, ['type', 'url', 'width', 'height'], 'image block');
|
|
210
|
+
if (typeof block.url !== 'string' || !/^https:\/\//.test(block.url) || [block.width, block.height].filter(v => v !== undefined).some(v => !Number.isFinite(v) || v <= 0 || v > 2000)) { fail('INVALID_CONFIG', 'image'); }
|
|
211
|
+
return `<p><img src="${escape(block.url)}"${block.width ? ` width="${block.width}"` : ''}${block.height ? ` height="${block.height}"` : ''}/></p>`;
|
|
212
|
+
}
|
|
213
|
+
if (block.type === 'divider') { keys(block, ['type'], 'divider'); return '<hr/>'; }
|
|
214
|
+
fail('UNSUPPORTED', `block: ${block.type}`);
|
|
215
|
+
});
|
|
216
|
+
const title = config.title ? `<h1 style="text-align:center;">${escape(config.title)}</h1>` : '';
|
|
217
|
+
const termContents = `<div style="font-family:${FONT_VALUES[config.font || '宋体']};font-size:${config.fontSize || 14}px;line-height:1.6;${config.width ? `width:${config.width}px;` : ''}">${title}${blocks.join('<p></p>')}</div>`;
|
|
218
|
+
return validateNative({ termContents, termJsonAttr, dictionaryList }, dictionary);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Accept authored documents or complete native exports; partial native bodies are rejected. */
|
|
222
|
+
function compile(raw, dictionary) {
|
|
223
|
+
if (object(raw) && Object.hasOwn(raw, 'document')) {
|
|
224
|
+
keys(raw, ['document'], 'config');
|
|
225
|
+
return build(raw.document, dictionary);
|
|
226
|
+
}
|
|
227
|
+
keys(raw, ['termContents', 'termJsonAttr', 'dictionaryList'], 'native config');
|
|
228
|
+
if (['termContents', 'termJsonAttr', 'dictionaryList'].some(key => !Object.hasOwn(raw, key))) { fail('INVALID_CONFIG', 'complete native config required'); }
|
|
229
|
+
return validateNative({ ...raw, termJsonAttr: json(raw.termJsonAttr, 'termJsonAttr'), dictionaryList: json(raw.dictionaryList, 'dictionaryList') }, dictionary);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = { fields, resolve, build, compile, validateNative, escape };
|