vcorp-chrome 1.5.0 → 1.6.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.
@@ -492,9 +492,24 @@ ref 就作废了**(ref 绑定在某一份快照上)。
492
492
  → { "outcome": "continued|cancelled|timed_out|completed|disabled", "text": "…" }
493
493
  ```
494
494
 
495
+ 扫码、验证码、滑块不带 `kind`:这种只能本人做,免确认开关管不到。
496
+ 对外动作(发消息、投递、发帖)要写 `"kind": "confirm"`,别的值一律当成没写。
497
+
498
+ ```jsonc
499
+ { "cmd": "ask", "params": {
500
+ "kind": "confirm",
501
+ "title": "确认发消息",
502
+ "prompt": "向对方发送下面这段话。点「我完成了」就发出去。"
503
+ } }
504
+ ```
505
+
506
+ 云端通道里,用户开了「对外动作不再逐条确认」,或在这个任务的浮条上点过「本任务内都同意」,
507
+ 后续 `kind: "confirm"` 的 ask 不再弹窗,直接 `{ "outcome": "continued", "auto": true }`。
508
+ 支付确认不走这条命令,没有跳过的办法。
509
+
495
510
  | outcome | 含义 | agent 该怎么办 |
496
511
  |---|---|---|
497
- | `continued` | 用户点了「我完成了」 | 继续 |
512
+ | `continued` | 用户点了「我完成了」,或免确认直接放行(此时还有 `auto: true`) | 继续;`auto: true` 时在交付里记一笔 |
498
513
  | `completed` | `until` 判据自动命中 | 继续 |
499
514
  | `cancelled` | 用户点了取消 | **停止这个任务**,不要换个方式重试 |
500
515
  | `timed_out` | 无人响应 | 用户不在电脑前 |
@@ -2626,6 +2626,7 @@ const HANDLERS = {
2626
2626
  const started = Date.now();
2627
2627
  await chrome.tabs.sendMessage(id, {
2628
2628
  __hcAsk: 'show', title: p.title, prompt: p.prompt, timeout, wantNote: p.wantNote !== false,
2629
+ ...(p.kind === 'confirm' ? { offerAlways: true } : {}),
2629
2630
  });
2630
2631
  const panel = pollPanel(id, timeout);
2631
2632
 
@@ -2650,6 +2651,7 @@ const HANDLERS = {
2650
2651
 
2651
2652
  return {
2652
2653
  outcome: res.outcome,
2654
+ ...(res.always ? { always: true } : {}),
2653
2655
  text: `${head}${res.note ? `\n用户留言:${res.note}` : ''}\n\n${snap.text}`,
2654
2656
  };
2655
2657
  },
@@ -334,6 +334,9 @@ export function checkCommand(cmd, params, { hosts, origin }) {
334
334
  return p;
335
335
  case 'ask':
336
336
  p.timeout = Math.min(Math.max(Number(p.timeout) || 300000, 5000), 600000);
337
+ // 只有对外动作的确认可以被免确认开关跳过。别的值(含没写)一律当成人必须在场:
338
+ // 扫码、验证码、滑块不能因为用户开了开关就被静默放行。
339
+ if (p.kind !== 'confirm') delete p.kind;
337
340
  return p;
338
341
  case 'sites': {
339
342
  const { sites, bad } = cleanSites(p.add);
@@ -5,7 +5,9 @@
5
5
  // 所以这里的每一条命令都按「对面可能已经被攻破」来处理,规矩全在 cloud-policy.js:
6
6
  // 只有页面操作那一小组命令、只能停在允许名单(以及本任务里批准的网站)上、只能碰自己开的标签页、
7
7
  // 上传只能用服务器上的素材。新任务要用户点头;开了自动允许则新任务和它一开始的网站直接放行。
8
- // 任务开始之后追加的网站、本地文件夹,仍要逐个点头。没有预设的平台表:要去哪个网站,任务自己申请。
8
+ // 任务开始之后追加的网站、本地文件夹,仍要逐个点头。带 kind=confirm 的 ask 是对外动作的确认:
9
+ // 用户开了免确认,或在这个任务里点过「本任务内都同意」,就直接放行。扫码和验证码不带 kind,照旧等人。
10
+ // 没有预设的平台表:要去哪个网站,任务自己申请。
9
11
  //
10
12
  // 身份:配对时在这里生成一对 ECDSA P-256 密钥,私钥以不可导出的 CryptoKey 存在
11
13
  // IndexedDB,服务器只拿到公钥。每次连接对服务器给的随机数签名,签名里带上服务器
@@ -153,7 +155,7 @@ async function confirmClaim() {
153
155
  await setCfg({
154
156
  origin: c.origin, deviceId: r.deviceId, account: String(r.account || c.account), pairedAt: Date.now(),
155
157
  // 换绑时沿用之前允许的网站;第一次配对名单是空的,等任务申请、用户点允许
156
- sites: policy.cleanAllowList(old?.sites), auto: false, paused: false,
158
+ sites: policy.cleanAllowList(old?.sites), auto: false, autoConfirm: false, paused: false,
157
159
  });
158
160
  await chrome.storage.session.remove(CLAIM);
159
161
  await log({ ev: 'paired', account: r.account || c.account, origin: c.origin });
@@ -724,12 +726,27 @@ async function exec(m) {
724
726
  return policy.redactOffList(tab.pendingUrl || tab.url);
725
727
  }
726
728
 
729
+ // 对外动作的确认:全局开关或本任务里点过「都同意」,就不弹浮条。
730
+ // 不带 kind 的 ask(扫码、验证码)不满足条件,继续往下弹窗。
731
+ if (m.cmd === 'ask' && p.kind === 'confirm' && (cfg.autoConfirm || updated.confirmAll)) {
732
+ await log({ ev: 'ask_auto', job: jobId, msg: String(p.prompt || '').slice(0, 160) });
733
+ return { outcome: 'continued', auto: true, text: '✅ 用户已设置对外动作免确认,直接继续。' };
734
+ }
735
+
727
736
  // 浮条上写明是谁在问:云端发来的文字可能是钓鱼,用户得知道这不是本机 agent 说的
728
737
  if (m.cmd === 'ask') p.title = `☁ ${policy.hostOf(cfg.origin)}:${String(p.title || '需要你搭把手').slice(0, 60)}`;
729
738
  const data = m.cmd === 'upload'
730
739
  ? await uploadMedia(p, tid, sid, label)
731
740
  : await deps.run(m.cmd, p, tid, sid, label);
732
741
 
742
+ // 「本任务内都同意」只记在这个任务上。任务结束时整条记录删掉,不会带到下一个任务。
743
+ // always 是扩展内部的记号,不回给服务器。
744
+ if (m.cmd === 'ask' && data?.always) {
745
+ await updateTask(jobId, (t) => { t.confirmAll = true; });
746
+ await log({ ev: 'confirm_task', job: jobId, msg: String(p.prompt || '').slice(0, 160) });
747
+ delete data.always;
748
+ }
749
+
733
750
  // 结果出门前再查一遍本任务所有标签页:有一页跳出了白名单,整份结果都不能出去
734
751
  const after = (await getTasks())[jobId]?.tabs || [];
735
752
  for (const id of after) {
@@ -814,7 +831,7 @@ async function panelState() {
814
831
  getCfg(), chrome.storage.session.get(STATUS), chrome.storage.session.get(CLAIM), getTasks(), chrome.storage.local.get(LOG),
815
832
  ]);
816
833
  return {
817
- cfg: cfg && { origin: cfg.origin, account: cfg.account, sites: allowOf(cfg), auto: cfg.auto, paused: cfg.paused, pairedAt: cfg.pairedAt },
834
+ cfg: cfg && { origin: cfg.origin, account: cfg.account, sites: allowOf(cfg), auto: cfg.auto, autoConfirm: !!cfg.autoConfirm, paused: cfg.paused, pairedAt: cfg.pairedAt },
818
835
  status: status || { state: 'off' },
819
836
  claim: claimed && claimed.expiresAt > Date.now() ? { origin: claimed.origin, account: claimed.account } : null,
820
837
  tasks: Object.values(tasks).sort((a, b) => b.lastAt - a.lastAt),
@@ -856,6 +873,11 @@ async function onPanel(m) {
856
873
  await log({ ev: 'site_off', msg: host });
857
874
  return panelState();
858
875
  }
876
+ case 'setAutoConfirm': {
877
+ await patchCfg({ autoConfirm: !!m.autoConfirm });
878
+ await log({ ev: m.autoConfirm ? 'confirm_on' : 'confirm_off' });
879
+ return panelState();
880
+ }
859
881
  case 'setAuto': {
860
882
  await patchCfg({ auto: !!m.auto });
861
883
  await log({ ev: m.auto ? 'auto_on' : 'auto_off' });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "vcorp-chrome",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "description": "让 AI agent 操控你已登录的 Chrome。基于花叔的 huashu-chrome(MIT)修改。",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq6xRqEwG4yTuHRG1Zah3j//T3B72XnazyZ+9fOCa5P4bBLPmNBN7YAGL5WnjtPVJefXT9r2Ol0qvNztayDG3dMjK6AZoWccgKa5rhxcvWgFUjanXtmBBQ0qgYwNhuEJSkdinLLtyUg6YmK7Mn72NWpfa/NfqNHdK5AHslvvtJ50iQAYmabl2Nd3vOJ9DxtLBt8a2DfoGPWizgEB3+gkqm2PB8LywwwoiGKd7acr1wZEWmWK9/s0YWdzIstjlvk5AO5XFn6EtEzW8tAAOwBka9239pcyj0kEogWctHLJn7p9VFru38r+I1USyGn6u9XQ68n9edWddxBYokETCqEMT9QIDAQAB",
7
7
  "minimum_chrome_version": "116",
package/extension/mark.js CHANGED
@@ -213,6 +213,9 @@
213
213
  .ask .ok:hover { background: #ea580c; }
214
214
  .ask .no { background: #fff; color: #666; border-color: #e2e2e2; }
215
215
  .ask .no:hover { background: #f6f6f6; }
216
+ .ask.confirm { width: 420px; }
217
+ .ask .all { background: #fff; color: #9a3412; border-color: #fed7aa; font-size: 12px; padding: 7px 10px; }
218
+ .ask .all:hover { background: #fff7ed; }
216
219
  /* 支付确认:同一个浮条换一身红。这类打断一年遇不上几次,
217
220
  必须一眼就和「帮我解个验证码」区分开——看错了是要花钱的。 */
218
221
  .ask.danger .head { background: linear-gradient(135deg,#fef2f2,#fee2e2); }
@@ -228,6 +231,8 @@
228
231
  .ask .note { background: #2a2a2c; border-color: #3a3a3c; color: #f2f2f2; }
229
232
  .ask .no { background: #2a2a2c; color: #ccc; border-color: #3a3a3c; }
230
233
  .ask .no:hover { background: #333; }
234
+ .ask .all { background: #2a2a2c; color: #fdba74; border-color: #7c2d12; }
235
+ .ask .all:hover { background: #3b2a1a; }
231
236
  .ask.danger .head { background: linear-gradient(135deg,#3f1d1d,#2a1414); }
232
237
  .ask .what { background: #2a2a2c; }
233
238
  .ask .amount { color: #f87171; }
@@ -546,11 +551,12 @@
546
551
  let askOutcome = null; // 已结算的结果,等 background 来取
547
552
  let askTimer = null;
548
553
 
549
- function closeAsk(result, note) {
554
+ function closeAsk(result, note, extra) {
550
555
  if (askEl) { askEl.remove(); askEl = null; }
551
556
  if (askTimer) { clearInterval(askTimer); askTimer = null; }
552
557
  askSettle = null;
553
558
  askOutcome = { outcome: result, note: note || '' };
559
+ if (extra?.always) askOutcome.always = true;
554
560
  maybeTeardown();
555
561
  }
556
562
 
@@ -578,6 +584,7 @@
578
584
  askEl.querySelector('.t').textContent = msg.title || `${chrome.runtime.getManifest().name} 需要你搭把手`;
579
585
  askEl.querySelector('.p').textContent = msg.prompt || '';
580
586
  if (msg.danger) askEl.classList.add('danger');
587
+ if (msg.offerAlways && !msg.danger) askEl.classList.add('confirm');
581
588
  if (msg.okText) askEl.querySelector('.ok').textContent = msg.okText;
582
589
  if (msg.noText) askEl.querySelector('.no').textContent = msg.noText;
583
590
  // 「点哪个按钮、在哪个站、多少钱」单独拎出来,不混在正文里。
@@ -609,6 +616,14 @@
609
616
 
610
617
  askEl.querySelector('.ok').addEventListener('click', () => closeAsk('continued', noteEl?.value));
611
618
  askEl.querySelector('.no').addEventListener('click', () => closeAsk('cancelled', noteEl?.value));
619
+ // 文案写死在扩展里,不接 agent 传来的字。支付确认不给这个按钮。
620
+ if (msg.offerAlways && !msg.danger) {
621
+ const always = document.createElement('button');
622
+ always.className = 'all';
623
+ always.textContent = '本任务内都同意';
624
+ askEl.querySelector('.ok').before(always);
625
+ always.addEventListener('click', () => closeAsk('continued', noteEl?.value, { always: true }));
626
+ }
612
627
 
613
628
  // 挂在 own 之外:会话区重画时不触碰 ask(输入框的焦点经不起挪动)
614
629
  wrap.querySelector('.dock').appendChild(askEl);
@@ -163,6 +163,10 @@
163
163
  <span class="txt">自动允许新任务<div class="sub">不再逐个弹窗确认。只在你信任这个账号时打开:新任务,以及它一开始就要去的网站,会直接放行。任务开始之后再加的网站,还有本地文件夹,仍然要你逐个点头</div></span>
164
164
  <input type="checkbox" class="toggle" id="cloudAuto" />
165
165
  </label>
166
+ <label class="sw-row">
167
+ <span class="txt">对外动作不再逐条确认<div class="sub">发消息、投递、发帖之前不再问你。扫码、验证码仍然要你本人来。付款一律不做</div></span>
168
+ <input type="checkbox" class="toggle" id="cloudAutoConfirm" />
169
+ </label>
166
170
  <label class="sw-row">
167
171
  <span class="txt">暂停<div class="sub">立刻断开,云端的命令一条都不接,直到你再打开</div></span>
168
172
  <input type="checkbox" class="toggle" id="cloudPause" />
@@ -251,7 +251,9 @@ const LOG_TEXT = {
251
251
  paired: '配对', unpaired: '解除配对', approved: '批准任务', denied: '拒绝任务', ended: '任务结束',
252
252
  sites_on: '允许了新网站', sites_off: '拒绝了新网站', sites_timeout: '新网站没回应',
253
253
  folders_on: '允许了本地文件夹', folders_off: '拒绝了本地文件夹', folders_timeout: '本地文件夹没回应',
254
- site_on: '网站加入允许名单', site_off: '从允许名单移除网站', auto_on: '打开自动允许', auto_off: '关闭自动允许', paused: '暂停', resumed: '恢复',
254
+ site_on: '网站加入允许名单', site_off: '从允许名单移除网站', auto_on: '打开自动允许', auto_off: '关闭自动允许',
255
+ confirm_on: '打开对外动作免确认', confirm_off: '关闭对外动作免确认', ask_auto: '免确认放行', confirm_task: '本任务内都同意',
256
+ paused: '暂停', resumed: '恢复',
255
257
  };
256
258
 
257
259
  const PERM_WORD = { granted: '可读', prompt: '需要恢复访问', denied: '已拒绝', unknown: '打不开' };
@@ -303,6 +305,7 @@ function renderCloud(s) {
303
305
  $('cloudSub').textContent = s.status?.state === 'error' || s.status?.state === 'paused'
304
306
  ? s.status.message : `账号「${s.cfg.account || ''}」`;
305
307
  $('cloudAuto').checked = !!s.cfg.auto;
308
+ $('cloudAutoConfirm').checked = !!s.cfg.autoConfirm;
306
309
  $('cloudPause').checked = !!s.cfg.paused;
307
310
 
308
311
  // 允许名单:用户点过「允许」的网站,含子域。没有预设的平台。
@@ -396,6 +399,7 @@ $('claimYes').onclick = async () => {
396
399
  };
397
400
  $('claimNo').onclick = () => cloudCall('cancelClaim');
398
401
  $('cloudAuto').onchange = (e) => cloudCall('setAuto', { auto: e.target.checked });
402
+ $('cloudAutoConfirm').onchange = (e) => cloudCall('setAutoConfirm', { autoConfirm: e.target.checked });
399
403
  $('cloudPause').onchange = (e) => cloudCall('setPaused', { paused: e.target.checked });
400
404
  $('cloudReconn').onclick = () => cloudCall('reconnect');
401
405
  $('cloudUnpair').onclick = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vcorp-chrome",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "让任何 AI agent 操控你自己的 Chrome——带着全部登录态。MCP + Chrome 扩展,Claude Code / Codex / Cursor / Gemini CLI 通用。",
5
5
  "type": "module",
6
6
  "bin": {