@myassis/gateway 1.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 +194 -0
- package/dist/.env +6 -0
- package/dist/api/index.js +182 -0
- package/dist/config/index.js +41 -0
- package/dist/index.js +183 -0
- package/dist/middleware/auth.js +53 -0
- package/dist/middleware/errorHandler.js +20 -0
- package/dist/routes/agent.js +513 -0
- package/dist/routes/auth.js +172 -0
- package/dist/routes/chat.js +45 -0
- package/dist/routes/config.js +21 -0
- package/dist/routes/models.js +123 -0
- package/dist/routes/service.js +240 -0
- package/dist/routes/settings.js +101 -0
- package/dist/routes/skillHub.js +126 -0
- package/dist/routes/skills.js +159 -0
- package/dist/routes/tasks.js +149 -0
- package/dist/routes/upload.js +129 -0
- package/dist/routes/version.js +66 -0
- package/dist/services/HMSPushService.js +24 -0
- package/dist/services/LocalTaskService.js +223 -0
- package/dist/services/NotificationService.js +242 -0
- package/dist/services/ServiceManager.js +348 -0
- package/dist/services/TaskSchedulerService.js +195 -0
- package/dist/services/TaskService.js +240 -0
- package/dist/services/WebSocketService.js +236 -0
- package/dist/services/agent/Agent.js +120 -0
- package/dist/services/agent/AgentManager.js +265 -0
- package/dist/services/agent/AgentStore.js +73 -0
- package/dist/services/dataService.js +293 -0
- package/dist/services/index.js +15 -0
- package/dist/services/llm/LLMClient.js +724 -0
- package/dist/services/memory/MemoryManager.js +117 -0
- package/dist/services/model/ModelCapabilities.js +141 -0
- package/dist/services/model/index.js +4 -0
- package/dist/services/models.js +16 -0
- package/dist/services/session/MigrationManager.js +176 -0
- package/dist/services/session/Session.js +733 -0
- package/dist/services/session/SessionManager.js +255 -0
- package/dist/services/session/SessionStore.js +186 -0
- package/dist/services/session/index.js +3 -0
- package/dist/services/skills.js +34 -0
- package/dist/services/systemPrompt.js +150 -0
- package/dist/services/task/PushTokenStore.js +124 -0
- package/dist/services/task/TaskStore.js +143 -0
- package/dist/services/tools/calculator.js +27 -0
- package/dist/services/tools/edit.js +318 -0
- package/dist/services/tools/exec.js +119 -0
- package/dist/services/tools/fetch.js +155 -0
- package/dist/services/tools/file.js +315 -0
- package/dist/services/tools/index.js +48 -0
- package/dist/services/tools/keyboard.js +145 -0
- package/dist/services/tools/model.js +86 -0
- package/dist/services/tools/mouse.js +55 -0
- package/dist/services/tools/screenshot.js +19 -0
- package/dist/services/tools/search.js +53 -0
- package/dist/services/tools/skill.js +108 -0
- package/dist/services/tools/task.js +110 -0
- package/dist/services/tools/types.js +1 -0
- package/dist/services/tools/webFetch.js +34 -0
- package/dist/stores/authStore.js +178 -0
- package/dist/stores/index.js +6 -0
- package/dist/stores/memoryStore.js +191 -0
- package/dist/stores/persistStore.js +317 -0
- package/package.json +94 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export const SERVICE_NAME = 'myclaw-gateway';
|
|
9
|
+
export const SERVICE_DISPLAY_NAME = 'MyClaw Desktop Gateway Service';
|
|
10
|
+
// ─── 平台无关工具 ────────────────────────────────────────────
|
|
11
|
+
function getServiceScript() {
|
|
12
|
+
// pkg 环境
|
|
13
|
+
const execDir = path.dirname(process.execPath);
|
|
14
|
+
const pkgScript = path.join(execDir, 'gateway', 'dist', 'index.js');
|
|
15
|
+
if (fs.existsSync(pkgScript))
|
|
16
|
+
return pkgScript;
|
|
17
|
+
// 开发模式
|
|
18
|
+
const devScript = path.join(process.cwd(), 'dist', 'index.js');
|
|
19
|
+
if (fs.existsSync(devScript))
|
|
20
|
+
return devScript;
|
|
21
|
+
return pkgScript;
|
|
22
|
+
}
|
|
23
|
+
function getNodeExec() {
|
|
24
|
+
return process.execPath;
|
|
25
|
+
}
|
|
26
|
+
// ─── Windows 实现 ───────────────────────────────────────────
|
|
27
|
+
async function queryServiceWindows() {
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execAsync(`sc query ${SERVICE_NAME}`, { timeout: 5000 });
|
|
30
|
+
return { installed: true, running: stdout.includes('RUNNING') };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { installed: false, running: false };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function installWindows() {
|
|
37
|
+
const script = getServiceScript();
|
|
38
|
+
const workDir = path.dirname(script);
|
|
39
|
+
const nodeExe = getNodeExec();
|
|
40
|
+
const { installed } = await queryServiceWindows();
|
|
41
|
+
if (installed)
|
|
42
|
+
await stopService();
|
|
43
|
+
const nssmPath = path.join(path.dirname(process.execPath), 'nssm.exe');
|
|
44
|
+
const useNssm = fs.existsSync(nssmPath);
|
|
45
|
+
try {
|
|
46
|
+
if (useNssm) {
|
|
47
|
+
await execAsync(`"${nssmPath}" install ${SERVICE_NAME} "${nodeExe}" "${script}"`, { timeout: 15000, cwd: workDir });
|
|
48
|
+
await execAsync(`"${nssmPath}" set ${SERVICE_NAME} AppDirectory "${workDir}"`, { timeout: 10000 });
|
|
49
|
+
await execAsync(`"${nssmPath}" set ${SERVICE_NAME} DisplayName "${SERVICE_DISPLAY_NAME}"`, { timeout: 10000 });
|
|
50
|
+
await execAsync(`"${nssmPath}" set ${SERVICE_NAME} Start SERVICE_AUTO_START`, { timeout: 10000 });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await execAsync(`sc create ${SERVICE_NAME} binPath= "\\"${nodeExe}\\" \\"${script}\\"" DisplayName= "${SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
|
|
54
|
+
}
|
|
55
|
+
await startService();
|
|
56
|
+
return { success: true, message: '服务安装并启动成功' };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { success: false, message: `安装失败: ${err.message}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function uninstallWindows() {
|
|
63
|
+
try {
|
|
64
|
+
await stopService();
|
|
65
|
+
const nssmPath = path.join(path.dirname(process.execPath), 'nssm.exe');
|
|
66
|
+
if (fs.existsSync(nssmPath)) {
|
|
67
|
+
await execAsync(`"${nssmPath}" remove ${SERVICE_NAME} confirm`, { timeout: 10000 });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
await execAsync(`sc delete ${SERVICE_NAME}`, { timeout: 10000 });
|
|
71
|
+
}
|
|
72
|
+
return { success: true, message: '服务卸载成功' };
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return { success: false, message: `卸载失败: ${err.message}` };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function startServiceWindows() {
|
|
79
|
+
try {
|
|
80
|
+
await execAsync(`sc start ${SERVICE_NAME}`, { timeout: 10000 });
|
|
81
|
+
return { success: true, message: '服务启动成功' };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return { success: false, message: `启动失败: ${err.message}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function restartServiceWindows() {
|
|
88
|
+
const nssmPath = path.join(path.dirname(process.execPath), 'nssm.exe');
|
|
89
|
+
// 无 nssm 时需要用户手动更新
|
|
90
|
+
if (!fs.existsSync(nssmPath)) {
|
|
91
|
+
const info = await getServiceInfo();
|
|
92
|
+
if (info.installed) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
message: '检测到您尚未安装 nssm,无法自动重启服务。请手动执行以下步骤更新 Gateway:\n' +
|
|
96
|
+
'1. 在桌面上停止 Gateway 服务(系统托盘 → 右键 → 退出)\n' +
|
|
97
|
+
'2. 运行 `npm install -g @myclaw/gateway@latest`\n' +
|
|
98
|
+
'3. 重新启动 Gateway 服务\n\n' +
|
|
99
|
+
'如需自动重启功能,请从 https://nssm.cc/download 下载 nssm 并放置到 Gateway 同目录下',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// 服务未安装时,可直接更新
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await execAsync(`"${nssmPath}" restart ${SERVICE_NAME}`, { timeout: 15000 });
|
|
106
|
+
return { success: true, message: '服务重启成功' };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return { success: false, message: `重启失败: ${err.message}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function stopServiceWindows() {
|
|
113
|
+
try {
|
|
114
|
+
await execAsync(`sc stop ${SERVICE_NAME}`, { timeout: 10000 });
|
|
115
|
+
return { success: true, message: '服务停止成功' };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return { success: false, message: `停止失败: ${err.message}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ─── Linux / macOS 实现 ─────────────────────────────────────
|
|
122
|
+
async function queryServiceLinux() {
|
|
123
|
+
try {
|
|
124
|
+
const { stdout } = await execAsync(`systemctl is-active ${SERVICE_NAME}`, { timeout: 5000 });
|
|
125
|
+
const running = stdout.trim() === 'active';
|
|
126
|
+
return { installed: true, running };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const code = err.code;
|
|
130
|
+
if (code === 4)
|
|
131
|
+
return { installed: false, running: false };
|
|
132
|
+
const msg = err.stdout?.toString() || '';
|
|
133
|
+
if (msg.trim() === 'inactive' || msg.trim() === 'failed') {
|
|
134
|
+
return { installed: true, running: false };
|
|
135
|
+
}
|
|
136
|
+
return { installed: false, running: false };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function installLinux() {
|
|
140
|
+
const script = getServiceScript();
|
|
141
|
+
const workDir = path.dirname(script);
|
|
142
|
+
const nodeExe = getNodeExec();
|
|
143
|
+
try {
|
|
144
|
+
const { installed } = await queryServiceLinux();
|
|
145
|
+
if (installed)
|
|
146
|
+
await stopService();
|
|
147
|
+
const unitContent = `[Unit]
|
|
148
|
+
Description=${SERVICE_DISPLAY_NAME}
|
|
149
|
+
After=network.target
|
|
150
|
+
|
|
151
|
+
[Service]
|
|
152
|
+
Type=simple
|
|
153
|
+
User=${process.env.USER || 'root'}
|
|
154
|
+
WorkingDirectory=${workDir}
|
|
155
|
+
ExecStart=${nodeExe} ${script}
|
|
156
|
+
Restart=always
|
|
157
|
+
RestartSec=5
|
|
158
|
+
Environment=NODE_ENV=production
|
|
159
|
+
|
|
160
|
+
[Install]
|
|
161
|
+
WantedBy=multi-user.target
|
|
162
|
+
`;
|
|
163
|
+
await fs.promises.writeFile('/tmp/myclaw-gateway.service', unitContent, 'utf8');
|
|
164
|
+
await execAsync('cp /tmp/myclaw-gateway.service /etc/systemd/system/myclaw-gateway.service', { timeout: 10000 });
|
|
165
|
+
await execAsync('systemctl daemon-reload', { timeout: 10000 });
|
|
166
|
+
await execAsync('systemctl enable myclaw-gateway', { timeout: 10000 });
|
|
167
|
+
await execAsync('systemctl start myclaw-gateway', { timeout: 10000 });
|
|
168
|
+
return { success: true, message: '服务安装并启动成功' };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
return { success: false, message: `安装失败: ${err.message}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function uninstallLinux() {
|
|
175
|
+
try {
|
|
176
|
+
await execAsync('systemctl stop myclaw-gateway', { timeout: 10000 });
|
|
177
|
+
await execAsync('systemctl disable myclaw-gateway', { timeout: 10000 });
|
|
178
|
+
await execAsync('rm -f /etc/systemd/system/myclaw-gateway.service', { timeout: 10000 });
|
|
179
|
+
await execAsync('systemctl daemon-reload', { timeout: 10000 });
|
|
180
|
+
return { success: true, message: '服务卸载成功' };
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return { success: false, message: `卸载失败: ${err.message}` };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function startServiceLinux() {
|
|
187
|
+
try {
|
|
188
|
+
await execAsync(`systemctl start ${SERVICE_NAME}`, { timeout: 10000 });
|
|
189
|
+
return { success: true, message: '服务启动成功' };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
return { success: false, message: `启动失败: ${err.message}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function restartServiceLinux() {
|
|
196
|
+
try {
|
|
197
|
+
await execAsync(`systemctl restart ${SERVICE_NAME}`, { timeout: 15000 });
|
|
198
|
+
return { success: true, message: '服务重启成功' };
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return { success: false, message: `重启失败: ${err.message}` };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function stopServiceLinux() {
|
|
205
|
+
try {
|
|
206
|
+
await execAsync(`systemctl stop ${SERVICE_NAME}`, { timeout: 10000 });
|
|
207
|
+
return { success: true, message: '服务停止成功' };
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
return { success: false, message: `停止失败: ${err.message}` };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── 平台分发 ───────────────────────────────────────────────
|
|
214
|
+
export async function getServiceInfo() {
|
|
215
|
+
const platform = process.platform;
|
|
216
|
+
let installed = false;
|
|
217
|
+
let running = false;
|
|
218
|
+
if (platform === 'win32') {
|
|
219
|
+
const r = await queryServiceWindows();
|
|
220
|
+
installed = r.installed;
|
|
221
|
+
running = r.running;
|
|
222
|
+
}
|
|
223
|
+
else if (platform === 'linux' || platform === 'darwin') {
|
|
224
|
+
const r = await queryServiceLinux();
|
|
225
|
+
installed = r.installed;
|
|
226
|
+
running = r.running;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
installed,
|
|
230
|
+
running,
|
|
231
|
+
displayName: SERVICE_DISPLAY_NAME,
|
|
232
|
+
canManage: platform === 'win32' || platform === 'linux',
|
|
233
|
+
platform,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export async function installService() {
|
|
237
|
+
const platform = process.platform;
|
|
238
|
+
if (platform === 'win32')
|
|
239
|
+
return installWindows();
|
|
240
|
+
if (platform === 'linux')
|
|
241
|
+
return installLinux();
|
|
242
|
+
return { success: false, message: `当前平台 ${platform} 不支持自动安装服务` };
|
|
243
|
+
}
|
|
244
|
+
export async function uninstallService() {
|
|
245
|
+
const platform = process.platform;
|
|
246
|
+
if (platform === 'win32')
|
|
247
|
+
return uninstallWindows();
|
|
248
|
+
if (platform === 'linux')
|
|
249
|
+
return uninstallLinux();
|
|
250
|
+
return { success: false, message: `当前平台 ${platform} 不支持自动卸载服务` };
|
|
251
|
+
}
|
|
252
|
+
export async function startService() {
|
|
253
|
+
const platform = process.platform;
|
|
254
|
+
if (platform === 'win32')
|
|
255
|
+
return startServiceWindows();
|
|
256
|
+
if (platform === 'linux')
|
|
257
|
+
return startServiceLinux();
|
|
258
|
+
return { success: false, message: `当前平台 ${platform} 不支持自动启动服务` };
|
|
259
|
+
}
|
|
260
|
+
export async function stopService() {
|
|
261
|
+
const platform = process.platform;
|
|
262
|
+
if (platform === 'win32')
|
|
263
|
+
return stopServiceWindows();
|
|
264
|
+
if (platform === 'linux')
|
|
265
|
+
return stopServiceLinux();
|
|
266
|
+
return { success: false, message: `当前平台 ${platform} 不支持自动停止服务` };
|
|
267
|
+
}
|
|
268
|
+
export async function restartService() {
|
|
269
|
+
const platform = process.platform;
|
|
270
|
+
if (platform === 'win32')
|
|
271
|
+
return restartServiceWindows();
|
|
272
|
+
if (platform === 'linux')
|
|
273
|
+
return restartServiceLinux();
|
|
274
|
+
return { success: false, message: `当前平台 ${platform} 不支持自动重启服务` };
|
|
275
|
+
}
|
|
276
|
+
// ─── 版本更新 ─────────────────────────────────────────
|
|
277
|
+
function getPackageVersion() {
|
|
278
|
+
try {
|
|
279
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
280
|
+
const pkgPath = path.join(scriptDir, '..', '..', 'package.json');
|
|
281
|
+
if (fs.existsSync(pkgPath)) {
|
|
282
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
283
|
+
return pkg.version || '0.0.0';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
return '0.0.0';
|
|
290
|
+
}
|
|
291
|
+
function compareVersions(a, b) {
|
|
292
|
+
const pa = (a || '0.0.0').split('.').map(Number);
|
|
293
|
+
const pb = (b || '0.0.0').split('.').map(Number);
|
|
294
|
+
for (let i = 0; i < 3; i++) {
|
|
295
|
+
const va = pa[i] || 0;
|
|
296
|
+
const vb = pb[i] || 0;
|
|
297
|
+
if (va < vb)
|
|
298
|
+
return -1;
|
|
299
|
+
if (va > vb)
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
export async function checkForUpdates() {
|
|
305
|
+
try {
|
|
306
|
+
const currentVersion = getPackageVersion();
|
|
307
|
+
const response = await axios.get('https://registry.npmjs.org/@myclaw/gateway/latest', { timeout: 10000 });
|
|
308
|
+
const latestVersion = response.data.version;
|
|
309
|
+
if (!latestVersion) {
|
|
310
|
+
return { hasUpdate: false, currentVersion, latestVersion: currentVersion, error: '无法获取最新版本' };
|
|
311
|
+
}
|
|
312
|
+
const hasUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
|
313
|
+
return { hasUpdate, currentVersion, latestVersion };
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
return { hasUpdate: false, currentVersion: '0.0.0', latestVersion: '0.0.0', error: err.message };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
export async function updateService() {
|
|
320
|
+
try {
|
|
321
|
+
const check = await checkForUpdates();
|
|
322
|
+
if (check.error) {
|
|
323
|
+
return { success: false, message: `检查更新失败: ${check.error}` };
|
|
324
|
+
}
|
|
325
|
+
if (!check.hasUpdate) {
|
|
326
|
+
return { success: true, message: `已是最新版本 ${check.currentVersion}` };
|
|
327
|
+
}
|
|
328
|
+
console.log(`发现新版本 ${check.latestVersion},正在更新...`);
|
|
329
|
+
const { stdout, stderr } = await execAsync('npm install -g @myclaw/gateway@latest', { timeout: 120000 });
|
|
330
|
+
if (stdout)
|
|
331
|
+
console.log(stdout);
|
|
332
|
+
if (stderr)
|
|
333
|
+
console.error(stderr);
|
|
334
|
+
console.log('✅ npm 更新完成');
|
|
335
|
+
const info = await getServiceInfo();
|
|
336
|
+
if (info.installed) {
|
|
337
|
+
console.log('正在重启服务...');
|
|
338
|
+
const r = await restartService();
|
|
339
|
+
if (!r.success) {
|
|
340
|
+
return { success: false, message: `更新成功但服务重启失败: ${r.message}` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { success: true, message: `更新成功: ${check.currentVersion} → ${check.latestVersion}` };
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
return { success: false, message: `更新失败: ${err.message}` };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway 任务调度服务
|
|
3
|
+
* 基于 SQLite 数据库的任务调度
|
|
4
|
+
* 每分钟检查待执行任务并发送通知
|
|
5
|
+
*/
|
|
6
|
+
import { taskStore } from './task/TaskStore';
|
|
7
|
+
import { webSocketService } from './WebSocketService';
|
|
8
|
+
import { sessionManager } from './session';
|
|
9
|
+
import { getLogger, getUTCTimeKey, formatUTCForLog, holidayService } from '@pocketclaw/shared';
|
|
10
|
+
import { tasksService } from './dataService';
|
|
11
|
+
const logger = getLogger('TaskSchedulerService');
|
|
12
|
+
const EXPIRED_THRESHOLD = 60 * 60 * 1000; // 1小时
|
|
13
|
+
const CHECK_INTERVAL = 60 * 1000; // 1分钟
|
|
14
|
+
class TaskSchedulerService {
|
|
15
|
+
intervalId = null;
|
|
16
|
+
initialized = false;
|
|
17
|
+
constructor() {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 初始化 - 启动调度器
|
|
21
|
+
*/
|
|
22
|
+
start() {
|
|
23
|
+
if (this.initialized) {
|
|
24
|
+
logger.warn('Already initialized');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
logger.info('Starting Task Scheduler Service...');
|
|
28
|
+
// 预加载节假日数据
|
|
29
|
+
this.preloadHolidayData();
|
|
30
|
+
// 立即执行一次检查
|
|
31
|
+
this.processCurrentMinuteTasks();
|
|
32
|
+
this.processCurrentMinuteExpiredTasks();
|
|
33
|
+
// 每分钟检查任务
|
|
34
|
+
this.intervalId = setInterval(() => {
|
|
35
|
+
this.processCurrentMinuteTasks();
|
|
36
|
+
this.processCurrentMinuteExpiredTasks();
|
|
37
|
+
}, CHECK_INTERVAL);
|
|
38
|
+
this.initialized = true;
|
|
39
|
+
logger.info('Task Scheduler Service started');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 停止调度器
|
|
43
|
+
*/
|
|
44
|
+
stop() {
|
|
45
|
+
if (this.intervalId) {
|
|
46
|
+
clearInterval(this.intervalId);
|
|
47
|
+
this.intervalId = null;
|
|
48
|
+
this.initialized = false;
|
|
49
|
+
logger.info('Task Scheduler Service stopped');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 预加载节假日数据
|
|
54
|
+
*/
|
|
55
|
+
async preloadHolidayData() {
|
|
56
|
+
try {
|
|
57
|
+
await holidayService.preloadYears([new Date().getFullYear()]);
|
|
58
|
+
logger.info('Holiday data preloaded');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
logger.error('Failed to preload holiday data:', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async processCurrentMinuteExpiredTasks() {
|
|
65
|
+
try {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const nowBeforeHour = now - EXPIRED_THRESHOLD;
|
|
68
|
+
const response = await tasksService.list();
|
|
69
|
+
if (!response.success) {
|
|
70
|
+
logger.error('获取任务失败');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// 获取过期待执行任务
|
|
74
|
+
const tasks = response.data.filter(x => x.scheduledAt < nowBeforeHour && x.status !== 'completed' && x.status !== 'expired' && x.status !== 'error');
|
|
75
|
+
if (tasks.length > 0) {
|
|
76
|
+
for (let task of tasks) {
|
|
77
|
+
await tasksService.updateStatus(task.id, 'expired');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logger.error(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 处理当前分钟的任务
|
|
87
|
+
*/
|
|
88
|
+
async processCurrentMinuteTasks() {
|
|
89
|
+
try {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const nowBeforeHour = now - EXPIRED_THRESHOLD;
|
|
92
|
+
const timeKey = getUTCTimeKey(new Date());
|
|
93
|
+
const response = await tasksService.list();
|
|
94
|
+
if (!response.success) {
|
|
95
|
+
logger.error('获取任务失败');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 获取待执行任务
|
|
99
|
+
const tasks = response.data.filter(x => x.scheduledAt >= nowBeforeHour && x.scheduledAt <= now && x.status === 'pending');
|
|
100
|
+
if (tasks.length > 0) {
|
|
101
|
+
logger.info(`Found ${tasks.length} tasks to execute at ${timeKey}`);
|
|
102
|
+
for (const task of tasks) {
|
|
103
|
+
await this.executeTask(task);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error('Error processing tasks:', error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 执行任务
|
|
113
|
+
*/
|
|
114
|
+
async executeTask(task) {
|
|
115
|
+
try {
|
|
116
|
+
// 更新任务状态为 notifying
|
|
117
|
+
taskStore.updateStatus(task.id, 'notifying');
|
|
118
|
+
logger.info(`Executing task: ${task.title}`, {
|
|
119
|
+
taskId: task.id,
|
|
120
|
+
scheduledAt: formatUTCForLog(task.scheduledAt),
|
|
121
|
+
});
|
|
122
|
+
// 优先通过 WebSocket 通知 Desktop 在线执行任务
|
|
123
|
+
const wsSent = webSocketService.sendTaskTrigger({
|
|
124
|
+
id: task.id,
|
|
125
|
+
title: task.title,
|
|
126
|
+
description: task.description,
|
|
127
|
+
taskType: task.taskType,
|
|
128
|
+
scheduledAt: task.scheduledAt,
|
|
129
|
+
userId: task.userId,
|
|
130
|
+
});
|
|
131
|
+
if (wsSent) {
|
|
132
|
+
// Desktop 在线,由 Desktop 端播放铃声并执行任务
|
|
133
|
+
logger.info(`Task ${task.title}: WebSocket notification sent to Desktop`);
|
|
134
|
+
await tasksService.updateStatus(task.id, 'notifying');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Desktop 不在线,Gateway 本地直接执行任务
|
|
138
|
+
logger.info(`Task ${task.title}: No WebSocket, executing locally`);
|
|
139
|
+
await tasksService.updateStatus(task.id, 'running');
|
|
140
|
+
await this.executeTaskLocally(task);
|
|
141
|
+
await tasksService.updateStatus(task.id, 'completed');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.error(`Failed to execute task ${task.id}:`, error);
|
|
146
|
+
taskStore.updateStatus(task.id, 'failed');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Gateway 本地执行任务(Desktop 不在线时)
|
|
151
|
+
* 选取 isCurrent=true 的 Session,通过 streamChat 发送给大模型
|
|
152
|
+
*/
|
|
153
|
+
async executeTaskLocally(task) {
|
|
154
|
+
try {
|
|
155
|
+
const currentSession = sessionManager.getCurrentSession();
|
|
156
|
+
if (!currentSession) {
|
|
157
|
+
logger.warn(`No current session found, cannot execute task: ${task.title}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const taskContent = task.description || task.title;
|
|
161
|
+
const userMessageId = `task-user-${task.id}`;
|
|
162
|
+
const assistantMessageId = `task-assistant-${task.id}`;
|
|
163
|
+
logger.info(`Executing task locally in session ${currentSession.id}:${currentSession.title}: ${task.title}`);
|
|
164
|
+
// res=null 表示本地执行,不需要 SSE 响应
|
|
165
|
+
await currentSession.streamChat(taskContent, [], null, userMessageId, assistantMessageId);
|
|
166
|
+
logger.info(`Local task execution completed: ${task.title}`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
logger.error(`Local task execution failed for ${task.title}:`, error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 获取调度器状态
|
|
174
|
+
*/
|
|
175
|
+
getStatus() {
|
|
176
|
+
const pendingCount = taskStore.findByStatus('pending').length;
|
|
177
|
+
const notifyingCount = taskStore.findByStatus('notifying').length;
|
|
178
|
+
const completedCount = taskStore.findByStatus('completed').length;
|
|
179
|
+
const failedCount = taskStore.findByStatus('failed').length;
|
|
180
|
+
const expiredCount = taskStore.findByStatus('expired').length;
|
|
181
|
+
return {
|
|
182
|
+
initialized: this.initialized,
|
|
183
|
+
running: this.intervalId !== null,
|
|
184
|
+
checkInterval: CHECK_INTERVAL,
|
|
185
|
+
totalTasks: taskStore.count(),
|
|
186
|
+
pendingTasks: pendingCount,
|
|
187
|
+
notifyingTasks: notifyingCount,
|
|
188
|
+
completedTasks: completedCount,
|
|
189
|
+
failedTasks: failedCount,
|
|
190
|
+
expiredTasks: expiredCount,
|
|
191
|
+
nextCheck: formatUTCForLog(Date.now() + CHECK_INTERVAL),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export const taskSchedulerService = new TaskSchedulerService();
|