openyida 2026.9.13 → 2026.9.15

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.
Files changed (30) hide show
  1. package/lib/app/canvas-compile.js +37 -4
  2. package/lib/app/create-form/batch.js +54 -6
  3. package/lib/app/create-form.js +89 -11
  4. package/lib/app/display-page-readback.js +63 -5
  5. package/lib/app/get-schema.js +158 -5
  6. package/lib/app/publish.js +127 -5
  7. package/lib/core/agent-capabilities.js +6 -7
  8. package/lib/core/command-manifest.js +2 -2
  9. package/lib/core/locales/en.js +9 -0
  10. package/lib/core/locales/zh.js +9 -0
  11. package/lib/core/utils.js +358 -0
  12. package/package.json +1 -1
  13. package/yida-skills/skills/yida-app/SKILL.md +11 -5
  14. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +5 -3
  15. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +4 -3
  16. package/yida-skills/skills/yida-app/workflow/step-2-design.md +2 -2
  17. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +5 -2
  18. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +18 -14
  19. package/yida-skills/skills/yida-create-form-page/SKILL.md +18 -3
  20. package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +4 -3
  21. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +22 -19
  22. package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
  23. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +1 -1
  24. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
  25. package/yida-skills/skills/yida-integration/SKILL.md +1 -0
  26. package/yida-skills/skills/yida-nav-shell/SKILL.md +4 -4
  27. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  28. package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +2 -2
  29. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
  30. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +17 -18
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.9.13",
3
+ "version": "2026.9.15",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -9,11 +9,17 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
9
9
 
10
10
  ## 模式入口(先按这里路由)
11
11
 
12
+ `intake.designMode` 是整轮搭建工作流的模式状态,随 intake、需求记录、PRD/视觉设计、方案确认和实施阶段持续传递。它的初始值来自用户的明确选择,后续也只由用户新的明确模式选择更新;其余未决 intake 字段按当前模式的稳定默认策略补齐。Plan 依次产出需求事实、`build-plan.html` / `build-plan.json` 和带 revision 的最终确认;与当前 revision 匹配的 `confirm_build` 是进入真实资源实施阶段的状态转换。在此转换发生前,工作范围保持在需求、设计、方案产物和只读资源检查。
13
+
14
+ `continue_editing` 使当前计划沿既有 lineage 继续演进。根据变更粒度使用 `design-plan patch`,或编辑当前计划源后 materialize,产出单调递增的新 revision 并再次展示最终确认卡。最近一次获确认的 revision 是后续实施阶段的唯一方案输入。
15
+
12
16
  Plan 模式只读取 `workflow/step-1-resource-context.md`、`workflow/step-2-design.md` 和精确路径 `workflow/plan/workflow.md`;后者已经包含完整 Plan 入口。禁止用 Glob 查找 Plan 文件,也不要额外读取 `workflow/plan/step-1-understand.md` 或 `workflow/plan/step-2-confirm.md`。Plan 确认恢复后,若 `explicitScope.allowInferredResources=false`,直接读取 `workflow/step-4-forms-processes.md` 实施范围内资源,不再重读 Step 1、调用 list-forms 或做应用设置预检。
13
17
 
14
- ## 步骤模版(进行时展示给用户看的步骤)
18
+ ## 执行步骤(进行时展示给用户)
19
+
20
+ 先从已经确认的 `execution.explicitScope` 生成本轮步骤,再开始资源实施。步骤只对应范围内尚未完成的资源和交付;资源成功回读后从剩余步骤中移除。`allowInferredResources=true` 表示完整应用交付,可使用下面的完整应用模版;`allowInferredResources=false` 表示边界已闭合,使用“需求识别与分析 / 设计功能和页面 / 生成PRD方案&确认(Plan)+ 范围内资源步骤 + 检查范围并交付”的精简步骤。
15
21
 
16
- 完整应用搭建时,直接使用对应模式的步骤名称。
22
+ 完整应用搭建时使用对应模式的步骤名称。
17
23
 
18
24
  **Plan 模式**
19
25
 
