dsh-fish-pet 0.2.5 → 0.2.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6] - 2026-10-31
4
+
5
+ ### Changed
6
+ - 回复长度限制 60 字 → **30 字**(越短开口越快)
7
+ - 常驻 TTS 进程:单句合成 4 秒 → **2 秒**
8
+ - 分段流水线:先合成第一句就开播,后面边播边合成,开口等待再减半
9
+ - 中英历史隔离:中文模式只带中文历史、英文模式只带英文历史,避免串味
10
+
11
+ ### Fixed
12
+ - 中文提问偶尔用英文回答(历史残留英文带跑)
3
13
  ## [0.2.5] - 2026-10-31
4
14
 
5
15
  ### Added
package/README.md CHANGED
@@ -139,34 +139,50 @@ powershell -ExecutionPolicy Bypass -File tools\encrypt-keys.ps1
139
139
 
140
140
  之后鱼和耳朵会优先读 `deepseek.key.enc` / `siliconflow.key.enc`,明文文件会被自动删除。
141
141
 
142
- ## 配置文件
143
-
144
- 首次启动会生成 `%USERPROFILE%\.dsh\fish-pet\config.json`,可以改:
145
-
146
- ```json
147
- {
148
- "persona": {
149
- "name": "大肥鱼",
150
- "dad": "光光",
151
- "mom": "丽丽",
152
- "birthday": "2026年10月30日",
153
- "birthplace": "中国广西北海",
154
- "likes": "白饭、被摸头、主人夸它",
155
- "dislikes": "被说胖、被冷落、吵",
156
- "extra": "你说话简短、俏皮、微微傲娇,用中文,称呼用户为\"主人\",每次回复不超过60个字,不要用表情符号。"
157
- },
158
- "bubble": { "opacity": 0.15 },
159
- "voice": { "silenceGate": 1200, "boost": 1, "engine": "cloud", "localConf": 0.55 },
160
- "tts": { "engine": "edge", "voice": "zh-CN-XiaoxiaoNeural", "rate": "+12%", "pitch": "+10Hz" }
161
- }
162
- ```
163
-
164
- - `persona`:名字、家人、生日、出生地、喜好、厌恶、说话风格——改完重启鱼即生效
165
- - `bubble.opacity`:气泡文字透明度(0 = 全透明,1 = 不透明)
166
- - `voice.silenceGate`:说话要多响才算"开始说话"(越大越不灵敏)
167
- - `voice.boost`:麦克风增益倍数
168
- - `voice.engine`:语音识别引擎,`cloud`(硅基流动 Qwen3-ASR,最准)/ `local`(Windows 自带,免费但吃字)/ `auto`(本地优先、云端兜底)
169
- - `voice.localConf`:`auto` 模式下本地识别置信度门槛,低于它就转云端
142
+ ## 配置文件
143
+
144
+ 首次启动会生成 `%USERPROFILE%\.dsh\fish-pet\config.json`,可以改:
145
+
146
+ ```json
147
+ {
148
+ "persona": {
149
+ "name": "大肥鱼",
150
+ "nameEn": "Mia",
151
+ "dad": "光光",
152
+ "mom": "丽丽",
153
+ "birthday": "2026年10月30日",
154
+ "birthplace": "中国广西北海",
155
+ "likes": "白饭、被摸头、主人夸它",
156
+ "dislikes": "被说胖、被冷落、吵",
157
+ "lang": "auto",
158
+ "extra": "你说话简短、俏皮、微微傲娇,称呼用户为\"主人\",每次回复不超过30个字,不要用表情符号。"
159
+ },
160
+ "bubble": { "opacity": 0.15 },
161
+ "voice": { "silenceGate": 600, "silenceMs": 900, "boost": 1, "engine": "cloud", "localConf": 0.55 },
162
+ "tts": {
163
+ "engine": "edge",
164
+ "voice": "zh-CN-XiaoxiaoNeural",
165
+ "rate": "+12%",
166
+ "pitch": "+10Hz",
167
+ "voiceEn": "en-US-AnaNeural",
168
+ "rateEn": "+8%",
169
+ "pitchEn": "+15Hz"
170
+ }
171
+ }
172
+ ```
173
+
174
+ - `persona.name`:中文名(说中文时自称)
175
+ - `persona.nameEn`:**英文名**(说英文时自称,默认 `Mia`/米娅)
176
+ - `persona.dad` / `mom` / `birthday` / `birthplace`:家人、生日、出生地
177
+ - `persona.likes` / `dislikes`:喜好、厌恶
178
+ - `persona.lang`:语言模式,`zh`(只用中文)/ `en`(只用英文)/ `auto`(跟着你说,默认)
179
+ - `persona.extra`:说话风格与长度限制(默认 30 字以内,回复越短开口越快)
180
+ - `bubble.opacity`:气泡文字透明度(0 = 全透明,1 = 不透明)
181
+ - `voice.silenceGate`:说话要多响才算"开始说话"(越小越灵敏,默认 600)
182
+ - `voice.silenceMs`:说完后等多久算"讲完了"(英文建议 900~1100,太小会把句子切断)
183
+ - `voice.boost`:麦克风增益倍数
184
+ - `voice.engine`:语音识别引擎,`cloud`(硅基流动 Qwen3-ASR,最准)/ `local`(Windows 自带,免费但吃字)/ `auto`(本地优先、云端兜底)
185
+ - `voice.localConf`:`auto` 模式下本地识别置信度门槛,低于它就转云端
170
186
 
