vcorp-chrome 2.2.0 → 2.3.5
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/README.md +1 -0
- package/docs/macOS/346/241/214/351/235/242/350/203/275/345/212/233.md +109 -0
- package/docs//344/272/221/347/253/257/345/217/221/345/270/203.md +2 -2
- package/extension/background.js +11 -4
- package/extension/cloud-policy.js +13 -4
- package/extension/cloud.js +1385 -1314
- package/extension/content.js +41 -8
- package/extension/device.js +35 -0
- package/extension/folders.js +30 -0
- package/extension/manifest.json +1 -1
- package/extension/mount.html +1 -1
- package/extension/panel.html +15 -2
- package/extension/panel.js +48 -6
- package/package.json +1 -1
- package/src/lib/native-install.js +10 -2
package/extension/content.js
CHANGED
|
@@ -702,6 +702,36 @@
|
|
|
702
702
|
}
|
|
703
703
|
};
|
|
704
704
|
|
|
705
|
+
// ---------- 穿透 shadow 的命中检测 ----------
|
|
706
|
+
//
|
|
707
|
+
// document.elementFromPoint 碰到 shadow 只返回最外层宿主,Node.contains 又不跨
|
|
708
|
+
// shadow 边界。两个一起用,shadow 里的控件永远「被自己的宿主遮挡」——微前端
|
|
709
|
+
// (wujie 把整个子应用放进 <wujie-app> 的 shadow root)上每个按钮都点不了。
|
|
710
|
+
// 不能简单放行「命中宿主」:宿主里盖着弹层时那才是真遮挡,所以要逐层钻到底。
|
|
711
|
+
// closed root 读不到、停在宿主上:快照本来也进不去,那里头的元素拿不到 ref。
|
|
712
|
+
function deepElementFromPoint(x, y) {
|
|
713
|
+
let hit = document.elementFromPoint(x, y);
|
|
714
|
+
while (hit?.shadowRoot) {
|
|
715
|
+
const inner = hit.shadowRoot.elementFromPoint(x, y);
|
|
716
|
+
if (!inner || inner === hit) break;
|
|
717
|
+
hit = inner;
|
|
718
|
+
}
|
|
719
|
+
return hit;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// 沿渲染树往上走(assignedSlot 优先):a 是否在渲染上包含 n。
|
|
723
|
+
// 只走 parentNode 的话,shadow 里的容器「不包含」塞进它 <slot> 的内容。
|
|
724
|
+
function flatContains(a, n) {
|
|
725
|
+
for (; n; n = n.assignedSlot || n.parentNode || n.host) if (n === a) return true;
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 点这个坐标会打到别的东西上时返回那个东西,否则 null
|
|
730
|
+
function occluderAt(el, x, y) {
|
|
731
|
+
const hit = deepElementFromPoint(x, y);
|
|
732
|
+
return hit && !flatContains(el, hit) && !flatContains(hit, el) ? hit : null;
|
|
733
|
+
}
|
|
734
|
+
|
|
705
735
|
function realClick(el) {
|
|
706
736
|
const r = el.getBoundingClientRect();
|
|
707
737
|
const x = r.left + r.width / 2;
|
|
@@ -710,8 +740,8 @@
|
|
|
710
740
|
// 真实点击的 event.target 是光标下**最内层**的元素,事件再往上冒泡。
|
|
711
741
|
// 直接派发在容器上,target 就成了容器本身——组件按 event.target 分支的逻辑
|
|
712
742
|
// 会走进另一条路。react-select 的 Control 就读 event.target.tagName。
|
|
713
|
-
const inner =
|
|
714
|
-
if (inner && el
|
|
743
|
+
const inner = deepElementFromPoint(x, y);
|
|
744
|
+
if (inner && flatContains(el, inner)) el = inner;
|
|
715
745
|
const base = { bubbles: true, cancelable: true, composed: true, view: window, clientX: x, clientY: y, button: 0 };
|
|
716
746
|
const ptr = { ...base, pointerId: 1, pointerType: 'mouse', isPrimary: true };
|
|
717
747
|
|
|
@@ -925,15 +955,14 @@
|
|
|
925
955
|
|
|
926
956
|
// 虚拟光标(mark.js,和本文件同一个 isolated world):坐标就地递过去,
|
|
927
957
|
// 零消息往返。L1 和 L2 都走 locate,所以两条路的光标一致。
|
|
928
|
-
//
|
|
958
|
+
// **必须在下面的遮挡检测之前调**——坐标落在驾驶舱底下时它要
|
|
929
959
|
// 先让路(pointer-events:none 同步生效),否则遮挡检测会把我们自己的
|
|
930
960
|
// 面板当成遮挡物,L2 的真实点击更会直接点进面板里。
|
|
931
961
|
window.__hcCursor?.(x, y, ({ click: 'click', type: 'type', select: 'type', key: 'key' })[p.forCmd] || 'aim');
|
|
932
962
|
|
|
933
963
|
// 遮挡检测和 doClick 保持同一套判断——L2 打的是坐标,遮挡时点中的是遮挡物,
|
|
934
964
|
// 比 L1 更危险,更不能放过
|
|
935
|
-
|
|
936
|
-
if (top && top !== el && !el.contains(top) && !top.contains(el)) {
|
|
965
|
+
if (occluderAt(el, x, y)) {
|
|
937
966
|
throw fail('NOT_INTERACTABLE', `${p.ref || p.selector} 被其它元素遮挡(可能有弹窗/浮层)。先处理遮挡物。`);
|
|
938
967
|
}
|
|
939
968
|
// 视口外的坐标 CDP 打不中。scrollIntoView 之后仍在视口外,说明它在一个
|
|
@@ -1228,8 +1257,7 @@
|
|
|
1228
1257
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1229
1258
|
// 被遮挡检测:点下去要是命中别的元素,多半有弹层盖着,这时候点了就是误操作
|
|
1230
1259
|
const r = el.getBoundingClientRect();
|
|
1231
|
-
|
|
1232
|
-
if (top && top !== el && !el.contains(top) && !top.contains(el)) {
|
|
1260
|
+
if (occluderAt(el, r.left + r.width / 2, r.top + r.height / 2)) {
|
|
1233
1261
|
throw fail('NOT_INTERACTABLE', `${p.ref} 被其它元素遮挡(可能有弹窗/浮层)。先处理遮挡物。`);
|
|
1234
1262
|
}
|
|
1235
1263
|
realClick(el);
|
|
@@ -1778,10 +1806,15 @@
|
|
|
1778
1806
|
async function doUploadUrl(p) {
|
|
1779
1807
|
let blob;
|
|
1780
1808
|
try {
|
|
1781
|
-
|
|
1809
|
+
// 到点取消,文件就不会在报错之后才进页面,重试不会传出第二份
|
|
1810
|
+
const signal = p.fetchMs ? AbortSignal.timeout(p.fetchMs) : undefined;
|
|
1811
|
+
const res = await fetch(p.url, { credentials: 'omit', cache: 'no-store', signal });
|
|
1782
1812
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1783
1813
|
blob = await res.blob();
|
|
1784
1814
|
} catch (e) {
|
|
1815
|
+
if (e?.name === 'TimeoutError') {
|
|
1816
|
+
throw fail('UPLOAD_SLOW', `素材 ${Math.round((p.bytes || 0) / 1048576)}MB 在 ${Math.round(p.fetchMs / 1000)} 秒内没有下载完,已取消,文件没有进页面。压缩后再传,或请用户手动上传。`);
|
|
1817
|
+
}
|
|
1785
1818
|
throw fail('FETCH_FAILED', `页面里取素材失败:${e.message}`);
|
|
1786
1819
|
}
|
|
1787
1820
|
if (p.bytes && blob.size !== p.bytes) throw fail('INTERNAL', `素材大小不对:应为 ${p.bytes},实际 ${blob.size}`);
|
package/extension/device.js
CHANGED
|
@@ -28,6 +28,41 @@ const ID_RE = /^[\w.:-]{1,80}$/;
|
|
|
28
28
|
|
|
29
29
|
const err = (code, message, details) => Object.assign(new Error(message), { code, ...(details ? { details } : {}) });
|
|
30
30
|
|
|
31
|
+
// 一个账号下常有好几个浏览器实例(家里的 Chrome、公司的 Edge、Mac 上的 Brave)。
|
|
32
|
+
// 用户给每个实例起名字来区分;服务器再按浏览器种类和系统画图标。
|
|
33
|
+
// 种类和系统只认下面这几个词,别的一律归到 chromium / 空——它们只决定图标,不决定权限。
|
|
34
|
+
export const BROWSERS = ['chrome', 'edge', 'brave', 'opera', 'chromium'];
|
|
35
|
+
export const PLATFORMS = ['windows', 'macos', 'linux', 'chromeos'];
|
|
36
|
+
const BROWSER_LABEL = { chrome: 'Chrome', edge: 'Edge', brave: 'Brave', opera: 'Opera', chromium: 'Chromium' };
|
|
37
|
+
const PLATFORM_LABEL = { windows: 'Windows', macos: 'macOS', linux: 'Linux', chromeos: 'ChromeOS' };
|
|
38
|
+
export const DEVICE_NAME_MAX = 40;
|
|
39
|
+
|
|
40
|
+
// brands / platform 来自 navigator.userAgentData,ua 是 navigator.userAgent(老 Chrome 没有前者)
|
|
41
|
+
export function browserIdentity({ brands = [], platform = '', ua = '' } = {}) {
|
|
42
|
+
const names = (Array.isArray(brands) ? brands : []).map((b) => String(b?.brand || ''));
|
|
43
|
+
const text = String(ua || '');
|
|
44
|
+
const seen = (brand, inUa) => names.some((n) => brand.test(n)) || (inUa ? inUa.test(text) : false);
|
|
45
|
+
const browser = seen(/Microsoft Edge/, /\bEdg(A|iOS)?\//) ? 'edge'
|
|
46
|
+
: seen(/Brave/) ? 'brave'
|
|
47
|
+
: seen(/Opera/, /\bOPR\//) ? 'opera'
|
|
48
|
+
: seen(/Google Chrome/) || (!names.length && /\bChrome\//.test(text)) ? 'chrome'
|
|
49
|
+
: 'chromium';
|
|
50
|
+
const p = `${platform || ''} ${text}`;
|
|
51
|
+
const os = /Windows/i.test(p) ? 'windows'
|
|
52
|
+
: /Mac/i.test(p) ? 'macos'
|
|
53
|
+
: /CrOS|Chrome OS/i.test(p) ? 'chromeos'
|
|
54
|
+
: /Linux|X11/i.test(p) ? 'linux'
|
|
55
|
+
: '';
|
|
56
|
+
return { browser, platform: os };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const defaultDeviceName = ({ browser, platform } = {}) =>
|
|
60
|
+
[BROWSER_LABEL[browser] || 'Chromium', PLATFORM_LABEL[platform]].filter(Boolean).join(' · ');
|
|
61
|
+
|
|
62
|
+
export function cleanDeviceName(raw) {
|
|
63
|
+
return String(raw ?? '').replace(/\s+/g, ' ').replace(/[\x00-\x1f\x7f]/g, '').trim().slice(0, DEVICE_NAME_MAX);
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
export function cleanDeviceId(raw) {
|
|
32
67
|
const s = String(raw ?? '').trim();
|
|
33
68
|
return ID_RE.test(s) ? s : '';
|
package/extension/folders.js
CHANGED
|
@@ -206,6 +206,36 @@ export async function listTree(handle, { path = '', depth = 3, glob = '', folder
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// 网页上文档库的悬停提示:现场数第一层有多少文件、多少子文件夹,只回数字,不回名字。
|
|
210
|
+
// 不往下层走:一层一般不到一秒,整棵树可能要很久。隐藏项和 node_modules 跳过,和任务看到的一致。
|
|
211
|
+
const COUNT_MAX = 20000;
|
|
212
|
+
export async function countTop(id) {
|
|
213
|
+
const row = await get(String(id || ''));
|
|
214
|
+
if (!row?.handle) return { state: 'missing' };
|
|
215
|
+
let permission;
|
|
216
|
+
try { permission = await row.handle.queryPermission({ mode: 'read' }); } catch { permission = 'denied'; }
|
|
217
|
+
if (permission !== 'granted') return { state: 'locked' };
|
|
218
|
+
let files = 0;
|
|
219
|
+
let readable = 0;
|
|
220
|
+
let dirs = 0;
|
|
221
|
+
let more = false;
|
|
222
|
+
try {
|
|
223
|
+
for await (const [name, h] of row.handle.entries()) {
|
|
224
|
+
if (files + dirs >= COUNT_MAX) { more = true; break; }
|
|
225
|
+
if (skipEntry(name)) continue;
|
|
226
|
+
if (h.kind === 'directory') dirs++;
|
|
227
|
+
else {
|
|
228
|
+
files++;
|
|
229
|
+
if (fileAllowed(name)) readable++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
const mapped = mapFsError(e);
|
|
234
|
+
return { state: mapped.code === 'FOLDER_LOCKED' ? 'locked' : mapped.code === 'NOT_FOUND' ? 'gone' : 'error' };
|
|
235
|
+
}
|
|
236
|
+
return { state: 'ok', files, readable, dirs, more };
|
|
237
|
+
}
|
|
238
|
+
|
|
209
239
|
export async function readFile(handle, rel) {
|
|
210
240
|
await assertGranted(handle);
|
|
211
241
|
let file;
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "vcorp-chrome",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.5",
|
|
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/mount.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<main>
|
|
41
41
|
<div>
|
|
42
42
|
<h1>挂载本地文件夹</h1>
|
|
43
|
-
<p
|
|
43
|
+
<p>选一个文件夹之后,配对的网站可以申请读取里面的文档。挂上就是把它分享给你的任务:开了「自动允许新任务」时按需直接读,没开时每次都会问你。只读,不会改你的文件。</p>
|
|
44
44
|
</div>
|
|
45
45
|
<section>
|
|
46
46
|
<button class="btn" id="pick" type="button">选择文件夹</button>
|
package/extension/panel.html
CHANGED
|
@@ -83,6 +83,8 @@
|
|
|
83
83
|
.row { display: flex; gap: 6px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
|
|
84
84
|
.msg { font-size: 12px; margin-top: 6px; }
|
|
85
85
|
.msg.bad { color: var(--bad); }
|
|
86
|
+
.msg.ok { color: var(--ok); }
|
|
87
|
+
.msg.warn { color: var(--warn); }
|
|
86
88
|
.confirm { border: 1px solid var(--warn); border-radius: 9px; padding: 9px 10px; margin-top: 8px; }
|
|
87
89
|
.confirm b { word-break: break-all; }
|
|
88
90
|
.sites { display: flex; flex-direction: column; margin-top: 4px; }
|
|
@@ -141,6 +143,10 @@
|
|
|
141
143
|
<div class="confirm">
|
|
142
144
|
<div>要把这个浏览器交给 <b id="claimOrigin"></b> 上的账号 <b id="claimAccount"></b> 吗?</div>
|
|
143
145
|
<div class="muted" style="margin-top:4px">配对码如果是别人发给你的,点取消。</div>
|
|
146
|
+
<label class="field"><span>给这个浏览器起个名字</span>
|
|
147
|
+
<input type="text" id="claimName" maxlength="40" autocomplete="off" spellcheck="false" />
|
|
148
|
+
<span class="muted">同一个账号连了好几个浏览器时,网页上靠它区分,比如「家里 Mac」「公司 Edge」</span>
|
|
149
|
+
</label>
|
|
144
150
|
<div class="row"><button class="btn" id="claimYes">确认连接</button><button class="btn ghost" id="claimNo">取消</button></div>
|
|
145
151
|
</div>
|
|
146
152
|
</div>
|
|
@@ -177,6 +183,13 @@
|
|
|
177
183
|
<div id="cloudOn" hidden>
|
|
178
184
|
<div class="light"><span class="dot" id="cloudDot"></span>
|
|
179
185
|
<div><div class="name" id="cloudName"></div><div class="sub" id="cloudSub"></div></div></div>
|
|
186
|
+
<label class="field"><span class="lab">这个浏览器的名字</span>
|
|
187
|
+
<span class="row" style="margin-top:0">
|
|
188
|
+
<input type="text" id="deviceName" maxlength="40" autocomplete="off" spellcheck="false" style="flex:1" />
|
|
189
|
+
<button class="btn ghost" id="deviceNameSave" type="button">保存</button>
|
|
190
|
+
</span>
|
|
191
|
+
</label>
|
|
192
|
+
<div class="msg" id="deviceNameMsg" role="status" aria-live="polite" hidden></div>
|
|
180
193
|
<div id="cloudTasks"></div>
|
|
181
194
|
<div class="lab" style="margin-top:10px">允许它使用的网站</div>
|
|
182
195
|
<div class="sites" id="cloudSites"></div>
|
|
@@ -257,12 +270,12 @@
|
|
|
257
270
|
</details>
|
|
258
271
|
<details>
|
|
259
272
|
<summary>自动允许和免确认</summary>
|
|
260
|
-
<div
|
|
273
|
+
<div>「自动允许新任务」:新任务,以及它一开始就要去的网站,会直接放行。已挂载的本地文件夹也直接让任务读。任务开始之后再加的网站,仍然要你逐个点头。</div>
|
|
261
274
|
<div>「对外动作不再逐条确认」:发消息、投递、发帖之前不再问你。扫码、验证码仍然要你本人来。付款一律不做。</div>
|
|
262
275
|
</details>
|
|
263
276
|
<details>
|
|
264
277
|
<summary>本地文件夹</summary>
|
|
265
|
-
<div>挂载之后,配对的网站可以申请读取里面的文档(Markdown、文本、PDF、Word、CSV、JSON、HTML
|
|
278
|
+
<div>挂载之后,配对的网站可以申请读取里面的文档(Markdown、文本、PDF、Word、Excel、PowerPoint、CSV、JSON、HTML、图片)。只读,不会改你的文件。开了「自动允许新任务」时任务按需直接读;没开时每个任务读之前会问你。浏览器重启后如果显示需要恢复,点「恢复访问」。</div>
|
|
266
279
|
</details>
|
|
267
280
|
<details>
|
|
268
281
|
<summary>桌面应用</summary>
|
package/extension/panel.js
CHANGED
|
@@ -258,9 +258,9 @@ const CLOUD_STATE = {
|
|
|
258
258
|
off: ['', '未连接'],
|
|
259
259
|
};
|
|
260
260
|
const LOG_TEXT = {
|
|
261
|
-
paired: '配对', unpaired: '解除配对', approved: '批准任务', denied: '拒绝任务', ended: '任务结束',
|
|
261
|
+
paired: '配对', unpaired: '解除配对', renamed: '改名为', approved: '批准任务', denied: '拒绝任务', ended: '任务结束',
|
|
262
262
|
sites_on: '允许了新网站', sites_off: '拒绝了新网站', sites_timeout: '新网站没回应',
|
|
263
|
-
folders_on: '允许了本地文件夹', folders_off: '拒绝了本地文件夹', folders_timeout: '本地文件夹没回应',
|
|
263
|
+
folders_on: '允许了本地文件夹', folders_auto: '自动放行本地文件夹', folders_off: '拒绝了本地文件夹', folders_timeout: '本地文件夹没回应',
|
|
264
264
|
apps_on: '允许了桌面应用', apps_off: '拒绝了桌面应用', apps_timeout: '桌面应用没回应',
|
|
265
265
|
desk_confirm: '同意了桌面动作', desk_declined: '没同意桌面动作', desktop_on: '启用桌面能力', desktop_off: '停用桌面能力',
|
|
266
266
|
desktop_reload: '为启用桌面能力重载扩展', desktop_fail: '桌面能力没启用成功:',
|
|
@@ -345,8 +345,12 @@ function renderCloud(s) {
|
|
|
345
345
|
if (s.claim) {
|
|
346
346
|
$('claimOrigin').textContent = new URL(s.claim.origin).host;
|
|
347
347
|
$('claimAccount').textContent = `「${s.claim.account || '(未命名)'}」`;
|
|
348
|
+
if (!$('claimName').value) $('claimName').value = s.claim.name;
|
|
348
349
|
}
|
|
349
350
|
if (!on) { syncAlert(); return; }
|
|
351
|
+
if (document.activeElement !== $('deviceName')) $('deviceName').value = s.cfg.name;
|
|
352
|
+
// 改名时没连上:连上那一刻 hello 已把名字写进服务器,提示跟着变
|
|
353
|
+
if (namePending === s.cfg.name && s.status?.state === 'online') nameSaved(s);
|
|
350
354
|
|
|
351
355
|
const [cls, word] = CLOUD_STATE[s.status?.state] || CLOUD_STATE.off;
|
|
352
356
|
$('cloudDot').className = `dot ${cls}`;
|
|
@@ -424,11 +428,47 @@ $('pairGo').onclick = async () => {
|
|
|
424
428
|
$('pairCode').onkeydown = (e) => { if (e.key === 'Enter') $('pairGo').click(); };
|
|
425
429
|
$('claimYes').onclick = async () => {
|
|
426
430
|
$('claimYes').disabled = true;
|
|
427
|
-
await cloudCall('confirm');
|
|
431
|
+
await cloudCall('confirm', { name: $('claimName').value });
|
|
428
432
|
$('claimYes').disabled = false;
|
|
429
433
|
$('pairCode').value = '';
|
|
434
|
+
$('claimName').value = '';
|
|
430
435
|
};
|
|
431
|
-
$('claimNo').onclick = () => cloudCall('cancelClaim');
|
|
436
|
+
$('claimNo').onclick = () => { $('claimName').value = ''; cloudCall('cancelClaim'); };
|
|
437
|
+
// 改名的结果就写在输入框下面,不弹窗;服务器确认写入才说「已保存」
|
|
438
|
+
let namePending = null;
|
|
439
|
+
let nameMsgTimer = null;
|
|
440
|
+
function nameMsg(cls, text) {
|
|
441
|
+
clearTimeout(nameMsgTimer);
|
|
442
|
+
const box = $('deviceNameMsg');
|
|
443
|
+
box.className = `msg ${cls}`;
|
|
444
|
+
box.textContent = text;
|
|
445
|
+
box.hidden = !text;
|
|
446
|
+
}
|
|
447
|
+
function nameSaved(s) {
|
|
448
|
+
namePending = null;
|
|
449
|
+
nameMsg('ok', `已保存,${new URL(s.cfg.origin).host} 的「执行设备」里现在显示「${s.cfg.name}」`);
|
|
450
|
+
nameMsgTimer = setTimeout(() => nameMsg('', ''), 8000);
|
|
451
|
+
}
|
|
452
|
+
$('deviceNameSave').onclick = async () => {
|
|
453
|
+
const name = $('deviceName').value;
|
|
454
|
+
$('deviceNameSave').disabled = true;
|
|
455
|
+
$('deviceName').blur();
|
|
456
|
+
namePending = null;
|
|
457
|
+
nameMsg('muted', '保存中…');
|
|
458
|
+
const r = await cloudCall('rename', { name });
|
|
459
|
+
$('deviceNameSave').disabled = false;
|
|
460
|
+
if (!r?.ok) { nameMsg('bad', `没有保存:${r?.error || '扩展没有响应,稍后再试'}`); return; }
|
|
461
|
+
const host = new URL(r.state.cfg.origin).host;
|
|
462
|
+
if (r.state.rename === 'saved') nameSaved(r.state);
|
|
463
|
+
else if (r.state.rename === 'unconfirmed') nameMsg('warn', `已发给 ${host},但没等到确认。请到它的「执行设备」里看一下名字对不对`);
|
|
464
|
+
else {
|
|
465
|
+
namePending = r.state.cfg.name;
|
|
466
|
+
nameMsg('warn', r.state.status?.state === 'paused'
|
|
467
|
+
? `已存到本机。云端已暂停,恢复后会更新到 ${host}`
|
|
468
|
+
: `已存到本机,还没连上 ${host},连上后会自动更新`);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
$('deviceName').onkeydown = (e) => { if (e.key === 'Enter') $('deviceNameSave').click(); };
|
|
432
472
|
$('cloudAuto').onchange = (e) => cloudCall('setAuto', { auto: e.target.checked });
|
|
433
473
|
$('cloudAutoConfirm').onchange = (e) => cloudCall('setAutoConfirm', { autoConfirm: e.target.checked });
|
|
434
474
|
$('cloudPause').onchange = (e) => cloudCall('setPaused', { paused: e.target.checked });
|
|
@@ -442,7 +482,9 @@ $('deskOn').onclick = async () => {
|
|
|
442
482
|
const granted = await chrome.permissions.request({ permissions: ['nativeMessaging'] }).catch(() => false);
|
|
443
483
|
if (!granted) { $('deskState').textContent = '没有拿到权限,桌面能力保持关闭'; return; }
|
|
444
484
|
$('deskState').textContent = '启用中…第一次启用要重载扩展,侧栏若关掉了,重新打开即可';
|
|
445
|
-
await cloudCall('desktopEnable');
|
|
485
|
+
const r = await cloudCall('desktopEnable');
|
|
486
|
+
// 失败原因写在桌面这一栏,不要留着「启用中…」,也不用在顶上再报一遍
|
|
487
|
+
if (r && !r.ok && r.error) { $('deskState').textContent = r.error; $('cloudErr').hidden = true; }
|
|
446
488
|
};
|
|
447
489
|
$('deskOff').onclick = async () => {
|
|
448
490
|
await cloudCall('desktopDisable');
|
|
@@ -453,7 +495,7 @@ $('deskOff').onclick = async () => {
|
|
|
453
495
|
|
|
454
496
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
455
497
|
const keys0 = Object.keys(changes);
|
|
456
|
-
if (keys0.some((k) => ['cloud', 'cloudTasks', 'cloudLog', 'cloudStatus', 'cloudClaim', 'folderTick', 'cloudDesktop'].includes(k))) void cloudCall('state');
|
|
498
|
+
if (keys0.some((k) => ['cloud', 'cloudTasks', 'cloudLog', 'cloudStatus', 'cloudClaim', 'folderTick', 'cloudDesktop', 'cloudDesktopError'].includes(k))) void cloudCall('state');
|
|
457
499
|
if (area !== 'session') return;
|
|
458
500
|
const keys = Object.keys(changes);
|
|
459
501
|
if (keys.includes(`panel:${tabId}`) || keys.includes('liveSids')) renderPage();
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Chrome 找宿主靠一份清单:Windows 在注册表 HKCU 下登记清单路径,macOS / Linux 把清单放进
|
|
4
4
|
// 浏览器约定的目录。清单的 path 指向一个启动脚本,脚本用装它时的那个 node 跑 native-host/host.js。
|
|
5
|
-
// allowed_origins
|
|
5
|
+
// allowed_origins 只写本扩展:开发者模式加载的 id 由 manifest.json 里的 key 算出来,再加上商店版的 id。
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
@@ -28,6 +28,14 @@ function ourExtensionId() {
|
|
|
28
28
|
return extensionId(mf.key);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// 商店版的 id 是商店分配的:打商店包时 manifest.key 被剥掉(scripts/pack-extension.mjs),算不出来,只能写死
|
|
32
|
+
export const STORE_EXTENSION_ID = 'migojdmaegllchbambngejkclndannnf';
|
|
33
|
+
|
|
34
|
+
// 清单放行的来源:开发者模式加载的(id 由 key 算出)和商店装的,两种都是本扩展
|
|
35
|
+
export function allowedOrigins(id = ourExtensionId()) {
|
|
36
|
+
return [...new Set([id, STORE_EXTENSION_ID])].map((x) => `chrome-extension://${x}/`);
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
const REG_KEYS = [
|
|
32
40
|
`HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`,
|
|
33
41
|
`HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${HOST_NAME}`,
|
|
@@ -57,7 +65,7 @@ export function install() {
|
|
|
57
65
|
description: 'vcorp-chrome desktop host',
|
|
58
66
|
path: launcher,
|
|
59
67
|
type: 'stdio',
|
|
60
|
-
allowed_origins:
|
|
68
|
+
allowed_origins: allowedOrigins(id),
|
|
61
69
|
}, null, 2));
|
|
62
70
|
const placed = [];
|
|
63
71
|
if (win) {
|