openyida 2026.6.1-beta.0 → 2026.6.3-beta.0

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.
@@ -139,16 +139,28 @@ async function updateFormConfig(appType, formUuid, authRef) {
139
139
  return result;
140
140
  }
141
141
 
142
- // ── 适配 SerialNumberField formula ───────────────────
142
+ // ── 适配 Schema 中的应用 / 表单标识符 ─────────────────
143
143
  //
144
- // Schema 中所有 SerialNumberField formula 里的旧 appType 替换为新 appType,
145
- // 同时将旧 formUuid 替换为新 formUuid。
144
+ // 导入时需要把 Schema 里所有对旧 appType / formUuid 的引用(页面 id、组件
145
+ // props.appType、actions.source、SerialNumberField 公式等)重映射为新值。
146
+ //
147
+ // 早期实现对整个 Schema JSON 做无约束的全局字符串替换,存在数据腐蚀风险:
148
+ // 若某个字段的文本值、label、备注恰好包含旧 appType / formUuid 子串,会被
149
+ // 误替换。这里改为「标识符边界」约束替换——appType / formUuid 都是由字母、
150
+ // 数字、下划线、连字符构成的标识符,只有当匹配片段两侧不是同类标识符字符时
151
+ // 才替换,从而避免误伤包含旧标识符子串的普通文本。
152
+
153
+ function buildIdentifierBoundaryRegExp(identifier) {
154
+ // 标识符字符集:字母、数字、下划线、连字符。
155
+ // 使用 lookbehind / lookahead 保证匹配片段不是更长标识符的一部分。
156
+ return new RegExp(`(?<![A-Za-z0-9_-])${escapeRegExp(identifier)}(?![A-Za-z0-9_-])`, 'g');
157
+ }
146
158
 
