@aster110/cc2wechat 3.0.0 → 3.2.0
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/CODE_REVIEW.md +182 -0
- package/EXPERIENCE.md +42 -1
- package/TEST_REVIEW.md +55 -0
- package/dist/auth.js +3 -1
- package/dist/auth.js.map +1 -1
- package/dist/daemon.js +7 -154
- package/dist/daemon.js.map +1 -1
- package/dist/handlers/pipe.d.ts +3 -0
- package/dist/handlers/pipe.js +47 -0
- package/dist/handlers/pipe.js.map +1 -0
- package/dist/handlers/terminal.d.ts +3 -0
- package/dist/handlers/terminal.js +95 -0
- package/dist/handlers/terminal.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +31 -0
- package/dist/utils.js.map +1 -0
- package/package.json +4 -2
- package/src/auth.ts +3 -1
- package/src/daemon.ts +8 -160
- package/src/handlers/pipe.ts +56 -0
- package/src/handlers/terminal.ts +104 -0
- package/src/utils.ts +30 -0
- package/tests/store.test.ts +112 -0
- package/tests/utils.test.ts +116 -0
- package/tests/wechat-api.test.ts +223 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { extractText, userIdToSessionUUID, sleep } from '../utils.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// iTerm Tab Management
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Maintain tab state per WeChat user
|
|
8
|
+
const userTabs = new Map(); // userId -> tabName
|
|
9
|
+
// Track tab IDs - persisted to file so it survives daemon restart
|
|
10
|
+
const TAB_REGISTRY_PATH = '/tmp/cc2wechat-tabs.json';
|
|
11
|
+
const tabSessionIds = new Map(); // tabName -> iTerm window id
|
|
12
|
+
// Load persisted tab registry on startup
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(TAB_REGISTRY_PATH)) {
|
|
15
|
+
const data = JSON.parse(fs.readFileSync(TAB_REGISTRY_PATH, 'utf-8'));
|
|
16
|
+
for (const [k, v] of Object.entries(data)) {
|
|
17
|
+
tabSessionIds.set(k, v);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
function saveTabRegistry() {
|
|
23
|
+
fs.writeFileSync(TAB_REGISTRY_PATH, JSON.stringify(Object.fromEntries(tabSessionIds)));
|
|
24
|
+
}
|
|
25
|
+
function tabExists(tabName) {
|
|
26
|
+
const windowId = tabSessionIds.get(tabName);
|
|
27
|
+
if (!windowId)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
const result = execSync(`osascript -e '
|
|
31
|
+
tell application "iTerm2"
|
|
32
|
+
try
|
|
33
|
+
set w to (first window whose id is ${windowId})
|
|
34
|
+
return "found"
|
|
35
|
+
on error
|
|
36
|
+
return "not_found"
|
|
37
|
+
end try
|
|
38
|
+
end tell
|
|
39
|
+
'`, { encoding: 'utf-8' }).trim();
|
|
40
|
+
return result === 'found';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function createTabAndStartCC(tabName, ccSessionId, cwd) {
|
|
47
|
+
// Create NEW WINDOW and capture window ID (same approach as cc-mesh)
|
|
48
|
+
const windowId = execSync(`osascript -e '
|
|
49
|
+
tell application "iTerm2"
|
|
50
|
+
set w to (create window with default profile)
|
|
51
|
+
tell current session of w
|
|
52
|
+
write text "cd ${cwd} && claude --resume ${ccSessionId} --dangerously-skip-permissions"
|
|
53
|
+
end tell
|
|
54
|
+
return id of w
|
|
55
|
+
end tell
|
|
56
|
+
'`, { encoding: 'utf-8' }).trim();
|
|
57
|
+
tabSessionIds.set(tabName, windowId);
|
|
58
|
+
saveTabRegistry();
|
|
59
|
+
console.log(`[cc2wechat] window created: ${tabName} -> window id: ${windowId}`);
|
|
60
|
+
}
|
|
61
|
+
function injectMessage(tabName, message) {
|
|
62
|
+
const windowId = tabSessionIds.get(tabName);
|
|
63
|
+
if (!windowId)
|
|
64
|
+
return;
|
|
65
|
+
const escaped = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
66
|
+
execSync(`osascript -e '
|
|
67
|
+
tell application "iTerm2"
|
|
68
|
+
tell current session of (first window whose id is ${windowId})
|
|
69
|
+
write text "${escaped}"
|
|
70
|
+
end tell
|
|
71
|
+
end tell
|
|
72
|
+
'`);
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Terminal mode message handler
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export async function handleMessageTerminal(msg, account) {
|
|
78
|
+
const text = extractText(msg);
|
|
79
|
+
const userId = msg.from_user_id ?? '';
|
|
80
|
+
const sessionId = userIdToSessionUUID(userId);
|
|
81
|
+
const tabName = `wechat-${userId.slice(0, 8)}`;
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
if (tabExists(tabName)) {
|
|
84
|
+
console.log(`[cc2wechat] -> inject to existing window: ${tabName}`);
|
|
85
|
+
injectMessage(tabName, `[微信] ${text}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`[cc2wechat] -> creating window: ${tabName} (session: ${sessionId})`);
|
|
89
|
+
createTabAndStartCC(tabName, sessionId, cwd);
|
|
90
|
+
await sleep(5000);
|
|
91
|
+
injectMessage(tabName, `[微信] ${text}`);
|
|
92
|
+
}
|
|
93
|
+
userTabs.set(userId, tabName);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=terminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal.js","sourceRoot":"","sources":["../../src/handlers/terminal.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { MessageItemType } from './types.js';
|
|
3
|
+
export function userIdToSessionUUID(userId) {
|
|
4
|
+
const hash = createHash('md5').update(`cc2wechat:${userId}`).digest('hex');
|
|
5
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
|
|
6
|
+
}
|
|
7
|
+
export function extractText(msg) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const item of msg.item_list ?? []) {
|
|
10
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text) {
|
|
11
|
+
parts.push(item.text_item.text);
|
|
12
|
+
}
|
|
13
|
+
else if (item.type === MessageItemType.IMAGE) {
|
|
14
|
+
parts.push('[Image]');
|
|
15
|
+
}
|
|
16
|
+
else if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
|
|
17
|
+
parts.push(`[Voice] ${item.voice_item.text}`);
|
|
18
|
+
}
|
|
19
|
+
else if (item.type === MessageItemType.FILE && item.file_item?.file_name) {
|
|
20
|
+
parts.push(`[File: ${item.file_item.file_name}]`);
|
|
21
|
+
}
|
|
22
|
+
else if (item.type === MessageItemType.VIDEO) {
|
|
23
|
+
parts.push('[Video]');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return parts.join('\n') || '[Empty message]';
|
|
27
|
+
}
|
|
28
|
+
export function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3E,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AACvH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAkB;IAC5C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;YAC/D,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,KAAK,EAAE,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC;YACxE,KAAK,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC;YAC3E,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,KAAK,EAAE,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aster110/cc2wechat",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "WeChat channel for Claude Code — chat with Claude Code from WeChat via iLink Bot API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"dev": "tsx src/server.ts",
|
|
14
|
+
"test": "vitest run",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|
|
16
17
|
"engines": {
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^22.0.0",
|
|
25
26
|
"tsx": "^4.19.0",
|
|
26
|
-
"typescript": "^5.7.0"
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"vitest": "^3.0.0"
|
|
27
29
|
},
|
|
28
30
|
"license": "MIT"
|
|
29
31
|
}
|
package/src/auth.ts
CHANGED
|
@@ -155,7 +155,9 @@ function buildQRPage(qrUrl: string): string {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
function openBrowser(url: string): void {
|
|
158
|
-
const cmd = process.platform === 'darwin' ? 'open'
|
|
158
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
159
|
+
: process.platform === 'win32' ? 'start'
|
|
160
|
+
: 'xdg-open';
|
|
159
161
|
exec(`${cmd} ${url}`, () => {});
|
|
160
162
|
}
|
|
161
163
|
|
package/src/daemon.ts
CHANGED
|
@@ -1,148 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync } from 'node:child_process';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
3
|
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const replyCli = path.join(__dirname, 'reply-cli.js');
|
|
11
|
-
const IS_MACOS = process.platform === 'darwin';
|
|
12
4
|
|
|
13
5
|
import { loginWithQRWeb } from './auth.js';
|
|
14
6
|
import { getActiveAccount, saveAccount, loadSyncBuf, saveSyncBuf } from './store.js';
|
|
15
7
|
import { getUpdates, sendTyping, getConfig } from './wechat-api.js';
|
|
16
8
|
import type { WeixinMessage } from './types.js';
|
|
17
|
-
import { MessageItemType } from './types.js';
|
|
18
9
|
import type { AccountData } from './store.js';
|
|
10
|
+
import { extractText } from './utils.js';
|
|
11
|
+
import { handleMessageTerminal } from './handlers/terminal.js';
|
|
12
|
+
import { handleMessagePipe } from './handlers/pipe.js';
|
|
19
13
|
|
|
20
14
|
// ---------------------------------------------------------------------------
|
|
21
15
|
// Constants
|
|
22
16
|
// ---------------------------------------------------------------------------
|
|
23
17
|
|
|
18
|
+
const IS_MACOS = process.platform === 'darwin';
|
|
19
|
+
|
|
24
20
|
const SESSION_EXPIRED_ERRCODE = -14;
|
|
25
21
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
26
22
|
const BACKOFF_DELAY_MS = 30_000;
|
|
27
23
|
const RETRY_DELAY_MS = 2_000;
|
|
28
24
|
const SESSION_PAUSE_MS = 5 * 60_000;
|
|
29
25
|
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Helpers
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
function userIdToSessionUUID(userId: string): string {
|
|
35
|
-
const hash = createHash('md5').update(`cc2wechat:${userId}`).digest('hex');
|
|
36
|
-
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function extractText(msg: WeixinMessage): string {
|
|
40
|
-
const parts: string[] = [];
|
|
41
|
-
for (const item of msg.item_list ?? []) {
|
|
42
|
-
if (item.type === MessageItemType.TEXT && item.text_item?.text) {
|
|
43
|
-
parts.push(item.text_item.text);
|
|
44
|
-
} else if (item.type === MessageItemType.IMAGE) {
|
|
45
|
-
parts.push('[Image]');
|
|
46
|
-
} else if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
|
|
47
|
-
parts.push(`[Voice] ${item.voice_item.text}`);
|
|
48
|
-
} else if (item.type === MessageItemType.FILE && item.file_item?.file_name) {
|
|
49
|
-
parts.push(`[File: ${item.file_item.file_name}]`);
|
|
50
|
-
} else if (item.type === MessageItemType.VIDEO) {
|
|
51
|
-
parts.push('[Video]');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return parts.join('\n') || '[Empty message]';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
26
|
function sleep(ms: number): Promise<void> {
|
|
58
27
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
59
28
|
}
|
|
60
29
|
|
|
61
30
|
// ---------------------------------------------------------------------------
|
|
62
|
-
//
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
// Maintain tab state per WeChat user
|
|
66
|
-
const userTabs = new Map<string, string>(); // userId -> tabName
|
|
67
|
-
|
|
68
|
-
// Track tab IDs - persisted to file so it survives daemon restart
|
|
69
|
-
const TAB_REGISTRY_PATH = '/tmp/cc2wechat-tabs.json';
|
|
70
|
-
const tabSessionIds = new Map<string, string>(); // tabName -> iTerm session id
|
|
71
|
-
|
|
72
|
-
// Load persisted tab registry on startup
|
|
73
|
-
try {
|
|
74
|
-
if (fs.existsSync(TAB_REGISTRY_PATH)) {
|
|
75
|
-
const data = JSON.parse(fs.readFileSync(TAB_REGISTRY_PATH, 'utf-8'));
|
|
76
|
-
for (const [k, v] of Object.entries(data)) {
|
|
77
|
-
tabSessionIds.set(k, v as string);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch {}
|
|
81
|
-
|
|
82
|
-
function saveTabRegistry(): void {
|
|
83
|
-
fs.writeFileSync(TAB_REGISTRY_PATH, JSON.stringify(Object.fromEntries(tabSessionIds)));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function tabExists(tabName: string): boolean {
|
|
87
|
-
const windowId = tabSessionIds.get(tabName);
|
|
88
|
-
if (!windowId) return false;
|
|
89
|
-
try {
|
|
90
|
-
const result = execSync(`osascript -e '
|
|
91
|
-
tell application "iTerm2"
|
|
92
|
-
try
|
|
93
|
-
set w to (first window whose id is ${windowId})
|
|
94
|
-
return "found"
|
|
95
|
-
on error
|
|
96
|
-
return "not_found"
|
|
97
|
-
end try
|
|
98
|
-
end tell
|
|
99
|
-
'`, { encoding: 'utf-8' }).trim();
|
|
100
|
-
return result === 'found';
|
|
101
|
-
} catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function createTabAndStartCC(tabName: string, ccSessionId: string, cwd: string): void {
|
|
107
|
-
// Create NEW WINDOW and capture window ID (same approach as cc-mesh)
|
|
108
|
-
const windowId = execSync(`osascript -e '
|
|
109
|
-
tell application "iTerm2"
|
|
110
|
-
set w to (create window with default profile)
|
|
111
|
-
tell current session of w
|
|
112
|
-
write text "cd ${cwd} && claude --resume ${ccSessionId} --dangerously-skip-permissions"
|
|
113
|
-
end tell
|
|
114
|
-
return id of w
|
|
115
|
-
end tell
|
|
116
|
-
'`, { encoding: 'utf-8' }).trim();
|
|
117
|
-
tabSessionIds.set(tabName, windowId);
|
|
118
|
-
saveTabRegistry();
|
|
119
|
-
console.log(`[cc2wechat] window created: ${tabName} -> window id: ${windowId}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function injectMessage(tabName: string, message: string): void {
|
|
123
|
-
const windowId = tabSessionIds.get(tabName);
|
|
124
|
-
if (!windowId) return;
|
|
125
|
-
const escaped = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
126
|
-
execSync(`osascript -e '
|
|
127
|
-
tell application "iTerm2"
|
|
128
|
-
tell current session of (first window whose id is ${windowId})
|
|
129
|
-
write text "${escaped}"
|
|
130
|
-
end tell
|
|
131
|
-
end tell
|
|
132
|
-
'`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// Core message handler
|
|
31
|
+
// Core message handler — routes to platform-specific handler
|
|
137
32
|
// ---------------------------------------------------------------------------
|
|
138
33
|
|
|
139
34
|
async function handleMessage(msg: WeixinMessage, account: AccountData): Promise<void> {
|
|
140
35
|
const text = extractText(msg);
|
|
141
36
|
const userId = msg.from_user_id ?? '';
|
|
142
37
|
const contextToken = msg.context_token ?? '';
|
|
143
|
-
const sessionId = userIdToSessionUUID(userId);
|
|
144
|
-
const tabName = `wechat-${userId.slice(0, 8)}`;
|
|
145
|
-
const cwd = process.cwd();
|
|
146
38
|
|
|
147
39
|
console.log(`[cc2wechat] <- ${userId.slice(0, 10)}...: ${text.slice(0, 50)}`);
|
|
148
40
|
|
|
@@ -165,53 +57,9 @@ async function handleMessage(msg: WeixinMessage, account: AccountData): Promise<
|
|
|
165
57
|
}
|
|
166
58
|
|
|
167
59
|
if (IS_MACOS) {
|
|
168
|
-
|
|
169
|
-
if (tabExists(tabName)) {
|
|
170
|
-
console.log(`[cc2wechat] -> inject to existing window: ${tabName}`);
|
|
171
|
-
injectMessage(tabName, `[微信] ${text}`);
|
|
172
|
-
} else {
|
|
173
|
-
console.log(`[cc2wechat] -> creating window: ${tabName} (session: ${sessionId})`);
|
|
174
|
-
createTabAndStartCC(tabName, sessionId, cwd);
|
|
175
|
-
await sleep(5000);
|
|
176
|
-
injectMessage(tabName, `[微信] ${text}`);
|
|
177
|
-
}
|
|
178
|
-
userTabs.set(userId, tabName);
|
|
60
|
+
await handleMessageTerminal(msg, account);
|
|
179
61
|
} else {
|
|
180
|
-
|
|
181
|
-
console.log(`[cc2wechat] -> pipe mode: ${text.slice(0, 30)}...`);
|
|
182
|
-
const prompt = JSON.stringify(text);
|
|
183
|
-
const systemPrompt = JSON.stringify(`You are responding to a WeChat message. Keep replies concise. Use this to reply: node ${replyCli} --text "reply" or node ${replyCli} --image /path/to/file`);
|
|
184
|
-
let result: string;
|
|
185
|
-
try {
|
|
186
|
-
result = execSync(
|
|
187
|
-
`claude -p ${prompt} --resume ${sessionId} --output-format text --permission-mode bypassPermissions --system-prompt ${systemPrompt}`,
|
|
188
|
-
{ encoding: 'utf-8', timeout: 120_000, maxBuffer: 10 * 1024 * 1024, cwd },
|
|
189
|
-
).trim();
|
|
190
|
-
} catch (err: unknown) {
|
|
191
|
-
const execErr = err as { stdout?: string; stderr?: string; message?: string };
|
|
192
|
-
if (!execErr.stdout?.trim()) {
|
|
193
|
-
try {
|
|
194
|
-
result = execSync(
|
|
195
|
-
`claude -p ${prompt} --session-id ${sessionId} --output-format text --permission-mode bypassPermissions --system-prompt ${systemPrompt}`,
|
|
196
|
-
{ encoding: 'utf-8', timeout: 120_000, maxBuffer: 10 * 1024 * 1024, cwd },
|
|
197
|
-
).trim();
|
|
198
|
-
} catch (err2: unknown) {
|
|
199
|
-
const execErr2 = err2 as { stdout?: string; message?: string };
|
|
200
|
-
result = execErr2.stdout?.trim() || `Error: ${execErr2.message ?? 'unknown'}`;
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
result = execErr.stdout.trim();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Auto-send text reply
|
|
207
|
-
const { sendMessage: sendMsg } = await import('./wechat-api.js');
|
|
208
|
-
const plain = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, c: string) => c.trim())
|
|
209
|
-
.replace(/\*\*(.+?)\*\*/g, '$1').replace(/^#{1,6}\s+/gm, '').trim();
|
|
210
|
-
const chunks = plain.length <= 3900 ? [plain] : [plain.slice(0, 3900), plain.slice(3900)];
|
|
211
|
-
for (const chunk of chunks) {
|
|
212
|
-
await sendMsg(account.token, userId, chunk, contextToken, account.baseUrl);
|
|
213
|
-
}
|
|
214
|
-
console.log(`[cc2wechat] -> replied (${chunks.length} chunk)`);
|
|
62
|
+
await handleMessagePipe(msg, account);
|
|
215
63
|
}
|
|
216
64
|
}
|
|
217
65
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import type { WeixinMessage } from '../types.js';
|
|
6
|
+
import type { AccountData } from '../store.js';
|
|
7
|
+
import { extractText, userIdToSessionUUID } from '../utils.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const replyCli = path.join(__dirname, '..', 'reply-cli.js');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Pipe mode message handler (Windows/Linux)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export async function handleMessagePipe(msg: WeixinMessage, account: AccountData): Promise<void> {
|
|
17
|
+
const text = extractText(msg);
|
|
18
|
+
const userId = msg.from_user_id ?? '';
|
|
19
|
+
const sessionId = userIdToSessionUUID(userId);
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
|
|
22
|
+
console.log(`[cc2wechat] -> pipe mode: ${text.slice(0, 30)}...`);
|
|
23
|
+
const prompt = JSON.stringify(text);
|
|
24
|
+
const systemPrompt = JSON.stringify(`You are responding to a WeChat message. Keep replies concise. Use this to reply: node ${replyCli} --text "reply" or node ${replyCli} --image /path/to/file`);
|
|
25
|
+
let result: string;
|
|
26
|
+
try {
|
|
27
|
+
result = execSync(
|
|
28
|
+
`claude -p ${prompt} --resume ${sessionId} --output-format text --permission-mode bypassPermissions --system-prompt ${systemPrompt}`,
|
|
29
|
+
{ encoding: 'utf-8', timeout: 120_000, maxBuffer: 10 * 1024 * 1024, cwd },
|
|
30
|
+
).trim();
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
const execErr = err as { stdout?: string; stderr?: string; message?: string };
|
|
33
|
+
if (!execErr.stdout?.trim()) {
|
|
34
|
+
try {
|
|
35
|
+
result = execSync(
|
|
36
|
+
`claude -p ${prompt} --session-id ${sessionId} --output-format text --permission-mode bypassPermissions --system-prompt ${systemPrompt}`,
|
|
37
|
+
{ encoding: 'utf-8', timeout: 120_000, maxBuffer: 10 * 1024 * 1024, cwd },
|
|
38
|
+
).trim();
|
|
39
|
+
} catch (err2: unknown) {
|
|
40
|
+
const execErr2 = err2 as { stdout?: string; message?: string };
|
|
41
|
+
result = execErr2.stdout?.trim() || `Error: ${execErr2.message ?? 'unknown'}`;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
result = execErr.stdout.trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Auto-send text reply
|
|
48
|
+
const { sendMessage: sendMsg } = await import('../wechat-api.js');
|
|
49
|
+
const plain = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, c: string) => c.trim())
|
|
50
|
+
.replace(/\*\*(.+?)\*\*/g, '$1').replace(/^#{1,6}\s+/gm, '').trim();
|
|
51
|
+
const chunks = plain.length <= 3900 ? [plain] : [plain.slice(0, 3900), plain.slice(3900)];
|
|
52
|
+
for (const chunk of chunks) {
|
|
53
|
+
await sendMsg(account.token, userId, chunk, msg.context_token ?? '', account.baseUrl);
|
|
54
|
+
}
|
|
55
|
+
console.log(`[cc2wechat] -> replied (${chunks.length} chunk)`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import type { WeixinMessage } from '../types.js';
|
|
5
|
+
import type { AccountData } from '../store.js';
|
|
6
|
+
import { getConfig, sendTyping } from '../wechat-api.js';
|
|
7
|
+
import { extractText, userIdToSessionUUID, sleep } from '../utils.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// iTerm Tab Management
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
// Maintain tab state per WeChat user
|
|
14
|
+
const userTabs = new Map<string, string>(); // userId -> tabName
|
|
15
|
+
|
|
16
|
+
// Track tab IDs - persisted to file so it survives daemon restart
|
|
17
|
+
const TAB_REGISTRY_PATH = '/tmp/cc2wechat-tabs.json';
|
|
18
|
+
const tabSessionIds = new Map<string, string>(); // tabName -> iTerm window id
|
|
19
|
+
|
|
20
|
+
// Load persisted tab registry on startup
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(TAB_REGISTRY_PATH)) {
|
|
23
|
+
const data = JSON.parse(fs.readFileSync(TAB_REGISTRY_PATH, 'utf-8'));
|
|
24
|
+
for (const [k, v] of Object.entries(data)) {
|
|
25
|
+
tabSessionIds.set(k, v as string);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
function saveTabRegistry(): void {
|
|
31
|
+
fs.writeFileSync(TAB_REGISTRY_PATH, JSON.stringify(Object.fromEntries(tabSessionIds)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tabExists(tabName: string): boolean {
|
|
35
|
+
const windowId = tabSessionIds.get(tabName);
|
|
36
|
+
if (!windowId) return false;
|
|
37
|
+
try {
|
|
38
|
+
const result = execSync(`osascript -e '
|
|
39
|
+
tell application "iTerm2"
|
|
40
|
+
try
|
|
41
|
+
set w to (first window whose id is ${windowId})
|
|
42
|
+
return "found"
|
|
43
|
+
on error
|
|
44
|
+
return "not_found"
|
|
45
|
+
end try
|
|
46
|
+
end tell
|
|
47
|
+
'`, { encoding: 'utf-8' }).trim();
|
|
48
|
+
return result === 'found';
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createTabAndStartCC(tabName: string, ccSessionId: string, cwd: string): void {
|
|
55
|
+
// Create NEW WINDOW and capture window ID (same approach as cc-mesh)
|
|
56
|
+
const windowId = execSync(`osascript -e '
|
|
57
|
+
tell application "iTerm2"
|
|
58
|
+
set w to (create window with default profile)
|
|
59
|
+
tell current session of w
|
|
60
|
+
write text "cd ${cwd} && claude --resume ${ccSessionId} --dangerously-skip-permissions"
|
|
61
|
+
end tell
|
|
62
|
+
return id of w
|
|
63
|
+
end tell
|
|
64
|
+
'`, { encoding: 'utf-8' }).trim();
|
|
65
|
+
tabSessionIds.set(tabName, windowId);
|
|
66
|
+
saveTabRegistry();
|
|
67
|
+
console.log(`[cc2wechat] window created: ${tabName} -> window id: ${windowId}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function injectMessage(tabName: string, message: string): void {
|
|
71
|
+
const windowId = tabSessionIds.get(tabName);
|
|
72
|
+
if (!windowId) return;
|
|
73
|
+
const escaped = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
74
|
+
execSync(`osascript -e '
|
|
75
|
+
tell application "iTerm2"
|
|
76
|
+
tell current session of (first window whose id is ${windowId})
|
|
77
|
+
write text "${escaped}"
|
|
78
|
+
end tell
|
|
79
|
+
end tell
|
|
80
|
+
'`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Terminal mode message handler
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export async function handleMessageTerminal(msg: WeixinMessage, account: AccountData): Promise<void> {
|
|
88
|
+
const text = extractText(msg);
|
|
89
|
+
const userId = msg.from_user_id ?? '';
|
|
90
|
+
const sessionId = userIdToSessionUUID(userId);
|
|
91
|
+
const tabName = `wechat-${userId.slice(0, 8)}`;
|
|
92
|
+
const cwd = process.cwd();
|
|
93
|
+
|
|
94
|
+
if (tabExists(tabName)) {
|
|
95
|
+
console.log(`[cc2wechat] -> inject to existing window: ${tabName}`);
|
|
96
|
+
injectMessage(tabName, `[微信] ${text}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(`[cc2wechat] -> creating window: ${tabName} (session: ${sessionId})`);
|
|
99
|
+
createTabAndStartCC(tabName, sessionId, cwd);
|
|
100
|
+
await sleep(5000);
|
|
101
|
+
injectMessage(tabName, `[微信] ${text}`);
|
|
102
|
+
}
|
|
103
|
+
userTabs.set(userId, tabName);
|
|
104
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { WeixinMessage } from './types.js';
|
|
3
|
+
import { MessageItemType } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function userIdToSessionUUID(userId: string): string {
|
|
6
|
+
const hash = createHash('md5').update(`cc2wechat:${userId}`).digest('hex');
|
|
7
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function extractText(msg: WeixinMessage): string {
|
|
11
|
+
const parts: string[] = [];
|
|
12
|
+
for (const item of msg.item_list ?? []) {
|
|
13
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text) {
|
|
14
|
+
parts.push(item.text_item.text);
|
|
15
|
+
} else if (item.type === MessageItemType.IMAGE) {
|
|
16
|
+
parts.push('[Image]');
|
|
17
|
+
} else if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
|
|
18
|
+
parts.push(`[Voice] ${item.voice_item.text}`);
|
|
19
|
+
} else if (item.type === MessageItemType.FILE && item.file_item?.file_name) {
|
|
20
|
+
parts.push(`[File: ${item.file_item.file_name}]`);
|
|
21
|
+
} else if (item.type === MessageItemType.VIDEO) {
|
|
22
|
+
parts.push('[Video]');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return parts.join('\n') || '[Empty message]';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sleep(ms: number): Promise<void> {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|