diff --git a/.env.example b/.env.example index 95da56b..4abb1ab 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,14 @@ -# Claude Code Remote Email Configuration +# Claude Code Remote Email Configuration Example +# Copy this file to .env and configure with your actual values # ===== SMTP 发送邮件配置 ===== SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_SECURE=false +SMTP_PORT=465 +SMTP_SECURE=true SMTP_USER=your-email@gmail.com SMTP_PASS=your-app-password -# 发件人信息 +# 发件人信息 (可选,默认使用 SMTP_USER) EMAIL_FROM=your-email@gmail.com EMAIL_FROM_NAME=Claude Code Remote 通知系统 @@ -20,19 +21,19 @@ IMAP_PASS=your-app-password # ===== 邮件路由配置 ===== # 接收通知的邮箱地址 -EMAIL_TO=your-notification-email@gmail.com +EMAIL_TO=your-email@gmail.com # 允许发送命令的邮箱地址(安全白名单) -ALLOWED_SENDERS=your-notification-email@gmail.com +ALLOWED_SENDERS=your-email@gmail.com # ===== 系统配置 ===== -# 会话映射文件路径 (请替换为你的实际路径) -SESSION_MAP_PATH=/Users/your-username/path/to/Claude-Code-Remote/src/data/session-map.json +# 会话映射文件路径 +SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json # 运行模式:pty 或 tmux INJECTION_MODE=pty -# Claude CLI 路径(默认使用系统PATH中的claude) +# Claude CLI 路径(可选,默认使用系统PATH中的claude) CLAUDE_CLI_PATH=claude # 日志级别:debug, info, warn, error @@ -41,9 +42,55 @@ LOG_LEVEL=info # 是否记录PTY输出(调试用) PTY_OUTPUT_LOG=false +# ===== 超时配置 ===== +# 命令执行超时时间(毫秒) +COMMAND_TIMEOUT=10000 + +# SMTP 连接超时时间(毫秒) +SMTP_TIMEOUT=10000 + +# 通知超时时间(毫秒) +NOTIFICATION_TIMEOUT=3000 + +# 通知显示时间(毫秒) +NOTIFICATION_DISPLAY_TIME=10000 + # ===== 邮件模板配置 ===== # 邮件检查间隔(秒) -CHECK_INTERVAL=30 +CHECK_INTERVAL=20 # 会话超时时间(小时) -SESSION_TIMEOUT=24 \ No newline at end of file +SESSION_TIMEOUT=24 + +# ===== 测试配置(可选)===== +# 测试邮件使用的固定令牌(可选,默认动态生成) +TEST_TOKEN= + +# Gmail 应用密码(用于测试脚本,可选) +GMAIL_APP_PASSWORD= + +# ===== Gmail 配置说明 ===== +# 1. 启用两步验证: https://myaccount.google.com/security +# 2. 生成应用密码: https://myaccount.google.com/apppasswords +# 3. 将生成的16位密码填入 SMTP_PASS 和 IMAP_PASS +# 4. 确保 SMTP_PORT=465 和 SMTP_SECURE=true (推荐SSL连接) + +# ===== 其他邮件服务商配置示例 ===== +# QQ邮箱: +# SMTP_HOST=smtp.qq.com +# SMTP_PORT=587 或 465 +# IMAP_HOST=imap.qq.com +# IMAP_PORT=993 + +# 163邮箱: +# SMTP_HOST=smtp.163.com +# SMTP_PORT=587 或 465 +# IMAP_HOST=imap.163.com +# IMAP_PORT=993 + +# Outlook: +# SMTP_HOST=smtp.live.com +# SMTP_PORT=587 +# IMAP_HOST=imap-mail.outlook.com +# IMAP_PORT=993 +EOF < /dev/null \ No newline at end of file diff --git a/.gitignore b/.gitignore index 662ef49..23c36e2 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ tmp/ temp/src/data/sessions/ src/data/processed-messages.json src/data/session-map.json +src/data/sessions/*.json diff --git a/claude-control.js b/claude-control.js index 64642ae..93ef015 100644 --- a/claude-control.js +++ b/claude-control.js @@ -178,7 +178,8 @@ class RemoteControlSetup { } // If clauderun fails, try using full path command console.log('🔄 Trying full path command...'); - const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" /Users/jessytsui/.nvm/versions/node/v18.17.0/bin/claude --dangerously-skip-permissions`; + const claudePath = process.env.CLAUDE_CLI_PATH || 'claude'; + const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" ${claudePath} --dangerously-skip-permissions`; exec(fallbackCommand, (fallbackError) => { if (fallbackError) { console.log(`❌ Full path command also failed: ${fallbackError.message}`); @@ -234,7 +235,7 @@ class RemoteControlSetup { console.log('📱 Email testing:'); console.log(' Token will include session information, automatically routing to correct tmux session'); - console.log(' Recipient email: jiaxicui446@gmail.com'); + console.log(` Recipient email: ${process.env.EMAIL_TO}`); console.log(' Reply with command: echo "Remote control test"\n'); console.log('🚨 Important reminders:'); diff --git a/claude-remote.js b/claude-remote.js index 8c52d88..41a595c 100755 --- a/claude-remote.js +++ b/claude-remote.js @@ -5,6 +5,9 @@ * Main entry point for the CLI tool */ +// Load environment variables +require('dotenv').config(); + const Logger = require('./src/core/logger'); const Notifier = require('./src/core/notifier'); const ConfigManager = require('./src/core/config'); diff --git a/config/channels.json b/config/channels.json index 6100a16..ad49722 100644 --- a/config/channels.json +++ b/config/channels.json @@ -6,32 +6,7 @@ }, "email": { "type": "email", - "enabled": true, - "config": { - "smtp": { - "host": "smtp.feishu.cn", - "port": 465, - "secure": true, - "auth": { - "user": "noreply@pandalla.ai", - "pass": "kKgS3tNReRTL3RQC" - } - }, - "imap": { - "host": "imap.feishu.cn", - "port": 993, - "secure": true, - "auth": { - "user": "noreply@pandalla.ai", - "pass": "kKgS3tNReRTL3RQC" - } - }, - "from": "Claude-Code-Remote Notification System ", - "to": "jiaxicui446@gmail.com", - "template": { - "checkInterval": 30 - } - } + "enabled": true }, "discord": { "type": "chat", diff --git a/send-test-reply.js b/send-test-reply.js index a7d94a8..a31edd2 100644 --- a/send-test-reply.js +++ b/send-test-reply.js @@ -8,24 +8,45 @@ require('dotenv').config(); async function sendTestReply() { console.log('📧 Sending test email reply...\n'); - // Create test SMTP transporter (using Gmail) + // Create test SMTP transporter (using environment variables) const transporter = nodemailer.createTransport({ - service: 'gmail', + host: process.env.SMTP_HOST || 'smtp.gmail.com', + port: parseInt(process.env.SMTP_PORT) || 587, + secure: process.env.SMTP_SECURE === 'true', auth: { - user: 'jiaxicui446@gmail.com', - pass: process.env.GMAIL_APP_PASSWORD || 'your-app-password' + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS } }); - // Use latest token - const testToken = 'V5UPZ1UE'; // Latest token from session-map.json + // Generate or use test token from environment + let testToken = process.env.TEST_TOKEN; + + if (!testToken) { + // Try to read latest token from session map + try { + const sessionMapPath = process.env.SESSION_MAP_PATH || './src/data/session-map.json'; + if (require('fs').existsSync(sessionMapPath)) { + const sessionMap = JSON.parse(require('fs').readFileSync(sessionMapPath, 'utf8')); + const tokens = Object.keys(sessionMap); + testToken = tokens[tokens.length - 1]; // Use latest token + } + } catch (error) { + console.log('Could not read session map, using generated token'); + } + + // Fallback: generate a test token + if (!testToken) { + testToken = Math.random().toString(36).substr(2, 8).toUpperCase(); + } + } const mailOptions = { - from: 'jiaxicui446@gmail.com', - to: 'noreply@pandalla.ai', + from: process.env.SMTP_USER, + to: process.env.SMTP_USER, // Self-send for testing subject: `Re: [Claude-Code-Remote #${testToken}] Claude Code Task Completed - Claude-Code-Remote`, text: 'Please explain the basic principles of quantum computing', - replyTo: 'jiaxicui446@gmail.com' + replyTo: process.env.EMAIL_TO || process.env.ALLOWED_SENDERS }; try { diff --git a/src/channels/email/smtp.js b/src/channels/email/smtp.js index 7b8e008..5453b39 100644 --- a/src/channels/email/smtp.js +++ b/src/channels/email/smtp.js @@ -58,9 +58,9 @@ class EmailChannel extends NotificationChannel { pass: this.config.smtp.auth.pass }, // Add timeout settings - connectionTimeout: 10000, - greetingTimeout: 10000, - socketTimeout: 10000 + connectionTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000, + greetingTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000, + socketTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000 }); this.logger.debug('Email transporter initialized'); diff --git a/src/channels/local/desktop.js b/src/channels/local/desktop.js index 85058cc..a83d6e7 100644 --- a/src/channels/local/desktop.js +++ b/src/channels/local/desktop.js @@ -44,12 +44,12 @@ class DesktopChannel extends NotificationChannel { // Try terminal-notifier first try { const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "claude-code-remote"`; - execSync(cmd, { timeout: 3000 }); + execSync(cmd, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 }); return true; } catch (e) { // Fallback to osascript const script = `display notification "${message}" with title "${title}"`; - execSync(`osascript -e '${script}'`, { timeout: 3000 }); + execSync(`osascript -e '${script}'`, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 }); // Play sound separately this._playSound(sound); @@ -63,7 +63,9 @@ class DesktopChannel extends NotificationChannel { _sendLinux(title, message, sound) { try { - execSync(`notify-send "${title}" "${message}" -t 10000`, { timeout: 3000 }); + const notificationTimeout = parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000; + const displayTime = parseInt(process.env.NOTIFICATION_DISPLAY_TIME) || 10000; + execSync(`notify-send "${title}" "${message}" -t ${displayTime}`, { timeout: notificationTimeout }); this._playSound(sound); return true; } catch (error) { diff --git a/src/core/config.js b/src/core/config.js index 5b6eaa2..815dd90 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -58,19 +58,31 @@ class ConfigManager { }, email: { type: 'email', - enabled: false, + enabled: process.env.SMTP_USER ? true : false, config: { smtp: { - host: '', - port: 587, - secure: false, + host: process.env.SMTP_HOST || 'smtp.gmail.com', + port: parseInt(process.env.SMTP_PORT) || 587, + secure: process.env.SMTP_SECURE === 'true', auth: { - user: '', - pass: '' + user: process.env.SMTP_USER || '', + pass: process.env.SMTP_PASS || '' } }, - from: '', - to: [] + imap: { + host: process.env.IMAP_HOST || 'imap.gmail.com', + port: parseInt(process.env.IMAP_PORT) || 993, + secure: process.env.IMAP_SECURE !== 'false', + auth: { + user: process.env.IMAP_USER || process.env.SMTP_USER || '', + pass: process.env.IMAP_PASS || process.env.SMTP_PASS || '' + } + }, + from: process.env.EMAIL_FROM || `${process.env.EMAIL_FROM_NAME || 'Claude Code Remote'} <${process.env.SMTP_USER}>`, + to: process.env.EMAIL_TO || '', + template: { + checkInterval: parseInt(process.env.CHECK_INTERVAL) || 30 + } } }, discord: { @@ -125,7 +137,7 @@ class ConfigManager { try { if (fs.existsSync(this.channelsConfigPath)) { const fileChannels = JSON.parse(fs.readFileSync(this.channelsConfigPath, 'utf8')); - this._channels = { ...this._channels, ...fileChannels }; + this._channels = this._deepMerge(this._channels, fileChannels); } } catch (error) { this.logger.warn('Failed to load channels config:', error.message); diff --git a/src/relay/relay-pty.js b/src/relay/relay-pty.js index f6d70cd..535966f 100644 --- a/src/relay/relay-pty.js +++ b/src/relay/relay-pty.js @@ -89,12 +89,11 @@ function isAllowed(fromAddress) { return ALLOWED_SENDERS.some(allowed => addr.includes(allowed)); } -// Extract TaskPing token from subject +// Extract Claude-Code-Remote token from subject function extractTokenFromSubject(subject = '') { const patterns = [ - /\[TaskPing\s+#([A-Za-z0-9_-]+)\]/, - /\[TaskPing\s+([A-Za-z0-9_-]+)\]/, - /TaskPing:\s*([A-Za-z0-9_-]+)/i + /\[Claude-Code-Remote\s+#([A-Z0-9]+)\]/, + /Re:\s*\[Claude-Code-Remote\s+#([A-Z0-9]+)\]/ ]; for (const pattern of patterns) { @@ -118,7 +117,7 @@ function cleanEmailText(text = '') { line.includes('On') && line.includes('wrote:') || line.includes('Session ID:') || line.includes('Session ID:') || - line.includes('') || + line.includes(`<${process.env.SMTP_USER}>`) || line.includes('Claude-Code-Remote Notification System') || line.includes('on 2025') && line.includes('wrote:') || line.match(/^>.*/) || // Quote lines start with > @@ -161,7 +160,7 @@ function cleanEmailText(text = '') { // Skip remaining email quotes if (trimmedLine.includes('Claude-Code-Remote Notification System') || - trimmedLine.includes('') || + trimmedLine.includes(`<${process.env.SMTP_USER}>`) || trimmedLine.includes('on 2025')) { continue; } diff --git a/test-smtp.js b/test-smtp.js new file mode 100644 index 0000000..2772345 --- /dev/null +++ b/test-smtp.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node + +/** + * Test SMTP connection using environment variables + */ + +require('dotenv').config(); +const nodemailer = require('nodemailer'); + +async function testSMTP() { + console.log('🔧 Testing SMTP connection...\n'); + + const config = { + host: process.env.SMTP_HOST || 'smtp.gmail.com', + port: parseInt(process.env.SMTP_PORT) || 587, + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }; + + console.log('📋 SMTP Configuration:'); + console.log(` Host: ${config.host}`); + console.log(` Port: ${config.port}`); + console.log(` Secure: ${config.secure}`); + console.log(` User: ${config.auth.user}`); + console.log(` Pass: ${'*'.repeat(config.auth.pass?.length || 0)}\n`); + + try { + const transporter = nodemailer.createTransport(config); + + console.log('🔄 Verifying connection...'); + await transporter.verify(); + console.log('✅ SMTP connection successful!\n'); + + console.log('📧 Sending test email...'); + const info = await transporter.sendMail({ + from: process.env.EMAIL_FROM || `Claude Code Remote <${process.env.SMTP_USER}>`, + to: process.env.EMAIL_TO, + subject: 'Claude Code Remote - SMTP Test', + text: 'This is a test email from Claude Code Remote. If you receive this, SMTP is working correctly!' + }); + + console.log(`✅ Test email sent successfully!`); + console.log(`📧 Message ID: ${info.messageId}`); + console.log(`📬 To: ${process.env.EMAIL_TO}`); + + } catch (error) { + console.error('❌ SMTP test failed:', error.message); + + if (error.code === 'EAUTH') { + console.error('\n💡 Authentication failed. Please check:'); + console.error(' - Gmail App Password is correct'); + console.error(' - Two-factor authentication is enabled'); + console.error(' - Email credentials in .env file'); + } else if (error.code === 'ETIMEDOUT') { + console.error('\n💡 Connection timeout. Please check:'); + console.error(' - Internet connection'); + console.error(' - Firewall settings'); + console.error(' - SMTP host and port'); + } + } +} + +testSMTP().catch(console.error); \ No newline at end of file