@xqyz/xq-cli 0.3.6 → 0.3.8

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 +35 -2
  2. package/package.json +1 -1
  3. package/src/cli.mjs +102 -7
package/CHANGELOG.md CHANGED
@@ -10,12 +10,45 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
- ### 正文阶段安全闸门复核
13
+ ## [0.3.8 / WorkBuddy Plugin 0.10.21 / SkillHub 1.0.5] - 2026-08-25
14
+
15
+ ### 规划模式卡住保护
16
+
17
+ - 解析唤醒后的 `success=2` 只允许在 30 秒内等待后台一致性同步;状态没有推进时返回可诊断的 `TASK_PARSE_NOT_ADVANCED`,不再继续等待到 10/30 分钟超时。
18
+ - WorkBuddy Plugin 固定依赖 `@xqyz/xq-cli@0.3.8`,并把最低版本校验同步提升,避免旧 CLI 重新进入不完整链路。
19
+ - SkillHub 安装说明明确要求重启后核对实际加载的 Plugin/CLI 版本;旧 MCP 进程仍在运行时不得宣称已更新。
20
+ - 新增回归覆盖:`multiBiddingChoice` 延迟推进、持续 `success=2` 快速失败,以及旧版本运行时拒绝。
21
+
22
+ ## [0.3.7 / WorkBuddy Plugin 0.10.20 / SkillHub 1.0.4] - 2026-08-25
23
+
24
+ ### 规划模式解析唤醒修复
25
+
26
+ - 修复规划模式新任务只调用 `/task/preSet`、没有调用 `/proxy/multiBiddingChoice` 的问题。
27
+ - 规划模式现在与 Web 端一致:先保存用户配置,再唤醒异步解析;解析结果补齐后才展示“智能解读确认”,不会提前生成大纲。
28
+ - 保留规划模式确认闸门:用户确认智能解读后才调用 `plan confirm-outline`,不会因为修复唤醒链路而跳过大纲、目录审阅。
29
+ - 新增真实模拟回归测试,覆盖解析字段为空时的唤醒顺序和禁止提前调用正式生成接口。
30
+
31
+ ## [0.3.6 / WorkBuddy Plugin 0.10.19] - 2026-08-24
32
+
33
+ ### 安装和二次更新闭环
34
+
35
+ - SkillHub 正式身份统一为 `@org-kkw9656k/xique-ai`;安装器会同步完整 Skill 到正式目录、修正 `.skills_store_lock.json`,并把历史 `xique-bid` 重复目录移入可恢复的 `.skill-backup`。
36
+ - 用户明确安装或更新时通过 npm 受控标签 `workbuddy-stable` 获取最新稳定安装器;安装器解析自身版本后,仍将精确版本写入 `mcp.json`,所以 WorkBuddy 重启不会因浮动标签加载不同构建。
37
+ - 一次更新同时覆盖 Skill、MCP Plugin 和内置 xq-cli。MCP 配置保留其他服务,更新前备份;新 MCP 必须完成 `initialize` 握手,失败会恢复旧配置且不宣称成功。
38
+ - Windows 支持带空格的 Node/npx 路径;macOS/Linux 不依赖 `sort -V`,支持 WorkBuddy 的 `bin/node` 和扁平 Node 布局,并检查执行权限。
39
+ - 新增新装、旧 Skill 身份迁移、锁记录修复、重复更新幂等、MCP 启动失败回滚及 Windows 安装回归测试。
40
+ - npm 正式包并发安装验收发现共享 npx 缓存会在 Windows 产生 `ENOTEMPTY` 和包文件缺失;`0.10.16` 起为每次 bootstrap 使用独立缓存,并为安装器增加跨进程锁和喜鹊专属 MCP 运行缓存。
41
+ - `0.10.17` 在 Windows 上通过隐藏 Node 进程延迟清理 bootstrap 缓存,并在下次安装时回收陈旧目录,避免 npx 子进程短暂占用造成残留。
42
+ - `0.10.18` 查询 `workbuddy-stable` 时强制 `--prefer-online`,避免 npm 本地元数据缓存让已移动的稳定标签继续解析到旧版本。
43
+ - `0.10.19` 改为读取 npm `dist-tags --json` 中的 `workbuddy-stable` 字段,不再依赖 `包@标签 version` 的 CDN 别名解析,避免标签移动后的短暂旧值。
44
+ - 安装前先从 npm 官方 registry 解析 `workbuddy-stable` 的精确版本,安装器再校验自身版本;即使 npx 复用错误缓存,也会在写配置前失败。`0.10.15` 已发布但不作为稳定安装版本。
45
+
46
+ ### 正文阶段安全闸门
14
47
 
