openyida 2026.10.22 → 2026.10.23-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 (32) hide show
  1. package/README.md +2 -1
  2. package/bin/yida.js +5 -0
  3. package/lib/app/form-message.js +352 -0
  4. package/lib/app/services/canvas-page-schema-builder.js +43 -1
  5. package/lib/core/command-manifest.js +33 -3
  6. package/lib/core/locales/en.js +12 -0
  7. package/lib/core/locales/zh.js +12 -0
  8. package/lib/core/task-center.js +32 -4
  9. package/lib/navigation/page-navigation.js +62 -0
  10. package/lib/samples/openyida-scaffold/canvas-navigation.jsx +6 -2
  11. package/lib/task-center/contract.js +26 -1
  12. package/lib/task-center/loader.js +5 -0
  13. package/lib/task-center/navigation.js +41 -0
  14. package/lib/task-center/scaffold.js +9 -0
  15. package/package.json +1 -1
  16. package/yida-skills/SKILL.md +2 -1
  17. package/yida-skills/references/yida-api.md +3 -1
  18. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +4 -2
  19. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +2 -0
  20. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +2 -0
  21. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +2 -2
  22. package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +29 -1
  23. package/yida-skills/skills/yida-create-page/SKILL.md +1 -0
  24. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  25. package/yida-skills/skills/yida-design/references/application-style-library.md +2 -0
  26. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +4 -0
  27. package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +27 -44
  28. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +3 -1
  29. package/yida-skills/skills/yida-form-message/SKILL.md +53 -0
  30. package/yida-skills/skills/yida-form-message/references/guide.md +111 -0
  31. package/yida-skills/skills/yida-task-center/SKILL.md +3 -1
  32. package/yida-skills/skills-index.json +13 -0
package/README.md CHANGED
@@ -516,6 +516,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
516
516
  | `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 |
517
517
  | `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 |
518
518
  | `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 |
519
+ | `openyida form-message <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [messageId] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native form notifications: inspect triggers, recipients and templates, then safely maintain bindings |
519
520
  | `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
520
521
  | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
521
522
  | `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
@@ -533,7 +534,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
533
534
  | Command | Description |
534
535
  |---------|-------------|
