chatccc 0.2.235 → 0.2.237

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.
@@ -818,38 +818,64 @@ export interface RestartSpawnDeps {
818
818
  trace?: typeof appendStartupTrace;
819
819
  /** restart 子进程 stderr 的落盘目录;默认 ~/.chatccc/logs */
820
820
  restartLogDir?: string;
821
+ /** 终端检测(默认按 process.stdout/stderr.isTTY);测试注入 */
822
+ isTty?: () => boolean;
821
823
  }
822
824
 
823
825
  /**
824
826
  * spawn 自重启子进程。
825
827
  *
826
- * stderr 不用 pipe 收集:pipe 读端随父进程退出关闭后,子进程(尤其经 tsx
827
- * 包装)再写 stderr(如飞书 SDK 内部 console.warn)会 EPIPE → uncaughtException
828
- * 整个服务崩溃。改为把 stderr 重定向到磁盘日志文件:子进程继承文件句柄,
829
- * 父进程退出不影响写入,启动诊断同样留痕。若日志文件打开失败,退回 pipe
830
- * 收集(旧行为),并记录 trace。
828
+ * stdout/stderr 按启动方式分流:
829
+ * - **终端(TTY)场景**(用户从 cmd/PowerShell/node.exe 窗口启动):stdio
830
+ * ["ignore", "inherit", "inherit"] 直接继承终端句柄,restart 后窗口日志不中断。
831
+ * 终端句柄的生命周期不随父进程退出而关闭,因此不存在 EPIPE 风险。
832
+ * - **非终端场景**(守护进程/黑匣子等管道或文件启动):stderr 重定向到磁盘日志
833
+ * 文件(restart-*.log),子进程继承文件句柄,父进程退出不影响写入。
834
+ * 不能用 pipe 收集:pipe 读端随父进程退出关闭后,子进程(尤其经 tsx 包装)
835
+ * 再写 stderr(如飞书 SDK 内部 console.warn)会 EPIPE → uncaughtException
836
+ * → 整个服务崩溃。若日志文件打开失败,退回 pipe 收集(旧行为),并记录 trace。
831
837
  */
