Skip to content

Commit 1101f8d

Browse files
committed
feat: 完整实现 Deno Deploy 使用 shared 库 🦕
新增功能: ✅ Deno 版本的 QQBot 适配器(继承 shared) ✅ Deno KV 数据库适配器(实现 DatabaseAdapter 接口) ✅ WebSocket 管理器(使用 BroadcastChannel) ✅ 完整的 Webhook 路由逻辑 ✅ WebSocket 实时推送支持 主要改动: 1. deno.json - 添加 @webhook-proxy/shared 导入映射 - 添加 tweetnacl 依赖 2. src/adapters/qqbot-deno.ts (新建) - 继承 QQBotAdapter from shared - Deno 特定的 handleWebhook 实现 - 代码量 110 行(vs Cloudflare 130 行) 3. src/db/kv.ts - 实现 DatabaseAdapter 接口 - 完整的 CRUD 操作 - 原子事务支持 - 多索引设计(random_key, id, user_id) 4. src/websocket/manager.ts - 实现 WebSocketManager 接口 - BroadcastChannel 跨实例通信 - 连接生命周期管理 - 实时事件广播 5. mod.ts - 集成所有核心逻辑 - Webhook 接收和处理 - WebSocket 连接管理 - 优雅关闭支持 6. shared/types/index.ts - 添加 platform_app_id 字段 - 添加 verify_signature 字段 架构对比: | 特性 | Cloudflare Workers | Deno Deploy | |------|-------------------|-------------| | 数据库 | D1 (SQLite) | Deno KV | | 实时通信 | Durable Objects | BroadcastChannel | | WebSocket | ✅ | ✅ | | QQBot 适配器 | 130 行 | 110 行 | | 共享逻辑 | ✅ 来自 shared | ✅ 来自 shared | 优势: - 代码完全复用 shared 核心逻辑 - Deno 和 Cloudflare 实现高度一致 - 易于维护和扩展 - 类型安全 测试状态: ✅ pnpm type-check 通过 ✅ Deno 导入映射配置正确 ⏳ 待本地测试和部署 下一步:本地测试 Deno 应用
1 parent d84d4b4 commit 1101f8d

6 files changed

Lines changed: 622 additions & 374 deletions

File tree

