@wenbin_wb/dsh-bridge 2.5.4 → 2.6.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.
Binary file
@@ -389,6 +389,18 @@ export class FeishuConversationNode extends ConversationBridge {
389
389
  const disposer = this.ctx.on('approval/request', listener)
390
390
  this.disposers.push(disposer)
391
391
  }
392
+
393
+ dispose() {
394
+ if (this._patchTimer) {
395
+ clearTimeout(this._patchTimer)
396
+ this._patchTimer = null
397
+ }
398
+ this._inTurn = false
399
+ this._streamCardId = null
400
+ this._streamContent = ''
401
+ this._lastPeer = null
402
+ super.dispose()
403
+ }
392
404
  }
393
405
 
394
406
  export const feishuNodeHelpers = {
package/lib/index.js CHANGED
@@ -108,10 +108,52 @@ class QrCache {
108
108
  }
109
109
 
110
110
  /**
111
- * 非安全上下文(http://LAN-IP:端口)里浏览器没有 crypto.randomUUID,
112
- * DSH 连接层 mint RPC id 时会抛错,注入 polyfill 修复。
111
+ * PWA Web App Manifest 与 App 启动图标
113
112
  */
114
- const RANDOM_UUID_POLYFILL = `<script data-dsh-bridge-polyfill="1">!function(){try{if(self.crypto&&!self.crypto.randomUUID){self.crypto.randomUUID=function(){var b=new Uint8Array(16);self.crypto.getRandomValues(b);b[6]=b[6]&15|64;b[8]=b[8]&63|128;var h="";for(var i=0;i<16;i++){var x=b[i].toString(16);h+=(x.length<2?"0":"")+x;if(i===3||i===5||i===7||i===9)h+="-";}return h;}}}catch(e){}}();</script>`;
113
+ const PWA_MANIFEST = JSON.stringify({
114
+ name: 'DeepSeek Harness',
115
+ short_name: 'DSH',
116
+ description: 'DeepSeek Harness Remote & Mobile Workspace',
117
+ start_url: '/',
118
+ display: 'standalone',
119
+ background_color: '#181825',
120
+ theme_color: '#1e1e2e',
121
+ orientation: 'any',
122
+ icons: [
123
+ {
124
+ src: '/__dsh_bridge__/pwa-icon.svg',
125
+ sizes: 'any',
126
+ type: 'image/svg+xml',
127
+ purpose: 'any maskable'
128
+ }
129
+ ]
130
+ }, null, 2);
131
+
132
+ const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
133
+ <defs>
134
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
135
+ <stop offset="0%" stop-color="#4f6ef7"/>
136
+ <stop offset="100%" stop-color="#24388a"/>
137
+ </linearGradient>
138
+ </defs>
139
+ <rect width="512" height="512" rx="128" fill="url(#g)"/>
140
+ <path d="M150 170 C150 140, 362 140, 362 170 L362 330 C362 360, 150 360, 150 330 Z" fill="#ffffff" fill-opacity="0.12"/>
141
+ <circle cx="206" cy="220" r="28" fill="#ffffff"/>
142
+ <circle cx="306" cy="220" r="28" fill="#ffffff"/>
143
+ <path d="M200 290 Q256 340 312 290" stroke="#ffffff" stroke-width="24" stroke-linecap="round" fill="none"/>
144
+ <rect x="236" y="90" width="40" height="60" rx="10" fill="#ffffff"/>
145
+ <circle cx="256" cy="80" r="16" fill="#4f6ef7"/>
146
+ </svg>`;
147
+
148
+ const HTML_HEAD_INJECTIONS = `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
149
+ <meta name="apple-mobile-web-app-capable" content="yes">
150
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
151
+ <meta name="apple-mobile-web-app-title" content="DSH">
152
+ <meta name="theme-color" content="#1e1e2e">
153
+ <link rel="manifest" href="/manifest.webmanifest">
154
+ <link rel="icon" type="image/svg+xml" href="/__dsh_bridge__/pwa-icon.svg">
155
+ <link rel="apple-touch-icon" href="/__dsh_bridge__/pwa-icon.svg">
156
+ <script data-dsh-bridge-polyfill="1">!function(){try{if(self.crypto&&!self.crypto.randomUUID){self.crypto.randomUUID=function(){var b=new Uint8Array(16);self.crypto.getRandomValues(b);b[6]=b[6]&15|64;b[8]=b[8]&63|128;var h="";for(var i=0;i<16;i++){var x=b[i].toString(16);h+=(x.length<2?"0":"")+x;if(i===3||i===5||i===7||i===9)h+="-";}return h;}}}catch(e){}}();</script>`;
115
157
  const INJECT_MARK = 'data-dsh-bridge-polyfill="1"';
116
158
 
117
159
  function isCompressed(headers) {
@@ -148,8 +190,22 @@ class ProxyServer {
148
190
  if (this.server) return;
149
191
 
150
192
  this.server = createServer((req, res) => {
193
+ const pathname = (req.url || '/').split('?')[0].replace(/\/+$/, '') || '/';
194
+
195
+ // 0. PWA Web App Manifest 与 App 图标支持
196
+ if (pathname === '/manifest.webmanifest' || pathname === '/manifest.json') {
197
+ res.writeHead(200, { 'Content-Type': 'application/manifest+json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
198
+ res.end(PWA_MANIFEST);
199
+ return;
200
+ }
201
+ if (pathname === '/__dsh_bridge__/pwa-icon.svg' || pathname === '/apple-touch-icon.png') {
202
+ res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
203
+ res.end(PWA_ICON_SVG);
204
+ return;
205
+ }
206
+
151
207
  // 1. 处理登录 API: POST /__dsh_bridge__/login
152
- if (req.url === '/__dsh_bridge__/login' && req.method === 'POST') {
208
+ if (pathname === '/__dsh_bridge__/login' && req.method === 'POST') {
153
209
  const chunks = [];
154
210
  req.on('data', (c) => chunks.push(c));
155
211
  req.on('end', () => {
@@ -177,7 +233,7 @@ class ProxyServer {
177
233
  }
178
234
 
179
235
  // 2. 处理登出 API: POST /__dsh_bridge__/logout
180
- if (req.url === '/__dsh_bridge__/logout' && req.method === 'POST') {
236
+ if (pathname === '/__dsh_bridge__/logout' && req.method === 'POST') {
181
237
  res.writeHead(200, {
182
238
  'Content-Type': 'application/json; charset=utf-8',
183
239
  'Set-Cookie': 'dsh_bridge_auth=; Path=/; HttpOnly; Max-Age=0',
@@ -187,14 +243,14 @@ class ProxyServer {
187
243
  }
188
244
 
189
245
  // 3. 处理鉴权状态 API: GET /__dsh_bridge__/auth-status (严格脱敏,不暴露 secretToken)
190
- if (req.url === '/__dsh_bridge__/auth-status' && req.method === 'GET') {
246
+ if (pathname === '/__dsh_bridge__/auth-status' && req.method === 'GET') {
191
247
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
192
248
  res.end(JSON.stringify(this.authManager?.getPublicStatus() ?? { enabled: false }));
193
249
  return;
194
250
  }
195
251
 
196
252
  // 3.1 本机特权 Token 签发:仅限真正物理回环连接(127.0.0.1 / ::1,严禁隧道转发流量伪造)
197
- if (req.url === '/__dsh_bridge__/loopback-token') {
253
+ if (pathname === '/__dsh_bridge__/loopback-token') {
198
254
  const corsHeaders = {
199
255
  'Access-Control-Allow-Origin': '*',
200
256
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
@@ -287,7 +343,7 @@ class ProxyServer {
287
343
  proxyRes.on('end', () => {
288
344
  let html = Buffer.concat(chunks).toString('utf8');
289
345
  if (!html.includes(INJECT_MARK)) {
290
- html = html.replace(/<head[^>]*>/i, (m) => `${m}${RANDOM_UUID_POLYFILL}`);
346
+ html = html.replace(/<head[^>]*>/i, (m) => `${m}${HTML_HEAD_INJECTIONS}`);
291
347
  }
292
348
  const out = Buffer.from(html, 'utf8');
293
349
  const outHeaders = { ...proxyRes.headers };
@@ -582,14 +638,17 @@ class BridgeService {
582
638
  // 检查 npm 上是否有新版本(优先国内高速镜像 npmmirror,降级 npmjs 官方源)
583
639
  async checkVersion() {
584
640
  const fetchRegistry = (url, timeoutMs = 4000) => new Promise((resolve, reject) => {
585
- const req = httpsGet(url, { timeout: timeoutMs }, (res) => {
641
+ const req = httpsGet(url, { timeout: timeoutMs, headers: { 'User-Agent': 'dsh-bridge' } }, (res) => {
586
642
  if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
587
643
  const chunks = [];
588
644
  res.on('data', (c) => chunks.push(c));
589
645
  res.on('end', () => {
590
646
  try {
591
647
  const data = JSON.parse(Buffer.concat(chunks).toString());
592
- resolve(data.version ?? null);
648
+ resolve({
649
+ version: data.version ?? null,
650
+ releaseNotes: data.releaseNotes ?? data.description ?? null,
651
+ });
593
652
  } catch (e) {
594
653
  reject(e);
595
654
  }
@@ -600,9 +659,13 @@ class BridgeService {
600
659
  });
601
660
 
602
661
  try {
603
- const latest = await fetchRegistry('https://registry.npmmirror.com/@wenbin_wb/dsh-bridge/latest', 3500)
662
+ const latestData = await fetchRegistry('https://registry.npmmirror.com/@wenbin_wb/dsh-bridge/latest', 3500)
604
663
  .catch(() => fetchRegistry('https://registry.npmjs.org/@wenbin_wb/dsh-bridge/latest', 5000));
605
- return { current: VERSION, latest: latest ?? null };
664
+ return {
665
+ current: VERSION,
666
+ latest: latestData?.version ?? null,
667
+ releaseNotes: latestData?.releaseNotes ?? null,
668
+ };
606
669
  } catch (e) {
607
670
  return { current: VERSION, latest: null, error: e.message ?? '检查失败' };
608
671
  }
@@ -175,6 +175,7 @@ function digestLine(session) {
175
175
  lastTool = event.data.name
176
176
  }
177
177
  }
178
+ if (!inTurn || turn === 0) return null
178
179
  const steps = tools > 0 ? `${tools} 次工具调用` : '思考中'
179
180
  const last = lastTool ? ` | 最近: ${lastTool}` : ''
180
181
  return `[处理中] 第 ${turn} 轮 | ${steps}${last}`
@@ -271,6 +272,7 @@ export class ConversationBridge {
271
272
  }
272
273
 
273
274
  setActiveSession(session) {
275
+ this.stopAllHeartbeats()
274
276
  this.activeSessionId = session.id
275
277
  try { this.onActiveSessionChange?.(session.id) } catch { /* 持久化失败不致命 */ }
276
278
  }
@@ -279,6 +281,7 @@ export class ConversationBridge {
279
281
  // 发消息时通过 re-attach 逻辑拉起 agent。
280
282
  setActiveSessionById(id) {
281
283
  if (!id) return
284
+ this.stopAllHeartbeats()
282
285
  this.activeSessionId = id
283
286
  try { this.onActiveSessionChange?.(id) } catch { /* 持久化失败不致命 */ }
284
287
  }
@@ -388,7 +391,19 @@ export class ConversationBridge {
388
391
  return false
389
392
  }
390
393
 
394
+ stopAllHeartbeats() {
395
+ if (this._digestState) {
396
+ for (const state of this._digestState.values()) {
397
+ if (state && state.heartbeat) {
398
+ clearInterval(state.heartbeat)
399
+ state.heartbeat = undefined
400
+ }
401
+ }
402
+ }
403
+ }
404
+
391
405
  dispose() {
406
+ this.stopAllHeartbeats()
392
407
  for (const disposer of this.disposers) {
393
408
  try { disposer() } catch { /* 忽略 */ }
394
409
  }
@@ -563,9 +578,9 @@ export class ConversationBridge {
563
578
  // ---- 出站事件绑定 ----
564
579
 
565
580
  _attachOutbound() {
566
- const digestState = new Map()
581
+ this._digestState = new Map()
567
582
  const stopHeartbeat = (state) => {
568
- if (state.heartbeat) {
583
+ if (state && state.heartbeat) {
569
584
  clearInterval(state.heartbeat)
570
585
  state.heartbeat = undefined
571
586
  }
@@ -574,16 +589,32 @@ export class ConversationBridge {
574
589
  stopHeartbeat(state)
575
590
  if (this.config.digestIntervalSec <= 0) return
576
591
  state.heartbeat = setInterval(() => {
592
+ // 1. 必须依然是当前活动会话
593
+ if (this.activeSessionId !== session.id) {
594
+ stopHeartbeat(state)
595
+ return
596
+ }
597
+ // 2. 检查会话当前是否真正处于 inTurn 状态中
598
+ const line = digestLine(session)
599
+ if (!line) {
600
+ stopHeartbeat(state)
601
+ return
602
+ }
577
603
  // 心跳时同时刷新 typing 状态(微信 typing 只维持 15 秒)
578
604
  if (this.peerId) this.sendTyping(1).catch(() => {})
579
- void this.sendText(digestLine(session))
605
+ void this.sendText(line)
580
606
  }, this.config.digestIntervalSec * 1000)
581
607
  if (typeof state.heartbeat.unref === 'function') state.heartbeat.unref()
582
608
  }
583
609
  const onEvent = (session, event) => {
610
+ const state = this._digestState.get(session.id) ?? { startedTurns: new Set(), createdFiles: new Set() }
611
+ this._digestState.set(session.id, state)
612
+
613
+ // 无论是否为当前活动会话,一旦收到 turn/end,立即停止该 session 的心跳定时器
614
+ if (event.type === 'turn/end') {
615
+ stopHeartbeat(state)
616
+ }
584
617
  if (session.id !== this.activeSessionId) return
585
- const state = digestState.get(session.id) ?? { startedTurns: new Set(), createdFiles: new Set() }
586
- digestState.set(session.id, state)
587
618
 
588
619
  if (event.type === 'turn/start') {
589
620
  const turn = event.data?.turn
@@ -930,6 +961,7 @@ async function routeCommand(node, text) {
930
961
  return true
931
962
  }
932
963
  case 'stop': {
964
+ node.stopAllHeartbeats()
933
965
  const agent = node.activeAgent()
934
966
  if (!agent) {
935
967
  await node.sendText(`ℹ️ **当前没有正在运行的 Agent 任务**`)
@@ -940,6 +972,7 @@ async function routeCommand(node, text) {
940
972
  return true
941
973
  }
942
974
  case 'end': {
975
+ node.stopAllHeartbeats()
943
976
  // 结束当前会话:停止 agent 并清除活动会话(进入"没有活动会话"状态)
944
977
  const agent = node.activeAgent()
945
978
  if (agent) agent.cancel({ kind: 'user' })
package/lib/qq/node.js CHANGED
@@ -508,6 +508,13 @@ export class QqConversationNode extends ConversationBridge {
508
508
  this.logger?.info?.(`[dsh-bridge qq] unknown button interaction: ${buttonId}`)
509
509
  }
510
510
  }
511
+
512
+ dispose() {
513
+ this._lastPeer = null
514
+ this._replyMsgId = null
515
+ this.lastMessageId = null
516
+ super.dispose()
517
+ }
511
518
  }
512
519
 
513
520
  // 导出,便于测试与复用
@@ -337,6 +337,14 @@ export class TelegramConversationNode extends ConversationBridge {
337
337
  if (!peerId || !this.gateway) return
338
338
  return this.gateway.sendTyping(peerId)
339
339
  }
340
+
341
+ dispose() {
342
+ this._inTurn = false
343
+ this._streamMsgId = null
344
+ this._streamContent = ''
345
+ this._lastPeer = null
346
+ super.dispose()
347
+ }
340
348
  }
341
349
 
342
350
  export const telegramNodeHelpers = { makePlatform }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@wenbin_wb/dsh-bridge",
3
- "version": "2.5.4",
3
+ "version": "2.6.0",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
+ "releaseNotes": "【v2.6.0 移动端交互重构与长按菜单】\n• 📱 深度重构移动端触控交互,极简通透顶栏与大圆角输入胶囊\n• 🖐️ 侧边栏支持长按(Long Press)呼出操作菜单(重命名/分叉/归档),边缘滑动开闭抽屉\n• 🛠️ 彻底修复「运行中」等状态药丸徽标在手机屏幕下的换行问题\n• ⚡ 优化版本检查与升级提示,直观展示新版本更新内容亮点",
5
6
  "type": "module",
6
7
  "main": "lib/index.js",
7
8
  "exports": {