ricebug commited on
Commit
1bd4705
·
verified ·
1 Parent(s): 1a25715

Create main.ts

Browse files
Files changed (1) hide show
  1. main.ts +2740 -0
main.ts ADDED
@@ -0,0 +1,2740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /**
3
+ * ZtoApi - OpenAI兼容API代理服务器
4
+ *
5
+ * 功能概述:
6
+ * - 为 Z.ai 的 GLM-4.5 模型提供 OpenAI 兼容的 API 接口
7
+ * - 支持流式和非流式响应模式
8
+ * - 提供实时监控 Dashboard 功能
9
+ * - 支持匿名 token 自动获取
10
+ * - 智能处理模型思考过程展示
11
+ * - 完整的请求统计和错误处理
12
+ *
13
+ * 技术栈:
14
+ * - Deno 原生 HTTP API
15
+ * - TypeScript 类型安全
16
+ * - Server-Sent Events (SSE) 流式传输
17
+ * - 支持 Deno Deploy 和自托管部署
18
+ *
19
+ * @author ZtoApi Team
20
+ * @version 2.0.0
21
+ * @since 2024
22
+ */
23
+ declare namespace Deno {
24
+ interface Conn {
25
+ readonly rid: number;
26
+ localAddr: Addr;
27
+ remoteAddr: Addr;
28
+ read(p: Uint8Array): Promise<number | null>;
29
+ write(p: Uint8Array): Promise<number>;
30
+ close(): void;
31
+ }
32
+
33
+ interface Addr {
34
+ hostname: string;
35
+ port: number;
36
+ transport: string;
37
+ }
38
+
39
+ interface Listener extends AsyncIterable<Conn> {
40
+ readonly addr: Addr;
41
+ accept(): Promise<Conn>;
42
+ close(): void;
43
+ [Symbol.asyncIterator](): AsyncIterableIterator<Conn>;
44
+ }
45
+
46
+ interface HttpConn {
47
+ nextRequest(): Promise<RequestEvent | null>;
48
+ [Symbol.asyncIterator](): AsyncIterableIterator<RequestEvent>;
49
+ }
50
+
51
+ interface RequestEvent {
52
+ request: Request;
53
+ respondWith(r: Response | Promise<Response>): Promise<void>;
54
+ }
55
+
56
+ function listen(options: { port: number }): Listener;
57
+ function serveHttp(conn: Conn): HttpConn;
58
+ function serve(handler: (request: Request) => Promise<Response>): void;
59
+
60
+ namespace env {
61
+ function get(key: string): string | undefined;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 请求统计信息接口
67
+ * 用于跟踪API调用的各项指标
68
+ */
69
+ interface RequestStats {
70
+ totalRequests: number;
71
+ successfulRequests: number;
72
+ failedRequests: number;
73
+ lastRequestTime: Date;
74
+ averageResponseTime: number;
75
+ }
76
+
77
+ /**
78
+ * 实时请求信息接口
79
+ * 用于Dashboard显示最近的API请求记录
80
+ */
81
+ interface LiveRequest {
82
+ id: string;
83
+ timestamp: Date;
84
+ method: string;
85
+ path: string;
86
+ status: number;
87
+ duration: number;
88
+ userAgent: string;
89
+ model?: string;
90
+ }
91
+
92
+ /**
93
+ * OpenAI兼容请求结构
94
+ * 标准的聊天完成API请求格式
95
+ */
96
+ interface OpenAIRequest {
97
+ model: string;
98
+ messages: Message[];
99
+ stream?: boolean;
100
+ temperature?: number;
101
+ max_tokens?: number;
102
+ }
103
+
104
+ /**
105
+ * 聊天消息结构
106
+ * 支持全方位多模态内容:文本、图像、视频、文档
107
+ */
108
+ interface Message {
109
+ role: string;
110
+ content: string | Array<{
111
+ type: string;
112
+ text?: string;
113
+ image_url?: {url: string};
114
+ video_url?: {url: string};
115
+ document_url?: {url: string};
116
+ audio_url?: {url: string};
117
+ }>;
118
+ }
119
+
120
+ /**
121
+ * 上游服务请求结构
122
+ * 向Z.ai服务发送的请求格式
123
+ */
124
+ interface UpstreamRequest {
125
+ stream: boolean;
126
+ model: string;
127
+ messages: Message[];
128
+ params: Record<string, unknown>;
129
+ features: Record<string, unknown>;
130
+ background_tasks?: Record<string, boolean>;
131
+ chat_id?: string;
132
+ id?: string;
133
+ mcp_servers?: string[];
134
+ model_item?: {
135
+ id: string;
136
+ name: string;
137
+ owned_by: string;
138
+ openai?: any;
139
+ urlIdx?: number;
140
+ info?: any;
141
+ actions?: any[];
142
+ tags?: any[];
143
+ };
144
+ tool_servers?: string[];
145
+ variables?: Record<string, string>;
146
+ }
147
+
148
+ /**
149
+ * OpenAI兼容响应结构
150
+ */
151
+ interface OpenAIResponse {
152
+ id: string;
153
+ object: string;
154
+ created: number;
155
+ model: string;
156
+ choices: Choice[];
157
+ usage?: Usage;
158
+ }
159
+
160
+ interface Choice {
161
+ index: number;
162
+ message?: Message;
163
+ delta?: Delta;
164
+ finish_reason?: string;
165
+ }
166
+
167
+ interface Delta {
168
+ role?: string;
169
+ content?: string;
170
+ }
171
+
172
+ interface Usage {
173
+ prompt_tokens: number;
174
+ completion_tokens: number;
175
+ total_tokens: number;
176
+ }
177
+
178
+ /**
179
+ * 上游SSE数据结构
180
+ */
181
+ interface UpstreamData {
182
+ type: string;
183
+ data: {
184
+ delta_content: string;
185
+ phase: string;
186
+ done: boolean;
187
+ usage?: Usage;
188
+ error?: UpstreamError;
189
+ inner?: {
190
+ error?: UpstreamError;
191
+ };
192
+ };
193
+ error?: UpstreamError;
194
+ }
195
+
196
+ interface UpstreamError {
197
+ detail: string;
198
+ code: number;
199
+ }
200
+
201
+ interface ModelsResponse {
202
+ object: string;
203
+ data: Model[];
204
+ }
205
+
206
+ interface Model {
207
+ id: string;
208
+ object: string;
209
+ created: number;
210
+ owned_by: string;
211
+ }
212
+
213
+ /**
214
+ * 配置常量定义
215
+ */
216
+
217
+ // 思考内容处理策略: strip-去除<details>标签, think-转为<thinking>标签, raw-保留原样
218
+ const THINK_TAGS_MODE = "strip";
219
+
220
+ // 伪装前端头部(来自抓包分析)
221
+ const X_FE_VERSION = "prod-fe-1.0.70";
222
+ const BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0";
223
+ const SEC_CH_UA = "\"Not;A=Brand\";v=\"99\", \"Microsoft Edge\";v=\"139\", \"Chromium\";v=\"139\"";
224
+ const SEC_CH_UA_MOB = "?0";
225
+ const SEC_CH_UA_PLAT = "\"Windows\"";
226
+ const ORIGIN_BASE = "https://chat.z.ai";
227
+
228
+ const ANON_TOKEN_ENABLED = true;
229
+
230
+ /**
231
+ * 环境变量配置
232
+ */
233
+ const UPSTREAM_URL = Deno.env.get("UPSTREAM_URL") || "https://chat.z.ai/api/chat/completions";
234
+ const DEFAULT_KEY = Deno.env.get("DEFAULT_KEY") || "sk-your-key";
235
+ const ZAI_TOKEN = Deno.env.get("ZAI_TOKEN") || "";
236
+
237
+ /**
238
+ * 支持的模型配置
239
+ */
240
+ interface ModelConfig {
241
+ id: string; // OpenAI API中的模型ID
242
+ name: string; // 显示名称
243
+ upstreamId: string; // Z.ai上游的模型ID
244
+ capabilities: {
245
+ vision: boolean;
246
+ mcp: boolean;
247
+ thinking: boolean;
248
+ };
249
+ defaultParams: {
250
+ top_p: number;
251
+ temperature: number;
252
+ max_tokens?: number;
253
+ };
254
+ }
255
+
256
+ const SUPPORTED_MODELS: ModelConfig[] = [
257
+ {
258
+ id: "0727-360B-API",
259
+ name: "GLM-4.5",
260
+ upstreamId: "0727-360B-API",
261
+ capabilities: {
262
+ vision: false,
263
+ mcp: true,
264
+ thinking: true
265
+ },
266
+ defaultParams: {
267
+ top_p: 0.95,
268
+ temperature: 0.6,
269
+ max_tokens: 80000
270
+ }
271
+ },
272
+ {
273
+ id: "glm-4.5v",
274
+ name: "GLM-4.5V",
275
+ upstreamId: "glm-4.5v",
276
+ capabilities: {
277
+ vision: true,
278
+ mcp: false,
279
+ thinking: true
280
+ },
281
+ defaultParams: {
282
+ top_p: 0.6,
283
+ temperature: 0.8
284
+ }
285
+ }
286
+ ];
287
+
288
+ // 默认模型
289
+ const DEFAULT_MODEL = SUPPORTED_MODELS[0];
290
+
291
+ // 根据模型ID获取配置
292
+ function getModelConfig(modelId: string): ModelConfig {
293
+ // 标准化模型ID,处理Cherry Studio等客户端的大小写差异
294
+ const normalizedModelId = normalizeModelId(modelId);
295
+ const found = SUPPORTED_MODELS.find(m => m.id === normalizedModelId);
296
+
297
+ if (!found) {
298
+ debugLog("⚠️ 未找到模型配置: %s (标准化后: %s),使用默认模型: %s",
299
+ modelId, normalizedModelId, DEFAULT_MODEL.name);
300
+ }
301
+
302
+ return found || DEFAULT_MODEL;
303
+ }
304
+
305
+ /**
306
+ * 标准化模型ID,处理不同客户端的命名差异
307
+ * Cherry Studio等客户端可能使用不同的大小写格式
308
+ */
309
+ function normalizeModelId(modelId: string): string {
310
+ const normalized = modelId.toLowerCase().trim();
311
+
312
+ // 处理常见的模型ID映射
313
+ const modelMappings: Record<string, string> = {
314
+ 'glm-4.5v': 'glm-4.5v',
315
+ 'glm4.5v': 'glm-4.5v',
316
+ 'glm_4.5v': 'glm-4.5v',
317
+ 'gpt-4-vision-preview': 'glm-4.5v', // 向后兼容
318
+ '0727-360b-api': '0727-360B-API',
319
+ 'glm-4.5': '0727-360B-API',
320
+ 'glm4.5': '0727-360B-API',
321
+ 'glm_4.5': '0727-360B-API',
322
+ 'gpt-4': '0727-360B-API' // 向后兼容
323
+ };
324
+
325
+ const mapped = modelMappings[normalized];
326
+ if (mapped) {
327
+ debugLog("🔄 模型ID映射: %s → %s", modelId, mapped);
328
+ return mapped;
329
+ }
330
+
331
+ return normalized;
332
+ }
333
+
334
+ /**
335
+ * 处理和验证全方位多模态消息
336
+ * 支持图像、视频、文档、音频等多种媒体类型
337
+ */
338
+ function processMessages(messages: Message[], modelConfig: ModelConfig): Message[] {
339
+ const processedMessages: Message[] = [];
340
+
341
+ for (const message of messages) {
342
+ const processedMessage: Message = { ...message };
343
+
344
+ // 检查是否为多模态消息
345
+ if (Array.isArray(message.content)) {
346
+ debugLog("检测到多模态消息,内容块数量: %d", message.content.length);
347
+
348
+ // 统计各种媒体类型
349
+ const mediaStats = {
350
+ text: 0,
351
+ images: 0,
352
+ videos: 0,
353
+ documents: 0,
354
+ audios: 0,
355
+ others: 0
356
+ };
357
+
358
+ // 验证模型是否支持多模态
359
+ if (!modelConfig.capabilities.vision) {
360
+ debugLog("警告: 模型 %s 不支持多模态,但收到了多模态消息", modelConfig.name);
361
+ // 只保留文本内容
362
+ const textContent = message.content
363
+ .filter(block => block.type === 'text')
364
+ .map(block => block.text)
365
+ .join('\n');
366
+ processedMessage.content = textContent;
367
+ } else {
368
+ // GLM-4.5V 支持全方位多模态,处理所有内容类型
369
+ for (const block of message.content) {
370
+ switch (block.type) {
371
+ case 'text':
372
+ if (block.text) {
373
+ mediaStats.text++;
374
+ debugLog("📝 文本内容,长度: %d", block.text.length);
375
+ }
376
+ break;
377
+
378
+ case 'image_url':
379
+ if (block.image_url?.url) {
380
+ mediaStats.images++;
381
+ const url = block.image_url.url;
382
+ if (url.startsWith('data:image/')) {
383
+ const mimeMatch = url.match(/data:image\/([^;]+)/);
384
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
385
+ debugLog("🖼️ 图像数据: %s格式, 大小: %d字符", format, url.length);
386
+ } else if (url.startsWith('http')) {
387
+ debugLog("🔗 图像URL: %s", url);
388
+ } else {
389
+ debugLog("⚠️ 未知图像格式: %s", url.substring(0, 50));
390
+ }
391
+ }
392
+ break;
393
+
394
+ case 'video_url':
395
+ if (block.video_url?.url) {
396
+ mediaStats.videos++;
397
+ const url = block.video_url.url;
398
+ if (url.startsWith('data:video/')) {
399
+ const mimeMatch = url.match(/data:video\/([^;]+)/);
400
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
401
+ debugLog("🎥 视频数据: %s格式, 大小: %d字符", format, url.length);
402
+ } else if (url.startsWith('http')) {
403
+ debugLog("🔗 视频URL: %s", url);
404
+ } else {
405
+ debugLog("⚠️ 未知视频格式: %s", url.substring(0, 50));
406
+ }
407
+ }
408
+ break;
409
+
410
+ case 'document_url':
411
+ if (block.document_url?.url) {
412
+ mediaStats.documents++;
413
+ const url = block.document_url.url;
414
+ if (url.startsWith('data:application/')) {
415
+ const mimeMatch = url.match(/data:application\/([^;]+)/);
416
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
417
+ debugLog("📄 文档数据: %s格式, 大小: %d字符", format, url.length);
418
+ } else if (url.startsWith('http')) {
419
+ debugLog("🔗 文档URL: %s", url);
420
+ } else {
421
+ debugLog("⚠️ 未知文档格式: %s", url.substring(0, 50));
422
+ }
423
+ }
424
+ break;
425
+
426
+ case 'audio_url':
427
+ if (block.audio_url?.url) {
428
+ mediaStats.audios++;
429
+ const url = block.audio_url.url;
430
+ if (url.startsWith('data:audio/')) {
431
+ const mimeMatch = url.match(/data:audio\/([^;]+)/);
432
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
433
+ debugLog("🎵 音频数据: %s格式, 大小: %d字符", format, url.length);
434
+ } else if (url.startsWith('http')) {
435
+ debugLog("🔗 音频URL: %s", url);
436
+ } else {
437
+ debugLog("⚠️ 未知音频格式: %s", url.substring(0, 50));
438
+ }
439
+ }
440
+ break;
441
+
442
+ default:
443
+ mediaStats.others++;
444
+ debugLog("❓ 未知内容类型: %s", block.type);
445
+ }
446
+ }
447
+
448
+ // 输出统计信息
449
+ const totalMedia = mediaStats.images + mediaStats.videos + mediaStats.documents + mediaStats.audios;
450
+ if (totalMedia > 0) {
451
+ debugLog("🎯 多模态内容统计: 文本(%d) 图像(%d) 视频(%d) 文档(%d) 音频(%d)",
452
+ mediaStats.text, mediaStats.images, mediaStats.videos, mediaStats.documents, mediaStats.audios);
453
+ }
454
+ }
455
+ } else if (typeof message.content === 'string') {
456
+ debugLog("📝 纯文本消息,长度: %d", message.content.length);
457
+ }
458
+
459
+ processedMessages.push(processedMessage);
460
+ }
461
+
462
+ return processedMessages;
463
+ }
464
+
465
+ const DEBUG_MODE = Deno.env.get("DEBUG_MODE") !== "false"; // 默认为true
466
+ const DEFAULT_STREAM = Deno.env.get("DEFAULT_STREAM") !== "false"; // 默认为true
467
+ const DASHBOARD_ENABLED = Deno.env.get("DASHBOARD_ENABLED") !== "false"; // 默认为true
468
+
469
+ /**
470
+ * 全局状态变量
471
+ */
472
+
473
+ let stats: RequestStats = {
474
+ totalRequests: 0,
475
+ successfulRequests: 0,
476
+ failedRequests: 0,
477
+ lastRequestTime: new Date(),
478
+ averageResponseTime: 0
479
+ };
480
+
481
+ let liveRequests: LiveRequest[] = [];
482
+
483
+ /**
484
+ * 工具函数
485
+ */
486
+
487
+ function debugLog(format: string, ...args: unknown[]): void {
488
+ if (DEBUG_MODE) {
489
+ console.log(`[DEBUG] ${format}`, ...args);
490
+ }
491
+ }
492
+
493
+ function recordRequestStats(startTime: number, path: string, status: number): void {
494
+ const duration = Date.now() - startTime;
495
+
496
+ stats.totalRequests++;
497
+ stats.lastRequestTime = new Date();
498
+
499
+ if (status >= 200 && status < 300) {
500
+ stats.successfulRequests++;
501
+ } else {
502
+ stats.failedRequests++;
503
+ }
504
+
505
+ // 更新平均响应时间
506
+ if (stats.totalRequests > 0) {
507
+ const totalDuration = stats.averageResponseTime * (stats.totalRequests - 1) + duration;
508
+ stats.averageResponseTime = totalDuration / stats.totalRequests;
509
+ } else {
510
+ stats.averageResponseTime = duration;
511
+ }
512
+ }
513
+
514
+ function addLiveRequest(method: string, path: string, status: number, duration: number, userAgent: string, model?: string): void {
515
+ const request: LiveRequest = {
516
+ id: Date.now().toString(),
517
+ timestamp: new Date(),
518
+ method,
519
+ path,
520
+ status,
521
+ duration,
522
+ userAgent,
523
+ model
524
+ };
525
+
526
+ liveRequests.push(request);
527
+
528
+ // 只保留最近的100条请求
529
+ if (liveRequests.length > 100) {
530
+ liveRequests = liveRequests.slice(1);
531
+ }
532
+ }
533
+
534
+ function getLiveRequestsData(): string {
535
+ try {
536
+ // 确保liveRequests是数组
537
+ if (!Array.isArray(liveRequests)) {
538
+ debugLog("liveRequests不是数组,重置为空数组");
539
+ liveRequests = [];
540
+ }
541
+
542
+ // 确保返回的数据格式与前端期望的一致
543
+ const requestData = liveRequests.map(req => ({
544
+ id: req.id || "",
545
+ timestamp: req.timestamp || new Date(),
546
+ method: req.method || "",
547
+ path: req.path || "",
548
+ status: req.status || 0,
549
+ duration: req.duration || 0,
550
+ user_agent: req.userAgent || ""
551
+ }));
552
+
553
+ return JSON.stringify(requestData);
554
+ } catch (error) {
555
+ debugLog("获取实时请求数据失败: %v", error);
556
+ return JSON.stringify([]);
557
+ }
558
+ }
559
+
560
+ function getStatsData(): string {
561
+ try {
562
+ // 确保stats对象存在
563
+ if (!stats) {
564
+ debugLog("stats对象不存在,使用默认值");
565
+ stats = {
566
+ totalRequests: 0,
567
+ successfulRequests: 0,
568
+ failedRequests: 0,
569
+ lastRequestTime: new Date(),
570
+ averageResponseTime: 0
571
+ };
572
+ }
573
+
574
+ // 确保返回的数据格式与前端期望的一致
575
+ const statsData = {
576
+ totalRequests: stats.totalRequests || 0,
577
+ successfulRequests: stats.successfulRequests || 0,
578
+ failedRequests: stats.failedRequests || 0,
579
+ averageResponseTime: stats.averageResponseTime || 0
580
+ };
581
+
582
+ return JSON.stringify(statsData);
583
+ } catch (error) {
584
+ debugLog("获取统计数据失败: %v", error);
585
+ return JSON.stringify({
586
+ totalRequests: 0,
587
+ successfulRequests: 0,
588
+ failedRequests: 0,
589
+ averageResponseTime: 0
590
+ });
591
+ }
592
+ }
593
+
594
+ function getClientIP(request: Request): string {
595
+ // 检查X-Forwarded-For头
596
+ const xff = request.headers.get("X-Forwarded-For");
597
+ if (xff) {
598
+ const ips = xff.split(",");
599
+ if (ips.length > 0) {
600
+ return ips[0].trim();
601
+ }
602
+ }
603
+
604
+ // 检查X-Real-IP头
605
+ const xri = request.headers.get("X-Real-IP");
606
+ if (xri) {
607
+ return xri;
608
+ }
609
+
610
+ // 对于Deno Deploy,我们无法直接获取RemoteAddr,返回一个默认值
611
+ return "unknown";
612
+ }
613
+
614
+ function setCORSHeaders(headers: Headers): void {
615
+ headers.set("Access-Control-Allow-Origin", "*");
616
+ headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
617
+ headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
618
+ headers.set("Access-Control-Allow-Credentials", "true");
619
+ }
620
+
621
+ function validateApiKey(authHeader: string | null): boolean {
622
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
623
+ return false;
624
+ }
625
+
626
+ const apiKey = authHeader.substring(7);
627
+ return apiKey === DEFAULT_KEY;
628
+ }
629
+
630
+ async function getAnonymousToken(): Promise<string> {
631
+ try {
632
+ const response = await fetch(`${ORIGIN_BASE}/api/v1/auths/`, {
633
+ method: "GET",
634
+ headers: {
635
+ "User-Agent": BROWSER_UA,
636
+ "Accept": "*/*",
637
+ "Accept-Language": "zh-CN,zh;q=0.9",
638
+ "X-FE-Version": X_FE_VERSION,
639
+ "sec-ch-ua": SEC_CH_UA,
640
+ "sec-ch-ua-mobile": SEC_CH_UA_MOB,
641
+ "sec-ch-ua-platform": SEC_CH_UA_PLAT,
642
+ "Origin": ORIGIN_BASE,
643
+ "Referer": `${ORIGIN_BASE}/`
644
+ }
645
+ });
646
+
647
+ if (!response.ok) {
648
+ throw new Error(`Anonymous token request failed with status ${response.status}`);
649
+ }
650
+
651
+ const data = await response.json() as { token: string };
652
+ if (!data.token) {
653
+ throw new Error("Anonymous token is empty");
654
+ }
655
+
656
+ return data.token;
657
+ } catch (error) {
658
+ debugLog("获取匿名token失败: %v", error);
659
+ throw error;
660
+ }
661
+ }
662
+
663
+ // 调用上游API
664
+ async function callUpstreamWithHeaders(
665
+ upstreamReq: UpstreamRequest,
666
+ refererChatID: string,
667
+ authToken: string
668
+ ): Promise<Response> {
669
+ try {
670
+ debugLog("调用上游API: %s", UPSTREAM_URL);
671
+
672
+ // 特别检查和记录全方位多模态内容
673
+ const hasMultimedia = upstreamReq.messages.some(msg =>
674
+ Array.isArray(msg.content) &&
675
+ msg.content.some(block =>
676
+ ['image_url', 'video_url', 'document_url', 'audio_url'].includes(block.type)
677
+ )
678
+ );
679
+
680
+ if (hasMultimedia) {
681
+ debugLog("🎯 请求包含多模态数据,正在发送到上游...");
682
+
683
+ for (let i = 0; i < upstreamReq.messages.length; i++) {
684
+ const msg = upstreamReq.messages[i];
685
+ if (Array.isArray(msg.content)) {
686
+ for (let j = 0; j < msg.content.length; j++) {
687
+ const block = msg.content[j];
688
+
689
+ // 处理图像
690
+ if (block.type === 'image_url' && block.image_url?.url) {
691
+ const url = block.image_url.url;
692
+ if (url.startsWith('data:image/')) {
693
+ const mimeMatch = url.match(/data:image\/([^;]+)/);
694
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
695
+ const sizeKB = Math.round(url.length * 0.75 / 1024); // base64 大约是原文件的 1.33 倍
696
+ debugLog("🖼️ 消息[%d] 图像[%d]: %s格式, 数据长度: %d字符 (~%dKB)",
697
+ i, j, format, url.length, sizeKB);
698
+
699
+ // 图片大小警告
700
+ if (sizeKB > 1000) {
701
+ debugLog("⚠️ 图片较大 (%dKB),可能导致上游处理失败", sizeKB);
702
+ debugLog("💡 建议: 将图片压缩到 500KB 以下");
703
+ } else if (sizeKB > 500) {
704
+ debugLog("⚠️ 图片偏大 (%dKB),建议压缩", sizeKB);
705
+ }
706
+ } else {
707
+ debugLog("🔗 消息[%d] 图像[%d]: 外部URL - %s", i, j, url);
708
+ }
709
+ }
710
+
711
+ // 处理视频
712
+ if (block.type === 'video_url' && block.video_url?.url) {
713
+ const url = block.video_url.url;
714
+ if (url.startsWith('data:video/')) {
715
+ const mimeMatch = url.match(/data:video\/([^;]+)/);
716
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
717
+ debugLog("🎥 消息[%d] 视频[%d]: %s格式, 数据长度: %d字符",
718
+ i, j, format, url.length);
719
+ } else {
720
+ debugLog("🔗 消息[%d] 视频[%d]: 外部URL - %s", i, j, url);
721
+ }
722
+ }
723
+
724
+ // 处理文档
725
+ if (block.type === 'document_url' && block.document_url?.url) {
726
+ const url = block.document_url.url;
727
+ if (url.startsWith('data:application/')) {
728
+ const mimeMatch = url.match(/data:application\/([^;]+)/);
729
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
730
+ debugLog("📄 消息[%d] 文档[%d]: %s格式, 数据长度: %d字符",
731
+ i, j, format, url.length);
732
+ } else {
733
+ debugLog("🔗 消息[%d] 文档[%d]: 外部URL - %s", i, j, url);
734
+ }
735
+ }
736
+
737
+ // 处理音频
738
+ if (block.type === 'audio_url' && block.audio_url?.url) {
739
+ const url = block.audio_url.url;
740
+ if (url.startsWith('data:audio/')) {
741
+ const mimeMatch = url.match(/data:audio\/([^;]+)/);
742
+ const format = mimeMatch ? mimeMatch[1] : 'unknown';
743
+ debugLog("🎵 消息[%d] 音频[%d]: %s格式, 数据长度: %d字符",
744
+ i, j, format, url.length);
745
+ } else {
746
+ debugLog("🔗 消息[%d] 音频[%d]: 外部URL - %s", i, j, url);
747
+ }
748
+ }
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ debugLog("上游请求体: %s", JSON.stringify(upstreamReq));
755
+
756
+ const response = await fetch(UPSTREAM_URL, {
757
+ method: "POST",
758
+ headers: {
759
+ "Content-Type": "application/json",
760
+ "Accept": "application/json, text/event-stream",
761
+ "User-Agent": BROWSER_UA,
762
+ "Authorization": `Bearer ${authToken}`,
763
+ "Accept-Language": "zh-CN",
764
+ "sec-ch-ua": SEC_CH_UA,
765
+ "sec-ch-ua-mobile": SEC_CH_UA_MOB,
766
+ "sec-ch-ua-platform": SEC_CH_UA_PLAT,
767
+ "X-FE-Version": X_FE_VERSION,
768
+ "Origin": ORIGIN_BASE,
769
+ "Referer": `${ORIGIN_BASE}/c/${refererChatID}`
770
+ },
771
+ body: JSON.stringify(upstreamReq)
772
+ });
773
+
774
+ debugLog("上游响应状态: %d %s", response.status, response.statusText);
775
+ return response;
776
+ } catch (error) {
777
+ debugLog("调用上游失败: %v", error);
778
+ throw error;
779
+ }
780
+ }
781
+
782
+ function transformThinking(content: string): string {
783
+ // 去 <summary>…</summary>
784
+ let result = content.replace(/<summary>.*?<\/summary>/gs, "");
785
+ // 清理残留自定义标签,如 </thinking>、<Full> 等
786
+ result = result.replace(/<\/thinking>/g, "");
787
+ result = result.replace(/<Full>/g, "");
788
+ result = result.replace(/<\/Full>/g, "");
789
+ result = result.trim();
790
+
791
+ switch (THINK_TAGS_MODE as "strip" | "think" | "raw") {
792
+ case "think":
793
+ result = result.replace(/<details[^>]*>/g, "<thinking>");
794
+ result = result.replace(/<\/details>/g, "</thinking>");
795
+ break;
796
+ case "strip":
797
+ result = result.replace(/<details[^>]*>/g, "");
798
+ result = result.replace(/<\/details>/g, "");
799
+ break;
800
+ }
801
+
802
+ // 处理每行前缀 "> "(包括起始位置)
803
+ result = result.replace(/^> /, "");
804
+ result = result.replace(/\n> /g, "\n");
805
+ return result.trim();
806
+ }
807
+
808
+ async function processUpstreamStream(
809
+ body: ReadableStream<Uint8Array>,
810
+ writer: WritableStreamDefaultWriter<Uint8Array>,
811
+ encoder: TextEncoder,
812
+ modelName: string
813
+ ): Promise<void> {
814
+ const reader = body.getReader();
815
+ const decoder = new TextDecoder();
816
+ let buffer = "";
817
+
818
+ try {
819
+ while (true) {
820
+ const { done, value } = await reader.read();
821
+ if (done) break;
822
+
823
+ buffer += decoder.decode(value, { stream: true });
824
+ const lines = buffer.split('\n');
825
+ buffer = lines.pop() || ""; // 保留最后一个不完整的行
826
+
827
+ for (const line of lines) {
828
+ if (line.startsWith("data: ")) {
829
+ const dataStr = line.substring(6);
830
+ if (dataStr === "") continue;
831
+
832
+ debugLog("收到SSE数据: %s", dataStr);
833
+
834
+ try {
835
+ const upstreamData = JSON.parse(dataStr) as UpstreamData;
836
+
837
+ // 错误检测
838
+ if (upstreamData.error || upstreamData.data.error ||
839
+ (upstreamData.data.inner && upstreamData.data.inner.error)) {
840
+ const errObj = upstreamData.error || upstreamData.data.error ||
841
+ (upstreamData.data.inner && upstreamData.data.inner.error);
842
+ debugLog("上游错误: code=%d, detail=%s", errObj?.code, errObj?.detail);
843
+
844
+ // 分析错误类型,特别是多模态相关错误
845
+ const errorDetail = (errObj?.detail || "").toLowerCase();
846
+ if (errorDetail.includes("something went wrong") || errorDetail.includes("try again later")) {
847
+ debugLog("🚨 Z.ai 服务器错误分析:");
848
+ debugLog(" 📋 错误详情: %s", errObj?.detail);
849
+ debugLog(" 🖼️ 可能原因: 图片处理失败");
850
+ debugLog(" 💡 建议解决方案:");
851
+ debugLog(" 1. 使用更小的图片 (< 500KB)");
852
+ debugLog(" 2. 尝试不同的图片格式 (JPEG 而不是 PNG)");
853
+ debugLog(" 3. 稍后重试 (可能是服务器负载问题)");
854
+ debugLog(" 4. 检查图片是否损坏");
855
+ }
856
+
857
+ // 发送结束chunk
858
+ const endChunk: OpenAIResponse = {
859
+ id: `chatcmpl-${Date.now()}`,
860
+ object: "chat.completion.chunk",
861
+ created: Math.floor(Date.now() / 1000),
862
+ model: modelName,
863
+ choices: [
864
+ {
865
+ index: 0,
866
+ delta: {},
867
+ finish_reason: "stop"
868
+ }
869
+ ]
870
+ };
871
+
872
+ await writer.write(encoder.encode(`data: ${JSON.stringify(endChunk)}\n\n`));
873
+ await writer.write(encoder.encode("data: [DONE]\n\n"));
874
+ return;
875
+ }
876
+
877
+ debugLog("解析成功 - 类型: %s, 阶段: %s, 内容长度: %d, 完成: %v",
878
+ upstreamData.type, upstreamData.data.phase,
879
+ upstreamData.data.delta_content ? upstreamData.data.delta_content.length : 0,
880
+ upstreamData.data.done);
881
+
882
+ // 处理内容
883
+ if (upstreamData.data.delta_content && upstreamData.data.delta_content !== "") {
884
+ let out = upstreamData.data.delta_content;
885
+ if (upstreamData.data.phase === "thinking") {
886
+ out = transformThinking(out);
887
+ }
888
+
889
+ if (out !== "") {
890
+ debugLog("发送内容(%s): %s", upstreamData.data.phase, out);
891
+
892
+ const chunk: OpenAIResponse = {
893
+ id: `chatcmpl-${Date.now()}`,
894
+ object: "chat.completion.chunk",
895
+ created: Math.floor(Date.now() / 1000),
896
+ model: modelName,
897
+ choices: [
898
+ {
899
+ index: 0,
900
+ delta: { content: out }
901
+ }
902
+ ]
903
+ };
904
+
905
+ await writer.write(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
906
+ }
907
+ }
908
+
909
+ // 检查是否结束
910
+ if (upstreamData.data.done || upstreamData.data.phase === "done") {
911
+ debugLog("检测到流结束信号");
912
+
913
+ // 发送结束chunk
914
+ const endChunk: OpenAIResponse = {
915
+ id: `chatcmpl-${Date.now()}`,
916
+ object: "chat.completion.chunk",
917
+ created: Math.floor(Date.now() / 1000),
918
+ model: modelName,
919
+ choices: [
920
+ {
921
+ index: 0,
922
+ delta: {},
923
+ finish_reason: "stop"
924
+ }
925
+ ]
926
+ };
927
+
928
+ await writer.write(encoder.encode(`data: ${JSON.stringify(endChunk)}\n\n`));
929
+ await writer.write(encoder.encode("data: [DONE]\n\n"));
930
+ return;
931
+ }
932
+ } catch (error) {
933
+ debugLog("SSE数据解析失败: %v", error);
934
+ }
935
+ }
936
+ }
937
+ }
938
+ } finally {
939
+ writer.close();
940
+ }
941
+ }
942
+
943
+ // 收集完整响应(用于非流式响应)
944
+ async function collectFullResponse(body: ReadableStream<Uint8Array>): Promise<string> {
945
+ const reader = body.getReader();
946
+ const decoder = new TextDecoder();
947
+ let buffer = "";
948
+ let fullContent = "";
949
+
950
+ try {
951
+ while (true) {
952
+ const { done, value } = await reader.read();
953
+ if (done) break;
954
+
955
+ buffer += decoder.decode(value, { stream: true });
956
+ const lines = buffer.split('\n');
957
+ buffer = lines.pop() || ""; // 保留最后一个不完整的行
958
+
959
+ for (const line of lines) {
960
+ if (line.startsWith("data: ")) {
961
+ const dataStr = line.substring(6);
962
+ if (dataStr === "") continue;
963
+
964
+ try {
965
+ const upstreamData = JSON.parse(dataStr) as UpstreamData;
966
+
967
+ if (upstreamData.data.delta_content !== "") {
968
+ let out = upstreamData.data.delta_content;
969
+ if (upstreamData.data.phase === "thinking") {
970
+ out = transformThinking(out);
971
+ }
972
+
973
+ if (out !== "") {
974
+ fullContent += out;
975
+ }
976
+ }
977
+
978
+ // 检查是否结束
979
+ if (upstreamData.data.done || upstreamData.data.phase === "done") {
980
+ debugLog("检测到完成信号,停止收集");
981
+ return fullContent;
982
+ }
983
+ } catch (error) {
984
+ // 忽略解析错误
985
+ }
986
+ }
987
+ }
988
+ }
989
+ } finally {
990
+ reader.releaseLock();
991
+ }
992
+
993
+ return fullContent;
994
+ }
995
+
996
+ /**
997
+ * HTTP服务器和路由处理
998
+ */
999
+
1000
+ function getIndexHTML(): string {
1001
+ return `<!DOCTYPE html>
1002
+ <html lang="zh-CN">
1003
+ <head>
1004
+ <meta charset="UTF-8">
1005
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1006
+ <title>ZtoApi - OpenAI兼容API代理</title>
1007
+ <style>
1008
+ body {
1009
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1010
+ margin: 0;
1011
+ padding: 0;
1012
+ background-color: #f5f5f5;
1013
+ line-height: 1.6;
1014
+ }
1015
+ .container {
1016
+ max-width: 1200px;
1017
+ margin: 0 auto;
1018
+ background-color: white;
1019
+ border-radius: 8px;
1020
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1021
+ padding: 40px;
1022
+ margin-top: 40px;
1023
+ }
1024
+ header {
1025
+ text-align: center;
1026
+ margin-bottom: 40px;
1027
+ }
1028
+ h1 {
1029
+ color: #333;
1030
+ margin-bottom: 10px;
1031
+ font-size: 2.5rem;
1032
+ }
1033
+ .subtitle {
1034
+ color: #666;
1035
+ font-size: 1.2rem;
1036
+ margin-bottom: 30px;
1037
+ }
1038
+ .links {
1039
+ display: grid;
1040
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1041
+ gap: 20px;
1042
+ margin-top: 40px;
1043
+ }
1044
+ .link-card {
1045
+ background-color: #f8f9fa;
1046
+ border-radius: 8px;
1047
+ padding: 20px;
1048
+ text-align: center;
1049
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
1050
+ border: 1px solid #e9ecef;
1051
+ }
1052
+ .link-card:hover {
1053
+ transform: translateY(-5px);
1054
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
1055
+ }
1056
+ .link-card h3 {
1057
+ margin-top: 0;
1058
+ color: #007bff;
1059
+ }
1060
+ .link-card p {
1061
+ color: #666;
1062
+ margin-bottom: 20px;
1063
+ }
1064
+ .link-card a {
1065
+ display: inline-block;
1066
+ background-color: #007bff;
1067
+ color: white;
1068
+ padding: 10px 20px;
1069
+ border-radius: 4px;
1070
+ text-decoration: none;
1071
+ font-weight: bold;
1072
+ transition: background-color 0.3s ease;
1073
+ }
1074
+ .link-card a:hover {
1075
+ background-color: #0056b3;
1076
+ }
1077
+ .features {
1078
+ margin-top: 60px;
1079
+ }
1080
+ .features h2 {
1081
+ text-align: center;
1082
+ color: #333;
1083
+ margin-bottom: 30px;
1084
+ }
1085
+ .feature-list {
1086
+ display: grid;
1087
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1088
+ gap: 20px;
1089
+ }
1090
+ .feature-item {
1091
+ text-align: center;
1092
+ padding: 20px;
1093
+ }
1094
+ .feature-item i {
1095
+ font-size: 2rem;
1096
+ color: #007bff;
1097
+ margin-bottom: 15px;
1098
+ }
1099
+ .feature-item h3 {
1100
+ color: #333;
1101
+ margin-bottom: 10px;
1102
+ }
1103
+ .feature-item p {
1104
+ color: #666;
1105
+ }
1106
+ footer {
1107
+ text-align: center;
1108
+ margin-top: 60px;
1109
+ padding-top: 20px;
1110
+ border-top: 1px solid #e9ecef;
1111
+ color: #666;
1112
+ }
1113
+ </style>
1114
+ </head>
1115
+ <body>
1116
+ <div class="container">
1117
+ <header>
1118
+ <h1>ZtoApi</h1>
1119
+ <div class="subtitle">OpenAI兼容API代理 for Z.ai GLM-4.5</div>
1120
+ <p>一个高性能、易于部署的API代理服务,让你能够使用OpenAI兼容的格式访问Z.ai的GLM-4.5模型。</p>
1121
+ </header>
1122
+
1123
+ <div class="links">
1124
+ <div class="link-card">
1125
+ <h3>📖 API文档</h3>
1126
+ <p>查看完整的API文档,了解如何使用本服务。</p>
1127
+ <a href="/docs">查看文档</a>
1128
+ </div>
1129
+
1130
+ <div class="link-card">
1131
+ <h3>📊 API调用看板</h3>
1132
+ <p>实时监控API调用情况,查看请求统计和性能指标。</p>
1133
+ <a href="/dashboard">查看看板</a>
1134
+ </div>
1135
+
1136
+ <div class="link-card">
1137
+ <h3>🤖 模型列表</h3>
1138
+ <p>查看可用的AI模型列表及其详细信息。</p>
1139
+ <a href="/v1/models">查看模型</a>
1140
+ </div>
1141
+ </div>
1142
+
1143
+ <div class="features">
1144
+ <h2>功能特性</h2>
1145
+ <div class="feature-list">
1146
+ <div class="feature-item">
1147
+ <div>🔄</div>
1148
+ <h3>OpenAI API兼容</h3>
1149
+ <p>完全兼容OpenAI的API格式,无需修改客户端代码</p>
1150
+ </div>
1151
+
1152
+ <div class="feature-item">
1153
+ <div>🌊</div>
1154
+ <h3>流式响应支持</h3>
1155
+ <p>支持实时流式输出,提供更好的用户体验</p>
1156
+ </div>
1157
+
1158
+ <div class="feature-item">
1159
+ <div>🔐</div>
1160
+ <h3>身份验证</h3>
1161
+ <p>支持API密钥验证,确保服务安全</p>
1162
+ </div>
1163
+
1164
+ <div class="feature-item">
1165
+ <div>🛠️</div>
1166
+ <h3>灵活配置</h3>
1167
+ <p>通过环境变量进行灵活配置</p>
1168
+ </div>
1169
+
1170
+ <div class="feature-item">
1171
+ <div>📝</div>
1172
+ <h3>思考过程展示</h3>
1173
+ <p>智能处理并展示模型的思考过程</p>
1174
+ </div>
1175
+
1176
+ <div class="feature-item">
1177
+ <div>📊</div>
1178
+ <h3>实时监控</h3>
1179
+ <p>提供Web仪表板,实时显示API转发情况和统计信息</p>
1180
+ </div>
1181
+ </div>
1182
+ </div>
1183
+
1184
+ <footer>
1185
+ <p>© 2024 ZtoApi. Powered by Deno & Z.ai GLM-4.5</p>
1186
+ </footer>
1187
+ </div>
1188
+ </body>
1189
+ </html>`;
1190
+ }
1191
+
1192
+ async function handleIndex(request: Request): Promise<Response> {
1193
+ if (request.method !== "GET") {
1194
+ return new Response("Method not allowed", { status: 405 });
1195
+ }
1196
+
1197
+ return new Response(getIndexHTML(), {
1198
+ status: 200,
1199
+ headers: {
1200
+ "Content-Type": "text/html; charset=utf-8"
1201
+ }
1202
+ });
1203
+ }
1204
+
1205
+ async function handleOptions(request: Request): Promise<Response> {
1206
+ const headers = new Headers();
1207
+ setCORSHeaders(headers);
1208
+
1209
+ if (request.method === "OPTIONS") {
1210
+ return new Response(null, { status: 200, headers });
1211
+ }
1212
+
1213
+ return new Response("Not Found", { status: 404, headers });
1214
+ }
1215
+
1216
+ async function handleModels(request: Request): Promise<Response> {
1217
+ const headers = new Headers();
1218
+ setCORSHeaders(headers);
1219
+
1220
+ if (request.method === "OPTIONS") {
1221
+ return new Response(null, { status: 200, headers });
1222
+ }
1223
+
1224
+ // 支持的模型
1225
+ const models = SUPPORTED_MODELS.map(model => ({
1226
+ id: model.name,
1227
+ object: "model",
1228
+ created: Math.floor(Date.now() / 1000),
1229
+ owned_by: "z.ai"
1230
+ }));
1231
+
1232
+ const response: ModelsResponse = {
1233
+ object: "list",
1234
+ data: models
1235
+ };
1236
+
1237
+ headers.set("Content-Type", "application/json");
1238
+ return new Response(JSON.stringify(response), {
1239
+ status: 200,
1240
+ headers
1241
+ });
1242
+ }
1243
+
1244
+ async function handleChatCompletions(request: Request): Promise<Response> {
1245
+ const startTime = Date.now();
1246
+ const url = new URL(request.url);
1247
+ const path = url.pathname;
1248
+ const userAgent = request.headers.get("User-Agent") || "";
1249
+
1250
+ debugLog("收到chat completions请求");
1251
+ debugLog("🌐 User-Agent: %s", userAgent);
1252
+
1253
+ // Cherry Studio 检测
1254
+ const isCherryStudio = userAgent.toLowerCase().includes('cherry') || userAgent.toLowerCase().includes('studio');
1255
+ if (isCherryStudio) {
1256
+ debugLog("🍒 检测到 Cherry Studio 客户端版本: %s",
1257
+ userAgent.match(/CherryStudio\/([^\s]+)/)?.[1] || 'unknown');
1258
+ }
1259
+
1260
+ const headers = new Headers();
1261
+ setCORSHeaders(headers);
1262
+
1263
+ if (request.method === "OPTIONS") {
1264
+ return new Response(null, { status: 200, headers });
1265
+ }
1266
+
1267
+ // 验证API Key
1268
+ const authHeader = request.headers.get("Authorization");
1269
+ if (!validateApiKey(authHeader)) {
1270
+ debugLog("缺少或无效的Authorization头");
1271
+ const duration = Date.now() - startTime;
1272
+ recordRequestStats(startTime, path, 401);
1273
+ addLiveRequest(request.method, path, 401, duration, userAgent);
1274
+ return new Response("Missing or invalid Authorization header", {
1275
+ status: 401,
1276
+ headers
1277
+ });
1278
+ }
1279
+
1280
+ debugLog("API key验证通过");
1281
+
1282
+ // 读取请求体
1283
+ let body: string;
1284
+ try {
1285
+ body = await request.text();
1286
+ debugLog("📥 收到请求体长度: %d 字符", body.length);
1287
+
1288
+ // 为Cherry Studio调试:记录原始请求体(截取前1000字符避免日志过长)
1289
+ const bodyPreview = body.length > 1000 ? body.substring(0, 1000) + "..." : body;
1290
+ debugLog("📄 请求体预览: %s", bodyPreview);
1291
+ } catch (error) {
1292
+ debugLog("读取请求体失败: %v", error);
1293
+ const duration = Date.now() - startTime;
1294
+ recordRequestStats(startTime, path, 400);
1295
+ addLiveRequest(request.method, path, 400, duration, userAgent);
1296
+ return new Response("Failed to read request body", {
1297
+ status: 400,
1298
+ headers
1299
+ });
1300
+ }
1301
+
1302
+ // 解析请求
1303
+ let req: OpenAIRequest;
1304
+ try {
1305
+ req = JSON.parse(body) as OpenAIRequest;
1306
+ debugLog("✅ JSON解析成功");
1307
+ } catch (error) {
1308
+ debugLog("JSON解析失败: %v", error);
1309
+ const duration = Date.now() - startTime;
1310
+ recordRequestStats(startTime, path, 400);
1311
+ addLiveRequest(request.method, path, 400, duration, userAgent);
1312
+ return new Response("Invalid JSON", {
1313
+ status: 400,
1314
+ headers
1315
+ });
1316
+ }
1317
+
1318
+ // 如果客户端没有明确指定stream参数,使用默认值
1319
+ if (!body.includes('"stream"')) {
1320
+ req.stream = DEFAULT_STREAM;
1321
+ debugLog("客户端未指定stream参数,使用默认值: %v", DEFAULT_STREAM);
1322
+ }
1323
+
1324
+ // 获取模型配置
1325
+ const modelConfig = getModelConfig(req.model);
1326
+ debugLog("请求解析成功 - 模型: %s (%s), 流式: %v, 消息数: %d", req.model, modelConfig.name, req.stream, req.messages.length);
1327
+
1328
+ // Cherry Studio 调试:详细检查每条消息
1329
+ debugLog("🔍 Cherry Studio 调试 - 检查原始消息:");
1330
+ for (let i = 0; i < req.messages.length; i++) {
1331
+ const msg = req.messages[i];
1332
+ debugLog(" 消息[%d] role: %s", i, msg.role);
1333
+
1334
+ if (typeof msg.content === 'string') {
1335
+ debugLog(" 消息[%d] content: 字符串类型, 长度: %d", i, msg.content.length);
1336
+ if (msg.content.length === 0) {
1337
+ debugLog(" ⚠️ 消息[%d] 内容为空字符串!", i);
1338
+ } else {
1339
+ debugLog(" 消息[%d] 内容预览: %s", i, msg.content.substring(0, 100));
1340
+ }
1341
+ } else if (Array.isArray(msg.content)) {
1342
+ debugLog(" 消息[%d] content: 数组类型, 块数: %d", i, msg.content.length);
1343
+ for (let j = 0; j < msg.content.length; j++) {
1344
+ const block = msg.content[j];
1345
+ debugLog(" 块[%d] type: %s", j, block.type);
1346
+ if (block.type === 'text' && block.text) {
1347
+ debugLog(" 块[%d] text: %s", j, block.text.substring(0, 50));
1348
+ } else if (block.type === 'image_url' && block.image_url?.url) {
1349
+ debugLog(" 块[%d] image_url: %s格式, 长度: %d", j,
1350
+ block.image_url.url.startsWith('data:') ? 'base64' : 'url',
1351
+ block.image_url.url.length);
1352
+ }
1353
+ }
1354
+ } else {
1355
+ debugLog(" ⚠️ 消息[%d] content 类型异常: %s", i, typeof msg.content);
1356
+ }
1357
+ }
1358
+
1359
+ // 处理和验证消息(特别是多模态内容)
1360
+ const processedMessages = processMessages(req.messages, modelConfig);
1361
+ debugLog("消息处理完成,处理后消息数: %d", processedMessages.length);
1362
+
1363
+ // 检查是否包含多模态内容
1364
+ const hasMultimodal = processedMessages.some(msg =>
1365
+ Array.isArray(msg.content) &&
1366
+ msg.content.some(block =>
1367
+ ['image_url', 'video_url', 'document_url', 'audio_url'].includes(block.type)
1368
+ )
1369
+ );
1370
+
1371
+ if (hasMultimodal) {
1372
+ debugLog("🎯 检测到全方位多模态请求,模型: %s", modelConfig.name);
1373
+ if (!modelConfig.capabilities.vision) {
1374
+ debugLog("❌ 严重错误: 模型不支持多模态,但收到了多媒体内容!");
1375
+ debugLog("💡 Cherry Studio用户请检查: 确认选择了 'glm-4.5v' 而不是 'GLM-4.5'");
1376
+ debugLog("🔧 模型映射状态: %s → %s (vision: %s)",
1377
+ req.model, modelConfig.upstreamId, modelConfig.capabilities.vision);
1378
+ } else {
1379
+ debugLog("✅ GLM-4.5V支持全方位多模态理解:图像、视频、文档、音频");
1380
+
1381
+ // 检查是否使用匿名token(多模态功能的重要限制)
1382
+ if (!ZAI_TOKEN || ZAI_TOKEN.trim() === "") {
1383
+ debugLog("⚠️ 重要警告: 正在使用匿名token处理多模态请求");
1384
+ debugLog("💡 Z.ai的匿名token可能不支持图像/视频/文档处理");
1385
+ debugLog("🔧 解决方案: 设置 ZAI_TOKEN 环境变量为正式的API Token");
1386
+ debugLog("📋 如果请求失败,这很可能是token权限问题");
1387
+ } else {
1388
+ debugLog("✅ 使用正式API Token,支持完整多模态功能");
1389
+ }
1390
+ }
1391
+ } else if (modelConfig.capabilities.vision && modelConfig.id === 'glm-4.5v') {
1392
+ debugLog("ℹ️ 使用GLM-4.5V模型但未检测到多媒体数据,仅处理文本内容");
1393
+ }
1394
+
1395
+ // 生成会话相关ID
1396
+ const chatID = `${Date.now()}-${Math.floor(Date.now() / 1000)}`;
1397
+ const msgID = Date.now().toString();
1398
+
1399
+ // 构造上游请求
1400
+ const upstreamReq: UpstreamRequest = {
1401
+ stream: true, // 总是使用流式从上游获取
1402
+ chat_id: chatID,
1403
+ id: msgID,
1404
+ model: modelConfig.upstreamId,
1405
+ messages: processedMessages,
1406
+ params: modelConfig.defaultParams,
1407
+ features: {
1408
+ enable_thinking: modelConfig.capabilities.thinking,
1409
+ image_generation: false,
1410
+ web_search: false,
1411
+ auto_web_search: false,
1412
+ preview_mode: modelConfig.capabilities.vision
1413
+ },
1414
+ background_tasks: {
1415
+ title_generation: false,
1416
+ tags_generation: false
1417
+ },
1418
+ mcp_servers: modelConfig.capabilities.mcp ? [] : undefined,
1419
+ model_item: {
1420
+ id: modelConfig.upstreamId,
1421
+ name: modelConfig.name,
1422
+ owned_by: "openai",
1423
+ openai: {
1424
+ id: modelConfig.upstreamId,
1425
+ name: modelConfig.upstreamId,
1426
+ owned_by: "openai",
1427
+ openai: {
1428
+ id: modelConfig.upstreamId
1429
+ },
1430
+ urlIdx: 1
1431
+ },
1432
+ urlIdx: 1,
1433
+ info: {
1434
+ id: modelConfig.upstreamId,
1435
+ user_id: "api-user",
1436
+ base_model_id: null,
1437
+ name: modelConfig.name,
1438
+ params: modelConfig.defaultParams,
1439
+ meta: {
1440
+ profile_image_url: "/static/favicon.png",
1441
+ description: modelConfig.capabilities.vision ? "Advanced visual understanding and analysis" : "Most advanced model, proficient in coding and tool use",
1442
+ capabilities: {
1443
+ vision: modelConfig.capabilities.vision,
1444
+ citations: false,
1445
+ preview_mode: modelConfig.capabilities.vision,
1446
+ web_search: false,
1447
+ language_detection: false,
1448
+ restore_n_source: false,
1449
+ mcp: modelConfig.capabilities.mcp,
1450
+ file_qa: modelConfig.capabilities.mcp,
1451
+ returnFc: true,
1452
+ returnThink: modelConfig.capabilities.thinking,
1453
+ think: modelConfig.capabilities.thinking
1454
+ }
1455
+ }
1456
+ }
1457
+ },
1458
+ tool_servers: [],
1459
+ variables: {
1460
+ "{{USER_NAME}}": `Guest-${Date.now()}`,
1461
+ "{{USER_LOCATION}}": "Unknown",
1462
+ "{{CURRENT_DATETIME}}": new Date().toLocaleString('zh-CN'),
1463
+ "{{CURRENT_DATE}}": new Date().toLocaleDateString('zh-CN'),
1464
+ "{{CURRENT_TIME}}": new Date().toLocaleTimeString('zh-CN'),
1465
+ "{{CURRENT_WEEKDAY}}": new Date().toLocaleDateString('zh-CN', { weekday: 'long' }),
1466
+ "{{CURRENT_TIMEZONE}}": "Asia/Shanghai",
1467
+ "{{USER_LANGUAGE}}": "zh-CN"
1468
+ }
1469
+ };
1470
+
1471
+ // 选择本次对话使用的token
1472
+ let authToken = ZAI_TOKEN;
1473
+ if (ANON_TOKEN_ENABLED) {
1474
+ try {
1475
+ const anonToken = await getAnonymousToken();
1476
+ authToken = anonToken;
1477
+ debugLog("匿名token获取成功: %s...", anonToken.substring(0, 10));
1478
+ } catch (error) {
1479
+ debugLog("匿名token获取失败,回退固定token: %v", error);
1480
+ }
1481
+ }
1482
+
1483
+ // 调用上游API
1484
+ try {
1485
+ if (req.stream) {
1486
+ return await handleStreamResponse(upstreamReq, chatID, authToken, startTime, path, userAgent, req, modelConfig);
1487
+ } else {
1488
+ return await handleNonStreamResponse(upstreamReq, chatID, authToken, startTime, path, userAgent, req, modelConfig);
1489
+ }
1490
+ } catch (error) {
1491
+ debugLog("调用上游失败: %v", error);
1492
+ const duration = Date.now() - startTime;
1493
+ recordRequestStats(startTime, path, 502);
1494
+ addLiveRequest(request.method, path, 502, duration, userAgent);
1495
+ return new Response("Failed to call upstream", {
1496
+ status: 502,
1497
+ headers
1498
+ });
1499
+ }
1500
+ }
1501
+
1502
+ async function handleStreamResponse(
1503
+ upstreamReq: UpstreamRequest,
1504
+ chatID: string,
1505
+ authToken: string,
1506
+ startTime: number,
1507
+ path: string,
1508
+ userAgent: string,
1509
+ req: OpenAIRequest,
1510
+ modelConfig: ModelConfig
1511
+ ): Promise<Response> {
1512
+ debugLog("开始处理流式响应 (chat_id=%s)", chatID);
1513
+
1514
+ try {
1515
+ const response = await callUpstreamWithHeaders(upstreamReq, chatID, authToken);
1516
+
1517
+ if (!response.ok) {
1518
+ debugLog("上游返回错误状态: %d", response.status);
1519
+ const duration = Date.now() - startTime;
1520
+ recordRequestStats(startTime, path, 502);
1521
+ addLiveRequest("POST", path, 502, duration, userAgent);
1522
+ return new Response("Upstream error", { status: 502 });
1523
+ }
1524
+
1525
+ if (!response.body) {
1526
+ debugLog("上游响应体为空");
1527
+ const duration = Date.now() - startTime;
1528
+ recordRequestStats(startTime, path, 502);
1529
+ addLiveRequest("POST", path, 502, duration, userAgent);
1530
+ return new Response("Upstream response body is empty", { status: 502 });
1531
+ }
1532
+
1533
+ // 创建可读流
1534
+ const { readable, writable } = new TransformStream();
1535
+ const writer = writable.getWriter();
1536
+ const encoder = new TextEncoder();
1537
+
1538
+ // 发送第一个chunk(role)
1539
+ const firstChunk: OpenAIResponse = {
1540
+ id: `chatcmpl-${Date.now()}`,
1541
+ object: "chat.completion.chunk",
1542
+ created: Math.floor(Date.now() / 1000),
1543
+ model: req.model,
1544
+ choices: [
1545
+ {
1546
+ index: 0,
1547
+ delta: { role: "assistant" }
1548
+ }
1549
+ ]
1550
+ };
1551
+
1552
+ // 写入第一个chunk
1553
+ writer.write(encoder.encode(`data: ${JSON.stringify(firstChunk)}\n\n`));
1554
+
1555
+ // 处理上游SSE流
1556
+ processUpstreamStream(response.body, writer, encoder, req.model).catch(error => {
1557
+ debugLog("处理上游流时出错: %v", error);
1558
+ });
1559
+
1560
+ // 记录成功请求统计
1561
+ const duration = Date.now() - startTime;
1562
+ recordRequestStats(startTime, path, 200);
1563
+ addLiveRequest("POST", path, 200, duration, userAgent, modelConfig.name);
1564
+
1565
+ return new Response(readable, {
1566
+ status: 200,
1567
+ headers: {
1568
+ "Content-Type": "text/event-stream",
1569
+ "Cache-Control": "no-cache",
1570
+ "Connection": "keep-alive",
1571
+ "Access-Control-Allow-Origin": "*",
1572
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1573
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
1574
+ "Access-Control-Allow-Credentials": "true"
1575
+ }
1576
+ });
1577
+ } catch (error) {
1578
+ debugLog("处理流式响应时出错: %v", error);
1579
+ const duration = Date.now() - startTime;
1580
+ recordRequestStats(startTime, path, 502);
1581
+ addLiveRequest("POST", path, 502, duration, userAgent);
1582
+ return new Response("Failed to process stream response", { status: 502 });
1583
+ }
1584
+ }
1585
+
1586
+ async function handleNonStreamResponse(
1587
+ upstreamReq: UpstreamRequest,
1588
+ chatID: string,
1589
+ authToken: string,
1590
+ startTime: number,
1591
+ path: string,
1592
+ userAgent: string,
1593
+ req: OpenAIRequest,
1594
+ modelConfig: ModelConfig
1595
+ ): Promise<Response> {
1596
+ debugLog("开始处理非流式响应 (chat_id=%s)", chatID);
1597
+
1598
+ try {
1599
+ const response = await callUpstreamWithHeaders(upstreamReq, chatID, authToken);
1600
+
1601
+ if (!response.ok) {
1602
+ debugLog("上游返回错误状态: %d", response.status);
1603
+ const duration = Date.now() - startTime;
1604
+ recordRequestStats(startTime, path, 502);
1605
+ addLiveRequest("POST", path, 502, duration, userAgent);
1606
+ return new Response("Upstream error", { status: 502 });
1607
+ }
1608
+
1609
+ if (!response.body) {
1610
+ debugLog("上游响应体为空");
1611
+ const duration = Date.now() - startTime;
1612
+ recordRequestStats(startTime, path, 502);
1613
+ addLiveRequest("POST", path, 502, duration, userAgent);
1614
+ return new Response("Upstream response body is empty", { status: 502 });
1615
+ }
1616
+
1617
+ // 收集完整响应
1618
+ const finalContent = await collectFullResponse(response.body);
1619
+ debugLog("内容收集完成,最终长度: %d", finalContent.length);
1620
+
1621
+ // 构造完整响应
1622
+ const openAIResponse: OpenAIResponse = {
1623
+ id: `chatcmpl-${Date.now()}`,
1624
+ object: "chat.completion",
1625
+ created: Math.floor(Date.now() / 1000),
1626
+ model: req.model,
1627
+ choices: [
1628
+ {
1629
+ index: 0,
1630
+ message: {
1631
+ role: "assistant",
1632
+ content: finalContent
1633
+ },
1634
+ finish_reason: "stop"
1635
+ }
1636
+ ],
1637
+ usage: {
1638
+ prompt_tokens: 0,
1639
+ completion_tokens: 0,
1640
+ total_tokens: 0
1641
+ }
1642
+ };
1643
+
1644
+ // 记录成功请求统计
1645
+ const duration = Date.now() - startTime;
1646
+ recordRequestStats(startTime, path, 200);
1647
+ addLiveRequest("POST", path, 200, duration, userAgent, modelConfig.name);
1648
+
1649
+ return new Response(JSON.stringify(openAIResponse), {
1650
+ status: 200,
1651
+ headers: {
1652
+ "Content-Type": "application/json",
1653
+ "Access-Control-Allow-Origin": "*",
1654
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1655
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
1656
+ "Access-Control-Allow-Credentials": "true"
1657
+ }
1658
+ });
1659
+ } catch (error) {
1660
+ debugLog("处理非流式响应时出错: %v", error);
1661
+ const duration = Date.now() - startTime;
1662
+ recordRequestStats(startTime, path, 502);
1663
+ addLiveRequest("POST", path, 502, duration, userAgent);
1664
+ return new Response("Failed to process non-stream response", { status: 502 });
1665
+ }
1666
+ }
1667
+
1668
+ /**
1669
+ * 生成 Dashboard 监控页面HTML模板
1670
+ * 提供实时API调用监控和统计信息展示
1671
+ * @returns string 完整的HTML页面内容
1672
+ */
1673
+ function getDashboardHTML(): string {
1674
+ return `<!DOCTYPE html>
1675
+ <html lang="zh-CN">
1676
+ <head>
1677
+ <meta charset="UTF-8">
1678
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1679
+ <title>API调用看板</title>
1680
+ <style>
1681
+ body {
1682
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1683
+ margin: 0;
1684
+ padding: 20px;
1685
+ background-color: #f5f5f5;
1686
+ }
1687
+ .container {
1688
+ max-width: 1200px;
1689
+ margin: 0 auto;
1690
+ background-color: white;
1691
+ border-radius: 8px;
1692
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1693
+ padding: 20px;
1694
+ }
1695
+ h1 {
1696
+ color: #333;
1697
+ text-align: center;
1698
+ margin-bottom: 30px;
1699
+ }
1700
+ .stats-container {
1701
+ display: grid;
1702
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1703
+ gap: 20px;
1704
+ margin-bottom: 30px;
1705
+ }
1706
+ .stat-card {
1707
+ background-color: #f8f9fa;
1708
+ border-radius: 6px;
1709
+ padding: 15px;
1710
+ text-align: center;
1711
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1712
+ }
1713
+ .stat-value {
1714
+ font-size: 24px;
1715
+ font-weight: bold;
1716
+ color: #007bff;
1717
+ }
1718
+ .stat-label {
1719
+ font-size: 14px;
1720
+ color: #6c757d;
1721
+ margin-top: 5px;
1722
+ }
1723
+ .requests-container {
1724
+ margin-top: 30px;
1725
+ }
1726
+ .requests-table {
1727
+ width: 100%;
1728
+ border-collapse: collapse;
1729
+ }
1730
+ .requests-table th, .requests-table td {
1731
+ padding: 10px;
1732
+ text-align: left;
1733
+ border-bottom: 1px solid #ddd;
1734
+ }
1735
+ .requests-table th {
1736
+ background-color: #f8f9fa;
1737
+ }
1738
+ .status-success {
1739
+ color: #28a745;
1740
+ }
1741
+ .status-error {
1742
+ color: #dc3545;
1743
+ }
1744
+ .refresh-info {
1745
+ text-align: center;
1746
+ margin-top: 20px;
1747
+ color: #6c757d;
1748
+ font-size: 14px;
1749
+ }
1750
+ .pagination-container {
1751
+ display: flex;
1752
+ justify-content: center;
1753
+ align-items: center;
1754
+ margin-top: 20px;
1755
+ gap: 10px;
1756
+ }
1757
+ .pagination-container button {
1758
+ padding: 5px 10px;
1759
+ background-color: #007bff;
1760
+ color: white;
1761
+ border: none;
1762
+ border-radius: 4px;
1763
+ cursor: pointer;
1764
+ }
1765
+ .pagination-container button:disabled {
1766
+ background-color: #cccccc;
1767
+ cursor: not-allowed;
1768
+ }
1769
+ .pagination-container button:hover:not(:disabled) {
1770
+ background-color: #0056b3;
1771
+ }
1772
+ .chart-container {
1773
+ margin-top: 30px;
1774
+ height: 300px;
1775
+ background-color: #f8f9fa;
1776
+ border-radius: 6px;
1777
+ padding: 15px;
1778
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1779
+ }
1780
+ </style>
1781
+ </head>
1782
+ <body>
1783
+ <div class="container">
1784
+ <h1>API调用看板</h1>
1785
+
1786
+ <div class="stats-container">
1787
+ <div class="stat-card">
1788
+ <div class="stat-value" id="total-requests">0</div>
1789
+ <div class="stat-label">总请求数</div>
1790
+ </div>
1791
+ <div class="stat-card">
1792
+ <div class="stat-value" id="successful-requests">0</div>
1793
+ <div class="stat-label">成功请求</div>
1794
+ </div>
1795
+ <div class="stat-card">
1796
+ <div class="stat-value" id="failed-requests">0</div>
1797
+ <div class="stat-label">失败请求</div>
1798
+ </div>
1799
+ <div class="stat-card">
1800
+ <div class="stat-value" id="avg-response-time">0s</div>
1801
+ <div class="stat-label">平均响应时间</div>
1802
+ </div>
1803
+ </div>
1804
+
1805
+ <div class="chart-container">
1806
+ <h2>请求统计图表</h2>
1807
+ <canvas id="requestsChart"></canvas>
1808
+ </div>
1809
+
1810
+ <div class="requests-container">
1811
+ <h2>实时请求</h2>
1812
+ <table class="requests-table">
1813
+ <thead>
1814
+ <tr>
1815
+ <th>时间</th>
1816
+ <th>模型</th>
1817
+ <th>方法</th>
1818
+ <th>状态</th>
1819
+ <th>耗时</th>
1820
+ <th>User Agent</th>
1821
+ </tr>
1822
+ </thead>
1823
+ <tbody id="requests-tbody">
1824
+ <!-- 请求记录将通过JavaScript动态添加 -->
1825
+ </tbody>
1826
+ </table>
1827
+ <div class="pagination-container">
1828
+ <button id="prev-page" disabled>上一页</button>
1829
+ <span id="page-info">第 1 页,共 1 页</span>
1830
+ <button id="next-page" disabled>下一页</button>
1831
+ </div>
1832
+ </div>
1833
+
1834
+ <div class="refresh-info">
1835
+ 数据每5秒自动刷新一次
1836
+ </div>
1837
+ </div>
1838
+
1839
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1840
+ <script>
1841
+ // 全局变量
1842
+ let allRequests = [];
1843
+ let currentPage = 1;
1844
+ const itemsPerPage = 10;
1845
+ let requestsChart = null;
1846
+
1847
+ // 更新统计数据
1848
+ function updateStats() {
1849
+ fetch('/dashboard/stats')
1850
+ .then(response => response.json())
1851
+ .then(data => {
1852
+ document.getElementById('total-requests').textContent = data.totalRequests || 0;
1853
+ document.getElementById('successful-requests').textContent = data.successfulRequests || 0;
1854
+ document.getElementById('failed-requests').textContent = data.failedRequests || 0;
1855
+ document.getElementById('avg-response-time').textContent = ((data.averageResponseTime || 0) / 1000).toFixed(2) + 's';
1856
+ })
1857
+ .catch(error => console.error('Error fetching stats:', error));
1858
+ }
1859
+
1860
+ // 更新请求列表
1861
+ function updateRequests() {
1862
+ fetch('/dashboard/requests')
1863
+ .then(response => response.json())
1864
+ .then(data => {
1865
+ // 检查数据是否为数组
1866
+ if (!Array.isArray(data)) {
1867
+ console.error('返回的数据不是数组:', data);
1868
+ return;
1869
+ }
1870
+
1871
+ // 保存所有请求数据
1872
+ allRequests = data;
1873
+
1874
+ // 按时间倒序排列
1875
+ allRequests.sort((a, b) => {
1876
+ const timeA = new Date(a.timestamp);
1877
+ const timeB = new Date(b.timestamp);
1878
+ return timeB - timeA;
1879
+ });
1880
+
1881
+ // 更新表格
1882
+ updateTable();
1883
+
1884
+ // 更新图表
1885
+ updateChart();
1886
+
1887
+ // 更新分页信息
1888
+ updatePagination();
1889
+ })
1890
+ .catch(error => console.error('Error fetching requests:', error));
1891
+ }
1892
+
1893
+ // 更新表格显示
1894
+ function updateTable() {
1895
+ const tbody = document.getElementById('requests-tbody');
1896
+ tbody.innerHTML = '';
1897
+
1898
+ // 计算当前页的数据范围
1899
+ const startIndex = (currentPage - 1) * itemsPerPage;
1900
+ const endIndex = startIndex + itemsPerPage;
1901
+ const currentRequests = allRequests.slice(startIndex, endIndex);
1902
+
1903
+ currentRequests.forEach(request => {
1904
+ const row = document.createElement('tr');
1905
+
1906
+ // 格式化时间 - 检查时间戳是否有效
1907
+ let timeStr = "Invalid Date";
1908
+ if (request.timestamp) {
1909
+ try {
1910
+ const time = new Date(request.timestamp);
1911
+ if (!isNaN(time.getTime())) {
1912
+ timeStr = time.toLocaleTimeString();
1913
+ }
1914
+ } catch (e) {
1915
+ console.error("时间格式化错误:", e);
1916
+ }
1917
+ }
1918
+
1919
+ // 判断模型名称
1920
+ let modelName = "GLM-4.5";
1921
+ if (request.path && request.path.includes('glm-4.5v')) {
1922
+ modelName = "GLM-4.5V";
1923
+ } else if (request.model) {
1924
+ modelName = request.model;
1925
+ }
1926
+
1927
+ // 状态样式
1928
+ const statusClass = request.status >= 200 && request.status < 300 ? 'status-success' : 'status-error';
1929
+ const status = request.status || "undefined";
1930
+
1931
+ // 截断 User Agent,避免过长
1932
+ let userAgent = request.user_agent || "undefined";
1933
+ if (userAgent.length > 30) {
1934
+ userAgent = userAgent.substring(0, 30) + "...";
1935
+ }
1936
+
1937
+ row.innerHTML = "<td>" + timeStr + "</td>" + "<td>" + modelName + "</td>" + "<td>" + (request.method || "undefined") + "</td>" + "<td class='" + statusClass + "'>" + status + "</td>" + "<td>" + ((request.duration / 1000).toFixed(2) || "undefined") + "s</td>" + "<td title='" + (request.user_agent || "") + "'>" + userAgent + "</td>";
1938
+
1939
+ tbody.appendChild(row);
1940
+ });
1941
+ }
1942
+
1943
+ // 更新分页信息
1944
+ function updatePagination() {
1945
+ const totalPages = Math.ceil(allRequests.length / itemsPerPage);
1946
+ document.getElementById('page-info').textContent = "第 " + currentPage + " 页,共 " + totalPages + " 页";
1947
+
1948
+ document.getElementById('prev-page').disabled = currentPage <= 1;
1949
+ document.getElementById('next-page').disabled = currentPage >= totalPages;
1950
+ }
1951
+
1952
+ // 更新图表
1953
+ function updateChart() {
1954
+ const ctx = document.getElementById('requestsChart').getContext('2d');
1955
+
1956
+ // 准备图表数据 - 最近20条请求的响应时间
1957
+ const chartData = allRequests.slice(0, 20).reverse();
1958
+ const labels = chartData.map(req => {
1959
+ const time = new Date(req.timestamp);
1960
+ return time.toLocaleTimeString();
1961
+ });
1962
+ const responseTimes = chartData.map(req => req.duration);
1963
+
1964
+ // 如果图表已存在,先销毁
1965
+ if (requestsChart) {
1966
+ requestsChart.destroy();
1967
+ }
1968
+
1969
+ // 创建新图表
1970
+ requestsChart = new Chart(ctx, {
1971
+ type: 'line',
1972
+ data: {
1973
+ labels: labels,
1974
+ datasets: [{
1975
+ label: '响应时间 (s)',
1976
+ data: responseTimes.map(time => time / 1000),
1977
+ borderColor: '#007bff',
1978
+ backgroundColor: 'rgba(0, 123, 255, 0.1)',
1979
+ tension: 0.1,
1980
+ fill: true
1981
+ }]
1982
+ },
1983
+ options: {
1984
+ responsive: true,
1985
+ maintainAspectRatio: false,
1986
+ scales: {
1987
+ y: {
1988
+ beginAtZero: true,
1989
+ title: {
1990
+ display: true,
1991
+ text: '响应时间 (s)'
1992
+ }
1993
+ },
1994
+ x: {
1995
+ title: {
1996
+ display: true,
1997
+ text: '时间'
1998
+ }
1999
+ }
2000
+ },
2001
+ plugins: {
2002
+ title: {
2003
+ display: true,
2004
+ text: '最近20条请求的响应时间趋势 (s)'
2005
+ }
2006
+ }
2007
+ }
2008
+ });
2009
+ }
2010
+
2011
+ // 分页按钮事件
2012
+ document.getElementById('prev-page').addEventListener('click', function() {
2013
+ if (currentPage > 1) {
2014
+ currentPage--;
2015
+ updateTable();
2016
+ updatePagination();
2017
+ }
2018
+ });
2019
+
2020
+ document.getElementById('next-page').addEventListener('click', function() {
2021
+ const totalPages = Math.ceil(allRequests.length / itemsPerPage);
2022
+ if (currentPage < totalPages) {
2023
+ currentPage++;
2024
+ updateTable();
2025
+ updatePagination();
2026
+ }
2027
+ });
2028
+
2029
+ // 初始加载
2030
+ updateStats();
2031
+ updateRequests();
2032
+
2033
+ // 定时刷新
2034
+ setInterval(updateStats, 5000);
2035
+ setInterval(updateRequests, 5000);
2036
+ </script>
2037
+ </body>
2038
+ </html>`;
2039
+ }
2040
+
2041
+ /**
2042
+ * 处理 Dashboard 监控页面请求
2043
+ * 返回实时监控面板的HTML页面
2044
+ * @param request HTTP请求对象
2045
+ * @returns Promise<Response> HTML响应
2046
+ */
2047
+ async function handleDashboard(request: Request): Promise<Response> {
2048
+ if (request.method !== "GET") {
2049
+ return new Response("Method not allowed", { status: 405 });
2050
+ }
2051
+
2052
+ return new Response(getDashboardHTML(), {
2053
+ status: 200,
2054
+ headers: {
2055
+ "Content-Type": "text/html; charset=utf-8"
2056
+ }
2057
+ });
2058
+ }
2059
+
2060
+ // 处理Dashboard统计数据
2061
+ async function handleDashboardStats(request: Request): Promise<Response> {
2062
+ return new Response(getStatsData(), {
2063
+ status: 200,
2064
+ headers: {
2065
+ "Content-Type": "application/json"
2066
+ }
2067
+ });
2068
+ }
2069
+
2070
+ async function handleDashboardRequests(request: Request): Promise<Response> {
2071
+ return new Response(getLiveRequestsData(), {
2072
+ status: 200,
2073
+ headers: {
2074
+ "Content-Type": "application/json"
2075
+ }
2076
+ });
2077
+ }
2078
+
2079
+
2080
+ function getDocsHTML(): string {
2081
+ return `<!DOCTYPE html>
2082
+ <html lang="zh-CN">
2083
+ <head>
2084
+ <meta charset="UTF-8">
2085
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2086
+ <title>ZtoApi 文档</title>
2087
+ <style>
2088
+ body {
2089
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
2090
+ margin: 0;
2091
+ padding: 20px;
2092
+ background-color: #f5f5f5;
2093
+ line-height: 1.6;
2094
+ }
2095
+ .container {
2096
+ max-width: 1200px;
2097
+ margin: 0 auto;
2098
+ background-color: white;
2099
+ border-radius: 8px;
2100
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
2101
+ padding: 30px;
2102
+ }
2103
+ h1 {
2104
+ color: #333;
2105
+ text-align: center;
2106
+ margin-bottom: 30px;
2107
+ border-bottom: 2px solid #007bff;
2108
+ padding-bottom: 10px;
2109
+ }
2110
+ h2 {
2111
+ color: #007bff;
2112
+ margin-top: 30px;
2113
+ margin-bottom: 15px;
2114
+ }
2115
+ h3 {
2116
+ color: #333;
2117
+ margin-top: 25px;
2118
+ margin-bottom: 10px;
2119
+ }
2120
+ .endpoint {
2121
+ background-color: #f8f9fa;
2122
+ border-radius: 6px;
2123
+ padding: 15px;
2124
+ margin-bottom: 20px;
2125
+ border-left: 4px solid #007bff;
2126
+ }
2127
+ .method {
2128
+ display: inline-block;
2129
+ padding: 4px 8px;
2130
+ border-radius: 4px;
2131
+ color: white;
2132
+ font-weight: bold;
2133
+ margin-right: 10px;
2134
+ font-size: 14px;
2135
+ }
2136
+ .get { background-color: #28a745; }
2137
+ .post { background-color: #007bff; }
2138
+ .path {
2139
+ font-family: monospace;
2140
+ background-color: #e9ecef;
2141
+ padding: 2px 6px;
2142
+ border-radius: 3px;
2143
+ font-size: 16px;
2144
+ }
2145
+ .description {
2146
+ margin: 15px 0;
2147
+ }
2148
+ .parameters {
2149
+ margin: 15px 0;
2150
+ }
2151
+ table {
2152
+ width: 100%;
2153
+ border-collapse: collapse;
2154
+ margin: 15px 0;
2155
+ }
2156
+ th, td {
2157
+ padding: 10px;
2158
+ text-align: left;
2159
+ border-bottom: 1px solid #ddd;
2160
+ }
2161
+ th {
2162
+ background-color: #f8f9fa;
2163
+ font-weight: bold;
2164
+ }
2165
+ .example {
2166
+ background-color: #f8f9fa;
2167
+ border-radius: 6px;
2168
+ padding: 15px;
2169
+ margin: 15px 0;
2170
+ font-family: monospace;
2171
+ white-space: pre-wrap;
2172
+ overflow-x: auto;
2173
+ }
2174
+ .note {
2175
+ background-color: #fff3cd;
2176
+ border-left: 4px solid #ffc107;
2177
+ padding: 10px 15px;
2178
+ margin: 15px 0;
2179
+ border-radius: 0 4px 4px 0;
2180
+ }
2181
+ .response {
2182
+ background-color: #f8f9fa;
2183
+ border-radius: 6px;
2184
+ padding: 15px;
2185
+ margin: 15px 0;
2186
+ font-family: monospace;
2187
+ white-space: pre-wrap;
2188
+ overflow-x: auto;
2189
+ }
2190
+ .tab {
2191
+ overflow: hidden;
2192
+ border: 1px solid #ccc;
2193
+ background-color: #f1f1f1;
2194
+ border-radius: 4px 4px 0 0;
2195
+ }
2196
+ .tab button {
2197
+ background-color: inherit;
2198
+ float: left;
2199
+ border: none;
2200
+ outline: none;
2201
+ cursor: pointer;
2202
+ padding: 14px 16px;
2203
+ transition: 0.3s;
2204
+ font-size: 16px;
2205
+ }
2206
+ .tab button:hover {
2207
+ background-color: #ddd;
2208
+ }
2209
+ .tab button.active {
2210
+ background-color: #ccc;
2211
+ }
2212
+ .tabcontent {
2213
+ display: none;
2214
+ padding: 6px 12px;
2215
+ border: 1px solid #ccc;
2216
+ border-top: none;
2217
+ border-radius: 0 0 4px 4px;
2218
+ }
2219
+ .toc {
2220
+ background-color: #f8f9fa;
2221
+ border-radius: 6px;
2222
+ padding: 15px;
2223
+ margin-bottom: 20px;
2224
+ }
2225
+ .toc ul {
2226
+ padding-left: 20px;
2227
+ }
2228
+ .toc li {
2229
+ margin: 5px 0;
2230
+ }
2231
+ .toc a {
2232
+ color: #007bff;
2233
+ text-decoration: none;
2234
+ }
2235
+ .toc a:hover {
2236
+ text-decoration: underline;
2237
+ }
2238
+ </style>
2239
+ </head>
2240
+ <body>
2241
+ <div class="container">
2242
+ <h1>ZtoApi 文档</h1>
2243
+
2244
+ <div class="toc">
2245
+ <h2>目录</h2>
2246
+ <ul>
2247
+ <li><a href="#overview">概述</a></li>
2248
+ <li><a href="#authentication">身份验证</a></li>
2249
+ <li><a href="#endpoints">API端点</a>
2250
+ <ul>
2251
+ <li><a href="#models">获取模型列表</a></li>
2252
+ <li><a href="#chat-completions">聊天完成</a></li>
2253
+ </ul>
2254
+ </li>
2255
+ <li><a href="#examples">使用示例</a></li>
2256
+ <li><a href="#error-handling">错误处理</a></li>
2257
+ </ul>
2258
+ </div>
2259
+
2260
+ <section id="overview">
2261
+ <h2>概述</h2>
2262
+ <p>这是一个为Z.ai GLM-4.5模型提供OpenAI兼容API接口的代理服务器。它允许你使用标准的OpenAI API格式与Z.ai的GLM-4.5模型进行交互,支持流式和非流式响应。</p>
2263
+ <p><strong>基础URL:</strong> <code>http://localhost:9090/v1</code></p>
2264
+ <div class="note">
2265
+ <strong>注意:</strong> 默认端口为9090,可以通过环境变量PORT进行修改。
2266
+ </div>
2267
+ </section>
2268
+
2269
+ <section id="authentication">
2270
+ <h2>身份验证</h2>
2271
+ <p>所有API请求都需要在请求头中包含有效的API密钥进行身份验证:</p>
2272
+ <div class="example">
2273
+ Authorization: Bearer your-api-key</div>
2274
+ <p>默认的API密钥为 <code>sk-your-key</code>,可以通过环境变量 <code>DEFAULT_KEY</code> 进行修改。</p>
2275
+ </section>
2276
+
2277
+ <section id="endpoints">
2278
+ <h2>API端点</h2>
2279
+
2280
+ <div class="endpoint" id="models">
2281
+ <h3>获取模型列表</h3>
2282
+ <div>
2283
+ <span class="method get">GET</span>
2284
+ <span class="path">/v1/models</span>
2285
+ </div>
2286
+ <div class="description">
2287
+ <p>获取可用模型列表。</p>
2288
+ </div>
2289
+ <div class="parameters">
2290
+ <h4>请求参数</h4>
2291
+ <p>无</p>
2292
+ </div>
2293
+ <div class="response">
2294
+ {
2295
+ "object": "list",
2296
+ "data": [
2297
+ {
2298
+ "id": "GLM-4.5",
2299
+ "object": "model",
2300
+ "created": 1756788845,
2301
+ "owned_by": "z.ai"
2302
+ }
2303
+ ]
2304
+ }</div>
2305
+ </div>
2306
+
2307
+ <div class="endpoint" id="chat-completions">
2308
+ <h3>聊天完成</h3>
2309
+ <div>
2310
+ <span class="method post">POST</span>
2311
+ <span class="path">/v1/chat/completions</span>
2312
+ </div>
2313
+ <div class="description">
2314
+ <p>基于消息列表生成模型响应。支持流式和非流式两种模式。</p>
2315
+ </div>
2316
+ <div class="parameters">
2317
+ <h4>请求参数</h4>
2318
+ <table>
2319
+ <thead>
2320
+ <tr>
2321
+ <th>参数名</th>
2322
+ <th>类型</th>
2323
+ <th>必需</th>
2324
+ <th>说明</th>
2325
+ </tr>
2326
+ </thead>
2327
+ <tbody>
2328
+ <tr>
2329
+ <td>model</td>
2330
+ <td>string</td>
2331
+ <td>是</td>
2332
+ <td>要使用的模型ID,例如 "GLM-4.5"</td>
2333
+ </tr>
2334
+ <tr>
2335
+ <td>messages</td>
2336
+ <td>array</td>
2337
+ <td>是</td>
2338
+ <td>消息列表,包含角色和内容</td>
2339
+ </tr>
2340
+ <tr>
2341
+ <td>stream</td>
2342
+ <td>boolean</td>
2343
+ <td>否</td>
2344
+ <td>是否使用流式响应,默认为true</td>
2345
+ </tr>
2346
+ <tr>
2347
+ <td>temperature</td>
2348
+ <td>number</td>
2349
+ <td>否</td>
2350
+ <td>采样温度,控制随机性</td>
2351
+ </tr>
2352
+ <tr>
2353
+ <td>max_tokens</td>
2354
+ <td>integer</td>
2355
+ <td>否</td>
2356
+ <td>生成的最大令牌数</td>
2357
+ </tr>
2358
+ </tbody>
2359
+ </table>
2360
+ </div>
2361
+ <div class="parameters">
2362
+ <h4>消息格式</h4>
2363
+ <table>
2364
+ <thead>
2365
+ <tr>
2366
+ <th>字段</th>
2367
+ <th>类型</th>
2368
+ <th>说明</th>
2369
+ </tr>
2370
+ </thead>
2371
+ <tbody>
2372
+ <tr>
2373
+ <td>role</td>
2374
+ <td>string</td>
2375
+ <td>消息角色,可选值:system、user、assistant</td>
2376
+ </tr>
2377
+ <tr>
2378
+ <td>content</td>
2379
+ <td>string</td>
2380
+ <td>消息内容</td>
2381
+ </tr>
2382
+ </tbody>
2383
+ </table>
2384
+ </div>
2385
+ </div>
2386
+ </section>
2387
+
2388
+ <section id="examples">
2389
+ <h2>使用示例</h2>
2390
+
2391
+ <div class="tab">
2392
+ <button class="tablinks active" onclick="openTab(event, 'python-tab')">Python</button>
2393
+ <button class="tablinks" onclick="openTab(event, 'curl-tab')">cURL</button>
2394
+ <button class="tablinks" onclick="openTab(event, 'javascript-tab')">JavaScript</button>
2395
+ </div>
2396
+
2397
+ <div id="python-tab" class="tabcontent" style="display: block;">
2398
+ <h3>Python示例</h3>
2399
+ <div class="example">
2400
+ import openai
2401
+
2402
+ # 配置客户端
2403
+ client = openai.OpenAI(
2404
+ api_key="your-api-key", # 对应 DEFAULT_KEY
2405
+ base_url="http://localhost:9090/v1"
2406
+ )
2407
+
2408
+ # 非流式请求 - 使用GLM-4.5
2409
+ response = client.chat.completions.create(
2410
+ model="GLM-4.5",
2411
+ messages=[{"role": "user", "content": "你好,请介绍一下自己"}]
2412
+ )
2413
+
2414
+ print(response.choices[0].message.content)
2415
+
2416
+
2417
+ # 流式请求 - 使用GLM-4.5
2418
+ response = client.chat.completions.create(
2419
+ model="GLM-4.5",
2420
+ messages=[{"role": "user", "content": "请写一首关于春天的诗"}],
2421
+ stream=True
2422
+ )
2423
+
2424
+
2425
+ for chunk in response:
2426
+ if chunk.choices[0].delta.content:
2427
+ print(chunk.choices[0].delta.content, end="")</div>
2428
+ </div>
2429
+
2430
+ <div id="curl-tab" class="tabcontent">
2431
+ <h3>cURL示例</h3>
2432
+ <div class="example">
2433
+ # 非流式请求
2434
+ curl -X POST http://localhost:9090/v1/chat/completions \
2435
+ -H "Content-Type: application/json" \
2436
+ -H "Authorization: Bearer your-api-key" \
2437
+ -d '{
2438
+ "model": "GLM-4.5",
2439
+ "messages": [{"role": "user", "content": "你好"}],
2440
+ "stream": false
2441
+ }'
2442
+
2443
+ # 流式请求
2444
+ curl -X POST http://localhost:9090/v1/chat/completions \
2445
+ -H "Content-Type: application/json" \
2446
+ -H "Authorization: Bearer your-api-key" \
2447
+ -d '{
2448
+ "model": "GLM-4.5",
2449
+ "messages": [{"role": "user", "content": "你好"}],
2450
+ "stream": true
2451
+ }'</div>
2452
+ </div>
2453
+
2454
+ <div id="javascript-tab" class="tabcontent">
2455
+ <h3>JavaScript示例</h3>
2456
+ <div class="example">
2457
+ const fetch = require('node-fetch');
2458
+
2459
+ async function chatWithGLM(message, stream = false) {
2460
+ const response = await fetch('http://localhost:9090/v1/chat/completions', {
2461
+ method: 'POST',
2462
+ headers: {
2463
+ 'Content-Type': 'application/json',
2464
+ 'Authorization': 'Bearer your-api-key'
2465
+ },
2466
+ body: JSON.stringify({
2467
+ model: 'GLM-4.5',
2468
+ messages: [{ role: 'user', content: message }],
2469
+ stream: stream
2470
+ })
2471
+ });
2472
+
2473
+ if (stream) {
2474
+ // 处理流式响应
2475
+ const reader = response.body.getReader();
2476
+ const decoder = new TextDecoder();
2477
+
2478
+ while (true) {
2479
+ const { done, value } = await reader.read();
2480
+ if (done) break;
2481
+
2482
+ const chunk = decoder.decode(value);
2483
+ const lines = chunk.split('\n');
2484
+
2485
+ for (const line of lines) {
2486
+ if (line.startsWith('data: ')) {
2487
+ const data = line.slice(6);
2488
+ if (data === '[DONE]') {
2489
+ console.log('\n流式响应完成');
2490
+ return;
2491
+ }
2492
+
2493
+ try {
2494
+ const parsed = JSON.parse(data);
2495
+ const content = parsed.choices[0]?.delta?.content;
2496
+ if (content) {
2497
+ process.stdout.write(content);
2498
+ }
2499
+ } catch (e) {
2500
+ // 忽略解析错误
2501
+ }
2502
+ }
2503
+ }
2504
+ }
2505
+ } else {
2506
+ // 处理非流式响应
2507
+ const data = await response.json();
2508
+ console.log(data.choices[0].message.content);
2509
+ }
2510
+ }
2511
+
2512
+ // 使用示例
2513
+ chatWithGLM('你好,请介绍一下JavaScript', false);</div>
2514
+ </div>
2515
+ </section>
2516
+
2517
+ <section id="error-handling">
2518
+ <h2>错误处理</h2>
2519
+ <p>API使用标准HTTP状态码来表示请求的成功或失败:</p>
2520
+ <table>
2521
+ <thead>
2522
+ <tr>
2523
+ <th>状态码</th>
2524
+ <th>说明</th>
2525
+ </tr>
2526
+ </thead>
2527
+ <tbody>
2528
+ <tr>
2529
+ <td>200 OK</td>
2530
+ <td>请求成功</td>
2531
+ </tr>
2532
+ <tr>
2533
+ <td>400 Bad Request</td>
2534
+ <td>请求格式错误或参数无效</td>
2535
+ </tr>
2536
+ <tr>
2537
+ <td>401 Unauthorized</td>
2538
+ <td>API密钥无效或缺失</td>
2539
+ </tr>
2540
+ <tr>
2541
+ <td>502 Bad Gateway</td>
2542
+ <td>上游服务错误</td>
2543
+ </tr>
2544
+ </tbody>
2545
+ </table>
2546
+ <div class="note">
2547
+ <strong>注意:</strong> 在调试模式下,服务器会输出详细的日志信息,可以通过设置环境变量 DEBUG_MODE=true 来启用。
2548
+ </div>
2549
+ </section>
2550
+ </div>
2551
+
2552
+ <script>
2553
+ function openTab(evt, tabName) {
2554
+ var i, tabcontent, tablinks;
2555
+ tabcontent = document.getElementsByClassName("tabcontent");
2556
+ for (i = 0; i < tabcontent.length; i++) {
2557
+ tabcontent[i].style.display = "none";
2558
+ }
2559
+ tablinks = document.getElementsByClassName("tablinks");
2560
+ for (i = 0; i < tablinks.length; i++) {
2561
+ tablinks[i].className = tablinks[i].className.replace(" active", "");
2562
+ }
2563
+ document.getElementById(tabName).style.display = "block";
2564
+ evt.currentTarget.className += " active";
2565
+ }
2566
+ </script>
2567
+ </body>
2568
+ </html>`;
2569
+ }
2570
+
2571
+ // 处理API文档页面
2572
+ async function handleDocs(request: Request): Promise<Response> {
2573
+ if (request.method !== "GET") {
2574
+ return new Response("Method not allowed", { status: 405 });
2575
+ }
2576
+
2577
+ return new Response(getDocsHTML(), {
2578
+ status: 200,
2579
+ headers: {
2580
+ "Content-Type": "text/html; charset=utf-8"
2581
+ }
2582
+ });
2583
+ }
2584
+
2585
+ // 主HTTP服务器
2586
+ async function main() {
2587
+ console.log(`OpenAI兼容API服务器启动`);
2588
+ console.log(`支持的模型: ${SUPPORTED_MODELS.map(m => `${m.id} (${m.name})`).join(', ')}`);
2589
+ console.log(`上游: ${UPSTREAM_URL}`);
2590
+ console.log(`Debug模式: ${DEBUG_MODE}`);
2591
+ console.log(`默认流式响应: ${DEFAULT_STREAM}`);
2592
+ console.log(`Dashboard启用: ${DASHBOARD_ENABLED}`);
2593
+
2594
+ // 检测是否在Deno Deploy上运行
2595
+ const isDenoDeploy = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined;
2596
+
2597
+ if (isDenoDeploy) {
2598
+ // Deno Deploy环境
2599
+ console.log("运行在Deno Deploy环境中");
2600
+ Deno.serve(handleRequest);
2601
+ } else {
2602
+ // 本地或自托管环境
2603
+ const port = parseInt(Deno.env.get("PORT") || "9090");
2604
+ console.log(`运行在本地环境中,端口: ${port}`);
2605
+
2606
+ if (DASHBOARD_ENABLED) {
2607
+ console.log(`Dashboard已启用,访问地址: http://localhost:${port}/dashboard`);
2608
+ }
2609
+
2610
+ const server = Deno.listen({ port });
2611
+
2612
+ for await (const conn of server) {
2613
+ handleHttp(conn);
2614
+ }
2615
+ }
2616
+ }
2617
+
2618
+ // 处理HTTP连接(用于本地环境)
2619
+ async function handleHttp(conn: Deno.Conn) {
2620
+ const httpConn = Deno.serveHttp(conn);
2621
+
2622
+ while (true) {
2623
+ const requestEvent = await httpConn.nextRequest();
2624
+ if (!requestEvent) break;
2625
+
2626
+ const { request, respondWith } = requestEvent;
2627
+ const url = new URL(request.url);
2628
+ const startTime = Date.now();
2629
+ const userAgent = request.headers.get("User-Agent") || "";
2630
+
2631
+ try {
2632
+ // 路由分发
2633
+ if (url.pathname === "/") {
2634
+ const response = await handleIndex(request);
2635
+ await respondWith(response);
2636
+ recordRequestStats(startTime, url.pathname, response.status);
2637
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2638
+ } else if (url.pathname === "/v1/models") {
2639
+ const response = await handleModels(request);
2640
+ await respondWith(response);
2641
+ recordRequestStats(startTime, url.pathname, response.status);
2642
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2643
+ } else if (url.pathname === "/v1/chat/completions") {
2644
+ const response = await handleChatCompletions(request);
2645
+ await respondWith(response);
2646
+ // 请求统计已在handleChatCompletions中记录
2647
+ } else if (url.pathname === "/docs") {
2648
+ const response = await handleDocs(request);
2649
+ await respondWith(response);
2650
+ recordRequestStats(startTime, url.pathname, response.status);
2651
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2652
+ } else if (url.pathname === "/dashboard" && DASHBOARD_ENABLED) {
2653
+ const response = await handleDashboard(request);
2654
+ await respondWith(response);
2655
+ recordRequestStats(startTime, url.pathname, response.status);
2656
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2657
+ } else if (url.pathname === "/dashboard/stats" && DASHBOARD_ENABLED) {
2658
+ const response = await handleDashboardStats(request);
2659
+ await respondWith(response);
2660
+ recordRequestStats(startTime, url.pathname, response.status);
2661
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2662
+ } else if (url.pathname === "/dashboard/requests" && DASHBOARD_ENABLED) {
2663
+ const response = await handleDashboardRequests(request);
2664
+ await respondWith(response);
2665
+ recordRequestStats(startTime, url.pathname, response.status);
2666
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2667
+ } else {
2668
+ const response = await handleOptions(request);
2669
+ await respondWith(response);
2670
+ recordRequestStats(startTime, url.pathname, response.status);
2671
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2672
+ }
2673
+ } catch (error) {
2674
+ debugLog("处理请求时出错: %v", error);
2675
+ const response = new Response("Internal Server Error", { status: 500 });
2676
+ await respondWith(response);
2677
+ recordRequestStats(startTime, url.pathname, 500);
2678
+ addLiveRequest(request.method, url.pathname, 500, Date.now() - startTime, userAgent);
2679
+ }
2680
+ }
2681
+ }
2682
+
2683
+ // 处理HTTP请求(用于Deno Deploy环境)
2684
+ async function handleRequest(request: Request): Promise<Response> {
2685
+ const url = new URL(request.url);
2686
+ const startTime = Date.now();
2687
+ const userAgent = request.headers.get("User-Agent") || "";
2688
+
2689
+ try {
2690
+ // 路由分发
2691
+ if (url.pathname === "/") {
2692
+ const response = await handleIndex(request);
2693
+ recordRequestStats(startTime, url.pathname, response.status);
2694
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2695
+ return response;
2696
+ } else if (url.pathname === "/v1/models") {
2697
+ const response = await handleModels(request);
2698
+ recordRequestStats(startTime, url.pathname, response.status);
2699
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2700
+ return response;
2701
+ } else if (url.pathname === "/v1/chat/completions") {
2702
+ const response = await handleChatCompletions(request);
2703
+ // 请求统计已在handleChatCompletions中记录
2704
+ return response;
2705
+ } else if (url.pathname === "/docs") {
2706
+ const response = await handleDocs(request);
2707
+ recordRequestStats(startTime, url.pathname, response.status);
2708
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2709
+ return response;
2710
+ } else if (url.pathname === "/dashboard" && DASHBOARD_ENABLED) {
2711
+ const response = await handleDashboard(request);
2712
+ recordRequestStats(startTime, url.pathname, response.status);
2713
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2714
+ return response;
2715
+ } else if (url.pathname === "/dashboard/stats" && DASHBOARD_ENABLED) {
2716
+ const response = await handleDashboardStats(request);
2717
+ recordRequestStats(startTime, url.pathname, response.status);
2718
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2719
+ return response;
2720
+ } else if (url.pathname === "/dashboard/requests" && DASHBOARD_ENABLED) {
2721
+ const response = await handleDashboardRequests(request);
2722
+ recordRequestStats(startTime, url.pathname, response.status);
2723
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2724
+ return response;
2725
+ } else {
2726
+ const response = await handleOptions(request);
2727
+ recordRequestStats(startTime, url.pathname, response.status);
2728
+ addLiveRequest(request.method, url.pathname, response.status, Date.now() - startTime, userAgent);
2729
+ return response;
2730
+ }
2731
+ } catch (error) {
2732
+ debugLog("处理请求时出错: %v", error);
2733
+ recordRequestStats(startTime, url.pathname, 500);
2734
+ addLiveRequest(request.method, url.pathname, 500, Date.now() - startTime, userAgent);
2735
+ return new Response("Internal Server Error", { status: 500 });
2736
+ }
2737
+ }
2738
+
2739
+ // 启动服务器
2740
+ main();