Spaces:
Running
Running
Update src/lightweight-client-express.js
Browse files
src/lightweight-client-express.js
CHANGED
@@ -7,11 +7,9 @@ import chalk from 'chalk';
|
|
7 |
import {
|
8 |
ChatMessage, ChatCompletionRequest, Choice, ChoiceDelta, ChatCompletionChunk
|
9 |
} from './models.js';
|
10 |
-
import {
|
11 |
-
initialize,
|
12 |
-
streamNotionResponse
|
13 |
-
} from './lightweight-client.js';
|
14 |
import { cookieManager } from './CookieManager.js';
|
|
|
|
|
15 |
|
16 |
// 获取当前文件的目录路径
|
17 |
const __filename = fileURLToPath(import.meta.url);
|
@@ -54,7 +52,6 @@ async function validateProxy() {
|
|
54 |
}
|
55 |
|
56 |
try {
|
57 |
-
const { default: fetch } = await import('node-fetch');
|
58 |
const { HttpsProxyAgent } = await import('https-proxy-agent');
|
59 |
|
60 |
const testResponse = await fetch('https://httpbin.org/ip', {
|
@@ -78,6 +75,282 @@ async function validateProxy() {
|
|
78 |
}
|
79 |
}
|
80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
// 构建Notion请求的函数
|
82 |
function buildNotionRequest(requestData) {
|
83 |
const cookieData = cookieManager.getNext();
|
@@ -487,5 +760,3 @@ initialize().then(async (initResult) => {
|
|
487 |
logger.warning(`服务已在 0.0.0.0:${PORT} 上启动(初始化失败模式)`);
|
488 |
});
|
489 |
});
|
490 |
-
|
491 |
-
|
|
|
7 |
import {
|
8 |
ChatMessage, ChatCompletionRequest, Choice, ChoiceDelta, ChatCompletionChunk
|
9 |
} from './models.js';
|
|
|
|
|
|
|
|
|
10 |
import { cookieManager } from './CookieManager.js';
|
11 |
+
import { PassThrough } from 'stream';
|
12 |
+
import fetch from 'node-fetch';
|
13 |
|
14 |
// 获取当前文件的目录路径
|
15 |
const __filename = fileURLToPath(import.meta.url);
|
|
|
52 |
}
|
53 |
|
54 |
try {
|
|
|
55 |
const { HttpsProxyAgent } = await import('https-proxy-agent');
|
56 |
|
57 |
const testResponse = await fetch('https://httpbin.org/ip', {
|
|
|
75 |
}
|
76 |
}
|
77 |
|
78 |
+
// 初始化函数
|
79 |
+
async function initialize() {
|
80 |
+
logger.info('开始系统初始化...');
|
81 |
+
|
82 |
+
try {
|
83 |
+
const envCookie = process.env.NOTION_COOKIE;
|
84 |
+
const envCookieFile = process.env.COOKIE_FILE;
|
85 |
+
|
86 |
+
if (envCookie) {
|
87 |
+
logger.info('发现环境变量中的NOTION_COOKIE,正在初始化...');
|
88 |
+
const success = await cookieManager.initialize(envCookie);
|
89 |
+
if (success) {
|
90 |
+
logger.success('Cookie初始化成功');
|
91 |
+
return true;
|
92 |
+
} else {
|
93 |
+
logger.error('Cookie初始化失败');
|
94 |
+
return false;
|
95 |
+
}
|
96 |
+
} else if (envCookieFile) {
|
97 |
+
logger.info(`发现环境变量中的COOKIE_FILE: ${envCookieFile},正在加载...`);
|
98 |
+
const success = await cookieManager.loadFromFile(envCookieFile);
|
99 |
+
if (success) {
|
100 |
+
logger.success('从文件加载Cookie成功');
|
101 |
+
return true;
|
102 |
+
} else {
|
103 |
+
logger.error('从文件加载Cookie失败');
|
104 |
+
return false;
|
105 |
+
}
|
106 |
+
} else {
|
107 |
+
logger.warning('未找到NOTION_COOKIE或COOKIE_FILE环境变量');
|
108 |
+
return false;
|
109 |
+
}
|
110 |
+
} catch (error) {
|
111 |
+
logger.error(`初始化过程出错: ${error.message}`);
|
112 |
+
return false;
|
113 |
+
}
|
114 |
+
}
|
115 |
+
|
116 |
+
// 流式响应函数
|
117 |
+
async function streamNotionResponse(notionRequestBody) {
|
118 |
+
const stream = new PassThrough();
|
119 |
+
let streamClosed = false;
|
120 |
+
|
121 |
+
const originalEnd = stream.end;
|
122 |
+
stream.end = function(...args) {
|
123 |
+
if (streamClosed) return;
|
124 |
+
streamClosed = true;
|
125 |
+
return originalEnd.apply(this, args);
|
126 |
+
};
|
127 |
+
|
128 |
+
stream.write(':\n\n');
|
129 |
+
|
130 |
+
const timeoutId = setTimeout(() => {
|
131 |
+
if (streamClosed) return;
|
132 |
+
|
133 |
+
logger.warning(`请求超时,30秒内未收到响应`);
|
134 |
+
try {
|
135 |
+
const endChunk = new ChatCompletionChunk({
|
136 |
+
choices: [
|
137 |
+
new Choice({
|
138 |
+
delta: new ChoiceDelta({ content: "请求超时,未收到Notion响应。" }),
|
139 |
+
finish_reason: "timeout"
|
140 |
+
})
|
141 |
+
]
|
142 |
+
});
|
143 |
+
stream.write(`data: ${JSON.stringify(endChunk)}\n\n`);
|
144 |
+
stream.write('data: [DONE]\n\n');
|
145 |
+
stream.end();
|
146 |
+
} catch (error) {
|
147 |
+
logger.error(`发送超时消息时出错: ${error}`);
|
148 |
+
if (!streamClosed) stream.end();
|
149 |
+
}
|
150 |
+
}, 30000);
|
151 |
+
|
152 |
+
fetchNotionResponse(stream, notionRequestBody, timeoutId).catch((error) => {
|
153 |
+
if (streamClosed) return;
|
154 |
+
|
155 |
+
logger.error(`流处理出错: ${error}`);
|
156 |
+
clearTimeout(timeoutId);
|
157 |
+
|
158 |
+
try {
|
159 |
+
const errorChunk = new ChatCompletionChunk({
|
160 |
+
choices: [
|
161 |
+
new Choice({
|
162 |
+
delta: new ChoiceDelta({ content: `处理请求时出错: ${error.message}` }),
|
163 |
+
finish_reason: "error"
|
164 |
+
})
|
165 |
+
]
|
166 |
+
});
|
167 |
+
stream.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
|
168 |
+
stream.write('data: [DONE]\n\n');
|
169 |
+
} catch (e) {
|
170 |
+
logger.error(`发送错误消息时出错: ${e}`);
|
171 |
+
} finally {
|
172 |
+
if (!streamClosed) stream.end();
|
173 |
+
}
|
174 |
+
});
|
175 |
+
|
176 |
+
return stream;
|
177 |
+
}
|
178 |
+
|
179 |
+
// 实际请求Notion API的函数
|
180 |
+
async function fetchNotionResponse(chunkQueue, notionRequestBody, timeoutId) {
|
181 |
+
let responseReceived = false;
|
182 |
+
|
183 |
+
const isStreamClosed = () => {
|
184 |
+
return chunkQueue.destroyed || (typeof chunkQueue.closed === 'boolean' && chunkQueue.closed);
|
185 |
+
};
|
186 |
+
|
187 |
+
const safeWrite = (data) => {
|
188 |
+
if (!isStreamClosed()) {
|
189 |
+
try {
|
190 |
+
return chunkQueue.write(data);
|
191 |
+
} catch (error) {
|
192 |
+
logger.error(`流写入错误: ${error.message}`);
|
193 |
+
return false;
|
194 |
+
}
|
195 |
+
}
|
196 |
+
return false;
|
197 |
+
};
|
198 |
+
|
199 |
+
try {
|
200 |
+
const cookieData = cookieManager.getNext();
|
201 |
+
if (!cookieData) {
|
202 |
+
throw new Error('没有可用的cookie');
|
203 |
+
}
|
204 |
+
|
205 |
+
const headers = {
|
206 |
+
'Content-Type': 'application/json',
|
207 |
+
'accept': 'application/x-ndjson',
|
208 |
+
'accept-language': 'en-US,en;q=0.9',
|
209 |
+
'notion-audit-log-platform': 'web',
|
210 |
+
'notion-client-version': '23.13.0.3686',
|
211 |
+
'origin': 'https://www.notion.so',
|
212 |
+
'referer': 'https://www.notion.so/chat',
|
213 |
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
|
214 |
+
'x-notion-active-user-header': cookieData.userId,
|
215 |
+
'x-notion-space-id': cookieData.spaceId,
|
216 |
+
'Cookie': cookieData.cookie
|
217 |
+
};
|
218 |
+
|
219 |
+
const fetchOptions = {
|
220 |
+
method: 'POST',
|
221 |
+
headers: headers,
|
222 |
+
body: JSON.stringify(notionRequestBody),
|
223 |
+
};
|
224 |
+
|
225 |
+
// 添加代理配置
|
226 |
+
if (PROXY_URL) {
|
227 |
+
const { HttpsProxyAgent } = await import('https-proxy-agent');
|
228 |
+
fetchOptions.agent = new HttpsProxyAgent(PROXY_URL);
|
229 |
+
logger.info(`使用固定代理连接Notion API`);
|
230 |
+
}
|
231 |
+
|
232 |
+
const response = await fetch('https://www.notion.so/api/v3/runInferenceTranscript', fetchOptions);
|
233 |
+
|
234 |
+
if (response.status === 401) {
|
235 |
+
logger.error(`收到401未授权错误,cookie可能已失效`);
|
236 |
+
cookieManager.markAsInvalid(cookieData.userId);
|
237 |
+
throw new Error('Cookie已失效');
|
238 |
+
}
|
239 |
+
|
240 |
+
if (!response.ok) {
|
241 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
242 |
+
}
|
243 |
+
|
244 |
+
if (!response.body) {
|
245 |
+
throw new Error("Response body is null");
|
246 |
+
}
|
247 |
+
|
248 |
+
const reader = response.body;
|
249 |
+
let buffer = '';
|
250 |
+
|
251 |
+
reader.on('data', (chunk) => {
|
252 |
+
if (isStreamClosed()) {
|
253 |
+
try {
|
254 |
+
reader.destroy();
|
255 |
+
} catch (error) {
|
256 |
+
logger.error(`销毁reader时出错: ${error.message}`);
|
257 |
+
}
|
258 |
+
return;
|
259 |
+
}
|
260 |
+
|
261 |
+
try {
|
262 |
+
if (!responseReceived) {
|
263 |
+
responseReceived = true;
|
264 |
+
logger.info(`已连接Notion API`);
|
265 |
+
clearTimeout(timeoutId);
|
266 |
+
}
|
267 |
+
|
268 |
+
const text = chunk.toString('utf8');
|
269 |
+
buffer += text;
|
270 |
+
|
271 |
+
const lines = buffer.split('\n');
|
272 |
+
buffer = lines.pop() || '';
|
273 |
+
|
274 |
+
for (const line of lines) {
|
275 |
+
if (!line.trim()) continue;
|
276 |
+
|
277 |
+
try {
|
278 |
+
const jsonData = JSON.parse(line);
|
279 |
+
|
280 |
+
if (jsonData?.type === "markdown-chat" && typeof jsonData?.value === "string") {
|
281 |
+
const content = jsonData.value;
|
282 |
+
if (!content) continue;
|
283 |
+
|
284 |
+
const chunk = new ChatCompletionChunk({
|
285 |
+
choices: [
|
286 |
+
new Choice({
|
287 |
+
delta: new ChoiceDelta({ content }),
|
288 |
+
finish_reason: null
|
289 |
+
})
|
290 |
+
]
|
291 |
+
});
|
292 |
+
|
293 |
+
const dataStr = `data: ${JSON.stringify(chunk)}\n\n`;
|
294 |
+
if (!safeWrite(dataStr)) {
|
295 |
+
try {
|
296 |
+
reader.destroy();
|
297 |
+
} catch (error) {
|
298 |
+
logger.error(`写入失败后销毁reader时出错: ${error.message}`);
|
299 |
+
}
|
300 |
+
return;
|
301 |
+
}
|
302 |
+
}
|
303 |
+
} catch (parseError) {
|
304 |
+
logger.error(`解析JSON失败: ${parseError.message}`);
|
305 |
+
}
|
306 |
+
}
|
307 |
+
} catch (error) {
|
308 |
+
logger.error(`处理数据块时出错: ${error.message}`);
|
309 |
+
}
|
310 |
+
});
|
311 |
+
|
312 |
+
reader.on('end', () => {
|
313 |
+
logger.info('Notion API响应流结束');
|
314 |
+
clearTimeout(timeoutId);
|
315 |
+
|
316 |
+
try {
|
317 |
+
const endChunk = new ChatCompletionChunk({
|
318 |
+
choices: [
|
319 |
+
new Choice({
|
320 |
+
delta: new ChoiceDelta({ content: "" }),
|
321 |
+
finish_reason: "stop"
|
322 |
+
})
|
323 |
+
]
|
324 |
+
});
|
325 |
+
safeWrite(`data: ${JSON.stringify(endChunk)}\n\n`);
|
326 |
+
safeWrite('data: [DONE]\n\n');
|
327 |
+
} catch (error) {
|
328 |
+
logger.error(`发送结束标记时出错: ${error.message}`);
|
329 |
+
}
|
330 |
+
|
331 |
+
if (!isStreamClosed()) {
|
332 |
+
chunkQueue.end();
|
333 |
+
}
|
334 |
+
});
|
335 |
+
|
336 |
+
reader.on('error', (error) => {
|
337 |
+
logger.error(`读取响应流时出错: ${error.message}`);
|
338 |
+
clearTimeout(timeoutId);
|
339 |
+
if (!isStreamClosed()) {
|
340 |
+
chunkQueue.end();
|
341 |
+
}
|
342 |
+
});
|
343 |
+
|
344 |
+
} catch (error) {
|
345 |
+
logger.error(`请求Notion API失败: ${error.message}`);
|
346 |
+
clearTimeout(timeoutId);
|
347 |
+
if (!isStreamClosed()) {
|
348 |
+
chunkQueue.end();
|
349 |
+
}
|
350 |
+
throw error;
|
351 |
+
}
|
352 |
+
}
|
353 |
+
|
354 |
// 构建Notion请求的函数
|
355 |
function buildNotionRequest(requestData) {
|
356 |
const cookieData = cookieManager.getNext();
|
|
|
760 |
logger.warning(`服务已在 0.0.0.0:${PORT} 上启动(初始化失败模式)`);
|
761 |
});
|
762 |
});
|
|
|
|