15
48
  - 大纲和目录实际保存前强制读取喜鹊后台权威任务状态,不再因为本机没有任务缓存而绕过“正文已开始/已完成”保护。
16
49
  - 状态校验放在 `--dry-run` 和用户确认之后、写接口之前,保留离线差异预览能力,同时覆盖确认后发生的并发状态变化。
17
50
  - 新增回归测试:无本地缓存的已完成任务不会调用大纲或目录保存接口;纯预览不会额外依赖任务详情接口。
18
- - 该修复当前属于未发布改动;npm 上的 `@xqyz/xq-cli@0.3.5` 和已发布 WorkBuddy Plugin `0.10.14` 尚未包含它,发布前必须递增 xq-cli 补丁版本并同步 Plugin 依赖。
51
+ - `@xqyz/xq-cli@0.3.6` 包含该修复,WorkBuddy Plugin `0.10.19` 固定依赖此版本。
19
52
 
20
53
  ### WorkBuddy Plugin 0.10.14 / SkillHub 1.0.11 发布记录
21
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xqyz/xq-cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "CLI for xique bid-book task workflows",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import http from 'http';
5
5
  import https from 'https';
6
6
  import os from 'os';
7
7
  import path from 'path';
8
- import readline from 'readline/promises';
8
+ import readline from 'node:readline/promises';
9
9
  import { spawn } from 'child_process';
10
10
  import { randomUUID } from 'crypto';
11
11
  import axios from 'axios';
@@ -35,6 +35,12 @@ const DEFAULT_BASE_URL = 'https://ai.bidfile.qianlima.com/api';
35
35
  const DEFAULT_PARSE_INTERVAL_MS = 10_000;
36
36
  const DEFAULT_TASK_INTERVAL_MS = 10_000;
37
37
  const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
38
+ // multiBiddingChoice acknowledges the request before the extraction service
39
+ // necessarily updates getFileAsyncResult. Give that short async hand-off a
40
+ // bounded window, then fail with diagnostics instead of entering the long
41
+ // outline polling timeout.
42
+ const DEFAULT_PARSER_WAKEUP_TIMEOUT_MS = 30_000;
43
+ const PARSER_WAKEUP_INTERVAL_MS = 2_000;
38
44
  const DEFAULT_SOURCE_FROM = 'xique';
39
45
  const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
40
46
  const DEFAULT_EXPORT_TEMPLATE_INDEX = 1;
