renote-server 1.0.1 → 1.0.3
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/claude/chatService.js +131 -0
- package/dist/config.js +27 -4
- package/dist/http/server.js +2 -2
- package/dist/index.js +0 -0
- package/dist/terminal/terminalWebSocket.js +9 -1
- package/dist/websocket/server.js +76 -11
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claudeChatService = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
class ClaudeChatService {
|
|
7
|
+
constructor() {
|
|
8
|
+
// 存储正在运行的进程,按 sessionId 索引
|
|
9
|
+
this.runningProcesses = new Map();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 发送消息给 Claude CLI
|
|
13
|
+
* 使用 -p (print) 模式,消息通过 stdin 传入,响应通过 jsonl 文件监听获取
|
|
14
|
+
*/
|
|
15
|
+
async sendMessage(options) {
|
|
16
|
+
const { workspaceDirName, sessionId, newSessionId, message, cwd, allowedTools } = options;
|
|
17
|
+
// 确定实际使用的 sessionId
|
|
18
|
+
const effectiveSessionId = sessionId || newSessionId;
|
|
19
|
+
// 检查该会话是否已有进程在运行
|
|
20
|
+
if (effectiveSessionId && this.runningProcesses.has(effectiveSessionId)) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: 'A message is already being processed for this session',
|
|
24
|
+
sessionId: effectiveSessionId,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const args = ['-p'];
|
|
28
|
+
// 恢复现有会话
|
|
29
|
+
if (sessionId) {
|
|
30
|
+
args.push('--resume', sessionId);
|
|
31
|
+
}
|
|
32
|
+
// 新建会话并指定 ID
|
|
33
|
+
else if (newSessionId) {
|
|
34
|
+
args.push('--session-id', newSessionId);
|
|
35
|
+
}
|
|
36
|
+
// 添加允许的工具参数
|
|
37
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
38
|
+
args.push('--allowedTools', allowedTools.join(','));
|
|
39
|
+
}
|
|
40
|
+
logger_1.logger.info(`[ChatService] Sending message to Claude CLI`);
|
|
41
|
+
logger_1.logger.info(`[ChatService] Args: ${args.join(' ')}`);
|
|
42
|
+
logger_1.logger.info(`[ChatService] CWD: ${cwd || 'default'}`);
|
|
43
|
+
logger_1.logger.info(`[ChatService] Message length: ${message.length}`);
|
|
44
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
45
|
+
logger_1.logger.info(`[ChatService] Allowed tools: ${allowedTools.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
49
|
+
cwd: cwd || process.cwd(),
|
|
50
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
// 确保 Claude CLI 不会等待用户输入
|
|
54
|
+
TERM: 'dumb',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
// 记录进程
|
|
58
|
+
if (effectiveSessionId) {
|
|
59
|
+
this.runningProcesses.set(effectiveSessionId, proc);
|
|
60
|
+
}
|
|
61
|
+
// 写入消息到 stdin
|
|
62
|
+
proc.stdin.write(message);
|
|
63
|
+
proc.stdin.end();
|
|
64
|
+
// 收集输出(主要用于错误检测)
|
|
65
|
+
let stdout = '';
|
|
66
|
+
let stderr = '';
|
|
67
|
+
proc.stdout.on('data', (data) => {
|
|
68
|
+
stdout += data.toString();
|
|
69
|
+
// 可以选择性地记录 stdout,但主要响应通过 jsonl 监听获取
|
|
70
|
+
logger_1.logger.debug(`[ChatService] stdout: ${data.toString().substring(0, 200)}...`);
|
|
71
|
+
});
|
|
72
|
+
proc.stderr.on('data', (data) => {
|
|
73
|
+
stderr += data.toString();
|
|
74
|
+
logger_1.logger.warn(`[ChatService] stderr: ${data.toString()}`);
|
|
75
|
+
});
|
|
76
|
+
proc.on('close', (code) => {
|
|
77
|
+
// 移除进程记录
|
|
78
|
+
if (effectiveSessionId) {
|
|
79
|
+
this.runningProcesses.delete(effectiveSessionId);
|
|
80
|
+
}
|
|
81
|
+
if (code === 0) {
|
|
82
|
+
logger_1.logger.info(`[ChatService] Claude CLI completed successfully`);
|
|
83
|
+
resolve({
|
|
84
|
+
success: true,
|
|
85
|
+
sessionId: effectiveSessionId,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
logger_1.logger.error(`[ChatService] Claude CLI exited with code ${code}`);
|
|
90
|
+
resolve({
|
|
91
|
+
success: false,
|
|
92
|
+
error: stderr || `Exit code: ${code}`,
|
|
93
|
+
sessionId: effectiveSessionId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
proc.on('error', (err) => {
|
|
98
|
+
// 移除进程记录
|
|
99
|
+
if (effectiveSessionId) {
|
|
100
|
+
this.runningProcesses.delete(effectiveSessionId);
|
|
101
|
+
}
|
|
102
|
+
logger_1.logger.error(`[ChatService] Failed to spawn Claude CLI:`, err);
|
|
103
|
+
resolve({
|
|
104
|
+
success: false,
|
|
105
|
+
error: err.message,
|
|
106
|
+
sessionId: effectiveSessionId,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 检查指定会话是否有正在进行的消息处理
|
|
113
|
+
*/
|
|
114
|
+
isProcessing(sessionId) {
|
|
115
|
+
return this.runningProcesses.has(sessionId);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 取消正在进行的消息处理
|
|
119
|
+
*/
|
|
120
|
+
cancelMessage(sessionId) {
|
|
121
|
+
const proc = this.runningProcesses.get(sessionId);
|
|
122
|
+
if (proc) {
|
|
123
|
+
proc.kill('SIGTERM');
|
|
124
|
+
this.runningProcesses.delete(sessionId);
|
|
125
|
+
logger_1.logger.info(`[ChatService] Cancelled message processing for session ${sessionId}`);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.claudeChatService = new ClaudeChatService();
|
package/dist/config.js
CHANGED
|
@@ -4,13 +4,36 @@ exports.CONFIG = void 0;
|
|
|
4
4
|
const dotenv_1 = require("dotenv");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
(0, dotenv_1.config)();
|
|
7
|
+
/**
|
|
8
|
+
* Parse CLI arguments: --port, --host, --token, --claude-home
|
|
9
|
+
* These take precedence over environment variables.
|
|
10
|
+
*/
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = {};
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
if (arg.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
17
|
+
const key = arg.slice(2);
|
|
18
|
+
args[key] = argv[i + 1];
|
|
19
|
+
i++;
|
|
20
|
+
}
|
|
21
|
+
else if (arg.startsWith('--') && arg.includes('=')) {
|
|
22
|
+
const [key, value] = arg.slice(2).split('=');
|
|
23
|
+
args[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return args;
|
|
27
|
+
}
|
|
28
|
+
const args = parseArgs();
|
|
7
29
|
exports.CONFIG = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
30
|
+
host: args['host'] || process.env.HOST || '0.0.0.0',
|
|
31
|
+
port: parseInt(args['port'] || process.env.PORT || '9080'),
|
|
32
|
+
authToken: args['token'] || process.env.AUTH_TOKEN || '',
|
|
33
|
+
claudeHome: args['claude-home'] || process.env.CLAUDE_HOME || `${(0, os_1.homedir)()}/.claude`,
|
|
11
34
|
maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760'),
|
|
12
35
|
searchTimeout: parseInt(process.env.SEARCH_TIMEOUT || '5000'),
|
|
13
|
-
logLevel: process.env.LOG_LEVEL || 'info',
|
|
36
|
+
logLevel: args['log-level'] || process.env.LOG_LEVEL || 'info',
|
|
14
37
|
};
|
|
15
38
|
if (!exports.CONFIG.authToken) {
|
|
16
39
|
console.warn('WARNING: AUTH_TOKEN not set. Generate: openssl rand -hex 32');
|
package/dist/http/server.js
CHANGED
|
@@ -21,8 +21,8 @@ function createHttpServer() {
|
|
|
21
21
|
res.json({ token: config_1.CONFIG.authToken });
|
|
22
22
|
});
|
|
23
23
|
const httpPort = config_1.CONFIG.port + 1;
|
|
24
|
-
app.listen(httpPort, () => {
|
|
25
|
-
logger_1.logger.info(`HTTP server running on
|
|
24
|
+
app.listen(httpPort, config_1.CONFIG.host, () => {
|
|
25
|
+
logger_1.logger.info(`HTTP server running on ${config_1.CONFIG.host}:${httpPort}`);
|
|
26
26
|
});
|
|
27
27
|
return app;
|
|
28
28
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -38,6 +38,8 @@ class TerminalWebSocketHandler {
|
|
|
38
38
|
const type = (url.searchParams.get('type') || 'shell');
|
|
39
39
|
const cols = parseInt(url.searchParams.get('cols') || '80', 10);
|
|
40
40
|
const rows = parseInt(url.searchParams.get('rows') || '24', 10);
|
|
41
|
+
const claudeArgsParam = url.searchParams.get('claudeArgs');
|
|
42
|
+
const cwd = url.searchParams.get('cwd') || undefined;
|
|
41
43
|
// Validate token
|
|
42
44
|
if (!this.authManager.validateToken(token)) {
|
|
43
45
|
logger_1.logger.warn('Terminal WebSocket: invalid token');
|
|
@@ -54,7 +56,13 @@ class TerminalWebSocketHandler {
|
|
|
54
56
|
this.wsToSession.set(ws, { clientId, sessionId });
|
|
55
57
|
logger_1.logger.info(`Terminal WebSocket connected: clientId=${clientId}, sessionId=${sessionId}, type=${type}, ${cols}x${rows}`);
|
|
56
58
|
const connection = localTerminalManager_1.localTerminalManager.getOrCreateConnection(clientId);
|
|
57
|
-
const options = {
|
|
59
|
+
const options = {
|
|
60
|
+
type,
|
|
61
|
+
cols,
|
|
62
|
+
rows,
|
|
63
|
+
cwd,
|
|
64
|
+
claudeArgs: claudeArgsParam ? JSON.parse(decodeURIComponent(claudeArgsParam)) : undefined,
|
|
65
|
+
};
|
|
58
66
|
const success = connection.startTerminal(sessionId, (data) => {
|
|
59
67
|
// Send terminal output as text frame
|
|
60
68
|
if (ws.readyState === ws_1.default.OPEN) {
|
package/dist/websocket/server.js
CHANGED
|
@@ -16,6 +16,7 @@ const sessionBrowser_1 = require("../claude/sessionBrowser");
|
|
|
16
16
|
const terminal_1 = require("../terminal");
|
|
17
17
|
const git_1 = require("../git");
|
|
18
18
|
const terminalWebSocket_1 = require("../terminal/terminalWebSocket");
|
|
19
|
+
const chatService_1 = require("../claude/chatService");
|
|
19
20
|
class WebSocketServer {
|
|
20
21
|
constructor() {
|
|
21
22
|
this.clients = new Map();
|
|
@@ -27,51 +28,60 @@ class WebSocketServer {
|
|
|
27
28
|
this.gitHandler = new git_1.GitHandler(this.send.bind(this));
|
|
28
29
|
// Handle HTTP upgrade requests
|
|
29
30
|
server.on('upgrade', (request, socket, head) => {
|
|
31
|
+
const clientIp = request.socket.remoteAddress;
|
|
32
|
+
const clientPort = request.socket.remotePort;
|
|
33
|
+
logger_1.logger.info(`[WS Upgrade] Request from ${clientIp}:${clientPort}, URL: ${request.url}`);
|
|
30
34
|
// Route /terminal to terminal direct WebSocket handler
|
|
31
35
|
if (terminalWebSocket_1.terminalWebSocketHandler.shouldHandle(request)) {
|
|
36
|
+
logger_1.logger.info(`[WS Upgrade] Routing to terminal handler`);
|
|
32
37
|
const terminalWss = new ws_1.default.Server({ noServer: true });
|
|
33
38
|
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
|
39
|
+
logger_1.logger.info(`[WS Upgrade] Terminal upgrade complete`);
|
|
34
40
|
terminalWebSocket_1.terminalWebSocketHandler.handleConnection(ws, request);
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
43
|
else {
|
|
38
44
|
// Default: main WebSocket for JSON-RPC style messages
|
|
45
|
+
logger_1.logger.info(`[WS Upgrade] Routing to main handler`);
|
|
39
46
|
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
47
|
+
logger_1.logger.info(`[WS Upgrade] Main upgrade complete`);
|
|
40
48
|
this.wss.emit('connection', ws, request);
|
|
41
49
|
});
|
|
42
50
|
}
|
|
43
51
|
});
|
|
44
52
|
this.setupWebSocket();
|
|
45
|
-
server.listen(config_1.CONFIG.port, () => {
|
|
46
|
-
logger_1.logger.info(`WebSocket server running on
|
|
47
|
-
logger_1.logger.info(` - Main API: ws
|
|
48
|
-
logger_1.logger.info(` - Terminal direct: ws
|
|
53
|
+
server.listen(config_1.CONFIG.port, config_1.CONFIG.host, () => {
|
|
54
|
+
logger_1.logger.info(`WebSocket server running on ${config_1.CONFIG.host}:${config_1.CONFIG.port}`);
|
|
55
|
+
logger_1.logger.info(` - Main API: ws://${config_1.CONFIG.host}:${config_1.CONFIG.port}/`);
|
|
56
|
+
logger_1.logger.info(` - Terminal direct: ws://${config_1.CONFIG.host}:${config_1.CONFIG.port}/terminal`);
|
|
49
57
|
});
|
|
50
58
|
}
|
|
51
59
|
setupWebSocket() {
|
|
52
|
-
this.wss.on('connection', (ws) => {
|
|
53
|
-
|
|
60
|
+
this.wss.on('connection', (ws, request) => {
|
|
61
|
+
const clientIp = request?.socket?.remoteAddress || 'unknown';
|
|
62
|
+
logger_1.logger.info(`[WS Connection] New client from ${clientIp}`);
|
|
54
63
|
ws.on('message', async (data) => {
|
|
55
64
|
try {
|
|
56
65
|
const message = JSON.parse(data.toString());
|
|
66
|
+
logger_1.logger.info(`[WS Message] Type: ${message.type}, from ${clientIp}`);
|
|
57
67
|
await this.handleMessage(ws, message);
|
|
58
68
|
}
|
|
59
69
|
catch (error) {
|
|
60
|
-
logger_1.logger.error(
|
|
70
|
+
logger_1.logger.error(`[WS Message] Parse error from ${clientIp}:`, error);
|
|
61
71
|
this.sendError(ws, 'Invalid message format');
|
|
62
72
|
}
|
|
63
73
|
});
|
|
64
|
-
ws.on('close', () => {
|
|
74
|
+
ws.on('close', (code, reason) => {
|
|
65
75
|
const clientId = this.getClientId(ws);
|
|
76
|
+
logger_1.logger.info(`[WS Close] Client ${clientId || 'unknown'} closed with code ${code}, reason: ${reason?.toString() || 'none'}`);
|
|
66
77
|
if (clientId) {
|
|
67
78
|
this.terminalHandler.cleanup(clientId);
|
|
68
79
|
(0, sessionBrowser_1.unwatchSession)(clientId);
|
|
69
80
|
this.clients.delete(clientId);
|
|
70
|
-
logger_1.logger.info(`Client ${clientId} disconnected`);
|
|
71
81
|
}
|
|
72
82
|
});
|
|
73
83
|
ws.on('error', (error) => {
|
|
74
|
-
logger_1.logger.error('
|
|
84
|
+
logger_1.logger.error('[WS Error]:', error);
|
|
75
85
|
});
|
|
76
86
|
});
|
|
77
87
|
}
|
|
@@ -126,6 +136,9 @@ class WebSocketServer {
|
|
|
126
136
|
case 'get_session_folder_info':
|
|
127
137
|
await this.handleGetSessionFolderInfo(ws, message);
|
|
128
138
|
break;
|
|
139
|
+
case 'send_claude_message':
|
|
140
|
+
await this.handleSendClaudeMessage(ws, message);
|
|
141
|
+
break;
|
|
129
142
|
default:
|
|
130
143
|
const clientId = this.getClientId(ws);
|
|
131
144
|
if (!clientId) {
|
|
@@ -465,7 +478,58 @@ class WebSocketServer {
|
|
|
465
478
|
this.sendError(ws, `Failed to get session folder info: ${error}`);
|
|
466
479
|
}
|
|
467
480
|
}
|
|
481
|
+
async handleSendClaudeMessage(ws, message) {
|
|
482
|
+
const clientId = this.getClientId(ws);
|
|
483
|
+
if (!clientId) {
|
|
484
|
+
this.sendError(ws, 'Not authenticated');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const { data } = message;
|
|
489
|
+
if (!data || !data.workspaceDirName || !data.message) {
|
|
490
|
+
this.sendError(ws, 'workspaceDirName and message are required');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const { workspaceDirName, sessionId, newSessionId, message: userMessage, allowedTools } = data;
|
|
494
|
+
// 解码 workspaceDirName 为实际路径
|
|
495
|
+
const cwd = '/' + workspaceDirName.replace(/-/g, '/');
|
|
496
|
+
logger_1.logger.info(`[SendClaudeMessage] Sending message to Claude CLI`);
|
|
497
|
+
logger_1.logger.info(`[SendClaudeMessage] CWD: ${cwd}`);
|
|
498
|
+
logger_1.logger.info(`[SendClaudeMessage] SessionId: ${sessionId || 'none'}`);
|
|
499
|
+
logger_1.logger.info(`[SendClaudeMessage] NewSessionId: ${newSessionId || 'none'}`);
|
|
500
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
501
|
+
logger_1.logger.info(`[SendClaudeMessage] AllowedTools: ${allowedTools.join(', ')}`);
|
|
502
|
+
}
|
|
503
|
+
// 调用 chatService 发送消息
|
|
504
|
+
const result = await chatService_1.claudeChatService.sendMessage({
|
|
505
|
+
workspaceDirName,
|
|
506
|
+
sessionId,
|
|
507
|
+
newSessionId,
|
|
508
|
+
message: userMessage,
|
|
509
|
+
cwd,
|
|
510
|
+
allowedTools,
|
|
511
|
+
});
|
|
512
|
+
// 响应客户端
|
|
513
|
+
this.send(ws, {
|
|
514
|
+
type: 'send_claude_message_response',
|
|
515
|
+
data: {
|
|
516
|
+
success: result.success,
|
|
517
|
+
error: result.error,
|
|
518
|
+
sessionId: result.sessionId,
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
// 如果成功,响应会通过现有的 watchSession 机制推送
|
|
522
|
+
if (result.success) {
|
|
523
|
+
logger_1.logger.info(`[SendClaudeMessage] Message sent successfully, responses will be pushed via watchSession`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
logger_1.logger.error('Error sending Claude message:', error);
|
|
528
|
+
this.sendError(ws, `Failed to send message: ${error}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
468
531
|
async handleAuth(ws, token) {
|
|
532
|
+
logger_1.logger.info(`[Auth] Validating token: "${token ? '***' : '(empty)'}"`);
|
|
469
533
|
if (this.authManager.validateToken(token)) {
|
|
470
534
|
const clientId = this.authManager.generateClientId();
|
|
471
535
|
this.clients.set(clientId, ws);
|
|
@@ -474,9 +538,10 @@ class WebSocketServer {
|
|
|
474
538
|
type: 'auth_success',
|
|
475
539
|
data: { clientId }
|
|
476
540
|
});
|
|
477
|
-
logger_1.logger.info(`Client ${clientId} authenticated`);
|
|
541
|
+
logger_1.logger.info(`[Auth] Client ${clientId} authenticated successfully`);
|
|
478
542
|
}
|
|
479
543
|
else {
|
|
544
|
+
logger_1.logger.warn(`[Auth] Invalid token rejected`);
|
|
480
545
|
this.sendError(ws, 'Invalid token');
|
|
481
546
|
ws.close();
|
|
482
547
|
}
|