832
838
  export function spawnRestartChild(deps: RestartSpawnDeps = {}): ChildProcess {
833
839
  const projectRoot = deps.projectRoot ?? PROJECT_ROOT;
834
840
  const spawnImpl = deps.spawnImpl ?? spawn;
835
841
  const trace = deps.trace ?? appendStartupTrace;
836
842
  const restartLogDir = deps.restartLogDir ?? LOG_DIR;
843
+ const isTty = deps.isTty ?? (() => process.stdout.isTTY === true || process.stderr.isTTY === true);
837
844
  const { command, args } = buildRestartSpawnSpec(projectRoot);
838
845
 
839
846
  let stderrFd: number | undefined;
840
847
  const stdio: StdioOptions = ["ignore", "ignore", "pipe"];
841
- try {
842
- mkdirSync(restartLogDir, { recursive: true });
843
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
844
- const restartLogPath = join(restartLogDir, `restart-${timestamp}.log`);
845
- stderrFd = openSync(restartLogPath, "a");
846
- stdio[2] = stderrFd;
847
- } catch (err) {
848
- trace("restart: stderr log open failed, falling back to pipe", {
849
- error: err instanceof Error ? err.message : String(err),
850
- });
848
+ const tty = isTty();
849
+ if (tty) {
850
+ // 终端场景:全部 inherit(含 stdin)。注意 stdin 不能是 "ignore"
851
+ // Windows 上 detached + stdio[0]=ignore 的组合(DETACHED_PROCESS)会让
852
+ // 子进程丢失控制台关联,或在部分 Node/libuv 组合下触发 CREATE_NEW_CONSOLE
853
+ // 弹出新窗口——日志全跑到新窗口,用户当前窗口反而看不到。
854
+ // inherit 让子进程直接复用当前终端句柄,父进程退出后窗口日志不中断,
855
+ // 且终端句柄不随父进程退出关闭,天然无 EPIPE 风险。
856
+ stdio[0] = "inherit";
857
+ stdio[1] = "inherit";
858
+ stdio[2] = "inherit";
859
+ } else {
860
+ try {
861
+ mkdirSync(restartLogDir, { recursive: true });
862
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
863
+ const restartLogPath = join(restartLogDir, `restart-${timestamp}.log`);
864
+ stderrFd = openSync(restartLogPath, "a");
865
+ stdio[2] = stderrFd;
866
+ } catch (err) {
867
+ trace("restart: stderr log open failed, falling back to pipe", {
868
+ error: err instanceof Error ? err.message : String(err),
869
+ });
870
+ }
851
871
  }
852
872
 
873
+ // 运行时自检:记录本次 restart 子进程的启动方式,便于确认日志走向。
874
+ trace("restart: spawn child", {
875
+ isTty: tty,
876
+ stdio: JSON.stringify(stdio),
877
+ });
878
+
853
879
  const child = spawnImpl(command, args, {
854
880
  cwd: projectRoot,
855
881
  detached: true,
package/src/web-ui.ts CHANGED
@@ -637,11 +637,16 @@ async function handleForgetIlink(_req: IncomingMessage, res: ServerResponse): Pr
637
637
  }
638
638
 
639
639
  async function handleClaudeSdkStatus(_req: IncomingMessage, res: ServerResponse): Promise<void> {
640
+ const progress = getLastInstallProgress();
640
641
  jsonReply(res, 200, {
641
642
  installed: isClaudeSdkInstalled(),
642
643
  version: getClaudeSdkInstalledVersion(),
643
644
  running: isInstallRunning(),
644
- progress: getLastInstallProgress(),
645
+ // 展开到顶层:前端轮询 JS 读顶层 phase/message/percent/error(历史契约),
646
+ // 若只放在 progress 嵌套里,前端 s.phase 恒为 undefined → 进度条永不渲染。
647
+ ...progress,
648
+ // 保留嵌套结构,兼容其它调用方。
649
+ progress,
645
650
  });
646
651
  }
647
652
 
@@ -935,7 +940,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
935
940
  <!-- Claude 卡片 -->
936
941
  <div class="agent-card" id="agent-card-claude">
937
942
  <div class="agent-card-header">
938
- <input type="checkbox" class="agent-toggle" id="agent-enable-claude" onchange="onAgentToggle('claude', this.checked)">
943
+ <input type="checkbox" class="agent-toggle" id="agent-enable-claude" onchange="onClaudeToggle(this)">
939
944
  <div class="meta">
940
945
  <div class="name">Claude Code</div>
941
946
  <div class="desc">Anthropic Claude Code CLI<br>模型、effort 均为选填</div>
@@ -975,7 +980,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
975
980
  </div>
976
981
  <div id="claude-engine-progress-text" style="font-size:12px;color:#64748b;margin-top:4px"></div>
977
982
  </div>
978
- <button class="btn btn-outline" id="claude-engine-install-btn" onclick="installClaudeEngine()">安装引擎(约 220MB)</button>
983
+ <button class="btn btn-outline" id="claude-engine-install-btn" onclick="installClaudeEngine()">安装 Claude Code SDK</button>
979
984
  <div class="hint" style="margin-top:6px">ChatCCC 通过 Claude Agent SDK 调用 Claude Code;SDK 引擎按需下载到本机(仅启用 Claude Code 时需要),安装期间请保持网络畅通。</div>
980
985
  </div>
981
986
  </fieldset>
@@ -1596,6 +1601,10 @@ function renderStep2() {
1596
1601
  var cursorOn = isAgentEnabled(c.cursor, CURSOR_FALLBACK_KEYS);
1597
1602
  var codexOn = isAgentEnabled(c.codex, CODEX_FALLBACK_KEYS);
1598
1603
  var cccOn = isAgentEnabled(c.ccc, CCC_FALLBACK_KEYS);
1604
+ // 全新用户:四个 Agent 均无启用/配置痕迹时,只默认勾选 DeepCCC(ccc),其余不勾
1605
+ if (!claudeOn && !cursorOn && !codexOn && !cccOn) {
1606
+ cccOn = true;
1607
+ }
1599
1608
  state.defaultAgent = resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn);
1600
1609
  document.getElementById('agent-enable-claude').checked = claudeOn;
1601
1610
  document.getElementById('agent-enable-cursor').checked = cursorOn;
@@ -2228,6 +2237,38 @@ function validateCli(tool) {
2228
2237
 
2229
2238
  // ---- Claude Code 引擎(Agent SDK)按需安装 ----
2230
2239
  var claudeEnginePollTimer = null;
2240
+ var claudeEngineState = null;
2241
+
2242
+ // Claude Code 开关:从关闭 → 打开时弹窗确认(SDK 必装才能使用)。
2243
+ // 页面初始化(按已有 config 设置开关)直接调 onAgentToggle,不经由此函数,不会误弹窗。
2244
+ function onClaudeToggle(el) {
2245
+ var enabled = el.checked;
2246
+ if (!enabled) {
2247
+ onAgentToggle('claude', false);
2248
+ return;
2249
+ }
2250
+ // 实时查询后端安装状态(不依赖可能过期的 claudeEngineState 缓存),
2251
+ // 已安装/正在安装时直接打开开关,不重复安装。
2252
+ api('/api/claude-sdk/status', 'GET').then(function(s){
2253
+ if (!s || !s.phase) { el.checked = false; return; }
2254
+ claudeEngineState = s;
2255
+ var sdkReady = s.installed === true || s.phase === 'done' ||
2256
+ s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting';
2257
+ if (sdkReady) {
2258
+ // 已安装 / 正在安装 / 正在检测:直接打开开关,不再触发安装
2259
+ onAgentToggle('claude', true);
2260
+ return;
2261
+ }
2262
+ if (!confirm('Claude Code 必须先安装 Claude Code SDK 才能使用(必装)。是否立即安装?')) {
2263
+ el.checked = false; // 用户取消 → 回滚开关为关闭
2264
+ return;
2265
+ }
2266
+ onAgentToggle('claude', true);
2267
+ installClaudeEngine();
2268
+ }).catch(function(){
2269
+ el.checked = false; // 状态查询失败 → 回滚开关,避免误判
2270
+ });
2271
+ }
2231
2272
 
2232
2273
  function claudeEngineEl(id) { return document.getElementById(id); }
2233
2274
 
@@ -2240,6 +2281,10 @@ function claudeEngineRenderStatus(s) {
2240
2281
  if (phase === 'done') color = '#16a34a';
2241
2282
  else if (phase === 'error') color = '#ef4444';
2242
2283
  else if (phase === 'downloading' || phase === 'installing') color = '#3b82f6';
2284
+ if (!text && phase === 'idle') {
2285
+ // 初始/空闲状态给出明确文案,而不是“未知状态”
2286
+ text = s.installed ? ('已安装 v' + (s.version || '未知版本')) : '未安装';
2287
+ }
2243
2288
  el.innerHTML = '<span style="color:' + color + '">' + (text ? text : '未知状态') + '</span>';
2244
2289
  if (s.error) el.innerHTML += '<br><span style="color:#ef4444;font-size:12px">' + s.error + '</span>';
2245
2290
  }
@@ -2259,18 +2304,19 @@ function claudeEngineRenderProgress(p) {
2259
2304
  function claudeEngineRefreshStatus() {
2260
2305
  api('/api/claude-sdk/status', 'GET').then(function(s){
2261
2306
  if (!s || !s.phase) return;
2307
+ claudeEngineState = s;
2262
2308
  claudeEngineRenderStatus(s);
2263
2309
  claudeEngineRenderProgress(s);
2264
2310
  var btn = claudeEngineEl('claude-engine-install-btn');
2265
2311
  if (btn) {
2266
2312
  if (s.phase === 'done' || s.installed) {
2267
- btn.textContent = '重新安装引擎';
2313
+ btn.textContent = '重新安装 Claude Code SDK';
2268
2314
  btn.disabled = false;
2269
2315
  } else if (s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting') {
2270
2316
  btn.textContent = '安装中…';
2271
2317
  btn.disabled = true;
2272
2318
  } else {
2273
- btn.textContent = '安装引擎(约 220MB)';
2319
+ btn.textContent = '安装 Claude Code SDK';
2274
2320
  btn.disabled = false;
2275
2321
  }
2276
2322
  }