@xqyz/xq-cli 0.3.13 → 0.3.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.
package/src/cli.mjs CHANGED
@@ -40,8 +40,10 @@ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
40
40
  // outline polling timeout.
41
41
  const DEFAULT_PARSER_WAKEUP_TIMEOUT_MS = 30_000;
42
42
  const PARSER_WAKEUP_INTERVAL_MS = 2_000;
43
+ const KNOWN_PARSE_SUCCESS_CODES = new Set([0, 1, 2, 3, 4]);
43
44
  const DEFAULT_SOURCE_FROM = 'xique';
44
45
  const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
46
+ const API_KEY_MANAGEMENT_URL = 'https://xiquebiaoshu.com/account/api-keys';
45
47
  const DEFAULT_EXPORT_TEMPLATE_INDEX = 1;
46
48
  const DEFAULT_PAGE_SCOPE_CODE = 1;
47
49
  const DEFAULT_BASE = '0';
@@ -396,9 +398,32 @@ async function main() {
396
398
  case 'period-confirm':
397
399
  await handlePeriodConfirm(parsed, state);
398
400
  return;
401
+ case 'page-scope-status':
402
+ await handlePageScopeStatus(parsed, state);
403
+ return;
404
+ case 'page-scope-accept':
405
+ await handlePageScopeAccept(parsed, state);
406
+ return;
407
+ case 'commercial-status':
408
+ await handleCommercialStatus(parsed, state);
409
+ return;
410
+ case 'commercial-confirm':
411
+ await handleCommercialConfirm(parsed, state);
412
+ return;
413
+ case 'company-list':
414
+ await handleCompanyList(parsed, state);
415
+ return;
399
416
  case 'bidder-role':
400
417
  await handleBidderRole(parsed, state);
401
418
  return;
419
+ case 'basis-select':
420
+ case 'directory-basis':
421
+ await handleBasisSelection(parsed, state);
422
+ return;
423
+ case 'supplement':
424
+ case 'score-supplement':
425
+ await handleSupplement(parsed, state);
426
+ return;
402
427
  case 'outline':
403
428
  if (isOutlineAction(parsed, ['view', 'show'])) {
404
429
  await handleOutlineView(parsed, state);
@@ -468,7 +493,14 @@ Commands
468
493
  parse-status Query or wait for parse completion
469
494
  period-status Query engineering project period confirmation status
470
495
  period-confirm Save confirmed engineering project period
496
+ page-scope-status Check whether the current outline supports the selected page scope
497
+ page-scope-accept Accept the backend's recommended page scope
498
+ commercial-status Query commercial bid framework status
499
+ commercial-confirm Confirm use of the commercial default framework
500
+ company-list Query selectable bidder companies for commercial bids
471
501
  bidder-role Save the bidder's goods supply method for the current task
502
+ basis-select Save the parsing-stage outline basis for the current task
503
+ supplement Submit missing requirement / bill / score / directory content and continue the same task
472
504
  outline Pre-set task params and trigger outline generation
473
505
  outline view View the current outline without triggering generation
474
506
  outline update Update the current outline from an edited JSON file
@@ -500,7 +532,13 @@ Examples
500
532
  $env:XQ_API_KEY="xq_sk_xxx"
501
533
  xq-cli init --file D:\\bids\\tender.docx --wait
502
534
  xq-cli parse-status --cid <cid> --uuid <uuid> --wait
535
+ xq-cli commercial-status --cid <cid> --wait
536
+ xq-cli commercial-confirm --cid <cid> --wait
537
+ xq-cli company-list --json
503
538
  xq-cli bidder-role --cid <cid> --uuid <uuid> --bidder-role <1|2>
539
+ xq-cli basis-select --cid <cid> --uuid <uuid> --basis <scoreAndDir|scoreOnly|dirOnly> --wait
540
+ xq-cli supplement --cid <cid> --uuid <uuid> --type score --text "评分细则..." --wait
541
+ xq-cli supplement --cid <cid> --uuid <uuid> --type score --file D:\\bids\\score.docx --wait
504
542
  xq-cli choices
505
543
  xq-cli choices outline --json
506
544
  xq-cli gallery-status --json
@@ -540,13 +578,19 @@ Common options
540
578
  --timeout-sec <n> Poll timeout in seconds, default 600
541
579
  --interval-sec <n> Poll interval in seconds, default 10
542
580
  --reference-type <0|1|2> Outline reference choice: 0=merge, 1=format only, 2=score only
581
+ --basis <scoreAndDir|scoreOnly|dirOnly> Parsing-stage outline basis choice
543
582
  --bidder-role <1|2> Goods supply method: 1=self production, 2=procured supply
583
+ --supplement-type/--type <type> Missing content type: score, requirement, billOfQuantities, or directory
584
+ --text <content> Text content for a supplement (HTML is accepted)
585
+ --file <path> Supplement file path (.doc/.docx/.pdf/.xls/.xlsx)
586
+ --ignore Explicitly skip a skippable missing-content check
544
587
  --api-key <key> Save a user API key locally and verify it with /user/info
545
588
  --no-open Do not open the local login page automatically
546
589
  --interactive Open prompt menus for the current command
547
590
  --dry-run Preview an outline update without saving it
548
591
  --yes Confirm an outline update without an interactive prompt
549
592
  --allow-delete Allow an outline update that removes existing chapters
593
+ --check-id <id> Page-scope check ID returned by page-scope-status
550
594
  --ignore-short-requirement Follow the frontend ignore/continue path for requirement_judgement=0
551
595
  --ignore-missing-bill Follow the frontend ignore/continue path for billOfQuantities_judgement=0
552
596
 
@@ -770,6 +814,7 @@ async function handleInit(args, state) {
770
814
  cid,
771
815
  uuid: snapshot.uuid,
772
816
  title: snapshot.projectName || snapshot.title,
817
+ originType: snapshot.originType,
773
818
  files,
774
819
  fileTypes: fileMeta.types,
775
820
  parseResult: finalParse,
@@ -879,6 +924,216 @@ async function handlePeriodConfirm(args, state) {
879
924
  return payload;
880
925
  }
881
926
 
927
+ async function handlePageScopeStatus(args, state) {
928
+ const client = createAuthorizedClient(args, state);
929
+ const cid = resolveCid(args, state);
930
+ const result = await checkPageScopeBeforeWrite(client, state, cid, args);
931
+ const payload = {
932
+ command: 'page-scope-status',
933
+ cid,
934
+ ...result,
935
+ };
936
+ outputResult(args, payload, [
937
+ `page-scope check: cid=${cid}`,
938
+ `phase: ${result.phase}`,
939
+ `checkStatus: ${result.pageScopeResult?.checkStatus ?? '-'}`,
940
+ result.phase === 'confirmation_required'
941
+ ? `confirmation required: target=${result.pageScopeResult?.targetPageScope ?? '-'}, recommended=${result.pageScopeResult?.recommendedPageScope ?? '-'}`
942
+ : result.phase === 'passed'
943
+ ? 'selected page scope is supported by the current outline'
944
+ : 'page-scope response is not recognized; do not start content generation',
945
+ ]);
946
+ return payload;
947
+ }
948
+
949
+ async function handlePageScopeAccept(args, state) {
950
+ const client = createAuthorizedClient(args, state);
951
+ const cid = resolveCid(args, state);
952
+ const checkId = requiredOption(args, 'checkId');
953
+ const authoritativeState = await fetchAuthoritativeTaskState(client, cid);
954
+ assertContentGenerationAllowed(authoritativeState, '接受建议篇幅');
955
+ const response = await postJson(client, '/task/acceptRecommendedPageScope', {
956
+ cid,
957
+ checkId: String(checkId),
958
+ });
959
+ const payload = {
960
+ command: 'page-scope-accept',
961
+ cid,
962
+ checkId: String(checkId),
963
+ accepted: true,
964
+ response,
965
+ };
966
+ outputResult(args, payload, [
967
+ `recommended page scope accepted: cid=${cid}`,
968
+ `checkId: ${String(checkId)}`,
969
+ 'content generation has not been triggered; rerun write for the same cid.',
970
+ ]);
971
+ return payload;
972
+ }
973
+
974
+ async function handleCommercialStatus(args, state) {
975
+ const client = createAuthorizedClient(args, state);
976
+ const cid = resolveCid(args, state);
977
+ const taskResponse = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
978
+ const taskDetail = taskResponse?.data || {};
979
+ const originType = Number(taskDetail.originType);
980
+ if (originType !== 3) {
981
+ const payload = {
982
+ command: 'commercial-status',
983
+ cid,
984
+ originType: taskDetail.originType,
985
+ phase: 'not-commercial',
986
+ commercial: false,
987
+ taskDetail,
988
+ };
989
+ outputResult(args, payload, [
990
+ `commercial framework: cid=${cid}`,
991
+ 'commercial framework is not required for this task',
992
+ ]);
993
+ return payload;
994
+ }
995
+
996
+ const result = booleanOption(args, 'wait')
997
+ ? await pollCommercialFrameworkUntilReady(client, cid, args)
998
+ : await fetchCommercialFrameworkStatus(client, cid);
999
+ const payload = {
1000
+ command: 'commercial-status',
1001
+ cid,
1002
+ originType,
1003
+ commercial: true,
1004
+ ...result,
1005
+ };
1006
+ saveTaskSnapshot(state, cid, {
1007
+ originType,
1008
+ commercialFrameworkPhase: payload.phase,
1009
+ commercialFrameworkStatus: payload.data,
1010
+ commercialFrameworkCheckedAt: new Date().toISOString(),
1011
+ });
1012
+ outputResult(args, payload, [
1013
+ `commercial framework: cid=${cid}`,
1014
+ `phase: ${payload.phase}`,
1015
+ `task_status: ${payload.data?.task_status ?? '-'}`,
1016
+ `directories_ready: ${String(payload.data?.directories_ready ?? false)}`,
1017
+ payload.phase === 'commercial_default_template_required'
1018
+ ? 'confirmation required: use the default commercial bid framework'
1019
+ : payload.phase === 'commercial_framework_ready'
1020
+ ? 'commercial framework is ready; content generation may continue'
1021
+ : payload.phase === 'commercial_framework_failed'
1022
+ ? `commercial framework failed: ${payload.data?.status_msg || payload.data?.status_message || '-'}`
1023
+ : 'commercial framework is still running',
1024
+ ]);
1025
+ return payload;
1026
+ }
1027
+
1028
+ async function handleCommercialConfirm(args, state) {
1029
+ const client = createAuthorizedClient(args, state);
1030
+ const cid = resolveCid(args, state);
1031
+ const taskResponse = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
1032
+ if (Number(taskResponse?.data?.originType) !== 3) {
1033
+ throw new Error('当前任务不是商务标,无需确认默认标书框架。');
1034
+ }
1035
+ const response = await postJson(client, '/commercial/default-template/confirm', { cid });
1036
+ const status = booleanOption(args, 'wait')
1037
+ ? await pollCommercialFrameworkUntilReady(client, cid, args)
1038
+ : await fetchCommercialFrameworkStatus(client, cid);
1039
+ const payload = {
1040
+ command: 'commercial-confirm',
1041
+ cid,
1042
+ confirmationResponse: response,
1043
+ ...status,
1044
+ };
1045
+ saveTaskSnapshot(state, cid, {
1046
+ commercialFrameworkPhase: payload.phase,
1047
+ commercialFrameworkStatus: payload.data,
1048
+ commercialFrameworkConfirmedAt: new Date().toISOString(),
1049
+ });
1050
+ outputResult(args, payload, [
1051
+ `commercial framework confirmed: cid=${cid}`,
1052
+ `phase: ${payload.phase}`,
1053
+ payload.phase === 'commercial_framework_ready'
1054
+ ? 'commercial framework is ready; content generation may continue'
1055
+ : 'commercial framework confirmation accepted; continue polling the same cid',
1056
+ ]);
1057
+ return payload;
1058
+ }
1059
+
1060
+ async function handleCompanyList(args, state) {
1061
+ const client = createAuthorizedClient(args, state);
1062
+ const response = await postJson(client, '/xq/enterprise/company/list', {});
1063
+ const rawList = response?.data?.list ?? response?.data ?? [];
1064
+ const companies = (Array.isArray(rawList) ? rawList : [])
1065
+ .map(item => {
1066
+ if (!item || typeof item !== 'object') return null;
1067
+ const id = String(item.id ?? item.companyId ?? '').trim();
1068
+ const name = String(item.name ?? item.companyName ?? '').trim();
1069
+ if (!id || !name) return null;
1070
+ return {
1071
+ id,
1072
+ name,
1073
+ industry: String(item.industry ?? '').trim(),
1074
+ registeredCapital: String(item.registeredCapital ?? '').trim(),
1075
+ };
1076
+ })
1077
+ .filter(Boolean);
1078
+ const payload = {
1079
+ command: 'company-list',
1080
+ companies,
1081
+ total: companies.length,
1082
+ };
1083
+ outputResult(args, payload, [
1084
+ `bidder companies: ${companies.length}`,
1085
+ ...companies.map(company => `${company.id}: ${company.name}`),
1086
+ ]);
1087
+ return payload;
1088
+ }
1089
+
1090
+ async function fetchCommercialFrameworkStatus(client, cid) {
1091
+ const response = await getJson(client, '/commercial/task/status', { cid }, { retries: DEFAULT_RETRY_COUNT });
1092
+ const data = response?.data || {};
1093
+ return {
1094
+ phase: classifyCommercialFrameworkPhase(data),
1095
+ data,
1096
+ };
1097
+ }
1098
+
1099
+ async function pollCommercialFrameworkUntilReady(client, cid, args) {
1100
+ return pollUntil(async () => {
1101
+ const result = await fetchCommercialFrameworkStatus(client, cid);
1102
+ const terminal = [
1103
+ 'commercial_framework_ready',
1104
+ 'commercial_default_template_required',
1105
+ 'commercial_framework_failed',
1106
+ ].includes(result.phase);
1107
+ return {
1108
+ done: terminal,
1109
+ data: result,
1110
+ progressLabel: `phase=${result.phase}, task_status=${result.data?.task_status ?? '-'}, directories_ready=${String(result.data?.directories_ready ?? false)}`,
1111
+ };
1112
+ }, {
1113
+ intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
1114
+ timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
1115
+ quiet: booleanOption(args, 'json'),
1116
+ waitingText: 'waiting commercial framework',
1117
+ });
1118
+ }
1119
+
1120
+ function classifyCommercialFrameworkPhase(data = {}) {
1121
+ const taskStatus = Number(data.task_status);
1122
+ const fallback = data.fallback_confirm_required === true
1123
+ || data.fallback_confirm_required === 1
1124
+ || ['1', 'true'].includes(String(data.fallback_confirm_required || '').trim().toLowerCase())
1125
+ || taskStatus === 6;
1126
+ if (fallback) return 'commercial_default_template_required';
1127
+ // task_status=4 is downstream body processing; it does not mean the
1128
+ // commercial framework construction failed when directories are ready.
1129
+ if (taskStatus === 2) return 'commercial_framework_failed';
1130
+ const ready = [true, 1, '1', 'true'].includes(
1131
+ typeof data.directories_ready === 'string' ? data.directories_ready.trim().toLowerCase() : data.directories_ready,
1132
+ );
1133
+ if (ready) return 'commercial_framework_ready';
1134
+ return 'commercial_framework_pending';
1135
+ }
1136
+
882
1137
  async function handleBidderRole(args, state) {
883
1138
  const client = createAuthorizedClient(args, state);
884
1139
  const cid = resolveCid(args, state);
@@ -914,6 +1169,312 @@ async function handleBidderRole(args, state) {
914
1169
  return payload;
915
1170
  }
916
1171
 
1172
+ async function handleBasisSelection(args, state) {
1173
+ const client = createAuthorizedClient(args, state);
1174
+ const cid = resolveCid(args, state);
1175
+ const snapshot = await hydrateTaskSnapshot(client, state, cid);
1176
+ const uuid = stringOption(args, 'uuid', snapshot.uuid);
1177
+ if (!uuid) {
1178
+ throw new Error(`No uuid found for task ${cid}. Pass --uuid explicitly or run init first.`);
1179
+ }
1180
+
1181
+ const basis = normalizeDirectoryBasis(stringOptionAny(args, ['basis', 'directoryBasis']));
1182
+ if (!basis) {
1183
+ throw new Error('Option --basis expects scoreAndDir, scoreOnly, or dirOnly.');
1184
+ }
1185
+ if (snapshot.directoryBasisSubmittedAt) {
1186
+ if (snapshot.directoryBasis === basis) {
1187
+ const payload = {
1188
+ command: 'basis-select',
1189
+ cid,
1190
+ uuid,
1191
+ basis,
1192
+ alreadyApplied: true,
1193
+ duplicateRequestPrevented: true,
1194
+ };
1195
+ outputResult(args, payload, [
1196
+ `outline basis already applied: cid=${cid}, basis=${basis}`,
1197
+ 'duplicate request prevented; continue querying the same task.',
1198
+ ]);
1199
+ return payload;
1200
+ }
1201
+ throw new Error('该任务的解析阶段大纲依据已经提交,不能二次修改;请继续查询同一 cid。');
1202
+ }
1203
+
1204
+ const parseStatus = await fetchParseStatus(client, state, cid, uuid);
1205
+ const issues = collectParseIssues(parseStatus);
1206
+ if (!issues.includes('scoreAndDirectory_judgement')) {
1207
+ throw new Error('当前任务不在解析阶段大纲依据选择状态,请先查询 parse-status。');
1208
+ }
1209
+
1210
+ const response = await getJson(client, '/proxy/saveDirectoryBasis', {
1211
+ uuid,
1212
+ basis,
1213
+ });
1214
+ saveTaskSnapshot(state, cid, {
1215
+ uuid,
1216
+ directoryBasis: basis,
1217
+ directoryBasisSubmittedAt: new Date().toISOString(),
1218
+ directoryBasisIssue: 'scoreAndDirectory_judgement',
1219
+ });
1220
+
1221
+ const waitResult = booleanOption(args, 'wait')
1222
+ ? await waitAfterSupplement(client, state, cid, uuid, args, 'scoreAndDirectory_judgement')
1223
+ : null;
1224
+ const payload = {
1225
+ command: 'basis-select',
1226
+ cid,
1227
+ uuid,
1228
+ basis,
1229
+ response,
1230
+ waitResult,
1231
+ };
1232
+ outputResult(args, payload, [
1233
+ `outline basis submitted: cid=${cid}`,
1234
+ `basis: ${basis} (${describeDirectoryBasis(basis)})`,
1235
+ waitResult?.phase === 'ready'
1236
+ ? 'same task outline is ready for review.'
1237
+ : waitResult?.phase === 'analysis_ready'
1238
+ ? 'same task analysis is ready; confirm outline generation next.'
1239
+ : waitResult?.phase === 'basis_selection_required'
1240
+ ? 'same task still requires outline basis selection; do not submit a second choice.'
1241
+ : waitResult?.phase === 'reference_selection_required'
1242
+ ? 'same task now requires an explicit outline reference selection.'
1243
+ : waitResult?.phase === 'bidder_role_required'
1244
+ ? 'same task now requires a bidder supply-role selection.'
1245
+ : booleanOption(args, 'wait')
1246
+ ? 'same task is continuing; query status for the next stage.'
1247
+ : 'submission accepted; use parse-status or outline --wait to continue polling the same task.',
1248
+ ]);
1249
+ return payload;
1250
+ }
1251
+
1252
+ function normalizeDirectoryBasis(value) {
1253
+ const normalized = String(value || '').trim().toLowerCase();
1254
+ const aliases = {
1255
+ scoreanddir: 'scoreAndDir',
1256
+ scoreanddirectory: 'scoreAndDir',
1257
+ merge: 'scoreAndDir',
1258
+ '融合评分和参考目录': 'scoreAndDir',
1259
+ scoreonly: 'scoreOnly',
1260
+ score: 'scoreOnly',
1261
+ '仅依据评分': 'scoreOnly',
1262
+ dironly: 'dirOnly',
1263
+ directoryonly: 'dirOnly',
1264
+ format: 'dirOnly',
1265
+ '仅依据参考目录': 'dirOnly',
1266
+ };
1267
+ return aliases[normalized] || aliases[String(value || '').trim()] || '';
1268
+ }
1269
+
1270
+ function describeDirectoryBasis(value) {
1271
+ return {
1272
+ scoreAndDir: '融合评分和参考目录',
1273
+ scoreOnly: '仅依据评分',
1274
+ dirOnly: '仅依据参考目录',
1275
+ }[value] || value;
1276
+ }
1277
+
1278
+ async function handleSupplement(args, state) {
1279
+ const client = createAuthorizedClient(args, state);
1280
+ const cid = resolveCid(args, state);
1281
+ const snapshot = await hydrateTaskSnapshot(client, state, cid);
1282
+ const uuid = stringOption(args, 'uuid', snapshot.uuid);
1283
+ if (!uuid) {
1284
+ throw new Error(`No uuid found for task ${cid}. Pass --uuid explicitly or run init first.`);
1285
+ }
1286
+
1287
+ const issue = normalizeSupplementIssue(stringOptionAny(args, ['issue', 'supplementIssue']));
1288
+ const type = normalizeSupplementType(stringOptionAny(args, ['supplementType', 'type']), issue);
1289
+ const filePath = stringOption(args, 'file', stringOption(args, 'scoreFile'));
1290
+ const text = stringOptionAny(args, ['text', 'content', 'score', 'requirement', 'directory']);
1291
+ const ignore = booleanOption(args, 'ignore');
1292
+ if (filePath && text) {
1293
+ throw new Error('补充内容只能选择 --text 或 --file 其中一种。');
1294
+ }
1295
+ if (!ignore && !filePath && !text) {
1296
+ throw new Error('补充内容需要 --text <内容> 或 --file <路径>;跳过可补充项时才使用 --ignore。');
1297
+ }
1298
+ if (ignore && (filePath || text)) {
1299
+ throw new Error('--ignore 不能和 --text/--file 同时使用。');
1300
+ }
1301
+ if (!ignore && type === 'billOfQuantities' && text && !filePath) {
1302
+ throw new Error('工程量清单请使用 --file 上传,网页端不支持直接输入文本。');
1303
+ }
1304
+ if (!ignore && type === 'directory' && filePath) {
1305
+ throw new Error('技术方案目录要求请使用 --text 输入,网页端不支持以文件补充。');
1306
+ }
1307
+ if (filePath) {
1308
+ ensureFileExists(filePath);
1309
+ }
1310
+
1311
+ const currentIssues = collectParseIssues(await fetchParseStatus(client, state, cid, uuid));
1312
+ const targetIssue = issue || currentIssues.find(item => issueType(item) === type) || currentIssues[0] || '';
1313
+ if (ignore && (!targetIssue || !AUTO_IGNORE_ISSUE_CONFIG[targetIssue])) {
1314
+ throw new Error('当前任务没有可忽略的对应解析问题;请传 --issue <问题字段>,或使用评分/需求补充内容。');
1315
+ }
1316
+ const payload = ignore
1317
+ ? AUTO_IGNORE_ISSUE_CONFIG[targetIssue].buildPayload(uuid)
1318
+ : filePath
1319
+ ? null
1320
+ : buildSupplementPayload(uuid, type, text);
1321
+ const response = filePath
1322
+ ? await submitSupplementFile(client, uuid, type, filePath)
1323
+ : await submitIntermediateChoice(client, payload);
1324
+
1325
+ saveTaskSnapshot(state, cid, {
1326
+ uuid,
1327
+ supplementIssue: targetIssue,
1328
+ supplementType: type,
1329
+ supplementSubmittedAt: new Date().toISOString(),
1330
+ ...(ignore ? { [targetIssue + '_ignored']: true } : {}),
1331
+ });
1332
+
1333
+ const waitResult = booleanOption(args, 'wait')
1334
+ ? await waitAfterSupplement(client, state, cid, uuid, args, targetIssue)
1335
+ : null;
1336
+ const resultPayload = {
1337
+ command: 'supplement',
1338
+ cid,
1339
+ uuid,
1340
+ issue: targetIssue || null,
1341
+ type,
1342
+ mode: ignore ? 'ignore' : filePath ? 'file' : 'text',
1343
+ file: filePath || null,
1344
+ response,
1345
+ waitResult,
1346
+ };
1347
+ outputResult(args, resultPayload, [
1348
+ `supplement submitted: cid=${cid}`,
1349
+ `type: ${type}`,
1350
+ `mode: ${resultPayload.mode}`,
1351
+ targetIssue ? `issue: ${targetIssue}` : '',
1352
+ waitResult?.phase === 'ready'
1353
+ ? 'same task outline is ready for review.'
1354
+ : waitResult?.phase === 'analysis_ready'
1355
+ ? 'same task analysis is ready; confirm outline generation next.'
1356
+ : waitResult?.phase === 'basis_selection_required'
1357
+ ? 'same task now requires an explicit parsing-stage outline basis selection.'
1358
+ : waitResult?.phase === 'reference_selection_required'
1359
+ ? 'same task now requires an explicit outline reference selection.'
1360
+ : waitResult?.phase === 'bidder_role_required'
1361
+ ? 'same task now requires a bidder supply-role selection.'
1362
+ : booleanOption(args, 'wait')
1363
+ ? 'same task is continuing; query status for the next stage.'
1364
+ : 'submission accepted; use --wait to continue polling the same task.',
1365
+ ].filter(Boolean));
1366
+ return resultPayload;
1367
+ }
1368
+
1369
+ function normalizeSupplementIssue(value) {
1370
+ if (!value) return '';
1371
+ const aliases = {
1372
+ requirement: 'requirement_exist',
1373
+ requirement_short: 'requirement_judgement',
1374
+ bill: 'billOfQuantities_judgement',
1375
+ billOfQuantities: 'billOfQuantities_judgement',
1376
+ score: 'score_judgement',
1377
+ directory: 'directory_judgement',
1378
+ basis: 'scoreAndDirectory_judgement',
1379
+ bidder: 'bidderIdentity_judgement',
1380
+ };
1381
+ const normalized = String(value).trim();
1382
+ return aliases[normalized] || normalized;
1383
+ }
1384
+
1385
+ function normalizeSupplementType(value, issue = '') {
1386
+ const normalized = String(value || '').trim().toLowerCase();
1387
+ const aliases = {
1388
+ score: 'score',
1389
+ rating: 'score',
1390
+ requirement: 'requirement',
1391
+ demand: 'requirement',
1392
+ bill: 'billOfQuantities',
1393
+ billofquantities: 'billOfQuantities',
1394
+ directory: 'directory',
1395
+ };
1396
+ if (normalized && aliases[normalized]) return aliases[normalized];
1397
+ if (issue) return issueType(issue);
1398
+ throw new Error('补充类型必须是 score、requirement、billOfQuantities 或 directory。');
1399
+ }
1400
+
1401
+ function issueType(issue) {
1402
+ if (issue === 'score_judgement') return 'score';
1403
+ if (issue === 'billOfQuantities_judgement') return 'billOfQuantities';
1404
+ if (issue === 'directory_judgement') return 'directory';
1405
+ return 'requirement';
1406
+ }
1407
+
1408
+ function buildSupplementPayload(uuid, type, text) {
1409
+ const payload = {
1410
+ uuid,
1411
+ requirement: '',
1412
+ score: '',
1413
+ type,
1414
+ };
1415
+ if (type === 'score') payload.score = String(text || '').trim();
1416
+ else if (type === 'directory') {
1417
+ payload.directory = String(text || '').trim();
1418
+ payload.modify = 1;
1419
+ } else payload.requirement = String(text || '').trim();
1420
+ if (!payload.score && !payload.requirement && !payload.directory) {
1421
+ throw new Error('补充文本不能为空。');
1422
+ }
1423
+ return payload;
1424
+ }
1425
+
1426
+ async function submitSupplementFile(client, uuid, type, filePath) {
1427
+ const formData = new FormData();
1428
+ formData.append('uuid', uuid);
1429
+ formData.append('type', type);
1430
+ formData.append('files', fs.createReadStream(filePath), path.basename(filePath));
1431
+ return postForm(client, '/proxy/formatFileExtend/upload', formData);
1432
+ }
1433
+
1434
+ async function waitAfterSupplement(client, state, cid, uuid, args, targetIssue = '') {
1435
+ await waitForSupplementParseResolution(client, state, cid, uuid, args, normalizeSupplementIssue(targetIssue));
1436
+ const task = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
1437
+ // The mode was explicitly confirmed when the task was created. Prefer an
1438
+ // explicit continuation argument, then that durable local snapshot, and
1439
+ // only use the backend mirror as a fallback because older deployments may
1440
+ // briefly omit or return a stale planMode after a supplement request.
1441
+ const planning = Number(
1442
+ args?.planMode
1443
+ ?? args?.plan_mode
1444
+ ?? state.tasks?.[cid]?.planMode
1445
+ ?? task?.data?.planMode
1446
+ ?? task?.data?.plan_mode,
1447
+ ) === 1;
1448
+ return planning
1449
+ ? waitForPlanningAnalysisReady(client, state, cid, uuid, args)
1450
+ : waitForOutlineReady(client, state, cid, uuid, args);
1451
+ }
1452
+
1453
+ async function waitForSupplementParseResolution(client, state, cid, uuid, args, targetIssue = '') {
1454
+ if (!targetIssue) return;
1455
+ await pollUntil(async () => {
1456
+ const result = await fetchParseStatus(client, state, cid, uuid);
1457
+ const issues = collectParseIssues(result);
1458
+ // The parsing-stage basis choice is acknowledged by a dedicated
1459
+ // backend endpoint. Do not treat success=1/2 as resolved while the
1460
+ // same judgement flag is still zero; that would make the continuation
1461
+ // worker report a false next stage and could expose a duplicate card.
1462
+ const resolved = !issues.includes(targetIssue)
1463
+ || (targetIssue !== 'scoreAndDirectory_judgement'
1464
+ && (Number(result.success) === 1 || Number(result.success) === 2));
1465
+ return {
1466
+ done: resolved,
1467
+ data: result,
1468
+ progressLabel: `success=${result.success}, pending=${issues.join(',') || 'none'}`,
1469
+ };
1470
+ }, {
1471
+ intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_PARSE_INTERVAL_MS / 1000)),
1472
+ timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
1473
+ quiet: booleanOption(args, 'json'),
1474
+ waitingText: 'waiting supplement parse result',
1475
+ });
1476
+ }
1477
+
917
1478
  function handleChoices(args) {
918
1479
  const scope = normalizeChoicesScope(args._[1] || 'all');
919
1480
  const payload = buildChoicesPayload(scope);
@@ -965,6 +1526,33 @@ async function handleWizard(args, state) {
965
1526
  interactive: false,
966
1527
  }, state);
967
1528
 
1529
+ // A wizard is still a staged workflow. If outline generation pauses for
1530
+ // any user action, stop here and return the durable task identifiers so
1531
+ // the caller can resume the same cid instead of starting body generation
1532
+ // with an incomplete outline.
1533
+ const outlinePhase = outlineResult?.waitResult?.phase || outlineResult?.phase || '';
1534
+ const outlineReady = outlinePhase === 'ready'
1535
+ || Boolean(outlineResult?.waitResult?.outlineDetail)
1536
+ || Boolean(outlineResult?.waitResult?.contentDetail);
1537
+ if (!outlineReady) {
1538
+ const resultPayload = {
1539
+ command: 'wizard',
1540
+ cid,
1541
+ initResult,
1542
+ outlineResult,
1543
+ writeResult: null,
1544
+ exportResult: null,
1545
+ paused: true,
1546
+ phase: outlinePhase || 'outline_pending',
1547
+ };
1548
+ outputResult(args, resultPayload, [
1549
+ `wizard paused: cid=${cid}`,
1550
+ `phase: ${outlinePhase || 'outline_pending'}`,
1551
+ 'resume the required confirmation or supplement on the same cid before generating content.',
1552
+ ]);
1553
+ return resultPayload;
1554
+ }
1555
+
968
1556
  const writeResult = await handleWrite({
969
1557
  ...wizardArgs,
970
1558
  _: ['write'],
@@ -973,6 +1561,29 @@ async function handleWizard(args, state) {
973
1561
  interactive: false,
974
1562
  }, state);
975
1563
 
1564
+ const writeReady = Boolean(writeResult?.waitResult?.task_status)
1565
+ || Boolean(writeResult?.waitResult?.content_portion_status)
1566
+ || Boolean(writeResult?.writePreparation?.viewStep)
1567
+ || writeResult?.directoryWaitResult?.phase === 'ready';
1568
+ if (!writeReady) {
1569
+ const resultPayload = {
1570
+ command: 'wizard',
1571
+ cid,
1572
+ initResult,
1573
+ outlineResult,
1574
+ writeResult: writeResult || null,
1575
+ exportResult: null,
1576
+ paused: true,
1577
+ phase: writeResult?.directoryWaitResult?.phase || 'content_pending',
1578
+ };
1579
+ outputResult(args, resultPayload, [
1580
+ `wizard paused before export: cid=${cid}`,
1581
+ `phase: ${resultPayload.phase}`,
1582
+ 'resume the same task after its directory or content stage is ready.',
1583
+ ]);
1584
+ return resultPayload;
1585
+ }
1586
+
976
1587
  const exportResult = await handleExport({
977
1588
  ...wizardArgs,
978
1589
  _: ['export'],
@@ -1494,9 +2105,18 @@ async function handleOutlineUnlocked(args, state) {
1494
2105
  for (const hint of collectIssueHints(waitResult.issues, args)) {
1495
2106
  summaryLines.push(hint);
1496
2107
  }
2108
+ } else if (waitResult?.phase === 'basis_selection_required') {
2109
+ summaryLines.push('outline blocked: parsing-stage outline basis selection required');
2110
+ summaryLines.push('hint: run xq-cli basis-select --cid <cid> --uuid <uuid> --basis <scoreAndDir|scoreOnly|dirOnly> --wait.');
1497
2111
  } else if (waitResult?.phase === 'reference_selection_required') {
1498
2112
  summaryLines.push('outline blocked: reference selection required');
1499
2113
  summaryLines.push('hint: rerun with --wait in an interactive terminal, or pass --reference-type <0|1|2>.');
2114
+ } else if (waitResult?.phase === 'bidder_role_required') {
2115
+ summaryLines.push('outline blocked: bidder role selection required');
2116
+ summaryLines.push('hint: run xq-cli bidder-role --cid <cid> --uuid <uuid> --bidder-role <1|2>, then continue polling the same task.');
2117
+ } else if (waitResult?.phase === 'unsupported_intermediate_state') {
2118
+ summaryLines.push(`outline blocked: unsupported backend interaction (${waitResult.issues.join(', ')})`);
2119
+ summaryLines.push('hint: upgrade xq-cli/WorkBuddy to a version that supports this backend interaction; the same cid remains available for recovery.');
1500
2120
  } else if (waitResult?.phase === 'failed') {
1501
2121
  summaryLines.push(`outline failed: task_status=${waitResult.taskDetail?.task_status ?? '-'}`);
1502
2122
  } else {
@@ -1843,6 +2463,30 @@ async function handleWriteUnlocked(args, state) {
1843
2463
  assertContentGenerationAllowed(contentDetail.data, '生成正文');
1844
2464
  saveTaskSnapshot(state, cid, pickTaskSnapshot(contentDetail.data));
1845
2465
 
2466
+ const pageScopeResult = await checkPageScopeBeforeWrite(client, state, cid, args);
2467
+ if (pageScopeResult.phase !== 'passed') {
2468
+ const blockedPayload = {
2469
+ command: 'write',
2470
+ cid,
2471
+ type,
2472
+ directoryWaitResult,
2473
+ pageScopeResult,
2474
+ writePreparation: null,
2475
+ triggerResult: null,
2476
+ waitResult: null,
2477
+ };
2478
+ outputResult(args, blockedPayload, [
2479
+ `content generation blocked: cid=${cid}`,
2480
+ pageScopeResult.phase === 'confirmation_required'
2481
+ ? 'page-scope confirmation required before charging or starting content'
2482
+ : 'page-scope check returned an unsupported state; content was not started',
2483
+ pageScopeResult.pageScopeResult?.checkId
2484
+ ? `checkId: ${pageScopeResult.pageScopeResult.checkId}`
2485
+ : '',
2486
+ ].filter(Boolean));
2487
+ return blockedPayload;
2488
+ }
2489
+
1846
2490
  const writePreparation = await prepareContentGeneration(client, state, cid, contentDetail, args);
1847
2491
  let triggerResult = null;
1848
2492
  if (writePreparation.shouldTrigger) {
@@ -1858,6 +2502,15 @@ async function handleWriteUnlocked(args, state) {
1858
2502
  if (booleanOption(args, 'wait')) {
1859
2503
  status = await pollContentUntilReady(client, state, cid, args);
1860
2504
  }
2505
+ if (status?.phase === 'failed') {
2506
+ throw new Error(
2507
+ '[CONTENT_GENERATION_FAILED] 喜鹊后台报告正文生成失败,task_status='
2508
+ + String(status.task_status ?? '-')
2509
+ + ',content_portion_status='
2510
+ + String(status.content_portion_status ?? '-')
2511
+ + ';已停止等待和导出,请查询同一任务状态。',
2512
+ );
2513
+ }
1861
2514
  if (status && status.task_status !== undefined) {
1862
2515
  try {
1863
2516
  assertContentCompleted(status, '确认正文完成');
@@ -1875,6 +2528,7 @@ async function handleWriteUnlocked(args, state) {
1875
2528
  cid,
1876
2529
  type,
1877
2530
  directoryWaitResult,
2531
+ pageScopeResult,
1878
2532
  writePreparation,
1879
2533
  triggerResult,
1880
2534
  waitResult: status,
@@ -2013,6 +2667,7 @@ async function handleExport(args, state) {
2013
2667
 
2014
2668
  async function hydrateTaskSnapshot(client, state, cid) {
2015
2669
  const cached = state.tasks?.[cid] || {};
2670
+ const confirmedPlanMode = cached.planMode;
2016
2671
  let detailData = {};
2017
2672
  try {
2018
2673
  const detail = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
@@ -2027,6 +2682,12 @@ async function hydrateTaskSnapshot(client, state, cid) {
2027
2682
  ...detailData,
2028
2683
  cid,
2029
2684
  });
2685
+ // planMode is immutable for a generation run. Preserve the mode already
2686
+ // recorded with the confirmed CLI configuration when a legacy backend
2687
+ // mirror is missing or temporarily stale during continuation.
2688
+ if (confirmedPlanMode !== undefined && confirmedPlanMode !== null && confirmedPlanMode !== '') {
2689
+ snapshot.planMode = confirmedPlanMode;
2690
+ }
2030
2691
  saveTaskSnapshot(state, cid, snapshot);
2031
2692
  return snapshot;
2032
2693
  }
@@ -2063,6 +2724,8 @@ function buildOutlineParams(snapshot, args) {
2063
2724
  const plan = stringOption(args, 'plan', snapshot.plan || '');
2064
2725
  const requirement = stringOption(args, 'requirement', snapshot.requirement || '');
2065
2726
  const rating = stringOption(args, 'rating', snapshot.rating || '');
2727
+ const bidderCompanyId = stringOption(args, 'bidderCompanyId', snapshot.bidderCompanyId || '');
2728
+ const bidderCompanyName = stringOption(args, 'bidderCompanyName', snapshot.bidderCompanyName || '');
2066
2729
  const blindGenerationMode = enumOption(
2067
2730
  args,
2068
2731
  ['blindGenerationMode', 'anonymousBid'],
@@ -2076,6 +2739,9 @@ function buildOutlineParams(snapshot, args) {
2076
2739
  if (!title) {
2077
2740
  throw new Error('Missing title for outline generation.');
2078
2741
  }
2742
+ if (Number(snapshot.originType) === 3 && (!bidderCompanyId || !bidderCompanyName)) {
2743
+ throw new Error('commercial_bidder_company_required: 商务标必须先选择投标公司。');
2744
+ }
2079
2745
 
2080
2746
  return compactObject({
2081
2747
  cid: snapshot.cid,
@@ -2106,6 +2772,10 @@ function buildOutlineParams(snapshot, args) {
2106
2772
  pageMarginSetting: snapshot.pageMarginSetting,
2107
2773
  multiBidId: stringOption(args, 'multiBidId', snapshot.multiBidId),
2108
2774
  multiBidType: stringOption(args, 'multiBidType', snapshot.multiBidType),
2775
+ ...(bidderCompanyId || bidderCompanyName ? {
2776
+ bidderCompanyId,
2777
+ bidderCompanyName,
2778
+ } : {}),
2109
2779
  planMode: enumOption(args, 'planMode', PLAN_MODE_OPTIONS, snapshot.planMode ?? 0),
2110
2780
  epcEngineerType: normalizeEpcEngineerType(stringOption(args, 'epcEngineerType', snapshot.epcEngineerType)),
2111
2781
  blindBidConfirm: {
@@ -2522,8 +3192,15 @@ function buildFileMetadata(files, args) {
2522
3192
  async function pollParseUntilReady(client, state, cid, uuid, args) {
2523
3193
  return pollUntil(async () => {
2524
3194
  const result = await fetchParseStatus(client, state, cid, uuid);
3195
+ const success = Number(result.success);
3196
+ if (!KNOWN_PARSE_SUCCESS_CODES.has(success)) {
3197
+ throw new Error(
3198
+ `[UNSUPPORTED_PARSE_STATE] 后端返回了未支持的解析状态 success=${String(result.success)},` +
3199
+ '已停止轮询;请升级 xq-cli/WorkBuddy 后使用同一任务恢复。',
3200
+ );
3201
+ }
2525
3202
  return {
2526
- done: isParseReadyForNextStep(result.success),
3203
+ done: isParseReadyForNextStep(success),
2527
3204
  data: result,
2528
3205
  progressLabel: `success=${result.success}`,
2529
3206
  };
@@ -2603,6 +3280,7 @@ async function waitForOutlineReady(client, state, cid, uuid, args, initialSubmit
2603
3280
  const planModePhase = normalizePlanModePhase(taskDetail.data?.plan_mode_phase);
2604
3281
  const referenceData = referenceStatus?.data || {};
2605
3282
  const referencePhase = normalizeReferenceStatus(referenceData.status);
3283
+ let parseReadyForOutline = !uuid;
2606
3284
  if (referencePhase !== null) {
2607
3285
  saveTaskSnapshot(state, cid, {
2608
3286
  referenceStatus: referencePhase,
@@ -2640,60 +3318,82 @@ async function waitForOutlineReady(client, state, cid, uuid, args, initialSubmit
2640
3318
  };
2641
3319
  }
2642
3320
 
2643
- if (hasOutline) {
2644
- return {
2645
- done: true,
2646
- data: {
2647
- phase: 'ready',
2648
- taskDetail: taskDetail.data,
2649
- outlineDetail: outlineData,
2650
- },
2651
- progressLabel: `rootChapters=${countOutlineRoots({ outlineDetail: outlineData, taskDetail: taskDetail.data })}`,
2652
- };
2653
- }
2654
-
2655
- if (referencePhase === 1) {
2656
- if (submittedReferenceType !== null) {
3321
+ if (uuid) {
3322
+ const parseStatus = await fetchParseStatus(client, state, cid, uuid);
3323
+ const parseSuccess = Number(parseStatus.success);
3324
+ if (!KNOWN_PARSE_SUCCESS_CODES.has(parseSuccess)) {
2657
3325
  return {
2658
- done: false,
2659
- data: null,
2660
- progressLabel: `reference_status=1, referenceType=${submittedReferenceType}, waiting confirmation`,
3326
+ done: true,
3327
+ data: {
3328
+ phase: 'unsupported_parse_state',
3329
+ parseStatus,
3330
+ taskDetail: taskDetail.data,
3331
+ outlineDetail: outlineData,
3332
+ },
3333
+ progressLabel: `unsupported_parse_state=${String(parseStatus.success)}`,
2661
3334
  };
2662
3335
  }
3336
+ const issues = collectParseIssues(parseStatus);
3337
+ const unsupportedIssues = collectUnsupportedParseIssues(parseStatus);
2663
3338
 
2664
- const selection = await resolveOutlineReferenceSelection(args, referenceData);
2665
- if (!selection.ready) {
3339
+ if (parseSuccess === 4 && unsupportedIssues.length > 0) {
2666
3340
  return {
2667
3341
  done: true,
2668
3342
  data: {
2669
- phase: 'reference_selection_required',
3343
+ phase: 'unsupported_intermediate_state',
3344
+ issues: unsupportedIssues,
3345
+ parseStatus,
2670
3346
  taskDetail: taskDetail.data,
2671
3347
  outlineDetail: outlineData,
2672
- referenceDetail: referenceData,
2673
- options: buildOutlineReferenceChoices(referenceData),
2674
3348
  },
2675
- progressLabel: 'reference_selection_required',
3349
+ progressLabel: `unsupported_intermediate_state=${unsupportedIssues.join(',')}`,
2676
3350
  };
2677
3351
  }
2678
3352
 
2679
- await submitOutlineReferenceType(client, cid, selection.referenceType);
2680
- submittedReferenceType = selection.referenceType;
2681
- saveTaskSnapshot(state, cid, {
2682
- outlineReferenceType: selection.referenceType,
2683
- });
2684
- return {
2685
- done: false,
2686
- data: null,
2687
- progressLabel: `referenceType=${selection.referenceType} submitted, waiting outline`,
2688
- };
2689
- }
2690
-
2691
- if (uuid) {
2692
- const parseStatus = await fetchParseStatus(client, state, cid, uuid);
2693
- const parseSuccess = Number(parseStatus.success);
2694
- const issues = collectParseIssues(parseStatus);
2695
-
2696
3353
  if (parseSuccess === 4 && issues.length > 0) {
3354
+ const continuation = classifyParseContinuation(issues);
3355
+ if (continuation === 'basis_selection_required') {
3356
+ return {
3357
+ done: true,
3358
+ data: {
3359
+ phase: continuation,
3360
+ issues,
3361
+ parseStatus,
3362
+ taskDetail: taskDetail.data,
3363
+ outlineDetail: outlineData,
3364
+ },
3365
+ progressLabel: 'basis_selection_required from parse status',
3366
+ };
3367
+ }
3368
+ if (continuation === 'reference_selection_required') {
3369
+ return {
3370
+ done: true,
3371
+ data: {
3372
+ phase: continuation,
3373
+ issues,
3374
+ parseStatus,
3375
+ taskDetail: taskDetail.data,
3376
+ outlineDetail: outlineData,
3377
+ referenceDetail: referenceData,
3378
+ options: buildOutlineReferenceChoices(referenceData),
3379
+ },
3380
+ progressLabel: 'reference_selection_required from parse status',
3381
+ };
3382
+ }
3383
+ if (continuation === 'bidder_role_required') {
3384
+ return {
3385
+ done: true,
3386
+ data: {
3387
+ phase: continuation,
3388
+ issues,
3389
+ parseStatus,
3390
+ taskDetail: taskDetail.data,
3391
+ outlineDetail: outlineData,
3392
+ bidderRoleIssue: 'bidderIdentity_judgement',
3393
+ },
3394
+ progressLabel: 'bidder_role_required from parse status',
3395
+ };
3396
+ }
2697
3397
  const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
2698
3398
 
2699
3399
  if (autoIgnore) {
@@ -2719,10 +3419,69 @@ async function waitForOutlineReady(client, state, cid, uuid, args, initialSubmit
2719
3419
  };
2720
3420
  }
2721
3421
 
3422
+ // A stale outline response can outlive the current parser state.
3423
+ // Only allow it to finish the wait after parsing is in a known
3424
+ // ready state and all human gates have been cleared.
3425
+ parseReadyForOutline = parseSuccess === 1
3426
+ || parseSuccess === 2
3427
+ || (parseSuccess === 4 && issues.length === 0);
3428
+
3429
+ if (!parseReadyForOutline) {
3430
+ return {
3431
+ done: false,
3432
+ data: null,
3433
+ progressLabel: `success=${parseSuccess}, task_status=${taskDetail.data?.task_status ?? '-'}, plan_mode_phase=${formatPlanModePhase(planModePhase)}, directory_status=${taskDetail.data?.directory_portion_status ?? '-'}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
3434
+ };
3435
+ }
3436
+ }
3437
+
3438
+ if (referencePhase === 1) {
3439
+ if (submittedReferenceType !== null) {
3440
+ return {
3441
+ done: false,
3442
+ data: null,
3443
+ progressLabel: `reference_status=1, referenceType=${submittedReferenceType}, waiting confirmation`,
3444
+ };
3445
+ }
3446
+
3447
+ const selection = await resolveOutlineReferenceSelection(args, referenceData);
3448
+ if (!selection.ready) {
3449
+ return {
3450
+ done: true,
3451
+ data: {
3452
+ phase: 'reference_selection_required',
3453
+ taskDetail: taskDetail.data,
3454
+ outlineDetail: outlineData,
3455
+ referenceDetail: referenceData,
3456
+ options: buildOutlineReferenceChoices(referenceData),
3457
+ },
3458
+ progressLabel: 'reference_selection_required',
3459
+ };
3460
+ }
3461
+
3462
+ await submitOutlineReferenceType(client, cid, selection.referenceType);
3463
+ submittedReferenceType = selection.referenceType;
3464
+ saveTaskSnapshot(state, cid, {
3465
+ outlineReferenceType: submittedReferenceType,
3466
+ });
2722
3467
  return {
2723
3468
  done: false,
2724
3469
  data: null,
2725
- progressLabel: `success=${parseSuccess}, task_status=${taskDetail.data?.task_status ?? '-'}, plan_mode_phase=${formatPlanModePhase(planModePhase)}, directory_status=${taskDetail.data?.directory_portion_status ?? '-'}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
3470
+ progressLabel: `referenceType=${selection.referenceType} submitted, waiting outline`,
3471
+ };
3472
+ }
3473
+
3474
+ // checkReference status=0 means the backend is still preparing the
3475
+ // outline basis. A stale outline response must not bypass that gate.
3476
+ if (hasOutline && parseReadyForOutline && referencePhase !== 0) {
3477
+ return {
3478
+ done: true,
3479
+ data: {
3480
+ phase: 'ready',
3481
+ taskDetail: taskDetail.data,
3482
+ outlineDetail: outlineData,
3483
+ },
3484
+ progressLabel: `rootChapters=${countOutlineRoots({ outlineDetail: outlineData, taskDetail: taskDetail.data })}`,
2726
3485
  };
2727
3486
  }
2728
3487
 
@@ -2759,13 +3518,77 @@ async function waitForPlanningAnalysisReady(client, state, cid, uuid, args) {
2759
3518
  ? await fetchParseStatus(client, state, cid, uuid)
2760
3519
  : { success: 1 };
2761
3520
  const parseSuccess = Number(parseStatus.success);
3521
+ if (!KNOWN_PARSE_SUCCESS_CODES.has(parseSuccess)) {
3522
+ return {
3523
+ done: true,
3524
+ data: {
3525
+ phase: 'unsupported_parse_state',
3526
+ parseStatus,
3527
+ taskDetail,
3528
+ },
3529
+ progressLabel: `unsupported_parse_state=${String(parseStatus.success)}`,
3530
+ };
3531
+ }
2762
3532
  if (parseSuccess === 2) {
2763
3533
  throw new Error(
2764
3534
  '[TASK_PARSE_NOT_ADVANCED] 规划模式仍处于等待标包/EPC选择(success=2),未进入解析阶段,已停止等待。',
2765
3535
  );
2766
3536
  }
2767
3537
  const issues = collectParseIssues(parseStatus);
3538
+ const unsupportedIssues = collectUnsupportedParseIssues(parseStatus);
3539
+ if (parseSuccess === 4 && unsupportedIssues.length > 0) {
3540
+ return {
3541
+ done: true,
3542
+ data: {
3543
+ phase: 'unsupported_intermediate_state',
3544
+ issues: unsupportedIssues,
3545
+ parseStatus,
3546
+ taskDetail,
3547
+ },
3548
+ progressLabel: `unsupported_intermediate_state=${unsupportedIssues.join(',')}`,
3549
+ };
3550
+ }
2768
3551
  if (parseSuccess === 4 && issues.length > 0) {
3552
+ const continuation = classifyParseContinuation(issues);
3553
+ if (continuation === 'basis_selection_required') {
3554
+ return {
3555
+ done: true,
3556
+ data: {
3557
+ phase: continuation,
3558
+ issues,
3559
+ parseStatus,
3560
+ taskDetail,
3561
+ },
3562
+ progressLabel: 'basis_selection_required from parse status',
3563
+ };
3564
+ }
3565
+ if (continuation === 'reference_selection_required') {
3566
+ return {
3567
+ done: true,
3568
+ data: {
3569
+ phase: continuation,
3570
+ issues,
3571
+ parseStatus,
3572
+ taskDetail,
3573
+ referenceDetail: {},
3574
+ options: [],
3575
+ },
3576
+ progressLabel: 'reference_selection_required from parse status',
3577
+ };
3578
+ }
3579
+ if (continuation === 'bidder_role_required') {
3580
+ return {
3581
+ done: true,
3582
+ data: {
3583
+ phase: continuation,
3584
+ issues,
3585
+ parseStatus,
3586
+ taskDetail,
3587
+ bidderRoleIssue: 'bidderIdentity_judgement',
3588
+ },
3589
+ progressLabel: 'bidder_role_required from parse status',
3590
+ };
3591
+ }
2769
3592
  const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
2770
3593
  if (autoIgnore) {
2771
3594
  await submitIntermediateChoice(client, autoIgnore.payload);
@@ -2832,6 +3655,18 @@ async function pollContentUntilReady(client, state, cid, args) {
2832
3655
  const totalChapter = numberOrDefault(contentData.totalChapter, 0);
2833
3656
  const percentage = numberOrDefault(contentData.percentage, 0);
2834
3657
 
3658
+ if (taskStatus === 7 || contentPortionStatus === 7) {
3659
+ return {
3660
+ done: true,
3661
+ data: {
3662
+ ...contentData,
3663
+ phase: 'failed',
3664
+ failureCode: 'CONTENT_GENERATION_FAILED',
3665
+ },
3666
+ progressLabel: `task_status=${taskStatus}, content_portion_status=${contentPortionStatus}`,
3667
+ };
3668
+ }
3669
+
2835
3670
  if (isContentComplete(contentData)) {
2836
3671
  return {
2837
3672
  done: true,
@@ -2884,6 +3719,79 @@ async function fetchOutlineDetail(client, cid) {
2884
3719
  return getJson(client, '/c/getOutLineDetail', { cid }, { retries: DEFAULT_RETRY_COUNT });
2885
3720
  }
2886
3721
 
3722
+ async function checkPageScopeBeforeWrite(client, state, cid, args = {}) {
3723
+ const outlineResponse = await fetchOutlineDetail(client, cid);
3724
+ const outlineDetail = outlineResponse?.data || {};
3725
+ const outlineRows = getOutlineRows(outlineDetail);
3726
+ if (outlineRows.length === 0) {
3727
+ return {
3728
+ phase: 'unsupported_page_scope_state',
3729
+ pageScopeResult: {
3730
+ checkStatus: null,
3731
+ reason: 'current_outline_empty',
3732
+ },
3733
+ outlineDetail,
3734
+ };
3735
+ }
3736
+
3737
+ // This mirrors the Web outline submit payload. The check is deliberately
3738
+ // made against the authoritative outline, so a CLI/WorkBuddy resume cannot
3739
+ // silently reuse a stale local snapshot.
3740
+ const response = await postJson(client, '/task/checkPageScope', {
3741
+ cid,
3742
+ updateStatus: 1,
3743
+ outLine: buildPageScopeCheckRows(outlineRows),
3744
+ });
3745
+ const pageScopeResult = response?.data || {};
3746
+ const checkStatus = Number(pageScopeResult.checkStatus);
3747
+ const phase = checkStatus === 1
3748
+ ? 'passed'
3749
+ : checkStatus === 0
3750
+ ? 'confirmation_required'
3751
+ : 'unsupported_page_scope_state';
3752
+ saveTaskSnapshot(state, cid, {
3753
+ pageScopePhase: phase,
3754
+ pageScopeResult,
3755
+ pageScopeCheckedAt: new Date().toISOString(),
3756
+ });
3757
+ return {
3758
+ phase,
3759
+ pageScopeResult,
3760
+ outlineDetail,
3761
+ checkId: pageScopeResult.checkId,
3762
+ response,
3763
+ };
3764
+ }
3765
+
3766
+ function buildPageScopeCheckRows(outlineRows) {
3767
+ return outlineRows.map((item, index) => {
3768
+ const rawText = String(item?.text || '').trim();
3769
+ const text = /^第[一二三四五六七八九十百千万零]+章\s*/.test(rawText)
3770
+ ? rawText
3771
+ : `第${numberToChinese(index + 1)}章 ${rawText}`;
3772
+ return compactObject({
3773
+ text,
3774
+ theme: item?.theme,
3775
+ updateFlag: item?.updateFlag || 0,
3776
+ important: item?.important,
3777
+ ...(item?.sequelFlag === 1 ? { sequelFlag: 1 } : {}),
3778
+ ...(item?.id ? { id: item.id } : {}),
3779
+ });
3780
+ });
3781
+ }
3782
+
3783
+ function numberToChinese(value) {
3784
+ const digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
3785
+ if (value <= 10) return digits[value];
3786
+ if (value < 20) return '十' + (value % 10 ? digits[value % 10] : '');
3787
+ if (value < 100) {
3788
+ const tens = Math.floor(value / 10);
3789
+ const ones = value % 10;
3790
+ return digits[tens] + '十' + (ones ? digits[ones] : '');
3791
+ }
3792
+ return String(value);
3793
+ }
3794
+
2887
3795
  async function fetchReferenceStatusSafe(client, cid) {
2888
3796
  try {
2889
3797
  return await getJson(client, `/task/directory/checkReference/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
@@ -2974,6 +3882,21 @@ async function ensureDirectoryReadyForWrite(client, state, cid, args) {
2974
3882
  const referenceData = referenceStatus?.data || {};
2975
3883
  const referencePhase = normalizeReferenceStatus(referenceData.status);
2976
3884
 
3885
+ const backendTaskStatus = Number(effectiveContentData.task_status);
3886
+ const backendContentStatus = Number(effectiveContentData.content_portion_status);
3887
+ if (backendTaskStatus === 7 || backendContentStatus === 7) {
3888
+ return {
3889
+ done: true,
3890
+ data: {
3891
+ phase: 'failed',
3892
+ taskDetail: taskData,
3893
+ contentDetail: effectiveContentData,
3894
+ referenceDetail: referenceData,
3895
+ },
3896
+ progressLabel: `task_status=${backendTaskStatus}, content_portion_status=${backendContentStatus}`,
3897
+ };
3898
+ }
3899
+
2977
3900
  saveTaskSnapshot(state, cid, pickTaskSnapshot({
2978
3901
  ...taskData,
2979
3902
  ...contentData,
@@ -3271,10 +4194,52 @@ function collectParseIssues(result) {
3271
4194
  return issueKeys.filter(key => {
3272
4195
  const directValue = result[key];
3273
4196
  const nestedValue = result.judgement_status?.[key.replace('_judgement', '')];
3274
- return String(directValue ?? nestedValue ?? '1') === '0';
4197
+ return isFalsyStatus(directValue ?? nestedValue ?? '1');
3275
4198
  });
3276
4199
  }
3277
4200
 
4201
+ function isFalsyStatus(value) {
4202
+ if (value === false || value === 0) return true;
4203
+ const normalized = String(value ?? '').trim().toLowerCase();
4204
+ return normalized === '0' || normalized === 'false';
4205
+ }
4206
+
4207
+ function collectUnsupportedParseIssues(result = {}) {
4208
+ const knownKeys = new Set([
4209
+ 'requirement_exist',
4210
+ 'requirement_judgement',
4211
+ 'billOfQuantities_judgement',
4212
+ 'score_judgement',
4213
+ 'directory_judgement',
4214
+ 'scoreAndDirectory_judgement',
4215
+ 'bidderIdentity_judgement',
4216
+ ]);
4217
+ const unsupported = new Set();
4218
+ for (const [key, value] of Object.entries(result || {})) {
4219
+ if (key.endsWith('_judgement') && !knownKeys.has(key) && isFalsyStatus(value)) {
4220
+ unsupported.add(key);
4221
+ }
4222
+ }
4223
+ for (const [key, value] of Object.entries(result?.judgement_status || {})) {
4224
+ const issueKey = key.endsWith('_judgement') ? key : `${key}_judgement`;
4225
+ if (!knownKeys.has(issueKey) && isFalsyStatus(value)) {
4226
+ unsupported.add(issueKey);
4227
+ }
4228
+ }
4229
+ return [...unsupported];
4230
+ }
4231
+
4232
+ function classifyParseContinuation(issues = []) {
4233
+ const normalized = new Set(Array.isArray(issues) ? issues : []);
4234
+ if (normalized.has('scoreAndDirectory_judgement')) {
4235
+ return 'basis_selection_required';
4236
+ }
4237
+ if (normalized.has('bidderIdentity_judgement')) {
4238
+ return 'bidder_role_required';
4239
+ }
4240
+ return 'supplement_required';
4241
+ }
4242
+
3278
4243
  function normalizeReferenceStatus(value) {
3279
4244
  if (value === undefined || value === null || value === '') {
3280
4245
  return null;
@@ -4297,7 +5262,7 @@ function renderLoginPage() {
4297
5262
  button{margin-top:13px;border:0;background:#536da9;color:#fff;box-shadow:0 10px 22px rgba(83,109,169,.23);} button:hover{background:#47619b;box-shadow:0 13px 26px rgba(83,109,169,.28);transform:translateY(-1px);} button:disabled{cursor:wait;opacity:.72;transform:none;}
4298
5263
  .button-arrow{font-size:18px;line-height:1;}.divider{display:flex;align-items:center;gap:12px;color:#a1aec2;margin:23px 0 13px;font-size:12px;}.divider:before,.divider:after{content:"";height:1px;background:#e8edf4;flex:1;}
4299
5264
  a{border:1px solid #d5dfed;color:#304364;background:#fff;} a:hover{border-color:#91a6cc;background:#f7f9fd;transform:translateY(-1px);}
4300
- .hint{margin:16px 0 0;color:#8390a6;font-size:12px;line-height:1.7;}.hint strong{color:#536da9;font-weight:700;}.secure-note{display:flex;align-items:center;gap:8px;margin-top:30px;padding-top:17px;border-top:1px solid #edf0f5;color:#9aa6b7;font-size:11px;}.secure-note span{width:7px;height:7px;border-radius:50%;background:#35a779;box-shadow:0 0 0 4px rgba(53,167,121,.10);}
5265
+ .hint{margin:16px 0 0;color:#8390a6;font-size:12px;line-height:1.7;}.hint strong{color:#536da9;font-weight:700;}.hint a{display:inline;width:auto;min-height:0;margin:0;padding:0;border:0;border-radius:0;color:#536da9;background:transparent;font-size:inherit;font-weight:700;line-height:inherit;text-align:left;text-decoration:underline;box-shadow:none;}.hint a:hover{background:transparent;border:0;color:#304f91;transform:none;box-shadow:none;}.secure-note{display:flex;align-items:center;gap:8px;margin-top:30px;padding-top:17px;border-top:1px solid #edf0f5;color:#9aa6b7;font-size:11px;}.secure-note span{width:7px;height:7px;border-radius:50%;background:#35a779;box-shadow:0 0 0 4px rgba(53,167,121,.10);}
4301
5266
  @media (max-width:820px){body{padding:18px;}.page-shell{grid-template-columns:1fr;max-width:520px;}.brand-panel{display:none;}.card{min-height:0;padding:32px 26px;border-radius:20px;}}
4302
5267
  @media (max-width:420px){body{padding:12px;}.card{padding:27px 20px;}.card-heading h2{font-size:22px;}}
4303
5268
  </style>
@@ -4328,7 +5293,7 @@ function renderLoginPage() {
4328
5293
  <div class="input-wrap"><span class="field-icon" aria-hidden="true"></span><input id="api-key" name="apiKey" type="password" autocomplete="off" placeholder="xq_sk_..." required /></div>
4329
5294
  <button type="submit"><span>使用 API Key 登录</span><span class="button-arrow">→</span></button>
4330
5295
  </form>
4331
- <p class="hint">API Key 可在喜鹊的 API Key 管理页创建。验证成功后即可关闭页面。</p>
5296
+ <p class="hint">没有 API Key?<a href="${API_KEY_MANAGEMENT_URL}" target="_blank" rel="noopener noreferrer">前往 API Key 管理页获取</a>。验证成功后即可关闭页面。</p>
4332
5297
  <div class="secure-note"><span></span>登录状态仅用于当前 CLI 工作流</div>
4333
5298
  </main>
4334
5299
  </div>
@@ -4934,6 +5899,8 @@ function pickOutlineConfig(params) {
4934
5899
  blindBidConfirm: params.blindBidConfirm,
4935
5900
  multiBidId: params.multiBidId,
4936
5901
  multiBidType: params.multiBidType,
5902
+ bidderCompanyId: params.bidderCompanyId,
5903
+ bidderCompanyName: params.bidderCompanyName,
4937
5904
  epcEngineerType: params.epcEngineerType,
4938
5905
  });
4939
5906
  }
@@ -5174,6 +6141,8 @@ function pickTaskSnapshot(source) {
5174
6141
  choiceMultiBiddingId: source.choiceMultiBiddingId,
5175
6142
  epcEngineerType: source.epcEngineerType,
5176
6143
  bidderRole: source.bidderRole,
6144
+ bidderCompanyId: source.bidderCompanyId,
6145
+ bidderCompanyName: source.bidderCompanyName,
5177
6146
  outlineReferenceType: source.outlineReferenceType,
5178
6147
  referenceStatus: source.referenceStatus,
5179
6148
  themeStyleSetting: source.themeStyleSetting,