清理项目并重构:整合邮件自动化功能
- 删除所有测试和调试文件,保持代码库清洁 - 重写 README.md,提供完整的功能介绍和使用指南 - 整合核心功能:智能邮件通知、回复自动执行、会话管理 - 添加故障排除指南和使用场景说明 - 实现邮件去重机制和单实例运行保障 - 提供全局 claude-control 命令支持 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9d180fca79
commit
b8f79ea2cc
|
|
@ -0,0 +1,154 @@
|
|||
# TaskPing 邮件回复功能使用指南
|
||||
|
||||
## 📋 完整使用流程
|
||||
|
||||
### 步骤1:启动邮件监听服务
|
||||
在终端1中运行:
|
||||
```bash
|
||||
cd /Users/jessytsui/dev/TaskPing
|
||||
npm run relay:pty
|
||||
```
|
||||
|
||||
这会启动邮件监听服务,监听 `noreply@pandalla.ai` 收到的回复邮件。
|
||||
|
||||
### 步骤2:启动Claude Code并集成TaskPing
|
||||
在终端2中运行:
|
||||
```bash
|
||||
# 启动Claude Code
|
||||
claude
|
||||
|
||||
# 在Claude Code中使用TaskPing发送邮件通知
|
||||
# 例如:当任务完成时会自动发送邮件
|
||||
```
|
||||
|
||||
### 步骤3:配置Claude Code钩子(如果还没配置)
|
||||
在Claude Code中运行:
|
||||
```bash
|
||||
# 查看当前钩子配置
|
||||
cat ~/.config/claude-code/settings/hooks.json
|
||||
|
||||
# 如果没有配置,需要设置TaskPing钩子
|
||||
# 复制TaskPing的钩子配置文件
|
||||
```
|
||||
|
||||
## 📧 邮件回复测试流程
|
||||
|
||||
### 方法1:手动测试发送邮件
|
||||
```bash
|
||||
# 在TaskPing目录中运行
|
||||
node test-smtp-token.js
|
||||
```
|
||||
|
||||
这会发送一封测试邮件到 `jiaxicui446@gmail.com`,邮件主题包含Token格式:
|
||||
`[TaskPing #XXXXXXXX] Claude Code 任务完成 - TaskPing-Token-Test`
|
||||
|
||||
### 方法2:实际集成测试
|
||||
1. 在Claude Code中执行一个任务
|
||||
2. 任务完成后,TaskPing会自动发送邮件通知
|
||||
3. 邮件会发送到配置的邮箱(`jiaxicui446@gmail.com`)
|
||||
|
||||
## 💌 如何回复邮件发送命令
|
||||
|
||||
### 收到邮件后:
|
||||
1. 在 `jiaxicui446@gmail.com` 收到邮件,主题如:
|
||||
```
|
||||
[TaskPing #A53PXR7F] Claude Code 任务完成 - 项目名
|
||||
```
|
||||
|
||||
2. **直接回复邮件**,在正文中输入命令:
|
||||
```
|
||||
继续优化代码
|
||||
```
|
||||
或
|
||||
```
|
||||
生成单元测试
|
||||
```
|
||||
或
|
||||
```
|
||||
解释这个函数的作用
|
||||
```
|
||||
|
||||
3. 发送回复后,邮件监听服务会:
|
||||
- 收到回复邮件
|
||||
- 提取Token(A53PXR7F)
|
||||
- 找到对应的PTY会话
|
||||
- 将命令注入到Claude Code CLI
|
||||
|
||||
## 🔧 配置文件说明
|
||||
|
||||
### .env 配置
|
||||
```env
|
||||
# 发件配置(飞书邮箱)
|
||||
SMTP_HOST=smtp.feishu.cn
|
||||
SMTP_USER=noreply@pandalla.ai
|
||||
SMTP_PASS=kKgS3tNReRTL3RQC
|
||||
|
||||
# 收件配置(飞书邮箱)
|
||||
IMAP_HOST=imap.feishu.cn
|
||||
IMAP_USER=noreply@pandalla.ai
|
||||
IMAP_PASS=kKgS3tNReRTL3RQC
|
||||
|
||||
# 用户通知邮箱
|
||||
EMAIL_TO=jiaxicui446@gmail.com
|
||||
|
||||
# 允许发送命令的邮箱(安全白名单)
|
||||
ALLOWED_SENDERS=jiaxicui446@gmail.com
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 1. 收不到邮件回复
|
||||
检查:
|
||||
- 邮件监听服务是否正在运行(`npm run relay:pty`)
|
||||
- 是否从白名单邮箱(`jiaxicui446@gmail.com`)发送回复
|
||||
- 邮件主题是否包含正确的Token格式
|
||||
|
||||
### 2. 命令没有注入到Claude Code
|
||||
检查:
|
||||
- Claude Code是否还在运行
|
||||
- PTY会话是否还有效(Token未过期)
|
||||
- 检查服务日志输出
|
||||
|
||||
### 3. 查看调试日志
|
||||
```bash
|
||||
# 查看详细的邮件监听日志
|
||||
DEBUG=true npm run relay:pty
|
||||
```
|
||||
|
||||
## 📱 支持的邮件客户端
|
||||
|
||||
用户可以从任意邮箱回复到 `noreply@pandalla.ai`:
|
||||
- ✅ Gmail 网页版/客户端
|
||||
- ✅ 手机Gmail APP
|
||||
- ✅ Apple Mail
|
||||
- ✅ Outlook
|
||||
- ✅ QQ邮箱
|
||||
- ✅ 163邮箱
|
||||
- ✅ 任何支持SMTP的邮箱
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
1. **Token验证**:每个会话有唯一Token,防止误操作
|
||||
2. **发件人白名单**:只有授权邮箱可以发送命令
|
||||
3. **会话过期**:Token有24小时有效期
|
||||
4. **命令过滤**:自动过滤潜在危险命令
|
||||
|
||||
## 🎯 实际使用场景
|
||||
|
||||
### 场景1:长时间构建
|
||||
```
|
||||
1. 在Claude Code中启动项目构建
|
||||
2. 离开电脑,在手机收到构建完成邮件
|
||||
3. 手机直接回复:"继续部署到生产环境"
|
||||
4. 回到电脑时部署已完成
|
||||
```
|
||||
|
||||
### 场景2:代码审查
|
||||
```
|
||||
1. Claude Code完成代码生成
|
||||
2. 收到邮件通知
|
||||
3. 回复:"请添加单元测试和文档"
|
||||
4. Claude自动生成测试和文档
|
||||
```
|
||||
|
||||
这样就可以实现真正的远程控制Claude Code了!
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# 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任务通知功能。
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
# 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 # 连接中继服务
|
||||
```
|
||||
|
||||
这个结构为未来的扩展提供了良好的基础。
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# 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完成任务时会主动通知你。**
|
||||
432
README.md
432
README.md
|
|
@ -1,80 +1,41 @@
|
|||
# TaskPing - Claude Code 邮件自动化
|
||||
# TaskPing - 智能邮件自动化 Claude Code 助手
|
||||
|
||||
TaskPing 是一个智能的邮件自动化工具,可以监听你的邮件回复,并将回复内容自动输入到 Claude Code 中执行。
|
||||
TaskPing 是一个智能的邮件自动化工具,实现了 Claude Code 与邮件系统的深度集成。通过监听邮件回复,自动将回复内容输入到对应的 Claude Code 会话中执行,让你可以在任何地方通过邮件远程控制 Claude Code。
|
||||
|
||||
## 🚀 快速开始
|
||||
## 🚀 核心功能
|
||||
|
||||
### 1. 配置邮箱
|
||||
```bash
|
||||
npm run config
|
||||
```
|
||||
按照提示配置你的邮箱信息(SMTP和IMAP)。
|
||||
### 📧 智能邮件通知
|
||||
- **自动检测**: 基于 Claude Code 官方 hooks 机制,自动识别任务完成和等待输入状态
|
||||
- **实时通知**: 任务完成时自动发送邮件,包含完整的用户问题和 Claude 回复内容
|
||||
- **会话绑定**: 邮件与特定的 tmux 会话绑定,确保回复到正确的 Claude Code 窗口
|
||||
|
||||
### 2. 启动服务
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
### 🔄 邮件回复自动执行
|
||||
- **远程控制**: 直接回复邮件,内容自动输入到对应的 Claude Code 会话中
|
||||
- **智能注入**: 自动检测 tmux 会话状态,将命令精确注入到正确的窗口
|
||||
- **防重复处理**: 实现邮件去重机制,避免重复处理同一封邮件
|
||||
|
||||
### 3. 使用流程
|
||||
1. 当 Claude Code 完成任务时,TaskPing 会发送邮件通知到你的邮箱
|
||||
2. 直接回复这封邮件,在邮件中写入你想让 Claude Code 执行的下一个命令
|
||||
3. TaskPing 会自动监听到你的回复,提取命令内容,并自动输入到 Claude Code 中
|
||||
4. 命令会自动执行,无需任何手动操作
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 🎯 智能检测
|
||||
- 基于Claude Code官方hooks机制
|
||||
- 自动识别任务完成和等待输入状态
|
||||
- 无需手动监控,完全自动化
|
||||
|
||||
### 📢 多渠道通知
|
||||
- **桌面通知**:即时本地通知
|
||||
- **邮件通知**:远程邮件提醒 + 回复执行命令
|
||||
- 支持自定义通知声音和消息内容
|
||||
- 同时启用多个通知渠道
|
||||
|
||||
### 🏠 远程命令执行
|
||||
- **邮件回复**:直接回复邮件执行下一步命令
|
||||
- **自动化流程**:人不在电脑前也能继续对话
|
||||
- **安全机制**:会话过期、命令过滤、来源验证
|
||||
|
||||
### 🌍 跨平台支持
|
||||
- **macOS**:原生通知中心 + 系统提示音
|
||||
- **Windows**:Toast通知系统
|
||||
- **Linux**:libnotify桌面通知
|
||||
|
||||
### 🎛️ 灵活配置
|
||||
- 多语言支持(中文、英文、日文)
|
||||
- 自定义提示音(支持系统音效)
|
||||
- 邮件 SMTP/IMAP 配置
|
||||
- 可调节通知频率和超时时间
|
||||
### 🛡️ 稳定性保障
|
||||
- **单实例运行**: 确保只有一个邮件监听进程运行,避免重复处理
|
||||
- **状态管理**: 完善的会话状态跟踪和错误恢复机制
|
||||
- **安全验证**: 邮件来源验证,确保只处理授权用户的回复
|
||||
|
||||
## 📦 快速安装
|
||||
|
||||
### 自动安装(推荐)
|
||||
|
||||
### 1. 克隆项目
|
||||
```bash
|
||||
# 1. 克隆或下载项目
|
||||
git clone <repository-url>
|
||||
git clone https://github.com/your-username/TaskPing.git
|
||||
cd TaskPing
|
||||
|
||||
# 2. 运行安装脚本
|
||||
node taskping.js install
|
||||
|
||||
# 3. 按提示完成配置
|
||||
# 安装器会自动配置Claude Code的hooks设置
|
||||
npm install
|
||||
```
|
||||
|
||||
### 手动安装
|
||||
|
||||
### 2. 配置邮箱
|
||||
```bash
|
||||
# 1. 测试通知功能
|
||||
node taskping.js test
|
||||
|
||||
# 2. 配置Claude Code
|
||||
# 将以下内容添加到 ~/.claude/settings.json 的 "hooks" 部分:
|
||||
npm run config
|
||||
```
|
||||
按照提示配置你的邮箱信息(SMTP 和 IMAP)。
|
||||
|
||||
### 3. 配置 Claude Code 钩子
|
||||
将以下内容添加到 `~/.claude/settings.json` 的 `hooks` 部分:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -99,266 +60,155 @@ node taskping.js test
|
|||
}
|
||||
```
|
||||
|
||||
### 4. 全局安装 claude-control 命令
|
||||
```bash
|
||||
node install-global.js
|
||||
```
|
||||
|
||||
### 5. 启动邮件监听服务
|
||||
```bash
|
||||
npm run relay:pty
|
||||
```
|
||||
|
||||
## 🎮 使用方法
|
||||
|
||||
### 基本使用
|
||||
|
||||
安装完成后,TaskPing会自动工作:
|
||||
|
||||
### 1. 创建 Claude Code 会话
|
||||
```bash
|
||||
# 1. 正常启动Claude Code
|
||||
claude
|
||||
|
||||
# 2. 执行任务
|
||||
> 请帮我重构这个项目的代码结构
|
||||
|
||||
# 3. 当Claude完成任务时,你会收到通知:
|
||||
# 📱 "任务已完成,Claude正在等待下一步指令"
|
||||
|
||||
# 4. 当Claude需要你的输入时,你会收到提醒:
|
||||
# 📱 "Claude需要您的进一步指导"
|
||||
# 在任何目录下都可以运行
|
||||
claude-control --session project-name
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
### 2. 正常使用 Claude Code
|
||||
在 tmux 会话中正常与 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
|
||||
Claude 回复...
|
||||
```
|
||||
|
||||
## 🔧 配置选项
|
||||
### 3. 自动邮件通知
|
||||
当 Claude 完成任务时,你会收到包含完整对话内容的邮件通知。
|
||||
|
||||
### 语言设置
|
||||
- `zh-CN`:简体中文
|
||||
- `en`:英语
|
||||
- `ja`:日语
|
||||
|
||||
### 提示音选择(macOS)
|
||||
- `Glass`:清脆玻璃音(推荐用于任务完成)
|
||||
- `Tink`:轻柔提示音(推荐用于等待输入)
|
||||
- `Ping`、`Pop`、`Basso` 等系统音效
|
||||
|
||||
### 基础配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "zh-CN",
|
||||
"sound": {
|
||||
"completed": "Glass",
|
||||
"waiting": "Tink"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 5
|
||||
}
|
||||
### 4. 邮件回复控制
|
||||
直接回复邮件,输入下一个指令:
|
||||
```
|
||||
请继续优化代码性能
|
||||
```
|
||||
|
||||
### 邮件配置示例
|
||||
### 5. 自动执行
|
||||
你的回复会自动注入到对应的 Claude Code 会话中并执行。
|
||||
|
||||
```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/ # 文档目录
|
||||
├── src/
|
||||
│ ├── channels/email/
|
||||
│ │ └── smtp.js # SMTP 邮件发送
|
||||
│ ├── core/
|
||||
│ │ ├── config.js # 配置管理
|
||||
│ │ ├── logger.js # 日志系统
|
||||
│ │ └── notifier.js # 通知协调器
|
||||
│ ├── data/
|
||||
│ │ ├── session-map.json # 会话映射表
|
||||
│ │ └── processed-messages.json # 已处理邮件记录
|
||||
│ ├── relay/
|
||||
│ │ └── relay-pty.js # 邮件监听和 PTY 注入服务
|
||||
│ └── utils/
|
||||
│ └── tmux-monitor.js # Tmux 会话监控
|
||||
├── taskping.js # 主入口文件
|
||||
├── claude-control.js # Claude Code 会话管理
|
||||
├── start-relay-pty.js # 邮件监听服务启动器
|
||||
└── install-global.js # 全局安装脚本
|
||||
```
|
||||
|
||||
## 🔮 发展规划
|
||||
## 🛠️ 核心技术实现
|
||||
|
||||
TaskPing按照产品规格文档分阶段开发:
|
||||
### 邮件监听与处理
|
||||
- 使用 `node-imap` 监听 IMAP 邮箱新邮件
|
||||
- 实现邮件去重机制(基于 UID、messageId 和内容哈希)
|
||||
- 异步事件处理,避免竞态条件
|
||||
|
||||
### ✅ Phase 1 - 本地通知MVP(已完成)
|
||||
- 本地桌面通知
|
||||
- Claude Code hooks集成
|
||||
- 基础配置管理
|
||||
### 会话管理
|
||||
- Tmux 会话自动检测和命令注入
|
||||
- 会话状态持久化存储
|
||||
- 支持多会话并发处理
|
||||
|
||||
### ✅ Phase 2 - 邮件通知和远程执行(已完成)
|
||||
- 📧 邮件通知功能
|
||||
- 🔄 邮件回复命令执行
|
||||
- 🔒 安全会话管理
|
||||
- 🛠️ 命令中继服务
|
||||
### 通知系统
|
||||
- 自动捕获当前 tmux 会话的用户问题和 Claude 回复
|
||||
- 生成包含完整对话内容的邮件通知
|
||||
- 支持多种通知渠道(桌面通知、邮件等)
|
||||
|
||||
### 🚧 Phase 3 - 多渠道通知(规划中)
|
||||
- Telegram/Discord/WhatsApp/飞书集成
|
||||
- 移动端推送通知
|
||||
- 多渠道命令中继
|
||||
## 🔍 故障排除
|
||||
|
||||
### 🌟 Phase 4 - 企业级功能(未来)
|
||||
- 团队协作功能
|
||||
- 用户权限管理
|
||||
- 审计日志
|
||||
- API 接口
|
||||
### 邮件重复处理问题
|
||||
确保只运行一个邮件监听进程:
|
||||
```bash
|
||||
# 检查运行状态
|
||||
ps aux | grep relay-pty
|
||||
|
||||
# 停止所有进程
|
||||
pkill -f relay-pty
|
||||
|
||||
# 重新启动
|
||||
npm run relay:pty
|
||||
```
|
||||
|
||||
### 命令注入失败
|
||||
检查 tmux 会话状态:
|
||||
```bash
|
||||
# 查看所有会话
|
||||
tmux list-sessions
|
||||
|
||||
# 检查会话内容
|
||||
tmux capture-pane -t session-name -p
|
||||
```
|
||||
|
||||
### 邮件配置问题
|
||||
测试邮件连接:
|
||||
```bash
|
||||
# 测试 SMTP
|
||||
node -e "
|
||||
const config = require('./config/user.json');
|
||||
console.log('SMTP Config:', config.email.config.smtp);
|
||||
"
|
||||
|
||||
# 测试 IMAP
|
||||
node -e "
|
||||
const config = require('./config/user.json');
|
||||
console.log('IMAP Config:', config.email.config.imap);
|
||||
"
|
||||
```
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 远程编程工作流
|
||||
1. 在办公室启动一个 Claude Code 代码审查任务
|
||||
2. 下班回家,收到邮件"代码审查完成,发现3个问题"
|
||||
3. 回复邮件"请修复第一个问题"
|
||||
4. Claude 自动开始修复,完成后再次发送邮件通知
|
||||
5. 继续回复邮件进行下一步操作
|
||||
|
||||
### 长时间任务监控
|
||||
1. 启动大型项目重构任务
|
||||
2. Claude 分步骤完成各个模块
|
||||
3. 每个阶段完成都发送邮件通知进度
|
||||
4. 通过邮件回复指导下一步方向
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎参与TaskPing的开发!
|
||||
|
||||
### 如何贡献
|
||||
1. Fork本项目
|
||||
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开发
|
||||
5. 提交 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT License](LICENSE) 开源协议。
|
||||
|
||||
## 💬 联系我们
|
||||
|
||||
- 🐛 **问题反馈**:[提交Issue](https://github.com/TaskPing/TaskPing/issues)
|
||||
- 💡 **功能建议**:[Discussion](https://github.com/TaskPing/TaskPing/discussions)
|
||||
- 📧 **邮件联系**:contact@taskping.dev
|
||||
本项目采用 MIT License 开源协议。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
**让 Claude Code 工作流程更加智能高效!**
|
||||
|
||||
**让Claude Code工作流程更加智能高效!**
|
||||
|
||||
⭐ 如果这个项目对你有帮助,请给我们一个Star!
|
||||
|
||||
</div>
|
||||
如果这个项目对你有帮助,请给我们一个 ⭐!
|
||||
199
TaskPing.md
199
TaskPing.md
|
|
@ -1,199 +0,0 @@
|
|||
# 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,287 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TaskPing 无人值守远程控制设置助手
|
||||
*/
|
||||
|
||||
const { exec, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class RemoteControlSetup {
|
||||
constructor(sessionName = null) {
|
||||
this.sessionName = sessionName || 'claude-taskping';
|
||||
this.taskpingHome = this.findTaskPingHome();
|
||||
}
|
||||
|
||||
findTaskPingHome() {
|
||||
// If TASKPING_HOME environment variable is set, use it
|
||||
if (process.env.TASKPING_HOME) {
|
||||
return process.env.TASKPING_HOME;
|
||||
}
|
||||
|
||||
// If running from the TaskPing directory, use current directory
|
||||
if (fs.existsSync(path.join(__dirname, 'package.json'))) {
|
||||
const packagePath = path.join(__dirname, 'package.json');
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
if (packageJson.name && packageJson.name.includes('taskping')) {
|
||||
return __dirname;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue searching
|
||||
}
|
||||
}
|
||||
|
||||
// Search for TaskPing in common locations
|
||||
const commonPaths = [
|
||||
path.join(process.env.HOME, 'dev', 'TaskPing'),
|
||||
path.join(process.env.HOME, 'Projects', 'TaskPing'),
|
||||
path.join(process.env.HOME, 'taskping'),
|
||||
__dirname // fallback to current script directory
|
||||
];
|
||||
|
||||
for (const searchPath of commonPaths) {
|
||||
if (fs.existsSync(searchPath) && fs.existsSync(path.join(searchPath, 'package.json'))) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(searchPath, 'package.json'), 'utf8'));
|
||||
if (packageJson.name && packageJson.name.toLowerCase().includes('taskping')) {
|
||||
return searchPath;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue searching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, use current directory as fallback
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
console.log('🚀 TaskPing 无人值守远程控制设置\n');
|
||||
console.log('🎯 目标: 人在外面用手机→家中电脑Claude Code自动执行命令\n');
|
||||
|
||||
try {
|
||||
// 1. 检查tmux
|
||||
await this.checkAndInstallTmux();
|
||||
|
||||
// 2. 检查Claude CLI
|
||||
await this.checkClaudeCLI();
|
||||
|
||||
// 3. 设置Claude tmux会话
|
||||
await this.setupClaudeSession();
|
||||
|
||||
// 4. 会话创建完成
|
||||
console.log('\n4️⃣ 会话创建完成');
|
||||
|
||||
// 5. 提供使用指南
|
||||
this.showUsageGuide();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 设置过程中发生错误:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async checkAndInstallTmux() {
|
||||
console.log('1️⃣ 检查tmux安装状态...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec('which tmux', (error, stdout) => {
|
||||
if (error) {
|
||||
console.log('❌ tmux未安装');
|
||||
console.log('📦 正在安装tmux...');
|
||||
|
||||
exec('brew install tmux', (installError, installStdout, installStderr) => {
|
||||
if (installError) {
|
||||
console.log('❌ tmux安装失败,请手动安装:');
|
||||
console.log(' brew install tmux');
|
||||
console.log(' 或从 https://github.com/tmux/tmux 下载');
|
||||
} else {
|
||||
console.log('✅ tmux安装成功');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
console.log(`✅ tmux已安装: ${stdout.trim()}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async checkClaudeCLI() {
|
||||
console.log('\n2️⃣ 检查Claude CLI状态...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec('which claude', (error, stdout) => {
|
||||
if (error) {
|
||||
console.log('❌ Claude CLI未找到');
|
||||
console.log('📦 请安装Claude CLI:');
|
||||
console.log(' npm install -g @anthropic-ai/claude-code');
|
||||
} else {
|
||||
console.log(`✅ Claude CLI已安装: ${stdout.trim()}`);
|
||||
|
||||
// 检查版本
|
||||
exec('claude --version', (versionError, versionStdout) => {
|
||||
if (!versionError) {
|
||||
console.log(`📋 版本: ${versionStdout.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async setupClaudeSession() {
|
||||
console.log('\n3️⃣ 设置Claude tmux会话...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 检查是否已有会话
|
||||
exec(`tmux has-session -t ${this.sessionName} 2>/dev/null`, (checkError) => {
|
||||
if (!checkError) {
|
||||
console.log('⚠️ Claude tmux会话已存在');
|
||||
console.log('🔄 是否重新创建会话? (会杀死现有会话)');
|
||||
|
||||
// 简单起见,直接重建
|
||||
this.killAndCreateSession(resolve);
|
||||
} else {
|
||||
this.createNewSession(resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
killAndCreateSession(resolve) {
|
||||
exec(`tmux kill-session -t ${this.sessionName} 2>/dev/null`, () => {
|
||||
setTimeout(() => {
|
||||
this.createNewSession(resolve);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
createNewSession(resolve) {
|
||||
// 使用TaskPing主目录作为工作目录
|
||||
const workingDir = this.taskpingHome;
|
||||
const command = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" clauderun`;
|
||||
|
||||
console.log(`🚀 创建Claude tmux会话: ${this.sessionName}`);
|
||||
console.log(`📁 工作目录: ${workingDir}`);
|
||||
console.log(`💡 使用便捷命令: clauderun (等同于 claude --dangerously-skip-permissions)`);
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`❌ 会话创建失败: ${error.message}`);
|
||||
if (stderr) {
|
||||
console.log(`错误详情: ${stderr}`);
|
||||
}
|
||||
// 如果clauderun失败,尝试使用完整路径命令
|
||||
console.log('🔄 尝试使用完整路径命令...');
|
||||
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" /Users/jessytsui/.nvm/versions/node/v18.17.0/bin/claude --dangerously-skip-permissions`;
|
||||
exec(fallbackCommand, (fallbackError) => {
|
||||
if (fallbackError) {
|
||||
console.log(`❌ 完整路径命令也失败: ${fallbackError.message}`);
|
||||
} else {
|
||||
console.log('✅ Claude tmux会话创建成功 (使用完整路径)');
|
||||
console.log(`📺 查看会话: tmux attach -t ${this.sessionName}`);
|
||||
console.log(`🔚 退出会话: Ctrl+B, D (不会关闭Claude)`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
console.log('✅ Claude tmux会话创建成功');
|
||||
console.log(`📺 查看会话: tmux attach -t ${this.sessionName}`);
|
||||
console.log(`🔚 退出会话: Ctrl+B, D (不会关闭Claude)`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async testRemoteInjection() {
|
||||
console.log('\n💡 会话已就绪,可以开始使用');
|
||||
console.log('📋 Claude Code正在等待您的指令');
|
||||
console.log('🔧 如需测试注入功能,请使用单独的测试脚本');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showUsageGuide() {
|
||||
console.log('\n🎉 设置完成!无人值守远程控制已就绪\n');
|
||||
|
||||
console.log('🎯 新功能: clauderun 便捷命令');
|
||||
console.log(' 现在可以使用 clauderun 代替 claude --dangerously-skip-permissions');
|
||||
console.log(' 更清晰的Claude Code启动方式\n');
|
||||
|
||||
console.log('📋 使用流程:');
|
||||
console.log('1. 🏠 在家启动邮件监听: npm run relay:pty');
|
||||
console.log('2. 🚪 出门时Claude继续在tmux中运行');
|
||||
console.log('3. 📱 手机收到TaskPing邮件通知');
|
||||
console.log('4. 💬 手机回复邮件输入命令');
|
||||
console.log('5. 🤖 家中Claude自动接收并执行命令');
|
||||
console.log('6. 🔄 循环上述过程,完全无人值守\n');
|
||||
|
||||
console.log('🔧 管理命令:');
|
||||
console.log(` 查看Claude会话: tmux attach -t ${this.sessionName}`);
|
||||
console.log(` 退出会话(不关闭): Ctrl+B, D`);
|
||||
console.log(` 杀死会话: tmux kill-session -t ${this.sessionName}`);
|
||||
console.log(` 查看所有会话: tmux list-sessions\n`);
|
||||
|
||||
console.log('🎛️ 多会话支持:');
|
||||
console.log(' 创建自定义会话: node claude-control.js --session my-project');
|
||||
console.log(' 创建多个会话: node claude-control.js --session frontend');
|
||||
console.log(' node claude-control.js --session backend');
|
||||
console.log(' 邮件回复会自动路由到对应的会话\n');
|
||||
|
||||
console.log('📱 邮件测试:');
|
||||
console.log(' Token将包含会话信息,自动路由到正确的tmux会话');
|
||||
console.log(' 收件邮箱: jiaxicui446@gmail.com');
|
||||
console.log(' 回复邮件输入: echo "远程控制测试"\n');
|
||||
|
||||
console.log('🚨 重要提醒:');
|
||||
console.log('- Claude会话在tmux中持续运行,断网重连也不会中断');
|
||||
console.log('- 邮件监听服务需要保持运行状态');
|
||||
console.log('- 家中电脑需要保持开机和网络连接');
|
||||
console.log('- 手机可以从任何地方发送邮件命令');
|
||||
console.log('- 支持同时运行多个不同项目的Claude会话\n');
|
||||
|
||||
console.log('✅ 现在你可以实现真正的无人值守远程控制了!🎯');
|
||||
}
|
||||
|
||||
// 快速重建会话的方法
|
||||
async quickRestart() {
|
||||
console.log('🔄 快速重启Claude会话...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.killAndCreateSession(() => {
|
||||
console.log('✅ Claude会话已重启');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行参数处理
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// 解析会话名称参数
|
||||
let sessionName = null;
|
||||
const sessionIndex = args.indexOf('--session');
|
||||
if (sessionIndex !== -1 && args[sessionIndex + 1]) {
|
||||
sessionName = args[sessionIndex + 1];
|
||||
}
|
||||
|
||||
const setup = new RemoteControlSetup(sessionName);
|
||||
|
||||
if (sessionName) {
|
||||
console.log(`🎛️ 使用自定义会话名称: ${sessionName}`);
|
||||
}
|
||||
|
||||
if (args.includes('--restart')) {
|
||||
setup.quickRestart();
|
||||
} else {
|
||||
setup.setup();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RemoteControlSetup;
|
||||
494
debug-email.js
494
debug-email.js
|
|
@ -1,494 +0,0 @@
|
|||
#!/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);
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
#!/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;
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
# 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**: 测试和优化
|
||||
- 端到端测试
|
||||
- 错误处理完善
|
||||
- 性能优化
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
# TaskPing 邮件架构说明
|
||||
|
||||
## 📧 正确的邮件流程
|
||||
|
||||
### 1. 服务端配置(TaskPing 系统)
|
||||
|
||||
```
|
||||
发送邮箱: noreply@pandalla.ai (飞书邮箱)
|
||||
接收邮箱: noreply@pandalla.ai (飞书邮箱)
|
||||
```
|
||||
|
||||
**用途**:
|
||||
- 发送通知邮件给用户
|
||||
- 接收用户的回复命令
|
||||
- 处理邮件并注入到 Claude Code CLI
|
||||
|
||||
### 2. 用户端配置
|
||||
|
||||
```
|
||||
通知接收邮箱: jiaxicui446@gmail.com (可配置为任意邮箱)
|
||||
回复目标邮箱: noreply@pandalla.ai
|
||||
```
|
||||
|
||||
**用途**:
|
||||
- 接收 TaskPing 的任务通知
|
||||
- 从任意邮箱回复命令到服务端
|
||||
|
||||
## 🔄 邮件流程图
|
||||
|
||||
```
|
||||
1. 任务通知流程:
|
||||
TaskPing 系统 → noreply@pandalla.ai → jiaxicui446@gmail.com
|
||||
|
||||
2. 命令回复流程:
|
||||
用户邮箱(任意) → noreply@pandalla.ai → TaskPing 系统 → Claude Code CLI
|
||||
```
|
||||
|
||||
## 📱 支持的用户邮箱
|
||||
|
||||
用户可以从以下**任意邮箱**发送回复到 `noreply@pandalla.ai`:
|
||||
|
||||
- ✅ Gmail (`jiaxicui446@gmail.com`)
|
||||
- ✅ QQ邮箱 (`xxx@qq.com`)
|
||||
- ✅ 163邮箱 (`xxx@163.com`)
|
||||
- ✅ Outlook (`xxx@outlook.com`)
|
||||
- ✅ 企业邮箱 (`xxx@company.com`)
|
||||
- ✅ 任何支持SMTP的邮箱
|
||||
|
||||
## 🔧 配置文件
|
||||
|
||||
### .env 配置
|
||||
|
||||
```env
|
||||
# 发件配置(飞书邮箱)
|
||||
SMTP_HOST=smtp.feishu.cn
|
||||
SMTP_USER=noreply@pandalla.ai
|
||||
SMTP_PASS=kKgS3tNReRTL3RQC
|
||||
|
||||
# 收件配置(飞书邮箱)
|
||||
IMAP_HOST=imap.feishu.cn
|
||||
IMAP_USER=noreply@pandalla.ai
|
||||
IMAP_PASS=kKgS3tNReRTL3RQC
|
||||
|
||||
# 用户通知邮箱(可配置)
|
||||
EMAIL_TO=jiaxicui446@gmail.com
|
||||
|
||||
# 白名单(可配置多个用户邮箱)
|
||||
ALLOWED_SENDERS=jiaxicui446@gmail.com
|
||||
```
|
||||
|
||||
## 📝 使用方法
|
||||
|
||||
### 1. 接收通知
|
||||
用户在 `jiaxicui446@gmail.com` 收到类似邮件:
|
||||
```
|
||||
发件人: TaskPing 通知系统 <noreply@pandalla.ai>
|
||||
主题: [TaskPing #ABC123] 任务等待您的指示
|
||||
内容: 任务详情...
|
||||
```
|
||||
|
||||
### 2. 发送命令
|
||||
用户可以:
|
||||
|
||||
**方式1:直接回复**
|
||||
- 直接回复邮件
|
||||
- 输入命令,如:`继续执行`
|
||||
|
||||
**方式2:新邮件**
|
||||
- 从任意邮箱发送到 `noreply@pandalla.ai`
|
||||
- 主题包含:`[TaskPing #ABC123]`
|
||||
- 内容为命令
|
||||
|
||||
### 3. 系统处理
|
||||
1. 飞书邮箱接收用户回复
|
||||
2. TaskPing 解析命令
|
||||
3. 通过 PTY 注入到 Claude Code
|
||||
4. 任务继续执行
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
1. **发件人验证**:只有白名单中的邮箱可以发送命令
|
||||
2. **Token验证**:邮件主题必须包含有效的会话Token
|
||||
3. **命令过滤**:自动过滤危险命令
|
||||
4. **会话过期**:Token有时间限制
|
||||
5. **去重处理**:防止重复执行同一命令
|
||||
|
||||
## 🚀 优势
|
||||
|
||||
1. **统一管理**:服务端使用单一邮箱管理
|
||||
2. **用户灵活**:用户可用任意邮箱接收和回复
|
||||
3. **简单配置**:只需配置一个飞书邮箱
|
||||
4. **多用户支持**:可配置多个用户邮箱到白名单
|
||||
5. **跨平台**:支持所有邮件客户端
|
||||
|
||||
## 📊 实际场景
|
||||
|
||||
### 场景1:移动办公
|
||||
```
|
||||
1. 开发者在电脑上运行 Claude Code 构建项目
|
||||
2. 离开电脑,在手机 Gmail 收到构建完成通知
|
||||
3. 直接在手机回复:"继续部署"
|
||||
4. 电脑上的 Claude Code 自动执行部署命令
|
||||
```
|
||||
|
||||
### 场景2:团队协作
|
||||
```
|
||||
1. 团队领导 leader@company.com 收到项目通知
|
||||
2. 从企业邮箱回复:"批准发布"
|
||||
3. 系统自动执行发布流程
|
||||
4. 所有团队成员收到发布完成通知
|
||||
```
|
||||
|
||||
### 场景3:多设备同步
|
||||
```
|
||||
1. 在办公室电脑启动长时间任务
|
||||
2. 回家路上用个人 QQ 邮箱收到通知
|
||||
3. 在家用 163 邮箱发送下一步指令
|
||||
4. 办公室电脑自动执行,第二天查看结果
|
||||
```
|
||||
|
||||
这种架构实现了:
|
||||
- 服务端邮箱统一管理
|
||||
- 用户邮箱灵活配置
|
||||
- 多邮箱品牌支持
|
||||
- 简单可靠的通信机制
|
||||
|
|
@ -1,346 +0,0 @@
|
|||
# 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 编程体验!🚀
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
# TaskPing 邮件回复功能使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
TaskPing 的邮件回复功能允许您通过回复邮件的方式向 Claude Code CLI 发送命令。当您在移动设备或其他电脑上收到 TaskPing 的任务提醒邮件时,可以直接回复邮件来控制 Claude Code 继续执行任务。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **发送提醒**: Claude Code 在任务暂停时发送包含会话 Token 的提醒邮件
|
||||
2. **邮件监听**: PTY Relay 服务持续监听您的收件箱
|
||||
3. **命令提取**: 从回复邮件中提取命令内容
|
||||
4. **命令注入**: 通过 node-pty 将命令注入到对应的 Claude Code 会话
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置邮件账号
|
||||
|
||||
复制环境配置文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件,填入您的邮件配置:
|
||||
```env
|
||||
# Gmail 示例
|
||||
IMAP_HOST=imap.gmail.com
|
||||
IMAP_PORT=993
|
||||
IMAP_SECURE=true
|
||||
IMAP_USER=your-email@gmail.com
|
||||
IMAP_PASS=your-app-password # 使用应用专用密码
|
||||
|
||||
# 安全设置
|
||||
ALLOWED_SENDERS=your-email@gmail.com,trusted@company.com
|
||||
```
|
||||
|
||||
### 2. 启动 PTY Relay 服务
|
||||
|
||||
```bash
|
||||
npm run relay:pty
|
||||
# 或者
|
||||
./start-relay-pty.js
|
||||
```
|
||||
|
||||
### 3. 测试邮件解析
|
||||
|
||||
运行测试工具验证配置:
|
||||
```bash
|
||||
npm run relay:test
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 邮件格式要求
|
||||
|
||||
#### 主题格式
|
||||
邮件主题必须包含 TaskPing Token,支持以下格式:
|
||||
- `[TaskPing #TOKEN]` - 推荐格式
|
||||
- `[TaskPing TOKEN]`
|
||||
- `TaskPing: TOKEN`
|
||||
|
||||
例如:
|
||||
- `Re: [TaskPing #ABC123] 任务等待您的指示`
|
||||
- `回复: TaskPing: XYZ789`
|
||||
|
||||
#### 命令格式
|
||||
支持三种命令输入方式:
|
||||
|
||||
1. **直接输入**(最简单)
|
||||
```
|
||||
继续执行
|
||||
```
|
||||
|
||||
2. **CMD 前缀**(明确标识)
|
||||
```
|
||||
CMD: npm run build
|
||||
```
|
||||
|
||||
3. **代码块**(复杂命令)
|
||||
````
|
||||
```
|
||||
git add .
|
||||
git commit -m "Update features"
|
||||
git push
|
||||
```
|
||||
````
|
||||
|
||||
### 示例场景
|
||||
|
||||
#### 场景 1: 简单确认
|
||||
收到邮件:
|
||||
```
|
||||
主题: [TaskPing #TASK001] 是否继续部署到生产环境?
|
||||
```
|
||||
|
||||
回复:
|
||||
```
|
||||
yes
|
||||
```
|
||||
|
||||
#### 场景 2: 执行具体命令
|
||||
收到邮件:
|
||||
```
|
||||
主题: [TaskPing #BUILD123] 构建失败,请输入修复命令
|
||||
```
|
||||
|
||||
回复:
|
||||
```
|
||||
CMD: npm install missing-package
|
||||
```
|
||||
|
||||
#### 场景 3: 多行命令
|
||||
收到邮件:
|
||||
```
|
||||
主题: [TaskPing #DEPLOY456] 准备部署,请确认步骤
|
||||
```
|
||||
|
||||
回复:
|
||||
````
|
||||
执行以下命令:
|
||||
|
||||
```
|
||||
npm run test
|
||||
npm run build
|
||||
npm run deploy
|
||||
```
|
||||
````
|
||||
|
||||
## 高级配置
|
||||
|
||||
### 会话管理
|
||||
|
||||
会话映射文件 `session-map.json` 结构:
|
||||
```json
|
||||
{
|
||||
"TOKEN123": {
|
||||
"type": "pty",
|
||||
"createdAt": 1234567890,
|
||||
"expiresAt": 1234654290,
|
||||
"cwd": "/path/to/project",
|
||||
"description": "构建项目 X"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量说明
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `IMAP_HOST` | IMAP 服务器地址 | 必需 |
|
||||
| `IMAP_PORT` | IMAP 端口 | 993 |
|
||||
| `IMAP_SECURE` | 使用 SSL/TLS | true |
|
||||
| `IMAP_USER` | 邮箱账号 | 必需 |
|
||||
| `IMAP_PASS` | 邮箱密码 | 必需 |
|
||||
| `ALLOWED_SENDERS` | 允许的发件人列表 | 空(接受所有) |
|
||||
| `SESSION_MAP_PATH` | 会话映射文件路径 | ./src/data/session-map.json |
|
||||
| `CLAUDE_CLI_PATH` | Claude CLI 路径 | claude |
|
||||
| `LOG_LEVEL` | 日志级别 | info |
|
||||
| `PTY_OUTPUT_LOG` | 记录 PTY 输出 | false |
|
||||
|
||||
### 邮件服务器配置示例
|
||||
|
||||
#### Gmail
|
||||
1. 启用 IMAP: 设置 → 转发和 POP/IMAP → 启用 IMAP
|
||||
2. 生成应用专用密码: 账号设置 → 安全 → 应用专用密码
|
||||
|
||||
```env
|
||||
IMAP_HOST=imap.gmail.com
|
||||
IMAP_PORT=993
|
||||
IMAP_SECURE=true
|
||||
```
|
||||
|
||||
#### Outlook/Office 365
|
||||
```env
|
||||
IMAP_HOST=outlook.office365.com
|
||||
IMAP_PORT=993
|
||||
IMAP_SECURE=true
|
||||
```
|
||||
|
||||
#### QQ 邮箱
|
||||
```env
|
||||
IMAP_HOST=imap.qq.com
|
||||
IMAP_PORT=993
|
||||
IMAP_SECURE=true
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **发件人验证**: 始终配置 `ALLOWED_SENDERS` 以限制谁可以发送命令
|
||||
2. **命令过滤**: 系统会自动过滤危险命令(如 `rm -rf`)
|
||||
3. **会话过期**: 会话有过期时间,过期后无法接受命令
|
||||
4. **邮件加密**: 使用 SSL/TLS 加密的 IMAP 连接
|
||||
5. **密码安全**: 使用应用专用密码而非主密码
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **无法连接到邮件服务器**
|
||||
- 检查 IMAP 是否已启用
|
||||
- 验证服务器地址和端口
|
||||
- 确认防火墙设置
|
||||
|
||||
2. **邮件未被处理**
|
||||
- 检查发件人是否在白名单中
|
||||
- 验证邮件主题格式
|
||||
- 查看日志中的错误信息
|
||||
|
||||
3. **命令未执行**
|
||||
- 确认 Claude Code 进程正在运行
|
||||
- 检查会话是否已过期
|
||||
- 验证命令格式是否正确
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
# 启动时查看详细日志
|
||||
LOG_LEVEL=debug npm run relay:pty
|
||||
|
||||
# 查看 PTY 输出
|
||||
PTY_OUTPUT_LOG=true npm run relay:pty
|
||||
```
|
||||
|
||||
### 测试命令
|
||||
|
||||
```bash
|
||||
# 测试邮件解析
|
||||
npm run relay:test
|
||||
|
||||
# 手动启动(调试模式)
|
||||
INJECTION_MODE=pty LOG_LEVEL=debug node src/relay/relay-pty.js
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用专用邮箱**: 为 TaskPing 创建专用邮箱账号
|
||||
2. **定期清理**: 定期清理过期会话和已处理邮件
|
||||
3. **命令简洁**: 保持命令简短明确
|
||||
4. **及时回复**: 在会话过期前回复邮件
|
||||
5. **安全优先**: 不要在邮件中包含敏感信息
|
||||
|
||||
## 集成到现有项目
|
||||
|
||||
如果您想将邮件回复功能集成到现有的 Claude Code 工作流:
|
||||
|
||||
1. 在发送提醒时创建会话:
|
||||
```javascript
|
||||
const token = generateToken();
|
||||
const session = {
|
||||
type: 'pty',
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: Math.floor((Date.now() + 3600000) / 1000),
|
||||
cwd: process.cwd()
|
||||
};
|
||||
saveSession(token, session);
|
||||
```
|
||||
|
||||
2. 在邮件主题中包含 Token:
|
||||
```javascript
|
||||
const subject = `[TaskPing #${token}] ${taskDescription}`;
|
||||
```
|
||||
|
||||
3. 启动 PTY Relay 服务监听回复
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [邮件配置指南](./EMAIL_GUIDE.md)
|
||||
- [快速邮件设置](./QUICK_EMAIL_SETUP.md)
|
||||
- [系统架构说明](./EMAIL_ARCHITECTURE.md)
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题,请查看:
|
||||
- 项目 Issue: https://github.com/JessyTsui/TaskPing/issues
|
||||
- 详细日志: `LOG_LEVEL=debug npm run relay:pty`
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
# 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编程的便利吧!🚀
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
# TaskPing 快速配置指南
|
||||
|
||||
## 🚀 5分钟快速开始
|
||||
|
||||
本指南帮助您快速配置 TaskPing 的邮件通知和回复功能。
|
||||
|
||||
## 第一步:配置邮件账号
|
||||
|
||||
### 1.1 复制环境配置文件
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 1.2 编辑 .env 文件
|
||||
|
||||
您的配置已经部分完成:
|
||||
- ✅ 发件服务器:飞书邮箱 (noreply@pandalla.ai)
|
||||
- ✅ 收件邮箱:Gmail (jiaxicui446@gmail.com)
|
||||
- ❌ Gmail 应用密码:需要您配置
|
||||
|
||||
### 1.3 获取 Gmail 应用专用密码
|
||||
|
||||
1. 登录您的 Gmail 账号 (jiaxicui446@gmail.com)
|
||||
2. 访问 [Google 账号安全设置](https://myaccount.google.com/security)
|
||||
3. 开启"两步验证"(如果未开启)
|
||||
4. 访问 [应用专用密码页面](https://myaccount.google.com/apppasswords)
|
||||
5. 选择应用"邮件",设备选择"其他"
|
||||
6. 输入名称"TaskPing"
|
||||
7. 点击"生成"获取16位密码
|
||||
8. 将密码复制到 .env 文件的 `IMAP_PASS` 字段
|
||||
|
||||
## 第二步:更新配置
|
||||
|
||||
```bash
|
||||
# 更新邮件配置到系统
|
||||
npm run email:config
|
||||
```
|
||||
|
||||
## 第三步:测试邮件发送
|
||||
|
||||
```bash
|
||||
# 发送测试邮件
|
||||
npm run email:test
|
||||
```
|
||||
|
||||
成功后您会看到:
|
||||
```
|
||||
✅ 邮件发送成功!
|
||||
Message ID: <xxx@pandalla.ai>
|
||||
Response: 250 2.0.0 OK: queued
|
||||
```
|
||||
|
||||
## 第四步:启动邮件监听服务
|
||||
|
||||
```bash
|
||||
# 启动 PTY 模式的邮件监听
|
||||
npm run relay:pty
|
||||
```
|
||||
|
||||
服务启动后会显示:
|
||||
```
|
||||
🚀 正在启动 TaskPing PTY Relay 服务...
|
||||
📧 IMAP服务器: imap.gmail.com
|
||||
👤 邮件账号: jiaxicui446@gmail.com
|
||||
```
|
||||
|
||||
## 第五步:测试完整流程
|
||||
|
||||
1. **检查收件箱**
|
||||
- 查看 jiaxicui446@gmail.com 是否收到测试邮件
|
||||
- 主题类似:`[TaskPing #TESTXXXXX] 测试邮件 - 等待您的指令`
|
||||
|
||||
2. **回复测试命令**
|
||||
- 直接回复邮件
|
||||
- 内容输入:`echo "Hello from TaskPing"`
|
||||
- 发送
|
||||
|
||||
3. **查看服务日志**
|
||||
- PTY Relay 服务会显示收到的命令
|
||||
- Claude Code 会执行该命令
|
||||
|
||||
## 📝 常用命令速查
|
||||
|
||||
| 功能 | 命令 |
|
||||
|------|------|
|
||||
| 更新邮件配置 | `npm run email:config` |
|
||||
| 测试邮件发送 | `npm run email:test` |
|
||||
| 启动监听服务 | `npm run relay:pty` |
|
||||
| 测试邮件解析 | `npm run relay:test` |
|
||||
| 查看配置状态 | `cat .env` |
|
||||
|
||||
## 🔧 故障排查
|
||||
|
||||
### 邮件发送失败
|
||||
- 检查飞书邮箱 SMTP 密码是否正确
|
||||
- 确认网络可以访问 smtp.feishu.cn:465
|
||||
|
||||
### 邮件接收失败
|
||||
- 确认 Gmail 应用专用密码已配置
|
||||
- 检查 Gmail 是否开启了 IMAP
|
||||
- 查看服务日志中的错误信息
|
||||
|
||||
### 命令未执行
|
||||
- 确认回复邮件的主题包含原始 Token
|
||||
- 检查邮件内容格式是否正确
|
||||
- 验证 Claude Code 是否在运行
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
1. **邮件回复格式**
|
||||
```
|
||||
# 简单命令
|
||||
继续
|
||||
|
||||
# 明确命令
|
||||
CMD: npm run build
|
||||
|
||||
# 多行命令
|
||||
```
|
||||
git add .
|
||||
git commit -m "Update"
|
||||
```
|
||||
```
|
||||
|
||||
2. **会话管理**
|
||||
- 每个会话有1小时有效期
|
||||
- Token 在邮件主题中:`[TaskPing #TOKEN]`
|
||||
- 过期后需要新的通知邮件
|
||||
|
||||
3. **安全建议**
|
||||
- 只从受信任的邮箱发送命令
|
||||
- 定期更换应用专用密码
|
||||
- 不要在邮件中包含敏感信息
|
||||
|
||||
## 🎯 实际使用场景
|
||||
|
||||
1. **移动办公**
|
||||
- 在手机上收到任务通知
|
||||
- 直接回复邮件继续任务
|
||||
- 无需返回电脑操作
|
||||
|
||||
2. **远程协作**
|
||||
- 团队成员可以通过邮件控制任务
|
||||
- 支持多人协作(配置白名单)
|
||||
- 保留邮件审计记录
|
||||
|
||||
3. **自动化工作流**
|
||||
- 集成到现有邮件系统
|
||||
- 支持邮件规则触发
|
||||
- 可以配置自动回复
|
||||
|
||||
## 📚 更多文档
|
||||
|
||||
- [详细邮件配置指南](./EMAIL_GUIDE.md)
|
||||
- [邮件回复功能说明](./EMAIL_REPLY_GUIDE.md)
|
||||
- [系统架构文档](./EMAIL_ARCHITECTURE.md)
|
||||
|
||||
---
|
||||
|
||||
有问题?查看 [GitHub Issues](https://github.com/JessyTsui/TaskPing/issues) 或运行调试模式:
|
||||
```bash
|
||||
LOG_LEVEL=debug npm run relay:pty
|
||||
```
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
#!/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);
|
||||
364
email-checker.js
364
email-checker.js
|
|
@ -1,364 +0,0 @@
|
|||
#!/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,155 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Global Installation Script for TaskPing claude-control
|
||||
* Makes claude-control.js accessible from any directory
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const SCRIPT_NAME = 'claude-control';
|
||||
const SOURCE_PATH = path.join(__dirname, 'claude-control.js');
|
||||
const TARGET_DIR = '/usr/local/bin';
|
||||
const TARGET_PATH = path.join(TARGET_DIR, SCRIPT_NAME);
|
||||
|
||||
function checkRequirements() {
|
||||
// Check if claude-control.js exists
|
||||
if (!fs.existsSync(SOURCE_PATH)) {
|
||||
console.error('❌ Error: claude-control.js not found in current directory');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if /usr/local/bin is writable
|
||||
try {
|
||||
fs.accessSync(TARGET_DIR, fs.constants.W_OK);
|
||||
} catch (error) {
|
||||
console.error('❌ Error: No write permission to /usr/local/bin');
|
||||
console.log('💡 Try running with sudo:');
|
||||
console.log(' sudo node install-global.js');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function createGlobalScript() {
|
||||
const scriptContent = `#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Global Claude Control Wrapper
|
||||
* Executes claude-control.js from its original location
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// TaskPing installation directory
|
||||
const TASKPING_DIR = '${__dirname}';
|
||||
const CLAUDE_CONTROL_PATH = path.join(TASKPING_DIR, 'claude-control.js');
|
||||
|
||||
// Get command line arguments (excluding node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Change to TaskPing directory before execution
|
||||
process.chdir(TASKPING_DIR);
|
||||
|
||||
// Execute claude-control.js with original arguments
|
||||
const child = spawn('node', [CLAUDE_CONTROL_PATH, ...args], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, TASKPING_HOME: TASKPING_DIR }
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error('Error executing claude-control:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
} else {
|
||||
process.exit(code || 0);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
return scriptContent;
|
||||
}
|
||||
|
||||
function install() {
|
||||
console.log('🚀 Installing claude-control globally...\n');
|
||||
|
||||
try {
|
||||
// Create the global script
|
||||
const scriptContent = createGlobalScript();
|
||||
fs.writeFileSync(TARGET_PATH, scriptContent);
|
||||
|
||||
// Make it executable
|
||||
fs.chmodSync(TARGET_PATH, 0o755);
|
||||
|
||||
console.log('✅ Installation completed successfully!');
|
||||
console.log(`📁 Installed to: ${TARGET_PATH}`);
|
||||
console.log('\n🎉 Usage:');
|
||||
console.log(' claude-control --session myproject');
|
||||
console.log(' claude-control --list');
|
||||
console.log(' claude-control --kill all');
|
||||
console.log('\nYou can now run claude-control from any directory!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Installation failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function uninstall() {
|
||||
console.log('🗑️ Uninstalling claude-control...\n');
|
||||
|
||||
try {
|
||||
if (fs.existsSync(TARGET_PATH)) {
|
||||
fs.unlinkSync(TARGET_PATH);
|
||||
console.log('✅ Uninstallation completed successfully!');
|
||||
console.log(`🗑️ Removed: ${TARGET_PATH}`);
|
||||
} else {
|
||||
console.log('⚠️ claude-control is not installed globally');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Uninstallation failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
console.log('TaskPing Claude Control - Global Installation\n');
|
||||
console.log('Usage:');
|
||||
console.log(' node install-global.js [install] - Install globally');
|
||||
console.log(' node install-global.js uninstall - Uninstall');
|
||||
console.log(' node install-global.js --help - Show this help\n');
|
||||
console.log('Requirements:');
|
||||
console.log(' - Write permission to /usr/local/bin (may need sudo)');
|
||||
console.log(' - claude-control.js must exist in current directory');
|
||||
}
|
||||
|
||||
function main() {
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command === '--help' || command === '-h') {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'uninstall') {
|
||||
uninstall();
|
||||
return;
|
||||
}
|
||||
|
||||
// Default action is install
|
||||
checkRequirements();
|
||||
install();
|
||||
}
|
||||
|
||||
// Run only if this script is executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { install, uninstall };
|
||||
|
|
@ -1,447 +0,0 @@
|
|||
# TaskPing — 邮件回复写回 Claude Code CLI(执行手册)
|
||||
|
||||
> 目标读者:能执行脚本的 AI/自动化代理
|
||||
> 运行环境:Linux/macOS(建议),Node.js ≥ 18,Claude Code 已安装并可在终端运行 `claude`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
**项目背景**:用户在本机通过 Claude Code(CLI)进行编码。当前已实现:
|
||||
- 通过 hooks 在任务结束/等待下一步时**发出邮件提醒**;
|
||||
- 用户在手机上**回复邮件**;
|
||||
- 本机有一个常驻“中继/Relay”进程轮询收件箱,能够拿到该回复。
|
||||
|
||||
**当前卡点**:**把邮件正文写回到对应的 Claude Code CLI 会话,并回车执行**。
|
||||
|
||||
**本文档目的**:为 AI/自动化代理提供两种可执行的落地方案:
|
||||
1) **子进程/PTY 模式**:用 `node-pty` 驱动一个 Claude Code 子进程,直接向伪终端写入并回车;
|
||||
2) **tmux 模式**:让 Claude Code 运行在指定 `tmux` pane,收到邮件后用 `tmux send-keys` 注入文本并回车。
|
||||
|
||||
两种方案都要求:
|
||||
- 能**路由**到正确的 Claude 会话;
|
||||
- 能**清洗**邮件正文,仅保留要注入的命令;
|
||||
- 有**安全**与**幂等**控制;
|
||||
- 有**可观测性**(日志/Tracing)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 全局约束与安全要求
|
||||
|
||||
- **会话识别**:优先按 `In-Reply-To` 的 `Message-ID` 匹配;回退使用 `Subject` 中的 token(形如 `[TaskPing #ABC123]`)。
|
||||
- **白名单**:仅允许来自配置的发件人域/邮箱(通过 SPF/DKIM/DMARC 验证后再放行)。
|
||||
- **正文提取**:只取**最新回复**;去除历史引用、签名、HTML 标签、图片占位。支持三种输入:
|
||||
- 纯文本一行命令;
|
||||
- 代码块 ``` 包起来的命令(优先级最高);
|
||||
- 主题/首行以 `CMD:` 前缀标识。
|
||||
- **幂等**:用 `Message-ID` 或 `gmailThreadId` 去重(重复投递不再次注入)。
|
||||
- **限流**:同一会话每分钟最多 1 条;单条长度上限(例如 8KB)。
|
||||
- **日志**:记录 token、会话、pane/pty id、注入摘要(前 120 字符),屏蔽隐私。
|
||||
|
||||
---
|
||||
|
||||
## 3. 公共依赖与配置
|
||||
|
||||
### 3.1 依赖(Node.js)
|
||||
```bash
|
||||
npm i imapflow mailparser node-pty pino dotenv execa
|
||||
# tmux 方案需要系统安装 tmux:macOS: brew install tmux;Debian/Ubuntu: apt-get install tmux
|
||||
```
|
||||
|
||||
### 3.2 环境变量(`.env`)
|
||||
```bash
|
||||
# 邮件接收
|
||||
IMAP_HOST=imap.example.com
|
||||
IMAP_PORT=993
|
||||
IMAP_SECURE=true
|
||||
IMAP_USER=bot@example.com
|
||||
IMAP_PASS=********
|
||||
|
||||
# 邮件路由安全
|
||||
ALLOWED_SENDERS=jessy@example.com,panda@company.com
|
||||
|
||||
# 路由与会话存储(JSON 文件路径)
|
||||
SESSION_MAP_PATH=/var/lib/taskping/session-map.json
|
||||
|
||||
# 模式选择:pty 或 tmux
|
||||
INJECTION_MODE=pty
|
||||
|
||||
# tmux 方案可选:默认 session/pane 名称前缀
|
||||
TMUX_SESSION_PREFIX=taskping
|
||||
```
|
||||
|
||||
### 3.3 会话映射(`SESSION_MAP_PATH`)
|
||||
结构示例:
|
||||
```json
|
||||
{
|
||||
"ABC123": {
|
||||
"type": "pty",
|
||||
"ptyId": "b4c1...",
|
||||
"createdAt": 1732672100,
|
||||
"expiresAt": 1732679300,
|
||||
"messageId": "<CA+abc@mail.example.com>",
|
||||
"imapUid": 12345
|
||||
},
|
||||
"XYZ789": {
|
||||
"type": "tmux",
|
||||
"session": "taskping-XYZ789",
|
||||
"pane": "taskping-XYZ789.0",
|
||||
"createdAt": 1732672200,
|
||||
"expiresAt": 1732679400,
|
||||
"messageId": "<CA+xyz@mail.example.com>"
|
||||
}
|
||||
}
|
||||
```
|
||||
> 注:会话映射由**发送提醒时**创建(包含 token 与目标 CLI 会话信息)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 方案一:子进程/PTY 模式(推荐)
|
||||
|
||||
### 4.1 适用场景
|
||||
- 由中继程序**直接管理** Claude Code 生命周期;
|
||||
- 需要最稳定的注入通道(不依赖额外终端复用器)。
|
||||
|
||||
### 4.2 实现思路
|
||||
- 用 `node-pty` `spawn('claude', [...])` 启动 CLI;
|
||||
- 将返回的 `pty` 与生成的 token 绑定,写入 `SESSION_MAP_PATH`;
|
||||
- 收到邮件 → 路由 token → 清洗正文 → `pty.write(cmd + '\r')`;
|
||||
- 监控 `pty.onData`,必要时截取摘要回传提醒(可选)。
|
||||
|
||||
### 4.3 关键代码骨架(`relay-pty.js`)
|
||||
```js
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import pino from 'pino';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { spawn as spawnPty } from 'node-pty';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
const log = pino({ level: 'info' });
|
||||
const SESS_PATH = process.env.SESSION_MAP_PATH;
|
||||
|
||||
function loadSessions() {
|
||||
if (!existsSync(SESS_PATH)) return {};
|
||||
return JSON.parse(readFileSync(SESS_PATH, 'utf8'));
|
||||
}
|
||||
function saveSessions(map) {
|
||||
writeFileSync(SESS_PATH, JSON.stringify(map, null, 2));
|
||||
}
|
||||
|
||||
function normalizeAllowlist() {
|
||||
return (process.env.ALLOWED_SENDERS || '')
|
||||
.split(',')
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
const ALLOW = new Set(normalizeAllowlist());
|
||||
|
||||
function isAllowed(addressObj) {
|
||||
const list = []
|
||||
.concat(addressObj?.value || [])
|
||||
.map(a => (a.address || '').toLowerCase());
|
||||
return list.some(addr => ALLOW.has(addr));
|
||||
}
|
||||
|
||||
function extractTokenFromSubject(subject = '') {
|
||||
const m = subject.match(/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
function stripReply(text = '') {
|
||||
// 优先识别 ``` 块
|
||||
const codeBlock = text.match(/```[\s\S]*?```/);
|
||||
if (codeBlock) {
|
||||
return codeBlock[0].replace(/```/g, '').trim();
|
||||
}
|
||||
// 取首行或以 CMD: 前缀
|
||||
const firstLine = text.split(/\r?\n/).find(l => l.trim().length > 0) || '';
|
||||
const cmdPrefix = firstLine.match(/^CMD:\s*(.+)$/i);
|
||||
const candidate = (cmdPrefix ? cmdPrefix[1] : firstLine).trim();
|
||||
|
||||
// 去除引用与签名
|
||||
return candidate
|
||||
.replace(/^>.*$/gm, '')
|
||||
.replace(/^--\s+[\s\S]*$/m, '')
|
||||
.slice(0, 8192) // 长度限制
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function handleMailMessage(source) {
|
||||
const parsed = await simpleParser(source);
|
||||
if (!isAllowed(parsed.from)) {
|
||||
log.warn({ from: parsed.from?.text }, 'sender not allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = parsed.subject || '';
|
||||
const token = extractTokenFromSubject(subject);
|
||||
if (!token) {
|
||||
log.warn({ subject }, 'no token in subject');
|
||||
return;
|
||||
}
|
||||
|
||||
const sessions = loadSessions();
|
||||
const sess = sessions[token];
|
||||
if (!sess || sess.expiresAt * 1000 < Date.now()) {
|
||||
log.warn({ token }, 'session not found or expired');
|
||||
return;
|
||||
}
|
||||
if (sess.type !== 'pty' || !sess.ptyId) {
|
||||
log.error({ token }, 'session is not pty type');
|
||||
return;
|
||||
}
|
||||
|
||||
const cmd = stripReply(parsed.text || parsed.html || '');
|
||||
if (!cmd) {
|
||||
log.warn({ token }, 'empty command after strip');
|
||||
return;
|
||||
}
|
||||
|
||||
// 取得 pty 实例:这里演示为“按需重建/保持单例”两种策略之一
|
||||
const pty = getOrRestorePty(sess);
|
||||
log.info({ token, cmd: cmd.slice(0, 120) }, 'inject command');
|
||||
pty.write(cmd + '\r');
|
||||
}
|
||||
|
||||
const PTY_POOL = new Map();
|
||||
function getOrRestorePty(sess) {
|
||||
if (PTY_POOL.has(sess.ptyId)) return PTY_POOL.get(sess.ptyId);
|
||||
|
||||
// 如果需要重建,会话应当保存启动参数;这里演示简化为新起一个 claude
|
||||
const shell = 'claude'; // 或绝对路径
|
||||
const pty = spawnPty(shell, [], {
|
||||
name: 'xterm-color',
|
||||
cols: 120,
|
||||
rows: 32
|
||||
});
|
||||
PTY_POOL.set(sess.ptyId, pty);
|
||||
pty.onData(d => process.stdout.write(d)); // 可替换为日志/回传
|
||||
pty.onExit(() => PTY_POOL.delete(sess.ptyId));
|
||||
return pty;
|
||||
}
|
||||
|
||||
async function startImap() {
|
||||
const client = new ImapFlow({
|
||||
host: process.env.IMAP_HOST,
|
||||
port: Number(process.env.IMAP_PORT || 993),
|
||||
secure: process.env.IMAP_SECURE === 'true',
|
||||
auth: { user: process.env.IMAP_USER, pass: process.env.IMAP_PASS }
|
||||
});
|
||||
await client.connect();
|
||||
const lock = await client.getMailboxLock('INBOX');
|
||||
try {
|
||||
for await (const msg of client.monitor()) {
|
||||
if (msg.type === 'exists') {
|
||||
const { uid } = msg;
|
||||
const { source } = await client.download('INBOX', uid);
|
||||
const chunks = [];
|
||||
for await (const c of source) chunks.push(c);
|
||||
await handleMailMessage(Buffer.concat(chunks));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.INJECTION_MODE === 'pty') {
|
||||
startImap().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
> 说明:生产化时,建议在**发送提醒**一侧创建会话并持久化 `ptyId` 与参数;这里演示了“按需复原”的简化路径。
|
||||
|
||||
### 4.4 启动
|
||||
```bash
|
||||
node relay-pty.js
|
||||
```
|
||||
|
||||
### 4.5 验收清单
|
||||
- [ ] 未授权邮箱无法触发注入;
|
||||
- [ ] 合法邮件写入后,Claude Code 能立即进入下一步;
|
||||
- [ ] 重复转发的同一邮件不会二次注入;
|
||||
- [ ] 过期会话拒绝注入;
|
||||
- [ ] 大段/HTML/带签名的邮件能正确抽取命令。
|
||||
|
||||
---
|
||||
|
||||
## 5. 方案二:tmux 模式(简单稳妥)
|
||||
|
||||
### 5.1 适用场景
|
||||
- 你已经在 `tmux` 里运行 Claude Code;
|
||||
- 希望由外部进程向**指定 pane** 注入按键,不改变现有启动流程。
|
||||
|
||||
### 5.2 实现思路
|
||||
- 为每个 token 创建/记录一个 `tmux` session 与 pane:`session = taskping-<token>`;
|
||||
- Claude Code 在该 pane 里运行;
|
||||
- 收到邮件 → 定位到 pane → `tmux send-keys -t <pane> "<cmd>" Enter`。
|
||||
|
||||
### 5.3 管理脚本(`tmux-utils.sh`)
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SESSION="$1" # 例如 taskping-ABC123
|
||||
CMD="${2:-}" # 可选:一次性注入命令
|
||||
|
||||
if ! tmux has-session -t "${SESSION}" 2>/dev/null; then
|
||||
tmux new-session -d -s "${SESSION}"
|
||||
tmux rename-window -t "${SESSION}:0" "claude"
|
||||
tmux send-keys -t "${SESSION}:0" "claude" C-m
|
||||
sleep 0.5
|
||||
fi
|
||||
|
||||
if [ -n "${CMD}" ]; then
|
||||
tmux send-keys -t "${SESSION}:0" "${CMD}" C-m
|
||||
fi
|
||||
|
||||
# 输出 pane 目标名,供上层程序记录
|
||||
echo "${SESSION}.0"
|
||||
```
|
||||
|
||||
### 5.4 注入程序(`relay-tmux.js`)
|
||||
```js
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import pino from 'pino';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import dotenv from 'dotenv';
|
||||
import { execa } from 'execa';
|
||||
|
||||
dotenv.config();
|
||||
const log = pino({ level: 'info' });
|
||||
const SESS_PATH = process.env.SESSION_MAP_PATH;
|
||||
|
||||
function loadSessions() {
|
||||
if (!existsSync(SESS_PATH)) return {};
|
||||
return JSON.parse(readFileSync(SESS_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
function extractTokenFromSubject(subject = '') {
|
||||
const m = subject.match(/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
function stripReply(text = '') {
|
||||
const codeBlock = text.match(/```[\s\S]*?```/);
|
||||
if (codeBlock) return codeBlock[0].replace(/```/g, '').trim();
|
||||
const firstLine = text.split(/\r?\n/).find(l => l.trim().length > 0) || '';
|
||||
const cmdPrefix = firstLine.match(/^CMD:\s*(.+)$/i);
|
||||
return (cmdPrefix ? cmdPrefix[1] : firstLine).trim().slice(0, 8192);
|
||||
}
|
||||
|
||||
async function injectToTmux(sessionName, cmd) {
|
||||
const utils = new URL('./tmux-utils.sh', import.meta.url).pathname;
|
||||
const { stdout } = await execa('bash', [utils, sessionName, cmd], { stdio: 'pipe' });
|
||||
return stdout.trim(); // 返回 pane 目标名
|
||||
}
|
||||
|
||||
async function startImap() {
|
||||
const client = new ImapFlow({
|
||||
host: process.env.IMAP_HOST,
|
||||
port: Number(process.env.IMAP_PORT || 993),
|
||||
secure: process.env.IMAP_SECURE === 'true',
|
||||
auth: { user: process.env.IMAP_USER, pass: process.env.IMAP_PASS }
|
||||
});
|
||||
await client.connect();
|
||||
await client.mailboxOpen('INBOX');
|
||||
|
||||
for await (const msg of client.monitor()) {
|
||||
if (msg.type !== 'exists') continue;
|
||||
const { uid } = msg;
|
||||
const { source } = await client.download('INBOX', uid);
|
||||
const chunks = [];
|
||||
for await (const c of source) chunks.push(c);
|
||||
const parsed = await simpleParser(Buffer.concat(chunks));
|
||||
|
||||
const token = extractTokenFromSubject(parsed.subject || '');
|
||||
if (!token) continue;
|
||||
|
||||
const cmd = stripReply(parsed.text || parsed.html || '');
|
||||
if (!cmd) continue;
|
||||
|
||||
const sessionName = `${process.env.TMUX_SESSION_PREFIX || 'taskping'}-${token}`;
|
||||
const pane = await injectToTmux(sessionName, cmd);
|
||||
console.log(`[inject] ${sessionName} -> ${pane} :: ${cmd.slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.INJECTION_MODE === 'tmux') {
|
||||
startImap().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 启动
|
||||
```bash
|
||||
chmod +x tmux-utils.sh
|
||||
INJECTION_MODE=tmux node relay-tmux.js
|
||||
```
|
||||
|
||||
### 5.6 验收清单
|
||||
- [ ] `tmux` 中能看到 Claude 进程始终在运行;
|
||||
- [ ] 邮件回复注入后,当前 pane 光标位置正确、命令无截断;
|
||||
- [ ] 会话名与 token 一一对应,过期会话自动清理;
|
||||
- [ ] `tmux` 重启或 SSH 断连后自动恢复。
|
||||
|
||||
---
|
||||
|
||||
## 6. 选择建议与权衡
|
||||
|
||||
| 维度 | 子进程/PTY | tmux |
|
||||
|---|---|---|
|
||||
| 复杂度 | 中 | 低 |
|
||||
| 稳定性 | 高(直连伪终端) | 高(依赖 tmux 自愈) |
|
||||
| 多会话并发 | 简单(多 PTY) | 简单(多 pane/session) |
|
||||
| 恢复/重连 | 需自管 | `tmux` 自带 |
|
||||
| 可移植性 | 受 pty/Windows 影响 | Linux/macOS 友好 |
|
||||
| 外部可观测 | 需自行实现 | `tmux capture-pane` 可用 |
|
||||
|
||||
**建议**:桌面或服务器长期常驻,优先 **PTY 模式**;已广泛使用 `tmux` 的团队,直接 **tmux 模式**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试用例(AI 执行)
|
||||
|
||||
1. **路由正确性**:构造两个 token(A、B),分别回复不同命令,验证注入不串台。
|
||||
2. **HTML 邮件**:从 iOS/Android 客户端发送富文本,确认能抽取纯文本命令。
|
||||
3. **代码块优先**:正文包含代码块和普通文本时,优先选用代码块内容。
|
||||
4. **幂等**:同一封邮件重复投递三次,仅执行一次。
|
||||
5. **过期处理**:将 `expiresAt` 回退到过去,验证拒绝注入。
|
||||
6. **白名单**:非授权邮箱发送,日志报警且不注入。
|
||||
7. **超长命令**:超过 8KB 截断并拒绝,返回提示。
|
||||
|
||||
---
|
||||
|
||||
## 8. 运行与守护
|
||||
|
||||
- 建议用 `pm2`/`systemd` 管理 `relay-pty.js` 或 `relay-tmux.js`;
|
||||
- 每 20 分钟向 IMAP 发送 NOOP(ImapFlow monitor 已处理),断线自动重连;
|
||||
- 定时清理过期会话与孤儿 `tmux` session / PTY。
|
||||
|
||||
---
|
||||
|
||||
## 9. 故障排查
|
||||
|
||||
- **注入后无响应**:检查会话路由是否匹配(token、pane/pty);确认 Claude 是否在**等待输入**的状态。
|
||||
- **乱码/截断**:调大 pty 列数;tmux 保证 pane 有焦点且无行编辑冲突。
|
||||
- **重复执行**:确认以 `Message-ID` 去重;避免多客户端造成“已读后又转发”。
|
||||
- **权限问题**:`tmux` 由同一用户启动;Node 进程需与 `tmux` 同一用户。
|
||||
|
||||
---
|
||||
|
||||
## 10. 清单(AI 请按序执行)
|
||||
|
||||
1. 安装依赖与 `tmux`(如需)。
|
||||
2. 写入 `.env`(IMAP/白名单/模式)。
|
||||
3. 准备并加载 `SESSION_MAP_PATH`(由发送提醒侧写入)。
|
||||
4. 选择 **INJECTION_MODE=pty** 或 **tmux**,启动对应 `relay-*.js`。
|
||||
5. 发送带 `[TaskPing #TOKEN]` 的提醒邮件,收到后**回复命令**进行回归测试。
|
||||
6. 通过测试清单后,接入 hooks,开启常驻与告警监控。
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 发送测试邮件回复到relay服务
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
async function sendTestReply() {
|
||||
console.log('📧 发送测试邮件回复...\n');
|
||||
|
||||
// 创建测试用的SMTP传输器(使用Gmail)
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jiaxicui446@gmail.com',
|
||||
pass: process.env.GMAIL_APP_PASSWORD || 'your-app-password'
|
||||
}
|
||||
});
|
||||
|
||||
// 使用最新的token
|
||||
const testToken = 'V5UPZ1UE'; // 来自session-map.json的最新token
|
||||
|
||||
const mailOptions = {
|
||||
from: 'jiaxicui446@gmail.com',
|
||||
to: 'noreply@pandalla.ai',
|
||||
subject: `Re: [TaskPing #${testToken}] Claude Code 任务完成 - TaskPing`,
|
||||
text: '请解释一下量子计算的基本原理',
|
||||
replyTo: 'jiaxicui446@gmail.com'
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log('✅ 测试邮件发送成功!');
|
||||
console.log(`📧 Message ID: ${info.messageId}`);
|
||||
console.log(`📋 Token: ${testToken}`);
|
||||
console.log(`💬 Command: ${mailOptions.text}`);
|
||||
console.log('\n🔍 现在监控relay服务日志...');
|
||||
|
||||
// 等待几秒让邮件被处理
|
||||
setTimeout(() => {
|
||||
console.log('\n📋 请检查relay-debug.log文件查看处理日志');
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 邮件发送失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
sendTestReply().catch(console.error);
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Gmail 应用专用密码设置指南
|
||||
* 帮助用户配置 Gmail 的应用专用密码
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
|
||||
function displayInstructions() {
|
||||
console.log('╔═══════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Gmail 应用专用密码配置指南 ║');
|
||||
console.log('╚═══════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
console.log('📋 配置步骤:\n');
|
||||
|
||||
console.log('1️⃣ 登录 Gmail 账号');
|
||||
console.log(' 🌐 访问: https://myaccount.google.com/');
|
||||
console.log(' 👤 使用您的账号: jiaxicui446@gmail.com\n');
|
||||
|
||||
console.log('2️⃣ 开启两步验证(如果未开启)');
|
||||
console.log(' 🌐 访问: https://myaccount.google.com/security');
|
||||
console.log(' 🔐 找到"两步验证"并开启');
|
||||
console.log(' 📱 可以使用手机短信或认证器应用\n');
|
||||
|
||||
console.log('3️⃣ 生成应用专用密码');
|
||||
console.log(' 🌐 访问: https://myaccount.google.com/apppasswords');
|
||||
console.log(' 📧 选择应用: "邮件"');
|
||||
console.log(' 💻 选择设备: "其他(自定义名称)"');
|
||||
console.log(' ✏️ 输入名称: "TaskPing Email Relay"');
|
||||
console.log(' 🔑 点击"生成"');
|
||||
console.log(' 📋 复制 16 位密码(格式类似:abcd efgh ijkl mnop)\n');
|
||||
|
||||
console.log('4️⃣ 启用 IMAP(如果未启用)');
|
||||
console.log(' 🌐 访问: https://mail.google.com/mail/u/0/#settings/fwdandpop');
|
||||
console.log(' 📩 找到"IMAP 访问"');
|
||||
console.log(' ✅ 选择"启用 IMAP"');
|
||||
console.log(' 💾 保存更改\n');
|
||||
|
||||
console.log('⚠️ 重要提示:');
|
||||
console.log(' • 应用专用密码只显示一次,请妥善保存');
|
||||
console.log(' • 不要使用您的 Google 账号密码');
|
||||
console.log(' • 密码格式是 16 位,包含空格(请保留空格)');
|
||||
console.log(' • 如果忘记密码,需要删除后重新生成\n');
|
||||
}
|
||||
|
||||
async function promptForPassword() {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log('📝 请输入您生成的应用专用密码:');
|
||||
console.log(' (格式: abcd efgh ijkl mnop)');
|
||||
rl.question('密码: ', (password) => {
|
||||
rl.close();
|
||||
resolve(password.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
// Gmail 应用专用密码格式:16位字符,可能包含空格
|
||||
const cleanPassword = password.replace(/\s/g, '');
|
||||
|
||||
if (cleanPassword.length !== 16) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '密码长度应为16位字符'
|
||||
};
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9]+$/.test(cleanPassword)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '密码只能包含字母和数字'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
cleanPassword
|
||||
};
|
||||
}
|
||||
|
||||
function updateEnvFile(password) {
|
||||
const envPath = '.env';
|
||||
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.error('❌ 未找到 .env 文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let content = fs.readFileSync(envPath, 'utf8');
|
||||
|
||||
// 替换 IMAP_PASS 行
|
||||
const updated = content.replace(
|
||||
/IMAP_PASS=.*/,
|
||||
`IMAP_PASS=${password}`
|
||||
);
|
||||
|
||||
fs.writeFileSync(envPath, updated);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 更新 .env 文件失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection(password) {
|
||||
console.log('\n🔍 测试 Gmail IMAP 连接...');
|
||||
|
||||
// 临时设置环境变量
|
||||
process.env.IMAP_PASS = password;
|
||||
|
||||
try {
|
||||
// 动态导入 ImapFlow
|
||||
const { ImapFlow } = require('imapflow');
|
||||
|
||||
const client = new ImapFlow({
|
||||
host: 'imap.gmail.com',
|
||||
port: 993,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'jiaxicui446@gmail.com',
|
||||
pass: password
|
||||
},
|
||||
logger: false
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
console.log('✅ Gmail IMAP 连接成功!');
|
||||
|
||||
const status = await client.status('INBOX', { messages: true, unseen: true });
|
||||
console.log(`📧 收件箱状态: ${status.messages} 封邮件,${status.unseen} 封未读`);
|
||||
|
||||
await client.logout();
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 连接失败:', error.message);
|
||||
|
||||
if (error.code === 'EAUTH') {
|
||||
console.log('\n可能的原因:');
|
||||
console.log('• 应用专用密码错误');
|
||||
console.log('• 两步验证未开启');
|
||||
console.log('• IMAP 未启用');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
displayInstructions();
|
||||
|
||||
console.log('═'.repeat(65));
|
||||
console.log('现在开始配置应用专用密码\n');
|
||||
|
||||
// 检查是否已经完成网页配置
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const ready = await new Promise((resolve) => {
|
||||
rl.question('已完成上述步骤并获得应用专用密码?(y/n): ', (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.toLowerCase() === 'y');
|
||||
});
|
||||
});
|
||||
|
||||
if (!ready) {
|
||||
console.log('\n请先完成上述配置步骤,然后重新运行此脚本');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取密码
|
||||
const password = await promptForPassword();
|
||||
|
||||
if (!password) {
|
||||
console.log('❌ 未输入密码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码格式
|
||||
const validation = validatePassword(password);
|
||||
if (!validation.valid) {
|
||||
console.log(`❌ 密码格式错误: ${validation.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanPassword = validation.cleanPassword;
|
||||
console.log('✅ 密码格式正确');
|
||||
|
||||
// 测试连接
|
||||
const connected = await testConnection(cleanPassword);
|
||||
|
||||
if (!connected) {
|
||||
console.log('\n请检查配置后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 .env 文件
|
||||
const updated = updateEnvFile(cleanPassword);
|
||||
|
||||
if (updated) {
|
||||
console.log('\n✅ 配置已保存到 .env 文件');
|
||||
console.log('\n🚀 下一步操作:');
|
||||
console.log(' 1. 运行: npm run email:config');
|
||||
console.log(' 2. 运行: npm run email:test');
|
||||
console.log(' 3. 运行: npm run relay:pty');
|
||||
console.log('\n🎉 Gmail 应用专用密码配置完成!');
|
||||
} else {
|
||||
console.log('\n❌ 保存配置失败,请手动编辑 .env 文件');
|
||||
console.log(` IMAP_PASS=${cleanPassword}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
main().catch(console.error);
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
#!/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;
|
||||
197
simple-start.js
197
simple-start.js
|
|
@ -1,197 +0,0 @@
|
|||
#!/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);
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ const nodemailer = require('nodemailer');
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const TmuxMonitor = require('../../utils/tmux-monitor');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class EmailChannel extends NotificationChannel {
|
||||
constructor(config = {}) {
|
||||
|
|
@ -15,6 +17,7 @@ class EmailChannel extends NotificationChannel {
|
|||
this.transporter = null;
|
||||
this.sessionsDir = path.join(__dirname, '../../data/sessions');
|
||||
this.templatesDir = path.join(__dirname, '../../assets/email-templates');
|
||||
this.tmuxMonitor = new TmuxMonitor();
|
||||
|
||||
this._ensureDirectories();
|
||||
this._initializeTransporter();
|
||||
|
|
@ -29,6 +32,16 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
}
|
||||
|
||||
_generateToken() {
|
||||
// 生成简短的Token (大写字母+数字,8位)
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let token = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
_initializeTransporter() {
|
||||
if (!this.config.smtp) {
|
||||
this.logger.warn('SMTP configuration not found');
|
||||
|
|
@ -56,6 +69,21 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
}
|
||||
|
||||
_getCurrentTmuxSession() {
|
||||
try {
|
||||
// Try to get current tmux session
|
||||
const tmuxSession = execSync('tmux display-message -p "#S"', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
return tmuxSession || null;
|
||||
} catch (error) {
|
||||
// Not in a tmux session or tmux not available
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async _sendImpl(notification) {
|
||||
if (!this.transporter) {
|
||||
throw new Error('Email transporter not initialized');
|
||||
|
|
@ -65,14 +93,26 @@ class EmailChannel extends NotificationChannel {
|
|||
throw new Error('Email recipient not configured');
|
||||
}
|
||||
|
||||
// 生成会话ID
|
||||
// 生成会话ID和Token
|
||||
const sessionId = uuidv4();
|
||||
const token = this._generateToken();
|
||||
|
||||
// 获取当前tmux会话和对话内容
|
||||
const tmuxSession = this._getCurrentTmuxSession();
|
||||
if (tmuxSession && !notification.metadata) {
|
||||
const conversation = this.tmuxMonitor.getRecentConversation(tmuxSession);
|
||||
notification.metadata = {
|
||||
userQuestion: conversation.userQuestion || notification.message,
|
||||
claudeResponse: conversation.claudeResponse || notification.message,
|
||||
tmuxSession: tmuxSession
|
||||
};
|
||||
}
|
||||
|
||||
// 创建会话记录
|
||||
await this._createSession(sessionId, notification);
|
||||
await this._createSession(sessionId, notification, token);
|
||||
|
||||
// 生成邮件内容
|
||||
const emailContent = this._generateEmailContent(notification, sessionId);
|
||||
const emailContent = this._generateEmailContent(notification, sessionId, token);
|
||||
|
||||
const mailOptions = {
|
||||
from: this.config.from || this.config.smtp.auth.user,
|
||||
|
|
@ -99,11 +139,16 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
}
|
||||
|
||||
async _createSession(sessionId, notification) {
|
||||
async _createSession(sessionId, notification, token) {
|
||||
const session = {
|
||||
id: sessionId,
|
||||
token: token,
|
||||
type: 'pty',
|
||||
created: new Date().toISOString(),
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24小时后过期
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
|
||||
cwd: process.cwd(),
|
||||
notification: {
|
||||
type: notification.type,
|
||||
project: notification.project,
|
||||
|
|
@ -117,7 +162,39 @@ class EmailChannel extends NotificationChannel {
|
|||
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
|
||||
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
|
||||
|
||||
this.logger.debug(`Session created: ${sessionId}`);
|
||||
// 同时保存到PTY映射格式
|
||||
const sessionMapPath = process.env.SESSION_MAP_PATH || path.join(__dirname, '../../data/session-map.json');
|
||||
let sessionMap = {};
|
||||
if (fs.existsSync(sessionMapPath)) {
|
||||
try {
|
||||
sessionMap = JSON.parse(fs.readFileSync(sessionMapPath, 'utf8'));
|
||||
} catch (e) {
|
||||
sessionMap = {};
|
||||
}
|
||||
}
|
||||
|
||||
// 使用传入的tmux会话名称或检测当前会话
|
||||
let tmuxSession = notification.metadata?.tmuxSession || this._getCurrentTmuxSession() || 'claude-taskping';
|
||||
|
||||
sessionMap[token] = {
|
||||
type: 'pty',
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
|
||||
cwd: process.cwd(),
|
||||
sessionId: sessionId,
|
||||
tmuxSession: tmuxSession,
|
||||
description: `${notification.type} - ${notification.project}`
|
||||
};
|
||||
|
||||
// 确保目录存在
|
||||
const mapDir = path.dirname(sessionMapPath);
|
||||
if (!fs.existsSync(mapDir)) {
|
||||
fs.mkdirSync(mapDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(sessionMapPath, JSON.stringify(sessionMap, null, 2));
|
||||
|
||||
this.logger.debug(`Session created: ${sessionId}, Token: ${token}`);
|
||||
}
|
||||
|
||||
async _removeSession(sessionId) {
|
||||
|
|
@ -128,20 +205,50 @@ class EmailChannel extends NotificationChannel {
|
|||
}
|
||||
}
|
||||
|
||||
_generateEmailContent(notification, sessionId) {
|
||||
_generateEmailContent(notification, sessionId, token) {
|
||||
const template = this._getTemplate(notification.type);
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
|
||||
// 获取项目目录名(最后一级目录)
|
||||
const projectDir = path.basename(process.cwd());
|
||||
|
||||
// 提取用户问题(从notification.metadata中获取,如果有的话)
|
||||
let userQuestion = '';
|
||||
let claudeResponse = '';
|
||||
|
||||
if (notification.metadata) {
|
||||
userQuestion = notification.metadata.userQuestion || '';
|
||||
claudeResponse = notification.metadata.claudeResponse || '';
|
||||
}
|
||||
|
||||
// 限制用户问题长度用于标题
|
||||
const maxQuestionLength = 30;
|
||||
const shortQuestion = userQuestion.length > maxQuestionLength ?
|
||||
userQuestion.substring(0, maxQuestionLength) + '...' : userQuestion;
|
||||
|
||||
// 生成更具辨识度的标题
|
||||
let enhancedSubject = template.subject;
|
||||
if (shortQuestion) {
|
||||
enhancedSubject = enhancedSubject.replace('{{project}}', `${projectDir} | ${shortQuestion}`);
|
||||
} else {
|
||||
enhancedSubject = enhancedSubject.replace('{{project}}', projectDir);
|
||||
}
|
||||
|
||||
// 模板变量替换
|
||||
const variables = {
|
||||
project: notification.project,
|
||||
project: projectDir,
|
||||
message: notification.message,
|
||||
timestamp: timestamp,
|
||||
sessionId: sessionId,
|
||||
type: notification.type === 'completed' ? '任务完成' : '等待输入'
|
||||
token: token,
|
||||
type: notification.type === 'completed' ? '任务完成' : '等待输入',
|
||||
userQuestion: userQuestion || '未指定任务',
|
||||
claudeResponse: claudeResponse || notification.message,
|
||||
projectDir: projectDir,
|
||||
shortQuestion: shortQuestion || '无具体问题'
|
||||
};
|
||||
|
||||
let subject = template.subject;
|
||||
let subject = enhancedSubject;
|
||||
let html = template.html;
|
||||
let text = template.text;
|
||||
|
||||
|
|
@ -160,7 +267,7 @@ class EmailChannel extends NotificationChannel {
|
|||
// 默认模板
|
||||
const templates = {
|
||||
completed: {
|
||||
subject: '[TaskPing] Claude Code 任务完成 - {{project}}',
|
||||
subject: '[TaskPing #{{token}}] 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);">
|
||||
|
|
@ -170,14 +277,20 @@ class EmailChannel extends NotificationChannel {
|
|||
|
||||
<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> {{projectDir}}<br>
|
||||
<strong>时间:</strong> {{timestamp}}<br>
|
||||
<strong>状态:</strong> {{type}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3e0; padding: 15px; border-radius: 6px; border-left: 4px solid #ff9800; margin: 20px 0;">
|
||||
<h4 style="margin-top: 0; color: #e65100;">📝 您的问题</h4>
|
||||
<p style="margin: 0; color: #2c3e50; font-style: italic;">{{userQuestion}}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #e8f5e8; padding: 15px; border-radius: 6px; border-left: 4px solid #27ae60;">
|
||||
<p style="margin: 0; color: #2c3e50;">{{message}}</p>
|
||||
<h4 style="margin-top: 0; color: #27ae60;">🤖 Claude 的回复</h4>
|
||||
<p style="margin: 0; color: #2c3e50;">{{claudeResponse}}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 25px 0; padding: 20px; background-color: #fff3cd; border-radius: 6px; border-left: 4px solid #ffc107;">
|
||||
|
|
@ -202,13 +315,17 @@ class EmailChannel extends NotificationChannel {
|
|||
</div>
|
||||
`,
|
||||
text: `
|
||||
[TaskPing] Claude Code 任务完成 - {{project}}
|
||||
[TaskPing #{{token}}] Claude Code 任务完成 - {{projectDir}} | {{shortQuestion}}
|
||||
|
||||
项目: {{project}}
|
||||
项目: {{projectDir}}
|
||||
时间: {{timestamp}}
|
||||
状态: {{type}}
|
||||
|
||||
消息: {{message}}
|
||||
📝 您的问题:
|
||||
{{userQuestion}}
|
||||
|
||||
🤖 Claude 的回复:
|
||||
{{claudeResponse}}
|
||||
|
||||
如何继续对话:
|
||||
要继续与 Claude Code 对话,请直接回复此邮件,在邮件正文中输入您的指令。
|
||||
|
|
@ -223,7 +340,7 @@ class EmailChannel extends NotificationChannel {
|
|||
`
|
||||
},
|
||||
waiting: {
|
||||
subject: '[TaskPing] Claude Code 等待输入 - {{project}}',
|
||||
subject: '[TaskPing #{{token}}] 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);">
|
||||
|
|
@ -233,13 +350,14 @@ class EmailChannel extends NotificationChannel {
|
|||
|
||||
<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> {{projectDir}}<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;">
|
||||
<h4 style="margin-top: 0; color: #e67e22;">⏳ 等待处理</h4>
|
||||
<p style="margin: 0; color: #2c3e50;">{{message}}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -259,13 +377,13 @@ class EmailChannel extends NotificationChannel {
|
|||
</div>
|
||||
`,
|
||||
text: `
|
||||
[TaskPing] Claude Code 等待输入 - {{project}}
|
||||
[TaskPing #{{token}}] Claude Code 等待输入 - {{projectDir}}
|
||||
|
||||
项目: {{project}}
|
||||
项目: {{projectDir}}
|
||||
时间: {{timestamp}}
|
||||
状态: {{type}}
|
||||
|
||||
消息: {{message}}
|
||||
⏳ 等待处理: {{message}}
|
||||
|
||||
Claude 需要您的进一步指导。请回复此邮件告诉 Claude 下一步应该做什么。
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"commandQueue": [],
|
||||
"lastSaved": "2025-07-12T22:26:32.914Z"
|
||||
}
|
||||
|
|
@ -1,23 +1 @@
|
|||
{
|
||||
"TESTMDKLS3MK": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753555687,
|
||||
"expiresAt": 1753559287,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"description": "测试会话"
|
||||
},
|
||||
"TESTMDKM0Q3M": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753556089,
|
||||
"expiresAt": 1753559689,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"description": "测试会话"
|
||||
},
|
||||
"TESTMDKMI9XE": {
|
||||
"type": "pty",
|
||||
"createdAt": 1753556908,
|
||||
"expiresAt": 1753560508,
|
||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||
"description": "测试会话"
|
||||
}
|
||||
}
|
||||
{}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "409d0107-1d7f-4892-a456-04d0f3c5300d",
|
||||
"created": "2025-07-26T18:34:54.528Z",
|
||||
"expires": "2025-07-27T18:34:54.528Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "667e76f3-fdd8-407e-89c7-c120311d1e98",
|
||||
"created": "2025-07-23T21:45:03.924Z",
|
||||
"expires": "2025-07-24T21:45:03.924Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla.ai",
|
||||
"message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "67a7a308-29b1-4236-835e-784f87e24632",
|
||||
"created": "2025-07-23T12:49:11.463Z",
|
||||
"expires": "2025-07-24T12:49:11.463Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "video_tag",
|
||||
"message": "[video_tag] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "68ff37be-a0c6-4709-bc95-c38d02f5c7e5",
|
||||
"created": "2025-07-14T10:28:56.812Z",
|
||||
"expires": "2025-07-15T10:28:56.812Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "694590e6-e5ce-492a-a289-4967108342e9",
|
||||
"created": "2025-07-18T12:12:01.327Z",
|
||||
"expires": "2025-07-19T12:12:01.327Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "JobShield",
|
||||
"message": "[JobShield] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "6a00efc7-b169-4517-bc6b-03d9c7296e6b",
|
||||
"created": "2025-07-12T22:30:53.659Z",
|
||||
"expires": "2025-07-13T22:30:53.659Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "6d115776-ab6b-48fc-ad91-fcff5d512085",
|
||||
"created": "2025-07-13T13:08:07.069Z",
|
||||
"expires": "2025-07-14T13:08:07.069Z",
|
||||
"notification": {
|
||||
"type": "waiting",
|
||||
"project": "TaskPing",
|
||||
"message": "[TaskPing] Claude需要您的进一步指导"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "6e85295a-e56f-4d44-a537-5be3a026f109",
|
||||
"created": "2025-07-14T14:33:33.979Z",
|
||||
"expires": "2025-07-15T14:33:33.979Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令"
|
||||
},
|
||||
"status": "waiting",
|
||||
"commandCount": 0,
|
||||
"maxCommands": 10
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"id": "708b349a-6fac-441f-98c0-acd0cc7e175c",
|
||||
"created": "2025-07-13T12:58:56.851Z",
|
||||
"expires": "2025-07-14T12:58:56.851Z",
|
||||
"notification": {
|
||||
"type": "completed",
|
||||
"project": "pandalla_api",
|
||||
"message": "[pandalla_api] 任务已完成,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