openyida 2026.10.19-4 → 2026.10.20
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 +1 -0
- package/bin/yida.js +5 -0
- package/lib/app/create-form.js +3 -0
- package/lib/app/custom-button.js +372 -0
- package/lib/app/services/form-compiler.js +3 -0
- package/lib/core/command-manifest.js +26 -0
- package/lib/core/locales/en.js +11 -0
- package/lib/core/locales/zh.js +11 -0
- package/package.json +1 -1
- package/yida-skills/SKILL.md +2 -1
- package/yida-skills/skills/yida-custom-button/SKILL.md +71 -0
- package/yida-skills/skills-index.json +159 -20
package/README.md
CHANGED
|
@@ -510,6 +510,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
|
|
|
510
510
|
| `openyida list-forms <appType> [--keyword <text>]` | List forms/pages in an app |
|
|
511
511
|
| `openyida aggregate-table <list\|create-empty\|inspect\|preview\|save\|publish\|status> <appType> ...` | Manage aggregate tables (virtualView) |
|
|
512
512
|
| `openyida form-view <list\|inspect\|save\|create\|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]` | Native form views: list, inspect and configure columns, widths, frozen columns and filters |
|
|
513
|
+
| `openyida custom-button <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native custom buttons: inspect and configure form edits, automation flows and page links |
|
|
513
514
|
| `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
|
|
514
515
|
| `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
|
|
515
516
|
| `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
|
package/bin/yida.js
CHANGED
|
@@ -832,6 +832,11 @@ async function main() {
|
|
|
832
832
|
break;
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
+
case 'custom-button': {
|
|
836
|
+
const { run } = require('../lib/app/custom-button');
|
|
837
|
+
await run(args);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
835
840
|
case 'form-view': {
|
|
836
841
|
const { run } = require('../lib/app/form-view');
|
|
837
842
|
await run(args);
|
package/lib/app/create-form.js
CHANGED
|
@@ -1440,6 +1440,7 @@ function resolveFieldIdReferences(fieldComponents) {
|
|
|
1440
1440
|
if (resolvedId) {
|
|
1441
1441
|
info(t('create_form.filling_rule_resolved', targetLabel, resolvedId));
|
|
1442
1442
|
rule.target = resolvedId;
|
|
1443
|
+
rule.targetFieldId = resolvedId;
|
|
1443
1444
|
} else {
|
|
1444
1445
|
warn(t('create_form.filling_rule_failed', targetLabel));
|
|
1445
1446
|
}
|
|
@@ -1451,6 +1452,7 @@ function resolveFieldIdReferences(fieldComponents) {
|
|
|
1451
1452
|
if (resolvedSourceId) {
|
|
1452
1453
|
info(t('create_form.filling_rule_resolved', sourceLabel, resolvedSourceId));
|
|
1453
1454
|
rule.source = resolvedSourceId;
|
|
1455
|
+
rule.sourceFieldId = resolvedSourceId;
|
|
1454
1456
|
} else {
|
|
1455
1457
|
warn(t('create_form.filling_rule_failed', sourceLabel));
|
|
1456
1458
|
}
|
|
@@ -1476,6 +1478,7 @@ function resolveFieldIdReferences(fieldComponents) {
|
|
|
1476
1478
|
if (resolvedId) {
|
|
1477
1479
|
info(t('create_form.table_rule_resolved', ruleIndex + 1, targetLabel, resolvedId));
|
|
1478
1480
|
rule.target = resolvedId;
|
|
1481
|
+
rule.targetFieldId = resolvedId;
|
|
1479
1482
|
} else {
|
|
1480
1483
|
warn(t('create_form.table_rule_failed', targetLabel));
|
|
1481
1484
|
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
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 FIELDS = ['name', 'description', 'icon', 'actionType', 'actionConfig', 'permissionConfig', 'tableViewUuids', 'detailId'];
|
|
14
|
+
const JSON_FIELDS = FIELDS.filter(key => key !== 'actionType');
|
|
15
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
const idPattern = /^[A-Za-z0-9_-]+$/;
|
|
17
|
+
// Same operator codes as the native custom-button rule editor.
|
|
18
|
+
const OP_CODES = {
|
|
19
|
+
等于: 'Equal', 不等于: 'NotEqual', 大于: 'Bigger', 大于等于: 'BiggerOrEqual', 小于: 'Lesser', 小于等于: 'LesserOrEqual',
|
|
20
|
+
早于: 'Before', 晚于: 'After', 不早于: 'NotBefore', 不晚于: 'NotAfter', 包含: 'Contain', 不包含: 'NotContain',
|
|
21
|
+
开始为: 'BeginWith', 结束为: 'EndWith', 有值: 'ExistValue', 没有值: 'NotExistValue', 等于任意一个: 'EqualAny',
|
|
22
|
+
不等于任意一个: 'NotEqualAny', 在范围内: 'InRange', 不在范围内: 'NotInRange', 属于: 'Contain', 不属于: 'NotContain', 匹配: 'Like',
|
|
23
|
+
};
|
|
24
|
+
const NATIVE_OP_CODES = new Set(Object.values(OP_CODES));
|
|
25
|
+
const SQL_OP_CODES = { eq: 'Equal', ne: 'NotEqual', gt: 'Bigger', ge: 'BiggerOrEqual', lt: 'Lesser', le: 'LesserOrEqual',
|
|
26
|
+
ilike: 'Like', in: 'EqualAny', not_in: 'NotEqualAny', is: 'NotExistValue', is_not: 'ExistValue', between: 'InRange' };
|
|
27
|
+
const EN_OP_CODES = { equal: 'Equal', 'not equal': 'NotEqual', before: 'Before', after: 'After',
|
|
28
|
+
'not before': 'NotBefore', 'not after': 'NotAfter', 'is null': 'NotExistValue', 'is not null': 'ExistValue',
|
|
29
|
+
'in any': 'EqualAny', 'not in any': 'NotEqualAny', like: 'Like', between: 'InRange' };
|
|
30
|
+
const EXCLUDED_VARIABLES = new Set(['originatorCorp', 'processInstanceId', 'createTime', 'modifiedTime', 'currentNodeName', 'taskId']);
|
|
31
|
+
const RULE_TYPES = { TextField: 'rule_text', TextareaField: 'rule_text', SerialNumberField: 'rule_text',
|
|
32
|
+
NumberField: 'rule_number', SelectField: 'rule_select', RadioField: 'rule_radio', DateField: 'rule_date', EmployeeField: 'rule_employee' };
|
|
33
|
+
|
|
34
|
+
function fail(code, key, detail = '', details) {
|
|
35
|
+
throw new CliError(t(`custom_button.${key}`, detail), { code: `CUSTOM_BUTTON_${code}`, details });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseJson(value, label) {
|
|
39
|
+
try { return typeof value === 'string' ? JSON.parse(value) : value; }
|
|
40
|
+
catch (_) { return fail('INVALID_CONFIG', 'invalid_config', label); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safe(value) {
|
|
44
|
+
if (!value || typeof value !== 'object') { return; }
|
|
45
|
+
for (const key of Object.keys(value)) {
|
|
46
|
+
if (['__proto__', 'prototype', 'constructor'].includes(key)) { fail('INVALID_CONFIG', 'invalid_config', key); }
|
|
47
|
+
safe(value[key]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stable(value) {
|
|
52
|
+
if (Array.isArray(value)) { return value.map(stable); }
|
|
53
|
+
return object(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])) : value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function configHash(value) {
|
|
57
|
+
return crypto.createHash('sha256').update(JSON.stringify(stable(writable(value)))).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writable(value) {
|
|
61
|
+
return Object.fromEntries(FIELDS.map(key => [key, value[key] ?? null]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function matchesReadback(saved, expected) {
|
|
65
|
+
return FIELDS.every(key => {
|
|
66
|
+
// The server expands i18n values with fallback translations and nullable
|
|
67
|
+
// metadata. Verify every supplied/preserved value without requiring those
|
|
68
|
+
// derived properties to have existed in the request.
|
|
69
|
+
if (['name', 'description'].includes(key) && object(expected[key]) && object(saved[key])) {
|
|
70
|
+
return Object.entries(expected[key]).every(([locale, value]) => locale === 'pureEn_US'
|
|
71
|
+
|| isDeepStrictEqual(stable(saved[key][locale]), stable(value)));
|
|
72
|
+
}
|
|
73
|
+
return isDeepStrictEqual(stable(saved[key] ?? null), stable(expected[key] ?? null));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalize(raw) {
|
|
78
|
+
if (!object(raw) || typeof raw.buttonUuid !== 'string' || !idPattern.test(raw.buttonUuid)) {
|
|
79
|
+
fail('INVALID_RESPONSE', 'invalid_response', 'buttonUuid');
|
|
80
|
+
}
|
|
81
|
+
const value = { ...raw };
|
|
82
|
+
for (const key of JSON_FIELDS) { value[key] = raw[key] === null || raw[key] === undefined || raw[key] === '' ? null : parseJson(raw[key], key); }
|
|
83
|
+
if (!object(value.actionConfig)) { fail('INVALID_RESPONSE', 'invalid_response', 'actionConfig'); }
|
|
84
|
+
// Match the native editor: individual properties may themselves be JSON
|
|
85
|
+
// strings (arrays, booleans, or translated objects). Plain strings stay as is.
|
|
86
|
+
value.actionConfig = { ...value.actionConfig };
|
|
87
|
+
for (const [key, item] of Object.entries(value.actionConfig)) {
|
|
88
|
+
if (typeof item === 'string') {
|
|
89
|
+
try { value.actionConfig[key] = JSON.parse(item); } catch (_) { /* native scalar string */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const key of ['tableViewUuids', 'detailId']) { value[key] = value[key] || []; }
|
|
93
|
+
safe(value);
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseArgs(args) {
|
|
98
|
+
const positional = [];
|
|
99
|
+
const flags = {};
|
|
100
|
+
for (let i = 0; i < args.length; i++) {
|
|
101
|
+
const arg = args[i];
|
|
102
|
+
if (Object.hasOwn(flags, arg)) { fail('USAGE', 'usage'); }
|
|
103
|
+
if (['--json', '--yes', '--help', '-h'].includes(arg)) { flags[arg] = true; continue; }
|
|
104
|
+
if (['--config', '--backup', '--expected-hash'].includes(arg)) {
|
|
105
|
+
if (!args[i + 1] || args[i + 1].startsWith('-')) { fail('USAGE', 'usage'); }
|
|
106
|
+
flags[arg] = args[++i]; continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg.startsWith('-')) { fail('USAGE', 'usage'); }
|
|
109
|
+
positional.push(arg);
|
|
110
|
+
}
|
|
111
|
+
if (flags['--help'] || flags['-h']) { return { help: true }; }
|
|
112
|
+
const [action, appType, formUuid, buttonUuid] = positional;
|
|
113
|
+
const targeted = ['inspect', 'update', 'delete'].includes(action);
|
|
114
|
+
if (!ACTIONS.includes(action) || positional.length !== (targeted ? 4 : 3)
|
|
115
|
+
|| [appType, formUuid, ...(targeted ? [buttonUuid] : [])].some(id => !id || !idPattern.test(id))
|
|
116
|
+
|| (['create', 'update'].includes(action) && !flags['--config'])
|
|
117
|
+
|| (action === 'delete' && (!flags['--yes'] || !flags['--expected-hash']))) { fail('USAGE', 'usage'); }
|
|
118
|
+
const allowed = {
|
|
119
|
+
list: [], inspect: [], options: [], create: ['--config', '--backup'],
|
|
120
|
+
update: ['--config', '--backup', '--expected-hash'], delete: ['--yes', '--backup', '--expected-hash'],
|
|
121
|
+
};
|
|
122
|
+
if (Object.keys(flags).some(key => key !== '--json' && !allowed[action].includes(key))) { fail('USAGE', 'usage'); }
|
|
123
|
+
return { action, appType, formUuid, buttonUuid, flags };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unwrap(response, endpoint) {
|
|
127
|
+
if (!response || response.success !== true || response.__needLogin || response.__csrfExpired || response.content === false) {
|
|
128
|
+
fail('API_ERROR', 'api_error', response?.errorMsg || response?.message || endpoint,
|
|
129
|
+
{ endpoint, platformErrorCode: response?.errorCode, needLogin: !!response?.__needLogin });
|
|
130
|
+
}
|
|
131
|
+
return response.content;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function array(value, label) {
|
|
135
|
+
if (!Array.isArray(value)) { fail('INVALID_RESPONSE', 'invalid_response', label); }
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ids(value, label, nonempty = false) {
|
|
140
|
+
if (!Array.isArray(value) || (nonempty && !value.length) || value.some(id => typeof id !== 'string' || !idPattern.test(id))
|
|
141
|
+
|| new Set(value).size !== value.length) { fail('INVALID_CONFIG', 'invalid_config', label); }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function i18n(value, label, required) {
|
|
145
|
+
if (!required && value === null) { return; }
|
|
146
|
+
if (!object(value) || (required && !Object.entries(value).some(([key, text]) => key !== 'type' && typeof text === 'string' && text.trim()))
|
|
147
|
+
|| Object.entries(value).some(([key, text]) => key !== 'type' && text !== null && typeof text !== 'string')) {
|
|
148
|
+
fail('INVALID_CONFIG', 'invalid_config', label);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function patchConfig(base, patch) {
|
|
153
|
+
if (!object(patch) || !Object.keys(patch).length || Object.keys(patch).some(key => !FIELDS.includes(key))) {
|
|
154
|
+
fail('INVALID_CONFIG', 'invalid_config', FIELDS.join(', '));
|
|
155
|
+
}
|
|
156
|
+
safe(patch);
|
|
157
|
+
for (const key of ['name', 'description', 'icon', 'actionConfig']) {
|
|
158
|
+
if (patch[key] !== undefined && !(patch[key] === null && ['description', 'icon'].includes(key)) && !object(patch[key])) {
|
|
159
|
+
fail('INVALID_CONFIG', 'invalid_config', key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const changedAction = (patch.actionType !== undefined && patch.actionType !== base.actionType)
|
|
163
|
+
|| (patch.actionConfig?.fillType !== undefined && patch.actionConfig.fillType !== base.actionConfig?.fillType)
|
|
164
|
+
|| (patch.actionConfig?.urlType !== undefined && patch.actionConfig.urlType !== base.actionConfig?.urlType);
|
|
165
|
+
const next = { ...base, ...patch };
|
|
166
|
+
// Translations merge. Arrays, conditions and a changed action replace.
|
|
167
|
+
for (const key of ['name', 'description', 'icon', 'actionConfig']) {
|
|
168
|
+
if (patch[key] === undefined && !(key === 'actionConfig' && changedAction)) { next[key] = base[key]; continue; }
|
|
169
|
+
if (patch[key] === null) { next[key] = null; continue; }
|
|
170
|
+
next[key] = key === 'actionConfig' && changedAction ? { ...patch[key] } : { ...base[key], ...patch[key] };
|
|
171
|
+
}
|
|
172
|
+
// Reference validation enriches action/rule metadata. Never mutate the read
|
|
173
|
+
// snapshot used for backups, concurrency checks or no-op comparisons.
|
|
174
|
+
return JSON.parse(JSON.stringify(next));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validateConfig(value) {
|
|
178
|
+
i18n(value.name, 'name', true);
|
|
179
|
+
if (Object.entries(value.name).some(([locale, text]) => /^[a-z]{2}_[A-Z]{2}$/.test(locale)
|
|
180
|
+
&& typeof text === 'string' && text.length > 10)) { fail('INVALID_CONFIG', 'invalid_config', 'name (max 10)'); }
|
|
181
|
+
i18n(value.description, 'description', false);
|
|
182
|
+
if ((value.icon !== null && !object(value.icon)) || !object(value.actionConfig)) { fail('INVALID_CONFIG', 'invalid_config', 'icon / actionConfig'); }
|
|
183
|
+
ids(value.tableViewUuids, 'tableViewUuids'); ids(value.detailId, 'detailId');
|
|
184
|
+
const config = value.actionConfig;
|
|
185
|
+
if (value.actionType === 'fillInForm') {
|
|
186
|
+
if (config.fillType === 'fillCurrentForm') { ids(config.fieldsSelected, 'fieldsSelected', true); }
|
|
187
|
+
else if (config.fillType !== 'fillLinkageForm' || !config.linkageFormId) { fail('INVALID_CONFIG', 'invalid_config', 'fillType / linkageFormId'); }
|
|
188
|
+
} else if (value.actionType === 'invokeFlow') {
|
|
189
|
+
if (!config.processCode || !Array.isArray(config.needSecondCheck)
|
|
190
|
+
|| config.needSecondCheck.length > 1 || config.needSecondCheck.some(item => item !== 'y')) {
|
|
191
|
+
fail('INVALID_CONFIG', 'invalid_config', 'processCode / needSecondCheck');
|
|
192
|
+
}
|
|
193
|
+
} else if (value.actionType === 'linkToPage') {
|
|
194
|
+
if (!['new', 'slide'].includes(config.openWith)) { fail('INVALID_CONFIG', 'invalid_config', 'openWith'); }
|
|
195
|
+
if (config.urlType === 'outer') {
|
|
196
|
+
let url;
|
|
197
|
+
try { url = new URL(config.url); } catch (_) { fail('INVALID_CONFIG', 'invalid_config', 'url'); }
|
|
198
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) { fail('INVALID_CONFIG', 'invalid_config', 'url'); }
|
|
199
|
+
} else if (config.urlType !== 'inner' || !config.urlFormUuid) { fail('INVALID_CONFIG', 'invalid_config', 'urlType / urlFormUuid'); }
|
|
200
|
+
if (config.paramInfo !== undefined && (!Array.isArray(config.paramInfo) || config.paramInfo.some(param => !object(param)
|
|
201
|
+
|| typeof param.paramName !== 'string' || !param.paramName.trim()
|
|
202
|
+
|| !['constant', 'variable', 'formula'].includes(param.paramType) || param.paramValue === undefined))) {
|
|
203
|
+
fail('INVALID_CONFIG', 'invalid_config', 'paramInfo');
|
|
204
|
+
}
|
|
205
|
+
} else { fail('INVALID_CONFIG', 'invalid_config', 'actionType'); }
|
|
206
|
+
if (value.permissionConfig !== null && !object(value.permissionConfig)) { fail('INVALID_CONFIG', 'invalid_config', 'permissionConfig'); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function execute(parsed, client = createYidaClient()) {
|
|
210
|
+
const { action, appType, formUuid, buttonUuid, flags } = parsed;
|
|
211
|
+
const target = { appType, formUuid, ...(buttonUuid ? { buttonUuid } : {}) };
|
|
212
|
+
const prefix = `/dingtalk/web/${appType}/query/`;
|
|
213
|
+
const read = async (endpoint, params = {}) => unwrap(await client.get(prefix + endpoint, { formUuid, ...params }), endpoint);
|
|
214
|
+
const list = async () => {
|
|
215
|
+
const rows = array(await read('customButtonManage/list.json'), 'buttons').map(normalize);
|
|
216
|
+
if (rows.some(row => row.formUuid && row.formUuid !== formUuid)
|
|
217
|
+
|| new Set(rows.map(row => row.buttonUuid)).size !== rows.length) { fail('INVALID_RESPONSE', 'invalid_response', 'buttons'); }
|
|
218
|
+
return rows;
|
|
219
|
+
};
|
|
220
|
+
const metadata = async () => {
|
|
221
|
+
const info = await read('formdesign/getFormAndAppInfo.json', { appType });
|
|
222
|
+
const fields = array(info?.formDatas?.values?.[0]?.fields, 'fields');
|
|
223
|
+
const variables = array(await read('formProcInstData/getFormVariables.json', { appType, operatorMode: 'local' }), 'variables')
|
|
224
|
+
.filter(field => !EXCLUDED_VARIABLES.has(field.componentId) && Array.isArray(field.operatorMap) && field.operatorMap.length);
|
|
225
|
+
const relatedForms = array(await read('formdata/getAssociatedForm.json'), 'relatedForms');
|
|
226
|
+
const forms = array((await read('formdesign/getFormListOrder.json'))?.data, 'pages');
|
|
227
|
+
const flows = array((await read('appLogicflowBinding/listflowByFormUuid.json', { type: 6 }))?.data, 'flows');
|
|
228
|
+
const views = array(await read('dataview/listDataViewInForm.json', { appType }), 'views').filter(view => view.viewType === 'table');
|
|
229
|
+
const details = array(await read('configdesign/listConfigDesignInfo.json', { relationCode: 'DETAIL' }), 'details');
|
|
230
|
+
return { fields, variables, relatedForms, pages: forms, flows, tableViews: views, detailPages: details };
|
|
231
|
+
};
|
|
232
|
+
const output = { success: true, action, ...target };
|
|
233
|
+
if (action === 'options') { return { ...output, options: await metadata() }; }
|
|
234
|
+
const beforeList = await list();
|
|
235
|
+
const before = buttonUuid ? beforeList.find(button => button.buttonUuid === buttonUuid) : null;
|
|
236
|
+
if (buttonUuid && !before) { fail('NOT_FOUND', 'not_found', buttonUuid); }
|
|
237
|
+
if (action === 'list') { return { ...output, buttons: beforeList, totalCount: beforeList.length }; }
|
|
238
|
+
if (action === 'inspect') { return { ...output, button: before, configHash: configHash(before) }; }
|
|
239
|
+
if (flags['--expected-hash'] && flags['--expected-hash'] !== configHash(before)) { fail('CONFLICT', 'conflict'); }
|
|
240
|
+
let expected;
|
|
241
|
+
if (action !== 'delete') {
|
|
242
|
+
const patch = parseJson(fs.readFileSync(flags['--config'], 'utf8'), 'config');
|
|
243
|
+
if (action === 'create' && beforeList.length >= 10) { fail('LIMIT', 'limit'); }
|
|
244
|
+
const defaults = { name: {}, description: {}, icon: { type: 'icon', value: 'icon_diagonal_outlined' },
|
|
245
|
+
actionConfig: {}, permissionConfig: null, tableViewUuids: [], detailId: [] };
|
|
246
|
+
expected = patchConfig(before || defaults, patch);
|
|
247
|
+
if (expected.actionType === 'invokeFlow' && expected.actionConfig.needSecondCheck === undefined) {
|
|
248
|
+
expected.actionConfig.needSecondCheck = ['y'];
|
|
249
|
+
}
|
|
250
|
+
validateConfig(expected);
|
|
251
|
+
const options = await metadata();
|
|
252
|
+
validateReferences(expected, options, formUuid);
|
|
253
|
+
if (before && isDeepStrictEqual(stable(writable(expected)), stable(writable(before)))) {
|
|
254
|
+
return { ...output, changed: false, button: before, configHash: configHash(before), readbackVerified: true };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// List includes display bindings. A concurrent edit anywhere stops the write.
|
|
258
|
+
if (!isDeepStrictEqual(stable(beforeList.map(row => [row.buttonUuid, configHash(row)])),
|
|
259
|
+
stable((await list()).map(row => [row.buttonUuid, configHash(row)])))) { fail('CONFLICT', 'conflict'); }
|
|
260
|
+
const backupPath = flags['--backup'] ? path.resolve(flags['--backup']) : path.join(resolveProjectRoot().projectRoot,
|
|
261
|
+
'.cache', 'openyida', 'custom-button', `${Date.now()}-${crypto.randomUUID()}.json`);
|
|
262
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
263
|
+
fs.writeFileSync(backupPath, JSON.stringify({ ...target, action, buttons: beforeList }, null, 2), { flag: 'wx', mode: 0o600 });
|
|
264
|
+
const mutationDetails = { target, backupPath, retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true };
|
|
265
|
+
const endpoint = action === 'delete' ? 'customButtonManage/delete.json' : 'customButtonManage/saveButtonConfig.json';
|
|
266
|
+
const params = action === 'delete' ? { formUuid, buttonUuid } : serialize(expected, formUuid, buttonUuid);
|
|
267
|
+
try { unwrap(await client.postFormOnce(prefix + endpoint, params), endpoint); }
|
|
268
|
+
catch (error) { throw new CliError(error.message, { code: error.code || 'CUSTOM_BUTTON_WRITE_FAILED', details: { ...error.details, ...mutationDetails } }); }
|
|
269
|
+
try {
|
|
270
|
+
const after = await list();
|
|
271
|
+
let saved;
|
|
272
|
+
if (action === 'delete') {
|
|
273
|
+
if (after.some(row => row.buttonUuid === buttonUuid)) { fail('READBACK_MISMATCH', 'readback_mismatch'); }
|
|
274
|
+
} else {
|
|
275
|
+
const added = after.filter(row => !beforeList.some(old => old.buttonUuid === row.buttonUuid));
|
|
276
|
+
saved = action === 'create' ? (added.length === 1 ? added[0] : null) : after.find(row => row.buttonUuid === buttonUuid);
|
|
277
|
+
if (!saved || !matchesReadback(saved, expected)) { fail('READBACK_MISMATCH', 'readback_mismatch'); }
|
|
278
|
+
}
|
|
279
|
+
return { ...output, buttonUuid: saved?.buttonUuid || buttonUuid, changed: true, backupPath, readbackVerified: true,
|
|
280
|
+
...(saved ? { button: saved, configHash: configHash(saved) } : {}) };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new CliError(error.message, { code: error.code || 'CUSTOM_BUTTON_READBACK_FAILED',
|
|
283
|
+
details: { ...mutationDetails, mutationAccepted: true, sideEffectState: 'accepted_unverified' } });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function validateReferences(value, options, formUuid) {
|
|
288
|
+
const config = value.actionConfig;
|
|
289
|
+
const check = (condition, label) => { if (!condition) { fail('INVALID_CONFIG', 'invalid_config', label); } };
|
|
290
|
+
for (const id of value.tableViewUuids) { check(options.tableViews.some(view => view.viewUuid === id), `tableViewUuids: ${id}`); }
|
|
291
|
+
for (const id of value.detailId) { check(options.detailPages.some(page => page.designId === id), `detailId: ${id}`); }
|
|
292
|
+
if (value.actionType === 'fillInForm') {
|
|
293
|
+
if (config.fillType === 'fillCurrentForm') {
|
|
294
|
+
for (const id of config.fieldsSelected) { check(options.fields.some(field => field.fieldId === id), `fieldsSelected: ${id}`); }
|
|
295
|
+
value.relationId = formUuid;
|
|
296
|
+
} else {
|
|
297
|
+
const form = options.relatedForms.find(form => form.formUuid === config.linkageFormId);
|
|
298
|
+
check(form && form.appType && form.formType, 'linkageFormId');
|
|
299
|
+
check(!config.linkageFormAppType || config.linkageFormAppType === form.appType, 'linkageFormAppType');
|
|
300
|
+
check(!config.linkageFormType || config.linkageFormType === form.formType, 'linkageFormType');
|
|
301
|
+
config.linkageFormAppType = form.appType; config.linkageFormType = form.formType;
|
|
302
|
+
value.relationId = form.formUuid;
|
|
303
|
+
}
|
|
304
|
+
} else if (value.actionType === 'invokeFlow') {
|
|
305
|
+
check(options.flows.some(flow => flow.processCode === config.processCode && flow.status === 'y'), 'processCode (published type 6)');
|
|
306
|
+
value.relationId = config.processCode;
|
|
307
|
+
} else if (config.urlType === 'inner') {
|
|
308
|
+
const page = options.pages.find(page => page.formUuid === config.urlFormUuid);
|
|
309
|
+
check(page && ['receipt', 'process', 'report', 'display', 'portal', 'manage', 'datav'].includes(page.formType), 'urlFormUuid');
|
|
310
|
+
check(!config.urlFormType || config.urlFormType === page.formType, 'urlFormType');
|
|
311
|
+
config.urlFormType = page.formType;
|
|
312
|
+
}
|
|
313
|
+
const visit = (group, parentId) => {
|
|
314
|
+
check(object(group) && ['AND', 'OR'].includes(group.condition) && Array.isArray(group.rules) && group.rules.length, 'permissionConfig');
|
|
315
|
+
group.conditionCode = group.condition === 'AND' ? '&&' : '||';
|
|
316
|
+
group.ruleId = group.ruleId || `group-${crypto.randomUUID()}`;
|
|
317
|
+
if (parentId) { group.parentId = parentId; }
|
|
318
|
+
for (const rule of group.rules) {
|
|
319
|
+
check(object(rule), 'permissionConfig.rules');
|
|
320
|
+
if (rule.rules) { visit(rule, group.ruleId); continue; }
|
|
321
|
+
const field = options.variables.find(field => field.id === rule.id);
|
|
322
|
+
// Platform metadata uses SQL keys (eq, in, is...) while saved editor
|
|
323
|
+
// trees may use labels or canonical codes. Resolve through the same field.
|
|
324
|
+
const operator = field?.operatorMap?.find(op => op.value === rule.op || op.label === rule.op || op.labelEn === rule.op
|
|
325
|
+
|| (OP_CODES[op.label] || EN_OP_CODES[op.labelEn] || SQL_OP_CODES[op.value]) === rule.op);
|
|
326
|
+
check(operator, `permissionConfig: ${rule.id} / ${rule.op}`);
|
|
327
|
+
const code = operator.opCode || OP_CODES[operator.label] || EN_OP_CODES[operator.labelEn] || SQL_OP_CODES[operator.value]
|
|
328
|
+
|| OP_CODES[rule.op] || (NATIVE_OP_CODES.has(rule.op) ? rule.op : null);
|
|
329
|
+
check(code && (rule.opCode === undefined || rule.opCode === code), 'permissionConfig.opCode');
|
|
330
|
+
rule.opCode = code;
|
|
331
|
+
if (rule.valueType === undefined) { rule.valueType = rule.extValue === 'column' ? 'column' : 'literal'; }
|
|
332
|
+
check(['literal', 'column'].includes(rule.valueType), 'permissionConfig.valueType');
|
|
333
|
+
check(rule.value !== undefined || ['ExistValue', 'NotExistValue'].includes(code), 'permissionConfig.value');
|
|
334
|
+
// Native runtime evaluates ruleValue using the component's rule parser;
|
|
335
|
+
// id/op/value alone renders in the editor but cannot execute correctly.
|
|
336
|
+
rule.ruleId = rule.ruleId || `item-${crypto.randomUUID()}`;
|
|
337
|
+
rule.parentId = group.ruleId;
|
|
338
|
+
if (field.componentName) { rule.componentType = field.componentName; }
|
|
339
|
+
rule.ruleType = rule.ruleType || RULE_TYPES[rule.componentType] || 'rule_text';
|
|
340
|
+
rule.name = rule.name || field.varName?.zh_CN || field.varName?.en_US || rule.id;
|
|
341
|
+
rule.extValue = rule.valueType === 'column' ? 'column' : 'value';
|
|
342
|
+
rule.ruleValue = rule.value ?? '';
|
|
343
|
+
if (rule.value === undefined) { rule.value = ''; }
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
if (value.permissionConfig) { visit(value.permissionConfig); }
|
|
347
|
+
for (const param of config.paramInfo || []) {
|
|
348
|
+
if (param.paramType === 'variable') {
|
|
349
|
+
check(['formUuid', 'formInstId'].includes(param.paramValue) || options.fields.some(field => field.fieldId === param.paramValue), 'paramInfo.paramValue');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function serialize(value, formUuid, buttonUuid) {
|
|
355
|
+
const params = { formUuid, buttonUuid: buttonUuid || '' };
|
|
356
|
+
for (const key of FIELDS) {
|
|
357
|
+
params[key] = JSON_FIELDS.includes(key)
|
|
358
|
+
? (value[key] === null || (['tableViewUuids', 'detailId'].includes(key) && !value[key].length) ? '' : JSON.stringify(value[key])) : value[key];
|
|
359
|
+
}
|
|
360
|
+
if (value.actionType !== 'linkToPage') { params.relationId = value.relationId; }
|
|
361
|
+
return params;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function run(args, options = {}) {
|
|
365
|
+
const parsed = parseArgs(args);
|
|
366
|
+
if (parsed.help) { process.stdout.write(t('custom_button.usage') + '\n'); return; }
|
|
367
|
+
const result = await execute(parsed, options.client);
|
|
368
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = { run, execute, parseArgs, normalize, configHash, patchConfig, validateConfig, validateReferences, serialize };
|
|
@@ -1375,6 +1375,7 @@ function resolveFieldIdReferences(fieldComponents) {
|
|
|
1375
1375
|
const resolvedId = labelToFieldId[targetLabel];
|
|
1376
1376
|
if (resolvedId) {
|
|
1377
1377
|
rule.target = resolvedId;
|
|
1378
|
+
rule.targetFieldId = resolvedId;
|
|
1378
1379
|
}
|
|
1379
1380
|
}
|
|
1380
1381
|
// 解析 source 中的 @label:xxx 引用
|
|
@@ -1383,6 +1384,7 @@ function resolveFieldIdReferences(fieldComponents) {
|
|
|
1383
1384
|
const resolvedSourceId = labelToFieldId[sourceLabel];
|
|
1384
1385
|
if (resolvedSourceId) {
|
|
1385
1386
|
rule.source = resolvedSourceId;
|
|
1387
|
+
rule.sourceFieldId = resolvedSourceId;
|
|
1386
1388
|
}
|
|
1387
1389
|
}
|
|
1388
1390
|
});
|
|
@@ -1403,6 +1405,7 @@ function resolveFieldIdReferences(fieldComponents) {
|
|
|
1403
1405
|
const resolvedId = labelToFieldId[targetLabel];
|
|
1404
1406
|
if (resolvedId) {
|
|
1405
1407
|
rule.target = resolvedId;
|
|
1408
|
+
rule.targetFieldId = resolvedId;
|
|
1406
1409
|
}
|
|
1407
1410
|
}
|
|
1408
1411
|
});
|
|
@@ -542,6 +542,12 @@ const COMMAND_SIDE_EFFECTS = new Map([
|
|
|
542
542
|
mutating_actions: ['connect', 'run', 'disconnect', 'logout'],
|
|
543
543
|
note: 'Explicit local-agent control. Remote runs use only the current task grant and never read a personal OpenYida profile.',
|
|
544
544
|
})],
|
|
545
|
+
['custom-button', sideEffect('mixed', {
|
|
546
|
+
mutates_yida: true,
|
|
547
|
+
mutates_local: true,
|
|
548
|
+
read_actions: ['list', 'inspect', 'options'],
|
|
549
|
+
mutating_actions: ['create', 'update', 'delete'],
|
|
550
|
+
})],
|
|
545
551
|
['cool-card', sideEffect('mixed', { mutates_yida: true, mutates_local: true, read_actions: ['list', 'templates', 'get'], mutating_actions: ['create'] })],
|
|
546
552
|
['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.' })],
|
|
547
553
|
['form-view', sideEffect('mixed', {
|
|
@@ -847,6 +853,13 @@ const COMMAND_PERMISSIONS = new Map([
|
|
|
847
853
|
preauthorized_actions: ['save', 'create', 'order'],
|
|
848
854
|
})),
|
|
849
855
|
|
|
856
|
+
...permissionEntries([
|
|
857
|
+
'custom-button',
|
|
858
|
+
], actionDependentPermission({
|
|
859
|
+
preauthorized_actions: ['create', 'update'],
|
|
860
|
+
ask_actions: ['delete'],
|
|
861
|
+
})),
|
|
862
|
+
|
|
850
863
|
...permissionEntries([
|
|
851
864
|
'aggregate-table',
|
|
852
865
|
], actionDependentPermission({
|
|
@@ -1608,6 +1621,19 @@ const COMMAND_GROUPS = [
|
|
|
1608
1621
|
examples: ['openyida form-view list APP_XXX FORM_XXX --with-config --json', 'openyida form-view save APP_XXX FORM_XXX VIEW_XXX --config view-patch.json --json'],
|
|
1609
1622
|
notes: ['Reads native multiViewInfos and per-view staticConfig/defaultConfig. Saves a recursive object patch; arrays, filter and dataRange replace. --replace restores full staticConfig/defaultConfig and requires --expected-hash. showFields controls visibility and order, widths controls pixel widths, lockFieldIds controls frozen columns, filter controls default filtering, staticConfig.columnFilter controls available fields. Backups precede mutations and success requires readback. --expected-hash detects stale edits; the platform has no atomic compare-and-swap. Personal browser view settings can override published defaults. create adds a table view; order requires every existing view UUID exactly once. No delete operation.'],
|
|
1610
1623
|
}),
|
|
1624
|
+
command('custom-button', ['custom-button'], 'custom-button <list|inspect|options|create|update|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]', 'help.cmd_custom_button', {
|
|
1625
|
+
managedRun: { scope: 'actions', actions: {
|
|
1626
|
+
list: { scope: 'app', appPosition: 1 },
|
|
1627
|
+
inspect: { scope: 'app', appPosition: 1 },
|
|
1628
|
+
options: { scope: 'app', appPosition: 1 },
|
|
1629
|
+
create: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
|
|
1630
|
+
update: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
|
|
1631
|
+
delete: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
|
|
1632
|
+
} },
|
|
1633
|
+
output: 'json',
|
|
1634
|
+
examples: ['openyida custom-button options APP_XXX FORM_XXX --json', 'openyida custom-button create APP_XXX FORM_XXX --config button.json --json'],
|
|
1635
|
+
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.'],
|
|
1636
|
+
}),
|
|
1611
1637
|
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 },
|
|
1612
1638
|
examples: ['openyida get-schema APP_XXX FORM_XXX --field-map-json'],
|
|
1613
1639
|
}),
|
package/lib/core/locales/en.js
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
custom_button: {
|
|
8
|
+
'usage': 'Usage: openyida custom-button <list|inspect|options|create|update|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]',
|
|
9
|
+
'invalid_config': 'Invalid configuration or argument: {0}',
|
|
10
|
+
'invalid_response': 'Incomplete platform custom-button response: {0}',
|
|
11
|
+
'api_error': 'Custom button API failed: {0}. Check login, permissions and edition availability.',
|
|
12
|
+
'not_found': 'Button does not exist in the target form: {0}',
|
|
13
|
+
'conflict': 'Configuration changed. Inspect again before saving.',
|
|
14
|
+
'limit': 'A form supports at most 10 custom buttons.',
|
|
15
|
+
'readback_mismatch': 'Saved custom button does not match readback. Keep the backup and inspect remote state.'
|
|
16
|
+
},
|
|
7
17
|
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'},
|
|
8
18
|
cool_card: {'description': 'Create and read native YiDA cool cards (interactive and pinned)', 'usage': 'Usage: openyida cool-card <list|templates|create|get> <appType> [cardPageCode] [--type im|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]', 'failed': 'Native cool card operation stopped: {0}'},
|
|
9
19
|
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.'},
|
|
@@ -124,6 +134,7 @@ module.exports = {
|
|
|
124
134
|
|
|
125
135
|
// ── bin/yida.js printHelp() ────────────────────────
|
|
126
136
|
help: {
|
|
137
|
+
cmd_custom_button: 'Native custom buttons: inspect and configure form edits, automation flows and page links',
|
|
127
138
|
cmd_check_design: 'Validate design documents, theme tokens and PRD handoff',
|
|
128
139
|
subtitle: 'AI-powered low-code development tool for Yida',
|
|
129
140
|
usage: 'Usage:',
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
custom_button: {
|
|
8
|
+
'usage': '用法:openyida custom-button <list|inspect|options|create|update|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]',
|
|
9
|
+
'invalid_config': '配置或参数无效:{0}',
|
|
10
|
+
'invalid_response': '平台返回的按钮配置不完整:{0}',
|
|
11
|
+
'api_error': '自定义按钮接口失败:{0}。请检查登录、权限和版本支持。',
|
|
12
|
+
'not_found': '按钮不存在于目标表单:{0}',
|
|
13
|
+
'conflict': '配置已变化,请重新查询后再保存。',
|
|
14
|
+
'limit': '单个表单最多支持 10 个自定义按钮。',
|
|
15
|
+
'readback_mismatch': '保存后的配置与回读不一致,请保留备份并检查远端状态。'
|
|
16
|
+
},
|
|
7
17
|
dws_external: {'note': 'dws 是独立的外部工具;OpenYida 仅提供安装指引,不下载或安装它。', 'install_guide': '显示钉钉 CLI 官方安装指引'},
|
|
8
18
|
cool_card: {'description': '创建和读取宜搭原生酷卡片(交互卡片与置顶卡片)', 'usage': '用法: openyida cool-card <list|templates|create|get> <appType> [cardPageCode] [--type im|onebox] [--name <title>] [--page <n>] [--size <n>] [--search <text>] [--out <file>] [--json]', 'failed': '原生酷卡片操作已停止:{0}'},
|
|
9
19
|
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':'回读配置或发布状态与预期不一致,请保留备份并检查卡片后再重试。'},
|
|
@@ -124,6 +134,7 @@ module.exports = {
|
|
|
124
134
|
|
|
125
135
|
// ── bin/yida.js printHelp() ────────────────────────
|
|
126
136
|
help: {
|
|
137
|
+
cmd_custom_button: '原生自定义按钮:查询、配置填写表单、自动化流及页面跳转',
|
|
127
138
|
cmd_check_design: '检查设计文档、主题变量与 PRD 交接',
|
|
128
139
|
subtitle: '宜搭低代码 AI 开发工具',
|
|
129
140
|
usage: '用法:',
|
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-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` |
|
|
@@ -198,6 +198,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
|
|
|
198
198
|
| 增加待办中心、审批工作台或任务页签,但未指定自定义页面 | 不加载 `yida-task-center`、不默认创建自定义待办页面;先复用原生入口,已有入口整理用 `yida-nav-group`。完整应用仍由 `yida-app` 编排;Agent 建议自定义实现时先 ask_human,明确同意后才交接;Fast、通用授权和已有 Canvas 均不能替代该选择 |
|
|
199
199
|
| 业务操作队列、业务工作台或办事门户页面 | `yida-canvas-custom-page`;接业务数据时加载 `yida-canvas-data-binding`,不因页面名称或辅助统计转为经营看板;不顺带新增自定义审批待办中心 |
|
|
200
200
|
| 批量录入、表格填写、多行编辑 | 默认 `yida-canvas-table-form`;已检测到平台 JSX 组件页面、native 页面或存量源码使用 `this.utils.yida.saveFormData` 时用 `yida-table-form` |
|
|
201
|
+
| 页面设置中的自定义按钮、客户跟进/分配、审批后发起订单、按钮触发自动化或打开页面 | `yida-custom-button`;配置原生按钮定义和显示位置,自定义页面内 React 按钮不走此入口 |
|
|
201
202
|
| 原生数据管理页视图列表、显示列配置、字段显隐与顺序、列宽、冻结列、默认筛选、多视图 | `yida-form-view`;使用原生配置读写,保存前备份并回读,不通过 CSS 隐藏字段或重建自定义列表 |
|
|
202
203
|
| 当前数据管理页换风格、只留横线、突出新增/筛选/行操作、列级强调、状态列标签、数据管理 UI 异常、自定义简单数据管理 | `yida-data-manage-ui`(优先于通用美化);保留承载类型和业务,从目标推导维度、定位 token/CSS 并按页改写;备份发布并实测,不要求选择配方 |
|
|
203
204
|
| 页面视觉方向、页面美化、去 AI 味、优化应用 UI、不好看、太乱、美化后不满意或恢复 | `yida-design`;已有资源优先走 `sub_skill/ui-improve/SKILL.md`,先展示视觉方案,经 `ask_human` 确认并备份后才交接实施,只改 UI;不满意恢复本轮前状态。新页面设计沿用原设计流程 |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yida-custom-button
|
|
3
|
+
description: 配置已有普通表单或流程表单的原生自定义按钮。用于客户跟进、线索分配、审批后发起订单、触发自动化或打开业务页面;不用于自定义页面内的 React 按钮。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 表单自定义按钮
|
|
7
|
+
|
|
8
|
+
使用 `openyida custom-button` 管理「页面设置 > 自定义按钮」中的按钮定义及显示位置。
|
|
9
|
+
|
|
10
|
+
| 业务场景 | 动作 |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| 分配客户跟进人、员工转正、补充当前记录 | `fillInForm` / `fillCurrentForm`,选择可修改字段 |
|
|
13
|
+
| 从客户填写跟进记录、采购审批后发起订单 | `fillInForm` / `fillLinkageForm`,选择平台返回的被关联表单 |
|
|
14
|
+
| 手动执行复杂业务处理 | `invokeFlow`,绑定当前表单已发布的自定义按钮手动触发流(type 6) |
|
|
15
|
+
| 查看业务说明、进入应用页面或外部系统 | `linkToPage`,应用内页面或 HTTP(S) 链接 |
|
|
16
|
+
|
|
17
|
+
先查目标表单的按钮和可选资源,按业务意图编写配置。不要仅凭表单名猜字段或资源 ID。
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
openyida custom-button list <appType> <formUuid> --json
|
|
21
|
+
openyida custom-button options <appType> <formUuid> --json
|
|
22
|
+
openyida custom-button inspect <appType> <formUuid> <buttonUuid> --json
|
|
23
|
+
openyida custom-button create <appType> <formUuid> --config button.json --json
|
|
24
|
+
openyida custom-button update <appType> <formUuid> <buttonUuid> --config patch.json --expected-hash <configHash> --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`options` 返回字段、筛选变量、被关联表单、页面、type 6 自动化流、表格视图和自定义详情页。没有符合条件的流时,用 `yida-integration` 配置按钮手动触发的自动化;本命令不会创建或执行自动化。
|
|
28
|
+
|
|
29
|
+
按钮名称的每种语言文案最长 10 个字符。平台会自动补齐备用翻译与空元数据;CLI 回读会核对显式设置和原有翻译,接受这些平台补充项。
|
|
30
|
+
|
|
31
|
+
可用条件会补齐原生字段类型、规则类型、`ruleValue` 和组/条目 ID;只保存 `id/op/value` 可能在编辑器中可见却无法执行。真实测试应分别验证符合与不符合条件的记录。创建手动触发流当前需在平台设计器完成,再从 `options` 返回的已发布流中绑定。
|
|
32
|
+
|
|
33
|
+
修改当前记录示例(用查询得到的 ID 替换占位符):
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"name": {"type":"i18n","zh_CN":"分配","en_US":"Assign"},
|
|
38
|
+
"actionType":"fillInForm",
|
|
39
|
+
"actionConfig": {"fillType":"fillCurrentForm","fieldsSelected":["实际字段ID"]},
|
|
40
|
+
"tableViewUuids":["实际表格视图ID"],
|
|
41
|
+
"detailId":[]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
关联表单的 `actionConfig` 使用 `fillType: fillLinkageForm`、`linkageFormId`。CLI 根据关联列表补齐 `linkageFormAppType` 和 `linkageFormType`;自动填充取决于目标表单已有的关联填充配置。
|
|
46
|
+
|
|
47
|
+
自动化使用 `actionType: invokeFlow`、`actionConfig: {"processCode":"实际流程编码","needSecondCheck":["y"]}`。二次确认默认开启,显式 `[]` 才关闭。这是成员点击按钮时的提示。
|
|
48
|
+
|
|
49
|
+
页面跳转使用 `actionType: linkToPage`,`actionConfig` 中:
|
|
50
|
+
|
|
51
|
+
- 应用内:`urlType: inner`、`urlFormUuid`,CLI 补齐 `urlFormType`。
|
|
52
|
+
- 外部:`urlType: outer`、`url: https://...`。
|
|
53
|
+
- `openWith` 为 `new` 或 `slide`。可选 `paramInfo` 是 `{paramName,paramType,paramValue}` 数组,类型为 `constant`、`variable`、`formula`;字段或公式从实际元数据生成。
|
|
54
|
+
|
|
55
|
+
可用条件使用 `permissionConfig` 的平台规则树,`null` 表示全部数据。条件按完整树替换,格式为 `{"condition":"AND","rules":[{"id":"实际字段ID","op":"eq","value":"目标值"}]}`。`id` 和 `op` 从 `options.variables[].operatorMap` 取得;CLI 根据该字段支持的操作符解析 SQL 键、原生标签或标准码,补齐 `conditionCode`、`opCode` 和默认 `valueType: literal`。已有树可保留有效的 `opCode` 与 `valueType`,不匹配的操作码会被拒绝。不要将数据查询条件或权限组规则直接传入。公式只使用已有有效平台表达式。隐藏/提示属性受平台能力约束,不绕过灰度或版本限制。
|
|
56
|
+
|
|
57
|
+
更新时多语言文案与同类动作属性合并,数组及条件整体替换。切换 `actionType`、`fillType` 或 `urlType` 时提供完整新 `actionConfig`。保留未修改的翻译、条件和显示位置;`tableViewUuids: []` / `detailId: []` 清空对应显示引用。旧版详情页和自定义详情布局还可能需要在对应设计器中添加引用与排序,本命令不重写详情布局或系统按钮顺序。
|
|
58
|
+
|
|
59
|
+
删除定义需要用户明确要求,先 `inspect` 确认目标,再使用真实 hash:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
openyida custom-button delete <appType> <formUuid> <buttonUuid> --expected-hash <configHash> --yes --json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
删除定义与移除显示引用不同。备份包含原定义,但重建会得到新 UUID,不能自动恢复其他地方的旧引用。仅要求隐藏某处按钮时,调整对应引用。
|
|
66
|
+
|
|
67
|
+
写入前自动备份,成功需返回 `readbackVerified: true`。超时、认证失败或回读不一致时,保留 `backupPath` 并只读检查,不直接重复创建/删除。hash 能检测读取后的变化,平台没有原子比较写入能力。
|
|
68
|
+
|
|
69
|
+
验收按本轮授权范围,在详情页/数据管理页核对名称、位置、条件和动作配置。执行按钮可能改业务记录或触发外部操作,只有用户授权相应测试时才执行。保存回读不能证明移动端展示、关联填充或自动化执行成功。
|
|
70
|
+
|
|
71
|
+
平台边界见[官方说明](https://docs.aliwork.com/docs/yida_support/wtwabe/ckyvwt/ohyllrtgx9v5opox):每表最多 10 个;流程数据结束后才展示;编辑受字段权限约束;自动化按钮不另按字段权限控制触发,应结合页面访问与流程设计确定范围;未开启的流不会生效。版本/权限拒绝如实报告,不修改组织版本、授权或灰度开关。
|
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
"source": "openyida",
|
|
4
4
|
"entry": "openyida",
|
|
5
5
|
"skills": [
|
|
6
|
+
{
|
|
7
|
+
"name": "yida-custom-button",
|
|
8
|
+
"path": "skills/yida-custom-button/SKILL.md",
|
|
9
|
+
"display_name": "表单自定义按钮",
|
|
10
|
+
"description": "配置页面设置中的原生自定义按钮:修改当前记录、填写被关联表单、触发自动化、打开页面和设置显示位置。",
|
|
11
|
+
"category": "yida-skills/form",
|
|
12
|
+
"tags": [
|
|
13
|
+
"自定义按钮",
|
|
14
|
+
"页面设置",
|
|
15
|
+
"客户跟进",
|
|
16
|
+
"审批后发起订单"
|
|
17
|
+
],
|
|
18
|
+
"aliases": [
|
|
19
|
+
"表单按钮",
|
|
20
|
+
"详情页自定义操作"
|
|
21
|
+
],
|
|
22
|
+
"positive_signals": [
|
|
23
|
+
"配置表单自定义按钮",
|
|
24
|
+
"客户详情页添加跟进按钮",
|
|
25
|
+
"按钮触发手动自动化",
|
|
26
|
+
"审批结束后发起采购订单",
|
|
27
|
+
"表格视图中显示分配按钮"
|
|
28
|
+
],
|
|
29
|
+
"negative_signals": [
|
|
30
|
+
"自定义页面React按钮",
|
|
31
|
+
"仅修改按钮颜色",
|
|
32
|
+
"提交表单按钮文案"
|
|
33
|
+
],
|
|
34
|
+
"command_ids": [
|
|
35
|
+
"custom-button"
|
|
36
|
+
],
|
|
37
|
+
"requires_login": true
|
|
38
|
+
},
|
|
6
39
|
{
|
|
7
40
|
"name": "yida-cool-card",
|
|
8
41
|
"path": "skills/yida-cool-card/SKILL.md",
|
|
@@ -259,10 +292,24 @@
|
|
|
259
292
|
"display_name": "宜搭需求分析",
|
|
260
293
|
"description": "从应用需求推导核心功能候选,尽早确认范围和实际用法,再推导入口建议,整合 .cache/openyida/<项目名>/requirement-brief.json;平台导航或自定义导航交下游 AI 判断,不生成 PRD、视觉设计或页面源码。",
|
|
261
294
|
"category": "yida-skills/design",
|
|
262
|
-
"tags": [
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
295
|
+
"tags": [
|
|
296
|
+
"需求分析",
|
|
297
|
+
"整理需求",
|
|
298
|
+
"完整应用"
|
|
299
|
+
],
|
|
300
|
+
"aliases": [
|
|
301
|
+
"整理用户需求",
|
|
302
|
+
"需求分析"
|
|
303
|
+
],
|
|
304
|
+
"positive_signals": [
|
|
305
|
+
"整理用户需求",
|
|
306
|
+
"完整应用需求分析"
|
|
307
|
+
],
|
|
308
|
+
"negative_signals": [
|
|
309
|
+
"写 PRD",
|
|
310
|
+
"视觉设计",
|
|
311
|
+
"页面实现"
|
|
312
|
+
],
|
|
266
313
|
"command_ids": [],
|
|
267
314
|
"done_when": ".cache/openyida/<项目名>/requirement-brief.json 已写入、可解析,且没有会改变资源范围的未决问题。"
|
|
268
315
|
},
|
|
@@ -272,10 +319,27 @@
|
|
|
272
319
|
"display_name": "宜搭应用 PRD",
|
|
273
320
|
"description": "读取整理后的用户需求,输出 prd/<项目名>/prd.md;负责业务目标、资源蓝图、页面契约、三种顺序与验收,不生成 design.md 或页面源码。",
|
|
274
321
|
"category": "yida-skills/design",
|
|
275
|
-
"tags": [
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
322
|
+
"tags": [
|
|
323
|
+
"PRD",
|
|
324
|
+
"产品设计",
|
|
325
|
+
"资源蓝图",
|
|
326
|
+
"页面结构"
|
|
327
|
+
],
|
|
328
|
+
"aliases": [
|
|
329
|
+
"应用 PRD",
|
|
330
|
+
"产品需求文档"
|
|
331
|
+
],
|
|
332
|
+
"positive_signals": [
|
|
333
|
+
"写 PRD",
|
|
334
|
+
"资源蓝图",
|
|
335
|
+
"页面结构规划",
|
|
336
|
+
"导航顺序"
|
|
337
|
+
],
|
|
338
|
+
"negative_signals": [
|
|
339
|
+
"页面美化",
|
|
340
|
+
"视觉方向",
|
|
341
|
+
"页面源码"
|
|
342
|
+
],
|
|
279
343
|
"command_ids": [],
|
|
280
344
|
"done_when": "prd/<项目名>/prd.md 已写入,包含资源创建顺序、页面实现交付顺序、导航顺序、页面 handoff 和验收标准。"
|
|
281
345
|
},
|
|
@@ -328,7 +392,10 @@
|
|
|
328
392
|
"表单字段建模",
|
|
329
393
|
"数据记录操作"
|
|
330
394
|
],
|
|
331
|
-
"command_ids": [
|
|
395
|
+
"command_ids": [
|
|
396
|
+
"app-theme.snapshot",
|
|
397
|
+
"app-theme.restore"
|
|
398
|
+
],
|
|
332
399
|
"done_when": "新设计输出 prd/<项目名>/design.md,不写 prd.md。已有资源 UI 美化以用户验收保留或本轮修改已恢复并核验为完成;等待方案确认或效果验收时保持对应等待状态。"
|
|
333
400
|
},
|
|
334
401
|
{
|
|
@@ -337,11 +404,37 @@
|
|
|
337
404
|
"display_name": "宜搭图片素材准备",
|
|
338
405
|
"description": "按页面并发选图,允许外链的图片检查后直接使用,需托管时上传。",
|
|
339
406
|
"category": "yida-skills/design",
|
|
340
|
-
"tags": [
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
407
|
+
"tags": [
|
|
408
|
+
"图片素材",
|
|
409
|
+
"联网搜图",
|
|
410
|
+
"图片生成",
|
|
411
|
+
"背景图",
|
|
412
|
+
"商品图"
|
|
413
|
+
],
|
|
414
|
+
"aliases": [
|
|
415
|
+
"素材设计",
|
|
416
|
+
"图片搜索",
|
|
417
|
+
"生成图片",
|
|
418
|
+
"商品占位图"
|
|
419
|
+
],
|
|
420
|
+
"positive_signals": [
|
|
421
|
+
"Hero 图",
|
|
422
|
+
"背景图",
|
|
423
|
+
"商品图片",
|
|
424
|
+
"场景图片",
|
|
425
|
+
"联网搜索图片",
|
|
426
|
+
"生成 UI 素材"
|
|
427
|
+
],
|
|
428
|
+
"negative_signals": [
|
|
429
|
+
"纯表单",
|
|
430
|
+
"审批配置",
|
|
431
|
+
"权限设置",
|
|
432
|
+
"普通 CRUD 台账"
|
|
433
|
+
],
|
|
434
|
+
"command_ids": [
|
|
435
|
+
"agent-capabilities",
|
|
436
|
+
"asset"
|
|
437
|
+
],
|
|
345
438
|
"done_when": "生成 prd/<项目名>/asset-manifest.json,记录能力、来源、授权、尺寸和缺口;必需素材不完整时保持 draft。"
|
|
346
439
|
},
|
|
347
440
|
{
|
|
@@ -1106,7 +1199,12 @@
|
|
|
1106
1199
|
"name": "yida-form-permission",
|
|
1107
1200
|
"path": "skills/yida-form-permission/SKILL.md",
|
|
1108
1201
|
"display_name": "表单权限组管理",
|
|
1109
|
-
"command_ids": [
|
|
1202
|
+
"command_ids": [
|
|
1203
|
+
"get-permission",
|
|
1204
|
+
"save-permission",
|
|
1205
|
+
"permission-manage",
|
|
1206
|
+
"matrix"
|
|
1207
|
+
],
|
|
1110
1208
|
"description": "管理表单权限组的成员范围、数据范围、操作权限和字段权限;支持差异预览和已有矩阵引用。",
|
|
1111
1209
|
"category": "yida-skills/access",
|
|
1112
1210
|
"tags": [
|
|
@@ -1242,7 +1340,20 @@
|
|
|
1242
1340
|
"只创建 HTTP 连接器动作",
|
|
1243
1341
|
"页面 dataSourceMap 数据源"
|
|
1244
1342
|
],
|
|
1245
|
-
"command_ids": [
|
|
1343
|
+
"command_ids": [
|
|
1344
|
+
"integration.create",
|
|
1345
|
+
"integration.update",
|
|
1346
|
+
"integration.list",
|
|
1347
|
+
"integration.enable",
|
|
1348
|
+
"integration.disable",
|
|
1349
|
+
"integration.check",
|
|
1350
|
+
"integration.diagnose",
|
|
1351
|
+
"integration.log-detail",
|
|
1352
|
+
"integration.monitor",
|
|
1353
|
+
"integration.copy",
|
|
1354
|
+
"integration.rename",
|
|
1355
|
+
"integration.delete"
|
|
1356
|
+
],
|
|
1246
1357
|
"done_when": "集成自动化创建/启停/诊断完成,并返回 processCode 或检查结果。"
|
|
1247
1358
|
},
|
|
1248
1359
|
{
|
|
@@ -1385,15 +1496,43 @@
|
|
|
1385
1496
|
"display_name": "流程与自动化通用节点配置",
|
|
1386
1497
|
"description": "配置审批、办理、分支、并行、循环、数据和通知节点;人员、角色、条件选人、字段权限及变量各在一处说明。",
|
|
1387
1498
|
"category": "yida-skills/process",
|
|
1388
|
-
"tags": [
|
|
1389
|
-
|
|
1390
|
-
|
|
1499
|
+
"tags": [
|
|
1500
|
+
"通用节点",
|
|
1501
|
+
"指定角色",
|
|
1502
|
+
"字段权限",
|
|
1503
|
+
"操作按钮",
|
|
1504
|
+
"条件变量",
|
|
1505
|
+
"字段比较",
|
|
1506
|
+
"数据节点",
|
|
1507
|
+
"通知节点",
|
|
1508
|
+
"扩展执行"
|
|
1509
|
+
],
|
|
1510
|
+
"positive_signals": [
|
|
1511
|
+
"节点指定角色",
|
|
1512
|
+
"审批人条件模式",
|
|
1513
|
+
"流程自动化共用节点配置",
|
|
1514
|
+
"通知财务角色",
|
|
1515
|
+
"审批人允许编辑单价",
|
|
1516
|
+
"配置加签和转交",
|
|
1517
|
+
"数据节点字段赋值",
|
|
1518
|
+
"字段与字段比较",
|
|
1519
|
+
"当前库存低于库存下限"
|
|
1520
|
+
],
|
|
1521
|
+
"negative_signals": [
|
|
1522
|
+
"表单权限组数据范围",
|
|
1523
|
+
"维护角色成员",
|
|
1524
|
+
"修改版本权益"
|
|
1525
|
+
]
|
|
1391
1526
|
},
|
|
1392
1527
|
{
|
|
1393
1528
|
"name": "yida-process-rule",
|
|
1394
1529
|
"path": "skills/yida-process-rule/SKILL.md",
|
|
1395
1530
|
"display_name": "流程规则配置",
|
|
1396
|
-
"command_ids": [
|
|
1531
|
+
"command_ids": [
|
|
1532
|
+
"configure-process",
|
|
1533
|
+
"process.fields",
|
|
1534
|
+
"process.preview"
|
|
1535
|
+
],
|
|
1397
1536
|
"description": "配置已有流程的审批、办理、抄送、条件分支和字段权限,保存发布后核对配置。人员、按条件选人和节点参数按需读取通用节点说明。",
|
|
1398
1537
|
"category": "yida-skills/process",
|
|
1399
1538
|
"tags": [
|