@@ -38,11 +44,11 @@ Plan 模式只读取 `workflow/step-1-resource-context.md`、`workflow/step-2-de
38
44
  7. 发布页面与配置导航
39
45
  8. 检查功能并交付
40
46
 
41
- 已有应用时,直接省略“创建应用”,不另列“复用现有应用”待办,后续步骤重新编号;无需示例数据时跳过对应步骤。步骤状态按真实进度更新,有独立输入的工作可同时进行。
47
+ 已有应用时省略“创建应用”,后续步骤重新编号。步骤状态按真实进度更新,有独立输入的工作可同时进行。
42
48
 
43
49
  用户明确把本轮交付限定为一个或若干具体表单、流程、报表或页面时,按 `explicitScope` 只保留达到该交付所需的步骤;即使需求背景使用“应用/系统”,也不自动补示例数据、自定义工作台、主题设置、导航排序或其他资源。Plan 模式仍生成并确认方案,但确认后只执行该窄范围。
44
50
 
45
- 若窄范围只要求创建一个普通表单并交付链接,成功的 `create-form create` 结果就是本轮资源回读证据:立即交付其中的真实链接并停止。不要再调用 `get-schema`、`list-forms`、数据管理技能、示例数据、主题或导航命令,除非创建结果明确缺少 ID/链接或用户另外要求这些内容。
51
+ 若窄范围只要求创建一个普通表单并交付链接,成功的 `create-form create` 结果就是本轮资源回读证据。`url` 是兼容字段,与 `formUrl` 表示同一表单入口;`appUrl` 表示应用工作台入口。按用户要求的入口层级选择权威字段,资源保存成功且证据齐全后完成当前范围;证据缺失时执行一次针对性只读回查。普通表单保存成功后进入可用状态,自定义展示页遵循页面发布生命周期。
46
52
 
47
53
  禁区:待办标题、说明和进度不出现技能名、命令、文件路径、登录账号、内部资源 ID;这些留在工具调用里。用户明确询问技术细节时再解释。
48
54
 
@@ -81,7 +87,7 @@ Plan 模式只读取 `workflow/step-1-resource-context.md`、`workflow/step-2-de
81
87
  7. **删除必须确认**:用户要求删除应用时,先展示应用名称、应用 ID 和影响范围,等待明确“确认删除”后才能执行。
82
88
  8. **列表页选择**:默认使用普通表单的数据管理页;用户明确要求自定义列表页时才创建 display 页面。
83
89
  9. **交付物收口**:Step 2 的三个文件和 Step 9 的 build manifest 都是内部文件,不是用户交付物。表单、流程、报表和页面只在业务总结中概述,不逐项生成用户可见附件;宿主支持交付工具时,final 只交付一次“应用访问入口”组。
84
- 10. **窄范围停止点**:完成 `explicitScope` 中的资源回读与真实链接交付后立即停止;不得为了满足完整应用默认完成条件继续进入被裁剪的 Step 5-8。
90
+ 10. **范围完成点**:以 `explicitScope` 的资源集合为任务队列;集合清空且真实链接已交付时,本轮完成。完整应用默认完成条件只在 `allowInferredResources=true` 时生效。
85
91
 
86
92
  ## 关键决策树
87
93
 
@@ -24,7 +24,7 @@ openyida design-plan materialize prd/<项目名>/build-plan.json --from-preview
24
24
 
25
25
  CLI 完整校验后一起保存源计划、`prd.md`、`design.md`、`build-plan.html` 和 `app-theme.css`。HTML 使用预置模板,业务内容与 PRD 一致。
26
26
 
27
- 标准首版必须直接执行 init 返回的 `materialize.command`;不得先试 `--from-preview`、`preview`、无参数 materialize 或 shell 重定向。成功 JSON 已包含 `outputs.html` 和 `revision`,直接用于下一步同一次结构化提问,不再 Glob 目录,也不额外 Read `prd.md`、`design.md` 或 `build-plan.json`。
27
+ 标准首版的生成入口是 init 返回的 `materialize.command`。成功 JSON 中的 `outputs.html` 和 `revision` 直接构成下一步结构化确认的附件与版本输入。按模块更新过的草稿使用本节单列的 `--from-preview` 命令;诊断使用下方 `--check` 命令。
28
28
 
