claude-codex-wechat 0.1.36 → 0.1.38

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.
@@ -99,17 +99,22 @@ import { homedir } from "node:os";
99
99
  import { fileURLToPath } from "node:url";
100
100
  import { z as z2 } from "zod";
101
101
  var BRIDGE_API_URL2 = process.env.BRIDGE_API_URL || "http://localhost:8787";
102
- function findDouyinScript() {
103
- const here = dirname(fileURLToPath(import.meta.url));
104
- const candidates = [
105
- // Bundled with project (primary)
106
- join(here, "..", "scripts", "douyin-download.mjs"),
107
- // Built output
108
- join(here, "..", "..", "mcp", "scripts", "douyin-download.mjs"),
102
+ function douyinScriptCandidates(baseDir) {
103
+ return [
104
+ // Source layout: src/mcp/tools -> src/mcp/scripts
105
+ join(baseDir, "..", "scripts", "douyin-download.mjs"),
106
+ // Bundled layout: dist/mcp/mediaServer.js -> dist/mcp/scripts
107
+ join(baseDir, "scripts", "douyin-download.mjs"),
108
+ // Older/alternate dist layout fallback
109
+ join(baseDir, "..", "..", "mcp", "scripts", "douyin-download.mjs"),
109
110
  // User skills locations (cross-platform)
110
111
  join(homedir(), ".agents", "skills", "douyin-download", "scripts", "douyin-download.mjs"),
111
112
  join(homedir(), ".claude", "skills", "douyin-download", "scripts", "douyin-download.mjs")
112
113
  ];
114
+ }
115
+ function findDouyinScript() {
116
+ const here = dirname(fileURLToPath(import.meta.url));
117
+ const candidates = douyinScriptCandidates(here);
113
118
  for (const p of candidates) {
114
119
  const resolved = resolve(p);
115
120
  if (existsSync(resolved)) return resolved;
@@ -2977,17 +2977,52 @@ function runHelpProbe(command) {
2977
2977
  }
2978
2978
 
2979
2979
  // src/providers/claude-code/claudeStreamingRunner.ts
2980
+ var IDLE_SENTINEL = /* @__PURE__ */ Symbol("idle_timeout");
2981
+ function capTail(text, capBytes) {
2982
+ if (capBytes <= 0 || text.length <= capBytes) return text;
2983
+ return text.slice(text.length - capBytes);
2984
+ }
2985
+ function readWithIdleTimeout(handle, ms) {
2986
+ if (!ms || ms <= 0) return handle.read();
2987
+ return new Promise((resolve2) => {
2988
+ let settled = false;
2989
+ const timer = setTimeout(() => {
2990
+ if (!settled) {
2991
+ settled = true;
2992
+ resolve2(IDLE_SENTINEL);
2993
+ }
2994
+ }, ms);
2995
+ timer.unref?.();
2996
+ void handle.read().then((chunk) => {
2997
+ if (!settled) {
2998
+ settled = true;
2999
+ clearTimeout(timer);
3000
+ resolve2(chunk);
3001
+ }
3002
+ });
3003
+ });
3004
+ }
2980
3005
  var ClaudeStreamingRunner = class {
2981
3006
  sessions = /* @__PURE__ */ new Map();
2982
3007
  command;
2983
3008
  spawner;
2984
3009
  capabilityProbe;
2985
3010
  mcpConfigPath;
3011
+ idleTimeoutMs;
3012
+ maxProcessAgeMs;
3013
+ maxTurns;
3014
+ stderrCapBytes;
3015
+ maxLineBytes;
2986
3016
  constructor(input = {}) {
2987
3017
  this.command = input.command ?? "claude";
2988
- this.spawner = input.spawner ?? defaultClaudeStreamSpawner;
2989
3018
  this.capabilityProbe = input.capabilityProbe ?? probeAppendSystemPromptSupport;
2990
3019
  this.mcpConfigPath = input.mcpConfigPath;
3020
+ this.idleTimeoutMs = input.idleTimeoutMs ?? 18e4;
3021
+ this.maxProcessAgeMs = input.maxProcessAgeMs ?? 2 * 60 * 60 * 1e3;
3022
+ this.maxTurns = input.maxTurns ?? 50;
3023
+ this.stderrCapBytes = input.stderrCapBytes ?? 64 * 1024;
3024
+ this.maxLineBytes = input.maxLineBytes ?? 10 * 1024 * 1024;
3025
+ this.spawner = input.spawner ?? ((call) => defaultClaudeStreamSpawner(call, { stderrCapBytes: this.stderrCapBytes, maxLineBytes: this.maxLineBytes }));
2991
3026
  }
2992
3027
  async startSession(input) {
2993
3028
  const resumeId = input.options?.providerSessionId;
@@ -3000,7 +3035,8 @@ var ClaudeStreamingRunner = class {
3000
3035
  cwd: input.cwd,
3001
3036
  status: "idle",
3002
3037
  resumeId,
3003
- pendingFollowUps: 0
3038
+ pendingFollowUps: 0,
3039
+ turnCount: 0
3004
3040
  };
3005
3041
  this.sessions.set(input.bridgeSessionId, session);
3006
3042
  return session;
@@ -3015,7 +3051,13 @@ var ClaudeStreamingRunner = class {
3015
3051
  session.pendingFollowUps = 0;
3016
3052
  handle.write(userEnvelope(input.text));
3017
3053
  while (true) {
3018
- const chunk = await handle.read();
3054
+ const chunk = await readWithIdleTimeout(handle, this.idleTimeoutMs);
3055
+ if (chunk === IDLE_SENTINEL) {
3056
+ handle.close();
3057
+ session.handle = void 0;
3058
+ yield { type: "error", error: "idle_timeout", code: "idle_timeout" };
3059
+ return;
3060
+ }
3019
3061
  if (chunk.type === "exit") {
3020
3062
  session.handle = void 0;
3021
3063
  yield { type: "error", error: chunk.stderr || `claude stream exited with ${chunk.code}` };
@@ -3064,6 +3106,7 @@ var ClaudeStreamingRunner = class {
3064
3106
  session.claudeSessionId = sessionId;
3065
3107
  session.resumeId = sessionId;
3066
3108
  }
3109
+ session.turnCount += 1;
3067
3110
  yield { type: "message_done" };
3068
3111
  if (session.pendingFollowUps > 0) {
3069
3112
  session.pendingFollowUps -= 1;
@@ -3096,11 +3139,22 @@ var ClaudeStreamingRunner = class {
3096
3139
  this.sessions.delete(bridgeSessionId);
3097
3140
  }
3098
3141
  ensureHandle(session) {
3099
- if (session.handle) return session.handle;
3142
+ if (session.handle && !this.shouldRetire(session)) return session.handle;
3143
+ if (session.handle) {
3144
+ session.handle.close();
3145
+ session.handle = void 0;
3146
+ }
3100
3147
  const args = buildStreamingArgs(session, this.mcpConfigPath);
3101
3148
  session.handle = this.spawner({ command: this.command, args, cwd: session.cwd });
3149
+ session.spawnedAt = Date.now();
3150
+ session.turnCount = 0;
3102
3151
  return session.handle;
3103
3152
  }
3153
+ shouldRetire(session) {
3154
+ if (this.maxTurns > 0 && session.turnCount >= this.maxTurns) return true;
3155
+ if (this.maxProcessAgeMs > 0 && session.spawnedAt && Date.now() - session.spawnedAt >= this.maxProcessAgeMs) return true;
3156
+ return false;
3157
+ }
3104
3158
  };
3105
3159
  function buildStreamingArgs(session, mcpConfigPath) {
3106
3160
  const args = [
@@ -3147,7 +3201,9 @@ function parseJsonLine(line) {
3147
3201
  return null;
3148
3202
  }
3149
3203
  }
3150
- function defaultClaudeStreamSpawner(call) {
3204
+ function defaultClaudeStreamSpawner(call, opts = {}) {
3205
+ const stderrCapBytes = opts.stderrCapBytes ?? 64 * 1024;
3206
+ const maxLineBytes = opts.maxLineBytes ?? 10 * 1024 * 1024;
3151
3207
  const child = spawn3(call.command, call.args, { cwd: expandTilde(call.cwd) ?? call.cwd, stdio: ["pipe", "pipe", "pipe"], shell: useShellForCli() });
3152
3208
  let stderr = "";
3153
3209
  let buffer = "";
@@ -3169,9 +3225,15 @@ function defaultClaudeStreamSpawner(call) {
3169
3225
  for (const line of parts) {
3170
3226
  if (line.trim()) push({ type: "line", line });
3171
3227
  }
3228
+ if (buffer.length > maxLineBytes) {
3229
+ stderr = capTail(stderr + `
3230
+ [bridge] dropped oversized line buffer (${buffer.length} bytes)
3231
+ `, stderrCapBytes);
3232
+ buffer = "";
3233
+ }
3172
3234
  });
3173
3235
  child.stderr.on("data", (data) => {
3174
- stderr += String(data);
3236
+ stderr = capTail(stderr + String(data), stderrCapBytes);
3175
3237
  });
3176
3238
  child.on("error", (error) => {
3177
3239
  push({ type: "exit", code: error.code ?? "ERROR", stderr: stderr || error.message });
@@ -3657,9 +3719,22 @@ var CodexProvider = class {
3657
3719
  };
3658
3720
 
3659
3721
  // src/providers/defaultProviders.ts
3722
+ function readEnvInt(raw, fallback) {
3723
+ if (!raw) return fallback;
3724
+ const n = Number.parseInt(raw, 10);
3725
+ if (!Number.isFinite(n) || n <= 0) return fallback;
3726
+ return n;
3727
+ }
3660
3728
  function createDefaultProviders(input = {}) {
3729
+ const claudeRunner = new ClaudeStreamingRunner({
3730
+ command: input.claudeCommand,
3731
+ mcpConfigPath: input.mcpConfigPath,
3732
+ idleTimeoutMs: readEnvInt(process.env.BRIDGE_CLAUDE_IDLE_TIMEOUT_MS, 18e4),
3733
+ maxProcessAgeMs: readEnvInt(process.env.BRIDGE_CLAUDE_MAX_PROCESS_AGE_MS, 2 * 60 * 60 * 1e3),
3734
+ maxTurns: readEnvInt(process.env.BRIDGE_CLAUDE_MAX_TURNS, 50)
3735
+ });
3661
3736
  return [
3662
- new ClaudeCodeProvider({ runner: new ClaudeStreamingRunner({ command: input.claudeCommand, mcpConfigPath: input.mcpConfigPath }) }),
3737
+ new ClaudeCodeProvider({ runner: claudeRunner }),
3663
3738
  new CodexProvider({ runner: new CodexInteractiveRunner({ command: input.codexCommand }) })
3664
3739
  ];
3665
3740
  }
@@ -4108,6 +4183,13 @@ var MessageRouter = class _MessageRouter {
4108
4183
  active.abort();
4109
4184
  void Promise.resolve(this.providers.get(active.providerId)?.interruptSession?.(active.bridgeSessionId)).catch(() => void 0);
4110
4185
  }
4186
+ // 切换/清除当前会话前,回收旧 provider 的持久子进程,防止孤儿堆积。
4187
+ // stopSession 内部是同步 close/kill,不会挂起;fire-and-forget,不阻塞命令链。
4188
+ recycleProviderProcess(session) {
4189
+ if (!session) return;
4190
+ const provider = this.providers.get(session.providerId);
4191
+ void Promise.resolve(provider?.stopSession(session.id)).catch(() => void 0);
4192
+ }
4111
4193
  async runChatGeneration(message, user, text, seq) {
4112
4194
  const superseded = () => (this.latestMutatingSeq.get(message.chatId) ?? 0) > seq;
4113
4195
  if (superseded()) return;
@@ -4219,7 +4301,7 @@ var MessageRouter = class _MessageRouter {
4219
4301
  await this.sendToChat({ chatId: message.chatId, kind: "text", text: bufferedText });
4220
4302
  bufferedText = "";
4221
4303
  }
4222
- const errorText = `Provider error: ${event.error}`;
4304
+ const errorText = event.code === "idle_timeout" ? "\u26A0\uFE0F \u4E0A\u4E00\u8F6E\u957F\u65F6\u95F4\u65E0\u54CD\u5E94(\u7591\u4F3C\u5DE5\u5177\u5361\u6B7B),\u5DF2\u81EA\u52A8\u7EC8\u6B62\u5E76\u4FDD\u7559\u4F1A\u8BDD\u3002\u76F4\u63A5\u91CD\u53D1\u6D88\u606F\u5373\u53EF\u7EE7\u7EED\u3002" : `Provider error: ${event.error}`;
4223
4305
  await this.sendToChat({
4224
4306
  chatId: message.chatId,
4225
4307
  kind: "status",
@@ -4272,9 +4354,11 @@ var MessageRouter = class _MessageRouter {
4272
4354
  }
4273
4355
  if (command.kind === "new_session") {
4274
4356
  this.pendingChoices.delete(chatId);
4357
+ const previousSession = this.conversation.getCurrent();
4275
4358
  await this.preemptActiveGeneration(chatId);
4276
- const providerId = command.providerId ?? this.conversation.getCurrent()?.providerId ?? this.options.defaults?.defaultProvider ?? "claude-code";
4277
- const cwd = command.cwd ?? this.conversation.getCurrent()?.cwd ?? this.options.defaults?.defaultWorkspace ?? defaultWorkspaceDir();
4359
+ this.recycleProviderProcess(previousSession);
4360
+ const providerId = command.providerId ?? previousSession?.providerId ?? this.options.defaults?.defaultProvider ?? "claude-code";
4361
+ const cwd = command.cwd ?? previousSession?.cwd ?? this.options.defaults?.defaultWorkspace ?? defaultWorkspaceDir();
4278
4362
  const session = this.conversation.create({
4279
4363
  chatId,
4280
4364
  ownerUserId: user.id,
@@ -4346,8 +4430,9 @@ var MessageRouter = class _MessageRouter {
4346
4430
  await this.sendToChat({ chatId, kind: "status", text: "\u7528\u6CD5\uFF1A/resume <\u7F16\u53F7>" });
4347
4431
  return;
4348
4432
  }
4433
+ const previousSession = this.conversation.getCurrent();
4349
4434
  await this.preemptActiveGeneration(chatId);
4350
- let providerId = this.conversation.getCurrent()?.providerId ?? this.options.defaults?.defaultProvider ?? "claude-code";
4435
+ let providerId = previousSession?.providerId ?? this.options.defaults?.defaultProvider ?? "claude-code";
4351
4436
  let resolvedId = ref;
4352
4437
  if (/^\d+$/.test(ref)) {
4353
4438
  const cached = this.sessionListCache.get(chatId);
@@ -4363,7 +4448,7 @@ var MessageRouter = class _MessageRouter {
4363
4448
  resolvedId = target;
4364
4449
  providerId = cached.providerId;
4365
4450
  }
4366
- const previousCwd = this.conversation.getCurrent()?.cwd;
4451
+ const previousCwd = previousSession?.cwd;
4367
4452
  const provider = this.providers.get(providerId);
4368
4453
  if (!provider?.attachSession || !provider.listRecoverableSessions) {
4369
4454
  await this.sendToChat({ chatId, kind: "status", text: `\u5F53\u524D provider\uFF08${providerId}\uFF09\u4E0D\u652F\u6301\u4F1A\u8BDD\u6062\u590D` });
@@ -4374,6 +4459,7 @@ var MessageRouter = class _MessageRouter {
4374
4459
  await this.sendToChat({ chatId, kind: "status", text: `\u627E\u4E0D\u5230\u4F1A\u8BDD ${ref}` });
4375
4460
  return;
4376
4461
  }
4462
+ this.recycleProviderProcess(previousSession);
4377
4463
  const cwdUnresolved = !candidate.cwd;
4378
4464
  const attached = await attachProviderSessionToBridge({
4379
4465
  conversationStore: this.conversation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-wechat",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.5.0",