@qzsy/dsh-bridge 2.18.0 → 3.0.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +12 -1098
  2. package/LICENSE +21 -21
  3. package/README.md +72 -476
  4. package/client/build.mjs +1 -3
  5. package/client/client.js +381 -9747
  6. package/client/index.js +242 -6828
  7. package/client/locales.mjs +99 -233
  8. package/cordis.patch.yml +5 -3
  9. package/lib/config.js +93 -0
  10. package/lib/data-dir.js +16 -0
  11. package/lib/dsh-cookie.js +111 -0
  12. package/lib/gate.js +382 -0
  13. package/lib/identity.js +23 -0
  14. package/lib/index.js +302 -3496
  15. package/lib/saas/tunnel-client.mjs +529 -467
  16. package/locale/en.json +3 -3
  17. package/locale/zh.json +3 -3
  18. package/package.json +15 -70
  19. package/README.en.md +0 -488
  20. package/client/host-families.js +0 -442
  21. package/client/mobile-styles.js +0 -1299
  22. package/client/mobile-yield.js +0 -219
  23. package/client/panel-locales.mjs +0 -777
  24. package/client/picker-yield.js +0 -77
  25. package/client/resource-url-compat.js +0 -90
  26. package/client/settings-nav-icon.js +0 -105
  27. package/client/theme-tokens.js +0 -49
  28. package/client/tunnel-guard.js +0 -46
  29. package/client/unlock-manager.js +0 -144
  30. package/lib/auth/dsh-native-cookie.js +0 -148
  31. package/lib/auth/login-template.js +0 -516
  32. package/lib/auth/manager.js +0 -596
  33. package/lib/bridge-rpc-constants.js +0 -66
  34. package/lib/bridge-rpc.js +0 -722
  35. package/lib/cloudflared-manager.mjs +0 -1277
  36. package/lib/cloudflared-network-args.js +0 -99
  37. package/lib/cloudflared-process-scan.js +0 -153
  38. package/lib/compat.js +0 -172
  39. package/lib/connection-compat.js +0 -115
  40. package/lib/feishu/comment.js +0 -50
  41. package/lib/feishu/gateway.js +0 -644
  42. package/lib/feishu/index.js +0 -432
  43. package/lib/feishu/lark-bundled.mjs +0 -131929
  44. package/lib/feishu/markdown.js +0 -148
  45. package/lib/feishu/node.js +0 -1152
  46. package/lib/feishu/slash-commands.js +0 -255
  47. package/lib/platform/account-ops.js +0 -248
  48. package/lib/platform/activity.js +0 -169
  49. package/lib/platform/ask-user.js +0 -293
  50. package/lib/platform/base.js +0 -147
  51. package/lib/platform/changes-digest.js +0 -93
  52. package/lib/platform/commands.js +0 -267
  53. package/lib/platform/config-restore.js +0 -78
  54. package/lib/platform/conversation-bridge.js +0 -1544
  55. package/lib/platform/dsh-storage.js +0 -222
  56. package/lib/platform/index.js +0 -10
  57. package/lib/platform/instances.js +0 -458
  58. package/lib/platform/manager.js +0 -130
  59. package/lib/platform/message-split.js +0 -229
  60. package/lib/platform/session-catalog.js +0 -405
  61. package/lib/platform/session-config.js +0 -52
  62. package/lib/platform/stream-slices.js +0 -21
  63. package/lib/qq/gateway.js +0 -1107
  64. package/lib/qq/index.js +0 -561
  65. package/lib/qq/node.js +0 -1153
  66. package/lib/qq/qqbot-bundled.mjs +0 -8273
  67. package/lib/restart-helper.mjs +0 -133
  68. package/lib/security/log-sanitize.js +0 -50
  69. package/lib/security/path-validator.js +0 -195
  70. package/lib/security/rate-limiter.js +0 -93
  71. package/lib/session-strip.js +0 -100
  72. package/lib/sse-keepalive.js +0 -115
  73. package/lib/telegram/gateway.js +0 -643
  74. package/lib/telegram/index.js +0 -255
  75. package/lib/telegram/node.js +0 -410
  76. package/lib/tunnel-client.mjs +0 -537
  77. package/lib/wechat/gateway.js +0 -1000
  78. package/lib/wechat/index.js +0 -282
  79. package/lib/wechat/media.js +0 -302
  80. package/lib/wechat/node.js +0 -360
  81. package/llms.txt +0 -36
