dsh-mobile-plugin 0.1.87 → 0.1.88
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/relay/lan-proxy.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 局域网直连代理:手机在同一个 Wi-Fi 下直接访问
|
|
5
|
-
* http://<电脑IP>:<lanPort
|
|
5
|
+
* http://<电脑IP>:<lanPort>,带上配对拿到的令牌后代理到本机 dsh web。
|
|
6
|
+
*
|
|
7
|
+
* 只服务 App:未认证的请求一律 JSON 401,不给任何网页。曾经的 HTML 配对页
|
|
8
|
+
* 与配对后能进入的网页本体已删除(见 docs/decisions.md 2026-10-19)。
|
|
6
9
|
*
|
|
7
10
|
* 与中转隧道的关系:同网用本代理(零延迟、不依赖公网),
|
|
8
11
|
* 异地走 dsh-mobile-server 中转。两者互不影响,可同时开启。
|
|
@@ -154,69 +157,15 @@ function createLanProxy(opts) {
|
|
|
154
157
|
+ `; Path=/; HttpOnly; SameSite=Lax; Max-Age=${core.MOBILE_TOKEN_TTL_SEC}`,
|
|
155
158
|
);
|
|
156
159
|
}
|
|
157
|
-
function clearCookie(res) {
|
|
158
|
-
// 与 setCookie 保持同一组属性:属性不一致时部分客户端会当成另一个 cookie,删不掉。
|
|
159
|
-
res.setHeader('set-cookie', 'dsh_lan=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const PAGE = `<!DOCTYPE html>
|
|
163
|
-
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
164
|
-
<title>连接 DeepSeek Harness</title>
|
|
165
|
-
<style>
|
|
166
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
167
|
-
body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',sans-serif;background:#0f1115;color:#e6e8eb;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
|
|
168
|
-
.card{width:min(380px,100%);background:#171a21;border:1px solid #262b36;border-radius:16px;padding:28px}
|
|
169
|
-
h1{font-size:18px;margin-bottom:6px}
|
|
170
|
-
.sub{font-size:13px;color:#8b93a1;margin-bottom:20px;line-height:1.6}
|
|
171
|
-
label{display:block;font-size:12px;color:#8b93a1;margin:14px 0 6px}
|
|
172
|
-
input{width:100%;padding:10px 12px;font-size:15px;background:#0f1115;color:#e6e8eb;border:1px solid #2c3340;border-radius:10px;outline:none}
|
|
173
|
-
input:focus{border-color:#4d6bfe}
|
|
174
|
-
button{width:100%;margin-top:20px;padding:11px;font-size:15px;background:#4d6bfe;color:#fff;border:none;border-radius:10px;cursor:pointer}
|
|
175
|
-
.err{margin-top:14px;font-size:13px;color:#f85149;min-height:18px}
|
|
176
|
-
</style></head><body>
|
|
177
|
-
<div class="card">
|
|
178
|
-
<h1>连接 DeepSeek Harness</h1>
|
|
179
|
-
<div class="sub">局域网直连 · 实例 <code>${instanceId}</code><br>请输入桌面端「远程连接」面板显示的 6 位配对码</div>
|
|
180
|
-
<form id="f"><label for="code">配对码</label><input id="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code">
|
|
181
|
-
<button type="submit">连接</button></form>
|
|
182
|
-
<div class="err" id="err"></div>
|
|
183
|
-
</div>
|
|
184
|
-
<script>
|
|
185
|
-
document.getElementById('f').addEventListener('submit', async (e) => {
|
|
186
|
-
e.preventDefault();
|
|
187
|
-
const code = document.getElementById('code').value.trim();
|
|
188
|
-
if (!/^\d{6}$/.test(code)) {
|
|
189
|
-
document.getElementById('err').textContent = '请输入 6 位数字配对码';
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const r = await fetch('/__lan_pair', { method:'POST', headers:{'content-type':'application/json'},
|
|
193
|
-
body: JSON.stringify({ code }) });
|
|
194
|
-
if (r.ok) { location.href = '/'; }
|
|
195
|
-
else { document.getElementById('err').textContent = '配对码错误或尝试过于频繁'; }
|
|
196
|
-
});
|
|
197
|
-
</script></body></html>`;
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* 是否是「程序化 API 请求」。
|
|
201
|
-
*
|
|
202
|
-
* 未认证时给浏览器导航回配对页(200 HTML)是刻意的:用户直接在手机浏览器里
|
|
203
|
-
* 打开局域网地址即可输入配对码。但**绝不能**这样答复 API 请求 ——
|
|
204
|
-
* `/api/mobile/v1/*` 被答成 200 + HTML 时,App 的 `app/src/api.ts` 会把它当成
|
|
205
|
-
* 成功(`response.ok` 为真 → `publishRoute` 把连接通道钉在局域网、并重置局域网
|
|
206
|
-
* 熔断),随后 `res.json()` 解析失败,`unwrapResult` 抛出「请求失败
|
|
207
|
-
* (HTTP 200)」。结果是桌面端换过 lanSecret、或设备凭据被吊销之后,App 会永久
|
|
208
|
-
* 卡在局域网通道上:既不会回退到中转,也不会提示重新配对。
|
|
209
|
-
*
|
|
210
|
-
* 判定规则:路径以 `/api/` 开头,或客户端明确不接受 `text/html`。
|
|
211
|
-
*/
|
|
212
|
-
function isApiRequest(req, url) {
|
|
213
|
-
if (url.pathname.startsWith('/api/')) return true;
|
|
214
|
-
return !/\btext\/html\b/i.test(String(req.headers.accept || ''));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
160
|
/**
|
|
218
|
-
*
|
|
161
|
+
* 未认证的请求一律回 JSON 401,与移动 API 的鉴权失败保持同一种形状 ——
|
|
219
162
|
* App 据此显示可读的「缺少或无效的移动 token」,而不是「请求失败 (HTTP 200)」。
|
|
163
|
+
*
|
|
164
|
+
* 浏览器导航也走这条路:这里**不再**提供网页版的配对页与配对后进入的网页本体,
|
|
165
|
+
* 手机端只有 App 一条路。给网页回 200 HTML 曾是刻意行为,但 App 的
|
|
166
|
+
* `app/src/api.ts` 会把 `response.ok` 当成功、把连接通道钉在局域网并重置熔断,
|
|
167
|
+
* 随后 `res.json()` 解析失败,桌面端换过 lanSecret 或凭据被吊销之后就永久卡住 ——
|
|
168
|
+
* 一个「谁都能打开的页面」换来的这点便利不值这条风险。
|
|
220
169
|
*/
|
|
221
170
|
function sendUnauthorized(res) {
|
|
222
171
|
try {
|
|
@@ -225,11 +174,6 @@ document.getElementById('f').addEventListener('submit', async (e) => {
|
|
|
225
174
|
} catch { /* 客户端已断开 */ }
|
|
226
175
|
}
|
|
227
176
|
|
|
228
|
-
function sendPage(res, status) {
|
|
229
|
-
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
|
|
230
|
-
res.end(PAGE);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
177
|
function proxyToUpstream(req, res) {
|
|
234
178
|
// 读取隔离:命中本地镜像快照时直接回,不再连主进程。
|
|
235
179
|
// 例外:带 ?fresh=1 的写后刷新必须回源——镜像最多旧 500ms,撞上写操作会把刚做完的
|
|
@@ -440,12 +384,6 @@ document.getElementById('f').addEventListener('submit', async (e) => {
|
|
|
440
384
|
server = http.createServer((req, res) => {
|
|
441
385
|
const url = new URL(req.url, 'http://lan.local');
|
|
442
386
|
try {
|
|
443
|
-
if (url.pathname === '/__lan_logout') {
|
|
444
|
-
clearCookie(res);
|
|
445
|
-
res.writeHead(302, { location: '/' });
|
|
446
|
-
res.end();
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
387
|
if (url.pathname === '/__lan_pair' && req.method === 'POST') {
|
|
450
388
|
const ip = req.socket.remoteAddress || '';
|
|
451
389
|
let b = '';
|
|
@@ -546,9 +484,7 @@ document.getElementById('f').addEventListener('submit', async (e) => {
|
|
|
546
484
|
return;
|
|
547
485
|
}
|
|
548
486
|
if (!authorized(req)) {
|
|
549
|
-
|
|
550
|
-
if (isApiRequest(req, url)) sendUnauthorized(res);
|
|
551
|
-
else sendPage(res, 200);
|
|
487
|
+
sendUnauthorized(res);
|
|
552
488
|
return;
|
|
553
489
|
}
|
|
554
490
|
const c = credential(req);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.88",
|
|
4
4
|
"description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (0.2.0-rc.2 compatible, peers resolved by the host)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|