‎packages/deno-deploy/deno.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
},
1010
"imports": {
1111
"hono": "https://deno.land/x/hono@v4.6.14/mod.ts",
12-
"hono/": "https://deno.land/x/hono@v4.6.14/"
12+
"hono/": "https://deno.land/x/hono@v4.6.14/",
13+
"@webhook-proxy/shared": "../shared/src/index.ts",
14+
"@webhook-proxy/shared/": "../shared/src/",
15+
"tweetnacl": "https://esm.sh/tweetnacl@1.0.3"
1316
},
1417
"compilerOptions": {
1518
"lib": ["deno.window", "deno.unstable"],

‎packages/deno-deploy/mod.ts‎

Lines changed: 192 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,204 @@
11
/**
22
* Webhook Proxy - Deno Deploy 版本
3-
*
4-
* 这是 Cloudflare Workers 版本的 Deno 适配
5-
* 主要差异:
6-
* - D1 Database → Deno KV
7-
* - Durable Objects → Deno Broadcasts / BroadcastChannel
8-
* - Workers KV → Deno KV
3+
* 使用 @webhook-proxy/shared 核心逻辑
94
*/
105

116
import { Hono } from 'hono';
12-
import { cors } from 'hono/cors';
13-
import { logger } from 'hono/logger';
14-
15-
// 类型定义
16-
interface Env {
17-
KV: Deno.Kv;
18-
ENVIRONMENT: string;
19-
}
20-
21-
const app = new Hono<{ Bindings: Env }>();
22-
23-
// 中间件
24-
app.use('*', cors());
25-
app.use('*', logger());
26-
27-
// 健康检查
28-
app.get('/health', (c) => {
29-
return c.json({
30-
status: 'ok',
31-
platform: 'deno-deploy',
32-
timestamp: Date.now()
33-
});
34-
});
7+
import { createQQBotAdapter } from './src/adapters/qqbot-deno.ts';
8+
import { db } from './src/db/kv.ts';
9+
import { wsManager } from './src/websocket/manager.ts';
10+
import type { Platform } from '@webhook-proxy/shared';
11+
12+
const app = new Hono();
13+
14+
/**
15+
* 支持的平台列表
16+
*/
17+
const SUPPORTED_PLATFORMS: Platform[] = [
18+
'github', 'gitlab', 'qqbot', 'telegram',
19+
'stripe', 'jenkins', 'jira', 'sentry', 'generic'
20+
];
21+
22+
/**
23+
* 健康检查
24+
*/
25+
app.get('/health', (c) => c.json({ status: 'ok', runtime: 'deno-deploy' }));
3526

36-
// 首页
27+
/**
28+
* 首页
29+
*/
3730
app.get('/', (c) => {
38-
return c.html(`
39-
<!DOCTYPE html>
40-
<html>
41-
<head>
42-
<title>Webhook Proxy - Deno Deploy</title>
43-
<meta charset="UTF-8">
44-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
45-
<style>
46-
body {
47-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
48-
max-width: 800px;
49-
margin: 50px auto;
50-
padding: 20px;
51-
line-height: 1.6;
52-
}
53-
h1 { color: #333; }
54-
.badge {
55-
display: inline-block;
56-
padding: 4px 8px;
57-
background: #0ea5e9;
58-
color: white;
59-
border-radius: 4px;
60-
font-size: 12px;
61-
font-weight: 600;
62-
}
63-
a { color: #0ea5e9; text-decoration: none; }
64-
a:hover { text-decoration: underline; }
65-
</style>
66-
</head>
67-
<body>
68-
<h1>Webhook Proxy <span class="badge">Deno Deploy</span></h1>
69-
<p>统一的 Webhook 代理服务 - Deno Deploy 版本</p>
70-
<p>
71-
<a href="https://github.com/lc-cn/webhook-proxy" target="_blank">GitHub</a> |
72-
<a href="/docs">文档</a> |
73-
<a href="/dashboard">Dashboard</a>
74-
</p>
75-
</body>
76-
</html>
77-
`);
31+
return c.text('Webhook Proxy (Deno Deploy) - Powered by @webhook-proxy/shared');
32+
});
33+
34+
/**
35+
* WebSocket 连接统计
36+
*/
37+
app.get('/stats', (c) => {
38+
return c.json(wsManager.getStats());
39+
});
40+
41+
/**
42+
* Webhook 接收端点
43+
*/
44+
app.post('/:platform/:randomKey', async (c) => {
45+
const platform = c.req.param('platform') as Platform;
46+
const randomKey = c.req.param('randomKey');
47+
48+
const startTime = Date.now();
49+
50+
try {
51+
// 验证平台
52+
if (!SUPPORTED_PLATFORMS.includes(platform)) {
53+
return c.text('Invalid platform', 400);
54+
}
55+
56+
// 查找 proxy 配置
57+
const proxy = await db.getProxyByRandomKey(randomKey);
58+
59+
if (!proxy) {
60+
console.warn(`[Webhook] Proxy not found: ${randomKey}`);
61+
return c.text('Proxy not found', 404);
62+
}
63+
64+
if (!proxy.active) {
65+
return c.text('Proxy is inactive', 403);
66+
}
67+
68+
if (proxy.platform !== platform) {
69+
return c.text('Platform mismatch', 400);
70+
}
71+
72+
// 创建适配器(目前仅支持 QQBot)
73+
let adapter;
74+
75+
switch (platform) {
76+
case 'qqbot':
77+
adapter = createQQBotAdapter({
78+
appId: proxy.platform_app_id || '',
79+
secret: proxy.webhook_secret || '',
80+
verifySignature: proxy.verify_signature,
81+
});
82+
break;
83+
84+
default:
85+
return c.text(`Platform ${platform} not yet implemented in Deno Deploy`, 501);
86+
}
87+
88+
// 处理 Webhook 请求
89+
const response = await adapter.handleWebhook(c.req.raw);
90+
91+
// 如果验证成功,转换并广播事件
92+
if (response.status === 200) {
93+
// QQ Bot 特殊处理:只广播 OpCode 0 的事件
94+
if (platform === 'qqbot') {
95+
const bodyText = await c.req.raw.clone().text();
96+
const payload = JSON.parse(bodyText);
97+
98+
console.log(`[Webhook] QQ Bot OpCode: ${payload.op}`);
99+
100+
if (payload.op === 0) {
101+
console.log('[Webhook] 📡 Broadcasting QQ Bot event...');
102+
103+
// 转换事件
104+
const event = adapter.transform(payload, c.req.raw);
105+
106+
// 更新事件计数
107+
await db.updateProxyEventCount(proxy.id);
108+
109+
// 广播事件
110+
const result = await wsManager.broadcastEvent(randomKey, event);
111+
console.log(`[Webhook] Broadcast result:`, result);
112+
} else {
113+
console.log(`[Webhook] Skip broadcast for OpCode ${payload.op}`);
114+
}
115+
}
116+
}
117+
118+
const duration = Date.now() - startTime;
119+
console.log(`[Webhook] ✅ Completed in ${duration}ms, status: ${response.status}`);
120+
121+
return response;
122+
123+
} catch (error) {
124+
const duration = Date.now() - startTime;
125+
console.error(`[Webhook] ❌ Error after ${duration}ms:`, error);
126+
127+
if (error instanceof SyntaxError) {
128+
return c.text('Invalid JSON payload', 400);
129+
}
130+
131+
return c.text('Internal server error', 500);
132+
}
133+
});
134+
135+
/**
136+
* WebSocket 连接端点
137+
*/
138+
app.get('/:platform/:randomKey/ws', async (c) => {
139+
const randomKey = c.req.param('randomKey');
140+
141+
try {
142+
// 验证 proxy 是否存在
143+
const proxy = await db.getProxyByRandomKey(randomKey);
144+
145+
if (!proxy) {
146+
return c.text('Proxy not found', 404);
147+
}
148+
149+
if (!proxy.active) {
150+
return c.text('Proxy is inactive', 403);
151+
}
152+
153+
// 升级到 WebSocket
154+
const upgrade = c.req.header('upgrade') || '';
155+
156+
if (upgrade.toLowerCase() !== 'websocket') {
157+
return c.text('Expected WebSocket', 426);
158+
}
159+
160+
const { socket, response } = Deno.upgradeWebSocket(c.req.raw);
161+
162+
// 注册连接
163+
socket.onopen = () => {
164+
const connectionId = wsManager.addConnection(randomKey, socket);
165+
console.log(`[WebSocket] New connection ${connectionId} for key ${randomKey}`);
166+
167+
// 发送欢迎消息
168+
socket.send(JSON.stringify({
169+
type: 'connected',
170+
randomKey,
171+
timestamp: Date.now(),
172+
}));
173+
};
174+
175+
return response;
176+
177+
} catch (error) {
178+
console.error('[WebSocket] Connection error:', error);
179+
return c.text('WebSocket upgrade failed', 500);
180+
}
78181
});
79182

80-
// 启动服务器
81-
const port = parseInt(Deno.env.get('PORT') || '8000');
183+
/**
184+
* 优雅关闭
185+
*/
186+
Deno.addSignalListener('SIGINT', () => {
187+
console.log('🛑 Shutting down...');
188+
wsManager.cleanup();
189+
Deno.exit(0);
190+
});
82191

83-
console.log(`🚀 Webhook Proxy (Deno Deploy) starting on port ${port}`);
84-
console.log(`📝 Environment: ${Deno.env.get('DENO_DEPLOYMENT_ID') ? 'production' : 'development'}`);
192+
Deno.addSignalListener('SIGTERM', () => {
193+
console.log('🛑 Shutting down...');
194+
wsManager.cleanup();
195+
Deno.exit(0);
196+
});
85197

86-
Deno.serve({ port }, app.fetch);
198+
/**
199+
* 启动服务
200+
*/
201+
console.log('🦕 Webhook Proxy (Deno Deploy) starting...');
202+
console.log('📦 Using @webhook-proxy/shared core logic');
87203

204+
Deno.serve(app.fetch);

0 commit comments

Comments
 (0)