diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bf65ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs/ +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +coverage/ +.nyc_output/ + +# Build output +dist/ +build/ + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..7adf51d --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,42 @@ +# TaskPing 项目结构 + +## 📁 核心文件 + +### 🎯 主要脚本 +- **`hook-notify.js`** - 核心通知脚本,被Claude Code hooks调用 +- **`config-tool.js`** - 交互式配置管理工具 +- **`install.js`** - 自动安装脚本,配置Claude Code hooks + +### ⚙️ 配置文件 +- **`config.json`** - 用户配置(语言、音效、自定义消息等) +- **`i18n.json`** - 多语言文本 +- **`claude-hooks.json`** - Claude Code hooks配置模板 + +### 📚 文档 +- **`README.md`** - 完整项目文档 +- **`QUICKSTART.md`** - 快速开始指南 +- **`TaskPing.md`** - 产品规格文档 + +### 🎵 音效 +- **`sounds/`** - 自定义音效目录 + - 支持格式:`.wav`, `.mp3`, `.m4a`, `.aiff`, `.ogg` + - 用户可以添加自己的音效文件 + +### 📦 包管理 +- **`package.json`** - Node.js项目配置 +- **`LICENSE`** - MIT开源协议 + +## 🚀 使用流程 + +1. **安装**: `node install.js` +2. **配置**: `node config-tool.js` +3. **使用**: 正常使用Claude Code,自动收到通知 + +## 🔧 开发说明 + +- 主要语言:JavaScript (Node.js) +- 支持平台:macOS, Windows, Linux +- 核心依赖:无(使用Node.js内置模块) +- 音效播放:系统原生通知API + +项目专注于简洁、高效的Claude Code任务通知功能。 \ No newline at end of file diff --git a/PROJECT_STRUCTURE_NEW.md b/PROJECT_STRUCTURE_NEW.md new file mode 100644 index 0000000..6afe323 --- /dev/null +++ b/PROJECT_STRUCTURE_NEW.md @@ -0,0 +1,106 @@ +# TaskPing 新项目结构设计 + +## 📁 重构后的目录结构 + +``` +TaskPing/ +├── 📦 src/ # 源代码目录 +│ ├── 🎯 core/ # 核心模块 +│ │ ├── notifier.js # 通知核心类 +│ │ ├── config.js # 配置管理 +│ │ └── logger.js # 日志记录 +│ ├── 📢 channels/ # 通知渠道 +│ │ ├── base/ # 基础接口 +│ │ │ └── channel.js # 通知渠道基类 +│ │ ├── local/ # 本地通知 +│ │ │ └── desktop.js # 桌面通知 +│ │ ├── email/ # 邮件通知 +│ │ │ └── smtp.js # SMTP邮件 +│ │ ├── chat/ # 聊天平台 +│ │ │ ├── discord.js # Discord +│ │ │ ├── telegram.js # Telegram +│ │ │ ├── whatsapp.js # WhatsApp +│ │ │ └── feishu.js # 飞书 +│ │ └── webhook/ # Webhook通知 +│ │ └── generic.js # 通用Webhook +│ ├── 🔄 relay/ # 命令中继模块 +│ │ ├── server.js # 中继服务器 +│ │ ├── client.js # 中继客户端 +│ │ └── commands.js # 命令处理 +│ ├── 🛠️ tools/ # 工具模块 +│ │ ├── cli.js # 命令行工具 +│ │ ├── installer.js # 安装器 +│ │ └── config-manager.js # 配置管理器 +│ └── 🎵 assets/ # 静态资源 +│ └── sounds/ # 音效文件 +├── 📋 config/ # 配置文件 +│ ├── default.json # 默认配置 +│ ├── user.json # 用户配置 +│ ├── channels.json # 渠道配置 +│ └── templates/ # 配置模板 +│ ├── claude-hooks.json # Claude Code hooks +│ └── channel-templates/ # 各渠道配置模板 +├── 📚 docs/ # 文档目录 +│ ├── README.md # 主文档 +│ ├── QUICKSTART.md # 快速开始 +│ ├── api/ # API文档 +│ ├── channels/ # 各渠道使用指南 +│ └── examples/ # 使用示例 +├── 🧪 tests/ # 测试目录 +│ ├── unit/ # 单元测试 +│ ├── integration/ # 集成测试 +│ └── fixtures/ # 测试数据 +├── 📦 项目根文件 +│ ├── package.json # Node.js配置 +│ ├── taskping.js # 主入口文件 +│ ├── LICENSE # 开源协议 +│ └── .gitignore # Git忽略 +└── 🚀 scripts/ # 构建脚本 + ├── build.js # 构建脚本 + ├── dev.js # 开发脚本 + └── deploy.js # 部署脚本 +``` + +## 🏗️ 模块设计原则 + +### 1. 核心抽象 +- **NotificationChannel**: 所有通知渠道的基类 +- **CommandRelay**: 命令中继的统一接口 +- **ConfigManager**: 统一的配置管理 + +### 2. 插件化架构 +- 每个通知渠道独立模块 +- 支持动态加载和卸载 +- 标准化的接口和生命周期 + +### 3. 配置分离 +- 用户配置与默认配置分离 +- 渠道配置模块化 +- 敏感信息加密存储 + +## 🔄 工作流程 + +### Phase 1: 本地通知 (当前) +Claude Code → hook-notify.js → Desktop Notification + +### Phase 2: 多渠道通知 +Claude Code → Core Notifier → Channel Router → [Email|Discord|Telegram|...] + +### Phase 3: 命令中继 +Channel → Command Relay Server → Claude Code Client → Execute Command + +## 🎯 入口点设计 + +```bash +# 主命令 +taskping [command] [options] + +# 子命令 +taskping install # 安装和配置 +taskping config # 配置管理 +taskping test # 测试通知 +taskping serve # 启动中继服务 +taskping relay # 连接中继服务 +``` + +这个结构为未来的扩展提供了良好的基础。 \ No newline at end of file diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..c05f4f1 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,74 @@ +# TaskPing 快速开始指南 + +## 30秒快速设置 + +### 1️⃣ 安装 +```bash +node taskping.js install +# 按提示输入 'y' 确认安装 +``` + +### 2️⃣ 测试 +```bash +node taskping.js test +# 应该看到两个测试通知 +``` + +### 3️⃣ 使用 +```bash +claude +# 正常使用Claude Code,会自动收到通知 +``` + +## 工作原理 + +**TaskPing通过Claude Code的hooks机制工作:** + +- ✅ **任务完成时** → 桌面通知:"任务已完成,Claude正在等待下一步指令" +- ⏳ **等待输入时** → 桌面通知:"Claude需要您的进一步指导" + +## 实际效果 + +``` +你: 请帮我重构这个函数 +Claude: [分析代码中...] + +📱 通知: 任务已完成,Claude正在等待下一步指令 + +Claude: 我发现了3个改进点,你想看哪个? + +📱 通知: Claude需要您的进一步指导 +``` + +## 配置 + +```bash +# 打开配置菜单 +node taskping.js config + +# 查看当前配置 +node taskping.js config --show + +# 查看系统状态 +node taskping.js status + +# 可以调整: +# - 语言 (中文/英文/日文) +# - 提示音 +# - 启用/禁用通知 +``` + +## 故障排除 + +**收不到通知?** +- macOS: 在系统偏好设置中允许终端发送通知 +- Linux: 安装 `sudo apt-get install libnotify-bin` +- 测试: `node taskping.js test` + +**安装失败?** +- 确保Node.js版本 >= 14 +- 检查Claude Code配置目录权限 + +--- + +**就这么简单!现在你可以专心做其他事情,Claude完成任务时会主动通知你。** \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa33e2f --- /dev/null +++ b/README.md @@ -0,0 +1,364 @@ +# TaskPing - Claude Code 邮件自动化 + +TaskPing 是一个智能的邮件自动化工具,可以监听你的邮件回复,并将回复内容自动输入到 Claude Code 中执行。 + +## 🚀 快速开始 + +### 1. 配置邮箱 +```bash +npm run config +``` +按照提示配置你的邮箱信息(SMTP和IMAP)。 + +### 2. 启动服务 +```bash +npm start +``` + +### 3. 使用流程 +1. 当 Claude Code 完成任务时,TaskPing 会发送邮件通知到你的邮箱 +2. 直接回复这封邮件,在邮件中写入你想让 Claude Code 执行的下一个命令 +3. TaskPing 会自动监听到你的回复,提取命令内容,并自动输入到 Claude Code 中 +4. 命令会自动执行,无需任何手动操作 + +## ✨ 核心特性 + +### 🎯 智能检测 +- 基于Claude Code官方hooks机制 +- 自动识别任务完成和等待输入状态 +- 无需手动监控,完全自动化 + +### 📢 多渠道通知 +- **桌面通知**:即时本地通知 +- **邮件通知**:远程邮件提醒 + 回复执行命令 +- 支持自定义通知声音和消息内容 +- 同时启用多个通知渠道 + +### 🏠 远程命令执行 +- **邮件回复**:直接回复邮件执行下一步命令 +- **自动化流程**:人不在电脑前也能继续对话 +- **安全机制**:会话过期、命令过滤、来源验证 + +### 🌍 跨平台支持 +- **macOS**:原生通知中心 + 系统提示音 +- **Windows**:Toast通知系统 +- **Linux**:libnotify桌面通知 + +### 🎛️ 灵活配置 +- 多语言支持(中文、英文、日文) +- 自定义提示音(支持系统音效) +- 邮件 SMTP/IMAP 配置 +- 可调节通知频率和超时时间 + +## 📦 快速安装 + +### 自动安装(推荐) + +```bash +# 1. 克隆或下载项目 +git clone +cd TaskPing + +# 2. 运行安装脚本 +node taskping.js install + +# 3. 按提示完成配置 +# 安装器会自动配置Claude Code的hooks设置 +``` + +### 手动安装 + +```bash +# 1. 测试通知功能 +node taskping.js test + +# 2. 配置Claude Code +# 将以下内容添加到 ~/.claude/settings.json 的 "hooks" 部分: +``` + +```json +{ + "hooks": { + "Stop": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "node /path/to/TaskPing/taskping.js notify --type completed", + "timeout": 5 + }] + }], + "SubagentStop": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "node /path/to/TaskPing/taskping.js notify --type waiting", + "timeout": 5 + }] + }] + } +} +``` + +## 🎮 使用方法 + +### 基本使用 + +安装完成后,TaskPing会自动工作: + +```bash +# 1. 正常启动Claude Code +claude + +# 2. 执行任务 +> 请帮我重构这个项目的代码结构 + +# 3. 当Claude完成任务时,你会收到通知: +# 📱 "任务已完成,Claude正在等待下一步指令" + +# 4. 当Claude需要你的输入时,你会收到提醒: +# 📱 "Claude需要您的进一步指导" +``` + +### 配置管理 + +```bash +# 启动配置工具 +node taskping.js config + +# 快速查看当前配置 +node taskping.js config --show + +# 查看系统状态 +node taskping.js status + +# 测试通知功能 +node taskping.js test + +# 启动邮件命令中继服务 (NEW!) +node taskping.js relay start +``` + +## 🔧 配置选项 + +### 语言设置 +- `zh-CN`:简体中文 +- `en`:英语 +- `ja`:日语 + +### 提示音选择(macOS) +- `Glass`:清脆玻璃音(推荐用于任务完成) +- `Tink`:轻柔提示音(推荐用于等待输入) +- `Ping`、`Pop`、`Basso` 等系统音效 + +### 基础配置示例 + +```json +{ + "language": "zh-CN", + "sound": { + "completed": "Glass", + "waiting": "Tink" + }, + "enabled": true, + "timeout": 5 +} +``` + +### 邮件配置示例 + +```json +{ + "email": { + "enabled": true, + "config": { + "smtp": { + "host": "smtp.gmail.com", + "port": 587, + "secure": false, + "auth": { + "user": "your-email@gmail.com", + "pass": "your-app-password" + } + }, + "imap": { + "host": "imap.gmail.com", + "port": 993, + "secure": true + }, + "from": "TaskPing ", + "to": "your-email@gmail.com" + } + } +} +``` + +**📧 邮件功能设置指南**: 查看 [邮件功能详细指南](docs/EMAIL_GUIDE.md) 了解完整的配置和使用方法。 + +## 💡 实际应用场景 + +### 🏗️ 代码重构项目 +``` +你:请帮我重构这个React组件,提高性能 +Claude:开始分析组件结构... +📱 通知:任务完成! +Claude:我找到了3个优化方案,你倾向于哪种? +📱 通知:Claude需要您的进一步指导 +``` + +### 📚 文档生成 +``` +你:为这个API生成完整的文档 +Claude:正在分析API接口... +📱 通知:任务完成! +Claude:文档已生成,需要我添加使用示例吗? +📱 通知:Claude需要您的进一步指导 +``` + +### 🐛 Bug调试 +``` +你:帮我找出这个内存泄漏的原因 +Claude:开始深度分析代码... +📱 通知:任务完成! +Claude:发现了2个可能的原因,需要查看哪个文件? +📱 通知:Claude需要您的进一步指导 +``` + +### 📧 远程邮件工作流程 +``` +1. 你在家里:启动 Claude Code 任务 +2. 你出门了:📧 收到邮件 "任务完成,Claude等待下一步指令" +3. 在路上:回复邮件 "请继续优化代码性能" +4. 自动执行:命令自动在你的电脑上执行 +5. 再次收到:📧 "优化完成" 邮件通知 +6. 继续回复:进行下一步操作 + +真正实现远程 AI 编程!🚀 +``` + +## 🛠️ 故障排除 + +### macOS权限问题 +如果收不到通知,请检查: +1. 打开"系统偏好设置" → "安全性与隐私" → "隐私" +2. 选择"通知",确保终端应用有权限 +3. 或者在"系统偏好设置" → "通知"中启用终端通知 + +### Linux依赖缺失 +```bash +# Ubuntu/Debian +sudo apt-get install libnotify-bin + +# Fedora/RHEL +sudo dnf install libnotify + +# Arch Linux +sudo pacman -S libnotify +``` + +### Windows执行策略 +```powershell +# 如果遇到PowerShell执行策略限制 +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +### 测试通知 +```bash +# 测试所有通知渠道 +node taskping.js test + +# 手动发送通知 +node taskping.js notify --type completed +node taskping.js notify --type waiting + +# 查看系统状态 +node taskping.js status +``` + +## 📊 项目结构 + +``` +TaskPing/ +├── 📄 README.md # 项目文档 +├── 🚀 taskping.js # 主要CLI入口 +├── 📋 TaskPing.md # 产品规格文档 +├── 📦 package.json # 项目依赖 +├── config/ # 配置文件目录 +│ ├── defaults/ # 默认配置模板 +│ ├── user.json # 用户个人配置 +│ └── channels.json # 通知渠道配置 +├── src/ # 核心源代码 +│ ├── core/ # 核心模块 +│ │ ├── config.js # 配置管理器 +│ │ ├── logger.js # 日志系统 +│ │ └── notifier.js # 通知编排器 +│ ├── channels/ # 通知渠道实现 +│ │ ├── local/ # 本地通知 +│ │ ├── email/ # 邮件通知 +│ │ └── chat/ # 聊天应用通知 +│ ├── tools/ # 管理工具 +│ │ ├── installer.js # 安装器 +│ │ └── config-manager.js # 配置管理器 +│ └── assets/ # 静态资源 +└── docs/ # 文档目录 +``` + +## 🔮 发展规划 + +TaskPing按照产品规格文档分阶段开发: + +### ✅ Phase 1 - 本地通知MVP(已完成) +- 本地桌面通知 +- Claude Code hooks集成 +- 基础配置管理 + +### ✅ Phase 2 - 邮件通知和远程执行(已完成) +- 📧 邮件通知功能 +- 🔄 邮件回复命令执行 +- 🔒 安全会话管理 +- 🛠️ 命令中继服务 + +### 🚧 Phase 3 - 多渠道通知(规划中) +- Telegram/Discord/WhatsApp/飞书集成 +- 移动端推送通知 +- 多渠道命令中继 + +### 🌟 Phase 4 - 企业级功能(未来) +- 团队协作功能 +- 用户权限管理 +- 审计日志 +- API 接口 + +## 🤝 贡献指南 + +欢迎参与TaskPing的开发! + +### 如何贡献 +1. Fork本项目 +2. 创建功能分支:`git checkout -b feature/new-feature` +3. 提交更改:`git commit -am 'Add new feature'` +4. 推送分支:`git push origin feature/new-feature` +5. 提交Pull Request + +### 开发环境 +- Node.js >= 14.0.0 +- 支持macOS、Linux、Windows开发 + +## 📄 许可证 + +本项目采用 [MIT License](LICENSE) 开源协议。 + +## 💬 联系我们 + +- 🐛 **问题反馈**:[提交Issue](https://github.com/TaskPing/TaskPing/issues) +- 💡 **功能建议**:[Discussion](https://github.com/TaskPing/TaskPing/discussions) +- 📧 **邮件联系**:contact@taskping.dev + +--- + +
+ +**让Claude Code工作流程更加智能高效!** + +⭐ 如果这个项目对你有帮助,请给我们一个Star! + +
\ No newline at end of file diff --git a/TaskPing.md b/TaskPing.md new file mode 100644 index 0000000..bc6fff8 --- /dev/null +++ b/TaskPing.md @@ -0,0 +1,199 @@ +# Claude Code Notify Assistant – Product Specification + +## 1  Purpose & Vision + +Provide developers with an **event‑driven companion** for Claude‑powered CLI sessions. When a long‑running task finishes, the assistant instantly notifies the user on their preferred channel and lets them **reply from mobile to trigger the next command**, achieving a seamless *desktop↔mobile↔AI* loop. + +--- + +## 2  Problem Statement + +CLI workflows often involve scripts that run for minutes or hours. Users must poll the terminal or stay near the computer, wasting time and focus. Existing notification tools are channel‑specific or lack bi‑directional control, and none are optimised for Claude Code agents. + +--- + +## 3  Solution Overview + +1. **CLI Hook**: A tiny cross‑platform wrapper (`claude-notify run `) 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 +{ + "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 ` – 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* + diff --git a/config/channels.json b/config/channels.json new file mode 100644 index 0000000..9dee6d9 --- /dev/null +++ b/config/channels.json @@ -0,0 +1,69 @@ +{ + "desktop": { + "type": "local", + "enabled": true, + "config": {} + }, + "email": { + "type": "email", + "enabled": true, + "config": { + "smtp": { + "host": "smtp.feishu.cn", + "port": 465, + "secure": true, + "auth": { + "user": "panda@pandalla.ai", + "pass": "1fIvPAo0lagVG9gS" + } + }, + "imap": { + "host": "imap.feishu.cn", + "port": 993, + "secure": true, + "auth": { + "user": "panda@pandalla.ai", + "pass": "1fIvPAo0lagVG9gS" + } + }, + "from": "TaskPing ", + "to": "jiaxicui446@gmail.com", + "template": { + "checkInterval": 30 + } + } + }, + "discord": { + "type": "chat", + "enabled": false, + "config": { + "webhook": "", + "username": "TaskPing", + "avatar": null + } + }, + "telegram": { + "type": "chat", + "enabled": false, + "config": { + "token": "", + "chatId": "" + } + }, + "whatsapp": { + "type": "chat", + "enabled": false, + "config": { + "webhook": "", + "apiKey": "" + } + }, + "feishu": { + "type": "chat", + "enabled": false, + "config": { + "webhook": "", + "secret": "" + } + } +} \ No newline at end of file diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..8bf3597 --- /dev/null +++ b/config/default.json @@ -0,0 +1,27 @@ +{ + "language": "zh-CN", + "sound": { + "completed": "Glass", + "waiting": "Tink" + }, + "enabled": true, + "timeout": 5, + "customMessages": { + "completed": null, + "waiting": null + }, + "channels": { + "desktop": { + "enabled": true, + "priority": 1 + } + }, + "relay": { + "enabled": false, + "port": 3000, + "auth": { + "enabled": false, + "token": null + } + } +} \ No newline at end of file diff --git a/config/defaults/claude-hooks.json b/config/defaults/claude-hooks.json new file mode 100644 index 0000000..3966e79 --- /dev/null +++ b/config/defaults/claude-hooks.json @@ -0,0 +1,28 @@ +{ + "hooks": { + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "node {TASKPING_ROOT}/taskping.js notify --type completed", + "timeout": 5 + } + ] + } + ], + "SubagentStop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "node {TASKPING_ROOT}/taskping.js notify --type waiting", + "timeout": 5 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/config/defaults/config.json b/config/defaults/config.json new file mode 100644 index 0000000..f967e43 --- /dev/null +++ b/config/defaults/config.json @@ -0,0 +1,13 @@ +{ + "language": "zh-CN", + "sound": { + "completed": "Submarine", + "waiting": "Hero" + }, + "enabled": true, + "timeout": 5, + "customMessages": { + "completed": null, + "waiting": null + } +} \ No newline at end of file diff --git a/config/defaults/i18n.json b/config/defaults/i18n.json new file mode 100644 index 0000000..c841294 --- /dev/null +++ b/config/defaults/i18n.json @@ -0,0 +1,32 @@ +{ + "zh-CN": { + "completed": { + "title": "Claude Code - 任务完成", + "message": "[{project}] 任务已完成,Claude正在等待下一步指令" + }, + "waiting": { + "title": "Claude Code - 等待输入", + "message": "[{project}] Claude需要您的进一步指导" + } + }, + "en": { + "completed": { + "title": "Claude Code - Task Completed", + "message": "[{project}] Task completed, Claude is waiting for next instruction" + }, + "waiting": { + "title": "Claude Code - Waiting for Input", + "message": "[{project}] Claude needs your further guidance" + } + }, + "ja": { + "completed": { + "title": "Claude Code - タスク完了", + "message": "[{project}] タスクが完了しました。Claudeが次の指示を待っています" + }, + "waiting": { + "title": "Claude Code - 入力待ち", + "message": "[{project}] Claudeにはあなたのさらなるガイダンスが必要です" + } + } +} \ No newline at end of file diff --git a/config/email-template.json b/config/email-template.json new file mode 100644 index 0000000..574f178 --- /dev/null +++ b/config/email-template.json @@ -0,0 +1,48 @@ +{ + "email": { + "type": "email", + "enabled": true, + "config": { + "smtp": { + "host": "smtp.gmail.com", + "port": 587, + "secure": false, + "auth": { + "user": "your-email@gmail.com", + "pass": "your-app-password" + } + }, + "imap": { + "host": "imap.gmail.com", + "port": 993, + "secure": true, + "auth": { + "user": "your-email@gmail.com", + "pass": "your-app-password" + } + }, + "from": "TaskPing ", + "to": "your-email@gmail.com", + "template": { + "checkInterval": 30 + } + } + } +} + +# 邮件配置说明 +# +# Gmail 配置示例: +# 1. 启用两步验证 +# 2. 生成应用密码(16位) +# 3. 替换上面的 your-email@gmail.com 和 your-app-password +# +# 其他邮箱提供商: +# QQ邮箱: smtp.qq.com (587) / imap.qq.com (993) +# 163邮箱: smtp.163.com (587) / imap.163.com (993) +# Outlook: smtp.live.com (587) / imap-mail.outlook.com (993) +# +# 配置完成后: +# 1. 复制email部分到 config/channels.json +# 2. 运行: taskping test +# 3. 运行: taskping relay start \ No newline at end of file diff --git a/config/user.json b/config/user.json new file mode 100644 index 0000000..721d5f9 --- /dev/null +++ b/config/user.json @@ -0,0 +1,27 @@ +{ + "language": "zh-CN", + "sound": { + "completed": "Submarine", + "waiting": "Hero" + }, + "enabled": true, + "timeout": 5, + "customMessages": { + "completed": null, + "waiting": null + }, + "channels": { + "desktop": { + "enabled": true, + "priority": 1 + } + }, + "relay": { + "enabled": false, + "port": 3000, + "auth": { + "enabled": false, + "token": null + } + } +} \ No newline at end of file diff --git a/debug-email.js b/debug-email.js new file mode 100755 index 0000000..907aa5b --- /dev/null +++ b/debug-email.js @@ -0,0 +1,494 @@ +#!/usr/bin/env node + +/** + * TaskPing 邮件调试工具 + * 用于调试邮件监听和自动化问题 + */ + +const Imap = require('node-imap'); +const { simpleParser } = require('mailparser'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +class DebugEmailAutomation { + constructor() { + this.configPath = path.join(__dirname, 'config/channels.json'); + this.config = null; + this.imap = null; + } + + async start() { + console.log('🔍 TaskPing 邮件调试工具启动\n'); + + // 1. 检查配置 + console.log('📋 1. 检查配置文件...'); + if (!this.loadConfig()) { + return; + } + console.log('✅ 配置文件加载成功'); + console.log(`📧 IMAP服务器: ${this.config.imap.host}:${this.config.imap.port}`); + console.log(`👤 用户: ${this.config.imap.auth.user}`); + console.log(`📬 通知发送到: ${this.config.to}\n`); + + // 2. 测试IMAP连接 + console.log('🔌 2. 测试IMAP连接...'); + try { + await this.testConnection(); + console.log('✅ IMAP连接成功\n'); + } catch (error) { + console.error('❌ IMAP连接失败:', error.message); + return; + } + + // 3. 检查最近邮件 + console.log('📧 3. 检查最近邮件...'); + try { + await this.checkRecentEmails(); + } catch (error) { + console.error('❌ 检查邮件失败:', error.message); + } + + // 4. 开始实时监听 + console.log('\n👂 4. 开始实时监听邮件回复...'); + console.log('💌 现在可以回复TaskPing邮件来测试自动化功能'); + console.log('🔍 调试信息会实时显示\n'); + + this.startRealTimeListening(); + } + + loadConfig() { + try { + const data = fs.readFileSync(this.configPath, 'utf8'); + const config = JSON.parse(data); + + if (!config.email?.enabled) { + console.error('❌ 邮件功能未启用'); + console.log('💡 请运行: npm run config'); + return false; + } + + this.config = config.email.config; + return true; + } catch (error) { + console.error('❌ 配置文件读取失败:', error.message); + console.log('💡 请运行: npm run config'); + return false; + } + } + + async testConnection() { + return new Promise((resolve, reject) => { + this.imap = new Imap({ + user: this.config.imap.auth.user, + password: this.config.imap.auth.pass, + host: this.config.imap.host, + port: this.config.imap.port, + tls: this.config.imap.secure, + connTimeout: 30000, + authTimeout: 15000, + // debug: console.log // 暂时禁用调试 + }); + + this.imap.once('ready', () => { + console.log('🔗 IMAP ready事件触发'); + resolve(); + }); + + this.imap.once('error', (error) => { + console.error('🔗 IMAP error事件:', error.message); + reject(error); + }); + + this.imap.connect(); + }); + } + + async checkRecentEmails() { + return new Promise((resolve, reject) => { + this.imap.openBox('INBOX', true, (err, box) => { + if (err) { + reject(err); + return; + } + + console.log(`📫 收件箱状态: 总邮件 ${box.messages.total}, 未读 ${box.messages.unseen}`); + + // 查找最近1小时的所有邮件 + const since = new Date(); + since.setHours(since.getHours() - 1); + + console.log(`🔍 搜索 ${since.toLocaleString()} 之后的邮件...`); + + this.imap.search([['SINCE', since]], (searchErr, results) => { + if (searchErr) { + reject(searchErr); + return; + } + + console.log(`📨 找到 ${results.length} 封最近邮件`); + + if (results.length === 0) { + console.log('ℹ️ 没有找到最近的邮件'); + resolve(); + return; + } + + // 获取最近几封邮件的详情 + const fetch = this.imap.fetch(results.slice(-3), { + bodies: 'HEADER', + struct: true + }); + + fetch.on('message', (msg, seqno) => { + console.log(`\n📧 邮件 ${seqno}:`); + + msg.on('body', (stream, info) => { + let buffer = ''; + stream.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + }); + stream.once('end', () => { + const lines = buffer.split('\n'); + const subject = lines.find(line => line.startsWith('Subject:')); + const from = lines.find(line => line.startsWith('From:')); + const date = lines.find(line => line.startsWith('Date:')); + + console.log(` 📄 ${subject || 'Subject: (未知)'}`); + console.log(` 👤 ${from || 'From: (未知)'}`); + console.log(` 📅 ${date || 'Date: (未知)'}`); + + if (subject && subject.includes('[TaskPing]')) { + console.log(' 🎯 这是TaskPing邮件!'); + } + }); + }); + }); + + fetch.once('end', () => { + resolve(); + }); + + fetch.once('error', (fetchErr) => { + reject(fetchErr); + }); + }); + }); + }); + } + + startRealTimeListening() { + // 重新连接用于监听 + this.imap.end(); + + setTimeout(() => { + this.connectForListening(); + }, 1000); + } + + async connectForListening() { + this.imap = new Imap({ + user: this.config.imap.auth.user, + password: this.config.imap.auth.pass, + host: this.config.imap.host, + port: this.config.imap.port, + tls: this.config.imap.secure, + connTimeout: 60000, + authTimeout: 30000, + keepalive: true + }); + + this.imap.once('ready', () => { + console.log('✅ 监听连接建立'); + this.openInboxForListening(); + }); + + this.imap.once('error', (error) => { + console.error('❌ 监听连接错误:', error.message); + }); + + this.imap.once('end', () => { + console.log('🔄 连接断开,尝试重连...'); + setTimeout(() => this.connectForListening(), 5000); + }); + + this.imap.connect(); + } + + openInboxForListening() { + this.imap.openBox('INBOX', false, (err, box) => { + if (err) { + console.error('❌ 打开收件箱失败:', err.message); + return; + } + + console.log('📬 收件箱已打开,开始监听新邮件...'); + + // 设置定期检查 + setInterval(() => { + this.checkNewEmails(); + }, 10000); // 每10秒检查一次 + + // 立即检查一次 + this.checkNewEmails(); + }); + } + + checkNewEmails() { + const since = new Date(); + since.setMinutes(since.getMinutes() - 5); // 检查最近5分钟的邮件 + + this.imap.search([['UNSEEN'], ['SINCE', since]], (err, results) => { + if (err) { + console.error('🔍 搜索新邮件失败:', err.message); + return; + } + + if (results.length > 0) { + console.log(`\n🚨 发现 ${results.length} 封新邮件!`); + this.processNewEmails(results); + } + }); + } + + processNewEmails(emailUids) { + const fetch = this.imap.fetch(emailUids, { + bodies: '', + markSeen: true + }); + + fetch.on('message', (msg, seqno) => { + console.log(`\n📨 处理新邮件 ${seqno}:`); + let buffer = ''; + + msg.on('body', (stream) => { + stream.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + }); + + stream.once('end', async () => { + try { + const parsed = await simpleParser(buffer); + await this.analyzeEmail(parsed, seqno); + } catch (error) { + console.error(`❌ 解析邮件 ${seqno} 失败:`, error.message); + } + }); + }); + }); + } + + async analyzeEmail(email, seqno) { + console.log(`📧 邮件 ${seqno} 分析:`); + console.log(` 📄 主题: ${email.subject || '(无主题)'}`); + console.log(` 👤 发件人: ${email.from?.text || '(未知)'}`); + console.log(` 📅 时间: ${email.date || '(未知)'}`); + + // 检查是否是TaskPing回复 + const isTaskPingReply = this.isTaskPingReply(email); + console.log(` 🎯 TaskPing回复: ${isTaskPingReply ? '是' : '否'}`); + + if (!isTaskPingReply) { + console.log(` ⏭️ 跳过非TaskPing邮件`); + return; + } + + // 提取命令 + const command = this.extractCommand(email); + console.log(` 💬 邮件内容长度: ${(email.text || '').length} 字符`); + console.log(` 🎯 提取的命令: "${command || '(无)'}"`); + + if (!command || command.trim().length === 0) { + console.log(` ⚠️ 未找到有效命令`); + return; + } + + console.log(`\n🚀 准备执行命令...`); + await this.executeCommand(command, seqno); + } + + isTaskPingReply(email) { + const subject = email.subject || ''; + return subject.includes('[TaskPing]') || + subject.match(/^(Re:|RE:|回复:)/i); + } + + extractCommand(email) { + let text = email.text || ''; + console.log(` 📝 原始邮件文本:\n${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + + const lines = text.split('\n'); + const commandLines = []; + + for (const line of lines) { + if (line.includes('-----Original Message-----') || + line.includes('--- Original Message ---') || + line.includes('在') && line.includes('写道:') || + line.includes('On') && line.includes('wrote:') || + line.match(/^>\s*/) || + line.includes('会话ID:')) { + console.log(` ✂️ 在此行停止解析: ${line.substring(0, 50)}`); + break; + } + + if (line.includes('--') || + line.includes('Sent from') || + line.includes('发自我的')) { + console.log(` ✂️ 跳过签名行: ${line.substring(0, 50)}`); + break; + } + + commandLines.push(line); + } + + const extractedCommand = commandLines.join('\n').trim(); + console.log(` 🎯 清理后的命令:\n"${extractedCommand}"`); + return extractedCommand; + } + + async executeCommand(command, seqno) { + console.log(`🤖 执行命令 (来自邮件 ${seqno}):`); + console.log(`📝 命令内容: "${command}"`); + + try { + // 1. 复制到剪贴板 + console.log(`📋 1. 复制到剪贴板...`); + const clipboardSuccess = await this.copyToClipboard(command); + console.log(`📋 剪贴板: ${clipboardSuccess ? '✅ 成功' : '❌ 失败'}`); + + // 2. 尝试自动化 + console.log(`🤖 2. 尝试自动输入...`); + const automationSuccess = await this.attemptAutomation(command); + console.log(`🤖 自动化: ${automationSuccess ? '✅ 成功' : '❌ 失败'}`); + + // 3. 发送通知 + console.log(`🔔 3. 发送通知...`); + const notificationSuccess = await this.sendNotification(command); + console.log(`🔔 通知: ${notificationSuccess ? '✅ 成功' : '❌ 失败'}`); + + if (automationSuccess) { + console.log(`\n🎉 邮件命令已自动执行到Claude Code!`); + } else { + console.log(`\n⚠️ 自动化失败,但命令已复制到剪贴板`); + console.log(`💡 请手动在Claude Code中粘贴 (Cmd+V)`); + } + + } catch (error) { + console.error(`❌ 执行命令失败:`, error.message); + } + } + + async copyToClipboard(command) { + return new Promise((resolve) => { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(command); + pbcopy.stdin.end(); + + pbcopy.on('close', (code) => { + resolve(code === 0); + }); + + pbcopy.on('error', () => resolve(false)); + }); + } + + async attemptAutomation(command) { + return new Promise((resolve) => { + const escapedCommand = command + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/'/g, "\\'"); + + const script = ` + tell application "System Events" + set claudeApps to {"Claude", "Claude Code", "Claude Desktop"} + set devApps to {"Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code"} + set targetApp to null + set appName to "" + + -- 查找Claude应用 + repeat with app in claudeApps + try + if application process app exists then + set targetApp to application process app + set appName to app + exit repeat + end if + end try + end repeat + + -- 如果没找到Claude,查找开发工具 + if targetApp is null then + repeat with app in devApps + try + if application process app exists then + set targetApp to application process app + set appName to app + exit repeat + end if + end try + end repeat + end if + + if targetApp is not null then + set frontmost of targetApp to true + delay 1 + + keystroke "a" using command down + delay 0.3 + keystroke "${escapedCommand}" + delay 0.5 + keystroke return + + return "success:" & appName + else + return "no_app" + end if + end tell + `; + + console.log(`🍎 执行AppleScript自动化...`); + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.on('close', (code) => { + console.log(`🍎 AppleScript结果: 退出码=${code}, 输出="${output}"`); + const success = code === 0 && output.startsWith('success:'); + if (success) { + const appName = output.split(':')[1]; + console.log(`🍎 成功输入到应用: ${appName}`); + } + resolve(success); + }); + + osascript.on('error', (error) => { + console.log(`🍎 AppleScript错误: ${error.message}`); + resolve(false); + }); + }); + } + + async sendNotification(command) { + const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command; + + const script = ` + display notification "邮件命令: ${shortCommand.replace(/"/g, '\\"')}" with title "TaskPing Debug" sound name "default" + `; + + const osascript = spawn('osascript', ['-e', script]); + + return new Promise((resolve) => { + osascript.on('close', (code) => resolve(code === 0)); + osascript.on('error', () => resolve(false)); + }); + } +} + +// 启动调试工具 +const debugTool = new DebugEmailAutomation(); +debugTool.start().catch(console.error); \ No newline at end of file diff --git a/diagnose-automation.js b/diagnose-automation.js new file mode 100755 index 0000000..876c584 --- /dev/null +++ b/diagnose-automation.js @@ -0,0 +1,380 @@ +#!/usr/bin/env node + +/** + * 自动化功能诊断工具 + * 详细检测和诊断自动粘贴功能的问题 + */ + +const { spawn } = require('child_process'); + +class AutomationDiagnostic { + constructor() { + this.tests = []; + } + + async runDiagnostic() { + console.log('🔍 自动化功能诊断工具\n'); + + if (process.platform !== 'darwin') { + console.log('❌ 此诊断工具仅适用于 macOS'); + return; + } + + // 运行所有测试 + await this.testBasicAppleScript(); + await this.testSystemEventsAccess(); + await this.testKeystrokePermission(); + await this.testApplicationDetection(); + await this.testClipboardAccess(); + await this.testDetailedPermissions(); + + // 显示总结 + this.showSummary(); + await this.provideSolutions(); + } + + async testBasicAppleScript() { + console.log('1. 📋 测试基本 AppleScript 执行...'); + try { + const result = await this.runAppleScript('return "AppleScript works"'); + if (result === 'AppleScript works') { + console.log(' ✅ 基本 AppleScript 执行正常'); + this.tests.push({ name: 'AppleScript', status: 'pass' }); + } else { + console.log(' ❌ AppleScript 返回异常结果'); + this.tests.push({ name: 'AppleScript', status: 'fail', error: 'Unexpected result' }); + } + } catch (error) { + console.log(' ❌ AppleScript 执行失败:', error.message); + this.tests.push({ name: 'AppleScript', status: 'fail', error: error.message }); + } + console.log(''); + } + + async testSystemEventsAccess() { + console.log('2. 🖥️ 测试 System Events 访问...'); + try { + const script = ` + tell application "System Events" + return name of first application process whose frontmost is true + end tell + `; + const result = await this.runAppleScript(script); + if (result && result !== 'permission_denied') { + console.log(` ✅ 可以访问 System Events,当前前台应用: ${result}`); + this.tests.push({ name: 'System Events', status: 'pass', data: result }); + } else { + console.log(' ❌ System Events 访问被拒绝'); + this.tests.push({ name: 'System Events', status: 'fail', error: 'Access denied' }); + } + } catch (error) { + console.log(' ❌ System Events 访问失败:', error.message); + this.tests.push({ name: 'System Events', status: 'fail', error: error.message }); + } + console.log(''); + } + + async testKeystrokePermission() { + console.log('3. ⌨️ 测试按键发送权限...'); + try { + const script = ` + tell application "System Events" + try + keystroke "test" without key down + return "keystroke_success" + on error errorMessage + return "keystroke_failed: " & errorMessage + end try + end tell + `; + const result = await this.runAppleScript(script); + if (result === 'keystroke_success') { + console.log(' ✅ 按键发送权限正常'); + this.tests.push({ name: 'Keystroke', status: 'pass' }); + } else { + console.log(` ❌ 按键发送失败: ${result}`); + this.tests.push({ name: 'Keystroke', status: 'fail', error: result }); + } + } catch (error) { + console.log(' ❌ 按键测试异常:', error.message); + this.tests.push({ name: 'Keystroke', status: 'fail', error: error.message }); + } + console.log(''); + } + + async testApplicationDetection() { + console.log('4. 🔍 测试应用程序检测...'); + try { + const script = ` + tell application "System Events" + set appList to {"Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor", "Claude Code"} + set foundApps to {} + repeat with appName in appList + try + if application process appName exists then + set foundApps to foundApps & {appName} + end if + end try + end repeat + return foundApps as string + end tell + `; + const result = await this.runAppleScript(script); + if (result) { + console.log(` ✅ 检测到以下应用: ${result}`); + this.tests.push({ name: 'App Detection', status: 'pass', data: result }); + } else { + console.log(' ⚠️ 未检测到目标应用程序'); + this.tests.push({ name: 'App Detection', status: 'warn', error: 'No target apps found' }); + } + } catch (error) { + console.log(' ❌ 应用检测失败:', error.message); + this.tests.push({ name: 'App Detection', status: 'fail', error: error.message }); + } + console.log(''); + } + + async testClipboardAccess() { + console.log('5. 📋 测试剪贴板访问...'); + try { + // 测试写入剪贴板 + await this.setClipboard('test_clipboard_content'); + const content = await this.getClipboard(); + + if (content.includes('test_clipboard_content')) { + console.log(' ✅ 剪贴板读写正常'); + this.tests.push({ name: 'Clipboard', status: 'pass' }); + } else { + console.log(' ❌ 剪贴板内容不匹配'); + this.tests.push({ name: 'Clipboard', status: 'fail', error: 'Content mismatch' }); + } + } catch (error) { + console.log(' ❌ 剪贴板访问失败:', error.message); + this.tests.push({ name: 'Clipboard', status: 'fail', error: error.message }); + } + console.log(''); + } + + async testDetailedPermissions() { + console.log('6. 🔐 详细权限检查...'); + + try { + // 检查当前运行的进程 + const whoami = await this.runCommand('whoami'); + console.log(` 👤 当前用户: ${whoami}`); + + // 检查终端应用 + const terminal = process.env.TERM_PROGRAM || 'Unknown'; + console.log(` 💻 终端程序: ${terminal}`); + + // 检查是否在 IDE 中运行 + const isInIDE = process.env.VSCODE_PID || process.env.CURSOR_SESSION_ID || process.env.JB_IDE_PID; + if (isInIDE) { + console.log(' 🔧 检测到在 IDE 中运行'); + } + + // 尝试获取更详细的权限信息 + const script = ` + tell application "System Events" + try + set frontApp to name of first application process whose frontmost is true + set allApps to name of every application process + return "Front: " & frontApp & ", All: " & (count of allApps) + on error errorMsg + return "Error: " & errorMsg + end try + end tell + `; + + const permResult = await this.runAppleScript(script); + console.log(` 📊 权限测试结果: ${permResult}`); + + this.tests.push({ + name: 'Detailed Permissions', + status: 'info', + data: { user: whoami, terminal, result: permResult } + }); + + } catch (error) { + console.log(' ❌ 详细检查失败:', error.message); + this.tests.push({ name: 'Detailed Permissions', status: 'fail', error: error.message }); + } + console.log(''); + } + + showSummary() { + console.log('📊 诊断结果总结:\n'); + + const passed = this.tests.filter(t => t.status === 'pass').length; + const failed = this.tests.filter(t => t.status === 'fail').length; + const warned = this.tests.filter(t => t.status === 'warn').length; + + this.tests.forEach(test => { + const icon = test.status === 'pass' ? '✅' : + test.status === 'fail' ? '❌' : + test.status === 'warn' ? '⚠️' : 'ℹ️'; + console.log(`${icon} ${test.name}`); + if (test.error) { + console.log(` 错误: ${test.error}`); + } + if (test.data) { + console.log(` 数据: ${test.data}`); + } + }); + + console.log(`\n📈 总计: ${passed} 通过, ${failed} 失败, ${warned} 警告\n`); + } + + async provideSolutions() { + const keystrokeTest = this.tests.find(t => t.name === 'Keystroke'); + const systemEventsTest = this.tests.find(t => t.name === 'System Events'); + + console.log('💡 解决方案建议:\n'); + + if (keystrokeTest && keystrokeTest.status === 'fail') { + console.log('🔧 按键发送问题解决方案:'); + console.log(' 1. 打开 系统偏好设置 > 安全性与隐私 > 隐私 > 辅助功能'); + console.log(' 2. 移除并重新添加你的终端应用 (Terminal/iTerm2/VS Code)'); + console.log(' 3. 确保勾选框已被选中'); + console.log(' 4. 重启终端应用'); + console.log(''); + } + + if (systemEventsTest && systemEventsTest.status === 'fail') { + console.log('🔧 System Events 访问问题解决方案:'); + console.log(' 1. 检查 系统偏好设置 > 安全性与隐私 > 隐私 > 自动化'); + console.log(' 2. 确保你的终端应用下勾选了 "System Events"'); + console.log(' 3. 如果没有看到你的应用,先运行一次自动化脚本触发权限请求'); + console.log(''); + } + + console.log('🚀 额外建议:'); + console.log(' • 尝试完全退出并重启终端应用'); + console.log(' • 在 Terminal 中运行而不是在 IDE 集成终端中'); + console.log(' • 检查是否有安全软件阻止自动化'); + console.log(' • 尝试在不同的终端应用中运行测试'); + + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const answer = await this.question(rl, '\n是否尝试一个简单的修复测试?(y/n): '); + + if (answer.toLowerCase() === 'y') { + await this.runSimpleFixTest(); + } + + rl.close(); + } + + async runSimpleFixTest() { + console.log('\n🔨 运行简单修复测试...'); + + try { + // 尝试最基本的自动化 + const script = ` + display dialog "TaskPing 自动化测试" with title "权限测试" buttons {"确定"} default button 1 giving up after 3 + `; + + await this.runAppleScript(script); + console.log('✅ 基本对话框测试成功'); + + } catch (error) { + console.log('❌ 基本测试失败:', error.message); + } + } + + async runAppleScript(script) { + return new Promise((resolve, reject) => { + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + let error = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString(); + }); + + osascript.stderr.on('data', (data) => { + error += data.toString(); + }); + + osascript.on('close', (code) => { + if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(error || `Exit code: ${code}`)); + } + }); + }); + } + + async runCommand(command) { + return new Promise((resolve, reject) => { + const proc = spawn('sh', ['-c', command]); + let output = ''; + + proc.stdout.on('data', (data) => { + output += data.toString(); + }); + + proc.on('close', (code) => { + if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + }); + } + + async setClipboard(text) { + return new Promise((resolve, reject) => { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(text); + pbcopy.stdin.end(); + + pbcopy.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error('Failed to set clipboard')); + } + }); + }); + } + + async getClipboard() { + return new Promise((resolve, reject) => { + const pbpaste = spawn('pbpaste'); + let output = ''; + + pbpaste.stdout.on('data', (data) => { + output += data.toString(); + }); + + pbpaste.on('close', (code) => { + if (code === 0) { + resolve(output); + } else { + reject(new Error('Failed to get clipboard')); + } + }); + }); + } + + question(rl, prompt) { + return new Promise(resolve => { + rl.question(prompt, resolve); + }); + } +} + +// 运行诊断 +if (require.main === module) { + const diagnostic = new AutomationDiagnostic(); + diagnostic.runDiagnostic().catch(console.error); +} + +module.exports = AutomationDiagnostic; \ No newline at end of file diff --git a/docs/EMAIL_ARCHITECTURE.md b/docs/EMAIL_ARCHITECTURE.md new file mode 100644 index 0000000..c8cadd9 --- /dev/null +++ b/docs/EMAIL_ARCHITECTURE.md @@ -0,0 +1,207 @@ +# TaskPing 邮件功能架构设计 + +## 功能概述 + +实现邮件通知和远程命令执行功能,用户可以: +1. 接收 Claude Code 任务完成的邮件通知 +2. 通过回复邮件来远程执行下一步命令 +3. 在不坐在电脑前的情况下继续 Claude Code 对话 + +## 整体架构 + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Claude Code │ ──▶│ TaskPing CLI │ ──▶│ Email Channel │ +│ (hooks) │ │ │ │ (SMTP Send) │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────┐ ┌─────────────────┐ + │ Command Relay │◀───│ Email Listener │ + │ Service │ │ (IMAP Receive) │ + └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Claude Code │ + │ (stdin input) │ + └──────────────────┘ +``` + +## 核心组件 + +### 1. 邮件通知渠道 (Email Channel) +- **位置**: `src/channels/email/smtp.js` +- **功能**: 发送任务完成通知邮件 +- **特性**: + - 支持多种 SMTP 配置 + - 邮件模板化 + - 会话 ID 生成和嵌入 + - 安全回复指令 + +### 2. 邮件监听器 (Email Listener) +- **位置**: `src/relay/email-listener.js` +- **功能**: 监听和解析邮件回复 +- **特性**: + - IMAP/POP3 邮件接收 + - 邮件解析和命令提取 + - 会话 ID 验证 + - 安全检查 + +### 3. 命令中继服务 (Command Relay Service) +- **位置**: `src/relay/command-relay.js` +- **功能**: 管理命令队列和执行 +- **特性**: + - 会话管理 + - 命令队列 + - Claude Code 集成 + - 安全验证 + +### 4. 会话管理器 (Session Manager) +- **位置**: `src/relay/session-manager.js` +- **功能**: 管理 Claude Code 会话状态 +- **特性**: + - 会话创建和跟踪 + - 超时管理 + - 状态持久化 + +## 数据流程 + +### 发送通知流程 +1. Claude Code 触发 hook → TaskPing CLI +2. TaskPing 创建会话 ID 和通知 +3. 邮件渠道发送包含会话信息的邮件 +4. 会话管理器记录会话状态 + +### 命令执行流程 +1. 用户回复邮件 → 邮件监听器接收 +2. 解析邮件内容和会话 ID +3. 验证会话有效性和安全性 +4. 将命令加入执行队列 +5. 命令中继服务执行命令 + +## 邮件模板设计 + +### 通知邮件模板 +``` +主题: [TaskPing] Claude Code 任务完成 - {project} + +{user_name},您好! + +Claude Code 已完成任务,正在等待您的下一步指令。 + +项目: {project} +时间: {timestamp} +状态: {status} + +要继续对话,请直接回复此邮件,在邮件正文中输入您的指令。 + +示例回复: +"请继续优化代码" +"生成单元测试" +"解释这个函数的作用" + +--- +会话ID: {session_id} +安全提示: 请勿转发此邮件,会话将在24小时后过期 +``` + +### 回复解析规则 +- 提取邮件正文作为 Claude Code 指令 +- 忽略邮件签名和引用内容 +- 验证会话 ID 有效性 +- 检查指令安全性 + +## 安全机制 + +### 1. 会话验证 +- 唯一会话 ID (UUID v4) +- 24小时过期时间 +- 用户邮箱验证 + +### 2. 命令安全 +- 危险命令黑名单 +- 长度限制 (< 1000 字符) +- 特殊字符过滤 + +### 3. 频率限制 +- 每会话最多 10 次命令 +- 每小时最多 5 个新会话 +- 异常行为检测 + +## 配置结构 + +### 邮件配置 (config/channels.json) +```json +{ + "email": { + "type": "email", + "enabled": true, + "config": { + "smtp": { + "host": "smtp.gmail.com", + "port": 587, + "secure": false, + "auth": { + "user": "your-email@gmail.com", + "pass": "your-app-password" + } + }, + "imap": { + "host": "imap.gmail.com", + "port": 993, + "secure": true, + "auth": { + "user": "your-email@gmail.com", + "pass": "your-app-password" + } + }, + "from": "TaskPing ", + "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**: 测试和优化 + - 端到端测试 + - 错误处理完善 + - 性能优化 \ No newline at end of file diff --git a/docs/EMAIL_GUIDE.md b/docs/EMAIL_GUIDE.md new file mode 100644 index 0000000..d943b25 --- /dev/null +++ b/docs/EMAIL_GUIDE.md @@ -0,0 +1,346 @@ +# TaskPing 邮件功能使用指南 + +## 🌟 功能概述 + +TaskPing 现在支持邮件通知和远程命令执行功能,让您可以: + +1. **📧 接收邮件通知** - 当 Claude Code 任务完成时,自动发送邮件通知 +2. **🔄 远程命令执行** - 通过回复邮件来远程执行 Claude Code 命令 +3. **🏠 真正的远程工作** - 即使不在电脑前,也能继续与 Claude Code 对话 + +## 🚀 快速开始 + +### 步骤 1: 配置邮件设置 + +```bash +# 启动配置管理器 +node taskping.js config + +# 选择 "3. 通知渠道" +# 然后选择 "2. 邮件通知" +``` + +### 步骤 2: 输入邮箱配置 + +以 Gmail 为例: + +``` +SMTP 主机: smtp.gmail.com +SMTP 端口: 587 +使用 SSL/TLS: n (使用 STARTTLS) +SMTP 用户名: your-email@gmail.com +SMTP 密码: your-app-password # 需要使用应用密码 + +IMAP 主机: imap.gmail.com +IMAP 端口: 993 +IMAP 使用 SSL: y + +收件人邮箱: your-email@gmail.com +发件人显示名: TaskPing +``` + +### 步骤 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 编程体验!🚀 \ No newline at end of file diff --git a/docs/QUICK_EMAIL_SETUP.md b/docs/QUICK_EMAIL_SETUP.md new file mode 100644 index 0000000..c635f4c --- /dev/null +++ b/docs/QUICK_EMAIL_SETUP.md @@ -0,0 +1,266 @@ +# TaskPing 邮件功能快速配置指南 + +## 🚀 三种配置方式 + +TaskPing 现在提供三种方式来配置邮件功能,您可以选择最适合的方式: + +### 方式 1: 快速配置向导 (推荐 ⭐) + +```bash +node taskping.js setup-email +``` + +**优点**: +- 🎯 一步到位,引导式配置 +- 🛡️ 内置常见邮箱提供商设置 +- 🧪 配置完成后可立即测试 + +**适用于**: 初次配置,想要快速开始使用的用户 + +--- + +### 方式 2: 直接编辑配置文件 + +```bash +node taskping.js edit-config channels +``` + +**优点**: +- ⚡ 最快速,适合有经验的用户 +- 🔧 完全控制所有配置选项 +- 📝 支持批量修改和复制粘贴 + +**适用于**: 熟悉JSON格式,需要精确控制配置的用户 + +--- + +### 方式 3: 交互式配置管理器 + +```bash +node taskping.js config +# 选择 "3. 通知渠道" → "2. 邮件通知" +``` + +**优点**: +- 🎮 交互式界面,逐步配置 +- 💡 详细的配置说明和提示 +- 🔄 可以随时修改现有配置 + +**适用于**: 喜欢逐步配置,需要详细指导的用户 + +--- + +## 📧 常见邮箱配置 + +### Gmail 配置 + +**前提条件**: +1. 启用两步验证 +2. 生成应用密码 (16位) + +**配置参数**: +```json +{ + "smtp": { + "host": "smtp.gmail.com", + "port": 587, + "secure": false + }, + "imap": { + "host": "imap.gmail.com", + "port": 993, + "secure": true + } +} +``` + +### QQ邮箱配置 + +**前提条件**: +1. 开启SMTP/IMAP服务 +2. 获取授权码 + +**配置参数**: +```json +{ + "smtp": { + "host": "smtp.qq.com", + "port": 587, + "secure": false + }, + "imap": { + "host": "imap.qq.com", + "port": 993, + "secure": true + } +} +``` + +### 163邮箱配置 + +**配置参数**: +```json +{ + "smtp": { + "host": "smtp.163.com", + "port": 587, + "secure": false + }, + "imap": { + "host": "imap.163.com", + "port": 993, + "secure": true + } +} +``` + +--- + +## 🛠️ 配置后的使用流程 + +### 1. 测试邮件发送 + +```bash +node taskping.js test +``` + +应该能看到: +``` +Testing notification channels... + +✅ desktop: PASS +✅ email: PASS + +Test completed: 2/2 channels passed +``` + +### 2. 查看系统状态 + +```bash +node taskping.js status +``` + +应该能看到: +``` +TaskPing Status + +Configuration: + Enabled: Yes + Language: zh-CN + Sounds: Submarine / Hero + +Channels: + desktop: + Enabled: ✅ + Configured: ✅ + Supports Relay: ❌ + email: + Enabled: ✅ + Configured: ✅ + Supports Relay: ✅ +``` + +### 3. 启动命令中继服务 + +```bash +node taskping.js relay start +``` + +看到以下信息表示成功: +``` +🚀 启动邮件命令中继服务... +✅ 命令中继服务已启动 +📧 正在监听邮件回复... +💡 现在您可以通过回复邮件来远程执行Claude Code命令 + +按 Ctrl+C 停止服务 +``` + +### 4. 开始使用 + +现在当您使用 Claude Code 时: +1. 任务完成时会收到邮件通知 +2. 回复邮件即可远程执行下一步命令 +3. 享受远程AI编程的便利! + +--- + +## 🚨 常见问题解决 + +### Q: 邮件发送失败 + +**检查清单**: +- ✅ 邮箱密码是否使用应用密码 (不是登录密码) +- ✅ SMTP/IMAP 服务是否已开启 +- ✅ 网络连接是否正常 +- ✅ 防火墙是否阻止连接 + +**解决方法**: +```bash +# 重新配置 +node taskping.js setup-email + +# 或检查配置文件 +node taskping.js edit-config channels +``` + +### Q: 收不到邮件 + +**检查清单**: +- ✅ 垃圾邮件文件夹 +- ✅ 邮件地址是否正确 +- ✅ 邮箱存储空间是否充足 + +### Q: 无法接收回复 + +**检查清单**: +- ✅ 中继服务是否运行 (`taskping relay status`) +- ✅ IMAP 配置是否正确 +- ✅ 是否回复的是 TaskPing 发送的邮件 + +--- + +## 📱 使用技巧 + +### 1. 邮件模板定制 + +您可以通过编辑配置文件自定义邮件检查间隔: + +```bash +node taskping.js edit-config channels +``` + +找到 `template.checkInterval` 并修改值 (单位:秒): +```json +"template": { + "checkInterval": 30 // 每30秒检查一次新邮件 +} +``` + +### 2. 多项目管理 + +不同项目的通知会自动包含项目名称: +``` +[TaskPing] Claude Code 任务完成 - MyProject +[TaskPing] Claude Code 任务完成 - AnotherProject +``` + +### 3. 批量配置 + +如果您有多台电脑需要配置,可以: +1. 在一台电脑上配置好 +2. 复制 `config/channels.json` 文件 +3. 粘贴到其他电脑的同一位置 + +--- + +## 🎯 最佳实践 + +1. **安全性**: 定期更换应用密码 +2. **性能**: 根据使用频率调整检查间隔 +3. **维护**: 定期清理命令历史 (`taskping relay cleanup`) +4. **监控**: 定期检查中继服务状态 (`taskping relay status`) + +--- + +开始享受远程AI编程的便利吧!🚀 \ No newline at end of file diff --git a/email-automation.js b/email-automation.js new file mode 100755 index 0000000..4d32c4e --- /dev/null +++ b/email-automation.js @@ -0,0 +1,369 @@ +#!/usr/bin/env node + +/** + * TaskPing 邮件自动化 + * 监听邮件回复并自动输入到Claude Code + */ + +const Imap = require('node-imap'); +const { simpleParser } = require('mailparser'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +class EmailAutomation { + constructor() { + this.configPath = path.join(__dirname, 'config/channels.json'); + this.imap = null; + this.isRunning = false; + this.config = null; + } + + async start() { + console.log('🚀 TaskPing 邮件自动化启动中...\n'); + + // 加载配置 + if (!this.loadConfig()) { + console.log('❌ 请先配置邮件: npm run config'); + process.exit(1); + } + + console.log(`📧 监听邮箱: ${this.config.imap.auth.user}`); + console.log(`📬 发送通知到: ${this.config.to}\n`); + + try { + await this.connectToEmail(); + this.startListening(); + + console.log('✅ 邮件监听启动成功'); + console.log('💌 现在可以回复TaskPing邮件来发送命令到Claude Code'); + console.log('按 Ctrl+C 停止服务\n'); + + this.setupGracefulShutdown(); + process.stdin.resume(); + + } catch (error) { + console.error('❌ 启动失败:', error.message); + process.exit(1); + } + } + + loadConfig() { + try { + const data = fs.readFileSync(this.configPath, 'utf8'); + const config = JSON.parse(data); + + if (!config.email?.enabled) { + console.log('❌ 邮件功能未启用'); + return false; + } + + this.config = config.email.config; + return true; + } catch (error) { + console.log('❌ 配置文件读取失败'); + return false; + } + } + + async connectToEmail() { + return new Promise((resolve, reject) => { + this.imap = new Imap({ + user: this.config.imap.auth.user, + password: this.config.imap.auth.pass, + host: this.config.imap.host, + port: this.config.imap.port, + tls: this.config.imap.secure, + connTimeout: 60000, + authTimeout: 30000, + keepalive: true + }); + + this.imap.once('ready', () => { + console.log('📬 IMAP连接成功'); + resolve(); + }); + + this.imap.once('error', reject); + this.imap.once('end', () => { + if (this.isRunning) { + console.log('🔄 连接断开,尝试重连...'); + setTimeout(() => this.connectToEmail().catch(console.error), 5000); + } + }); + + this.imap.connect(); + }); + } + + startListening() { + this.isRunning = true; + console.log('👂 开始监听新邮件...'); + + // 每15秒检查一次新邮件 + setInterval(() => { + if (this.isRunning) { + this.checkNewEmails(); + } + }, 15000); + + // 立即检查一次 + this.checkNewEmails(); + } + + async checkNewEmails() { + try { + await this.openInbox(); + + // 查找最近1小时内的未读邮件 + const since = new Date(); + since.setHours(since.getHours() - 1); + + this.imap.search([['UNSEEN'], ['SINCE', since]], (err, results) => { + if (err) { + console.error('搜索邮件失败:', err.message); + return; + } + + if (results.length > 0) { + console.log(`📧 发现 ${results.length} 封新邮件`); + this.processEmails(results); + } + }); + } catch (error) { + console.error('检查邮件失败:', error.message); + } + } + + openInbox() { + return new Promise((resolve, reject) => { + this.imap.openBox('INBOX', false, (err, box) => { + if (err) reject(err); + else resolve(box); + }); + }); + } + + processEmails(emailUids) { + const fetch = this.imap.fetch(emailUids, { + bodies: '', + markSeen: true + }); + + fetch.on('message', (msg) => { + let buffer = ''; + + msg.on('body', (stream) => { + stream.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + }); + + stream.once('end', async () => { + try { + const parsed = await simpleParser(buffer); + await this.handleEmailReply(parsed); + } catch (error) { + console.error('处理邮件失败:', error.message); + } + }); + }); + }); + } + + async handleEmailReply(email) { + // 检查是否是TaskPing回复 + if (!this.isTaskPingReply(email)) { + return; + } + + // 提取命令 + const command = this.extractCommand(email); + if (!command) { + console.log('邮件中未找到有效命令'); + return; + } + + console.log(`🎯 收到命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`); + + // 执行命令到Claude Code + await this.sendToClaudeCode(command); + } + + isTaskPingReply(email) { + const subject = email.subject || ''; + return subject.includes('[TaskPing]') || + subject.match(/^(Re:|RE:|回复:)/i); + } + + extractCommand(email) { + let text = email.text || ''; + const lines = text.split('\n'); + const commandLines = []; + + for (const line of lines) { + // 停止处理当遇到原始邮件标记 + if (line.includes('-----Original Message-----') || + line.includes('--- Original Message ---') || + line.includes('在') && line.includes('写道:') || + line.includes('On') && line.includes('wrote:') || + line.match(/^>\s*/) || + line.includes('会话ID:')) { + break; + } + + // 跳过签名 + if (line.includes('--') || + line.includes('Sent from') || + line.includes('发自我的')) { + break; + } + + commandLines.push(line); + } + + return commandLines.join('\n').trim(); + } + + async sendToClaudeCode(command) { + console.log('🤖 正在发送命令到Claude Code...'); + + try { + // 方法1: 复制到剪贴板 + await this.copyToClipboard(command); + + // 方法2: 强制自动化输入 + const success = await this.forceAutomation(command); + + if (success) { + console.log('✅ 命令已自动输入到Claude Code'); + } else { + console.log('⚠️ 自动输入失败,命令已复制到剪贴板'); + console.log('💡 请手动在Claude Code中粘贴 (Cmd+V)'); + + // 发送通知提醒 + await this.sendNotification(command); + } + } catch (error) { + console.error('❌ 发送命令失败:', error.message); + } + } + + async copyToClipboard(command) { + return new Promise((resolve, reject) => { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(command); + pbcopy.stdin.end(); + + pbcopy.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error('剪贴板复制失败')); + } + }); + }); + } + + async forceAutomation(command) { + return new Promise((resolve) => { + // 转义命令中的特殊字符 + const escapedCommand = command + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/'/g, "\\'"); + + const script = ` + tell application "System Events" + -- 查找Claude Code相关应用 + set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"} + set targetApp to null + + repeat with appName in claudeApps + try + if application process appName exists then + set targetApp to application process appName + exit repeat + end if + end try + end repeat + + -- 如果没找到Claude,查找开发工具 + if targetApp is null then + set devApps to {"Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor"} + repeat with appName in devApps + try + if application process appName exists then + set targetApp to application process appName + exit repeat + end if + end try + end repeat + end if + + if targetApp is not null then + -- 激活应用 + set frontmost of targetApp to true + delay 1 + + -- 确保窗口激活 + repeat 10 times + if frontmost of targetApp then exit repeat + delay 0.1 + end repeat + + -- 清空并输入命令 + keystroke "a" using command down + delay 0.3 + keystroke "${escapedCommand}" + delay 0.5 + keystroke return + + return "success" + else + return "no_app" + end if + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.on('close', (code) => { + const success = code === 0 && output === 'success'; + resolve(success); + }); + + osascript.on('error', () => resolve(false)); + }); + } + + async sendNotification(command) { + const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command; + + const script = ` + display notification "邮件命令已准备好,请在Claude Code中粘贴执行" with title "TaskPing" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "default" + `; + + spawn('osascript', ['-e', script]); + } + + setupGracefulShutdown() { + process.on('SIGINT', () => { + console.log('\n🛑 正在停止邮件监听...'); + this.isRunning = false; + if (this.imap) { + this.imap.end(); + } + console.log('✅ 服务已停止'); + process.exit(0); + }); + } +} + +// 启动服务 +const automation = new EmailAutomation(); +automation.start().catch(console.error); \ No newline at end of file diff --git a/email-checker.js b/email-checker.js new file mode 100644 index 0000000..669dda4 --- /dev/null +++ b/email-checker.js @@ -0,0 +1,364 @@ +#!/usr/bin/env node + +/** + * TaskPing 邮件检查器 + * 更强大的邮件搜索和内容提取工具 + */ + +const Imap = require('node-imap'); +const { simpleParser } = require('mailparser'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +class EmailChecker { + constructor() { + this.configPath = path.join(__dirname, 'config/channels.json'); + this.config = null; + this.imap = null; + } + + async start() { + console.log('🔍 TaskPing 强化邮件检查器启动\n'); + + // 加载配置 + if (!this.loadConfig()) { + console.log('❌ 请先配置邮件: npm run config'); + process.exit(1); + } + + console.log(`📧 邮箱: ${this.config.imap.auth.user}`); + console.log(`📬 通知发送到: ${this.config.to}\n`); + + try { + await this.connectToEmail(); + await this.comprehensiveEmailCheck(); + await this.startContinuousMonitoring(); + } catch (error) { + console.error('❌ 启动失败:', error.message); + process.exit(1); + } + } + + loadConfig() { + try { + const data = fs.readFileSync(this.configPath, 'utf8'); + const config = JSON.parse(data); + + if (!config.email?.enabled) { + console.log('❌ 邮件功能未启用'); + return false; + } + + this.config = config.email.config; + return true; + } catch (error) { + console.log('❌ 配置文件读取失败'); + return false; + } + } + + async connectToEmail() { + return new Promise((resolve, reject) => { + this.imap = new Imap({ + user: this.config.imap.auth.user, + password: this.config.imap.auth.pass, + host: this.config.imap.host, + port: this.config.imap.port, + tls: this.config.imap.secure, + connTimeout: 60000, + authTimeout: 30000, + keepalive: true + }); + + this.imap.once('ready', () => { + console.log('✅ IMAP连接成功'); + resolve(); + }); + + this.imap.once('error', reject); + this.imap.connect(); + }); + } + + async comprehensiveEmailCheck() { + console.log('\n🔍 开始全面邮件检查...\n'); + + await this.openInbox(); + + // 1. 检查最近24小时所有邮件(不仅是未读) + console.log('📅 1. 检查最近24小时所有邮件...'); + await this.searchEmails('24h', false); + + // 2. 检查最近1小时未读邮件 + console.log('\n📧 2. 检查最近1小时未读邮件...'); + await this.searchEmails('1h', true); + + // 3. 检查主题包含特定关键词的邮件 + console.log('\n🎯 3. 检查TaskPing相关邮件...'); + await this.searchBySubject(); + + // 4. 检查来自特定发件人的邮件 + console.log('\n👤 4. 检查来自目标邮箱的邮件...'); + await this.searchByFrom(); + } + + async openInbox() { + return new Promise((resolve, reject) => { + this.imap.openBox('INBOX', false, (err, box) => { + if (err) { + reject(err); + return; + } + console.log(`📫 收件箱: 总计${box.messages.total}封邮件`); + resolve(box); + }); + }); + } + + async searchEmails(timeRange, unseenOnly = false) { + return new Promise((resolve) => { + const since = new Date(); + if (timeRange === '1h') { + since.setHours(since.getHours() - 1); + } else if (timeRange === '24h') { + since.setDate(since.getDate() - 1); + } + + let searchCriteria = [['SINCE', since]]; + if (unseenOnly) { + searchCriteria.push(['UNSEEN']); + } + + console.log(`🔍 搜索条件: ${timeRange}, ${unseenOnly ? '仅未读' : '全部'}`); + + this.imap.search(searchCriteria, (err, results) => { + if (err) { + console.error('❌ 搜索失败:', err.message); + resolve(); + return; + } + + console.log(`📨 找到 ${results.length} 封邮件`); + + if (results.length > 0) { + this.analyzeEmails(results.slice(-5)); // 只分析最新5封 + } + resolve(); + }); + }); + } + + async searchBySubject() { + return new Promise((resolve) => { + // 搜索主题包含Re:或TaskPing的邮件 + this.imap.search([['OR', ['SUBJECT', 'Re:'], ['SUBJECT', 'TaskPing']]], (err, results) => { + if (err) { + console.error('❌ 主题搜索失败:', err.message); + resolve(); + return; + } + + console.log(`📨 找到 ${results.length} 封相关邮件`); + + if (results.length > 0) { + this.analyzeEmails(results.slice(-3)); // 只分析最新3封 + } + resolve(); + }); + }); + } + + async searchByFrom() { + return new Promise((resolve) => { + // 搜索来自目标邮箱的邮件 + const targetEmail = this.config.to; + + this.imap.search([['FROM', targetEmail]], (err, results) => { + if (err) { + console.error('❌ 发件人搜索失败:', err.message); + resolve(); + return; + } + + console.log(`📨 找到来自 ${targetEmail} 的 ${results.length} 封邮件`); + + if (results.length > 0) { + this.analyzeEmails(results.slice(-3)); // 只分析最新3封 + } + resolve(); + }); + }); + } + + analyzeEmails(emailUids) { + const fetch = this.imap.fetch(emailUids, { + bodies: '', + markSeen: false // 不标记为已读 + }); + + fetch.on('message', (msg, seqno) => { + console.log(`\n📧 分析邮件 ${seqno}:`); + let buffer = ''; + + msg.on('body', (stream) => { + stream.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + }); + + stream.once('end', async () => { + try { + const parsed = await simpleParser(buffer); + await this.processEmailContent(parsed, seqno); + } catch (error) { + console.error(`❌ 解析邮件 ${seqno} 失败:`, error.message); + } + }); + }); + }); + } + + async processEmailContent(email, seqno) { + console.log(` 📄 主题: ${email.subject || '(无主题)'}`); + console.log(` 👤 发件人: ${email.from?.text || '(未知)'}`); + console.log(` 📅 时间: ${email.date || '(未知)'}`); + + // 判断是否是潜在的回复邮件 + const isPotentialReply = this.isPotentialReply(email); + console.log(` 🎯 潜在回复: ${isPotentialReply ? '是' : '否'}`); + + if (isPotentialReply) { + const command = this.extractCommand(email); + console.log(` 💬 邮件内容长度: ${(email.text || '').length} 字符`); + console.log(` 📝 提取的内容:\n"${command.substring(0, 200)}${command.length > 200 ? '...' : ''}"`); + + if (command && command.trim().length > 0) { + console.log(`\n🎉 发现有效命令! (邮件 ${seqno})`); + await this.handleCommand(command, seqno); + } + } + } + + isPotentialReply(email) { + const subject = email.subject || ''; + const from = email.from?.text || ''; + const targetEmail = this.config.to; + + // 检查多种条件 + return ( + subject.includes('[TaskPing]') || + subject.match(/^(Re:|RE:|回复:)/i) || + from.includes(targetEmail) || + (email.text && email.text.length > 10) // 有实际内容 + ); + } + + extractCommand(email) { + let text = email.text || ''; + const lines = text.split('\n'); + const commandLines = []; + + for (const line of lines) { + // 停止处理当遇到原始邮件标记 + if (line.includes('-----Original Message-----') || + line.includes('--- Original Message ---') || + (line.includes('在') && line.includes('写道:')) || + (line.includes('On') && line.includes('wrote:')) || + line.match(/^>\s*/) || + line.includes('会话ID:') || + line.includes('TaskPing <') || + line.match(/\d{4}年\d{1,2}月\d{1,2}日/)) { + break; + } + + // 跳过签名和空行 + if (line.includes('--') || + line.includes('Sent from') || + line.includes('发自我的') || + line.trim() === '') { + continue; + } + + commandLines.push(line); + } + + return commandLines.join('\n').trim(); + } + + async handleCommand(command, seqno) { + console.log(`\n🚀 处理命令 (来自邮件 ${seqno}):`); + console.log(`📝 命令内容: "${command}"`); + + try { + // 复制到剪贴板 + await this.copyToClipboard(command); + console.log('✅ 命令已复制到剪贴板'); + + // 发送通知 + await this.sendNotification(command); + console.log('✅ 通知已发送'); + + console.log('\n🎯 请在Claude Code中粘贴命令 (Cmd+V)'); + + } catch (error) { + console.error('❌ 处理命令失败:', error.message); + } + } + + async copyToClipboard(command) { + return new Promise((resolve, reject) => { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(command); + pbcopy.stdin.end(); + + pbcopy.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error('剪贴板复制失败')); + } + }); + }); + } + + async sendNotification(command) { + const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command; + + const script = ` + display notification "邮件命令已复制到剪贴板,请在Claude Code中粘贴" with title "TaskPing" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Glass" + `; + + spawn('osascript', ['-e', script]); + } + + async startContinuousMonitoring() { + console.log('\n👂 开始持续监控新邮件...'); + console.log('💌 现在可以回复邮件测试功能'); + console.log('🔍 每30秒检查一次新邮件\n'); + + setInterval(async () => { + try { + console.log('🔄 检查新邮件...'); + await this.searchEmails('1h', false); // 搜索所有邮件,不只是未读 + } catch (error) { + console.error('❌ 监控检查失败:', error.message); + } + }, 30000); // 每30秒检查一次 + + // 设置优雅关闭 + process.on('SIGINT', () => { + console.log('\n🛑 停止邮件监控...'); + if (this.imap) { + this.imap.end(); + } + console.log('✅ 服务已停止'); + process.exit(0); + }); + + process.stdin.resume(); + } +} + +// 启动检查器 +const checker = new EmailChecker(); +checker.start().catch(console.error); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ebdb996 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,403 @@ +{ + "name": "taskping", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "taskping", + "version": "1.0.0", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "mailparser": "^3.7.4", + "node-imap": "^0.9.6", + "nodemailer": "^7.0.5", + "uuid": "^11.1.0" + }, + "bin": { + "taskping-config": "config-tool.js", + "taskping-install": "install.js", + "taskping-notify": "hook-notify.js" + }, + "devDependencies": {}, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==" + }, + "node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmmirror.com/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/mailparser": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/mailparser/-/mailparser-3.7.4.tgz", + "integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==", + "dependencies": { + "encoding-japanese": "2.2.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.6.3", + "libmime": "5.3.7", + "linkify-it": "5.0.0", + "mailsplit": "5.4.5", + "nodemailer": "7.0.4", + "punycode.js": "2.3.1", + "tlds": "1.259.0" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/mailsplit": { + "version": "5.4.5", + "resolved": "https://registry.npmmirror.com/mailsplit/-/mailsplit-5.4.5.tgz", + "integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/node-imap": { + "version": "0.9.6", + "resolved": "https://registry.npmmirror.com/node-imap/-/node-imap-0.9.6.tgz", + "integrity": "sha512-pYQ2AtjQwrSvILq8EYInv3E3svrJwrTOxzW7uBGpP//AkCs/pMdO+O6KEgUlSchh/0/N0MSWs5io3xZhxJ9yLg==", + "dependencies": { + "readable-stream": "^3.6.0", + "utf7": "^1.0.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nodemailer": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmmirror.com/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.3.0.tgz", + "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tlds": { + "version": "1.259.0", + "resolved": "https://registry.npmmirror.com/tlds/-/tlds-1.259.0.tgz", + "integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/utf7": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/utf7/-/utf7-1.0.2.tgz", + "integrity": "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==", + "dependencies": { + "semver": "~5.3.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..33c8143 --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "name": "taskping", + "version": "1.0.0", + "description": "Claude Code智能任务通知系统 - 当Claude完成任务或需要输入时发送桌面通知", + "main": "hook-notify.js", + "scripts": { + "install": "node install.js", + "config": "node taskping-config.js", + "test": "node config-tool.js --test", + "test-completed": "node hook-notify.js --type completed", + "test-waiting": "node hook-notify.js --type waiting", + "daemon:start": "node taskping.js daemon start", + "daemon:stop": "node taskping.js daemon stop", + "daemon:status": "node taskping.js daemon status", + "test:clipboard": "node test-clipboard.js", + "start": "node email-automation.js" + }, + "bin": { + "taskping-install": "./install.js", + "taskping-config": "./config-tool.js", + "taskping-notify": "./hook-notify.js" + }, + "keywords": [ + "claude-code", + "notification", + "desktop-notification", + "hooks", + "productivity", + "development-tools", + "task-management", + "claude", + "ai-assistant" + ], + "author": "TaskPing Team", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "os": [ + "darwin", + "linux", + "win32" + ], + "repository": { + "type": "git", + "url": "https://github.com/TaskPing/TaskPing.git" + }, + "bugs": { + "url": "https://github.com/TaskPing/TaskPing/issues" + }, + "homepage": "https://github.com/TaskPing/TaskPing#readme", + "dependencies": { + "mailparser": "^3.7.4", + "node-imap": "^0.9.6", + "nodemailer": "^7.0.5", + "uuid": "^11.1.0" + }, + "files": [ + "hook-notify.js", + "config-tool.js", + "install.js", + "config.json", + "i18n.json", + "claude-hooks.json", + "sounds/", + "README.md", + "QUICKSTART.md", + "LICENSE" + ] +} diff --git a/setup-permissions.js b/setup-permissions.js new file mode 100755 index 0000000..fa3d09d --- /dev/null +++ b/setup-permissions.js @@ -0,0 +1,184 @@ +#!/usr/bin/env node + +/** + * macOS 权限设置助手 + * 帮助用户设置必要的系统权限 + */ + +const { spawn } = require('child_process'); + +class PermissionSetup { + constructor() { + this.requiredPermissions = [ + 'Accessibility', + 'Automation' + ]; + } + + async checkAndSetup() { + console.log('🔒 macOS 权限设置助手\n'); + + if (process.platform !== 'darwin') { + console.log('ℹ️ 此工具仅适用于 macOS 系统'); + return; + } + + console.log('TaskPing 需要以下权限才能自动粘贴邮件回复到 Claude Code:\n'); + + console.log('1. 🖱️ 辅助功能权限 (Accessibility)'); + console.log(' - 允许控制其他应用程序'); + console.log(' - 自动输入和点击'); + + console.log('\n2. 🤖 自动化权限 (Automation)'); + console.log(' - 允许发送 Apple Events'); + console.log(' - 控制应用程序行为\n'); + + // 检查当前权限状态 + await this.checkCurrentPermissions(); + + console.log('📋 设置步骤:\n'); + + console.log('步骤 1: 打开系统偏好设置'); + console.log(' → 苹果菜单 > 系统偏好设置 > 安全性与隐私 > 隐私\n'); + + console.log('步骤 2: 设置辅助功能权限'); + console.log(' → 点击左侧 "辅助功能"'); + console.log(' → 点击锁图标并输入密码'); + console.log(' → 添加以下应用:'); + console.log(' • Terminal (如果你在 Terminal 中运行 TaskPing)'); + console.log(' • iTerm2 (如果使用 iTerm2)'); + console.log(' • Visual Studio Code (如果在 VS Code 中运行)'); + console.log(' • 或者你当前使用的终端应用\n'); + + console.log('步骤 3: 设置自动化权限'); + console.log(' → 点击左侧 "自动化"'); + console.log(' → 在你的终端应用下勾选:'); + console.log(' • System Events'); + console.log(' • Claude Code (如果有的话)'); + console.log(' • Terminal\n'); + + console.log('🚀 快速打开系统偏好设置?'); + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const answer = await this.question(rl, '是否现在打开系统偏好设置?(y/n): '); + + if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { + await this.openSystemPreferences(); + } + + rl.close(); + + console.log('\n✅ 设置完成后,重新运行以下命令测试:'); + console.log(' node taskping.js test-paste\n'); + } + + async checkCurrentPermissions() { + console.log('🔍 检查当前权限状态...\n'); + + try { + // 测试基本的 AppleScript 执行 + const result = await this.runAppleScript('tell application "System Events" to return name of first process'); + if (result) { + console.log('✅ 基本 AppleScript 权限:正常'); + } + } catch (error) { + console.log('❌ 基本 AppleScript 权限:需要设置'); + } + + try { + // 测试辅助功能权限 + const result = await this.runAppleScript(` + tell application "System Events" + try + return name of first application process whose frontmost is true + on error + return "permission_denied" + end try + end tell + `); + + if (result && result !== 'permission_denied') { + console.log('✅ 辅助功能权限:已授权'); + } else { + console.log('❌ 辅助功能权限:需要授权'); + } + } catch (error) { + console.log('❌ 辅助功能权限:需要授权'); + } + + console.log(''); + } + + async runAppleScript(script) { + return new Promise((resolve, reject) => { + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + let error = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString(); + }); + + osascript.stderr.on('data', (data) => { + error += data.toString(); + }); + + osascript.on('close', (code) => { + if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(error || 'AppleScript execution failed')); + } + }); + }); + } + + async openSystemPreferences() { + try { + console.log('\n🔧 正在打开系统偏好设置...'); + + // 直接打开安全性与隐私 > 隐私 > 辅助功能 + const script = ` + tell application "System Preferences" + activate + set current pane to pane "com.apple.preference.security" + delay 1 + tell application "System Events" + tell window 1 of application process "System Preferences" + click tab "Privacy" + delay 0.5 + tell outline 1 of scroll area 1 + select row "Accessibility" + end tell + end tell + end tell + end tell + `; + + await this.runAppleScript(script); + console.log('✅ 已打开辅助功能设置页面'); + + } catch (error) { + console.log('⚠️ 无法自动打开,请手动打开系统偏好设置'); + console.log('💡 你也可以运行:open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"'); + } + } + + question(rl, prompt) { + return new Promise(resolve => { + rl.question(prompt, resolve); + }); + } +} + +// 运行权限设置助手 +if (require.main === module) { + const setup = new PermissionSetup(); + setup.checkAndSetup().catch(console.error); +} + +module.exports = PermissionSetup; \ No newline at end of file diff --git a/simple-start.js b/simple-start.js new file mode 100755 index 0000000..52b3e04 --- /dev/null +++ b/simple-start.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +/** + * TaskPing 简化启动脚本 + * 专注解决核心问题,减少复杂性 + */ + +const fs = require('fs'); +const path = require('path'); +const SimplifiedEmailAutomation = require('./src/simplified/email-automation'); + +class SimpleTaskPing { + constructor() { + this.configPath = path.join(__dirname, 'config/channels.json'); + this.automation = null; + } + + async start() { + console.log('🚀 TaskPing 简化版启动中...\n'); + + try { + // 加载配置 + const config = this.loadConfig(); + if (!config) { + console.log('❌ 配置加载失败,请先配置邮件'); + console.log('💡 运行: npm run config'); + process.exit(1); + } + + // 验证配置 + if (!this.validateConfig(config)) { + console.log('❌ 邮件配置不完整,请重新配置'); + console.log('💡 运行: npm run config'); + process.exit(1); + } + + console.log('✅ 配置验证通过'); + console.log(`📧 邮箱: ${config.email.config.imap.auth.user}`); + console.log(`📬 通知发送到: ${config.email.config.to}\n`); + + // 启动简化的邮件自动化 + this.automation = new SimplifiedEmailAutomation(config.email.config); + + // 设置事件监听 + this.automation.on('commandExecuted', (data) => { + console.log(`\n🎉 成功处理邮件命令 (邮件 ${data.emailSeq})`); + console.log('💡 命令已复制到剪贴板,请在 Claude Code 中粘贴'); + }); + + this.automation.on('commandFailed', (data) => { + console.log(`\n❌ 处理命令失败 (邮件 ${data.emailSeq}): ${data.error.message}`); + }); + + // 启动服务 + await this.automation.start(); + + console.log('\n🎯 TaskPing 简化版运行中...'); + console.log('💌 现在你可以回复 TaskPing 邮件来发送命令了'); + console.log('📋 命令会自动复制到剪贴板,你只需要在 Claude Code 中粘贴'); + console.log('\n按 Ctrl+C 停止服务\n'); + + // 处理优雅关闭 + process.on('SIGINT', async () => { + console.log('\n🛑 正在停止 TaskPing...'); + if (this.automation) { + await this.automation.stop(); + } + console.log('✅ 服务已停止'); + process.exit(0); + }); + + // 保持进程运行 + process.stdin.resume(); + + } catch (error) { + console.error('❌ 启动失败:', error.message); + process.exit(1); + } + } + + loadConfig() { + try { + if (!fs.existsSync(this.configPath)) { + console.log('❌ 配置文件不存在'); + return null; + } + + const data = fs.readFileSync(this.configPath, 'utf8'); + return JSON.parse(data); + } catch (error) { + console.error('❌ 配置文件读取失败:', error.message); + return null; + } + } + + validateConfig(config) { + if (!config.email || !config.email.enabled) { + console.log('❌ 邮件功能未启用'); + return false; + } + + const emailConfig = config.email.config; + + // 检查必需字段 + const required = [ + 'smtp.host', + 'smtp.auth.user', + 'smtp.auth.pass', + 'imap.host', + 'imap.auth.user', + 'imap.auth.pass', + 'to' + ]; + + for (const field of required) { + if (!this.getNestedValue(emailConfig, field)) { + console.log(`❌ 缺少必需配置: ${field}`); + return false; + } + } + + return true; + } + + getNestedValue(obj, path) { + return path.split('.').reduce((current, key) => { + return current && current[key] !== undefined ? current[key] : undefined; + }, obj); + } + + async status() { + if (!this.automation) { + console.log('❌ 服务未运行'); + return; + } + + const status = this.automation.getStatus(); + console.log('📊 TaskPing 简化版状态:'); + console.log(` 运行状态: ${status.running ? '✅ 运行中' : '❌ 已停止'}`); + console.log(` IMAP 连接: ${status.connected ? '✅ 已连接' : '❌ 未连接'}`); + console.log(` 命令文件: ${status.commandFile}`); + console.log(` 最新命令: ${status.lastCommandExists ? '✅ 有' : '❌ 无'}`); + } + + showHelp() { + console.log(` +🚀 TaskPing 简化版 + +用法: node simple-start.js [命令] + +命令: + start 启动邮件监听服务 (默认) + status 显示服务状态 + help 显示帮助信息 + +配置: + npm run config 交互式配置邮件 + +特点: + • 🎯 专注核心功能,减少复杂性 + • 📧 稳定的邮件监听和解析 + • 📋 自动复制命令到剪贴板 + • 🔔 友好的通知提醒 + • 🛡️ 降级方案,减少权限依赖 + +使用流程: + 1. 配置邮箱: npm run config + 2. 启动服务: node simple-start.js + 3. 回复 TaskPing 邮件发送命令 + 4. 在 Claude Code 中粘贴命令 (Cmd+V) + `); + } +} + +// 处理命令行参数 +const args = process.argv.slice(2); +const command = args[0] || 'start'; + +const taskping = new SimpleTaskPing(); + +switch (command) { + case 'start': + taskping.start(); + break; + case 'status': + taskping.status(); + break; + case 'help': + case '--help': + case '-h': + taskping.showHelp(); + break; + default: + console.log(`未知命令: ${command}`); + taskping.showHelp(); + process.exit(1); +} \ No newline at end of file diff --git a/src/automation/claude-automation.js b/src/automation/claude-automation.js new file mode 100644 index 0000000..05842a1 --- /dev/null +++ b/src/automation/claude-automation.js @@ -0,0 +1,395 @@ +/** + * Claude Code 专用自动化 + * 专门针对 Claude Code 的完全自动化解决方案 + */ + +const { spawn } = require('child_process'); +const Logger = require('../core/logger'); + +class ClaudeAutomation { + constructor() { + this.logger = new Logger('ClaudeAutomation'); + } + + /** + * 完全自动化发送命令到 Claude Code + * @param {string} command - 要发送的命令 + * @param {string} sessionId - 会话ID + * @returns {Promise} - 是否成功 + */ + async sendCommand(command, sessionId = '') { + try { + this.logger.info(`Sending command to Claude Code: ${command.substring(0, 50)}...`); + + // 首先复制命令到剪贴板 + await this._copyToClipboard(command); + + // 然后执行完全自动化的粘贴和执行 + const success = await this._fullAutomation(command); + + if (success) { + this.logger.info('Command sent and executed successfully'); + return true; + } else { + // 如果失败,尝试备选方案 + return await this._fallbackAutomation(command); + } + + } catch (error) { + this.logger.error('Claude automation failed:', error.message); + return false; + } + } + + /** + * 复制命令到剪贴板 + */ + async _copyToClipboard(command) { + return new Promise((resolve, reject) => { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(command); + pbcopy.stdin.end(); + + pbcopy.on('close', (code) => { + if (code === 0) { + this.logger.debug('Command copied to clipboard'); + resolve(); + } else { + reject(new Error('Failed to copy to clipboard')); + } + }); + + pbcopy.on('error', reject); + }); + } + + /** + * 完全自动化方案 + */ + async _fullAutomation(command) { + if (process.platform !== 'darwin') { + return false; + } + + return new Promise((resolve) => { + const script = ` + tell application "System Events" + -- 定义可能的 Claude Code 应用名称 + set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"} + set terminalApps to {"Terminal", "iTerm2", "iTerm", "Warp Terminal", "Warp"} + set codeApps to {"Visual Studio Code", "Code", "Cursor", "Sublime Text", "Atom"} + + -- 首先尝试找到 Claude Code + set targetApp to null + set appName to "" + + -- 检查 Claude 应用 + repeat with app in claudeApps + try + if application process app exists then + set targetApp to application process app + set appName to app + exit repeat + end if + end try + end repeat + + -- 如果没找到 Claude,检查终端应用 + if targetApp is null then + repeat with app in terminalApps + try + if application process app exists then + set targetApp to application process app + set appName to app + exit repeat + end if + end try + end repeat + end if + + -- 如果还没找到,检查代码编辑器 + if targetApp is null then + repeat with app in codeApps + try + if application process app exists then + set targetApp to application process app + set appName to app + exit repeat + end if + end try + end repeat + end if + + if targetApp is not null then + -- 激活应用 + set frontmost of targetApp to true + delay 0.8 + + -- 等待应用完全激活 + repeat while (frontmost of targetApp) is false + delay 0.1 + end repeat + + -- 根据不同应用类型执行不同操作 + if appName contains "Claude" then + -- Claude Code 特定操作 + try + -- 尝试点击输入框 + click (first text field of window 1) + delay 0.3 + on error + -- 如果没有文本框,尝试按键导航 + key code 125 -- 向下箭头 + delay 0.2 + end try + + -- 清空当前内容并粘贴新命令 + keystroke "a" using command down + delay 0.2 + keystroke "v" using command down + delay 0.5 + + -- 执行命令 + keystroke return + + else if appName contains "Terminal" or appName contains "iTerm" or appName contains "Warp" then + -- 终端应用操作 + delay 0.5 + keystroke "v" using command down + delay 0.3 + keystroke return + + else + -- 其他应用(代码编辑器等) + delay 0.5 + keystroke "v" using command down + delay 0.3 + keystroke return + end if + + return "success:" & appName + else + return "no_app_found" + end if + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + let error = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.stderr.on('data', (data) => { + error += data.toString(); + }); + + osascript.on('close', (code) => { + if (code === 0 && output.startsWith('success:')) { + const appName = output.split(':')[1]; + this.logger.info(`Command successfully sent to ${appName}`); + resolve(true); + } else { + this.logger.warn(`Full automation failed: ${output || error}`); + resolve(false); + } + }); + + osascript.on('error', (err) => { + this.logger.error('AppleScript execution error:', err.message); + resolve(false); + }); + }); + } + + /** + * 备选自动化方案 - 更强制性的方法 + */ + async _fallbackAutomation(command) { + if (process.platform !== 'darwin') { + return false; + } + + return new Promise((resolve) => { + // 更强制性的方案,直接输入文本 + const escapedCommand = command + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n'); + + const script = ` + tell application "System Events" + -- 获取当前前台应用 + set frontApp to first application process whose frontmost is true + set appName to name of frontApp + + -- 等待一下确保应用响应 + delay 1 + + -- 直接输入命令文本(不依赖剪贴板) + try + -- 先清空可能的现有内容 + keystroke "a" using command down + delay 0.2 + + -- 输入命令 + keystroke "${escapedCommand}" + delay 0.5 + + -- 执行 + keystroke return + + return "typed_success:" & appName + on error errorMsg + -- 如果直接输入失败,尝试粘贴 + try + keystroke "v" using command down + delay 0.3 + keystroke return + return "paste_success:" & appName + on error + return "failed:" & errorMsg + end try + end try + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.on('close', (code) => { + if (code === 0 && (output.includes('success'))) { + this.logger.info(`Fallback automation succeeded: ${output}`); + resolve(true); + } else { + this.logger.error(`Fallback automation failed: ${output}`); + resolve(false); + } + }); + + osascript.on('error', () => { + resolve(false); + }); + }); + } + + /** + * 专门激活 Claude Code 应用 + */ + async activateClaudeCode() { + if (process.platform !== 'darwin') { + return false; + } + + return new Promise((resolve) => { + const script = ` + tell application "System Events" + set claudeApps to {"Claude", "Claude Code", "Claude Desktop", "Anthropic Claude"} + + repeat with appName in claudeApps + try + if application process appName exists then + set frontmost of application process appName to true + return "activated:" & appName + end if + end try + end repeat + + return "not_found" + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.on('close', (code) => { + if (code === 0 && output.startsWith('activated:')) { + this.logger.info('Claude Code activated successfully'); + resolve(true); + } else { + this.logger.warn('Could not activate Claude Code'); + resolve(false); + } + }); + + osascript.on('error', () => resolve(false)); + }); + } + + /** + * 检查系统权限并尝试请求 + */ + async requestPermissions() { + if (process.platform !== 'darwin') { + return false; + } + + try { + // 尝试一个简单的操作来触发权限请求 + const script = ` + tell application "System Events" + try + set frontApp to name of first application process whose frontmost is true + return "permission_granted" + on error + return "permission_denied" + end try + end tell + `; + + const result = await this._runAppleScript(script); + return result === 'permission_granted'; + } catch (error) { + this.logger.error('Permission check failed:', error.message); + return false; + } + } + + async _runAppleScript(script) { + return new Promise((resolve, reject) => { + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + let error = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString(); + }); + + osascript.stderr.on('data', (data) => { + error += data.toString(); + }); + + osascript.on('close', (code) => { + if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(error || `Exit code: ${code}`)); + } + }); + }); + } + + /** + * 获取状态信息 + */ + getStatus() { + return { + platform: process.platform, + supported: process.platform === 'darwin', + name: 'Claude Code Automation' + }; + } +} + +module.exports = ClaudeAutomation; \ No newline at end of file diff --git a/src/automation/clipboard-automation.js b/src/automation/clipboard-automation.js new file mode 100644 index 0000000..136b52d --- /dev/null +++ b/src/automation/clipboard-automation.js @@ -0,0 +1,257 @@ +/** + * Clipboard Automation + * 通过剪贴板和键盘自动化来发送命令到Claude Code + */ + +const { spawn } = require('child_process'); +const Logger = require('../core/logger'); + +class ClipboardAutomation { + constructor() { + this.logger = new Logger('ClipboardAutomation'); + } + + /** + * 发送命令到Claude Code(通过剪贴板) + * @param {string} command - 要发送的命令 + * @returns {Promise} - 是否成功 + */ + async sendCommand(command) { + try { + // 第一步:将命令复制到剪贴板 + await this._copyToClipboard(command); + + // 第二步:激活Claude Code并粘贴 + const success = await this._activateAndPaste(); + + if (success) { + this.logger.info('Command sent successfully via clipboard automation'); + return true; + } else { + this.logger.warn('Failed to activate Claude Code, trying fallback'); + // 尝试通用方案 + return await this._sendToActiveWindow(command); + } + + } catch (error) { + this.logger.error('Clipboard automation failed:', error.message); + return false; + } + } + + /** + * 将文本复制到剪贴板 + */ + async _copyToClipboard(text) { + return new Promise((resolve, reject) => { + if (process.platform === 'darwin') { + // macOS + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(text); + pbcopy.stdin.end(); + + pbcopy.on('close', (code) => { + if (code === 0) { + this.logger.debug('Text copied to clipboard'); + resolve(); + } else { + reject(new Error('Failed to copy to clipboard')); + } + }); + + pbcopy.on('error', reject); + } else if (process.platform === 'linux') { + // Linux (需要 xclip 或 xsel) + const xclip = spawn('xclip', ['-selection', 'clipboard']); + xclip.stdin.write(text); + xclip.stdin.end(); + + xclip.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + // 尝试 xsel + const xsel = spawn('xsel', ['--clipboard', '--input']); + xsel.stdin.write(text); + xsel.stdin.end(); + + xsel.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error('Failed to copy to clipboard (xclip/xsel not available)')); + } + }); + } + }); + } else { + reject(new Error('Clipboard automation not supported on this platform')); + } + }); + } + + /** + * 激活Claude Code并粘贴命令 + */ + async _activateAndPaste() { + if (process.platform !== 'darwin') { + return false; + } + + return new Promise((resolve) => { + const script = ` + tell application "System Events" + -- 尝试找到 Claude Code 相关的应用 + set targetApps to {"Claude Code", "Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code", "Cursor"} + set foundApp to null + set appName to "" + + repeat with currentApp in targetApps + try + if application process currentApp exists then + set foundApp to application process currentApp + set appName to currentApp + exit repeat + end if + end try + end repeat + + if foundApp is not null then + -- 激活应用 + set frontmost of foundApp to true + delay 0.5 + + -- 尝试找到输入框并点击 + try + -- 对于一些应用,可能需要点击特定的输入区域 + if appName is "Claude Code" then + -- Claude Code 特定的处理 + key code 125 -- 向下箭头,确保光标在输入框 + delay 0.2 + end if + + -- 粘贴内容 + keystroke "v" using command down + delay 0.3 + + -- 发送命令(回车) + keystroke return + + return "success" + on error errorMessage + return "paste_failed: " & errorMessage + end try + else + return "no_app_found" + end if + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + let error = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.stderr.on('data', (data) => { + error += data.toString(); + }); + + osascript.on('close', (code) => { + if (code === 0 && output === 'success') { + this.logger.debug('Successfully activated app and pasted command'); + resolve(true); + } else { + this.logger.warn('AppleScript execution result:', { code, output, error }); + resolve(false); + } + }); + + osascript.on('error', (err) => { + this.logger.error('AppleScript execution error:', err.message); + resolve(false); + }); + }); + } + + /** + * 发送到当前活动窗口(通用方案) + */ + async _sendToActiveWindow(command) { + if (process.platform !== 'darwin') { + return false; + } + + return new Promise((resolve) => { + const script = ` + tell application "System Events" + -- 获取当前活动应用 + set activeApp to name of first application process whose frontmost is true + + -- 粘贴命令到当前活动窗口 + keystroke "v" using command down + delay 0.3 + keystroke return + + return "sent_to_" & activeApp + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.on('close', (code) => { + if (code === 0) { + this.logger.debug('Command sent to active window:', output); + resolve(true); + } else { + resolve(false); + } + }); + + osascript.on('error', () => { + resolve(false); + }); + }); + } + + /** + * 检查是否支持剪贴板自动化 + */ + isSupported() { + return process.platform === 'darwin' || process.platform === 'linux'; + } + + /** + * 获取当前剪贴板内容(用于测试) + */ + async getClipboardContent() { + if (process.platform === 'darwin') { + return new Promise((resolve, reject) => { + const pbpaste = spawn('pbpaste'); + let content = ''; + + pbpaste.stdout.on('data', (data) => { + content += data.toString(); + }); + + pbpaste.on('close', (code) => { + if (code === 0) { + resolve(content); + } else { + reject(new Error('Failed to read clipboard')); + } + }); + }); + } + return null; + } +} + +module.exports = ClipboardAutomation; \ No newline at end of file diff --git a/src/automation/simple-automation.js b/src/automation/simple-automation.js new file mode 100644 index 0000000..5618dee --- /dev/null +++ b/src/automation/simple-automation.js @@ -0,0 +1,297 @@ +/** + * 简单自动化方案 + * 使用更简单的方式来处理邮件回复命令 + */ + +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const Logger = require('../core/logger'); + +class SimpleAutomation { + constructor() { + this.logger = new Logger('SimpleAutomation'); + this.commandFile = path.join(__dirname, '../data/current_command.txt'); + this.notificationSent = false; + } + + /** + * 发送命令 - 使用多种简单方式 + * @param {string} command - 要发送的命令 + * @param {string} sessionId - 会话ID + * @returns {Promise} - 是否成功 + */ + async sendCommand(command, sessionId = '') { + try { + // 方法1: 保存到文件,用户可以手动复制 + await this._saveCommandToFile(command, sessionId); + + // 方法2: 复制到剪贴板 + const clipboardSuccess = await this._copyToClipboard(command); + + // 方法3: 发送富通知(包含命令内容) + const notificationSuccess = await this._sendRichNotification(command, sessionId); + + // 方法4: 尝试简单的自动化(不依赖复杂权限) + const autoSuccess = await this._trySimpleAutomation(command); + + if (clipboardSuccess || notificationSuccess || autoSuccess) { + this.logger.info('Command sent successfully via simple automation'); + return true; + } else { + this.logger.warn('All simple automation methods failed'); + return false; + } + + } catch (error) { + this.logger.error('Simple automation failed:', error.message); + return false; + } + } + + /** + * 保存命令到文件 + */ + async _saveCommandToFile(command, sessionId) { + try { + const timestamp = new Date().toLocaleString('zh-CN'); + const content = `# TaskPing 邮件回复命令 +# 时间: ${timestamp} +# 会话ID: ${sessionId} +# +# 请复制下面的命令到 Claude Code 中执行: + +${command} + +# =============================== +# 执行完成后可以删除此文件 +`; + + fs.writeFileSync(this.commandFile, content, 'utf8'); + this.logger.info(`Command saved to file: ${this.commandFile}`); + return true; + } catch (error) { + this.logger.error('Failed to save command to file:', error.message); + return false; + } + } + + /** + * 复制到剪贴板 + */ + async _copyToClipboard(command) { + try { + if (process.platform === 'darwin') { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(command); + pbcopy.stdin.end(); + + return new Promise((resolve) => { + pbcopy.on('close', (code) => { + if (code === 0) { + this.logger.info('Command copied to clipboard'); + resolve(true); + } else { + resolve(false); + } + }); + + pbcopy.on('error', () => resolve(false)); + }); + } + return false; + } catch (error) { + this.logger.error('Failed to copy to clipboard:', error.message); + return false; + } + } + + /** + * 发送富通知 + */ + async _sendRichNotification(command, sessionId) { + try { + if (process.platform === 'darwin') { + const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command; + + // 创建详细的通知 + const script = ` + set commandText to "${command.replace(/"/g, '\\"').replace(/\n/g, '\\n')}" + + display notification "命令已复制到剪贴板,请粘贴到 Claude Code 中" with title "TaskPing - 新邮件命令" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "default" + + -- 同时显示对话框(可选,用户可以取消) + try + set userChoice to display dialog "收到新的邮件命令:" & return & return & commandText buttons {"打开命令文件", "取消", "已粘贴"} default button "已粘贴" with title "TaskPing 邮件中继" giving up after 10 + + if button returned of userChoice is "打开命令文件" then + do shell script "open -t '${this.commandFile}'" + end if + end try + `; + + const osascript = spawn('osascript', ['-e', script]); + + return new Promise((resolve) => { + osascript.on('close', (code) => { + if (code === 0) { + this.logger.info('Rich notification sent successfully'); + resolve(true); + } else { + this.logger.warn('Rich notification failed, trying simple notification'); + this._sendSimpleNotification(command); + resolve(true); // 即使失败也算成功,因为有备选方案 + } + }); + + osascript.on('error', () => { + this._sendSimpleNotification(command); + resolve(true); + }); + }); + } + return false; + } catch (error) { + this.logger.error('Failed to send rich notification:', error.message); + return false; + } + } + + /** + * 发送简单通知 + */ + async _sendSimpleNotification(command) { + try { + const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command; + const script = `display notification "命令: ${shortCommand.replace(/"/g, '\\"')}" with title "TaskPing - 邮件命令" sound name "default"`; + + const osascript = spawn('osascript', ['-e', script]); + osascript.on('close', () => { + this.logger.info('Simple notification sent'); + }); + } catch (error) { + this.logger.warn('Simple notification also failed:', error.message); + } + } + + /** + * 尝试简单自动化(无需复杂权限) + */ + async _trySimpleAutomation(command) { + try { + if (process.platform === 'darwin') { + // 只尝试最基本的操作,不强制要求权限 + const script = ` + try + tell application "System Events" + -- 尝试获取前台应用 + set frontApp to name of first application process whose frontmost is true + + -- 如果是终端或代码编辑器,尝试粘贴 + if frontApp contains "Terminal" or frontApp contains "iTerm" or frontApp contains "Code" or frontApp contains "Claude" then + keystroke "v" using command down + delay 0.2 + keystroke return + return "success" + else + return "not_target_app" + end if + end tell + on error + return "no_permission" + end try + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + return new Promise((resolve) => { + osascript.on('close', (code) => { + if (code === 0 && output === 'success') { + this.logger.info('Simple automation succeeded'); + resolve(true); + } else { + this.logger.debug(`Simple automation result: ${output}`); + resolve(false); + } + }); + + osascript.on('error', () => resolve(false)); + }); + } + return false; + } catch (error) { + this.logger.error('Simple automation error:', error.message); + return false; + } + } + + /** + * 打开命令文件 + */ + async openCommandFile() { + try { + if (fs.existsSync(this.commandFile)) { + if (process.platform === 'darwin') { + spawn('open', ['-t', this.commandFile]); + this.logger.info('Command file opened'); + return true; + } + } + return false; + } catch (error) { + this.logger.error('Failed to open command file:', error.message); + return false; + } + } + + /** + * 清理命令文件 + */ + cleanupCommandFile() { + try { + if (fs.existsSync(this.commandFile)) { + fs.unlinkSync(this.commandFile); + this.logger.info('Command file cleaned up'); + } + } catch (error) { + this.logger.error('Failed to cleanup command file:', error.message); + } + } + + /** + * 获取状态 + */ + getStatus() { + return { + supported: process.platform === 'darwin', + commandFile: this.commandFile, + commandFileExists: fs.existsSync(this.commandFile), + lastCommand: this._getLastCommand() + }; + } + + _getLastCommand() { + try { + if (fs.existsSync(this.commandFile)) { + const content = fs.readFileSync(this.commandFile, 'utf8'); + const lines = content.split('\n'); + const commandLines = lines.filter(line => + !line.startsWith('#') && + !line.startsWith('=') && + line.trim().length > 0 + ); + return commandLines.join('\n').trim(); + } + } catch (error) { + this.logger.error('Failed to get last command:', error.message); + } + return null; + } +} + +module.exports = SimpleAutomation; \ No newline at end of file diff --git a/src/channels/base/channel.js b/src/channels/base/channel.js new file mode 100644 index 0000000..938d9c1 --- /dev/null +++ b/src/channels/base/channel.js @@ -0,0 +1,123 @@ +/** + * Base Notification Channel + * Abstract base class for all notification channels + */ + +const Logger = require('../../core/logger'); + +class NotificationChannel { + constructor(name, config = {}) { + this.name = name; + this.config = config; + this.logger = new Logger(`Channel:${name}`); + this.enabled = config.enabled !== false; + } + + /** + * Send a notification + * @param {Object} notification - Notification object + * @param {string} notification.type - Type: 'completed' | 'waiting' + * @param {string} notification.title - Notification title + * @param {string} notification.message - Notification message + * @param {string} notification.project - Project name + * @param {Object} notification.metadata - Additional metadata + * @returns {Promise} Success status + */ + async send(notification) { + if (!this.enabled) { + this.logger.debug('Channel disabled, skipping notification'); + return false; + } + + this.logger.debug('Sending notification:', notification.type); + + try { + const result = await this._sendImpl(notification); + if (result) { + this.logger.info(`Notification sent successfully: ${notification.type}`); + } else { + this.logger.warn(`Failed to send notification: ${notification.type}`); + } + return result; + } catch (error) { + this.logger.error('Error sending notification:', error.message); + return false; + } + } + + /** + * Test the channel configuration + * @returns {Promise} Test success status + */ + async test() { + this.logger.debug('Testing channel...'); + + const testNotification = { + type: 'completed', + title: 'TaskPing Test', + message: `Test notification from ${this.name} channel`, + project: 'test-project', + metadata: { test: true } + }; + + return await this.send(testNotification); + } + + /** + * Check if the channel supports command relay + * @returns {boolean} Support status + */ + supportsRelay() { + return false; + } + + /** + * Handle incoming command from this channel (if supported) + * @param {string} command - Command to execute + * @param {Object} context - Command context + * @returns {Promise} Success status + */ + async handleCommand(command, context = {}) { + if (!this.supportsRelay()) { + this.logger.warn('Channel does not support command relay'); + return false; + } + + this.logger.info('Received command:', command); + // Implemented by subclasses + return false; + } + + /** + * Implementation-specific send logic + * Must be implemented by subclasses + * @param {Object} notification - Notification object + * @returns {Promise} Success status + */ + async _sendImpl(notification) { + throw new Error('_sendImpl must be implemented by subclass'); + } + + /** + * Validate channel configuration + * @returns {boolean} Validation status + */ + validateConfig() { + return true; + } + + /** + * Get channel status + * @returns {Object} Status information + */ + getStatus() { + return { + name: this.name, + enabled: this.enabled, + configured: this.validateConfig(), + supportsRelay: this.supportsRelay() + }; + } +} + +module.exports = NotificationChannel; \ No newline at end of file diff --git a/src/channels/email/smtp.js b/src/channels/email/smtp.js new file mode 100644 index 0000000..2b0b8f9 --- /dev/null +++ b/src/channels/email/smtp.js @@ -0,0 +1,346 @@ +/** + * Email Notification Channel + * Sends notifications via email with reply support + */ + +const NotificationChannel = require('../base/channel'); +const nodemailer = require('nodemailer'); +const { v4: uuidv4 } = require('uuid'); +const path = require('path'); +const fs = require('fs'); + +class EmailChannel extends NotificationChannel { + constructor(config = {}) { + super('email', config); + this.transporter = null; + this.sessionsDir = path.join(__dirname, '../../data/sessions'); + this.templatesDir = path.join(__dirname, '../../assets/email-templates'); + + this._ensureDirectories(); + this._initializeTransporter(); + } + + _ensureDirectories() { + if (!fs.existsSync(this.sessionsDir)) { + fs.mkdirSync(this.sessionsDir, { recursive: true }); + } + if (!fs.existsSync(this.templatesDir)) { + fs.mkdirSync(this.templatesDir, { recursive: true }); + } + } + + _initializeTransporter() { + if (!this.config.smtp) { + this.logger.warn('SMTP configuration not found'); + return; + } + + try { + this.transporter = nodemailer.createTransport({ + host: this.config.smtp.host, + port: this.config.smtp.port, + secure: this.config.smtp.secure || false, + auth: { + user: this.config.smtp.auth.user, + pass: this.config.smtp.auth.pass + }, + // 添加超时设置 + connectionTimeout: 10000, + greetingTimeout: 10000, + socketTimeout: 10000 + }); + + this.logger.debug('Email transporter initialized'); + } catch (error) { + this.logger.error('Failed to initialize email transporter:', error.message); + } + } + + async _sendImpl(notification) { + if (!this.transporter) { + throw new Error('Email transporter not initialized'); + } + + if (!this.config.to) { + throw new Error('Email recipient not configured'); + } + + // 生成会话ID + const sessionId = uuidv4(); + + // 创建会话记录 + await this._createSession(sessionId, notification); + + // 生成邮件内容 + const emailContent = this._generateEmailContent(notification, sessionId); + + const mailOptions = { + from: this.config.from || this.config.smtp.auth.user, + to: this.config.to, + subject: emailContent.subject, + html: emailContent.html, + text: emailContent.text, + // 添加自定义头部用于回复识别 + headers: { + 'X-TaskPing-Session-ID': sessionId, + 'X-TaskPing-Type': notification.type + } + }; + + try { + const result = await this.transporter.sendMail(mailOptions); + this.logger.info(`Email sent successfully to ${this.config.to}, Session: ${sessionId}`); + return true; + } catch (error) { + this.logger.error('Failed to send email:', error.message); + // 清理失败的会话 + await this._removeSession(sessionId); + return false; + } + } + + async _createSession(sessionId, notification) { + const session = { + id: sessionId, + created: new Date().toISOString(), + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24小时后过期 + notification: { + type: notification.type, + project: notification.project, + message: notification.message + }, + status: 'waiting', + commandCount: 0, + maxCommands: 10 + }; + + const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`); + fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2)); + + this.logger.debug(`Session created: ${sessionId}`); + } + + async _removeSession(sessionId) { + const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`); + if (fs.existsSync(sessionFile)) { + fs.unlinkSync(sessionFile); + this.logger.debug(`Session removed: ${sessionId}`); + } + } + + _generateEmailContent(notification, sessionId) { + const template = this._getTemplate(notification.type); + const timestamp = new Date().toLocaleString('zh-CN'); + + // 模板变量替换 + const variables = { + project: notification.project, + message: notification.message, + timestamp: timestamp, + sessionId: sessionId, + type: notification.type === 'completed' ? '任务完成' : '等待输入' + }; + + let subject = template.subject; + let html = template.html; + let text = template.text; + + // 替换模板变量 + Object.keys(variables).forEach(key => { + const placeholder = new RegExp(`{{${key}}}`, 'g'); + subject = subject.replace(placeholder, variables[key]); + html = html.replace(placeholder, variables[key]); + text = text.replace(placeholder, variables[key]); + }); + + return { subject, html, text }; + } + + _getTemplate(type) { + // 默认模板 + const templates = { + completed: { + subject: '[TaskPing] Claude Code 任务完成 - {{project}}', + html: ` +
+
+

