vcorp-chrome 2.0.0 → 2.2.1

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.
@@ -0,0 +1,128 @@
1
+ # 桌面完全模式 · 交接(2026-11-04)
2
+
3
+ 目标分两步:
4
+ 1. **第一阶段(已完成)**:云端任务在用户电脑上拥有和本机 agent(goose 一类)一样的能力,包括命令行、任意文件和全部桌面命令。
5
+ 2. **第二阶段(未开始)**:在不废掉这些能力的前提下,重新设计安全方案。
6
+
7
+ 另有一项**提权操作**(UAC / 管理员程序),尚未实现,见第四节。
8
+
9
+ ---
10
+
11
+ ## 一、当前进度
12
+
13
+ ### 已上线
14
+
15
+ | 部分 | 位置 | 状态 |
16
+ |---|---|---|
17
+ | 扩展 + 原生宿主 2.2.0 | huashu-chrome `2928698`(分支 `my-dev`,**未合进 main**) | 已推送;本机扩展已重载到 2.2.0 |
18
+ | 隐私政策说明 | huashu-chrome `bd165d9`(`my-dev`) | 已推送 |
19
+ | 服务端路由 | opus-video-maker `1609f71`(main) | Coolify 已部署到 maker.vcorp.ai |
20
+ | 沙箱镜像(6 个) | gooseboxd 7.79,由 `1609f71` 构建 | 已推送,各镜像的 default_model 保持不变 |
21
+
22
+ 全链路已经实测通过:maker 任务 → 服务器 → 扩展 → 原生宿主 → `shell` 执行 `Get-WinEvent`,结果正常返回。
23
+
24
+ ### 开关
25
+
26
+ - 打开:`npx vcorp-chrome desktop full on`;关闭:`npx vcorp-chrome desktop full off`;查看:`desktop status`。
27
+ - 开关写在 `~/.vcorp-chrome/desktop-full.json`,原生宿主每条命令都现读一次,服务器、网页和扩展都改不了它。
28
+ - **作者这台电脑(desktop-m_f7feacbc0db40b8f)目前是开着的。**
29
+
30
+ ### 完全模式下云端多出的能力
31
+
32
+ - `shell`:默认用 PowerShell(输出 UTF-8),也可以选 pwsh / cmd / bash / sh。支持 `cwd`、`timeout`(最长 7 分钟,超时连同进程树一起杀掉)、`stdin`、`env`。
33
+ - `file`:stat / list / read / write / mkdir / delete / move / copy,路径不限。read 支持 `offset`/`length` 分段,单次最多 16MB;可以用 base64 读写二进制。
34
+ - `desktop`:agent-desktop 的全部命令,不需要申请应用,不挡浏览器窗口,允许 `--headed`、截图、剪贴板和通知;用 `argv` 可以把任意命令原样传给 agent-desktop。
35
+ - 大回包分页:超过 1MB 的回包先写进 `%TEMP%/vcorp-desktop-pages/`,扩展再用 `op: 'page'` 分段取回。
36
+
37
+ 完全模式关闭时,行为和 2.1.0 完全一样。
38
+
39
+ ### 代码位置
40
+
41
+ - `native-host/full.js`:shell、file 和分页的实现。
42
+ - `native-host/host.js`:新增 `mode` / `shell` / `file` / `page` 四种 op;完全模式下 `run` 只检查 argv 的形状。
43
+ - `extension/cloud.js`:`hostCall` 负责分页拼接;`desktopFull`、`fullCmd`、`hostFull` 是完全模式的入口。
44
+ - `extension/desktop-policy.js`:`checkDesktop` 的 `ctx.full` 为真时跳过所有闸门。
45
+ - `extension/cloud-policy.js`:`CLOUD_CMDS` 加入了 `shell` 和 `file`。
46
+ - `src/cli.js`、`src/lib/paths.js`:开关命令和 `fullMode()`。
47
+ - `test/desktop-full.test.js`:新增测试。全套 174 个测试通过。
48
+ - opus-video-maker:`extlink.js`(`DESKTOP_CMDS` 路由)、`skills/social-publisher/scripts/browser.mjs`(`file.out` 以及桌面截图存盘)、`skills/user-browser/SKILL.md`(完全模式的用法说明)。
49
+
50
+ ### 已知缺口和待办
51
+
52
+ - **`my-dev` 还没合进 `main`**。如果从 main 发布或打包,会丢掉 2.2.0。
53
+ - 侧栏不显示完全模式的状态,也没有一键关闭;目前只能在终端里看和关。
54
+ - 完全模式同时跳过了「发送/删除先确认」和「付款一律拒绝」。这是第一阶段有意的选择,第二阶段应最先恢复。
55
+ - 每条桌面命令前会多一次 `mode` 宿主调用(约一次进程启动的开销)。
56
+ - 仍然有效的限制:每个任务最多 1500 条命令,每分钟 120 条,服务器每条命令最多等 8 分钟。
57
+ - 回滚:gooseboxd 上旧代码在 `~/ovm-images/opus-video-maker.prev-20261104`;扩展回退到 `07f6c47`(2.1.0);或者直接 `desktop full off`。
58
+
59
+ ---
60
+
61
+ ## 二、第二阶段:安全方案(待定,讨论记录)
62
+
63
+ 核心判断:**按 exe 名字开闸管的是入口,真正的风险在动作上**。一旦开放了 shell,应用名单、浏览器窗口闸这类限制就都拦不住什么了,所以新方案不应该回到逐个 exe 批准的老路。
64
+
65
+ 主要威胁:
66
+ 1. **提示注入**:云端 agent 读到的网页、应用内容里藏着指令,变成在用户电脑上执行的命令。
67
+ 2. **服务器或账号被攻破**:所有开了完全模式的电脑一起沦陷。
68
+ 3. **点到扩展自己的控件**:桌面侧能点 Chrome 主窗口里的侧栏「自动确认」、钱包和密码管理,扩展里的闸门就会被绕过。
69
+
70
+ 候选方向:
71
+ - **设备级档位**(只能在本机设置):观察(只读 + 截图)/ 协作(普通应用可写,终端只读,发送/删除要确认,付款拒绝)/ 完全(现状)。
72
+ - **所有档位都保留的底线**:不碰密码管理器;付款按钮必须用户亲手点;完全档以下不碰浏览器主窗口。
73
+ - **动作分级确认**:不可逆或对外的动作(删除、发送、改系统设置、写系统目录)弹通知等用户确认,而不是按应用名拦截。
74
+ - **可见、可停**:侧栏实时显示桌面上正在执行的命令(审计日志已经有了),提供一键暂停;完全模式开着时托盘或侧栏常驻提示。
75
+ - **按任务的范围**:任务开始时声明要用 shell 或文件,用户一次批准,批准只在这个任务内有效。
76
+ - **本地审计**:shell 的命令和输出写进 `~/.vcorp-chrome/audit.jsonl`,出了问题能查。
77
+ - **只读的系统探针 `sys`**(备选):固定、参数有类型的查询(事件日志、崩溃、进程、服务、磁盘、更新),用于观察档下的系统体检,不需要开放 shell。
78
+ - 远期:桌面作为独立设备,用自己的密钥直连 maker(`desktop serve` 常驻进程),不再依赖浏览器中继,同时解决原生消息每次启动一个进程的开销。
79
+
80
+ ---
81
+
82
+ ## 三、为什么操作不了提权程序(UIPI)
83
+
84
+ 现象:用户在 UAC 上点了「是」之后,HWiNFO64 就无法再操作了。
85
+
86
+ 原因不在 agent-desktop,而是 Windows 的**用户界面权限隔离(UIPI)**:
87
+ - 点了 UAC 的程序运行在**高完整性级别**(管理员)。agent-desktop 由 Chrome 经原生宿主启动,和 Chrome 一样是**中完整性级别**。
88
+ - 低级别进程不能控制高级别窗口:SendInput 和 SendMessage 会被系统丢掉,UIA 的 Invoke 和 SetValue 会失败,读取树最多只能读到一部分。
89
+ - 所有不提权运行的自动化工具都受这个限制(AutoHotkey、Power Automate、pywinauto 都一样)。
90
+ - `shell` 也一样跑在普通权限下:读 Security 日志、改服务、写 `C:\Windows` 都做不到。
91
+ - UAC 弹窗本身显示在安全桌面上,任何用户进程都点不到,这是有意的设计,**只能由人来点**。
92
+
93
+ ---
94
+
95
+ ## 四、未来的高权限操作方案
96
+
97
+ ### 方案 1:提权代理(推荐)
98
+
99
+ - 用户以管理员身份登记一次计划任务:用户登录时以「最高权限」启动一个常驻代理(`native-host/elevated.js`)。之后每次启动都不弹 UAC。
100
+ - 代理监听一个命名管道,ACL 只允许当前用户的 SID 连接;可以再加一个启动时生成、写在仅用户可读文件里的令牌。
101
+ - 原生宿主遇到以下情况时把命令转给代理:命令带 `elevated: true`,或者目标窗口属于提权进程(可以检查进程的完整性级别)。
102
+ - 代理以管理员身份运行 agent-desktop 和 shell,回包格式与现在一致。
103
+ - 开关同样只在本机:例如 `desktop full on --elevated` 或单独的 `desktop elevated install|uninstall`(登记或删除计划任务)。
104
+ - 代价:配对服务器上的任务在这台电脑上**等于管理员**。这一档必须和第二阶段的确认、可见、可停机制一起上线,不建议单独开放。
105
+
106
+ 需要改的地方:新增 `native-host/elevated.js`;`src/cli.js` 加计划任务的登记和注销命令;`native-host/host.js` 加转发逻辑;`extension/cloud.js` 和 `desktop-policy.js` 透传 `elevated`;`SKILL.md` 补充用法;补测试(管道 ACL、未登记时的报错、转发路径)。
107
+
108
+ ### 方案 2:uiAccess 签名程序
109
+
110
+ - 给 agent-desktop 的 exe 加 `uiAccess=true` 清单,签名后装到 `Program Files`,就能绕过 UIPI 控制提权窗口(屏幕键盘、讲述人就是这么做的)。
111
+ - 缺点:需要一张本机信任的代码签名证书(自签后导入受信任根也行);每次升级 agent-desktop 都要重新签名;**只解决界面操作,shell 还是普通权限**。
112
+
113
+ ### 方案 3:绕开界面,读数据
114
+
115
+ - 像 HWiNFO 这种只是要读传感器数据的场景,用它的共享内存或 CSV 日志,再用 `shell` / `file` 去读。比读界面快,也更准,而且不需要提权。
116
+
117
+ ### 不建议
118
+
119
+ - 关闭 UAC,或者把 Chrome 整个以管理员身份运行:整台机器的所有程序都会默认拿到管理员权限。
120
+
121
+ ---
122
+
123
+ ## 五、接手时先做的事
124
+
125
+ 1. 确认 `my-dev` 上的 2.2.0 和 `bd165d9` 是否要合进 main 并发布。
126
+ 2. 决定第二阶段的档位设计,优先恢复「付款拒绝」和「发送/删除确认」。
127
+ 3. 侧栏加上完全模式的状态显示和一键关闭。
128
+ 4. 第二阶段落地后,再评估是否做方案 1(提权代理)。
@@ -41,6 +41,8 @@ export const CLOUD_CMDS = new Set([
41
41
  // 桌面:经同机的原生宿主转给 agent-desktop。按任务批准应用(apps),规矩在 desktop-policy.js;
42
42
  // 用户没在侧栏里启用桌面能力时宿主根本连不上,这几条只会报「没有桌面设备」。
43
43
  'desktop', 'apps', 'devices',
44
+ // 桌面完全模式:跑命令、读写文件。开关在用户本机,宿主每条现查;没开时这两条只会报 FULL_OFF
45
+ 'shell', 'file',
44
46
  ]);
