dsh-fish-pet 0.2.5 → 0.2.7

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.7] - 2026-10-31
4
+
5
+ ### Fixed
6
+ - 喂食没有明显反馈:现在会冒泡 + 说话(按主人上一句的语言选台词)
7
+ - 喂食语音和当前对话抢话:正在说话时只冒泡,不插话
8
+
9
+ ### Added
10
+ - `tools/check-versions.ps1`:一条命令检查 Gitee / GitHub / npm 三个渠道版本是否一致
11
+ ## [0.2.6] - 2026-10-31
12
+
13
+ ### Changed
14
+ - 回复长度限制 60 字 → **30 字**(越短开口越快)
15
+ - 常驻 TTS 进程:单句合成 4 秒 → **2 秒**
16
+ - 分段流水线:先合成第一句就开播,后面边播边合成,开口等待再减半
17
+ - 中英历史隔离:中文模式只带中文历史、英文模式只带英文历史,避免串味
18
+
19
+ ### Fixed
20
+ - 中文提问偶尔用英文回答(历史残留英文带跑)
3
21
  ## [0.2.5] - 2026-10-31
4
22
 
5
23
  ### 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' }
@@ -675,6 +675,17 @@ function Invoke-Feed {
675
675
  foreach ($dx in @(-26, 4, 30)) {
676
676
  $script:hearts += @{ dx = $dx; age = 0.0; life = 1.4 }
677
677
  }
678
+ # 给个明显的反馈:气泡 + 说话
679
+ $zhFeed = @('哼,就一口啊…再来一碗嘛', '白饭!主人最好了', '才、才不是为了你才吃的呢', '谢谢主人,鱼鱼吃饱了')
680
+ $enFeed = @('Hmph, only one bite? More please.', 'Rice! You are the best, master.', 'I am NOT eating because of you, okay?', 'Thank you master, Fatty Fish is full.')
681
+ $feedLine = ''
682
+ $feedLang = $script:langMode
683
+ if ($feedLang -eq 'auto') { try { $feedLang = Get-TextLang ([string]$script:lastUser) } catch { $feedLang = 'zh' } }
684
+ if ($feedLang -eq 'en') { $feedLine = Get-Random -InputObject $enFeed } else { $feedLine = Get-Random -InputObject $zhFeed }
685
+ Show-Bubble $feedLine
686
+ # 正在说话时只冒泡,不插话(否则语音和喂食不搭调)
687
+ $speechBusy = ($script:ttsSegPlaying -ge 0) -or (@($script:ttsSegs).Count -gt 0) -or ($null -ne $script:ttsHandle)
688
+ if (-not $speechBusy) { try { Speak-Text $feedLine } catch { } }
678
689
  Save-State
679
690
  } catch {
680
691
  $line = (Get-Date -Format 'HH:mm:ss') + ' FEED-ERR ' + $_.Exception.Message
@@ -1137,38 +1148,135 @@ function Play-TtsFile([string]$mp3, [string]$voice) {
1137
1148
  if ($null -eq $script:mp) {
1138
1149
  Add-Type -AssemblyName PresentationCore
1139
1150
  $script:mp = New-Object System.Windows.Media.MediaPlayer
1140
- $script:mp.Add_MediaEnded({ $script:speakEndAt = (Get-Date) })
1151
+ $script:mp.Add_MediaEnded({ Complete-TtsSegment })
1141
1152
  $script:mp.Add_MediaOpened({
1142
1153
  try { if ($script:mp.NaturalDuration.HasTimeSpan) { $script:speakEndAt = (Get-Date).AddSeconds($script:mp.NaturalDuration.TimeSpan.TotalSeconds + 0.8) } } catch { }
1143
1154
  })
1144
1155
  }
1145
1156
  $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
1157
  $script:mp.Play()
1152
1158
  Write-Err ('TTS-EDGE voice=' + $voice + ' file=' + (Get-Item $mp3).Length)
1153
1159
  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
1160
  } catch { Write-Err ('TTS-PLAY-ERR ' + $_.Exception.Message) }
1155
1161
  }
1156
1162
 