29
29
  完整文件的职责与版本规则见 [完整文件合并](../parallel-work.md#plan-的-cli-交接)。直接维护源计划时先设 `meta.status=awaiting_confirmation`,再执行 `openyida design-plan materialize prd/<项目名>/build-plan.json --json`;仅做诊断时使用 `openyida design-plan materialize prd/<项目名>/build-plan.json --check --json`。正常生成已经包含完整校验,不先运行一次 --check 再重复生成。
30
30
 
@@ -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`。只输出方案正文或普通 assistant 文本后结束本轮属于未完成,严禁用它替代 `ask_human`;也不得改成项目标题,或先发普通文本附件、再单独提问。
40
+ 2. 实际调用 `ask_human` 创建结构化提问。调用对象严格采用交互契约中的唯一 payload schema;`attachments` 携带 `name: "build-plan.html"`、`path: "prd/<项目名>/build-plan.html"`,`revision` 使用当前 `meta.revision`,`options` 固定为 `confirm_build` 和 `continue_editing`。一次成功调用同时建立方案展示、版本绑定和最终选择。
41
41
  3. 结构化交互成功创建后内部记录 `presentedRevision=meta.revision`。询问“确认并开始搭建”或“继续调整”,提交时由宿主原样回传 revision,将确认结果绑定到本次展示版本。用户可见版本称为“第 N 版方案”,展示序号与内部 revision 绑定。
42
42
 
43
43
  只有以下条件同时成立才交接;它们由本轮 ask_human 请求和回传在运行时判定,不要求把确认状态写回 workspace 文件:
@@ -46,10 +46,12 @@ HTML 保留“需求总览、数据模型、业务流程、页面规划”四章
46
46
  - `meta.planState.planConfirmed=true`
47
47
  - `meta.revision=presentedRevision=confirmedRevision`
48
48
 
49
- 收到“确认并开始搭建”且回传 revision 等于展示 revision 后,直接进入同版本资源实施。确认之后严禁再次 materialize、patch、Edit 或 Read 计划来“同步确认状态”;不存在可写 `meta.planState.planConfirmed` 的确认命令。`explicitScope.allowInferredResources=false` 时也不执行主题 CSS、应用设置或导航交接,只创建范围内资源并回读、交付。
49
+ 收到 `confirm_build` 且回传 revision 等于展示 revision 后,以该版本计划作为实施阶段的唯一方案输入并直接创建范围内资源。`explicitScope.allowInferredResources=false` 时,实施范围由 `explicitScope` 决定,完成资源创建、回读和交付。
50
50
 
51
51
  ## 4. 处理调整
52
52
 
53
+ `continue_editing` 把工作流从 `awaiting_confirmation` 转为 `editing`。下一次交互收集变更内容;收到变更后更新当前计划源,物化新 revision,并重新进入最终确认。
54
+
53
55
  按字段更新源事实并重新生成,例如同时调整品牌色和圆角:
54
56
 
55
57
  ```bash
@@ -6,8 +6,9 @@
6
6
 
7
7
  - 规划功能、页面和配色:设计功能和页面
8
8
  - 生成方案并等待用户确认:生成PRD方案&确认
9
- - 新建应用确认后:创建应用 → 搭建表单与审批流 → 准备示例数据 → 搭建业务页面 → 发布页面与配置导航 → 检查功能并交付
10
- - 已有应用确认后:搭建表单与审批流 → 准备示例数据 → 搭建业务页面 → 发布页面与配置导航 → 检查功能并交付
9
+ - 方案确认后:读取同一 revision 的 `execution.explicitScope`,为其中非空的资源集合生成对应实施步骤,最后增加“检查范围并交付”
10
+ - `allowInferredResources=true` 的完整应用:按 `yida-app` 完整应用模版生成后续步骤
11
+ - `allowInferredResources=false` 的闭合范围:后续步骤总集等于确认方案中的资源步骤与交付步骤;每完成并回读一个资源,就从剩余集合中移除
11
12
 
12
13
  沿用已有待办,不把下面的内部执行顺序另建成一份任务列表。
13
14
 
@@ -25,7 +26,7 @@
25
26
  4. init 已创建并预填 `business.json` 骨架;先 Read 该文件,保留 `base`,一次补完后再 Write/Edit,避免覆盖保护失败。`facts` 只允许 overview、dataModels、businessFlows、pages 和可选 execution,绝不写 `visualStyle`。同一次补齐所有普通表单的 sampleDataPlan(窄范围不造数时写 skipReason)及所有自定义页面的 permissionSummary,然后直接执行 init 返回的 `materialize.command`,只物化一次。标准首版禁止先试 `--from-preview`、`preview`、`--check` 或无参数 materialize;成功 JSON 已返回 HTML 路径和 revision,不再用 Glob、Read 或帮助命令检查产物。只有存在品牌稿、参考图、页面级特殊风格或用户明确要求精修时,才执行 `optionalTasks.visual-refinement` 后再物化。
26
27
  5. 读取精确路径 `workflow/plan/step-4-deliver.md`,按其中契约直接展示并确认当前方案。超大需求需要展示中间进展或用户明确要求边生成边查看时,才使用 [按模块更新方案](../incremental-preview.md);普通首版不逐模块预览和重复渲染。
27
28
 
28
- 明确窄交付时,方案只描述 `explicitScope` 中的资源;确认后裁剪无关步骤,在这些资源回读并交付真实链接后停止。不得因用户使用“应用”一词自行增加 seed records、自定义页面、主题或导航工作。
29
+ 明确范围的方案以 `explicitScope` 作为完整执行清单。确认后的每个写操作都对应清单中的一个资源或交付项;清单资源全部回读且真实链接完成交付时,本轮达到完成态。
29
30
 
30
31
  初次编写只读 CLI 返回的紧凑契约、当前主题上下文及共享需求;模板全文由 CLI 读取。具体组件定制、暗色浮层或复杂页面需要额外规则时,再读取对应章节。
31
32
 
@@ -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、业务模块、页面与表单、导航归属及风格。导航 `ask_human` 仅提供“宜搭原生导航”和“自定义导航”两个选项,原生导航就是平台导航,并按 [导航选项说明](../../yida-requirement-analysis/workflow/prepare-brief.md#导航选项说明) 备注实现方式,不标记“推荐”。顶部、侧边、L 型等布局在用户所选归属内根据场景确定,不询问布局或样式;用户已明确的要求优先,后续阶段复用归属选择和布局依据。首次搭建在必要回答写回、`intake.confirmed=true` 后继续。
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,澄清业务模块、页面、导航或风格时不得再次询问模式,也不得在合并回答后改写为 Fast;进入 2.1 前必须以用户最后一次明确选择校验 `intake.designMode`。Fast 同理。
15
+ `intake.designMode` 是本次搭建的权威路由输入。进入 intake 和每次 `ask_human` 恢复后都读取该状态:未初始化时收集明确模式选择,已初始化时按问题声明的写回字段合并回答,新的明确模式选择负责更新模式状态。进入 2.1 时直接按合并后的当前状态路由。
16
16
 
17
17
  执行规划前读取 `constraints.prohibitedActions`。PRD 与 design 必须把禁止项写成实现门禁:`theme-file` 禁止时沿用现有平台主题且不安排主题文件任务;`page-source` 禁止时只允许只读核查与非源码配置;`publish` 禁止时把发布明确标记为跳过。不得为了满足默认九步流程静默删除这些约束。
18
18
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  按 PRD 的依赖创建或复用表单和流程。同一轮需要新建两个及以上普通表单时,把独立表单和关联表单写入同一个 `forms.json`,通过 `dependsOn` / `$form` 表达依赖,并且只调用一次 `openyida create-form batch`;由 CLI 内部完成分组、真实 ID 回读和依赖调度,不逐个 create,也不由模型拆成多次 batch。调用前确认实际项目根,让 Write 的绝对路径与 Bash 从项目根使用的 `.cache/openyida/<项目名>/forms.json` 指向同一个物理文件,并用 Read 确认任务文件存在;不要用 batch 探测路径,收到 background pending 后也不要重试 batch。某页所需表单、流程就绪后即可接入该页;不要因其他页面的资源未完成而阻塞无依赖页面开发。
4
4
 
5
- `explicitScope.allowInferredResources=false` 且本轮只有一个普通表单时走最短路径:Write 一份字段文件后调用一次 `openyida create-form create`。Write 会创建父目录,禁止先用 `ls` 或 Bash 检查缓存目录。成功结果已包含真实 formUuid/链接时,直接交付并停止;不得继续 `get-schema`、写 schema evidence、加载数据管理技能、造示例数据、更新主题或排序导航。只有创建结果缺少本次明确验收所需字段时才做一次针对性回读。
5
+ `explicitScope.allowInferredResources=false` 且本轮只有一个普通表单时走最短路径:以 Write 创建字段文件及其父目录,随后调用一次 `openyida create-form create`。成功结果中的 `url`/`formUrl` 是表单入口,`appUrl` 是应用工作台入口。将该表单标记为已完成并计算 `remainingScope`;为空时按用户要求的入口层级交付并完成本轮范围。普通表单保存成功后进入可用状态,自定义展示页遵循页面发布生命周期;验收证据缺失时执行一次针对性只读回查。
6
6
 
7
7
  拿到真实 `appType` 和已确认的业务契约即可开始本步骤,不等待主题 CSS 上传或应用主题设置回读。主题分支与本步骤并行,按 [主题与业务资源的依赖](parallel-work.md#主题与业务资源的依赖) 汇合。
8
8
 
@@ -91,4 +91,7 @@ PRD 导航类型为自定义导航时,对本轮创建或复用的每个表单
91
91
 
92
92
  ## 下一步
93
93
 
94
- → [Step 5:写入初始表单数据](step-5-seed-records.md)
94
+ - `remainingScope` 为空:→ [Step 9:按确认范围输出与收尾](step-9-output-finish.md)
95
+ - `remainingScope` 包含 seed records:→ [Step 5:写入初始表单数据](step-5-seed-records.md)
96
+ - `remainingScope` 包含自定义页面:→ [Step 6:创建或复用主页面](step-6-main-page.md)
97
+ - 其他资源:继续执行本步骤中与该资源类型对应的能力,直到资源集合完成
@@ -16,8 +16,12 @@
16
16
 
17
17
  主入口与页面入口必须取自本轮 CLI 成功结果返回的 `appUrl`、`workbenchUrl` 或 `url`,并在后续只读回查中保持一致。不得由模型根据 `appType` 自行拼接或猜测链接;下文 URL 规则仅用于校验服务端/CLI 返回值,不是缺失链接时的生成规则。若成功结果没有权威 URL,必须明确交付失败并执行只读回查,不能交付空卡片或用模板补齐。
18
18
 
19
+ 入口层级由用户的交付目标决定:应用访问、应用入口或应用工作台使用成功结果的 `appUrl`;具体表单、流程、报表或页面入口使用对应资源结果的 `url` / `workbenchUrl`。同一结果同时提供两种入口时,优先满足用户明确点名的层级。
20
+
19
21
  ## 完成条件核对
20
22
 
23
+ `execution.explicitScope.allowInferredResources=false` 时走范围完成路径:逐项核对清单内资源的成功结果或只读回读、用户要求的入口层级和交付状态;`remainingScope` 为空且真实链接已交付即为完成。下方完整应用检查表仅适用于 `allowInferredResources=true`。
24
+
21
25
  完整应用默认完成需要同时满足:
22
26
 
23
27
  1. 主页面发布成功;
@@ -67,17 +71,17 @@
67
71
 
68
72
  ## 结果输出格式
69
73
 
70
- - 先写 2-3 句业务交付总结,再给一个名为“应用访问入口”的入口组。
71
- - 调用 `notify_human` 前必须先输出上述正文总结;用户明确要求记录数、资源 ID、发布状态或核验结果时,先在正文写完经 readback 支撑的简洁清单,再调用交付工具。链接卡或平台产物卡不能替代正文总结与显式要求的清单。
72
- - 一次完整应用搭建只产生这一组用户可见交付,不把表单、流程、报表、页面、资源清单或内部文件分别登记为附件、链接卡或下载卡。
73
- - 业务资源只在总结中按能力或数量概述是默认规则,例如“已完成 4 张业务表单、1 条审批流程和 1 个经营看板”;不默认输出资源 ID 表格、资源清单、长列表、appType、formUuid、pageId、reportId。
74
- - 用户或调用方明确要求资源清单、资源 UUID/ID、发布状态或测试数据摘要时,final 必须在业务总结与唯一入口组之间补充一个简洁的“交付清单”。清单只列本轮已通过真实返回值或只读 readback 核验的资源名称、类型和 ID,并同时写明主页面发布状态及 seed records 写入/抽查摘要;不得遗漏已创建或发布的资源,不得用链接卡代替正文清单,也不得编造未知 ID。
75
- - 即使用户没有要求技术清单,业务总结中的资源数量、seed records 数量以及“已写入/已验证/已就绪”等完成状态也必须逐资源来自真实返回值或只读 readback。证据不完整时缩小表述范围并明确未核验项,禁止为了让总结完整而补齐推测数字。
76
- - 新增、修改或发布单个具体页面时,仍只交付当前页面,不扩展成完整应用入口组。
77
- - 完整应用的入口组始终包含“应用工作台” `{base_url}/{appType}/workbench`。
78
- - 主页面在 PRD 中为 `entryMode=standalone`,且 Step 8 回读确认 `isRenderNav=false` 时,入口组额外包含“独立业务入口” `{base_url}/{appType}/custom/{formUuid}`;否则不得输出。
79
- - 先读取 `openyida agent-capabilities --summary-json` 的 `application_entry_policy.entries.admin`:值为 `include` 时,入口组额外包含“应用开发后台” `{base_url}/{appType}/admin`;值为 `omit` 时不得输出。不要根据 Agent 名称或自然语言猜测云端/非云端。
80
- - 三个入口属于同一个应用入口组,不得各自连同业务资源再生成多组交付。
74
+ - 准备 2-3 句业务交付总结,并给一个名为“应用访问入口”的入口组。
75
+ - `notify_human` 是终态交付动作,其终态 artifact 的可见 `description` 是本轮业务总结、readback 事实和必要上下文的统一载体;用户要求的记录数、资源 ID、发布状态或核验结果也写入这里。调用成功即完成本轮交付。
76
+ - 一次完整应用 run 交付一组用户可见的“应用访问入口”。同一应用的后续交付从已有资源和已验证 URL 生成当前 run 的入口组;资源变更由用户本轮明确要求的变更范围驱动。
77
+ - 业务资源在总结中按能力或数量概述,例如“已完成 4 张业务表单、1 条审批流程和 1 个经营看板”。
78
+ - 用户或调用方明确要求资源清单、资源 UUID/ID、发布状态或测试数据摘要时,终态 artifact 的 `description` 包含简洁的“交付清单”。清单是本轮真实返回值和只读 readback 的投影,覆盖已创建或发布资源的名称、类型、ID、主页面发布状态及 seed records 写入/抽查摘要;未知信息标记为未核验。
79
+ - 业务总结中的资源数量、seed records 数量和完成状态与逐资源真实返回值/readback 一一对应;证据不完整的资源标记为未核验。
80
+ - 新增、修改或发布单个具体页面时,交付当前页面并保持单页范围。
81
+ - 完整应用的入口组包含“应用工作台” `{base_url}/{appType}/workbench`。
82
+ - 主页面在 PRD 中为 `entryMode=standalone`,且 Step 8 回读确认 `isRenderNav=false` 时,入口组包含“独立业务入口” `{base_url}/{appType}/custom/{formUuid}`。
83
+ - “应用开发后台”的入口谓词是 `application_entry_policy.entries.admin=include`;命中时加入 `{base_url}/{appType}/admin`,值为 `omit` 时入口组保持已有入口。该策略的事实源是 `openyida agent-capabilities --summary-json`,其中 include 由已注入可用鉴权 `builder_path.auth.auth_runtime=env_token_bootstrap && can_auto_use=true` 推导。
84
+ - 工作台、符合条件的独立业务入口和开发后台共同组成同一个应用入口组。
81
85
  - 不把 `g.alicdn.com` 的 `index.css`、`index.js`、`index.html`、`locales/*.json`、构建产物 URL、CDN 资源 URL 或中间文件链接当成最终结果展示。
82
86
  - 调用方或评测要求结构化结果时,额外输出顶层 `skillsUsed`,只填写本轮实际读取并使用的 `yida-*` 子技能名;不得把计划使用或未加载的技能写入。
83
87
 
@@ -132,10 +136,10 @@
132
136
 
133
137
  ## Checklist
134
138
 
135
- - [ ] final 先写业务总结,再给唯一一组“应用访问入口”;
136
- - [ ] `notify_human` 在正文总结与用户显式要求的事实清单之后调用,未把链接卡当成正文替代品;
139
+ - [ ] 唯一一组“应用访问入口”的终态 artifact `description` 已包含业务总结;
140
+ - [ ] `notify_human` 作为最后一个动作调用,用户显式要求的事实清单和资源 ID 已进入可见 `description`;
137
141
  - [ ] final 中每个资源数量和数据完成声明都有对应资源自己的成功返回值/readback;未把一张表单的记录数套用到其他表单;
138
- - [ ] 用户或调用方显式要求资源清单/UUID/发布状态/测试数据摘要时,正文已完整列出经核验的交付清单,未只返回链接卡;
142
+ - [ ] 用户或调用方显式要求资源清单/UUID/发布状态/测试数据摘要时,终态 artifact `description` 已完整列出经核验的交付清单;
139
143
  - [ ] 已写入轻量 build-manifest 并运行页面/资源数量完整性风险检查;未通过时没有声称“已按 PRD 完成搭建”;
140
144
  - [ ] 已按 `constraints.prohibitedActions` 调整完成条件;跳过写操作时没有伪造已换肤、已修改源码或已发布;
141
145
  - [ ] 未把内部文件或每个业务资源分别交付;