@xqyz/xq-cli 0.3.14 → 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/CHANGELOG.md +8 -1
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/cli.mjs +1011 -43
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## [xq-cli 0.3.15 / WorkBuddy Plugin 0.10.34 / SkillHub 1.0.16] - 2026-09-01(npm 待发布,SkillHub 待手动上传)
|
|
14
|
+
|
|
15
|
+
### 解析补充与任务恢复
|
|
16
|
+
|
|
17
|
+
- 解析补充支持评分细则文本/HTML输入和评分文件上传,需求、工程量清单、技术方案目录按喜鹊网页端规则展示对应补充卡。
|
|
18
|
+
- CLI 与 WorkBuddy 补充提交均沿用原 `runId/cid/uuid` 续跑,提交后的短暂旧状态不会重复提交;可选项支持明确跳过并拦截重复或不匹配提交。
|
|
19
|
+
- 补齐规划模式自动确认后的再次补充、工期确认和历史阻断记录恢复分支;未知 `*_judgement=0` 状态会给出升级提示并停止轮询,避免任务无限卡住。
|
|
20
|
+
- Plugin 固定依赖 `@xqyz/xq-cli@0.3.15`,MCP/Connector/配置基线同步到 `0.10.34`;已生成 SkillHub `1.0.16` 上传包,仍需发布人手动上传。
|
|
14
21
|
|
|
15
22
|
## [xq-cli 0.3.14 / WorkBuddy Plugin 0.10.33] - 2026-08-28
|
|
16
23
|
|
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ xq-cli wizard
|
|
|
58
58
|
xq-cli wizard --file D:\bids\tender.docx
|
|
59
59
|
xq-cli init --file D:\bids\tender.docx --wait
|
|
60
60
|
xq-cli bidder-role --cid <cid> --uuid <uuid> --bidder-role <1|2>
|
|
61
|
+
xq-cli supplement --cid <cid> --uuid <uuid> --type score --text "评分细则..." --wait
|
|
62
|
+
xq-cli supplement --cid <cid> --uuid <uuid> --type score --file D:\bids\score.docx --wait
|
|
61
63
|
xq-cli outline --cid <cid> --interactive --wait
|
|
62
64
|
xq-cli outline --cid <cid> --wait
|
|
63
65
|
xq-cli outline view --cid <cid>
|
|
@@ -96,6 +98,16 @@ For goods procurement tasks paused on the bidder supply method, use
|
|
|
96
98
|
self production, `2` means procured supply). The command only submits the
|
|
97
99
|
choice for the existing parsed task; it does not upload or create a task.
|
|
98
100
|
|
|
101
|
+
When parsing pauses with `supplement_required`, use `xq-cli supplement` with
|
|
102
|
+
the same `cid` and `uuid`. Rating rules accept text/HTML via `--text` or a
|
|
103
|
+
`.doc/.docx/.pdf/.xlsx/.xls` file via `--file`; the file path is uploaded as
|
|
104
|
+
`files` to the frontend-compatible endpoint. Requirement details use
|
|
105
|
+
`--type requirement`, bills of quantities use `--type billOfQuantities` with a
|
|
106
|
+
file, and technical-solution directory requirements use `--type directory`
|
|
107
|
+
with text. The optional `--ignore` flag is only valid for an explicitly
|
|
108
|
+
skippable parser check. `--wait` continues polling the same task and never
|
|
109
|
+
reinitializes or creates a second task.
|
|
110
|
+
|
|
99
111
|
`write --wait` now also retries `sequelForOutline` when the directory phase stalls at the outline-complete step, and auto-calls `generateAllUnfinished` if the backend reports `100%` but still leaves unfinished chapters behind.
|
|
100
112
|
|
|
101
113
|
`write` follows the frontend flow:
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -40,6 +40,7 @@ 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;
|
|
45
46
|
const API_KEY_MANAGEMENT_URL = 'https://xiquebiaoshu.com/account/api-keys';
|
|
@@ -397,9 +398,32 @@ async function main() {
|
|
|
397
398
|
case 'period-confirm':
|
|
398
399
|
await handlePeriodConfirm(parsed, state);
|
|
399
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;
|
|
400
416
|
case 'bidder-role':
|
|
401
417
|
await handleBidderRole(parsed, state);
|
|
402
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;
|
|
403
427
|
case 'outline':
|
|
404
428
|
if (isOutlineAction(parsed, ['view', 'show'])) {
|
|
405
429
|
await handleOutlineView(parsed, state);
|
|
@@ -469,7 +493,14 @@ Commands
|
|
|
469
493
|
parse-status Query or wait for parse completion
|
|
470
494
|
period-status Query engineering project period confirmation status
|
|
471
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
|
|
472
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
|
|
473
504
|
outline Pre-set task params and trigger outline generation
|
|
474
505
|
outline view View the current outline without triggering generation
|
|
475
506
|
outline update Update the current outline from an edited JSON file
|
|
@@ -501,7 +532,13 @@ Examples
|
|
|
501
532
|
$env:XQ_API_KEY="xq_sk_xxx"
|
|
502
533
|
xq-cli init --file D:\\bids\\tender.docx --wait
|
|
503
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
|
|
504
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
|
|
505
542
|
xq-cli choices
|
|
506
543
|
xq-cli choices outline --json
|
|
507
544
|
xq-cli gallery-status --json
|
|
@@ -541,13 +578,19 @@ Common options
|
|
|
541
578
|
--timeout-sec <n> Poll timeout in seconds, default 600
|
|
542
579
|
--interval-sec <n> Poll interval in seconds, default 10
|
|
543
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
|
|
544
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
|
|
545
587
|
--api-key <key> Save a user API key locally and verify it with /user/info
|
|
546
588
|
--no-open Do not open the local login page automatically
|
|
547
589
|
--interactive Open prompt menus for the current command
|
|
548
590
|
--dry-run Preview an outline update without saving it
|
|
549
591
|
--yes Confirm an outline update without an interactive prompt
|
|
550
592
|
--allow-delete Allow an outline update that removes existing chapters
|
|
593
|
+
--check-id <id> Page-scope check ID returned by page-scope-status
|
|
551
594
|
--ignore-short-requirement Follow the frontend ignore/continue path for requirement_judgement=0
|
|
552
595
|
--ignore-missing-bill Follow the frontend ignore/continue path for billOfQuantities_judgement=0
|
|
553
596
|
|
|
@@ -771,6 +814,7 @@ async function handleInit(args, state) {
|
|
|
771
814
|
cid,
|
|
772
815
|
uuid: snapshot.uuid,
|
|
773
816
|
title: snapshot.projectName || snapshot.title,
|
|
817
|
+
originType: snapshot.originType,
|
|
774
818
|
files,
|
|
775
819
|
fileTypes: fileMeta.types,
|
|
776
820
|
parseResult: finalParse,
|
|
@@ -880,6 +924,216 @@ async function handlePeriodConfirm(args, state) {
|
|
|
880
924
|
return payload;
|
|
881
925
|
}
|
|
882
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
|
+
|
|
883
1137
|
async function handleBidderRole(args, state) {
|
|
884
1138
|
const client = createAuthorizedClient(args, state);
|
|
885
1139
|
const cid = resolveCid(args, state);
|
|
@@ -915,6 +1169,312 @@ async function handleBidderRole(args, state) {
|
|
|
915
1169
|
return payload;
|
|
916
1170
|
}
|
|
917
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
|
+
|
|
918
1478
|
function handleChoices(args) {
|
|
919
1479
|
const scope = normalizeChoicesScope(args._[1] || 'all');
|
|
920
1480
|
const payload = buildChoicesPayload(scope);
|
|
@@ -966,6 +1526,33 @@ async function handleWizard(args, state) {
|
|
|
966
1526
|
interactive: false,
|
|
967
1527
|
}, state);
|
|
968
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
|
+
|
|
969
1556
|
const writeResult = await handleWrite({
|
|
970
1557
|
...wizardArgs,
|
|
971
1558
|
_: ['write'],
|
|
@@ -974,6 +1561,29 @@ async function handleWizard(args, state) {
|
|
|
974
1561
|
interactive: false,
|
|
975
1562
|
}, state);
|
|
976
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
|
+
|
|
977
1587
|
const exportResult = await handleExport({
|
|
978
1588
|
...wizardArgs,
|
|
979
1589
|
_: ['export'],
|
|
@@ -1495,9 +2105,18 @@ async function handleOutlineUnlocked(args, state) {
|
|
|
1495
2105
|
for (const hint of collectIssueHints(waitResult.issues, args)) {
|
|
1496
2106
|
summaryLines.push(hint);
|
|
1497
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.');
|
|
1498
2111
|
} else if (waitResult?.phase === 'reference_selection_required') {
|
|
1499
2112
|
summaryLines.push('outline blocked: reference selection required');
|
|
1500
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.');
|
|
1501
2120
|
} else if (waitResult?.phase === 'failed') {
|
|
1502
2121
|
summaryLines.push(`outline failed: task_status=${waitResult.taskDetail?.task_status ?? '-'}`);
|
|
1503
2122
|
} else {
|
|
@@ -1844,6 +2463,30 @@ async function handleWriteUnlocked(args, state) {
|
|
|
1844
2463
|
assertContentGenerationAllowed(contentDetail.data, '生成正文');
|
|
1845
2464
|
saveTaskSnapshot(state, cid, pickTaskSnapshot(contentDetail.data));
|
|
1846
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
|
+
|
|
1847
2490
|
const writePreparation = await prepareContentGeneration(client, state, cid, contentDetail, args);
|
|
1848
2491
|
let triggerResult = null;
|
|
1849
2492
|
if (writePreparation.shouldTrigger) {
|
|
@@ -1859,6 +2502,15 @@ async function handleWriteUnlocked(args, state) {
|
|
|
1859
2502
|
if (booleanOption(args, 'wait')) {
|
|
1860
2503
|
status = await pollContentUntilReady(client, state, cid, args);
|
|
1861
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
|
+
}
|
|
1862
2514
|
if (status && status.task_status !== undefined) {
|
|
1863
2515
|
try {
|
|
1864
2516
|
assertContentCompleted(status, '确认正文完成');
|
|
@@ -1876,6 +2528,7 @@ async function handleWriteUnlocked(args, state) {
|
|
|
1876
2528
|
cid,
|
|
1877
2529
|
type,
|
|
1878
2530
|
directoryWaitResult,
|
|
2531
|
+
pageScopeResult,
|
|
1879
2532
|
writePreparation,
|
|
1880
2533
|
triggerResult,
|
|
1881
2534
|
waitResult: status,
|
|
@@ -2014,6 +2667,7 @@ async function handleExport(args, state) {
|
|
|
2014
2667
|
|
|
2015
2668
|
async function hydrateTaskSnapshot(client, state, cid) {
|
|
2016
2669
|
const cached = state.tasks?.[cid] || {};
|
|
2670
|
+
const confirmedPlanMode = cached.planMode;
|
|
2017
2671
|
let detailData = {};
|
|
2018
2672
|
try {
|
|
2019
2673
|
const detail = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
@@ -2028,6 +2682,12 @@ async function hydrateTaskSnapshot(client, state, cid) {
|
|
|
2028
2682
|
...detailData,
|
|
2029
2683
|
cid,
|
|
2030
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
|
+
}
|
|
2031
2691
|
saveTaskSnapshot(state, cid, snapshot);
|
|
2032
2692
|
return snapshot;
|
|
2033
2693
|
}
|
|
@@ -2064,6 +2724,8 @@ function buildOutlineParams(snapshot, args) {
|
|
|
2064
2724
|
const plan = stringOption(args, 'plan', snapshot.plan || '');
|
|
2065
2725
|
const requirement = stringOption(args, 'requirement', snapshot.requirement || '');
|
|
2066
2726
|
const rating = stringOption(args, 'rating', snapshot.rating || '');
|
|
2727
|
+
const bidderCompanyId = stringOption(args, 'bidderCompanyId', snapshot.bidderCompanyId || '');
|
|
2728
|
+
const bidderCompanyName = stringOption(args, 'bidderCompanyName', snapshot.bidderCompanyName || '');
|
|
2067
2729
|
const blindGenerationMode = enumOption(
|
|
2068
2730
|
args,
|
|
2069
2731
|
['blindGenerationMode', 'anonymousBid'],
|
|
@@ -2077,6 +2739,9 @@ function buildOutlineParams(snapshot, args) {
|
|
|
2077
2739
|
if (!title) {
|
|
2078
2740
|
throw new Error('Missing title for outline generation.');
|
|
2079
2741
|
}
|
|
2742
|
+
if (Number(snapshot.originType) === 3 && (!bidderCompanyId || !bidderCompanyName)) {
|
|
2743
|
+
throw new Error('commercial_bidder_company_required: 商务标必须先选择投标公司。');
|
|
2744
|
+
}
|
|
2080
2745
|
|
|
2081
2746
|
return compactObject({
|
|
2082
2747
|
cid: snapshot.cid,
|
|
@@ -2107,6 +2772,10 @@ function buildOutlineParams(snapshot, args) {
|
|
|
2107
2772
|
pageMarginSetting: snapshot.pageMarginSetting,
|
|
2108
2773
|
multiBidId: stringOption(args, 'multiBidId', snapshot.multiBidId),
|
|
2109
2774
|
multiBidType: stringOption(args, 'multiBidType', snapshot.multiBidType),
|
|
2775
|
+
...(bidderCompanyId || bidderCompanyName ? {
|
|
2776
|
+
bidderCompanyId,
|
|
2777
|
+
bidderCompanyName,
|
|
2778
|
+
} : {}),
|
|
2110
2779
|
planMode: enumOption(args, 'planMode', PLAN_MODE_OPTIONS, snapshot.planMode ?? 0),
|
|
2111
2780
|
epcEngineerType: normalizeEpcEngineerType(stringOption(args, 'epcEngineerType', snapshot.epcEngineerType)),
|
|
2112
2781
|
blindBidConfirm: {
|
|
@@ -2523,8 +3192,15 @@ function buildFileMetadata(files, args) {
|
|
|
2523
3192
|
async function pollParseUntilReady(client, state, cid, uuid, args) {
|
|
2524
3193
|
return pollUntil(async () => {
|
|
2525
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
|
+
}
|
|
2526
3202
|
return {
|
|
2527
|
-
done: isParseReadyForNextStep(
|
|
3203
|
+
done: isParseReadyForNextStep(success),
|
|
2528
3204
|
data: result,
|
|
2529
3205
|
progressLabel: `success=${result.success}`,
|
|
2530
3206
|
};
|
|
@@ -2604,6 +3280,7 @@ async function waitForOutlineReady(client, state, cid, uuid, args, initialSubmit
|
|
|
2604
3280
|
const planModePhase = normalizePlanModePhase(taskDetail.data?.plan_mode_phase);
|
|
2605
3281
|
const referenceData = referenceStatus?.data || {};
|
|
2606
3282
|
const referencePhase = normalizeReferenceStatus(referenceData.status);
|
|
3283
|
+
let parseReadyForOutline = !uuid;
|
|
2607
3284
|
if (referencePhase !== null) {
|
|
2608
3285
|
saveTaskSnapshot(state, cid, {
|
|
2609
3286
|
referenceStatus: referencePhase,
|
|
@@ -2641,60 +3318,82 @@ async function waitForOutlineReady(client, state, cid, uuid, args, initialSubmit
|
|
|
2641
3318
|
};
|
|
2642
3319
|
}
|
|
2643
3320
|
|
|
2644
|
-
if (
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
phase: 'ready',
|
|
2649
|
-
taskDetail: taskDetail.data,
|
|
2650
|
-
outlineDetail: outlineData,
|
|
2651
|
-
},
|
|
2652
|
-
progressLabel: `rootChapters=${countOutlineRoots({ outlineDetail: outlineData, taskDetail: taskDetail.data })}`,
|
|
2653
|
-
};
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
if (referencePhase === 1) {
|
|
2657
|
-
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)) {
|
|
2658
3325
|
return {
|
|
2659
|
-
done:
|
|
2660
|
-
data:
|
|
2661
|
-
|
|
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)}`,
|
|
2662
3334
|
};
|
|
2663
3335
|
}
|
|
3336
|
+
const issues = collectParseIssues(parseStatus);
|
|
3337
|
+
const unsupportedIssues = collectUnsupportedParseIssues(parseStatus);
|
|
2664
3338
|
|
|
2665
|
-
|
|
2666
|
-
if (!selection.ready) {
|
|
3339
|
+
if (parseSuccess === 4 && unsupportedIssues.length > 0) {
|
|
2667
3340
|
return {
|
|
2668
3341
|
done: true,
|
|
2669
3342
|
data: {
|
|
2670
|
-
phase: '
|
|
3343
|
+
phase: 'unsupported_intermediate_state',
|
|
3344
|
+
issues: unsupportedIssues,
|
|
3345
|
+
parseStatus,
|
|
2671
3346
|
taskDetail: taskDetail.data,
|
|
2672
3347
|
outlineDetail: outlineData,
|
|
2673
|
-
referenceDetail: referenceData,
|
|
2674
|
-
options: buildOutlineReferenceChoices(referenceData),
|
|
2675
3348
|
},
|
|
2676
|
-
progressLabel: '
|
|
3349
|
+
progressLabel: `unsupported_intermediate_state=${unsupportedIssues.join(',')}`,
|
|
2677
3350
|
};
|
|
2678
3351
|
}
|
|
2679
3352
|
|
|
2680
|
-
await submitOutlineReferenceType(client, cid, selection.referenceType);
|
|
2681
|
-
submittedReferenceType = selection.referenceType;
|
|
2682
|
-
saveTaskSnapshot(state, cid, {
|
|
2683
|
-
outlineReferenceType: selection.referenceType,
|
|
2684
|
-
});
|
|
2685
|
-
return {
|
|
2686
|
-
done: false,
|
|
2687
|
-
data: null,
|
|
2688
|
-
progressLabel: `referenceType=${selection.referenceType} submitted, waiting outline`,
|
|
2689
|
-
};
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
if (uuid) {
|
|
2693
|
-
const parseStatus = await fetchParseStatus(client, state, cid, uuid);
|
|
2694
|
-
const parseSuccess = Number(parseStatus.success);
|
|
2695
|
-
const issues = collectParseIssues(parseStatus);
|
|
2696
|
-
|
|
2697
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
|
+
}
|
|
2698
3397
|
const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
|
|
2699
3398
|
|
|
2700
3399
|
if (autoIgnore) {
|
|
@@ -2720,10 +3419,69 @@ async function waitForOutlineReady(client, state, cid, uuid, args, initialSubmit
|
|
|
2720
3419
|
};
|
|
2721
3420
|
}
|
|
2722
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
|
+
});
|
|
2723
3467
|
return {
|
|
2724
3468
|
done: false,
|
|
2725
3469
|
data: null,
|
|
2726
|
-
progressLabel: `
|
|
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 })}`,
|
|
2727
3485
|
};
|
|
2728
3486
|
}
|
|
2729
3487
|
|
|
@@ -2760,13 +3518,77 @@ async function waitForPlanningAnalysisReady(client, state, cid, uuid, args) {
|
|
|
2760
3518
|
? await fetchParseStatus(client, state, cid, uuid)
|
|
2761
3519
|
: { success: 1 };
|
|
2762
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
|
+
}
|
|
2763
3532
|
if (parseSuccess === 2) {
|
|
2764
3533
|
throw new Error(
|
|
2765
3534
|
'[TASK_PARSE_NOT_ADVANCED] 规划模式仍处于等待标包/EPC选择(success=2),未进入解析阶段,已停止等待。',
|
|
2766
3535
|
);
|
|
2767
3536
|
}
|
|
2768
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
|
+
}
|
|
2769
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
|
+
}
|
|
2770
3592
|
const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
|
|
2771
3593
|
if (autoIgnore) {
|
|
2772
3594
|
await submitIntermediateChoice(client, autoIgnore.payload);
|
|
@@ -2833,6 +3655,18 @@ async function pollContentUntilReady(client, state, cid, args) {
|
|
|
2833
3655
|
const totalChapter = numberOrDefault(contentData.totalChapter, 0);
|
|
2834
3656
|
const percentage = numberOrDefault(contentData.percentage, 0);
|
|
2835
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
|
+
|
|
2836
3670
|
if (isContentComplete(contentData)) {
|
|
2837
3671
|
return {
|
|
2838
3672
|
done: true,
|
|
@@ -2885,6 +3719,79 @@ async function fetchOutlineDetail(client, cid) {
|
|
|
2885
3719
|
return getJson(client, '/c/getOutLineDetail', { cid }, { retries: DEFAULT_RETRY_COUNT });
|
|
2886
3720
|
}
|
|
2887
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
|
+
|
|
2888
3795
|
async function fetchReferenceStatusSafe(client, cid) {
|
|
2889
3796
|
try {
|
|
2890
3797
|
return await getJson(client, `/task/directory/checkReference/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
@@ -2975,6 +3882,21 @@ async function ensureDirectoryReadyForWrite(client, state, cid, args) {
|
|
|
2975
3882
|
const referenceData = referenceStatus?.data || {};
|
|
2976
3883
|
const referencePhase = normalizeReferenceStatus(referenceData.status);
|
|
2977
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
|
+
|
|
2978
3900
|
saveTaskSnapshot(state, cid, pickTaskSnapshot({
|
|
2979
3901
|
...taskData,
|
|
2980
3902
|
...contentData,
|
|
@@ -3272,10 +4194,52 @@ function collectParseIssues(result) {
|
|
|
3272
4194
|
return issueKeys.filter(key => {
|
|
3273
4195
|
const directValue = result[key];
|
|
3274
4196
|
const nestedValue = result.judgement_status?.[key.replace('_judgement', '')];
|
|
3275
|
-
return
|
|
4197
|
+
return isFalsyStatus(directValue ?? nestedValue ?? '1');
|
|
3276
4198
|
});
|
|
3277
4199
|
}
|
|
3278
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
|
+
|
|
3279
4243
|
function normalizeReferenceStatus(value) {
|
|
3280
4244
|
if (value === undefined || value === null || value === '') {
|
|
3281
4245
|
return null;
|
|
@@ -4935,6 +5899,8 @@ function pickOutlineConfig(params) {
|
|
|
4935
5899
|
blindBidConfirm: params.blindBidConfirm,
|
|
4936
5900
|
multiBidId: params.multiBidId,
|
|
4937
5901
|
multiBidType: params.multiBidType,
|
|
5902
|
+
bidderCompanyId: params.bidderCompanyId,
|
|
5903
|
+
bidderCompanyName: params.bidderCompanyName,
|
|
4938
5904
|
epcEngineerType: params.epcEngineerType,
|
|
4939
5905
|
});
|
|
4940
5906
|
}
|
|
@@ -5175,6 +6141,8 @@ function pickTaskSnapshot(source) {
|
|
|
5175
6141
|
choiceMultiBiddingId: source.choiceMultiBiddingId,
|
|
5176
6142
|
epcEngineerType: source.epcEngineerType,
|
|
5177
6143
|
bidderRole: source.bidderRole,
|
|
6144
|
+
bidderCompanyId: source.bidderCompanyId,
|
|
6145
|
+
bidderCompanyName: source.bidderCompanyName,
|
|
5178
6146
|
outlineReferenceType: source.outlineReferenceType,
|
|
5179
6147
|
referenceStatus: source.referenceStatus,
|
|
5180
6148
|
themeStyleSetting: source.themeStyleSetting,
|