claude-codex-wechat 0.1.34 → 0.1.36
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.
- package/dist/mcp/scripts/douyin-download.mjs +140 -0
- package/dist/server/cli.js +0 -14
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 抖音无水印视频下载脚本(自包含,无外部依赖)。
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* node douyin-download.mjs "<抖音链接>"
|
|
8
|
+
* node douyin-download.mjs "<抖音链接>" --output ./videos/
|
|
9
|
+
* node douyin-download.mjs "<分享文案含链接>"
|
|
10
|
+
*
|
|
11
|
+
* 要求:Node.js 18+(需要全局 fetch)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
// ─── 常量 ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const MOBILE_UA =
|
|
20
|
+
"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
|
+
const DESKTOP_UA =
|
|
22
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
23
|
+
|
|
24
|
+
// ─── 解析逻辑 ────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** 从任意文本里提取第一个抖音 URL */
|
|
27
|
+
function extractDouyinUrl(text) {
|
|
28
|
+
const m = text.match(
|
|
29
|
+
/https?:\/\/(?:v\.douyin\.com|www\.douyin\.com|www\.iesdouyin\.com|iesdouyin\.com)\/[^\s"'))】]+/i
|
|
30
|
+
);
|
|
31
|
+
return m ? m[0] : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 把任意输入解析成 aweme_id */
|
|
35
|
+
async function resolveAwemeId(input) {
|
|
36
|
+
const trimmed = input.trim();
|
|
37
|
+
if (/^\d{15,}$/.test(trimmed)) return trimmed;
|
|
38
|
+
|
|
39
|
+
const url = extractDouyinUrl(trimmed) ?? trimmed;
|
|
40
|
+
|
|
41
|
+
const direct = url.match(/(?:share\/)?video\/(\d{15,})/);
|
|
42
|
+
if (direct) return direct[1];
|
|
43
|
+
|
|
44
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
45
|
+
throw new Error(`无法识别的输入(既不是抖音链接,也不是 aweme_id):${trimmed}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const resp = await fetch(url, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
headers: { "User-Agent": MOBILE_UA },
|
|
51
|
+
redirect: "follow",
|
|
52
|
+
});
|
|
53
|
+
const m = resp.url.match(/(\d{15,})/);
|
|
54
|
+
if (!m) throw new Error(`短链重定向后未找到 aweme_id,最终地址:${resp.url}`);
|
|
55
|
+
return m[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 从抖音分享页提取 video_id */
|
|
59
|
+
async function resolveVideoId(awemeId) {
|
|
60
|
+
const shareUrl = `https://www.iesdouyin.com/share/video/${awemeId}/`;
|
|
61
|
+
const resp = await fetch(shareUrl, { headers: { "User-Agent": MOBILE_UA } });
|
|
62
|
+
if (!resp.ok) throw new Error(`分享页请求失败:HTTP ${resp.status}(aweme_id=${awemeId})`);
|
|
63
|
+
const html = await resp.text();
|
|
64
|
+
const m =
|
|
65
|
+
html.match(/video_id=([a-z0-9]+)/i) ??
|
|
66
|
+
html.match(/"video_id"\s*:\s*"([a-z0-9]+)"/i) ??
|
|
67
|
+
html.match(/playwm\/\?[^"'\\]*?video_id=([a-z0-9]+)/i);
|
|
68
|
+
if (!m) throw new Error(`分享页里未找到 video_id(aweme_id=${awemeId})`);
|
|
69
|
+
return m[1];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 拼无水印播放入口(play 而非 playwm) */
|
|
73
|
+
function buildPlayUrlNoWatermark(videoId, ratio = "720p") {
|
|
74
|
+
return `https://aweme.snssdk.com/aweme/v1/play/?ratio=${ratio}&video_id=${videoId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── 下载逻辑 ────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
async function download(input, outputDir) {
|
|
80
|
+
console.log("正在解析...");
|
|
81
|
+
|
|
82
|
+
const trimmed = input.trim();
|
|
83
|
+
let awemeId = "";
|
|
84
|
+
let videoId = "";
|
|
85
|
+
|
|
86
|
+
if (/^v0[a-z0-9]{10,}$/i.test(trimmed)) {
|
|
87
|
+
videoId = trimmed;
|
|
88
|
+
} else {
|
|
89
|
+
awemeId = await resolveAwemeId(trimmed);
|
|
90
|
+
videoId = await resolveVideoId(awemeId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`aweme_id: ${awemeId || "-"} video_id: ${videoId}`);
|
|
94
|
+
|
|
95
|
+
const playUrl = buildPlayUrlNoWatermark(videoId);
|
|
96
|
+
console.log("正在下载无水印视频...");
|
|
97
|
+
|
|
98
|
+
const resp = await fetch(playUrl, {
|
|
99
|
+
headers: { "User-Agent": DESKTOP_UA, Referer: "https://www.douyin.com/" },
|
|
100
|
+
redirect: "follow",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!resp.ok) {
|
|
104
|
+
throw new Error(`下载失败: HTTP ${resp.status}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
108
|
+
const filename = `douyin_${awemeId || videoId}.mp4`;
|
|
109
|
+
|
|
110
|
+
const dir = outputDir ? resolve(outputDir) : process.cwd();
|
|
111
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
112
|
+
|
|
113
|
+
const filepath = resolve(dir, filename);
|
|
114
|
+
writeFileSync(filepath, buf);
|
|
115
|
+
|
|
116
|
+
const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
|
|
117
|
+
console.log(`✅ 已保存: ${filepath} (${sizeMB} MB)`);
|
|
118
|
+
return filepath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── CLI 入口 ─────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
const argv = process.argv.slice(2);
|
|
124
|
+
const outputIdx = argv.indexOf("--output");
|
|
125
|
+
let outputDir;
|
|
126
|
+
if (outputIdx !== -1) {
|
|
127
|
+
outputDir = argv[outputIdx + 1];
|
|
128
|
+
argv.splice(outputIdx, 2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const input = argv.join(" ").trim();
|
|
132
|
+
if (!input) {
|
|
133
|
+
console.error('用法: node douyin-download.mjs "<抖音链接>" [--output <目录>]');
|
|
134
|
+
process.exit(2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
download(input, outputDir).catch((error) => {
|
|
138
|
+
console.error("❌ 错误:", error.message || error);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
package/dist/server/cli.js
CHANGED
|
@@ -11,7 +11,6 @@ import { networkInterfaces } from "node:os";
|
|
|
11
11
|
|
|
12
12
|
// src/daemon/server.ts
|
|
13
13
|
import { mkdtempSync, existsSync as existsSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
14
|
-
import { execSync } from "node:child_process";
|
|
15
14
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
16
15
|
import { dirname as dirname7, join as join11 } from "node:path";
|
|
17
16
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -4928,18 +4927,6 @@ function readUpdateStatusBestEffort(configPath) {
|
|
|
4928
4927
|
return { ok: false };
|
|
4929
4928
|
}
|
|
4930
4929
|
}
|
|
4931
|
-
function ensureCodexMcpRegistered(mediaServerPath, bridgePort) {
|
|
4932
|
-
try {
|
|
4933
|
-
execSync("codex --version", { stdio: "ignore" });
|
|
4934
|
-
const list = execSync("codex mcp list", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
|
|
4935
|
-
if (list.includes("wechat-media")) return;
|
|
4936
|
-
execSync(
|
|
4937
|
-
`codex mcp add wechat-media --env "BRIDGE_API_URL=http://localhost:${bridgePort}" -- node "${mediaServerPath}"`,
|
|
4938
|
-
{ stdio: "ignore" }
|
|
4939
|
-
);
|
|
4940
|
-
} catch {
|
|
4941
|
-
}
|
|
4942
|
-
}
|
|
4943
4930
|
function createDaemonServer(options = {}) {
|
|
4944
4931
|
const app = Fastify({ logger: true });
|
|
4945
4932
|
const events = new BridgeEventHub();
|
|
@@ -4996,7 +4983,6 @@ function createDaemonServer(options = {}) {
|
|
|
4996
4983
|
}
|
|
4997
4984
|
}
|
|
4998
4985
|
}, null, 2) + "\n");
|
|
4999
|
-
ensureCodexMcpRegistered(useTs ? mcpMediaServerTs : mcpMediaServerJs, bridgePort);
|
|
5000
4986
|
const providerAdapters = options.providers ?? createDefaultProviders({
|
|
5001
4987
|
claudeCommand: options.providerCommands?.claude?.command,
|
|
5002
4988
|
codexCommand: options.providerCommands?.codex?.command,
|