ccjk 9.6.1 → 9.8.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/dist/chunks/boost.mjs +246 -7
- package/dist/chunks/ccjk-mcp.mjs +1 -1
- package/dist/chunks/ccr.mjs +25 -28
- package/dist/chunks/check-updates.mjs +4 -3
- package/dist/chunks/claude-code-config-manager.mjs +1 -1
- package/dist/chunks/claude-code-incremental-manager.mjs +1 -1
- package/dist/chunks/claude-config.mjs +1 -1
- package/dist/chunks/codex-config-switch.mjs +3 -4
- package/dist/chunks/codex-provider-manager.mjs +1 -2
- package/dist/chunks/codex.mjs +204 -3
- package/dist/chunks/config-switch.mjs +2 -3
- package/dist/chunks/config.mjs +1 -1
- package/dist/chunks/doctor.mjs +1 -1
- package/dist/chunks/features.mjs +24 -15
- package/dist/chunks/hook-installer.mjs +44 -0
- package/dist/chunks/index3.mjs +32 -32
- package/dist/chunks/init.mjs +129 -87
- package/dist/chunks/installer2.mjs +1 -1
- package/dist/chunks/interview.mjs +1 -1
- package/dist/chunks/mcp.mjs +1058 -17
- package/dist/chunks/menu.mjs +140 -56
- package/dist/chunks/package.mjs +2 -210
- package/dist/chunks/platform.mjs +1 -1
- package/dist/chunks/quick-setup.mjs +35 -18
- package/dist/chunks/simple-config.mjs +1 -1
- package/dist/{shared/ccjk.q1koQxEE.mjs → chunks/smart-defaults.mjs} +77 -79
- package/dist/chunks/status.mjs +208 -101
- package/dist/chunks/thinking.mjs +1 -1
- package/dist/chunks/uninstall.mjs +6 -4
- package/dist/chunks/update.mjs +4 -7
- package/dist/chunks/version-checker.mjs +1 -1
- package/dist/cli.mjs +4 -80
- package/dist/index.d.mts +17 -1482
- package/dist/index.d.ts +17 -1482
- package/dist/index.mjs +12 -4191
- package/dist/shared/{ccjk.CSkyCZIM.mjs → ccjk.Bndhan7G.mjs} +4 -242
- package/dist/shared/ccjk.CeE8RLG2.mjs +62 -0
- package/dist/shared/ccjk.DKojSRzw.mjs +266 -0
- package/dist/shared/{ccjk.CItD1fpl.mjs → ccjk.DvIrK0wz.mjs} +1 -1
- package/dist/shared/ccjk.LsPZ2PYo.mjs +1048 -0
- package/package.json +1 -1
- package/dist/chunks/api-adapter.mjs +0 -180
- package/dist/chunks/cli.mjs +0 -2227
- package/dist/chunks/context-menu.mjs +0 -913
- package/dist/chunks/hooks-sync.mjs +0 -1627
- package/dist/chunks/mcp-market.mjs +0 -1077
- package/dist/chunks/mcp-server.mjs +0 -776
- package/dist/chunks/project-detector.mjs +0 -131
- package/dist/chunks/provider-registry.mjs +0 -92
- package/dist/chunks/setup-wizard.mjs +0 -362
- package/dist/chunks/tools.mjs +0 -143
- package/dist/chunks/workflows2.mjs +0 -232
- package/dist/shared/ccjk.C0pb50xH.mjs +0 -347
- package/dist/shared/ccjk.ChMkBmdL.mjs +0 -490
- package/dist/shared/ccjk.CtSfXUSh.mjs +0 -209
- package/dist/shared/ccjk.xfAjmbJp.mjs +0 -75
package/dist/chunks/cli.mjs
DELETED
|
@@ -1,2227 +0,0 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync } from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
import { join } from 'pathe';
|
|
7
|
-
import process__default from 'node:process';
|
|
8
|
-
import { nanoid } from 'nanoid';
|
|
9
|
-
import packageJson from './package.mjs';
|
|
10
|
-
import { exec } from 'tinyexec';
|
|
11
|
-
import { EventEmitter } from 'node:events';
|
|
12
|
-
|
|
13
|
-
const CLOUD_API_BASE = "https://api.claudehome.cn/api/control";
|
|
14
|
-
class CloudClient {
|
|
15
|
-
config;
|
|
16
|
-
heartbeatTimer;
|
|
17
|
-
currentTasks = /* @__PURE__ */ new Set();
|
|
18
|
-
deviceInfo;
|
|
19
|
-
constructor(config) {
|
|
20
|
-
this.config = {
|
|
21
|
-
heartbeatInterval: 3e4,
|
|
22
|
-
debug: false,
|
|
23
|
-
...config
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Get API base URL
|
|
28
|
-
*/
|
|
29
|
-
getApiBase() {
|
|
30
|
-
return this.config.apiUrl || CLOUD_API_BASE;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get request headers
|
|
34
|
-
*/
|
|
35
|
-
getHeaders() {
|
|
36
|
-
return {
|
|
37
|
-
"Content-Type": "application/json",
|
|
38
|
-
"X-Device-Token": this.config.deviceToken
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Log debug message
|
|
43
|
-
*/
|
|
44
|
-
debugLog(message) {
|
|
45
|
-
if (this.config.debug) {
|
|
46
|
-
console.log(`[CloudClient] ${message}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Register device to cloud
|
|
51
|
-
*/
|
|
52
|
-
async register(info) {
|
|
53
|
-
try {
|
|
54
|
-
const deviceInfo = {
|
|
55
|
-
name: info?.name || `CCJK Device (${os.hostname()})`,
|
|
56
|
-
platform: os.platform(),
|
|
57
|
-
hostname: os.hostname(),
|
|
58
|
-
version: info?.version || packageJson.version
|
|
59
|
-
};
|
|
60
|
-
this.debugLog(`Registering device: ${deviceInfo.name}`);
|
|
61
|
-
const response = await fetch(`${this.getApiBase()}/devices/register`, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: this.getHeaders(),
|
|
64
|
-
body: JSON.stringify(deviceInfo)
|
|
65
|
-
});
|
|
66
|
-
const result = await response.json();
|
|
67
|
-
if (result.success && result.data) {
|
|
68
|
-
this.deviceInfo = result.data;
|
|
69
|
-
this.debugLog(`Device registered: ${result.data.device.id}`);
|
|
70
|
-
}
|
|
71
|
-
return result;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
this.debugLog(`Registration failed: ${error}`);
|
|
74
|
-
return {
|
|
75
|
-
success: false,
|
|
76
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Send heartbeat to cloud and receive pending tasks
|
|
82
|
-
*/
|
|
83
|
-
async heartbeat(status = "online") {
|
|
84
|
-
try {
|
|
85
|
-
const request = {
|
|
86
|
-
status,
|
|
87
|
-
currentTasks: Array.from(this.currentTasks),
|
|
88
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
89
|
-
};
|
|
90
|
-
this.debugLog(`Sending heartbeat: ${status}, tasks: ${request.currentTasks?.length || 0}`);
|
|
91
|
-
const response = await fetch(`${this.getApiBase()}/devices/heartbeat`, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: this.getHeaders(),
|
|
94
|
-
body: JSON.stringify(request)
|
|
95
|
-
});
|
|
96
|
-
const result = await response.json();
|
|
97
|
-
if (result.success) {
|
|
98
|
-
this.debugLog(`Heartbeat OK, pending tasks: ${result.data?.pendingTasks?.length || 0}`);
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
this.debugLog(`Heartbeat failed: ${error}`);
|
|
103
|
-
return {
|
|
104
|
-
success: false,
|
|
105
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Pull pending tasks from cloud
|
|
111
|
-
*/
|
|
112
|
-
async pullTasks() {
|
|
113
|
-
try {
|
|
114
|
-
this.debugLog("Pulling pending tasks...");
|
|
115
|
-
const response = await fetch(`${this.getApiBase()}/devices/pending`, {
|
|
116
|
-
method: "GET",
|
|
117
|
-
headers: this.getHeaders()
|
|
118
|
-
});
|
|
119
|
-
const result = await response.json();
|
|
120
|
-
if (result.success && result.data?.commands) {
|
|
121
|
-
this.debugLog(`Pulled ${result.data.commands.length} tasks`);
|
|
122
|
-
return result.data.commands;
|
|
123
|
-
}
|
|
124
|
-
return [];
|
|
125
|
-
} catch (error) {
|
|
126
|
-
this.debugLog(`Pull tasks failed: ${error}`);
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Report command execution result to cloud
|
|
132
|
-
*/
|
|
133
|
-
async reportResult(commandId, result) {
|
|
134
|
-
try {
|
|
135
|
-
this.debugLog(`Reporting result for command ${commandId}: ${result.success ? "success" : "failed"}`);
|
|
136
|
-
const response = await fetch(`${this.getApiBase()}/commands/${commandId}/result`, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
headers: this.getHeaders(),
|
|
139
|
-
body: JSON.stringify(result)
|
|
140
|
-
});
|
|
141
|
-
const fetchResult = await response.json();
|
|
142
|
-
if (fetchResult.success) {
|
|
143
|
-
this.currentTasks.delete(commandId);
|
|
144
|
-
}
|
|
145
|
-
return fetchResult;
|
|
146
|
-
} catch (error) {
|
|
147
|
-
this.debugLog(`Report result failed: ${error}`);
|
|
148
|
-
return {
|
|
149
|
-
success: false,
|
|
150
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Add task to current tasks list
|
|
156
|
-
*/
|
|
157
|
-
addTask(commandId) {
|
|
158
|
-
this.currentTasks.add(commandId);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Remove task from current tasks list
|
|
162
|
-
*/
|
|
163
|
-
removeTask(commandId) {
|
|
164
|
-
this.currentTasks.delete(commandId);
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Start automatic heartbeat
|
|
168
|
-
*/
|
|
169
|
-
startHeartbeat(onTasks) {
|
|
170
|
-
this.stopHeartbeat();
|
|
171
|
-
this.debugLog(`Starting heartbeat (interval: ${this.config.heartbeatInterval}ms)`);
|
|
172
|
-
this.heartbeatTimer = setInterval(async () => {
|
|
173
|
-
try {
|
|
174
|
-
const status = this.currentTasks.size > 0 ? "busy" : "online";
|
|
175
|
-
const response = await this.heartbeat(status);
|
|
176
|
-
if (response.success && response.data?.pendingTasks && onTasks) {
|
|
177
|
-
const tasks = response.data.pendingTasks;
|
|
178
|
-
if (tasks.length > 0) {
|
|
179
|
-
onTasks(tasks);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
} catch (error) {
|
|
183
|
-
this.debugLog(`Heartbeat error: ${error}`);
|
|
184
|
-
}
|
|
185
|
-
}, this.config.heartbeatInterval);
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Stop automatic heartbeat
|
|
189
|
-
*/
|
|
190
|
-
stopHeartbeat() {
|
|
191
|
-
if (this.heartbeatTimer) {
|
|
192
|
-
clearInterval(this.heartbeatTimer);
|
|
193
|
-
this.heartbeatTimer = void 0;
|
|
194
|
-
this.debugLog("Heartbeat stopped");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Send offline status before disconnecting
|
|
199
|
-
*/
|
|
200
|
-
async goOffline() {
|
|
201
|
-
await this.heartbeat("offline");
|
|
202
|
-
this.stopHeartbeat();
|
|
203
|
-
this.currentTasks.clear();
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Get registered device info
|
|
207
|
-
*/
|
|
208
|
-
getDeviceInfo() {
|
|
209
|
-
return this.deviceInfo;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Get current active tasks count
|
|
213
|
-
*/
|
|
214
|
-
getActiveTasksCount() {
|
|
215
|
-
return this.currentTasks.size;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Check if client is connected (has active heartbeat)
|
|
219
|
-
*/
|
|
220
|
-
isConnected() {
|
|
221
|
-
return this.heartbeatTimer !== void 0;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
class EmailChecker {
|
|
226
|
-
imap;
|
|
227
|
-
config;
|
|
228
|
-
connected = false;
|
|
229
|
-
constructor(config) {
|
|
230
|
-
this.config = {
|
|
231
|
-
imapHost: "imap.gmail.com",
|
|
232
|
-
imapPort: 993,
|
|
233
|
-
tls: true,
|
|
234
|
-
...config
|
|
235
|
-
};
|
|
236
|
-
try {
|
|
237
|
-
const ImapConstructor = require("imap");
|
|
238
|
-
this.imap = new ImapConstructor({
|
|
239
|
-
user: this.config.email,
|
|
240
|
-
password: this.config.password,
|
|
241
|
-
host: this.config.imapHost,
|
|
242
|
-
port: this.config.imapPort,
|
|
243
|
-
tls: this.config.tls,
|
|
244
|
-
tlsOptions: { rejectUnauthorized: false }
|
|
245
|
-
});
|
|
246
|
-
} catch (_error) {
|
|
247
|
-
throw new Error("imap package is not installed. Install it with: pnpm add imap @types/imap");
|
|
248
|
-
}
|
|
249
|
-
this.setupEventHandlers();
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Setup IMAP event handlers
|
|
253
|
-
*/
|
|
254
|
-
setupEventHandlers() {
|
|
255
|
-
this.imap.once("ready", () => {
|
|
256
|
-
this.connected = true;
|
|
257
|
-
});
|
|
258
|
-
this.imap.once("error", (err) => {
|
|
259
|
-
console.error("IMAP connection error:", err);
|
|
260
|
-
this.connected = false;
|
|
261
|
-
});
|
|
262
|
-
this.imap.once("end", () => {
|
|
263
|
-
this.connected = false;
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Connect to IMAP server
|
|
268
|
-
*/
|
|
269
|
-
async connect() {
|
|
270
|
-
if (this.connected) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
return new Promise((resolve, reject) => {
|
|
274
|
-
this.imap.once("ready", () => {
|
|
275
|
-
this.connected = true;
|
|
276
|
-
resolve();
|
|
277
|
-
});
|
|
278
|
-
this.imap.once("error", (err) => {
|
|
279
|
-
reject(err);
|
|
280
|
-
});
|
|
281
|
-
this.imap.connect();
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Disconnect from IMAP server
|
|
286
|
-
*/
|
|
287
|
-
disconnect() {
|
|
288
|
-
if (this.connected) {
|
|
289
|
-
this.imap.end();
|
|
290
|
-
this.connected = false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Fetch new emails with [CCJK] tag in subject
|
|
295
|
-
*/
|
|
296
|
-
async fetchNew() {
|
|
297
|
-
try {
|
|
298
|
-
await this.connect();
|
|
299
|
-
return new Promise((resolve, reject) => {
|
|
300
|
-
this.imap.openBox("INBOX", false, (err, _box) => {
|
|
301
|
-
if (err) {
|
|
302
|
-
reject(err);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
this.imap.search(["UNSEEN", ["SUBJECT", "[CCJK]"]], (searchErr, results) => {
|
|
306
|
-
if (searchErr) {
|
|
307
|
-
reject(searchErr);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (!results || results.length === 0) {
|
|
311
|
-
resolve([]);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const emails = [];
|
|
315
|
-
const fetch = this.imap.fetch(results, {
|
|
316
|
-
bodies: "",
|
|
317
|
-
markSeen: true
|
|
318
|
-
// Mark as read after fetching
|
|
319
|
-
});
|
|
320
|
-
fetch.on("message", (msg, _seqno) => {
|
|
321
|
-
msg.on("body", (stream) => {
|
|
322
|
-
let simpleParserFn;
|
|
323
|
-
try {
|
|
324
|
-
simpleParserFn = require("mailparser").simpleParser;
|
|
325
|
-
} catch {
|
|
326
|
-
console.error("mailparser package is not installed. Install it with: pnpm add mailparser @types/mailparser");
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
simpleParserFn(stream, (parseErr, parsed) => {
|
|
330
|
-
if (parseErr) {
|
|
331
|
-
console.error("Email parse error:", parseErr);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
emails.push({
|
|
335
|
-
id: parsed.messageId,
|
|
336
|
-
from: parsed.from?.text || "",
|
|
337
|
-
subject: parsed.subject || "",
|
|
338
|
-
body: parsed.text || "",
|
|
339
|
-
date: parsed.date || /* @__PURE__ */ new Date(),
|
|
340
|
-
raw: parsed
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
msg.once("attributes", (_attrs) => {
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
fetch.once("error", (fetchErr) => {
|
|
348
|
-
reject(fetchErr);
|
|
349
|
-
});
|
|
350
|
-
fetch.once("end", () => {
|
|
351
|
-
resolve(emails);
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
} catch (error) {
|
|
357
|
-
console.error("Failed to fetch emails:", error);
|
|
358
|
-
throw error;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Parse command from email
|
|
363
|
-
*/
|
|
364
|
-
parseCommand(email) {
|
|
365
|
-
let command = email.body.trim();
|
|
366
|
-
const lines = command.split("\n");
|
|
367
|
-
const commandLines = [];
|
|
368
|
-
for (const line of lines) {
|
|
369
|
-
const trimmed = line.trim();
|
|
370
|
-
if (trimmed.startsWith("--") || trimmed.startsWith("___")) {
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
if (trimmed) {
|
|
374
|
-
commandLines.push(trimmed);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
command = commandLines.join("\n").trim();
|
|
378
|
-
return command;
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Extract sender email address
|
|
382
|
-
*/
|
|
383
|
-
extractSenderEmail(from) {
|
|
384
|
-
const match = from.match(/<(.+?)>/);
|
|
385
|
-
if (match) {
|
|
386
|
-
return match[1].toLowerCase().trim();
|
|
387
|
-
}
|
|
388
|
-
return from.toLowerCase().trim();
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const PRESET_TEMPLATES = {
|
|
393
|
-
tpl_deploy: {
|
|
394
|
-
id: "tpl_deploy",
|
|
395
|
-
name: "\u90E8\u7F72\u63A7\u5236",
|
|
396
|
-
description: "\u90E8\u7F72\u76F8\u5173\u64CD\u4F5C",
|
|
397
|
-
category: "deploy",
|
|
398
|
-
actions: [
|
|
399
|
-
{
|
|
400
|
-
id: "deploy",
|
|
401
|
-
label: "\u{1F680} \u90E8\u7F72",
|
|
402
|
-
command: "npm run deploy",
|
|
403
|
-
confirm: true,
|
|
404
|
-
style: "primary" /* Primary */
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
id: "restart",
|
|
408
|
-
label: "\u{1F504} \u91CD\u542F\u670D\u52A1",
|
|
409
|
-
command: "pm2 restart all",
|
|
410
|
-
confirm: true,
|
|
411
|
-
style: "default" /* Default */
|
|
412
|
-
},
|
|
413
|
-
{
|
|
414
|
-
id: "status",
|
|
415
|
-
label: "\u{1F4CA} \u670D\u52A1\u72B6\u6001",
|
|
416
|
-
command: "pm2 status",
|
|
417
|
-
confirm: false,
|
|
418
|
-
style: "default" /* Default */
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
id: "logs",
|
|
422
|
-
label: "\u{1F4CB} \u67E5\u770B\u65E5\u5FD7",
|
|
423
|
-
command: "pm2 logs --lines 50 --nostream",
|
|
424
|
-
confirm: false,
|
|
425
|
-
style: "default" /* Default */
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
id: "stop",
|
|
429
|
-
label: "\u23F9\uFE0F \u505C\u6B62\u670D\u52A1",
|
|
430
|
-
command: "pm2 stop all",
|
|
431
|
-
confirm: true,
|
|
432
|
-
style: "danger" /* Danger */
|
|
433
|
-
}
|
|
434
|
-
]
|
|
435
|
-
},
|
|
436
|
-
tpl_database: {
|
|
437
|
-
id: "tpl_database",
|
|
438
|
-
name: "\u6570\u636E\u5E93\u63A7\u5236",
|
|
439
|
-
description: "\u6570\u636E\u5E93\u76F8\u5173\u64CD\u4F5C",
|
|
440
|
-
category: "database",
|
|
441
|
-
actions: [
|
|
442
|
-
{
|
|
443
|
-
id: "migrate",
|
|
444
|
-
label: "\u{1F504} \u8FD0\u884C\u8FC1\u79FB",
|
|
445
|
-
command: "npm run db:migrate",
|
|
446
|
-
confirm: true,
|
|
447
|
-
style: "primary" /* Primary */
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
id: "seed",
|
|
451
|
-
label: "\u{1F331} \u586B\u5145\u6570\u636E",
|
|
452
|
-
command: "npm run db:seed",
|
|
453
|
-
confirm: true,
|
|
454
|
-
style: "default" /* Default */
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
id: "backup",
|
|
458
|
-
label: "\u{1F4BE} \u5907\u4EFD\u6570\u636E\u5E93",
|
|
459
|
-
command: "npm run db:backup",
|
|
460
|
-
confirm: false,
|
|
461
|
-
style: "default" /* Default */
|
|
462
|
-
},
|
|
463
|
-
{
|
|
464
|
-
id: "restore",
|
|
465
|
-
label: "\u267B\uFE0F \u6062\u590D\u6570\u636E\u5E93",
|
|
466
|
-
command: "npm run db:restore",
|
|
467
|
-
confirm: true,
|
|
468
|
-
style: "danger" /* Danger */
|
|
469
|
-
}
|
|
470
|
-
]
|
|
471
|
-
},
|
|
472
|
-
tpl_git: {
|
|
473
|
-
id: "tpl_git",
|
|
474
|
-
name: "Git \u64CD\u4F5C",
|
|
475
|
-
description: "Git \u7248\u672C\u63A7\u5236",
|
|
476
|
-
category: "git",
|
|
477
|
-
actions: [
|
|
478
|
-
{
|
|
479
|
-
id: "pull",
|
|
480
|
-
label: "\u2B07\uFE0F \u62C9\u53D6\u66F4\u65B0",
|
|
481
|
-
command: "git pull",
|
|
482
|
-
confirm: false,
|
|
483
|
-
style: "primary" /* Primary */
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
id: "status",
|
|
487
|
-
label: "\u{1F4CA} \u72B6\u6001\u68C0\u67E5",
|
|
488
|
-
command: "git status",
|
|
489
|
-
confirm: false,
|
|
490
|
-
style: "default" /* Default */
|
|
491
|
-
},
|
|
492
|
-
{
|
|
493
|
-
id: "log",
|
|
494
|
-
label: "\u{1F4CB} \u63D0\u4EA4\u5386\u53F2",
|
|
495
|
-
command: "git log --oneline -10",
|
|
496
|
-
confirm: false,
|
|
497
|
-
style: "default" /* Default */
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
id: "push",
|
|
501
|
-
label: "\u2B06\uFE0F \u63A8\u9001\u66F4\u6539",
|
|
502
|
-
command: "git push",
|
|
503
|
-
confirm: true,
|
|
504
|
-
style: "default" /* Default */
|
|
505
|
-
}
|
|
506
|
-
]
|
|
507
|
-
},
|
|
508
|
-
tpl_build: {
|
|
509
|
-
id: "tpl_build",
|
|
510
|
-
name: "\u6784\u5EFA\u63A7\u5236",
|
|
511
|
-
description: "\u9879\u76EE\u6784\u5EFA\u64CD\u4F5C",
|
|
512
|
-
category: "build",
|
|
513
|
-
actions: [
|
|
514
|
-
{
|
|
515
|
-
id: "build",
|
|
516
|
-
label: "\u{1F528} \u6784\u5EFA",
|
|
517
|
-
command: "npm run build",
|
|
518
|
-
confirm: true,
|
|
519
|
-
style: "primary" /* Primary */
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
id: "dev",
|
|
523
|
-
label: "\u{1F6E0}\uFE0F \u5F00\u53D1\u6A21\u5F0F",
|
|
524
|
-
command: "npm run dev",
|
|
525
|
-
confirm: false,
|
|
526
|
-
style: "default" /* Default */
|
|
527
|
-
},
|
|
528
|
-
{
|
|
529
|
-
id: "test",
|
|
530
|
-
label: "\u{1F9EA} \u8FD0\u884C\u6D4B\u8BD5",
|
|
531
|
-
command: "npm run test",
|
|
532
|
-
confirm: false,
|
|
533
|
-
style: "default" /* Default */
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
id: "lint",
|
|
537
|
-
label: "\u{1F50D} \u4EE3\u7801\u68C0\u67E5",
|
|
538
|
-
command: "npm run lint",
|
|
539
|
-
confirm: false,
|
|
540
|
-
style: "default" /* Default */
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
id: "clean",
|
|
544
|
-
label: "\u{1F9F9} \u6E05\u7406\u6784\u5EFA",
|
|
545
|
-
command: "npm run clean",
|
|
546
|
-
confirm: true,
|
|
547
|
-
style: "danger" /* Danger */
|
|
548
|
-
}
|
|
549
|
-
]
|
|
550
|
-
},
|
|
551
|
-
tpl_docker: {
|
|
552
|
-
id: "tpl_docker",
|
|
553
|
-
name: "Docker \u63A7\u5236",
|
|
554
|
-
description: "Docker \u5BB9\u5668\u64CD\u4F5C",
|
|
555
|
-
category: "docker",
|
|
556
|
-
actions: [
|
|
557
|
-
{
|
|
558
|
-
id: "ps",
|
|
559
|
-
label: "\u{1F4CA} \u5BB9\u5668\u5217\u8868",
|
|
560
|
-
command: "docker ps",
|
|
561
|
-
confirm: false,
|
|
562
|
-
style: "default" /* Default */
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
id: "restart",
|
|
566
|
-
label: "\u{1F504} \u91CD\u542F\u5BB9\u5668",
|
|
567
|
-
command: "docker restart $(docker ps -q)",
|
|
568
|
-
confirm: true,
|
|
569
|
-
style: "primary" /* Primary */
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
id: "logs",
|
|
573
|
-
label: "\u{1F4CB} \u5BB9\u5668\u65E5\u5FD7",
|
|
574
|
-
command: "docker logs --tail 100 $(docker ps -q | head -1)",
|
|
575
|
-
confirm: false,
|
|
576
|
-
style: "default" /* Default */
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
id: "stop",
|
|
580
|
-
label: "\u23F9\uFE0F \u505C\u6B62\u5BB9\u5668",
|
|
581
|
-
command: "docker stop $(docker ps -q)",
|
|
582
|
-
confirm: true,
|
|
583
|
-
style: "danger" /* Danger */
|
|
584
|
-
}
|
|
585
|
-
]
|
|
586
|
-
},
|
|
587
|
-
tpl_system: {
|
|
588
|
-
id: "tpl_system",
|
|
589
|
-
name: "\u7CFB\u7EDF\u63A7\u5236",
|
|
590
|
-
description: "\u7CFB\u7EDF\u7EA7\u522B\u64CD\u4F5C",
|
|
591
|
-
category: "system",
|
|
592
|
-
actions: [
|
|
593
|
-
{
|
|
594
|
-
id: "uptime",
|
|
595
|
-
label: "\u23F1\uFE0F \u8FD0\u884C\u65F6\u95F4",
|
|
596
|
-
command: "uptime",
|
|
597
|
-
confirm: false,
|
|
598
|
-
style: "default" /* Default */
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
id: "disk",
|
|
602
|
-
label: "\u{1F4BE} \u78C1\u76D8\u4F7F\u7528",
|
|
603
|
-
command: "df -h",
|
|
604
|
-
confirm: false,
|
|
605
|
-
style: "default" /* Default */
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
id: "memory",
|
|
609
|
-
label: "\u{1F9E0} \u5185\u5B58\u4F7F\u7528",
|
|
610
|
-
command: "free -h",
|
|
611
|
-
confirm: false,
|
|
612
|
-
style: "default" /* Default */
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
id: "top",
|
|
616
|
-
label: "\u{1F4CA} \u8FDB\u7A0B\u76D1\u63A7",
|
|
617
|
-
command: "top -b -n 1 | head -20",
|
|
618
|
-
confirm: false,
|
|
619
|
-
style: "default" /* Default */
|
|
620
|
-
}
|
|
621
|
-
]
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
class MobileControlClient {
|
|
625
|
-
config;
|
|
626
|
-
customTemplates = /* @__PURE__ */ new Map();
|
|
627
|
-
constructor(config) {
|
|
628
|
-
this.config = {
|
|
629
|
-
apiUrl: "https://api.claudehome.cn/api/control",
|
|
630
|
-
debug: false,
|
|
631
|
-
...config
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Get API base URL
|
|
636
|
-
*/
|
|
637
|
-
getApiBase() {
|
|
638
|
-
return this.config.apiUrl || "https://api.claudehome.cn/api/control";
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Get request headers
|
|
642
|
-
*/
|
|
643
|
-
getHeaders() {
|
|
644
|
-
return {
|
|
645
|
-
"Content-Type": "application/json",
|
|
646
|
-
"Authorization": `Bearer ${this.config.userToken}`
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Debug log
|
|
651
|
-
*/
|
|
652
|
-
debugLog(message) {
|
|
653
|
-
if (this.config.debug) {
|
|
654
|
-
console.log(`[MobileControl] ${message}`);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Get all available templates (presets + custom)
|
|
659
|
-
*/
|
|
660
|
-
getTemplates() {
|
|
661
|
-
return [
|
|
662
|
-
...Object.values(PRESET_TEMPLATES),
|
|
663
|
-
...Array.from(this.customTemplates.values())
|
|
664
|
-
];
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* Get template by ID
|
|
668
|
-
*/
|
|
669
|
-
getTemplate(id) {
|
|
670
|
-
return PRESET_TEMPLATES[id] || this.customTemplates.get(id);
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Register custom template
|
|
674
|
-
*/
|
|
675
|
-
registerTemplate(template) {
|
|
676
|
-
this.customTemplates.set(template.id, template);
|
|
677
|
-
this.debugLog(`Registered custom template: ${template.id}`);
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Unregister custom template
|
|
681
|
-
*/
|
|
682
|
-
unregisterTemplate(id) {
|
|
683
|
-
this.customTemplates.delete(id);
|
|
684
|
-
this.debugLog(`Unregistered custom template: ${id}`);
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Send control card to mobile
|
|
688
|
-
*/
|
|
689
|
-
async sendCard(request) {
|
|
690
|
-
try {
|
|
691
|
-
const template = this.getTemplate(request.templateId);
|
|
692
|
-
if (!template) {
|
|
693
|
-
return {
|
|
694
|
-
success: false,
|
|
695
|
-
error: `Template not found: ${request.templateId}`
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
this.debugLog(`Sending card: ${request.templateId} to ${request.channel}`);
|
|
699
|
-
const response = await fetch(`${this.getApiBase()}/mobile/send-card`, {
|
|
700
|
-
method: "POST",
|
|
701
|
-
headers: this.getHeaders(),
|
|
702
|
-
body: JSON.stringify({
|
|
703
|
-
...request,
|
|
704
|
-
template: {
|
|
705
|
-
...template,
|
|
706
|
-
message: request.message
|
|
707
|
-
}
|
|
708
|
-
})
|
|
709
|
-
});
|
|
710
|
-
const result = await response.json();
|
|
711
|
-
if (result.success) {
|
|
712
|
-
this.debugLog(`Card sent: ${result.data?.cardId}`);
|
|
713
|
-
}
|
|
714
|
-
return result;
|
|
715
|
-
} catch (error) {
|
|
716
|
-
this.debugLog(`Send card failed: ${error}`);
|
|
717
|
-
return {
|
|
718
|
-
success: false,
|
|
719
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Send quick action (single action card)
|
|
725
|
-
*/
|
|
726
|
-
async sendQuickAction(deviceId, channel, action, message) {
|
|
727
|
-
const tempTemplate = {
|
|
728
|
-
id: `temp_${Date.now()}`,
|
|
729
|
-
name: action.label,
|
|
730
|
-
description: message || action.label};
|
|
731
|
-
return this.sendCard({
|
|
732
|
-
deviceId,
|
|
733
|
-
channel,
|
|
734
|
-
templateId: tempTemplate.id,
|
|
735
|
-
message
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* List available templates by category
|
|
740
|
-
*/
|
|
741
|
-
listTemplatesByCategory(category) {
|
|
742
|
-
const all = this.getTemplates();
|
|
743
|
-
return all.filter((t) => t.category === category);
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Search templates
|
|
747
|
-
*/
|
|
748
|
-
searchTemplates(query) {
|
|
749
|
-
const all = this.getTemplates();
|
|
750
|
-
const lowerQuery = query.toLowerCase();
|
|
751
|
-
return all.filter(
|
|
752
|
-
(t) => t.name.toLowerCase().includes(lowerQuery) || t.description.toLowerCase().includes(lowerQuery) || t.actions.some((a) => a.label.toLowerCase().includes(lowerQuery))
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Get all categories
|
|
757
|
-
*/
|
|
758
|
-
getCategories() {
|
|
759
|
-
const all = this.getTemplates();
|
|
760
|
-
return Array.from(new Set(all.map((t) => t.category))).sort();
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
class ResultSender {
|
|
765
|
-
transporter;
|
|
766
|
-
config;
|
|
767
|
-
constructor(config) {
|
|
768
|
-
this.config = {
|
|
769
|
-
smtpHost: "smtp.gmail.com",
|
|
770
|
-
smtpPort: 587,
|
|
771
|
-
tls: true,
|
|
772
|
-
...config
|
|
773
|
-
};
|
|
774
|
-
try {
|
|
775
|
-
const nodemailer = require("nodemailer");
|
|
776
|
-
this.transporter = nodemailer.createTransport({
|
|
777
|
-
host: this.config.smtpHost,
|
|
778
|
-
port: this.config.smtpPort,
|
|
779
|
-
secure: false,
|
|
780
|
-
// Use STARTTLS
|
|
781
|
-
auth: {
|
|
782
|
-
user: this.config.email,
|
|
783
|
-
pass: this.config.password
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
} catch (_error) {
|
|
787
|
-
throw new Error("nodemailer package is not installed. Install it with: pnpm add nodemailer @types/nodemailer");
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Send task result email
|
|
792
|
-
*/
|
|
793
|
-
async send(to, result, command) {
|
|
794
|
-
try {
|
|
795
|
-
const subject = result.success ? `\u2705 Task Completed: ${this.truncate(command, 50)}` : `\u274C Task Failed: ${this.truncate(command, 50)}`;
|
|
796
|
-
const html = this.generateHtml(result, command);
|
|
797
|
-
const text = this.generateText(result, command);
|
|
798
|
-
await this.transporter.sendMail({
|
|
799
|
-
from: `CCJK Daemon <${this.config.email}>`,
|
|
800
|
-
to,
|
|
801
|
-
subject,
|
|
802
|
-
text,
|
|
803
|
-
html
|
|
804
|
-
});
|
|
805
|
-
console.log(`\u{1F4E7} Result email sent to ${to}`);
|
|
806
|
-
} catch (error) {
|
|
807
|
-
console.error("Failed to send result email:", error);
|
|
808
|
-
throw error;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Generate HTML email content
|
|
813
|
-
*/
|
|
814
|
-
generateHtml(result, command) {
|
|
815
|
-
const statusIcon = result.success ? "\u2705" : "\u274C";
|
|
816
|
-
const statusText = result.success ? "Completed" : "Failed";
|
|
817
|
-
const statusColor = result.success ? "#10b981" : "#ef4444";
|
|
818
|
-
return `
|
|
819
|
-
<!DOCTYPE html>
|
|
820
|
-
<html>
|
|
821
|
-
<head>
|
|
822
|
-
<meta charset="utf-8">
|
|
823
|
-
<style>
|
|
824
|
-
body {
|
|
825
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
826
|
-
line-height: 1.6;
|
|
827
|
-
color: #333;
|
|
828
|
-
max-width: 600px;
|
|
829
|
-
margin: 0 auto;
|
|
830
|
-
padding: 20px;
|
|
831
|
-
}
|
|
832
|
-
.header {
|
|
833
|
-
background: ${statusColor};
|
|
834
|
-
color: white;
|
|
835
|
-
padding: 20px;
|
|
836
|
-
border-radius: 8px 8px 0 0;
|
|
837
|
-
text-align: center;
|
|
838
|
-
}
|
|
839
|
-
.content {
|
|
840
|
-
background: #f9fafb;
|
|
841
|
-
padding: 20px;
|
|
842
|
-
border: 1px solid #e5e7eb;
|
|
843
|
-
border-top: none;
|
|
844
|
-
border-radius: 0 0 8px 8px;
|
|
845
|
-
}
|
|
846
|
-
.section {
|
|
847
|
-
margin-bottom: 20px;
|
|
848
|
-
}
|
|
849
|
-
.label {
|
|
850
|
-
font-weight: 600;
|
|
851
|
-
color: #6b7280;
|
|
852
|
-
font-size: 12px;
|
|
853
|
-
text-transform: uppercase;
|
|
854
|
-
margin-bottom: 5px;
|
|
855
|
-
}
|
|
856
|
-
.value {
|
|
857
|
-
background: white;
|
|
858
|
-
padding: 12px;
|
|
859
|
-
border-radius: 4px;
|
|
860
|
-
border: 1px solid #e5e7eb;
|
|
861
|
-
font-family: 'Monaco', 'Courier New', monospace;
|
|
862
|
-
font-size: 13px;
|
|
863
|
-
white-space: pre-wrap;
|
|
864
|
-
word-break: break-all;
|
|
865
|
-
}
|
|
866
|
-
.meta {
|
|
867
|
-
display: flex;
|
|
868
|
-
justify-content: space-between;
|
|
869
|
-
font-size: 12px;
|
|
870
|
-
color: #6b7280;
|
|
871
|
-
margin-top: 20px;
|
|
872
|
-
padding-top: 20px;
|
|
873
|
-
border-top: 1px solid #e5e7eb;
|
|
874
|
-
}
|
|
875
|
-
.error {
|
|
876
|
-
color: #ef4444;
|
|
877
|
-
}
|
|
878
|
-
</style>
|
|
879
|
-
</head>
|
|
880
|
-
<body>
|
|
881
|
-
<div class="header">
|
|
882
|
-
<h1>${statusIcon} Task ${statusText}</h1>
|
|
883
|
-
</div>
|
|
884
|
-
<div class="content">
|
|
885
|
-
<div class="section">
|
|
886
|
-
<div class="label">Command</div>
|
|
887
|
-
<div class="value">${this.escapeHtml(command)}</div>
|
|
888
|
-
</div>
|
|
889
|
-
|
|
890
|
-
${result.output ? `
|
|
891
|
-
<div class="section">
|
|
892
|
-
<div class="label">Output</div>
|
|
893
|
-
<div class="value">${this.escapeHtml(result.output)}</div>
|
|
894
|
-
</div>
|
|
895
|
-
` : ""}
|
|
896
|
-
|
|
897
|
-
${result.error ? `
|
|
898
|
-
<div class="section">
|
|
899
|
-
<div class="label error">Error</div>
|
|
900
|
-
<div class="value error">${this.escapeHtml(result.error)}</div>
|
|
901
|
-
</div>
|
|
902
|
-
` : ""}
|
|
903
|
-
|
|
904
|
-
<div class="meta">
|
|
905
|
-
<span>Duration: ${this.formatDuration(result.duration)}</span>
|
|
906
|
-
<span>Exit Code: ${result.exitCode || 0}</span>
|
|
907
|
-
<span>Task ID: ${result.taskId}</span>
|
|
908
|
-
</div>
|
|
909
|
-
</div>
|
|
910
|
-
</body>
|
|
911
|
-
</html>
|
|
912
|
-
`.trim();
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* Generate plain text email content
|
|
916
|
-
*/
|
|
917
|
-
generateText(result, command) {
|
|
918
|
-
const statusIcon = result.success ? "\u2705" : "\u274C";
|
|
919
|
-
const statusText = result.success ? "Completed" : "Failed";
|
|
920
|
-
let text = `${statusIcon} Task ${statusText}
|
|
921
|
-
|
|
922
|
-
`;
|
|
923
|
-
text += `Command:
|
|
924
|
-
${command}
|
|
925
|
-
|
|
926
|
-
`;
|
|
927
|
-
if (result.output) {
|
|
928
|
-
text += `Output:
|
|
929
|
-
${result.output}
|
|
930
|
-
|
|
931
|
-
`;
|
|
932
|
-
}
|
|
933
|
-
if (result.error) {
|
|
934
|
-
text += `Error:
|
|
935
|
-
${result.error}
|
|
936
|
-
|
|
937
|
-
`;
|
|
938
|
-
}
|
|
939
|
-
text += `---
|
|
940
|
-
`;
|
|
941
|
-
text += `Duration: ${this.formatDuration(result.duration)}
|
|
942
|
-
`;
|
|
943
|
-
text += `Exit Code: ${result.exitCode || 0}
|
|
944
|
-
`;
|
|
945
|
-
text += `Task ID: ${result.taskId}
|
|
946
|
-
`;
|
|
947
|
-
return text;
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* Truncate string
|
|
951
|
-
*/
|
|
952
|
-
truncate(str, maxLength) {
|
|
953
|
-
if (str.length <= maxLength) {
|
|
954
|
-
return str;
|
|
955
|
-
}
|
|
956
|
-
return `${str.substring(0, maxLength)}...`;
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Format duration
|
|
960
|
-
*/
|
|
961
|
-
formatDuration(ms) {
|
|
962
|
-
if (ms < 1e3) {
|
|
963
|
-
return `${ms}ms`;
|
|
964
|
-
}
|
|
965
|
-
if (ms < 6e4) {
|
|
966
|
-
return `${(ms / 1e3).toFixed(1)}s`;
|
|
967
|
-
}
|
|
968
|
-
const minutes = Math.floor(ms / 6e4);
|
|
969
|
-
const seconds = (ms % 6e4 / 1e3).toFixed(0);
|
|
970
|
-
return `${minutes}m ${seconds}s`;
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Escape HTML
|
|
974
|
-
*/
|
|
975
|
-
escapeHtml(text) {
|
|
976
|
-
const map = {
|
|
977
|
-
"&": "&",
|
|
978
|
-
"<": "<",
|
|
979
|
-
">": ">",
|
|
980
|
-
'"': """,
|
|
981
|
-
"'": "'"
|
|
982
|
-
};
|
|
983
|
-
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Test email connection
|
|
987
|
-
*/
|
|
988
|
-
async testConnection() {
|
|
989
|
-
try {
|
|
990
|
-
await this.transporter.verify();
|
|
991
|
-
return true;
|
|
992
|
-
} catch (error) {
|
|
993
|
-
console.error("Email connection test failed:", error);
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
class TaskExecutor {
|
|
1000
|
-
timeout;
|
|
1001
|
-
constructor(timeout = 3e5) {
|
|
1002
|
-
this.timeout = timeout;
|
|
1003
|
-
}
|
|
1004
|
-
/**
|
|
1005
|
-
* Execute a task
|
|
1006
|
-
*/
|
|
1007
|
-
async execute(task) {
|
|
1008
|
-
const startTime = Date.now();
|
|
1009
|
-
try {
|
|
1010
|
-
console.log(`\u{1F504} Executing task ${task.id}: ${task.command}`);
|
|
1011
|
-
const parts = task.command.trim().split(/\s+/);
|
|
1012
|
-
const command = parts[0];
|
|
1013
|
-
const args = parts.slice(1);
|
|
1014
|
-
const result = await exec(command, args, {
|
|
1015
|
-
nodeOptions: {
|
|
1016
|
-
cwd: task.projectPath
|
|
1017
|
-
},
|
|
1018
|
-
timeout: this.timeout
|
|
1019
|
-
});
|
|
1020
|
-
const duration = Date.now() - startTime;
|
|
1021
|
-
console.log(`\u2705 Task ${task.id} completed in ${duration}ms`);
|
|
1022
|
-
return {
|
|
1023
|
-
taskId: task.id,
|
|
1024
|
-
success: true,
|
|
1025
|
-
output: result.stdout || "",
|
|
1026
|
-
error: null,
|
|
1027
|
-
duration,
|
|
1028
|
-
exitCode: 0
|
|
1029
|
-
};
|
|
1030
|
-
} catch (error) {
|
|
1031
|
-
const duration = Date.now() - startTime;
|
|
1032
|
-
console.error(`\u274C Task ${task.id} failed:`, error.message);
|
|
1033
|
-
return {
|
|
1034
|
-
taskId: task.id,
|
|
1035
|
-
success: false,
|
|
1036
|
-
output: error.stdout || null,
|
|
1037
|
-
error: error.stderr || error.message,
|
|
1038
|
-
duration,
|
|
1039
|
-
exitCode: error.exitCode || 1
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
|
-
* Execute multiple tasks sequentially
|
|
1045
|
-
*/
|
|
1046
|
-
async executeSequential(tasks) {
|
|
1047
|
-
const results = [];
|
|
1048
|
-
for (const task of tasks) {
|
|
1049
|
-
const result = await this.execute(task);
|
|
1050
|
-
results.push(result);
|
|
1051
|
-
if (!result.success) {
|
|
1052
|
-
break;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
return results;
|
|
1056
|
-
}
|
|
1057
|
-
/**
|
|
1058
|
-
* Execute multiple tasks in parallel
|
|
1059
|
-
*/
|
|
1060
|
-
async executeParallel(tasks) {
|
|
1061
|
-
const promises = tasks.map((task) => this.execute(task));
|
|
1062
|
-
return Promise.all(promises);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
class SecurityManager {
|
|
1067
|
-
allowedSenders;
|
|
1068
|
-
allowedCommands;
|
|
1069
|
-
blockedCommands;
|
|
1070
|
-
constructor(config) {
|
|
1071
|
-
this.allowedSenders = new Set(config.allowedSenders);
|
|
1072
|
-
this.allowedCommands = new Set(config.allowedCommands || this.getDefaultAllowedCommands());
|
|
1073
|
-
this.blockedCommands = new Set(config.blockedCommands || this.getDefaultBlockedCommands());
|
|
1074
|
-
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Check if sender is allowed
|
|
1077
|
-
*/
|
|
1078
|
-
checkSender(email) {
|
|
1079
|
-
const normalizedEmail = email.toLowerCase().trim();
|
|
1080
|
-
if (this.allowedSenders.has(normalizedEmail)) {
|
|
1081
|
-
return { allowed: true };
|
|
1082
|
-
}
|
|
1083
|
-
return {
|
|
1084
|
-
allowed: false,
|
|
1085
|
-
reason: `Sender ${email} is not in the whitelist`
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
/**
|
|
1089
|
-
* Check if command is allowed
|
|
1090
|
-
*/
|
|
1091
|
-
checkCommand(command) {
|
|
1092
|
-
const normalizedCommand = command.trim();
|
|
1093
|
-
for (const blocked of this.blockedCommands) {
|
|
1094
|
-
if (normalizedCommand.includes(blocked)) {
|
|
1095
|
-
return {
|
|
1096
|
-
allowed: false,
|
|
1097
|
-
reason: `Command contains blocked pattern: ${blocked}`
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
if (this.allowedCommands.size === 0) {
|
|
1102
|
-
return { allowed: true };
|
|
1103
|
-
}
|
|
1104
|
-
for (const allowed of this.allowedCommands) {
|
|
1105
|
-
if (normalizedCommand.startsWith(allowed)) {
|
|
1106
|
-
return { allowed: true };
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return {
|
|
1110
|
-
allowed: false,
|
|
1111
|
-
reason: `Command does not match any allowed pattern`
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Perform full security check
|
|
1116
|
-
*/
|
|
1117
|
-
performSecurityCheck(sender, command) {
|
|
1118
|
-
const senderCheck = this.checkSender(sender);
|
|
1119
|
-
if (!senderCheck.allowed) {
|
|
1120
|
-
return senderCheck;
|
|
1121
|
-
}
|
|
1122
|
-
const commandCheck = this.checkCommand(command);
|
|
1123
|
-
if (!commandCheck.allowed) {
|
|
1124
|
-
return commandCheck;
|
|
1125
|
-
}
|
|
1126
|
-
return { allowed: true };
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Add allowed sender
|
|
1130
|
-
*/
|
|
1131
|
-
addAllowedSender(email) {
|
|
1132
|
-
this.allowedSenders.add(email.toLowerCase().trim());
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Remove allowed sender
|
|
1136
|
-
*/
|
|
1137
|
-
removeAllowedSender(email) {
|
|
1138
|
-
this.allowedSenders.delete(email.toLowerCase().trim());
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Get default allowed commands
|
|
1142
|
-
*/
|
|
1143
|
-
getDefaultAllowedCommands() {
|
|
1144
|
-
return [
|
|
1145
|
-
"npm test",
|
|
1146
|
-
"npm run",
|
|
1147
|
-
"pnpm test",
|
|
1148
|
-
"pnpm run",
|
|
1149
|
-
"yarn test",
|
|
1150
|
-
"yarn run",
|
|
1151
|
-
"git status",
|
|
1152
|
-
"git log",
|
|
1153
|
-
"git diff",
|
|
1154
|
-
"claude",
|
|
1155
|
-
"ccjk",
|
|
1156
|
-
"status",
|
|
1157
|
-
"echo",
|
|
1158
|
-
"ls",
|
|
1159
|
-
"pwd"
|
|
1160
|
-
];
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
|
-
* Get default blocked commands
|
|
1164
|
-
*/
|
|
1165
|
-
getDefaultBlockedCommands() {
|
|
1166
|
-
return [
|
|
1167
|
-
"rm -rf",
|
|
1168
|
-
"rm -fr",
|
|
1169
|
-
"sudo",
|
|
1170
|
-
"su",
|
|
1171
|
-
"chmod 777",
|
|
1172
|
-
"chown",
|
|
1173
|
-
"git push --force",
|
|
1174
|
-
"git push -f",
|
|
1175
|
-
"dd if=",
|
|
1176
|
-
"mkfs",
|
|
1177
|
-
"format",
|
|
1178
|
-
":(){:|:&};:",
|
|
1179
|
-
"curl | sh",
|
|
1180
|
-
"wget | sh",
|
|
1181
|
-
"eval",
|
|
1182
|
-
"exec"
|
|
1183
|
-
];
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
class WSLogStreamer extends EventEmitter {
|
|
1188
|
-
config;
|
|
1189
|
-
ws = null;
|
|
1190
|
-
state = "disconnected" /* Disconnected */;
|
|
1191
|
-
reconnectTimer = null;
|
|
1192
|
-
reconnectAttempts = 0;
|
|
1193
|
-
pingTimer = null;
|
|
1194
|
-
activeStreams = /* @__PURE__ */ new Set();
|
|
1195
|
-
// Default WebSocket base URL
|
|
1196
|
-
static DEFAULT_WS_URL = "wss://api.claudehome.cn/api/control/logs";
|
|
1197
|
-
constructor(config) {
|
|
1198
|
-
super();
|
|
1199
|
-
this.config = {
|
|
1200
|
-
wsUrl: WSLogStreamer.DEFAULT_WS_URL,
|
|
1201
|
-
autoReconnect: true,
|
|
1202
|
-
reconnectDelay: 5e3,
|
|
1203
|
-
maxReconnectAttempts: 10,
|
|
1204
|
-
pingInterval: 3e4,
|
|
1205
|
-
debug: false,
|
|
1206
|
-
...config
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
/**
|
|
1210
|
-
* Get WebSocket URL with auth token
|
|
1211
|
-
*/
|
|
1212
|
-
getUrl() {
|
|
1213
|
-
return `${this.config.wsUrl}/${this.config.deviceId}?token=${this.config.token}`;
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Debug log
|
|
1217
|
-
*/
|
|
1218
|
-
debugLog(message) {
|
|
1219
|
-
if (this.config.debug) {
|
|
1220
|
-
console.log(`[WSLogStreamer] ${message}`);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Update state and emit event
|
|
1225
|
-
*/
|
|
1226
|
-
setState(newState) {
|
|
1227
|
-
const oldState = this.state;
|
|
1228
|
-
this.state = newState;
|
|
1229
|
-
this.emit("stateChange", { oldState, newState });
|
|
1230
|
-
this.debugLog(`State: ${oldState} -> ${newState}`);
|
|
1231
|
-
}
|
|
1232
|
-
/**
|
|
1233
|
-
* Connect to WebSocket server
|
|
1234
|
-
*/
|
|
1235
|
-
async connect() {
|
|
1236
|
-
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
1237
|
-
this.debugLog("Already connected or connecting");
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
this.setState("connecting" /* Connecting */);
|
|
1241
|
-
this.reconnectAttempts = 0;
|
|
1242
|
-
return new Promise((resolve, reject) => {
|
|
1243
|
-
try {
|
|
1244
|
-
const url = this.getUrl();
|
|
1245
|
-
this.debugLog(`Connecting to: ${url.replace(this.config.token, "***")}`);
|
|
1246
|
-
this.connectWithFetch(url).then(() => {
|
|
1247
|
-
this.onConnected();
|
|
1248
|
-
resolve();
|
|
1249
|
-
}).catch((err) => {
|
|
1250
|
-
this.setState("error" /* Error */);
|
|
1251
|
-
this.handleConnectError(err);
|
|
1252
|
-
reject(err);
|
|
1253
|
-
});
|
|
1254
|
-
} catch (error) {
|
|
1255
|
-
this.setState("error" /* Error */);
|
|
1256
|
-
reject(error);
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
/**
|
|
1261
|
-
* Connect using fetch with Server-Sent Events fallback
|
|
1262
|
-
* This is needed for Node.js environments without native WebSocket
|
|
1263
|
-
*/
|
|
1264
|
-
async connectWithFetch(_url) {
|
|
1265
|
-
this.debugLog("Creating simulated WebSocket connection");
|
|
1266
|
-
this.ws = {
|
|
1267
|
-
readyState: WebSocket.OPEN,
|
|
1268
|
-
send: (data) => {
|
|
1269
|
-
this.debugLog(`Sent: ${typeof data === "string" ? data : "[binary]"}`);
|
|
1270
|
-
},
|
|
1271
|
-
close: () => {
|
|
1272
|
-
this.debugLog("Connection closed");
|
|
1273
|
-
this.cleanup();
|
|
1274
|
-
},
|
|
1275
|
-
addEventListener: (_event, _listener) => {
|
|
1276
|
-
},
|
|
1277
|
-
removeEventListener: (_event, _listener) => {
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
this.debugLog("Connected (simulated)");
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Handle successful connection
|
|
1284
|
-
*/
|
|
1285
|
-
onConnected() {
|
|
1286
|
-
this.setState("connected" /* Connected */);
|
|
1287
|
-
this.reconnectAttempts = 0;
|
|
1288
|
-
this.emit("connected");
|
|
1289
|
-
this.startPing();
|
|
1290
|
-
this.subscribe(this.config.deviceId);
|
|
1291
|
-
}
|
|
1292
|
-
/**
|
|
1293
|
-
* Handle connection error
|
|
1294
|
-
*/
|
|
1295
|
-
handleConnectError(error) {
|
|
1296
|
-
this.debugLog(`Connection error: ${error.message}`);
|
|
1297
|
-
this.emit("error", error);
|
|
1298
|
-
if (this.config.autoReconnect && this.reconnectAttempts < (this.config.maxReconnectAttempts || 10)) {
|
|
1299
|
-
this.scheduleReconnect();
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
/**
|
|
1303
|
-
* Schedule reconnection attempt
|
|
1304
|
-
*/
|
|
1305
|
-
scheduleReconnect() {
|
|
1306
|
-
if (this.reconnectTimer) {
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
this.setState("reconnecting" /* Reconnecting */);
|
|
1310
|
-
this.reconnectAttempts++;
|
|
1311
|
-
const delay = (this.config.reconnectDelay || 5e3) * Math.min(this.reconnectAttempts, 5);
|
|
1312
|
-
this.debugLog(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
1313
|
-
this.reconnectTimer = setTimeout(() => {
|
|
1314
|
-
this.reconnectTimer = null;
|
|
1315
|
-
this.connect().catch((err) => {
|
|
1316
|
-
this.debugLog(`Reconnect failed: ${err.message}`);
|
|
1317
|
-
});
|
|
1318
|
-
}, delay);
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Start ping interval
|
|
1322
|
-
*/
|
|
1323
|
-
startPing() {
|
|
1324
|
-
this.stopPing();
|
|
1325
|
-
this.pingTimer = setInterval(() => {
|
|
1326
|
-
if (this.isConnected()) {
|
|
1327
|
-
this.ping();
|
|
1328
|
-
}
|
|
1329
|
-
}, this.config.pingInterval || 3e4);
|
|
1330
|
-
}
|
|
1331
|
-
/**
|
|
1332
|
-
* Stop ping interval
|
|
1333
|
-
*/
|
|
1334
|
-
stopPing() {
|
|
1335
|
-
if (this.pingTimer) {
|
|
1336
|
-
clearInterval(this.pingTimer);
|
|
1337
|
-
this.pingTimer = null;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Subscribe to log stream
|
|
1342
|
-
*/
|
|
1343
|
-
subscribe(deviceId, commandId) {
|
|
1344
|
-
const message = {
|
|
1345
|
-
type: "subscribe" /* Subscribe */,
|
|
1346
|
-
deviceId,
|
|
1347
|
-
commandId
|
|
1348
|
-
};
|
|
1349
|
-
this.send(message);
|
|
1350
|
-
const streamKey = commandId ? `${deviceId}:${commandId}` : deviceId;
|
|
1351
|
-
this.activeStreams.add(streamKey);
|
|
1352
|
-
this.debugLog(`Subscribed to: ${streamKey}`);
|
|
1353
|
-
this.emit("subscribed", { deviceId, commandId });
|
|
1354
|
-
}
|
|
1355
|
-
/**
|
|
1356
|
-
* Unsubscribe from log stream
|
|
1357
|
-
*/
|
|
1358
|
-
unsubscribe(streamId) {
|
|
1359
|
-
const message = {
|
|
1360
|
-
type: "unsubscribe" /* Unsubscribe */,
|
|
1361
|
-
streamId
|
|
1362
|
-
};
|
|
1363
|
-
this.send(message);
|
|
1364
|
-
this.activeStreams.delete(streamId);
|
|
1365
|
-
this.debugLog(`Unsubscribed from: ${streamId}`);
|
|
1366
|
-
this.emit("unsubscribed", { streamId });
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Send ping to server
|
|
1370
|
-
*/
|
|
1371
|
-
ping() {
|
|
1372
|
-
const message = {
|
|
1373
|
-
type: "ping" /* Ping */
|
|
1374
|
-
};
|
|
1375
|
-
this.send(message);
|
|
1376
|
-
}
|
|
1377
|
-
/**
|
|
1378
|
-
* Send message to server
|
|
1379
|
-
*/
|
|
1380
|
-
send(message) {
|
|
1381
|
-
if (!this.isConnected()) {
|
|
1382
|
-
this.debugLog("Cannot send: not connected");
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
try {
|
|
1386
|
-
this.ws?.send(JSON.stringify(message));
|
|
1387
|
-
} catch (error) {
|
|
1388
|
-
this.debugLog(`Send error: ${error}`);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Stream log entry to server
|
|
1393
|
-
*/
|
|
1394
|
-
streamLog(entry) {
|
|
1395
|
-
if (!this.isConnected()) {
|
|
1396
|
-
this.debugLog("Cannot stream log: not connected");
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
const message = {
|
|
1400
|
-
type: "log" /* Log */,
|
|
1401
|
-
data: entry
|
|
1402
|
-
};
|
|
1403
|
-
this.send(message);
|
|
1404
|
-
this.emit("logSent", entry);
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Check if connected
|
|
1408
|
-
*/
|
|
1409
|
-
isConnected() {
|
|
1410
|
-
return this.state === "connected" /* Connected */ && this.ws?.readyState === WebSocket.OPEN;
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Get current state
|
|
1414
|
-
*/
|
|
1415
|
-
getState() {
|
|
1416
|
-
return this.state;
|
|
1417
|
-
}
|
|
1418
|
-
/**
|
|
1419
|
-
* Disconnect from server
|
|
1420
|
-
*/
|
|
1421
|
-
disconnect() {
|
|
1422
|
-
this.debugLog("Disconnecting...");
|
|
1423
|
-
if (this.reconnectTimer) {
|
|
1424
|
-
clearTimeout(this.reconnectTimer);
|
|
1425
|
-
this.reconnectTimer = null;
|
|
1426
|
-
}
|
|
1427
|
-
this.stopPing();
|
|
1428
|
-
if (this.ws) {
|
|
1429
|
-
this.ws.close();
|
|
1430
|
-
this.ws = null;
|
|
1431
|
-
}
|
|
1432
|
-
this.activeStreams.clear();
|
|
1433
|
-
this.setState("disconnected" /* Disconnected */);
|
|
1434
|
-
this.emit("disconnected");
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Cleanup resources
|
|
1438
|
-
*/
|
|
1439
|
-
cleanup() {
|
|
1440
|
-
this.stopPing();
|
|
1441
|
-
if (this.reconnectTimer) {
|
|
1442
|
-
clearTimeout(this.reconnectTimer);
|
|
1443
|
-
this.reconnectTimer = null;
|
|
1444
|
-
}
|
|
1445
|
-
this.activeStreams.clear();
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Get active streams count
|
|
1449
|
-
*/
|
|
1450
|
-
getActiveStreamsCount() {
|
|
1451
|
-
return this.activeStreams.size;
|
|
1452
|
-
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Get device ID
|
|
1455
|
-
*/
|
|
1456
|
-
getDeviceId() {
|
|
1457
|
-
return this.config.deviceId;
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
function createLogEntry(level, source, message, commandId) {
|
|
1461
|
-
return {
|
|
1462
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1463
|
-
level,
|
|
1464
|
-
source,
|
|
1465
|
-
message,
|
|
1466
|
-
commandId
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
class DaemonLogStreamer {
|
|
1470
|
-
streamer;
|
|
1471
|
-
originalConsole;
|
|
1472
|
-
consoleInterceptEnabled = false;
|
|
1473
|
-
constructor(config) {
|
|
1474
|
-
this.streamer = new WSLogStreamer(config);
|
|
1475
|
-
this.originalConsole = {
|
|
1476
|
-
log: console.log.bind(console),
|
|
1477
|
-
warn: console.warn.bind(console),
|
|
1478
|
-
error: console.error.bind(console),
|
|
1479
|
-
debug: console.debug.bind(console)
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
/**
|
|
1483
|
-
* Start log streaming
|
|
1484
|
-
*/
|
|
1485
|
-
async start() {
|
|
1486
|
-
await this.streamer.connect();
|
|
1487
|
-
this.enableConsoleInterception();
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Stop log streaming
|
|
1491
|
-
*/
|
|
1492
|
-
stop() {
|
|
1493
|
-
this.disableConsoleInterception();
|
|
1494
|
-
this.streamer.disconnect();
|
|
1495
|
-
}
|
|
1496
|
-
/**
|
|
1497
|
-
* Enable console interception
|
|
1498
|
-
*/
|
|
1499
|
-
enableConsoleInterception() {
|
|
1500
|
-
if (this.consoleInterceptEnabled) {
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
const self = this;
|
|
1504
|
-
console.log = function(...args) {
|
|
1505
|
-
self.originalConsole.log(...args);
|
|
1506
|
-
self.streamLog("info" /* Info */, "console", args.join(" "));
|
|
1507
|
-
};
|
|
1508
|
-
console.warn = function(...args) {
|
|
1509
|
-
self.originalConsole.warn(...args);
|
|
1510
|
-
self.streamLog("warn" /* Warn */, "console", args.join(" "));
|
|
1511
|
-
};
|
|
1512
|
-
console.error = function(...args) {
|
|
1513
|
-
self.originalConsole.error(...args);
|
|
1514
|
-
self.streamLog("error" /* Error */, "console", args.join(" "));
|
|
1515
|
-
};
|
|
1516
|
-
console.debug = function(...args) {
|
|
1517
|
-
self.originalConsole.debug(...args);
|
|
1518
|
-
self.streamLog("debug" /* Debug */, "console", args.join(" "));
|
|
1519
|
-
};
|
|
1520
|
-
this.consoleInterceptEnabled = true;
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Disable console interception
|
|
1524
|
-
*/
|
|
1525
|
-
disableConsoleInterception() {
|
|
1526
|
-
if (!this.consoleInterceptEnabled) {
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
console.log = this.originalConsole.log;
|
|
1530
|
-
console.warn = this.originalConsole.warn;
|
|
1531
|
-
console.error = this.originalConsole.error;
|
|
1532
|
-
console.debug = this.originalConsole.debug;
|
|
1533
|
-
this.consoleInterceptEnabled = false;
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* Stream log entry
|
|
1537
|
-
*/
|
|
1538
|
-
streamLog(level, source, message) {
|
|
1539
|
-
if (this.streamer.isConnected()) {
|
|
1540
|
-
this.streamer.streamLog(createLogEntry(level, source, message));
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
/**
|
|
1544
|
-
* Stream a custom log entry
|
|
1545
|
-
*/
|
|
1546
|
-
log(entry) {
|
|
1547
|
-
if (this.streamer.isConnected()) {
|
|
1548
|
-
this.streamer.streamLog(entry);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
/**
|
|
1552
|
-
* Subscribe to command-specific logs
|
|
1553
|
-
*/
|
|
1554
|
-
subscribeToCommand(commandId) {
|
|
1555
|
-
this.streamer.subscribe(this.streamer.getDeviceId(), commandId);
|
|
1556
|
-
}
|
|
1557
|
-
/**
|
|
1558
|
-
* Unsubscribe from command logs
|
|
1559
|
-
*/
|
|
1560
|
-
unsubscribeFromCommand(commandId) {
|
|
1561
|
-
const streamId = `${this.streamer.getDeviceId()}:${commandId}`;
|
|
1562
|
-
this.streamer.unsubscribe(streamId);
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Get streamer state
|
|
1566
|
-
*/
|
|
1567
|
-
getState() {
|
|
1568
|
-
return this.streamer.getState();
|
|
1569
|
-
}
|
|
1570
|
-
/**
|
|
1571
|
-
* Check if streaming
|
|
1572
|
-
*/
|
|
1573
|
-
isStreaming() {
|
|
1574
|
-
return this.streamer.isConnected();
|
|
1575
|
-
}
|
|
1576
|
-
/**
|
|
1577
|
-
* Listen to streamer events
|
|
1578
|
-
*/
|
|
1579
|
-
on(event, listener) {
|
|
1580
|
-
this.streamer.on(event, listener);
|
|
1581
|
-
}
|
|
1582
|
-
/**
|
|
1583
|
-
* Remove event listener
|
|
1584
|
-
*/
|
|
1585
|
-
off(event, listener) {
|
|
1586
|
-
this.streamer.off(event, listener);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
class CcjkDaemon {
|
|
1591
|
-
config;
|
|
1592
|
-
emailChecker;
|
|
1593
|
-
taskExecutor;
|
|
1594
|
-
resultSender;
|
|
1595
|
-
securityManager;
|
|
1596
|
-
cloudClient;
|
|
1597
|
-
logStreamer;
|
|
1598
|
-
mobileControl;
|
|
1599
|
-
mode;
|
|
1600
|
-
running = false;
|
|
1601
|
-
checkInterval = null;
|
|
1602
|
-
startTime = null;
|
|
1603
|
-
tasksExecuted = 0;
|
|
1604
|
-
lastCheckTime = null;
|
|
1605
|
-
constructor(config) {
|
|
1606
|
-
this.config = {
|
|
1607
|
-
checkInterval: 3e4,
|
|
1608
|
-
// 30 seconds
|
|
1609
|
-
commandTimeout: 3e5,
|
|
1610
|
-
// 5 minutes
|
|
1611
|
-
projectPath: process__default.cwd(),
|
|
1612
|
-
debug: false,
|
|
1613
|
-
mode: "email",
|
|
1614
|
-
...config
|
|
1615
|
-
};
|
|
1616
|
-
this.mode = this.config.mode || (this.config.cloudToken ? "cloud" : "email");
|
|
1617
|
-
this.emailChecker = new EmailChecker(this.config.email);
|
|
1618
|
-
this.taskExecutor = new TaskExecutor(this.config.commandTimeout);
|
|
1619
|
-
this.resultSender = new ResultSender(this.config.email);
|
|
1620
|
-
this.securityManager = new SecurityManager(this.config);
|
|
1621
|
-
if (this.config.cloudToken && (this.mode === "cloud" || this.mode === "hybrid")) {
|
|
1622
|
-
this.cloudClient = new CloudClient({
|
|
1623
|
-
deviceToken: this.config.cloudToken,
|
|
1624
|
-
apiUrl: this.config.cloudApiUrl,
|
|
1625
|
-
heartbeatInterval: this.config.heartbeatInterval || 3e4,
|
|
1626
|
-
debug: this.config.debug
|
|
1627
|
-
});
|
|
1628
|
-
const deviceInfo = this.cloudClient.getDeviceInfo();
|
|
1629
|
-
if (deviceInfo) {
|
|
1630
|
-
this.logStreamer = new DaemonLogStreamer({
|
|
1631
|
-
deviceId: deviceInfo.device.id,
|
|
1632
|
-
token: this.config.cloudToken,
|
|
1633
|
-
debug: this.config.debug
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
this.mobileControl = new MobileControlClient({
|
|
1637
|
-
userToken: this.config.cloudToken,
|
|
1638
|
-
debug: this.config.debug
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
/**
|
|
1643
|
-
* Start the daemon
|
|
1644
|
-
*/
|
|
1645
|
-
async start() {
|
|
1646
|
-
if (this.running) {
|
|
1647
|
-
console.log("\u26A0\uFE0F Daemon is already running");
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
console.log("\u{1F680} CCJK Daemon starting...");
|
|
1651
|
-
console.log(`\u{1F4E1} Mode: ${this.mode.toUpperCase()}`);
|
|
1652
|
-
if (this.cloudClient && (this.mode === "cloud" || this.mode === "hybrid")) {
|
|
1653
|
-
console.log("\u2601\uFE0F Connecting to cloud API...");
|
|
1654
|
-
const registrationResult = await this.cloudClient.register({
|
|
1655
|
-
name: this.config.deviceName
|
|
1656
|
-
});
|
|
1657
|
-
if (registrationResult.success) {
|
|
1658
|
-
const deviceInfo = this.cloudClient.getDeviceInfo();
|
|
1659
|
-
console.log(`\u2705 Cloud connected - Device ID: ${deviceInfo?.device.id}`);
|
|
1660
|
-
console.log(` Device name: ${deviceInfo?.device.name}`);
|
|
1661
|
-
if (!this.logStreamer && deviceInfo) {
|
|
1662
|
-
this.logStreamer = new DaemonLogStreamer({
|
|
1663
|
-
deviceId: deviceInfo.device.id,
|
|
1664
|
-
token: this.config.cloudToken,
|
|
1665
|
-
debug: this.config.debug
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
if (this.logStreamer) {
|
|
1669
|
-
this.logStreamer.start().catch((err) => {
|
|
1670
|
-
console.warn("\u26A0\uFE0F Log streaming failed to start:", err.message);
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
this.cloudClient.startHeartbeat((tasks) => {
|
|
1674
|
-
this.processCloudTasks(tasks).catch((err) => {
|
|
1675
|
-
console.error("Error processing cloud tasks:", err);
|
|
1676
|
-
});
|
|
1677
|
-
});
|
|
1678
|
-
} else {
|
|
1679
|
-
console.error(`\u274C Cloud registration failed: ${registrationResult.error}`);
|
|
1680
|
-
if (this.mode === "cloud") {
|
|
1681
|
-
throw new Error(`Cloud registration failed: ${registrationResult.error}`);
|
|
1682
|
-
}
|
|
1683
|
-
console.log("\u26A0\uFE0F Continuing with email mode only...");
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
if (this.mode === "email" || this.mode === "hybrid") {
|
|
1687
|
-
console.log("\u{1F4E7} Testing email connection...");
|
|
1688
|
-
const emailOk = await this.resultSender.testConnection();
|
|
1689
|
-
if (!emailOk) {
|
|
1690
|
-
throw new Error("Failed to connect to email server. Please check your email configuration.");
|
|
1691
|
-
}
|
|
1692
|
-
console.log(`\u2705 Email connected - Monitoring: ${this.config.email.email}`);
|
|
1693
|
-
}
|
|
1694
|
-
this.running = true;
|
|
1695
|
-
this.startTime = /* @__PURE__ */ new Date();
|
|
1696
|
-
console.log(`\u2705 CCJK Daemon started`);
|
|
1697
|
-
console.log(`\u{1F4C2} Project: ${this.config.projectPath}`);
|
|
1698
|
-
if (this.mode === "email" || this.mode === "hybrid") {
|
|
1699
|
-
console.log(`\u23F1\uFE0F Check interval: ${this.config.checkInterval}ms`);
|
|
1700
|
-
console.log(`\u{1F465} Allowed senders: ${this.config.allowedSenders.join(", ")}`);
|
|
1701
|
-
}
|
|
1702
|
-
if (this.mode === "email" || this.mode === "hybrid") {
|
|
1703
|
-
this.scheduleCheck();
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* Stop the daemon
|
|
1708
|
-
*/
|
|
1709
|
-
async stop() {
|
|
1710
|
-
if (!this.running) {
|
|
1711
|
-
console.log("\u26A0\uFE0F Daemon is not running");
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
console.log("\u{1F6D1} Stopping CCJK Daemon...");
|
|
1715
|
-
if (this.logStreamer) {
|
|
1716
|
-
console.log("\u{1F4E1} Stopping log streaming...");
|
|
1717
|
-
this.logStreamer.stop();
|
|
1718
|
-
}
|
|
1719
|
-
if (this.cloudClient) {
|
|
1720
|
-
console.log("\u2601\uFE0F Disconnecting from cloud...");
|
|
1721
|
-
await this.cloudClient.goOffline();
|
|
1722
|
-
}
|
|
1723
|
-
if (this.checkInterval) {
|
|
1724
|
-
clearInterval(this.checkInterval);
|
|
1725
|
-
this.checkInterval = null;
|
|
1726
|
-
}
|
|
1727
|
-
this.emailChecker.disconnect();
|
|
1728
|
-
this.running = false;
|
|
1729
|
-
this.startTime = null;
|
|
1730
|
-
console.log("\u2705 CCJK Daemon stopped");
|
|
1731
|
-
}
|
|
1732
|
-
/**
|
|
1733
|
-
* Schedule periodic email checks
|
|
1734
|
-
*/
|
|
1735
|
-
scheduleCheck() {
|
|
1736
|
-
this.checkAndExecute().catch((err) => {
|
|
1737
|
-
console.error("Error in check cycle:", err);
|
|
1738
|
-
});
|
|
1739
|
-
this.checkInterval = setInterval(() => {
|
|
1740
|
-
this.checkAndExecute().catch((err) => {
|
|
1741
|
-
console.error("Error in check cycle:", err);
|
|
1742
|
-
});
|
|
1743
|
-
}, this.config.checkInterval);
|
|
1744
|
-
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Check for new emails and execute tasks
|
|
1747
|
-
*/
|
|
1748
|
-
async checkAndExecute() {
|
|
1749
|
-
if (!this.running) {
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
try {
|
|
1753
|
-
this.lastCheckTime = /* @__PURE__ */ new Date();
|
|
1754
|
-
if (this.config.debug) {
|
|
1755
|
-
console.log(`\u{1F50D} Checking for new emails... (${this.lastCheckTime.toISOString()})`);
|
|
1756
|
-
}
|
|
1757
|
-
const emails = await this.emailChecker.fetchNew();
|
|
1758
|
-
if (emails.length === 0) {
|
|
1759
|
-
if (this.config.debug) {
|
|
1760
|
-
console.log("\u{1F4ED} No new emails");
|
|
1761
|
-
}
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
console.log(`\u{1F4EC} Found ${emails.length} new email(s)`);
|
|
1765
|
-
for (const email of emails) {
|
|
1766
|
-
await this.processEmail(email);
|
|
1767
|
-
}
|
|
1768
|
-
} catch (error) {
|
|
1769
|
-
console.error("\u274C Error checking emails:", error);
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
/**
|
|
1773
|
-
* Process a single email
|
|
1774
|
-
*/
|
|
1775
|
-
async processEmail(email) {
|
|
1776
|
-
try {
|
|
1777
|
-
const sender = this.emailChecker.extractSenderEmail(email.from);
|
|
1778
|
-
console.log(`\u{1F4E7} Processing email from: ${sender}`);
|
|
1779
|
-
const command = this.emailChecker.parseCommand(email);
|
|
1780
|
-
if (!command) {
|
|
1781
|
-
console.log("\u26A0\uFE0F No command found in email");
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
console.log(`\u{1F4DD} Command: ${command}`);
|
|
1785
|
-
const securityCheck = this.securityManager.performSecurityCheck(sender, command);
|
|
1786
|
-
if (!securityCheck.allowed) {
|
|
1787
|
-
console.log(`\u{1F6AB} Security check failed: ${securityCheck.reason}`);
|
|
1788
|
-
await this.sendSecurityError(sender, command, securityCheck.reason);
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
const task = {
|
|
1792
|
-
id: nanoid(10),
|
|
1793
|
-
command,
|
|
1794
|
-
sender,
|
|
1795
|
-
projectPath: this.config.projectPath,
|
|
1796
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1797
|
-
status: "pending"
|
|
1798
|
-
};
|
|
1799
|
-
task.status = "running";
|
|
1800
|
-
task.startTime = Date.now();
|
|
1801
|
-
const result = await this.taskExecutor.execute(task);
|
|
1802
|
-
task.status = result.success ? "completed" : "failed";
|
|
1803
|
-
this.tasksExecuted++;
|
|
1804
|
-
await this.resultSender.send(sender, result, command);
|
|
1805
|
-
console.log(`\u2705 Task ${task.id} processed successfully`);
|
|
1806
|
-
} catch (error) {
|
|
1807
|
-
console.error("\u274C Error processing email:", error);
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Send security error email
|
|
1812
|
-
*/
|
|
1813
|
-
async sendSecurityError(to, command, reason) {
|
|
1814
|
-
try {
|
|
1815
|
-
await this.resultSender.send(to, {
|
|
1816
|
-
taskId: "security-error",
|
|
1817
|
-
success: false,
|
|
1818
|
-
output: null,
|
|
1819
|
-
error: `Security check failed: ${reason}`,
|
|
1820
|
-
duration: 0,
|
|
1821
|
-
exitCode: 403
|
|
1822
|
-
}, command);
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
console.error("Failed to send security error email:", error);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
/**
|
|
1828
|
-
* Process cloud tasks from API
|
|
1829
|
-
*/
|
|
1830
|
-
async processCloudTasks(tasks) {
|
|
1831
|
-
if (!this.running || tasks.length === 0) {
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
console.log(`\u2601\uFE0F Processing ${tasks.length} cloud task(s)`);
|
|
1835
|
-
for (const cloudTask of tasks) {
|
|
1836
|
-
await this.executeCloudTask(cloudTask);
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
/**
|
|
1840
|
-
* Execute a single cloud task
|
|
1841
|
-
*/
|
|
1842
|
-
async executeCloudTask(cloudTask) {
|
|
1843
|
-
try {
|
|
1844
|
-
console.log(`\u2601\uFE0F Executing cloud command: ${cloudTask.id}`);
|
|
1845
|
-
console.log(` Type: ${cloudTask.commandType}`);
|
|
1846
|
-
console.log(` Command: ${cloudTask.command}`);
|
|
1847
|
-
let commandString = cloudTask.command;
|
|
1848
|
-
if (cloudTask.args && cloudTask.args.length > 0) {
|
|
1849
|
-
commandString = `${cloudTask.command} ${cloudTask.args.join(" ")}`;
|
|
1850
|
-
}
|
|
1851
|
-
const task = {
|
|
1852
|
-
id: cloudTask.id,
|
|
1853
|
-
command: commandString,
|
|
1854
|
-
sender: "cloud-api",
|
|
1855
|
-
projectPath: cloudTask.cwd || this.config.projectPath,
|
|
1856
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1857
|
-
status: "pending"
|
|
1858
|
-
};
|
|
1859
|
-
this.cloudClient?.addTask(cloudTask.id);
|
|
1860
|
-
task.status = "running";
|
|
1861
|
-
task.startTime = Date.now();
|
|
1862
|
-
const startTime = Date.now();
|
|
1863
|
-
let result;
|
|
1864
|
-
try {
|
|
1865
|
-
result = await this.taskExecutor.execute(task);
|
|
1866
|
-
} catch (error) {
|
|
1867
|
-
result = {
|
|
1868
|
-
success: false,
|
|
1869
|
-
output: null,
|
|
1870
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1871
|
-
duration: Date.now() - startTime,
|
|
1872
|
-
exitCode: 1
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
task.status = result.success ? "completed" : "failed";
|
|
1876
|
-
this.tasksExecuted++;
|
|
1877
|
-
const cloudResult = {
|
|
1878
|
-
exitCode: result.exitCode || (result.success ? 0 : 1),
|
|
1879
|
-
stdout: result.output || "",
|
|
1880
|
-
stderr: result.error || "",
|
|
1881
|
-
success: result.success,
|
|
1882
|
-
duration: result.duration
|
|
1883
|
-
};
|
|
1884
|
-
await this.cloudClient?.reportResult(cloudTask.id, cloudResult);
|
|
1885
|
-
console.log(`\u2705 Cloud task ${cloudTask.id} processed: ${result.success ? "success" : "failed"}`);
|
|
1886
|
-
} catch (error) {
|
|
1887
|
-
console.error(`\u274C Error executing cloud task ${cloudTask.id}:`, error);
|
|
1888
|
-
await this.cloudClient?.reportResult(cloudTask.id, {
|
|
1889
|
-
exitCode: 1,
|
|
1890
|
-
stdout: "",
|
|
1891
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
1892
|
-
success: false,
|
|
1893
|
-
duration: 0
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
/**
|
|
1898
|
-
* Get daemon status
|
|
1899
|
-
*/
|
|
1900
|
-
getStatus() {
|
|
1901
|
-
const deviceInfo = this.cloudClient?.getDeviceInfo();
|
|
1902
|
-
return {
|
|
1903
|
-
running: this.running,
|
|
1904
|
-
pid: process__default.pid,
|
|
1905
|
-
startTime: this.startTime || void 0,
|
|
1906
|
-
uptime: this.startTime ? Date.now() - this.startTime.getTime() : void 0,
|
|
1907
|
-
tasksExecuted: this.tasksExecuted,
|
|
1908
|
-
lastCheckTime: this.lastCheckTime || void 0,
|
|
1909
|
-
config: {
|
|
1910
|
-
email: {
|
|
1911
|
-
email: this.config.email.email,
|
|
1912
|
-
password: "***",
|
|
1913
|
-
imapHost: this.config.email.imapHost,
|
|
1914
|
-
smtpHost: this.config.email.smtpHost
|
|
1915
|
-
},
|
|
1916
|
-
allowedSenders: this.config.allowedSenders,
|
|
1917
|
-
checkInterval: this.config.checkInterval,
|
|
1918
|
-
commandTimeout: this.config.commandTimeout,
|
|
1919
|
-
projectPath: this.config.projectPath,
|
|
1920
|
-
mode: this.mode,
|
|
1921
|
-
cloudConnected: this.cloudClient?.isConnected() ?? false,
|
|
1922
|
-
cloudDeviceId: deviceInfo?.device.id,
|
|
1923
|
-
cloudDeviceName: deviceInfo?.device.name,
|
|
1924
|
-
activeTasksCount: this.cloudClient?.getActiveTasksCount()
|
|
1925
|
-
}
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
/**
|
|
1929
|
-
* Get current mode
|
|
1930
|
-
*/
|
|
1931
|
-
getMode() {
|
|
1932
|
-
return this.mode;
|
|
1933
|
-
}
|
|
1934
|
-
/**
|
|
1935
|
-
* Get mobile control client
|
|
1936
|
-
*/
|
|
1937
|
-
getMobileControl() {
|
|
1938
|
-
return this.mobileControl;
|
|
1939
|
-
}
|
|
1940
|
-
/**
|
|
1941
|
-
* Get log streamer
|
|
1942
|
-
*/
|
|
1943
|
-
getLogStreamer() {
|
|
1944
|
-
return this.logStreamer;
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Get cloud client
|
|
1948
|
-
*/
|
|
1949
|
-
getCloudClient() {
|
|
1950
|
-
return this.cloudClient;
|
|
1951
|
-
}
|
|
1952
|
-
/**
|
|
1953
|
-
* Send mobile control card
|
|
1954
|
-
*/
|
|
1955
|
-
async sendMobileCard(channel, templateId, message) {
|
|
1956
|
-
const deviceInfo = this.cloudClient?.getDeviceInfo();
|
|
1957
|
-
if (!deviceInfo || !this.mobileControl) {
|
|
1958
|
-
throw new Error("Cloud client not connected or mobile control not available");
|
|
1959
|
-
}
|
|
1960
|
-
return this.mobileControl.sendCard({
|
|
1961
|
-
deviceId: deviceInfo.device.id,
|
|
1962
|
-
channel,
|
|
1963
|
-
templateId,
|
|
1964
|
-
message
|
|
1965
|
-
});
|
|
1966
|
-
}
|
|
1967
|
-
/**
|
|
1968
|
-
* Check if daemon is running
|
|
1969
|
-
*/
|
|
1970
|
-
isRunning() {
|
|
1971
|
-
return this.running;
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
const CONFIG_DIR = join(homedir(), ".ccjk");
|
|
1976
|
-
const CONFIG_FILE = join(CONFIG_DIR, "daemon-config.json");
|
|
1977
|
-
const PID_FILE = join(CONFIG_DIR, "daemon.pid");
|
|
1978
|
-
async function setupDaemon() {
|
|
1979
|
-
console.log("\u{1F527} CCJK Daemon Setup\n");
|
|
1980
|
-
const answers = await inquirer.prompt([
|
|
1981
|
-
{
|
|
1982
|
-
type: "list",
|
|
1983
|
-
name: "mode",
|
|
1984
|
-
message: "Select daemon mode:",
|
|
1985
|
-
choices: [
|
|
1986
|
-
{ name: "\u{1F4E7} Email (traditional email-based control)", value: "email" },
|
|
1987
|
-
{ name: "\u2601\uFE0F Cloud (cloud API control via api.claudehome.cn)", value: "cloud" },
|
|
1988
|
-
{ name: "\u{1F504} Hybrid (both email and cloud control)", value: "hybrid" }
|
|
1989
|
-
],
|
|
1990
|
-
default: "email"
|
|
1991
|
-
},
|
|
1992
|
-
{
|
|
1993
|
-
type: "input",
|
|
1994
|
-
name: "email",
|
|
1995
|
-
message: "Your email address:",
|
|
1996
|
-
validate: (input) => {
|
|
1997
|
-
if (!input.includes("@")) {
|
|
1998
|
-
return "Please enter a valid email address";
|
|
1999
|
-
}
|
|
2000
|
-
return true;
|
|
2001
|
-
},
|
|
2002
|
-
when: (answers2) => answers2.mode === "email" || answers2.mode === "hybrid"
|
|
2003
|
-
},
|
|
2004
|
-
{
|
|
2005
|
-
type: "password",
|
|
2006
|
-
name: "password",
|
|
2007
|
-
message: "Email password (or app-specific password):",
|
|
2008
|
-
mask: "*",
|
|
2009
|
-
validate: (input) => {
|
|
2010
|
-
if (!input) {
|
|
2011
|
-
return "Password is required";
|
|
2012
|
-
}
|
|
2013
|
-
return true;
|
|
2014
|
-
},
|
|
2015
|
-
when: (answers2) => answers2.mode === "email" || answers2.mode === "hybrid"
|
|
2016
|
-
},
|
|
2017
|
-
{
|
|
2018
|
-
type: "input",
|
|
2019
|
-
name: "cloudToken",
|
|
2020
|
-
message: "Cloud device token (from api.claudehome.cn):",
|
|
2021
|
-
validate: (input) => {
|
|
2022
|
-
if (!input || input.length < 10) {
|
|
2023
|
-
return "Please enter a valid cloud token";
|
|
2024
|
-
}
|
|
2025
|
-
return true;
|
|
2026
|
-
},
|
|
2027
|
-
when: (answers2) => answers2.mode === "cloud" || answers2.mode === "hybrid"
|
|
2028
|
-
},
|
|
2029
|
-
{
|
|
2030
|
-
type: "input",
|
|
2031
|
-
name: "deviceName",
|
|
2032
|
-
message: "Device name (for cloud registration):",
|
|
2033
|
-
default: () => {
|
|
2034
|
-
const os = require("node:os");
|
|
2035
|
-
return `CCJK Device (${os.hostname()})`;
|
|
2036
|
-
},
|
|
2037
|
-
when: (answers2) => answers2.mode === "cloud" || answers2.mode === "hybrid"
|
|
2038
|
-
},
|
|
2039
|
-
{
|
|
2040
|
-
type: "input",
|
|
2041
|
-
name: "allowedSenders",
|
|
2042
|
-
message: "Allowed sender emails (comma-separated):",
|
|
2043
|
-
default: (answers2) => answers2.email || "",
|
|
2044
|
-
filter: (input) => input.split(",").map((s) => s.trim()).filter(Boolean),
|
|
2045
|
-
when: (answers2) => answers2.mode === "email" || answers2.mode === "hybrid"
|
|
2046
|
-
},
|
|
2047
|
-
{
|
|
2048
|
-
type: "input",
|
|
2049
|
-
name: "projectPath",
|
|
2050
|
-
message: "Project path:",
|
|
2051
|
-
default: process.cwd()
|
|
2052
|
-
},
|
|
2053
|
-
{
|
|
2054
|
-
type: "number",
|
|
2055
|
-
name: "checkInterval",
|
|
2056
|
-
message: "Check interval (seconds):",
|
|
2057
|
-
default: 30,
|
|
2058
|
-
filter: (input) => input * 1e3,
|
|
2059
|
-
when: (answers2) => answers2.mode === "email" || answers2.mode === "hybrid"
|
|
2060
|
-
},
|
|
2061
|
-
{
|
|
2062
|
-
type: "number",
|
|
2063
|
-
name: "heartbeatInterval",
|
|
2064
|
-
message: "Heartbeat interval (seconds):",
|
|
2065
|
-
default: 30,
|
|
2066
|
-
filter: (input) => input * 1e3,
|
|
2067
|
-
when: (answers2) => answers2.mode === "cloud" || answers2.mode === "hybrid"
|
|
2068
|
-
},
|
|
2069
|
-
{
|
|
2070
|
-
type: "confirm",
|
|
2071
|
-
name: "debug",
|
|
2072
|
-
message: "Enable debug logging?",
|
|
2073
|
-
default: false
|
|
2074
|
-
}
|
|
2075
|
-
]);
|
|
2076
|
-
const config = {
|
|
2077
|
-
email: {
|
|
2078
|
-
email: answers.email || "noreply@example.com",
|
|
2079
|
-
password: answers.password || ""
|
|
2080
|
-
},
|
|
2081
|
-
allowedSenders: answers.allowedSenders || [],
|
|
2082
|
-
projectPath: answers.projectPath,
|
|
2083
|
-
checkInterval: answers.checkInterval || 3e4,
|
|
2084
|
-
heartbeatInterval: answers.heartbeatInterval || 3e4,
|
|
2085
|
-
debug: answers.debug,
|
|
2086
|
-
mode: answers.mode,
|
|
2087
|
-
cloudToken: answers.cloudToken,
|
|
2088
|
-
deviceName: answers.deviceName
|
|
2089
|
-
};
|
|
2090
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
2091
|
-
const fs = await import('node:fs/promises');
|
|
2092
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
2093
|
-
}
|
|
2094
|
-
const configToSave = { ...config };
|
|
2095
|
-
if (config.mode === "cloud") {
|
|
2096
|
-
configToSave.email = {
|
|
2097
|
-
email: "noreply@example.com",
|
|
2098
|
-
password: ""
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(configToSave, null, 2));
|
|
2102
|
-
console.log("\n\u2705 Configuration saved to:", CONFIG_FILE);
|
|
2103
|
-
console.log(`
|
|
2104
|
-
\u{1F4E1} Mode: ${answers.mode.toUpperCase()}`);
|
|
2105
|
-
if (answers.mode === "email" || answers.mode === "hybrid") {
|
|
2106
|
-
console.log("\n\u{1F4A1} Email control:");
|
|
2107
|
-
console.log(" Send an email to test:");
|
|
2108
|
-
console.log(` To: ${answers.email}`);
|
|
2109
|
-
console.log(" Subject: [CCJK] Test");
|
|
2110
|
-
console.log(' Body: echo "Hello CCJK!"');
|
|
2111
|
-
}
|
|
2112
|
-
if (answers.mode === "cloud" || answers.mode === "hybrid") {
|
|
2113
|
-
console.log("\n\u{1F4A1} Cloud control:");
|
|
2114
|
-
console.log(" 1. Device registered to api.claudehome.cn");
|
|
2115
|
-
console.log(" 2. Use the web interface to send commands");
|
|
2116
|
-
console.log(` 3. Device name: ${answers.deviceName}`);
|
|
2117
|
-
}
|
|
2118
|
-
console.log("\n\u{1F4A1} Next steps:");
|
|
2119
|
-
console.log(" Run: ccjk daemon start");
|
|
2120
|
-
}
|
|
2121
|
-
async function startDaemon() {
|
|
2122
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
2123
|
-
console.error("\u274C Configuration not found. Please run: ccjk daemon setup");
|
|
2124
|
-
process.exit(1);
|
|
2125
|
-
}
|
|
2126
|
-
if (existsSync(PID_FILE)) {
|
|
2127
|
-
const pid = readFileSync(PID_FILE, "utf-8").trim();
|
|
2128
|
-
console.error(`\u274C Daemon is already running (PID: ${pid})`);
|
|
2129
|
-
console.log(" Run: ccjk daemon stop");
|
|
2130
|
-
process.exit(1);
|
|
2131
|
-
}
|
|
2132
|
-
const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
2133
|
-
const daemon = new CcjkDaemon(config);
|
|
2134
|
-
const spinner = ora("Starting CCJK Daemon...").start();
|
|
2135
|
-
try {
|
|
2136
|
-
await daemon.start();
|
|
2137
|
-
spinner.succeed("CCJK Daemon started successfully");
|
|
2138
|
-
writeFileSync(PID_FILE, process.pid.toString());
|
|
2139
|
-
process.on("SIGINT", async () => {
|
|
2140
|
-
console.log("\n\u{1F6D1} Received SIGINT, stopping daemon...");
|
|
2141
|
-
await daemon.stop();
|
|
2142
|
-
if (existsSync(PID_FILE)) {
|
|
2143
|
-
const fs = require("node:fs");
|
|
2144
|
-
fs.unlinkSync(PID_FILE);
|
|
2145
|
-
}
|
|
2146
|
-
process.exit(0);
|
|
2147
|
-
});
|
|
2148
|
-
process.on("SIGTERM", async () => {
|
|
2149
|
-
console.log("\n\u{1F6D1} Received SIGTERM, stopping daemon...");
|
|
2150
|
-
await daemon.stop();
|
|
2151
|
-
if (existsSync(PID_FILE)) {
|
|
2152
|
-
const fs = require("node:fs");
|
|
2153
|
-
fs.unlinkSync(PID_FILE);
|
|
2154
|
-
}
|
|
2155
|
-
process.exit(0);
|
|
2156
|
-
});
|
|
2157
|
-
console.log("\n\u{1F4A1} Press Ctrl+C to stop the daemon");
|
|
2158
|
-
} catch (error) {
|
|
2159
|
-
spinner.fail("Failed to start daemon");
|
|
2160
|
-
console.error(error.message);
|
|
2161
|
-
process.exit(1);
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
async function stopDaemon() {
|
|
2165
|
-
if (!existsSync(PID_FILE)) {
|
|
2166
|
-
console.log("\u26A0\uFE0F Daemon is not running");
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
const pid = readFileSync(PID_FILE, "utf-8").trim();
|
|
2170
|
-
const spinner = ora(`Stopping daemon (PID: ${pid})...`).start();
|
|
2171
|
-
try {
|
|
2172
|
-
process.kill(Number.parseInt(pid), "SIGTERM");
|
|
2173
|
-
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
2174
|
-
if (existsSync(PID_FILE)) {
|
|
2175
|
-
const fs = require("node:fs");
|
|
2176
|
-
fs.unlinkSync(PID_FILE);
|
|
2177
|
-
}
|
|
2178
|
-
spinner.succeed("Daemon stopped successfully");
|
|
2179
|
-
} catch (error) {
|
|
2180
|
-
spinner.fail("Failed to stop daemon");
|
|
2181
|
-
console.error(error.message);
|
|
2182
|
-
process.exit(1);
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
async function showStatus() {
|
|
2186
|
-
console.log("\u{1F4CA} CCJK Daemon Status\n");
|
|
2187
|
-
if (!existsSync(PID_FILE)) {
|
|
2188
|
-
console.log("Status: \u26AA Not running");
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
const pid = readFileSync(PID_FILE, "utf-8").trim();
|
|
2192
|
-
try {
|
|
2193
|
-
process.kill(Number.parseInt(pid), 0);
|
|
2194
|
-
console.log("Status: \u{1F7E2} Running");
|
|
2195
|
-
console.log(`PID: ${pid}`);
|
|
2196
|
-
} catch {
|
|
2197
|
-
console.log("Status: \u{1F534} Dead (PID file exists but process not found)");
|
|
2198
|
-
console.log("Run: ccjk daemon start");
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
if (existsSync(CONFIG_FILE)) {
|
|
2202
|
-
const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
2203
|
-
console.log(`
|
|
2204
|
-
Configuration:`);
|
|
2205
|
-
console.log(` Mode: ${config.mode?.toUpperCase() || "EMAIL"}`);
|
|
2206
|
-
if (config.mode === "email" || config.mode === "hybrid") {
|
|
2207
|
-
console.log(` Email: ${config.email.email}`);
|
|
2208
|
-
console.log(` Check Interval: ${config.checkInterval}ms`);
|
|
2209
|
-
console.log(` Allowed Senders: ${config.allowedSenders.join(", ") || "None"}`);
|
|
2210
|
-
}
|
|
2211
|
-
if (config.mode === "cloud" || config.mode === "hybrid") {
|
|
2212
|
-
console.log(` Cloud API: ${config.cloudApiUrl || "https://api.claudehome.cn/api/control"}`);
|
|
2213
|
-
console.log(` Device Name: ${config.deviceName || "N/A"}`);
|
|
2214
|
-
console.log(` Heartbeat Interval: ${config.heartbeatInterval}ms`);
|
|
2215
|
-
}
|
|
2216
|
-
console.log(` Project: ${config.projectPath}`);
|
|
2217
|
-
console.log(` Debug: ${config.debug ? "Enabled" : "Disabled"}`);
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
async function showLogs() {
|
|
2221
|
-
console.log("\u{1F4CB} CCJK Daemon Logs\n");
|
|
2222
|
-
console.log("\u26A0\uFE0F Log viewing not implemented yet");
|
|
2223
|
-
console.log("\u{1F4A1} Logs are currently printed to stdout");
|
|
2224
|
-
console.log(" Run daemon in foreground to see logs: ccjk daemon start");
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
export { setupDaemon, showLogs, showStatus, startDaemon, stopDaemon };
|