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 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
@@ -0,0 +1,2 @@
1
+ # 此目录用于存储配置数据
2
+
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whistle SSE Mock 插件
3
+ * 用于模拟 SSE 流数据,支持自定义数据输入和流式输出
4
+ */
5
+
6
+ // 导出 server 模块 - 处理代理请求
7
+ exports.server = require('./lib/server');
8
+
9
+ // 导出 uiServer 模块 - 提供配置 UI
10
+ exports.uiServer = require('./lib/uiServer');
11
+
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
+
@@ -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
+