vcorp-chrome 1.7.0 → 2.0.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.
@@ -15,6 +15,7 @@ import { CRED_URL, redactCreds } from './redact.js';
15
15
  import { identityOf, stripMarkPrefix } from './identity.js';
16
16
  import { validateScript, condText, repeatMax, EXEC_BUDGET, collectRefs, toRecipe } from './script.js';
17
17
  import { initCloud, ensureCloud } from './cloud.js';
18
+ import { loadDef, loadHostAllowed, checkLoadParams, checkCallParams } from './load.js';
18
19
 
19
20
  // ---------- 连接层 ----------
20
21
  //
@@ -658,6 +659,22 @@ async function runCloud(cmd, params, tabId, sid, label) {
658
659
 
659
660
  const liveList = async () => (await chrome.storage.session.get(LIVE_SIDS))[LIVE_SIDS] || [];
660
661
 
662
+ // 桥的名单只有桥能更新,而桥一死就没人再推了——扩展不自己清,死会话会一直算在线,
663
+ // 它们留在页面上的标记也摘不掉。只在确认重连失败之后调(见 hc-keepalive),
664
+ // 瞬断时不能清,否则标记会闪一下。桥回来时会在 welcome 后补一份真名单。
665
+ async function dropBridgeSessions() {
666
+ const { [BRIDGE_SIDS]: bridge = [] } = await chrome.storage.session.get(BRIDGE_SIDS).catch(() => ({}));
667
+ if (bridge.length) await onBridgeEvent({ event: 'sessions', live: [] });
668
+ }
669
+
670
+ // 面板显示用:没连着桥时只有云端名单可信。比 dropBridgeSessions 更早生效,
671
+ // 否则「未连接」旁边最多还会挂 30 秒已经过期的本机 agent。
672
+ async function shownSids() {
673
+ if (await connected()) return liveList();
674
+ const { [CLOUD_SIDS]: cloud = [] } = await chrome.storage.session.get(CLOUD_SIDS);
675
+ return cloud;
676
+ }
677
+
661
678
  // 这个标签页此刻的主人们。只算**还连着**的会话——判据和 liveOwnersOf 一致:
662
679
  // 已结束的会话留下的槽不算数,否则关掉一个终端窗口之后,它的标记会永远挂在那儿。
663
680
  //
@@ -870,6 +887,8 @@ function actText(cmd, p = {}) {
870
887
  case 'download': return '下载';
871
888
  case 'upload': return '上传文件';
872
889
  case 'eval': return '执行脚本';
890
+ case 'load': return `加载能力包 ${p.name || ''}`;
891
+ case 'call': return `${p.name || ''} ${p.action || ''}`;
873
892
  case 'wait': return '等待';
874
893
  case 'ask': return '请用户搭把手';
875
894
  // act 的每一步在批处理循环里单独同步过了(带「第 i/n 步」),
@@ -1904,6 +1923,69 @@ function sparse(text) {
1904
1923
  return n <= 3 && excerpt.replace(/\s+/g, '').length < 160;
1905
1924
  }
1906
1925
 
1926
+ // ---------- 能力包 ----------
1927
+ //
1928
+ // 装包的顺序:先核对页面在包的 hosts 里,再等页面自己的全局(waitFor)出现,
1929
+ // 最后注入扩展包里的文件、调 ready()。页面上已经是同一版本就不重复注入——
1930
+ // 重注会把上游里的模块状态(缓存、事件监听)再建一份。
1931
+
1932
+ function withTimeout(promise, ms, what) {
1933
+ let t;
1934
+ return Promise.race([
1935
+ promise,
1936
+ new Promise((_, reject) => {
1937
+ t = setTimeout(() => reject(err('TIMEOUT', `${what} 超过 ${Math.round(ms / 1000)} 秒没返回`)), ms);
1938
+ }),
1939
+ ]).finally(() => clearTimeout(t));
1940
+ }
1941
+
1942
+ const LOAD_WAIT_MS = 30000;
1943
+
1944
+ async function ensureLoad(id, def, { force = false } = {}) {
1945
+ const tab = await chrome.tabs.get(id);
1946
+ if (!loadHostAllowed(def, tab.url)) {
1947
+ throw err('LOAD_DENIED', `能力包 ${def.name} 只能用在 ${def.hosts.join('、')},这个标签页在 ${hostOf(tab.url) || '非网页地址'}`);
1948
+ }
1949
+ const probe = async () => (await chrome.scripting.executeScript({
1950
+ target: { tabId: id },
1951
+ world: 'MAIN',
1952
+ func: (name, waitFor) => ({
1953
+ page: !waitFor || typeof window[waitFor] !== 'undefined',
1954
+ version: (window.__vcorpLoads && window.__vcorpLoads[name] && window.__vcorpLoads[name].version) || '',
1955
+ }),
1956
+ args: [def.name, def.waitFor],
1957
+ }))[0]?.result;
1958
+
1959
+ const until = Date.now() + LOAD_WAIT_MS;
1960
+ let st = await probe();
1961
+ while (!st?.page) {
1962
+ if (Date.now() > until) throw err('LOAD_NOT_READY', `页面 ${LOAD_WAIT_MS / 1000} 秒内没准备好(缺 ${def.waitFor})。等编辑器加载完、或先打开一个工程再试`);
1963
+ await sleep(500);
1964
+ st = await probe();
1965
+ }
1966
+ if (force || st.version !== def.version) {
1967
+ await chrome.scripting.executeScript({ target: { tabId: id }, world: 'MAIN', files: def.files });
1968
+ }
1969
+ // ready() 说没准备好(比如编辑器还在打开工程)就接着等,和上面等 waitFor 共用一个期限
1970
+ const readyOnce = async () => (await withTimeout(chrome.scripting.executeScript({
1971
+ target: { tabId: id },
1972
+ world: 'MAIN',
1973
+ func: async (name) => {
1974
+ const load = window.__vcorpLoads && window.__vcorpLoads[name];
1975
+ if (!load) return { ok: false, reason: '注入后没注册上' };
1976
+ try { return await load.ready(); } catch (e) { return { ok: false, reason: String((e && e.message) || e) }; }
1977
+ },
1978
+ args: [def.name],
1979
+ }), LOAD_WAIT_MS, `${def.name}.ready`))[0]?.result;
1980
+ let ready = await readyOnce();
1981
+ while (!ready?.ok && Date.now() < until) {
1982
+ await sleep(1000);
1983
+ ready = await readyOnce();
1984
+ }
1985
+ if (!ready?.ok) throw err('LOAD_NOT_READY', `能力包 ${def.name} 没准备好:${ready?.reason || '未知原因'}`);
1986
+ return { version: def.version, ready };
1987
+ }
1988
+
1907
1989
  const HANDLERS = {
1908
1990
  async snapshot(p, tabId, ctx) {
1909
1991
  const id = await resolveTab(tabId);
@@ -2368,6 +2450,63 @@ const HANDLERS = {
2368
2450
  //
2369
2451
  // 换到 MAIN world 之后受页面 CSP 管:没设 CSP 的站点能用了,大站仍然拦——
2370
2452
  // 但那是真实且可解释的边界,不再是「哪儿都不能用」。
2453
+ // 能力包:把扩展包里的一套有类型的接口装进页面(见 load.js 顶部)。
2454
+ // 和 eval 的区别:注入的是扩展自己的文件(executeScript files),调用传的是结构化克隆的数据,
2455
+ // 没有字符串变代码这一步——所以云端可以开放它,页面 CSP 也拦不住。
2456
+ async load(p, tabId) {
2457
+ const { name } = checkLoadParams(p);
2458
+ const id = await resolveTab(tabId);
2459
+ const def = loadDef(name);
2460
+ const info = await ensureLoad(id, def, { force: !!p.reload });
2461
+ const lines = [
2462
+ `已加载能力包 ${def.name}(${def.title})${info.version}`,
2463
+ `页面状态:${JSON.stringify(info.ready)}`,
2464
+ `用 call {name:"${def.name}", action, payload} 调用。`,
2465
+ `只读(${def.actions.read.length}):${def.actions.read.join(' ')}`,
2466
+ `改动设计(${def.actions.write.length}):${def.actions.write.join(' ')}`,
2467
+ ];
2468
+ return { untrusted: true, text: lines.join('\n') };
2469
+ },
2470
+
2471
+ async call(p, tabId) {
2472
+ const { name, action, payload, timeout, kind } = checkCallParams(p);
2473
+ const def = loadDef(name);
2474
+ const id = await resolveTab(tabId);
2475
+ const run = () => withTimeout(chrome.scripting.executeScript({
2476
+ target: { tabId: id },
2477
+ world: 'MAIN',
2478
+ func: async (name, action, payload) => {
2479
+ const load = window.__vcorpLoads && window.__vcorpLoads[name];
2480
+ if (!load) return { gone: true };
2481
+ try {
2482
+ const v = await load.call(action, payload);
2483
+ let text;
2484
+ try { text = JSON.stringify(v === undefined ? null : v); } catch { text = JSON.stringify(String(v)); }
2485
+ return { ok: true, text };
2486
+ } catch (e) {
2487
+ return { ok: false, code: (e && e.code) || '', message: String((e && e.message) || e) };
2488
+ }
2489
+ },
2490
+ args: [name, action, payload],
2491
+ }), timeout, `${name}.${action}`).then((r) => r?.[0]?.result);
2492
+
2493
+ let result;
2494
+ try {
2495
+ await ensureLoad(id, def);
2496
+ result = await run();
2497
+ // 页面刚好在两步之间刷新了:再装一次,只重试这一回
2498
+ if (result?.gone) { await ensureLoad(id, def); result = await run(); }
2499
+ } catch (e) {
2500
+ if (e.code) throw e;
2501
+ throw err('LOAD_GONE', `${name}.${action} 没跑完:${e.message}(页面可能刷新或关掉了,结果未知,先读一下现状再决定要不要重做)`);
2502
+ }
2503
+ if (!result || result.gone) throw err('LOAD_GONE', `页面里找不到能力包 ${name}`);
2504
+ if (!result.ok) {
2505
+ throw err(result.code === 'LOAD_DENIED' ? 'LOAD_DENIED' : 'LOAD_ERROR', `${name}.${action} 失败:${result.message}`);
2506
+ }
2507
+ return { untrusted: true, text: result.text, ...(kind === 'write' ? { audit: `${name}.${action}` } : {}) };
2508
+ },
2509
+
2371
2510
  async eval(p, tabId) {
2372
2511
  const id = await resolveTab(tabId);
2373
2512
 
@@ -2734,7 +2873,9 @@ chrome.alarms?.onAlarm.addListener(async () => {
2734
2873
  const kicked = await chrome.runtime.sendMessage({ __hcBridge: 'kick' }).catch(() => null);
2735
2874
  if (kicked?.connected) return setBadge(true);
2736
2875
  await directConnect();
2737
- setBadge(await connected());
2876
+ const up = await connected();
2877
+ setBadge(up);
2878
+ if (!up) await dropBridgeSessions();
2738
2879
  });
2739
2880
  chrome.tabs.onRemoved.addListener(async (tabId) => {
2740
2881
  // 全局槽 + 所有会话级槽一次扫清,别留指向死 tab 的槽
@@ -2845,7 +2986,7 @@ chrome.runtime.onMessage.addListener((m, sender, sendResponse) => {
2845
2986
  if (m.__hcPopup === 'sessions') {
2846
2987
  (async () => {
2847
2988
  try {
2848
- const [all, lives] = await Promise.all([chrome.storage.local.get(null), liveList()]);
2989
+ const [all, lives] = await Promise.all([chrome.storage.local.get(null), shownSids()]);
2849
2990
  const clients = await chrome.storage.session.get(lives.map(sidClientKey));
2850
2991
  const rows = [];
2851
2992
  for (const sid of lives) {