@xqyz/xq-cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +27 -5
  2. package/package.json +1 -1
  3. package/src/cli.mjs +122 -12
package/CHANGELOG.md CHANGED
@@ -10,12 +10,26 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
- ### WorkBuddy
13
+ ## [0.3.2] - 2026-08-17
14
+
15
+ ### xq-cli / WorkBuddy 规划模式
16
+
17
+ - 规划模式首次 `outline --plan-mode 1` 在解析完成后明确返回“智能解读待审核”,不再误报大纲已完成或提前进入正文。
18
+ - WorkBuddy 新增智能解读审核、补充/修正暂存和确认续跑流程;确认时才调用喜鹊 `generateDirectoryByCid` 生成大纲。
19
+ - 新增 CLI 与 WorkBuddy 回归测试,验证同一 `cid` / `runId` 不重复提交。
20
+
21
+ ### xq-cli `0.3.1` / WorkBuddy `0.10.1`
22
+
23
+ - 修复普通项目在后端 `task_status=6`(文件解析中)时被误判为多标包/EPC,导致没有调用正常大纲生成接口、任务长时间停留在解析状态的问题。
24
+ - 轮询 GET 请求统一增加网络重试,并识别 `stream has been aborted`、连接提前关闭等瞬时传输错误;不会因一次流中断误报生成失败。
25
+ - WorkBuddy 对大纲阶段的传输中断保留同一 `cid` 继续核验,禁止重复创建任务;新增回归测试覆盖该恢复路径。
26
+ - 本次配套版本:`@xqyz/xq-cli@0.3.1`、`@xqyz/workbuddy-plugin-xique@0.10.1`、Connector `0.10.1`。
14
27
 
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 的功能、依赖、测试和发布结果。
28
+ ### WorkBuddy / SkillHub(待发布)
29
+
30
+ - 修复 SkillHub 旧版 Skill 将用户明确选择的“规划”强制改为“快速”的规则冲突;规划模式现在必须传递 `--plan-mode 1`。
31
+ - 增加 Skill 文案回归断言,禁止再次出现“固定使用计划模式=快速”或“不得接受规划模式”。
32
+ - 发布清单新增 SkillHub 同步、客户本地更新和新会话验收步骤,避免 npm Plugin 与 SkillHub Skill 版本脱节。
19
33
 
20
34
  ## [0.3.0] - 2026-08-14
21
35
 
@@ -26,6 +40,14 @@
26
40
  - AI 大纲候选不再生成本地伪章节 ID;最终保存请求与前端一致,仅提交章节内容、主题、重要度和续写标记,交由后台分配节点 ID。
27
41
  - 增加规划模式模拟 API 回归测试,覆盖意见提交、AI 候选不提前落库、候选确认和最终目录生成的顺序。
28
42
 
43
+ ## [0.10.0] - 2026-08-14
44
+
45
+ ### WorkBuddy
46
+
47
+ - 固定依赖 `@xqyz/xq-cli@0.3.0`,支持完整规划模式命令链。
48
+ - 更新 Skill、安装脚本、MCP Server、MCP App 和 Connector 启动参数到 `0.10.0`。
49
+ - 通过 32 项 WorkBuddy 功能测试和 npm 包冒烟测试;npm `latest` 已指向 `0.10.0`。
50
+
29
51
  ## [0.2.1] - 2026-08-11
30
52
 
31
53
  ### 安全性
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xqyz/xq-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for xique bid-book task workflows",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/cli.mjs CHANGED
@@ -1278,7 +1278,10 @@ async function handleOutline(args, state) {
1278
1278
  }
1279
1279
  const params = buildOutlineParams(outlineSnapshot, effectiveArgs);
1280
1280
 
