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. 测试远程主机调用
|