@sovovs/bycli 2.1.59 → 2.1.60
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/README.zh-CN.md +21 -0
- package/cli-manifest.json +3 -3
- package/clis/ima/ax.js +41 -10
- package/clis/ima/download.js +57 -10
- package/clis/ima/native-client.js +55 -4
- package/dist/src/browser/cdp.js +8 -0
- package/dist/src/browser/page.js +12 -0
- package/extension/README.md +26 -0
- package/extension/dist/background.js +3380 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +41 -0
- package/extension/popup.html +146 -0
- package/extension/popup.js +77 -0
- package/package.json +9 -2
- package/scripts/postinstall.js +5 -2
- package/scripts/recorder.sh +186 -0
package/README.zh-CN.md
CHANGED
|
@@ -22,6 +22,27 @@ bycli <site> <command> --help # 查看命令的参数和选项
|
|
|
22
22
|
bycli <site> <command> -f yaml # 结构化输出(适合 agent)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
### Linux 下载 IMA 知识库 PDF
|
|
26
|
+
|
|
27
|
+
Linux 不需要安装 IMA 桌面客户端。安装 bycli 后,把 npm 包内的
|
|
28
|
+
`extension/` 目录加载到 Chrome/Chromium 的开发者模式,启动 daemon,并在浏览器中登录
|
|
29
|
+
`ima.qq.com`。Browser Bridge 会复用浏览器会话获取文件地址:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bycli daemon start
|
|
33
|
+
bycli ima download "知识库名称" "文件名.pdf" --output ./downloads -f json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
全局安装时扩展目录可用 `npm root -g` 定位:
|
|
37
|
+
`$(npm root -g)/@sovovs/bycli/extension`。如果浏览器会话需要跨机器提供,可在受控浏览器端
|
|
38
|
+
导出完整 `x-ima-cookie` 后设置 `BYCLI_IMA_COOKIE`,下载命令会切换到 Node 直连模式,Linux
|
|
39
|
+
主机无需安装 IMA App 或运行 Chrome:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export BYCLI_IMA_COOKIE='完整的 x-ima-cookie'
|
|
43
|
+
bycli ima download "知识库名称" "文件名.pdf" --output ./downloads -f json
|
|
44
|
+
```
|
|
45
|
+
|
|
25
46
|
示例:
|
|
26
47
|
|
|
27
48
|
```bash
|
package/cli-manifest.json
CHANGED
|
@@ -13549,11 +13549,11 @@
|
|
|
13549
13549
|
{
|
|
13550
13550
|
"site": "ima",
|
|
13551
13551
|
"name": "download",
|
|
13552
|
-
"description": "下载 ima
|
|
13552
|
+
"description": "下载 ima 知识库中的完整原始文件(Linux 无需 IMA App)",
|
|
13553
13553
|
"access": "read",
|
|
13554
13554
|
"domain": "ima.qq.com",
|
|
13555
13555
|
"strategy": "cookie",
|
|
13556
|
-
"browser":
|
|
13556
|
+
"browser": "conditional",
|
|
13557
13557
|
"args": [
|
|
13558
13558
|
{
|
|
13559
13559
|
"name": "knowledgeBase",
|
|
@@ -13586,7 +13586,7 @@
|
|
|
13586
13586
|
"name": "viewerUrl",
|
|
13587
13587
|
"type": "str",
|
|
13588
13588
|
"required": false,
|
|
13589
|
-
"help": "已打开的 IMA 查看器 URL(含 originUrl
|
|
13589
|
+
"help": "已打开的 IMA 查看器 URL(含 originUrl 签名参数;Linux 可省略)"
|
|
13590
13590
|
}
|
|
13591
13591
|
],
|
|
13592
13592
|
"columns": [
|
package/clis/ima/ax.js
CHANGED
|
@@ -163,12 +163,15 @@ func knowledgeWindow(_ app: AXUIElement) -> AXUIElement? {
|
|
|
163
163
|
return windows(app).first(where: { !pageKnowledgeID($0).isEmpty })
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
func publicURL(in root: AXUIElement) -> String? {
|
|
166
|
+
func publicURL(in root: AXUIElement, allowViewer: Bool = false) -> String? {
|
|
167
167
|
func acceptable(_ value: String?) -> String? {
|
|
168
|
-
guard let value
|
|
168
|
+
guard let value else { return nil }
|
|
169
|
+
if allowViewer && value.hasPrefix("chrome-extension://") && value.contains("originUrl=") { return value }
|
|
170
|
+
guard value.hasPrefix("http://") || value.hasPrefix("https://") else { return nil }
|
|
169
171
|
return value.contains("ima.qq.com") || value.contains("ima.copilot") ? nil : value
|
|
170
172
|
}
|
|
171
173
|
if let value = acceptable(stringAttribute(root, kAXURLAttribute as CFString)) { return value }
|
|
174
|
+
if let value = acceptable(stringAttribute(root, kAXDocumentAttribute as CFString)) { return value }
|
|
172
175
|
for element in descendants(root, limit: 4000) {
|
|
173
176
|
let elementRole = role(element)
|
|
174
177
|
let description = stringAttribute(element, kAXDescriptionAttribute as CFString) ?? ""
|
|
@@ -176,6 +179,7 @@ func publicURL(in root: AXUIElement) -> String? {
|
|
|
176
179
|
|| (elementRole == (kAXTextFieldRole as String) && description.contains("地址"))
|
|
177
180
|
guard isDocumentLocation else { continue }
|
|
178
181
|
if let value = acceptable(stringAttribute(element, kAXURLAttribute as CFString)) { return value }
|
|
182
|
+
if let value = acceptable(stringAttribute(element, kAXDocumentAttribute as CFString)) { return value }
|
|
179
183
|
if let value = acceptable(stringAttribute(element, kAXValueAttribute as CFString)) { return value }
|
|
180
184
|
}
|
|
181
185
|
return nil
|
|
@@ -197,8 +201,9 @@ func readPublicURL(
|
|
|
197
201
|
let deadline = Date().addingTimeInterval(expectsURL ? 12 : 2)
|
|
198
202
|
repeat {
|
|
199
203
|
for window in windows(app).reversed()
|
|
200
|
-
where !known.contains(windowSignature(window))
|
|
201
|
-
|
|
204
|
+
where (expectsURL || !known.contains(windowSignature(window)))
|
|
205
|
+
&& (expectsURL || pageKnowledgeID(window).isEmpty) {
|
|
206
|
+
if let url = publicURL(in: window, allowViewer: expectsURL) {
|
|
202
207
|
return (window, url, CFEqual(window, knowledgeBeforeOpen))
|
|
203
208
|
}
|
|
204
209
|
let title = label(window) ?? ""
|
|
@@ -230,10 +235,12 @@ func closeArticlePage(app: AXUIElement, _ window: AXUIElement, reusedKnowledgeWi
|
|
|
230
235
|
|
|
231
236
|
func pageKnowledgeID(_ window: AXUIElement) -> String {
|
|
232
237
|
for element in [window] + descendants(window, limit: 4000) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
for key in [kAXURLAttribute, kAXDocumentAttribute] {
|
|
239
|
+
guard let value = stringAttribute(element, key as CFString),
|
|
240
|
+
let range = value.range(of: "knowledgeBaseId=") else { continue }
|
|
241
|
+
let suffix = value[range.upperBound...]
|
|
242
|
+
return String(suffix.prefix { $0 != "&" && $0 != "#" })
|
|
243
|
+
}
|
|
237
244
|
}
|
|
238
245
|
return ""
|
|
239
246
|
}
|
|
@@ -494,7 +501,7 @@ func scrapeCurrentFolder(
|
|
|
494
501
|
throw DriverError(code: "KNOWLEDGE_WINDOW_LOST", message: "Knowledge-base window disappeared")
|
|
495
502
|
}
|
|
496
503
|
var url: String? = nil
|
|
497
|
-
let expectsURL = contentType == "公众号" || contentType == "网页"
|
|
504
|
+
let expectsURL = contentType == "公众号" || contentType == "网页" || contentType == "PDF"
|
|
498
505
|
if press(row.element), let opened = readPublicURL(
|
|
499
506
|
app: app,
|
|
500
507
|
excluding: before,
|
|
@@ -502,7 +509,7 @@ func scrapeCurrentFolder(
|
|
|
502
509
|
expectsURL: expectsURL
|
|
503
510
|
) {
|
|
504
511
|
url = opened.1
|
|
505
|
-
if !closeArticlePage(app: app, opened.0, reusedKnowledgeWindow: opened.2) {
|
|
512
|
+
if !closeArticlePage(app: app, opened.0, reusedKnowledgeWindow: opened.2 && contentType != "PDF") {
|
|
506
513
|
throw DriverError(code: "TAB_CLOSE_FAILED", message: "Could not close article tab '\(row.title)' safely")
|
|
507
514
|
}
|
|
508
515
|
}
|
|
@@ -653,4 +660,28 @@ export function readKnowledgeBase(query) {
|
|
|
653
660
|
return envelope;
|
|
654
661
|
}
|
|
655
662
|
|
|
663
|
+
const CURRENT_VIEWER_SCRIPT = String.raw`
|
|
664
|
+
import Cocoa
|
|
665
|
+
import ApplicationServices
|
|
666
|
+
func attr(_ e: AXUIElement, _ key: CFString) -> AnyObject? { var v: CFTypeRef?; guard AXUIElementCopyAttributeValue(e, key, &v) == .success else { return nil }; return v }
|
|
667
|
+
func children(_ e: AXUIElement) -> [AXUIElement] { attr(e, kAXChildrenAttribute as CFString) as? [AXUIElement] ?? [] }
|
|
668
|
+
func string(_ e: AXUIElement, _ key: CFString) -> String? { if let v = attr(e, key) as? String { return v }; if let v = attr(e, key) as? URL { return v.absoluteString }; return nil }
|
|
669
|
+
func all(_ root: AXUIElement) -> [AXUIElement] { var out = [root], q = [root], seen = Set<CFHashCode>(); while let e = q.popLast() { let h = CFHash(e); if seen.contains(h) { continue }; seen.insert(h); for c in children(e) { out.append(c); q.append(c) } }; return out }
|
|
670
|
+
let apps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.tencent.imamac")
|
|
671
|
+
var urls: [String] = []
|
|
672
|
+
for app in apps where !app.isTerminated { let root = AXUIElementCreateApplication(app.processIdentifier); let windows = attr(root, kAXWindowsAttribute as CFString) as? [AXUIElement] ?? []; for e in windows.flatMap(all) { for key in [kAXURLAttribute, kAXDocumentAttribute, kAXValueAttribute] { if let v = string(e, key as CFString), v.hasPrefix("chrome-extension://"), v.contains("originUrl=") && !urls.contains(v) { urls.append(v) } } } }
|
|
673
|
+
let data = try! JSONSerialization.data(withJSONObject: urls, options: []); print(String(data: data, encoding: .utf8)!)
|
|
674
|
+
`;
|
|
675
|
+
|
|
676
|
+
export function readCurrentViewerUrl(title = '') {
|
|
677
|
+
if (process.platform !== 'darwin') return null;
|
|
678
|
+
try {
|
|
679
|
+
const output = execFileSync('swift', ['-'], { input: CURRENT_VIEWER_SCRIPT, encoding: 'utf8', timeout: 20_000, maxBuffer: 8 * 1024 * 1024, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
680
|
+
const urls = String(output).trim().split(/\r?\n/).reverse().reduce((found, line) => {
|
|
681
|
+
try { const parsed = JSON.parse(line); return Array.isArray(parsed) ? parsed : found; } catch { return found; }
|
|
682
|
+
}, []);
|
|
683
|
+
return urls.find((url) => { try { const origin = new URL(url).searchParams.get('originUrl'); return !title || (origin && new URL(origin).searchParams.get('media_title') === title); } catch { return false; } }) || null;
|
|
684
|
+
} catch { return null; }
|
|
685
|
+
}
|
|
686
|
+
|
|
656
687
|
export const __test__ = { AX_KNOWLEDGE_SCRIPT, OPEN_KNOWLEDGE_APPLESCRIPT };
|
package/clis/ima/download.js
CHANGED
|
@@ -4,6 +4,7 @@ import { httpDownload } from '@sovovs/bycli/download';
|
|
|
4
4
|
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
5
5
|
import { CommandExecutionError, EmptyResultError } from '@sovovs/bycli/errors';
|
|
6
6
|
import { readImaMediaUrl, readKnowledgeBaseFromChrome } from './native-client.js';
|
|
7
|
+
import { readCurrentViewerUrl } from './ax.js';
|
|
7
8
|
import { extractOriginUrl, resolveOutputPath, sha256File } from './download-utils.js';
|
|
8
9
|
|
|
9
10
|
function countPdfPages(filePath) {
|
|
@@ -16,29 +17,75 @@ export async function runDownloadCommand(page, kwargs, deps = {}) {
|
|
|
16
17
|
const kb = String(kwargs.knowledgeBase ?? '').trim();
|
|
17
18
|
const title = String(kwargs.file ?? '').trim();
|
|
18
19
|
if (!kb || !title) throw new CommandExecutionError('knowledge base and file title are required');
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const currentViewer = deps.readCurrentViewerUrl?.(title)
|
|
21
|
+
?? (process.platform === 'darwin' ? readCurrentViewerUrl(title) : null);
|
|
22
|
+
const effectivePage = page ?? (process.env.BYCLI_IMA_COOKIE ? {
|
|
23
|
+
fetchJson: async (url, options = {}) => {
|
|
24
|
+
const response = await fetch(url, { method: options.method ?? 'GET', headers: options.headers, body: options.body === undefined ? undefined : JSON.stringify(options.body) });
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
try { return JSON.parse(text); } catch { throw new Error(`IMA HTTP ${response.status}: invalid JSON response`); }
|
|
27
|
+
},
|
|
28
|
+
} : page);
|
|
29
|
+
const reader = deps.read ?? ((query) => readKnowledgeBaseFromChrome(effectivePage, query));
|
|
30
|
+
let envelope;
|
|
31
|
+
if (currentViewer) {
|
|
32
|
+
const origin = extractOriginUrl(currentViewer);
|
|
33
|
+
const parsed = new URL(origin);
|
|
34
|
+
envelope = {
|
|
35
|
+
knowledgeBase: kb,
|
|
36
|
+
items: [{ title, contentType: 'PDF', mediaId: parsed.searchParams.get('media_id') || '', url: currentViewer }],
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
try {
|
|
40
|
+
envelope = await reader(kb);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (process.platform !== 'darwin') throw error;
|
|
43
|
+
const { readKnowledgeBase: readKnowledgeBaseFromDesktop } = await import('./ax.js');
|
|
44
|
+
envelope = await Promise.resolve(readKnowledgeBaseFromDesktop(kb));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
const items = (envelope?.items ?? []).filter((item) => item.title === title
|
|
22
48
|
&& (!kwargs.mediaId || item.mediaId === kwargs.mediaId));
|
|
23
49
|
if (items.length === 0) throw new EmptyResultError('ima download', `File "${title}" was not found`);
|
|
24
50
|
if (items.length > 1) throw new CommandExecutionError('Multiple files matched; specify --media-id');
|
|
25
51
|
const item = items[0];
|
|
26
52
|
let originUrl;
|
|
27
|
-
if (
|
|
53
|
+
if (currentViewer) originUrl = extractOriginUrl(currentViewer);
|
|
54
|
+
if (item.url) {
|
|
55
|
+
originUrl = String(item.url).startsWith('chrome-extension://')
|
|
56
|
+
? extractOriginUrl(item.url)
|
|
57
|
+
: item.url;
|
|
58
|
+
}
|
|
28
59
|
if (!originUrl && kwargs.viewerUrl) originUrl = extractOriginUrl(kwargs.viewerUrl);
|
|
29
|
-
if (!originUrl && page
|
|
60
|
+
if (!originUrl && page?.getCurrentUrl) {
|
|
30
61
|
const currentUrl = await page.getCurrentUrl();
|
|
31
62
|
if (currentUrl?.includes('originUrl=')) originUrl = extractOriginUrl(currentUrl);
|
|
32
63
|
}
|
|
33
|
-
|
|
34
|
-
if (!originUrl &&
|
|
64
|
+
let mediaError;
|
|
65
|
+
if (!originUrl && deps.readOriginalUrl) {
|
|
66
|
+
try {
|
|
67
|
+
originUrl = await deps.readOriginalUrl(item);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
mediaError = error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!originUrl && (effectivePage?.requestImaMedia || effectivePage?.fetchJson)) {
|
|
73
|
+
try {
|
|
74
|
+
originUrl = await readImaMediaUrl(effectivePage, { ...item, knowledgeBaseId: item.knowledgeBaseId || envelope.knowledgeBaseId });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
mediaError = error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
35
79
|
if (!originUrl && process.platform === 'darwin') {
|
|
36
80
|
const { readKnowledgeBase: readKnowledgeBaseFromDesktop } = await import('./ax.js');
|
|
37
81
|
const desktop = await Promise.resolve(readKnowledgeBaseFromDesktop(kb));
|
|
38
82
|
const desktopItem = desktop?.items?.find((entry) => entry.title === title);
|
|
39
83
|
if (desktopItem?.url) originUrl = desktopItem.url;
|
|
40
84
|
}
|
|
41
|
-
if (!originUrl)
|
|
85
|
+
if (!originUrl) {
|
|
86
|
+
const message = mediaError instanceof Error ? mediaError.message : 'IMA original file URL is unavailable; open the file in the IMA client first';
|
|
87
|
+
throw new CommandExecutionError(message);
|
|
88
|
+
}
|
|
42
89
|
const outputPath = resolveOutputPath(kwargs.output || '.', item.title);
|
|
43
90
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
44
91
|
const download = deps.httpDownload ?? httpDownload;
|
|
@@ -51,14 +98,14 @@ export async function runDownloadCommand(page, kwargs, deps = {}) {
|
|
|
51
98
|
}
|
|
52
99
|
|
|
53
100
|
cli({
|
|
54
|
-
site: 'ima', name: 'download', access: 'read', description: '下载 ima
|
|
55
|
-
domain: 'ima.qq.com', strategy: Strategy.COOKIE, browser:
|
|
101
|
+
site: 'ima', name: 'download', access: 'read', description: '下载 ima 知识库中的完整原始文件(Linux 无需 IMA App)',
|
|
102
|
+
domain: 'ima.qq.com', strategy: Strategy.COOKIE, browser: () => !process.env.BYCLI_IMA_COOKIE, navigateBefore: false,
|
|
56
103
|
args: [
|
|
57
104
|
{ name: 'knowledgeBase', positional: true, required: true, help: '知识库名称或 ID' },
|
|
58
105
|
{ name: 'file', positional: true, required: true, help: '文件标题' },
|
|
59
106
|
{ name: 'output', flags: '--output <path>', default: '.', help: '输出文件或目录' },
|
|
60
107
|
{ name: 'mediaId', flags: '--media-id <id>', help: 'IMA mediaId,用于精确定位' },
|
|
61
|
-
{ name: 'viewerUrl', flags: '--viewer-url <url>', help: '已打开的 IMA 查看器 URL(含 originUrl
|
|
108
|
+
{ name: 'viewerUrl', flags: '--viewer-url <url>', help: '已打开的 IMA 查看器 URL(含 originUrl 签名参数;Linux 可省略)' },
|
|
62
109
|
],
|
|
63
110
|
columns: ['path', 'title', 'contentType', 'size', 'sha256', 'pages'],
|
|
64
111
|
func: (page, kwargs) => runDownloadCommand(page, kwargs),
|
|
@@ -2,6 +2,30 @@ import { listKnowledgeBases, readKnowledgeBaseFromApi } from './native-api.js';
|
|
|
2
2
|
|
|
3
3
|
const IMA_WIKIS_URL = 'https://ima.qq.com/wikis';
|
|
4
4
|
const IMA_GET_MEDIA_URL = 'https://ima.qq.com/cgi-bin/file_manager/get_media';
|
|
5
|
+
const IMA_READER_URL = 'https://ima.qq.com/cgi-bin/knowledge_tab_reader';
|
|
6
|
+
|
|
7
|
+
function imaBknFromCookie(cookie) {
|
|
8
|
+
const token = String(cookie).split(';').map((part) => part.trim())
|
|
9
|
+
.find((part) => part.startsWith('IMA-TOKEN='))?.slice('IMA-TOKEN='.length);
|
|
10
|
+
if (!token) return '';
|
|
11
|
+
let hash = 5381;
|
|
12
|
+
for (const character of token) hash += (hash << 5) + character.charCodeAt(0);
|
|
13
|
+
return String(hash & 0x7fffffff);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function directImaRequest(page, dependencies = {}) {
|
|
17
|
+
const imaCookie = dependencies.imaCookie ?? process.env.BYCLI_IMA_COOKIE;
|
|
18
|
+
if (!imaCookie || !page || typeof page.fetchJson !== 'function') return null;
|
|
19
|
+
const headers = {
|
|
20
|
+
'x-ima-cookie': String(imaCookie),
|
|
21
|
+
'x-ima-bkn': dependencies.imaBkn ?? imaBknFromCookie(imaCookie),
|
|
22
|
+
from_browser_ima: '1',
|
|
23
|
+
extension_version: String(dependencies.extensionVersion ?? process.env.BYCLI_IMA_EXTENSION_VERSION ?? '2.1.23'),
|
|
24
|
+
};
|
|
25
|
+
return (path, body) => page.fetchJson(`${IMA_READER_URL}${path}`, {
|
|
26
|
+
method: 'POST', headers, body,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
function codedError(code, message) {
|
|
7
31
|
return Object.assign(new Error(message), { code });
|
|
@@ -66,6 +90,8 @@ async function releaseImaAuth(page, authId) {
|
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
export async function readKnowledgeBasesFromChrome(page, dependencies = {}) {
|
|
93
|
+
const directRequest = directImaRequest(page, dependencies);
|
|
94
|
+
if (directRequest) return listKnowledgeBases(directRequest);
|
|
69
95
|
const authId = await acquireImaChromeAuth(page, undefined, dependencies);
|
|
70
96
|
try {
|
|
71
97
|
return await listKnowledgeBases((path, body) => page.requestImaReader(authId, path, body));
|
|
@@ -75,6 +101,8 @@ export async function readKnowledgeBasesFromChrome(page, dependencies = {}) {
|
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
export async function readKnowledgeBaseFromChrome(page, query, dependencies = {}) {
|
|
104
|
+
const directRequest = directImaRequest(page, dependencies);
|
|
105
|
+
if (directRequest) return readKnowledgeBaseFromApi(query, directRequest);
|
|
78
106
|
const authId = await acquireImaChromeAuth(page, query, dependencies);
|
|
79
107
|
try {
|
|
80
108
|
return await readKnowledgeBaseFromApi(
|
|
@@ -87,15 +115,38 @@ export async function readKnowledgeBaseFromChrome(page, query, dependencies = {}
|
|
|
87
115
|
}
|
|
88
116
|
|
|
89
117
|
export async function readImaMediaUrl(page, item, dependencies = {}) {
|
|
118
|
+
const imaCookie = dependencies.imaCookie ?? process.env.BYCLI_IMA_COOKIE;
|
|
119
|
+
if (imaCookie && page && typeof page.fetchJson === 'function') {
|
|
120
|
+
const response = await page.fetchJson(IMA_GET_MEDIA_URL, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'x-ima-cookie': String(imaCookie),
|
|
124
|
+
'x-ima-bkn': dependencies.imaBkn ?? imaBknFromCookie(imaCookie),
|
|
125
|
+
from_browser_ima: '1',
|
|
126
|
+
extension_version: String(dependencies.extensionVersion ?? process.env.BYCLI_IMA_EXTENSION_VERSION ?? '2.1.23'),
|
|
127
|
+
},
|
|
128
|
+
body: {
|
|
129
|
+
knowledge_base_id: String(item?.knowledgeBaseId ?? ''),
|
|
130
|
+
media_id: String(item?.mediaId ?? ''),
|
|
131
|
+
scene: dependencies.scene ?? 4,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const data = response && typeof response === 'object' ? response : {};
|
|
135
|
+
const url = data.jumpUrlInfo?.url ?? data.jump_url_info?.url;
|
|
136
|
+
if (Number(data.action) !== 1 || typeof url !== 'string' || !url) {
|
|
137
|
+
throw codedError('IMA_ORIGINAL_URL_UNAVAILABLE', data.toastText || data.toast_text || 'IMA did not return an exportable file URL');
|
|
138
|
+
}
|
|
139
|
+
return url;
|
|
140
|
+
}
|
|
90
141
|
if (typeof page.requestImaMedia === 'function' && typeof page.startImaAuthCapture === 'function') {
|
|
91
142
|
const authId = await acquireImaChromeAuth(page, item?.knowledgeBaseId, dependencies);
|
|
92
143
|
try {
|
|
93
144
|
const response = await page.requestImaMedia(authId, {
|
|
94
|
-
|
|
95
|
-
|
|
145
|
+
knowledge_base_id: String(item?.knowledgeBaseId ?? ''),
|
|
146
|
+
media_id: String(item?.mediaId ?? ''),
|
|
96
147
|
scene: dependencies.scene ?? 4,
|
|
97
|
-
...(item?.shareId ? {
|
|
98
|
-
...(item?.sourceKnowledgeBaseId ? {
|
|
148
|
+
...(item?.shareId ? { share_id: String(item.shareId) } : {}),
|
|
149
|
+
...(item?.sourceKnowledgeBaseId ? { source_knowledge_base_id: String(item.sourceKnowledgeBaseId) } : {}),
|
|
99
150
|
});
|
|
100
151
|
const data = response && typeof response === 'object' ? response : {};
|
|
101
152
|
const url = data.jumpUrlInfo?.url ?? data.jump_url_info?.url;
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -232,6 +232,14 @@ class CDPPage extends BasePage {
|
|
|
232
232
|
const maxMs = options?.settleMs ?? 1000;
|
|
233
233
|
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
234
234
|
}
|
|
235
|
+
try {
|
|
236
|
+
const current = await this.evaluate('window.location.href');
|
|
237
|
+
if (typeof current === 'string' && current)
|
|
238
|
+
this._lastUrl = current;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Retain the requested URL when the document is still unavailable.
|
|
242
|
+
}
|
|
235
243
|
}
|
|
236
244
|
finally {
|
|
237
245
|
if (guard)
|
package/dist/src/browser/page.js
CHANGED
|
@@ -108,6 +108,16 @@ export class Page extends BasePage {
|
|
|
108
108
|
this._page = result.page;
|
|
109
109
|
}
|
|
110
110
|
this._lastUrl = url;
|
|
111
|
+
const refreshUrl = async () => {
|
|
112
|
+
try {
|
|
113
|
+
const current = await this.evaluate('window.location.href');
|
|
114
|
+
if (typeof current === 'string' && current)
|
|
115
|
+
this._lastUrl = current;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Navigation may still be settling; retain the requested URL as fallback.
|
|
119
|
+
}
|
|
120
|
+
};
|
|
111
121
|
// Inject stealth + settle in a single round-trip instead of two sequential exec calls.
|
|
112
122
|
// Some security-sensitive editors reject runtime environment patches, so callers can
|
|
113
123
|
// retain the normal DOM-settle behavior while opting out of stealth injection.
|
|
@@ -139,6 +149,7 @@ export class Page extends BasePage {
|
|
|
139
149
|
throw retryErr;
|
|
140
150
|
}
|
|
141
151
|
}
|
|
152
|
+
await refreshUrl();
|
|
142
153
|
}
|
|
143
154
|
else if (useStealth) {
|
|
144
155
|
// Even with waitUntil='none', still inject stealth (best-effort)
|
|
@@ -151,6 +162,7 @@ export class Page extends BasePage {
|
|
|
151
162
|
catch {
|
|
152
163
|
// Non-fatal: stealth is best-effort
|
|
153
164
|
}
|
|
165
|
+
await refreshUrl();
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
/** Get the active page identity (targetId) */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# byCLI Browser Bridge Extension
|
|
2
|
+
|
|
3
|
+
The extension connects Chrome tabs to the local byCLI daemon. It uses Chrome
|
|
4
|
+
extension APIs only as a transport and browser-control layer for explicit CLI
|
|
5
|
+
commands.
|
|
6
|
+
|
|
7
|
+
## Permission Notes
|
|
8
|
+
|
|
9
|
+
- `debugger`: sends CDP commands to byCLI-controlled or bound tabs.
|
|
10
|
+
- `tabs` / `tabGroups`: manages the dedicated byCLI automation container and
|
|
11
|
+
reports selected tab metadata back to the CLI.
|
|
12
|
+
- `cookies`: reads cookies for browser-backed adapters that need authenticated
|
|
13
|
+
fetches.
|
|
14
|
+
- `downloads`: surfaces download lifecycle to `bycli browser wait download`.
|
|
15
|
+
The extension observes started / in-progress / completed / failed downloads so
|
|
16
|
+
the CLI can wait for a file triggered by an automation command. byCLI
|
|
17
|
+
filters by the command's filename/URL pattern and timeout, and does not modify,
|
|
18
|
+
redirect, or persist browser download history.
|
|
19
|
+
|
|
20
|
+
Suggested Chrome Web Store justification for `downloads`:
|
|
21
|
+
|
|
22
|
+
> This extension uses `chrome.downloads` to surface download lifecycle
|
|
23
|
+
> (started / in-progress / completed / failed) to the byCLI command-line tool,
|
|
24
|
+
> so agents can wait for downloads triggered during an automation workflow. The
|
|
25
|
+
> command filters by a user-provided filename or URL pattern and timeout. We do
|
|
26
|
+
> not modify, redirect, or persist user download history.
|