openyida 2026.9.13 → 2026.9.14
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/lib/app/canvas-compile.js +37 -4
- package/lib/app/display-page-readback.js +63 -5
- package/lib/app/get-schema.js +158 -5
- package/lib/app/publish.js +127 -5
- package/lib/core/command-manifest.js +2 -2
- package/lib/core/locales/en.js +9 -0
- package/lib/core/locales/zh.js +9 -0
- package/lib/core/utils.js +358 -0
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +3 -1
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +2 -2
- package/yida-skills/skills/yida-create-form-page/SKILL.md +1 -0
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +40 -6
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +1 -1
- package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
- package/yida-skills/skills/yida-integration/SKILL.md +1 -0
- package/yida-skills/skills/yida-nav-shell/SKILL.md +4 -4
- package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
- package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +2 -2
- package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +13 -16
package/lib/core/locales/en.js
CHANGED
|
@@ -915,6 +915,15 @@ Examples:
|
|
|
915
915
|
response_body: ' Response body: {0}',
|
|
916
916
|
response_detail: ' Response detail: {0}',
|
|
917
917
|
response_not_json: 'response is not JSON',
|
|
918
|
+
code_bundle_download_failed: 'CodeBundle download failed: {0} ({1})',
|
|
919
|
+
code_bundle_forbidden: 'permission verification or OSS access was denied',
|
|
920
|
+
code_bundle_not_found: 'the file does not exist or has been removed',
|
|
921
|
+
code_bundle_http_error: 'HTTP {0}',
|
|
922
|
+
code_bundle_html_response: 'received an HTML error page; the environment or route may be incorrect',
|
|
923
|
+
code_bundle_json_response: 'the service returned a JSON error response: {0}',
|
|
924
|
+
code_bundle_unexpected_content_type: 'unexpected response Content-Type: {0}',
|
|
925
|
+
code_bundle_invalid_utf8: 'the response is not valid UTF-8 text',
|
|
926
|
+
code_bundle_integrity_failed: 'CodeBundle {0} integrity verification failed ({1})',
|
|
918
927
|
login_expired: ' Login session expired: {0}',
|
|
919
928
|
csrf_expired: ' CSRF token expired: {0}',
|
|
920
929
|
csrf_refreshed: ' csrf_token refreshed',
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -886,6 +886,15 @@ openyida - 宜搭命令行工具
|
|
|
886
886
|
response_body: ' 响应内容: {0}',
|
|
887
887
|
response_detail: ' 响应详情: {0}',
|
|
888
888
|
response_not_json: '响应非 JSON',
|
|
889
|
+
code_bundle_download_failed: 'CodeBundle 下载失败:{0}({1})',
|
|
890
|
+
code_bundle_forbidden: '权限校验或 OSS 访问被拒绝',
|
|
891
|
+
code_bundle_not_found: '文件不存在或已被清理',
|
|
892
|
+
code_bundle_http_error: 'HTTP {0}',
|
|
893
|
+
code_bundle_html_response: '返回了 HTML 错误页,可能是环境或路由错误',
|
|
894
|
+
code_bundle_json_response: '服务端返回 JSON 错误响应:{0}',
|
|
895
|
+
code_bundle_unexpected_content_type: '响应 Content-Type 不符合预期:{0}',
|
|
896
|
+
code_bundle_invalid_utf8: '响应内容不是有效 UTF-8 文本',
|
|
897
|
+
code_bundle_integrity_failed: 'CodeBundle {0} 完整性校验失败({1})',
|
|
889
898
|
login_expired: ' 检测到登录过期: {0}',
|
|
890
899
|
csrf_expired: ' 检测到 csrf_token 过期: {0}',
|
|
891
900
|
csrf_refreshed: ' csrf_token 已刷新',
|
package/lib/core/utils.js
CHANGED
|
@@ -1080,6 +1080,362 @@ function createNonJsonResponseResult(statusCode, data) {
|
|
|
1080
1080
|
};
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
|
+
function buildRequestUrl(baseUrl, requestPath, queryParams) {
|
|
1084
|
+
const normalizedBaseUrl = String(baseUrl || '').replace(/\/+$/, '') + '/';
|
|
1085
|
+
const requestUrl = new URL(requestPath, normalizedBaseUrl);
|
|
1086
|
+
if (queryParams) {
|
|
1087
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
1088
|
+
if (value !== undefined && value !== null) {
|
|
1089
|
+
requestUrl.searchParams.set(key, String(value));
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
return requestUrl;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function fetchWithTimeout(requestUrl, requestOptions, timeout, readResponse) {
|
|
1097
|
+
const controller = new AbortController();
|
|
1098
|
+
const timer = setTimeout(() => controller.abort(), timeout || 30000);
|
|
1099
|
+
try {
|
|
1100
|
+
const response = await fetch(requestUrl, {
|
|
1101
|
+
...requestOptions,
|
|
1102
|
+
signal: controller.signal,
|
|
1103
|
+
});
|
|
1104
|
+
return readResponse ? await readResponse(response) : response;
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
if (error && error.name === 'AbortError') {
|
|
1107
|
+
throw new Error(t('common.request_timeout'));
|
|
1108
|
+
}
|
|
1109
|
+
throw error;
|
|
1110
|
+
} finally {
|
|
1111
|
+
clearTimeout(timer);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function parseJsonFetchResponse(response, options = {}) {
|
|
1116
|
+
const data = await response.text();
|
|
1117
|
+
if (!options.silentStatus) {
|
|
1118
|
+
warn(t('common.http_status', response.status));
|
|
1119
|
+
}
|
|
1120
|
+
if (isHttpRedirectStatus(response.status) || isHttpAuthStatus(response.status)) {
|
|
1121
|
+
return {
|
|
1122
|
+
__needLogin: true,
|
|
1123
|
+
__httpStatus: response.status,
|
|
1124
|
+
__location: response.headers.get('location') || '',
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
const parsed = JSON.parse(data);
|
|
1129
|
+
if (isLoginExpired(parsed)) {
|
|
1130
|
+
return { __needLogin: true };
|
|
1131
|
+
}
|
|
1132
|
+
if (isCsrfTokenExpired(parsed)) {
|
|
1133
|
+
return { __csrfExpired: true };
|
|
1134
|
+
}
|
|
1135
|
+
return parsed;
|
|
1136
|
+
} catch {
|
|
1137
|
+
if (!options.silentStatus) {
|
|
1138
|
+
warn(t('common.http_response', data.substring(0, 500)));
|
|
1139
|
+
}
|
|
1140
|
+
return createNonJsonResponseResult(response.status, data);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function appendMultipartFile(form, fieldName, file) {
|
|
1145
|
+
if (!file || file.content === undefined || file.content === null) {
|
|
1146
|
+
throw new TypeError(`multipart file ${fieldName} is required`);
|
|
1147
|
+
}
|
|
1148
|
+
const content = Buffer.isBuffer(file.content)
|
|
1149
|
+
? file.content
|
|
1150
|
+
: Buffer.from(String(file.content), 'utf8');
|
|
1151
|
+
form.append(
|
|
1152
|
+
fieldName,
|
|
1153
|
+
new Blob([content], { type: file.contentType || 'application/octet-stream' }),
|
|
1154
|
+
file.fileName || fieldName
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* 发送 multipart/form-data POST。Content-Type boundary 由 Node.js FormData 生成。
|
|
1160
|
+
*/
|
|
1161
|
+
async function httpPostMultipart(baseUrl, requestPath, fields, files, optionsOrLegacyCookies, maybeOptions) {
|
|
1162
|
+
if (typeof fetch !== 'function' || typeof FormData !== 'function' || typeof Blob !== 'function') {
|
|
1163
|
+
throw new Error('当前 Node.js 环境缺少 fetch/FormData/Blob,请使用 Node.js 18+');
|
|
1164
|
+
}
|
|
1165
|
+
const optionsOverride = resolveRequestOptions(optionsOrLegacyCookies, maybeOptions);
|
|
1166
|
+
const authHeaders = await resolveRequestAuthHeaders(optionsOverride);
|
|
1167
|
+
const form = new FormData();
|
|
1168
|
+
Object.entries(fields || {}).forEach(([key, value]) => {
|
|
1169
|
+
if (value !== undefined && value !== null) {
|
|
1170
|
+
form.append(key, String(value));
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
Object.entries(files || {}).forEach(([key, file]) => appendMultipartFile(form, key, file));
|
|
1174
|
+
|
|
1175
|
+
const requestUrl = buildRequestUrl(baseUrl, requestPath);
|
|
1176
|
+
return fetchWithTimeout(requestUrl, {
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
redirect: 'manual',
|
|
1179
|
+
headers: {
|
|
1180
|
+
Accept: 'application/json, text/plain, */*',
|
|
1181
|
+
Origin: requestUrl.origin,
|
|
1182
|
+
Referer: optionsOverride.referer || requestUrl.origin + '/',
|
|
1183
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
1184
|
+
...authHeaders,
|
|
1185
|
+
},
|
|
1186
|
+
body: form,
|
|
1187
|
+
}, optionsOverride.timeout, response => parseJsonFetchResponse(response, optionsOverride));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async function readFetchResponseBuffer(response, maxBytes, timeout) {
|
|
1191
|
+
const limit = Number(maxBytes);
|
|
1192
|
+
const timeoutMs = Number(timeout) || 30000;
|
|
1193
|
+
const contentLength = Number(response.headers.get('content-length') || 0);
|
|
1194
|
+
if (Number.isFinite(limit) && limit > 0 && contentLength > limit) {
|
|
1195
|
+
const error = new Error(`响应内容超过允许大小 ${limit} bytes`);
|
|
1196
|
+
error.code = 'HTTP_RESPONSE_TOO_LARGE';
|
|
1197
|
+
throw error;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
1201
|
+
let timer;
|
|
1202
|
+
const arrayBuffer = await Promise.race([
|
|
1203
|
+
response.arrayBuffer(),
|
|
1204
|
+
new Promise((resolve, reject) => {
|
|
1205
|
+
void resolve;
|
|
1206
|
+
timer = setTimeout(() => reject(new Error(t('common.request_timeout'))), timeoutMs);
|
|
1207
|
+
}),
|
|
1208
|
+
]).finally(() => clearTimeout(timer));
|
|
1209
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
1210
|
+
if (Number.isFinite(limit) && limit > 0 && buffer.length > limit) {
|
|
1211
|
+
const error = new Error(`响应内容超过允许大小 ${limit} bytes`);
|
|
1212
|
+
error.code = 'HTTP_RESPONSE_TOO_LARGE';
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
return buffer;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const chunks = [];
|
|
1219
|
+
let size = 0;
|
|
1220
|
+
const reader = response.body.getReader();
|
|
1221
|
+
let timedOut = false;
|
|
1222
|
+
const timer = setTimeout(() => {
|
|
1223
|
+
timedOut = true;
|
|
1224
|
+
void reader.cancel();
|
|
1225
|
+
}, timeoutMs);
|
|
1226
|
+
try {
|
|
1227
|
+
for (;;) {
|
|
1228
|
+
const { done, value } = await reader.read();
|
|
1229
|
+
if (done) {
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
const chunk = Buffer.from(value);
|
|
1233
|
+
size += chunk.length;
|
|
1234
|
+
if (Number.isFinite(limit) && limit > 0 && size > limit) {
|
|
1235
|
+
await reader.cancel();
|
|
1236
|
+
const error = new Error(`响应内容超过允许大小 ${limit} bytes`);
|
|
1237
|
+
error.code = 'HTTP_RESPONSE_TOO_LARGE';
|
|
1238
|
+
throw error;
|
|
1239
|
+
}
|
|
1240
|
+
chunks.push(chunk);
|
|
1241
|
+
}
|
|
1242
|
+
if (timedOut) {
|
|
1243
|
+
throw new Error(t('common.request_timeout'));
|
|
1244
|
+
}
|
|
1245
|
+
} finally {
|
|
1246
|
+
clearTimeout(timer);
|
|
1247
|
+
reader.releaseLock();
|
|
1248
|
+
}
|
|
1249
|
+
return Buffer.concat(chunks, size);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function normalizeResponseContentType(value) {
|
|
1253
|
+
return String(value || '').split(';')[0].trim().toLowerCase();
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function responseRequestId(response) {
|
|
1257
|
+
return response.headers.get('x-request-id') || '';
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function buildCodeBundleResponseMetadata(requestUrl, response) {
|
|
1261
|
+
const metadata = {
|
|
1262
|
+
baseUrl: requestUrl.origin,
|
|
1263
|
+
finalHost: requestUrl.host,
|
|
1264
|
+
status: response.status,
|
|
1265
|
+
contentType: normalizeResponseContentType(response.headers.get('content-type')),
|
|
1266
|
+
eagleeyeTraceId: response.headers.get('eagleeye-traceid') || '',
|
|
1267
|
+
requestId: responseRequestId(response),
|
|
1268
|
+
};
|
|
1269
|
+
metadata.context = [
|
|
1270
|
+
`baseUrl=${metadata.baseUrl}`,
|
|
1271
|
+
`finalHost=${metadata.finalHost}`,
|
|
1272
|
+
`status=${metadata.status}`,
|
|
1273
|
+
`contentType=${metadata.contentType || 'missing'}`,
|
|
1274
|
+
metadata.eagleeyeTraceId ? `eagleeyeTraceId=${metadata.eagleeyeTraceId}` : '',
|
|
1275
|
+
metadata.requestId ? `requestId=${metadata.requestId}` : '',
|
|
1276
|
+
].filter(Boolean).join(', ');
|
|
1277
|
+
return metadata;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function createCodeBundleDownloadError(code, reason, metadata, responsePreview) {
|
|
1281
|
+
const suffix = responsePreview ? `; response=${responsePreview}` : '';
|
|
1282
|
+
const error = new Error(t('common.code_bundle_download_failed', reason, metadata.context) + suffix);
|
|
1283
|
+
error.code = code;
|
|
1284
|
+
error.details = {
|
|
1285
|
+
...metadata,
|
|
1286
|
+
responsePreview: responsePreview || undefined,
|
|
1287
|
+
};
|
|
1288
|
+
return error;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function compactResponsePreview(buffer) {
|
|
1292
|
+
return buffer.toString('utf8').replace(/\s+/g, ' ').trim().substring(0, 500);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async function readErrorResponsePreview(response, timeout) {
|
|
1296
|
+
try {
|
|
1297
|
+
return compactResponsePreview(await readFetchResponseBuffer(response, 4096, timeout));
|
|
1298
|
+
} catch {
|
|
1299
|
+
return '';
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function looksLikeHtml(buffer) {
|
|
1304
|
+
const prefix = buffer.toString('utf8', 0, Math.min(buffer.length, 256)).trimStart().toLowerCase();
|
|
1305
|
+
return prefix.startsWith('<!doctype html')
|
|
1306
|
+
|| prefix.startsWith('<html')
|
|
1307
|
+
|| prefix.startsWith('<head')
|
|
1308
|
+
|| prefix.startsWith('<body');
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function expectedContentTypeMatches(actualContentType, expectedContentTypes) {
|
|
1312
|
+
if (!Array.isArray(expectedContentTypes) || expectedContentTypes.length === 0) {
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
return expectedContentTypes.some(value => normalizeResponseContentType(value) === actualContentType);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function jsonResponseReason(preview) {
|
|
1319
|
+
if (!preview) {
|
|
1320
|
+
return t('common.code_bundle_json_response', t('common.unknown_error'));
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
const payload = JSON.parse(preview);
|
|
1324
|
+
const message = payload && (payload.errorMsg || payload.message || payload.throwable || payload.errorCode);
|
|
1325
|
+
return t('common.code_bundle_json_response', message || t('common.unknown_error'));
|
|
1326
|
+
} catch {
|
|
1327
|
+
return t('common.code_bundle_json_response', t('common.unknown_error'));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* 从宜搭首方接口直接获取 CodeBundle 文本。
|
|
1333
|
+
*/
|
|
1334
|
+
async function httpGetCodeBundleText(baseUrl, requestPath, queryParams, optionsOrLegacyCookies, maybeOptions) {
|
|
1335
|
+
if (typeof fetch !== 'function') {
|
|
1336
|
+
throw new Error('当前 Node.js 环境缺少 fetch,请使用 Node.js 18+');
|
|
1337
|
+
}
|
|
1338
|
+
const optionsOverride = resolveRequestOptions(optionsOrLegacyCookies, maybeOptions);
|
|
1339
|
+
const authHeaders = await resolveRequestAuthHeaders(optionsOverride);
|
|
1340
|
+
const requestUrl = buildRequestUrl(baseUrl, requestPath, queryParams);
|
|
1341
|
+
const maxBytes = Number(optionsOverride.maxBytes) || 5 * 1024 * 1024;
|
|
1342
|
+
const response = await fetchWithTimeout(requestUrl, {
|
|
1343
|
+
method: 'GET',
|
|
1344
|
+
redirect: 'manual',
|
|
1345
|
+
headers: {
|
|
1346
|
+
Accept: 'text/plain, application/javascript, */*',
|
|
1347
|
+
Origin: requestUrl.origin,
|
|
1348
|
+
Referer: requestUrl.origin + '/',
|
|
1349
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
1350
|
+
...authHeaders,
|
|
1351
|
+
},
|
|
1352
|
+
}, optionsOverride.timeout);
|
|
1353
|
+
const metadata = buildCodeBundleResponseMetadata(requestUrl, response);
|
|
1354
|
+
|
|
1355
|
+
if (response.status === 401) {
|
|
1356
|
+
return { __needLogin: true, __httpStatus: response.status };
|
|
1357
|
+
}
|
|
1358
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1359
|
+
const preview = await readErrorResponsePreview(response, optionsOverride.timeout);
|
|
1360
|
+
if (response.status === 403) {
|
|
1361
|
+
throw createCodeBundleDownloadError(
|
|
1362
|
+
'CODE_BUNDLE_DOWNLOAD_FORBIDDEN',
|
|
1363
|
+
t('common.code_bundle_forbidden'),
|
|
1364
|
+
metadata,
|
|
1365
|
+
preview
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
if (response.status === 404) {
|
|
1369
|
+
throw createCodeBundleDownloadError(
|
|
1370
|
+
'CODE_BUNDLE_DOWNLOAD_NOT_FOUND',
|
|
1371
|
+
t('common.code_bundle_not_found'),
|
|
1372
|
+
metadata,
|
|
1373
|
+
preview
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
throw createCodeBundleDownloadError(
|
|
1377
|
+
'CODE_BUNDLE_DOWNLOAD_HTTP_ERROR',
|
|
1378
|
+
t('common.code_bundle_http_error', response.status),
|
|
1379
|
+
metadata,
|
|
1380
|
+
preview
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const contentType = metadata.contentType;
|
|
1385
|
+
const htmlResponse = contentType === 'text/html' || contentType === 'application/xhtml+xml';
|
|
1386
|
+
const jsonResponse = contentType === 'application/json' || contentType.endsWith('+json');
|
|
1387
|
+
if (htmlResponse || jsonResponse
|
|
1388
|
+
|| !expectedContentTypeMatches(contentType, optionsOverride.expectedContentTypes)) {
|
|
1389
|
+
const preview = await readErrorResponsePreview(response, optionsOverride.timeout);
|
|
1390
|
+
if (htmlResponse) {
|
|
1391
|
+
throw createCodeBundleDownloadError(
|
|
1392
|
+
'CODE_BUNDLE_DOWNLOAD_HTML_RESPONSE',
|
|
1393
|
+
t('common.code_bundle_html_response'),
|
|
1394
|
+
metadata,
|
|
1395
|
+
preview
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (jsonResponse) {
|
|
1399
|
+
throw createCodeBundleDownloadError(
|
|
1400
|
+
'CODE_BUNDLE_DOWNLOAD_JSON_RESPONSE',
|
|
1401
|
+
jsonResponseReason(preview),
|
|
1402
|
+
metadata,
|
|
1403
|
+
preview
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
throw createCodeBundleDownloadError(
|
|
1407
|
+
'CODE_BUNDLE_DOWNLOAD_UNEXPECTED_CONTENT_TYPE',
|
|
1408
|
+
t('common.code_bundle_unexpected_content_type', contentType || 'missing'),
|
|
1409
|
+
metadata,
|
|
1410
|
+
preview
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const buffer = await readFetchResponseBuffer(response, maxBytes, optionsOverride.timeout);
|
|
1415
|
+
if (looksLikeHtml(buffer)) {
|
|
1416
|
+
throw createCodeBundleDownloadError(
|
|
1417
|
+
'CODE_BUNDLE_DOWNLOAD_HTML_RESPONSE',
|
|
1418
|
+
t('common.code_bundle_html_response'),
|
|
1419
|
+
metadata,
|
|
1420
|
+
compactResponsePreview(buffer)
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
let value;
|
|
1424
|
+
try {
|
|
1425
|
+
value = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
1426
|
+
} catch {
|
|
1427
|
+
throw createCodeBundleDownloadError(
|
|
1428
|
+
'CODE_BUNDLE_DOWNLOAD_INVALID_UTF8',
|
|
1429
|
+
t('common.code_bundle_invalid_utf8'),
|
|
1430
|
+
metadata
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (typeof optionsOverride.onResponseMetadata === 'function') {
|
|
1434
|
+
optionsOverride.onResponseMetadata(metadata);
|
|
1435
|
+
}
|
|
1436
|
+
return value;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1083
1439
|
/**
|
|
1084
1440
|
* 发送 HTTP POST 请求(application/x-www-form-urlencoded)
|
|
1085
1441
|
* @param {string} baseUrl
|
|
@@ -1504,8 +1860,10 @@ module.exports = {
|
|
|
1504
1860
|
isLoginExpired,
|
|
1505
1861
|
isCsrfTokenExpired,
|
|
1506
1862
|
httpPost,
|
|
1863
|
+
httpPostMultipart,
|
|
1507
1864
|
httpPostJson,
|
|
1508
1865
|
httpGet,
|
|
1866
|
+
httpGetCodeBundleText,
|
|
1509
1867
|
requestWithAutoLogin,
|
|
1510
1868
|
requestNonIdempotentWithAuthPreflight,
|
|
1511
1869
|
getNpmExecutable,
|
package/package.json
CHANGED
|
@@ -37,7 +37,7 @@ HTML 保留“需求总览、数据模型、业务流程、页面规划”四章
|
|
|
37
37
|
按 [用户交互契约](../../../yida-design/references/ask-human-interaction-contract.md) 执行:
|
|
38
38
|
|
|
39
39
|
1. 在会话中展示“当前这版方案”,并用 3–7 条业务摘要说明方案内容。
|
|
40
|
-
2. 必须实际调用 `ask_human` 创建结构化提问,并通过同一次调用的 `attachments` 携带可打开的 `prd/<项目名>/build-plan.html`;附件对象固定使用 `name: "build-plan.html"`,并将 `revision` 设为当前 `meta.revision
|
|
40
|
+
2. 必须实际调用 `ask_human` 创建结构化提问,并通过同一次调用的 `attachments` 携带可打开的 `prd/<项目名>/build-plan.html`;附件对象固定使用 `name: "build-plan.html"`,并将 `revision` 设为当前 `meta.revision`。调用必须遵守交互契约中的实际参数模板:一个顶层 `question`、恰好两个顶层 `options`(value 只能是 `confirm_build`、`continue_editing`)、同次调用的 `attachments`、`revision` 和 `submitLabel`。禁止使用 `fields`、`text`、`textarea`,禁止增加“调整说明”或其他条件式输入。只输出方案正文或普通 assistant 文本后结束本轮属于未完成,严禁用它替代 `ask_human`;也不得改成项目标题,或先发普通文本附件、再单独提问。
|
|
41
41
|
3. 结构化交互成功创建后内部记录 `presentedRevision=meta.revision`。询问“确认并开始搭建”或“继续调整”,提交时由宿主原样回传 revision,将确认结果绑定到本次展示版本。用户可见版本称为“第 N 版方案”,展示序号与内部 revision 绑定。
|
|
42
42
|
|
|
43
43
|
只有以下条件同时成立才交接;它们由本轮 ask_human 请求和回传在运行时判定,不要求把确认状态写回 workspace 文件:
|
|
@@ -50,6 +50,8 @@ HTML 保留“需求总览、数据模型、业务流程、页面规划”四章
|
|
|
50
50
|
|
|
51
51
|
## 4. 处理调整
|
|
52
52
|
|
|
53
|
+
用户选择“继续调整”后保持在 Plan Design,本轮不创建应用、表单、流程或页面。下一轮再单独询问需要修改的内容;不要把调整说明、自由文本或 textarea 塞进最终确认卡。收到具体调整后才执行下面的 patch,物化新 revision 并重新展示最终确认。
|
|
54
|
+
|
|
53
55
|
按字段更新源事实并重新生成,例如同时调整品牌色和圆角:
|
|
54
56
|
|
|
55
57
|
```bash
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
## 2.0 先分析并确认需求
|
|
10
10
|
|
|
11
|
-
调用 `yida-requirement-analysis`,按 [需求分析与首次搭建确认](../../yida-requirement-analysis/workflow/prepare-brief.md) 整理来源、复用资源及用户已有计划,在同一轮一次性确认尚未明确的 Fast / Plan
|
|
11
|
+
调用 `yida-requirement-analysis`,按 [需求分析与首次搭建确认](../../yida-requirement-analysis/workflow/prepare-brief.md) 整理来源、复用资源及用户已有计划,在同一轮一次性确认尚未明确的 Fast / Plan、业务模块、页面与表单及风格。导航方式与布局按 [导航设计](../../yida-requirement-analysis/workflow/prepare-brief.md#导航设计) 确定;用户已明确要求优先,后续阶段复用判断和依据。首次搭建在必要回答与导航判断写回、`intake.confirmed=true` 后继续。
|
|
12
12
|
|
|
13
13
|
回答齐全后直接保存内部需求记录并进入 2.1;不把“生成需求简报”列为独立任务,不再扩写或展示简报请用户确认。已有确认记录且需求未变化时直接复用。记录粒度、保存和校验规则统一遵守上述需求分析流程。
|
|
14
14
|
|
|
15
|
-
显式搭建方式属于本次任务的粘性输入。若用户在首次消息已选择 Plan
|
|
15
|
+
显式搭建方式属于本次任务的粘性输入。若用户在首次消息已选择 Plan,澄清业务模块、页面或风格时不得再次询问模式,也不得在合并回答后改写为 Fast;进入 2.1 前必须以用户最后一次明确选择校验 `intake.designMode`。Fast 同理。
|
|
16
16
|
|
|
17
17
|
执行规划前读取 `constraints.prohibitedActions`。PRD 与 design 必须把禁止项写成实现门禁:`theme-file` 禁止时沿用现有平台主题且不安排主题文件任务;`page-source` 禁止时只允许只读核查与非源码配置;`publish` 禁止时把发布明确标记为跳过。不得为了满足默认九步流程静默删除这些约束。
|
|
18
18
|
|
|
@@ -37,6 +37,7 @@ description: 表单页面创建与更新;支持 19 种业务字段和 Divider
|
|
|
37
37
|
- update / add-option / bind-datasource / validation / rule 等字段级操作不要求先执行外部 `get-schema`;直接提交 compact JSON 或字段 label/fieldId,CLI 会内部读取 schema、定位字段,并在成功 JSON 中输出 compact `resolved`/`updatedProps` evidence。字段解析失败/歧义时按 `diagnostics[].candidates` 补 `tableLabel`、修正 label 或再执行一次 compact `get-schema`。
|
|
38
38
|
- 字段定义或变更定义需要落盘时,必须使用 agent 的结构化文件写入工具创建到 `<projectRoot>/.cache/openyida/<项目名或任务名>/`,例如 `<projectRoot>/.cache/openyida/pm/pm-fields-team.json`
|
|
39
39
|
- 普通表单分组必须优先使用 `Divider`,多列排版必须通过字段 JSON 中的 `ColumnContainer` 局部表达
|
|
40
|
+
- **重复结构化记录默认使用 `TableField`**:用户未指定具体字段类型时,凡一个业务字段承载多条同构记录,且每条记录由一组固定子字段组成,必须用 `TableField + children` 建模,不以字段名称或业务领域作为判断依据;用户明确指定具体字段类型时按用户要求执行,不将模型推断、跨会话记忆或历史兼容性说法视为用户指定。未在当前应用、当前提交链路验证的限制不能作为字段改型依据。
|
|
40
41
|
- **本技能不读写 memory**:formUuid 等信息输出到 stdout,通过 `.cache/<项目名>-schema.json` 持久化,不依赖跨会话的 memory 状态
|
|
41
42
|
|
|
42
43
|
## 适用场景
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
1. 进度消息使用业务结果和下一步动作。结构化问题紧随其后时,不额外发送内部分析结论;多个问题确需共同背景时,只在整组交互的引导中说明一次,不写入每个问题的 `title` 或 `prompt`。
|
|
61
61
|
2. `title`、`prompt`、`label`、`description` 和普通对话只使用用户能直接理解的业务词。
|
|
62
62
|
3. `interactionId`、`questionType`、`value`、`writeBackPath`、`reason`、状态字段和文件路径仅供内部执行,不进入用户可见内容。
|
|
63
|
-
4. `title` 只写确认主题,`prompt`
|
|
63
|
+
4. `title` 只写确认主题,`prompt` 只写用户当前需要回答的问题;搭建模式保持中性,不标记“推荐”或“默认”,不预选、不按偏好排序,也不在引导或描述中推荐。导航方式、布局和呈现样式由 AI 根据场景确定,用户明确要求优先。其他问题存在合理默认值时可标记。只有某个选择会产生重要且难以撤销的影响时,才在对应选项的 `description` 中说明影响。
|
|
64
64
|
5. 用户已经提供的信息直接写入内部事实,不重复总结成“缺口分析”或再次追问。
|
|
65
65
|
6. PRD 的业务说明、HTML、进度消息和交付总结使用功能、体验和验收结果描述配置。接口参数、配置键值、内部 ID 和 CLI 选项保留在 Agent 实施交接中;访问链接使用业务名称,链接目标保留必需参数。用户明确询问实现细节时,再解释对应技术内容。
|
|
66
66
|
7. 规划写预期行为,进度写正在进行的动作,完成总结依据实际验证结果。导航方案写“采用自定义导航,支持各业务页面间切换”;完成配置并验证跳转后写“已启用自定义导航,可在各业务页面间切换”。
|
|
@@ -100,7 +100,6 @@ Fast / Plan 是面向用户的模式名称,可以展示。已有详细计划
|
|
|
100
100
|
| ---------------- | ---------------------------------------------- | ---------------------------- | ----------------------------- |
|
|
101
101
|
| 搭建方式 | `single_choice` | 按需求详细程度提供 PRD 是/否或 Fast/Plan | brief 的 `intake.designMode` |
|
|
102
102
|
| 业务模块与页面范围 | `multi_choice` | 具体业务模块、对应页面、表单及用途,可合为一题 | brief 的 `coreFunctions/businessObjects/pageScenes/explicitScope` |
|
|
103
|
-
| 导航归属 | `single_choice` | 仅“宜搭原生导航”和“自定义导航”两个选项;原生导航就是平台导航,布局根据场景确定 | brief 的 `navigation`(按场景补齐 `type`,自定义含 `variant`,`reason` 区分用户归属选择与布局依据) |
|
|
104
103
|
| 应用设计风格 | `single_choice` | 面向业务的风格选项与自定义偏好 | brief 的 `visualSelection` |
|
|
105
104
|
| 来源补齐 | `free_text` | 可读正文、链接或附件 | `meta.source` |
|
|
106
105
|
| 最新搭建计划确认 | `confirm` | 当前版本、确认生成、继续调整 | `meta.planState` |
|
|
@@ -124,9 +123,9 @@ Fast / Plan 是面向用户的模式名称,可以展示。已有详细计划
|
|
|
124
123
|
|
|
125
124
|
## 首次搭建确认
|
|
126
125
|
|
|
127
|
-
先完成需求分析,再按 [首次搭建确认表](../../yida-requirement-analysis/workflow/prepare-brief.md#2-确认首次搭建的未决事项)
|
|
126
|
+
先完成需求分析,再按 [首次搭建确认表](../../yida-requirement-analysis/workflow/prepare-brief.md#2-确认首次搭建的未决事项) 询问尚未明确的事项。该表统一维护详细计划复用、搭建方式、业务模块、风格和页面范围;已有业务应用的局部增改按本次疑问澄清。
|
|
128
127
|
|
|
129
|
-
首次搭建的全部未决事项在一次结构化提问调用中呈现并一次性作答,Fast / Plan
|
|
128
|
+
首次搭建的全部未决事项在一次结构化提问调用中呈现并一次性作答,Fast / Plan、业务模块和页面范围不得分轮收集。导航按 [导航设计](../../yida-requirement-analysis/workflow/prepare-brief.md#导航设计) 确定,用户已明确的方式、布局或样式直接沿用。工具限制问题数量时合并为复合问题,多选能力不足时使用同组自由输入;没有可用结构化工具时,在一条消息中展示全部问题。具体合并方式遵守首次搭建确认表,全部必要回答写回后才开始后续规划。
|
|
130
129
|
|
|
131
130
|
视觉映射统一写入 brief 的 `visualSelection`,由后续计划沿用 visualDirection、selectedTheme、colorStrategy、navigationStyle;未选候选保留在会话中。
|
|
132
131
|
|
|
@@ -141,7 +140,7 @@ Plan Design 完成当前版本后,按以下顺序与用户交互:
|
|
|
141
140
|
|
|
142
141
|
`build-plan.html` 不承载对话控件或确认按钮;用户在会话中完成确认。
|
|
143
142
|
|
|
144
|
-
|
|
143
|
+
最终确认是一个 `single_select` 语义问题:只使用一个顶层 `question` 和恰好两个顶层 `options`,附件与版本必须放在同一次调用中。下面先记录内部逻辑身份;`interactionId`、`questionType`、`prompt`、`allowCustom` 和 `writeBackPath` 只用于状态理解,不是可传给工具的参数:
|
|
145
144
|
|
|
146
145
|
```json
|
|
147
146
|
{
|
|
@@ -173,6 +172,41 @@ Plan Design 完成当前版本后,按以下顺序与用户交互:
|
|
|
173
172
|
}
|
|
174
173
|
```
|
|
175
174
|
|
|
175
|
+
#### 实际 `ask_human` 调用参数
|
|
176
|
+
|
|
177
|
+
模型必须直接复制下面的可调用结构,并将路径、`{revision}` 替换为本次 materialize 返回的真实值:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"question": "是否按当前这版方案开始搭建?",
|
|
182
|
+
"title": "确认整体方案",
|
|
183
|
+
"options": [
|
|
184
|
+
{
|
|
185
|
+
"value": "confirm_build",
|
|
186
|
+
"label": "确认并开始搭建",
|
|
187
|
+
"description": "按当前方案创建应用、表单、流程和页面。"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"value": "continue_editing",
|
|
191
|
+
"label": "继续调整",
|
|
192
|
+
"description": "继续完善当前方案,确认后再开始搭建。"
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"attachments": [
|
|
196
|
+
{
|
|
197
|
+
"name": "build-plan.html",
|
|
198
|
+
"path": "prd/<项目名>/build-plan.html"
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"revision": "{revision}",
|
|
202
|
+
"submitLabel": "提交选择"
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
最终确认 payload 禁止使用 `fields`、`text`、`textarea`,也禁止增加“调整说明”或其他条件式补充问题;`options` 的 value 集合必须恰好为 `confirm_build`、`continue_editing`。确认与调整内容不能合并收集。
|
|
207
|
+
|
|
208
|
+
用户选择 `continue_editing` 后保持在 Plan Design,本次确认恢复不创建任何应用资源;在下一轮用新的问题单独收集要修改的内容,完成 patch/materialize、生成新 revision 后,再展示同样结构的最终确认。用户选择 `confirm_build` 且回传 revision 匹配时,才进入资源实施。
|
|
209
|
+
|
|
176
210
|
## 版本与确认状态
|
|
177
211
|
|
|
178
212
|
`build-plan.json` 使用 `meta.revision`、`meta.status` 和 `meta.planState` 共同记录当前计划状态:
|
|
@@ -198,4 +232,4 @@ Plan Design 完成当前版本后,按以下顺序与用户交互:
|
|
|
198
232
|
3. 计划展示完成后设置 `meta.status=awaiting_confirmation` 和 `presentedRevision=meta.revision`。
|
|
199
233
|
4. 用户在最终确认交互中选择“确认并开始搭建”时,设置 `meta.status=confirmed`、`planConfirmed=true`、`confirmedRevision=meta.revision`,同时记录交互 ID 和确认时间。
|
|
200
234
|
5. 只有 `meta.status=confirmed`、`planConfirmed=true` 且 `meta.revision=presentedRevision=confirmedRevision` 时,Plan Design 才能返回 `yida-app` Step 3。
|
|
201
|
-
6. 用户选择“继续调整”时保持在 Plan Design
|
|
235
|
+
6. 用户选择“继续调整”时保持在 Plan Design,本轮不创建应用资源;下一轮单独收集调整内容,生成新 revision 并重新确认。用户取消或关闭交互时停止执行,不创建应用资源。
|
package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md
CHANGED
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
|
|
143
143
|
约束:
|
|
144
144
|
|
|
145
|
-
-
|
|
145
|
+
- 计划生成前先确定应用范围,导航方式与布局按 [导航设计](../../../../yida-requirement-analysis/workflow/prepare-brief.md#导航设计) 写入 brief,用户明确要求优先。视觉候选沿用该导航决策,随整体方案确认。其余必要问题必须有 `interactionId` 和 `writeBackPath`。
|
|
146
146
|
- `questionType` 只取 `single_choice`、`multi_choice`、`free_text` 或最终计划使用的 `confirm`,不设置 AI 代填项中间确认类型。
|
|
147
147
|
- 只有宽泛应用名称且缺少模块、场景、任务、流程和可读需求时才问应用范围。
|
|
148
148
|
- 审批、角色权限、字段、首页、看板、页面和常规流程细节不进入 `ask_human`;用户已提供时承接,未提供时基于业务推断并在最终计划统一呈现。
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## 自定义导航设计
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
沿用 brief 中 AI 判断或用户明确指定的导航方式与布局。参考 [导航壳形态目录](../../yida-nav-shell/references/nav-shell-patterns.md) 设计位置、比例、留白与选中态,代码示例只按需参考。顶部默认浮导;侧导及顶部+侧边布局写清折叠/展开、恢复宽度、拖拽边界、内容区联动和移动端收起方式。已有满意的导航保留外观,只补缺失交互。
|
|
17
17
|
|
|
18
18
|
区分三种操作:本页视图切换、保留导航并更新主内容 iframe、当前标签跨页跳转。管理入口使用 workbench,办理入口使用 submission;不要把应用级办理导航设计为每次弹抽屉。页面内新增/详情按钮才采用下面的抽屉规则。设计结果写入当前 `design.md`;Plan 模式先更新计划源事实再物化。
|
|
19
19
|
|
|
@@ -421,6 +421,7 @@ openyida integration diagnose --file project/tickets/automation-error.txt --json
|
|
|
421
421
|
- 流水号:新增/编辑触发时,触发 payload 中的流水号可能为空或不是最新值;需要先获取自身,再引用获取节点里的流水号。
|
|
422
422
|
- 定时自动化:定时触发值可能是历史数据;需要最新值时先获取自身或获取目标数据。
|
|
423
423
|
- 直接更新:匹配字段只能消费当前触发表字段,不能随意匹配前置节点字段;文本字段不要匹配单选/多选字段。直接更新不会触发被更新表单上的集成自动化。
|
|
424
|
+
- 跨应用写入不级联触发(阻塞项):A 应用表单的集成自动化通过内置新增/编辑/删除数据节点写 B 应用表单(跨应用)时,B 表单上已配置的新增/编辑/删除自动触发集成业务流不会生效。涉及跨应用链式联动时这是阻塞项,不得假设 B 表自动化会被接力触发。
|
|
424
425
|
- 条件分支:空值判断优先用公式 `ISEMPTY()`,不要只依赖“没有值”选项。
|
|
425
426
|
- 异常重试:重试通常会从头执行,已执行过的新增/更新节点可能再次写入或覆盖数据;重试前先确认幂等性。
|
|
426
427
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yida-nav-shell
|
|
3
|
-
description:
|
|
3
|
+
description: 需求规划采用自定义应用导航或用户明确要求自绘时使用;先隐藏应用导航 hideAppNav。页面级 isRenderNav=false 不等于应用导航隐藏。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# yida-nav-shell
|
|
7
7
|
|
|
8
8
|
## 先判断
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
按 brief/PRD 的导航决策路由:采用自定义应用导航时使用本技能;采用宜搭原生导航时使用 `yida-nav-group`。导航方式由 [需求分析](../yida-requirement-analysis/workflow/prepare-brief.md#导航设计) 判断,用户明确要求优先。
|
|
11
11
|
|
|
12
12
|
| 用户需求 | 怎么处理 |
|
|
13
13
|
| --- | --- |
|
|
@@ -26,7 +26,7 @@ description: 自定义页明确要自绘应用级导航时使用;先隐藏应
|
|
|
26
26
|
|
|
27
27
|
## 必做配置
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
需求规划采用自定义导航后,就将应用导航隐藏纳入应用基础设置更新:新建应用时,等 `app-theme.css` 生成后,将 `--hide-app-nav` 与主题、Logo、布局等设置合并到同一次 `update-app`,按 [应用设置同步](../yida-app/workflow/step-3-create-or-reuse-app.md) 执行并回读。已有应用只需切换导航时执行:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
openyida update-app <appType> --hide-app-nav
|
|
@@ -50,7 +50,7 @@ openyida get-form-config <appType> <formUuid> --json
|
|
|
50
50
|
- **画布与浮导间距**:平台宿主继续消费 `--pod-page-bg-color`;自绘导航页的内部画布由 `design.md` 定义,可采用浅灰、浅彩、渐变或局部纹理,不强制跟随白色平台底色。浅色非白画布上,顶部浮导默认白色或近白半透明,卡片默认白色无框;品牌色集中于选中态和主操作,深色方案单独设计。只作用于当前页面选择器,不修改应用全局变量。根节点使用 `display:flow-root` 或 flex/grid,让浮导上边距留在根节点内。
|
|
51
51
|
|
|
52
52
|
- **先选形态,再写 UI**:根据已确认的 PRD、`design.md` 和用户参考确定布局。模块多用侧栏,模块少且内容需要宽度用顶部,两级业务用顶部+侧边,沉浸展示可用悬浮 Dock,同模块视图用标签。已确认的选择直接沿用,不重新提问。
|
|
53
|
-
-
|
|
53
|
+
- 自定义顶部导航默认推荐浮导,可按内容宽度设计为紧凑胶囊或悬浮栏;“顶部导航”不等于贴边通栏。位置、比例、留白、材质和选中态根据业务与设计实现,不由现成组件决定。此推荐只针对顶部样式,不改变已确定的导航方式。
|
|
54
54
|
- 需要布局方向和小段代码时读 [导航壳形态目录](references/nav-shell-patterns.md)。按场景设计和手写实现,不强制复制任何导航组件。已有导航符合设计时直接复用,只补缺失功能;不能仅因存在新示例而替换现有外观。
|
|
55
55
|
- 自定义侧边导航(含顶部+侧边)的 PC 端必须支持折叠/展开和拖拽调宽;展开恢复折叠前宽度,宽度变化时内容区同步调整。移动端改为可展开/收起的菜单,详见 [侧栏交互](references/nav-shell-patterns.md#侧栏交互)。
|
|
56
56
|
- 菜单数量、名称、顺序、分组和入口用途来自 PRD,通常工作台在首位;用当前访问者的 `getAccessableNavs.json` 过滤可见范围,详见 [导航数据来源](references/nav-shell-patterns.md#导航数据来源)。数据逻辑可直接复用,不要求采用同一套 UI。
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
| 配置项 | 值 |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| 导航类型 | <平台L型导航 / 平台顶部导航 / 平台侧边导航 /
|
|
27
|
+
| 导航类型 | <平台L型导航 / 平台顶部导航 / 平台侧边导航 / 自定义导航;沿用需求阶段的导航判断和用户明确要求> |
|
|
28
28
|
| 是否使用平台应用导航 | <前三种为是,自定义导航为否> |
|
|
29
29
|
| 页面导航配置 | <自定义导航:列出本轮全部表单、流程表单、自定义页面及需配置的其他页面,统一隐藏平台页面导航;平台导航:保留页面设置,明确独立入口例外> |
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
导航方式与布局沿用 brief 中的决策,判断规则见 [导航设计](../../yida-requirement-analysis/workflow/prepare-brief.md#导航设计)。上表记录可执行方案和依据,区分用户明确要求与 AI 判断,说明页面入口和跨页切换方式;导航配色单独说明深色或浅色。
|
|
32
32
|
|
|
33
33
|
## 3. 数据结构(业务语义,不含细节 ID)
|
|
34
34
|
|