openyida 2026.10.20 → 2026.10.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/yida.js +9 -0
- package/lib/app/agent-app-type.js +12 -0
- package/lib/app/cool-card.js +5 -0
- package/lib/app/upgrade-app-theme.js +2 -1
- package/lib/core/command-manifest.js +10 -5
- package/lib/core/locales/en.js +12 -1
- package/lib/core/locales/zh.js +12 -1
- package/lib/core/sample.js +1 -0
- package/lib/core/task-center.js +3 -1
- package/lib/core/update.js +1 -1
- package/lib/feedback/feedback.js +1 -0
- package/lib/feedback/github.js +130 -0
- package/lib/print-template/builder.js +232 -0
- package/lib/print-template/codes.js +46 -0
- package/lib/print-template/contract.js +31 -0
- package/lib/print-template/detail.js +36 -0
- package/lib/print-template/model.js +97 -0
- package/lib/print-template/print-template.js +302 -0
- package/lib/print-template/scenes.js +29 -0
- package/lib/samples/yida-print-template/approval.json +14 -0
- package/lib/samples/yida-print-template/codes.json +12 -0
- package/lib/samples/yida-print-template/delivery.json +14 -0
- package/lib/samples/yida-print-template/order.json +14 -0
- package/lib/samples/yida-print-template/quotation.json +13 -0
- package/lib/samples/yida-print-template/receipt.json +13 -0
- package/lib/task-center/lookups.js +11 -4
- package/package.json +1 -1
- package/yida-skills/SKILL.md +3 -2
- package/yida-skills/skills/yida-cool-card/SKILL.md +3 -1
- package/yida-skills/skills/yida-feedback/SKILL.md +1 -1
- package/yida-skills/skills/yida-feedback/references/github-contribution.md +19 -2
- package/yida-skills/skills/yida-print-template/SKILL.md +134 -0
- package/yida-skills/skills/yida-print-template/references/detail.md +40 -0
- package/yida-skills/skills/yida-print-template/references/guide.md +35 -0
- package/yida-skills/skills/yida-print-template/references/scenarios.md +72 -0
- package/yida-skills/skills/yida-task-center/references/task-filters.md +2 -0
- package/yida-skills/skills-index.json +14 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isDeepStrictEqual } = require('util');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
const { CliError } = require('../core/cli-error');
|
|
6
|
+
const { createYidaClient } = require('../core/yida-client');
|
|
7
|
+
const { buildYidaTitleI18n } = require('../core/yida-i18n');
|
|
8
|
+
const m = require('./model');
|
|
9
|
+
const builder = require('./builder');
|
|
10
|
+
const codes = require('./codes');
|
|
11
|
+
const scenes = require('./scenes');
|
|
12
|
+
const detail = require('./detail');
|
|
13
|
+
|
|
14
|
+
const ACTIONS = ['list', 'inspect', 'fields', 'build', 'create', 'copy', 'save', 'restore', 'settings', 'status', 'order', 'render', 'files'];
|
|
15
|
+
const TARGETED = ['inspect', 'copy', 'save', 'restore', 'settings', 'status', 'render'];
|
|
16
|
+
const HASHED = ['copy', 'save', 'restore', 'settings', 'status'];
|
|
17
|
+
const OPTIONS = {
|
|
18
|
+
list: ['--instance', '--detail-url'], inspect: ['--out'], fields: ['--out'], build: ['--config', '--scene', '--bindings', '--out'], create: ['--name', '--config', '--backup'],
|
|
19
|
+
copy: ['--name', '--expected-hash', '--backup'], save: ['--config', '--expected-hash', '--backup'],
|
|
20
|
+
restore: ['--config', '--expected-hash', '--backup'], settings: ['--config', '--expected-hash', '--backup', '--allow-permission-change'],
|
|
21
|
+
status: ['--status', '--expected-hash', '--backup'], order: ['--config', '--expected-hash', '--backup'],
|
|
22
|
+
render: ['--instance', '--detail-url', '--template-id'], files: ['--page', '--size'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Parse exact command contracts before auth or remote access. */
|
|
26
|
+
function parseArgs(args) {
|
|
27
|
+
const positional = [];
|
|
28
|
+
const flags = {};
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (Object.hasOwn(flags, arg)) { m.fail('USAGE', arg); }
|
|
32
|
+
if (['--json', '--help', '-h', '--allow-permission-change'].includes(arg)) { flags[arg] = true; continue; }
|
|
33
|
+
if (Object.values(OPTIONS).flat().includes(arg)) {
|
|
34
|
+
if (!args[i + 1] || args[i + 1].startsWith('-')) { m.fail('USAGE', arg); }
|
|
35
|
+
flags[arg] = args[++i]; continue;
|
|
36
|
+
}
|
|
37
|
+
if (arg.startsWith('-')) { m.fail('USAGE', arg); }
|
|
38
|
+
positional.push(arg);
|
|
39
|
+
}
|
|
40
|
+
if (flags['--help'] || flags['-h']) { return { help: true }; }
|
|
41
|
+
if (flags['--detail-url']) {
|
|
42
|
+
if (!['list', 'render'].includes(positional[0]) || positional.length !== 1 || flags['--instance']) { m.fail('USAGE', '--detail-url: list/render only, without positional target'); }
|
|
43
|
+
const resolved = detail.parse(flags['--detail-url']);
|
|
44
|
+
positional.push(resolved.appType, resolved.formUuid);
|
|
45
|
+
if (positional[0] === 'render') { positional.push(flags['--template-id']); }
|
|
46
|
+
flags['--instance'] = resolved.instance;
|
|
47
|
+
} else if (flags['--template-id']) { m.fail('USAGE', '--template-id requires --detail-url'); }
|
|
48
|
+
const [action, appType, formUuid, templateId] = positional;
|
|
49
|
+
if (!ACTIONS.includes(action) || positional.length !== (TARGETED.includes(action) ? 4 : 3)
|
|
50
|
+
|| !/^APP_[A-Za-z0-9_-]+$/.test(appType || '') || !/^[A-Za-z0-9_-]+$/.test(formUuid || '')
|
|
51
|
+
|| TARGETED.includes(action) && !/^(0|[1-9]\d*)$/.test(templateId || '')
|
|
52
|
+
|| Object.keys(flags).some(key => key !== '--json' && !OPTIONS[action].includes(key))) { m.fail('USAGE', t('print_template.usage')); }
|
|
53
|
+
if (['save', 'restore', 'settings', 'order'].includes(action) && !flags['--config']
|
|
54
|
+
|| action === 'build' && (Boolean(flags['--config']) === Boolean(flags['--scene']) || flags['--bindings'] && !flags['--scene']
|
|
55
|
+
|| flags['--scene'] && !scenes.names.includes(flags['--scene']))
|
|
56
|
+
|| ['create', 'copy'].includes(action) && !flags['--name']?.trim()
|
|
57
|
+
|| (HASHED.includes(action) || action === 'order') && !/^[a-f0-9]{64}$/.test(flags['--expected-hash'] || '')
|
|
58
|
+
|| action === 'status' && !['START', 'STOP'].includes(flags['--status'])
|
|
59
|
+
|| (action === 'render' || flags['--instance']) && !/^[A-Za-z0-9_-]+$/.test(flags['--instance'] || '')) { m.fail('USAGE', t('print_template.usage')); }
|
|
60
|
+
for (const [key, max] of [['--page', 100000], ['--size', 100]]) {
|
|
61
|
+
if (flags[key] !== undefined && (!/^[1-9]\d*$/.test(flags[key]) || Number(flags[key]) > max)) { m.fail('USAGE', key); }
|
|
62
|
+
}
|
|
63
|
+
if (['create', 'copy'].includes(action)) {
|
|
64
|
+
const name = flags['--name'].trim();
|
|
65
|
+
if ([...name].reduce((sum, ch) => sum + (ch.charCodeAt(0) > 127 ? 2 : 1), 0) > 40) { m.fail('INVALID_CONFIG', 'name: max 40 characters / 20 Chinese characters'); }
|
|
66
|
+
}
|
|
67
|
+
return { action, appType, formUuid, templateId, flags };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Validate a metadata-only patch, preserving translations and permission scope by default. */
|
|
71
|
+
function settings(base, raw, allowPermissions) {
|
|
72
|
+
const allowed = ['title', 'description', 'fileNameConfig', 'roleData'];
|
|
73
|
+
if (!m.object(raw) || !Object.keys(raw).length || Object.keys(raw).some(key => !allowed.includes(key))) { m.fail('INVALID_CONFIG', allowed.join(', ')); }
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const key of ['title', 'description']) {
|
|
76
|
+
if (!Object.hasOwn(raw, key)) { continue; }
|
|
77
|
+
const value = typeof raw[key] === 'string' ? { zh_CN: raw[key], en_US: raw[key] } : raw[key];
|
|
78
|
+
if (!m.object(value) || Object.values(value).some(item => item !== null && typeof item !== 'string')
|
|
79
|
+
|| key === 'title' && !value.zh_CN?.trim()) { m.fail('INVALID_CONFIG', key); }
|
|
80
|
+
result[key] = { ...(m.object(base[key]) ? base[key] : {}), ...value };
|
|
81
|
+
if (key === 'title' && [...result[key].zh_CN].reduce((sum, ch) => sum + (ch.charCodeAt(0) > 127 ? 2 : 1), 0) > 40) { m.fail('INVALID_CONFIG', 'title length'); }
|
|
82
|
+
}
|
|
83
|
+
if (Object.hasOwn(raw, 'description') && !Object.hasOwn(raw, 'title')) { result.title = base.title; }
|
|
84
|
+
// The native rename endpoint also replaces description; preserve its sibling.
|
|
85
|
+
if (Object.hasOwn(raw, 'title') && !Object.hasOwn(raw, 'description')) { result.description = base.description; }
|
|
86
|
+
if (Object.hasOwn(raw, 'fileNameConfig')) {
|
|
87
|
+
if (typeof raw.fileNameConfig !== 'string' || !raw.fileNameConfig.trim() || raw.fileNameConfig.length > 256
|
|
88
|
+
|| /#|\$[^!]|\$!(?!\{(?:originator|instanceTitle|createTime|serialNo)\})/.test(raw.fileNameConfig)) {
|
|
89
|
+
m.fail('INVALID_CONFIG', 'fileNameConfig: use $!{originator}, $!{instanceTitle}, $!{createTime}, $!{serialNo}');
|
|
90
|
+
}
|
|
91
|
+
result.fileNameConfig = raw.fileNameConfig;
|
|
92
|
+
}
|
|
93
|
+
if (Object.hasOwn(raw, 'roleData')) {
|
|
94
|
+
if (!allowPermissions) { m.fail('PERMISSION_FLAG_REQUIRED', '--allow-permission-change'); }
|
|
95
|
+
if (!Array.isArray(raw.roleData) || !raw.roleData.length || raw.roleData.some(role => !m.object(role) || typeof role.roleType !== 'string' || typeof role.roleValue !== 'string'
|
|
96
|
+
|| Object.keys(role).some(key => !['roleUuid', 'roleType', 'roleValue', 'isExclude'].includes(key)))) {
|
|
97
|
+
m.fail('INVALID_CONFIG', 'native roleData');
|
|
98
|
+
}
|
|
99
|
+
result.roleData = raw.roleData;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Execute native print operations with exclusive backups, stale-edit checks and no write replay. */
|
|
105
|
+
async function execute(parsed, client = createYidaClient(), preparedContent) {
|
|
106
|
+
const { action, appType, formUuid, templateId, flags } = parsed;
|
|
107
|
+
const detailTarget = flags['--detail-url'] ? detail.parse(flags['--detail-url']) : null;
|
|
108
|
+
if (detailTarget) { detail.verifyOrigin(detailTarget, client); }
|
|
109
|
+
const prefix = `/dingtalk/web/${appType}/query/printTemplate/`;
|
|
110
|
+
const target = { appType, formUuid, ...(templateId ? { templateId } : {}) };
|
|
111
|
+
const read = async (endpoint, params = {}) => m.unwrap(await client.get(prefix + endpoint + '.json', { formUuid, ...params }), endpoint);
|
|
112
|
+
const mutate = async (endpoint, params) => m.unwrap(await client.postFormOnce(prefix + endpoint + '.json', { formUuid, ...params }), endpoint);
|
|
113
|
+
const list = async (instance) => {
|
|
114
|
+
const raw = await read(instance ? 'listTemplateWithAuth' : 'listTemplate', instance ? { formInstId: instance } : {});
|
|
115
|
+
if (!Array.isArray(raw) || raw.some(item => !m.object(item) || !/^(0|[1-9]\d*)$/.test(String(item.templateId)) || item.formUuid !== formUuid)
|
|
116
|
+
|| new Set(raw.map(item => String(item.templateId))).size !== raw.length) { m.fail('INVALID_RESPONSE', 'template list'); }
|
|
117
|
+
return raw.map(item => ({ ...item, templateId: String(item.templateId), title: m.json(item.title, 'title') || {}, description: m.json(item.description, 'description') || {} }));
|
|
118
|
+
};
|
|
119
|
+
const snapshot = async id => {
|
|
120
|
+
const template = (await list()).find(item => item.templateId === id);
|
|
121
|
+
if (!template) { m.fail('NOT_FOUND', id); }
|
|
122
|
+
if (!['', 'DEFAULT', 'CUSTOM', null, undefined].includes(template.templateType)) { m.fail('UNSUPPORTED', `templateType: ${template.templateType}`); }
|
|
123
|
+
const body = id === '0' ? {} : await read('queryTemplateById', { templateId: id });
|
|
124
|
+
const result = { appType, formUuid, templateId: id, template, content: m.content(body) };
|
|
125
|
+
return { ...result, configHash: m.hash(result) };
|
|
126
|
+
};
|
|
127
|
+
const getFields = async () => {
|
|
128
|
+
const [dictionary, codeCapability] = await Promise.all([
|
|
129
|
+
read('listDictionarys', { ccMode: 'tile' }), codes.capabilities(client),
|
|
130
|
+
]);
|
|
131
|
+
return builder.fields(dictionary, codeCapability);
|
|
132
|
+
};
|
|
133
|
+
const output = { action, ...target };
|
|
134
|
+
if (action === 'list') {
|
|
135
|
+
const templates = await list(flags['--instance']);
|
|
136
|
+
return { ...output, templates, ...(flags['--instance'] ? { instance: flags['--instance'], authorizationScoped: true }
|
|
137
|
+
: { orderHash: m.hash({ template: templates, content: null }) }) };
|
|
138
|
+
}
|
|
139
|
+
if (action === 'fields') {
|
|
140
|
+
const result = { ...output, fields: await getFields() };
|
|
141
|
+
if (flags['--out']) { result.outputPath = m.write(result, flags['--out'], 'fields'); }
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
if (action === 'build') {
|
|
145
|
+
const source = flags['--scene'] ? scenes.document(flags['--scene'], flags['--bindings'] ? m.read(flags['--bindings']) : {}) : m.read(flags['--config']);
|
|
146
|
+
const config = builder.compile(source, await getFields());
|
|
147
|
+
const result = { ...output, config, saved: false, ...(flags['--scene'] ? { scene: flags['--scene'], document: source.document } : {}) };
|
|
148
|
+
if (flags['--out']) { result.outputPath = m.write(config, flags['--out'], 'built'); }
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
if (action === 'files') {
|
|
152
|
+
const result = m.unwrap(await client.get(`/dingtalk/web/${appType}/query/attach/list.json`, {
|
|
153
|
+
formUuid, currentPage: flags['--page'] || '1', pageSize: flags['--size'] || '50', source: 'PRINT',
|
|
154
|
+
}), 'attach/list');
|
|
155
|
+
if (!m.object(result) || !Array.isArray(result.data) || !Number.isFinite(result.totalCount)) { m.fail('INVALID_RESPONSE', 'print file list'); }
|
|
156
|
+
return { ...output, files: result };
|
|
157
|
+
}
|
|
158
|
+
// Validate content before creating anything. A later save failure must retain the created ID.
|
|
159
|
+
const initialContent = action === 'create' && flags['--config'] ? builder.compile(m.read(flags['--config']), await getFields()) : null;
|
|
160
|
+
let before;
|
|
161
|
+
if (action === 'render') {
|
|
162
|
+
const available = await list(flags['--instance']);
|
|
163
|
+
const template = available.find(item => item.templateId === templateId && item.status !== 'STOP');
|
|
164
|
+
if (!template) { m.fail('NOT_FOUND', 'template unavailable for this record'); }
|
|
165
|
+
if (!['', 'DEFAULT', 'CUSTOM', null, undefined].includes(template.templateType)) { m.fail('UNSUPPORTED', `templateType: ${template.templateType}`); }
|
|
166
|
+
before = { ...target, instance: flags['--instance'], template, authorizationScoped: true };
|
|
167
|
+
} else {
|
|
168
|
+
before = action === 'create' || action === 'order' ? { appType, formUuid, templates: await list() } : await snapshot(templateId);
|
|
169
|
+
}
|
|
170
|
+
if (action === 'inspect') {
|
|
171
|
+
if (flags['--out']) { before = { ...before, outputPath: m.write(before, flags['--out'], 'export') }; }
|
|
172
|
+
return { ...output, ...before };
|
|
173
|
+
}
|
|
174
|
+
const expectedHash = action === 'order' ? m.hash({ template: before.templates, content: null }) : before.configHash;
|
|
175
|
+
if ((HASHED.includes(action) || action === 'order') && flags['--expected-hash'] !== expectedHash) { m.fail('STALE_CONFIG', 'inspect again'); }
|
|
176
|
+
if (['save', 'restore'].includes(action) && (templateId === '0' || before.template.templateType === 'DEFAULT')) { m.fail('UNSUPPORTED', 'built-in default body'); }
|
|
177
|
+
let endpoint;
|
|
178
|
+
let params;
|
|
179
|
+
let expected;
|
|
180
|
+
if (action === 'save' || action === 'restore') {
|
|
181
|
+
let input = preparedContent || m.read(flags['--config']);
|
|
182
|
+
if (action === 'restore') {
|
|
183
|
+
if (input.appType !== appType || input.formUuid !== formUuid || String(input.templateId) !== templateId || !m.object(input.content)) { m.fail('TARGET_MISMATCH', 'backup target'); }
|
|
184
|
+
input = Object.fromEntries(['termContents', 'termJsonAttr', 'dictionaryList'].map(key => [key, input.content[key]]));
|
|
185
|
+
}
|
|
186
|
+
expected = builder.compile(input, await getFields());
|
|
187
|
+
endpoint = 'saveTemplate';
|
|
188
|
+
params = { templateId, termContents: expected.termContents, termJsonAttr: JSON.stringify(expected.termJsonAttr), dictionaryList: JSON.stringify(expected.dictionaryList) };
|
|
189
|
+
const modified = before.content.gmtModified;
|
|
190
|
+
if (modified !== null && modified !== undefined) {
|
|
191
|
+
const stamp = typeof modified === 'number' || /^\d+$/.test(String(modified)) ? Number(modified) : Date.parse(modified);
|
|
192
|
+
if (!Number.isFinite(stamp)) { m.fail('INVALID_RESPONSE', 'gmtModified'); }
|
|
193
|
+
params.gmtModified = String(stamp);
|
|
194
|
+
}
|
|
195
|
+
} else if (action === 'settings') {
|
|
196
|
+
expected = settings(before.template, m.read(flags['--config']), flags['--allow-permission-change']);
|
|
197
|
+
if (templateId === '0' && Object.keys(expected).some(key => key !== 'roleData')) { m.fail('UNSUPPORTED', 'default template settings: roleData only'); }
|
|
198
|
+
endpoint = 'updatePrintBaseInfo';
|
|
199
|
+
params = { templateId, ...Object.fromEntries(Object.entries(expected).map(([key, value]) => [key, typeof value === 'string' ? value : JSON.stringify(value)])) };
|
|
200
|
+
} else if (action === 'status') {
|
|
201
|
+
expected = flags['--status']; endpoint = 'savePrintTemplateStatus'; params = { templateId, status: expected };
|
|
202
|
+
} else if (action === 'order') {
|
|
203
|
+
expected = m.read(flags['--config']);
|
|
204
|
+
if (!Array.isArray(expected)) { m.fail('INVALID_CONFIG', 'template ID array'); }
|
|
205
|
+
expected = expected.map(String);
|
|
206
|
+
if (expected.length !== before.templates.length || new Set(expected).size !== expected.length || expected.some(id => !before.templates.some(item => item.templateId === id))) { m.fail('INVALID_CONFIG', 'include every template exactly once'); }
|
|
207
|
+
endpoint = 'savePrintTemplateOrder'; params = { templateIdList: JSON.stringify(expected.map(Number)) };
|
|
208
|
+
} else if (action === 'create' || action === 'copy') {
|
|
209
|
+
endpoint = action === 'create' ? 'createTemplate' : 'copyTemplateIndependently';
|
|
210
|
+
expected = flags['--name'].trim();
|
|
211
|
+
params = { title: JSON.stringify(buildYidaTitleI18n(expected)), description: JSON.stringify(buildYidaTitleI18n('')),
|
|
212
|
+
...(action === 'copy' ? { originTemplateId: templateId } : {}) };
|
|
213
|
+
} else if (action === 'render') {
|
|
214
|
+
endpoint = 'print'; params = { ...(templateId === '0' ? {} : { templateId }), formInstId: flags['--instance'],
|
|
215
|
+
templateType: templateId === '0' ? 'DEFAULT' : 'CUSTOM', ...(detailTarget?.activityId ? { activityId: detailTarget.activityId } : {}) };
|
|
216
|
+
}
|
|
217
|
+
const backupPath = m.write({ ...before, action }, flags['--backup']);
|
|
218
|
+
const details = { target, backupPath, retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true };
|
|
219
|
+
let accepted;
|
|
220
|
+
try { accepted = await mutate(endpoint, params); }
|
|
221
|
+
catch (error) { throw new CliError(error.message, { code: error.code || 'PRINT_TEMPLATE_WRITE_FAILED', details: { ...error.details, ...details } }); }
|
|
222
|
+
try {
|
|
223
|
+
if (action === 'render') {
|
|
224
|
+
if (!/^[1-9]\d*$/.test(String(accepted))) { m.fail('INVALID_RESPONSE', 'print file ID'); }
|
|
225
|
+
return { ...output, instance: flags['--instance'], authorizationScoped: true, backupPath, fileId: String(accepted), generationRequested: true, fileReadyVerified: false };
|
|
226
|
+
}
|
|
227
|
+
if (action === 'order') {
|
|
228
|
+
const after = await list();
|
|
229
|
+
if (!isDeepStrictEqual(after.map(item => item.templateId), expected)) { m.fail('READBACK_MISMATCH', action); }
|
|
230
|
+
return { ...output, backupPath, templates: after, readbackVerified: true };
|
|
231
|
+
}
|
|
232
|
+
let id = templateId;
|
|
233
|
+
if (action === 'create' || action === 'copy') {
|
|
234
|
+
id = String(accepted);
|
|
235
|
+
if (!/^[1-9]\d*$/.test(id)) { m.fail('INVALID_RESPONSE', 'new template ID'); }
|
|
236
|
+
details.createdTemplateId = id;
|
|
237
|
+
}
|
|
238
|
+
let after = await snapshot(id);
|
|
239
|
+
if (action === 'copy' && before.content.dictionaryList.length && !after.content.dictionaryList.length
|
|
240
|
+
&& before.content.termContents === after.content.termContents
|
|
241
|
+
&& isDeepStrictEqual(m.stable(before.content.termJsonAttr), m.stable(after.content.termJsonAttr))) {
|
|
242
|
+
// Native copy can omit the dictionary. Complete this specific known partial
|
|
243
|
+
// result once, preserving the new target's concurrency token and backup.
|
|
244
|
+
const copyBackupPath = m.write(after);
|
|
245
|
+
details.copyBackupPath = copyBackupPath;
|
|
246
|
+
details.createdTemplateId = id;
|
|
247
|
+
const native = builder.validateNative(before.content, await getFields());
|
|
248
|
+
const stamp = after.content.gmtModified;
|
|
249
|
+
await mutate('saveTemplate', { templateId: id, termContents: native.termContents,
|
|
250
|
+
termJsonAttr: JSON.stringify(native.termJsonAttr), dictionaryList: JSON.stringify(native.dictionaryList),
|
|
251
|
+
...(stamp !== null && stamp !== undefined ? { gmtModified: String(stamp) } : {}) });
|
|
252
|
+
after = await snapshot(id);
|
|
253
|
+
}
|
|
254
|
+
if (['save', 'restore'].includes(action) && !m.matches(expected, after.content)) { m.fail('READBACK_MISMATCH', action); }
|
|
255
|
+
if (action === 'status' && after.template.status !== expected) { m.fail('READBACK_MISMATCH', action); }
|
|
256
|
+
if (['create', 'copy'].includes(action) && m.label(after.template.title) !== expected) { m.fail('READBACK_MISMATCH', 'title'); }
|
|
257
|
+
if (action === 'create' && initialContent) {
|
|
258
|
+
try {
|
|
259
|
+
const saved = await execute({ action: 'save', appType, formUuid, templateId: id,
|
|
260
|
+
flags: { '--config': flags['--config'], '--expected-hash': after.configHash } }, client, initialContent);
|
|
261
|
+
return { ...saved, action, created: true, contentSaved: true, createBackupPath: backupPath };
|
|
262
|
+
} catch (error) {
|
|
263
|
+
details.contentSaveDetails = error.details;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (action === 'copy' && !isDeepStrictEqual(m.stable(Object.fromEntries(['termContents', 'termJsonAttr', 'dictionaryList'].map(key => [key, before.content[key]]))),
|
|
268
|
+
m.stable(Object.fromEntries(['termContents', 'termJsonAttr', 'dictionaryList'].map(key => [key, after.content[key]]))))) { m.fail('READBACK_MISMATCH', 'copied content'); }
|
|
269
|
+
if (action === 'settings') {
|
|
270
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
271
|
+
const saved = after.template[key];
|
|
272
|
+
if (key === 'roleData') {
|
|
273
|
+
const members = after.template.roleMembers;
|
|
274
|
+
if (!Array.isArray(members) || value.some(role => {
|
|
275
|
+
const member = members.find(item => item.roleType === role.roleType);
|
|
276
|
+
return !member || member.roleValue.map(item => item.key).sort().join(',') !== role.roleValue.split(',').sort().join(',')
|
|
277
|
+
|| role.isExclude !== undefined && String(role.isExclude) !== String(member.isExclude ?? '');
|
|
278
|
+
}) || members.some(member => !value.some(role => role.roleType === member.roleType) && member.roleValue?.some(item => item.key && item.key !== 'n'))) { m.fail('READBACK_MISMATCH', key); }
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (['title', 'description'].includes(key)) {
|
|
282
|
+
if (Object.keys(value).some(locale => locale !== 'pureEn_US' && String(value[locale] ?? '') !== String(saved?.[locale] ?? ''))) { m.fail('READBACK_MISMATCH', key); }
|
|
283
|
+
} else if (!isDeepStrictEqual(m.stable(value), m.stable(saved))) { m.fail('READBACK_MISMATCH', key); }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { ...output, ...after, backupPath, ...(details.copyBackupPath ? { copyBackupPath: details.copyBackupPath } : {}), readbackVerified: true };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new CliError(error.message, { code: error.code || 'PRINT_TEMPLATE_READBACK_FAILED',
|
|
289
|
+
details: { ...error.details, ...details, mutationAccepted: true, acceptedResult: accepted, sideEffectState: 'accepted_unverified' } });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** CLI entry: JSON receipts share the same execution path as programmatic calls. */
|
|
294
|
+
async function run(args, options = {}) {
|
|
295
|
+
const parsed = parseArgs(args);
|
|
296
|
+
if (parsed.help) { process.stdout.write(t('print_template.usage') + '\n'); return; }
|
|
297
|
+
const result = await execute(parsed, options.client);
|
|
298
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = { run, execute, parseArgs, settings };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { object, fail, safe } = require('./model');
|
|
4
|
+
const names = ['order', 'quotation', 'delivery', 'receipt', 'approval', 'codes'];
|
|
5
|
+
|
|
6
|
+
/** Map scene roles to explicit live field references; compilation verifies IDs and table ownership. */
|
|
7
|
+
function document(scene, bindings = {}) {
|
|
8
|
+
if (!names.includes(scene)) { fail('INVALID_CONFIG', `scene: ${names.join('|')}`); }
|
|
9
|
+
safe(bindings);
|
|
10
|
+
if (!object(bindings) || Object.values(bindings).some(value => typeof value !== 'string' || !value.trim())) {
|
|
11
|
+
fail('INVALID_CONFIG', 'bindings: field role -> actual field name/key');
|
|
12
|
+
}
|
|
13
|
+
const result = JSON.parse(JSON.stringify(require(`../samples/yida-print-template/${scene}.json`)));
|
|
14
|
+
const used = new Set();
|
|
15
|
+
const ref = name => { used.add(name); return bindings[name] ?? name; };
|
|
16
|
+
for (const block of result.document.blocks) {
|
|
17
|
+
if (block.field) { block.field = ref(block.field); }
|
|
18
|
+
if (block.fields) { block.fields = block.fields.map(field => typeof field === 'string' ? ref(field) : { ...field, field: ref(field.field) }); }
|
|
19
|
+
if (block.columns && Array.isArray(block.columns)) {
|
|
20
|
+
block.columns = block.columns.map(field => typeof field === 'string' ? ref(field) : { ...field, field: ref(field.field) });
|
|
21
|
+
}
|
|
22
|
+
if (block.html) { block.html = block.html.replace(/\{\{([^{}]+)\}\}/g, (_, field) => `{{${ref(field.trim())}}}`); }
|
|
23
|
+
}
|
|
24
|
+
const unused = Object.keys(bindings).filter(key => !used.has(key));
|
|
25
|
+
if (unused.length) { fail('INVALID_CONFIG', `unknown scene roles: ${unused.join(', ')}`); }
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { names, document };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"title": "申请审批单",
|
|
4
|
+
"style": "formal",
|
|
5
|
+
"blocks": [
|
|
6
|
+
{ "type": "fields", "columns": 2, "fields": ["申请编号", "申请人", "申请日期", "申请金额"] },
|
|
7
|
+
{ "type": "heading", "text": "申请事由" },
|
|
8
|
+
{ "type": "field", "field": "申请说明" },
|
|
9
|
+
{ "type": "heading", "text": "审批记录" },
|
|
10
|
+
{ "type": "table", "field": "审批历史" },
|
|
11
|
+
{ "type": "text", "text": "归档经办人:________________" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"title": "订单扫码核验单",
|
|
4
|
+
"blocks": [
|
|
5
|
+
{ "type": "fields", "fields": ["订单编号"] },
|
|
6
|
+
{ "type": "heading", "text": "订单二维码" },
|
|
7
|
+
{ "type": "qrcode", "field": "订单编号", "width": 128, "height": 128 },
|
|
8
|
+
{ "type": "heading", "text": "订单条形码" },
|
|
9
|
+
{ "type": "barcode", "field": "订单编号", "width": 320, "height": 100 }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"title": "发货交接单",
|
|
4
|
+
"style": "document",
|
|
5
|
+
"blocks": [
|
|
6
|
+
{ "type": "fields", "columns": 2, "fields": ["发货编号", "收货单位", "发货日期", "经办人"] },
|
|
7
|
+
{ "type": "heading", "text": "交接明细" },
|
|
8
|
+
{ "type": "table", "field": "发货明细", "columns": ["商品名称", "发货数量"] },
|
|
9
|
+
{ "type": "fields", "fields": ["交接备注"] },
|
|
10
|
+
{ "type": "text", "text": "发货人:________________ 收货人:________________" },
|
|
11
|
+
{ "type": "text", "text": "验收结果:________________ 签收日期:________________" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"title": "销售订单",
|
|
4
|
+
"style": "document",
|
|
5
|
+
"font": "宋体",
|
|
6
|
+
"blocks": [
|
|
7
|
+
{ "type": "fields", "columns": 2, "fields": ["订单编号", "客户名称", "订单日期", "经办人"] },
|
|
8
|
+
{ "type": "heading", "text": "商品明细" },
|
|
9
|
+
{ "type": "table", "field": "订单明细", "columns": ["商品名称", "数量", "单价", "金额"] },
|
|
10
|
+
{ "type": "fields", "fields": ["合计金额", "备注"] },
|
|
11
|
+
{ "type": "text", "text": "客户签字:________________ 经办人签字:________________" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"title": "销售报价单",
|
|
4
|
+
"style": "formal",
|
|
5
|
+
"blocks": [
|
|
6
|
+
{ "type": "fields", "columns": 2, "fields": ["报价编号", "客户名称", "报价日期", "经办人"] },
|
|
7
|
+
{ "type": "heading", "text": "报价明细" },
|
|
8
|
+
{ "type": "table", "field": "报价明细", "columns": ["商品名称", "数量", "单价", "金额"] },
|
|
9
|
+
{ "type": "fields", "fields": ["报价总额", "报价说明"] },
|
|
10
|
+
{ "type": "text", "text": "报价确认签字:________________ 日期:________________" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"title": "收款凭证",
|
|
4
|
+
"style": "compact",
|
|
5
|
+
"width": 320,
|
|
6
|
+
"fontSize": 12,
|
|
7
|
+
"blocks": [
|
|
8
|
+
{ "type": "fields", "fields": ["收据编号", "付款单位", "收款金额", "收款日期"] },
|
|
9
|
+
{ "type": "divider" },
|
|
10
|
+
{ "type": "html", "html": "<p>收款人:{{发起人}}</p><p>签字:________________</p>" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -22,9 +22,11 @@ function createTaskCenterLookups() {
|
|
|
22
22
|
if (type === 'originators') {
|
|
23
23
|
return { method: 'GET', path: '/query/userservice/searchUsers.json', params: { key, size } };
|
|
24
24
|
}
|
|
25
|
+
// Match taskCenter.html advanced search. `formTypes=process` hydrates processCode for process rows;
|
|
26
|
+
// the endpoint still returns receipt rows for the "created by me" form selector.
|
|
25
27
|
return {
|
|
26
28
|
method: 'POST', path: '/dingtalk/web/' + options.appType + '/query/formdesign/listFormDesignsByFormType.json',
|
|
27
|
-
params: { formTypes:
|
|
29
|
+
params: { formTypes: 'process', key, pageIndex: 1, pageSize: size },
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
function text(value) {
|
|
@@ -39,20 +41,25 @@ function createTaskCenterLookups() {
|
|
|
39
41
|
if (raw !== null && !Array.isArray(raw)) { fail('response'); }
|
|
40
42
|
const seen = new Set();
|
|
41
43
|
const items = [];
|
|
44
|
+
const keyword = type === 'originators' ? '' : String(context.keyword || '').trim().toLocaleLowerCase();
|
|
42
45
|
for (const row of raw || []) {
|
|
43
46
|
if (!row || typeof row !== 'object' || Array.isArray(row)) { fail('item'); }
|
|
44
47
|
if (context.appType && row.appType && row.appType !== context.appType) { fail('scope'); }
|
|
45
48
|
const process = typeof row.processCode === 'string' && row.processCode.trim();
|
|
46
49
|
const form = typeof row.formUuid === 'string' && row.formUuid.trim();
|
|
47
|
-
if (type === 'processes' && !process) { continue; }
|
|
50
|
+
if (type === 'processes' && (row.formType !== 'process' || !process)) { continue; }
|
|
48
51
|
if (type === 'forms' && (!form || !['receipt', 'process'].includes(row.formType))) { continue; }
|
|
49
52
|
const value = type === 'originators' ? row.staffId || row.userId || row.emplId : row.formType === 'receipt' ? form : process || form;
|
|
50
53
|
const label = type === 'originators' ? text(row.displayName || row.name) : text(row.title || row.i18nTitle);
|
|
51
54
|
if (typeof value !== 'string' || !value.trim() || !label || seen.has(value)) { continue; }
|
|
55
|
+
if (keyword && !label.toLocaleLowerCase().includes(keyword)) { continue; }
|
|
52
56
|
seen.add(value);
|
|
53
57
|
items.push({ value, label, ...(type !== 'originators' ? { formUuid: form, formType: row.formType || '' } : {}) });
|
|
54
58
|
}
|
|
55
|
-
return { items, total: null };
|
|
59
|
+
if (type === 'originators') { return { items, total: null }; }
|
|
60
|
+
// The native task center sends key/pageSize too, but the endpoint can return every design. Keep the lookup contract deterministic.
|
|
61
|
+
const pageSize = context.pageSize === undefined ? 50 : Number(context.pageSize);
|
|
62
|
+
return { items: items.slice(0, pageSize), total: items.length };
|
|
56
63
|
}
|
|
57
64
|
async function load(type, options, fetcher) {
|
|
58
65
|
const operation = request(type, options);
|
|
@@ -67,7 +74,7 @@ function createTaskCenterLookups() {
|
|
|
67
74
|
if (!response || response.ok !== true || typeof response.json !== 'function') { fail('request'); }
|
|
68
75
|
let payload;
|
|
69
76
|
try { payload = await response.json(); } catch { fail('response'); }
|
|
70
|
-
return normalize(type, payload, options);
|
|
77
|
+
return normalize(type, payload, { ...options, keyword: operation.params.key, pageSize: operation.params.pageSize });
|
|
71
78
|
}
|
|
72
79
|
return { request, normalize, load };
|
|
73
80
|
}
|
package/package.json
CHANGED
package/yida-skills/SKILL.md
CHANGED
|
@@ -135,7 +135,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
|
|
|
135
135
|
|
|
136
136
|
`yida-data-card` 只在用户显式指定宜搭原生数据卡片,或继续已明确的原生卡片上下文时触发。泛称数据卡片、指标卡、看板、投放、数据播报或发送到群不满足条件;不能在完整应用搭建中自行添加原生数据卡片。用户主动提供原生卡片链接并要求操作该资源属于明确指定;否定原生时按实际页面、图表或自动化意图路由。按本次实际修改对象判断,提及或引用原生卡片不等于请求修改该资源。
|
|
137
137
|
|
|
138
|
-
`yida-cool-card` 只在用户明确指定宜搭原生酷卡片(交互卡片、置顶卡片),或继续已明确的酷卡片任务时使用。它使用 `cardPageCode`/`cardTemplateId`,独立于 `yida-data-card` 的 `CARD-xxx` 和页面中的展示卡片。`dataCardDesigner.html?cardCode=CARD-...` 属于原生数据卡片;酷卡片管理入口为 `coolCardManage
|
|
138
|
+
`yida-cool-card` 只在用户明确指定宜搭原生酷卡片(交互卡片、置顶卡片),或继续已明确的酷卡片任务时使用。它使用 `cardPageCode`/`cardTemplateId`,独立于 `yida-data-card` 的 `CARD-xxx` 和页面中的展示卡片。`dataCardDesigner.html?cardCode=CARD-...` 属于原生数据卡片;酷卡片管理入口为 `coolCardManage`。创建前核对认证环境;集团版(`alibaba`)直接告知“集团版不支持酷卡片搭建”并停止搭建流程。名称与链接冲突时说明实际资源并核对目标,不能互换接口。泛称卡片、图表或群通知不自动选择这两类原生资源。
|
|
139
139
|
|
|
140
140
|
### 大类目录
|
|
141
141
|
|
|
@@ -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-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` |
|
|
@@ -199,6 +199,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
|
|
|
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
201
|
| 页面设置中的自定义按钮、客户跟进/分配、审批后发起订单、按钮触发自动化或打开页面 | `yida-custom-button`;配置原生按钮定义和显示位置,自定义页面内 React 按钮不走此入口 |
|
|
202
|
+
| 打印这条数据/数据详情链接、原生打印模板、打印设置、订单/报价/出入库/审批单、单据版式与字段明细、字段二维码/条形码、备份恢复和PDF生成 | `yida-print-template`;复用已有表单和模板,以真实打印字典生成动态内容;Word组件、叮当打印和浏览器页面打印不走此入口 |
|
|
202
203
|
| 原生数据管理页视图列表、显示列配置、字段显隐与顺序、列宽、冻结列、默认筛选、多视图 | `yida-form-view`;使用原生配置读写,保存前备份并回读,不通过 CSS 隐藏字段或重建自定义列表 |
|
|
203
204
|
| 当前数据管理页换风格、只留横线、突出新增/筛选/行操作、列级强调、状态列标签、数据管理 UI 异常、自定义简单数据管理 | `yida-data-manage-ui`(优先于通用美化);保留承载类型和业务,从目标推导维度、定位 token/CSS 并按页改写;备份发布并实测,不要求选择配方 |
|
|
204
205
|
| 页面视觉方向、页面美化、去 AI 味、优化应用 UI、不好看、太乱、美化后不满意或恢复 | `yida-design`;已有资源优先走 `sub_skill/ui-improve/SKILL.md`,先展示视觉方案,经 `ask_human` 确认并备份后才交接实施,只改 UI;不满意恢复本轮前状态。新页面设计沿用原设计流程 |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yida-cool-card
|
|
3
|
-
description:
|
|
3
|
+
description: 仅在用户明确要求宜搭原生酷卡片(交互卡片、置顶卡片),或继续该上下文时使用。集团版不支持酷卡片搭建,先核对认证环境。提供创建、读取、查询模板、导出配置和样式设计。样式限于钉钉原生组件,不能运行任意 HTML/CSS/React;超出能力时说明限制并提供替代方案。返回官方编辑器链接,由用户配置保存;CLI 不保存、发布或发送。原生数据卡片、Canvas 展示卡片及泛称卡片不触发。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 宜搭原生酷卡片
|
|
@@ -21,6 +21,8 @@ description: 仅在用户明确要求宜搭原生酷卡片(交互卡片、置
|
|
|
21
21
|
|
|
22
22
|
先用 `openyida login --check-only --json` 核对目标组织、用户和环境,再复用用户指定的应用。用户要求读取已有卡片时不新建;已有标识不明时先列出核对。
|
|
23
23
|
|
|
24
|
+
**集团版不支持酷卡片搭建。** 用户要求创建或搭建酷卡片时,先依据认证连接的实际 `baseUrl` 判断环境;复用 CLI 的环境识别语义,`alibaba` 为集团版(包括 `yida-group.alibaba-inc.com` 及已识别的内网宜搭环境)。已确认集团版时,直接告知“集团版不支持酷卡片搭建”,结束搭建流程,不创建应用或卡片、不查询模板或提供编辑器链接,也不引导安装 dws 绕过限制。环境不明时先查登录状态,不凭应用名称、组织名称或公有云付费版本猜测。CLI 的 `COOL_CARD_UNSUPPORTED_ENVIRONMENT` 同样按此处理。
|
|
25
|
+
|
|
24
26
|
```bash
|
|
25
27
|
openyida app-settings APP_XXX --section cool-card --json
|
|
26
28
|
openyida cool-card list APP_XXX --type im --json
|
|
@@ -37,7 +37,7 @@ openyida feedback report --input summary.json --output-dir feedback-draft-001
|
|
|
37
37
|
## 按用户条件交付
|
|
38
38
|
|
|
39
39
|
- **无 GitHub 账号**:先引导用户创建 GitHub 账号,由用户自行完成注册和登录。实在不行,只提示“反馈到AI开发者群”,附上已整理的脱敏报告。不索取群链接或群 ID,不编造入群入口,不替用户发送群消息。离线或暂不愿注册时同样交付报告,明确“材料已生成,尚未提交”。
|
|
40
|
-
- **有 GitHub 账号,提交 Issue**:不需 clone/fork
|
|
40
|
+
- **有 GitHub 账号,提交 Issue**:不需 clone/fork。用户明确选择提交 Issue 后,用 `openyida feedback github status --json` 检查当前 GitHub 账号的 Star 与个人 Fork 状态;仅对明确未完成的操作提供一次可选邀请,已有的不重复提示,未知状态不说成未完成。按 [GitHub 提交](references/github-contribution.md) 处理用户选择、正文公开确认与查重。拒绝 Star/Fork 不影响提交;登录由用户在可信本机流程完成,不索取 Token。
|
|
41
41
|
- **有账号,希望提交修复 PR**:仅适用于公开 OpenYida CLI/skill/docs 的可验证问题。先准备最小修复和验证,再通过用户自己的 Fork 提交到公开上游,详见同一参考。没有源码时由 Agent 在独立目录准备公开源码,不修改 npm 全局安装或用户业务目录。无法定位/验证时如实写入测试对比,优先交付 Issue 草稿;不要承诺未经验证的修复。
|
|
42
42
|
|
|
43
43
|
创建 Issue、评论、Fork、push 或 PR 前必须有用户对相应外部动作及公开内容的授权;已有对同一最终稿和截图的隐私确认不重复问;单纯授权提 Issue/PR 不能替代公开内容审阅。只要求“排查”“整理反馈”不代表允许公开提交。提交超时先查是否成功,不自动重复创建。
|
|
@@ -6,6 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
## Issue(无需源码)
|
|
8
8
|
|
|
9
|
+
用户明确要向 OpenYida 提交 Issue,且选择 GitHub 路径后,再执行以下只读检查;普通排障、离线报告、安装及自动更新不触发:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
openyida feedback github status --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
命令复用本机 `gh` 的 github.com 当前账号,不读取 Token,不自动安装或登录。`starred` 为 true/false/null,个人 `fork.state` 为 ready/absent/conflict/unknown;同时检查改名的个人 Fork。只有 `suggestedActions` 中的缺失项可提供一次可选邀请:例如“是否愿意 Star 支持项目,或 Fork 留作后续贡献?也可以直接提交 Issue”。已有 Star/Fork 不重复提示;接口失败、权限不足或状态未知时不推断为未完成,不阻塞 Issue。没有可用 gh 时沿用连接器或浏览器;未登录时由用户自行登录,不索取凭据。
|
|
16
|
+
|
|
17
|
+
用户分别明确选择后,Agent 才执行相应命令(`--confirm` 记录其真实授权,不能自行代选):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
openyida feedback github star --confirm --json
|
|
21
|
+
openyida feedback github fork --confirm --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
命令固定目标为 `openyida/openyida`,已有操作直接复用;Fork 只创建到当前用户个人账号,不 clone、不修改 remote。Fork 异步创建可能返回 `pending-verification`;先只读查状态,不重复创建。无论拒绝、失败或未登录,都可继续整理并提交 Issue;Star/Fork 是可选支持,不是提 Issue 的条件,也不代替正文与附件的公开确认。同一反馈流程拒绝过的选项不再次询问。现有 PR 流程的 Fork 授权仅用于贡献代码,不隐含 Star 授权。
|
|
25
|
+
|
|
9
26
|
1. 先完成 [交互与公开确认](user-interaction.md):展示实际问题、复现步骤和全部截图,取得用户对最终公开内容的确认。用户未登录时引导注册/登录;实在不行提示“反馈到AI开发者群”。只有用户选择 GitHub 路径才检查 `gh auth status --hostname github.com`;不要读/打印 Token,也不要自动装 gh 或创建账号。
|
|
10
27
|
2. 用 `gh issue list --repo github.com/openyida/openyida --state all --search '<问题关键词>' --json number,title,url` 查重,阅读匹配正文核实症状与版本。相同问题给现有链接;用户明确允许补充时才评论,不关闭别人的 Issue。无法查询时说明查重未完成。
|
|
11
28
|
3. 已完成最终稿和截图的公开确认后,用 `gh issue create --repo github.com/openyida/openyida --title '<脱敏标题>' --body-file '<issue.md>'`。有已确认截图时,先检查当前 `gh issue create --help` 是否支持 `--attach`,每张显式追加 `--attach <审阅后的截图>`;同一命令只包含用户批准的图片,不上传额外附件。不支持时用可操作的 GitHub 网页上传,不静默丢图。默认不指定未知 label/assignee。
|
|
@@ -17,10 +34,10 @@ PR 需要真实修复 diff、修复前后测试对比和最终内容公开确认
|
|
|
17
34
|
|
|
18
35
|
1. 先说明将下载公开 OpenYida 源码。用户已明确要求修复/PR 时,在业务目录之外的新贡献目录准备代码;无网络、空间不足或不愿下载则交付报告,不阻塞用户原任务。不覆盖现有目录,不修改全局 npm 包。仅 clone `https://github.com/openyida/openyida.git`,读取其 AGENTS.md,fetch 最新公开 main 后新建 `codex/<修复名>` 分支。复现用户安装版本与当前 main 差异,问题已修复时给版本证据,不重复造修复。
|
|
19
36
|
2. 用公开代码/技能和最小合成样例定位问题,补最小改动和有意义的回归验证。验证须覆盖原复现,不把本地通过说成平台/浏览器通过。先交付 diff、修复前后同一复现/测试的实际结果、回归结果、未验证事项和 PR 正文。用结构化问答确认正文、截图、测试报告中有无敏感信息或不想公开的内容;用户要求删除/修改后重新展示并确认,再进行授权范围内的远端操作;仓库约定仍适用。
|
|
20
|
-
3. 读取 `
|
|
37
|
+
3. 读取 `openyida feedback github status --json`,复用当前用户已验证的 `fork.url`(包括改名的个人 Fork)。即使该账号有上游写权限,也坚持个人 Fork。仅当状态为 absent 且用户已授权 Fork/PR 时执行 `openyida feedback github fork --confirm --json`;pending-verification 先只读核查,不重复创建。无 gh 时用现有连接器/浏览器完成同等核验。不能将网络或权限错误当作不存在。再次核验 Fork、parent 为 `openyida/openyida` 且所有者是当前用户;同名非 Fork、组织账号或所有者为上游时停止远端写入并回到 Issue 路径。PR/Fork 授权不包含 Star。
|
|
21
38
|
4. 在贡献目录添加指向已核实个人 Fork 的专用 remote(如 `contributor`)。先读 `git remote -v`;同名 remote 不同目标时换新名称,不覆盖。只向该个人 Fork push 当前修复分支。不要执行裸 `git push`,不要推上游,不 force push,不携带业务目录文件。仅在用户授权提交/push 时执行;如需 commit 遵循仓库约定,明确列出本次文件,禁用 `git add .`。
|
|
22
39
|
5. 先查该 head 是否已有 PR:`gh pr list --repo github.com/openyida/openyida --state all --head '<分支>' --json number,url,state,headRepositoryOwner,headRepository,baseRefName`,再核对 head 所有者和仓库是已验证个人 Fork(list 的 --head 不支持 owner:branch);已有则读取并复用,关闭/合并过的 PR 先核对状态,不自行重开。
|
|
23
40
|
6. 明确指定 head,防止 gh 自动推送或选择上游分支:`gh pr create --repo github.com/openyida/openyida --base main --head '<用户>:<分支>' --draft --title '<脱敏标题>' --body-file '<pr-body.md>'`。PR 正文包含原问题、实际复现步骤、修复行为、修复前后测试对比(版本/命令/实际结果/证据)、回归验证和剩余限制。有已确认截图时检查当前 gh 是否支持 `--attach`,逐张显式附加;不支持则走可操作的网页上传,不省略用户批准的图片。不得使用 `--dry-run` 作为零副作用验证,它可能 push。
|
|
24
41
|
7. 读回 PR 的 base、head 仓库/所有者、分支、正文和 URL,确认个人 Fork → 上游 main。失败或超时先只读检查 Issue/PR/分支,不能自动重复创建或换账号。最终给真实链接与验证结论,不代合并。
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
官方契约:[Star API](https://docs.github.com/en/rest/activity/starring)、[Fork API](https://docs.github.com/en/rest/repos/forks)、[个人 Fork 查询](https://docs.github.com/en/graphql/reference/repos)、[Issue](https://cli.github.com/manual/gh_issue_create)、[PR](https://cli.github.com/manual/gh_pr_create)、[PR 查重](https://cli.github.com/manual/gh_pr_list)。
|