@pointylab/dsh 0.0.0-stage → 0.4.3

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.
Files changed (88) hide show
  1. package/ASSETS.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +85 -2
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +33 -0
  6. package/package.json +38 -4
  7. package/pet/app/card/card.css +365 -0
  8. package/pet/app/card/card.html +68 -0
  9. package/pet/app/card/card.js +896 -0
  10. package/pet/app/card/markdown.js +64 -0
  11. package/pet/app/card/mascot/blink.png +0 -0
  12. package/pet/app/card/mascot/done.png +0 -0
  13. package/pet/app/card/mascot/error.png +0 -0
  14. package/pet/app/card/mascot/idle.png +0 -0
  15. package/pet/app/card/mascot/peek.png +0 -0
  16. package/pet/app/card/mascot/think.png +0 -0
  17. package/pet/app/card/preload.js +21 -0
  18. package/pet/app/fullscreen.js +131 -0
  19. package/pet/app/main.js +1252 -0
  20. package/pet/app/native/uia-selection.ps1 +158 -0
  21. package/pet/app/notice/notice.html +37 -0
  22. package/pet/app/notice/notice.js +31 -0
  23. package/pet/app/notice/preload.js +7 -0
  24. package/pet/app/overlay/overlay.html +30 -0
  25. package/pet/app/overlay/overlay.js +42 -0
  26. package/pet/app/overlay/preload.js +7 -0
  27. package/pet/app/pill/pill.css +73 -0
  28. package/pet/app/pill/pill.html +11 -0
  29. package/pet/app/pill/pill.js +35 -0
  30. package/pet/app/pill/preload.js +8 -0
  31. package/pet/app/preload.js +16 -0
  32. package/pet/app/renderer/index.html +24 -0
  33. package/pet/app/renderer/lines.js +25 -0
  34. package/pet/app/renderer/pet.js +292 -0
  35. package/pet/app/renderer/renderer2d.js +71 -0
  36. package/pet/app/renderer/style.css +157 -0
  37. package/pet/app/selection.js +246 -0
  38. package/pet/app/shake.js +76 -0
  39. package/pet/app/uipick.js +314 -0
  40. package/pet/assets/cursor/cursor-32.cur +0 -0
  41. package/pet/assets/cursor/cursor-32.png +0 -0
  42. package/pet/assets/cursor/cursor-48.cur +0 -0
  43. package/pet/assets/cursor/cursor-48.png +0 -0
  44. package/pet/assets/cursor/cursor-64.cur +0 -0
  45. package/pet/assets/cursor/cursor-64.png +0 -0
  46. package/pet/assets/cursor/cursor-clean.png +0 -0
  47. package/pet/assets/cursor/make_cursor.py +85 -0
  48. package/pet/assets/cursor/source.png +0 -0
  49. package/pet/assets/pointy.ico +0 -0
  50. package/pet/assets/tray.png +0 -0
  51. package/pet/assets/tray@2x.png +0 -0
  52. package/pet/boot.js +6 -0
  53. package/pet/extension/background.js +252 -0
  54. package/pet/extension/cursor-32.png +0 -0
  55. package/pet/extension/cursor-48.png +0 -0
  56. package/pet/extension/cursor-64.png +0 -0
  57. package/pet/extension/icon.png +0 -0
  58. package/pet/extension/manifest.json +12 -0
  59. package/pet/extension/pick.js +391 -0
  60. package/pet/extension/popup.html +29 -0
  61. package/pet/extension/popup.js +30 -0
  62. package/pet/package.json +11 -0
  63. package/pet/vendor/koffi/LICENSE.txt +22 -0
  64. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  65. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  66. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.node +0 -0
  67. package/pet/vendor/koffi/index.js +634 -0
  68. package/pet/vendor/koffi/indirect.js +533 -0
  69. package/pet/vendor/koffi/package.json +35 -0
  70. package/pet/vendor/ws/LICENSE +20 -0
  71. package/pet/vendor/ws/index.js +22 -0
  72. package/pet/vendor/ws/lib/buffer-util.js +142 -0
  73. package/pet/vendor/ws/lib/constants.js +19 -0
  74. package/pet/vendor/ws/lib/event-target.js +292 -0
  75. package/pet/vendor/ws/lib/extension.js +203 -0
  76. package/pet/vendor/ws/lib/limiter.js +55 -0
  77. package/pet/vendor/ws/lib/permessage-deflate.js +530 -0
  78. package/pet/vendor/ws/lib/receiver.js +743 -0
  79. package/pet/vendor/ws/lib/sender.js +610 -0
  80. package/pet/vendor/ws/lib/stream.js +161 -0
  81. package/pet/vendor/ws/lib/subprotocol.js +62 -0
  82. package/pet/vendor/ws/lib/validation.js +152 -0
  83. package/pet/vendor/ws/lib/websocket-server.js +562 -0
  84. package/pet/vendor/ws/lib/websocket.js +1426 -0
  85. package/pet/vendor/ws/package.json +74 -0
  86. package/pet/vendor/ws/wrapper.mjs +21 -0
  87. package/src/node.js +732 -0
  88. package/src/runtime.js +178 -0
