Convert entire project to English version
- Translated all Chinese comments and documentation to professional English - Converted all console messages and user interface text to English - Updated README.md with comprehensive English documentation - Translated configuration files and templates to English - Converted error messages and status indicators to English - Maintained all functionality while improving international accessibility - Used consistent technical terminology throughout the codebase Major changes: - README.md: Complete English rewrite with detailed usage instructions - All .js files: Chinese comments → English technical documentation - Configuration files: Chinese labels → English descriptions - User messages: Chinese prompts → English user interface - Error handling: Chinese errors → English error messages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b8f79ea2cc
commit
5d7601f593
200
README.md
200
README.md
|
|
@ -1,41 +1,41 @@
|
|||
# TaskPing - 智能邮件自动化 Claude Code 助手
|
||||
# TaskPing - Intelligent Email Automation Assistant for Claude Code
|
||||
|
||||
TaskPing 是一个智能的邮件自动化工具,实现了 Claude Code 与邮件系统的深度集成。通过监听邮件回复,自动将回复内容输入到对应的 Claude Code 会话中执行,让你可以在任何地方通过邮件远程控制 Claude Code。
|
||||
TaskPing is an intelligent email automation tool that deeply integrates Claude Code with email systems. By monitoring email replies, it automatically inputs reply content into corresponding Claude Code sessions for execution, allowing you to remotely control Claude Code from anywhere via email.
|
||||
|
||||
## 🚀 核心功能
|
||||
## 🚀 Core Features
|
||||
|
||||
### 📧 智能邮件通知
|
||||
- **自动检测**: 基于 Claude Code 官方 hooks 机制,自动识别任务完成和等待输入状态
|
||||
- **实时通知**: 任务完成时自动发送邮件,包含完整的用户问题和 Claude 回复内容
|
||||
- **会话绑定**: 邮件与特定的 tmux 会话绑定,确保回复到正确的 Claude Code 窗口
|
||||
### 📧 Smart Email Notifications
|
||||
- **Auto Detection**: Based on Claude Code official hooks mechanism, automatically identifies task completion and waiting input states
|
||||
- **Real-time Notifications**: Automatically sends emails when tasks complete, including complete user questions and Claude responses
|
||||
- **Session Binding**: Emails are bound to specific tmux sessions, ensuring replies go to the correct Claude Code window
|
||||
|
||||
### 🔄 邮件回复自动执行
|
||||
- **远程控制**: 直接回复邮件,内容自动输入到对应的 Claude Code 会话中
|
||||
- **智能注入**: 自动检测 tmux 会话状态,将命令精确注入到正确的窗口
|
||||
- **防重复处理**: 实现邮件去重机制,避免重复处理同一封邮件
|
||||
### 🔄 Email Reply Auto-Execution
|
||||
- **Remote Control**: Directly reply to emails, content automatically inputs into corresponding Claude Code sessions
|
||||
- **Smart Injection**: Automatically detects tmux session state, precisely injects commands into correct windows
|
||||
- **Duplicate Prevention**: Implements email deduplication mechanism to avoid processing the same email twice
|
||||
|
||||
### 🛡️ 稳定性保障
|
||||
- **单实例运行**: 确保只有一个邮件监听进程运行,避免重复处理
|
||||
- **状态管理**: 完善的会话状态跟踪和错误恢复机制
|
||||
- **安全验证**: 邮件来源验证,确保只处理授权用户的回复
|
||||
### 🛡️ Stability Assurance
|
||||
- **Single Instance**: Ensures only one email monitoring process runs, avoiding duplicate processing
|
||||
- **State Management**: Comprehensive session state tracking and error recovery mechanisms
|
||||
- **Security Verification**: Email source verification, ensures only authorized user replies are processed
|
||||
|
||||
## 📦 快速安装
|
||||
## 📦 Quick Installation
|
||||
|
||||
### 1. 克隆项目
|
||||
### 1. Clone Project
|
||||
```bash
|
||||
git clone https://github.com/your-username/TaskPing.git
|
||||
git clone https://github.com/JessyTsui/TaskPing.git
|
||||
cd TaskPing
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 配置邮箱
|
||||
### 2. Configure Email
|
||||
```bash
|
||||
npm run config
|
||||
```
|
||||
按照提示配置你的邮箱信息(SMTP 和 IMAP)。
|
||||
Follow prompts to configure your email information (SMTP and IMAP).
|
||||
|
||||
### 3. 配置 Claude Code 钩子
|
||||
将以下内容添加到 `~/.claude/settings.json` 的 `hooks` 部分:
|
||||
### 3. Configure Claude Code Hooks
|
||||
Add the following content to the `hooks` section of `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -60,155 +60,155 @@ npm run config
|
|||
}
|
||||
```
|
||||
|
||||
### 4. 全局安装 claude-control 命令
|
||||
### 4. Install Global claude-control Command
|
||||
```bash
|
||||
node install-global.js
|
||||
```
|
||||
|
||||
### 5. 启动邮件监听服务
|
||||
### 5. Start Email Monitoring Service
|
||||
```bash
|
||||
npm run relay:pty
|
||||
```
|
||||
|
||||
## 🎮 使用方法
|
||||
## 🎮 Usage
|
||||
|
||||
### 1. 创建 Claude Code 会话
|
||||
### 1. Create Claude Code Session
|
||||
```bash
|
||||
# 在任何目录下都可以运行
|
||||
# Can run from any directory
|
||||
claude-control --session project-name
|
||||
```
|
||||
|
||||
### 2. 正常使用 Claude Code
|
||||
在 tmux 会话中正常与 Claude 对话:
|
||||
### 2. Use Claude Code Normally
|
||||
Have normal conversations with Claude in tmux session:
|
||||
```
|
||||
> 请帮我分析这个项目的代码结构
|
||||
> Please help me analyze the code structure of this project
|
||||
|
||||
Claude 回复...
|
||||
Claude responds...
|
||||
```
|
||||
|
||||
### 3. 自动邮件通知
|
||||
当 Claude 完成任务时,你会收到包含完整对话内容的邮件通知。
|
||||
### 3. Automatic Email Notifications
|
||||
When Claude completes tasks, you'll receive email notifications containing complete conversation content.
|
||||
|
||||
### 4. 邮件回复控制
|
||||
直接回复邮件,输入下一个指令:
|
||||
### 4. Email Reply Control
|
||||
Directly reply to emails with your next instruction:
|
||||
```
|
||||
请继续优化代码性能
|
||||
Please continue optimizing code performance
|
||||
```
|
||||
|
||||
### 5. 自动执行
|
||||
你的回复会自动注入到对应的 Claude Code 会话中并执行。
|
||||
### 5. Automatic Execution
|
||||
Your reply will be automatically injected into the corresponding Claude Code session and executed.
|
||||
|
||||
## 🔧 项目架构
|
||||
## 🔧 Project Architecture
|
||||
|
||||
```
|
||||
TaskPing/
|
||||
├── src/
|
||||
│ ├── channels/email/
|
||||
│ │ └── smtp.js # SMTP 邮件发送
|
||||
│ │ └── smtp.js # SMTP email sending
|
||||
│ ├── core/
|
||||
│ │ ├── config.js # 配置管理
|
||||
│ │ ├── logger.js # 日志系统
|
||||
│ │ └── notifier.js # 通知协调器
|
||||
│ │ ├── config.js # Configuration management
|
||||
│ │ ├── logger.js # Logging system
|
||||
│ │ └── notifier.js # Notification coordinator
|
||||
│ ├── data/
|
||||
│ │ ├── session-map.json # 会话映射表
|
||||
│ │ └── processed-messages.json # 已处理邮件记录
|
||||
│ │ ├── session-map.json # Session mapping table
|
||||
│ │ └── processed-messages.json # Processed email records
|
||||
│ ├── relay/
|
||||
│ │ └── relay-pty.js # 邮件监听和 PTY 注入服务
|
||||
│ │ └── relay-pty.js # Email monitoring and PTY injection service
|
||||
│ └── utils/
|
||||
│ └── tmux-monitor.js # Tmux 会话监控
|
||||
├── taskping.js # 主入口文件
|
||||
├── claude-control.js # Claude Code 会话管理
|
||||
├── start-relay-pty.js # 邮件监听服务启动器
|
||||
└── install-global.js # 全局安装脚本
|
||||
│ └── tmux-monitor.js # Tmux session monitoring
|
||||
├── taskping.js # Main entry file
|
||||
├── claude-control.js # Claude Code session management
|
||||
├── start-relay-pty.js # Email monitoring service starter
|
||||
└── install-global.js # Global installation script
|
||||
```
|
||||
|
||||
## 🛠️ 核心技术实现
|
||||
## 🛠️ Core Technical Implementation
|
||||
|
||||
### 邮件监听与处理
|
||||
- 使用 `node-imap` 监听 IMAP 邮箱新邮件
|
||||
- 实现邮件去重机制(基于 UID、messageId 和内容哈希)
|
||||
- 异步事件处理,避免竞态条件
|
||||
### Email Monitoring and Processing
|
||||
- Uses `node-imap` to monitor IMAP mailbox for new emails
|
||||
- Implements email deduplication mechanism (based on UID, messageId, and content hash)
|
||||
- Asynchronous event handling to avoid race conditions
|
||||
|
||||
### 会话管理
|
||||
- Tmux 会话自动检测和命令注入
|
||||
- 会话状态持久化存储
|
||||
- 支持多会话并发处理
|
||||
### Session Management
|
||||
- Tmux session auto-detection and command injection
|
||||
- Session state persistent storage
|
||||
- Support for concurrent multi-session processing
|
||||
|
||||
### 通知系统
|
||||
- 自动捕获当前 tmux 会话的用户问题和 Claude 回复
|
||||
- 生成包含完整对话内容的邮件通知
|
||||
- 支持多种通知渠道(桌面通知、邮件等)
|
||||
### Notification System
|
||||
- Automatically captures current tmux session's user questions and Claude responses
|
||||
- Generates email notifications containing complete conversation content
|
||||
- Supports multiple notification channels (desktop notifications, email, etc.)
|
||||
|
||||
## 🔍 故障排除
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### 邮件重复处理问题
|
||||
确保只运行一个邮件监听进程:
|
||||
### Email Duplicate Processing Issue
|
||||
Ensure only one email monitoring process is running:
|
||||
```bash
|
||||
# 检查运行状态
|
||||
# Check running status
|
||||
ps aux | grep relay-pty
|
||||
|
||||
# 停止所有进程
|
||||
# Stop all processes
|
||||
pkill -f relay-pty
|
||||
|
||||
# 重新启动
|
||||
# Restart
|
||||
npm run relay:pty
|
||||
```
|
||||
|
||||
### 命令注入失败
|
||||
检查 tmux 会话状态:
|
||||
### Command Injection Failure
|
||||
Check tmux session status:
|
||||
```bash
|
||||
# 查看所有会话
|
||||
# View all sessions
|
||||
tmux list-sessions
|
||||
|
||||
# 检查会话内容
|
||||
# Check session content
|
||||
tmux capture-pane -t session-name -p
|
||||
```
|
||||
|
||||
### 邮件配置问题
|
||||
测试邮件连接:
|
||||
### Email Configuration Issues
|
||||
Test email connection:
|
||||
```bash
|
||||
# 测试 SMTP
|
||||
# Test SMTP
|
||||
node -e "
|
||||
const config = require('./config/user.json');
|
||||
console.log('SMTP Config:', config.email.config.smtp);
|
||||
"
|
||||
|
||||
# 测试 IMAP
|
||||
# Test IMAP
|
||||
node -e "
|
||||
const config = require('./config/user.json');
|
||||
console.log('IMAP Config:', config.email.config.imap);
|
||||
"
|
||||
```
|
||||
|
||||
## 🎯 使用场景
|
||||
## 🎯 Use Cases
|
||||
|
||||
### 远程编程工作流
|
||||
1. 在办公室启动一个 Claude Code 代码审查任务
|
||||
2. 下班回家,收到邮件"代码审查完成,发现3个问题"
|
||||
3. 回复邮件"请修复第一个问题"
|
||||
4. Claude 自动开始修复,完成后再次发送邮件通知
|
||||
5. 继续回复邮件进行下一步操作
|
||||
### Remote Programming Workflow
|
||||
1. Start a Claude Code code review task at the office
|
||||
2. Go home, receive email "Code review completed, found 3 issues"
|
||||
3. Reply to email "Please fix the first issue"
|
||||
4. Claude automatically starts fixing, sends email notification when complete
|
||||
5. Continue replying to emails for next steps
|
||||
|
||||
### 长时间任务监控
|
||||
1. 启动大型项目重构任务
|
||||
2. Claude 分步骤完成各个模块
|
||||
3. 每个阶段完成都发送邮件通知进度
|
||||
4. 通过邮件回复指导下一步方向
|
||||
### Long-running Task Monitoring
|
||||
1. Start large project refactoring task
|
||||
2. Claude completes modules step by step
|
||||
3. Each stage completion sends email notification of progress
|
||||
4. Guide next steps through email replies
|
||||
|
||||
## 🤝 贡献指南
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork 本项目
|
||||
2. 创建功能分支:`git checkout -b feature/new-feature`
|
||||
3. 提交更改:`git commit -am 'Add new feature'`
|
||||
4. 推送分支:`git push origin feature/new-feature`
|
||||
5. 提交 Pull Request
|
||||
1. Fork this project
|
||||
2. Create feature branch: `git checkout -b feature/new-feature`
|
||||
3. Commit changes: `git commit -am 'Add new feature'`
|
||||
4. Push branch: `git push origin feature/new-feature`
|
||||
5. Submit Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
## 📄 License
|
||||
|
||||
本项目采用 MIT License 开源协议。
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
---
|
||||
|
||||
**让 Claude Code 工作流程更加智能高效!**
|
||||
**Make Claude Code workflows smarter and more efficient!**
|
||||
|
||||
如果这个项目对你有帮助,请给我们一个 ⭐!
|
||||
If this project helps you, please give us a ⭐!
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing 无人值守远程控制设置助手
|
||||
* TaskPing Unattended Remote Control Setup Assistant
|
||||
*/
|
||||
|
||||
const { exec, spawn } = require('child_process');
|
||||
|
|
@ -59,51 +59,51 @@ class RemoteControlSetup {
|
|||
}
|
||||
|
||||
async setup() {
|
||||
console.log('🚀 TaskPing 无人值守远程控制设置\n');
|
||||
console.log('🎯 目标: 人在外面用手机→家中电脑Claude Code自动执行命令\n');
|
||||
console.log('🚀 TaskPing Unattended Remote Control Setup\n');
|
||||
console.log('🎯 Goal: Remote access via mobile phone → Home computer Claude Code automatically executes commands\n');
|
||||
|
||||
try {
|
||||
// 1. 检查tmux
|
||||
// 1. Check tmux
|
||||
await this.checkAndInstallTmux();
|
||||
|
||||
// 2. 检查Claude CLI
|
||||
// 2. Check Claude CLI
|
||||
await this.checkClaudeCLI();
|
||||
|
||||
// 3. 设置Claude tmux会话
|
||||
// 3. Setup Claude tmux session
|
||||
await this.setupClaudeSession();
|
||||
|
||||
// 4. 会话创建完成
|
||||
console.log('\n4️⃣ 会话创建完成');
|
||||
// 4. Session creation complete
|
||||
console.log('\n4️⃣ Session creation complete');
|
||||
|
||||
// 5. 提供使用指南
|
||||
// 5. Provide usage guide
|
||||
this.showUsageGuide();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 设置过程中发生错误:', error.message);
|
||||
console.error('❌ Error occurred during setup:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async checkAndInstallTmux() {
|
||||
console.log('1️⃣ 检查tmux安装状态...');
|
||||
console.log('1️⃣ Checking tmux installation status...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec('which tmux', (error, stdout) => {
|
||||
if (error) {
|
||||
console.log('❌ tmux未安装');
|
||||
console.log('📦 正在安装tmux...');
|
||||
console.log('❌ tmux not installed');
|
||||
console.log('📦 Installing tmux...');
|
||||
|
||||
exec('brew install tmux', (installError, installStdout, installStderr) => {
|
||||
if (installError) {
|
||||
console.log('❌ tmux安装失败,请手动安装:');
|
||||
console.log('❌ tmux installation failed, please install manually:');
|
||||
console.log(' brew install tmux');
|
||||
console.log(' 或从 https://github.com/tmux/tmux 下载');
|
||||
console.log(' or download from https://github.com/tmux/tmux');
|
||||
} else {
|
||||
console.log('✅ tmux安装成功');
|
||||
console.log('✅ tmux installation successful');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
console.log(`✅ tmux已安装: ${stdout.trim()}`);
|
||||
console.log(`✅ tmux already installed: ${stdout.trim()}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
@ -111,21 +111,21 @@ class RemoteControlSetup {
|
|||
}
|
||||
|
||||
async checkClaudeCLI() {
|
||||
console.log('\n2️⃣ 检查Claude CLI状态...');
|
||||
console.log('\n2️⃣ Checking Claude CLI status...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec('which claude', (error, stdout) => {
|
||||
if (error) {
|
||||
console.log('❌ Claude CLI未找到');
|
||||
console.log('📦 请安装Claude CLI:');
|
||||
console.log('❌ Claude CLI not found');
|
||||
console.log('📦 Please install Claude CLI:');
|
||||
console.log(' npm install -g @anthropic-ai/claude-code');
|
||||
} else {
|
||||
console.log(`✅ Claude CLI已安装: ${stdout.trim()}`);
|
||||
console.log(`✅ Claude CLI installed: ${stdout.trim()}`);
|
||||
|
||||
// 检查版本
|
||||
// Check version
|
||||
exec('claude --version', (versionError, versionStdout) => {
|
||||
if (!versionError) {
|
||||
console.log(`📋 版本: ${versionStdout.trim()}`);
|
||||
console.log(`📋 Version: ${versionStdout.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -135,16 +135,16 @@ class RemoteControlSetup {
|
|||
}
|
||||
|
||||
async setupClaudeSession() {
|
||||
console.log('\n3️⃣ 设置Claude tmux会话...');
|
||||
console.log('\n3️⃣ Setting up Claude tmux session...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 检查是否已有会话
|
||||
// Check if session already exists
|
||||
exec(`tmux has-session -t ${this.sessionName} 2>/dev/null`, (checkError) => {
|
||||
if (!checkError) {
|
||||
console.log('⚠️ Claude tmux会话已存在');
|
||||
console.log('🔄 是否重新创建会话? (会杀死现有会话)');
|
||||
console.log('⚠️ Claude tmux session already exists');
|
||||
console.log('🔄 Recreating session? (will kill existing session)');
|
||||
|
||||
// 简单起见,直接重建
|
||||
// For simplicity, recreate directly
|
||||
this.killAndCreateSession(resolve);
|
||||
} else {
|
||||
this.createNewSession(resolve);
|
||||
|
|
@ -162,109 +162,109 @@ class RemoteControlSetup {
|
|||
}
|
||||
|
||||
createNewSession(resolve) {
|
||||
// 使用TaskPing主目录作为工作目录
|
||||
// Use TaskPing home directory as working directory
|
||||
const workingDir = this.taskpingHome;
|
||||
const command = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" clauderun`;
|
||||
|
||||
console.log(`🚀 创建Claude tmux会话: ${this.sessionName}`);
|
||||
console.log(`📁 工作目录: ${workingDir}`);
|
||||
console.log(`💡 使用便捷命令: clauderun (等同于 claude --dangerously-skip-permissions)`);
|
||||
console.log(`🚀 Creating Claude tmux session: ${this.sessionName}`);
|
||||
console.log(`📁 Working directory: ${workingDir}`);
|
||||
console.log(`💡 Using convenience command: clauderun (equivalent to claude --dangerously-skip-permissions)`);
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`❌ 会话创建失败: ${error.message}`);
|
||||
console.log(`❌ Session creation failed: ${error.message}`);
|
||||
if (stderr) {
|
||||
console.log(`错误详情: ${stderr}`);
|
||||
console.log(`Error details: ${stderr}`);
|
||||
}
|
||||
// 如果clauderun失败,尝试使用完整路径命令
|
||||
console.log('🔄 尝试使用完整路径命令...');
|
||||
// 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`;
|
||||
exec(fallbackCommand, (fallbackError) => {
|
||||
if (fallbackError) {
|
||||
console.log(`❌ 完整路径命令也失败: ${fallbackError.message}`);
|
||||
console.log(`❌ Full path command also failed: ${fallbackError.message}`);
|
||||
} else {
|
||||
console.log('✅ Claude tmux会话创建成功 (使用完整路径)');
|
||||
console.log(`📺 查看会话: tmux attach -t ${this.sessionName}`);
|
||||
console.log(`🔚 退出会话: Ctrl+B, D (不会关闭Claude)`);
|
||||
console.log('✅ Claude tmux session created successfully (using full path)');
|
||||
console.log(`📺 View session: tmux attach -t ${this.sessionName}`);
|
||||
console.log(`🔚 Exit session: Ctrl+B, D (won't close Claude)`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
console.log('✅ Claude tmux会话创建成功');
|
||||
console.log(`📺 查看会话: tmux attach -t ${this.sessionName}`);
|
||||
console.log(`🔚 退出会话: Ctrl+B, D (不会关闭Claude)`);
|
||||
console.log('✅ Claude tmux session created successfully');
|
||||
console.log(`📺 View session: tmux attach -t ${this.sessionName}`);
|
||||
console.log(`🔚 Exit session: Ctrl+B, D (won't close Claude)`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async testRemoteInjection() {
|
||||
console.log('\n💡 会话已就绪,可以开始使用');
|
||||
console.log('📋 Claude Code正在等待您的指令');
|
||||
console.log('🔧 如需测试注入功能,请使用单独的测试脚本');
|
||||
console.log('\n💡 Session is ready, you can start using it');
|
||||
console.log('📋 Claude Code is waiting for your instructions');
|
||||
console.log('🔧 To test injection functionality, please use separate test script');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showUsageGuide() {
|
||||
console.log('\n🎉 设置完成!无人值守远程控制已就绪\n');
|
||||
console.log('\n🎉 Setup complete! Unattended remote control is ready\n');
|
||||
|
||||
console.log('🎯 新功能: clauderun 便捷命令');
|
||||
console.log(' 现在可以使用 clauderun 代替 claude --dangerously-skip-permissions');
|
||||
console.log(' 更清晰的Claude Code启动方式\n');
|
||||
console.log('🎯 New feature: clauderun convenience command');
|
||||
console.log(' You can now use clauderun instead of claude --dangerously-skip-permissions');
|
||||
console.log(' Clearer Claude Code startup method\n');
|
||||
|
||||
console.log('📋 使用流程:');
|
||||
console.log('1. 🏠 在家启动邮件监听: npm run relay:pty');
|
||||
console.log('2. 🚪 出门时Claude继续在tmux中运行');
|
||||
console.log('3. 📱 手机收到TaskPing邮件通知');
|
||||
console.log('4. 💬 手机回复邮件输入命令');
|
||||
console.log('5. 🤖 家中Claude自动接收并执行命令');
|
||||
console.log('6. 🔄 循环上述过程,完全无人值守\n');
|
||||
console.log('📋 Usage workflow:');
|
||||
console.log('1. 🏠 Start email monitoring at home: npm run relay:pty');
|
||||
console.log('2. 🚪 When going out, Claude continues running in tmux');
|
||||
console.log('3. 📱 Receive TaskPing email notifications on mobile');
|
||||
console.log('4. 💬 Reply to email with commands on mobile');
|
||||
console.log('5. 🤖 Claude at home automatically receives and executes commands');
|
||||
console.log('6. 🔄 Repeat above process, completely unattended\n');
|
||||
|
||||
console.log('🔧 管理命令:');
|
||||
console.log(` 查看Claude会话: tmux attach -t ${this.sessionName}`);
|
||||
console.log(` 退出会话(不关闭): Ctrl+B, D`);
|
||||
console.log(` 杀死会话: tmux kill-session -t ${this.sessionName}`);
|
||||
console.log(` 查看所有会话: tmux list-sessions\n`);
|
||||
console.log('🔧 Management commands:');
|
||||
console.log(` View Claude session: tmux attach -t ${this.sessionName}`);
|
||||
console.log(` Exit session (without closing): Ctrl+B, D`);
|
||||
console.log(` Kill session: tmux kill-session -t ${this.sessionName}`);
|
||||
console.log(` View all sessions: tmux list-sessions\n`);
|
||||
|
||||
console.log('🎛️ 多会话支持:');
|
||||
console.log(' 创建自定义会话: node claude-control.js --session my-project');
|
||||
console.log(' 创建多个会话: node claude-control.js --session frontend');
|
||||
console.log('🎛️ Multi-session support:');
|
||||
console.log(' Create custom session: node claude-control.js --session my-project');
|
||||
console.log(' Create multiple sessions: node claude-control.js --session frontend');
|
||||
console.log(' node claude-control.js --session backend');
|
||||
console.log(' 邮件回复会自动路由到对应的会话\n');
|
||||
console.log(' Email replies will automatically route to corresponding session\n');
|
||||
|
||||
console.log('📱 邮件测试:');
|
||||
console.log(' Token将包含会话信息,自动路由到正确的tmux会话');
|
||||
console.log(' 收件邮箱: jiaxicui446@gmail.com');
|
||||
console.log(' 回复邮件输入: echo "远程控制测试"\n');
|
||||
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(' Reply with command: echo "Remote control test"\n');
|
||||
|
||||
console.log('🚨 重要提醒:');
|
||||
console.log('- Claude会话在tmux中持续运行,断网重连也不会中断');
|
||||
console.log('- 邮件监听服务需要保持运行状态');
|
||||
console.log('- 家中电脑需要保持开机和网络连接');
|
||||
console.log('- 手机可以从任何地方发送邮件命令');
|
||||
console.log('- 支持同时运行多个不同项目的Claude会话\n');
|
||||
console.log('🚨 Important reminders:');
|
||||
console.log('- Claude session runs continuously in tmux, won\'t be interrupted by network disconnection/reconnection');
|
||||
console.log('- Email monitoring service needs to remain running');
|
||||
console.log('- Home computer needs to stay powered on with network connection');
|
||||
console.log('- Mobile can send email commands from anywhere');
|
||||
console.log('- Supports running multiple Claude sessions for different projects simultaneously\n');
|
||||
|
||||
console.log('✅ 现在你可以实现真正的无人值守远程控制了!🎯');
|
||||
console.log('✅ Now you can achieve true unattended remote control! 🎯');
|
||||
}
|
||||
|
||||
// 快速重建会话的方法
|
||||
// Quick session restart method
|
||||
async quickRestart() {
|
||||
console.log('🔄 快速重启Claude会话...');
|
||||
console.log('🔄 Quick restart Claude session...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.killAndCreateSession(() => {
|
||||
console.log('✅ Claude会话已重启');
|
||||
console.log('✅ Claude session restarted');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行参数处理
|
||||
// Command line parameter processing
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// 解析会话名称参数
|
||||
// Parse session name parameter
|
||||
let sessionName = null;
|
||||
const sessionIndex = args.indexOf('--session');
|
||||
if (sessionIndex !== -1 && args[sessionIndex + 1]) {
|
||||
|
|
@ -274,7 +274,7 @@ if (require.main === module) {
|
|||
const setup = new RemoteControlSetup(sessionName);
|
||||
|
||||
if (sessionName) {
|
||||
console.log(`🎛️ 使用自定义会话名称: ${sessionName}`);
|
||||
console.log(`🎛️ Using custom session name: ${sessionName}`);
|
||||
}
|
||||
|
||||
if (args.includes('--restart')) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"pass": "kKgS3tNReRTL3RQC"
|
||||
}
|
||||
},
|
||||
"from": "TaskPing 通知系统 <noreply@pandalla.ai>",
|
||||
"from": "TaskPing Notification System <noreply@pandalla.ai>",
|
||||
"to": "jiaxicui446@gmail.com",
|
||||
"template": {
|
||||
"checkInterval": 30
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"zh-CN": {
|
||||
"completed": {
|
||||
"title": "Claude Code - 任务完成",
|
||||
"message": "[{project}] 任务已完成,Claude正在等待下一步指令"
|
||||
"title": "Claude Code - Task Completed",
|
||||
"message": "[{project}] Task completed, Claude is waiting for next instruction"
|
||||
},
|
||||
"waiting": {
|
||||
"title": "Claude Code - 等待输入",
|
||||
"message": "[{project}] Claude需要您的进一步指导"
|
||||
"title": "Claude Code - Waiting for Input",
|
||||
"message": "[{project}] Claude needs your further guidance"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
|
|
|
|||
|
|
@ -30,19 +30,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
# 邮件配置说明
|
||||
# Email Configuration Instructions
|
||||
#
|
||||
# Gmail 配置示例:
|
||||
# 1. 启用两步验证
|
||||
# 2. 生成应用密码(16位)
|
||||
# 3. 替换上面的 your-email@gmail.com 和 your-app-password
|
||||
# Gmail Configuration Example:
|
||||
# 1. Enable two-factor authentication
|
||||
# 2. Generate app password (16 digits)
|
||||
# 3. Replace your-email@gmail.com and your-app-password above
|
||||
#
|
||||
# 其他邮箱提供商:
|
||||
# QQ邮箱: smtp.qq.com (587) / imap.qq.com (993)
|
||||
# 163邮箱: smtp.163.com (587) / imap.163.com (993)
|
||||
# Other Email Providers:
|
||||
# QQ Mail: smtp.qq.com (587) / imap.qq.com (993)
|
||||
# 163 Mail: smtp.163.com (587) / imap.163.com (993)
|
||||
# Outlook: smtp.live.com (587) / imap-mail.outlook.com (993)
|
||||
#
|
||||
# 配置完成后:
|
||||
# 1. 复制email部分到 config/channels.json
|
||||
# 2. 运行: taskping test
|
||||
# 3. 运行: taskping relay start
|
||||
# After Configuration:
|
||||
# 1. Copy email section to config/channels.json
|
||||
# 2. Run: taskping test
|
||||
# 3. Run: taskping relay start
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "taskping",
|
||||
"version": "1.0.0",
|
||||
"description": "Claude Code智能任务通知系统 - 当Claude完成任务或需要输入时发送桌面通知",
|
||||
"description": "Claude Code Smart Notification System - Send desktop notifications when Claude completes tasks or needs input",
|
||||
"main": "hook-notify.js",
|
||||
"scripts": {
|
||||
"install": "node install.js",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
* 发送测试邮件回复到relay服务
|
||||
* Send test email reply to relay service
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
async function sendTestReply() {
|
||||
console.log('📧 发送测试邮件回复...\n');
|
||||
console.log('📧 Sending test email reply...\n');
|
||||
|
||||
// 创建测试用的SMTP传输器(使用Gmail)
|
||||
// Create test SMTP transporter (using Gmail)
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
|
|
@ -17,32 +17,32 @@ async function sendTestReply() {
|
|||
}
|
||||
});
|
||||
|
||||
// 使用最新的token
|
||||
const testToken = 'V5UPZ1UE'; // 来自session-map.json的最新token
|
||||
// Use latest token
|
||||
const testToken = 'V5UPZ1UE'; // Latest token from session-map.json
|
||||
|
||||
const mailOptions = {
|
||||
from: 'jiaxicui446@gmail.com',
|
||||
to: 'noreply@pandalla.ai',
|
||||
subject: `Re: [TaskPing #${testToken}] Claude Code 任务完成 - TaskPing`,
|
||||
text: '请解释一下量子计算的基本原理',
|
||||
subject: `Re: [TaskPing #${testToken}] Claude Code Task Completed - TaskPing`,
|
||||
text: 'Please explain the basic principles of quantum computing',
|
||||
replyTo: 'jiaxicui446@gmail.com'
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log('✅ 测试邮件发送成功!');
|
||||
console.log('✅ Test email sent successfully!');
|
||||
console.log(`📧 Message ID: ${info.messageId}`);
|
||||
console.log(`📋 Token: ${testToken}`);
|
||||
console.log(`💬 Command: ${mailOptions.text}`);
|
||||
console.log('\n🔍 现在监控relay服务日志...');
|
||||
console.log('\n🔍 Now monitoring relay service logs...');
|
||||
|
||||
// 等待几秒让邮件被处理
|
||||
// Wait a few seconds for email processing
|
||||
setTimeout(() => {
|
||||
console.log('\n📋 请检查relay-debug.log文件查看处理日志');
|
||||
console.log('\n📋 Please check relay-debug.log file for processing logs');
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 邮件发送失败:', error.message);
|
||||
console.error('❌ Email sending failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Claude Code 专用自动化
|
||||
* 专门针对 Claude Code 的完全自动化解决方案
|
||||
* Claude Code Dedicated Automation
|
||||
* Complete automation solution specifically designed for Claude Code
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
|
@ -12,26 +12,26 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 完全自动化发送命令到 Claude Code
|
||||
* @param {string} command - 要发送的命令
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
* Fully automated command sending to Claude Code
|
||||
* @param {string} command - Command to send
|
||||
* @param {string} sessionId - Session ID
|
||||
* @returns {Promise<boolean>} - Whether successful
|
||||
*/
|
||||
async sendCommand(command, sessionId = '') {
|
||||
try {
|
||||
this.logger.info(`Sending command to Claude Code: ${command.substring(0, 50)}...`);
|
||||
|
||||
// 首先复制命令到剪贴板
|
||||
// First copy command to clipboard
|
||||
await this._copyToClipboard(command);
|
||||
|
||||
// 然后执行完全自动化的粘贴和执行
|
||||
// Then execute fully automated paste and execution
|
||||
const success = await this._fullAutomation(command);
|
||||
|
||||
if (success) {
|
||||
this.logger.info('Command sent and executed successfully');
|
||||
return true;
|
||||
} else {
|
||||
// 如果失败,尝试备选方案
|
||||
// If failed, try fallback option
|
||||
return await this._fallbackAutomation(command);
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 复制命令到剪贴板
|
||||
* Copy command to clipboard
|
||||
*/
|
||||
async _copyToClipboard(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -64,7 +64,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 完全自动化方案
|
||||
* Full automation solution
|
||||
*/
|
||||
async _fullAutomation(command) {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
|
@ -74,16 +74,16 @@ class ClaudeAutomation {
|
|||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 定义可能的 Claude Code 应用名称
|
||||
-- Define possible Claude Code application names
|
||||
set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"}
|
||||
set terminalApps to {"Terminal", "iTerm2", "iTerm", "Warp Terminal", "Warp"}
|
||||
set codeApps to {"Visual Studio Code", "Code", "Cursor", "Sublime Text", "Atom"}
|
||||
|
||||
-- 首先尝试找到 Claude Code
|
||||
-- First try to find Claude Code
|
||||
set targetApp to null
|
||||
set appName to ""
|
||||
|
||||
-- 检查 Claude 应用
|
||||
-- Check Claude applications
|
||||
repeat with app in claudeApps
|
||||
try
|
||||
if application process app exists then
|
||||
|
|
@ -94,7 +94,7 @@ class ClaudeAutomation {
|
|||
end try
|
||||
end repeat
|
||||
|
||||
-- 如果没找到 Claude,检查终端应用
|
||||
-- If Claude not found, check terminal applications
|
||||
if targetApp is null then
|
||||
repeat with app in terminalApps
|
||||
try
|
||||
|
|
@ -107,7 +107,7 @@ class ClaudeAutomation {
|
|||
end repeat
|
||||
end if
|
||||
|
||||
-- 如果还没找到,检查代码编辑器
|
||||
-- If still not found, check code editors
|
||||
if targetApp is null then
|
||||
repeat with app in codeApps
|
||||
try
|
||||
|
|
@ -121,46 +121,46 @@ class ClaudeAutomation {
|
|||
end if
|
||||
|
||||
if targetApp is not null then
|
||||
-- 激活应用
|
||||
-- Activate application
|
||||
set frontmost of targetApp to true
|
||||
delay 0.8
|
||||
|
||||
-- 等待应用完全激活
|
||||
-- Wait for application to fully activate
|
||||
repeat while (frontmost of targetApp) is false
|
||||
delay 0.1
|
||||
end repeat
|
||||
|
||||
-- 根据不同应用类型执行不同操作
|
||||
-- Execute different operations based on application type
|
||||
if appName contains "Claude" then
|
||||
-- Claude Code 特定操作
|
||||
-- Claude Code specific operations
|
||||
try
|
||||
-- 尝试点击输入框
|
||||
-- Try to click input box
|
||||
click (first text field of window 1)
|
||||
delay 0.3
|
||||
on error
|
||||
-- 如果没有文本框,尝试按键导航
|
||||
key code 125 -- 向下箭头
|
||||
-- If no text box, try keyboard navigation
|
||||
key code 125 -- Down arrow
|
||||
delay 0.2
|
||||
end try
|
||||
|
||||
-- 清空当前内容并粘贴新命令
|
||||
-- Clear current content and paste new command
|
||||
keystroke "a" using command down
|
||||
delay 0.2
|
||||
keystroke "v" using command down
|
||||
delay 0.5
|
||||
|
||||
-- 执行命令
|
||||
-- Execute command
|
||||
keystroke return
|
||||
|
||||
else if appName contains "Terminal" or appName contains "iTerm" or appName contains "Warp" then
|
||||
-- 终端应用操作
|
||||
-- Terminal application operations
|
||||
delay 0.5
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
keystroke return
|
||||
|
||||
else
|
||||
-- 其他应用(代码编辑器等)
|
||||
-- Other applications (code editors, etc.)
|
||||
delay 0.5
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
|
|
@ -205,7 +205,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 备选自动化方案 - 更强制性的方法
|
||||
* Fallback automation solution - more forceful method
|
||||
*/
|
||||
async _fallbackAutomation(command) {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
|
@ -213,7 +213,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 更强制性的方案,直接输入文本
|
||||
// More forceful approach, directly input text
|
||||
const escapedCommand = command
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
|
|
@ -222,29 +222,29 @@ class ClaudeAutomation {
|
|||
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 获取当前前台应用
|
||||
-- Get current foreground application
|
||||
set frontApp to first application process whose frontmost is true
|
||||
set appName to name of frontApp
|
||||
|
||||
-- 等待一下确保应用响应
|
||||
-- Wait a moment to ensure application responds
|
||||
delay 1
|
||||
|
||||
-- 直接输入命令文本(不依赖剪贴板)
|
||||
-- Directly input command text (not dependent on clipboard)
|
||||
try
|
||||
-- 先清空可能的现有内容
|
||||
-- First clear possible existing content
|
||||
keystroke "a" using command down
|
||||
delay 0.2
|
||||
|
||||
-- 输入命令
|
||||
-- Input command
|
||||
keystroke "${escapedCommand}"
|
||||
delay 0.5
|
||||
|
||||
-- 执行
|
||||
-- Execute
|
||||
keystroke return
|
||||
|
||||
return "typed_success:" & appName
|
||||
on error errorMsg
|
||||
-- 如果直接输入失败,尝试粘贴
|
||||
-- If direct input fails, try paste
|
||||
try
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
|
|
@ -281,7 +281,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 专门激活 Claude Code 应用
|
||||
* Specifically activate Claude Code application
|
||||
*/
|
||||
async activateClaudeCode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
|
@ -328,7 +328,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 检查系统权限并尝试请求
|
||||
* Check system permissions and attempt to request
|
||||
*/
|
||||
async requestPermissions() {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
|
@ -336,7 +336,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
try {
|
||||
// 尝试一个简单的操作来触发权限请求
|
||||
// Try a simple operation to trigger permission request
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
try
|
||||
|
|
@ -381,7 +381,7 @@ class ClaudeAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取状态信息
|
||||
* Get status information
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Clipboard Automation
|
||||
* 通过剪贴板和键盘自动化来发送命令到Claude Code
|
||||
* Send commands to Claude Code via clipboard and keyboard automation
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
|
@ -12,16 +12,16 @@ class ClipboardAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 发送命令到Claude Code(通过剪贴板)
|
||||
* @param {string} command - 要发送的命令
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
* Send command to Claude Code (via clipboard)
|
||||
* @param {string} command - Command to send
|
||||
* @returns {Promise<boolean>} - Whether successful
|
||||
*/
|
||||
async sendCommand(command) {
|
||||
try {
|
||||
// 第一步:将命令复制到剪贴板
|
||||
// Step 1: Copy command to clipboard
|
||||
await this._copyToClipboard(command);
|
||||
|
||||
// 第二步:激活Claude Code并粘贴
|
||||
// Step 2: Activate Claude Code and paste
|
||||
const success = await this._activateAndPaste();
|
||||
|
||||
if (success) {
|
||||
|
|
@ -29,7 +29,7 @@ class ClipboardAutomation {
|
|||
return true;
|
||||
} else {
|
||||
this.logger.warn('Failed to activate Claude Code, trying fallback');
|
||||
// 尝试通用方案
|
||||
// Try generic approach
|
||||
return await this._sendToActiveWindow(command);
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class ClipboardAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 将文本复制到剪贴板
|
||||
* Copy text to clipboard
|
||||
*/
|
||||
async _copyToClipboard(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -61,7 +61,7 @@ class ClipboardAutomation {
|
|||
|
||||
pbcopy.on('error', reject);
|
||||
} else if (process.platform === 'linux') {
|
||||
// Linux (需要 xclip 或 xsel)
|
||||
// Linux (requires xclip or xsel)
|
||||
const xclip = spawn('xclip', ['-selection', 'clipboard']);
|
||||
xclip.stdin.write(text);
|
||||
xclip.stdin.end();
|
||||
|
|
@ -70,7 +70,7 @@ class ClipboardAutomation {
|
|||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
// 尝试 xsel
|
||||
// Try xsel
|
||||
const xsel = spawn('xsel', ['--clipboard', '--input']);
|
||||
xsel.stdin.write(text);
|
||||
xsel.stdin.end();
|
||||
|
|
@ -91,7 +91,7 @@ class ClipboardAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 激活Claude Code并粘贴命令
|
||||
* Activate Claude Code and paste command
|
||||
*/
|
||||
async _activateAndPaste() {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
|
@ -101,7 +101,7 @@ class ClipboardAutomation {
|
|||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 尝试找到 Claude Code 相关的应用
|
||||
-- Try to find Claude Code related applications
|
||||
set targetApps to {"Claude Code", "Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor"}
|
||||
set foundApp to null
|
||||
set appName to ""
|
||||
|
|
@ -117,24 +117,24 @@ class ClipboardAutomation {
|
|||
end repeat
|
||||
|
||||
if foundApp is not null then
|
||||
-- 激活应用
|
||||
-- Activate application
|
||||
set frontmost of foundApp to true
|
||||
delay 0.5
|
||||
|
||||
-- 尝试找到输入框并点击
|
||||
-- Try to find input box and click
|
||||
try
|
||||
-- 对于一些应用,可能需要点击特定的输入区域
|
||||
-- For some applications, may need to click specific input area
|
||||
if appName is "Claude Code" then
|
||||
-- Claude Code 特定的处理
|
||||
key code 125 -- 向下箭头,确保光标在输入框
|
||||
-- Claude Code specific handling
|
||||
key code 125 -- Down arrow, ensure cursor is in input box
|
||||
delay 0.2
|
||||
end if
|
||||
|
||||
-- 粘贴内容
|
||||
-- Paste content
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
|
||||
-- 发送命令(回车)
|
||||
-- Send command (Enter)
|
||||
keystroke return
|
||||
|
||||
return "success"
|
||||
|
|
@ -177,7 +177,7 @@ class ClipboardAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 发送到当前活动窗口(通用方案)
|
||||
* Send to current active window (generic approach)
|
||||
*/
|
||||
async _sendToActiveWindow(command) {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
|
@ -187,10 +187,10 @@ class ClipboardAutomation {
|
|||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 获取当前活动应用
|
||||
-- Get current active application
|
||||
set activeApp to name of first application process whose frontmost is true
|
||||
|
||||
-- 粘贴命令到当前活动窗口
|
||||
-- Paste command to current active window
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
keystroke return
|
||||
|
|
@ -222,14 +222,14 @@ class ClipboardAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持剪贴板自动化
|
||||
* Check if clipboard automation is supported
|
||||
*/
|
||||
isSupported() {
|
||||
return process.platform === 'darwin' || process.platform === 'linux';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前剪贴板内容(用于测试)
|
||||
* Get current clipboard content (for testing)
|
||||
*/
|
||||
async getClipboardContent() {
|
||||
if (process.platform === 'darwin') {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 简单自动化方案
|
||||
* 使用更简单的方式来处理邮件回复命令
|
||||
* Simple Automation Solution
|
||||
* Use simpler methods to handle email reply commands
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
|
@ -16,23 +16,23 @@ class SimpleAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 发送命令 - 使用多种简单方式
|
||||
* @param {string} command - 要发送的命令
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
* Send command - using multiple simple methods
|
||||
* @param {string} command - Command to send
|
||||
* @param {string} sessionId - Session ID
|
||||
* @returns {Promise<boolean>} - Whether successful
|
||||
*/
|
||||
async sendCommand(command, sessionId = '') {
|
||||
try {
|
||||
// 方法1: 保存到文件,用户可以手动复制
|
||||
// Method 1: Save to file, user can manually copy
|
||||
await this._saveCommandToFile(command, sessionId);
|
||||
|
||||
// 方法2: 复制到剪贴板
|
||||
// Method 2: Copy to clipboard
|
||||
const clipboardSuccess = await this._copyToClipboard(command);
|
||||
|
||||
// 方法3: 发送富通知(包含命令内容)
|
||||
// Method 3: Send rich notification (including command content)
|
||||
const notificationSuccess = await this._sendRichNotification(command, sessionId);
|
||||
|
||||
// 方法4: 尝试简单的自动化(不依赖复杂权限)
|
||||
// Method 4: Try simple automation (no complex permissions needed)
|
||||
const autoSuccess = await this._trySimpleAutomation(command);
|
||||
|
||||
if (clipboardSuccess || notificationSuccess || autoSuccess) {
|
||||
|
|
@ -50,21 +50,21 @@ class SimpleAutomation {
|
|||
}
|
||||
|
||||
/**
|
||||
* 保存命令到文件
|
||||
* Save command to file
|
||||
*/
|
||||
async _saveCommandToFile(command, sessionId) {
|
||||
try {
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
const content = `# TaskPing 邮件回复命令
|
||||
# 时间: ${timestamp}
|
||||
# 会话ID: ${sessionId}
|
||||
const content = `# TaskPing Email Reply Command
|
||||
# Time: ${timestamp}
|
||||
# Session ID: ${sessionId}
|
||||
#
|
||||
# 请复制下面的命令到 Claude Code 中执行:
|
||||
# Please copy the command below to Claude Code for execution:
|
||||
|
||||
${command}
|
||||
|
||||
# ===============================
|
||||
# 执行完成后可以删除此文件
|
||||
# This file can be deleted after execution
|
||||
`;
|
||||
|
||||
fs.writeFileSync(this.commandFile, content, 'utf8');
|
||||
|
|
@ -77,7 +77,7 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
* Copy to clipboard
|
||||
*/
|
||||
async _copyToClipboard(command) {
|
||||
try {
|
||||
|
|
@ -107,24 +107,24 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 发送富通知
|
||||
* Send rich notification
|
||||
*/
|
||||
async _sendRichNotification(command, sessionId) {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
||||
|
||||
// 创建详细的通知
|
||||
// Create detailed notification
|
||||
const script = `
|
||||
set commandText to "${command.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"
|
||||
|
||||
display notification "命令已复制到剪贴板,请粘贴到 Claude Code 中" with title "TaskPing - 新邮件命令" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "default"
|
||||
display notification "Command copied to clipboard, please paste to Claude Code" with title "TaskPing - New Email Command" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "default"
|
||||
|
||||
-- 同时显示对话框(可选,用户可以取消)
|
||||
-- Also show dialog (optional, user can cancel)
|
||||
try
|
||||
set userChoice to display dialog "收到新的邮件命令:" & return & return & commandText buttons {"打开命令文件", "取消", "已粘贴"} default button "已粘贴" with title "TaskPing 邮件中继" giving up after 10
|
||||
set userChoice to display dialog "Received new email command:" & return & return & commandText buttons {"Open Command File", "Cancel", "Pasted"} default button "Pasted" with title "TaskPing Email Relay" giving up after 10
|
||||
|
||||
if button returned of userChoice is "打开命令文件" then
|
||||
if button returned of userChoice is "Open Command File" then
|
||||
do shell script "open -t '${this.commandFile}'"
|
||||
end if
|
||||
end try
|
||||
|
|
@ -140,7 +140,7 @@ ${command}
|
|||
} else {
|
||||
this.logger.warn('Rich notification failed, trying simple notification');
|
||||
this._sendSimpleNotification(command);
|
||||
resolve(true); // 即使失败也算成功,因为有备选方案
|
||||
resolve(true); // Consider successful even if failed, because there are backup options
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -158,12 +158,12 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 发送简单通知
|
||||
* Send simple notification
|
||||
*/
|
||||
async _sendSimpleNotification(command) {
|
||||
try {
|
||||
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
||||
const script = `display notification "命令: ${shortCommand.replace(/"/g, '\\"')}" with title "TaskPing - 邮件命令" sound name "default"`;
|
||||
const script = `display notification "Command: ${shortCommand.replace(/"/g, '\\"')}" with title "TaskPing - Email Command" sound name "default"`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
osascript.on('close', () => {
|
||||
|
|
@ -175,19 +175,19 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 尝试简单自动化(无需复杂权限)
|
||||
* Try simple automation (no complex permissions needed)
|
||||
*/
|
||||
async _trySimpleAutomation(command) {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
// 只尝试最基本的操作,不强制要求权限
|
||||
// Only try basic operations, don't force permissions
|
||||
const script = `
|
||||
try
|
||||
tell application "System Events"
|
||||
-- 尝试获取前台应用
|
||||
-- Try to get frontmost application
|
||||
set frontApp to name of first application process whose frontmost is true
|
||||
|
||||
-- 如果是终端或代码编辑器,尝试粘贴
|
||||
-- If it's terminal or code editor, try to paste
|
||||
if frontApp contains "Terminal" or frontApp contains "iTerm" or frontApp contains "Code" or frontApp contains "Claude" then
|
||||
keystroke "v" using command down
|
||||
delay 0.2
|
||||
|
|
@ -231,7 +231,7 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 打开命令文件
|
||||
* Open command file
|
||||
*/
|
||||
async openCommandFile() {
|
||||
try {
|
||||
|
|
@ -250,7 +250,7 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 清理命令文件
|
||||
* Clean up command files
|
||||
*/
|
||||
cleanupCommandFile() {
|
||||
try {
|
||||
|
|
@ -264,7 +264,7 @@ ${command}
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
* Get status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
|
||||
_generateToken() {
|
||||
// 生成简短的Token (大写字母+数字,8位)
|
||||
// Generate short Token (uppercase letters + numbers, 8 digits)
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let token = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
|
|
@ -57,7 +57,7 @@ class EmailChannel extends NotificationChannel {
|
|||
user: this.config.smtp.auth.user,
|
||||
pass: this.config.smtp.auth.pass
|
||||
},
|
||||
// 添加超时设置
|
||||
// Add timeout settings
|
||||
connectionTimeout: 10000,
|
||||
greetingTimeout: 10000,
|
||||
socketTimeout: 10000
|
||||
|
|
@ -93,11 +93,11 @@ class EmailChannel extends NotificationChannel {
|
|||
throw new Error('Email recipient not configured');
|
||||
}
|
||||
|
||||
// 生成会话ID和Token
|
||||
// Generate session ID and Token
|
||||
const sessionId = uuidv4();
|
||||
const token = this._generateToken();
|
||||
|
||||
// 获取当前tmux会话和对话内容
|
||||
// Get current tmux session and conversation content
|
||||
const tmuxSession = this._getCurrentTmuxSession();
|
||||
if (tmuxSession && !notification.metadata) {
|
||||
const conversation = this.tmuxMonitor.getRecentConversation(tmuxSession);
|
||||
|
|
@ -108,10 +108,10 @@ class EmailChannel extends NotificationChannel {
|
|||
};
|
||||
}
|
||||
|
||||
// 创建会话记录
|
||||
// Create session record
|
||||
await this._createSession(sessionId, notification, token);
|
||||
|
||||
// 生成邮件内容
|
||||
// Generate email content
|
||||
const emailContent = this._generateEmailContent(notification, sessionId, token);
|
||||
|
||||
const mailOptions = {
|
||||
|
|
@ -120,7 +120,7 @@ class EmailChannel extends NotificationChannel {
|
|||
subject: emailContent.subject,
|
||||
html: emailContent.html,
|
||||
text: emailContent.text,
|
||||
// 添加自定义头部用于回复识别
|
||||
// Add custom headers for reply recognition
|
||||
headers: {
|
||||
'X-TaskPing-Session-ID': sessionId,
|
||||
'X-TaskPing-Type': notification.type
|
||||
|
|
@ -133,7 +133,7 @@ class EmailChannel extends NotificationChannel {
|
|||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send email:', error.message);
|
||||
// 清理失败的会话
|
||||
// Clean up failed session
|
||||
await this._removeSession(sessionId);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ class EmailChannel extends NotificationChannel {
|
|||
token: token,
|
||||
type: 'pty',
|
||||
created: new Date().toISOString(),
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24小时后过期
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // Expires after 24 hours
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
|
||||
cwd: process.cwd(),
|
||||
|
|
@ -162,7 +162,7 @@ class EmailChannel extends NotificationChannel {
|
|||
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
|
||||
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
|
||||
|
||||
// 同时保存到PTY映射格式
|
||||
// Also save in PTY mapping format
|
||||
const sessionMapPath = process.env.SESSION_MAP_PATH || path.join(__dirname, '../../data/session-map.json');
|
||||
let sessionMap = {};
|
||||
if (fs.existsSync(sessionMapPath)) {
|
||||
|
|
@ -173,7 +173,7 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
}
|
||||
|
||||
// 使用传入的tmux会话名称或检测当前会话
|
||||
// Use passed tmux session name or detect current session
|
||||
let tmuxSession = notification.metadata?.tmuxSession || this._getCurrentTmuxSession() || 'claude-taskping';
|
||||
|
||||
sessionMap[token] = {
|
||||
|
|
@ -186,7 +186,7 @@ class EmailChannel extends NotificationChannel {
|
|||
description: `${notification.type} - ${notification.project}`
|
||||
};
|
||||
|
||||
// 确保目录存在
|
||||
// Ensure directory exists
|
||||
const mapDir = path.dirname(sessionMapPath);
|
||||
if (!fs.existsSync(mapDir)) {
|
||||
fs.mkdirSync(mapDir, { recursive: true });
|
||||
|
|
@ -209,10 +209,10 @@ class EmailChannel extends NotificationChannel {
|
|||
const template = this._getTemplate(notification.type);
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
|
||||
// 获取项目目录名(最后一级目录)
|
||||
// Get project directory name (last level directory)
|
||||
const projectDir = path.basename(process.cwd());
|
||||
|
||||
// 提取用户问题(从notification.metadata中获取,如果有的话)
|
||||
// Extract user question (from notification.metadata if available)
|
||||
let userQuestion = '';
|
||||
let claudeResponse = '';
|
||||
|
||||
|
|
@ -221,12 +221,12 @@ class EmailChannel extends NotificationChannel {
|
|||
claudeResponse = notification.metadata.claudeResponse || '';
|
||||
}
|
||||
|
||||
// 限制用户问题长度用于标题
|
||||
// Limit user question length for title
|
||||
const maxQuestionLength = 30;
|
||||
const shortQuestion = userQuestion.length > maxQuestionLength ?
|
||||
userQuestion.substring(0, maxQuestionLength) + '...' : userQuestion;
|
||||
|
||||
// 生成更具辨识度的标题
|
||||
// Generate more distinctive title
|
||||
let enhancedSubject = template.subject;
|
||||
if (shortQuestion) {
|
||||
enhancedSubject = enhancedSubject.replace('{{project}}', `${projectDir} | ${shortQuestion}`);
|
||||
|
|
@ -234,25 +234,25 @@ class EmailChannel extends NotificationChannel {
|
|||
enhancedSubject = enhancedSubject.replace('{{project}}', projectDir);
|
||||
}
|
||||
|
||||
// 模板变量替换
|
||||
// Template variable replacement
|
||||
const variables = {
|
||||
project: projectDir,
|
||||
message: notification.message,
|
||||
timestamp: timestamp,
|
||||
sessionId: sessionId,
|
||||
token: token,
|
||||
type: notification.type === 'completed' ? '任务完成' : '等待输入',
|
||||
userQuestion: userQuestion || '未指定任务',
|
||||
type: notification.type === 'completed' ? 'Task completed' : 'Waiting for input',
|
||||
userQuestion: userQuestion || 'No specified task',
|
||||
claudeResponse: claudeResponse || notification.message,
|
||||
projectDir: projectDir,
|
||||
shortQuestion: shortQuestion || '无具体问题'
|
||||
shortQuestion: shortQuestion || 'No specific question'
|
||||
};
|
||||
|
||||
let subject = enhancedSubject;
|
||||
let html = template.html;
|
||||
let text = template.text;
|
||||
|
||||
// 替换模板变量
|
||||
// Replace template variables
|
||||
Object.keys(variables).forEach(key => {
|
||||
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
||||
subject = subject.replace(placeholder, variables[key]);
|
||||
|
|
@ -264,131 +264,131 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
|
||||
_getTemplate(type) {
|
||||
// 默认模板
|
||||
// Default templates
|
||||
const templates = {
|
||||
completed: {
|
||||
subject: '[TaskPing #{{token}}] Claude Code 任务完成 - {{project}}',
|
||||
subject: '[TaskPing #{{token}}] Claude Code Task Completed - {{project}}',
|
||||
html: `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9f9f9;">
|
||||
<div style="background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #2c3e50; margin-top: 0; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
|
||||
🎉 Claude Code 任务完成
|
||||
🎉 Claude Code Task Completed
|
||||
</h2>
|
||||
|
||||
<div style="background-color: #ecf0f1; padding: 15px; border-radius: 6px; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #2c3e50;">
|
||||
<strong>项目:</strong> {{projectDir}}<br>
|
||||
<strong>时间:</strong> {{timestamp}}<br>
|
||||
<strong>状态:</strong> {{type}}
|
||||
<strong>Project:</strong> {{projectDir}}<br>
|
||||
<strong>Time:</strong> {{timestamp}}<br>
|
||||
<strong>Status:</strong> {{type}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3e0; padding: 15px; border-radius: 6px; border-left: 4px solid #ff9800; margin: 20px 0;">
|
||||
<h4 style="margin-top: 0; color: #e65100;">📝 您的问题</h4>
|
||||
<h4 style="margin-top: 0; color: #e65100;">📝 Your Question</h4>
|
||||
<p style="margin: 0; color: #2c3e50; font-style: italic;">{{userQuestion}}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #e8f5e8; padding: 15px; border-radius: 6px; border-left: 4px solid #27ae60;">
|
||||
<h4 style="margin-top: 0; color: #27ae60;">🤖 Claude 的回复</h4>
|
||||
<h4 style="margin-top: 0; color: #27ae60;">🤖 Claude's Response</h4>
|
||||
<p style="margin: 0; color: #2c3e50;">{{claudeResponse}}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 25px 0; padding: 20px; background-color: #fff3cd; border-radius: 6px; border-left: 4px solid #ffc107;">
|
||||
<h3 style="margin-top: 0; color: #856404;">💡 如何继续对话</h3>
|
||||
<h3 style="margin-top: 0; color: #856404;">💡 How to Continue the Conversation</h3>
|
||||
<p style="margin: 10px 0; color: #856404;">
|
||||
要继续与 Claude Code 对话,请直接<strong>回复此邮件</strong>,在邮件正文中输入您的指令。
|
||||
To continue conversation with Claude Code, please <strong>reply to this email</strong> directly and enter your instructions in the email body.
|
||||
</p>
|
||||
<div style="background-color: white; padding: 10px; border-radius: 4px; font-family: monospace; color: #495057;">
|
||||
示例回复:<br>
|
||||
• "请继续优化代码"<br>
|
||||
• "生成单元测试"<br>
|
||||
• "解释这个函数的作用"
|
||||
Example replies:<br>
|
||||
• "Please continue optimizing the code"<br>
|
||||
• "Generate unit tests"<br>
|
||||
• "Explain the purpose of this function"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d;">
|
||||
<p style="margin: 5px 0;">会话ID: <code>{{sessionId}}</code></p>
|
||||
<p style="margin: 5px 0;">🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期</p>
|
||||
<p style="margin: 5px 0;">📧 这是一封来自 TaskPing 的自动邮件</p>
|
||||
<p style="margin: 5px 0;">Session ID: <code>{{sessionId}}</code></p>
|
||||
<p style="margin: 5px 0;">🔒 Security note: Please do not forward this email, session will automatically expire after 24 hours</p>
|
||||
<p style="margin: 5px 0;">📧 This is an automated email from TaskPing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
text: `
|
||||
[TaskPing #{{token}}] Claude Code 任务完成 - {{projectDir}} | {{shortQuestion}}
|
||||
[TaskPing #{{token}}] Claude Code Task Completed - {{projectDir}} | {{shortQuestion}}
|
||||
|
||||
项目: {{projectDir}}
|
||||
时间: {{timestamp}}
|
||||
状态: {{type}}
|
||||
Project: {{projectDir}}
|
||||
Time: {{timestamp}}
|
||||
Status: {{type}}
|
||||
|
||||
📝 您的问题:
|
||||
📝 Your Question:
|
||||
{{userQuestion}}
|
||||
|
||||
🤖 Claude 的回复:
|
||||
🤖 Claude's Response:
|
||||
{{claudeResponse}}
|
||||
|
||||
如何继续对话:
|
||||
要继续与 Claude Code 对话,请直接回复此邮件,在邮件正文中输入您的指令。
|
||||
How to Continue Conversation:
|
||||
To continue conversation with Claude Code, please reply to this email directly and enter your instructions in the email body.
|
||||
|
||||
示例回复:
|
||||
• "请继续优化代码"
|
||||
• "生成单元测试"
|
||||
• "解释这个函数的作用"
|
||||
Example Replies:
|
||||
• "Please continue optimizing the code"
|
||||
• "Generate unit tests"
|
||||
• "Explain the purpose of this function"
|
||||
|
||||
会话ID: {{sessionId}}
|
||||
安全提示: 请勿转发此邮件,会话将在24小时后自动过期
|
||||
Session ID: {{sessionId}}
|
||||
Security Note: Please do not forward this email, session will automatically expire after 24 hours
|
||||
`
|
||||
},
|
||||
waiting: {
|
||||
subject: '[TaskPing #{{token}}] Claude Code 等待输入 - {{project}}',
|
||||
subject: '[TaskPing #{{token}}] Claude Code Waiting for Input - {{project}}',
|
||||
html: `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9f9f9;">
|
||||
<div style="background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #2c3e50; margin-top: 0; border-bottom: 2px solid #e74c3c; padding-bottom: 10px;">
|
||||
⏳ Claude Code 等待您的指导
|
||||
⏳ Claude Code Waiting for Your Guidance
|
||||
</h2>
|
||||
|
||||
<div style="background-color: #ecf0f1; padding: 15px; border-radius: 6px; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #2c3e50;">
|
||||
<strong>项目:</strong> {{projectDir}}<br>
|
||||
<strong>时间:</strong> {{timestamp}}<br>
|
||||
<strong>状态:</strong> {{type}}
|
||||
<strong>Project:</strong> {{projectDir}}<br>
|
||||
<strong>Time:</strong> {{timestamp}}<br>
|
||||
<strong>Status:</strong> {{type}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fdf2e9; padding: 15px; border-radius: 6px; border-left: 4px solid #e67e22;">
|
||||
<h4 style="margin-top: 0; color: #e67e22;">⏳ 等待处理</h4>
|
||||
<h4 style="margin-top: 0; color: #e67e22;">⏳ Waiting for Processing</h4>
|
||||
<p style="margin: 0; color: #2c3e50;">{{message}}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 25px 0; padding: 20px; background-color: #d1ecf1; border-radius: 6px; border-left: 4px solid #17a2b8;">
|
||||
<h3 style="margin-top: 0; color: #0c5460;">💬 请提供指导</h3>
|
||||
<h3 style="margin-top: 0; color: #0c5460;">💬 Please Provide Guidance</h3>
|
||||
<p style="margin: 10px 0; color: #0c5460;">
|
||||
Claude 需要您的进一步指导。请<strong>回复此邮件</strong>告诉 Claude 下一步应该做什么。
|
||||
Claude needs your further guidance. Please <strong>reply to this email</strong> to tell Claude what to do next.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d;">
|
||||
<p style="margin: 5px 0;">会话ID: <code>{{sessionId}}</code></p>
|
||||
<p style="margin: 5px 0;">🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期</p>
|
||||
<p style="margin: 5px 0;">📧 这是一封来自 TaskPing 的自动邮件</p>
|
||||
<p style="margin: 5px 0;">Session ID: <code>{{sessionId}}</code></p>
|
||||
<p style="margin: 5px 0;">🔒 Security note: Please do not forward this email, session will automatically expire after 24 hours</p>
|
||||
<p style="margin: 5px 0;">📧 This is an automated email from TaskPing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
text: `
|
||||
[TaskPing #{{token}}] Claude Code 等待输入 - {{projectDir}}
|
||||
[TaskPing #{{token}}] Claude Code Waiting for Input - {{projectDir}}
|
||||
|
||||
项目: {{projectDir}}
|
||||
时间: {{timestamp}}
|
||||
状态: {{type}}
|
||||
Project: {{projectDir}}
|
||||
Time: {{timestamp}}
|
||||
Status: {{type}}
|
||||
|
||||
⏳ 等待处理: {{message}}
|
||||
⏳ Waiting for Processing: {{message}}
|
||||
|
||||
Claude 需要您的进一步指导。请回复此邮件告诉 Claude 下一步应该做什么。
|
||||
Claude needs your further guidance. Please reply to this email to tell Claude what to do next.
|
||||
|
||||
会话ID: {{sessionId}}
|
||||
安全提示: 请勿转发此邮件,会话将在24小时后自动过期
|
||||
Session ID: {{sessionId}}
|
||||
Security Note: Please do not forward this email, session will automatically expire after 24 hours
|
||||
`
|
||||
}
|
||||
};
|
||||
|
|
@ -422,14 +422,14 @@ Claude 需要您的进一步指导。请回复此邮件告诉 Claude 下一步
|
|||
throw new Error('Email transporter not initialized');
|
||||
}
|
||||
|
||||
// 验证 SMTP 连接
|
||||
// Verify SMTP connection
|
||||
await this.transporter.verify();
|
||||
|
||||
// 发送测试邮件
|
||||
// Send test email
|
||||
const testNotification = {
|
||||
type: 'completed',
|
||||
title: 'TaskPing 测试',
|
||||
message: '这是一封测试邮件,用于验证邮件通知功能是否正常工作。',
|
||||
title: 'TaskPing Test',
|
||||
message: 'This is a test email to verify that the email notification function is working properly.',
|
||||
project: 'TaskPing-Test',
|
||||
metadata: {
|
||||
test: true,
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class Notifier {
|
|||
this._loadI18n();
|
||||
}
|
||||
|
||||
const langData = this.i18n[lang] || this.i18n['zh-CN'];
|
||||
const langData = this.i18n[lang] || this.i18n['en'];
|
||||
return langData[type] || langData.completed;
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +156,12 @@ class Notifier {
|
|||
this.i18n = {
|
||||
'zh-CN': {
|
||||
completed: {
|
||||
title: 'Claude Code - 任务完成',
|
||||
message: '[{project}] 任务已完成,Claude正在等待下一步指令'
|
||||
title: 'Claude Code - Task Completed',
|
||||
message: '[{project}] Task completed, Claude is waiting for next instruction'
|
||||
},
|
||||
waiting: {
|
||||
title: 'Claude Code - 等待输入',
|
||||
message: '[{project}] Claude需要您的进一步指导'
|
||||
title: 'Claude Code - Waiting for Input',
|
||||
message: '[{project}] Claude needs your further guidance'
|
||||
}
|
||||
},
|
||||
'en': {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
/**
|
||||
* TaskPing Daemon Service
|
||||
* 后台守护进程,用于监听邮件和处理远程命令
|
||||
* Background daemon process for monitoring emails and processing remote commands
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
|
@ -20,7 +20,7 @@ class TaskPingDaemon {
|
|||
this.relayService = null;
|
||||
this.isRunning = false;
|
||||
|
||||
// 确保数据目录存在
|
||||
// Ensure data directory exists
|
||||
const dataDir = path.dirname(this.pidFile);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
|
|
@ -29,18 +29,18 @@ class TaskPingDaemon {
|
|||
|
||||
async start(detached = true) {
|
||||
try {
|
||||
// 检查是否已经运行
|
||||
// Check if already running
|
||||
if (this.isAlreadyRunning()) {
|
||||
console.log('❌ TaskPing daemon 已经在运行中');
|
||||
console.log('💡 使用 "taskping daemon stop" 停止现有服务');
|
||||
console.log('❌ TaskPing daemon is already running');
|
||||
console.log('💡 Use "taskping daemon stop" to stop existing service');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (detached) {
|
||||
// 以守护进程模式启动
|
||||
// Start in daemon mode
|
||||
await this.startDetached();
|
||||
} else {
|
||||
// 直接在当前进程运行
|
||||
// Run directly in current process
|
||||
await this.startForeground();
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -50,41 +50,41 @@ class TaskPingDaemon {
|
|||
}
|
||||
|
||||
async startDetached() {
|
||||
console.log('🚀 启动 TaskPing 守护进程...');
|
||||
console.log('🚀 Starting TaskPing daemon...');
|
||||
|
||||
// 创建子进程
|
||||
// Create child process
|
||||
const child = spawn(process.execPath, [__filename, '--foreground'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// 重定向日志
|
||||
// Redirect logs
|
||||
const logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
||||
child.stdout.pipe(logStream);
|
||||
child.stderr.pipe(logStream);
|
||||
|
||||
// 保存 PID
|
||||
// Save PID
|
||||
fs.writeFileSync(this.pidFile, child.pid.toString());
|
||||
|
||||
// 分离子进程
|
||||
// Detach child process
|
||||
child.unref();
|
||||
|
||||
console.log(`✅ TaskPing 守护进程已启动 (PID: ${child.pid})`);
|
||||
console.log(`📝 日志文件: ${this.logFile}`);
|
||||
console.log('💡 使用 "taskping daemon status" 查看状态');
|
||||
console.log('💡 使用 "taskping daemon stop" 停止服务');
|
||||
console.log(`✅ TaskPing daemon started (PID: ${child.pid})`);
|
||||
console.log(`📝 Log file: ${this.logFile}`);
|
||||
console.log('💡 Use "taskping daemon status" to view status');
|
||||
console.log('💡 Use "taskping daemon stop" to stop service');
|
||||
}
|
||||
|
||||
async startForeground() {
|
||||
console.log('🚀 TaskPing 守护进程启动中...');
|
||||
console.log('🚀 TaskPing daemon starting...');
|
||||
|
||||
this.isRunning = true;
|
||||
process.title = 'taskping-daemon';
|
||||
|
||||
// 加载配置
|
||||
// Load configuration
|
||||
this.config.load();
|
||||
|
||||
// 初始化邮件中继服务
|
||||
// Initialize email relay service
|
||||
const emailConfig = this.config.getChannel('email');
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
this.logger.warn('Email channel not configured or disabled');
|
||||
|
|
@ -94,19 +94,19 @@ class TaskPingDaemon {
|
|||
const CommandRelayService = require('../relay/command-relay');
|
||||
this.relayService = new CommandRelayService(emailConfig.config);
|
||||
|
||||
// 设置事件监听
|
||||
// Setup event handlers
|
||||
this.setupEventHandlers();
|
||||
|
||||
// 启动服务
|
||||
// Start service
|
||||
await this.relayService.start();
|
||||
this.logger.info('Email relay service started');
|
||||
|
||||
// 保持进程运行
|
||||
// Keep process running
|
||||
this.keepAlive();
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
// 优雅关闭
|
||||
// Graceful shutdown
|
||||
const gracefulShutdown = async (signal) => {
|
||||
this.logger.info(`Received ${signal}, shutting down gracefully...`);
|
||||
this.isRunning = false;
|
||||
|
|
@ -115,7 +115,7 @@ class TaskPingDaemon {
|
|||
await this.relayService.stop();
|
||||
}
|
||||
|
||||
// 删除 PID 文件
|
||||
// Delete PID file
|
||||
if (fs.existsSync(this.pidFile)) {
|
||||
fs.unlinkSync(this.pidFile);
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ class TaskPingDaemon {
|
|||
this.config.load();
|
||||
});
|
||||
|
||||
// 中继服务事件
|
||||
// Relay service events
|
||||
if (this.relayService) {
|
||||
this.relayService.on('started', () => {
|
||||
this.logger.info('Command relay service started');
|
||||
|
|
@ -149,7 +149,7 @@ class TaskPingDaemon {
|
|||
});
|
||||
}
|
||||
|
||||
// 未捕获异常处理
|
||||
// Uncaught exception handling
|
||||
process.on('uncaughtException', (error) => {
|
||||
this.logger.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
|
|
@ -162,48 +162,48 @@ class TaskPingDaemon {
|
|||
}
|
||||
|
||||
keepAlive() {
|
||||
// 保持进程运行
|
||||
// Keep process running
|
||||
const heartbeat = setInterval(() => {
|
||||
if (!this.isRunning) {
|
||||
clearInterval(heartbeat);
|
||||
return;
|
||||
}
|
||||
this.logger.debug('Heartbeat');
|
||||
}, 60000); // 每分钟输出一次心跳日志
|
||||
}, 60000); // Output heartbeat log every minute
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.isAlreadyRunning()) {
|
||||
console.log('❌ TaskPing daemon 没有运行');
|
||||
console.log('❌ TaskPing daemon is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pid = this.getPid();
|
||||
console.log(`🛑 正在停止 TaskPing 守护进程 (PID: ${pid})...`);
|
||||
console.log(`🛑 Stopping TaskPing daemon (PID: ${pid})...`);
|
||||
|
||||
// 发送 SIGTERM 信号
|
||||
// Send SIGTERM signal
|
||||
process.kill(pid, 'SIGTERM');
|
||||
|
||||
// 等待进程结束
|
||||
// Wait for process to end
|
||||
await this.waitForStop(pid);
|
||||
|
||||
console.log('✅ TaskPing 守护进程已停止');
|
||||
console.log('✅ TaskPing daemon stopped');
|
||||
} catch (error) {
|
||||
console.error('❌ 停止守护进程失败:', error.message);
|
||||
console.error('❌ Failed to stop daemon:', error.message);
|
||||
|
||||
// 强制删除 PID 文件
|
||||
// Force delete PID file
|
||||
if (fs.existsSync(this.pidFile)) {
|
||||
fs.unlinkSync(this.pidFile);
|
||||
console.log('🧹 已清理 PID 文件');
|
||||
console.log('🧹 PID file cleaned up');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restart() {
|
||||
console.log('🔄 重启 TaskPing 守护进程...');
|
||||
console.log('🔄 Restarting TaskPing daemon...');
|
||||
await this.stop();
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
|
||||
await this.start();
|
||||
}
|
||||
|
||||
|
|
@ -223,28 +223,28 @@ class TaskPingDaemon {
|
|||
showStatus() {
|
||||
const status = this.getStatus();
|
||||
|
||||
console.log('📊 TaskPing 守护进程状态\n');
|
||||
console.log('📊 TaskPing daemon status\n');
|
||||
|
||||
if (status.running) {
|
||||
console.log('✅ 状态: 运行中');
|
||||
console.log('✅ Status: Running');
|
||||
console.log(`🆔 PID: ${status.pid}`);
|
||||
console.log(`⏱️ 运行时间: ${status.uptime || '未知'}`);
|
||||
console.log(`⏱️ Uptime: ${status.uptime || 'Unknown'}`);
|
||||
} else {
|
||||
console.log('❌ 状态: 未运行');
|
||||
console.log('❌ Status: Not running');
|
||||
}
|
||||
|
||||
console.log(`📝 日志文件: ${status.logFile}`);
|
||||
console.log(`📁 PID 文件: ${status.pidFile}`);
|
||||
console.log(`📝 Log file: ${status.logFile}`);
|
||||
console.log(`📁 PID file: ${status.pidFile}`);
|
||||
|
||||
// 显示最近的日志
|
||||
// Show recent logs
|
||||
if (fs.existsSync(status.logFile)) {
|
||||
console.log('\n📋 最近日志:');
|
||||
console.log('\n📋 Recent logs:');
|
||||
try {
|
||||
const logs = fs.readFileSync(status.logFile, 'utf8');
|
||||
const lines = logs.split('\n').filter(line => line.trim()).slice(-5);
|
||||
lines.forEach(line => console.log(` ${line}`));
|
||||
} catch (error) {
|
||||
console.log(' 无法读取日志文件');
|
||||
console.log(' Unable to read log file');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,11 +256,11 @@ class TaskPingDaemon {
|
|||
|
||||
try {
|
||||
const pid = parseInt(fs.readFileSync(this.pidFile, 'utf8'));
|
||||
// 检查进程是否仍在运行
|
||||
// Check if process is still running
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 进程不存在,删除过时的 PID 文件
|
||||
// Process doesn't exist, delete outdated PID file
|
||||
fs.unlinkSync(this.pidFile);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -281,18 +281,18 @@ class TaskPingDaemon {
|
|||
process.kill(pid, 0);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
// 进程已停止
|
||||
// Process has stopped
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 超时,强制结束
|
||||
throw new Error('进程停止超时,可能需要手动结束');
|
||||
// Timeout, force termination
|
||||
throw new Error('Process stop timeout, may need manual termination');
|
||||
}
|
||||
|
||||
getUptime(pid) {
|
||||
try {
|
||||
// 在 macOS 和 Linux 上获取进程启动时间
|
||||
// Get process start time on macOS and Linux
|
||||
const { execSync } = require('child_process');
|
||||
const result = execSync(`ps -o lstart= -p ${pid}`, { encoding: 'utf8' });
|
||||
const startTime = new Date(result.trim());
|
||||
|
|
@ -303,12 +303,12 @@ class TaskPingDaemon {
|
|||
|
||||
return `${hours}h ${minutes}m`;
|
||||
} catch (error) {
|
||||
return '未知';
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行接口
|
||||
// Command line interface
|
||||
if (require.main === module) {
|
||||
const daemon = new TaskPingDaemon();
|
||||
const command = process.argv[2];
|
||||
|
|
|
|||
|
|
@ -1 +1,38 @@
|
|||
{}
|
||||
{
|
||||
"7HUMGXOT": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753601264,
|
||||
"expiresAt": 1753687664,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"sessionId": "3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec",
|
||||
"tmuxSession": "hailuo",
|
||||
"description": "completed - TaskPing"
|
||||
},
|
||||
"5CLDW6NQ": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753602124,
|
||||
"expiresAt": 1753688524,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"sessionId": "c7b6750f-6246-4ed3-bca5-81201ab980ee",
|
||||
"tmuxSession": "hailuo",
|
||||
"description": "completed - TaskPing"
|
||||
},
|
||||
"ONY66DAE": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753604311,
|
||||
"expiresAt": 1753690711,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"sessionId": "2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621",
|
||||
"tmuxSession": "hailuo",
|
||||
"description": "waiting - TaskPing"
|
||||
},
|
||||
"QHFI9FIJ": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753607753,
|
||||
"expiresAt": 1753694153,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"sessionId": "a966681d-5cfd-47b9-bb1b-c7ee9655b97b",
|
||||
"tmuxSession": "a-0",
|
||||
"description": "waiting - TaskPing"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621",
|
||||
"token": "ONY66DAE",
|
||||
"type": "pty",
|
||||
"created": "2025-07-27T08:18:31.722Z",
|
||||
"expires": "2025-07-28T08:18:31.722Z",
|
||||
"createdAt": 1753604311,
|
||||
"expiresAt": 1753690711,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] Claude需要您的进一步指导"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec",
|
||||
"token": "7HUMGXOT",
|
||||
"type": "pty",
|
||||
"created": "2025-07-27T07:27:44.413Z",
|
||||
"expires": "2025-07-28T07:27:44.413Z",
|
||||
"createdAt": 1753601264,
|
||||
"expiresAt": 1753687664,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "a966681d-5cfd-47b9-bb1b-c7ee9655b97b",
|
||||
"token": "QHFI9FIJ",
|
||||
"type": "pty",
|
||||
"created": "2025-07-27T09:15:53.533Z",
|
||||
"expires": "2025-07-28T09:15:53.533Z",
|
||||
"createdAt": 1753607753,
|
||||
"expiresAt": 1753694153,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] Claude needs your further guidance"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "c7b6750f-6246-4ed3-bca5-81201ab980ee",
|
||||
"token": "5CLDW6NQ",
|
||||
"type": "pty",
|
||||
"created": "2025-07-27T07:42:04.988Z",
|
||||
"expires": "2025-07-28T07:42:04.988Z",
|
||||
"createdAt": 1753602124,
|
||||
"expiresAt": 1753688524,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Claude Command Bridge
|
||||
* 通过文件系统与Claude Code进行通信的桥接器
|
||||
* Bridge for communicating with Claude Code via file system
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
|
@ -25,17 +25,17 @@ class ClaudeCommandBridge {
|
|||
}
|
||||
|
||||
/**
|
||||
* 发送命令给Claude Code
|
||||
* @param {string} command - 要执行的命令
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
* Send command to Claude Code
|
||||
* @param {string} command - Command to execute
|
||||
* @param {string} sessionId - Session ID
|
||||
* @returns {Promise<boolean>} - Whether successful
|
||||
*/
|
||||
async sendCommand(command, sessionId) {
|
||||
try {
|
||||
const timestamp = Date.now();
|
||||
const commandId = `${sessionId}_${timestamp}`;
|
||||
|
||||
// 创建命令文件
|
||||
// Create command file
|
||||
const commandFile = path.join(this.commandsDir, `${commandId}.json`);
|
||||
const commandData = {
|
||||
id: commandId,
|
||||
|
|
@ -48,13 +48,13 @@ class ClaudeCommandBridge {
|
|||
|
||||
fs.writeFileSync(commandFile, JSON.stringify(commandData, null, 2));
|
||||
|
||||
// 创建通知文件 (Claude Code可以监控这个文件变化)
|
||||
// Create notification file (Claude Code can monitor this file for changes)
|
||||
const notificationFile = path.join(this.commandsDir, '.new_command');
|
||||
fs.writeFileSync(notificationFile, commandId);
|
||||
|
||||
this.logger.info(`Command sent via file bridge: ${commandId}`);
|
||||
|
||||
// 尝试使用系统通知
|
||||
// Try to use system notification
|
||||
await this._sendSystemNotification(command, commandId);
|
||||
|
||||
return true;
|
||||
|
|
@ -67,18 +67,18 @@ class ClaudeCommandBridge {
|
|||
|
||||
async _sendSystemNotification(command, commandId) {
|
||||
try {
|
||||
const title = 'TaskPing - 新邮件命令';
|
||||
const body = `命令: ${command.length > 50 ? command.substring(0, 50) + '...' : command}\n\n点击查看详情或在Claude Code中输入命令`;
|
||||
const title = 'TaskPing - New Email Command';
|
||||
const body = `Command: ${command.length > 50 ? command.substring(0, 50) + '...' : command}\n\nClick to view details or enter command in Claude Code`;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS 通知
|
||||
// macOS notification
|
||||
const script = `
|
||||
display notification "${body.replace(/"/g, '\\"')}" with title "${title}" sound name "default"
|
||||
`;
|
||||
const { spawn } = require('child_process');
|
||||
spawn('osascript', ['-e', script]);
|
||||
} else if (process.platform === 'linux') {
|
||||
// Linux 通知
|
||||
// Linux notification
|
||||
const { spawn } = require('child_process');
|
||||
spawn('notify-send', [title, body]);
|
||||
}
|
||||
|
|
@ -90,8 +90,8 @@ class ClaudeCommandBridge {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取待处理的命令
|
||||
* @returns {Array} 命令列表
|
||||
* Get pending commands
|
||||
* @returns {Array} Command list
|
||||
*/
|
||||
getPendingCommands() {
|
||||
try {
|
||||
|
|
@ -121,10 +121,10 @@ class ClaudeCommandBridge {
|
|||
}
|
||||
|
||||
/**
|
||||
* 标记命令为已处理
|
||||
* @param {string} commandId - 命令ID
|
||||
* @param {string} status - 状态 (completed/failed)
|
||||
* @param {string} response - 响应内容
|
||||
* Mark command as processed
|
||||
* @param {string} commandId - Command ID
|
||||
* @param {string} status - Status (completed/failed)
|
||||
* @param {string} response - Response content
|
||||
*/
|
||||
markCommandProcessed(commandId, status = 'completed', response = '') {
|
||||
try {
|
||||
|
|
@ -136,11 +136,11 @@ class ClaudeCommandBridge {
|
|||
commandData.processedAt = new Date().toISOString();
|
||||
commandData.response = response;
|
||||
|
||||
// 保存到响应目录
|
||||
// Save to response directory
|
||||
const responseFile = path.join(this.responseDir, `${commandId}.json`);
|
||||
fs.writeFileSync(responseFile, JSON.stringify(commandData, null, 2));
|
||||
|
||||
// 删除原命令文件
|
||||
// Delete original command file
|
||||
fs.unlinkSync(commandFile);
|
||||
|
||||
this.logger.info(`Command ${commandId} marked as ${status}`);
|
||||
|
|
@ -151,8 +151,8 @@ class ClaudeCommandBridge {
|
|||
}
|
||||
|
||||
/**
|
||||
* 清理旧的命令和响应文件
|
||||
* @param {number} maxAge - 最大保留时间(小时)
|
||||
* Clean up old command and response files
|
||||
* @param {number} maxAge - Maximum retention time (hours)
|
||||
*/
|
||||
cleanup(maxAge = 24) {
|
||||
const cutoff = Date.now() - (maxAge * 60 * 60 * 1000);
|
||||
|
|
@ -182,7 +182,7 @@ class ClaudeCommandBridge {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取桥接器状态
|
||||
* Get bridge status
|
||||
*/
|
||||
getStatus() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -72,29 +72,29 @@ class CommandRelayService extends EventEmitter {
|
|||
}
|
||||
|
||||
try {
|
||||
// 验证邮件配置
|
||||
// Validate email configuration
|
||||
if (!this.config.imap) {
|
||||
throw new Error('IMAP configuration required for command relay');
|
||||
}
|
||||
|
||||
// 启动邮件监听器
|
||||
// Start email listener
|
||||
this.emailListener = new EmailListener(this.config);
|
||||
|
||||
// 监听命令事件
|
||||
// Listen for command events
|
||||
this.emailListener.on('command', (commandData) => {
|
||||
this._queueCommand(commandData);
|
||||
});
|
||||
|
||||
// 启动邮件监听
|
||||
// Start email listening
|
||||
await this.emailListener.start();
|
||||
|
||||
// 启动命令处理
|
||||
// Start command processing
|
||||
this._startCommandProcessor();
|
||||
|
||||
this.isRunning = true;
|
||||
this.logger.info('Command relay service started successfully');
|
||||
|
||||
// 发送启动通知
|
||||
// Send startup notification
|
||||
this.emit('started');
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -110,13 +110,13 @@ class CommandRelayService extends EventEmitter {
|
|||
|
||||
this.isRunning = false;
|
||||
|
||||
// 停止邮件监听器
|
||||
// Stop email listener
|
||||
if (this.emailListener) {
|
||||
await this.emailListener.stop();
|
||||
this.emailListener = null;
|
||||
}
|
||||
|
||||
// 保存状态
|
||||
// Save state
|
||||
this._saveState();
|
||||
|
||||
this.logger.info('Command relay service stopped');
|
||||
|
|
@ -146,15 +146,15 @@ class CommandRelayService extends EventEmitter {
|
|||
}
|
||||
|
||||
_startCommandProcessor() {
|
||||
// 立即处理队列
|
||||
// Process queue immediately
|
||||
this._processCommandQueue();
|
||||
|
||||
// 定期处理队列
|
||||
// Process queue periodically
|
||||
setInterval(() => {
|
||||
if (this.isRunning) {
|
||||
this._processCommandQueue();
|
||||
}
|
||||
}, 5000); // 每5秒检查一次
|
||||
}, 5000); // Check every 5 seconds
|
||||
}
|
||||
|
||||
async _processCommandQueue() {
|
||||
|
|
@ -190,21 +190,21 @@ class CommandRelayService extends EventEmitter {
|
|||
commandItem.executedAt = new Date().toISOString();
|
||||
|
||||
try {
|
||||
// 检查Claude Code进程是否运行
|
||||
// Check if Claude Code process is running
|
||||
const claudeProcess = await this._findClaudeCodeProcess();
|
||||
|
||||
if (!claudeProcess || !claudeProcess.available) {
|
||||
throw new Error('Claude Code not available');
|
||||
}
|
||||
|
||||
// 执行命令 - 使用多种方式尝试
|
||||
// Execute command - try multiple methods
|
||||
const success = await this._sendCommandToClaudeCode(commandItem.command, claudeProcess, commandItem.sessionId);
|
||||
|
||||
if (success) {
|
||||
commandItem.status = 'completed';
|
||||
commandItem.completedAt = new Date().toISOString();
|
||||
|
||||
// 更新会话命令计数
|
||||
// Update session command count
|
||||
if (this.emailListener) {
|
||||
await this.emailListener.updateSessionCommandCount(commandItem.sessionId);
|
||||
}
|
||||
|
|
@ -231,7 +231,7 @@ class CommandRelayService extends EventEmitter {
|
|||
|
||||
async _findClaudeCodeProcess() {
|
||||
return new Promise((resolve) => {
|
||||
// 查找Claude Code相关进程
|
||||
// Find Claude Code related processes
|
||||
const ps = spawn('ps', ['aux']);
|
||||
let output = '';
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ class CommandRelayService extends EventEmitter {
|
|||
);
|
||||
|
||||
if (claudeProcesses.length > 0) {
|
||||
// 解析进程ID
|
||||
// Parse process ID
|
||||
const processLine = claudeProcesses[0];
|
||||
const parts = processLine.trim().split(/\s+/);
|
||||
const pid = parseInt(parts[1]);
|
||||
|
|
@ -263,7 +263,7 @@ class CommandRelayService extends EventEmitter {
|
|||
command: processLine
|
||||
});
|
||||
} else {
|
||||
// 如果没找到进程,假设 Claude Code 可以通过桌面自动化访问
|
||||
// If no process found, assume Claude Code can be accessed via desktop automation
|
||||
this.logger.debug('No Claude Code process found, will try desktop automation');
|
||||
resolve({ pid: null, available: true });
|
||||
}
|
||||
|
|
@ -271,7 +271,7 @@ class CommandRelayService extends EventEmitter {
|
|||
|
||||
ps.on('error', (error) => {
|
||||
this.logger.error('Error finding Claude Code process:', error.message);
|
||||
// 即使出错,也尝试桌面自动化
|
||||
// Even if error occurs, try desktop automation
|
||||
resolve({ pid: null, available: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -280,7 +280,7 @@ class CommandRelayService extends EventEmitter {
|
|||
async _sendCommandToClaudeCode(command, claudeProcess, sessionId) {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
// 方法1: Claude Code 专用自动化 (最直接和可靠)
|
||||
// Method 1: Claude Code dedicated automation (most direct and reliable)
|
||||
this.logger.info('Attempting to send command via Claude automation...');
|
||||
const claudeSuccess = await this.claudeAutomation.sendCommand(command, sessionId);
|
||||
|
||||
|
|
@ -290,7 +290,7 @@ class CommandRelayService extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// 方法2: 剪贴板自动化 (需要权限)
|
||||
// Method 2: Clipboard automation (requires permissions)
|
||||
if (this.clipboardAutomation.isSupported()) {
|
||||
this.logger.info('Attempting to send command via clipboard automation...');
|
||||
const clipboardSuccess = await this.clipboardAutomation.sendCommand(command);
|
||||
|
|
@ -303,7 +303,7 @@ class CommandRelayService extends EventEmitter {
|
|||
this.logger.warn('Clipboard automation failed, trying other methods...');
|
||||
}
|
||||
|
||||
// 方法3: 简单自动化方案 (包含多种备选方案)
|
||||
// Method 3: Simple automation solution (includes multiple fallback options)
|
||||
this.logger.info('Attempting to send command via simple automation...');
|
||||
const simpleSuccess = await this.simpleAutomation.sendCommand(command, sessionId);
|
||||
|
||||
|
|
@ -313,7 +313,7 @@ class CommandRelayService extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// 方法4: 使用命令桥接器 (文件方式)
|
||||
// Method 4: Use command bridge (file-based approach)
|
||||
this.logger.info('Attempting to send command via bridge...');
|
||||
const bridgeSuccess = await this.commandBridge.sendCommand(command, sessionId);
|
||||
|
||||
|
|
@ -323,7 +323,7 @@ class CommandRelayService extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// 方法5: 发送通知作为最后备选
|
||||
// Method 5: Send notification as final fallback
|
||||
this.logger.info('Using notification as final fallback...');
|
||||
this._sendCommandViaNotification(command)
|
||||
.then((success) => {
|
||||
|
|
@ -341,14 +341,14 @@ class CommandRelayService extends EventEmitter {
|
|||
|
||||
async _sendCommandViaMacOS(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 使用AppleScript自动化输入到活动窗口
|
||||
// Use AppleScript automation to input to active window
|
||||
const escapedCommand = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/'/g, "\\'");
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 获取当前活动应用
|
||||
-- Get current active application
|
||||
set activeApp to name of first application process whose frontmost is true
|
||||
|
||||
-- 尝试找到 Claude Code、Terminal 或其他开发工具
|
||||
-- Try to find Claude Code, Terminal or other development tools
|
||||
set targetApps to {"Claude Code", "Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code"}
|
||||
set foundApp to null
|
||||
|
||||
|
|
@ -362,18 +362,18 @@ class CommandRelayService extends EventEmitter {
|
|||
end repeat
|
||||
|
||||
if foundApp is not null then
|
||||
-- 切换到目标应用
|
||||
-- Switch to target application
|
||||
set frontmost of foundApp to true
|
||||
delay 1
|
||||
|
||||
-- 发送命令
|
||||
-- Send command
|
||||
keystroke "${escapedCommand}"
|
||||
delay 0.3
|
||||
keystroke return
|
||||
|
||||
return "success"
|
||||
else
|
||||
-- 如果没找到特定应用,尝试当前活动窗口
|
||||
-- If no specific application found, try current active window
|
||||
keystroke "${escapedCommand}"
|
||||
delay 0.3
|
||||
keystroke return
|
||||
|
|
@ -411,15 +411,15 @@ class CommandRelayService extends EventEmitter {
|
|||
}
|
||||
|
||||
async _sendCommandViaNotification(command) {
|
||||
// 作为备选方案,发送桌面通知提醒用户
|
||||
// As fallback option, send desktop notification to remind user
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const commandPreview = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS 通知,包含更多信息
|
||||
// macOS notification with more information
|
||||
const script = `
|
||||
display notification "命令: ${commandPreview.replace(/"/g, '\\"')}" with title "TaskPing - 邮件命令" subtitle "点击Terminal或Claude Code窗口,然后粘贴命令" sound name "default"
|
||||
display notification "Command: ${commandPreview.replace(/"/g, '\\"')}" with title "TaskPing - Email Command" subtitle "Click Terminal or Claude Code window, then paste command" sound name "default"
|
||||
`;
|
||||
const notification = spawn('osascript', ['-e', script]);
|
||||
|
||||
|
|
@ -432,10 +432,10 @@ class CommandRelayService extends EventEmitter {
|
|||
resolve(false);
|
||||
});
|
||||
} else {
|
||||
// Linux 通知
|
||||
// Linux notification
|
||||
const notification = spawn('notify-send', [
|
||||
'TaskPing - 邮件命令',
|
||||
`命令: ${commandPreview}`
|
||||
'TaskPing - Email Command',
|
||||
`Command: ${commandPreview}`
|
||||
]);
|
||||
|
||||
notification.on('close', () => {
|
||||
|
|
@ -459,12 +459,12 @@ class CommandRelayService extends EventEmitter {
|
|||
commandItem.retries = (commandItem.retries || 0) + 1;
|
||||
|
||||
if (commandItem.retries < commandItem.maxRetries) {
|
||||
// 重试
|
||||
// Retry
|
||||
commandItem.status = 'queued';
|
||||
commandItem.retryAt = new Date(Date.now() + (commandItem.retries * 60000)).toISOString(); // 延迟重试
|
||||
commandItem.retryAt = new Date(Date.now() + (commandItem.retries * 60000)).toISOString(); // Delayed retry
|
||||
this.logger.info(`Command ${commandItem.id} will be retried (attempt ${commandItem.retries + 1})`);
|
||||
} else {
|
||||
// 达到最大重试次数
|
||||
// Reached maximum retry count
|
||||
commandItem.status = 'failed';
|
||||
this.logger.error(`Command ${commandItem.id} failed after ${commandItem.retries} retries`);
|
||||
}
|
||||
|
|
@ -494,12 +494,12 @@ class CommandRelayService extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
// 手动清理已完成的命令
|
||||
// Manually cleanup completed commands
|
||||
cleanupCompletedCommands() {
|
||||
const beforeCount = this.commandQueue.length;
|
||||
this.commandQueue = this.commandQueue.filter(cmd =>
|
||||
cmd.status !== 'completed' ||
|
||||
new Date(cmd.completedAt) > new Date(Date.now() - 24 * 60 * 60 * 1000) // 保留24小时内的记录
|
||||
new Date(cmd.completedAt) > new Date(Date.now() - 24 * 60 * 60 * 1000) // Keep records within 24 hours
|
||||
);
|
||||
|
||||
const removedCount = beforeCount - this.commandQueue.length;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class EmailListener extends EventEmitter {
|
|||
this.isConnected = false;
|
||||
this.isListening = false;
|
||||
this.sessionsDir = path.join(__dirname, '../data/sessions');
|
||||
this.checkInterval = (config.template?.checkInterval || 30) * 1000; // 转换为毫秒
|
||||
this.checkInterval = (config.template?.checkInterval || 30) * 1000; // Convert to milliseconds
|
||||
this.lastCheckTime = new Date();
|
||||
|
||||
this._ensureDirectories();
|
||||
|
|
@ -98,7 +98,7 @@ class EmailListener extends EventEmitter {
|
|||
}
|
||||
|
||||
_startListening() {
|
||||
// 定期检查新邮件
|
||||
// Periodically check for new emails
|
||||
this._checkNewMails();
|
||||
setInterval(() => {
|
||||
if (this.isListening && this.isConnected) {
|
||||
|
|
@ -114,7 +114,7 @@ class EmailListener extends EventEmitter {
|
|||
} catch (error) {
|
||||
this.logger.error('Error checking emails:', error.message);
|
||||
|
||||
// 如果连接断开,尝试重连
|
||||
// If connection is lost, try to reconnect
|
||||
if (!this.isConnected) {
|
||||
this.logger.info('Attempting to reconnect...');
|
||||
try {
|
||||
|
|
@ -140,7 +140,7 @@ class EmailListener extends EventEmitter {
|
|||
|
||||
async _searchAndProcessMails() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 搜索最近的未读邮件
|
||||
// Search for recent unread emails
|
||||
const searchCriteria = [
|
||||
'UNSEEN',
|
||||
['SINCE', this.lastCheckTime]
|
||||
|
|
@ -213,40 +213,40 @@ class EmailListener extends EventEmitter {
|
|||
|
||||
async _handleParsedEmail(email, seqno) {
|
||||
try {
|
||||
// 检查是否是回复邮件
|
||||
// Check if it's a reply email
|
||||
if (!this._isReplyEmail(email)) {
|
||||
this.logger.debug(`Email ${seqno} is not a TaskPing reply`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取会话ID
|
||||
// Extract session ID
|
||||
const sessionId = this._extractSessionId(email);
|
||||
if (!sessionId) {
|
||||
this.logger.warn(`No session ID found in email ${seqno}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证会话
|
||||
// Validate session
|
||||
const session = await this._validateSession(sessionId);
|
||||
if (!session) {
|
||||
this.logger.warn(`Invalid session ID in email ${seqno}: ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取命令
|
||||
// Extract command
|
||||
const command = this._extractCommand(email);
|
||||
if (!command) {
|
||||
this.logger.warn(`No command found in email ${seqno}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全检查
|
||||
// Security check
|
||||
if (!this._isCommandSafe(command)) {
|
||||
this.logger.warn(`Unsafe command in email ${seqno}: ${command}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发出命令事件
|
||||
// Emit command event
|
||||
this.emit('command', {
|
||||
sessionId,
|
||||
command: command.trim(),
|
||||
|
|
@ -270,40 +270,40 @@ class EmailListener extends EventEmitter {
|
|||
}
|
||||
|
||||
_isReplyEmail(email) {
|
||||
// 检查主题是否包含 TaskPing 标识
|
||||
// Check if subject contains TaskPing identifier
|
||||
const subject = email.subject || '';
|
||||
if (!subject.includes('[TaskPing]')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是回复 (Re: 或 RE:)
|
||||
const isReply = /^(Re:|RE:|回复:)/i.test(subject);
|
||||
// Check if it's a reply (Re: or RE:)
|
||||
const isReply = /^(Re:|RE:|Reply:)/i.test(subject);
|
||||
if (isReply) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查邮件头中是否有会话ID
|
||||
// Check if session ID exists in email headers
|
||||
const sessionId = this._extractSessionId(email);
|
||||
return !!sessionId;
|
||||
}
|
||||
|
||||
_extractSessionId(email) {
|
||||
// 从邮件头中提取
|
||||
// Extract from email headers
|
||||
const headers = email.headers;
|
||||
if (headers && headers.get('x-taskping-session-id')) {
|
||||
return headers.get('x-taskping-session-id');
|
||||
}
|
||||
|
||||
// 从邮件正文中提取 (作为备用方案)
|
||||
// Extract from email body (as backup method)
|
||||
const text = email.text || '';
|
||||
const sessionMatch = text.match(/会话ID:\s*([a-f0-9-]{36})/i);
|
||||
const sessionMatch = text.match(/Session ID:\s*([a-f0-9-]{36})/i);
|
||||
if (sessionMatch) {
|
||||
return sessionMatch[1];
|
||||
}
|
||||
|
||||
// 从引用的邮件中提取
|
||||
// Extract from quoted email
|
||||
const html = email.html || '';
|
||||
const htmlSessionMatch = html.match(/会话ID:\s*<code>([a-f0-9-]{36})<\/code>/i);
|
||||
const htmlSessionMatch = html.match(/Session ID:\s*<code>([a-f0-9-]{36})<\/code>/i);
|
||||
if (htmlSessionMatch) {
|
||||
return htmlSessionMatch[1];
|
||||
}
|
||||
|
|
@ -321,18 +321,18 @@ class EmailListener extends EventEmitter {
|
|||
try {
|
||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
||||
|
||||
// 检查会话是否过期
|
||||
// Check if session has expired
|
||||
const now = new Date();
|
||||
const expires = new Date(sessionData.expires);
|
||||
|
||||
if (now > expires) {
|
||||
this.logger.debug(`Session ${sessionId} has expired`);
|
||||
// 删除过期会话
|
||||
// Delete expired session
|
||||
fs.unlinkSync(sessionFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查命令数量限制
|
||||
// Check command count limit
|
||||
if (sessionData.commandCount >= sessionData.maxCommands) {
|
||||
this.logger.debug(`Session ${sessionId} has reached command limit`);
|
||||
return null;
|
||||
|
|
@ -348,37 +348,37 @@ class EmailListener extends EventEmitter {
|
|||
_extractCommand(email) {
|
||||
let text = email.text || '';
|
||||
|
||||
// 移除邮件签名和引用内容
|
||||
// Remove email signature and quoted content
|
||||
text = this._cleanEmailContent(text);
|
||||
|
||||
// 移除空行和多余的空白字符
|
||||
// Remove empty lines and excess whitespace
|
||||
text = text.replace(/\n\s*\n/g, '\n').trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
_cleanEmailContent(text) {
|
||||
// 移除常见的邮件引用标记
|
||||
// Remove common email quote markers
|
||||
const lines = text.split('\n');
|
||||
const cleanLines = [];
|
||||
let foundOriginalMessage = false;
|
||||
|
||||
for (const line of lines) {
|
||||
// 检查是否到达原始邮件开始
|
||||
// Check if reached original email start
|
||||
if (line.includes('-----Original Message-----') ||
|
||||
line.includes('--- Original Message ---') ||
|
||||
line.includes('在') && line.includes('写道:') ||
|
||||
line.includes('at') && line.includes('wrote:') ||
|
||||
line.includes('On') && line.includes('wrote:') ||
|
||||
line.match(/^>\s*/) || // 引用标记
|
||||
line.includes('会话ID:')) {
|
||||
line.match(/^>\s*/) || // Quote marker
|
||||
line.includes('Session ID:')) {
|
||||
foundOriginalMessage = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 跳过常见的邮件签名
|
||||
// Skip common email signatures
|
||||
if (line.includes('--') ||
|
||||
line.includes('Sent from') ||
|
||||
line.includes('发自我的')) {
|
||||
line.includes('Sent from my')) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -389,12 +389,12 @@ class EmailListener extends EventEmitter {
|
|||
}
|
||||
|
||||
_isCommandSafe(command) {
|
||||
// 基本安全检查
|
||||
// Basic security check
|
||||
if (command.length > 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 危险命令黑名单
|
||||
// Dangerous command blacklist
|
||||
const dangerousPatterns = [
|
||||
/rm\s+-rf/i,
|
||||
/sudo\s+/i,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* relay-pty.js - 修复版本
|
||||
* 使用 node-imap 替代 ImapFlow 来解决飞书邮箱兼容性问题
|
||||
* relay-pty.js - Fixed version
|
||||
* Uses node-imap instead of ImapFlow to resolve Feishu email compatibility issues
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
|
@ -13,7 +13,7 @@ const { existsSync, readFileSync, writeFileSync } = require('fs');
|
|||
const path = require('path');
|
||||
const pino = require('pino');
|
||||
|
||||
// 配置日志
|
||||
// Configure logging
|
||||
const log = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transport: {
|
||||
|
|
@ -25,23 +25,23 @@ const log = pino({
|
|||
}
|
||||
});
|
||||
|
||||
// 全局配置
|
||||
// Global configuration
|
||||
const SESS_PATH = process.env.SESSION_MAP_PATH || path.join(__dirname, '../data/session-map.json');
|
||||
const PROCESSED_PATH = path.join(__dirname, '../data/processed-messages.json');
|
||||
const ALLOWED_SENDERS = (process.env.ALLOWED_SENDERS || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
||||
const PTY_POOL = new Map();
|
||||
let PROCESSED_MESSAGES = new Set();
|
||||
|
||||
// 加载已处理消息
|
||||
// Load processed messages
|
||||
function loadProcessedMessages() {
|
||||
if (existsSync(PROCESSED_PATH)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(PROCESSED_PATH, 'utf8'));
|
||||
const now = Date.now();
|
||||
// 只保留7天内的记录
|
||||
// Keep only records from the last 7 days
|
||||
const validMessages = data.filter(item => (now - item.timestamp) < 7 * 24 * 60 * 60 * 1000);
|
||||
PROCESSED_MESSAGES = new Set(validMessages.map(item => item.id));
|
||||
// 更新文件,移除过期记录
|
||||
// Update file, remove expired records
|
||||
saveProcessedMessages();
|
||||
} catch (error) {
|
||||
log.error({ error }, 'Failed to load processed messages');
|
||||
|
|
@ -50,7 +50,7 @@ function loadProcessedMessages() {
|
|||
}
|
||||
}
|
||||
|
||||
// 保存已处理消息
|
||||
// Save processed messages
|
||||
function saveProcessedMessages() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
|
@ -59,7 +59,7 @@ function saveProcessedMessages() {
|
|||
timestamp: now
|
||||
}));
|
||||
|
||||
// 确保目录存在
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(PROCESSED_PATH);
|
||||
if (!existsSync(dir)) {
|
||||
require('fs').mkdirSync(dir, { recursive: true });
|
||||
|
|
@ -71,7 +71,7 @@ function saveProcessedMessages() {
|
|||
}
|
||||
}
|
||||
|
||||
// 加载会话映射
|
||||
// Load session mapping
|
||||
function loadSessions() {
|
||||
if (!existsSync(SESS_PATH)) return {};
|
||||
try {
|
||||
|
|
@ -82,14 +82,14 @@ function loadSessions() {
|
|||
}
|
||||
}
|
||||
|
||||
// 检查发件人是否在白名单中
|
||||
// Check if sender is in whitelist
|
||||
function isAllowed(fromAddress) {
|
||||
if (!fromAddress) return false;
|
||||
const addr = fromAddress.toLowerCase();
|
||||
return ALLOWED_SENDERS.some(allowed => addr.includes(allowed));
|
||||
}
|
||||
|
||||
// 从主题中提取 TaskPing token
|
||||
// Extract TaskPing token from subject
|
||||
function extractTokenFromSubject(subject = '') {
|
||||
const patterns = [
|
||||
/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/,
|
||||
|
|
@ -105,23 +105,23 @@ function extractTokenFromSubject(subject = '') {
|
|||
return null;
|
||||
}
|
||||
|
||||
// 清理邮件正文
|
||||
// Clean email text
|
||||
function cleanEmailText(text = '') {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const cleanLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// 检测引用内容(更全面的检测)
|
||||
// Detect quoted content (more comprehensive detection)
|
||||
if (line.includes('-----Original Message-----') ||
|
||||
line.includes('--- Original Message ---') ||
|
||||
line.includes('在') && line.includes('写道:') ||
|
||||
line.includes('at') && line.includes('wrote:') ||
|
||||
line.includes('On') && line.includes('wrote:') ||
|
||||
line.includes('会话ID:') ||
|
||||
line.includes('Session ID:') ||
|
||||
line.includes('Session ID:') ||
|
||||
line.includes('<noreply@pandalla.ai>') ||
|
||||
line.includes('TaskPing 通知系统') ||
|
||||
line.includes('于2025年') && line.includes('写道:') ||
|
||||
line.match(/^>.*/) || // 引用行以 > 开头
|
||||
line.includes('TaskPing Notification System') ||
|
||||
line.includes('on 2025') && line.includes('wrote:') ||
|
||||
line.match(/^>.*/) || // Quote lines start with >
|
||||
line.includes('From:') && line.includes('@') ||
|
||||
line.includes('To:') && line.includes('@') ||
|
||||
line.includes('Subject:') ||
|
||||
|
|
@ -130,71 +130,71 @@ function cleanEmailText(text = '') {
|
|||
break;
|
||||
}
|
||||
|
||||
// 检测邮件签名
|
||||
// Detect email signature
|
||||
if (line.match(/^--\s*$/) ||
|
||||
line.includes('Sent from') ||
|
||||
line.includes('发自我的') ||
|
||||
line.includes('Sent from my') ||
|
||||
line.includes('Best regards') ||
|
||||
line.includes('此致敬礼')) {
|
||||
line.includes('Sincerely')) {
|
||||
break;
|
||||
}
|
||||
|
||||
cleanLines.push(line);
|
||||
}
|
||||
|
||||
// 获取有效内容
|
||||
// Get valid content
|
||||
const cleanText = cleanLines.join('\n').trim();
|
||||
|
||||
// 查找实际的命令内容(跳过问候语等)
|
||||
// Find actual command content (skip greetings, etc.)
|
||||
const contentLines = cleanText.split(/\r?\n/).filter(l => l.trim().length > 0);
|
||||
|
||||
// 查找命令行(通常是包含实际命令的行)
|
||||
// Find command line (usually contains the actual command)
|
||||
for (const line of contentLines) {
|
||||
const trimmedLine = line.trim();
|
||||
// 跳过常见的问候语
|
||||
if (trimmedLine.match(/^(hi|hello|谢谢|thanks|好的|ok|是的|yes)/i)) {
|
||||
// Skip common greetings
|
||||
if (trimmedLine.match(/^(hi|hello|thank you|thanks|ok|yes)/i)) {
|
||||
continue;
|
||||
}
|
||||
// 跳过纯中文问候
|
||||
if (trimmedLine.match(/^(这是|请|帮我|您好)/)) {
|
||||
// Skip pure Chinese greetings
|
||||
if (trimmedLine.match(/^(this is|please|help me|hello)/)) {
|
||||
continue;
|
||||
}
|
||||
// 跳过邮件引用残留
|
||||
if (trimmedLine.includes('TaskPing 通知系统') ||
|
||||
// Skip remaining email quotes
|
||||
if (trimmedLine.includes('TaskPing Notification System') ||
|
||||
trimmedLine.includes('<noreply@pandalla.ai>') ||
|
||||
trimmedLine.includes('于2025年')) {
|
||||
trimmedLine.includes('on 2025')) {
|
||||
continue;
|
||||
}
|
||||
// 如果找到疑似命令的行,检查并去重
|
||||
// If a suspected command line is found, check and deduplicate
|
||||
if (trimmedLine.length > 3) {
|
||||
const command = trimmedLine.slice(0, 8192);
|
||||
// 检查命令是否重复(如:"喝可乐好吗喝可乐好吗")
|
||||
// Check if command is duplicated (e.g., "drink cola okay drink cola okay")
|
||||
const deduplicatedCommand = deduplicateCommand(command);
|
||||
return deduplicatedCommand;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到明显的命令,返回第一行非空内容(并去重)
|
||||
// If no obvious command is found, return first non-empty line (and deduplicate)
|
||||
const firstLine = contentLines[0] || '';
|
||||
const command = firstLine.slice(0, 8192).trim();
|
||||
return deduplicateCommand(command);
|
||||
}
|
||||
|
||||
// 去重复的命令文本(处理如:"喝可乐好吗喝可乐好吗" -> "喝可乐好吗")
|
||||
// Deduplicate command text (handle cases like: "drink cola okay drink cola okay" -> "drink cola okay")
|
||||
function deduplicateCommand(command) {
|
||||
if (!command || command.length === 0) {
|
||||
return command;
|
||||
}
|
||||
|
||||
// 检查命令是否是自己重复的
|
||||
// Check if command is self-repeating
|
||||
const length = command.length;
|
||||
for (let i = 1; i <= Math.floor(length / 2); i++) {
|
||||
const firstPart = command.substring(0, i);
|
||||
const remaining = command.substring(i);
|
||||
|
||||
// 检查剩余部分是否完全重复第一部分
|
||||
// Check if remaining part completely repeats the first part
|
||||
if (remaining === firstPart.repeat(Math.floor(remaining.length / firstPart.length))) {
|
||||
// 找到重复模式,返回第一部分
|
||||
// Found repetition pattern, return first part
|
||||
log.debug({
|
||||
originalCommand: command,
|
||||
deduplicatedCommand: firstPart,
|
||||
|
|
@ -204,11 +204,11 @@ function deduplicateCommand(command) {
|
|||
}
|
||||
}
|
||||
|
||||
// 没有检测到重复,返回原命令
|
||||
// No repetition detected, return original command
|
||||
return command;
|
||||
}
|
||||
|
||||
// 无人值守远程命令注入 - tmux优先,智能备用
|
||||
// Unattended remote command injection - tmux priority, smart fallback
|
||||
async function injectCommandRemote(token, command) {
|
||||
const sessions = loadSessions();
|
||||
const session = sessions[token];
|
||||
|
|
@ -218,7 +218,7 @@ async function injectCommandRemote(token, command) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 检查会话是否过期
|
||||
// Check if session has expired
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (session.expiresAt && session.expiresAt < now) {
|
||||
log.warn({ token }, 'Session expired');
|
||||
|
|
@ -228,7 +228,7 @@ async function injectCommandRemote(token, command) {
|
|||
try {
|
||||
log.info({ token, command }, 'Starting remote command injection');
|
||||
|
||||
// 方法1: 优先使用tmux无人值守注入
|
||||
// Method 1: Prefer tmux unattended injection
|
||||
const TmuxInjector = require('./tmux-injector');
|
||||
const tmuxSessionName = session.tmuxSession || 'claude-taskping';
|
||||
const tmuxInjector = new TmuxInjector(log, tmuxSessionName);
|
||||
|
|
@ -241,7 +241,7 @@ async function injectCommandRemote(token, command) {
|
|||
} else {
|
||||
log.warn({ token, error: tmuxResult.error }, 'Tmux injection failed, trying smart fallback');
|
||||
|
||||
// 方法2: 回退到智能注入器
|
||||
// Method 2: Fall back to smart injector
|
||||
const SmartInjector = require('./smart-injector');
|
||||
const smartInjector = new SmartInjector(log);
|
||||
|
||||
|
|
@ -262,10 +262,10 @@ async function injectCommandRemote(token, command) {
|
|||
}
|
||||
}
|
||||
|
||||
// 尝试自动粘贴到活跃窗口
|
||||
// Try automatic paste to active window
|
||||
async function tryAutoPaste(command) {
|
||||
return new Promise((resolve) => {
|
||||
// 先复制命令到剪贴板
|
||||
// First copy command to clipboard
|
||||
const { spawn } = require('child_process');
|
||||
const pbcopy = spawn('pbcopy');
|
||||
pbcopy.stdin.write(command);
|
||||
|
|
@ -277,7 +277,7 @@ async function tryAutoPaste(command) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 执行AppleScript自动粘贴
|
||||
// Execute AppleScript auto-paste
|
||||
const autoScript = `
|
||||
tell application "System Events"
|
||||
set claudeApps to {"Claude", "Claude Code", "Terminal", "iTerm2", "iTerm"}
|
||||
|
|
@ -333,10 +333,10 @@ async function tryAutoPaste(command) {
|
|||
|
||||
switch(result) {
|
||||
case 'terminal_typed':
|
||||
resolve({ success: true, method: '终端直接输入' });
|
||||
resolve({ success: true, method: 'Terminal direct input' });
|
||||
break;
|
||||
case 'claude_pasted':
|
||||
resolve({ success: true, method: 'Claude应用粘贴' });
|
||||
resolve({ success: true, method: 'Claude app paste' });
|
||||
break;
|
||||
case 'no_target_found':
|
||||
resolve({ success: false, error: 'no_target_application' });
|
||||
|
|
@ -349,10 +349,10 @@ async function tryAutoPaste(command) {
|
|||
});
|
||||
}
|
||||
|
||||
// 回退到剪贴板+强提醒
|
||||
// Fallback to clipboard + strong reminder
|
||||
async function fallbackToClipboard(command) {
|
||||
return new Promise((resolve) => {
|
||||
// 复制到剪贴板
|
||||
// Copy to clipboard
|
||||
const { spawn } = require('child_process');
|
||||
const pbcopy = spawn('pbcopy');
|
||||
pbcopy.stdin.write(command);
|
||||
|
|
@ -364,10 +364,10 @@ async function fallbackToClipboard(command) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 发送强提醒通知
|
||||
// Send strong reminder notification
|
||||
const shortCommand = command.length > 30 ? command.substring(0, 30) + '...' : command;
|
||||
const notificationScript = `
|
||||
display notification "🚨 邮件命令已自动复制!请立即在Claude Code中粘贴执行 (Cmd+V)" with title "TaskPing 自动注入" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Basso"
|
||||
display notification "🚨 Email command auto-copied! Please paste and execute in Claude Code immediately (Cmd+V)" with title "TaskPing Auto-Injection" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Basso"
|
||||
`;
|
||||
|
||||
const { exec } = require('child_process');
|
||||
|
|
@ -383,15 +383,15 @@ async function fallbackToClipboard(command) {
|
|||
});
|
||||
}
|
||||
|
||||
// 处理邮件消息
|
||||
// Handle email message
|
||||
async function handleMailMessage(parsed) {
|
||||
try {
|
||||
log.debug({ uid: parsed.uid, messageId: parsed.messageId }, 'handleMailMessage called');
|
||||
// 简化的重复检测(UID已在前面检查过)
|
||||
// Simplified duplicate detection (UID already checked earlier)
|
||||
const uid = parsed.uid;
|
||||
const messageId = parsed.messageId;
|
||||
|
||||
// 仅对没有UID的邮件进行额外检查
|
||||
// Only perform additional checks for emails without UID
|
||||
if (!uid) {
|
||||
const identifier = messageId;
|
||||
if (identifier && PROCESSED_MESSAGES.has(identifier)) {
|
||||
|
|
@ -399,7 +399,7 @@ async function handleMailMessage(parsed) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 内容哈希去重(作为最后手段)
|
||||
// Content hash deduplication (as last resort)
|
||||
const emailSubject = parsed.subject || '';
|
||||
const emailDate = parsed.date || new Date();
|
||||
const contentHash = `${emailSubject}_${emailDate.getTime()}`;
|
||||
|
|
@ -410,13 +410,13 @@ async function handleMailMessage(parsed) {
|
|||
}
|
||||
}
|
||||
|
||||
// 验证发件人
|
||||
// Verify sender
|
||||
if (!isAllowed(parsed.from?.text || '')) {
|
||||
log.warn({ from: parsed.from?.text }, 'Sender not allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取token
|
||||
// Extract token
|
||||
const subject = parsed.subject || '';
|
||||
const token = extractTokenFromSubject(subject);
|
||||
|
||||
|
|
@ -425,7 +425,7 @@ async function handleMailMessage(parsed) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 提取命令 - 添加详细调试
|
||||
// Extract command - add detailed debugging
|
||||
log.debug({
|
||||
token,
|
||||
rawEmailText: parsed.text?.substring(0, 500),
|
||||
|
|
@ -447,7 +447,7 @@ async function handleMailMessage(parsed) {
|
|||
|
||||
log.info({ token, command }, 'Processing email command');
|
||||
|
||||
// 无人值守远程命令注入(tmux优先,智能备用)
|
||||
// Unattended remote command injection (tmux priority, smart fallback)
|
||||
const success = await injectCommandRemote(token, command);
|
||||
|
||||
if (!success) {
|
||||
|
|
@ -455,19 +455,19 @@ async function handleMailMessage(parsed) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 标记为已处理(只在成功处理后标记)
|
||||
// Mark as processed (only mark after successful processing)
|
||||
if (uid) {
|
||||
// 标记UID为已处理
|
||||
// Mark UID as processed
|
||||
PROCESSED_MESSAGES.add(uid);
|
||||
log.debug({ uid }, 'Marked message UID as processed');
|
||||
} else {
|
||||
// 没有UID的邮件,使用messageId和内容哈希
|
||||
// For emails without UID, use messageId and content hash
|
||||
if (messageId) {
|
||||
PROCESSED_MESSAGES.add(messageId);
|
||||
log.debug({ messageId }, 'Marked message as processed by messageId');
|
||||
}
|
||||
|
||||
// 内容哈希标记
|
||||
// Content hash marking
|
||||
const emailSubject = parsed.subject || '';
|
||||
const emailDate = parsed.date || new Date();
|
||||
const contentHash = `${emailSubject}_${emailDate.getTime()}`;
|
||||
|
|
@ -475,7 +475,7 @@ async function handleMailMessage(parsed) {
|
|||
log.debug({ contentHash }, 'Marked message as processed by content hash');
|
||||
}
|
||||
|
||||
// 持久化已处理消息
|
||||
// Persist processed messages
|
||||
saveProcessedMessages();
|
||||
|
||||
log.info({ token }, 'Command injected successfully via remote method');
|
||||
|
|
@ -485,9 +485,9 @@ async function handleMailMessage(parsed) {
|
|||
}
|
||||
}
|
||||
|
||||
// 启动IMAP监听
|
||||
// Start IMAP listening
|
||||
function startImap() {
|
||||
// 首先加载已处理消息
|
||||
// First load processed messages
|
||||
loadProcessedMessages();
|
||||
|
||||
log.info('Starting relay-pty service', {
|
||||
|
|
@ -521,29 +521,29 @@ function startImap() {
|
|||
|
||||
log.info(`Mailbox opened: ${box.messages.total} total messages, ${box.messages.new} new`);
|
||||
|
||||
// 只在启动时处理现有的未读邮件
|
||||
// Only process existing unread emails at startup
|
||||
processExistingEmails(imap);
|
||||
|
||||
// 监听新邮件(主要机制)
|
||||
// Listen for new emails (main mechanism)
|
||||
imap.on('mail', function(numNewMsgs) {
|
||||
log.info({ newMessages: numNewMsgs }, 'New mail arrived');
|
||||
// 增加延迟,避免与现有邮件处理冲突
|
||||
// Add delay to avoid conflicts with existing email processing
|
||||
setTimeout(() => {
|
||||
processNewEmails(imap);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 定期检查新邮件(仅作为备用,延长间隔)
|
||||
// Periodic check for new emails (backup only, extended interval)
|
||||
setInterval(() => {
|
||||
log.debug('Periodic email check...');
|
||||
processNewEmails(imap);
|
||||
}, 120000); // 每2分钟检查一次,减少频率
|
||||
}, 120000); // Check every 2 minutes, reduced frequency
|
||||
});
|
||||
});
|
||||
|
||||
imap.once('error', function(err) {
|
||||
log.error({ error: err.message }, 'IMAP error');
|
||||
// 重连机制
|
||||
// Reconnection mechanism
|
||||
setTimeout(() => {
|
||||
log.info('Attempting to reconnect...');
|
||||
startImap();
|
||||
|
|
@ -556,7 +556,7 @@ function startImap() {
|
|||
|
||||
imap.connect();
|
||||
|
||||
// 优雅关闭
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
log.info('Shutting down gracefully...');
|
||||
imap.end();
|
||||
|
|
@ -564,9 +564,9 @@ function startImap() {
|
|||
});
|
||||
}
|
||||
|
||||
// 处理现有邮件
|
||||
// Process existing emails
|
||||
function processExistingEmails(imap) {
|
||||
// 搜索未读邮件
|
||||
// Search unread emails
|
||||
imap.search(['UNSEEN'], function(err, results) {
|
||||
if (err) {
|
||||
log.error({ error: err.message }, 'Failed to search emails');
|
||||
|
|
@ -583,9 +583,9 @@ function processExistingEmails(imap) {
|
|||
});
|
||||
}
|
||||
|
||||
// 处理新邮件
|
||||
// Process new emails
|
||||
function processNewEmails(imap) {
|
||||
// 搜索最近5分钟的邮件
|
||||
// Search emails from the last 5 minutes
|
||||
const since = new Date();
|
||||
since.setMinutes(since.getMinutes() - 5);
|
||||
const sinceStr = since.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
|
@ -603,12 +603,12 @@ function processNewEmails(imap) {
|
|||
});
|
||||
}
|
||||
|
||||
// 获取并处理邮件
|
||||
// Fetch and process emails
|
||||
function fetchAndProcessEmails(imap, uids) {
|
||||
log.debug({ uids }, 'Starting to fetch emails');
|
||||
const fetch = imap.fetch(uids, {
|
||||
bodies: '', // 获取完整邮件
|
||||
markSeen: true // 标记为已读
|
||||
bodies: '', // Get complete email
|
||||
markSeen: true // Mark as read
|
||||
});
|
||||
|
||||
fetch.on('message', function(msg, seqno) {
|
||||
|
|
@ -618,21 +618,21 @@ function fetchAndProcessEmails(imap, uids) {
|
|||
let bodyProcessed = false;
|
||||
let attributesReceived = false;
|
||||
|
||||
// 获取UID以防重复处理
|
||||
// Get UID to prevent duplicate processing
|
||||
msg.once('attributes', function(attrs) {
|
||||
messageUid = attrs.uid;
|
||||
attributesReceived = true;
|
||||
log.debug({ uid: messageUid, seqno }, 'Received attributes');
|
||||
|
||||
// 只检查是否已处理,不要立即标记
|
||||
// Only check if already processed, don't mark immediately
|
||||
if (messageUid && PROCESSED_MESSAGES.has(messageUid)) {
|
||||
log.debug({ uid: messageUid, seqno }, 'Message UID already processed, skipping entire message');
|
||||
skipProcessing = true;
|
||||
return; // 直接返回,不继续处理
|
||||
return; // Return directly, do not continue processing
|
||||
}
|
||||
log.debug({ uid: messageUid, seqno }, 'Message UID ready for processing');
|
||||
|
||||
// 如果body已经处理完了,现在可以解析邮件
|
||||
// If body is processed, can now parse email
|
||||
if (bodyProcessed && !skipProcessing) {
|
||||
processEmailBuffer(buffer, messageUid, seqno);
|
||||
}
|
||||
|
|
@ -647,14 +647,14 @@ function fetchAndProcessEmails(imap, uids) {
|
|||
bodyProcessed = true;
|
||||
log.debug({ uid: messageUid, seqno, bufferLength: buffer.length, attributesReceived }, 'Body stream ended');
|
||||
|
||||
// 如果attributes已经收到且没有标记跳过,现在可以解析邮件
|
||||
// If attributes received and not marked to skip, can now parse email
|
||||
if (attributesReceived && !skipProcessing) {
|
||||
processEmailBuffer(buffer, messageUid, seqno);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 分离出的邮件处理函数
|
||||
// Separated email processing function
|
||||
function processEmailBuffer(buffer, uid, seqno) {
|
||||
if (buffer.length > 0 && uid) {
|
||||
log.debug({ uid, seqno }, 'Starting email parsing');
|
||||
|
|
@ -687,7 +687,7 @@ function fetchAndProcessEmails(imap, uids) {
|
|||
});
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
// Start service
|
||||
if (require.main === module) {
|
||||
startImap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 智能命令注入器 - 多种方式确保命令能够到达Claude Code
|
||||
* Smart Command Injector - Multiple methods to ensure commands reach Claude Code
|
||||
*/
|
||||
|
||||
const { exec, spawn } = require('child_process');
|
||||
|
|
@ -22,7 +22,7 @@ class SmartInjector {
|
|||
}
|
||||
|
||||
async injectCommand(token, command) {
|
||||
this.log.info(`🎯 智能注入命令: ${command.slice(0, 50)}...`);
|
||||
this.log.info(`🎯 Smart command injection: ${command.slice(0, 50)}...`);
|
||||
|
||||
const methods = [
|
||||
this.tryAppleScriptInjection.bind(this),
|
||||
|
|
@ -32,31 +32,31 @@ class SmartInjector {
|
|||
];
|
||||
|
||||
for (let i = 0; i < methods.length; i++) {
|
||||
const methodName = ['AppleScript自动注入', '文件拖拽注入', '持久通知注入', '紧急剪贴板'][i];
|
||||
const methodName = ['AppleScript Auto-injection', 'File Drag Injection', 'Persistent Notification Injection', 'Emergency Clipboard'][i];
|
||||
|
||||
try {
|
||||
this.log.info(`🔄 尝试方法 ${i + 1}: ${methodName}`);
|
||||
this.log.info(`🔄 Trying method ${i + 1}: ${methodName}`);
|
||||
const result = await methods[i](token, command);
|
||||
|
||||
if (result.success) {
|
||||
this.log.info(`✅ ${methodName}成功: ${result.message}`);
|
||||
this.log.info(`✅ ${methodName} successful: ${result.message}`);
|
||||
return true;
|
||||
} else {
|
||||
this.log.warn(`⚠️ ${methodName}失败: ${result.error}`);
|
||||
this.log.warn(`⚠️ ${methodName} failed: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.error(`❌ ${methodName}异常: ${error.message}`);
|
||||
this.log.error(`❌ ${methodName} exception: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log.error('🚨 所有注入方法都失败了');
|
||||
this.log.error('🚨 All injection methods failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 方法1: AppleScript自动注入
|
||||
// Method 1: AppleScript Auto-injection
|
||||
async tryAppleScriptInjection(token, command) {
|
||||
return new Promise((resolve) => {
|
||||
// 先复制到剪贴板
|
||||
// First copy to clipboard
|
||||
this.copyToClipboard(command).then(() => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
|
|
@ -87,7 +87,7 @@ class SmartInjector {
|
|||
|
||||
exec(`osascript -e '${script}'`, (error, stdout) => {
|
||||
if (error) {
|
||||
if (error.message.includes('1002') || error.message.includes('不允许')) {
|
||||
if (error.message.includes('1002') || error.message.includes('not allowed')) {
|
||||
resolve({ success: false, error: 'permission_denied' });
|
||||
} else {
|
||||
resolve({ success: false, error: error.message });
|
||||
|
|
@ -95,7 +95,7 @@ class SmartInjector {
|
|||
} else {
|
||||
const result = stdout.trim();
|
||||
if (result === 'success') {
|
||||
resolve({ success: true, message: '自动粘贴成功' });
|
||||
resolve({ success: true, message: 'Auto-paste successful' });
|
||||
} else {
|
||||
resolve({ success: false, error: result });
|
||||
}
|
||||
|
|
@ -105,27 +105,27 @@ class SmartInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 方法2: 文件拖拽注入
|
||||
// Method 2: File Drag Injection
|
||||
async tryFileDropInjection(token, command) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// 创建临时命令文件
|
||||
// Create temporary command file
|
||||
const fileName = `taskping-command-${token}.txt`;
|
||||
const filePath = path.join(this.tempDir, fileName);
|
||||
|
||||
fs.writeFileSync(filePath, command);
|
||||
|
||||
// 复制文件路径到剪贴板
|
||||
// Copy file path to clipboard
|
||||
this.copyToClipboard(filePath).then(() => {
|
||||
// 发送通知指导用户
|
||||
// Send notification to guide user
|
||||
const notificationScript = `
|
||||
display notification "💡 命令文件已创建并复制路径到剪贴板!\\n1. 在Finder中按Cmd+G并粘贴路径\\n2. 将文件拖拽到Claude Code窗口" with title "TaskPing 文件注入" subtitle "拖拽文件: ${fileName}" sound name "Glass"
|
||||
display notification "💡 Command file created and path copied to clipboard!\\n1. Press Cmd+G in Finder and paste path\\n2. Drag file to Claude Code window" with title "TaskPing File Injection" subtitle "Drag file: ${fileName}" sound name "Glass"
|
||||
`;
|
||||
|
||||
exec(`osascript -e '${notificationScript}'`, () => {
|
||||
// 尝试自动打开Finder到目标目录
|
||||
// Try to automatically open Finder to target directory
|
||||
exec(`open "${this.tempDir}"`, () => {
|
||||
resolve({ success: true, message: '文件已创建,通知已发送' });
|
||||
resolve({ success: true, message: 'File created, notification sent' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -136,15 +136,15 @@ class SmartInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 方法3: 持久通知注入
|
||||
// Method 3: Persistent Notification Injection
|
||||
async tryClipboardWithPersistentNotification(token, command) {
|
||||
return new Promise((resolve) => {
|
||||
this.copyToClipboard(command).then(() => {
|
||||
// 发送多次通知确保用户看到
|
||||
// Send multiple notifications to ensure user sees them
|
||||
const notifications = [
|
||||
{ delay: 0, sound: 'Basso', message: '🚨 邮件命令已复制!请立即粘贴到Claude Code (Cmd+V)' },
|
||||
{ delay: 3000, sound: 'Ping', message: '⏰ 提醒:命令仍在剪贴板中,请粘贴执行' },
|
||||
{ delay: 8000, sound: 'Purr', message: '💡 最后提醒:在Claude Code中按Cmd+V粘贴命令' }
|
||||
{ delay: 0, sound: 'Basso', message: '🚨 Email command copied! Please paste immediately to Claude Code (Cmd+V)' },
|
||||
{ delay: 3000, sound: 'Ping', message: '⏰ Reminder: Command still in clipboard, please paste and execute' },
|
||||
{ delay: 8000, sound: 'Purr', message: '💡 Final reminder: Press Cmd+V in Claude Code to paste command' }
|
||||
];
|
||||
|
||||
let completedNotifications = 0;
|
||||
|
|
@ -152,13 +152,13 @@ class SmartInjector {
|
|||
notifications.forEach((notif, index) => {
|
||||
setTimeout(() => {
|
||||
const script = `
|
||||
display notification "${notif.message}" with title "TaskPing 持久提醒 ${index + 1}/3" subtitle "${command.slice(0, 30)}..." sound name "${notif.sound}"
|
||||
display notification "${notif.message}" with title "TaskPing Persistent Reminder ${index + 1}/3" subtitle "${command.slice(0, 30)}..." sound name "${notif.sound}"
|
||||
`;
|
||||
|
||||
exec(`osascript -e '${script}'`, () => {
|
||||
completedNotifications++;
|
||||
if (completedNotifications === notifications.length) {
|
||||
resolve({ success: true, message: '持久通知序列完成' });
|
||||
resolve({ success: true, message: 'Persistent notification sequence completed' });
|
||||
}
|
||||
});
|
||||
}, notif.delay);
|
||||
|
|
@ -170,31 +170,31 @@ class SmartInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 方法4: 紧急剪贴板(最后手段)
|
||||
// Method 4: Emergency Clipboard (last resort)
|
||||
async tryUrgentClipboard(token, command) {
|
||||
return new Promise((resolve) => {
|
||||
this.copyToClipboard(command).then(() => {
|
||||
// 创建桌面快捷文件
|
||||
// Create desktop shortcut file
|
||||
const desktopPath = path.join(require('os').homedir(), 'Desktop');
|
||||
const shortcutContent = `#!/bin/bash
|
||||
echo "TaskPing命令: ${command}"
|
||||
echo "已复制到剪贴板,请在Claude Code中按Cmd+V粘贴"
|
||||
echo "TaskPing Command: ${command}"
|
||||
echo "Copied to clipboard, please press Cmd+V in Claude Code to paste"
|
||||
echo "${command}" | pbcopy
|
||||
echo "✅ 命令已刷新到剪贴板"
|
||||
echo "✅ Command refreshed to clipboard"
|
||||
`;
|
||||
|
||||
const shortcutPath = path.join(desktopPath, `TaskPing-${token}.command`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(shortcutPath, shortcutContent);
|
||||
fs.chmodSync(shortcutPath, '755'); // 可执行权限
|
||||
fs.chmodSync(shortcutPath, '755'); // Executable permission
|
||||
|
||||
const script = `
|
||||
display notification "🆘 紧急模式:桌面已创建快捷文件 TaskPing-${token}.command\\n双击可重新复制命令到剪贴板" with title "TaskPing 紧急模式" subtitle "命令: ${command.slice(0, 20)}..." sound name "Sosumi"
|
||||
display notification "🆘 Emergency Mode: Desktop shortcut file TaskPing-${token}.command created\\nDouble-click to re-copy command to clipboard" with title "TaskPing Emergency Mode" subtitle "Command: ${command.slice(0, 20)}..." sound name "Sosumi"
|
||||
`;
|
||||
|
||||
exec(`osascript -e '${script}'`, () => {
|
||||
resolve({ success: true, message: '紧急模式:桌面快捷文件已创建' });
|
||||
resolve({ success: true, message: 'Emergency mode: Desktop shortcut file created' });
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -207,7 +207,7 @@ echo "✅ 命令已刷新到剪贴板"
|
|||
});
|
||||
}
|
||||
|
||||
// 辅助方法:复制到剪贴板
|
||||
// Helper method: Copy to clipboard
|
||||
async copyToClipboard(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pbcopy = spawn('pbcopy');
|
||||
|
|
@ -224,7 +224,7 @@ echo "✅ 命令已刷新到剪贴板"
|
|||
});
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
// Clean up temporary files
|
||||
cleanup() {
|
||||
try {
|
||||
if (fs.existsSync(this.tempDir)) {
|
||||
|
|
@ -236,14 +236,14 @@ echo "✅ 命令已刷新到剪贴板"
|
|||
const stats = fs.statSync(filePath);
|
||||
const age = now - stats.mtime.getTime();
|
||||
|
||||
// 删除超过1小时的临时文件
|
||||
// Delete temporary files older than 1 hour
|
||||
if (age > 60 * 60 * 1000) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.warn(`清理临时文件失败: ${error.message}`);
|
||||
this.log.warn(`Failed to clean up temporary files: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* tmux命令注入器 - 无人值守远程控制解决方案
|
||||
* Tmux Command Injector - Unattended remote control solution
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
|
|
@ -23,7 +23,7 @@ class TmuxInjector {
|
|||
}
|
||||
}
|
||||
|
||||
// 检查tmux是否安装
|
||||
// Check if tmux is installed
|
||||
async checkTmuxAvailable() {
|
||||
return new Promise((resolve) => {
|
||||
exec('which tmux', (error) => {
|
||||
|
|
@ -32,7 +32,7 @@ class TmuxInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 检查Claude tmux会话是否存在
|
||||
// Check if Claude tmux session exists
|
||||
async checkClaudeSession() {
|
||||
return new Promise((resolve) => {
|
||||
exec(`tmux has-session -t ${this.sessionName} 2>/dev/null`, (error) => {
|
||||
|
|
@ -41,10 +41,10 @@ class TmuxInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 创建Claude tmux会话
|
||||
// Create Claude tmux session
|
||||
async createClaudeSession() {
|
||||
return new Promise((resolve) => {
|
||||
// 使用clauderun命令启动Claude(不预填充任何命令)
|
||||
// Use clauderun command to start Claude (without pre-filling any commands)
|
||||
const command = `tmux new-session -d -s ${this.sessionName} -c "${process.cwd()}" clauderun`;
|
||||
|
||||
this.log.info(`Creating tmux session with clauderun command: ${command}`);
|
||||
|
|
@ -52,7 +52,7 @@ class TmuxInjector {
|
|||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
this.log.warn(`Failed to create tmux session with clauderun: ${error.message}`);
|
||||
// 如果clauderun失败,尝试使用完整路径命令
|
||||
// If clauderun fails, try using full path command
|
||||
this.log.info('Fallback to full path command...');
|
||||
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${process.cwd()}" /Users/jessytsui/.nvm/versions/node/v18.17.0/bin/claude --dangerously-skip-permissions`;
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ class TmuxInjector {
|
|||
});
|
||||
} else {
|
||||
this.log.info('Tmux Claude session created successfully (clauderun)');
|
||||
// 等待Claude初始化
|
||||
// Wait for Claude initialization
|
||||
setTimeout(() => {
|
||||
resolve({ success: true });
|
||||
}, 3000);
|
||||
|
|
@ -78,18 +78,18 @@ class TmuxInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 向tmux会话注入命令(智能处理Claude确认)
|
||||
// Inject command into tmux session (intelligently handle Claude confirmations)
|
||||
async injectCommand(command) {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
// 1. 清空输入框
|
||||
// 1. Clear input field
|
||||
const clearCommand = `tmux send-keys -t ${this.sessionName} C-u`;
|
||||
|
||||
// 2. 发送命令
|
||||
// 2. Send command
|
||||
const escapedCommand = command.replace(/'/g, "'\"'\"'");
|
||||
const sendCommand = `tmux send-keys -t ${this.sessionName} '${escapedCommand}'`;
|
||||
|
||||
// 3. 发送回车
|
||||
// 3. Send enter
|
||||
const enterCommand = `tmux send-keys -t ${this.sessionName} C-m`;
|
||||
|
||||
this.log.info(`Injecting command via tmux: ${command}`);
|
||||
|
|
@ -97,7 +97,7 @@ class TmuxInjector {
|
|||
this.log.info(`Step 2 - Send: ${sendCommand}`);
|
||||
this.log.info(`Step 3 - Enter: ${enterCommand}`);
|
||||
|
||||
// 执行三个步骤
|
||||
// Execute three steps
|
||||
exec(clearCommand, (clearError) => {
|
||||
if (clearError) {
|
||||
this.log.error(`Failed to clear input: ${clearError.message}`);
|
||||
|
|
@ -105,7 +105,7 @@ class TmuxInjector {
|
|||
return;
|
||||
}
|
||||
|
||||
// 短暂等待
|
||||
// Brief wait
|
||||
setTimeout(() => {
|
||||
exec(sendCommand, (sendError) => {
|
||||
if (sendError) {
|
||||
|
|
@ -114,7 +114,7 @@ class TmuxInjector {
|
|||
return;
|
||||
}
|
||||
|
||||
// 短暂等待
|
||||
// Brief wait
|
||||
setTimeout(() => {
|
||||
exec(enterCommand, async (enterError) => {
|
||||
if (enterError) {
|
||||
|
|
@ -125,19 +125,19 @@ class TmuxInjector {
|
|||
|
||||
this.log.info('Command sent successfully in 3 steps');
|
||||
|
||||
// 短暂等待命令发送
|
||||
// Brief wait for command sending
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// 检查命令是否已在Claude中显示
|
||||
// Check if command is already displayed in Claude
|
||||
const capture = await this.getCaptureOutput();
|
||||
if (capture.success) {
|
||||
this.log.info(`Claude state after injection: ${capture.output.slice(-200).replace(/\n/g, ' ')}`);
|
||||
}
|
||||
|
||||
// 等待并检查是否需要确认
|
||||
// Wait and check if confirmation is needed
|
||||
await this.handleConfirmations();
|
||||
|
||||
// 记录注入日志
|
||||
// Record injection log
|
||||
this.logInjection(command);
|
||||
|
||||
resolve({ success: true });
|
||||
|
|
@ -153,7 +153,7 @@ class TmuxInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 自动处理Claude的确认对话框
|
||||
// Automatically handle Claude confirmation dialogs
|
||||
async handleConfirmations() {
|
||||
const maxAttempts = 8;
|
||||
let attempts = 0;
|
||||
|
|
@ -161,10 +161,10 @@ class TmuxInjector {
|
|||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// 等待Claude处理
|
||||
// Wait for Claude processing
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// 获取当前屏幕内容
|
||||
// Get current screen content
|
||||
const capture = await this.getCaptureOutput();
|
||||
|
||||
if (!capture.success) {
|
||||
|
|
@ -174,20 +174,20 @@ class TmuxInjector {
|
|||
const output = capture.output;
|
||||
this.log.info(`Confirmation check ${attempts}: ${output.slice(-200).replace(/\n/g, ' ')}`);
|
||||
|
||||
// 检查是否有多选项确认对话框(优先处理)
|
||||
// Check for multi-option confirmation dialog (priority handling)
|
||||
if (output.includes('Do you want to proceed?') &&
|
||||
(output.includes('1. Yes') || output.includes('2. Yes, and don\'t ask again'))) {
|
||||
|
||||
this.log.info(`Detected multi-option confirmation, selecting option 2 (attempt ${attempts})`);
|
||||
|
||||
// 选择"2. Yes, and don't ask again"以避免未来的确认对话框
|
||||
// Select "2. Yes, and don't ask again" to avoid future confirmation dialogs
|
||||
await new Promise((resolve) => {
|
||||
exec(`tmux send-keys -t ${this.sessionName} '2'`, (error) => {
|
||||
if (error) {
|
||||
this.log.warn('Failed to send option 2');
|
||||
} else {
|
||||
this.log.info('Auto-confirmation sent (option 2)');
|
||||
// 发送Enter键
|
||||
// Send Enter key
|
||||
setTimeout(() => {
|
||||
exec(`tmux send-keys -t ${this.sessionName} 'Enter'`, (enterError) => {
|
||||
if (enterError) {
|
||||
|
|
@ -202,12 +202,12 @@ class TmuxInjector {
|
|||
});
|
||||
});
|
||||
|
||||
// 等待确认生效
|
||||
// Wait for confirmation to take effect
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有单选项确认
|
||||
// Check for single option confirmation
|
||||
if (output.includes('❯ 1. Yes') || output.includes('▷ 1. Yes')) {
|
||||
this.log.info(`Detected single option confirmation, selecting option 1 (attempt ${attempts})`);
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ class TmuxInjector {
|
|||
this.log.warn('Failed to send option 1');
|
||||
} else {
|
||||
this.log.info('Auto-confirmation sent (option 1)');
|
||||
// 发送Enter键
|
||||
// Send Enter key
|
||||
setTimeout(() => {
|
||||
exec(`tmux send-keys -t ${this.sessionName} 'Enter'`, (enterError) => {
|
||||
if (enterError) {
|
||||
|
|
@ -235,7 +235,7 @@ class TmuxInjector {
|
|||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有简单的Y/N确认
|
||||
// Check for simple Y/N confirmation
|
||||
if (output.includes('(y/n)') || output.includes('[Y/n]') || output.includes('[y/N]')) {
|
||||
this.log.info(`Detected y/n prompt, sending 'y' (attempt ${attempts})`);
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ class TmuxInjector {
|
|||
this.log.warn('Failed to send y');
|
||||
} else {
|
||||
this.log.info('Auto-confirmation sent (y)');
|
||||
// 发送Enter键
|
||||
// Send Enter key
|
||||
setTimeout(() => {
|
||||
exec(`tmux send-keys -t ${this.sessionName} 'Enter'`, (enterError) => {
|
||||
if (enterError) {
|
||||
|
|
@ -263,7 +263,7 @@ class TmuxInjector {
|
|||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有按Enter继续的提示
|
||||
// Check for press Enter to continue prompts
|
||||
if (output.includes('Press Enter to continue') ||
|
||||
output.includes('Enter to confirm') ||
|
||||
output.includes('Press Enter')) {
|
||||
|
|
@ -283,7 +283,7 @@ class TmuxInjector {
|
|||
continue;
|
||||
}
|
||||
|
||||
// 检查是否命令正在执行
|
||||
// Check if command is currently executing
|
||||
if (output.includes('Clauding…') ||
|
||||
output.includes('Waiting…') ||
|
||||
output.includes('Processing…') ||
|
||||
|
|
@ -292,7 +292,7 @@ class TmuxInjector {
|
|||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有新的空输入框(表示完成)
|
||||
// Check for new empty input box (indicates completion)
|
||||
if ((output.includes('│ >') || output.includes('> ')) &&
|
||||
!output.includes('Do you want to proceed?') &&
|
||||
!output.includes('1. Yes') &&
|
||||
|
|
@ -301,13 +301,13 @@ class TmuxInjector {
|
|||
break;
|
||||
}
|
||||
|
||||
// 检查是否有错误信息
|
||||
// Check for error messages
|
||||
if (output.includes('Error:') || output.includes('error:') || output.includes('failed')) {
|
||||
this.log.warn('Detected error in output, stopping confirmation attempts');
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果什么都没检测到,等待更长时间再检查
|
||||
// If nothing detected, wait longer before checking again
|
||||
if (attempts < maxAttempts) {
|
||||
this.log.info('No confirmation prompts detected, waiting longer...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
|
@ -316,14 +316,14 @@ class TmuxInjector {
|
|||
|
||||
this.log.info(`Confirmation handling completed after ${attempts} attempts`);
|
||||
|
||||
// 最终状态检查
|
||||
// Final state check
|
||||
const finalCapture = await this.getCaptureOutput();
|
||||
if (finalCapture.success) {
|
||||
this.log.info(`Final state: ${finalCapture.output.slice(-100).replace(/\n/g, ' ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取tmux会话输出
|
||||
// Get tmux session output
|
||||
async getCaptureOutput() {
|
||||
return new Promise((resolve) => {
|
||||
const command = `tmux capture-pane -t ${this.sessionName} -p`;
|
||||
|
|
@ -338,35 +338,35 @@ class TmuxInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 重启Claude会话
|
||||
// Restart Claude session
|
||||
async restartClaudeSession() {
|
||||
return new Promise(async (resolve) => {
|
||||
this.log.info('Restarting Claude tmux session...');
|
||||
|
||||
// 杀死现有会话
|
||||
// Kill existing session
|
||||
exec(`tmux kill-session -t ${this.sessionName} 2>/dev/null`, async () => {
|
||||
// 等待一下
|
||||
// Wait a moment
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// 创建新会话
|
||||
// Create new session
|
||||
const result = await this.createClaudeSession();
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 完整的命令注入流程
|
||||
// Complete command injection workflow
|
||||
async injectCommandFull(token, command) {
|
||||
try {
|
||||
this.log.info(`🎯 开始tmux命令注入 (Token: ${token})`);
|
||||
this.log.info(`🎯 Starting tmux command injection (Token: ${token})`);
|
||||
|
||||
// 1. 检查tmux是否可用
|
||||
// 1. Check if tmux is available
|
||||
const tmuxAvailable = await this.checkTmuxAvailable();
|
||||
if (!tmuxAvailable) {
|
||||
return { success: false, error: 'tmux_not_installed', message: '需要安装tmux: brew install tmux' };
|
||||
return { success: false, error: 'tmux_not_installed', message: 'Need to install tmux: brew install tmux' };
|
||||
}
|
||||
|
||||
// 2. 检查Claude会话是否存在
|
||||
// 2. Check if Claude session exists
|
||||
const sessionExists = await this.checkClaudeSession();
|
||||
|
||||
if (!sessionExists) {
|
||||
|
|
@ -378,16 +378,16 @@ class TmuxInjector {
|
|||
}
|
||||
}
|
||||
|
||||
// 3. 注入命令
|
||||
// 3. Inject command
|
||||
const injectResult = await this.injectCommand(command);
|
||||
|
||||
if (injectResult.success) {
|
||||
// 4. 发送成功通知
|
||||
// 4. Send success notification
|
||||
await this.sendSuccessNotification(command);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '命令已成功注入到Claude tmux会话',
|
||||
message: 'Command successfully injected into Claude tmux session',
|
||||
session: this.sessionName
|
||||
};
|
||||
} else {
|
||||
|
|
@ -404,11 +404,11 @@ class TmuxInjector {
|
|||
}
|
||||
}
|
||||
|
||||
// 发送成功通知
|
||||
// Send success notification
|
||||
async sendSuccessNotification(command) {
|
||||
const shortCommand = command.length > 30 ? command.substring(0, 30) + '...' : command;
|
||||
const notificationScript = `
|
||||
display notification "🎉 命令已自动注入到Claude!无需手动操作" with title "TaskPing 远程控制成功" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Glass"
|
||||
display notification "🎉 Command automatically injected into Claude! No manual operation needed" with title "TaskPing Remote Control Success" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Glass"
|
||||
`;
|
||||
|
||||
exec(`osascript -e '${notificationScript}'`, (error) => {
|
||||
|
|
@ -420,7 +420,7 @@ class TmuxInjector {
|
|||
});
|
||||
}
|
||||
|
||||
// 记录注入日志
|
||||
// Record injection log
|
||||
logInjection(command) {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -438,7 +438,7 @@ class TmuxInjector {
|
|||
}
|
||||
}
|
||||
|
||||
// 获取会话状态信息
|
||||
// Get session status information
|
||||
async getSessionInfo() {
|
||||
return new Promise((resolve) => {
|
||||
const command = `tmux list-sessions | grep ${this.sessionName}`;
|
||||
|
|
|
|||
|
|
@ -46,29 +46,29 @@ class ConfigurationManager {
|
|||
}
|
||||
|
||||
displayCurrentConfig() {
|
||||
console.log('\n当前配置:');
|
||||
console.log('├─ 语言:', this.config.get('language'));
|
||||
console.log('├─ 启用状态:', this.config.get('enabled') ? '启用' : '禁用');
|
||||
console.log('├─ 超时时间:', this.config.get('timeout') + '秒');
|
||||
console.log('├─ 完成提示音:', this.config.get('sound.completed'));
|
||||
console.log('└─ 等待提示音:', this.config.get('sound.waiting'));
|
||||
console.log('\nCurrent configuration:');
|
||||
console.log('├─ Language:', this.config.get('language'));
|
||||
console.log('├─ Enabled status:', this.config.get('enabled') ? 'Enabled' : 'Disabled');
|
||||
console.log('├─ Timeout:', this.config.get('timeout') + ' seconds');
|
||||
console.log('├─ Completion sound:', this.config.get('sound.completed'));
|
||||
console.log('└─ Waiting sound:', this.config.get('sound.waiting'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
async showMainMenu() {
|
||||
while (true) {
|
||||
console.log('\n=== TaskPing 配置管理器 ===');
|
||||
console.log('\n=== TaskPing Configuration Manager ===');
|
||||
this.displayCurrentConfig();
|
||||
console.log('选项:');
|
||||
console.log('1. 基础设置');
|
||||
console.log('2. 音效配置');
|
||||
console.log('3. 通知渠道');
|
||||
console.log('4. 命令中继');
|
||||
console.log('5. 测试通知');
|
||||
console.log('6. 保存并退出');
|
||||
console.log('7. 退出(不保存)');
|
||||
console.log('Options:');
|
||||
console.log('1. Basic Settings');
|
||||
console.log('2. Sound Configuration');
|
||||
console.log('3. Notification Channels');
|
||||
console.log('4. Command Relay');
|
||||
console.log('5. Test Notifications');
|
||||
console.log('6. Save and Exit');
|
||||
console.log('7. Exit (without saving)');
|
||||
|
||||
const choice = await this.question('\n请选择 (1-7): ');
|
||||
const choice = await this.question('\nPlease select (1-7): ');
|
||||
|
||||
switch (choice) {
|
||||
case '1':
|
||||
|
|
@ -88,32 +88,32 @@ class ConfigurationManager {
|
|||
break;
|
||||
case '6':
|
||||
if (this.config.save()) {
|
||||
console.log('✅ 配置已保存');
|
||||
console.log('✅ Configuration saved');
|
||||
this.rl.close();
|
||||
return;
|
||||
} else {
|
||||
console.log('❌ 保存失败');
|
||||
console.log('❌ Save failed');
|
||||
}
|
||||
break;
|
||||
case '7':
|
||||
console.log('退出(未保存更改)');
|
||||
console.log('Exit (changes not saved)');
|
||||
this.rl.close();
|
||||
return;
|
||||
default:
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async configureBasicSettings() {
|
||||
console.log('\n=== 基础设置 ===');
|
||||
console.log('1. 配置语言');
|
||||
console.log('2. 切换启用状态');
|
||||
console.log('3. 配置超时时间');
|
||||
console.log('4. 自定义消息');
|
||||
console.log('0. 返回主菜单');
|
||||
console.log('\n=== Basic Settings ===');
|
||||
console.log('1. Configure Language');
|
||||
console.log('2. Toggle Enabled Status');
|
||||
console.log('3. Configure Timeout');
|
||||
console.log('4. Custom Messages');
|
||||
console.log('0. Return to Main Menu');
|
||||
|
||||
const choice = await this.question('\n请选择 (0-4): ');
|
||||
const choice = await this.question('\n Please select (0-4): ');
|
||||
|
||||
switch (choice) {
|
||||
case '1':
|
||||
|
|
@ -131,42 +131,42 @@ class ConfigurationManager {
|
|||
case '0':
|
||||
return;
|
||||
default:
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
}
|
||||
}
|
||||
|
||||
async configureLanguage() {
|
||||
const languages = ['zh-CN', 'en', 'ja'];
|
||||
console.log('\n可用语言:');
|
||||
console.log('\nAvailable languages:');
|
||||
languages.forEach((lang, index) => {
|
||||
console.log(`${index + 1}. ${lang}`);
|
||||
});
|
||||
|
||||
const choice = await this.question(`选择语言 (1-${languages.length}): `);
|
||||
const choice = await this.question(`Select language (1-${languages.length}): `);
|
||||
const index = parseInt(choice) - 1;
|
||||
|
||||
if (index >= 0 && index < languages.length) {
|
||||
this.config.set('language', languages[index]);
|
||||
console.log(`✅ 语言已设置为: ${languages[index]}`);
|
||||
console.log(`✅ Language set to: ${languages[index]}`);
|
||||
} else {
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
}
|
||||
}
|
||||
|
||||
async toggleEnabled() {
|
||||
const current = this.config.get('enabled', true);
|
||||
this.config.set('enabled', !current);
|
||||
console.log(`✅ 通知已${!current ? '启用' : '禁用'}`);
|
||||
console.log(`✅ Notifications ${!current ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
async configureTimeout() {
|
||||
const timeout = await this.question('设置超时时间(秒): ');
|
||||
const timeout = await this.question('Set timeout (seconds): ');
|
||||
const timeoutNum = parseInt(timeout);
|
||||
if (timeoutNum > 0 && timeoutNum <= 30) {
|
||||
this.config.set('timeout', timeoutNum);
|
||||
console.log(`✅ 超时时间已设置为: ${timeoutNum}秒`);
|
||||
console.log(`✅ Timeout set to: ${timeoutNum} seconds`);
|
||||
} else {
|
||||
console.log('❌ 无效的超时时间 (1-30秒)');
|
||||
console.log('❌ Invalid timeout (1-30 seconds)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,36 +176,36 @@ class ConfigurationManager {
|
|||
const desktop = new DesktopChannel();
|
||||
const soundCategories = desktop.getAvailableSounds();
|
||||
|
||||
console.log('\n=== 音效配置 ===');
|
||||
console.log('\n=== Sound Configuration ===');
|
||||
|
||||
// Configure completed sound
|
||||
console.log('\n--- 配置任务完成提示音 ---');
|
||||
const completedSound = await this.selectSoundFromCategories(soundCategories, '任务完成');
|
||||
console.log('\n--- Configure Task Completion Sound ---');
|
||||
const completedSound = await this.selectSoundFromCategories(soundCategories, 'task completion');
|
||||
if (completedSound) {
|
||||
this.config.set('sound.completed', completedSound);
|
||||
console.log(`✅ 任务完成提示音已设置为: ${completedSound}`);
|
||||
console.log(`✅ Task completion sound set to: ${completedSound}`);
|
||||
}
|
||||
|
||||
// Configure waiting sound
|
||||
console.log('\n--- 配置等待输入提示音 ---');
|
||||
const waitingSound = await this.selectSoundFromCategories(soundCategories, '等待输入');
|
||||
console.log('\n--- Configure Waiting Input Sound ---');
|
||||
const waitingSound = await this.selectSoundFromCategories(soundCategories, 'waiting input');
|
||||
if (waitingSound) {
|
||||
this.config.set('sound.waiting', waitingSound);
|
||||
console.log(`✅ 等待输入提示音已设置为: ${waitingSound}`);
|
||||
console.log(`✅ Waiting input sound set to: ${waitingSound}`);
|
||||
}
|
||||
}
|
||||
|
||||
async selectSoundFromCategories(soundCategories, type) {
|
||||
const categories = Object.keys(soundCategories);
|
||||
|
||||
console.log(`\n选择${type}音效分类:`);
|
||||
console.log(`\nSelect ${type} sound category:`);
|
||||
categories.forEach((category, index) => {
|
||||
const count = soundCategories[category].length;
|
||||
console.log(`${index + 1}. ${category} (${count}个音效)`);
|
||||
console.log(`${index + 1}. ${category} (${count} sounds)`);
|
||||
});
|
||||
console.log('0. 跳过');
|
||||
console.log('0. Skip');
|
||||
|
||||
const choice = await this.question(`\n请选择分类 (0-${categories.length}): `);
|
||||
const choice = await this.question(`\nPlease select category (0-${categories.length}): `);
|
||||
const index = parseInt(choice) - 1;
|
||||
|
||||
if (choice === '0') {
|
||||
|
|
@ -217,19 +217,19 @@ class ConfigurationManager {
|
|||
const sounds = soundCategories[category];
|
||||
return await this.selectSoundFromList(sounds, type);
|
||||
} else {
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async selectSoundFromList(sounds, type) {
|
||||
console.log(`\n选择${type}提示音:`);
|
||||
console.log(`\nSelect ${type} sound:`);
|
||||
sounds.forEach((sound, index) => {
|
||||
console.log(`${index + 1}. ${sound}`);
|
||||
});
|
||||
console.log('0. 返回分类选择');
|
||||
console.log('0. Return to category selection');
|
||||
|
||||
const choice = await this.question(`\n请选择 (0-${sounds.length}): `);
|
||||
const choice = await this.question(`\nPlease select (0-${sounds.length}): `);
|
||||
const index = parseInt(choice) - 1;
|
||||
|
||||
if (choice === '0') {
|
||||
|
|
@ -244,37 +244,37 @@ class ConfigurationManager {
|
|||
const DesktopChannel = require('../channels/local/desktop');
|
||||
const desktop = new DesktopChannel();
|
||||
desktop._playSound(selectedSound);
|
||||
console.log(`播放音效: ${selectedSound}`);
|
||||
console.log(`Playing sound: ${selectedSound}`);
|
||||
} catch (error) {
|
||||
// Ignore playback errors
|
||||
}
|
||||
|
||||
const confirm = await this.question('确认使用这个音效吗? (y/n): ');
|
||||
const confirm = await this.question('Confirm using this sound? (y/n): ');
|
||||
if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') {
|
||||
return selectedSound;
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async configureChannels() {
|
||||
console.log('\n=== 通知渠道配置 ===');
|
||||
console.log('1. 桌面通知 (已启用)');
|
||||
console.log('2. 邮件通知');
|
||||
console.log('3. Discord通知 (即将支持)');
|
||||
console.log('4. Telegram通知 (即将支持)');
|
||||
console.log('5. WhatsApp通知 (即将支持)');
|
||||
console.log('6. 飞书通知 (即将支持)');
|
||||
console.log('0. 返回主菜单');
|
||||
console.log('\n=== Notification Channel Configuration ===');
|
||||
console.log('1. Desktop Notifications (Enabled)');
|
||||
console.log('2. Email Notifications');
|
||||
console.log('3. Discord Notifications (Coming Soon)');
|
||||
console.log('4. Telegram Notifications (Coming Soon)');
|
||||
console.log('5. WhatsApp Notifications (Coming Soon)');
|
||||
console.log('6. Feishu Notifications (Coming Soon)');
|
||||
console.log('0. Return to Main Menu');
|
||||
|
||||
const choice = await this.question('\n请选择要配置的渠道 (0-6): ');
|
||||
const choice = await this.question('\nPlease select channel to configure (0-6): ');
|
||||
|
||||
switch (choice) {
|
||||
case '1':
|
||||
console.log('\n桌面通知已启用且工作正常!');
|
||||
console.log('\nDesktop notifications are enabled and working properly!');
|
||||
break;
|
||||
case '2':
|
||||
await this.configureEmailChannel();
|
||||
|
|
@ -283,142 +283,142 @@ class ConfigurationManager {
|
|||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
console.log('\n此渠道即将在后续版本中支持!');
|
||||
console.log('\nThis channel will be supported in future versions!');
|
||||
break;
|
||||
case '0':
|
||||
return;
|
||||
default:
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
}
|
||||
|
||||
if (choice !== '0') {
|
||||
await this.question('\n按回车继续...');
|
||||
await this.question('\nPress Enter to continue...');
|
||||
}
|
||||
}
|
||||
|
||||
async configureRelay() {
|
||||
console.log('\n=== 命令中继配置 ===');
|
||||
console.log('(此功能将在后续版本中实现)');
|
||||
console.log('将支持通过通知渠道发送命令,自动在Claude Code中执行');
|
||||
console.log('\n=== Command Relay Configuration ===');
|
||||
console.log('(This feature will be implemented in future versions)');
|
||||
console.log('Will support sending commands via notification channels and auto-executing in Claude Code');
|
||||
|
||||
await this.question('\n按回车继续...');
|
||||
await this.question('\nPress Enter to continue...');
|
||||
}
|
||||
|
||||
async configureCustomMessages() {
|
||||
console.log('\n=== 自定义消息配置 ===');
|
||||
console.log('提示:使用 {project} 作为项目名占位符');
|
||||
console.log('示例:[{project}] 任务已完成!\n');
|
||||
console.log('\n=== Custom Message Configuration ===');
|
||||
console.log('Tip: Use {project} as project name placeholder');
|
||||
console.log('Example: [{project}] Task completed!\n');
|
||||
|
||||
// Configure completed message
|
||||
const currentCompleted = this.config.get('customMessages.completed') || '使用默认文本';
|
||||
console.log(`当前任务完成文本: ${currentCompleted}`);
|
||||
const completedMsg = await this.question('新的任务完成文本 (回车跳过): ');
|
||||
const currentCompleted = this.config.get('customMessages.completed') || 'Use default text';
|
||||
console.log(`Current task completion text: ${currentCompleted}`);
|
||||
const completedMsg = await this.question('New task completion text (Enter to skip): ');
|
||||
if (completedMsg.trim()) {
|
||||
this.config.set('customMessages.completed', completedMsg.trim());
|
||||
console.log('✅ 已更新任务完成文本');
|
||||
console.log('✅ Updated task completion text');
|
||||
}
|
||||
|
||||
// Configure waiting message
|
||||
const currentWaiting = this.config.get('customMessages.waiting') || '使用默认文本';
|
||||
console.log(`\n当前等待输入文本: ${currentWaiting}`);
|
||||
const waitingMsg = await this.question('新的等待输入文本 (回车跳过): ');
|
||||
const currentWaiting = this.config.get('customMessages.waiting') || 'Use default text';
|
||||
console.log(`\nCurrent waiting input text: ${currentWaiting}`);
|
||||
const waitingMsg = await this.question('New waiting input text (Enter to skip): ');
|
||||
if (waitingMsg.trim()) {
|
||||
this.config.set('customMessages.waiting', waitingMsg.trim());
|
||||
console.log('✅ 已更新等待输入文本');
|
||||
console.log('✅ Updated waiting input text');
|
||||
}
|
||||
}
|
||||
|
||||
async testNotifications() {
|
||||
console.log('\n=== 测试通知 ===');
|
||||
console.log('\n=== Test Notifications ===');
|
||||
|
||||
try {
|
||||
const Notifier = require('../core/notifier');
|
||||
const notifier = new Notifier(this.config);
|
||||
await notifier.initializeChannels();
|
||||
|
||||
console.log('发送任务完成通知...');
|
||||
console.log('Sending task completion notification...');
|
||||
await notifier.notify('completed', { test: true });
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
console.log('发送等待输入通知...');
|
||||
console.log('Sending waiting input notification...');
|
||||
await notifier.notify('waiting', { test: true });
|
||||
|
||||
console.log('✅ 测试完成');
|
||||
console.log('✅ Test completed');
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
console.error('❌ Test failed:', error.message);
|
||||
}
|
||||
|
||||
await this.question('\n按回车继续...');
|
||||
await this.question('\nPress Enter to continue...');
|
||||
}
|
||||
|
||||
async configureEmailChannel() {
|
||||
console.log('\n=== 邮件通知配置 ===');
|
||||
console.log('\n=== Email Notification Configuration ===');
|
||||
|
||||
// 获取当前邮件配置
|
||||
// Get current email configuration
|
||||
const currentEmailConfig = this.config.getChannel('email') || { enabled: false, config: {} };
|
||||
const emailConfig = currentEmailConfig.config || {};
|
||||
|
||||
console.log(`当前状态: ${currentEmailConfig.enabled ? '✅ 已启用' : '❌ 已禁用'}`);
|
||||
console.log(`Current status: ${currentEmailConfig.enabled ? '✅ Enabled' : '❌ Disabled'}`);
|
||||
|
||||
console.log('\n📧 SMTP 发送配置:');
|
||||
console.log('\n📧 SMTP Sending Configuration:');
|
||||
|
||||
// SMTP 主机配置
|
||||
// SMTP host configuration
|
||||
const currentHost = emailConfig.smtp?.host || '';
|
||||
console.log(`当前 SMTP 主机: ${currentHost || '未配置'}`);
|
||||
const smtpHost = await this.question('SMTP 主机 (如: smtp.gmail.com): ');
|
||||
console.log(`Current SMTP host: ${currentHost || 'Not configured'}`);
|
||||
const smtpHost = await this.question('SMTP host (e.g., smtp.gmail.com): ');
|
||||
|
||||
// SMTP 端口配置
|
||||
// SMTP port configuration
|
||||
const currentPort = emailConfig.smtp?.port || 587;
|
||||
console.log(`当前 SMTP 端口: ${currentPort}`);
|
||||
const smtpPortInput = await this.question('SMTP 端口 (默认 587): ');
|
||||
console.log(`Current SMTP port: ${currentPort}`);
|
||||
const smtpPortInput = await this.question('SMTP port (default 587): ');
|
||||
const smtpPort = parseInt(smtpPortInput) || 587;
|
||||
|
||||
// 安全连接配置
|
||||
// Security connection configuration
|
||||
const currentSecure = emailConfig.smtp?.secure || false;
|
||||
console.log(`当前安全连接: ${currentSecure ? 'SSL/TLS' : 'STARTTLS'}`);
|
||||
const secureInput = await this.question('使用 SSL/TLS? (y/n,默认n): ');
|
||||
console.log(`Current secure connection: ${currentSecure ? 'SSL/TLS' : 'STARTTLS'}`);
|
||||
const secureInput = await this.question('Use SSL/TLS? (y/n, default n): ');
|
||||
const secure = secureInput.toLowerCase() === 'y';
|
||||
|
||||
// 用户名配置
|
||||
// Username configuration
|
||||
const currentUser = emailConfig.smtp?.auth?.user || '';
|
||||
console.log(`当前用户名: ${currentUser || '未配置'}`);
|
||||
const smtpUser = await this.question('SMTP 用户名 (邮箱地址): ');
|
||||
console.log(`Current username: ${currentUser || 'Not configured'}`);
|
||||
const smtpUser = await this.question('SMTP username (email address): ');
|
||||
|
||||
// 密码配置
|
||||
console.log('SMTP 密码: [隐藏]');
|
||||
const smtpPass = await this.question('SMTP 密码 (应用密码): ');
|
||||
// Password configuration
|
||||
console.log('SMTP password: [Hidden]');
|
||||
const smtpPass = await this.question('SMTP password (app password): ');
|
||||
|
||||
console.log('\n📥 IMAP 接收配置 (用于接收回复):');
|
||||
console.log('\n📥 IMAP Receiving Configuration (for receiving replies):');
|
||||
|
||||
// IMAP 主机配置
|
||||
// IMAP host configuration
|
||||
const currentImapHost = emailConfig.imap?.host || '';
|
||||
console.log(`当前 IMAP 主机: ${currentImapHost || '未配置'}`);
|
||||
const imapHost = await this.question('IMAP 主机 (如: imap.gmail.com): ');
|
||||
console.log(`Current IMAP host: ${currentImapHost || 'Not configured'}`);
|
||||
const imapHost = await this.question('IMAP host (e.g., imap.gmail.com): ');
|
||||
|
||||
// IMAP 端口配置
|
||||
// IMAP port configuration
|
||||
const currentImapPort = emailConfig.imap?.port || 993;
|
||||
console.log(`当前 IMAP 端口: ${currentImapPort}`);
|
||||
const imapPortInput = await this.question('IMAP 端口 (默认 993): ');
|
||||
console.log(`Current IMAP port: ${currentImapPort}`);
|
||||
const imapPortInput = await this.question('IMAP port (default 993): ');
|
||||
const imapPort = parseInt(imapPortInput) || 993;
|
||||
|
||||
// IMAP 安全连接
|
||||
// IMAP secure connection
|
||||
const currentImapSecure = emailConfig.imap?.secure !== false;
|
||||
const imapSecureInput = await this.question('IMAP 使用 SSL? (y/n,默认y): ');
|
||||
const imapSecureInput = await this.question('IMAP use SSL? (y/n, default y): ');
|
||||
const imapSecure = imapSecureInput.toLowerCase() !== 'n';
|
||||
|
||||
// 收件人配置
|
||||
console.log('\n📬 收件人配置:');
|
||||
// Recipient configuration
|
||||
console.log('\n📬 Recipient Configuration:');
|
||||
const currentTo = emailConfig.to || '';
|
||||
console.log(`当前收件人: ${currentTo || '未配置'}`);
|
||||
const toEmail = await this.question('收件人邮箱: ');
|
||||
console.log(`Current recipient: ${currentTo || 'Not configured'}`);
|
||||
const toEmail = await this.question('Recipient email: ');
|
||||
|
||||
// 发件人配置
|
||||
// Sender configuration
|
||||
const currentFrom = emailConfig.from || '';
|
||||
console.log(`当前发件人: ${currentFrom || '未配置'}`);
|
||||
const fromEmail = await this.question(`发件人显示名 (默认: TaskPing <${smtpUser}>): `);
|
||||
console.log(`Current sender: ${currentFrom || 'Not configured'}`);
|
||||
const fromEmail = await this.question(`Sender display name (default: TaskPing <${smtpUser}>): `);
|
||||
|
||||
// 构建邮件配置
|
||||
// Build email configuration
|
||||
const newEmailConfig = {
|
||||
enabled: true,
|
||||
config: {
|
||||
|
|
@ -445,26 +445,26 @@ class ConfigurationManager {
|
|||
}
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
// Save configuration
|
||||
this.config.setChannel('email', newEmailConfig);
|
||||
console.log('\n✅ 邮件配置已保存');
|
||||
console.log('\n✅ Email configuration saved');
|
||||
|
||||
// 询问是否测试
|
||||
const testChoice = await this.question('\n测试邮件发送? (y/n): ');
|
||||
// Ask whether to test
|
||||
const testChoice = await this.question('\nTest email sending? (y/n): ');
|
||||
if (testChoice.toLowerCase() === 'y') {
|
||||
await this.testEmailChannel();
|
||||
}
|
||||
}
|
||||
|
||||
async testEmailChannel() {
|
||||
console.log('\n🧪 测试邮件发送...');
|
||||
console.log('\n🧪 Testing email sending...');
|
||||
|
||||
try {
|
||||
const EmailChannel = require('../channels/email/smtp');
|
||||
const emailConfig = this.config.getChannel('email');
|
||||
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
console.log('❌ 邮件渠道未启用');
|
||||
console.log('❌ Email channel not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -472,16 +472,16 @@ class ConfigurationManager {
|
|||
const testResult = await emailChannel.test();
|
||||
|
||||
if (testResult) {
|
||||
console.log('✅ 邮件发送测试成功!');
|
||||
console.log('📧 请检查您的邮箱,应该收到一封测试邮件');
|
||||
console.log('💡 您可以尝试回复该邮件来测试命令中继功能');
|
||||
console.log('✅ Email sending test successful!');
|
||||
console.log('📧 Please check your inbox, you should receive a test email');
|
||||
console.log('💡 You can try replying to that email to test the command relay feature');
|
||||
} else {
|
||||
console.log('❌ 邮件发送测试失败');
|
||||
console.log('请检查您的 SMTP 配置是否正确');
|
||||
console.log('❌ Email sending test failed');
|
||||
console.log('Please check if your SMTP configuration is correct');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 邮件测试失败:', error.message);
|
||||
console.log('请检查您的网络连接和邮件配置');
|
||||
console.log('❌ Email test failed:', error.message);
|
||||
console.log('Please check your network connection and email configuration');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -492,15 +492,15 @@ TaskPing Configuration Manager
|
|||
Usage: taskping config [options]
|
||||
|
||||
Options:
|
||||
--show 显示当前配置
|
||||
--help 显示帮助信息
|
||||
--show Show current configuration
|
||||
--help Show help information
|
||||
|
||||
Interactive Commands:
|
||||
1. 基础设置 - 语言、启用状态、超时时间等
|
||||
2. 音效配置 - 配置任务完成和等待输入的提示音
|
||||
3. 通知渠道 - 配置邮件、Discord、Telegram等通知渠道
|
||||
4. 命令中继 - 配置远程命令执行功能
|
||||
5. 测试通知 - 测试所有配置的通知渠道
|
||||
1. Basic Settings - Language, enabled status, timeout, etc.
|
||||
2. Sound Configuration - Configure task completion and waiting input sounds
|
||||
3. Notification Channels - Configure email, Discord, Telegram and other notification channels
|
||||
4. Command Relay - Configure remote command execution features
|
||||
5. Test Notifications - Test all configured notification channels
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,20 +50,20 @@ class Installer {
|
|||
}
|
||||
|
||||
async run(args = []) {
|
||||
console.log('=== TaskPing Claude Code 安装器 ===\n');
|
||||
console.log('=== TaskPing Claude Code Installer ===\n');
|
||||
|
||||
// Check dependencies
|
||||
if (!this.checkDependencies()) {
|
||||
console.log('\n请先安装必要的依赖');
|
||||
console.log('\nPlease install required dependencies first');
|
||||
this.rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nClaude Code 配置目录: ${this.claudeConfigDir}`);
|
||||
console.log(`\nClaude Code configuration directory: ${this.claudeConfigDir}`);
|
||||
|
||||
const proceed = await this.question('\n继续安装? (y/n): ');
|
||||
const proceed = await this.question('\nContinue with installation? (y/n): ');
|
||||
if (proceed.toLowerCase() !== 'y' && proceed.toLowerCase() !== 'yes') {
|
||||
console.log('安装已取消');
|
||||
console.log('Installation cancelled');
|
||||
this.rl.close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ class Installer {
|
|||
await this.initializeConfig();
|
||||
|
||||
// Test installation
|
||||
const testChoice = await this.question('\n测试安装? (y/n): ');
|
||||
const testChoice = await this.question('\nTest installation? (y/n): ');
|
||||
if (testChoice.toLowerCase() === 'y' || testChoice.toLowerCase() === 'yes') {
|
||||
await this.testInstallation();
|
||||
}
|
||||
|
|
@ -89,14 +89,14 @@ class Installer {
|
|||
}
|
||||
|
||||
checkDependencies() {
|
||||
console.log('检查依赖...');
|
||||
console.log('Checking dependencies...');
|
||||
|
||||
// Check Node.js
|
||||
try {
|
||||
const nodeVersion = process.version;
|
||||
console.log(`✅ Node.js ${nodeVersion}`);
|
||||
} catch (error) {
|
||||
console.log('❌ Node.js 未安装');
|
||||
console.log('❌ Node.js not installed');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -104,16 +104,16 @@ class Installer {
|
|||
const platform = process.platform;
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
console.log('✅ macOS 通知支持');
|
||||
console.log('✅ macOS notification support');
|
||||
break;
|
||||
case 'linux':
|
||||
console.log('ℹ️ Linux 系统,请确保安装 libnotify-bin');
|
||||
console.log('ℹ️ Linux system, please ensure libnotify-bin is installed');
|
||||
break;
|
||||
case 'win32':
|
||||
console.log('✅ Windows 通知支持');
|
||||
console.log('✅ Windows notification support');
|
||||
break;
|
||||
default:
|
||||
console.log(`⚠️ 平台 ${platform} 可能不完全支持`);
|
||||
console.log(`⚠️ Platform ${platform} may not be fully supported`);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -153,12 +153,12 @@ class Installer {
|
|||
}
|
||||
|
||||
async installHooks() {
|
||||
console.log('\n安装 Claude Code hooks...');
|
||||
console.log('\nInstalling Claude Code hooks...');
|
||||
|
||||
// Create config directory if it doesn't exist
|
||||
if (!fs.existsSync(this.claudeConfigDir)) {
|
||||
fs.mkdirSync(this.claudeConfigDir, { recursive: true });
|
||||
console.log(`✅ 创建配置目录: ${this.claudeConfigDir}`);
|
||||
console.log(`✅ Created configuration directory: ${this.claudeConfigDir}`);
|
||||
}
|
||||
|
||||
const settingsPath = path.join(this.claudeConfigDir, 'settings.json');
|
||||
|
|
@ -169,9 +169,9 @@ class Installer {
|
|||
try {
|
||||
const content = fs.readFileSync(settingsPath, 'utf8');
|
||||
settings = JSON.parse(content);
|
||||
console.log('✅ 读取现有 Claude Code 设置');
|
||||
console.log('✅ Loaded existing Claude Code settings');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 无法解析现有设置,将创建新的配置');
|
||||
console.log('⚠️ Unable to parse existing settings, will create new configuration');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,61 +187,61 @@ class Installer {
|
|||
// Save updated settings
|
||||
try {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
console.log(`✅ Claude Code hooks 已安装到: ${settingsPath}`);
|
||||
console.log(`✅ Claude Code hooks installed to: ${settingsPath}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ 安装失败: ${error.message}`);
|
||||
console.error(`❌ Installation failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async initializeConfig() {
|
||||
console.log('\n初始化配置...');
|
||||
console.log('\nInitializing configuration...');
|
||||
|
||||
// Load and save default configuration
|
||||
this.config.load();
|
||||
this.config.save();
|
||||
|
||||
console.log('✅ 配置文件已初始化');
|
||||
console.log('✅ Configuration file initialized');
|
||||
}
|
||||
|
||||
async testInstallation() {
|
||||
console.log('\n测试安装...');
|
||||
console.log('\nTesting installation...');
|
||||
|
||||
try {
|
||||
const TaskPingCLI = require('../../taskping');
|
||||
const cli = new TaskPingCLI();
|
||||
await cli.init();
|
||||
|
||||
console.log('测试任务完成通知...');
|
||||
console.log('Testing task completion notification...');
|
||||
await cli.handleNotify(['--type', 'completed']);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
console.log('测试等待输入通知...');
|
||||
console.log('Testing waiting input notification...');
|
||||
await cli.handleNotify(['--type', 'waiting']);
|
||||
|
||||
console.log('✅ 测试成功!');
|
||||
console.log('✅ Test successful!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ 测试失败: ${error.message}`);
|
||||
console.error(`❌ Test failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
displayUsage() {
|
||||
console.log('\n=== 安装完成 ===');
|
||||
console.log('\n=== Installation Complete ===');
|
||||
console.log('');
|
||||
console.log('现在当您使用 Claude Code 时:');
|
||||
console.log('• 任务完成时会收到通知');
|
||||
console.log('• Claude 等待输入时会收到提醒');
|
||||
console.log('Now when you use Claude Code:');
|
||||
console.log('• You will receive notifications when tasks are completed');
|
||||
console.log('• You will receive reminders when Claude is waiting for input');
|
||||
console.log('');
|
||||
console.log('常用命令:');
|
||||
console.log('Common commands:');
|
||||
console.log(` node "${path.join(this.projectDir, 'taskping.js')}" config`);
|
||||
console.log(` node "${path.join(this.projectDir, 'taskping.js')}" test`);
|
||||
console.log(` node "${path.join(this.projectDir, 'taskping.js')}" status`);
|
||||
console.log('');
|
||||
console.log('如需卸载,请手动删除 Claude Code 设置中的 hooks 配置。');
|
||||
console.log('To uninstall, manually delete the hooks configuration from Claude Code settings.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* 对话追踪器 - 用于捕获用户问题和Claude回复
|
||||
* Conversation Tracker - Used to capture user questions and Claude responses
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
|
@ -18,7 +18,7 @@ class ConversationTracker {
|
|||
}
|
||||
}
|
||||
|
||||
// 记录用户问题
|
||||
// Record user question
|
||||
recordUserMessage(sessionId, message) {
|
||||
const conversations = this.loadConversations();
|
||||
if (!conversations[sessionId]) {
|
||||
|
|
@ -37,7 +37,7 @@ class ConversationTracker {
|
|||
this.saveConversations(conversations);
|
||||
}
|
||||
|
||||
// 记录Claude回复
|
||||
// Record Claude response
|
||||
recordClaudeResponse(sessionId, response) {
|
||||
const conversations = this.loadConversations();
|
||||
if (!conversations[sessionId]) {
|
||||
|
|
@ -56,7 +56,7 @@ class ConversationTracker {
|
|||
this.saveConversations(conversations);
|
||||
}
|
||||
|
||||
// 获取最近的对话内容
|
||||
// Get recent conversation content
|
||||
getRecentConversation(sessionId, limit = 2) {
|
||||
const conversations = this.loadConversations();
|
||||
const session = conversations[sessionId];
|
||||
|
|
@ -65,11 +65,11 @@ class ConversationTracker {
|
|||
return { userQuestion: '', claudeResponse: '' };
|
||||
}
|
||||
|
||||
const messages = session.messages.slice(-limit * 2); // 获取最近的用户-Claude对话
|
||||
const messages = session.messages.slice(-limit * 2); // Get recent user-Claude conversation
|
||||
let userQuestion = '';
|
||||
let claudeResponse = '';
|
||||
|
||||
// 从后往前找最近的用户问题和Claude回复
|
||||
// Find most recent user question and Claude response from back to front
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.type === 'claude' && !claudeResponse) {
|
||||
|
|
@ -82,12 +82,12 @@ class ConversationTracker {
|
|||
}
|
||||
|
||||
return {
|
||||
userQuestion: userQuestion || '未记录的用户问题',
|
||||
claudeResponse: claudeResponse || '未记录的Claude回复'
|
||||
userQuestion: userQuestion || 'Unrecorded user question',
|
||||
claudeResponse: claudeResponse || 'Unrecorded Claude response'
|
||||
};
|
||||
}
|
||||
|
||||
// 清理过期对话(超过7天)
|
||||
// Clean up expired conversations (older than 7 days)
|
||||
cleanupOldConversations() {
|
||||
const conversations = this.loadConversations();
|
||||
const now = new Date();
|
||||
|
|
|
|||
|
|
@ -174,8 +174,8 @@ class TmuxMonitor {
|
|||
}
|
||||
|
||||
return {
|
||||
userQuestion: userQuestion || '无用户输入',
|
||||
claudeResponse: claudeResponse || '无Claude回复'
|
||||
userQuestion: userQuestion || 'No user input',
|
||||
claudeResponse: claudeResponse || 'No Claude response'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,123 +1,123 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing PTY Relay 启动脚本
|
||||
* 启动基于 node-pty 的邮件命令中继服务
|
||||
* TaskPing PTY Relay Startup Script
|
||||
* Start node-pty based email command relay service
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 检查环境配置
|
||||
// Check environment configuration
|
||||
function checkConfig() {
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.error('❌ 错误: 未找到 .env 配置文件');
|
||||
console.log('\n请先复制 .env.example 到 .env 并配置您的邮件信息:');
|
||||
console.error('❌ Error: .env configuration file not found');
|
||||
console.log('\nPlease first copy .env.example to .env and configure your email information:');
|
||||
console.log(' cp .env.example .env');
|
||||
console.log(' 然后编辑 .env 文件填入您的邮件配置\n');
|
||||
console.log(' Then edit .env file to fill in your email configuration\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 加载环境变量
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
// 检查必需的配置
|
||||
// Check required configuration
|
||||
const required = ['IMAP_HOST', 'IMAP_USER', 'IMAP_PASS'];
|
||||
const missing = required.filter(key => !process.env[key]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error('❌ 错误: 缺少必需的环境变量:');
|
||||
console.error('❌ Error: Missing required environment variables:');
|
||||
missing.forEach(key => console.log(` - ${key}`));
|
||||
console.log('\n请编辑 .env 文件并填入所有必需的配置\n');
|
||||
console.log('\nPlease edit .env file and fill in all required configurations\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ 配置检查通过');
|
||||
console.log(`📧 IMAP服务器: ${process.env.IMAP_HOST}`);
|
||||
console.log(`👤 邮件账号: ${process.env.IMAP_USER}`);
|
||||
console.log(`🔒 白名单发件人: ${process.env.ALLOWED_SENDERS || '(未设置,将接受所有邮件)'}`);
|
||||
console.log(`💾 会话存储路径: ${process.env.SESSION_MAP_PATH || '(使用默认路径)'}`);
|
||||
console.log('✅ Configuration check passed');
|
||||
console.log(`📧 IMAP server: ${process.env.IMAP_HOST}`);
|
||||
console.log(`👤 Email account: ${process.env.IMAP_USER}`);
|
||||
console.log(`🔒 Whitelist senders: ${process.env.ALLOWED_SENDERS || '(Not set, will accept all emails)'}`);
|
||||
console.log(`💾 Session storage path: ${process.env.SESSION_MAP_PATH || '(Using default path)'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 创建会话示例
|
||||
// Create example session
|
||||
function createExampleSession() {
|
||||
const sessionMapPath = process.env.SESSION_MAP_PATH || path.join(__dirname, 'src/data/session-map.json');
|
||||
const sessionDir = path.dirname(sessionMapPath);
|
||||
|
||||
// 确保目录存在
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
fs.mkdirSync(sessionDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 如果会话文件不存在,创建一个示例
|
||||
// If session file doesn't exist, create an example
|
||||
if (!fs.existsSync(sessionMapPath)) {
|
||||
const exampleToken = 'TEST123';
|
||||
const exampleSession = {
|
||||
[exampleToken]: {
|
||||
type: 'pty',
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000), // 24小时后过期
|
||||
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000), // Expires after 24 hours
|
||||
cwd: process.cwd(),
|
||||
description: '测试会话 - 发送邮件时主题包含 [TaskPing #TEST123]'
|
||||
description: 'Test session - Include [TaskPing #TEST123] in email subject when sending'
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(sessionMapPath, JSON.stringify(exampleSession, null, 2));
|
||||
console.log(`📝 已创建示例会话文件: ${sessionMapPath}`);
|
||||
console.log(`🔑 测试Token: ${exampleToken}`);
|
||||
console.log(' 发送测试邮件时,主题中包含: [TaskPing #TEST123]');
|
||||
console.log(`📝 Created example session file: ${sessionMapPath}`);
|
||||
console.log(`🔑 Test Token: ${exampleToken}`);
|
||||
console.log(' When sending test email, include in subject: [TaskPing #TEST123]');
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
// PID文件路径
|
||||
// PID file path
|
||||
const PID_FILE = path.join(__dirname, 'relay-pty.pid');
|
||||
|
||||
// 检查是否已有实例在运行
|
||||
// Check if an instance is already running
|
||||
function checkSingleInstance() {
|
||||
if (fs.existsSync(PID_FILE)) {
|
||||
try {
|
||||
const oldPid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
||||
// 检查进程是否真的在运行
|
||||
// Check if process is actually running
|
||||
process.kill(oldPid, 0);
|
||||
// 如果没有抛出错误,说明进程还在运行
|
||||
console.error('❌ 错误: relay-pty 服务已经在运行中 (PID: ' + oldPid + ')');
|
||||
console.log('\n如果您确定服务没有运行,可以删除 PID 文件:');
|
||||
// If no error thrown, process is still running
|
||||
console.error('❌ Error: relay-pty service is already running (PID: ' + oldPid + ')');
|
||||
console.log('\nIf you\'re sure the service is not running, you can delete the PID file:');
|
||||
console.log(' rm ' + PID_FILE);
|
||||
console.log('\n或停止现有服务:');
|
||||
console.log('\nOr stop existing service:');
|
||||
console.log(' kill ' + oldPid);
|
||||
process.exit(1);
|
||||
} catch (err) {
|
||||
// 进程不存在,删除旧的 PID 文件
|
||||
// Process doesn't exist, delete old PID file
|
||||
fs.unlinkSync(PID_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
// 写入当前进程的 PID
|
||||
// Write current process PID
|
||||
fs.writeFileSync(PID_FILE, process.pid.toString());
|
||||
}
|
||||
|
||||
// 清理 PID 文件
|
||||
// Clean up PID file
|
||||
function cleanupPidFile() {
|
||||
if (fs.existsSync(PID_FILE)) {
|
||||
fs.unlinkSync(PID_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
// Start service
|
||||
function startService() {
|
||||
// 检查单实例
|
||||
// Check single instance
|
||||
checkSingleInstance();
|
||||
|
||||
console.log('🚀 正在启动 TaskPing PTY Relay 服务...\n');
|
||||
console.log('🚀 Starting TaskPing PTY Relay service...\n');
|
||||
|
||||
const relayPath = path.join(__dirname, 'src/relay/relay-pty.js');
|
||||
|
||||
// 使用 node 直接运行,这样可以看到完整的日志输出
|
||||
// Use node to run directly, so we can see complete log output
|
||||
const relay = spawn('node', [relayPath], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
|
|
@ -126,9 +126,9 @@ function startService() {
|
|||
}
|
||||
});
|
||||
|
||||
// 处理退出
|
||||
// Handle exit
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n⏹️ 正在停止服务...');
|
||||
console.log('\n⏹️ Stopping service...');
|
||||
relay.kill('SIGINT');
|
||||
cleanupPidFile();
|
||||
process.exit(0);
|
||||
|
|
@ -138,7 +138,7 @@ function startService() {
|
|||
process.on('SIGTERM', cleanupPidFile);
|
||||
|
||||
relay.on('error', (error) => {
|
||||
console.error('❌ 启动失败:', error.message);
|
||||
console.error('❌ Startup failed:', error.message);
|
||||
cleanupPidFile();
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -146,50 +146,50 @@ function startService() {
|
|||
relay.on('exit', (code, signal) => {
|
||||
cleanupPidFile();
|
||||
if (signal) {
|
||||
console.log(`\n服务已停止 (信号: ${signal})`);
|
||||
console.log(`\nService stopped (signal: ${signal})`);
|
||||
} else if (code !== 0) {
|
||||
console.error(`\n服务异常退出 (代码: ${code})`);
|
||||
console.error(`\nService exited abnormally (code: ${code})`);
|
||||
process.exit(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示使用说明
|
||||
// Show usage instructions
|
||||
function showInstructions() {
|
||||
console.log('📖 使用说明:');
|
||||
console.log('1. 在 Claude Code 中执行任务时,会发送包含 Token 的提醒邮件');
|
||||
console.log('2. 回复该邮件,内容为要执行的命令');
|
||||
console.log('3. 支持的命令格式:');
|
||||
console.log(' - 直接输入命令文本');
|
||||
console.log(' - 使用 CMD: 前缀,如 "CMD: 继续"');
|
||||
console.log(' - 使用代码块包裹,如:');
|
||||
console.log('📖 Usage instructions:');
|
||||
console.log('1. When executing tasks in Claude Code, reminder emails containing Token will be sent');
|
||||
console.log('2. Reply to that email with the commands to execute');
|
||||
console.log('3. Supported command formats:');
|
||||
console.log(' - Enter command text directly');
|
||||
console.log(' - Use CMD: prefix, like "CMD: continue"');
|
||||
console.log(' - Use code block wrapping, like:');
|
||||
console.log(' ```');
|
||||
console.log(' 你的命令');
|
||||
console.log(' your command');
|
||||
console.log(' ```');
|
||||
console.log('4. 系统会自动提取命令并注入到对应的 Claude Code 会话中');
|
||||
console.log('\n⌨️ 按 Ctrl+C 停止服务\n');
|
||||
console.log('4. System will automatically extract commands and inject them into corresponding Claude Code session');
|
||||
console.log('\n⌨️ Press Ctrl+C to stop service\n');
|
||||
console.log('━'.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
// 主函数
|
||||
// Main function
|
||||
function main() {
|
||||
console.log('╔══════════════════════════════════════════════════════════╗');
|
||||
console.log('║ TaskPing PTY Relay Service ║');
|
||||
console.log('║ 邮件命令中继服务 - 基于 node-pty 的 PTY 模式 ║');
|
||||
console.log('║ Email Command Relay Service - node-pty based PTY mode ║');
|
||||
console.log('╚══════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
// 检查配置
|
||||
// Check configuration
|
||||
checkConfig();
|
||||
|
||||
// 创建示例会话
|
||||
// Create example session
|
||||
createExampleSession();
|
||||
|
||||
// 显示使用说明
|
||||
// Show usage instructions
|
||||
showInstructions();
|
||||
|
||||
// 启动服务
|
||||
// Start service
|
||||
startService();
|
||||
}
|
||||
|
||||
// 运行
|
||||
// Run
|
||||
main();
|
||||
376
taskping.js
376
taskping.js
|
|
@ -108,7 +108,7 @@ class TaskPingCLI {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// 自动捕获当前tmux会话的对话内容
|
||||
// Automatically capture current tmux session conversation content
|
||||
const metadata = await this.captureCurrentConversation();
|
||||
|
||||
const result = await this.notifier.notify(type, metadata);
|
||||
|
|
@ -127,7 +127,7 @@ class TaskPingCLI {
|
|||
const { execSync } = require('child_process');
|
||||
const TmuxMonitor = require('./src/utils/tmux-monitor');
|
||||
|
||||
// 获取当前tmux会话名称
|
||||
// Get current tmux session name
|
||||
let currentSession = null;
|
||||
try {
|
||||
currentSession = execSync('tmux display-message -p "#S"', {
|
||||
|
|
@ -135,7 +135,7 @@ class TaskPingCLI {
|
|||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
} catch (e) {
|
||||
// 不在tmux中运行,返回空metadata
|
||||
// Not running in tmux, return empty metadata
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ class TaskPingCLI {
|
|||
return {};
|
||||
}
|
||||
|
||||
// 使用TmuxMonitor捕获对话
|
||||
// Use TmuxMonitor to capture conversation
|
||||
const tmuxMonitor = new TmuxMonitor();
|
||||
const conversation = tmuxMonitor.getRecentConversation(currentSession);
|
||||
|
||||
|
|
@ -192,11 +192,11 @@ class TaskPingCLI {
|
|||
|
||||
console.log('\nChannels:');
|
||||
|
||||
// 显示所有可用的渠道,包括未启用的
|
||||
// Display all available channels, including disabled ones
|
||||
const allChannels = this.config._channels || {};
|
||||
const activeChannels = status.channels || {};
|
||||
|
||||
// 合并所有渠道信息
|
||||
// Merge all channel information
|
||||
const channelNames = new Set([
|
||||
...Object.keys(allChannels),
|
||||
...Object.keys(activeChannels)
|
||||
|
|
@ -209,12 +209,12 @@ class TaskPingCLI {
|
|||
let enabled, configured, relay;
|
||||
|
||||
if (channelStatus) {
|
||||
// 活跃渠道,使用实际状态
|
||||
// Active channel, use actual status
|
||||
enabled = channelStatus.enabled ? '✅' : '❌';
|
||||
configured = channelStatus.configured ? '✅' : '❌';
|
||||
relay = channelStatus.supportsRelay ? '✅' : '❌';
|
||||
} else {
|
||||
// 非活跃渠道,使用配置状态
|
||||
// Inactive channel, use configuration status
|
||||
enabled = channelConfig.enabled ? '✅' : '❌';
|
||||
configured = this._isChannelConfigured(name, channelConfig) ? '✅' : '❌';
|
||||
relay = this._supportsRelay(name) ? '✅' : '❌';
|
||||
|
|
@ -230,7 +230,7 @@ class TaskPingCLI {
|
|||
_isChannelConfigured(name, config) {
|
||||
switch (name) {
|
||||
case 'desktop':
|
||||
return true; // 桌面通知不需要特殊配置
|
||||
return true; // Desktop notifications don't need special configuration
|
||||
case 'email':
|
||||
return config.config &&
|
||||
config.config.smtp &&
|
||||
|
|
@ -287,10 +287,10 @@ class TaskPingCLI {
|
|||
console.error('Usage: taskping relay <start|stop|status|cleanup>');
|
||||
console.log('');
|
||||
console.log('Commands:');
|
||||
console.log(' start 启动邮件命令中继服务');
|
||||
console.log(' stop 停止邮件命令中继服务');
|
||||
console.log(' status 查看中继服务状态');
|
||||
console.log(' cleanup 清理已完成的命令历史');
|
||||
console.log(' start Start email command relay service');
|
||||
console.log(' stop Stop email command relay service');
|
||||
console.log(' status View relay service status');
|
||||
console.log(' cleanup Clean up completed command history');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -301,59 +301,59 @@ class TaskPingCLI {
|
|||
const emailConfig = this.config.getChannel('email');
|
||||
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
console.error('❌ 邮件渠道未配置或未启用');
|
||||
console.log('请先运行: taskping config');
|
||||
console.error('❌ Email channel not configured or disabled');
|
||||
console.log('Please run first: taskping config');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🚀 启动邮件命令中继服务...');
|
||||
console.log('🚀 Starting email command relay service...');
|
||||
|
||||
const relayService = new CommandRelayService(emailConfig.config);
|
||||
|
||||
// 监听事件
|
||||
// Listen for events
|
||||
relayService.on('started', () => {
|
||||
console.log('✅ 命令中继服务已启动');
|
||||
console.log('📧 正在监听邮件回复...');
|
||||
console.log('💡 现在您可以通过回复邮件来远程执行Claude Code命令');
|
||||
console.log('✅ Command relay service started');
|
||||
console.log('📧 Listening for email replies...');
|
||||
console.log('💡 You can now remotely execute Claude Code commands by replying to emails');
|
||||
console.log('');
|
||||
console.log('按 Ctrl+C 停止服务');
|
||||
console.log('Press Ctrl+C to stop the service');
|
||||
});
|
||||
|
||||
relayService.on('commandQueued', (command) => {
|
||||
console.log(`📨 收到新命令: ${command.command.substring(0, 50)}...`);
|
||||
console.log(`📨 Received new command: ${command.command.substring(0, 50)}...`);
|
||||
});
|
||||
|
||||
relayService.on('commandExecuted', (command) => {
|
||||
console.log(`✅ 命令执行成功: ${command.id}`);
|
||||
console.log(`✅ Command executed successfully: ${command.id}`);
|
||||
});
|
||||
|
||||
relayService.on('commandFailed', (command, error) => {
|
||||
console.log(`❌ 命令执行失败: ${command.id} - ${error.message}`);
|
||||
console.log(`❌ Command execution failed: ${command.id} - ${error.message}`);
|
||||
});
|
||||
|
||||
// 处理优雅关闭
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n🛑 正在停止命令中继服务...');
|
||||
console.log('\n🛑 Stopping command relay service...');
|
||||
await relayService.stop();
|
||||
console.log('✅ 服务已停止');
|
||||
console.log('✅ Service stopped');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 启动服务
|
||||
// Start service
|
||||
await relayService.start();
|
||||
|
||||
// 保持进程运行
|
||||
// Keep process running
|
||||
process.stdin.resume();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 启动中继服务失败:', error.message);
|
||||
console.error('❌ Failed to start relay service:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async stopRelay(args) {
|
||||
console.log('💡 命令中继服务通常通过 Ctrl+C 停止');
|
||||
console.log('如果服务仍在运行,请找到对应的进程并手动终止');
|
||||
console.log('💡 Command relay service usually stopped with Ctrl+C');
|
||||
console.log('If the service is still running, please find the corresponding process and terminate it manually');
|
||||
}
|
||||
|
||||
async relayStatus(args) {
|
||||
|
|
@ -362,27 +362,27 @@ class TaskPingCLI {
|
|||
const path = require('path');
|
||||
const stateFile = path.join(__dirname, 'src/data/relay-state.json');
|
||||
|
||||
console.log('📊 命令中继服务状态\n');
|
||||
console.log('📊 Command relay service status\n');
|
||||
|
||||
// 检查邮件配置
|
||||
// Check email configuration
|
||||
const emailConfig = this.config.getChannel('email');
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
console.log('❌ 邮件渠道未配置');
|
||||
console.log('❌ Email channel not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 邮件配置已启用');
|
||||
console.log('✅ Email configuration enabled');
|
||||
console.log(`📧 SMTP: ${emailConfig.config.smtp.host}:${emailConfig.config.smtp.port}`);
|
||||
console.log(`📥 IMAP: ${emailConfig.config.imap.host}:${emailConfig.config.imap.port}`);
|
||||
console.log(`📬 收件人: ${emailConfig.config.to}`);
|
||||
console.log(`📬 Recipient: ${emailConfig.config.to}`);
|
||||
|
||||
// 检查中继状态
|
||||
// Check relay status
|
||||
if (fs.existsSync(stateFile)) {
|
||||
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
console.log(`\n📋 命令队列: ${state.commandQueue?.length || 0} 个命令`);
|
||||
console.log(`\n📋 Command queue: ${state.commandQueue?.length || 0} commands`);
|
||||
|
||||
if (state.commandQueue && state.commandQueue.length > 0) {
|
||||
console.log('\n最近的命令:');
|
||||
console.log('\nRecent commands:');
|
||||
state.commandQueue.slice(-5).forEach(cmd => {
|
||||
const status = cmd.status === 'completed' ? '✅' :
|
||||
cmd.status === 'failed' ? '❌' :
|
||||
|
|
@ -391,11 +391,11 @@ class TaskPingCLI {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
console.log('\n📋 无命令历史记录');
|
||||
console.log('\n📋 No command history found');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 获取状态失败:', error.message);
|
||||
console.error('❌ Failed to get status:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -406,14 +406,14 @@ class TaskPingCLI {
|
|||
const stateFile = path.join(__dirname, 'src/data/relay-state.json');
|
||||
|
||||
if (!fs.existsSync(stateFile)) {
|
||||
console.log('📋 无需清理,没有找到命令历史');
|
||||
console.log('📋 No cleanup needed, no command history found');
|
||||
return;
|
||||
}
|
||||
|
||||
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
const beforeCount = state.commandQueue?.length || 0;
|
||||
|
||||
// 清理已完成的命令 (保留24小时内的)
|
||||
// Clean up completed commands (keep those within 24 hours)
|
||||
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
state.commandQueue = (state.commandQueue || []).filter(cmd =>
|
||||
cmd.status !== 'completed' ||
|
||||
|
|
@ -425,11 +425,11 @@ class TaskPingCLI {
|
|||
|
||||
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
||||
|
||||
console.log(`🧹 清理完成: 移除了 ${removedCount} 个已完成的命令`);
|
||||
console.log(`📋 剩余 ${afterCount} 个命令在队列中`);
|
||||
console.log(`🧹 Cleanup completed: removed ${removedCount} completed commands`);
|
||||
console.log(`📋 ${afterCount} commands remaining in queue`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 清理失败:', error.message);
|
||||
console.error('❌ Cleanup failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -440,13 +440,13 @@ class TaskPingCLI {
|
|||
const configType = args[0];
|
||||
|
||||
if (!configType) {
|
||||
console.log('可用的配置文件:');
|
||||
console.log(' user - 用户个人配置 (config/user.json)');
|
||||
console.log(' channels - 通知渠道配置 (config/channels.json)');
|
||||
console.log(' default - 默认配置模板 (config/default.json)');
|
||||
console.log('Available configuration files:');
|
||||
console.log(' user - User personal configuration (config/user.json)');
|
||||
console.log(' channels - Notification channel configuration (config/channels.json)');
|
||||
console.log(' default - Default configuration template (config/default.json)');
|
||||
console.log('');
|
||||
console.log('使用方法: taskping edit-config <配置类型>');
|
||||
console.log('例如: taskping edit-config channels');
|
||||
console.log('Usage: taskping edit-config <configuration-type>');
|
||||
console.log('Example: taskping edit-config channels');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -458,23 +458,23 @@ class TaskPingCLI {
|
|||
|
||||
const configFile = configFiles[configType];
|
||||
if (!configFile) {
|
||||
console.error('❌ 无效的配置类型:', configType);
|
||||
console.log('可用类型: user, channels, default');
|
||||
console.error('❌ Invalid configuration type:', configType);
|
||||
console.log('Available types: user, channels, default');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
// Check if file exists
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(configFile)) {
|
||||
console.error('❌ 配置文件不存在:', configFile);
|
||||
console.error('❌ Configuration file does not exist:', configFile);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📝 正在打开配置文件: ${configFile}`);
|
||||
console.log('💡 编辑完成后保存并关闭编辑器即可生效');
|
||||
console.log(`📝 Opening configuration file: ${configFile}`);
|
||||
console.log('💡 Save and close the editor after editing to take effect');
|
||||
console.log('');
|
||||
|
||||
// 确定使用的编辑器
|
||||
// Determine the editor to use
|
||||
const editor = process.env.EDITOR || process.env.VISUAL || this._getDefaultEditor();
|
||||
|
||||
try {
|
||||
|
|
@ -484,36 +484,36 @@ class TaskPingCLI {
|
|||
|
||||
editorProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('✅ 配置文件已保存');
|
||||
console.log('💡 运行 "taskping status" 查看更新后的配置');
|
||||
console.log('✅ Configuration file saved');
|
||||
console.log('💡 Run "taskping status" to view updated configuration');
|
||||
} else {
|
||||
console.log('❌ 编辑器异常退出');
|
||||
console.log('❌ Editor exited abnormally');
|
||||
}
|
||||
});
|
||||
|
||||
editorProcess.on('error', (error) => {
|
||||
console.error('❌ 无法启动编辑器:', error.message);
|
||||
console.error('❌ Unable to start editor:', error.message);
|
||||
console.log('');
|
||||
console.log('💡 你可以手动编辑配置文件:');
|
||||
console.log('💡 You can manually edit the configuration file:');
|
||||
console.log(` ${configFile}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 启动编辑器失败:', error.message);
|
||||
console.error('❌ Failed to start editor:', error.message);
|
||||
console.log('');
|
||||
console.log('💡 你可以手动编辑配置文件:');
|
||||
console.log('💡 You can manually edit the configuration file:');
|
||||
console.log(` ${configFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
_getDefaultEditor() {
|
||||
// 根据平台确定默认编辑器
|
||||
// Determine default editor based on platform
|
||||
if (process.platform === 'win32') {
|
||||
return 'notepad';
|
||||
} else if (process.platform === 'darwin') {
|
||||
return 'nano'; // 在macOS上使用nano,因为大多数用户都有
|
||||
return 'nano'; // Use nano on macOS as most users have it
|
||||
} else {
|
||||
return 'nano'; // Linux默认使用nano
|
||||
return 'nano'; // Linux default to nano
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -534,17 +534,17 @@ class TaskPingCLI {
|
|||
};
|
||||
|
||||
try {
|
||||
console.log('🚀 TaskPing 邮件快速配置向导\n');
|
||||
console.log('🚀 TaskPing Email Quick Setup Wizard\n');
|
||||
|
||||
// 选择邮箱提供商
|
||||
console.log('请选择您的邮箱提供商:');
|
||||
// Select email provider
|
||||
console.log('Please select your email provider:');
|
||||
console.log('1. Gmail');
|
||||
console.log('2. QQ邮箱');
|
||||
console.log('3. 163邮箱');
|
||||
console.log('2. QQ Email');
|
||||
console.log('3. 163 Email');
|
||||
console.log('4. Outlook/Hotmail');
|
||||
console.log('5. 自定义');
|
||||
console.log('5. Custom');
|
||||
|
||||
const providerChoice = await question('\n请选择 (1-5): ');
|
||||
const providerChoice = await question('\nPlease select (1-5): ');
|
||||
|
||||
let smtpHost, smtpPort, imapHost, imapPort, secure;
|
||||
|
||||
|
|
@ -555,8 +555,8 @@ class TaskPingCLI {
|
|||
imapHost = 'imap.gmail.com';
|
||||
imapPort = 993;
|
||||
secure = false;
|
||||
console.log('\n📧 Gmail 配置');
|
||||
console.log('💡 需要先启用两步验证并生成应用密码');
|
||||
console.log('\n📧 Gmail Configuration');
|
||||
console.log('💡 Need to enable two-factor authentication and generate app password first');
|
||||
break;
|
||||
case '2':
|
||||
smtpHost = 'smtp.qq.com';
|
||||
|
|
@ -564,7 +564,7 @@ class TaskPingCLI {
|
|||
imapHost = 'imap.qq.com';
|
||||
imapPort = 993;
|
||||
secure = false;
|
||||
console.log('\n📧 QQ邮箱配置');
|
||||
console.log('\n📧 QQ Email Configuration');
|
||||
break;
|
||||
case '3':
|
||||
smtpHost = 'smtp.163.com';
|
||||
|
|
@ -572,7 +572,7 @@ class TaskPingCLI {
|
|||
imapHost = 'imap.163.com';
|
||||
imapPort = 993;
|
||||
secure = false;
|
||||
console.log('\n📧 163邮箱配置');
|
||||
console.log('\n📧 163 Email Configuration');
|
||||
break;
|
||||
case '4':
|
||||
smtpHost = 'smtp.live.com';
|
||||
|
|
@ -580,29 +580,29 @@ class TaskPingCLI {
|
|||
imapHost = 'imap-mail.outlook.com';
|
||||
imapPort = 993;
|
||||
secure = false;
|
||||
console.log('\n📧 Outlook 配置');
|
||||
console.log('\n📧 Outlook Configuration');
|
||||
break;
|
||||
case '5':
|
||||
console.log('\n📧 自定义配置');
|
||||
smtpHost = await question('SMTP 主机: ');
|
||||
smtpPort = parseInt(await question('SMTP 端口 (默认587): ') || '587');
|
||||
imapHost = await question('IMAP 主机: ');
|
||||
imapPort = parseInt(await question('IMAP 端口 (默认993): ') || '993');
|
||||
const secureInput = await question('使用 SSL/TLS? (y/n): ');
|
||||
console.log('\n📧 Custom Configuration');
|
||||
smtpHost = await question('SMTP Host: ');
|
||||
smtpPort = parseInt(await question('SMTP Port (default 587): ') || '587');
|
||||
imapHost = await question('IMAP Host: ');
|
||||
imapPort = parseInt(await question('IMAP Port (default 993): ') || '993');
|
||||
const secureInput = await question('Use SSL/TLS? (y/n): ');
|
||||
secure = secureInput.toLowerCase() === 'y';
|
||||
break;
|
||||
default:
|
||||
console.log('❌ 无效选择');
|
||||
console.log('❌ Invalid selection');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取邮箱账户信息
|
||||
console.log('\n📝 请输入邮箱账户信息:');
|
||||
const email = await question('邮箱地址: ');
|
||||
const password = await question('密码/应用密码: ');
|
||||
// Get email account information
|
||||
console.log('\n📝 Please enter email account information:');
|
||||
const email = await question('Email address: ');
|
||||
const password = await question('Password/App password: ');
|
||||
|
||||
// 构建配置
|
||||
// Build configuration
|
||||
const emailConfig = {
|
||||
type: "email",
|
||||
enabled: true,
|
||||
|
|
@ -633,7 +633,7 @@ class TaskPingCLI {
|
|||
}
|
||||
};
|
||||
|
||||
// 读取现有配置
|
||||
// Read existing configuration
|
||||
const channelsFile = path.join(__dirname, 'config/channels.json');
|
||||
let channels = {};
|
||||
|
||||
|
|
@ -641,24 +641,24 @@ class TaskPingCLI {
|
|||
channels = JSON.parse(fs.readFileSync(channelsFile, 'utf8'));
|
||||
}
|
||||
|
||||
// 更新邮件配置
|
||||
// Update email configuration
|
||||
channels.email = emailConfig;
|
||||
|
||||
// 保存配置
|
||||
// Save configuration
|
||||
fs.writeFileSync(channelsFile, JSON.stringify(channels, null, 2));
|
||||
|
||||
console.log('\n✅ 邮件配置已保存!');
|
||||
console.log('\n🧪 现在可以测试邮件功能:');
|
||||
console.log('\n✅ Email configuration saved!');
|
||||
console.log('\n🧪 You can now test email functionality:');
|
||||
console.log(' taskping test');
|
||||
console.log('\n🚀 启动命令中继服务:');
|
||||
console.log('\n🚀 Start command relay service:');
|
||||
console.log(' taskping relay start');
|
||||
|
||||
// 询问是否立即测试
|
||||
const testNow = await question('\n立即测试邮件发送? (y/n): ');
|
||||
// Ask if user wants to test immediately
|
||||
const testNow = await question('\nTest email sending now? (y/n): ');
|
||||
if (testNow.toLowerCase() === 'y') {
|
||||
rl.close();
|
||||
|
||||
// 重新加载配置并测试
|
||||
// Reload configuration and test
|
||||
await this.init();
|
||||
await this.handleTest([]);
|
||||
} else {
|
||||
|
|
@ -666,7 +666,7 @@ class TaskPingCLI {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 配置失败:', error.message);
|
||||
console.error('❌ Configuration failed:', error.message);
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -694,10 +694,10 @@ class TaskPingCLI {
|
|||
console.log('Usage: taskping daemon <start|stop|restart|status>');
|
||||
console.log('');
|
||||
console.log('Commands:');
|
||||
console.log(' start 启动后台守护进程');
|
||||
console.log(' stop 停止后台守护进程');
|
||||
console.log(' restart 重启后台守护进程');
|
||||
console.log(' status 查看守护进程状态');
|
||||
console.log(' start Start background daemon process');
|
||||
console.log(' stop Stop background daemon process');
|
||||
console.log(' restart Restart background daemon process');
|
||||
console.log(' status View daemon process status');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -711,13 +711,13 @@ class TaskPingCLI {
|
|||
switch (command) {
|
||||
case 'list':
|
||||
const pending = bridge.getPendingCommands();
|
||||
console.log(`📋 待处理命令: ${pending.length} 个\n`);
|
||||
console.log(`📋 Pending commands: ${pending.length}\n`);
|
||||
if (pending.length > 0) {
|
||||
pending.forEach((cmd, index) => {
|
||||
console.log(`${index + 1}. ${cmd.id}`);
|
||||
console.log(` 命令: ${cmd.command}`);
|
||||
console.log(` 时间: ${cmd.timestamp}`);
|
||||
console.log(` 会话: ${cmd.sessionId}`);
|
||||
console.log(` Command: ${cmd.command}`);
|
||||
console.log(` Time: ${cmd.timestamp}`);
|
||||
console.log(` Session: ${cmd.sessionId}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
|
@ -725,13 +725,13 @@ class TaskPingCLI {
|
|||
|
||||
case 'status':
|
||||
const status = bridge.getStatus();
|
||||
console.log('📊 命令桥接器状态\n');
|
||||
console.log(`待处理命令: ${status.pendingCommands}`);
|
||||
console.log(`已处理命令: ${status.processedCommands}`);
|
||||
console.log(`命令目录: ${status.commandsDir}`);
|
||||
console.log(`响应目录: ${status.responseDir}`);
|
||||
console.log('📊 Command bridge status\n');
|
||||
console.log(`Pending commands: ${status.pendingCommands}`);
|
||||
console.log(`Processed commands: ${status.processedCommands}`);
|
||||
console.log(`Commands directory: ${status.commandsDir}`);
|
||||
console.log(`Response directory: ${status.responseDir}`);
|
||||
if (status.recentCommands.length > 0) {
|
||||
console.log('\n最近命令:');
|
||||
console.log('\nRecent commands:');
|
||||
status.recentCommands.forEach(cmd => {
|
||||
console.log(` • ${cmd.command} (${cmd.timestamp})`);
|
||||
});
|
||||
|
|
@ -740,7 +740,7 @@ class TaskPingCLI {
|
|||
|
||||
case 'cleanup':
|
||||
bridge.cleanup();
|
||||
console.log('🧹 已清理旧的命令文件');
|
||||
console.log('🧹 Old command files cleaned up');
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
|
|
@ -748,17 +748,17 @@ class TaskPingCLI {
|
|||
for (const cmd of pending2) {
|
||||
bridge.markCommandProcessed(cmd.id, 'cancelled', 'Manually cancelled');
|
||||
}
|
||||
console.log(`🗑️ 已清除 ${pending2.length} 个待处理命令`);
|
||||
console.log(`🗑️ Cleared ${pending2.length} pending commands`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: taskping commands <list|status|cleanup|clear>');
|
||||
console.log('');
|
||||
console.log('Commands:');
|
||||
console.log(' list 显示待处理的邮件命令');
|
||||
console.log(' status 显示命令桥接器状态');
|
||||
console.log(' cleanup 清理旧的命令文件');
|
||||
console.log(' clear 清除所有待处理命令');
|
||||
console.log(' list Show pending email commands');
|
||||
console.log(' status Show command bridge status');
|
||||
console.log(' cleanup Clean up old command files');
|
||||
console.log(' clear Clear all pending commands');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -767,14 +767,14 @@ class TaskPingCLI {
|
|||
const ClipboardAutomation = require('./src/automation/clipboard-automation');
|
||||
const automation = new ClipboardAutomation();
|
||||
|
||||
const testCommand = args.join(' ') || 'echo "测试邮件回复自动粘贴功能"';
|
||||
const testCommand = args.join(' ') || 'echo "Testing email reply auto-paste functionality"';
|
||||
|
||||
console.log('🧪 测试自动粘贴功能');
|
||||
console.log(`📝 测试命令: ${testCommand}`);
|
||||
console.log('\n⚠️ 请确保 Claude Code 或 Terminal 窗口已打开并处于活动状态');
|
||||
console.log('⏳ 3 秒后自动发送命令...\n');
|
||||
console.log('🧪 Testing auto-paste functionality');
|
||||
console.log(`📝 Test command: ${testCommand}`);
|
||||
console.log('\n⚠️ Please ensure Claude Code or Terminal window is open and active');
|
||||
console.log('⏳ Command will be sent automatically in 3 seconds...\n');
|
||||
|
||||
// 倒计时
|
||||
// Countdown
|
||||
for (let i = 3; i > 0; i--) {
|
||||
process.stdout.write(`${i}... `);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
|
@ -784,14 +784,14 @@ class TaskPingCLI {
|
|||
try {
|
||||
const success = await automation.sendCommand(testCommand);
|
||||
if (success) {
|
||||
console.log('✅ 命令已自动粘贴!');
|
||||
console.log('💡 如果没有看到效果,请检查应用权限和窗口状态');
|
||||
console.log('✅ Command has been auto-pasted!');
|
||||
console.log('💡 If you don\'t see the effect, please check app permissions and window status');
|
||||
} else {
|
||||
console.log('❌ 自动粘贴失败');
|
||||
console.log('💡 请确保给予自动化权限并打开目标应用');
|
||||
console.log('❌ Auto-paste failed');
|
||||
console.log('💡 Please ensure automation permissions are granted and target app is open');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
console.error('❌ Test failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -805,37 +805,37 @@ class TaskPingCLI {
|
|||
const SimpleAutomation = require('./src/automation/simple-automation');
|
||||
const automation = new SimpleAutomation();
|
||||
|
||||
const testCommand = args.join(' ') || 'echo "测试简单自动化功能"';
|
||||
const testCommand = args.join(' ') || 'echo "Testing simple automation functionality"';
|
||||
|
||||
console.log('🧪 测试简单自动化功能');
|
||||
console.log(`📝 测试命令: ${testCommand}`);
|
||||
console.log('\n这个测试会:');
|
||||
console.log('1. 📋 将命令复制到剪贴板');
|
||||
console.log('2. 📄 保存命令到文件');
|
||||
console.log('3. 🔔 发送通知(包含对话框)');
|
||||
console.log('4. 🤖 尝试自动粘贴(如果有权限)');
|
||||
console.log('\n⏳ 开始测试...\n');
|
||||
console.log('🧪 Testing simple automation functionality');
|
||||
console.log(`📝 Test command: ${testCommand}`);
|
||||
console.log('\nThis test will:');
|
||||
console.log('1. 📋 Copy command to clipboard');
|
||||
console.log('2. 📄 Save command to file');
|
||||
console.log('3. 🔔 Send notification (including dialog box)');
|
||||
console.log('4. 🤖 Attempt auto-paste (if permissions granted)');
|
||||
console.log('\n⏳ Starting test...\n');
|
||||
|
||||
try {
|
||||
const success = await automation.sendCommand(testCommand, 'test-session');
|
||||
if (success) {
|
||||
console.log('✅ 测试成功!');
|
||||
console.log('\n📋 下一步操作:');
|
||||
console.log('1. 检查是否收到了通知');
|
||||
console.log('2. 检查命令是否已复制到剪贴板');
|
||||
console.log('3. 如果看到对话框,可以选择打开命令文件');
|
||||
console.log('4. 手动粘贴到 Claude Code 中(如果没有自动粘贴)');
|
||||
console.log('✅ Test successful!');
|
||||
console.log('\n📋 Next steps:');
|
||||
console.log('1. Check if you received notification');
|
||||
console.log('2. Check if command was copied to clipboard');
|
||||
console.log('3. If you see dialog box, you can choose to open command file');
|
||||
console.log('4. Manually paste to Claude Code (if auto-paste didn\'t work)');
|
||||
|
||||
const status = automation.getStatus();
|
||||
console.log(`\n📄 命令文件: ${status.commandFile}`);
|
||||
console.log(`\n📄 Command file: ${status.commandFile}`);
|
||||
if (status.commandFileExists) {
|
||||
console.log('💡 可以运行 "open -t ' + status.commandFile + '" 查看命令文件');
|
||||
console.log('💡 You can run "open -t ' + status.commandFile + '" to view command file');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 测试失败');
|
||||
console.log('❌ Test failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 测试过程中发生错误:', error.message);
|
||||
console.error('❌ Error occurred during test:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -843,50 +843,50 @@ class TaskPingCLI {
|
|||
const ClaudeAutomation = require('./src/automation/claude-automation');
|
||||
const automation = new ClaudeAutomation();
|
||||
|
||||
const testCommand = args.join(' ') || 'echo "这是一个自动化测试命令,来自邮件回复"';
|
||||
const testCommand = args.join(' ') || 'echo "This is an automated test command from email reply"';
|
||||
|
||||
console.log('🤖 测试 Claude Code 专用自动化');
|
||||
console.log(`📝 测试命令: ${testCommand}`);
|
||||
console.log('\n⚠️ 请确保:');
|
||||
console.log(' 1. Claude Code 应用已打开');
|
||||
console.log(' 2. 或者 Terminal/iTerm2 等终端应用已打开');
|
||||
console.log(' 3. 已经给予必要的辅助功能权限');
|
||||
console.log('\n⏳ 5 秒后开始完全自动化测试...\n');
|
||||
console.log('🤖 Testing Claude Code specialized automation');
|
||||
console.log(`📝 Test command: ${testCommand}`);
|
||||
console.log('\n⚠️ Please ensure:');
|
||||
console.log(' 1. Claude Code application is open');
|
||||
console.log(' 2. Or Terminal/iTerm2 etc. terminal applications are open');
|
||||
console.log(' 3. Necessary accessibility permissions have been granted');
|
||||
console.log('\n⏳ Full automation test will start in 5 seconds...\n');
|
||||
|
||||
// 倒计时
|
||||
// Countdown
|
||||
for (let i = 5; i > 0; i--) {
|
||||
process.stdout.write(`${i}... `);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
console.log('\n🚀 开始自动化...\n');
|
||||
console.log('\n🚀 Starting automation...\n');
|
||||
|
||||
try {
|
||||
// 检查权限
|
||||
// Check permissions
|
||||
const hasPermission = await automation.requestPermissions();
|
||||
if (!hasPermission) {
|
||||
console.log('⚠️ 权限检查失败,但仍会尝试执行...');
|
||||
console.log('⚠️ Permission check failed, but will still attempt execution...');
|
||||
}
|
||||
|
||||
// 执行完全自动化
|
||||
// Execute full automation
|
||||
const success = await automation.sendCommand(testCommand, 'test-session');
|
||||
|
||||
if (success) {
|
||||
console.log('✅ 完全自动化测试成功!');
|
||||
console.log('💡 命令应该已经自动输入到 Claude Code 并开始执行');
|
||||
console.log('🔍 请检查 Claude Code 窗口是否收到了命令');
|
||||
console.log('✅ Full automation test successful!');
|
||||
console.log('💡 Command should have been automatically input to Claude Code and started execution');
|
||||
console.log('🔍 Please check Claude Code window to see if command was received');
|
||||
} else {
|
||||
console.log('❌ 自动化测试失败');
|
||||
console.log('💡 可能的原因:');
|
||||
console.log(' • 没有找到 Claude Code 或终端应用');
|
||||
console.log(' • 权限不足');
|
||||
console.log(' • 应用没有响应');
|
||||
console.log('\n🔧 建议:');
|
||||
console.log(' 1. 运行 "taskping setup-permissions" 检查权限');
|
||||
console.log(' 2. 确保 Claude Code 在前台运行');
|
||||
console.log(' 3. 尝试先手动在 Claude Code 中点击输入框');
|
||||
console.log('❌ Automation test failed');
|
||||
console.log('💡 Possible reasons:');
|
||||
console.log(' • Claude Code or terminal application not found');
|
||||
console.log(' • Insufficient permissions');
|
||||
console.log(' • Application not responding');
|
||||
console.log('\n🔧 Suggestions:');
|
||||
console.log(' 1. Run "taskping setup-permissions" to check permissions');
|
||||
console.log(' 2. Ensure Claude Code is running in foreground');
|
||||
console.log(' 3. Try manually clicking input box in Claude Code first');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 测试过程中发生错误:', error.message);
|
||||
console.error('❌ Error occurred during test:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -943,15 +943,15 @@ Commands Subcommands:
|
|||
Examples:
|
||||
taskping notify --type completed
|
||||
taskping test
|
||||
taskping setup-email # 快速配置邮件 (推荐)
|
||||
taskping edit-config channels # 直接编辑配置文件
|
||||
taskping config # 交互式配置
|
||||
taskping setup-email # Quick email setup (recommended)
|
||||
taskping edit-config channels # Edit configuration files directly
|
||||
taskping config # Interactive configuration
|
||||
taskping install
|
||||
taskping daemon start # 启动后台服务 (推荐)
|
||||
taskping daemon status # 查看服务状态
|
||||
taskping test-claude # 测试完全自动化 (推荐)
|
||||
taskping commands list # 查看待处理的邮件命令
|
||||
taskping relay start # 前台运行 (需要保持窗口)
|
||||
taskping daemon start # Start background service (recommended)
|
||||
taskping daemon status # View service status
|
||||
taskping test-claude # Test full automation (recommended)
|
||||
taskping commands list # View pending email commands
|
||||
taskping relay start # Run in foreground (need to keep window open)
|
||||
|
||||
For more information, visit: https://github.com/TaskPing/TaskPing
|
||||
`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue