931 lines
35 KiB
JavaScript
931 lines
35 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* TaskPing - Claude Code Smart Notification System
|
|||
|
|
* Main entry point for the CLI tool
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const Logger = require('./src/core/logger');
|
|||
|
|
const Notifier = require('./src/core/notifier');
|
|||
|
|
const ConfigManager = require('./src/core/config');
|
|||
|
|
|
|||
|
|
class TaskPingCLI {
|
|||
|
|
constructor() {
|
|||
|
|
this.logger = new Logger('CLI');
|
|||
|
|
this.config = new ConfigManager();
|
|||
|
|
this.notifier = new Notifier(this.config);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async init() {
|
|||
|
|
// Load configuration
|
|||
|
|
this.config.load();
|
|||
|
|
|
|||
|
|
// Initialize channels
|
|||
|
|
await this.notifier.initializeChannels();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async run() {
|
|||
|
|
const args = process.argv.slice(2);
|
|||
|
|
const command = args[0];
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await this.init();
|
|||
|
|
|
|||
|
|
switch (command) {
|
|||
|
|
case 'notify':
|
|||
|
|
await this.handleNotify(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'test':
|
|||
|
|
await this.handleTest(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'status':
|
|||
|
|
await this.handleStatus(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'config':
|
|||
|
|
await this.handleConfig(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'install':
|
|||
|
|
await this.handleInstall(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'relay':
|
|||
|
|
await this.handleRelay(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'edit-config':
|
|||
|
|
await this.handleEditConfig(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'setup-email':
|
|||
|
|
await this.handleSetupEmail(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'daemon':
|
|||
|
|
await this.handleDaemon(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'commands':
|
|||
|
|
await this.handleCommands(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'test-paste':
|
|||
|
|
await this.handleTestPaste(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'test-simple':
|
|||
|
|
await this.handleTestSimple(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'test-claude':
|
|||
|
|
await this.handleTestClaude(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'setup-permissions':
|
|||
|
|
await this.handleSetupPermissions(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'diagnose':
|
|||
|
|
await this.handleDiagnose(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case '--help':
|
|||
|
|
case '-h':
|
|||
|
|
case undefined:
|
|||
|
|
this.showHelp();
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.error(`Unknown command: ${command}`);
|
|||
|
|
this.showHelp();
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('CLI error:', error.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleNotify(args) {
|
|||
|
|
const typeIndex = args.findIndex(arg => arg === '--type');
|
|||
|
|
|
|||
|
|
if (typeIndex === -1 || typeIndex + 1 >= args.length) {
|
|||
|
|
console.error('Usage: taskping notify --type <completed|waiting>');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const type = args[typeIndex + 1];
|
|||
|
|
|
|||
|
|
if (!['completed', 'waiting'].includes(type)) {
|
|||
|
|
console.error('Invalid type. Use: completed or waiting');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = await this.notifier.notify(type);
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
this.logger.info(`${type} notification sent successfully`);
|
|||
|
|
process.exit(0);
|
|||
|
|
} else {
|
|||
|
|
this.logger.error(`Failed to send ${type} notification`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleTest(args) {
|
|||
|
|
console.log('Testing notification channels...\n');
|
|||
|
|
|
|||
|
|
const results = await this.notifier.test();
|
|||
|
|
|
|||
|
|
for (const [channel, result] of Object.entries(results)) {
|
|||
|
|
const status = result.success ? '✅ PASS' : '❌ FAIL';
|
|||
|
|
console.log(`${channel}: ${status}`);
|
|||
|
|
if (result.error) {
|
|||
|
|
console.log(` Error: ${result.error}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const passCount = Object.values(results).filter(r => r.success).length;
|
|||
|
|
const totalCount = Object.keys(results).length;
|
|||
|
|
|
|||
|
|
console.log(`\nTest completed: ${passCount}/${totalCount} channels passed`);
|
|||
|
|
|
|||
|
|
if (passCount === 0) {
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleStatus(args) {
|
|||
|
|
const status = this.notifier.getStatus();
|
|||
|
|
|
|||
|
|
console.log('TaskPing Status\n');
|
|||
|
|
console.log('Configuration:');
|
|||
|
|
console.log(` Enabled: ${status.enabled ? 'Yes' : 'No'}`);
|
|||
|
|
console.log(` Language: ${status.config.language}`);
|
|||
|
|
console.log(` Sounds: ${status.config.sound.completed} / ${status.config.sound.waiting}`);
|
|||
|
|
|
|||
|
|
console.log('\nChannels:');
|
|||
|
|
|
|||
|
|
// 显示所有可用的渠道,包括未启用的
|
|||
|
|
const allChannels = this.config._channels || {};
|
|||
|
|
const activeChannels = status.channels || {};
|
|||
|
|
|
|||
|
|
// 合并所有渠道信息
|
|||
|
|
const channelNames = new Set([
|
|||
|
|
...Object.keys(allChannels),
|
|||
|
|
...Object.keys(activeChannels)
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
for (const name of channelNames) {
|
|||
|
|
const channelConfig = allChannels[name] || {};
|
|||
|
|
const channelStatus = activeChannels[name];
|
|||
|
|
|
|||
|
|
let enabled, configured, relay;
|
|||
|
|
|
|||
|
|
if (channelStatus) {
|
|||
|
|
// 活跃渠道,使用实际状态
|
|||
|
|
enabled = channelStatus.enabled ? '✅' : '❌';
|
|||
|
|
configured = channelStatus.configured ? '✅' : '❌';
|
|||
|
|
relay = channelStatus.supportsRelay ? '✅' : '❌';
|
|||
|
|
} else {
|
|||
|
|
// 非活跃渠道,使用配置状态
|
|||
|
|
enabled = channelConfig.enabled ? '✅' : '❌';
|
|||
|
|
configured = this._isChannelConfigured(name, channelConfig) ? '✅' : '❌';
|
|||
|
|
relay = this._supportsRelay(name) ? '✅' : '❌';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(` ${name}:`);
|
|||
|
|
console.log(` Enabled: ${enabled}`);
|
|||
|
|
console.log(` Configured: ${configured}`);
|
|||
|
|
console.log(` Supports Relay: ${relay}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_isChannelConfigured(name, config) {
|
|||
|
|
switch (name) {
|
|||
|
|
case 'desktop':
|
|||
|
|
return true; // 桌面通知不需要特殊配置
|
|||
|
|
case 'email':
|
|||
|
|
return config.config &&
|
|||
|
|
config.config.smtp &&
|
|||
|
|
config.config.smtp.host &&
|
|||
|
|
config.config.smtp.auth &&
|
|||
|
|
config.config.smtp.auth.user &&
|
|||
|
|
config.config.to;
|
|||
|
|
default:
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_supportsRelay(name) {
|
|||
|
|
switch (name) {
|
|||
|
|
case 'email':
|
|||
|
|
return true;
|
|||
|
|
case 'desktop':
|
|||
|
|
default:
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleConfig(args) {
|
|||
|
|
// Launch the configuration tool
|
|||
|
|
const ConfigTool = require('./src/tools/config-manager');
|
|||
|
|
const configTool = new ConfigTool(this.config);
|
|||
|
|
await configTool.run(args);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleInstall(args) {
|
|||
|
|
// Launch the installer
|
|||
|
|
const Installer = require('./src/tools/installer');
|
|||
|
|
const installer = new Installer(this.config);
|
|||
|
|
await installer.run(args);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleRelay(args) {
|
|||
|
|
const subcommand = args[0];
|
|||
|
|
|
|||
|
|
switch (subcommand) {
|
|||
|
|
case 'start':
|
|||
|
|
await this.startRelay(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'stop':
|
|||
|
|
await this.stopRelay(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'status':
|
|||
|
|
await this.relayStatus(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
case 'cleanup':
|
|||
|
|
await this.cleanupRelay(args.slice(1));
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.error('Usage: taskping relay <start|stop|status|cleanup>');
|
|||
|
|
console.log('');
|
|||
|
|
console.log('Commands:');
|
|||
|
|
console.log(' start 启动邮件命令中继服务');
|
|||
|
|
console.log(' stop 停止邮件命令中继服务');
|
|||
|
|
console.log(' status 查看中继服务状态');
|
|||
|
|
console.log(' cleanup 清理已完成的命令历史');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async startRelay(args) {
|
|||
|
|
try {
|
|||
|
|
const CommandRelayService = require('./src/relay/command-relay');
|
|||
|
|
const emailConfig = this.config.getChannel('email');
|
|||
|
|
|
|||
|
|
if (!emailConfig || !emailConfig.enabled) {
|
|||
|
|
console.error('❌ 邮件渠道未配置或未启用');
|
|||
|
|
console.log('请先运行: taskping config');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('🚀 启动邮件命令中继服务...');
|
|||
|
|
|
|||
|
|
const relayService = new CommandRelayService(emailConfig.config);
|
|||
|
|
|
|||
|
|
// 监听事件
|
|||
|
|
relayService.on('started', () => {
|
|||
|
|
console.log('✅ 命令中继服务已启动');
|
|||
|
|
console.log('📧 正在监听邮件回复...');
|
|||
|
|
console.log('💡 现在您可以通过回复邮件来远程执行Claude Code命令');
|
|||
|
|
console.log('');
|
|||
|
|
console.log('按 Ctrl+C 停止服务');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
relayService.on('commandQueued', (command) => {
|
|||
|
|
console.log(`📨 收到新命令: ${command.command.substring(0, 50)}...`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
relayService.on('commandExecuted', (command) => {
|
|||
|
|
console.log(`✅ 命令执行成功: ${command.id}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
relayService.on('commandFailed', (command, error) => {
|
|||
|
|
console.log(`❌ 命令执行失败: ${command.id} - ${error.message}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 处理优雅关闭
|
|||
|
|
process.on('SIGINT', async () => {
|
|||
|
|
console.log('\n🛑 正在停止命令中继服务...');
|
|||
|
|
await relayService.stop();
|
|||
|
|
console.log('✅ 服务已停止');
|
|||
|
|
process.exit(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 启动服务
|
|||
|
|
await relayService.start();
|
|||
|
|
|
|||
|
|
// 保持进程运行
|
|||
|
|
process.stdin.resume();
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 启动中继服务失败:', error.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async stopRelay(args) {
|
|||
|
|
console.log('💡 命令中继服务通常通过 Ctrl+C 停止');
|
|||
|
|
console.log('如果服务仍在运行,请找到对应的进程并手动终止');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async relayStatus(args) {
|
|||
|
|
try {
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const stateFile = path.join(__dirname, 'src/data/relay-state.json');
|
|||
|
|
|
|||
|
|
console.log('📊 命令中继服务状态\n');
|
|||
|
|
|
|||
|
|
// 检查邮件配置
|
|||
|
|
const emailConfig = this.config.getChannel('email');
|
|||
|
|
if (!emailConfig || !emailConfig.enabled) {
|
|||
|
|
console.log('❌ 邮件渠道未配置');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('✅ 邮件配置已启用');
|
|||
|
|
console.log(`📧 SMTP: ${emailConfig.config.smtp.host}:${emailConfig.config.smtp.port}`);
|
|||
|
|
console.log(`📥 IMAP: ${emailConfig.config.imap.host}:${emailConfig.config.imap.port}`);
|
|||
|
|
console.log(`📬 收件人: ${emailConfig.config.to}`);
|
|||
|
|
|
|||
|
|
// 检查中继状态
|
|||
|
|
if (fs.existsSync(stateFile)) {
|
|||
|
|
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|||
|
|
console.log(`\n📋 命令队列: ${state.commandQueue?.length || 0} 个命令`);
|
|||
|
|
|
|||
|
|
if (state.commandQueue && state.commandQueue.length > 0) {
|
|||
|
|
console.log('\n最近的命令:');
|
|||
|
|
state.commandQueue.slice(-5).forEach(cmd => {
|
|||
|
|
const status = cmd.status === 'completed' ? '✅' :
|
|||
|
|
cmd.status === 'failed' ? '❌' :
|
|||
|
|
cmd.status === 'executing' ? '⏳' : '⏸️';
|
|||
|
|
console.log(` ${status} ${cmd.id}: ${cmd.command.substring(0, 50)}...`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
console.log('\n📋 无命令历史记录');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 获取状态失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async cleanupRelay(args) {
|
|||
|
|
try {
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const stateFile = path.join(__dirname, 'src/data/relay-state.json');
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(stateFile)) {
|
|||
|
|
console.log('📋 无需清理,没有找到命令历史');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|||
|
|
const beforeCount = state.commandQueue?.length || 0;
|
|||
|
|
|
|||
|
|
// 清理已完成的命令 (保留24小时内的)
|
|||
|
|
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|||
|
|
state.commandQueue = (state.commandQueue || []).filter(cmd =>
|
|||
|
|
cmd.status !== 'completed' ||
|
|||
|
|
new Date(cmd.completedAt || cmd.queuedAt) > cutoff
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const afterCount = state.commandQueue.length;
|
|||
|
|
const removedCount = beforeCount - afterCount;
|
|||
|
|
|
|||
|
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|||
|
|
|
|||
|
|
console.log(`🧹 清理完成: 移除了 ${removedCount} 个已完成的命令`);
|
|||
|
|
console.log(`📋 剩余 ${afterCount} 个命令在队列中`);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 清理失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleEditConfig(args) {
|
|||
|
|
const { spawn } = require('child_process');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const configType = args[0];
|
|||
|
|
|
|||
|
|
if (!configType) {
|
|||
|
|
console.log('可用的配置文件:');
|
|||
|
|
console.log(' user - 用户个人配置 (config/user.json)');
|
|||
|
|
console.log(' channels - 通知渠道配置 (config/channels.json)');
|
|||
|
|
console.log(' default - 默认配置模板 (config/default.json)');
|
|||
|
|
console.log('');
|
|||
|
|
console.log('使用方法: taskping edit-config <配置类型>');
|
|||
|
|
console.log('例如: taskping edit-config channels');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const configFiles = {
|
|||
|
|
'user': path.join(__dirname, 'config/user.json'),
|
|||
|
|
'channels': path.join(__dirname, 'config/channels.json'),
|
|||
|
|
'default': path.join(__dirname, 'config/default.json')
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const configFile = configFiles[configType];
|
|||
|
|
if (!configFile) {
|
|||
|
|
console.error('❌ 无效的配置类型:', configType);
|
|||
|
|
console.log('可用类型: user, channels, default');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查文件是否存在
|
|||
|
|
const fs = require('fs');
|
|||
|
|
if (!fs.existsSync(configFile)) {
|
|||
|
|
console.error('❌ 配置文件不存在:', configFile);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📝 正在打开配置文件: ${configFile}`);
|
|||
|
|
console.log('💡 编辑完成后保存并关闭编辑器即可生效');
|
|||
|
|
console.log('');
|
|||
|
|
|
|||
|
|
// 确定使用的编辑器
|
|||
|
|
const editor = process.env.EDITOR || process.env.VISUAL || this._getDefaultEditor();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const editorProcess = spawn(editor, [configFile], {
|
|||
|
|
stdio: 'inherit'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
editorProcess.on('close', (code) => {
|
|||
|
|
if (code === 0) {
|
|||
|
|
console.log('✅ 配置文件已保存');
|
|||
|
|
console.log('💡 运行 "taskping status" 查看更新后的配置');
|
|||
|
|
} else {
|
|||
|
|
console.log('❌ 编辑器异常退出');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
editorProcess.on('error', (error) => {
|
|||
|
|
console.error('❌ 无法启动编辑器:', error.message);
|
|||
|
|
console.log('');
|
|||
|
|
console.log('💡 你可以手动编辑配置文件:');
|
|||
|
|
console.log(` ${configFile}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 启动编辑器失败:', error.message);
|
|||
|
|
console.log('');
|
|||
|
|
console.log('💡 你可以手动编辑配置文件:');
|
|||
|
|
console.log(` ${configFile}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_getDefaultEditor() {
|
|||
|
|
// 根据平台确定默认编辑器
|
|||
|
|
if (process.platform === 'win32') {
|
|||
|
|
return 'notepad';
|
|||
|
|
} else if (process.platform === 'darwin') {
|
|||
|
|
return 'nano'; // 在macOS上使用nano,因为大多数用户都有
|
|||
|
|
} else {
|
|||
|
|
return 'nano'; // Linux默认使用nano
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleSetupEmail(args) {
|
|||
|
|
const readline = require('readline');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const rl = readline.createInterface({
|
|||
|
|
input: process.stdin,
|
|||
|
|
output: process.stdout
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const question = (prompt) => {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
rl.question(prompt, resolve);
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log('🚀 TaskPing 邮件快速配置向导\n');
|
|||
|
|
|
|||
|
|
// 选择邮箱提供商
|
|||
|
|
console.log('请选择您的邮箱提供商:');
|
|||
|
|
console.log('1. Gmail');
|
|||
|
|
console.log('2. QQ邮箱');
|
|||
|
|
console.log('3. 163邮箱');
|
|||
|
|
console.log('4. Outlook/Hotmail');
|
|||
|
|
console.log('5. 自定义');
|
|||
|
|
|
|||
|
|
const providerChoice = await question('\n请选择 (1-5): ');
|
|||
|
|
|
|||
|
|
let smtpHost, smtpPort, imapHost, imapPort, secure;
|
|||
|
|
|
|||
|
|
switch (providerChoice) {
|
|||
|
|
case '1':
|
|||
|
|
smtpHost = 'smtp.gmail.com';
|
|||
|
|
smtpPort = 587;
|
|||
|
|
imapHost = 'imap.gmail.com';
|
|||
|
|
imapPort = 993;
|
|||
|
|
secure = false;
|
|||
|
|
console.log('\n📧 Gmail 配置');
|
|||
|
|
console.log('💡 需要先启用两步验证并生成应用密码');
|
|||
|
|
break;
|
|||
|
|
case '2':
|
|||
|
|
smtpHost = 'smtp.qq.com';
|
|||
|
|
smtpPort = 587;
|
|||
|
|
imapHost = 'imap.qq.com';
|
|||
|
|
imapPort = 993;
|
|||
|
|
secure = false;
|
|||
|
|
console.log('\n📧 QQ邮箱配置');
|
|||
|
|
break;
|
|||
|
|
case '3':
|
|||
|
|
smtpHost = 'smtp.163.com';
|
|||
|
|
smtpPort = 587;
|
|||
|
|
imapHost = 'imap.163.com';
|
|||
|
|
imapPort = 993;
|
|||
|
|
secure = false;
|
|||
|
|
console.log('\n📧 163邮箱配置');
|
|||
|
|
break;
|
|||
|
|
case '4':
|
|||
|
|
smtpHost = 'smtp.live.com';
|
|||
|
|
smtpPort = 587;
|
|||
|
|
imapHost = 'imap-mail.outlook.com';
|
|||
|
|
imapPort = 993;
|
|||
|
|
secure = false;
|
|||
|
|
console.log('\n📧 Outlook 配置');
|
|||
|
|
break;
|
|||
|
|
case '5':
|
|||
|
|
console.log('\n📧 自定义配置');
|
|||
|
|
smtpHost = await question('SMTP 主机: ');
|
|||
|
|
smtpPort = parseInt(await question('SMTP 端口 (默认587): ') || '587');
|
|||
|
|
imapHost = await question('IMAP 主机: ');
|
|||
|
|
imapPort = parseInt(await question('IMAP 端口 (默认993): ') || '993');
|
|||
|
|
const secureInput = await question('使用 SSL/TLS? (y/n): ');
|
|||
|
|
secure = secureInput.toLowerCase() === 'y';
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.log('❌ 无效选择');
|
|||
|
|
rl.close();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取邮箱账户信息
|
|||
|
|
console.log('\n📝 请输入邮箱账户信息:');
|
|||
|
|
const email = await question('邮箱地址: ');
|
|||
|
|
const password = await question('密码/应用密码: ');
|
|||
|
|
|
|||
|
|
// 构建配置
|
|||
|
|
const emailConfig = {
|
|||
|
|
type: "email",
|
|||
|
|
enabled: true,
|
|||
|
|
config: {
|
|||
|
|
smtp: {
|
|||
|
|
host: smtpHost,
|
|||
|
|
port: smtpPort,
|
|||
|
|
secure: secure,
|
|||
|
|
auth: {
|
|||
|
|
user: email,
|
|||
|
|
pass: password
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
imap: {
|
|||
|
|
host: imapHost,
|
|||
|
|
port: imapPort,
|
|||
|
|
secure: true,
|
|||
|
|
auth: {
|
|||
|
|
user: email,
|
|||
|
|
pass: password
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
from: `TaskPing <${email}>`,
|
|||
|
|
to: email,
|
|||
|
|
template: {
|
|||
|
|
checkInterval: 30
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 读取现有配置
|
|||
|
|
const channelsFile = path.join(__dirname, 'config/channels.json');
|
|||
|
|
let channels = {};
|
|||
|
|
|
|||
|
|
if (fs.existsSync(channelsFile)) {
|
|||
|
|
channels = JSON.parse(fs.readFileSync(channelsFile, 'utf8'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新邮件配置
|
|||
|
|
channels.email = emailConfig;
|
|||
|
|
|
|||
|
|
// 保存配置
|
|||
|
|
fs.writeFileSync(channelsFile, JSON.stringify(channels, null, 2));
|
|||
|
|
|
|||
|
|
console.log('\n✅ 邮件配置已保存!');
|
|||
|
|
console.log('\n🧪 现在可以测试邮件功能:');
|
|||
|
|
console.log(' taskping test');
|
|||
|
|
console.log('\n🚀 启动命令中继服务:');
|
|||
|
|
console.log(' taskping relay start');
|
|||
|
|
|
|||
|
|
// 询问是否立即测试
|
|||
|
|
const testNow = await question('\n立即测试邮件发送? (y/n): ');
|
|||
|
|
if (testNow.toLowerCase() === 'y') {
|
|||
|
|
rl.close();
|
|||
|
|
|
|||
|
|
// 重新加载配置并测试
|
|||
|
|
await this.init();
|
|||
|
|
await this.handleTest([]);
|
|||
|
|
} else {
|
|||
|
|
rl.close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 配置失败:', error.message);
|
|||
|
|
rl.close();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleDaemon(args) {
|
|||
|
|
const TaskPingDaemon = require('./src/daemon/taskping-daemon');
|
|||
|
|
const daemon = new TaskPingDaemon();
|
|||
|
|
|
|||
|
|
const command = args[0];
|
|||
|
|
|
|||
|
|
switch (command) {
|
|||
|
|
case 'start':
|
|||
|
|
await daemon.start();
|
|||
|
|
break;
|
|||
|
|
case 'stop':
|
|||
|
|
await daemon.stop();
|
|||
|
|
break;
|
|||
|
|
case 'restart':
|
|||
|
|
await daemon.restart();
|
|||
|
|
break;
|
|||
|
|
case 'status':
|
|||
|
|
daemon.showStatus();
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.log('Usage: taskping daemon <start|stop|restart|status>');
|
|||
|
|
console.log('');
|
|||
|
|
console.log('Commands:');
|
|||
|
|
console.log(' start 启动后台守护进程');
|
|||
|
|
console.log(' stop 停止后台守护进程');
|
|||
|
|
console.log(' restart 重启后台守护进程');
|
|||
|
|
console.log(' status 查看守护进程状态');
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleCommands(args) {
|
|||
|
|
const ClaudeCommandBridge = require('./src/relay/claude-command-bridge');
|
|||
|
|
const bridge = new ClaudeCommandBridge();
|
|||
|
|
|
|||
|
|
const command = args[0];
|
|||
|
|
|
|||
|
|
switch (command) {
|
|||
|
|
case 'list':
|
|||
|
|
const pending = bridge.getPendingCommands();
|
|||
|
|
console.log(`📋 待处理命令: ${pending.length} 个\n`);
|
|||
|
|
if (pending.length > 0) {
|
|||
|
|
pending.forEach((cmd, index) => {
|
|||
|
|
console.log(`${index + 1}. ${cmd.id}`);
|
|||
|
|
console.log(` 命令: ${cmd.command}`);
|
|||
|
|
console.log(` 时间: ${cmd.timestamp}`);
|
|||
|
|
console.log(` 会话: ${cmd.sessionId}`);
|
|||
|
|
console.log('');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'status':
|
|||
|
|
const status = bridge.getStatus();
|
|||
|
|
console.log('📊 命令桥接器状态\n');
|
|||
|
|
console.log(`待处理命令: ${status.pendingCommands}`);
|
|||
|
|
console.log(`已处理命令: ${status.processedCommands}`);
|
|||
|
|
console.log(`命令目录: ${status.commandsDir}`);
|
|||
|
|
console.log(`响应目录: ${status.responseDir}`);
|
|||
|
|
if (status.recentCommands.length > 0) {
|
|||
|
|
console.log('\n最近命令:');
|
|||
|
|
status.recentCommands.forEach(cmd => {
|
|||
|
|
console.log(` • ${cmd.command} (${cmd.timestamp})`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'cleanup':
|
|||
|
|
bridge.cleanup();
|
|||
|
|
console.log('🧹 已清理旧的命令文件');
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'clear':
|
|||
|
|
const pending2 = bridge.getPendingCommands();
|
|||
|
|
for (const cmd of pending2) {
|
|||
|
|
bridge.markCommandProcessed(cmd.id, 'cancelled', 'Manually cancelled');
|
|||
|
|
}
|
|||
|
|
console.log(`🗑️ 已清除 ${pending2.length} 个待处理命令`);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
console.log('Usage: taskping commands <list|status|cleanup|clear>');
|
|||
|
|
console.log('');
|
|||
|
|
console.log('Commands:');
|
|||
|
|
console.log(' list 显示待处理的邮件命令');
|
|||
|
|
console.log(' status 显示命令桥接器状态');
|
|||
|
|
console.log(' cleanup 清理旧的命令文件');
|
|||
|
|
console.log(' clear 清除所有待处理命令');
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleTestPaste(args) {
|
|||
|
|
const ClipboardAutomation = require('./src/automation/clipboard-automation');
|
|||
|
|
const automation = new ClipboardAutomation();
|
|||
|
|
|
|||
|
|
const testCommand = args.join(' ') || 'echo "测试邮件回复自动粘贴功能"';
|
|||
|
|
|
|||
|
|
console.log('🧪 测试自动粘贴功能');
|
|||
|
|
console.log(`📝 测试命令: ${testCommand}`);
|
|||
|
|
console.log('\n⚠️ 请确保 Claude Code 或 Terminal 窗口已打开并处于活动状态');
|
|||
|
|
console.log('⏳ 3 秒后自动发送命令...\n');
|
|||
|
|
|
|||
|
|
// 倒计时
|
|||
|
|
for (let i = 3; i > 0; i--) {
|
|||
|
|
process.stdout.write(`${i}... `);
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|||
|
|
}
|
|||
|
|
console.log('\n');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const success = await automation.sendCommand(testCommand);
|
|||
|
|
if (success) {
|
|||
|
|
console.log('✅ 命令已自动粘贴!');
|
|||
|
|
console.log('💡 如果没有看到效果,请检查应用权限和窗口状态');
|
|||
|
|
} else {
|
|||
|
|
console.log('❌ 自动粘贴失败');
|
|||
|
|
console.log('💡 请确保给予自动化权限并打开目标应用');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 测试失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleSetupPermissions(args) {
|
|||
|
|
const PermissionSetup = require('./setup-permissions');
|
|||
|
|
const setup = new PermissionSetup();
|
|||
|
|
await setup.checkAndSetup();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleTestSimple(args) {
|
|||
|
|
const SimpleAutomation = require('./src/automation/simple-automation');
|
|||
|
|
const automation = new SimpleAutomation();
|
|||
|
|
|
|||
|
|
const testCommand = args.join(' ') || 'echo "测试简单自动化功能"';
|
|||
|
|
|
|||
|
|
console.log('🧪 测试简单自动化功能');
|
|||
|
|
console.log(`📝 测试命令: ${testCommand}`);
|
|||
|
|
console.log('\n这个测试会:');
|
|||
|
|
console.log('1. 📋 将命令复制到剪贴板');
|
|||
|
|
console.log('2. 📄 保存命令到文件');
|
|||
|
|
console.log('3. 🔔 发送通知(包含对话框)');
|
|||
|
|
console.log('4. 🤖 尝试自动粘贴(如果有权限)');
|
|||
|
|
console.log('\n⏳ 开始测试...\n');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const success = await automation.sendCommand(testCommand, 'test-session');
|
|||
|
|
if (success) {
|
|||
|
|
console.log('✅ 测试成功!');
|
|||
|
|
console.log('\n📋 下一步操作:');
|
|||
|
|
console.log('1. 检查是否收到了通知');
|
|||
|
|
console.log('2. 检查命令是否已复制到剪贴板');
|
|||
|
|
console.log('3. 如果看到对话框,可以选择打开命令文件');
|
|||
|
|
console.log('4. 手动粘贴到 Claude Code 中(如果没有自动粘贴)');
|
|||
|
|
|
|||
|
|
const status = automation.getStatus();
|
|||
|
|
console.log(`\n📄 命令文件: ${status.commandFile}`);
|
|||
|
|
if (status.commandFileExists) {
|
|||
|
|
console.log('💡 可以运行 "open -t ' + status.commandFile + '" 查看命令文件');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
console.log('❌ 测试失败');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 测试过程中发生错误:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleTestClaude(args) {
|
|||
|
|
const ClaudeAutomation = require('./src/automation/claude-automation');
|
|||
|
|
const automation = new ClaudeAutomation();
|
|||
|
|
|
|||
|
|
const testCommand = args.join(' ') || 'echo "这是一个自动化测试命令,来自邮件回复"';
|
|||
|
|
|
|||
|
|
console.log('🤖 测试 Claude Code 专用自动化');
|
|||
|
|
console.log(`📝 测试命令: ${testCommand}`);
|
|||
|
|
console.log('\n⚠️ 请确保:');
|
|||
|
|
console.log(' 1. Claude Code 应用已打开');
|
|||
|
|
console.log(' 2. 或者 Terminal/iTerm2 等终端应用已打开');
|
|||
|
|
console.log(' 3. 已经给予必要的辅助功能权限');
|
|||
|
|
console.log('\n⏳ 5 秒后开始完全自动化测试...\n');
|
|||
|
|
|
|||
|
|
// 倒计时
|
|||
|
|
for (let i = 5; i > 0; i--) {
|
|||
|
|
process.stdout.write(`${i}... `);
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|||
|
|
}
|
|||
|
|
console.log('\n🚀 开始自动化...\n');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 检查权限
|
|||
|
|
const hasPermission = await automation.requestPermissions();
|
|||
|
|
if (!hasPermission) {
|
|||
|
|
console.log('⚠️ 权限检查失败,但仍会尝试执行...');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行完全自动化
|
|||
|
|
const success = await automation.sendCommand(testCommand, 'test-session');
|
|||
|
|
|
|||
|
|
if (success) {
|
|||
|
|
console.log('✅ 完全自动化测试成功!');
|
|||
|
|
console.log('💡 命令应该已经自动输入到 Claude Code 并开始执行');
|
|||
|
|
console.log('🔍 请检查 Claude Code 窗口是否收到了命令');
|
|||
|
|
} else {
|
|||
|
|
console.log('❌ 自动化测试失败');
|
|||
|
|
console.log('💡 可能的原因:');
|
|||
|
|
console.log(' • 没有找到 Claude Code 或终端应用');
|
|||
|
|
console.log(' • 权限不足');
|
|||
|
|
console.log(' • 应用没有响应');
|
|||
|
|
console.log('\n🔧 建议:');
|
|||
|
|
console.log(' 1. 运行 "taskping setup-permissions" 检查权限');
|
|||
|
|
console.log(' 2. 确保 Claude Code 在前台运行');
|
|||
|
|
console.log(' 3. 尝试先手动在 Claude Code 中点击输入框');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 测试过程中发生错误:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleDiagnose(args) {
|
|||
|
|
const AutomationDiagnostic = require('./diagnose-automation');
|
|||
|
|
const diagnostic = new AutomationDiagnostic();
|
|||
|
|
await diagnostic.runDiagnostic();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showHelp() {
|
|||
|
|
console.log(`
|
|||
|
|
TaskPing - Claude Code Smart Notification System
|
|||
|
|
|
|||
|
|
Usage: taskping <command> [options]
|
|||
|
|
|
|||
|
|
Commands:
|
|||
|
|
notify --type <type> Send a notification (completed|waiting)
|
|||
|
|
test Test all notification channels
|
|||
|
|
status Show system status
|
|||
|
|
config Launch configuration manager
|
|||
|
|
setup-email Quick email setup wizard
|
|||
|
|
edit-config <type> Edit configuration files directly
|
|||
|
|
install Install and configure Claude Code hooks
|
|||
|
|
relay <subcommand> Manage email command relay service
|
|||
|
|
daemon <subcommand> Manage background daemon service
|
|||
|
|
commands <subcommand> Manage email commands and bridge
|
|||
|
|
test-paste [command] Test automatic paste functionality
|
|||
|
|
test-simple [command] Test simple automation (recommended)
|
|||
|
|
test-claude [command] Test Claude Code full automation
|
|||
|
|
setup-permissions Setup macOS permissions for automation
|
|||
|
|
diagnose Diagnose automation issues
|
|||
|
|
|
|||
|
|
Options:
|
|||
|
|
-h, --help Show this help message
|
|||
|
|
|
|||
|
|
Relay Subcommands:
|
|||
|
|
relay start Start email command relay service
|
|||
|
|
relay stop Stop email command relay service
|
|||
|
|
relay status Show relay service status
|
|||
|
|
relay cleanup Clean up completed command history
|
|||
|
|
|
|||
|
|
Daemon Subcommands:
|
|||
|
|
daemon start Start background daemon service
|
|||
|
|
daemon stop Stop background daemon service
|
|||
|
|
daemon restart Restart background daemon service
|
|||
|
|
daemon status Show daemon service status
|
|||
|
|
|
|||
|
|
Commands Subcommands:
|
|||
|
|
commands list Show pending email commands
|
|||
|
|
commands status Show command bridge status
|
|||
|
|
commands cleanup Clean up old command files
|
|||
|
|
commands clear Clear all pending commands
|
|||
|
|
|
|||
|
|
Examples:
|
|||
|
|
taskping notify --type completed
|
|||
|
|
taskping test
|
|||
|
|
taskping setup-email # 快速配置邮件 (推荐)
|
|||
|
|
taskping edit-config channels # 直接编辑配置文件
|
|||
|
|
taskping config # 交互式配置
|
|||
|
|
taskping install
|
|||
|
|
taskping daemon start # 启动后台服务 (推荐)
|
|||
|
|
taskping daemon status # 查看服务状态
|
|||
|
|
taskping test-claude # 测试完全自动化 (推荐)
|
|||
|
|
taskping commands list # 查看待处理的邮件命令
|
|||
|
|
taskping relay start # 前台运行 (需要保持窗口)
|
|||
|
|
|
|||
|
|
For more information, visit: https://github.com/TaskPing/TaskPing
|
|||
|
|
`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Run CLI if this file is executed directly
|
|||
|
|
if (require.main === module) {
|
|||
|
|
const cli = new TaskPingCLI();
|
|||
|
|
cli.run().catch(error => {
|
|||
|
|
console.error('Fatal error:', error.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = TaskPingCLI;
|