dsh-fish-pet 0.2.1 → 0.2.3
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 +63 -0
- package/README.md +85 -5
- package/assets/asr-listener.ps1 +266 -83
- package/assets/fish-pet.ps1 +1657 -993
- package/lib/index.js +3 -2
- package/package.json +3 -1
- package/tools/encrypt-keys.ps1 +28 -0
- package/tools/publish.ps1 +46 -0
package/assets/fish-pet.ps1
CHANGED
|
@@ -1,1002 +1,1666 @@
|
|
|
1
|
-
# ============================================================
|
|
2
|
-
# 桌面蓝鱼 (Desktop Blue Fish) - DSH 插件版
|
|
3
|
-
# WinForms layered window + GDI+ 逐帧绘制,无外部依赖。
|
|
4
|
-
# 左键拖动 / 单击喂食,右键菜单。
|
|
5
|
-
# ============================================================
|
|
6
|
-
|
|
7
|
-
param(
|
|
8
|
-
[double]$Scale = 0.0,
|
|
9
|
-
[int]$Speed = 0,
|
|
10
|
-
[string]$Say = '',
|
|
11
|
-
[switch]$Listen
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
$ErrorActionPreference = 'Stop'
|
|
15
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
16
|
-
Add-Type -AssemblyName System.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
#
|
|
28
|
-
try {
|
|
29
|
-
New-Item -ItemType Directory -Force -Path (Split-Path $__pidFile) | Out-Null
|
|
30
|
-
$__json = [ordered]@{ pid = $PID; startedAt = (Get-Date).ToString('o'); script = $PSCommandPath } | ConvertTo-Json -Compress
|
|
31
|
-
[IO.File]::WriteAllText($__pidFile, $__json, (New-Object Text.UTF8Encoding($false)))
|
|
32
|
-
} catch { }
|
|
33
|
-
|
|
34
|
-
# 异常兜底:不允许 WinForms 弹「未处理异常」模态框(那就是闪退现场)
|
|
35
|
-
try {
|
|
36
|
-
[System.Windows.Forms.Application]::SetUnhandledExceptionMode([System.Windows.Forms.UnhandledExceptionMode]::CatchException)
|
|
37
|
-
[System.Windows.Forms.Application]::add_ThreadException({
|
|
38
|
-
param($sender, $eventArgs)
|
|
39
|
-
$line = (Get-Date -Format 'HH:mm:ss') + ' THREAD-EXC ' + $eventArgs.Exception.Message
|
|
40
|
-
try { Add-Content -LiteralPath (Join-Path $env:USERPROFILE '.dsh\fish-pet\error.log') -Value $line -Encoding UTF8 } catch { }
|
|
41
|
-
})
|
|
42
|
-
} catch { }
|
|
43
|
-
|
|
44
|
-
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
45
|
-
$DataDir = Join-Path $env:USERPROFILE '.dsh\fish-pet'
|
|
46
|
-
if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
|
|
47
|
-
|
|
48
|
-
$
|
|
49
|
-
$
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
1
|
+
# ============================================================
|
|
2
|
+
# 桌面蓝鱼 (Desktop Blue Fish) - DSH 插件版
|
|
3
|
+
# WinForms layered window + GDI+ 逐帧绘制,无外部依赖。
|
|
4
|
+
# 左键拖动 / 单击喂食,右键菜单。
|
|
5
|
+
# ============================================================
|
|
6
|
+
|
|
7
|
+
param(
|
|
8
|
+
[double]$Scale = 0.0,
|
|
9
|
+
[int]$Speed = 0,
|
|
10
|
+
[string]$Say = '',
|
|
11
|
+
[switch]$Listen
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
$ErrorActionPreference = 'Stop'
|
|
15
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
16
|
+
try { Add-Type -AssemblyName System.Net.Http -ErrorAction Stop } catch { }
|
|
17
|
+
Add-Type -AssemblyName System.Drawing
|
|
18
|
+
|
|
19
|
+
# 单实例:系统互斥量(比 pid 文件可靠,彻底防双开)
|
|
20
|
+
$__pidFile = Join-Path (Join-Path $env:USERPROFILE '.dsh\fish-pet') 'pet.pid'
|
|
21
|
+
try {
|
|
22
|
+
$__mutex = New-Object System.Threading.Mutex($false, 'Global\dsh-fish-pet-single')
|
|
23
|
+
$__got = $false
|
|
24
|
+
try { $__got = $__mutex.WaitOne(0, $false) } catch { $__got = $false }
|
|
25
|
+
if (-not $__got) { exit 0 }
|
|
26
|
+
} catch { }
|
|
27
|
+
# 写 pid,供耳朵/守护识别
|
|
28
|
+
try {
|
|
29
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $__pidFile) | Out-Null
|
|
30
|
+
$__json = [ordered]@{ pid = $PID; startedAt = (Get-Date).ToString('o'); script = $PSCommandPath } | ConvertTo-Json -Compress
|
|
31
|
+
[IO.File]::WriteAllText($__pidFile, $__json, (New-Object Text.UTF8Encoding($false)))
|
|
32
|
+
} catch { }
|
|
33
|
+
|
|
34
|
+
# 异常兜底:不允许 WinForms 弹「未处理异常」模态框(那就是闪退现场)
|
|
35
|
+
try {
|
|
36
|
+
[System.Windows.Forms.Application]::SetUnhandledExceptionMode([System.Windows.Forms.UnhandledExceptionMode]::CatchException)
|
|
37
|
+
[System.Windows.Forms.Application]::add_ThreadException({
|
|
38
|
+
param($sender, $eventArgs)
|
|
39
|
+
$line = (Get-Date -Format 'HH:mm:ss') + ' THREAD-EXC ' + $eventArgs.Exception.Message
|
|
40
|
+
try { Add-Content -LiteralPath (Join-Path $env:USERPROFILE '.dsh\fish-pet\error.log') -Value $line -Encoding UTF8 } catch { }
|
|
41
|
+
})
|
|
42
|
+
} catch { }
|
|
43
|
+
|
|
44
|
+
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
45
|
+
$DataDir = Join-Path $env:USERPROFILE '.dsh\fish-pet'
|
|
46
|
+
if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
|
|
47
|
+
try { Remove-Item (Join-Path $DataDir 'asr.stop') -Force -ErrorAction SilentlyContinue } catch { }
|
|
48
|
+
$StatePath = Join-Path $DataDir 'state.json'
|
|
49
|
+
$ConfigPath = Join-Path $DataDir 'config.json'
|
|
50
|
+
function Load-Config {
|
|
51
|
+
$def = [ordered]@{
|
|
52
|
+
persona = [ordered]@{ name = '大肥鱼'; dad = '光光'; mom = '丽丽'; birthday = '2026年10月30日'; birthplace = '中国广西北海'; likes = '白饭、被摸头、主人夸它'; dislikes = '被说胖、被冷落、吵'; ownerBirthday = ''; extra = '你说话简短、俏皮、微微傲娇,用中文,称呼用户为"主人",每次回复不超过60个字,不要用表情符号。' }
|
|
53
|
+
bubble = [ordered]@{ opacity = 0.15 }
|
|
54
|
+
voice = [ordered]@{ silenceGate = 1200; boost = 1; engine = 'cloud'; localConf = 0.55 }
|
|
55
|
+
tts = [ordered]@{ engine = 'edge'; voice = 'zh-CN-XiaoxiaoNeural'; rate = '+12%'; pitch = '+10Hz' }
|
|
56
|
+
}
|
|
57
|
+
$script:cfg = $def
|
|
58
|
+
try {
|
|
59
|
+
if (-not (Test-Path $ConfigPath)) {
|
|
60
|
+
[IO.File]::WriteAllText($ConfigPath, ($def | ConvertTo-Json -Depth 5), (New-Object Text.UTF8Encoding($false)))
|
|
61
|
+
}
|
|
62
|
+
$j = [IO.File]::ReadAllText($ConfigPath, [Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
63
|
+
if ($j.persona) { foreach ($k in @('name','dad','mom','extra','birthday','birthplace','likes','dislikes','ownerBirthday')) { $v = $j.persona.$k; if ($v) { $script:cfg.persona[$k] = [string]$v } } }
|
|
64
|
+
if ($j.bubble -and $j.bubble.opacity) { $script:cfg.bubble.opacity = [double]$j.bubble.opacity }
|
|
65
|
+
if ($j.tts) { foreach ($k2 in @('engine','voice','rate','pitch')) { $v2 = $j.tts.$k2; if ($v2) { $script:cfg.tts[$k2] = [string]$v2 } } }
|
|
66
|
+
if ($j.voice) {
|
|
67
|
+
if ($j.voice.silenceGate) { $script:cfg.voice.silenceGate = [int]$j.voice.silenceGate }
|
|
68
|
+
if ($j.voice.boost) { $script:cfg.voice.boost = [int]$j.voice.boost }
|
|
69
|
+
}
|
|
70
|
+
} catch { $script:cfg = $def }
|
|
71
|
+
}
|
|
72
|
+
Load-Config
|
|
73
|
+
$script:ttsEngine = $script:cfg.tts.engine
|
|
74
|
+
$script:ttsVoice = $script:cfg.tts.voice
|
|
75
|
+
$script:ttsRate = $script:cfg.tts.rate
|
|
76
|
+
$script:ttsPitch = $script:cfg.tts.pitch
|
|
77
|
+
$PidPath = Join-Path $DataDir 'pet.pid'
|
|
78
|
+
$ErrorPath = Join-Path $DataDir 'error.log'
|
|
79
|
+
|
|
80
|
+
# ---------- 分层窗口(逐像素 alpha,无锯齿毛边) ----------
|
|
81
|
+
$cs = @"
|
|
82
|
+
using System;
|
|
83
|
+
using System.Drawing;
|
|
84
|
+
using System.Runtime.InteropServices;
|
|
85
|
+
using System.Windows.Forms;
|
|
86
|
+
|
|
87
|
+
public class FishPetWindow : Form
|
|
88
|
+
{
|
|
89
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
90
|
+
private struct POINT { public int X; public int Y; }
|
|
91
|
+
|
|
92
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
93
|
+
private struct SIZE { public int cx; public int cy; }
|
|
94
|
+
|
|
95
|
+
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
|
96
|
+
private struct BLENDFUNCTION
|
|
97
|
+
{
|
|
98
|
+
public byte BlendOp;
|
|
99
|
+
public byte BlendFlags;
|
|
100
|
+
public byte SourceConstantAlpha;
|
|
101
|
+
public byte AlphaFormat;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[DllImport("user32.dll")] private static extern IntPtr GetDC(IntPtr hWnd);
|
|
105
|
+
[DllImport("user32.dll")] private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
|
106
|
+
[DllImport("user32.dll")] private static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst,
|
|
107
|
+
ref POINT pptDst, ref SIZE psize, IntPtr hdcSrc, ref POINT pptSrc,
|
|
108
|
+
int crKey, ref BLENDFUNCTION pblend, int dwFlags);
|
|
109
|
+
[DllImport("gdi32.dll")] private static extern IntPtr CreateCompatibleDC(IntPtr hDC);
|
|
110
|
+
[DllImport("gdi32.dll")] private static extern bool DeleteDC(IntPtr hdc);
|
|
111
|
+
[DllImport("gdi32.dll")] private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
|
112
|
+
[DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr hObject);
|
|
113
|
+
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
114
|
+
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
|
|
115
|
+
int X, int Y, int cx, int cy, uint uFlags);
|
|
116
|
+
|
|
117
|
+
private const int WS_EX_LAYERED = 0x00080000;
|
|
118
|
+
private const int WS_EX_TOOLWINDOW = 0x00000080;
|
|
119
|
+
private const int ULW_ALPHA = 0x00000002;
|
|
120
|
+
|
|
121
|
+
public FishPetWindow()
|
|
122
|
+
{
|
|
123
|
+
this.FormBorderStyle = FormBorderStyle.None;
|
|
124
|
+
this.ShowInTaskbar = false;
|
|
125
|
+
this.TopMost = true;
|
|
126
|
+
this.StartPosition = FormStartPosition.Manual;
|
|
127
|
+
this.Text = "fish-pet";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected override CreateParams CreateParams
|
|
131
|
+
{
|
|
132
|
+
get
|
|
133
|
+
{
|
|
134
|
+
CreateParams cp = base.CreateParams;
|
|
135
|
+
cp.ExStyle |= WS_EX_LAYERED | WS_EX_TOOLWINDOW;
|
|
136
|
+
return cp;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public void ShowNow()
|
|
141
|
+
{
|
|
142
|
+
// A task launched with -WindowStyle Hidden can keep the first window
|
|
143
|
+
// hidden; force it visible and back to the top of the z-order.
|
|
144
|
+
ShowWindow(this.Handle, 5);
|
|
145
|
+
SetWindowPos(this.Handle, (IntPtr)(-1), 0, 0, 0, 0, 0x0001 | 0x0002);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public void Push(Bitmap bmp, int x, int y)
|
|
149
|
+
{
|
|
150
|
+
IntPtr screenDc = GetDC(IntPtr.Zero);
|
|
151
|
+
IntPtr memDc = CreateCompatibleDC(screenDc);
|
|
152
|
+
IntPtr hBmp = IntPtr.Zero;
|
|
153
|
+
IntPtr old = IntPtr.Zero;
|
|
154
|
+
try
|
|
155
|
+
{
|
|
156
|
+
hBmp = bmp.GetHbitmap(Color.FromArgb(0));
|
|
157
|
+
old = SelectObject(memDc, hBmp);
|
|
158
|
+
|
|
159
|
+
POINT dst; dst.X = x; dst.Y = y;
|
|
160
|
+
POINT src; src.X = 0; src.Y = 0;
|
|
161
|
+
SIZE size; size.cx = bmp.Width; size.cy = bmp.Height;
|
|
162
|
+
|
|
163
|
+
BLENDFUNCTION blend;
|
|
164
|
+
blend.BlendOp = 0;
|
|
165
|
+
blend.BlendFlags = 0;
|
|
166
|
+
blend.SourceConstantAlpha = 255;
|
|
167
|
+
blend.AlphaFormat = 1;
|
|
168
|
+
|
|
169
|
+
UpdateLayeredWindow(this.Handle, screenDc, ref dst, ref size,
|
|
170
|
+
memDc, ref src, 0, ref blend, ULW_ALPHA);
|
|
171
|
+
}
|
|
172
|
+
finally
|
|
173
|
+
{
|
|
174
|
+
if (old != IntPtr.Zero) SelectObject(memDc, old);
|
|
175
|
+
if (hBmp != IntPtr.Zero) DeleteObject(hBmp);
|
|
176
|
+
DeleteDC(memDc);
|
|
177
|
+
ReleaseDC(IntPtr.Zero, screenDc);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
"@
|
|
153
182
|
Add-Type -TypeDefinition $cs -ReferencedAssemblies 'System.Drawing','System.Windows.Forms','System'
|
|
154
183
|
|
|
155
|
-
Add-Type -ReferencedAssemblies 'System.Drawing','System.Windows.Forms','System' -TypeDefinition @"
|
|
156
|
-
using System;
|
|
157
|
-
using System.Drawing;
|
|
158
|
-
using System.Drawing.Drawing2D;
|
|
159
|
-
using System.Windows.Forms;
|
|
160
|
-
|
|
161
|
-
public class CoolMenuRenderer : ToolStripProfessionalRenderer
|
|
162
|
-
{
|
|
163
|
-
public CoolMenuRenderer() : base(new ToolStripProfessionalRenderer().ColorTable) { this.RoundedEdges = true; }
|
|
164
|
-
|
|
165
|
-
public static GraphicsPath RoundRect(Rectangle r, int radius)
|
|
166
|
-
{
|
|
167
|
-
var p = new GraphicsPath();
|
|
168
|
-
int d = radius * 2;
|
|
169
|
-
p.AddArc(r.X, r.Y, d, d, 180, 90);
|
|
170
|
-
p.AddArc(r.Right - d - 1, r.Y, d, d, 270, 90);
|
|
171
|
-
p.AddArc(r.Right - d - 1, r.Bottom - d - 1, d, d, 0, 90);
|
|
172
|
-
p.AddArc(r.X, r.Bottom - d - 1, d, d, 90, 90);
|
|
173
|
-
p.CloseFigure();
|
|
174
|
-
return p;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
|
|
178
|
-
{
|
|
179
|
-
var r = e.ToolStrip.ClientRectangle;
|
|
180
|
-
if (r.Width <= 0 || r.Height <= 0) return;
|
|
181
|
-
using (var b = new LinearGradientBrush(r, Color.FromArgb(26, 40, 72), Color.FromArgb(12, 22, 44), 90f))
|
|
182
|
-
e.Graphics.FillRectangle(b, r);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
|
|
186
|
-
{
|
|
187
|
-
var r = new Rectangle(0, 0, e.ToolStrip.Width - 1, e.ToolStrip.Height - 1);
|
|
188
|
-
using (var pen = new Pen(Color.FromArgb(110, 190, 255), 1f))
|
|
189
|
-
using (var path = RoundRect(r, 12))
|
|
190
|
-
e.Graphics.DrawPath(pen, path);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
|
|
194
|
-
{
|
|
195
|
-
if (!e.Item.Selected || !e.Item.Enabled) return;
|
|
196
|
-
var r = new Rectangle(4, 1, e.Item.Width - 8, e.Item.Height - 2);
|
|
197
|
-
using (var path = RoundRect(r, 9))
|
|
198
|
-
{
|
|
199
|
-
using (var b = new LinearGradientBrush(r, Color.FromArgb(90, 190, 255), Color.FromArgb(40, 120, 210), 0f))
|
|
200
|
-
e.Graphics.FillPath(b, path);
|
|
201
|
-
using (var pen = new Pen(Color.FromArgb(150, 225, 255), 1f))
|
|
202
|
-
e.Graphics.DrawPath(pen, path);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
|
207
|
-
{
|
|
208
|
-
e.TextColor = e.Item.Selected ? Color.White : Color.FromArgb(214, 232, 252);
|
|
209
|
-
base.OnRenderItemText(e);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e)
|
|
213
|
-
{
|
|
214
|
-
using (var pen = new Pen(Color.FromArgb(70, 105, 150), 1f))
|
|
215
|
-
e.Graphics.DrawLine(pen, 14, e.Item.Height / 2, e.Item.Width - 14, e.Item.Height / 2);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
"@
|
|
219
|
-
|
|
220
|
-
# ---------- 画布与初始状态 ----------
|
|
221
|
-
$W = 320; $H = 240
|
|
222
|
-
$bmp = New-Object System.Drawing.Bitmap($W, $H, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
223
|
-
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
224
|
-
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
|
225
|
-
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
226
|
-
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
227
|
-
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
|
228
|
-
|
|
229
|
-
function New-Solid([string]$hex, [int]$a = 255) {
|
|
230
|
-
$c = [System.Drawing.ColorTranslator]::FromHtml($hex)
|
|
231
|
-
return New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb($a, $c.R, $c.G, $c.B))
|
|
232
|
-
}
|
|
233
|
-
function New-Pen([string]$hex, [float]$w, [int]$a = 255) {
|
|
234
|
-
$c = [System.Drawing.ColorTranslator]::FromHtml($hex)
|
|
235
|
-
$p = New-Object System.Drawing.Pen([System.Drawing.Color]::FromArgb($a, $c.R, $c.G, $c.B), $w)
|
|
236
|
-
$p.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
|
|
237
|
-
return $p
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
# 颜色卡(蓝白配色)
|
|
241
|
-
$C_OUTLINE = '#1B3B5F'
|
|
242
|
-
$C_BODY1 = '#9FD9FF'
|
|
243
|
-
$C_BODY2 = '#3F8FD8'
|
|
244
|
-
$C_BELLY = '#EAF7FF'
|
|
245
|
-
$C_FIN1 = '#BDE5FF'
|
|
246
|
-
$C_FIN2 = '#5EA9E6'
|
|
247
|
-
$C_CHEEK = '#FF9BB4'
|
|
248
|
-
$C_EYEWHITE= '#FFFFFF'
|
|
249
|
-
$C_PUPIL = '#132A45'
|
|
250
|
-
|
|
251
|
-
# ---------- 读档 ----------
|
|
252
|
-
$script:st = @{ x = $null; y = $null; dir = 1; scale = 1.0; feeds = 0 }
|
|
253
|
-
if (Test-Path $StatePath) {
|
|
254
|
-
try {
|
|
255
|
-
$j = Get-Content $StatePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
256
|
-
if ($null -ne $j.x) { $script:st.x = [int]$j.x }
|
|
257
|
-
if ($null -ne $j.y) { $script:st.y = [int]$j.y }
|
|
258
|
-
if ($null -ne $j.dir) { $script:st.dir = [int]$j.dir }
|
|
259
|
-
if ($null -ne $j.scale) { $script:st.scale = [double]$j.scale }
|
|
260
|
-
if ($null -ne $j.feeds) { $script:st.feeds = [int]$j.feeds }
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
$
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
$
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
$script:
|
|
273
|
-
$script:
|
|
274
|
-
|
|
275
|
-
$script:
|
|
276
|
-
$script:
|
|
277
|
-
$script:
|
|
278
|
-
$script:
|
|
279
|
-
$script:
|
|
280
|
-
$script:
|
|
281
|
-
$script:
|
|
282
|
-
$script:
|
|
283
|
-
$script:
|
|
284
|
-
$script:
|
|
285
|
-
$script:
|
|
286
|
-
$script:
|
|
287
|
-
$script:
|
|
288
|
-
$script:
|
|
289
|
-
$script:
|
|
290
|
-
$script:
|
|
291
|
-
$script:
|
|
292
|
-
$script:
|
|
293
|
-
$script:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
$
|
|
309
|
-
$
|
|
310
|
-
|
|
311
|
-
$
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
$
|
|
331
|
-
$
|
|
332
|
-
$
|
|
333
|
-
$
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
$
|
|
338
|
-
$
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
$g.
|
|
342
|
-
$g.
|
|
343
|
-
$
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
$
|
|
347
|
-
$
|
|
348
|
-
$
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
$
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
$
|
|
355
|
-
$
|
|
356
|
-
$g.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
$
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
$
|
|
365
|
-
$
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
$
|
|
371
|
-
$
|
|
372
|
-
$
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
$
|
|
379
|
-
$
|
|
380
|
-
$
|
|
381
|
-
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
$g.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
$
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
$
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
$g.FillEllipse((New-Solid '#
|
|
417
|
-
$g.
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
$
|
|
424
|
-
|
|
425
|
-
$
|
|
426
|
-
$
|
|
427
|
-
$
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
$
|
|
431
|
-
$
|
|
432
|
-
$g.
|
|
433
|
-
$
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
$
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
$
|
|
442
|
-
$
|
|
443
|
-
$
|
|
444
|
-
$
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
$
|
|
452
|
-
$
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
$
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
$
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
#
|
|
470
|
-
$
|
|
471
|
-
|
|
472
|
-
|
|
184
|
+
Add-Type -ReferencedAssemblies 'System.Drawing','System.Windows.Forms','System' -TypeDefinition @"
|
|
185
|
+
using System;
|
|
186
|
+
using System.Drawing;
|
|
187
|
+
using System.Drawing.Drawing2D;
|
|
188
|
+
using System.Windows.Forms;
|
|
189
|
+
|
|
190
|
+
public class CoolMenuRenderer : ToolStripProfessionalRenderer
|
|
191
|
+
{
|
|
192
|
+
public CoolMenuRenderer() : base(new ToolStripProfessionalRenderer().ColorTable) { this.RoundedEdges = true; }
|
|
193
|
+
|
|
194
|
+
public static GraphicsPath RoundRect(Rectangle r, int radius)
|
|
195
|
+
{
|
|
196
|
+
var p = new GraphicsPath();
|
|
197
|
+
int d = radius * 2;
|
|
198
|
+
p.AddArc(r.X, r.Y, d, d, 180, 90);
|
|
199
|
+
p.AddArc(r.Right - d - 1, r.Y, d, d, 270, 90);
|
|
200
|
+
p.AddArc(r.Right - d - 1, r.Bottom - d - 1, d, d, 0, 90);
|
|
201
|
+
p.AddArc(r.X, r.Bottom - d - 1, d, d, 90, 90);
|
|
202
|
+
p.CloseFigure();
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
|
|
207
|
+
{
|
|
208
|
+
var r = e.ToolStrip.ClientRectangle;
|
|
209
|
+
if (r.Width <= 0 || r.Height <= 0) return;
|
|
210
|
+
using (var b = new LinearGradientBrush(r, Color.FromArgb(26, 40, 72), Color.FromArgb(12, 22, 44), 90f))
|
|
211
|
+
e.Graphics.FillRectangle(b, r);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
|
|
215
|
+
{
|
|
216
|
+
var r = new Rectangle(0, 0, e.ToolStrip.Width - 1, e.ToolStrip.Height - 1);
|
|
217
|
+
using (var pen = new Pen(Color.FromArgb(110, 190, 255), 1f))
|
|
218
|
+
using (var path = RoundRect(r, 12))
|
|
219
|
+
e.Graphics.DrawPath(pen, path);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
|
|
223
|
+
{
|
|
224
|
+
if (!e.Item.Selected || !e.Item.Enabled) return;
|
|
225
|
+
var r = new Rectangle(4, 1, e.Item.Width - 8, e.Item.Height - 2);
|
|
226
|
+
using (var path = RoundRect(r, 9))
|
|
227
|
+
{
|
|
228
|
+
using (var b = new LinearGradientBrush(r, Color.FromArgb(90, 190, 255), Color.FromArgb(40, 120, 210), 0f))
|
|
229
|
+
e.Graphics.FillPath(b, path);
|
|
230
|
+
using (var pen = new Pen(Color.FromArgb(150, 225, 255), 1f))
|
|
231
|
+
e.Graphics.DrawPath(pen, path);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
|
236
|
+
{
|
|
237
|
+
e.TextColor = e.Item.Selected ? Color.White : Color.FromArgb(214, 232, 252);
|
|
238
|
+
base.OnRenderItemText(e);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e)
|
|
242
|
+
{
|
|
243
|
+
using (var pen = new Pen(Color.FromArgb(70, 105, 150), 1f))
|
|
244
|
+
e.Graphics.DrawLine(pen, 14, e.Item.Height / 2, e.Item.Width - 14, e.Item.Height / 2);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
"@
|
|
248
|
+
|
|
249
|
+
# ---------- 画布与初始状态 ----------
|
|
250
|
+
$W = 320; $H = 240
|
|
251
|
+
$bmp = New-Object System.Drawing.Bitmap($W, $H, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
252
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
253
|
+
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
|
254
|
+
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
255
|
+
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
256
|
+
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
|
257
|
+
|
|
258
|
+
function New-Solid([string]$hex, [int]$a = 255) {
|
|
259
|
+
$c = [System.Drawing.ColorTranslator]::FromHtml($hex)
|
|
260
|
+
return New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb($a, $c.R, $c.G, $c.B))
|
|
261
|
+
}
|
|
262
|
+
function New-Pen([string]$hex, [float]$w, [int]$a = 255) {
|
|
263
|
+
$c = [System.Drawing.ColorTranslator]::FromHtml($hex)
|
|
264
|
+
$p = New-Object System.Drawing.Pen([System.Drawing.Color]::FromArgb($a, $c.R, $c.G, $c.B), $w)
|
|
265
|
+
$p.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
|
|
266
|
+
return $p
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# 颜色卡(蓝白配色)
|
|
270
|
+
$C_OUTLINE = '#1B3B5F'
|
|
271
|
+
$C_BODY1 = '#9FD9FF'
|
|
272
|
+
$C_BODY2 = '#3F8FD8'
|
|
273
|
+
$C_BELLY = '#EAF7FF'
|
|
274
|
+
$C_FIN1 = '#BDE5FF'
|
|
275
|
+
$C_FIN2 = '#5EA9E6'
|
|
276
|
+
$C_CHEEK = '#FF9BB4'
|
|
277
|
+
$C_EYEWHITE= '#FFFFFF'
|
|
278
|
+
$C_PUPIL = '#132A45'
|
|
279
|
+
|
|
280
|
+
# ---------- 读档 ----------
|
|
281
|
+
$script:st = @{ x = $null; y = $null; dir = 1; scale = 1.0; feeds = 0; hunger = 30; intimacy = 10; mood = 'normal'; seen = $null }
|
|
282
|
+
if (Test-Path $StatePath) {
|
|
283
|
+
try {
|
|
284
|
+
$j = Get-Content $StatePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
285
|
+
if ($null -ne $j.x) { $script:st.x = [int]$j.x }
|
|
286
|
+
if ($null -ne $j.y) { $script:st.y = [int]$j.y }
|
|
287
|
+
if ($null -ne $j.dir) { $script:st.dir = [int]$j.dir }
|
|
288
|
+
if ($null -ne $j.scale) { $script:st.scale = [double]$j.scale }
|
|
289
|
+
if ($null -ne $j.feeds) { $script:st.feeds = [int]$j.feeds }
|
|
290
|
+
if ($null -ne $j.hunger) { $script:st.hunger = [double]$j.hunger }
|
|
291
|
+
if ($null -ne $j.intimacy) { $script:st.intimacy = [double]$j.intimacy }
|
|
292
|
+
if ($null -ne $j.mood) { $script:st.mood = [string]$j.mood }
|
|
293
|
+
if ($null -ne $j.seen) { $script:st.seen = [string]$j.seen }
|
|
294
|
+
} catch { }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
$wa = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
|
|
298
|
+
function Get-DefaultX { return [int]($wa.Right - $W - 30) }
|
|
299
|
+
function Get-DefaultY { return [int]($wa.Bottom - $H - 10) }
|
|
300
|
+
|
|
301
|
+
$script:x = if ($null -ne $script:st.x) { $script:st.x } else { Get-DefaultX }
|
|
302
|
+
$script:y = if ($null -ne $script:st.y) { $script:st.y } else { Get-DefaultY }
|
|
303
|
+
# 位置兜底:别让鱼跑到屏幕外
|
|
304
|
+
$script:x = [Math]::Max(0, [Math]::Min([int]$script:x, [int]($wa.Right - $W)))
|
|
305
|
+
$script:y = [Math]::Max(0, [Math]::Min([int]$script:y, [int]($wa.Bottom - $H - 4)))
|
|
306
|
+
$script:baseY = $script:y
|
|
307
|
+
$script:dir = if ($script:st.dir -ne 0) { $script:st.dir } else { 1 }
|
|
308
|
+
$script:scale = if ($Scale -gt 0) { $Scale } else { [double]$script:st.scale }
|
|
309
|
+
$script:speed = if ($Speed -gt 0) { [double]$Speed } else { 26.0 }
|
|
310
|
+
$script:vx = -$script:speed * $script:dir
|
|
311
|
+
$script:paused = $false
|
|
312
|
+
$script:dragging = $false
|
|
313
|
+
$script:dragMoved = 0
|
|
314
|
+
$script:dragOff = @{ x = 0; y = 0 }
|
|
315
|
+
$script:dragStart = @{ x = 0; y = 0 }
|
|
316
|
+
$script:dragActive = $false
|
|
317
|
+
$script:t = 0.0
|
|
318
|
+
$script:pulse = 1.0
|
|
319
|
+
$script:tx = $null; $script:ty = $null; $script:pauseUntil = 0.0
|
|
320
|
+
$script:lastPx = -9999; $script:lastPy = -9999
|
|
321
|
+
$script:satiety = 0.0
|
|
322
|
+
$script:feeds = [int]$script:st.feeds
|
|
323
|
+
# ---- 情绪状态:饥饿 / 亲密度 / 心情 ----
|
|
324
|
+
$script:hunger = [double]$script:st.hunger
|
|
325
|
+
$script:intimacy = [double]$script:st.intimacy
|
|
326
|
+
$script:mood = [string]$script:st.mood
|
|
327
|
+
if (-not $script:mood) { $script:mood = 'normal' }
|
|
328
|
+
if ($script:st.seen) {
|
|
329
|
+
try {
|
|
330
|
+
$hoursAway = ((Get-Date) - [datetime]::Parse([string]$script:st.seen)).TotalHours
|
|
331
|
+
if ($hoursAway -gt 0) {
|
|
332
|
+
$script:hunger = [Math]::Min(100, $script:hunger + $hoursAway * 3)
|
|
333
|
+
$script:intimacy = [Math]::Max(0, $script:intimacy - $hoursAway * 0.5)
|
|
334
|
+
}
|
|
335
|
+
} catch { }
|
|
336
|
+
}
|
|
337
|
+
$script:birthdayMMDD = ''
|
|
338
|
+
try { if ($script:cfg.persona.birthday -match '(\d{1,2})月(\d{1,2})日') { $script:birthdayMMDD = ('{0:D2}-{1:D2}' -f [int]$Matches[1], [int]$Matches[2]) } } catch { }
|
|
339
|
+
$script:moodTick = Get-Date
|
|
340
|
+
$script:moodUntil = $null
|
|
341
|
+
$script:bubbles = @()
|
|
342
|
+
$script:hearts = @()
|
|
343
|
+
$script:nextBubbleAt = 0.0
|
|
344
|
+
$script:nextBlinkAt = 3.0
|
|
345
|
+
$script:blinkUntil = -1.0
|
|
346
|
+
$script:nextTurnAt = 9.0
|
|
347
|
+
$script:lastSave = Get-Date
|
|
348
|
+
|
|
349
|
+
# ---------- 画鱼 ----------
|
|
350
|
+
function Draw-Fish {
|
|
351
|
+
param(
|
|
352
|
+
[System.Drawing.Graphics]$g,
|
|
353
|
+
[double]$t, [int]$dir, [double]$scale, [double]$pulse,
|
|
354
|
+
[bool]$blink, [int]$mood, [string]$expr = 'normal'
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
$g.Clear([System.Drawing.Color]::Transparent)
|
|
358
|
+
|
|
359
|
+
$bob = [Math]::Sin($t * 1.7) * 3.5
|
|
360
|
+
$sway = [Math]::Sin($t * 3.4) * 11.0
|
|
361
|
+
$up = [Math]::Sin($t * 2.1) * 2.0
|
|
362
|
+
$s = $scale * $pulse
|
|
363
|
+
|
|
364
|
+
$g.ResetTransform()
|
|
365
|
+
$g.TranslateTransform(160.0, 130.0)
|
|
366
|
+
$g.ScaleTransform([float]($s * $dir), [float]$s)
|
|
367
|
+
$g.TranslateTransform(-160.0, -130.0 + [float]$bob)
|
|
368
|
+
|
|
369
|
+
# ---- 尾鳍(绕 86,130 摆动)----
|
|
370
|
+
$stateTail = $g.Save()
|
|
371
|
+
$g.TranslateTransform(86.0, 130.0)
|
|
372
|
+
$g.RotateTransform([float]$sway)
|
|
373
|
+
$g.TranslateTransform(-86.0, -130.0)
|
|
374
|
+
$tail = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
375
|
+
$tail.AddBezier(88, 122, 52, 96, 22, 52, 12, 44)
|
|
376
|
+
$tail.AddBezier(12, 44, 30, 96, 40, 118, 40, 130)
|
|
377
|
+
$tail.AddBezier(40, 130, 40, 142, 30, 164, 12, 216)
|
|
378
|
+
$tail.AddBezier(12, 216, 22, 208, 52, 164, 88, 138)
|
|
379
|
+
$tail.CloseFigure()
|
|
380
|
+
$tailBrush = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
|
|
381
|
+
(New-Object System.Drawing.Point(12, 60)), (New-Object System.Drawing.Point(90, 150)),
|
|
382
|
+
[System.Drawing.ColorTranslator]::FromHtml($C_FIN1), [System.Drawing.ColorTranslator]::FromHtml($C_FIN2))
|
|
383
|
+
$g.FillPath($tailBrush, $tail)
|
|
384
|
+
$tailPen = New-Pen $C_OUTLINE 3.2
|
|
385
|
+
$g.DrawPath($tailPen, $tail)
|
|
386
|
+
$tailBrush.Dispose(); $tailPen.Dispose(); $tail.Dispose()
|
|
387
|
+
$g.Restore($stateTail)
|
|
388
|
+
|
|
389
|
+
# ---- 背鳍 ----
|
|
390
|
+
$dorsal = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
391
|
+
$dorsal.AddBezier(152, 74, 172, 34, 198, 24, 214, 56)
|
|
392
|
+
$dorsal.AddBezier(214, 56, 196, 68, 176, 76, 170, 82)
|
|
393
|
+
$dorsal.CloseFigure()
|
|
394
|
+
$g.FillPath((New-Solid $C_FIN2 235), $dorsal)
|
|
395
|
+
$g.DrawPath((New-Pen $C_OUTLINE 2.6), $dorsal)
|
|
396
|
+
$dorsal.Dispose()
|
|
397
|
+
|
|
398
|
+
# ---- 身体(大肥)----
|
|
399
|
+
$body = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
400
|
+
$body.AddEllipse(70, 60, 180, 140)
|
|
401
|
+
$bodyBrush = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
|
|
402
|
+
(New-Object System.Drawing.Point(150, 58)), (New-Object System.Drawing.Point(150, 202)),
|
|
403
|
+
[System.Drawing.ColorTranslator]::FromHtml($C_BODY1), [System.Drawing.ColorTranslator]::FromHtml($C_BODY2))
|
|
404
|
+
$g.FillPath($bodyBrush, $body)
|
|
405
|
+
|
|
406
|
+
# 肚子
|
|
407
|
+
$belly = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
408
|
+
$belly.AddEllipse(96, 112, 140, 82)
|
|
409
|
+
$g.FillPath((New-Solid $C_BELLY 210), $belly)
|
|
410
|
+
|
|
411
|
+
# 高光
|
|
412
|
+
$shine = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
413
|
+
$shine.AddEllipse(104, 74, 66, 26)
|
|
414
|
+
$g.FillPath((New-Solid '#FFFFFF' 70), $shine)
|
|
415
|
+
|
|
416
|
+
# 身体轮廓
|
|
417
|
+
$g.DrawPath((New-Pen $C_OUTLINE 3.4), $body)
|
|
418
|
+
$body.Dispose(); $belly.Dispose(); $shine.Dispose(); $bodyBrush.Dispose()
|
|
419
|
+
|
|
420
|
+
# ---- 胸鳍(贴在下腹边缘,随游动轻摆)----
|
|
421
|
+
$stateFin = $g.Save()
|
|
422
|
+
$g.TranslateTransform(146.0, 170.0)
|
|
423
|
+
$g.RotateTransform([float](-34 + [Math]::Sin($t * 4.2) * 8))
|
|
424
|
+
$g.TranslateTransform(-24.0, -10.0)
|
|
425
|
+
$g.FillEllipse((New-Solid $C_FIN1 245), (New-Object System.Drawing.RectangleF(0, 0, 48, 20)))
|
|
426
|
+
$g.DrawEllipse((New-Pen $C_OUTLINE 2.4), (New-Object System.Drawing.RectangleF(0, 0, 48, 20)))
|
|
427
|
+
$g.Restore($stateFin)
|
|
428
|
+
|
|
429
|
+
# ---- 鳃线 ----
|
|
430
|
+
$gill = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
431
|
+
$gill.AddBezier(176, 94, 188, 114, 188, 150, 174, 172)
|
|
432
|
+
$g.DrawPath((New-Pen '#3F86C4' 2.2 150), $gill)
|
|
433
|
+
$gill.Dispose()
|
|
434
|
+
|
|
435
|
+
# ---- 眼睛(随心情变化)----
|
|
436
|
+
if ($expr -eq 'happy') {
|
|
437
|
+
if ($blink) {
|
|
438
|
+
# 开心时也要眨眼
|
|
439
|
+
$bp2 = New-Pen $C_PUPIL 3.6
|
|
440
|
+
$g.DrawArc($bp2, 199, 104, 34, 20, 200, 140)
|
|
441
|
+
$bp2.Dispose()
|
|
442
|
+
} else {
|
|
443
|
+
$g.FillEllipse((New-Solid $C_EYEWHITE), 199, 93, 34, 34)
|
|
444
|
+
$g.FillEllipse((New-Solid $C_PUPIL), 207, 101, 20, 20)
|
|
445
|
+
$g.FillEllipse((New-Solid '#FFFFFF' 255), 219, 100, 8, 8)
|
|
446
|
+
$g.FillEllipse((New-Solid '#FFFFFF' 200), 211, 113, 6, 6)
|
|
447
|
+
}
|
|
448
|
+
} elseif ($expr -eq 'sad') {
|
|
449
|
+
# 委屈:眼睛半垂 + 一滴泪
|
|
450
|
+
$g.FillEllipse((New-Solid $C_EYEWHITE), 199, 95, 34, 30)
|
|
451
|
+
$g.FillEllipse((New-Solid $C_PUPIL), 208, 108, 18, 16)
|
|
452
|
+
$g.FillEllipse((New-Solid '#FFFFFF' 235), 218, 106, 6, 6)
|
|
453
|
+
$lid = New-Pen $C_PUPIL 3.0
|
|
454
|
+
$g.DrawArc($lid, 199, 88, 34, 22, 200, 140)
|
|
455
|
+
$lid.Dispose()
|
|
456
|
+
$g.FillEllipse((New-Solid '#7FC7F0' 200), 206, 128, 7, 11)
|
|
457
|
+
} elseif ($expr -eq 'angry') {
|
|
458
|
+
# 生气:眼睛压低 + 斜眉
|
|
459
|
+
$g.FillEllipse((New-Solid $C_EYEWHITE), 199, 97, 34, 30)
|
|
460
|
+
$g.FillEllipse((New-Solid $C_PUPIL), 207, 106, 20, 18)
|
|
461
|
+
$g.FillEllipse((New-Solid '#FFFFFF' 235), 219, 105, 6, 6)
|
|
462
|
+
$brow = New-Pen $C_OUTLINE 3.2
|
|
463
|
+
$g.DrawLine($brow, 197, 86, 232, 94)
|
|
464
|
+
$brow.Dispose()
|
|
465
|
+
} elseif ($blink) {
|
|
466
|
+
$blinkPen = New-Pen $C_PUPIL 3.6
|
|
467
|
+
$g.DrawArc($blinkPen, 199, 104, 34, 20, 200, 140)
|
|
468
|
+
$blinkPen.Dispose()
|
|
469
|
+
} else {
|
|
470
|
+
$g.FillEllipse((New-Solid $C_EYEWHITE), 199, 93, 34, 34)
|
|
471
|
+
$g.FillEllipse((New-Solid $C_PUPIL), 207, 101, 20, 20)
|
|
472
|
+
$g.FillEllipse((New-Solid '#FFFFFF' 235), 219, 100, 7, 7)
|
|
473
|
+
$g.FillEllipse((New-Solid '#FFFFFF' 110), 211, 113, 5, 5)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# ---- 腮红 ----
|
|
477
|
+
$g.FillEllipse((New-Solid $C_CHEEK 95), 202, 140, 30, 20)
|
|
478
|
+
|
|
479
|
+
# ---- 嘴 ----
|
|
480
|
+
$mouthPen = New-Pen $C_OUTLINE 2.8
|
|
481
|
+
if ($mood -eq 1) {
|
|
482
|
+
$g.FillEllipse((New-Solid '#C9526B'), 236, 128, 16, 12)
|
|
483
|
+
$mouthPen.Dispose()
|
|
484
|
+
$mouthPen = New-Pen $C_OUTLINE 2.2
|
|
485
|
+
$g.DrawEllipse($mouthPen, 236, 128, 16, 12)
|
|
486
|
+
} else {
|
|
487
|
+
$g.DrawArc($mouthPen, 236, 126, 20, 16, 338, 200)
|
|
488
|
+
}
|
|
489
|
+
$mouthPen.Dispose()
|
|
490
|
+
|
|
491
|
+
# ---- 泡泡 ----
|
|
492
|
+
foreach ($b in @($script:bubbles)) {
|
|
493
|
+
$bx = 250 + [Math]::Sin($b.seed + $b.age * 2.2) * 8
|
|
494
|
+
$by = 128 - $b.age * 46
|
|
495
|
+
$r = 5 + $b.size
|
|
496
|
+
$al = [int](180 * [Math]::Max(0, 1 - $b.age / $b.life))
|
|
497
|
+
if ($al -gt 0) {
|
|
498
|
+
$g.FillEllipse((New-Solid '#CFEFFF' ([Math]::Min(255, $al))), ($bx - $r), ($by - $r), (2 * $r), (2 * $r))
|
|
499
|
+
$g.DrawEllipse((New-Pen '#9FD8F5' 1.4 $al), ($bx - $r), ($by - $r), (2 * $r), (2 * $r))
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# ---- 爱心 ----
|
|
504
|
+
foreach ($h in @($script:hearts)) {
|
|
505
|
+
$al = [int](230 * [Math]::Max(0, 1 - $h.age / $h.life))
|
|
506
|
+
if ($al -le 6) { continue }
|
|
507
|
+
$hx = 160 + $h.dx
|
|
508
|
+
$hy = 70 - $h.age * 42 + [Math]::Sin($h.age * 5) * 2
|
|
509
|
+
$k = 1.0 + $h.age * 0.5
|
|
510
|
+
$heart = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
511
|
+
$heart.AddBezier($hx, $hy + 8 * $k, $hx - 14 * $k, $hy - 6 * $k, $hx - 6 * $k, $hy - 16 * $k, $hx, $hy - 7 * $k)
|
|
512
|
+
$heart.AddBezier($hx, $hy - 7 * $k, $hx + 6 * $k, $hy - 16 * $k, $hx + 14 * $k, $hy - 6 * $k, $hx, $hy + 8 * $k)
|
|
513
|
+
$heart.CloseFigure()
|
|
514
|
+
$g.FillPath((New-Solid '#FF5C8A' $al), $heart)
|
|
515
|
+
$heart.Dispose()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
$g.ResetTransform()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# ---------- 窗口 ----------
|
|
522
|
+
$form = New-Object FishPetWindow
|
|
523
|
+
if ([int]$script:x -ne $script:lastPx -or [int]$script:y -ne $script:lastPy) { $form.SetBounds([int]$script:x, [int]$script:y, $W, $H); $script:lastPx = [int]$script:x; $script:lastPy = [int]$script:y }
|
|
524
|
+
|
|
525
|
+
$form.Show()
|
|
526
|
+
$form.ShowNow()
|
|
527
|
+
$form.BringToFront()
|
|
528
|
+
$pidJson = [ordered]@{ pid = $PID; startedAt = (Get-Date).ToString('o'); script = $PSCommandPath } | ConvertTo-Json -Compress
|
|
529
|
+
[IO.File]::WriteAllText($PidPath, $pidJson, (New-Object Text.UTF8Encoding($false)))
|
|
530
|
+
|
|
531
|
+
# 自检用:启动 1.5 秒后自动说一句(-Say "..."),方便无人工验证聊天链路
|
|
532
|
+
if ($Say) {
|
|
533
|
+
$script:sayText = $Say
|
|
534
|
+
$script:sayTimer = New-Object System.Windows.Forms.Timer
|
|
535
|
+
$script:sayTimer.Interval = 1500
|
|
536
|
+
$script:sayTimer.Add_Tick({ $script:sayTimer.Stop(); Start-Chat $script:sayText })
|
|
537
|
+
$script:sayTimer.Start()
|
|
538
|
+
}
|
|
539
|
+
# 耳朵在后台独立跑(asr-listener.ps1),鱼只读结果,不再自己占麦克风
|
|
540
|
+
$script:asrFile = Join-Path $DataDir 'asr.log'
|
|
541
|
+
$script:asrPos = 0
|
|
542
|
+
function Read-NewAsrLines {
|
|
543
|
+
$res = @()
|
|
544
|
+
try {
|
|
545
|
+
if (-not (Test-Path $script:asrFile)) { return @() }
|
|
546
|
+
$len = (Get-Item -LiteralPath $script:asrFile).Length
|
|
547
|
+
if ($len -lt [int64]$script:asrPos) { $script:asrPos = 0; $script:asrRest = '' }
|
|
548
|
+
if ($len -eq [int64]$script:asrPos) { return @() }
|
|
549
|
+
$fs = [IO.File]::Open($script:asrFile, 'Open', 'Read', 'ReadWrite')
|
|
550
|
+
[void]$fs.Seek([int64]$script:asrPos, 'Begin')
|
|
551
|
+
$sr = New-Object IO.StreamReader($fs, [Text.Encoding]::UTF8)
|
|
552
|
+
$chunk = $sr.ReadToEnd()
|
|
553
|
+
$script:asrPos = $fs.Position
|
|
554
|
+
$sr.Dispose(); $fs.Dispose()
|
|
555
|
+
$all = [string]$script:asrRest + $chunk
|
|
556
|
+
$arr = $all -split "`n"
|
|
557
|
+
$script:asrRest = [string]$arr[-1]
|
|
558
|
+
if ($arr.Count -gt 1) {
|
|
559
|
+
$res = @($arr[0..($arr.Count - 2)] | ForEach-Object { $_.TrimEnd("`r") } | Where-Object { $_ })
|
|
560
|
+
}
|
|
561
|
+
} catch { }
|
|
562
|
+
return $res
|
|
563
|
+
}
|
|
564
|
+
$script:asrRest = ''
|
|
565
|
+
$script:asrPoll = 0
|
|
566
|
+
$script:earCheck = 0
|
|
567
|
+
function Start-Ear {
|
|
568
|
+
try { Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File',(Join-Path (Split-Path -Parent $PSCommandPath) 'asr-listener.ps1')) -WindowStyle Hidden } catch { }
|
|
569
|
+
}
|
|
570
|
+
function Stop-Ear {
|
|
571
|
+
try { Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" | Where-Object { $_.CommandLine -match 'asr-listener' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } catch { }
|
|
572
|
+
}
|
|
573
|
+
$script:chatOn = $true
|
|
574
|
+
|
|
575
|
+
# ---------- 菜单 ----------
|
|
576
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
577
|
+
$miFeed = $menu.Items.Add('喂它一口')
|
|
578
|
+
$miType = $menu.Items.Add('打字说两句')
|
|
579
|
+
$miTalk = $menu.Items.Add('开始听我说(麦克风)')
|
|
473
580
|
$miVoice = $menu.Items.Add('语音朗读:开')
|
|
581
|
+
$miVoiceSet = New-Object System.Windows.Forms.ToolStripMenuItem('换个声音')
|
|
582
|
+
$menu.Items.Add($miVoiceSet) | Out-Null
|
|
583
|
+
$voiceChoices = @(
|
|
584
|
+
@('zh-CN-XiaoyiNeural', '活泼少女·晓伊'),
|
|
585
|
+
@('zh-CN-XiaoxiaoNeural', '温柔姐姐·晓晓'),
|
|
586
|
+
@('zh-CN-liaoning-XiaobeiNeural', '东北话·小北'),
|
|
587
|
+
@('zh-CN-shaanxi-XiaoniNeural', '陕西话·小妮'),
|
|
588
|
+
@('zh-CN-YunxiNeural', '阳光男声·云希'),
|
|
589
|
+
@('zh-CN-YunxiaNeural', '可爱男声·云夏')
|
|
590
|
+
)
|
|
591
|
+
foreach ($vc in $voiceChoices) {
|
|
592
|
+
$it = New-Object System.Windows.Forms.ToolStripMenuItem(($vc[1] + '(' + $vc[0] + ')'))
|
|
593
|
+
$it.Tag = $vc[0]
|
|
594
|
+
$it.Add_Click({
|
|
595
|
+
param($s, $e)
|
|
596
|
+
$script:voicePickTag = [string]$s.Tag
|
|
597
|
+
$script:voicePickLabel = [string]$s.Text
|
|
598
|
+
Set-TtsVoice $script:voicePickTag $script:voicePickLabel
|
|
599
|
+
})
|
|
600
|
+
$miVoiceSet.DropDownItems.Add($it) | Out-Null
|
|
601
|
+
}
|
|
474
602
|
$miWake = $menu.Items.Add('语音待命:开')
|
|
475
|
-
$miChat = $menu.Items.Add('语音聊天:开')
|
|
476
|
-
$
|
|
477
|
-
$
|
|
478
|
-
$
|
|
479
|
-
$
|
|
480
|
-
$menu.Items.Add(
|
|
481
|
-
$
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
$
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
$
|
|
511
|
-
$
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
$script:
|
|
517
|
-
$script:
|
|
518
|
-
$script:
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if ($
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
$
|
|
553
|
-
$
|
|
554
|
-
$
|
|
555
|
-
$
|
|
556
|
-
$
|
|
557
|
-
$
|
|
558
|
-
$
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
$
|
|
568
|
-
$
|
|
569
|
-
$
|
|
570
|
-
$
|
|
571
|
-
$
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
try {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
$
|
|
586
|
-
|
|
587
|
-
if (-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
$
|
|
595
|
-
$
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
$script:
|
|
602
|
-
$script:
|
|
603
|
-
$script:
|
|
604
|
-
$script:
|
|
605
|
-
$script:
|
|
606
|
-
$script:
|
|
607
|
-
$script:
|
|
608
|
-
$script:
|
|
609
|
-
$script:
|
|
610
|
-
$script:
|
|
611
|
-
$script:
|
|
612
|
-
$script:
|
|
613
|
-
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
$
|
|
624
|
-
$
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
603
|
+
$miChat = $menu.Items.Add('语音聊天:开')
|
|
604
|
+
$miBalance = $menu.Items.Add('余额:--')
|
|
605
|
+
$miPause = $menu.Items.Add('停下歇会儿')
|
|
606
|
+
$miHome = $menu.Items.Add('回到右下角')
|
|
607
|
+
$miBigger = $menu.Items.Add('长大一点')
|
|
608
|
+
$miSmall = $menu.Items.Add('变小一点')
|
|
609
|
+
$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
|
|
610
|
+
$miRemind = $menu.Items.Add('加个提醒…')
|
|
611
|
+
$miRemindList = $menu.Items.Add('提醒清单…')
|
|
612
|
+
$miClear = $menu.Items.Add('清除记忆…')
|
|
613
|
+
$miQuit = $menu.Items.Add('让它游走(退出)')
|
|
614
|
+
|
|
615
|
+
function Save-State {
|
|
616
|
+
$obj = [ordered]@{
|
|
617
|
+
x = [int]$script:x; y = [int]$script:y; dir = [int]$script:dir
|
|
618
|
+
scale = [Math]::Round($script:scale, 3); feeds = [int]$script:feeds
|
|
619
|
+
hunger = [Math]::Round($script:hunger, 1); intimacy = [Math]::Round($script:intimacy, 1)
|
|
620
|
+
mood = [string]$script:mood; seen = (Get-Date).ToString('o')
|
|
621
|
+
}
|
|
622
|
+
try { ($obj | ConvertTo-Json -Compress) | Set-Content -Path $StatePath -Encoding UTF8 } catch { }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function Invoke-Feed {
|
|
626
|
+
try {
|
|
627
|
+
$script:feeds += 1
|
|
628
|
+
$script:satiety += 1
|
|
629
|
+
$script:hunger = [Math]::Max(0, $script:hunger - 30)
|
|
630
|
+
$script:intimacy = [Math]::Min(100, $script:intimacy + 2)
|
|
631
|
+
$script:mood = 'happy'
|
|
632
|
+
$script:pulse = 1.16
|
|
633
|
+
foreach ($dx in @(-26, 4, 30)) {
|
|
634
|
+
$script:hearts += @{ dx = $dx; age = 0.0; life = 1.4 }
|
|
635
|
+
}
|
|
636
|
+
Save-State
|
|
637
|
+
} catch {
|
|
638
|
+
$line = (Get-Date -Format 'HH:mm:ss') + ' FEED-ERR ' + $_.Exception.Message
|
|
639
|
+
try { Add-Content -LiteralPath (Join-Path $env:USERPROFILE '.dsh\fish-pet\error.log') -Value $line -Encoding UTF8 } catch { }
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# ---------- 聊天:输入框 / 气泡 / 语音 ----------
|
|
644
|
+
$script:chatBusy = $false
|
|
645
|
+
$script:chatPending = $null
|
|
646
|
+
$script:memPath = Join-Path $DataDir 'memory.json'
|
|
647
|
+
$script:memorySummary = ''
|
|
648
|
+
$script:summaryCount = 0
|
|
649
|
+
function Load-Memory {
|
|
650
|
+
try {
|
|
651
|
+
if (Test-Path $script:memPath) {
|
|
652
|
+
$j = [IO.File]::ReadAllText($script:memPath, [Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
653
|
+
if ($j.summary) { $script:memorySummary = [string]$j.summary }
|
|
654
|
+
if ($j.history) {
|
|
655
|
+
$script:chatHistory = @($j.history | ForEach-Object { @{ role = [string]$_.role; content = [string]$_.content } })
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} catch { }
|
|
659
|
+
}
|
|
660
|
+
# ---- 主人交代过的事(便签)----
|
|
661
|
+
$script:notesPath = Join-Path $DataDir 'notes.json'
|
|
662
|
+
function Load-Notes {
|
|
663
|
+
try {
|
|
664
|
+
if (Test-Path $script:notesPath) {
|
|
665
|
+
$arr = [IO.File]::ReadAllText($script:notesPath, [Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
666
|
+
return @($arr)
|
|
667
|
+
}
|
|
668
|
+
} catch { }
|
|
669
|
+
return @()
|
|
670
|
+
}
|
|
671
|
+
# 从一句话里解析提醒时间,解析不出就返回 $null
|
|
672
|
+
function Get-DueFromText([string]$s) {
|
|
673
|
+
try {
|
|
674
|
+
$now = Get-Date
|
|
675
|
+
if ($s -match '(\d+)\s*分钟(后|以后|之后)') { return $now.AddMinutes([int]$Matches[1]) }
|
|
676
|
+
if ($s -match '(\d+)\s*小时(后|以后|之后)') { return $now.AddHours([int]$Matches[1]) }
|
|
677
|
+
$dayShift = 0
|
|
678
|
+
if ($s -match '后天') { $dayShift = 2 } elseif ($s -match '明天|明早') { $dayShift = 1 }
|
|
679
|
+
if ($s -match '(早上|上午|中午|下午|晚上)?\s*(\d{1,2})\s*[点::]\s*(半|\d{1,2})?') {
|
|
680
|
+
$perm = [string]$Matches[1]
|
|
681
|
+
$h = [int]$Matches[2]
|
|
682
|
+
$mm = 0
|
|
683
|
+
if ($Matches[3] -eq '半') { $mm = 30 } elseif ($Matches[3]) { $mm = [int]$Matches[3] }
|
|
684
|
+
if (($perm -eq '下午' -or $perm -eq '晚上') -and $h -lt 12) { $h += 12 }
|
|
685
|
+
$due = (Get-Date).Date.AddDays($dayShift).AddHours($h).AddMinutes($mm)
|
|
686
|
+
if ($dayShift -eq 0 -and $due -lt $now) { $due = $due.AddDays(1) }
|
|
687
|
+
return $due
|
|
688
|
+
}
|
|
689
|
+
} catch { }
|
|
690
|
+
return $null
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function Save-Note([string]$text, $due = $null) {
|
|
694
|
+
try {
|
|
695
|
+
$arr = @(Load-Notes)
|
|
696
|
+
$entry = @{ t = (Get-Date).ToString('yyyy-MM-dd HH:mm'); text = [string]$text }
|
|
697
|
+
if ($due) { $entry.due = $due.ToString('o'); $entry.fired = $false }
|
|
698
|
+
$arr += $entry
|
|
699
|
+
if ($arr.Count -gt 20) { $arr = @($arr[($arr.Count - 20)..($arr.Count - 1)]) }
|
|
700
|
+
[IO.File]::WriteAllText($script:notesPath, ($arr | ConvertTo-Json -Depth 4), (New-Object Text.UTF8Encoding($false)))
|
|
701
|
+
} catch { }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function Save-Memory {
|
|
705
|
+
try {
|
|
706
|
+
$j = @{ history = $script:chatHistory; summary = $script:memorySummary; savedAt = (Get-Date).ToString('o') } | ConvertTo-Json -Depth 5 -Compress
|
|
707
|
+
[IO.File]::WriteAllText($script:memPath, $j, (New-Object Text.UTF8Encoding($false)))
|
|
708
|
+
} catch { }
|
|
709
|
+
}
|
|
710
|
+
# 把最近对话压缩成长期记忆摘要
|
|
711
|
+
function Update-Summary {
|
|
712
|
+
try {
|
|
713
|
+
$k = Get-ApiKey
|
|
714
|
+
if (-not $k) { return }
|
|
715
|
+
$convo = (@($script:chatHistory | Select-Object -Last 16 | ForEach-Object { $(if ($_.role -eq 'user') { '主人' } else { '大肥鱼' }) + ':' + $_.content }) -join "`n")
|
|
716
|
+
if (-not $convo) { return }
|
|
717
|
+
$sys2 = '你在帮一条桌面宠物鱼整理长期记忆。把对话压缩成不超过80字的中文要点,只保留关于主人的事实(称呼、喜好、约定)和主人交代要记住的事。不要评论,不要加引号,只输出要点本身。'
|
|
718
|
+
$old = $(if ($script:memorySummary) { $script:memorySummary } else { '(无)' })
|
|
719
|
+
$u = '旧记忆:' + $old + "`n最近对话:`n" + $convo
|
|
720
|
+
$body = @{ model = 'deepseek-flash'; messages = @(@{ role = 'system'; content = $sys2 }, @{ role = 'user'; content = $u }); max_tokens = 220; temperature = 0.3; thinking = @{ type = 'disabled' } } | ConvertTo-Json -Depth 6
|
|
721
|
+
$resp = Invoke-WebRequest -Uri 'https://api.deepseek.com/chat/completions' -Method Post -Headers @{ Authorization = "Bearer $k" } -ContentType 'application/json' -Body ([Text.Encoding]::UTF8.GetBytes($body)) -TimeoutSec 60 -UseBasicParsing
|
|
722
|
+
$obj = [Text.Encoding]::UTF8.GetString($resp.RawContentStream.ToArray()) | ConvertFrom-Json
|
|
723
|
+
$s = [string]$obj.choices[0].message.content
|
|
724
|
+
if ($s) { $script:memorySummary = $s.Trim(); Save-Memory }
|
|
725
|
+
} catch { }
|
|
726
|
+
}
|
|
727
|
+
$script:chatHistory = @()
|
|
728
|
+
Load-Memory
|
|
729
|
+
$script:speakOn = $true
|
|
730
|
+
$script:synth = $null
|
|
731
|
+
$script:chatForm = $null
|
|
732
|
+
$script:chatBox = $null
|
|
733
|
+
$script:bubble = $null
|
|
734
|
+
$script:bubbleLabel = $null
|
|
735
|
+
$script:bubbleUntil = [DateTime]::MinValue
|
|
736
|
+
$script:chatPs = $null
|
|
737
|
+
$script:chatAsync = $null
|
|
738
|
+
$script:chatTask = $null
|
|
739
|
+
$script:http = $null
|
|
740
|
+
$script:lastUser = ''
|
|
741
|
+
|
|
742
|
+
function Write-Err([string]$tag) {
|
|
743
|
+
try {
|
|
744
|
+
$ep = Join-Path (Join-Path $env:USERPROFILE '.dsh\fish-pet') 'error.log'
|
|
745
|
+
if ((Test-Path $ep) -and (Get-Item $ep).Length -gt 1MB) {
|
|
746
|
+
Remove-Item ($ep + '.1') -Force -ErrorAction SilentlyContinue
|
|
747
|
+
Move-Item $ep ($ep + '.1') -Force
|
|
748
|
+
}
|
|
749
|
+
} catch { }
|
|
750
|
+
try {
|
|
751
|
+
$line = (Get-Date -Format 'HH:mm:ss') + ' ' + $tag
|
|
752
|
+
Add-Content -LiteralPath $ErrorPath -Value $line -Encoding UTF8
|
|
753
|
+
} catch { }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
# 读取密钥:环境变量 → DPAPI 加密文件 → 明文文件
|
|
757
|
+
function Read-Key([string]$name) {
|
|
758
|
+
$dir = Join-Path $env:USERPROFILE '.dsh\fish-pet'
|
|
759
|
+
$enc = Join-Path $dir ($name + '.key.enc')
|
|
760
|
+
if (Test-Path $enc) {
|
|
761
|
+
try {
|
|
762
|
+
$sec = ConvertTo-SecureString (Get-Content -LiteralPath $enc -Raw).Trim()
|
|
763
|
+
$b = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)
|
|
764
|
+
try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($b) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b) }
|
|
765
|
+
} catch { }
|
|
766
|
+
}
|
|
767
|
+
$plain = Join-Path $dir ($name + '.key')
|
|
768
|
+
if (Test-Path $plain) { return (Get-Content -LiteralPath $plain -Raw -ErrorAction SilentlyContinue).Trim() }
|
|
769
|
+
return $null
|
|
770
|
+
}
|
|
771
|
+
function Get-ApiKey {
|
|
772
|
+
if ($env:DEEPSEEK_API_KEY) { return $env:DEEPSEEK_API_KEY }
|
|
773
|
+
$kk = Read-Key 'deepseek'
|
|
774
|
+
if ($kk) { return $kk }
|
|
775
|
+
try {
|
|
776
|
+
$kf = Join-Path $DataDir 'deepseek.key'
|
|
777
|
+
if (Test-Path $kf) { $v = (Get-Content -LiteralPath $kf -Raw).Trim(); if ($v) { return $v } }
|
|
778
|
+
} catch { }
|
|
779
|
+
try {
|
|
780
|
+
$raw = Get-Content (Join-Path $env:USERPROFILE '.dsh\.credentials.yaml') -Raw -ErrorAction Stop
|
|
781
|
+
$m = [regex]::Match($raw, 'DEEPSEEK_API_KEY:\s*(\S+)')
|
|
782
|
+
if ($m.Success) { return $m.Groups[1].Value }
|
|
783
|
+
} catch { }
|
|
784
|
+
return $null
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
# 打字聊天窗口
|
|
788
|
+
# 手动添加提醒
|
|
789
|
+
# 提醒清单:查看 / 删除
|
|
790
|
+
function Show-ReminderList {
|
|
791
|
+
try {
|
|
792
|
+
$dlg = New-Object System.Windows.Forms.Form
|
|
793
|
+
$dlg.Text = '提醒清单'
|
|
794
|
+
$dlg.Size = New-Object System.Drawing.Size(560, 380)
|
|
795
|
+
$dlg.StartPosition = 'CenterScreen'
|
|
796
|
+
$dlg.TopMost = $true
|
|
797
|
+
$dlg.FormBorderStyle = 'FixedDialog'
|
|
798
|
+
$dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false
|
|
799
|
+
$dlg.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
800
|
+
$lb = New-Object System.Windows.Forms.ListBox
|
|
801
|
+
$lb.Location = New-Object System.Drawing.Point(16, 14)
|
|
802
|
+
$lb.Size = New-Object System.Drawing.Size(512, 240)
|
|
803
|
+
$lb.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
804
|
+
$dlg.Controls.Add($lb)
|
|
805
|
+
$btnDel = New-Object System.Windows.Forms.Button
|
|
806
|
+
$btnDel.Text = '删除选中'
|
|
807
|
+
$btnDel.Location = New-Object System.Drawing.Point(16, 268)
|
|
808
|
+
$btnDel.Width = 120
|
|
809
|
+
$dlg.Controls.Add($btnDel)
|
|
810
|
+
$btnClear = New-Object System.Windows.Forms.Button
|
|
811
|
+
$btnClear.Text = '清空全部'
|
|
812
|
+
$btnClear.Location = New-Object System.Drawing.Point(150, 268)
|
|
813
|
+
$btnClear.Width = 120
|
|
814
|
+
$dlg.Controls.Add($btnClear)
|
|
815
|
+
$btnClose = New-Object System.Windows.Forms.Button
|
|
816
|
+
$btnClose.Text = '关闭'
|
|
817
|
+
$btnClose.Location = New-Object System.Drawing.Point(408, 268)
|
|
818
|
+
$btnClose.Width = 120
|
|
819
|
+
$dlg.Controls.Add($btnClose)
|
|
820
|
+
$script:remList = $lb
|
|
821
|
+
$script:remDlg = $dlg
|
|
822
|
+
$script:remBtnDel = $btnDel
|
|
823
|
+
$script:remBtnClear = $btnClear
|
|
824
|
+
$script:remBtnClose = $btnClose
|
|
825
|
+
$script:remItems = @()
|
|
826
|
+
$script:remRefresh = {
|
|
827
|
+
$script:remList.Items.Clear()
|
|
828
|
+
$script:remItems = @(Load-Notes)
|
|
829
|
+
$i = 0
|
|
830
|
+
foreach ($n in $script:remItems) {
|
|
831
|
+
$i++
|
|
832
|
+
$dueText = '不限时'
|
|
833
|
+
if ($n.due) {
|
|
834
|
+
try {
|
|
835
|
+
$d = [datetime]::Parse([string]$n.due)
|
|
836
|
+
$firedMark = ''
|
|
837
|
+
try { if ($n.fired) { $firedMark = '(已提醒)' } } catch { }
|
|
838
|
+
$dueText = $d.ToString('MM-dd HH:mm') + $firedMark
|
|
839
|
+
} catch { }
|
|
840
|
+
}
|
|
841
|
+
[void]$script:remList.Items.Add(('{0}. {1} 〔{2}〕' -f $i, [string]$n.text, $dueText))
|
|
842
|
+
}
|
|
843
|
+
if ($script:remList.Items.Count -eq 0) { [void]$script:remList.Items.Add('(还没有提醒事项)') }
|
|
844
|
+
}
|
|
845
|
+
$btnClose.Add_Click({ $script:remDlg.Close() })
|
|
846
|
+
$dlg.Add_Shown({
|
|
847
|
+
& $script:remRefresh
|
|
848
|
+
})
|
|
849
|
+
$btnDel.Add_Click({
|
|
850
|
+
$sel = $script:remList.SelectedIndex
|
|
851
|
+
$arr = @(Load-Notes)
|
|
852
|
+
if ($sel -ge 0 -and $arr.Count -gt $sel) {
|
|
853
|
+
$kept = @()
|
|
854
|
+
for ($i = 0; $i -lt $arr.Count; $i++) { if ($i -ne $sel) { $kept += $arr[$i] } }
|
|
855
|
+
[IO.File]::WriteAllText($script:notesPath, (ConvertTo-Json -InputObject @($kept) -Depth 4), (New-Object Text.UTF8Encoding($false)))
|
|
856
|
+
& $script:remRefresh
|
|
857
|
+
}
|
|
858
|
+
})
|
|
859
|
+
$btnClear.Add_Click({
|
|
860
|
+
$r = [System.Windows.Forms.MessageBox]::Show('清空全部提醒事项?', '提醒清单', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question)
|
|
861
|
+
if ($r -eq [System.Windows.Forms.DialogResult]::Yes) {
|
|
862
|
+
[IO.File]::WriteAllText($script:notesPath, '[]', (New-Object Text.UTF8Encoding($false)))
|
|
863
|
+
& $script:remRefresh
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
[void]$dlg.ShowDialog()
|
|
867
|
+
} catch { Write-Err 'REMLIST-ERR' }
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
# 切换朗读音色(立即生效并写入 config.json)
|
|
871
|
+
function Set-TtsVoice([string]$voice, [string]$label) {
|
|
872
|
+
try {
|
|
873
|
+
$script:ttsVoice = $voice
|
|
874
|
+
try {
|
|
875
|
+
$cfg = Join-Path $DataDir 'config.json'
|
|
876
|
+
if (Test-Path $cfg) {
|
|
877
|
+
$j = [IO.File]::ReadAllText($cfg, [Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
878
|
+
if (-not $j.tts) { $j | Add-Member -NotePropertyName tts -NotePropertyValue ([ordered]@{ engine = 'edge'; voice = $voice; rate = '+12%'; pitch = '+10Hz' }) }
|
|
879
|
+
$j.tts.voice = $voice
|
|
880
|
+
[IO.File]::WriteAllText($cfg, ($j | ConvertTo-Json -Depth 6), (New-Object Text.UTF8Encoding($false)))
|
|
881
|
+
}
|
|
882
|
+
} catch { }
|
|
883
|
+
Show-Bubble ('声音换成:' + $label)
|
|
884
|
+
Speak-Text ('主人你好呀,我是大肥鱼,现在用的是' + $label + '的声音')
|
|
885
|
+
} catch { Write-Err 'VOICE-SET-ERR' }
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function Show-Reminder {
|
|
889
|
+
try {
|
|
890
|
+
$dlg = New-Object System.Windows.Forms.Form
|
|
891
|
+
$dlg.Text = '加个提醒'
|
|
892
|
+
$dlg.Size = New-Object System.Drawing.Size(460, 190)
|
|
893
|
+
$dlg.StartPosition = 'CenterScreen'
|
|
894
|
+
$dlg.TopMost = $true
|
|
895
|
+
$dlg.FormBorderStyle = 'FixedDialog'
|
|
896
|
+
$dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false
|
|
897
|
+
$dlg.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
898
|
+
$lbl = New-Object System.Windows.Forms.Label
|
|
899
|
+
$lbl.Text = '写一句话,能带时间就定时提醒:' + [Environment]::NewLine + '例:15:30 开会 / 明天7点 起床 / 10分钟后 喝水'
|
|
900
|
+
$lbl.Location = New-Object System.Drawing.Point(16, 12)
|
|
901
|
+
$lbl.Width = 420; $lbl.Height = 44
|
|
902
|
+
$dlg.Controls.Add($lbl)
|
|
903
|
+
$tb = New-Object System.Windows.Forms.TextBox
|
|
904
|
+
$tb.Location = New-Object System.Drawing.Point(16, 62)
|
|
905
|
+
$tb.Width = 412
|
|
906
|
+
$tb.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 11)
|
|
907
|
+
$dlg.Controls.Add($tb)
|
|
908
|
+
$btn = New-Object System.Windows.Forms.Button
|
|
909
|
+
$btn.Text = '记下'
|
|
910
|
+
$btn.Location = New-Object System.Drawing.Point(328, 104)
|
|
911
|
+
$btn.Width = 100
|
|
912
|
+
$dlg.Controls.Add($btn)
|
|
913
|
+
$script:inputDlg = $dlg
|
|
914
|
+
$script:inputBox = $tb
|
|
915
|
+
$script:inputText = $null
|
|
916
|
+
$btn.Add_Click({ $script:inputText = $script:inputBox.Text; $script:inputDlg.Close() })
|
|
917
|
+
$tb.Add_KeyDown({ param($s, $e) if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) { $script:inputText = $script:inputBox.Text; $script:inputDlg.Close() } })
|
|
918
|
+
$dlg.Add_Shown({ $script:inputBox.Focus() })
|
|
919
|
+
[void]$dlg.ShowDialog()
|
|
920
|
+
$txt = [string]$script:inputText
|
|
921
|
+
$script:inputText = $null
|
|
922
|
+
if ($txt -and $txt.Trim().Length -gt 0) {
|
|
923
|
+
$txt = $txt.Trim()
|
|
924
|
+
$due = Get-DueFromText $txt
|
|
925
|
+
Save-Note $txt $due
|
|
926
|
+
if ($due) {
|
|
927
|
+
Show-Bubble ('记下了:' + $txt + '(' + $due.ToString('MM-dd HH:mm') + ' 提醒你)')
|
|
928
|
+
} else {
|
|
929
|
+
Show-Bubble ('记下了:' + $txt + '(没说时间,我就一直记着)')
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
} catch { Write-Err 'REMIND-ERR' }
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function Show-Input {
|
|
936
|
+
Write-Err 'INPUT-OPEN'
|
|
937
|
+
try {
|
|
938
|
+
$dlg = New-Object System.Windows.Forms.Form
|
|
939
|
+
$dlg.Text = '跟大肥鱼说两句'
|
|
940
|
+
$dlg.Size = New-Object System.Drawing.Size(410, 140)
|
|
941
|
+
$dlg.StartPosition = 'CenterScreen'
|
|
942
|
+
$dlg.TopMost = $true
|
|
943
|
+
$dlg.FormBorderStyle = 'FixedDialog'
|
|
944
|
+
$dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false
|
|
945
|
+
$dlg.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
946
|
+
$tb = New-Object System.Windows.Forms.TextBox
|
|
947
|
+
$tb.Location = New-Object System.Drawing.Point(16, 16)
|
|
948
|
+
$tb.Width = 362
|
|
949
|
+
$tb.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 11)
|
|
950
|
+
$dlg.Controls.Add($tb)
|
|
951
|
+
$btn = New-Object System.Windows.Forms.Button
|
|
952
|
+
$btn.Text = '发过去'
|
|
953
|
+
$btn.Location = New-Object System.Drawing.Point(278, 56)
|
|
954
|
+
$btn.Width = 100
|
|
955
|
+
$dlg.Controls.Add($btn)
|
|
956
|
+
$script:inputDlg = $dlg
|
|
957
|
+
$script:inputBox = $tb
|
|
958
|
+
$btn.Add_Click({ $script:inputText = $script:inputBox.Text; $script:inputDlg.Close() })
|
|
959
|
+
$tb.Add_KeyDown({ param($s, $e) if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) { $script:inputText = $script:inputBox.Text; $script:inputDlg.Close() } })
|
|
960
|
+
$dlg.Add_Shown({ $script:inputBox.Focus() })
|
|
961
|
+
[void]$dlg.ShowDialog()
|
|
962
|
+
$txt = [string]$script:inputText
|
|
963
|
+
$script:inputText = $null
|
|
964
|
+
Write-Err ('INPUT-SEND [' + $txt + ']')
|
|
965
|
+
if ($txt -and $txt.Trim().Length -gt 0) {
|
|
966
|
+
$script:lastReplyAt = Get-Date
|
|
967
|
+
Start-Chat $txt.Trim()
|
|
968
|
+
}
|
|
969
|
+
} catch { Write-Err 'INPUT-ERR' }
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
# 取 DeepSeek 余额文本(缓存 60 秒,避免每次开菜单都卡一下)
|
|
973
|
+
function Get-BalanceText {
|
|
974
|
+
if ($script:balCache -and ((Get-Date) - $script:balAt).TotalSeconds -lt 60) { return $script:balCache }
|
|
975
|
+
$txt = '余额:查询失败'
|
|
976
|
+
try {
|
|
977
|
+
$k = Get-ApiKey
|
|
978
|
+
if (-not $k) {
|
|
979
|
+
$txt = '余额:未配置 key'
|
|
980
|
+
} else {
|
|
981
|
+
$r = Invoke-RestMethod -Uri 'https://api.deepseek.com/user/balance' -Headers @{ Authorization = "Bearer $k" } -TimeoutSec 8
|
|
982
|
+
$b = @($r.balance_infos) | Select-Object -First 1
|
|
983
|
+
if ($b) {
|
|
984
|
+
$sym = switch ([string]$b.currency) { 'CNY' { '¥' } 'USD' { '$' } default { [string]$b.currency + ' ' } }
|
|
985
|
+
$txt = '余额:' + $sym + $b.total_balance
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} catch { }
|
|
989
|
+
$script:balCache = $txt
|
|
990
|
+
$script:balAt = Get-Date
|
|
991
|
+
return $txt
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function Show-Bubble([string]$text) {
|
|
995
|
+
try {
|
|
996
|
+
if ($null -eq $script:bubble) {
|
|
997
|
+
$b = New-Object System.Windows.Forms.Form
|
|
998
|
+
$b.FormBorderStyle = 'None'
|
|
999
|
+
$b.StartPosition = 'Manual'
|
|
1000
|
+
$b.TopMost = $true
|
|
1001
|
+
$b.ShowInTaskbar = $false
|
|
1002
|
+
$b.BackColor = [System.Drawing.Color]::FromArgb(255, 20, 40, 70)
|
|
1003
|
+
$b.TransparencyKey = [System.Drawing.Color]::FromArgb(255, 20, 40, 70)
|
|
1004
|
+
$b.Opacity = [double]$script:cfg.bubble.opacity
|
|
1005
|
+
$lbl = New-Object System.Windows.Forms.Label
|
|
1006
|
+
$lbl.AutoSize = $true
|
|
1007
|
+
$lbl.MaximumSize = New-Object System.Drawing.Size(340, 0)
|
|
1008
|
+
$lbl.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
1009
|
+
$lbl.ForeColor = [System.Drawing.Color]::FromArgb(255, 8, 12, 18)
|
|
1010
|
+
$lbl.BackColor = [System.Drawing.Color]::FromArgb(255, 20, 40, 70)
|
|
1011
|
+
$lbl.Padding = New-Object System.Windows.Forms.Padding(12, 9, 12, 9)
|
|
1012
|
+
$lbl.Text = ''
|
|
1013
|
+
$lbl.Add_Click({ $script:bubble.Hide() })
|
|
1014
|
+
$b.Controls.Add($lbl)
|
|
1015
|
+
$script:bubble = $b
|
|
1016
|
+
$script:bubbleLabel = $lbl
|
|
1017
|
+
}
|
|
1018
|
+
$script:bubbleLabel.Text = $text
|
|
1019
|
+
$sz = $script:bubbleLabel.PreferredSize
|
|
1020
|
+
$w = [Math]::Min(364, [Math]::Max(120, $sz.Width))
|
|
1021
|
+
$h = $sz.Height
|
|
1022
|
+
$bx = [Math]::Min([Math]::Max(0, [int]$script:x + [int](150 - ($w / 2))), $wa.Right - $w - 8)
|
|
1023
|
+
$by = [int]$script:y + 44 - $h
|
|
1024
|
+
if ($by -lt 0) { $by = [Math]::Min($wa.Bottom - $h - 8, [int]$script:y + $H - 10) }
|
|
1025
|
+
$script:bubble.SetBounds($bx, $by, $w, $h)
|
|
1026
|
+
$script:bubble.Show()
|
|
1027
|
+
$script:bubble.BringToFront()
|
|
1028
|
+
$script:bubbleUntil = (Get-Date).AddSeconds([Math]::Max(6, [Math]::Min(18, $text.Length / 7 + 5)))
|
|
1029
|
+
} catch { Write-Err 'BUBBLE-ERR' }
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function Speak-Text([string]$text) {
|
|
1033
|
+
if (-not $script:speakOn) { return }
|
|
1034
|
+
$talkFile = Join-Path $DataDir 'asr.talking'
|
|
1035
|
+
try { [IO.File]::WriteAllText($talkFile, '1') } catch { }
|
|
1036
|
+
# 云语音(Edge 免费音色)优先;失败退回本机 SAPI
|
|
1037
|
+
if ($script:ttsEngine -eq 'edge') {
|
|
1038
|
+
try {
|
|
1039
|
+
$mp3 = Join-Path $env:TEMP ('fish-tts-' + [guid]::NewGuid().ToString('N') + '.mp3')
|
|
1040
|
+
$eargs = @('-m', 'edge_tts', '--voice', $script:ttsVoice, ('--rate=' + $script:ttsRate), ('--pitch=' + $script:ttsPitch), '--text', $text, '--write-media', $mp3)
|
|
1041
|
+
& python @eargs 2>$null | Out-Null
|
|
1042
|
+
if ((Test-Path $mp3) -and (Get-Item $mp3).Length -gt 2000) {
|
|
1043
|
+
if ($null -eq $script:mp) {
|
|
1044
|
+
Add-Type -AssemblyName PresentationCore
|
|
1045
|
+
$script:mp = New-Object System.Windows.Media.MediaPlayer
|
|
1046
|
+
$script:mp.Add_MediaEnded({
|
|
1047
|
+
try { Remove-Item -LiteralPath (Join-Path $DataDir 'asr.talking') -Force -ErrorAction SilentlyContinue } catch { }
|
|
1048
|
+
try { [IO.File]::WriteAllText((Join-Path $DataDir 'asr.pause'), (Get-Date).ToString('o')) } catch { }
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
$script:mp.Open([uri]$mp3)
|
|
1052
|
+
$script:mp.Play()
|
|
1053
|
+
Write-Err ('TTS-EDGE len=' + $text.Length + ' file=' + (Get-Item $mp3).Length)
|
|
1054
|
+
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 { }
|
|
1055
|
+
return
|
|
1056
|
+
}
|
|
1057
|
+
} catch { Write-Err ('TTS-EDGE-ERR ' + $_.Exception.Message) }
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
if ($null -eq $script:synth) {
|
|
1061
|
+
Add-Type -AssemblyName System.Speech
|
|
1062
|
+
$script:synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
|
1063
|
+
$zh = $script:synth.GetInstalledVoices() | Where-Object { $_.VoiceInfo.Culture.Name -like 'zh*' } | Select-Object -First 1
|
|
1064
|
+
if ($zh) { $script:synth.SelectVoice($zh.VoiceInfo.Name) }
|
|
1065
|
+
$script:synth.Rate = 1
|
|
1066
|
+
}
|
|
1067
|
+
if (-not $script:pauseHook) {
|
|
1068
|
+
$script:pauseHook = $true
|
|
1069
|
+
$script:synth.add_SpeakCompleted({
|
|
1070
|
+
try { Remove-Item -LiteralPath (Join-Path $DataDir 'asr.talking') -Force -ErrorAction SilentlyContinue } catch { }
|
|
1071
|
+
try { [IO.File]::WriteAllText((Join-Path $DataDir 'asr.pause'), (Get-Date).ToString('o')) } catch { }
|
|
1072
|
+
})
|
|
1073
|
+
}
|
|
1074
|
+
switch ([string]$script:mood) {
|
|
1075
|
+
'happy' { $script:synth.Rate = 2; $script:synth.Volume = 100 }
|
|
1076
|
+
'sad' { $script:synth.Rate = -2; $script:synth.Volume = 82 }
|
|
1077
|
+
'hungry' { $script:synth.Rate = -1; $script:synth.Volume = 92 }
|
|
1078
|
+
'lonely' { $script:synth.Rate = -1; $script:synth.Volume = 88 }
|
|
1079
|
+
default { $script:synth.Rate = 1; $script:synth.Volume = 100 }
|
|
1080
|
+
}
|
|
1081
|
+
Write-Err ('TTS-SPEAK len=' + $text.Length + ' mood=' + $script:mood + ' rate=' + $script:synth.Rate)
|
|
1082
|
+
$script:synth.SpeakAsyncCancelAll()
|
|
1083
|
+
$script:synth.SpeakAsync($text) | Out-Null
|
|
1084
|
+
} catch { Write-Err ('TTS-ERR ' + $_.Exception.Message) }
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
# ---------- 麦克风:离线中文识别 + 唤醒词 ----------
|
|
1088
|
+
$script:rec = $null
|
|
1089
|
+
$script:gWake = $null
|
|
1090
|
+
$script:gDict = $null
|
|
1091
|
+
$script:autoOn = $false
|
|
1092
|
+
$script:dictating = $false
|
|
1093
|
+
$script:woke = $false
|
|
1094
|
+
$script:heard = $null
|
|
1095
|
+
$script:heardConf = 0.0
|
|
1096
|
+
$script:dictDeadline = [DateTime]::MinValue
|
|
1097
|
+
$script:resumeAt = $null
|
|
1098
|
+
$script:listen = $false
|
|
1099
|
+
$script:listenDeadline = [DateTime]::MinValue
|
|
1100
|
+
$script:fixWords = @{ '大飞宇' = '大肥鱼'; '大飞鱼' = '大肥鱼'; '大肥于' = '大肥鱼'; '飞鱼' = '肥鱼' }
|
|
1101
|
+
|
|
1102
|
+
function Ensure-Recognizer {
|
|
1103
|
+
if ($null -ne $script:rec) { return $true }
|
|
1104
|
+
try {
|
|
1105
|
+
Add-Type -AssemblyName System.Speech
|
|
1106
|
+
$r = New-Object System.Speech.Recognition.SpeechRecognitionEngine([System.Globalization.CultureInfo]'zh-CN')
|
|
1107
|
+
$r.SetInputToDefaultAudioDevice()
|
|
1108
|
+
|
|
1109
|
+
$dict = New-Object System.Speech.Recognition.DictationGrammar
|
|
1110
|
+
$dict.Name = 'dictation'
|
|
1111
|
+
$dict.Enabled = $false
|
|
1112
|
+
$r.LoadGrammar($dict)
|
|
1113
|
+
|
|
1114
|
+
$wc = New-Object System.Speech.Recognition.Choices
|
|
1115
|
+
$wc.Add(@('大肥鱼', '大肥鱼你在吗', '喂大肥鱼', '小鱼', '蓝鱼', '小蓝鱼'))
|
|
628
1116
|
$wb = New-Object System.Speech.Recognition.GrammarBuilder
|
|
629
|
-
$wb.Culture = [System.Globalization.CultureInfo]'zh-CN'
|
|
630
|
-
$wb.Append($wc)
|
|
631
|
-
$wg = New-Object System.Speech.Recognition.Grammar($wb)
|
|
632
|
-
$wg.Name = 'wake'
|
|
633
|
-
$wg.Enabled = $true
|
|
634
|
-
$r.LoadGrammar($wg)
|
|
635
|
-
|
|
636
|
-
$r.Add_SpeechRecognized({
|
|
637
|
-
param($s, $e)
|
|
638
|
-
if (-not $e.Result) { return }
|
|
639
|
-
$name = ''
|
|
640
|
-
try { $name = $e.Result.Grammar.Name } catch { }
|
|
641
|
-
$txt = [string]$e.Result.Text
|
|
642
|
-
if ($txt) {
|
|
643
|
-
$script:heard = $txt
|
|
644
|
-
$script:heardConf = [double]$e.Result.Confidence
|
|
645
|
-
try { Add-Content -LiteralPath $ErrorPath -Value ((Get-Date -Format 'HH:mm:ss') + ' HEARD[' + [Math]::Round($e.Result.Confidence,2) + '] ' + $txt) -Encoding UTF8 } catch { }
|
|
646
|
-
}
|
|
647
|
-
})
|
|
648
|
-
$script:rec = $r
|
|
649
|
-
$script:gWake = $wg
|
|
650
|
-
$script:gDict = $dict
|
|
651
|
-
return $true
|
|
652
|
-
} catch {
|
|
653
|
-
Write-Err 'SR-INIT'
|
|
654
|
-
return $false
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function Start-Auto {
|
|
659
|
-
if (-not (Ensure-Recognizer)) { return }
|
|
660
|
-
$script:autoOn = $true
|
|
661
|
-
$script:dictating = $false
|
|
662
|
-
$script:woke = $false
|
|
663
|
-
try { $script:gDict.Enabled = $true; $script:gWake.Enabled = $false } catch { }
|
|
664
|
-
try { $script:rec.RecognizeAsync([System.Speech.Recognition.RecognizeMode]::Multiple); Add-Content -LiteralPath $ErrorPath -Value ((Get-Date -Format 'HH:mm:ss') + ' AUTO-ON') -Encoding UTF8 } catch { Write-Err 'SR-AUTO' }
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function Enter-Dictation {
|
|
668
|
-
if (-not (Ensure-Recognizer)) { Show-Bubble '耳朵打不开…检查一下麦克风?'; return }
|
|
669
|
-
$script:dictating = $true
|
|
670
|
-
$script:woke = $false
|
|
671
|
-
$script:heard = $null
|
|
672
|
-
$script:heardConf = 0.0
|
|
673
|
-
$script:dictDeadline = (Get-Date).AddSeconds(11)
|
|
674
|
-
try { $script:gWake.Enabled = $false; $script:gDict.Enabled = $true } catch { }
|
|
675
|
-
[System.Media.SystemSounds]::Asterisk.Play()
|
|
676
|
-
Show-Bubble '我在听…(说完停一下)'
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function BackToWake {
|
|
680
|
-
$script:dictating = $false
|
|
681
|
-
$script:heard = $null
|
|
682
|
-
$script:heardConf = 0.0
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function Start-Listen { Show-Bubble '语音在后台自动听,主人直接说话就行。' }
|
|
687
|
-
function Stop-Listen { BackToWake }
|
|
688
|
-
|
|
689
|
-
function Open-Chat {
|
|
690
|
-
try {
|
|
691
|
-
if ($null -eq $script:chatForm) {
|
|
692
|
-
$cf = New-Object System.Windows.Forms.Form
|
|
693
|
-
$cf.FormBorderStyle = 'FixedToolWindow'
|
|
694
|
-
$cf.StartPosition = 'Manual'
|
|
695
|
-
$cf.TopMost = $true
|
|
696
|
-
$cf.ShowInTaskbar = $false
|
|
697
|
-
$cf.Text = '和大肥鱼说话'
|
|
698
|
-
$cf.Width = 360
|
|
699
|
-
$cf.Height = 96
|
|
700
|
-
$cf.BackColor = [System.Drawing.Color]::FromArgb(255, 252, 244)
|
|
701
|
-
$tb = New-Object System.Windows.Forms.TextBox
|
|
702
|
-
$tb.SetBounds(12, 12, 324, 26)
|
|
703
|
-
$tb.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
704
|
-
$cf.Controls.Add($tb)
|
|
705
|
-
$cf.Add_Shown({ $script:chatBox.Focus() })
|
|
706
|
-
$tb.Add_KeyDown({
|
|
707
|
-
param($s, $e)
|
|
708
|
-
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
|
|
709
|
-
$e.SuppressKeyPress = $true
|
|
710
|
-
$txt = $script:chatBox.Text.Trim()
|
|
711
|
-
if ($txt) {
|
|
712
|
-
$script:chatBox.Text = ''
|
|
713
|
-
$script:chatForm.Hide()
|
|
714
|
-
Start-Chat $txt
|
|
715
|
-
}
|
|
716
|
-
} elseif ($e.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
|
|
717
|
-
$script:chatForm.Hide()
|
|
718
|
-
}
|
|
719
|
-
})
|
|
720
|
-
$script:chatForm = $cf
|
|
721
|
-
$script:chatBox = $tb
|
|
722
|
-
}
|
|
723
|
-
$script:chatForm.Left = [Math]::Min([Math]::Max(0, [int]$script:x + 20), $wa.Right - 370)
|
|
724
|
-
$script:chatForm.Top = [Math]::Max(0, [int]$script:y - 110)
|
|
725
|
-
$script:chatForm.Show()
|
|
726
|
-
$script:chatForm.BringToFront()
|
|
727
|
-
$script:chatBox.Focus()
|
|
728
|
-
} catch { Write-Err 'CHATUI-ERR' }
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (-not $
|
|
735
|
-
$
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
foreach ($
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
$
|
|
745
|
-
$
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
$
|
|
751
|
-
$
|
|
752
|
-
|
|
753
|
-
$script:
|
|
754
|
-
$script:
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
$
|
|
759
|
-
$
|
|
760
|
-
$
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
$
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if ($
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
$script:
|
|
780
|
-
$script:
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
$script:
|
|
792
|
-
$script:
|
|
793
|
-
$
|
|
794
|
-
$
|
|
795
|
-
$
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
$
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
$
|
|
802
|
-
$
|
|
803
|
-
|
|
804
|
-
$script:
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
$script:
|
|
808
|
-
$script:
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
$
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
$script:
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
$
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
$
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1117
|
+
$wb.Culture = [System.Globalization.CultureInfo]'zh-CN'
|
|
1118
|
+
$wb.Append($wc)
|
|
1119
|
+
$wg = New-Object System.Speech.Recognition.Grammar($wb)
|
|
1120
|
+
$wg.Name = 'wake'
|
|
1121
|
+
$wg.Enabled = $true
|
|
1122
|
+
$r.LoadGrammar($wg)
|
|
1123
|
+
|
|
1124
|
+
$r.Add_SpeechRecognized({
|
|
1125
|
+
param($s, $e)
|
|
1126
|
+
if (-not $e.Result) { return }
|
|
1127
|
+
$name = ''
|
|
1128
|
+
try { $name = $e.Result.Grammar.Name } catch { }
|
|
1129
|
+
$txt = [string]$e.Result.Text
|
|
1130
|
+
if ($txt) {
|
|
1131
|
+
$script:heard = $txt
|
|
1132
|
+
$script:heardConf = [double]$e.Result.Confidence
|
|
1133
|
+
try { Add-Content -LiteralPath $ErrorPath -Value ((Get-Date -Format 'HH:mm:ss') + ' HEARD[' + [Math]::Round($e.Result.Confidence,2) + '] ' + $txt) -Encoding UTF8 } catch { }
|
|
1134
|
+
}
|
|
1135
|
+
})
|
|
1136
|
+
$script:rec = $r
|
|
1137
|
+
$script:gWake = $wg
|
|
1138
|
+
$script:gDict = $dict
|
|
1139
|
+
return $true
|
|
1140
|
+
} catch {
|
|
1141
|
+
Write-Err 'SR-INIT'
|
|
1142
|
+
return $false
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function Start-Auto {
|
|
1147
|
+
if (-not (Ensure-Recognizer)) { return }
|
|
1148
|
+
$script:autoOn = $true
|
|
1149
|
+
$script:dictating = $false
|
|
1150
|
+
$script:woke = $false
|
|
1151
|
+
try { $script:gDict.Enabled = $true; $script:gWake.Enabled = $false } catch { }
|
|
1152
|
+
try { $script:rec.RecognizeAsync([System.Speech.Recognition.RecognizeMode]::Multiple); Add-Content -LiteralPath $ErrorPath -Value ((Get-Date -Format 'HH:mm:ss') + ' AUTO-ON') -Encoding UTF8 } catch { Write-Err 'SR-AUTO' }
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function Enter-Dictation {
|
|
1156
|
+
if (-not (Ensure-Recognizer)) { Show-Bubble '耳朵打不开…检查一下麦克风?'; return }
|
|
1157
|
+
$script:dictating = $true
|
|
1158
|
+
$script:woke = $false
|
|
1159
|
+
$script:heard = $null
|
|
1160
|
+
$script:heardConf = 0.0
|
|
1161
|
+
$script:dictDeadline = (Get-Date).AddSeconds(11)
|
|
1162
|
+
try { $script:gWake.Enabled = $false; $script:gDict.Enabled = $true } catch { }
|
|
1163
|
+
[System.Media.SystemSounds]::Asterisk.Play()
|
|
1164
|
+
Show-Bubble '我在听…(说完停一下)'
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function BackToWake {
|
|
1168
|
+
$script:dictating = $false
|
|
1169
|
+
$script:heard = $null
|
|
1170
|
+
$script:heardConf = 0.0
|
|
1171
|
+
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function Start-Listen { Show-Bubble '语音在后台自动听,主人直接说话就行。' }
|
|
1175
|
+
function Stop-Listen { BackToWake }
|
|
1176
|
+
|
|
1177
|
+
function Open-Chat {
|
|
1178
|
+
try {
|
|
1179
|
+
if ($null -eq $script:chatForm) {
|
|
1180
|
+
$cf = New-Object System.Windows.Forms.Form
|
|
1181
|
+
$cf.FormBorderStyle = 'FixedToolWindow'
|
|
1182
|
+
$cf.StartPosition = 'Manual'
|
|
1183
|
+
$cf.TopMost = $true
|
|
1184
|
+
$cf.ShowInTaskbar = $false
|
|
1185
|
+
$cf.Text = '和大肥鱼说话'
|
|
1186
|
+
$cf.Width = 360
|
|
1187
|
+
$cf.Height = 96
|
|
1188
|
+
$cf.BackColor = [System.Drawing.Color]::FromArgb(255, 252, 244)
|
|
1189
|
+
$tb = New-Object System.Windows.Forms.TextBox
|
|
1190
|
+
$tb.SetBounds(12, 12, 324, 26)
|
|
1191
|
+
$tb.Font = New-Object System.Drawing.Font('Microsoft YaHei UI', 10.5)
|
|
1192
|
+
$cf.Controls.Add($tb)
|
|
1193
|
+
$cf.Add_Shown({ $script:chatBox.Focus() })
|
|
1194
|
+
$tb.Add_KeyDown({
|
|
1195
|
+
param($s, $e)
|
|
1196
|
+
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
|
|
1197
|
+
$e.SuppressKeyPress = $true
|
|
1198
|
+
$txt = $script:chatBox.Text.Trim()
|
|
1199
|
+
if ($txt) {
|
|
1200
|
+
$script:chatBox.Text = ''
|
|
1201
|
+
$script:chatForm.Hide()
|
|
1202
|
+
Start-Chat $txt
|
|
1203
|
+
}
|
|
1204
|
+
} elseif ($e.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
|
|
1205
|
+
$script:chatForm.Hide()
|
|
1206
|
+
}
|
|
1207
|
+
})
|
|
1208
|
+
$script:chatForm = $cf
|
|
1209
|
+
$script:chatBox = $tb
|
|
1210
|
+
}
|
|
1211
|
+
$script:chatForm.Left = [Math]::Min([Math]::Max(0, [int]$script:x + 20), $wa.Right - 370)
|
|
1212
|
+
$script:chatForm.Top = [Math]::Max(0, [int]$script:y - 110)
|
|
1213
|
+
$script:chatForm.Show()
|
|
1214
|
+
$script:chatForm.BringToFront()
|
|
1215
|
+
$script:chatBox.Focus()
|
|
1216
|
+
} catch { Write-Err 'CHATUI-ERR' }
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
# 语音识别常见同音错字纠正
|
|
1221
|
+
function Fix-AsrText([string]$s) {
|
|
1222
|
+
if (-not $s) { return $s }
|
|
1223
|
+
$map = @{
|
|
1224
|
+
'大飞云' = '大肥鱼'; '大肥云' = '大肥鱼'; '白回鱼' = '大肥鱼'; '但肥鱼' = '大肥鱼'
|
|
1225
|
+
'大分鱼' = '大肥鱼'; '大福鱼' = '大肥鱼'; '大雨' = '大肥鱼'; '大肥' = '大肥鱼'
|
|
1226
|
+
'小金' = '小鲸'; '小青' = '小鲸'; '小静' = '小鲸'; '小欣' = '小鲸'; '小京' = '小鲸'
|
|
1227
|
+
}
|
|
1228
|
+
foreach ($k in $map.Keys) { $s = $s.Replace($k, $map[$k]) }
|
|
1229
|
+
return $s
|
|
1230
|
+
}
|
|
1231
|
+
function Start-Chat([string]$text) {
|
|
1232
|
+
Write-Err ('CHAT-START busy=' + $script:chatBusy + ' text=' + $text)
|
|
1233
|
+
$text = Fix-AsrText $text
|
|
1234
|
+
if ($text -match '记住|别忘了|提醒我|答应我|约定') {
|
|
1235
|
+
# 问句不算交代,别把"我们约定过什么"记成便签
|
|
1236
|
+
if ($text -notmatch '[??]' -and $text -notmatch '什么|哪件事|哪一个|是不是|吗') { Save-Note $text (Get-DueFromText $text) }
|
|
1237
|
+
}
|
|
1238
|
+
if ($script:chatBusy) { $script:chatPending = $text; Show-Bubble '等我先说完这一句…'; return }
|
|
1239
|
+
$key = Get-ApiKey
|
|
1240
|
+
if (-not $key) { Show-Bubble '我没有钥匙,打不开话匣子…'; return }
|
|
1241
|
+
$script:chatBusy = $true
|
|
1242
|
+
$script:lastUser = $text
|
|
1243
|
+
Show-Bubble '…'
|
|
1244
|
+
$pn = $script:cfg.persona
|
|
1245
|
+
$sys = '你是主人桌面上的一条圆滚滚的蓝色小鱼,名字叫' + $pn.name + ',爸爸叫' + $pn.dad + ',妈妈叫' + $pn.mom + ',生日是' + $pn.birthday + ',出生在' + $pn.birthplace + '。有人问起家人、生日、出生地就照这个答,不要瞎编。' + $pn.extra
|
|
1246
|
+
$moodMap = @{ 'happy' = '心情很好'; 'sad' = '有点委屈'; 'hungry' = '肚子有点饿'; 'lonely' = '有点被冷落'; 'normal' = '心情平静' }
|
|
1247
|
+
$moodDesc = $moodMap[[string]$script:mood]
|
|
1248
|
+
if (-not $moodDesc) { $moodDesc = '心情平静' }
|
|
1249
|
+
if ($pn.likes) { $sys = $sys + ' 你喜欢:' + $pn.likes + '。' }
|
|
1250
|
+
if ($pn.dislikes) { $sys = $sys + ' 你讨厌:' + $pn.dislikes + '。' }
|
|
1251
|
+
$noteArr = @(Load-Notes)
|
|
1252
|
+
if ($noteArr.Count -gt 0) {
|
|
1253
|
+
$noteText = (@($noteArr | Select-Object -Last 5 | ForEach-Object { [string]$_.text }) -join ';')
|
|
1254
|
+
if ($noteText) { $sys = $sys + ' 主人交代过要你记住的事:' + $noteText + '。' }
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
if ($((Get-Date).ToString('MM-dd')) -eq $script:birthdayMMDD) { $sys = $sys + ' 提醒你:今天是你自己的生日,可以主动跟主人讨点好吃的。' }
|
|
1258
|
+
} catch { }
|
|
1259
|
+
$sys = $sys + ' 你现在的状态:' + $moodDesc + ',饥饿值约' + [int]$script:hunger + ',和主人的亲密度约' + [int]$script:intimacy + '。说话要自然带出这种状态,但不要报具体数字。'
|
|
1260
|
+
if ($script:memorySummary) { $sys = $sys + ' 你记得之前和主人聊过:' + $script:memorySummary }
|
|
1261
|
+
$msgs = @(@{ role = 'system'; content = $sys })
|
|
1262
|
+
foreach ($m in $script:chatHistory) { $msgs += $m }
|
|
1263
|
+
$msgs += @{ role = 'user'; content = $text }
|
|
1264
|
+
$body = @{ model = 'deepseek-flash'; messages = $msgs; max_tokens = 300; temperature = 0.9; thinking = @{ type = 'disabled' } } | ConvertTo-Json -Depth 6
|
|
1265
|
+
if ($null -eq $script:http) {
|
|
1266
|
+
$script:http = New-Object System.Net.Http.HttpClient
|
|
1267
|
+
$script:http.Timeout = [TimeSpan]::FromSeconds(40)
|
|
1268
|
+
$script:http.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $key)
|
|
1269
|
+
}
|
|
1270
|
+
$content = New-Object System.Net.Http.StringContent($body, [Text.Encoding]::UTF8, 'application/json')
|
|
1271
|
+
$script:chatTask = $script:http.PostAsync('https://api.deepseek.com/chat/completions', $content)
|
|
1272
|
+
$script:chatTimer.Start()
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
$script:chatTimer = New-Object System.Windows.Forms.Timer
|
|
1276
|
+
$script:chatTimer.Interval = 300
|
|
1277
|
+
$script:chatTimer.Add_Tick({
|
|
1278
|
+
try {
|
|
1279
|
+
if ($null -eq $script:chatTask -or -not $script:chatTask.IsCompleted) { return }
|
|
1280
|
+
$script:chatTimer.Stop()
|
|
1281
|
+
$resp = $script:chatTask.GetAwaiter().GetResult()
|
|
1282
|
+
$jsonText = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
|
1283
|
+
try { $resp.Dispose() } catch { }
|
|
1284
|
+
$script:chatTask = $null
|
|
1285
|
+
$reply = ''
|
|
1286
|
+
if ($jsonText) { try { $reply = [string]($jsonText | ConvertFrom-Json).choices[0].message.content } catch { } }
|
|
1287
|
+
if ($out -and $out.Count -gt 0) { $reply = [string]$out[0] }
|
|
1288
|
+
if ([string]::IsNullOrWhiteSpace($reply)) { $reply = '(吐了个泡泡)没听清,再说一遍嘛' }
|
|
1289
|
+
$reply = $reply.Trim()
|
|
1290
|
+
$script:chatHistory += @{ role = 'user'; content = $script:lastUser }
|
|
1291
|
+
$script:chatHistory += @{ role = 'assistant'; content = $reply }
|
|
1292
|
+
if ($script:chatHistory.Count -gt 24) { $script:chatHistory = @($script:chatHistory[($script:chatHistory.Count - 24)..($script:chatHistory.Count - 1)]); Save-Memory }
|
|
1293
|
+
Save-Memory
|
|
1294
|
+
Save-State
|
|
1295
|
+
$script:summaryCount = [int]$script:summaryCount + 1
|
|
1296
|
+
if (($script:summaryCount % 10) -eq 0 -and $script:chatHistory.Count -ge 6) { Update-Summary }
|
|
1297
|
+
# 亲密度与情绪
|
|
1298
|
+
$script:intimacy = [Math]::Min(100, $script:intimacy + 1.5)
|
|
1299
|
+
if ($script:lastUser -match '闭嘴|烦|滚|讨厌|笨蛋|丑') {
|
|
1300
|
+
$script:mood = 'sad'; $script:moodUntil = (Get-Date).AddSeconds(150)
|
|
1301
|
+
} elseif ($script:lastUser -match '乖|可爱|好鱼|棒|喜欢你|真聪明') {
|
|
1302
|
+
$script:mood = 'happy'; $script:moodUntil = (Get-Date).AddSeconds(150)
|
|
1303
|
+
}
|
|
1304
|
+
# 情绪:被夸冒爱心,被说胖就鼓起来
|
|
1305
|
+
if ($script:lastUser -match '乖|可爱|好鱼|棒|喜欢你|真聪明') {
|
|
1306
|
+
$script:pulse = 1.18
|
|
1307
|
+
foreach ($dx in @(-24, 0, 24)) { $script:hearts += @{ dx = $dx; age = 0.0; life = 1.4 } }
|
|
1308
|
+
} elseif ($script:lastUser -match '胖|肥|圆|丑') {
|
|
1309
|
+
$script:pulse = 1.26
|
|
1310
|
+
}
|
|
1311
|
+
$script:lastReply = $reply
|
|
1312
|
+
Show-Bubble $reply
|
|
1313
|
+
Speak-Text $reply
|
|
1314
|
+
$script:chatBusy = $false
|
|
1315
|
+
if ($script:chatPending) { $pp = $script:chatPending; $script:chatPending = $null; Start-Chat $pp }
|
|
1316
|
+
} catch {
|
|
1317
|
+
$script:chatBusy = $false
|
|
1318
|
+
$script:chatTimer.Stop()
|
|
1319
|
+
Show-Bubble '(呛了口水)刚才没接通…再试一次?'
|
|
1320
|
+
Write-Err 'CHAT-ERR'
|
|
1321
|
+
}
|
|
1322
|
+
})
|
|
1323
|
+
|
|
1324
|
+
$form.Add_MouseDown({
|
|
1325
|
+
param($s, $e)
|
|
1326
|
+
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
1327
|
+
$script:dragging = $true
|
|
1328
|
+
$script:dragActive = $false
|
|
1329
|
+
$script:dragMoved = 0
|
|
1330
|
+
$script:dragOff.x = $e.X
|
|
1331
|
+
$script:dragOff.y = $e.Y
|
|
1332
|
+
$script:dragStart.x = $e.X
|
|
1333
|
+
$script:dragStart.y = $e.Y
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
$form.Add_MouseMove({
|
|
1337
|
+
param($s, $e)
|
|
1338
|
+
if ($script:dragging) {
|
|
1339
|
+
$dx = $e.X - $script:dragStart.x
|
|
1340
|
+
$dy = $e.Y - $script:dragStart.y
|
|
1341
|
+
if (-not $script:dragActive -and ([Math]::Abs($dx) + [Math]::Abs($dy)) -lt 4) { return }
|
|
1342
|
+
$script:dragActive = $true
|
|
1343
|
+
$script:x = $e.X + $form.Left - $script:dragOff.x
|
|
1344
|
+
$script:y = $e.Y + $form.Top - $script:dragOff.y
|
|
1345
|
+
$script:dragMoved += 1
|
|
1346
|
+
$script:baseY = $script:y
|
|
1347
|
+
if ([int]$script:x -ne $script:lastPx -or [int]$script:y -ne $script:lastPy) { $form.SetBounds([int]$script:x, [int]$script:y, $W, $H); $script:lastPx = [int]$script:x; $script:lastPy = [int]$script:y }
|
|
1348
|
+
}
|
|
1349
|
+
})
|
|
1350
|
+
$form.Add_MouseUp({
|
|
1351
|
+
param($s, $e)
|
|
1352
|
+
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
1353
|
+
$wasDrag = $script:dragging -and $script:dragActive
|
|
1354
|
+
$script:dragging = $false
|
|
1355
|
+
$script:dragActive = $false
|
|
1356
|
+
$script:baseY = $script:y
|
|
1357
|
+
if ($wasDrag) {
|
|
1358
|
+
$snap = 18
|
|
1359
|
+
$maxX = $wa.Right - $W
|
|
1360
|
+
$bottom = $wa.Bottom - $H
|
|
1361
|
+
if ($script:x -lt $snap) { $script:x = 0 }
|
|
1362
|
+
elseif ($script:x -gt ($maxX - $snap)) { $script:x = $maxX }
|
|
1363
|
+
if ($script:y -lt $snap) { $script:y = 0 }
|
|
1364
|
+
elseif ($script:y -gt ($bottom - $snap)) { $script:y = $bottom }
|
|
1365
|
+
if ([int]$script:x -ne $script:lastPx -or [int]$script:y -ne $script:lastPy) { $form.SetBounds([int]$script:x, [int]$script:y, $W, $H); $script:lastPx = [int]$script:x; $script:lastPy = [int]$script:y }
|
|
1366
|
+
}
|
|
1367
|
+
if (-not $wasDrag) { Invoke-Feed }
|
|
1368
|
+
Save-State
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
# 右键菜单:延迟到鼠标事件之后再弹出,避免"闪退"(事件循环里 Show 会被立刻关掉)
|
|
1372
|
+
$script:menuTimer = New-Object System.Windows.Forms.Timer
|
|
1373
|
+
$script:menuTimer.Interval = 120
|
|
1374
|
+
$script:menuTimer.Add_Tick({
|
|
1375
|
+
$script:menuTimer.Stop()
|
|
1376
|
+
try {
|
|
1377
|
+
$miPause.Text = if ($script:paused) { '继续游' } else { '停下歇会儿' }
|
|
1378
|
+
try { $miBalance.Text = Get-BalanceText } catch { }
|
|
1379
|
+
$menu.Show($script:menuScreenPt)
|
|
1380
|
+
} catch { Write-Err 'MENU-SHOW-ERR' }
|
|
1381
|
+
})
|
|
1382
|
+
$form.Add_MouseUp({
|
|
1383
|
+
param($s, $e)
|
|
1384
|
+
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Right) {
|
|
1385
|
+
$script:menuScreenPt = [System.Windows.Forms.Cursor]::Position
|
|
1386
|
+
$script:menuTimer.Start()
|
|
1387
|
+
}
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
$miFeed.Add_Click({ Invoke-Feed })
|
|
832
1391
|
$miTalk.Add_Click({ Start-Listen })
|
|
833
1392
|
$miWake.Add_Click({
|
|
834
1393
|
$script:autoOn = -not $script:autoOn
|
|
835
1394
|
if ($script:autoOn) { Start-Auto; $miWake.Text = '语音待命:开' } else { try { if ($script:rec) { $script:rec.RecognizeAsyncCancel() } } catch { }; BackToWake; $miWake.Text = '语音待命:关' }
|
|
836
|
-
})
|
|
837
|
-
$
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
$
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
$script:
|
|
854
|
-
$
|
|
855
|
-
$script:
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
$
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
$
|
|
865
|
-
$script:
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
$
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
$
|
|
872
|
-
$
|
|
873
|
-
$script:
|
|
874
|
-
|
|
875
|
-
$
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
$script:
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
$
|
|
930
|
-
$script:
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
$
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
$
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
#
|
|
975
|
-
if ($script:
|
|
976
|
-
$
|
|
977
|
-
$
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
$
|
|
1002
|
-
|
|
1395
|
+
})
|
|
1396
|
+
$miType.Add_Click({ Show-Input })
|
|
1397
|
+
$miRemind.Add_Click({ Show-Reminder })
|
|
1398
|
+
$miRemindList.Add_Click({ Show-ReminderList })
|
|
1399
|
+
$miClear.Add_Click({
|
|
1400
|
+
$r = [System.Windows.Forms.MessageBox]::Show('确定要清空全部聊天记录和长期摘要吗?
|
|
1401
|
+
清空后不可恢复(提醒事项会保留)。', '清除记忆', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Warning)
|
|
1402
|
+
if ($r -eq [System.Windows.Forms.DialogResult]::Yes) {
|
|
1403
|
+
$script:chatHistory = @()
|
|
1404
|
+
$script:memorySummary = ''
|
|
1405
|
+
try { Remove-Item -LiteralPath $script:memPath -Force -ErrorAction SilentlyContinue } catch { }
|
|
1406
|
+
try { Save-Memory } catch { }
|
|
1407
|
+
Show-Bubble '记忆清空了,咱们重新认识一下吧'
|
|
1408
|
+
Write-Err 'MEMORY-CLEARED'
|
|
1409
|
+
}
|
|
1410
|
+
})
|
|
1411
|
+
$miChat.Add_Click({
|
|
1412
|
+
$script:chatOn = -not $script:chatOn
|
|
1413
|
+
$miChat.Text = if ($script:chatOn) { '语音聊天:开' } else { '语音聊天:关(省 token)' }
|
|
1414
|
+
if ($script:chatOn) { Start-Ear; Show-Bubble '聊天开啦,我继续听' } else { Stop-Ear; Show-Bubble '聊天先关着,省点 token~' }
|
|
1415
|
+
})
|
|
1416
|
+
$miBalance.Add_Click({
|
|
1417
|
+
$script:balCache = $null
|
|
1418
|
+
$miBalance.Text = Get-BalanceText
|
|
1419
|
+
Show-Bubble $miBalance.Text
|
|
1420
|
+
})
|
|
1421
|
+
$miVoice.Add_Click({
|
|
1422
|
+
$script:speakOn = -not $script:speakOn
|
|
1423
|
+
$miVoice.Text = if ($script:speakOn) { '语音朗读:开' } else { '语音朗读:关' }
|
|
1424
|
+
if (-not $script:speakOn -and $script:synth) { try { $script:synth.SpeakAsyncCancelAll() } catch { } }
|
|
1425
|
+
})
|
|
1426
|
+
$miPause.Add_Click({
|
|
1427
|
+
$script:paused = -not $script:paused
|
|
1428
|
+
if (-not $script:paused) { $script:baseY = $script:y }
|
|
1429
|
+
})
|
|
1430
|
+
$miHome.Add_Click({
|
|
1431
|
+
$script:x = Get-DefaultX
|
|
1432
|
+
$script:y = Get-DefaultY
|
|
1433
|
+
$script:baseY = $script:y
|
|
1434
|
+
$script:dir = -1
|
|
1435
|
+
$script:vx = -26.0
|
|
1436
|
+
if ([int]$script:x -ne $script:lastPx -or [int]$script:y -ne $script:lastPy) { $form.SetBounds([int]$script:x, [int]$script:y, $W, $H); $script:lastPx = [int]$script:x; $script:lastPy = [int]$script:y }
|
|
1437
|
+
Save-State
|
|
1438
|
+
})
|
|
1439
|
+
$miBigger.Add_Click({
|
|
1440
|
+
$script:scale = [Math]::Min(1.6, $script:scale + 0.1)
|
|
1441
|
+
Save-State
|
|
1442
|
+
})
|
|
1443
|
+
$miSmall.Add_Click({
|
|
1444
|
+
$script:scale = [Math]::Max(0.6, $script:scale - 0.1)
|
|
1445
|
+
Save-State
|
|
1446
|
+
})
|
|
1447
|
+
$miQuit.Add_Click({
|
|
1448
|
+
try { [IO.File]::WriteAllText((Join-Path $DataDir 'asr.stop'), (Get-Date).ToString('o')) } catch { }
|
|
1449
|
+
try { Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" | Where-Object { $_.CommandLine -match 'asr-listener' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } catch { }
|
|
1450
|
+
Save-State
|
|
1451
|
+
$form.Close()
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
# ---------- 动画计时器 ----------
|
|
1455
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
1456
|
+
$timer.Interval = 33
|
|
1457
|
+
$script:frames = 0
|
|
1458
|
+
|
|
1459
|
+
$timer.Add_Tick({
|
|
1460
|
+
try {
|
|
1461
|
+
$dt = 0.033
|
|
1462
|
+
$script:t += $dt
|
|
1463
|
+
|
|
1464
|
+
# 启动 3 秒后自报家门,让人知道它在听
|
|
1465
|
+
if (-not $script:greeted -and $script:t -gt 3) {
|
|
1466
|
+
$script:greeted = $true
|
|
1467
|
+
Show-Bubble '我在这里听你说话,说完停一下'
|
|
1468
|
+
try { [void](Get-BalanceText) } catch { }
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
# 每 15 秒看一眼耳朵还在不在,掉了就拉起来
|
|
1472
|
+
$script:earCheck += 1
|
|
1473
|
+
if ($script:chatOn -and $script:earCheck % 450 -eq 0) {
|
|
1474
|
+
$alive = $false
|
|
1475
|
+
try { $alive = [bool](Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" | Where-Object { $_.CommandLine -match 'asr-listener' }) } catch { }
|
|
1476
|
+
if (-not $alive) {
|
|
1477
|
+
try { Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File',(Join-Path (Split-Path -Parent $PSCommandPath) 'asr-listener.ps1')) -WindowStyle Hidden } catch { }
|
|
1478
|
+
Show-Bubble '耳朵刚重启,继续听你说'
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
# 脉动回归
|
|
1483
|
+
if ($script:pulse -gt 1.0) { $script:pulse = [Math]::Max(1.0, $script:pulse - $dt * 0.55) }
|
|
1484
|
+
|
|
1485
|
+
# 游动:随机目标点,平滑靠近(不叠加额外抖动)
|
|
1486
|
+
if (-not $script:paused -and -not $script:dragging -and -not $menu.Visible) {
|
|
1487
|
+
$maxTX = [Math]::Max(20, $wa.Right - $W - 10)
|
|
1488
|
+
$maxTY = [Math]::Max(20, $wa.Bottom - $H - 10)
|
|
1489
|
+
if ($null -eq $script:tx) { $script:tx = Get-Random -Minimum 10 -Maximum $maxTX; $script:ty = Get-Random -Minimum 10 -Maximum $maxTY }
|
|
1490
|
+
$dx = [double]$script:tx - [double]$script:x
|
|
1491
|
+
$dy = [double]$script:ty - [double]$script:y
|
|
1492
|
+
$dist = [Math]::Sqrt($dx * $dx + $dy * $dy)
|
|
1493
|
+
if ($dist -lt 14 -and $script:t -gt $script:pauseUntil) {
|
|
1494
|
+
$script:pauseUntil = $script:t + (Get-Random -Minimum 1 -Maximum 4)
|
|
1495
|
+
$script:tx = Get-Random -Minimum 10 -Maximum $maxTX
|
|
1496
|
+
$script:ty = Get-Random -Minimum 10 -Maximum $maxTY
|
|
1497
|
+
} elseif ($script:t -gt $script:pauseUntil) {
|
|
1498
|
+
# 越靠近越减速,避免在目标点附近来回抖
|
|
1499
|
+
$k = 2.4 * $dt
|
|
1500
|
+
if ($dist -lt 60) { $k = [Math]::Max(0.08, $dist / 60.0 * 2.4 * $dt) }
|
|
1501
|
+
if ($k -gt 1.0) { $k = 1.0 }
|
|
1502
|
+
$script:x += $dx * $k
|
|
1503
|
+
$script:y += $dy * $k
|
|
1504
|
+
if ([Math]::Abs($dx) -gt 4) { if ($dx -gt 0) { $script:dir = 1 } else { $script:dir = -1 } }
|
|
1505
|
+
}
|
|
1506
|
+
if ($script:x -lt 0) { $script:x = 0 }; if ($script:x -gt $maxTX) { $script:x = $maxTX }
|
|
1507
|
+
if ($script:y -lt 0) { $script:y = 0 }; if ($script:y -gt $maxTY) { $script:y = $maxTY }
|
|
1508
|
+
$script:baseY = $script:y
|
|
1509
|
+
if ([int]$script:x -ne $script:lastPx -or [int]$script:y -ne $script:lastPy) { $form.SetBounds([int]$script:x, [int]$script:y, $W, $H); $script:lastPx = [int]$script:x; $script:lastPy = [int]$script:y }
|
|
1510
|
+
|
|
1511
|
+
# 气泡每帧跟随鱼,避免错位
|
|
1512
|
+
if ($script:bubble -and $script:bubble.Visible) {
|
|
1513
|
+
$bw = [int]$script:bubble.Width
|
|
1514
|
+
$bh = [int]$script:bubble.Height
|
|
1515
|
+
$nbx = [Math]::Min([Math]::Max(0, [int]$script:x + [int](150 - ($bw / 2))), $wa.Right - $bw - 8)
|
|
1516
|
+
$nby = [int]$script:y + 44 - $bh
|
|
1517
|
+
if ($nby -lt 0) { $nby = [Math]::Min($wa.Bottom - $bh - 8, [int]$script:y + $H - 10) }
|
|
1518
|
+
if ([int]$script:bubble.Left -ne $nbx -or [int]$script:bubble.Top -ne $nby) {
|
|
1519
|
+
$script:bubble.Location = New-Object System.Drawing.Point($nbx, $nby)
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
# 随机转身,别像传送带
|
|
1525
|
+
if ($false -and $script:t -gt $script:nextTurnAt) {
|
|
1526
|
+
$script:nextTurnAt = $script:t + (Get-Random -Minimum 8 -Maximum 16)
|
|
1527
|
+
if ((Get-Random -Minimum 0 -Maximum 100) -lt 45) {
|
|
1528
|
+
$script:dir = -$script:dir
|
|
1529
|
+
$script:vx = -$script:vx
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
# 眨眼
|
|
1534
|
+
if ($script:t -gt $script:nextBlinkAt) {
|
|
1535
|
+
$script:blinkUntil = $script:t + 0.13
|
|
1536
|
+
$script:nextBlinkAt = $script:t + (Get-Random -Minimum 2.8 -Maximum 6.5)
|
|
1537
|
+
}
|
|
1538
|
+
$blink = $script:t -lt $script:blinkUntil
|
|
1539
|
+
|
|
1540
|
+
# 泡泡(数组追加,别用定长集合的 Add)
|
|
1541
|
+
if (-not $script:paused -and $script:t -gt $script:nextBubbleAt) {
|
|
1542
|
+
$script:nextBubbleAt = $script:t + (Get-Random -Minimum 1.1 -Maximum 2.4)
|
|
1543
|
+
$script:bubbles += @{ age = 0.0; life = 2.2; size = (Get-Random -Minimum 0 -Maximum 4) / 2.0; seed = (Get-Random -Minimum 0 -Maximum 628) / 100.0 }
|
|
1544
|
+
}
|
|
1545
|
+
$alive = @()
|
|
1546
|
+
foreach ($b in @($script:bubbles)) {
|
|
1547
|
+
$b.age += $dt
|
|
1548
|
+
if ($b.age -lt $b.life) { $alive += $b }
|
|
1549
|
+
}
|
|
1550
|
+
$script:bubbles = $alive
|
|
1551
|
+
|
|
1552
|
+
$heartsAlive = @()
|
|
1553
|
+
foreach ($h in @($script:hearts)) {
|
|
1554
|
+
$h.age += $dt
|
|
1555
|
+
if ($h.age -lt $h.life) { $heartsAlive += $h }
|
|
1556
|
+
}
|
|
1557
|
+
$script:hearts = $heartsAlive
|
|
1558
|
+
|
|
1559
|
+
# 读后台耳朵的识别结果(每 0.5 秒一次,只读新增部分,不再整文件重读)
|
|
1560
|
+
$script:asrPoll = ([int]$script:asrPoll + 1) % 9
|
|
1561
|
+
if ($script:asrPoll -eq 0 -and -not $script:chatBusy) {
|
|
1562
|
+
foreach ($ln in (Read-NewAsrLines)) {
|
|
1563
|
+
$parts = $ln -split "`t"
|
|
1564
|
+
if ($parts.Count -ge 2 -and $parts[1] -eq 'H') {
|
|
1565
|
+
if (-not $script:lastHeardBubble -or ((Get-Date) - $script:lastHeardBubble).TotalSeconds -gt 12) {
|
|
1566
|
+
$script:lastHeardBubble = Get-Date
|
|
1567
|
+
Show-Bubble '我在听…'
|
|
1568
|
+
}
|
|
1569
|
+
} elseif ($parts.Count -ge 2 -and $parts[1] -eq '?') {
|
|
1570
|
+
if (-not $script:chatBusy) { Show-Bubble '(没听清,再说一遍嘛)' }
|
|
1571
|
+
} elseif ($parts.Count -ge 3 -and $parts[1] -eq '1') {
|
|
1572
|
+
$text = $parts[2].Trim()
|
|
1573
|
+
$dup = $false
|
|
1574
|
+
if ($text.Length -ge 1) {
|
|
1575
|
+
if ($script:lastHeard -eq $text -and ((Get-Date) - $script:lastHeardAt).TotalSeconds -lt 20) { $dup = $true }
|
|
1576
|
+
if (-not $dup) { $script:lastHeard = $text; $script:lastHeardAt = Get-Date }
|
|
1577
|
+
}
|
|
1578
|
+
$lastR = [string]$script:lastReply
|
|
1579
|
+
$echo = $false
|
|
1580
|
+
if ($lastR.Length -ge 6 -and $text.Length -ge 6) {
|
|
1581
|
+
$head = $text.Substring(0, 6)
|
|
1582
|
+
if ($lastR.Contains($head)) { $echo = $true }
|
|
1583
|
+
}
|
|
1584
|
+
$cool = (-not $script:lastReplyAt) -or (((Get-Date) - $script:lastReplyAt).TotalSeconds -gt 3)
|
|
1585
|
+
$long = $text.Length -ge 3
|
|
1586
|
+
if ($text.Length -ge 1 -and -not $dup -and -not $echo -and $script:chatOn -and $long) {
|
|
1587
|
+
if ($cool) { $script:lastReplyAt = Get-Date; Start-Chat $text; break }
|
|
1588
|
+
else { $script:chatPending = $text; break }
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
# 冷却期内排队的消息,冷却过了补发
|
|
1595
|
+
if ($script:chatPending -and -not $script:chatBusy) {
|
|
1596
|
+
if ((-not $script:lastReplyAt) -or (((Get-Date) - $script:lastReplyAt).TotalSeconds -gt 3)) {
|
|
1597
|
+
$pp = $script:chatPending; $script:chatPending = $null; $script:lastReplyAt = Get-Date; Start-Chat $pp
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
# 情绪随时间变化:每分钟饥饿 +1.2,心情按状态重算
|
|
1601
|
+
if (((Get-Date) - $script:moodTick).TotalSeconds -gt 60) {
|
|
1602
|
+
$script:moodTick = Get-Date
|
|
1603
|
+
$script:hunger = [Math]::Min(100, $script:hunger + 1.2)
|
|
1604
|
+
if (-not ($script:moodUntil -and (Get-Date) -lt $script:moodUntil)) {
|
|
1605
|
+
if ($script:hunger -ge 75) { $script:mood = 'hungry' }
|
|
1606
|
+
elseif ($script:intimacy -le 12) { $script:mood = 'lonely' }
|
|
1607
|
+
elseif ($script:intimacy -ge 70 -and $script:hunger -le 40) { $script:mood = 'happy' }
|
|
1608
|
+
else { $script:mood = 'normal' }
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
# 到点的提醒:主动冒泡 + 开口
|
|
1613
|
+
if ($script:frames % 600 -eq 0) {
|
|
1614
|
+
try {
|
|
1615
|
+
$remArr = @(Load-Notes)
|
|
1616
|
+
$remChanged = $false
|
|
1617
|
+
foreach ($n in $remArr) {
|
|
1618
|
+
$dueStr = [string]$n.due
|
|
1619
|
+
if (-not $dueStr) { continue }
|
|
1620
|
+
$isFired = $false
|
|
1621
|
+
try { if ($n.fired) { $isFired = [bool]$n.fired } } catch { }
|
|
1622
|
+
if ($isFired) { continue }
|
|
1623
|
+
try {
|
|
1624
|
+
if ((Get-Date) -ge [datetime]::Parse($dueStr)) {
|
|
1625
|
+
$n | Add-Member -NotePropertyName fired -NotePropertyValue $true -Force
|
|
1626
|
+
$remChanged = $true
|
|
1627
|
+
Show-Bubble ([char]0x23F0 + ' 主人,你让我提醒你:' + [string]$n.text)
|
|
1628
|
+
Speak-Text ('主人,你让我提醒你:' + [string]$n.text)
|
|
1629
|
+
}
|
|
1630
|
+
} catch { }
|
|
1631
|
+
}
|
|
1632
|
+
if ($remChanged) {
|
|
1633
|
+
[IO.File]::WriteAllText($script:notesPath, (ConvertTo-Json -InputObject @($remArr) -Depth 4), (New-Object Text.UTF8Encoding($false)))
|
|
1634
|
+
}
|
|
1635
|
+
} catch { }
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
# 空闲时常驻“在听…”,回复后 8 秒内让位给回复气泡
|
|
1639
|
+
if ($script:chatOn -and -not $script:chatBusy -and -not (Test-Path (Join-Path $DataDir 'asr.talking'))) {
|
|
1640
|
+
$quiet = (-not $script:lastReplyAt) -or (((Get-Date) - $script:lastReplyAt).TotalSeconds -gt 8)
|
|
1641
|
+
$due = (-not $script:lastIdleHint) -or (((Get-Date) - $script:lastIdleHint).TotalSeconds -gt 4)
|
|
1642
|
+
if ($quiet -and $due) {
|
|
1643
|
+
$script:lastIdleHint = Get-Date
|
|
1644
|
+
Show-Bubble '在听…'
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
# 气泡到期就收
|
|
1649
|
+
if ($script:bubble -and $script:bubble.Visible -and (Get-Date) -gt $script:bubbleUntil) { $script:bubble.Hide() }
|
|
1650
|
+
|
|
1651
|
+
# 画画
|
|
1652
|
+
Draw-Fish -g $g -t $script:t -dir $script:dir -scale $script:scale -pulse $script:pulse -blink $blink -mood 0 -expr $script:mood
|
|
1653
|
+
$form.Push($bmp, [int]$script:x, [int]$script:y)
|
|
1654
|
+
|
|
1655
|
+
# 定期存盘
|
|
1656
|
+
$script:frames += 1
|
|
1657
|
+
if ($script:frames % 300 -eq 0) { Save-State }
|
|
1658
|
+
} catch {
|
|
1659
|
+
$line = (Get-Date -Format 'HH:mm:ss') + ' ' + $_.Exception.Message
|
|
1660
|
+
try { Add-Content -LiteralPath $ErrorPath -Value $line -Encoding UTF8 } catch { }
|
|
1661
|
+
}
|
|
1662
|
+
})
|
|
1663
|
+
|
|
1664
|
+
$form.Add_FormClosing({ $timer.Stop(); $script:chatTimer.Stop(); Save-State; Remove-Item -LiteralPath $PidPath -Force -ErrorAction SilentlyContinue })
|
|
1665
|
+
$timer.Start()
|
|
1666
|
+
[System.Windows.Forms.Application]::Run($form)
|