45
47
 
46
48
  // ---------- 站点申请 ----------
@@ -362,6 +364,13 @@ export function checkCommand(cmd, params, { hosts, origin }) {
362
364
  return p;
363
365
  case 'devices':
364
366
  return {};
367
+ // 原样交给宿主,由完全模式开关决定能不能跑
368
+ case 'shell':
369
+ if (typeof p.command !== 'string' || !p.command.trim()) throw err('CLOUD_DENIED', 'shell 要给 command');
370
+ return p;
371
+ case 'file':
372
+ if (typeof p.path !== 'string' || !p.path.trim()) throw err('CLOUD_DENIED', 'file 要给 path');
373
+ return p;
365
374
  case 'folders': {
366
375
  // use 可以是空的:那是在问「这个浏览器挂了哪些」,不是在申请。
367
376
  const { names, bad } = cleanFolderNames(p.use);
@@ -668,11 +668,35 @@ const updateTask = (jobId, fn) => serial(async () => {
668
668
  const HOST = 'ai.vcorp.desktop';
669
669
  const DESK = 'cloudDesktop'; // storage.local:启用桌面能力时宿主报的设备
670
670
  const DESK_PENDING = 'cloudDesktopPending'; // 为补上 nativeMessaging 绑定而重载,重载后接着启用
671
+ const DESK_ERR = 'cloudDesktopError'; // 上次启用失败的原因,给侧栏看。重载后的那次启用没人接它抛的错
671
672
  const DESK_SNAPS = 4; // 每个任务记几份快照的户口。户口按 ref 存名字,大应用一份上百 KB
672
673
 
673
674
  const deskInfo = async () => (await chrome.storage.local.get(DESK))[DESK] || null;
674
675
 
675
- function hostCall(msg) {
676
+ // 宿主回包超过 1MB 时先回 { paged },这里一段一段取回再拼(见 native-host/full.js)
677
+ async function hostCall(msg) {
678
+ const r = await hostCallOnce(msg);
679
+ if (!r.paged) return r;
680
+ const { id, size } = r.paged;
681
+ const parts = [];
682
+ let offset = 0;
683
+ for (let i = 0; i < 200; i++) {
684
+ const pg = await hostCallOnce({ op: 'page', id, offset });
685
+ if (!pg.ok || !pg.page) return pg;
686
+ const bin = atob(pg.page.data);
687
+ const bytes = new Uint8Array(bin.length);
688
+ for (let j = 0; j < bin.length; j++) bytes[j] = bin.charCodeAt(j);
689
+ parts.push(bytes);
690
+ offset += bytes.length;
691
+ if (pg.page.done || offset >= size) break;
692
+ }
693
+ const all = new Uint8Array(offset);
694
+ let at = 0;
695
+ for (const b of parts) { all.set(b, at); at += b.length; }
696
+ return JSON.parse(new TextDecoder().decode(all));
697
+ }
698
+
699
+ function hostCallOnce(msg) {
676
700
  if (!chrome.runtime.sendNativeMessage) {
677
701
  return Promise.reject(err('NO_DEVICE', '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。'));
678
702
  }
@@ -694,6 +718,7 @@ async function desktopChild() {
694
718
  }
695
719
 
696
720
  async function enableDesktop() {
721
+ await chrome.storage.local.remove(DESK_ERR);
697
722
  // 运行时才授予的 nativeMessaging,Chrome 不给已经在跑的 service worker 补上绑定,重载后才有
698
723
  if (!chrome.runtime.sendNativeMessage) {
699
724
  if (!(await chrome.permissions.contains({ permissions: ['nativeMessaging'] }))) {
@@ -704,9 +729,16 @@ async function enableDesktop() {
704
729
  chrome.runtime.reload();
705
730
  return;
706
731
  }
707
- const r = await hostCall({ op: 'hello' });
708
- const device = cleanDevice(r.device);
709
- if (!device || device.kind !== 'desktop') throw err('DEVICE_OFFLINE', '桌面宿主报上来的设备不对');
732
+ let device;
733
+ let r;
734
+ try {
735
+ r = await hostCall({ op: 'hello' });
736
+ device = cleanDevice(r.device);
737
+ if (!device || device.kind !== 'desktop') throw err('DEVICE_OFFLINE', '桌面宿主报上来的设备不对');
738
+ } catch (e) {
739
+ await chrome.storage.local.set({ [DESK_ERR]: (e.message || String(e)).replace('请用户在终端', '请在终端') });
740
+ throw e;
741
+ }
710
742
  await chrome.storage.local.set({ [DESK]: { device, ready: !!r.ready, at: Date.now() } });
711
743
  await log({ ev: 'desktop_on', msg: device.id });
712
744
  // 重连一次,服务器在 hello 里看到这台设备
@@ -714,7 +746,7 @@ async function enableDesktop() {
714
746
  }
715
747
 
716
748
  async function disableDesktop() {
717
- await chrome.storage.local.remove(DESK);
749
+ await chrome.storage.local.remove([DESK, DESK_ERR]);
718
750
  await log({ ev: 'desktop_off' });
719
751
  disconnect(); retryDelay = 0; void connect();
720
752
  }
@@ -812,8 +844,70 @@ function decideDesk(jobId, allow) {
812
844
 
813
845
  const windowsMap = (list) => Object.fromEntries((Array.isArray(list) ? list : []).map((w) => [w.id, { app: w.app_name, title: w.title || '' }]));
814
846
 
847
+ const NO_DESK = '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。';
848
+ const FULL_OFF = '这台电脑没有打开桌面完全模式。请用户在这台电脑的终端运行 npx vcorp-chrome desktop full on';
849
+
850
+ async function hostFull() {
851
+ try { return (await hostCallOnce({ op: 'mode' })).full === true; } catch { return false; }
852
+ }
853
+
854
+ const hostTimeout = (p, base = 30000) => Math.min(Math.max((Number(p.timeout) || 0) + 10000, base), 7 * 60 * 1000);
855
+
856
+ // 完全模式的 desktop:argv 原样交给 agent-desktop(任意命令),或照常写 cmd/app/ref,
857
+ // 只做参数规整、不做任何闸门。截图回 dataUrl,客户端存成文件。
858
+ async function desktopFull(p, task) {
859
+ const session = `cloud-${task.jobId}`;
860
+ let argv;
861
+ let cmd;
862
+ let target = null;
863
+ if (Array.isArray(p.argv)) {
864
+ argv = p.argv.map(String);
865
+ cmd = argv[0] || '';
866
+ if (!argv.some((a) => a.startsWith('--session'))) argv.push(`--session=${session}`);
867
+ } else {
868
+ const chk = checkDesktop(p, { channel: 'cloud', session, full: true, lookup: () => null, windows: () => null });
869
+ ({ argv, cmd, target } = chk);
870
+ }
871
+ const env = await hostCall({ op: 'run', argv, timeoutMs: hostTimeout(p) });
872
+ if (!env.ok) throw err(env.error?.code || 'INTERNAL', renderEnvelope(cmd, env, { target }));
873
+ if (cmd === 'screenshot' && typeof env.data?.data === 'string') {
874
+ const { data, format = 'png', width, height } = env.data;
875
+ return { text: `截图 ${width}×${height}`, dataUrl: `data:image/${format};base64,${data}`, audit: `screenshot ${p.app || ''}` };
876
+ }
877
+ return { text: renderEnvelope(cmd, env, { target }), audit: `${cmd}${p.app ? ` ${p.app}` : ''}` };
878
+ }
879
+
880
+ // shell / file:只在完全模式下有,宿主那边再查一次开关
881
+ async function fullCmd(cmd, p) {
882
+ if (!(await desktopChild())) throw err('NO_DEVICE', NO_DESK);
883
+ if (!(await hostFull())) throw err('FULL_OFF', FULL_OFF);
884
+ const env = await hostCall({ op: cmd, params: p });
885
+ if (!env.ok) throw err(env.error?.code || 'INTERNAL', `${env.error?.code || 'INTERNAL'} ${env.error?.message || ''}`.trim());
886
+ const d = env.data || {};
887
+ if (cmd === 'shell') {
888
+ const lines = [`exit ${d.exitCode}${d.timedOut ? '(超时被杀)' : ''}${d.truncated ? '(输出过长,已截断)' : ''} · ${d.ms}ms · ${d.cwd}`];
889
+ if (d.stdout) lines.push(d.stdout);
890
+ if (d.stderr) lines.push(`--- stderr ---\n${d.stderr}`);
891
+ const { stdout: _o, stderr: _e, ...meta } = d; // 正文已在 text 里,不再传第二份
892
+ return { text: lines.join('\n'), ...meta, audit: `shell ${String(p.command).slice(0, 120)}` };
893
+ }
894
+ if (p.action === 'read') {
895
+ const { content, ...meta } = d;
896
+ // base64 的内容放 content,客户端按 out 存成文件;文本直接给 text
897
+ return d.encoding === 'utf8'
898
+ ? { text: content, ...meta, audit: `file read ${d.path}` }
899
+ : { text: `(base64,${d.bytes} 字节)`, content, ...meta, audit: `file read ${d.path}` };
900
+ }
901
+ if (p.action === 'list') {
902
+ 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 项,没列完)' : '');
903
+ return { text, path: d.path, count: d.items.length, more: d.more, audit: `file list ${d.path}` };
904
+ }
905
+ return { text: JSON.stringify(d), ...d, audit: `file ${p.action} ${d.path || d.from || ''}` };
906
+ }
907
+
815
908
  async function desktopCmd(p, task, cfg) {
816
- if (!(await desktopChild())) throw err('NO_DEVICE', '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。');
909
+ if (!(await desktopChild())) throw err('NO_DEVICE', NO_DESK);
910
+ if (await hostFull()) return desktopFull(p, task);
817
911
  if (p.steps !== undefined) throw err('DESKTOP_DENIED', '云端的桌面命令一条一条发,不支持 steps');
818
912
  const session = `cloud-${task.jobId}`;
819
913
  const desk = { snaps: { ...(task.desk?.snaps || {}) }, windows: { ...(task.desk?.windows || {}) } };
@@ -864,7 +958,7 @@ async function handle(m) {
864
958
  let from = self;
865
959
  try {
866
960
  const child = await desktopChild();
867
- const where = routeHere({ ...m, cap: m.cmd === 'desktop' ? 'desktop' : capOf(m) }, { self, children: child ? [child] : [] });
961
+ const where = routeHere({ ...m, cap: ['desktop', 'shell', 'file'].includes(m.cmd) ? 'desktop' : capOf(m) }, { self, children: child ? [child] : [] });
868
962
  from = where.device;
869
963
  const data = await exec(m);
870
964
  let out = data;
@@ -904,7 +998,7 @@ async function exec(m) {
904
998
 
905
999
  if (m.cmd === 'devices') return devicesCmd(cfg);
906
1000
  const reason = ['sites', 'folders', 'apps'].includes(m.cmd) ? String(m.params?.reason || '').slice(0, 200) : '';
907
- const task = await taskFor(m.job, cfg, reason, { allowEmpty: m.cmd === 'folders' || m.cmd === 'apps' });
1001
+ const task = await taskFor(m.job, cfg, reason, { allowEmpty: ['folders', 'apps', 'desktop', 'shell', 'file'].includes(m.cmd) });
908
1002
  const jobId = task.jobId;
909
1003
  const updated = await updateTask(jobId, (t) => { t.count += 1; t.lastAt = Date.now(); });
910
1004
  if (!updated) throw err('CLOUD_DENIED', '这个任务已经结束');
@@ -916,6 +1010,7 @@ async function exec(m) {
916
1010
  if (m.cmd === 'folders') return requestFolders(updated, p, cfg);
917
1011
  if (m.cmd === 'apps') return requestApps(updated, p, cfg);
918
1012
  if (m.cmd === 'desktop') return desktopCmd(p, updated, cfg);
1013
+ if (m.cmd === 'shell' || m.cmd === 'file') return fullCmd(m.cmd, p);
919
1014
  if (m.cmd === 'fs_list' || m.cmd === 'fs_read') return fsCmd(m.cmd, p, updated);
920
1015
  const sid = sidOf(jobId);
921
1016
  const label = labelOf(cfg.origin);
@@ -1070,8 +1165,8 @@ async function uploadMedia(p, tid, sid, label) {
1070
1165
  const fromPanel = (sender) => !sender.tab && typeof sender.url === 'string' && sender.url.startsWith(chrome.runtime.getURL(''));
1071
1166
 
1072
1167
  async function panelState() {
1073
- const [cfg, { [STATUS]: status }, { [CLAIM]: claimed }, tasks, { [LOG]: list = [] }] = await Promise.all([
1074
- getCfg(), chrome.storage.session.get(STATUS), chrome.storage.session.get(CLAIM), getTasks(), chrome.storage.local.get(LOG),
1168
+ const [cfg, { [STATUS]: status }, { [CLAIM]: claimed }, tasks, { [LOG]: list = [], [DESK_ERR]: deskErr }] = await Promise.all([
1169
+ getCfg(), chrome.storage.session.get(STATUS), chrome.storage.session.get(CLAIM), getTasks(), chrome.storage.local.get([LOG, DESK_ERR]),
1075
1170
  ]);
1076
1171
  return {
1077
1172
  cfg: cfg && { origin: cfg.origin, account: cfg.account, sites: allowOf(cfg), auto: cfg.auto, autoConfirm: !!cfg.autoConfirm, paused: cfg.paused, pairedAt: cfg.pairedAt },
@@ -1079,7 +1174,7 @@ async function panelState() {
1079
1174
  claim: claimed && claimed.expiresAt > Date.now() ? { origin: claimed.origin, account: claimed.account } : null,
1080
1175
  tasks: Object.values(tasks).sort((a, b) => b.lastAt - a.lastAt).map(({ desk: _desk, ...t }) => t),
1081
1176
  folders: await folders.listFolders().catch(() => []),
1082
- desktop: await deskInfo().then((d) => (d && chrome.runtime.sendNativeMessage ? d : null)),
1177
+ desktop: await deskInfo().then((d) => (d && chrome.runtime.sendNativeMessage ? d : deskErr ? { error: deskErr } : null)),
1083
1178
  log: list.slice(-40).reverse(),
1084
1179
  defaultOrigin: policy.DEFAULT_ORIGIN,
1085
1180
  };
@@ -187,11 +187,13 @@ function browserGate(app, title, fileDialog, { preflight = false } = {}) {
187
187
  export function checkDesktop(params, ctx = {}) {
188
188
  const p = params && typeof params === 'object' ? params : {};
189
189
  const channel = ctx.channel === 'cloud' ? 'cloud' : 'local';
190
+ // 完全模式(用户在本机打开的):不认应用名单、不挡浏览器窗口、不查快照户口,参数形状照旧规整
191
+ const full = ctx.full === true;
190
192
  const cmd = String(p.cmd || '');
191
193
  const spec = DESKTOP_CMDS[cmd];
192
- if (!spec) throw err('DESKTOP_DENIED', `desktop 不支持 ${cmd || '(没写 cmd)'}`);
193
- if (!spec[channel]) throw err('DESKTOP_DENIED', `${channel === 'cloud' ? '云端任务' : '本机'}不能用 desktop ${cmd}`);
194
- if (p.headed && channel === 'cloud') throw err('DESKTOP_DENIED', '云端任务不能用真实鼠标键盘(headed)');
194
+ if (!spec) throw err('DESKTOP_DENIED', `desktop 不支持 ${cmd || '(没写 cmd)'}${full ? ':其它命令用 argv 原样传' : ''}`);
195
+ if (!full && !spec[channel]) throw err('DESKTOP_DENIED', `${channel === 'cloud' ? '云端任务' : '本机'}不能用 desktop ${cmd}`);
196
+ if (!full && p.headed && channel === 'cloud') throw err('DESKTOP_DENIED', '云端任务不能用真实鼠标键盘(headed)');
195
197
 
196
198
  const argv = [cmd];
197
199
  let app = p.app !== undefined ? str(p.app, 80).trim() : '';
@@ -200,10 +202,12 @@ export function checkDesktop(params, ctx = {}) {
200
202
  let title = '';
201
203
  if (win) {
202
204
  const w = ctx.windows?.(win);
203
- if (!w) throw err('DESKTOP_DENIED', `不认识窗口 ${win}:先 list-windows`);
204
- app = app || w.app;
205
- title = w.title || '';
206
- if (appKey(w.app) !== appKey(app)) throw err('DESKTOP_DENIED', `窗口 ${win} 属于 ${w.app},不是 ${app}`);
205
+ if (!w && !full) throw err('DESKTOP_DENIED', `不认识窗口 ${win}:先 list-windows`);
206
+ if (w) {
207
+ app = app || w.app;
208
+ title = w.title || '';
209
+ if (appKey(w.app) !== appKey(app)) throw err('DESKTOP_DENIED', `窗口 ${win} 属于 ${w.app},不是 ${app}`);
210
+ }
207
211
  }
208
212
 
209
213
  let rec = null;
@@ -212,19 +216,21 @@ export function checkDesktop(params, ctx = {}) {
212
216
  const ref = str(spec.ref ? p.ref : p.root, 60);
213
217
  if (!REF_RE.test(ref)) throw err('DESKTOP_DENIED', 'ref 要用快照里完整的形式,如 @s8f3k2p9:e8');
214
218
  rec = ctx.lookup?.(snapshotIdOf(ref)) || null;
215
- if (!rec) throw err('DESKTOP_DENIED', `不认识 ${ref} 所属的快照:先在这个会话里 snapshot 一次`);
216
- app = rec.app;
217
- title = rec.window?.title || '';
218
- target = rec.refs[/:(e\d+)$/.exec(ref)[1]] || null;
219
+ if (!rec && !full) throw err('DESKTOP_DENIED', `不认识 ${ref} 所属的快照:先在这个会话里 snapshot 一次`);
220
+ if (rec) {
221
+ app = rec.app;
222
+ title = rec.window?.title || '';
223
+ target = rec.refs[/:(e\d+)$/.exec(ref)[1]] || null;
224
+ }
219
225
  if (spec.ref) argv.push(ref);
220
226
  else argv.push('--root', ref);
221
227
  }
222
228
 
223
229
  const appless = ['list-apps', 'clipboard-set', 'clipboard-get', 'list-notifications', 'notification-action'];
224
- if (!app && !appless.includes(cmd) && !(cmd === 'wait' && p.ms !== undefined && p.text === undefined)) {
230
+ if (!full && !app && !appless.includes(cmd) && !(cmd === 'wait' && p.ms !== undefined && p.text === undefined)) {
225
231
  if (channel === 'cloud' || cmd !== 'list-windows') throw err('DESKTOP_DENIED', `desktop ${cmd} 要给 app 或 window`);
226
232
  }
227
- if (app) {
233
+ if (app && !full) {
228
234
  const why = deniedApp(app, channel);
229
235
  if (why) throw err('DESKTOP_DENIED', why);
230
236
  if (channel === 'cloud' && !(ctx.apps || []).includes(appKey(app))) {
@@ -1,41 +1,41 @@
1
- {
2
- "manifest_version": 3,
3
- "name": "vcorp-chrome",
4
- "version": "2.0.0",
5
- "description": "让 AI agent 操控你已登录的 Chrome。基于花叔的 huashu-chrome(MIT)修改。",
6
- "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq6xRqEwG4yTuHRG1Zah3j//T3B72XnazyZ+9fOCa5P4bBLPmNBN7YAGL5WnjtPVJefXT9r2Ol0qvNztayDG3dMjK6AZoWccgKa5rhxcvWgFUjanXtmBBQ0qgYwNhuEJSkdinLLtyUg6YmK7Mn72NWpfa/NfqNHdK5AHslvvtJ50iQAYmabl2Nd3vOJ9DxtLBt8a2DfoGPWizgEB3+gkqm2PB8LywwwoiGKd7acr1wZEWmWK9/s0YWdzIstjlvk5AO5XFn6EtEzW8tAAOwBka9239pcyj0kEogWctHLJn7p9VFru38r+I1USyGn6u9XQ68n9edWddxBYokETCqEMT9QIDAQAB",
7
- "minimum_chrome_version": "116",
8
- "background": {
9
- "service_worker": "background.js",
10
- "type": "module"
11
- },
12
- "action": {
13
- "default_title": "vcorp-chrome"
14
- },
15
- "permissions": [
16
- "tabs",
17
- "tabGroups",
18
- "scripting",
19
- "storage",
20
- "alarms",
21
- "offscreen",
22
- "downloads",
23
- "sidePanel",
24
- "notifications",
25
- "debugger"
26
- ],
27
- "optional_permissions": [
28
- "nativeMessaging"
29
- ],
30
- "host_permissions": [
31
- "<all_urls>"
32
- ],
33
- "icons": {
34
- "16": "icons/icon16.png",
35
- "48": "icons/icon48.png",
36
- "128": "icons/icon128.png"
37
- },
38
- "side_panel": {
39
- "default_path": "panel.html"
40
- }
41
- }
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "vcorp-chrome",
4
+ "version": "2.2.1",
5
+ "description": "让 AI agent 操控你已登录的 Chrome。基于花叔的 huashu-chrome(MIT)修改。",
6
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq6xRqEwG4yTuHRG1Zah3j//T3B72XnazyZ+9fOCa5P4bBLPmNBN7YAGL5WnjtPVJefXT9r2Ol0qvNztayDG3dMjK6AZoWccgKa5rhxcvWgFUjanXtmBBQ0qgYwNhuEJSkdinLLtyUg6YmK7Mn72NWpfa/NfqNHdK5AHslvvtJ50iQAYmabl2Nd3vOJ9DxtLBt8a2DfoGPWizgEB3+gkqm2PB8LywwwoiGKd7acr1wZEWmWK9/s0YWdzIstjlvk5AO5XFn6EtEzW8tAAOwBka9239pcyj0kEogWctHLJn7p9VFru38r+I1USyGn6u9XQ68n9edWddxBYokETCqEMT9QIDAQAB",
7
+ "minimum_chrome_version": "116",
8
+ "background": {
9
+ "service_worker": "background.js",
10
+ "type": "module"
11
+ },
12
+ "action": {
13
+ "default_title": "vcorp-chrome"
14
+ },
15
+ "permissions": [
16
+ "tabs",
17
+ "tabGroups",
18
+ "scripting",
19
+ "storage",
20
+ "alarms",
21
+ "offscreen",
22
+ "downloads",
23
+ "sidePanel",
24
+ "notifications",
25
+ "debugger"
26
+ ],
27
+ "optional_permissions": [
28
+ "nativeMessaging"
29
+ ],
30
+ "host_permissions": [
31
+ "<all_urls>"
32
+ ],
33
+ "icons": {
34
+ "16": "icons/icon16.png",
35
+ "48": "icons/icon48.png",
36
+ "128": "icons/icon128.png"
37
+ },
38
+ "side_panel": {
39
+ "default_path": "panel.html"
40
+ }
41
+ }