369 lines
12 KiB
JavaScript
369 lines
12 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* TaskPing 邮件自动化
|
|||
|
|
* 监听邮件回复并自动输入到Claude Code
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const Imap = require('node-imap');
|
|||
|
|
const { simpleParser } = require('mailparser');
|
|||
|
|
const { spawn } = require('child_process');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
class EmailAutomation {
|
|||
|
|
constructor() {
|
|||
|
|
this.configPath = path.join(__dirname, 'config/channels.json');
|
|||
|
|
this.imap = null;
|
|||
|
|
this.isRunning = false;
|
|||
|
|
this.config = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async start() {
|
|||
|
|
console.log('🚀 TaskPing 邮件自动化启动中...\n');
|
|||
|
|
|
|||
|
|
// 加载配置
|
|||
|
|
if (!this.loadConfig()) {
|
|||
|
|
console.log('❌ 请先配置邮件: npm run config');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📧 监听邮箱: ${this.config.imap.auth.user}`);
|
|||
|
|
console.log(`📬 发送通知到: ${this.config.to}\n`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await this.connectToEmail();
|
|||
|
|
this.startListening();
|
|||
|
|
|
|||
|
|
console.log('✅ 邮件监听启动成功');
|
|||
|
|
console.log('💌 现在可以回复TaskPing邮件来发送命令到Claude Code');
|
|||
|
|
console.log('按 Ctrl+C 停止服务\n');
|
|||
|
|
|
|||
|
|
this.setupGracefulShutdown();
|
|||
|
|
process.stdin.resume();
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 启动失败:', error.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadConfig() {
|
|||
|
|
try {
|
|||
|
|
const data = fs.readFileSync(this.configPath, 'utf8');
|
|||
|
|
const config = JSON.parse(data);
|
|||
|
|
|
|||
|
|
if (!config.email?.enabled) {
|
|||
|
|
console.log('❌ 邮件功能未启用');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.config = config.email.config;
|
|||
|
|
return true;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.log('❌ 配置文件读取失败');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async connectToEmail() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
this.imap = new Imap({
|
|||
|
|
user: this.config.imap.auth.user,
|
|||
|
|
password: this.config.imap.auth.pass,
|
|||
|
|
host: this.config.imap.host,
|
|||
|
|
port: this.config.imap.port,
|
|||
|
|
tls: this.config.imap.secure,
|
|||
|
|
connTimeout: 60000,
|
|||
|
|
authTimeout: 30000,
|
|||
|
|
keepalive: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.imap.once('ready', () => {
|
|||
|
|
console.log('📬 IMAP连接成功');
|
|||
|
|
resolve();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.imap.once('error', reject);
|
|||
|
|
this.imap.once('end', () => {
|
|||
|
|
if (this.isRunning) {
|
|||
|
|
console.log('🔄 连接断开,尝试重连...');
|
|||
|
|
setTimeout(() => this.connectToEmail().catch(console.error), 5000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.imap.connect();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
startListening() {
|
|||
|
|
this.isRunning = true;
|
|||
|
|
console.log('👂 开始监听新邮件...');
|
|||
|
|
|
|||
|
|
// 每15秒检查一次新邮件
|
|||
|
|
setInterval(() => {
|
|||
|
|
if (this.isRunning) {
|
|||
|
|
this.checkNewEmails();
|
|||
|
|
}
|
|||
|
|
}, 15000);
|
|||
|
|
|
|||
|
|
// 立即检查一次
|
|||
|
|
this.checkNewEmails();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async checkNewEmails() {
|
|||
|
|
try {
|
|||
|
|
await this.openInbox();
|
|||
|
|
|
|||
|
|
// 查找最近1小时内的未读邮件
|
|||
|
|
const since = new Date();
|
|||
|
|
since.setHours(since.getHours() - 1);
|
|||
|
|
|
|||
|
|
this.imap.search([['UNSEEN'], ['SINCE', since]], (err, results) => {
|
|||
|
|
if (err) {
|
|||
|
|
console.error('搜索邮件失败:', err.message);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (results.length > 0) {
|
|||
|
|
console.log(`📧 发现 ${results.length} 封新邮件`);
|
|||
|
|
this.processEmails(results);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('检查邮件失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
openInbox() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
this.imap.openBox('INBOX', false, (err, box) => {
|
|||
|
|
if (err) reject(err);
|
|||
|
|
else resolve(box);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
processEmails(emailUids) {
|
|||
|
|
const fetch = this.imap.fetch(emailUids, {
|
|||
|
|
bodies: '',
|
|||
|
|
markSeen: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
fetch.on('message', (msg) => {
|
|||
|
|
let buffer = '';
|
|||
|
|
|
|||
|
|
msg.on('body', (stream) => {
|
|||
|
|
stream.on('data', (chunk) => {
|
|||
|
|
buffer += chunk.toString('utf8');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
stream.once('end', async () => {
|
|||
|
|
try {
|
|||
|
|
const parsed = await simpleParser(buffer);
|
|||
|
|
await this.handleEmailReply(parsed);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('处理邮件失败:', error.message);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleEmailReply(email) {
|
|||
|
|
// 检查是否是TaskPing回复
|
|||
|
|
if (!this.isTaskPingReply(email)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提取命令
|
|||
|
|
const command = this.extractCommand(email);
|
|||
|
|
if (!command) {
|
|||
|
|
console.log('邮件中未找到有效命令');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`🎯 收到命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`);
|
|||
|
|
|
|||
|
|
// 执行命令到Claude Code
|
|||
|
|
await this.sendToClaudeCode(command);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isTaskPingReply(email) {
|
|||
|
|
const subject = email.subject || '';
|
|||
|
|
return subject.includes('[TaskPing]') ||
|
|||
|
|
subject.match(/^(Re:|RE:|回复:)/i);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extractCommand(email) {
|
|||
|
|
let text = email.text || '';
|
|||
|
|
const lines = text.split('\n');
|
|||
|
|
const commandLines = [];
|
|||
|
|
|
|||
|
|
for (const line of lines) {
|
|||
|
|
// 停止处理当遇到原始邮件标记
|
|||
|
|
if (line.includes('-----Original Message-----') ||
|
|||
|
|
line.includes('--- Original Message ---') ||
|
|||
|
|
line.includes('在') && line.includes('写道:') ||
|
|||
|
|
line.includes('On') && line.includes('wrote:') ||
|
|||
|
|
line.match(/^>\s*/) ||
|
|||
|
|
line.includes('会话ID:')) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 跳过签名
|
|||
|
|
if (line.includes('--') ||
|
|||
|
|
line.includes('Sent from') ||
|
|||
|
|
line.includes('发自我的')) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
commandLines.push(line);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return commandLines.join('\n').trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async sendToClaudeCode(command) {
|
|||
|
|
console.log('🤖 正在发送命令到Claude Code...');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 方法1: 复制到剪贴板
|
|||
|
|
await this.copyToClipboard(command);
|
|||
|
|
|
|||
|
|
// 方法2: 强制自动化输入
|
|||
|
|
const success = await this.forceAutomation(command);
|
|||
|
|
|
|||
|
|
if (success) {
|
|||
|
|
console.log('✅ 命令已自动输入到Claude Code');
|
|||
|
|
} else {
|
|||
|
|
console.log('⚠️ 自动输入失败,命令已复制到剪贴板');
|
|||
|
|
console.log('💡 请手动在Claude Code中粘贴 (Cmd+V)');
|
|||
|
|
|
|||
|
|
// 发送通知提醒
|
|||
|
|
await this.sendNotification(command);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 发送命令失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async copyToClipboard(command) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const pbcopy = spawn('pbcopy');
|
|||
|
|
pbcopy.stdin.write(command);
|
|||
|
|
pbcopy.stdin.end();
|
|||
|
|
|
|||
|
|
pbcopy.on('close', (code) => {
|
|||
|
|
if (code === 0) {
|
|||
|
|
resolve();
|
|||
|
|
} else {
|
|||
|
|
reject(new Error('剪贴板复制失败'));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async forceAutomation(command) {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
// 转义命令中的特殊字符
|
|||
|
|
const escapedCommand = command
|
|||
|
|
.replace(/\\/g, '\\\\')
|
|||
|
|
.replace(/"/g, '\\"')
|
|||
|
|
.replace(/'/g, "\\'");
|
|||
|
|
|
|||
|
|
const script = `
|
|||
|
|
tell application "System Events"
|
|||
|
|
-- 查找Claude Code相关应用
|
|||
|
|
set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"}
|
|||
|
|
set targetApp to null
|
|||
|
|
|
|||
|
|
repeat with appName in claudeApps
|
|||
|
|
try
|
|||
|
|
if application process appName exists then
|
|||
|
|
set targetApp to application process appName
|
|||
|
|
exit repeat
|
|||
|
|
end if
|
|||
|
|
end try
|
|||
|
|
end repeat
|
|||
|
|
|
|||
|
|
-- 如果没找到Claude,查找开发工具
|
|||
|
|
if targetApp is null then
|
|||
|
|
set devApps to {"Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor"}
|
|||
|
|
repeat with appName in devApps
|
|||
|
|
try
|
|||
|
|
if application process appName exists then
|
|||
|
|
set targetApp to application process appName
|
|||
|
|
exit repeat
|
|||
|
|
end if
|
|||
|
|
end try
|
|||
|
|
end repeat
|
|||
|
|
end if
|
|||
|
|
|
|||
|
|
if targetApp is not null then
|
|||
|
|
-- 激活应用
|
|||
|
|
set frontmost of targetApp to true
|
|||
|
|
delay 1
|
|||
|
|
|
|||
|
|
-- 确保窗口激活
|
|||
|
|
repeat 10 times
|
|||
|
|
if frontmost of targetApp then exit repeat
|
|||
|
|
delay 0.1
|
|||
|
|
end repeat
|
|||
|
|
|
|||
|
|
-- 清空并输入命令
|
|||
|
|
keystroke "a" using command down
|
|||
|
|
delay 0.3
|
|||
|
|
keystroke "${escapedCommand}"
|
|||
|
|
delay 0.5
|
|||
|
|
keystroke return
|
|||
|
|
|
|||
|
|
return "success"
|
|||
|
|
else
|
|||
|
|
return "no_app"
|
|||
|
|
end if
|
|||
|
|
end tell
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const osascript = spawn('osascript', ['-e', script]);
|
|||
|
|
let output = '';
|
|||
|
|
|
|||
|
|
osascript.stdout.on('data', (data) => {
|
|||
|
|
output += data.toString().trim();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
osascript.on('close', (code) => {
|
|||
|
|
const success = code === 0 && output === 'success';
|
|||
|
|
resolve(success);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
osascript.on('error', () => resolve(false));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async sendNotification(command) {
|
|||
|
|
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
|||
|
|
|
|||
|
|
const script = `
|
|||
|
|
display notification "邮件命令已准备好,请在Claude Code中粘贴执行" with title "TaskPing" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "default"
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
spawn('osascript', ['-e', script]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setupGracefulShutdown() {
|
|||
|
|
process.on('SIGINT', () => {
|
|||
|
|
console.log('\n🛑 正在停止邮件监听...');
|
|||
|
|
this.isRunning = false;
|
|||
|
|
if (this.imap) {
|
|||
|
|
this.imap.end();
|
|||
|
|
}
|
|||
|
|
console.log('✅ 服务已停止');
|
|||
|
|
process.exit(0);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 启动服务
|
|||
|
|
const automation = new EmailAutomation();
|
|||
|
|
automation.start().catch(console.error);
|