openyida 2026.10.22 → 2026.10.23-1
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 +2 -1
- package/bin/yida.js +5 -0
- package/lib/app/form-message.js +352 -0
- package/lib/app/services/canvas-page-schema-builder.js +43 -1
- package/lib/core/command-manifest.js +33 -3
- package/lib/core/locales/en.js +12 -0
- package/lib/core/locales/zh.js +12 -0
- package/lib/core/task-center.js +32 -4
- package/lib/navigation/page-navigation.js +62 -0
- package/lib/samples/openyida-scaffold/canvas-navigation.jsx +6 -2
- package/lib/task-center/contract.js +26 -1
- package/lib/task-center/loader.js +5 -0
- package/lib/task-center/navigation.js +41 -0
- package/lib/task-center/scaffold.js +9 -0
- package/package.json +1 -1
- package/yida-skills/SKILL.md +2 -1
- package/yida-skills/references/yida-api.md +3 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +2 -0
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +1 -1
- package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +29 -1
- package/yida-skills/skills/yida-form-message/SKILL.md +53 -0
- package/yida-skills/skills/yida-form-message/references/guide.md +111 -0
- package/yida-skills/skills/yida-task-center/SKILL.md +2 -0
- 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
|
|
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',
|
package/lib/core/locales/en.js
CHANGED
|
@@ -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:',
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -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: '用法:',
|
package/lib/core/task-center.js
CHANGED
|
@@ -50,11 +50,13 @@ Usage:
|
|
|
50
50
|
--keyword TEXT 搜索关键词
|
|
51
51
|
--no-detail 不返回详情(仅 created 类型支持)
|
|
52
52
|
|
|
53
|
+
openyida task-center link [appType] --scope app|corp --type todo|created|done|cc [--base-url URL] [--mode dingtalk|auto|browser] --json
|
|
54
|
+
openyida task-center count --scope corp --type todo|created|done|cc [--filters-json JSON] --json
|
|
53
55
|
openyida task-center capabilities --json
|
|
54
56
|
openyida task-center scaffold --spec <file.json> --output <file.canvas.jsx>
|
|
55
57
|
openyida task-center options <appType> --type <originators|processes|forms> [--keyword TEXT] [--size N] --json
|
|
56
58
|
openyida task-center inspect <appType> --process-inst-id <id> [--form-uuid <id>] [--history] [--task-id <id>] --json
|
|
57
|
-
openyida task-center count <appType> --type <todo|created|done|cc> [--transport app-v1|app-query] [--filters-json <json>] --json
|
|
59
|
+
openyida task-center count <appType> [--scope app] --type <todo|created|done|cc> [--transport app-v1|app-query] [--filters-json <json>] --json
|
|
58
60
|
`;
|
|
59
61
|
|
|
60
62
|
// ── 工具函数 ──────────────────────────────────────────
|
|
@@ -217,6 +219,26 @@ async function run(args) {
|
|
|
217
219
|
return result;
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
if (type === 'link') {
|
|
223
|
+
const allowed = ['scope', 'type', 'base_url', 'mode', 'json'];
|
|
224
|
+
const invalid = Object.keys(options).find(key => !allowed.includes(key));
|
|
225
|
+
if (invalid || positionals.length > 2 || options.json !== undefined && options.json !== true) {
|
|
226
|
+
fail(t('task_center.invalid_spec', invalid || 'arguments'));
|
|
227
|
+
}
|
|
228
|
+
if (!['app', 'corp'].includes(options.scope)) { fail(t('task_center.invalid_spec', 'scope')); }
|
|
229
|
+
const baseUrl = options.base_url === undefined ? (await ensureSession()).baseUrl : options.base_url;
|
|
230
|
+
const target = require('../task-center/navigation').target({
|
|
231
|
+
scope: options.scope, kind: options.type, baseUrl, mode: options.mode,
|
|
232
|
+
...(positionals[1] !== undefined ? { appType: positionals[1] } : {}),
|
|
233
|
+
});
|
|
234
|
+
const nativeTarget = require('../task-center/navigation').target({ scope: options.scope, kind: options.type, baseUrl, mode: 'dingtalk',
|
|
235
|
+
...(positionals[1] !== undefined ? { appType: positionals[1] } : {}) });
|
|
236
|
+
const dingtalkUrl = require('../dingtalk/dingtalk-link').buildDingTalkPageLink({ url: nativeTarget.pageUrl });
|
|
237
|
+
const result = { ...target, dingtalkUrl, opened: false };
|
|
238
|
+
console.log(JSON.stringify(result, null, 2));
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
220
242
|
if (type === 'options') {
|
|
221
243
|
const invalid = Object.keys(options).find(key => !['type', 'keyword', 'size', 'json'].includes(key));
|
|
222
244
|
if (invalid || positionals.length !== 2) { fail(t('task_center.invalid_spec', invalid || 'arguments')); }
|
|
@@ -234,9 +256,9 @@ async function run(args) {
|
|
|
234
256
|
}
|
|
235
257
|
|
|
236
258
|
if (type === 'inspect' || type === 'count') {
|
|
237
|
-
const allowed = type === 'inspect' ? ['process_inst_id', 'form_uuid', 'task_id', 'history', 'json'] : ['type', 'transport', 'filters_json', 'json'];
|
|
259
|
+
const allowed = type === 'inspect' ? ['process_inst_id', 'form_uuid', 'task_id', 'history', 'json'] : ['type', 'transport', 'filters_json', 'scope', 'json'];
|
|
238
260
|
const invalid = Object.keys(options).find(key => !allowed.includes(key));
|
|
239
|
-
if (invalid || positionals.length !== 2 || options.json !== undefined && options.json !== true || options.history !== undefined && options.history !== true) {
|
|
261
|
+
if (invalid || (positionals.length !== 2 && !(type === 'count' && options.scope === 'corp' && positionals.length === 1)) || options.json !== undefined && options.json !== true || options.history !== undefined && options.history !== true) {
|
|
240
262
|
fail(t('task_center.invalid_spec', invalid || 'arguments'));
|
|
241
263
|
}
|
|
242
264
|
const tasks = require('../task-center/contract');
|
|
@@ -251,7 +273,13 @@ async function run(args) {
|
|
|
251
273
|
if (options.filters_json !== undefined) {
|
|
252
274
|
try { filters = JSON.parse(options.filters_json); } catch { fail(t('task_center.invalid_spec', 'filters-json')); }
|
|
253
275
|
}
|
|
254
|
-
|
|
276
|
+
if (options.scope !== undefined && !['app', 'corp'].includes(options.scope)) { fail(t('task_center.invalid_spec', 'scope')); }
|
|
277
|
+
if (options.scope === 'corp' && positionals[1] !== undefined) { fail(t('task_center.invalid_spec', 'appType')); }
|
|
278
|
+
const transport = options.transport || (options.scope !== undefined ? 'app-query' : undefined);
|
|
279
|
+
input = { ...tasks.normalizeFilters(options.type, filters, transport),
|
|
280
|
+
...(options.scope === 'corp' ? { scope: 'corp' } : { appType: tasks.appOf(positionals[1]) }),
|
|
281
|
+
transport: tasks.transportOf(transport) };
|
|
282
|
+
if (input.scope === 'corp' && input.transport !== 'app-query') { fail(t('task_center.invalid_spec', 'transport')); }
|
|
255
283
|
}
|
|
256
284
|
const client = createYidaClient({ authRef: await ensureSession() });
|
|
257
285
|
const request = descriptor => client.get(descriptor.path, { _api: 'nattyFetch', _mock: 'false', ...descriptor.params });
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Portable: publish injects this exact implementation into the page utility bridge.
|
|
4
|
+
function createPageNavigation(win) {
|
|
5
|
+
function fail(code) {
|
|
6
|
+
const error = new Error(code);
|
|
7
|
+
error.code = code;
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
function normalize(input, options) {
|
|
11
|
+
const value = input && typeof input === 'object' && !Array.isArray(input) ? input.url : input;
|
|
12
|
+
if (typeof value !== 'string' || !value.trim()) { fail('PAGE_URL_INVALID'); }
|
|
13
|
+
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) { fail('PAGE_OPEN_OPTIONS_INVALID'); }
|
|
14
|
+
const mode = (options && options.mode) || 'auto';
|
|
15
|
+
if (!['auto', 'dingtalk', 'browser'].includes(mode)) { fail('PAGE_OPEN_MODE_INVALID'); }
|
|
16
|
+
let url;
|
|
17
|
+
try { url = new URL(value, win.location.href || win.location.origin); } catch { fail('PAGE_URL_INVALID'); }
|
|
18
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) { fail('PAGE_URL_INVALID'); }
|
|
19
|
+
const inDingTalk = /dingtalk/i.test((win.navigator && win.navigator.userAgent) || '');
|
|
20
|
+
const useDingTalk = mode === 'dingtalk' || (mode === 'auto' && inDingTalk);
|
|
21
|
+
if (useDingTalk) { url.searchParams.set('ddtab', 'true'); }
|
|
22
|
+
return { url: url.href, mode, inDingTalk, useDingTalk };
|
|
23
|
+
}
|
|
24
|
+
function invokeNative(invoke, url) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
function success(value) { clearTimeout(timer); resolve(value); }
|
|
27
|
+
function rejectOpen(reason) {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
const error = new Error('DINGTALK_OPEN_FAILED');
|
|
30
|
+
error.code = 'DINGTALK_OPEN_FAILED';
|
|
31
|
+
error.cause = reason;
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
34
|
+
const timer = setTimeout(() => rejectOpen('timeout'), 15000);
|
|
35
|
+
try {
|
|
36
|
+
const result = invoke({ url, onSuccess: success, onFail: rejectOpen });
|
|
37
|
+
if (result && typeof result.then === 'function') { result.then(success, rejectOpen); }
|
|
38
|
+
} catch (error) { rejectOpen(error); }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function open(input, options) {
|
|
42
|
+
const target = normalize(input, options);
|
|
43
|
+
if (!target.useDingTalk) {
|
|
44
|
+
win.open(target.url, '_blank', 'noopener,noreferrer');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!target.inDingTalk) { fail('DINGTALK_ENV_REQUIRED'); }
|
|
48
|
+
// Use the publishing host's bridge. Do not scan parent DOM or cross-origin windows.
|
|
49
|
+
const dd = win.dd;
|
|
50
|
+
if (dd && dd.env && dd.env.platform === 'notInDingTalk') { fail('DINGTALK_API_UNAVAILABLE'); }
|
|
51
|
+
if (dd && dd.biz && dd.biz.util && typeof dd.biz.util.openLink === 'function') {
|
|
52
|
+
return invokeNative(options => dd.biz.util.openLink(options), target.url);
|
|
53
|
+
}
|
|
54
|
+
if (win.Ali && typeof win.Ali.pushWindow === 'function') {
|
|
55
|
+
return invokeNative(options => win.Ali.pushWindow(options), target.url);
|
|
56
|
+
}
|
|
57
|
+
fail('DINGTALK_API_UNAVAILABLE');
|
|
58
|
+
}
|
|
59
|
+
return { normalize, open };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { createPageNavigation };
|
|
@@ -46,8 +46,12 @@ function navigateCanvasPage(entry, context = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
const href = buildCanvasPageUrl(entry, context);
|
|
48
48
|
const utils = context.utils || window.__OPENYIDA_UTILS__;
|
|
49
|
-
if (entry.openMode === 'new-tab' || entry.targetType === 'url') {
|
|
50
|
-
if (typeof utils?.openPage === 'function') return
|
|
49
|
+
if (entry.openMode === 'dingtalk' || entry.openMode === 'new-tab' || entry.targetType === 'url') {
|
|
50
|
+
if (typeof utils?.openPage === 'function') return entry.openMode === 'dingtalk'
|
|
51
|
+
? utils.openPage(href, { mode: 'dingtalk' }) : utils.openPage(href);
|
|
52
|
+
if (entry.openMode === 'dingtalk' || /dingtalk/i.test(window.navigator?.userAgent || '')) {
|
|
53
|
+
throw new Error('DINGTALK_API_UNAVAILABLE');
|
|
54
|
+
}
|
|
51
55
|
window.open(href, '_blank', 'noopener,noreferrer');
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
@@ -119,6 +119,31 @@ function createTaskCenterContract() {
|
|
|
119
119
|
return { kind: canonical, transport, method: def.method, endpoint: def.endpoint,
|
|
120
120
|
path: '/dingtalk/web/' + params.appType + '/v1/' + def.endpoint + '.json', params };
|
|
121
121
|
}
|
|
122
|
+
function corpCountRequest(kind, options) {
|
|
123
|
+
const canonical = kindOf(kind);
|
|
124
|
+
if (!options || options.scope !== 'corp' || options.appType !== undefined || options.transport !== 'app-query') {
|
|
125
|
+
fail('TASK_FILTER_INVALID', 'scope');
|
|
126
|
+
}
|
|
127
|
+
const filters = { ...options };
|
|
128
|
+
delete filters.scope;
|
|
129
|
+
delete filters.transport;
|
|
130
|
+
const normalizedFilters = normalizeFilters(canonical, filters, 'app-query');
|
|
131
|
+
return { kind: canonical, transport: 'app-query', path: '/query/task/' + queryDefinitions[canonical].endpoint + '.json',
|
|
132
|
+
params: { _api: 'nattyFetch', _mock: 'false', ...(canonical === 'todo' ? { taskQueryType: 1 } : {}),
|
|
133
|
+
query: JSON.stringify({ page: 1, limit: 1, ...normalizedFilters, appKeys: [] }) }, filters: normalizedFilters };
|
|
134
|
+
}
|
|
135
|
+
function corpCount(payload, kind, filters) {
|
|
136
|
+
checkFailure(payload);
|
|
137
|
+
const page = Object.prototype.hasOwnProperty.call(payload, 'content') ? payload.content : payload;
|
|
138
|
+
checkFailure(page);
|
|
139
|
+
if (!Array.isArray(page.values)) { fail('TASK_RESPONSE_INVALID', 'values'); }
|
|
140
|
+
let total = null;
|
|
141
|
+
if (page.totalCount !== undefined && page.totalCount !== null) {
|
|
142
|
+
if (!/^\d+$/.test(String(page.totalCount)) || !Number.isSafeInteger(Number(page.totalCount))) { fail('TASK_RESPONSE_INVALID', 'totalCount'); }
|
|
143
|
+
total = Number(page.totalCount);
|
|
144
|
+
}
|
|
145
|
+
return { appType: null, kind: kindOf(kind), total, available: total !== null, basis: 'server-total', scope: 'current-visitor-in-corp', filters };
|
|
146
|
+
}
|
|
122
147
|
function text(value) {
|
|
123
148
|
let parsed = value;
|
|
124
149
|
if (typeof value === 'string' && /^\s*\{/.test(value)) {
|
|
@@ -243,7 +268,7 @@ function createTaskCenterContract() {
|
|
|
243
268
|
identity: 'current-visitor', detail: 'native-page', theme: 'existing-application',
|
|
244
269
|
};
|
|
245
270
|
}
|
|
246
|
-
return { kindOf, appOf, transportOf, filterSchema, normalizeFilters, request, normalize, capabilities, text, outcome, summary, checkFailure };
|
|
271
|
+
return { kindOf, appOf, transportOf, filterSchema, normalizeFilters, request, corpCountRequest, corpCount, normalize, capabilities, text, outcome, summary, checkFailure };
|
|
247
272
|
}
|
|
248
273
|
|
|
249
274
|
module.exports = { createTaskCenterContract, ...createTaskCenterContract() };
|
|
@@ -50,6 +50,11 @@ function createTaskCenterLoader(contract, resolveBridge, requestJson) {
|
|
|
50
50
|
error.code = 'TASK_FILTER_UNSUPPORTED';
|
|
51
51
|
throw error;
|
|
52
52
|
}
|
|
53
|
+
if (options.scope === 'corp') {
|
|
54
|
+
const request = contract.corpCountRequest(kind, options);
|
|
55
|
+
const payload = await (requestJson || fetchPage)(request);
|
|
56
|
+
return contract.corpCount(payload, kind, request.filters);
|
|
57
|
+
}
|
|
53
58
|
const canonical = contract.kindOf(kind);
|
|
54
59
|
const { appType, transport, ...filters } = options;
|
|
55
60
|
const normalizedFilters = contract.normalizeFilters(canonical, filters, transport);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared by the CLI and the published page bridge; scope is always explicit.
|
|
4
|
+
function createTaskCenterNavigation() {
|
|
5
|
+
const kinds = {
|
|
6
|
+
todo: { tab: 'running', nav: 'NAV-SYSTEM-RUNNING-UUID' },
|
|
7
|
+
created: { tab: 'fromMe', nav: 'NAV-SYSTEM-FROM-ME-UUID' },
|
|
8
|
+
done: { tab: 'done', nav: 'NAV-SYSTEM-DONE-UUID' },
|
|
9
|
+
cc: { tab: 'notifyMe', nav: 'NAV-SYSTEM-NOTIFY-ME-UUID' },
|
|
10
|
+
};
|
|
11
|
+
function fail(field) {
|
|
12
|
+
const error = new Error('TASK_LINK_INVALID: ' + field);
|
|
13
|
+
error.code = 'TASK_LINK_INVALID';
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
function target(input) {
|
|
17
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) { fail('options'); }
|
|
18
|
+
for (const key of Object.keys(input)) {
|
|
19
|
+
if (!['scope', 'appType', 'kind', 'baseUrl', 'mode'].includes(key)) { fail(key); }
|
|
20
|
+
}
|
|
21
|
+
const { scope, appType } = input;
|
|
22
|
+
const kind = input.kind === 'submitted' ? 'created' : input.kind;
|
|
23
|
+
if (!Object.prototype.hasOwnProperty.call(kinds, kind)) { fail('kind'); }
|
|
24
|
+
if (!['app', 'corp'].includes(scope)) { fail('scope'); }
|
|
25
|
+
if (scope === 'app' && (typeof appType !== 'string' || !/^APP_[A-Za-z0-9_-]+$/.test(appType))) { fail('appType'); }
|
|
26
|
+
if (scope === 'corp' && appType !== undefined) { fail('appType'); }
|
|
27
|
+
let origin;
|
|
28
|
+
try { origin = new URL(input.baseUrl); } catch { fail('baseUrl'); }
|
|
29
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.username || origin.password) { fail('baseUrl'); }
|
|
30
|
+
const mode = input.mode === undefined ? 'auto' : input.mode;
|
|
31
|
+
if (!['auto', 'dingtalk', 'browser'].includes(mode)) { fail('mode'); }
|
|
32
|
+
const definition = kinds[kind];
|
|
33
|
+
const url = new URL(scope === 'app' ? '/' + appType + '/workbench/' + definition.nav : '/taskCenter.html', origin.origin);
|
|
34
|
+
if (scope === 'app') { url.searchParams.set('showTodoTab', 'true'); } else { url.searchParams.set('tab', definition.tab); }
|
|
35
|
+
if (mode === 'dingtalk') { url.searchParams.set('ddtab', 'true'); }
|
|
36
|
+
return { scope, appType: scope === 'app' ? appType : null, kind, pageUrl: url.href, mode };
|
|
37
|
+
}
|
|
38
|
+
return { target };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { createTaskCenterNavigation, ...createTaskCenterNavigation() };
|
|
@@ -19,6 +19,12 @@ function fail(code, field) {
|
|
|
19
19
|
function capabilities() {
|
|
20
20
|
return {
|
|
21
21
|
...contract.capabilities(),
|
|
22
|
+
navigation: {
|
|
23
|
+
command: 'task-center link [appType] --scope app|corp --type todo|created|done|cc [--base-url URL] [--mode dingtalk|auto|browser] --json',
|
|
24
|
+
scopes: ['app', 'corp'], scopeRequired: true, defaultMode: 'auto',
|
|
25
|
+
runtime: 'window.__OPENYIDA_UTILS__.openTaskCenter({ scope, appType (app only), kind, mode })',
|
|
26
|
+
opensFromCli: false,
|
|
27
|
+
},
|
|
22
28
|
lookups: {
|
|
23
29
|
command: 'task-center options <appType> --type <originators|processes|forms> [--keyword TEXT] [--size N] --json',
|
|
24
30
|
kinds: ['originators', 'processes', 'forms'],
|
|
@@ -30,6 +36,9 @@ function capabilities() {
|
|
|
30
36
|
count: 'task-center count <appType> --type todo|created|done|cc [--transport app-v1|app-query] [--filters-json JSON] --json',
|
|
31
37
|
scope: 'current-visitor-visible-process', historyDefault: false,
|
|
32
38
|
results: { personal: 'personalResult on done tasks', process: 'processResult', task: 'taskResult matched by taskId; not proof of current-visitor ownership' },
|
|
39
|
+
runtimeCount: 'window.__OPENYIDA_UTILS__.taskCenterCount({ scope, appType (app only), kind, filters })',
|
|
40
|
+
scopedCount: 'task-center count [appType] --scope app|corp --type todo|created|done|cc --json',
|
|
41
|
+
corpCount: 'task-center count --scope corp --type todo|created|done|cc [--filters-json JSON] --json',
|
|
33
42
|
countBasis: 'server-total', unknownCount: null, historyFailure: 'explicit-partial-error',
|
|
34
43
|
},
|
|
35
44
|
scaffold: {
|
package/package.json
CHANGED
package/yida-skills/SKILL.md
CHANGED
|
@@ -144,7 +144,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
|
|
|
144
144
|
| `yida-skills/context` | 登录、退出、切换组织、组织版本/容量、Schema、fieldId、执行前检查 | `yida-login`、`yida-logout`、`yida-basic-info`、`yida-get-schema`、`yida-corp-efficiency` |
|
|
145
145
|
| `yida-skills/app` | 从零搭应用、完整系统、应用启停、应用导航、多语言、AI 老应用主题升级 | `yida-app`、`yida-create-app`、`yida-app-lifecycle`、`yida-nav-group`、`yida-i18n`、`yida-upgrade-app-theme`(仅明确升级要求并 ask_human 确认) |
|
|
146
146
|
| `yida-skills/design` | 完整应用需求分析、PRD、视觉设计、单页 UI 改造、应用主题色、全局换肤、图片素材 | `yida-requirement-analysis`、`yida-prd`、`yida-design`、`yida-data-manage-ui`、`yida-image-assets` |
|
|
147
|
-
| `yida-skills/form` |
|
|
147
|
+
| `yida-skills/form` | 表单字段、公式、校验、业务关联规则、原生自定义按钮、消息通知、打印模板、批量录入、数据记录 | `yida-create-form-page`、`yida-custom-button`、`yida-form-message`、`yida-print-template`、`yida-form-view`、`yida-formula`、`yida-formula-evaluate`、`yida-business-rule`、`yida-canvas-table-form`、`yida-table-form`、`yida-data-management` |
|
|
148
148
|
| `yida-skills/process` | 审批、流程表单、流程规则、节点/分支/字段权限、流程代理 | `yida-create-process`、`yida-process-rule`、`yida-flow-node-config`、`yida-agent-center` |
|
|
149
149
|
| `yida-skills/page` | 工作台、办事门户、任务看板、自定义展示页、页面源码开发、平台 JSX 组件页面维护、页面发布、页面内导航、PPT 页面 | `yida-create-page`、`yida-canvas-custom-page`、`yida-custom-page`、`yida-canvas-data-binding`、`yida-task-center`(仅显式选择自定义待办)、`yida-canvas-upgrade`、`yida-publish-page`、`yida-openyida-publish-guard`、`yida-density`、`yida-nav-shell`、`yida-ppt-slider` |
|
|
150
150
|
| `yida-skills/analytics` | 用户显式指定宜搭原生数据卡片(含其投放播报);聚合表、虚拟视图、报表、统计、图表、Recharts、ECharts、经营分析看板、驾驶舱、大屏 | `yida-data-card`、`yida-aggregate-table`、`yida-report`、`yida-rechart`、`yida-chart`、`yida-dashboard` |
|
|
@@ -200,6 +200,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
|
|
|
200
200
|
| 业务操作队列、业务工作台或办事门户页面 | `yida-canvas-custom-page`;接业务数据时加载 `yida-canvas-data-binding`,不因页面名称或辅助统计转为经营看板;不顺带新增自定义审批待办中心 |
|
|
201
201
|
| 批量录入、表格填写、多行编辑 | 默认 `yida-canvas-table-form`;已检测到平台 JSX 组件页面、native 页面或存量源码使用 `this.utils.yida.saveFormData` 时用 `yida-table-form` |
|
|
202
202
|
| 页面设置中的自定义按钮、客户跟进/分配、审批后发起订单、按钮触发自动化或打开页面 | `yida-custom-button`;配置原生按钮定义和显示位置,自定义页面内 React 按钮不走此入口 |
|
|
203
|
+
| 页面设置中的消息通知、表单提交/编辑/删除/评论后通知、原生工作通知与详情/置顶卡片 | `yida-form-message`;选择发送时机、接收人和七种消息样式,保存后核对配置;按用户要求执行真实发送测试,沿用已明确的接收对象与测试授权 |
|
|
203
204
|
| 打印这条数据/数据详情链接、原生打印模板、打印设置、订单/报价/出入库/审批单、单据版式与字段明细、字段二维码/条形码、备份恢复和PDF生成 | `yida-print-template`;复用已有表单和模板,以真实打印字典生成动态内容;Word组件、叮当打印和浏览器页面打印不走此入口 |
|
|
204
205
|
| 原生数据管理页视图列表、显示列配置、字段显隐与顺序、列宽、冻结列、默认筛选、多视图 | `yida-form-view`;使用原生配置读写,保存前备份并回读,不通过 CSS 隐藏字段或重建自定义列表 |
|
|
205
206
|
| 当前数据管理页换风格、只留横线、突出新增/筛选/行操作、列级强调、状态列标签、数据管理 UI 异常、自定义简单数据管理 | `yida-data-manage-ui`(优先于通用美化);保留承载类型和业务,从目标推导维度、定位 token/CSS 并按页改写;备份发布并实测,不要求选择配方 |
|
|
@@ -1109,7 +1109,9 @@ export function didMount() {
|
|
|
1109
1109
|
|
|
1110
1110
|
### openPage
|
|
1111
1111
|
|
|
1112
|
-
|
|
1112
|
+
**描述**:打开新页面。原生 `this.utils.openPage(url)` 的参数为 URL 字符串。
|
|
1113
|
+
|
|
1114
|
+
Canvas 页面使用 OpenYida 发布桥 `window.__OPENYIDA_UTILS__.openPage(url)`,默认跟随当前浏览器或钉钉环境;浏览器正常打开页面,钉钉内添加 `ddtab=true` 并调用端内接口。只有明确要求严格端内打开时才传 `{mode:'dingtalk'}`。`mode` 是发布桥的能力;调用原生同名函数时按其原生签名传参。待办数量与入口使用发布桥的 `taskCenterCount / taskCenterLink / openTaskCenter`,参见[个人任务能力](../skills/yida-canvas-data-binding/references/workflow-capabilities.md)。
|
|
1113
1115
|
|
|
1114
1116
|
### router.push
|
|
1115
1117
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
- `isRenderNav=false` 表示导航展示配置,不证明当前页面位于 iframe。不要仅凭该参数修改 `window.top.location`;嵌入方式、跨域和宿主能力需分别验证。
|
|
10
10
|
- 宜搭应用壳根据 `navUuid/formUuid` 和 `workbench` 路径维护选中态;同应用页面应进入 `/{appType}/workbench/{formUuid}` 或对应壳层路由。
|
|
11
11
|
- 发布层会把根级工具注册到 `window.__OPENYIDA_UTILS__`;可用时优先通过 `window.__OPENYIDA_UTILS__.router.push` 做应用内跳转,通过 `window.__OPENYIDA_UTILS__.openPage` 打开外部链接或新窗口场景。
|
|
12
|
+
- `openPage(url, { mode: 'auto' })` 接收 URL 字符串;发布桥兼容旧 `{url}` 参数,但新代码统一传字符串。`auto` 在钉钉内添加 `ddtab=true` 并调用原生接口,失败报错;浏览器环境沿用浏览器打开。`mode:'dingtalk'` 要求钉钉环境并禁止失败后回退系统浏览器,`mode:'browser'` 才显式打开浏览器。调用返回 Promise,点击事件必须捕获并展示错误。
|
|
13
|
+
- 待办卡片使用 `utils.taskCenterCount / taskCenterLink / openTaskCenter`,范围与 CLI 对齐,详见[数量卡片与原生入口](../../yida-canvas-data-binding/references/workflow-capabilities.md)。范围不明先 `ask_human`;不要自行拼跨应用入口或扫描 DOM。
|
|
12
14
|
- `openPage` / `window.open` 更适合外部链接、新标签、钉钉链接、文件预览等场景;同应用页面默认在当前应用壳内切换。
|
|
13
15
|
- 页面隐藏应用导航后,页面内自绘导航壳接管跨视图切换;导航可见时使用平台导航承载同级页面切换。
|
|
14
16
|
- 自绘应用导航按 PRD 安排数量、顺序、分组和任务入口,通常工作台在首位;当前用户的 `getAccessableNavs.json` 结果只用于过滤不可见入口。数据片段和接入规则见 [导航数据来源](../../yida-nav-shell/references/nav-shell-patterns.md#导航数据来源)。
|
|
@@ -7,7 +7,7 @@ description: 为 YidaCodeCanvas 自定义内容接入真实表单、个人流程
|
|
|
7
7
|
|
|
8
8
|
## 核心定位
|
|
9
9
|
|
|
10
|
-
先根据用户描述确定“待办”指什么:对象是个人流程任务还是业务记录,与当前访问者有什么关系,需要哪些信息、筛选、刷新及点击行为。页面、区块、卡片或“待办”名称都不能代替业务含义;只接入需求涉及的能力,不默认增加四类栏目、统计或审批按钮。关键语义无法从上下文确定时,只澄清影响数据来源或权限范围的问题。
|
|
10
|
+
先根据用户描述确定“待办”指什么:对象是个人流程任务还是业务记录,与当前访问者有什么关系,需要哪些信息、筛选、刷新及点击行为。页面、区块、卡片或“待办”名称都不能代替业务含义;只接入需求涉及的能力,不默认增加四类栏目、统计或审批按钮。关键语义无法从上下文确定时,只澄清影响数据来源或权限范围的问题。 个人任务的当前应用或本组织所有应用范围不明确时,必须通过 `ask_human` 确认;数量与原生入口使用同一范围。数量卡片及端内入口复用[流程任务能力](references/workflow-capabilities.md)中的 CLI 与发布桥,不手拼另一套请求或打开逻辑。
|
|
11
11
|
|
|
12
12
|
个人流程任务复用 CLI 的 `todo`(待我处理)、`created`(我创建的普通表单与流程)、`done`(我已处理)、`cc`(抄送我的)契约;先用 `openyida task-center capabilities --json` 查询条件,再按所需类型执行 `openyida data query tasks <appType> --type <类型> --normalized` 核查。需要加载实现时复用 CLI 装配产物中的任务加载与详情助手,保留当前展示布局;发布运行态读取实际访问者的任务,不把 CLI 登录人的结果或凭证嵌入页面。订单待发货、工单待处理等业务记录仍按真实表单字段和权限查询,不用流程任务接口代替。
|
|
13
13
|
|
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
按业务含义选能力,再沿用当前布局。首页的本人待办使用个人任务查询;采购单详情的审批进度使用该业务记录关联的流程实例;订单待发货等业务状态仍查询业务表单。只读查询不要求选择自定义实现,也不创建页面。新增自定义展示仍须用户显式要求或经 ask_human 明确同意。
|
|
4
4
|
|
|
5
|
+
## 数量卡片与原生入口
|
|
6
|
+
|
|
7
|
+
范围不明确时先 `ask_human` 确认当前应用或本组织所有应用;数量和入口共用同一范围。只需卡片数量与原生入口时,复用下列能力,不必装配自定义任务列表。
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openyida task-center count APP_REPLACE --scope app --type todo --json
|
|
11
|
+
openyida task-center count --scope corp --type todo --json
|
|
12
|
+
openyida task-center link APP_REPLACE --scope app --type todo --base-url https://example.aliwork.com --json
|
|
13
|
+
openyida task-center link --scope corp --type cc --base-url https://example.aliwork.com --json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`link` 只生成可检查的原生地址,`opened=false`;不打开本机浏览器。默认 `mode=auto`,生成普通页面 URL,由页面运行时按访问环境选择打开方式;CLI 本身不推断目标访客环境。显式 `--mode dingtalk` 时 URL 含 `ddtab=true`。另返回 `dingtalkUrl`,仅在用户明确要求从浏览器唤起钉钉时绑定为跨端点击入口。传 `--base-url` 可离线生成;否则读取当前登录组织。受应用约束的 managed run 不允许组织范围查询或覆盖组织域名,不绕过该约束。
|
|
17
|
+
|
|
18
|
+
页面发布层提供以下共用接口(必须用包含此能力的 OpenYida 编译发布):
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const utils = window.__OPENYIDA_UTILS__;
|
|
22
|
+
const selection = { scope: 'app', appType: 'APP_REPLACE', kind: 'todo' };
|
|
23
|
+
const count = await utils.taskCenterCount(selection);
|
|
24
|
+
// count.available=false 时显示“未知”;请求报错显示失败,不能显示成 0。
|
|
25
|
+
await utils.openTaskCenter(selection); // 默认跟随浏览器/钉钉环境;放在点击事件中并捕获错误
|
|
26
|
+
// 组织范围:{ scope: 'corp', kind: 'todo' },不要传 appType。
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`taskCenterCount` 固定同源 `app-query`、实际访问者身份与服务端总数,只读一页一条;可选 `filters` 与 CLI app-query 支持的条件一致,不扫描全历史或嵌入 CLI 身份。`taskCenterLink(selection)` 可读取同一地址。应用范围固定 `appKeys=[appType]`,组织范围固定 `appKeys=[]`。旧版不带 scope 的 CLI count 保留原 transport 默认值;新卡片应显式指定 scope,使 CLI 核查与页面采用同一口径。
|
|
30
|
+
|
|
31
|
+
`openTaskCenter` 默认 `mode:'auto'`:浏览器内直接打开浏览器新页签,钉钉内添加唯一 `ddtab=true` 并调用端内打开接口。浏览器默认打开不能因缺少钉钉环境而报错,也不应默认唤起钉钉。钉钉内接口缺失、调用失败或超时仍须报错,不静默跨到系统浏览器。显式 `mode:'dingtalk'` 是仅限端内的严格模式;跨端唤起用用户明确选择的 `dingtalkUrl` 入口。`mode:'browser'` 显式使用浏览器打开方式。不得用浏览器验证结果宣称钉钉新窗口或免登已验收。四类入口分别使用应用系统导航或平台任务中心原生页签,无需猜测 DOM。
|
|
32
|
+
|
|
5
33
|
## CLI 能力与读取
|
|
6
34
|
|
|
7
35
|
先执行 `openyida task-center capabilities --json`。固定接口、范围校验和返回格式由 CLI 维护,不在页面重新实现请求。以下标识均为待替换占位符,实例必须来自真实业务关联,不能按标题猜测。
|
|
@@ -22,7 +50,7 @@ openyida task-center inspect APP_REPLACE --process-inst-id INSTANCE_REPLACE --hi
|
|
|
22
50
|
|
|
23
51
|
- 个人任务仍为 `todo / created / done / cc`;四类条件和人员、流程、表单候选见[任务筛选示例](../../yida-task-center/references/task-filters.md)。不支持的条件直接报错,不静默删条件。
|
|
24
52
|
- 如果业务需要“我发起且仍在审批中的报销”,而个人任务接口不支持该状态条件,先确认真实流程表单,再选择既有 `data query process` 的服务端状态和发起人条件,并明确实例查询口径。运行态使用实际访问者身份,不能把 CLI 登录人的 ID 固化到页面;不在加载器中自动换接口。
|
|
25
|
-
- `count.total`
|
|
53
|
+
- `count.total` 是所选范围、访问者及所传条件下的服务端总数;`available=false / total=null` 表示未知。不是当前页长度,也不是全历史审批数量。“我创建的”包含普通表单与流程,不能先排除普通表单再把原总数称作审批数量。
|
|
26
54
|
- `inspect` 读取当前访问者有权查看的指定实例,不要求该实例由本人发起。返回 `summary / currentNodes / submittedAt / processFinishedAt / processResult / detailTarget`。范围或实例身份不符直接失败;缺少节点、人员或时间保持未知,不用历史最后一条猜当前环节。
|
|
27
55
|
- 页面运行时优先复用发布层可用的流程 JS-API 桥,仅在方法不可用时使用固定同源读取;桥调用报错不切换通道重试。缓存仅限已识别访问者、目标应用及实例,刷新和返回后失效,不缓存失败历史。
|
|
28
56
|
- `history.state` 为 `not-requested / ready / error`。历史失败时可保留详情并展示明确错误,不能展示成“暂无历史”;实例和历史归属冲突、登录失败仍整体报错。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yida-form-message
|
|
3
|
+
description: 为已有普通表单配置消息通知,选择新增、编辑、删除或评论后的发送时机、接收人和消息样式。支持工作通知、详情卡片、图文卡片、评论卡片和群置顶卡片等七种模板;按用户要求执行真实发送测试。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 表单消息通知
|
|
7
|
+
|
|
8
|
+
为已有普通表单配置“什么时候、向谁、发送什么通知”。使用 `openyida form-message` 查询、新增、修改和删除「页面设置 > 消息通知」中的配置。保存后,表单事件发生时发送通知。
|
|
9
|
+
|
|
10
|
+
## 适用场景
|
|
11
|
+
|
|
12
|
+
| 用户需求 | 使用入口 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| 配置表单新增、编辑、删除或评论后的消息通知 | 本技能 |
|
|
15
|
+
| 查询日期定时、流程开始/结束、流程节点或审批操作的已有通知 | 本技能查询;这些类型的修改使用宜搭页面 |
|
|
16
|
+
| 配置集成自动化中的通知节点 | `yida-integration` |
|
|
17
|
+
| 应用级通知、自定义页面消息、机器人消息 | 对应功能入口,独立于表单消息设置 |
|
|
18
|
+
|
|
19
|
+
## 操作步骤
|
|
20
|
+
|
|
21
|
+
1. 查询现有通知和可用选项,核实目标表单、字段、接收人和模板。
|
|
22
|
+
2. 按用户要求准备配置文件,再创建或修改通知。首次配置可从[发送给发起人的示例](references/guide.md)开始。
|
|
23
|
+
3. 核对保存结果中的 `readbackVerified: true`,交付可点击的消息通知配置链接(CLI 返回的 `configUrl`),附上通知 ID、标题、触发时机和接收对象。完整应用搭建和单点新增、修改通知均按此要求交付。用户要求真实测试时,按已指定的对象和范围执行并确认送达;已有明确授权可直接沿用。
|
|
24
|
+
|
|
25
|
+
`configUrl` 指向该表单的消息通知列表,地址为 `<当前认证环境域名>/<appType>/admin/<formUuid>/settings/messageSetting`。使用返回的真实域名与标识;用户打开后可按通知标题找到本次配置。
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
openyida form-message list <appType> <formUuid> --json
|
|
29
|
+
openyida form-message options <appType> <formUuid> --json
|
|
30
|
+
openyida form-message inspect <appType> <formUuid> <messageId> --json
|
|
31
|
+
openyida form-message create <appType> <formUuid> --config message.json --json
|
|
32
|
+
openyida form-message update <appType> <formUuid> <messageId> --config patch.json --expected-hash <configHash> --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 配置规则
|
|
36
|
+
|
|
37
|
+
普通表单支持新增、编辑、删除和评论后通知。接收对象支持指定人员、角色、表单人员字段和发起人;群置顶模板使用平台群 ID。模板中文名称、参数和选用方式见[配置指南](references/guide.md)。
|
|
38
|
+
|
|
39
|
+
更新时 `template` 属性合并,`recipients`、`groups` 和 `template.fields` 数组整体替换。CLI 写入前自动备份并检查配置是否变化。遇到超时或回读不一致,保留 `backupPath`,先执行 `list` / `inspect` 确认远端状态,再决定下一步;不自动重放写请求。
|
|
40
|
+
|
|
41
|
+
## 删除与测试
|
|
42
|
+
|
|
43
|
+
用户明确要求移除通知时,先用 `inspect` 获取最新 `configHash`,再执行删除。平台当前提供配置删除功能,没有独立启停接口;`isSendMessage` 是平台原生字段,不是通知开关。
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openyida form-message delete <appType> <formUuid> <messageId> --expected-hash <configHash> --yes --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
删除后重建会产生新 ID;备份保留原配置内容,恢复时需重新核对引用。真实测试使用独立测试目标,业务配置按用户指定的表单执行。测试前确认用户已授权触发表单事件及对应接收对象;交付时分别记录配置保存、事件触发和接收端送达结果。测试结束后,按用户要求保留或清理配置和数据。
|
|
50
|
+
|
|
51
|
+
## 参考文件
|
|
52
|
+
|
|
53
|
+
- [配置指南](references/guide.md):四种触发、七种模板、接收对象、变量格式和配置示例。
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# 表单消息通知配置
|
|
2
|
+
|
|
3
|
+
选择触发时机、消息样式或接收对象,以及编写配置文件时,查阅本指南。
|
|
4
|
+
|
|
5
|
+
## 场景与触发
|
|
6
|
+
|
|
7
|
+
| 场景 | trigger | 说明 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| 新记录提醒 | `FORM_DATA_CREATE` | 表单创建成功后发送 |
|
|
10
|
+
| 记录被修改 | `FORM_DATA_EDIT` | 任意编辑成功后发送;当前配置范围为整条记录的编辑事件 |
|
|
11
|
+
| 删除审计提醒 | `FORM_DATA_DELETE` | 删除成功后发送,接收人需在删除后仍可解析 |
|
|
12
|
+
| 评论协作提醒 | `FORM_COMMENT` | 评论成功后发送 |
|
|
13
|
+
|
|
14
|
+
日期字段定时、流程开始/结束、到达节点和审批操作的已有通知支持查询;配置这些发送规则时,使用宜搭页面设置对应的时间、流程版本、节点和审批结果。
|
|
15
|
+
|
|
16
|
+
## 七种消息样式
|
|
17
|
+
|
|
18
|
+
| 页面名称 | `template.key` | 主要用途 | 内容参数 |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| 纯文本普通消息(工作通知) | `workNotice` | 向接收人发送文字提醒 | `content` |
|
|
21
|
+
| 图文详情卡片 | `detailImage` | 用图文卡片展示表单字段,配置详情入口 | `fields` |
|
|
22
|
+
| 图文卡片 | `imageCard` | 用图文展示通知内容 | `content` |
|
|
23
|
+
| 纯文本详情卡片 | `detailText` | 用文字卡片展示表单字段,配置详情入口 | `fields` |
|
|
24
|
+
| 图文评论卡片 | `commentCard` | 展示通知内容,配置评论入口 | `content` |
|
|
25
|
+
| 图文群置顶卡片 | `pinnedImage` | 在指定群中置顶图文通知 | `content` |
|
|
26
|
+
| 纯文本详情置顶卡片 | `pinnedText` | 在指定群中置顶文字通知,配置详情入口 | `content` |
|
|
27
|
+
|
|
28
|
+
前五种模板通过 `recipients` 选择接收人;后两种通过 `groups` 选择群。七种模板均有配置支持,实际送达、详情跳转、评论和群置顶效果分别在接收端验证。
|
|
29
|
+
|
|
30
|
+
## 配置示例
|
|
31
|
+
|
|
32
|
+
### 发送给发起人
|
|
33
|
+
|
|
34
|
+
将下面的配置保存为 `message.json`,用于普通表单新增后通知发起人:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"trigger": "FORM_DATA_CREATE",
|
|
39
|
+
"recipients": [
|
|
40
|
+
{"type": "variable", "value": "originator"}
|
|
41
|
+
],
|
|
42
|
+
"groups": [],
|
|
43
|
+
"template": {
|
|
44
|
+
"key": "workNotice",
|
|
45
|
+
"title": "订单已提交",
|
|
46
|
+
"content": "订单 $!{serialNo} 已提交,请及时处理",
|
|
47
|
+
"fields": [],
|
|
48
|
+
"image": [],
|
|
49
|
+
"button": null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
变量格式为 `$!{变量或字段ID}`。系统字段从 `options.systemFields` 选取;表单字段从 `options.messageFields` 选取实际 ID。详情卡片中的 `fields` 也使用这两类选项。
|
|
55
|
+
|
|
56
|
+
### 展示表单详情
|
|
57
|
+
|
|
58
|
+
下面的金额字段 ID 替换为 `options.messageFields` 中的实际 ID:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"trigger": "FORM_DATA_CREATE",
|
|
63
|
+
"recipients": [{"type": "variable", "value": "originator"}],
|
|
64
|
+
"groups": [],
|
|
65
|
+
"template": {
|
|
66
|
+
"key": "detailText",
|
|
67
|
+
"title": "订单详情",
|
|
68
|
+
"fields": [
|
|
69
|
+
{"type": "variable", "value": "serialNo", "title": "流水号"},
|
|
70
|
+
{"type": "formField", "value": "实际金额字段ID", "title": "金额"}
|
|
71
|
+
],
|
|
72
|
+
"image": [],
|
|
73
|
+
"button": {"name": "查看详情", "type": "formInstDetail", "value": ""}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`detailImage` / `detailText` 使用 `fields`;`workNotice` / `imageCard` / `commentCard` / `pinnedImage` / `pinnedText` 使用 `content`。`button.type` 可为 `formInstDetail`、`comment` 或 `custom`;自定义链接仅接受无账号密码的 HTTP(S) URL。
|
|
79
|
+
|
|
80
|
+
### 在群中置顶
|
|
81
|
+
|
|
82
|
+
群置顶模板把 `recipients` 设为空,并提供平台返回的群标识:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"trigger": "FORM_DATA_EDIT",
|
|
87
|
+
"recipients": [],
|
|
88
|
+
"groups": [{"cid": "实际群ID", "name": "业务群"}],
|
|
89
|
+
"template": {
|
|
90
|
+
"key": "pinnedText",
|
|
91
|
+
"title": "订单状态更新",
|
|
92
|
+
"content": "订单状态已更新",
|
|
93
|
+
"fields": [],
|
|
94
|
+
"image": []
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 接收对象
|
|
100
|
+
|
|
101
|
+
- `employee`:指定人员的平台 ID,选择目标组织中仍有效且可见的人员。
|
|
102
|
+
- `role`:宜搭角色 ID;实际成员按触发时权限解析。
|
|
103
|
+
- `formField`:目标表单中的人员字段,必须出现在 `options.recipientFields`。
|
|
104
|
+
- `variable`:当前仅支持原生 `originator`(发起人)。
|
|
105
|
+
- `groups`:原生群置顶卡片目标;群权限、酷应用支持和成员关系仍由平台校验。
|
|
106
|
+
|
|
107
|
+
向指定人员发送时,将接收项设为 `{"type":"employee","value":"实际人员ID"}`;向表单人员字段中的成员发送时,使用 `{"type":"formField","value":"实际人员字段ID"}`。一次消息最多选择 100 人,具体可见范围和可用能力以目标组织为准。
|
|
108
|
+
|
|
109
|
+
## 备份位置
|
|
110
|
+
|
|
111
|
+
创建、修改和删除配置时,CLI 自动把原配置备份到 `.cache/openyida/form-message/`。传入 `--backup` 可指定一个新的备份文件路径;操作返回的 `backupPath` 可用于查找该文件。
|
|
@@ -7,6 +7,8 @@ description: 仅在用户明确要求用自定义页面开发待办中心,或
|
|
|
7
7
|
|
|
8
8
|
先核对用户已明确选择自定义实现及目标应用。仅提到待办中心、Fast 模式、已有 Canvas 页面或委托 AI 决定均不算选择;不要自行补确认记录。应用搭建中由 `yida-app` 先 ask_human,明确同意后才交接本技能;用户已显式要求时不重复问。
|
|
9
9
|
|
|
10
|
+
先确认数据与入口范围:当前应用(`scope=app`)还是当前用户在本组织所有应用(`scope=corp`)。用户只说“我的待办”且上下文不能确定范围时,必须通过 `ask_human` 确认,不能因正在编辑某个应用就默认应用范围。用户已明确范围时沿用,不重复询问。数量与点击入口必须使用相同范围;不把组织任务误称为所有组织。
|
|
11
|
+
|
|
10
12
|
按用户描述确定所需个人流程任务及交互,不按整页或区块划分业务能力。仅需接入某项任务数据时走 `yida-canvas-data-binding`,不强制使用本装配;业务记录的待处理状态也由数据绑定处理。
|
|
11
13
|
|
|
12
14
|
需要筛选时按需读取[个人任务筛选示例](references/task-filters.md),包含四类任务的组合条件、候选项查询及初始配置片段。
|
|
@@ -49,6 +49,19 @@
|
|
|
49
49
|
],
|
|
50
50
|
"requires_login": true
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
"name": "yida-form-message",
|
|
54
|
+
"path": "skills/yida-form-message/SKILL.md",
|
|
55
|
+
"display_name": "表单消息通知",
|
|
56
|
+
"description": "配置普通表单新增、编辑、删除或评论后的消息通知,选择接收人和七种消息样式;保存后核对配置,按用户要求执行真实发送测试。",
|
|
57
|
+
"category": "yida-skills/form",
|
|
58
|
+
"tags": ["消息通知", "表单通知", "工作通知", "消息卡片", "群置顶卡片"],
|
|
59
|
+
"aliases": ["表单消息", "页面消息通知", "form-message"],
|
|
60
|
+
"positive_signals": ["配置表单提交后消息通知", "修改页面设置里的消息通知", "表单编辑后通知发起人", "查询消息通知接收对象", "配置表单原生消息卡片"],
|
|
61
|
+
"negative_signals": ["自定义页面发送消息", "机器人群消息", "应用级通知", "集成自动化发消息", "原生酷卡片资源"],
|
|
62
|
+
"command_ids": ["form-message"],
|
|
63
|
+
"requires_login": true
|
|
64
|
+
},
|
|
52
65
|
{
|
|
53
66
|
"name": "yida-cool-card",
|
|
54
67
|
"path": "skills/yida-cool-card/SKILL.md",
|