dsh-multi-chat 0.4.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,180 @@
1
+ #requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ 启动多个 DSH web 实例(多窗口墙直接内嵌在官方界面里,无需独立服务器)。
5
+
6
+ .DESCRIPTION
7
+ 依次启动 `dsh web --port <n>`(默认 3080..3083 共 4 个实例)。每个实例的
8
+ 侧边栏底部都有「多窗口墙」按钮(安装 plugin/dsh-client-ui-multi-wall 后),
9
+ 点击即可在官方界面内看到所有实例并排显示。所有子进程 PID 记录到状态文件,
10
+ 供 stop-multi.ps1 一键停止。
11
+
12
+ 官方 CLI 出于安全禁止 `--host 0.0.0.0`(会向网络暴露远程代码执行),因此
13
+ 手机/远程访问请加 -Remote:脚本会在每个实例前启动一个带令牌认证的网关
14
+ (scripts/gateway.mjs),网关监听 0.0.0.0,实例本身保持仅本机回环。手机在
15
+ 同一内网打开 http://<局域网IP>:<网关端口>/,输入令牌即可登录使用。
16
+
17
+ .PARAMETER Ports
18
+ 逗号分隔的端口列表,例如 -Ports "3080,3081,3082,3084"。
19
+ 默认 "3080,3081,3082,3083"。
20
+
21
+ .PARAMETER Patch
22
+ 启用多窗口墙的 patch 文件(默认 patches/multi-wall.yml);若已把 insert
23
+ 写进 profile 的 cordis.patch.yml,可传 $null 跳过。
24
+
25
+ .PARAMETER Remote
26
+ 为每个实例启动一个带令牌认证的网关(0.0.0.0),用于手机/远程访问。
27
+
28
+ .PARAMETER Token
29
+ 网关登录令牌;-Remote 下必填(缺省则自动生成并打印)。
30
+
31
+ .PARAMETER GatewayPorts
32
+ 网关外部端口(逗号分隔,数量须与实例一致);默认 = 实例端口 + 5000,
33
+ 若落在 Windows 排除端口段(Hyper-V/WinNAT)则自动顺延。
34
+
35
+ .PARAMETER TlsCert / TlsKey
36
+ 提供 PEM 证书与私钥时,网关以 HTTPS(加密)对外服务。
37
+
38
+ .PARAMETER NoOpen
39
+ 不自动打开浏览器。
40
+
41
+ .EXAMPLE
42
+ .\start-multi.ps1 -Ports "3080,3081,3082,3084"
43
+ .\start-multi.ps1 -Ports "3080,3081" -NoOpen
44
+ .\start-multi.ps1 -Ports "3080,3081" -Remote -Token "my-secret"
45
+ .\start-multi.ps1 -Ports "3080" -Remote -Token "s3cret" -TlsCert cert.pem -TlsKey key.pem
46
+ #>
47
+ param(
48
+ [string]$Ports = "3080,3081,3082,3083",
49
+ [string]$Patch = "",
50
+ [switch]$Remote,
51
+ [string]$Token = "",
52
+ [string]$GatewayPorts = "",
53
+ [string]$TlsCert = "",
54
+ [string]$TlsKey = "",
55
+ [string]$Name = "DSH",
56
+ [switch]$NoOpen
57
+ )
58
+
59
+ $ErrorActionPreference = "Stop"
60
+ $root = Split-Path -Parent $PSScriptRoot
61
+ $stateFile = Join-Path $root ".wall-pids.json"
62
+
63
+ # 1) 解析端口列表
64
+ $portList = @($Ports -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^\d+$' })
65
+ if ($portList.Count -eq 0) { throw "没有有效的端口列表:$Ports" }
66
+
67
+ # 2) 解析 dsh 的真实 JS bin(npm 全局装的 dsh 在 PATH 里是 .ps1/.cmd shim,
68
+ # node 不能直接执行 shim,必须解析出真正的 bin.js)
69
+ function Get-DshJsBin {
70
+ $cmd = Get-Command dsh -ErrorAction SilentlyContinue
71
+ if (-not $cmd) { return $null }
72
+ $source = $cmd.Source
73
+ if ($source -match 'bin\.js$') { return $source }
74
+ if ($source -match '\.ps1$|\.cmd$') {
75
+ $content = Get-Content $source -Raw
76
+ if ($content -match 'node_modules[\\/]@deepseek-ai[\\/]dsh[\\/]([^\s"''%]+)') {
77
+ $rel = $Matches[1] -replace '/', '\'
78
+ $candidate = Join-Path (Split-Path $source) "node_modules\@deepseek-ai\dsh\$rel"
79
+ if (Test-Path $candidate) { return $candidate }
80
+ }
81
+ }
82
+ return $null
83
+ }
84
+ $dshJs = Get-DshJsBin
85
+ if (-not $dshJs) {
86
+ Write-Host "警告:无法解析 dsh 的 JS bin,将尝试直接调用 dsh 命令。"
87
+ $dshCmd = Get-Command dsh -ErrorAction SilentlyContinue
88
+ if (-not $dshCmd) { throw "未找到 dsh 命令,请先安装 @deepseek-ai/dsh(npm i -g @deepseek-ai/dsh)" }
89
+ }
90
+
91
+ # 3) 远程模式:令牌与网关端口
92
+ if ($Remote -and [string]::IsNullOrEmpty($Token)) {
93
+ $Token = -join ((48..57) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ })
94
+ Write-Host "已生成网关令牌:$Token (请保存!)"
95
+ }
96
+ $gwPortList = @()
97
+ if ($Remote) {
98
+ if (-not [string]::IsNullOrEmpty($GatewayPorts)) {
99
+ $gwPortList = @($GatewayPorts -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^\d+$' })
100
+ if ($gwPortList.Count -lt $portList.Count) { throw "GatewayPorts 数量($($gwPortList.Count))少于实例数量($($portList.Count))" }
101
+ } else {
102
+ # 默认实例端口 + 5000,避开 Windows 排除端口段与已占用端口
103
+ $excluded = @()
104
+ netsh interface ipv4 show excludedportrange protocol=tcp 2>$null |
105
+ Select-String '^\s*\d+\s+\d+\s*$' |
106
+ ForEach-Object {
107
+ if ($_ -match '^\s*(\d+)\s+(\d+)\s*$') { $excluded += ,@([int]$Matches[1], [int]$Matches[2]) }
108
+ }
109
+ foreach ($p in $portList) {
110
+ $candidate = [int]$p + 5000
111
+ for (; $candidate -lt [int]$p + 5500; $candidate++) {
112
+ $inUse = Get-NetTCPConnection -LocalPort $candidate -State Listen -ErrorAction SilentlyContinue
113
+ if ($inUse) { continue }
114
+ $blocked = $false
115
+ foreach ($range in $excluded) {
116
+ if ($candidate -ge $range[0] -and $candidate -le $range[1]) { $blocked = $true; break }
117
+ }
118
+ if (-not $blocked) { break }
119
+ }
120
+ $gwPortList += $candidate
121
+ if ($candidate -ne ([int]$p + 5000)) { Write-Host "网关端口 $([int]$p + 5000) 不可用,改用 $candidate" }
122
+ }
123
+ }
124
+ }
125
+
126
+ # 4) 启动每个 dsh web 实例
127
+ $pids = @()
128
+ $started = @()
129
+ $gateways = @()
130
+ foreach ($p in $portList) {
131
+ if ($dshJs) {
132
+ $args = @($dshJs, "web", "--port", $p)
133
+ $proc = Start-Process -FilePath (Get-Command node).Source -ArgumentList $args -PassThru -WindowStyle Hidden
134
+ } else {
135
+ $args = @("web", "--port", $p)
136
+ $proc = Start-Process -FilePath (Get-Command dsh).Source -ArgumentList $args -PassThru -WindowStyle Hidden
137
+ }
138
+ $pids += $proc.Id
139
+ $started += $p
140
+ Write-Host "dsh web --port $p (pid $($proc.Id))"
141
+ Start-Sleep -Milliseconds 800
142
+ }
143
+
144
+ # 5) 远程模式:每个实例前启动一个带令牌认证的网关
145
+ if ($Remote) {
146
+ $gatewayScript = Join-Path $root "scripts\gateway.mjs"
147
+ if (-not (Test-Path $gatewayScript)) { throw "未找到网关脚本:$gatewayScript" }
148
+ for ($i = 0; $i -lt $portList.Count; $i++) {
149
+ $gArgs = @($gatewayScript, "--target", "127.0.0.1:$($portList[$i])", "--listen", "0.0.0.0:$($gwPortList[$i])", "--token", $Token, "--name", $Name)
150
+ if ($TlsCert -and $TlsKey) { $gArgs += @("--tls-cert", $TlsCert, "--tls-key", $TlsKey) }
151
+ $gwProc = Start-Process -FilePath (Get-Command node).Source -ArgumentList $gArgs -PassThru -WindowStyle Hidden
152
+ $gateways += ,@{ port = [int]$portList[$i]; gatewayPort = [int]$gwPortList[$i]; pid = $gwProc.Id }
153
+ Write-Host "网关 :$($gwPortList[$i]) -> 127.0.0.1:$($portList[$i]) (pid $($gwProc.Id))"
154
+ Start-Sleep -Milliseconds 300
155
+ }
156
+ }
157
+
158
+ # 6) 记录 PID 状态
159
+ @{ pid = $pids; ports = $started; gateways = $gateways; startedAt = (Get-Date).ToString("o") } | ConvertTo-Json -Depth 4 | Set-Content -Path $stateFile -Encoding UTF8
160
+ Write-Host "状态已保存到 $stateFile"
161
+
162
+ # 7) 打印手机/远程访问地址 + 打开第一个实例
163
+ if ($Remote) {
164
+ $ips = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { -not $_.IPAddress.StartsWith("127.") -and $_.PrefixOrigin -ne "WellKnown" } | Select-Object -ExpandProperty IPAddress -Unique)
165
+ if ($ips.Count -eq 0) { $ips = @("localhost") }
166
+ $scheme = "http"
167
+ if ($TlsCert -and $TlsKey) { $scheme = "https" }
168
+ Write-Host ""
169
+ Write-Host "手机/远程访问(需输入令牌):"
170
+ for ($i = 0; $i -lt $gateways.Count; $i++) {
171
+ $g = $gateways[$i]
172
+ $urls = ($ips | ForEach-Object { "$scheme`://$_`:$($g.gatewayPort)/" }) -join " "
173
+ Write-Host " $urls -> dsh :$($g.port)"
174
+ }
175
+ Write-Host " 令牌:$Token"
176
+ if (-not $NoOpen) { Start-Process "$scheme`://127.0.0.1`:$($gwPortList[0])" }
177
+ } elseif (-not $NoOpen) {
178
+ Start-Process "http://127.0.0.1:$($started[0])"
179
+ }
180
+ Write-Host "完成:$($started -join ', ')。每个实例侧边栏底部都有「多窗口墙」按钮。"
@@ -0,0 +1,42 @@
1
+ #requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ 停止 start-multi.ps1 启动的全部进程(多个 dsh web 实例 + 认证网关)。
5
+
6
+ .DESCRIPTION
7
+ 读取 .wall-pids.json 中的 PID 列表(含 gateways 里的网关 PID),逐个停止;
8
+ 结束后删除状态文件。只停止本脚本记录过的进程,不会误杀其它 dsh 实例。
9
+
10
+ .EXAMPLE
11
+ .\stop-multi.ps1
12
+ #>
13
+ param()
14
+
15
+ $ErrorActionPreference = "Stop"
16
+ $root = Split-Path -Parent $PSScriptRoot
17
+ $stateFile = Join-Path $root ".wall-pids.json"
18
+
19
+ if (-not (Test-Path $stateFile)) {
20
+ Write-Host "没有找到状态文件 $stateFile —— 可能尚未通过 start-multi.ps1 启动。"
21
+ exit 0
22
+ }
23
+
24
+ $state = Get-Content $stateFile -Raw | ConvertFrom-Json
25
+ $stopped = 0
26
+
27
+ $gatewayPids = @()
28
+ foreach ($g in @($state.gateways)) { $gatewayPids += [int]$g.pid }
29
+ foreach ($id in @($state.pid) + $gatewayPids) {
30
+ $proc = Get-Process -Id $id -ErrorAction SilentlyContinue
31
+ if ($proc) {
32
+ Stop-Process -Id $id -Force -ErrorAction SilentlyContinue
33
+ $stopped++
34
+ Write-Host "已停止 pid $id"
35
+ } else {
36
+ Write-Host "pid $id 已不存在,跳过"
37
+ }
38
+ }
39
+
40
+ Remove-Item $stateFile -Force -ErrorAction SilentlyContinue
41
+ $ports = @($state.ports) -join ", "
42
+ Write-Host "已停止 $stopped 个进程(dsh 实例 $ports + 认证网关)。"