1163
+ # 把回复拆成 1~3 段,边合成边播,缩短"开口等待"
1164
+ function Split-TtsSegments([string]$text) {
1165
+ if (-not $text) { return @() }
1166
+ $parts = @($text -split '(?<=[。!?!?;;…\.\r\n])' | Where-Object { $_.Trim() })
1167
+ if ($parts.Count -le 1) { return @($text) }
1168
+ # 第一段尽量短(先开口),后面合并成最多 2 段
1169
+ $first = $parts[0].Trim()
1170
+ $rest = ''
1171
+ for ($i = 1; $i -lt $parts.Count; $i++) { $rest = $rest + $parts[$i] }
1172
+ $rest = $rest.Trim()
1173
+ if (-not $rest) { return @($text) }
1174
+ if ($rest.Length -le 26) { return ,@($first, $rest) }
1175
+ # 剩余拆成两半
1176
+ $mid = [Math]::Floor($rest.Length / 2)
1177
+ $cut = $rest.LastIndexOfAny([char[]]@(',', ',', ';', '、', ' ', '。', '.'), $mid)
1178
+ if ($cut -lt 8) { $cut = $mid }
1179
+ $a = $rest.Substring(0, $cut + 1).Trim()
1180
+ $b = $rest.Substring($cut + 1).Trim()
1181
+ if (-not $b) { return ,@($first, $a) }
1182
+ return ,@($first, $a, $b)
1183
+ }
1184
+
1185
+ function Start-TtsPipeline([string]$text, [string]$voice, [string]$rate, [string]$pitch) {
1186
+ $segs = Split-TtsSegments $text
1187
+ $script:ttsSegs = @($segs)
1188
+ $script:ttsSegVoice = $voice
1189
+ $script:ttsSegRate = $rate
1190
+ $script:ttsSegPitch = $pitch
1191
+ $script:ttsSegFiles = @()
1192
+ $script:ttsSegDone = @()
1193
+ $script:ttsSegPlayed = @()
1194
+ $script:ttsSegReq = @()
1195
+ for ($i = 0; $i -lt $script:ttsSegs.Count; $i++) {
1196
+ $script:ttsSegFiles += (Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3'))
1197
+ $script:ttsSegDone += $false
1198
+ $script:ttsSegPlayed += $false
1199
+ $script:ttsSegReq += ''
1200
+ }
1201
+ $script:ttsSegPlaying = -1
1202
+ Write-Err ('TTS-PIPE segs=' + $script:ttsSegs.Count + ' len=' + $text.Length)
1203
+ $script:ttsTimer.Start()
1204
+ }
1205
+
1206
+ function Request-TtsSegment([int]$idx) {
1207
+ if ($idx -lt 0 -or $idx -ge $script:ttsSegs.Count) { return }
1208
+ if ($script:ttsSegReq[$idx]) { return }
1209
+ try {
1210
+ $reqId = '{0:D2}{1}' -f $idx, ([guid]::NewGuid().ToString('N').Substring(0, 8))
1211
+ $reqFile = Join-Path $env:TEMP ('fish-tts-req-' + $reqId + '.json')
1212
+ $reqObj = @{
1213
+ id = $reqId
1214
+ text = $script:ttsSegs[$idx]
1215
+ voice = $script:ttsSegVoice
1216
+ rate = $script:ttsSegRate
1217
+ pitch = $script:ttsSegPitch
1218
+ out = $script:ttsSegFiles[$idx]
1219
+ status = $script:ttsStatusFile
1220
+ } | ConvertTo-Json -Compress
1221
+ [IO.File]::WriteAllText($reqFile, $reqObj, (New-Object Text.UTF8Encoding($false)))
1222
+ $script:ttsProc.StandardInput.WriteLine('@' + $reqFile)
1223
+ $script:ttsProc.StandardInput.Flush()
1224
+ $script:ttsSegReq[$idx] = $reqId
1225
+ Write-Err ('TTS-REQ idx=' + $idx + ' id=' + $reqId)
1226
+ } catch { Write-Err ('TTS-REQ-ERR ' + $_.Exception.Message) }
1227
+ }
1228
+
1229
+ function Complete-TtsSegment {
1230
+ try {
1231
+ $cur = $script:ttsSegPlaying
1232
+ if ($cur -ge 0 -and $cur -lt $script:ttsSegPlayed.Count) { $script:ttsSegPlayed[$cur] = $true }
1233
+ $script:ttsSegPlaying = -1
1234
+ $script:speakEndAt = (Get-Date)
1235
+ } catch { }
1236
+ }
1237
+
1238
+ # 启动常驻 TTS 进程(复用连接,比每次启动 python 快一倍)
1239
+ function Start-TtsWorker {
1240
+ try {
1241
+ if ($script:ttsProc -and -not $script:ttsProc.HasExited) { return $true }
1242
+ $workerPath = Join-Path (Split-Path -Parent $PSCommandPath) 'tts_worker.py'
1243
+ if (-not (Test-Path $workerPath)) { Write-Err 'TTS-WORKER-MISSING'; return $false }
1244
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
1245
+ $psi.FileName = 'python'
1246
+ $psi.Arguments = '"' + $workerPath + '"'
1247
+ $psi.UseShellExecute = $false
1248
+ $psi.RedirectStandardInput = $true
1249
+ $psi.CreateNoWindow = $true
1250
+ $script:ttsProc = [System.Diagnostics.Process]::Start($psi)
1251
+ Start-Sleep -Milliseconds 350
1252
+ if ($script:ttsProc.HasExited) { Write-Err 'TTS-WORKER-DIED'; return $false }
1253
+ $script:ttsStatusFile = Join-Path $env:TEMP 'fish-tts-status.txt'
1254
+ Write-Err ('TTS-WORKER-START pid=' + $script:ttsProc.Id)
1255
+ return $true
1256
+ } catch { Write-Err ('TTS-WORKER-ERR ' + $_.Exception.Message); return $false }
1257
+ }
1258
+
1157
1259
  function Speak-Text([string]$text) {
1158
1260
  if (-not $script:speakOn) { return }
1159
1261
  $talkFile = Join-Path $DataDir 'asr.talking'
1160
1262
  try { [IO.File]::WriteAllText($talkFile, '1') } catch { }
1161
1263
  if ($script:ttsEngine -eq 'edge') {
1162
1264
  try {
1163
- $mp3 = Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3')
1164
- # 英文回复用英文音色,中文用中文音色
1165
1265
  $spkLang = Get-TextLang $text
1166
1266
  if ($spkLang -eq 'en') { $vUse = $script:ttsVoiceEn; $rUse = $script:ttsRateEn; $pUse = $script:ttsPitchEn } else { $vUse = $script:ttsVoice; $rUse = $script:ttsRate; $pUse = $script:ttsPitch }
1167
- # 后台线程生成音频:不阻塞界面和游动
1267
+ if (Start-TtsWorker) {
1268
+ $script:ttsMode = 'worker'
1269
+ $script:ttsVoiceUsed = $vUse
1270
+ $script:ttsText = $text
1271
+ Start-TtsPipeline $text $vUse $rUse $pUse
1272
+ return
1273
+ }
1274
+ # 常驻进程起不来:退回后台线程整段合成
1168
1275
  if ($null -eq $script:ttsPool) {
1169
1276
  $script:ttsPool = [runspacefactory]::CreateRunspacePool(1, 2)
1170
1277
  $script:ttsPool.Open()
1171
1278
  }
1279
+ $mp3 = Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3')
1172
1280
  $ps = [PowerShell]::Create()
1173
1281
  $ps.RunspacePool = $script:ttsPool
1174
1282
  [void]$ps.AddScript({
@@ -1176,22 +1284,65 @@ function Speak-Text([string]$text) {
1176
1284
  & $exe -m edge_tts --voice $voice ('--rate=' + $rate) ('--pitch=' + $pitch) '--text' $txt '--write-media' $out 2>$null | Out-Null
1177
1285
  }).AddArgument('python').AddArgument($vUse).AddArgument($rUse).AddArgument($pUse).AddArgument($text).AddArgument($mp3)
1178
1286
  $script:ttsPS = $ps
1287
+ $script:ttsHandle = $ps.BeginInvoke()
1288
+ $script:ttsMode = 'thread'
1179
1289
  $script:ttsMp3 = $mp3
1180
- $script:ttsText = $text
1181
1290
  $script:ttsVoiceUsed = $vUse
1182
- $script:ttsHandle = $ps.BeginInvoke()
1291
+ $script:ttsText = $text
1183
1292
  $script:ttsTimer.Start()
1184
- Write-Err ('TTS-EDGE-START len=' + $text.Length + ' voice=' + $vUse)
1293
+ Write-Err ('TTS-EDGE-START len=' + $text.Length + ' voice=' + $vUse + ' mode=thread')
1185
1294
  return
1186
1295
  } catch { Write-Err ('TTS-EDGE-ERR ' + $_.Exception.Message) }
1187
1296
  }
1188
1297
  Speak-Sapi $text
1189
1298
  }
1190
1299
 
1191
- # 后台 TTS 完成检测(UI 线程轮询,拿回来再播放)
1300
+ # 流水线调度:边合成边播
1192
1301
  $script:ttsTimer = New-Object System.Windows.Forms.Timer
1193
- $script:ttsTimer.Interval = 100
1302
+ $script:ttsTimer.Interval = 90
1194
1303
  $script:ttsTimer.Add_Tick({
1304
+ # ---- 常驻进程 + 分段流水线 ----
1305
+ if ($script:ttsMode -eq 'worker' -and $script:ttsSegs.Count -gt 0) {
1306
+ # 1) 扫状态文件,标记完成的分段
1307
+ for ($i = 0; $i -lt $script:ttsSegs.Count; $i++) {
1308
+ if ($script:ttsSegDone[$i] -or -not $script:ttsSegReq[$i]) { continue }
1309
+ $rid = $script:ttsSegReq[$i]
1310
+ try {
1311
+ $hit = Get-Content $script:ttsStatusFile -ErrorAction SilentlyContinue | Where-Object { $_ -match ('^' + $rid + ' ') } | Select-Object -Last 1
1312
+ } catch { $hit = $null }
1313
+ if ($hit) {
1314
+ if ($hit -like ('*' + $rid + ' OK*')) { $script:ttsSegDone[$i] = $true }
1315
+ else { Write-Err ('TTS-SEG-FAIL idx=' + $i); $script:ttsSegDone[$i] = $true }
1316
+ }
1317
+ }
1318
+ # 2) 没有在播 → 找下一段
1319
+ if ($script:ttsSegPlaying -lt 0) {
1320
+ $next = -1
1321
+ for ($i = 0; $i -lt $script:ttsSegs.Count; $i++) { if (-not $script:ttsSegPlayed[$i]) { $next = $i; break } }
1322
+ if ($next -lt 0) {
1323
+ $script:ttsTimer.Stop()
1324
+ $script:ttsSegs = @()
1325
+ return
1326
+ }
1327
+ if ($script:ttsSegDone[$next]) {
1328
+ $file = $script:ttsSegFiles[$next]
1329
+ if ((Test-Path $file) -and (Get-Item $file).Length -gt 800) {
1330
+ $script:ttsSegPlaying = $next
1331
+ Play-TtsFile $file $script:ttsVoiceUsed
1332
+ } else {
1333
+ $script:ttsSegPlayed[$next] = $true
1334
+ }
1335
+ } elseif (-not $script:ttsSegReq[$next]) {
1336
+ Request-TtsSegment $next
1337
+ }
1338
+ } else {
1339
+ # 3) 正在播:预取下一段
1340
+ $nx = $script:ttsSegPlaying + 1
1341
+ if ($nx -lt $script:ttsSegs.Count -and -not $script:ttsSegDone[$nx] -and -not $script:ttsSegReq[$nx]) { Request-TtsSegment $nx }
1342
+ }
1343
+ return
1344
+ }
1345
+ # ---- 后台线程整段模式 ----
1195
1346
  if ($null -eq $script:ttsHandle) { $script:ttsTimer.Stop(); return }
1196
1347
  if (-not $script:ttsHandle.IsCompleted) { return }
1197
1348
  $script:ttsTimer.Stop()
@@ -1382,7 +1533,7 @@ function Start-Chat([string]$text) {
1382
1533
  $replyLang = Get-ReplyLang $text
1383
1534
  if ($replyLang -eq 'en') {
1384
1535
  $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.'
1536
+ $sys = $sys + ' Reply ONLY in English, max 20 words, playful and a little tsundere, call the user "master", no emoji.'
1386
1537
  if ($pn.likes) { $sys = $sys + ' You like: ' + $pn.likes + ' .' }
1387
1538
  if ($pn.dislikes) { $sys = $sys + ' You dislike: ' + $pn.dislikes + ' .' }
1388
1539
  } else {
@@ -1424,10 +1575,13 @@ function Start-Chat([string]$text) {
1424
1575
  if ($histEn.Count -gt 4) { $histEn = $histEn[($histEn.Count - 4)..($histEn.Count - 1)] }
1425
1576
  foreach ($m in $histEn) { $msgs += $m }
1426
1577
  } else {
1427
- foreach ($m in $script:chatHistory) { $msgs += $m }
1578
+ # 中文模式只带中文历史,避免被上一段英文带跑
1579
+ $histZh = @($script:chatHistory | Where-Object { (Get-TextLang ([string]$_.content)) -eq 'zh' })
1580
+ if ($histZh.Count -gt 10) { $histZh = $histZh[($histZh.Count - 10)..($histZh.Count - 1)] }
1581
+ foreach ($m in $histZh) { $msgs += $m }
1428
1582
  }
1429
1583
  $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
1584
+ $body = @{ model = 'deepseek-flash'; messages = $msgs; max_tokens = 160; temperature = 0.9; thinking = @{ type = 'disabled' } } | ConvertTo-Json -Depth 6
1431
1585
  if ($null -eq $script:http) {
1432
1586
  $script:http = New-Object System.Net.Http.HttpClient
1433
1587
  $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.7",
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",
@@ -0,0 +1,61 @@
1
+ # check-versions.ps1 · 检查 Gitee / GitHub / npm 三个渠道的版本是否一致
2
+ # 用法:powershell -ExecutionPolicy Bypass -File tools\check-versions.ps1
3
+ # 退出码:0 = 全部一致;1 = 有不一致
4
+
5
+ $ErrorActionPreference = 'Stop'
6
+ $pkgDir = Split-Path -Parent $PSScriptRoot
7
+ $reg = 'https://registry.npmjs.org'
8
+
9
+ Push-Location $pkgDir
10
+ try {
11
+ $localVer = (Get-Content 'package.json' -Raw | ConvertFrom-Json).version
12
+
13
+ $gitVer = ''
14
+ try { $gitVer = (git show HEAD:package.json | Out-String | ConvertFrom-Json).version } catch { $gitVer = '(读取失败)' }
15
+
16
+ $gitHubVer = ''
17
+ try { $gitHubVer = (git show 'github/main:package.json' | Out-String | ConvertFrom-Json).version } catch { $gitHubVer = '(未同步)' }
18
+
19
+ $npmVer = ''
20
+ try {
21
+ $npmVer = (& npm view dsh-fish-pet version --registry=$reg 2>$null | Out-String).Trim()
22
+ if (-not $npmVer) { $npmVer = '(查询失败)' }
23
+ } catch { $npmVer = '(查询失败)' }
24
+
25
+ $gitHead = (git rev-parse --short HEAD 2>$null | Out-String).Trim()
26
+ $gitGitee = (git rev-parse --short origin/main 2>$null | Out-String).Trim()
27
+ $gitGH = (git rev-parse --short github/main 2>$null | Out-String).Trim()
28
+
29
+ Write-Host ''
30
+ Write-Host '渠道 版本 提交' -ForegroundColor Cyan
31
+ Write-Host ('本地工作区 {0,-10} {1}' -f $localVer, $gitHead)
32
+ Write-Host ('Gitee (origin) {0,-10} {1}' -f $gitVer, $gitGitee)
33
+ Write-Host ('GitHub {0,-10} {1}' -f $gitHubVer, $gitGH)
34
+ Write-Host ('npm (latest) {0,-10}' -f $npmVer)
35
+ Write-Host ''
36
+
37
+ $problems = @()
38
+ if ($localVer -ne $gitVer) { $problems += "本地 package.json($localVer) 与 Gitee($gitVer) 不一致——忘了提交?" }
39
+ if ($gitVer -ne $gitHubVer) { $problems += "Gitee($gitVer) 与 GitHub($gitHubVer) 不一致——GitHub 没推上去" }
40
+ if ($localVer -ne $npmVer) {
41
+ if ($npmVer -match '^\d') {
42
+ $problems += "本地($localVer) 与 npm($npmVer) 不一致——npm 可能还在审核,或没发布"
43
+ } else {
44
+ $problems += "npm 版本查询失败($npmVer),检查网络或 registry 配置"
45
+ }
46
+ }
47
+
48
+ if ($problems.Count -eq 0) {
49
+ Write-Host '✅ 三个渠道版本一致' -ForegroundColor Green
50
+ exit 0
51
+ } else {
52
+ Write-Host '⚠️ 发现不一致:' -ForegroundColor Yellow
53
+ $problems | ForEach-Object { Write-Host (" - " + $_) -ForegroundColor Yellow }
54
+ Write-Host ''
55
+ Write-Host '提示:npm 发布后要等自动审核(几分钟),审核通过 latest 才会跳到新版本。' -ForegroundColor DarkGray
56
+ exit 1
57
+ }
58
+ }
59
+ finally {
60
+ Pop-Location
61
+ }