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