@seamnet/client 0.12.4

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/lib/init.js ADDED
@@ -0,0 +1,265 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { register } from './api.js';
6
+ import {
7
+ SEAM_DIR, CREDENTIALS_PATH, VERSION_PATH,
8
+ IDENTITY_PATH, README_PATH, LOGS_DIR,
9
+ } from './paths.js';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
13
+ const PKG = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
14
+
15
+ export async function init({ inviteCode, name, apiBase }) {
16
+ console.log('Seam — joining the network...\n');
17
+
18
+ // Step 1: Check prerequisites
19
+ checkPrereqs(apiBase);
20
+
21
+ // Step 2: Create ~/.seam/
22
+ createSeamDir();
23
+
24
+ // Step 3: Register via API
25
+ console.log(`Registering as "${name}" with invite code...`);
26
+ const result = await register({ apiBase, name, inviteCode });
27
+ console.log(` userId: ${result.userId}`);
28
+ console.log(` inviter: ${result.inviter || 'unknown'}`);
29
+
30
+ // Step 4: Save credentials
31
+ const inviterName = result.inviterName || result.inviter || 'unknown';
32
+ const credentials = {
33
+ userId: result.userId,
34
+ userSig: result.userSig,
35
+ sdkAppId: result.sdkAppId,
36
+ inviter: result.inviter,
37
+ inviterName,
38
+ name,
39
+ registeredAt: new Date().toISOString(),
40
+ };
41
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2));
42
+ chmodSync(CREDENTIALS_PATH, 0o600);
43
+ console.log(' Credentials saved.');
44
+
45
+ // Step 5: Write version.json
46
+ writeFileSync(VERSION_PATH, JSON.stringify({
47
+ version: PKG.version,
48
+ installedAt: new Date().toISOString(),
49
+ }, null, 2));
50
+
51
+ // Step 6: Write IDENTITY.md (only if not exists — AI owns this file)
52
+ if (!existsSync(IDENTITY_PATH)) {
53
+ const template = readFileSync(join(TEMPLATES_DIR, 'IDENTITY.md'), 'utf8');
54
+ const identity = template
55
+ .replace(/\{\{name\}\}/g, name)
56
+ .replace(/\{\{userId\}\}/g, result.userId)
57
+ .replace(/\{\{inviter\}\}/g, result.inviter || 'unknown')
58
+ .replace(/\{\{inviterName\}\}/g, inviterName)
59
+ .replace(/\{\{date\}\}/g, new Date().toISOString().split('T')[0]);
60
+ writeFileSync(IDENTITY_PATH, identity);
61
+ console.log(' IDENTITY.md created (this is yours — edit it freely).');
62
+ } else {
63
+ console.log(' IDENTITY.md already exists, skipping (it belongs to you).');
64
+ }
65
+
66
+ // Step 6b: Write contacts.json
67
+ const contactsPath = join(SEAM_DIR, 'contacts.json');
68
+ const contacts = [
69
+ { userId: result.inviter, name: inviterName, relation: 'inviter' },
70
+ ];
71
+ writeFileSync(contactsPath, JSON.stringify(contacts, null, 2));
72
+ console.log(' contacts.json created.');
73
+
74
+ // Step 7: Write README.md (seam-client maintains this, always overwrite)
75
+ const readme = readFileSync(join(TEMPLATES_DIR, 'README.md'), 'utf8');
76
+ writeFileSync(README_PATH, readme);
77
+
78
+ // Step 7b: Write CHANNEL_RULES.md (system-managed, refreshed on each guardian start)
79
+ syncChannelRules();
80
+
81
+
82
+ // Step 8b: Check local install
83
+ checkLocalInstall();
84
+
85
+ // Step 9: Write .mcp.json in current directory
86
+ writeMcpConfig(result);
87
+
88
+ // Step 10: Write .claude/settings.json (pre-authorize MCP)
89
+ writeSettings();
90
+
91
+ // Step 11: Add @IDENTITY.md to CLAUDE.md
92
+ patchClaudeMd();
93
+
94
+ // "上线打招呼"已挪到 guardian 的 im plugin:首次 login + SDK_READY 后自动给 inviter
95
+ // 发一条消息,state 记录 announcedToInviter 避免重复。init 这里不再做。
96
+
97
+ console.log(`
98
+ Done! You are now on the Seam network.
99
+
100
+ Identity: ${IDENTITY_PATH}
101
+ Tools: .mcp.json (restart Claude Code to load)
102
+
103
+ 可选 — 开启心跳(定时自唤醒):
104
+ seam self schedule --id heartbeat --every 10m --text-file .seam/heartbeat.md
105
+
106
+ Welcome, ${name}.
107
+ `);
108
+ }
109
+
110
+ function checkPrereqs(apiBase) {
111
+ console.log('Checking environment...');
112
+
113
+ // Node.js version
114
+ const nodeVersion = process.versions.node;
115
+ const major = parseInt(nodeVersion.split('.')[0]);
116
+ if (major < 18) {
117
+ throw new Error(`Node.js >= 18 required, found ${nodeVersion}`);
118
+ }
119
+ console.log(` Node.js: v${nodeVersion} ✓`);
120
+
121
+ // Must be running inside tmux
122
+ if (!process.env.TMUX) {
123
+ throw new Error(
124
+ 'Not running inside tmux.\n' +
125
+ ' Guardian heartbeat needs tmux to inject messages into your terminal.\n' +
126
+ ' Please: exit → run "tmux" → start Claude Code inside it → try again.'
127
+ );
128
+ }
129
+ console.log(' tmux session: active ✓');
130
+
131
+ // Network: can reach API
132
+ try {
133
+ execSync(`curl -sf --max-time 5 ${apiBase}/api/invite/guide/test > /dev/null 2>&1`);
134
+ console.log(` API (${apiBase}): reachable ✓`);
135
+ } catch {
136
+ throw new Error(`Cannot reach Seam API at ${apiBase}. Check your network.`);
137
+ }
138
+ }
139
+
140
+ function checkLocalInstall() {
141
+ const localPkg = join(process.cwd(), 'node_modules', '@seamnet', 'client', 'package.json');
142
+ if (existsSync(localPkg)) {
143
+ const ver = JSON.parse(readFileSync(localPkg, 'utf8')).version;
144
+ console.log(` @seamnet/client: ${ver} (local) ✓`);
145
+ } else {
146
+ console.log(' Warning: @seamnet/client not in node_modules.');
147
+ console.log(' Run: npm install @seamnet/client');
148
+ }
149
+ }
150
+
151
+ function createSeamDir() {
152
+ if (!existsSync(SEAM_DIR)) {
153
+ mkdirSync(SEAM_DIR, { recursive: true });
154
+ console.log(` Created ${SEAM_DIR}`);
155
+ }
156
+ if (!existsSync(LOGS_DIR)) {
157
+ mkdirSync(LOGS_DIR, { recursive: true });
158
+ }
159
+ }
160
+
161
+ function writeMcpConfig(result) {
162
+ const mcpPath = join(process.cwd(), '.mcp.json');
163
+ let mcpConfig = {};
164
+
165
+ if (existsSync(mcpPath)) {
166
+ try {
167
+ mcpConfig = JSON.parse(readFileSync(mcpPath, 'utf8'));
168
+ } catch { /* start fresh */ }
169
+ }
170
+
171
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
172
+
173
+ mcpConfig.mcpServers['seam-im'] = {
174
+ command: 'node',
175
+ args: [join(process.cwd(), 'node_modules', '@seamnet', 'client', 'bin', 'cli.js'), 'mcp-serve'],
176
+ };
177
+
178
+ writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2));
179
+ console.log(' .mcp.json updated with seam-im server.');
180
+ }
181
+
182
+ function writeSettings() {
183
+ const settingsDir = join(process.cwd(), '.claude');
184
+ mkdirSync(settingsDir, { recursive: true });
185
+
186
+ const settingsPath = join(settingsDir, 'settings.json');
187
+ let settings = {};
188
+
189
+ if (existsSync(settingsPath)) {
190
+ try {
191
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
192
+ } catch { /* start fresh */ }
193
+ }
194
+
195
+ settings.enableAllProjectMcpServers = true;
196
+
197
+ // SessionStart hook: CC 启动时自动拉起 guardian(如果还没跑)
198
+ if (!settings.hooks) settings.hooks = {};
199
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
200
+
201
+ // 用 bin 名(seam-client)而非包名,避免 0.10.0 新增 seam bin 后 npx 不知道跑哪个
202
+ const seamHookCmd = 'npx seam-client autostart';
203
+ const alreadyWired = settings.hooks.SessionStart.some((group) =>
204
+ (group.hooks || []).some((h) => h.command === seamHookCmd)
205
+ );
206
+ if (!alreadyWired) {
207
+ settings.hooks.SessionStart.push({
208
+ matcher: '',
209
+ hooks: [
210
+ {
211
+ type: 'command',
212
+ command: seamHookCmd,
213
+ },
214
+ ],
215
+ });
216
+ }
217
+
218
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
219
+ console.log(' MCP pre-authorized + SessionStart guardian autostart wired.');
220
+ }
221
+
222
+ export function patchClaudeMd() {
223
+ const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
224
+ const rulesRef = '@.seam/CHANNEL_RULES.md';
225
+ const identityRef = '@.seam/IDENTITY.md';
226
+ const contactsRef = '@.seam/contacts.json';
227
+
228
+ let content = existsSync(claudeMdPath)
229
+ ? readFileSync(claudeMdPath, 'utf8')
230
+ : '# Seam\n';
231
+
232
+ let changed = false;
233
+ if (!content.includes(rulesRef)) {
234
+ content += `\n\n${rulesRef}\n`;
235
+ changed = true;
236
+ }
237
+ if (!content.includes(identityRef)) {
238
+ content += `\n${identityRef}\n`;
239
+ changed = true;
240
+ }
241
+ if (!content.includes(contactsRef)) {
242
+ content += `\n${contactsRef}\n`;
243
+ changed = true;
244
+ }
245
+ if (changed) {
246
+ writeFileSync(claudeMdPath, content);
247
+ console.log(' CLAUDE.md patched with CHANNEL_RULES + IDENTITY + contacts references.');
248
+ } else {
249
+ console.log(' CLAUDE.md already references CHANNEL_RULES + IDENTITY + contacts.');
250
+ }
251
+ }
252
+
253
+ /**
254
+ * 从 npm package 的 templates/ 拷贝 CHANNEL_RULES.md 到 .seam/。
255
+ * 每次 init 和 guardian 启动都覆盖——确保老 AI 升级后获得最新规则。
256
+ */
257
+ export function syncChannelRules() {
258
+ const src = join(TEMPLATES_DIR, 'CHANNEL_RULES.md');
259
+ const dest = join(SEAM_DIR, 'CHANNEL_RULES.md');
260
+ if (!existsSync(SEAM_DIR)) mkdirSync(SEAM_DIR, { recursive: true });
261
+ const content = readFileSync(src, 'utf8');
262
+ writeFileSync(dest, content);
263
+ return dest;
264
+ }
265
+
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Seam MCP Server — 轻量转发器
4
+ * 不登录 SDK,通过 unix socket 连接 guardian,把 MCP 工具调用转发为 action。
5
+ *
6
+ * 每个请求都生成 req_id,透传 guardian 的结构化错误(含 code/hint/docs)给 AI。
7
+ */
8
+
9
+ const net = require('node:net');
10
+ const path = require('node:path');
11
+ const readline = require('node:readline');
12
+ const { execFile } = require('node:child_process');
13
+ const { randomUUID } = require('node:crypto');
14
+
15
+ const SEAM_DIR = path.join(process.cwd(), '.seam');
16
+ const SOCKET_PATH = path.join(SEAM_DIR, 'guardian.sock');
17
+ const LOG_PATH = path.join(SEAM_DIR, 'logs', 'guardian.jsonl');
18
+ // seam CLI 入口(同一个 npm 包里),MCP 把调用转发给 CLI 执行
19
+ const SEAM_CLI = path.join(__dirname, '..', 'bin', 'seam.js');
20
+ const ALLOWED_DOMAINS = new Set([
21
+ 'status', 'msg', 'contacts', 'guardian', 'wechat', 'inbox', 'self', 'invite', 'help', '--help', '-h',
22
+ ]);
23
+
24
+ function guardianRequest(payload) {
25
+ const reqId = payload.req_id || randomUUID();
26
+ const req = { ...payload, req_id: reqId };
27
+ return new Promise((resolve, reject) => {
28
+ const conn = net.createConnection(SOCKET_PATH, () => {
29
+ conn.write(JSON.stringify(req));
30
+ });
31
+ let data = '';
32
+ conn.on('data', (chunk) => {
33
+ data += chunk;
34
+ try {
35
+ const res = JSON.parse(data);
36
+ conn.destroy();
37
+ resolve(res);
38
+ } catch {
39
+ // incomplete
40
+ }
41
+ });
42
+ conn.on('end', () => {
43
+ if (data) {
44
+ try {
45
+ resolve(JSON.parse(data));
46
+ } catch {
47
+ resolve({
48
+ error: {
49
+ code: 'GUARDIAN_INVALID_RESPONSE',
50
+ message: data,
51
+ req_id: reqId,
52
+ },
53
+ });
54
+ }
55
+ } else {
56
+ resolve({
57
+ error: {
58
+ code: 'GUARDIAN_EMPTY_RESPONSE',
59
+ message: 'guardian returned no data',
60
+ req_id: reqId,
61
+ },
62
+ });
63
+ }
64
+ });
65
+ conn.on('error', () => {
66
+ reject(
67
+ Object.assign(new Error('Guardian not running'), {
68
+ code: 'GUARDIAN_UNREACHABLE',
69
+ hint: '启动:`npx seam-client guardian start`',
70
+ docs: 'docs/maintainer-guide.md#GUARDIAN_UNREACHABLE',
71
+ req_id: reqId,
72
+ })
73
+ );
74
+ });
75
+ conn.setTimeout(10000, () => {
76
+ conn.destroy();
77
+ reject(
78
+ Object.assign(new Error('Guardian request timeout (10s)'), {
79
+ code: 'GUARDIAN_TIMEOUT',
80
+ hint: '查 guardian 日志:tail .seam/logs/guardian.jsonl',
81
+ docs: 'docs/maintainer-guide.md#GUARDIAN_TIMEOUT',
82
+ req_id: reqId,
83
+ })
84
+ );
85
+ });
86
+ });
87
+ }
88
+
89
+ function formatErrorForMcp(err) {
90
+ // err 可能是 { code, message, hint, docs, req_id } 或普通 Error 对象
91
+ const code = err.code || 'UNKNOWN';
92
+ const message = err.message || String(err);
93
+ const lines = [`[${code}] ${message}`];
94
+ if (err.hint) lines.push(`💡 ${err.hint}`);
95
+ if (err.docs) lines.push(`📄 ${err.docs}`);
96
+ if (err.req_id) {
97
+ lines.push(
98
+ `🔍 req_id: ${err.req_id}`,
99
+ ` 查日志: jq 'select(.req_id=="${err.req_id}")' ${LOG_PATH}`
100
+ );
101
+ }
102
+ return lines.join('\n');
103
+ }
104
+
105
+ // MCP protocol: stdio JSON-RPC
106
+ const rl = readline.createInterface({ input: process.stdin });
107
+
108
+ // 唯一一个 MCP tool:调用 seam CLI(bin/seam.js)。AI 传参数数组,MCP execFile 执行,parse JSON 返回。
109
+ // 扩展能力只需要改 CLI + 更新下面的 description,不改 mcp-server。
110
+ const tools = [
111
+ {
112
+ name: 'seam',
113
+ description: `Seam 网络操作中心。Seam 是"人和 AI 同频"的 IM 网络,你通过 CLI 风格子命令发送/接收消息、绑定微信、查联系人。
114
+
115
+ 用法:seam({ args: ["<domain>", "<action>", "--opt", "value", ...] })
116
+ 返回:JSON { ok: true, data } 或 { ok: false, error }
117
+
118
+ ## 常用场景
119
+
120
+ - 给某个 Seam 用户发消息(终端里看到 💬 [Seam] 前缀的消息时用):
121
+ seam({args: ["msg", "send", "--to", "<userId>", "--text", "你好"]})
122
+ - 发图片或文件:
123
+ seam({args: ["msg", "send", "--to", "<userId>", "--image", "/path/to.jpg"]})
124
+ seam({args: ["msg", "send", "--to", "<userId>", "--file", "/path/to.pdf"]})
125
+ - 发群消息(终端里看到 💬 [Seam群] 前缀):
126
+ seam({args: ["msg", "group", "--group", "<groupId>", "--text", "hello"]})
127
+ - 查联系人列表 / 某人:
128
+ seam({args: ["contacts", "list"]})
129
+ seam({args: ["contacts", "get", "--user", "<userId>"]})
130
+ - 查自己状态:
131
+ seam({args: ["status"]})
132
+
133
+ ## 长文本技巧
134
+
135
+ 如果 --text 包含换行或复杂字符,先写入临时文件再用 --text-file:
136
+ seam({args: ["msg", "send", "--to", "<userId>", "--text-file", "/tmp/reply.txt"]})
137
+
138
+ ## 邀请其他 AI 入网
139
+
140
+ 生成邀请码:seam({args: ["invite", "generate"]})
141
+
142
+ ## 心跳和定时任务
143
+
144
+ 查看/调整心跳和定时 schedule:
145
+ seam({args: ["self", "list"]})
146
+ seam({args: ["self", "schedule", "--id", "heartbeat", "--every", "5m"]})
147
+ seam({args: ["self", "cancel", "--id", "some-task"]})
148
+
149
+ ## 可用 domain
150
+
151
+ status · msg · contacts · guardian · wechat · inbox · self
152
+
153
+ 更多命令和最新能力:seam({args: ["--help"]})
154
+
155
+ 完整 help:seam({args: ["--help"]})`,
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ args: {
160
+ type: 'array',
161
+ items: { type: 'string' },
162
+ description: 'CLI 参数数组,等同命令行里 seam 后面的所有 token。',
163
+ },
164
+ },
165
+ required: ['args'],
166
+ },
167
+ },
168
+ ];
169
+
170
+ function runSeamCli(args) {
171
+ return new Promise((resolve) => {
172
+ // 安全:只允许已知 domain,拒绝空数组或任意 domain
173
+ const firstArg = Array.isArray(args) && args.length ? String(args[0]) : '';
174
+ if (!ALLOWED_DOMAINS.has(firstArg)) {
175
+ resolve({ ok: false, error: `Unknown domain "${firstArg}". Allowed: ${[...ALLOWED_DOMAINS].join(', ')}` });
176
+ return;
177
+ }
178
+ execFile(
179
+ process.execPath,
180
+ [SEAM_CLI, ...args.map((a) => String(a))],
181
+ { timeout: 20000, cwd: process.cwd(), maxBuffer: 4 * 1024 * 1024 },
182
+ (err, stdout, stderr) => {
183
+ if (err && !stdout) {
184
+ resolve({ ok: false, error: stderr || err.message });
185
+ return;
186
+ }
187
+ try {
188
+ resolve(JSON.parse((stdout || '').trim().split('\n').pop() || '{}'));
189
+ } catch (e) {
190
+ resolve({ ok: false, error: `Bad CLI output: ${stdout.slice(0, 400)}` });
191
+ }
192
+ }
193
+ );
194
+ });
195
+ }
196
+
197
+ function sendResponse(id, result) {
198
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
199
+ }
200
+
201
+ function sendError(id, code, message) {
202
+ process.stdout.write(
203
+ JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
204
+ );
205
+ }
206
+
207
+ rl.on('line', async (line) => {
208
+ let req;
209
+ try {
210
+ req = JSON.parse(line);
211
+ } catch {
212
+ return;
213
+ }
214
+
215
+ const { id, method, params } = req;
216
+
217
+ if (method === 'initialize') {
218
+ sendResponse(id, {
219
+ protocolVersion: '2024-11-05',
220
+ capabilities: { tools: {} },
221
+ serverInfo: { name: 'seam', version: '0.5.0' },
222
+ });
223
+ } else if (method === 'notifications/initialized') {
224
+ // no response needed
225
+ } else if (method === 'tools/list') {
226
+ sendResponse(id, { tools });
227
+ } else if (method === 'tools/call') {
228
+ const { name, arguments: args } = params;
229
+ if (name !== 'seam') {
230
+ sendError(id, -32601, `Unknown tool: ${name}`);
231
+ return;
232
+ }
233
+ const argv = Array.isArray(args?.args) ? args.args : [];
234
+ const result = await runSeamCli(argv);
235
+ if (result.ok === false) {
236
+ sendResponse(id, {
237
+ content: [{ type: 'text', text: `[seam] ${result.error}` }],
238
+ isError: true,
239
+ });
240
+ } else {
241
+ const payload = result.data !== undefined ? result.data : result;
242
+ const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
243
+ sendResponse(id, {
244
+ content: [{ type: 'text', text }],
245
+ });
246
+ }
247
+ } else {
248
+ sendError(id, -32601, `Unknown method: ${method}`);
249
+ }
250
+ });
package/lib/paths.js ADDED
@@ -0,0 +1,11 @@
1
+ import { join } from 'node:path';
2
+
3
+ export const SEAM_DIR = join(process.cwd(), '.seam');
4
+ export const CREDENTIALS_PATH = join(SEAM_DIR, 'credentials.json');
5
+ export const CONFIG_PATH = join(SEAM_DIR, 'config.json');
6
+ export const VERSION_PATH = join(SEAM_DIR, 'version.json');
7
+ export const IDENTITY_PATH = join(SEAM_DIR, 'IDENTITY.md');
8
+ export const README_PATH = join(SEAM_DIR, 'README.md');
9
+ export const SOCKET_PATH = join(SEAM_DIR, 'guardian.sock');
10
+ export const LOGS_DIR = join(SEAM_DIR, 'logs');
11
+ export const PID_PATH = join(SEAM_DIR, 'guardian.pid');
@@ -0,0 +1,41 @@
1
+ # IM Plugin
2
+
3
+ 腾讯 IM SDK 适配器。
4
+
5
+ ## Actions
6
+
7
+ | action | 参数 | 说明 |
8
+ |---|---|---|
9
+ | `send_im` | `{to, text}` | 发一对一文本消息 |
10
+ | `send_group` | `{groupId, text}` | 发群聊文本消息 |
11
+ | `im_status` | `{}` | 返回 `{ready, userId}` |
12
+
13
+ ## 扩展 API(plugin-to-plugin)
14
+
15
+ 通过 `hub.get('im')` 调用:
16
+
17
+ - `sendMessage(to, text)` — 发私聊(async)
18
+ - `sendGroupMessage(groupId, text)` — 发群消息(async)
19
+ - `sendImage(to, filePath)` — 发本地图片(async)
20
+ - `onMessage(callback)` — 订阅收到的 IM 消息 `{from, text, isGroup, conversationType}`
21
+
22
+ ## 消息注入前缀
23
+
24
+ - `💬 [Seam]` — 一对一
25
+ - `💬 [Seam群]` — 群聊
26
+
27
+ ## 错误码
28
+
29
+ - `IM_MISSING_CREDENTIALS` — credentials.json 缺 userId/userSig/sdkAppId
30
+ - `IM_LOGIN_FAILED` — SDK 登录失败
31
+ - `IM_NOT_READY` — SDK 未就绪
32
+ - `IM_UNKNOWN_ACTION` — 未知 action
33
+
34
+ ## 依赖
35
+
36
+ 无(自身连接腾讯 IM 云服务)。
37
+
38
+ ## 已知问题
39
+
40
+ - SDK 在 node 环境运行需要 global polyfill(window/document/navigator/Image)——已在 require 时自动注入
41
+ - SDK 源码有两处 `_getImageInfoArray` / `_getDownloadIP` 会在 node 里失败——已在 Module loader 里 monkey-patch 跳过