@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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 Python 脚本中的 TASK_PARAMS 常量 → 任务表单动态参数控件
|
|
3
|
+
*
|
|
4
|
+
* 对齐旧版 pet/src/ui/task_panel.py 的 `_parse_task_params`:
|
|
5
|
+
* - 只做**字面量解析**(不执行脚本),仅支持 dict / list / str / number / bool / None
|
|
6
|
+
* - 找不到 TASK_PARAMS 或解析失败 → 返回 null(表单回退到自由文本参数)
|
|
7
|
+
*
|
|
8
|
+
* 脚本内声明示例:
|
|
9
|
+
* TASK_PARAMS = [
|
|
10
|
+
* {"name": "--env", "label": "环境", "type": "select",
|
|
11
|
+
* "options": [{"value": "uat", "label": "UAT"}]},
|
|
12
|
+
* {"name": "--services", "label": "服务", "type": "multiselect",
|
|
13
|
+
* "options": [{"value": "biz", "label": "biz"}]},
|
|
14
|
+
* {"name": "--view", "label": "视图", "type": "text", "placeholder": "如 Y26M11"},
|
|
15
|
+
* ]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface TaskParamOption {
|
|
19
|
+
value: string
|
|
20
|
+
label?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TaskParamType = 'text' | 'select' | 'multiselect'
|
|
24
|
+
|
|
25
|
+
export interface TaskParamDef {
|
|
26
|
+
/** 命令行参数名,如 "--env" */
|
|
27
|
+
name: string
|
|
28
|
+
label?: string
|
|
29
|
+
type?: TaskParamType
|
|
30
|
+
required?: boolean
|
|
31
|
+
placeholder?: string
|
|
32
|
+
help?: string
|
|
33
|
+
options?: TaskParamOption[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ParamArgState {
|
|
37
|
+
values: Record<string, string | string[]>
|
|
38
|
+
/** 不属于任何已声明参数的原有参数(原样保留) */
|
|
39
|
+
extra: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 截取 `TASK_PARAMS = <字面量>` 的字面量源码(到配对括号结束) */
|
|
43
|
+
function extractTaskParamsSource(source: string): string | null {
|
|
44
|
+
const m = /(^|\n)[ \t]*TASK_PARAMS\s*=\s*/.exec(source)
|
|
45
|
+
if (!m) return null
|
|
46
|
+
const start = m.index + m[0].length
|
|
47
|
+
const end = scanBalanced(source, start)
|
|
48
|
+
return end === null ? null : source.slice(start, end)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 从 start 处的 `[` / `{` 起,返回配对括号的结束位置(不含) */
|
|
52
|
+
function scanBalanced(s: string, start: number): number | null {
|
|
53
|
+
let i = start
|
|
54
|
+
while (i < s.length && /\s/.test(s[i])) i++
|
|
55
|
+
const open = s[i]
|
|
56
|
+
if (open !== '[' && open !== '{') return null
|
|
57
|
+
|
|
58
|
+
const stack: string[] = []
|
|
59
|
+
let inStr: string | null = null
|
|
60
|
+
for (; i < s.length; i++) {
|
|
61
|
+
const ch = s[i]
|
|
62
|
+
if (inStr) {
|
|
63
|
+
if (ch === '\\') {
|
|
64
|
+
i++
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (ch === inStr) inStr = null
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (ch === '#') {
|
|
71
|
+
while (i < s.length && s[i] !== '\n') i++
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (ch === '"' || ch === "'") {
|
|
75
|
+
inStr = ch
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (ch === '[' || ch === '{') stack.push(ch)
|
|
79
|
+
else if (ch === ']' || ch === '}') {
|
|
80
|
+
stack.pop()
|
|
81
|
+
if (stack.length === 0) return i + 1
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Python 字面量递归下降解析器(受限子集) */
|
|
88
|
+
class PyLiteralParser {
|
|
89
|
+
private i = 0
|
|
90
|
+
private readonly src: string
|
|
91
|
+
|
|
92
|
+
constructor(src: string) {
|
|
93
|
+
this.src = src
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
parse(): unknown {
|
|
97
|
+
const v = this.value()
|
|
98
|
+
this.skipWs()
|
|
99
|
+
if (this.i < this.src.length) {
|
|
100
|
+
throw new Error(`存在多余内容: ${this.src.slice(this.i, this.i + 20)}`)
|
|
101
|
+
}
|
|
102
|
+
return v
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private skipWs(): void {
|
|
106
|
+
for (;;) {
|
|
107
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++
|
|
108
|
+
if (this.src[this.i] === '#') {
|
|
109
|
+
while (this.i < this.src.length && this.src[this.i] !== '\n') this.i++
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private value(): unknown {
|
|
117
|
+
this.skipWs()
|
|
118
|
+
const ch = this.src[this.i]
|
|
119
|
+
if (ch === '[') return this.list()
|
|
120
|
+
if (ch === '{') return this.dict()
|
|
121
|
+
if (ch === '"' || ch === "'") return this.string()
|
|
122
|
+
if (ch && /[A-Za-z_]/.test(ch)) {
|
|
123
|
+
const word = this.ident()
|
|
124
|
+
// 字符串前缀 r'' / b'' / u'' / f''(f-string 不支持)
|
|
125
|
+
const nxt = this.src[this.i]
|
|
126
|
+
if (nxt === '"' || nxt === "'") {
|
|
127
|
+
if (word === 'f' || word === 'F') throw new Error('不支持 f-string')
|
|
128
|
+
return this.string()
|
|
129
|
+
}
|
|
130
|
+
if (word === 'True') return true
|
|
131
|
+
if (word === 'False') return false
|
|
132
|
+
if (word === 'None') return null
|
|
133
|
+
throw new Error(`不支持的标识符: ${word}`)
|
|
134
|
+
}
|
|
135
|
+
return this.number()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private list(): unknown[] {
|
|
139
|
+
const out: unknown[] = []
|
|
140
|
+
this.i++
|
|
141
|
+
this.skipWs()
|
|
142
|
+
if (this.src[this.i] === ']') {
|
|
143
|
+
this.i++
|
|
144
|
+
return out
|
|
145
|
+
}
|
|
146
|
+
for (;;) {
|
|
147
|
+
out.push(this.value())
|
|
148
|
+
this.skipWs()
|
|
149
|
+
const ch = this.src[this.i]
|
|
150
|
+
if (ch === ',') {
|
|
151
|
+
this.i++
|
|
152
|
+
this.skipWs()
|
|
153
|
+
if (this.src[this.i] === ']') {
|
|
154
|
+
this.i++
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
if (ch === ']') {
|
|
160
|
+
this.i++
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
throw new Error('列表格式错误')
|
|
164
|
+
}
|
|
165
|
+
return out
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private dict(): Record<string, unknown> {
|
|
169
|
+
const out: Record<string, unknown> = {}
|
|
170
|
+
this.i++
|
|
171
|
+
this.skipWs()
|
|
172
|
+
if (this.src[this.i] === '}') {
|
|
173
|
+
this.i++
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
for (;;) {
|
|
177
|
+
this.skipWs()
|
|
178
|
+
const key = this.value()
|
|
179
|
+
this.skipWs()
|
|
180
|
+
if (this.src[this.i] !== ':') throw new Error('字典缺少冒号')
|
|
181
|
+
this.i++
|
|
182
|
+
out[String(key)] = this.value()
|
|
183
|
+
this.skipWs()
|
|
184
|
+
const ch = this.src[this.i]
|
|
185
|
+
if (ch === ',') {
|
|
186
|
+
this.i++
|
|
187
|
+
this.skipWs()
|
|
188
|
+
if (this.src[this.i] === '}') {
|
|
189
|
+
this.i++
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (ch === '}') {
|
|
195
|
+
this.i++
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
throw new Error('字典格式错误')
|
|
199
|
+
}
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private string(): string {
|
|
204
|
+
const quote = this.src[this.i]
|
|
205
|
+
this.i++
|
|
206
|
+
let out = ''
|
|
207
|
+
while (this.i < this.src.length) {
|
|
208
|
+
const ch = this.src[this.i]
|
|
209
|
+
if (ch === '\\') {
|
|
210
|
+
const next = this.src[this.i + 1]
|
|
211
|
+
this.i += 2
|
|
212
|
+
switch (next) {
|
|
213
|
+
case 'n':
|
|
214
|
+
out += '\n'
|
|
215
|
+
break
|
|
216
|
+
case 't':
|
|
217
|
+
out += '\t'
|
|
218
|
+
break
|
|
219
|
+
case 'r':
|
|
220
|
+
out += '\r'
|
|
221
|
+
break
|
|
222
|
+
case '\\':
|
|
223
|
+
out += '\\'
|
|
224
|
+
break
|
|
225
|
+
case "'":
|
|
226
|
+
out += "'"
|
|
227
|
+
break
|
|
228
|
+
case '"':
|
|
229
|
+
out += '"'
|
|
230
|
+
break
|
|
231
|
+
case 'u': {
|
|
232
|
+
const hex = this.src.slice(this.i, this.i + 4)
|
|
233
|
+
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
234
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
235
|
+
this.i += 4
|
|
236
|
+
}
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
out += next ?? ''
|
|
241
|
+
}
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
if (ch === quote) {
|
|
245
|
+
this.i++
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
out += ch
|
|
249
|
+
this.i++
|
|
250
|
+
}
|
|
251
|
+
throw new Error('字符串未闭合')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private ident(): string {
|
|
255
|
+
const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(this.src.slice(this.i))
|
|
256
|
+
if (!m) throw new Error('标识符格式错误')
|
|
257
|
+
this.i += m[0].length
|
|
258
|
+
return m[0]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private number(): number {
|
|
262
|
+
const m = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/.exec(this.src.slice(this.i))
|
|
263
|
+
if (!m || !m[0]) throw new Error(`无法解析的值: ${this.src.slice(this.i, this.i + 12)}`)
|
|
264
|
+
this.i += m[0].length
|
|
265
|
+
return Number(m[0])
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 解析脚本源码中的 TASK_PARAMS;无该常量或解析失败返回 null */
|
|
270
|
+
export function parseTaskParams(source: string): TaskParamDef[] | null {
|
|
271
|
+
if (!source) return null
|
|
272
|
+
try {
|
|
273
|
+
const literal = extractTaskParamsSource(source)
|
|
274
|
+
if (!literal) return null
|
|
275
|
+
const parsed = new PyLiteralParser(literal).parse()
|
|
276
|
+
if (!Array.isArray(parsed)) return null
|
|
277
|
+
|
|
278
|
+
const out: TaskParamDef[] = []
|
|
279
|
+
for (const raw of parsed) {
|
|
280
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue
|
|
281
|
+
const r = raw as Record<string, unknown>
|
|
282
|
+
const name = String(r['name'] ?? '').trim()
|
|
283
|
+
if (!name) continue
|
|
284
|
+
|
|
285
|
+
const rawType = String(r['type'] ?? 'text')
|
|
286
|
+
const type: TaskParamType =
|
|
287
|
+
rawType === 'select' || rawType === 'multiselect' ? rawType : 'text'
|
|
288
|
+
|
|
289
|
+
const options = Array.isArray(r['options'])
|
|
290
|
+
? (r['options'] as unknown[])
|
|
291
|
+
.map((o) => {
|
|
292
|
+
const oo = (o ?? {}) as Record<string, unknown>
|
|
293
|
+
return {
|
|
294
|
+
value: String(oo['value'] ?? ''),
|
|
295
|
+
label: oo['label'] != null ? String(oo['label']) : undefined
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
.filter((o) => o.value !== '')
|
|
299
|
+
: undefined
|
|
300
|
+
|
|
301
|
+
out.push({
|
|
302
|
+
name,
|
|
303
|
+
label: r['label'] != null ? String(r['label']) : undefined,
|
|
304
|
+
type,
|
|
305
|
+
required: r['required'] === true,
|
|
306
|
+
placeholder: r['placeholder'] != null ? String(r['placeholder']) : undefined,
|
|
307
|
+
help: r['help'] != null ? String(r['help']) : undefined,
|
|
308
|
+
options
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
return out.length ? out : null
|
|
312
|
+
} catch {
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 从已有脚本参数回填动态表单(对齐旧版 _restore_params_from_args) */
|
|
318
|
+
export function restoreParamState(params: TaskParamDef[], args: string[]): ParamArgState {
|
|
319
|
+
const values: Record<string, string | string[]> = {}
|
|
320
|
+
const consumed = new Set<number>()
|
|
321
|
+
|
|
322
|
+
for (const p of params) {
|
|
323
|
+
const idx = args.indexOf(p.name)
|
|
324
|
+
if (idx === -1 || idx + 1 >= args.length) continue
|
|
325
|
+
const next = args[idx + 1]
|
|
326
|
+
if (next.startsWith('--')) continue
|
|
327
|
+
consumed.add(idx)
|
|
328
|
+
consumed.add(idx + 1)
|
|
329
|
+
values[p.name] =
|
|
330
|
+
p.type === 'multiselect' ? next.split(',').map((s) => s.trim()).filter(Boolean) : next
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const extra = args.filter((_, i) => !consumed.has(i))
|
|
334
|
+
return { values, extra }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 由动态表单值生成脚本参数(对齐旧版 _collect_params_as_args) */
|
|
338
|
+
export function buildArgsFromParams(
|
|
339
|
+
params: TaskParamDef[],
|
|
340
|
+
values: Record<string, string | string[]>,
|
|
341
|
+
extra: string[] = []
|
|
342
|
+
): string[] {
|
|
343
|
+
const args: string[] = []
|
|
344
|
+
for (const p of params) {
|
|
345
|
+
const v = values[p.name]
|
|
346
|
+
if (p.type === 'multiselect') {
|
|
347
|
+
const arr = Array.isArray(v) ? v : []
|
|
348
|
+
if (arr.length) args.push(p.name, arr.join(','))
|
|
349
|
+
} else {
|
|
350
|
+
const s = typeof v === 'string' ? v.trim() : ''
|
|
351
|
+
if (s) args.push(p.name, s)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
args.push(...extra)
|
|
355
|
+
return args
|
|
356
|
+
}
|
package/shared/types.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享类型定义 — 主进程 / 渲染进程 / 预加载共用
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 宠物状态(值越大优先级越高,与旧版 Python 一致)
|
|
7
|
+
* 使用 const 对象 + 联合类型(兼容 Node 原生 TS strip-only 模式)
|
|
8
|
+
*/
|
|
9
|
+
export const PetState = {
|
|
10
|
+
SLEEP: 0,
|
|
11
|
+
IDLE: 1,
|
|
12
|
+
HOVER: 2,
|
|
13
|
+
BUSY: 3,
|
|
14
|
+
CLICK: 4
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export type PetState = (typeof PetState)[keyof typeof PetState]
|
|
18
|
+
|
|
19
|
+
export type PetStateName = 'sleep' | 'idle' | 'hover' | 'click' | 'busy'
|
|
20
|
+
|
|
21
|
+
export const PET_STATE_NAMES: readonly PetStateName[] = [
|
|
22
|
+
'sleep',
|
|
23
|
+
'idle',
|
|
24
|
+
'hover',
|
|
25
|
+
'click',
|
|
26
|
+
'busy'
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
export function petStateName(state: PetState): PetStateName {
|
|
30
|
+
switch (state) {
|
|
31
|
+
case PetState.SLEEP:
|
|
32
|
+
return 'sleep'
|
|
33
|
+
case PetState.IDLE:
|
|
34
|
+
return 'idle'
|
|
35
|
+
case PetState.HOVER:
|
|
36
|
+
return 'hover'
|
|
37
|
+
case PetState.BUSY:
|
|
38
|
+
return 'busy'
|
|
39
|
+
case PetState.CLICK:
|
|
40
|
+
return 'click'
|
|
41
|
+
default:
|
|
42
|
+
return 'idle'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 点击互动动画类型(轮流触发) */
|
|
47
|
+
export const ClickAnimationType = {
|
|
48
|
+
JUMP: 'jump',
|
|
49
|
+
SQUASH: 'squash',
|
|
50
|
+
SHAKE: 'shake'
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
export type ClickAnimationType = (typeof ClickAnimationType)[keyof typeof ClickAnimationType]
|
|
54
|
+
|
|
55
|
+
/** 系统设置(与旧版 config/settings.json 字段对齐) */
|
|
56
|
+
export interface Settings {
|
|
57
|
+
system: {
|
|
58
|
+
autoStart: boolean
|
|
59
|
+
powerMode: 'balanced' | 'low' | 'high'
|
|
60
|
+
idleSleepMinutes: number
|
|
61
|
+
}
|
|
62
|
+
audio: {
|
|
63
|
+
enabled: boolean
|
|
64
|
+
volume: number
|
|
65
|
+
}
|
|
66
|
+
/** 管理台 WebUI 服务 */
|
|
67
|
+
server: {
|
|
68
|
+
/** 127.0.0.1 仅本机;0.0.0.0 允许局域网访问 */
|
|
69
|
+
host: string
|
|
70
|
+
port: number
|
|
71
|
+
}
|
|
72
|
+
/** 任务可靠性 */
|
|
73
|
+
tasks: {
|
|
74
|
+
/** 连续失败熔断阈值(自动禁用任务),0=关闭 */
|
|
75
|
+
breakerThreshold: number
|
|
76
|
+
}
|
|
77
|
+
/** 微信 Bot(iLink Bot API) */
|
|
78
|
+
wechat: WechatSettings
|
|
79
|
+
/** 扫码枪(串口) */
|
|
80
|
+
scanner: ScannerSettings
|
|
81
|
+
/** Linux 服务器列表(SSH),任务可按 id 选择 */
|
|
82
|
+
servers: LinuxServer[]
|
|
83
|
+
/** Jenkins */
|
|
84
|
+
jenkins: JenkinsConfig
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WechatSettings {
|
|
88
|
+
notifyOnTaskComplete: boolean
|
|
89
|
+
notifyOnTaskFailed: boolean
|
|
90
|
+
notifyOnStartup: boolean
|
|
91
|
+
autoConnect: boolean
|
|
92
|
+
allowRemoteCommand: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ScannerSettings {
|
|
96
|
+
/** 启动时自动开启扫码枪 */
|
|
97
|
+
enabled: boolean
|
|
98
|
+
port: string
|
|
99
|
+
baud: number
|
|
100
|
+
/** 扫码后推送微信 */
|
|
101
|
+
notifyWechat: boolean
|
|
102
|
+
/** 扫码后桌面弹提醒 */
|
|
103
|
+
notifyDesktop: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Linux 服务器(SSH)— 任务可择一执行 Shell 命令 */
|
|
107
|
+
export interface LinuxServer {
|
|
108
|
+
id: string
|
|
109
|
+
/** 显示名,便于任务里选择时辨认 */
|
|
110
|
+
name: string
|
|
111
|
+
/** IP 或主机名 */
|
|
112
|
+
host: string
|
|
113
|
+
port: number
|
|
114
|
+
username: string
|
|
115
|
+
/** SSH 密码(明文保存在本机 settings.json;仅个人本机工具) */
|
|
116
|
+
password: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Jenkins 配置 — 任务可触发指定 Job 构建 */
|
|
120
|
+
export interface JenkinsConfig {
|
|
121
|
+
/** 例如 http://jenkins.example.com:8080 */
|
|
122
|
+
url: string
|
|
123
|
+
username: string
|
|
124
|
+
/** 密码或 API Token */
|
|
125
|
+
password: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 快捷指令(M5 花瓣菜单用,先占位) */
|
|
129
|
+
export interface QuickAction {
|
|
130
|
+
id: string
|
|
131
|
+
label: string
|
|
132
|
+
icon?: string
|
|
133
|
+
taskId?: string
|
|
134
|
+
color?: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 任务提醒
|
|
139
|
+
* 桌面级提醒(全屏闪烁 / 气泡窗口)已随桌面宠物窗口一起移除,
|
|
140
|
+
* 因此这里只保留提醒文本;呈现由管理台通知 + 系统原生通知负责。
|
|
141
|
+
*/
|
|
142
|
+
export interface Reminder {
|
|
143
|
+
text: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 系统通知 */
|
|
147
|
+
export interface AppNotification {
|
|
148
|
+
title: string
|
|
149
|
+
body: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 任务系统 (M2) ─────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export const TaskType = {
|
|
155
|
+
MANUAL: 'manual', // 手动
|
|
156
|
+
SCHEDULED: 'scheduled', // 定时
|
|
157
|
+
SCENE: 'scene' // 场景触发
|
|
158
|
+
} as const
|
|
159
|
+
export type TaskType = (typeof TaskType)[keyof typeof TaskType]
|
|
160
|
+
|
|
161
|
+
export const TaskStatus = {
|
|
162
|
+
IDLE: 'idle',
|
|
163
|
+
PENDING: 'pending',
|
|
164
|
+
RUNNING: 'running',
|
|
165
|
+
COMPLETED: 'completed',
|
|
166
|
+
FAILED: 'failed',
|
|
167
|
+
CANCELLED: 'cancelled',
|
|
168
|
+
DISABLED: 'disabled'
|
|
169
|
+
} as const
|
|
170
|
+
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus]
|
|
171
|
+
|
|
172
|
+
export const ScheduleType = {
|
|
173
|
+
ONCE: 'once',
|
|
174
|
+
CRON: 'cron',
|
|
175
|
+
DELAY: 'delay',
|
|
176
|
+
INTERVAL: 'interval'
|
|
177
|
+
} as const
|
|
178
|
+
export type ScheduleType = (typeof ScheduleType)[keyof typeof ScheduleType]
|
|
179
|
+
|
|
180
|
+
export const SceneTrigger = {
|
|
181
|
+
IDLE: 'idle',
|
|
182
|
+
STARTUP: 'startup',
|
|
183
|
+
NETWORK: 'network'
|
|
184
|
+
} as const
|
|
185
|
+
export type SceneTrigger = (typeof SceneTrigger)[keyof typeof SceneTrigger]
|
|
186
|
+
|
|
187
|
+
/** 任务触发条件(字段与旧版 tasks.json 对齐) */
|
|
188
|
+
export interface TaskTrigger {
|
|
189
|
+
/** once | cron | delay | interval | idle | startup | network */
|
|
190
|
+
type: string
|
|
191
|
+
/** "YYYY-MM-DD HH:MM" (once) */
|
|
192
|
+
datetime: string | null
|
|
193
|
+
/** cron 表达式(5 段) */
|
|
194
|
+
cron: string | null
|
|
195
|
+
/** 延时秒数 (delay) */
|
|
196
|
+
delaySeconds: number | null
|
|
197
|
+
/** 间隔分钟数 (interval) */
|
|
198
|
+
intervalMinutes: number | null
|
|
199
|
+
/** 闲置分钟数 (idle) */
|
|
200
|
+
idleMinutes: number | null
|
|
201
|
+
/** -1 无限循环 */
|
|
202
|
+
maxExecutions: number
|
|
203
|
+
/** 排除法定节假日(mac 暂不实现具体节假日表,默认 false) */
|
|
204
|
+
holidayCheck: boolean
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 任务配置(与旧版 TaskConfig + tasks.json 字段对齐) */
|
|
208
|
+
export interface TaskConfig {
|
|
209
|
+
name: string
|
|
210
|
+
type: TaskType
|
|
211
|
+
trigger: TaskTrigger
|
|
212
|
+
|
|
213
|
+
/** 动作标识: python_script / show_reminder / ssh_command / jenkins_build / ... */
|
|
214
|
+
action: string
|
|
215
|
+
actionParams: Record<string, unknown>
|
|
216
|
+
|
|
217
|
+
/** Python 脚本相关 */
|
|
218
|
+
scriptPath: string
|
|
219
|
+
interpreter: string | null
|
|
220
|
+
scriptArgs: string[]
|
|
221
|
+
scriptTimeout: number
|
|
222
|
+
scriptWorkDir: string | null
|
|
223
|
+
|
|
224
|
+
retryCount: number
|
|
225
|
+
enabled: boolean
|
|
226
|
+
|
|
227
|
+
/** 通知通道: ["desktop"] ["wechat"] ["desktop","wechat"] */
|
|
228
|
+
notifyChannels: string[]
|
|
229
|
+
|
|
230
|
+
/** 任务链: 完成后触发的下一个任务 id (M5) */
|
|
231
|
+
chainNext: string | null
|
|
232
|
+
/** 链触发条件: "exit==0" / "output contains xxx" (M5) */
|
|
233
|
+
chainCondition: string | null
|
|
234
|
+
|
|
235
|
+
/** 一次性任务(执行后自动删除) */
|
|
236
|
+
oneTime: boolean
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 任务运行时记录(调度器内存态) */
|
|
240
|
+
export interface TaskRecord {
|
|
241
|
+
id: string
|
|
242
|
+
config: TaskConfig
|
|
243
|
+
status: TaskStatus
|
|
244
|
+
createdAt: string
|
|
245
|
+
executionCount: number
|
|
246
|
+
lastRunAt: string | null
|
|
247
|
+
lastResult: string | null
|
|
248
|
+
lastError: string | null
|
|
249
|
+
nextRunAt: string | null
|
|
250
|
+
currentRetry: number
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 任务 DTO(对外/API/UI 使用) */
|
|
254
|
+
export type TaskDto = TaskRecord
|
|
255
|
+
|
|
256
|
+
/** 执行历史记录(SQLite executions 表) */
|
|
257
|
+
export interface ExecutionRecord {
|
|
258
|
+
id: number
|
|
259
|
+
taskId: string
|
|
260
|
+
taskName: string
|
|
261
|
+
status: string // running/completed/failed/timeout/cancelled
|
|
262
|
+
startedAt: string
|
|
263
|
+
finishedAt: string | null
|
|
264
|
+
exitCode: number | null
|
|
265
|
+
durationMs: number | null
|
|
266
|
+
/**
|
|
267
|
+
* 本次执行启动的子进程 pid(脚本类动作才有,其它动作为 null)。
|
|
268
|
+
*
|
|
269
|
+
* 存在的意义:进程被强杀(服务重启 / 崩溃)时走不到收尾逻辑,
|
|
270
|
+
* 而子进程在 Windows 上**不会**随父进程一起死。下次启动靠这个 pid 就能判断
|
|
271
|
+
* 「脚本是不是还在后台跑」,而不是一律判成「执行被中断」——
|
|
272
|
+
* 后者会让部署类脚本被误认为失败,甚至被重复触发。
|
|
273
|
+
*/
|
|
274
|
+
pid?: number | null
|
|
275
|
+
stdout: string
|
|
276
|
+
stderr: string
|
|
277
|
+
error: string
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 任务动作执行结果 */
|
|
281
|
+
export interface ActionResult {
|
|
282
|
+
success: boolean
|
|
283
|
+
stdout?: string
|
|
284
|
+
stderr?: string
|
|
285
|
+
error?: string
|
|
286
|
+
/** 脚本超时标记(执行记录记为 timeout) */
|
|
287
|
+
timedOut?: boolean
|
|
288
|
+
/** 脚本退出码(python_script 动作提供) */
|
|
289
|
+
exitCode?: number | null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 脚本执行结果 */
|
|
293
|
+
export interface ScriptResult {
|
|
294
|
+
success: boolean
|
|
295
|
+
exitCode: number | null
|
|
296
|
+
stdout: string
|
|
297
|
+
stderr: string
|
|
298
|
+
elapsedMs: number
|
|
299
|
+
timedOut: boolean
|
|
300
|
+
errorMessage: string
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 执行状态变更事件 */
|
|
304
|
+
export interface TaskStatusEvent {
|
|
305
|
+
taskId: string
|
|
306
|
+
taskName: string
|
|
307
|
+
status: TaskStatus
|
|
308
|
+
result?: string
|
|
309
|
+
}
|