@@ -1,516 +0,0 @@
1
- // lib/auth/login-template.js
2
- // DeepSeek Harness 官方质感高颜值远程访问认证登录页(零外部 CDN 依赖,响应式适配手机与桌面)
3
-
4
- // 登录页文案(中 / 英)。语言来源:服务端按请求的 Accept-Language 传 lang
5
- // (缺省/zh 开头 → 中文,保持改造前行为;其它 → 英文);页面内联脚本用注入的 I18N
6
- // 渲染动态文案,并把 auth/manager.js 返回的已知中文错误映射成英文(未知文案原样显示,
7
- // 不编造)。
8
- const LOGIN_I18N = {
9
- zh: {
10
- title: '远程访问认证 - DeepSeek Harness',
11
- heading: '远程安全访问认证',
12
- headingNoPassword: '远程访问 · 尚未设置密码',
13
- label: '访问密码 / PIN 码',
14
- toggleTitle: '显示/隐藏密码',
15
- unlock: '解锁并访问',
16
- placeholderWithPassword: '请输入管理员设置的访问密码',
17
- placeholderNoPassword: '(尚未设置密码)',
18
- placeholderAsk: '请输入访问密码',
19
- noticePasswordOnly: '管理员尚未设置访问密码(仅密码模式),请联系管理员在控制台「安全认证」中设置密码后再访问。',
20
- buttonPasswordOnly: '无法访问(未设置密码)',
21
- noticeTokenOnly: '当前为「仅安全 Token」模式,请使用管理员分享的专属二维码或带 Token 的链接访问(不支持密码登录)。',
22
- buttonTokenOnly: '需专属 Token',
23
- noticeOpen: '当前访问认证已开启,但管理员尚未设置任何访问密码 —— 免密开放状态,任何访客均可直接进入。请管理员尽快在控制台「安全认证」中设置访问密码。',
24
- buttonOpen: '直接进入',
25
- tokenOnlyHint: '仅凭专属 Token 进入 · 无 Token 将无法访问',
26
- openHint: '当前为免密开放状态 · 请管理员尽快在控制台设置访问密码',
27
- forgot: '❓ 忘记密码怎么办?',
28
- guideTitle: '🛟 找回与重置密码指引:',
29
- guide1: '1. <strong>电脑本机免密直连</strong>:在运行本程序的电脑本机打开本控制台,物理机享有永久免密特权,可直接修改或清除密码。',
30
- guide2: '2. <strong>服务器 / 无头环境</strong>:救急重置步骤请参见 <a href="https://github.com/wenbin-wb/dsh-bridge#readme" target="_blank" style="color: #93c5fd;">GitHub README</a> 的「三重容灾保命体系」章节。',
31
- footer: '由 <a href="https://github.com/wenbin-wb/dsh-bridge" target="_blank">dsh-bridge</a> 安全网关守护',
32
- verifying: '验证中…',
33
- success: '✓ 认证成功,正在跳转…',
34
- fallbackError: '访问密码错误',
35
- networkError: '网络请求失败,请重试',
36
- errors: {
37
- '访问密码错误': '访问密码错误',
38
- '尝试次数过多,请稍后再试': '尝试次数过多,请稍后再试',
39
- '尝试次数过多,请 60 秒后再试': '尝试次数过多,请 60 秒后再试',
40
- '当前仅允许专属安全 Token 扫码访问,不支持密码登录': '当前仅允许专属安全 Token 扫码访问,不支持密码登录',
41
- },
42
- },
43
- en: {
44
- title: 'Remote access authentication - DeepSeek Harness',
45
- heading: 'Remote secure access authentication',
46
- headingNoPassword: 'Remote access · no password set',
47
- label: 'Access password / PIN',
48
- toggleTitle: 'Show/hide password',
49
- unlock: 'Unlock and continue',
50
- placeholderWithPassword: 'Enter the access password set by the administrator',
51
- placeholderNoPassword: '(no password set yet)',
52
- placeholderAsk: 'Enter the access password',
53
- noticePasswordOnly: 'The administrator has not set an access password (password-only mode). Ask them to set one under “Security” in the console, then try again.',
54
- buttonPasswordOnly: 'Unavailable (no password set)',
55
- noticeTokenOnly: 'This bridge is in “Security Token only” mode. Use the dedicated QR code or the Token link shared by the administrator (password login is disabled).',
56
- buttonTokenOnly: 'Token required',
57
- noticeOpen: 'Access protection is enabled but the administrator has not set any password — the entry is currently open to anyone. Ask the administrator to set an access password under “Security” in the console.',
58
- buttonOpen: 'Enter anyway',
59
- tokenOnlyHint: 'Dedicated Token required · access without a Token is denied',
60
- openHint: 'Currently open (no password) · ask the administrator to set an access password',
61
- forgot: '❓ Forgot the password?',
62
- guideTitle: '🛟 How to recover or reset the password:',
63
- guide1: '1. <strong>From the computer itself</strong>: open this console on the machine running the program — the physical machine always has password-free access, so you can change or clear the password at any time.',
64
- guide2: '2. <strong>Server / headless environment</strong>: see the <a href="https://github.com/wenbin-wb/dsh-bridge/blob/main/README.en.md" target="_blank" style="color: #93c5fd;">English README</a> section “Triple Disaster Recovery (Never Locked Out)”.',
65
- footer: 'Protected by the <a href="https://github.com/wenbin-wb/dsh-bridge" target="_blank">dsh-bridge</a> security gateway',
66
- verifying: 'Verifying…',
67
- success: '✓ Authenticated, redirecting…',
68
- fallbackError: 'Incorrect access password',
69
- networkError: 'Network request failed, please retry',
70
- errors: {
71
- '访问密码错误': 'Incorrect access password',
72
- '尝试次数过多,请稍后再试': 'Too many attempts, please try again later',
73
- '尝试次数过多,请 60 秒后再试': 'Too many attempts, please try again in 60 seconds',
74
- '当前仅允许专属安全 Token 扫码访问,不支持密码登录': 'Only dedicated Security Token access is allowed; password login is disabled',
75
- },
76
- },
77
- };
78
-
79
- /**
80
- * 登录页与登录错误共用的语言判定:缺省或 zh 开头 → 中文(保持改造前行为),
81
- * 其余(en/de/ja/fr/* 等)→ 英文。两处必须共用,否则会出现「英文登录页 + 中文报错」。
82
- * @param {string} [acceptLanguage] 请求的 Accept-Language 头
83
- * @returns {'zh'|'en'}
84
- */
85
- export function pickLoginLang(acceptLanguage = '') {
86
- const value = String(acceptLanguage || '').toLowerCase();
87
- return !value || value.startsWith('zh') ? 'zh' : 'en';
88
- }
89
-
90
- /** 服务端认证错误的中英映射:英文请求返回英文文案,未知文案原样返回(不编造)。 */
91
- export function localizeAuthError(message, lang = 'zh') {
92
- if (!message) return message;
93
- if (!String(lang || '').toLowerCase().startsWith('en')) return message;
94
- return LOGIN_I18N.en.errors[message] || message;
95
- }
96
-
97
- export function renderLoginPage({ error = '', hasPassword = true, locked = false, noPasswordConfigured = false, mode = 'token_and_password', lang = 'zh' } = {}) {
98
- // 管理员从未设置任何密码时的如实提示:
99
- // - password_only 模式:门禁真实存在但管理员未初始化密码,登录页应明确告知,
100
- // 而不是让访客试一个永远不对的密码。
101
- // - token_and_password(默认):认证已开启但无任何密码可校验,密码登录对任意
102
- // 输入都放行(宽松降级),如实提示"免密开放"避免假门禁误导。
103
- // - token_only:仅专属 Token 可进,与密码无关,提示走二维码 / Token 链接。
104
- const isEn = String(lang || '').toLowerCase().startsWith('en');
105
- const L = LOGIN_I18N[isEn ? 'en' : 'zh'];
106
- // 已知中文错误在英文页面里映射为英文;未知文案原样显示(不编造)
107
- const localizeError = (message) => localizeAuthError(message, isEn ? 'en' : 'zh');
108
- let noPasswordNotice = '';
109
- let buttonLabel = L.unlock;
110
- if (noPasswordConfigured) {
111
- if (mode === 'password_only') {
112
- noPasswordNotice = L.noticePasswordOnly;
113
- buttonLabel = L.buttonPasswordOnly;
114
- } else if (mode === 'token_only') {
115
- noPasswordNotice = L.noticeTokenOnly;
116
- buttonLabel = L.buttonTokenOnly;
117
- } else {
118
- noPasswordNotice = L.noticeOpen;
119
- buttonLabel = L.buttonOpen;
120
- }
121
- }
122
- const heading = noPasswordConfigured ? L.headingNoPassword : L.heading;
123
- const inputPlaceholder = hasPassword
124
- ? L.placeholderWithPassword
125
- : (noPasswordConfigured ? L.placeholderNoPassword : L.placeholderAsk);
126
- const errorText = localizeError(error);
127
- return `<!DOCTYPE html>
128
- <html lang="${isEn ? 'en' : 'zh-CN'}">
129
- <head>
130
- <meta charset="UTF-8">
131
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
132
- <meta name="apple-mobile-web-app-capable" content="yes">
133
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
134
- <meta name="mobile-web-app-capable" content="yes">
135
- <meta name="theme-color" content="#121413">
136
- <title>${L.title}</title>
137
- <style>
138
- :root {
139
- --bg: #121413;
140
- --card-bg: rgba(26, 29, 27, 0.85);
141
- --card-border: rgba(255, 255, 255, 0.08);
142
- --brand: #22c55e;
143
- --brand-hover: #16a34a;
144
- --brand-glow: rgba(34, 197, 94, 0.25);
145
- --text: #f3f4f6;
146
- --text-secondary: #9ca3af;
147
- --input-bg: rgba(18, 20, 19, 0.7);
148
- --input-border: rgba(255, 255, 255, 0.12);
149
- --error-bg: rgba(239, 68, 68, 0.12);
150
- --error-border: rgba(239, 68, 68, 0.3);
151
- --error-text: #f87171;
152
- }
153
-
154
- * {
155
- box-sizing: border-box;
156
- margin: 0;
157
- padding: 0;
158
- -webkit-tap-highlight-color: transparent;
159
- }
160
-
161
- body {
162
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
163
- background-color: var(--bg);
164
- background-image:
165
- radial-gradient(circle at 50% 0%, rgba(34, 197, 94, 0.12) 0%, transparent 50%),
166
- radial-gradient(circle at 100% 100%, rgba(16, 185, 129, 0.05) 0%, transparent 40%);
167
- color: var(--text);
168
- min-height: 100vh;
169
- display: flex;
170
- align-items: center;
171
- justify-content: center;
172
- padding: 20px;
173
- }
174
-
175
- .container {
176
- width: 100%;
177
- max-width: 400px;
178
- perspective: 1000px;
179
- }
180
-
181
- .card {
182
- background: var(--card-bg);
183
- backdrop-filter: blur(20px);
184
- -webkit-backdrop-filter: blur(20px);
185
- border: 1px solid var(--card-border);
186
- border-radius: 20px;
187
- padding: 36px 28px;
188
- box-shadow:
189
- 0 20px 40px -15px rgba(0, 0, 0, 0.5),
190
- 0 0 0 1px rgba(255, 255, 255, 0.05);
191
- animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
192
- }
193
-
194
- @keyframes fadeIn {
195
- from { opacity: 0; transform: translateY(16px) scale(0.98); }
196
- to { opacity: 1; transform: translateY(0) scale(1); }
197
- }
198
-
199
- .logo-area {
200
- text-align: center;
201
- margin-bottom: 28px;
202
- }
203
-
204
- .logo-icon {
205
- width: 56px;
206
- height: 56px;
207
- margin: 0 auto 16px;
208
- border-radius: 16px;
209
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.05));
210
- border: 1px solid rgba(34, 197, 94, 0.3);
211
- display: flex;
212
- align-items: center;
213
- justify-content: center;
214
- box-shadow: 0 8px 16px -4px var(--brand-glow);
215
- }
216
-
217
- .logo-icon svg {
218
- width: 32px;
219
- height: 32px;
220
- fill: var(--brand);
221
- }
222
-
223
- h1 {
224
- font-size: 20px;
225
- font-weight: 600;
226
- letter-spacing: -0.01em;
227
- color: #ffffff;
228
- margin-bottom: 6px;
229
- }
230
-
231
- .subtitle {
232
- font-size: 13px;
233
- color: var(--text-secondary);
234
- line-height: 1.5;
235
- }
236
-
237
- .alert {
238
- background: var(--error-bg);
239
- border: 1px solid var(--error-border);
240
- color: var(--error-text);
241
- padding: 10px 14px;
242
- border-radius: 10px;
243
- font-size: 13px;
244
- margin-bottom: 20px;
245
- display: flex;
246
- align-items: center;
247
- gap: 8px;
248
- animation: shake 0.4s ease;
249
- }
250
-
251
- @keyframes shake {
252
- 0%, 100% { transform: translateX(0); }
253
- 20%, 60% { transform: translateX(-6px); }
254
- 40%, 80% { transform: translateX(6px); }
255
- }
256
-
257
- .form-group {
258
- margin-bottom: 20px;
259
- }
260
-
261
- label {
262
- display: block;
263
- font-size: 12px;
264
- font-weight: 500;
265
- color: var(--text-secondary);
266
- margin-bottom: 8px;
267
- text-transform: uppercase;
268
- letter-spacing: 0.05em;
269
- }
270
-
271
- .input-wrapper {
272
- position: relative;
273
- display: flex;
274
- align-items: center;
275
- }
276
-
277
- input[type="password"], input[type="text"] {
278
- width: 100%;
279
- background: var(--input-bg);
280
- border: 1px solid var(--input-border);
281
- border-radius: 12px;
282
- padding: 14px 44px 14px 16px;
283
- font-size: 15px;
284
- color: #ffffff;
285
- outline: none;
286
- transition: all 0.2s ease;
287
- font-family: inherit;
288
- }
289
-
290
- input:focus {
291
- border-color: var(--brand);
292
- box-shadow: 0 0 0 3px var(--brand-glow);
293
- background: rgba(18, 20, 19, 0.9);
294
- }
295
-
296
- .toggle-pwd {
297
- position: absolute;
298
- right: 12px;
299
- background: none;
300
- border: none;
301
- cursor: pointer;
302
- color: var(--text-secondary);
303
- padding: 6px;
304
- display: flex;
305
- align-items: center;
306
- justify-content: center;
307
- border-radius: 6px;
308
- transition: color 0.2s;
309
- }
310
-
311
- .toggle-pwd:hover {
312
- color: #ffffff;
313
- }
314
-
315
- .submit-btn {
316
- width: 100%;
317
- background: var(--brand);
318
- color: #0b150f;
319
- border: none;
320
- border-radius: 12px;
321
- padding: 14px;
322
- font-size: 15px;
323
- font-weight: 600;
324
- cursor: pointer;
325
- display: flex;
326
- align-items: center;
327
- justify-content: center;
328
- gap: 8px;
329
- transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
330
- box-shadow: 0 4px 12px var(--brand-glow);
331
- }
332
-
333
- .submit-btn:hover {
334
- background: var(--brand-hover);
335
- transform: translateY(-1px);
336
- box-shadow: 0 6px 16px rgba(34, 197, 94, 0.35);
337
- }
338
-
339
- .submit-btn:active {
340
- transform: translateY(1px);
341
- }
342
-
343
- .submit-btn:disabled {
344
- opacity: 0.6;
345
- cursor: not-allowed;
346
- transform: none;
347
- }
348
-
349
- .spinner {
350
- width: 18px;
351
- height: 18px;
352
- border: 2px solid rgba(11, 21, 15, 0.3);
353
- border-top-color: #0b150f;
354
- border-radius: 50%;
355
- animation: spin 0.6s linear infinite;
356
- display: none;
357
- }
358
-
359
- @keyframes spin {
360
- to { transform: rotate(360deg); }
361
- }
362
-
363
- .footer {
364
- text-align: center;
365
- margin-top: 24px;
366
- font-size: 12px;
367
- color: rgba(255, 255, 255, 0.35);
368
- }
369
-
370
- .footer a {
371
- color: var(--text-secondary);
372
- text-decoration: none;
373
- }
374
- </style>
375
- </head>
376
- <body>
377
- <div class="container">
378
- <div class="card">
379
- <div class="logo-area">
380
- <div class="logo-icon">
381
- <svg viewBox="0 0 24 24">
382
- <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"/>
383
- </svg>
384
- </div>
385
- <h1>DeepSeek Harness</h1>
386
- <div class="subtitle">${heading}</div>
387
- </div>
388
-
389
- ${noPasswordNotice ? `
390
- <div id="noPwdNotice" style="background: rgba(245,158,11,0.12); border: 1px solid rgba(245,158,11,0.35); color: #fbbf24; padding: 10px 14px; border-radius: 10px; font-size: 13px; line-height: 1.6; margin-bottom: 16px; display: flex; align-items: flex-start; gap: 8px;">
391
- <span style="flex-shrink:0;">⚠️</span>
392
- <span>${noPasswordNotice}</span>
393
- </div>` : ''}
394
-
395
- <div id="errorAlert" class="alert" style="${errorText ? '' : 'display: none;'}">
396
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
397
- <circle cx="12" cy="12" r="10"></circle>
398
- <line x1="12" y1="8" x2="12" y2="12"></line>
399
- <line x1="12" y1="16" x2="12.01" y2="16"></line>
400
- </svg>
401
- <span id="errorMsg">${errorText}</span>
402
- </div>
403
-
404
- <form id="loginForm" onsubmit="return handleLogin(event)">
405
- <div class="form-group">
406
- <label for="password">${L.label}</label>
407
- <div class="input-wrapper">
408
- <input type="password" id="password" name="password" placeholder="${inputPlaceholder}" autocomplete="current-password" autofocus ${hasPassword ? 'required' : ''}>
409
- <button type="button" class="toggle-pwd" onclick="togglePassword()" title="${L.toggleTitle}">
410
- <svg id="eyeIcon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
411
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
412
- <circle cx="12" cy="12" r="3"></circle>
413
- </svg>
414
- </button>
415
- </div>
416
- </div>
417
-
418
- <button type="submit" id="submitBtn" class="submit-btn">
419
- <span class="spinner" id="btnSpinner"></span>
420
- <span id="btnText">${buttonLabel}</span>
421
- </button>
422
- </form>
423
-
424
- ${!hasPassword && noPasswordConfigured && mode !== 'password_only' ? `
425
- <div style="margin-top: 12px; text-align: center;">
426
- <span style="font-size: 12px; color: rgba(255,255,255,0.45);">${mode === 'token_only' ? L.tokenOnlyHint : L.openHint}</span>
427
- </div>` : ''}
428
-
429
- <div style="margin-top: 16px; text-align: center;">
430
- <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;">
431
- ${L.forgot}
432
- </a>
433
- </div>
434
-
435
- <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;">
436
- <div style="font-weight: 600; color: #f3f4f6; margin-bottom: 4px;">${L.guideTitle}</div>
437
- <div>${L.guide1}</div>
438
- <div style="margin-top: 4px;">${L.guide2}</div>
439
- </div>
440
-
441
- <div class="footer">
442
- ${L.footer}
443
- </div>
444
- </div>
445
- </div>
446
-
447
- <script>
448
- const I18N = ${JSON.stringify({ verifying: L.verifying, success: L.success, fallbackError: L.fallbackError, networkError: L.networkError, unlock: L.unlock })};
449
- function toggleForgotGuide() {
450
- const g = document.getElementById('forgotGuide');
451
- g.style.display = g.style.display === 'none' ? 'block' : 'none';
452
- }
453
-
454
- function togglePassword() {
455
- const input = document.getElementById('password');
456
- const icon = document.getElementById('eyeIcon');
457
- if (input.type === 'password') {
458
- input.type = 'text';
459
- 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>';
460
- } else {
461
- input.type = 'password';
462
- 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>';
463
- }
464
- }
465
-
466
- async function handleLogin(e) {
467
- e.preventDefault();
468
- const pwd = document.getElementById('password').value;
469
- const btn = document.getElementById('submitBtn');
470
- const spinner = document.getElementById('btnSpinner');
471
- const btnText = document.getElementById('btnText');
472
- const alert = document.getElementById('errorAlert');
473
- const errorMsg = document.getElementById('errorMsg');
474
-
475
- btn.disabled = true;
476
- spinner.style.display = 'inline-block';
477
- btnText.textContent = I18N.verifying;
478
- alert.style.display = 'none';
479
-
480
- try {
481
- const resp = await fetch('/__dsh_bridge__/login', {
482
- method: 'POST',
483
- headers: { 'Content-Type': 'application/json' },
484
- body: JSON.stringify({ password: pwd })
485
- });
486
- const data = await resp.json();
487
- if (data.ok) {
488
- btnText.textContent = I18N.success;
489
- setTimeout(() => {
490
- window.location.reload();
491
- }, 300);
492
- } else {
493
- errorMsg.textContent = data.error || I18N.fallbackError;
494
- alert.style.display = 'flex';
495
- alert.style.animation = 'none';
496
- void alert.offsetWidth; // Force a reflow
497
- alert.style.animation = 'shake 0.4s ease';
498
- btn.disabled = false;
499
- spinner.style.display = 'none';
500
- btnText.textContent = I18N.unlock;
501
- document.getElementById('password').focus();
502
- document.getElementById('password').select();
503
- }
504
- } catch (err) {
505
- errorMsg.textContent = I18N.networkError;
506
- alert.style.display = 'flex';
507
- btn.disabled = false;
508
- spinner.style.display = 'none';
509
- btnText.textContent = I18N.unlock;
510
- }
511
- return false;
512
- }
513
- </script>
514
- </body>
515
- </html>`;
516
- }