dsh-mcloud 0.2.0 → 0.3.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.
- package/CHANGELOG.md +93 -36
- package/CONTRIBUTING.md +1 -1
- package/FAQ.md +6 -4
- package/README.md +250 -224
- package/lib/client.js +2650 -501
- package/lib/index.js +171 -10
- package/package.json +64 -64
package/lib/index.js
CHANGED
|
@@ -61,16 +61,23 @@ const MCLOUD_RETRY_DELAY_MS = 400
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* 目录分页大小。
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* pageSize=
|
|
70
|
-
*
|
|
71
|
-
*
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ 这里的数据修正过一次。早期测出来的结论是「pageSize 越大越慢,几乎二次方恶化」
|
|
66
|
+
* (10 → 1.3s,100 → 25s),于是定成了 10。但后来在真实的大目录
|
|
67
|
+
* (手机图片,几百上千张)上重测,结果完全不同:
|
|
68
|
+
*
|
|
69
|
+
* pageSize= 10 → 218 ms 每 100 项约 2180 ms
|
|
70
|
+
* pageSize= 30 → 248 ms 每 100 项约 827 ms
|
|
71
|
+
* pageSize= 50 → 296 ms 每 100 项约 592 ms
|
|
72
|
+
* pageSize=100 → 451 ms 每 100 项约 451 ms
|
|
73
|
+
*
|
|
74
|
+
* 也就是说:**瓶颈是每次请求的固定开销,不是条目数**。
|
|
75
|
+
* 一次多拿几十条几乎不要钱,但每页只拿 10 条就要多跑好几倍的往返。
|
|
76
|
+
*
|
|
77
|
+
* 取 50:相比 10 把「单位成本」降到 1/3.7,同时给单次响应留了余量
|
|
78
|
+
* (某些目录条目体积大时,100 档的尾部延迟会更明显)。
|
|
72
79
|
*/
|
|
73
|
-
const LIST_PAGE_SIZE =
|
|
80
|
+
const LIST_PAGE_SIZE = 50
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
83
|
* 目录列表缓存。点文件夹「慢」的很大一部分原因是每次进出都重新拉一遍,
|
|
@@ -504,6 +511,21 @@ $script:timer.Stop()
|
|
|
504
511
|
if ($r -eq [System.Windows.Forms.DialogResult]::OK) { & $emit $d.SelectedPath }
|
|
505
512
|
`
|
|
506
513
|
|
|
514
|
+
/**
|
|
515
|
+
* 选「下载目录」专用的对话框。
|
|
516
|
+
*
|
|
517
|
+
* 和上传那个分开:文案不同,而且这里允许「新建文件夹」——
|
|
518
|
+
* 用户选下载目录时往往是想就地建一个,不给这个按钮反而别扭。
|
|
519
|
+
*/
|
|
520
|
+
const PICKER_DOWNLOAD_DIR = `${PICKER_PRELUDE}
|
|
521
|
+
$d = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
522
|
+
$d.Description = '选择文件下载到本地的目录'
|
|
523
|
+
$d.ShowNewFolderButton = $true
|
|
524
|
+
$r = $d.ShowDialog()
|
|
525
|
+
$script:timer.Stop()
|
|
526
|
+
if ($r -eq [System.Windows.Forms.DialogResult]::OK) { & $emit $d.SelectedPath }
|
|
527
|
+
`
|
|
528
|
+
|
|
507
529
|
/**
|
|
508
530
|
* 弹系统原生的「打开文件 / 选择文件夹」对话框,返回选中的真实路径。
|
|
509
531
|
* 走的是宿主端进程,所以拿到的是磁盘真实路径 —— 上传时直接从磁盘流式读,
|
|
@@ -513,7 +535,8 @@ function pickLocalPaths(mode) {
|
|
|
513
535
|
if (!windows) {
|
|
514
536
|
return Promise.reject(new Error('原生文件对话框目前只支持 Windows,请直接手填路径'))
|
|
515
537
|
}
|
|
516
|
-
const script =
|
|
538
|
+
const script =
|
|
539
|
+
mode === 'download-dir' ? PICKER_DOWNLOAD_DIR : mode === 'folder' ? PICKER_FOLDER : PICKER_FILES
|
|
517
540
|
return new Promise((resolvePick, rejectPick) => {
|
|
518
541
|
const child = spawn(powershellPath(), ['-NoProfile', '-STA', '-Command', script], {
|
|
519
542
|
windowsHide: true,
|
|
@@ -1029,6 +1052,24 @@ function createClient(store, logger, config) {
|
|
|
1029
1052
|
return true
|
|
1030
1053
|
}
|
|
1031
1054
|
|
|
1055
|
+
/**
|
|
1056
|
+
* 清空「已经结束」的任务记录。
|
|
1057
|
+
*
|
|
1058
|
+
* ⚠️ 只动内存里的记录表,**不碰网盘和本地文件**。
|
|
1059
|
+
* 正在跑的任务不能删 —— 删了界面就再也看不到它的进度了,
|
|
1060
|
+
* 而且用户点「清空」时想清的显然是历史,不是眼前这条。
|
|
1061
|
+
*/
|
|
1062
|
+
function clearFinishedJobs() {
|
|
1063
|
+
let removed = 0
|
|
1064
|
+
for (const [id, job] of jobs) {
|
|
1065
|
+
if (job.state !== 'running') {
|
|
1066
|
+
jobs.delete(id)
|
|
1067
|
+
removed += 1
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return removed
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1032
1073
|
/**
|
|
1033
1074
|
* 上传本地文件。流程见 transfer.js 顶部的注释。
|
|
1034
1075
|
* 注意:整个文件只流式读一遍算哈希,分片也是流式 PUT,不把文件读进内存。
|
|
@@ -1465,6 +1506,82 @@ function createClient(store, logger, config) {
|
|
|
1465
1506
|
return { cancelled: true }
|
|
1466
1507
|
}
|
|
1467
1508
|
|
|
1509
|
+
/**
|
|
1510
|
+
* 界面用的搜索:在当前目录及其子目录里按名字找。
|
|
1511
|
+
*
|
|
1512
|
+
* 和模型工具 `mcloud_search` 的差别:
|
|
1513
|
+
* · 工具版是 BFS 走 10 页,适合「帮我找找」,用户等得起;
|
|
1514
|
+
* · 界面版是人在输入框里打字,必须**尽快出第一批结果**,并且要能中途取消。
|
|
1515
|
+
*
|
|
1516
|
+
* 所以这里:先只搜当前这一层(最快),够了就停;不够再往下走,
|
|
1517
|
+
* 且总量封顶,避免用户搜个常见字把整个网盘翻一遍。
|
|
1518
|
+
*/
|
|
1519
|
+
async function searchFiles(keyword, parentFileId = ROOT_ID, options = {}) {
|
|
1520
|
+
const needle = String(keyword ?? '').trim().toLowerCase()
|
|
1521
|
+
if (!needle) return { items: [], truncated: false }
|
|
1522
|
+
|
|
1523
|
+
// 只在磁盘上跑:limit 是「返回上限」,pages 是「最多翻多少页」
|
|
1524
|
+
const limit = Math.max(1, Math.min(500, Number(options.limit) || 120))
|
|
1525
|
+
const maxPages = Math.max(1, Math.min(60, Number(options.maxPages) || 24))
|
|
1526
|
+
const depthLimit = Math.max(0, Math.min(8, Number(options.depth) ?? 4))
|
|
1527
|
+
|
|
1528
|
+
const matches = []
|
|
1529
|
+
let pages = 0
|
|
1530
|
+
let truncated = false
|
|
1531
|
+
|
|
1532
|
+
/** 走一层:返回这一层里的文件夹 id,供下一层继续。 */
|
|
1533
|
+
async function scanFolder(folderId, depth, pathLabel) {
|
|
1534
|
+
const children = []
|
|
1535
|
+
let cursor = ''
|
|
1536
|
+
do {
|
|
1537
|
+
if (pages >= maxPages || matches.length >= limit) {
|
|
1538
|
+
truncated = true
|
|
1539
|
+
return children
|
|
1540
|
+
}
|
|
1541
|
+
const page = await listFiles(folderId, cursor)
|
|
1542
|
+
pages += 1
|
|
1543
|
+
for (const item of page.items) {
|
|
1544
|
+
const hit = String(item.name || '').toLowerCase().includes(needle)
|
|
1545
|
+
if (hit) {
|
|
1546
|
+
matches.push({
|
|
1547
|
+
...item,
|
|
1548
|
+
parent: folderId,
|
|
1549
|
+
parentPath: pathLabel,
|
|
1550
|
+
})
|
|
1551
|
+
if (matches.length >= limit) {
|
|
1552
|
+
truncated = true
|
|
1553
|
+
return children
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (item.type === 'folder') children.push(item)
|
|
1557
|
+
}
|
|
1558
|
+
cursor = page.nextCursor || ''
|
|
1559
|
+
} while (cursor)
|
|
1560
|
+
return children
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// 第一层(当前目录):优先、同步完成
|
|
1564
|
+
const level1 = await scanFolder(parentFileId, 0, '/')
|
|
1565
|
+
|
|
1566
|
+
if (depthLimit > 0 && matches.length < limit && pages < maxPages) {
|
|
1567
|
+
let frontier = level1.map((folder) => ({ id: folder.id, path: '/' + folder.name }))
|
|
1568
|
+
for (let depth = 1; depth <= depthLimit && frontier.length && matches.length < limit; depth += 1) {
|
|
1569
|
+
const next = []
|
|
1570
|
+
for (const folder of frontier) {
|
|
1571
|
+
if (pages >= maxPages || matches.length >= limit) {
|
|
1572
|
+
truncated = true
|
|
1573
|
+
break
|
|
1574
|
+
}
|
|
1575
|
+
const kids = await scanFolder(folder.id, depth, folder.path)
|
|
1576
|
+
for (const kid of kids) next.push({ id: kid.id, path: folder.path + '/' + kid.name })
|
|
1577
|
+
}
|
|
1578
|
+
frontier = next
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
return { items: matches, truncated, pages }
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1468
1585
|
return {
|
|
1469
1586
|
status,
|
|
1470
1587
|
authorize,
|
|
@@ -1475,6 +1592,7 @@ function createClient(store, logger, config) {
|
|
|
1475
1592
|
cancelScan,
|
|
1476
1593
|
forget,
|
|
1477
1594
|
listFiles,
|
|
1595
|
+
searchFiles,
|
|
1478
1596
|
getDownloadUrl,
|
|
1479
1597
|
makeDir,
|
|
1480
1598
|
renameNode,
|
|
@@ -1485,6 +1603,7 @@ function createClient(store, logger, config) {
|
|
|
1485
1603
|
getJob,
|
|
1486
1604
|
listJobs,
|
|
1487
1605
|
cancelJob,
|
|
1606
|
+
clearFinishedJobs,
|
|
1488
1607
|
ensureFolder,
|
|
1489
1608
|
uploadFile,
|
|
1490
1609
|
uploadFolder,
|
|
@@ -1997,6 +2116,36 @@ function createRoutes(client) {
|
|
|
1997
2116
|
}
|
|
1998
2117
|
},
|
|
1999
2118
|
},
|
|
2119
|
+
{
|
|
2120
|
+
kind: 'exact',
|
|
2121
|
+
path: `${base}/search`,
|
|
2122
|
+
handler: async (req, res) => {
|
|
2123
|
+
if (!guard(req, res)) return
|
|
2124
|
+
try {
|
|
2125
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
2126
|
+
const keyword = url.searchParams.get('keyword') || ''
|
|
2127
|
+
const parentFileId = url.searchParams.get('parentFileId') || ROOT_ID
|
|
2128
|
+
const depth = url.searchParams.get('depth')
|
|
2129
|
+
const result = await client.searchFiles(keyword, parentFileId, {
|
|
2130
|
+
depth: depth === null ? undefined : Number(depth),
|
|
2131
|
+
})
|
|
2132
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
writeJson(res, 500, { ok: false, error: errorMessage(error) })
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
kind: 'exact',
|
|
2140
|
+
path: `${base}/pick-dir`,
|
|
2141
|
+
handler: async (req, res) => {
|
|
2142
|
+
if (!guardWrite(req, res)) return
|
|
2143
|
+
await post(req, res, async () => {
|
|
2144
|
+
const paths = await pickLocalPaths('download-dir')
|
|
2145
|
+
return { path: paths[0] || '', cancelled: paths.length === 0 }
|
|
2146
|
+
})
|
|
2147
|
+
},
|
|
2148
|
+
},
|
|
2000
2149
|
{
|
|
2001
2150
|
kind: 'exact',
|
|
2002
2151
|
path: `${base}/link`,
|
|
@@ -2060,6 +2209,18 @@ function createRoutes(client) {
|
|
|
2060
2209
|
})
|
|
2061
2210
|
},
|
|
2062
2211
|
},
|
|
2212
|
+
{
|
|
2213
|
+
kind: 'exact',
|
|
2214
|
+
path: `${base}/transfers/clear`,
|
|
2215
|
+
handler: async (req, res) => {
|
|
2216
|
+
if (!guardWrite(req, res)) return
|
|
2217
|
+
await post(req, res, async () => {
|
|
2218
|
+
// 只清记录,不碰网盘文件,也不需要用户确认
|
|
2219
|
+
const removed = client.clearFinishedJobs()
|
|
2220
|
+
return { removed }
|
|
2221
|
+
})
|
|
2222
|
+
},
|
|
2223
|
+
},
|
|
2063
2224
|
{
|
|
2064
2225
|
kind: 'exact',
|
|
2065
2226
|
path: `${base}/transfers`,
|
package/package.json
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-mcloud",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "中国移动云盘(139)接入 DeepSeek Harness:容量查询、列目录、上传下载、转移、以及在会话页提供一个可操作的标签面板。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"icon": "./icon.svg",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./lib/index.js",
|
|
10
|
-
"./client": "./lib/client.js",
|
|
11
|
-
"./locale/*.json": "./locale/*.json",
|
|
12
|
-
"./icon": "./icon.svg",
|
|
13
|
-
"./package.json": "./package.json"
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"lib",
|
|
17
|
-
"locale/*.json",
|
|
18
|
-
"icon.svg",
|
|
19
|
-
"cordis.patch.yml",
|
|
20
|
-
"README.md",
|
|
21
|
-
"CHANGELOG.md",
|
|
22
|
-
"CONTRIBUTING.md",
|
|
23
|
-
"FAQ.md",
|
|
24
|
-
"LICENSE"
|
|
25
|
-
],
|
|
26
|
-
"dsh": {
|
|
27
|
-
"bundle": {
|
|
28
|
-
"patch": "./cordis.patch.yml"
|
|
29
|
-
},
|
|
30
|
-
"client": {
|
|
31
|
-
"platform": "web",
|
|
32
|
-
"immediately": true,
|
|
33
|
-
"inject": [
|
|
34
|
-
"@deepseek-ai/dsh-client-locale"
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"keywords": [
|
|
39
|
-
"dsh",
|
|
40
|
-
"dsh-plugin",
|
|
41
|
-
"deepseek-harness",
|
|
42
|
-
"139",
|
|
43
|
-
"mobile-cloud",
|
|
44
|
-
"china-mobile",
|
|
45
|
-
"yun139"
|
|
46
|
-
],
|
|
47
|
-
"engines": {
|
|
48
|
-
"node": ">=22",
|
|
49
|
-
"dsh": ">=0.2.1-rc.5 <0.
|
|
50
|
-
},
|
|
51
|
-
"repository": {
|
|
52
|
-
"type": "git",
|
|
53
|
-
"url": "git+https://github.com/GIYN-S/dsh-mcloud.git"
|
|
54
|
-
},
|
|
55
|
-
"homepage": "https://github.com/GIYN-S/dsh-mcloud#readme",
|
|
56
|
-
"bugs": {
|
|
57
|
-
"url": "https://github.com/GIYN-S/dsh-mcloud/issues"
|
|
58
|
-
},
|
|
59
|
-
"publishConfig": {
|
|
60
|
-
"access": "public",
|
|
61
|
-
"registry": "https://registry.npmjs.org/"
|
|
62
|
-
},
|
|
63
|
-
"license": "MIT"
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-mcloud",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "中国移动云盘(139)接入 DeepSeek Harness:容量查询、列目录、上传下载、转移、以及在会话页提供一个可操作的标签面板。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"icon": "./icon.svg",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./locale/*.json": "./locale/*.json",
|
|
12
|
+
"./icon": "./icon.svg",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"locale/*.json",
|
|
18
|
+
"icon.svg",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"CONTRIBUTING.md",
|
|
23
|
+
"FAQ.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
},
|
|
30
|
+
"client": {
|
|
31
|
+
"platform": "web",
|
|
32
|
+
"immediately": true,
|
|
33
|
+
"inject": [
|
|
34
|
+
"@deepseek-ai/dsh-client-locale"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"dsh",
|
|
40
|
+
"dsh-plugin",
|
|
41
|
+
"deepseek-harness",
|
|
42
|
+
"139",
|
|
43
|
+
"mobile-cloud",
|
|
44
|
+
"china-mobile",
|
|
45
|
+
"yun139"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22",
|
|
49
|
+
"dsh": ">=0.2.1-rc.5 <0.3.0-0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/GIYN-S/dsh-mcloud.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/GIYN-S/dsh-mcloud#readme",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/GIYN-S/dsh-mcloud/issues"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"registry": "https://registry.npmjs.org/"
|
|
62
|
+
},
|
|
63
|
+
"license": "MIT"
|
|
64
|
+
}
|