@jackwener/opencli 1.6.8 → 1.6.9

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.
Files changed (84) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -1
  3. package/dist/clis/jianyu/search.d.ts +14 -0
  4. package/dist/clis/jianyu/search.js +135 -0
  5. package/dist/clis/jianyu/search.test.d.ts +1 -0
  6. package/dist/clis/jianyu/search.test.js +23 -0
  7. package/dist/clis/quark/ls.d.ts +1 -0
  8. package/dist/clis/quark/ls.js +63 -0
  9. package/dist/clis/quark/mkdir.d.ts +1 -0
  10. package/dist/clis/quark/mkdir.js +36 -0
  11. package/dist/clis/quark/mv.d.ts +1 -0
  12. package/dist/clis/quark/mv.js +53 -0
  13. package/dist/clis/quark/rename.d.ts +1 -0
  14. package/dist/clis/quark/rename.js +26 -0
  15. package/dist/clis/quark/rm.d.ts +1 -0
  16. package/dist/clis/quark/rm.js +24 -0
  17. package/dist/clis/quark/save.d.ts +1 -0
  18. package/dist/clis/quark/save.js +80 -0
  19. package/dist/clis/quark/share-tree.d.ts +1 -0
  20. package/dist/clis/quark/share-tree.js +45 -0
  21. package/dist/clis/quark/utils.d.ts +50 -0
  22. package/dist/clis/quark/utils.js +146 -0
  23. package/dist/clis/quark/utils.test.d.ts +1 -0
  24. package/dist/clis/quark/utils.test.js +58 -0
  25. package/dist/clis/twitter/reply.js +3 -8
  26. package/dist/clis/twitter/reply.test.js +5 -5
  27. package/dist/clis/xiaohongshu/note.js +8 -3
  28. package/dist/clis/xiaohongshu/note.test.js +11 -0
  29. package/dist/clis/zhihu/answer.d.ts +1 -0
  30. package/dist/clis/zhihu/answer.js +194 -0
  31. package/dist/clis/zhihu/answer.test.d.ts +1 -0
  32. package/dist/clis/zhihu/answer.test.js +81 -0
  33. package/dist/clis/zhihu/comment.d.ts +1 -0
  34. package/dist/clis/zhihu/comment.js +335 -0
  35. package/dist/clis/zhihu/comment.test.d.ts +1 -0
  36. package/dist/clis/zhihu/comment.test.js +54 -0
  37. package/dist/clis/zhihu/favorite.d.ts +1 -0
  38. package/dist/clis/zhihu/favorite.js +224 -0
  39. package/dist/clis/zhihu/favorite.test.d.ts +1 -0
  40. package/dist/clis/zhihu/favorite.test.js +196 -0
  41. package/dist/clis/zhihu/follow.d.ts +1 -0
  42. package/dist/clis/zhihu/follow.js +80 -0
  43. package/dist/clis/zhihu/follow.test.d.ts +1 -0
  44. package/dist/clis/zhihu/follow.test.js +45 -0
  45. package/dist/clis/zhihu/like.d.ts +1 -0
  46. package/dist/clis/zhihu/like.js +91 -0
  47. package/dist/clis/zhihu/like.test.d.ts +1 -0
  48. package/dist/clis/zhihu/like.test.js +64 -0
  49. package/dist/clis/zhihu/target.d.ts +24 -0
  50. package/dist/clis/zhihu/target.js +91 -0
  51. package/dist/clis/zhihu/target.test.d.ts +1 -0
  52. package/dist/clis/zhihu/target.test.js +77 -0
  53. package/dist/clis/zhihu/write-shared.d.ts +32 -0
  54. package/dist/clis/zhihu/write-shared.js +221 -0
  55. package/dist/clis/zhihu/write-shared.test.d.ts +1 -0
  56. package/dist/clis/zhihu/write-shared.test.js +175 -0
  57. package/dist/src/browser/bridge.d.ts +2 -0
  58. package/dist/src/browser/bridge.js +30 -24
  59. package/dist/src/browser/daemon-client.d.ts +17 -8
  60. package/dist/src/browser/daemon-client.js +12 -13
  61. package/dist/src/browser/daemon-client.test.js +32 -25
  62. package/dist/src/browser/index.d.ts +2 -1
  63. package/dist/src/browser/index.js +1 -1
  64. package/dist/src/browser.test.js +2 -3
  65. package/dist/src/cli.js +3 -3
  66. package/dist/src/clis/binance/commands.test.d.ts +1 -0
  67. package/dist/src/clis/binance/commands.test.js +54 -0
  68. package/dist/src/commanderAdapter.js +19 -6
  69. package/dist/src/diagnostic.d.ts +1 -0
  70. package/dist/src/diagnostic.js +64 -2
  71. package/dist/src/diagnostic.test.js +91 -1
  72. package/dist/src/doctor.d.ts +2 -0
  73. package/dist/src/doctor.js +59 -31
  74. package/dist/src/doctor.test.js +89 -16
  75. package/dist/src/execution.js +1 -13
  76. package/dist/src/explore.js +1 -1
  77. package/dist/src/generate.d.ts +2 -5
  78. package/dist/src/generate.js +2 -5
  79. package/dist/src/plugin.d.ts +2 -1
  80. package/dist/src/plugin.js +25 -8
  81. package/dist/src/plugin.test.js +16 -1
  82. package/package.json +3 -3
  83. package/dist/src/browser/discover.d.ts +0 -15
  84. package/dist/src/browser/discover.js +0 -19
