claude-codex-wechat 0.1.57 → 0.1.59

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.
@@ -142,6 +142,12 @@ function locateDouyinScript(input) {
142
142
  return void 0;
143
143
  }
144
144
 
145
+ // src/shared/platform.ts
146
+ var isWindows = process.platform === "win32";
147
+ function nodeExecutable() {
148
+ return process.execPath;
149
+ }
150
+
145
151
  // src/mcp/tools/douyinDownload.ts
146
152
  function findDouyinScript() {
147
153
  const here = dirname(fileURLToPath(import.meta.url));
@@ -152,7 +158,7 @@ function findDouyinScript() {
152
158
  function runScript(args) {
153
159
  const script = findDouyinScript();
154
160
  return new Promise((resolve2, reject) => {
155
- execFile("node", [script, ...args], { timeout: 12e4 }, (err, stdout, stderr) => {
161
+ execFile(nodeExecutable(), [script, ...args], { timeout: 18e4 }, (err, stdout, stderr) => {
156
162
  if (err) reject(new Error(stderr || err.message));
157
163
  else resolve2({ stdout, stderr });
158
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();
@@ -15,6 +15,106 @@ import { networkInterfaces } from "node:os";
15
15
  import { mkdtempSync, existsSync as existsSync10, writeFileSync as writeFileSync2 } from "node:fs";
16
16
  import { tmpdir as tmpdir3 } from "node:os";
17
17
  import { dirname as dirname8, isAbsolute, join as join13 } from "node:path";
18
+
19
+ // src/daemon/mcpConfig.ts
20
+ import { createRequire } from "node:module";
21
+
22
+ // src/shared/platform.ts
23
+ import { access, constants } from "node:fs/promises";
24
+ import { delimiter, join } from "node:path";
25
+ import { tmpdir } from "node:os";
26
+ import { spawn } from "node:child_process";
27
+ var isWindows = process.platform === "win32";
28
+ async function isExecutableFile(candidate) {
29
+ try {
30
+ await access(candidate, isWindows ? constants.F_OK : constants.X_OK);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function executableExtensions() {
37
+ if (!isWindows) return [""];
38
+ const pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
39
+ return ["", ...pathext.split(";").filter(Boolean)];
40
+ }
41
+ function isTemporaryShim(candidate) {
42
+ const tempDir = tmpdir();
43
+ return candidate.startsWith(tempDir);
44
+ }
45
+ async function findExecutable(command, pathEnv) {
46
+ const extensions = executableExtensions();
47
+ if (command.includes("/") || command.includes("\\")) {
48
+ for (const ext of extensions) {
49
+ if (await isExecutableFile(command + ext)) return command + ext;
50
+ }
51
+ return void 0;
52
+ }
53
+ const dirs = (pathEnv ?? "").split(delimiter).filter(Boolean);
54
+ let fallback;
55
+ for (const dir of dirs) {
56
+ for (const ext of extensions) {
57
+ const candidate = join(dir, command + ext);
58
+ if (await isExecutableFile(candidate)) {
59
+ if (isTemporaryShim(candidate)) {
60
+ fallback ??= candidate;
61
+ continue;
62
+ }
63
+ return candidate;
64
+ }
65
+ }
66
+ }
67
+ return fallback;
68
+ }
69
+ function terminateChild(child, signal = "SIGTERM") {
70
+ if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) {
71
+ return;
72
+ }
73
+ if (isWindows) {
74
+ try {
75
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
76
+ return;
77
+ } catch {
78
+ }
79
+ }
80
+ try {
81
+ child.kill(signal);
82
+ } catch {
83
+ }
84
+ }
85
+ function defaultWorkspaceDir() {
86
+ return join(tmpdir(), "project");
87
+ }
88
+ function useShellForCli() {
89
+ return isWindows;
90
+ }
91
+ function statePath(filename) {
92
+ return join(tmpdir(), filename);
93
+ }
94
+ function nodeExecutable() {
95
+ return process.execPath;
96
+ }
97
+
98
+ // src/daemon/mcpConfig.ts
99
+ function buildMcpServerCommand(input) {
100
+ if (input.exists(input.jsEntry)) {
101
+ return { command: nodeExecutable(), args: [input.jsEntry] };
102
+ }
103
+ const tsxCli = (input.resolveTsxCli ?? defaultResolveTsxCli)();
104
+ if (tsxCli) {
105
+ return { command: nodeExecutable(), args: [tsxCli, input.tsEntry] };
106
+ }
107
+ return { command: "tsx", args: [input.tsEntry] };
108
+ }
109
+ function defaultResolveTsxCli() {
110
+ try {
111
+ return createRequire(import.meta.url).resolve("tsx/dist/cli.mjs");
112
+ } catch {
113
+ return void 0;
114
+ }
115
+ }
116
+
117
+ // src/daemon/server.ts
18
118
  import { fileURLToPath as fileURLToPath3 } from "node:url";
19
119
  import Fastify from "fastify";
20
120
 
@@ -69,9 +169,9 @@ var WeixinDirectLoginClient = class {
69
169
  // src/daemon/config.ts
70
170
  import { existsSync, readFileSync } from "node:fs";
71
171
  import { homedir } from "node:os";
72
- import { join } from "node:path";
172
+ import { join as join2 } from "node:path";
73
173
  function defaultConfigPath() {
74
- return join(homedir(), ".claude-codex-wechat", "config.json");
174
+ return join2(homedir(), ".claude-codex-wechat", "config.json");
75
175
  }
76
176
  function loadBridgeConfig(path = process.env.BRIDGE_CONFIG ?? defaultConfigPath()) {
77
177
  if (!existsSync(path)) return normalizeBridgeConfig({}, process.env, path);
@@ -162,10 +262,10 @@ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:f
162
262
  import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, unlinkSync, writeSync } from "node:fs";
163
263
  import { mkdir, open, readdir, rename, stat, unlink } from "node:fs/promises";
164
264
  import { randomBytes } from "node:crypto";
165
- import { basename, dirname, join as join2 } from "node:path";
265
+ import { basename, dirname, join as join3 } from "node:path";
166
266
  var FILE_MODE = 384;
167
267
  function tempPathFor(path) {
168
- return join2(dirname(path), `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
268
+ return join3(dirname(path), `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
169
269
  }
170
270
  var RENAME_RETRY_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
171
271
  var MAX_RENAME_RETRIES = 10;
@@ -247,7 +347,7 @@ async function cleanupStaleTempFiles(targetPath, maxAgeMs = 36e5) {
247
347
  const now = Date.now();
248
348
  await Promise.all(entries.map(async (name) => {
249
349
  if (!name.startsWith(prefix) || !name.endsWith(".tmp")) return;
250
- const full = join2(dir, name);
350
+ const full = join3(dir, name);
251
351
  try {
252
352
  const info = await stat(full);
253
353
  if (now - info.mtimeMs >= maxAgeMs) await unlink(full);
@@ -501,7 +601,7 @@ var FileWeixinStateStore = class {
501
601
  // src/providers/claude-code/nativeSessions.ts
502
602
  import { appendFile, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, stat as stat2, unlink as unlink2, writeFile } from "node:fs/promises";
503
603
  import { homedir as homedir2 } from "node:os";
504
- import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
604
+ import { basename as basename2, dirname as dirname2, join as join4 } from "node:path";
505
605
 
506
606
  // src/session/sessionBridgeTag.ts
507
607
  function buildSessionBridgeName(input) {
@@ -528,7 +628,7 @@ function parseSessionBridgeName(value) {
528
628
 
529
629
  // src/providers/claude-code/nativeSessions.ts
530
630
  function resolveClaudeConfigDir(env = process.env) {
531
- return join3(env.HOME || homedir2(), ".claude");
631
+ return join4(env.HOME || homedir2(), ".claude");
532
632
  }
533
633
  async function prepareClaudeSessionForResume(input) {
534
634
  const env = input.env ?? process.env;
@@ -544,20 +644,20 @@ async function prepareClaudeSessionForResume(input) {
544
644
  return { ok: true, adoptedBackground: true };
545
645
  }
546
646
  async function listRecoverableClaudeSessions(env = process.env) {
547
- const projectsDir = join3(resolveClaudeConfigDir(env), "projects");
647
+ const projectsDir = join4(resolveClaudeConfigDir(env), "projects");
548
648
  const historyIndex = await readClaudeHistoryIndex(env);
549
649
  const candidates = [];
550
650
  try {
551
651
  const projectEntries = await readdir2(projectsDir, { withFileTypes: true });
552
652
  for (const entry of projectEntries) {
553
653
  if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
554
- const projectDir = join3(projectsDir, String(entry.name));
654
+ const projectDir = join4(projectsDir, String(entry.name));
555
655
  const sessionEntries = await readdir2(projectDir, { withFileTypes: true }).catch(() => []);
556
656
  for (const sessionEntry of sessionEntries) {
557
657
  if (!sessionEntry.isFile() || sessionEntry.isSymbolicLink()) continue;
558
658
  const fileName = String(sessionEntry.name);
559
659
  if (!fileName.endsWith(".jsonl")) continue;
560
- const filePath = join3(projectDir, fileName);
660
+ const filePath = join4(projectDir, fileName);
561
661
  const metadata = await stat2(filePath).catch(() => null);
562
662
  const parsedMeta = await readClaudeSessionMetadata(filePath).catch(() => null);
563
663
  const historyMeta = historyIndex.get(basename2(fileName, ".jsonl"));
@@ -583,12 +683,12 @@ async function listRecoverableClaudeSessions(env = process.env) {
583
683
  async function findRecoverableClaudeSessionPath(sessionId, env = process.env) {
584
684
  const trimmed = sessionId.trim();
585
685
  if (!trimmed || trimmed.includes("/") || trimmed.includes("\\")) return null;
586
- const projectsDir = join3(resolveClaudeConfigDir(env), "projects");
686
+ const projectsDir = join4(resolveClaudeConfigDir(env), "projects");
587
687
  try {
588
688
  const projectEntries = await readdir2(projectsDir, { withFileTypes: true });
589
689
  for (const entry of projectEntries) {
590
690
  if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
591
- const sessionPath = join3(projectsDir, String(entry.name), `${trimmed}.jsonl`);
691
+ const sessionPath = join4(projectsDir, String(entry.name), `${trimmed}.jsonl`);
592
692
  try {
593
693
  const metadata = await stat2(sessionPath);
594
694
  if (metadata.isFile()) return sessionPath;
@@ -636,7 +736,7 @@ async function hasClaudeSessionBridgeMetadata(input) {
636
736
  }
637
737
  async function hasClaudeHistoryDisplay(input) {
638
738
  const env = input.env ?? process.env;
639
- const historyPath = join3(resolveClaudeConfigDir(env), "history.jsonl");
739
+ const historyPath = join4(resolveClaudeConfigDir(env), "history.jsonl");
640
740
  try {
641
741
  const content = await readFile2(historyPath, "utf8");
642
742
  for (const line of content.split(/\r?\n/)) {
@@ -701,13 +801,13 @@ async function readClaudeSessionMetadata(filePath) {
701
801
  return { aiTitle, lastPrompt, sessionName, cwd };
702
802
  }
703
803
  async function readClaudeRuntimeSessionIndex(env) {
704
- const sessionsDir = join3(resolveClaudeConfigDir(env), "sessions");
804
+ const sessionsDir = join4(resolveClaudeConfigDir(env), "sessions");
705
805
  const index = /* @__PURE__ */ new Map();
706
806
  try {
707
807
  const entries = await readdir2(sessionsDir, { withFileTypes: true });
708
808
  for (const entry of entries) {
709
809
  if (!entry.isFile() || entry.isSymbolicLink() || !String(entry.name).endsWith(".json")) continue;
710
- const path = join3(sessionsDir, String(entry.name));
810
+ const path = join4(sessionsDir, String(entry.name));
711
811
  try {
712
812
  const raw = await readFile2(path, "utf8");
713
813
  const record = JSON.parse(raw);
@@ -725,7 +825,7 @@ async function readClaudeRuntimeSessionIndex(env) {
725
825
  return index;
726
826
  }
727
827
  async function readClaudeHistoryIndex(env) {
728
- const historyPath = join3(resolveClaudeConfigDir(env), "history.jsonl");
828
+ const historyPath = join4(resolveClaudeConfigDir(env), "history.jsonl");
729
829
  const index = /* @__PURE__ */ new Map();
730
830
  try {
731
831
  const content = await readFile2(historyPath, "utf8");
@@ -750,7 +850,7 @@ async function readClaudeHistoryIndex(env) {
750
850
  return index;
751
851
  }
752
852
  async function upsertClaudeHistoryDisplay(input) {
753
- const historyPath = join3(resolveClaudeConfigDir(input.env), "history.jsonl");
853
+ const historyPath = join4(resolveClaudeConfigDir(input.env), "history.jsonl");
754
854
  const lines = [];
755
855
  let replaced = false;
756
856
  let changed = false;
@@ -840,9 +940,9 @@ import Database from "better-sqlite3";
840
940
  import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "node:fs/promises";
841
941
  import { existsSync as existsSync4 } from "node:fs";
842
942
  import { homedir as homedir3 } from "node:os";
843
- import { basename as basename3, join as join4 } from "node:path";
943
+ import { basename as basename3, join as join5 } from "node:path";
844
944
  function resolveCodexSessionsRoot(env = process.env) {
845
- return join4(env.CODEX_HOME || join4(env.HOME || homedir3(), ".codex"), "sessions");
945
+ return join5(env.CODEX_HOME || join5(env.HOME || homedir3(), ".codex"), "sessions");
846
946
  }
847
947
  function isMatchingCodexRolloutFile(name) {
848
948
  return name.startsWith("rollout-") && name.endsWith(".jsonl");
@@ -865,7 +965,7 @@ async function listRecoverableCodexSessions(env = process.env) {
865
965
  for (const entry of entries) {
866
966
  if (entry.isSymbolicLink()) continue;
867
967
  const name = String(entry.name);
868
- const path = join4(currentDir, name);
968
+ const path = join5(currentDir, name);
869
969
  if (entry.isDirectory()) {
870
970
  await walk(path, depth + 1);
871
971
  continue;
@@ -901,7 +1001,7 @@ async function findRecoverableCodexSessionPath(sessionId, env = process.env) {
901
1001
  for (const entry of entries) {
902
1002
  if (entry.isSymbolicLink()) continue;
903
1003
  const name = String(entry.name);
904
- const path = join4(currentDir, name);
1004
+ const path = join5(currentDir, name);
905
1005
  if (entry.isDirectory()) {
906
1006
  const found = await walk(path, depth + 1);
907
1007
  if (found) return found;
@@ -925,7 +1025,7 @@ async function getCodexNativeVersion(input) {
925
1025
  };
926
1026
  }
927
1027
  async function readCodexSessionIndex(env) {
928
- const indexPath = join4(env.CODEX_HOME || join4(env.HOME || homedir3(), ".codex"), "session_index.jsonl");
1028
+ const indexPath = join5(env.CODEX_HOME || join5(env.HOME || homedir3(), ".codex"), "session_index.jsonl");
929
1029
  const index = /* @__PURE__ */ new Map();
930
1030
  try {
931
1031
  const content = await readFile3(indexPath, "utf8");
@@ -948,7 +1048,7 @@ async function readCodexSessionIndex(env) {
948
1048
  return index;
949
1049
  }
950
1050
  function readCodexThreadMetadata(env) {
951
- const dbPath = join4(env.CODEX_HOME || join4(env.HOME || homedir3(), ".codex"), "state_5.sqlite");
1051
+ const dbPath = join5(env.CODEX_HOME || join5(env.HOME || homedir3(), ".codex"), "state_5.sqlite");
952
1052
  const metadata = /* @__PURE__ */ new Map();
953
1053
  if (!existsSync4(dbPath)) return metadata;
954
1054
  let db = null;
@@ -1009,9 +1109,9 @@ import { nanoid } from "nanoid";
1009
1109
  // src/providers/codex/sessionIndex.ts
1010
1110
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
1011
1111
  import { homedir as homedir4 } from "node:os";
1012
- import { dirname as dirname3, join as join5 } from "node:path";
1112
+ import { dirname as dirname3, join as join6 } from "node:path";
1013
1113
  function resolveCodexIndexPath(env = process.env) {
1014
- return join5(env.CODEX_HOME || join5(env.HOME || homedir4(), ".codex"), "session_index.jsonl");
1114
+ return join6(env.CODEX_HOME || join6(env.HOME || homedir4(), ".codex"), "session_index.jsonl");
1015
1115
  }
1016
1116
  async function upsertCodexSessionIndexEntry(input, env = process.env) {
1017
1117
  const indexPath = resolveCodexIndexPath(env);
@@ -1057,9 +1157,9 @@ import Database2 from "better-sqlite3";
1057
1157
  import { existsSync as existsSync5 } from "node:fs";
1058
1158
  import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
1059
1159
  import { homedir as homedir5 } from "node:os";
1060
- import { join as join6 } from "node:path";
1160
+ import { join as join7 } from "node:path";
1061
1161
  function resolveCodexStateDbPath(env = process.env) {
1062
- return join6(env.CODEX_HOME || join6(env.HOME || homedir5(), ".codex"), "state_5.sqlite");
1162
+ return join7(env.CODEX_HOME || join7(env.HOME || homedir5(), ".codex"), "state_5.sqlite");
1063
1163
  }
1064
1164
  async function syncCodexThreadForResume(input) {
1065
1165
  const env = input.env ?? process.env;
@@ -1741,7 +1841,7 @@ function normalizeSettings(input, defaultWorkspace) {
1741
1841
  // src/admin/fsBrowseRoutes.ts
1742
1842
  import { readdir as readdir4 } from "node:fs/promises";
1743
1843
  import { homedir as homedir7 } from "node:os";
1744
- import { dirname as dirname4, join as join7, resolve } from "node:path";
1844
+ import { dirname as dirname4, join as join8, resolve } from "node:path";
1745
1845
 
1746
1846
  // src/shared/expandTilde.ts
1747
1847
  import { homedir as homedir6 } from "node:os";
@@ -1767,7 +1867,7 @@ function registerFsBrowseRoutes(input) {
1767
1867
  reply.code(400);
1768
1868
  return { error: "cannot_read_directory", path: target };
1769
1869
  }
1770
- const entries = dirents.filter((dirent) => dirent.isDirectory()).map((dirent) => ({ name: dirent.name, path: join7(target, dirent.name), isDirectory: true })).filter((entry) => !entry.name.startsWith(".") || isOnKeepChain(entry.path)).sort((a, b) => a.name.localeCompare(b.name));
1870
+ const entries = dirents.filter((dirent) => dirent.isDirectory()).map((dirent) => ({ name: dirent.name, path: join8(target, dirent.name), isDirectory: true })).filter((entry) => !entry.name.startsWith(".") || isOnKeepChain(entry.path)).sort((a, b) => a.name.localeCompare(b.name));
1771
1871
  const parent = dirname4(target);
1772
1872
  const isRoot = parent === target;
1773
1873
  const listing = {
@@ -2048,7 +2148,7 @@ var WeixinDirectApiClient = class {
2048
2148
 
2049
2149
  // src/channels/weixin-direct/adapter.ts
2050
2150
  import { setTimeout as delay2 } from "node:timers/promises";
2051
- import { join as join8 } from "node:path";
2151
+ import { join as join9 } from "node:path";
2052
2152
 
2053
2153
  // src/channels/weixin-direct/typingController.ts
2054
2154
  var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -2344,7 +2444,7 @@ var WeixinDirectAdapter = class _WeixinDirectAdapter {
2344
2444
  continue;
2345
2445
  }
2346
2446
  const { ext, mimeType } = mediaExtAndMime(meta);
2347
- const destPath = join8(mediaDir, `${idPrefix}_${i}${ext}`);
2447
+ const destPath = join9(mediaDir, `${idPrefix}_${i}${ext}`);
2348
2448
  const maxBytes = meta.kind === "video" ? VIDEO_MAX_BYTES : void 0;
2349
2449
  const result = await downloader.download(meta.media ?? {}, { destPath, aeskeyOverride: meta.aeskey, maxBytes });
2350
2450
  if (result.ok) {
@@ -2875,79 +2975,6 @@ function stamp(item) {
2875
2975
  import { spawn as spawn3 } from "node:child_process";
2876
2976
  import { randomUUID as randomUUID2 } from "node:crypto";
2877
2977
 
2878
- // src/shared/platform.ts
2879
- import { access, constants } from "node:fs/promises";
2880
- import { delimiter, join as join9 } from "node:path";
2881
- import { tmpdir } from "node:os";
2882
- import { spawn } from "node:child_process";
2883
- var isWindows = process.platform === "win32";
2884
- async function isExecutableFile(candidate) {
2885
- try {
2886
- await access(candidate, isWindows ? constants.F_OK : constants.X_OK);
2887
- return true;
2888
- } catch {
2889
- return false;
2890
- }
2891
- }
2892
- function executableExtensions() {
2893
- if (!isWindows) return [""];
2894
- const pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
2895
- return ["", ...pathext.split(";").filter(Boolean)];
2896
- }
2897
- function isTemporaryShim(candidate) {
2898
- const tempDir = tmpdir();
2899
- return candidate.startsWith(tempDir);
2900
- }
2901
- async function findExecutable(command, pathEnv) {
2902
- const extensions = executableExtensions();
2903
- if (command.includes("/") || command.includes("\\")) {
2904
- for (const ext of extensions) {
2905
- if (await isExecutableFile(command + ext)) return command + ext;
2906
- }
2907
- return void 0;
2908
- }
2909
- const dirs = (pathEnv ?? "").split(delimiter).filter(Boolean);
2910
- let fallback;
2911
- for (const dir of dirs) {
2912
- for (const ext of extensions) {
2913
- const candidate = join9(dir, command + ext);
2914
- if (await isExecutableFile(candidate)) {
2915
- if (isTemporaryShim(candidate)) {
2916
- fallback ??= candidate;
2917
- continue;
2918
- }
2919
- return candidate;
2920
- }
2921
- }
2922
- }
2923
- return fallback;
2924
- }
2925
- function terminateChild(child, signal = "SIGTERM") {
2926
- if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) {
2927
- return;
2928
- }
2929
- if (isWindows) {
2930
- try {
2931
- spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
2932
- return;
2933
- } catch {
2934
- }
2935
- }
2936
- try {
2937
- child.kill(signal);
2938
- } catch {
2939
- }
2940
- }
2941
- function defaultWorkspaceDir() {
2942
- return join9(tmpdir(), "project");
2943
- }
2944
- function useShellForCli() {
2945
- return isWindows;
2946
- }
2947
- function statePath(filename) {
2948
- return join9(tmpdir(), filename);
2949
- }
2950
-
2951
2978
  // src/shared/loginShellEnv.ts
2952
2979
  import { spawnSync } from "node:child_process";
2953
2980
  import { delimiter as delimiter2 } from "node:path";
@@ -5608,9 +5635,11 @@ function createDaemonServer(options = {}) {
5608
5635
  const serverDir = dirname8(fileURLToPath3(import.meta.url));
5609
5636
  const mcpMediaServerTs = join13(serverDir, "..", "mcp", "mediaServer.ts");
5610
5637
  const mcpMediaServerJs = join13(serverDir, "..", "mcp", "mediaServer.js");
5611
- const useTs = existsSync10(mcpMediaServerTs) && !existsSync10(mcpMediaServerJs);
5612
- const mcpCommand = useTs ? "tsx" : "node";
5613
- const mcpArgs = [useTs ? mcpMediaServerTs : mcpMediaServerJs];
5638
+ const { command: mcpCommand, args: mcpArgs } = buildMcpServerCommand({
5639
+ tsEntry: mcpMediaServerTs,
5640
+ jsEntry: mcpMediaServerJs,
5641
+ exists: existsSync10
5642
+ });
5614
5643
  writeFileSync2(mcpConfigPath, JSON.stringify({
5615
5644
  mcpServers: {
5616
5645
  "wechat-media": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-wechat",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.5.0",