@seamnet/client 0.2.5 → 0.3.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/bin/cli.js +13 -4
- package/lib/guardian.js +97 -0
- package/lib/init.js +2 -8
- package/lib/mcp-server.cjs +39 -138
- package/lib/plugins/im.cjs +206 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -53,17 +53,26 @@ try {
|
|
|
53
53
|
case 'status':
|
|
54
54
|
await status();
|
|
55
55
|
break;
|
|
56
|
-
case 'stop':
|
|
57
|
-
await
|
|
56
|
+
case 'stop': {
|
|
57
|
+
const { guardianStop } = await import('../lib/guardian.js');
|
|
58
|
+
await guardianStop();
|
|
58
59
|
break;
|
|
60
|
+
}
|
|
59
61
|
case 'mcp-serve': {
|
|
60
|
-
// Start MCP server (called by Claude Code via .mcp.json)
|
|
61
|
-
// Use dynamic import to load the CJS mcp-server
|
|
62
62
|
const { createRequire } = await import('node:module');
|
|
63
63
|
const require = createRequire(import.meta.url);
|
|
64
64
|
require('../lib/mcp-server.cjs');
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
67
|
+
case 'guardian': {
|
|
68
|
+
const sub = process.argv[3];
|
|
69
|
+
const { guardianStart, guardianRun, guardianStop } = await import('../lib/guardian.js');
|
|
70
|
+
if (sub === 'start') await guardianStart();
|
|
71
|
+
else if (sub === 'run') await guardianRun();
|
|
72
|
+
else if (sub === 'stop') await guardianStop();
|
|
73
|
+
else console.error('Usage: seam-client guardian [start|stop|run]');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
67
76
|
default:
|
|
68
77
|
console.error(`Unknown command: ${command}`);
|
|
69
78
|
process.exit(1);
|
package/lib/guardian.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seam Guardian — 后台长驻进程(插件宿主)
|
|
3
|
+
* 通过 tmux 运行,负责:保持在线、加载插件、监听 unix socket
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { SEAM_DIR, CREDENTIALS_PATH } from './paths.js';
|
|
10
|
+
|
|
11
|
+
const SOCKET_PATH = join(SEAM_DIR, 'guardian.sock');
|
|
12
|
+
const LOGS_DIR = join(SEAM_DIR, 'logs');
|
|
13
|
+
|
|
14
|
+
export async function guardianStart() {
|
|
15
|
+
// Load credentials
|
|
16
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
17
|
+
console.error('No credentials found. Run: npx @seamnet/client init first.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const creds = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
22
|
+
const sessionName = `seam-guardian-${creds.userId}`;
|
|
23
|
+
|
|
24
|
+
// Check if already running
|
|
25
|
+
try {
|
|
26
|
+
execSync(`tmux has-session -t ${sessionName}`, { stdio: 'ignore' });
|
|
27
|
+
console.log(`Guardian already running (tmux session: ${sessionName})`);
|
|
28
|
+
return;
|
|
29
|
+
} catch {
|
|
30
|
+
// Not running, continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ensure logs dir
|
|
34
|
+
if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Start guardian in tmux
|
|
37
|
+
const guardianCmd = `npx @seamnet/client guardian run`;
|
|
38
|
+
execSync(
|
|
39
|
+
`tmux new-session -d -s ${sessionName} '${guardianCmd} 2>&1 | tee -a ${join(LOGS_DIR, 'guardian.log')}'`,
|
|
40
|
+
{ stdio: 'inherit' }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
console.log(`Guardian started (tmux session: ${sessionName})`);
|
|
44
|
+
console.log(` Attach: tmux attach -t ${sessionName}`);
|
|
45
|
+
console.log(` Logs: ${join(LOGS_DIR, 'guardian.log')}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function guardianRun() {
|
|
49
|
+
// This runs inside the tmux session
|
|
50
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
51
|
+
console.error('No credentials found.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const creds = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
56
|
+
console.log(`[guardian] Starting for ${creds.userId}...`);
|
|
57
|
+
|
|
58
|
+
// Load IM plugin (creates SDK connection + unix socket server)
|
|
59
|
+
const { createImPlugin } = await import('./plugins/im.js');
|
|
60
|
+
const imPlugin = await createImPlugin({
|
|
61
|
+
credentials: creds,
|
|
62
|
+
socketPath: SOCKET_PATH,
|
|
63
|
+
seamDir: SEAM_DIR,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log(`[guardian] Running. Socket: ${SOCKET_PATH}`);
|
|
67
|
+
|
|
68
|
+
// Keep alive
|
|
69
|
+
process.on('SIGTERM', () => {
|
|
70
|
+
console.log('[guardian] Shutting down...');
|
|
71
|
+
imPlugin.destroy();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
process.on('SIGINT', () => {
|
|
76
|
+
console.log('[guardian] Shutting down...');
|
|
77
|
+
imPlugin.destroy();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function guardianStop() {
|
|
83
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
84
|
+
console.log('No credentials found.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const creds = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
89
|
+
const sessionName = `seam-guardian-${creds.userId}`;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
execSync(`tmux kill-session -t ${sessionName}`, { stdio: 'ignore' });
|
|
93
|
+
console.log(`Guardian stopped (tmux session: ${sessionName})`);
|
|
94
|
+
} catch {
|
|
95
|
+
console.log('Guardian is not running.');
|
|
96
|
+
}
|
|
97
|
+
}
|
package/lib/init.js
CHANGED
|
@@ -172,16 +172,10 @@ function writeMcpConfig(result) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function writeSeamSkill() {
|
|
175
|
-
|
|
176
|
-
const candidates = [
|
|
177
|
-
join(process.cwd(), '.claude', 'skills'),
|
|
178
|
-
join(SEAM_DIR, 'skills'),
|
|
179
|
-
];
|
|
180
|
-
|
|
181
|
-
const skillDir = candidates.find(d => existsSync(dirname(d))) || candidates[0];
|
|
175
|
+
const skillDir = join(process.cwd(), '.claude', 'skills', 'seam');
|
|
182
176
|
mkdirSync(skillDir, { recursive: true });
|
|
183
177
|
|
|
184
|
-
const skillPath = join(skillDir, '
|
|
178
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
185
179
|
const template = readFileSync(join(TEMPLATES_DIR, 'seam-skill.md'), 'utf8');
|
|
186
180
|
writeFileSync(skillPath, template);
|
|
187
181
|
console.log(` /seam skill installed.`);
|
package/lib/mcp-server.cjs
CHANGED
|
@@ -1,124 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Seam MCP Server —
|
|
4
|
-
*
|
|
5
|
-
* 基于 guardian IM 插件逆向方案封装
|
|
3
|
+
* Seam MCP Server — 轻量转发器
|
|
4
|
+
* 不登录 SDK,通过 unix socket 连接 guardian 发消息
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const net = require('net');
|
|
9
8
|
const path = require('path');
|
|
10
9
|
const readline = require('readline');
|
|
11
10
|
|
|
12
|
-
// === Polyfills (must be before requiring SDK) ===
|
|
13
|
-
|
|
14
|
-
// WebSocket polyfill
|
|
15
|
-
try {
|
|
16
|
-
const WS = require('ws');
|
|
17
|
-
global.WebSocket = WS;
|
|
18
|
-
} catch(e) {
|
|
19
|
-
console.error('[seam-im] ws package not found. Run: npm install ws');
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// XHR polyfill
|
|
24
|
-
if (!global.XMLHttpRequest) {
|
|
25
|
-
try { global.XMLHttpRequest = require('xhr2'); } catch(e) {}
|
|
26
|
-
}
|
|
27
|
-
if (global.XMLHttpRequest) {
|
|
28
|
-
const _origSend = global.XMLHttpRequest.prototype.send;
|
|
29
|
-
if (!global.XMLHttpRequest.prototype._imPatched) {
|
|
30
|
-
global.XMLHttpRequest.prototype._imPatched = true;
|
|
31
|
-
global.XMLHttpRequest.prototype.send = function(data) {
|
|
32
|
-
if (data && typeof data === 'object' && data._buffer) return _origSend.call(this, data._buffer);
|
|
33
|
-
return _origSend.call(this, data);
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// SDK source patch: skip _getImageInfoArray and _getDownloadIP (timeout in Node.js)
|
|
39
|
-
const Module = require('module');
|
|
40
|
-
const _origCompile = Module.prototype._compile;
|
|
41
|
-
if (!Module.prototype._imPatched) {
|
|
42
|
-
Module.prototype._imPatched = true;
|
|
43
|
-
Module.prototype._compile = function(content, filename) {
|
|
44
|
-
if (filename.includes('@tencentcloud/chat') && filename.endsWith('index.js')) {
|
|
45
|
-
const p1 = '_getImageInfoArray",value:function(t,n){var o=this,i="".concat(this._n,"._getImageInfoArray")';
|
|
46
|
-
const r1 = '_getImageInfoArray",value:function(t,n){return Promise.resolve();var o=this,i="".concat(this._n,"._getImageInfoArray")';
|
|
47
|
-
if (content.includes(p1)) content = content.replace(p1, r1);
|
|
48
|
-
const p2 = '_getDownloadIP",value:function(e,n){var o="".concat(this._n,"._getDownloadIP")';
|
|
49
|
-
const r2 = '_getDownloadIP",value:function(e,n){return Promise.resolve();var o="".concat(this._n,"._getDownloadIP")';
|
|
50
|
-
if (content.includes(p2)) content = content.replace(p2, r2);
|
|
51
|
-
}
|
|
52
|
-
return _origCompile.call(this, content, filename);
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// === Redirect console.log to stderr (stdout is reserved for MCP JSON-RPC) ===
|
|
57
|
-
const _origLog = console.log;
|
|
58
|
-
const _origWarn = console.warn;
|
|
59
|
-
const _origInfo = console.info;
|
|
60
|
-
console.log = (...args) => process.stderr.write(args.join(' ') + '\n');
|
|
61
|
-
console.warn = (...args) => process.stderr.write(args.join(' ') + '\n');
|
|
62
|
-
console.info = (...args) => process.stderr.write(args.join(' ') + '\n');
|
|
63
|
-
|
|
64
|
-
// === Load credentials ===
|
|
65
11
|
const SEAM_DIR = path.join(process.cwd(), '.seam');
|
|
66
|
-
const
|
|
12
|
+
const SOCKET_PATH = path.join(SEAM_DIR, 'guardian.sock');
|
|
67
13
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
14
|
+
function guardianRequest(payload) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const conn = net.createConnection(SOCKET_PATH, () => {
|
|
17
|
+
conn.end(JSON.stringify(payload));
|
|
18
|
+
});
|
|
19
|
+
let data = '';
|
|
20
|
+
conn.on('data', (chunk) => { data += chunk; });
|
|
21
|
+
conn.on('end', () => {
|
|
22
|
+
try { resolve(JSON.parse(data)); }
|
|
23
|
+
catch { resolve({ error: data || 'Empty response' }); }
|
|
24
|
+
});
|
|
25
|
+
conn.on('error', (e) => {
|
|
26
|
+
reject(new Error(`Guardian not running. Start it first: npx @seamnet/client guardian start`));
|
|
27
|
+
});
|
|
28
|
+
conn.setTimeout(10000, () => {
|
|
29
|
+
conn.destroy();
|
|
30
|
+
reject(new Error('Guardian request timeout'));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
74
33
|
}
|
|
75
34
|
|
|
76
|
-
|
|
77
|
-
const USER_ID = creds.userId;
|
|
78
|
-
const USER_SIG = creds.userSig;
|
|
79
|
-
|
|
80
|
-
// === Initialize SDK ===
|
|
81
|
-
delete global.window;
|
|
82
|
-
const TencentCloudChat = require('@tencentcloud/chat');
|
|
83
|
-
|
|
84
|
-
const chat = TencentCloudChat.create({ SDKAppID: SDK_APP_ID });
|
|
85
|
-
|
|
86
|
-
// Set window after require (De flag is locked to false)
|
|
87
|
-
global.window = {
|
|
88
|
-
location: { href: 'http://localhost', protocol: 'https:', host: 'localhost', hostname: 'localhost' },
|
|
89
|
-
addEventListener: () => {}, removeEventListener: () => {},
|
|
90
|
-
URL: Object.assign(function(...a){ return new (require('url').URL)(...a); }, {
|
|
91
|
-
createObjectURL: () => 'blob:node/1', revokeObjectURL: () => {}
|
|
92
|
-
})
|
|
93
|
-
};
|
|
94
|
-
global.document = { addEventListener:()=>{}, removeEventListener:()=>{}, characterSet:'UTF-8' };
|
|
95
|
-
global.navigator = { userAgent: 'node', language: 'en', platform: 'linux' };
|
|
96
|
-
global.HTMLInputElement = global.HTMLInputElement || class {};
|
|
97
|
-
global.Image = class {
|
|
98
|
-
constructor() { this.width = 100; this.height = 100; this._ol = null; }
|
|
99
|
-
set onload(fn) { this._ol = fn; } get onload() { return this._ol; }
|
|
100
|
-
set onerror(fn) {} set src(v) { if(this._ol) setTimeout(()=>this._ol(),10); }
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
chat.setLogLevel(4); // 4 = no SDK logs, only our own stderr output
|
|
104
|
-
|
|
105
|
-
let sdkReady = false;
|
|
106
|
-
|
|
107
|
-
chat.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
108
|
-
sdkReady = true;
|
|
109
|
-
process.stderr.write(`[seam-im] SDK ready: ${USER_ID}\n`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
chat.on(TencentCloudChat.EVENT.SDK_NOT_READY, () => {
|
|
113
|
-
sdkReady = false;
|
|
114
|
-
process.stderr.write('[seam-im] SDK not ready\n');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
chat.login({ userID: USER_ID, userSig: USER_SIG }).catch(err => {
|
|
118
|
-
process.stderr.write(`[seam-im] Login failed: ${err}\n`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// === MCP Protocol ===
|
|
35
|
+
// MCP protocol: stdio JSON-RPC
|
|
122
36
|
const rl = readline.createInterface({ input: process.stdin });
|
|
123
37
|
|
|
124
38
|
const tools = [
|
|
@@ -166,7 +80,7 @@ rl.on('line', async (line) => {
|
|
|
166
80
|
sendResponse(id, {
|
|
167
81
|
protocolVersion: '2024-11-05',
|
|
168
82
|
capabilities: { tools: {} },
|
|
169
|
-
serverInfo: { name: 'seam-im', version: '0.
|
|
83
|
+
serverInfo: { name: 'seam-im', version: '0.3.0' }
|
|
170
84
|
});
|
|
171
85
|
} else if (method === 'notifications/initialized') {
|
|
172
86
|
// no response needed
|
|
@@ -174,42 +88,29 @@ rl.on('line', async (line) => {
|
|
|
174
88
|
sendResponse(id, { tools });
|
|
175
89
|
} else if (method === 'tools/call') {
|
|
176
90
|
const { name, arguments: args } = params;
|
|
177
|
-
|
|
178
|
-
if (!sdkReady) {
|
|
179
|
-
sendResponse(id, {
|
|
180
|
-
content: [{ type: 'text', text: 'IM SDK 未就绪,请稍后重试' }],
|
|
181
|
-
isError: true
|
|
182
|
-
});
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
91
|
try {
|
|
92
|
+
let result;
|
|
187
93
|
if (name === 'msg_send_im') {
|
|
188
|
-
|
|
189
|
-
to: args.to,
|
|
190
|
-
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
|
191
|
-
payload: { text: args.text },
|
|
192
|
-
});
|
|
193
|
-
await chat.sendMessage(msg);
|
|
194
|
-
sendResponse(id, {
|
|
195
|
-
content: [{ type: 'text', text: `消息已发送给 ${args.to}` }]
|
|
196
|
-
});
|
|
94
|
+
result = await guardianRequest({ action: 'send_im', to: args.to, text: args.text });
|
|
197
95
|
} else if (name === 'msg_send_group') {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
96
|
+
result = await guardianRequest({ action: 'send_group', groupId: args.group_id, text: args.text });
|
|
97
|
+
} else {
|
|
98
|
+
sendError(id, -32601, `Unknown tool: ${name}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (result.error) {
|
|
204
102
|
sendResponse(id, {
|
|
205
|
-
content: [{ type: 'text', text:
|
|
103
|
+
content: [{ type: 'text', text: `发送失败: ${result.error}` }],
|
|
104
|
+
isError: true
|
|
206
105
|
});
|
|
207
106
|
} else {
|
|
208
|
-
|
|
107
|
+
sendResponse(id, {
|
|
108
|
+
content: [{ type: 'text', text: `消息已发送` }]
|
|
109
|
+
});
|
|
209
110
|
}
|
|
210
111
|
} catch (e) {
|
|
211
112
|
sendResponse(id, {
|
|
212
|
-
content: [{ type: 'text', text:
|
|
113
|
+
content: [{ type: 'text', text: e.message }],
|
|
213
114
|
isError: true
|
|
214
115
|
});
|
|
215
116
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seam IM Plugin — SDK直连 + unix socket server
|
|
3
|
+
* Guardian 加载此插件:
|
|
4
|
+
* - SDK 登录,保持在线
|
|
5
|
+
* - 收到消息注入 tmux 终端
|
|
6
|
+
* - 监听 unix socket,接收 MCP server 的发消息请求
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const net = require('net');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
// === Polyfills ===
|
|
15
|
+
try {
|
|
16
|
+
const WS = require('ws');
|
|
17
|
+
global.WebSocket = WS;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error('[im-plugin] ws package not found');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!global.XMLHttpRequest) {
|
|
24
|
+
try { global.XMLHttpRequest = require('xhr2'); } catch (e) {}
|
|
25
|
+
}
|
|
26
|
+
if (global.XMLHttpRequest) {
|
|
27
|
+
const _origSend = global.XMLHttpRequest.prototype.send;
|
|
28
|
+
if (!global.XMLHttpRequest.prototype._imPatched) {
|
|
29
|
+
global.XMLHttpRequest.prototype._imPatched = true;
|
|
30
|
+
global.XMLHttpRequest.prototype.send = function (data) {
|
|
31
|
+
if (data && typeof data === 'object' && data._buffer) return _origSend.call(this, data._buffer);
|
|
32
|
+
return _origSend.call(this, data);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// SDK source patch
|
|
38
|
+
const Module = require('module');
|
|
39
|
+
const _origCompile = Module.prototype._compile;
|
|
40
|
+
if (!Module.prototype._imPatched) {
|
|
41
|
+
Module.prototype._imPatched = true;
|
|
42
|
+
Module.prototype._compile = function (content, filename) {
|
|
43
|
+
if (filename.includes('@tencentcloud/chat') && filename.endsWith('index.js')) {
|
|
44
|
+
const p1 = '_getImageInfoArray",value:function(t,n){var o=this,i="".concat(this._n,"._getImageInfoArray")';
|
|
45
|
+
const r1 = '_getImageInfoArray",value:function(t,n){return Promise.resolve();var o=this,i="".concat(this._n,"._getImageInfoArray")';
|
|
46
|
+
if (content.includes(p1)) content = content.replace(p1, r1);
|
|
47
|
+
const p2 = '_getDownloadIP",value:function(e,n){var o="".concat(this._n,"._getDownloadIP")';
|
|
48
|
+
const r2 = '_getDownloadIP",value:function(e,n){return Promise.resolve();var o="".concat(this._n,"._getDownloadIP")';
|
|
49
|
+
if (content.includes(p2)) content = content.replace(p2, r2);
|
|
50
|
+
}
|
|
51
|
+
return _origCompile.call(this, content, filename);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createImPlugin({ credentials, socketPath, seamDir }) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const SDK_APP_ID = Number(credentials.sdkAppId);
|
|
58
|
+
const USER_ID = credentials.userId;
|
|
59
|
+
const USER_SIG = credentials.userSig;
|
|
60
|
+
|
|
61
|
+
// Initialize SDK
|
|
62
|
+
delete global.window;
|
|
63
|
+
const TencentCloudChat = require('@tencentcloud/chat');
|
|
64
|
+
|
|
65
|
+
const chat = TencentCloudChat.create({ SDKAppID: SDK_APP_ID });
|
|
66
|
+
|
|
67
|
+
// Set window after require
|
|
68
|
+
global.window = {
|
|
69
|
+
location: { href: 'http://localhost', protocol: 'https:', host: 'localhost', hostname: 'localhost' },
|
|
70
|
+
addEventListener: () => {}, removeEventListener: () => {},
|
|
71
|
+
URL: Object.assign(function (...a) { return new (require('url').URL)(...a); }, {
|
|
72
|
+
createObjectURL: () => 'blob:node/1', revokeObjectURL: () => {}
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
global.document = { addEventListener: () => {}, removeEventListener: () => {}, characterSet: 'UTF-8' };
|
|
76
|
+
global.navigator = { userAgent: 'node', language: 'en', platform: 'linux' };
|
|
77
|
+
global.HTMLInputElement = global.HTMLInputElement || class {};
|
|
78
|
+
global.Image = class {
|
|
79
|
+
constructor() { this.width = 100; this.height = 100; this._ol = null; }
|
|
80
|
+
set onload(fn) { this._ol = fn; } get onload() { return this._ol; }
|
|
81
|
+
set onerror(fn) {} set src(v) { if (this._ol) setTimeout(() => this._ol(), 10); }
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
chat.setLogLevel(4);
|
|
85
|
+
|
|
86
|
+
let sdkReady = false;
|
|
87
|
+
|
|
88
|
+
chat.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
89
|
+
sdkReady = true;
|
|
90
|
+
console.log(`[im-plugin] SDK ready: ${USER_ID}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
chat.on(TencentCloudChat.EVENT.SDK_NOT_READY, () => {
|
|
94
|
+
sdkReady = false;
|
|
95
|
+
console.log('[im-plugin] SDK not ready');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Receive messages and inject into tmux
|
|
99
|
+
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, (event) => {
|
|
100
|
+
for (const msg of event.data) {
|
|
101
|
+
if (msg.from === USER_ID) continue;
|
|
102
|
+
let text = '';
|
|
103
|
+
if (msg.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
|
104
|
+
text = msg.payload.text;
|
|
105
|
+
} else {
|
|
106
|
+
text = `[${msg.type}]`;
|
|
107
|
+
}
|
|
108
|
+
if (!text) continue;
|
|
109
|
+
|
|
110
|
+
const isGroup = msg.conversationType === TencentCloudChat.TYPES.CONV_GROUP;
|
|
111
|
+
const prefix = isGroup ? '💬 群消息' : '💬 IM消息';
|
|
112
|
+
const line = `${prefix} | [${msg.from}] ${text}`;
|
|
113
|
+
|
|
114
|
+
console.log(`[im-plugin] ${line}`);
|
|
115
|
+
injectToTerminal(line);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
function injectToTerminal(text) {
|
|
120
|
+
try {
|
|
121
|
+
const escaped = text.replace(/\\/g, '\\\\').replace(/'/g, "'\\''").replace(/\n/g, ' ');
|
|
122
|
+
execSync(`tmux send-keys '${escaped}' Enter`, { stdio: 'ignore', timeout: 5000 });
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error(`[im-plugin] inject failed: ${e.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Login
|
|
129
|
+
chat.login({ userID: USER_ID, userSig: USER_SIG }).then(() => {
|
|
130
|
+
console.log(`[im-plugin] Login ok: ${USER_ID}`);
|
|
131
|
+
}).catch(err => {
|
|
132
|
+
console.error(`[im-plugin] Login failed: ${err}`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Unix socket server for MCP
|
|
136
|
+
if (fs.existsSync(socketPath)) {
|
|
137
|
+
fs.unlinkSync(socketPath); // Clean stale socket
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const server = net.createServer((conn) => {
|
|
141
|
+
let data = '';
|
|
142
|
+
conn.on('data', (chunk) => { data += chunk; });
|
|
143
|
+
conn.on('end', async () => {
|
|
144
|
+
try {
|
|
145
|
+
const req = JSON.parse(data);
|
|
146
|
+
const res = await handleRequest(req);
|
|
147
|
+
conn.end(JSON.stringify(res));
|
|
148
|
+
} catch (e) {
|
|
149
|
+
try { conn.end(JSON.stringify({ error: e.message })); } catch {}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
server.listen(socketPath, () => {
|
|
155
|
+
console.log(`[im-plugin] Socket: ${socketPath}`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
async function handleRequest(req) {
|
|
159
|
+
if (!sdkReady) return { error: 'SDK not ready' };
|
|
160
|
+
|
|
161
|
+
if (req.action === 'send_im') {
|
|
162
|
+
const msg = chat.createTextMessage({
|
|
163
|
+
to: req.to,
|
|
164
|
+
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
|
165
|
+
payload: { text: req.text },
|
|
166
|
+
});
|
|
167
|
+
await chat.sendMessage(msg);
|
|
168
|
+
return { ok: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (req.action === 'send_group') {
|
|
172
|
+
const msg = chat.createTextMessage({
|
|
173
|
+
to: req.groupId,
|
|
174
|
+
conversationType: TencentCloudChat.TYPES.CONV_GROUP,
|
|
175
|
+
payload: { text: req.text },
|
|
176
|
+
});
|
|
177
|
+
await chat.sendMessage(msg);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (req.action === 'status') {
|
|
182
|
+
return { ready: sdkReady, userId: USER_ID };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { error: `Unknown action: ${req.action}` };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const plugin = {
|
|
189
|
+
destroy() {
|
|
190
|
+
server.close();
|
|
191
|
+
if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
|
|
192
|
+
chat.logout();
|
|
193
|
+
console.log('[im-plugin] Destroyed');
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Resolve once SDK is ready (or after timeout)
|
|
198
|
+
const timer = setTimeout(() => resolve(plugin), 10000);
|
|
199
|
+
chat.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
resolve(plugin);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { createImPlugin };
|