vcorp-chrome 2.0.0 → 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/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/extension/cloud-policy.js +9 -0
- package/extension/cloud.js +90 -4
- package/extension/desktop-policy.js +19 -13
- package/extension/manifest.json +41 -41
- package/extension/panel.html +105 -46
- package/extension/panel.js +55 -62
- package/native-host/full.js +217 -0
- package/native-host/host.js +36 -3
- package/package.json +53 -53
- package/src/cli.js +16 -1
- package/src/lib/paths.js +15 -0
|
@@ -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);
|
package/extension/cloud.js
CHANGED
|
@@ -672,7 +672,30 @@ const DESK_SNAPS = 4; // 每个任务记几份快照的户口。户口
|
|
|
672
672
|
|
|
673
673
|
const deskInfo = async () => (await chrome.storage.local.get(DESK))[DESK] || null;
|
|
674
674
|
|
|
675
|
-
|
|
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) {
|
|
676
699
|
if (!chrome.runtime.sendNativeMessage) {
|
|
677
700
|
return Promise.reject(err('NO_DEVICE', '这个浏览器没有启用桌面能力。请用户在扩展侧栏「桌面应用」里点「启用桌面能力」。'));
|
|
678
701
|
}
|
|
@@ -812,8 +835,70 @@ function decideDesk(jobId, allow) {
|
|
|
812
835
|
|
|
813
836
|
const windowsMap = (list) => Object.fromEntries((Array.isArray(list) ? list : []).map((w) => [w.id, { app: w.app_name, title: w.title || '' }]));
|
|
814
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
|
+
|
|
815
899
|
async function desktopCmd(p, task, cfg) {
|
|
816
|
-
if (!(await desktopChild())) throw err('NO_DEVICE',
|
|
900
|
+
if (!(await desktopChild())) throw err('NO_DEVICE', NO_DESK);
|
|
901
|
+
if (await hostFull()) return desktopFull(p, task);
|
|
817
902
|
if (p.steps !== undefined) throw err('DESKTOP_DENIED', '云端的桌面命令一条一条发,不支持 steps');
|
|
818
903
|
const session = `cloud-${task.jobId}`;
|
|
819
904
|
const desk = { snaps: { ...(task.desk?.snaps || {}) }, windows: { ...(task.desk?.windows || {}) } };
|
|
@@ -864,7 +949,7 @@ async function handle(m) {
|
|
|
864
949
|
let from = self;
|
|
865
950
|
try {
|
|
866
951
|
const child = await desktopChild();
|
|
867
|
-
const where = routeHere({ ...m, cap:
|
|
952
|
+
const where = routeHere({ ...m, cap: ['desktop', 'shell', 'file'].includes(m.cmd) ? 'desktop' : capOf(m) }, { self, children: child ? [child] : [] });
|
|
868
953
|
from = where.device;
|
|
869
954
|
const data = await exec(m);
|
|
870
955
|
let out = data;
|
|
@@ -904,7 +989,7 @@ async function exec(m) {
|
|
|
904
989
|
|
|
905
990
|
if (m.cmd === 'devices') return devicesCmd(cfg);
|
|
906
991
|
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:
|
|
992
|
+
const task = await taskFor(m.job, cfg, reason, { allowEmpty: ['folders', 'apps', 'desktop', 'shell', 'file'].includes(m.cmd) });
|
|
908
993
|
const jobId = task.jobId;
|
|
909
994
|
const updated = await updateTask(jobId, (t) => { t.count += 1; t.lastAt = Date.now(); });
|
|
910
995
|
if (!updated) throw err('CLOUD_DENIED', '这个任务已经结束');
|
|
@@ -916,6 +1001,7 @@ async function exec(m) {
|
|
|
916
1001
|
if (m.cmd === 'folders') return requestFolders(updated, p, cfg);
|
|
917
1002
|
if (m.cmd === 'apps') return requestApps(updated, p, cfg);
|
|
918
1003
|
if (m.cmd === 'desktop') return desktopCmd(p, updated, cfg);
|
|
1004
|
+
if (m.cmd === 'shell' || m.cmd === 'file') return fullCmd(m.cmd, p);
|
|
919
1005
|
if (m.cmd === 'fs_list' || m.cmd === 'fs_read') return fsCmd(m.cmd, p, updated);
|
|
920
1006
|
const sid = sidOf(jobId);
|
|
921
1007
|
const label = labelOf(cfg.origin);
|
|
@@ -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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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))) {
|
package/extension/manifest.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"manifest_version": 3,
|
|
3
|
-
"name": "vcorp-chrome",
|
|
4
|
-
"version": "2.
|
|
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.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
|
+
}
|