+ 🎉 Claude Code 任务完成 +

+ +
+

+ 项目: {{project}}
+ 时间: {{timestamp}}
+ 状态: {{type}} +

+
+ +
+

{{message}}

+
+ +
+

💡 如何继续对话

+

+ 要继续与 Claude Code 对话,请直接回复此邮件,在邮件正文中输入您的指令。 +

+
+ 示例回复:
+ • "请继续优化代码"
+ • "生成单元测试"
+ • "解释这个函数的作用" +
+
+ +
+

会话ID: {{sessionId}}

+

🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期

+

📧 这是一封来自 TaskPing 的自动邮件

+
+
+
+ `, + text: ` +[TaskPing] Claude Code 任务完成 - {{project}} + +项目: {{project}} +时间: {{timestamp}} +状态: {{type}} + +消息: {{message}} + +如何继续对话: +要继续与 Claude Code 对话,请直接回复此邮件,在邮件正文中输入您的指令。 + +示例回复: +• "请继续优化代码" +• "生成单元测试" +• "解释这个函数的作用" + +会话ID: {{sessionId}} +安全提示: 请勿转发此邮件,会话将在24小时后自动过期 + ` + }, + waiting: { + subject: '[TaskPing] Claude Code 等待输入 - {{project}}', + html: ` +
+
+

