@jaychang1989/dsh-webchat 0.7.0 → 0.7.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.
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/lib/client.js +64 -35
- package/lib/index.js +52 -21
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/README.md
CHANGED
|
@@ -54,12 +54,17 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
|
54
54
|
|
|
55
55
|
## 登录状态
|
|
56
56
|
|
|
57
|
-
桌面端给浏览器访客的分区是**进程内**的(每次运行随机命名、不带 `persist:`),所以 cookie 和站点存储本来会随退出一起消失——DSH
|
|
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
|
-
-
|
|
67
|
+
- 如果 DeepSeek 的登录态是 HttpOnly cookie、而宿主又拿不到 Electron,就只能保留存储部分——那种情况下重登仍会发生。
|
|
63
68
|
|
|
64
69
|
## 已知限制
|
|
65
70
|
|
package/lib/client.js
CHANGED
|
@@ -207,28 +207,50 @@ window.__ModuleLoader__.load({
|
|
|
207
207
|
return box
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Read everything the guest page itself can reach: its storage and its
|
|
212
|
+
* own cookie string. That string cannot carry HttpOnly cookies — only the
|
|
213
|
+
* host half can, and only when it reaches Electron — but it is what keeps
|
|
214
|
+
* a login alive when it cannot.
|
|
215
|
+
* @param element - the live webview.
|
|
216
|
+
* @returns the guest state as JSON text.
|
|
217
|
+
*/
|
|
218
|
+
function readGuestState(element) {
|
|
212
219
|
return element.executeJavaScript(
|
|
213
|
-
"JSON.stringify(Object.keys(localStorage).map(function (key) { return [key, localStorage.getItem(key)] }))",
|
|
220
|
+
"JSON.stringify({ storage: Object.keys(localStorage).map(function (key) { return [key, localStorage.getItem(key)] }), cookie: document.cookie })",
|
|
214
221
|
)
|
|
215
222
|
}
|
|
216
223
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Write a guest state back into the page: storage keys, then cookies.
|
|
226
|
+
* Cookies are re-scoped to the site root, which is where a login cookie
|
|
227
|
+
* belongs; the original attributes are not recoverable from
|
|
228
|
+
* `document.cookie`.
|
|
229
|
+
* @param element - the live webview.
|
|
230
|
+
* @param state - `{ storage, cookie }`.
|
|
231
|
+
*/
|
|
232
|
+
function writeGuestState(element, state) {
|
|
219
233
|
return element.executeJavaScript(
|
|
220
|
-
"(function () { var
|
|
221
|
-
+ "
|
|
222
|
-
+ "
|
|
234
|
+
"(function () { var state = " + JSON.stringify(state) + ";"
|
|
235
|
+
+ " var storage = Array.isArray(state.storage) ? state.storage : [];"
|
|
236
|
+
+ " for (var i = 0; i < storage.length; i++) { try { localStorage.setItem(storage[i][0], storage[i][1]) } catch (error) {} }"
|
|
237
|
+
+ " if (typeof state.cookie === 'string' && state.cookie !== '') {"
|
|
238
|
+
+ " var parts = state.cookie.split('; ');"
|
|
239
|
+
+ " for (var j = 0; j < parts.length; j++) {"
|
|
240
|
+
+ " var pair = parts[j];"
|
|
241
|
+
+ " if (pair.indexOf('=') === -1) continue;"
|
|
242
|
+
+ " try { document.cookie = pair + '; path=/' } catch (error) {}"
|
|
243
|
+
+ " } }"
|
|
244
|
+
+ " return true })()",
|
|
223
245
|
)
|
|
224
246
|
}
|
|
225
247
|
|
|
226
248
|
/**
|
|
227
|
-
* Ask the host to
|
|
228
|
-
*
|
|
229
|
-
*
|
|
249
|
+
* Ask the host to put the saved session back before the page is
|
|
250
|
+
* navigated: cookies through the shell's session API where that is
|
|
251
|
+
* reachable, and the guest's own storage and cookie string either way.
|
|
230
252
|
* @param partition - the partition the shell issued for the lease.
|
|
231
|
-
* @returns
|
|
253
|
+
* @returns `{ storage, cookie }` to apply, or null when there is nothing.
|
|
232
254
|
*/
|
|
233
255
|
async function restoreGuestSession(partition) {
|
|
234
256
|
try {
|
|
@@ -238,11 +260,14 @@ window.__ModuleLoader__.load({
|
|
|
238
260
|
body: JSON.stringify({ partition }),
|
|
239
261
|
})
|
|
240
262
|
const payload = await response.json()
|
|
241
|
-
if (payload === null || typeof payload !== "object"
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
if (payload === null || typeof payload !== "object") return null
|
|
264
|
+
if (payload.ok !== true) {
|
|
265
|
+
console.warn("[dsh-webchat] session cookies:", payload.error || "not restored")
|
|
244
266
|
}
|
|
245
|
-
|
|
267
|
+
const storage = Array.isArray(payload.storage) ? payload.storage : []
|
|
268
|
+
const cookie = typeof payload.cookie === "string" ? payload.cookie : ""
|
|
269
|
+
if (storage.length === 0 && cookie === "") return null
|
|
270
|
+
return { storage, cookie }
|
|
246
271
|
} catch (error) {
|
|
247
272
|
console.warn("[dsh-webchat] session restore failed:", error)
|
|
248
273
|
return null
|
|
@@ -250,23 +275,26 @@ window.__ModuleLoader__.load({
|
|
|
250
275
|
}
|
|
251
276
|
|
|
252
277
|
/**
|
|
253
|
-
* Ask the host to refresh the snapshot.
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* @param
|
|
278
|
+
* Ask the host to refresh the snapshot. `withState` ships what only the
|
|
279
|
+
* guest can read; the unload beacon deliberately skips it (the host then
|
|
280
|
+
* keeps the previous values).
|
|
281
|
+
* @param withState - whether to include the guest's own state.
|
|
257
282
|
* @param target - the guest record; defaults to the live one, which
|
|
258
283
|
* disposal clears before its last save.
|
|
259
284
|
*/
|
|
260
|
-
async function saveGuestSession(
|
|
285
|
+
async function saveGuestSession(withState, target) {
|
|
261
286
|
const record = target === undefined ? guest : target
|
|
262
287
|
if (record === null || record === undefined) return
|
|
263
288
|
const body = { partition: record.partition }
|
|
264
|
-
if (
|
|
289
|
+
if (withState) {
|
|
265
290
|
try {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
291
|
+
const state = JSON.parse(await readGuestState(record.element))
|
|
292
|
+
if (state !== null && typeof state === "object") {
|
|
293
|
+
if (Array.isArray(state.storage)) body.storage = state.storage
|
|
294
|
+
if (typeof state.cookie === "string") body.cookie = state.cookie
|
|
295
|
+
}
|
|
268
296
|
} catch (error) {
|
|
269
|
-
// Not ready
|
|
297
|
+
// Not ready: the host still snapshots whatever it can reach.
|
|
270
298
|
}
|
|
271
299
|
}
|
|
272
300
|
try {
|
|
@@ -294,8 +322,9 @@ window.__ModuleLoader__.load({
|
|
|
294
322
|
|| typeof reservation.lease !== "string" || typeof reservation.partition !== "string") {
|
|
295
323
|
throw new Error("the desktop browser bridge returned no reservation")
|
|
296
324
|
}
|
|
297
|
-
// Cookies land on the partition before anything is requested
|
|
298
|
-
|
|
325
|
+
// Cookies land on the partition before anything is requested; the
|
|
326
|
+
// guest's own storage and cookie string come back with them.
|
|
327
|
+
const restored = await restoreGuestSession(reservation.partition)
|
|
299
328
|
const element = document.createElement("webview")
|
|
300
329
|
element.setAttribute("name", reservation.lease)
|
|
301
330
|
element.setAttribute("partition", reservation.partition)
|
|
@@ -317,17 +346,17 @@ window.__ModuleLoader__.load({
|
|
|
317
346
|
failure = messageOf(error)
|
|
318
347
|
})
|
|
319
348
|
}, { once: true })
|
|
320
|
-
//
|
|
321
|
-
// origin, so
|
|
322
|
-
// page is asked to load once more — this time already
|
|
323
|
-
// Until that settles, snapshots are skipped: the
|
|
324
|
-
// page still has empty storage, and saving that
|
|
325
|
-
// very snapshot being restored.
|
|
326
|
-
let storageState =
|
|
349
|
+
// Storage and cookies can only be written while the page is on
|
|
350
|
+
// its own origin, so both are restored after the first load and
|
|
351
|
+
// the page is then asked to load once more — this time already
|
|
352
|
+
// signed in. Until that settles, snapshots are skipped: the
|
|
353
|
+
// freshly loaded page still has empty storage, and saving that
|
|
354
|
+
// would erase the very snapshot being restored.
|
|
355
|
+
let storageState = restored === null ? "none" : "pending"
|
|
327
356
|
element.addEventListener("did-finish-load", function () {
|
|
328
357
|
if (storageState !== "pending") return
|
|
329
358
|
storageState = "restoring"
|
|
330
|
-
Promise.resolve(
|
|
359
|
+
Promise.resolve(writeGuestState(element, restored))
|
|
331
360
|
.then(function () {
|
|
332
361
|
storageState = "done"
|
|
333
362
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|