171
187
  ### 语音朗读(TTS)
172
188
 
@@ -2,7 +2,7 @@
2
2
  # 一次连续采集,检测到"说完"立刻上传,不再固定切片
3
3
  param(
4
4
  [int]$MaxMs = 10000,
5
- [int]$SilenceMs = 1100,
5
+ [int]$SilenceMs = 900,
6
6
  [int]$Boost = 1,
7
7
  [int]$SilenceGate = 600
8
8
  )
@@ -49,7 +49,7 @@ $StatePath = Join-Path $DataDir 'state.json'
49
49
  $ConfigPath = Join-Path $DataDir 'config.json'
50
50
  function Load-Config {
51
51
  $def = [ordered]@{
52
- persona = [ordered]@{ name = '大肥鱼'; nameEn = 'Mia'; dad = '光光'; mom = '丽丽'; birthday = '2026年10月30日'; birthplace = '中国广西北海'; likes = '白饭、被摸头、主人夸它'; dislikes = '被说胖、被冷落、吵'; ownerBirthday = ''; lang = 'auto'; extra = '你说话简短、俏皮、微微傲娇,用中文,称呼用户为"主人",每次回复不超过60个字,不要用表情符号。' }
52
+ persona = [ordered]@{ name = '大肥鱼'; nameEn = 'Mia'; dad = '光光'; mom = '丽丽'; birthday = '2026年10月30日'; birthplace = '中国广西北海'; likes = '白饭、被摸头、主人夸它'; dislikes = '被说胖、被冷落、吵'; ownerBirthday = ''; lang = 'auto'; extra = '你说话简短、俏皮、微微傲娇,用中文,称呼用户为"主人",每次回复不超过30个字,不要用表情符号。' }
53
53
  bubble = [ordered]@{ opacity = 0.15 }
54
54
  voice = [ordered]@{ silenceGate = 1200; boost = 1; engine = 'cloud'; localConf = 0.55 }
55
55
  tts = [ordered]@{ engine = 'edge'; voice = 'zh-CN-XiaoxiaoNeural'; rate = '+12%'; pitch = '+10Hz'; voiceEn = 'en-US-AnaNeural'; rateEn = '+8%'; pitchEn = '+15Hz' }
@@ -1137,38 +1137,135 @@ function Play-TtsFile([string]$mp3, [string]$voice) {
1137
1137
  if ($null -eq $script:mp) {
1138
1138
  Add-Type -AssemblyName PresentationCore
1139
1139
  $script:mp = New-Object System.Windows.Media.MediaPlayer
1140
- $script:mp.Add_MediaEnded({ $script:speakEndAt = (Get-Date) })
1140
+ $script:mp.Add_MediaEnded({ Complete-TtsSegment })
1141
1141
  $script:mp.Add_MediaOpened({
1142
1142
  try { if ($script:mp.NaturalDuration.HasTimeSpan) { $script:speakEndAt = (Get-Date).AddSeconds($script:mp.NaturalDuration.TimeSpan.TotalSeconds + 0.8) } } catch { }
1143
1143
  })
1144
1144
  }
1145
1145
  $script:mp.Open([uri]$mp3)
1146
- # 用时长估算"说到什么时候",避免 MediaEnded 提前误触发导致自己听见自己
1147
- $dur = 6.0
1148
- try { if ($script:mp.NaturalDuration.HasTimeSpan) { $dur = $script:mp.NaturalDuration.TimeSpan.TotalSeconds } } catch { }
1149
- if ($dur -lt 0.5) { $dur = [Math]::Max(3.0, $text.Length * 0.22) }
1150
- $script:speakEndAt = (Get-Date).AddSeconds($dur + 0.8)
1151
1146
  $script:mp.Play()
1152
1147
  Write-Err ('TTS-EDGE voice=' + $voice + ' file=' + (Get-Item $mp3).Length)
1153
1148
  try { Get-ChildItem $env:TEMP -Filter 'fish-tts-*.mp3' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddMinutes(-10) } | Remove-Item -Force -ErrorAction SilentlyContinue } catch { }
1154
1149
  } catch { Write-Err ('TTS-PLAY-ERR ' + $_.Exception.Message) }
1155
1150
  }
