vcorp-chrome 1.8.1 → 2.2.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/README.md +20 -1
- package/docs//345/215/217/350/256/256.md +53 -0
- package/docs//345/257/271/346/257/224.md +55 -53
- package/docs//346/241/214/351/235/242/345/256/214/345/205/250/346/250/241/345/274/217-/344/272/244/346/216/245.md +128 -0
- package/docs//350/203/275/345/212/233/345/214/205/344/270/216eda-/344/272/244/346/216/245.md +288 -0
- package/extension/background.js +20 -2
- package/extension/cloud-policy.js +23 -0
- package/extension/cloud.js +315 -6
- package/extension/desktop-policy.js +406 -0
- package/extension/desktop-render.js +81 -0
- package/extension/device.js +144 -0
- package/extension/manifest.json +41 -38
- package/extension/panel.html +114 -45
- package/extension/panel.js +469 -427
- package/native-host/full.js +217 -0
- package/native-host/host.js +155 -0
- package/package.json +53 -49
- package/src/bridge.js +31 -2
- package/src/cli.js +50 -1
- package/src/lib/desktop.js +171 -0
- package/src/lib/native-install.js +139 -0
- package/src/lib/paths.js +35 -0
- package/src/lib/rpc.js +7 -2
- package/src/mcp-server.js +135 -10
package/extension/cloud.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
import * as policy from './cloud-policy.js';
|
|
20
20
|
import * as folders from './folders.js';
|
|
21
|
+
import { checkDesktop, checkObserved, recordOf, filterWindows, appsToAsk, APPS_MAX } from './desktop-policy.js';
|
|
22
|
+
import { renderEnvelope } from './desktop-render.js';
|
|
23
|
+
import { routeHere, stampFrom, capOf, cleanDevice, describeDevices } from './device.js';
|
|
21
24
|
|
|
22
25
|
const CFG = 'cloud'; // storage.local:配对信息和用户的选择
|
|
23
26
|
const TASKS = 'cloudTasks'; // storage.local:任务(批准状态、自己开的标签页)
|
|
@@ -240,10 +243,14 @@ export async function connect() {
|
|
|
240
243
|
try {
|
|
241
244
|
const sig = await sign(helloText(cfg.origin, cfg.deviceId, String(m.nonce || '')));
|
|
242
245
|
const mounted = await folders.names().catch(() => []);
|
|
246
|
+
// 名下的设备(目前只有同机的桌面)。签名只盖浏览器自己:桌面经这条已认证的连接中继,
|
|
247
|
+
// 服务器能见到的桌面只有这个浏览器报上来的
|
|
248
|
+
const child = await desktopChild();
|
|
243
249
|
sock.send(JSON.stringify({
|
|
244
250
|
type: 'hello', v: 1, deviceId: cfg.deviceId, sig,
|
|
245
251
|
version: chrome.runtime.getManifest().version, sites: policy.cleanAllowList(cfg.sites), auto: !!cfg.auto, autoConfirm: !!cfg.autoConfirm,
|
|
246
252
|
folders: mounted,
|
|
253
|
+
devices: child ? [{ ...child, parent: cfg.deviceId }] : [],
|
|
247
254
|
}));
|
|
248
255
|
} catch { try { sock.close(); } catch { /* 已经废了 */ } }
|
|
249
256
|
return;
|
|
@@ -348,7 +355,7 @@ async function taskFor(job, cfg, reason = '', { allowEmpty = false } = {}) {
|
|
|
348
355
|
await log({ ev: 'approved', job: jobId, msg: '自动放行' });
|
|
349
356
|
await pushLive();
|
|
350
357
|
}
|
|
351
|
-
if (t.state === 'nosites') throw err('NO_SITES', '先用 sites 申请要访问的网站(如 sites {"add":["zhipin.com"],"reason":"找兼职"}),或用 folders
|
|
358
|
+
if (t.state === 'nosites') throw err('NO_SITES', '先用 sites 申请要访问的网站(如 sites {"add":["zhipin.com"],"reason":"找兼职"}),或用 folders 申请本地文件夹、apps 申请桌面应用。用户批准后才能继续。');
|
|
352
359
|
if (t.state === 'denied') throw err('CLOUD_DENIED', '用户拒绝了这个任务。不要重试。');
|
|
353
360
|
if (t.state === 'approved') return t;
|
|
354
361
|
|
|
@@ -653,12 +660,297 @@ const updateTask = (jobId, fn) => serial(async () => {
|
|
|
653
660
|
return tasks[jobId];
|
|
654
661
|
});
|
|
655
662
|
|
|
663
|
+
// ---------- 桌面 ----------
|
|
664
|
+
//
|
|
665
|
+
// 同机的桌面是这个浏览器名下的子设备:命令经原生消息宿主 ai.vcorp.desktop 交给 agent-desktop。
|
|
666
|
+
// 规矩全在 desktop-policy.js(云端那一列),宿主那边再审一遍。快照户口记在任务上,任务结束一起删。
|
|
667
|
+
|
|
668
|
+
const HOST = 'ai.vcorp.desktop';
|
|
669
|
+
const DESK = 'cloudDesktop'; // storage.local:启用桌面能力时宿主报的设备
|
|
670
|
+
const DESK_PENDING = 'cloudDesktopPending'; // 为补上 nativeMessaging 绑定而重载,重载后接着启用
|
|
671
|
+
const DESK_SNAPS = 4; // 每个任务记几份快照的户口。户口按 ref 存名字,大应用一份上百 KB
|
|
672
|
+
|
|
673
|
+
const deskInfo = async () => (await chrome.storage.local.get(DESK))[DESK] || null;
|
|
674
|
+
|
|
675
|
+
// 宿主回包超过 1MB 时先回 { paged },这里一段一段取回再拼(见 native-host/full.js)
|
|
676
|
+
async function hostCall(msg) {
|
|
677
|
+
const r = await hostCallOnce(msg);
|
|
678
|
+
if (!r.paged) return r;
|
|
679
|
+
const { id, size } = r.paged;
|
|
680
|
+
const parts = [];
|
|
681
|
+
let offset = 0;
|
|
682
|
+
for (let i = 0; i < 200; i++) {
|
|
683
|
+
const pg = await hostCallOnce({ op: 'page', id, offset });
|
|
684
|
+
if (!pg.ok || !pg.page) return pg;
|
|
685
|
+
const bin = atob(pg.page.data);
|
|
686
|
+
const bytes = new Uint8Array(bin.length);
|
|
687
|
+
for (let j = 0; j < bin.length; j++) bytes[j] = bin.charCodeAt(j);
|
|
688
|
+
parts.push(bytes);
|
|
689
|
+
offset += bytes.length;
|
|
690
|
+
if (pg.page.done || offset >= size) break;
|
|
691
|
+
}
|
|
692
|
+
const all = new Uint8Array(offset);
|
|
693
|
+
let at = 0;
|
|
694
|
+
for (const b of parts) { all.set(b, at); at += b.length; }
|
|
695
|
+
return JSON.parse(new TextDecoder().decode(all));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function hostCallOnce(msg) {
|
|
699
|
+
if (!chrome.runtime.sendNativeMessage) {
|
|
700
|
+
return Promise.reject(err('NO_DEVICE', '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。'));
|
|
701
|
+
}
|
|
702
|
+
return new Promise((resolve, reject) => {
|
|
703
|
+
chrome.runtime.sendNativeMessage(HOST, msg, (r) => {
|
|
704
|
+
const e = chrome.runtime.lastError;
|
|
705
|
+
if (e) reject(err('DEVICE_OFFLINE', `桌面宿主没有应答(${e.message})。请用户在终端运行 npx vcorp-chrome desktop install。`));
|
|
706
|
+
else if (!r || typeof r !== 'object') reject(err('DEVICE_OFFLINE', '桌面宿主的回包不对'));
|
|
707
|
+
else resolve(r);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 名下的桌面设备:启用过、权限还在才算
|
|
713
|
+
async function desktopChild() {
|
|
714
|
+
const info = await deskInfo();
|
|
715
|
+
if (!info?.device || !chrome.runtime.sendNativeMessage) return null;
|
|
716
|
+
return info.device;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function enableDesktop() {
|
|
720
|
+
// 运行时才授予的 nativeMessaging,Chrome 不给已经在跑的 service worker 补上绑定,重载后才有
|
|
721
|
+
if (!chrome.runtime.sendNativeMessage) {
|
|
722
|
+
if (!(await chrome.permissions.contains({ permissions: ['nativeMessaging'] }))) {
|
|
723
|
+
throw err('NO_DEVICE', '没有 nativeMessaging 权限,桌面能力保持关闭');
|
|
724
|
+
}
|
|
725
|
+
await chrome.storage.local.set({ [DESK_PENDING]: Date.now() });
|
|
726
|
+
await log({ ev: 'desktop_reload' });
|
|
727
|
+
chrome.runtime.reload();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const r = await hostCall({ op: 'hello' });
|
|
731
|
+
const device = cleanDevice(r.device);
|
|
732
|
+
if (!device || device.kind !== 'desktop') throw err('DEVICE_OFFLINE', '桌面宿主报上来的设备不对');
|
|
733
|
+
await chrome.storage.local.set({ [DESK]: { device, ready: !!r.ready, at: Date.now() } });
|
|
734
|
+
await log({ ev: 'desktop_on', msg: device.id });
|
|
735
|
+
// 重连一次,服务器在 hello 里看到这台设备
|
|
736
|
+
disconnect(); retryDelay = 0; void connect();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function disableDesktop() {
|
|
740
|
+
await chrome.storage.local.remove(DESK);
|
|
741
|
+
await log({ ev: 'desktop_off' });
|
|
742
|
+
disconnect(); retryDelay = 0; void connect();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function devicesCmd(cfg) {
|
|
746
|
+
const child = await desktopChild();
|
|
747
|
+
const list = [
|
|
748
|
+
{ id: cfg.deviceId, kind: 'browser', name: deviceName(), online: true },
|
|
749
|
+
...(child ? [{ ...child, parent: cfg.deviceId }] : []),
|
|
750
|
+
].map(cleanDevice).filter(Boolean);
|
|
751
|
+
return { text: describeDevices(list), devices: list };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function notifyApps(t, cfg) {
|
|
755
|
+
chrome.notifications?.create(`hc-cloud-apps:${t.jobId}`, {
|
|
756
|
+
type: 'basic', iconUrl: 'icons/icon128.png', priority: 2, requireInteraction: true,
|
|
757
|
+
title: `${policy.hostOf(cfg.origin)} 的任务想操作你电脑上的应用`,
|
|
758
|
+
message: `${t.title ? `「${t.title}」` : '任务'} → ${(t.appAsk || []).join('、')}${t.appAskWhy ? `\n用途:${t.appAskWhy}` : ''}`,
|
|
759
|
+
buttons: [{ title: '允许' }, { title: '拒绝' }],
|
|
760
|
+
}, () => void chrome.runtime.lastError);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 和文件夹一样:自动允许管不到应用,每个任务逐个点头;批准只记在这个任务上
|
|
764
|
+
async function requestApps(task, p, cfg) {
|
|
765
|
+
if (!(await desktopChild())) throw err('NO_DEVICE', '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。');
|
|
766
|
+
const split = appsToAsk(p.add, { apps: task.apps || [], refused: task.appRefused || [] });
|
|
767
|
+
if (split.ask.length) {
|
|
768
|
+
const t = await updateTask(task.jobId, (x) => {
|
|
769
|
+
x.appAsk = split.ask;
|
|
770
|
+
x.appAskWhy = p.reason;
|
|
771
|
+
});
|
|
772
|
+
if (!t) throw err('CLOUD_DENIED', '这个任务已经结束');
|
|
773
|
+
notifyApps(t, cfg);
|
|
774
|
+
const allow = await waitFor(`apps:${task.jobId}`);
|
|
775
|
+
await updateTask(task.jobId, (x) => {
|
|
776
|
+
if (!Array.isArray(x.apps)) x.apps = [];
|
|
777
|
+
if (!Array.isArray(x.appRefused)) x.appRefused = [];
|
|
778
|
+
if (allow) for (const a of split.ask) if (!x.apps.includes(a) && x.apps.length < APPS_MAX) x.apps.push(a);
|
|
779
|
+
else if (allow === false) for (const a of split.ask) if (!x.appRefused.includes(a)) x.appRefused.push(a);
|
|
780
|
+
x.appAsk = [];
|
|
781
|
+
x.appAskWhy = '';
|
|
782
|
+
});
|
|
783
|
+
chrome.notifications?.clear(`hc-cloud-apps:${task.jobId}`, () => void chrome.runtime.lastError);
|
|
784
|
+
await log({ ev: allow ? 'apps_on' : allow === false ? 'apps_off' : 'apps_timeout', job: task.jobId, msg: split.ask.join(',') });
|
|
785
|
+
}
|
|
786
|
+
const now = (await getTasks())[task.jobId] || task;
|
|
787
|
+
const granted = now.apps || [];
|
|
788
|
+
const refused = now.appRefused || [];
|
|
789
|
+
const lines = [`本任务可以操作:${granted.join('、') || '(无)'}`];
|
|
790
|
+
const no = p.add.filter((a) => refused.includes(a));
|
|
791
|
+
const later = split.ask.filter((a) => !granted.includes(a) && !refused.includes(a));
|
|
792
|
+
if (no.length) lines.push(`用户不同意:${no.join('、')}。不要再申请。`);
|
|
793
|
+
if (later.length) lines.push(`用户 5 分钟内没有回应:${later.join('、')}。可以稍后再申请一次。`);
|
|
794
|
+
if (p.overflow.length) lines.push(`超出一个任务最多 ${APPS_MAX} 个应用的上限,这次没有询问:${p.overflow.join('、')}`);
|
|
795
|
+
if (p.bad.length) lines.push(`不能申请(浏览器、密码管理器、终端这类一律不碰,或名字不合法):${p.bad.join('、')}`);
|
|
796
|
+
return { text: lines.join('\n'), apps: granted, audit: `apps ${p.add.join(',') || '(list)'}` };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function decideApps(jobId, allow) {
|
|
800
|
+
chrome.notifications?.clear(`hc-cloud-apps:${jobId}`, () => void chrome.runtime.lastError);
|
|
801
|
+
for (const fn of waiters.get(`apps:${jobId}`) || []) fn(!!allow);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// 发送、删除一类的桌面动作:和页面上的对外动作同一套免确认开关;否则弹通知等用户点头。
|
|
805
|
+
// 桌面没有标签页可以挂浮条,所以走通知和侧栏卡片。
|
|
806
|
+
async function confirmDesk(chk, task, cfg) {
|
|
807
|
+
const jobId = task.jobId;
|
|
808
|
+
const what = `${chk.cmd}「${String(chk.target?.name || '').slice(0, 40)}」(${chk.app})`;
|
|
809
|
+
if (cfg.autoConfirm || task.confirmAll) {
|
|
810
|
+
await log({ ev: 'ask_auto', job: jobId, msg: what });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
await updateTask(jobId, (x) => { x.deskAsk = what; });
|
|
814
|
+
chrome.notifications?.create(`hc-cloud-desk:${jobId}`, {
|
|
815
|
+
type: 'basic', iconUrl: 'icons/icon128.png', priority: 2, requireInteraction: true,
|
|
816
|
+
title: `${policy.hostOf(cfg.origin)} 的任务要在你的电脑上做一件对外的事`,
|
|
817
|
+
message: what,
|
|
818
|
+
buttons: [{ title: '同意' }, { title: '不同意' }],
|
|
819
|
+
}, () => void chrome.runtime.lastError);
|
|
820
|
+
const allow = await waitFor(`desk:${jobId}`);
|
|
821
|
+
await updateTask(jobId, (x) => { x.deskAsk = ''; });
|
|
822
|
+
chrome.notifications?.clear(`hc-cloud-desk:${jobId}`, () => void chrome.runtime.lastError);
|
|
823
|
+
await log({ ev: allow ? 'desk_confirm' : 'desk_declined', job: jobId, msg: what });
|
|
824
|
+
if (!allow) {
|
|
825
|
+
throw err('CONFIRM_DECLINED', allow === null
|
|
826
|
+
? `用户 5 分钟内没有确认 ${what},没有执行。在报告里写明,不要重试。`
|
|
827
|
+
: `用户没有同意 ${what},没有执行。这是明确的「别做」,不要换个方式重试。`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function decideDesk(jobId, allow) {
|
|
832
|
+
chrome.notifications?.clear(`hc-cloud-desk:${jobId}`, () => void chrome.runtime.lastError);
|
|
833
|
+
for (const fn of waiters.get(`desk:${jobId}`) || []) fn(!!allow);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const windowsMap = (list) => Object.fromEntries((Array.isArray(list) ? list : []).map((w) => [w.id, { app: w.app_name, title: w.title || '' }]));
|
|
837
|
+
|
|
838
|
+
const NO_DESK = '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。';
|
|
839
|
+
const FULL_OFF = '这台电脑没有打开桌面完全模式。请用户在这台电脑的终端运行 npx vcorp-chrome desktop full on';
|
|
840
|
+
|
|
841
|
+
async function hostFull() {
|
|
842
|
+
try { return (await hostCallOnce({ op: 'mode' })).full === true; } catch { return false; }
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const hostTimeout = (p, base = 30000) => Math.min(Math.max((Number(p.timeout) || 0) + 10000, base), 7 * 60 * 1000);
|
|
846
|
+
|
|
847
|
+
// 完全模式的 desktop:argv 原样交给 agent-desktop(任意命令),或照常写 cmd/app/ref,
|
|
848
|
+
// 只做参数规整、不做任何闸门。截图回 dataUrl,客户端存成文件。
|
|
849
|
+
async function desktopFull(p, task) {
|
|
850
|
+
const session = `cloud-${task.jobId}`;
|
|
851
|
+
let argv;
|
|
852
|
+
let cmd;
|
|
853
|
+
let target = null;
|
|
854
|
+
if (Array.isArray(p.argv)) {
|
|
855
|
+
argv = p.argv.map(String);
|
|
856
|
+
cmd = argv[0] || '';
|
|
857
|
+
if (!argv.some((a) => a.startsWith('--session'))) argv.push(`--session=${session}`);
|
|
858
|
+
} else {
|
|
859
|
+
const chk = checkDesktop(p, { channel: 'cloud', session, full: true, lookup: () => null, windows: () => null });
|
|
860
|
+
({ argv, cmd, target } = chk);
|
|
861
|
+
}
|
|
862
|
+
const env = await hostCall({ op: 'run', argv, timeoutMs: hostTimeout(p) });
|
|
863
|
+
if (!env.ok) throw err(env.error?.code || 'INTERNAL', renderEnvelope(cmd, env, { target }));
|
|
864
|
+
if (cmd === 'screenshot' && typeof env.data?.data === 'string') {
|
|
865
|
+
const { data, format = 'png', width, height } = env.data;
|
|
866
|
+
return { text: `截图 ${width}×${height}`, dataUrl: `data:image/${format};base64,${data}`, audit: `screenshot ${p.app || ''}` };
|
|
867
|
+
}
|
|
868
|
+
return { text: renderEnvelope(cmd, env, { target }), audit: `${cmd}${p.app ? ` ${p.app}` : ''}` };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// shell / file:只在完全模式下有,宿主那边再查一次开关
|
|
872
|
+
async function fullCmd(cmd, p) {
|
|
873
|
+
if (!(await desktopChild())) throw err('NO_DEVICE', NO_DESK);
|
|
874
|
+
if (!(await hostFull())) throw err('FULL_OFF', FULL_OFF);
|
|
875
|
+
const env = await hostCall({ op: cmd, params: p });
|
|
876
|
+
if (!env.ok) throw err(env.error?.code || 'INTERNAL', `${env.error?.code || 'INTERNAL'} ${env.error?.message || ''}`.trim());
|
|
877
|
+
const d = env.data || {};
|
|
878
|
+
if (cmd === 'shell') {
|
|
879
|
+
const lines = [`exit ${d.exitCode}${d.timedOut ? '(超时被杀)' : ''}${d.truncated ? '(输出过长,已截断)' : ''} · ${d.ms}ms · ${d.cwd}`];
|
|
880
|
+
if (d.stdout) lines.push(d.stdout);
|
|
881
|
+
if (d.stderr) lines.push(`--- stderr ---\n${d.stderr}`);
|
|
882
|
+
const { stdout: _o, stderr: _e, ...meta } = d; // 正文已在 text 里,不再传第二份
|
|
883
|
+
return { text: lines.join('\n'), ...meta, audit: `shell ${String(p.command).slice(0, 120)}` };
|
|
884
|
+
}
|
|
885
|
+
if (p.action === 'read') {
|
|
886
|
+
const { content, ...meta } = d;
|
|
887
|
+
// base64 的内容放 content,客户端按 out 存成文件;文本直接给 text
|
|
888
|
+
return d.encoding === 'utf8'
|
|
889
|
+
? { text: content, ...meta, audit: `file read ${d.path}` }
|
|
890
|
+
: { text: `(base64,${d.bytes} 字节)`, content, ...meta, audit: `file read ${d.path}` };
|
|
891
|
+
}
|
|
892
|
+
if (p.action === 'list') {
|
|
893
|
+
const text = d.items.map((x) => `${x.type === 'dir' ? 'd' : '-'} ${String(x.size ?? '').padStart(10)} ${x.mtime || ''} ${x.path}`).join('\n') + (d.more ? '\n…(超过 2000 项,没列完)' : '');
|
|
894
|
+
return { text, path: d.path, count: d.items.length, more: d.more, audit: `file list ${d.path}` };
|
|
895
|
+
}
|
|
896
|
+
return { text: JSON.stringify(d), ...d, audit: `file ${p.action} ${d.path || d.from || ''}` };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function desktopCmd(p, task, cfg) {
|
|
900
|
+
if (!(await desktopChild())) throw err('NO_DEVICE', NO_DESK);
|
|
901
|
+
if (await hostFull()) return desktopFull(p, task);
|
|
902
|
+
if (p.steps !== undefined) throw err('DESKTOP_DENIED', '云端的桌面命令一条一条发,不支持 steps');
|
|
903
|
+
const session = `cloud-${task.jobId}`;
|
|
904
|
+
const desk = { snaps: { ...(task.desk?.snaps || {}) }, windows: { ...(task.desk?.windows || {}) } };
|
|
905
|
+
if (p.window !== undefined && !desk.windows[String(p.window)]) {
|
|
906
|
+
const env = await hostCall({ op: 'run', argv: ['list-windows', `--session=${session}`] });
|
|
907
|
+
if (env.ok) desk.windows = windowsMap(env.data);
|
|
908
|
+
}
|
|
909
|
+
const chk = checkDesktop(p, {
|
|
910
|
+
channel: 'cloud', session, apps: task.apps || [],
|
|
911
|
+
lookup: (id) => desk.snaps[id] || null,
|
|
912
|
+
windows: (id) => desk.windows[id] || null,
|
|
913
|
+
});
|
|
914
|
+
if (chk.sensitive === 'pay') throw err('PAY_REFUSED', `「${chk.target?.name}」像是要花钱的按钮。桌面能力不代做付款,请用户亲手点。`);
|
|
915
|
+
if (chk.sensitive === 'sensitive') await confirmDesk(chk, task, cfg);
|
|
916
|
+
|
|
917
|
+
const env = await hostCall({ op: 'run', argv: chk.argv, timeoutMs: Math.min(Math.max((Number(p.timeout) || 0) + 10000, 30000), 90000) });
|
|
918
|
+
if (env.ok && (chk.cmd === 'snapshot' || chk.cmd === 'find')) {
|
|
919
|
+
const rec = recordOf(env.data);
|
|
920
|
+
if (rec && !rec.app) rec.app = chk.app;
|
|
921
|
+
if (chk.cmd === 'snapshot') checkObserved(rec);
|
|
922
|
+
if (rec) {
|
|
923
|
+
const prev = chk.cmd === 'find' ? desk.snaps[rec.id] : null;
|
|
924
|
+
delete desk.snaps[rec.id];
|
|
925
|
+
desk.snaps[rec.id] = prev ? { ...prev, refs: { ...prev.refs, ...rec.refs } } : rec;
|
|
926
|
+
const ids = Object.keys(desk.snaps);
|
|
927
|
+
while (ids.length > DESK_SNAPS) delete desk.snaps[ids.shift()];
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (env.ok && chk.cmd === 'list-windows') {
|
|
931
|
+
env.data = filterWindows(env.data, 'cloud');
|
|
932
|
+
desk.windows = windowsMap(env.data);
|
|
933
|
+
}
|
|
934
|
+
const held = await updateTask(task.jobId, (x) => { x.desk = desk; });
|
|
935
|
+
if (!held) throw err('CLOUD_DENIED', '这个任务已经结束');
|
|
936
|
+
const text = renderEnvelope(chk.cmd, env, { target: chk.target });
|
|
937
|
+
if (!env.ok) throw err(env.error?.code || 'INTERNAL', text);
|
|
938
|
+
return { text, audit: `${chk.cmd}${chk.app ? ` ${chk.app}` : ''}${chk.target?.name ? ` ${String(chk.target.name).slice(0, 40)}` : ''}` };
|
|
939
|
+
}
|
|
940
|
+
|
|
656
941
|
// ---------- 命令 ----------
|
|
657
942
|
|
|
943
|
+
// 结果盖上执行它的设备(from)。服务器据此知道回包来自浏览器还是它名下的桌面。
|
|
658
944
|
async function handle(m) {
|
|
659
945
|
const started = Date.now();
|
|
660
946
|
const jobId = String(m.job?.id || '');
|
|
947
|
+
const cfg = await getCfg();
|
|
948
|
+
const self = { id: cfg?.deviceId, kind: 'browser' };
|
|
949
|
+
let from = self;
|
|
661
950
|
try {
|
|
951
|
+
const child = await desktopChild();
|
|
952
|
+
const where = routeHere({ ...m, cap: ['desktop', 'shell', 'file'].includes(m.cmd) ? 'desktop' : capOf(m) }, { self, children: child ? [child] : [] });
|
|
953
|
+
from = where.device;
|
|
662
954
|
const data = await exec(m);
|
|
663
955
|
let out = data;
|
|
664
956
|
const audit = out && typeof out === 'object' ? out.audit : '';
|
|
@@ -666,10 +958,10 @@ async function handle(m) {
|
|
|
666
958
|
if (JSON.stringify(out ?? null).length > policy.RESULT_MAX) {
|
|
667
959
|
out = { text: String(data?.text || '').slice(0, 200000) + '\n…(结果太大,已截断)' };
|
|
668
960
|
}
|
|
669
|
-
send({ type: 'res', id: m.id, ok: true, data: out });
|
|
961
|
+
send(stampFrom({ type: 'res', id: m.id, ok: true, data: out }, from));
|
|
670
962
|
await log({ job: jobId, cmd: m.cmd, ok: true, ms: Date.now() - started, ...(audit ? { msg: String(audit).slice(0, 160) } : {}) });
|
|
671
963
|
} catch (e) {
|
|
672
|
-
send({ type: 'res', id: m.id, ok: false, error: { code: e.code || 'INTERNAL', message: e.message || String(e) } });
|
|
964
|
+
send(stampFrom({ type: 'res', id: m.id, ok: false, error: { code: e.code || 'INTERNAL', message: e.message || String(e) } }, from));
|
|
673
965
|
await log({ job: jobId, cmd: m.cmd, ok: false, code: e.code || 'INTERNAL', msg: String(e.message || e).slice(0, 120) });
|
|
674
966
|
}
|
|
675
967
|
}
|
|
@@ -695,8 +987,9 @@ async function exec(m) {
|
|
|
695
987
|
if (recent.length >= PER_MINUTE) throw err('CLOUD_RATE', '命令太密(每分钟最多 120 条),放慢一点');
|
|
696
988
|
recent.push(now);
|
|
697
989
|
|
|
698
|
-
|
|
699
|
-
const
|
|
990
|
+
if (m.cmd === 'devices') return devicesCmd(cfg);
|
|
991
|
+
const reason = ['sites', 'folders', 'apps'].includes(m.cmd) ? String(m.params?.reason || '').slice(0, 200) : '';
|
|
992
|
+
const task = await taskFor(m.job, cfg, reason, { allowEmpty: ['folders', 'apps', 'desktop', 'shell', 'file'].includes(m.cmd) });
|
|
700
993
|
const jobId = task.jobId;
|
|
701
994
|
const updated = await updateTask(jobId, (t) => { t.count += 1; t.lastAt = Date.now(); });
|
|
702
995
|
if (!updated) throw err('CLOUD_DENIED', '这个任务已经结束');
|
|
@@ -706,6 +999,9 @@ async function exec(m) {
|
|
|
706
999
|
const p = policy.checkCommand(m.cmd, m.params, { hosts, origin: cfg.origin });
|
|
707
1000
|
if (m.cmd === 'sites') return requestSites(updated, p, cfg);
|
|
708
1001
|
if (m.cmd === 'folders') return requestFolders(updated, p, cfg);
|
|
1002
|
+
if (m.cmd === 'apps') return requestApps(updated, p, cfg);
|
|
1003
|
+
if (m.cmd === 'desktop') return desktopCmd(p, updated, cfg);
|
|
1004
|
+
if (m.cmd === 'shell' || m.cmd === 'file') return fullCmd(m.cmd, p);
|
|
709
1005
|
if (m.cmd === 'fs_list' || m.cmd === 'fs_read') return fsCmd(m.cmd, p, updated);
|
|
710
1006
|
const sid = sidOf(jobId);
|
|
711
1007
|
const label = labelOf(cfg.origin);
|
|
@@ -867,8 +1163,9 @@ async function panelState() {
|
|
|
867
1163
|
cfg: cfg && { origin: cfg.origin, account: cfg.account, sites: allowOf(cfg), auto: cfg.auto, autoConfirm: !!cfg.autoConfirm, paused: cfg.paused, pairedAt: cfg.pairedAt },
|
|
868
1164
|
status: status || { state: 'off' },
|
|
869
1165
|
claim: claimed && claimed.expiresAt > Date.now() ? { origin: claimed.origin, account: claimed.account } : null,
|
|
870
|
-
tasks: Object.values(tasks).sort((a, b) => b.lastAt - a.lastAt),
|
|
1166
|
+
tasks: Object.values(tasks).sort((a, b) => b.lastAt - a.lastAt).map(({ desk: _desk, ...t }) => t),
|
|
871
1167
|
folders: await folders.listFolders().catch(() => []),
|
|
1168
|
+
desktop: await deskInfo().then((d) => (d && chrome.runtime.sendNativeMessage ? d : null)),
|
|
872
1169
|
log: list.slice(-40).reverse(),
|
|
873
1170
|
defaultOrigin: policy.DEFAULT_ORIGIN,
|
|
874
1171
|
};
|
|
@@ -930,6 +1227,10 @@ async function onPanel(m) {
|
|
|
930
1227
|
case 'decide': await decide(String(m.jobId), !!m.allow); return panelState();
|
|
931
1228
|
case 'decideSites': decideSites(String(m.jobId), !!m.allow); return panelState();
|
|
932
1229
|
case 'decideFolders': decideFolders(String(m.jobId), !!m.allow); return panelState();
|
|
1230
|
+
case 'decideApps': decideApps(String(m.jobId), !!m.allow); return panelState();
|
|
1231
|
+
case 'decideDesk': decideDesk(String(m.jobId), !!m.allow); return panelState();
|
|
1232
|
+
case 'desktopEnable': await enableDesktop(); return panelState();
|
|
1233
|
+
case 'desktopDisable': await disableDesktop(); return panelState();
|
|
933
1234
|
case 'openMount': await openMount(m.folderId ? String(m.folderId) : ''); return panelState();
|
|
934
1235
|
case 'removeFolder': await folders.removeFolder(String(m.id || '')); await reportFolders(); return panelState();
|
|
935
1236
|
case 'foldersChanged': await reportFolders(); return panelState();
|
|
@@ -956,6 +1257,8 @@ export function initCloud(d) {
|
|
|
956
1257
|
|
|
957
1258
|
chrome.notifications?.onButtonClicked.addListener((id, idx) => {
|
|
958
1259
|
if (id.startsWith('hc-cloud-folders:')) decideFolders(id.slice('hc-cloud-folders:'.length), idx === 0);
|
|
1260
|
+
else if (id.startsWith('hc-cloud-apps:')) decideApps(id.slice('hc-cloud-apps:'.length), idx === 0);
|
|
1261
|
+
else if (id.startsWith('hc-cloud-desk:')) decideDesk(id.slice('hc-cloud-desk:'.length), idx === 0);
|
|
959
1262
|
else if (id.startsWith('hc-cloud-sites:')) decideSites(id.slice('hc-cloud-sites:'.length), idx === 0);
|
|
960
1263
|
else if (id.startsWith('hc-cloud:')) void decide(id.slice('hc-cloud:'.length), idx === 0);
|
|
961
1264
|
else if (id.startsWith('hc-folder:')) void openMount(id.slice('hc-folder:'.length));
|
|
@@ -1002,4 +1305,10 @@ export function initCloud(d) {
|
|
|
1002
1305
|
}).then(pushLive);
|
|
1003
1306
|
|
|
1004
1307
|
void ensureCloud();
|
|
1308
|
+
|
|
1309
|
+
void chrome.storage.local.get(DESK_PENDING).then(async (r) => {
|
|
1310
|
+
if (!r[DESK_PENDING]) return;
|
|
1311
|
+
await chrome.storage.local.remove(DESK_PENDING);
|
|
1312
|
+
await enableDesktop().catch((e) => log({ ev: 'desktop_fail', msg: e.message || String(e) }));
|
|
1313
|
+
});
|
|
1005
1314
|
}
|