@wanghaopeng1148/deskpet 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +394 -0
- package/bin/deskpet.mjs +142 -0
- package/dist/web/assets/DashboardView-BLXa7VrZ.css +1 -0
- package/dist/web/assets/DashboardView-CTyWcLtQ.js +60 -0
- package/dist/web/assets/SettingsView-7c3RiRrt.js +1 -0
- package/dist/web/assets/SettingsView-BkL0OpZC.css +1 -0
- package/dist/web/assets/TasksView-BTK1OTwU.css +1 -0
- package/dist/web/assets/TasksView-s1MaR5sZ.js +5 -0
- package/dist/web/assets/ToolsView-B8V-A1j_.js +178 -0
- package/dist/web/assets/ToolsView-DGLJATQ9.css +1 -0
- package/dist/web/assets/WeChatView-ChLBhPso.js +1 -0
- package/dist/web/assets/WeChatView-CvdhJ05E.css +1 -0
- package/dist/web/assets/browser-CjSdxGTc.js +8 -0
- package/dist/web/assets/dashboard-DJ_Miuzx.js +93 -0
- package/dist/web/assets/dashboard-Fbcagzwx.css +1 -0
- package/dist/web/dashboard/index.html +13 -0
- package/package.json +71 -0
- package/resources/icons/tray.png +0 -0
- package/resources/icons/tray@2x.png +0 -0
- package/resources/previews/busy.png +0 -0
- package/resources/previews/click.png +0 -0
- package/resources/previews/hover.png +0 -0
- package/resources/previews/idle.png +0 -0
- package/resources/previews/preview.png +0 -0
- package/resources/previews/sleep.png +0 -0
- package/resources/skins/default_cute/pet.json +7 -0
- package/resources/skins/default_cute/spritesheet.webp +0 -0
- package/resources/skins/default_cute/submission.json +29 -0
- package/server/db/database.ts +118 -0
- package/server/db/migrate-legacy.ts +121 -0
- package/server/db/task-repository.ts +585 -0
- package/server/http/http-server.ts +725 -0
- package/server/http/ws-hub.ts +66 -0
- package/server/main.ts +322 -0
- package/server/plugins/actions/builtin.ts +73 -0
- package/server/plugins/actions/clipboard-watch.ts +38 -0
- package/server/plugins/actions/http-request.ts +53 -0
- package/server/plugins/actions/jenkins-build.ts +209 -0
- package/server/plugins/actions/open-app.ts +40 -0
- package/server/plugins/actions/python-script.ts +187 -0
- package/server/plugins/actions/screenshot.ts +41 -0
- package/server/plugins/actions/send-keystroke.ts +103 -0
- package/server/plugins/actions/show-reminder.ts +16 -0
- package/server/plugins/actions/ssh-command.ts +148 -0
- package/server/plugins/actions/task-chain.ts +30 -0
- package/server/plugins/actions/volume-control.ts +35 -0
- package/server/plugins/index.ts +37 -0
- package/server/plugins/registry.ts +72 -0
- package/server/services/clipboard-watcher.ts +126 -0
- package/server/services/config-store.ts +152 -0
- package/server/services/idle-monitor.ts +146 -0
- package/server/services/notifier.ts +54 -0
- package/server/services/quick-actions-store.ts +54 -0
- package/server/services/remote-connector.ts +75 -0
- package/server/services/scanner-reader.ts +257 -0
- package/server/services/script-runner.ts +263 -0
- package/server/services/snapshot-service.ts +196 -0
- package/server/services/task-scheduler.ts +900 -0
- package/server/services/wechat-bot.ts +744 -0
- package/server/services/wechat-command-types.ts +15 -0
- package/server/services/wechat-commands.ts +367 -0
- package/server/suppress-warnings.ts +10 -0
- package/server/utils/asset-url.ts +27 -0
- package/server/utils/auto-start.ts +90 -0
- package/server/utils/clipboard.ts +50 -0
- package/server/utils/dashboard-url.ts +9 -0
- package/server/utils/instance-guard.ts +170 -0
- package/server/utils/native-notify.ts +68 -0
- package/server/utils/open.ts +38 -0
- package/server/utils/paths.ts +72 -0
- package/server/utils/python-interpreter.ts +154 -0
- package/shared/animation-engine.ts +422 -0
- package/shared/chain-condition.ts +60 -0
- package/shared/cron-weekly.ts +131 -0
- package/shared/py-task-params.ts +356 -0
- package/shared/types.ts +309 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动画引擎 — 5 状态动画调度、优先级管理、算法微动
|
|
3
|
+
*
|
|
4
|
+
* 从旧版 Python animation_engine.py 1:1 移植。
|
|
5
|
+
*
|
|
6
|
+
* 状态优先级: 点击(4) > 忙碌(3) > 悬浮(2) > 待机(1) > 休眠(0)
|
|
7
|
+
* 过渡: 0.15s 线性插值
|
|
8
|
+
* 点击动画: 跳跃 / 压扁回弹 / 左右抖动 轮流触发
|
|
9
|
+
*/
|
|
10
|
+
import { ClickAnimationType, PetState } from './types.ts'
|
|
11
|
+
|
|
12
|
+
export interface StateParams {
|
|
13
|
+
fps: number
|
|
14
|
+
/** 上下浮动幅度(px) */
|
|
15
|
+
floatAmplitudePx: number
|
|
16
|
+
/** 浮动周期(s) */
|
|
17
|
+
floatPeriodS: number
|
|
18
|
+
/** 基础缩放 */
|
|
19
|
+
scaleBase: number
|
|
20
|
+
/** 缩放变化幅度 */
|
|
21
|
+
scaleAmplitude: number
|
|
22
|
+
/** 旋转微动幅度(度) */
|
|
23
|
+
rotationAmplitude: number
|
|
24
|
+
/** 循环周期(s) */
|
|
25
|
+
cycleDurationS: number
|
|
26
|
+
/** 淡入淡出时长(s) */
|
|
27
|
+
fadeDurationS: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STATE_PARAMS: Record<PetState, StateParams> = {
|
|
31
|
+
[PetState.SLEEP]: {
|
|
32
|
+
fps: 10,
|
|
33
|
+
floatAmplitudePx: 1.0,
|
|
34
|
+
floatPeriodS: 5.0,
|
|
35
|
+
scaleBase: 1.0,
|
|
36
|
+
scaleAmplitude: 0.01,
|
|
37
|
+
rotationAmplitude: 0.0,
|
|
38
|
+
cycleDurationS: 5.0,
|
|
39
|
+
fadeDurationS: 0.15
|
|
40
|
+
},
|
|
41
|
+
[PetState.IDLE]: {
|
|
42
|
+
fps: 20,
|
|
43
|
+
floatAmplitudePx: 0.0,
|
|
44
|
+
floatPeriodS: 2.5,
|
|
45
|
+
scaleBase: 1.0,
|
|
46
|
+
scaleAmplitude: 0.0,
|
|
47
|
+
rotationAmplitude: 0.0,
|
|
48
|
+
cycleDurationS: 2.5,
|
|
49
|
+
fadeDurationS: 0.15
|
|
50
|
+
},
|
|
51
|
+
[PetState.HOVER]: {
|
|
52
|
+
fps: 30,
|
|
53
|
+
floatAmplitudePx: 2.0,
|
|
54
|
+
floatPeriodS: 1.5,
|
|
55
|
+
scaleBase: 1.05,
|
|
56
|
+
scaleAmplitude: 0.01,
|
|
57
|
+
rotationAmplitude: 2.0,
|
|
58
|
+
cycleDurationS: 1.5,
|
|
59
|
+
fadeDurationS: 0.15
|
|
60
|
+
},
|
|
61
|
+
[PetState.BUSY]: {
|
|
62
|
+
fps: 30,
|
|
63
|
+
floatAmplitudePx: 4.0,
|
|
64
|
+
floatPeriodS: 1.2,
|
|
65
|
+
scaleBase: 1.0,
|
|
66
|
+
scaleAmplitude: 0.03,
|
|
67
|
+
rotationAmplitude: 3.0,
|
|
68
|
+
cycleDurationS: 1.2,
|
|
69
|
+
fadeDurationS: 0.15
|
|
70
|
+
},
|
|
71
|
+
[PetState.CLICK]: {
|
|
72
|
+
fps: 30,
|
|
73
|
+
floatAmplitudePx: 0.0,
|
|
74
|
+
floatPeriodS: 0.4,
|
|
75
|
+
scaleBase: 1.0,
|
|
76
|
+
scaleAmplitude: 0.0,
|
|
77
|
+
rotationAmplitude: 0.0,
|
|
78
|
+
cycleDurationS: 0.4,
|
|
79
|
+
fadeDurationS: 0.15
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 点击动画时长(按类型) */
|
|
84
|
+
const CLICK_DURATIONS: Record<ClickAnimationType, number> = {
|
|
85
|
+
[ClickAnimationType.JUMP]: 0.6,
|
|
86
|
+
[ClickAnimationType.SQUASH]: 0.5,
|
|
87
|
+
[ClickAnimationType.SHAKE]: 0.5
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 单帧变换参数(渲染层直接消费) */
|
|
91
|
+
export interface AnimationFrame {
|
|
92
|
+
offsetX: number
|
|
93
|
+
offsetY: number
|
|
94
|
+
scale: number
|
|
95
|
+
rotation: number
|
|
96
|
+
opacity: number
|
|
97
|
+
state: PetState
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type StateChangeCallback = (oldState: PetState, newState: PetState) => void
|
|
101
|
+
|
|
102
|
+
export class AnimationEngine {
|
|
103
|
+
private currentState: PetState = PetState.IDLE
|
|
104
|
+
private targetState: PetState = PetState.IDLE
|
|
105
|
+
private previousState: PetState = PetState.IDLE
|
|
106
|
+
|
|
107
|
+
private stateStartTime: number
|
|
108
|
+
private lastInteractionTime: number
|
|
109
|
+
private idleTimeoutS: number
|
|
110
|
+
|
|
111
|
+
private clickAnimating = false
|
|
112
|
+
private clickStartTime = 0
|
|
113
|
+
private clickDuration = STATE_PARAMS[PetState.CLICK].cycleDurationS
|
|
114
|
+
private clickAnimType: ClickAnimationType = ClickAnimationType.SQUASH
|
|
115
|
+
private clickAnimCycle: ClickAnimationType[] = [
|
|
116
|
+
ClickAnimationType.JUMP,
|
|
117
|
+
ClickAnimationType.SQUASH,
|
|
118
|
+
ClickAnimationType.SHAKE
|
|
119
|
+
]
|
|
120
|
+
private clickAnimIndex = 0
|
|
121
|
+
|
|
122
|
+
private inTransition = false
|
|
123
|
+
private transitionStart = 0
|
|
124
|
+
private transitionDuration = 0.15
|
|
125
|
+
|
|
126
|
+
private enabled = true
|
|
127
|
+
private speedMultiplier = 1.0
|
|
128
|
+
private amplitudeMultiplier = 1.0
|
|
129
|
+
|
|
130
|
+
private stateChangeCallbacks: StateChangeCallback[] = []
|
|
131
|
+
|
|
132
|
+
constructor(idleTimeoutS = 180.0) {
|
|
133
|
+
this.stateStartTime = now()
|
|
134
|
+
this.lastInteractionTime = now()
|
|
135
|
+
this.idleTimeoutS = idleTimeoutS
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 当前对外状态(过渡期间返回目标状态) */
|
|
139
|
+
get state(): PetState {
|
|
140
|
+
return this.inTransition ? this.targetState : this.currentState
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get isSleeping(): boolean {
|
|
144
|
+
return this.currentState === PetState.SLEEP
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get isEnabled(): boolean {
|
|
148
|
+
return this.enabled
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setEnabled(value: boolean): void {
|
|
152
|
+
this.enabled = value
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get speed(): number {
|
|
156
|
+
return this.speedMultiplier
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setSpeed(value: number): void {
|
|
160
|
+
this.speedMultiplier = clamp(value, 0.1, 5.0)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get amplitude(): number {
|
|
164
|
+
return this.amplitudeMultiplier
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setAmplitude(value: number): void {
|
|
168
|
+
this.amplitudeMultiplier = clamp(value, 0.1, 5.0)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 当前状态已持续毫秒数(渲染层用于推进精灵帧) */
|
|
172
|
+
get stateElapsedMs(): number {
|
|
173
|
+
return (now() - this.stateStartTime) * 1000
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 请求切换到指定状态(遵循优先级规则) */
|
|
177
|
+
requestState(state: PetState, force = false): boolean {
|
|
178
|
+
if (!this.enabled && state !== PetState.SLEEP) return false
|
|
179
|
+
|
|
180
|
+
// 点击动画播放中,仅更高优先级可打断
|
|
181
|
+
if (this.clickAnimating) {
|
|
182
|
+
if (state > PetState.CLICK) {
|
|
183
|
+
this.clickAnimating = false
|
|
184
|
+
} else {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const effectiveState = this.inTransition ? this.targetState : this.currentState
|
|
190
|
+
|
|
191
|
+
// 优先级检查(SLEEP 总是允许)
|
|
192
|
+
if (!force && state < effectiveState && state !== PetState.SLEEP) return false
|
|
193
|
+
|
|
194
|
+
// 同状态不重复切换(CLICK 总是响应)
|
|
195
|
+
if (state === effectiveState && state !== PetState.CLICK) {
|
|
196
|
+
this.lastInteractionTime = now()
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── CLICK 单次动画 ──────────────────────────────
|
|
201
|
+
if (state === PetState.CLICK) {
|
|
202
|
+
this.clickAnimating = true
|
|
203
|
+
this.clickStartTime = now()
|
|
204
|
+
this.clickAnimType = this.clickAnimCycle[this.clickAnimIndex]
|
|
205
|
+
this.clickAnimIndex = (this.clickAnimIndex + 1) % this.clickAnimCycle.length
|
|
206
|
+
this.clickDuration = CLICK_DURATIONS[this.clickAnimType] ?? 0.4
|
|
207
|
+
// 点击动画即时响应,跳过过渡
|
|
208
|
+
this.previousState = this.currentState
|
|
209
|
+
this.currentState = state
|
|
210
|
+
this.inTransition = false
|
|
211
|
+
this.stateStartTime = now()
|
|
212
|
+
this.lastInteractionTime = now()
|
|
213
|
+
this.emitStateChange(this.previousState, state)
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.previousState = this.currentState
|
|
218
|
+
this.targetState = state
|
|
219
|
+
this.startTransition()
|
|
220
|
+
this.lastInteractionTime = now()
|
|
221
|
+
this.emitStateChange(this.previousState, this.targetState)
|
|
222
|
+
return true
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 通知有用户交互(重置闲置计时器) */
|
|
226
|
+
notifyInteraction(): void {
|
|
227
|
+
this.lastInteractionTime = now()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 计算当前帧变换参数(每帧调用一次,渲染层 rAF 驱动)
|
|
232
|
+
*/
|
|
233
|
+
computeFrame(): AnimationFrame {
|
|
234
|
+
const t = now()
|
|
235
|
+
|
|
236
|
+
// ── 状态过渡 ────────────────────────────────────
|
|
237
|
+
if (this.inTransition) {
|
|
238
|
+
const progress = (t - this.transitionStart) / this.transitionDuration
|
|
239
|
+
if (progress >= 1.0) {
|
|
240
|
+
this.finishTransition()
|
|
241
|
+
} else {
|
|
242
|
+
const prev = this.computeStateFrame(this.previousState, t)
|
|
243
|
+
const target = this.computeStateFrame(this.targetState, t)
|
|
244
|
+
const p = clamp(progress, 0, 1)
|
|
245
|
+
return {
|
|
246
|
+
offsetX: lerp(prev.offsetX, target.offsetX, p),
|
|
247
|
+
offsetY: lerp(prev.offsetY, target.offsetY, p),
|
|
248
|
+
scale: lerp(prev.scale, target.scale, p),
|
|
249
|
+
rotation: lerp(prev.rotation, target.rotation, p),
|
|
250
|
+
opacity: lerp(prev.opacity, target.opacity, p),
|
|
251
|
+
state: this.targetState
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── 点击动画单次处理 ────────────────────────────
|
|
257
|
+
if (this.clickAnimating) {
|
|
258
|
+
const elapsed = t - this.clickStartTime
|
|
259
|
+
if (elapsed >= this.clickDuration) {
|
|
260
|
+
this.clickAnimating = false
|
|
261
|
+
this.previousState = this.currentState
|
|
262
|
+
this.targetState = PetState.IDLE
|
|
263
|
+
this.startTransition()
|
|
264
|
+
return this.computeFrame() // 递归一次获取过渡帧
|
|
265
|
+
}
|
|
266
|
+
return this.computeClickFrame(elapsed)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── 闲置检测(超时进入休眠) ────────────────────
|
|
270
|
+
if (
|
|
271
|
+
this.idleTimeoutS > 0 &&
|
|
272
|
+
this.currentState !== PetState.SLEEP &&
|
|
273
|
+
this.currentState !== PetState.CLICK
|
|
274
|
+
) {
|
|
275
|
+
const idle = t - this.lastInteractionTime
|
|
276
|
+
if (idle >= this.idleTimeoutS) {
|
|
277
|
+
this.requestState(PetState.SLEEP)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return this.computeStateFrame(this.currentState, t)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private computeStateFrame(state: PetState, t: number): AnimationFrame {
|
|
285
|
+
const params = STATE_PARAMS[state]
|
|
286
|
+
const elapsed = t - this.stateStartTime
|
|
287
|
+
|
|
288
|
+
const period = params.cycleDurationS / Math.max(this.speedMultiplier, 0.01)
|
|
289
|
+
const phase = (elapsed % period) / period
|
|
290
|
+
const wave = easeInOutSine(phase)
|
|
291
|
+
|
|
292
|
+
const frame: AnimationFrame = {
|
|
293
|
+
offsetX: 0,
|
|
294
|
+
offsetY: wave * params.floatAmplitudePx * this.amplitudeMultiplier,
|
|
295
|
+
scale: params.scaleBase + wave * params.scaleAmplitude * this.amplitudeMultiplier,
|
|
296
|
+
rotation: wave * params.rotationAmplitude * this.amplitudeMultiplier,
|
|
297
|
+
opacity: 1.0,
|
|
298
|
+
state
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (state === PetState.SLEEP) {
|
|
302
|
+
frame.opacity = 0.95 + wave * 0.05
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return frame
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private computeClickFrame(elapsed: number): AnimationFrame {
|
|
309
|
+
const total = this.clickDuration
|
|
310
|
+
const progress = Math.min(elapsed / total, 1.0)
|
|
311
|
+
switch (this.clickAnimType) {
|
|
312
|
+
case ClickAnimationType.JUMP:
|
|
313
|
+
return this.computeJumpFrame(progress)
|
|
314
|
+
case ClickAnimationType.SQUASH:
|
|
315
|
+
return this.computeSquashFrame(progress)
|
|
316
|
+
default:
|
|
317
|
+
return this.computeShakeFrame(progress)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 跳跃: 上升 → 下落 → 落地压扁 → 回弹恢复 (0.6s) */
|
|
322
|
+
private computeJumpFrame(progress: number): AnimationFrame {
|
|
323
|
+
const jumpHeight = 35.0
|
|
324
|
+
let offsetY = 0
|
|
325
|
+
let scale = 1.0
|
|
326
|
+
|
|
327
|
+
if (progress < 0.35) {
|
|
328
|
+
const t = progress / 0.35
|
|
329
|
+
offsetY = -jumpHeight * (2 * t - t * t)
|
|
330
|
+
} else if (progress < 0.65) {
|
|
331
|
+
const t = (progress - 0.35) / 0.3
|
|
332
|
+
offsetY = -jumpHeight * (1 - t * t)
|
|
333
|
+
} else if (progress < 0.8) {
|
|
334
|
+
const t = (progress - 0.65) / 0.15
|
|
335
|
+
scale = 1.0 - 0.1 * t
|
|
336
|
+
} else {
|
|
337
|
+
const t = (progress - 0.8) / 0.2
|
|
338
|
+
scale = 0.9 + 0.1 * t
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return { offsetX: 0, offsetY, scale, rotation: 0, opacity: 1, state: PetState.CLICK }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** 压扁回弹: 压扁 100→82% → 拉伸 82→112% → 回弹 (0.5s) */
|
|
345
|
+
private computeSquashFrame(progress: number): AnimationFrame {
|
|
346
|
+
let scale: number
|
|
347
|
+
if (progress < 0.35) {
|
|
348
|
+
const t = progress / 0.35
|
|
349
|
+
scale = 1.0 - 0.18 * t
|
|
350
|
+
} else if (progress < 0.65) {
|
|
351
|
+
const t = (progress - 0.35) / 0.3
|
|
352
|
+
scale = 0.82 + 0.3 * t
|
|
353
|
+
} else {
|
|
354
|
+
const t = (progress - 0.65) / 0.35
|
|
355
|
+
scale = 1.12 - 0.12 * t
|
|
356
|
+
}
|
|
357
|
+
return { offsetX: 0, offsetY: 0, scale, rotation: 0, opacity: 1, state: PetState.CLICK }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 左右抖动: 高频正弦摆动,幅度衰减 (0.5s) */
|
|
361
|
+
private computeShakeFrame(progress: number): AnimationFrame {
|
|
362
|
+
const maxOffset = 10.0
|
|
363
|
+
const decay = 1.0 - progress * 0.7
|
|
364
|
+
const shake = Math.sin(progress * Math.PI * 6) * maxOffset * decay
|
|
365
|
+
return { offsetX: shake, offsetY: 0, scale: 1, rotation: 0, opacity: 1, state: PetState.CLICK }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private startTransition(): void {
|
|
369
|
+
this.inTransition = true
|
|
370
|
+
this.transitionStart = now()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private finishTransition(): void {
|
|
374
|
+
this.inTransition = false
|
|
375
|
+
this.currentState = this.targetState
|
|
376
|
+
this.stateStartTime = now()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 注册状态变更回调 */
|
|
380
|
+
onStateChange(cb: StateChangeCallback): () => void {
|
|
381
|
+
this.stateChangeCallbacks.push(cb)
|
|
382
|
+
return () => {
|
|
383
|
+
const i = this.stateChangeCallbacks.indexOf(cb)
|
|
384
|
+
if (i >= 0) this.stateChangeCallbacks.splice(i, 1)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private emitStateChange(oldState: PetState, newState: PetState): void {
|
|
389
|
+
for (const cb of this.stateChangeCallbacks) {
|
|
390
|
+
try {
|
|
391
|
+
cb(oldState, newState)
|
|
392
|
+
} catch {
|
|
393
|
+
// 回调异常不阻断引擎
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 批量配置动画参数 */
|
|
399
|
+
configure(enabled: boolean, speed: number, amplitude: number): void {
|
|
400
|
+
this.enabled = enabled
|
|
401
|
+
this.speedMultiplier = speed
|
|
402
|
+
this.amplitudeMultiplier = amplitude
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── 工具函数 ────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
function now(): number {
|
|
409
|
+
return performance.now() / 1000
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function clamp(v: number, min: number, max: number): number {
|
|
413
|
+
return Math.max(min, Math.min(v, max))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function lerp(a: number, b: number, t: number): number {
|
|
417
|
+
return a + (b - a) * clamp(t, 0, 1)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function easeInOutSine(t: number): number {
|
|
421
|
+
return Math.sin(t * Math.PI * 2)
|
|
422
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务链条件判定(方案 §3.3.B:条件分支——退出码/输出匹配)
|
|
3
|
+
*
|
|
4
|
+
* 语法:
|
|
5
|
+
* (空)/ always / 成功 → 仅成功时继续
|
|
6
|
+
* failed / 失败 → 仅失败时继续
|
|
7
|
+
* exit==0 / exit!=0 / exit>=N ... → 按脚本退出码
|
|
8
|
+
* output contains xxx → stdout 包含子串
|
|
9
|
+
*/
|
|
10
|
+
import type { ActionResult } from './types.ts'
|
|
11
|
+
|
|
12
|
+
export interface ChainContext {
|
|
13
|
+
success: boolean
|
|
14
|
+
exitCode: number | null
|
|
15
|
+
stdout: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function evalChainCondition(
|
|
19
|
+
cond: string | null | undefined,
|
|
20
|
+
ctx: ChainContext
|
|
21
|
+
): boolean {
|
|
22
|
+
const c = (cond ?? '').trim().toLowerCase()
|
|
23
|
+
if (!c || c === 'always' || c === 'success' || c === '成功') return ctx.success
|
|
24
|
+
if (c === 'failed' || c === '失败') return !ctx.success
|
|
25
|
+
|
|
26
|
+
const m = /^exit\s*(==|!=|>=|<=|>|<)\s*(-?\d+)$/.exec(c)
|
|
27
|
+
if (m) {
|
|
28
|
+
const code = ctx.exitCode ?? (ctx.success ? 0 : -1)
|
|
29
|
+
const target = parseInt(m[2], 10)
|
|
30
|
+
switch (m[1]) {
|
|
31
|
+
case '==':
|
|
32
|
+
return code === target
|
|
33
|
+
case '!=':
|
|
34
|
+
return code !== target
|
|
35
|
+
case '>=':
|
|
36
|
+
return code >= target
|
|
37
|
+
case '<=':
|
|
38
|
+
return code <= target
|
|
39
|
+
case '>':
|
|
40
|
+
return code > target
|
|
41
|
+
case '<':
|
|
42
|
+
return code < target
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const om = /^output\s+contains\s+(.+)$/.exec(c)
|
|
47
|
+
if (om) return ctx.stdout.toLowerCase().includes(om[1].trim().toLowerCase())
|
|
48
|
+
|
|
49
|
+
// 未知条件按成功处理
|
|
50
|
+
return ctx.success
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 便捷:从 ActionResult 构造判定上下文 */
|
|
54
|
+
export function chainContextOf(result: ActionResult): ChainContext {
|
|
55
|
+
return {
|
|
56
|
+
success: result.success,
|
|
57
|
+
exitCode: result.exitCode ?? null,
|
|
58
|
+
stdout: result.stdout ?? ''
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每周定时(cron)的友好表达 —— 对齐旧版「指定时间(每天/每周)」的交互
|
|
3
|
+
*
|
|
4
|
+
* 旧版 pet(src/ui/task_panel.py)是「HH:MM 输入框 + 周一~周日复选框」,
|
|
5
|
+
* 保存为 `分 时 * * 周几`。这里把这套搬到 Web 端,并加上 cron 描述文本。
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ 周几编号采用**标准 cron 语义**:0=周日, 1=周一, ... 6=周六
|
|
8
|
+
* (与 node-schedule / crontab 一致,调度器直接用 schedule.scheduleJob(cron))。
|
|
9
|
+
* 旧版把「周一」映射成 0,与标准相反(属旧版 bug),此处**不沿用**——
|
|
10
|
+
* 否则周一的任务会在周日跑。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 显示顺序(中文习惯从周一开始) */
|
|
14
|
+
export const WEEKDAY_ORDER = [1, 2, 3, 4, 5, 6, 0] as const
|
|
15
|
+
|
|
16
|
+
/** cron 值 → 中文名 */
|
|
17
|
+
export const WEEKDAY_NAMES: Record<number, string> = {
|
|
18
|
+
1: '周一',
|
|
19
|
+
2: '周二',
|
|
20
|
+
3: '周三',
|
|
21
|
+
4: '周四',
|
|
22
|
+
5: '周五',
|
|
23
|
+
6: '周六',
|
|
24
|
+
0: '周日'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 全部 7 天(cron 值) */
|
|
28
|
+
export const ALL_WEEKDAYS = [0, 1, 2, 3, 4, 5, 6]
|
|
29
|
+
|
|
30
|
+
export interface WeeklyCron {
|
|
31
|
+
hour: number
|
|
32
|
+
minute: number
|
|
33
|
+
/** cron 周几值(0=周日) */
|
|
34
|
+
weekdays: number[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function norm(v: number, max: number): number {
|
|
38
|
+
if (!Number.isFinite(v)) return 0
|
|
39
|
+
const n = Math.round(v)
|
|
40
|
+
return Math.max(0, Math.min(max, n))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mod7(v: number): number {
|
|
44
|
+
return ((v % 7) + 7) % 7
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 解析「分 时 * * 周几」形式的 cron
|
|
49
|
+
* @returns 解析成功返回 {hour, minute, weekdays};
|
|
50
|
+
* 无法表示为「每周定时」(如带步进写法、指定了日/月、跨周区间)时返回 null,
|
|
51
|
+
* 调用方应回退到「手动编辑 cron 表达式」,避免把用户的表达式改坏。
|
|
52
|
+
*/
|
|
53
|
+
export function parseWeeklyCron(cron: string | null | undefined): WeeklyCron | null {
|
|
54
|
+
const s = (cron ?? '').trim()
|
|
55
|
+
if (!s) return null
|
|
56
|
+
const parts = s.split(/\s+/)
|
|
57
|
+
if (parts.length !== 5) return null
|
|
58
|
+
const [mi, ho, dom, mon, dow] = parts
|
|
59
|
+
|
|
60
|
+
// 只支持「每周」形态:日、月字段必须为 *
|
|
61
|
+
if (dom !== '*' || mon !== '*') return null
|
|
62
|
+
if (!/^\d{1,2}$/.test(mi) || !/^\d{1,2}$/.test(ho)) return null
|
|
63
|
+
|
|
64
|
+
const minute = Number(mi)
|
|
65
|
+
const hour = Number(ho)
|
|
66
|
+
if (minute > 59 || hour > 23) return null
|
|
67
|
+
|
|
68
|
+
const weekdays = parseDowField(dow)
|
|
69
|
+
if (!weekdays) return null
|
|
70
|
+
return { hour, minute, weekdays }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseDowField(field: string): number[] | null {
|
|
74
|
+
const f = field.trim()
|
|
75
|
+
if (f === '*' || f === '?') return [...ALL_WEEKDAYS]
|
|
76
|
+
const set = new Set<number>()
|
|
77
|
+
for (const raw of f.split(',')) {
|
|
78
|
+
const token = raw.trim()
|
|
79
|
+
if (!token) return null
|
|
80
|
+
// 步进(*/2、1-5/2)不在友好 UI 的表达范围内 → 交给手动编辑
|
|
81
|
+
if (token.includes('/')) return null
|
|
82
|
+
const m = /^(\d)(?:-(\d))?$/.exec(token)
|
|
83
|
+
if (!m) return null
|
|
84
|
+
const a = Number(m[1])
|
|
85
|
+
const b = m[2] === undefined ? a : Number(m[2])
|
|
86
|
+
if (a > 7 || b > 7) return null
|
|
87
|
+
if (a > b) return null // 跨周环绕(如 6-1)不支持
|
|
88
|
+
for (let i = a; i <= b; i++) set.add(mod7(i))
|
|
89
|
+
}
|
|
90
|
+
return set.size > 0 ? [...set].sort((x, y) => x - y) : null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 由「时间 + 周几」生成 cron
|
|
95
|
+
* - 7 天全选(或全不选)→ 周几字段用 `*`(每天)
|
|
96
|
+
*/
|
|
97
|
+
export function buildWeeklyCron(minute: number, hour: number, weekdays: number[]): string {
|
|
98
|
+
const m = norm(minute, 59)
|
|
99
|
+
const h = norm(hour, 23)
|
|
100
|
+
const set = [...new Set((weekdays ?? []).map(mod7))].sort((a, b) => a - b)
|
|
101
|
+
const dow = set.length === 0 || set.length === 7 ? '*' : set.join(',')
|
|
102
|
+
return `${m} ${h} * * ${dow}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 生成中文说明(任务列表展示用),如「每周一、三、五 17:55」「每天 09:00」 */
|
|
106
|
+
export function formatWeeklyCron(cron: string | null | undefined): string | null {
|
|
107
|
+
const p = parseWeeklyCron(cron)
|
|
108
|
+
if (!p) return null
|
|
109
|
+
const time = `${String(p.hour).padStart(2, '0')}:${String(p.minute).padStart(2, '0')}`
|
|
110
|
+
if (p.weekdays.length === 7) return `每天 ${time}`
|
|
111
|
+
// 按中文习惯顺序(周一 → 周日)展示;后续项省略「周」字 → 「每周一、三、五」
|
|
112
|
+
const labels = WEEKDAY_ORDER.filter((w) => p.weekdays.includes(w)).map(
|
|
113
|
+
(w) => WEEKDAY_NAMES[w] ?? String(w)
|
|
114
|
+
)
|
|
115
|
+
const joined = labels.map((n, i) => (i === 0 ? n : n.replace(/^周/, ''))).join('、')
|
|
116
|
+
return `每${joined} ${time}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 快速选择预设 */
|
|
120
|
+
export const WEEKDAY_PRESETS: Array<{ label: string; weekdays: number[] }> = [
|
|
121
|
+
{ label: '每天', weekdays: [...ALL_WEEKDAYS] },
|
|
122
|
+
{ label: '工作日', weekdays: [1, 2, 3, 4, 5] },
|
|
123
|
+
{ label: '周末', weekdays: [6, 0] }
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
/** 判断两个周几数组是否等价(忽略顺序与重复) */
|
|
127
|
+
export function sameWeekdays(a: number[], b: number[]): boolean {
|
|
128
|
+
const sa = [...new Set(a.map(mod7))].sort((x, y) => x - y).join(',')
|
|
129
|
+
const sb = [...new Set(b.map(mod7))].sort((x, y) => x - y).join(',')
|
|
130
|
+
return sa === sb
|
|
131
|
+
}
|