@yezack/dsh-remote-mobile 1.10.0 → 1.10.2
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/CHANGELOG.md +54 -42
- package/LICENSE +21 -21
- package/README.md +203 -61
- package/README_EN.md +205 -57
- package/lib/auth/gate.js +1 -1
- package/lib/auth/token.d.ts +10 -10
- package/lib/auth/token.js +18 -15
- package/lib/bridge/compat.js +260 -260
- package/lib/bridge/directory-picker.d.ts +118 -0
- package/lib/bridge/directory-picker.js +617 -0
- package/lib/client.js +1010 -178
- package/lib/gateway/detector.d.ts +10 -12
- package/lib/gateway/detector.js +12 -25
- package/lib/gateway/server.d.ts +9 -9
- package/lib/gateway/server.js +10 -10
- package/lib/index.d.ts +1 -0
- package/lib/index.js +86 -35
- package/lib/routes/api.d.ts +15 -12
- package/lib/routes/api.js +113 -4
- package/lib/routes/login-page.js +9 -7
- package/lib/routes/register.d.ts +9 -0
- package/lib/routes/register.js +53 -0
- package/lib/startup-hint.d.ts +5 -0
- package/lib/startup-hint.js +72 -0
- package/lib/styles/style-snippets.js +639 -639
- package/package.json +3 -3
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 目录选择器(Directory Picker)配置检测与幂等补丁管理模块
|
|
3
|
+
*
|
|
4
|
+
* 【背景与问题】
|
|
5
|
+
* DSH 官方默认的 `id: directory-picker`(`@deepseek-ai/dsh-host-directory-picker-auto`)
|
|
6
|
+
* 仅在进程启动时根据 `webServer.host` 是否为 127.0.0.1 回环地址来判断“本机 / 远程”:
|
|
7
|
+
* - 本插件默认采用安全接入代理模式(DSH 核心保持 127.0.0.1:3080 回环,由 3081 代理对外);
|
|
8
|
+
* - `auto` 因此将所有来自手机端 3081 的请求误判为“本机桌面操作”,直接调用 PC 的系统原生文件选择窗(native);
|
|
9
|
+
* - 最终导致弹窗出现在 PC 屏幕上,手机浏览器一直转圈等待,无法在手机端选择文件夹或添加工作区。
|
|
10
|
+
*
|
|
11
|
+
* 【解决方案】
|
|
12
|
+
* 1. 运行时检测:结合 Cordis loader 激活树与 `~/.dsh/profiles/web/cordis.patch.yml` 配置文件,
|
|
13
|
+
* 精准判断当前是否已启用网页内目录选择器(`browse` 模式);
|
|
14
|
+
* 2. 幂等补丁写入:支持在设置页一键开启/恢复,写入时对 `directory-picker`、`directory-picker-browse`、
|
|
15
|
+
* `ui-directory-picker-browse` 三项分别执行严格结构化去重,绝不产生重复条目,且完整保留用户其它配置。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { DSH_HOME } from '../auth/token.js';
|
|
20
|
+
export const AUTO_DIRECTORY_PICKER_ID = 'directory-picker';
|
|
21
|
+
export const AUTO_DIRECTORY_PICKER_PKG = '@deepseek-ai/dsh-host-directory-picker-auto';
|
|
22
|
+
export const HOST_DIRECTORY_PICKER_BROWSE_ID = 'directory-picker-browse';
|
|
23
|
+
export const HOST_DIRECTORY_PICKER_BROWSE_PKG = '@deepseek-ai/dsh-host-directory-picker-browse';
|
|
24
|
+
export const UI_DIRECTORY_PICKER_BROWSE_ID = 'ui-directory-picker-browse';
|
|
25
|
+
export const UI_DIRECTORY_PICKER_BROWSE_PKG = '@deepseek-ai/dsh-client-ui-directory-picker-browse';
|
|
26
|
+
/** 标准可复制/可注入的 YAML 配置片段 */
|
|
27
|
+
export const DIRECTORY_PICKER_YAML_SNIPPET = [
|
|
28
|
+
'# 远程手机访问时固定使用应用内目录浏览器。',
|
|
29
|
+
'# DSH 的 dsh-host-directory-picker-auto 只根据 DSH 核心是否绑定 127.0.0.1 来判断“本机/远程”;',
|
|
30
|
+
'# 本插件让核心保持回环、由 3081 代理对外,因此手机请求也被 auto 误判成“本机”,于是 native 选择器弹在 PC 上。',
|
|
31
|
+
'# 可以用下面配置强制使用网页目录选择器。',
|
|
32
|
+
'- id: directory-picker',
|
|
33
|
+
' disabled: true',
|
|
34
|
+
'- insert:',
|
|
35
|
+
' - id: directory-picker-browse',
|
|
36
|
+
" name: '@deepseek-ai/dsh-host-directory-picker-browse'",
|
|
37
|
+
' - id: ui-directory-picker-browse',
|
|
38
|
+
" name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'",
|
|
39
|
+
].join('\n');
|
|
40
|
+
/** 运行时上下文绑定状态(由插件主入口 apply 注入) */
|
|
41
|
+
let boundCtx = null;
|
|
42
|
+
let boundIsLoopbackCore = () => true;
|
|
43
|
+
/**
|
|
44
|
+
* 绑定 Cordis 运行时上下文与回环核心判定函数(供 /status 路由实时查询)
|
|
45
|
+
*/
|
|
46
|
+
export function bindDirectoryPickerRuntime(ctx, isLoopbackCore) {
|
|
47
|
+
boundCtx = ctx;
|
|
48
|
+
if (typeof isLoopbackCore === 'function') {
|
|
49
|
+
boundIsLoopbackCore = isLoopbackCore;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 定位当前 DSH 的 web profile `cordis.patch.yml` 路径
|
|
54
|
+
*
|
|
55
|
+
* 优先使用 `~/.dsh/profiles/web/cordis.patch.yml`;若存在其他单 profile 则按优先级兜底。
|
|
56
|
+
*/
|
|
57
|
+
export function resolveDirectoryPickerPatchPath(customPath) {
|
|
58
|
+
if (customPath && typeof customPath === 'string')
|
|
59
|
+
return customPath;
|
|
60
|
+
const webPatch = join(DSH_HOME, 'profiles', 'web', 'cordis.patch.yml');
|
|
61
|
+
if (existsSync(webPatch))
|
|
62
|
+
return webPatch;
|
|
63
|
+
try {
|
|
64
|
+
const profilesDir = join(DSH_HOME, 'profiles');
|
|
65
|
+
if (existsSync(profilesDir)) {
|
|
66
|
+
const dirs = readdirSync(profilesDir, { withFileTypes: true })
|
|
67
|
+
.filter((e) => e.isDirectory())
|
|
68
|
+
.map((e) => e.name);
|
|
69
|
+
if (dirs.includes('web'))
|
|
70
|
+
return webPatch;
|
|
71
|
+
for (const d of dirs) {
|
|
72
|
+
const candidate = join(profilesDir, d, 'cordis.patch.yml');
|
|
73
|
+
if (existsSync(candidate))
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
return webPatch;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 去除 YAML 标量值两端的单双引号与行尾注释
|
|
83
|
+
*/
|
|
84
|
+
function stripYamlScalar(raw) {
|
|
85
|
+
let val = raw.trim();
|
|
86
|
+
// 若未加引号且带注释,剥离 # 注释
|
|
87
|
+
if (!val.startsWith("'") && !val.startsWith('"') && val.includes('#')) {
|
|
88
|
+
val = val.split('#')[0].trim();
|
|
89
|
+
}
|
|
90
|
+
if ((val.startsWith("'") && val.endsWith("'")) || (val.startsWith('"') && val.endsWith('"'))) {
|
|
91
|
+
val = val.slice(1, -1).trim();
|
|
92
|
+
}
|
|
93
|
+
return val;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 将 cordis.patch.yml 按顶层列表项(`- ...`)解析为结构化块,便于精确检查、去重与移除
|
|
97
|
+
*/
|
|
98
|
+
function parsePatchBlocks(lines) {
|
|
99
|
+
const topIndices = [];
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
if (/^-\s+/.test(lines[i])) {
|
|
102
|
+
topIndices.push(i);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const blocks = [];
|
|
106
|
+
for (let idx = 0; idx < topIndices.length; idx++) {
|
|
107
|
+
const startLine = topIndices[idx];
|
|
108
|
+
let endLine = idx + 1 < topIndices.length ? topIndices[idx + 1] - 1 : lines.length - 1;
|
|
109
|
+
// 末尾的注释或空行如果不属于当前块(例如紧贴下一个块上方的注释),剥离出来
|
|
110
|
+
while (endLine > startLine) {
|
|
111
|
+
const l = lines[endLine].trim();
|
|
112
|
+
if (l === '' || l.startsWith('#')) {
|
|
113
|
+
endLine--;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const firstLine = lines[startLine];
|
|
120
|
+
if (/^-\s*insert\s*:\s*(?:#.*)?$/.test(firstLine)) {
|
|
121
|
+
// 解析 - insert: 下的子列表项
|
|
122
|
+
const subStartIndices = [];
|
|
123
|
+
for (let i = startLine + 1; i <= endLine; i++) {
|
|
124
|
+
if (/^\s+-\s+/.test(lines[i])) {
|
|
125
|
+
subStartIndices.push(i);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const subItems = [];
|
|
129
|
+
for (let sIdx = 0; sIdx < subStartIndices.length; sIdx++) {
|
|
130
|
+
const sStart = subStartIndices[sIdx];
|
|
131
|
+
let sEnd = sIdx + 1 < subStartIndices.length ? subStartIndices[sIdx + 1] - 1 : endLine;
|
|
132
|
+
while (sEnd > sStart && (lines[sEnd].trim() === '' || lines[sEnd].trim().startsWith('#'))) {
|
|
133
|
+
sEnd--;
|
|
134
|
+
}
|
|
135
|
+
let itemId = '';
|
|
136
|
+
let itemName = '';
|
|
137
|
+
let itemDisabled = null;
|
|
138
|
+
for (let k = sStart; k <= sEnd; k++) {
|
|
139
|
+
const text = lines[k].replace(/^\s*-\s*/, '').trim();
|
|
140
|
+
const idMatch = text.match(/^id\s*:\s*(.+)$/);
|
|
141
|
+
if (idMatch)
|
|
142
|
+
itemId = stripYamlScalar(idMatch[1]);
|
|
143
|
+
const nameMatch = text.match(/^name\s*:\s*(.+)$/);
|
|
144
|
+
if (nameMatch)
|
|
145
|
+
itemName = stripYamlScalar(nameMatch[1]);
|
|
146
|
+
const disMatch = text.match(/^disabled\s*:\s*(.+)$/);
|
|
147
|
+
if (disMatch) {
|
|
148
|
+
const v = stripYamlScalar(disMatch[1]).toLowerCase();
|
|
149
|
+
if (v === 'true')
|
|
150
|
+
itemDisabled = true;
|
|
151
|
+
else if (v === 'false')
|
|
152
|
+
itemDisabled = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
subItems.push({
|
|
156
|
+
startLine: sStart,
|
|
157
|
+
endLine: sEnd,
|
|
158
|
+
id: itemId,
|
|
159
|
+
name: itemName,
|
|
160
|
+
disabled: itemDisabled,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
blocks.push({
|
|
164
|
+
kind: 'insert',
|
|
165
|
+
startLine,
|
|
166
|
+
endLine,
|
|
167
|
+
id: '',
|
|
168
|
+
name: '',
|
|
169
|
+
disabled: null,
|
|
170
|
+
disabledLineIndex: -1,
|
|
171
|
+
hasOtherKeys: false,
|
|
172
|
+
subItems,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// 普通顶层条目(如 `- id: directory-picker`)
|
|
177
|
+
let entryId = '';
|
|
178
|
+
let entryName = '';
|
|
179
|
+
let entryDisabled = null;
|
|
180
|
+
let disabledLineIndex = -1;
|
|
181
|
+
let hasOtherKeys = false;
|
|
182
|
+
for (let k = startLine; k <= endLine; k++) {
|
|
183
|
+
const rawLine = lines[k];
|
|
184
|
+
const trimmed = rawLine.trim();
|
|
185
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
186
|
+
continue;
|
|
187
|
+
const clean = k === startLine ? rawLine.replace(/^-\s*/, '').trim() : trimmed;
|
|
188
|
+
const idMatch = clean.match(/^id\s*:\s*(.+)$/);
|
|
189
|
+
if (idMatch) {
|
|
190
|
+
entryId = stripYamlScalar(idMatch[1]);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const nameMatch = clean.match(/^name\s*:\s*(.+)$/);
|
|
194
|
+
if (nameMatch) {
|
|
195
|
+
entryName = stripYamlScalar(nameMatch[1]);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const disMatch = clean.match(/^disabled\s*:\s*(.+)$/);
|
|
199
|
+
if (disMatch) {
|
|
200
|
+
const v = stripYamlScalar(disMatch[1]).toLowerCase();
|
|
201
|
+
if (v === 'true')
|
|
202
|
+
entryDisabled = true;
|
|
203
|
+
else if (v === 'false')
|
|
204
|
+
entryDisabled = false;
|
|
205
|
+
disabledLineIndex = k;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
hasOtherKeys = true;
|
|
209
|
+
}
|
|
210
|
+
blocks.push({
|
|
211
|
+
kind: entryId || entryName ? 'entry' : 'other',
|
|
212
|
+
startLine,
|
|
213
|
+
endLine,
|
|
214
|
+
id: entryId,
|
|
215
|
+
name: entryName,
|
|
216
|
+
disabled: entryDisabled,
|
|
217
|
+
disabledLineIndex,
|
|
218
|
+
hasOtherKeys,
|
|
219
|
+
subItems: [],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return blocks;
|
|
224
|
+
}
|
|
225
|
+
function isHostBrowseItem(item) {
|
|
226
|
+
return item.name === HOST_DIRECTORY_PICKER_BROWSE_PKG || item.id === HOST_DIRECTORY_PICKER_BROWSE_ID;
|
|
227
|
+
}
|
|
228
|
+
function isUiBrowseItem(item) {
|
|
229
|
+
return item.name === UI_DIRECTORY_PICKER_BROWSE_PKG || item.id === UI_DIRECTORY_PICKER_BROWSE_ID;
|
|
230
|
+
}
|
|
231
|
+
function isAutoPickerBlock(block) {
|
|
232
|
+
return (block.kind === 'entry' &&
|
|
233
|
+
(block.id === AUTO_DIRECTORY_PICKER_ID || block.name === AUTO_DIRECTORY_PICKER_PKG));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 解析 `cordis.patch.yml` 文本内容,检查目录选择器相关配置状态
|
|
237
|
+
*/
|
|
238
|
+
export function inspectDirectoryPickerPatchContent(content) {
|
|
239
|
+
if (!content || typeof content !== 'string') {
|
|
240
|
+
return {
|
|
241
|
+
autoDisabled: false,
|
|
242
|
+
hasAutoEntry: false,
|
|
243
|
+
hostBrowseInserted: false,
|
|
244
|
+
uiBrowseInserted: false,
|
|
245
|
+
configured: false,
|
|
246
|
+
partial: false,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const lines = content.split(/\r?\n/);
|
|
250
|
+
const blocks = parsePatchBlocks(lines);
|
|
251
|
+
let hasAutoEntry = false;
|
|
252
|
+
let autoDisabled = false;
|
|
253
|
+
let hostBrowseInserted = false;
|
|
254
|
+
let uiBrowseInserted = false;
|
|
255
|
+
for (const block of blocks) {
|
|
256
|
+
if (isAutoPickerBlock(block)) {
|
|
257
|
+
hasAutoEntry = true;
|
|
258
|
+
autoDisabled = block.disabled === true;
|
|
259
|
+
}
|
|
260
|
+
else if (block.kind === 'entry') {
|
|
261
|
+
if (isHostBrowseItem(block) && block.disabled !== true) {
|
|
262
|
+
hostBrowseInserted = true;
|
|
263
|
+
}
|
|
264
|
+
if (isUiBrowseItem(block) && block.disabled !== true) {
|
|
265
|
+
uiBrowseInserted = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (block.kind === 'insert') {
|
|
269
|
+
for (const sub of block.subItems) {
|
|
270
|
+
if (isHostBrowseItem(sub) && sub.disabled !== true) {
|
|
271
|
+
hostBrowseInserted = true;
|
|
272
|
+
}
|
|
273
|
+
if (isUiBrowseItem(sub) && sub.disabled !== true) {
|
|
274
|
+
uiBrowseInserted = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const configured = autoDisabled && hostBrowseInserted && uiBrowseInserted;
|
|
280
|
+
const partial = !configured && (autoDisabled || hostBrowseInserted || uiBrowseInserted);
|
|
281
|
+
return {
|
|
282
|
+
autoDisabled,
|
|
283
|
+
hasAutoEntry,
|
|
284
|
+
hostBrowseInserted,
|
|
285
|
+
uiBrowseInserted,
|
|
286
|
+
configured,
|
|
287
|
+
partial,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* 检查 Cordis 运行时 loader 中的目录选择器插件激活状态
|
|
292
|
+
*/
|
|
293
|
+
export function inspectRuntimeDirectoryPicker(ctx = boundCtx) {
|
|
294
|
+
try {
|
|
295
|
+
const loader = typeof ctx?.get === 'function' ? ctx.get('loader') : ctx?.loader;
|
|
296
|
+
if (!loader || typeof loader.entries !== 'function') {
|
|
297
|
+
return { detected: false, browseActive: false, autoActive: false };
|
|
298
|
+
}
|
|
299
|
+
const entries = [...loader.entries()];
|
|
300
|
+
let sawPickerEntry = false;
|
|
301
|
+
let autoActive = false;
|
|
302
|
+
let hostBrowseActive = false;
|
|
303
|
+
let uiBrowseActive = false;
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
try {
|
|
306
|
+
const id = typeof entry?.id === 'string' ? entry.id : '';
|
|
307
|
+
const name = typeof entry?.options?.name === 'string' ? entry.options.name : '';
|
|
308
|
+
const disabled = Boolean(entry?.disabled);
|
|
309
|
+
if (id === AUTO_DIRECTORY_PICKER_ID || name === AUTO_DIRECTORY_PICKER_PKG) {
|
|
310
|
+
sawPickerEntry = true;
|
|
311
|
+
if (!disabled)
|
|
312
|
+
autoActive = true;
|
|
313
|
+
}
|
|
314
|
+
if (id === HOST_DIRECTORY_PICKER_BROWSE_ID || name === HOST_DIRECTORY_PICKER_BROWSE_PKG) {
|
|
315
|
+
sawPickerEntry = true;
|
|
316
|
+
if (!disabled)
|
|
317
|
+
hostBrowseActive = true;
|
|
318
|
+
}
|
|
319
|
+
if (id === UI_DIRECTORY_PICKER_BROWSE_ID || name === UI_DIRECTORY_PICKER_BROWSE_PKG) {
|
|
320
|
+
sawPickerEntry = true;
|
|
321
|
+
if (!disabled)
|
|
322
|
+
uiBrowseActive = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch { }
|
|
326
|
+
}
|
|
327
|
+
if (!sawPickerEntry) {
|
|
328
|
+
return { detected: false, browseActive: false, autoActive: false };
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
detected: true,
|
|
332
|
+
browseActive: hostBrowseActive && uiBrowseActive && !autoActive,
|
|
333
|
+
autoActive,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return { detected: false, browseActive: false, autoActive: false };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 幂等合并网页目录选择器配置到 `cordis.patch.yml` 内容中(严格去重)
|
|
342
|
+
*
|
|
343
|
+
* 去重规则:
|
|
344
|
+
* 1. 若已存在一个或多个 `- id: directory-picker`:就地将其设为 `disabled: true`,合并重复项;
|
|
345
|
+
* 2. 若已存在 `directory-picker-browse` 或 `ui-directory-picker-browse`(无论在哪个 `insert` 块下):
|
|
346
|
+
* 不重复插入,仅补充缺失的那一项(若原项被设为 `disabled: true` 则移除禁用标记);
|
|
347
|
+
* 3. 若三项均已就绪,原样返回,`changed: false`。
|
|
348
|
+
*/
|
|
349
|
+
export function applyDirectoryPickerBrowsePatch(rawContent) {
|
|
350
|
+
const initialInspection = inspectDirectoryPickerPatchContent(rawContent);
|
|
351
|
+
if (initialInspection.configured) {
|
|
352
|
+
return { content: rawContent, changed: false };
|
|
353
|
+
}
|
|
354
|
+
const lines = rawContent ? rawContent.split(/\r?\n/) : [];
|
|
355
|
+
const blocks = parsePatchBlocks(lines);
|
|
356
|
+
const linesToRemove = new Set();
|
|
357
|
+
// 1. 处理已有的 `- id: directory-picker` 块(就地更新首块,清除多余重复块)
|
|
358
|
+
const autoBlocks = blocks.filter(isAutoPickerBlock);
|
|
359
|
+
let autoHandledInPlace = false;
|
|
360
|
+
if (autoBlocks.length > 0) {
|
|
361
|
+
const primary = autoBlocks[0];
|
|
362
|
+
autoHandledInPlace = true;
|
|
363
|
+
if (primary.disabled !== true) {
|
|
364
|
+
if (primary.disabledLineIndex !== -1) {
|
|
365
|
+
lines[primary.disabledLineIndex] = ' disabled: true';
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
lines[primary.startLine] = `${lines[primary.startLine]}\n disabled: true`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// 若用户此前手误写了多个重复的 `- id: directory-picker`,清理后续重复项
|
|
372
|
+
for (let i = 1; i < autoBlocks.length; i++) {
|
|
373
|
+
const dup = autoBlocks[i];
|
|
374
|
+
for (let k = dup.startLine; k <= dup.endLine; k++) {
|
|
375
|
+
linesToRemove.add(k);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// 2. 检查已有的 browse 条目是否有被设为 disabled: true 的,若有则解除禁用
|
|
380
|
+
for (const block of blocks) {
|
|
381
|
+
if (block.kind === 'insert') {
|
|
382
|
+
for (const sub of block.subItems) {
|
|
383
|
+
if ((isHostBrowseItem(sub) || isUiBrowseItem(sub)) && sub.disabled === true) {
|
|
384
|
+
for (let k = sub.startLine; k <= sub.endLine; k++) {
|
|
385
|
+
if (/^\s*disabled\s*:\s*true\b/i.test(lines[k])) {
|
|
386
|
+
linesToRemove.add(k);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const filteredLines = lines.filter((_, idx) => !linesToRemove.has(idx));
|
|
394
|
+
let intermediate = filteredLines.join('\n');
|
|
395
|
+
const afterInPlace = inspectDirectoryPickerPatchContent(intermediate);
|
|
396
|
+
const needAuto = !autoHandledInPlace && !afterInPlace.autoDisabled;
|
|
397
|
+
const needHostBrowse = !afterInPlace.hostBrowseInserted;
|
|
398
|
+
const needUiBrowse = !afterInPlace.uiBrowseInserted;
|
|
399
|
+
const appendLines = [];
|
|
400
|
+
const hasHeaderComment = intermediate.includes('dsh-host-directory-picker-auto') ||
|
|
401
|
+
intermediate.includes('远程手机访问时固定使用应用内目录浏览器');
|
|
402
|
+
if ((needAuto || needHostBrowse || needUiBrowse) && !hasHeaderComment) {
|
|
403
|
+
appendLines.push('# 远程手机访问时固定使用应用内目录浏览器。', '# DSH 的 dsh-host-directory-picker-auto 只根据 DSH 核心是否绑定 127.0.0.1 来判断“本机/远程”;', '# 本插件让核心保持回环、由 3081 代理对外,因此手机请求也被 auto 误判成“本机”,于是 native 选择器弹在 PC 上。', '# 可以用下面配置强制使用网页目录选择器。');
|
|
404
|
+
}
|
|
405
|
+
if (needAuto) {
|
|
406
|
+
appendLines.push('- id: directory-picker', ' disabled: true');
|
|
407
|
+
}
|
|
408
|
+
if (needHostBrowse || needUiBrowse) {
|
|
409
|
+
appendLines.push('- insert:');
|
|
410
|
+
if (needHostBrowse) {
|
|
411
|
+
appendLines.push(` - id: ${HOST_DIRECTORY_PICKER_BROWSE_ID}`, ` name: '${HOST_DIRECTORY_PICKER_BROWSE_PKG}'`);
|
|
412
|
+
}
|
|
413
|
+
if (needUiBrowse) {
|
|
414
|
+
appendLines.push(` - id: ${UI_DIRECTORY_PICKER_BROWSE_ID}`, ` name: '${UI_DIRECTORY_PICKER_BROWSE_PKG}'`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (appendLines.length > 0) {
|
|
418
|
+
const trimmedBase = intermediate.replace(/\s+$/, '');
|
|
419
|
+
intermediate = trimmedBase
|
|
420
|
+
? `${trimmedBase}\n\n${appendLines.join('\n')}\n`
|
|
421
|
+
: `${appendLines.join('\n')}\n`;
|
|
422
|
+
}
|
|
423
|
+
else if (intermediate && !intermediate.endsWith('\n')) {
|
|
424
|
+
intermediate += '\n';
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
content: intermediate,
|
|
428
|
+
changed: intermediate !== rawContent,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 判断一行注释是否属于目录选择器补丁自带的说明注释
|
|
433
|
+
*/
|
|
434
|
+
function isDirectoryPickerPatchComment(line) {
|
|
435
|
+
const trimmed = line.trim();
|
|
436
|
+
if (!trimmed.startsWith('#'))
|
|
437
|
+
return false;
|
|
438
|
+
return (trimmed.includes('远程手机访问时固定使用应用内目录浏览器') ||
|
|
439
|
+
trimmed.includes('dsh-host-directory-picker-auto') ||
|
|
440
|
+
trimmed.includes('native 选择器弹在 PC 上') ||
|
|
441
|
+
trimmed.includes('强制使用网页目录选择器') ||
|
|
442
|
+
trimmed.includes('directory-picker-browse'));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* 从 `cordis.patch.yml` 内容中安全移除网页目录选择器补丁(恢复 PC 原生目录选择器)
|
|
446
|
+
*
|
|
447
|
+
* 仅移除与 directory-picker 相关的禁用项与两个 browse 插入项,100% 保留用户其他配置。
|
|
448
|
+
*/
|
|
449
|
+
export function removeDirectoryPickerBrowsePatch(rawContent) {
|
|
450
|
+
if (!rawContent || typeof rawContent !== 'string') {
|
|
451
|
+
return { content: rawContent || '', changed: false };
|
|
452
|
+
}
|
|
453
|
+
const lines = rawContent.split(/\r?\n/);
|
|
454
|
+
const blocks = parsePatchBlocks(lines);
|
|
455
|
+
const linesToRemove = new Set();
|
|
456
|
+
// 1. 移除目录选择器补丁专属的说明注释行
|
|
457
|
+
for (let i = 0; i < lines.length; i++) {
|
|
458
|
+
if (isDirectoryPickerPatchComment(lines[i])) {
|
|
459
|
+
linesToRemove.add(i);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// 2. 处理各个结构化块
|
|
463
|
+
for (const block of blocks) {
|
|
464
|
+
if (isAutoPickerBlock(block)) {
|
|
465
|
+
if (block.hasOtherKeys) {
|
|
466
|
+
// 如果用户在 directory-picker 下还写了其他属性(如 config),仅移除 disabled 行
|
|
467
|
+
if (block.disabledLineIndex !== -1) {
|
|
468
|
+
linesToRemove.add(block.disabledLineIndex);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
for (let k = block.startLine; k <= block.endLine; k++) {
|
|
473
|
+
linesToRemove.add(k);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (block.kind === 'entry' && (isHostBrowseItem(block) || isUiBrowseItem(block))) {
|
|
478
|
+
for (let k = block.startLine; k <= block.endLine; k++) {
|
|
479
|
+
linesToRemove.add(k);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (block.kind === 'insert') {
|
|
483
|
+
let remainingSubCount = 0;
|
|
484
|
+
for (const sub of block.subItems) {
|
|
485
|
+
if (isHostBrowseItem(sub) || isUiBrowseItem(sub)) {
|
|
486
|
+
for (let k = sub.startLine; k <= sub.endLine; k++) {
|
|
487
|
+
linesToRemove.add(k);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
remainingSubCount++;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// 若该 `- insert:` 块下的所有子项均已被移除,则连同 `- insert:` 头一起清理
|
|
495
|
+
if (remainingSubCount === 0 && block.subItems.length > 0) {
|
|
496
|
+
for (let k = block.startLine; k <= block.endLine; k++) {
|
|
497
|
+
linesToRemove.add(k);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (linesToRemove.size === 0) {
|
|
503
|
+
return { content: rawContent, changed: false };
|
|
504
|
+
}
|
|
505
|
+
const keptLines = lines.filter((_, idx) => !linesToRemove.has(idx));
|
|
506
|
+
// 折叠连续超过 2 行的空白行,保持文件整洁
|
|
507
|
+
const normalizedLines = [];
|
|
508
|
+
let blankRun = 0;
|
|
509
|
+
for (const line of keptLines) {
|
|
510
|
+
if (line.trim() === '') {
|
|
511
|
+
blankRun++;
|
|
512
|
+
if (blankRun <= 1)
|
|
513
|
+
normalizedLines.push('');
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
blankRun = 0;
|
|
517
|
+
normalizedLines.push(line);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const joined = normalizedLines.join('\n').replace(/^\n+/, '').replace(/\s+$/, '');
|
|
521
|
+
const finalContent = joined ? `${joined}\n` : '';
|
|
522
|
+
return {
|
|
523
|
+
content: finalContent,
|
|
524
|
+
changed: finalContent !== rawContent,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* 原子写入目标 `cordis.patch.yml` 文件
|
|
529
|
+
*/
|
|
530
|
+
function atomicWritePatchFile(filePath, content) {
|
|
531
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
532
|
+
try {
|
|
533
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
534
|
+
writeFileSync(tmpPath, content, 'utf8');
|
|
535
|
+
renameSync(tmpPath, filePath);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
try {
|
|
539
|
+
if (existsSync(tmpPath))
|
|
540
|
+
unlinkSync(tmpPath);
|
|
541
|
+
}
|
|
542
|
+
catch { }
|
|
543
|
+
throw err;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* 获取目录选择器当前综合状态(供 `/api/remote-mobile/status` 与设置页消费)
|
|
548
|
+
*/
|
|
549
|
+
export function getDirectoryPickerStatus(customPatchPath) {
|
|
550
|
+
const patchFilePath = resolveDirectoryPickerPatchPath(customPatchPath);
|
|
551
|
+
let rawContent = '';
|
|
552
|
+
try {
|
|
553
|
+
if (existsSync(patchFilePath)) {
|
|
554
|
+
rawContent = readFileSync(patchFilePath, 'utf8');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch { }
|
|
558
|
+
const patchInfo = inspectDirectoryPickerPatchContent(rawContent);
|
|
559
|
+
const runtimeInfo = inspectRuntimeDirectoryPicker();
|
|
560
|
+
const isLoopback = boundIsLoopbackCore();
|
|
561
|
+
// 若运行时 loader 可探测到目录选择器条目,则以 loader 实际状态为准;否则以配置文件状态为准
|
|
562
|
+
const runtimeActive = runtimeInfo.detected ? runtimeInfo.browseActive : patchInfo.configured;
|
|
563
|
+
const restartRequired = runtimeInfo.detected ? patchInfo.configured !== runtimeInfo.browseActive : false;
|
|
564
|
+
// 当核心绑定在本地回环(如默认 proxy 模式)且既未在配置中开启 browse、运行时也未激活 browse 时,展示醒目提醒
|
|
565
|
+
const needsWarning = isLoopback && !patchInfo.configured && !runtimeActive;
|
|
566
|
+
return {
|
|
567
|
+
patchEnabled: patchInfo.configured,
|
|
568
|
+
patchPartial: patchInfo.partial,
|
|
569
|
+
runtimeActive,
|
|
570
|
+
restartRequired,
|
|
571
|
+
needsWarning,
|
|
572
|
+
patchFilePath,
|
|
573
|
+
yamlSnippet: DIRECTORY_PICKER_YAML_SNIPPET,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 在目标 `cordis.patch.yml` 中一键开启网页目录选择器(幂等去重)
|
|
578
|
+
*/
|
|
579
|
+
export function enableDirectoryPickerBrowse(customPatchPath) {
|
|
580
|
+
const patchFilePath = resolveDirectoryPickerPatchPath(customPatchPath);
|
|
581
|
+
let rawContent = '';
|
|
582
|
+
try {
|
|
583
|
+
if (existsSync(patchFilePath)) {
|
|
584
|
+
rawContent = readFileSync(patchFilePath, 'utf8');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch { }
|
|
588
|
+
const { content, changed } = applyDirectoryPickerBrowsePatch(rawContent);
|
|
589
|
+
if (changed || !existsSync(patchFilePath)) {
|
|
590
|
+
atomicWritePatchFile(patchFilePath, content);
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
...getDirectoryPickerStatus(patchFilePath),
|
|
594
|
+
changed,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 从目标 `cordis.patch.yml` 中移除网页目录选择器配置(恢复 PC 原生目录选择器)
|
|
599
|
+
*/
|
|
600
|
+
export function disableDirectoryPickerBrowse(customPatchPath) {
|
|
601
|
+
const patchFilePath = resolveDirectoryPickerPatchPath(customPatchPath);
|
|
602
|
+
if (!existsSync(patchFilePath)) {
|
|
603
|
+
return {
|
|
604
|
+
...getDirectoryPickerStatus(patchFilePath),
|
|
605
|
+
changed: false,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const rawContent = readFileSync(patchFilePath, 'utf8');
|
|
609
|
+
const { content, changed } = removeDirectoryPickerBrowsePatch(rawContent);
|
|
610
|
+
if (changed) {
|
|
611
|
+
atomicWritePatchFile(patchFilePath, content);
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
...getDirectoryPickerStatus(patchFilePath),
|
|
615
|
+
changed,
|
|
616
|
+
};
|
|
617
|
+
}
|