@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.
- package/README.en.md +31 -10
- package/README.md +31 -10
- package/client/client.js +538 -26
- package/client/index.js +2746 -2193
- package/docs/screenshots/mobile-chat.jpg +0 -0
- package/docs/screenshots/mobile-drawer.jpg +0 -0
- package/docs/screenshots/mobile-settings-im.jpg +0 -0
- package/docs/screenshots/mobile-settings-lan.jpg +0 -0
- package/docs/screenshots/mobile-settings-security.jpg +0 -0
- package/docs/screenshots/mobile-settings-tunnel.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -0
- package/lib/feishu/node.js +12 -0
- package/lib/index.js +75 -12
- package/lib/platform/conversation-bridge.js +38 -5
- package/lib/qq/node.js +7 -0
- package/lib/telegram/node.js +8 -0
- package/package.json +2 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/feishu/node.js
CHANGED
|
@@ -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
|
-
*
|
|
112
|
-
* DSH 连接层 mint RPC id 时会抛错,注入 polyfill 修复。
|
|
111
|
+
* PWA Web App Manifest 与 App 启动图标
|
|
113
112
|
*/
|
|
114
|
-
const
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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}${
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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
|
// 导出,便于测试与复用
|
package/lib/telegram/node.js
CHANGED
|
@@ -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.
|
|
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": {
|