dsh-jiuling-remote-mobile 1.0.9 → 1.0.11
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/client.js +106 -7
- package/lib/index.js +73 -13
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -135,6 +135,12 @@ window.__ModuleLoader__.load({
|
|
|
135
135
|
'flex:1 1 100%;min-width:0;' +
|
|
136
136
|
'color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;',
|
|
137
137
|
pairInput: 'width:140px;text-transform:uppercase;letter-spacing:2px;',
|
|
138
|
+
// 解绑二次确认那一行:提示文字占满整行,两个按钮跟在下面(手机上 360px
|
|
139
|
+
// 放不下「文字 + 两个按钮」一行,所以让文字 `flex:1 1 100%` 自己换行)。
|
|
140
|
+
pairConfirm:
|
|
141
|
+
'display:flex;flex-wrap:wrap;align-items:center;gap:8px;' +
|
|
142
|
+
'font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);',
|
|
143
|
+
pairConfirmText: 'flex:1 1 100%;',
|
|
138
144
|
pairMsg: 'padding:0 4px 8px;font-size:12px;line-height:18px;',
|
|
139
145
|
pairOk: 'color:var(--dsw-alias-state-success-primary);',
|
|
140
146
|
};
|
|
@@ -313,9 +319,17 @@ window.__ModuleLoader__.load({
|
|
|
313
319
|
* 配对状态(与目录浏览无关,所以单独一个 state)。
|
|
314
320
|
*
|
|
315
321
|
* `endpoint === null` 表示还没查过;`paired === false` 表示
|
|
316
|
-
*
|
|
322
|
+
* 手机还没有任何凭据(令牌为空),需要用户去电脑上取一个配对码。
|
|
317
323
|
*/
|
|
318
|
-
var pairPair = react.useState({
|
|
324
|
+
var pairPair = react.useState({
|
|
325
|
+
endpoint: null,
|
|
326
|
+
paired: false,
|
|
327
|
+
code: '',
|
|
328
|
+
busy: false,
|
|
329
|
+
// 「重新配对」的两步确认:`true` 时才显示「确定解绑 / 取消」。
|
|
330
|
+
confirming: false,
|
|
331
|
+
message: null,
|
|
332
|
+
});
|
|
319
333
|
var pairState = pairPair[0];
|
|
320
334
|
var setPairState = pairPair[1];
|
|
321
335
|
|
|
@@ -372,10 +386,9 @@ window.__ModuleLoader__.load({
|
|
|
372
386
|
react.useEffect(
|
|
373
387
|
function () {
|
|
374
388
|
if (!open) return undefined;
|
|
375
|
-
// ⚠️
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// 完全不知道要先去配对。这里直接给出可操作的提示。
|
|
389
|
+
// ⚠️ **没配对就别去问电脑要目录**。未配对时手机没有任何凭据,
|
|
390
|
+
// 打过去必然是 401/502 —— 用户看到的是转圈 10 秒后一句
|
|
391
|
+
// 「远程请求失败」,完全不知道要先去配对。这里直接给出可操作的提示。
|
|
379
392
|
if (tab === 'desktop' && pairState.paired !== true) {
|
|
380
393
|
patch({ loading: false, listing: null, error: null });
|
|
381
394
|
return undefined;
|
|
@@ -453,6 +466,92 @@ window.__ModuleLoader__.load({
|
|
|
453
466
|
);
|
|
454
467
|
};
|
|
455
468
|
|
|
469
|
+
/**
|
|
470
|
+
* 解除配对:清掉本地凭据,退回未配对状态。
|
|
471
|
+
*
|
|
472
|
+
* 成功后 `paired` 翻回 `false`,配对条自己就会换成输入框(同一个
|
|
473
|
+
* `pairState`),所以这里不用手工重挂组件。
|
|
474
|
+
*/
|
|
475
|
+
var submitUnpair = function () {
|
|
476
|
+
if (pairState.busy) return;
|
|
477
|
+
patchPair({ busy: true, message: null });
|
|
478
|
+
bridge('unpair', {}).then(
|
|
479
|
+
function (result) {
|
|
480
|
+
if (!aliveRef.current) return;
|
|
481
|
+
patchPair({
|
|
482
|
+
busy: false,
|
|
483
|
+
confirming: false,
|
|
484
|
+
paired: false,
|
|
485
|
+
endpoint: result.endpoint,
|
|
486
|
+
code: '',
|
|
487
|
+
message: { kind: 'ok', text: '已解绑,请到电脑上生成新的配对码' },
|
|
488
|
+
});
|
|
489
|
+
patch({ path: null, listing: null });
|
|
490
|
+
},
|
|
491
|
+
function (reason) {
|
|
492
|
+
if (!aliveRef.current) return;
|
|
493
|
+
patchPair({
|
|
494
|
+
busy: false,
|
|
495
|
+
message: { kind: 'error', text: String((reason && reason.message) || reason) },
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 已配对时的「重新配对」入口。
|
|
503
|
+
*
|
|
504
|
+
* ⚠️ **这是必需的回退路径,不是便利功能。** 电脑端一旦「解绑」,它会换掉
|
|
505
|
+
* 令牌并停掉隧道;而手机这边 `mobile.json` 里还留着旧令牌,`pairStatus`
|
|
506
|
+
* 会一直报「已配对」,于是输入框永远不渲染 —— 用户卡在 502,界面上无路
|
|
507
|
+
* 可走,只能手工删 `mobile.json` 或清 App 数据。补上这个入口,回退才闭环。
|
|
508
|
+
*
|
|
509
|
+
* 两步确认:解绑会停隧道、清远程会话清单,误触代价太大。
|
|
510
|
+
*/
|
|
511
|
+
var unpairControls = function () {
|
|
512
|
+
if (pairState.confirming !== true) {
|
|
513
|
+
return h(
|
|
514
|
+
Button,
|
|
515
|
+
{
|
|
516
|
+
variant: 'ghost',
|
|
517
|
+
size: 'sm',
|
|
518
|
+
onClick: function () {
|
|
519
|
+
patchPair({ confirming: true, message: null });
|
|
520
|
+
},
|
|
521
|
+
disabled: pairState.busy,
|
|
522
|
+
},
|
|
523
|
+
'重新配对',
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return h(
|
|
527
|
+
'span',
|
|
528
|
+
{ style: CSS.pairConfirm },
|
|
529
|
+
h('span', { style: CSS.pairConfirmText }, '解绑后这台手机要重新输入配对码。'),
|
|
530
|
+
h(
|
|
531
|
+
Button,
|
|
532
|
+
{
|
|
533
|
+
variant: 'outline',
|
|
534
|
+
size: 'sm',
|
|
535
|
+
onClick: submitUnpair,
|
|
536
|
+
disabled: pairState.busy,
|
|
537
|
+
},
|
|
538
|
+
pairState.busy ? '解绑中…' : '确定解绑',
|
|
539
|
+
),
|
|
540
|
+
h(
|
|
541
|
+
Button,
|
|
542
|
+
{
|
|
543
|
+
variant: 'ghost',
|
|
544
|
+
size: 'sm',
|
|
545
|
+
onClick: function () {
|
|
546
|
+
patchPair({ confirming: false });
|
|
547
|
+
},
|
|
548
|
+
disabled: pairState.busy,
|
|
549
|
+
},
|
|
550
|
+
'取消',
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
};
|
|
554
|
+
|
|
456
555
|
/**
|
|
457
556
|
* 「电脑」页签顶部的配对条。
|
|
458
557
|
*
|
|
@@ -476,7 +575,7 @@ window.__ModuleLoader__.load({
|
|
|
476
575
|
'还没配对。请在电脑的「设置 → 手机远程」里生成 6 位配对码,填在这里。',
|
|
477
576
|
),
|
|
478
577
|
pairState.paired === true
|
|
479
|
-
?
|
|
578
|
+
? unpairControls()
|
|
480
579
|
: h(Input, {
|
|
481
580
|
value: pairState.code,
|
|
482
581
|
onChange: function (event) {
|
package/lib/index.js
CHANGED
|
@@ -105,24 +105,41 @@ function saveConfig(endpoint, token) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
108
|
+
* 令牌为空 —— 表示**尚未配对**。
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ 早期这里硬编码了一个「内置访问令牌」,让两端装完即连。那是真实漏洞:
|
|
111
|
+
* 常量随 npm 包公开,任何未配对的电脑都跑在**同一个公开令牌**上,谁都能连上去
|
|
112
|
+
* 读他的电脑(实测用包内公开令牌打公网隧道 → HTTP 200,读到 177 个目录条目)。
|
|
113
|
+
* 现在**包里不含任何凭据**:令牌只能由配对流程从服务器 claim 得到(见
|
|
114
|
+
* `claimPairing`),未配对时就是空字符串,`callRemote` / `follow` 会直接报
|
|
115
|
+
* 「尚未配对」,不会静默用一个假令牌去连。
|
|
116
|
+
*/
|
|
117
|
+
const EMPTY_TOKEN = '';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* **已作废**的旧公开令牌 —— 只用于识别,**不是凭据**。
|
|
109
121
|
*
|
|
110
|
-
*
|
|
111
|
-
* `
|
|
122
|
+
* 1.0.9 及更早的版本把它当默认令牌写进 `mobile.json`。老用户升级后文件里还留着
|
|
123
|
+
* 这个值,若不识别就会被 `pairStatus` 判成「已配对」,而实际上电脑端早已改用随机
|
|
124
|
+
* 令牌 —— 症状是「显示已配对,一发消息就 401」。所以见到它就当成未配对。
|
|
112
125
|
*/
|
|
113
|
-
const
|
|
126
|
+
const LEGACY_PUBLIC_TOKEN = 'f6721b0cf82414709df172056193c78a6c374e9a920e51145dd7883923cf16e7';
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
129
|
* 读配置。首次运行写一份模板,用户直接改文件即可。
|
|
117
130
|
*
|
|
118
|
-
*
|
|
131
|
+
* ⚠️ 这里**没有**任何内置令牌兜底(早期有,是漏洞)。`token` 为空即「未配对」,
|
|
132
|
+
* 调用方据此直接拒绝,而不是拿一个包里公开的常量去连。
|
|
119
133
|
*
|
|
120
134
|
* @returns `{ endpoint, token }`。
|
|
121
135
|
*/
|
|
122
136
|
function loadConfig() {
|
|
123
137
|
try {
|
|
124
138
|
const parsed = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
125
|
-
|
|
139
|
+
// ⚠️ 旧版写进文件的公开令牌要当成**未配对**,否则界面显示已配对、
|
|
140
|
+
// 实际一调用就 401(电脑端已换随机令牌)。
|
|
141
|
+
const raw = typeof parsed.token === 'string' ? parsed.token : EMPTY_TOKEN;
|
|
142
|
+
const token = raw === LEGACY_PUBLIC_TOKEN ? EMPTY_TOKEN : raw;
|
|
126
143
|
return {
|
|
127
144
|
endpoint:
|
|
128
145
|
typeof parsed.endpoint === 'string' && parsed.endpoint !== ''
|
|
@@ -135,13 +152,13 @@ function loadConfig() {
|
|
|
135
152
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
136
153
|
writeFileSync(
|
|
137
154
|
CONFIG_PATH,
|
|
138
|
-
JSON.stringify({ endpoint: DEFAULT_ENDPOINT, token:
|
|
155
|
+
JSON.stringify({ endpoint: DEFAULT_ENDPOINT, token: EMPTY_TOKEN }, null, 2),
|
|
139
156
|
{ mode: 0o600 },
|
|
140
157
|
);
|
|
141
158
|
} catch {
|
|
142
159
|
/* 只读环境:用默认值继续 */
|
|
143
160
|
}
|
|
144
|
-
return { endpoint: DEFAULT_ENDPOINT, token:
|
|
161
|
+
return { endpoint: DEFAULT_ENDPOINT, token: EMPTY_TOKEN };
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
|
|
@@ -218,7 +235,8 @@ async function remoteFetch(url, init) {
|
|
|
218
235
|
*/
|
|
219
236
|
async function callRemote(config, method, params = {}) {
|
|
220
237
|
if (config.token === '') {
|
|
221
|
-
|
|
238
|
+
// 未配对 = 没有令牌。**不要**提示用户去手改文件:面向大众的主路径是配对。
|
|
239
|
+
throw new Error('还没配对:请在电脑的「设置 → 手机远程」里生成配对码,再到「添加工作区 → 电脑」填进去');
|
|
222
240
|
}
|
|
223
241
|
const res = await remoteFetch(`${config.endpoint}/api/dsh-remote/rpc`, {
|
|
224
242
|
method: 'POST',
|
|
@@ -352,7 +370,7 @@ export function apply(ctx) {
|
|
|
352
370
|
const config = loadConfig();
|
|
353
371
|
ctx.logger?.info?.(`[dsh-remote-mobile] 远端: ${config.endpoint}`);
|
|
354
372
|
if (config.token === '') {
|
|
355
|
-
ctx.logger?.
|
|
373
|
+
ctx.logger?.info?.('[dsh-remote-mobile] 尚未配对:请到「添加工作区 → 电脑」填配对码(配对后自动连接)');
|
|
356
374
|
}
|
|
357
375
|
|
|
358
376
|
/** 远程会话 id 集合:这些会话的 prompt 转发电脑,不在手机本地跑模型。 */
|
|
@@ -675,6 +693,9 @@ export function apply(ctx) {
|
|
|
675
693
|
const result = await handleBridge(ctx, config, {
|
|
676
694
|
remoteSessions,
|
|
677
695
|
follow,
|
|
696
|
+
// 解绑要能停掉全部旧连接、清掉全部搬运状态(见 `unpair`)。
|
|
697
|
+
follows,
|
|
698
|
+
mirrorStates,
|
|
678
699
|
persist: () => saveRemoteSessions(remoteSessions, ctx.logger),
|
|
679
700
|
body,
|
|
680
701
|
});
|
|
@@ -814,7 +835,7 @@ async function ensureDesktopWorkspace(ctx, desktopPath) {
|
|
|
814
835
|
*
|
|
815
836
|
* @param ctx - Cordis 上下文。
|
|
816
837
|
* @param config - 远程配置。
|
|
817
|
-
* @param io - `{ remoteSessions, follow, persist, body }`。
|
|
838
|
+
* @param io - `{ remoteSessions, follow, follows, mirrorStates, persist, body }`。
|
|
818
839
|
*/
|
|
819
840
|
async function handleBridge(ctx, config, io) {
|
|
820
841
|
const params = io.body?.params ?? {};
|
|
@@ -838,7 +859,7 @@ async function handleBridge(ctx, config, io) {
|
|
|
838
859
|
throw new Error('请先在电脑页签里选一个目录');
|
|
839
860
|
}
|
|
840
861
|
if (config.token === '') {
|
|
841
|
-
throw new Error(
|
|
862
|
+
throw new Error('还没配对:请先在「电脑」页签里填配对码');
|
|
842
863
|
}
|
|
843
864
|
const sessionId = `session-${randomUUID()}`;
|
|
844
865
|
// ⚠️ 手机端 cwd 必须是**手机本地**的绝对路径,绝不把电脑路径传进来
|
|
@@ -928,7 +949,46 @@ async function handleBridge(ctx, config, io) {
|
|
|
928
949
|
|
|
929
950
|
/** 当前连的是哪台电脑(前端用来显示状态)。 */
|
|
930
951
|
case 'pairStatus':
|
|
931
|
-
|
|
952
|
+
// 「已配对」的判据是**有令牌**。早期拿「令牌不等于内置常量」当判据,
|
|
953
|
+
// 那个常量已删(它是公开凭据,见 EMPTY_TOKEN 的注释)。
|
|
954
|
+
return { endpoint: config.endpoint, paired: config.token !== EMPTY_TOKEN };
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* 解除配对:清掉本地凭据,退回未配对状态,让用户能重新填配对码。
|
|
958
|
+
*
|
|
959
|
+
* ⚠️ **这个方法是必需的回退路径,不是便利功能**。电脑端一旦「解绑」,
|
|
960
|
+
* 它会换掉令牌并停掉隧道;手机这边 `mobile.json` 里还留着旧令牌,
|
|
961
|
+
* `pairStatus` 就一直报「已配对」⇒ 前端那条「已配对」横幅永远挂着、
|
|
962
|
+
* **输入框根本不渲染**(见 client.js 里 `paired === true` 的分支)。
|
|
963
|
+
* 结果就是:电脑解绑后手机永久卡在 502,界面上没有任何重新配对的入口,
|
|
964
|
+
* 只能手工删 `mobile.json` 或清 App 数据。
|
|
965
|
+
*
|
|
966
|
+
* 清理分三部分,缺一不可:
|
|
967
|
+
* ① 凭据 —— `config` 就地清空 + 写盘,否则重启后旧令牌又回来了;
|
|
968
|
+
* ② 连接 —— 停掉所有跟随旧电脑的事件流,否则它们会拿着旧令牌
|
|
969
|
+
* 无限重连(退避到 15s 一次)一直打一个已经不属于自己的域名;
|
|
970
|
+
* ③ 会话归属 —— `remoteSessions` 里的会话原本靠「转发电脑」工作,
|
|
971
|
+
* 解绑后电脑那边可能已经不存在了,继续留着会让这些会话的 prompt
|
|
972
|
+
* 走 `callRemote` 报错。一并清掉,它们就退回普通本地会话。
|
|
973
|
+
*
|
|
974
|
+
* @returns 清空后的 `{ endpoint, paired: false }`。
|
|
975
|
+
*/
|
|
976
|
+
case 'unpair': {
|
|
977
|
+
Object.assign(config, saveConfig(DEFAULT_ENDPOINT, EMPTY_TOKEN));
|
|
978
|
+
for (const controller of io.follows.values()) {
|
|
979
|
+
try {
|
|
980
|
+
controller.abort();
|
|
981
|
+
} catch {
|
|
982
|
+
/* 已经结束了 */
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
io.follows.clear();
|
|
986
|
+
io.mirrorStates.clear();
|
|
987
|
+
io.remoteSessions.clear();
|
|
988
|
+
io.persist();
|
|
989
|
+
ctx.logger?.info?.('[dsh-remote-mobile] 已解除配对,本地凭据与远程会话清单已清空');
|
|
990
|
+
return { endpoint: config.endpoint, paired: false };
|
|
991
|
+
}
|
|
932
992
|
|
|
933
993
|
default:
|
|
934
994
|
throw new Error(`unknown method "${String(io.body?.method)}"`);
|