Add TaskPing project files
- Initial project structure with documentation - Email automation and notification system - Claude command integration - Configuration management system - Daemon process for task monitoring - Multi-channel notification support (email, desktop) - Session data storage - Development and deployment scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
64df43a4dc
commit
45576e5a3e
|
|
@ -0,0 +1,50 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# TaskPing 项目结构
|
||||
|
||||
## 📁 核心文件
|
||||
|
||||
### 🎯 主要脚本
|
||||
- **`hook-notify.js`** - 核心通知脚本,被Claude Code hooks调用
|
||||
- **`config-tool.js`** - 交互式配置管理工具
|
||||
- **`install.js`** - 自动安装脚本,配置Claude Code hooks
|
||||
|
||||
### ⚙️ 配置文件
|
||||
- **`config.json`** - 用户配置(语言、音效、自定义消息等)
|
||||
- **`i18n.json`** - 多语言文本
|
||||
- **`claude-hooks.json`** - Claude Code hooks配置模板
|
||||
|
||||
### 📚 文档
|
||||
- **`README.md`** - 完整项目文档
|
||||
- **`QUICKSTART.md`** - 快速开始指南
|
||||
- **`TaskPing.md`** - 产品规格文档
|
||||
|
||||
### 🎵 音效
|
||||
- **`sounds/`** - 自定义音效目录
|
||||
- 支持格式:`.wav`, `.mp3`, `.m4a`, `.aiff`, `.ogg`
|
||||
- 用户可以添加自己的音效文件
|
||||
|
||||
### 📦 包管理
|
||||
- **`package.json`** - Node.js项目配置
|
||||
- **`LICENSE`** - MIT开源协议
|
||||
|
||||
## 🚀 使用流程
|
||||
|
||||
1. **安装**: `node install.js`
|
||||
2. **配置**: `node config-tool.js`
|
||||
3. **使用**: 正常使用Claude Code,自动收到通知
|
||||
|
||||
## 🔧 开发说明
|
||||
|
||||
- 主要语言:JavaScript (Node.js)
|
||||
- 支持平台:macOS, Windows, Linux
|
||||
- 核心依赖:无(使用Node.js内置模块)
|
||||
- 音效播放:系统原生通知API
|
||||
|
||||
项目专注于简洁、高效的Claude Code任务通知功能。
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# TaskPing 新项目结构设计
|
||||
|
||||
## 📁 重构后的目录结构
|
||||
|
||||
```
|
||||
TaskPing/
|
||||
├── 📦 src/ # 源代码目录
|
||||
│ ├── 🎯 core/ # 核心模块
|
||||
│ │ ├── notifier.js # 通知核心类
|
||||
│ │ ├── config.js # 配置管理
|
||||
│ │ └── logger.js # 日志记录
|
||||
│ ├── 📢 channels/ # 通知渠道
|
||||
│ │ ├── base/ # 基础接口
|
||||
│ │ │ └── channel.js # 通知渠道基类
|
||||
│ │ ├── local/ # 本地通知
|
||||
│ │ │ └── desktop.js # 桌面通知
|
||||
│ │ ├── email/ # 邮件通知
|
||||
│ │ │ └── smtp.js # SMTP邮件
|
||||
│ │ ├── chat/ # 聊天平台
|
||||
│ │ │ ├── discord.js # Discord
|
||||
│ │ │ ├── telegram.js # Telegram
|
||||
│ │ │ ├── whatsapp.js # WhatsApp
|
||||
│ │ │ └── feishu.js # 飞书
|
||||
│ │ └── webhook/ # Webhook通知
|
||||
│ │ └── generic.js # 通用Webhook
|
||||
│ ├── 🔄 relay/ # 命令中继模块
|
||||
│ │ ├── server.js # 中继服务器
|
||||
│ │ ├── client.js # 中继客户端
|
||||
│ │ └── commands.js # 命令处理
|
||||
│ ├── 🛠️ tools/ # 工具模块
|
||||
│ │ ├── cli.js # 命令行工具
|
||||
│ │ ├── installer.js # 安装器
|
||||
│ │ └── config-manager.js # 配置管理器
|
||||
│ └── 🎵 assets/ # 静态资源
|
||||
│ └── sounds/ # 音效文件
|
||||
├── 📋 config/ # 配置文件
|
||||
│ ├── default.json # 默认配置
|
||||
│ ├── user.json # 用户配置
|
||||
│ ├── channels.json # 渠道配置
|
||||
│ └── templates/ # 配置模板
|
||||
│ ├── claude-hooks.json # Claude Code hooks
|
||||
│ └── channel-templates/ # 各渠道配置模板
|
||||
├── 📚 docs/ # 文档目录
|
||||
│ ├── README.md # 主文档
|
||||
│ ├── QUICKSTART.md # 快速开始
|
||||
│ ├── api/ # API文档
|
||||
│ ├── channels/ # 各渠道使用指南
|
||||
│ └── examples/ # 使用示例
|
||||
├── 🧪 tests/ # 测试目录
|
||||
│ ├── unit/ # 单元测试
|
||||
│ ├── integration/ # 集成测试
|
||||
│ └── fixtures/ # 测试数据
|
||||
├── 📦 项目根文件
|
||||
│ ├── package.json # Node.js配置
|
||||
│ ├── taskping.js # 主入口文件
|
||||
│ ├── LICENSE # 开源协议
|
||||
│ └── .gitignore # Git忽略
|
||||
└── 🚀 scripts/ # 构建脚本
|
||||
├── build.js # 构建脚本
|
||||
├── dev.js # 开发脚本
|
||||
└── deploy.js # 部署脚本
|
||||
```
|
||||
|
||||
## 🏗️ 模块设计原则
|
||||
|
||||
### 1. 核心抽象
|
||||
- **NotificationChannel**: 所有通知渠道的基类
|
||||
- **CommandRelay**: 命令中继的统一接口
|
||||
- **ConfigManager**: 统一的配置管理
|
||||
|
||||
### 2. 插件化架构
|
||||
- 每个通知渠道独立模块
|
||||
- 支持动态加载和卸载
|
||||
- 标准化的接口和生命周期
|
||||
|
||||
### 3. 配置分离
|
||||
- 用户配置与默认配置分离
|
||||
- 渠道配置模块化
|
||||
- 敏感信息加密存储
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
### Phase 1: 本地通知 (当前)
|
||||
Claude Code → hook-notify.js → Desktop Notification
|
||||
|
||||
### Phase 2: 多渠道通知
|
||||
Claude Code → Core Notifier → Channel Router → [Email|Discord|Telegram|...]
|
||||
|
||||
### Phase 3: 命令中继
|
||||
Channel → Command Relay Server → Claude Code Client → Execute Command
|
||||
|
||||
## 🎯 入口点设计
|
||||
|
||||
```bash
|
||||
# 主命令
|
||||
taskping [command] [options]
|
||||
|
||||
# 子命令
|
||||
taskping install # 安装和配置
|
||||
taskping config # 配置管理
|
||||
taskping test # 测试通知
|
||||
taskping serve # 启动中继服务
|
||||
taskping relay # 连接中继服务
|
||||
```
|
||||
|
||||
这个结构为未来的扩展提供了良好的基础。
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# TaskPing 快速开始指南
|
||||
|
||||
## 30秒快速设置
|
||||
|
||||
### 1️⃣ 安装
|
||||
```bash
|
||||
node taskping.js install
|
||||
# 按提示输入 'y' 确认安装
|
||||
```
|
||||
|
||||
### 2️⃣ 测试
|
||||
```bash
|
||||
node taskping.js test
|
||||
# 应该看到两个测试通知
|
||||
```
|
||||
|
||||
### 3️⃣ 使用
|
||||
```bash
|
||||
claude
|
||||
# 正常使用Claude Code,会自动收到通知
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
**TaskPing通过Claude Code的hooks机制工作:**
|
||||
|
||||
- ✅ **任务完成时** → 桌面通知:"任务已完成,Claude正在等待下一步指令"
|
||||
- ⏳ **等待输入时** → 桌面通知:"Claude需要您的进一步指导"
|
||||
|
||||
## 实际效果
|
||||
|
||||
```
|
||||
你: 请帮我重构这个函数
|
||||
Claude: [分析代码中...]
|
||||
|
||||
📱 通知: 任务已完成,Claude正在等待下一步指令
|
||||
|
||||
Claude: 我发现了3个改进点,你想看哪个?
|
||||
|
||||
📱 通知: Claude需要您的进一步指导
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```bash
|
||||
# 打开配置菜单
|
||||
node taskping.js config
|
||||
|
||||
# 查看当前配置
|
||||
node taskping.js config --show
|
||||
|
||||
# 查看系统状态
|
||||
node taskping.js status
|
||||
|
||||
# 可以调整:
|
||||
# - 语言 (中文/英文/日文)
|
||||
# - 提示音
|
||||
# - 启用/禁用通知
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
**收不到通知?**
|
||||
- macOS: 在系统偏好设置中允许终端发送通知
|
||||
- Linux: 安装 `sudo apt-get install libnotify-bin`
|
||||
- 测试: `node taskping.js test`
|
||||
|
||||
**安装失败?**
|
||||
- 确保Node.js版本 >= 14
|
||||
- 检查Claude Code配置目录权限
|
||||
|
||||
---
|
||||
|
||||
**就这么简单!现在你可以专心做其他事情,Claude完成任务时会主动通知你。**
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
# TaskPing - Claude Code 邮件自动化
|
||||
|
||||
TaskPing 是一个智能的邮件自动化工具,可以监听你的邮件回复,并将回复内容自动输入到 Claude Code 中执行。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 配置邮箱
|
||||
```bash
|
||||
npm run config
|
||||
```
|
||||
按照提示配置你的邮箱信息(SMTP和IMAP)。
|
||||
|
||||
### 2. 启动服务
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### 3. 使用流程
|
||||
1. 当 Claude Code 完成任务时,TaskPing 会发送邮件通知到你的邮箱
|
||||
2. 直接回复这封邮件,在邮件中写入你想让 Claude Code 执行的下一个命令
|
||||
3. TaskPing 会自动监听到你的回复,提取命令内容,并自动输入到 Claude Code 中
|
||||
4. 命令会自动执行,无需任何手动操作
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 🎯 智能检测
|
||||
- 基于Claude Code官方hooks机制
|
||||
- 自动识别任务完成和等待输入状态
|
||||
- 无需手动监控,完全自动化
|
||||
|
||||
### 📢 多渠道通知
|
||||
- **桌面通知**:即时本地通知
|
||||
- **邮件通知**:远程邮件提醒 + 回复执行命令
|
||||
- 支持自定义通知声音和消息内容
|
||||
- 同时启用多个通知渠道
|
||||
|
||||
### 🏠 远程命令执行
|
||||
- **邮件回复**:直接回复邮件执行下一步命令
|
||||
- **自动化流程**:人不在电脑前也能继续对话
|
||||
- **安全机制**:会话过期、命令过滤、来源验证
|
||||
|
||||
### 🌍 跨平台支持
|
||||
- **macOS**:原生通知中心 + 系统提示音
|
||||
- **Windows**:Toast通知系统
|
||||
- **Linux**:libnotify桌面通知
|
||||
|
||||
### 🎛️ 灵活配置
|
||||
- 多语言支持(中文、英文、日文)
|
||||
- 自定义提示音(支持系统音效)
|
||||
- 邮件 SMTP/IMAP 配置
|
||||
- 可调节通知频率和超时时间
|
||||
|
||||
## 📦 快速安装
|
||||
|
||||
### 自动安装(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 克隆或下载项目
|
||||
git clone <repository-url>
|
||||
cd TaskPing
|
||||
|
||||
# 2. 运行安装脚本
|
||||
node taskping.js install
|
||||
|
||||
# 3. 按提示完成配置
|
||||
# 安装器会自动配置Claude Code的hooks设置
|
||||
```
|
||||
|
||||
### 手动安装
|
||||
|
||||
```bash
|
||||
# 1. 测试通知功能
|
||||
node taskping.js test
|
||||
|
||||
# 2. 配置Claude Code
|
||||
# 将以下内容添加到 ~/.claude/settings.json 的 "hooks" 部分:
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node /path/to/TaskPing/taskping.js notify --type completed",
|
||||
"timeout": 5
|
||||
}]
|
||||
}],
|
||||
"SubagentStop": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node /path/to/TaskPing/taskping.js notify --type waiting",
|
||||
"timeout": 5
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎮 使用方法
|
||||
|
||||
### 基本使用
|
||||
|
||||
安装完成后,TaskPing会自动工作:
|
||||
|
||||
```bash
|
||||
# 1. 正常启动Claude Code
|
||||
claude
|
||||
|
||||
# 2. 执行任务
|
||||
> 请帮我重构这个项目的代码结构
|
||||
|
||||
# 3. 当Claude完成任务时,你会收到通知:
|
||||
# 📱 "任务已完成,Claude正在等待下一步指令"
|
||||
|
||||
# 4. 当Claude需要你的输入时,你会收到提醒:
|
||||
# 📱 "Claude需要您的进一步指导"
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```bash
|
||||
# 启动配置工具
|
||||
node taskping.js config
|
||||
|
||||
# 快速查看当前配置
|
||||
node taskping.js config --show
|
||||
|
||||
# 查看系统状态
|
||||
node taskping.js status
|
||||
|
||||
# 测试通知功能
|
||||
node taskping.js test
|
||||
|
||||
# 启动邮件命令中继服务 (NEW!)
|
||||
node taskping.js relay start
|
||||
```
|
||||
|
||||
## 🔧 配置选项
|
||||
|
||||
### 语言设置
|
||||
- `zh-CN`:简体中文
|
||||
- `en`:英语
|
||||
- `ja`:日语
|
||||
|
||||
### 提示音选择(macOS)
|
||||
- `Glass`:清脆玻璃音(推荐用于任务完成)
|
||||
- `Tink`:轻柔提示音(推荐用于等待输入)
|
||||
- `Ping`、`Pop`、`Basso` 等系统音效
|
||||
|
||||
### 基础配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "zh-CN",
|
||||
"sound": {
|
||||
"completed": "Glass",
|
||||
"waiting": "Tink"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 邮件配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"email": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"auth": {
|
||||
"user": "your-email@gmail.com",
|
||||
"pass": "your-app-password"
|
||||
}
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"secure": true
|
||||
},
|
||||
"from": "TaskPing <your-email@gmail.com>",
|
||||
"to": "your-email@gmail.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**📧 邮件功能设置指南**: 查看 [邮件功能详细指南](docs/EMAIL_GUIDE.md) 了解完整的配置和使用方法。
|
||||
|
||||
## 💡 实际应用场景
|
||||
|
||||
### 🏗️ 代码重构项目
|
||||
```
|
||||
你:请帮我重构这个React组件,提高性能
|
||||
Claude:开始分析组件结构...
|
||||
📱 通知:任务完成!
|
||||
Claude:我找到了3个优化方案,你倾向于哪种?
|
||||
📱 通知:Claude需要您的进一步指导
|
||||
```
|
||||
|
||||
### 📚 文档生成
|
||||
```
|
||||
你:为这个API生成完整的文档
|
||||
Claude:正在分析API接口...
|
||||
📱 通知:任务完成!
|
||||
Claude:文档已生成,需要我添加使用示例吗?
|
||||
📱 通知:Claude需要您的进一步指导
|
||||
```
|
||||
|
||||
### 🐛 Bug调试
|
||||
```
|
||||
你:帮我找出这个内存泄漏的原因
|
||||
Claude:开始深度分析代码...
|
||||
📱 通知:任务完成!
|
||||
Claude:发现了2个可能的原因,需要查看哪个文件?
|
||||
📱 通知:Claude需要您的进一步指导
|
||||
```
|
||||
|
||||
### 📧 远程邮件工作流程
|
||||
```
|
||||
1. 你在家里:启动 Claude Code 任务
|
||||
2. 你出门了:📧 收到邮件 "任务完成,Claude等待下一步指令"
|
||||
3. 在路上:回复邮件 "请继续优化代码性能"
|
||||
4. 自动执行:命令自动在你的电脑上执行
|
||||
5. 再次收到:📧 "优化完成" 邮件通知
|
||||
6. 继续回复:进行下一步操作
|
||||
|
||||
真正实现远程 AI 编程!🚀
|
||||
```
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### macOS权限问题
|
||||
如果收不到通知,请检查:
|
||||
1. 打开"系统偏好设置" → "安全性与隐私" → "隐私"
|
||||
2. 选择"通知",确保终端应用有权限
|
||||
3. 或者在"系统偏好设置" → "通知"中启用终端通知
|
||||
|
||||
### Linux依赖缺失
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libnotify-bin
|
||||
|
||||
# Fedora/RHEL
|
||||
sudo dnf install libnotify
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S libnotify
|
||||
```
|
||||
|
||||
### Windows执行策略
|
||||
```powershell
|
||||
# 如果遇到PowerShell执行策略限制
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
### 测试通知
|
||||
```bash
|
||||
# 测试所有通知渠道
|
||||
node taskping.js test
|
||||
|
||||
# 手动发送通知
|
||||
node taskping.js notify --type completed
|
||||
node taskping.js notify --type waiting
|
||||
|
||||
# 查看系统状态
|
||||
node taskping.js status
|
||||
```
|
||||
|
||||
## 📊 项目结构
|
||||
|
||||
```
|
||||
TaskPing/
|
||||
├── 📄 README.md # 项目文档
|
||||
├── 🚀 taskping.js # 主要CLI入口
|
||||
├── 📋 TaskPing.md # 产品规格文档
|
||||
├── 📦 package.json # 项目依赖
|
||||
├── config/ # 配置文件目录
|
||||
│ ├── defaults/ # 默认配置模板
|
||||
│ ├── user.json # 用户个人配置
|
||||
│ └── channels.json # 通知渠道配置
|
||||
├── src/ # 核心源代码
|
||||
│ ├── core/ # 核心模块
|
||||
│ │ ├── config.js # 配置管理器
|
||||
│ │ ├── logger.js # 日志系统
|
||||
│ │ └── notifier.js # 通知编排器
|
||||
│ ├── channels/ # 通知渠道实现
|
||||
│ │ ├── local/ # 本地通知
|
||||
│ │ ├── email/ # 邮件通知
|
||||
│ │ └── chat/ # 聊天应用通知
|
||||
│ ├── tools/ # 管理工具
|
||||
│ │ ├── installer.js # 安装器
|
||||
│ │ └── config-manager.js # 配置管理器
|
||||
│ └── assets/ # 静态资源
|
||||
└── docs/ # 文档目录
|
||||
```
|
||||
|
||||
## 🔮 发展规划
|
||||
|
||||
TaskPing按照产品规格文档分阶段开发:
|
||||
|
||||
### ✅ Phase 1 - 本地通知MVP(已完成)
|
||||
- 本地桌面通知
|
||||
- Claude Code hooks集成
|
||||
- 基础配置管理
|
||||
|
||||
### ✅ Phase 2 - 邮件通知和远程执行(已完成)
|
||||
- 📧 邮件通知功能
|
||||
- 🔄 邮件回复命令执行
|
||||
- 🔒 安全会话管理
|
||||
- 🛠️ 命令中继服务
|
||||
|
||||
### 🚧 Phase 3 - 多渠道通知(规划中)
|
||||
- Telegram/Discord/WhatsApp/飞书集成
|
||||
- 移动端推送通知
|
||||
- 多渠道命令中继
|
||||
|
||||
### 🌟 Phase 4 - 企业级功能(未来)
|
||||
- 团队协作功能
|
||||
- 用户权限管理
|
||||
- 审计日志
|
||||
- API 接口
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎参与TaskPing的开发!
|
||||
|
||||
### 如何贡献
|
||||
1. Fork本项目
|
||||
2. 创建功能分支:`git checkout -b feature/new-feature`
|
||||
3. 提交更改:`git commit -am 'Add new feature'`
|
||||
4. 推送分支:`git push origin feature/new-feature`
|
||||
5. 提交Pull Request
|
||||
|
||||
### 开发环境
|
||||
- Node.js >= 14.0.0
|
||||
- 支持macOS、Linux、Windows开发
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT License](LICENSE) 开源协议。
|
||||
|
||||
## 💬 联系我们
|
||||
|
||||
- 🐛 **问题反馈**:[提交Issue](https://github.com/TaskPing/TaskPing/issues)
|
||||
- 💡 **功能建议**:[Discussion](https://github.com/TaskPing/TaskPing/discussions)
|
||||
- 📧 **邮件联系**:contact@taskping.dev
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**让Claude Code工作流程更加智能高效!**
|
||||
|
||||
⭐ 如果这个项目对你有帮助,请给我们一个Star!
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
# Claude Code Notify Assistant – Product Specification
|
||||
|
||||
## 1 Purpose & Vision
|
||||
|
||||
Provide developers with an **event‑driven companion** for Claude‑powered CLI sessions. When a long‑running task finishes, the assistant instantly notifies the user on their preferred channel and lets them **reply from mobile to trigger the next command**, achieving a seamless *desktop↔mobile↔AI* loop.
|
||||
|
||||
---
|
||||
|
||||
## 2 Problem Statement
|
||||
|
||||
CLI workflows often involve scripts that run for minutes or hours. Users must poll the terminal or stay near the computer, wasting time and focus. Existing notification tools are channel‑specific or lack bi‑directional control, and none are optimised for Claude Code agents.
|
||||
|
||||
---
|
||||
|
||||
## 3 Solution Overview
|
||||
|
||||
1. **CLI Hook**: A tiny cross‑platform wrapper (`claude-notify run <cmd>`) that executes any shell command, streams logs to the backend, and raises a `TaskFinished` event when exit‑code ≠ “running”.
|
||||
2. **Notification Orchestrator (backend)**: Consumes events, applies user quota/business rules, and fan‑outs messages via pluggable channel adapters.
|
||||
3. **Interactive Relay**: Converts user replies (e.g., from Telegram) into authenticated API calls that queue the next CLI command on the origin machine through a persistent WebSocket tunnel.
|
||||
4. **Usage Metering**: Tracks daily quota (3 e‑mails for free tier) and subscription status (Stripe webhook).
|
||||
|
||||
---
|
||||
|
||||
## 4 Personas & User Journeys
|
||||
|
||||
| Persona | Primary Goal | Typical Flow |
|
||||
| -------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Indie Dev – Free** | Compile & test large project remotely | 1) `claude-notify run make` → 2) E‑mail sent on finish → 3) Next morning sees mail; quota resets |
|
||||
| **Startup Engineer – Pro** | Continuous fine‑tuning jobs on GPU VM | 1) Task completes → 2) Telegram alert with success log snippet → 3) Replies `launch next.sh` from subway → 4) Claude Code executes instantly |
|
||||
| **Ops Lead – Pro** | Batch data pipelines | 1) Weekly cron triggers job → 2) Feishu group ping with status + link to dashboard |
|
||||
|
||||
---
|
||||
|
||||
## 5 Feature Matrix
|
||||
|
||||
| Area | Free | Pro (USD 4.9/mo) |
|
||||
| ------------------------ | ----------- | ------------------------------------------------ |
|
||||
| Daily notification quota | 3 | Unlimited |
|
||||
| Channels | E‑mail | E‑mail, Discord, Telegram, SMS, Feishu, Webhooks |
|
||||
| Command Relay | — | ✅ |
|
||||
| Notification Templates | Default | Custom markdown & rich‑link cards |
|
||||
| Log Attachment | 10 KB tail | Full log (configurable) |
|
||||
| Team Workspaces | — | Up to 5 members |
|
||||
| SLA | Best effort | 99.9 % |
|
||||
|
||||
---
|
||||
|
||||
## 6 System Architecture
|
||||
|
||||
```
|
||||
+-----------+ HTTPS / WSS +-----------------+
|
||||
| CLI Agent | <──────────▶ API Gateway ───────────▶ | Auth Service |
|
||||
| (Go/Rust)| +-----------------+
|
||||
| |── stream logs ─▶ Event Queue (NATS) ──▶ Task Processor
|
||||
+-----------+ │(Python Worker)
|
||||
▲ │ │
|
||||
│ SSH/WebSocket │ │
|
||||
│ ▼ ▼
|
||||
│ +-----------------+ +-----------------+
|
||||
│ | Notification | | Usage / Billing|
|
||||
└── receive command ◀── Relay ◀──────┤ Fan‑out | | (Postgres + |
|
||||
+-----------------+ | Stripe) |
|
||||
```
|
||||
|
||||
**Tech choices**
|
||||
|
||||
- **CLI Agent**: Rust binary, \~3 MB, single‑file, auto‑updates (GitHub Releases).
|
||||
- **Backend**: FastAPI + Celery, Redis broker, Postgres store.
|
||||
- **Channel Adapters**: Modular; each implements `send(message: Notification) -> DeliveryResult`.
|
||||
- **Security**: JWT (CLI) + signed HMAC Webhook secrets; all data in‑flight TLS 1.2+.
|
||||
- **Scaling**: Horizontal via Kubernetes; stateless workers.
|
||||
|
||||
---
|
||||
|
||||
## 7 API Contracts (simplified)
|
||||
|
||||
```http
|
||||
POST /v1/tasks
|
||||
{
|
||||
"cmd": "python train.py",
|
||||
"session_id": "abc",
|
||||
"notify_on": "exit", // or "partial", "custom_regex"
|
||||
"channels": ["email", "tg"]
|
||||
}
|
||||
→ 201 Created {"task_id":"t123"}
|
||||
|
||||
POST /v1/commands
|
||||
Authorization: Bearer <mobile_reply_token>
|
||||
{
|
||||
"session_id": "abc",
|
||||
"command": "git pull && make test"
|
||||
}
|
||||
→ 202 Accepted
|
||||
```
|
||||
|
||||
*Complete OpenAPI spec in appendix.*
|
||||
|
||||
---
|
||||
|
||||
## 8 Data Model (Postgres)
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id UUID PK, email TEXT UNIQUE, plan TEXT, quota_used INT, …
|
||||
);
|
||||
CREATE TABLE tasks (
|
||||
id UUID PK, user_id FK, session_id TEXT, cmd TEXT, status TEXT,
|
||||
started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, log_url TEXT, …
|
||||
);
|
||||
CREATE TABLE notifications (
|
||||
id UUID PK, task_id FK, channel TEXT, delivered BOOL, …
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9 Quota & Pricing Logic
|
||||
|
||||
```text
|
||||
If plan == "free":
|
||||
limit daily_sent_email <= 3
|
||||
reject other channel
|
||||
Else if plan == "pro":
|
||||
no channel limit
|
||||
fair‑use: 30 SMS / day default
|
||||
```
|
||||
|
||||
Billing via **Stripe Billing Portal**; pro‑rated upgrades.
|
||||
|
||||
---
|
||||
|
||||
## 10 On‑boarding Flow
|
||||
|
||||
1. OAuth (GitHub) or E‑mail link signup.
|
||||
2. Download CLI (`curl -sL https://cli.claude‑notify.sh | bash`).
|
||||
3. `claude-notify login <token>` – stores token locally.
|
||||
4. Configure channels in Web UI (Discord bot auth, etc.).
|
||||
5. First task run offers interactive tour.
|
||||
|
||||
---
|
||||
|
||||
## 11 Operational & Security Considerations
|
||||
|
||||
- **Secrets**: Only hash task logs > 10 MB for storage; encrypt full logs (AES‑256‑GCM) at rest.
|
||||
- **Abuse prevention**: Per‑minute rate limits on incoming mobile commands; banlist keywords.
|
||||
- **Observability**: Prometheus metrics, Grafana dashboard; Sentry for exceptions.
|
||||
- **Compliance**: GDPR + China PIPL data residency via multi‑region storage.
|
||||
|
||||
---
|
||||
|
||||
## 11a Implementation Phases
|
||||
|
||||
### Phase 1 – Local Desktop Notification MVP
|
||||
|
||||
- **Scope**: Notify on local machine only.
|
||||
- **Trigger**: CLI Agent detects task exit and calls OS‑native notification helper.
|
||||
- **OS Support**:
|
||||
- macOS: `terminal-notifier` or AppleScript.
|
||||
- Windows: Toast via `SnoreToast`.
|
||||
- Linux: `libnotify`.
|
||||
- **Customization**: `--title`, `--message`, `--sound=/path/to/file` flags; sensible defaults provided.
|
||||
- **Offline & Privacy**: No network calls; runs entirely locally.
|
||||
- **Quota**: Unlimited (cloud quota applies in later phases).
|
||||
- **Future‑Proofing**: Uses the same `TaskFinished` event schema as the cloud orchestrator, enabling a smooth upgrade path.
|
||||
|
||||
### Phase 2 – Cloud Notification Service (E‑mail, Telegram, Discord)
|
||||
|
||||
- Add backend Notification Orchestrator, channel adapters, and basic auth.
|
||||
- Implement daily quota enforcement and Pro billing (Stripe).
|
||||
|
||||
### Phase 3 – Mobile Command Relay & Team Features
|
||||
|
||||
- Enable interactive replies that queue new commands via WebSocket tunnel.
|
||||
- Introduce Workspaces, role permissions, and activity audit logs.
|
||||
|
||||
---
|
||||
|
||||
## 12 Roadmap
|
||||
|
||||
| Quarter | Milestone |
|
||||
| ------- | ---------------------------------------------------------- |
|
||||
| Q3‑25 | MVP (E‑mail + Telegram) / Invite‑only beta |
|
||||
| Q4‑25 | Payments, Discord & Feishu adapters / Public launch |
|
||||
| Q1‑26 | Mobile app (Flutter) push notifications / Team workspaces |
|
||||
| Q2‑26 | Marketplace for community adapters (e.g., Slack, WhatsApp) |
|
||||
|
||||
---
|
||||
|
||||
## 13 Open Questions
|
||||
|
||||
1. Support for streaming partial logs?
|
||||
2. Granular role permissions for team plans.
|
||||
3. Enterprise SSO & on‑prem backend – worth pursuing?
|
||||
4. Automatic Claude Code context carry‑over between sequential commands.
|
||||
|
||||
---
|
||||
|
||||
*© 2025 Panda Villa Tech Limited – Internal draft – v0.9*
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"desktop": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"config": {}
|
||||
},
|
||||
"email": {
|
||||
"type": "email",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"smtp": {
|
||||
"host": "smtp.feishu.cn",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"auth": {
|
||||
"user": "panda@pandalla.ai",
|
||||
"pass": "1fIvPAo0lagVG9gS"
|
||||
}
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.feishu.cn",
|
||||
"port": 993,
|
||||
"secure": true,
|
||||
"auth": {
|
||||
"user": "panda@pandalla.ai",
|
||||
"pass": "1fIvPAo0lagVG9gS"
|
||||
}
|
||||
},
|
||||
"from": "TaskPing <panda@pandalla.ai>",
|
||||
"to": "jiaxicui446@gmail.com",
|
||||
"template": {
|
||||
"checkInterval": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"type": "chat",
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"webhook": "",
|
||||
"username": "TaskPing",
|
||||
"avatar": null
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"type": "chat",
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"token": "",
|
||||
"chatId": ""
|
||||
}
|
||||
},
|
||||
"whatsapp": {
|
||||
"type": "chat",
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"webhook": "",
|
||||
"apiKey": ""
|
||||
}
|
||||
},
|
||||
"feishu": {
|
||||
"type": "chat",
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"webhook": "",
|
||||
"secret": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"language": "zh-CN",
|
||||
"sound": {
|
||||
"completed": "Glass",
|
||||
"waiting": "Tink"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 5,
|
||||
"customMessages": {
|
||||
"completed": null,
|
||||
"waiting": null
|
||||
},
|
||||
"channels": {
|
||||
"desktop": {
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"relay": {
|
||||
"enabled": false,
|
||||
"port": 3000,
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"token": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node {TASKPING_ROOT}/taskping.js notify --type completed",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node {TASKPING_ROOT}/taskping.js notify --type waiting",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"language": "zh-CN",
|
||||
"sound": {
|
||||
"completed": "Submarine",
|
||||
"waiting": "Hero"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 5,
|
||||
"customMessages": {
|
||||
"completed": null,
|
||||
"waiting": null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"zh-CN": {
|
||||
"completed": {
|
||||
"title": "Claude Code - 任务完成",
|
||||
"message": "[{project}] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"waiting": {
|
||||
"title": "Claude Code - 等待输入",
|
||||
"message": "[{project}] Claude需要您的进一步指导"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"completed": {
|
||||
"title": "Claude Code - Task Completed",
|
||||
"message": "[{project}] Task completed, Claude is waiting for next instruction"
|
||||
},
|
||||
"waiting": {
|
||||
"title": "Claude Code - Waiting for Input",
|
||||
"message": "[{project}] Claude needs your further guidance"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"completed": {
|
||||
"title": "Claude Code - タスク完了",
|
||||
"message": "[{project}] タスクが完了しました。Claudeが次の指示を待っています"
|
||||
},
|
||||
"waiting": {
|
||||
"title": "Claude Code - 入力待ち",
|
||||
"message": "[{project}] Claudeにはあなたのさらなるガイダンスが必要です"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"email": {
|
||||
"type": "email",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"auth": {
|
||||
"user": "your-email@gmail.com",
|
||||
"pass": "your-app-password"
|
||||
}
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"secure": true,
|
||||
"auth": {
|
||||
"user": "your-email@gmail.com",
|
||||
"pass": "your-app-password"
|
||||
}
|
||||
},
|
||||
"from": "TaskPing <your-email@gmail.com>",
|
||||
"to": "your-email@gmail.com",
|
||||
"template": {
|
||||
"checkInterval": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 邮件配置说明
|
||||
#
|
||||
# Gmail 配置示例:
|
||||
# 1. 启用两步验证
|
||||
# 2. 生成应用密码(16位)
|
||||
# 3. 替换上面的 your-email@gmail.com 和 your-app-password
|
||||
#
|
||||
# 其他邮箱提供商:
|
||||
# QQ邮箱: smtp.qq.com (587) / imap.qq.com (993)
|
||||
# 163邮箱: smtp.163.com (587) / imap.163.com (993)
|
||||
# Outlook: smtp.live.com (587) / imap-mail.outlook.com (993)
|
||||
#
|
||||
# 配置完成后:
|
||||
# 1. 复制email部分到 config/channels.json
|
||||
# 2. 运行: taskping test
|
||||
# 3. 运行: taskping relay start
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"language": "zh-CN",
|
||||
"sound": {
|
||||
"completed": "Submarine",
|
||||
"waiting": "Hero"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 5,
|
||||
"customMessages": {
|
||||
"completed": null,
|
||||
"waiting": null
|
||||
},
|
||||
"channels": {
|
||||
"desktop": {
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"relay": {
|
||||
"enabled": false,
|
||||
"port": 3000,
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"token": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,494 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing 邮件调试工具
|
||||
* 用于调试邮件监听和自动化问题
|
||||
*/
|
||||
|
||||
const Imap = require('node-imap');
|
||||
const { simpleParser } = require('mailparser');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class DebugEmailAutomation {
|
||||
constructor() {
|
||||
this.configPath = path.join(__dirname, 'config/channels.json');
|
||||
this.config = null;
|
||||
this.imap = null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log('🔍 TaskPing 邮件调试工具启动\n');
|
||||
|
||||
// 1. 检查配置
|
||||
console.log('📋 1. 检查配置文件...');
|
||||
if (!this.loadConfig()) {
|
||||
return;
|
||||
}
|
||||
console.log('✅ 配置文件加载成功');
|
||||
console.log(`📧 IMAP服务器: ${this.config.imap.host}:${this.config.imap.port}`);
|
||||
console.log(`👤 用户: ${this.config.imap.auth.user}`);
|
||||
console.log(`📬 通知发送到: ${this.config.to}\n`);
|
||||
|
||||
// 2. 测试IMAP连接
|
||||
console.log('🔌 2. 测试IMAP连接...');
|
||||
try {
|
||||
await this.testConnection();
|
||||
console.log('✅ IMAP连接成功\n');
|
||||
} catch (error) {
|
||||
console.error('❌ IMAP连接失败:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 检查最近邮件
|
||||
console.log('📧 3. 检查最近邮件...');
|
||||
try {
|
||||
await this.checkRecentEmails();
|
||||
} catch (error) {
|
||||
console.error('❌ 检查邮件失败:', error.message);
|
||||
}
|
||||
|
||||
// 4. 开始实时监听
|
||||
console.log('\n👂 4. 开始实时监听邮件回复...');
|
||||
console.log('💌 现在可以回复TaskPing邮件来测试自动化功能');
|
||||
console.log('🔍 调试信息会实时显示\n');
|
||||
|
||||
this.startRealTimeListening();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
try {
|
||||
const data = fs.readFileSync(this.configPath, 'utf8');
|
||||
const config = JSON.parse(data);
|
||||
|
||||
if (!config.email?.enabled) {
|
||||
console.error('❌ 邮件功能未启用');
|
||||
console.log('💡 请运行: npm run config');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.config = config.email.config;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 配置文件读取失败:', error.message);
|
||||
console.log('💡 请运行: npm run config');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
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: 30000,
|
||||
authTimeout: 15000,
|
||||
// debug: console.log // 暂时禁用调试
|
||||
});
|
||||
|
||||
this.imap.once('ready', () => {
|
||||
console.log('🔗 IMAP ready事件触发');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.imap.once('error', (error) => {
|
||||
console.error('🔗 IMAP error事件:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.imap.connect();
|
||||
});
|
||||
}
|
||||
|
||||
async checkRecentEmails() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.imap.openBox('INBOX', true, (err, box) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📫 收件箱状态: 总邮件 ${box.messages.total}, 未读 ${box.messages.unseen}`);
|
||||
|
||||
// 查找最近1小时的所有邮件
|
||||
const since = new Date();
|
||||
since.setHours(since.getHours() - 1);
|
||||
|
||||
console.log(`🔍 搜索 ${since.toLocaleString()} 之后的邮件...`);
|
||||
|
||||
this.imap.search([['SINCE', since]], (searchErr, results) => {
|
||||
if (searchErr) {
|
||||
reject(searchErr);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📨 找到 ${results.length} 封最近邮件`);
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log('ℹ️ 没有找到最近的邮件');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最近几封邮件的详情
|
||||
const fetch = this.imap.fetch(results.slice(-3), {
|
||||
bodies: 'HEADER',
|
||||
struct: true
|
||||
});
|
||||
|
||||
fetch.on('message', (msg, seqno) => {
|
||||
console.log(`\n📧 邮件 ${seqno}:`);
|
||||
|
||||
msg.on('body', (stream, info) => {
|
||||
let buffer = '';
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
});
|
||||
stream.once('end', () => {
|
||||
const lines = buffer.split('\n');
|
||||
const subject = lines.find(line => line.startsWith('Subject:'));
|
||||
const from = lines.find(line => line.startsWith('From:'));
|
||||
const date = lines.find(line => line.startsWith('Date:'));
|
||||
|
||||
console.log(` 📄 ${subject || 'Subject: (未知)'}`);
|
||||
console.log(` 👤 ${from || 'From: (未知)'}`);
|
||||
console.log(` 📅 ${date || 'Date: (未知)'}`);
|
||||
|
||||
if (subject && subject.includes('[TaskPing]')) {
|
||||
console.log(' 🎯 这是TaskPing邮件!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fetch.once('end', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
fetch.once('error', (fetchErr) => {
|
||||
reject(fetchErr);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startRealTimeListening() {
|
||||
// 重新连接用于监听
|
||||
this.imap.end();
|
||||
|
||||
setTimeout(() => {
|
||||
this.connectForListening();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async connectForListening() {
|
||||
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('✅ 监听连接建立');
|
||||
this.openInboxForListening();
|
||||
});
|
||||
|
||||
this.imap.once('error', (error) => {
|
||||
console.error('❌ 监听连接错误:', error.message);
|
||||
});
|
||||
|
||||
this.imap.once('end', () => {
|
||||
console.log('🔄 连接断开,尝试重连...');
|
||||
setTimeout(() => this.connectForListening(), 5000);
|
||||
});
|
||||
|
||||
this.imap.connect();
|
||||
}
|
||||
|
||||
openInboxForListening() {
|
||||
this.imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
console.error('❌ 打开收件箱失败:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📬 收件箱已打开,开始监听新邮件...');
|
||||
|
||||
// 设置定期检查
|
||||
setInterval(() => {
|
||||
this.checkNewEmails();
|
||||
}, 10000); // 每10秒检查一次
|
||||
|
||||
// 立即检查一次
|
||||
this.checkNewEmails();
|
||||
});
|
||||
}
|
||||
|
||||
checkNewEmails() {
|
||||
const since = new Date();
|
||||
since.setMinutes(since.getMinutes() - 5); // 检查最近5分钟的邮件
|
||||
|
||||
this.imap.search([['UNSEEN'], ['SINCE', since]], (err, results) => {
|
||||
if (err) {
|
||||
console.error('🔍 搜索新邮件失败:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log(`\n🚨 发现 ${results.length} 封新邮件!`);
|
||||
this.processNewEmails(results);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
processNewEmails(emailUids) {
|
||||
const fetch = this.imap.fetch(emailUids, {
|
||||
bodies: '',
|
||||
markSeen: true
|
||||
});
|
||||
|
||||
fetch.on('message', (msg, seqno) => {
|
||||
console.log(`\n📨 处理新邮件 ${seqno}:`);
|
||||
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.analyzeEmail(parsed, seqno);
|
||||
} catch (error) {
|
||||
console.error(`❌ 解析邮件 ${seqno} 失败:`, error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async analyzeEmail(email, seqno) {
|
||||
console.log(`📧 邮件 ${seqno} 分析:`);
|
||||
console.log(` 📄 主题: ${email.subject || '(无主题)'}`);
|
||||
console.log(` 👤 发件人: ${email.from?.text || '(未知)'}`);
|
||||
console.log(` 📅 时间: ${email.date || '(未知)'}`);
|
||||
|
||||
// 检查是否是TaskPing回复
|
||||
const isTaskPingReply = this.isTaskPingReply(email);
|
||||
console.log(` 🎯 TaskPing回复: ${isTaskPingReply ? '是' : '否'}`);
|
||||
|
||||
if (!isTaskPingReply) {
|
||||
console.log(` ⏭️ 跳过非TaskPing邮件`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取命令
|
||||
const command = this.extractCommand(email);
|
||||
console.log(` 💬 邮件内容长度: ${(email.text || '').length} 字符`);
|
||||
console.log(` 🎯 提取的命令: "${command || '(无)'}"`);
|
||||
|
||||
if (!command || command.trim().length === 0) {
|
||||
console.log(` ⚠️ 未找到有效命令`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n🚀 准备执行命令...`);
|
||||
await this.executeCommand(command, seqno);
|
||||
}
|
||||
|
||||
isTaskPingReply(email) {
|
||||
const subject = email.subject || '';
|
||||
return subject.includes('[TaskPing]') ||
|
||||
subject.match(/^(Re:|RE:|回复:)/i);
|
||||
}
|
||||
|
||||
extractCommand(email) {
|
||||
let text = email.text || '';
|
||||
console.log(` 📝 原始邮件文本:\n${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
|
||||
|
||||
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:')) {
|
||||
console.log(` ✂️ 在此行停止解析: ${line.substring(0, 50)}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.includes('--') ||
|
||||
line.includes('Sent from') ||
|
||||
line.includes('发自我的')) {
|
||||
console.log(` ✂️ 跳过签名行: ${line.substring(0, 50)}`);
|
||||
break;
|
||||
}
|
||||
|
||||
commandLines.push(line);
|
||||
}
|
||||
|
||||
const extractedCommand = commandLines.join('\n').trim();
|
||||
console.log(` 🎯 清理后的命令:\n"${extractedCommand}"`);
|
||||
return extractedCommand;
|
||||
}
|
||||
|
||||
async executeCommand(command, seqno) {
|
||||
console.log(`🤖 执行命令 (来自邮件 ${seqno}):`);
|
||||
console.log(`📝 命令内容: "${command}"`);
|
||||
|
||||
try {
|
||||
// 1. 复制到剪贴板
|
||||
console.log(`📋 1. 复制到剪贴板...`);
|
||||
const clipboardSuccess = await this.copyToClipboard(command);
|
||||
console.log(`📋 剪贴板: ${clipboardSuccess ? '✅ 成功' : '❌ 失败'}`);
|
||||
|
||||
// 2. 尝试自动化
|
||||
console.log(`🤖 2. 尝试自动输入...`);
|
||||
const automationSuccess = await this.attemptAutomation(command);
|
||||
console.log(`🤖 自动化: ${automationSuccess ? '✅ 成功' : '❌ 失败'}`);
|
||||
|
||||
// 3. 发送通知
|
||||
console.log(`🔔 3. 发送通知...`);
|
||||
const notificationSuccess = await this.sendNotification(command);
|
||||
console.log(`🔔 通知: ${notificationSuccess ? '✅ 成功' : '❌ 失败'}`);
|
||||
|
||||
if (automationSuccess) {
|
||||
console.log(`\n🎉 邮件命令已自动执行到Claude Code!`);
|
||||
} else {
|
||||
console.log(`\n⚠️ 自动化失败,但命令已复制到剪贴板`);
|
||||
console.log(`💡 请手动在Claude Code中粘贴 (Cmd+V)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 执行命令失败:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async copyToClipboard(command) {
|
||||
return new Promise((resolve) => {
|
||||
const pbcopy = spawn('pbcopy');
|
||||
pbcopy.stdin.write(command);
|
||||
pbcopy.stdin.end();
|
||||
|
||||
pbcopy.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
pbcopy.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
async attemptAutomation(command) {
|
||||
return new Promise((resolve) => {
|
||||
const escapedCommand = command
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/'/g, "\\'");
|
||||
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
set claudeApps to {"Claude", "Claude Code", "Claude Desktop"}
|
||||
set devApps to {"Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code"}
|
||||
set targetApp to null
|
||||
set appName to ""
|
||||
|
||||
-- 查找Claude应用
|
||||
repeat with app in claudeApps
|
||||
try
|
||||
if application process app exists then
|
||||
set targetApp to application process app
|
||||
set appName to app
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
-- 如果没找到Claude,查找开发工具
|
||||
if targetApp is null then
|
||||
repeat with app in devApps
|
||||
try
|
||||
if application process app exists then
|
||||
set targetApp to application process app
|
||||
set appName to app
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if targetApp is not null then
|
||||
set frontmost of targetApp to true
|
||||
delay 1
|
||||
|
||||
keystroke "a" using command down
|
||||
delay 0.3
|
||||
keystroke "${escapedCommand}"
|
||||
delay 0.5
|
||||
keystroke return
|
||||
|
||||
return "success:" & appName
|
||||
else
|
||||
return "no_app"
|
||||
end if
|
||||
end tell
|
||||
`;
|
||||
|
||||
console.log(`🍎 执行AppleScript自动化...`);
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
console.log(`🍎 AppleScript结果: 退出码=${code}, 输出="${output}"`);
|
||||
const success = code === 0 && output.startsWith('success:');
|
||||
if (success) {
|
||||
const appName = output.split(':')[1];
|
||||
console.log(`🍎 成功输入到应用: ${appName}`);
|
||||
}
|
||||
resolve(success);
|
||||
});
|
||||
|
||||
osascript.on('error', (error) => {
|
||||
console.log(`🍎 AppleScript错误: ${error.message}`);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async sendNotification(command) {
|
||||
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
||||
|
||||
const script = `
|
||||
display notification "邮件命令: ${shortCommand.replace(/"/g, '\\"')}" with title "TaskPing Debug" sound name "default"
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
osascript.on('close', (code) => resolve(code === 0));
|
||||
osascript.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 启动调试工具
|
||||
const debugTool = new DebugEmailAutomation();
|
||||
debugTool.start().catch(console.error);
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 自动化功能诊断工具
|
||||
* 详细检测和诊断自动粘贴功能的问题
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
class AutomationDiagnostic {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
}
|
||||
|
||||
async runDiagnostic() {
|
||||
console.log('🔍 自动化功能诊断工具\n');
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
console.log('❌ 此诊断工具仅适用于 macOS');
|
||||
return;
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
await this.testBasicAppleScript();
|
||||
await this.testSystemEventsAccess();
|
||||
await this.testKeystrokePermission();
|
||||
await this.testApplicationDetection();
|
||||
await this.testClipboardAccess();
|
||||
await this.testDetailedPermissions();
|
||||
|
||||
// 显示总结
|
||||
this.showSummary();
|
||||
await this.provideSolutions();
|
||||
}
|
||||
|
||||
async testBasicAppleScript() {
|
||||
console.log('1. 📋 测试基本 AppleScript 执行...');
|
||||
try {
|
||||
const result = await this.runAppleScript('return "AppleScript works"');
|
||||
if (result === 'AppleScript works') {
|
||||
console.log(' ✅ 基本 AppleScript 执行正常');
|
||||
this.tests.push({ name: 'AppleScript', status: 'pass' });
|
||||
} else {
|
||||
console.log(' ❌ AppleScript 返回异常结果');
|
||||
this.tests.push({ name: 'AppleScript', status: 'fail', error: 'Unexpected result' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ AppleScript 执行失败:', error.message);
|
||||
this.tests.push({ name: 'AppleScript', status: 'fail', error: error.message });
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async testSystemEventsAccess() {
|
||||
console.log('2. 🖥️ 测试 System Events 访问...');
|
||||
try {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
return name of first application process whose frontmost is true
|
||||
end tell
|
||||
`;
|
||||
const result = await this.runAppleScript(script);
|
||||
if (result && result !== 'permission_denied') {
|
||||
console.log(` ✅ 可以访问 System Events,当前前台应用: ${result}`);
|
||||
this.tests.push({ name: 'System Events', status: 'pass', data: result });
|
||||
} else {
|
||||
console.log(' ❌ System Events 访问被拒绝');
|
||||
this.tests.push({ name: 'System Events', status: 'fail', error: 'Access denied' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ System Events 访问失败:', error.message);
|
||||
this.tests.push({ name: 'System Events', status: 'fail', error: error.message });
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async testKeystrokePermission() {
|
||||
console.log('3. ⌨️ 测试按键发送权限...');
|
||||
try {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
try
|
||||
keystroke "test" without key down
|
||||
return "keystroke_success"
|
||||
on error errorMessage
|
||||
return "keystroke_failed: " & errorMessage
|
||||
end try
|
||||
end tell
|
||||
`;
|
||||
const result = await this.runAppleScript(script);
|
||||
if (result === 'keystroke_success') {
|
||||
console.log(' ✅ 按键发送权限正常');
|
||||
this.tests.push({ name: 'Keystroke', status: 'pass' });
|
||||
} else {
|
||||
console.log(` ❌ 按键发送失败: ${result}`);
|
||||
this.tests.push({ name: 'Keystroke', status: 'fail', error: result });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ 按键测试异常:', error.message);
|
||||
this.tests.push({ name: 'Keystroke', status: 'fail', error: error.message });
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async testApplicationDetection() {
|
||||
console.log('4. 🔍 测试应用程序检测...');
|
||||
try {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
set appList to {"Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor", "Claude Code"}
|
||||
set foundApps to {}
|
||||
repeat with appName in appList
|
||||
try
|
||||
if application process appName exists then
|
||||
set foundApps to foundApps & {appName}
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
return foundApps as string
|
||||
end tell
|
||||
`;
|
||||
const result = await this.runAppleScript(script);
|
||||
if (result) {
|
||||
console.log(` ✅ 检测到以下应用: ${result}`);
|
||||
this.tests.push({ name: 'App Detection', status: 'pass', data: result });
|
||||
} else {
|
||||
console.log(' ⚠️ 未检测到目标应用程序');
|
||||
this.tests.push({ name: 'App Detection', status: 'warn', error: 'No target apps found' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ 应用检测失败:', error.message);
|
||||
this.tests.push({ name: 'App Detection', status: 'fail', error: error.message });
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async testClipboardAccess() {
|
||||
console.log('5. 📋 测试剪贴板访问...');
|
||||
try {
|
||||
// 测试写入剪贴板
|
||||
await this.setClipboard('test_clipboard_content');
|
||||
const content = await this.getClipboard();
|
||||
|
||||
if (content.includes('test_clipboard_content')) {
|
||||
console.log(' ✅ 剪贴板读写正常');
|
||||
this.tests.push({ name: 'Clipboard', status: 'pass' });
|
||||
} else {
|
||||
console.log(' ❌ 剪贴板内容不匹配');
|
||||
this.tests.push({ name: 'Clipboard', status: 'fail', error: 'Content mismatch' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ 剪贴板访问失败:', error.message);
|
||||
this.tests.push({ name: 'Clipboard', status: 'fail', error: error.message });
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async testDetailedPermissions() {
|
||||
console.log('6. 🔐 详细权限检查...');
|
||||
|
||||
try {
|
||||
// 检查当前运行的进程
|
||||
const whoami = await this.runCommand('whoami');
|
||||
console.log(` 👤 当前用户: ${whoami}`);
|
||||
|
||||
// 检查终端应用
|
||||
const terminal = process.env.TERM_PROGRAM || 'Unknown';
|
||||
console.log(` 💻 终端程序: ${terminal}`);
|
||||
|
||||
// 检查是否在 IDE 中运行
|
||||
const isInIDE = process.env.VSCODE_PID || process.env.CURSOR_SESSION_ID || process.env.JB_IDE_PID;
|
||||
if (isInIDE) {
|
||||
console.log(' 🔧 检测到在 IDE 中运行');
|
||||
}
|
||||
|
||||
// 尝试获取更详细的权限信息
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
try
|
||||
set frontApp to name of first application process whose frontmost is true
|
||||
set allApps to name of every application process
|
||||
return "Front: " & frontApp & ", All: " & (count of allApps)
|
||||
on error errorMsg
|
||||
return "Error: " & errorMsg
|
||||
end try
|
||||
end tell
|
||||
`;
|
||||
|
||||
const permResult = await this.runAppleScript(script);
|
||||
console.log(` 📊 权限测试结果: ${permResult}`);
|
||||
|
||||
this.tests.push({
|
||||
name: 'Detailed Permissions',
|
||||
status: 'info',
|
||||
data: { user: whoami, terminal, result: permResult }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(' ❌ 详细检查失败:', error.message);
|
||||
this.tests.push({ name: 'Detailed Permissions', status: 'fail', error: error.message });
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
showSummary() {
|
||||
console.log('📊 诊断结果总结:\n');
|
||||
|
||||
const passed = this.tests.filter(t => t.status === 'pass').length;
|
||||
const failed = this.tests.filter(t => t.status === 'fail').length;
|
||||
const warned = this.tests.filter(t => t.status === 'warn').length;
|
||||
|
||||
this.tests.forEach(test => {
|
||||
const icon = test.status === 'pass' ? '✅' :
|
||||
test.status === 'fail' ? '❌' :
|
||||
test.status === 'warn' ? '⚠️' : 'ℹ️';
|
||||
console.log(`${icon} ${test.name}`);
|
||||
if (test.error) {
|
||||
console.log(` 错误: ${test.error}`);
|
||||
}
|
||||
if (test.data) {
|
||||
console.log(` 数据: ${test.data}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n📈 总计: ${passed} 通过, ${failed} 失败, ${warned} 警告\n`);
|
||||
}
|
||||
|
||||
async provideSolutions() {
|
||||
const keystrokeTest = this.tests.find(t => t.name === 'Keystroke');
|
||||
const systemEventsTest = this.tests.find(t => t.name === 'System Events');
|
||||
|
||||
console.log('💡 解决方案建议:\n');
|
||||
|
||||
if (keystrokeTest && keystrokeTest.status === 'fail') {
|
||||
console.log('🔧 按键发送问题解决方案:');
|
||||
console.log(' 1. 打开 系统偏好设置 > 安全性与隐私 > 隐私 > 辅助功能');
|
||||
console.log(' 2. 移除并重新添加你的终端应用 (Terminal/iTerm2/VS Code)');
|
||||
console.log(' 3. 确保勾选框已被选中');
|
||||
console.log(' 4. 重启终端应用');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (systemEventsTest && systemEventsTest.status === 'fail') {
|
||||
console.log('🔧 System Events 访问问题解决方案:');
|
||||
console.log(' 1. 检查 系统偏好设置 > 安全性与隐私 > 隐私 > 自动化');
|
||||
console.log(' 2. 确保你的终端应用下勾选了 "System Events"');
|
||||
console.log(' 3. 如果没有看到你的应用,先运行一次自动化脚本触发权限请求');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('🚀 额外建议:');
|
||||
console.log(' • 尝试完全退出并重启终端应用');
|
||||
console.log(' • 在 Terminal 中运行而不是在 IDE 集成终端中');
|
||||
console.log(' • 检查是否有安全软件阻止自动化');
|
||||
console.log(' • 尝试在不同的终端应用中运行测试');
|
||||
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const answer = await this.question(rl, '\n是否尝试一个简单的修复测试?(y/n): ');
|
||||
|
||||
if (answer.toLowerCase() === 'y') {
|
||||
await this.runSimpleFixTest();
|
||||
}
|
||||
|
||||
rl.close();
|
||||
}
|
||||
|
||||
async runSimpleFixTest() {
|
||||
console.log('\n🔨 运行简单修复测试...');
|
||||
|
||||
try {
|
||||
// 尝试最基本的自动化
|
||||
const script = `
|
||||
display dialog "TaskPing 自动化测试" with title "权限测试" buttons {"确定"} default button 1 giving up after 3
|
||||
`;
|
||||
|
||||
await this.runAppleScript(script);
|
||||
console.log('✅ 基本对话框测试成功');
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ 基本测试失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async runAppleScript(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
let error = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
osascript.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output.trim());
|
||||
} else {
|
||||
reject(new Error(error || `Exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async runCommand(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn('sh', ['-c', command]);
|
||||
let output = '';
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output.trim());
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async setClipboard(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pbcopy = spawn('pbcopy');
|
||||
pbcopy.stdin.write(text);
|
||||
pbcopy.stdin.end();
|
||||
|
||||
pbcopy.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Failed to set clipboard'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getClipboard() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pbpaste = spawn('pbpaste');
|
||||
let output = '';
|
||||
|
||||
pbpaste.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
pbpaste.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output);
|
||||
} else {
|
||||
reject(new Error('Failed to get clipboard'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
question(rl, prompt) {
|
||||
return new Promise(resolve => {
|
||||
rl.question(prompt, resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 运行诊断
|
||||
if (require.main === module) {
|
||||
const diagnostic = new AutomationDiagnostic();
|
||||
diagnostic.runDiagnostic().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = AutomationDiagnostic;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
# TaskPing 邮件功能架构设计
|
||||
|
||||
## 功能概述
|
||||
|
||||
实现邮件通知和远程命令执行功能,用户可以:
|
||||
1. 接收 Claude Code 任务完成的邮件通知
|
||||
2. 通过回复邮件来远程执行下一步命令
|
||||
3. 在不坐在电脑前的情况下继续 Claude Code 对话
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Claude Code │ ──▶│ TaskPing CLI │ ──▶│ Email Channel │
|
||||
│ (hooks) │ │ │ │ (SMTP Send) │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐ ┌─────────────────┐
|
||||
│ Command Relay │◀───│ Email Listener │
|
||||
│ Service │ │ (IMAP Receive) │
|
||||
└──────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Claude Code │
|
||||
│ (stdin input) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. 邮件通知渠道 (Email Channel)
|
||||
- **位置**: `src/channels/email/smtp.js`
|
||||
- **功能**: 发送任务完成通知邮件
|
||||
- **特性**:
|
||||
- 支持多种 SMTP 配置
|
||||
- 邮件模板化
|
||||
- 会话 ID 生成和嵌入
|
||||
- 安全回复指令
|
||||
|
||||
### 2. 邮件监听器 (Email Listener)
|
||||
- **位置**: `src/relay/email-listener.js`
|
||||
- **功能**: 监听和解析邮件回复
|
||||
- **特性**:
|
||||
- IMAP/POP3 邮件接收
|
||||
- 邮件解析和命令提取
|
||||
- 会话 ID 验证
|
||||
- 安全检查
|
||||
|
||||
### 3. 命令中继服务 (Command Relay Service)
|
||||
- **位置**: `src/relay/command-relay.js`
|
||||
- **功能**: 管理命令队列和执行
|
||||
- **特性**:
|
||||
- 会话管理
|
||||
- 命令队列
|
||||
- Claude Code 集成
|
||||
- 安全验证
|
||||
|
||||
### 4. 会话管理器 (Session Manager)
|
||||
- **位置**: `src/relay/session-manager.js`
|
||||
- **功能**: 管理 Claude Code 会话状态
|
||||
- **特性**:
|
||||
- 会话创建和跟踪
|
||||
- 超时管理
|
||||
- 状态持久化
|
||||
|
||||
## 数据流程
|
||||
|
||||
### 发送通知流程
|
||||
1. Claude Code 触发 hook → TaskPing CLI
|
||||
2. TaskPing 创建会话 ID 和通知
|
||||
3. 邮件渠道发送包含会话信息的邮件
|
||||
4. 会话管理器记录会话状态
|
||||
|
||||
### 命令执行流程
|
||||
1. 用户回复邮件 → 邮件监听器接收
|
||||
2. 解析邮件内容和会话 ID
|
||||
3. 验证会话有效性和安全性
|
||||
4. 将命令加入执行队列
|
||||
5. 命令中继服务执行命令
|
||||
|
||||
## 邮件模板设计
|
||||
|
||||
### 通知邮件模板
|
||||
```
|
||||
主题: [TaskPing] Claude Code 任务完成 - {project}
|
||||
|
||||
{user_name},您好!
|
||||
|
||||
Claude Code 已完成任务,正在等待您的下一步指令。
|
||||
|
||||
项目: {project}
|
||||
时间: {timestamp}
|
||||
状态: {status}
|
||||
|
||||
要继续对话,请直接回复此邮件,在邮件正文中输入您的指令。
|
||||
|
||||
示例回复:
|
||||
"请继续优化代码"
|
||||
"生成单元测试"
|
||||
"解释这个函数的作用"
|
||||
|
||||
---
|
||||
会话ID: {session_id}
|
||||
安全提示: 请勿转发此邮件,会话将在24小时后过期
|
||||
```
|
||||
|
||||
### 回复解析规则
|
||||
- 提取邮件正文作为 Claude Code 指令
|
||||
- 忽略邮件签名和引用内容
|
||||
- 验证会话 ID 有效性
|
||||
- 检查指令安全性
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 1. 会话验证
|
||||
- 唯一会话 ID (UUID v4)
|
||||
- 24小时过期时间
|
||||
- 用户邮箱验证
|
||||
|
||||
### 2. 命令安全
|
||||
- 危险命令黑名单
|
||||
- 长度限制 (< 1000 字符)
|
||||
- 特殊字符过滤
|
||||
|
||||
### 3. 频率限制
|
||||
- 每会话最多 10 次命令
|
||||
- 每小时最多 5 个新会话
|
||||
- 异常行为检测
|
||||
|
||||
## 配置结构
|
||||
|
||||
### 邮件配置 (config/channels.json)
|
||||
```json
|
||||
{
|
||||
"email": {
|
||||
"type": "email",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"auth": {
|
||||
"user": "your-email@gmail.com",
|
||||
"pass": "your-app-password"
|
||||
}
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"secure": true,
|
||||
"auth": {
|
||||
"user": "your-email@gmail.com",
|
||||
"pass": "your-app-password"
|
||||
}
|
||||
},
|
||||
"from": "TaskPing <your-email@gmail.com>",
|
||||
"to": "your-email@gmail.com",
|
||||
"template": {
|
||||
"subject": "[TaskPing] Claude Code 任务完成 - {{project}}",
|
||||
"checkInterval": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 中继配置 (config/relay.json)
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"security": {
|
||||
"sessionTimeout": 86400,
|
||||
"maxCommandsPerSession": 10,
|
||||
"maxSessionsPerHour": 5,
|
||||
"commandMaxLength": 1000
|
||||
},
|
||||
"claudeCode": {
|
||||
"detectMethod": "ps",
|
||||
"inputMethod": "stdin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实现计划
|
||||
|
||||
1. **Phase 1**: 邮件发送功能
|
||||
- 实现 SMTP 邮件渠道
|
||||
- 创建邮件模板系统
|
||||
- 配置管理界面
|
||||
|
||||
2. **Phase 2**: 邮件接收功能
|
||||
- IMAP 邮件监听器
|
||||
- 邮件解析和命令提取
|
||||
- 会话管理器
|
||||
|
||||
3. **Phase 3**: 命令中继功能
|
||||
- 命令队列系统
|
||||
- Claude Code 集成
|
||||
- 安全验证机制
|
||||
|
||||
4. **Phase 4**: 测试和优化
|
||||
- 端到端测试
|
||||
- 错误处理完善
|
||||
- 性能优化
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
# TaskPing 邮件功能使用指南
|
||||
|
||||
## 🌟 功能概述
|
||||
|
||||
TaskPing 现在支持邮件通知和远程命令执行功能,让您可以:
|
||||
|
||||
1. **📧 接收邮件通知** - 当 Claude Code 任务完成时,自动发送邮件通知
|
||||
2. **🔄 远程命令执行** - 通过回复邮件来远程执行 Claude Code 命令
|
||||
3. **🏠 真正的远程工作** - 即使不在电脑前,也能继续与 Claude Code 对话
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 步骤 1: 配置邮件设置
|
||||
|
||||
```bash
|
||||
# 启动配置管理器
|
||||
node taskping.js config
|
||||
|
||||
# 选择 "3. 通知渠道"
|
||||
# 然后选择 "2. 邮件通知"
|
||||
```
|
||||
|
||||
### 步骤 2: 输入邮箱配置
|
||||
|
||||
以 Gmail 为例:
|
||||
|
||||
```
|
||||
SMTP 主机: smtp.gmail.com
|
||||
SMTP 端口: 587
|
||||
使用 SSL/TLS: n (使用 STARTTLS)
|
||||
SMTP 用户名: your-email@gmail.com
|
||||
SMTP 密码: your-app-password # 需要使用应用密码
|
||||
|
||||
IMAP 主机: imap.gmail.com
|
||||
IMAP 端口: 993
|
||||
IMAP 使用 SSL: y
|
||||
|
||||
收件人邮箱: your-email@gmail.com
|
||||
发件人显示名: TaskPing <your-email@gmail.com>
|
||||
```
|
||||
|
||||
### 步骤 3: 测试邮件发送
|
||||
|
||||
配置完成后,选择测试邮件发送功能,您应该会收到一封测试邮件。
|
||||
|
||||
### 步骤 4: 启动命令中继服务
|
||||
|
||||
```bash
|
||||
# 启动邮件命令中继服务
|
||||
node taskping.js relay start
|
||||
```
|
||||
|
||||
## 📋 详细配置指南
|
||||
|
||||
### Gmail 配置
|
||||
|
||||
1. **启用两步验证**
|
||||
- 登录 Google 账户
|
||||
- 进入"安全"设置
|
||||
- 启用两步验证
|
||||
|
||||
2. **生成应用密码**
|
||||
- 在 Google 账户安全设置中
|
||||
- 选择"应用密码"
|
||||
- 选择"邮件"和您的设备
|
||||
- 复制生成的 16 位密码
|
||||
|
||||
3. **启用 IMAP**
|
||||
- 登录 Gmail
|
||||
- 进入设置 → 转发和POP/IMAP
|
||||
- 启用 IMAP 访问
|
||||
|
||||
### Outlook/Hotmail 配置
|
||||
|
||||
```
|
||||
SMTP 主机: smtp.live.com
|
||||
SMTP 端口: 587
|
||||
IMAP 主机: imap-mail.outlook.com
|
||||
IMAP 端口: 993
|
||||
```
|
||||
|
||||
### 其他邮箱提供商
|
||||
|
||||
| 提供商 | SMTP 主机 | SMTP 端口 | IMAP 主机 | IMAP 端口 |
|
||||
|--------|-----------|-----------|-----------|-----------|
|
||||
| QQ邮箱 | smtp.qq.com | 587 | imap.qq.com | 993 |
|
||||
| 163邮箱 | smtp.163.com | 587 | imap.163.com | 993 |
|
||||
| 126邮箱 | smtp.126.com | 587 | imap.126.com | 993 |
|
||||
|
||||
## 🔄 使用流程
|
||||
|
||||
### 1. 正常工作流程
|
||||
|
||||
```
|
||||
1. 启动 Claude Code
|
||||
2. 执行任务 (如: "帮我重构这个组件")
|
||||
3. 📧 收到邮件通知: "任务完成"
|
||||
4. 💬 回复邮件: "请添加单元测试"
|
||||
5. ⚡ 命令自动在 Claude Code 中执行
|
||||
```
|
||||
|
||||
### 2. 邮件通知示例
|
||||
|
||||
当 Claude Code 完成任务时,您会收到如下邮件:
|
||||
|
||||
```
|
||||
主题: [TaskPing] Claude Code 任务完成 - MyProject
|
||||
|
||||
🎉 Claude Code 任务完成
|
||||
|
||||
项目: MyProject
|
||||
时间: 2025-07-12 19:45:30
|
||||
状态: 任务完成
|
||||
|
||||
消息: 任务已完成,Claude正在等待下一步指令
|
||||
|
||||
💡 如何继续对话
|
||||
要继续与 Claude Code 对话,请直接回复此邮件,在邮件正文中输入您的指令。
|
||||
|
||||
示例回复:
|
||||
• "请继续优化代码"
|
||||
• "生成单元测试"
|
||||
• "解释这个函数的作用"
|
||||
|
||||
会话ID: 123e4567-e89b-12d3-a456-426614174000
|
||||
🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期
|
||||
```
|
||||
|
||||
### 3. 回复邮件执行命令
|
||||
|
||||
直接回复邮件,在正文中输入要执行的命令:
|
||||
|
||||
```
|
||||
请添加错误处理和日志记录功能
|
||||
```
|
||||
|
||||
系统会自动:
|
||||
1. 识别邮件回复
|
||||
2. 提取命令内容
|
||||
3. 验证会话有效性
|
||||
4. 在 Claude Code 中执行命令
|
||||
|
||||
## 🛠️ 管理命令
|
||||
|
||||
### 查看中继服务状态
|
||||
|
||||
```bash
|
||||
node taskping.js relay status
|
||||
```
|
||||
|
||||
输出示例:
|
||||
```
|
||||
📊 命令中继服务状态
|
||||
|
||||
✅ 邮件配置已启用
|
||||
📧 SMTP: smtp.gmail.com:587
|
||||
📥 IMAP: imap.gmail.com:993
|
||||
📬 收件人: your-email@gmail.com
|
||||
|
||||
📋 命令队列: 3 个命令
|
||||
|
||||
最近的命令:
|
||||
✅ abc123: 请添加错误处理功能...
|
||||
⏳ def456: 生成单元测试...
|
||||
⏸️ ghi789: 优化性能...
|
||||
```
|
||||
|
||||
### 清理命令历史
|
||||
|
||||
```bash
|
||||
node taskping.js relay cleanup
|
||||
```
|
||||
|
||||
### 停止中继服务
|
||||
|
||||
在运行中继服务的终端中按 `Ctrl+C`
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
### 会话管理
|
||||
- **唯一会话ID**: 每个通知邮件包含唯一的 UUID
|
||||
- **24小时过期**: 会话自动过期,防止滥用
|
||||
- **命令限制**: 每个会话最多 10 个命令
|
||||
|
||||
### 内容安全
|
||||
- **邮件验证**: 验证回复邮件来源
|
||||
- **命令过滤**: 过滤危险命令
|
||||
- **长度限制**: 命令长度限制在 1000 字符内
|
||||
|
||||
### 危险命令黑名单
|
||||
系统会自动拒绝以下类型的命令:
|
||||
- `rm -rf` (删除文件)
|
||||
- `sudo` (提权操作)
|
||||
- `curl | sh` (执行远程脚本)
|
||||
- `eval` / `exec` (代码执行)
|
||||
|
||||
## 🚨 故障排除
|
||||
|
||||
### 无法发送邮件
|
||||
|
||||
**问题**: 邮件发送失败
|
||||
**解决方案**:
|
||||
1. 检查 SMTP 配置是否正确
|
||||
2. 确认使用应用密码而非普通密码
|
||||
3. 检查网络连接
|
||||
4. 查看防火墙设置
|
||||
|
||||
```bash
|
||||
# 测试邮件发送
|
||||
node taskping.js config
|
||||
# 选择通知渠道 → 邮件通知 → 测试
|
||||
```
|
||||
|
||||
### 无法接收回复
|
||||
|
||||
**问题**: 回复邮件后命令不执行
|
||||
**解决方案**:
|
||||
1. 确认中继服务正在运行
|
||||
2. 检查 IMAP 配置
|
||||
3. 确认回复的是 TaskPing 邮件
|
||||
4. 检查命令是否被安全过滤器拦截
|
||||
|
||||
```bash
|
||||
# 检查中继服务状态
|
||||
node taskping.js relay status
|
||||
|
||||
# 重启中继服务
|
||||
node taskping.js relay start
|
||||
```
|
||||
|
||||
### 会话过期
|
||||
|
||||
**问题**: 提示会话过期
|
||||
**解决方案**:
|
||||
- 会话在24小时后自动过期
|
||||
- 需要等待新的任务完成通知
|
||||
- 或手动发送测试通知
|
||||
|
||||
### Claude Code 进程检测失败
|
||||
|
||||
**问题**: 无法找到 Claude Code 进程
|
||||
**解决方案**:
|
||||
1. 确保 Claude Code 正在运行
|
||||
2. 检查进程名称是否正确
|
||||
3. 目前支持自动化输入的平台:macOS
|
||||
|
||||
## 📱 高级用法
|
||||
|
||||
### 多项目管理
|
||||
|
||||
不同项目的通知邮件会包含项目名称,方便区分:
|
||||
|
||||
```
|
||||
[TaskPing] Claude Code 任务完成 - Frontend-Project
|
||||
[TaskPing] Claude Code 任务完成 - Backend-API
|
||||
[TaskPing] Claude Code 任务完成 - Mobile-App
|
||||
```
|
||||
|
||||
### 命令模板
|
||||
|
||||
常用命令模板:
|
||||
|
||||
```bash
|
||||
# 代码优化
|
||||
"请优化性能并添加注释"
|
||||
|
||||
# 测试生成
|
||||
"为这个函数生成单元测试"
|
||||
|
||||
# 文档生成
|
||||
"生成 API 文档"
|
||||
|
||||
# 代码审查
|
||||
"审查代码并指出潜在问题"
|
||||
|
||||
# 重构建议
|
||||
"建议如何重构这段代码"
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
|
||||
可以在一封回复邮件中包含多个步骤:
|
||||
|
||||
```
|
||||
请按以下步骤处理:
|
||||
1. 优化函数性能
|
||||
2. 添加错误处理
|
||||
3. 生成单元测试
|
||||
4. 更新文档
|
||||
```
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. 安全建议
|
||||
- 不要在邮件中包含敏感信息
|
||||
- 定期更换应用密码
|
||||
- 不要转发 TaskPing 通知邮件
|
||||
- 及时清理过期会话
|
||||
|
||||
### 2. 命令编写
|
||||
- 使用清晰、具体的指令
|
||||
- 避免过于复杂的命令
|
||||
- 一次专注一个任务
|
||||
- 使用自然语言,无需特殊格式
|
||||
|
||||
### 3. 工作流程
|
||||
- 启动工作时开启中继服务
|
||||
- 结束工作时关闭中继服务
|
||||
- 定期查看中继状态
|
||||
- 及时清理命令历史
|
||||
|
||||
## 🆘 常见问题
|
||||
|
||||
**Q: 邮件功能会影响现有的桌面通知吗?**
|
||||
A: 不会。邮件通知和桌面通知是独立的,可以同时启用。
|
||||
|
||||
**Q: 可以配置多个邮箱吗?**
|
||||
A: 目前支持一个邮箱配置,但可以发送给多个收件人(在配置中用逗号分隔)。
|
||||
|
||||
**Q: 支持哪些邮箱提供商?**
|
||||
A: 支持所有标准的 SMTP/IMAP 邮箱服务,包括 Gmail、Outlook、QQ、163 等。
|
||||
|
||||
**Q: 命令执行失败怎么办?**
|
||||
A: 系统会自动重试 3 次,如果仍然失败,会在状态中显示错误信息。
|
||||
|
||||
**Q: 如何确保数据安全?**
|
||||
A: 所有邮件配置存储在本地,使用应用密码而非主密码,会话自动过期。
|
||||
|
||||
## 🎉 开始使用
|
||||
|
||||
现在您已经了解了 TaskPing 邮件功能的所有细节,可以开始配置和使用了:
|
||||
|
||||
```bash
|
||||
# 1. 配置邮箱
|
||||
node taskping.js config
|
||||
|
||||
# 2. 测试通知
|
||||
node taskping.js test
|
||||
|
||||
# 3. 启动中继服务
|
||||
node taskping.js relay start
|
||||
|
||||
# 4. 开始使用 Claude Code,享受远程工作的便利!
|
||||
```
|
||||
|
||||
享受您的远程 AI 编程体验!🚀
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
# TaskPing 邮件功能快速配置指南
|
||||
|
||||
## 🚀 三种配置方式
|
||||
|
||||
TaskPing 现在提供三种方式来配置邮件功能,您可以选择最适合的方式:
|
||||
|
||||
### 方式 1: 快速配置向导 (推荐 ⭐)
|
||||
|
||||
```bash
|
||||
node taskping.js setup-email
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 🎯 一步到位,引导式配置
|
||||
- 🛡️ 内置常见邮箱提供商设置
|
||||
- 🧪 配置完成后可立即测试
|
||||
|
||||
**适用于**: 初次配置,想要快速开始使用的用户
|
||||
|
||||
---
|
||||
|
||||
### 方式 2: 直接编辑配置文件
|
||||
|
||||
```bash
|
||||
node taskping.js edit-config channels
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ⚡ 最快速,适合有经验的用户
|
||||
- 🔧 完全控制所有配置选项
|
||||
- 📝 支持批量修改和复制粘贴
|
||||
|
||||
**适用于**: 熟悉JSON格式,需要精确控制配置的用户
|
||||
|
||||
---
|
||||
|
||||
### 方式 3: 交互式配置管理器
|
||||
|
||||
```bash
|
||||
node taskping.js config
|
||||
# 选择 "3. 通知渠道" → "2. 邮件通知"
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 🎮 交互式界面,逐步配置
|
||||
- 💡 详细的配置说明和提示
|
||||
- 🔄 可以随时修改现有配置
|
||||
|
||||
**适用于**: 喜欢逐步配置,需要详细指导的用户
|
||||
|
||||
---
|
||||
|
||||
## 📧 常见邮箱配置
|
||||
|
||||
### Gmail 配置
|
||||
|
||||
**前提条件**:
|
||||
1. 启用两步验证
|
||||
2. 生成应用密码 (16位)
|
||||
|
||||
**配置参数**:
|
||||
```json
|
||||
{
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"secure": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### QQ邮箱配置
|
||||
|
||||
**前提条件**:
|
||||
1. 开启SMTP/IMAP服务
|
||||
2. 获取授权码
|
||||
|
||||
**配置参数**:
|
||||
```json
|
||||
{
|
||||
"smtp": {
|
||||
"host": "smtp.qq.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.qq.com",
|
||||
"port": 993,
|
||||
"secure": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 163邮箱配置
|
||||
|
||||
**配置参数**:
|
||||
```json
|
||||
{
|
||||
"smtp": {
|
||||
"host": "smtp.163.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
"imap": {
|
||||
"host": "imap.163.com",
|
||||
"port": 993,
|
||||
"secure": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 配置后的使用流程
|
||||
|
||||
### 1. 测试邮件发送
|
||||
|
||||
```bash
|
||||
node taskping.js test
|
||||
```
|
||||
|
||||
应该能看到:
|
||||
```
|
||||
Testing notification channels...
|
||||
|
||||
✅ desktop: PASS
|
||||
✅ email: PASS
|
||||
|
||||
Test completed: 2/2 channels passed
|
||||
```
|
||||
|
||||
### 2. 查看系统状态
|
||||
|
||||
```bash
|
||||
node taskping.js status
|
||||
```
|
||||
|
||||
应该能看到:
|
||||
```
|
||||
TaskPing Status
|
||||
|
||||
Configuration:
|
||||
Enabled: Yes
|
||||
Language: zh-CN
|
||||
Sounds: Submarine / Hero
|
||||
|
||||
Channels:
|
||||
desktop:
|
||||
Enabled: ✅
|
||||
Configured: ✅
|
||||
Supports Relay: ❌
|
||||
email:
|
||||
Enabled: ✅
|
||||
Configured: ✅
|
||||
Supports Relay: ✅
|
||||
```
|
||||
|
||||
### 3. 启动命令中继服务
|
||||
|
||||
```bash
|
||||
node taskping.js relay start
|
||||
```
|
||||
|
||||
看到以下信息表示成功:
|
||||
```
|
||||
🚀 启动邮件命令中继服务...
|
||||
✅ 命令中继服务已启动
|
||||
📧 正在监听邮件回复...
|
||||
💡 现在您可以通过回复邮件来远程执行Claude Code命令
|
||||
|
||||
按 Ctrl+C 停止服务
|
||||
```
|
||||
|
||||
### 4. 开始使用
|
||||
|
||||
现在当您使用 Claude Code 时:
|
||||
1. 任务完成时会收到邮件通知
|
||||
2. 回复邮件即可远程执行下一步命令
|
||||
3. 享受远程AI编程的便利!
|
||||
|
||||
---
|
||||
|
||||
## 🚨 常见问题解决
|
||||
|
||||
### Q: 邮件发送失败
|
||||
|
||||
**检查清单**:
|
||||
- ✅ 邮箱密码是否使用应用密码 (不是登录密码)
|
||||
- ✅ SMTP/IMAP 服务是否已开启
|
||||
- ✅ 网络连接是否正常
|
||||
- ✅ 防火墙是否阻止连接
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 重新配置
|
||||
node taskping.js setup-email
|
||||
|
||||
# 或检查配置文件
|
||||
node taskping.js edit-config channels
|
||||
```
|
||||
|
||||
### Q: 收不到邮件
|
||||
|
||||
**检查清单**:
|
||||
- ✅ 垃圾邮件文件夹
|
||||
- ✅ 邮件地址是否正确
|
||||
- ✅ 邮箱存储空间是否充足
|
||||
|
||||
### Q: 无法接收回复
|
||||
|
||||
**检查清单**:
|
||||
- ✅ 中继服务是否运行 (`taskping relay status`)
|
||||
- ✅ IMAP 配置是否正确
|
||||
- ✅ 是否回复的是 TaskPing 发送的邮件
|
||||
|
||||
---
|
||||
|
||||
## 📱 使用技巧
|
||||
|
||||
### 1. 邮件模板定制
|
||||
|
||||
您可以通过编辑配置文件自定义邮件检查间隔:
|
||||
|
||||
```bash
|
||||
node taskping.js edit-config channels
|
||||
```
|
||||
|
||||
找到 `template.checkInterval` 并修改值 (单位:秒):
|
||||
```json
|
||||
"template": {
|
||||
"checkInterval": 30 // 每30秒检查一次新邮件
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 多项目管理
|
||||
|
||||
不同项目的通知会自动包含项目名称:
|
||||
```
|
||||
[TaskPing] Claude Code 任务完成 - MyProject
|
||||
[TaskPing] Claude Code 任务完成 - AnotherProject
|
||||
```
|
||||
|
||||
### 3. 批量配置
|
||||
|
||||
如果您有多台电脑需要配置,可以:
|
||||
1. 在一台电脑上配置好
|
||||
2. 复制 `config/channels.json` 文件
|
||||
3. 粘贴到其他电脑的同一位置
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
1. **安全性**: 定期更换应用密码
|
||||
2. **性能**: 根据使用频率调整检查间隔
|
||||
3. **维护**: 定期清理命令历史 (`taskping relay cleanup`)
|
||||
4. **监控**: 定期检查中继服务状态 (`taskping relay status`)
|
||||
|
||||
---
|
||||
|
||||
开始享受远程AI编程的便利吧!🚀
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
#!/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);
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing 邮件检查器
|
||||
* 更强大的邮件搜索和内容提取工具
|
||||
*/
|
||||
|
||||
const Imap = require('node-imap');
|
||||
const { simpleParser } = require('mailparser');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class EmailChecker {
|
||||
constructor() {
|
||||
this.configPath = path.join(__dirname, 'config/channels.json');
|
||||
this.config = null;
|
||||
this.imap = 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();
|
||||
await this.comprehensiveEmailCheck();
|
||||
await this.startContinuousMonitoring();
|
||||
} 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.connect();
|
||||
});
|
||||
}
|
||||
|
||||
async comprehensiveEmailCheck() {
|
||||
console.log('\n🔍 开始全面邮件检查...\n');
|
||||
|
||||
await this.openInbox();
|
||||
|
||||
// 1. 检查最近24小时所有邮件(不仅是未读)
|
||||
console.log('📅 1. 检查最近24小时所有邮件...');
|
||||
await this.searchEmails('24h', false);
|
||||
|
||||
// 2. 检查最近1小时未读邮件
|
||||
console.log('\n📧 2. 检查最近1小时未读邮件...');
|
||||
await this.searchEmails('1h', true);
|
||||
|
||||
// 3. 检查主题包含特定关键词的邮件
|
||||
console.log('\n🎯 3. 检查TaskPing相关邮件...');
|
||||
await this.searchBySubject();
|
||||
|
||||
// 4. 检查来自特定发件人的邮件
|
||||
console.log('\n👤 4. 检查来自目标邮箱的邮件...');
|
||||
await this.searchByFrom();
|
||||
}
|
||||
|
||||
async openInbox() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
console.log(`📫 收件箱: 总计${box.messages.total}封邮件`);
|
||||
resolve(box);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async searchEmails(timeRange, unseenOnly = false) {
|
||||
return new Promise((resolve) => {
|
||||
const since = new Date();
|
||||
if (timeRange === '1h') {
|
||||
since.setHours(since.getHours() - 1);
|
||||
} else if (timeRange === '24h') {
|
||||
since.setDate(since.getDate() - 1);
|
||||
}
|
||||
|
||||
let searchCriteria = [['SINCE', since]];
|
||||
if (unseenOnly) {
|
||||
searchCriteria.push(['UNSEEN']);
|
||||
}
|
||||
|
||||
console.log(`🔍 搜索条件: ${timeRange}, ${unseenOnly ? '仅未读' : '全部'}`);
|
||||
|
||||
this.imap.search(searchCriteria, (err, results) => {
|
||||
if (err) {
|
||||
console.error('❌ 搜索失败:', err.message);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📨 找到 ${results.length} 封邮件`);
|
||||
|
||||
if (results.length > 0) {
|
||||
this.analyzeEmails(results.slice(-5)); // 只分析最新5封
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async searchBySubject() {
|
||||
return new Promise((resolve) => {
|
||||
// 搜索主题包含Re:或TaskPing的邮件
|
||||
this.imap.search([['OR', ['SUBJECT', 'Re:'], ['SUBJECT', 'TaskPing']]], (err, results) => {
|
||||
if (err) {
|
||||
console.error('❌ 主题搜索失败:', err.message);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📨 找到 ${results.length} 封相关邮件`);
|
||||
|
||||
if (results.length > 0) {
|
||||
this.analyzeEmails(results.slice(-3)); // 只分析最新3封
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async searchByFrom() {
|
||||
return new Promise((resolve) => {
|
||||
// 搜索来自目标邮箱的邮件
|
||||
const targetEmail = this.config.to;
|
||||
|
||||
this.imap.search([['FROM', targetEmail]], (err, results) => {
|
||||
if (err) {
|
||||
console.error('❌ 发件人搜索失败:', err.message);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📨 找到来自 ${targetEmail} 的 ${results.length} 封邮件`);
|
||||
|
||||
if (results.length > 0) {
|
||||
this.analyzeEmails(results.slice(-3)); // 只分析最新3封
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
analyzeEmails(emailUids) {
|
||||
const fetch = this.imap.fetch(emailUids, {
|
||||
bodies: '',
|
||||
markSeen: false // 不标记为已读
|
||||
});
|
||||
|
||||
fetch.on('message', (msg, seqno) => {
|
||||
console.log(`\n📧 分析邮件 ${seqno}:`);
|
||||
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.processEmailContent(parsed, seqno);
|
||||
} catch (error) {
|
||||
console.error(`❌ 解析邮件 ${seqno} 失败:`, error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async processEmailContent(email, seqno) {
|
||||
console.log(` 📄 主题: ${email.subject || '(无主题)'}`);
|
||||
console.log(` 👤 发件人: ${email.from?.text || '(未知)'}`);
|
||||
console.log(` 📅 时间: ${email.date || '(未知)'}`);
|
||||
|
||||
// 判断是否是潜在的回复邮件
|
||||
const isPotentialReply = this.isPotentialReply(email);
|
||||
console.log(` 🎯 潜在回复: ${isPotentialReply ? '是' : '否'}`);
|
||||
|
||||
if (isPotentialReply) {
|
||||
const command = this.extractCommand(email);
|
||||
console.log(` 💬 邮件内容长度: ${(email.text || '').length} 字符`);
|
||||
console.log(` 📝 提取的内容:\n"${command.substring(0, 200)}${command.length > 200 ? '...' : ''}"`);
|
||||
|
||||
if (command && command.trim().length > 0) {
|
||||
console.log(`\n🎉 发现有效命令! (邮件 ${seqno})`);
|
||||
await this.handleCommand(command, seqno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isPotentialReply(email) {
|
||||
const subject = email.subject || '';
|
||||
const from = email.from?.text || '';
|
||||
const targetEmail = this.config.to;
|
||||
|
||||
// 检查多种条件
|
||||
return (
|
||||
subject.includes('[TaskPing]') ||
|
||||
subject.match(/^(Re:|RE:|回复:)/i) ||
|
||||
from.includes(targetEmail) ||
|
||||
(email.text && email.text.length > 10) // 有实际内容
|
||||
);
|
||||
}
|
||||
|
||||
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:') ||
|
||||
line.includes('TaskPing <') ||
|
||||
line.match(/\d{4}年\d{1,2}月\d{1,2}日/)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 跳过签名和空行
|
||||
if (line.includes('--') ||
|
||||
line.includes('Sent from') ||
|
||||
line.includes('发自我的') ||
|
||||
line.trim() === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
commandLines.push(line);
|
||||
}
|
||||
|
||||
return commandLines.join('\n').trim();
|
||||
}
|
||||
|
||||
async handleCommand(command, seqno) {
|
||||
console.log(`\n🚀 处理命令 (来自邮件 ${seqno}):`);
|
||||
console.log(`📝 命令内容: "${command}"`);
|
||||
|
||||
try {
|
||||
// 复制到剪贴板
|
||||
await this.copyToClipboard(command);
|
||||
console.log('✅ 命令已复制到剪贴板');
|
||||
|
||||
// 发送通知
|
||||
await this.sendNotification(command);
|
||||
console.log('✅ 通知已发送');
|
||||
|
||||
console.log('\n🎯 请在Claude Code中粘贴命令 (Cmd+V)');
|
||||
|
||||
} 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 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 "Glass"
|
||||
`;
|
||||
|
||||
spawn('osascript', ['-e', script]);
|
||||
}
|
||||
|
||||
async startContinuousMonitoring() {
|
||||
console.log('\n👂 开始持续监控新邮件...');
|
||||
console.log('💌 现在可以回复邮件测试功能');
|
||||
console.log('🔍 每30秒检查一次新邮件\n');
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
console.log('🔄 检查新邮件...');
|
||||
await this.searchEmails('1h', false); // 搜索所有邮件,不只是未读
|
||||
} catch (error) {
|
||||
console.error('❌ 监控检查失败:', error.message);
|
||||
}
|
||||
}, 30000); // 每30秒检查一次
|
||||
|
||||
// 设置优雅关闭
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 停止邮件监控...');
|
||||
if (this.imap) {
|
||||
this.imap.end();
|
||||
}
|
||||
console.log('✅ 服务已停止');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.stdin.resume();
|
||||
}
|
||||
}
|
||||
|
||||
// 启动检查器
|
||||
const checker = new EmailChecker();
|
||||
checker.start().catch(console.error);
|
||||
|
|
@ -0,0 +1,403 @@
|
|||
{
|
||||
"name": "taskping",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "taskping",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"mailparser": "^3.7.4",
|
||||
"node-imap": "^0.9.6",
|
||||
"nodemailer": "^7.0.5",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"taskping-config": "config-tool.js",
|
||||
"taskping-install": "install.js",
|
||||
"taskping-notify": "hook-notify.js"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@selderee/plugin-htmlparser2": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-japanese": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
|
||||
"integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==",
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||
"dependencies": {
|
||||
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"htmlparser2": "^8.0.2",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/leac": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/leac/-/leac-0.6.0.tgz",
|
||||
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/libbase64": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/libbase64/-/libbase64-1.3.0.tgz",
|
||||
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="
|
||||
},
|
||||
"node_modules/libmime": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/libmime/-/libmime-5.3.7.tgz",
|
||||
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"libbase64": "1.3.0",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/libqp": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/libqp/-/libqp-2.1.1.tgz",
|
||||
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser": {
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmmirror.com/mailparser/-/mailparser-3.7.4.tgz",
|
||||
"integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
"he": "1.2.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"iconv-lite": "0.6.3",
|
||||
"libmime": "5.3.7",
|
||||
"linkify-it": "5.0.0",
|
||||
"mailsplit": "5.4.5",
|
||||
"nodemailer": "7.0.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"tlds": "1.259.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser/node_modules/nodemailer": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.4.tgz",
|
||||
"integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mailsplit": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmmirror.com/mailsplit/-/mailsplit-5.4.5.tgz",
|
||||
"integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==",
|
||||
"dependencies": {
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.7",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-imap": {
|
||||
"version": "0.9.6",
|
||||
"resolved": "https://registry.npmmirror.com/node-imap/-/node-imap-0.9.6.tgz",
|
||||
"integrity": "sha512-pYQ2AtjQwrSvILq8EYInv3E3svrJwrTOxzW7uBGpP//AkCs/pMdO+O6KEgUlSchh/0/N0MSWs5io3xZhxJ9yLg==",
|
||||
"dependencies": {
|
||||
"readable-stream": "^3.6.0",
|
||||
"utf7": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmmirror.com/parseley/-/parseley-0.12.1.tgz",
|
||||
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||
"dependencies": {
|
||||
"leac": "^0.6.0",
|
||||
"peberminta": "^0.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/peberminta": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.9.0.tgz",
|
||||
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/selderee": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/selderee/-/selderee-0.11.0.tgz",
|
||||
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||
"dependencies": {
|
||||
"parseley": "^0.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-5.3.0.tgz",
|
||||
"integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.259.0",
|
||||
"resolved": "https://registry.npmmirror.com/tlds/-/tlds-1.259.0.tgz",
|
||||
"integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==",
|
||||
"bin": {
|
||||
"tlds": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
||||
},
|
||||
"node_modules/utf7": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/utf7/-/utf7-1.0.2.tgz",
|
||||
"integrity": "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==",
|
||||
"dependencies": {
|
||||
"semver": "~5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "taskping",
|
||||
"version": "1.0.0",
|
||||
"description": "Claude Code智能任务通知系统 - 当Claude完成任务或需要输入时发送桌面通知",
|
||||
"main": "hook-notify.js",
|
||||
"scripts": {
|
||||
"install": "node install.js",
|
||||
"config": "node taskping-config.js",
|
||||
"test": "node config-tool.js --test",
|
||||
"test-completed": "node hook-notify.js --type completed",
|
||||
"test-waiting": "node hook-notify.js --type waiting",
|
||||
"daemon:start": "node taskping.js daemon start",
|
||||
"daemon:stop": "node taskping.js daemon stop",
|
||||
"daemon:status": "node taskping.js daemon status",
|
||||
"test:clipboard": "node test-clipboard.js",
|
||||
"start": "node email-automation.js"
|
||||
},
|
||||
"bin": {
|
||||
"taskping-install": "./install.js",
|
||||
"taskping-config": "./config-tool.js",
|
||||
"taskping-notify": "./hook-notify.js"
|
||||
},
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
"notification",
|
||||
"desktop-notification",
|
||||
"hooks",
|
||||
"productivity",
|
||||
"development-tools",
|
||||
"task-management",
|
||||
"claude",
|
||||
"ai-assistant"
|
||||
],
|
||||
"author": "TaskPing Team",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TaskPing/TaskPing.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/TaskPing/TaskPing/issues"
|
||||
},
|
||||
"homepage": "https://github.com/TaskPing/TaskPing#readme",
|
||||
"dependencies": {
|
||||
"mailparser": "^3.7.4",
|
||||
"node-imap": "^0.9.6",
|
||||
"nodemailer": "^7.0.5",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"files": [
|
||||
"hook-notify.js",
|
||||
"config-tool.js",
|
||||
"install.js",
|
||||
"config.json",
|
||||
"i18n.json",
|
||||
"claude-hooks.json",
|
||||
"sounds/",
|
||||
"README.md",
|
||||
"QUICKSTART.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* macOS 权限设置助手
|
||||
* 帮助用户设置必要的系统权限
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
class PermissionSetup {
|
||||
constructor() {
|
||||
this.requiredPermissions = [
|
||||
'Accessibility',
|
||||
'Automation'
|
||||
];
|
||||
}
|
||||
|
||||
async checkAndSetup() {
|
||||
console.log('🔒 macOS 权限设置助手\n');
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
console.log('ℹ️ 此工具仅适用于 macOS 系统');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('TaskPing 需要以下权限才能自动粘贴邮件回复到 Claude Code:\n');
|
||||
|
||||
console.log('1. 🖱️ 辅助功能权限 (Accessibility)');
|
||||
console.log(' - 允许控制其他应用程序');
|
||||
console.log(' - 自动输入和点击');
|
||||
|
||||
console.log('\n2. 🤖 自动化权限 (Automation)');
|
||||
console.log(' - 允许发送 Apple Events');
|
||||
console.log(' - 控制应用程序行为\n');
|
||||
|
||||
// 检查当前权限状态
|
||||
await this.checkCurrentPermissions();
|
||||
|
||||
console.log('📋 设置步骤:\n');
|
||||
|
||||
console.log('步骤 1: 打开系统偏好设置');
|
||||
console.log(' → 苹果菜单 > 系统偏好设置 > 安全性与隐私 > 隐私\n');
|
||||
|
||||
console.log('步骤 2: 设置辅助功能权限');
|
||||
console.log(' → 点击左侧 "辅助功能"');
|
||||
console.log(' → 点击锁图标并输入密码');
|
||||
console.log(' → 添加以下应用:');
|
||||
console.log(' • Terminal (如果你在 Terminal 中运行 TaskPing)');
|
||||
console.log(' • iTerm2 (如果使用 iTerm2)');
|
||||
console.log(' • Visual Studio Code (如果在 VS Code 中运行)');
|
||||
console.log(' • 或者你当前使用的终端应用\n');
|
||||
|
||||
console.log('步骤 3: 设置自动化权限');
|
||||
console.log(' → 点击左侧 "自动化"');
|
||||
console.log(' → 在你的终端应用下勾选:');
|
||||
console.log(' • System Events');
|
||||
console.log(' • Claude Code (如果有的话)');
|
||||
console.log(' • Terminal\n');
|
||||
|
||||
console.log('🚀 快速打开系统偏好设置?');
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const answer = await this.question(rl, '是否现在打开系统偏好设置?(y/n): ');
|
||||
|
||||
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
||||
await this.openSystemPreferences();
|
||||
}
|
||||
|
||||
rl.close();
|
||||
|
||||
console.log('\n✅ 设置完成后,重新运行以下命令测试:');
|
||||
console.log(' node taskping.js test-paste\n');
|
||||
}
|
||||
|
||||
async checkCurrentPermissions() {
|
||||
console.log('🔍 检查当前权限状态...\n');
|
||||
|
||||
try {
|
||||
// 测试基本的 AppleScript 执行
|
||||
const result = await this.runAppleScript('tell application "System Events" to return name of first process');
|
||||
if (result) {
|
||||
console.log('✅ 基本 AppleScript 权限:正常');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 基本 AppleScript 权限:需要设置');
|
||||
}
|
||||
|
||||
try {
|
||||
// 测试辅助功能权限
|
||||
const result = await this.runAppleScript(`
|
||||
tell application "System Events"
|
||||
try
|
||||
return name of first application process whose frontmost is true
|
||||
on error
|
||||
return "permission_denied"
|
||||
end try
|
||||
end tell
|
||||
`);
|
||||
|
||||
if (result && result !== 'permission_denied') {
|
||||
console.log('✅ 辅助功能权限:已授权');
|
||||
} else {
|
||||
console.log('❌ 辅助功能权限:需要授权');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 辅助功能权限:需要授权');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async runAppleScript(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
let error = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
osascript.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output.trim());
|
||||
} else {
|
||||
reject(new Error(error || 'AppleScript execution failed'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async openSystemPreferences() {
|
||||
try {
|
||||
console.log('\n🔧 正在打开系统偏好设置...');
|
||||
|
||||
// 直接打开安全性与隐私 > 隐私 > 辅助功能
|
||||
const script = `
|
||||
tell application "System Preferences"
|
||||
activate
|
||||
set current pane to pane "com.apple.preference.security"
|
||||
delay 1
|
||||
tell application "System Events"
|
||||
tell window 1 of application process "System Preferences"
|
||||
click tab "Privacy"
|
||||
delay 0.5
|
||||
tell outline 1 of scroll area 1
|
||||
select row "Accessibility"
|
||||
end tell
|
||||
end tell
|
||||
end tell
|
||||
end tell
|
||||
`;
|
||||
|
||||
await this.runAppleScript(script);
|
||||
console.log('✅ 已打开辅助功能设置页面');
|
||||
|
||||
} catch (error) {
|
||||
console.log('⚠️ 无法自动打开,请手动打开系统偏好设置');
|
||||
console.log('💡 你也可以运行:open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"');
|
||||
}
|
||||
}
|
||||
|
||||
question(rl, prompt) {
|
||||
return new Promise(resolve => {
|
||||
rl.question(prompt, resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 运行权限设置助手
|
||||
if (require.main === module) {
|
||||
const setup = new PermissionSetup();
|
||||
setup.checkAndSetup().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = PermissionSetup;
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing 简化启动脚本
|
||||
* 专注解决核心问题,减少复杂性
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const SimplifiedEmailAutomation = require('./src/simplified/email-automation');
|
||||
|
||||
class SimpleTaskPing {
|
||||
constructor() {
|
||||
this.configPath = path.join(__dirname, 'config/channels.json');
|
||||
this.automation = null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log('🚀 TaskPing 简化版启动中...\n');
|
||||
|
||||
try {
|
||||
// 加载配置
|
||||
const config = this.loadConfig();
|
||||
if (!config) {
|
||||
console.log('❌ 配置加载失败,请先配置邮件');
|
||||
console.log('💡 运行: npm run config');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if (!this.validateConfig(config)) {
|
||||
console.log('❌ 邮件配置不完整,请重新配置');
|
||||
console.log('💡 运行: npm run config');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ 配置验证通过');
|
||||
console.log(`📧 邮箱: ${config.email.config.imap.auth.user}`);
|
||||
console.log(`📬 通知发送到: ${config.email.config.to}\n`);
|
||||
|
||||
// 启动简化的邮件自动化
|
||||
this.automation = new SimplifiedEmailAutomation(config.email.config);
|
||||
|
||||
// 设置事件监听
|
||||
this.automation.on('commandExecuted', (data) => {
|
||||
console.log(`\n🎉 成功处理邮件命令 (邮件 ${data.emailSeq})`);
|
||||
console.log('💡 命令已复制到剪贴板,请在 Claude Code 中粘贴');
|
||||
});
|
||||
|
||||
this.automation.on('commandFailed', (data) => {
|
||||
console.log(`\n❌ 处理命令失败 (邮件 ${data.emailSeq}): ${data.error.message}`);
|
||||
});
|
||||
|
||||
// 启动服务
|
||||
await this.automation.start();
|
||||
|
||||
console.log('\n🎯 TaskPing 简化版运行中...');
|
||||
console.log('💌 现在你可以回复 TaskPing 邮件来发送命令了');
|
||||
console.log('📋 命令会自动复制到剪贴板,你只需要在 Claude Code 中粘贴');
|
||||
console.log('\n按 Ctrl+C 停止服务\n');
|
||||
|
||||
// 处理优雅关闭
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n🛑 正在停止 TaskPing...');
|
||||
if (this.automation) {
|
||||
await this.automation.stop();
|
||||
}
|
||||
console.log('✅ 服务已停止');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 保持进程运行
|
||||
process.stdin.resume();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 启动失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
try {
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
console.log('❌ 配置文件不存在');
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(this.configPath, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error('❌ 配置文件读取失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
validateConfig(config) {
|
||||
if (!config.email || !config.email.enabled) {
|
||||
console.log('❌ 邮件功能未启用');
|
||||
return false;
|
||||
}
|
||||
|
||||
const emailConfig = config.email.config;
|
||||
|
||||
// 检查必需字段
|
||||
const required = [
|
||||
'smtp.host',
|
||||
'smtp.auth.user',
|
||||
'smtp.auth.pass',
|
||||
'imap.host',
|
||||
'imap.auth.user',
|
||||
'imap.auth.pass',
|
||||
'to'
|
||||
];
|
||||
|
||||
for (const field of required) {
|
||||
if (!this.getNestedValue(emailConfig, field)) {
|
||||
console.log(`❌ 缺少必需配置: ${field}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getNestedValue(obj, path) {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
return current && current[key] !== undefined ? current[key] : undefined;
|
||||
}, obj);
|
||||
}
|
||||
|
||||
async status() {
|
||||
if (!this.automation) {
|
||||
console.log('❌ 服务未运行');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.automation.getStatus();
|
||||
console.log('📊 TaskPing 简化版状态:');
|
||||
console.log(` 运行状态: ${status.running ? '✅ 运行中' : '❌ 已停止'}`);
|
||||
console.log(` IMAP 连接: ${status.connected ? '✅ 已连接' : '❌ 未连接'}`);
|
||||
console.log(` 命令文件: ${status.commandFile}`);
|
||||
console.log(` 最新命令: ${status.lastCommandExists ? '✅ 有' : '❌ 无'}`);
|
||||
}
|
||||
|
||||
showHelp() {
|
||||
console.log(`
|
||||
🚀 TaskPing 简化版
|
||||
|
||||
用法: node simple-start.js [命令]
|
||||
|
||||
命令:
|
||||
start 启动邮件监听服务 (默认)
|
||||
status 显示服务状态
|
||||
help 显示帮助信息
|
||||
|
||||
配置:
|
||||
npm run config 交互式配置邮件
|
||||
|
||||
特点:
|
||||
• 🎯 专注核心功能,减少复杂性
|
||||
• 📧 稳定的邮件监听和解析
|
||||
• 📋 自动复制命令到剪贴板
|
||||
• 🔔 友好的通知提醒
|
||||
• 🛡️ 降级方案,减少权限依赖
|
||||
|
||||
使用流程:
|
||||
1. 配置邮箱: npm run config
|
||||
2. 启动服务: node simple-start.js
|
||||
3. 回复 TaskPing 邮件发送命令
|
||||
4. 在 Claude Code 中粘贴命令 (Cmd+V)
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理命令行参数
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'start';
|
||||
|
||||
const taskping = new SimpleTaskPing();
|
||||
|
||||
switch (command) {
|
||||
case 'start':
|
||||
taskping.start();
|
||||
break;
|
||||
case 'status':
|
||||
taskping.status();
|
||||
break;
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
taskping.showHelp();
|
||||
break;
|
||||
default:
|
||||
console.log(`未知命令: ${command}`);
|
||||
taskping.showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -0,0 +1,395 @@
|
|||
/**
|
||||
* Claude Code 专用自动化
|
||||
* 专门针对 Claude Code 的完全自动化解决方案
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const Logger = require('../core/logger');
|
||||
|
||||
class ClaudeAutomation {
|
||||
constructor() {
|
||||
this.logger = new Logger('ClaudeAutomation');
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全自动化发送命令到 Claude Code
|
||||
* @param {string} command - 要发送的命令
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
*/
|
||||
async sendCommand(command, sessionId = '') {
|
||||
try {
|
||||
this.logger.info(`Sending command to Claude Code: ${command.substring(0, 50)}...`);
|
||||
|
||||
// 首先复制命令到剪贴板
|
||||
await this._copyToClipboard(command);
|
||||
|
||||
// 然后执行完全自动化的粘贴和执行
|
||||
const success = await this._fullAutomation(command);
|
||||
|
||||
if (success) {
|
||||
this.logger.info('Command sent and executed successfully');
|
||||
return true;
|
||||
} else {
|
||||
// 如果失败,尝试备选方案
|
||||
return await this._fallbackAutomation(command);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Claude automation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制命令到剪贴板
|
||||
*/
|
||||
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) {
|
||||
this.logger.debug('Command copied to clipboard');
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Failed to copy to clipboard'));
|
||||
}
|
||||
});
|
||||
|
||||
pbcopy.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全自动化方案
|
||||
*/
|
||||
async _fullAutomation(command) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 定义可能的 Claude Code 应用名称
|
||||
set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"}
|
||||
set terminalApps to {"Terminal", "iTerm2", "iTerm", "Warp Terminal", "Warp"}
|
||||
set codeApps to {"Visual Studio Code", "Code", "Cursor", "Sublime Text", "Atom"}
|
||||
|
||||
-- 首先尝试找到 Claude Code
|
||||
set targetApp to null
|
||||
set appName to ""
|
||||
|
||||
-- 检查 Claude 应用
|
||||
repeat with app in claudeApps
|
||||
try
|
||||
if application process app exists then
|
||||
set targetApp to application process app
|
||||
set appName to app
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
-- 如果没找到 Claude,检查终端应用
|
||||
if targetApp is null then
|
||||
repeat with app in terminalApps
|
||||
try
|
||||
if application process app exists then
|
||||
set targetApp to application process app
|
||||
set appName to app
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
end if
|
||||
|
||||
-- 如果还没找到,检查代码编辑器
|
||||
if targetApp is null then
|
||||
repeat with app in codeApps
|
||||
try
|
||||
if application process app exists then
|
||||
set targetApp to application process app
|
||||
set appName to app
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if targetApp is not null then
|
||||
-- 激活应用
|
||||
set frontmost of targetApp to true
|
||||
delay 0.8
|
||||
|
||||
-- 等待应用完全激活
|
||||
repeat while (frontmost of targetApp) is false
|
||||
delay 0.1
|
||||
end repeat
|
||||
|
||||
-- 根据不同应用类型执行不同操作
|
||||
if appName contains "Claude" then
|
||||
-- Claude Code 特定操作
|
||||
try
|
||||
-- 尝试点击输入框
|
||||
click (first text field of window 1)
|
||||
delay 0.3
|
||||
on error
|
||||
-- 如果没有文本框,尝试按键导航
|
||||
key code 125 -- 向下箭头
|
||||
delay 0.2
|
||||
end try
|
||||
|
||||
-- 清空当前内容并粘贴新命令
|
||||
keystroke "a" using command down
|
||||
delay 0.2
|
||||
keystroke "v" using command down
|
||||
delay 0.5
|
||||
|
||||
-- 执行命令
|
||||
keystroke return
|
||||
|
||||
else if appName contains "Terminal" or appName contains "iTerm" or appName contains "Warp" then
|
||||
-- 终端应用操作
|
||||
delay 0.5
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
keystroke return
|
||||
|
||||
else
|
||||
-- 其他应用(代码编辑器等)
|
||||
delay 0.5
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
keystroke return
|
||||
end if
|
||||
|
||||
return "success:" & appName
|
||||
else
|
||||
return "no_app_found"
|
||||
end if
|
||||
end tell
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
let error = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
osascript.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0 && output.startsWith('success:')) {
|
||||
const appName = output.split(':')[1];
|
||||
this.logger.info(`Command successfully sent to ${appName}`);
|
||||
resolve(true);
|
||||
} else {
|
||||
this.logger.warn(`Full automation failed: ${output || error}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', (err) => {
|
||||
this.logger.error('AppleScript execution error:', err.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 备选自动化方案 - 更强制性的方法
|
||||
*/
|
||||
async _fallbackAutomation(command) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 更强制性的方案,直接输入文本
|
||||
const escapedCommand = command
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/\n/g, '\\n');
|
||||
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 获取当前前台应用
|
||||
set frontApp to first application process whose frontmost is true
|
||||
set appName to name of frontApp
|
||||
|
||||
-- 等待一下确保应用响应
|
||||
delay 1
|
||||
|
||||
-- 直接输入命令文本(不依赖剪贴板)
|
||||
try
|
||||
-- 先清空可能的现有内容
|
||||
keystroke "a" using command down
|
||||
delay 0.2
|
||||
|
||||
-- 输入命令
|
||||
keystroke "${escapedCommand}"
|
||||
delay 0.5
|
||||
|
||||
-- 执行
|
||||
keystroke return
|
||||
|
||||
return "typed_success:" & appName
|
||||
on error errorMsg
|
||||
-- 如果直接输入失败,尝试粘贴
|
||||
try
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
keystroke return
|
||||
return "paste_success:" & appName
|
||||
on error
|
||||
return "failed:" & errorMsg
|
||||
end try
|
||||
end try
|
||||
end tell
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0 && (output.includes('success'))) {
|
||||
this.logger.info(`Fallback automation succeeded: ${output}`);
|
||||
resolve(true);
|
||||
} else {
|
||||
this.logger.error(`Fallback automation failed: ${output}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 专门激活 Claude Code 应用
|
||||
*/
|
||||
async activateClaudeCode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"}
|
||||
|
||||
repeat with appName in claudeApps
|
||||
try
|
||||
if application process appName exists then
|
||||
set frontmost of application process appName to true
|
||||
return "activated:" & appName
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
return "not_found"
|
||||
end tell
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0 && output.startsWith('activated:')) {
|
||||
this.logger.info('Claude Code activated successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.logger.warn('Could not activate Claude Code');
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查系统权限并尝试请求
|
||||
*/
|
||||
async requestPermissions() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试一个简单的操作来触发权限请求
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
try
|
||||
set frontApp to name of first application process whose frontmost is true
|
||||
return "permission_granted"
|
||||
on error
|
||||
return "permission_denied"
|
||||
end try
|
||||
end tell
|
||||
`;
|
||||
|
||||
const result = await this._runAppleScript(script);
|
||||
return result === 'permission_granted';
|
||||
} catch (error) {
|
||||
this.logger.error('Permission check failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _runAppleScript(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
let error = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
osascript.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output.trim());
|
||||
} else {
|
||||
reject(new Error(error || `Exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态信息
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
platform: process.platform,
|
||||
supported: process.platform === 'darwin',
|
||||
name: 'Claude Code Automation'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClaudeAutomation;
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* Clipboard Automation
|
||||
* 通过剪贴板和键盘自动化来发送命令到Claude Code
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const Logger = require('../core/logger');
|
||||
|
||||
class ClipboardAutomation {
|
||||
constructor() {
|
||||
this.logger = new Logger('ClipboardAutomation');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送命令到Claude Code(通过剪贴板)
|
||||
* @param {string} command - 要发送的命令
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
*/
|
||||
async sendCommand(command) {
|
||||
try {
|
||||
// 第一步:将命令复制到剪贴板
|
||||
await this._copyToClipboard(command);
|
||||
|
||||
// 第二步:激活Claude Code并粘贴
|
||||
const success = await this._activateAndPaste();
|
||||
|
||||
if (success) {
|
||||
this.logger.info('Command sent successfully via clipboard automation');
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn('Failed to activate Claude Code, trying fallback');
|
||||
// 尝试通用方案
|
||||
return await this._sendToActiveWindow(command);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Clipboard automation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本复制到剪贴板
|
||||
*/
|
||||
async _copyToClipboard(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS
|
||||
const pbcopy = spawn('pbcopy');
|
||||
pbcopy.stdin.write(text);
|
||||
pbcopy.stdin.end();
|
||||
|
||||
pbcopy.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.logger.debug('Text copied to clipboard');
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Failed to copy to clipboard'));
|
||||
}
|
||||
});
|
||||
|
||||
pbcopy.on('error', reject);
|
||||
} else if (process.platform === 'linux') {
|
||||
// Linux (需要 xclip 或 xsel)
|
||||
const xclip = spawn('xclip', ['-selection', 'clipboard']);
|
||||
xclip.stdin.write(text);
|
||||
xclip.stdin.end();
|
||||
|
||||
xclip.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
// 尝试 xsel
|
||||
const xsel = spawn('xsel', ['--clipboard', '--input']);
|
||||
xsel.stdin.write(text);
|
||||
xsel.stdin.end();
|
||||
|
||||
xsel.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Failed to copy to clipboard (xclip/xsel not available)'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(new Error('Clipboard automation not supported on this platform'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活Claude Code并粘贴命令
|
||||
*/
|
||||
async _activateAndPaste() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 尝试找到 Claude Code 相关的应用
|
||||
set targetApps to {"Claude Code", "Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor"}
|
||||
set foundApp to null
|
||||
set appName to ""
|
||||
|
||||
repeat with currentApp in targetApps
|
||||
try
|
||||
if application process currentApp exists then
|
||||
set foundApp to application process currentApp
|
||||
set appName to currentApp
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
if foundApp is not null then
|
||||
-- 激活应用
|
||||
set frontmost of foundApp to true
|
||||
delay 0.5
|
||||
|
||||
-- 尝试找到输入框并点击
|
||||
try
|
||||
-- 对于一些应用,可能需要点击特定的输入区域
|
||||
if appName is "Claude Code" then
|
||||
-- Claude Code 特定的处理
|
||||
key code 125 -- 向下箭头,确保光标在输入框
|
||||
delay 0.2
|
||||
end if
|
||||
|
||||
-- 粘贴内容
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
|
||||
-- 发送命令(回车)
|
||||
keystroke return
|
||||
|
||||
return "success"
|
||||
on error errorMessage
|
||||
return "paste_failed: " & errorMessage
|
||||
end try
|
||||
else
|
||||
return "no_app_found"
|
||||
end if
|
||||
end tell
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
let error = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
osascript.stderr.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0 && output === 'success') {
|
||||
this.logger.debug('Successfully activated app and pasted command');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.logger.warn('AppleScript execution result:', { code, output, error });
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', (err) => {
|
||||
this.logger.error('AppleScript execution error:', err.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送到当前活动窗口(通用方案)
|
||||
*/
|
||||
async _sendToActiveWindow(command) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
-- 获取当前活动应用
|
||||
set activeApp to name of first application process whose frontmost is true
|
||||
|
||||
-- 粘贴命令到当前活动窗口
|
||||
keystroke "v" using command down
|
||||
delay 0.3
|
||||
keystroke return
|
||||
|
||||
return "sent_to_" & activeApp
|
||||
end tell
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.logger.debug('Command sent to active window:', output);
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持剪贴板自动化
|
||||
*/
|
||||
isSupported() {
|
||||
return process.platform === 'darwin' || process.platform === 'linux';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前剪贴板内容(用于测试)
|
||||
*/
|
||||
async getClipboardContent() {
|
||||
if (process.platform === 'darwin') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pbpaste = spawn('pbpaste');
|
||||
let content = '';
|
||||
|
||||
pbpaste.stdout.on('data', (data) => {
|
||||
content += data.toString();
|
||||
});
|
||||
|
||||
pbpaste.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(content);
|
||||
} else {
|
||||
reject(new Error('Failed to read clipboard'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClipboardAutomation;
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
/**
|
||||
* 简单自动化方案
|
||||
* 使用更简单的方式来处理邮件回复命令
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Logger = require('../core/logger');
|
||||
|
||||
class SimpleAutomation {
|
||||
constructor() {
|
||||
this.logger = new Logger('SimpleAutomation');
|
||||
this.commandFile = path.join(__dirname, '../data/current_command.txt');
|
||||
this.notificationSent = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送命令 - 使用多种简单方式
|
||||
* @param {string} command - 要发送的命令
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<boolean>} - 是否成功
|
||||
*/
|
||||
async sendCommand(command, sessionId = '') {
|
||||
try {
|
||||
// 方法1: 保存到文件,用户可以手动复制
|
||||
await this._saveCommandToFile(command, sessionId);
|
||||
|
||||
// 方法2: 复制到剪贴板
|
||||
const clipboardSuccess = await this._copyToClipboard(command);
|
||||
|
||||
// 方法3: 发送富通知(包含命令内容)
|
||||
const notificationSuccess = await this._sendRichNotification(command, sessionId);
|
||||
|
||||
// 方法4: 尝试简单的自动化(不依赖复杂权限)
|
||||
const autoSuccess = await this._trySimpleAutomation(command);
|
||||
|
||||
if (clipboardSuccess || notificationSuccess || autoSuccess) {
|
||||
this.logger.info('Command sent successfully via simple automation');
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn('All simple automation methods failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Simple automation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存命令到文件
|
||||
*/
|
||||
async _saveCommandToFile(command, sessionId) {
|
||||
try {
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
const content = `# TaskPing 邮件回复命令
|
||||
# 时间: ${timestamp}
|
||||
# 会话ID: ${sessionId}
|
||||
#
|
||||
# 请复制下面的命令到 Claude Code 中执行:
|
||||
|
||||
${command}
|
||||
|
||||
# ===============================
|
||||
# 执行完成后可以删除此文件
|
||||
`;
|
||||
|
||||
fs.writeFileSync(this.commandFile, content, 'utf8');
|
||||
this.logger.info(`Command saved to file: ${this.commandFile}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to save command to file:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
async _copyToClipboard(command) {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
const pbcopy = spawn('pbcopy');
|
||||
pbcopy.stdin.write(command);
|
||||
pbcopy.stdin.end();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
pbcopy.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.logger.info('Command copied to clipboard');
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
pbcopy.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to copy to clipboard:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送富通知
|
||||
*/
|
||||
async _sendRichNotification(command, sessionId) {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
||||
|
||||
// 创建详细的通知
|
||||
const script = `
|
||||
set commandText to "${command.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"
|
||||
|
||||
display notification "命令已复制到剪贴板,请粘贴到 Claude Code 中" with title "TaskPing - 新邮件命令" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "default"
|
||||
|
||||
-- 同时显示对话框(可选,用户可以取消)
|
||||
try
|
||||
set userChoice to display dialog "收到新的邮件命令:" & return & return & commandText buttons {"打开命令文件", "取消", "已粘贴"} default button "已粘贴" with title "TaskPing 邮件中继" giving up after 10
|
||||
|
||||
if button returned of userChoice is "打开命令文件" then
|
||||
do shell script "open -t '${this.commandFile}'"
|
||||
end if
|
||||
end try
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
this.logger.info('Rich notification sent successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.logger.warn('Rich notification failed, trying simple notification');
|
||||
this._sendSimpleNotification(command);
|
||||
resolve(true); // 即使失败也算成功,因为有备选方案
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', () => {
|
||||
this._sendSimpleNotification(command);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send rich notification:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送简单通知
|
||||
*/
|
||||
async _sendSimpleNotification(command) {
|
||||
try {
|
||||
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
||||
const script = `display notification "命令: ${shortCommand.replace(/"/g, '\\"')}" with title "TaskPing - 邮件命令" sound name "default"`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
osascript.on('close', () => {
|
||||
this.logger.info('Simple notification sent');
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn('Simple notification also failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试简单自动化(无需复杂权限)
|
||||
*/
|
||||
async _trySimpleAutomation(command) {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
// 只尝试最基本的操作,不强制要求权限
|
||||
const script = `
|
||||
try
|
||||
tell application "System Events"
|
||||
-- 尝试获取前台应用
|
||||
set frontApp to name of first application process whose frontmost is true
|
||||
|
||||
-- 如果是终端或代码编辑器,尝试粘贴
|
||||
if frontApp contains "Terminal" or frontApp contains "iTerm" or frontApp contains "Code" or frontApp contains "Claude" then
|
||||
keystroke "v" using command down
|
||||
delay 0.2
|
||||
keystroke return
|
||||
return "success"
|
||||
else
|
||||
return "not_target_app"
|
||||
end if
|
||||
end tell
|
||||
on error
|
||||
return "no_permission"
|
||||
end try
|
||||
`;
|
||||
|
||||
const osascript = spawn('osascript', ['-e', script]);
|
||||
let output = '';
|
||||
|
||||
osascript.stdout.on('data', (data) => {
|
||||
output += data.toString().trim();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
osascript.on('close', (code) => {
|
||||
if (code === 0 && output === 'success') {
|
||||
this.logger.info('Simple automation succeeded');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.logger.debug(`Simple automation result: ${output}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
osascript.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.logger.error('Simple automation error:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开命令文件
|
||||
*/
|
||||
async openCommandFile() {
|
||||
try {
|
||||
if (fs.existsSync(this.commandFile)) {
|
||||
if (process.platform === 'darwin') {
|
||||
spawn('open', ['-t', this.commandFile]);
|
||||
this.logger.info('Command file opened');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to open command file:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理命令文件
|
||||
*/
|
||||
cleanupCommandFile() {
|
||||
try {
|
||||
if (fs.existsSync(this.commandFile)) {
|
||||
fs.unlinkSync(this.commandFile);
|
||||
this.logger.info('Command file cleaned up');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to cleanup command file:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
supported: process.platform === 'darwin',
|
||||
commandFile: this.commandFile,
|
||||
commandFileExists: fs.existsSync(this.commandFile),
|
||||
lastCommand: this._getLastCommand()
|
||||
};
|
||||
}
|
||||
|
||||
_getLastCommand() {
|
||||
try {
|
||||
if (fs.existsSync(this.commandFile)) {
|
||||
const content = fs.readFileSync(this.commandFile, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const commandLines = lines.filter(line =>
|
||||
!line.startsWith('#') &&
|
||||
!line.startsWith('=') &&
|
||||
line.trim().length > 0
|
||||
);
|
||||
return commandLines.join('\n').trim();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get last command:', error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SimpleAutomation;
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* Base Notification Channel
|
||||
* Abstract base class for all notification channels
|
||||
*/
|
||||
|
||||
const Logger = require('../../core/logger');
|
||||
|
||||
class NotificationChannel {
|
||||
constructor(name, config = {}) {
|
||||
this.name = name;
|
||||
this.config = config;
|
||||
this.logger = new Logger(`Channel:${name}`);
|
||||
this.enabled = config.enabled !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification
|
||||
* @param {Object} notification - Notification object
|
||||
* @param {string} notification.type - Type: 'completed' | 'waiting'
|
||||
* @param {string} notification.title - Notification title
|
||||
* @param {string} notification.message - Notification message
|
||||
* @param {string} notification.project - Project name
|
||||
* @param {Object} notification.metadata - Additional metadata
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async send(notification) {
|
||||
if (!this.enabled) {
|
||||
this.logger.debug('Channel disabled, skipping notification');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.debug('Sending notification:', notification.type);
|
||||
|
||||
try {
|
||||
const result = await this._sendImpl(notification);
|
||||
if (result) {
|
||||
this.logger.info(`Notification sent successfully: ${notification.type}`);
|
||||
} else {
|
||||
this.logger.warn(`Failed to send notification: ${notification.type}`);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Error sending notification:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the channel configuration
|
||||
* @returns {Promise<boolean>} Test success status
|
||||
*/
|
||||
async test() {
|
||||
this.logger.debug('Testing channel...');
|
||||
|
||||
const testNotification = {
|
||||
type: 'completed',
|
||||
title: 'TaskPing Test',
|
||||
message: `Test notification from ${this.name} channel`,
|
||||
project: 'test-project',
|
||||
metadata: { test: true }
|
||||
};
|
||||
|
||||
return await this.send(testNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the channel supports command relay
|
||||
* @returns {boolean} Support status
|
||||
*/
|
||||
supportsRelay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming command from this channel (if supported)
|
||||
* @param {string} command - Command to execute
|
||||
* @param {Object} context - Command context
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async handleCommand(command, context = {}) {
|
||||
if (!this.supportsRelay()) {
|
||||
this.logger.warn('Channel does not support command relay');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.info('Received command:', command);
|
||||
// Implemented by subclasses
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation-specific send logic
|
||||
* Must be implemented by subclasses
|
||||
* @param {Object} notification - Notification object
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async _sendImpl(notification) {
|
||||
throw new Error('_sendImpl must be implemented by subclass');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate channel configuration
|
||||
* @returns {boolean} Validation status
|
||||
*/
|
||||
validateConfig() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel status
|
||||
* @returns {Object} Status information
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
name: this.name,
|
||||
enabled: this.enabled,
|
||||
configured: this.validateConfig(),
|
||||
supportsRelay: this.supportsRelay()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotificationChannel;
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
/**
|
||||
* Email Notification Channel
|
||||
* Sends notifications via email with reply support
|
||||
*/
|
||||
|
||||
const NotificationChannel = require('../base/channel');
|
||||
const nodemailer = require('nodemailer');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
class EmailChannel extends NotificationChannel {
|
||||
constructor(config = {}) {
|
||||
super('email', config);
|
||||
this.transporter = null;
|
||||
this.sessionsDir = path.join(__dirname, '../../data/sessions');
|
||||
this.templatesDir = path.join(__dirname, '../../assets/email-templates');
|
||||
|
||||
this._ensureDirectories();
|
||||
this._initializeTransporter();
|
||||
}
|
||||
|
||||
_ensureDirectories() {
|
||||
if (!fs.existsSync(this.sessionsDir)) {
|
||||
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(this.templatesDir)) {
|
||||
fs.mkdirSync(this.templatesDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
_initializeTransporter() {
|
||||
if (!this.config.smtp) {
|
||||
this.logger.warn('SMTP configuration not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: this.config.smtp.host,
|
||||
port: this.config.smtp.port,
|
||||
secure: this.config.smtp.secure || false,
|
||||
auth: {
|
||||
user: this.config.smtp.auth.user,
|
||||
pass: this.config.smtp.auth.pass
|
||||
},
|
||||
// 添加超时设置
|
||||
connectionTimeout: 10000,
|
||||
greetingTimeout: 10000,
|
||||
socketTimeout: 10000
|
||||
});
|
||||
|
||||
this.logger.debug('Email transporter initialized');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize email transporter:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async _sendImpl(notification) {
|
||||
if (!this.transporter) {
|
||||
throw new Error('Email transporter not initialized');
|
||||
}
|
||||
|
||||
if (!this.config.to) {
|
||||
throw new Error('Email recipient not configured');
|
||||
}
|
||||
|
||||
// 生成会话ID
|
||||
const sessionId = uuidv4();
|
||||
|
||||
// 创建会话记录
|
||||
await this._createSession(sessionId, notification);
|
||||
|
||||
// 生成邮件内容
|
||||
const emailContent = this._generateEmailContent(notification, sessionId);
|
||||
|
||||
const mailOptions = {
|
||||
from: this.config.from || this.config.smtp.auth.user,
|
||||
to: this.config.to,
|
||||
subject: emailContent.subject,
|
||||
html: emailContent.html,
|
||||
text: emailContent.text,
|
||||
// 添加自定义头部用于回复识别
|
||||
headers: {
|
||||
'X-TaskPing-Session-ID': sessionId,
|
||||
'X-TaskPing-Type': notification.type
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await this.transporter.sendMail(mailOptions);
|
||||
this.logger.info(`Email sent successfully to ${this.config.to}, Session: ${sessionId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send email:', error.message);
|
||||
// 清理失败的会话
|
||||
await this._removeSession(sessionId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _createSession(sessionId, notification) {
|
||||
const session = {
|
||||
id: sessionId,
|
||||
created: new Date().toISOString(),
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24小时后过期
|
||||
notification: {
|
||||
type: notification.type,
|
||||
project: notification.project,
|
||||
message: notification.message
|
||||
},
|
||||
status: 'waiting',
|
||||
commandCount: 0,
|
||||
maxCommands: 10
|
||||
};
|
||||
|
||||
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
|
||||
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
|
||||
|
||||
this.logger.debug(`Session created: ${sessionId}`);
|
||||
}
|
||||
|
||||
async _removeSession(sessionId) {
|
||||
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
|
||||
if (fs.existsSync(sessionFile)) {
|
||||
fs.unlinkSync(sessionFile);
|
||||
this.logger.debug(`Session removed: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
_generateEmailContent(notification, sessionId) {
|
||||
const template = this._getTemplate(notification.type);
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
|
||||
// 模板变量替换
|
||||
const variables = {
|
||||
project: notification.project,
|
||||
message: notification.message,
|
||||
timestamp: timestamp,
|
||||
sessionId: sessionId,
|
||||
type: notification.type === 'completed' ? '任务完成' : '等待输入'
|
||||
};
|
||||
|
||||
let subject = template.subject;
|
||||
let html = template.html;
|
||||
let text = template.text;
|
||||
|
||||
// 替换模板变量
|
||||
Object.keys(variables).forEach(key => {
|
||||
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
||||
subject = subject.replace(placeholder, variables[key]);
|
||||
html = html.replace(placeholder, variables[key]);
|
||||
text = text.replace(placeholder, variables[key]);
|
||||
});
|
||||
|
||||
return { subject, html, text };
|
||||
}
|
||||
|
||||
_getTemplate(type) {
|
||||
// 默认模板
|
||||
const templates = {
|
||||
completed: {
|
||||
subject: '[TaskPing] Claude Code 任务完成 - {{project}}',
|
||||
html: `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9f9f9;">
|
||||
<div style="background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #2c3e50; margin-top: 0; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
|
||||
🎉 Claude Code 任务完成
|
||||
</h2>
|
||||
|
||||
<div style="background-color: #ecf0f1; padding: 15px; border-radius: 6px; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #2c3e50;">
|
||||
<strong>项目:</strong> {{project}}<br>
|
||||
<strong>时间:</strong> {{timestamp}}<br>
|
||||
<strong>状态:</strong> {{type}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #e8f5e8; padding: 15px; border-radius: 6px; border-left: 4px solid #27ae60;">
|
||||
<p style="margin: 0; color: #2c3e50;">{{message}}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 25px 0; padding: 20px; background-color: #fff3cd; border-radius: 6px; border-left: 4px solid #ffc107;">
|
||||
<h3 style="margin-top: 0; color: #856404;">💡 如何继续对话</h3>
|
||||
<p style="margin: 10px 0; color: #856404;">
|
||||
要继续与 Claude Code 对话,请直接<strong>回复此邮件</strong>,在邮件正文中输入您的指令。
|
||||
</p>
|
||||
<div style="background-color: white; padding: 10px; border-radius: 4px; font-family: monospace; color: #495057;">
|
||||
示例回复:<br>
|
||||
• "请继续优化代码"<br>
|
||||
• "生成单元测试"<br>
|
||||
• "解释这个函数的作用"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d;">
|
||||
<p style="margin: 5px 0;">会话ID: <code>{{sessionId}}</code></p>
|
||||
<p style="margin: 5px 0;">🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期</p>
|
||||
<p style="margin: 5px 0;">📧 这是一封来自 TaskPing 的自动邮件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
text: `
|
||||
[TaskPing] Claude Code 任务完成 - {{project}}
|
||||
|
||||
项目: {{project}}
|
||||
时间: {{timestamp}}
|
||||
状态: {{type}}
|
||||
|
||||
消息: {{message}}
|
||||
|
||||
如何继续对话:
|
||||
要继续与 Claude Code 对话,请直接回复此邮件,在邮件正文中输入您的指令。
|
||||
|
||||
示例回复:
|
||||
• "请继续优化代码"
|
||||
• "生成单元测试"
|
||||
• "解释这个函数的作用"
|
||||
|
||||
会话ID: {{sessionId}}
|
||||
安全提示: 请勿转发此邮件,会话将在24小时后自动过期
|
||||
`
|
||||
},
|
||||
waiting: {
|
||||
subject: '[TaskPing] Claude Code 等待输入 - {{project}}',
|
||||
html: `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9f9f9;">
|
||||
<div style="background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #2c3e50; margin-top: 0; border-bottom: 2px solid #e74c3c; padding-bottom: 10px;">
|
||||
⏳ Claude Code 等待您的指导
|
||||
</h2>
|
||||
|
||||
<div style="background-color: #ecf0f1; padding: 15px; border-radius: 6px; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #2c3e50;">
|
||||
<strong>项目:</strong> {{project}}<br>
|
||||
<strong>时间:</strong> {{timestamp}}<br>
|
||||
<strong>状态:</strong> {{type}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fdf2e9; padding: 15px; border-radius: 6px; border-left: 4px solid #e67e22;">
|
||||
<p style="margin: 0; color: #2c3e50;">{{message}}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 25px 0; padding: 20px; background-color: #d1ecf1; border-radius: 6px; border-left: 4px solid #17a2b8;">
|
||||
<h3 style="margin-top: 0; color: #0c5460;">💬 请提供指导</h3>
|
||||
<p style="margin: 10px 0; color: #0c5460;">
|
||||
Claude 需要您的进一步指导。请<strong>回复此邮件</strong>告诉 Claude 下一步应该做什么。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d;">
|
||||
<p style="margin: 5px 0;">会话ID: <code>{{sessionId}}</code></p>
|
||||
<p style="margin: 5px 0;">🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期</p>
|
||||
<p style="margin: 5px 0;">📧 这是一封来自 TaskPing 的自动邮件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
text: `
|
||||
[TaskPing] Claude Code 等待输入 - {{project}}
|
||||
|
||||
项目: {{project}}
|
||||
时间: {{timestamp}}
|
||||
状态: {{type}}
|
||||
|
||||
消息: {{message}}
|
||||
|
||||
Claude 需要您的进一步指导。请回复此邮件告诉 Claude 下一步应该做什么。
|
||||
|
||||
会话ID: {{sessionId}}
|
||||
安全提示: 请勿转发此邮件,会话将在24小时后自动过期
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
return templates[type] || templates.completed;
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
if (!this.config.smtp) {
|
||||
return { valid: false, error: 'SMTP configuration required' };
|
||||
}
|
||||
|
||||
if (!this.config.smtp.host) {
|
||||
return { valid: false, error: 'SMTP host required' };
|
||||
}
|
||||
|
||||
if (!this.config.smtp.auth || !this.config.smtp.auth.user || !this.config.smtp.auth.pass) {
|
||||
return { valid: false, error: 'SMTP authentication required' };
|
||||
}
|
||||
|
||||
if (!this.config.to) {
|
||||
return { valid: false, error: 'Recipient email required' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
async test() {
|
||||
try {
|
||||
if (!this.transporter) {
|
||||
throw new Error('Email transporter not initialized');
|
||||
}
|
||||
|
||||
// 验证 SMTP 连接
|
||||
await this.transporter.verify();
|
||||
|
||||
// 发送测试邮件
|
||||
const testNotification = {
|
||||
type: 'completed',
|
||||
title: 'TaskPing 测试',
|
||||
message: '这是一封测试邮件,用于验证邮件通知功能是否正常工作。',
|
||||
project: 'TaskPing-Test',
|
||||
metadata: {
|
||||
test: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
const result = await this._sendImpl(testNotification);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Email test failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
const baseStatus = super.getStatus();
|
||||
return {
|
||||
...baseStatus,
|
||||
configured: this.validateConfig().valid,
|
||||
supportsRelay: true,
|
||||
smtp: {
|
||||
host: this.config.smtp?.host || 'not configured',
|
||||
port: this.config.smtp?.port || 'not configured',
|
||||
secure: this.config.smtp?.secure || false
|
||||
},
|
||||
recipient: this.config.to || 'not configured'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmailChannel;
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Desktop Notification Channel
|
||||
* Sends notifications to the local desktop
|
||||
*/
|
||||
|
||||
const NotificationChannel = require('../base/channel');
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
class DesktopChannel extends NotificationChannel {
|
||||
constructor(config = {}) {
|
||||
super('desktop', config);
|
||||
this.platform = process.platform;
|
||||
this.soundsDir = path.join(__dirname, '../../assets/sounds');
|
||||
}
|
||||
|
||||
async _sendImpl(notification) {
|
||||
const { title, message } = notification;
|
||||
const sound = this._getSoundForType(notification.type);
|
||||
|
||||
switch (this.platform) {
|
||||
case 'darwin':
|
||||
return this._sendMacOS(title, message, sound);
|
||||
case 'linux':
|
||||
return this._sendLinux(title, message, sound);
|
||||
case 'win32':
|
||||
return this._sendWindows(title, message, sound);
|
||||
default:
|
||||
this.logger.warn(`Platform ${this.platform} not supported`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_getSoundForType(type) {
|
||||
const soundMap = {
|
||||
completed: this.config.completedSound || 'Glass',
|
||||
waiting: this.config.waitingSound || 'Tink'
|
||||
};
|
||||
return soundMap[type] || 'Glass';
|
||||
}
|
||||
|
||||
_sendMacOS(title, message, sound) {
|
||||
try {
|
||||
// Try terminal-notifier first
|
||||
try {
|
||||
const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "taskping"`;
|
||||
execSync(cmd, { timeout: 3000 });
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Fallback to osascript
|
||||
const script = `display notification "${message}" with title "${title}"`;
|
||||
execSync(`osascript -e '${script}'`, { timeout: 3000 });
|
||||
|
||||
// Play sound separately
|
||||
this._playSound(sound);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('macOS notification failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_sendLinux(title, message, sound) {
|
||||
try {
|
||||
execSync(`notify-send "${title}" "${message}" -t 10000`, { timeout: 3000 });
|
||||
this._playSound(sound);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Linux notification failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_sendWindows(title, message, sound) {
|
||||
try {
|
||||
const script = `
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
||||
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
||||
$xml = [xml] $template.GetXml()
|
||||
$xml.toast.visual.binding.text[0].AppendChild($xml.CreateTextNode("${title}")) > $null
|
||||
$xml.toast.visual.binding.text[1].AppendChild($xml.CreateTextNode("${message}")) > $null
|
||||
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("TaskPing").Show($toast)
|
||||
`;
|
||||
|
||||
execSync(`powershell -Command "${script}"`, { timeout: 5000 });
|
||||
this._playSound(sound);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Windows notification failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_playSound(soundName) {
|
||||
if (!soundName || soundName === 'default') return;
|
||||
|
||||
try {
|
||||
if (this.platform === 'darwin') {
|
||||
const soundPath = `/System/Library/Sounds/${soundName}.aiff`;
|
||||
const audioProcess = spawn('afplay', [soundPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
audioProcess.unref();
|
||||
} else if (this.platform === 'linux') {
|
||||
const soundPath = `/usr/share/sounds/freedesktop/stereo/${soundName.toLowerCase()}.oga`;
|
||||
const audioProcess = spawn('paplay', [soundPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
audioProcess.unref();
|
||||
} else if (this.platform === 'win32') {
|
||||
const audioProcess = spawn('powershell', ['-c', `[console]::beep(800,300)`], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
audioProcess.unref();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug('Sound playback failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
// Desktop notifications don't require configuration
|
||||
return true;
|
||||
}
|
||||
|
||||
getAvailableSounds() {
|
||||
const sounds = {
|
||||
'System Sounds': ['Glass', 'Tink', 'Ping', 'Pop', 'Basso', 'Blow', 'Bottle',
|
||||
'Frog', 'Funk', 'Hero', 'Morse', 'Purr', 'Sosumi', 'Submarine'],
|
||||
'Alert Sounds': ['Beep', 'Boop', 'Sosumi', 'Tink', 'Glass'],
|
||||
'Nature Sounds': ['Frog', 'Submarine'],
|
||||
'Musical Sounds': ['Funk', 'Hero', 'Morse', 'Sosumi']
|
||||
};
|
||||
|
||||
// Add custom sounds from assets directory
|
||||
try {
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(this.soundsDir)) {
|
||||
const customSounds = fs.readdirSync(this.soundsDir)
|
||||
.filter(file => /\.(wav|mp3|m4a|aiff|ogg)$/i.test(file))
|
||||
.map(file => path.basename(file, path.extname(file)));
|
||||
|
||||
if (customSounds.length > 0) {
|
||||
sounds['Custom Sounds'] = customSounds;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug('Failed to load custom sounds:', error.message);
|
||||
}
|
||||
|
||||
return sounds;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DesktopChannel;
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
const Logger = require('./core/logger');
|
||||
const logger = new Logger('ConfigManager');
|
||||
|
||||
class ConfigManager {
|
||||
constructor() {
|
||||
this.configPath = path.join(__dirname, '../config/channels.json');
|
||||
this.rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const data = fs.readFileSync(this.configPath, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load config:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async saveConfig(config) {
|
||||
try {
|
||||
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
||||
logger.info('Configuration saved successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to save config:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async question(prompt) {
|
||||
return new Promise((resolve) => {
|
||||
this.rl.question(prompt, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async configureEmail() {
|
||||
console.log('\n📧 Email Configuration Setup\n');
|
||||
|
||||
const config = await this.loadConfig();
|
||||
|
||||
console.log('Please enter your email configuration:');
|
||||
console.log('(Press Enter to keep current value)\n');
|
||||
|
||||
// SMTP Configuration
|
||||
console.log('--- SMTP Settings (for sending emails) ---');
|
||||
const smtpHost = await this.question(`SMTP Host [${config.email.config.smtp.host || 'smtp.gmail.com'}]: `);
|
||||
config.email.config.smtp.host = smtpHost || config.email.config.smtp.host || 'smtp.gmail.com';
|
||||
|
||||
const smtpPort = await this.question(`SMTP Port [${config.email.config.smtp.port || 587}]: `);
|
||||
config.email.config.smtp.port = parseInt(smtpPort) || config.email.config.smtp.port || 587;
|
||||
|
||||
const smtpUser = await this.question(`Email Address [${config.email.config.smtp.auth.user || ''}]: `);
|
||||
config.email.config.smtp.auth.user = smtpUser || config.email.config.smtp.auth.user;
|
||||
|
||||
const smtpPass = await this.question(`App Password [${config.email.config.smtp.auth.pass ? '***' : ''}]: `);
|
||||
if (smtpPass) {
|
||||
config.email.config.smtp.auth.pass = smtpPass;
|
||||
}
|
||||
|
||||
// IMAP Configuration
|
||||
console.log('\n--- IMAP Settings (for receiving emails) ---');
|
||||
const imapHost = await this.question(`IMAP Host [${config.email.config.imap.host || 'imap.gmail.com'}]: `);
|
||||
config.email.config.imap.host = imapHost || config.email.config.imap.host || 'imap.gmail.com';
|
||||
|
||||
const imapPort = await this.question(`IMAP Port [${config.email.config.imap.port || 993}]: `);
|
||||
config.email.config.imap.port = parseInt(imapPort) || config.email.config.imap.port || 993;
|
||||
|
||||
// Use same credentials as SMTP by default
|
||||
config.email.config.imap.auth.user = config.email.config.smtp.auth.user;
|
||||
config.email.config.imap.auth.pass = config.email.config.smtp.auth.pass;
|
||||
|
||||
// Email addresses
|
||||
console.log('\n--- Email Addresses ---');
|
||||
const fromEmail = await this.question(`From Address [${config.email.config.from || `TaskPing <${config.email.config.smtp.auth.user}>`}]: `);
|
||||
config.email.config.from = fromEmail || config.email.config.from || `TaskPing <${config.email.config.smtp.auth.user}>`;
|
||||
|
||||
const toEmail = await this.question(`To Address [${config.email.config.to || config.email.config.smtp.auth.user}]: `);
|
||||
config.email.config.to = toEmail || config.email.config.to || config.email.config.smtp.auth.user;
|
||||
|
||||
// Enable email
|
||||
const enable = await this.question('\nEnable email notifications? (y/n) [y]: ');
|
||||
config.email.enabled = enable.toLowerCase() !== 'n';
|
||||
|
||||
await this.saveConfig(config);
|
||||
console.log('\n✅ Email configuration completed!');
|
||||
|
||||
if (config.email.enabled) {
|
||||
console.log('\n📌 Important: Make sure to use an App Password (not your regular password)');
|
||||
console.log(' Gmail: https://support.google.com/accounts/answer/185833');
|
||||
console.log(' Outlook: https://support.microsoft.com/en-us/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944');
|
||||
}
|
||||
}
|
||||
|
||||
async showCurrentConfig() {
|
||||
const config = await this.loadConfig();
|
||||
console.log('\n📋 Current Configuration:\n');
|
||||
|
||||
for (const [channel, settings] of Object.entries(config)) {
|
||||
console.log(`${channel}:`);
|
||||
console.log(` Enabled: ${settings.enabled ? '✅' : '❌'}`);
|
||||
|
||||
if (channel === 'email' && settings.config.smtp.auth.user) {
|
||||
console.log(` Email: ${settings.config.smtp.auth.user}`);
|
||||
console.log(` SMTP: ${settings.config.smtp.host}:${settings.config.smtp.port}`);
|
||||
console.log(` IMAP: ${settings.config.imap.host}:${settings.config.imap.port}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
async toggleChannel(channelName) {
|
||||
const config = await this.loadConfig();
|
||||
|
||||
if (!config[channelName]) {
|
||||
console.log(`❌ Channel "${channelName}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
config[channelName].enabled = !config[channelName].enabled;
|
||||
await this.saveConfig(config);
|
||||
|
||||
console.log(`${channelName}: ${config[channelName].enabled ? '✅ Enabled' : '❌ Disabled'}`);
|
||||
}
|
||||
|
||||
async interactiveMenu() {
|
||||
console.log('\n🛠️ TaskPing Configuration Manager\n');
|
||||
|
||||
while (true) {
|
||||
console.log('\nChoose an option:');
|
||||
console.log('1. Configure Email');
|
||||
console.log('2. Show Current Configuration');
|
||||
console.log('3. Toggle Channel (enable/disable)');
|
||||
console.log('4. Exit');
|
||||
|
||||
const choice = await this.question('\nYour choice (1-4): ');
|
||||
|
||||
switch (choice) {
|
||||
case '1':
|
||||
await this.configureEmail();
|
||||
break;
|
||||
case '2':
|
||||
await this.showCurrentConfig();
|
||||
break;
|
||||
case '3':
|
||||
const channel = await this.question('Channel name (desktop/email/discord/telegram/whatsapp/feishu): ');
|
||||
await this.toggleChannel(channel);
|
||||
break;
|
||||
case '4':
|
||||
console.log('\n👋 Goodbye!');
|
||||
this.rl.close();
|
||||
return;
|
||||
default:
|
||||
console.log('Invalid choice. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run as standalone script
|
||||
if (require.main === module) {
|
||||
const manager = new ConfigManager();
|
||||
manager.interactiveMenu().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = ConfigManager;
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* TaskPing Configuration Manager
|
||||
* Handles loading, merging, and saving configurations
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Logger = require('./logger');
|
||||
|
||||
class ConfigManager {
|
||||
constructor(configDir = null) {
|
||||
this.logger = new Logger('Config');
|
||||
this.configDir = configDir || path.join(__dirname, '../../config');
|
||||
this.userConfigPath = path.join(this.configDir, 'user.json');
|
||||
this.defaultConfigPath = path.join(this.configDir, 'default.json');
|
||||
this.channelsConfigPath = path.join(this.configDir, 'channels.json');
|
||||
|
||||
this._config = null;
|
||||
this._channels = null;
|
||||
}
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
language: 'zh-CN',
|
||||
sound: {
|
||||
completed: 'Glass',
|
||||
waiting: 'Tink'
|
||||
},
|
||||
enabled: true,
|
||||
timeout: 5,
|
||||
customMessages: {
|
||||
completed: null,
|
||||
waiting: null
|
||||
},
|
||||
channels: {
|
||||
desktop: {
|
||||
enabled: true,
|
||||
priority: 1
|
||||
}
|
||||
},
|
||||
relay: {
|
||||
enabled: false,
|
||||
port: 3000,
|
||||
auth: {
|
||||
enabled: false,
|
||||
token: null
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getDefaultChannelsConfig() {
|
||||
return {
|
||||
desktop: {
|
||||
type: 'local',
|
||||
enabled: true,
|
||||
config: {}
|
||||
},
|
||||
email: {
|
||||
type: 'email',
|
||||
enabled: false,
|
||||
config: {
|
||||
smtp: {
|
||||
host: '',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: '',
|
||||
pass: ''
|
||||
}
|
||||
},
|
||||
from: '',
|
||||
to: []
|
||||
}
|
||||
},
|
||||
discord: {
|
||||
type: 'chat',
|
||||
enabled: false,
|
||||
config: {
|
||||
webhook: '',
|
||||
username: 'TaskPing',
|
||||
avatar: null
|
||||
}
|
||||
},
|
||||
telegram: {
|
||||
type: 'chat',
|
||||
enabled: false,
|
||||
config: {
|
||||
token: '',
|
||||
chatId: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
load() {
|
||||
this.logger.debug('Loading configuration...');
|
||||
|
||||
// Load default config
|
||||
let defaultConfig = this.getDefaultConfig();
|
||||
try {
|
||||
if (fs.existsSync(this.defaultConfigPath)) {
|
||||
const fileConfig = JSON.parse(fs.readFileSync(this.defaultConfigPath, 'utf8'));
|
||||
defaultConfig = { ...defaultConfig, ...fileConfig };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to load default config:', error.message);
|
||||
}
|
||||
|
||||
// Load user config
|
||||
let userConfig = {};
|
||||
try {
|
||||
if (fs.existsSync(this.userConfigPath)) {
|
||||
userConfig = JSON.parse(fs.readFileSync(this.userConfigPath, 'utf8'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to load user config:', error.message);
|
||||
}
|
||||
|
||||
// Merge configs
|
||||
this._config = this._deepMerge(defaultConfig, userConfig);
|
||||
|
||||
// Load channels config
|
||||
this._channels = this.getDefaultChannelsConfig();
|
||||
try {
|
||||
if (fs.existsSync(this.channelsConfigPath)) {
|
||||
const fileChannels = JSON.parse(fs.readFileSync(this.channelsConfigPath, 'utf8'));
|
||||
this._channels = { ...this._channels, ...fileChannels };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to load channels config:', error.message);
|
||||
}
|
||||
|
||||
this.logger.info('Configuration loaded successfully');
|
||||
return this._config;
|
||||
}
|
||||
|
||||
save() {
|
||||
this.logger.debug('Saving user configuration...');
|
||||
|
||||
try {
|
||||
// Ensure config directory exists
|
||||
if (!fs.existsSync(this.configDir)) {
|
||||
fs.mkdirSync(this.configDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save user config
|
||||
fs.writeFileSync(this.userConfigPath, JSON.stringify(this._config, null, 2));
|
||||
|
||||
// Save channels config
|
||||
fs.writeFileSync(this.channelsConfigPath, JSON.stringify(this._channels, null, 2));
|
||||
|
||||
this.logger.info('Configuration saved successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to save configuration:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get(key, defaultValue = undefined) {
|
||||
if (!this._config) {
|
||||
this.load();
|
||||
}
|
||||
|
||||
const keys = key.split('.');
|
||||
let value = this._config;
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k];
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (!this._config) {
|
||||
this.load();
|
||||
}
|
||||
|
||||
const keys = key.split('.');
|
||||
let target = this._config;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!(k in target) || typeof target[k] !== 'object') {
|
||||
target[k] = {};
|
||||
}
|
||||
target = target[k];
|
||||
}
|
||||
|
||||
target[keys[keys.length - 1]] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
getChannel(channelName) {
|
||||
if (!this._channels) {
|
||||
this.load();
|
||||
}
|
||||
return this._channels[channelName];
|
||||
}
|
||||
|
||||
setChannel(channelName, config) {
|
||||
if (!this._channels) {
|
||||
this.load();
|
||||
}
|
||||
this._channels[channelName] = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
getProjectName() {
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
// Try to get git repository name first
|
||||
const gitName = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf8' }).trim();
|
||||
if (gitName) {
|
||||
return path.basename(gitName);
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a git repository
|
||||
}
|
||||
|
||||
// Fall back to current directory name
|
||||
return path.basename(process.cwd());
|
||||
}
|
||||
|
||||
_deepMerge(target, source) {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
||||
result[key] = this._deepMerge(result[key] || {}, source[key]);
|
||||
} else {
|
||||
result[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConfigManager;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* TaskPing Logger
|
||||
* Centralized logging utility
|
||||
*/
|
||||
|
||||
class Logger {
|
||||
constructor(namespace = 'TaskPing') {
|
||||
this.namespace = namespace;
|
||||
this.logLevel = process.env.TASKPING_LOG_LEVEL || 'info';
|
||||
}
|
||||
|
||||
_log(level, message, ...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = `[${timestamp}] [${this.namespace}] [${level.toUpperCase()}]`;
|
||||
|
||||
if (this._shouldLog(level)) {
|
||||
console.log(prefix, message, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
_shouldLog(level) {
|
||||
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
return levels[level] >= levels[this.logLevel];
|
||||
}
|
||||
|
||||
debug(message, ...args) {
|
||||
this._log('debug', message, ...args);
|
||||
}
|
||||
|
||||
info(message, ...args) {
|
||||
this._log('info', message, ...args);
|
||||
}
|
||||
|
||||
warn(message, ...args) {
|
||||
this._log('warn', message, ...args);
|
||||
}
|
||||
|
||||
error(message, ...args) {
|
||||
this._log('error', message, ...args);
|
||||
}
|
||||
|
||||
child(namespace) {
|
||||
return new Logger(`${this.namespace}:${namespace}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* TaskPing Core Notifier
|
||||
* Central notification orchestrator that manages multiple channels
|
||||
*/
|
||||
|
||||
const Logger = require('./logger');
|
||||
const ConfigManager = require('./config');
|
||||
|
||||
class Notifier {
|
||||
constructor(configManager = null) {
|
||||
this.logger = new Logger('Notifier');
|
||||
this.config = configManager || new ConfigManager();
|
||||
this.channels = new Map();
|
||||
this.i18n = null;
|
||||
|
||||
this._loadI18n();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a notification channel
|
||||
* @param {string} name - Channel name
|
||||
* @param {NotificationChannel} channel - Channel instance
|
||||
*/
|
||||
registerChannel(name, channel) {
|
||||
this.logger.debug(`Registering channel: ${name}`);
|
||||
this.channels.set(name, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default channels
|
||||
*/
|
||||
async initializeChannels() {
|
||||
this.logger.debug('Initializing channels...');
|
||||
|
||||
// Load desktop channel
|
||||
const DesktopChannel = require('../channels/local/desktop');
|
||||
const desktopConfig = this.config.getChannel('desktop');
|
||||
if (desktopConfig && desktopConfig.enabled) {
|
||||
const desktop = new DesktopChannel(desktopConfig.config || {});
|
||||
desktop.config.completedSound = this.config.get('sound.completed');
|
||||
desktop.config.waitingSound = this.config.get('sound.waiting');
|
||||
this.registerChannel('desktop', desktop);
|
||||
}
|
||||
|
||||
// Load email channel
|
||||
const EmailChannel = require('../channels/email/smtp');
|
||||
const emailConfig = this.config.getChannel('email');
|
||||
if (emailConfig && emailConfig.enabled) {
|
||||
const email = new EmailChannel(emailConfig.config || {});
|
||||
this.registerChannel('email', email);
|
||||
}
|
||||
|
||||
// TODO: Load other channels based on configuration
|
||||
// Discord, Telegram, etc.
|
||||
|
||||
this.logger.info(`Initialized ${this.channels.size} channels`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to all enabled channels
|
||||
* @param {string} type - Notification type: 'completed' | 'waiting'
|
||||
* @param {Object} metadata - Additional metadata
|
||||
* @returns {Promise<Object>} Results from all channels
|
||||
*/
|
||||
async notify(type, metadata = {}) {
|
||||
if (!this.config.get('enabled', true)) {
|
||||
this.logger.debug('Notifications disabled');
|
||||
return { success: false, reason: 'disabled' };
|
||||
}
|
||||
|
||||
const notification = this._buildNotification(type, metadata);
|
||||
this.logger.info(`Sending ${type} notification for project: ${notification.project}`);
|
||||
|
||||
const results = {};
|
||||
const promises = [];
|
||||
|
||||
// Send to all channels in parallel
|
||||
for (const [name, channel] of this.channels) {
|
||||
if (channel.enabled) {
|
||||
promises.push(
|
||||
channel.send(notification)
|
||||
.then(success => ({ name, success }))
|
||||
.catch(error => ({ name, success: false, error: error.message }))
|
||||
);
|
||||
} else {
|
||||
results[name] = { success: false, reason: 'disabled' };
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all channels to complete
|
||||
const channelResults = await Promise.all(promises);
|
||||
channelResults.forEach(result => {
|
||||
results[result.name] = result;
|
||||
});
|
||||
|
||||
const successCount = Object.values(results).filter(r => r.success).length;
|
||||
this.logger.info(`Notification sent to ${successCount}/${this.channels.size} channels`);
|
||||
|
||||
return {
|
||||
success: successCount > 0,
|
||||
results,
|
||||
notification
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build notification object from type and metadata
|
||||
* @param {string} type - Notification type
|
||||
* @param {Object} metadata - Additional metadata
|
||||
* @returns {Object} Notification object
|
||||
*/
|
||||
_buildNotification(type, metadata = {}) {
|
||||
const project = metadata.project || this.config.getProjectName();
|
||||
const lang = this.config.get('language', 'zh-CN');
|
||||
const content = this._getNotificationContent(type, lang);
|
||||
|
||||
// Replace project placeholder
|
||||
const message = content.message.replace('{project}', project);
|
||||
|
||||
// Use custom message if configured
|
||||
const customMessage = this.config.get(`customMessages.${type}`);
|
||||
const finalMessage = customMessage ? customMessage.replace('{project}', project) : message;
|
||||
|
||||
return {
|
||||
type,
|
||||
title: content.title,
|
||||
message: finalMessage,
|
||||
project,
|
||||
metadata: {
|
||||
timestamp: new Date().toISOString(),
|
||||
language: lang,
|
||||
...metadata
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification content for type and language
|
||||
* @param {string} type - Notification type
|
||||
* @param {string} lang - Language code
|
||||
* @returns {Object} Content object with title and message
|
||||
*/
|
||||
_getNotificationContent(type, lang) {
|
||||
if (!this.i18n) {
|
||||
this._loadI18n();
|
||||
}
|
||||
|
||||
const langData = this.i18n[lang] || this.i18n['zh-CN'];
|
||||
return langData[type] || langData.completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load internationalization data
|
||||
*/
|
||||
_loadI18n() {
|
||||
this.i18n = {
|
||||
'zh-CN': {
|
||||
completed: {
|
||||
title: 'Claude Code - 任务完成',
|
||||
message: '[{project}] 任务已完成,Claude正在等待下一步指令'
|
||||
},
|
||||
waiting: {
|
||||
title: 'Claude Code - 等待输入',
|
||||
message: '[{project}] Claude需要您的进一步指导'
|
||||
}
|
||||
},
|
||||
'en': {
|
||||
completed: {
|
||||
title: 'Claude Code - Task Completed',
|
||||
message: '[{project}] Task completed, Claude is waiting for next instruction'
|
||||
},
|
||||
waiting: {
|
||||
title: 'Claude Code - Waiting for Input',
|
||||
message: '[{project}] Claude needs your further guidance'
|
||||
}
|
||||
},
|
||||
'ja': {
|
||||
completed: {
|
||||
title: 'Claude Code - タスク完了',
|
||||
message: '[{project}] タスクが完了しました。Claudeが次の指示を待っています'
|
||||
},
|
||||
waiting: {
|
||||
title: 'Claude Code - 入力待ち',
|
||||
message: '[{project}] Claudeにはあなたのさらなるガイダンスが必要です'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test all channels
|
||||
* @returns {Promise<Object>} Test results
|
||||
*/
|
||||
async test() {
|
||||
this.logger.info('Testing all channels...');
|
||||
|
||||
const results = {};
|
||||
for (const [name, channel] of this.channels) {
|
||||
try {
|
||||
const success = await channel.test();
|
||||
results[name] = { success };
|
||||
this.logger.info(`Channel ${name}: ${success ? 'PASS' : 'FAIL'}`);
|
||||
} catch (error) {
|
||||
results[name] = { success: false, error: error.message };
|
||||
this.logger.error(`Channel ${name}: ERROR - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all channels
|
||||
* @returns {Object} Status information
|
||||
*/
|
||||
getStatus() {
|
||||
const channels = {};
|
||||
for (const [name, channel] of this.channels) {
|
||||
channels[name] = channel.getStatus();
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: this.config.get('enabled', true),
|
||||
channels,
|
||||
config: {
|
||||
language: this.config.get('language'),
|
||||
sound: this.config.get('sound'),
|
||||
customMessages: this.config.get('customMessages')
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Notifier;
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing Daemon Service
|
||||
* 后台守护进程,用于监听邮件和处理远程命令
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawn, exec } = require('child_process');
|
||||
const Logger = require('../core/logger');
|
||||
const ConfigManager = require('../core/config');
|
||||
|
||||
class TaskPingDaemon {
|
||||
constructor() {
|
||||
this.logger = new Logger('Daemon');
|
||||
this.config = new ConfigManager();
|
||||
this.pidFile = path.join(__dirname, '../data/taskping.pid');
|
||||
this.logFile = path.join(__dirname, '../data/daemon.log');
|
||||
this.relayService = null;
|
||||
this.isRunning = false;
|
||||
|
||||
// 确保数据目录存在
|
||||
const dataDir = path.dirname(this.pidFile);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async start(detached = true) {
|
||||
try {
|
||||
// 检查是否已经运行
|
||||
if (this.isAlreadyRunning()) {
|
||||
console.log('❌ TaskPing daemon 已经在运行中');
|
||||
console.log('💡 使用 "taskping daemon stop" 停止现有服务');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (detached) {
|
||||
// 以守护进程模式启动
|
||||
await this.startDetached();
|
||||
} else {
|
||||
// 直接在当前进程运行
|
||||
await this.startForeground();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to start daemon:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async startDetached() {
|
||||
console.log('🚀 启动 TaskPing 守护进程...');
|
||||
|
||||
// 创建子进程
|
||||
const child = spawn(process.execPath, [__filename, '--foreground'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// 重定向日志
|
||||
const logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
||||
child.stdout.pipe(logStream);
|
||||
child.stderr.pipe(logStream);
|
||||
|
||||
// 保存 PID
|
||||
fs.writeFileSync(this.pidFile, child.pid.toString());
|
||||
|
||||
// 分离子进程
|
||||
child.unref();
|
||||
|
||||
console.log(`✅ TaskPing 守护进程已启动 (PID: ${child.pid})`);
|
||||
console.log(`📝 日志文件: ${this.logFile}`);
|
||||
console.log('💡 使用 "taskping daemon status" 查看状态');
|
||||
console.log('💡 使用 "taskping daemon stop" 停止服务');
|
||||
}
|
||||
|
||||
async startForeground() {
|
||||
console.log('🚀 TaskPing 守护进程启动中...');
|
||||
|
||||
this.isRunning = true;
|
||||
process.title = 'taskping-daemon';
|
||||
|
||||
// 加载配置
|
||||
this.config.load();
|
||||
|
||||
// 初始化邮件中继服务
|
||||
const emailConfig = this.config.getChannel('email');
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
this.logger.warn('Email channel not configured or disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const CommandRelayService = require('../relay/command-relay');
|
||||
this.relayService = new CommandRelayService(emailConfig.config);
|
||||
|
||||
// 设置事件监听
|
||||
this.setupEventHandlers();
|
||||
|
||||
// 启动服务
|
||||
await this.relayService.start();
|
||||
this.logger.info('Email relay service started');
|
||||
|
||||
// 保持进程运行
|
||||
this.keepAlive();
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
// 优雅关闭
|
||||
const gracefulShutdown = async (signal) => {
|
||||
this.logger.info(`Received ${signal}, shutting down gracefully...`);
|
||||
this.isRunning = false;
|
||||
|
||||
if (this.relayService) {
|
||||
await this.relayService.stop();
|
||||
}
|
||||
|
||||
// 删除 PID 文件
|
||||
if (fs.existsSync(this.pidFile)) {
|
||||
fs.unlinkSync(this.pidFile);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
process.on('SIGHUP', () => {
|
||||
this.logger.info('Received SIGHUP, reloading configuration...');
|
||||
this.config.load();
|
||||
});
|
||||
|
||||
// 中继服务事件
|
||||
if (this.relayService) {
|
||||
this.relayService.on('started', () => {
|
||||
this.logger.info('Command relay service started');
|
||||
});
|
||||
|
||||
this.relayService.on('commandQueued', (command) => {
|
||||
this.logger.info(`Command queued: ${command.id}`);
|
||||
});
|
||||
|
||||
this.relayService.on('commandExecuted', (command) => {
|
||||
this.logger.info(`Command executed: ${command.id}`);
|
||||
});
|
||||
|
||||
this.relayService.on('commandFailed', (command, error) => {
|
||||
this.logger.error(`Command failed: ${command.id} - ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 未捕获异常处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
this.logger.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
this.logger.error('Unhandled rejection at:', promise, 'reason:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
keepAlive() {
|
||||
// 保持进程运行
|
||||
const heartbeat = setInterval(() => {
|
||||
if (!this.isRunning) {
|
||||
clearInterval(heartbeat);
|
||||
return;
|
||||
}
|
||||
this.logger.debug('Heartbeat');
|
||||
}, 60000); // 每分钟输出一次心跳日志
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.isAlreadyRunning()) {
|
||||
console.log('❌ TaskPing daemon 没有运行');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pid = this.getPid();
|
||||
console.log(`🛑 正在停止 TaskPing 守护进程 (PID: ${pid})...`);
|
||||
|
||||
// 发送 SIGTERM 信号
|
||||
process.kill(pid, 'SIGTERM');
|
||||
|
||||
// 等待进程结束
|
||||
await this.waitForStop(pid);
|
||||
|
||||
console.log('✅ TaskPing 守护进程已停止');
|
||||
} catch (error) {
|
||||
console.error('❌ 停止守护进程失败:', error.message);
|
||||
|
||||
// 强制删除 PID 文件
|
||||
if (fs.existsSync(this.pidFile)) {
|
||||
fs.unlinkSync(this.pidFile);
|
||||
console.log('🧹 已清理 PID 文件');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restart() {
|
||||
console.log('🔄 重启 TaskPing 守护进程...');
|
||||
await this.stop();
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒
|
||||
await this.start();
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
const isRunning = this.isAlreadyRunning();
|
||||
const pid = isRunning ? this.getPid() : null;
|
||||
|
||||
return {
|
||||
running: isRunning,
|
||||
pid: pid,
|
||||
pidFile: this.pidFile,
|
||||
logFile: this.logFile,
|
||||
uptime: isRunning ? this.getUptime(pid) : null
|
||||
};
|
||||
}
|
||||
|
||||
showStatus() {
|
||||
const status = this.getStatus();
|
||||
|
||||
console.log('📊 TaskPing 守护进程状态\n');
|
||||
|
||||
if (status.running) {
|
||||
console.log('✅ 状态: 运行中');
|
||||
console.log(`🆔 PID: ${status.pid}`);
|
||||
console.log(`⏱️ 运行时间: ${status.uptime || '未知'}`);
|
||||
} else {
|
||||
console.log('❌ 状态: 未运行');
|
||||
}
|
||||
|
||||
console.log(`📝 日志文件: ${status.logFile}`);
|
||||
console.log(`📁 PID 文件: ${status.pidFile}`);
|
||||
|
||||
// 显示最近的日志
|
||||
if (fs.existsSync(status.logFile)) {
|
||||
console.log('\n📋 最近日志:');
|
||||
try {
|
||||
const logs = fs.readFileSync(status.logFile, 'utf8');
|
||||
const lines = logs.split('\n').filter(line => line.trim()).slice(-5);
|
||||
lines.forEach(line => console.log(` ${line}`));
|
||||
} catch (error) {
|
||||
console.log(' 无法读取日志文件');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isAlreadyRunning() {
|
||||
if (!fs.existsSync(this.pidFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const pid = parseInt(fs.readFileSync(this.pidFile, 'utf8'));
|
||||
// 检查进程是否仍在运行
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 进程不存在,删除过时的 PID 文件
|
||||
fs.unlinkSync(this.pidFile);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getPid() {
|
||||
if (!fs.existsSync(this.pidFile)) {
|
||||
return null;
|
||||
}
|
||||
return parseInt(fs.readFileSync(this.pidFile, 'utf8'));
|
||||
}
|
||||
|
||||
async waitForStop(pid, timeout = 10000) {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
// 进程已停止
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 超时,强制结束
|
||||
throw new Error('进程停止超时,可能需要手动结束');
|
||||
}
|
||||
|
||||
getUptime(pid) {
|
||||
try {
|
||||
// 在 macOS 和 Linux 上获取进程启动时间
|
||||
const { execSync } = require('child_process');
|
||||
const result = execSync(`ps -o lstart= -p ${pid}`, { encoding: 'utf8' });
|
||||
const startTime = new Date(result.trim());
|
||||
const uptime = Date.now() - startTime.getTime();
|
||||
|
||||
const hours = Math.floor(uptime / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
return `${hours}h ${minutes}m`;
|
||||
} catch (error) {
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行接口
|
||||
if (require.main === module) {
|
||||
const daemon = new TaskPingDaemon();
|
||||
const command = process.argv[2];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
switch (command) {
|
||||
case 'start':
|
||||
await daemon.start(true);
|
||||
break;
|
||||
case '--foreground':
|
||||
await daemon.start(false);
|
||||
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>');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
module.exports = TaskPingDaemon;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"commandQueue": [],
|
||||
"lastSaved": "2025-07-12T22:26:32.914Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "01a2841c-a865-4465-b462-32ddcfb7111f",
|
||||
"created": "2025-07-18T10:19:27.379Z",
|
||||
"expires": "2025-07-19T10:19:27.379Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "01eaaf76-ff26-4bec-a488-cdc8508d9779",
|
||||
"created": "2025-07-13T17:38:51.925Z",
|
||||
"expires": "2025-07-14T17:38:51.925Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "0232a3b7-37ca-413a-be8d-77bf9e88f898",
|
||||
"created": "2025-07-23T12:20:51.786Z",
|
||||
"expires": "2025-07-24T12:20:51.786Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "0268b1c3-9be8-4c7f-b7f6-aec0baca70df",
|
||||
"created": "2025-07-23T22:15:19.656Z",
|
||||
"expires": "2025-07-24T22:15:19.656Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "035c7c3d-8dcc-4f13-9e3b-cd5885e1a27e",
|
||||
"created": "2025-07-18T12:36:48.562Z",
|
||||
"expires": "2025-07-19T12:36:48.562Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "047e6c5b-b403-43dd-89c2-f3a154b386d0",
|
||||
"created": "2025-07-15T18:21:56.989Z",
|
||||
"expires": "2025-07-16T18:21:56.989Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "coverr_video",
|
||||
"message": "[coverr_video] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "05ce5d20-928d-4cb6-b196-c298659dfae5",
|
||||
"created": "2025-07-13T21:25:56.713Z",
|
||||
"expires": "2025-07-14T21:25:56.713Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "07db2a97-0224-44e8-8b41-d352cfb5df37",
|
||||
"created": "2025-07-12T23:29:10.943Z",
|
||||
"expires": "2025-07-13T23:29:10.943Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "0ded7ffb-02bf-4c99-bef3-3ba7bb4cc15b",
|
||||
"created": "2025-07-12T22:49:17.851Z",
|
||||
"expires": "2025-07-13T22:49:17.851Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "0e8ce4ea-d146-4721-8730-6d440341b12b",
|
||||
"created": "2025-07-18T13:08:23.027Z",
|
||||
"expires": "2025-07-19T13:08:23.027Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "11356ad8-74d8-46dd-9729-708c7da03245",
|
||||
"created": "2025-07-12T22:28:05.462Z",
|
||||
"expires": "2025-07-13T22:28:05.462Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "116a5568-47e4-47ab-8e57-e94adf84fc87",
|
||||
"created": "2025-07-18T15:47:26.181Z",
|
||||
"expires": "2025-07-19T15:47:26.181Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "11c13096-f65f-490d-82b6-92e813c92d5f",
|
||||
"created": "2025-07-18T13:47:59.853Z",
|
||||
"expires": "2025-07-19T13:47:59.853Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "141614ee-d5fe-4a21-8384-c11d382d6b2a",
|
||||
"created": "2025-07-23T11:54:30.146Z",
|
||||
"expires": "2025-07-24T11:54:30.146Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "17b08d21-4e1a-4991-93b9-b227d430227e",
|
||||
"created": "2025-07-23T11:49:03.622Z",
|
||||
"expires": "2025-07-24T11:49:03.622Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "17cddd72-6761-4c87-a6f3-535dffd46847",
|
||||
"created": "2025-07-13T17:49:21.732Z",
|
||||
"expires": "2025-07-14T17:49:21.732Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "19168db8-122b-40d4-930e-eaa7f442dc5e",
|
||||
"created": "2025-07-14T10:25:17.872Z",
|
||||
"expires": "2025-07-15T10:25:17.872Z",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] Claude需要您的进一步指导"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "197a28ea-945e-4cb1-87d5-6e240eb66c7f",
|
||||
"created": "2025-07-14T08:35:53.805Z",
|
||||
"expires": "2025-07-15T08:35:53.805Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "19bb6891-1db6-4dbc-92a9-aa078dbaa49e",
|
||||
"created": "2025-07-18T11:52:29.822Z",
|
||||
"expires": "2025-07-19T11:52:29.822Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "19cc38c0-5be4-4656-8982-89f75c1cd3b1",
|
||||
"created": "2025-07-24T01:06:31.410Z",
|
||||
"expires": "2025-07-25T01:06:31.410Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "1b507e97-135d-4fe5-99d3-b654ad68557c",
|
||||
"created": "2025-07-13T17:24:45.125Z",
|
||||
"expires": "2025-07-14T17:24:45.125Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "1cabf8e7-3d60-41b1-877e-1c3633dbca38",
|
||||
"created": "2025-07-13T13:10:52.253Z",
|
||||
"expires": "2025-07-14T13:10:52.253Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "205bb739-9a12-44ff-a7fa-3d564a5503d0",
|
||||
"created": "2025-07-14T09:44:29.946Z",
|
||||
"expires": "2025-07-15T09:44:29.946Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "231c3c61-3d7a-4576-83c7-73b52267843f",
|
||||
"created": "2025-07-18T12:43:18.785Z",
|
||||
"expires": "2025-07-19T12:43:18.785Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2391dc73-fca1-41b1-bdc2-5a354bce4c1b",
|
||||
"created": "2025-07-12T22:44:10.848Z",
|
||||
"expires": "2025-07-13T22:44:10.848Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "25713a2c-a3df-4e38-92bd-c4c74b97805c",
|
||||
"created": "2025-07-12T22:47:15.254Z",
|
||||
"expires": "2025-07-13T22:47:15.254Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "257faefb-02c0-4016-8314-2abbecf6979b",
|
||||
"created": "2025-07-13T13:15:51.472Z",
|
||||
"expires": "2025-07-14T13:15:51.472Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "28efcda0-58a3-4bd0-8421-f7375fd513b6",
|
||||
"created": "2025-07-18T12:56:58.947Z",
|
||||
"expires": "2025-07-19T12:56:58.947Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2addd4be-c563-42bc-a6c3-3479e81c80e1",
|
||||
"created": "2025-07-18T21:00:15.789Z",
|
||||
"expires": "2025-07-19T21:00:15.789Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2c21f2ba-3f72-40bd-8231-a5604e300057",
|
||||
"created": "2025-07-14T09:25:46.676Z",
|
||||
"expires": "2025-07-15T09:25:46.676Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2cb3cb02-50e4-4e95-944f-c371f7863eaa",
|
||||
"created": "2025-07-24T07:03:49.922Z",
|
||||
"expires": "2025-07-25T07:03:49.922Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2d1c0454-ac79-48bf-858d-6a7b19d5619a",
|
||||
"created": "2025-07-12T22:28:33.723Z",
|
||||
"expires": "2025-07-13T22:28:33.723Z",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] Claude需要您的进一步指导"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2d9168b2-0c0b-4a37-92c9-c34138ee9196",
|
||||
"created": "2025-07-14T09:33:43.775Z",
|
||||
"expires": "2025-07-15T09:33:43.775Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "2f2c7670-4b82-4dba-8595-f53fad37ba2f",
|
||||
"created": "2025-07-18T11:22:22.962Z",
|
||||
"expires": "2025-07-19T11:22:22.962Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "31fa7938-ae5f-4079-a8f5-af083bae0f03",
|
||||
"created": "2025-07-18T11:37:00.528Z",
|
||||
"expires": "2025-07-19T11:37:00.528Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3556e5c1-ae72-4009-9062-6e94df22bc6c",
|
||||
"created": "2025-07-18T21:06:25.623Z",
|
||||
"expires": "2025-07-19T21:06:25.623Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "374cc8b9-a894-43f6-9bd6-7ee37c878800",
|
||||
"created": "2025-07-24T00:01:43.501Z",
|
||||
"expires": "2025-07-25T00:01:43.501Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "394f897f-af05-4931-b193-6e423ddcd5ae",
|
||||
"created": "2025-07-12T22:38:15.434Z",
|
||||
"expires": "2025-07-13T22:38:15.434Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3afbafe7-a999-4055-8761-a182cf1c5ea0",
|
||||
"created": "2025-07-18T17:13:24.018Z",
|
||||
"expires": "2025-07-19T17:13:24.018Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3b03b7e6-7ffe-41b5-83b2-3e54de65258f",
|
||||
"created": "2025-07-18T12:52:33.189Z",
|
||||
"expires": "2025-07-19T12:52:33.189Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3bd55fff-83d3-4435-86a1-86f0762ac52d",
|
||||
"created": "2025-07-23T12:41:33.713Z",
|
||||
"expires": "2025-07-24T12:41:33.713Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3ca02587-e55f-42bd-8d6b-d4075e0abe45",
|
||||
"created": "2025-07-13T13:18:56.254Z",
|
||||
"expires": "2025-07-14T13:18:56.254Z",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] Claude需要您的进一步指导"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3ead9678-b6bd-49fb-ae64-7303a4c6c740",
|
||||
"created": "2025-07-23T12:39:08.805Z",
|
||||
"expires": "2025-07-24T12:39:08.805Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "3f156d12-c0e6-4489-8bf9-1c7b53ecf165",
|
||||
"created": "2025-07-23T20:28:32.489Z",
|
||||
"expires": "2025-07-24T20:28:32.489Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "412531dd-cd61-4f27-b330-07b5196ef64f",
|
||||
"created": "2025-07-13T13:14:14.490Z",
|
||||
"expires": "2025-07-14T13:14:14.490Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "44b11a87-a008-47bd-affa-72e7daff37d5",
|
||||
"created": "2025-07-13T17:16:58.667Z",
|
||||
"expires": "2025-07-14T17:16:58.667Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "4543a139-f0a2-427a-bcf3-229c998d50c1",
|
||||
"created": "2025-07-18T16:58:38.999Z",
|
||||
"expires": "2025-07-19T16:58:38.999Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "48ad0b9e-6646-47ac-a691-6d2407203484",
|
||||
"created": "2025-07-18T21:09:38.626Z",
|
||||
"expires": "2025-07-19T21:09:38.626Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "490021fb-856e-4176-b889-af8a9041e44f",
|
||||
"created": "2025-07-15T18:07:33.555Z",
|
||||
"expires": "2025-07-16T18:07:33.555Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "coverr_video",
|
||||
"message": "[coverr_video] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "4ad8d491-776e-4901-9210-84fa76c09b69",
|
||||
"created": "2025-07-23T23:02:23.225Z",
|
||||
"expires": "2025-07-24T23:02:23.225Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "4be36d8d-ab24-47fa-8580-a5a145055ae0",
|
||||
"created": "2025-07-23T12:19:49.293Z",
|
||||
"expires": "2025-07-24T12:19:49.293Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "4e5150dc-adfd-439a-8a50-1319f5956efd",
|
||||
"created": "2025-07-17T05:16:42.315Z",
|
||||
"expires": "2025-07-18T05:16:42.315Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "4e85e5f7-f538-4e86-bd2a-7097e2bc8f16",
|
||||
"created": "2025-07-12T22:30:43.403Z",
|
||||
"expires": "2025-07-13T22:30:43.403Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "4ee3f0b1-7d23-4eef-a660-9d4fa63d7a2e",
|
||||
"created": "2025-07-18T13:37:08.299Z",
|
||||
"expires": "2025-07-19T13:37:08.299Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "535a51d2-480e-46ac-988a-a0012babe13f",
|
||||
"created": "2025-07-18T11:56:52.666Z",
|
||||
"expires": "2025-07-19T11:56:52.666Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "53b27eaa-89cf-4a64-a62a-510a3fa3f9b9",
|
||||
"created": "2025-07-18T11:18:14.937Z",
|
||||
"expires": "2025-07-19T11:18:14.937Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "56a35209-9174-4a75-b7f5-43b564e972cd",
|
||||
"created": "2025-07-12T22:45:31.091Z",
|
||||
"expires": "2025-07-13T22:45:31.091Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "590cc9b0-d985-488d-97b4-478076ce0d67",
|
||||
"created": "2025-07-13T13:07:29.360Z",
|
||||
"expires": "2025-07-14T13:07:29.360Z",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] Claude需要您的进一步指导"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "602070fc-f0a9-42f3-b305-6b5149d9da60",
|
||||
"created": "2025-07-12T22:50:13.065Z",
|
||||
"expires": "2025-07-13T22:50:13.065Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "60281214-9e5a-4a6a-b3bd-b9a849b7ee9c",
|
||||
"created": "2025-07-18T13:04:16.855Z",
|
||||
"expires": "2025-07-19T13:04:16.855Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "615e56ff-f4ce-4220-b3b1-24aefcba386f",
|
||||
"created": "2025-07-18T12:32:15.171Z",
|
||||
"expires": "2025-07-19T12:32:15.171Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "628c6e13-e109-4bf0-a98a-8db0005ac55f",
|
||||
"created": "2025-07-18T13:33:08.127Z",
|
||||
"expires": "2025-07-19T13:33:08.127Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "6344f960-c6b6-40e3-9626-7b92c40d148b",
|
||||
"created": "2025-07-18T17:06:22.270Z",
|
||||
"expires": "2025-07-19T17:06:22.270Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "63d537b7-63e3-4c23-8358-b63d5c28c72d",
|
||||
"created": "2025-07-23T22:50:41.568Z",
|
||||
"expires": "2025-07-24T22:50:41.568Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue