dsh-mcloud 0.2.1 → 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/lib/index.js CHANGED
@@ -61,16 +61,23 @@ const MCLOUD_RETRY_DELAY_MS = 400
61
61
 
62
62
  /**
63
63
  * 目录分页大小。
64
- * 实测(同一个 100 项的目录,各测 3 次取中位数):
65
- * pageSize= 10 → 1353 ms
66
- * pageSize= 20 → 6631 ms
67
- * pageSize= 30 → 13692 ms
68
- * pageSize= 50 → 16088 ms
69
- * pageSize=100 → 25311 ms(还会超时/ECONNRESET)
70
- * 这个接口对「一次要多少条」极度敏感,几乎是二次方恶化。
71
- * 所以只请求 10 项 —— 首屏要的是「快」,剩下的靠「加载更多」。
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 = 10
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 = mode === 'folder' ? PICKER_FOLDER : PICKER_FILES
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.2.1",
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
- }
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
+ }