@@ -0,0 +1,85 @@
1
+ """把 source.png(画进图里的灰白格子"假透明")抠成真透明,裁到内容,
2
+ 再缩成光标用的 PNG 和 .cur(热点在箭头尖)。用法:python make_cursor.py"""
3
+ import io
4
+ import struct
5
+ from collections import deque
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ from PIL import Image
10
+
11
+ HERE = Path(__file__).parent
12
+ SIZES = (32, 48, 64)
13
+
14
+ img = np.array(Image.open(HERE / 'source.png').convert('RGB')).astype(int)
15
+ h, w, _ = img.shape
16
+ r, g, b = img[..., 0], img[..., 1], img[..., 2]
17
+ # 格子是浅灰和白,几乎没有颜色;人物和箭头都有深蓝描边把里面围起来
18
+ neutral = (np.abs(r - g) < 18) & (np.abs(g - b) < 18) & (np.abs(r - b) < 18) & (r > 110)
19
+
20
+ # 从四条边往里漫水,只吃连通的"无色浅色",描边挡住,箭头里面的白不会被吃掉
21
+ bg = np.zeros((h, w), bool)
22
+ q = deque((y, x) for y in range(h) for x in (0, w - 1)) + deque((y, x) for x in range(w) for y in (0, h - 1))
23
+ while q:
24
+ y, x = q.popleft()
25
+ if bg[y, x] or not neutral[y, x]:
26
+ continue
27
+ bg[y, x] = True
28
+ if y > 0: q.append((y - 1, x))
29
+ if y < h - 1: q.append((y + 1, x))
30
+ if x > 0: q.append((y, x - 1))
31
+ if x < w - 1: q.append((y, x + 1))
32
+
33
+ # 被描边围住的小洞(比如呆毛圈里)漫水进不去;格子是深浅两种灰交替,按"一块无色区域里明暗差得多"认出来
34
+ seen = bg.copy()
35
+ for sy, sx in zip(*np.where(neutral & ~bg)):
36
+ if seen[sy, sx]:
37
+ continue
38
+ comp, q = [], deque([(sy, sx)])
39
+ seen[sy, sx] = True
40
+ while q:
41
+ y, x = q.popleft()
42
+ comp.append((y, x))
43
+ for ny, nx in ((y - 1, x), (y + 1, x), (y, x - 1), (y, x + 1)):
44
+ if 0 <= ny < h and 0 <= nx < w and not seen[ny, nx] and neutral[ny, nx]:
45
+ seen[ny, nx] = True
46
+ q.append((ny, nx))
47
+ vals = np.array([r[p] for p in comp])
48
+ if len(comp) > 200 and vals.std() > 15:
49
+ for p in comp:
50
+ bg[p] = True
51
+
52
+ rgba = np.dstack([img, np.where(bg, 0, 255)]).astype(np.uint8)
53
+ out = Image.fromarray(rgba, 'RGBA')
54
+ ys, xs = np.where(~bg)
55
+ box = (xs.min(), ys.min(), xs.max() + 1, ys.max() + 1)
56
+ out = out.crop(box)
57
+
58
+ # 箭头尖:最左边那一列里最靠上的不透明像素(箭头是整张图最左的东西)
59
+ a = np.array(out)[..., 3]
60
+ col = np.where(a.any(axis=0))[0][0]
61
+ tip = (int(col), int(np.where(a[:, col])[0][0]))
62
+ side = max(out.size)
63
+ square = Image.new('RGBA', (side, side), (0, 0, 0, 0))
64
+ square.paste(out, (0, 0)) # 左上对齐,箭头尖靠近左上角
65
+ square.save(HERE / 'cursor-clean.png')
66
+ print('cropped', out.size, 'tip in cropped', tip)
67
+
68
+
69
+ def cur_file(img, size, hx, hy):
70
+ # 先存成 BMP 格式的 .ico,再把类型改成光标(2)、把热点写进 planes / bitcount 的位置。
71
+ # PNG 压缩的 .cur LoadImage 读不了,必须用 BMP
72
+ buf = io.BytesIO()
73
+ img.save(buf, 'ICO', sizes=[(size, size)], bitmap_format='bmp')
74
+ b = bytearray(buf.getvalue())
75
+ struct.pack_into('<H', b, 2, 2)
76
+ struct.pack_into('<HH', b, 6 + 4, hx, hy)
77
+ return bytes(b)
78
+
79
+
80
+ for s in SIZES:
81
+ small = square.resize((s, s), Image.LANCZOS)
82
+ small.save(HERE / f'cursor-{s}.png')
83
+ hx, hy = round(tip[0] * s / side), round(tip[1] * s / side)
84
+ (HERE / f'cursor-{s}.cur').write_bytes(cur_file(small, s, hx, hy))
85
+ print(f'{s}px hotspot', hx, hy)
Binary file
Binary file
Binary file
Binary file
package/pet/boot.js ADDED
@@ -0,0 +1,6 @@
1
+ // 生成的文件(tools/pack-plugin.mjs):让 require('koffi') / require('ws') 找到 vendor 里的副本
2
+ const path = require('node:path')
3
+ const Module = require('node:module')
4
+ process.env.NODE_PATH = [path.join(__dirname, 'vendor'), process.env.NODE_PATH].filter(Boolean).join(path.delimiter)
5
+ Module._initPaths()
6
+ require('./app/main.js')
@@ -0,0 +1,252 @@
1
+ // 和本机 pointy 保持一条 WebSocket:Pointy按快捷键时来要选区,右键菜单时主动发过去。
2
+ // 只在这两种时候读页面,平时不碰任何标签页。
3
+ // Pointy默认端口;storage 里存了 port 就用那个(Pointy改过 POINTY_PORT 时)
4
+ const DEFAULT_PORT = 47321
5
+ const petUrl = async () => `ws://127.0.0.1:${(await chrome.storage.local.get('port')).port ?? DEFAULT_PORT}`
6
+ const BROWSER = navigator.userAgent.includes('Edg/') ? 'edge' : 'chrome'
7
+ const KEEPALIVE_MS = 20_000
8
+
9
+ let ws
10
+ let authed = false
11
+ let keepalive
12
+
13
+ const enc = new TextEncoder()
14
+ const hex = (buf) => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')
15
+
16
+ async function hmac(token, data) {
17
+ const key = await crypto.subtle.importKey('raw', enc.encode(token), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
18
+ return hex(await crypto.subtle.sign('HMAC', key, enc.encode(data)))
19
+ }
20
+
21
+ const randomHex = () => hex(crypto.getRandomValues(new Uint8Array(16)))
22
+
23
+ async function connect() {
24
+ if (ws && ws.readyState <= WebSocket.OPEN) return
25
+ const { id, token } = await chrome.storage.local.get(['id', 'token'])
26
+ // 还没配对:自己去问 Pointy,Pointy 弹窗让用户点"允许"
27
+ if (!id || !token) { if (Date.now() >= pairQuietUntil) await pair(false); return }
28
+ const nonce = randomHex()
29
+ const sock = new WebSocket(await petUrl())
30
+ ws = sock
31
+ authed = false
32
+ sock.onmessage = async (ev) => {
33
+ const msg = JSON.parse(ev.data)
34
+ if (msg.type === 'challenge') {
35
+ sock.send(JSON.stringify({ type: 'hello', role: 'extension', id, browser: BROWSER, version: chrome.runtime.getManifest().version, nonce, proof: await hmac(token, `ext|${msg.nonce}`) }))
36
+ } else if (msg.type === 'hello-ok') {
37
+ if (msg.proof !== await hmac(token, `pet|${nonce}`)) return sock.close()
38
+ authed = true
39
+ clearInterval(keepalive)
40
+ // 有流量的 WebSocket 会让 MV3 service worker 一直活着
41
+ keepalive = setInterval(() => sock.readyState === WebSocket.OPEN && sock.send('{"type":"ping"}'), KEEPALIVE_MS)
42
+ } else if (authed && msg.type === 'grab') {
43
+ const result = await grabActiveTab()
44
+ sock.send(JSON.stringify({ type: 'grab-result', reqId: msg.reqId, ...result }))
45
+ } else if (authed && msg.type === 'pick-abort') {
46
+ if (pickTab) chrome.tabs.sendMessage(pickTab.id, { type: 'pick-abort' }).catch(() => {})
47
+ pickReq = undefined
48
+ } else if (authed && msg.type === 'focus-tab') {
49
+ focusTab(msg.origin)
50
+ } else if (authed && msg.type === 'reload') {
51
+ // 磁盘上的扩展比正在跑的新(Pointy 看 manifest 版本号判断),自己重载一次,不用去 chrome://extensions 点刷新
52
+ chrome.runtime.reload()
53
+ } else if (authed && msg.type === 'pick-release') {
54
+ const tabId = heldTabs.get(msg.reqId)
55
+ heldTabs.delete(msg.reqId)
56
+ if (tabId !== undefined) chrome.tabs.sendMessage(tabId, { type: 'pick-release', reqId: msg.reqId }).catch(() => {})
57
+ } else if (authed && msg.type === 'pick-start') {
58
+ const ok = await startPick(msg.cursor, msg.urlHint)
59
+ if (!ok) sock.send(JSON.stringify({ type: 'pick-unavailable', reqId: msg.reqId }))
60
+ else {
61
+ pickReq = msg.reqId
62
+ // 回报注入到了哪个标签页,Pointy记日志,方便查"高亮框跑哪去了"
63
+ sock.send(JSON.stringify({ type: 'pick-started', reqId: msg.reqId, tabTitle: pickTab?.title, tabUrl: pickTab?.url }))
64
+ }
65
+ }
66
+ }
67
+ sock.onclose = () => {
68
+ if (ws === sock) { ws = undefined; authed = false }
69
+ clearInterval(keepalive)
70
+ }
71
+ }
72
+
73
+ /** 在页面里执行:取选区和它前后的文字。密码框直接拒绝。 */
74
+ function extractSelection() {
75
+ const active = document.activeElement
76
+ if (active && active.tagName === 'INPUT' && active.type === 'password') return { blocked: '焦点在密码框里' }
77
+ const editable = active && (active.tagName === 'TEXTAREA' || (active.tagName === 'INPUT' && /^(text|search|url|email|)$/.test(active.type)))
78
+ if (editable && active.selectionStart !== active.selectionEnd) {
79
+ const { value, selectionStart: s, selectionEnd: e } = active
80
+ return { text: value.slice(s, e).slice(0, 20000), before: value.slice(Math.max(0, s - 400), s).trim(), after: value.slice(e, e + 400).trim() }
81
+ }
82
+ const sel = window.getSelection()
83
+ const text = sel ? sel.toString() : ''
84
+ let before = ''
85
+ let after = ''
86
+ if (text && sel.rangeCount) {
87
+ let block = sel.getRangeAt(0).commonAncestorContainer
88
+ if (block.nodeType !== 1) block = block.parentElement
89
+ block = block.closest('p,li,td,blockquote,pre,article,section,div') || block
90
+ const full = block.innerText || ''
91
+ const needle = text.trim().slice(0, 60)
92
+ const at = needle ? full.indexOf(needle) : -1
93
+ if (at >= 0) {
94
+ before = full.slice(Math.max(0, at - 400), at).trim()
95
+ after = full.slice(at + text.trim().length, at + text.trim().length + 400).trim()
96
+ }
97
+ if (!before) before = (block.previousElementSibling?.innerText || '').slice(-400).trim()
98
+ if (!after) after = (block.nextElementSibling?.innerText || '').slice(0, 400).trim()
99
+ }
100
+ return { text: text.slice(0, 20000), before, after }
101
+ }
102
+
103
+ async function grabActiveTab(tab) {
104
+ tab ??= (await chrome.tabs.query({ active: true, lastFocusedWindow: true }))[0]
105
+ if (!tab) return {}
106
+ const base = { url: tab.url, pageTitle: tab.title, browser: BROWSER }
107
+ // chrome:// 之类的页面不让注入脚本,只带网址和标题
108
+ if (!/^(https?|file):/.test(tab.url ?? '')) return base
109
+ try {
110
+ const frames = await chrome.scripting.executeScript({ target: { tabId: tab.id, allFrames: true }, func: extractSelection })
111
+ const blocked = frames.find(f => f.result?.blocked)
112
+ if (blocked) return { ...base, blocked: blocked.result.blocked }
113
+ const hit = frames.find(f => f.result?.text) ?? frames.find(f => f.frameId === 0)
114
+ return { ...base, ...(hit?.result ?? {}) }
115
+ } catch {
116
+ return base
117
+ }
118
+ }
119
+
120
+ let pickReq
121
+ let pickTab
122
+ const heldTabs = new Map() // reqId -> 还留着高亮框的标签页 id,卡片关掉时Pointy发 pick-release 过来
123
+
124
+ /** 往当前标签页注入指认脚本;chrome:// 之类的页面注入不了就返回 false,Pointy会改用系统层面的指认 */
125
+ async function startPick(cursor, urlHint) {
126
+ // 正常是晃鼠标时眼前的那个标签页;Pointy也可以指定网址(测试时用,不必把窗口切到前台)
127
+ const [tab] = urlHint
128
+ ? (await chrome.tabs.query({})).filter(t => t.url === urlHint || t.url?.startsWith(urlHint))
129
+ : await chrome.tabs.query({ active: true, lastFocusedWindow: true })
130
+ if (!tab || !/^(https?|file):/.test(tab.url ?? '')) return false
131
+ try {
132
+ await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (c) => { window.__pointyStart = c }, args: [cursor ?? null] })
133
+ await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['pick.js'] })
134
+ pickTab = tab
135
+ return true
136
+ } catch {
137
+ return false
138
+ }
139
+ }
140
+
141
+ /** 截当前可见区域,按元素位置裁出来;返回 data URL */
142
+ async function cropElement(tab, rect, viewport) {
143
+ const shot = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' })
144
+ const bitmap = await createImageBitmap(await (await fetch(shot)).blob())
145
+ const scale = bitmap.width / viewport.width
146
+ const pad = 8
147
+ const sx = Math.max(0, Math.floor((rect.x - pad) * scale))
148
+ const sy = Math.max(0, Math.floor((rect.y - pad) * scale))
149
+ const sw = Math.min(bitmap.width - sx, Math.ceil((rect.width + pad * 2) * scale))
150
+ const sh = Math.min(bitmap.height - sy, Math.ceil((rect.height + pad * 2) * scale))
151
+ if (sw < 4 || sh < 4) return undefined
152
+ // 太大的缩到 1600 宽以内,够模型看清
153
+ const k = Math.min(1, 1600 / sw)
154
+ const canvas = new OffscreenCanvas(Math.round(sw * k), Math.round(sh * k))
155
+ canvas.getContext('2d').drawImage(bitmap, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height)
156
+ const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.85 })
157
+ const bytes = new Uint8Array(await blob.arrayBuffer())
158
+ let bin = ''
159
+ for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
160
+ return `data:image/jpeg;base64,${btoa(bin)}`
161
+ }
162
+
163
+ async function onPickResult(msg, sender) {
164
+ const reqId = pickReq
165
+ pickReq = undefined
166
+ if (!authed || reqId === undefined) return
167
+ if (msg.cancelled || !msg.result) return ws.send(JSON.stringify({ type: 'pick-cancel', reqId }))
168
+ const tab = sender.tab ?? pickTab
169
+ // 图片、视频、画布这类光看文字不知道是什么的,截那一块一起发(只在内存里传,不存盘)
170
+ const r = msg.result
171
+ const visual = r.kind === 'image' || r.kind === 'video' || r.tag === 'canvas' || ((r.text ?? '').trim().length < 30 && r.images?.length > 0)
172
+ const image = visual ? await cropElement(tab, r.rect, r.viewport).catch(() => undefined) : undefined
173
+ // 截完了,让页面把高亮框重新显示出来,卡片开着的时候一直留着
174
+ heldTabs.set(reqId, tab.id)
175
+ chrome.tabs.sendMessage(tab.id, { type: 'pick-hold', reqId }).catch(() => {})
176
+ ws.send(JSON.stringify({ type: 'picked', reqId, url: tab.url, pageTitle: tab.title, browser: BROWSER, element: r, image }))
177
+ }
178
+
179
+ // web 版 DSH:Pointy 卡片上点"在 DSH 中打开"时,把开着它的标签页切到前面
180
+ async function focusTab(origin) {
181
+ if (typeof origin !== 'string' || !/^https?:\/\/[^/]+$/.test(origin)) return
182
+ const tabs = await chrome.tabs.query({})
183
+ const tab = tabs.find((t) => t.url?.startsWith(origin + '/'))
184
+ // 没开着就不新开:web 版首次打开要带 token 的地址,扩展拿不到
185
+ if (!tab) return
186
+ await chrome.tabs.update(tab.id, { active: true })
187
+ await chrome.windows.update(tab.windowId, { focused: true })
188
+ }
189
+
190
+ // 用户点了"不允许"或 Pointy 正忙着问别的:过一阵再自动问;弹窗里手动点"连接"不受这个限制
191
+ const PAIR_QUIET_MS = 10 * 60_000
192
+ let pairQuietUntil = 0
193
+ let pairing
194
+
195
+ function pair(manual) {
196
+ if (pairing) return pairing
197
+ pairing = (async () => {
198
+ const url = await petUrl()
199
+ return new Promise((resolve) => {
200
+ const sock = new WebSocket(url)
201
+ let timer
202
+ let settled = false
203
+ const finish = (result) => { if (settled) return; settled = true; clearTimeout(timer); pairing = undefined; resolve(result); try { sock.close() } catch {} }
204
+ // Pointy 在等用户点的时候每 10 秒发一条 pair-wait;超过这个没动静就当它不在了
205
+ const arm = () => { clearTimeout(timer); timer = setTimeout(() => finish({ ok: false, error: 'Pointy 没响应' }), 20_000) }
206
+ arm()
207
+ sock.onerror = () => finish({ ok: false, error: '连不上 Pointy:确认 DSH 开着、Pointy 插件已启用' })
208
+ sock.onclose = () => finish({ ok: false, error: '连接断了' })
209
+ sock.onmessage = async (ev) => {
210
+ const msg = JSON.parse(ev.data)
211
+ arm()
212
+ if (msg.type === 'challenge') sock.send(JSON.stringify({ type: 'pair-request', browser: BROWSER, manual }))
213
+ else if (msg.type === 'paired') {
214
+ await chrome.storage.local.set({ id: msg.id, token: msg.token })
215
+ finish({ ok: true })
216
+ connect()
217
+ } else if (msg.type === 'pair-denied' || msg.type === 'pair-busy') {
218
+ pairQuietUntil = Date.now() + PAIR_QUIET_MS
219
+ finish({ ok: false, error: msg.type === 'pair-busy' ? 'Pointy 正在问别的,稍后再试' : '在 Pointy 那边没有允许' })
220
+ }
221
+ }
222
+ })
223
+ })()
224
+ return pairing
225
+ }
226
+
227
+ chrome.runtime.onInstalled.addListener(() => {
228
+ chrome.contextMenus.create({ id: 'ask-pet', title: '问 Pointy:“%s”', contexts: ['selection'] })
229
+ chrome.contextMenus.create({ id: 'ask-pet-page', title: '问 Pointy 这个页面', contexts: ['page'] })
230
+ })
231
+
232
+ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
233
+ await connect()
234
+ const result = await grabActiveTab(tab)
235
+ if (!result.text && info.selectionText) result.text = info.selectionText
236
+ if (authed) ws.send(JSON.stringify({ type: 'selection', ...result }))
237
+ })
238
+
239
+ chrome.runtime.onMessage.addListener((msg, sender, reply) => {
240
+ if (msg.type === 'pick-result') { onPickResult(msg, sender); return }
241
+ if (msg.type === 'pair') { pair(true).then(reply); return true }
242
+ if (msg.type === 'status') {
243
+ chrome.storage.local.get(['id']).then(({ id }) => reply({ paired: !!id, connected: authed }))
244
+ return true
245
+ }
246
+ if (msg.type === 'unpair') { chrome.storage.local.remove(['id', 'token']).then(() => { ws?.close(); reply({ ok: true }) }); return true }
247
+ })
248
+
249
+ chrome.alarms.create('reconnect', { periodInMinutes: 1 })
250
+ chrome.alarms.onAlarm.addListener(() => connect())
251
+ chrome.runtime.onStartup.addListener(() => connect())
252
+ connect()
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,12 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Pointy 指哪问哪",
4
+ "version": "0.4.0",
5
+ "description": "晃一下鼠标指认网页上的内容,或右键「问 Pointy」,把指到的内容、网址和标题交给本机的 Pointy。只在被叫到时读页面。",
6
+ "permissions": ["storage", "scripting", "contextMenus", "alarms"],
7
+ "host_permissions": ["<all_urls>"],
8
+ "background": { "service_worker": "background.js" },
9
+ "action": { "default_title": "Pointy", "default_popup": "popup.html" },
10
+ "icons": { "128": "icon.png" },
11
+ "web_accessible_resources": [{ "resources": ["cursor-32.png", "cursor-48.png", "cursor-64.png"], "matches": ["<all_urls>"] }]
12
+ }