dsh-chat-embed 0.0.0-stage → 0.6.0

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.
@@ -0,0 +1,272 @@
1
+ /**
2
+ * patch-asar.cjs - two idempotent patches for the desktop shell's embedded
3
+ * browser partitions (resources/app.asar -> lib/main.js).
4
+ *
5
+ * Patch 1 (persistent login): the shell names each workspace's partition
6
+ * `dsh-sidebar-browser-<random uuid>` - no `persist:` prefix (memory only) and
7
+ * a fresh uuid every launch, so every embedded webview is signed out on
8
+ * restart. Hashing the workspace identity instead makes it stable on disk.
9
+ *
10
+ * Patch 2 (no disk cache, optional): the same partition is created with
11
+ * `{ cache: false }`, so Chromium keeps Cookies / Local Storage / IndexedDB
12
+ * (the login) but stops writing the multi-megabyte HTTP and code caches.
13
+ *
14
+ * Both steps are idempotent: run build against the original asar or against an
15
+ * already patched one and the result is the same file.
16
+ *
17
+ * Usage:
18
+ * node patch-asar.cjs build [--from <asar>] [--out <asar>] [--no-cache-patch]
19
+ * node patch-asar.cjs verify [--from <asar>] [--out <asar>]
20
+ */
21
+
22
+ const crypto = require('node:crypto')
23
+ const fs = require('node:fs')
24
+ const path = require('node:path')
25
+ const os = require('node:os')
26
+
27
+ /**
28
+ * Install locations tried in order when neither --resources nor DSH_RESOURCES
29
+ * is given, so the tool works without an edit on any machine.
30
+ */
31
+ const RESOURCES_CANDIDATES = [
32
+ process.env.DSH_RESOURCES,
33
+ process.env.LOCALAPPDATA === undefined || process.env.LOCALAPPDATA === ''
34
+ ? undefined
35
+ : path.join(process.env.LOCALAPPDATA, 'Programs', 'DeepSeek Harness', 'resources'),
36
+ 'C:\\Program Files\\DeepSeek Harness\\resources',
37
+ 'C:\\Program Files (x86)\\DeepSeek Harness\\resources',
38
+ ]
39
+
40
+ const LOG = path.join(os.tmpdir(), 'dsh-chat-embed-patch-asar.log')
41
+
42
+ /** @returns {string | undefined} the resources directory that holds app.asar */
43
+ function detectResources() {
44
+ for (const candidate of RESOURCES_CANDIDATES) {
45
+ if (typeof candidate !== 'string' || candidate === '') continue
46
+ if (fs.existsSync(path.join(candidate, 'app.asar'))) return candidate
47
+ }
48
+ return undefined
49
+ }
50
+
51
+ const PERSIST_OLD = 'partition = `dsh-sidebar-browser-${randomUUID()}`;'
52
+ const PERSIST_NEW =
53
+ 'partition = `persist:dsh-sidebar-${createHash("sha256").update(workspace).digest("hex").slice(0, 24)}`;'
54
+ const CACHE_OLD = 'this.configureSession(session.fromPartition(partition));'
55
+ const CACHE_NEW = 'this.configureSession(session.fromPartition(partition, { cache: false }));'
56
+
57
+ function log(message) {
58
+ const line = `[${new Date().toISOString()}] ${message}`
59
+ console.log(line)
60
+ try {
61
+ fs.appendFileSync(LOG, line + '\n')
62
+ } catch (error) {
63
+ void error
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {string[]} argv
69
+ * @returns {{ mode: string, from?: string, out?: string, cachePatch: boolean, help: boolean }}
70
+ */
71
+ function parseArgs(argv) {
72
+ const options = { mode: 'build', from: undefined, out: undefined, cachePatch: true, help: false }
73
+ const rest = argv.slice(2)
74
+ let resources
75
+ for (let i = 0; i < rest.length; i++) {
76
+ const arg = rest[i]
77
+ if (arg === 'build' || arg === 'verify') options.mode = arg
78
+ else if (arg === '--resources') resources = rest[++i]
79
+ else if (arg === '--from') options.from = rest[++i]
80
+ else if (arg === '--out') options.out = rest[++i]
81
+ else if (arg === '--no-cache-patch') options.cachePatch = false
82
+ else if (arg === '--help' || arg === '-h') options.help = true
83
+ else {
84
+ console.error('unknown argument: ' + arg)
85
+ options.help = true
86
+ }
87
+ }
88
+ const resolved = resources === undefined ? detectResources() : resources
89
+ if (options.from === undefined && resolved !== undefined) options.from = path.join(resolved, 'app.asar')
90
+ if (options.out === undefined) {
91
+ options.out = resolved !== undefined ? path.join(resolved, 'app.asar.new') : undefined
92
+ }
93
+ return options
94
+ }
95
+
96
+ function usage() {
97
+ console.log(
98
+ [
99
+ 'patch-asar.cjs — persistent partitions + no disk cache for the embedded browser',
100
+ '',
101
+ 'Usage:',
102
+ ' node patch-asar.cjs build [--resources <dir>] [--from <asar>] [--out <asar>] [--no-cache-patch]',
103
+ ' node patch-asar.cjs verify [--resources <dir>] [--from <asar>] [--out <asar>]',
104
+ '',
105
+ '--resources the install directory that contains app.asar (default: $DSH_RESOURCES,',
106
+ ' then %LOCALAPPDATA%\\Programs\\DeepSeek Harness\\resources, then Program Files)',
107
+ '',
108
+ 'build writes <resources>/app.asar.new; replace app.asar with it while the app is closed.',
109
+ ].join('\n'),
110
+ )
111
+ }
112
+
113
+ function readAsar(file) {
114
+ const buf = fs.readFileSync(file)
115
+ const jsonLen = buf.readUInt32LE(12)
116
+ const headerStart = 16
117
+ const header = JSON.parse(buf.toString('utf8', headerStart, headerStart + jsonLen))
118
+ const pad = (4 - (jsonLen % 4)) % 4
119
+ const dataStart = headerStart + jsonLen + pad
120
+ return { buf, header, dataStart, data: buf.subarray(dataStart) }
121
+ }
122
+
123
+ function collect(header) {
124
+ const files = []
125
+ const walk = (node, prefix) => {
126
+ for (const [name, child] of Object.entries(node.files || {})) {
127
+ const p = prefix === '' ? name : prefix + '/' + name
128
+ if (child.files !== undefined) {
129
+ walk(child, p)
130
+ continue
131
+ }
132
+ files.push({ path: p, node: child })
133
+ }
134
+ }
135
+ walk(header, '')
136
+ return files
137
+ }
138
+
139
+ function integrityFor(content) {
140
+ const blockSize = 4194304
141
+ const blocks = []
142
+ for (let offset = 0; offset < content.length; offset += blockSize) {
143
+ blocks.push(
144
+ crypto
145
+ .createHash('sha256')
146
+ .update(content.subarray(offset, Math.min(offset + blockSize, content.length)))
147
+ .digest('hex'),
148
+ )
149
+ }
150
+ if (blocks.length === 0) blocks.push(crypto.createHash('sha256').update(Buffer.alloc(0)).digest('hex'))
151
+ return {
152
+ algorithm: 'SHA256',
153
+ hash: crypto.createHash('sha256').update(content).digest('hex'),
154
+ blockSize,
155
+ blocks,
156
+ }
157
+ }
158
+
159
+ function applyPatches(source, cachePatch) {
160
+ let text = source
161
+ const applied = []
162
+ if (text.includes(PERSIST_OLD)) {
163
+ text = text.replace(PERSIST_OLD, PERSIST_NEW)
164
+ applied.push('persist-partition')
165
+ }
166
+ if (cachePatch && text.includes(CACHE_OLD)) {
167
+ text = text.replace(CACHE_OLD, CACHE_NEW)
168
+ applied.push('no-disk-cache')
169
+ }
170
+ const ok = text.includes(PERSIST_NEW) && (cachePatch === false || text.includes(CACHE_NEW))
171
+ return { text, applied, ok }
172
+ }
173
+
174
+ function build(options) {
175
+ const { header, data } = readAsar(options.from)
176
+ const files = collect(header)
177
+ log(`source: ${options.from} (${files.length} entries, data ${data.length} bytes)`)
178
+
179
+ const packed = files
180
+ .filter((file) => file.node.unpacked !== true)
181
+ .sort((a, b) => Number(a.node.offset) - Number(b.node.offset))
182
+
183
+ const chunks = []
184
+ let cursor = 0
185
+ let patched = false
186
+
187
+ for (const file of packed) {
188
+ const offset = Number(file.node.offset)
189
+ const size = Number(file.node.size)
190
+ let content = data.subarray(offset, offset + size)
191
+
192
+ if (file.path === 'lib/main.js') {
193
+ const source = content.toString('utf8')
194
+ const result = applyPatches(source, options.cachePatch)
195
+ if (!result.ok) throw new Error('lib/main.js does not contain the expected patch sites')
196
+ content = Buffer.from(result.text, 'utf8')
197
+ file.node.size = content.length
198
+ file.node.integrity = integrityFor(content)
199
+ patched = true
200
+ log(`lib/main.js: applied [${result.applied.join(', ') || 'nothing (already patched)'}], ${content.length} bytes`)
201
+ }
202
+
203
+ file.node.offset = String(cursor)
204
+ chunks.push(content)
205
+ cursor += content.length
206
+ }
207
+
208
+ if (!patched) throw new Error('lib/main.js not found')
209
+
210
+ const newData = Buffer.concat(chunks)
211
+ const headerJson = JSON.stringify(header)
212
+ const headerBuf = Buffer.from(headerJson, 'utf8')
213
+ const pad = (4 - (headerBuf.length % 4)) % 4
214
+ const prefix = Buffer.alloc(16)
215
+ prefix.writeUInt32LE(4, 0)
216
+ prefix.writeUInt32LE(headerBuf.length + 8, 4)
217
+ prefix.writeUInt32LE(headerBuf.length + 4, 8)
218
+ prefix.writeUInt32LE(headerBuf.length, 12)
219
+
220
+ const out = Buffer.concat([prefix, headerBuf, Buffer.alloc(pad), newData])
221
+ fs.writeFileSync(options.out, out)
222
+ log(`wrote ${options.out} (${out.length} bytes)`)
223
+ verify(options)
224
+ }
225
+
226
+ function verify(options) {
227
+ if (!fs.existsSync(options.out)) throw new Error(options.out + ' is missing')
228
+ const base = readAsar(options.from)
229
+ const built = readAsar(options.out)
230
+ const baseFiles = collect(base.header)
231
+ const builtFiles = collect(built.header)
232
+
233
+ if (baseFiles.length !== builtFiles.length) {
234
+ throw new Error(`entry count changed: ${baseFiles.length} -> ${builtFiles.length}`)
235
+ }
236
+
237
+ const baseByPath = new Map(baseFiles.map((file) => [file.path, file]))
238
+ let compared = 0
239
+ let patchedSeen = false
240
+
241
+ for (const file of builtFiles) {
242
+ const before = baseByPath.get(file.path)
243
+ if (before === undefined) throw new Error(`new entry appeared: ${file.path}`)
244
+ if (file.node.unpacked === true || before.node.unpacked === true) {
245
+ if (Number(file.node.size) !== Number(before.node.size)) throw new Error(`size drift on unpacked ${file.path}`)
246
+ continue
247
+ }
248
+ const builtBytes = built.data.subarray(Number(file.node.offset), Number(file.node.offset) + Number(file.node.size))
249
+ if (file.path === 'lib/main.js') {
250
+ const text = builtBytes.toString('utf8')
251
+ if (!text.includes(PERSIST_NEW)) throw new Error('persist patch missing in rebuilt asar')
252
+ if (options.cachePatch !== false && !text.includes(CACHE_NEW)) throw new Error('cache patch missing in rebuilt asar')
253
+ patchedSeen = true
254
+ continue
255
+ }
256
+ const beforeBytes = base.data.subarray(Number(before.node.offset), Number(before.node.offset) + Number(before.node.size))
257
+ if (!builtBytes.equals(beforeBytes)) throw new Error(`content drift: ${file.path}`)
258
+ compared += 1
259
+ }
260
+
261
+ if (!patchedSeen) throw new Error('patched file not found during verification')
262
+ log(`verified: ${compared} files byte-identical, lib/main.js patched, ${builtFiles.length} entries total`)
263
+ }
264
+
265
+ const options = parseArgs(process.argv)
266
+ if (options.help === true || options.from === undefined || options.out === undefined) {
267
+ usage()
268
+ process.exit(options.help === true ? 0 : 1)
269
+ }
270
+ if (options.mode === 'build') build(options)
271
+ else verify(options)
272
+ log('done: ' + options.mode)
@@ -0,0 +1,124 @@
1
+ param(
2
+ [string]$Resources = $env:DSH_RESOURCES,
3
+ [string]$Exe
4
+ )
5
+
6
+ $ErrorActionPreference = 'Continue'
7
+
8
+ # Find the install without a hard-coded path, so the same script works on any
9
+ # machine. -Resources / DSH_RESOURCES override the search.
10
+ if (-not $Resources) {
11
+ $candidates = @()
12
+ if ($env:LOCALAPPDATA) { $candidates += (Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness\resources') }
13
+ $candidates += 'C:\Program Files\DeepSeek Harness\resources'
14
+ $candidates += 'C:\Program Files (x86)\DeepSeek Harness\resources'
15
+ foreach ($candidate in $candidates) {
16
+ if (Test-Path (Join-Path $candidate 'app.asar')) { $Resources = $candidate; break }
17
+ }
18
+ }
19
+ if (-not $Resources) {
20
+ Write-Host 'watchdog: cannot find the DSH install. Pass -Resources <dir> or set DSH_RESOURCES.'
21
+ exit 1
22
+ }
23
+ if (-not $Exe) { $Exe = Join-Path (Split-Path -Parent $Resources) 'DeepSeek Harness.exe' }
24
+
25
+ $res = $Resources
26
+ $asar = Join-Path $res 'app.asar'
27
+ $new = Join-Path $res 'app.asar.new'
28
+ $orig = Join-Path $res 'app.asar.orig'
29
+ $log = Join-Path $env:TEMP 'dsh-chat-embed-watchdog.log'
30
+ $mark = Join-Path $env:TEMP 'dsh-chat-embed-patched.txt'
31
+ $app = 'DeepSeek Harness'
32
+
33
+ function Log($message) {
34
+ Add-Content -Path $log -Value ("[{0}] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $message)
35
+ }
36
+
37
+ Log ('watchdog v4 started (pid ' + $PID + ', event-driven)')
38
+
39
+ if (Test-Path $mark) { Log 'already patched; exiting'; exit 0 }
40
+ if (-not (Test-Path $new)) { Log 'app.asar.new missing; exiting'; exit 1 }
41
+
42
+ # Pick the Electron main process: a DeepSeek Harness process whose parent is not
43
+ # itself another DeepSeek Harness process. Everything else is a child worker.
44
+ $alive = @(Get-CimInstance Win32_Process -Filter "Name='DeepSeek Harness.exe'" -ErrorAction SilentlyContinue)
45
+ $mainPid = 0
46
+ foreach ($proc in $alive) {
47
+ $parent = @($alive | Where-Object { $_.ProcessId -eq $proc.ParentProcessId })
48
+ if ($parent.Count -eq 0) { $mainPid = $proc.ProcessId; break }
49
+ }
50
+ if ($mainPid -eq 0 -and $alive.Count -gt 0) { $mainPid = ($alive | Select-Object -First 1).ProcessId }
51
+
52
+ if ($mainPid -ne 0) {
53
+ Log ('waiting on the main process handle (pid ' + $mainPid + ')')
54
+ # Blocks on the process handle (WaitForSingleObject). No polling, no CPU, and
55
+ # no -Timeout (its ceiling on Windows PowerShell is 32767 seconds).
56
+ while ($true) {
57
+ if (-not (Get-Process -Id $mainPid -ErrorAction SilentlyContinue)) { break }
58
+ try {
59
+ Wait-Process -Id $mainPid -ErrorAction Stop
60
+ } catch {
61
+ if (-not (Get-Process -Id $mainPid -ErrorAction SilentlyContinue)) { break }
62
+ Start-Sleep -Milliseconds 200
63
+ }
64
+ }
65
+ Log 'main process exited (handle event)'
66
+ } else {
67
+ Log 'the app is not running; patching immediately'
68
+ }
69
+
70
+ # Children can outlive the main process by a moment; wait for the tree to clear.
71
+ $deadline = (Get-Date).AddSeconds(30)
72
+ while ((Get-Date) -lt $deadline) {
73
+ if (-not (Get-Process -Name $app -ErrorAction SilentlyContinue)) { break }
74
+ Start-Sleep -Milliseconds 200
75
+ }
76
+ if (Get-Process -Name $app -ErrorAction SilentlyContinue) {
77
+ Log 'processes still alive after 30s; nothing changed'
78
+ exit 1
79
+ }
80
+
81
+ # Let Windows drop the memory-mapped asar, then swap with retries.
82
+ Start-Sleep -Milliseconds 600
83
+
84
+ # Pre-flight: re-verify the staged archive against the original before touching it.
85
+ $verifyScript = Join-Path $PSScriptRoot 'patch-asar.cjs'
86
+ try {
87
+ & 'node' $verifyScript 'verify' '--resources' $res *> $null
88
+ if ($LASTEXITCODE -ne 0) {
89
+ Log 'pre-flight verification failed; nothing changed'
90
+ exit 1
91
+ }
92
+ Log 'pre-flight verification passed (byte-identical except lib/main.js)'
93
+ } catch {
94
+ Log ('pre-flight verification unavailable: ' + $_.Exception.Message + '; continuing')
95
+ }
96
+ $swapped = $false
97
+ for ($attempt = 1; $attempt -le 30; $attempt++) {
98
+ try {
99
+ if (Test-Path $orig) { Move-Item -Force $orig ($orig + '.' + (Get-Date -Format 'yyyyMMdd-HHmmss')) }
100
+ Move-Item -Force $asar $orig
101
+ Move-Item -Force $new $asar
102
+ $swapped = $true
103
+ break
104
+ } catch {
105
+ Log ('attempt ' + $attempt + ' failed: ' + $_.Exception.Message)
106
+ if ((-not (Test-Path $asar)) -and (Test-Path $orig)) { Move-Item -Force $orig $asar }
107
+ Start-Sleep -Milliseconds 400
108
+ }
109
+ }
110
+
111
+ if (-not $swapped) { Log 'could not swap app.asar; original preserved'; exit 1 }
112
+
113
+ $size = (Get-Item $asar).Length
114
+ if ($size -lt 213202950) {
115
+ Log ('patched file looks wrong (' + $size + ' bytes); restoring original')
116
+ Move-Item -Force $asar ($asar + '.broken')
117
+ Move-Item -Force $orig $asar
118
+ exit 1
119
+ }
120
+
121
+ Set-Content -Path $mark -Value ('patched ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
122
+ Log ('patched: app.asar is now ' + $size + ' bytes; original kept as app.asar.orig')
123
+ Log 'done; the app was NOT started - open it yourself when ready'
124
+ exit 0
@@ -0,0 +1,12 @@
1
+ @echo off
2
+ rem Free the disposable Chromium caches of the embedded browser, keep the logins.
3
+ rem
4
+ rem Usage: 清理聊天缓存.cmd [partitions-dir]
5
+ rem The directory may also come from DSH_PARTITIONS; the usual locations are
6
+ rem tried otherwise. Close DSH before running this.
7
+ if "%~1"=="" (
8
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\shell-patch\clean-cache.ps1"
9
+ ) else (
10
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\shell-patch\clean-cache.ps1" -Partitions "%~1"
11
+ )
12
+ pause