dsh-site-bridge 0.7.0 → 0.7.4

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.
@@ -149,5 +149,8 @@
149
149
  },
150
150
  "disconnectTab": {
151
151
  "message": "Disconnect this tab"
152
+ },
153
+ "notClaimable": {
154
+ "message": "This page cannot be attached: Chrome does not allow service pages or other extensions' pages"
152
155
  }
153
156
  }
@@ -149,5 +149,8 @@
149
149
  },
150
150
  "disconnectTab": {
151
151
  "message": "Отключить эту вкладку"
152
+ },
153
+ "notClaimable": {
154
+ "message": "Эту страницу нельзя подключить: Chrome не даёт доступ к служебным страницам и страницам других расширений"
152
155
  }
153
156
  }
@@ -149,5 +149,8 @@
149
149
  },
150
150
  "disconnectTab": {
151
151
  "message": "断开此标签页"
152
+ },
153
+ "notClaimable": {
154
+ "message": "无法连接此页面:Chrome 不允许访问服务页面或其他扩展的页面"
152
155
  }
153
156
  }
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "default_locale": "en",
4
4
  "name": "__MSG_appName__",
5
- "version": "0.7.0",
5
+ "version": "0.7.4",
6
6
  "description": "__MSG_appDesc__",
7
7
  "minimum_chrome_version": "116",
