@wenbin_wb/dsh-bridge 2.10.4 → 2.10.6

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/index.js CHANGED
@@ -798,7 +798,7 @@ class BridgeService {
798
798
  },
799
799
 
800
800
  cloudflared: {
801
- running: !!this.cloudflared,
801
+ running: this.isCloudflaredActive(),
802
802
  url: cloudflaredUrl,
803
803
  rawUrl: baseCloudflaredUrl,
804
804
  qr: cloudflaredUrl
@@ -942,8 +942,18 @@ class BridgeService {
942
942
  await this.onPersist?.({ customTunnel: this.customTunnelConfig });
943
943
  }
944
944
 
945
+ // 仅断开与状态,不动持久化配置(dispose 路径用)
946
+ async _haltCustomTunnel() {
947
+ if (this.customTunnel) {
948
+ this.customTunnel.disconnect();
949
+ this.customTunnel = null;
950
+ this.customTunnelState = { phase: 'idle', detail: '' };
951
+ }
952
+ }
953
+
945
954
  async startCloudflared({ autoStart = true } = {}) {
946
- if (this.cloudflared) {
955
+ // 活动中的隧道不允许重复开启;error / idle(引用保留但已停止)允许重建。
956
+ if (this.cloudflared && this.isCloudflaredActive()) {
947
957
  throw new Error('Cloudflare 隧道已在运行');
948
958
  }
949
959
 
@@ -953,6 +963,12 @@ class BridgeService {
953
963
  };
954
964
  await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
955
965
 
966
+ // 若存在旧 manager(error/自愈中断留的引用),先彻底停止再重建,杜绝双跑
967
+ if (this.cloudflared) {
968
+ this.cloudflared.stop();
969
+ this.cloudflared = null;
970
+ }
971
+
956
972
  this.cloudflaredState = { phase: 'connecting', detail: '正在初始化...' };
957
973
  this.cloudflared = new CloudflaredManager({
958
974
  port: this.proxyPort,
@@ -961,10 +977,10 @@ class BridgeService {
961
977
  hostname: this.cloudflaredConfig?.hostname,
962
978
  onStateChange: (state) => {
963
979
  this.cloudflaredState = state;
964
- // 出错时自动清理,让用户可以重新开启
965
- if (state.phase === 'error') {
966
- this.cloudflared = null;
967
- }
980
+ // 注意:不在 error 时清空 this.cloudflared 引用。
981
+ // manager 在 error/意外退出时会自行退避重试(reconnecting),引用必须保留到
982
+ // 用户显式 stop / dispose——否则自愈后 UI 失去控制句柄("关闭"杀不掉、
983
+ // "开启"叠新 manager 双跑)。
968
984
  },
969
985
  logger: this.logger,
970
986
  });
@@ -973,6 +989,15 @@ class BridgeService {
973
989
  this.cloudflared.start();
974
990
  }
975
991
 
992
+ // cloudflared 是否处于"活动"(进程存在或有意图运行)状态。
993
+ // 引用保留期间(自愈退避中)引用仍在,但不算 running——UI 据此显示"重连中"而非"运行中"。
994
+ isCloudflaredActive() {
995
+ const phase = this.cloudflaredState?.phase;
996
+ return phase === 'ready' || phase === 'connecting' || phase === 'downloading' || phase === 'reconnecting';
997
+ }
998
+
999
+ // 用户显式关闭:停止进程并持久化 autoStart=false(与 UI"关闭"语义一致:关了就不该再自启)。
1000
+ // 内部 dispose 请走 _haltCloudflared(),避免误改用户配置。
976
1001
  async stopCloudflared() {
977
1002
  if (this.cloudflared) {
978
1003
  this.cloudflared.stop();
@@ -986,6 +1011,15 @@ class BridgeService {
986
1011
  await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
987
1012
  }
988
1013
 
1014
+ // 仅停止进程与状态,不动持久化配置(插件卸载/重启路径用——用户的 autoStart 意图要保留)
1015
+ async _haltCloudflared() {
1016
+ if (this.cloudflared) {
1017
+ this.cloudflared.stop();
1018
+ this.cloudflared = null;
1019
+ this.cloudflaredState = { phase: 'idle', detail: '' };
1020
+ }
1021
+ }
1022
+
989
1023
  // 重置 Cloudflare 隧道:关闭隧道 + 删除已下载的 cloudflared 二进制
990
1024
  async resetCloudflared() {
991
1025
  await this.stopCloudflared();
@@ -1581,8 +1615,9 @@ class BridgeService {
1581
1615
  }
1582
1616
 
1583
1617
  async dispose() {
1584
- this.stopCustomTunnel();
1585
- this.stopCloudflared();
1618
+ await this._haltCustomTunnel();
1619
+ // 只停进程与状态,不改持久化:插件卸载/重载不应悄悄抹掉用户的 autoStart 意图
1620
+ await this._haltCloudflared();
1586
1621
  if (this.proxy) {
1587
1622
  await this.proxy.stop();
1588
1623
  this.proxy = null;
@@ -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
+ }