dsh-chat-embed 0.0.0-stage → 0.5.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.
@@ -0,0 +1,76 @@
1
+ # clean-cache.ps1 - free the persistent sidebar-browser caches, keep the logins.
2
+ #
3
+ # The shell patch made `dsh-sidebar-browser-*` partitions persistent, so each
4
+ # workspace keeps its Chromium profile on disk. Most of that is disposable
5
+ # cache; the login itself lives in IndexedDB / Local Storage / Cookies, which
6
+ # this script never touches.
7
+ #
8
+ # The app must be closed: Chromium holds exclusive locks on the cache files.
9
+
10
+ param(
11
+ [string]$Partitions = $env:DSH_PARTITIONS
12
+ )
13
+
14
+ $ErrorActionPreference = 'Continue'
15
+
16
+ $app = 'DeepSeek Harness'
17
+ if (-not $Partitions) {
18
+ $candidates = @()
19
+ if ($env:APPDATA) { $candidates += (Join-Path $env:APPDATA '@deepseek-ai\dsh-desktop\Partitions') }
20
+ if ($env:APPDATA) { $candidates += (Join-Path $env:APPDATA 'DeepSeek Harness\Partitions') }
21
+ foreach ($candidate in $candidates) {
22
+ if (Test-Path $candidate) { $Partitions = $candidate; break }
23
+ }
24
+ }
25
+ $base = $Partitions
26
+ $cacheDirs = @(
27
+ 'Cache',
28
+ 'Code Cache',
29
+ 'GPUCache',
30
+ 'DawnGraphiteCache',
31
+ 'DawnWebGPUCache',
32
+ 'GrShaderCache',
33
+ 'ShaderCache',
34
+ 'Shared Dictionary'
35
+ )
36
+
37
+ if (-not $base) {
38
+ Write-Host ' 找不到 Partitions 目录:请用 -Partitions <dir> 指定,或设置 DSH_PARTITIONS。'
39
+ exit 1
40
+ }
41
+
42
+ $running = Get-Process -Name $app -ErrorAction SilentlyContinue
43
+ if ($running) {
44
+ Write-Host ''
45
+ Write-Host ' DSH 正在运行,缓存文件被占用,无法清理。' -ForegroundColor Yellow
46
+ Write-Host ' 请先完全退出 DSH(托盘/菜单「退出」),再运行本脚本。' -ForegroundColor Yellow
47
+ Write-Host ''
48
+ exit 1
49
+ }
50
+
51
+ if (-not (Test-Path $base)) {
52
+ Write-Host ' 没有找到 Partitions 目录,无需清理。'
53
+ exit 0
54
+ }
55
+
56
+ $before = (Get-ChildItem $base -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
57
+
58
+ foreach ($partition in (Get-ChildItem $base -Directory -ErrorAction SilentlyContinue)) {
59
+ $freed = 0
60
+ foreach ($name in $cacheDirs) {
61
+ $target = Join-Path $partition.FullName $name
62
+ if (-not (Test-Path $target)) { continue }
63
+ $size = (Get-ChildItem $target -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
64
+ Remove-Item -Recurse -Force $target -ErrorAction SilentlyContinue
65
+ if (-not (Test-Path $target)) { $freed += $size }
66
+ }
67
+ if ($freed -gt 0) {
68
+ Write-Host (" {0}: 释放 {1:N1} MB(登录数据保留)" -f $partition.Name, ($freed / 1MB))
69
+ }
70
+ }
71
+
72
+ $after = (Get-ChildItem $base -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
73
+ Write-Host ''
74
+ Write-Host (" 总计释放 {0:N1} MB,剩余 {1:N1} MB" -f (($before - $after) / 1MB), ($after / 1MB))
75
+ Write-Host ' 登录状态、Cookie、LocalStorage 均未改动。'
76
+ Write-Host ''
@@ -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