whistle.sse-mock 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -0
- package/data/.gitkeep +2 -0
- package/index.js +11 -0
- package/lib/server.js +152 -0
- package/lib/storage.js +120 -0
- package/lib/uiServer.js +167 -0
- package/package.json +47 -0
- package/public/index.html +1394 -0
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# whistle.sse-mock
|
|
2
|
+
|
|
3
|
+
一个用于模拟 SSE (Server-Sent Events) 流数据的 Whistle 插件。
|
|
4
|
+
|
|
5
|
+
## 功能特点
|
|
6
|
+
|
|
7
|
+
- 🎯 **自定义 SSE 数据**:支持配置多条 SSE 消息,每条消息可设置延迟时间
|
|
8
|
+
- 🔄 **多配置管理**:支持创建多个配置,通过规则指定使用哪个配置
|
|
9
|
+
- 💻 **可视化界面**:提供友好的 Web UI 来管理 SSE 配置
|
|
10
|
+
- ⚡ **实时预览**:在界面中直接测试 SSE 输出效果
|
|
11
|
+
- 🔁 **循环播放**:支持消息循环播放模式
|
|
12
|
+
- 📝 **完整 SSE 支持**:支持 event、id、data 等 SSE 字段
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
### 方式一:使用 lack 脚手架(官方推荐)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 全局安装 lack
|
|
20
|
+
npm i -g lack
|
|
21
|
+
|
|
22
|
+
# 进入插件目录
|
|
23
|
+
cd whistle.sse-mock
|
|
24
|
+
|
|
25
|
+
# 开发模式运行(自动挂载到 Whistle)
|
|
26
|
+
lack watch
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> 参考:[Whistle 插件开发指南](https://wproxy.org/docs/extensions/dev.html)
|
|
30
|
+
|
|
31
|
+
### 方式二:本地安装
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 进入插件目录
|
|
35
|
+
cd whistle.sse-mock
|
|
36
|
+
|
|
37
|
+
# 使用 whistle 安装插件(在插件目录执行)
|
|
38
|
+
w2 i
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 方式三:npm link
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 进入插件目录
|
|
45
|
+
cd whistle.sse-mock
|
|
46
|
+
|
|
47
|
+
# 链接到全局
|
|
48
|
+
npm link
|
|
49
|
+
|
|
50
|
+
# 重启 whistle
|
|
51
|
+
w2 restart
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 使用方法
|
|
55
|
+
|
|
56
|
+
### 1. 配置 SSE 数据
|
|
57
|
+
|
|
58
|
+
安装插件后,在 Whistle 界面的 Plugins 标签页中,点击 `sse-mock` 打开配置界面。
|
|
59
|
+
|
|
60
|
+
在界面中可以:
|
|
61
|
+
- 创建/编辑/删除 SSE 配置
|
|
62
|
+
- 为每条消息设置数据内容和延迟时间
|
|
63
|
+
- 设置是否循环播放
|
|
64
|
+
- 实时预览 SSE 输出效果
|
|
65
|
+
|
|
66
|
+
### 2. 添加 Whistle 规则
|
|
67
|
+
|
|
68
|
+
在 Whistle Rules 中添加规则,格式为:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
# 使用默认配置
|
|
72
|
+
api.example.com/sse/stream sse-mock://
|
|
73
|
+
|
|
74
|
+
# 使用指定配置
|
|
75
|
+
api.example.com/chat/stream sse-mock://chatgpt
|
|
76
|
+
|
|
77
|
+
# 正则匹配
|
|
78
|
+
/api\/.*\/stream/ sse-mock://default
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. 访问接口
|
|
82
|
+
|
|
83
|
+
现在访问匹配的接口时,Whistle 会返回你配置的 SSE 流数据:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -N http://api.example.com/sse/stream
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 配置示例
|
|
90
|
+
|
|
91
|
+
### 基础配置
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"name": "default",
|
|
96
|
+
"description": "默认 SSE 配置",
|
|
97
|
+
"messages": [
|
|
98
|
+
{ "data": "Hello SSE!", "delay": 1000 },
|
|
99
|
+
{ "data": "This is mock data", "delay": 1000 },
|
|
100
|
+
{ "data": "[DONE]", "delay": 0 }
|
|
101
|
+
],
|
|
102
|
+
"loop": false
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### ChatGPT 风格配置
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"name": "chatgpt",
|
|
111
|
+
"description": "ChatGPT 风格 SSE 响应",
|
|
112
|
+
"messages": [
|
|
113
|
+
{ "data": "{\"choices\":[{\"delta\":{\"content\":\"你\"}}]}", "delay": 100 },
|
|
114
|
+
{ "data": "{\"choices\":[{\"delta\":{\"content\":\"好\"}}]}", "delay": 100 },
|
|
115
|
+
{ "data": "{\"choices\":[{\"delta\":{\"content\":\"!\"}}]}", "delay": 100 },
|
|
116
|
+
{ "data": "[DONE]", "delay": 0 }
|
|
117
|
+
],
|
|
118
|
+
"loop": false
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 带事件类型的配置
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"name": "with-events",
|
|
127
|
+
"messages": [
|
|
128
|
+
{ "event": "start", "data": "开始", "delay": 0 },
|
|
129
|
+
{ "event": "message", "data": "消息内容", "delay": 500 },
|
|
130
|
+
{ "event": "end", "data": "结束", "delay": 500 }
|
|
131
|
+
],
|
|
132
|
+
"loop": false
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 消息字段说明
|
|
137
|
+
|
|
138
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
139
|
+
|------|------|------|------|
|
|
140
|
+
| data | string | 是 | SSE 数据内容,可以是普通字符串或 JSON 字符串 |
|
|
141
|
+
| delay | number | 否 | 发送前的延迟时间(毫秒),默认 100 |
|
|
142
|
+
| event | string | 否 | SSE 事件类型,对应 `event:` 字段 |
|
|
143
|
+
| id | string | 否 | SSE 消息 ID,对应 `id:` 字段 |
|
|
144
|
+
|
|
145
|
+
## 目录结构
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
whistle.sse-mock/
|
|
149
|
+
├── package.json # 包配置
|
|
150
|
+
├── index.js # 插件入口
|
|
151
|
+
├── lib/
|
|
152
|
+
│ ├── server.js # SSE 服务器(处理代理请求)
|
|
153
|
+
│ ├── uiServer.js # UI 服务器(提供配置界面 API)
|
|
154
|
+
│ └── storage.js # 存储模块(管理配置数据)
|
|
155
|
+
├── public/
|
|
156
|
+
│ └── index.html # 配置界面
|
|
157
|
+
├── data/
|
|
158
|
+
│ └── config.json # 配置数据(自动生成)
|
|
159
|
+
└── README.md # 说明文档
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 常见问题
|
|
163
|
+
|
|
164
|
+
### Q: 插件安装后在 Whistle 中看不到?
|
|
165
|
+
|
|
166
|
+
A: 确保:
|
|
167
|
+
1. 插件名称以 `whistle.` 开头
|
|
168
|
+
2. 已执行 `npm install` 安装依赖
|
|
169
|
+
3. 重启 Whistle:`w2 restart`
|
|
170
|
+
|
|
171
|
+
### Q: 规则配置正确但没有生效?
|
|
172
|
+
|
|
173
|
+
A: 检查:
|
|
174
|
+
1. 规则格式是否正确(注意 `://` 符号)
|
|
175
|
+
2. URL 匹配是否正确
|
|
176
|
+
3. 查看 Whistle Network 面板确认请求是否被匹配
|
|
177
|
+
|
|
178
|
+
### Q: 如何查看原始 SSE 响应?
|
|
179
|
+
|
|
180
|
+
A: 在浏览器开发者工具的 Network 面板中,找到对应请求,切换到 EventStream 标签页查看。
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
185
|
+
|
package/data/.gitkeep
ADDED
package/index.js
ADDED
package/lib/server.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE 服务器处理模块
|
|
3
|
+
* 处理代理请求,返回 SSE 流数据(支持分片延时)
|
|
4
|
+
*
|
|
5
|
+
* 使用方式:在 Whistle Rules 中配置
|
|
6
|
+
* pattern sse-mock://configName
|
|
7
|
+
* pattern whistle.sse-mock://configName
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const storage = require('./storage');
|
|
11
|
+
|
|
12
|
+
// 延时函数
|
|
13
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
// 格式化 SSE 消息
|
|
16
|
+
function formatSSEMessage(message) {
|
|
17
|
+
let output = '';
|
|
18
|
+
|
|
19
|
+
if (message.event) {
|
|
20
|
+
output += `event: ${message.event}\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (message.id) {
|
|
24
|
+
output += `id: ${message.id}\n`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = message.data !== undefined ? message.data : message;
|
|
28
|
+
if (typeof data === 'object') {
|
|
29
|
+
output += `data: ${JSON.stringify(data)}\n\n`;
|
|
30
|
+
} else {
|
|
31
|
+
output += `data: ${data}\n\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = (server, options) => {
|
|
38
|
+
server.on('request', async (req, res) => {
|
|
39
|
+
const originalReq = req.originalReq || {};
|
|
40
|
+
const ruleValue = originalReq.ruleValue || '';
|
|
41
|
+
|
|
42
|
+
// 获取请求的 Origin(用于 CORS)
|
|
43
|
+
const origin = req.headers?.origin || req.headers?.Origin || '*';
|
|
44
|
+
|
|
45
|
+
// 完整的 CORS 响应头
|
|
46
|
+
const corsHeaders = {
|
|
47
|
+
'Access-Control-Allow-Origin': origin,
|
|
48
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH',
|
|
49
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, Accept, Origin, Cache-Control, Pragma, User-Agent, Referer, DNT, ddmc-longitude, ddmc-build-version, ddmc-channel, ddmc-device-id, ddmc-app-client-id, ddmc-ip, ddmc-api-version, ddmc-city-number, ddmc-latitude, ddmc-station-id, *',
|
|
50
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
51
|
+
'Access-Control-Expose-Headers': 'Content-Type, Content-Length',
|
|
52
|
+
'Access-Control-Max-Age': '86400'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 处理 OPTIONS 预检请求
|
|
56
|
+
if (req.method === 'OPTIONS') {
|
|
57
|
+
res.writeHead(204, corsHeaders);
|
|
58
|
+
res.end();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 消费 POST body(如果有)
|
|
63
|
+
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
|
64
|
+
await new Promise((resolve) => {
|
|
65
|
+
req.on('data', () => {});
|
|
66
|
+
req.on('end', resolve);
|
|
67
|
+
req.on('error', resolve);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = storage.getConfig();
|
|
72
|
+
|
|
73
|
+
let configName = 'default';
|
|
74
|
+
if (ruleValue && ruleValue.trim()) {
|
|
75
|
+
configName = ruleValue.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sseConfig = config.configs?.[configName] || config.configs?.['default'] || {
|
|
79
|
+
messages: [
|
|
80
|
+
{ data: 'Hello SSE!', delay: 100 },
|
|
81
|
+
{ data: 'This is mock data', delay: 100 },
|
|
82
|
+
{ data: '[DONE]', delay: 0 }
|
|
83
|
+
],
|
|
84
|
+
loop: false
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const messages = sseConfig.messages || [];
|
|
88
|
+
|
|
89
|
+
// 合并 CORS 头和 SSE 响应头
|
|
90
|
+
const responseHeaders = {
|
|
91
|
+
...corsHeaders,
|
|
92
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
93
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
94
|
+
'Pragma': 'no-cache',
|
|
95
|
+
'Expires': '0',
|
|
96
|
+
'Connection': 'keep-alive',
|
|
97
|
+
'Transfer-Encoding': 'chunked',
|
|
98
|
+
'X-Accel-Buffering': 'no'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
res.writeHead(200, responseHeaders);
|
|
102
|
+
|
|
103
|
+
// 标记连接是否已关闭
|
|
104
|
+
let closed = false;
|
|
105
|
+
req.on('close', () => { closed = true; });
|
|
106
|
+
req.on('error', () => { closed = true; });
|
|
107
|
+
res.on('close', () => { closed = true; });
|
|
108
|
+
res.on('error', () => { closed = true; });
|
|
109
|
+
|
|
110
|
+
// 流式发送消息(带延时)
|
|
111
|
+
const sendMessages = async () => {
|
|
112
|
+
for (let i = 0; i < messages.length; i++) {
|
|
113
|
+
if (closed) break;
|
|
114
|
+
|
|
115
|
+
const message = messages[i];
|
|
116
|
+
const delay = message.delay !== undefined ? message.delay : 100;
|
|
117
|
+
|
|
118
|
+
// 先等待延时
|
|
119
|
+
if (delay > 0) {
|
|
120
|
+
await sleep(delay);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (closed) break;
|
|
124
|
+
|
|
125
|
+
// 发送消息
|
|
126
|
+
const sseData = formatSSEMessage(message);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
res.write(sseData);
|
|
130
|
+
|
|
131
|
+
// 尝试 flush
|
|
132
|
+
if (typeof res.flush === 'function') {
|
|
133
|
+
res.flush();
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 循环模式
|
|
141
|
+
if (!closed && sseConfig.loop) {
|
|
142
|
+
await sendMessages();
|
|
143
|
+
} else {
|
|
144
|
+
res.end();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 开始发送
|
|
149
|
+
await sendMessages();
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存储模块
|
|
3
|
+
* 用于管理 SSE 配置数据
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// 配置文件路径
|
|
10
|
+
const CONFIG_FILE = path.join(__dirname, '../data/config.json');
|
|
11
|
+
|
|
12
|
+
// 默认配置
|
|
13
|
+
const defaultConfig = {
|
|
14
|
+
configs: {
|
|
15
|
+
default: {
|
|
16
|
+
name: 'default',
|
|
17
|
+
description: '默认 SSE 配置',
|
|
18
|
+
messages: [
|
|
19
|
+
{ data: 'Hello SSE!', delay: 1000 },
|
|
20
|
+
{ data: 'This is mock data from whistle.sse-mock', delay: 1000 },
|
|
21
|
+
{ data: JSON.stringify({ type: 'message', content: 'JSON 数据示例' }), delay: 1000 },
|
|
22
|
+
{ data: '[DONE]', delay: 0 }
|
|
23
|
+
],
|
|
24
|
+
loop: false
|
|
25
|
+
},
|
|
26
|
+
chatgpt: {
|
|
27
|
+
name: 'chatgpt',
|
|
28
|
+
description: 'ChatGPT 风格 SSE 响应',
|
|
29
|
+
messages: [
|
|
30
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '你' } }] }), delay: 100 },
|
|
31
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '好' } }] }), delay: 100 },
|
|
32
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '!' } }] }), delay: 100 },
|
|
33
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '我' } }] }), delay: 100 },
|
|
34
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '是' } }] }), delay: 100 },
|
|
35
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: 'AI' } }] }), delay: 100 },
|
|
36
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '助' } }] }), delay: 100 },
|
|
37
|
+
{ data: JSON.stringify({ choices: [{ delta: { content: '手' } }] }), delay: 100 },
|
|
38
|
+
{ data: '[DONE]', delay: 0 }
|
|
39
|
+
],
|
|
40
|
+
loop: false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// 确保数据目录存在
|
|
46
|
+
function ensureDataDir() {
|
|
47
|
+
const dataDir = path.join(__dirname, '../data');
|
|
48
|
+
if (!fs.existsSync(dataDir)) {
|
|
49
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 获取配置
|
|
54
|
+
function getConfig() {
|
|
55
|
+
try {
|
|
56
|
+
ensureDataDir();
|
|
57
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
58
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
59
|
+
return JSON.parse(content);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('读取配置文件失败:', error);
|
|
63
|
+
}
|
|
64
|
+
return defaultConfig;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 保存配置
|
|
68
|
+
function saveConfig(config) {
|
|
69
|
+
try {
|
|
70
|
+
ensureDataDir();
|
|
71
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('保存配置文件失败:', error);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 获取单个配置
|
|
80
|
+
function getConfigByName(name) {
|
|
81
|
+
const config = getConfig();
|
|
82
|
+
return config.configs?.[name] || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 保存单个配置
|
|
86
|
+
function saveConfigByName(name, sseConfig) {
|
|
87
|
+
const config = getConfig();
|
|
88
|
+
if (!config.configs) {
|
|
89
|
+
config.configs = {};
|
|
90
|
+
}
|
|
91
|
+
config.configs[name] = { ...sseConfig, name };
|
|
92
|
+
return saveConfig(config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 删除配置
|
|
96
|
+
function deleteConfigByName(name) {
|
|
97
|
+
const config = getConfig();
|
|
98
|
+
if (config.configs && config.configs[name]) {
|
|
99
|
+
delete config.configs[name];
|
|
100
|
+
return saveConfig(config);
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 获取所有配置名称
|
|
106
|
+
function getConfigNames() {
|
|
107
|
+
const config = getConfig();
|
|
108
|
+
return Object.keys(config.configs || {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
getConfig,
|
|
113
|
+
saveConfig,
|
|
114
|
+
getConfigByName,
|
|
115
|
+
saveConfigByName,
|
|
116
|
+
deleteConfigByName,
|
|
117
|
+
getConfigNames,
|
|
118
|
+
defaultConfig
|
|
119
|
+
};
|
|
120
|
+
|
package/lib/uiServer.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI 服务器模块
|
|
3
|
+
* 提供配置界面 API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const storage = require('./storage');
|
|
9
|
+
|
|
10
|
+
module.exports = (server, options) => {
|
|
11
|
+
// 解析 JSON body
|
|
12
|
+
const parseBody = (req) => {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let body = '';
|
|
15
|
+
req.on('data', chunk => {
|
|
16
|
+
body += chunk.toString();
|
|
17
|
+
});
|
|
18
|
+
req.on('end', () => {
|
|
19
|
+
try {
|
|
20
|
+
resolve(body ? JSON.parse(body) : {});
|
|
21
|
+
} catch (e) {
|
|
22
|
+
resolve({});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
req.on('error', reject);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 发送 JSON 响应
|
|
30
|
+
const sendJson = (res, data, status = 200) => {
|
|
31
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
32
|
+
res.end(JSON.stringify(data));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 发送 HTML 响应
|
|
36
|
+
const sendHtml = (res, html) => {
|
|
37
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
38
|
+
res.end(html);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
server.on('request', async (req, res) => {
|
|
42
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
43
|
+
const pathname = url.pathname;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 首页 - 返回配置界面
|
|
47
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
48
|
+
const htmlPath = path.join(__dirname, '../public/index.html');
|
|
49
|
+
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
50
|
+
sendHtml(res, html);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// API: 获取所有配置
|
|
55
|
+
if (pathname === '/api/configs' && req.method === 'GET') {
|
|
56
|
+
const config = storage.getConfig();
|
|
57
|
+
sendJson(res, { success: true, data: config.configs || {} });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// API: 获取配置名称列表
|
|
62
|
+
if (pathname === '/api/config-names' && req.method === 'GET') {
|
|
63
|
+
const names = storage.getConfigNames();
|
|
64
|
+
sendJson(res, { success: true, data: names });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// API: 获取单个配置
|
|
69
|
+
if (pathname.startsWith('/api/config/') && req.method === 'GET') {
|
|
70
|
+
const name = pathname.replace('/api/config/', '');
|
|
71
|
+
const config = storage.getConfigByName(decodeURIComponent(name));
|
|
72
|
+
if (config) {
|
|
73
|
+
sendJson(res, { success: true, data: config });
|
|
74
|
+
} else {
|
|
75
|
+
sendJson(res, { success: false, error: '配置不存在' }, 404);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// API: 保存配置
|
|
81
|
+
if (pathname === '/api/config' && req.method === 'POST') {
|
|
82
|
+
const body = await parseBody(req);
|
|
83
|
+
const { name, ...configData } = body;
|
|
84
|
+
if (!name) {
|
|
85
|
+
sendJson(res, { success: false, error: '配置名称不能为空' }, 400);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const success = storage.saveConfigByName(name, configData);
|
|
89
|
+
sendJson(res, { success, message: success ? '保存成功' : '保存失败' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// API: 删除配置
|
|
94
|
+
if (pathname.startsWith('/api/config/') && req.method === 'DELETE') {
|
|
95
|
+
const name = pathname.replace('/api/config/', '');
|
|
96
|
+
const success = storage.deleteConfigByName(decodeURIComponent(name));
|
|
97
|
+
sendJson(res, { success, message: success ? '删除成功' : '删除失败' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// API: 测试 SSE 流
|
|
102
|
+
if (pathname === '/api/test-sse' && req.method === 'GET') {
|
|
103
|
+
const configName = url.searchParams.get('config') || 'default';
|
|
104
|
+
const sseConfig = storage.getConfigByName(configName);
|
|
105
|
+
|
|
106
|
+
if (!sseConfig) {
|
|
107
|
+
sendJson(res, { success: false, error: '配置不存在' }, 404);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 设置 SSE 响应头
|
|
112
|
+
res.writeHead(200, {
|
|
113
|
+
'Content-Type': 'text/event-stream',
|
|
114
|
+
'Cache-Control': 'no-cache',
|
|
115
|
+
'Connection': 'keep-alive'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let closed = false;
|
|
119
|
+
let currentIndex = 0;
|
|
120
|
+
let timer = null;
|
|
121
|
+
|
|
122
|
+
req.on('close', () => {
|
|
123
|
+
closed = true;
|
|
124
|
+
if (timer) clearTimeout(timer);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const sendNext = () => {
|
|
128
|
+
if (closed) return;
|
|
129
|
+
|
|
130
|
+
const messages = sseConfig.messages || [];
|
|
131
|
+
if (currentIndex >= messages.length) {
|
|
132
|
+
res.end();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const message = messages[currentIndex];
|
|
137
|
+
const delay = message.delay !== undefined ? message.delay : 100;
|
|
138
|
+
|
|
139
|
+
timer = setTimeout(() => {
|
|
140
|
+
if (closed) return;
|
|
141
|
+
|
|
142
|
+
let output = '';
|
|
143
|
+
if (message.event) output += `event: ${message.event}\n`;
|
|
144
|
+
if (message.id) output += `id: ${message.id}\n`;
|
|
145
|
+
const data = message.data || message;
|
|
146
|
+
output += `data: ${typeof data === 'object' ? JSON.stringify(data) : data}\n\n`;
|
|
147
|
+
|
|
148
|
+
res.write(output);
|
|
149
|
+
currentIndex++;
|
|
150
|
+
sendNext();
|
|
151
|
+
}, delay);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
sendNext();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 404
|
|
159
|
+
sendJson(res, { success: false, error: 'Not Found' }, 404);
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('UI Server Error:', error);
|
|
163
|
+
sendJson(res, { success: false, error: error.message }, 500);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whistle.sse-mock",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Whistle 插件 - 模拟 SSE (Server-Sent Events) 流数据,支持可视化配置、批量导入、分片延时输出",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"whistle",
|
|
8
|
+
"whistle-plugin",
|
|
9
|
+
"plugin",
|
|
10
|
+
"sse",
|
|
11
|
+
"server-sent-events",
|
|
12
|
+
"mock",
|
|
13
|
+
"stream",
|
|
14
|
+
"proxy",
|
|
15
|
+
"chatgpt",
|
|
16
|
+
"ai"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/zjians/whistle.sse-mock"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/zjians/whistle.sse-mock",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/zjians/whistle.sse-mock/issues"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"lib/",
|
|
31
|
+
"public/",
|
|
32
|
+
"data/.gitkeep",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=12.0.0"
|
|
37
|
+
},
|
|
38
|
+
"whistleConfig": {
|
|
39
|
+
"priority": 0,
|
|
40
|
+
"hintList": [
|
|
41
|
+
"sse-mock://default",
|
|
42
|
+
"sse-mock://chatgpt",
|
|
43
|
+
"sse-mock://{configName}"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|