147
- function adaptSerialNumberFormulas(schema, oldAppType, newAppType, oldFormUuid, newFormUuid) {
159
+ function adaptSchemaIdentifiers(schema, oldAppType, newAppType, oldFormUuid, newFormUuid) {
148
160
  const schemaStr = JSON.stringify(schema);
149
161
  const adapted = schemaStr
150
- .replace(new RegExp(escapeRegExp(oldAppType), 'g'), newAppType)
151
- .replace(new RegExp(escapeRegExp(oldFormUuid), 'g'), newFormUuid);
162
+ .replace(buildIdentifierBoundaryRegExp(oldAppType), newAppType)
163
+ .replace(buildIdentifierBoundaryRegExp(oldFormUuid), newFormUuid);
152
164
  return JSON.parse(adapted);
153
165
  }
154
166
 
@@ -287,8 +299,8 @@ async function run(args) {
287
299
  continue;
288
300
  }
289
301
 
290
- // 将 Schema 中所有旧 appType / formUuid 替换为新值
291
- const adaptedSchema = adaptSerialNumberFormulas(
302
+ // 将 Schema 中所有旧 appType / formUuid 引用重映射为新值(标识符边界约束,避免误伤普通文本)
303
+ const adaptedSchema = adaptSchemaIdentifiers(
292
304
  originalSchema,
293
305
  sourceAppType,
294
306
  newAppType,
@@ -377,7 +389,7 @@ module.exports = {
377
389
  __test__: {
378
390
  normalizeImportFormType,
379
391
  buildCreateFormPostData,
380
- adaptSerialNumberFormulas,
392
+ adaptSchemaIdentifiers,
381
393
  extractSchemaContent,
382
394
  },
383
395
  };
@@ -2,6 +2,42 @@
2
2
  * 执行动作配置生成模块
3
3
  */
4
4
 
5
+ // 可能携带凭证 / 敏感信息的请求头(小写匹配)。
6
+ // 这些头的真实值绝不能写入生成的 Action 配置,否则会把用户 token 泄漏到
7
+ // 落盘的连接器配置里。命中时用占位符替换,提示用户在连接器认证中配置。
8
+ const SENSITIVE_HEADER_PATTERNS = [
9
+ 'authorization',
10
+ 'cookie',
11
+ 'token',
12
+ 'api-key',
13
+ 'apikey',
14
+ 'secret',
15
+ 'access-token',
16
+ 'x-acs-dingtalk-access-token',
17
+ 'x-api-key',
18
+ 'proxy-authorization',
19
+ ];
20
+
21
+ function isSensitiveHeader(headerName) {
22
+ const lower = String(headerName || '').toLowerCase();
23
+ return SENSITIVE_HEADER_PATTERNS.some((pattern) => lower.includes(pattern));
24
+ }
25
+
26
+ /**
27
+ * 计算请求头写入 Action 配置的默认值。
28
+ * 敏感头返回占位符(不泄漏真实凭证),普通头截断展示前 50 字符。
29
+ * @param {string} headerName - 请求头名称
30
+ * @param {string} headerValue - 请求头原始值
31
+ * @returns {string} 可安全写入配置的默认值
32
+ */
33
+ function buildHeaderDefaultValue(headerName, headerValue) {
34
+ if (isSensitiveHeader(headerName)) {
35
+ return '';
36
+ }
37
+ const value = String(headerValue === null || headerValue === undefined ? '' : headerValue);
38
+ return value.length > 50 ? value.substring(0, 50) + '...' : value;
39
+ }
40
+
5
41
  /**
6
42
  * 根据URL路径生成有意义的动作名称和描述
7
43
  * @param {string} path - URL路径
@@ -125,7 +161,7 @@ function generateOperation(curlData, relevantHeaders) {
125
161
  paramType: 'String',
126
162
  desc: key,
127
163
  required: false,
128
- defaultValue: value.substring(0, 50) + (value.length > 50 ? '...' : ''),
164
+ defaultValue: buildHeaderDefaultValue(key, value),
129
165
  children: [],
130
166
  childList: [],
131
167
  __level: 0,
@@ -262,5 +298,7 @@ function generateOperation(curlData, relevantHeaders) {
262
298
 
263
299
  module.exports = {
264
300
  generateActionInfo,
265
- generateOperation
301
+ generateOperation,
302
+ isSensitiveHeader,
303
+ buildHeaderDefaultValue
266
304
  };
@@ -60,7 +60,13 @@ function printTable(headers, rows) {
60
60
  function buildOperationsSummary(operations) {
61
61
  if (!Array.isArray(operations) || operations.length === 0) {return '';}
62
62
 
63
- const summaries = operations.map(op => op.summary || op.operationId);
63
+ const summaries = operations
64
+ .map(op => op.summary || op.name || op.operationId || op.id || op.url || op.path)
65
+ .filter(Boolean);
66
+
67
+ if (summaries.length === 0) {
68
+ return '';
69
+ }
64
70
 
65
71
  if (summaries.length === 1) {
66
72
  return `支持${summaries[0]}`;
@@ -23,6 +23,7 @@ const {
23
23
  getConnectorDetail,
24
24
  saveConnector,
25
25
  } = require('./api');
26
+ const { normalizeOperations } = require('./operation-normalizer');
26
27
 
27
28
  function showUsage() {
28
29
  console.log(`
@@ -99,6 +100,7 @@ async function run(args) {
99
100
  if (!Array.isArray(newOperations)) {
100
101
  newOperations = [newOperations];
101
102
  }
103
+ newOperations = normalizeOperations(newOperations);
102
104
  console.log(`✓ 已加载 ${newOperations.length} 个执行动作`);
103
105
  newOperations.forEach((operation, index) => {
104
106
  console.log(` [${index + 1}] ${operation.operationId}: ${operation.summary}`);
@@ -123,7 +125,7 @@ async function run(args) {
123
125
  const detail = await getConnectorDetail(connector.connectorName, authRef);
124
126
  targetConnector = { ...connector, detail };
125
127
 
126
- const existingOps = JSON.parse(targetConnector.detail.operations || '[]');
128
+ const existingOps = normalizeOperations(JSON.parse(targetConnector.detail.operations || '[]'));
127
129
  console.log('📋 即将追加到以下连接器:');
128
130
  console.log(` 名称: ${targetConnector.displayName}`);
129
131
  console.log(` ID: ${targetConnector.id}`);
@@ -164,7 +166,7 @@ async function run(args) {
164
166
  }
165
167
 
166
168
  // 合并执行动作
167
- const existingOperations = JSON.parse(targetConnector.detail.operations || '[]');
169
+ const existingOperations = normalizeOperations(JSON.parse(targetConnector.detail.operations || '[]'));
168
170
  const existingIds = new Set(existingOperations.map(op => op.operationId));
169
171
  const duplicates = newOperations.filter(op => existingIds.has(op.operationId));
170
172
 
@@ -31,6 +31,7 @@ const {
31
31
  getConnectorDetail,
32
32
  saveConnector,
33
33
  } = require('./api');
34
+ const { normalizeOperations } = require('./operation-normalizer');
34
35
 
35
36
  function showUsage() {
36
37
  console.log(`
@@ -232,11 +233,11 @@ async function run(args) {
232
233
  console.log(`当前描述: ${connector.connectorDesc || '(空)'}\n`);
233
234
 
234
235
  const detail = await getConnectorDetail(connector.connectorName, authRef);
235
- const currentOperations = JSON.parse(detail.operations || '[]');
236
+ const currentOperations = normalizeOperations(JSON.parse(detail.operations || '[]'));
236
237
  const newDesc = buildConnectorDesc(options.description, connector.connectorDesc, authRef, currentOperations);
237
238
 
238
239
  await saveConnector({
239
- operations: detail.operations || '[]',
240
+ operations: JSON.stringify(currentOperations),
240
241
  displayName: detail.displayName,
241
242
  iconUrl: detail.iconUrl || 'chaxun%%#FFA200',
242
243
  connectorDesc: newDesc,
@@ -278,6 +279,7 @@ async function run(args) {
278
279
  fail('错误: operations 文件不能为空数组');
279
280
  process.exit(1);
280
281
  }
282
+ operations = normalizeOperations(operations);
281
283
  console.log(`✓ 已加载 ${operations.length} 个执行动作`);
282
284
  } catch (e) {
283
285
  fail(`读取执行动作配置文件失败: ${e.message}`);
@@ -17,6 +17,7 @@ const {
17
17
  getConnectorDetail,
18
18
  saveConnector,
19
19
  } = require('./api');
20
+ const { normalizeOperations } = require('./operation-normalizer');
20
21
 
21
22
  function showUsage() {
22
23
  console.log(`
@@ -63,7 +64,7 @@ async function run(args) {
63
64
  }
64
65
 
65
66
  const detail = await getConnectorDetail(connector.connectorName, authRef);
66
- const currentOperations = JSON.parse(detail.operations || '[]');
67
+ const currentOperations = normalizeOperations(JSON.parse(detail.operations || '[]'));
67
68
  const targetOperation = currentOperations.find(op => op.operationId === operationId);
68
69
 
69
70
  if (!targetOperation) {
@@ -2,6 +2,36 @@
2
2
  * Curl 命令解析模块
3
3
  */
4
4
 
5
+ /**
6
+ * 从 curl 命令中提取 URL。
7
+ * 早期实现只匹配 `curl "<url>"`(必须带引号且紧跟 curl),导致裸 URL、
8
+ * 单引号、`--url` 参数、`-X POST` 在 URL 之前等常见写法全部解析失败。
9
+ * 这里按优先级多策略提取:
10
+ * 1. 显式 `--url <url>` 参数
11
+ * 2. 命令中任意带引号的 http(s) URL
12
+ * 3. 命令中任意裸 http(s) URL(截断到空白处)
13
+ * @param {string} curlCommand - curl 命令字符串
14
+ * @returns {string} 提取到的 URL,未找到返回空字符串
15
+ */
16
+ function extractCurlUrl(curlCommand) {
17
+ const explicitUrl = curlCommand.match(/--url\s+['"]?([^'"\s]+)['"]?/);
18
+ if (explicitUrl) {
19
+ return explicitUrl[1];
20
+ }
21
+
22
+ const quotedUrl = curlCommand.match(/['"](https?:\/\/[^'"]+)['"]/);
23
+ if (quotedUrl) {
24
+ return quotedUrl[1];
25
+ }
26
+
27
+ const bareUrl = curlCommand.match(/(https?:\/\/[^\s'"]+)/);
28
+ if (bareUrl) {
29
+ return bareUrl[1];
30
+ }
31
+
32
+ return '';
33
+ }
34
+
5
35
  /**
6
36
  * 解析 curl 命令
7
37
  * @param {string} curlCommand - curl 命令字符串
@@ -19,10 +49,9 @@ function parseCurl(curlCommand) {
19
49
  };
20
50
 
21
51
  try {
22
- // 提取 URL
23
- const urlMatch = curlCommand.match(/curl\s+['"]([^'"]+)['"]/);
24
- if (urlMatch) {
25
- result.url = urlMatch[1];
52
+ // 提取 URL:兼容带引号 URL、裸 URL、--url 参数、-X POST 在前等多种写法
53
+ result.url = extractCurlUrl(curlCommand);
54
+ if (result.url) {
26
55
  const url = new URL(result.url);
27
56
  result.protocol = url.protocol.replace(':', '');
28
57
  result.host = url.hostname;
@@ -30,10 +59,10 @@ function parseCurl(curlCommand) {
30
59
  }
31
60
 
32
61
  // 提取方法
33
- const methodMatch = curlCommand.match(/-X\s+(\w+)/);
62
+ const methodMatch = curlCommand.match(/(?:-X|--request)\s+['"]?(\w+)['"]?/);
34
63
  if (methodMatch) {
35
64
  result.method = methodMatch[1].toUpperCase();
36
- } else if (curlCommand.includes('--data') || curlCommand.includes('-d')) {
65
+ } else if (curlCommand.includes('--data') || /\s-d\b/.test(curlCommand)) {
37
66
  result.method = 'POST';
38
67
  }
39
68
 
@@ -64,10 +93,11 @@ function detectAuthType(headers) {
64
93
  const authHeader = headers['Authorization'] || headers['authorization'];
65
94
 
66
95
  if (authHeader) {
67
- if (authHeader.startsWith('Bearer')) {
96
+ const scheme = authHeader.trim().toLowerCase();
97
+ if (scheme.startsWith('bearer')) {
68
98
  return { type: 'API密钥', code: 'ApiKeyAuth', headerName: 'Authorization' };
69
99
  }
70
- if (authHeader.startsWith('Basic')) {
100
+ if (scheme.startsWith('basic')) {
71
101
  return { type: '基本身份验证', code: 'BasicAuth', headerName: 'Authorization' };
72
102
  }
73
103
  }
@@ -116,6 +146,7 @@ function filterBrowserHeaders(headers) {
116
146
 
117
147
  module.exports = {
118
148
  parseCurl,
149
+ extractCurlUrl,
119
150
  detectAuthType,
120
151
  filterBrowserHeaders,
121
152
  BROWSER_HEADERS
@@ -104,21 +104,22 @@ class MarkdownParser extends BaseDocParser {
104
104
  }
105
105
 
106
106
  parseRequestHeaders() {
107
- const headerSection = this.extractSection('请求头', '请求参数');
107
+ const headerSection = this.extractSection('请求头', ['查询参数', '请求参数', '请求体', '响应']);
108
108
  if (headerSection) {
109
109
  this.result.requestInfo.headers = this.parseTable(headerSection);
110
110
  }
111
111
  }
112
112
 
113
113
  parseQueryParams() {
114
- const querySection = this.extractSection('查询参数', '请求体');
114
+ const querySection = this.extractSection('查询参数', ['请求体', '响应', '错误码']) ||
115
+ this.extractSection('请求参数', ['请求体', '响应', '错误码']);
115
116
  if (querySection) {
116
117
  this.result.requestInfo.query = this.parseTable(querySection);
117
118
  }
118
119
  }
119
120
 
120
121
  parseRequestBody() {
121
- const bodySection = this.extractSection('请求体', '响应');
122
+ const bodySection = this.extractSection('请求体', ['响应', '返回', '错误码']);
122
123
  if (!bodySection) {return;}
123
124
 
124
125
  const jsonMatch = bodySection.match(/```json\s*([\s\S]*?)```/);
@@ -289,10 +290,13 @@ class MarkdownParser extends BaseDocParser {
289
290
 
290
291
  let endIndex = this.content.length;
291
292
  if (endMarker) {
292
- const endMatch = this.content.indexOf(endMarker, startIndex + startMarker.length);
293
- if (endMatch !== -1) {
294
- endIndex = endMatch;
295
- }
293
+ const markers = Array.isArray(endMarker) ? endMarker : [endMarker];
294
+ markers.forEach(marker => {
295
+ const endMatch = this.content.indexOf(marker, startIndex + startMarker.length);
296
+ if (endMatch !== -1 && endMatch < endIndex) {
297
+ endIndex = endMatch;
298
+ }
299
+ });
296
300
  }
297
301
 
298
302
  return this.content.substring(startIndex, endIndex);
@@ -343,6 +347,15 @@ class MarkdownParser extends BaseDocParser {
343
347
  }
344
348
  }
345
349
 
350
+ function applyParamLocation(nodes, paramLocation) {
351
+ (Array.isArray(nodes) ? nodes : []).forEach(node => {
352
+ node.paramLocation = paramLocation;
353
+ applyParamLocation(node.childList, paramLocation);
354
+ applyParamLocation(node.children, paramLocation);
355
+ });
356
+ return nodes;
357
+ }
358
+
346
359
  /**
347
360
  * 文档解析器工厂
348
361
  */
@@ -424,6 +437,7 @@ function convertToOperationConfig(parseResult) {
424
437
  childList: [],
425
438
  __level: 0,
426
439
  hidden: false,
440
+ paramLocation: 'header',
427
441
  required: h.required === 'true' || h.required === '是' || h.required === '**' || h.required === true,
428
442
  defaultValue: h.example || h.value || ''
429
443
  }));
@@ -433,7 +447,8 @@ function convertToOperationConfig(parseResult) {
433
447
  desc: '请求头',
434
448
  name: 'Headers',
435
449
  paramType: 'Object',
436
- required: false
450
+ required: false,
451
+ paramLocation: 'header'
437
452
  });
438
453
 
439
454
  operation.parameters.header = parseResult.requestInfo.headers.map(h => ({
@@ -452,7 +467,8 @@ function convertToOperationConfig(parseResult) {
452
467
  children: [],
453
468
  childList: [],
454
469
  __level: 0,
455
- hidden: false
470
+ hidden: false,
471
+ paramLocation: 'query'
456
472
  }));
457
473
 
458
474
  operation.inputs.push({
@@ -460,7 +476,8 @@ function convertToOperationConfig(parseResult) {
460
476
  desc: '查询参数',
461
477
  name: 'Query',
462
478
  paramType: 'Object',
463
- required: false
479
+ required: false,
480
+ paramLocation: 'query'
464
481
  });
465
482
 
466
483
  operation.parameters.query = parseResult.requestInfo.query.map(q => ({
@@ -478,7 +495,7 @@ function convertToOperationConfig(parseResult) {
478
495
  default: JSON.stringify(requestExample)
479
496
  };
480
497
 
481
- const requestChildList = generateChildList(requestSchema, operation.operationId);
498
+ const requestChildList = applyParamLocation(generateChildList(requestSchema, operation.operationId), 'body');
482
499
  if (requestChildList.length > 0) {
483
500
  operation.inputs.push({
484
501
  defaultValue: JSON.stringify(requestExample),
@@ -486,7 +503,8 @@ function convertToOperationConfig(parseResult) {
486
503
  name: 'Body',
487
504
  paramType: 'Object',
488
505
  required: false,
489
- childList: requestChildList
506
+ childList: requestChildList,
507
+ paramLocation: 'body'
490
508
  });
491
509
  }
492
510
  }
@@ -496,13 +514,6 @@ function convertToOperationConfig(parseResult) {
496
514
  const responseSchema = parseResult.responseInfo.schema;
497
515
  operation.responses = responseSchema;
498
516
 
499
- if (!operation.parameters.body) {
500
- const example = generateExample(responseSchema);
501
- operation.parameters.body = {
502
- default: JSON.stringify(example)
503
- };
504
- }
505
-
506
517
  operation.outputs = [generateOutputs(responseSchema, operation.operationId)];
507
518
  } else {
508
519
  operation.responses = { type: 'object' };
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ const { generateOutputs } = require('./response-parser');
4
+
5
+ function slugifyOperationId(value, fallback) {
6
+ const slug = String(value || '')
7
+ .trim()
8
+ .replace(/^\//, '')
9
+ .replace(/[^A-Za-z0-9_]+/g, '_')
10
+ .replace(/^_+|_+$/g, '');
11
+
12
+ return slug || fallback;
13
+ }
14
+
15
+ function deriveOperationId(operation, index) {
16
+ const explicitId = String(operation.operationId || '').trim();
17
+ if (explicitId) {
18
+ return explicitId;
19
+ }
20
+
21
+ const fallback = `operation_${index + 1}`;
22
+ const candidates = [operation.url, operation.path, operation.id, operation.name, operation.summary];
23
+ for (const candidate of candidates) {
24
+ const operationId = slugifyOperationId(candidate, '');
25
+ if (operationId) {
26
+ return operationId;
27
+ }
28
+ }
29
+
30
+ return fallback;
31
+ }
32
+
33
+ function normalizeUrl(value) {
34
+ return String(value || '').trim().replace(/^\/+/, '');
35
+ }
36
+
37
+ function inferParamLocation(input) {
38
+ if (input.paramLocation) {
39
+ return input.paramLocation;
40
+ }
41
+
42
+ const name = String(input.name || '').toLowerCase();
43
+ if (name === 'headers' || name === 'header') {
44
+ return 'header';
45
+ }
46
+ if (name === 'query' || name === 'queries') {
47
+ return 'query';
48
+ }
49
+ if (name === 'path' || name === 'paths') {
50
+ return 'path';
51
+ }
52
+ if (name === 'body') {
53
+ return 'body';
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ function normalizeParamNode(node, paramLocation) {
60
+ if (!node || typeof node !== 'object') {
61
+ return node;
62
+ }
63
+
64
+ const normalized = { ...node };
65
+ if (paramLocation && !normalized.paramLocation) {
66
+ normalized.paramLocation = paramLocation;
67
+ }
68
+ if (!Array.isArray(normalized.children)) {
69
+ normalized.children = [];
70
+ }
71
+ if (!Array.isArray(normalized.childList)) {
72
+ normalized.childList = [];
73
+ }
74
+ normalized.children = normalized.children.map(child => normalizeParamNode(child, paramLocation));
75
+ normalized.childList = normalized.childList.map(child => normalizeParamNode(child, paramLocation));
76
+ return normalized;
77
+ }
78
+
79
+ function normalizeInput(input) {
80
+ if (!input || typeof input !== 'object') {
81
+ return input;
82
+ }
83
+
84
+ const paramLocation = inferParamLocation(input);
85
+ const normalized = { ...input };
86
+ if (paramLocation && !normalized.paramLocation) {
87
+ normalized.paramLocation = paramLocation;
88
+ }
89
+ if (!Array.isArray(normalized.childList)) {
90
+ normalized.childList = [];
91
+ }
92
+ normalized.childList = normalized.childList.map(child => normalizeParamNode(child, paramLocation));
93
+ return normalized;
94
+ }
95
+
96
+ function buildParametersFromInputs(inputs) {
97
+ const parameters = { header: [] };
98
+
99
+ for (const input of inputs) {
100
+ if (!input || typeof input !== 'object') {
101
+ continue;
102
+ }
103
+
104
+ const location = inferParamLocation(input);
105
+ const childList = Array.isArray(input.childList) ? input.childList : [];
106
+
107
+ if (location === 'header') {
108
+ parameters.header = childList.map(child => ({
109
+ name: child.name,
110
+ value: child.defaultValue || '',
111
+ }));
112
+ } else if (location === 'query') {
113
+ parameters.query = childList.map(child => ({
114
+ name: child.name,
115
+ value: child.defaultValue || '',
116
+ }));
117
+ } else if (location === 'body' && input.defaultValue !== undefined) {
118
+ parameters.body = { default: input.defaultValue };
119
+ }
120
+ }
121
+
122
+ return parameters;
123
+ }
124
+
125
+ function normalizeParameters(parameters, inputs) {
126
+ const normalized = parameters && typeof parameters === 'object' && !Array.isArray(parameters)
127
+ ? { ...parameters }
128
+ : buildParametersFromInputs(inputs);
129
+
130
+ if (!Array.isArray(normalized.header)) {
131
+ normalized.header = [];
132
+ }
133
+
134
+ return normalized;
135
+ }
136
+
137
+ function normalizeOutput(output) {
138
+ if (!output || typeof output !== 'object') {
139
+ return output;
140
+ }
141
+
142
+ const normalized = { ...output };
143
+ if (!Array.isArray(normalized.childList)) {
144
+ normalized.childList = [];
145
+ }
146
+ return normalized;
147
+ }
148
+
149
+ function normalizeOperation(operation, index) {
150
+ if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
151
+ throw new Error(`第 ${index + 1} 个执行动作必须是对象`);
152
+ }
153
+
154
+ const operationId = deriveOperationId(operation, index);
155
+ const rawUrl = operation.url !== undefined && operation.url !== null ? operation.url : operation.path;
156
+ const hasUrl = rawUrl !== undefined && rawUrl !== null && String(rawUrl).trim() !== '';
157
+ const url = normalizeUrl(rawUrl);
158
+
159
+ if (!hasUrl) {
160
+ throw new Error(`第 ${index + 1} 个执行动作缺少 url/path`);
161
+ }
162
+
163
+ const summary = operation.summary || operation.name || operationId;
164
+ const description = operation.description || operation.desc || summary;
165
+ const method = String(operation.method || 'get').toLowerCase();
166
+ const inputs = Array.isArray(operation.inputs)
167
+ ? operation.inputs.map(normalizeInput)
168
+ : [];
169
+ const responses = operation.responses || { type: 'object', properties: {} };
170
+ const outputs = Array.isArray(operation.outputs) && operation.outputs.length > 0
171
+ ? operation.outputs.map(normalizeOutput)
172
+ : [generateOutputs(responses, operationId)];
173
+
174
+ return {
175
+ ...operation,
176
+ id: operation.id || `operation-${operationId}`,
177
+ operationId,
178
+ summary,
179
+ description,
180
+ url,
181
+ method,
182
+ inputs,
183
+ parameters: normalizeParameters(operation.parameters, inputs),
184
+ responses,
185
+ outputs,
186
+ origin: operation.origin !== undefined ? operation.origin : true,
187
+ };
188
+ }
189
+
190
+ function normalizeOperations(operations) {
191
+ const list = Array.isArray(operations) ? operations : [operations];
192
+ return list.map(normalizeOperation);
193
+ }
194
+
195
+ module.exports = {
196
+ normalizeOperation,
197
+ normalizeOperations,
198
+ };
package/lib/core/chalk.js CHANGED
@@ -36,6 +36,8 @@ const c = {
36
36
  cyan: '\x1b[36m',
37
37
  white: '\x1b[37m',
38
38
  gray: '\x1b[90m',
39
+ bgBlue: '\x1b[44m',
40
+ bgCyan: '\x1b[46m',
39
41
  };
40
42
 
41
43
  // ── 图标常量 ───────────────────────────────────────────
@@ -13,7 +13,7 @@
13
13
 
14
14
  const { execSync } = require('child_process');
15
15
  const readline = require('readline');
16
- const { warn } = require('./chalk');
16
+ const { c, warn } = require('./chalk');
17
17
  const {
18
18
  loadEnvsConfig,
19
19
  saveEnvsConfig,
@@ -28,14 +28,14 @@ const {
28
28
 
29
29
  // ── 颜色常量 ──────────────────────────────────────────
30
30
 
31
- const RESET = '\x1b[0m';
32
- const BOLD = '\x1b[1m';
33
- const DIM = '\x1b[2m';
34
- const GREEN = '\x1b[32m';
35
- const YELLOW = '\x1b[33m';
36
- const CYAN = '\x1b[36m';
37
- const RED = '\x1b[31m';
38
- const BLUE = '\x1b[34m';
31
+ const RESET = c.reset;
32
+ const BOLD = c.bold;
33
+ const DIM = c.dim;
34
+ const GREEN = c.green;
35
+ const YELLOW = c.yellow;
36
+ const CYAN = c.cyan;
37
+ const RED = c.red;
38
+ const BLUE = c.blue;
39
39
 
40
40
  // ── 工具函数 ──────────────────────────────────────────
41
41
 
@@ -12,18 +12,18 @@
12
12
  const { execSync } = require('child_process');
13
13
  const { fetchLatestVersion, isNewer } = require('./check-update');
14
14
  const { t } = require('./i18n');
15
- const { warn } = require('./chalk');
15
+ const { c, warn } = require('./chalk');
16
16
  const { getNpmExecutable } = require('./utils');
17
17
 
18
18
  // ── ANSI 颜色常量 ──────────────────────────────────
19
- const RESET = '\x1b[0m';
20
- const BOLD = '\x1b[1m';
21
- const DIM = '\x1b[2m';
22
- const GREEN = '\x1b[32m';
23
- const YELLOW = '\x1b[33m';
24
- const CYAN = '\x1b[36m';
25
- const RED = '\x1b[31m';
26
- const MAGENTA = '\x1b[35m';
19
+ const RESET = c.reset;
20
+ const BOLD = c.bold;
21
+ const DIM = c.dim;
22
+ const GREEN = c.green;
23
+ const YELLOW = c.yellow;
24
+ const CYAN = c.cyan;
25
+ const RED = c.red;
26
+ const MAGENTA = c.magenta;
27
27
 
28
28
  // ── Spinner 动画 ───────────────────────────────────
29
29
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -12,7 +12,7 @@
12
12
 
13
13
  const { execSync, spawn } = require('child_process');
14
14
  const { t } = require('../core/i18n');
15
- const { warn } = require('../core/chalk');
15
+ const { c, warn } = require('../core/chalk');
16
16
 
17
17
  const DWS_BINARY_NAME = 'dws';
18
18
  const INSTALL_SCRIPTS = {
@@ -51,16 +51,16 @@ function getDwsVersion() {
51
51
  * 显示安装指引
52
52
  */
53
53
  function showInstallGuide() {
54
- const RESET = '\x1b[0m';
55
- const BOLD = '\x1b[1m';
56
- const DIM = '\x1b[2m';
57
- const CYAN = '\x1b[36m';
58
- const GREEN = '\x1b[32m';
59
- const YELLOW = '\x1b[33m';
60
- const BLUE = '\x1b[34m';
54
+ const RESET = c.reset;
55
+ const BOLD = c.bold;
56
+ const DIM = c.dim;
57
+ const CYAN = c.cyan;
58
+ const GREEN = c.green;
59
+ const YELLOW = c.yellow;
60
+ const BLUE = c.blue;
61
61
 
62
- const BG_BLUE = '\x1b[44m';
63
- const WHITE = '\x1b[37m';
62
+ const BG_BLUE = c.bgBlue;
63
+ const WHITE = c.white;
64
64
 
65
65
  const isWindows = process.platform === 'win32';
66
66
  const installCommand = isWindows ? INSTALL_SCRIPTS.windows : INSTALL_SCRIPTS.unix;
@@ -221,11 +221,11 @@ async function executeDwsCommand(args) {
221
221
  * 显示 dws 帮助信息
222
222
  */
223
223
  function showDwsHelp() {
224
- const RESET = '\x1b[0m';
225
- const BOLD = '\x1b[1m';
226
- const CYAN = '\x1b[36m';
227
- const GREEN = '\x1b[32m';
228
- const YELLOW = '\x1b[33m';
224
+ const RESET = c.reset;
225
+ const BOLD = c.bold;
226
+ const CYAN = c.cyan;
227
+ const GREEN = c.green;
228
+ const YELLOW = c.yellow;
229
229
 
230
230
  console.log('');
231
231
  console.log(`${BOLD}openyida dws - 钉钉 CLI 集成${RESET}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.6.1-beta.0",
3
+ "version": "2026.6.3-beta.0",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -181,7 +181,7 @@ openyida copy
181
181
  | `yida-corp-manager` | `skills/yida-corp-manager/SKILL.md` | 平台管理员、应用管理员、子管理员与通讯录权限 | `openyida corp-manager <子命令>` |
182
182
  | `yida-agent-center` | `skills/yida-agent-center/SKILL.md` | 代理中心:在职流程代理、离职代理、撤销和部分流程范围 | `openyida agent-center <子命令>` |
183
183
  | `yida-form-detail` | `skills/yida-form-detail/SKILL.md` | 表单详情页 formDetail 样式优化 | 详见 SKILL.md |
184
- | `yida-data-management` | `skills/yida-data-management/SKILL.md` | 表单/流程/任务数据查询与变更 | `openyida data query form <appType> <formUuid>` |
184
+ | `yida-data-management` | `skills/yida-data-management/SKILL.md` | 表单/子表/流程/任务数据查询与变更 | `openyida data query form <appType> <formUuid>` |
185
185
  | `yida-corp-efficiency` | `skills/yida-corp-efficiency/SKILL.md` | 平台管理企业效能概览、查看明细报表、报表接口模板、学习成果和通知群动作 | `openyida corp-efficiency` |
186
186
  | `yida-table-form` | `skills/yida-table-form/SKILL.md` | 表格形态批量录入页面 | 详见 SKILL.md |
187
187
  | `yida-process-rule` | `skills/yida-process-rule/SKILL.md` | 配置流程规则、审批/办理/抄送、条件/并行分支、字段权限、跳转规则和官方组件节点配置适配 | `openyida configure-process <appType> <formUuid> <流程JSON>` |
@@ -224,6 +224,9 @@ openyida copy
224
224
  - **避免无效重试**:同一命令失败后,先根据错误信息检查登录态、组织、参数和字段 ID;不要无修改地连续重试超过 1 次。
225
225
  - **数据性能优先**:统计/聚合类需求优先使用 `yida-report` 原生报表服务端聚合;不要在自定义页面前端分页拉取大量表单数据后自行聚合。
226
226
  - **模板优先**:自定义页面、表单字段、报表配置等复杂产物优先使用 `openyida sample` 或现有示例生成骨架,再做最小改动。
227
+ - **官方示例范式优先**:用户要求参考/蒸馏宜搭示例中心时,先按 [官方示例中心 Schema 范式](references/official-example-schema-patterns.md) 理解脱敏 schema 的承载方式,再优化技能或实现方案;不要只凭截图、卡片标题或页面视觉做判断。
228
+ - **按 schema 证据选择技能**:先看 `formType`、组件树和 `dataSource.online`。`receipt/process/report` 分别优先落到表单、流程、报表技能;只有默认页是自定义展示页、或确实需要列表/看板/工具页交互时,才默认落到 `yida-custom-page`。
229
+ - **配置承载优先于代码承载**:字段结构、公式、联动、报表聚合、审批规则、集成自动化、连接器动作应分别由对应技能承载;自定义页面代码只做展示、事件分发和必要胶水。
227
230
 
228
231
  ### 3. corpId 一致性检查(必须遵守)
229
232
 
@@ -0,0 +1,217 @@
1
+ # 官方示例中心 Schema 范式
2
+
3
+ > 来源:2026-06-01 对宜搭「开发者赋能平台 / 示例中心」capacity 标签下 156 个示例做只读 schema 抽取。本文只沉淀脱敏后的结构规律,不保存真实 cookie、CSRF token、用户信息或可直接复用的业务密钥。
4
+
5
+ ## 对 OpenYida 技能的核心启发
6
+
7
+ 官方示例中心反复出现的不是“每个需求都写一整页自定义代码”,而是以下范式:
8
+
9
+ 1. **Schema 类型先行**:先看 `formType`、组件树、`dataSource.online`,再决定技能;不要只按卡片标签或标题选择技能。
10
+ 2. **低代码配置优先**:能用表单字段、字段公式、字段联动、原生报表、流程规则、集成自动化表达的能力,不要默认落到自定义页面 JS。
11
+ 3. **页面三层结构**:页面型能力通常拆成 `状态 VALUE 数据源`、`REMOTE/连接器数据源`、`薄动作/渲染层`,而不是把状态、请求和 UI 全塞进一个函数。
12
+ 4. **单能力薄示例**:示例应用通常围绕一个能力闭环展开,先做可学习、可启用、可验证的小闭环,再扩展为完整业务应用。
13
+ 5. **数据源可见可审计**:连接器和远程 API 通过设计器数据源进入 schema,页面代码只通过 `this.dataSourceMap.<name>.load()` 或 OpenYida 封装 API 调用。
14
+ 6. **原生报表服务端聚合**:报表类示例优先用 `Youshu*` 原生报表组件和服务端聚合;自定义看板只负责展示和交互增强。
15
+
16
+ 把这些范式反哺技能时,核心不是复制官方 schema 字段,而是改进 Agent 的默认决策:先分类、再选择低代码承载面、最后只在必要处写自定义代码。
17
+
18
+ ## 只读抓取链路
19
+
20
+ 当用户要求“蒸馏示例中心”“参考官方示例”“把示例 schema 反哺技能”时,优先抓 schema,而不是只看截图或卡片文案。
21
+
22
+ 1. 示例中心页面本身是宜搭自定义页面,可通过只读接口获取页面 schema:
23
+
24
+ ```text
25
+ GET /bench/getLatestFormWithNavNew.json?formUuid=coe
26
+ ```
27
+
28
+ 2. 示例列表来自模板中心数据源:
29
+
30
+ ```text
31
+ GET /query/loginFreeFormData/listFormDataByType.json?type=templateCenter
32
+ ```
33
+
34
+ 关键参数:
35
+
36
+ ```json
37
+ {
38
+ "pageSize": 24,
39
+ "currentPage": 1,
40
+ "userLanguage": "zh_CN",
41
+ "searchFieldJson": "{\"isShow\":\"n\",\"tags\":\"capacity\",\"templateTitle\":\"\",\"description\":\"\"}",
42
+ "dynamicOrder": "{\"orderNum\":\"+\"}"
43
+ }
44
+ ```
45
+
46
+ 3. 通过模板 ID 获取源模板应用:
47
+
48
+ ```text
49
+ GET /query/appTpl/getAppTplComplexInfo.json?appTplUuid=TPL_XXX
50
+ ```
51
+
52
+ 读取 `content.latestOnlineAppTpl.appType`,不要调用 `copyTemplateApp`,那会创建真实应用。
53
+
54
+ 4. 打开模板只读预览页,从 HTML 的 `window.pageConfig.formUuid` 读取默认展示页:
55
+
56
+ ```text
57
+ https://template.aliwork.com/{APP_TYPE}/workbench/?appTplUuid=TPL_XXX&__yida_hide_enable_template__=true
58
+ ```
59
+
60
+ 5. 通过模板域名读取页面 schema:
61
+
62
+ ```text
63
+ GET https://template.aliwork.com/alibaba/web/{APP_TYPE}/query/formdesign/getSchemaWithAllNavs.json?formUuid={FORM_UUID}
64
+ ```
65
+
66
+ 只读抓取可以落到 `.cache/openyida/<项目名>/`,不要把原始 schema、cookie、token 或模板详情提交进仓库。
67
+
68
+ ## 本轮样本分布
69
+
70
+ 156 个 capacity 示例全部能通过上述链路拿到默认访问页 schema。默认页类型分布:
71
+
72
+ | 类型 | 数量 | 说明 |
73
+ | --- | ---: | --- |
74
+ | `receipt` | 80 | 普通表单页,常见于表单、公式、集成和连接器示例 |
75
+ | 自定义展示页 | 60 | `content.pages[0].formType` 为空,通常是自定义页面/数据看板/工具页 |
76
+ | `report` | 10 | 原生报表页,组件多为 `Youshu*` |
77
+ | `process` | 5 | 流程表单页 |
78
+ | `view` | 1 | 视图页 |
79
+
80
+ 标签不能直接等同于 schema 类型。例如 `report` 标签下既有原生报表,也有表单数据准备页和自定义看板;`integration` 标签下大多默认打开表单页,自动化逻辑流本体不在默认页面 schema 内。选择 OpenYida 子技能时先看 `formType`、组件树和数据源,再看标签。
81
+
82
+ 组件分布反映了官方默认承载方式:
83
+
84
+ | 类型 | 高频组件 | 范式解释 |
85
+ | --- | --- | --- |
86
+ | 表单页 | `FormContainer`、`TextField`、`NumberField`、`DateField`、`EmployeeField`、`TableField`、`RichText` | 字段结构、公式和少量说明文本承载能力 |
87
+ | 自定义页 | `Div`、`Text`、`Image`、`Button`、`PageSection`、`Dialog`、`TablePc`、`Pagination` | 列表、看板、工具页、弹窗和分页承载交互 |
88
+ | 报表页 | `YoushuPageHeader`、`YoushuTopFilterContainer`、`YoushuSelectFilter`、`YoushuTable`、`YoushuPieChart` | 原生报表负责筛选、聚合和图表 |
89
+ | 流程页 | `FormContainer` + 表单字段 | 表单只承载数据,审批节点由流程规则承载 |
90
+
91
+ ## 示例中心自身的页面模式
92
+
93
+ 示例中心 `coe` 页面是一个典型“模板列表 + 筛选 + 搜索 + 分页 + 点赞 + 启用弹窗”的自定义页面。它的 schema 规律适合反哺列表页、资源中心、模板中心类页面:
94
+
95
+ - `VALUE` 数据源保存页面状态:`currentPage`、`pageSize`、`searchWord`、`filterValue`、`tags`、`loading`、`tplItem`、`tplInfo` 等。
96
+ - 列表数据源调用 `listFormDataByType.json?type=templateCenter`,参数由 `state` 组合并 JSON 化。
97
+ - 搜索条件使用 `searchFieldJson`,例如 `isShow`、`tags`、`templateTitle`、`description`。
98
+ - 排序使用 `dynamicOrder`,例如 `{"orderNum":"+"}`。
99
+ - 详情弹窗先按 `appTplUuid` 查模板记录,再查 `getAppTplComplexInfo.json`。
100
+ - 点赞和启用记录通过独立数据源写表;启用模板通过 `copyTemplateApp`,属于有副作用接口,未经用户明确确认不要调用。
101
+
102
+ ## 自定义页面三层范式
103
+
104
+ 官方自定义页反复使用三层模型:
105
+
106
+ | 层 | schema 形态 | OpenYida 生成时的落点 |
107
+ | --- | --- | --- |
108
+ | 状态层 | `VALUE` 数据源:`loading`、`pageSize`、`currentPage`、`tableData`、`searchFieldJson`、`selectedRowKeys` | `_customState` 固定字段,`setCustomState()` 更新 |
109
+ | 数据层 | `REMOTE` / 连接器数据源:表单查询、任务、流程、保存、删除、连接器动作 | 优先 `this.dataSourceMap.<name>.load()`;否则用 `this.utils.yida.*` |
110
+ | 交互层 | `Div/Text/Button/TablePc/Dialog/Pagination/Tabs/Collapse` + 少量 JS 方法 | `renderJsx` 只做展示和事件分发,业务方法独立 `export function` |
111
+
112
+ 因此,生成列表/看板/工具页时,默认先设计状态字典和数据源清单,再写 JSX。不要一开始就写一个巨大的 `renderJsx`。
113
+
114
+ ## 自定义页面数据源模式
115
+
116
+ 官方自定义页面 schema 中常见 `dataSource.online` 结构:
117
+
118
+ ```json
119
+ {
120
+ "name": "getData",
121
+ "protocal": "REMOTE",
122
+ "dpType": "REMOTE",
123
+ "type": "legao",
124
+ "isInit": true,
125
+ "requestHandler": {
126
+ "type": "JSExpression",
127
+ "value": "this.utils.legaoBuiltin.dataSourceHandler"
128
+ },
129
+ "options": {
130
+ "method": "GET",
131
+ "url": {
132
+ "type": "variable",
133
+ "variable": "`/${window.pageConfig.appType || window.g_config.appKey}/v1/form/searchFormDatas.json`"
134
+ },
135
+ "params": {
136
+ "type": "variable",
137
+ "variable": "{ formUuid: state.formUuid, pageSize: state.pageSize, currentPage: state.currentPage }"
138
+ },
139
+ "didFetch": {
140
+ "source": "function didFetch(content) { return content; }"
141
+ },
142
+ "onError": {
143
+ "source": "function onError(error) { this.utils.toast({ title: error.message, type: 'error' }); }"
144
+ }
145
+ },
146
+ "dataHandler": {
147
+ "source": "function(data, err) { this.setState({ getData: data }); return data; }"
148
+ }
149
+ }
150
+ ```
151
+
152
+ 落到 OpenYida 手写 `.oyd.jsx` 时,不要照抄低代码 schema 里的 `this.setState`。应转换为:
153
+
154
+ - 页面状态放在 `_customState`,用 `setCustomState()` 合并更新。
155
+ - 读取宜搭表单/流程/任务数据时优先用 `this.utils.yida.*` 或已建好的 `this.dataSourceMap.<name>.load()`。
156
+ - 远程连接器和第三方 API 必须走设计器数据源,不要在 JSX 里直接 `fetch` 外部地址。
157
+ - 列表页保持 `loading / list / currentPage / pageSize / totalCount / filters / selectedRowKeys` 的稳定状态结构,失败时把 `loading` 置回 `false` 并 toast。
158
+ - 需要搜索或筛选时优先形成 `searchFieldJson` 状态,提交前再 JSON 化;不要把散落的筛选字段硬写进多个请求函数。
159
+
160
+ ## 连接器数据源回读形态
161
+
162
+ 连接器类示例的默认页 schema 中,部分数据源会被平台归一为普通 `REMOTE` 数据源:
163
+
164
+ ```json
165
+ {
166
+ "protocal": "REMOTE",
167
+ "dpType": "REMOTE",
168
+ "type": "legao",
169
+ "options": {
170
+ "method": "POST",
171
+ "url": "/query/publicService/invokeService.json?_csrf_token=***",
172
+ "params": {
173
+ "inputs": "{\"Headers\":{},\"Query\":{},\"Body\":{}}",
174
+ "serviceInfo": "{\"connectorInfo\":{\"connectorId\":\"Http_xxx\",\"actionId\":\"operationId\",\"type\":\"httpConnector\",\"connection\":123}}"
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ 因此:
181
+
182
+ - 新建页面数据源仍按 `yida-data-source-connectors` 技能规划,让设计器能看到数据源和连接器动作。
183
+ - 发布后回读 schema 时,既要接受显式 `YIDACONNECTOR` 形态,也要接受 `REMOTE + publicService/invokeService + serviceInfo.connectorInfo` 这种平台归一形态。
184
+ - 任何 `_csrf_token` 都来自运行时页面配置,不能写死到技能文档或页面源码。
185
+
186
+ ## 原生报表与自定义看板边界
187
+
188
+ 原生报表页 schema 通常满足:
189
+
190
+ - `formType: "report"`。
191
+ - 组件树包含 `YoushuPageHeader`、`YoushuTopFilterContainer`、`YoushuSelectFilter`、`YoushuTable`、`Youshu*Chart` 等。
192
+ - 标签为 `report` 但 `formType` 不是 `report` 时,往往只是报表数据准备页或自定义页面看板。
193
+
194
+ 落地规则:
195
+
196
+ - 创建或改造原生报表优先用 `yida-report`,不要手写 `Youshu*` schema。
197
+ - 需要更强视觉和交互时,先用原生报表做服务端聚合,再用 `yida-chart`/`yida-custom-page` 渲染。
198
+ - 不要在自定义页面前端分页拉全量表单数据做大规模聚合。
199
+
200
+ ## 表单、公式、流程与集成边界
201
+
202
+ - 公式示例主要是 `receipt` 或 `process` schema,公式通常落在字段属性 `valueType: "formula"`、`complexValue.formula`、`formula`,字段引用必须是 `#{fieldId}`。
203
+ - 流程示例默认页可能是表单页,也可能是自定义展示页;审批流节点和规则不一定在默认页面 schema 内,应改用 `yida-process-rule` 查询或配置。
204
+ - 集成自动化示例默认页多是触发表单或说明页;逻辑流本体不在默认页面 schema 内,应改用 `yida-integration` 查询/创建。
205
+ - 连接器示例默认页只能证明页面如何调用数据源,连接器本体和动作定义仍由 `yida-connector` / `yida-connector-safe-actions` 管理。
206
+
207
+ ## 技能选择速查
208
+
209
+ | schema 观察 | 优先技能 |
210
+ | --- | --- |
211
+ | `formType: "receipt"`,组件多为 `TextField` / `NumberField` / `TableField` | `yida-create-form-page`,公式需求再用 `yida-formula` |
212
+ | `formType: "process"` | `yida-create-process` / `yida-process-rule` |
213
+ | `formType: "report"` 或组件为 `Youshu*` | `yida-report` |
214
+ | 自定义展示页,组件为 `Div` / `Text` / `Button` / `TablePc` / `Dialog` | `yida-custom-page` |
215
+ | 自定义页面里有 `dataSource.online` 的远程接口 | `yida-data-source-connectors` + `yida-custom-page` |
216
+ | `publicService/invokeService` 或 `serviceInfo.connectorInfo` | `yida-connector` / `yida-data-source-connectors` |
217
+ | 默认页看不到逻辑流,但标签是 `integration` | `yida-integration` |
@@ -18,6 +18,16 @@ description: 宜搭表单页面创建与更新。适用于:创建新表单、
18
18
  - 字段定义或变更定义需要落盘时,必须写入 `.cache/openyida/<项目名>/`,例如 `.cache/openyida/pm/pm-fields-team.json`;不要在仓库根目录生成 `*-fields*.json` 或 `*-changes*.json`
19
19
  - **本技能不读写 memory**:formUuid 等信息输出到 stdout,通过 `.cache/<项目名>-schema.json` 持久化,不依赖跨会话的 memory 状态
20
20
 
21
+ ## 官方表单示例范式
22
+
23
+ 官方示例中心的表单类能力大多用 `FormContainer + 标准字段 + 字段属性/公式/联动` 承载,少量 `RichText` 用于说明。创建或更新表单时优先按这个顺序落地:
24
+
25
+ 1. 字段结构:用 `TextField`、`NumberField`、`DateField`、`EmployeeField`、`SelectField`、`TableField`、`AssociationFormField` 等标准字段表达数据模型。
26
+ 2. 字段公式:计算、默认值、日期/文本转换等用字段 `valueType: "formula"`、`complexValue.formula`、`formula`,不要改写成自定义页面 JS。
27
+ 3. 字段联动:显示隐藏、只读、onChange 自动赋值优先用 `rule` 模式;只有 OpenYida DSL 不覆盖的平台属性才用 `patch`。
28
+ 4. 说明/示例文字:需要解释能力时可增加 `RichText` 或说明字段,但业务字段仍应保持结构化。
29
+ 5. 提交后跨表/通知/流程动作不要塞进字段 JS;分别交给 `yida-integration`、`yida-process-rule` 或 `yida-connector`。
30
+
21
31
  ## 适用场景
22
32
 
23
33
  用户需要"创建表单"、"新增字段"、"修改表单结构"时使用。
@@ -44,6 +44,20 @@ description: 宜搭自定义页面 JSX 开发规范。React 16 宜搭原生 expo
44
44
  > 每条规则的代码示例、反模式和常见错误见 [编码指南](references/coding-guide.md)(编写代码前强制必读)。
45
45
  > 表单类 JSX 控件、筛选栏、表格、成员/附件等组件写法见 [组件指南](references/component-jsx-guide.md);未验证的平台组件能力不得编造。
46
46
 
47
+ ## 官方示例范式内化
48
+
49
+ 官方示例中心的自定义页不是“整页手写逻辑”,而是 `状态层 + 数据源层 + 交互层` 三层结构。生成页面前先列出这三层,再写代码:
50
+
51
+ | 层 | 默认内容 | 生成要求 |
52
+ | --- | --- | --- |
53
+ | 状态层 | `loading`、`list/tableData`、`currentPage`、`pageSize`、`totalCount`、`filters/searchFieldJson`、`selectedRowKeys`、`dialogVisible` | 放入 `_customState`,所有失败路径必须恢复 `loading: false` |
54
+ | 数据源层 | 表单查询、保存、更新、删除、流程发起、任务列表、连接器动作 | 优先调用已有 `this.dataSourceMap.<name>.load()`;没有数据源且是宜搭内置数据时用 `this.utils.yida.*`;第三方/连接器必须先走 `yida-data-source-connectors` |
55
+ | 交互层 | 筛选栏、表格/卡片列表、分页、弹窗、Tab/Collapse、操作按钮 | `renderJsx` 只负责展示和事件分发,业务逻辑拆成 `export function` |
56
+
57
+ 默认页面结构按官方高频组件范式转译为 JSX:顶部筛选/操作区、主体表格或卡片列表、分页、详情/编辑弹窗、空态/错误态。不要把数据查询、复杂计算和大段 DOM 混在一个 `renderJsx` 里。
58
+
59
+ 如果用户的需求实际是字段公式、字段联动、原生报表、审批规则或集成自动化,先切换到对应技能;自定义页面只在需要跨数据展示、工具页交互、可视化看板或连接器调用界面时承担前端层。
60
+
47
61
  ## 适用场景
48
62
 
49
63
  **正向触发**:
@@ -456,6 +470,7 @@ openyida check-page pages/src/home.oyd.jsx --json # 输出机器可读的
456
470
  | [编码指南](references/coding-guide.md) | 文件结构模板、状态管理、生命周期、19 条编码规范 | 编写任何页面代码前必读 |
457
471
  | [设计规范](references/design-system.md) | 色彩/圆角/字体/间距系统、7 类组件样式模板、8 条反模式 | 实现 UI 样式时必读 |
458
472
  | [素材资源](references/assets-guide.md) | 图片/音乐/Icon 素材库、CDN 安全规范 | 需要引入图片、图标、音效时阅读 |
473
+ | [官方示例中心 Schema 范式](../../references/official-example-schema-patterns.md) | 示例中心 156 个 capacity 模板的 schema 抽取链路、类型分布、数据源/报表/连接器模式 | 用户要求参考官方示例、蒸馏模板能力、或实现列表/模板中心/数据源驱动页面时阅读 |
459
474
  | **全局共享文档** | | |
460
475
  | [宜搭 API](../../references/yida-api.md) | 表单/流程/工具 API 完整参数文档 | 调用 `this.utils.yida.*` 前必读 |
461
476
  | [大模型 API](../../references/model-api.md) | AI 文本生成接口参数 | 调用 `txtFromAI` 前必读 |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yida-data-management
3
- description: 宜搭数据管理。表单实例/流程实例/任务中心的查询、新增、更新。表单走 /v1/form/,流程走 /v1/process/,不能混用。
3
+ description: 宜搭数据管理。表单实例/子表/流程实例/任务中心的查询、新增、更新。表单走 /v1/form/,流程走 /v1/process/,不能混用。
4
4
  ---
5
5
 
6
6
  # 数据管理
@@ -20,6 +20,7 @@ description: 宜搭数据管理。表单实例/流程实例/任务中心的查
20
20
  - **录入/更新数据前,必须先执行 `openyida get-schema` 获取真实字段 ID,并将字段 ID 映射记录到 `.cache/<项目名>-schema.json`**
21
21
  - **生成测试数据或录入/更新数据时,`DateField` / `CascadeDateField` 必须使用 13 位毫秒时间戳(如 `1719705600000`),不要传 `YYYY-MM-DD`、`YYYY-MM-DD HH:mm:ss` 或 ISO 字符串**
22
22
  - **录入数据后,必须执行 `openyida data query` 抽查至少 1 条记录,确认 `formData` 中字段有实际值(非空),否则说明字段 ID 有误,需重新排查**
23
+ - **读取子表明细超过 50 行时,必须使用 `openyida data query subform` 或 `listTableDataByFormInstIdAndTableId` 分页查询完整子表;不要把 `searchFormDatas.currentPage` 当作子表分页**
23
24
  - **本技能不读写 memory**:数据操作通过 CLI 命令写入宜搭平台,不依赖跨会话的 memory 状态
24
25
  - 一次性造数、旧数据修正、字段迁移脚本可以使用 Python 或 JS,优先选择更快更清晰的实现;脚本、导入数据、查询条件文件必须放在 `.cache/openyida/` 下,并复用真实查询到的 appType/formUuid/fieldId/formInstId
25
26
  - **禁止在仓库根目录生成导入用的 `*.json`、`*.js`、`*.py`、`*.csv` 临时文件**;推荐使用 `.cache/openyida/data-import/` 存放数据文件,`.cache/openyida/scripts/` 存放一次性执行脚本
@@ -31,6 +32,7 @@ description: 宜搭数据管理。表单实例/流程实例/任务中心的查
31
32
  **关键区分**:
32
33
  - 操作表单数据记录(增删改查)→ 本技能
33
34
  - 修改表单结构(字段增删改)→ `yida-create-form-page`
35
+ - 自定义页面调用连接器或外部 API 数据源 → `yida-data-source-connectors`
34
36
  - 表单接口(`/v1/form/`)vs 流程接口(`/v1/process/`)不能混用
35
37
 
36
38
  ## 触发条件
@@ -43,6 +45,7 @@ description: 宜搭数据管理。表单实例/流程实例/任务中心的查
43
45
  **不适用场景(不要触发)**:
44
46
  - 修改表单结构(字段增删改)→ `yida-create-form-page`
45
47
  - 配置集成自动化 → `yida-integration`
48
+ - 自定义页面接入 HTTP 连接器、第三方接口或外部系统数据 → `yida-data-source-connectors`
46
49
  - 获取字段 ID → `yida-get-schema`
47
50
  - 表单接口(`/v1/form/`)和流程接口(`/v1/process/`)不能混用
48
51
 
@@ -69,11 +72,26 @@ openyida data create form <appType> <formUuid> --data-json '<json>' [--resolve-a
69
72
  openyida data create form <appType> <formUuid> --data-file .cache/openyida/data-import/record.json [--resolve-aliases]
70
73
  openyida data update form <appType> --inst-id <formInstId> --form-uuid <formUuid> --data-json '<json>' [--resolve-aliases]
71
74
  openyida data update form <appType> --inst-id <formInstId> --form-uuid <formUuid> --data-file .cache/openyida/data-import/patch.json [--resolve-aliases]
72
- openyida data query subform <appType> <formUuid> --inst-id <formInstId> --table-field-id <fieldId|alias> [--resolve-aliases]
75
+ openyida data query subform <appType> <formUuid> --inst-id <formInstId> --table-field-id <fieldId|alias> [--page 1 --size 100] [--resolve-aliases]
73
76
  ```
74
77
 
75
78
  当 JSON 使用宜搭组件别名作为 key 时,追加 `--resolve-aliases`,OpenYida 会先读取表单 Schema 中的 `componentAlias.items`,再将别名转换为真实 `fieldId` 后调用数据接口。更新类命令若要解析别名,必须额外传 `--form-uuid <formUuid>`。
76
79
 
80
+ ### 子表超过 50 行
81
+
82
+ 当 `searchFormDatas` 或 `getFormDataById` 返回的 `formData.tableField_xxx` 刚好是 50 行时,优先判断为详情接口对子表内嵌数据做了截断,不要直接下结论为"没有更多数据"。
83
+
84
+ 正确处理流程:
85
+
86
+ 1. 先确认主记录的 `formInstId` 和子表真实 `tableFieldId`;如果只有别名,先执行 `openyida get-schema` 或给 `query subform` 追加 `--resolve-aliases`。
87
+ 2. 使用 `openyida data query subform <appType> <formUuid> --inst-id <formInstId> --table-field-id <tableFieldId> --page 1 --size 100` 查询子表第一页。
88
+ 3. 如果返回 `totalCount` 大于 `data.length`,继续按 `currentPage/pageSize` 翻页,或在脚本中复用 `listTableDataByFormInstIdAndTableId` 拉全量。
89
+ 4. 不要通过 DOM、虚拟滚动列表、页面全局变量抓取子表全量数据;这类方式依赖页面实现,不能作为稳定数据管理方案。
90
+
91
+ 注意:`searchFormDatas.currentPage` 分页的是主表实例列表,不是某条实例里的子表行。把 `currentPage` 改为 2 后返回空,只能说明主表第二页没有记录,不能证明子表不支持分页。
92
+
93
+ 不要把"创建自定义数据源"作为解决宜搭表单/子表 50 行截断的首选方案;只有在确实需要调用 HTTP 连接器、第三方接口或外部系统数据时,才切换到 `yida-data-source-connectors`。
94
+
77
95
  ### 流程实例
78
96
 
79
97
  ```bash
@@ -209,6 +227,7 @@ openyida sample yida-data-management form-field-template # 表单字段定义
209
227
  | 异常场景 | 处理方式 |
210
228
  |---------|----------|
211
229
  | 查询返回空结果 | 确认 formUuid 正确,检查查询条件是否过于严格 |
230
+ | 子表只返回 50 行 | 不要翻 `searchFormDatas.currentPage`;使用 `query subform` / `listTableDataByFormInstIdAndTableId` 按 `formInstId + tableFieldId` 分页查询 |
212
231
  | 新增数据后字段值为空 | 字段 ID 有误,先执行 `openyida get-schema` 获取真实 fieldId |
213
232
  | 更新失败(formInstId 不存在) | 先用 query 命令确认记录存在,不要猜测 formInstId |
214
233
  | 接口返回 401/未登录 | 执行 `openyida login` 重新登录 |
@@ -11,6 +11,8 @@ description: 宜搭自定义页面连接器/远程 API 数据源接入规范。
11
11
 
12
12
  禁止在自定义页面里直接用 `fetch`、`XMLHttpRequest`、`/query/newconnector/testConnector.json`、`ConnectorFactory.testConnector` 或手写远程 URL 绕过设计器数据源。例外只允许用于一次性本地诊断,不得发布到正式页面 Schema。
13
13
 
14
+ 官方示例中心的 schema 回读规律见 [官方示例中心 Schema 范式](../../references/official-example-schema-patterns.md)。其中连接器调用有时会被平台归一为 `REMOTE + /query/publicService/invokeService.json + serviceInfo.connectorInfo`,因此验收时既看设计器数据源是否存在,也要识别这种归一形态。
15
+
14
16
  ## 适用场景
15
17
 
16
18
  - 自定义页面、Dashboard、数据大屏读取外部系统接口。
@@ -18,10 +20,17 @@ description: 宜搭自定义页面连接器/远程 API 数据源接入规范。
18
20
  - 用户要求“把连接器操作添加到页面数据源”“左侧数据源里要能看到连接器”“远程 API 不要写死在 JSX 里”。
19
21
  - 修复页面一直卡在“加载中”,且原因是代码绕过数据源直接请求连接器或外部域名。
20
22
 
23
+ ## 不适用场景
24
+
25
+ - 读取宜搭表单、流程、任务或子表数据 → 使用 `yida-data-management`。
26
+ - 子表内嵌明细只返回 50 行 → 使用 `openyida data query subform` / `listTableDataByFormInstIdAndTableId` 按 `formInstId + tableFieldId` 分页查询,不要为此新建连接器数据源。
27
+ - 修改表单或页面结构 → 使用 `yida-create-form-page` / `yida-custom-page`。
28
+
21
29
  ## 与其他技能的分工
22
30
 
23
31
  - 创建 HTTP 连接器本体、账号、动作列表:使用 `yida-connector`。
24
32
  - 从 API 文件或后端 Controller 生成安全动作 JSON:使用 `yida-connector-safe-actions`。
33
+ - 查询表单、流程、任务或子表数据:使用 `yida-data-management`。
25
34
  - 编写自定义页面 UI 和生命周期:使用 `yida-custom-page`。
26
35
  - 发布页面:使用 `yida-publish-page`,发布后确认数据源仍被保留或补回。
27
36
 
@@ -42,6 +51,16 @@ openyida connector list-actions <connector-id>
42
51
  - `tricolorGetUserDtuSns`
43
52
  - `tricolorGetDtuSnStateList`
44
53
 
54
+ 官方示例里高频数据源类别包括表单查询、任务列表、流程发起、保存/更新/删除、连接器动作。OpenYida 生成时应把每个远程能力设计为独立数据源,并为页面状态保留明确字段:
55
+
56
+ | 能力 | 数据源命名建议 | 状态字段建议 |
57
+ | --- | --- | --- |
58
+ | 查询列表 | `get<Biz>List` | `loading`、`tableData/list`、`currentPage`、`pageSize`、`totalCount`、`filters/searchFieldJson` |
59
+ | 查询详情 | `get<Biz>ById` | `detailLoading`、`currentRecord` |
60
+ | 保存/更新 | `save<Biz>` / `update<Biz>` | `submitting`、`dialogVisible` |
61
+ | 删除/批量删除 | `delete<Biz>` / `batchDelete<Biz>` | `selectedRowKeys`、`deleting` |
62
+ | 连接器动作 | `<service><Action>` | 与动作结果同名的 `result` / `rawResult` |
63
+
45
64
  3. 在页面 Schema 的 Page 根节点 `dataSource.online` 中登记连接器数据源。
46
65
 
47
66
  数据源必须满足:
@@ -53,6 +72,15 @@ openyida connector list-actions <connector-id>
53
72
  - `options.connectorAction.value` 使用动作 `operationId`
54
73
  - `options.params.inputs` 包含 `Headers`、`Query`、`Body`
55
74
  - `options.shouldFetch: false`,由页面代码按需触发
75
+ - `options.didFetch` 必须返回处理后的 content;返回结构不稳定时做归一化
76
+ - `options.onError` 必须 toast 具体数据源/动作名,并让页面加载态恢复
77
+
78
+ 发布后回读 schema 时,平台可能把连接器数据源归一成以下只读形态;这是可接受的,但不要在源码里写死 `_csrf_token`:
79
+
80
+ - `dpType: "REMOTE"` / `protocal: "REMOTE"`
81
+ - `options.url` 为 `/query/publicService/invokeService.json?...`
82
+ - `options.params.serviceInfo` 内含 `connectorInfo.connectorId`、`actionId`、`type`、`connection`
83
+ - `requestHandler.value` 仍是 `this.utils.legaoBuiltin.dataSourceHandler`
56
84
 
57
85
  4. 页面代码只调用数据源。
58
86
 
@@ -90,6 +118,7 @@ openyida get-schema <appType> <formUuid> > .cache/openyida/<page>-schema.json
90
118
  检查点:
91
119
 
92
120
  - 设计器左侧“数据源”能看到新增连接器数据源。
121
+ - `dataSource.online` 中能看到显式连接器数据源,或回读为 `REMOTE + publicService/invokeService + serviceInfo.connectorInfo` 的平台归一形态。
93
122
  - `actions.module.source` 中没有 `ConnectorFactory.testConnector`、`newconnector/testConnector`、外部 API 域名直连代码。
94
123
  - 页面运行时使用 `this.dataSourceMap.<name>.load()`。
95
124
 
@@ -41,6 +41,8 @@ description: 宜搭表单公式编写规范,包含函数速查、语法规则
41
41
 
42
42
  > 完整函数列表参见 `../../references/formula-functions.md`
43
43
 
44
+ 官方示例中心的公式类示例基本落在普通表单或流程表单 schema 中,而不是自定义页面。遇到“日期转换、流水号、登录人、主管、相隔时间、求和”等能力时,默认先配置字段公式或字段联动;只有公式无法表达、且需要跨表提交后动作时,再切到 `yida-integration` 或 `yida-business-rule`。
45
+
44
46
  ---
45
47
 
46
48
  ## ⚠️ 重要:公式配置方式说明
@@ -16,6 +16,7 @@ description: 宜搭集成&自动化配置技能。支持创建/查询/开启/关
16
16
  - **创建/发布前必须确认**:执行集成自动化创建或发布操作前,必须向用户展示逻辑流配置摘要(触发条件、节点列表、通知对象),获得用户明确同意后再执行
17
17
  - 创建前先确认触发表单的 formUuid 和相关字段 ID
18
18
  - 创建成功后记录逻辑流 ID 到 `.cache/<项目名>-schema.json`
19
+ - 参考官方示例时不要只看默认页面 schema:集成自动化示例的默认页通常只是触发表单或说明页,逻辑流本体需要通过集成自动化接口/命令查询或创建
19
20
 
20
21
  ## 适用场景
21
22
 
@@ -53,6 +54,8 @@ description: 宜搭集成&自动化配置技能。支持创建/查询/开启/关
53
54
 
54
55
  本技能用于在宜搭平台创建「集成&自动化」(逻辑流),支持场景:**表单事件触发 → 多节点组合处理 → 钉钉工作通知 / 数据操作**。
55
56
 
57
+ 官方示例中心体现的集成范式是“表单收集数据,逻辑流处理副作用”。因此,跨表新增/更新、通知、创建待办、调用钉钉能力等提交后动作,默认用本技能;不要把这些副作用塞进表单字段 JS 或自定义页面按钮,除非用户明确要求一次性人工触发工具页。
58
+
56
59
  ## 功能概述
57
60
 
58
61
  - 监听指定表单的新增 / 更新 / 删除 / 评论事件
@@ -16,6 +16,7 @@ description: "宜搭原生报表技能,用于创建宜搭平台内置的原生
16
16
 
17
17
  - **创建/发布前必须确认**:执行报表创建或发布操作前,必须向用户展示报表配置摘要(图表类型、数据源、字段映射),获得用户明确同意后再执行
18
18
  - 普通"报表"、"统计"需求默认使用本技能,不要直接跳到 `yida-chart`
19
+ - 参考官方示例时先确认 schema 证据:只有 `formType: "report"` 或组件树出现 `Youshu*` 报表组件时才按原生报表处理;`report` 标签但默认页是 `receipt` 或自定义页时,先判断是否只是数据准备页或看板页
19
20
  - 调用报表数据 API 前必须确认 `reportId` 和 `datasetId` 来自真实报表
20
21
  - 解析报表数据时必须处理 `data.rows` 为空的情况,避免页面崩溃
21
22
  - 报表配置 JSON 需要落盘时,必须写入 `.cache/openyida/<项目名>/`,例如 `.cache/openyida/pm/pm-report-team.json`;不要在仓库根目录生成 `*-report*.json`
@@ -69,6 +70,8 @@ description: "宜搭原生报表技能,用于创建宜搭平台内置的原生
69
70
  ECharts 自定义页面(前端渲染)
70
71
  ```
71
72
 
73
+ 官方示例中心的报表范式是“原生报表先聚合,自定义页面后增强”。因此,除非用户明确要做高级视觉看板,否则先创建或复用原生报表;只有在已有报表提供聚合数据后,再让 `yida-chart` 或 `yida-custom-page` 承担展示层。
74
+
72
75
  **为什么不用 `searchFormDatas` 前端聚合?**
73
76
 
74
77
  | 对比项 | `searchFormDatas` 前端聚合 | 原生报表 API |