+ ⏳ Claude Code 等待您的指导 +

+ +
+

+ 项目: {{project}}
+ 时间: {{timestamp}}
+ 状态: {{type}} +

+
+ +
+

{{message}}

+
+ +
+

💬 请提供指导

+

+ Claude 需要您的进一步指导。请回复此邮件告诉 Claude 下一步应该做什么。 +

+
+ +
+

会话ID: {{sessionId}}

+

🔒 安全提示: 请勿转发此邮件,会话将在24小时后自动过期

+

📧 这是一封来自 TaskPing 的自动邮件

+
+
+
+ `, + text: ` +[TaskPing] Claude Code 等待输入 - {{project}} + +项目: {{project}} +时间: {{timestamp}} +状态: {{type}} + +消息: {{message}} + +Claude 需要您的进一步指导。请回复此邮件告诉 Claude 下一步应该做什么。 + +会话ID: {{sessionId}} +安全提示: 请勿转发此邮件,会话将在24小时后自动过期 + ` + } + }; + + return templates[type] || templates.completed; + } + + validateConfig() { + if (!this.config.smtp) { + return { valid: false, error: 'SMTP configuration required' }; + } + + if (!this.config.smtp.host) { + return { valid: false, error: 'SMTP host required' }; + } + + if (!this.config.smtp.auth || !this.config.smtp.auth.user || !this.config.smtp.auth.pass) { + return { valid: false, error: 'SMTP authentication required' }; + } + + if (!this.config.to) { + return { valid: false, error: 'Recipient email required' }; + } + + return { valid: true }; + } + + async test() { + try { + if (!this.transporter) { + throw new Error('Email transporter not initialized'); + } + + // 验证 SMTP 连接 + await this.transporter.verify(); + + // 发送测试邮件 + const testNotification = { + type: 'completed', + title: 'TaskPing 测试', + message: '这是一封测试邮件,用于验证邮件通知功能是否正常工作。', + project: 'TaskPing-Test', + metadata: { + test: true, + timestamp: new Date().toISOString() + } + }; + + const result = await this._sendImpl(testNotification); + return result; + } catch (error) { + this.logger.error('Email test failed:', error.message); + return false; + } + } + + getStatus() { + const baseStatus = super.getStatus(); + return { + ...baseStatus, + configured: this.validateConfig().valid, + supportsRelay: true, + smtp: { + host: this.config.smtp?.host || 'not configured', + port: this.config.smtp?.port || 'not configured', + secure: this.config.smtp?.secure || false + }, + recipient: this.config.to || 'not configured' + }; + } +} + +module.exports = EmailChannel; \ No newline at end of file diff --git a/src/channels/local/desktop.js b/src/channels/local/desktop.js new file mode 100644 index 0000000..a8658dd --- /dev/null +++ b/src/channels/local/desktop.js @@ -0,0 +1,160 @@ +/** + * Desktop Notification Channel + * Sends notifications to the local desktop + */ + +const NotificationChannel = require('../base/channel'); +const { execSync, spawn } = require('child_process'); +const path = require('path'); + +class DesktopChannel extends NotificationChannel { + constructor(config = {}) { + super('desktop', config); + this.platform = process.platform; + this.soundsDir = path.join(__dirname, '../../assets/sounds'); + } + + async _sendImpl(notification) { + const { title, message } = notification; + const sound = this._getSoundForType(notification.type); + + switch (this.platform) { + case 'darwin': + return this._sendMacOS(title, message, sound); + case 'linux': + return this._sendLinux(title, message, sound); + case 'win32': + return this._sendWindows(title, message, sound); + default: + this.logger.warn(`Platform ${this.platform} not supported`); + return false; + } + } + + _getSoundForType(type) { + const soundMap = { + completed: this.config.completedSound || 'Glass', + waiting: this.config.waitingSound || 'Tink' + }; + return soundMap[type] || 'Glass'; + } + + _sendMacOS(title, message, sound) { + try { + // Try terminal-notifier first + try { + const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "taskping"`; + execSync(cmd, { timeout: 3000 }); + return true; + } catch (e) { + // Fallback to osascript + const script = `display notification "${message}" with title "${title}"`; + execSync(`osascript -e '${script}'`, { timeout: 3000 }); + + // Play sound separately + this._playSound(sound); + return true; + } + } catch (error) { + this.logger.error('macOS notification failed:', error.message); + return false; + } + } + + _sendLinux(title, message, sound) { + try { + execSync(`notify-send "${title}" "${message}" -t 10000`, { timeout: 3000 }); + this._playSound(sound); + return true; + } catch (error) { + this.logger.error('Linux notification failed:', error.message); + return false; + } + } + + _sendWindows(title, message, sound) { + try { + const script = ` + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null + $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02) + $xml = [xml] $template.GetXml() + $xml.toast.visual.binding.text[0].AppendChild($xml.CreateTextNode("${title}")) > $null + $xml.toast.visual.binding.text[1].AppendChild($xml.CreateTextNode("${message}")) > $null + $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) + [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("TaskPing").Show($toast) + `; + + execSync(`powershell -Command "${script}"`, { timeout: 5000 }); + this._playSound(sound); + return true; + } catch (error) { + this.logger.error('Windows notification failed:', error.message); + return false; + } + } + + _playSound(soundName) { + if (!soundName || soundName === 'default') return; + + try { + if (this.platform === 'darwin') { + const soundPath = `/System/Library/Sounds/${soundName}.aiff`; + const audioProcess = spawn('afplay', [soundPath], { + detached: true, + stdio: 'ignore' + }); + audioProcess.unref(); + } else if (this.platform === 'linux') { + const soundPath = `/usr/share/sounds/freedesktop/stereo/${soundName.toLowerCase()}.oga`; + const audioProcess = spawn('paplay', [soundPath], { + detached: true, + stdio: 'ignore' + }); + audioProcess.unref(); + } else if (this.platform === 'win32') { + const audioProcess = spawn('powershell', ['-c', `[console]::beep(800,300)`], { + detached: true, + stdio: 'ignore' + }); + audioProcess.unref(); + } + } catch (error) { + this.logger.debug('Sound playback failed:', error.message); + } + } + + validateConfig() { + // Desktop notifications don't require configuration + return true; + } + + getAvailableSounds() { + const sounds = { + 'System Sounds': ['Glass', 'Tink', 'Ping', 'Pop', 'Basso', 'Blow', 'Bottle', + 'Frog', 'Funk', 'Hero', 'Morse', 'Purr', 'Sosumi', 'Submarine'], + 'Alert Sounds': ['Beep', 'Boop', 'Sosumi', 'Tink', 'Glass'], + 'Nature Sounds': ['Frog', 'Submarine'], + 'Musical Sounds': ['Funk', 'Hero', 'Morse', 'Sosumi'] + }; + + // Add custom sounds from assets directory + try { + const fs = require('fs'); + if (fs.existsSync(this.soundsDir)) { + const customSounds = fs.readdirSync(this.soundsDir) + .filter(file => /\.(wav|mp3|m4a|aiff|ogg)$/i.test(file)) + .map(file => path.basename(file, path.extname(file))); + + if (customSounds.length > 0) { + sounds['Custom Sounds'] = customSounds; + } + } + } catch (error) { + this.logger.debug('Failed to load custom sounds:', error.message); + } + + return sounds; + } +} + +module.exports = DesktopChannel; \ No newline at end of file diff --git a/src/config-manager.js b/src/config-manager.js new file mode 100644 index 0000000..d6e0470 --- /dev/null +++ b/src/config-manager.js @@ -0,0 +1,175 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); +const Logger = require('./core/logger'); +const logger = new Logger('ConfigManager'); + +class ConfigManager { + constructor() { + this.configPath = path.join(__dirname, '../config/channels.json'); + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + } + + async loadConfig() { + try { + const data = fs.readFileSync(this.configPath, 'utf8'); + return JSON.parse(data); + } catch (error) { + logger.error('Failed to load config:', error); + throw error; + } + } + + async saveConfig(config) { + try { + fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); + logger.info('Configuration saved successfully'); + } catch (error) { + logger.error('Failed to save config:', error); + throw error; + } + } + + async question(prompt) { + return new Promise((resolve) => { + this.rl.question(prompt, resolve); + }); + } + + async configureEmail() { + console.log('\n📧 Email Configuration Setup\n'); + + const config = await this.loadConfig(); + + console.log('Please enter your email configuration:'); + console.log('(Press Enter to keep current value)\n'); + + // SMTP Configuration + console.log('--- SMTP Settings (for sending emails) ---'); + const smtpHost = await this.question(`SMTP Host [${config.email.config.smtp.host || 'smtp.gmail.com'}]: `); + config.email.config.smtp.host = smtpHost || config.email.config.smtp.host || 'smtp.gmail.com'; + + const smtpPort = await this.question(`SMTP Port [${config.email.config.smtp.port || 587}]: `); + config.email.config.smtp.port = parseInt(smtpPort) || config.email.config.smtp.port || 587; + + const smtpUser = await this.question(`Email Address [${config.email.config.smtp.auth.user || ''}]: `); + config.email.config.smtp.auth.user = smtpUser || config.email.config.smtp.auth.user; + + const smtpPass = await this.question(`App Password [${config.email.config.smtp.auth.pass ? '***' : ''}]: `); + if (smtpPass) { + config.email.config.smtp.auth.pass = smtpPass; + } + + // IMAP Configuration + console.log('\n--- IMAP Settings (for receiving emails) ---'); + const imapHost = await this.question(`IMAP Host [${config.email.config.imap.host || 'imap.gmail.com'}]: `); + config.email.config.imap.host = imapHost || config.email.config.imap.host || 'imap.gmail.com'; + + const imapPort = await this.question(`IMAP Port [${config.email.config.imap.port || 993}]: `); + config.email.config.imap.port = parseInt(imapPort) || config.email.config.imap.port || 993; + + // Use same credentials as SMTP by default + config.email.config.imap.auth.user = config.email.config.smtp.auth.user; + config.email.config.imap.auth.pass = config.email.config.smtp.auth.pass; + + // Email addresses + console.log('\n--- Email Addresses ---'); + const fromEmail = await this.question(`From Address [${config.email.config.from || `TaskPing <${config.email.config.smtp.auth.user}>`}]: `); + config.email.config.from = fromEmail || config.email.config.from || `TaskPing <${config.email.config.smtp.auth.user}>`; + + const toEmail = await this.question(`To Address [${config.email.config.to || config.email.config.smtp.auth.user}]: `); + config.email.config.to = toEmail || config.email.config.to || config.email.config.smtp.auth.user; + + // Enable email + const enable = await this.question('\nEnable email notifications? (y/n) [y]: '); + config.email.enabled = enable.toLowerCase() !== 'n'; + + await this.saveConfig(config); + console.log('\n✅ Email configuration completed!'); + + if (config.email.enabled) { + console.log('\n📌 Important: Make sure to use an App Password (not your regular password)'); + console.log(' Gmail: https://support.google.com/accounts/answer/185833'); + console.log(' Outlook: https://support.microsoft.com/en-us/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944'); + } + } + + async showCurrentConfig() { + const config = await this.loadConfig(); + console.log('\n📋 Current Configuration:\n'); + + for (const [channel, settings] of Object.entries(config)) { + console.log(`${channel}:`); + console.log(` Enabled: ${settings.enabled ? '✅' : '❌'}`); + + if (channel === 'email' && settings.config.smtp.auth.user) { + console.log(` Email: ${settings.config.smtp.auth.user}`); + console.log(` SMTP: ${settings.config.smtp.host}:${settings.config.smtp.port}`); + console.log(` IMAP: ${settings.config.imap.host}:${settings.config.imap.port}`); + } + console.log(); + } + } + + async toggleChannel(channelName) { + const config = await this.loadConfig(); + + if (!config[channelName]) { + console.log(`❌ Channel "${channelName}" not found`); + return; + } + + config[channelName].enabled = !config[channelName].enabled; + await this.saveConfig(config); + + console.log(`${channelName}: ${config[channelName].enabled ? '✅ Enabled' : '❌ Disabled'}`); + } + + async interactiveMenu() { + console.log('\n🛠️ TaskPing Configuration Manager\n'); + + while (true) { + console.log('\nChoose an option:'); + console.log('1. Configure Email'); + console.log('2. Show Current Configuration'); + console.log('3. Toggle Channel (enable/disable)'); + console.log('4. Exit'); + + const choice = await this.question('\nYour choice (1-4): '); + + switch (choice) { + case '1': + await this.configureEmail(); + break; + case '2': + await this.showCurrentConfig(); + break; + case '3': + const channel = await this.question('Channel name (desktop/email/discord/telegram/whatsapp/feishu): '); + await this.toggleChannel(channel); + break; + case '4': + console.log('\n👋 Goodbye!'); + this.rl.close(); + return; + default: + console.log('Invalid choice. Please try again.'); + } + } + } + + close() { + this.rl.close(); + } +} + +// Run as standalone script +if (require.main === module) { + const manager = new ConfigManager(); + manager.interactiveMenu().catch(console.error); +} + +module.exports = ConfigManager; \ No newline at end of file diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 0000000..fba513f --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,246 @@ +/** + * TaskPing Configuration Manager + * Handles loading, merging, and saving configurations + */ + +const fs = require('fs'); +const path = require('path'); +const Logger = require('./logger'); + +class ConfigManager { + constructor(configDir = null) { + this.logger = new Logger('Config'); + this.configDir = configDir || path.join(__dirname, '../../config'); + this.userConfigPath = path.join(this.configDir, 'user.json'); + this.defaultConfigPath = path.join(this.configDir, 'default.json'); + this.channelsConfigPath = path.join(this.configDir, 'channels.json'); + + this._config = null; + this._channels = null; + } + + getDefaultConfig() { + return { + language: 'zh-CN', + sound: { + completed: 'Glass', + waiting: 'Tink' + }, + enabled: true, + timeout: 5, + customMessages: { + completed: null, + waiting: null + }, + channels: { + desktop: { + enabled: true, + priority: 1 + } + }, + relay: { + enabled: false, + port: 3000, + auth: { + enabled: false, + token: null + } + } + }; + } + + getDefaultChannelsConfig() { + return { + desktop: { + type: 'local', + enabled: true, + config: {} + }, + email: { + type: 'email', + enabled: false, + config: { + smtp: { + host: '', + port: 587, + secure: false, + auth: { + user: '', + pass: '' + } + }, + from: '', + to: [] + } + }, + discord: { + type: 'chat', + enabled: false, + config: { + webhook: '', + username: 'TaskPing', + avatar: null + } + }, + telegram: { + type: 'chat', + enabled: false, + config: { + token: '', + chatId: '' + } + } + }; + } + + load() { + this.logger.debug('Loading configuration...'); + + // Load default config + let defaultConfig = this.getDefaultConfig(); + try { + if (fs.existsSync(this.defaultConfigPath)) { + const fileConfig = JSON.parse(fs.readFileSync(this.defaultConfigPath, 'utf8')); + defaultConfig = { ...defaultConfig, ...fileConfig }; + } + } catch (error) { + this.logger.warn('Failed to load default config:', error.message); + } + + // Load user config + let userConfig = {}; + try { + if (fs.existsSync(this.userConfigPath)) { + userConfig = JSON.parse(fs.readFileSync(this.userConfigPath, 'utf8')); + } + } catch (error) { + this.logger.warn('Failed to load user config:', error.message); + } + + // Merge configs + this._config = this._deepMerge(defaultConfig, userConfig); + + // Load channels config + this._channels = this.getDefaultChannelsConfig(); + try { + if (fs.existsSync(this.channelsConfigPath)) { + const fileChannels = JSON.parse(fs.readFileSync(this.channelsConfigPath, 'utf8')); + this._channels = { ...this._channels, ...fileChannels }; + } + } catch (error) { + this.logger.warn('Failed to load channels config:', error.message); + } + + this.logger.info('Configuration loaded successfully'); + return this._config; + } + + save() { + this.logger.debug('Saving user configuration...'); + + try { + // Ensure config directory exists + if (!fs.existsSync(this.configDir)) { + fs.mkdirSync(this.configDir, { recursive: true }); + } + + // Save user config + fs.writeFileSync(this.userConfigPath, JSON.stringify(this._config, null, 2)); + + // Save channels config + fs.writeFileSync(this.channelsConfigPath, JSON.stringify(this._channels, null, 2)); + + this.logger.info('Configuration saved successfully'); + return true; + } catch (error) { + this.logger.error('Failed to save configuration:', error.message); + return false; + } + } + + get(key, defaultValue = undefined) { + if (!this._config) { + this.load(); + } + + const keys = key.split('.'); + let value = this._config; + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k]; + } else { + return defaultValue; + } + } + + return value; + } + + set(key, value) { + if (!this._config) { + this.load(); + } + + const keys = key.split('.'); + let target = this._config; + + for (let i = 0; i < keys.length - 1; i++) { + const k = keys[i]; + if (!(k in target) || typeof target[k] !== 'object') { + target[k] = {}; + } + target = target[k]; + } + + target[keys[keys.length - 1]] = value; + return this; + } + + getChannel(channelName) { + if (!this._channels) { + this.load(); + } + return this._channels[channelName]; + } + + setChannel(channelName, config) { + if (!this._channels) { + this.load(); + } + this._channels[channelName] = config; + return this; + } + + getProjectName() { + try { + const { execSync } = require('child_process'); + // Try to get git repository name first + const gitName = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf8' }).trim(); + if (gitName) { + return path.basename(gitName); + } + } catch (e) { + // Not a git repository + } + + // Fall back to current directory name + return path.basename(process.cwd()); + } + + _deepMerge(target, source) { + const result = { ...target }; + + for (const key in source) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + result[key] = this._deepMerge(result[key] || {}, source[key]); + } else { + result[key] = source[key]; + } + } + + return result; + } +} + +module.exports = ConfigManager; \ No newline at end of file diff --git a/src/core/logger.js b/src/core/logger.js new file mode 100644 index 0000000..7836d1c --- /dev/null +++ b/src/core/logger.js @@ -0,0 +1,47 @@ +/** + * TaskPing Logger + * Centralized logging utility + */ + +class Logger { + constructor(namespace = 'TaskPing') { + this.namespace = namespace; + this.logLevel = process.env.TASKPING_LOG_LEVEL || 'info'; + } + + _log(level, message, ...args) { + const timestamp = new Date().toISOString(); + const prefix = `[${timestamp}] [${this.namespace}] [${level.toUpperCase()}]`; + + if (this._shouldLog(level)) { + console.log(prefix, message, ...args); + } + } + + _shouldLog(level) { + const levels = { debug: 0, info: 1, warn: 2, error: 3 }; + return levels[level] >= levels[this.logLevel]; + } + + debug(message, ...args) { + this._log('debug', message, ...args); + } + + info(message, ...args) { + this._log('info', message, ...args); + } + + warn(message, ...args) { + this._log('warn', message, ...args); + } + + error(message, ...args) { + this._log('error', message, ...args); + } + + child(namespace) { + return new Logger(`${this.namespace}:${namespace}`); + } +} + +module.exports = Logger; \ No newline at end of file diff --git a/src/core/notifier.js b/src/core/notifier.js new file mode 100644 index 0000000..c9e42c2 --- /dev/null +++ b/src/core/notifier.js @@ -0,0 +1,234 @@ +/** + * TaskPing Core Notifier + * Central notification orchestrator that manages multiple channels + */ + +const Logger = require('./logger'); +const ConfigManager = require('./config'); + +class Notifier { + constructor(configManager = null) { + this.logger = new Logger('Notifier'); + this.config = configManager || new ConfigManager(); + this.channels = new Map(); + this.i18n = null; + + this._loadI18n(); + } + + /** + * Register a notification channel + * @param {string} name - Channel name + * @param {NotificationChannel} channel - Channel instance + */ + registerChannel(name, channel) { + this.logger.debug(`Registering channel: ${name}`); + this.channels.set(name, channel); + } + + /** + * Initialize default channels + */ + async initializeChannels() { + this.logger.debug('Initializing channels...'); + + // Load desktop channel + const DesktopChannel = require('../channels/local/desktop'); + const desktopConfig = this.config.getChannel('desktop'); + if (desktopConfig && desktopConfig.enabled) { + const desktop = new DesktopChannel(desktopConfig.config || {}); + desktop.config.completedSound = this.config.get('sound.completed'); + desktop.config.waitingSound = this.config.get('sound.waiting'); + this.registerChannel('desktop', desktop); + } + + // Load email channel + const EmailChannel = require('../channels/email/smtp'); + const emailConfig = this.config.getChannel('email'); + if (emailConfig && emailConfig.enabled) { + const email = new EmailChannel(emailConfig.config || {}); + this.registerChannel('email', email); + } + + // TODO: Load other channels based on configuration + // Discord, Telegram, etc. + + this.logger.info(`Initialized ${this.channels.size} channels`); + } + + /** + * Send notification to all enabled channels + * @param {string} type - Notification type: 'completed' | 'waiting' + * @param {Object} metadata - Additional metadata + * @returns {Promise} Results from all channels + */ + async notify(type, metadata = {}) { + if (!this.config.get('enabled', true)) { + this.logger.debug('Notifications disabled'); + return { success: false, reason: 'disabled' }; + } + + const notification = this._buildNotification(type, metadata); + this.logger.info(`Sending ${type} notification for project: ${notification.project}`); + + const results = {}; + const promises = []; + + // Send to all channels in parallel + for (const [name, channel] of this.channels) { + if (channel.enabled) { + promises.push( + channel.send(notification) + .then(success => ({ name, success })) + .catch(error => ({ name, success: false, error: error.message })) + ); + } else { + results[name] = { success: false, reason: 'disabled' }; + } + } + + // Wait for all channels to complete + const channelResults = await Promise.all(promises); + channelResults.forEach(result => { + results[result.name] = result; + }); + + const successCount = Object.values(results).filter(r => r.success).length; + this.logger.info(`Notification sent to ${successCount}/${this.channels.size} channels`); + + return { + success: successCount > 0, + results, + notification + }; + } + + /** + * Build notification object from type and metadata + * @param {string} type - Notification type + * @param {Object} metadata - Additional metadata + * @returns {Object} Notification object + */ + _buildNotification(type, metadata = {}) { + const project = metadata.project || this.config.getProjectName(); + const lang = this.config.get('language', 'zh-CN'); + const content = this._getNotificationContent(type, lang); + + // Replace project placeholder + const message = content.message.replace('{project}', project); + + // Use custom message if configured + const customMessage = this.config.get(`customMessages.${type}`); + const finalMessage = customMessage ? customMessage.replace('{project}', project) : message; + + return { + type, + title: content.title, + message: finalMessage, + project, + metadata: { + timestamp: new Date().toISOString(), + language: lang, + ...metadata + } + }; + } + + /** + * Get notification content for type and language + * @param {string} type - Notification type + * @param {string} lang - Language code + * @returns {Object} Content object with title and message + */ + _getNotificationContent(type, lang) { + if (!this.i18n) { + this._loadI18n(); + } + + const langData = this.i18n[lang] || this.i18n['zh-CN']; + return langData[type] || langData.completed; + } + + /** + * Load internationalization data + */ + _loadI18n() { + this.i18n = { + 'zh-CN': { + completed: { + title: 'Claude Code - 任务完成', + message: '[{project}] 任务已完成,Claude正在等待下一步指令' + }, + waiting: { + title: 'Claude Code - 等待输入', + message: '[{project}] Claude需要您的进一步指导' + } + }, + 'en': { + completed: { + title: 'Claude Code - Task Completed', + message: '[{project}] Task completed, Claude is waiting for next instruction' + }, + waiting: { + title: 'Claude Code - Waiting for Input', + message: '[{project}] Claude needs your further guidance' + } + }, + 'ja': { + completed: { + title: 'Claude Code - タスク完了', + message: '[{project}] タスクが完了しました。Claudeが次の指示を待っています' + }, + waiting: { + title: 'Claude Code - 入力待ち', + message: '[{project}] Claudeにはあなたのさらなるガイダンスが必要です' + } + } + }; + } + + /** + * Test all channels + * @returns {Promise} Test results + */ + async test() { + this.logger.info('Testing all channels...'); + + const results = {}; + for (const [name, channel] of this.channels) { + try { + const success = await channel.test(); + results[name] = { success }; + this.logger.info(`Channel ${name}: ${success ? 'PASS' : 'FAIL'}`); + } catch (error) { + results[name] = { success: false, error: error.message }; + this.logger.error(`Channel ${name}: ERROR - ${error.message}`); + } + } + + return results; + } + + /** + * Get status of all channels + * @returns {Object} Status information + */ + getStatus() { + const channels = {}; + for (const [name, channel] of this.channels) { + channels[name] = channel.getStatus(); + } + + return { + enabled: this.config.get('enabled', true), + channels, + config: { + language: this.config.get('language'), + sound: this.config.get('sound'), + customMessages: this.config.get('customMessages') + } + }; + } +} + +module.exports = Notifier; \ No newline at end of file diff --git a/src/daemon/taskping-daemon.js b/src/daemon/taskping-daemon.js new file mode 100755 index 0000000..fce561d --- /dev/null +++ b/src/daemon/taskping-daemon.js @@ -0,0 +1,345 @@ +#!/usr/bin/env node + +/** + * TaskPing Daemon Service + * 后台守护进程,用于监听邮件和处理远程命令 + */ + +const fs = require('fs'); +const path = require('path'); +const { spawn, exec } = require('child_process'); +const Logger = require('../core/logger'); +const ConfigManager = require('../core/config'); + +class TaskPingDaemon { + constructor() { + this.logger = new Logger('Daemon'); + this.config = new ConfigManager(); + this.pidFile = path.join(__dirname, '../data/taskping.pid'); + this.logFile = path.join(__dirname, '../data/daemon.log'); + this.relayService = null; + this.isRunning = false; + + // 确保数据目录存在 + const dataDir = path.dirname(this.pidFile); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + } + + async start(detached = true) { + try { + // 检查是否已经运行 + if (this.isAlreadyRunning()) { + console.log('❌ TaskPing daemon 已经在运行中'); + console.log('💡 使用 "taskping daemon stop" 停止现有服务'); + process.exit(1); + } + + if (detached) { + // 以守护进程模式启动 + await this.startDetached(); + } else { + // 直接在当前进程运行 + await this.startForeground(); + } + } catch (error) { + this.logger.error('Failed to start daemon:', error); + throw error; + } + } + + async startDetached() { + console.log('🚀 启动 TaskPing 守护进程...'); + + // 创建子进程 + const child = spawn(process.execPath, [__filename, '--foreground'], { + detached: true, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + // 重定向日志 + const logStream = fs.createWriteStream(this.logFile, { flags: 'a' }); + child.stdout.pipe(logStream); + child.stderr.pipe(logStream); + + // 保存 PID + fs.writeFileSync(this.pidFile, child.pid.toString()); + + // 分离子进程 + child.unref(); + + console.log(`✅ TaskPing 守护进程已启动 (PID: ${child.pid})`); + console.log(`📝 日志文件: ${this.logFile}`); + console.log('💡 使用 "taskping daemon status" 查看状态'); + console.log('💡 使用 "taskping daemon stop" 停止服务'); + } + + async startForeground() { + console.log('🚀 TaskPing 守护进程启动中...'); + + this.isRunning = true; + process.title = 'taskping-daemon'; + + // 加载配置 + this.config.load(); + + // 初始化邮件中继服务 + const emailConfig = this.config.getChannel('email'); + if (!emailConfig || !emailConfig.enabled) { + this.logger.warn('Email channel not configured or disabled'); + return; + } + + const CommandRelayService = require('../relay/command-relay'); + this.relayService = new CommandRelayService(emailConfig.config); + + // 设置事件监听 + this.setupEventHandlers(); + + // 启动服务 + await this.relayService.start(); + this.logger.info('Email relay service started'); + + // 保持进程运行 + this.keepAlive(); + } + + setupEventHandlers() { + // 优雅关闭 + const gracefulShutdown = async (signal) => { + this.logger.info(`Received ${signal}, shutting down gracefully...`); + this.isRunning = false; + + if (this.relayService) { + await this.relayService.stop(); + } + + // 删除 PID 文件 + if (fs.existsSync(this.pidFile)) { + fs.unlinkSync(this.pidFile); + } + + process.exit(0); + }; + + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); + process.on('SIGHUP', () => { + this.logger.info('Received SIGHUP, reloading configuration...'); + this.config.load(); + }); + + // 中继服务事件 + if (this.relayService) { + this.relayService.on('started', () => { + this.logger.info('Command relay service started'); + }); + + this.relayService.on('commandQueued', (command) => { + this.logger.info(`Command queued: ${command.id}`); + }); + + this.relayService.on('commandExecuted', (command) => { + this.logger.info(`Command executed: ${command.id}`); + }); + + this.relayService.on('commandFailed', (command, error) => { + this.logger.error(`Command failed: ${command.id} - ${error.message}`); + }); + } + + // 未捕获异常处理 + process.on('uncaughtException', (error) => { + this.logger.error('Uncaught exception:', error); + process.exit(1); + }); + + process.on('unhandledRejection', (reason, promise) => { + this.logger.error('Unhandled rejection at:', promise, 'reason:', reason); + process.exit(1); + }); + } + + keepAlive() { + // 保持进程运行 + const heartbeat = setInterval(() => { + if (!this.isRunning) { + clearInterval(heartbeat); + return; + } + this.logger.debug('Heartbeat'); + }, 60000); // 每分钟输出一次心跳日志 + } + + async stop() { + if (!this.isAlreadyRunning()) { + console.log('❌ TaskPing daemon 没有运行'); + return; + } + + try { + const pid = this.getPid(); + console.log(`🛑 正在停止 TaskPing 守护进程 (PID: ${pid})...`); + + // 发送 SIGTERM 信号 + process.kill(pid, 'SIGTERM'); + + // 等待进程结束 + await this.waitForStop(pid); + + console.log('✅ TaskPing 守护进程已停止'); + } catch (error) { + console.error('❌ 停止守护进程失败:', error.message); + + // 强制删除 PID 文件 + if (fs.existsSync(this.pidFile)) { + fs.unlinkSync(this.pidFile); + console.log('🧹 已清理 PID 文件'); + } + } + } + + async restart() { + console.log('🔄 重启 TaskPing 守护进程...'); + await this.stop(); + await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒 + await this.start(); + } + + getStatus() { + const isRunning = this.isAlreadyRunning(); + const pid = isRunning ? this.getPid() : null; + + return { + running: isRunning, + pid: pid, + pidFile: this.pidFile, + logFile: this.logFile, + uptime: isRunning ? this.getUptime(pid) : null + }; + } + + showStatus() { + const status = this.getStatus(); + + console.log('📊 TaskPing 守护进程状态\n'); + + if (status.running) { + console.log('✅ 状态: 运行中'); + console.log(`🆔 PID: ${status.pid}`); + console.log(`⏱️ 运行时间: ${status.uptime || '未知'}`); + } else { + console.log('❌ 状态: 未运行'); + } + + console.log(`📝 日志文件: ${status.logFile}`); + console.log(`📁 PID 文件: ${status.pidFile}`); + + // 显示最近的日志 + if (fs.existsSync(status.logFile)) { + console.log('\n📋 最近日志:'); + try { + const logs = fs.readFileSync(status.logFile, 'utf8'); + const lines = logs.split('\n').filter(line => line.trim()).slice(-5); + lines.forEach(line => console.log(` ${line}`)); + } catch (error) { + console.log(' 无法读取日志文件'); + } + } + } + + isAlreadyRunning() { + if (!fs.existsSync(this.pidFile)) { + return false; + } + + try { + const pid = parseInt(fs.readFileSync(this.pidFile, 'utf8')); + // 检查进程是否仍在运行 + process.kill(pid, 0); + return true; + } catch (error) { + // 进程不存在,删除过时的 PID 文件 + fs.unlinkSync(this.pidFile); + return false; + } + } + + getPid() { + if (!fs.existsSync(this.pidFile)) { + return null; + } + return parseInt(fs.readFileSync(this.pidFile, 'utf8')); + } + + async waitForStop(pid, timeout = 10000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + process.kill(pid, 0); + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + // 进程已停止 + return; + } + } + + // 超时,强制结束 + throw new Error('进程停止超时,可能需要手动结束'); + } + + getUptime(pid) { + try { + // 在 macOS 和 Linux 上获取进程启动时间 + const { execSync } = require('child_process'); + const result = execSync(`ps -o lstart= -p ${pid}`, { encoding: 'utf8' }); + const startTime = new Date(result.trim()); + const uptime = Date.now() - startTime.getTime(); + + const hours = Math.floor(uptime / (1000 * 60 * 60)); + const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60)); + + return `${hours}h ${minutes}m`; + } catch (error) { + return '未知'; + } + } +} + +// 命令行接口 +if (require.main === module) { + const daemon = new TaskPingDaemon(); + const command = process.argv[2]; + + (async () => { + try { + switch (command) { + case 'start': + await daemon.start(true); + break; + case '--foreground': + await daemon.start(false); + break; + case 'stop': + await daemon.stop(); + break; + case 'restart': + await daemon.restart(); + break; + case 'status': + daemon.showStatus(); + break; + default: + console.log('Usage: taskping-daemon '); + process.exit(1); + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } + })(); +} + +module.exports = TaskPingDaemon; \ No newline at end of file diff --git a/src/data/relay-state.json b/src/data/relay-state.json new file mode 100644 index 0000000..6456524 --- /dev/null +++ b/src/data/relay-state.json @@ -0,0 +1,4 @@ +{ + "commandQueue": [], + "lastSaved": "2025-07-12T22:26:32.914Z" +} \ No newline at end of file diff --git a/src/data/sessions/01a2841c-a865-4465-b462-32ddcfb7111f.json b/src/data/sessions/01a2841c-a865-4465-b462-32ddcfb7111f.json new file mode 100644 index 0000000..4e1dc54 --- /dev/null +++ b/src/data/sessions/01a2841c-a865-4465-b462-32ddcfb7111f.json @@ -0,0 +1,13 @@ +{ + "id": "01a2841c-a865-4465-b462-32ddcfb7111f", + "created": "2025-07-18T10:19:27.379Z", + "expires": "2025-07-19T10:19:27.379Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/01eaaf76-ff26-4bec-a488-cdc8508d9779.json b/src/data/sessions/01eaaf76-ff26-4bec-a488-cdc8508d9779.json new file mode 100644 index 0000000..3a0774a --- /dev/null +++ b/src/data/sessions/01eaaf76-ff26-4bec-a488-cdc8508d9779.json @@ -0,0 +1,13 @@ +{ + "id": "01eaaf76-ff26-4bec-a488-cdc8508d9779", + "created": "2025-07-13T17:38:51.925Z", + "expires": "2025-07-14T17:38:51.925Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/0232a3b7-37ca-413a-be8d-77bf9e88f898.json b/src/data/sessions/0232a3b7-37ca-413a-be8d-77bf9e88f898.json new file mode 100644 index 0000000..f05ed9e --- /dev/null +++ b/src/data/sessions/0232a3b7-37ca-413a-be8d-77bf9e88f898.json @@ -0,0 +1,13 @@ +{ + "id": "0232a3b7-37ca-413a-be8d-77bf9e88f898", + "created": "2025-07-23T12:20:51.786Z", + "expires": "2025-07-24T12:20:51.786Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/0268b1c3-9be8-4c7f-b7f6-aec0baca70df.json b/src/data/sessions/0268b1c3-9be8-4c7f-b7f6-aec0baca70df.json new file mode 100644 index 0000000..88c6292 --- /dev/null +++ b/src/data/sessions/0268b1c3-9be8-4c7f-b7f6-aec0baca70df.json @@ -0,0 +1,13 @@ +{ + "id": "0268b1c3-9be8-4c7f-b7f6-aec0baca70df", + "created": "2025-07-23T22:15:19.656Z", + "expires": "2025-07-24T22:15:19.656Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/035c7c3d-8dcc-4f13-9e3b-cd5885e1a27e.json b/src/data/sessions/035c7c3d-8dcc-4f13-9e3b-cd5885e1a27e.json new file mode 100644 index 0000000..0c7e059 --- /dev/null +++ b/src/data/sessions/035c7c3d-8dcc-4f13-9e3b-cd5885e1a27e.json @@ -0,0 +1,13 @@ +{ + "id": "035c7c3d-8dcc-4f13-9e3b-cd5885e1a27e", + "created": "2025-07-18T12:36:48.562Z", + "expires": "2025-07-19T12:36:48.562Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/047e6c5b-b403-43dd-89c2-f3a154b386d0.json b/src/data/sessions/047e6c5b-b403-43dd-89c2-f3a154b386d0.json new file mode 100644 index 0000000..94278a8 --- /dev/null +++ b/src/data/sessions/047e6c5b-b403-43dd-89c2-f3a154b386d0.json @@ -0,0 +1,13 @@ +{ + "id": "047e6c5b-b403-43dd-89c2-f3a154b386d0", + "created": "2025-07-15T18:21:56.989Z", + "expires": "2025-07-16T18:21:56.989Z", + "notification": { + "type": "completed", + "project": "coverr_video", + "message": "[coverr_video] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/05ce5d20-928d-4cb6-b196-c298659dfae5.json b/src/data/sessions/05ce5d20-928d-4cb6-b196-c298659dfae5.json new file mode 100644 index 0000000..6e826ff --- /dev/null +++ b/src/data/sessions/05ce5d20-928d-4cb6-b196-c298659dfae5.json @@ -0,0 +1,13 @@ +{ + "id": "05ce5d20-928d-4cb6-b196-c298659dfae5", + "created": "2025-07-13T21:25:56.713Z", + "expires": "2025-07-14T21:25:56.713Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/07db2a97-0224-44e8-8b41-d352cfb5df37.json b/src/data/sessions/07db2a97-0224-44e8-8b41-d352cfb5df37.json new file mode 100644 index 0000000..04e24f3 --- /dev/null +++ b/src/data/sessions/07db2a97-0224-44e8-8b41-d352cfb5df37.json @@ -0,0 +1,13 @@ +{ + "id": "07db2a97-0224-44e8-8b41-d352cfb5df37", + "created": "2025-07-12T23:29:10.943Z", + "expires": "2025-07-13T23:29:10.943Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/0ded7ffb-02bf-4c99-bef3-3ba7bb4cc15b.json b/src/data/sessions/0ded7ffb-02bf-4c99-bef3-3ba7bb4cc15b.json new file mode 100644 index 0000000..5019a37 --- /dev/null +++ b/src/data/sessions/0ded7ffb-02bf-4c99-bef3-3ba7bb4cc15b.json @@ -0,0 +1,13 @@ +{ + "id": "0ded7ffb-02bf-4c99-bef3-3ba7bb4cc15b", + "created": "2025-07-12T22:49:17.851Z", + "expires": "2025-07-13T22:49:17.851Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/0e8ce4ea-d146-4721-8730-6d440341b12b.json b/src/data/sessions/0e8ce4ea-d146-4721-8730-6d440341b12b.json new file mode 100644 index 0000000..ddd891c --- /dev/null +++ b/src/data/sessions/0e8ce4ea-d146-4721-8730-6d440341b12b.json @@ -0,0 +1,13 @@ +{ + "id": "0e8ce4ea-d146-4721-8730-6d440341b12b", + "created": "2025-07-18T13:08:23.027Z", + "expires": "2025-07-19T13:08:23.027Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/11356ad8-74d8-46dd-9729-708c7da03245.json b/src/data/sessions/11356ad8-74d8-46dd-9729-708c7da03245.json new file mode 100644 index 0000000..8b471e3 --- /dev/null +++ b/src/data/sessions/11356ad8-74d8-46dd-9729-708c7da03245.json @@ -0,0 +1,13 @@ +{ + "id": "11356ad8-74d8-46dd-9729-708c7da03245", + "created": "2025-07-12T22:28:05.462Z", + "expires": "2025-07-13T22:28:05.462Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/116a5568-47e4-47ab-8e57-e94adf84fc87.json b/src/data/sessions/116a5568-47e4-47ab-8e57-e94adf84fc87.json new file mode 100644 index 0000000..0e6c97f --- /dev/null +++ b/src/data/sessions/116a5568-47e4-47ab-8e57-e94adf84fc87.json @@ -0,0 +1,13 @@ +{ + "id": "116a5568-47e4-47ab-8e57-e94adf84fc87", + "created": "2025-07-18T15:47:26.181Z", + "expires": "2025-07-19T15:47:26.181Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/11c13096-f65f-490d-82b6-92e813c92d5f.json b/src/data/sessions/11c13096-f65f-490d-82b6-92e813c92d5f.json new file mode 100644 index 0000000..304a1fe --- /dev/null +++ b/src/data/sessions/11c13096-f65f-490d-82b6-92e813c92d5f.json @@ -0,0 +1,13 @@ +{ + "id": "11c13096-f65f-490d-82b6-92e813c92d5f", + "created": "2025-07-18T13:47:59.853Z", + "expires": "2025-07-19T13:47:59.853Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/141614ee-d5fe-4a21-8384-c11d382d6b2a.json b/src/data/sessions/141614ee-d5fe-4a21-8384-c11d382d6b2a.json new file mode 100644 index 0000000..d77e5f8 --- /dev/null +++ b/src/data/sessions/141614ee-d5fe-4a21-8384-c11d382d6b2a.json @@ -0,0 +1,13 @@ +{ + "id": "141614ee-d5fe-4a21-8384-c11d382d6b2a", + "created": "2025-07-23T11:54:30.146Z", + "expires": "2025-07-24T11:54:30.146Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/17b08d21-4e1a-4991-93b9-b227d430227e.json b/src/data/sessions/17b08d21-4e1a-4991-93b9-b227d430227e.json new file mode 100644 index 0000000..95688a4 --- /dev/null +++ b/src/data/sessions/17b08d21-4e1a-4991-93b9-b227d430227e.json @@ -0,0 +1,13 @@ +{ + "id": "17b08d21-4e1a-4991-93b9-b227d430227e", + "created": "2025-07-23T11:49:03.622Z", + "expires": "2025-07-24T11:49:03.622Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/17cddd72-6761-4c87-a6f3-535dffd46847.json b/src/data/sessions/17cddd72-6761-4c87-a6f3-535dffd46847.json new file mode 100644 index 0000000..9e2eb26 --- /dev/null +++ b/src/data/sessions/17cddd72-6761-4c87-a6f3-535dffd46847.json @@ -0,0 +1,13 @@ +{ + "id": "17cddd72-6761-4c87-a6f3-535dffd46847", + "created": "2025-07-13T17:49:21.732Z", + "expires": "2025-07-14T17:49:21.732Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/19168db8-122b-40d4-930e-eaa7f442dc5e.json b/src/data/sessions/19168db8-122b-40d4-930e-eaa7f442dc5e.json new file mode 100644 index 0000000..1d3e92d --- /dev/null +++ b/src/data/sessions/19168db8-122b-40d4-930e-eaa7f442dc5e.json @@ -0,0 +1,13 @@ +{ + "id": "19168db8-122b-40d4-930e-eaa7f442dc5e", + "created": "2025-07-14T10:25:17.872Z", + "expires": "2025-07-15T10:25:17.872Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/197a28ea-945e-4cb1-87d5-6e240eb66c7f.json b/src/data/sessions/197a28ea-945e-4cb1-87d5-6e240eb66c7f.json new file mode 100644 index 0000000..c6618be --- /dev/null +++ b/src/data/sessions/197a28ea-945e-4cb1-87d5-6e240eb66c7f.json @@ -0,0 +1,13 @@ +{ + "id": "197a28ea-945e-4cb1-87d5-6e240eb66c7f", + "created": "2025-07-14T08:35:53.805Z", + "expires": "2025-07-15T08:35:53.805Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/19bb6891-1db6-4dbc-92a9-aa078dbaa49e.json b/src/data/sessions/19bb6891-1db6-4dbc-92a9-aa078dbaa49e.json new file mode 100644 index 0000000..182b7f0 --- /dev/null +++ b/src/data/sessions/19bb6891-1db6-4dbc-92a9-aa078dbaa49e.json @@ -0,0 +1,13 @@ +{ + "id": "19bb6891-1db6-4dbc-92a9-aa078dbaa49e", + "created": "2025-07-18T11:52:29.822Z", + "expires": "2025-07-19T11:52:29.822Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/19cc38c0-5be4-4656-8982-89f75c1cd3b1.json b/src/data/sessions/19cc38c0-5be4-4656-8982-89f75c1cd3b1.json new file mode 100644 index 0000000..f951e69 --- /dev/null +++ b/src/data/sessions/19cc38c0-5be4-4656-8982-89f75c1cd3b1.json @@ -0,0 +1,13 @@ +{ + "id": "19cc38c0-5be4-4656-8982-89f75c1cd3b1", + "created": "2025-07-24T01:06:31.410Z", + "expires": "2025-07-25T01:06:31.410Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/1b507e97-135d-4fe5-99d3-b654ad68557c.json b/src/data/sessions/1b507e97-135d-4fe5-99d3-b654ad68557c.json new file mode 100644 index 0000000..1c0fb77 --- /dev/null +++ b/src/data/sessions/1b507e97-135d-4fe5-99d3-b654ad68557c.json @@ -0,0 +1,13 @@ +{ + "id": "1b507e97-135d-4fe5-99d3-b654ad68557c", + "created": "2025-07-13T17:24:45.125Z", + "expires": "2025-07-14T17:24:45.125Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/1cabf8e7-3d60-41b1-877e-1c3633dbca38.json b/src/data/sessions/1cabf8e7-3d60-41b1-877e-1c3633dbca38.json new file mode 100644 index 0000000..632a405 --- /dev/null +++ b/src/data/sessions/1cabf8e7-3d60-41b1-877e-1c3633dbca38.json @@ -0,0 +1,13 @@ +{ + "id": "1cabf8e7-3d60-41b1-877e-1c3633dbca38", + "created": "2025-07-13T13:10:52.253Z", + "expires": "2025-07-14T13:10:52.253Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/205bb739-9a12-44ff-a7fa-3d564a5503d0.json b/src/data/sessions/205bb739-9a12-44ff-a7fa-3d564a5503d0.json new file mode 100644 index 0000000..7be0916 --- /dev/null +++ b/src/data/sessions/205bb739-9a12-44ff-a7fa-3d564a5503d0.json @@ -0,0 +1,13 @@ +{ + "id": "205bb739-9a12-44ff-a7fa-3d564a5503d0", + "created": "2025-07-14T09:44:29.946Z", + "expires": "2025-07-15T09:44:29.946Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/231c3c61-3d7a-4576-83c7-73b52267843f.json b/src/data/sessions/231c3c61-3d7a-4576-83c7-73b52267843f.json new file mode 100644 index 0000000..743a03d --- /dev/null +++ b/src/data/sessions/231c3c61-3d7a-4576-83c7-73b52267843f.json @@ -0,0 +1,13 @@ +{ + "id": "231c3c61-3d7a-4576-83c7-73b52267843f", + "created": "2025-07-18T12:43:18.785Z", + "expires": "2025-07-19T12:43:18.785Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2391dc73-fca1-41b1-bdc2-5a354bce4c1b.json b/src/data/sessions/2391dc73-fca1-41b1-bdc2-5a354bce4c1b.json new file mode 100644 index 0000000..8c11b61 --- /dev/null +++ b/src/data/sessions/2391dc73-fca1-41b1-bdc2-5a354bce4c1b.json @@ -0,0 +1,13 @@ +{ + "id": "2391dc73-fca1-41b1-bdc2-5a354bce4c1b", + "created": "2025-07-12T22:44:10.848Z", + "expires": "2025-07-13T22:44:10.848Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/25713a2c-a3df-4e38-92bd-c4c74b97805c.json b/src/data/sessions/25713a2c-a3df-4e38-92bd-c4c74b97805c.json new file mode 100644 index 0000000..61aa8b2 --- /dev/null +++ b/src/data/sessions/25713a2c-a3df-4e38-92bd-c4c74b97805c.json @@ -0,0 +1,13 @@ +{ + "id": "25713a2c-a3df-4e38-92bd-c4c74b97805c", + "created": "2025-07-12T22:47:15.254Z", + "expires": "2025-07-13T22:47:15.254Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/257faefb-02c0-4016-8314-2abbecf6979b.json b/src/data/sessions/257faefb-02c0-4016-8314-2abbecf6979b.json new file mode 100644 index 0000000..d2f77a2 --- /dev/null +++ b/src/data/sessions/257faefb-02c0-4016-8314-2abbecf6979b.json @@ -0,0 +1,13 @@ +{ + "id": "257faefb-02c0-4016-8314-2abbecf6979b", + "created": "2025-07-13T13:15:51.472Z", + "expires": "2025-07-14T13:15:51.472Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/28efcda0-58a3-4bd0-8421-f7375fd513b6.json b/src/data/sessions/28efcda0-58a3-4bd0-8421-f7375fd513b6.json new file mode 100644 index 0000000..4650f28 --- /dev/null +++ b/src/data/sessions/28efcda0-58a3-4bd0-8421-f7375fd513b6.json @@ -0,0 +1,13 @@ +{ + "id": "28efcda0-58a3-4bd0-8421-f7375fd513b6", + "created": "2025-07-18T12:56:58.947Z", + "expires": "2025-07-19T12:56:58.947Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2addd4be-c563-42bc-a6c3-3479e81c80e1.json b/src/data/sessions/2addd4be-c563-42bc-a6c3-3479e81c80e1.json new file mode 100644 index 0000000..b18b5bb --- /dev/null +++ b/src/data/sessions/2addd4be-c563-42bc-a6c3-3479e81c80e1.json @@ -0,0 +1,13 @@ +{ + "id": "2addd4be-c563-42bc-a6c3-3479e81c80e1", + "created": "2025-07-18T21:00:15.789Z", + "expires": "2025-07-19T21:00:15.789Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2c21f2ba-3f72-40bd-8231-a5604e300057.json b/src/data/sessions/2c21f2ba-3f72-40bd-8231-a5604e300057.json new file mode 100644 index 0000000..c877542 --- /dev/null +++ b/src/data/sessions/2c21f2ba-3f72-40bd-8231-a5604e300057.json @@ -0,0 +1,13 @@ +{ + "id": "2c21f2ba-3f72-40bd-8231-a5604e300057", + "created": "2025-07-14T09:25:46.676Z", + "expires": "2025-07-15T09:25:46.676Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2cb3cb02-50e4-4e95-944f-c371f7863eaa.json b/src/data/sessions/2cb3cb02-50e4-4e95-944f-c371f7863eaa.json new file mode 100644 index 0000000..90bb06a --- /dev/null +++ b/src/data/sessions/2cb3cb02-50e4-4e95-944f-c371f7863eaa.json @@ -0,0 +1,13 @@ +{ + "id": "2cb3cb02-50e4-4e95-944f-c371f7863eaa", + "created": "2025-07-24T07:03:49.922Z", + "expires": "2025-07-25T07:03:49.922Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2d1c0454-ac79-48bf-858d-6a7b19d5619a.json b/src/data/sessions/2d1c0454-ac79-48bf-858d-6a7b19d5619a.json new file mode 100644 index 0000000..e21c598 --- /dev/null +++ b/src/data/sessions/2d1c0454-ac79-48bf-858d-6a7b19d5619a.json @@ -0,0 +1,13 @@ +{ + "id": "2d1c0454-ac79-48bf-858d-6a7b19d5619a", + "created": "2025-07-12T22:28:33.723Z", + "expires": "2025-07-13T22:28:33.723Z", + "notification": { + "type": "waiting", + "project": "TaskPing", + "message": "[TaskPing] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2d9168b2-0c0b-4a37-92c9-c34138ee9196.json b/src/data/sessions/2d9168b2-0c0b-4a37-92c9-c34138ee9196.json new file mode 100644 index 0000000..b429a8e --- /dev/null +++ b/src/data/sessions/2d9168b2-0c0b-4a37-92c9-c34138ee9196.json @@ -0,0 +1,13 @@ +{ + "id": "2d9168b2-0c0b-4a37-92c9-c34138ee9196", + "created": "2025-07-14T09:33:43.775Z", + "expires": "2025-07-15T09:33:43.775Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/2f2c7670-4b82-4dba-8595-f53fad37ba2f.json b/src/data/sessions/2f2c7670-4b82-4dba-8595-f53fad37ba2f.json new file mode 100644 index 0000000..69100b9 --- /dev/null +++ b/src/data/sessions/2f2c7670-4b82-4dba-8595-f53fad37ba2f.json @@ -0,0 +1,13 @@ +{ + "id": "2f2c7670-4b82-4dba-8595-f53fad37ba2f", + "created": "2025-07-18T11:22:22.962Z", + "expires": "2025-07-19T11:22:22.962Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/31fa7938-ae5f-4079-a8f5-af083bae0f03.json b/src/data/sessions/31fa7938-ae5f-4079-a8f5-af083bae0f03.json new file mode 100644 index 0000000..0bce058 --- /dev/null +++ b/src/data/sessions/31fa7938-ae5f-4079-a8f5-af083bae0f03.json @@ -0,0 +1,13 @@ +{ + "id": "31fa7938-ae5f-4079-a8f5-af083bae0f03", + "created": "2025-07-18T11:37:00.528Z", + "expires": "2025-07-19T11:37:00.528Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3556e5c1-ae72-4009-9062-6e94df22bc6c.json b/src/data/sessions/3556e5c1-ae72-4009-9062-6e94df22bc6c.json new file mode 100644 index 0000000..260a7a7 --- /dev/null +++ b/src/data/sessions/3556e5c1-ae72-4009-9062-6e94df22bc6c.json @@ -0,0 +1,13 @@ +{ + "id": "3556e5c1-ae72-4009-9062-6e94df22bc6c", + "created": "2025-07-18T21:06:25.623Z", + "expires": "2025-07-19T21:06:25.623Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/374cc8b9-a894-43f6-9bd6-7ee37c878800.json b/src/data/sessions/374cc8b9-a894-43f6-9bd6-7ee37c878800.json new file mode 100644 index 0000000..651fb8c --- /dev/null +++ b/src/data/sessions/374cc8b9-a894-43f6-9bd6-7ee37c878800.json @@ -0,0 +1,13 @@ +{ + "id": "374cc8b9-a894-43f6-9bd6-7ee37c878800", + "created": "2025-07-24T00:01:43.501Z", + "expires": "2025-07-25T00:01:43.501Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/394f897f-af05-4931-b193-6e423ddcd5ae.json b/src/data/sessions/394f897f-af05-4931-b193-6e423ddcd5ae.json new file mode 100644 index 0000000..8541516 --- /dev/null +++ b/src/data/sessions/394f897f-af05-4931-b193-6e423ddcd5ae.json @@ -0,0 +1,13 @@ +{ + "id": "394f897f-af05-4931-b193-6e423ddcd5ae", + "created": "2025-07-12T22:38:15.434Z", + "expires": "2025-07-13T22:38:15.434Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3afbafe7-a999-4055-8761-a182cf1c5ea0.json b/src/data/sessions/3afbafe7-a999-4055-8761-a182cf1c5ea0.json new file mode 100644 index 0000000..9cc91f3 --- /dev/null +++ b/src/data/sessions/3afbafe7-a999-4055-8761-a182cf1c5ea0.json @@ -0,0 +1,13 @@ +{ + "id": "3afbafe7-a999-4055-8761-a182cf1c5ea0", + "created": "2025-07-18T17:13:24.018Z", + "expires": "2025-07-19T17:13:24.018Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3b03b7e6-7ffe-41b5-83b2-3e54de65258f.json b/src/data/sessions/3b03b7e6-7ffe-41b5-83b2-3e54de65258f.json new file mode 100644 index 0000000..0770cd0 --- /dev/null +++ b/src/data/sessions/3b03b7e6-7ffe-41b5-83b2-3e54de65258f.json @@ -0,0 +1,13 @@ +{ + "id": "3b03b7e6-7ffe-41b5-83b2-3e54de65258f", + "created": "2025-07-18T12:52:33.189Z", + "expires": "2025-07-19T12:52:33.189Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3bd55fff-83d3-4435-86a1-86f0762ac52d.json b/src/data/sessions/3bd55fff-83d3-4435-86a1-86f0762ac52d.json new file mode 100644 index 0000000..44b0acc --- /dev/null +++ b/src/data/sessions/3bd55fff-83d3-4435-86a1-86f0762ac52d.json @@ -0,0 +1,13 @@ +{ + "id": "3bd55fff-83d3-4435-86a1-86f0762ac52d", + "created": "2025-07-23T12:41:33.713Z", + "expires": "2025-07-24T12:41:33.713Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3ca02587-e55f-42bd-8d6b-d4075e0abe45.json b/src/data/sessions/3ca02587-e55f-42bd-8d6b-d4075e0abe45.json new file mode 100644 index 0000000..b43261b --- /dev/null +++ b/src/data/sessions/3ca02587-e55f-42bd-8d6b-d4075e0abe45.json @@ -0,0 +1,13 @@ +{ + "id": "3ca02587-e55f-42bd-8d6b-d4075e0abe45", + "created": "2025-07-13T13:18:56.254Z", + "expires": "2025-07-14T13:18:56.254Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3ead9678-b6bd-49fb-ae64-7303a4c6c740.json b/src/data/sessions/3ead9678-b6bd-49fb-ae64-7303a4c6c740.json new file mode 100644 index 0000000..ea5733d --- /dev/null +++ b/src/data/sessions/3ead9678-b6bd-49fb-ae64-7303a4c6c740.json @@ -0,0 +1,13 @@ +{ + "id": "3ead9678-b6bd-49fb-ae64-7303a4c6c740", + "created": "2025-07-23T12:39:08.805Z", + "expires": "2025-07-24T12:39:08.805Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/3f156d12-c0e6-4489-8bf9-1c7b53ecf165.json b/src/data/sessions/3f156d12-c0e6-4489-8bf9-1c7b53ecf165.json new file mode 100644 index 0000000..b499c6b --- /dev/null +++ b/src/data/sessions/3f156d12-c0e6-4489-8bf9-1c7b53ecf165.json @@ -0,0 +1,13 @@ +{ + "id": "3f156d12-c0e6-4489-8bf9-1c7b53ecf165", + "created": "2025-07-23T20:28:32.489Z", + "expires": "2025-07-24T20:28:32.489Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/412531dd-cd61-4f27-b330-07b5196ef64f.json b/src/data/sessions/412531dd-cd61-4f27-b330-07b5196ef64f.json new file mode 100644 index 0000000..b5e73c3 --- /dev/null +++ b/src/data/sessions/412531dd-cd61-4f27-b330-07b5196ef64f.json @@ -0,0 +1,13 @@ +{ + "id": "412531dd-cd61-4f27-b330-07b5196ef64f", + "created": "2025-07-13T13:14:14.490Z", + "expires": "2025-07-14T13:14:14.490Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/44b11a87-a008-47bd-affa-72e7daff37d5.json b/src/data/sessions/44b11a87-a008-47bd-affa-72e7daff37d5.json new file mode 100644 index 0000000..774f006 --- /dev/null +++ b/src/data/sessions/44b11a87-a008-47bd-affa-72e7daff37d5.json @@ -0,0 +1,13 @@ +{ + "id": "44b11a87-a008-47bd-affa-72e7daff37d5", + "created": "2025-07-13T17:16:58.667Z", + "expires": "2025-07-14T17:16:58.667Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/4543a139-f0a2-427a-bcf3-229c998d50c1.json b/src/data/sessions/4543a139-f0a2-427a-bcf3-229c998d50c1.json new file mode 100644 index 0000000..88d100f --- /dev/null +++ b/src/data/sessions/4543a139-f0a2-427a-bcf3-229c998d50c1.json @@ -0,0 +1,13 @@ +{ + "id": "4543a139-f0a2-427a-bcf3-229c998d50c1", + "created": "2025-07-18T16:58:38.999Z", + "expires": "2025-07-19T16:58:38.999Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/48ad0b9e-6646-47ac-a691-6d2407203484.json b/src/data/sessions/48ad0b9e-6646-47ac-a691-6d2407203484.json new file mode 100644 index 0000000..b3a7b36 --- /dev/null +++ b/src/data/sessions/48ad0b9e-6646-47ac-a691-6d2407203484.json @@ -0,0 +1,13 @@ +{ + "id": "48ad0b9e-6646-47ac-a691-6d2407203484", + "created": "2025-07-18T21:09:38.626Z", + "expires": "2025-07-19T21:09:38.626Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/490021fb-856e-4176-b889-af8a9041e44f.json b/src/data/sessions/490021fb-856e-4176-b889-af8a9041e44f.json new file mode 100644 index 0000000..8619f85 --- /dev/null +++ b/src/data/sessions/490021fb-856e-4176-b889-af8a9041e44f.json @@ -0,0 +1,13 @@ +{ + "id": "490021fb-856e-4176-b889-af8a9041e44f", + "created": "2025-07-15T18:07:33.555Z", + "expires": "2025-07-16T18:07:33.555Z", + "notification": { + "type": "completed", + "project": "coverr_video", + "message": "[coverr_video] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/4ad8d491-776e-4901-9210-84fa76c09b69.json b/src/data/sessions/4ad8d491-776e-4901-9210-84fa76c09b69.json new file mode 100644 index 0000000..2242550 --- /dev/null +++ b/src/data/sessions/4ad8d491-776e-4901-9210-84fa76c09b69.json @@ -0,0 +1,13 @@ +{ + "id": "4ad8d491-776e-4901-9210-84fa76c09b69", + "created": "2025-07-23T23:02:23.225Z", + "expires": "2025-07-24T23:02:23.225Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/4be36d8d-ab24-47fa-8580-a5a145055ae0.json b/src/data/sessions/4be36d8d-ab24-47fa-8580-a5a145055ae0.json new file mode 100644 index 0000000..4a08531 --- /dev/null +++ b/src/data/sessions/4be36d8d-ab24-47fa-8580-a5a145055ae0.json @@ -0,0 +1,13 @@ +{ + "id": "4be36d8d-ab24-47fa-8580-a5a145055ae0", + "created": "2025-07-23T12:19:49.293Z", + "expires": "2025-07-24T12:19:49.293Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/4e5150dc-adfd-439a-8a50-1319f5956efd.json b/src/data/sessions/4e5150dc-adfd-439a-8a50-1319f5956efd.json new file mode 100644 index 0000000..085d18f --- /dev/null +++ b/src/data/sessions/4e5150dc-adfd-439a-8a50-1319f5956efd.json @@ -0,0 +1,13 @@ +{ + "id": "4e5150dc-adfd-439a-8a50-1319f5956efd", + "created": "2025-07-17T05:16:42.315Z", + "expires": "2025-07-18T05:16:42.315Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/4e85e5f7-f538-4e86-bd2a-7097e2bc8f16.json b/src/data/sessions/4e85e5f7-f538-4e86-bd2a-7097e2bc8f16.json new file mode 100644 index 0000000..9a2fca5 --- /dev/null +++ b/src/data/sessions/4e85e5f7-f538-4e86-bd2a-7097e2bc8f16.json @@ -0,0 +1,13 @@ +{ + "id": "4e85e5f7-f538-4e86-bd2a-7097e2bc8f16", + "created": "2025-07-12T22:30:43.403Z", + "expires": "2025-07-13T22:30:43.403Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/4ee3f0b1-7d23-4eef-a660-9d4fa63d7a2e.json b/src/data/sessions/4ee3f0b1-7d23-4eef-a660-9d4fa63d7a2e.json new file mode 100644 index 0000000..9c48ea2 --- /dev/null +++ b/src/data/sessions/4ee3f0b1-7d23-4eef-a660-9d4fa63d7a2e.json @@ -0,0 +1,13 @@ +{ + "id": "4ee3f0b1-7d23-4eef-a660-9d4fa63d7a2e", + "created": "2025-07-18T13:37:08.299Z", + "expires": "2025-07-19T13:37:08.299Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/535a51d2-480e-46ac-988a-a0012babe13f.json b/src/data/sessions/535a51d2-480e-46ac-988a-a0012babe13f.json new file mode 100644 index 0000000..441b709 --- /dev/null +++ b/src/data/sessions/535a51d2-480e-46ac-988a-a0012babe13f.json @@ -0,0 +1,13 @@ +{ + "id": "535a51d2-480e-46ac-988a-a0012babe13f", + "created": "2025-07-18T11:56:52.666Z", + "expires": "2025-07-19T11:56:52.666Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/53b27eaa-89cf-4a64-a62a-510a3fa3f9b9.json b/src/data/sessions/53b27eaa-89cf-4a64-a62a-510a3fa3f9b9.json new file mode 100644 index 0000000..8d1e4ae --- /dev/null +++ b/src/data/sessions/53b27eaa-89cf-4a64-a62a-510a3fa3f9b9.json @@ -0,0 +1,13 @@ +{ + "id": "53b27eaa-89cf-4a64-a62a-510a3fa3f9b9", + "created": "2025-07-18T11:18:14.937Z", + "expires": "2025-07-19T11:18:14.937Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/56a35209-9174-4a75-b7f5-43b564e972cd.json b/src/data/sessions/56a35209-9174-4a75-b7f5-43b564e972cd.json new file mode 100644 index 0000000..7bb36d9 --- /dev/null +++ b/src/data/sessions/56a35209-9174-4a75-b7f5-43b564e972cd.json @@ -0,0 +1,13 @@ +{ + "id": "56a35209-9174-4a75-b7f5-43b564e972cd", + "created": "2025-07-12T22:45:31.091Z", + "expires": "2025-07-13T22:45:31.091Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/590cc9b0-d985-488d-97b4-478076ce0d67.json b/src/data/sessions/590cc9b0-d985-488d-97b4-478076ce0d67.json new file mode 100644 index 0000000..af8239a --- /dev/null +++ b/src/data/sessions/590cc9b0-d985-488d-97b4-478076ce0d67.json @@ -0,0 +1,13 @@ +{ + "id": "590cc9b0-d985-488d-97b4-478076ce0d67", + "created": "2025-07-13T13:07:29.360Z", + "expires": "2025-07-14T13:07:29.360Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/602070fc-f0a9-42f3-b305-6b5149d9da60.json b/src/data/sessions/602070fc-f0a9-42f3-b305-6b5149d9da60.json new file mode 100644 index 0000000..18a4a05 --- /dev/null +++ b/src/data/sessions/602070fc-f0a9-42f3-b305-6b5149d9da60.json @@ -0,0 +1,13 @@ +{ + "id": "602070fc-f0a9-42f3-b305-6b5149d9da60", + "created": "2025-07-12T22:50:13.065Z", + "expires": "2025-07-13T22:50:13.065Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/60281214-9e5a-4a6a-b3bd-b9a849b7ee9c.json b/src/data/sessions/60281214-9e5a-4a6a-b3bd-b9a849b7ee9c.json new file mode 100644 index 0000000..ae56128 --- /dev/null +++ b/src/data/sessions/60281214-9e5a-4a6a-b3bd-b9a849b7ee9c.json @@ -0,0 +1,13 @@ +{ + "id": "60281214-9e5a-4a6a-b3bd-b9a849b7ee9c", + "created": "2025-07-18T13:04:16.855Z", + "expires": "2025-07-19T13:04:16.855Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/615e56ff-f4ce-4220-b3b1-24aefcba386f.json b/src/data/sessions/615e56ff-f4ce-4220-b3b1-24aefcba386f.json new file mode 100644 index 0000000..023f23c --- /dev/null +++ b/src/data/sessions/615e56ff-f4ce-4220-b3b1-24aefcba386f.json @@ -0,0 +1,13 @@ +{ + "id": "615e56ff-f4ce-4220-b3b1-24aefcba386f", + "created": "2025-07-18T12:32:15.171Z", + "expires": "2025-07-19T12:32:15.171Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/628c6e13-e109-4bf0-a98a-8db0005ac55f.json b/src/data/sessions/628c6e13-e109-4bf0-a98a-8db0005ac55f.json new file mode 100644 index 0000000..437ad31 --- /dev/null +++ b/src/data/sessions/628c6e13-e109-4bf0-a98a-8db0005ac55f.json @@ -0,0 +1,13 @@ +{ + "id": "628c6e13-e109-4bf0-a98a-8db0005ac55f", + "created": "2025-07-18T13:33:08.127Z", + "expires": "2025-07-19T13:33:08.127Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/6344f960-c6b6-40e3-9626-7b92c40d148b.json b/src/data/sessions/6344f960-c6b6-40e3-9626-7b92c40d148b.json new file mode 100644 index 0000000..2522088 --- /dev/null +++ b/src/data/sessions/6344f960-c6b6-40e3-9626-7b92c40d148b.json @@ -0,0 +1,13 @@ +{ + "id": "6344f960-c6b6-40e3-9626-7b92c40d148b", + "created": "2025-07-18T17:06:22.270Z", + "expires": "2025-07-19T17:06:22.270Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/63d537b7-63e3-4c23-8358-b63d5c28c72d.json b/src/data/sessions/63d537b7-63e3-4c23-8358-b63d5c28c72d.json new file mode 100644 index 0000000..686b74b --- /dev/null +++ b/src/data/sessions/63d537b7-63e3-4c23-8358-b63d5c28c72d.json @@ -0,0 +1,13 @@ +{ + "id": "63d537b7-63e3-4c23-8358-b63d5c28c72d", + "created": "2025-07-23T22:50:41.568Z", + "expires": "2025-07-24T22:50:41.568Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/667e76f3-fdd8-407e-89c7-c120311d1e98.json b/src/data/sessions/667e76f3-fdd8-407e-89c7-c120311d1e98.json new file mode 100644 index 0000000..9f7ad7e --- /dev/null +++ b/src/data/sessions/667e76f3-fdd8-407e-89c7-c120311d1e98.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/67a7a308-29b1-4236-835e-784f87e24632.json b/src/data/sessions/67a7a308-29b1-4236-835e-784f87e24632.json new file mode 100644 index 0000000..658bad4 --- /dev/null +++ b/src/data/sessions/67a7a308-29b1-4236-835e-784f87e24632.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/68ff37be-a0c6-4709-bc95-c38d02f5c7e5.json b/src/data/sessions/68ff37be-a0c6-4709-bc95-c38d02f5c7e5.json new file mode 100644 index 0000000..1b9396c --- /dev/null +++ b/src/data/sessions/68ff37be-a0c6-4709-bc95-c38d02f5c7e5.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/694590e6-e5ce-492a-a289-4967108342e9.json b/src/data/sessions/694590e6-e5ce-492a-a289-4967108342e9.json new file mode 100644 index 0000000..bb7a731 --- /dev/null +++ b/src/data/sessions/694590e6-e5ce-492a-a289-4967108342e9.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/6a00efc7-b169-4517-bc6b-03d9c7296e6b.json b/src/data/sessions/6a00efc7-b169-4517-bc6b-03d9c7296e6b.json new file mode 100644 index 0000000..e358674 --- /dev/null +++ b/src/data/sessions/6a00efc7-b169-4517-bc6b-03d9c7296e6b.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/6d115776-ab6b-48fc-ad91-fcff5d512085.json b/src/data/sessions/6d115776-ab6b-48fc-ad91-fcff5d512085.json new file mode 100644 index 0000000..301bd73 --- /dev/null +++ b/src/data/sessions/6d115776-ab6b-48fc-ad91-fcff5d512085.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/6e85295a-e56f-4d44-a537-5be3a026f109.json b/src/data/sessions/6e85295a-e56f-4d44-a537-5be3a026f109.json new file mode 100644 index 0000000..1ac1ad1 --- /dev/null +++ b/src/data/sessions/6e85295a-e56f-4d44-a537-5be3a026f109.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/708b349a-6fac-441f-98c0-acd0cc7e175c.json b/src/data/sessions/708b349a-6fac-441f-98c0-acd0cc7e175c.json new file mode 100644 index 0000000..9ea7ded --- /dev/null +++ b/src/data/sessions/708b349a-6fac-441f-98c0-acd0cc7e175c.json @@ -0,0 +1,13 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/data/sessions/70f4e977-cd61-4b63-87cc-41dc94276e73.json b/src/data/sessions/70f4e977-cd61-4b63-87cc-41dc94276e73.json new file mode 100644 index 0000000..481f7b6 --- /dev/null +++ b/src/data/sessions/70f4e977-cd61-4b63-87cc-41dc94276e73.json @@ -0,0 +1,13 @@ +{ + "id": "70f4e977-cd61-4b63-87cc-41dc94276e73", + "created": "2025-07-18T21:03:11.363Z", + "expires": "2025-07-19T21:03:11.363Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/71d878dc-e75b-4c53-9b26-fb21676120de.json b/src/data/sessions/71d878dc-e75b-4c53-9b26-fb21676120de.json new file mode 100644 index 0000000..cc20b5b --- /dev/null +++ b/src/data/sessions/71d878dc-e75b-4c53-9b26-fb21676120de.json @@ -0,0 +1,13 @@ +{ + "id": "71d878dc-e75b-4c53-9b26-fb21676120de", + "created": "2025-07-23T12:36:25.228Z", + "expires": "2025-07-24T12:36:25.228Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/72f1784b-19fb-4ce0-8bf9-ac4a26049020.json b/src/data/sessions/72f1784b-19fb-4ce0-8bf9-ac4a26049020.json new file mode 100644 index 0000000..79b43f0 --- /dev/null +++ b/src/data/sessions/72f1784b-19fb-4ce0-8bf9-ac4a26049020.json @@ -0,0 +1,13 @@ +{ + "id": "72f1784b-19fb-4ce0-8bf9-ac4a26049020", + "created": "2025-07-14T09:36:42.867Z", + "expires": "2025-07-15T09:36:42.867Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7357bf2d-f2f3-4246-b95c-eb53b41090f2.json b/src/data/sessions/7357bf2d-f2f3-4246-b95c-eb53b41090f2.json new file mode 100644 index 0000000..b6b989b --- /dev/null +++ b/src/data/sessions/7357bf2d-f2f3-4246-b95c-eb53b41090f2.json @@ -0,0 +1,13 @@ +{ + "id": "7357bf2d-f2f3-4246-b95c-eb53b41090f2", + "created": "2025-07-18T13:29:24.730Z", + "expires": "2025-07-19T13:29:24.730Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/75ed2ecd-04b6-4f50-9f00-419319410eb9.json b/src/data/sessions/75ed2ecd-04b6-4f50-9f00-419319410eb9.json new file mode 100644 index 0000000..05b894c --- /dev/null +++ b/src/data/sessions/75ed2ecd-04b6-4f50-9f00-419319410eb9.json @@ -0,0 +1,13 @@ +{ + "id": "75ed2ecd-04b6-4f50-9f00-419319410eb9", + "created": "2025-07-24T00:01:16.660Z", + "expires": "2025-07-25T00:01:16.660Z", + "notification": { + "type": "waiting", + "project": "pandalla.ai", + "message": "[pandalla.ai] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/77fd9ef3-2633-46b0-aba0-8cf92e8382e3.json b/src/data/sessions/77fd9ef3-2633-46b0-aba0-8cf92e8382e3.json new file mode 100644 index 0000000..1d2491e --- /dev/null +++ b/src/data/sessions/77fd9ef3-2633-46b0-aba0-8cf92e8382e3.json @@ -0,0 +1,13 @@ +{ + "id": "77fd9ef3-2633-46b0-aba0-8cf92e8382e3", + "created": "2025-07-12T22:35:18.401Z", + "expires": "2025-07-13T22:35:18.401Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/783a9f43-a016-4ce4-ae53-b50e55d68bc0.json b/src/data/sessions/783a9f43-a016-4ce4-ae53-b50e55d68bc0.json new file mode 100644 index 0000000..392b984 --- /dev/null +++ b/src/data/sessions/783a9f43-a016-4ce4-ae53-b50e55d68bc0.json @@ -0,0 +1,13 @@ +{ + "id": "783a9f43-a016-4ce4-ae53-b50e55d68bc0", + "created": "2025-07-13T17:44:58.245Z", + "expires": "2025-07-14T17:44:58.245Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/79a75dd9-2d81-4e1a-a52b-d648e0fc2d1f.json b/src/data/sessions/79a75dd9-2d81-4e1a-a52b-d648e0fc2d1f.json new file mode 100644 index 0000000..5abcd21 --- /dev/null +++ b/src/data/sessions/79a75dd9-2d81-4e1a-a52b-d648e0fc2d1f.json @@ -0,0 +1,13 @@ +{ + "id": "79a75dd9-2d81-4e1a-a52b-d648e0fc2d1f", + "created": "2025-07-13T16:56:58.351Z", + "expires": "2025-07-14T16:56:58.351Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7a047613-8892-4483-9553-3866ce3aba0e.json b/src/data/sessions/7a047613-8892-4483-9553-3866ce3aba0e.json new file mode 100644 index 0000000..8bc43eb --- /dev/null +++ b/src/data/sessions/7a047613-8892-4483-9553-3866ce3aba0e.json @@ -0,0 +1,13 @@ +{ + "id": "7a047613-8892-4483-9553-3866ce3aba0e", + "created": "2025-07-12T22:57:09.397Z", + "expires": "2025-07-13T22:57:09.397Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7ba59d33-febd-4533-9534-b24674f49ab1.json b/src/data/sessions/7ba59d33-febd-4533-9534-b24674f49ab1.json new file mode 100644 index 0000000..c4485bb --- /dev/null +++ b/src/data/sessions/7ba59d33-febd-4533-9534-b24674f49ab1.json @@ -0,0 +1,13 @@ +{ + "id": "7ba59d33-febd-4533-9534-b24674f49ab1", + "created": "2025-07-13T13:29:27.538Z", + "expires": "2025-07-14T13:29:27.538Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7d646653-8da9-4a05-b95d-cbf4dccef64e.json b/src/data/sessions/7d646653-8da9-4a05-b95d-cbf4dccef64e.json new file mode 100644 index 0000000..0463508 --- /dev/null +++ b/src/data/sessions/7d646653-8da9-4a05-b95d-cbf4dccef64e.json @@ -0,0 +1,13 @@ +{ + "id": "7d646653-8da9-4a05-b95d-cbf4dccef64e", + "created": "2025-07-24T06:57:57.603Z", + "expires": "2025-07-25T06:57:57.603Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7dbd15c9-f9dd-425a-bfa9-1f493b0aea06.json b/src/data/sessions/7dbd15c9-f9dd-425a-bfa9-1f493b0aea06.json new file mode 100644 index 0000000..9f8b527 --- /dev/null +++ b/src/data/sessions/7dbd15c9-f9dd-425a-bfa9-1f493b0aea06.json @@ -0,0 +1,13 @@ +{ + "id": "7dbd15c9-f9dd-425a-bfa9-1f493b0aea06", + "created": "2025-07-14T09:32:17.232Z", + "expires": "2025-07-15T09:32:17.232Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7f2221ae-92c7-4e53-bea2-1a670e53b2fa.json b/src/data/sessions/7f2221ae-92c7-4e53-bea2-1a670e53b2fa.json new file mode 100644 index 0000000..ebb1bdc --- /dev/null +++ b/src/data/sessions/7f2221ae-92c7-4e53-bea2-1a670e53b2fa.json @@ -0,0 +1,13 @@ +{ + "id": "7f2221ae-92c7-4e53-bea2-1a670e53b2fa", + "created": "2025-07-18T13:20:39.822Z", + "expires": "2025-07-19T13:20:39.822Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/7fa6cf40-8073-4f09-b898-7aa9df8f18da.json b/src/data/sessions/7fa6cf40-8073-4f09-b898-7aa9df8f18da.json new file mode 100644 index 0000000..ed5c5c9 --- /dev/null +++ b/src/data/sessions/7fa6cf40-8073-4f09-b898-7aa9df8f18da.json @@ -0,0 +1,13 @@ +{ + "id": "7fa6cf40-8073-4f09-b898-7aa9df8f18da", + "created": "2025-07-23T12:20:56.079Z", + "expires": "2025-07-24T12:20:56.079Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/83a59c83-57a5-45f9-b60f-de5e64805516.json b/src/data/sessions/83a59c83-57a5-45f9-b60f-de5e64805516.json new file mode 100644 index 0000000..ae6fa1e --- /dev/null +++ b/src/data/sessions/83a59c83-57a5-45f9-b60f-de5e64805516.json @@ -0,0 +1,13 @@ +{ + "id": "83a59c83-57a5-45f9-b60f-de5e64805516", + "created": "2025-07-18T13:45:19.036Z", + "expires": "2025-07-19T13:45:19.036Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/86dc8fcb-34a5-4f0f-ab7a-4153b5fae8c6.json b/src/data/sessions/86dc8fcb-34a5-4f0f-ab7a-4153b5fae8c6.json new file mode 100644 index 0000000..31b2468 --- /dev/null +++ b/src/data/sessions/86dc8fcb-34a5-4f0f-ab7a-4153b5fae8c6.json @@ -0,0 +1,13 @@ +{ + "id": "86dc8fcb-34a5-4f0f-ab7a-4153b5fae8c6", + "created": "2025-07-23T23:37:49.309Z", + "expires": "2025-07-24T23:37:49.309Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/881b6505-ecb2-4f76-95a5-ffc7f8d8d769.json b/src/data/sessions/881b6505-ecb2-4f76-95a5-ffc7f8d8d769.json new file mode 100644 index 0000000..0347be3 --- /dev/null +++ b/src/data/sessions/881b6505-ecb2-4f76-95a5-ffc7f8d8d769.json @@ -0,0 +1,13 @@ +{ + "id": "881b6505-ecb2-4f76-95a5-ffc7f8d8d769", + "created": "2025-07-12T23:11:20.140Z", + "expires": "2025-07-13T23:11:20.140Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/8841268c-2781-4966-b6f1-b6bbd68b3f5f.json b/src/data/sessions/8841268c-2781-4966-b6f1-b6bbd68b3f5f.json new file mode 100644 index 0000000..38a3e83 --- /dev/null +++ b/src/data/sessions/8841268c-2781-4966-b6f1-b6bbd68b3f5f.json @@ -0,0 +1,13 @@ +{ + "id": "8841268c-2781-4966-b6f1-b6bbd68b3f5f", + "created": "2025-07-23T23:59:12.060Z", + "expires": "2025-07-24T23:59:12.060Z", + "notification": { + "type": "waiting", + "project": "pandalla.ai", + "message": "[pandalla.ai] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/8b56de5e-3af9-4f62-a178-e205107effa8.json b/src/data/sessions/8b56de5e-3af9-4f62-a178-e205107effa8.json new file mode 100644 index 0000000..ed3b3f8 --- /dev/null +++ b/src/data/sessions/8b56de5e-3af9-4f62-a178-e205107effa8.json @@ -0,0 +1,13 @@ +{ + "id": "8b56de5e-3af9-4f62-a178-e205107effa8", + "created": "2025-07-23T23:05:43.845Z", + "expires": "2025-07-24T23:05:43.845Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/8d2cd1e5-1ade-4e58-b7e0-354c3b048ae3.json b/src/data/sessions/8d2cd1e5-1ade-4e58-b7e0-354c3b048ae3.json new file mode 100644 index 0000000..914efc3 --- /dev/null +++ b/src/data/sessions/8d2cd1e5-1ade-4e58-b7e0-354c3b048ae3.json @@ -0,0 +1,13 @@ +{ + "id": "8d2cd1e5-1ade-4e58-b7e0-354c3b048ae3", + "created": "2025-07-12T23:12:02.275Z", + "expires": "2025-07-13T23:12:02.275Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/91bf99e1-93a7-4c3d-8b31-ffbb2bda4e5a.json b/src/data/sessions/91bf99e1-93a7-4c3d-8b31-ffbb2bda4e5a.json new file mode 100644 index 0000000..afe7dca --- /dev/null +++ b/src/data/sessions/91bf99e1-93a7-4c3d-8b31-ffbb2bda4e5a.json @@ -0,0 +1,13 @@ +{ + "id": "91bf99e1-93a7-4c3d-8b31-ffbb2bda4e5a", + "created": "2025-07-18T12:21:42.450Z", + "expires": "2025-07-19T12:21:42.450Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/936051d7-fac6-4d3f-8743-20f9492dc17e.json b/src/data/sessions/936051d7-fac6-4d3f-8743-20f9492dc17e.json new file mode 100644 index 0000000..e71f4a8 --- /dev/null +++ b/src/data/sessions/936051d7-fac6-4d3f-8743-20f9492dc17e.json @@ -0,0 +1,13 @@ +{ + "id": "936051d7-fac6-4d3f-8743-20f9492dc17e", + "created": "2025-07-18T21:18:29.084Z", + "expires": "2025-07-19T21:18:29.084Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/9757efc0-55f2-4e51-a546-fc8602248b9d.json b/src/data/sessions/9757efc0-55f2-4e51-a546-fc8602248b9d.json new file mode 100644 index 0000000..3242cdb --- /dev/null +++ b/src/data/sessions/9757efc0-55f2-4e51-a546-fc8602248b9d.json @@ -0,0 +1,13 @@ +{ + "id": "9757efc0-55f2-4e51-a546-fc8602248b9d", + "created": "2025-07-24T00:11:31.889Z", + "expires": "2025-07-25T00:11:31.889Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/99d7499e-20e6-4104-9d21-e5a0ce96ac4e.json b/src/data/sessions/99d7499e-20e6-4104-9d21-e5a0ce96ac4e.json new file mode 100644 index 0000000..546ba11 --- /dev/null +++ b/src/data/sessions/99d7499e-20e6-4104-9d21-e5a0ce96ac4e.json @@ -0,0 +1,13 @@ +{ + "id": "99d7499e-20e6-4104-9d21-e5a0ce96ac4e", + "created": "2025-07-23T12:07:44.610Z", + "expires": "2025-07-24T12:07:44.610Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/9c1e952e-8883-4a3d-8d8d-c7632c5d73e0.json b/src/data/sessions/9c1e952e-8883-4a3d-8d8d-c7632c5d73e0.json new file mode 100644 index 0000000..3c9b3db --- /dev/null +++ b/src/data/sessions/9c1e952e-8883-4a3d-8d8d-c7632c5d73e0.json @@ -0,0 +1,13 @@ +{ + "id": "9c1e952e-8883-4a3d-8d8d-c7632c5d73e0", + "created": "2025-07-12T22:40:15.010Z", + "expires": "2025-07-13T22:40:15.010Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/9d303a61-c307-4c15-810c-13c9a958a4a4.json b/src/data/sessions/9d303a61-c307-4c15-810c-13c9a958a4a4.json new file mode 100644 index 0000000..f57f336 --- /dev/null +++ b/src/data/sessions/9d303a61-c307-4c15-810c-13c9a958a4a4.json @@ -0,0 +1,13 @@ +{ + "id": "9d303a61-c307-4c15-810c-13c9a958a4a4", + "created": "2025-07-12T23:22:35.971Z", + "expires": "2025-07-13T23:22:35.971Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/9e948c0e-4e47-4a26-931a-30151f3fd4f6.json b/src/data/sessions/9e948c0e-4e47-4a26-931a-30151f3fd4f6.json new file mode 100644 index 0000000..c6ca42c --- /dev/null +++ b/src/data/sessions/9e948c0e-4e47-4a26-931a-30151f3fd4f6.json @@ -0,0 +1,13 @@ +{ + "id": "9e948c0e-4e47-4a26-931a-30151f3fd4f6", + "created": "2025-07-14T09:23:28.488Z", + "expires": "2025-07-15T09:23:28.488Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/9f27e639-fca2-436b-a20c-8a39f0b3ef70.json b/src/data/sessions/9f27e639-fca2-436b-a20c-8a39f0b3ef70.json new file mode 100644 index 0000000..18096cd --- /dev/null +++ b/src/data/sessions/9f27e639-fca2-436b-a20c-8a39f0b3ef70.json @@ -0,0 +1,13 @@ +{ + "id": "9f27e639-fca2-436b-a20c-8a39f0b3ef70", + "created": "2025-07-26T12:35:10.184Z", + "expires": "2025-07-27T12:35:10.184Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/a3a18160-3e5e-4199-8b5e-4f046f4c7c0d.json b/src/data/sessions/a3a18160-3e5e-4199-8b5e-4f046f4c7c0d.json new file mode 100644 index 0000000..6715ad2 --- /dev/null +++ b/src/data/sessions/a3a18160-3e5e-4199-8b5e-4f046f4c7c0d.json @@ -0,0 +1,13 @@ +{ + "id": "a3a18160-3e5e-4199-8b5e-4f046f4c7c0d", + "created": "2025-07-13T21:41:05.285Z", + "expires": "2025-07-14T21:41:05.285Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/a5344dfe-ffde-4f6c-9c5a-fadafd044a32.json b/src/data/sessions/a5344dfe-ffde-4f6c-9c5a-fadafd044a32.json new file mode 100644 index 0000000..c4d92e1 --- /dev/null +++ b/src/data/sessions/a5344dfe-ffde-4f6c-9c5a-fadafd044a32.json @@ -0,0 +1,13 @@ +{ + "id": "a5344dfe-ffde-4f6c-9c5a-fadafd044a32", + "created": "2025-07-23T22:19:52.121Z", + "expires": "2025-07-24T22:19:52.121Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/a543782e-c92d-44ba-ae9d-264f9184f8a4.json b/src/data/sessions/a543782e-c92d-44ba-ae9d-264f9184f8a4.json new file mode 100644 index 0000000..a988bd8 --- /dev/null +++ b/src/data/sessions/a543782e-c92d-44ba-ae9d-264f9184f8a4.json @@ -0,0 +1,13 @@ +{ + "id": "a543782e-c92d-44ba-ae9d-264f9184f8a4", + "created": "2025-07-13T17:39:13.757Z", + "expires": "2025-07-14T17:39:13.757Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/a6042e37-ed3f-4173-b23a-fe42fd0c4d35.json b/src/data/sessions/a6042e37-ed3f-4173-b23a-fe42fd0c4d35.json new file mode 100644 index 0000000..daf5ea5 --- /dev/null +++ b/src/data/sessions/a6042e37-ed3f-4173-b23a-fe42fd0c4d35.json @@ -0,0 +1,13 @@ +{ + "id": "a6042e37-ed3f-4173-b23a-fe42fd0c4d35", + "created": "2025-07-23T12:19:26.327Z", + "expires": "2025-07-24T12:19:26.327Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/a9503913-5a84-4b83-93a7-132d5551e892.json b/src/data/sessions/a9503913-5a84-4b83-93a7-132d5551e892.json new file mode 100644 index 0000000..dd78077 --- /dev/null +++ b/src/data/sessions/a9503913-5a84-4b83-93a7-132d5551e892.json @@ -0,0 +1,13 @@ +{ + "id": "a9503913-5a84-4b83-93a7-132d5551e892", + "created": "2025-07-13T13:23:24.882Z", + "expires": "2025-07-14T13:23:24.882Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/ab697267-7e75-44fc-aac8-155453d3abcc.json b/src/data/sessions/ab697267-7e75-44fc-aac8-155453d3abcc.json new file mode 100644 index 0000000..6449427 --- /dev/null +++ b/src/data/sessions/ab697267-7e75-44fc-aac8-155453d3abcc.json @@ -0,0 +1,13 @@ +{ + "id": "ab697267-7e75-44fc-aac8-155453d3abcc", + "created": "2025-07-14T09:19:41.807Z", + "expires": "2025-07-15T09:19:41.807Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/adeb1011-cd41-4c88-8ce9-2f86fd7a22bb.json b/src/data/sessions/adeb1011-cd41-4c88-8ce9-2f86fd7a22bb.json new file mode 100644 index 0000000..3816665 --- /dev/null +++ b/src/data/sessions/adeb1011-cd41-4c88-8ce9-2f86fd7a22bb.json @@ -0,0 +1,13 @@ +{ + "id": "adeb1011-cd41-4c88-8ce9-2f86fd7a22bb", + "created": "2025-07-15T12:29:15.330Z", + "expires": "2025-07-16T12:29:15.330Z", + "notification": { + "type": "completed", + "project": "PandaRank", + "message": "[PandaRank] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/af6551d4-b61c-4fd9-ba7e-cfb4bc96a730.json b/src/data/sessions/af6551d4-b61c-4fd9-ba7e-cfb4bc96a730.json new file mode 100644 index 0000000..af8d714 --- /dev/null +++ b/src/data/sessions/af6551d4-b61c-4fd9-ba7e-cfb4bc96a730.json @@ -0,0 +1,13 @@ +{ + "id": "af6551d4-b61c-4fd9-ba7e-cfb4bc96a730", + "created": "2025-07-18T13:16:01.559Z", + "expires": "2025-07-19T13:16:01.559Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/af9972fd-8e95-4873-a700-2216fcd6e3ab.json b/src/data/sessions/af9972fd-8e95-4873-a700-2216fcd6e3ab.json new file mode 100644 index 0000000..26ae9ff --- /dev/null +++ b/src/data/sessions/af9972fd-8e95-4873-a700-2216fcd6e3ab.json @@ -0,0 +1,13 @@ +{ + "id": "af9972fd-8e95-4873-a700-2216fcd6e3ab", + "created": "2025-07-23T22:00:46.554Z", + "expires": "2025-07-24T22:00:46.554Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/afae20d9-2dc0-44d7-925b-de5eb98d65c8.json b/src/data/sessions/afae20d9-2dc0-44d7-925b-de5eb98d65c8.json new file mode 100644 index 0000000..24da44b --- /dev/null +++ b/src/data/sessions/afae20d9-2dc0-44d7-925b-de5eb98d65c8.json @@ -0,0 +1,13 @@ +{ + "id": "afae20d9-2dc0-44d7-925b-de5eb98d65c8", + "created": "2025-07-23T21:34:11.244Z", + "expires": "2025-07-24T21:34:11.244Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/b095fa44-7c8b-4283-a3ab-5eb39df19043.json b/src/data/sessions/b095fa44-7c8b-4283-a3ab-5eb39df19043.json new file mode 100644 index 0000000..a1dcf1f --- /dev/null +++ b/src/data/sessions/b095fa44-7c8b-4283-a3ab-5eb39df19043.json @@ -0,0 +1,13 @@ +{ + "id": "b095fa44-7c8b-4283-a3ab-5eb39df19043", + "created": "2025-07-14T14:21:10.412Z", + "expires": "2025-07-15T14:21:10.412Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/b09f0aab-c797-45be-9ee6-35d5abf98a8a.json b/src/data/sessions/b09f0aab-c797-45be-9ee6-35d5abf98a8a.json new file mode 100644 index 0000000..a36bdec --- /dev/null +++ b/src/data/sessions/b09f0aab-c797-45be-9ee6-35d5abf98a8a.json @@ -0,0 +1,13 @@ +{ + "id": "b09f0aab-c797-45be-9ee6-35d5abf98a8a", + "created": "2025-07-18T11:03:41.609Z", + "expires": "2025-07-19T11:03:41.609Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/b54374c2-af8e-4389-8e54-f93177317d1b.json b/src/data/sessions/b54374c2-af8e-4389-8e54-f93177317d1b.json new file mode 100644 index 0000000..ed0b5c5 --- /dev/null +++ b/src/data/sessions/b54374c2-af8e-4389-8e54-f93177317d1b.json @@ -0,0 +1,13 @@ +{ + "id": "b54374c2-af8e-4389-8e54-f93177317d1b", + "created": "2025-07-13T17:30:32.643Z", + "expires": "2025-07-14T17:30:32.643Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/b5c49341-a764-4dcc-ad8d-c0efac856500.json b/src/data/sessions/b5c49341-a764-4dcc-ad8d-c0efac856500.json new file mode 100644 index 0000000..c2d0e5d --- /dev/null +++ b/src/data/sessions/b5c49341-a764-4dcc-ad8d-c0efac856500.json @@ -0,0 +1,13 @@ +{ + "id": "b5c49341-a764-4dcc-ad8d-c0efac856500", + "created": "2025-07-12T22:28:31.823Z", + "expires": "2025-07-13T22:28:31.823Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/b8b28027-d80d-4dad-b78a-eceb85c022ed.json b/src/data/sessions/b8b28027-d80d-4dad-b78a-eceb85c022ed.json new file mode 100644 index 0000000..6d1b4a2 --- /dev/null +++ b/src/data/sessions/b8b28027-d80d-4dad-b78a-eceb85c022ed.json @@ -0,0 +1,13 @@ +{ + "id": "b8b28027-d80d-4dad-b78a-eceb85c022ed", + "created": "2025-07-18T11:25:42.162Z", + "expires": "2025-07-19T11:25:42.162Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/bcd403a2-f757-48c2-bdb8-a369d6e53a08.json b/src/data/sessions/bcd403a2-f757-48c2-bdb8-a369d6e53a08.json new file mode 100644 index 0000000..9cc5a48 --- /dev/null +++ b/src/data/sessions/bcd403a2-f757-48c2-bdb8-a369d6e53a08.json @@ -0,0 +1,13 @@ +{ + "id": "bcd403a2-f757-48c2-bdb8-a369d6e53a08", + "created": "2025-07-18T12:40:05.075Z", + "expires": "2025-07-19T12:40:05.075Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/bdf0e984-43f9-4479-a60c-551a5725eaf4.json b/src/data/sessions/bdf0e984-43f9-4479-a60c-551a5725eaf4.json new file mode 100644 index 0000000..8e63264 --- /dev/null +++ b/src/data/sessions/bdf0e984-43f9-4479-a60c-551a5725eaf4.json @@ -0,0 +1,13 @@ +{ + "id": "bdf0e984-43f9-4479-a60c-551a5725eaf4", + "created": "2025-07-12T22:54:59.612Z", + "expires": "2025-07-13T22:54:59.612Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/bf66f380-a2da-4f9a-be6c-001dd0ea5d3d.json b/src/data/sessions/bf66f380-a2da-4f9a-be6c-001dd0ea5d3d.json new file mode 100644 index 0000000..c371d7e --- /dev/null +++ b/src/data/sessions/bf66f380-a2da-4f9a-be6c-001dd0ea5d3d.json @@ -0,0 +1,13 @@ +{ + "id": "bf66f380-a2da-4f9a-be6c-001dd0ea5d3d", + "created": "2025-07-17T05:16:22.479Z", + "expires": "2025-07-18T05:16:22.479Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/ca96998d-f46c-41f1-999d-0b6a1537b58e.json b/src/data/sessions/ca96998d-f46c-41f1-999d-0b6a1537b58e.json new file mode 100644 index 0000000..78bff7c --- /dev/null +++ b/src/data/sessions/ca96998d-f46c-41f1-999d-0b6a1537b58e.json @@ -0,0 +1,13 @@ +{ + "id": "ca96998d-f46c-41f1-999d-0b6a1537b58e", + "created": "2025-07-12T22:37:10.322Z", + "expires": "2025-07-13T22:37:10.322Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/cb11eea6-c700-403d-abcf-7dc2d8d7bcf1.json b/src/data/sessions/cb11eea6-c700-403d-abcf-7dc2d8d7bcf1.json new file mode 100644 index 0000000..7e56668 --- /dev/null +++ b/src/data/sessions/cb11eea6-c700-403d-abcf-7dc2d8d7bcf1.json @@ -0,0 +1,13 @@ +{ + "id": "cb11eea6-c700-403d-abcf-7dc2d8d7bcf1", + "created": "2025-07-12T23:28:29.122Z", + "expires": "2025-07-13T23:28:29.122Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/d3c89698-d823-429e-8858-98744d713313.json b/src/data/sessions/d3c89698-d823-429e-8858-98744d713313.json new file mode 100644 index 0000000..0697bb6 --- /dev/null +++ b/src/data/sessions/d3c89698-d823-429e-8858-98744d713313.json @@ -0,0 +1,13 @@ +{ + "id": "d3c89698-d823-429e-8858-98744d713313", + "created": "2025-07-13T12:04:24.834Z", + "expires": "2025-07-14T12:04:24.834Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/d42c602e-9890-4789-adfd-7738b0208858.json b/src/data/sessions/d42c602e-9890-4789-adfd-7738b0208858.json new file mode 100644 index 0000000..0c31d56 --- /dev/null +++ b/src/data/sessions/d42c602e-9890-4789-adfd-7738b0208858.json @@ -0,0 +1,13 @@ +{ + "id": "d42c602e-9890-4789-adfd-7738b0208858", + "created": "2025-07-14T09:44:10.727Z", + "expires": "2025-07-15T09:44:10.727Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/d5842899-7765-4e4d-a172-37f7be3f0fcc.json b/src/data/sessions/d5842899-7765-4e4d-a172-37f7be3f0fcc.json new file mode 100644 index 0000000..7f4cb84 --- /dev/null +++ b/src/data/sessions/d5842899-7765-4e4d-a172-37f7be3f0fcc.json @@ -0,0 +1,13 @@ +{ + "id": "d5842899-7765-4e4d-a172-37f7be3f0fcc", + "created": "2025-07-13T17:26:13.794Z", + "expires": "2025-07-14T17:26:13.794Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/da66ab5c-a85d-40da-92f1-7f353976356e.json b/src/data/sessions/da66ab5c-a85d-40da-92f1-7f353976356e.json new file mode 100644 index 0000000..2bc1a9d --- /dev/null +++ b/src/data/sessions/da66ab5c-a85d-40da-92f1-7f353976356e.json @@ -0,0 +1,13 @@ +{ + "id": "da66ab5c-a85d-40da-92f1-7f353976356e", + "created": "2025-07-24T07:08:07.332Z", + "expires": "2025-07-25T07:08:07.332Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/db0c7af9-b1fa-4826-983d-d580f851aee3.json b/src/data/sessions/db0c7af9-b1fa-4826-983d-d580f851aee3.json new file mode 100644 index 0000000..14648eb --- /dev/null +++ b/src/data/sessions/db0c7af9-b1fa-4826-983d-d580f851aee3.json @@ -0,0 +1,13 @@ +{ + "id": "db0c7af9-b1fa-4826-983d-d580f851aee3", + "created": "2025-07-18T15:44:18.125Z", + "expires": "2025-07-19T15:44:18.125Z", + "notification": { + "type": "waiting", + "project": "JobShield", + "message": "[JobShield] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/dc3965b8-4188-4d23-a780-4e6b25f65167.json b/src/data/sessions/dc3965b8-4188-4d23-a780-4e6b25f65167.json new file mode 100644 index 0000000..bdab64e --- /dev/null +++ b/src/data/sessions/dc3965b8-4188-4d23-a780-4e6b25f65167.json @@ -0,0 +1,13 @@ +{ + "id": "dc3965b8-4188-4d23-a780-4e6b25f65167", + "created": "2025-07-23T22:09:56.764Z", + "expires": "2025-07-24T22:09:56.764Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/dc765fa5-9e78-4fca-b337-2ab13958ce31.json b/src/data/sessions/dc765fa5-9e78-4fca-b337-2ab13958ce31.json new file mode 100644 index 0000000..73eb32e --- /dev/null +++ b/src/data/sessions/dc765fa5-9e78-4fca-b337-2ab13958ce31.json @@ -0,0 +1,13 @@ +{ + "id": "dc765fa5-9e78-4fca-b337-2ab13958ce31", + "created": "2025-07-14T10:19:59.412Z", + "expires": "2025-07-15T10:19:59.412Z", + "notification": { + "type": "waiting", + "project": "pandalla_api", + "message": "[pandalla_api] Claude需要您的进一步指导" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/e347d063-1d52-497e-a2be-01cb35acf985.json b/src/data/sessions/e347d063-1d52-497e-a2be-01cb35acf985.json new file mode 100644 index 0000000..bfece86 --- /dev/null +++ b/src/data/sessions/e347d063-1d52-497e-a2be-01cb35acf985.json @@ -0,0 +1,13 @@ +{ + "id": "e347d063-1d52-497e-a2be-01cb35acf985", + "created": "2025-07-23T11:57:59.324Z", + "expires": "2025-07-24T11:57:59.324Z", + "notification": { + "type": "completed", + "project": "video_tag", + "message": "[video_tag] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/e92d7906-174b-4041-bcce-6d35f1e5d0ce.json b/src/data/sessions/e92d7906-174b-4041-bcce-6d35f1e5d0ce.json new file mode 100644 index 0000000..7f2e15d --- /dev/null +++ b/src/data/sessions/e92d7906-174b-4041-bcce-6d35f1e5d0ce.json @@ -0,0 +1,13 @@ +{ + "id": "e92d7906-174b-4041-bcce-6d35f1e5d0ce", + "created": "2025-07-23T21:27:14.081Z", + "expires": "2025-07-24T21:27:14.081Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/ec2a3e14-5a67-4c82-8f79-cfd47dc749e0.json b/src/data/sessions/ec2a3e14-5a67-4c82-8f79-cfd47dc749e0.json new file mode 100644 index 0000000..496d705 --- /dev/null +++ b/src/data/sessions/ec2a3e14-5a67-4c82-8f79-cfd47dc749e0.json @@ -0,0 +1,13 @@ +{ + "id": "ec2a3e14-5a67-4c82-8f79-cfd47dc749e0", + "created": "2025-07-14T09:16:51.780Z", + "expires": "2025-07-15T09:16:51.780Z", + "notification": { + "type": "completed", + "project": "pandalla_api", + "message": "[pandalla_api] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/eeb90ce8-a724-46bb-be51-0924f5260f36.json b/src/data/sessions/eeb90ce8-a724-46bb-be51-0924f5260f36.json new file mode 100644 index 0000000..775df45 --- /dev/null +++ b/src/data/sessions/eeb90ce8-a724-46bb-be51-0924f5260f36.json @@ -0,0 +1,13 @@ +{ + "id": "eeb90ce8-a724-46bb-be51-0924f5260f36", + "created": "2025-07-18T11:46:45.421Z", + "expires": "2025-07-19T11:46:45.421Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/f365d443-694c-4e4f-9843-dab6a578d61a.json b/src/data/sessions/f365d443-694c-4e4f-9843-dab6a578d61a.json new file mode 100644 index 0000000..af59d64 --- /dev/null +++ b/src/data/sessions/f365d443-694c-4e4f-9843-dab6a578d61a.json @@ -0,0 +1,13 @@ +{ + "id": "f365d443-694c-4e4f-9843-dab6a578d61a", + "created": "2025-07-23T20:56:09.678Z", + "expires": "2025-07-24T20:56:09.678Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/f42db288-d410-4e3b-bf54-8f766a817613.json b/src/data/sessions/f42db288-d410-4e3b-bf54-8f766a817613.json new file mode 100644 index 0000000..9c2c60c --- /dev/null +++ b/src/data/sessions/f42db288-d410-4e3b-bf54-8f766a817613.json @@ -0,0 +1,13 @@ +{ + "id": "f42db288-d410-4e3b-bf54-8f766a817613", + "created": "2025-07-13T17:38:11.270Z", + "expires": "2025-07-14T17:38:11.270Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/f44f853d-d936-4b74-87bd-b63d0cca2885.json b/src/data/sessions/f44f853d-d936-4b74-87bd-b63d0cca2885.json new file mode 100644 index 0000000..a551721 --- /dev/null +++ b/src/data/sessions/f44f853d-d936-4b74-87bd-b63d0cca2885.json @@ -0,0 +1,13 @@ +{ + "id": "f44f853d-d936-4b74-87bd-b63d0cca2885", + "created": "2025-07-18T11:54:24.717Z", + "expires": "2025-07-19T11:54:24.717Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/f4601ed9-b556-41bc-8abb-691679403a2d.json b/src/data/sessions/f4601ed9-b556-41bc-8abb-691679403a2d.json new file mode 100644 index 0000000..8aa67c3 --- /dev/null +++ b/src/data/sessions/f4601ed9-b556-41bc-8abb-691679403a2d.json @@ -0,0 +1,13 @@ +{ + "id": "f4601ed9-b556-41bc-8abb-691679403a2d", + "created": "2025-07-13T17:48:17.334Z", + "expires": "2025-07-14T17:48:17.334Z", + "notification": { + "type": "completed", + "project": "TaskPing", + "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/f812dabb-bce1-45a2-b818-d3d05f21468b.json b/src/data/sessions/f812dabb-bce1-45a2-b818-d3d05f21468b.json new file mode 100644 index 0000000..975898a --- /dev/null +++ b/src/data/sessions/f812dabb-bce1-45a2-b818-d3d05f21468b.json @@ -0,0 +1,13 @@ +{ + "id": "f812dabb-bce1-45a2-b818-d3d05f21468b", + "created": "2025-07-18T13:56:12.911Z", + "expires": "2025-07-19T13:56:12.911Z", + "notification": { + "type": "completed", + "project": "JobShield", + "message": "[JobShield] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/f87ccea0-5c1c-4257-91cf-7fa8ac0f7f07.json b/src/data/sessions/f87ccea0-5c1c-4257-91cf-7fa8ac0f7f07.json new file mode 100644 index 0000000..c36d463 --- /dev/null +++ b/src/data/sessions/f87ccea0-5c1c-4257-91cf-7fa8ac0f7f07.json @@ -0,0 +1,13 @@ +{ + "id": "f87ccea0-5c1c-4257-91cf-7fa8ac0f7f07", + "created": "2025-07-12T22:27:57.091Z", + "expires": "2025-07-13T22:27:57.091Z", + "notification": { + "type": "completed", + "project": "TaskPing-Test", + "message": "这是一封测试邮件,用于验证邮件通知功能是否正常工作。" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/fb2e5275-a6d5-4517-b485-29884747f0de.json b/src/data/sessions/fb2e5275-a6d5-4517-b485-29884747f0de.json new file mode 100644 index 0000000..aa32f93 --- /dev/null +++ b/src/data/sessions/fb2e5275-a6d5-4517-b485-29884747f0de.json @@ -0,0 +1,13 @@ +{ + "id": "fb2e5275-a6d5-4517-b485-29884747f0de", + "created": "2025-07-23T21:09:53.860Z", + "expires": "2025-07-24T21:09:53.860Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/data/sessions/febf9d85-1579-41c4-aca1-91ddf782b961.json b/src/data/sessions/febf9d85-1579-41c4-aca1-91ddf782b961.json new file mode 100644 index 0000000..e1e6472 --- /dev/null +++ b/src/data/sessions/febf9d85-1579-41c4-aca1-91ddf782b961.json @@ -0,0 +1,13 @@ +{ + "id": "febf9d85-1579-41c4-aca1-91ddf782b961", + "created": "2025-07-23T20:44:25.533Z", + "expires": "2025-07-24T20:44:25.533Z", + "notification": { + "type": "completed", + "project": "pandalla.ai", + "message": "[pandalla.ai] 任务已完成,Claude正在等待下一步指令" + }, + "status": "waiting", + "commandCount": 0, + "maxCommands": 10 +} \ No newline at end of file diff --git a/src/relay/claude-command-bridge.js b/src/relay/claude-command-bridge.js new file mode 100644 index 0000000..6df5674 --- /dev/null +++ b/src/relay/claude-command-bridge.js @@ -0,0 +1,214 @@ +/** + * Claude Command Bridge + * 通过文件系统与Claude Code进行通信的桥接器 + */ + +const fs = require('fs'); +const path = require('path'); +const Logger = require('../core/logger'); + +class ClaudeCommandBridge { + constructor() { + this.logger = new Logger('CommandBridge'); + this.commandsDir = path.join(__dirname, '../data/commands'); + this.responseDir = path.join(__dirname, '../data/responses'); + + this._ensureDirectories(); + } + + _ensureDirectories() { + [this.commandsDir, this.responseDir].forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + }); + } + + /** + * 发送命令给Claude Code + * @param {string} command - 要执行的命令 + * @param {string} sessionId - 会话ID + * @returns {Promise} - 是否成功 + */ + async sendCommand(command, sessionId) { + try { + const timestamp = Date.now(); + const commandId = `${sessionId}_${timestamp}`; + + // 创建命令文件 + const commandFile = path.join(this.commandsDir, `${commandId}.json`); + const commandData = { + id: commandId, + sessionId, + command, + timestamp: new Date().toISOString(), + status: 'pending', + source: 'email' + }; + + fs.writeFileSync(commandFile, JSON.stringify(commandData, null, 2)); + + // 创建通知文件 (Claude Code可以监控这个文件变化) + const notificationFile = path.join(this.commandsDir, '.new_command'); + fs.writeFileSync(notificationFile, commandId); + + this.logger.info(`Command sent via file bridge: ${commandId}`); + + // 尝试使用系统通知 + await this._sendSystemNotification(command, commandId); + + return true; + + } catch (error) { + this.logger.error('Failed to send command via bridge:', error.message); + return false; + } + } + + async _sendSystemNotification(command, commandId) { + try { + const title = 'TaskPing - 新邮件命令'; + const body = `命令: ${command.length > 50 ? command.substring(0, 50) + '...' : command}\n\n点击查看详情或在Claude Code中输入命令`; + + if (process.platform === 'darwin') { + // macOS 通知 + const script = ` + display notification "${body.replace(/"/g, '\\"')}" with title "${title}" sound name "default" + `; + const { spawn } = require('child_process'); + spawn('osascript', ['-e', script]); + } else if (process.platform === 'linux') { + // Linux 通知 + const { spawn } = require('child_process'); + spawn('notify-send', [title, body]); + } + + this.logger.debug('System notification sent'); + } catch (error) { + this.logger.warn('Failed to send system notification:', error.message); + } + } + + /** + * 获取待处理的命令 + * @returns {Array} 命令列表 + */ + getPendingCommands() { + try { + const files = fs.readdirSync(this.commandsDir) + .filter(file => file.endsWith('.json')) + .sort(); + + const commands = []; + for (const file of files) { + try { + const commandData = JSON.parse( + fs.readFileSync(path.join(this.commandsDir, file), 'utf8') + ); + if (commandData.status === 'pending') { + commands.push(commandData); + } + } catch (error) { + this.logger.warn(`Failed to parse command file ${file}:`, error.message); + } + } + + return commands; + } catch (error) { + this.logger.error('Failed to get pending commands:', error.message); + return []; + } + } + + /** + * 标记命令为已处理 + * @param {string} commandId - 命令ID + * @param {string} status - 状态 (completed/failed) + * @param {string} response - 响应内容 + */ + markCommandProcessed(commandId, status = 'completed', response = '') { + try { + const commandFile = path.join(this.commandsDir, `${commandId}.json`); + + if (fs.existsSync(commandFile)) { + const commandData = JSON.parse(fs.readFileSync(commandFile, 'utf8')); + commandData.status = status; + commandData.processedAt = new Date().toISOString(); + commandData.response = response; + + // 保存到响应目录 + const responseFile = path.join(this.responseDir, `${commandId}.json`); + fs.writeFileSync(responseFile, JSON.stringify(commandData, null, 2)); + + // 删除原命令文件 + fs.unlinkSync(commandFile); + + this.logger.info(`Command ${commandId} marked as ${status}`); + } + } catch (error) { + this.logger.error(`Failed to mark command ${commandId} as processed:`, error.message); + } + } + + /** + * 清理旧的命令和响应文件 + * @param {number} maxAge - 最大保留时间(小时) + */ + cleanup(maxAge = 24) { + const cutoff = Date.now() - (maxAge * 60 * 60 * 1000); + let cleaned = 0; + + [this.commandsDir, this.responseDir].forEach(dir => { + try { + const files = fs.readdirSync(dir).filter(file => file.endsWith('.json')); + + for (const file of files) { + const filePath = path.join(dir, file); + const stats = fs.statSync(filePath); + + if (stats.mtime.getTime() < cutoff) { + fs.unlinkSync(filePath); + cleaned++; + } + } + } catch (error) { + this.logger.warn(`Failed to cleanup directory ${dir}:`, error.message); + } + }); + + if (cleaned > 0) { + this.logger.info(`Cleaned up ${cleaned} old command files`); + } + } + + /** + * 获取桥接器状态 + */ + getStatus() { + try { + const pendingCommands = this.getPendingCommands(); + const responseFiles = fs.readdirSync(this.responseDir).filter(f => f.endsWith('.json')); + + return { + pendingCommands: pendingCommands.length, + processedCommands: responseFiles.length, + commandsDir: this.commandsDir, + responseDir: this.responseDir, + recentCommands: pendingCommands.slice(-5).map(cmd => ({ + id: cmd.id, + command: cmd.command.substring(0, 50) + '...', + timestamp: cmd.timestamp + })) + }; + } catch (error) { + this.logger.error('Failed to get bridge status:', error.message); + return { + pendingCommands: 0, + processedCommands: 0, + error: error.message + }; + } + } +} + +module.exports = ClaudeCommandBridge; \ No newline at end of file diff --git a/src/relay/command-relay.js b/src/relay/command-relay.js new file mode 100644 index 0000000..cd40e57 --- /dev/null +++ b/src/relay/command-relay.js @@ -0,0 +1,513 @@ +/** + * Command Relay Service + * Manages email listening and command execution for Claude Code + */ + +const EventEmitter = require('events'); +const EmailListener = require('./email-listener'); +const ClaudeCommandBridge = require('./claude-command-bridge'); +const ClipboardAutomation = require('../automation/clipboard-automation'); +const SimpleAutomation = require('../automation/simple-automation'); +const ClaudeAutomation = require('../automation/claude-automation'); +const Logger = require('../core/logger'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +class CommandRelayService extends EventEmitter { + constructor(config) { + super(); + this.logger = new Logger('CommandRelay'); + this.config = config; + this.emailListener = null; + this.commandBridge = new ClaudeCommandBridge(); + this.clipboardAutomation = new ClipboardAutomation(); + this.simpleAutomation = new SimpleAutomation(); + this.claudeAutomation = new ClaudeAutomation(); + this.isRunning = false; + this.commandQueue = []; + this.processingQueue = false; + this.stateFile = path.join(__dirname, '../data/relay-state.json'); + + this._ensureDirectories(); + this._loadState(); + } + + _ensureDirectories() { + const dataDir = path.join(__dirname, '../data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + } + + _loadState() { + try { + if (fs.existsSync(this.stateFile)) { + const state = JSON.parse(fs.readFileSync(this.stateFile, 'utf8')); + this.commandQueue = state.commandQueue || []; + this.logger.debug(`Loaded ${this.commandQueue.length} queued commands`); + } + } catch (error) { + this.logger.warn('Failed to load relay state:', error.message); + this.commandQueue = []; + } + } + + _saveState() { + try { + const state = { + commandQueue: this.commandQueue, + lastSaved: new Date().toISOString() + }; + fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2)); + } catch (error) { + this.logger.error('Failed to save relay state:', error.message); + } + } + + async start() { + if (this.isRunning) { + this.logger.warn('Command relay service already running'); + return; + } + + try { + // 验证邮件配置 + if (!this.config.imap) { + throw new Error('IMAP configuration required for command relay'); + } + + // 启动邮件监听器 + this.emailListener = new EmailListener(this.config); + + // 监听命令事件 + this.emailListener.on('command', (commandData) => { + this._queueCommand(commandData); + }); + + // 启动邮件监听 + await this.emailListener.start(); + + // 启动命令处理 + this._startCommandProcessor(); + + this.isRunning = true; + this.logger.info('Command relay service started successfully'); + + // 发送启动通知 + this.emit('started'); + + } catch (error) { + this.logger.error('Failed to start command relay service:', error.message); + throw error; + } + } + + async stop() { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + + // 停止邮件监听器 + if (this.emailListener) { + await this.emailListener.stop(); + this.emailListener = null; + } + + // 保存状态 + this._saveState(); + + this.logger.info('Command relay service stopped'); + this.emit('stopped'); + } + + _queueCommand(commandData) { + const queueItem = { + id: this._generateId(), + ...commandData, + queuedAt: new Date().toISOString(), + status: 'queued', + retries: 0, + maxRetries: 3 + }; + + this.commandQueue.push(queueItem); + this._saveState(); + + this.logger.info(`Command queued:`, { + id: queueItem.id, + sessionId: queueItem.sessionId, + command: queueItem.command.substring(0, 50) + '...' + }); + + this.emit('commandQueued', queueItem); + } + + _startCommandProcessor() { + // 立即处理队列 + this._processCommandQueue(); + + // 定期处理队列 + setInterval(() => { + if (this.isRunning) { + this._processCommandQueue(); + } + }, 5000); // 每5秒检查一次 + } + + async _processCommandQueue() { + if (this.processingQueue || this.commandQueue.length === 0) { + return; + } + + this.processingQueue = true; + + try { + const pendingCommands = this.commandQueue.filter(cmd => cmd.status === 'queued'); + + for (const command of pendingCommands) { + try { + await this._executeCommand(command); + } catch (error) { + this.logger.error(`Failed to execute command ${command.id}:`, error.message); + this._handleCommandError(command, error); + } + } + } finally { + this.processingQueue = false; + } + } + + async _executeCommand(commandItem) { + this.logger.info(`Executing command ${commandItem.id}:`, { + sessionId: commandItem.sessionId, + command: commandItem.command.substring(0, 100) + }); + + commandItem.status = 'executing'; + commandItem.executedAt = new Date().toISOString(); + + try { + // 检查Claude Code进程是否运行 + const claudeProcess = await this._findClaudeCodeProcess(); + + if (!claudeProcess || !claudeProcess.available) { + throw new Error('Claude Code not available'); + } + + // 执行命令 - 使用多种方式尝试 + const success = await this._sendCommandToClaudeCode(commandItem.command, claudeProcess, commandItem.sessionId); + + if (success) { + commandItem.status = 'completed'; + commandItem.completedAt = new Date().toISOString(); + + // 更新会话命令计数 + if (this.emailListener) { + await this.emailListener.updateSessionCommandCount(commandItem.sessionId); + } + + this.logger.info(`Command ${commandItem.id} executed successfully`); + this.emit('commandExecuted', commandItem); + } else { + throw new Error('Failed to send command to Claude Code'); + } + + } catch (error) { + commandItem.status = 'failed'; + commandItem.error = error.message; + commandItem.failedAt = new Date().toISOString(); + + this.logger.error(`Command ${commandItem.id} failed:`, error.message); + this.emit('commandFailed', commandItem, error); + + throw error; + } finally { + this._saveState(); + } + } + + async _findClaudeCodeProcess() { + return new Promise((resolve) => { + // 查找Claude Code相关进程 + const ps = spawn('ps', ['aux']); + let output = ''; + + ps.stdout.on('data', (data) => { + output += data.toString(); + }); + + ps.on('close', (code) => { + const lines = output.split('\n'); + const claudeProcesses = lines.filter(line => + (line.includes('claude') || + line.includes('anthropic') || + line.includes('Claude') || + line.includes('node') && line.includes('claude')) && + !line.includes('grep') && + !line.includes('taskping') && + !line.includes('ps aux') + ); + + if (claudeProcesses.length > 0) { + // 解析进程ID + const processLine = claudeProcesses[0]; + const parts = processLine.trim().split(/\s+/); + const pid = parseInt(parts[1]); + + this.logger.debug('Found Claude Code process:', processLine); + resolve({ + pid, + command: processLine + }); + } else { + // 如果没找到进程,假设 Claude Code 可以通过桌面自动化访问 + this.logger.debug('No Claude Code process found, will try desktop automation'); + resolve({ pid: null, available: true }); + } + }); + + ps.on('error', (error) => { + this.logger.error('Error finding Claude Code process:', error.message); + // 即使出错,也尝试桌面自动化 + resolve({ pid: null, available: true }); + }); + }); + } + + async _sendCommandToClaudeCode(command, claudeProcess, sessionId) { + return new Promise(async (resolve) => { + try { + // 方法1: Claude Code 专用自动化 (最直接和可靠) + this.logger.info('Attempting to send command via Claude automation...'); + const claudeSuccess = await this.claudeAutomation.sendCommand(command, sessionId); + + if (claudeSuccess) { + this.logger.info('Command sent and executed successfully via Claude automation'); + resolve(true); + return; + } + + // 方法2: 剪贴板自动化 (需要权限) + if (this.clipboardAutomation.isSupported()) { + this.logger.info('Attempting to send command via clipboard automation...'); + const clipboardSuccess = await this.clipboardAutomation.sendCommand(command); + + if (clipboardSuccess) { + this.logger.info('Command sent successfully via clipboard automation'); + resolve(true); + return; + } + this.logger.warn('Clipboard automation failed, trying other methods...'); + } + + // 方法3: 简单自动化方案 (包含多种备选方案) + this.logger.info('Attempting to send command via simple automation...'); + const simpleSuccess = await this.simpleAutomation.sendCommand(command, sessionId); + + if (simpleSuccess) { + this.logger.info('Command sent successfully via simple automation'); + resolve(true); + return; + } + + // 方法4: 使用命令桥接器 (文件方式) + this.logger.info('Attempting to send command via bridge...'); + const bridgeSuccess = await this.commandBridge.sendCommand(command, sessionId); + + if (bridgeSuccess) { + this.logger.info('Command sent successfully via bridge'); + resolve(true); + return; + } + + // 方法5: 发送通知作为最后备选 + this.logger.info('Using notification as final fallback...'); + this._sendCommandViaNotification(command) + .then((success) => { + this.logger.info('Command notification sent as fallback'); + resolve(success); + }) + .catch(() => resolve(false)); + + } catch (error) { + this.logger.error('Error sending command to Claude Code:', error.message); + resolve(false); + } + }); + } + + async _sendCommandViaMacOS(command) { + return new Promise((resolve, reject) => { + // 使用AppleScript自动化输入到活动窗口 + const escapedCommand = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/'/g, "\\'"); + const script = ` + tell application "System Events" + -- 获取当前活动应用 + set activeApp to name of first application process whose frontmost is true + + -- 尝试找到 Claude Code、Terminal 或其他开发工具 + set targetApps to {"Claude Code", "Terminal", "iTerm2", "iTerm", "Visual Studio Code", "Code"} + set foundApp to null + + repeat with appName in targetApps + try + if application process appName exists then + set foundApp to application process appName + exit repeat + end if + end try + end repeat + + if foundApp is not null then + -- 切换到目标应用 + set frontmost of foundApp to true + delay 1 + + -- 发送命令 + keystroke "${escapedCommand}" + delay 0.3 + keystroke return + + return "success" + else + -- 如果没找到特定应用,尝试当前活动窗口 + keystroke "${escapedCommand}" + delay 0.3 + keystroke return + return "fallback" + end if + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + let error = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + osascript.stderr.on('data', (data) => { + error += data.toString(); + }); + + osascript.on('close', (code) => { + if (code === 0 && (output === 'success' || output === 'fallback')) { + this.logger.debug(`Command sent via macOS automation (${output})`); + resolve(true); + } else { + this.logger.warn('AppleScript failed:', { code, output, error }); + reject(new Error(`macOS automation failed: ${error || output}`)); + } + }); + + osascript.on('error', (error) => { + reject(error); + }); + }); + } + + async _sendCommandViaNotification(command) { + // 作为备选方案,发送桌面通知提醒用户 + return new Promise((resolve) => { + try { + const commandPreview = command.length > 50 ? command.substring(0, 50) + '...' : command; + + if (process.platform === 'darwin') { + // macOS 通知,包含更多信息 + const script = ` + display notification "命令: ${commandPreview.replace(/"/g, '\\"')}" with title "TaskPing - 邮件命令" subtitle "点击Terminal或Claude Code窗口,然后粘贴命令" sound name "default" + `; + const notification = spawn('osascript', ['-e', script]); + + notification.on('close', () => { + this.logger.info('macOS notification sent to user'); + resolve(true); + }); + + notification.on('error', () => { + resolve(false); + }); + } else { + // Linux 通知 + const notification = spawn('notify-send', [ + 'TaskPing - 邮件命令', + `命令: ${commandPreview}` + ]); + + notification.on('close', () => { + this.logger.info('Linux notification sent to user'); + resolve(true); + }); + + notification.on('error', () => { + resolve(false); + }); + } + + } catch (error) { + this.logger.warn('Failed to send notification:', error.message); + resolve(false); + } + }); + } + + _handleCommandError(commandItem, error) { + commandItem.retries = (commandItem.retries || 0) + 1; + + if (commandItem.retries < commandItem.maxRetries) { + // 重试 + commandItem.status = 'queued'; + commandItem.retryAt = new Date(Date.now() + (commandItem.retries * 60000)).toISOString(); // 延迟重试 + this.logger.info(`Command ${commandItem.id} will be retried (attempt ${commandItem.retries + 1})`); + } else { + // 达到最大重试次数 + commandItem.status = 'failed'; + this.logger.error(`Command ${commandItem.id} failed after ${commandItem.retries} retries`); + } + + this._saveState(); + } + + _generateId() { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + getStatus() { + return { + isRunning: this.isRunning, + queueLength: this.commandQueue.length, + processing: this.processingQueue, + emailListener: this.emailListener ? { + connected: this.emailListener.isConnected, + listening: this.emailListener.isListening + } : null, + recentCommands: this.commandQueue.slice(-5).map(cmd => ({ + id: cmd.id, + status: cmd.status, + queuedAt: cmd.queuedAt, + command: cmd.command.substring(0, 50) + '...' + })) + }; + } + + // 手动清理已完成的命令 + cleanupCompletedCommands() { + const beforeCount = this.commandQueue.length; + this.commandQueue = this.commandQueue.filter(cmd => + cmd.status !== 'completed' || + new Date(cmd.completedAt) > new Date(Date.now() - 24 * 60 * 60 * 1000) // 保留24小时内的记录 + ); + + const removedCount = beforeCount - this.commandQueue.length; + if (removedCount > 0) { + this.logger.info(`Cleaned up ${removedCount} completed commands`); + this._saveState(); + } + } +} + +module.exports = CommandRelayService; \ No newline at end of file diff --git a/src/relay/email-listener.js b/src/relay/email-listener.js new file mode 100644 index 0000000..171e98a --- /dev/null +++ b/src/relay/email-listener.js @@ -0,0 +1,436 @@ +/** + * Email Listener + * Monitors IMAP inbox for replies and extracts commands + */ + +const Imap = require('node-imap'); +const { simpleParser } = require('mailparser'); +const EventEmitter = require('events'); +const Logger = require('../core/logger'); +const fs = require('fs'); +const path = require('path'); + +class EmailListener extends EventEmitter { + constructor(config) { + super(); + this.logger = new Logger('EmailListener'); + this.config = config; + this.imap = null; + this.isConnected = false; + this.isListening = false; + this.sessionsDir = path.join(__dirname, '../data/sessions'); + this.checkInterval = (config.template?.checkInterval || 30) * 1000; // 转换为毫秒 + this.lastCheckTime = new Date(); + + this._ensureDirectories(); + } + + _ensureDirectories() { + if (!fs.existsSync(this.sessionsDir)) { + fs.mkdirSync(this.sessionsDir, { recursive: true }); + } + } + + async start() { + if (this.isListening) { + this.logger.warn('Email listener already running'); + return; + } + + try { + await this._connect(); + this._startListening(); + this.isListening = true; + this.logger.info('Email listener started successfully'); + } catch (error) { + this.logger.error('Failed to start email listener:', error.message); + throw error; + } + } + + async stop() { + if (!this.isListening) { + return; + } + + this.isListening = false; + + if (this.imap) { + this.imap.end(); + this.imap = null; + } + + this.isConnected = false; + this.logger.info('Email listener stopped'); + } + + async _connect() { + 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: 10000, + authTimeout: 5000, + keepalive: true + }); + + this.imap.once('ready', () => { + this.isConnected = true; + this.logger.debug('IMAP connection established'); + resolve(); + }); + + this.imap.once('error', (error) => { + this.logger.error('IMAP connection error:', error.message); + reject(error); + }); + + this.imap.once('end', () => { + this.isConnected = false; + this.logger.debug('IMAP connection ended'); + }); + + this.imap.connect(); + }); + } + + _startListening() { + // 定期检查新邮件 + this._checkNewMails(); + setInterval(() => { + if (this.isListening && this.isConnected) { + this._checkNewMails(); + } + }, this.checkInterval); + } + + async _checkNewMails() { + try { + await this._openInbox(); + await this._searchAndProcessMails(); + } catch (error) { + this.logger.error('Error checking emails:', error.message); + + // 如果连接断开,尝试重连 + if (!this.isConnected) { + this.logger.info('Attempting to reconnect...'); + try { + await this._connect(); + } catch (reconnectError) { + this.logger.error('Reconnection failed:', reconnectError.message); + } + } + } + } + + async _openInbox() { + return new Promise((resolve, reject) => { + this.imap.openBox('INBOX', false, (error, box) => { + if (error) { + reject(error); + } else { + resolve(box); + } + }); + }); + } + + async _searchAndProcessMails() { + return new Promise((resolve, reject) => { + // 搜索最近的未读邮件 + const searchCriteria = [ + 'UNSEEN', + ['SINCE', this.lastCheckTime] + ]; + + this.imap.search(searchCriteria, (searchError, results) => { + if (searchError) { + reject(searchError); + return; + } + + if (results.length === 0) { + resolve(); + return; + } + + this.logger.debug(`Found ${results.length} new emails`); + + const fetch = this.imap.fetch(results, { + bodies: '', + markSeen: true + }); + + fetch.on('message', (msg, seqno) => { + this._processMessage(msg, seqno); + }); + + fetch.once('error', (fetchError) => { + this.logger.error('Fetch error:', fetchError.message); + reject(fetchError); + }); + + fetch.once('end', () => { + this.lastCheckTime = new Date(); + resolve(); + }); + }); + }); + } + + _processMessage(msg, seqno) { + let buffer = ''; + + msg.on('body', (stream, info) => { + stream.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + }); + + stream.once('end', async () => { + try { + const parsed = await simpleParser(buffer); + await this._handleParsedEmail(parsed, seqno); + } catch (parseError) { + this.logger.error('Email parsing error:', parseError.message); + } + }); + }); + + msg.once('attributes', (attrs) => { + this.logger.debug(`Processing email ${seqno}:`, { + date: attrs.date, + flags: attrs.flags + }); + }); + + msg.once('end', () => { + this.logger.debug(`Finished processing email ${seqno}`); + }); + } + + async _handleParsedEmail(email, seqno) { + try { + // 检查是否是回复邮件 + if (!this._isReplyEmail(email)) { + this.logger.debug(`Email ${seqno} is not a TaskPing reply`); + return; + } + + // 提取会话ID + const sessionId = this._extractSessionId(email); + if (!sessionId) { + this.logger.warn(`No session ID found in email ${seqno}`); + return; + } + + // 验证会话 + const session = await this._validateSession(sessionId); + if (!session) { + this.logger.warn(`Invalid session ID in email ${seqno}: ${sessionId}`); + return; + } + + // 提取命令 + const command = this._extractCommand(email); + if (!command) { + this.logger.warn(`No command found in email ${seqno}`); + return; + } + + // 安全检查 + if (!this._isCommandSafe(command)) { + this.logger.warn(`Unsafe command in email ${seqno}: ${command}`); + return; + } + + // 发出命令事件 + this.emit('command', { + sessionId, + command: command.trim(), + email: { + from: email.from?.text, + subject: email.subject, + date: email.date + }, + session + }); + + this.logger.info(`Command extracted from email ${seqno}:`, { + sessionId, + command: command.substring(0, 100) + (command.length > 100 ? '...' : ''), + from: email.from?.text + }); + + } catch (error) { + this.logger.error(`Error handling email ${seqno}:`, error.message); + } + } + + _isReplyEmail(email) { + // 检查主题是否包含 TaskPing 标识 + const subject = email.subject || ''; + if (!subject.includes('[TaskPing]')) { + return false; + } + + // 检查是否是回复 (Re: 或 RE:) + const isReply = /^(Re:|RE:|回复:)/i.test(subject); + if (isReply) { + return true; + } + + // 检查邮件头中是否有会话ID + const sessionId = this._extractSessionId(email); + return !!sessionId; + } + + _extractSessionId(email) { + // 从邮件头中提取 + const headers = email.headers; + if (headers && headers.get('x-taskping-session-id')) { + return headers.get('x-taskping-session-id'); + } + + // 从邮件正文中提取 (作为备用方案) + const text = email.text || ''; + const sessionMatch = text.match(/会话ID:\s*([a-f0-9-]{36})/i); + if (sessionMatch) { + return sessionMatch[1]; + } + + // 从引用的邮件中提取 + const html = email.html || ''; + const htmlSessionMatch = html.match(/会话ID:\s*([a-f0-9-]{36})<\/code>/i); + if (htmlSessionMatch) { + return htmlSessionMatch[1]; + } + + return null; + } + + async _validateSession(sessionId) { + const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`); + + if (!fs.existsSync(sessionFile)) { + return null; + } + + try { + const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); + + // 检查会话是否过期 + const now = new Date(); + const expires = new Date(sessionData.expires); + + if (now > expires) { + this.logger.debug(`Session ${sessionId} has expired`); + // 删除过期会话 + fs.unlinkSync(sessionFile); + return null; + } + + // 检查命令数量限制 + if (sessionData.commandCount >= sessionData.maxCommands) { + this.logger.debug(`Session ${sessionId} has reached command limit`); + return null; + } + + return sessionData; + } catch (error) { + this.logger.error(`Error reading session ${sessionId}:`, error.message); + return null; + } + } + + _extractCommand(email) { + let text = email.text || ''; + + // 移除邮件签名和引用内容 + text = this._cleanEmailContent(text); + + // 移除空行和多余的空白字符 + text = text.replace(/\n\s*\n/g, '\n').trim(); + + return text; + } + + _cleanEmailContent(text) { + // 移除常见的邮件引用标记 + const lines = text.split('\n'); + const cleanLines = []; + let foundOriginalMessage = false; + + 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:')) { + foundOriginalMessage = true; + break; + } + + // 跳过常见的邮件签名 + if (line.includes('--') || + line.includes('Sent from') || + line.includes('发自我的')) { + break; + } + + cleanLines.push(line); + } + + return cleanLines.join('\n').trim(); + } + + _isCommandSafe(command) { + // 基本安全检查 + if (command.length > 1000) { + return false; + } + + // 危险命令黑名单 + const dangerousPatterns = [ + /rm\s+-rf/i, + /sudo\s+/i, + /chmod\s+777/i, + />\s*\/dev\/null/i, + /curl.*\|\s*sh/i, + /wget.*\|\s*sh/i, + /eval\s*\(/i, + /exec\s*\(/i + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(command)) { + return false; + } + } + + return true; + } + + async updateSessionCommandCount(sessionId) { + const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`); + + if (fs.existsSync(sessionFile)) { + try { + const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); + sessionData.commandCount = (sessionData.commandCount || 0) + 1; + sessionData.lastCommand = new Date().toISOString(); + + fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2)); + this.logger.debug(`Updated command count for session ${sessionId}: ${sessionData.commandCount}`); + } catch (error) { + this.logger.error(`Error updating session ${sessionId}:`, error.message); + } + } + } +} + +module.exports = EmailListener; \ No newline at end of file diff --git a/src/simplified/email-automation.js b/src/simplified/email-automation.js new file mode 100644 index 0000000..37e2f8d --- /dev/null +++ b/src/simplified/email-automation.js @@ -0,0 +1,406 @@ +/** + * 简化版邮件自动化 + * 专注于解决核心问题,减少复杂性 + */ + +const Imap = require('node-imap'); +const { simpleParser } = require('mailparser'); +const { spawn } = require('child_process'); +const EventEmitter = require('events'); +const fs = require('fs'); +const path = require('path'); + +class SimplifiedEmailAutomation extends EventEmitter { + constructor(config) { + super(); + this.config = config; + this.imap = null; + this.isRunning = false; + this.sessionsDir = path.join(__dirname, '../data/sessions'); + this.commandFile = path.join(__dirname, '../data/latest_command.txt'); + + // 增加超时时间解决连接问题 + this.imapConfig = { + user: config.imap.auth.user, + password: config.imap.auth.pass, + host: config.imap.host, + port: config.imap.port, + tls: config.imap.secure, + connTimeout: 60000, // 增加到60秒 + authTimeout: 30000, // 增加到30秒 + keepalive: true, + debug: process.env.DEBUG_IMAP === 'true' + }; + + this._ensureDirectories(); + } + + _ensureDirectories() { + [this.sessionsDir, path.dirname(this.commandFile)].forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + }); + } + + async start() { + console.log('🚀 启动简化邮件自动化服务...'); + + if (this.isRunning) { + console.log('⚠️ 服务已在运行中'); + return; + } + + try { + await this._connectWithRetry(); + this._startListening(); + this.isRunning = true; + console.log('✅ 邮件监听服务启动成功'); + console.log(`📧 监听邮箱: ${this.config.imap.auth.user}`); + console.log('💡 现在可以回复 TaskPing 邮件来发送命令了'); + } catch (error) { + console.error('❌ 启动失败:', error.message); + throw error; + } + } + + async _connectWithRetry(maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + console.log(`🔄 尝试连接 IMAP (${i + 1}/${maxRetries})...`); + await this._connect(); + console.log('✅ IMAP 连接成功'); + return; + } catch (error) { + console.log(`❌ 连接失败 (${i + 1}/${maxRetries}): ${error.message}`); + if (i === maxRetries - 1) throw error; + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } + } + + async _connect() { + return new Promise((resolve, reject) => { + this.imap = new Imap(this.imapConfig); + + this.imap.once('ready', () => { + console.log('📬 IMAP 连接就绪'); + resolve(); + }); + + this.imap.once('error', (error) => { + console.error('📬 IMAP 连接错误:', error.message); + reject(error); + }); + + this.imap.once('end', () => { + console.log('📬 IMAP 连接结束'); + if (this.isRunning) { + console.log('🔄 尝试重新连接...'); + setTimeout(() => this._connectWithRetry().catch(console.error), 10000); + } + }); + + this.imap.connect(); + }); + } + + _startListening() { + console.log('👂 开始监听新邮件...'); + + // 立即检查一次 + this._checkNewEmails(); + + // 定期检查(每30秒) + setInterval(() => { + if (this.isRunning) { + this._checkNewEmails(); + } + }, 30000); + } + + async _checkNewEmails() { + try { + await this._openInbox(); + + // 只查找最近1小时内的未读邮件 + const yesterday = new Date(); + yesterday.setHours(yesterday.getHours() - 1); + + const searchCriteria = [ + 'UNSEEN', + ['SINCE', yesterday] + ]; + + this.imap.search(searchCriteria, (err, results) => { + if (err) { + console.error('🔍 搜索邮件失败:', err.message); + return; + } + + if (results.length === 0) { + return; // 没有新邮件 + } + + 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, 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._handleEmail(parsed, seqno); + } catch (error) { + console.error(`❌ 处理邮件 ${seqno} 失败:`, error.message); + } + }); + }); + }); + + fetch.once('error', (err) => { + console.error('📧 获取邮件失败:', err.message); + }); + } + + async _handleEmail(email, seqno) { + console.log(`📨 处理邮件 ${seqno}: ${email.subject}`); + + // 简化的 TaskPing 邮件检查 + if (!this._isTaskPingReply(email)) { + console.log(`📨 邮件 ${seqno} 不是 TaskPing 回复,跳过`); + return; + } + + // 提取命令内容 + const command = this._extractCommand(email); + if (!command || command.trim().length === 0) { + console.log(`📨 邮件 ${seqno} 没有找到有效命令`); + return; + } + + console.log(`🎯 提取到命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`); + + // 执行命令 + await this._executeCommand(command, seqno); + } + + _isTaskPingReply(email) { + const subject = email.subject || ''; + + // 检查是否是 TaskPing 相关邮件 + return subject.includes('[TaskPing]') || + subject.match(/^(Re:|RE:|回复:)/i); + } + + _extractCommand(email) { + let text = email.text || ''; + + // 清理邮件内容 - 移除引用和签名 + const lines = text.split('\n'); + const cleanLines = []; + + 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('Session ID:')) { + break; + } + + // 跳过签名 + if (line.includes('--') || + line.includes('Sent from') || + line.includes('发自我的')) { + break; + } + + cleanLines.push(line); + } + + return cleanLines.join('\n').trim(); + } + + async _executeCommand(command, emailSeq) { + try { + console.log(`🚀 执行命令 (来自邮件 ${emailSeq})`); + + // 保存最新命令到文件 + await this._saveCommand(command, emailSeq); + + // 复制到剪贴板 + const clipboardSuccess = await this._copyToClipboard(command); + + // 发送通知 + const notificationSuccess = await this._sendNotification(command); + + // 尝试简单的自动粘贴 + const pasteSuccess = await this._attemptSimplePaste(); + + console.log('📊 执行结果:'); + console.log(` 📄 文件保存: ${await this._saveCommand(command, emailSeq) ? '✅' : '❌'}`); + console.log(` 📋 剪贴板: ${clipboardSuccess ? '✅' : '❌'}`); + console.log(` 🔔 通知: ${notificationSuccess ? '✅' : '❌'}`); + console.log(` 🤖 自动粘贴: ${pasteSuccess ? '✅' : '❌'}`); + + this.emit('commandExecuted', { + command, + emailSeq, + success: clipboardSuccess || notificationSuccess + }); + + } catch (error) { + console.error('❌ 命令执行失败:', error.message); + this.emit('commandFailed', { command, emailSeq, error }); + } + } + + async _saveCommand(command, emailSeq) { + try { + const content = `# TaskPing 邮件命令 (邮件 ${emailSeq}) +# 时间: ${new Date().toLocaleString('zh-CN')} +# +# 命令内容: +${command} + +# ============================== +# 说明: 这个命令来自邮件回复 +# 使用: 复制上面的命令到 Claude Code 中执行 +`; + fs.writeFileSync(this.commandFile, content, 'utf8'); + return true; + } catch (error) { + console.error('保存命令文件失败:', error.message); + return false; + } + } + + async _copyToClipboard(command) { + try { + const pbcopy = spawn('pbcopy'); + pbcopy.stdin.write(command); + pbcopy.stdin.end(); + + return new Promise((resolve) => { + pbcopy.on('close', (code) => resolve(code === 0)); + pbcopy.on('error', () => resolve(false)); + }); + } catch (error) { + return false; + } + } + + async _sendNotification(command) { + try { + const shortCommand = command.length > 60 ? command.substring(0, 60) + '...' : command; + + const script = ` + display notification "新邮件命令已复制到剪贴板!请到 Claude Code 中粘贴执行" with title "TaskPing" subtitle "命令: ${shortCommand.replace(/"/g, '\\"')}" 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)); + }); + } catch (error) { + return false; + } + } + + async _attemptSimplePaste() { + try { + // 只在明确是开发环境的应用时尝试自动粘贴 + const script = ` + tell application "System Events" + try + set frontApp to name of first application process whose frontmost is true + + -- 只在特定应用中尝试自动粘贴 + if frontApp contains "Claude" or frontApp contains "Terminal" or frontApp contains "iTerm" then + delay 0.5 + keystroke "v" using command down + delay 0.2 + keystroke return + return "success" + else + return "skip" + end if + on error + return "failed" + end try + end tell + `; + + const osascript = spawn('osascript', ['-e', script]); + let output = ''; + + osascript.stdout.on('data', (data) => { + output += data.toString().trim(); + }); + + return new Promise((resolve) => { + osascript.on('close', (code) => { + resolve(code === 0 && output === 'success'); + }); + osascript.on('error', () => resolve(false)); + }); + } catch (error) { + return false; + } + } + + async stop() { + console.log('🛑 停止邮件监听服务...'); + this.isRunning = false; + + if (this.imap) { + this.imap.end(); + this.imap = null; + } + + console.log('✅ 服务已停止'); + } + + getStatus() { + return { + running: this.isRunning, + connected: this.imap && this.imap.state === 'authenticated', + commandFile: this.commandFile, + lastCommandExists: fs.existsSync(this.commandFile) + }; + } +} + +module.exports = SimplifiedEmailAutomation; \ No newline at end of file diff --git a/src/tools/config-manager.js b/src/tools/config-manager.js new file mode 100644 index 0000000..d27db22 --- /dev/null +++ b/src/tools/config-manager.js @@ -0,0 +1,508 @@ +/** + * TaskPing Configuration Manager + * Interactive configuration tool for managing settings + */ + +const readline = require('readline'); +const Logger = require('../core/logger'); + +class ConfigurationManager { + constructor(configManager) { + this.config = configManager; + this.logger = new Logger('ConfigManager'); + + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + // Enable keypress events + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + readline.emitKeypressEvents(process.stdin, this.rl); + } + + async run(args = []) { + if (args.includes('--help') || args.includes('-h')) { + this.showHelp(); + this.rl.close(); + return; + } + + if (args.includes('--show')) { + this.displayCurrentConfig(); + this.rl.close(); + return; + } + + await this.showMainMenu(); + } + + async question(prompt) { + return new Promise((resolve) => { + this.rl.question(prompt, resolve); + }); + } + + displayCurrentConfig() { + console.log('\n当前配置:'); + console.log('├─ 语言:', this.config.get('language')); + console.log('├─ 启用状态:', this.config.get('enabled') ? '启用' : '禁用'); + console.log('├─ 超时时间:', this.config.get('timeout') + '秒'); + console.log('├─ 完成提示音:', this.config.get('sound.completed')); + console.log('└─ 等待提示音:', this.config.get('sound.waiting')); + console.log(); + } + + async showMainMenu() { + while (true) { + console.log('\n=== TaskPing 配置管理器 ==='); + this.displayCurrentConfig(); + console.log('选项:'); + console.log('1. 基础设置'); + console.log('2. 音效配置'); + console.log('3. 通知渠道'); + console.log('4. 命令中继'); + console.log('5. 测试通知'); + console.log('6. 保存并退出'); + console.log('7. 退出(不保存)'); + + const choice = await this.question('\n请选择 (1-7): '); + + switch (choice) { + case '1': + await this.configureBasicSettings(); + break; + case '2': + await this.configureSounds(); + break; + case '3': + await this.configureChannels(); + break; + case '4': + await this.configureRelay(); + break; + case '5': + await this.testNotifications(); + break; + case '6': + if (this.config.save()) { + console.log('✅ 配置已保存'); + this.rl.close(); + return; + } else { + console.log('❌ 保存失败'); + } + break; + case '7': + console.log('退出(未保存更改)'); + this.rl.close(); + return; + default: + console.log('❌ 无效选择'); + } + } + } + + async configureBasicSettings() { + console.log('\n=== 基础设置 ==='); + console.log('1. 配置语言'); + console.log('2. 切换启用状态'); + console.log('3. 配置超时时间'); + console.log('4. 自定义消息'); + console.log('0. 返回主菜单'); + + const choice = await this.question('\n请选择 (0-4): '); + + switch (choice) { + case '1': + await this.configureLanguage(); + break; + case '2': + await this.toggleEnabled(); + break; + case '3': + await this.configureTimeout(); + break; + case '4': + await this.configureCustomMessages(); + break; + case '0': + return; + default: + console.log('❌ 无效选择'); + } + } + + async configureLanguage() { + const languages = ['zh-CN', 'en', 'ja']; + console.log('\n可用语言:'); + languages.forEach((lang, index) => { + console.log(`${index + 1}. ${lang}`); + }); + + const choice = await this.question(`选择语言 (1-${languages.length}): `); + const index = parseInt(choice) - 1; + + if (index >= 0 && index < languages.length) { + this.config.set('language', languages[index]); + console.log(`✅ 语言已设置为: ${languages[index]}`); + } else { + console.log('❌ 无效选择'); + } + } + + async toggleEnabled() { + const current = this.config.get('enabled', true); + this.config.set('enabled', !current); + console.log(`✅ 通知已${!current ? '启用' : '禁用'}`); + } + + async configureTimeout() { + const timeout = await this.question('设置超时时间(秒): '); + const timeoutNum = parseInt(timeout); + if (timeoutNum > 0 && timeoutNum <= 30) { + this.config.set('timeout', timeoutNum); + console.log(`✅ 超时时间已设置为: ${timeoutNum}秒`); + } else { + console.log('❌ 无效的超时时间 (1-30秒)'); + } + } + + async configureSounds() { + // Load desktop channel to get available sounds + const DesktopChannel = require('../channels/local/desktop'); + const desktop = new DesktopChannel(); + const soundCategories = desktop.getAvailableSounds(); + + console.log('\n=== 音效配置 ==='); + + // Configure completed sound + console.log('\n--- 配置任务完成提示音 ---'); + const completedSound = await this.selectSoundFromCategories(soundCategories, '任务完成'); + if (completedSound) { + this.config.set('sound.completed', completedSound); + console.log(`✅ 任务完成提示音已设置为: ${completedSound}`); + } + + // Configure waiting sound + console.log('\n--- 配置等待输入提示音 ---'); + const waitingSound = await this.selectSoundFromCategories(soundCategories, '等待输入'); + if (waitingSound) { + this.config.set('sound.waiting', waitingSound); + console.log(`✅ 等待输入提示音已设置为: ${waitingSound}`); + } + } + + async selectSoundFromCategories(soundCategories, type) { + const categories = Object.keys(soundCategories); + + console.log(`\n选择${type}音效分类:`); + categories.forEach((category, index) => { + const count = soundCategories[category].length; + console.log(`${index + 1}. ${category} (${count}个音效)`); + }); + console.log('0. 跳过'); + + const choice = await this.question(`\n请选择分类 (0-${categories.length}): `); + const index = parseInt(choice) - 1; + + if (choice === '0') { + return null; + } + + if (index >= 0 && index < categories.length) { + const category = categories[index]; + const sounds = soundCategories[category]; + return await this.selectSoundFromList(sounds, type); + } else { + console.log('❌ 无效选择'); + return null; + } + } + + async selectSoundFromList(sounds, type) { + console.log(`\n选择${type}提示音:`); + sounds.forEach((sound, index) => { + console.log(`${index + 1}. ${sound}`); + }); + console.log('0. 返回分类选择'); + + const choice = await this.question(`\n请选择 (0-${sounds.length}): `); + const index = parseInt(choice) - 1; + + if (choice === '0') { + return null; + } + + if (index >= 0 && index < sounds.length) { + const selectedSound = sounds[index]; + + // Play sound preview + try { + const DesktopChannel = require('../channels/local/desktop'); + const desktop = new DesktopChannel(); + desktop._playSound(selectedSound); + console.log(`播放音效: ${selectedSound}`); + } catch (error) { + // Ignore playback errors + } + + const confirm = await this.question('确认使用这个音效吗? (y/n): '); + if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') { + return selectedSound; + } + } else { + console.log('❌ 无效选择'); + } + + return null; + } + + async configureChannels() { + console.log('\n=== 通知渠道配置 ==='); + console.log('1. 桌面通知 (已启用)'); + console.log('2. 邮件通知'); + console.log('3. Discord通知 (即将支持)'); + console.log('4. Telegram通知 (即将支持)'); + console.log('5. WhatsApp通知 (即将支持)'); + console.log('6. 飞书通知 (即将支持)'); + console.log('0. 返回主菜单'); + + const choice = await this.question('\n请选择要配置的渠道 (0-6): '); + + switch (choice) { + case '1': + console.log('\n桌面通知已启用且工作正常!'); + break; + case '2': + await this.configureEmailChannel(); + break; + case '3': + case '4': + case '5': + case '6': + console.log('\n此渠道即将在后续版本中支持!'); + break; + case '0': + return; + default: + console.log('❌ 无效选择'); + } + + if (choice !== '0') { + await this.question('\n按回车继续...'); + } + } + + async configureRelay() { + console.log('\n=== 命令中继配置 ==='); + console.log('(此功能将在后续版本中实现)'); + console.log('将支持通过通知渠道发送命令,自动在Claude Code中执行'); + + await this.question('\n按回车继续...'); + } + + async configureCustomMessages() { + console.log('\n=== 自定义消息配置 ==='); + console.log('提示:使用 {project} 作为项目名占位符'); + console.log('示例:[{project}] 任务已完成!\n'); + + // Configure completed message + const currentCompleted = this.config.get('customMessages.completed') || '使用默认文本'; + console.log(`当前任务完成文本: ${currentCompleted}`); + const completedMsg = await this.question('新的任务完成文本 (回车跳过): '); + if (completedMsg.trim()) { + this.config.set('customMessages.completed', completedMsg.trim()); + console.log('✅ 已更新任务完成文本'); + } + + // Configure waiting message + const currentWaiting = this.config.get('customMessages.waiting') || '使用默认文本'; + console.log(`\n当前等待输入文本: ${currentWaiting}`); + const waitingMsg = await this.question('新的等待输入文本 (回车跳过): '); + if (waitingMsg.trim()) { + this.config.set('customMessages.waiting', waitingMsg.trim()); + console.log('✅ 已更新等待输入文本'); + } + } + + async testNotifications() { + console.log('\n=== 测试通知 ==='); + + try { + const Notifier = require('../core/notifier'); + const notifier = new Notifier(this.config); + await notifier.initializeChannels(); + + console.log('发送任务完成通知...'); + await notifier.notify('completed', { test: true }); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + console.log('发送等待输入通知...'); + await notifier.notify('waiting', { test: true }); + + console.log('✅ 测试完成'); + } catch (error) { + console.error('❌ 测试失败:', error.message); + } + + await this.question('\n按回车继续...'); + } + + async configureEmailChannel() { + console.log('\n=== 邮件通知配置 ==='); + + // 获取当前邮件配置 + const currentEmailConfig = this.config.getChannel('email') || { enabled: false, config: {} }; + const emailConfig = currentEmailConfig.config || {}; + + console.log(`当前状态: ${currentEmailConfig.enabled ? '✅ 已启用' : '❌ 已禁用'}`); + + console.log('\n📧 SMTP 发送配置:'); + + // SMTP 主机配置 + const currentHost = emailConfig.smtp?.host || ''; + console.log(`当前 SMTP 主机: ${currentHost || '未配置'}`); + const smtpHost = await this.question('SMTP 主机 (如: smtp.gmail.com): '); + + // SMTP 端口配置 + const currentPort = emailConfig.smtp?.port || 587; + console.log(`当前 SMTP 端口: ${currentPort}`); + const smtpPortInput = await this.question('SMTP 端口 (默认 587): '); + const smtpPort = parseInt(smtpPortInput) || 587; + + // 安全连接配置 + const currentSecure = emailConfig.smtp?.secure || false; + console.log(`当前安全连接: ${currentSecure ? 'SSL/TLS' : 'STARTTLS'}`); + const secureInput = await this.question('使用 SSL/TLS? (y/n,默认n): '); + const secure = secureInput.toLowerCase() === 'y'; + + // 用户名配置 + const currentUser = emailConfig.smtp?.auth?.user || ''; + console.log(`当前用户名: ${currentUser || '未配置'}`); + const smtpUser = await this.question('SMTP 用户名 (邮箱地址): '); + + // 密码配置 + console.log('SMTP 密码: [隐藏]'); + const smtpPass = await this.question('SMTP 密码 (应用密码): '); + + console.log('\n📥 IMAP 接收配置 (用于接收回复):'); + + // IMAP 主机配置 + const currentImapHost = emailConfig.imap?.host || ''; + console.log(`当前 IMAP 主机: ${currentImapHost || '未配置'}`); + const imapHost = await this.question('IMAP 主机 (如: imap.gmail.com): '); + + // IMAP 端口配置 + const currentImapPort = emailConfig.imap?.port || 993; + console.log(`当前 IMAP 端口: ${currentImapPort}`); + const imapPortInput = await this.question('IMAP 端口 (默认 993): '); + const imapPort = parseInt(imapPortInput) || 993; + + // IMAP 安全连接 + const currentImapSecure = emailConfig.imap?.secure !== false; + const imapSecureInput = await this.question('IMAP 使用 SSL? (y/n,默认y): '); + const imapSecure = imapSecureInput.toLowerCase() !== 'n'; + + // 收件人配置 + console.log('\n📬 收件人配置:'); + const currentTo = emailConfig.to || ''; + console.log(`当前收件人: ${currentTo || '未配置'}`); + const toEmail = await this.question('收件人邮箱: '); + + // 发件人配置 + const currentFrom = emailConfig.from || ''; + console.log(`当前发件人: ${currentFrom || '未配置'}`); + const fromEmail = await this.question(`发件人显示名 (默认: TaskPing <${smtpUser}>): `); + + // 构建邮件配置 + const newEmailConfig = { + enabled: true, + config: { + smtp: { + host: smtpHost || currentHost, + port: smtpPort, + secure: secure, + auth: { + user: smtpUser || currentUser, + pass: smtpPass || emailConfig.smtp?.auth?.pass || '' + } + }, + imap: { + host: imapHost || currentImapHost, + port: imapPort, + secure: imapSecure, + auth: { + user: smtpUser || currentUser, + pass: smtpPass || emailConfig.imap?.auth?.pass || '' + } + }, + from: fromEmail || `TaskPing <${smtpUser || currentUser}>`, + to: toEmail || currentTo + } + }; + + // 保存配置 + this.config.setChannel('email', newEmailConfig); + console.log('\n✅ 邮件配置已保存'); + + // 询问是否测试 + const testChoice = await this.question('\n测试邮件发送? (y/n): '); + if (testChoice.toLowerCase() === 'y') { + await this.testEmailChannel(); + } + } + + async testEmailChannel() { + console.log('\n🧪 测试邮件发送...'); + + try { + const EmailChannel = require('../channels/email/smtp'); + const emailConfig = this.config.getChannel('email'); + + if (!emailConfig || !emailConfig.enabled) { + console.log('❌ 邮件渠道未启用'); + return; + } + + const emailChannel = new EmailChannel(emailConfig.config); + const testResult = await emailChannel.test(); + + if (testResult) { + console.log('✅ 邮件发送测试成功!'); + console.log('📧 请检查您的邮箱,应该收到一封测试邮件'); + console.log('💡 您可以尝试回复该邮件来测试命令中继功能'); + } else { + console.log('❌ 邮件发送测试失败'); + console.log('请检查您的 SMTP 配置是否正确'); + } + } catch (error) { + console.log('❌ 邮件测试失败:', error.message); + console.log('请检查您的网络连接和邮件配置'); + } + } + + showHelp() { + console.log(` +TaskPing Configuration Manager + +Usage: taskping config [options] + +Options: + --show 显示当前配置 + --help 显示帮助信息 + +Interactive Commands: + 1. 基础设置 - 语言、启用状态、超时时间等 + 2. 音效配置 - 配置任务完成和等待输入的提示音 + 3. 通知渠道 - 配置邮件、Discord、Telegram等通知渠道 + 4. 命令中继 - 配置远程命令执行功能 + 5. 测试通知 - 测试所有配置的通知渠道 + `); + } +} + +module.exports = ConfigurationManager; \ No newline at end of file diff --git a/src/tools/installer.js b/src/tools/installer.js new file mode 100644 index 0000000..31b62dd --- /dev/null +++ b/src/tools/installer.js @@ -0,0 +1,248 @@ +/** + * TaskPing Installer + * Handles installation and configuration of Claude Code hooks + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const readline = require('readline'); +const Logger = require('../core/logger'); + +class Installer { + constructor(configManager) { + this.config = configManager; + this.logger = new Logger('Installer'); + this.projectDir = path.join(__dirname, '../..'); + this.claudeConfigDir = this.getClaudeConfigDir(); + + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + } + + getClaudeConfigDir() { + const homeDir = os.homedir(); + + // Common Claude Code configuration paths + const possiblePaths = [ + path.join(homeDir, '.claude'), + path.join(homeDir, '.config', 'claude'), + path.join(homeDir, 'Library', 'Application Support', 'Claude'), + path.join(homeDir, 'AppData', 'Roaming', 'Claude') + ]; + + for (const configPath of possiblePaths) { + if (fs.existsSync(configPath)) { + return configPath; + } + } + + // Default fallback + return path.join(homeDir, '.claude'); + } + + async question(prompt) { + return new Promise((resolve) => { + this.rl.question(prompt, resolve); + }); + } + + async run(args = []) { + console.log('=== TaskPing Claude Code 安装器 ===\n'); + + // Check dependencies + if (!this.checkDependencies()) { + console.log('\n请先安装必要的依赖'); + this.rl.close(); + return; + } + + console.log(`\nClaude Code 配置目录: ${this.claudeConfigDir}`); + + const proceed = await this.question('\n继续安装? (y/n): '); + if (proceed.toLowerCase() !== 'y' && proceed.toLowerCase() !== 'yes') { + console.log('安装已取消'); + this.rl.close(); + return; + } + + // Install hooks + const hookSuccess = await this.installHooks(); + if (!hookSuccess) { + this.rl.close(); + return; + } + + // Initialize configuration + await this.initializeConfig(); + + // Test installation + const testChoice = await this.question('\n测试安装? (y/n): '); + if (testChoice.toLowerCase() === 'y' || testChoice.toLowerCase() === 'yes') { + await this.testInstallation(); + } + + this.displayUsage(); + this.rl.close(); + } + + checkDependencies() { + console.log('检查依赖...'); + + // Check Node.js + try { + const nodeVersion = process.version; + console.log(`✅ Node.js ${nodeVersion}`); + } catch (error) { + console.log('❌ Node.js 未安装'); + return false; + } + + // Check platform-specific notification tools + const platform = process.platform; + switch (platform) { + case 'darwin': + console.log('✅ macOS 通知支持'); + break; + case 'linux': + console.log('ℹ️ Linux 系统,请确保安装 libnotify-bin'); + break; + case 'win32': + console.log('✅ Windows 通知支持'); + break; + default: + console.log(`⚠️ 平台 ${platform} 可能不完全支持`); + } + + return true; + } + + createHooksConfig() { + const taskpingPath = path.join(this.projectDir, 'taskping.js'); + + return { + hooks: { + Stop: [ + { + matcher: "*", + hooks: [ + { + type: "command", + command: `node "${taskpingPath}" notify --type completed`, + timeout: 5 + } + ] + } + ], + SubagentStop: [ + { + matcher: "*", + hooks: [ + { + type: "command", + command: `node "${taskpingPath}" notify --type waiting`, + timeout: 5 + } + ] + } + ] + } + }; + } + + async installHooks() { + console.log('\n安装 Claude Code hooks...'); + + // Create config directory if it doesn't exist + if (!fs.existsSync(this.claudeConfigDir)) { + fs.mkdirSync(this.claudeConfigDir, { recursive: true }); + console.log(`✅ 创建配置目录: ${this.claudeConfigDir}`); + } + + const settingsPath = path.join(this.claudeConfigDir, 'settings.json'); + let settings = {}; + + // Load existing settings + if (fs.existsSync(settingsPath)) { + try { + const content = fs.readFileSync(settingsPath, 'utf8'); + settings = JSON.parse(content); + console.log('✅ 读取现有 Claude Code 设置'); + } catch (error) { + console.log('⚠️ 无法解析现有设置,将创建新的配置'); + } + } + + // Create or update hooks configuration + const hooksConfig = this.createHooksConfig(); + + // Merge with existing settings + settings.hooks = { + ...settings.hooks, + ...hooksConfig.hooks + }; + + // Save updated settings + try { + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + console.log(`✅ Claude Code hooks 已安装到: ${settingsPath}`); + return true; + } catch (error) { + console.error(`❌ 安装失败: ${error.message}`); + return false; + } + } + + async initializeConfig() { + console.log('\n初始化配置...'); + + // Load and save default configuration + this.config.load(); + this.config.save(); + + console.log('✅ 配置文件已初始化'); + } + + async testInstallation() { + console.log('\n测试安装...'); + + try { + const TaskPingCLI = require('../../taskping'); + const cli = new TaskPingCLI(); + await cli.init(); + + console.log('测试任务完成通知...'); + await cli.handleNotify(['--type', 'completed']); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + console.log('测试等待输入通知...'); + await cli.handleNotify(['--type', 'waiting']); + + console.log('✅ 测试成功!'); + return true; + } catch (error) { + console.error(`❌ 测试失败: ${error.message}`); + return false; + } + } + + displayUsage() { + console.log('\n=== 安装完成 ==='); + console.log(''); + console.log('现在当您使用 Claude Code 时:'); + console.log('• 任务完成时会收到通知'); + console.log('• Claude 等待输入时会收到提醒'); + console.log(''); + console.log('常用命令:'); + console.log(` node "${path.join(this.projectDir, 'taskping.js')}" config`); + console.log(` node "${path.join(this.projectDir, 'taskping.js')}" test`); + console.log(` node "${path.join(this.projectDir, 'taskping.js')}" status`); + console.log(''); + console.log('如需卸载,请手动删除 Claude Code 设置中的 hooks 配置。'); + } +} + +module.exports = Installer; \ No newline at end of file diff --git a/taskping-config.js b/taskping-config.js new file mode 100755 index 0000000..342ec3f --- /dev/null +++ b/taskping-config.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +const ConfigManager = require('./src/config-manager'); + +const manager = new ConfigManager(); +manager.interactiveMenu().catch(console.error); \ No newline at end of file diff --git a/taskping.js b/taskping.js new file mode 100755 index 0000000..ae2fb87 --- /dev/null +++ b/taskping.js @@ -0,0 +1,931 @@ +#!/usr/bin/env node + +/** + * TaskPing - Claude Code Smart Notification System + * Main entry point for the CLI tool + */ + +const Logger = require('./src/core/logger'); +const Notifier = require('./src/core/notifier'); +const ConfigManager = require('./src/core/config'); + +class TaskPingCLI { + constructor() { + this.logger = new Logger('CLI'); + this.config = new ConfigManager(); + this.notifier = new Notifier(this.config); + } + + async init() { + // Load configuration + this.config.load(); + + // Initialize channels + await this.notifier.initializeChannels(); + } + + async run() { + const args = process.argv.slice(2); + const command = args[0]; + + try { + await this.init(); + + switch (command) { + case 'notify': + await this.handleNotify(args.slice(1)); + break; + case 'test': + await this.handleTest(args.slice(1)); + break; + case 'status': + await this.handleStatus(args.slice(1)); + break; + case 'config': + await this.handleConfig(args.slice(1)); + break; + case 'install': + await this.handleInstall(args.slice(1)); + break; + case 'relay': + await this.handleRelay(args.slice(1)); + break; + case 'edit-config': + await this.handleEditConfig(args.slice(1)); + break; + case 'setup-email': + await this.handleSetupEmail(args.slice(1)); + break; + case 'daemon': + await this.handleDaemon(args.slice(1)); + break; + case 'commands': + await this.handleCommands(args.slice(1)); + break; + case 'test-paste': + await this.handleTestPaste(args.slice(1)); + break; + case 'test-simple': + await this.handleTestSimple(args.slice(1)); + break; + case 'test-claude': + await this.handleTestClaude(args.slice(1)); + break; + case 'setup-permissions': + await this.handleSetupPermissions(args.slice(1)); + break; + case 'diagnose': + await this.handleDiagnose(args.slice(1)); + break; + case '--help': + case '-h': + case undefined: + this.showHelp(); + break; + default: + console.error(`Unknown command: ${command}`); + this.showHelp(); + process.exit(1); + } + } catch (error) { + this.logger.error('CLI error:', error.message); + process.exit(1); + } + } + + async handleNotify(args) { + const typeIndex = args.findIndex(arg => arg === '--type'); + + if (typeIndex === -1 || typeIndex + 1 >= args.length) { + console.error('Usage: taskping notify --type '); + process.exit(1); + } + + const type = args[typeIndex + 1]; + + if (!['completed', 'waiting'].includes(type)) { + console.error('Invalid type. Use: completed or waiting'); + process.exit(1); + } + + const result = await this.notifier.notify(type); + + if (result.success) { + this.logger.info(`${type} notification sent successfully`); + process.exit(0); + } else { + this.logger.error(`Failed to send ${type} notification`); + process.exit(1); + } + } + + async handleTest(args) { + console.log('Testing notification channels...\n'); + + const results = await this.notifier.test(); + + for (const [channel, result] of Object.entries(results)) { + const status = result.success ? '✅ PASS' : '❌ FAIL'; + console.log(`${channel}: ${status}`); + if (result.error) { + console.log(` Error: ${result.error}`); + } + } + + const passCount = Object.values(results).filter(r => r.success).length; + const totalCount = Object.keys(results).length; + + console.log(`\nTest completed: ${passCount}/${totalCount} channels passed`); + + if (passCount === 0) { + process.exit(1); + } + } + + async handleStatus(args) { + const status = this.notifier.getStatus(); + + console.log('TaskPing Status\n'); + console.log('Configuration:'); + console.log(` Enabled: ${status.enabled ? 'Yes' : 'No'}`); + console.log(` Language: ${status.config.language}`); + console.log(` Sounds: ${status.config.sound.completed} / ${status.config.sound.waiting}`); + + console.log('\nChannels:'); + + // 显示所有可用的渠道,包括未启用的 + const allChannels = this.config._channels || {}; + const activeChannels = status.channels || {}; + + // 合并所有渠道信息 + const channelNames = new Set([ + ...Object.keys(allChannels), + ...Object.keys(activeChannels) + ]); + + for (const name of channelNames) { + const channelConfig = allChannels[name] || {}; + const channelStatus = activeChannels[name]; + + let enabled, configured, relay; + + if (channelStatus) { + // 活跃渠道,使用实际状态 + enabled = channelStatus.enabled ? '✅' : '❌'; + configured = channelStatus.configured ? '✅' : '❌'; + relay = channelStatus.supportsRelay ? '✅' : '❌'; + } else { + // 非活跃渠道,使用配置状态 + enabled = channelConfig.enabled ? '✅' : '❌'; + configured = this._isChannelConfigured(name, channelConfig) ? '✅' : '❌'; + relay = this._supportsRelay(name) ? '✅' : '❌'; + } + + console.log(` ${name}:`); + console.log(` Enabled: ${enabled}`); + console.log(` Configured: ${configured}`); + console.log(` Supports Relay: ${relay}`); + } + } + + _isChannelConfigured(name, config) { + switch (name) { + case 'desktop': + return true; // 桌面通知不需要特殊配置 + case 'email': + return config.config && + config.config.smtp && + config.config.smtp.host && + config.config.smtp.auth && + config.config.smtp.auth.user && + config.config.to; + default: + return false; + } + } + + _supportsRelay(name) { + switch (name) { + case 'email': + return true; + case 'desktop': + default: + return false; + } + } + + async handleConfig(args) { + // Launch the configuration tool + const ConfigTool = require('./src/tools/config-manager'); + const configTool = new ConfigTool(this.config); + await configTool.run(args); + } + + async handleInstall(args) { + // Launch the installer + const Installer = require('./src/tools/installer'); + const installer = new Installer(this.config); + await installer.run(args); + } + + async handleRelay(args) { + const subcommand = args[0]; + + switch (subcommand) { + case 'start': + await this.startRelay(args.slice(1)); + break; + case 'stop': + await this.stopRelay(args.slice(1)); + break; + case 'status': + await this.relayStatus(args.slice(1)); + break; + case 'cleanup': + await this.cleanupRelay(args.slice(1)); + break; + default: + console.error('Usage: taskping relay '); + console.log(''); + console.log('Commands:'); + console.log(' start 启动邮件命令中继服务'); + console.log(' stop 停止邮件命令中继服务'); + console.log(' status 查看中继服务状态'); + console.log(' cleanup 清理已完成的命令历史'); + process.exit(1); + } + } + + async startRelay(args) { + try { + const CommandRelayService = require('./src/relay/command-relay'); + const emailConfig = this.config.getChannel('email'); + + if (!emailConfig || !emailConfig.enabled) { + console.error('❌ 邮件渠道未配置或未启用'); + console.log('请先运行: taskping config'); + process.exit(1); + } + + console.log('🚀 启动邮件命令中继服务...'); + + const relayService = new CommandRelayService(emailConfig.config); + + // 监听事件 + relayService.on('started', () => { + console.log('✅ 命令中继服务已启动'); + console.log('📧 正在监听邮件回复...'); + console.log('💡 现在您可以通过回复邮件来远程执行Claude Code命令'); + console.log(''); + console.log('按 Ctrl+C 停止服务'); + }); + + relayService.on('commandQueued', (command) => { + console.log(`📨 收到新命令: ${command.command.substring(0, 50)}...`); + }); + + relayService.on('commandExecuted', (command) => { + console.log(`✅ 命令执行成功: ${command.id}`); + }); + + relayService.on('commandFailed', (command, error) => { + console.log(`❌ 命令执行失败: ${command.id} - ${error.message}`); + }); + + // 处理优雅关闭 + process.on('SIGINT', async () => { + console.log('\n🛑 正在停止命令中继服务...'); + await relayService.stop(); + console.log('✅ 服务已停止'); + process.exit(0); + }); + + // 启动服务 + await relayService.start(); + + // 保持进程运行 + process.stdin.resume(); + + } catch (error) { + console.error('❌ 启动中继服务失败:', error.message); + process.exit(1); + } + } + + async stopRelay(args) { + console.log('💡 命令中继服务通常通过 Ctrl+C 停止'); + console.log('如果服务仍在运行,请找到对应的进程并手动终止'); + } + + async relayStatus(args) { + try { + const fs = require('fs'); + const path = require('path'); + const stateFile = path.join(__dirname, 'src/data/relay-state.json'); + + console.log('📊 命令中继服务状态\n'); + + // 检查邮件配置 + const emailConfig = this.config.getChannel('email'); + if (!emailConfig || !emailConfig.enabled) { + console.log('❌ 邮件渠道未配置'); + return; + } + + console.log('✅ 邮件配置已启用'); + console.log(`📧 SMTP: ${emailConfig.config.smtp.host}:${emailConfig.config.smtp.port}`); + console.log(`📥 IMAP: ${emailConfig.config.imap.host}:${emailConfig.config.imap.port}`); + console.log(`📬 收件人: ${emailConfig.config.to}`); + + // 检查中继状态 + if (fs.existsSync(stateFile)) { + const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + console.log(`\n📋 命令队列: ${state.commandQueue?.length || 0} 个命令`); + + if (state.commandQueue && state.commandQueue.length > 0) { + console.log('\n最近的命令:'); + state.commandQueue.slice(-5).forEach(cmd => { + const status = cmd.status === 'completed' ? '✅' : + cmd.status === 'failed' ? '❌' : + cmd.status === 'executing' ? '⏳' : '⏸️'; + console.log(` ${status} ${cmd.id}: ${cmd.command.substring(0, 50)}...`); + }); + } + } else { + console.log('\n📋 无命令历史记录'); + } + + } catch (error) { + console.error('❌ 获取状态失败:', error.message); + } + } + + async cleanupRelay(args) { + try { + const fs = require('fs'); + const path = require('path'); + const stateFile = path.join(__dirname, 'src/data/relay-state.json'); + + if (!fs.existsSync(stateFile)) { + console.log('📋 无需清理,没有找到命令历史'); + return; + } + + const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + const beforeCount = state.commandQueue?.length || 0; + + // 清理已完成的命令 (保留24小时内的) + const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); + state.commandQueue = (state.commandQueue || []).filter(cmd => + cmd.status !== 'completed' || + new Date(cmd.completedAt || cmd.queuedAt) > cutoff + ); + + const afterCount = state.commandQueue.length; + const removedCount = beforeCount - afterCount; + + fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); + + console.log(`🧹 清理完成: 移除了 ${removedCount} 个已完成的命令`); + console.log(`📋 剩余 ${afterCount} 个命令在队列中`); + + } catch (error) { + console.error('❌ 清理失败:', error.message); + } + } + + async handleEditConfig(args) { + const { spawn } = require('child_process'); + const path = require('path'); + + const configType = args[0]; + + if (!configType) { + console.log('可用的配置文件:'); + console.log(' user - 用户个人配置 (config/user.json)'); + console.log(' channels - 通知渠道配置 (config/channels.json)'); + console.log(' default - 默认配置模板 (config/default.json)'); + console.log(''); + console.log('使用方法: taskping edit-config <配置类型>'); + console.log('例如: taskping edit-config channels'); + return; + } + + const configFiles = { + 'user': path.join(__dirname, 'config/user.json'), + 'channels': path.join(__dirname, 'config/channels.json'), + 'default': path.join(__dirname, 'config/default.json') + }; + + const configFile = configFiles[configType]; + if (!configFile) { + console.error('❌ 无效的配置类型:', configType); + console.log('可用类型: user, channels, default'); + return; + } + + // 检查文件是否存在 + const fs = require('fs'); + if (!fs.existsSync(configFile)) { + console.error('❌ 配置文件不存在:', configFile); + return; + } + + console.log(`📝 正在打开配置文件: ${configFile}`); + console.log('💡 编辑完成后保存并关闭编辑器即可生效'); + console.log(''); + + // 确定使用的编辑器 + const editor = process.env.EDITOR || process.env.VISUAL || this._getDefaultEditor(); + + try { + const editorProcess = spawn(editor, [configFile], { + stdio: 'inherit' + }); + + editorProcess.on('close', (code) => { + if (code === 0) { + console.log('✅ 配置文件已保存'); + console.log('💡 运行 "taskping status" 查看更新后的配置'); + } else { + console.log('❌ 编辑器异常退出'); + } + }); + + editorProcess.on('error', (error) => { + console.error('❌ 无法启动编辑器:', error.message); + console.log(''); + console.log('💡 你可以手动编辑配置文件:'); + console.log(` ${configFile}`); + }); + + } catch (error) { + console.error('❌ 启动编辑器失败:', error.message); + console.log(''); + console.log('💡 你可以手动编辑配置文件:'); + console.log(` ${configFile}`); + } + } + + _getDefaultEditor() { + // 根据平台确定默认编辑器 + if (process.platform === 'win32') { + return 'notepad'; + } else if (process.platform === 'darwin') { + return 'nano'; // 在macOS上使用nano,因为大多数用户都有 + } else { + return 'nano'; // Linux默认使用nano + } + } + + async handleSetupEmail(args) { + const readline = require('readline'); + const fs = require('fs'); + const path = require('path'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const question = (prompt) => { + return new Promise((resolve) => { + rl.question(prompt, resolve); + }); + }; + + try { + console.log('🚀 TaskPing 邮件快速配置向导\n'); + + // 选择邮箱提供商 + console.log('请选择您的邮箱提供商:'); + console.log('1. Gmail'); + console.log('2. QQ邮箱'); + console.log('3. 163邮箱'); + console.log('4. Outlook/Hotmail'); + console.log('5. 自定义'); + + const providerChoice = await question('\n请选择 (1-5): '); + + let smtpHost, smtpPort, imapHost, imapPort, secure; + + switch (providerChoice) { + case '1': + smtpHost = 'smtp.gmail.com'; + smtpPort = 587; + imapHost = 'imap.gmail.com'; + imapPort = 993; + secure = false; + console.log('\n📧 Gmail 配置'); + console.log('💡 需要先启用两步验证并生成应用密码'); + break; + case '2': + smtpHost = 'smtp.qq.com'; + smtpPort = 587; + imapHost = 'imap.qq.com'; + imapPort = 993; + secure = false; + console.log('\n📧 QQ邮箱配置'); + break; + case '3': + smtpHost = 'smtp.163.com'; + smtpPort = 587; + imapHost = 'imap.163.com'; + imapPort = 993; + secure = false; + console.log('\n📧 163邮箱配置'); + break; + case '4': + smtpHost = 'smtp.live.com'; + smtpPort = 587; + imapHost = 'imap-mail.outlook.com'; + imapPort = 993; + secure = false; + console.log('\n📧 Outlook 配置'); + break; + case '5': + console.log('\n📧 自定义配置'); + smtpHost = await question('SMTP 主机: '); + smtpPort = parseInt(await question('SMTP 端口 (默认587): ') || '587'); + imapHost = await question('IMAP 主机: '); + imapPort = parseInt(await question('IMAP 端口 (默认993): ') || '993'); + const secureInput = await question('使用 SSL/TLS? (y/n): '); + secure = secureInput.toLowerCase() === 'y'; + break; + default: + console.log('❌ 无效选择'); + rl.close(); + return; + } + + // 获取邮箱账户信息 + console.log('\n📝 请输入邮箱账户信息:'); + const email = await question('邮箱地址: '); + const password = await question('密码/应用密码: '); + + // 构建配置 + const emailConfig = { + type: "email", + enabled: true, + config: { + smtp: { + host: smtpHost, + port: smtpPort, + secure: secure, + auth: { + user: email, + pass: password + } + }, + imap: { + host: imapHost, + port: imapPort, + secure: true, + auth: { + user: email, + pass: password + } + }, + from: `TaskPing <${email}>`, + to: email, + template: { + checkInterval: 30 + } + } + }; + + // 读取现有配置 + const channelsFile = path.join(__dirname, 'config/channels.json'); + let channels = {}; + + if (fs.existsSync(channelsFile)) { + channels = JSON.parse(fs.readFileSync(channelsFile, 'utf8')); + } + + // 更新邮件配置 + channels.email = emailConfig; + + // 保存配置 + fs.writeFileSync(channelsFile, JSON.stringify(channels, null, 2)); + + console.log('\n✅ 邮件配置已保存!'); + console.log('\n🧪 现在可以测试邮件功能:'); + console.log(' taskping test'); + console.log('\n🚀 启动命令中继服务:'); + console.log(' taskping relay start'); + + // 询问是否立即测试 + const testNow = await question('\n立即测试邮件发送? (y/n): '); + if (testNow.toLowerCase() === 'y') { + rl.close(); + + // 重新加载配置并测试 + await this.init(); + await this.handleTest([]); + } else { + rl.close(); + } + + } catch (error) { + console.error('❌ 配置失败:', error.message); + rl.close(); + } + } + + async handleDaemon(args) { + const TaskPingDaemon = require('./src/daemon/taskping-daemon'); + const daemon = new TaskPingDaemon(); + + const command = args[0]; + + switch (command) { + case 'start': + await daemon.start(); + break; + case 'stop': + await daemon.stop(); + break; + case 'restart': + await daemon.restart(); + break; + case 'status': + daemon.showStatus(); + break; + default: + console.log('Usage: taskping daemon '); + console.log(''); + console.log('Commands:'); + console.log(' start 启动后台守护进程'); + console.log(' stop 停止后台守护进程'); + console.log(' restart 重启后台守护进程'); + console.log(' status 查看守护进程状态'); + break; + } + } + + async handleCommands(args) { + const ClaudeCommandBridge = require('./src/relay/claude-command-bridge'); + const bridge = new ClaudeCommandBridge(); + + const command = args[0]; + + switch (command) { + case 'list': + const pending = bridge.getPendingCommands(); + console.log(`📋 待处理命令: ${pending.length} 个\n`); + if (pending.length > 0) { + pending.forEach((cmd, index) => { + console.log(`${index + 1}. ${cmd.id}`); + console.log(` 命令: ${cmd.command}`); + console.log(` 时间: ${cmd.timestamp}`); + console.log(` 会话: ${cmd.sessionId}`); + console.log(''); + }); + } + break; + + case 'status': + const status = bridge.getStatus(); + console.log('📊 命令桥接器状态\n'); + console.log(`待处理命令: ${status.pendingCommands}`); + console.log(`已处理命令: ${status.processedCommands}`); + console.log(`命令目录: ${status.commandsDir}`); + console.log(`响应目录: ${status.responseDir}`); + if (status.recentCommands.length > 0) { + console.log('\n最近命令:'); + status.recentCommands.forEach(cmd => { + console.log(` • ${cmd.command} (${cmd.timestamp})`); + }); + } + break; + + case 'cleanup': + bridge.cleanup(); + console.log('🧹 已清理旧的命令文件'); + break; + + case 'clear': + const pending2 = bridge.getPendingCommands(); + for (const cmd of pending2) { + bridge.markCommandProcessed(cmd.id, 'cancelled', 'Manually cancelled'); + } + console.log(`🗑️ 已清除 ${pending2.length} 个待处理命令`); + break; + + default: + console.log('Usage: taskping commands '); + console.log(''); + console.log('Commands:'); + console.log(' list 显示待处理的邮件命令'); + console.log(' status 显示命令桥接器状态'); + console.log(' cleanup 清理旧的命令文件'); + console.log(' clear 清除所有待处理命令'); + break; + } + } + + async handleTestPaste(args) { + const ClipboardAutomation = require('./src/automation/clipboard-automation'); + const automation = new ClipboardAutomation(); + + const testCommand = args.join(' ') || 'echo "测试邮件回复自动粘贴功能"'; + + console.log('🧪 测试自动粘贴功能'); + console.log(`📝 测试命令: ${testCommand}`); + console.log('\n⚠️ 请确保 Claude Code 或 Terminal 窗口已打开并处于活动状态'); + console.log('⏳ 3 秒后自动发送命令...\n'); + + // 倒计时 + for (let i = 3; i > 0; i--) { + process.stdout.write(`${i}... `); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + console.log('\n'); + + try { + const success = await automation.sendCommand(testCommand); + if (success) { + console.log('✅ 命令已自动粘贴!'); + console.log('💡 如果没有看到效果,请检查应用权限和窗口状态'); + } else { + console.log('❌ 自动粘贴失败'); + console.log('💡 请确保给予自动化权限并打开目标应用'); + } + } catch (error) { + console.error('❌ 测试失败:', error.message); + } + } + + async handleSetupPermissions(args) { + const PermissionSetup = require('./setup-permissions'); + const setup = new PermissionSetup(); + await setup.checkAndSetup(); + } + + async handleTestSimple(args) { + const SimpleAutomation = require('./src/automation/simple-automation'); + const automation = new SimpleAutomation(); + + const testCommand = args.join(' ') || 'echo "测试简单自动化功能"'; + + console.log('🧪 测试简单自动化功能'); + console.log(`📝 测试命令: ${testCommand}`); + console.log('\n这个测试会:'); + console.log('1. 📋 将命令复制到剪贴板'); + console.log('2. 📄 保存命令到文件'); + console.log('3. 🔔 发送通知(包含对话框)'); + console.log('4. 🤖 尝试自动粘贴(如果有权限)'); + console.log('\n⏳ 开始测试...\n'); + + try { + const success = await automation.sendCommand(testCommand, 'test-session'); + if (success) { + console.log('✅ 测试成功!'); + console.log('\n📋 下一步操作:'); + console.log('1. 检查是否收到了通知'); + console.log('2. 检查命令是否已复制到剪贴板'); + console.log('3. 如果看到对话框,可以选择打开命令文件'); + console.log('4. 手动粘贴到 Claude Code 中(如果没有自动粘贴)'); + + const status = automation.getStatus(); + console.log(`\n📄 命令文件: ${status.commandFile}`); + if (status.commandFileExists) { + console.log('💡 可以运行 "open -t ' + status.commandFile + '" 查看命令文件'); + } + } else { + console.log('❌ 测试失败'); + } + } catch (error) { + console.error('❌ 测试过程中发生错误:', error.message); + } + } + + async handleTestClaude(args) { + const ClaudeAutomation = require('./src/automation/claude-automation'); + const automation = new ClaudeAutomation(); + + const testCommand = args.join(' ') || 'echo "这是一个自动化测试命令,来自邮件回复"'; + + console.log('🤖 测试 Claude Code 专用自动化'); + console.log(`📝 测试命令: ${testCommand}`); + console.log('\n⚠️ 请确保:'); + console.log(' 1. Claude Code 应用已打开'); + console.log(' 2. 或者 Terminal/iTerm2 等终端应用已打开'); + console.log(' 3. 已经给予必要的辅助功能权限'); + console.log('\n⏳ 5 秒后开始完全自动化测试...\n'); + + // 倒计时 + for (let i = 5; i > 0; i--) { + process.stdout.write(`${i}... `); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + console.log('\n🚀 开始自动化...\n'); + + try { + // 检查权限 + const hasPermission = await automation.requestPermissions(); + if (!hasPermission) { + console.log('⚠️ 权限检查失败,但仍会尝试执行...'); + } + + // 执行完全自动化 + const success = await automation.sendCommand(testCommand, 'test-session'); + + if (success) { + console.log('✅ 完全自动化测试成功!'); + console.log('💡 命令应该已经自动输入到 Claude Code 并开始执行'); + console.log('🔍 请检查 Claude Code 窗口是否收到了命令'); + } else { + console.log('❌ 自动化测试失败'); + console.log('💡 可能的原因:'); + console.log(' • 没有找到 Claude Code 或终端应用'); + console.log(' • 权限不足'); + console.log(' • 应用没有响应'); + console.log('\n🔧 建议:'); + console.log(' 1. 运行 "taskping setup-permissions" 检查权限'); + console.log(' 2. 确保 Claude Code 在前台运行'); + console.log(' 3. 尝试先手动在 Claude Code 中点击输入框'); + } + } catch (error) { + console.error('❌ 测试过程中发生错误:', error.message); + } + } + + async handleDiagnose(args) { + const AutomationDiagnostic = require('./diagnose-automation'); + const diagnostic = new AutomationDiagnostic(); + await diagnostic.runDiagnostic(); + } + + showHelp() { + console.log(` +TaskPing - Claude Code Smart Notification System + +Usage: taskping [options] + +Commands: + notify --type Send a notification (completed|waiting) + test Test all notification channels + status Show system status + config Launch configuration manager + setup-email Quick email setup wizard + edit-config Edit configuration files directly + install Install and configure Claude Code hooks + relay Manage email command relay service + daemon Manage background daemon service + commands Manage email commands and bridge + test-paste [command] Test automatic paste functionality + test-simple [command] Test simple automation (recommended) + test-claude [command] Test Claude Code full automation + setup-permissions Setup macOS permissions for automation + diagnose Diagnose automation issues + +Options: + -h, --help Show this help message + +Relay Subcommands: + relay start Start email command relay service + relay stop Stop email command relay service + relay status Show relay service status + relay cleanup Clean up completed command history + +Daemon Subcommands: + daemon start Start background daemon service + daemon stop Stop background daemon service + daemon restart Restart background daemon service + daemon status Show daemon service status + +Commands Subcommands: + commands list Show pending email commands + commands status Show command bridge status + commands cleanup Clean up old command files + commands clear Clear all pending commands + +Examples: + taskping notify --type completed + taskping test + taskping setup-email # 快速配置邮件 (推荐) + taskping edit-config channels # 直接编辑配置文件 + taskping config # 交互式配置 + taskping install + taskping daemon start # 启动后台服务 (推荐) + taskping daemon status # 查看服务状态 + taskping test-claude # 测试完全自动化 (推荐) + taskping commands list # 查看待处理的邮件命令 + taskping relay start # 前台运行 (需要保持窗口) + +For more information, visit: https://github.com/TaskPing/TaskPing + `); + } +} + +// Run CLI if this file is executed directly +if (require.main === module) { + const cli = new TaskPingCLI(); + cli.run().catch(error => { + console.error('Fatal error:', error.message); + process.exit(1); + }); +} + +module.exports = TaskPingCLI; \ No newline at end of file diff --git a/test-clipboard.js b/test-clipboard.js new file mode 100755 index 0000000..795fff2 --- /dev/null +++ b/test-clipboard.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +/** + * 剪贴板自动化测试工具 + * 用于测试邮件回复命令的自动粘贴功能 + */ + +const ClipboardAutomation = require('./src/automation/clipboard-automation'); + +async function testClipboardAutomation() { + console.log('🧪 剪贴板自动化测试\n'); + + const automation = new ClipboardAutomation(); + + if (!automation.isSupported()) { + console.log('❌ 当前平台不支持剪贴板自动化'); + process.exit(1); + } + + console.log('✅ 剪贴板自动化支持检查通过'); + + // 测试命令 + const testCommand = 'echo "这是一个来自邮件回复的测试命令"'; + + console.log(`📝 测试命令: ${testCommand}`); + console.log('\n⚠️ 请确保:'); + console.log(' 1. Claude Code 或 Terminal 窗口已打开'); + console.log(' 2. 输入框处于活动状态'); + console.log(' 3. 准备好在 5 秒后接收自动输入'); + + // 等待用户准备 + console.log('\n⏳ 5 秒后开始测试...'); + for (let i = 5; i > 0; i--) { + process.stdout.write(` ${i}... `); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + console.log('\n'); + + try { + console.log('🚀 发送测试命令...'); + const success = await automation.sendCommand(testCommand); + + if (success) { + console.log('✅ 测试成功!命令应该已经自动粘贴到 Claude Code 中'); + console.log('💡 如果没有看到命令,请检查:'); + console.log(' - Claude Code 窗口是否在前台'); + console.log(' - 输入框是否处于焦点状态'); + console.log(' - 系统是否允许自动化权限'); + } else { + console.log('❌ 测试失败:命令发送不成功'); + console.log('💡 请尝试:'); + console.log(' - 给予应用自动化权限(系统偏好设置 > 安全性与隐私 > 隐私 > 辅助功能)'); + console.log(' - 确保 Claude Code 或 Terminal 正在运行'); + } + } catch (error) { + console.error('❌ 测试过程中发生错误:', error.message); + } + + // 显示剪贴板内容验证 + try { + const clipboardContent = await automation.getClipboardContent(); + if (clipboardContent) { + console.log(`\n📋 当前剪贴板内容: "${clipboardContent.trim()}"`); + } + } catch (error) { + console.log('📋 无法读取剪贴板内容'); + } +} + +// 处理命令行参数 +const args = process.argv.slice(2); + +if (args.includes('--help') || args.includes('-h')) { + console.log(` +剪贴板自动化测试工具 + +用法: node test-clipboard.js [选项] + +选项: + -h, --help 显示帮助信息 + +这个工具用于测试 TaskPing 的邮件回复自动化功能。 +它会模拟邮件回复的过程,将测试命令自动粘贴到 Claude Code 中。 + +确保在运行测试前: +1. 打开 Claude Code 或 Terminal +2. 点击输入框使其获得焦点 +3. 给予应用必要的自动化权限 + `); + process.exit(0); +} + +// 运行测试 +testClipboardAutomation().catch(console.error); \ No newline at end of file