@sovovs/bycli 2.1.56 → 2.1.58
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/cli-manifest.json +56 -0
- package/clis/ima/download-utils.js +32 -0
- package/clis/ima/download.js +67 -0
- package/clis/ima/native-client.js +21 -0
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -13546,6 +13546,62 @@
|
|
|
13546
13546
|
"sourceFile": "hupu/unlike.js",
|
|
13547
13547
|
"navigateBefore": false
|
|
13548
13548
|
},
|
|
13549
|
+
{
|
|
13550
|
+
"site": "ima",
|
|
13551
|
+
"name": "download",
|
|
13552
|
+
"description": "下载 ima 知识库中的原始文件",
|
|
13553
|
+
"access": "read",
|
|
13554
|
+
"domain": "ima.qq.com",
|
|
13555
|
+
"strategy": "cookie",
|
|
13556
|
+
"browser": true,
|
|
13557
|
+
"args": [
|
|
13558
|
+
{
|
|
13559
|
+
"name": "knowledgeBase",
|
|
13560
|
+
"type": "str",
|
|
13561
|
+
"required": true,
|
|
13562
|
+
"positional": true,
|
|
13563
|
+
"help": "知识库名称或 ID"
|
|
13564
|
+
},
|
|
13565
|
+
{
|
|
13566
|
+
"name": "file",
|
|
13567
|
+
"type": "str",
|
|
13568
|
+
"required": true,
|
|
13569
|
+
"positional": true,
|
|
13570
|
+
"help": "文件标题"
|
|
13571
|
+
},
|
|
13572
|
+
{
|
|
13573
|
+
"name": "output",
|
|
13574
|
+
"type": "str",
|
|
13575
|
+
"default": ".",
|
|
13576
|
+
"required": false,
|
|
13577
|
+
"help": "输出文件或目录"
|
|
13578
|
+
},
|
|
13579
|
+
{
|
|
13580
|
+
"name": "mediaId",
|
|
13581
|
+
"type": "str",
|
|
13582
|
+
"required": false,
|
|
13583
|
+
"help": "IMA mediaId,用于精确定位"
|
|
13584
|
+
},
|
|
13585
|
+
{
|
|
13586
|
+
"name": "viewerUrl",
|
|
13587
|
+
"type": "str",
|
|
13588
|
+
"required": false,
|
|
13589
|
+
"help": "已打开的 IMA 查看器 URL(含 originUrl 签名参数)"
|
|
13590
|
+
}
|
|
13591
|
+
],
|
|
13592
|
+
"columns": [
|
|
13593
|
+
"path",
|
|
13594
|
+
"title",
|
|
13595
|
+
"contentType",
|
|
13596
|
+
"size",
|
|
13597
|
+
"sha256",
|
|
13598
|
+
"pages"
|
|
13599
|
+
],
|
|
13600
|
+
"type": "js",
|
|
13601
|
+
"modulePath": "ima/download.js",
|
|
13602
|
+
"sourceFile": "ima/download.js",
|
|
13603
|
+
"navigateBefore": false
|
|
13604
|
+
},
|
|
13549
13605
|
{
|
|
13550
13606
|
"site": "ima",
|
|
13551
13607
|
"name": "knowledge",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const IMA_RESOURCE_HOSTS = new Set(['res-skb.ima.qq.com']);
|
|
6
|
+
|
|
7
|
+
export function validateImaOriginUrl(value) {
|
|
8
|
+
let url;
|
|
9
|
+
try { url = new URL(String(value)); } catch { throw new Error('IMA originUrl is not a valid URL'); }
|
|
10
|
+
if (url.protocol !== 'https:' || !IMA_RESOURCE_HOSTS.has(url.hostname)) {
|
|
11
|
+
throw new Error(`IMA resource URL is not allowed: ${url.hostname}`);
|
|
12
|
+
}
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extractOriginUrl(viewerUrl) {
|
|
17
|
+
let url;
|
|
18
|
+
try { url = new URL(String(viewerUrl)); } catch { throw new Error('viewer URL is invalid'); }
|
|
19
|
+
const origin = url.searchParams.get('originUrl');
|
|
20
|
+
if (!origin) throw new Error('viewer URL does not contain originUrl');
|
|
21
|
+
return validateImaOriginUrl(origin);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveOutputPath(output, title) {
|
|
25
|
+
const target = String(output || '.');
|
|
26
|
+
if (path.extname(target)) return target;
|
|
27
|
+
return path.join(target, path.basename(String(title || 'ima-download')));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sha256File(filePath) {
|
|
31
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { httpDownload } from '@sovovs/bycli/download';
|
|
4
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
5
|
+
import { CommandExecutionError, EmptyResultError } from '@sovovs/bycli/errors';
|
|
6
|
+
import { readImaMediaUrl, readKnowledgeBaseFromChrome } from './native-client.js';
|
|
7
|
+
import { extractOriginUrl, resolveOutputPath, sha256File } from './download-utils.js';
|
|
8
|
+
|
|
9
|
+
function countPdfPages(filePath) {
|
|
10
|
+
const bytes = fs.readFileSync(filePath);
|
|
11
|
+
if (bytes.subarray(0, 5).toString() !== '%PDF-') return null;
|
|
12
|
+
return (bytes.toString('latin1').match(/\/Type\s*\/Page\b/g) || []).length || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runDownloadCommand(page, kwargs, deps = {}) {
|
|
16
|
+
const kb = String(kwargs.knowledgeBase ?? '').trim();
|
|
17
|
+
const title = String(kwargs.file ?? '').trim();
|
|
18
|
+
if (!kb || !title) throw new CommandExecutionError('knowledge base and file title are required');
|
|
19
|
+
const reader = deps.read ?? ((query) => readKnowledgeBaseFromChrome(page, query));
|
|
20
|
+
const envelope = await reader(kb);
|
|
21
|
+
const items = (envelope?.items ?? []).filter((item) => item.title === title
|
|
22
|
+
&& (!kwargs.mediaId || item.mediaId === kwargs.mediaId));
|
|
23
|
+
if (items.length === 0) throw new EmptyResultError('ima download', `File "${title}" was not found`);
|
|
24
|
+
if (items.length > 1) throw new CommandExecutionError('Multiple files matched; specify --media-id');
|
|
25
|
+
const item = items[0];
|
|
26
|
+
let originUrl;
|
|
27
|
+
if (item.url) originUrl = item.url;
|
|
28
|
+
if (!originUrl && kwargs.viewerUrl) originUrl = extractOriginUrl(kwargs.viewerUrl);
|
|
29
|
+
if (!originUrl && page.getCurrentUrl) {
|
|
30
|
+
const currentUrl = await page.getCurrentUrl();
|
|
31
|
+
if (currentUrl?.includes('originUrl=')) originUrl = extractOriginUrl(currentUrl);
|
|
32
|
+
}
|
|
33
|
+
if (!originUrl && deps.readOriginalUrl) originUrl = await deps.readOriginalUrl(item);
|
|
34
|
+
if (!originUrl && page.fetchJson) originUrl = await readImaMediaUrl(page, { ...item, knowledgeBaseId: item.knowledgeBaseId || envelope.knowledgeBaseId });
|
|
35
|
+
if (!originUrl && process.platform === 'darwin') {
|
|
36
|
+
const { readKnowledgeBase: readKnowledgeBaseFromDesktop } = await import('./ax.js');
|
|
37
|
+
const desktop = await Promise.resolve(readKnowledgeBaseFromDesktop(kb));
|
|
38
|
+
const desktopItem = desktop?.items?.find((entry) => entry.title === title);
|
|
39
|
+
if (desktopItem?.url) originUrl = desktopItem.url;
|
|
40
|
+
}
|
|
41
|
+
if (!originUrl) throw new CommandExecutionError('IMA original file URL is unavailable; open the file in the IMA client first');
|
|
42
|
+
const outputPath = resolveOutputPath(kwargs.output || '.', item.title);
|
|
43
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
44
|
+
const download = deps.httpDownload ?? httpDownload;
|
|
45
|
+
const result = await download(originUrl, outputPath, { timeout: 120000 });
|
|
46
|
+
if (!result.success) throw new CommandExecutionError(result.error || 'IMA file download failed');
|
|
47
|
+
const stat = fs.statSync(outputPath);
|
|
48
|
+
const sha256 = await sha256File(outputPath);
|
|
49
|
+
const pages = item.contentType === 'PDF' ? countPdfPages(outputPath) : null;
|
|
50
|
+
return [{ path: outputPath, title: item.title, contentType: item.contentType, size: stat.size, sha256, pages }];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cli({
|
|
54
|
+
site: 'ima', name: 'download', access: 'read', description: '下载 ima 知识库中的原始文件',
|
|
55
|
+
domain: 'ima.qq.com', strategy: Strategy.COOKIE, browser: true, navigateBefore: false,
|
|
56
|
+
args: [
|
|
57
|
+
{ name: 'knowledgeBase', positional: true, required: true, help: '知识库名称或 ID' },
|
|
58
|
+
{ name: 'file', positional: true, required: true, help: '文件标题' },
|
|
59
|
+
{ name: 'output', flags: '--output <path>', default: '.', help: '输出文件或目录' },
|
|
60
|
+
{ name: 'mediaId', flags: '--media-id <id>', help: 'IMA mediaId,用于精确定位' },
|
|
61
|
+
{ name: 'viewerUrl', flags: '--viewer-url <url>', help: '已打开的 IMA 查看器 URL(含 originUrl 签名参数)' },
|
|
62
|
+
],
|
|
63
|
+
columns: ['path', 'title', 'contentType', 'size', 'sha256', 'pages'],
|
|
64
|
+
func: (page, kwargs) => runDownloadCommand(page, kwargs),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const __test__ = { countPdfPages, runDownloadCommand };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listKnowledgeBases, readKnowledgeBaseFromApi } from './native-api.js';
|
|
2
2
|
|
|
3
3
|
const IMA_WIKIS_URL = 'https://ima.qq.com/wikis';
|
|
4
|
+
const IMA_GET_MEDIA_URL = 'https://ima.qq.com/cgi-bin/file_manager/get_media';
|
|
4
5
|
|
|
5
6
|
function codedError(code, message) {
|
|
6
7
|
return Object.assign(new Error(message), { code });
|
|
@@ -84,3 +85,23 @@ export async function readKnowledgeBaseFromChrome(page, query, dependencies = {}
|
|
|
84
85
|
await releaseImaAuth(page, authId);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
export async function readImaMediaUrl(page, item, dependencies = {}) {
|
|
90
|
+
if (!page || typeof page.fetchJson !== 'function') {
|
|
91
|
+
throw codedError('IMA_ORIGINAL_URL_UNAVAILABLE', 'IMA browser page cannot call get_media');
|
|
92
|
+
}
|
|
93
|
+
const response = await page.fetchJson(IMA_GET_MEDIA_URL, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
body: {
|
|
96
|
+
knowledgeBaseId: String(item?.knowledgeBaseId ?? item?.knowledge_base_id ?? ''),
|
|
97
|
+
mediaId: String(item?.mediaId ?? item?.media_id ?? ''),
|
|
98
|
+
scene: dependencies.scene ?? 4,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const data = response && typeof response === 'object' ? response : {};
|
|
102
|
+
const url = data.jumpUrlInfo?.url ?? data.jump_url_info?.url;
|
|
103
|
+
if (Number(data.action) !== 1 || typeof url !== 'string' || !url) {
|
|
104
|
+
throw codedError('IMA_ORIGINAL_URL_UNAVAILABLE', data.toastText || data.toast_text || 'IMA did not return an exportable file URL');
|
|
105
|
+
}
|
|
106
|
+
return url;
|
|
107
|
+
}
|