@@ -1409,8 +1415,8 @@ async function handleOutlineUnlocked(args, state) {
1409
1415
 
1410
1416
  const planningMode = Number(params.planMode) === 1;
1411
1417
  const triggerMode = planningMode
1412
- ? await preparePlanningAnalysis(client, params)
1413
- : await triggerOutlineGeneration(client, outlineSnapshot, params);
1418
+ ? await preparePlanningAnalysis(client, state, outlineSnapshot, params, effectiveArgs)
1419
+ : await triggerOutlineGeneration(client, state, outlineSnapshot, params, effectiveArgs);
1414
1420
  saveTaskSnapshot(state, cid, {
1415
1421
  ...outlineSnapshot,
1416
1422
  ...pickTaskSnapshot(params),
@@ -2056,7 +2062,7 @@ function buildOutlineParams(snapshot, args) {
2056
2062
  });
2057
2063
  }
2058
2064
 
2059
- async function triggerOutlineGeneration(client, snapshot, params) {
2065
+ async function triggerOutlineGeneration(client, state, snapshot, params, args = {}) {
2060
2066
  // The web flow has two deliberately different paths:
2061
2067
  //
2062
2068
  // * legacy tasks (task_status === 0) already contain the parsed
@@ -2080,16 +2086,99 @@ async function triggerOutlineGeneration(client, snapshot, params) {
2080
2086
  multiBidId: params.multiBidId ?? snapshot.multiBidId,
2081
2087
  epcEngineerType: normalizeEpcEngineerTypeForProxy(params.epcEngineerType ?? snapshot.epcEngineerType),
2082
2088
  }));
2089
+ await verifyParserWakeUp(client, state, snapshot.cid, snapshot.uuid, params, false, args);
2083
2090
  return 'multiBiddingChoice';
2084
2091
  }
2085
2092
 
