@wanghaopeng1148/deskpet 2.0.0 → 2.0.2
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 +70 -16
- package/bin/deskpet.mjs +70 -2
- package/dist/node/server/db/database.js +113 -0
- package/dist/node/server/db/migrate-legacy.js +88 -0
- package/dist/node/server/db/task-repository.js +374 -0
- package/dist/node/server/http/http-server.js +493 -0
- package/dist/node/server/http/ws-hub.js +59 -0
- package/dist/node/server/main.js +291 -0
- package/dist/node/server/plugins/actions/builtin.js +67 -0
- package/dist/node/server/plugins/actions/clipboard-watch.js +27 -0
- package/dist/node/server/plugins/actions/http-request.js +41 -0
- package/dist/node/server/plugins/actions/jenkins-build.js +183 -0
- package/dist/node/server/plugins/actions/open-app.js +41 -0
- package/dist/node/server/plugins/actions/python-script.js +180 -0
- package/dist/node/server/plugins/actions/screenshot.js +38 -0
- package/dist/node/server/plugins/actions/send-keystroke.js +100 -0
- package/dist/node/server/plugins/actions/show-reminder.js +7 -0
- package/dist/node/server/plugins/actions/ssh-command.js +123 -0
- package/dist/node/server/plugins/actions/task-chain.js +24 -0
- package/dist/node/server/plugins/actions/volume-control.js +31 -0
- package/dist/node/server/plugins/index.js +35 -0
- package/dist/node/server/plugins/registry.js +23 -0
- package/dist/node/server/services/clipboard-watcher.js +112 -0
- package/dist/node/server/services/config-store.js +141 -0
- package/dist/node/server/services/idle-monitor.js +131 -0
- package/dist/node/server/services/notifier.js +36 -0
- package/dist/node/server/services/quick-actions-store.js +52 -0
- package/dist/node/server/services/remote-connector.js +67 -0
- package/dist/node/server/services/scanner-reader.js +217 -0
- package/dist/node/server/services/script-runner.js +228 -0
- package/dist/node/server/services/snapshot-service.js +135 -0
- package/dist/node/server/services/task-scheduler.js +813 -0
- package/dist/node/server/services/wechat-bot.js +635 -0
- package/dist/node/server/services/wechat-command-types.js +1 -0
- package/dist/node/server/services/wechat-commands.js +330 -0
- package/dist/node/server/suppress-warnings.js +12 -0
- package/dist/node/server/utils/asset-url.js +26 -0
- package/dist/node/server/utils/auto-start.js +186 -0
- package/dist/node/server/utils/clipboard.js +50 -0
- package/dist/node/server/utils/dashboard-url.js +8 -0
- package/dist/node/server/utils/instance-guard.js +165 -0
- package/dist/node/server/utils/native-notify.js +53 -0
- package/dist/node/server/utils/open.js +37 -0
- package/dist/node/server/utils/paths.js +95 -0
- package/dist/node/server/utils/python-interpreter.js +129 -0
- package/dist/node/shared/animation-engine.js +349 -0
- package/dist/node/shared/chain-condition.js +39 -0
- package/dist/node/shared/cron-weekly.js +124 -0
- package/dist/node/shared/py-task-params.js +335 -0
- package/dist/node/shared/types.js +69 -0
- package/package.json +6 -2
- package/server/http/http-server.ts +4 -1
- package/server/utils/auto-start.ts +158 -49
- package/server/utils/paths.ts +28 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信指令路由 — 把微信收到的文本翻译成本地操作(对齐旧版 wechat_commands.py)
|
|
3
|
+
*
|
|
4
|
+
* 8 类指令: 帮助 / 状态 / 任务列表 / 创建任务 / 执行 / 带参执行 / 参数 / 提醒
|
|
5
|
+
* 仅关键字匹配,不接任何大模型;未识别返回提示。
|
|
6
|
+
*/
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
export const HELP_TEXT = [
|
|
9
|
+
'🐾 桌面宠物 · 微信指令',
|
|
10
|
+
'✅创建任务 <名称> <日期> <时间>',
|
|
11
|
+
' 创建定时提醒任务',
|
|
12
|
+
' 如: 创建任务 党会 2026-09-20 14:00',
|
|
13
|
+
'✅任务列表 查看全部任务',
|
|
14
|
+
'✅执行 <名称|序号> -- <参数...>',
|
|
15
|
+
' 用自定义参数执行任务(临时覆盖,不改原配置)',
|
|
16
|
+
'✅带参执行 <名称|序号> -- <参数...> 同上(显式语义)',
|
|
17
|
+
'✅参数 <名称|序号> 查看任务运行参数',
|
|
18
|
+
'✅提醒 <内容> 在电脑上弹出全屏提醒',
|
|
19
|
+
'✅状态 查看桌宠与任务运行状态',
|
|
20
|
+
'✅帮助 显示本说明',
|
|
21
|
+
'任务执行完成后会自动推送结果到这里'
|
|
22
|
+
].join('\n');
|
|
23
|
+
const STATUS_ICON = {
|
|
24
|
+
running: '🔄',
|
|
25
|
+
completed: '✅',
|
|
26
|
+
failed: '❌',
|
|
27
|
+
disabled: '⏸',
|
|
28
|
+
idle: '⚪',
|
|
29
|
+
pending: '⏳',
|
|
30
|
+
cancelled: '🚫'
|
|
31
|
+
};
|
|
32
|
+
const TYPE_NAME = { manual: '手动', scheduled: '定时', scene: '场景' };
|
|
33
|
+
function fmtTime(iso) {
|
|
34
|
+
if (!iso)
|
|
35
|
+
return '-';
|
|
36
|
+
const d = new Date(iso);
|
|
37
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
38
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
39
|
+
}
|
|
40
|
+
export class WechatCommandRouter {
|
|
41
|
+
deps;
|
|
42
|
+
constructor(deps) {
|
|
43
|
+
this.deps = deps;
|
|
44
|
+
}
|
|
45
|
+
/** 处理一条微信消息;返回回复文本,null 表示不回复 */
|
|
46
|
+
handle(text) {
|
|
47
|
+
const t = (text ?? '').trim();
|
|
48
|
+
if (!t)
|
|
49
|
+
return null;
|
|
50
|
+
if (['帮助', 'help', '?', '?', '菜单'].includes(t.toLowerCase()) || t === '帮助') {
|
|
51
|
+
return HELP_TEXT;
|
|
52
|
+
}
|
|
53
|
+
if (['状态', 'status', '运行状态'].includes(t)) {
|
|
54
|
+
return this.status();
|
|
55
|
+
}
|
|
56
|
+
if (['任务列表', '任务', 'tasks', '列表'].includes(t)) {
|
|
57
|
+
return this.listTasks();
|
|
58
|
+
}
|
|
59
|
+
for (const prefix of ['创建任务', '新建任务', '添加任务']) {
|
|
60
|
+
if (t.startsWith(prefix))
|
|
61
|
+
return this.createTask(t.slice(prefix.length).trim());
|
|
62
|
+
}
|
|
63
|
+
for (const prefix of ['执行', '运行', 'run']) {
|
|
64
|
+
if (t.startsWith(prefix))
|
|
65
|
+
return this.execute(t.slice(prefix.length).trim(), false);
|
|
66
|
+
}
|
|
67
|
+
for (const prefix of ['带参执行', '带参运行']) {
|
|
68
|
+
if (t.startsWith(prefix))
|
|
69
|
+
return this.execute(t.slice(prefix.length).trim(), true);
|
|
70
|
+
}
|
|
71
|
+
if (t.startsWith('参数'))
|
|
72
|
+
return this.showParams(t.slice(2).trim());
|
|
73
|
+
if (t.toLowerCase().startsWith('params'))
|
|
74
|
+
return this.showParams(t.slice(6).trim());
|
|
75
|
+
if (t.startsWith('提醒'))
|
|
76
|
+
return this.remind(t.slice(2).trim());
|
|
77
|
+
return '❓ 未识别的指令,发送「帮助」查看可用指令';
|
|
78
|
+
}
|
|
79
|
+
// ── 状态 ────────────────────────────────────────────────
|
|
80
|
+
status() {
|
|
81
|
+
const sched = this.deps.scheduler;
|
|
82
|
+
if (!sched)
|
|
83
|
+
return '🟢 桌面宠物在线\n(任务调度器未接入)';
|
|
84
|
+
const tasks = sched.listTasks();
|
|
85
|
+
const running = tasks.filter((t) => t.status === 'running').length;
|
|
86
|
+
const enabled = tasks.filter((t) => t.config.enabled).length;
|
|
87
|
+
return ('🟢 桌面宠物在线\n' +
|
|
88
|
+
`时间:${fmtTime(new Date().toISOString())}\n` +
|
|
89
|
+
`主机:${os.hostname()}\n` +
|
|
90
|
+
`系统:${os.type()} ${os.release()}\n` +
|
|
91
|
+
`任务:共 ${tasks.length} 个 / 已启用 ${enabled} 个 / 运行中 ${running} 个\n` +
|
|
92
|
+
`调度器:${sched.isStarted ? '运行中' : '已停止'}`);
|
|
93
|
+
}
|
|
94
|
+
// ── 任务列表 ────────────────────────────────────────────
|
|
95
|
+
listTasks() {
|
|
96
|
+
const records = this.deps.scheduler?.listTasks() ?? [];
|
|
97
|
+
if (!records.length) {
|
|
98
|
+
return '📋 当前没有配置任何任务\n请在管理台「任务管理」中添加';
|
|
99
|
+
}
|
|
100
|
+
const lines = ['📋 任务列表'];
|
|
101
|
+
records.forEach((rec, i) => {
|
|
102
|
+
const icon = STATUS_ICON[rec.status] ?? '⚪';
|
|
103
|
+
const flag = rec.config.enabled ? '' : '(已禁用)';
|
|
104
|
+
lines.push(`${i + 1}. ${icon} ${rec.config.name}${flag}`);
|
|
105
|
+
});
|
|
106
|
+
lines.push('发送「执行 1」或「执行 任务名」立即运行');
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
// ── 任务解析:序号/精确/模糊 ────────────────────────────
|
|
110
|
+
resolveTask(target) {
|
|
111
|
+
const records = this.deps.scheduler?.listTasks() ?? [];
|
|
112
|
+
if (!records.length)
|
|
113
|
+
return { record: null, error: '❌ 当前没有配置任何任务' };
|
|
114
|
+
if (!target) {
|
|
115
|
+
return {
|
|
116
|
+
record: null,
|
|
117
|
+
error: '用法:执行/参数 <任务名或序号>\n发送「任务列表」查看可用任务'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let record = null;
|
|
121
|
+
// 1) 按序号
|
|
122
|
+
if (/^\d+$/.test(target)) {
|
|
123
|
+
const idx = parseInt(target, 10);
|
|
124
|
+
if (idx >= 1 && idx <= records.length)
|
|
125
|
+
record = records[idx - 1];
|
|
126
|
+
else
|
|
127
|
+
return { record: null, error: `❌ 序号超出范围(1-${records.length})` };
|
|
128
|
+
}
|
|
129
|
+
// 2) 名称精确匹配
|
|
130
|
+
if (!record) {
|
|
131
|
+
record = records.find((r) => r.config.name === target) ?? null;
|
|
132
|
+
}
|
|
133
|
+
// 3) 名称模糊匹配
|
|
134
|
+
if (!record) {
|
|
135
|
+
const hits = records.filter((r) => r.config.name.includes(target));
|
|
136
|
+
if (hits.length === 1)
|
|
137
|
+
record = hits[0];
|
|
138
|
+
else if (hits.length > 1) {
|
|
139
|
+
const names = hits
|
|
140
|
+
.slice(0, 5)
|
|
141
|
+
.map((r) => r.config.name)
|
|
142
|
+
.join('、');
|
|
143
|
+
return { record: null, error: `❓ 匹配到多个任务:${names}\n请发送完整任务名或序号` };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!record) {
|
|
147
|
+
return { record: null, error: `❌ 未找到任务「${target}」\n发送「任务列表」查看可用任务` };
|
|
148
|
+
}
|
|
149
|
+
return { record, error: null };
|
|
150
|
+
}
|
|
151
|
+
// ── 执行 / 带参执行 ─────────────────────────────────────
|
|
152
|
+
execute(target, forceArgs) {
|
|
153
|
+
const sched = this.deps.scheduler;
|
|
154
|
+
if (!sched)
|
|
155
|
+
return '❌ 任务调度器未接入,无法执行任务';
|
|
156
|
+
// 解析 "任务名 -- arg1 arg2"
|
|
157
|
+
let taskTarget = target;
|
|
158
|
+
let scriptArgs = [];
|
|
159
|
+
if (target.includes(' -- ')) {
|
|
160
|
+
const [head, tail] = target.split(' -- ', 2);
|
|
161
|
+
taskTarget = head.trim();
|
|
162
|
+
scriptArgs = tail.trim() ? tail.trim().split(/\s+/) : [];
|
|
163
|
+
}
|
|
164
|
+
if (forceArgs && !scriptArgs) {
|
|
165
|
+
return ('用法:带参执行 <任务名|序号> -- <参数1> <参数2> ...\n' +
|
|
166
|
+
'例如:带参执行 备份 -- /data /backup\n' +
|
|
167
|
+
'参数会临时覆盖原配置,执行完自动恢复');
|
|
168
|
+
}
|
|
169
|
+
const { record, error } = this.resolveTask(taskTarget);
|
|
170
|
+
if (!record)
|
|
171
|
+
return error;
|
|
172
|
+
if (!record.config.enabled) {
|
|
173
|
+
return `⏸ 任务「${record.config.name}」已被禁用,请先在管理台启用`;
|
|
174
|
+
}
|
|
175
|
+
if (record.status === 'running') {
|
|
176
|
+
return `🔄 任务「${record.config.name}」正在执行中,请稍候`;
|
|
177
|
+
}
|
|
178
|
+
if (scriptArgs.length > 0) {
|
|
179
|
+
if (record.config.action !== 'python_script') {
|
|
180
|
+
return `⚠️ 任务「${record.config.name}」不是脚本任务\n只有 Python 脚本任务支持自定义参数`;
|
|
181
|
+
}
|
|
182
|
+
const ok = sched.runTaskWithArgs(record.id, scriptArgs);
|
|
183
|
+
if (!ok)
|
|
184
|
+
return `❌ 任务「${record.config.name}」启动失败`;
|
|
185
|
+
return (`🔔 已开始带参执行任务「${record.config.name}」\n` +
|
|
186
|
+
`临时参数:${scriptArgs.join(' ')}\n` +
|
|
187
|
+
`(执行完自动恢复原配置参数)\n完成后会自动推送结果`);
|
|
188
|
+
}
|
|
189
|
+
const ok = sched.requestRun(record.id);
|
|
190
|
+
if (!ok)
|
|
191
|
+
return `❌ 任务「${record.config.name}」启动失败`;
|
|
192
|
+
return `🔔 已开始执行任务「${record.config.name}」\n完成后会自动推送结果`;
|
|
193
|
+
}
|
|
194
|
+
// ── 查看参数 ────────────────────────────────────────────
|
|
195
|
+
showParams(target) {
|
|
196
|
+
const { record, error } = this.resolveTask(target);
|
|
197
|
+
if (!record)
|
|
198
|
+
return error;
|
|
199
|
+
const cfg = record.config;
|
|
200
|
+
const lines = ['🔧 任务运行参数', `【${cfg.name}】`];
|
|
201
|
+
lines.push(`类型:${TYPE_NAME[cfg.type] ?? cfg.type}|状态:${record.status}|${cfg.enabled ? '启用' : '已禁用'}`);
|
|
202
|
+
lines.push(`动作:${cfg.action || '(无)'}`);
|
|
203
|
+
if (cfg.action === 'python_script') {
|
|
204
|
+
lines.push(`脚本:${cfg.scriptPath || '(未设置)'}`);
|
|
205
|
+
if (cfg.interpreter)
|
|
206
|
+
lines.push(`解释器:${cfg.interpreter}`);
|
|
207
|
+
if (cfg.scriptArgs?.length)
|
|
208
|
+
lines.push(`脚本参数:${cfg.scriptArgs.join(' ')}`);
|
|
209
|
+
if (cfg.scriptWorkDir)
|
|
210
|
+
lines.push(`工作目录:${cfg.scriptWorkDir}`);
|
|
211
|
+
lines.push(`超时:${cfg.scriptTimeout}s`);
|
|
212
|
+
}
|
|
213
|
+
const tr = cfg.trigger;
|
|
214
|
+
if (cfg.type === 'scheduled') {
|
|
215
|
+
let trig = '定时(未配置)';
|
|
216
|
+
if (tr.type === 'cron' && tr.cron)
|
|
217
|
+
trig = `定时 (cron: ${tr.cron})`;
|
|
218
|
+
else if (tr.type === 'once' && tr.datetime)
|
|
219
|
+
trig = `定时 (${tr.datetime})`;
|
|
220
|
+
else if (tr.type === 'delay' && tr.delaySeconds)
|
|
221
|
+
trig = `延时 (${tr.delaySeconds}s)`;
|
|
222
|
+
else if (tr.type === 'interval' && tr.intervalMinutes)
|
|
223
|
+
trig = `间隔 (${tr.intervalMinutes} 分钟)`;
|
|
224
|
+
lines.push(`触发:${trig}`);
|
|
225
|
+
}
|
|
226
|
+
else if (cfg.type === 'scene') {
|
|
227
|
+
const sceneMap = { idle: '闲置', startup: '开机', network: '联网' };
|
|
228
|
+
lines.push(`触发:场景(${sceneMap[tr.type] ?? tr.type})`);
|
|
229
|
+
}
|
|
230
|
+
if (cfg.actionParams && Object.keys(cfg.actionParams).length > 0) {
|
|
231
|
+
lines.push('运行参数:');
|
|
232
|
+
for (const [k, v] of Object.entries(cfg.actionParams)) {
|
|
233
|
+
lines.push(` ${k}:${String(v)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
lines.push('运行参数:(无)');
|
|
238
|
+
}
|
|
239
|
+
lines.push(`重试:${cfg.retryCount} 次`);
|
|
240
|
+
lines.push(`累计执行:${record.executionCount} 次`);
|
|
241
|
+
if (record.lastRunAt)
|
|
242
|
+
lines.push(`最后运行:${fmtTime(record.lastRunAt)}`);
|
|
243
|
+
return lines.join('\n');
|
|
244
|
+
}
|
|
245
|
+
// ── 提醒 ────────────────────────────────────────────────
|
|
246
|
+
remind(content) {
|
|
247
|
+
if (!content)
|
|
248
|
+
return '用法:提醒 <提醒内容>\n例如:提醒 该开会了';
|
|
249
|
+
try {
|
|
250
|
+
this.deps.reminder(content);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return `❌ 提醒弹出失败:${String(err)}`;
|
|
254
|
+
}
|
|
255
|
+
return `✅ 已在电脑上弹出提醒:${content}`;
|
|
256
|
+
}
|
|
257
|
+
// ── 创建任务(定时提醒,oneTime) ───────────────────────
|
|
258
|
+
createTask(content) {
|
|
259
|
+
const sched = this.deps.scheduler;
|
|
260
|
+
if (!sched)
|
|
261
|
+
return '❌ 任务调度器未接入,无法创建任务';
|
|
262
|
+
if (!content) {
|
|
263
|
+
return ('用法:创建任务 <任务名> <日期> <时间>\n' +
|
|
264
|
+
'例如:创建任务 党会 2026-09-20 14:00\n' +
|
|
265
|
+
'任务到时间会在电脑上弹出桌面提醒');
|
|
266
|
+
}
|
|
267
|
+
const dateMatch = /\d{4}[-/]\d{1,2}[-/]\d{1,2}/.exec(content);
|
|
268
|
+
const timeMatch = /\d{1,2}[::]\d{2}/.exec(content);
|
|
269
|
+
if (!dateMatch) {
|
|
270
|
+
return ('❌ 未识别到日期\n格式:创建任务 <任务名> YYYY-MM-DD HH:MM\n例如:创建任务 党会 2026-09-20 14:00');
|
|
271
|
+
}
|
|
272
|
+
// 规范化日期
|
|
273
|
+
const dateParts = dateMatch[0].replace(/\//g, '-').split('-');
|
|
274
|
+
const dateStr = `${dateParts[0]}-${String(Number(dateParts[1])).padStart(2, '0')}-${String(Number(dateParts[2])).padStart(2, '0')}`;
|
|
275
|
+
// 规范化时间(缺省 09:00)
|
|
276
|
+
let timeStr = '09:00';
|
|
277
|
+
if (timeMatch) {
|
|
278
|
+
const tp = timeMatch[0].replace(':', ':').split(':');
|
|
279
|
+
timeStr = `${String(Number(tp[0])).padStart(2, '0')}:${tp[1]}`;
|
|
280
|
+
}
|
|
281
|
+
const datetimeStr = `${dateStr} ${timeStr}`;
|
|
282
|
+
// 提取任务名:剔除日期与时间片段
|
|
283
|
+
let name = content;
|
|
284
|
+
if (timeMatch)
|
|
285
|
+
name = name.replace(timeMatch[0], ' ');
|
|
286
|
+
name = name.replace(dateMatch[0], ' ');
|
|
287
|
+
name = name.split(/\s+/).filter(Boolean).join(' ');
|
|
288
|
+
if (!name)
|
|
289
|
+
return '❌ 任务名不能为空\n例如:创建任务 党会 2026-09-20 14:00';
|
|
290
|
+
// 校验未来时间
|
|
291
|
+
const runTime = new Date(`${datetimeStr.replace(' ', 'T')}:00`);
|
|
292
|
+
if (Number.isNaN(runTime.getTime())) {
|
|
293
|
+
return `❌ 日期时间格式错误: ${datetimeStr}`;
|
|
294
|
+
}
|
|
295
|
+
if (runTime.getTime() <= Date.now()) {
|
|
296
|
+
return `❌ 时间已过去: ${datetimeStr}\n请指定未来的时间`;
|
|
297
|
+
}
|
|
298
|
+
const dto = sched.createTask({
|
|
299
|
+
name,
|
|
300
|
+
type: 'scheduled',
|
|
301
|
+
trigger: {
|
|
302
|
+
type: 'once',
|
|
303
|
+
datetime: datetimeStr,
|
|
304
|
+
cron: null,
|
|
305
|
+
delaySeconds: null,
|
|
306
|
+
intervalMinutes: null,
|
|
307
|
+
idleMinutes: null,
|
|
308
|
+
maxExecutions: -1,
|
|
309
|
+
holidayCheck: false
|
|
310
|
+
},
|
|
311
|
+
action: 'show_reminder',
|
|
312
|
+
actionParams: { text: `提醒:${name}` },
|
|
313
|
+
scriptPath: '',
|
|
314
|
+
interpreter: null,
|
|
315
|
+
scriptArgs: [],
|
|
316
|
+
scriptTimeout: 120,
|
|
317
|
+
scriptWorkDir: null,
|
|
318
|
+
retryCount: 0,
|
|
319
|
+
enabled: true,
|
|
320
|
+
notifyChannels: [],
|
|
321
|
+
chainNext: null,
|
|
322
|
+
chainCondition: null,
|
|
323
|
+
oneTime: true
|
|
324
|
+
});
|
|
325
|
+
return (`✅ 定时提醒已创建\n` +
|
|
326
|
+
`任务:${dto.config.name}\n` +
|
|
327
|
+
`时间:${datetimeStr}\n` +
|
|
328
|
+
`到点将在电脑上弹出全屏提醒,执行后自动删除`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程级警告开关 —— 必须在 main.ts 的所有其他导入之前引入(ESM 导入按顺序求值)。
|
|
3
|
+
* node:sqlite 在 Node 22(Electron 37 内置)中为实验性 API,在内置模块编译期
|
|
4
|
+
* (compileForInternalLoader)同步发出 ExperimentalWarning,该时机早于 'warning'
|
|
5
|
+
* 事件分派,内部调用也不经过 process.emitWarning 属性查找,故事件监听与包装均无法
|
|
6
|
+
* 阻止默认打印。Node 在 emitExperimentalWarning 内提供了 process.noProcessWarnings
|
|
7
|
+
* 开关(等价 --no-warnings 对实验警告的效果):在加载 node:sqlite 前置为 true 即可
|
|
8
|
+
* 静默实验警告;DeprecationWarning 等其他类型警告不受影响。
|
|
9
|
+
*/
|
|
10
|
+
;
|
|
11
|
+
process.noProcessWarnings = true;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 皮肤资源 URL — 取代原 deskpet:// 自定义协议
|
|
3
|
+
*
|
|
4
|
+
* 皮肤精灵图等本地文件通过 HTTP 接口 /api/assets?path=<绝对路径> 分发,
|
|
5
|
+
* 服务端做目录白名单校验,避免任意文件读取。
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { normalize, resolve, sep } from 'node:path';
|
|
9
|
+
import { getResourcesRoot, getUserSkinsDir } from "./paths.js";
|
|
10
|
+
export const ASSET_ENDPOINT = '/api/assets';
|
|
11
|
+
/**
|
|
12
|
+
* 校验并解析 /api/assets 的 path 参数
|
|
13
|
+
* 仅允许访问用户皮肤目录与内置资源目录内的已存在文件,越界返回 null
|
|
14
|
+
*/
|
|
15
|
+
export function resolveAssetRequest(raw) {
|
|
16
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
17
|
+
return null;
|
|
18
|
+
const abs = normalize(resolve(raw.trim()));
|
|
19
|
+
for (const root of [getUserSkinsDir(), getResourcesRoot()]) {
|
|
20
|
+
const r = normalize(resolve(root));
|
|
21
|
+
if (abs === r || abs.startsWith(r + sep)) {
|
|
22
|
+
return existsSync(abs) ? abs : null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 开机自启
|
|
3
|
+
*
|
|
4
|
+
* 各平台实现:
|
|
5
|
+
* Windows 启动文件夹放一个 .cmd —— 不依赖 reg / schtasks 命令,受管控的机器一样能用
|
|
6
|
+
* macOS ~/Library/LaunchAgents/<APP_ID>.plist
|
|
7
|
+
* Linux ~/.config/autostart/<APP_ID>.desktop(XDG 标准,GNOME/KDE 都认)
|
|
8
|
+
*
|
|
9
|
+
* 启动命令统一走 `bin/deskpet.mjs --no-open`,理由:
|
|
10
|
+
* · bin/ 在「源码运行」和「npm 包运行」两种形态下都存在(scripts/ 不进发布包)
|
|
11
|
+
* · 它内部用 process.execPath 拉起 server/main.ts,不依赖 PATH 里有没有 node / npm
|
|
12
|
+
* · 脚本自己会按 package.json 推导根目录,所以不依赖工作目录
|
|
13
|
+
* · --no-open 避免每次开机都弹一个浏览器
|
|
14
|
+
*
|
|
15
|
+
* 历史坑(说明为什么要主动清理旧文件):
|
|
16
|
+
* 本项目早期是 Electron 应用,自启是安装包写在启动文件夹里的快捷方式
|
|
17
|
+
* (DeskPet.lnk → pet\dist\DeskPet.exe)。后来改成纯 Node 服务、目录也从 pet 换成 pet2.0,
|
|
18
|
+
* 那个旧快捷方式却没人清理 —— 结果用户以为自启开着,实际每次开机拉起的是旧程序,
|
|
19
|
+
* 新服务仍然要手动启动。因此下面在写入自启时会**顺手删掉同名的旧 .lnk**。
|
|
20
|
+
*
|
|
21
|
+
* 仅在用户在设置中开启时写入;任何异常都只记录日志,不影响服务本身。
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
const APP_ID = 'DeskPet';
|
|
28
|
+
/** 应用根目录(源码运行为项目根,npm 包运行为包根) */
|
|
29
|
+
function appRoot() {
|
|
30
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
31
|
+
}
|
|
32
|
+
/** 开机要执行的 node 可执行文件与入口脚本 */
|
|
33
|
+
function launch() {
|
|
34
|
+
return { node: process.execPath, entry: join(appRoot(), 'bin', 'deskpet.mjs') };
|
|
35
|
+
}
|
|
36
|
+
// ── Windows ─────────────────────────────────────────────────
|
|
37
|
+
function winStartupDir() {
|
|
38
|
+
const appData = process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
|
|
39
|
+
return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
40
|
+
}
|
|
41
|
+
function winCmdPath() {
|
|
42
|
+
return join(winStartupDir(), `${APP_ID}.cmd`);
|
|
43
|
+
}
|
|
44
|
+
function winLegacyLnkPath() {
|
|
45
|
+
return join(winStartupDir(), `${APP_ID}.lnk`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 生成自启批处理。
|
|
49
|
+
*
|
|
50
|
+
* 注意两点:
|
|
51
|
+
* · 换行必须是 CRLF,注释用英文 —— cmd 按系统 ANSI 代码页解析批处理,
|
|
52
|
+
* 写中文注释在中文 Windows 上容易乱码(虽然注释不影响执行,但很难看)
|
|
53
|
+
* · 用 `start /min` 让服务窗口最小化,不打扰用户,同时任务栏里能看到它在跑
|
|
54
|
+
*/
|
|
55
|
+
function winCmdContent() {
|
|
56
|
+
const { node, entry } = launch();
|
|
57
|
+
return [
|
|
58
|
+
'@echo off',
|
|
59
|
+
'rem DeskPet autostart - generated by the DeskPet dashboard.',
|
|
60
|
+
'rem Delete this file to disable autostart.',
|
|
61
|
+
`cd /d "${appRoot()}"`,
|
|
62
|
+
`start "${APP_ID}" /min "${node}" "${entry}" --no-open`,
|
|
63
|
+
''
|
|
64
|
+
].join('\r\n');
|
|
65
|
+
}
|
|
66
|
+
// ── macOS ───────────────────────────────────────────────────
|
|
67
|
+
function macPlistPath() {
|
|
68
|
+
return join(homedir(), 'Library', 'LaunchAgents', `${APP_ID}.plist`);
|
|
69
|
+
}
|
|
70
|
+
function macPlistContent() {
|
|
71
|
+
const { node, entry } = launch();
|
|
72
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
74
|
+
<plist version="1.0">
|
|
75
|
+
<dict>
|
|
76
|
+
<key>Label</key>
|
|
77
|
+
<string>${APP_ID}</string>
|
|
78
|
+
<key>ProgramArguments</key>
|
|
79
|
+
<array>
|
|
80
|
+
<string>${node}</string>
|
|
81
|
+
<string>${entry}</string>
|
|
82
|
+
<string>--no-open</string>
|
|
83
|
+
</array>
|
|
84
|
+
<key>WorkingDirectory</key>
|
|
85
|
+
<string>${appRoot()}</string>
|
|
86
|
+
<key>RunAtLoad</key>
|
|
87
|
+
<true/>
|
|
88
|
+
</dict>
|
|
89
|
+
</plist>
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
// ── Linux (XDG autostart) ───────────────────────────────────
|
|
93
|
+
function linuxDesktopPath() {
|
|
94
|
+
return join(homedir(), '.config', 'autostart', `${APP_ID}.desktop`);
|
|
95
|
+
}
|
|
96
|
+
function linuxDesktopContent() {
|
|
97
|
+
const { node, entry } = launch();
|
|
98
|
+
return [
|
|
99
|
+
'[Desktop Entry]',
|
|
100
|
+
'Type=Application',
|
|
101
|
+
`Name=${APP_ID}`,
|
|
102
|
+
'Comment=DeskPet 本机任务自动化服务',
|
|
103
|
+
`Exec="${node}" "${entry}" --no-open`,
|
|
104
|
+
`Path=${appRoot()}`,
|
|
105
|
+
'X-GNOME-Autostart-enabled=true',
|
|
106
|
+
''
|
|
107
|
+
].join('\n');
|
|
108
|
+
}
|
|
109
|
+
// ── 对外接口 ────────────────────────────────────────────────
|
|
110
|
+
/** 开启 / 关闭开机自启,返回是否成功(失败原因会打印到日志) */
|
|
111
|
+
export async function setAutoStart(on) {
|
|
112
|
+
try {
|
|
113
|
+
if (process.platform === 'win32') {
|
|
114
|
+
const dir = winStartupDir();
|
|
115
|
+
mkdirSync(dir, { recursive: true });
|
|
116
|
+
// 清掉 Electron 时代遗留的失效快捷方式(它指向 pet\dist\DeskPet.exe)
|
|
117
|
+
const legacy = winLegacyLnkPath();
|
|
118
|
+
if (existsSync(legacy)) {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(legacy);
|
|
121
|
+
console.log(`[autostart] 已清理旧版自启快捷方式: ${legacy}`);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.warn(`[autostart] 旧快捷方式清理失败(可手动删除): ${String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const file = winCmdPath();
|
|
128
|
+
if (on) {
|
|
129
|
+
writeFileSync(file, winCmdContent(), 'utf-8');
|
|
130
|
+
console.log(`[autostart] 开机自启已开启: ${file}`);
|
|
131
|
+
}
|
|
132
|
+
else if (existsSync(file)) {
|
|
133
|
+
unlinkSync(file);
|
|
134
|
+
console.log('[autostart] 开机自启已关闭');
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (process.platform === 'darwin') {
|
|
139
|
+
const file = macPlistPath();
|
|
140
|
+
if (on) {
|
|
141
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
142
|
+
writeFileSync(file, macPlistContent(), 'utf-8');
|
|
143
|
+
console.log(`[autostart] 开机自启已开启: ${file}`);
|
|
144
|
+
}
|
|
145
|
+
else if (existsSync(file)) {
|
|
146
|
+
unlinkSync(file);
|
|
147
|
+
console.log('[autostart] 开机自启已关闭');
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (process.platform === 'linux') {
|
|
152
|
+
const file = linuxDesktopPath();
|
|
153
|
+
if (on) {
|
|
154
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
155
|
+
writeFileSync(file, linuxDesktopContent(), 'utf-8');
|
|
156
|
+
console.log(`[autostart] 开机自启已开启: ${file}`);
|
|
157
|
+
}
|
|
158
|
+
else if (existsSync(file)) {
|
|
159
|
+
unlinkSync(file);
|
|
160
|
+
console.log('[autostart] 开机自启已关闭');
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
console.warn(`[autostart] 暂不支持的平台: ${process.platform}`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.error('[autostart] 设置失败:', err);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** 查询当前是否已设置自启 */
|
|
173
|
+
export function autoStartEnabled() {
|
|
174
|
+
try {
|
|
175
|
+
if (process.platform === 'win32')
|
|
176
|
+
return existsSync(winCmdPath());
|
|
177
|
+
if (process.platform === 'darwin')
|
|
178
|
+
return existsSync(macPlistPath());
|
|
179
|
+
if (process.platform === 'linux')
|
|
180
|
+
return existsSync(linuxDesktopPath());
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* ignore */
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 剪贴板读写 — 取代 Electron clipboard 模块
|
|
3
|
+
* 通过系统命令实现,失败时静默返回空串(不阻塞任务链路)
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
function run(cmd, args, input) {
|
|
7
|
+
return new Promise((resolvePromise) => {
|
|
8
|
+
const child = execFile(cmd, args, { timeout: 3000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
9
|
+
resolvePromise(err ? '' : String(stdout ?? ''));
|
|
10
|
+
});
|
|
11
|
+
if (input !== undefined)
|
|
12
|
+
child.stdin?.end(input);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** 读取剪贴板文本;不支持的平台返回空串 */
|
|
16
|
+
export async function readClipboardText() {
|
|
17
|
+
try {
|
|
18
|
+
if (process.platform === 'darwin')
|
|
19
|
+
return await run('pbpaste', []);
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
return await run('powershell', [
|
|
22
|
+
'-NoProfile',
|
|
23
|
+
'-Sta',
|
|
24
|
+
'-Command',
|
|
25
|
+
'Get-Clipboard -Raw'
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === 'linux')
|
|
29
|
+
return await run('xclip', ['-selection', 'clipboard', '-o']);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
/** 清空剪贴板 */
|
|
37
|
+
export async function clearClipboard() {
|
|
38
|
+
try {
|
|
39
|
+
if (process.platform === 'darwin')
|
|
40
|
+
await run('pbcopy', [], '');
|
|
41
|
+
else if (process.platform === 'win32') {
|
|
42
|
+
await run('powershell', ['-NoProfile', '-Command', 'Set-Clipboard -Value $null']);
|
|
43
|
+
}
|
|
44
|
+
else if (process.platform === 'linux')
|
|
45
|
+
await run('xclip', ['-selection', 'clipboard'], '');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* ignore */
|
|
49
|
+
}
|
|
50
|
+
}
|