dsh-site-bridge 0.7.5 → 0.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.
@@ -1,6 +1,6 @@
1
1
  # Ограничения: что именно ломается и как это снять
2
2
 
3
- Четыре ограничения, которые чаще всего мешают на реальных сайтах. Для каждого: наблюдаемое
3
+ Пять ограничений, которые чаще всего мешают на реальных сайтах. Для каждого: наблюдаемое
4
4
  поведение, причина (со ссылкой на код), что нужно изменить и сколько это стоит.
5
5
 
6
6
  | # | Ограничение | Статус | Что осталось |
@@ -9,6 +9,7 @@
9
9
  | 2 | shadow DOM частично | **частично снято** | открытые корни: селекторы-цепочки и поиск работают; закрытые корни не видны |
10
10
  | 3 | тело ответа ≤ 400 КБ | **снято** | лимит 5 МБ в воркере, тела > 200 КБ выносятся в `bodies/<id>.json` |
11
11
  | 4 | один таб на сессию | **снято** | `Map<tabId, session>`, `tabId` в командах, переключатель в попапе |
12
+ | 5 | Chrome отказывает в `chrome.debugger.attach` отдельной вкладке (`Cannot access a chrome-extension:// URL of different extension`) | открыто | воспроизведено на `mail.google.com`: соседние сайты подключаются, Gmail — нет, при этом `tabs.get` вкладку отдаёт, а в списке целей у неё обычный `page` с https-адресом. Обход не найден; `site_tabs {action:"diagnose"}` показывает, что видит Chrome |
12
13
 
13
14
  Ниже — исходный разбор каждого ограничения; для снятых пунктов оставлено описание того, что
14
15
  именно было изменено и какие границы остались.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "default_locale": "en",
4
4
  "name": "__MSG_appName__",
5
- "version": "0.7.5",
5
+ "version": "0.8.1",
6
6
  "description": "__MSG_appDesc__",
7
7
  "minimum_chrome_version": "116",