535
536
  | `openyida data <query\|get\|create\|update\|transfer> <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) |
536
- | `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.) |
537
+ | `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|link\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
537
538
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
538
539
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
539
540
  | `openyida read-dingtalk-tingji <taskUuid> [--json]` | Fetch DingTalk Tingji details by task UUID |
package/bin/yida.js CHANGED
@@ -837,6 +837,11 @@ async function main() {
837
837
  await run(args);
838
838
  break;
839
839
  }
840
+ case 'form-message': {
841
+ const { run } = require('../lib/app/form-message');
842
+ await run(args);
843
+ break;
844
+ }
840
845
  case 'print-template': {
841
846
  const { run } = require('../lib/print-template/print-template');
842
847
  await run(args);
@@ -0,0 +1,352 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { isDeepStrictEqual } = require('util');
7
+ const { CliError } = require('../core/cli-error');
8
+ const { t } = require('../core/i18n');
9
+ const { resolveProjectRoot } = require('../core/utils');
10
+ const { createYidaClient } = require('../core/yida-client');
11
+
12
+ const ACTIONS = ['list', 'inspect', 'options', 'create', 'update', 'delete'];
13
+ const RECEIVER_TYPES = new Set(['employee', 'role', 'formField', 'variable']);
14
+ const FORM_TRIGGERS = new Set(['FORM_DATA_CREATE', 'FORM_DATA_EDIT', 'FORM_DATA_DELETE', 'FORM_COMMENT']);
15
+ const SYSTEM_VARIABLES = new Set(['originator']);
16
+ const MESSAGE_FIELD_COMPONENTS = new Set([
17
+ 'TextareaField', 'EmployeeField', 'SelectField', 'MultiSelectField', 'DateField', 'TextField',
18
+ 'RadioField', 'NumberField', 'CheckboxField', 'RateField', 'CascadeDateField', 'DepartmentSelectField',
19
+ ]);
20
+ const RECIPIENT_FIELD_COMPONENTS = new Set(['EmployeeField']);
21
+ const ID = /^[A-Za-z0-9_-]+$/;
22
+ const TEMPLATES = Object.freeze({
23
+ workNotice: { uuid: 'system_default_work_notice', type: 'workNotice', content: true, target: 'person' },
24
+ detailImage: { uuid: '8fbf8a81-c775-4570-b367-86d4d719a85f', type: 'card', fields: true, target: 'person' },
25
+ imageCard: { uuid: '39caf717-df43-46e1-98f9-564fdd1dc789', type: 'card', content: true, target: 'person' },
26
+ detailText: { uuid: '97bd36d0-64d8-4c50-96e8-f02312655700', type: 'card', fields: true, target: 'person' },
27
+ commentCard: { uuid: 'c298cd88-0de6-44c3-9023-c93e0d7db9ac', type: 'card', content: true, target: 'person' },
28
+ pinnedImage: { uuid: 'cdcdf076-3625-4da7-9a61-571591594209', type: 'ceilingCard', content: true, target: 'group' },
29
+ pinnedText: { uuid: '68240de5-8d9e-416a-b5b2-c71dca0ab703', type: 'ceilingCard', content: true, target: 'group' },
30
+ });
31
+ const TEMPLATE_BY_UUID = new Map(Object.entries(TEMPLATES).map(([key, item]) => [item.uuid, { key, ...item }]));
32
+
33
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
34
+ const clone = value => JSON.parse(JSON.stringify(value));
35
+
36
+ /** Build a stable fingerprint for concurrency checks and destructive actions. */
37
+ function hash(value) {
38
+ const stable = item => Array.isArray(item) ? item.map(stable) : object(item)
39
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, stable(item[key])])) : item;
40
+ return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
41
+ }
42
+
43
+ function fail(code, key, detail = '', details) {
44
+ throw new CliError(t(`form_message.${key}`, detail), { code: `FORM_MESSAGE_${code}`, details });
45
+ }
46
+
47
+ function safe(value) {
48
+ if (!value || typeof value !== 'object') {return;}
49
+ for (const key of Object.keys(value)) {
50
+ if (['__proto__', 'prototype', 'constructor'].includes(key)) {fail('INVALID_CONFIG', 'invalid_config', key);}
51
+ safe(value[key]);
52
+ }
53
+ }
54
+
55
+ function parseJson(value, label) {
56
+ try { return typeof value === 'string' ? JSON.parse(value) : value; }
57
+ catch (_) { return fail('INVALID_CONFIG', 'invalid_config', label); }
58
+ }
59
+
60
+ function unwrap(response, endpoint) {
61
+ if (!response || response.success !== true || response.__needLogin || response.__csrfExpired || response.content === false) {
62
+ fail('API_ERROR', 'api_error', response?.errorMsg || response?.message || endpoint, {
63
+ endpoint, platformErrorCode: response?.errorCode || null, needLogin: !!response?.__needLogin,
64
+ });
65
+ }
66
+ return response.content;
67
+ }
68
+
69
+ /** Normalize a native binding while retaining fields required for lossless backup and update. */
70
+ function normalize(raw) {
71
+ if (!object(raw) || raw.id === undefined || raw.id === null || !String(raw.id).trim()) {
72
+ fail('INVALID_RESPONSE', 'invalid_response', 'id');
73
+ }
74
+ const cardConfig = raw.cardConfig ? parseJson(raw.cardConfig, 'cardConfig') : {};
75
+ if (!object(cardConfig)) {fail('INVALID_RESPONSE', 'invalid_response', 'cardConfig');}
76
+ const recipients = [];
77
+ const add = (source, type) => String(source || '').split(',').map(value => value.trim()).filter(Boolean)
78
+ .forEach(value => recipients.push({ type, value }));
79
+ add(raw.persons, 'employee'); add(raw.roleId, 'role'); add(raw.formField, 'formField'); add(raw.variable, 'variable');
80
+ const nativeGroups = Array.isArray(cardConfig.groups) ? clone(cardConfig.groups) : [];
81
+ const template = TEMPLATE_BY_UUID.get(cardConfig.templateUuid) || {};
82
+ const buttons = Array.isArray(cardConfig.buttons) ? clone(cardConfig.buttons) : [];
83
+ return {
84
+ id: String(raw.id),
85
+ formUuid: raw.formUuid || null,
86
+ trigger: raw.phase || null,
87
+ messageType: raw.messageType || 'NORMAL',
88
+ recipients,
89
+ groups: nativeGroups,
90
+ template: {
91
+ key: template.key || null,
92
+ uuid: cardConfig.templateUuid || raw.messageUuid || null,
93
+ type: cardConfig.templateType || template.type || null,
94
+ title: cardConfig.cardTitle || '',
95
+ content: cardConfig.mainContent || '',
96
+ fields: Array.isArray(cardConfig.datas) ? clone(cardConfig.datas) : [],
97
+ image: Array.isArray(cardConfig.messageImage) ? clone(cardConfig.messageImage) : [],
98
+ contentType: cardConfig.messageContentType || (raw.messageUuid ? 'TEMPLATE' : 'CUSTOM'),
99
+ platformTemplateUuid: raw.messageUuid && !String(raw.messageUuid).includes('#') ? raw.messageUuid : null,
100
+ button: buttons[0] || null,
101
+ },
102
+ advanced: {
103
+ openMore: !!cardConfig.openMore,
104
+ activityIds: String(raw.activityId || '').split(',').filter(Boolean),
105
+ condition: raw.conditionJson ? parseJson(raw.conditionJson, 'conditionJson') : null,
106
+ processId: raw.processId || null,
107
+ outcome: String(raw.outResult || '').split(',').filter(Boolean),
108
+ currentGroup: raw.curDingGroup === 'y',
109
+ sendWorkNotice: raw.isSendMessage === 'y',
110
+ },
111
+ native: clone(raw),
112
+ };
113
+ }
114
+
115
+ function parseArgs(args) {
116
+ const positional = [];
117
+ const flags = {};
118
+ for (let i = 0; i < args.length; i++) {
119
+ const arg = args[i];
120
+ if (Object.hasOwn(flags, arg)) {fail('USAGE', 'usage');}
121
+ if (['--json', '--yes', '--help', '-h'].includes(arg)) { flags[arg] = true; continue; }
122
+ if (['--config', '--backup', '--expected-hash'].includes(arg)) {
123
+ if (!args[i + 1] || args[i + 1].startsWith('-')) {fail('USAGE', 'usage');}
124
+ flags[arg] = args[++i]; continue;
125
+ }
126
+ if (arg.startsWith('-')) {fail('USAGE', 'usage');}
127
+ positional.push(arg);
128
+ }
129
+ if (flags['--help'] || flags['-h']) {return { help: true };}
130
+ const [action, appType, formUuid, messageId] = positional;
131
+ const targeted = ['inspect', 'update', 'delete'].includes(action);
132
+ if (!ACTIONS.includes(action) || positional.length !== (targeted ? 4 : 3)
133
+ || !ID.test(appType || '') || !ID.test(formUuid || '') || (targeted && !ID.test(messageId || ''))
134
+ || (['create', 'update'].includes(action) && !flags['--config'])
135
+ || (action === 'delete' && (!flags['--yes'] || !flags['--expected-hash']))) {fail('USAGE', 'usage');}
136
+ const allowed = { list: [], inspect: [], options: [], create: ['--config', '--backup'],
137
+ update: ['--config', '--backup', '--expected-hash'], delete: ['--yes', '--backup', '--expected-hash'] };
138
+ if (Object.keys(flags).some(key => key !== '--json' && !allowed[action].includes(key))) {fail('USAGE', 'usage');}
139
+ return { action, appType, formUuid, messageId, flags };
140
+ }
141
+
142
+ function fieldId(field) { return field.fieldId || field.id || field.componentId; }
143
+ function fieldType(field) { return field.componentName || field.fieldType || field.type; }
144
+
145
+ function normalizeTemplate(value) {
146
+ if (!object(value)) {fail('INVALID_CONFIG', 'invalid_config', 'template');}
147
+ const registry = value.key ? TEMPLATES[value.key] : TEMPLATE_BY_UUID.get(value.uuid);
148
+ if (!registry || (value.uuid && value.uuid !== registry.uuid) || (value.type && value.type !== registry.type)) {
149
+ fail('INVALID_CONFIG', 'invalid_config', 'template.key / template.uuid / template.type');
150
+ }
151
+ const result = { ...value, key: value.key || TEMPLATE_BY_UUID.get(registry.uuid).key,
152
+ uuid: registry.uuid, type: registry.type };
153
+ result.title = String(result.title || '').trim();
154
+ result.content = String(result.content || '').trim();
155
+ result.fields = result.fields || [];
156
+ result.image = result.image || [];
157
+ result.contentType = 'CUSTOM';
158
+ result.platformTemplateUuid = null;
159
+ result.button = result.button || null;
160
+ if (!result.title || result.title.replace(/\$!\{\w+\}/g, '').length > 50) {fail('INVALID_CONFIG', 'invalid_config', 'template.title');}
161
+ if (registry.content && !result.content) {fail('INVALID_CONFIG', 'invalid_config', 'template.content');}
162
+ if (registry.fields && (!Array.isArray(result.fields) || !result.fields.length)) {fail('INVALID_CONFIG', 'invalid_config', 'template.fields');}
163
+ if (!Array.isArray(result.fields) || !Array.isArray(result.image)) {fail('INVALID_CONFIG', 'invalid_config', 'template.fields / template.image');}
164
+ if (result.button !== undefined && result.button !== null) {
165
+ if (!object(result.button) || typeof result.button.name !== 'string'
166
+ || !['formInstDetail', 'comment', 'custom'].includes(result.button.type)) {fail('INVALID_CONFIG', 'invalid_config', 'template.button');}
167
+ if (result.button.type === 'custom') {
168
+ let url; try { url = new URL(result.button.value); } catch (_) { fail('INVALID_CONFIG', 'invalid_config', 'template.button.value'); }
169
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) {fail('INVALID_CONFIG', 'invalid_config', 'template.button.value');}
170
+ }
171
+ }
172
+ return { result, registry };
173
+ }
174
+
175
+ /** Validate references against the target form metadata before a mutation. */
176
+ function validateConfig(value, metadata) {
177
+ if (!object(value) || !FORM_TRIGGERS.has(value.trigger)) {
178
+ fail('UNSUPPORTED_TRIGGER', 'unsupported_trigger', value?.trigger || '');
179
+ }
180
+ const formType = metadata.form.formType;
181
+ if (formType === 'process') {fail('UNSUPPORTED_FORM', 'unsupported_form', formType);}
182
+ const { result: template, registry } = normalizeTemplate(value.template);
183
+ const recipients = value.recipients || [];
184
+ const groups = value.groups || [];
185
+ if (!Array.isArray(recipients) || !Array.isArray(groups)) {fail('INVALID_CONFIG', 'invalid_config', 'recipients / groups');}
186
+ if (registry.target === 'person' && !recipients.length) {fail('INVALID_CONFIG', 'invalid_config', 'recipients');}
187
+ if (registry.target === 'group' && !groups.length) {fail('INVALID_CONFIG', 'invalid_config', 'groups');}
188
+ if (recipients.length > 100) {fail('INVALID_CONFIG', 'invalid_config', 'recipients (max 100)');}
189
+ const seen = new Set();
190
+ for (const receiver of recipients) {
191
+ if (!object(receiver) || !RECEIVER_TYPES.has(receiver.type) || !ID.test(String(receiver.value || ''))
192
+ || seen.has(`${receiver.type}:${receiver.value}`)) {fail('INVALID_CONFIG', 'invalid_config', 'recipients');}
193
+ seen.add(`${receiver.type}:${receiver.value}`);
194
+ if (receiver.type === 'variable' && !SYSTEM_VARIABLES.has(receiver.value)) {fail('INVALID_CONFIG', 'invalid_config', `recipient variable: ${receiver.value}`);}
195
+ if (receiver.type === 'formField' && !metadata.recipientFields.some(field => fieldId(field) === receiver.value)) {
196
+ fail('INVALID_CONFIG', 'invalid_config', `recipient field: ${receiver.value}`);
197
+ }
198
+ }
199
+ for (const group of groups) {
200
+ if (!object(group) || !ID.test(String(group.cid || group.id || ''))) {fail('INVALID_CONFIG', 'invalid_config', 'groups');}
201
+ }
202
+ for (const item of template.fields) {
203
+ if (!object(item) || !['variable', 'formField'].includes(item.type) || typeof item.title !== 'string' || !item.title.trim()
204
+ || !ID.test(String(item.value || ''))) {fail('INVALID_CONFIG', 'invalid_config', 'template.fields');}
205
+ if (item.type === 'variable' && !metadata.systemFields.some(field => field.value === item.value)) {
206
+ fail('INVALID_CONFIG', 'invalid_config', `template variable: ${item.value}`);
207
+ }
208
+ if (item.type === 'formField' && !metadata.messageFields.some(field => fieldId(field) === item.value)) {
209
+ fail('INVALID_CONFIG', 'invalid_config', `template field: ${item.value}`);
210
+ }
211
+ }
212
+ return { trigger: value.trigger, recipients: clone(recipients), groups: clone(groups), template };
213
+ }
214
+
215
+ function patchConfig(base, patch) {
216
+ if (!object(patch) || !Object.keys(patch).length || Object.keys(patch).some(key => !['trigger', 'recipients', 'groups', 'template'].includes(key))) {
217
+ fail('INVALID_CONFIG', 'invalid_config', 'trigger, recipients, groups, template');
218
+ }
219
+ safe(patch);
220
+ const next = { ...base, ...patch };
221
+ if (patch.template) {next.template = { ...base.template, ...patch.template };}
222
+ return next;
223
+ }
224
+
225
+ function editable(message) {
226
+ return { trigger: message.trigger, recipients: clone(message.recipients), groups: clone(message.groups), template: clone(message.template) };
227
+ }
228
+
229
+ function serialize(config, formUuid, messageId, native = {}) {
230
+ const byType = type => config.recipients.filter(item => item.type === type).map(item => item.value).join(',');
231
+ const registered = config.template.key ? TEMPLATES[config.template.key] : TEMPLATE_BY_UUID.get(config.template.uuid);
232
+ const nativeCardConfig = native.cardConfig ? parseJson(native.cardConfig, 'cardConfig') : {};
233
+ const cardConfig = {
234
+ ...nativeCardConfig,
235
+ openMore: false,
236
+ groups: config.groups,
237
+ messageImage: config.template.image,
238
+ cardTitle: config.template.title,
239
+ mainContent: config.template.content,
240
+ datas: config.template.fields,
241
+ templateUuid: config.template.uuid || registered.uuid,
242
+ templateType: config.template.type || registered.type,
243
+ messageContentType: 'CUSTOM',
244
+ buttons: config.template.button ? [config.template.button] : [],
245
+ };
246
+ return {
247
+ ...clone(native),
248
+ service: native.service || '', formUuid, phase: config.trigger, outResult: native.outResult || '',
249
+ formField: byType('formField'), studentField: native.studentField || '', roleId: byType('role'),
250
+ persons: byType('employee'), varField: native.varField || '', messageUuid: '', id: messageId || '',
251
+ variable: byType('variable'), email: native.email || '', formFieldEmail: native.formFieldEmail || '',
252
+ activityId: native.activityId || '', messageType: native.messageType || 'NORMAL',
253
+ curDingGroup: native.curDingGroup || 'n', isSendMessage: native.isSendMessage || 'n',
254
+ processId: native.processId || '', conditionJson: native.conditionJson || '', cardConfig: JSON.stringify(cardConfig),
255
+ };
256
+ }
257
+
258
+ async function execute(parsed, client = createYidaClient()) {
259
+ const { action, appType, formUuid, messageId, flags } = parsed;
260
+ const prefix = `/${appType}/query/`;
261
+ const read = async (endpoint, params = {}) => unwrap(await client.get(prefix + endpoint, { formUuid, ...params }), endpoint);
262
+ const list = async () => {
263
+ const rows = await read('formMsgTpl/listFormMessageBind.json');
264
+ if (!Array.isArray(rows)) {fail('INVALID_RESPONSE', 'invalid_response', 'messages');}
265
+ const messages = rows.map(normalize);
266
+ if (messages.some(item => item.formUuid && item.formUuid !== formUuid)
267
+ || new Set(messages.map(item => item.id)).size !== messages.length) {fail('INVALID_RESPONSE', 'invalid_response', 'messages');}
268
+ return messages;
269
+ };
270
+ const metadata = async () => {
271
+ const info = await read('formdesign/getFormAndAppInfo.json', { appType });
272
+ const form = info?.formDatas?.values?.find(item => item.formUuid === formUuid);
273
+ if (!form || !Array.isArray(form.fields)) {fail('INVALID_RESPONSE', 'invalid_response', 'form metadata');}
274
+ const fields = form.fields;
275
+ return {
276
+ form: { formUuid, formType: form.formType, title: form.title, processCode: form.processCode || null },
277
+ triggers: [...FORM_TRIGGERS],
278
+ readOnlyTriggers: ['RECUR_JOB_FIELD', 'INST_START', 'INST_COMPLETE', 'INST_NODE', 'TASK_GENERATE', 'TASK_COMPLETED'],
279
+ templates: Object.entries(TEMPLATES).map(([key, item]) => ({ key, ...item })),
280
+ systemFields: [
281
+ { value: 'formInstId', componentName: 'TextField' }, { value: 'originatorName', componentName: 'TextField' },
282
+ { value: 'originator', componentName: 'TextField' }, { value: 'createTime', componentName: 'DateField' },
283
+ { value: 'modifiedTime', componentName: 'DateField' }, { value: 'serialNo', componentName: 'TextField' },
284
+ { value: 'formTitle', componentName: 'TextField' }, { value: 'title', componentName: 'TextField' },
285
+ ],
286
+ messageFields: fields.filter(field => MESSAGE_FIELD_COMPONENTS.has(fieldType(field))),
287
+ recipientFields: fields.filter(field => RECIPIENT_FIELD_COMPONENTS.has(fieldType(field))),
288
+ recipientVariables: [...SYSTEM_VARIABLES],
289
+ supportsToggle: false,
290
+ };
291
+ };
292
+ // Link to the form's native notification settings in the authenticated environment.
293
+ const configUrl = new URL(`/${appType}/admin/${formUuid}/settings/messageSetting`, client.getAuthRef().baseUrl).href;
294
+ const output = { success: true, action, appType, formUuid, configUrl };
295
+ if (action === 'options') {return { ...output, options: await metadata() };}
296
+ const beforeList = await list();
297
+ const before = messageId ? beforeList.find(item => item.id === messageId) : null;
298
+ if (messageId && !before) {fail('NOT_FOUND', 'not_found', messageId);}
299
+ if (action === 'list') {return { ...output, messages: beforeList, totalCount: beforeList.length, supportsToggle: false };}
300
+ if (action === 'inspect') {return { ...output, messageId, message: before, configHash: hash(editable(before)), supportsToggle: false };}
301
+ if (flags['--expected-hash'] && flags['--expected-hash'] !== hash(editable(before))) {fail('CONFLICT', 'conflict');}
302
+
303
+ let expected;
304
+ if (action !== 'delete') {
305
+ let patch;
306
+ try { patch = parseJson(fs.readFileSync(path.resolve(flags['--config']), 'utf8'), 'config'); }
307
+ catch (error) { if (error instanceof CliError) {throw error;} fail('INVALID_CONFIG', 'invalid_config', 'config file'); }
308
+ const defaults = { trigger: 'FORM_DATA_CREATE', recipients: [{ type: 'variable', value: 'originator' }], groups: [],
309
+ template: { key: 'workNotice', title: '', content: '', fields: [], image: [], button: null } };
310
+ expected = validateConfig(patchConfig(before ? editable(before) : defaults, patch), await metadata());
311
+ if (before && isDeepStrictEqual(editable(before), expected)) {
312
+ return { ...output, messageId, changed: false, message: before, configHash: hash(expected), readbackVerified: true };
313
+ }
314
+ }
315
+ const snapshot = beforeList.map(item => [item.id, hash(item.native)]);
316
+ if (!isDeepStrictEqual(snapshot, (await list()).map(item => [item.id, hash(item.native)]))) {fail('CONFLICT', 'conflict');}
317
+ const backupPath = flags['--backup'] ? path.resolve(flags['--backup']) : path.join(resolveProjectRoot().projectRoot,
318
+ '.cache', 'openyida', 'form-message', `${Date.now()}-${crypto.randomUUID()}.json`);
319
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
320
+ fs.writeFileSync(backupPath, JSON.stringify({ appType, formUuid, action, messages: beforeList }, null, 2), { flag: 'wx', mode: 0o600 });
321
+ const details = { target: { appType, formUuid, ...(messageId ? { messageId } : {}) }, backupPath,
322
+ retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true };
323
+ const endpoint = action === 'delete' ? 'formMsgTpl/removeMessageBind.json' : 'formMsgTpl/saveOrUpdateMessageBind.json';
324
+ const params = action === 'delete' ? { id: messageId } : serialize(expected, formUuid, messageId, before?.native);
325
+ try { unwrap(await client.postFormOnce(prefix + endpoint, params), endpoint); }
326
+ catch (error) { throw new CliError(error.message, { code: error.code || 'FORM_MESSAGE_WRITE_FAILED', details: { ...error.details, ...details } }); }
327
+ try {
328
+ const after = await list();
329
+ if (action === 'delete') {
330
+ if (after.some(item => item.id === messageId)) {fail('READBACK_MISMATCH', 'readback_mismatch');}
331
+ return { ...output, messageId, changed: true, backupPath, readbackVerified: true };
332
+ }
333
+ const added = after.filter(item => !beforeList.some(old => old.id === item.id));
334
+ const saved = action === 'create' ? (added.length === 1 ? added[0] : null) : after.find(item => item.id === messageId);
335
+ if (!saved || !isDeepStrictEqual(editable(saved), expected)) {fail('READBACK_MISMATCH', 'readback_mismatch');}
336
+ return { ...output, messageId: saved.id, changed: true, backupPath, readbackVerified: true,
337
+ message: saved, configHash: hash(expected) };
338
+ } catch (error) {
339
+ throw new CliError(error.message, { code: error.code || 'FORM_MESSAGE_READBACK_FAILED',
340
+ details: { ...details, mutationAccepted: true, sideEffectState: 'accepted_unverified' } });
341
+ }
342
+ }
343
+
344
+ async function run(args, options = {}) {
345
+ const parsed = parseArgs(args);
346
+ if (parsed.help) { process.stdout.write(t('form_message.usage') + '\n'); return; }
347
+ const result = await execute(parsed, options.client);
348
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
349
+ return result;
350
+ }
351
+
352
+ module.exports = { ACTIONS, TEMPLATES, run, execute, parseArgs, normalize, validateConfig, patchConfig, serialize, hash, editable };
@@ -10,7 +10,16 @@ const {
10
10
  findEmojiInValue,
11
11
  } = require('../../core/no-emoji-detector');
12
12
 
13
- const CANVAS_YIDA_API_BRIDGE_SOURCE = `var OPENYIDA_YIDA_FORM_API_METHODS = [
13
+ const { createPageNavigation } = require('../../navigation/page-navigation');
14
+ const { createTaskCenterContract } = require('../../task-center/contract');
15
+ const { createTaskCenterLoader } = require('../../task-center/loader');
16
+ const { createTaskCenterNavigation } = require('../../task-center/navigation');
17
+
18
+ const CANVAS_YIDA_API_BRIDGE_SOURCE = `${createPageNavigation.toString()}
19
+ ${createTaskCenterNavigation.toString()}
20
+ ${createTaskCenterContract.toString()}
21
+ ${createTaskCenterLoader.toString()}
22
+ var OPENYIDA_YIDA_FORM_API_METHODS = [
14
23
  'saveFormData',
15
24
  'updateFormData',
16
25
  'searchFormDataIds',
@@ -239,8 +248,15 @@ function openyidaCreateRouterBridge(utils) {
239
248
 
240
249
  function openyidaCreateUtilsBridge(context, yidaBridge) {
241
250
  var utils = context && context.utils;
251
+ var navigation = createPageNavigation(window);
252
+ var taskNavigation = createTaskCenterNavigation();
253
+ var taskCounts = createTaskCenterLoader(createTaskCenterContract(), function() { return yidaBridge; });
242
254
  var methodNames = openyidaCollectUtilsMethodNames(utils);
243
255
  function invoke(methodName) {
256
+ if (methodName === 'openPage') { return navigation.open.apply(null, Array.prototype.slice.call(arguments, 1)); }
257
+ if (['taskCenterCount', 'taskCenterLink', 'openTaskCenter'].indexOf(methodName) !== -1) {
258
+ return bridge[methodName].apply(null, Array.prototype.slice.call(arguments, 1));
259
+ }
244
260
  if (!utils || typeof utils[methodName] !== 'function') {
245
261
  throw new Error('this.utils.' + methodName + ' is not available');
246
262
  }
@@ -251,6 +267,8 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
251
267
  apiGroups: {
252
268
  utility: OPENYIDA_UTIL_API_METHODS.slice(),
253
269
  router: ['push'],
270
+ navigation: ['openPage', 'taskCenterLink', 'openTaskCenter'],
271
+ tasks: ['taskCenterCount'],
254
272
  yida: OPENYIDA_KNOWN_YIDA_API_METHODS.slice()
255
273
  },
256
274
  knownMethods: OPENYIDA_UTIL_API_METHODS.slice(),
@@ -259,9 +277,33 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
259
277
  return !utils || typeof utils[methodName] !== 'function';
260
278
  }),
261
279
  invoke: invoke,
280
+ openPage: navigation.open,
281
+ taskCenterLink: function(options) {
282
+ return taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
283
+ },
284
+ taskCenterCount: async function(options) {
285
+ if (!options || typeof options !== 'object' || Array.isArray(options)
286
+ || Object.keys(options).some(function(key) { return ['scope', 'appType', 'kind', 'filters'].indexOf(key) === -1; })) {
287
+ var error = new Error('TASK_FILTER_INVALID: options');
288
+ error.code = 'TASK_FILTER_INVALID';
289
+ throw error;
290
+ }
291
+ var target = taskNavigation.target({ scope: options.scope, appType: options.appType, kind: options.kind, baseUrl: window.location.origin });
292
+ var filters = createTaskCenterContract().normalizeFilters(target.kind, options.filters === undefined ? {} : options.filters, 'app-query');
293
+ return taskCounts.count(target.kind, Object.assign({}, filters, { transport: 'app-query' },
294
+ target.scope === 'corp' ? { scope: 'corp' } : { appType: target.appType }));
295
+ },
296
+ openTaskCenter: function(options) {
297
+ var target = taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
298
+ return navigation.open(target.pageUrl, { mode: target.mode });
299
+ },
262
300
  router: openyidaCreateRouterBridge(utils),
263
301
  yida: yidaBridge
264
302
  };
303
+ ['openPage', 'taskCenterLink', 'openTaskCenter', 'taskCenterCount'].forEach(function(methodName) {
304
+ if (bridge.availableMethods.indexOf(methodName) === -1) { bridge.availableMethods.push(methodName); }
305
+ });
306
+ bridge.missingKnownMethods = bridge.missingKnownMethods.filter(function(name) { return name !== 'openPage'; });
265
307
  methodNames.forEach(function(methodName) {
266
308
  if (!bridge[methodName]) {
267
309
  bridge[methodName] = function() {
@@ -549,6 +549,12 @@ const COMMAND_SIDE_EFFECTS = new Map([
549
549
  read_actions: ['list', 'inspect', 'options'],
550
550
  mutating_actions: ['create', 'update', 'delete'],
551
551
  })],
552
+ ['form-message', sideEffect('mixed', {
553
+ mutates_yida: true,
554
+ mutates_local: true,
555
+ read_actions: ['list', 'inspect', 'options'],
556
+ mutating_actions: ['create', 'update', 'delete'],
557
+ })],
552
558
  ['cool-card', sideEffect('mixed', { mutates_yida: true, mutates_local: true, read_actions: ['list', 'templates', 'get'], mutating_actions: ['create'] })],
553
559
  ['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
560
  ['print-template', sideEffect('mixed', printTemplateEffects)],
@@ -677,7 +683,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
677
683
  ['task-center', sideEffect('mixed', {
678
684
  mutates_yida: false,
679
685
  mutates_local: true,
680
- read_actions: ['todo', 'created', 'done', 'cc', 'proxy', 'capabilities', 'options', 'inspect', 'count'],
686
+ read_actions: ['todo', 'created', 'done', 'cc', 'proxy', 'capabilities', 'link', 'options', 'inspect', 'count'],
681
687
  mutating_actions: ['scaffold'],
682
688
  })],
683
689
  ]);
@@ -863,6 +869,13 @@ const COMMAND_PERMISSIONS = new Map([
863
869
  ask_actions: ['delete'],
864
870
  })),
865
871
 
872
+ ...permissionEntries([
873
+ 'form-message',
874
+ ], actionDependentPermission({
875
+ preauthorized_actions: ['create', 'update'],
876
+ ask_actions: ['delete'],
877
+ })),
878
+
866
879
  ...permissionEntries([
867
880
  'aggregate-table',
868
881
  ], actionDependentPermission({
@@ -1638,6 +1651,19 @@ const COMMAND_GROUPS = [
1638
1651
  examples: ['openyida custom-button options APP_XXX FORM_XXX --json', 'openyida custom-button create APP_XXX FORM_XXX --config button.json --json'],
1639
1652
  notes: ['Manages native form custom buttons: edit current fields, fill an associated form, invoke a published manual custom-button flow (type 6), or open a page. options reads eligible fields, conditions, forms, flows and display locations. Updates merge action properties and translations; arrays and permissionConfig replace. Changing action type, fillType or urlType requires a complete new actionConfig. Saves back up first and verify by readback; writes are never replayed automatically. delete permanently removes the button definition and requires --yes and --expected-hash from inspect. This differs from removing a display reference. Platform permissions, edition limits, 10-button limit, and completed-process restrictions still apply. No runtime button execution. See docs/guides/custom-buttons.md.'],
1640
1653
  }),
1654
+ command('form-message', ['form-message'], 'form-message <list|inspect|options|create|update|delete> <appType> <formUuid> [messageId] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]', 'help.cmd_form_message', {
1655
+ managedRun: { scope: 'actions', actions: {
1656
+ list: { scope: 'app', appPosition: 1 },
1657
+ inspect: { scope: 'app', appPosition: 1 },
1658
+ options: { scope: 'app', appPosition: 1 },
1659
+ create: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
1660
+ update: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
1661
+ delete: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
1662
+ } },
1663
+ output: 'json',
1664
+ examples: ['openyida form-message options APP_XXX FORM_XXX --json', 'openyida form-message create APP_XXX FORM_XXX --config message.json --json'],
1665
+ notes: ['Manages native form-level message notification bindings, not application settings, custom-page messaging or integration automation. list and inspect preserve native configurations. create and update currently support verified immediate events for ordinary forms and seven native templates; scheduled and process-node writes are rejected until their rule and process context can be verified. The platform exposes no independent enable/disable state, so deletion is the only supported removal action and requires --yes plus the current inspect hash. Writes back up first, detect concurrent changes and require readback; requests are never replayed automatically. Saving a binding does not send a test message or prove delivery. See docs/guides/form-messages.md.'],
1666
+ }),
1641
1667
  command('get-schema', ['get-schema'], 'get-schema <appType> <formUuid|--all> [--summary-json|--field-map-json|--analysis-json]', 'help.cmd_get_schema', { managedRun: { scope: 'app', appPosition: 0 },
1642
1668
  examples: ['openyida get-schema APP_XXX FORM_XXX --field-map-json'],
1643
1669
  }),
@@ -1795,10 +1821,11 @@ const COMMAND_GROUPS = [
1795
1821
  'openyida data transfer task APP_XXX --process-inst-id PINST_XXX --form-uuid FORM_XXX --to-user-id USER_ID --expect-form-name "Approval form" --expect-form-type process --json',
1796
1822
  ],
1797
1823
  }),
1798
- command('task-center', ['task-center'], 'task-center <todo|created|done|cc|proxy|capabilities|scaffold|options|inspect|count> [--spec <file.json> --output <file.canvas.jsx>] [options]', 'help.cmd_task_center', {
1824
+ command('task-center', ['task-center'], 'task-center <todo|created|done|cc|proxy|capabilities|link|scaffold|options|inspect|count> [--spec <file.json> --output <file.canvas.jsx>] [options]', 'help.cmd_task_center', {
1799
1825
  requiresLogin: false,
1800
1826
  managedRun: { scope: 'actions', actions: {
1801
1827
  capabilities: { scope: 'local', allowedFlags: ['--json'] },
1828
+ link: { scope: 'local', allowedFlags: ['--scope', '--type', '--base-url', '--mode', '--json'] },
1802
1829
  scaffold: { scope: 'local' },
1803
1830
  options: { scope: 'app', appPosition: 1 },
1804
1831
  inspect: { scope: 'app', appPosition: 1 },
@@ -1807,13 +1834,16 @@ const COMMAND_GROUPS = [
1807
1834
  output: 'json',
1808
1835
  examples: [
1809
1836
  'openyida task-center capabilities --json',
1837
+ 'openyida task-center link APP_XXX --scope app --type todo --base-url https://tenant.aliwork.com --json',
1838
+ 'openyida task-center link --scope corp --type cc --base-url https://tenant.aliwork.com --json',
1839
+ 'openyida task-center count --scope corp --type created --json',
1810
1840
  'openyida task-center options APP_XXX --type originators --keyword 张 --json',
1811
1841
  'openyida task-center inspect APP_XXX --process-inst-id INSTANCE_REPLACE --form-uuid FORM_REPLACE --json',
1812
1842
  'openyida task-center inspect APP_XXX --process-inst-id INSTANCE_REPLACE --history --task-id TASK_REPLACE --json',
1813
1843
  'openyida task-center count APP_XXX --type todo --transport app-query --filters-json \'{"keyword":"采购"}\' --json',
1814
1844
  'openyida task-center scaffold --spec workflow-spec.json --output project/pages/src/progress.canvas.jsx',
1815
1845
  ],
1816
- notes: ['capabilities describes application task kinds, filters, lookup options, workflow read models and scaffold specs. inspect <appType> --process-inst-id ID [--history] [--task-id ID] reads authorized process progress and optional history; taskResult matches a task, without asserting visitor ownership. count <appType> --type KIND [--transport app-query] [--filters-json JSON] reads one list record and its server total; unknown totals stay null. Neither read command requires a custom-page choice. The tasks scaffold selects fields, filterFields and optional expandProcess; the progress view exports ProcessProgressSection for a real linked instance. options <appType> --type originators|processes|forms --keyword TEXT --size N --json reads current-visitor lookup candidates. scaffold creates a new local Canvas page only after an explicit custom-page request or affirmative ask_human choice, recorded as customPage.source and confirmed=true. A generic task-center request or Fast mode does not provide that choice. Pages use the current visitor and existing application theme; CLI credentials are never embedded. Global queries remain unavailable in application-bound managed runs.'],
1846
+ notes: ['link generates a native app or corporation task-center pageUrl and DingTalk AppLink without opening a browser or writing platform resources. --scope app|corp is required; app requires appType, corp rejects appType. --base-url allows offline URL generation; otherwise the active auth environment supplies the origin. --mode defaults to auto: browser pages stay in the browser and DingTalk pages use native openLink with ddtab=true; generating a link cannot detect a future page visitor environment. Explicit dingtalk mode adds ddtab=true. count --scope corp uses app-query with appKeys=[]; explicit --scope app defaults to app-query, while legacy count without --scope retains app-v1 compatibility. Scope must be clarified with the user when ambiguous. Published utilities expose openPage(url, {mode: auto|dingtalk|browser}), taskCenterLink and openTaskCenter. DingTalk mode never silently falls back to a browser. capabilities describes application task kinds, filters, lookup options, workflow read models and scaffold specs. inspect <appType> --process-inst-id ID [--history] [--task-id ID] reads authorized process progress and optional history; taskResult matches a task, without asserting visitor ownership. count <appType> --type KIND [--transport app-query] [--filters-json JSON] reads one list record and its server total; unknown totals stay null. Neither read command requires a custom-page choice. The tasks scaffold selects fields, filterFields and optional expandProcess; the progress view exports ProcessProgressSection for a real linked instance. options <appType> --type originators|processes|forms --keyword TEXT --size N --json reads current-visitor lookup candidates. scaffold creates a new local Canvas page only after an explicit custom-page request or affirmative ask_human choice, recorded as customPage.source and confirmed=true. A generic task-center request or Fast mode does not provide that choice. Pages use the current visitor and existing application theme; CLI credentials are never embedded. Global queries remain unavailable in application-bound managed runs.'],
1817
1847
  }),
1818
1848
  command('basic-info', ['basic-info'], 'basic-info <overview|commodity|grant|capacity|quota|abs-path|dataflow|i18n|domain>', 'help.cmd_basic_info', { managedRun: { scope: 'denied', reason: 'scope_not_audited' },
1819
1849
  output: 'json',
@@ -19,6 +19,17 @@ module.exports = {
19
19
  'limit': 'A form supports at most 10 custom buttons.',
20
20
  'readback_mismatch': 'Saved custom button does not match readback. Keep the backup and inspect remote state.'
21
21
  },
22
+ form_message: {
23
+ usage: 'Usage: openyida form-message <list|inspect|options|create|update|delete> <appType> <formUuid> [messageId] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]',
24
+ invalid_config: 'Invalid form message configuration or argument: {0}',
25
+ invalid_response: 'Incomplete platform form-message response: {0}',
26
+ api_error: 'Form message API failed: {0}. Check login, form administration permission, edition and feature availability.',
27
+ not_found: 'The message notification does not exist in the target form: {0}',
28
+ conflict: 'The message notification changed after it was read. Inspect again before saving.',
29
+ unsupported_trigger: 'This CLI has not verified writes for this trigger: {0}. Existing configurations remain readable.',
30
+ unsupported_form: 'This CLI has not verified notification writes for this form type: {0}. Existing configurations remain readable.',
31
+ readback_mismatch: 'Saved message notification does not match readback. Keep the backup and inspect remote state.',
32
+ },
22
33
  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'},
23
34
  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}'},
24
35
  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.'},
@@ -143,6 +154,7 @@ module.exports = {
143
154
  // ── bin/yida.js printHelp() ────────────────────────
144
155
  help: {
145
156
  cmd_custom_button: 'Native custom buttons: inspect and configure form edits, automation flows and page links',
157
+ cmd_form_message: 'Native form notifications: inspect triggers, recipients and templates, then safely maintain bindings',
146
158
  cmd_check_design: 'Validate design documents, theme tokens and PRD handoff',
147
159
  subtitle: 'AI-powered low-code development tool for Yida',
148
160
  usage: 'Usage:',
@@ -19,6 +19,17 @@ module.exports = {
19
19
  'limit': '单个表单最多支持 10 个自定义按钮。',
20
20
  'readback_mismatch': '保存后的配置与回读不一致,请保留备份并检查远端状态。'
21
21
  },
22
+ form_message: {
23
+ usage: '用法:openyida form-message <list|inspect|options|create|update|delete> <appType> <formUuid> [messageId] [--config <文件>] [--expected-hash <哈希>] [--backup <文件>] [--yes] [--json]',
24
+ invalid_config: '消息通知配置或参数无效:{0}',
25
+ invalid_response: '平台返回的消息通知配置不完整:{0}',
26
+ api_error: '表单消息通知接口失败:{0}。请检查登录、表单管理权限、组织版本和功能开关。',
27
+ not_found: '目标表单中不存在消息通知:{0}',
28
+ conflict: '消息通知在读取后发生变化,请重新查询后再保存。',
29
+ unsupported_trigger: '当前 CLI 尚未验证此触发规则的写入协议:{0}。仍可查询已有配置。',
30
+ unsupported_form: '当前 CLI 尚未验证此表单类型的消息通知写入协议:{0}。仍可查询已有配置。',
31
+ readback_mismatch: '保存后的消息通知与回读不一致,请保留备份并检查远端状态。',
32
+ },
22
33
  dws_external: {'note': 'dws 是独立的外部工具;OpenYida 仅提供安装指引,不下载或安装它。', 'install_guide': '显示钉钉 CLI 官方安装指引'},
23
34
  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}'},
24
35
  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':'回读配置或发布状态与预期不一致,请保留备份并检查卡片后再重试。'},
@@ -143,6 +154,7 @@ module.exports = {
143
154
  // ── bin/yida.js printHelp() ────────────────────────
144
155
  help: {
145
156
  cmd_custom_button: '原生自定义按钮:查询、配置填写表单、自动化流及页面跳转',
157
+ cmd_form_message: '原生表单消息通知:查询触发、接收对象与模板并安全维护配置',
146
158
  cmd_check_design: '检查设计文档、主题变量与 PRD 交接',
147
159
  subtitle: '宜搭低代码 AI 开发工具',
148
160
  usage: '用法:',