@jaychang1989/dsh-webchat 0.7.0 → 0.7.2

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.en.md CHANGED
@@ -41,7 +41,7 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
41
41
 
42
42
  ## Use
43
43
 
44
- 1. Click the "DeepSeek 网页 / DeepSeek Web" entry — the page loads in the center column; there is no second click.
44
+ 1. Click the "chat DeepSeek" entry — the page loads in the center column; there is no second click.
45
45
  2. Sign in to DeepSeek once — the plugin keeps that login across restarts (see below).
46
46
 
47
47
  Click again to collapse the panel. Switching to another panel (Plugins, Automation Tasks, the task board, a session …) only hides the guest, never unmounts it, so coming back neither reloads the page nor drops the session.
@@ -54,12 +54,17 @@ Click again to collapse the panel. Switching to another panel (Plugins, Automati
54
54
 
55
55
  ## Staying signed in
56
56
 
57
- The host hands browser guests a **process-lifetime** partition (a fresh random name per run, with no `persist:` prefix), so cookies and site storage would die with the app — DSH's own side-card browser behaves the same way. This plugin takes that over: its host half runs inside the **Electron main process**, which is the only place a partition's cookies can be read (**HttpOnly ones included**; a renderer can never see them). It snapshots the cookies and the page's localStorage while you use the page, and puts them back **before** the page loads on the next run.
57
+ The host hands browser guests a **process-lifetime** partition (a fresh random name per run, with no `persist:` prefix), so cookies and site storage would die with the app — DSH's own side-card browser behaves the same way. This plugin takes that over through **two channels**, because the host half does not always reach Electron:
58
+
59
+ 1. **Site storage** (localStorage) and the **page's own cookies**: carried by the guest itself, generically — no key names, no Electron.
60
+ 2. **Partition cookies, HttpOnly included**: only the Electron main process can read those. Used when reachable; `session.electron` in `GET /api/dsh-webchat/state` says plainly whether it is.
61
+
62
+ The order: reserve the lease → restore first (cookies land before the first navigation, so the page's very first request already carries them) → write storage and the page's cookies once the first load finishes → reload once → snapshot as the page is used.
58
63
 
59
64
  - Location: `%USERPROFILE%\.dsh\dsh-webchat\session.json`
60
65
  - **Deleting that file logs the plugin's guest out** — it keeps nothing else behind.
61
66
  - The file holds live session credentials in **plain text**. It sits in your own profile directory (user-private by default), but it is not as protected as a browser's encrypted cookie store.
62
- - If DeepSeek changes how it stores the session you may have to sign in once more; the snapshot is then taken again automatically.
67
+ - If DeepSeek keeps the session in an HttpOnly cookie *and* the host half cannot reach Electron, only the storage part can be kept — signing in again would then still happen.
63
68
 
64
69
  ## Known limitations
65
70
 
@@ -70,7 +75,7 @@ The host hands browser guests a **process-lifetime** partition (a fresh random n
70
75
 
71
76
  | Symptom | Cause / fix |
72
77
  | --- | --- |
73
- | No "DeepSeek Web" row in the sidebar | Check that `@jaychang1989/dsh-webchat` is in the profile's `dsh.profile.bundles`, then restart the desktop app |
78
+ | No "chat DeepSeek" row in the sidebar | Check that `@jaychang1989/dsh-webchat` is in the profile's `dsh.profile.bundles`, then restart the desktop app |
74
79
  | The center column says "载入失败:…" | The host refused the guest (the bridge threw). The text carries the host's reason |
75
80
  | The row opens a browser window instead of a panel | This renderer had no `dshDesktop.browser` (a plain web profile, for instance), so the fallback ran |
76
81
  | The row opens but the center column is blank | The panel fills the cell the shell allocates; in a very small window, or with the sidebar dragged extremely narrow, that cell can have no area |
package/README.md CHANGED
@@ -41,7 +41,7 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
41
41
 
42
42
  ## 使用
43
43
 
44
- 1. 点侧边栏的「DeepSeek 网页」入口 —— 页面直接在中栏载入,不需要第二次点击;
44
+ 1. 点侧边栏的「chat DeepSeek」入口 —— 页面直接在中栏载入,不需要第二次点击;
45
45
  2. 在里面登录一次 DeepSeek,当前这次运行内一直有效。
46
46
 
47
47
  再点一次入口收起面板。切换去别的面板(插件、自动化任务、任务看板、会话……)时访客只是被隐藏、从不被卸载,所以切回来不会重新加载、也不会掉登录。
@@ -54,12 +54,17 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
54
54
 
55
55
  ## 登录状态
56
56
 
57
- 桌面端给浏览器访客的分区是**进程内**的(每次运行随机命名、不带 `persist:`),所以 cookie 和站点存储本来会随退出一起消失——DSH 自带的侧栏浏览器也是这样。本插件把这个接管了:宿主半区运行在 **Electron 主进程里**,因此能读到该分区的 cookie(**含 HttpOnly**,渲染进程永远看不到),它会在你使用过程中把 cookie 与页面的 localStorage 快照下来,下次启动时**先回灌、再加载页面**。
57
+ 桌面端给浏览器访客的分区是**进程内**的(每次运行随机命名、不带 `persist:`),所以 cookie 和站点存储本来会随退出一起消失——DSH 自带的侧栏浏览器也是这样。本插件把这个接管了,用**两条通道**——因为宿主半区不一定拿得到 Electron:
58
+
59
+ 1. **站点存储**(localStorage)与**页面自己的 cookie**:由页面侧通用搬运,不依赖任何键名,也不依赖 Electron;
60
+ 2. **分区 cookie(含 HttpOnly)**:只有宿主的 Electron 主进程读得到。能拿到就走这条;`GET /api/dsh-webchat/state` 的 `session.electron` 会如实说明。
61
+
62
+ 顺序:拿到租约 → 回灌(cookie 在首次导航前写回,页面第一个请求就带着它)→ 首次加载完成后再写入存储与 cookie → 刷新一次 → 之后按使用情况快照。
58
63
 
59
64
  - 文件位置:`%USERPROFILE%\.dsh\dsh-webchat\session.json`
60
65
  - **删掉这个文件就等于退出登录**(本插件不会再有别的残留)。
61
66
  - 文件里是**明文**会话凭据。它在你自己的用户目录下(默认只有你的账户可读),但确实不如浏览器那种加密 cookie 库。
62
- - 若 DeepSeek 更换登录态的存储方式,可能要重新登录一次——之后会自动重新快照。
67
+ - 如果 DeepSeek 的登录态是 HttpOnly cookie、而宿主又拿不到 Electron,就只能保留存储部分——那种情况下重登仍会发生。
63
68
 
64
69
  ## 已知限制
65
70
 
@@ -70,7 +75,7 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
70
75
 
71
76
  | 现象 | 原因 / 处理 |
72
77
  | --- | --- |
73
- | 侧边栏没出现「DeepSeek 网页」这一行 | 确认 profile 的 `dsh.profile.bundles` 里有 `@jaychang1989/dsh-webchat`,然后重启桌面端 |
78
+ | 侧边栏没出现「chat DeepSeek」这一行 | 确认 profile 的 `dsh.profile.bundles` 里有 `@jaychang1989/dsh-webchat`,然后重启桌面端 |
74
79
  | 中栏提示「载入失败:…」 | 宿主拒绝了访客(桥接返回异常)。文本里带着宿主给的原因 |
75
80
  | 这一行点了但中栏没有页面,反而弹出浏览器窗口 | 说明当前渲染进程拿不到 `dshDesktop.browser`(例如在纯 web 环境),插件走了降级路径 |
76
81
  | 这一行点了但中栏是空白 | 面板显示的空间是 shell 分配的那个格子;若窗口极小或侧栏被拖到极窄,格子可能没有面积 |
package/lib/client.js CHANGED
@@ -60,24 +60,28 @@ window.__ModuleLoader__.load({
60
60
  return String(tag).toLowerCase().indexOf("zh") === 0
61
61
  }
62
62
 
63
- /** Every visible string this plugin owns. */
63
+ /**
64
+ * Every visible string this plugin owns. The row's name is deliberately
65
+ * the same in both languages: it is the product's own name, not a
66
+ * translation.
67
+ */
64
68
  function copy() {
65
69
  return isChinese()
66
70
  ? {
67
- label: "DeepSeek 网页",
71
+ label: "chat DeepSeek",
68
72
  failed: "载入失败",
69
73
  fallbackNote: "这个 Harness 没有提供原生浏览器访客(桌面端才有),无法在窗口内显示官方页面。",
70
74
  fallbackAction: "在窗口中打开",
71
- fallbackOpened: "已打开 DeepSeek 网页",
75
+ fallbackOpened: "已打开 chat DeepSeek",
72
76
  fallbackHanded: "已交给系统默认浏览器打开",
73
77
  fallbackFailed: "打开失败",
74
78
  }
75
79
  : {
76
- label: "DeepSeek Web",
80
+ label: "chat DeepSeek",
77
81
  failed: "Could not load it",
78
82
  fallbackNote: "This Harness provides no native browser guest (only the desktop app does), so the official page cannot render in the window.",
79
83
  fallbackAction: "Open it in a window",
80
- fallbackOpened: "DeepSeek Web is open",
84
+ fallbackOpened: "chat DeepSeek is open",
81
85
  fallbackHanded: "Handed to the system default browser",
82
86
  fallbackFailed: "Could not open",
83
87
  }
@@ -207,28 +211,50 @@ window.__ModuleLoader__.load({
207
211
  return box
208
212
  }
209
213
 
210
- /** Read the guest page's own storage as `[[key, value], …]`. */
211
- function readStorage(element) {
214
+ /**
215
+ * Read everything the guest page itself can reach: its storage and its
216
+ * own cookie string. That string cannot carry HttpOnly cookies — only the
217
+ * host half can, and only when it reaches Electron — but it is what keeps
218
+ * a login alive when it cannot.
219
+ * @param element - the live webview.
220
+ * @returns the guest state as JSON text.
221
+ */
222
+ function readGuestState(element) {
212
223
  return element.executeJavaScript(
213
- "JSON.stringify(Object.keys(localStorage).map(function (key) { return [key, localStorage.getItem(key)] }))",
224
+ "JSON.stringify({ storage: Object.keys(localStorage).map(function (key) { return [key, localStorage.getItem(key)] }), cookie: document.cookie })",
214
225
  )
215
226
  }
216
227
 
217
- /** Write `[[key, value], …]` back into the guest page's storage. */
218
- function writeStorage(element, entries) {
228
+ /**
229
+ * Write a guest state back into the page: storage keys, then cookies.
230
+ * Cookies are re-scoped to the site root, which is where a login cookie
231
+ * belongs; the original attributes are not recoverable from
232
+ * `document.cookie`.
233
+ * @param element - the live webview.
234
+ * @param state - `{ storage, cookie }`.
235
+ */
236
+ function writeGuestState(element, state) {
219
237
  return element.executeJavaScript(
220
- "(function () { var entries = " + JSON.stringify(entries) + ";"
221
- + " for (var i = 0; i < entries.length; i++) { try { localStorage.setItem(entries[i][0], entries[i][1]) } catch (error) {} }"
222
- + " return entries.length })()",
238
+ "(function () { var state = " + JSON.stringify(state) + ";"
239
+ + " var storage = Array.isArray(state.storage) ? state.storage : [];"
240
+ + " for (var i = 0; i < storage.length; i++) { try { localStorage.setItem(storage[i][0], storage[i][1]) } catch (error) {} }"
241
+ + " if (typeof state.cookie === 'string' && state.cookie !== '') {"
242
+ + " var parts = state.cookie.split('; ');"
243
+ + " for (var j = 0; j < parts.length; j++) {"
244
+ + " var pair = parts[j];"
245
+ + " if (pair.indexOf('=') === -1) continue;"
246
+ + " try { document.cookie = pair + '; path=/' } catch (error) {}"
247
+ + " } }"
248
+ + " return true })()",
223
249
  )
224
250
  }
225
251
 
226
252
  /**
227
- * Ask the host to copy the saved cookies onto this run's partition. This
228
- * runs before the first navigation, so the page's first request already
229
- * carries them.
253
+ * Ask the host to put the saved session back before the page is
254
+ * navigated: cookies through the shell's session API where that is
255
+ * reachable, and the guest's own storage and cookie string either way.
230
256
  * @param partition - the partition the shell issued for the lease.
231
- * @returns the saved site storage, or null when there is none usable.
257
+ * @returns `{ storage, cookie }` to apply, or null when there is nothing.
232
258
  */
233
259
  async function restoreGuestSession(partition) {
234
260
  try {
@@ -238,11 +264,14 @@ window.__ModuleLoader__.load({
238
264
  body: JSON.stringify({ partition }),
239
265
  })
240
266
  const payload = await response.json()
241
- if (payload === null || typeof payload !== "object" || payload.ok !== true) {
242
- console.warn("[dsh-webchat] session restore:", payload === null ? "no answer" : payload.error || "failed")
243
- return null
267
+ if (payload === null || typeof payload !== "object") return null
268
+ if (payload.ok !== true) {
269
+ console.warn("[dsh-webchat] session cookies:", payload.error || "not restored")
244
270
  }
245
- return Array.isArray(payload.storage) && payload.storage.length > 0 ? payload.storage : null
271
+ const storage = Array.isArray(payload.storage) ? payload.storage : []
272
+ const cookie = typeof payload.cookie === "string" ? payload.cookie : ""
273
+ if (storage.length === 0 && cookie === "") return null
274
+ return { storage, cookie }
246
275
  } catch (error) {
247
276
  console.warn("[dsh-webchat] session restore failed:", error)
248
277
  return null
@@ -250,23 +279,26 @@ window.__ModuleLoader__.load({
250
279
  }
251
280
 
252
281
  /**
253
- * Ask the host to refresh the snapshot. Cookies are read host-side;
254
- * `withStorage` additionally ships the page's storage, which the unload
255
- * beacon deliberately skips (the host then keeps the previous one).
256
- * @param withStorage - whether to include the guest's storage.
282
+ * Ask the host to refresh the snapshot. `withState` ships what only the
283
+ * guest can read; the unload beacon deliberately skips it (the host then
284
+ * keeps the previous values).
285
+ * @param withState - whether to include the guest's own state.
257
286
  * @param target - the guest record; defaults to the live one, which
258
287
  * disposal clears before its last save.
259
288
  */
260
- async function saveGuestSession(withStorage, target) {
289
+ async function saveGuestSession(withState, target) {
261
290
  const record = target === undefined ? guest : target
262
291
  if (record === null || record === undefined) return
263
292
  const body = { partition: record.partition }
264
- if (withStorage) {
293
+ if (withState) {
265
294
  try {
266
- const parsed = JSON.parse(await readStorage(record.element))
267
- if (Array.isArray(parsed)) body.storage = parsed
295
+ const state = JSON.parse(await readGuestState(record.element))
296
+ if (state !== null && typeof state === "object") {
297
+ if (Array.isArray(state.storage)) body.storage = state.storage
298
+ if (typeof state.cookie === "string") body.cookie = state.cookie
299
+ }
268
300
  } catch (error) {
269
- // Not ready, or nothing stored: cookies still get saved.
301
+ // Not ready: the host still snapshots whatever it can reach.
270
302
  }
271
303
  }
272
304
  try {
@@ -294,8 +326,9 @@ window.__ModuleLoader__.load({
294
326
  || typeof reservation.lease !== "string" || typeof reservation.partition !== "string") {
295
327
  throw new Error("the desktop browser bridge returned no reservation")
296
328
  }
297
- // Cookies land on the partition before anything is requested.
298
- const storage = await restoreGuestSession(reservation.partition)
329
+ // Cookies land on the partition before anything is requested; the
330
+ // guest's own storage and cookie string come back with them.
331
+ const restored = await restoreGuestSession(reservation.partition)
299
332
  const element = document.createElement("webview")
300
333
  element.setAttribute("name", reservation.lease)
301
334
  element.setAttribute("partition", reservation.partition)
@@ -317,17 +350,17 @@ window.__ModuleLoader__.load({
317
350
  failure = messageOf(error)
318
351
  })
319
352
  }, { once: true })
320
- // Site storage can only be written while the page is on its own
321
- // origin, so it is restored after the first load and then the
322
- // page is asked to load once more — this time already signed in.
323
- // Until that settles, snapshots are skipped: the freshly loaded
324
- // page still has empty storage, and saving that would erase the
325
- // very snapshot being restored.
326
- let storageState = storage === null ? "none" : "pending"
353
+ // Storage and cookies can only be written while the page is on
354
+ // its own origin, so both are restored after the first load and
355
+ // the page is then asked to load once more — this time already
356
+ // signed in. Until that settles, snapshots are skipped: the
357
+ // freshly loaded page still has empty storage, and saving that
358
+ // would erase the very snapshot being restored.
359
+ let storageState = restored === null ? "none" : "pending"
327
360
  element.addEventListener("did-finish-load", function () {
328
361
  if (storageState !== "pending") return
329
362
  storageState = "restoring"
330
- Promise.resolve(writeStorage(element, storage))
363
+ Promise.resolve(writeGuestState(element, restored))
331
364
  .then(function () {
332
365
  storageState = "done"
333
366
  element.reload()
package/lib/index.js CHANGED
@@ -134,6 +134,10 @@ export function readSnapshot(file) {
134
134
  return {
135
135
  cookies: Array.isArray(parsed.cookies) ? parsed.cookies : [],
136
136
  storage: Array.isArray(parsed.storage) ? parsed.storage : [],
137
+ // The page's own `document.cookie` string, kept by the browser half. It
138
+ // cannot carry HttpOnly cookies, but it is what survives when this process
139
+ // has no Electron session API at all.
140
+ cookie: typeof parsed.cookie === 'string' ? parsed.cookie : '',
137
141
  savedAt: typeof parsed.savedAt === 'string' ? parsed.savedAt : '',
138
142
  }
139
143
  } catch (error) {
@@ -288,8 +292,17 @@ function messageOf(error) {
288
292
  */
289
293
  async function electronApi() {
290
294
  if (electronLoader !== null) return electronLoader()
291
- const usable = (mod) => (mod !== null && typeof mod === 'object'
292
- && (mod.BrowserWindow !== undefined || mod.session !== undefined) ? mod : undefined)
295
+ // Electron's ESM namespace can carry the real API under `default` when its
296
+ // named exports are not statically detectable, and this loader hands back
297
+ // exactly that shape, so both forms are accepted.
298
+ const usable = (mod) => {
299
+ if (mod === null || typeof mod !== 'object') return undefined
300
+ if (mod.BrowserWindow !== undefined || mod.session !== undefined) return mod
301
+ const inner = mod.default
302
+ if (inner !== null && typeof inner === 'object'
303
+ && (inner.BrowserWindow !== undefined || inner.session !== undefined)) return inner
304
+ return undefined
305
+ }
293
306
  const failures = []
294
307
  try {
295
308
  const fromImport = usable(await import('electron'))
@@ -506,18 +519,28 @@ export function apply(ctx) {
506
519
  }
507
520
  const snapshot = readSnapshot(SESSION_FILE())
508
521
  if (snapshot === null) {
509
- json(res, 200, { ok: true, cookies: 0, storage: null, savedAt: '' })
522
+ json(res, 200, { ok: true, cookies: 0, storage: null, cookie: '', savedAt: '' })
510
523
  return
511
524
  }
525
+ let restored = 0
526
+ let error = ''
512
527
  try {
513
528
  const session = await partitionSession(partition)
514
- const restored = await restoreCookies(session.cookies, snapshot)
515
- json(res, 200, { ok: true, cookies: restored, storage: snapshot.storage, savedAt: snapshot.savedAt })
516
- } catch (error) {
517
- // No snapshot is worse than an unreachable session: the guest must still
518
- // load, so this answers with a reason instead of a failure status.
519
- json(res, 200, { ok: false, error: messageOf(error), storage: null })
529
+ restored = await restoreCookies(session.cookies, snapshot)
530
+ } catch (failure) {
531
+ // Without a session API the HttpOnly cookies cannot come back, but the
532
+ // guest still gets its storage and its own cookie string, so this is
533
+ // reported rather than fatal.
534
+ error = messageOf(failure)
520
535
  }
536
+ json(res, 200, {
537
+ ok: error === '',
538
+ error,
539
+ cookies: restored,
540
+ storage: snapshot.storage,
541
+ cookie: snapshot.cookie,
542
+ savedAt: snapshot.savedAt,
543
+ })
521
544
  },
522
545
  }), 'dsh-webchat: session restore route')
523
546
 
@@ -537,21 +560,29 @@ export function apply(ctx) {
537
560
  json(res, 400, { ok: false, error: 'dsh-webchat: not a browser-guest partition' })
538
561
  return
539
562
  }
563
+ const file = SESSION_FILE()
564
+ const previous = readSnapshot(file)
565
+ // A save without site storage (the unload beacon) must not erase what an
566
+ // earlier save captured.
567
+ const storage = body !== null && Array.isArray(body.storage)
568
+ ? body.storage
569
+ : (previous === null ? [] : previous.storage)
570
+ const cookie = body !== null && typeof body.cookie === 'string'
571
+ ? body.cookie
572
+ : (previous === null ? '' : previous.cookie)
573
+ // The snapshot is written even when this process cannot reach Electron:
574
+ // site storage and the page's own cookie string are carried by the guest
575
+ // and are still worth keeping.
576
+ let cookies = []
577
+ let error = ''
540
578
  try {
541
579
  const session = await partitionSession(partition)
542
- const cookies = await captureCookies(session.cookies)
543
- const file = SESSION_FILE()
544
- const previous = readSnapshot(file)
545
- // A save without site storage (the unload beacon) must not erase what an
546
- // earlier save captured.
547
- const storage = body !== null && Array.isArray(body.storage)
548
- ? body.storage
549
- : (previous === null ? [] : previous.storage)
550
- writeSnapshot(file, { version: 1, savedAt: new Date().toISOString(), cookies, storage })
551
- json(res, 200, { ok: true, cookies: cookies.length, storage: storage.length })
552
- } catch (error) {
553
- json(res, 502, { ok: false, error: messageOf(error) })
580
+ cookies = await captureCookies(session.cookies)
581
+ } catch (failure) {
582
+ error = messageOf(failure)
554
583
  }
584
+ writeSnapshot(file, { version: 1, savedAt: new Date().toISOString(), cookies, storage, cookie })
585
+ json(res, 200, { ok: true, cookies: cookies.length, storage: storage.length, cookie: cookie !== '', error })
555
586
  },
556
587
  }), 'dsh-webchat: session save route')
557
588
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaychang1989/dsh-webchat",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Opens the official DeepSeek web app (chat.deepseek.com) inside DeepSeek Harness: one sidebar entry renders the real page in the center column, through the same native browser guest the built-in side-card browser uses. No chat UI, no agent tools, no runtime dependencies.",
5
5
  "keywords": [
6
6
  "dsh",