@xqyz/xq-cli 0.2.1 → 0.3.0
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 +16 -0
- package/README.md +87 -3
- package/package.json +1 -1
- package/src/cli.mjs +932 -13
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
### WorkBuddy
|
|
14
|
+
|
|
15
|
+
- 修复计划模式默认值:未提供 `planMode` 时明确使用快速(`--plan-mode 0`),用户明确选择规划时保留 `--plan-mode 1`,不再发生模式被错误回退的问题。
|
|
16
|
+
- 准备发布 WorkBuddy Plugin/Connector `0.9.4`:恢复配置页“计划模式=快速/规划”选择,并将用户选择原样传递给 xq-cli。
|
|
17
|
+
- 统一 Skill、安装脚本、MCP Server 和 Connector 的版本指向,避免继续加载 `0.9.3` 的快速模式固定包。
|
|
18
|
+
- 新增 [版本与迭代记录](./docs/VERSION-HISTORY.md),记录 xq-cli、Plugin、Connector 的功能、依赖、测试和发布结果。
|
|
19
|
+
|
|
20
|
+
## [0.3.0] - 2026-08-14
|
|
21
|
+
|
|
22
|
+
### xq-cli
|
|
23
|
+
|
|
24
|
+
- 新增 `xq-cli plan` 规划模式命令链,覆盖补充/修正信息、确认生成/重新生成大纲、AI 调整大纲候选、确认采用候选、生成最终目录、重新生成目录、目录查看/修改和正文续写。
|
|
25
|
+
- 补充/修正与 AI 调整结果先保存在本地任务状态,只有在用户明确确认 `plan confirm-outline` 或 `plan directory-generate` / `plan directory-regenerate` 后才会触发对应后台写入或生成请求。
|
|
26
|
+
- AI 大纲候选不再生成本地伪章节 ID;最终保存请求与前端一致,仅提交章节内容、主题、重要度和续写标记,交由后台分配节点 ID。
|
|
27
|
+
- 增加规划模式模拟 API 回归测试,覆盖意见提交、AI 候选不提前落库、候选确认和最终目录生成的顺序。
|
|
28
|
+
|
|
13
29
|
## [0.2.1] - 2026-08-11
|
|
14
30
|
|
|
15
31
|
### 安全性
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Project execution rules for agents live in [AGENTS.md](./AGENTS.md). That file d
|
|
|
7
7
|
Project maintenance documents:
|
|
8
8
|
|
|
9
9
|
- [Version changelog](./CHANGELOG.md)
|
|
10
|
+
- [Version and iteration history](./docs/VERSION-HISTORY.md)
|
|
10
11
|
- [Release process](./docs/RELEASING.md)
|
|
11
12
|
|
|
12
13
|
## Install from npm
|
|
@@ -53,7 +54,8 @@ npm install -g .\xqyz-xq-cli-0.2.0.tgz
|
|
|
53
54
|
```bash
|
|
54
55
|
xq-cli login
|
|
55
56
|
xq-cli login --name your-account --password your-password
|
|
56
|
-
xq-cli login --
|
|
57
|
+
xq-cli login --api-key xq_sk_xxx
|
|
58
|
+
xq-cli login --browser
|
|
57
59
|
xq-cli wizard
|
|
58
60
|
xq-cli wizard --file D:\bids\tender.docx
|
|
59
61
|
xq-cli init --file D:\bids\tender.docx --wait
|
|
@@ -72,9 +74,9 @@ xq-cli export --cid <cid> --interactive --out D:\output
|
|
|
72
74
|
xq-cli export --cid <cid> --out D:\output
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
Fresh installs default to the production API address `https://ai.bidfile.qianlima.com/api`. Use `--base-url` only when you need to target another
|
|
77
|
+
Fresh installs default to the production API address `https://ai.bidfile.qianlima.com/api`. Use `--base-url` only when you need to target another API endpoint.
|
|
76
78
|
|
|
77
|
-
`login` now defaults to browser authorization. If you explicitly pass `--name` and `--password`, the CLI falls back to password login.
|
|
79
|
+
`login` now defaults to browser authorization. If you explicitly pass `--name` and `--password`, the CLI falls back to password login. If you pass `--api-key`, it saves the API key locally and uses it for later requests.
|
|
78
80
|
|
|
79
81
|
`directory view` reads the complete generated directory tree without triggering outline, directory, or content generation. Add `--json` for the raw API structure, or `--out <file-or-directory>` to save a UTF-8 JSON document.
|
|
80
82
|
|
|
@@ -128,6 +130,39 @@ xq-cli outline update --cid <cid> --file D:\output\outline.json --yes --allow-de
|
|
|
128
130
|
|
|
129
131
|
The update command automatically calculates `updateFlag` by comparing the edited file with the latest server outline. Changes to chapter titles or themes may cause the backend to regenerate affected child directories asynchronously.
|
|
130
132
|
|
|
133
|
+
## 完整规划模式流程
|
|
134
|
+
|
|
135
|
+
规划模式不是一次命令跑到底,而是把前端的“智能解读 → 大纲 → 目录 → 正文”拆成可恢复的阶段。先用普通 `outline --plan-mode planning` 启动任务;拿到 `cid` 后,用 `plan` 子命令推进后续阶段:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# 1. 补充或修正招标文件理解(只写入本地待提交区,不会请求后台)
|
|
139
|
+
xq-cli plan status --cid <cid> --json
|
|
140
|
+
xq-cli plan feedback --cid <cid> --text "补充项目实施边界" --quote "原文片段"
|
|
141
|
+
xq-cli plan correction --cid <cid> --file D:\output\corrections.json
|
|
142
|
+
|
|
143
|
+
# 2. 用户确认后,提交意见并生成/重新生成大纲
|
|
144
|
+
xq-cli plan confirm-outline --cid <cid> --yes --wait --json
|
|
145
|
+
|
|
146
|
+
# 3. 查看大纲;需要 AI 调整时先生成候选,不会立即改后台
|
|
147
|
+
xq-cli plan outline-view --cid <cid> --json
|
|
148
|
+
xq-cli plan outline-ai-adjust --cid <cid> --prompt "拆分实施方案和质量保障" --out D:\output
|
|
149
|
+
xq-cli plan outline-ai-apply --cid <cid> --file D:\output\outline-ai-<cid>.json --yes
|
|
150
|
+
|
|
151
|
+
# 4. 用户确认候选大纲后,保存大纲并生成最终目录(不会生成正文)
|
|
152
|
+
xq-cli plan directory-generate --cid <cid> --yes --wait
|
|
153
|
+
|
|
154
|
+
# 5. 查看/编辑目录;如果需要重新生成目录,必须明确确认
|
|
155
|
+
xq-cli plan directory-view --cid <cid> --json
|
|
156
|
+
xq-cli plan directory-update --cid <cid> --file D:\output\directory.json --dry-run
|
|
157
|
+
xq-cli plan directory-regenerate --cid <cid> --yes --wait
|
|
158
|
+
|
|
159
|
+
# 6. 目录确认后才开始正文和导出
|
|
160
|
+
xq-cli plan write --cid <cid> --wait
|
|
161
|
+
xq-cli export --cid <cid> --out D:\output
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
要点:`plan status` 是恢复和重跑前的权威入口;`plan feedback` / `plan correction` 只写本地暂存,不会重复请求后台;`plan outline-ai-adjust` 会请求 AI 但只返回候选,`plan outline-ai-apply` 仍只写本地候选且不带伪造章节 ID。只有 `plan confirm-outline`、`plan directory-generate` / `plan directory-regenerate` 明确确认后才会产生后台写入或启动阶段;首次目录必须使用 `directory-generate`,已有目录覆盖才使用 `directory-regenerate`。
|
|
165
|
+
|
|
131
166
|
## Interactive menus
|
|
132
167
|
|
|
133
168
|
Use the full wizard if you want a CLI flow close to the frontend configuration panel:
|
|
@@ -155,6 +190,55 @@ xq-cli outline --cid <cid> --interactive --wait
|
|
|
155
190
|
xq-cli export --cid <cid> --interactive --out D:\output
|
|
156
191
|
```
|
|
157
192
|
|
|
193
|
+
## Agent / Codex usage
|
|
194
|
+
|
|
195
|
+
When another agent (Codex, OpenCLI, or an MCP wrapper) invokes `xq-cli`, it
|
|
196
|
+
must treat bid generation as a stateful sequence rather than a single command:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
init (source file) -> outline (confirmed config + plan mode) -> review
|
|
200
|
+
-> directory review (planning mode) -> write -> export
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`--plan-mode` belongs on `outline`, not on `write` or `export`:
|
|
204
|
+
|
|
205
|
+
- `0` / `quick`: enter outline generation quickly; use when the user requests
|
|
206
|
+
a quick first run or explicitly chooses 快速.
|
|
207
|
+
- `1` / `planning`: enable the backend's fuller planning phase; it can take
|
|
208
|
+
longer, so use `--wait` and do not shorten the polling timeout. Use the
|
|
209
|
+
dedicated `plan` commands to collect corrections, review AI candidates,
|
|
210
|
+
generate/re-generate the directory, and stop at every confirmation gate.
|
|
211
|
+
|
|
212
|
+
The backend may report `plan_mode_phase=0..3` (`analysis`, `outline`,
|
|
213
|
+
`directory`, `content`) while polling. This is progress information, not a
|
|
214
|
+
subcommand. If the CLI returns `reference_selection_required` or
|
|
215
|
+
`supplement_required`, stop and ask the user for the required choice/input;
|
|
216
|
+
never invent it. `outline view` is read-only and safe for verification.
|
|
217
|
+
|
|
218
|
+
For planning mode, the agent must pause at three human confirmation gates:
|
|
219
|
+
|
|
220
|
+
1. collect any `plan feedback` / `plan correction`, then call
|
|
221
|
+
`plan confirm-outline` only after the user confirms generation;
|
|
222
|
+
2. show the outline. `plan outline-ai-adjust` returns an editable candidate,
|
|
223
|
+
and `plan outline-ai-apply` accepts it locally without writing the backend;
|
|
224
|
+
3. after the user confirms the final outline, call `plan directory-generate`,
|
|
225
|
+
show the directory, and wait for final directory confirmation before
|
|
226
|
+
`plan write`.
|
|
227
|
+
|
|
228
|
+
Use `plan directory-regenerate` only after an explicit request to replace the
|
|
229
|
+
existing directory. It is not a retry/status command. Fast mode has only the
|
|
230
|
+
outline confirmation gate and can then proceed to `write`.
|
|
231
|
+
|
|
232
|
+
For non-TTY or JSON automation, do not silently choose generation settings.
|
|
233
|
+
Ask for the missing configuration, print the final summary, wait for a clear
|
|
234
|
+
confirmation (`确认`, `开始`, or `就按这个跑`), then run the commands. Validate
|
|
235
|
+
enum values with `xq-cli choices outline --json` and
|
|
236
|
+
`xq-cli choices export --json` before constructing arguments. See
|
|
237
|
+
[`AGENTS.md`](./AGENTS.md) for the complete confirmation order and value map.
|
|
238
|
+
For the complete meaning of every outline/export choice, defaults, image-style
|
|
239
|
+
dependencies, EPC/multi-bid rules, margins, colors, and `--style-json`, see
|
|
240
|
+
[`docs/AGENT-PARAMETER-GUIDE.md`](./docs/AGENT-PARAMETER-GUIDE.md).
|
|
241
|
+
|
|
158
242
|
## Config choices
|
|
159
243
|
|
|
160
244
|
`xq-cli` now exposes the same major bid-book generation settings that the frontend outline/download flow uses.
|
package/package.json
CHANGED
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,6 +1849,10 @@ function buildOutlineParams(snapshot, args) {
|
|
|
1263
1849
|
async function triggerOutlineGeneration(client, snapshot, params) {
|
|
1264
1850
|
await postJson(client, '/task/preSet', params);
|
|
1265
1851
|
|
|
1852
|
+
if (Number(params.planMode) === 1) {
|
|
1853
|
+
await setPlanModePhase(client, params.cid, 1);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1266
1856
|
const hasTaskStatus = snapshot.task_status !== undefined && snapshot.task_status !== null;
|
|
1267
1857
|
if (hasTaskStatus && Number(snapshot.task_status) !== 0) {
|
|
1268
1858
|
await getJson(client, '/proxy/multiBiddingChoice', compactObject({
|
|
@@ -1277,6 +1867,241 @@ async function triggerOutlineGeneration(client, snapshot, params) {
|
|
|
1277
1867
|
return 'generateByRequirement';
|
|
1278
1868
|
}
|
|
1279
1869
|
|
|
1870
|
+
async function setPlanModePhase(client, cid, planModePhase) {
|
|
1871
|
+
const response = await postJson(client, '/task/setPlanModePhase', { cid, planModePhase });
|
|
1872
|
+
const returned = normalizePlanModePhase(response?.data?.planModePhase);
|
|
1873
|
+
return returned === null ? planModePhase : returned;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function ensurePlanningTask(snapshot) {
|
|
1877
|
+
if (Number(normalizePlanModeValue(snapshot?.planMode)) !== 1) {
|
|
1878
|
+
throw new Error('This command requires a planning-mode task. Start the outline with --plan-mode planning first.');
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async function confirmPlanAction(args, action, details = []) {
|
|
1883
|
+
if (booleanOption(args, 'yes')) {
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
if (!canPromptForInput()) {
|
|
1887
|
+
throw new Error(`Plan action "${action}" changes the task or starts generation. Review the result, then rerun with --yes.`);
|
|
1888
|
+
}
|
|
1889
|
+
return runPromptSession(args, async context => {
|
|
1890
|
+
context.promptStream.write(`\nPlan action: ${action}\n`);
|
|
1891
|
+
details.filter(Boolean).forEach(detail => context.promptStream.write(` ${detail}\n`));
|
|
1892
|
+
return promptYesNo(context, `Continue with ${action}`, false);
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function readPlanFeedbackArgs(args) {
|
|
1897
|
+
const entries = [];
|
|
1898
|
+
const quote = normalizePlanText(stringOption(args, 'quote'));
|
|
1899
|
+
const inputValues = [
|
|
1900
|
+
...ensureArray(args.text),
|
|
1901
|
+
...ensureArray(args.correction),
|
|
1902
|
+
...ensureArray(args.feedback),
|
|
1903
|
+
];
|
|
1904
|
+
inputValues.forEach(value => {
|
|
1905
|
+
const text = normalizePlanText(value);
|
|
1906
|
+
if (text) {
|
|
1907
|
+
entries.push({ quote, text });
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
const fileValue = stringOption(args, 'file');
|
|
1912
|
+
if (!fileValue) {
|
|
1913
|
+
return entries;
|
|
1914
|
+
}
|
|
1915
|
+
const filePath = path.resolve(fileValue);
|
|
1916
|
+
ensureFileExists(filePath);
|
|
1917
|
+
let parsed;
|
|
1918
|
+
try {
|
|
1919
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
|
|
1920
|
+
} catch {
|
|
1921
|
+
throw new Error(`Failed to parse plan feedback JSON: ${filePath}`);
|
|
1922
|
+
}
|
|
1923
|
+
const rows = Array.isArray(parsed)
|
|
1924
|
+
? parsed
|
|
1925
|
+
: (parsed?.feedback || parsed?.corrections || parsed?.chatList || []);
|
|
1926
|
+
if (!Array.isArray(rows)) {
|
|
1927
|
+
throw new Error('Plan feedback JSON must be an array or contain feedback, corrections, or chatList.');
|
|
1928
|
+
}
|
|
1929
|
+
rows.forEach((row, index) => {
|
|
1930
|
+
if (typeof row === 'string') {
|
|
1931
|
+
const text = normalizePlanText(row);
|
|
1932
|
+
if (text) entries.push({ quote: '', text });
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
1936
|
+
throw new Error(`Plan feedback item ${index + 1} must be a string or JSON object.`);
|
|
1937
|
+
}
|
|
1938
|
+
const text = normalizePlanText(row.text ?? row.content ?? row.user_comments);
|
|
1939
|
+
if (!text) {
|
|
1940
|
+
throw new Error(`Plan feedback item ${index + 1} has no text/content/user_comments value.`);
|
|
1941
|
+
}
|
|
1942
|
+
entries.push({ quote: normalizePlanText(row.quote ?? row.quote_paragraph), text });
|
|
1943
|
+
});
|
|
1944
|
+
return entries;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function normalizePlanFeedback(value) {
|
|
1948
|
+
if (!Array.isArray(value)) {
|
|
1949
|
+
return [];
|
|
1950
|
+
}
|
|
1951
|
+
return value
|
|
1952
|
+
.map(item => ({
|
|
1953
|
+
quote: normalizePlanText(item?.quote ?? item?.quote_paragraph),
|
|
1954
|
+
text: normalizePlanText(item?.text ?? item?.content ?? item?.user_comments),
|
|
1955
|
+
}))
|
|
1956
|
+
.filter(item => Boolean(item.text));
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function mergePlanFeedback(current, additions) {
|
|
1960
|
+
const seen = new Set();
|
|
1961
|
+
return [...current, ...additions].filter(item => {
|
|
1962
|
+
const normalized = {
|
|
1963
|
+
quote: normalizePlanText(item?.quote),
|
|
1964
|
+
text: normalizePlanText(item?.text),
|
|
1965
|
+
};
|
|
1966
|
+
if (!normalized.text) {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
const key = `${normalized.quote}\n${normalized.text}`;
|
|
1970
|
+
if (seen.has(key)) {
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
seen.add(key);
|
|
1974
|
+
return true;
|
|
1975
|
+
}).map(item => ({
|
|
1976
|
+
quote: normalizePlanText(item.quote),
|
|
1977
|
+
text: normalizePlanText(item.text),
|
|
1978
|
+
}));
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function extractPlanAiOutlineRows(answer) {
|
|
1982
|
+
const candidates = [
|
|
1983
|
+
answer?.modifiedChapters,
|
|
1984
|
+
answer?.outline,
|
|
1985
|
+
answer?.outLine,
|
|
1986
|
+
Array.isArray(answer) ? answer : null,
|
|
1987
|
+
];
|
|
1988
|
+
const rows = candidates.find(Array.isArray);
|
|
1989
|
+
return Array.isArray(rows) ? rows : [];
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function buildPlanAiOutlineCandidate(cid, prompt, conversationId, rows) {
|
|
1993
|
+
// The frontend's AI-apply path submits text/theme/important without IDs.
|
|
1994
|
+
// Let the backend assign IDs when it saves the accepted final outline;
|
|
1995
|
+
// synthetic local IDs would otherwise be mistaken for database node IDs.
|
|
1996
|
+
const candidateRows = buildPlanAiOutlineUpdateRows(rows);
|
|
1997
|
+
return {
|
|
1998
|
+
format: 'xq-cli-plan-ai-outline@1',
|
|
1999
|
+
cid,
|
|
2000
|
+
generatedAt: new Date().toISOString(),
|
|
2001
|
+
prompt,
|
|
2002
|
+
conversationId: conversationId || undefined,
|
|
2003
|
+
outLine: candidateRows,
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function buildPlanAiOutlineUpdateRows(rows, options = {}) {
|
|
2008
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2009
|
+
throw new Error('AI outline candidate must contain at least one chapter.');
|
|
2010
|
+
}
|
|
2011
|
+
return rows.map((row, index) => {
|
|
2012
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
2013
|
+
throw new Error(`AI outline chapter ${index + 1} must be a JSON object.`);
|
|
2014
|
+
}
|
|
2015
|
+
const text = normalizePlanText(row.text ?? row.title);
|
|
2016
|
+
if (!text) {
|
|
2017
|
+
throw new Error(`AI outline chapter ${index + 1} has an empty text field.`);
|
|
2018
|
+
}
|
|
2019
|
+
const theme = Array.isArray(row.theme)
|
|
2020
|
+
? row.theme.map(normalizePlanText).filter(Boolean).join('\n')
|
|
2021
|
+
: normalizePlanText(row.theme ?? row.content);
|
|
2022
|
+
const important = normalizePlanImportant(row.important, index);
|
|
2023
|
+
const sequelFlag = normalizePlanSequelFlag(row.sequelFlag, index);
|
|
2024
|
+
const id = options.preserveIds ? normalizePlanText(row.id) : '';
|
|
2025
|
+
return compactObject({
|
|
2026
|
+
id: id || undefined,
|
|
2027
|
+
text,
|
|
2028
|
+
theme,
|
|
2029
|
+
updateFlag: Number(row.updateFlag) === 1 ? 1 : 0,
|
|
2030
|
+
important,
|
|
2031
|
+
sequelFlag,
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function buildPlanServerOutlineRows(rows) {
|
|
2037
|
+
return buildPlanAiOutlineUpdateRows(rows);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function resolvePlanDirectoryOutline(args, state, cid, outlineDetail) {
|
|
2041
|
+
const explicitFile = stringOption(args, 'outlineFile');
|
|
2042
|
+
if (explicitFile) {
|
|
2043
|
+
const filePath = path.resolve(explicitFile);
|
|
2044
|
+
const document = readOutlineEditFile(filePath);
|
|
2045
|
+
validateOutlineEditCid(document, cid, filePath);
|
|
2046
|
+
return {
|
|
2047
|
+
source: 'outline file',
|
|
2048
|
+
rows: buildPlanAiOutlineUpdateRows(extractOutlineRows(document)),
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
const candidate = state.tasks?.[cid]?.planAiOutlineCandidate;
|
|
2053
|
+
if (candidate?.outLine) {
|
|
2054
|
+
return {
|
|
2055
|
+
source: 'AI candidate',
|
|
2056
|
+
rows: buildPlanAiOutlineUpdateRows(candidate.outLine),
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
if (!isOutlineReadyForCli(outlineDetail)) {
|
|
2061
|
+
throw new Error('The outline is not ready. Confirm and wait for the outline before generating the final directory.');
|
|
2062
|
+
}
|
|
2063
|
+
return {
|
|
2064
|
+
source: 'server outline',
|
|
2065
|
+
rows: buildPlanServerOutlineRows(getOutlineRows(outlineDetail)),
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function normalizePlanImportant(value, index) {
|
|
2070
|
+
if (value === undefined || value === null || value === '') {
|
|
2071
|
+
return 2;
|
|
2072
|
+
}
|
|
2073
|
+
const parsed = Number(value);
|
|
2074
|
+
if (parsed !== 1 && parsed !== 2) {
|
|
2075
|
+
throw new Error(`Outline important for chapter ${index + 1} must be 1 or 2.`);
|
|
2076
|
+
}
|
|
2077
|
+
return parsed;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function normalizePlanSequelFlag(value, index) {
|
|
2081
|
+
if (value === undefined || value === null || value === '') {
|
|
2082
|
+
return undefined;
|
|
2083
|
+
}
|
|
2084
|
+
const parsed = Number(value);
|
|
2085
|
+
if (parsed !== 0 && parsed !== 1) {
|
|
2086
|
+
throw new Error(`Outline sequelFlag for chapter ${index + 1} must be 0 or 1.`);
|
|
2087
|
+
}
|
|
2088
|
+
return parsed;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function normalizePlanText(value) {
|
|
2092
|
+
return value === undefined || value === null ? '' : String(value).trim();
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function writePlanAiOutlineFile(outputOption, cid, document) {
|
|
2096
|
+
const resolvedOutput = path.resolve(outputOption);
|
|
2097
|
+
const outputIsDirectory = !path.extname(resolvedOutput);
|
|
2098
|
+
const fileName = `outline-ai-${normalizeDownloadFileName(cid) || 'task'}.json`;
|
|
2099
|
+
const finalPath = outputIsDirectory ? path.join(resolvedOutput, fileName) : resolvedOutput;
|
|
2100
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
2101
|
+
fs.writeFileSync(finalPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
|
2102
|
+
return finalPath;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1280
2105
|
function buildFileMetadata(files, args) {
|
|
1281
2106
|
const typeValues = ensureArray(args.type);
|
|
1282
2107
|
const purposeValues = ensureArray(args.purpose);
|
|
@@ -1335,6 +2160,32 @@ async function pollParseUntilReady(client, state, cid, uuid, args) {
|
|
|
1335
2160
|
});
|
|
1336
2161
|
}
|
|
1337
2162
|
|
|
2163
|
+
async function ensurePeriodReady(client, state, cid, args) {
|
|
2164
|
+
return pollUntil(async () => {
|
|
2165
|
+
const response = await getJson(client, `/engineering/checkPeriod/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
2166
|
+
const data = response?.data || {};
|
|
2167
|
+
saveTaskSnapshot(state, cid, {
|
|
2168
|
+
periodStatus: data.status,
|
|
2169
|
+
contractPeriod: data.contractPeriod,
|
|
2170
|
+
contractStartDate: data.contractStartDate,
|
|
2171
|
+
contractEndDate: data.contractEndDate,
|
|
2172
|
+
});
|
|
2173
|
+
const status = Number(data.status);
|
|
2174
|
+
if (status === 1) {
|
|
2175
|
+
return { done: true, data: { phase: 'ready', ...data }, progressLabel: 'period_status=1' };
|
|
2176
|
+
}
|
|
2177
|
+
if (status === 2) {
|
|
2178
|
+
return { done: true, data: { phase: 'confirmation_required', ...data }, progressLabel: 'period_status=2' };
|
|
2179
|
+
}
|
|
2180
|
+
return { done: false, data: null, progressLabel: `period_status=${data.status ?? '-'}` };
|
|
2181
|
+
}, {
|
|
2182
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
2183
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
2184
|
+
quiet: booleanOption(args, 'json'),
|
|
2185
|
+
waitingText: 'waiting engineering period extraction',
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
|
|
1338
2189
|
function isParseReadyForNextStep(success) {
|
|
1339
2190
|
const parsed = Number(success);
|
|
1340
2191
|
return parsed === 1 || parsed === 4;
|
|
@@ -1371,6 +2222,7 @@ async function waitForOutlineReady(client, state, cid, uuid, args) {
|
|
|
1371
2222
|
|
|
1372
2223
|
const outlineData = outlineDetail?.data || {};
|
|
1373
2224
|
const hasOutline = isOutlineReadyForCli(outlineData);
|
|
2225
|
+
const planModePhase = normalizePlanModePhase(taskDetail.data?.plan_mode_phase);
|
|
1374
2226
|
const referenceData = referenceStatus?.data || {};
|
|
1375
2227
|
const referencePhase = normalizeReferenceStatus(referenceData.status);
|
|
1376
2228
|
if (referencePhase !== null) {
|
|
@@ -1492,14 +2344,14 @@ async function waitForOutlineReady(client, state, cid, uuid, args) {
|
|
|
1492
2344
|
return {
|
|
1493
2345
|
done: false,
|
|
1494
2346
|
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 ?? '-'}`,
|
|
2347
|
+
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
2348
|
};
|
|
1497
2349
|
}
|
|
1498
2350
|
|
|
1499
2351
|
return {
|
|
1500
2352
|
done: false,
|
|
1501
2353
|
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 ?? '-'}`,
|
|
2354
|
+
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
2355
|
};
|
|
1504
2356
|
}, {
|
|
1505
2357
|
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
@@ -1958,6 +2810,30 @@ function normalizeReferenceStatus(value) {
|
|
|
1958
2810
|
return Number.isNaN(parsed) ? null : parsed;
|
|
1959
2811
|
}
|
|
1960
2812
|
|
|
2813
|
+
function normalizePlanModeValue(value) {
|
|
2814
|
+
if (value === undefined || value === null || value === '') {
|
|
2815
|
+
return null;
|
|
2816
|
+
}
|
|
2817
|
+
const parsed = Number(value);
|
|
2818
|
+
return Number.isNaN(parsed) ? String(value).trim() : parsed;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function normalizePlanModePhase(value) {
|
|
2822
|
+
if (value === undefined || value === null || value === '') {
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
const parsed = Number(value);
|
|
2826
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 3 ? parsed : null;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
function formatPlanModePhase(value) {
|
|
2830
|
+
const normalized = normalizePlanModePhase(value);
|
|
2831
|
+
if (normalized === null) {
|
|
2832
|
+
return '-';
|
|
2833
|
+
}
|
|
2834
|
+
return `${normalized}:${PLAN_MODE_PHASE_LABELS[normalized] || normalized}`;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
1961
2837
|
function parseOutlineReferenceType(value) {
|
|
1962
2838
|
if (value === undefined || value === null || value === '') {
|
|
1963
2839
|
return null;
|
|
@@ -2777,13 +3653,17 @@ function collectIssueHints(issues, args) {
|
|
|
2777
3653
|
}
|
|
2778
3654
|
|
|
2779
3655
|
function saveLoginState(state, options) {
|
|
2780
|
-
const
|
|
3656
|
+
const resultData = options.result?.data || {};
|
|
3657
|
+
const userInfo = options.user || resultData?.user || resultData || {};
|
|
3658
|
+
const username = userInfo.username || userInfo.userName || resultData?.userName || options.name || '';
|
|
2781
3659
|
state.baseUrl = options.baseUrl;
|
|
2782
|
-
state.
|
|
3660
|
+
state.authType = options.authType || (options.apiKey ? 'api-key' : 'token');
|
|
3661
|
+
state.apiKey = state.authType === 'api-key' ? String(options.apiKey || '') : '';
|
|
3662
|
+
state.token = state.authType === 'token' ? String(resultData.token || options.token || '') : '';
|
|
2783
3663
|
state.user = {
|
|
2784
3664
|
name: options.name || username,
|
|
2785
3665
|
username,
|
|
2786
|
-
userId:
|
|
3666
|
+
userId: userInfo.userId || userInfo.id || resultData?.userId || '',
|
|
2787
3667
|
sourceFrom: options.sourceFrom,
|
|
2788
3668
|
loggedInAt: new Date().toISOString(),
|
|
2789
3669
|
};
|
|
@@ -2968,13 +3848,15 @@ function createAuthorizedClient(args, state) {
|
|
|
2968
3848
|
if (!baseUrl) {
|
|
2969
3849
|
throw new Error('Missing base URL. Run login first or pass --base-url.');
|
|
2970
3850
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
3851
|
+
const credential = resolveAuthCredential(args, state);
|
|
3852
|
+
if (!credential.value) {
|
|
3853
|
+
throw new Error('Missing local API key or token. Run login first.');
|
|
2973
3854
|
}
|
|
2974
|
-
return createClient(baseUrl,
|
|
3855
|
+
return createClient(baseUrl, credential.value);
|
|
2975
3856
|
}
|
|
2976
3857
|
|
|
2977
3858
|
function createClient(baseUrl, token = '') {
|
|
3859
|
+
const authorization = normalizeAuthorizationCredential(token);
|
|
2978
3860
|
return axios.create({
|
|
2979
3861
|
baseURL: baseUrl,
|
|
2980
3862
|
timeout: 1_200_000,
|
|
@@ -2982,15 +3864,47 @@ function createClient(baseUrl, token = '') {
|
|
|
2982
3864
|
maxContentLength: Infinity,
|
|
2983
3865
|
httpAgent: HTTP_AGENT,
|
|
2984
3866
|
httpsAgent: HTTPS_AGENT,
|
|
2985
|
-
headers:
|
|
3867
|
+
headers: authorization
|
|
2986
3868
|
? {
|
|
2987
|
-
Authorization:
|
|
3869
|
+
Authorization: authorization,
|
|
2988
3870
|
}
|
|
2989
3871
|
: {},
|
|
2990
3872
|
validateStatus: () => true,
|
|
2991
3873
|
});
|
|
2992
3874
|
}
|
|
2993
3875
|
|
|
3876
|
+
function resolveAuthCredential(args, state) {
|
|
3877
|
+
const envApiKey = normalizeApiKey(process.env.XQ_API_KEY);
|
|
3878
|
+
if (envApiKey) {
|
|
3879
|
+
return { value: envApiKey, type: 'api-key-env' };
|
|
3880
|
+
}
|
|
3881
|
+
const stateApiKey = normalizeApiKey(state?.apiKey);
|
|
3882
|
+
if (stateApiKey) {
|
|
3883
|
+
return { value: stateApiKey, type: 'api-key-config' };
|
|
3884
|
+
}
|
|
3885
|
+
const stateToken = normalizeApiKey(state?.token);
|
|
3886
|
+
if (stateToken) {
|
|
3887
|
+
return { value: stateToken, type: 'token' };
|
|
3888
|
+
}
|
|
3889
|
+
return { value: '', type: 'missing' };
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
function normalizeAuthorizationCredential(value) {
|
|
3893
|
+
const normalized = String(value || '').trim();
|
|
3894
|
+
if (!normalized) {
|
|
3895
|
+
return '';
|
|
3896
|
+
}
|
|
3897
|
+
if (normalized.toLowerCase().startsWith('bearer ')) {
|
|
3898
|
+
return normalized;
|
|
3899
|
+
}
|
|
3900
|
+
return `Bearer ${normalized}`;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
function normalizeApiKey(value) {
|
|
3904
|
+
const normalized = String(value || '').trim();
|
|
3905
|
+
return normalized || '';
|
|
3906
|
+
}
|
|
3907
|
+
|
|
2994
3908
|
async function getJson(client, url, params = {}, options = {}) {
|
|
2995
3909
|
const response = await requestWithRetry(async () => {
|
|
2996
3910
|
return client.get(url, { params });
|
|
@@ -3466,7 +4380,9 @@ function normalizeBaseUrl(value) {
|
|
|
3466
4380
|
function createDefaultState() {
|
|
3467
4381
|
return {
|
|
3468
4382
|
baseUrl: DEFAULT_BASE_URL,
|
|
4383
|
+
apiKey: '',
|
|
3469
4384
|
token: '',
|
|
4385
|
+
authType: '',
|
|
3470
4386
|
user: {},
|
|
3471
4387
|
lastCid: '',
|
|
3472
4388
|
tasks: {},
|
|
@@ -3484,7 +4400,9 @@ function readState() {
|
|
|
3484
4400
|
...defaultState,
|
|
3485
4401
|
...parsed,
|
|
3486
4402
|
baseUrl: normalizeBaseUrl(parsed?.baseUrl || defaultState.baseUrl),
|
|
4403
|
+
apiKey: normalizeApiKey(parsed?.apiKey),
|
|
3487
4404
|
token: parsed?.token || '',
|
|
4405
|
+
authType: parsed?.authType || (parsed?.apiKey ? 'api-key' : parsed?.token ? 'token' : ''),
|
|
3488
4406
|
user: parsed?.user && typeof parsed.user === 'object' ? parsed.user : {},
|
|
3489
4407
|
lastCid: parsed?.lastCid || '',
|
|
3490
4408
|
tasks: parsed?.tasks && typeof parsed.tasks === 'object' ? parsed.tasks : {},
|
|
@@ -3525,6 +4443,7 @@ function pickTaskSnapshot(source) {
|
|
|
3525
4443
|
base: source.base,
|
|
3526
4444
|
pageScopeCode: source.pageScopeCode,
|
|
3527
4445
|
task_status: source.task_status,
|
|
4446
|
+
plan_mode_phase: source.plan_mode_phase,
|
|
3528
4447
|
viewStep: source.viewStep,
|
|
3529
4448
|
percentage: source.percentage,
|
|
3530
4449
|
content_portion_status: source.content_portion_status,
|