@wenbin_wb/dsh-bridge 2.10.7 → 2.10.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.
- package/CHANGELOG.md +26 -0
- package/README.en.md +27 -0
- package/README.md +27 -0
- package/client/client.js +388 -5
- package/client/index.js +327 -3
- package/client/mobile-styles.js +68 -0
- package/docs/telegram-usage.md +1 -1
- package/lib/auth/dsh-native-cookie.js +148 -0
- package/lib/auth/manager.js +54 -4
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +17 -2
- package/lib/compat.js +129 -129
- package/lib/connection-compat.js +115 -0
- package/lib/feishu/index.js +225 -225
- package/lib/feishu/node.js +439 -439
- package/lib/index.js +163 -28
- package/lib/platform/base.js +147 -147
- package/lib/platform/commands.js +221 -221
- package/lib/platform/conversation-bridge.js +821 -821
- package/lib/platform/dsh-storage.js +117 -117
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +229 -229
- package/lib/platform/session-catalog.js +372 -372
- package/lib/platform/stream-slices.js +21 -21
- package/lib/qq/index.js +312 -312
- package/lib/telegram/index.js +216 -216
- package/lib/telegram/node.js +322 -322
- package/lib/wechat/gateway.js +986 -986
- package/lib/wechat/index.js +244 -244
- package/lib/wechat/media.js +285 -285
- package/lib/wechat/node.js +352 -352
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -10,10 +10,9 @@ import { get as httpsGet } from 'node:https';
|
|
|
10
10
|
import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
|
|
11
11
|
import { join, dirname, basename, resolve, normalize } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile, mkdir, unlink, readdir, stat, access } from 'node:fs/promises';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
|
-
import { createHash, createHmac } from 'node:crypto';
|
|
17
16
|
import QRCode from 'qrcode';
|
|
18
17
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
19
18
|
import { CustomTunnelClient } from './tunnel-client.mjs';
|
|
@@ -24,6 +23,7 @@ import { QqService } from './qq/index.js';
|
|
|
24
23
|
import { FeishuService } from './feishu/index.js';
|
|
25
24
|
import { TelegramService } from './telegram/index.js';
|
|
26
25
|
import { AuthManager } from './auth/manager.js';
|
|
26
|
+
import { getDshLoopbackCookie as getDshLoopbackCookieImpl } from './auth/dsh-native-cookie.js';
|
|
27
27
|
import { renderLoginPage } from './auth/login-template.js';
|
|
28
28
|
import { isSafeWorkspacePath, isSensitiveFolderName } from './security/path-validator.js';
|
|
29
29
|
import { stripSessionProjections } from './session-strip.js';
|
|
@@ -193,35 +193,14 @@ function isCompressed(headers) {
|
|
|
193
193
|
return /(^|,\s*)(gzip|br|deflate)(\s*,|$)/i.test(String(headers['content-encoding'] ?? ''));
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function encodeBase64Url(value) {
|
|
197
|
-
return Buffer.from(value).toString('base64url');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
196
|
/**
|
|
201
197
|
* 读取 DSH 本地凭证并生成 loopback dsh-auth 认证签名 Cookie (适配 DSH 新版原生认证)
|
|
198
|
+
*
|
|
199
|
+
* 实现见 lib/auth/dsh-native-cookie.js(含 cookie 名称/载荷/签名的格式说明与
|
|
200
|
+
* 凭据记录定位策略)。此处仅做 thin wrapper,保持既有调用点不变。
|
|
202
201
|
*/
|
|
203
202
|
function getDshLoopbackCookie(targetPort) {
|
|
204
|
-
|
|
205
|
-
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
206
|
-
const credPath = join(dshHome, '.credentials.yaml');
|
|
207
|
-
if (!existsSync(credPath)) return '';
|
|
208
|
-
const content = readFileSync(credPath, 'utf8');
|
|
209
|
-
const match = content.match(/secret:\s*([A-Za-z0-9_-]+)/);
|
|
210
|
-
if (!match) return '';
|
|
211
|
-
const secret = Buffer.from(match[1], 'base64url');
|
|
212
|
-
|
|
213
|
-
const authority = `127.0.0.1:${targetPort}`;
|
|
214
|
-
const name = 'dsh-auth-' + encodeBase64Url(createHash('sha256').update(authority).digest());
|
|
215
|
-
const issuedAt = Date.now() - 1000;
|
|
216
|
-
const expiresAt = issuedAt + 30 * 24 * 3600 * 1000;
|
|
217
|
-
const body = encodeBase64Url(Buffer.from(JSON.stringify({
|
|
218
|
-
version: 1, authority, issuedAt, expiresAt,
|
|
219
|
-
}), 'utf8'));
|
|
220
|
-
const sig = encodeBase64Url(createHmac('sha256', secret).update(body).digest());
|
|
221
|
-
return `${name}=v1.${body}.${sig}`;
|
|
222
|
-
} catch {
|
|
223
|
-
return '';
|
|
224
|
-
}
|
|
203
|
+
return getDshLoopbackCookieImpl(targetPort);
|
|
225
204
|
}
|
|
226
205
|
|
|
227
206
|
/** 把请求头中的 Host 和 Origin 改写成 loopback,让 DSH 的安全栅栏放行 */
|
|
@@ -1033,6 +1012,63 @@ class BridgeService {
|
|
|
1033
1012
|
}
|
|
1034
1013
|
|
|
1035
1014
|
// 检查 npm 上是否有新版本(优先国内高速镜像 npmmirror,降级 npmjs 官方源)
|
|
1015
|
+
// 探测 DSH 宿主 CLI 的安装形态,判断能否通过"npm 全局升级"自动更新。
|
|
1016
|
+
// 仅当 dsh 命令 resolve 到当前 node 同 prefix 的 node_modules/@deepseek-ai/dsh(标准 npm 全局安装)
|
|
1017
|
+
// 时才允许一键升级;pnpm / 源码 / Electron 加壳等形态无法用 npm -g 升级,仅返回提示。
|
|
1018
|
+
// @returns {Promise<{upgradable:boolean, rootGlobal?:string, dshRealPath?:string, reason?:string}>}
|
|
1019
|
+
async _probeDshUpgrade() {
|
|
1020
|
+
const isWin = process.platform === 'win32';
|
|
1021
|
+
const nodeDir = dirname(process.execPath);
|
|
1022
|
+
const existingPath = process.env.PATH || process.env.Path || '';
|
|
1023
|
+
const extraPaths = isWin ? [nodeDir] : [nodeDir, '/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin', '/usr/bin', '/bin', join(homedir(), '.local/bin'), join(homedir(), '.npm-global/bin')];
|
|
1024
|
+
const separator = isWin ? ';' : ':';
|
|
1025
|
+
const augmentedEnv = { ...process.env, PATH: [...extraPaths, existingPath].filter(Boolean).join(separator) };
|
|
1026
|
+
if (isWin) augmentedEnv.Path = augmentedEnv.PATH;
|
|
1027
|
+
|
|
1028
|
+
const runCmd = (cmd, args) => new Promise((resolve) => {
|
|
1029
|
+
let cp;
|
|
1030
|
+
try {
|
|
1031
|
+
cp = spawn(cmd, args, { windowsHide: true, shell: true, env: augmentedEnv, timeout: 8000 });
|
|
1032
|
+
} catch {
|
|
1033
|
+
return resolve(null);
|
|
1034
|
+
}
|
|
1035
|
+
let stdout = ''; let stderr = '';
|
|
1036
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
1037
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
1038
|
+
cp.on('error', () => resolve(null));
|
|
1039
|
+
cp.on('close', (code) => {
|
|
1040
|
+
if (code === 0 && stdout.trim()) resolve(stdout.trim().split(/\r?\n/)[0]);
|
|
1041
|
+
else resolve(null);
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
// 1. 找 dsh bin 的真实路径(Windows which 不存在,用 where)
|
|
1047
|
+
const dshBin = await runCmd(isWin ? 'where' : 'which', [isWin ? 'dsh.cmd' : 'dsh']);
|
|
1048
|
+
if (!dshBin) {
|
|
1049
|
+
return { upgradable: false, reason: '未找到 dsh 命令(可能通过源码/打包方式运行,非 npm 全局安装)' };
|
|
1050
|
+
}
|
|
1051
|
+
// realpath 在主进程内解析(子进程 node -e + shell 引号嵌套易出错)
|
|
1052
|
+
let dshRealPath = dshBin;
|
|
1053
|
+
try {
|
|
1054
|
+
dshRealPath = realpathSync(dshBin) || dshBin;
|
|
1055
|
+
} catch { /* 保留原始路径继续判断 */ }
|
|
1056
|
+
// 2. 当前 node 对应的全局 node_modules 根
|
|
1057
|
+
const globalNodeModules = await runCmd('npm', ['root', '-g']);
|
|
1058
|
+
const rootGlobal = globalNodeModules ? dirname(globalNodeModules) : dirname(nodeDir);
|
|
1059
|
+
const marker = join('node_modules', '@deepseek-ai', 'dsh');
|
|
1060
|
+
if (dshRealPath.includes(marker) && (dshRealPath.startsWith(rootGlobal) || dshRealPath.includes('node-v') && dshRealPath.includes('lib'))) {
|
|
1061
|
+
return { upgradable: true, rootGlobal, dshRealPath };
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
upgradable: false,
|
|
1065
|
+
reason: 'dsh 非标准 npm 全局安装(Electron/打包/源码/pnpm 等),无法自动升级;请按官方渠道手动更新',
|
|
1066
|
+
};
|
|
1067
|
+
} catch {
|
|
1068
|
+
return { upgradable: false, reason: '探测 dsh 安装形态失败,请按官方渠道手动更新' };
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1036
1072
|
async checkVersion() {
|
|
1037
1073
|
// TTL 缓存:窗口内直接返回上次结果,避免重复请求外网 registry
|
|
1038
1074
|
if (this._versionCheckCache && Date.now() - this._versionCheckCachedAt < this._versionCheckTtlMs) {
|
|
@@ -1062,18 +1098,27 @@ class BridgeService {
|
|
|
1062
1098
|
try {
|
|
1063
1099
|
const latestData = await fetchRegistry('https://registry.npmmirror.com/@wenbin_wb/dsh-bridge/latest', 3500)
|
|
1064
1100
|
.catch(() => fetchRegistry('https://registry.npmjs.org/@wenbin_wb/dsh-bridge/latest', 5000));
|
|
1101
|
+
// DSH 宿主 CLI 最新版本(npm 全局包 @deepseek-ai/dsh;与插件同源镜像,失败不阻断面板)
|
|
1102
|
+
const dshLatestData = await fetchRegistry('https://registry.npmmirror.com/@deepseek-ai/dsh/latest', 3500)
|
|
1103
|
+
.catch(() => fetchRegistry('https://registry.npmjs.org/@deepseek-ai/dsh/latest', 5000))
|
|
1104
|
+
.catch(() => null);
|
|
1105
|
+
// 安装形态探测(决定是否给"一键升级 DSH"按钮):npm 全局可升级,加壳/源码仅提醒
|
|
1106
|
+
const dshProbe = await this._probeDshUpgrade();
|
|
1065
1107
|
const result = {
|
|
1066
1108
|
current: VERSION,
|
|
1067
1109
|
latest: latestData?.version ?? null,
|
|
1068
1110
|
releaseNotes: latestData?.releaseNotes ?? null,
|
|
1069
1111
|
dshVersion: await this.getDshVersion(),
|
|
1112
|
+
dshLatest: dshLatestData?.version ?? null,
|
|
1113
|
+
dshUpgradable: dshProbe.upgradable,
|
|
1114
|
+
dshUpgradeReason: dshProbe.reason || '',
|
|
1070
1115
|
};
|
|
1071
1116
|
this._versionCheckCache = result;
|
|
1072
1117
|
this._versionCheckCachedAt = Date.now();
|
|
1073
1118
|
return result;
|
|
1074
1119
|
} catch (e) {
|
|
1075
1120
|
// 失败不缓存(让下次调用可重试),但记录便于诊断
|
|
1076
|
-
return { current: VERSION, latest: null, error: e.message ?? '检查失败', dshVersion: await this.getDshVersion() };
|
|
1121
|
+
return { current: VERSION, latest: null, error: e.message ?? '检查失败', dshVersion: await this.getDshVersion(), dshLatest: null, dshUpgradable: false, dshUpgradeReason: '' };
|
|
1077
1122
|
}
|
|
1078
1123
|
}
|
|
1079
1124
|
|
|
@@ -1165,6 +1210,96 @@ class BridgeService {
|
|
|
1165
1210
|
return { ok: false, error: lastError?.message ?? '升级命令执行失败', version: targetVersion };
|
|
1166
1211
|
}
|
|
1167
1212
|
|
|
1213
|
+
// 一键升级 DSH 宿主 CLI(npm 全局包 @deepseek-ai/dsh)。
|
|
1214
|
+
// 升级的是"与当前 node 配对"的全局 prefix(dsh 命令所在目录),完成后需重启 DSH 生效。
|
|
1215
|
+
async upgradeDsh({ version } = {}) {
|
|
1216
|
+
const targetVersion = version ? String(version).trim() : 'latest';
|
|
1217
|
+
// 严格 SemVer 白名单校验(dsh 用 rc 版本号,如 0.1.2-rc.1)
|
|
1218
|
+
if (!/^(latest|\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?)$/.test(targetVersion)) {
|
|
1219
|
+
return { ok: false, error: `非法的版本号格式: ${targetVersion}`, version: targetVersion };
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// 安装形态守卫:非标准 npm 全局安装(Electron/源码/pnpm 等)拒绝自动升级,
|
|
1223
|
+
// 避免"装一份不生效的新副本、误导用户以为升级成功"
|
|
1224
|
+
const probe = await this._probeDshUpgrade();
|
|
1225
|
+
if (!probe.upgradable) {
|
|
1226
|
+
return {
|
|
1227
|
+
ok: false,
|
|
1228
|
+
error: probe.reason || '当前 DSH 为非 npm 全局安装,无法自动升级;请按官方渠道手动更新',
|
|
1229
|
+
version: targetVersion,
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const pkgSpec = `@deepseek-ai/dsh@${targetVersion}`;
|
|
1234
|
+
const isWin = process.platform === 'win32';
|
|
1235
|
+
const nodeDir = dirname(process.execPath);
|
|
1236
|
+
const home = homedir();
|
|
1237
|
+
const extraPaths = isWin ? [nodeDir] : [
|
|
1238
|
+
nodeDir,
|
|
1239
|
+
'/opt/homebrew/bin',
|
|
1240
|
+
'/opt/homebrew/sbin',
|
|
1241
|
+
'/usr/local/bin',
|
|
1242
|
+
'/usr/bin',
|
|
1243
|
+
'/bin',
|
|
1244
|
+
join(home, '.nvm/current/bin'),
|
|
1245
|
+
join(home, '.fnm/current/bin'),
|
|
1246
|
+
join(home, '.local/bin'),
|
|
1247
|
+
join(home, '.cargo/bin'),
|
|
1248
|
+
];
|
|
1249
|
+
const separator = isWin ? ';' : ':';
|
|
1250
|
+
const existingPath = process.env.PATH || process.env.Path || '';
|
|
1251
|
+
const augmentedEnv = {
|
|
1252
|
+
...process.env,
|
|
1253
|
+
PATH: [...extraPaths, existingPath].filter(Boolean).join(separator),
|
|
1254
|
+
};
|
|
1255
|
+
if (isWin) augmentedEnv.Path = augmentedEnv.PATH;
|
|
1256
|
+
|
|
1257
|
+
// 与当前 node 配对的 npm 绝对路径(保证装进 dsh 所在 prefix,而非 PATH 里其它 npm)
|
|
1258
|
+
const siblingNpm = join(nodeDir, isWin ? 'npm.cmd' : 'npm');
|
|
1259
|
+
const npmBin = existsSync(siblingNpm) ? siblingNpm : 'npm';
|
|
1260
|
+
// 优先用探测到的全局 root(dsh 实际所在 prefix),其次 nodeDir 父目录
|
|
1261
|
+
const globalRoot = probe.rootGlobal || dirname(nodeDir);
|
|
1262
|
+
|
|
1263
|
+
const tasks = [
|
|
1264
|
+
{ cmd: npmBin, args: ['install', '-g', '--prefix', globalRoot, pkgSpec] },
|
|
1265
|
+
{ cmd: npmBin, args: ['install', '-g', pkgSpec] },
|
|
1266
|
+
];
|
|
1267
|
+
|
|
1268
|
+
let lastError = null;
|
|
1269
|
+
for (const task of tasks) {
|
|
1270
|
+
try {
|
|
1271
|
+
const res = await new Promise((resolve, reject) => {
|
|
1272
|
+
let cp;
|
|
1273
|
+
try {
|
|
1274
|
+
cp = spawn(task.cmd, task.args, {
|
|
1275
|
+
windowsHide: true,
|
|
1276
|
+
shell: true,
|
|
1277
|
+
env: augmentedEnv,
|
|
1278
|
+
timeout: 180000,
|
|
1279
|
+
});
|
|
1280
|
+
} catch (spawnErr) {
|
|
1281
|
+
return reject(spawnErr);
|
|
1282
|
+
}
|
|
1283
|
+
let stdout = '';
|
|
1284
|
+
let stderr = '';
|
|
1285
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
1286
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
1287
|
+
cp.on('error', reject);
|
|
1288
|
+
cp.on('close', (code) => {
|
|
1289
|
+
if (code === 0) resolve({ stdout, stderr });
|
|
1290
|
+
else reject(new Error(stderr || stdout || `进程退出码 ${code}`));
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
const output = (res.stdout || res.stderr || '升级成功').trim().slice(-500);
|
|
1294
|
+
this.logger?.info('dsh-bridge: DSH 升级命令成功: %s %s', task.cmd, task.args.join(' '));
|
|
1295
|
+
return { ok: true, command: `${task.cmd} ${task.args.join(' ')}`, output, version: targetVersion };
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
lastError = err;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return { ok: false, error: lastError?.message ?? '升级命令执行失败', version: targetVersion };
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1168
1303
|
// 优雅重启 DSH 服务(支持守护进程自动拉起或独立派生子进程重启)
|
|
1169
1304
|
async restartDsh() {
|
|
1170
1305
|
this.logger?.info('收到 DSH 重启请求,正在调度重启...');
|
package/lib/platform/base.js
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
// dsh-bridge 平台抽象基类
|
|
2
|
-
//
|
|
3
|
-
// 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
|
|
4
|
-
// 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
|
|
5
|
-
// (lib/platform/conversation-bridge.js)中实现,通过 platform 注入到 bridge。
|
|
6
|
-
//
|
|
7
|
-
// 生命周期:constructor → start() → stop() → dispose()
|
|
8
|
-
// 消息抽象:sendText / sendTyping / sendMedia(由子类实现)
|
|
9
|
-
|
|
10
|
-
export class Platform {
|
|
11
|
-
/**
|
|
12
|
-
* @param {object} opts
|
|
13
|
-
* @param {object} opts.ctx Cordis 上下文
|
|
14
|
-
* @param {object} opts.logger 日志器
|
|
15
|
-
* @param {object} [opts.config] 已持久化的平台配置(凭证等)
|
|
16
|
-
* @param {(patch: object) => (void|Promise<void>)} [opts.onPersist] 主插件保存回调
|
|
17
|
-
* @param {import('./conversation-bridge.js').ConversationBridge} [opts.bridge] 会话桥实例
|
|
18
|
-
*/
|
|
19
|
-
constructor({ ctx, logger, config = {}, onPersist, bridge } = {}) {
|
|
20
|
-
this.ctx = ctx
|
|
21
|
-
this.logger = logger
|
|
22
|
-
this.config = { ...config }
|
|
23
|
-
this.onPersist = onPersist ?? (() => {})
|
|
24
|
-
this.bridge = bridge ?? null
|
|
25
|
-
|
|
26
|
-
// 平台标识(子类必须设置)
|
|
27
|
-
this.id = ''
|
|
28
|
-
this.name = ''
|
|
29
|
-
|
|
30
|
-
// 连接状态与账号:子类可能用 getter 覆盖(如委托给 gateway),
|
|
31
|
-
// 因此仅在未被子类覆盖时才初始化默认值。
|
|
32
|
-
if (!('status' in this)) this.status = 'idle'
|
|
33
|
-
if (!('accountId' in this)) this.accountId = null
|
|
34
|
-
|
|
35
|
-
// 扫码/登录的流式状态(RPC 轮询读取)
|
|
36
|
-
this.loginState = {
|
|
37
|
-
phase: 'idle', // idle | qr | scaned | confirmed | done | error
|
|
38
|
-
qrPayload: null, // 待渲染内容:dataURL 图片 或 二维码文本
|
|
39
|
-
qrKind: null, // 'img' | 'text'
|
|
40
|
-
error: null,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this.disposers = []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---- 平台能力声明(子类可覆盖)----
|
|
47
|
-
|
|
48
|
-
get capabilities() {
|
|
49
|
-
return {
|
|
50
|
-
supportsGroup: false, // 是否支持群聊
|
|
51
|
-
supportsMedia: false, // 是否支持媒体收发
|
|
52
|
-
supportsVoice: false, // 是否支持语音
|
|
53
|
-
supportsTyping: false, // 是否支持 typing 状态
|
|
54
|
-
maxMessageChars: 2000, // 单条消息最大字符数
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get configured() {
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---- 生命周期(子类必须实现 start/stop;dispose 已提供默认实现)----
|
|
63
|
-
|
|
64
|
-
async start() {
|
|
65
|
-
throw new Error(`${this.id || 'platform'}: start() not implemented`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async stop() {
|
|
69
|
-
throw new Error(`${this.id || 'platform'}: stop() not implemented`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
dispose() {
|
|
73
|
-
for (const disposer of this.disposers) {
|
|
74
|
-
try { disposer() } catch { /* 忽略 */ }
|
|
75
|
-
}
|
|
76
|
-
this.disposers = []
|
|
77
|
-
this.bridge?.dispose?.()
|
|
78
|
-
this.bridge = null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---- 消息抽象(子类必须实现)----
|
|
82
|
-
|
|
83
|
-
async sendText(peerId, text, opts = {}) {
|
|
84
|
-
throw new Error(`${this.id || 'platform'}: sendText() not implemented`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async sendTyping(peerId, state) {
|
|
88
|
-
return Promise.resolve()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async sendMedia(peerId, media, opts = {}) {
|
|
92
|
-
throw new Error(`${this.id || 'platform'}: sendMedia() not implemented`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---- 登录(子类必须实现 login;getLoginState 已提供默认)----
|
|
96
|
-
|
|
97
|
-
async login(opts = {}) {
|
|
98
|
-
throw new Error(`${this.id || 'platform'}: login() not implemented`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getLoginState() {
|
|
102
|
-
return { ...this.loginState }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---- 状态汇总(供 RPC/UI 读取)----
|
|
106
|
-
|
|
107
|
-
getStatus() {
|
|
108
|
-
return {
|
|
109
|
-
id: this.id,
|
|
110
|
-
name: this.name,
|
|
111
|
-
status: this.status,
|
|
112
|
-
configured: this.configured,
|
|
113
|
-
accountId: this.accountId,
|
|
114
|
-
login: this.getLoginState(),
|
|
115
|
-
peerId: this.bridge?.peerId ?? null,
|
|
116
|
-
sessionId: this.bridge?.activeSessionId ?? null,
|
|
117
|
-
config: this.getEditableConfig?.(),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 可编辑配置(供 UI 设置面板读取);子类可覆盖返回具体字段。 */
|
|
122
|
-
getEditableConfig() {
|
|
123
|
-
return {}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---- 工具 ----
|
|
127
|
-
|
|
128
|
-
setStatus(status) {
|
|
129
|
-
if (this.status === status) return
|
|
130
|
-
this.status = status
|
|
131
|
-
try {
|
|
132
|
-
this.ctx.emit?.(`${this.id}/status`, status)
|
|
133
|
-
} catch { /* emit 失败不致命 */ }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async persist(patch) {
|
|
137
|
-
try {
|
|
138
|
-
await this.onPersist(patch)
|
|
139
|
-
} catch (err) {
|
|
140
|
-
this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async destroy() {
|
|
145
|
-
this.dispose()
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
// dsh-bridge 平台抽象基类
|
|
2
|
+
//
|
|
3
|
+
// 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
|
|
4
|
+
// 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
|
|
5
|
+
// (lib/platform/conversation-bridge.js)中实现,通过 platform 注入到 bridge。
|
|
6
|
+
//
|
|
7
|
+
// 生命周期:constructor → start() → stop() → dispose()
|
|
8
|
+
// 消息抽象:sendText / sendTyping / sendMedia(由子类实现)
|
|
9
|
+
|
|
10
|
+
export class Platform {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {object} opts.ctx Cordis 上下文
|
|
14
|
+
* @param {object} opts.logger 日志器
|
|
15
|
+
* @param {object} [opts.config] 已持久化的平台配置(凭证等)
|
|
16
|
+
* @param {(patch: object) => (void|Promise<void>)} [opts.onPersist] 主插件保存回调
|
|
17
|
+
* @param {import('./conversation-bridge.js').ConversationBridge} [opts.bridge] 会话桥实例
|
|
18
|
+
*/
|
|
19
|
+
constructor({ ctx, logger, config = {}, onPersist, bridge } = {}) {
|
|
20
|
+
this.ctx = ctx
|
|
21
|
+
this.logger = logger
|
|
22
|
+
this.config = { ...config }
|
|
23
|
+
this.onPersist = onPersist ?? (() => {})
|
|
24
|
+
this.bridge = bridge ?? null
|
|
25
|
+
|
|
26
|
+
// 平台标识(子类必须设置)
|
|
27
|
+
this.id = ''
|
|
28
|
+
this.name = ''
|
|
29
|
+
|
|
30
|
+
// 连接状态与账号:子类可能用 getter 覆盖(如委托给 gateway),
|
|
31
|
+
// 因此仅在未被子类覆盖时才初始化默认值。
|
|
32
|
+
if (!('status' in this)) this.status = 'idle'
|
|
33
|
+
if (!('accountId' in this)) this.accountId = null
|
|
34
|
+
|
|
35
|
+
// 扫码/登录的流式状态(RPC 轮询读取)
|
|
36
|
+
this.loginState = {
|
|
37
|
+
phase: 'idle', // idle | qr | scaned | confirmed | done | error
|
|
38
|
+
qrPayload: null, // 待渲染内容:dataURL 图片 或 二维码文本
|
|
39
|
+
qrKind: null, // 'img' | 'text'
|
|
40
|
+
error: null,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.disposers = []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- 平台能力声明(子类可覆盖)----
|
|
47
|
+
|
|
48
|
+
get capabilities() {
|
|
49
|
+
return {
|
|
50
|
+
supportsGroup: false, // 是否支持群聊
|
|
51
|
+
supportsMedia: false, // 是否支持媒体收发
|
|
52
|
+
supportsVoice: false, // 是否支持语音
|
|
53
|
+
supportsTyping: false, // 是否支持 typing 状态
|
|
54
|
+
maxMessageChars: 2000, // 单条消息最大字符数
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get configured() {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- 生命周期(子类必须实现 start/stop;dispose 已提供默认实现)----
|
|
63
|
+
|
|
64
|
+
async start() {
|
|
65
|
+
throw new Error(`${this.id || 'platform'}: start() not implemented`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async stop() {
|
|
69
|
+
throw new Error(`${this.id || 'platform'}: stop() not implemented`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dispose() {
|
|
73
|
+
for (const disposer of this.disposers) {
|
|
74
|
+
try { disposer() } catch { /* 忽略 */ }
|
|
75
|
+
}
|
|
76
|
+
this.disposers = []
|
|
77
|
+
this.bridge?.dispose?.()
|
|
78
|
+
this.bridge = null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- 消息抽象(子类必须实现)----
|
|
82
|
+
|
|
83
|
+
async sendText(peerId, text, opts = {}) {
|
|
84
|
+
throw new Error(`${this.id || 'platform'}: sendText() not implemented`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async sendTyping(peerId, state) {
|
|
88
|
+
return Promise.resolve()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async sendMedia(peerId, media, opts = {}) {
|
|
92
|
+
throw new Error(`${this.id || 'platform'}: sendMedia() not implemented`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- 登录(子类必须实现 login;getLoginState 已提供默认)----
|
|
96
|
+
|
|
97
|
+
async login(opts = {}) {
|
|
98
|
+
throw new Error(`${this.id || 'platform'}: login() not implemented`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getLoginState() {
|
|
102
|
+
return { ...this.loginState }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 状态汇总(供 RPC/UI 读取)----
|
|
106
|
+
|
|
107
|
+
getStatus() {
|
|
108
|
+
return {
|
|
109
|
+
id: this.id,
|
|
110
|
+
name: this.name,
|
|
111
|
+
status: this.status,
|
|
112
|
+
configured: this.configured,
|
|
113
|
+
accountId: this.accountId,
|
|
114
|
+
login: this.getLoginState(),
|
|
115
|
+
peerId: this.bridge?.peerId ?? null,
|
|
116
|
+
sessionId: this.bridge?.activeSessionId ?? null,
|
|
117
|
+
config: this.getEditableConfig?.(),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 可编辑配置(供 UI 设置面板读取);子类可覆盖返回具体字段。 */
|
|
122
|
+
getEditableConfig() {
|
|
123
|
+
return {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- 工具 ----
|
|
127
|
+
|
|
128
|
+
setStatus(status) {
|
|
129
|
+
if (this.status === status) return
|
|
130
|
+
this.status = status
|
|
131
|
+
try {
|
|
132
|
+
this.ctx.emit?.(`${this.id}/status`, status)
|
|
133
|
+
} catch { /* emit 失败不致命 */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async persist(patch) {
|
|
137
|
+
try {
|
|
138
|
+
await this.onPersist(patch)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async destroy() {
|
|
145
|
+
this.dispose()
|
|
146
|
+
}
|
|
147
|
+
}
|