claude-codex-wechat 0.1.58 → 0.1.60

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.
@@ -158,7 +158,7 @@ function findDouyinScript() {
158
158
  function runScript(args) {
159
159
  const script = findDouyinScript();
160
160
  return new Promise((resolve2, reject) => {
161
- execFile(nodeExecutable(), [script, ...args], { timeout: 12e4 }, (err, stdout, stderr) => {
161
+ execFile(nodeExecutable(), [script, ...args], { timeout: 18e4 }, (err, stdout, stderr) => {
162
162
  if (err) reject(new Error(stderr || err.message));
163
163
  else resolve2({ stdout, stderr });
164
164
  });
@@ -1,18 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * 抖音无水印视频下载脚本(自包含,无外部依赖)。
4
+ * 抖音无水印视频下载脚本(自包含,无下载器依赖)。
5
5
  *
6
6
  * 用法:
7
7
  * node douyin-download.mjs "<抖音链接>"
8
8
  * node douyin-download.mjs "<抖音链接>" --output ./videos/
9
9
  * node douyin-download.mjs "<分享文案含链接>"
10
10
  *
11
- * 要求:Node.js 18+(需要全局 fetch)
11
+ * 要求:Node.js 20+。新版抖音页面回退路径需要本机安装 Chrome/Chromium。
12
12
  */
13
13
 
14
- import { writeFileSync, mkdirSync, existsSync } from "node:fs";
14
+ import { execFile } from "node:child_process";
15
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
15
17
  import { resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { promisify } from "node:util";
20
+
21
+ const execFileAsync = promisify(execFile);
16
22
 
17
23
  // ─── 常量 ───────────────────────────────────────────────────────────────────
18
24
 
@@ -20,34 +26,38 @@ const MOBILE_UA =
20
26
  "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1";
21
27
  const DESKTOP_UA =
22
28
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
29
+ const PLAYER_LOAD_TIMEOUT_MS = 35_000;
30
+ const PLAYER_VIRTUAL_TIME_MS = 15_000;
31
+ const PLAYER_ATTEMPTS = 2;
32
+ const MAX_DOM_BYTES = 2 * 1024 * 1024;
33
+ const ALLOWED_VIDEO_HOST_SUFFIXES = [".douyinvod.com", ".douyin.com", ".snssdk.com"];
23
34
 
24
35
  // ─── 带重试/超时的 fetch ─────────────────────────────────────────────────────
25
36
 
26
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
37
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
27
38
 
28
39
  /**
29
40
  * fetch 包一层重试与超时,抵御瞬时网络抖动("fetch failed" 之类的底层网络错误)。
30
41
  * 仅对网络层错误和 5xx/429 重试;4xx(除 429)视为确定性失败,不重试。
31
42
  */
32
43
  async function fetchWithRetry(url, options = {}, { retries = 3, timeoutMs = 20_000 } = {}) {
33
- let lastErr;
44
+ let lastError;
34
45
  for (let attempt = 1; attempt <= retries; attempt += 1) {
35
46
  const controller = new AbortController();
36
47
  const timer = setTimeout(() => controller.abort(), timeoutMs);
37
48
  try {
38
- const resp = await fetch(url, { ...options, signal: controller.signal });
49
+ const response = await fetch(url, { ...options, signal: controller.signal });
39
50
  clearTimeout(timer);
40
- // 5xx / 429 属于可恢复的服务端抖动,重试;其余(含 4xx)直接返回交由调用方判断
41
- if ((resp.status >= 500 || resp.status === 429) && attempt < retries) {
42
- lastErr = new Error(`HTTP ${resp.status}`);
51
+ if ((response.status >= 500 || response.status === 429) && attempt < retries) {
52
+ lastError = new Error(`HTTP ${response.status}`);
43
53
  await sleep(attempt * 800);
44
54
  continue;
45
55
  }
46
- return resp;
47
- } catch (err) {
56
+ return response;
57
+ } catch (error) {
48
58
  clearTimeout(timer);
49
- lastErr = err;
50
- const reason = err?.name === "AbortError" ? `请求超时(>${timeoutMs}ms)` : (err?.message || String(err));
59
+ lastError = error;
60
+ const reason = error?.name === "AbortError" ? `请求超时(>${timeoutMs}ms)` : error?.message || String(error);
51
61
  if (attempt < retries) {
52
62
  await sleep(attempt * 800);
53
63
  continue;
@@ -55,26 +65,25 @@ async function fetchWithRetry(url, options = {}, { retries = 3, timeoutMs = 20_0
55
65
  throw new Error(`网络请求失败(已重试${retries}次):${reason}`);
56
66
  }
57
67
  }
58
- throw lastErr ?? new Error("网络请求失败");
68
+ throw lastError ?? new Error("网络请求失败");
59
69
  }
60
70
 
61
71
  // ─── 解析逻辑 ────────────────────────────────────────────────────────────────
62
72
 
63
- /** 从任意文本里提取第一个抖音 URL */
64
- function extractDouyinUrl(text) {
65
- const m = text.match(
66
- /https?:\/\/(?:v\.douyin\.com|www\.douyin\.com|www\.iesdouyin\.com|iesdouyin\.com)\/[^\s"'))】]+/i
73
+ /** 从任意文本里提取第一个抖音 URL。 */
74
+ export function extractDouyinUrl(text) {
75
+ const matched = text.match(
76
+ /https?:\/\/(?:v\.douyin\.com|www\.douyin\.com|www\.iesdouyin\.com|iesdouyin\.com)\/[^\s"'))】]+/i,
67
77
  );
68
- return m ? m[0] : null;
78
+ return matched ? matched[0] : null;
69
79
  }
70
80
 
71
- /** 把任意输入解析成 aweme_id */
72
- async function resolveAwemeId(input) {
81
+ /** 把任意输入解析成 aweme_id。 */
82
+ export async function resolveAwemeId(input) {
73
83
  const trimmed = input.trim();
74
84
  if (/^\d{15,}$/.test(trimmed)) return trimmed;
75
85
 
76
86
  const url = extractDouyinUrl(trimmed) ?? trimmed;
77
-
78
87
  const direct = url.match(/(?:share\/)?video\/(\d{15,})/);
79
88
  if (direct) return direct[1];
80
89
 
@@ -82,96 +91,194 @@ async function resolveAwemeId(input) {
82
91
  throw new Error(`无法识别的输入(既不是抖音链接,也不是 aweme_id):${trimmed}`);
83
92
  }
84
93
 
85
- const resp = await fetchWithRetry(url, {
94
+ const response = await fetchWithRetry(url, {
86
95
  method: "GET",
87
96
  headers: { "User-Agent": MOBILE_UA },
88
97
  redirect: "follow",
89
98
  });
90
- const m = resp.url.match(/(\d{15,})/);
91
- if (!m) throw new Error(`短链重定向后未找到 aweme_id,最终地址:${resp.url}`);
92
- return m[1];
99
+ const matched = response.url.match(/(\d{15,})/);
100
+ if (!matched) throw new Error(`短链重定向后未找到 aweme_id,最终地址:${response.url}`);
101
+ return matched[1];
93
102
  }
94
103
 
95
- /** 从抖音分享页提取 video_id */
96
- async function resolveVideoId(awemeId) {
97
- const shareUrl = `https://www.iesdouyin.com/share/video/${awemeId}/`;
98
- const resp = await fetchWithRetry(shareUrl, { headers: { "User-Agent": MOBILE_UA } });
99
- if (!resp.ok) throw new Error(`分享页请求失败:HTTP ${resp.status}(aweme_id=${awemeId})`);
100
- const html = await resp.text();
101
- const m =
102
- html.match(/video_id=([a-z0-9]+)/i) ??
103
- html.match(/"video_id"\s*:\s*"([a-z0-9]+)"/i) ??
104
- html.match(/playwm\/\?[^"'\\]*?video_id=([a-z0-9]+)/i);
105
- if (!m) throw new Error(`分享页里未找到 video_id(aweme_id=${awemeId})`);
106
- return m[1];
107
- }
108
-
109
- /** 拼无水印播放入口(play 而非 playwm) */
104
+ /** 拼无水印播放入口(play 而非 playwm)。 */
110
105
  function buildPlayUrlNoWatermark(videoId, ratio = "720p") {
111
106
  return `https://aweme.snssdk.com/aweme/v1/play/?ratio=${ratio}&video_id=${videoId}`;
112
107
  }
113
108
 
109
+ /** 查找用户机器上已有的 Chrome/Chromium,不下载浏览器。 */
110
+ export function findChromeExecutable({ platform = process.platform, env = process.env, exists = existsSync } = {}) {
111
+ const explicit = [env.DOUYIN_CHROME_EXECUTABLE, env.CHROME_PATH, env.GOOGLE_CHROME_BIN].filter(Boolean);
112
+ const home = env.HOME || env.USERPROFILE || homedir();
113
+ const candidates = [...explicit];
114
+
115
+ if (platform === "darwin") {
116
+ candidates.push(
117
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
118
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
119
+ resolve(home, "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
120
+ );
121
+ } else if (platform === "win32") {
122
+ for (const root of [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter(Boolean)) {
123
+ candidates.push(resolve(root, "Google/Chrome/Application/chrome.exe"), resolve(root, "Chromium/Application/chrome.exe"));
124
+ }
125
+ } else {
126
+ candidates.push(
127
+ "/usr/bin/google-chrome-stable",
128
+ "/usr/bin/google-chrome",
129
+ "/usr/bin/chromium",
130
+ "/usr/bin/chromium-browser",
131
+ "/snap/bin/chromium",
132
+ );
133
+ }
134
+
135
+ return candidates.find((candidate) => exists(candidate)) ?? "";
136
+ }
137
+
138
+ function decodeHtmlAttribute(value) {
139
+ return value
140
+ .replaceAll("&amp;", "&")
141
+ .replaceAll("&quot;", '"')
142
+ .replaceAll("&#39;", "'")
143
+ .replaceAll("&lt;", "<")
144
+ .replaceAll("&gt;", ">");
145
+ }
146
+
147
+ /** 从官方播放器渲染后的 DOM 中提取并校验视频 CDN 地址。 */
148
+ export function parseOfficialVideoUrl(dom) {
149
+ const videoTag = dom.match(/<video\b[^>]*>/i)?.[0];
150
+ const rawSource = videoTag?.match(/\bsrc=["']([^"']+)["']/i)?.[1];
151
+ if (!rawSource) throw new Error("抖音官方播放器未返回视频地址,作品可能已删除、设为私密或触发风控");
152
+
153
+ const normalized = decodeHtmlAttribute(rawSource);
154
+ let url;
155
+ try {
156
+ url = new URL(normalized.startsWith("//") ? `https:${normalized}` : normalized);
157
+ } catch {
158
+ throw new Error("抖音官方播放器返回了无效的视频地址");
159
+ }
160
+
161
+ const allowed =
162
+ url.protocol === "https:" && ALLOWED_VIDEO_HOST_SUFFIXES.some((suffix) => url.hostname.endsWith(suffix));
163
+ if (!allowed) throw new Error(`抖音官方播放器返回了非预期的视频域名:${url.hostname}`);
164
+ return url.toString();
165
+ }
166
+
167
+ async function renderOfficialPlayerDom(chrome, playerUrl) {
168
+ const { stdout } = await execFileAsync(
169
+ chrome,
170
+ [
171
+ "--headless=new",
172
+ "--no-sandbox",
173
+ "--disable-gpu",
174
+ `--virtual-time-budget=${PLAYER_VIRTUAL_TIME_MS}`,
175
+ "--dump-dom",
176
+ playerUrl,
177
+ ],
178
+ { timeout: PLAYER_LOAD_TIMEOUT_MS, maxBuffer: MAX_DOM_BYTES },
179
+ );
180
+ return stdout;
181
+ }
182
+
183
+ /** 由抖音官方播放器完成访客 Cookie 与动态签名,再读取真实 CDN 地址。 */
184
+ export async function resolveOfficialDouyinVideoUrl(
185
+ awemeId,
186
+ { chrome = findChromeExecutable(), renderDom = renderOfficialPlayerDom } = {},
187
+ ) {
188
+ if (!/^\d{15,}$/.test(awemeId)) throw new Error(`无效的抖音 aweme_id:${awemeId}`);
189
+ if (!chrome) {
190
+ throw new Error(
191
+ "旧分享页已不再提供 video_id,且未找到 Chrome/Chromium。请安装 Chrome,或设置 DOUYIN_CHROME_EXECUTABLE 指向浏览器可执行文件。",
192
+ );
193
+ }
194
+
195
+ const playerUrl = `https://open.douyin.com/player/video?vid=${awemeId}&autoplay=0`;
196
+ let lastError;
197
+ for (let attempt = 1; attempt <= PLAYER_ATTEMPTS; attempt += 1) {
198
+ try {
199
+ return parseOfficialVideoUrl(await renderDom(chrome, playerUrl));
200
+ } catch (error) {
201
+ lastError = error;
202
+ }
203
+ }
204
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
205
+ }
206
+
114
207
  // ─── 下载逻辑 ────────────────────────────────────────────────────────────────
115
208
 
116
- async function download(input, outputDir) {
209
+ async function fetchVideoSource(url, referer) {
210
+ return fetchWithRetry(
211
+ url,
212
+ {
213
+ headers: { "User-Agent": DESKTOP_UA, Referer: referer },
214
+ redirect: "follow",
215
+ },
216
+ { timeoutMs: 60_000 },
217
+ );
218
+ }
219
+
220
+ export async function download(input, outputDir) {
117
221
  console.log("正在解析...");
118
222
 
119
223
  const trimmed = input.trim();
120
224
  let awemeId = "";
121
225
  let videoId = "";
226
+ let playUrl;
227
+ let referer = "https://open.douyin.com/";
122
228
 
123
229
  if (/^v0[a-z0-9]{10,}$/i.test(trimmed)) {
124
230
  videoId = trimmed;
231
+ playUrl = buildPlayUrlNoWatermark(videoId);
232
+ referer = "https://www.douyin.com/";
125
233
  } else {
126
234
  awemeId = await resolveAwemeId(trimmed);
127
- videoId = await resolveVideoId(awemeId);
235
+ playUrl = await resolveOfficialDouyinVideoUrl(awemeId);
128
236
  }
129
237
 
130
- console.log(`aweme_id: ${awemeId || "-"} video_id: ${videoId}`);
131
-
132
- const playUrl = buildPlayUrlNoWatermark(videoId);
238
+ console.log(`aweme_id: ${awemeId || "-"} video_id: ${videoId || "-"}`);
133
239
  console.log("正在下载无水印视频...");
134
240
 
135
- const resp = await fetchWithRetry(playUrl, {
136
- headers: { "User-Agent": DESKTOP_UA, Referer: "https://www.douyin.com/" },
137
- redirect: "follow",
138
- }, { timeoutMs: 60_000 });
139
-
140
- if (!resp.ok) {
141
- throw new Error(`下载失败: HTTP ${resp.status}`);
142
- }
241
+ const response = await fetchVideoSource(playUrl, referer);
242
+ if (!response.ok) throw new Error(`下载失败: HTTP ${response.status}`);
143
243
 
144
- const buf = Buffer.from(await resp.arrayBuffer());
244
+ const buffer = Buffer.from(await response.arrayBuffer());
145
245
  const filename = `douyin_${awemeId || videoId}.mp4`;
146
-
147
246
  const dir = outputDir ? resolve(outputDir) : process.cwd();
148
247
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
149
248
 
150
249
  const filepath = resolve(dir, filename);
151
- writeFileSync(filepath, buf);
250
+ writeFileSync(filepath, buffer);
152
251
 
153
- const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
252
+ const sizeMB = (buffer.length / 1024 / 1024).toFixed(2);
154
253
  console.log(`✅ 已保存: ${filepath} (${sizeMB} MB)`);
155
254
  return filepath;
156
255
  }
157
256
 
158
- // ─── CLI 入口 ─────────────────────────────────────────────────────────────────
257
+ // ─── CLI 入口 ────────────────────────────────────────────────────────────────
159
258
 
160
- const argv = process.argv.slice(2);
161
- const outputIdx = argv.indexOf("--output");
162
- let outputDir;
163
- if (outputIdx !== -1) {
164
- outputDir = argv[outputIdx + 1];
165
- argv.splice(outputIdx, 2);
166
- }
259
+ async function main() {
260
+ const argv = process.argv.slice(2);
261
+ const outputIndex = argv.indexOf("--output");
262
+ let outputDir;
263
+ if (outputIndex !== -1) {
264
+ outputDir = argv[outputIndex + 1];
265
+ argv.splice(outputIndex, 2);
266
+ }
267
+
268
+ const input = argv.join(" ").trim();
269
+ if (!input) {
270
+ console.error('用法: node douyin-download.mjs "<抖音链接>" [--output <目录>]');
271
+ process.exitCode = 2;
272
+ return;
273
+ }
167
274
 
168
- const input = argv.join(" ").trim();
169
- if (!input) {
170
- console.error('用法: node douyin-download.mjs "<抖音链接>" [--output <目录>]');
171
- process.exit(2);
275
+ try {
276
+ await download(input, outputDir);
277
+ } catch (error) {
278
+ console.error("❌ 错误:", error.message || error);
279
+ process.exitCode = 1;
280
+ }
172
281
  }
173
282
 
174
- download(input, outputDir).catch((error) => {
175
- console.error("❌ 错误:", error.message || error);
176
- process.exit(1);
177
- });
283
+ const entryPath = process.argv[1] ? resolve(process.argv[1]) : "";
284
+ if (entryPath === fileURLToPath(import.meta.url)) await main();
@@ -757,10 +757,23 @@ async function hasClaudeHistoryDisplay(input) {
757
757
  async function getClaudeNativeVersion(input) {
758
758
  const sessionPath = await findRecoverableClaudeSessionPath(input.sessionId, input.env ?? process.env);
759
759
  if (!sessionPath) return null;
760
- const metadata = await stat2(sessionPath).catch(() => null);
761
- if (!metadata?.isFile()) return null;
760
+ const content = await readFile2(sessionPath, "utf8").catch(() => null);
761
+ if (content === null) return null;
762
+ let count = 0;
763
+ let lastUuid = "";
764
+ for (const line of content.split(/\r?\n/)) {
765
+ if (!line.trim()) continue;
766
+ try {
767
+ const record = JSON.parse(line);
768
+ if ((record.type === "user" || record.type === "assistant") && typeof record.uuid === "string") {
769
+ count += 1;
770
+ lastUuid = record.uuid;
771
+ }
772
+ } catch {
773
+ }
774
+ }
762
775
  return {
763
- fingerprint: `claude:${input.sessionId}:${Math.trunc(metadata.mtimeMs)}:${metadata.size}`,
776
+ fingerprint: `claude:${input.sessionId}:conv:${count}:${lastUuid}`,
764
777
  observedAt: Date.now()
765
778
  };
766
779
  }
@@ -1017,10 +1030,23 @@ async function findRecoverableCodexSessionPath(sessionId, env = process.env) {
1017
1030
  async function getCodexNativeVersion(input) {
1018
1031
  const sessionPath = await findRecoverableCodexSessionPath(input.sessionId, input.env ?? process.env);
1019
1032
  if (!sessionPath) return null;
1020
- const metadata = await stat3(sessionPath).catch(() => null);
1021
- if (!metadata?.isFile()) return null;
1033
+ const content = await readFile3(sessionPath, "utf8").catch(() => null);
1034
+ if (content === null) return null;
1035
+ let count = 0;
1036
+ let lastTimestamp = "";
1037
+ for (const line of content.split(/\r?\n/)) {
1038
+ if (!line.trim()) continue;
1039
+ try {
1040
+ const record = JSON.parse(line);
1041
+ if (record.type === "response_item") {
1042
+ count += 1;
1043
+ lastTimestamp = typeof record.timestamp === "string" ? record.timestamp : "";
1044
+ }
1045
+ } catch {
1046
+ }
1047
+ }
1022
1048
  return {
1023
- fingerprint: `codex:${input.sessionId}:${Math.trunc(metadata.mtimeMs)}:${metadata.size}`,
1049
+ fingerprint: `codex:${input.sessionId}:conv:${count}:${lastTimestamp}`,
1024
1050
  observedAt: Date.now()
1025
1051
  };
1026
1052
  }
@@ -4617,11 +4643,10 @@ var MessageRouter = class _MessageRouter {
4617
4643
  if ((this.latestMutatingSeq.get(message.chatId) ?? 0) > seq) {
4618
4644
  return { status: "accepted" };
4619
4645
  }
4620
- const chatText = this.resolveChoiceReply(message.chatId, command.text);
4621
- if (await this.maybeSteer(message.chatId, chatText)) {
4646
+ if (await this.maybeSteer(message.chatId, command.text)) {
4622
4647
  return { status: "accepted" };
4623
4648
  }
4624
- await this.runExclusive(() => this.runChatGeneration(message, user, chatText, seq));
4649
+ await this.runExclusive(() => this.runChatGeneration(message, user, command.text, seq));
4625
4650
  return { status: "accepted" };
4626
4651
  }
4627
4652
  // 消费一次上一轮暂存的 AskUserQuestion 选项:纯序号回复翻译成对应选项文字,
@@ -4643,7 +4668,7 @@ var MessageRouter = class _MessageRouter {
4643
4668
  if (!active) return Promise.resolve(false);
4644
4669
  const provider = this.providers.get(active.providerId);
4645
4670
  if (!provider?.steerSession) return Promise.resolve(false);
4646
- return provider.steerSession(active.bridgeSessionId, text).then(() => true);
4671
+ return provider.steerSession(active.bridgeSessionId, this.resolveChoiceReply(chatId, text)).then(() => true);
4647
4672
  }
4648
4673
  // Serialize session-mutating work onto one global chain. The chain is updated
4649
4674
  // synchronously (no await before the assignment) so two non-blocking channel
@@ -4687,6 +4712,17 @@ var MessageRouter = class _MessageRouter {
4687
4712
  }).catch(() => null);
4688
4713
  if (!version || !session.lastSeenNativeFingerprint) return { ok: true };
4689
4714
  if (version.fingerprint === session.lastSeenNativeFingerprint) return { ok: true };
4715
+ if (LEGACY_NATIVE_FINGERPRINT.test(session.lastSeenNativeFingerprint)) {
4716
+ this.conversation.update({ lastSeenNativeFingerprint: version.fingerprint }, session.id);
4717
+ return { ok: true };
4718
+ }
4719
+ if (provider.attachSession) {
4720
+ try {
4721
+ await this.reattachSession(session, provider);
4722
+ return { ok: true };
4723
+ } catch {
4724
+ }
4725
+ }
4690
4726
  this.conversation.update({
4691
4727
  staleReason: "native_advanced_elsewhere",
4692
4728
  lastActivityAt: Date.now()
@@ -4696,6 +4732,27 @@ var MessageRouter = class _MessageRouter {
4696
4732
  message: "\u8BE5\u4F1A\u8BDD\u5DF2\u5728\u53E6\u4E00\u7AEF\u7EE7\u7EED\uFF0C\u5F53\u524D\u6865\u63A5\u72B6\u6001\u5DF2\u8FC7\u671F\u3002\u8BF7\u5148 /reload \u540E\u518D\u7EE7\u7EED\u3002"
4697
4733
  };
4698
4734
  }
4735
+ // 回收旧子进程并以 resume 方式重新挂载同一原生会话,使其加载另一端追加的最新历史。
4736
+ async reattachSession(current, provider) {
4737
+ if (!current.providerSessionId || !provider.attachSession) return;
4738
+ for (const [chatId, pending] of this.pendingChoices) {
4739
+ if (pending.sessionId === current.id) this.pendingChoices.delete(chatId);
4740
+ }
4741
+ this.recycleProviderProcess(current);
4742
+ const reattached = await provider.attachSession({
4743
+ candidateId: current.providerSessionId,
4744
+ bridgeSessionId: current.id,
4745
+ cwd: current.cwd
4746
+ });
4747
+ this.conversation.update({
4748
+ providerSessionId: reattached.providerSessionId ?? current.providerSessionId,
4749
+ status: reattached.status,
4750
+ lastSeenNativeFingerprint: void 0,
4751
+ lastReloadedAt: Date.now(),
4752
+ staleReason: void 0,
4753
+ lastActivityAt: Date.now()
4754
+ }, current.id);
4755
+ }
4699
4756
  async refreshNativeFingerprint(session, provider) {
4700
4757
  if (!session.providerSessionId || !provider.getNativeVersion) return;
4701
4758
  const version = await provider.getNativeVersion({
@@ -4709,7 +4766,7 @@ var MessageRouter = class _MessageRouter {
4709
4766
  lastActivityAt: Date.now()
4710
4767
  }, session.id);
4711
4768
  }
4712
- async runChatGeneration(message, user, text, seq) {
4769
+ async runChatGeneration(message, user, rawText, seq) {
4713
4770
  const superseded = () => (this.latestMutatingSeq.get(message.chatId) ?? 0) > seq;
4714
4771
  if (superseded()) return;
4715
4772
  const sessionResumeTitle = buildSessionBridgeName({
@@ -4765,6 +4822,7 @@ var MessageRouter = class _MessageRouter {
4765
4822
  await this.sendToChat({ chatId: message.chatId, kind: "status", text: freshness.message });
4766
4823
  return;
4767
4824
  }
4825
+ const text = this.resolveChoiceReply(message.chatId, rawText);
4768
4826
  const genId = ++this.generationSeq;
4769
4827
  let abortGeneration;
4770
4828
  const aborted = new Promise((resolve3) => {
@@ -4912,20 +4970,7 @@ var MessageRouter = class _MessageRouter {
4912
4970
  return;
4913
4971
  }
4914
4972
  await this.preemptActiveGeneration(chatId);
4915
- this.recycleProviderProcess(current);
4916
- const reattached = await provider.attachSession({
4917
- candidateId: current.providerSessionId,
4918
- bridgeSessionId: current.id,
4919
- cwd: current.cwd
4920
- });
4921
- this.conversation.update({
4922
- providerSessionId: reattached.providerSessionId ?? current.providerSessionId,
4923
- status: reattached.status,
4924
- lastSeenNativeFingerprint: void 0,
4925
- lastReloadedAt: Date.now(),
4926
- staleReason: void 0,
4927
- lastActivityAt: Date.now()
4928
- }, current.id);
4973
+ await this.reattachSession(current, provider);
4929
4974
  await this.sendToChat({ chatId, kind: "status", text: "\u5DF2\u91CD\u65B0\u52A0\u8F7D\u5F53\u524D\u4F1A\u8BDD\uFF0C\u53EF\u7EE7\u7EED\u5BF9\u8BDD\u3002" });
4930
4975
  return;
4931
4976
  }
@@ -5087,6 +5132,7 @@ var MessageRouter = class _MessageRouter {
5087
5132
  }
5088
5133
  }
5089
5134
  };
5135
+ var LEGACY_NATIVE_FINGERPRINT = /^(claude|codex):[^:]+:\d+:\d+$/;
5090
5136
  function summarizeResumeTitle(text) {
5091
5137
  const normalized = text.replace(/\s+/g, " ").trim();
5092
5138
  if (!normalized) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-wechat",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.5.0",