8
8
  "permissions": [
@@ -138,10 +138,17 @@ function render(status) {
138
138
 
139
139
  // Nothing to claim once a tab is bound: the action would be a no-op.
140
140
  const selfAttached = selfTabId != null && targets.some((item) => item.id === selfTabId)
141
+ // Service pages (chrome://, other extensions, the Web Store) cannot be driven;
142
+ // do not offer a button that Chrome will refuse.
143
+ const claimable = selfTabUrl == null || /^(https?|file):/.test(selfTabUrl)
141
144
  claim.hidden = targets.length > 0
142
- claim.disabled = !connected || targets.length > 0
145
+ claim.disabled = !connected || !claimable || targets.length > 0
146
+ claim.title = claimable ? '' : t('notClaimable')
143
147
  claim.textContent = t('connectTab')
144
- $('claim-more').hidden = targets.length === 0 || selfAttached
148
+ const claimMore = $('claim-more')
149
+ claimMore.hidden = targets.length === 0 || selfAttached
150
+ claimMore.disabled = !connected || !claimable
151
+ claimMore.title = claimable ? t('attachAnother') : t('notClaimable')
145
152
 
146
153
  const pick = $('pick')
147
154
  pick.classList.toggle('active', status.picking === true)
@@ -170,12 +177,14 @@ function render(status) {
170
177
  // Which tab this popup is open on: attaching that tab again is a no-op, so the
171
178
  // attach button is only offered when the current tab is not attached yet.
172
179
  let selfTabId = null
180
+ let selfTabUrl = null
173
181
 
174
182
  async function refresh() {
175
183
  try {
176
184
  if (chrome.tabs?.query) {
177
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
185
+ const tab = await currentBrowserTab()
178
186
  selfTabId = tab?.id ?? null
187
+ selfTabUrl = tab?.url ?? null
179
188
  }
180
189
  render(await send({ type: 'status' }))
181
190
  } catch {
@@ -183,6 +192,42 @@ async function refresh() {
183
192
  }
184
193
  }
185
194
 
195
+ /**
196
+ * The browser tab this popup belongs to. `currentWindow` from a popup can point
197
+ * at the popup's own extension page, and Chrome refuses to read other
198
+ * extensions' pages, so prefer a real http(s)/file tab from the focused window.
199
+ */
200
+ async function currentBrowserTab() {
201
+ const scheme = (tab) => (/^(https?|file):/.test(String(tab?.url ?? '')) ? 'web' : 'other')
202
+ const scopes = [
203
+ { active: true, lastFocusedWindow: true, windowType: 'normal' },
204
+ { active: true, windowType: 'normal' },
205
+ { active: true, lastFocusedWindow: true },
206
+ { active: true, currentWindow: true }
207
+ ]
208
+ for (const scope of scopes) {
209
+ let tabs = []
210
+ try {
211
+ tabs = await chrome.tabs.query(scope)
212
+ } catch {
213
+ continue
214
+ }
215
+ if (!Array.isArray(tabs) || tabs.length === 0) continue
216
+ const ordered = [...tabs.filter((tab) => scheme(tab) === 'web'), ...tabs.filter((tab) => scheme(tab) !== 'web')]
217
+ for (const tab of ordered) {
218
+ // Ask Chrome whether this tab is readable at all: tabs in app windows of
219
+ // other extensions are listed but cannot be touched.
220
+ try {
221
+ await chrome.tabs.get(tab.id)
222
+ return tab
223
+ } catch {
224
+ // not ours to read — try the next candidate
225
+ }
226
+ }
227
+ }
228
+ return null
229
+ }
230
+
186
231
  /* Autosave: a complete pair is stored as soon as it appears, no button. */
187
232
  let saveTimer = null
188
233
 
@@ -262,7 +307,7 @@ $('import-file').addEventListener('change', async (event) => {
262
307
  async function attachCurrentTab() {
263
308
  let tabId = null
264
309
  try {
265
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
310
+ const tab = await currentBrowserTab()
266
311
  tabId = tab?.id ?? null
267
312
  } catch {
268
313
  tabId = null
package/extension/sw.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * host, which redacts and never writes it to disk.
16
16
  */
17
17
 
18
- const EXT_VERSION = '0.1.0'
18
+ const EXT_VERSION = chrome.runtime.getManifest().version
19
19
  const NET_CAP = 600
20
20
  // Bodies are kept up to this size in the worker, then marked as truncated.
21
21
  // Raise it per machine with chrome.storage.local.set({ maxBodyChars }): the
@@ -284,13 +284,30 @@ async function claimTab(tabId, attachedBy = 'agent') {
284
284
  // caller that only knows "this tab" cannot silently pass NaN.
285
285
  let id = Number(tabId)
286
286
  if (!Number.isFinite(id)) {
287
- const [active] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
287
+ const candidates = await chrome.tabs
288
+ .query({ active: true, lastFocusedWindow: true, windowType: 'normal' })
289
+ .catch(() => [])
290
+ const active = candidates.find((tab) => /^(https?|file):/.test(tab.url ?? '')) ?? candidates[0]
288
291
  id = active?.id
289
292
  }
290
293
  if (!Number.isFinite(id)) throw new Error('no tab to claim: pass tabId or open a page first')
291
- const tab = await chrome.tabs.get(id)
294
+ let tab = null
295
+ try {
296
+ tab = await chrome.tabs.get(id)
297
+ } catch (error) {
298
+ // Chrome refuses to read pages of other extensions and its own service
299
+ // pages; say that in words a person can act on.
300
+ const detail = String(error?.message ?? error)
301
+ const hint = /chrome-extension:\/\//.test(detail)
302
+ ? ' Похоже, вкладка открыта в окне-приложении другого расширения — откройте её как обычную вкладку.'
303
+ : ''
304
+ throw new Error(`эту вкладку нельзя подключить: ${detail}${hint}`)
305
+ }
292
306
  if (!/^https?:|^file:/.test(tab.url ?? '')) {
293
- throw new Error(`cannot drive ${tab.url || 'this tab'}: only http(s) and file:// pages are supported`)
307
+ const scheme = String(tab.url ?? '').split(':')[0] || 'неизвестная схема'
308
+ throw new Error(
309
+ `эту страницу нельзя подключить (${scheme}:): Chrome не даёт доступ к служебным страницам и страницам других расширений. Откройте обычный сайт http(s) или file://`
310
+ )
294
311
  }
295
312
  return attach(id, attachedBy)
296
313
  }
@@ -1489,17 +1506,26 @@ async function dispatch(op, args) {
1489
1506
  return { ok: true, version: EXT_VERSION }
1490
1507
  case 'tabs.list': {
1491
1508
  const tabs = await chrome.tabs.query({})
1509
+ // Window type explains why a tab can be listed but not read: app windows
1510
+ // belong to another extension, and Chrome refuses access to them.
1511
+ const windows = await chrome.windows.getAll({}).catch(() => [])
1512
+ const windowType = new Map(windows.map((window) => [window.id, window.type ?? 'normal']))
1492
1513
  const list = tabs
1493
1514
  .filter((tab) => /^https?:|^file:/.test(tab.url ?? ''))
1494
- .map((tab) => ({
1495
- id: tab.id,
1496
- title: tab.title,
1497
- url: tab.url,
1498
- active: tab.active,
1499
- windowId: tab.windowId,
1500
- attached: state.sessions.has(tab.id),
1501
- target: tab.id === state.activeTabId
1502
- }))
1515
+ .map((tab) => {
1516
+ const type = windowType.get(tab.windowId) ?? 'unknown'
1517
+ return {
1518
+ id: tab.id,
1519
+ title: tab.title,
1520
+ url: tab.url,
1521
+ active: tab.active,
1522
+ windowId: tab.windowId,
1523
+ windowType: type,
1524
+ readable: type === 'normal',
1525
+ attached: state.sessions.has(tab.id),
1526
+ target: tab.id === state.activeTabId
1527
+ }
1528
+ })
1503
1529
  await evt('tabs', { tabs: list })
1504
1530
  return { tabs: list }
1505
1531
  }
package/lib/index.js CHANGED
@@ -332,10 +332,10 @@ export function apply(ctx, config) {
332
332
  }
333
333
  const result = await instance.enqueue('tabs.list', {}, { timeout: 20000 })
334
334
  const tabs = Array.isArray(result?.tabs) ? result.tabs : []
335
- const lines = tabs.map(
336
- (tab) =>
337
- `${tab.target ? '*' : tab.attached ? '+' : ' '} [${tab.id}] ${tab.title || '(без названия)'} — ${tab.url}`
338
- )
335
+ const lines = tabs.map((tab) => {
336
+ const window = tab.windowType && tab.windowType !== 'normal' ? ` [окно: ${tab.windowType}, недоступно]` : ''
337
+ return `${tab.target ? '*' : tab.attached ? '+' : ' '} [${tab.id}] ${tab.title || '(без названия)'} — ${tab.url}${window}`
338
+ })
339
339
  return ok(
340
340
  tabs.length === 0
341
341
  ? 'Нет доступных вкладок (нужны http(s)/file://).'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-site-bridge",
3
- "version": "0.7.0",
3
+ "version": "0.7.4",
4
4
  "description": "Chrome/Edge MV3 bridge for DeepSeek Harness: capture page markup (DOM, styles, selectors) and BFF traffic (xhr/fetch with headers and bodies) into a workspace snapshot bundle, drive the page with trusted CDP input, and verify injected scripts. Authorization never leaves the browser and is never written to disk.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",