@xqyz/xq-cli 0.2.1 → 0.3.1
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 +30 -0
- package/README.md +87 -3
- package/package.json +1 -1
- package/src/cli.mjs +964 -18
package/src/cli.mjs
CHANGED
|
@@ -205,6 +205,12 @@ const PLAN_MODE_OPTIONS = [
|
|
|
205
205
|
{ value: 0, label: 'quick', description: 'quick mode', aliases: ['0', 'quick'] },
|
|
206
206
|
{ value: 1, label: 'planning', description: 'planning mode', aliases: ['1', 'planning', 'plan'] },
|
|
207
207
|
];
|
|
208
|
+
const PLAN_MODE_PHASE_LABELS = {
|
|
209
|
+
0: 'analysis',
|
|
210
|
+
1: 'outline',
|
|
211
|
+
2: 'directory',
|
|
212
|
+
3: 'content',
|
|
213
|
+
};
|
|
208
214
|
const LAYOUT_OPTIONS = [
|
|
209
215
|
{ value: 0, label: 'normal', description: 'regular layout', aliases: ['0', 'normal', 'default'] },
|
|
210
216
|
{ value: 1, label: 'table', description: 'full table layout', aliases: ['1', 'table'] },
|
|
@@ -338,6 +344,12 @@ async function main() {
|
|
|
338
344
|
case 'parse-status':
|
|
339
345
|
await handleParseStatus(parsed, state);
|
|
340
346
|
return;
|
|
347
|
+
case 'period-status':
|
|
348
|
+
await handlePeriodStatus(parsed, state);
|
|
349
|
+
return;
|
|
350
|
+
case 'period-confirm':
|
|
351
|
+
await handlePeriodConfirm(parsed, state);
|
|
352
|
+
return;
|
|
341
353
|
case 'outline':
|
|
342
354
|
if (isOutlineAction(parsed, ['view', 'show'])) {
|
|
343
355
|
await handleOutlineView(parsed, state);
|
|
@@ -354,7 +366,9 @@ async function main() {
|
|
|
354
366
|
await handleOutlineUpdate(parsed, state);
|
|
355
367
|
return;
|
|
356
368
|
case 'directory':
|
|
357
|
-
if (
|
|
369
|
+
if (isOutlineAction(parsed, ['update', 'edit'])) {
|
|
370
|
+
await handleDirectoryUpdate(parsed, state);
|
|
371
|
+
} else if (!parsed._[1] || isOutlineAction(parsed, ['view', 'show'])) {
|
|
358
372
|
await handleDirectoryView(parsed, state);
|
|
359
373
|
} else {
|
|
360
374
|
throw new Error(`Unknown directory action: ${parsed._[1]}`);
|
|
@@ -363,6 +377,12 @@ async function main() {
|
|
|
363
377
|
case 'directory-view':
|
|
364
378
|
await handleDirectoryView(parsed, state);
|
|
365
379
|
return;
|
|
380
|
+
case 'directory-prepare':
|
|
381
|
+
await handleDirectoryPrepare(parsed, state);
|
|
382
|
+
return;
|
|
383
|
+
case 'directory-update':
|
|
384
|
+
await handleDirectoryUpdate(parsed, state);
|
|
385
|
+
return;
|
|
366
386
|
case 'write':
|
|
367
387
|
await handleWrite(parsed, state);
|
|
368
388
|
return;
|
|
@@ -381,6 +401,9 @@ async function main() {
|
|
|
381
401
|
case 'wizard':
|
|
382
402
|
await handleWizard(parsed, state);
|
|
383
403
|
return;
|
|
404
|
+
case 'plan':
|
|
405
|
+
await handlePlan(parsed, state);
|
|
406
|
+
return;
|
|
384
407
|
default:
|
|
385
408
|
throw new Error(`Unsupported command: ${command}. Run "xq-cli help" to see available commands.`);
|
|
386
409
|
}
|
|
@@ -391,24 +414,41 @@ function printHelp() {
|
|
|
391
414
|
xq-cli: xique brand task CLI
|
|
392
415
|
|
|
393
416
|
Commands
|
|
394
|
-
login Save xique account token locally
|
|
417
|
+
login Save xique API key or account token locally
|
|
395
418
|
init Upload files and initialize a task
|
|
396
419
|
parse-status Query or wait for parse completion
|
|
420
|
+
period-status Query engineering project period confirmation status
|
|
421
|
+
period-confirm Save confirmed engineering project period
|
|
397
422
|
outline Pre-set task params and trigger outline generation
|
|
398
423
|
outline view View the current outline without triggering generation
|
|
399
424
|
outline update Update the current outline from an edited JSON file
|
|
400
425
|
directory view View the complete generated directory tree without triggering generation
|
|
426
|
+
directory update Apply an edited directory tree JSON file
|
|
427
|
+
directory-prepare Prepare/wait for the generated directory without starting body generation
|
|
401
428
|
write Charge if needed, enter content phase, and trigger content generation
|
|
429
|
+
plan Run an explicit planning-mode phase (feedback, outline, AI review, directory)
|
|
402
430
|
status Query task status
|
|
403
431
|
choices Print selectable outline/export config values
|
|
404
432
|
gallery-status Check whether My Gallery contains selectable images
|
|
405
433
|
wizard Run init -> outline -> write -> export with interactive menus
|
|
406
434
|
export Download the generated docx
|
|
407
435
|
|
|
436
|
+
Plan actions
|
|
437
|
+
feedback|supplement|correction Stage user additions/corrections locally
|
|
438
|
+
confirm-outline Submit staged feedback and generate the outline
|
|
439
|
+
outline-view View the current outline
|
|
440
|
+
outline-ai-adjust Ask AI for an outline candidate (no server write)
|
|
441
|
+
outline-ai-apply Accept a candidate locally for the directory step
|
|
442
|
+
directory-generate Save the confirmed outline and generate the final directory
|
|
443
|
+
directory-regenerate Explicitly replace and regenerate an existing directory
|
|
444
|
+
directory-view|directory-update Review or edit the directory
|
|
445
|
+
|
|
408
446
|
Examples
|
|
409
447
|
xq-cli login
|
|
410
448
|
xq-cli login --name your-account --password your-password
|
|
411
|
-
xq-cli login --
|
|
449
|
+
xq-cli login --api-key xq_sk_xxx
|
|
450
|
+
xq-cli login --browser
|
|
451
|
+
$env:XQ_API_KEY="xq_sk_xxx"
|
|
412
452
|
xq-cli init --file D:\\bids\\tender.docx --wait
|
|
413
453
|
xq-cli parse-status --cid <cid> --uuid <uuid> --wait
|
|
414
454
|
xq-cli choices
|
|
@@ -429,6 +469,14 @@ Examples
|
|
|
429
469
|
xq-cli outline update --cid <cid> --file D:\\output\\outline.json --yes
|
|
430
470
|
xq-cli directory view --cid <cid>
|
|
431
471
|
xq-cli directory view --cid <cid> --out D:\\output\\directory.json
|
|
472
|
+
xq-cli directory update --cid <cid> --file D:\\output\\directory.json --yes
|
|
473
|
+
xq-cli plan status --cid <cid> --json
|
|
474
|
+
xq-cli plan feedback --cid <cid> --text "Add the missing service boundary"
|
|
475
|
+
xq-cli plan confirm-outline --cid <cid> --yes --wait
|
|
476
|
+
xq-cli plan outline-ai-adjust --cid <cid> --prompt "Split chapter 2 into implementation and quality control" --out D:\\output\\
|
|
477
|
+
xq-cli plan outline-ai-apply --cid <cid> --file D:\\output\\outline-ai-<cid>.json --yes
|
|
478
|
+
xq-cli plan directory-generate --cid <cid> --yes --wait
|
|
479
|
+
xq-cli plan directory-regenerate --cid <cid> --yes --wait
|
|
432
480
|
xq-cli write --cid <cid> --type 1 --wait
|
|
433
481
|
xq-cli status --cid <cid> --content
|
|
434
482
|
xq-cli export --cid <cid> --interactive --out D:\\output\\
|
|
@@ -442,6 +490,7 @@ Common options
|
|
|
442
490
|
--interval-sec <n> Poll interval in seconds, default 10
|
|
443
491
|
--reference-type <0|1|2> Outline reference choice: 0=merge, 1=format only, 2=score only
|
|
444
492
|
--browser Force browser authorization login; browser is the default when credentials are not provided
|
|
493
|
+
--api-key <key> Save a user API key locally and verify it with /user/info
|
|
445
494
|
--interactive Open prompt menus for the current command
|
|
446
495
|
--dry-run Preview an outline update without saving it
|
|
447
496
|
--yes Confirm an outline update without an interactive prompt
|
|
@@ -504,6 +553,33 @@ async function handleLogin(args, state) {
|
|
|
504
553
|
throw new Error('Missing base URL. Pass --base-url <url>.');
|
|
505
554
|
}
|
|
506
555
|
const sourceFrom = stringOption(args, 'sourceFrom', DEFAULT_SOURCE_FROM);
|
|
556
|
+
const apiKey = normalizeApiKey(stringOption(args, 'apiKey'));
|
|
557
|
+
if (apiKey) {
|
|
558
|
+
const result = await loginWithApiKey(baseUrl, apiKey);
|
|
559
|
+
saveLoginState(state, {
|
|
560
|
+
baseUrl,
|
|
561
|
+
name: result.data?.username || result.data?.userName || result.data?.user?.username || state.user?.name || '',
|
|
562
|
+
sourceFrom,
|
|
563
|
+
result,
|
|
564
|
+
apiKey,
|
|
565
|
+
authType: 'api-key',
|
|
566
|
+
user: result.data || {},
|
|
567
|
+
});
|
|
568
|
+
outputResult(args, {
|
|
569
|
+
command: 'login',
|
|
570
|
+
mode: 'api-key',
|
|
571
|
+
authType: 'api-key',
|
|
572
|
+
baseUrl,
|
|
573
|
+
username: state.user.username,
|
|
574
|
+
apiKeySaved: true,
|
|
575
|
+
}, [
|
|
576
|
+
`login success: ${state.user.username}`,
|
|
577
|
+
'mode: api-key',
|
|
578
|
+
`baseUrl: ${baseUrl}`,
|
|
579
|
+
`config: ${CONFIG_FILE}`,
|
|
580
|
+
]);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
507
583
|
const loginMode = resolveLoginMode(args);
|
|
508
584
|
const result = loginMode === 'browser'
|
|
509
585
|
? await loginWithBrowser(baseUrl, sourceFrom, args)
|
|
@@ -519,6 +595,7 @@ async function handleLogin(args, state) {
|
|
|
519
595
|
outputResult(args, {
|
|
520
596
|
command: 'login',
|
|
521
597
|
mode: loginMode,
|
|
598
|
+
authType: loginMode === 'browser' ? 'token' : 'token',
|
|
522
599
|
baseUrl,
|
|
523
600
|
username: state.user.username,
|
|
524
601
|
tokenSaved: true,
|
|
@@ -534,11 +611,19 @@ function resolveLoginMode(args) {
|
|
|
534
611
|
if (booleanOption(args, 'browser')) {
|
|
535
612
|
return 'browser';
|
|
536
613
|
}
|
|
614
|
+
if (stringOption(args, 'apiKey')) {
|
|
615
|
+
return 'api-key';
|
|
616
|
+
}
|
|
537
617
|
const hasName = stringOption(args, 'name');
|
|
538
618
|
const hasPassword = stringOption(args, 'password');
|
|
539
619
|
return hasName || hasPassword ? 'password' : 'browser';
|
|
540
620
|
}
|
|
541
621
|
|
|
622
|
+
async function loginWithApiKey(baseUrl, apiKey) {
|
|
623
|
+
const client = createClient(baseUrl, apiKey);
|
|
624
|
+
return getJson(client, '/user/info');
|
|
625
|
+
}
|
|
626
|
+
|
|
542
627
|
async function loginWithPassword(baseUrl, sourceFrom, args) {
|
|
543
628
|
const name = requiredOption(args, 'name');
|
|
544
629
|
const password = requiredOption(args, 'password');
|
|
@@ -680,6 +765,72 @@ async function handleParseStatus(args, state) {
|
|
|
680
765
|
]);
|
|
681
766
|
}
|
|
682
767
|
|
|
768
|
+
async function handlePeriodStatus(args, state) {
|
|
769
|
+
const client = createAuthorizedClient(args, state);
|
|
770
|
+
const cid = resolveCid(args, state);
|
|
771
|
+
const response = await getJson(client, `/engineering/checkPeriod/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
772
|
+
const data = response?.data || {};
|
|
773
|
+
saveTaskSnapshot(state, cid, {
|
|
774
|
+
periodStatus: data.status,
|
|
775
|
+
contractPeriod: data.contractPeriod,
|
|
776
|
+
contractStartDate: data.contractStartDate,
|
|
777
|
+
contractEndDate: data.contractEndDate,
|
|
778
|
+
});
|
|
779
|
+
const payload = { command: 'period-status', cid, ...data };
|
|
780
|
+
outputResult(args, payload, [
|
|
781
|
+
`period status: cid=${cid}`,
|
|
782
|
+
`status: ${data.status ?? '-'}`,
|
|
783
|
+
data.status === 2
|
|
784
|
+
? `confirmation required: period=${data.contractPeriod ?? '-'}, start=${data.contractStartDate ?? '-'}, end=${data.contractEndDate ?? '-'}`
|
|
785
|
+
: data.status === 1
|
|
786
|
+
? 'period confirmation not required or already confirmed'
|
|
787
|
+
: 'period extraction is still running',
|
|
788
|
+
]);
|
|
789
|
+
return payload;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function handlePeriodConfirm(args, state) {
|
|
793
|
+
const client = createAuthorizedClient(args, state);
|
|
794
|
+
const cid = resolveCid(args, state);
|
|
795
|
+
const contractPeriod = numberOption(args, 'contractPeriod');
|
|
796
|
+
if (!Number.isInteger(contractPeriod) || contractPeriod <= 0) {
|
|
797
|
+
throw new Error('Option --contract-period expects a positive integer.');
|
|
798
|
+
}
|
|
799
|
+
const periodMode = numberOption(args, 'periodMode', 2);
|
|
800
|
+
if (![1, 2].includes(periodMode)) {
|
|
801
|
+
throw new Error('Option --period-mode expects 1 (known dates) or 2 (unknown dates).');
|
|
802
|
+
}
|
|
803
|
+
const contractStartDate = periodMode === 1
|
|
804
|
+
? requiredOption(args, 'contractStartDate')
|
|
805
|
+
: 'Day1';
|
|
806
|
+
const contractEndDate = periodMode === 1
|
|
807
|
+
? requiredOption(args, 'contractEndDate')
|
|
808
|
+
: `Day${contractPeriod}`;
|
|
809
|
+
const response = await postJson(client, '/engineering/resetPeriod', {
|
|
810
|
+
cid,
|
|
811
|
+
contractPeriod,
|
|
812
|
+
contractStartDate,
|
|
813
|
+
contractEndDate,
|
|
814
|
+
periodMode,
|
|
815
|
+
});
|
|
816
|
+
saveTaskSnapshot(state, cid, {
|
|
817
|
+
periodStatus: 2,
|
|
818
|
+
contractPeriod,
|
|
819
|
+
contractStartDate,
|
|
820
|
+
contractEndDate,
|
|
821
|
+
periodMode,
|
|
822
|
+
});
|
|
823
|
+
const payload = { command: 'period-confirm', cid, contractPeriod, contractStartDate, contractEndDate, periodMode, response };
|
|
824
|
+
outputResult(args, payload, [
|
|
825
|
+
`period confirmed: cid=${cid}`,
|
|
826
|
+
`contract period: ${contractPeriod}`,
|
|
827
|
+
`start: ${contractStartDate}`,
|
|
828
|
+
`end: ${contractEndDate}`,
|
|
829
|
+
`period mode: ${periodMode === 1 ? 'known dates' : 'unknown dates'}`,
|
|
830
|
+
]);
|
|
831
|
+
return payload;
|
|
832
|
+
}
|
|
833
|
+
|
|
683
834
|
function handleChoices(args) {
|
|
684
835
|
const scope = normalizeChoicesScope(args._[1] || 'all');
|
|
685
836
|
const payload = buildChoicesPayload(scope);
|
|
@@ -763,6 +914,336 @@ async function handleWizard(args, state) {
|
|
|
763
914
|
return resultPayload;
|
|
764
915
|
}
|
|
765
916
|
|
|
917
|
+
async function handlePlan(args, state) {
|
|
918
|
+
const action = String(args?._?.[1] || 'status').trim().toLowerCase();
|
|
919
|
+
switch (action) {
|
|
920
|
+
case 'status':
|
|
921
|
+
case 'show':
|
|
922
|
+
await handlePlanStatus(args, state);
|
|
923
|
+
return;
|
|
924
|
+
case 'feedback':
|
|
925
|
+
case 'supplement':
|
|
926
|
+
case 'correction':
|
|
927
|
+
await handlePlanFeedback(args, state);
|
|
928
|
+
return;
|
|
929
|
+
case 'confirm-outline':
|
|
930
|
+
case 'outline-confirm':
|
|
931
|
+
case 'generate-outline':
|
|
932
|
+
case 'regenerate-outline':
|
|
933
|
+
await handlePlanOutlineConfirmation(args, state);
|
|
934
|
+
return;
|
|
935
|
+
case 'outline-ai-adjust':
|
|
936
|
+
case 'ai-adjust':
|
|
937
|
+
case 'ai-outline':
|
|
938
|
+
await handlePlanAiOutlineAdjust(args, state);
|
|
939
|
+
return;
|
|
940
|
+
case 'outline-ai-apply':
|
|
941
|
+
case 'ai-apply':
|
|
942
|
+
await handlePlanAiOutlineApply(args, state);
|
|
943
|
+
return;
|
|
944
|
+
case 'outline-view':
|
|
945
|
+
await handleOutlineView({ ...args, _: ['outline', 'view'] }, state);
|
|
946
|
+
return;
|
|
947
|
+
case 'outline-update':
|
|
948
|
+
await handleOutlineUpdate({ ...args, _: ['outline', 'update'] }, state);
|
|
949
|
+
return;
|
|
950
|
+
case 'directory-generate':
|
|
951
|
+
case 'generate-directory':
|
|
952
|
+
await handlePlanDirectoryGenerate(args, state, false);
|
|
953
|
+
return;
|
|
954
|
+
case 'directory-regenerate':
|
|
955
|
+
case 'regenerate-directory':
|
|
956
|
+
await handlePlanDirectoryGenerate(args, state, true);
|
|
957
|
+
return;
|
|
958
|
+
case 'directory-view':
|
|
959
|
+
await handleDirectoryView({ ...args, _: ['directory', 'view'] }, state);
|
|
960
|
+
return;
|
|
961
|
+
case 'directory-update':
|
|
962
|
+
await handleDirectoryUpdate({ ...args, _: ['directory', 'update'] }, state);
|
|
963
|
+
return;
|
|
964
|
+
case 'write':
|
|
965
|
+
case 'generate-content':
|
|
966
|
+
await handleWrite({ ...args, _: ['write'] }, state);
|
|
967
|
+
return;
|
|
968
|
+
default:
|
|
969
|
+
throw new Error(`Unknown plan action: ${action}. Run "xq-cli help" to see planning-mode commands.`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function handlePlanStatus(args, state) {
|
|
974
|
+
const client = createAuthorizedClient(args, state);
|
|
975
|
+
const cid = resolveCid(args, state);
|
|
976
|
+
const [taskResponse, outlineResponse, contentResponse] = await Promise.all([
|
|
977
|
+
getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT }),
|
|
978
|
+
fetchOutlineDetail(client, cid),
|
|
979
|
+
fetchContentDetail(client, cid),
|
|
980
|
+
]);
|
|
981
|
+
const taskDetail = taskResponse?.data || {};
|
|
982
|
+
const outlineDetail = outlineResponse?.data || {};
|
|
983
|
+
const contentDetail = contentResponse?.data || {};
|
|
984
|
+
const phase = normalizePlanModePhase(taskDetail.plan_mode_phase);
|
|
985
|
+
const payload = {
|
|
986
|
+
command: 'plan-status',
|
|
987
|
+
cid,
|
|
988
|
+
planMode: normalizePlanModeValue(taskDetail.planMode),
|
|
989
|
+
planModePhase: phase,
|
|
990
|
+
phaseLabel: phase === null ? null : PLAN_MODE_PHASE_LABELS[phase],
|
|
991
|
+
taskDetail,
|
|
992
|
+
outlineDetail,
|
|
993
|
+
contentDetail,
|
|
994
|
+
pendingFeedback: normalizePlanFeedback(state.tasks?.[cid]?.planFeedback).length,
|
|
995
|
+
};
|
|
996
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot({ ...taskDetail, ...contentDetail }));
|
|
997
|
+
outputResult(args, payload, [
|
|
998
|
+
`plan status: cid=${cid}`,
|
|
999
|
+
`planMode: ${describeEnumValue(PLAN_MODE_OPTIONS, normalizePlanModeValue(taskDetail.planMode) ?? taskDetail.planMode)}`,
|
|
1000
|
+
`phase: ${formatPlanModePhase(phase)}`,
|
|
1001
|
+
`outline: ${outlineDetail.outline_status ? 'ready' : 'not-ready'} (${countOutlineRoots({ outlineDetail })} root chapters)`,
|
|
1002
|
+
`directory: task_status=${contentDetail.task_status ?? taskDetail.task_status ?? '-'}, directory_status=${contentDetail.directory_portion_status ?? taskDetail.directory_portion_status ?? '-'}`,
|
|
1003
|
+
`pending feedback: ${payload.pendingFeedback}`,
|
|
1004
|
+
]);
|
|
1005
|
+
return payload;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function handlePlanFeedback(args, state) {
|
|
1009
|
+
const cid = resolveCid(args, state);
|
|
1010
|
+
const task = state.tasks?.[cid] || {};
|
|
1011
|
+
if (booleanOption(args, 'clear')) {
|
|
1012
|
+
saveTaskSnapshot(state, cid, { planFeedback: [] });
|
|
1013
|
+
const payload = { command: 'plan-feedback', cid, cleared: true, feedback: [] };
|
|
1014
|
+
outputResult(args, payload, [`plan feedback cleared: cid=${cid}`]);
|
|
1015
|
+
return payload;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const feedback = readPlanFeedbackArgs(args);
|
|
1019
|
+
if (feedback.length === 0) {
|
|
1020
|
+
throw new Error('Plan feedback requires --text <message>, --correction <message>, or --file <feedback.json>.');
|
|
1021
|
+
}
|
|
1022
|
+
const merged = mergePlanFeedback(normalizePlanFeedback(task.planFeedback), feedback);
|
|
1023
|
+
saveTaskSnapshot(state, cid, {
|
|
1024
|
+
planFeedback: merged,
|
|
1025
|
+
// New feedback means the accepted AI candidate is no longer based on
|
|
1026
|
+
// the latest analysis and must not be reused accidentally.
|
|
1027
|
+
planAiOutlineCandidate: undefined,
|
|
1028
|
+
});
|
|
1029
|
+
const payload = {
|
|
1030
|
+
command: 'plan-feedback',
|
|
1031
|
+
cid,
|
|
1032
|
+
added: feedback,
|
|
1033
|
+
pendingFeedback: merged,
|
|
1034
|
+
};
|
|
1035
|
+
outputResult(args, payload, [
|
|
1036
|
+
`plan feedback saved locally: cid=${cid}`,
|
|
1037
|
+
`pending feedback: ${merged.length}`,
|
|
1038
|
+
'feedback is not sent to the backend yet; it will be included only after plan confirm-outline is explicitly confirmed.',
|
|
1039
|
+
]);
|
|
1040
|
+
return payload;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function handlePlanOutlineConfirmation(args, state) {
|
|
1044
|
+
const client = createAuthorizedClient(args, state);
|
|
1045
|
+
const cid = resolveCid(args, state);
|
|
1046
|
+
const snapshot = await hydrateTaskSnapshot(client, state, cid);
|
|
1047
|
+
const params = buildOutlineParams(snapshot, { ...args, planMode: 'planning' });
|
|
1048
|
+
const regenerating = booleanOption(args, 'regenerate') || normalizePlanModePhase(snapshot.plan_mode_phase) >= 1;
|
|
1049
|
+
const feedback = normalizePlanFeedback(state.tasks?.[cid]?.planFeedback);
|
|
1050
|
+
|
|
1051
|
+
await confirmPlanAction(args, regenerating ? 'regenerate outline' : 'confirm and generate outline', [
|
|
1052
|
+
`config: ${buildOutlineConfigSummary(params)}`,
|
|
1053
|
+
`pending feedback: ${feedback.length}`,
|
|
1054
|
+
]);
|
|
1055
|
+
|
|
1056
|
+
await postJson(client, '/task/preSet', params);
|
|
1057
|
+
const phaseResponse = await setPlanModePhase(client, cid, 1);
|
|
1058
|
+
const response = await postJson(client, '/task/generateDirectoryByCid', compactObject({
|
|
1059
|
+
cid,
|
|
1060
|
+
chatList: feedback.length > 0
|
|
1061
|
+
? feedback.map(item => ({ quote_paragraph: item.quote, user_comments: item.text }))
|
|
1062
|
+
: undefined,
|
|
1063
|
+
triggerType: 1,
|
|
1064
|
+
}));
|
|
1065
|
+
saveTaskSnapshot(state, cid, {
|
|
1066
|
+
...pickTaskSnapshot(params),
|
|
1067
|
+
planModePhase: 1,
|
|
1068
|
+
planFeedback: [],
|
|
1069
|
+
planAiOutlineCandidate: undefined,
|
|
1070
|
+
planOutlineConfirmedAt: new Date().toISOString(),
|
|
1071
|
+
planOutlineRegeneratedAt: regenerating ? new Date().toISOString() : undefined,
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const waitResult = booleanOption(args, 'wait')
|
|
1075
|
+
? await waitForOutlineReady(client, state, cid, snapshot.uuid, args)
|
|
1076
|
+
: null;
|
|
1077
|
+
const payload = {
|
|
1078
|
+
command: 'plan-confirm-outline',
|
|
1079
|
+
cid,
|
|
1080
|
+
regenerating,
|
|
1081
|
+
config: pickOutlineConfig(params),
|
|
1082
|
+
feedback,
|
|
1083
|
+
planModePhase: phaseResponse,
|
|
1084
|
+
response,
|
|
1085
|
+
waitResult,
|
|
1086
|
+
};
|
|
1087
|
+
outputResult(args, payload, [
|
|
1088
|
+
`plan outline ${regenerating ? 'regeneration' : 'generation'} started: cid=${cid}`,
|
|
1089
|
+
`phase: ${formatPlanModePhase(phaseResponse)}`,
|
|
1090
|
+
`feedback submitted: ${feedback.length}`,
|
|
1091
|
+
waitResult?.phase === 'ready'
|
|
1092
|
+
? `outline ready: ${countOutlineRoots(waitResult)} root chapters; review or use plan outline-ai-adjust before generating the final directory.`
|
|
1093
|
+
: 'outline generation started; query plan status or rerun with --wait to review it when ready.',
|
|
1094
|
+
]);
|
|
1095
|
+
return payload;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function handlePlanAiOutlineAdjust(args, state) {
|
|
1099
|
+
const client = createAuthorizedClient(args, state);
|
|
1100
|
+
const cid = resolveCid(args, state);
|
|
1101
|
+
const prompt = requiredOption(args, 'prompt').trim();
|
|
1102
|
+
if (!prompt) {
|
|
1103
|
+
throw new Error('Option --prompt cannot be empty.');
|
|
1104
|
+
}
|
|
1105
|
+
const task = state.tasks?.[cid] || {};
|
|
1106
|
+
const response = await postJson(client, '/outline/chat', compactObject({
|
|
1107
|
+
cid,
|
|
1108
|
+
prompt,
|
|
1109
|
+
conversationId: stringOption(args, 'conversationId', task.planAiConversationId),
|
|
1110
|
+
}));
|
|
1111
|
+
const result = response?.data?.result || response?.result || {};
|
|
1112
|
+
const conversationId = result.conversationId || response?.data?.conversationId || response?.conversationId || '';
|
|
1113
|
+
if (conversationId) {
|
|
1114
|
+
saveTaskSnapshot(state, cid, { planAiConversationId: conversationId });
|
|
1115
|
+
}
|
|
1116
|
+
const rows = extractPlanAiOutlineRows(result.answer);
|
|
1117
|
+
const candidate = rows.length > 0
|
|
1118
|
+
? buildPlanAiOutlineCandidate(cid, prompt, conversationId, rows)
|
|
1119
|
+
: null;
|
|
1120
|
+
const savedPath = candidate && stringOption(args, 'out')
|
|
1121
|
+
? writePlanAiOutlineFile(stringOption(args, 'out'), cid, candidate)
|
|
1122
|
+
: null;
|
|
1123
|
+
const payload = {
|
|
1124
|
+
command: 'plan-outline-ai-adjust',
|
|
1125
|
+
cid,
|
|
1126
|
+
prompt,
|
|
1127
|
+
conversationId: conversationId || null,
|
|
1128
|
+
responseType: result.type ?? null,
|
|
1129
|
+
message: rows.length === 0 ? normalizeText(result.answer) : null,
|
|
1130
|
+
candidate,
|
|
1131
|
+
savedPath,
|
|
1132
|
+
};
|
|
1133
|
+
outputResult(args, payload, [
|
|
1134
|
+
`plan AI response: cid=${cid}, type=${result.type ?? '-'}`,
|
|
1135
|
+
rows.length > 0
|
|
1136
|
+
? `AI outline candidate: ${rows.length} chapter(s), not saved to the backend.`
|
|
1137
|
+
: `AI message: ${normalizeText(result.answer) || 'no outline candidate returned'}`,
|
|
1138
|
+
savedPath
|
|
1139
|
+
? `candidate saved: ${savedPath}`
|
|
1140
|
+
: (rows.length > 0 ? 'hint: pass --out <file-or-directory> to save the candidate before applying it.' : ''),
|
|
1141
|
+
].filter(Boolean));
|
|
1142
|
+
return payload;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async function handlePlanAiOutlineApply(args, state) {
|
|
1146
|
+
const cid = resolveCid(args, state);
|
|
1147
|
+
const filePath = path.resolve(requiredOption(args, 'file'));
|
|
1148
|
+
const document = readOutlineEditFile(filePath);
|
|
1149
|
+
validateOutlineEditCid(document, cid, filePath);
|
|
1150
|
+
const rows = buildPlanAiOutlineUpdateRows(extractOutlineRows(document));
|
|
1151
|
+
if (booleanOption(args, 'dryRun')) {
|
|
1152
|
+
const payload = { command: 'plan-outline-ai-apply', cid, file: filePath, dryRun: true, applied: false, candidate: { cid, outLine: rows } };
|
|
1153
|
+
outputResult(args, payload, [
|
|
1154
|
+
`AI outline apply preview: cid=${cid}`,
|
|
1155
|
+
`chapters: ${rows.length}`,
|
|
1156
|
+
'dry-run only: the candidate was not accepted locally or sent to the backend.',
|
|
1157
|
+
]);
|
|
1158
|
+
return payload;
|
|
1159
|
+
}
|
|
1160
|
+
await confirmPlanAction(args, 'accept AI outline candidate', [
|
|
1161
|
+
`candidate file: ${filePath}`,
|
|
1162
|
+
`chapters: ${rows.length}`,
|
|
1163
|
+
'the candidate remains local until the final-directory step is explicitly confirmed.',
|
|
1164
|
+
]);
|
|
1165
|
+
saveTaskSnapshot(state, cid, {
|
|
1166
|
+
planAiOutlineCandidate: {
|
|
1167
|
+
format: 'xq-cli-plan-ai-outline@1',
|
|
1168
|
+
cid,
|
|
1169
|
+
acceptedAt: new Date().toISOString(),
|
|
1170
|
+
sourceFile: filePath,
|
|
1171
|
+
outLine: rows,
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
const payload = { command: 'plan-outline-ai-apply', cid, file: filePath, dryRun: false, applied: true, candidate: { cid, outLine: rows } };
|
|
1175
|
+
outputResult(args, payload, [
|
|
1176
|
+
`AI outline candidate accepted locally: cid=${cid}`,
|
|
1177
|
+
'the backend outline is unchanged. Review it, then run xq-cli plan directory-generate --cid <cid> --yes to save the candidate and generate the directory.',
|
|
1178
|
+
]);
|
|
1179
|
+
return payload;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async function handlePlanDirectoryGenerate(args, state, regenerating) {
|
|
1183
|
+
const client = createAuthorizedClient(args, state);
|
|
1184
|
+
const cid = resolveCid(args, state);
|
|
1185
|
+
const snapshot = await hydrateTaskSnapshot(client, state, cid);
|
|
1186
|
+
ensurePlanningTask(snapshot);
|
|
1187
|
+
const currentPhase = normalizePlanModePhase(snapshot.plan_mode_phase);
|
|
1188
|
+
if (regenerating && (currentPhase === null || currentPhase < 2)) {
|
|
1189
|
+
throw new Error('The task has not reached the directory phase yet. Use plan directory-generate for the first final-directory generation.');
|
|
1190
|
+
}
|
|
1191
|
+
if (!regenerating && currentPhase !== null && currentPhase >= 2) {
|
|
1192
|
+
throw new Error('This task has already entered the directory phase. Use plan directory-regenerate for an explicit replacement request.');
|
|
1193
|
+
}
|
|
1194
|
+
const outlineResponse = await fetchOutlineDetail(client, cid);
|
|
1195
|
+
const outlineDetail = outlineResponse?.data || {};
|
|
1196
|
+
const outlineSelection = resolvePlanDirectoryOutline(args, state, cid, outlineDetail);
|
|
1197
|
+
const rows = outlineSelection.rows;
|
|
1198
|
+
await confirmPlanAction(args, regenerating ? 'regenerate final directory' : 'generate final directory', [
|
|
1199
|
+
`outline chapters: ${rows.length}`,
|
|
1200
|
+
`outline source: ${outlineSelection.source}`,
|
|
1201
|
+
regenerating
|
|
1202
|
+
? 'the existing directory can be replaced after the outline is saved.'
|
|
1203
|
+
: 'the outline will be saved and the backend will start final-directory generation.',
|
|
1204
|
+
]);
|
|
1205
|
+
|
|
1206
|
+
if (!regenerating) {
|
|
1207
|
+
await postJson(client, '/task/checkPageScope', {
|
|
1208
|
+
cid,
|
|
1209
|
+
updateStatus: 1,
|
|
1210
|
+
outLine: rows,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
const outlineUpdate = await postJson(client, '/c/updateOutLine', {
|
|
1214
|
+
cid,
|
|
1215
|
+
outLine: rows,
|
|
1216
|
+
source: outlineSelection.source === 'AI candidate' ? 1 : 2,
|
|
1217
|
+
});
|
|
1218
|
+
const planModePhase = await setPlanModePhase(client, cid, 2);
|
|
1219
|
+
saveTaskSnapshot(state, cid, {
|
|
1220
|
+
planModePhase: 2,
|
|
1221
|
+
outlineUpdatedAt: new Date().toISOString(),
|
|
1222
|
+
directoryGenerationRequestedAt: new Date().toISOString(),
|
|
1223
|
+
directoryRegenerationRequestedAt: regenerating ? new Date().toISOString() : undefined,
|
|
1224
|
+
planAiOutlineCandidate: undefined,
|
|
1225
|
+
});
|
|
1226
|
+
const directoryWaitResult = booleanOption(args, 'wait')
|
|
1227
|
+
? await ensureDirectoryReadyForWrite(client, state, cid, args)
|
|
1228
|
+
: null;
|
|
1229
|
+
const payload = {
|
|
1230
|
+
command: regenerating ? 'plan-directory-regenerate' : 'plan-directory-generate',
|
|
1231
|
+
cid,
|
|
1232
|
+
regenerating,
|
|
1233
|
+
outlineUpdate,
|
|
1234
|
+
planModePhase,
|
|
1235
|
+
directoryWaitResult,
|
|
1236
|
+
};
|
|
1237
|
+
outputResult(args, payload, [
|
|
1238
|
+
`final directory ${regenerating ? 'regeneration' : 'generation'} requested: cid=${cid}`,
|
|
1239
|
+
`phase: ${formatPlanModePhase(planModePhase)}`,
|
|
1240
|
+
directoryWaitResult?.phase === 'ready'
|
|
1241
|
+
? 'directory ready for review; body generation has not started.'
|
|
1242
|
+
: 'the backend is generating the directory; query plan status or rerun with --wait.',
|
|
1243
|
+
]);
|
|
1244
|
+
return payload;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
766
1247
|
async function handleOutline(args, state) {
|
|
767
1248
|
validateOutlineReferenceTypeOption(args);
|
|
768
1249
|
const client = createAuthorizedClient(args, state);
|
|
@@ -775,6 +1256,26 @@ async function handleOutline(args, state) {
|
|
|
775
1256
|
const effectiveArgs = booleanOption(args, 'interactive')
|
|
776
1257
|
? await collectOutlineInteractiveArgs(args, outlineSnapshot)
|
|
777
1258
|
: args;
|
|
1259
|
+
const periodResult = booleanOption(effectiveArgs, 'wait')
|
|
1260
|
+
? await ensurePeriodReady(client, state, cid, effectiveArgs)
|
|
1261
|
+
: null;
|
|
1262
|
+
if (periodResult?.phase === 'confirmation_required') {
|
|
1263
|
+
const resultPayload = {
|
|
1264
|
+
command: 'outline',
|
|
1265
|
+
cid,
|
|
1266
|
+
uuid: outlineSnapshot.uuid,
|
|
1267
|
+
periodResult,
|
|
1268
|
+
waitResult: { phase: 'period_confirmation_required', periodResult },
|
|
1269
|
+
};
|
|
1270
|
+
outputResult(args, resultPayload, [
|
|
1271
|
+
`outline blocked: engineering period confirmation required, cid=${cid}`,
|
|
1272
|
+
`contract period: ${periodResult.contractPeriod ?? '-'}`,
|
|
1273
|
+
`start: ${periodResult.contractStartDate ?? '-'}`,
|
|
1274
|
+
`end: ${periodResult.contractEndDate ?? '-'}`,
|
|
1275
|
+
`hint: run xq-cli period-confirm --cid ${cid} --contract-period <days> --period-mode <1|2>, then rerun outline for the same cid.`,
|
|
1276
|
+
]);
|
|
1277
|
+
return resultPayload;
|
|
1278
|
+
}
|
|
778
1279
|
const params = buildOutlineParams(outlineSnapshot, effectiveArgs);
|
|
779
1280
|
|
|
780
1281
|
const triggerMode = await triggerOutlineGeneration(client, outlineSnapshot, params);
|
|
@@ -825,6 +1326,7 @@ async function handleOutline(args, state) {
|
|
|
825
1326
|
base: params.base,
|
|
826
1327
|
config: pickOutlineConfig(params),
|
|
827
1328
|
triggerMode,
|
|
1329
|
+
periodResult,
|
|
828
1330
|
waitResult,
|
|
829
1331
|
};
|
|
830
1332
|
outputResult(args, resultPayload, [
|
|
@@ -933,6 +1435,7 @@ async function handleOutlineUpdate(args, state) {
|
|
|
933
1435
|
const response = await postJson(client, '/c/updateOutLine', requestData);
|
|
934
1436
|
saveTaskSnapshot(state, cid, {
|
|
935
1437
|
outlineUpdatedAt: new Date().toISOString(),
|
|
1438
|
+
planAiOutlineCandidate: undefined,
|
|
936
1439
|
});
|
|
937
1440
|
const resultPayload = {
|
|
938
1441
|
...baseResult,
|
|
@@ -977,6 +1480,87 @@ async function handleDirectoryView(args, state) {
|
|
|
977
1480
|
return resultPayload;
|
|
978
1481
|
}
|
|
979
1482
|
|
|
1483
|
+
async function handleDirectoryPrepare(args, state) {
|
|
1484
|
+
const client = createAuthorizedClient(args, state);
|
|
1485
|
+
const cid = resolveCid(args, state);
|
|
1486
|
+
const directoryWaitResult = await ensureDirectoryReadyForWrite(client, state, cid, args);
|
|
1487
|
+
const payload = { command: 'directory-prepare', cid, directoryWaitResult };
|
|
1488
|
+
outputResult(args, payload, [
|
|
1489
|
+
`directory prepare: cid=${cid}`,
|
|
1490
|
+
`phase: ${directoryWaitResult.phase}`,
|
|
1491
|
+
`directory status: ${directoryWaitResult.contentDetail?.directory_portion_status ?? '-'}`,
|
|
1492
|
+
directoryWaitResult.phase === 'ready'
|
|
1493
|
+
? 'directory ready for user review; body generation has not started.'
|
|
1494
|
+
: 'directory is not ready; continue querying the same task.',
|
|
1495
|
+
]);
|
|
1496
|
+
return payload;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async function handleDirectoryUpdate(args, state) {
|
|
1500
|
+
const client = createAuthorizedClient(args, state);
|
|
1501
|
+
const cid = resolveCid(args, state);
|
|
1502
|
+
const filePath = path.resolve(requiredOption(args, 'file'));
|
|
1503
|
+
ensureFileExists(filePath);
|
|
1504
|
+
let document;
|
|
1505
|
+
try {
|
|
1506
|
+
document = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
|
|
1507
|
+
} catch {
|
|
1508
|
+
throw new Error(`Failed to parse directory JSON: ${filePath}`);
|
|
1509
|
+
}
|
|
1510
|
+
const documentCid = document && !Array.isArray(document) ? String(document.cid || '').trim() : '';
|
|
1511
|
+
if (documentCid && documentCid !== cid) {
|
|
1512
|
+
throw new Error(`Directory file cid (${documentCid}) does not match --cid (${cid}): ${filePath}`);
|
|
1513
|
+
}
|
|
1514
|
+
const directories = Array.isArray(document)
|
|
1515
|
+
? document
|
|
1516
|
+
: (Array.isArray(document?.directoryNodes) ? document.directoryNodes : null);
|
|
1517
|
+
if (!directories || directories.length === 0) {
|
|
1518
|
+
throw new Error('Directory JSON must be an array or contain a non-empty directoryNodes array.');
|
|
1519
|
+
}
|
|
1520
|
+
const requestPreview = { cid, directories };
|
|
1521
|
+
const resultPayload = {
|
|
1522
|
+
command: 'directory-update',
|
|
1523
|
+
cid,
|
|
1524
|
+
file: filePath,
|
|
1525
|
+
dryRun: booleanOption(args, 'dryRun'),
|
|
1526
|
+
applied: false,
|
|
1527
|
+
requestPreview,
|
|
1528
|
+
};
|
|
1529
|
+
if (booleanOption(args, 'dryRun')) {
|
|
1530
|
+
outputResult(args, resultPayload, [
|
|
1531
|
+
`directory update preview: cid=${cid}`,
|
|
1532
|
+
`roots: ${directories.length}`,
|
|
1533
|
+
'dry-run only: no server data was changed.',
|
|
1534
|
+
'hint: rerun with --yes to apply this update.',
|
|
1535
|
+
]);
|
|
1536
|
+
return resultPayload;
|
|
1537
|
+
}
|
|
1538
|
+
const confirmed = booleanOption(args, 'yes') || await confirmDirectoryUpdate(args, cid, directories);
|
|
1539
|
+
if (!confirmed) {
|
|
1540
|
+
outputResult(args, { ...resultPayload, reason: 'cancelled' }, [`directory update cancelled: cid=${cid}`]);
|
|
1541
|
+
return { ...resultPayload, reason: 'cancelled' };
|
|
1542
|
+
}
|
|
1543
|
+
const response = await postJson(client, '/task/updateDirectory', requestPreview);
|
|
1544
|
+
const payload = { ...resultPayload, applied: true, response };
|
|
1545
|
+
saveTaskSnapshot(state, cid, { directoryUpdatedAt: new Date().toISOString() });
|
|
1546
|
+
outputResult(args, payload, [
|
|
1547
|
+
`directory update accepted: cid=${cid}`,
|
|
1548
|
+
'note: directory changes may trigger asynchronous content regeneration.',
|
|
1549
|
+
`next: xq-cli directory view --cid ${cid}`,
|
|
1550
|
+
]);
|
|
1551
|
+
return payload;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async function confirmDirectoryUpdate(args, cid, directories) {
|
|
1555
|
+
if (!supportsInteractivePrompt()) {
|
|
1556
|
+
throw new Error('Directory update changes server data. Review it with --dry-run, then rerun with --yes.');
|
|
1557
|
+
}
|
|
1558
|
+
return runPromptSession(args, async context => {
|
|
1559
|
+
context.promptStream.write(`\nDirectory roots=${directories.length}\n`);
|
|
1560
|
+
return promptYesNo(context, `Apply this directory update to cid=${cid}`, false);
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
|
|
980
1564
|
async function confirmOutlineUpdate(args, cid, diff) {
|
|
981
1565
|
if (!supportsInteractivePrompt()) {
|
|
982
1566
|
throw new Error('Outline update changes server data. Review it with --dry-run, then rerun with --yes.');
|
|
@@ -1125,6 +1709,8 @@ async function handleStatus(args, state) {
|
|
|
1125
1709
|
`status: cid=${cid}`,
|
|
1126
1710
|
`title: ${detail.data.title || detail.data.projectName || '-'}`,
|
|
1127
1711
|
`task_status: ${detail.data.task_status ?? '-'}`,
|
|
1712
|
+
`planMode: ${describeEnumValue(PLAN_MODE_OPTIONS, normalizePlanModeValue(detail.data.planMode) ?? detail.data.planMode)}`,
|
|
1713
|
+
`plan_mode_phase: ${formatPlanModePhase(detail.data.plan_mode_phase)}`,
|
|
1128
1714
|
`viewStep: ${contentDetail?.data?.viewStep ?? detail.data.viewStep ?? '-'}`,
|
|
1129
1715
|
`percentage: ${contentDetail?.data?.percentage ?? detail.data.percentage ?? '-'}`,
|
|
1130
1716
|
]);
|
|
@@ -1263,8 +1849,20 @@ function buildOutlineParams(snapshot, args) {
|
|
|
1263
1849
|
async function triggerOutlineGeneration(client, snapshot, params) {
|
|
1264
1850
|
await postJson(client, '/task/preSet', params);
|
|
1265
1851
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1852
|
+
if (Number(params.planMode) === 1) {
|
|
1853
|
+
await setPlanModePhase(client, params.cid, 1);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// task_status=6 means "file parsing" in the Xique backend. It is not a
|
|
1857
|
+
// signal that this is a multi-bid/EPC task. Only route through the
|
|
1858
|
+
// special-selection endpoint when the caller actually supplied a
|
|
1859
|
+
// multi-bid or EPC selection; ordinary tasks must use the normal
|
|
1860
|
+
// generation endpoint even while the task is transitioning from parse.
|
|
1861
|
+
const hasSpecialSelection = Boolean(
|
|
1862
|
+
String(params.multiBidId ?? snapshot.multiBidId ?? '').trim()
|
|
1863
|
+
|| String(params.epcEngineerType ?? snapshot.epcEngineerType ?? '').trim()
|
|
1864
|
+
);
|
|
1865
|
+
if (hasSpecialSelection) {
|
|
1268
1866
|
await getJson(client, '/proxy/multiBiddingChoice', compactObject({
|
|
1269
1867
|
uuid: snapshot.uuid,
|
|
1270
1868
|
multiBidId: params.multiBidId ?? snapshot.multiBidId,
|
|
@@ -1277,6 +1875,241 @@ async function triggerOutlineGeneration(client, snapshot, params) {
|
|
|
1277
1875
|
return 'generateByRequirement';
|
|
1278
1876
|
}
|
|
1279
1877
|
|
|
1878
|
+
async function setPlanModePhase(client, cid, planModePhase) {
|
|
1879
|
+
const response = await postJson(client, '/task/setPlanModePhase', { cid, planModePhase });
|
|
1880
|
+
const returned = normalizePlanModePhase(response?.data?.planModePhase);
|
|
1881
|
+
return returned === null ? planModePhase : returned;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function ensurePlanningTask(snapshot) {
|
|
1885
|
+
if (Number(normalizePlanModeValue(snapshot?.planMode)) !== 1) {
|
|
1886
|
+
throw new Error('This command requires a planning-mode task. Start the outline with --plan-mode planning first.');
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function confirmPlanAction(args, action, details = []) {
|
|
1891
|
+
if (booleanOption(args, 'yes')) {
|
|
1892
|
+
return true;
|
|
1893
|
+
}
|
|
1894
|
+
if (!canPromptForInput()) {
|
|
1895
|
+
throw new Error(`Plan action "${action}" changes the task or starts generation. Review the result, then rerun with --yes.`);
|
|
1896
|
+
}
|
|
1897
|
+
return runPromptSession(args, async context => {
|
|
1898
|
+
context.promptStream.write(`\nPlan action: ${action}\n`);
|
|
1899
|
+
details.filter(Boolean).forEach(detail => context.promptStream.write(` ${detail}\n`));
|
|
1900
|
+
return promptYesNo(context, `Continue with ${action}`, false);
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function readPlanFeedbackArgs(args) {
|
|
1905
|
+
const entries = [];
|
|
1906
|
+
const quote = normalizePlanText(stringOption(args, 'quote'));
|
|
1907
|
+
const inputValues = [
|
|
1908
|
+
...ensureArray(args.text),
|
|
1909
|
+
...ensureArray(args.correction),
|
|
1910
|
+
...ensureArray(args.feedback),
|
|
1911
|
+
];
|
|
1912
|
+
inputValues.forEach(value => {
|
|
1913
|
+
const text = normalizePlanText(value);
|
|
1914
|
+
if (text) {
|
|
1915
|
+
entries.push({ quote, text });
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
const fileValue = stringOption(args, 'file');
|
|
1920
|
+
if (!fileValue) {
|
|
1921
|
+
return entries;
|
|
1922
|
+
}
|
|
1923
|
+
const filePath = path.resolve(fileValue);
|
|
1924
|
+
ensureFileExists(filePath);
|
|
1925
|
+
let parsed;
|
|
1926
|
+
try {
|
|
1927
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
|
|
1928
|
+
} catch {
|
|
1929
|
+
throw new Error(`Failed to parse plan feedback JSON: ${filePath}`);
|
|
1930
|
+
}
|
|
1931
|
+
const rows = Array.isArray(parsed)
|
|
1932
|
+
? parsed
|
|
1933
|
+
: (parsed?.feedback || parsed?.corrections || parsed?.chatList || []);
|
|
1934
|
+
if (!Array.isArray(rows)) {
|
|
1935
|
+
throw new Error('Plan feedback JSON must be an array or contain feedback, corrections, or chatList.');
|
|
1936
|
+
}
|
|
1937
|
+
rows.forEach((row, index) => {
|
|
1938
|
+
if (typeof row === 'string') {
|
|
1939
|
+
const text = normalizePlanText(row);
|
|
1940
|
+
if (text) entries.push({ quote: '', text });
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
1944
|
+
throw new Error(`Plan feedback item ${index + 1} must be a string or JSON object.`);
|
|
1945
|
+
}
|
|
1946
|
+
const text = normalizePlanText(row.text ?? row.content ?? row.user_comments);
|
|
1947
|
+
if (!text) {
|
|
1948
|
+
throw new Error(`Plan feedback item ${index + 1} has no text/content/user_comments value.`);
|
|
1949
|
+
}
|
|
1950
|
+
entries.push({ quote: normalizePlanText(row.quote ?? row.quote_paragraph), text });
|
|
1951
|
+
});
|
|
1952
|
+
return entries;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function normalizePlanFeedback(value) {
|
|
1956
|
+
if (!Array.isArray(value)) {
|
|
1957
|
+
return [];
|
|
1958
|
+
}
|
|
1959
|
+
return value
|
|
1960
|
+
.map(item => ({
|
|
1961
|
+
quote: normalizePlanText(item?.quote ?? item?.quote_paragraph),
|
|
1962
|
+
text: normalizePlanText(item?.text ?? item?.content ?? item?.user_comments),
|
|
1963
|
+
}))
|
|
1964
|
+
.filter(item => Boolean(item.text));
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function mergePlanFeedback(current, additions) {
|
|
1968
|
+
const seen = new Set();
|
|
1969
|
+
return [...current, ...additions].filter(item => {
|
|
1970
|
+
const normalized = {
|
|
1971
|
+
quote: normalizePlanText(item?.quote),
|
|
1972
|
+
text: normalizePlanText(item?.text),
|
|
1973
|
+
};
|
|
1974
|
+
if (!normalized.text) {
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
const key = `${normalized.quote}\n${normalized.text}`;
|
|
1978
|
+
if (seen.has(key)) {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
seen.add(key);
|
|
1982
|
+
return true;
|
|
1983
|
+
}).map(item => ({
|
|
1984
|
+
quote: normalizePlanText(item.quote),
|
|
1985
|
+
text: normalizePlanText(item.text),
|
|
1986
|
+
}));
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function extractPlanAiOutlineRows(answer) {
|
|
1990
|
+
const candidates = [
|
|
1991
|
+
answer?.modifiedChapters,
|
|
1992
|
+
answer?.outline,
|
|
1993
|
+
answer?.outLine,
|
|
1994
|
+
Array.isArray(answer) ? answer : null,
|
|
1995
|
+
];
|
|
1996
|
+
const rows = candidates.find(Array.isArray);
|
|
1997
|
+
return Array.isArray(rows) ? rows : [];
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function buildPlanAiOutlineCandidate(cid, prompt, conversationId, rows) {
|
|
2001
|
+
// The frontend's AI-apply path submits text/theme/important without IDs.
|
|
2002
|
+
// Let the backend assign IDs when it saves the accepted final outline;
|
|
2003
|
+
// synthetic local IDs would otherwise be mistaken for database node IDs.
|
|
2004
|
+
const candidateRows = buildPlanAiOutlineUpdateRows(rows);
|
|
2005
|
+
return {
|
|
2006
|
+
format: 'xq-cli-plan-ai-outline@1',
|
|
2007
|
+
cid,
|
|
2008
|
+
generatedAt: new Date().toISOString(),
|
|
2009
|
+
prompt,
|
|
2010
|
+
conversationId: conversationId || undefined,
|
|
2011
|
+
outLine: candidateRows,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function buildPlanAiOutlineUpdateRows(rows, options = {}) {
|
|
2016
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2017
|
+
throw new Error('AI outline candidate must contain at least one chapter.');
|
|
2018
|
+
}
|
|
2019
|
+
return rows.map((row, index) => {
|
|
2020
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
2021
|
+
throw new Error(`AI outline chapter ${index + 1} must be a JSON object.`);
|
|
2022
|
+
}
|
|
2023
|
+
const text = normalizePlanText(row.text ?? row.title);
|
|
2024
|
+
if (!text) {
|
|
2025
|
+
throw new Error(`AI outline chapter ${index + 1} has an empty text field.`);
|
|
2026
|
+
}
|
|
2027
|
+
const theme = Array.isArray(row.theme)
|
|
2028
|
+
? row.theme.map(normalizePlanText).filter(Boolean).join('\n')
|
|
2029
|
+
: normalizePlanText(row.theme ?? row.content);
|
|
2030
|
+
const important = normalizePlanImportant(row.important, index);
|
|
2031
|
+
const sequelFlag = normalizePlanSequelFlag(row.sequelFlag, index);
|
|
2032
|
+
const id = options.preserveIds ? normalizePlanText(row.id) : '';
|
|
2033
|
+
return compactObject({
|
|
2034
|
+
id: id || undefined,
|
|
2035
|
+
text,
|
|
2036
|
+
theme,
|
|
2037
|
+
updateFlag: Number(row.updateFlag) === 1 ? 1 : 0,
|
|
2038
|
+
important,
|
|
2039
|
+
sequelFlag,
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function buildPlanServerOutlineRows(rows) {
|
|
2045
|
+
return buildPlanAiOutlineUpdateRows(rows);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function resolvePlanDirectoryOutline(args, state, cid, outlineDetail) {
|
|
2049
|
+
const explicitFile = stringOption(args, 'outlineFile');
|
|
2050
|
+
if (explicitFile) {
|
|
2051
|
+
const filePath = path.resolve(explicitFile);
|
|
2052
|
+
const document = readOutlineEditFile(filePath);
|
|
2053
|
+
validateOutlineEditCid(document, cid, filePath);
|
|
2054
|
+
return {
|
|
2055
|
+
source: 'outline file',
|
|
2056
|
+
rows: buildPlanAiOutlineUpdateRows(extractOutlineRows(document)),
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const candidate = state.tasks?.[cid]?.planAiOutlineCandidate;
|
|
2061
|
+
if (candidate?.outLine) {
|
|
2062
|
+
return {
|
|
2063
|
+
source: 'AI candidate',
|
|
2064
|
+
rows: buildPlanAiOutlineUpdateRows(candidate.outLine),
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (!isOutlineReadyForCli(outlineDetail)) {
|
|
2069
|
+
throw new Error('The outline is not ready. Confirm and wait for the outline before generating the final directory.');
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
source: 'server outline',
|
|
2073
|
+
rows: buildPlanServerOutlineRows(getOutlineRows(outlineDetail)),
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function normalizePlanImportant(value, index) {
|
|
2078
|
+
if (value === undefined || value === null || value === '') {
|
|
2079
|
+
return 2;
|
|
2080
|
+
}
|
|
2081
|
+
const parsed = Number(value);
|
|
2082
|
+
if (parsed !== 1 && parsed !== 2) {
|
|
2083
|
+
throw new Error(`Outline important for chapter ${index + 1} must be 1 or 2.`);
|
|
2084
|
+
}
|
|
2085
|
+
return parsed;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function normalizePlanSequelFlag(value, index) {
|
|
2089
|
+
if (value === undefined || value === null || value === '') {
|
|
2090
|
+
return undefined;
|
|
2091
|
+
}
|
|
2092
|
+
const parsed = Number(value);
|
|
2093
|
+
if (parsed !== 0 && parsed !== 1) {
|
|
2094
|
+
throw new Error(`Outline sequelFlag for chapter ${index + 1} must be 0 or 1.`);
|
|
2095
|
+
}
|
|
2096
|
+
return parsed;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function normalizePlanText(value) {
|
|
2100
|
+
return value === undefined || value === null ? '' : String(value).trim();
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function writePlanAiOutlineFile(outputOption, cid, document) {
|
|
2104
|
+
const resolvedOutput = path.resolve(outputOption);
|
|
2105
|
+
const outputIsDirectory = !path.extname(resolvedOutput);
|
|
2106
|
+
const fileName = `outline-ai-${normalizeDownloadFileName(cid) || 'task'}.json`;
|
|
2107
|
+
const finalPath = outputIsDirectory ? path.join(resolvedOutput, fileName) : resolvedOutput;
|
|
2108
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
2109
|
+
fs.writeFileSync(finalPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
|
2110
|
+
return finalPath;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
1280
2113
|
function buildFileMetadata(files, args) {
|
|
1281
2114
|
const typeValues = ensureArray(args.type);
|
|
1282
2115
|
const purposeValues = ensureArray(args.purpose);
|
|
@@ -1335,6 +2168,32 @@ async function pollParseUntilReady(client, state, cid, uuid, args) {
|
|
|
1335
2168
|
});
|
|
1336
2169
|
}
|
|
1337
2170
|
|
|
2171
|
+
async function ensurePeriodReady(client, state, cid, args) {
|
|
2172
|
+
return pollUntil(async () => {
|
|
2173
|
+
const response = await getJson(client, `/engineering/checkPeriod/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
2174
|
+
const data = response?.data || {};
|
|
2175
|
+
saveTaskSnapshot(state, cid, {
|
|
2176
|
+
periodStatus: data.status,
|
|
2177
|
+
contractPeriod: data.contractPeriod,
|
|
2178
|
+
contractStartDate: data.contractStartDate,
|
|
2179
|
+
contractEndDate: data.contractEndDate,
|
|
2180
|
+
});
|
|
2181
|
+
const status = Number(data.status);
|
|
2182
|
+
if (status === 1) {
|
|
2183
|
+
return { done: true, data: { phase: 'ready', ...data }, progressLabel: 'period_status=1' };
|
|
2184
|
+
}
|
|
2185
|
+
if (status === 2) {
|
|
2186
|
+
return { done: true, data: { phase: 'confirmation_required', ...data }, progressLabel: 'period_status=2' };
|
|
2187
|
+
}
|
|
2188
|
+
return { done: false, data: null, progressLabel: `period_status=${data.status ?? '-'}` };
|
|
2189
|
+
}, {
|
|
2190
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
2191
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
2192
|
+
quiet: booleanOption(args, 'json'),
|
|
2193
|
+
waitingText: 'waiting engineering period extraction',
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
1338
2197
|
function isParseReadyForNextStep(success) {
|
|
1339
2198
|
const parsed = Number(success);
|
|
1340
2199
|
return parsed === 1 || parsed === 4;
|
|
@@ -1371,6 +2230,7 @@ async function waitForOutlineReady(client, state, cid, uuid, args) {
|
|
|
1371
2230
|
|
|
1372
2231
|
const outlineData = outlineDetail?.data || {};
|
|
1373
2232
|
const hasOutline = isOutlineReadyForCli(outlineData);
|
|
2233
|
+
const planModePhase = normalizePlanModePhase(taskDetail.data?.plan_mode_phase);
|
|
1374
2234
|
const referenceData = referenceStatus?.data || {};
|
|
1375
2235
|
const referencePhase = normalizeReferenceStatus(referenceData.status);
|
|
1376
2236
|
if (referencePhase !== null) {
|
|
@@ -1492,14 +2352,14 @@ async function waitForOutlineReady(client, state, cid, uuid, args) {
|
|
|
1492
2352
|
return {
|
|
1493
2353
|
done: false,
|
|
1494
2354
|
data: null,
|
|
1495
|
-
progressLabel: `success=${parseSuccess}, task_status=${taskDetail.data?.task_status ?? '-'}, directory_status=${taskDetail.data?.directory_portion_status ?? '-'}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
|
|
2355
|
+
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 ?? '-'}`,
|
|
1496
2356
|
};
|
|
1497
2357
|
}
|
|
1498
2358
|
|
|
1499
2359
|
return {
|
|
1500
2360
|
done: false,
|
|
1501
2361
|
data: null,
|
|
1502
|
-
progressLabel: `task_status=${taskDetail.data?.task_status ?? '-'}, directory_status=${taskDetail.data?.directory_portion_status ?? '-'}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
|
|
2362
|
+
progressLabel: `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 ?? '-'}`,
|
|
1503
2363
|
};
|
|
1504
2364
|
}, {
|
|
1505
2365
|
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
@@ -1908,7 +2768,23 @@ function isContentRunningOrDone(detail) {
|
|
|
1908
2768
|
async function pollUntil(checkFn, options) {
|
|
1909
2769
|
const startedAt = Date.now();
|
|
1910
2770
|
while (true) {
|
|
1911
|
-
|
|
2771
|
+
let result;
|
|
2772
|
+
try {
|
|
2773
|
+
result = await checkFn();
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
// A transient connection reset must not turn an already accepted
|
|
2776
|
+
// asynchronous backend task into a false failure. Read requests
|
|
2777
|
+
// are retried by getJson; this catch keeps polling the same cid
|
|
2778
|
+
// when a response stream still closes during that retry window.
|
|
2779
|
+
if (!isRetryableNetworkError(error)) {
|
|
2780
|
+
throw error;
|
|
2781
|
+
}
|
|
2782
|
+
result = {
|
|
2783
|
+
done: false,
|
|
2784
|
+
data: null,
|
|
2785
|
+
progressLabel: `transient network error; retrying (${String(error?.message || error)})`,
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
1912
2788
|
if (result.done) {
|
|
1913
2789
|
return result.data;
|
|
1914
2790
|
}
|
|
@@ -1958,6 +2834,30 @@ function normalizeReferenceStatus(value) {
|
|
|
1958
2834
|
return Number.isNaN(parsed) ? null : parsed;
|
|
1959
2835
|
}
|
|
1960
2836
|
|
|
2837
|
+
function normalizePlanModeValue(value) {
|
|
2838
|
+
if (value === undefined || value === null || value === '') {
|
|
2839
|
+
return null;
|
|
2840
|
+
}
|
|
2841
|
+
const parsed = Number(value);
|
|
2842
|
+
return Number.isNaN(parsed) ? String(value).trim() : parsed;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
function normalizePlanModePhase(value) {
|
|
2846
|
+
if (value === undefined || value === null || value === '') {
|
|
2847
|
+
return null;
|
|
2848
|
+
}
|
|
2849
|
+
const parsed = Number(value);
|
|
2850
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 3 ? parsed : null;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function formatPlanModePhase(value) {
|
|
2854
|
+
const normalized = normalizePlanModePhase(value);
|
|
2855
|
+
if (normalized === null) {
|
|
2856
|
+
return '-';
|
|
2857
|
+
}
|
|
2858
|
+
return `${normalized}:${PLAN_MODE_PHASE_LABELS[normalized] || normalized}`;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
1961
2861
|
function parseOutlineReferenceType(value) {
|
|
1962
2862
|
if (value === undefined || value === null || value === '') {
|
|
1963
2863
|
return null;
|
|
@@ -2777,13 +3677,17 @@ function collectIssueHints(issues, args) {
|
|
|
2777
3677
|
}
|
|
2778
3678
|
|
|
2779
3679
|
function saveLoginState(state, options) {
|
|
2780
|
-
const
|
|
3680
|
+
const resultData = options.result?.data || {};
|
|
3681
|
+
const userInfo = options.user || resultData?.user || resultData || {};
|
|
3682
|
+
const username = userInfo.username || userInfo.userName || resultData?.userName || options.name || '';
|
|
2781
3683
|
state.baseUrl = options.baseUrl;
|
|
2782
|
-
state.
|
|
3684
|
+
state.authType = options.authType || (options.apiKey ? 'api-key' : 'token');
|
|
3685
|
+
state.apiKey = state.authType === 'api-key' ? String(options.apiKey || '') : '';
|
|
3686
|
+
state.token = state.authType === 'token' ? String(resultData.token || options.token || '') : '';
|
|
2783
3687
|
state.user = {
|
|
2784
3688
|
name: options.name || username,
|
|
2785
3689
|
username,
|
|
2786
|
-
userId:
|
|
3690
|
+
userId: userInfo.userId || userInfo.id || resultData?.userId || '',
|
|
2787
3691
|
sourceFrom: options.sourceFrom,
|
|
2788
3692
|
loggedInAt: new Date().toISOString(),
|
|
2789
3693
|
};
|
|
@@ -2968,13 +3872,15 @@ function createAuthorizedClient(args, state) {
|
|
|
2968
3872
|
if (!baseUrl) {
|
|
2969
3873
|
throw new Error('Missing base URL. Run login first or pass --base-url.');
|
|
2970
3874
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
3875
|
+
const credential = resolveAuthCredential(args, state);
|
|
3876
|
+
if (!credential.value) {
|
|
3877
|
+
throw new Error('Missing local API key or token. Run login first.');
|
|
2973
3878
|
}
|
|
2974
|
-
return createClient(baseUrl,
|
|
3879
|
+
return createClient(baseUrl, credential.value);
|
|
2975
3880
|
}
|
|
2976
3881
|
|
|
2977
3882
|
function createClient(baseUrl, token = '') {
|
|
3883
|
+
const authorization = normalizeAuthorizationCredential(token);
|
|
2978
3884
|
return axios.create({
|
|
2979
3885
|
baseURL: baseUrl,
|
|
2980
3886
|
timeout: 1_200_000,
|
|
@@ -2982,19 +3888,54 @@ function createClient(baseUrl, token = '') {
|
|
|
2982
3888
|
maxContentLength: Infinity,
|
|
2983
3889
|
httpAgent: HTTP_AGENT,
|
|
2984
3890
|
httpsAgent: HTTPS_AGENT,
|
|
2985
|
-
headers:
|
|
3891
|
+
headers: authorization
|
|
2986
3892
|
? {
|
|
2987
|
-
Authorization:
|
|
3893
|
+
Authorization: authorization,
|
|
2988
3894
|
}
|
|
2989
3895
|
: {},
|
|
2990
3896
|
validateStatus: () => true,
|
|
2991
3897
|
});
|
|
2992
3898
|
}
|
|
2993
3899
|
|
|
3900
|
+
function resolveAuthCredential(args, state) {
|
|
3901
|
+
const envApiKey = normalizeApiKey(process.env.XQ_API_KEY);
|
|
3902
|
+
if (envApiKey) {
|
|
3903
|
+
return { value: envApiKey, type: 'api-key-env' };
|
|
3904
|
+
}
|
|
3905
|
+
const stateApiKey = normalizeApiKey(state?.apiKey);
|
|
3906
|
+
if (stateApiKey) {
|
|
3907
|
+
return { value: stateApiKey, type: 'api-key-config' };
|
|
3908
|
+
}
|
|
3909
|
+
const stateToken = normalizeApiKey(state?.token);
|
|
3910
|
+
if (stateToken) {
|
|
3911
|
+
return { value: stateToken, type: 'token' };
|
|
3912
|
+
}
|
|
3913
|
+
return { value: '', type: 'missing' };
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
function normalizeAuthorizationCredential(value) {
|
|
3917
|
+
const normalized = String(value || '').trim();
|
|
3918
|
+
if (!normalized) {
|
|
3919
|
+
return '';
|
|
3920
|
+
}
|
|
3921
|
+
if (normalized.toLowerCase().startsWith('bearer ')) {
|
|
3922
|
+
return normalized;
|
|
3923
|
+
}
|
|
3924
|
+
return `Bearer ${normalized}`;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
function normalizeApiKey(value) {
|
|
3928
|
+
const normalized = String(value || '').trim();
|
|
3929
|
+
return normalized || '';
|
|
3930
|
+
}
|
|
3931
|
+
|
|
2994
3932
|
async function getJson(client, url, params = {}, options = {}) {
|
|
2995
3933
|
const response = await requestWithRetry(async () => {
|
|
2996
3934
|
return client.get(url, { params });
|
|
2997
|
-
},
|
|
3935
|
+
}, {
|
|
3936
|
+
retries: DEFAULT_RETRY_COUNT,
|
|
3937
|
+
...options,
|
|
3938
|
+
});
|
|
2998
3939
|
return unwrapApiResponse(response);
|
|
2999
3940
|
}
|
|
3000
3941
|
|
|
@@ -3050,7 +3991,7 @@ function isRetryableNetworkError(error) {
|
|
|
3050
3991
|
if (RETRYABLE_NETWORK_CODES.has(code)) {
|
|
3051
3992
|
return true;
|
|
3052
3993
|
}
|
|
3053
|
-
return /socket disconnected|econnreset|tls connection|network error|timeout/i.test(message);
|
|
3994
|
+
return /socket disconnected|econnreset|tls connection|network error|timeout|stream has been aborted|socket hang up|premature close|aborted/i.test(message);
|
|
3054
3995
|
}
|
|
3055
3996
|
|
|
3056
3997
|
function unwrapApiResponse(response) {
|
|
@@ -3466,7 +4407,9 @@ function normalizeBaseUrl(value) {
|
|
|
3466
4407
|
function createDefaultState() {
|
|
3467
4408
|
return {
|
|
3468
4409
|
baseUrl: DEFAULT_BASE_URL,
|
|
4410
|
+
apiKey: '',
|
|
3469
4411
|
token: '',
|
|
4412
|
+
authType: '',
|
|
3470
4413
|
user: {},
|
|
3471
4414
|
lastCid: '',
|
|
3472
4415
|
tasks: {},
|
|
@@ -3484,7 +4427,9 @@ function readState() {
|
|
|
3484
4427
|
...defaultState,
|
|
3485
4428
|
...parsed,
|
|
3486
4429
|
baseUrl: normalizeBaseUrl(parsed?.baseUrl || defaultState.baseUrl),
|
|
4430
|
+
apiKey: normalizeApiKey(parsed?.apiKey),
|
|
3487
4431
|
token: parsed?.token || '',
|
|
4432
|
+
authType: parsed?.authType || (parsed?.apiKey ? 'api-key' : parsed?.token ? 'token' : ''),
|
|
3488
4433
|
user: parsed?.user && typeof parsed.user === 'object' ? parsed.user : {},
|
|
3489
4434
|
lastCid: parsed?.lastCid || '',
|
|
3490
4435
|
tasks: parsed?.tasks && typeof parsed.tasks === 'object' ? parsed.tasks : {},
|
|
@@ -3525,6 +4470,7 @@ function pickTaskSnapshot(source) {
|
|
|
3525
4470
|
base: source.base,
|
|
3526
4471
|
pageScopeCode: source.pageScopeCode,
|
|
3527
4472
|
task_status: source.task_status,
|
|
4473
|
+
plan_mode_phase: source.plan_mode_phase,
|
|
3528
4474
|
viewStep: source.viewStep,
|
|
3529
4475
|
percentage: source.percentage,
|
|
3530
4476
|
content_portion_status: source.content_portion_status,
|