vcorp-chrome 1.7.0 → 1.8.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.
@@ -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
  //
@@ -870,6 +871,8 @@ function actText(cmd, p = {}) {
870
871
  case 'download': return '下载';
871
872
  case 'upload': return '上传文件';
872
873
  case 'eval': return '执行脚本';
874
+ case 'load': return `加载能力包 ${p.name || ''}`;
875
+ case 'call': return `${p.name || ''} ${p.action || ''}`;
873
876
  case 'wait': return '等待';
874
877
  case 'ask': return '请用户搭把手';
875
878
  // act 的每一步在批处理循环里单独同步过了(带「第 i/n 步」),
@@ -1904,6 +1907,69 @@ function sparse(text) {
1904
1907
  return n <= 3 && excerpt.replace(/\s+/g, '').length < 160;
1905
1908
  }
1906
1909
 
1910
+ // ---------- 能力包 ----------
1911
+ //
1912
+ // 装包的顺序:先核对页面在包的 hosts 里,再等页面自己的全局(waitFor)出现,
1913
+ // 最后注入扩展包里的文件、调 ready()。页面上已经是同一版本就不重复注入——
1914
+ // 重注会把上游里的模块状态(缓存、事件监听)再建一份。
1915
+
1916
+ function withTimeout(promise, ms, what) {
1917
+ let t;
1918
+ return Promise.race([
1919
+ promise,
1920
+ new Promise((_, reject) => {
1921
+ t = setTimeout(() => reject(err('TIMEOUT', `${what} 超过 ${Math.round(ms / 1000)} 秒没返回`)), ms);
1922
+ }),
1923
+ ]).finally(() => clearTimeout(t));
1924
+ }
1925
+
1926
+ const LOAD_WAIT_MS = 30000;
1927
+
1928
+ async function ensureLoad(id, def, { force = false } = {}) {
1929
+ const tab = await chrome.tabs.get(id);
1930
+ if (!loadHostAllowed(def, tab.url)) {
1931
+ throw err('LOAD_DENIED', `能力包 ${def.name} 只能用在 ${def.hosts.join('、')},这个标签页在 ${hostOf(tab.url) || '非网页地址'}`);
1932
+ }
1933
+ const probe = async () => (await chrome.scripting.executeScript({
1934
+ target: { tabId: id },
1935
+ world: 'MAIN',
1936
+ func: (name, waitFor) => ({
1937
+ page: !waitFor || typeof window[waitFor] !== 'undefined',
1938
+ version: (window.__vcorpLoads && window.__vcorpLoads[name] && window.__vcorpLoads[name].version) || '',
1939
+ }),
1940
+ args: [def.name, def.waitFor],
1941
+ }))[0]?.result;
1942
+
1943
+ const until = Date.now() + LOAD_WAIT_MS;
1944
+ let st = await probe();
1945
+ while (!st?.page) {
1946
+ if (Date.now() > until) throw err('LOAD_NOT_READY', `页面 ${LOAD_WAIT_MS / 1000} 秒内没准备好(缺 ${def.waitFor})。等编辑器加载完、或先打开一个工程再试`);
1947
+ await sleep(500);
1948
+ st = await probe();
1949
+ }
1950
+ if (force || st.version !== def.version) {
1951
+ await chrome.scripting.executeScript({ target: { tabId: id }, world: 'MAIN', files: def.files });
1952
+ }
1953
+ // ready() 说没准备好(比如编辑器还在打开工程)就接着等,和上面等 waitFor 共用一个期限
1954
+ const readyOnce = async () => (await withTimeout(chrome.scripting.executeScript({
1955
+ target: { tabId: id },
1956
+ world: 'MAIN',
1957
+ func: async (name) => {
1958
+ const load = window.__vcorpLoads && window.__vcorpLoads[name];
1959
+ if (!load) return { ok: false, reason: '注入后没注册上' };
1960
+ try { return await load.ready(); } catch (e) { return { ok: false, reason: String((e && e.message) || e) }; }
1961
+ },
1962
+ args: [def.name],
1963
+ }), LOAD_WAIT_MS, `${def.name}.ready`))[0]?.result;
1964
+ let ready = await readyOnce();
1965
+ while (!ready?.ok && Date.now() < until) {
1966
+ await sleep(1000);
1967
+ ready = await readyOnce();
1968
+ }
1969
+ if (!ready?.ok) throw err('LOAD_NOT_READY', `能力包 ${def.name} 没准备好:${ready?.reason || '未知原因'}`);
1970
+ return { version: def.version, ready };
1971
+ }
1972
+
1907
1973
  const HANDLERS = {
1908
1974
  async snapshot(p, tabId, ctx) {
1909
1975
  const id = await resolveTab(tabId);
@@ -2368,6 +2434,63 @@ const HANDLERS = {
2368
2434
  //
2369
2435
  // 换到 MAIN world 之后受页面 CSP 管:没设 CSP 的站点能用了,大站仍然拦——
2370
2436
  // 但那是真实且可解释的边界,不再是「哪儿都不能用」。
2437
+ // 能力包:把扩展包里的一套有类型的接口装进页面(见 load.js 顶部)。
2438
+ // 和 eval 的区别:注入的是扩展自己的文件(executeScript files),调用传的是结构化克隆的数据,
2439
+ // 没有字符串变代码这一步——所以云端可以开放它,页面 CSP 也拦不住。
2440
+ async load(p, tabId) {
2441
+ const { name } = checkLoadParams(p);
2442
+ const id = await resolveTab(tabId);
2443
+ const def = loadDef(name);
2444
+ const info = await ensureLoad(id, def, { force: !!p.reload });
2445
+ const lines = [
2446
+ `已加载能力包 ${def.name}(${def.title})${info.version}`,
2447
+ `页面状态:${JSON.stringify(info.ready)}`,
2448
+ `用 call {name:"${def.name}", action, payload} 调用。`,
2449
+ `只读(${def.actions.read.length}):${def.actions.read.join(' ')}`,
2450
+ `改动设计(${def.actions.write.length}):${def.actions.write.join(' ')}`,
2451
+ ];
2452
+ return { untrusted: true, text: lines.join('\n') };
2453
+ },
2454
+
2455
+ async call(p, tabId) {
2456
+ const { name, action, payload, timeout, kind } = checkCallParams(p);
2457
+ const def = loadDef(name);
2458
+ const id = await resolveTab(tabId);
2459
+ const run = () => withTimeout(chrome.scripting.executeScript({
2460
+ target: { tabId: id },
2461
+ world: 'MAIN',
2462
+ func: async (name, action, payload) => {
2463
+ const load = window.__vcorpLoads && window.__vcorpLoads[name];
2464
+ if (!load) return { gone: true };
2465
+ try {
2466
+ const v = await load.call(action, payload);
2467
+ let text;
2468
+ try { text = JSON.stringify(v === undefined ? null : v); } catch { text = JSON.stringify(String(v)); }
2469
+ return { ok: true, text };
2470
+ } catch (e) {
2471
+ return { ok: false, code: (e && e.code) || '', message: String((e && e.message) || e) };
2472
+ }
2473
+ },
2474
+ args: [name, action, payload],
2475
+ }), timeout, `${name}.${action}`).then((r) => r?.[0]?.result);
2476
+
2477
+ let result;
2478
+ try {
2479
+ await ensureLoad(id, def);
2480
+ result = await run();
2481
+ // 页面刚好在两步之间刷新了:再装一次,只重试这一回
2482
+ if (result?.gone) { await ensureLoad(id, def); result = await run(); }
2483
+ } catch (e) {
2484
+ if (e.code) throw e;
2485
+ throw err('LOAD_GONE', `${name}.${action} 没跑完:${e.message}(页面可能刷新或关掉了,结果未知,先读一下现状再决定要不要重做)`);
2486
+ }
2487
+ if (!result || result.gone) throw err('LOAD_GONE', `页面里找不到能力包 ${name}`);
2488
+ if (!result.ok) {
2489
+ throw err(result.code === 'LOAD_DENIED' ? 'LOAD_DENIED' : 'LOAD_ERROR', `${name}.${action} 失败:${result.message}`);
2490
+ }
2491
+ return { untrusted: true, text: result.text, ...(kind === 'write' ? { audit: `${name}.${action}` } : {}) };
2492
+ },
2493
+
2371
2494
  async eval(p, tabId) {
2372
2495
  const id = await resolveTab(tabId);
2373
2496