claude-codex-wechat 0.1.29 → 0.1.31
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 +454 -60
- 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:8787";
|
|
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";
|
|
@@ -2636,10 +2955,12 @@ var ClaudeStreamingRunner = class {
|
|
|
2636
2955
|
command;
|
|
2637
2956
|
spawner;
|
|
2638
2957
|
capabilityProbe;
|
|
2958
|
+
mcpConfigPath;
|
|
2639
2959
|
constructor(input = {}) {
|
|
2640
2960
|
this.command = input.command ?? "claude";
|
|
2641
2961
|
this.spawner = input.spawner ?? defaultClaudeStreamSpawner;
|
|
2642
2962
|
this.capabilityProbe = input.capabilityProbe ?? probeAppendSystemPromptSupport;
|
|
2963
|
+
this.mcpConfigPath = input.mcpConfigPath;
|
|
2643
2964
|
}
|
|
2644
2965
|
async startSession(input) {
|
|
2645
2966
|
const resumeId = input.options?.providerSessionId;
|
|
@@ -2749,12 +3070,12 @@ var ClaudeStreamingRunner = class {
|
|
|
2749
3070
|
}
|
|
2750
3071
|
ensureHandle(session) {
|
|
2751
3072
|
if (session.handle) return session.handle;
|
|
2752
|
-
const args = buildStreamingArgs(session);
|
|
3073
|
+
const args = buildStreamingArgs(session, this.mcpConfigPath);
|
|
2753
3074
|
session.handle = this.spawner({ command: this.command, args, cwd: session.cwd });
|
|
2754
3075
|
return session.handle;
|
|
2755
3076
|
}
|
|
2756
3077
|
};
|
|
2757
|
-
function buildStreamingArgs(session) {
|
|
3078
|
+
function buildStreamingArgs(session, mcpConfigPath) {
|
|
2758
3079
|
const args = [
|
|
2759
3080
|
"-p",
|
|
2760
3081
|
"--input-format",
|
|
@@ -2765,7 +3086,8 @@ function buildStreamingArgs(session) {
|
|
|
2765
3086
|
"--verbose",
|
|
2766
3087
|
"--dangerously-skip-permissions",
|
|
2767
3088
|
...session.supportsAppendSystemPrompt ? ["--append-system-prompt", BRIDGE_APPEND_SYSTEM_PROMPT] : [],
|
|
2768
|
-
"--replay-user-messages"
|
|
3089
|
+
"--replay-user-messages",
|
|
3090
|
+
...mcpConfigPath ? ["--mcp-config", mcpConfigPath] : []
|
|
2769
3091
|
];
|
|
2770
3092
|
if (session.resumeId) {
|
|
2771
3093
|
args.push("--resume", session.resumeId);
|
|
@@ -2909,7 +3231,7 @@ var CodexAppServerClient = class {
|
|
|
2909
3231
|
buffer = "";
|
|
2910
3232
|
constructor(input) {
|
|
2911
3233
|
const command = input.command ?? "codex";
|
|
2912
|
-
this.child = spawn4(command, ["app-server", "--listen", "stdio://"], {
|
|
3234
|
+
this.child = spawn4(command, ["app-server", "--listen", "stdio://", ...input.mcpServerConfigs ?? []], {
|
|
2913
3235
|
cwd: input.cwd,
|
|
2914
3236
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2915
3237
|
shell: useShellForCli()
|
|
@@ -3080,8 +3402,10 @@ var CodexInteractiveRunner = class {
|
|
|
3080
3402
|
sessions = /* @__PURE__ */ new Map();
|
|
3081
3403
|
command;
|
|
3082
3404
|
syncThreadForResume;
|
|
3405
|
+
mcpServerConfigs;
|
|
3083
3406
|
constructor(input = {}) {
|
|
3084
3407
|
this.command = input.command;
|
|
3408
|
+
this.mcpServerConfigs = input.mcpServerConfigs;
|
|
3085
3409
|
this.syncThreadForResume = input.syncThreadForResume ?? syncCodexThreadForResume;
|
|
3086
3410
|
}
|
|
3087
3411
|
async startSession(input) {
|
|
@@ -3204,7 +3528,8 @@ var CodexInteractiveRunner = class {
|
|
|
3204
3528
|
if (session.client) return session.client;
|
|
3205
3529
|
const client = new CodexAppServerClient({
|
|
3206
3530
|
command: this.command,
|
|
3207
|
-
cwd: session.cwd
|
|
3531
|
+
cwd: session.cwd,
|
|
3532
|
+
...this.mcpServerConfigs?.length ? { mcpServerConfigs: this.mcpServerConfigs } : {}
|
|
3208
3533
|
});
|
|
3209
3534
|
await client.initialize();
|
|
3210
3535
|
client.onNotification("item/agentMessage/delta", (params) => {
|
|
@@ -3310,8 +3635,8 @@ var CodexProvider = class {
|
|
|
3310
3635
|
// src/providers/defaultProviders.ts
|
|
3311
3636
|
function createDefaultProviders(input = {}) {
|
|
3312
3637
|
return [
|
|
3313
|
-
new ClaudeCodeProvider({ runner: new ClaudeStreamingRunner({ command: input.claudeCommand }) }),
|
|
3314
|
-
new CodexProvider({ runner: new CodexInteractiveRunner({ command: input.codexCommand }) })
|
|
3638
|
+
new ClaudeCodeProvider({ runner: new ClaudeStreamingRunner({ command: input.claudeCommand, mcpConfigPath: input.mcpConfigPath }) }),
|
|
3639
|
+
new CodexProvider({ runner: new CodexInteractiveRunner({ command: input.codexCommand, mcpServerConfigs: input.codexMcpServerConfigs }) })
|
|
3315
3640
|
];
|
|
3316
3641
|
}
|
|
3317
3642
|
|
|
@@ -3502,7 +3827,7 @@ function buildBridgeCommandHelpMarkdown(input) {
|
|
|
3502
3827
|
}
|
|
3503
3828
|
|
|
3504
3829
|
// src/session/currentConversationStore.ts
|
|
3505
|
-
import { existsSync as existsSync6, readFileSync as
|
|
3830
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
3506
3831
|
import { nanoid as nanoid2 } from "nanoid";
|
|
3507
3832
|
var CurrentConversationStore = class {
|
|
3508
3833
|
constructor(configPath, defaults) {
|
|
@@ -3562,7 +3887,7 @@ var CurrentConversationStore = class {
|
|
|
3562
3887
|
}
|
|
3563
3888
|
readState() {
|
|
3564
3889
|
if (!existsSync6(this.configPath)) return {};
|
|
3565
|
-
const raw = JSON.parse(
|
|
3890
|
+
const raw = JSON.parse(readFileSync5(this.configPath, "utf8"));
|
|
3566
3891
|
return raw && typeof raw === "object" ? raw : {};
|
|
3567
3892
|
}
|
|
3568
3893
|
writeCurrent(record) {
|
|
@@ -3633,7 +3958,12 @@ var MessageRouter = class _MessageRouter {
|
|
|
3633
3958
|
*/
|
|
3634
3959
|
async sendToChat(message) {
|
|
3635
3960
|
if (this.options.outboundGate) {
|
|
3636
|
-
await this.options.outboundGate.deliver(message.chatId, {
|
|
3961
|
+
await this.options.outboundGate.deliver(message.chatId, {
|
|
3962
|
+
kind: message.kind,
|
|
3963
|
+
text: message.text,
|
|
3964
|
+
...message.filePath ? { filePath: message.filePath } : {},
|
|
3965
|
+
...message.fileName ? { fileName: message.fileName } : {}
|
|
3966
|
+
});
|
|
3637
3967
|
} else {
|
|
3638
3968
|
await this.options.channel.sendMessage(message);
|
|
3639
3969
|
}
|
|
@@ -4121,7 +4451,7 @@ function composeInboundText(content) {
|
|
|
4121
4451
|
}
|
|
4122
4452
|
|
|
4123
4453
|
// src/storage/lastProviderSessionStore.ts
|
|
4124
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4454
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4125
4455
|
var LastProviderSessionStore = class {
|
|
4126
4456
|
constructor(configPath) {
|
|
4127
4457
|
this.configPath = configPath;
|
|
@@ -4148,7 +4478,7 @@ var LastProviderSessionStore = class {
|
|
|
4148
4478
|
}
|
|
4149
4479
|
readState() {
|
|
4150
4480
|
if (!existsSync7(this.configPath)) return {};
|
|
4151
|
-
const raw = JSON.parse(
|
|
4481
|
+
const raw = JSON.parse(readFileSync6(this.configPath, "utf8"));
|
|
4152
4482
|
return raw && typeof raw === "object" ? raw : {};
|
|
4153
4483
|
}
|
|
4154
4484
|
writeState(state) {
|
|
@@ -4158,7 +4488,7 @@ var LastProviderSessionStore = class {
|
|
|
4158
4488
|
};
|
|
4159
4489
|
|
|
4160
4490
|
// src/storage/runtimeUserStore.ts
|
|
4161
|
-
import { readFileSync as
|
|
4491
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "node:fs";
|
|
4162
4492
|
import { nanoid as nanoid3 } from "nanoid";
|
|
4163
4493
|
var RuntimeUserStore = class {
|
|
4164
4494
|
constructor(configPath) {
|
|
@@ -4219,7 +4549,7 @@ var RuntimeUserStore = class {
|
|
|
4219
4549
|
}
|
|
4220
4550
|
readState() {
|
|
4221
4551
|
if (!existsSync8(this.configPath)) return {};
|
|
4222
|
-
const raw = JSON.parse(
|
|
4552
|
+
const raw = JSON.parse(readFileSync7(this.configPath, "utf8"));
|
|
4223
4553
|
return raw && typeof raw === "object" ? raw : {};
|
|
4224
4554
|
}
|
|
4225
4555
|
writeState(state) {
|
|
@@ -4551,13 +4881,13 @@ var RelayTunnelRouter = class {
|
|
|
4551
4881
|
};
|
|
4552
4882
|
|
|
4553
4883
|
// src/shared/version.ts
|
|
4554
|
-
import { readFileSync as
|
|
4884
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4555
4885
|
import { dirname as dirname6, join as join10 } from "node:path";
|
|
4556
4886
|
import { fileURLToPath } from "node:url";
|
|
4557
4887
|
function readClientVersion() {
|
|
4558
4888
|
try {
|
|
4559
4889
|
const here2 = dirname6(fileURLToPath(import.meta.url));
|
|
4560
|
-
const pkg = JSON.parse(
|
|
4890
|
+
const pkg = JSON.parse(readFileSync8(join10(here2, "..", "..", "package.json"), "utf8"));
|
|
4561
4891
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
4562
4892
|
} catch {
|
|
4563
4893
|
return "0.0.0";
|
|
@@ -4612,9 +4942,37 @@ function createDaemonServer(options = {}) {
|
|
|
4612
4942
|
const activeUserStore = options.activeUserStore ?? new RuntimeUserStore(configPath);
|
|
4613
4943
|
const lastProviderSessions = new LastProviderSessionStore(configPath);
|
|
4614
4944
|
const sessionBindingMatch = /* @__PURE__ */ new Map();
|
|
4945
|
+
const bridgePort = process.env.BRIDGE_PORT ?? "8787";
|
|
4946
|
+
const mcpConfigPath = join11(dirname7(configPath), "mcp-media.json");
|
|
4947
|
+
const serverDir = dirname7(fileURLToPath2(import.meta.url));
|
|
4948
|
+
const mcpMediaServerTs = join11(serverDir, "..", "mcp", "mediaServer.ts");
|
|
4949
|
+
const mcpMediaServerJs = join11(serverDir, "..", "mcp", "mediaServer.js");
|
|
4950
|
+
const useTs = existsSync9(mcpMediaServerTs) && !existsSync9(mcpMediaServerJs);
|
|
4951
|
+
const mcpCommand = useTs ? "tsx" : "node";
|
|
4952
|
+
const mcpArgs = [useTs ? mcpMediaServerTs : mcpMediaServerJs];
|
|
4953
|
+
writeFileSync2(mcpConfigPath, JSON.stringify({
|
|
4954
|
+
mcpServers: {
|
|
4955
|
+
"wechat-media": {
|
|
4956
|
+
command: mcpCommand,
|
|
4957
|
+
args: mcpArgs,
|
|
4958
|
+
env: { BRIDGE_API_URL: `http://localhost:${bridgePort}` }
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
}, null, 2) + "\n");
|
|
4962
|
+
const codexMcpScript = useTs ? mcpMediaServerTs : mcpMediaServerJs;
|
|
4963
|
+
const codexMcpServerConfigs = [
|
|
4964
|
+
"-c",
|
|
4965
|
+
`mcp_servers.wechat-media.command="${mcpCommand}"`,
|
|
4966
|
+
"-c",
|
|
4967
|
+
`mcp_servers.wechat-media.args=["${codexMcpScript}"]`,
|
|
4968
|
+
"-c",
|
|
4969
|
+
`mcp_servers.wechat-media.env.BRIDGE_API_URL="http://localhost:${bridgePort}"`
|
|
4970
|
+
];
|
|
4615
4971
|
const providerAdapters = options.providers ?? createDefaultProviders({
|
|
4616
4972
|
claudeCommand: options.providerCommands?.claude?.command,
|
|
4617
|
-
codexCommand: options.providerCommands?.codex?.command
|
|
4973
|
+
codexCommand: options.providerCommands?.codex?.command,
|
|
4974
|
+
mcpConfigPath,
|
|
4975
|
+
codexMcpServerConfigs
|
|
4618
4976
|
});
|
|
4619
4977
|
let currentConversation = conversation.getCurrent();
|
|
4620
4978
|
if (currentConversation) {
|
|
@@ -4639,7 +4997,13 @@ function createDaemonServer(options = {}) {
|
|
|
4639
4997
|
const weixinOutboundGate = managedWechatChannel && weixinStateStore ? new WeixinOutboundGate({
|
|
4640
4998
|
store: weixinStateStore,
|
|
4641
4999
|
send: async (chatId, msg) => {
|
|
4642
|
-
await managedWechatChannel.sendMessage({
|
|
5000
|
+
await managedWechatChannel.sendMessage({
|
|
5001
|
+
chatId,
|
|
5002
|
+
kind: msg.kind,
|
|
5003
|
+
text: msg.text,
|
|
5004
|
+
...msg.filePath ? { filePath: msg.filePath } : {},
|
|
5005
|
+
...msg.fileName ? { fileName: msg.fileName } : {}
|
|
5006
|
+
});
|
|
4643
5007
|
}
|
|
4644
5008
|
}) : void 0;
|
|
4645
5009
|
const tunnelProvider = options.tunnelProvider ?? new RelayTunnelRouter({
|
|
@@ -4785,6 +5149,36 @@ function createDaemonServer(options = {}) {
|
|
|
4785
5149
|
request.raw.on("error", cleanup);
|
|
4786
5150
|
return reply.hijack();
|
|
4787
5151
|
});
|
|
5152
|
+
app.post("/api/channel/send-media", async (request, reply) => {
|
|
5153
|
+
const { kind, filePath, fileName, chatId } = request.body ?? {};
|
|
5154
|
+
if (!filePath || typeof filePath !== "string") {
|
|
5155
|
+
return reply.code(400).send({ ok: false, error: "filePath is required" });
|
|
5156
|
+
}
|
|
5157
|
+
const validKinds = ["image", "video", "audio", "file"];
|
|
5158
|
+
if (!kind || !validKinds.includes(kind)) {
|
|
5159
|
+
return reply.code(400).send({ ok: false, error: `kind must be one of: ${validKinds.join(", ")}` });
|
|
5160
|
+
}
|
|
5161
|
+
if (!channel) {
|
|
5162
|
+
return reply.code(503).send({ ok: false, error: "channel not available" });
|
|
5163
|
+
}
|
|
5164
|
+
const targetChatId = chatId || activeUserStore.getActiveUser()?.platformUserId;
|
|
5165
|
+
if (!targetChatId) {
|
|
5166
|
+
return reply.code(400).send({ ok: false, error: "no active chat target; provide chatId or ensure an active user exists" });
|
|
5167
|
+
}
|
|
5168
|
+
try {
|
|
5169
|
+
await channel.sendMessage({
|
|
5170
|
+
chatId: targetChatId,
|
|
5171
|
+
kind,
|
|
5172
|
+
text: "",
|
|
5173
|
+
filePath,
|
|
5174
|
+
...fileName ? { fileName } : {}
|
|
5175
|
+
});
|
|
5176
|
+
return { ok: true };
|
|
5177
|
+
} catch (e) {
|
|
5178
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
5179
|
+
return reply.code(500).send({ ok: false, error: msg });
|
|
5180
|
+
}
|
|
5181
|
+
});
|
|
4788
5182
|
app.addHook("onReady", async () => {
|
|
4789
5183
|
await channel?.start({ background: true });
|
|
4790
5184
|
});
|
|
@@ -4924,7 +5318,7 @@ async function resolveProviderCommands(providers) {
|
|
|
4924
5318
|
}
|
|
4925
5319
|
|
|
4926
5320
|
// src/daemon/staticFrontend.ts
|
|
4927
|
-
import { readFileSync as
|
|
5321
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4928
5322
|
import { join as join12 } from "node:path";
|
|
4929
5323
|
import fastifyStatic from "@fastify/static";
|
|
4930
5324
|
function attachStaticFrontend(webRoot2) {
|
|
@@ -4933,7 +5327,7 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4933
5327
|
root: webRoot2,
|
|
4934
5328
|
wildcard: false
|
|
4935
5329
|
});
|
|
4936
|
-
const indexHtml =
|
|
5330
|
+
const indexHtml = readFileSync9(join12(webRoot2, "index.html"), "utf8");
|
|
4937
5331
|
app.setNotFoundHandler((request, reply) => {
|
|
4938
5332
|
if (request.url.startsWith("/api")) {
|
|
4939
5333
|
reply.code(404).send({ error: "not_found" });
|
|
@@ -4945,7 +5339,7 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4945
5339
|
}
|
|
4946
5340
|
|
|
4947
5341
|
// src/daemon/service.ts
|
|
4948
|
-
import { existsSync as
|
|
5342
|
+
import { existsSync as existsSync10, statSync, openSync as openSync2, closeSync as closeSync2, watchFile } from "node:fs";
|
|
4949
5343
|
import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
4950
5344
|
import { homedir as homedir8 } from "node:os";
|
|
4951
5345
|
import { dirname as dirname8, join as join13 } from "node:path";
|
|
@@ -4999,13 +5393,13 @@ async function startService(context) {
|
|
|
4999
5393
|
async function stopService(context) {
|
|
5000
5394
|
if (process.platform === "darwin") {
|
|
5001
5395
|
const spec = buildLaunchdSpec(context);
|
|
5002
|
-
if (!
|
|
5396
|
+
if (!existsSync10(spec.plistPath)) return await readServiceStatus(context);
|
|
5003
5397
|
await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
|
|
5004
5398
|
return await readServiceStatus(context);
|
|
5005
5399
|
}
|
|
5006
5400
|
if (process.platform === "linux") {
|
|
5007
5401
|
const spec = buildSystemdUserSpec(context);
|
|
5008
|
-
if (!
|
|
5402
|
+
if (!existsSync10(spec.unitPath)) return await readServiceStatus(context);
|
|
5009
5403
|
await runSystemctl(["--user", "stop", spec.unitName]).catch(() => void 0);
|
|
5010
5404
|
return await readServiceStatus(context);
|
|
5011
5405
|
}
|
|
@@ -5023,7 +5417,7 @@ async function stopService(context) {
|
|
|
5023
5417
|
async function uninstallService(context) {
|
|
5024
5418
|
if (process.platform === "darwin") {
|
|
5025
5419
|
const spec = buildLaunchdSpec(context);
|
|
5026
|
-
if (
|
|
5420
|
+
if (existsSync10(spec.plistPath)) {
|
|
5027
5421
|
await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
|
|
5028
5422
|
await rm2(spec.plistPath, { force: true });
|
|
5029
5423
|
}
|
|
@@ -5031,7 +5425,7 @@ async function uninstallService(context) {
|
|
|
5031
5425
|
}
|
|
5032
5426
|
if (process.platform === "linux") {
|
|
5033
5427
|
const spec = buildSystemdUserSpec(context);
|
|
5034
|
-
if (
|
|
5428
|
+
if (existsSync10(spec.unitPath)) {
|
|
5035
5429
|
await runSystemctl(["--user", "disable", "--now", spec.unitName]).catch(() => void 0);
|
|
5036
5430
|
await rm2(spec.unitPath, { force: true });
|
|
5037
5431
|
await runSystemctl(["--user", "daemon-reload"]).catch(() => void 0);
|
|
@@ -5081,7 +5475,7 @@ async function tailServiceLogs(context) {
|
|
|
5081
5475
|
async function readServiceStatus(context) {
|
|
5082
5476
|
if (process.platform === "darwin") {
|
|
5083
5477
|
const spec = buildLaunchdSpec(context);
|
|
5084
|
-
const installed =
|
|
5478
|
+
const installed = existsSync10(spec.plistPath);
|
|
5085
5479
|
const running = installed ? await runLaunchctl(["print", `gui/${process.getuid?.() ?? 0}/${spec.label}`]).then(() => true, () => false) : false;
|
|
5086
5480
|
return {
|
|
5087
5481
|
manager: "launchd",
|
|
@@ -5095,7 +5489,7 @@ async function readServiceStatus(context) {
|
|
|
5095
5489
|
}
|
|
5096
5490
|
if (process.platform === "linux") {
|
|
5097
5491
|
const spec = buildSystemdUserSpec(context);
|
|
5098
|
-
const installed =
|
|
5492
|
+
const installed = existsSync10(spec.unitPath);
|
|
5099
5493
|
const running = installed ? await runSystemctl(["--user", "is-active", "--quiet", spec.unitName]).then(() => true, () => false) : false;
|
|
5100
5494
|
return {
|
|
5101
5495
|
manager: "systemd-user",
|
|
@@ -5280,7 +5674,7 @@ function isProcessAlive(pid) {
|
|
|
5280
5674
|
}
|
|
5281
5675
|
}
|
|
5282
5676
|
async function readPidFile(pidPath) {
|
|
5283
|
-
if (!
|
|
5677
|
+
if (!existsSync10(pidPath)) return null;
|
|
5284
5678
|
try {
|
|
5285
5679
|
const raw = (await readFile6(pidPath, "utf8")).trim();
|
|
5286
5680
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -5331,12 +5725,12 @@ async function spawnDetachedDaemon(spec) {
|
|
|
5331
5725
|
}
|
|
5332
5726
|
}
|
|
5333
5727
|
async function tailFile(path, lines) {
|
|
5334
|
-
if (!
|
|
5728
|
+
if (!existsSync10(path)) return "";
|
|
5335
5729
|
const content = await readFile6(path, "utf8");
|
|
5336
5730
|
return content.split(/\r?\n/).slice(-lines).join("\n").trim();
|
|
5337
5731
|
}
|
|
5338
5732
|
async function tailFiles(paths) {
|
|
5339
|
-
const existing = paths.filter((path) =>
|
|
5733
|
+
const existing = paths.filter((path) => existsSync10(path));
|
|
5340
5734
|
for (const path of existing) {
|
|
5341
5735
|
const tail = await tailFile(path, 200);
|
|
5342
5736
|
if (tail) process.stdout.write(`[${path}]
|
|
@@ -5387,7 +5781,7 @@ function fileSizeHint(path) {
|
|
|
5387
5781
|
}
|
|
5388
5782
|
|
|
5389
5783
|
// src/cli.ts
|
|
5390
|
-
var here = dirname9(
|
|
5784
|
+
var here = dirname9(fileURLToPath3(import.meta.url));
|
|
5391
5785
|
var webRoot = join14(here, "..", "web");
|
|
5392
5786
|
async function main() {
|
|
5393
5787
|
const command = process.argv[2];
|
|
@@ -5437,7 +5831,7 @@ async function main() {
|
|
|
5437
5831
|
}
|
|
5438
5832
|
}
|
|
5439
5833
|
async function cmdStart() {
|
|
5440
|
-
if (!
|
|
5834
|
+
if (!existsSync11(webRoot)) {
|
|
5441
5835
|
console.error(`\u627E\u4E0D\u5230\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269: ${webRoot}`);
|
|
5442
5836
|
console.error("\u8BF7\u91CD\u65B0\u5B89\u88C5\u5B8C\u6574\u7684 npm \u5305\u3002");
|
|
5443
5837
|
process.exitCode = 1;
|
|
@@ -5468,7 +5862,7 @@ async function cmdTail() {
|
|
|
5468
5862
|
await tailServiceLogs(createServiceContext());
|
|
5469
5863
|
}
|
|
5470
5864
|
async function cmdDaemon() {
|
|
5471
|
-
if (!
|
|
5865
|
+
if (!existsSync11(webRoot)) {
|
|
5472
5866
|
console.error(`\u627E\u4E0D\u5230\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269: ${webRoot}`);
|
|
5473
5867
|
process.exitCode = 1;
|
|
5474
5868
|
return;
|
|
@@ -5481,13 +5875,13 @@ async function cmdDaemon() {
|
|
|
5481
5875
|
async function cmdDoctor() {
|
|
5482
5876
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5483
5877
|
console.log("claude-codex-wechat doctor\n");
|
|
5484
|
-
report("\u914D\u7F6E\u6587\u4EF6",
|
|
5485
|
-
report("\u524D\u7AEF\u4EA7\u7269",
|
|
5878
|
+
report("\u914D\u7F6E\u6587\u4EF6", existsSync11(configPath) ? configPath : `\u7F3A\u5931 (${configPath})\uFF0C\u9996\u6B21 start \u65F6\u81EA\u52A8\u521B\u5EFA`);
|
|
5879
|
+
report("\u524D\u7AEF\u4EA7\u7269", existsSync11(webRoot) ? webRoot : `\u7F3A\u5931 (${webRoot})`);
|
|
5486
5880
|
const claude = await findExecutable("claude");
|
|
5487
5881
|
report("claude \u53EF\u6267\u884C", claude ?? "\u672A\u627E\u5230\uFF0C\u9700\u5148\u5B89\u88C5\u5E76\u767B\u5F55 Claude Code");
|
|
5488
5882
|
const codex = await findExecutable("codex");
|
|
5489
5883
|
report("codex \u53EF\u6267\u884C", codex ?? "\u672A\u627E\u5230\uFF08\u4EC5\u4F7F\u7528 Claude \u65F6\u53EF\u5FFD\u7565\uFF09");
|
|
5490
|
-
if (
|
|
5884
|
+
if (existsSync11(configPath)) {
|
|
5491
5885
|
const config = loadBridgeConfig(configPath);
|
|
5492
5886
|
const wechat = config.wechat;
|
|
5493
5887
|
report("\u5FAE\u4FE1\u542F\u7528", wechat?.enabled ? "\u662F" : "\u5426");
|
|
@@ -5498,7 +5892,7 @@ async function cmdDoctor() {
|
|
|
5498
5892
|
}
|
|
5499
5893
|
function printUpdateHint() {
|
|
5500
5894
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5501
|
-
if (!
|
|
5895
|
+
if (!existsSync11(configPath)) return;
|
|
5502
5896
|
let update;
|
|
5503
5897
|
try {
|
|
5504
5898
|
update = loadBridgeConfig(configPath).update;
|
|
@@ -5514,12 +5908,12 @@ function printUpdateHint() {
|
|
|
5514
5908
|
}
|
|
5515
5909
|
function cmdPrintConfig() {
|
|
5516
5910
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
5517
|
-
if (!
|
|
5911
|
+
if (!existsSync11(configPath)) {
|
|
5518
5912
|
console.error(`\u914D\u7F6E\u4E0D\u5B58\u5728: ${configPath}\uFF08\u9996\u6B21 start \u540E\u81EA\u52A8\u751F\u6210\uFF09`);
|
|
5519
5913
|
process.exitCode = 1;
|
|
5520
5914
|
return;
|
|
5521
5915
|
}
|
|
5522
|
-
console.log(
|
|
5916
|
+
console.log(readFileSync10(configPath, "utf8"));
|
|
5523
5917
|
}
|
|
5524
5918
|
async function cmdUninstall() {
|
|
5525
5919
|
const status = await uninstallService(createServiceContext());
|
|
@@ -5527,7 +5921,7 @@ async function cmdUninstall() {
|
|
|
5527
5921
|
}
|
|
5528
5922
|
function createServiceContext() {
|
|
5529
5923
|
return {
|
|
5530
|
-
cliEntrypointPath:
|
|
5924
|
+
cliEntrypointPath: fileURLToPath3(import.meta.url),
|
|
5531
5925
|
nodePath: process.execPath,
|
|
5532
5926
|
configPath: process.env.BRIDGE_CONFIG ?? defaultConfigPath(),
|
|
5533
5927
|
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.31",
|
|
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",
|