1156
1151
 
1152
+ # 把回复拆成 1~3 段,边合成边播,缩短"开口等待"
1153
+ function Split-TtsSegments([string]$text) {
1154
+ if (-not $text) { return @() }
1155
+ $parts = @($text -split '(?<=[。!?!?;;…\.\r\n])' | Where-Object { $_.Trim() })
1156
+ if ($parts.Count -le 1) { return @($text) }
1157
+ # 第一段尽量短(先开口),后面合并成最多 2 段
1158
+ $first = $parts[0].Trim()
1159
+ $rest = ''
1160
+ for ($i = 1; $i -lt $parts.Count; $i++) { $rest = $rest + $parts[$i] }
1161
+ $rest = $rest.Trim()
1162
+ if (-not $rest) { return @($text) }
1163
+ if ($rest.Length -le 26) { return ,@($first, $rest) }
1164
+ # 剩余拆成两半
1165
+ $mid = [Math]::Floor($rest.Length / 2)
1166
+ $cut = $rest.LastIndexOfAny([char[]]@(',', ',', ';', '、', ' ', '。', '.'), $mid)
1167
+ if ($cut -lt 8) { $cut = $mid }
1168
+ $a = $rest.Substring(0, $cut + 1).Trim()
1169
+ $b = $rest.Substring($cut + 1).Trim()
1170
+ if (-not $b) { return ,@($first, $a) }
1171
+ return ,@($first, $a, $b)
1172
+ }
1173
+
1174
+ function Start-TtsPipeline([string]$text, [string]$voice, [string]$rate, [string]$pitch) {
1175
+ $segs = Split-TtsSegments $text
1176
+ $script:ttsSegs = @($segs)
1177
+ $script:ttsSegVoice = $voice
1178
+ $script:ttsSegRate = $rate
1179
+ $script:ttsSegPitch = $pitch
1180
+ $script:ttsSegFiles = @()
1181
+ $script:ttsSegDone = @()
1182
+ $script:ttsSegPlayed = @()
1183
+ $script:ttsSegReq = @()
1184
+ for ($i = 0; $i -lt $script:ttsSegs.Count; $i++) {
1185
+ $script:ttsSegFiles += (Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3'))
1186
+ $script:ttsSegDone += $false
1187
+ $script:ttsSegPlayed += $false
1188
+ $script:ttsSegReq += ''
1189
+ }
1190
+ $script:ttsSegPlaying = -1
1191
+ Write-Err ('TTS-PIPE segs=' + $script:ttsSegs.Count + ' len=' + $text.Length)
1192
+ $script:ttsTimer.Start()
1193
+ }
1194
+
1195
+ function Request-TtsSegment([int]$idx) {
1196
+ if ($idx -lt 0 -or $idx -ge $script:ttsSegs.Count) { return }
1197
+ if ($script:ttsSegReq[$idx]) { return }
1198
+ try {
1199
+ $reqId = '{0:D2}{1}' -f $idx, ([guid]::NewGuid().ToString('N').Substring(0, 8))
1200
+ $reqFile = Join-Path $env:TEMP ('fish-tts-req-' + $reqId + '.json')
1201
+ $reqObj = @{
1202
+ id = $reqId
1203
+ text = $script:ttsSegs[$idx]
1204
+ voice = $script:ttsSegVoice
1205
+ rate = $script:ttsSegRate
1206
+ pitch = $script:ttsSegPitch
1207
+ out = $script:ttsSegFiles[$idx]
1208
+ status = $script:ttsStatusFile
1209
+ } | ConvertTo-Json -Compress
1210
+ [IO.File]::WriteAllText($reqFile, $reqObj, (New-Object Text.UTF8Encoding($false)))
1211
+ $script:ttsProc.StandardInput.WriteLine('@' + $reqFile)
1212
+ $script:ttsProc.StandardInput.Flush()
1213
+ $script:ttsSegReq[$idx] = $reqId
1214
+ Write-Err ('TTS-REQ idx=' + $idx + ' id=' + $reqId)
1215
+ } catch { Write-Err ('TTS-REQ-ERR ' + $_.Exception.Message) }
1216
+ }
1217
+
1218
+ function Complete-TtsSegment {
1219
+ try {
1220
+ $cur = $script:ttsSegPlaying
1221
+ if ($cur -ge 0 -and $cur -lt $script:ttsSegPlayed.Count) { $script:ttsSegPlayed[$cur] = $true }
1222
+ $script:ttsSegPlaying = -1
1223
+ $script:speakEndAt = (Get-Date)
1224
+ } catch { }
1225
+ }
1226
+
1227
+ # 启动常驻 TTS 进程(复用连接,比每次启动 python 快一倍)
1228
+ function Start-TtsWorker {
1229
+ try {
1230
+ if ($script:ttsProc -and -not $script:ttsProc.HasExited) { return $true }
1231
+ $workerPath = Join-Path (Split-Path -Parent $PSCommandPath) 'tts_worker.py'
1232
+ if (-not (Test-Path $workerPath)) { Write-Err 'TTS-WORKER-MISSING'; return $false }
1233
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
1234
+ $psi.FileName = 'python'
1235
+ $psi.Arguments = '"' + $workerPath + '"'
1236
+ $psi.UseShellExecute = $false
1237
+ $psi.RedirectStandardInput = $true
1238
+ $psi.CreateNoWindow = $true
1239
+ $script:ttsProc = [System.Diagnostics.Process]::Start($psi)
1240
+ Start-Sleep -Milliseconds 350
1241
+ if ($script:ttsProc.HasExited) { Write-Err 'TTS-WORKER-DIED'; return $false }
1242
+ $script:ttsStatusFile = Join-Path $env:TEMP 'fish-tts-status.txt'
1243
+ Write-Err ('TTS-WORKER-START pid=' + $script:ttsProc.Id)
1244
+ return $true
1245
+ } catch { Write-Err ('TTS-WORKER-ERR ' + $_.Exception.Message); return $false }
1246
+ }
1247
+
1157
1248
  function Speak-Text([string]$text) {
1158
1249
  if (-not $script:speakOn) { return }
1159
1250
  $talkFile = Join-Path $DataDir 'asr.talking'
1160
1251
  try { [IO.File]::WriteAllText($talkFile, '1') } catch { }
1161
1252
  if ($script:ttsEngine -eq 'edge') {
1162
1253
  try {
1163
- $mp3 = Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3')
1164
- # 英文回复用英文音色,中文用中文音色
1165
1254
  $spkLang = Get-TextLang $text
1166
1255
  if ($spkLang -eq 'en') { $vUse = $script:ttsVoiceEn; $rUse = $script:ttsRateEn; $pUse = $script:ttsPitchEn } else { $vUse = $script:ttsVoice; $rUse = $script:ttsRate; $pUse = $script:ttsPitch }
1167
- # 后台线程生成音频:不阻塞界面和游动
1256
+ if (Start-TtsWorker) {
1257
+ $script:ttsMode = 'worker'
1258
+ $script:ttsVoiceUsed = $vUse
1259
+ $script:ttsText = $text
1260
+ Start-TtsPipeline $text $vUse $rUse $pUse
1261
+ return
1262
+ }
1263
+ # 常驻进程起不来:退回后台线程整段合成
1168
1264
  if ($null -eq $script:ttsPool) {
1169
1265
  $script:ttsPool = [runspacefactory]::CreateRunspacePool(1, 2)
1170
1266
  $script:ttsPool.Open()
1171
1267
  }
1268
+ $mp3 = Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3')
1172
1269
  $ps = [PowerShell]::Create()
1173
1270
  $ps.RunspacePool = $script:ttsPool
1174
1271
  [void]$ps.AddScript({
@@ -1176,22 +1273,65 @@ function Speak-Text([string]$text) {
1176
1273
  & $exe -m edge_tts --voice $voice ('--rate=' + $rate) ('--pitch=' + $pitch) '--text' $txt '--write-media' $out 2>$null | Out-Null
1177
1274
  }).AddArgument('python').AddArgument($vUse).AddArgument($rUse).AddArgument($pUse).AddArgument($text).AddArgument($mp3)
1178
1275
  $script:ttsPS = $ps
1276
+ $script:ttsHandle = $ps.BeginInvoke()
1277
+ $script:ttsMode = 'thread'
1179
1278
  $script:ttsMp3 = $mp3
1180
- $script:ttsText = $text
1181
1279
  $script:ttsVoiceUsed = $vUse
1182
- $script:ttsHandle = $ps.BeginInvoke()
1280
+ $script:ttsText = $text
1183
1281
  $script:ttsTimer.Start()
1184
- Write-Err ('TTS-EDGE-START len=' + $text.Length + ' voice=' + $vUse)
1282
+ Write-Err ('TTS-EDGE-START len=' + $text.Length + ' voice=' + $vUse + ' mode=thread')
1185
1283
  return
1186
1284
  } catch { Write-Err ('TTS-EDGE-ERR ' + $_.Exception.Message) }
1187
1285
  }
1188
1286
  Speak-Sapi $text
1189
1287
  }
1190
1288
 
1191
- # 后台 TTS 完成检测(UI 线程轮询,拿回来再播放)
1289
+ # 流水线调度:边合成边播
1192
1290
  $script:ttsTimer = New-Object System.Windows.Forms.Timer
1193
- $script:ttsTimer.Interval = 100
1291
+ $script:ttsTimer.Interval = 90
1194
1292
  $script:ttsTimer.Add_Tick({
1293
+ # ---- 常驻进程 + 分段流水线 ----
1294
+ if ($script:ttsMode -eq 'worker' -and $script:ttsSegs.Count -gt 0) {
1295
+ # 1) 扫状态文件,标记完成的分段
1296
+ for ($i = 0; $i -lt $script:ttsSegs.Count; $i++) {
1297
+ if ($script:ttsSegDone[$i] -or -not $script:ttsSegReq[$i]) { continue }
1298
+ $rid = $script:ttsSegReq[$i]
1299
+ try {
1300
+ $hit = Get-Content $script:ttsStatusFile -ErrorAction SilentlyContinue | Where-Object { $_ -match ('^' + $rid + ' ') } | Select-Object -Last 1
1301
+ } catch { $hit = $null }
1302
+ if ($hit) {
1303
+ if ($hit -like ('*' + $rid + ' OK*')) { $script:ttsSegDone[$i] = $true }
1304
+ else { Write-Err ('TTS-SEG-FAIL idx=' + $i); $script:ttsSegDone[$i] = $true }
1305
+ }
1306
+ }
1307
+ # 2) 没有在播 → 找下一段
1308
+ if ($script:ttsSegPlaying -lt 0) {
1309
+ $next = -1
1310
+ for ($i = 0; $i -lt $script:ttsSegs.Count; $i++) { if (-not $script:ttsSegPlayed[$i]) { $next = $i; break } }
1311
+ if ($next -lt 0) {
1312
+ $script:ttsTimer.Stop()
1313
+ $script:ttsSegs = @()
1314
+ return
1315
+ }
1316
+ if ($script:ttsSegDone[$next]) {
1317
+ $file = $script:ttsSegFiles[$next]
1318
+ if ((Test-Path $file) -and (Get-Item $file).Length -gt 800) {
1319
+ $script:ttsSegPlaying = $next
1320
+ Play-TtsFile $file $script:ttsVoiceUsed
1321
+ } else {
1322
+ $script:ttsSegPlayed[$next] = $true
1323
+ }
1324
+ } elseif (-not $script:ttsSegReq[$next]) {
1325
+ Request-TtsSegment $next
1326
+ }
1327
+ } else {
1328
+ # 3) 正在播:预取下一段
1329
+ $nx = $script:ttsSegPlaying + 1
1330
+ if ($nx -lt $script:ttsSegs.Count -and -not $script:ttsSegDone[$nx] -and -not $script:ttsSegReq[$nx]) { Request-TtsSegment $nx }
1331
+ }
1332
+ return
1333
+ }
1334
+ # ---- 后台线程整段模式 ----
1195
1335
  if ($null -eq $script:ttsHandle) { $script:ttsTimer.Stop(); return }
1196
1336
  if (-not $script:ttsHandle.IsCompleted) { return }
1197
1337
  $script:ttsTimer.Stop()
@@ -1382,7 +1522,7 @@ function Start-Chat([string]$text) {
1382
1522
  $replyLang = Get-ReplyLang $text
1383
1523
  if ($replyLang -eq 'en') {
1384
1524
  $sys = 'YOU MUST REPLY IN ENGLISH ONLY, NO MATTER WHAT. You are a round, chubby blue fish living on the master''s Windows desktop. Your name is "' + $pn.nameEn + '" (in Chinese it is written 大肥鱼, said as Da Fei Yu). Your dad is Guangguang, your mom is Lili, your birthday is October 30 2026, you were born in Beihai, Guangxi, China. Answer questions about family/birthday/birthplace with these facts and never invent them. In English your name is always this one, never any other name; if earlier messages called you something different, correct them politely.'
1385
- $sys = $sys + ' Reply ONLY in English, max 40 words, playful and a little tsundere, call the user "master", no emoji.'
1525
+ $sys = $sys + ' Reply ONLY in English, max 20 words, playful and a little tsundere, call the user "master", no emoji.'
1386
1526
  if ($pn.likes) { $sys = $sys + ' You like: ' + $pn.likes + ' .' }
1387
1527
  if ($pn.dislikes) { $sys = $sys + ' You dislike: ' + $pn.dislikes + ' .' }
1388
1528
  } else {
@@ -1424,10 +1564,13 @@ function Start-Chat([string]$text) {
1424
1564
  if ($histEn.Count -gt 4) { $histEn = $histEn[($histEn.Count - 4)..($histEn.Count - 1)] }
1425
1565
  foreach ($m in $histEn) { $msgs += $m }
1426
1566
  } else {
1427
- foreach ($m in $script:chatHistory) { $msgs += $m }
1567
+ # 中文模式只带中文历史,避免被上一段英文带跑
1568
+ $histZh = @($script:chatHistory | Where-Object { (Get-TextLang ([string]$_.content)) -eq 'zh' })
1569
+ if ($histZh.Count -gt 10) { $histZh = $histZh[($histZh.Count - 10)..($histZh.Count - 1)] }
1570
+ foreach ($m in $histZh) { $msgs += $m }
1428
1571
  }
1429
1572
  $msgs += @{ role = 'user'; content = $text }
1430
- $body = @{ model = 'deepseek-flash'; messages = $msgs; max_tokens = 300; temperature = 0.9; thinking = @{ type = 'disabled' } } | ConvertTo-Json -Depth 6
1573
+ $body = @{ model = 'deepseek-flash'; messages = $msgs; max_tokens = 160; temperature = 0.9; thinking = @{ type = 'disabled' } } | ConvertTo-Json -Depth 6
1431
1574
  if ($null -eq $script:http) {
1432
1575
  $script:http = New-Object System.Net.Http.HttpClient
1433
1576
  $script:http.Timeout = [TimeSpan]::FromSeconds(40)
@@ -42,10 +42,18 @@ async def main():
42
42
  line = line.strip()
43
43
  if not line:
44
44
  continue
45
- try:
46
- req = json.loads(line)
47
- except Exception:
48
- continue
45
+ # 支持"@文件路径"形式:避免 Windows PowerShell 写 stdin 时的编码问题
46
+ if line.startswith("@"):
47
+ try:
48
+ with open(line[1:], "r", encoding="utf-8") as fh:
49
+ req = json.loads(fh.read())
50
+ except Exception:
51
+ continue
52
+ else:
53
+ try:
54
+ req = json.loads(line)
55
+ except Exception:
56
+ continue
49
57
  if req.get("cmd") == "quit":
50
58
  break
51
59
  try:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fish-pet",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Desktop blue fish pet for DeepSeek Harness: per-pixel transparent WinForms window, text chat, and cloud speech-to-text voice conversation.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",