1281
- const triggerMode = await triggerOutlineGeneration(client, outlineSnapshot, params);
1281
+ const planningMode = Number(params.planMode) === 1;
1282
+ const triggerMode = planningMode
1283
+ ? await preparePlanningAnalysis(client, params)
1284
+ : await triggerOutlineGeneration(client, outlineSnapshot, params);
1282
1285
  saveTaskSnapshot(state, cid, {
1283
1286
  ...outlineSnapshot,
1284
1287
  ...pickTaskSnapshot(params),
@@ -1288,7 +1291,9 @@ async function handleOutline(args, state) {
1288
1291
 
1289
1292
  let waitResult = null;
1290
1293
  if (booleanOption(effectiveArgs, 'wait')) {
1291
- waitResult = await waitForOutlineReady(client, state, cid, outlineSnapshot.uuid, effectiveArgs);
1294
+ waitResult = planningMode
1295
+ ? await waitForPlanningAnalysisReady(client, state, cid, outlineSnapshot.uuid, effectiveArgs)
1296
+ : await waitForOutlineReady(client, state, cid, outlineSnapshot.uuid, effectiveArgs);
1292
1297
  }
1293
1298
 
1294
1299
  const summaryLines = [
@@ -1299,7 +1304,9 @@ async function handleOutline(args, state) {
1299
1304
  `triggerMode: ${triggerMode}`,
1300
1305
  ];
1301
1306
 
1302
- if (waitResult?.phase === 'ready') {
1307
+ if (waitResult?.phase === 'analysis_ready') {
1308
+ summaryLines.push('planning analysis ready: review it, optionally add feedback, then run plan confirm-outline to generate the outline.');
1309
+ } else if (waitResult?.phase === 'ready') {
1303
1310
  summaryLines.push(`outline ready: rootChapters=${countOutlineRoots(waitResult)}`);
1304
1311
  } else if (waitResult?.phase === 'supplement_required') {
1305
1312
  summaryLines.push(`outline blocked: supplement required (${waitResult.issues.join(', ')})`);
@@ -1849,12 +1856,16 @@ function buildOutlineParams(snapshot, args) {
1849
1856
  async function triggerOutlineGeneration(client, snapshot, params) {
1850
1857
  await postJson(client, '/task/preSet', params);
1851
1858
 
1852
- if (Number(params.planMode) === 1) {
1853
- await setPlanModePhase(client, params.cid, 1);
1854
- }
1855
-
1856
- const hasTaskStatus = snapshot.task_status !== undefined && snapshot.task_status !== null;
1857
- if (hasTaskStatus && Number(snapshot.task_status) !== 0) {
1859
+ // task_status=6 means "file parsing" in the Xique backend. It is not a
1860
+ // signal that this is a multi-bid/EPC task. Only route through the
1861
+ // special-selection endpoint when the caller actually supplied a
1862
+ // multi-bid or EPC selection; ordinary tasks must use the normal
1863
+ // generation endpoint even while the task is transitioning from parse.
1864
+ const hasSpecialSelection = Boolean(
1865
+ String(params.multiBidId ?? snapshot.multiBidId ?? '').trim()
1866
+ || String(params.epcEngineerType ?? snapshot.epcEngineerType ?? '').trim()
1867
+ );
1868
+ if (hasSpecialSelection) {
1858
1869
  await getJson(client, '/proxy/multiBiddingChoice', compactObject({
1859
1870
  uuid: snapshot.uuid,
1860
1871
  multiBidId: params.multiBidId ?? snapshot.multiBidId,
@@ -1867,6 +1878,13 @@ async function triggerOutlineGeneration(client, snapshot, params) {
1867
1878
  return 'generateByRequirement';
1868
1879
  }
1869
1880
 
1881
+ async function preparePlanningAnalysis(client, params) {
1882
+ // The backend intentionally pauses plan-mode tasks after parsing. The user
1883
+ // must review that analysis before generateDirectoryByCid starts the outline.
1884
+ await postJson(client, '/task/preSet', params);
1885
+ return 'planningAnalysisPrepared';
1886
+ }
1887
+
1870
1888
  async function setPlanModePhase(client, cid, planModePhase) {
1871
1889
  const response = await postJson(client, '/task/setPlanModePhase', { cid, planModePhase });
1872
1890
  const returned = normalizePlanModePhase(response?.data?.planModePhase);
@@ -2361,6 +2379,79 @@ async function waitForOutlineReady(client, state, cid, uuid, args) {
2361
2379
  });
2362
2380
  }
2363
2381
 
2382
+ async function waitForPlanningAnalysisReady(client, state, cid, uuid, args) {
2383
+ const handledIssues = new Set();
2384
+
2385
+ return pollUntil(async () => {
2386
+ const taskResponse = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`);
2387
+ const taskDetail = taskResponse?.data || {};
2388
+ saveTaskSnapshot(state, cid, pickTaskSnapshot(taskDetail));
2389
+
2390
+ if (Number(taskDetail.task_status) === 7) {
2391
+ return {
2392
+ done: true,
2393
+ data: { phase: 'failed', taskDetail },
2394
+ progressLabel: 'task_status=7',
2395
+ };
2396
+ }
2397
+
2398
+ const parseStatus = uuid
2399
+ ? await fetchParseStatus(client, state, cid, uuid)
2400
+ : { success: 1 };
2401
+ const parseSuccess = Number(parseStatus.success);
2402
+ const issues = collectParseIssues(parseStatus);
2403
+ if (parseSuccess === 4 && issues.length > 0) {
2404
+ const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
2405
+ if (autoIgnore) {
2406
+ await submitIntermediateChoice(client, autoIgnore.payload);
2407
+ handledIssues.add(autoIgnore.issue);
2408
+ return {
2409
+ done: false,
2410
+ data: null,
2411
+ progressLabel: `ignored ${autoIgnore.issue}, waiting planning analysis`,
2412
+ };
2413
+ }
2414
+ return {
2415
+ done: true,
2416
+ data: {
2417
+ phase: 'supplement_required',
2418
+ issues,
2419
+ parseStatus,
2420
+ taskDetail,
2421
+ },
2422
+ progressLabel: `supplement_required=${issues.join(',')}`,
2423
+ };
2424
+ }
2425
+
2426
+ if (parseSuccess === 1 || (
2427
+ parseSuccess > 0
2428
+ && String(taskDetail.requirement || '').trim()
2429
+ && String(taskDetail.rating || '').trim()
2430
+ )) {
2431
+ return {
2432
+ done: true,
2433
+ data: {
2434
+ phase: 'analysis_ready',
2435
+ taskDetail,
2436
+ parseStatus,
2437
+ },
2438
+ progressLabel: 'planning analysis ready; waiting for outline confirmation',
2439
+ };
2440
+ }
2441
+
2442
+ return {
2443
+ done: false,
2444
+ data: null,
2445
+ progressLabel: `success=${parseSuccess}, task_status=${taskDetail.task_status ?? '-'}, planning analysis pending`,
2446
+ };
2447
+ }, {
2448
+ intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
2449
+ timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
2450
+ quiet: booleanOption(args, 'json'),
2451
+ waitingText: 'waiting planning analysis',
2452
+ });
2453
+ }
2454
+
2364
2455
  async function pollContentUntilReady(client, state, cid, args) {
2365
2456
  let unfinishedTriggered = false;
2366
2457
  let lastUnfinishedTriggerAt = 0;
@@ -2760,7 +2851,23 @@ function isContentRunningOrDone(detail) {
2760
2851
  async function pollUntil(checkFn, options) {
2761
2852
  const startedAt = Date.now();
2762
2853
  while (true) {
2763
- const result = await checkFn();
2854
+ let result;
2855
+ try {
2856
+ result = await checkFn();
2857
+ } catch (error) {
2858
+ // A transient connection reset must not turn an already accepted
2859
+ // asynchronous backend task into a false failure. Read requests
2860
+ // are retried by getJson; this catch keeps polling the same cid
2861
+ // when a response stream still closes during that retry window.
2862
+ if (!isRetryableNetworkError(error)) {
2863
+ throw error;
2864
+ }
2865
+ result = {
2866
+ done: false,
2867
+ data: null,
2868
+ progressLabel: `transient network error; retrying (${String(error?.message || error)})`,
2869
+ };
2870
+ }
2764
2871
  if (result.done) {
2765
2872
  return result.data;
2766
2873
  }
@@ -3908,7 +4015,10 @@ function normalizeApiKey(value) {
3908
4015
  async function getJson(client, url, params = {}, options = {}) {
3909
4016
  const response = await requestWithRetry(async () => {
3910
4017
  return client.get(url, { params });
3911
- }, options);
4018
+ }, {
4019
+ retries: DEFAULT_RETRY_COUNT,
4020
+ ...options,
4021
+ });
3912
4022
  return unwrapApiResponse(response);
3913
4023
  }
3914
4024
 
@@ -3964,7 +4074,7 @@ function isRetryableNetworkError(error) {
3964
4074
  if (RETRYABLE_NETWORK_CODES.has(code)) {
3965
4075
  return true;
3966
4076
  }
3967
- return /socket disconnected|econnreset|tls connection|network error|timeout/i.test(message);
4077
+ return /socket disconnected|econnreset|tls connection|network error|timeout|stream has been aborted|socket hang up|premature close|aborted/i.test(message);
3968
4078
  }
3969
4079
 
3970
4080
  function unwrapApiResponse(response) {