2086
- async function preparePlanningAnalysis(client, params) {
2087
- // The backend intentionally pauses plan-mode tasks after parsing. The user
2088
- // must review that analysis before generateDirectoryByCid starts the outline.
2093
+ async function preparePlanningAnalysis(client, state, snapshot, params, args = {}) {
2094
+ // Planning mode pauses before outline generation, but new tasks still need
2095
+ // the same parser wake-up as quick mode. Without multiBiddingChoice,
2096
+ // autoGenerateDirectoryJobHandler never fills requirement/rating/plan and
2097
+ // the analysis poll remains pending until the local timeout.
2098
+ if (Number(snapshot.task_status) === 0) {
2099
+ // Legacy tasks already contain parsed fields and do not need the
2100
+ // asynchronous parser wake-up endpoint.
2101
+ await postJson(client, '/task/preSet', params);
2102
+ return 'planningAnalysisPrepared';
2103
+ }
2089
2104
  await postJson(client, '/task/preSet', params);
2105
+ await getJson(client, '/proxy/multiBiddingChoice', compactObject({
2106
+ uuid: snapshot.uuid,
2107
+ multiBidId: params.multiBidId ?? snapshot.multiBidId,
2108
+ epcEngineerType: normalizeEpcEngineerTypeForProxy(params.epcEngineerType ?? snapshot.epcEngineerType),
2109
+ }));
2110
+ await verifyParserWakeUp(client, state, snapshot.cid, snapshot.uuid, params, true, args);
2090
2111
  return 'planningAnalysisPrepared';
2091
2112
  }
2092
2113
 
2114
+ async function verifyParserWakeUp(client, state, cid, uuid, params, planningMode, args = {}) {
2115
+ const expectedMultiBidId = String(params.multiBidId || '').trim();
2116
+ const requestedTimeoutMs = secondsToMs(
2117
+ numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000),
2118
+ );
2119
+ const timeoutMs = Math.min(DEFAULT_PARSER_WAKEUP_TIMEOUT_MS, requestedTimeoutMs);
2120
+ const deadline = Date.now() + timeoutMs;
2121
+ let latest = null;
2122
+
2123
+ // `multiBiddingChoice` returns HTTP 200 before the extraction record is
2124
+ // necessarily visible to the next read. Allow a short consistency window,
2125
+ // but never fall back to the long generation poll while the backend still
2126
+ // advertises "waiting for package selection" (success=2).
2127
+ for (let attempt = 0; ; attempt += 1) {
2128
+ const [taskResponse, parseStatus] = await Promise.all([
2129
+ getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT }),
2130
+ fetchParseStatus(client, state, cid, uuid),
2131
+ ]);
2132
+ const taskDetail = taskResponse?.data || {};
2133
+ const parseSuccess = Number(parseStatus.success);
2134
+ const selectedMultiBidId = String(
2135
+ parseStatus.choiceMultiBiddingId
2136
+ || taskDetail.choiceMultiBiddingId
2137
+ || taskDetail.multiBidId
2138
+ || '',
2139
+ ).trim();
2140
+ const savedPlanMode = normalizePlanModeValue(taskDetail.planMode);
2141
+ latest = {
2142
+ taskDetail,
2143
+ parseStatus,
2144
+ parseSuccess,
2145
+ selectedMultiBidId,
2146
+ savedPlanMode,
2147
+ };
2148
+ saveTaskSnapshot(state, cid, pickTaskSnapshot({
2149
+ ...taskDetail,
2150
+ ...parseStatus,
2151
+ cid,
2152
+ uuid,
2153
+ choiceMultiBiddingId: selectedMultiBidId,
2154
+ }));
2155
+
2156
+ const selectionRequired = expectedMultiBidId && expectedMultiBidId !== 'notMultiBid';
2157
+ const selectionMatches = !selectionRequired || selectedMultiBidId === expectedMultiBidId;
2158
+ const parseAdvanced = [1, 3, 4].includes(parseSuccess);
2159
+ const planModeMatches = !planningMode || savedPlanMode === 1;
2160
+ if (parseAdvanced && selectionMatches && planModeMatches) {
2161
+ return latest;
2162
+ }
2163
+
2164
+ const remainingMs = deadline - Date.now();
2165
+ if (remainingMs <= 0) {
2166
+ break;
2167
+ }
2168
+ await sleep(Math.min(PARSER_WAKEUP_INTERVAL_MS, remainingMs));
2169
+ }
2170
+
2171
+ const selected = latest?.selectedMultiBidId || '-';
2172
+ const expected = expectedMultiBidId || '-';
2173
+ const success = latest?.parseSuccess ?? '-';
2174
+ const savedPlanMode = latest?.savedPlanMode ?? '-';
2175
+ throw new Error(
2176
+ `[TASK_PARSE_NOT_ADVANCED] 标包/规划配置提交后后端状态没有推进,已停止等待。` +
2177
+ `期望标包=${expected},后端标包=${selected},解析状态 success=${success},` +
2178
+ `后端 planMode=${savedPlanMode}。请不要重复提交同一任务;先查询任务状态或重新安装最新版 WorkBuddy 插件。`,
2179
+ );
2180
+ }
2181
+
2093
2182
  async function setPlanModePhase(client, cid, planModePhase) {
2094
2183
  const response = await postJson(client, '/task/setPlanModePhase', { cid, planModePhase });
2095
2184
  const returned = normalizePlanModePhase(response?.data?.planModePhase);
@@ -2607,6 +2696,11 @@ async function waitForPlanningAnalysisReady(client, state, cid, uuid, args) {
2607
2696
  ? await fetchParseStatus(client, state, cid, uuid)
2608
2697
  : { success: 1 };
2609
2698
  const parseSuccess = Number(parseStatus.success);
2699
+ if (parseSuccess === 2) {
2700
+ throw new Error(
2701
+ '[TASK_PARSE_NOT_ADVANCED] 规划模式仍处于等待标包/EPC选择(success=2),未进入解析阶段,已停止等待。',
2702
+ );
2703
+ }
2610
2704
  const issues = collectParseIssues(parseStatus);
2611
2705
  if (parseSuccess === 4 && issues.length > 0) {
2612
2706
  const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
@@ -4912,6 +5006,7 @@ function pickTaskSnapshot(source) {
4912
5006
  blindGenerationMode: source.blindGenerationMode || source.blindBidConfirm?.blindGenerationMode,
4913
5007
  multiBidId: source.multiBidId,
4914
5008
  multiBidType: source.multiBidType,
5009
+ choiceMultiBiddingId: source.choiceMultiBiddingId,
4915
5010
  epcEngineerType: source.epcEngineerType,
4916
5011
  outlineReferenceType: source.outlineReferenceType,
4917
5012
  referenceStatus: source.referenceStatus,