8
8
  "permissions": [
package/extension/sw.js CHANGED
@@ -88,18 +88,27 @@ async function evt(kind, payload = {}) {
88
88
 
89
89
  let looping = false
90
90
 
91
+ /** Runs a promise with a deadline, so one stuck Chrome call cannot park the loop. */
92
+ function withDeadline(promise, ms, label) {
93
+ return Promise.race([
94
+ promise,
95
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} не ответил за ${ms} мс`)), ms))
96
+ ])
97
+ }
98
+
91
99
  async function ensureLoop() {
92
100
  if (looping) return
93
101
  looping = true
94
102
  let greeted = false
95
103
  try {
96
104
  for (;;) {
97
- const cfg = await loadCfg()
98
- if (!cfg.token) {
99
- await sleep(1500)
100
- continue
101
- }
105
+ state.lastTick = Date.now()
102
106
  try {
107
+ const cfg = await loadCfg()
108
+ if (!cfg.token) {
109
+ await sleep(1500)
110
+ continue
111
+ }
103
112
  const reply = await api(cfg, '/v1/poll?timeout=20000')
104
113
  if (!greeted) {
105
114
  greeted = true
@@ -115,7 +124,7 @@ async function ensureLoop() {
115
124
  state.attempts = 0
116
125
  for (const command of reply.commands || []) {
117
126
  // Sequential: a command may pin a tab for its whole duration.
118
- await runCommand(cfg, command)
127
+ await withDeadline(runCommand(cfg, command), 120000, `команда ${command?.op ?? '?'}`)
119
128
  }
120
129
  } catch (error) {
121
130
  greeted = false
@@ -129,6 +138,16 @@ async function ensureLoop() {
129
138
  }
130
139
  }
131
140
 
141
+ // If the loop parks on a Chrome call that never settles, restart it.
142
+ setInterval(() => {
143
+ if (!looping) return
144
+ if (Date.now() - (state.lastTick ?? 0) > 90000) {
145
+ state.lastError = 'цикл опроса завис, перезапускаю'
146
+ looping = false
147
+ void ensureLoop()
148
+ }
149
+ }, 30000)
150
+
132
151
  function sleep(ms) {
133
152
  return new Promise((resolve) => setTimeout(resolve, ms))
134
153
  }
@@ -186,6 +205,8 @@ async function handleMessageInner(message) {
186
205
  connected: state.lastPollOk > 0 && Date.now() - state.lastPollOk < 45000,
187
206
  lastError: state.lastError ?? null,
188
207
  attempts: state.attempts || 0,
208
+ loopAlive: looping,
209
+ lastTickAgoMs: state.lastTick ? Date.now() - state.lastTick : null,
189
210
  target: active?.target ?? null,
190
211
  activeTabId: state.activeTabId,
191
212
  targets: [...state.sessions.values()].map((session) => ({
@@ -1585,6 +1606,41 @@ async function dispatch(op, args) {
1585
1606
  if (tab.windowId != null) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {})
1586
1607
  return { focused: session.tabId }
1587
1608
  }
1609
+ case 'tab.diagnose': {
1610
+ const id = Number(args.tabId)
1611
+ if (!Number.isFinite(id)) throw new Error('нужен tabId')
1612
+ const info = await chrome.tabs.get(id).catch((error) => ({ error: String(error?.message ?? error) }))
1613
+ const windows = await chrome.windows.getAll({}).catch(() => [])
1614
+ const window = windows.find((item) => item.id === info?.windowId) ?? null
1615
+ const targets = await chrome.debugger.getTargets().catch(() => [])
1616
+ const mine = targets
1617
+ .filter((target) => target.tabId === id)
1618
+ .map((target) => ({
1619
+ id: target.id,
1620
+ type: target.type,
1621
+ url: String(target.url ?? '').slice(0, 300),
1622
+ title: String(target.title ?? '').slice(0, 120),
1623
+ attached: target.attached === true
1624
+ }))
1625
+ return {
1626
+ tab: info
1627
+ ? {
1628
+ id: info.id,
1629
+ url: String(info.url ?? '').slice(0, 300),
1630
+ pendingUrl: String(info.pendingUrl ?? '').slice(0, 300),
1631
+ title: String(info.title ?? '').slice(0, 120),
1632
+ status: info.status,
1633
+ active: info.active,
1634
+ discarded: info.discarded,
1635
+ windowId: info.windowId,
1636
+ index: info.index
1637
+ }
1638
+ : { error: info?.error },
1639
+ window: window ? { id: window.id, type: window.type, state: window.state } : null,
1640
+ targets: mine,
1641
+ targetCount: targets.length
1642
+ }
1643
+ }
1588
1644
  case 'snapshot.compact': {
1589
1645
  const session = requireSession(args.tabId)
1590
1646
  const { result } = await cdp('Runtime.evaluate', {
package/lib/index.js CHANGED
@@ -263,7 +263,7 @@ export function apply(ctx, config) {
263
263
  'ПРАВИЛО: подключать вкладку, которую пользователь не подключал сам, можно только с его явного ' +
264
264
  'разрешения. Без confirmed:true вызов claim отклоняется — сначала спросите пользователя.',
265
265
  {
266
- action: { type: 'string', description: 'list | claim | use | release | open | close | focus' },
266
+ action: { type: 'string', description: 'list | claim | use | release | open | close | focus | diagnose' },
267
267
  tabId: { type: 'number', description: 'id вкладки для claim / use / release / focus / close (из action=list)' },
268
268
  url: { type: 'string', description: 'адрес для action=open (по умолчанию about:blank)' },
269
269
  confirmed: {
@@ -306,6 +306,12 @@ export function apply(ctx, config) {
306
306
  await instance.enqueue('tab.focus', { tabId }, { timeout: 20000 })
307
307
  return ok(`Вкладка ${tabId} на переднем плане.`, { tabId })
308
308
  }
309
+ if (action === 'diagnose') {
310
+ const tabId = Number(args.tabId ?? instance.state().activeTabId)
311
+ if (!Number.isFinite(tabId)) throw new Error('нужен tabId')
312
+ const info = await instance.enqueue('tab.diagnose', { tabId }, { timeout: 20000 })
313
+ return ok(JSON.stringify(info, null, 2), { diagnose: info })
314
+ }
309
315
  if (action === 'claim' || action === 'use' || action === 'release') {
310
316
  const tabId = Number(args.tabId)
311
317
  if (!Number.isFinite(tabId)) throw new Error(`tabId is required for action=${action}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-site-bridge",
3
- "version": "0.7.5",
3
+ "version": "0.8.1",
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",