@wenbin_wb/dsh-bridge 2.3.3 → 2.5.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.
@@ -0,0 +1,89 @@
1
+ # Telegram 机器人接入指南
2
+
3
+ > 通过官方 Telegram Bot API,将本地 DeepSeek Harness 接入 Telegram,支持单聊与群聊、原生快捷指令菜单、交互卡片按键、增量打字机流式输出与权限审批。
4
+
5
+ ---
6
+
7
+ ## 🌟 核心优势
8
+
9
+ - **100% 免公网 IP / 免 Webhook**:基于 Telegram 官方 Long Polling(长轮询)机制,本地电脑或内网服务器即可直连 Telegram Bot API。
10
+ - **零依赖原生代理支持**:内置极简 HTTP/HTTPS CONNECT 隧道代理客户端,国内网络环境下仅需填入代理地址(如 `http://127.0.0.1:7890`)即可极速通信。
11
+ - **增量打字机流式输出**:接入轮次实时生命周期,通过 `editMessageText` 实现平滑打字机流式输出,单条气泡原地更新,告别刷屏。
12
+ - **原生快捷指令菜单 (`Menu` 按键)**:自动通过 `setMyCommands` 与 `setChatMenuButton` 注册全范围指令,输入 `/` 或点击左下角菜单即可一键直达。
13
+ - **原生交互卡片与审批按键**:审批请求下发 Inline Keyboard 按键 `[✓ 批准执行]` / `[✕ 拒绝执行]`,一键点击即时响应。
14
+ - **多工作区与会话持久化**:支持 `/sessions` 查看会话列表、`/use <序号>` 切换、`/workspaces` 调度工作区,重启 DSH 后白名单与会话不丢失。
15
+
16
+ ---
17
+
18
+ ## 🛠️ 第一步:在 Telegram 中创建机器人并获取 Token
19
+
20
+ 1. 打开 Telegram,搜索官方机器人管理号 [@BotFather](https://t.me/BotFather);
21
+ 2. 点击底部 `Start` 或发送 `/newbot` 指令;
22
+ 3. 根据提示依次输入:
23
+ - **机器人昵称**(如 `My DSH Agent`);
24
+ - **机器人用户名**(必须以 `bot` 结尾,如 `my_dsh_agent_bot`);
25
+ 4. 创建成功后,@BotFather 会返回一行 **HTTP API Token**(格式如 `123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ`)。
26
+
27
+ > 💡 **(可选)在 BotFather 中固化快捷指令**:
28
+ > 向 @BotFather 发送 `/setcommands`,选择你的机器人,直接复制并粘贴下方内容发送:
29
+ > ```text
30
+ > new - 新建会话并开始执行 (/new <提示词>)
31
+ > sessions - 列出所有会话列表与切换
32
+ > use - 切换活动会话 (/use <N>)
33
+ > workspaces - 列出本地所有可用工作区
34
+ > status - 查看 Agent 状态与会话摘要
35
+ > stop - 停止当前正在运行的任务
36
+ > end - 结束当前活动会话
37
+ > help - 显示快捷按键与完整帮助
38
+ > ```
39
+
40
+ ---
41
+
42
+ ## ⚙️ 第二步:在 DSH Bridge 中配置与连接
43
+
44
+ 1. 打开 DSH Web 设置页「**远程访问**」→「**IM 机器人**」;
45
+ 2. 在平台选择栏中点击「**Telegram**」卡片;
46
+ 3. 填入刚才获取的 **Bot Token**;
47
+ 4. **网络代理(可选)**:
48
+ - 若在中国大陆地区服务器或个人电脑上运行,填入本地代理地址,如 `http://127.0.0.1:7890`(支持 Clash / v2ray / Squid 等 HTTP/HTTPS 代理);
49
+ - 亦可直接在系统环境变量中设置 `HTTPS_PROXY=http://127.0.0.1:7890`;
50
+ 5. 点击「**保存并连接**」。
51
+
52
+ ---
53
+
54
+ ## 📱 第三步:扫码与自动白名单授权
55
+
56
+ 1. 连接成功后,面板将展示当前机器人的二维码与 `https://t.me/<username>` 直达链接;
57
+ 2. 用手机 Telegram 扫描二维码或点击链接打开与机器人的对话;
58
+ 3. 发送第一条消息(如 `/help` 或 `你好`),系统将**自动将你的 Telegram 账号加入白名单**并持久化;
59
+ 4. 之后即可在 Telegram 里随时随地向 Agent 下达任务指令。
60
+
61
+ ---
62
+
63
+ ## 💬 常用指令与卡片交互
64
+
65
+ | 指令 | 说明 | 交互支持 |
66
+ | :--- | :--- | :--- |
67
+ | *(普通文本)* | 发送给当前活动 Agent 执行任务 | 实时打字机流式输出 |
68
+ | `/new <提示词>` | 在当前工作区新建会话并立即执行 | 启动全新轮次 |
69
+ | `/new <提示词> @N` | 在指定工作区序号新建会话 | 多工作区调度 |
70
+ | `/sessions`(或 `/list`) | 查看所有会话列表 | 附带一键切换按键 |
71
+ | `/use N`(或 `/resume N`) | 切换活动会话至序号 N | 快速切换上下文 |
72
+ | `/workspaces` | 列出本地所有可用项目工作区 | 查看工作区路径 |
73
+ | `/status` | 查看当前 Agent 运行状态看板 | 附带刷新/停止/结束按键 |
74
+ | `/stop` | 强制停止当前正在运行的 Agent 任务 | 即刻中断执行 |
75
+ | `/end` | 结束当前活动会话 | 挂载快捷开始按键 |
76
+ | `/yes` `/no`(或 `1`/`2`) | 响应权限审批请求 | 支持直接点击卡片按钮 |
77
+ | `/help` | 查看快捷按键与完整使用帮助 | 挂载全套功能导航按键 |
78
+
79
+ ---
80
+
81
+ ## 🛡️ 多模态与安全说明
82
+
83
+ 1. **图片与文件双向传输**:
84
+ - 直接向 Telegram 机器人发送图片或文档,系统自动保存到本地并注入提示词供 Agent 解析;
85
+ - Agent 任务执行中生成的图片、文档等产物,在轮次结束时会自动推送回 Telegram 聊天。
86
+ 2. **安全白名单拦截**:
87
+ - 仅白名单内的用户消息会被放行给 Agent;非白名单用户发送的消息会被直接忽略,绝不消耗 Token 或喂给模型。
88
+ 3. **敏感操作审批**:
89
+ - 当 Agent 尝试执行系统命令或敏感文件读写时,Telegram 会自动下发 `[✓ 批准执行]` / `[✕ 拒绝执行]` 交互按键,10 分钟未处理自动超时拒绝。
@@ -0,0 +1,378 @@
1
+ // lib/auth/login-template.js
2
+ // DeepSeek Harness 官方质感高颜值远程访问认证登录页(零外部 CDN 依赖,响应式适配手机与桌面)
3
+
4
+ export function renderLoginPage({ error = '', hasPassword = true, locked = false } = {}) {
5
+ return `<!DOCTYPE html>
6
+ <html lang="zh-CN">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
10
+ <title>远程访问认证 - DeepSeek Harness</title>
11
+ <style>
12
+ :root {
13
+ --bg: #121413;
14
+ --card-bg: rgba(26, 29, 27, 0.85);
15
+ --card-border: rgba(255, 255, 255, 0.08);
16
+ --brand: #22c55e;
17
+ --brand-hover: #16a34a;
18
+ --brand-glow: rgba(34, 197, 94, 0.25);
19
+ --text: #f3f4f6;
20
+ --text-secondary: #9ca3af;
21
+ --input-bg: rgba(18, 20, 19, 0.7);
22
+ --input-border: rgba(255, 255, 255, 0.12);
23
+ --error-bg: rgba(239, 68, 68, 0.12);
24
+ --error-border: rgba(239, 68, 68, 0.3);
25
+ --error-text: #f87171;
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ margin: 0;
31
+ padding: 0;
32
+ -webkit-tap-highlight-color: transparent;
33
+ }
34
+
35
+ body {
36
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
37
+ background-color: var(--bg);
38
+ background-image:
39
+ radial-gradient(circle at 50% 0%, rgba(34, 197, 94, 0.12) 0%, transparent 50%),
40
+ radial-gradient(circle at 100% 100%, rgba(16, 185, 129, 0.05) 0%, transparent 40%);
41
+ color: var(--text);
42
+ min-height: 100vh;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ padding: 20px;
47
+ }
48
+
49
+ .container {
50
+ width: 100%;
51
+ max-width: 400px;
52
+ perspective: 1000px;
53
+ }
54
+
55
+ .card {
56
+ background: var(--card-bg);
57
+ backdrop-filter: blur(20px);
58
+ -webkit-backdrop-filter: blur(20px);
59
+ border: 1px solid var(--card-border);
60
+ border-radius: 20px;
61
+ padding: 36px 28px;
62
+ box-shadow:
63
+ 0 20px 40px -15px rgba(0, 0, 0, 0.5),
64
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
65
+ animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
66
+ }
67
+
68
+ @keyframes fadeIn {
69
+ from { opacity: 0; transform: translateY(16px) scale(0.98); }
70
+ to { opacity: 1; transform: translateY(0) scale(1); }
71
+ }
72
+
73
+ .logo-area {
74
+ text-align: center;
75
+ margin-bottom: 28px;
76
+ }
77
+
78
+ .logo-icon {
79
+ width: 56px;
80
+ height: 56px;
81
+ margin: 0 auto 16px;
82
+ border-radius: 16px;
83
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.05));
84
+ border: 1px solid rgba(34, 197, 94, 0.3);
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ box-shadow: 0 8px 16px -4px var(--brand-glow);
89
+ }
90
+
91
+ .logo-icon svg {
92
+ width: 32px;
93
+ height: 32px;
94
+ fill: var(--brand);
95
+ }
96
+
97
+ h1 {
98
+ font-size: 20px;
99
+ font-weight: 600;
100
+ letter-spacing: -0.01em;
101
+ color: #ffffff;
102
+ margin-bottom: 6px;
103
+ }
104
+
105
+ .subtitle {
106
+ font-size: 13px;
107
+ color: var(--text-secondary);
108
+ line-height: 1.5;
109
+ }
110
+
111
+ .alert {
112
+ background: var(--error-bg);
113
+ border: 1px solid var(--error-border);
114
+ color: var(--error-text);
115
+ padding: 10px 14px;
116
+ border-radius: 10px;
117
+ font-size: 13px;
118
+ margin-bottom: 20px;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 8px;
122
+ animation: shake 0.4s ease;
123
+ }
124
+
125
+ @keyframes shake {
126
+ 0%, 100% { transform: translateX(0); }
127
+ 20%, 60% { transform: translateX(-6px); }
128
+ 40%, 80% { transform: translateX(6px); }
129
+ }
130
+
131
+ .form-group {
132
+ margin-bottom: 20px;
133
+ }
134
+
135
+ label {
136
+ display: block;
137
+ font-size: 12px;
138
+ font-weight: 500;
139
+ color: var(--text-secondary);
140
+ margin-bottom: 8px;
141
+ text-transform: uppercase;
142
+ letter-spacing: 0.05em;
143
+ }
144
+
145
+ .input-wrapper {
146
+ position: relative;
147
+ display: flex;
148
+ align-items: center;
149
+ }
150
+
151
+ input[type="password"], input[type="text"] {
152
+ width: 100%;
153
+ background: var(--input-bg);
154
+ border: 1px solid var(--input-border);
155
+ border-radius: 12px;
156
+ padding: 14px 44px 14px 16px;
157
+ font-size: 15px;
158
+ color: #ffffff;
159
+ outline: none;
160
+ transition: all 0.2s ease;
161
+ font-family: inherit;
162
+ }
163
+
164
+ input:focus {
165
+ border-color: var(--brand);
166
+ box-shadow: 0 0 0 3px var(--brand-glow);
167
+ background: rgba(18, 20, 19, 0.9);
168
+ }
169
+
170
+ .toggle-pwd {
171
+ position: absolute;
172
+ right: 12px;
173
+ background: none;
174
+ border: none;
175
+ cursor: pointer;
176
+ color: var(--text-secondary);
177
+ padding: 6px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ border-radius: 6px;
182
+ transition: color 0.2s;
183
+ }
184
+
185
+ .toggle-pwd:hover {
186
+ color: #ffffff;
187
+ }
188
+
189
+ .submit-btn {
190
+ width: 100%;
191
+ background: var(--brand);
192
+ color: #0b150f;
193
+ border: none;
194
+ border-radius: 12px;
195
+ padding: 14px;
196
+ font-size: 15px;
197
+ font-weight: 600;
198
+ cursor: pointer;
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ gap: 8px;
203
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
204
+ box-shadow: 0 4px 12px var(--brand-glow);
205
+ }
206
+
207
+ .submit-btn:hover {
208
+ background: var(--brand-hover);
209
+ transform: translateY(-1px);
210
+ box-shadow: 0 6px 16px rgba(34, 197, 94, 0.35);
211
+ }
212
+
213
+ .submit-btn:active {
214
+ transform: translateY(1px);
215
+ }
216
+
217
+ .submit-btn:disabled {
218
+ opacity: 0.6;
219
+ cursor: not-allowed;
220
+ transform: none;
221
+ }
222
+
223
+ .spinner {
224
+ width: 18px;
225
+ height: 18px;
226
+ border: 2px solid rgba(11, 21, 15, 0.3);
227
+ border-top-color: #0b150f;
228
+ border-radius: 50%;
229
+ animation: spin 0.6s linear infinite;
230
+ display: none;
231
+ }
232
+
233
+ @keyframes spin {
234
+ to { transform: rotate(360deg); }
235
+ }
236
+
237
+ .footer {
238
+ text-align: center;
239
+ margin-top: 24px;
240
+ font-size: 12px;
241
+ color: rgba(255, 255, 255, 0.35);
242
+ }
243
+
244
+ .footer a {
245
+ color: var(--text-secondary);
246
+ text-decoration: none;
247
+ }
248
+ </style>
249
+ </head>
250
+ <body>
251
+ <div class="container">
252
+ <div class="card">
253
+ <div class="logo-area">
254
+ <div class="logo-icon">
255
+ <svg viewBox="0 0 24 24">
256
+ <path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
257
+ </svg>
258
+ </div>
259
+ <h1>DeepSeek Harness</h1>
260
+ <div class="subtitle">远程安全访问认证</div>
261
+ </div>
262
+
263
+ <div id="errorAlert" class="alert" style="${error ? '' : 'display: none;'}">
264
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
265
+ <circle cx="12" cy="12" r="10"></circle>
266
+ <line x1="12" y1="8" x2="12" y2="12"></line>
267
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
268
+ </svg>
269
+ <span id="errorMsg">${error || ''}</span>
270
+ </div>
271
+
272
+ <form id="loginForm" onsubmit="return handleLogin(event)">
273
+ <div class="form-group">
274
+ <label for="password">访问密码 / PIN 码</label>
275
+ <div class="input-wrapper">
276
+ <input type="password" id="password" name="password" placeholder="请输入管理员设置的访问密码" autocomplete="current-password" autofocus required>
277
+ <button type="button" class="toggle-pwd" onclick="togglePassword()" title="显示/隐藏密码">
278
+ <svg id="eyeIcon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
279
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
280
+ <circle cx="12" cy="12" r="3"></circle>
281
+ </svg>
282
+ </button>
283
+ </div>
284
+ </div>
285
+
286
+ <button type="submit" id="submitBtn" class="submit-btn">
287
+ <span class="spinner" id="btnSpinner"></span>
288
+ <span id="btnText">解锁并访问</span>
289
+ </button>
290
+ </form>
291
+
292
+ <div style="margin-top: 16px; text-align: center;">
293
+ <a href="javascript:void(0)" onclick="toggleForgotGuide()" style="font-size: 12px; color: var(--text-secondary); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; transition: color 0.2s;">
294
+ ❓ 忘记密码怎么办?
295
+ </a>
296
+ </div>
297
+
298
+ <div id="forgotGuide" style="display: none; margin-top: 14px; padding: 12px 14px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; font-size: 12px; line-height: 1.6; color: var(--text-secondary); text-align: left;">
299
+ <div style="font-weight: 600; color: #f3f4f6; margin-bottom: 4px;">🛟 找回与重置密码指引:</div>
300
+ <div>1. <strong>电脑本机免密直连</strong>:在运行本程序的电脑本机打开本控制台,物理机享有永久免密特权,可直接修改或清除密码。</div>
301
+ <div style="margin-top: 4px;">2. <strong>服务器救急指令</strong>:在宿主电脑终端执行 <code style="background: rgba(0,0,0,0.4); padding: 2px 6px; border-radius: 4px; color: #34d399; font-family: monospace;">touch ~/.dsh/dsh-bridge/reset-auth</code> 即可瞬间恢复初始免密状态。</div>
302
+ </div>
303
+
304
+ <div class="footer">
305
+ 由 <a href="https://github.com/wenbin-wb/dsh-bridge" target="_blank">dsh-bridge</a> 安全网关守护
306
+ </div>
307
+ </div>
308
+ </div>
309
+
310
+ <script>
311
+ function toggleForgotGuide() {
312
+ const g = document.getElementById('forgotGuide');
313
+ g.style.display = g.style.display === 'none' ? 'block' : 'none';
314
+ }
315
+
316
+ function togglePassword() {
317
+ const input = document.getElementById('password');
318
+ const icon = document.getElementById('eyeIcon');
319
+ if (input.type === 'password') {
320
+ input.type = 'text';
321
+ icon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
322
+ } else {
323
+ input.type = 'password';
324
+ icon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
325
+ }
326
+ }
327
+
328
+ async function handleLogin(e) {
329
+ e.preventDefault();
330
+ const pwd = document.getElementById('password').value;
331
+ const btn = document.getElementById('submitBtn');
332
+ const spinner = document.getElementById('btnSpinner');
333
+ const btnText = document.getElementById('btnText');
334
+ const alert = document.getElementById('errorAlert');
335
+ const errorMsg = document.getElementById('errorMsg');
336
+
337
+ btn.disabled = true;
338
+ spinner.style.display = 'inline-block';
339
+ btnText.textContent = '验证中…';
340
+ alert.style.display = 'none';
341
+
342
+ try {
343
+ const resp = await fetch('/__dsh_bridge__/login', {
344
+ method: 'POST',
345
+ headers: { 'Content-Type': 'application/json' },
346
+ body: JSON.stringify({ password: pwd })
347
+ });
348
+ const data = await resp.json();
349
+ if (data.ok) {
350
+ btnText.textContent = '✓ 认证成功,正在跳转…';
351
+ setTimeout(() => {
352
+ window.location.reload();
353
+ }, 300);
354
+ } else {
355
+ errorMsg.textContent = data.error || '访问密码错误';
356
+ alert.style.display = 'flex';
357
+ alert.style.animation = 'none';
358
+ void alert.offsetWidth; // 触发 reflow
359
+ alert.style.animation = 'shake 0.4s ease';
360
+ btn.disabled = false;
361
+ spinner.style.display = 'none';
362
+ btnText.textContent = '解锁并访问';
363
+ document.getElementById('password').focus();
364
+ document.getElementById('password').select();
365
+ }
366
+ } catch (err) {
367
+ errorMsg.textContent = '网络请求失败,请重试';
368
+ alert.style.display = 'flex';
369
+ btn.disabled = false;
370
+ spinner.style.display = 'none';
371
+ btnText.textContent = '解锁并访问';
372
+ }
373
+ return false;
374
+ }
375
+ </script>
376
+ </body>
377
+ </html>`;
378
+ }