@wenbin_wb/dsh-bridge 2.10.8 → 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 +12 -0
- package/README.en.md +27 -0
- package/README.md +27 -0
- package/docs/telegram-usage.md +1 -1
- package/lib/auth/dsh-native-cookie.js +148 -0
- package/lib/bridge-rpc.js +9 -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 +5 -26
- 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
|
@@ -13,7 +13,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
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 的安全栅栏放行 */
|
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
|
+
}
|