368 lines
10 KiB
Markdown
368 lines
10 KiB
Markdown
# Ring 通知功能实现计划
|
||
|
||
## 一、功能概述
|
||
|
||
### 1.1 使用场景
|
||
|
||
远程电脑执行长链任务(如 opencode、trae 等工具)时,通过 `ring.py` 脚本向主控电脑发送通知,提醒用户任务完成。
|
||
|
||
### 1.2 调用方式
|
||
|
||
```bash
|
||
python ring.py "任务完成"
|
||
python ring.py "编译成功" --title "Build"
|
||
python ring.py "下载完成" --sound
|
||
```
|
||
|
||
### 1.3 架构设计
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 主控电脑 (被控端) │
|
||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||
│ │ Ring Server (端口 3002) ││
|
||
│ │ - HTTP POST /ring ││
|
||
│ │ - 接收通知请求 ││
|
||
│ │ - 触发系统通知 (Windows Toast) ││
|
||
│ │ - 可选:播放提示音 ││
|
||
│ └─────────────────────────────────────────────────────────────┘│
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
▲
|
||
│ HTTP POST
|
||
│
|
||
┌─────────────────────────────┴───────────────────────────────────┐
|
||
│ 远程电脑 (控制端) │
|
||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||
│ │ ring.py ││
|
||
│ │ - 发送 HTTP 请求到主控电脑 ││
|
||
│ │ - 支持自定义消息、标题、提示音 ││
|
||
│ └─────────────────────────────────────────────────────────────┘│
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 二、实现步骤
|
||
|
||
### 步骤 1:创建 RingService 服务
|
||
|
||
**文件:** `src/services/ring/RingService.js`
|
||
|
||
**功能:**
|
||
- 使用 Windows Toast 通知 API
|
||
- 支持自定义标题和消息
|
||
- 可选播放提示音
|
||
|
||
**实现方式:**
|
||
- 使用 PowerShell 调用 Windows Toast 通知
|
||
- 或使用 `node-notifier` 库
|
||
|
||
### 步骤 2:创建 Ring Server
|
||
|
||
**文件:** `src/server/RingServer.js`
|
||
|
||
**功能:**
|
||
- 独立的 HTTP 服务器,监听端口 3002
|
||
- 提供 `/ring` POST 接口
|
||
- 调用 RingService 发送通知
|
||
|
||
**API 设计:**
|
||
```
|
||
POST /ring
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"message": "任务完成",
|
||
"title": "Ring", // 可选,默认 "Ring"
|
||
"sound": true // 可选,默认 false
|
||
}
|
||
```
|
||
|
||
### 步骤 3:集成到 App.js
|
||
|
||
**修改文件:** `src/core/App.js`
|
||
|
||
**修改内容:**
|
||
- 注册 RingService
|
||
- 注册 RingServer
|
||
- 启动时同时启动 Ring Server
|
||
|
||
### 步骤 4:更新配置
|
||
|
||
**修改文件:** `src/config/schema.js`
|
||
|
||
**新增配置:**
|
||
```javascript
|
||
ring: {
|
||
port: { type: 'number', default: 3002 },
|
||
enabled: { type: 'boolean', default: true }
|
||
}
|
||
```
|
||
|
||
### 步骤 5:创建 ring.py 客户端脚本
|
||
|
||
**文件:** `scripts/ring.py`
|
||
|
||
**功能:**
|
||
- 命令行参数解析
|
||
- 发送 HTTP 请求到主控电脑
|
||
- 支持配置目标地址
|
||
|
||
**使用方式:**
|
||
```bash
|
||
python ring.py "消息内容"
|
||
python ring.py "消息" --title "标题" --sound
|
||
python ring.py "消息" --host 192.168.1.100 --port 3002
|
||
```
|
||
|
||
---
|
||
|
||
## 三、文件清单
|
||
|
||
### 新增文件
|
||
|
||
| 文件路径 | 说明 |
|
||
|---------|------|
|
||
| `src/services/ring/RingService.js` | 通知服务,调用系统通知 |
|
||
| `src/services/ring/index.js` | 服务导出 |
|
||
| `src/server/RingServer.js` | 独立 HTTP 服务器 |
|
||
| `scripts/ring.py` | Python 客户端脚本 |
|
||
|
||
### 修改文件
|
||
|
||
| 文件路径 | 修改内容 |
|
||
|---------|---------|
|
||
| `src/core/App.js` | 注册并启动 Ring 服务 |
|
||
| `src/config/schema.js` | 添加 ring 配置项 |
|
||
| `config/default.json` | 添加 ring 默认配置 |
|
||
|
||
---
|
||
|
||
## 四、详细实现
|
||
|
||
### 4.1 RingService.js
|
||
|
||
```javascript
|
||
const { spawn } = require('child_process');
|
||
const logger = require('../../utils/logger');
|
||
|
||
class RingService {
|
||
async notify({ message, title = 'Ring', sound = false }) {
|
||
// 使用 PowerShell 发送 Windows Toast 通知
|
||
const psScript = `
|
||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||
|
||
$template = @"
|
||
<toast duration="short">
|
||
<visual>
|
||
<binding template="ToastText02">
|
||
<text id="1">${title}</text>
|
||
<text id="2">${message}</text>
|
||
</binding>
|
||
</visual>
|
||
${sound ? '<audio src="ms-winsoundevent:Notification.Default" loop="false"/>' : '<audio silent="true"/>'}
|
||
</toast>
|
||
"@
|
||
|
||
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||
$xml.LoadXml($template)
|
||
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Ring").Show($toast)
|
||
`;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const ps = spawn('powershell', ['-NoProfile', '-Command', psScript]);
|
||
ps.on('close', (code) => {
|
||
if (code === 0) {
|
||
logger.info('Ring notification sent', { title, message });
|
||
resolve(true);
|
||
} else {
|
||
resolve(false);
|
||
}
|
||
});
|
||
ps.on('error', (err) => {
|
||
logger.error('Ring notification failed', { error: err.message });
|
||
resolve(false);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = RingService;
|
||
```
|
||
|
||
### 4.2 RingServer.js
|
||
|
||
```javascript
|
||
const http = require('http');
|
||
const logger = require('../utils/logger');
|
||
|
||
class RingServer {
|
||
constructor(config = {}) {
|
||
this.port = config.port || 3002;
|
||
this.host = config.host || '0.0.0.0';
|
||
this.server = null;
|
||
this.ringService = config.ringService;
|
||
}
|
||
|
||
start() {
|
||
this.server = http.createServer((req, res) => {
|
||
if (req.method === 'POST' && req.url === '/ring') {
|
||
this.handleRing(req, res);
|
||
} else {
|
||
res.writeHead(404);
|
||
res.end('Not Found');
|
||
}
|
||
});
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.server.listen(this.port, this.host, () => {
|
||
logger.info('Ring server started', { port: this.port });
|
||
resolve({ port: this.port, host: this.host });
|
||
});
|
||
this.server.on('error', reject);
|
||
});
|
||
}
|
||
|
||
async handleRing(req, res) {
|
||
let body = '';
|
||
req.on('data', chunk => body += chunk);
|
||
req.on('end', async () => {
|
||
try {
|
||
const data = JSON.parse(body);
|
||
await this.ringService.notify(data);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ success: true }));
|
||
} catch (err) {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: err.message }));
|
||
}
|
||
});
|
||
}
|
||
|
||
stop() {
|
||
return new Promise((resolve) => {
|
||
if (this.server) {
|
||
this.server.close(() => {
|
||
logger.info('Ring server stopped');
|
||
resolve();
|
||
});
|
||
} else {
|
||
resolve();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = RingServer;
|
||
```
|
||
|
||
### 4.3 ring.py
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
import argparse
|
||
import requests
|
||
import sys
|
||
|
||
DEFAULT_HOST = "localhost"
|
||
DEFAULT_PORT = 3002
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description='Send notification to remote computer')
|
||
parser.add_argument('message', help='Notification message')
|
||
parser.add_argument('--title', '-t', default='Ring', help='Notification title')
|
||
parser.add_argument('--sound', '-s', action='store_true', help='Play notification sound')
|
||
parser.add_argument('--host', default=DEFAULT_HOST, help='Target host')
|
||
parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Target port')
|
||
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
response = requests.post(
|
||
f"http://{args.host}:{args.port}/ring",
|
||
json={
|
||
"message": args.message,
|
||
"title": args.title,
|
||
"sound": args.sound
|
||
},
|
||
timeout=5
|
||
)
|
||
if response.status_code == 200:
|
||
print("Notification sent successfully")
|
||
else:
|
||
print(f"Failed to send notification: {response.text}")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"Error: {e}")
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
---
|
||
|
||
## 五、使用示例
|
||
|
||
### 5.1 基本使用
|
||
|
||
```bash
|
||
# 发送简单通知
|
||
python ring.py "任务完成"
|
||
|
||
# 自定义标题
|
||
python ring.py "编译成功" --title "Build"
|
||
|
||
# 带提示音
|
||
python ring.py "下载完成" --sound
|
||
|
||
# 指定目标主机
|
||
python ring.py "远程任务完成" --host 192.168.1.100 --port 3002
|
||
```
|
||
|
||
### 5.2 在脚本中使用
|
||
|
||
```bash
|
||
# 长时间任务完成后通知
|
||
npm run build && python ring.py "Build completed" --sound
|
||
|
||
# 或者在脚本中
|
||
long_running_command
|
||
python ring.py "Command finished: $?"
|
||
```
|
||
|
||
---
|
||
|
||
## 六、配置说明
|
||
|
||
### config/default.json
|
||
|
||
```json
|
||
{
|
||
"ring": {
|
||
"enabled": true,
|
||
"port": 3002
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、安全考虑
|
||
|
||
1. **内网使用** - Ring Server 默认只监听内网,不暴露到公网
|
||
2. **可选认证** - 后续可添加简单 token 认证
|
||
3. **频率限制** - 防止通知轰炸
|
||
|
||
---
|
||
|
||
## 八、测试计划
|
||
|
||
1. 启动应用,验证 Ring Server 在 3002 端口启动
|
||
2. 使用 curl 测试:`curl -X POST http://localhost:3002/ring -H "Content-Type: application/json" -d '{"message":"test"}'`
|
||
3. 验证 Windows 通知弹出
|
||
4. 测试 ring.py 脚本
|
||
5. 测试远程主机调用
|