@sovovs/bycli 2.1.59 → 2.1.61
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 +34 -0
- package/cli-manifest.json +201 -3
- package/clis/ima/ax.js +41 -10
- package/clis/ima/download.js +57 -10
- package/clis/ima/native-client.js +55 -4
- package/clis/iwhalecloud/browser-download.js +83 -0
- package/clis/iwhalecloud/client.js +97 -0
- package/clis/iwhalecloud/download.js +77 -0
- package/clis/iwhalecloud/list.js +59 -0
- package/clis/iwhalecloud/read.js +25 -0
- 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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError, TimeoutError } from '@sovovs/bycli/errors';
|
|
3
|
+
|
|
4
|
+
export const HOST = 'mail.iwhalecloud.com';
|
|
5
|
+
export const BASE = `https://${HOST}`;
|
|
6
|
+
export const metadata = { site: 'iwhalecloud', domain: HOST, access: 'read', strategy: Strategy.COOKIE,
|
|
7
|
+
browser: true, navigateBefore: false, siteSession: 'persistent' };
|
|
8
|
+
export const idArg = { name: 'emailId', positional: true, required: true, help: 'list 返回的 emailId(请加引号)' };
|
|
9
|
+
export const SORT_FIELDS = Object.freeze({ date: 'DateTimeReceived', from: 'From', to: 'DisplayTo',
|
|
10
|
+
subject: 'Subject', attachments: 'HasAttachments', importance: 'Importance', size: 'Size',
|
|
11
|
+
sent: 'DateTimeSent', created: 'DateTimeCreated' });
|
|
12
|
+
const FOLDERS = Object.freeze({ inbox: 'inbox', sent: 'sentitems', drafts: 'drafts', trash: 'deleteditems', spam: 'junkemail' });
|
|
13
|
+
export function integer(value, name, min, max) {
|
|
14
|
+
const n = Number(value);
|
|
15
|
+
if (!Number.isSafeInteger(n) || n < min || n > max) throw new ArgumentError(`${name} must be an integer between ${min} and ${max}`);
|
|
16
|
+
return n;
|
|
17
|
+
}
|
|
18
|
+
export function requiredId(value) {
|
|
19
|
+
const id = String(value ?? '').trim();
|
|
20
|
+
if (!id) throw new ArgumentError('emailId is required; use an emailId returned by list');
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
export function folderId(args) {
|
|
24
|
+
if (args['folder-id']) return { __type: 'FolderId:#Exchange', Id: String(args['folder-id']) };
|
|
25
|
+
const key = String(args.folder ?? 'inbox');
|
|
26
|
+
if (!Object.hasOwn(FOLDERS, key)) throw new ArgumentError(`Unknown folder; choose ${Object.keys(FOLDERS).join(', ')} or --folder-id`);
|
|
27
|
+
return { __type: 'DistinguishedFolderId:#Exchange', Id: FOLDERS[key] };
|
|
28
|
+
}
|
|
29
|
+
export async function connect(page) {
|
|
30
|
+
await page.goto(`${BASE}/owa/`, { waitUntil: 'load' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Keep the session token inside the browser; never include it in CLI output or errors.
|
|
34
|
+
export async function request(page, action, body) {
|
|
35
|
+
const result = await page.evaluate(async (actionName, requestBody) => {
|
|
36
|
+
if (location.origin !== 'https://mail.iwhalecloud.com' || /\/auth\//i.test(location.pathname)) return { auth: true };
|
|
37
|
+
const cookie = document.cookie.split(';').map(x => x.trim()).find(x => x.startsWith('X-OWA-CANARY='));
|
|
38
|
+
if (!cookie) return { auth: true };
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`/owa/service.svc?action=${actionName}`, {
|
|
41
|
+
method: 'POST', credentials: 'same-origin', redirect: 'error', signal: AbortSignal.timeout(30000),
|
|
42
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Action: actionName,
|
|
43
|
+
'X-OWA-CANARY': decodeURIComponent(cookie.slice('X-OWA-CANARY='.length)) },
|
|
44
|
+
body: JSON.stringify({ __type: `${actionName}JsonRequest:#Exchange`,
|
|
45
|
+
Header: { __type: 'JsonRequestHeaders:#Exchange', RequestServerVersion: 'Exchange2013' }, Body: requestBody }),
|
|
46
|
+
});
|
|
47
|
+
if ([401, 403, 440].includes(response.status)) return { auth: true };
|
|
48
|
+
if (!response.ok) return { status: response.status };
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (/^\s*</.test(text)) return { auth: true };
|
|
51
|
+
try { return { data: JSON.parse(text) }; } catch { return { malformed: true }; }
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return { timeout: error.name === 'TimeoutError' || error.name === 'AbortError', failed: true };
|
|
54
|
+
}
|
|
55
|
+
}, action, body);
|
|
56
|
+
if (result?.auth) throw new AuthRequiredError(HOST);
|
|
57
|
+
if (result?.timeout) throw new TimeoutError(`iwhalecloud ${action}`, 30);
|
|
58
|
+
if (!result?.data) throw new CommandExecutionError(`iwhalecloud ${action} request failed${result?.status ? ` (HTTP ${result.status})` : ''}`);
|
|
59
|
+
const messages = result.data.Body?.ResponseMessages?.Items;
|
|
60
|
+
if (!Array.isArray(messages) || !messages.length) throw new CommandExecutionError(`iwhalecloud ${action}: invalid response envelope`);
|
|
61
|
+
for (const message of messages) {
|
|
62
|
+
if (message.ResponseClass !== 'Success' || message.ResponseCode !== 'NoError') {
|
|
63
|
+
const code = String(message.ResponseCode ?? 'UnknownError');
|
|
64
|
+
if (['ErrorInvalidClientAccessToken', 'ErrorInvalidUserSid'].includes(code)) throw new AuthRequiredError(HOST);
|
|
65
|
+
if (code === 'ErrorItemNotFound') throw new EmptyResultError(`iwhalecloud ${action}`, 'The mail may have been moved or deleted; refresh list');
|
|
66
|
+
throw new CommandExecutionError(`iwhalecloud ${action}: ${code}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return messages[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function mailRow(item) {
|
|
73
|
+
if (!item?.ItemId?.Id) throw new CommandExecutionError('OWA returned an item without an emailId');
|
|
74
|
+
const sender = item.From?.Mailbox ?? item.Sender?.Mailbox ?? {};
|
|
75
|
+
return { emailId: item.ItemId.Id, subject: item.Subject ?? '', fromName: sender.Name ?? '', fromEmail: sender.EmailAddress ?? '',
|
|
76
|
+
toDisplay: item.DisplayTo ?? '', date: item.DateTimeReceived ?? '', times: { sentAt: item.DateTimeSent ?? '', createdAt: item.DateTimeCreated ?? '' },
|
|
77
|
+
unread: typeof item.IsRead === 'boolean' ? !item.IsRead : null, hasAttachments: item.HasAttachments ?? false,
|
|
78
|
+
importance: item.Importance ?? '', size: item.Size ?? 0,
|
|
79
|
+
url: `${BASE}/owa/#path=/mail/item/${encodeURIComponent(item.ItemId.Id)}` };
|
|
80
|
+
}
|
|
81
|
+
export const listColumns = ['emailId', 'subject', 'fromName', 'fromEmail', 'toDisplay', 'date', 'times', 'unread', 'hasAttachments', 'importance', 'size', 'url'];
|
|
82
|
+
|
|
83
|
+
export async function getMail(page, emailId, bodyType = 'text') {
|
|
84
|
+
const message = await request(page, 'GetItem', { __type: 'GetItemRequest:#Exchange', ShapeName: 'ItemPart',
|
|
85
|
+
ItemShape: { __type: 'ItemResponseShape:#Exchange', BaseShape: 'AllProperties', BodyType: bodyType === 'html' ? 'HTML' : 'Text',
|
|
86
|
+
MaximumBodySize: 0, MaximumRecipientsToReturn: 10000 }, ItemIds: [{ __type: 'ItemId:#Exchange', Id: emailId }] });
|
|
87
|
+
if (!Array.isArray(message.Items) || message.Items.length !== 1 || message.Items[0]?.ItemId?.Id !== emailId) {
|
|
88
|
+
throw new CommandExecutionError('OWA GetItem did not return the requested mail');
|
|
89
|
+
}
|
|
90
|
+
return message.Items[0];
|
|
91
|
+
}
|
|
92
|
+
export function attachmentsOf(item) {
|
|
93
|
+
if (item.Attachments != null && !Array.isArray(item.Attachments)) throw new CommandExecutionError('OWA returned invalid attachments');
|
|
94
|
+
return (item.Attachments ?? []).map((a, index) => ({ index: index + 1, attachmentId: a.AttachmentId?.Id ?? '',
|
|
95
|
+
name: a.Name ?? '', contentType: a.ContentType ?? '', size: a.Size ?? 0, inline: a.IsInline ?? false,
|
|
96
|
+
kind: a.__type?.split(':')[0] ?? 'Unknown' }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { cli } from '@sovovs/bycli/registry';
|
|
6
|
+
import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError, EmptyResultError, TimeoutError } from '@sovovs/bycli/errors';
|
|
7
|
+
import { metadata, idArg, requiredId, connect, getMail, attachmentsOf, BASE, HOST } from './client.js';
|
|
8
|
+
import { browserDownloadResponse } from './browser-download.js';
|
|
9
|
+
|
|
10
|
+
function safeName(name) {
|
|
11
|
+
const leaf = String(name).replaceAll('\\', '/').split('/').pop() ?? '';
|
|
12
|
+
const clean = leaf.replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g, '_').replace(/^[. ]+|[. ]+$/g, '');
|
|
13
|
+
// Keep UTF-8 filenames below filesystem byte limits, preserving the extension.
|
|
14
|
+
const ext = path.extname(clean).slice(0, 20);
|
|
15
|
+
let stem = clean.slice(0, clean.length - ext.length) || 'attachment';
|
|
16
|
+
while (Buffer.byteLength(stem + ext) > 220) stem = Array.from(stem).slice(0, -1).join('');
|
|
17
|
+
return stem + ext;
|
|
18
|
+
}
|
|
19
|
+
async function reserveFile(directory, name) {
|
|
20
|
+
const ext = path.extname(name); const stem = name.slice(0, name.length - ext.length);
|
|
21
|
+
for (let index = 0; index < 10000; index++) {
|
|
22
|
+
const file = path.join(directory, index ? `${stem} (${index})${ext}` : name);
|
|
23
|
+
try { return { file, handle: await fs.open(file, 'wx', 0o600) }; }
|
|
24
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
25
|
+
}
|
|
26
|
+
throw new CommandExecutionError('Too many files with the same attachment name');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runDownload(page, args, deps = {}) {
|
|
30
|
+
const id = requiredId(args.emailId);
|
|
31
|
+
const output = path.resolve(String(args.output ?? '.'));
|
|
32
|
+
await connect(page);
|
|
33
|
+
const item = await getMail(page, id);
|
|
34
|
+
const attachments = attachmentsOf(item);
|
|
35
|
+
const selector = String(args.attachment ?? 'all');
|
|
36
|
+
const selected = selector === 'all' ? attachments.filter(a => !a.inline)
|
|
37
|
+
: attachments.filter(a => a.attachmentId === selector || String(a.index) === selector);
|
|
38
|
+
if (!selected.length) throw new EmptyResultError('iwhalecloud download', 'No matching attachments; use read to inspect attachment IDs and indexes');
|
|
39
|
+
if (selected.length > 1 && selector !== 'all') throw new ArgumentError('Attachment selector is ambiguous; use the attachment ID');
|
|
40
|
+
if (selected.some(a => !a.attachmentId || a.kind !== 'FileAttachment')) {
|
|
41
|
+
throw new CommandExecutionError('Only file attachments can be downloaded; select a FileAttachment ID shown by read');
|
|
42
|
+
}
|
|
43
|
+
try { await fs.mkdir(output, { recursive: true }); }
|
|
44
|
+
catch { throw new CommandExecutionError('Cannot create attachment output directory', 'Choose a writable directory with --output'); }
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const attachment of selected) {
|
|
47
|
+
let reserved;
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = deps.fetch
|
|
51
|
+
? await deps.fetch(`${BASE}/owa/service.svc/s/GetFileAttachment?id=${encodeURIComponent(attachment.attachmentId)}`)
|
|
52
|
+
: await browserDownloadResponse(page, attachment.attachmentId);
|
|
53
|
+
if ([301, 302, 303, 307, 308, 401, 403, 440].includes(response.status)) throw new AuthRequiredError(HOST);
|
|
54
|
+
if (!response.ok || !response.body) throw new CommandExecutionError(`Attachment download failed (HTTP ${response.status})`);
|
|
55
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
56
|
+
if (contentType.includes('text/html') && !attachment.contentType.includes('text/html')) throw new AuthRequiredError(HOST);
|
|
57
|
+
reserved = await reserveFile(output, safeName(attachment.name));
|
|
58
|
+
await pipeline(Readable.fromWeb(response.body), reserved.handle.createWriteStream());
|
|
59
|
+
const stat = await fs.stat(reserved.file);
|
|
60
|
+
rows.push({ emailId: id, attachmentId: attachment.attachmentId, name: attachment.name,
|
|
61
|
+
path: reserved.file, size: stat.size, contentType: attachment.contentType });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (reserved) { await reserved.handle.close().catch(() => {}); await fs.unlink(reserved.file).catch(() => {}); }
|
|
64
|
+
if (response?.body && !response.body.locked) await response.body.cancel().catch(() => {});
|
|
65
|
+
const saved = rows.length ? `; ${rows.length} earlier attachment(s) were saved in ${output}` : '';
|
|
66
|
+
if (error instanceof CliError) { error.message += saved; throw error; }
|
|
67
|
+
if (error.name === 'TimeoutError' || error.name === 'AbortError') throw new TimeoutError(`Attachment download${saved}`, 120);
|
|
68
|
+
throw new CommandExecutionError(`Attachment download failed${saved}`, 'Check connectivity and retry; existing files will not be overwritten');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return rows;
|
|
72
|
+
}
|
|
73
|
+
cli({ ...metadata, name: 'download', description: '下载浩鲸邮件附件到本地目录,同名文件自动编号',
|
|
74
|
+
example: 'bycli iwhalecloud download "<emailId>" --attachment all --output ./mail-downloads -f yaml',
|
|
75
|
+
args: [idArg, { name: 'attachment', default: 'all', help: 'all(不含内嵌图片)、附件 ID 或 read 中的序号(从 1 开始)' },
|
|
76
|
+
{ name: 'output', default: '.', help: '本地输出目录' }],
|
|
77
|
+
columns: ['emailId', 'attachmentId', 'name', 'path', 'size', 'contentType'], func: runDownload });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cli } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { metadata, SORT_FIELDS, integer, folderId, connect, request, mailRow, listColumns } from './client.js';
|
|
4
|
+
|
|
5
|
+
export async function runList(page, args) {
|
|
6
|
+
const limit = integer(args.limit ?? 20, 'limit', 1, 10000);
|
|
7
|
+
let offset = integer(args.offset ?? 0, 'offset', 0, 2147483647);
|
|
8
|
+
const sort = String(args.sort ?? 'default');
|
|
9
|
+
const order = String(args.order ?? 'default');
|
|
10
|
+
if (sort !== 'default' && !Object.hasOwn(SORT_FIELDS, sort)) throw new ArgumentError(`Unknown sort: ${sort}`);
|
|
11
|
+
if (!['default', 'asc', 'desc'].includes(order)) throw new ArgumentError('order must be default, asc or desc');
|
|
12
|
+
if (sort === 'default' && order !== 'default') throw new ArgumentError('Specify --sort to use --order; default preserves server ordering');
|
|
13
|
+
const folder = folderId(args);
|
|
14
|
+
await connect(page);
|
|
15
|
+
const rows = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
while (rows.length < limit) {
|
|
18
|
+
// Split the validated total into server pages; do not cap the caller's total.
|
|
19
|
+
const remaining = limit - rows.length;
|
|
20
|
+
const batchSize = remaining > 100 ? 100 : remaining;
|
|
21
|
+
const body = { __type: 'FindItemRequest:#Exchange',
|
|
22
|
+
ItemShape: { __type: 'ItemResponseShape:#Exchange', BaseShape: 'AllProperties' },
|
|
23
|
+
ParentFolderIds: [folder], Traversal: 'Shallow',
|
|
24
|
+
Paging: { __type: 'IndexedPageView:#Exchange', BasePoint: 'Beginning', Offset: offset, MaxEntriesReturned: batchSize } };
|
|
25
|
+
if (sort !== 'default') {
|
|
26
|
+
const ascending = order === 'asc' || (order === 'default' && ['from', 'to', 'subject'].includes(sort));
|
|
27
|
+
body.SortOrder = [{ __type: 'SortResults:#Exchange', Order: ascending ? 'Ascending' : 'Descending',
|
|
28
|
+
Path: { __type: 'PropertyUri:#Exchange', FieldURI: SORT_FIELDS[sort] } }];
|
|
29
|
+
if (sort !== 'date') body.SortOrder.push({ __type: 'SortResults:#Exchange', Order: 'Descending',
|
|
30
|
+
Path: { __type: 'PropertyUri:#Exchange', FieldURI: 'DateTimeReceived' } });
|
|
31
|
+
}
|
|
32
|
+
const message = await request(page, 'FindItem', body);
|
|
33
|
+
const root = message.RootFolder;
|
|
34
|
+
if (!root || !Array.isArray(root.Items)) throw new CommandExecutionError('OWA FindItem returned invalid items');
|
|
35
|
+
for (const item of root.Items) {
|
|
36
|
+
const row = mailRow(item);
|
|
37
|
+
if (seen.has(row.emailId)) throw new CommandExecutionError('Mailbox changed while paging; repeat list to avoid incomplete results');
|
|
38
|
+
seen.add(row.emailId); rows.push(row);
|
|
39
|
+
if (rows.length === limit) break;
|
|
40
|
+
}
|
|
41
|
+
if (root.IncludesLastItemInRange === true || rows.length >= limit) break;
|
|
42
|
+
const next = root.IndexedPagingOffset;
|
|
43
|
+
if (!root.Items.length || !Number.isSafeInteger(next) || next <= offset) throw new CommandExecutionError('OWA pagination did not advance');
|
|
44
|
+
offset = next;
|
|
45
|
+
}
|
|
46
|
+
if (!rows.length) throw new EmptyResultError('iwhalecloud list', 'No mail at this folder/offset');
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cli({ ...metadata, name: 'list', description: '浩鲸邮箱邮件列表:服务端分页、默认排序及 9 种字段排序',
|
|
51
|
+
example: 'bycli iwhalecloud list --sort date --order desc --limit 50 -f yaml',
|
|
52
|
+
args: [
|
|
53
|
+
{ name: 'folder', default: 'inbox', choices: ['inbox', 'sent', 'drafts', 'trash', 'spam'], help: '邮件文件夹' },
|
|
54
|
+
{ name: 'folder-id', help: '自定义文件夹的 OWA FolderId,优先于 --folder' },
|
|
55
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量,1–10000;自动分页' },
|
|
56
|
+
{ name: 'offset', type: 'int', default: 0, help: '服务端起始偏移,0 表示第一封' },
|
|
57
|
+
{ name: 'sort', default: 'default', choices: ['default', ...Object.keys(SORT_FIELDS)], help: 'default 保留服务端默认顺序' },
|
|
58
|
+
{ name: 'order', default: 'default', choices: ['default', 'asc', 'desc'], help: '默认:姓名/主题升序,其余降序;显式方向须指定 --sort' },
|
|
59
|
+
], columns: listColumns, func: runList });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { metadata, idArg, requiredId, connect, getMail, mailRow, listColumns, attachmentsOf } from './client.js';
|
|
4
|
+
|
|
5
|
+
export async function runRead(page, args) {
|
|
6
|
+
const id = requiredId(args.emailId);
|
|
7
|
+
const bodyType = String(args['body-type'] ?? 'text');
|
|
8
|
+
if (!['text', 'html'].includes(bodyType)) throw new ArgumentError('body-type must be text or html');
|
|
9
|
+
await connect(page);
|
|
10
|
+
const item = await getMail(page, id, bodyType);
|
|
11
|
+
if (typeof item.Body?.Value !== 'string' || item.Body.IsTruncated === true) {
|
|
12
|
+
throw new CommandExecutionError('OWA did not return a complete mail body', 'Open the mail in Outlook to check content restrictions');
|
|
13
|
+
}
|
|
14
|
+
const recipients = entries => (entries ?? []).map(entry => {
|
|
15
|
+
const mailbox = entry.Mailbox ?? entry;
|
|
16
|
+
return { name: mailbox.Name ?? '', email: mailbox.EmailAddress ?? '' };
|
|
17
|
+
});
|
|
18
|
+
return [{ ...mailRow(item), to: recipients(item.ToRecipients), cc: recipients(item.CcRecipients), bcc: recipients(item.BccRecipients),
|
|
19
|
+
recipientCounts: item.RecipientCounts ?? {}, bodyType: item.Body?.BodyType ?? bodyType, body: item.Body?.Value ?? '',
|
|
20
|
+
attachments: attachmentsOf(item) }];
|
|
21
|
+
}
|
|
22
|
+
cli({ ...metadata, name: 'read', description: '读取浩鲸邮件完整正文、收件人和附件列表(不标记已读)',
|
|
23
|
+
example: 'bycli iwhalecloud read "<emailId>" -f yaml',
|
|
24
|
+
args: [idArg, { name: 'body-type', default: 'text', choices: ['text', 'html'], help: '正文格式' }],
|
|
25
|
+
columns: [...listColumns, 'to', 'cc', 'bcc', 'recipientCounts', 'bodyType', 'body', 'attachments'], func: runRead });
|
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.
|