@unlinearity/cli-wechat-bridge 1.0.6 → 1.0.9

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.
@@ -23,6 +23,7 @@ export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-pane
23
23
  export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
24
24
  export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
25
25
  export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
26
+ export const EMOJI_BINDINGS_FILE = path.join(CHANNEL_DATA_DIR, "emoji-bindings.json");
26
27
  const LEGACY_GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
27
28
  const LEGACY_REPO_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
28
29
  const LEGACY_ENV_CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlinearity/cli-wechat-bridge",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
5
5
  "type": "module",
6
6
  "files": [
@@ -19,7 +19,7 @@
19
19
  "terminal"
20
20
  ],
21
21
  "author": "UNLINEARITY",
22
- "license": "MIT",
22
+ "license": "AGPL-3.0-or-later",
23
23
  "repository": {
24
24
  "type": "git",
25
25
  "url": "git+https://github.com/UNLINEARITY/CLI-WeChat-Bridge.git"
@@ -48,10 +48,8 @@
48
48
  "wechat-check-update": "bin/wechat-check-update.mjs"
49
49
  },
50
50
  "dependencies": {
51
- "@anthropic-ai/sdk": "^0.80.0",
52
51
  "@modelcontextprotocol/sdk": "^1.27.1",
53
52
  "@opencode-ai/sdk": "^1.3.2",
54
- "ajv-formats": "^3.0.1",
55
53
  "node-pty": "^1.1.0",
56
54
  "qrcode-terminal": "^0.12.0"
57
55
  },