package/README.md CHANGED
@@ -132,6 +132,7 @@ git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && n
132
132
  | **hupu** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` |
133
133
  | **twitter** | `trending` `search` `timeline` `bookmarks` `post` `download` `profile` `article` `like` `likes` `notifications` `reply` `reply-dm` `thread` `follow` `unfollow` `followers` `following` `block` `unblock` `bookmark` `unbookmark` `delete` `hide-reply` `accept` |
134
134
  | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `upvoted` `save` `saved` `comment` `subscribe` |
135
+ | **zhihu** | `hot` `search` `question` `download` `follow` `like` `favorite` `comment` `answer` |
135
136
  | **amazon** | `bestsellers` `search` `product` `offer` `discussion` `movers-shakers` `new-releases` |
136
137
  | **1688** | `search` `item` `assets` `download` `store` |
137
138
  | **gemini** | `new` `ask` `image` `deep-research` `deep-research-result` |
@@ -140,6 +141,7 @@ git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && n
140
141
  | **spotify** | `auth` `status` `play` `pause` `next` `prev` `volume` `search` `queue` `shuffle` `repeat` |
141
142
  | **xianyu** | `search` `item` `chat` |
142
143
  | **xiaoe** | `courses` `detail` `catalog` `play-url` `content` |
144
+ | **quark** | `ls` `mkdir` `mv` `rename` `rm` `save` `share-tree` |
143
145
 
144
146
  79+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)**
145
147
 
package/README.zh-CN.md CHANGED
@@ -147,9 +147,10 @@ npx skills add jackwener/opencli --skill opencli-oneshot # 快速命令参
147
147
  | **chatgpt** | `status` `new` `send` `read` `ask` `model` | 桌面端 |
148
148
  | **xiaohongshu** | `search` `notifications` `feed` `user` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 浏览器 |
149
149
  | **xiaoe** | `courses` `detail` `catalog` `play-url` `content` | 浏览器 |
150
+ | **quark** | `ls` `mkdir` `mv` `rename` `rm` `save` `share-tree` | 浏览器 |
150
151
  | **apple-podcasts** | `search` `episodes` `top` | 公开 |
151
152
  | **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 公开 |
152
- | **zhihu** | `hot` `search` `question` `download` | 浏览器 |
153
+ | **zhihu** | `hot` `search` `question` `download` `follow` `like` `favorite` `comment` `answer` | 浏览器 |
153
154
  | **weixin** | `download` | 浏览器 |
154
155
  | **youtube** | `search` `video` `transcript` | 浏览器 |
155
156
  | **boss** | `search` `detail` `recommend` `joblist` `greet` `batchgreet` `send` `chatlist` `chatmsg` `invite` `mark` `exchange` `resume` `stats` | 浏览器 |
@@ -0,0 +1,14 @@
1
+ interface JianyuCandidate {
2
+ title: string;
3
+ url: string;
4
+ date: string;
5
+ }
6
+ export declare function buildSearchUrl(query: string): string;
7
+ export declare function normalizeDate(raw: string): string;
8
+ declare function dedupeCandidates(items: JianyuCandidate[]): JianyuCandidate[];
9
+ export declare const __test__: {
10
+ buildSearchUrl: typeof buildSearchUrl;
11
+ normalizeDate: typeof normalizeDate;
12
+ dedupeCandidates: typeof dedupeCandidates;
13
+ };
14
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Jianyu search — browser DOM extraction from Jianyu bid search page.
3
+ */
4
+ import { cli, Strategy } from '@jackwener/opencli/registry';
5
+ import { AuthRequiredError } from '@jackwener/opencli/errors';
6
+ const SEARCH_ENTRY = 'https://www.jianyu360.cn/jylab/supsearch/index.html';
7
+ function cleanText(value) {
8
+ return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
9
+ }
10
+ export function buildSearchUrl(query) {
11
+ const url = new URL(SEARCH_ENTRY);
12
+ url.searchParams.set('keywords', query.trim());
13
+ url.searchParams.set('selectType', 'title');
14
+ url.searchParams.set('searchGroup', '1');
15
+ return url.toString();
16
+ }
17
+ export function normalizeDate(raw) {
18
+ const normalized = cleanText(raw);
19
+ const match = normalized.match(/(20\d{2})[.\-/年](\d{1,2})[.\-/月](\d{1,2})/);
20
+ if (!match)
21
+ return '';
22
+ const year = match[1];
23
+ const month = match[2].padStart(2, '0');
24
+ const day = match[3].padStart(2, '0');
25
+ return `${year}-${month}-${day}`;
26
+ }
27
+ function dedupeCandidates(items) {
28
+ const deduped = [];
29
+ const seen = new Set();
30
+ for (const item of items) {
31
+ const key = `${item.title}\t${item.url}`;
32
+ if (seen.has(key))
33
+ continue;
34
+ seen.add(key);
35
+ deduped.push(item);
36
+ }
37
+ return deduped;
38
+ }
39
+ cli({
40
+ site: 'jianyu',
41
+ name: 'search',
42
+ description: '搜索剑鱼标讯公告',
43
+ domain: 'www.jianyu360.cn',
44
+ strategy: Strategy.COOKIE,
45
+ browser: true,
46
+ args: [
47
+ { name: 'query', required: true, positional: true, help: 'Search keyword, e.g. "procurement"' },
48
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
49
+ ],
50
+ columns: ['rank', 'title', 'date', 'url'],
51
+ func: async (page, kwargs) => {
52
+ const query = cleanText(kwargs.query);
53
+ const limit = Math.max(1, Math.min(Number(kwargs.limit) || 20, 50));
54
+ const searchUrl = buildSearchUrl(query);
55
+ await page.goto(searchUrl);
56
+ await page.wait(2);
57
+ const payload = await page.evaluate(`
58
+ (() => {
59
+ const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
60
+ const toAbsolute = (href) => {
61
+ if (!href) return '';
62
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
63
+ if (href.startsWith('/')) return new URL(href, window.location.origin).toString();
64
+ return '';
65
+ };
66
+ const parseDate = (text) => {
67
+ const normalized = clean(text);
68
+ const match = normalized.match(/(20\\d{2})[.\\-/年](\\d{1,2})[.\\-/月](\\d{1,2})/);
69
+ if (!match) return '';
70
+ const month = String(match[2]).padStart(2, '0');
71
+ const day = String(match[3]).padStart(2, '0');
72
+ return match[1] + '-' + month + '-' + day;
73
+ };
74
+ const pickDateText = (node) => {
75
+ let cursor = node;
76
+ for (let i = 0; i < 4 && cursor; i++) {
77
+ const text = clean(cursor.innerText || cursor.textContent || '');
78
+ const date = parseDate(text);
79
+ if (date) return date;
80
+ cursor = cursor.parentElement;
81
+ }
82
+ return '';
83
+ };
84
+
85
+ const anchors = Array.from(
86
+ document.querySelectorAll('a[href*="/nologin/content/"], a[href*="/content/"]'),
87
+ );
88
+ const rows = [];
89
+ const seen = new Set();
90
+ for (const anchor of anchors) {
91
+ const url = toAbsolute(anchor.getAttribute('href') || anchor.href || '');
92
+ const title = clean(anchor.textContent || '');
93
+ if (!url || !title || title.length < 4) continue;
94
+ const key = title + '\\t' + url;
95
+ if (seen.has(key)) continue;
96
+ seen.add(key);
97
+ rows.push({
98
+ title,
99
+ url,
100
+ date: pickDateText(anchor),
101
+ });
102
+ }
103
+ return rows;
104
+ })()
105
+ `);
106
+ const pageText = cleanText(await page.evaluate('document.body ? document.body.innerText : ""'));
107
+ if (!Array.isArray(payload)
108
+ && /(请先登录|登录后|未登录|验证码)/.test(pageText)) {
109
+ throw new AuthRequiredError('www.jianyu360.cn', 'Jianyu search results require login or human verification');
110
+ }
111
+ const rows = Array.isArray(payload)
112
+ ? payload
113
+ .filter((item) => !!item && typeof item === 'object')
114
+ .map((item) => ({
115
+ title: cleanText(item.title),
116
+ url: cleanText(item.url),
117
+ date: normalizeDate(cleanText(item.date)),
118
+ }))
119
+ .filter((item) => item.title && item.url)
120
+ : [];
121
+ return dedupeCandidates(rows)
122
+ .slice(0, limit)
123
+ .map((item, index) => ({
124
+ rank: index + 1,
125
+ title: item.title,
126
+ date: item.date,
127
+ url: item.url,
128
+ }));
129
+ },
130
+ });
131
+ export const __test__ = {
132
+ buildSearchUrl,
133
+ normalizeDate,
134
+ dedupeCandidates,
135
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './search.js';
3
+ describe('jianyu search helpers', () => {
4
+ it('builds supsearch URL with required query params', () => {
5
+ const url = __test__.buildSearchUrl('procurement');
6
+ expect(url).toContain('keywords=procurement');
7
+ expect(url).toContain('selectType=title');
8
+ expect(url).toContain('searchGroup=1');
9
+ });
10
+ it('normalizes common date formats', () => {
11
+ expect(__test__.normalizeDate('2026-4-7')).toBe('2026-04-07');
12
+ expect(__test__.normalizeDate('2026年4月7日')).toBe('2026-04-07');
13
+ expect(__test__.normalizeDate('发布时间: 2026/04/07 09:00')).toBe('2026-04-07');
14
+ });
15
+ it('deduplicates by title and url', () => {
16
+ const deduped = __test__.dedupeCandidates([
17
+ { title: 'A', url: 'https://example.com/1', date: '2026-04-07' },
18
+ { title: 'A', url: 'https://example.com/1', date: '2026-04-07' },
19
+ { title: 'A', url: 'https://example.com/2', date: '2026-04-07' },
20
+ ]);
21
+ expect(deduped).toHaveLength(2);
22
+ });
23
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { findFolder, formatSize, listMyDrive, } from './utils.js';
3
+ async function buildTree(page, pdirFid, parentPath, depth, maxDepth, dirsOnly) {
4
+ if (depth > maxDepth)
5
+ return [];
6
+ const files = await listMyDrive(page, pdirFid);
7
+ const nodes = [];
8
+ for (const file of files) {
9
+ if (dirsOnly && !file.dir)
10
+ continue;
11
+ const path = parentPath ? `${parentPath}/${file.file_name}` : file.file_name;
12
+ const node = {
13
+ name: file.file_name,
14
+ fid: file.fid,
15
+ is_dir: file.dir,
16
+ size: formatSize(file.size),
17
+ path,
18
+ };
19
+ if (file.dir && depth < maxDepth) {
20
+ node.children = await buildTree(page, file.fid, path, depth + 1, maxDepth, dirsOnly);
21
+ }
22
+ nodes.push(node);
23
+ }
24
+ return nodes;
25
+ }
26
+ function flattenTree(nodes, level = 0) {
27
+ const result = [];
28
+ const indent = ' '.repeat(level);
29
+ for (const node of nodes) {
30
+ result.push({
31
+ name: `${indent}${node.name}`,
32
+ fid: node.fid,
33
+ is_dir: node.is_dir,
34
+ size: node.size,
35
+ path: node.path,
36
+ });
37
+ if (node.children) {
38
+ result.push(...flattenTree(node.children, level + 1));
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ cli({
44
+ site: 'quark',
45
+ name: 'ls',
46
+ description: 'List files in your Quark Drive',
47
+ domain: 'pan.quark.cn',
48
+ strategy: Strategy.COOKIE,
49
+ args: [
50
+ { name: 'path', positional: true, default: '', help: 'Folder path to list (empty for root)' },
51
+ { name: 'depth', type: 'int', default: 0, help: 'Max depth to traverse' },
52
+ { name: 'dirs-only', type: 'boolean', default: false, help: 'Show directories only' },
53
+ ],
54
+ columns: ['name', 'is_dir', 'size', 'fid', 'path'],
55
+ func: async (page, kwargs) => {
56
+ const path = kwargs.path ?? '';
57
+ const depth = Math.max(0, kwargs.depth ?? 0);
58
+ const dirsOnly = kwargs['dirs-only'] ?? false;
59
+ const rootFid = path ? await findFolder(page, path) : '0';
60
+ const tree = await buildTree(page, rootFid, path, 0, depth, dirsOnly);
61
+ return flattenTree(tree);
62
+ },
63
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { ArgumentError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { DRIVE_API, apiPost, findFolder } from './utils.js';
4
+ cli({
5
+ site: 'quark',
6
+ name: 'mkdir',
7
+ description: 'Create a folder in your Quark Drive',
8
+ domain: 'pan.quark.cn',
9
+ strategy: Strategy.COOKIE,
10
+ defaultFormat: 'json',
11
+ args: [
12
+ { name: 'name', required: true, positional: true, help: 'Folder name' },
13
+ { name: 'parent', help: 'Parent folder path (resolved by name)' },
14
+ { name: 'parent-fid', help: 'Parent folder fid (use directly)' },
15
+ ],
16
+ func: async (page, kwargs) => {
17
+ const name = kwargs.name;
18
+ if (!name.trim())
19
+ throw new ArgumentError('Folder name cannot be empty');
20
+ if (kwargs.parent && kwargs['parent-fid']) {
21
+ throw new ArgumentError('Cannot use both --parent and --parent-fid');
22
+ }
23
+ const parentFid = kwargs['parent-fid']
24
+ ? kwargs['parent-fid']
25
+ : kwargs.parent
26
+ ? await findFolder(page, kwargs.parent)
27
+ : '0';
28
+ const data = await apiPost(page, `${DRIVE_API}?pr=ucpro&fr=pc`, {
29
+ pdir_fid: parentFid,
30
+ file_name: name,
31
+ dir_path: '',
32
+ dir_init_lock: false,
33
+ });
34
+ return { status: 'ok', fid: data.fid, name };
35
+ },
36
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { DRIVE_API, apiPost, findFolder, pollTask } from './utils.js';
4
+ cli({
5
+ site: 'quark',
6
+ name: 'mv',
7
+ description: 'Move files to a folder in your Quark Drive',
8
+ domain: 'pan.quark.cn',
9
+ strategy: Strategy.COOKIE,
10
+ defaultFormat: 'json',
11
+ timeoutSeconds: 120,
12
+ args: [
13
+ { name: 'fids', required: true, positional: true, help: 'File IDs to move (comma-separated)' },
14
+ { name: 'to', default: '', help: 'Destination folder path (required unless --to-fid is set)' },
15
+ { name: 'to-fid', default: '', help: 'Destination folder ID (overrides --to)' },
16
+ ],
17
+ func: async (page, kwargs) => {
18
+ const to = kwargs.to;
19
+ const toFid = kwargs['to-fid'];
20
+ const fids = kwargs.fids;
21
+ const fidList = [...new Set(fids.split(',').map(id => id.trim()).filter(Boolean))];
22
+ if (fidList.length === 0)
23
+ throw new ArgumentError('No fids provided');
24
+ if (!to && !toFid)
25
+ throw new ArgumentError('Either --to or --to-fid is required');
26
+ if (to && toFid)
27
+ throw new ArgumentError('Cannot use both --to and --to-fid');
28
+ const targetFid = toFid || await findFolder(page, to);
29
+ const data = await apiPost(page, `${DRIVE_API}/move?pr=ucpro&fr=pc`, {
30
+ filelist: fidList,
31
+ to_pdir_fid: targetFid,
32
+ });
33
+ const result = {
34
+ status: 'pending',
35
+ count: fidList.length,
36
+ destination: to || toFid,
37
+ task_id: data.task_id,
38
+ completed: false,
39
+ };
40
+ if (data.task_id) {
41
+ const completed = await pollTask(page, data.task_id);
42
+ result.completed = completed;
43
+ result.status = completed ? 'ok' : 'error';
44
+ if (!completed)
45
+ throw new CommandExecutionError('quark: Move task timed out');
46
+ }
47
+ else {
48
+ result.status = 'ok';
49
+ result.completed = true;
50
+ }
51
+ return result;
52
+ },
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { ArgumentError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { DRIVE_API, apiPost } from './utils.js';
4
+ cli({
5
+ site: 'quark',
6
+ name: 'rename',
7
+ description: 'Rename a file in your Quark Drive',
8
+ domain: 'pan.quark.cn',
9
+ strategy: Strategy.COOKIE,
10
+ defaultFormat: 'json',
11
+ args: [
12
+ { name: 'fid', required: true, positional: true, help: 'File ID to rename' },
13
+ { name: 'name', required: true, help: 'New file name' },
14
+ ],
15
+ func: async (page, kwargs) => {
16
+ const fid = kwargs.fid;
17
+ const name = kwargs.name;
18
+ if (!name.trim())
19
+ throw new ArgumentError('New name cannot be empty');
20
+ await apiPost(page, `${DRIVE_API}/rename?pr=ucpro&fr=pc`, {
21
+ fid,
22
+ file_name: name,
23
+ });
24
+ return { status: 'ok', fid, new_name: name };
25
+ },
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { ArgumentError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { DRIVE_API, apiPost } from './utils.js';
4
+ cli({
5
+ site: 'quark',
6
+ name: 'rm',
7
+ description: 'Delete files from your Quark Drive',
8
+ domain: 'pan.quark.cn',
9
+ strategy: Strategy.COOKIE,
10
+ defaultFormat: 'json',
11
+ args: [
12
+ { name: 'fids', required: true, positional: true, help: 'File IDs to delete (comma-separated)' },
13
+ ],
14
+ func: async (page, kwargs) => {
15
+ const fids = kwargs.fids;
16
+ const fidList = [...new Set(fids.split(',').map(id => id.trim()).filter(Boolean))];
17
+ if (fidList.length === 0)
18
+ throw new ArgumentError('No fids provided');
19
+ await apiPost(page, `${DRIVE_API}/delete?pr=ucpro&fr=pc`, {
20
+ filelist: fidList,
21
+ });
22
+ return { status: 'ok', count: fidList.length, deleted_fids: fidList };
23
+ },
24
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { SHARE_API, extractPwdId, apiPost, getToken, pollTask, findFolder, } from './utils.js';
4
+ async function saveShare(page, pwdId, stoken, fidList, targetFid, saveAll) {
5
+ const data = await apiPost(page, `${SHARE_API}/save?pr=ucpro&fr=pc`, {
6
+ pwd_id: pwdId,
7
+ stoken,
8
+ pdir_fid: '0',
9
+ to_pdir_fid: targetFid,
10
+ fid_list: fidList,
11
+ pdir_save_all: saveAll,
12
+ scene: 'link',
13
+ });
14
+ return data.task_id;
15
+ }
16
+ cli({
17
+ site: 'quark',
18
+ name: 'save',
19
+ description: 'Save shared files to your Quark Drive',
20
+ domain: 'pan.quark.cn',
21
+ strategy: Strategy.COOKIE,
22
+ defaultFormat: 'json',
23
+ timeoutSeconds: 120,
24
+ args: [
25
+ { name: 'url', required: true, positional: true, help: 'Quark share URL or pwd_id' },
26
+ { name: 'to', default: '', help: 'Destination folder path' },
27
+ { name: 'to-fid', default: '', help: 'Destination folder ID (overrides --to)' },
28
+ { name: 'fids', default: '', help: 'File IDs to save (comma-separated, from share-tree). Omit to save all.' },
29
+ { name: 'stoken', default: '', help: 'Share token (from share-tree output, required with --fids)' },
30
+ { name: 'passcode', default: '', help: 'Share passcode (if required)' },
31
+ ],
32
+ func: async (page, kwargs) => {
33
+ const url = kwargs.url;
34
+ const to = kwargs.to;
35
+ const toFid = kwargs['to-fid'];
36
+ const fids = kwargs.fids;
37
+ const stokenArg = kwargs.stoken;
38
+ const passcode = kwargs.passcode;
39
+ if (!to && !toFid)
40
+ throw new ArgumentError('Either --to or --to-fid is required');
41
+ if (to && toFid)
42
+ throw new ArgumentError('Cannot use both --to and --to-fid');
43
+ const pwdId = extractPwdId(url);
44
+ const saveAll = !fids;
45
+ let stoken;
46
+ let fidList;
47
+ if (saveAll) {
48
+ stoken = stokenArg || await getToken(page, pwdId, passcode);
49
+ fidList = [];
50
+ }
51
+ else {
52
+ if (!stokenArg)
53
+ throw new ArgumentError('--stoken is required when using --fids');
54
+ stoken = stokenArg;
55
+ fidList = [...new Set(fids.split(',').map(id => id.trim()).filter(Boolean))];
56
+ }
57
+ const targetFid = toFid || await findFolder(page, to);
58
+ const taskId = await saveShare(page, pwdId, stoken, fidList, targetFid, saveAll);
59
+ const result = {
60
+ success: false,
61
+ task_id: taskId,
62
+ saved_to: to || toFid,
63
+ target_fid: targetFid,
64
+ ...(saveAll ? {} : { fids: fidList }),
65
+ };
66
+ if (taskId) {
67
+ const completed = await pollTask(page, taskId, (task) => {
68
+ result.save_count = task.save_as?.save_as_sum_num;
69
+ });
70
+ result.completed = completed;
71
+ result.success = completed;
72
+ if (!completed)
73
+ throw new CommandExecutionError('quark: Save task timed out');
74
+ }
75
+ else {
76
+ result.success = true;
77
+ }
78
+ return result;
79
+ },
80
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { extractPwdId, formatDate, getShareList, getToken, } from './utils.js';
3
+ async function buildTree(page, pwdId, stoken, pdirFid, depth, maxDepth) {
4
+ if (depth > maxDepth)
5
+ return [];
6
+ const files = await getShareList(page, pwdId, stoken, pdirFid, { sort: 'file_type:asc,file_name:asc' });
7
+ const nodes = [];
8
+ for (const file of files) {
9
+ const node = {
10
+ fid: file.fid,
11
+ name: file.file_name,
12
+ size: file.size,
13
+ is_dir: file.dir,
14
+ created_at: formatDate(file.created_at),
15
+ updated_at: formatDate(file.updated_at),
16
+ };
17
+ if (file.dir && depth < maxDepth) {
18
+ node.children = await buildTree(page, pwdId, stoken, file.fid, depth + 1, maxDepth);
19
+ }
20
+ nodes.push(node);
21
+ }
22
+ return nodes;
23
+ }
24
+ cli({
25
+ site: 'quark',
26
+ name: 'share-tree',
27
+ description: 'Get directory tree from Quark Drive share link as nested JSON',
28
+ domain: 'pan.quark.cn',
29
+ strategy: Strategy.COOKIE,
30
+ defaultFormat: 'json',
31
+ args: [
32
+ { name: 'url', required: true, positional: true, help: 'Quark share URL or pwd_id' },
33
+ { name: 'passcode', default: '', help: 'Share passcode (if required)' },
34
+ { name: 'depth', type: 'int', default: 10, help: 'Max directory depth' },
35
+ ],
36
+ func: async (page, kwargs) => {
37
+ const url = kwargs.url;
38
+ const passcode = kwargs.passcode || '';
39
+ const depth = kwargs.depth ?? 10;
40
+ const pwdId = extractPwdId(url);
41
+ const stoken = await getToken(page, pwdId, passcode);
42
+ const tree = await buildTree(page, pwdId, stoken, '0', 0, depth);
43
+ return { pwd_id: pwdId, stoken, tree };
44
+ },
45
+ });
@@ -0,0 +1,50 @@
1
+ import type { IPage } from '@jackwener/opencli/types';
2
+ export declare const SHARE_API = "https://drive-h.quark.cn/1/clouddrive/share/sharepage";
3
+ export declare const DRIVE_API = "https://drive-pc.quark.cn/1/clouddrive/file";
4
+ export declare const TASK_API = "https://drive-pc.quark.cn/1/clouddrive/task";
5
+ export interface ApiResponse<T = unknown> {
6
+ status: number;
7
+ code: number;
8
+ message: string;
9
+ data: T;
10
+ metadata?: {
11
+ _total?: number;
12
+ };
13
+ }
14
+ export interface ShareFile {
15
+ fid: string;
16
+ file_name: string;
17
+ size: number;
18
+ dir: boolean;
19
+ created_at: number;
20
+ updated_at: number;
21
+ }
22
+ export interface DriveFile {
23
+ fid: string;
24
+ file_name: string;
25
+ size: number;
26
+ dir: boolean;
27
+ }
28
+ export declare function extractPwdId(url: string): string;
29
+ export declare function fetchJson<T = unknown>(page: IPage, url: string, options?: {
30
+ method?: string;
31
+ body?: object;
32
+ }): Promise<ApiResponse<T>>;
33
+ export declare function apiGet<T = unknown>(page: IPage, url: string): Promise<T>;
34
+ export declare function apiPost<T = unknown>(page: IPage, url: string, body: object): Promise<T>;
35
+ export declare function getToken(page: IPage, pwdId: string, passcode?: string): Promise<string>;
36
+ export declare function getShareList(page: IPage, pwdId: string, stoken: string, pdirFid?: string, options?: {
37
+ sort?: string;
38
+ }): Promise<ShareFile[]>;
39
+ export declare function listMyDrive(page: IPage, pdirFid: string): Promise<DriveFile[]>;
40
+ export declare function findFolder(page: IPage, path: string): Promise<string>;
41
+ export declare function formatDate(ts: number): string;
42
+ export declare function formatSize(bytes: number): string;
43
+ export interface TaskStatus {
44
+ status: number;
45
+ save_as?: {
46
+ save_as_sum_num: number;
47
+ };
48
+ }
49
+ export declare function getTaskStatus(page: IPage, taskId: string): Promise<TaskStatus | null>;
50
+ export declare function pollTask(page: IPage, taskId: string, onDone?: (task: TaskStatus) => void, maxAttempts?: number, intervalMs?: number): Promise<boolean>;