claude-codex-wechat 0.1.28 → 0.1.30
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/mediaServer.js +91 -0
- package/dist/server/cli.js +451 -59
- package/package.json +4 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createRequire as __cjsCreateRequire } from 'node:module'; const require = __cjsCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/mcp/mediaServer.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var BRIDGE_API_URL = process.env.BRIDGE_API_URL || "http://localhost:4800";
|
|
8
|
+
var server = new McpServer({
|
|
9
|
+
name: "wechat-media",
|
|
10
|
+
version: "1.0.0"
|
|
11
|
+
});
|
|
12
|
+
async function sendMedia(kind, filePath, fileName) {
|
|
13
|
+
const response = await fetch(`${BRIDGE_API_URL}/api/channel/send-media`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "content-type": "application/json" },
|
|
16
|
+
body: JSON.stringify({ kind, filePath, fileName })
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
throw new Error(`\u53D1\u9001\u5931\u8D25 (HTTP ${response.status}): ${text}`);
|
|
21
|
+
}
|
|
22
|
+
const result = await response.json();
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
throw new Error(`\u53D1\u9001\u5931\u8D25: ${result.error ?? "\u672A\u77E5\u9519\u8BEF"}`);
|
|
25
|
+
}
|
|
26
|
+
return "\u53D1\u9001\u6210\u529F";
|
|
27
|
+
}
|
|
28
|
+
server.tool(
|
|
29
|
+
"send_image",
|
|
30
|
+
"\u53D1\u9001\u56FE\u7247\u6587\u4EF6\u5230\u5F53\u524D\u5FAE\u4FE1\u5BF9\u8BDD\u3002\u4F20\u5165\u56FE\u7247\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
|
|
31
|
+
{ filePath: z.string().describe("\u56FE\u7247\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u5982 /path/to/image.png") },
|
|
32
|
+
async ({ filePath }) => {
|
|
33
|
+
try {
|
|
34
|
+
const msg = await sendMedia("image", filePath);
|
|
35
|
+
return { content: [{ type: "text", text: msg }] };
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
server.tool(
|
|
42
|
+
"send_video",
|
|
43
|
+
"\u53D1\u9001\u89C6\u9891\u6587\u4EF6\u5230\u5F53\u524D\u5FAE\u4FE1\u5BF9\u8BDD\u3002\u4F20\u5165\u89C6\u9891\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
|
|
44
|
+
{ filePath: z.string().describe("\u89C6\u9891\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u5982 /path/to/video.mp4") },
|
|
45
|
+
async ({ filePath }) => {
|
|
46
|
+
try {
|
|
47
|
+
const msg = await sendMedia("video", filePath);
|
|
48
|
+
return { content: [{ type: "text", text: msg }] };
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
server.tool(
|
|
55
|
+
"send_audio",
|
|
56
|
+
"\u53D1\u9001\u97F3\u9891\u6587\u4EF6\u5230\u5F53\u524D\u5FAE\u4FE1\u5BF9\u8BDD\u3002\u4F20\u5165\u97F3\u9891\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002\u97F3\u9891\u6587\u4EF6\u4EE5\u6587\u4EF6\u9644\u4EF6\u5F62\u5F0F\u53D1\u9001\uFF0C\u7528\u6237\u53EF\u70B9\u51FB\u64AD\u653E\u3002",
|
|
57
|
+
{ filePath: z.string().describe("\u97F3\u9891\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u5982 /path/to/audio.mp3") },
|
|
58
|
+
async ({ filePath }) => {
|
|
59
|
+
try {
|
|
60
|
+
const fileName = filePath.split("/").pop() || "audio.mp3";
|
|
61
|
+
const msg = await sendMedia("file", filePath, fileName);
|
|
62
|
+
return { content: [{ type: "text", text: msg }] };
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
server.tool(
|
|
69
|
+
"send_file",
|
|
70
|
+
"\u53D1\u9001\u6587\u4EF6\u5230\u5F53\u524D\u5FAE\u4FE1\u5BF9\u8BDD\u3002\u4F20\u5165\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u548C\u53EF\u9009\u7684\u663E\u793A\u6587\u4EF6\u540D\u3002",
|
|
71
|
+
{
|
|
72
|
+
filePath: z.string().describe("\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u5982 /path/to/document.pdf"),
|
|
73
|
+
fileName: z.string().optional().describe("\u663E\u793A\u7ED9\u7528\u6237\u7684\u6587\u4EF6\u540D\uFF08\u53EF\u9009\uFF0C\u9ED8\u8BA4\u4F7F\u7528\u539F\u6587\u4EF6\u540D\uFF09")
|
|
74
|
+
},
|
|
75
|
+
async ({ filePath, fileName }) => {
|
|
76
|
+
try {
|
|
77
|
+
const msg = await sendMedia("file", filePath, fileName);
|
|
78
|
+
return { content: [{ type: "text", text: msg }] };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
async function main() {
|
|
85
|
+
const transport = new StdioServerTransport();
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
}
|
|
88
|
+
main().catch((err) => {
|
|
89
|
+
console.error("[wechat-media-mcp] fatal:", err);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
package/dist/server/cli.js
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
import { createRequire as __cjsCreateRequire } from 'node:module'; const require = __cjsCreateRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
6
6
|
import { dirname as dirname9, join as join14 } from "node:path";
|
|
7
|
-
import { fileURLToPath as
|
|
7
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8
8
|
|
|
9
9
|
// src/daemon/bootstrap.ts
|
|
10
10
|
import { networkInterfaces } from "node:os";
|
|
11
11
|
|
|
12
12
|
// src/daemon/server.ts
|
|
13
|
-
import { mkdtempSync } from "node:fs";
|
|
13
|
+
import { mkdtempSync, existsSync as existsSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
14
14
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
15
15
|
import { dirname as dirname7, join as join11 } from "node:path";
|
|
16
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16
17
|
import Fastify from "fastify";
|
|
17
18
|
|
|
18
19
|
// src/channels/platforms.ts
|
|
@@ -1866,6 +1867,105 @@ var WeixinDirectApiClient = class {
|
|
|
1866
1867
|
throw new Error(`weixin_send_typing_failed:${payload.ret ?? -1}:${payload.errmsg ?? "unknown_error"}`);
|
|
1867
1868
|
}
|
|
1868
1869
|
}
|
|
1870
|
+
/** Request a CDN upload URL from the iLink platform. */
|
|
1871
|
+
async getUploadUrl(input) {
|
|
1872
|
+
const response = await this.fetchImpl(`${this.baseUrl}/ilink/bot/getuploadurl`, {
|
|
1873
|
+
method: "POST",
|
|
1874
|
+
headers: {
|
|
1875
|
+
"content-type": "application/json",
|
|
1876
|
+
AuthorizationType: "ilink_bot_token",
|
|
1877
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
1878
|
+
"X-WECHAT-UIN": this.wechatUin
|
|
1879
|
+
},
|
|
1880
|
+
body: JSON.stringify({
|
|
1881
|
+
filekey: input.filekey,
|
|
1882
|
+
media_type: input.mediaType,
|
|
1883
|
+
to_user_id: input.toUserId,
|
|
1884
|
+
rawsize: input.rawsize,
|
|
1885
|
+
rawfilemd5: input.rawfilemd5,
|
|
1886
|
+
filesize: input.filesize,
|
|
1887
|
+
no_need_thumb: input.noNeedThumb ?? true,
|
|
1888
|
+
aeskey: input.aeskey,
|
|
1889
|
+
...input.contextToken ? { context_token: input.contextToken } : {},
|
|
1890
|
+
base_info: { channel_version: this.channelVersion }
|
|
1891
|
+
})
|
|
1892
|
+
});
|
|
1893
|
+
if (!response.ok) {
|
|
1894
|
+
const text = await response.text().catch(() => "");
|
|
1895
|
+
throw new Error(`weixin_get_upload_url_failed:http_${response.status}:${text.slice(0, 200)}`);
|
|
1896
|
+
}
|
|
1897
|
+
const payload = await response.json();
|
|
1898
|
+
if ((payload.errcode ?? 0) !== 0 || (payload.ret ?? 0) !== 0) {
|
|
1899
|
+
throw new Error(`weixin_get_upload_url_failed:${payload.errcode ?? payload.ret ?? -1}:${payload.errmsg ?? "unknown_error"}`);
|
|
1900
|
+
}
|
|
1901
|
+
if (!payload.upload_param && !payload.upload_full_url) {
|
|
1902
|
+
throw new Error("weixin_get_upload_url_failed:no_upload_param");
|
|
1903
|
+
}
|
|
1904
|
+
return {
|
|
1905
|
+
uploadParam: payload.upload_param ?? "",
|
|
1906
|
+
uploadFullUrl: payload.upload_full_url
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Send a media message (image/voice/file/video) to a user.
|
|
1911
|
+
* The media must already be uploaded to CDN; pass the resulting CDNMedia reference.
|
|
1912
|
+
*/
|
|
1913
|
+
async sendMediaMessage(input) {
|
|
1914
|
+
const item = this.buildMediaItem(input);
|
|
1915
|
+
const response = await this.fetchImpl(`${this.baseUrl}/ilink/bot/sendmessage`, {
|
|
1916
|
+
method: "POST",
|
|
1917
|
+
headers: {
|
|
1918
|
+
"content-type": "application/json",
|
|
1919
|
+
AuthorizationType: "ilink_bot_token",
|
|
1920
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
1921
|
+
"X-WECHAT-UIN": this.wechatUin
|
|
1922
|
+
},
|
|
1923
|
+
body: JSON.stringify({
|
|
1924
|
+
msg: {
|
|
1925
|
+
from_user_id: "",
|
|
1926
|
+
to_user_id: input.toUserId,
|
|
1927
|
+
client_id: randomUUID(),
|
|
1928
|
+
message_type: 2,
|
|
1929
|
+
message_state: 2,
|
|
1930
|
+
item_list: [item],
|
|
1931
|
+
context_token: input.contextToken
|
|
1932
|
+
},
|
|
1933
|
+
base_info: { channel_version: this.channelVersion }
|
|
1934
|
+
})
|
|
1935
|
+
});
|
|
1936
|
+
if (!response.ok) {
|
|
1937
|
+
throw new Error(`weixin_send_media_failed:${response.status}`);
|
|
1938
|
+
}
|
|
1939
|
+
const payload = await response.json();
|
|
1940
|
+
if ((payload.errcode ?? 0) !== 0 || (payload.ret ?? 0) !== 0) {
|
|
1941
|
+
throw new Error(`weixin_send_media_failed:${payload.errcode ?? payload.ret ?? -1}:${payload.errmsg ?? "unknown_error"}`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
buildMediaItem(input) {
|
|
1945
|
+
const aesKeyBase64 = Buffer.from(input.aesKeyHex).toString("base64");
|
|
1946
|
+
const cdnMedia = {
|
|
1947
|
+
encrypt_query_param: input.downloadParam,
|
|
1948
|
+
aes_key: aesKeyBase64,
|
|
1949
|
+
encrypt_type: 1
|
|
1950
|
+
};
|
|
1951
|
+
switch (input.itemType) {
|
|
1952
|
+
case 2:
|
|
1953
|
+
return { type: 2, image_item: { media: cdnMedia, mid_size: input.ciphertextSize } };
|
|
1954
|
+
case 3:
|
|
1955
|
+
return { type: 3, voice_item: {
|
|
1956
|
+
media: cdnMedia,
|
|
1957
|
+
...input.voiceInfo ? {
|
|
1958
|
+
encode_type: input.voiceInfo.encodeType,
|
|
1959
|
+
playtime: input.voiceInfo.playtime,
|
|
1960
|
+
...input.voiceInfo.sampleRate ? { sample_rate: input.voiceInfo.sampleRate } : {}
|
|
1961
|
+
} : {}
|
|
1962
|
+
} };
|
|
1963
|
+
case 4:
|
|
1964
|
+
return { type: 4, file_item: { media: cdnMedia, file_name: input.fileName ?? "file", len: String(input.rawSize) } };
|
|
1965
|
+
case 5:
|
|
1966
|
+
return { type: 5, video_item: { media: cdnMedia, video_size: input.ciphertextSize } };
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1869
1969
|
};
|
|
1870
1970
|
|
|
1871
1971
|
// src/channels/weixin-direct/adapter.ts
|
|
@@ -2021,18 +2121,49 @@ var WeixinDirectAdapter = class _WeixinDirectAdapter {
|
|
|
2021
2121
|
if (this.options.stateStore && !this.options.stateStore.canSend(message.chatId)) {
|
|
2022
2122
|
throw new Error(`weixin_push_quota_exceeded:${message.chatId}`);
|
|
2023
2123
|
}
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2124
|
+
const mediaKinds = /* @__PURE__ */ new Set(["image", "video", "audio", "file"]);
|
|
2125
|
+
if (mediaKinds.has(message.kind)) {
|
|
2126
|
+
await this.sendMediaMessage(message, contextToken);
|
|
2127
|
+
} else {
|
|
2128
|
+
const chunks = chunkText(message.text ?? "", MAX_TEXT_LENGTH);
|
|
2129
|
+
const chunkDelayMs = this.options.chunkDelayMs ?? DEFAULT_CHUNK_DELAY_MS;
|
|
2130
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
2131
|
+
if (i > 0 && chunkDelayMs > 0) await delay2(chunkDelayMs);
|
|
2132
|
+
await this.options.api.sendTextMessage({
|
|
2133
|
+
toUserId: message.chatId,
|
|
2134
|
+
text: chunks[i],
|
|
2135
|
+
contextToken
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2033
2138
|
}
|
|
2034
2139
|
this.options.stateStore?.recordSent(message.chatId);
|
|
2035
2140
|
}
|
|
2141
|
+
async sendMediaMessage(message, contextToken) {
|
|
2142
|
+
const uploader = this.options.mediaUploader;
|
|
2143
|
+
const api = this.options.api;
|
|
2144
|
+
if (!uploader || !api.sendMediaMessage) {
|
|
2145
|
+
throw new Error("weixin_media_upload_unavailable");
|
|
2146
|
+
}
|
|
2147
|
+
if (!message.filePath) {
|
|
2148
|
+
throw new Error("weixin_media_no_file_path");
|
|
2149
|
+
}
|
|
2150
|
+
const result = await uploader.upload(message.filePath, { contextToken, mediaType: mediaKindToMediaType(message.kind), toUserId: message.chatId });
|
|
2151
|
+
if (!result.ok) {
|
|
2152
|
+
throw new Error(`weixin_media_upload_failed:${result.reason}`);
|
|
2153
|
+
}
|
|
2154
|
+
const itemType = mediaKindToItemType(message.kind);
|
|
2155
|
+
await api.sendMediaMessage({
|
|
2156
|
+
toUserId: message.chatId,
|
|
2157
|
+
contextToken,
|
|
2158
|
+
itemType,
|
|
2159
|
+
downloadParam: result.downloadParam,
|
|
2160
|
+
aesKeyHex: result.aesKeyHex,
|
|
2161
|
+
rawSize: result.rawSize,
|
|
2162
|
+
ciphertextSize: result.ciphertextSize,
|
|
2163
|
+
fileName: message.fileName,
|
|
2164
|
+
...result.voiceInfo ? { voiceInfo: result.voiceInfo } : {}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2036
2167
|
async setTyping(chatId, active) {
|
|
2037
2168
|
await this.typing?.set(chatId, active);
|
|
2038
2169
|
}
|
|
@@ -2193,13 +2324,41 @@ function chunkText(text, limit) {
|
|
|
2193
2324
|
}
|
|
2194
2325
|
return chunks.length > 0 ? chunks : [""];
|
|
2195
2326
|
}
|
|
2327
|
+
function mediaKindToItemType(kind) {
|
|
2328
|
+
switch (kind) {
|
|
2329
|
+
case "image":
|
|
2330
|
+
return 2;
|
|
2331
|
+
case "audio":
|
|
2332
|
+
return 3;
|
|
2333
|
+
case "file":
|
|
2334
|
+
return 4;
|
|
2335
|
+
case "video":
|
|
2336
|
+
return 5;
|
|
2337
|
+
default:
|
|
2338
|
+
throw new Error(`weixin_unknown_media_kind:${kind}`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
function mediaKindToMediaType(kind) {
|
|
2342
|
+
switch (kind) {
|
|
2343
|
+
case "image":
|
|
2344
|
+
return 1;
|
|
2345
|
+
case "video":
|
|
2346
|
+
return 2;
|
|
2347
|
+
case "file":
|
|
2348
|
+
return 3;
|
|
2349
|
+
case "audio":
|
|
2350
|
+
return 4;
|
|
2351
|
+
default:
|
|
2352
|
+
return 3;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2196
2355
|
|
|
2197
2356
|
// src/channels/weixin-direct/mediaDownloader.ts
|
|
2198
2357
|
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
2199
2358
|
import { dirname as dirname5 } from "node:path";
|
|
2200
2359
|
|
|
2201
2360
|
// src/channels/weixin-direct/mediaCrypto.ts
|
|
2202
|
-
import { createDecipheriv } from "node:crypto";
|
|
2361
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes3 } from "node:crypto";
|
|
2203
2362
|
function decodeAesKey(encoded) {
|
|
2204
2363
|
if (/^[0-9a-fA-F]{32}$/.test(encoded)) {
|
|
2205
2364
|
return Buffer.from(encoded, "hex");
|
|
@@ -2223,6 +2382,19 @@ function decryptAesEcb(ciphertext, key) {
|
|
|
2223
2382
|
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
2224
2383
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
2225
2384
|
}
|
|
2385
|
+
function encryptAesEcb(plaintext, key) {
|
|
2386
|
+
if (key.length !== 16) {
|
|
2387
|
+
throw new Error(`weixin_media_aes_key_size:${key.length}`);
|
|
2388
|
+
}
|
|
2389
|
+
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
2390
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
2391
|
+
}
|
|
2392
|
+
function generateAesKey() {
|
|
2393
|
+
return randomBytes3(16);
|
|
2394
|
+
}
|
|
2395
|
+
function encodeAesKeyHex(key) {
|
|
2396
|
+
return key.toString("hex");
|
|
2397
|
+
}
|
|
2226
2398
|
|
|
2227
2399
|
// src/channels/weixin-direct/mediaDownloader.ts
|
|
2228
2400
|
var CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
@@ -2265,6 +2437,143 @@ var WeixinMediaDownloader = class {
|
|
|
2265
2437
|
}
|
|
2266
2438
|
};
|
|
2267
2439
|
|
|
2440
|
+
// src/channels/weixin-direct/mediaUploader.ts
|
|
2441
|
+
import { createHash, randomBytes as randomBytes4 } from "node:crypto";
|
|
2442
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2443
|
+
|
|
2444
|
+
// src/channels/weixin-direct/voiceInfo.ts
|
|
2445
|
+
function encodeTypeFromExt(ext) {
|
|
2446
|
+
switch (ext.toLowerCase()) {
|
|
2447
|
+
case ".mp3":
|
|
2448
|
+
return 7;
|
|
2449
|
+
case ".amr":
|
|
2450
|
+
return 5;
|
|
2451
|
+
case ".silk":
|
|
2452
|
+
case ".slk":
|
|
2453
|
+
return 6;
|
|
2454
|
+
case ".wav":
|
|
2455
|
+
case ".pcm":
|
|
2456
|
+
return 1;
|
|
2457
|
+
case ".ogg":
|
|
2458
|
+
case ".opus":
|
|
2459
|
+
return 8;
|
|
2460
|
+
case ".spx":
|
|
2461
|
+
return 4;
|
|
2462
|
+
default:
|
|
2463
|
+
return 7;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
function estimatePlaytime(buf, ext) {
|
|
2467
|
+
switch (ext.toLowerCase()) {
|
|
2468
|
+
case ".wav": {
|
|
2469
|
+
if (buf.length < 44) return 1e3;
|
|
2470
|
+
const byteRate = buf.readUInt32LE(28);
|
|
2471
|
+
if (byteRate === 0) return 1e3;
|
|
2472
|
+
const dataSize = buf.length - 44;
|
|
2473
|
+
return Math.round(dataSize / byteRate * 1e3);
|
|
2474
|
+
}
|
|
2475
|
+
case ".mp3": {
|
|
2476
|
+
return Math.round(buf.length * 8 / 128e3 * 1e3);
|
|
2477
|
+
}
|
|
2478
|
+
case ".amr": {
|
|
2479
|
+
const frames = Math.floor((buf.length - 6) / 32);
|
|
2480
|
+
return frames * 20;
|
|
2481
|
+
}
|
|
2482
|
+
default:
|
|
2483
|
+
return Math.round(buf.length * 8 / 16e3 * 1e3);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
function detectVoiceInfo(buf, fileName) {
|
|
2487
|
+
const ext = fileName.includes(".") ? "." + fileName.split(".").pop() : ".mp3";
|
|
2488
|
+
return {
|
|
2489
|
+
encodeType: encodeTypeFromExt(ext),
|
|
2490
|
+
playtime: estimatePlaytime(buf, ext),
|
|
2491
|
+
sampleRate: ext === ".wav" && buf.length >= 28 ? buf.readUInt32LE(24) : void 0
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// src/channels/weixin-direct/mediaUploader.ts
|
|
2496
|
+
var CDN_UPLOAD_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
2497
|
+
var WeixinMediaUploader = class {
|
|
2498
|
+
cdnBaseUrl;
|
|
2499
|
+
fetchImpl;
|
|
2500
|
+
getUploadUrl;
|
|
2501
|
+
constructor(options) {
|
|
2502
|
+
this.cdnBaseUrl = (options.cdnBaseUrl ?? CDN_UPLOAD_BASE_URL).replace(/\/+$/, "");
|
|
2503
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
2504
|
+
this.getUploadUrl = options.getUploadUrl;
|
|
2505
|
+
}
|
|
2506
|
+
async upload(filePath, options) {
|
|
2507
|
+
let plaintext;
|
|
2508
|
+
try {
|
|
2509
|
+
plaintext = readFileSync4(filePath);
|
|
2510
|
+
} catch {
|
|
2511
|
+
return { ok: false, reason: "read_failed" };
|
|
2512
|
+
}
|
|
2513
|
+
const rawsize = plaintext.length;
|
|
2514
|
+
const rawfilemd5 = createHash("md5").update(plaintext).digest("hex");
|
|
2515
|
+
const key = generateAesKey();
|
|
2516
|
+
const aeskeyHex = encodeAesKeyHex(key);
|
|
2517
|
+
let ciphertext;
|
|
2518
|
+
try {
|
|
2519
|
+
ciphertext = encryptAesEcb(plaintext, key);
|
|
2520
|
+
} catch {
|
|
2521
|
+
return { ok: false, reason: "encrypt_failed" };
|
|
2522
|
+
}
|
|
2523
|
+
const filesize = ciphertext.length;
|
|
2524
|
+
const filekey = randomBytes4(16).toString("hex");
|
|
2525
|
+
let uploadParam;
|
|
2526
|
+
let uploadFullUrl;
|
|
2527
|
+
try {
|
|
2528
|
+
const result = await this.getUploadUrl({
|
|
2529
|
+
filekey,
|
|
2530
|
+
rawsize,
|
|
2531
|
+
rawfilemd5,
|
|
2532
|
+
filesize,
|
|
2533
|
+
aeskey: aeskeyHex,
|
|
2534
|
+
noNeedThumb: true,
|
|
2535
|
+
...options?.contextToken ? { contextToken: options.contextToken } : {},
|
|
2536
|
+
...options?.mediaType != null ? { mediaType: options.mediaType } : {},
|
|
2537
|
+
...options?.toUserId ? { toUserId: options.toUserId } : {}
|
|
2538
|
+
});
|
|
2539
|
+
uploadParam = result.uploadParam;
|
|
2540
|
+
uploadFullUrl = result.uploadFullUrl;
|
|
2541
|
+
} catch (e) {
|
|
2542
|
+
const msg = e instanceof Error ? e.message : "";
|
|
2543
|
+
return { ok: false, reason: msg || "get_upload_url_failed" };
|
|
2544
|
+
}
|
|
2545
|
+
const uploadUrl = uploadFullUrl || `${this.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
2546
|
+
let response;
|
|
2547
|
+
try {
|
|
2548
|
+
response = await this.fetchImpl(uploadUrl, {
|
|
2549
|
+
method: "POST",
|
|
2550
|
+
headers: { "content-type": "application/octet-stream" },
|
|
2551
|
+
body: new Uint8Array(ciphertext),
|
|
2552
|
+
signal: AbortSignal.timeout(12e4)
|
|
2553
|
+
});
|
|
2554
|
+
} catch {
|
|
2555
|
+
return { ok: false, reason: "upload_failed" };
|
|
2556
|
+
}
|
|
2557
|
+
if (!response.ok) {
|
|
2558
|
+
return { ok: false, reason: `upload_http_${response.status}` };
|
|
2559
|
+
}
|
|
2560
|
+
const encryptedParam = response.headers.get("x-encrypted-param") ?? "";
|
|
2561
|
+
if (!encryptedParam) {
|
|
2562
|
+
return { ok: false, reason: "no_encrypted_param_in_response" };
|
|
2563
|
+
}
|
|
2564
|
+
const isAudio = options?.mediaType === 4;
|
|
2565
|
+
const voiceInfo = isAudio ? detectVoiceInfo(plaintext, filePath) : void 0;
|
|
2566
|
+
return {
|
|
2567
|
+
ok: true,
|
|
2568
|
+
downloadParam: encryptedParam,
|
|
2569
|
+
aesKeyHex: aeskeyHex,
|
|
2570
|
+
rawSize: rawsize,
|
|
2571
|
+
ciphertextSize: filesize,
|
|
2572
|
+
...voiceInfo ? { voiceInfo } : {}
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
|
|
2268
2577
|
// src/channels/weixin-direct/managedAdapter.ts
|
|
2269
2578
|
var ManagedWeixinDirectAdapter = class {
|
|
2270
2579
|
id = "weixin-managed";
|
|
@@ -2342,13 +2651,18 @@ function createWeixinAdapter(config, stateStore, mediaDir) {
|
|
|
2342
2651
|
if (config?.enabled !== true) return null;
|
|
2343
2652
|
if (!config.baseUrl || !config.token) return null;
|
|
2344
2653
|
const wechatUin = buildTransientWeixinUin();
|
|
2654
|
+
const apiClient = new WeixinDirectApiClient({
|
|
2655
|
+
baseUrl: config.baseUrl,
|
|
2656
|
+
botToken: config.token,
|
|
2657
|
+
wechatUin
|
|
2658
|
+
});
|
|
2659
|
+
const mediaUploader = new WeixinMediaUploader({
|
|
2660
|
+
getUploadUrl: (input) => apiClient.getUploadUrl(input)
|
|
2661
|
+
});
|
|
2345
2662
|
return new WeixinDirectAdapter({
|
|
2346
|
-
api:
|
|
2347
|
-
baseUrl: config.baseUrl,
|
|
2348
|
-
botToken: config.token,
|
|
2349
|
-
wechatUin
|
|
2350
|
-
}),
|
|
2663
|
+
api: apiClient,
|
|
2351
2664
|
stateStore,
|
|
2665
|
+
mediaUploader,
|
|
2352
2666
|
...mediaDir ? { mediaDownloader: new WeixinMediaDownloader(), mediaDir } : {}
|
|
2353
2667
|
});
|
|
2354
2668
|
}
|
|
@@ -2387,7 +2701,7 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
|
|
|
2387
2701
|
const buffered = this.pendingLast.get(chatId);
|
|
2388
2702
|
if (buffered) {
|
|
2389
2703
|
this.pendingLast.delete(chatId);
|
|
2390
|
-
await this.options.send(chatId,
|
|
2704
|
+
await this.options.send(chatId, appendHintIfText(buffered));
|
|
2391
2705
|
this.hintedReplies.set(chatId, Date.now());
|
|
2392
2706
|
store.enqueueOutbound(chatId, message);
|
|
2393
2707
|
return;
|
|
@@ -2428,7 +2742,7 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
|
|
|
2428
2742
|
const isLastSlot = quota.remaining === 1;
|
|
2429
2743
|
const hasMore = pending.length > 1;
|
|
2430
2744
|
if (isLastSlot && hasMore) {
|
|
2431
|
-
await this.options.send(chatId,
|
|
2745
|
+
await this.options.send(chatId, appendHintIfText(next));
|
|
2432
2746
|
this.hintedReplies.set(chatId, Date.now());
|
|
2433
2747
|
store.shiftOutbound(chatId);
|
|
2434
2748
|
break;
|
|
@@ -2439,6 +2753,11 @@ var WeixinOutboundGate = class _WeixinOutboundGate {
|
|
|
2439
2753
|
if (!store.hasPendingOutbound(chatId)) this.hintedReplies.delete(chatId);
|
|
2440
2754
|
}
|
|
2441
2755
|
};
|
|
2756
|
+
var MEDIA_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "file"]);
|
|
2757
|
+
function appendHintIfText(item) {
|
|
2758
|
+
if (MEDIA_KINDS.has(item.kind)) return item;
|
|
2759
|
+
return { ...item, text: item.text + CONTINUATION_HINT };
|
|
2760
|
+
}
|
|
2442
2761
|
|
|
2443
2762
|
// src/providers/claude-code/claudeStreamingRunner.ts
|
|
2444
2763
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -2463,6 +2782,10 @@ function executableExtensions() {
|
|
|
2463
2782
|
const pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
2464
2783
|
return ["", ...pathext.split(";").filter(Boolean)];
|
|
2465
2784
|
}
|
|
2785
|
+
function isTemporaryShim(candidate) {
|
|
2786
|
+
const tempDir = tmpdir();
|
|
2787
|
+
return candidate.startsWith(tempDir);
|
|
2788
|
+
}
|
|
2466
2789
|
async function findExecutable(command) {
|
|
2467
2790
|
const extensions = executableExtensions();
|
|
2468
2791
|
if (command.includes("/") || command.includes("\\")) {
|
|
@@ -2472,13 +2795,20 @@ async function findExecutable(command) {
|
|
|
2472
2795
|
return void 0;
|
|
2473
2796
|
}
|
|
2474
2797
|
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
2798
|
+
let fallback;
|
|
2475
2799
|
for (const dir of dirs) {
|
|
2476
2800
|
for (const ext of extensions) {
|
|
2477
2801
|
const candidate = join9(dir, command + ext);
|
|
2478
|
-
if (await isExecutableFile(candidate))
|
|
2802
|
+
if (await isExecutableFile(candidate)) {
|
|
2803
|
+
if (isTemporaryShim(candidate)) {
|
|
2804
|
+
fallback ??= candidate;
|
|
2805
|
+
continue;
|
|
2806
|
+
}
|
|
2807
|
+
return candidate;
|
|
2808
|
+
}
|
|
2479
2809
|
}
|
|
2480
2810
|
}
|
|
2481
|
-
return
|
|
2811
|
+
return fallback;
|
|
2482
2812
|
}
|
|
2483
2813
|
function terminateChild(child, signal = "SIGTERM") {
|
|
2484
2814
|
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) {
|
|
@@ -2625,10 +2955,12 @@ var ClaudeStreamingRunner = class {
|
|
|
2625
2955
|
command;
|
|
2626
2956
|
spawner;
|
|
2627
2957
|
capabilityProbe;
|
|
2958
|
+
mcpConfigPath;
|
|
2628
2959
|
constructor(input = {}) {
|
|
2629
2960
|
this.command = input.command ?? "claude";
|
|
2630
2961
|
this.spawner = input.spawner ?? defaultClaudeStreamSpawner;
|
|
2631
2962
|
this.capabilityProbe = input.capabilityProbe ?? probeAppendSystemPromptSupport;
|
|
2963
|
+
this.mcpConfigPath = input.mcpConfigPath;
|
|
2632
2964
|
}
|
|
2633
2965
|
async startSession(input) {
|
|
2634
2966
|
const resumeId = input.options?.providerSessionId;
|
|
@@ -2738,12 +3070,12 @@ var ClaudeStreamingRunner = class {
|
|
|
2738
3070
|
}
|
|
2739
3071
|
ensureHandle(session) {
|
|
2740
3072
|
if (session.handle) return session.handle;
|
|
2741
|
-
const args = buildStreamingArgs(session);
|
|
3073
|
+
const args = buildStreamingArgs(session, this.mcpConfigPath);
|
|
2742
3074
|
session.handle = this.spawner({ command: this.command, args, cwd: session.cwd });
|
|
2743
3075
|
return session.handle;
|
|
2744
3076
|
}
|
|
2745
3077
|
};
|
|
2746
|
-
function buildStreamingArgs(session) {
|
|
3078
|
+
function buildStreamingArgs(session, mcpConfigPath) {
|
|
2747
3079
|
const args = [
|
|
2748
3080
|
"-p",
|
|
2749
3081
|
"--input-format",
|
|
@@ -2754,7 +3086,8 @@ function buildStreamingArgs(session) {
|
|
|
2754
3086
|
"--verbose",
|
|
2755
3087
|
"--dangerously-skip-permissions",
|
|
2756
3088
|
...session.supportsAppendSystemPrompt ? ["--append-system-prompt", BRIDGE_APPEND_SYSTEM_PROMPT] : [],
|
|
2757
|
-
"--replay-user-messages"
|
|
3089
|
+
"--replay-user-messages",
|
|
3090
|
+
...mcpConfigPath ? ["--mcp-config", mcpConfigPath] : []
|
|
2758
3091
|
];
|
|
2759
3092
|
if (session.resumeId) {
|
|
2760
3093
|
args.push("--resume", session.resumeId);
|
|
@@ -3299,7 +3632,7 @@ var CodexProvider = class {
|
|
|
3299
3632
|
// src/providers/defaultProviders.ts
|
|
3300
3633
|
function createDefaultProviders(input = {}) {
|
|
3301
3634
|
return [
|
|
3302
|
-
new ClaudeCodeProvider({ runner: new ClaudeStreamingRunner({ command: input.claudeCommand }) }),
|
|
3635
|
+
new ClaudeCodeProvider({ runner: new ClaudeStreamingRunner({ command: input.claudeCommand, mcpConfigPath: input.mcpConfigPath }) }),
|
|
3303
3636
|
new CodexProvider({ runner: new CodexInteractiveRunner({ command: input.codexCommand }) })
|
|
3304
3637
|
];
|
|
3305
3638
|
}
|
|
@@ -3491,7 +3824,7 @@ function buildBridgeCommandHelpMarkdown(input) {
|
|
|
3491
3824
|
}
|
|
3492
3825
|
|
|
3493
3826
|
// src/session/currentConversationStore.ts
|
|
3494
|
-
import { existsSync as existsSync6, readFileSync as
|
|
3827
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
3495
3828
|
import { nanoid as nanoid2 } from "nanoid";
|
|
3496
3829
|
var CurrentConversationStore = class {
|
|
3497
3830
|
constructor(configPath, defaults) {
|
|
@@ -3551,7 +3884,7 @@ var CurrentConversationStore = class {
|
|
|
3551
3884
|
}
|
|
3552
3885
|
readState() {
|
|
3553
3886
|
if (!existsSync6(this.configPath)) return {};
|
|
3554
|
-
const raw = JSON.parse(
|
|
3887
|
+
const raw = JSON.parse(readFileSync5(this.configPath, "utf8"));
|
|
3555
3888
|
return raw && typeof raw === "object" ? raw : {};
|
|
3556
3889
|
}
|
|
3557
3890
|
writeCurrent(record) {
|
|
@@ -3622,7 +3955,12 @@ var MessageRouter = class _MessageRouter {
|
|
|
3622
3955
|
*/
|
|
3623
3956
|
async sendToChat(message) {
|
|
3624
3957
|
if (this.options.outboundGate) {
|
|
3625
|
-
await this.options.outboundGate.deliver(message.chatId, {
|
|
3958
|
+
await this.options.outboundGate.deliver(message.chatId, {
|
|
3959
|
+
kind: message.kind,
|
|
3960
|
+
text: message.text,
|
|
3961
|
+
...message.filePath ? { filePath: message.filePath } : {},
|
|
3962
|
+
...message.fileName ? { fileName: message.fileName } : {}
|
|
3963
|
+
});
|
|
3626
3964
|
} else {
|
|
3627
3965
|
await this.options.channel.sendMessage(message);
|
|
3628
3966
|
}
|
|
@@ -4110,7 +4448,7 @@ function composeInboundText(content) {
|
|
|
4110
4448
|
}
|
|
4111
4449
|
|
|
4112
4450
|
// src/storage/lastProviderSessionStore.ts
|
|
4113
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4451
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4114
4452
|
var LastProviderSessionStore = class {
|
|
4115
4453
|
constructor(configPath) {
|
|
4116
4454
|
this.configPath = configPath;
|
|
@@ -4137,7 +4475,7 @@ var LastProviderSessionStore = class {
|
|
|
4137
4475
|
}
|
|
4138
4476
|
readState() {
|
|
4139
4477
|
if (!existsSync7(this.configPath)) return {};
|
|
4140
|
-
const raw = JSON.parse(
|
|
4478
|
+
const raw = JSON.parse(readFileSync6(this.configPath, "utf8"));
|
|
4141
4479
|
return raw && typeof raw === "object" ? raw : {};
|
|
4142
4480
|
}
|
|
4143
4481
|
writeState(state) {
|
|
@@ -4147,7 +4485,7 @@ var LastProviderSessionStore = class {
|
|
|
4147
4485
|
};
|
|
4148
4486
|
|
|
4149
4487
|
// src/storage/runtimeUserStore.ts
|
|
4150
|
-
import { readFileSync as
|
|
4488
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "node:fs";
|
|
4151
4489
|
import { nanoid as nanoid3 } from "nanoid";
|
|
4152
4490
|
var RuntimeUserStore = class {
|
|
4153
4491
|
constructor(configPath) {
|
|
@@ -4208,7 +4546,7 @@ var RuntimeUserStore = class {
|
|
|
4208
4546
|
}
|
|
4209
4547
|
readState() {
|
|
4210
4548
|
if (!existsSync8(this.configPath)) return {};
|
|
4211
|
-
const raw = JSON.parse(
|
|
4549
|
+
const raw = JSON.parse(readFileSync7(this.configPath, "utf8"));
|
|
4212
4550
|
return raw && typeof raw === "object" ? raw : {};
|
|
4213
4551
|
}
|
|
4214
4552
|
writeState(state) {
|
|
@@ -4540,13 +4878,13 @@ var RelayTunnelRouter = class {
|
|
|
4540
4878
|
};
|
|
4541
4879
|
|
|
4542
4880
|
// src/shared/version.ts
|
|
4543
|
-
import { readFileSync as
|
|
4881
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4544
4882
|
import { dirname as dirname6, join as join10 } from "node:path";
|
|
4545
4883
|
import { fileURLToPath } from "node:url";
|
|
4546
4884
|
function readClientVersion() {
|
|
4547
4885
|
try {
|
|
4548
4886
|
const here2 = dirname6(fileURLToPath(import.meta.url));
|
|
4549
|
-
const pkg = JSON.parse(
|
|
4887
|
+
const pkg = JSON.parse(readFileSync8(join10(here2, "..", "..", "package.json"), "utf8"));
|
|
4550
4888
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
4551
4889
|
} catch {
|
|
4552
4890
|
return "0.0.0";
|
|
@@ -4601,9 +4939,27 @@ function createDaemonServer(options = {}) {
|
|
|
4601
4939
|
const activeUserStore = options.activeUserStore ?? new RuntimeUserStore(configPath);
|
|
4602
4940
|
const lastProviderSessions = new LastProviderSessionStore(configPath);
|
|
4603
4941
|
const sessionBindingMatch = /* @__PURE__ */ new Map();
|
|
4942
|
+
const bridgePort = process.env.BRIDGE_PORT ?? "8787";
|
|
4943
|
+
const mcpConfigPath = join11(dirname7(configPath), "mcp-media.json");
|
|
4944
|
+
const serverDir = dirname7(fileURLToPath2(import.meta.url));
|
|
4945
|
+
const mcpMediaServerTs = join11(serverDir, "..", "mcp", "mediaServer.ts");
|
|
4946
|
+
const mcpMediaServerJs = join11(serverDir, "..", "mcp", "mediaServer.js");
|
|
4947
|
+
const useTs = existsSync9(mcpMediaServerTs) && !existsSync9(mcpMediaServerJs);
|
|
4948
|
+
const mcpCommand = useTs ? "tsx" : "node";
|
|
4949
|
+
const mcpArgs = [useTs ? mcpMediaServerTs : mcpMediaServerJs];
|
|
4950
|
+
writeFileSync2(mcpConfigPath, JSON.stringify({
|
|
4951
|
+
mcpServers: {
|
|
4952
|
+
"wechat-media": {
|
|
4953
|
+
command: mcpCommand,
|
|
4954
|
+
args: mcpArgs,
|
|
4955
|
+
env: { BRIDGE_API_URL: `http://localhost:${bridgePort}` }
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
}, null, 2) + "\n");
|
|
4604
4959
|
const providerAdapters = options.providers ?? createDefaultProviders({
|
|
4605
4960
|
claudeCommand: options.providerCommands?.claude?.command,
|
|
4606
|
-
codexCommand: options.providerCommands?.codex?.command
|
|
4961
|
+
codexCommand: options.providerCommands?.codex?.command,
|
|
4962
|
+
mcpConfigPath
|
|
4607
4963
|
});
|
|
4608
4964
|
let currentConversation = conversation.getCurrent();
|
|
4609
4965
|
if (currentConversation) {
|
|
@@ -4628,7 +4984,13 @@ function createDaemonServer(options = {}) {
|
|
|
4628
4984
|
const weixinOutboundGate = managedWechatChannel && weixinStateStore ? new WeixinOutboundGate({
|
|
4629
4985
|
store: weixinStateStore,
|
|
4630
4986
|
send: async (chatId, msg) => {
|
|
4631
|
-
await managedWechatChannel.sendMessage({
|
|
4987
|
+
await managedWechatChannel.sendMessage({
|
|
4988
|
+
chatId,
|
|
4989
|
+
kind: msg.kind,
|
|
4990
|
+
text: msg.text,
|
|
4991
|
+
...msg.filePath ? { filePath: msg.filePath } : {},
|
|
4992
|
+
...msg.fileName ? { fileName: msg.fileName } : {}
|
|
4993
|
+
});
|
|
4632
4994
|
}
|
|
4633
4995
|
}) : void 0;
|
|
4634
4996
|
const tunnelProvider = options.tunnelProvider ?? new RelayTunnelRouter({
|
|
@@ -4774,6 +5136,36 @@ function createDaemonServer(options = {}) {
|
|
|
4774
5136
|
request.raw.on("error", cleanup);
|
|
4775
5137
|
return reply.hijack();
|
|
4776
5138
|
});
|
|
5139
|
+
app.post("/api/channel/send-media", async (request, reply) => {
|
|
5140
|
+
const { kind, filePath, fileName, chatId } = request.body ?? {};
|
|
5141
|
+
if (!filePath || typeof filePath !== "string") {
|
|
5142
|
+
return reply.code(400).send({ ok: false, error: "filePath is required" });
|
|
5143
|
+
}
|
|
5144
|
+
const validKinds = ["image", "video", "audio", "file"];
|
|
5145
|
+
if (!kind || !validKinds.includes(kind)) {
|
|
5146
|
+
return reply.code(400).send({ ok: false, error: `kind must be one of: ${validKinds.join(", ")}` });
|
|
5147
|
+
}
|
|
5148
|
+
if (!channel) {
|
|
5149
|
+
return reply.code(503).send({ ok: false, error: "channel not available" });
|
|
5150
|
+
}
|
|
5151
|
+
const targetChatId = chatId || activeUserStore.getActiveUser()?.platformUserId;
|
|
5152
|
+
if (!targetChatId) {
|
|
5153
|
+
return reply.code(400).send({ ok: false, error: "no active chat target; provide chatId or ensure an active user exists" });
|
|
5154
|
+
}
|
|
5155
|
+
try {
|
|
5156
|
+
await channel.sendMessage({
|
|
5157
|
+
chatId: targetChatId,
|
|
5158
|
+
kind,
|
|
5159
|
+
text: "",
|
|
5160
|
+
filePath,
|
|
5161
|
+
...fileName ? { fileName } : {}
|
|
5162
|
+
});
|
|
5163
|
+
return { ok: true };
|
|
5164
|
+
} catch (e) {
|
|
5165
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
5166
|
+
return reply.code(500).send({ ok: false, error: msg });
|
|
5167
|
+
}
|
|
5168
|
+
});
|
|
4777
5169
|
app.addHook("onReady", async () => {
|
|
4778
5170
|
await channel?.start({ background: true });
|
|
4779
5171
|
});
|
|
@@ -4913,7 +5305,7 @@ async function resolveProviderCommands(providers) {
|
|
|
4913
5305
|
}
|
|
4914
5306
|
|
|
4915
5307
|
// src/daemon/staticFrontend.ts
|
|
4916
|
-
import { readFileSync as
|
|
5308
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4917
5309
|
import { join as join12 } from "node:path";
|
|
4918
5310
|
import fastifyStatic from "@fastify/static";
|
|
4919
5311
|
function attachStaticFrontend(webRoot2) {
|
|
@@ -4922,7 +5314,7 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4922
5314
|
root: webRoot2,
|
|
4923
5315
|
wildcard: false
|
|
4924
5316
|
});
|
|
4925
|
-
const indexHtml =
|
|
5317
|
+
const indexHtml = readFileSync9(join12(webRoot2, "index.html"), "utf8");
|
|
4926
5318
|
app.setNotFoundHandler((request, reply) => {
|
|
4927
5319
|
if (request.url.startsWith("/api")) {
|
|
4928
5320
|
reply.code(404).send({ error: "not_found" });
|
|
@@ -4934,7 +5326,7 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4934
5326
|
}
|
|
4935
5327
|
|
|
4936
5328
|
// src/daemon/service.ts
|
|
4937
|
-
import { existsSync as
|
|
5329
|
+
import { existsSync as existsSync10, statSync, openSync as openSync2, closeSync as closeSync2, watchFile } from "node:fs";
|
|
4938
5330
|
import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
4939
5331
|
import { homedir as homedir8 } from "node:os";
|
|
4940
5332
|
import { dirname as dirname8, join as join13 } from "node:path";
|
|
@@ -4988,13 +5380,13 @@ async function startService(context) {
|
|
|
4988
5380
|
async function stopService(context) {
|
|
4989
5381
|
if (process.platform === "darwin") {
|
|
4990
5382
|
const spec = buildLaunchdSpec(context);
|
|
4991
|
-
if (!
|
|
5383
|
+
if (!existsSync10(spec.plistPath)) return await readServiceStatus(context);
|
|
4992
5384
|
await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
|
|
4993
5385
|
return await readServiceStatus(context);
|
|
4994
5386
|
}
|
|
4995
5387
|
if (process.platform === "linux") {
|
|
4996
5388
|
const spec = buildSystemdUserSpec(context);
|
|
4997
|
-
if (!
|
|
5389
|
+
if (!existsSync10(spec.unitPath)) return await readServiceStatus(context);
|
|
4998
5390
|
await runSystemctl(["--user", "stop", spec.unitName]).catch(() => void 0);
|
|
4999
5391
|
return await readServiceStatus(context);
|
|
5000
5392
|
}
|
|
@@ -5012,7 +5404,7 @@ async function stopService(context) {
|
|
|
5012
5404
|
async function uninstallService(context) {
|
|
5013
5405
|
if (process.platform === "darwin") {
|
|
5014
5406
|
const spec = buildLaunchdSpec(context);
|
|
5015
|
-
if (
|
|
5407
|
+
if (existsSync10(spec.plistPath)) {
|
|
5016
5408
|
await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
|
|
5017
5409
|
await rm2(spec.plistPath, { force: true });
|
|
5018
5410
|
}
|
|
@@ -5020,7 +5412,7 @@ async function uninstallService(context) {
|
|
|
5020
5412
|
}
|
|
5021
5413
|
if (process.platform === "linux") {
|
|
5022
5414
|
const spec = buildSystemdUserSpec(context);
|
|
5023
|
-
if (
|
|
5415
|
+
if (existsSync10(spec.unitPath)) {
|
|
5024
5416
|
await runSystemctl(["--user", "disable", "--now", spec.unitName]).catch(() => void 0);
|
|
5025
5417
|
await rm2(spec.unitPath, { force: true });
|
|
5026
5418
|
await runSystemctl(["--user", "daemon-reload"]).catch(() => void 0);
|
|
@@ -5070,7 +5462,7 @@ async function tailServiceLogs(context) {
|
|
|
5070
5462
|
async function readServiceStatus(context) {
|
|
5071
5463
|
if (process.platform === "darwin") {
|
|
5072
5464
|
const spec = buildLaunchdSpec(context);
|
|
5073
|
-
const installed =
|
|
5465
|
+
const installed = existsSync10(spec.plistPath);
|
|
5074
5466
|
const running = installed ? await runLaunchctl(["print", `gui/${process.getuid?.() ?? 0}/${spec.label}`]).then(() => true, () => false) : false;
|
|
5075
5467
|
return {
|
|
5076
5468
|
manager: "launchd",
|
|
@@ -5084,7 +5476,7 @@ async function readServiceStatus(context) {
|
|
|
5084
5476
|
}
|
|
5085
5477
|
if (process.platform === "linux") {
|
|
5086
5478
|
const spec = buildSystemdUserSpec(context);
|
|
5087
|
-
const installed =
|
|
5479
|
+
const installed = existsSync10(spec.unitPath);
|
|
5088
5480
|
const running = installed ? await runSystemctl(["--user", "is-active", "--quiet", spec.unitName]).then(() => true, () => false) : false;
|
|
5089
5481
|
return {
|
|
5090
5482
|
manager: "systemd-user",
|
|
@@ -5269,7 +5661,7 @@ function isProcessAlive(pid) {
|
|
|
5269
5661
|
}
|
|
5270
5662
|
}
|
|
5271
5663
|
async function readPidFile(pidPath) {
|
|
5272
|
-
if (!
|
|
5664
|
+
if (!existsSync10(pidPath)) return null;
|
|
5273
5665
|
try {
|
|
5274
5666
|
const raw = (await readFile6(pidPath, "utf8")).trim();
|
|
5275
5667
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -5320,12 +5712,12 @@ async function spawnDetachedDaemon(spec) {
|
|
|
5320
5712
|
}
|
|
5321
5713
|
}
|
|
5322
5714
|
async function tailFile(path, lines) {
|
|
5323
|
-
if (!
|
|
5715
|
+
if (!existsSync10(path)) return "";
|
|
5324
5716
|
const content = await readFile6(path, "utf8");
|
|
5325
5717
|
return content.split(/\r?\n/).slice(-lines).join("\n").trim();
|
|
5326
5718
|
}
|
|
5327
5719
|
async function tailFiles(paths) {
|
|
5328
|
-
const existing = paths.filter((path) =>
|
|
5720
|
+
const existing = paths.filter((path) => existsSync10(path));
|
|
5329
5721
|
for (const path of existing) {
|
|
5330
5722
|
const tail = await tailFile(path, 200);
|
|
5331
5723
|
if (tail) process.stdout.write(`[${path}]
|
|
@@ -5376,7 +5768,7 @@ function fileSizeHint(path) {
|
|
|
5376
5768
|
}
|
|
5377
5769
|
|
|
5378
5770
|
// src/cli.ts
|
|
5379
|
-
var here = dirname9(
|
|
5771
|
+
var here = dirname9(fileURLToPath3(import.meta.url));
|
|
5380
5772
|
var webRoot = join14(here, "..", "web");
|
|
5381
5773
|
async function main() {
|
|
5382
5774
|
const command = process.argv[2];
|
|
@@ -5426,7 +5818,7 @@ async function main() {
|
|
|
5426
5818
|
}
|
|
5427
5819
|
}
|
|
5428
5820
|
async function cmdStart() {
|
|
5429
|
-
if (!
|
|
5821
|
+
if (!existsSync11(webRoot)) {
|
|
5430
5822
|
console.error(`\u627E\u4E0D\u5230\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269: ${webRoot}`);
|
|
5431
5823
|
console.error("\u8BF7\u91CD\u65B0\u5B89\u88C5\u5B8C\u6574\u7684 npm \u5305\u3002");
|
|
5432
5824
|
process.exitCode = 1;
|
|
@@ -5457,7 +5849,7 @@ async function cmdTail() {
|
|
|
5457
5849
|
await tailServiceLogs(createServiceContext());
|
|
5458
5850
|
}
|
|
5459
5851
|
async function cmdDaemon() {
|
|
5460
|
-
if (!
|
|
5852
|
+
if (!existsSync11(webRoot)) {
|
|
5461
5853
|
console.error(`\u627E\u4E0D\u5230\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269: ${webRoot}`);
|
|
5462
5854
|
process.exitCode = 1;
|
|
5463
5855
|
return;
|
|
@@ -5470,13 +5862,13 @@ async function cmdDaemon() {
|
|
|
5470
5862
|
async function cmdDoctor() {
|
|
5471
5863
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5472
5864
|
console.log("claude-codex-wechat doctor\n");
|
|
5473
|
-
report("\u914D\u7F6E\u6587\u4EF6",
|
|
5474
|
-
report("\u524D\u7AEF\u4EA7\u7269",
|
|
5865
|
+
report("\u914D\u7F6E\u6587\u4EF6", existsSync11(configPath) ? configPath : `\u7F3A\u5931 (${configPath})\uFF0C\u9996\u6B21 start \u65F6\u81EA\u52A8\u521B\u5EFA`);
|
|
5866
|
+
report("\u524D\u7AEF\u4EA7\u7269", existsSync11(webRoot) ? webRoot : `\u7F3A\u5931 (${webRoot})`);
|
|
5475
5867
|
const claude = await findExecutable("claude");
|
|
5476
5868
|
report("claude \u53EF\u6267\u884C", claude ?? "\u672A\u627E\u5230\uFF0C\u9700\u5148\u5B89\u88C5\u5E76\u767B\u5F55 Claude Code");
|
|
5477
5869
|
const codex = await findExecutable("codex");
|
|
5478
5870
|
report("codex \u53EF\u6267\u884C", codex ?? "\u672A\u627E\u5230\uFF08\u4EC5\u4F7F\u7528 Claude \u65F6\u53EF\u5FFD\u7565\uFF09");
|
|
5479
|
-
if (
|
|
5871
|
+
if (existsSync11(configPath)) {
|
|
5480
5872
|
const config = loadBridgeConfig(configPath);
|
|
5481
5873
|
const wechat = config.wechat;
|
|
5482
5874
|
report("\u5FAE\u4FE1\u542F\u7528", wechat?.enabled ? "\u662F" : "\u5426");
|
|
@@ -5487,7 +5879,7 @@ async function cmdDoctor() {
|
|
|
5487
5879
|
}
|
|
5488
5880
|
function printUpdateHint() {
|
|
5489
5881
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5490
|
-
if (!
|
|
5882
|
+
if (!existsSync11(configPath)) return;
|
|
5491
5883
|
let update;
|
|
5492
5884
|
try {
|
|
5493
5885
|
update = loadBridgeConfig(configPath).update;
|
|
@@ -5503,12 +5895,12 @@ function printUpdateHint() {
|
|
|
5503
5895
|
}
|
|
5504
5896
|
function cmdPrintConfig() {
|
|
5505
5897
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5506
|
-
if (!
|
|
5898
|
+
if (!existsSync11(configPath)) {
|
|
5507
5899
|
console.error(`\u914D\u7F6E\u4E0D\u5B58\u5728: ${configPath}\uFF08\u9996\u6B21 start \u540E\u81EA\u52A8\u751F\u6210\uFF09`);
|
|
5508
5900
|
process.exitCode = 1;
|
|
5509
5901
|
return;
|
|
5510
5902
|
}
|
|
5511
|
-
console.log(
|
|
5903
|
+
console.log(readFileSync10(configPath, "utf8"));
|
|
5512
5904
|
}
|
|
5513
5905
|
async function cmdUninstall() {
|
|
5514
5906
|
const status = await uninstallService(createServiceContext());
|
|
@@ -5516,7 +5908,7 @@ async function cmdUninstall() {
|
|
|
5516
5908
|
}
|
|
5517
5909
|
function createServiceContext() {
|
|
5518
5910
|
return {
|
|
5519
|
-
cliEntrypointPath:
|
|
5911
|
+
cliEntrypointPath: fileURLToPath3(import.meta.url),
|
|
5520
5912
|
nodePath: process.execPath,
|
|
5521
5913
|
configPath: process.env.BRIDGE_CONFIG ?? defaultConfigPath(),
|
|
5522
5914
|
port: Number(process.env.BRIDGE_PORT ?? 8787)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-codex-wechat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.5.0",
|
|
@@ -39,10 +39,12 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@fastify/static": "^9.1.3",
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
43
|
"better-sqlite3": "latest",
|
|
43
44
|
"fastify": "latest",
|
|
44
45
|
"lucide-react": "^1.22.0",
|
|
45
|
-
"nanoid": "latest"
|
|
46
|
+
"nanoid": "latest",
|
|
47
|
+
"zod": "^4.4.3"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@fastify/middie": "^9.3.2",
|