@@ -1,72 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import { ensureWorkspaceChannelDir, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
4
- export function buildCodexPanelToken() {
5
- return crypto.randomBytes(18).toString("hex");
6
- }
7
- export function writeCodexPanelEndpoint(endpoint) {
8
- const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
9
- fs.writeFileSync(endpointFile, JSON.stringify(endpoint, null, 2), "utf8");
10
- }
11
- export function readCodexPanelEndpoint(cwd) {
12
- try {
13
- const { endpointFile } = getWorkspaceChannelPaths(cwd);
14
- if (!fs.existsSync(endpointFile)) {
15
- return null;
16
- }
17
- return JSON.parse(fs.readFileSync(endpointFile, "utf8"));
18
- }
19
- catch {
20
- return null;
21
- }
22
- }
23
- export function clearCodexPanelEndpoint(cwd, instanceId) {
24
- try {
25
- const { endpointFile } = getWorkspaceChannelPaths(cwd);
26
- if (!fs.existsSync(endpointFile)) {
27
- return;
28
- }
29
- if (!instanceId) {
30
- fs.rmSync(endpointFile, { force: true });
31
- return;
32
- }
33
- const endpoint = readCodexPanelEndpoint(cwd);
34
- if (!endpoint || endpoint.instanceId === instanceId) {
35
- fs.rmSync(endpointFile, { force: true });
36
- }
37
- }
38
- catch {
39
- // Best effort cleanup.
40
- }
41
- }
42
- export function sendCodexPanelMessage(socket, message) {
43
- socket.write(`${JSON.stringify(message)}\n`);
44
- }
45
- export function attachCodexPanelMessageListener(socket, onMessage) {
46
- let buffer = "";
47
- const onData = (chunk) => {
48
- buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
49
- while (true) {
50
- const newlineIndex = buffer.indexOf("\n");
51
- if (newlineIndex < 0) {
52
- return;
53
- }
54
- const line = buffer.slice(0, newlineIndex).trim();
55
- buffer = buffer.slice(newlineIndex + 1);
56
- if (!line) {
57
- continue;
58
- }
59
- try {
60
- onMessage(JSON.parse(line));
61
- }
62
- catch {
63
- // Ignore malformed local IPC frames.
64
- }
65
- }
66
- };
67
- socket.setEncoding("utf8");
68
- socket.on("data", onData);
69
- return () => {
70
- socket.off("data", onData);
71
- };
72
- }
@@ -1,179 +0,0 @@
1
- #!/usr/bin/env bun
2
- import net from "node:net";
3
- import path from "node:path";
4
- import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
5
- import { attachCodexPanelMessageListener, readCodexPanelEndpoint, sendCodexPanelMessage, } from "./codex-panel-link.js";
6
- import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
7
- function log(message) {
8
- process.stderr.write(`[codex-panel] ${message}\n`);
9
- }
10
- function parseCliArgs(argv) {
11
- let cwd = process.cwd();
12
- for (let i = 0; i < argv.length; i += 1) {
13
- const arg = argv[i];
14
- const next = argv[i + 1];
15
- if (arg === "--help" || arg === "-h") {
16
- process.stdout.write([
17
- "Usage: wechat-codex [--cwd <path>]",
18
- "",
19
- 'Starts the visible Codex panel and connects it to the running "wechat-bridge-codex" instance for the current directory.',
20
- "",
21
- ].join("\n"));
22
- process.exit(0);
23
- }
24
- if (arg === "--cwd") {
25
- if (!next) {
26
- throw new Error("--cwd requires a value");
27
- }
28
- cwd = path.resolve(next);
29
- i += 1;
30
- continue;
31
- }
32
- throw new Error(`Unknown argument: ${arg}`);
33
- }
34
- return { cwd };
35
- }
36
- async function main() {
37
- migrateLegacyChannelFiles(log);
38
- const options = parseCliArgs(process.argv.slice(2));
39
- const endpoint = readCodexPanelEndpoint(options.cwd);
40
- if (!endpoint) {
41
- throw new Error(`No active Codex bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-codex" in that directory first.`);
42
- }
43
- const socket = await new Promise((resolve, reject) => {
44
- const nextSocket = net.connect({
45
- host: "127.0.0.1",
46
- port: endpoint.port,
47
- });
48
- nextSocket.once("connect", () => resolve(nextSocket));
49
- nextSocket.once("error", (error) => reject(error));
50
- });
51
- socket.setNoDelay(true);
52
- const adapter = createBridgeAdapter({
53
- kind: "codex",
54
- command: endpoint.command,
55
- cwd: endpoint.cwd,
56
- profile: endpoint.profile,
57
- initialSharedThreadId: endpoint.sharedThreadId,
58
- renderMode: "panel",
59
- });
60
- let shuttingDown = false;
61
- let helloAcknowledged = false;
62
- let detachListener = null;
63
- const publishState = () => {
64
- sendCodexPanelMessage(socket, {
65
- type: "state",
66
- state: adapter.getState(),
67
- });
68
- };
69
- const sendResponse = (id, ok, result, error) => {
70
- sendCodexPanelMessage(socket, {
71
- type: "response",
72
- id,
73
- ok,
74
- result,
75
- error,
76
- });
77
- };
78
- const closePanel = async (exitCode = 0) => {
79
- if (shuttingDown) {
80
- return;
81
- }
82
- shuttingDown = true;
83
- detachListener?.();
84
- detachListener = null;
85
- try {
86
- await adapter.dispose();
87
- }
88
- catch {
89
- // Best effort cleanup.
90
- }
91
- try {
92
- socket.end();
93
- socket.destroy();
94
- }
95
- catch {
96
- // Best effort cleanup.
97
- }
98
- process.exit(exitCode);
99
- };
100
- adapter.setEventSink((event) => {
101
- sendCodexPanelMessage(socket, {
102
- type: "event",
103
- event,
104
- });
105
- publishState();
106
- });
107
- detachListener = attachCodexPanelMessageListener(socket, (message) => {
108
- if (!helloAcknowledged) {
109
- if (message.type === "hello_ack") {
110
- helloAcknowledged = true;
111
- }
112
- return;
113
- }
114
- if (message.type !== "request") {
115
- return;
116
- }
117
- void (async () => {
118
- try {
119
- switch (message.payload.command) {
120
- case "send_input":
121
- await adapter.sendInput(message.payload.text);
122
- sendResponse(message.id, true);
123
- break;
124
- case "list_resume_sessions":
125
- case "list_resume_threads":
126
- sendResponse(message.id, true, await adapter.listResumeSessions(message.payload.limit));
127
- break;
128
- case "resume_session":
129
- await adapter.resumeSession(message.payload.sessionId);
130
- publishState();
131
- sendResponse(message.id, true);
132
- break;
133
- case "resume_thread":
134
- await adapter.resumeSession(message.payload.threadId);
135
- publishState();
136
- sendResponse(message.id, true);
137
- break;
138
- case "interrupt":
139
- sendResponse(message.id, true, await adapter.interrupt());
140
- break;
141
- case "reset":
142
- await adapter.reset();
143
- publishState();
144
- sendResponse(message.id, true);
145
- break;
146
- case "resolve_approval":
147
- sendResponse(message.id, true, await adapter.resolveApproval(message.payload.action));
148
- break;
149
- case "dispose":
150
- sendResponse(message.id, true);
151
- await closePanel(0);
152
- break;
153
- }
154
- }
155
- catch (error) {
156
- const text = error instanceof Error ? error.message : String(error);
157
- sendResponse(message.id, false, undefined, text);
158
- }
159
- })();
160
- });
161
- socket.once("close", () => {
162
- void closePanel(0);
163
- });
164
- socket.once("error", () => {
165
- void closePanel(1);
166
- });
167
- sendCodexPanelMessage(socket, {
168
- type: "hello",
169
- token: endpoint.token,
170
- panelPid: process.pid,
171
- });
172
- await adapter.start();
173
- publishState();
174
- log(`Connected to bridge ${endpoint.instanceId}.`);
175
- }
176
- main().catch((error) => {
177
- log(error instanceof Error ? error.message : String(error));
178
- process.exit(1);
179
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,355 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Standalone WeChat + Claude bot.
4
- *
5
- * This script directly connects WeChat ClawBot to Claude API,
6
- * bypassing the MCP channel protocol.
7
- *
8
- * Usage:
9
- * bun run src/wechat/standalone-bot.ts
10
- */
11
- import crypto from "node:crypto";
12
- import fs from "node:fs";
13
- import path from "node:path";
14
- import Anthropic from "@anthropic-ai/sdk";
15
- import { CHANNEL_DATA_DIR, CREDENTIALS_FILE, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
16
- // Constants
17
- const LOG_DIR = path.join(CHANNEL_DATA_DIR, "logs");
18
- const BOT_TYPE = "3";
19
- const CHANNEL_VERSION = "0.1.0";
20
- const LONG_POLL_TIMEOUT_MS = 35_000;
21
- const MSG_TYPE_USER = 1;
22
- const MSG_TYPE_BOT = 2;
23
- const MSG_ITEM_TEXT = 1;
24
- const MSG_ITEM_VOICE = 3;
25
- const MSG_STATE_FINISH = 2;
26
- // Conversation history per user
27
- const conversationHistory = new Map();
28
- const MAX_HISTORY = 10;
29
- // Logging
30
- function log(message) {
31
- const timestamp = new Date().toISOString();
32
- console.log(`[${timestamp}] ${message}`);
33
- }
34
- function logError(message) {
35
- const timestamp = new Date().toISOString();
36
- console.error(`[${timestamp}] ERROR: ${message}`);
37
- }
38
- // Load WeChat credentials
39
- function loadCredentials() {
40
- try {
41
- if (!fs.existsSync(CREDENTIALS_FILE)) {
42
- logError(`Credentials file not found: ${CREDENTIALS_FILE}`);
43
- return null;
44
- }
45
- const content = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
46
- return JSON.parse(content);
47
- }
48
- catch (err) {
49
- logError(`Failed to read credentials: ${String(err)}`);
50
- return null;
51
- }
52
- }
53
- // WeChat API utilities
54
- function randomWechatUin() {
55
- const uint32 = crypto.randomBytes(4).readUInt32BE(0);
56
- return Buffer.from(String(uint32), "utf-8").toString("base64");
57
- }
58
- function buildHeaders(token, body) {
59
- const headers = {
60
- "Content-Type": "application/json",
61
- AuthorizationType: "ilink_bot_token",
62
- "X-WECHAT-UIN": randomWechatUin(),
63
- };
64
- if (body) {
65
- headers["Content-Length"] = String(Buffer.byteLength(body, "utf-8"));
66
- }
67
- if (token?.trim()) {
68
- headers.Authorization = `Bearer ${token.trim()}`;
69
- }
70
- return headers;
71
- }
72
- async function apiFetch(params) {
73
- const base = params.baseUrl.endsWith("/") ? params.baseUrl : `${params.baseUrl}/`;
74
- const url = new URL(params.endpoint, base).toString();
75
- const controller = new AbortController();
76
- const timer = setTimeout(() => controller.abort(), params.timeoutMs);
77
- try {
78
- const res = await fetch(url, {
79
- method: "POST",
80
- headers: buildHeaders(params.token, params.body),
81
- body: params.body,
82
- signal: controller.signal,
83
- });
84
- clearTimeout(timer);
85
- const text = await res.text();
86
- if (!res.ok) {
87
- throw new Error(`HTTP ${res.status}: ${text}`);
88
- }
89
- return text;
90
- }
91
- catch (err) {
92
- clearTimeout(timer);
93
- throw err;
94
- }
95
- }
96
- async function getUpdates(baseUrl, token, getUpdatesBuf) {
97
- try {
98
- const raw = await apiFetch({
99
- baseUrl,
100
- endpoint: "ilink/bot/getupdates",
101
- body: JSON.stringify({
102
- get_updates_buf: getUpdatesBuf,
103
- base_info: { channel_version: CHANNEL_VERSION },
104
- }),
105
- token,
106
- timeoutMs: LONG_POLL_TIMEOUT_MS,
107
- });
108
- return JSON.parse(raw);
109
- }
110
- catch (err) {
111
- if (err instanceof Error && err.name === "AbortError") {
112
- return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
113
- }
114
- throw err;
115
- }
116
- }
117
- function extractTextFromMessage(msg) {
118
- if (!msg.item_list?.length) {
119
- return "";
120
- }
121
- const lines = [];
122
- for (const item of msg.item_list) {
123
- if (item.type === MSG_ITEM_TEXT) {
124
- const text = item.text_item?.text?.trim();
125
- if (text) {
126
- lines.push(text);
127
- }
128
- }
129
- if (item.type === MSG_ITEM_VOICE) {
130
- const transcript = item.voice_item?.text?.trim();
131
- if (transcript) {
132
- lines.push(transcript);
133
- }
134
- }
135
- }
136
- return lines.join("\n").trim();
137
- }
138
- function generateClientId() {
139
- return `claude-wechat-bot:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
140
- }
141
- async function sendTextMessage(baseUrl, token, to, text, contextToken) {
142
- const trimmedText = text.trim();
143
- if (!trimmedText) {
144
- throw new Error("Reply text cannot be empty.");
145
- }
146
- const clientId = generateClientId();
147
- await apiFetch({
148
- baseUrl,
149
- endpoint: "ilink/bot/sendmessage",
150
- body: JSON.stringify({
151
- msg: {
152
- from_user_id: "",
153
- to_user_id: to,
154
- client_id: clientId,
155
- message_type: MSG_TYPE_BOT,
156
- message_state: MSG_STATE_FINISH,
157
- item_list: [{ type: MSG_ITEM_TEXT, text_item: { text: trimmedText } }],
158
- context_token: contextToken,
159
- },
160
- base_info: { channel_version: CHANNEL_VERSION },
161
- }),
162
- token,
163
- timeoutMs: 15_000,
164
- });
165
- return clientId;
166
- }
167
- // Message deduplication
168
- const recentMessageKeys = new Set();
169
- const recentMessageOrder = [];
170
- const RECENT_MESSAGE_CACHE_SIZE = 500;
171
- function buildMessageKey(msg) {
172
- return [
173
- msg.from_user_id ?? "",
174
- msg.client_id ?? "",
175
- String(msg.create_time_ms ?? ""),
176
- msg.context_token ?? "",
177
- ].join("|");
178
- }
179
- function rememberMessage(key) {
180
- if (!key || recentMessageKeys.has(key)) {
181
- return false;
182
- }
183
- recentMessageKeys.add(key);
184
- recentMessageOrder.push(key);
185
- while (recentMessageOrder.length > RECENT_MESSAGE_CACHE_SIZE) {
186
- const oldest = recentMessageOrder.shift();
187
- if (oldest) {
188
- recentMessageKeys.delete(oldest);
189
- }
190
- }
191
- return true;
192
- }
193
- // Claude API integration
194
- let anthropic = null;
195
- function initClaude() {
196
- // 使用与 Claude Code 相同的代理配置
197
- const baseURL = process.env.ANTHROPIC_BASE_URL || "http://127.0.0.1:15721";
198
- anthropic = new Anthropic({
199
- apiKey: "dummy-key", // 代理不需要真实的 API key
200
- baseURL: baseURL,
201
- dangerouslyAllowBrowser: true, // 允许在非标准环境下使用
202
- });
203
- log(`Claude API initialized (via proxy: ${baseURL})`);
204
- return true;
205
- }
206
- async function getClaudeResponse(userMessage, userId) {
207
- if (!anthropic) {
208
- throw new Error("Claude API not initialized");
209
- }
210
- // Get or create conversation history
211
- let history = conversationHistory.get(userId) || [];
212
- // Add user message to history
213
- history.push({ role: "user", content: userMessage });
214
- // Keep only recent messages
215
- if (history.length > MAX_HISTORY) {
216
- history = history.slice(-MAX_HISTORY);
217
- }
218
- try {
219
- const msg = await anthropic.messages.create({
220
- model: "claude-sonnet-4-20250514",
221
- max_tokens: 1024,
222
- system: "你是一个有帮助的AI助手。请用简洁、友好的方式回复,适合微信聊天场景。",
223
- messages: history,
224
- });
225
- // Extract the response text
226
- let responseText = "";
227
- for (const block of msg.content) {
228
- if (block.type === "text") {
229
- responseText += block.text;
230
- }
231
- }
232
- // Add assistant response to history
233
- history.push({ role: "assistant", content: responseText });
234
- conversationHistory.set(userId, history);
235
- return responseText;
236
- }
237
- catch (err) {
238
- logError(`Claude API error: ${String(err)}`);
239
- throw err;
240
- }
241
- }
242
- // Main polling loop
243
- async function startPolling(account) {
244
- const { baseUrl, token } = account;
245
- let getUpdatesBuf = "";
246
- // Load sync state
247
- try {
248
- if (fs.existsSync(SYNC_BUF_FILE)) {
249
- getUpdatesBuf = fs.readFileSync(SYNC_BUF_FILE, "utf-8");
250
- if (getUpdatesBuf) {
251
- log(`Recovered sync state from ${SYNC_BUF_FILE}`);
252
- }
253
- }
254
- }
255
- catch (err) {
256
- logError(`Failed to load sync state: ${String(err)}`);
257
- }
258
- log("Starting WeChat message polling loop...");
259
- log("Press Ctrl+C to stop");
260
- const contextTokenCache = new Map();
261
- while (true) {
262
- try {
263
- const resp = await getUpdates(baseUrl, token, getUpdatesBuf);
264
- const isError = (resp.ret !== undefined && resp.ret !== 0) ||
265
- (resp.errcode !== undefined && resp.errcode !== 0);
266
- if (isError) {
267
- logError(`getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ""}`);
268
- await new Promise((resolve) => setTimeout(resolve, 2000));
269
- continue;
270
- }
271
- // Save sync state
272
- if (resp.get_updates_buf) {
273
- getUpdatesBuf = resp.get_updates_buf;
274
- try {
275
- fs.writeFileSync(SYNC_BUF_FILE, getUpdatesBuf, "utf-8");
276
- }
277
- catch (err) {
278
- logError(`Failed to persist sync state: ${String(err)}`);
279
- }
280
- }
281
- // Process messages
282
- for (const msg of resp.msgs ?? []) {
283
- if (msg.message_type !== MSG_TYPE_USER) {
284
- continue;
285
- }
286
- const text = extractTextFromMessage(msg);
287
- if (!text) {
288
- continue;
289
- }
290
- const senderId = msg.from_user_id ?? "unknown";
291
- const messageKey = buildMessageKey(msg);
292
- if (!rememberMessage(messageKey)) {
293
- continue;
294
- }
295
- // Cache context token for replies
296
- if (msg.context_token) {
297
- contextTokenCache.set(senderId, msg.context_token);
298
- }
299
- log(`📨 收到消息 from ${senderId}: ${text.slice(0, 50)}${text.length > 50 ? "..." : ""}`);
300
- // Get response from Claude
301
- try {
302
- const response = await getClaudeResponse(text, senderId);
303
- log(`📤 发送回复 to ${senderId}: ${response.slice(0, 50)}${response.length > 50 ? "..." : ""}`);
304
- const contextToken = contextTokenCache.get(senderId) || "";
305
- await sendTextMessage(baseUrl, token, senderId, response, contextToken);
306
- }
307
- catch (err) {
308
- logError(`Failed to get/send response: ${String(err)}`);
309
- // Send error message to user
310
- try {
311
- const contextToken = contextTokenCache.get(senderId) || "";
312
- await sendTextMessage(baseUrl, token, senderId, "抱歉,我遇到了一些问题,请稍后再试。", contextToken);
313
- }
314
- catch {
315
- // Ignore
316
- }
317
- }
318
- }
319
- }
320
- catch (err) {
321
- logError(`Polling error: ${String(err)}`);
322
- await new Promise((resolve) => setTimeout(resolve, 2000));
323
- }
324
- }
325
- }
326
- // Main function
327
- async function main() {
328
- log("=== WeChat + Claude 独立机器人 ===");
329
- log("");
330
- migrateLegacyChannelFiles(log);
331
- // Load WeChat credentials
332
- log(`正在加载微信凭据: ${CREDENTIALS_FILE}`);
333
- const account = loadCredentials();
334
- if (!account) {
335
- logError("无法加载微信凭据,请先运行: bun run setup");
336
- process.exit(1);
337
- }
338
- log(`✓ 微信账号: ${account.accountId}`);
339
- // Initialize Claude API
340
- log("");
341
- log("正在初始化 Claude API...");
342
- if (!initClaude()) {
343
- logError("Claude API 初始化失败");
344
- process.exit(1);
345
- }
346
- log("");
347
- log("✓ 所有初始化完成!");
348
- log("");
349
- // Start polling
350
- await startPolling(account);
351
- }
352
- main().catch((err) => {
353
- logError(`Fatal: ${String(err)}`);
354
- process.exit(1);
355
- });