isididiidid commited on
Commit
5f40482
·
verified ·
1 Parent(s): 2dff040

Upload 6 files

Browse files
src/CookieManager.js ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { JSDOM } from 'jsdom';
2
+ import fetch from 'node-fetch';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+
8
+ // 获取当前文件的目录路径
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ // 日志配置
13
+ const logger = {
14
+ info: (message) => console.log(`\x1b[34m[info] ${message}\x1b[0m`),
15
+ error: (message) => console.error(`\x1b[31m[error] ${message}\x1b[0m`),
16
+ warning: (message) => console.warn(`\x1b[33m[warn] ${message}\x1b[0m`),
17
+ success: (message) => console.log(`\x1b[32m[success] ${message}\x1b[0m`),
18
+ };
19
+
20
+ class CookieManager {
21
+ constructor() {
22
+ this.cookieEntries = []; // 存储cookie及其对应的ID
23
+ this.currentIndex = 0;
24
+ this.initialized = false;
25
+ this.maxRetries = 3; // 最大重试次数
26
+ this.proxyUrl = process.env.PROXY_URL || "";
27
+ }
28
+
29
+ /**
30
+ * 从文件加载cookie
31
+ * @param {string} filePath - cookie文件路径
32
+ * @returns {Promise<boolean>} - 是否加载成功
33
+ */
34
+ async loadFromFile(filePath) {
35
+ try {
36
+ // 确保文件路径是绝对路径
37
+ const absolutePath = path.isAbsolute(filePath)
38
+ ? filePath
39
+ : path.join(dirname(__dirname), filePath);
40
+
41
+ logger.info(`从文件加载cookie: ${absolutePath}`);
42
+
43
+ // 检查文件是否存在
44
+ if (!fs.existsSync(absolutePath)) {
45
+ logger.error(`Cookie文件不存在: ${absolutePath}`);
46
+ return false;
47
+ }
48
+
49
+ // 读取文件内容
50
+ const fileContent = fs.readFileSync(absolutePath, 'utf8');
51
+
52
+ // 根据文件扩展名处理不同格式
53
+ const ext = path.extname(absolutePath).toLowerCase();
54
+ let cookieArray = [];
55
+
56
+ if (ext === '.json') {
57
+ // JSON格式
58
+ try {
59
+ const jsonData = JSON.parse(fileContent);
60
+ if (Array.isArray(jsonData)) {
61
+ cookieArray = jsonData;
62
+ } else if (jsonData.cookies && Array.isArray(jsonData.cookies)) {
63
+ cookieArray = jsonData.cookies;
64
+ } else {
65
+ logger.error('JSON文件格式错误,应为cookie数组或包含cookies数组的对象');
66
+ return false;
67
+ }
68
+ } catch (error) {
69
+ logger.error(`解析JSON文件失败: ${error.message}`);
70
+ return false;
71
+ }
72
+ } else {
73
+ // 文本格式,每行一个cookie
74
+ cookieArray = fileContent
75
+ .split('\n')
76
+ .map(line => line.trim())
77
+ .filter(line => line && !line.startsWith('#'));
78
+ }
79
+
80
+ logger.info(`从文件中读取了 ${cookieArray.length} 个cookie`);
81
+
82
+ // 初始化cookie
83
+ return await this.initialize(cookieArray.join('|'));
84
+
85
+ } catch (error) {
86
+ logger.error(`从文件加载cookie失败: ${error.message}`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 初始化cookie管理器
93
+ * @param {string} cookiesString - 以"|"分隔的cookie字符串
94
+ * @returns {Promise<boolean>} - 是否初始化成功
95
+ */
96
+ async initialize(cookiesString) {
97
+ if (!cookiesString) {
98
+ logger.error('未提供cookie字符串');
99
+ return false;
100
+ }
101
+
102
+ // 分割cookie字符串
103
+ const cookieArray = cookiesString.split('|').map(c => c.trim()).filter(c => c);
104
+
105
+ if (cookieArray.length === 0) {
106
+ logger.error('没有有效的cookie');
107
+ return false;
108
+ }
109
+
110
+ logger.info(`发现 ${cookieArray.length} 个cookie,开始获取对应的ID信息...`);
111
+
112
+ // 清空现有条目
113
+ this.cookieEntries = [];
114
+
115
+ // 为每个cookie获取ID
116
+ for (let i = 0; i < cookieArray.length; i++) {
117
+ const cookie = cookieArray[i];
118
+ logger.info(`正在处理第 ${i+1}/${cookieArray.length} 个cookie...`);
119
+
120
+ const result = await this.fetchNotionIds(cookie);
121
+ if (result.success) {
122
+ this.cookieEntries.push({
123
+ cookie,
124
+ spaceId: result.spaceId,
125
+ userId: result.userId,
126
+ valid: true,
127
+ lastUsed: 0 // 记录上次使用时间戳
128
+ });
129
+ logger.success(`第 ${i+1} 个cookie验证成功`);
130
+ } else {
131
+ if (result.status === 401) {
132
+ logger.error(`第 ${i+1} 个cookie无效(401未授权),已跳过`);
133
+ } else {
134
+ logger.warning(`第 ${i+1} 个cookie验证失败: ${result.error},已跳过`);
135
+ }
136
+ }
137
+ }
138
+
139
+ // 检查是否有有效的cookie
140
+ if (this.cookieEntries.length === 0) {
141
+ logger.error('没有有效的cookie,初始化失败');
142
+ return false;
143
+ }
144
+
145
+ logger.success(`成功初始化 ${this.cookieEntries.length}/${cookieArray.length} 个cookie`);
146
+ this.initialized = true;
147
+ this.currentIndex = 0;
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * 保存cookie到文件
153
+ * @param {string} filePath - 保存路径
154
+ * @param {boolean} onlyValid - 是否只保存有效的cookie
155
+ * @returns {boolean} - 是否保存成功
156
+ */
157
+ saveToFile(filePath, onlyValid = true) {
158
+ try {
159
+ // 确保文件路径是绝对路径
160
+ const absolutePath = path.isAbsolute(filePath)
161
+ ? filePath
162
+ : path.join(dirname(__dirname), filePath);
163
+
164
+ // 获取要保存的cookie
165
+ const cookiesToSave = onlyValid
166
+ ? this.cookieEntries.filter(entry => entry.valid).map(entry => entry.cookie)
167
+ : this.cookieEntries.map(entry => entry.cookie);
168
+
169
+ // 根据文件扩展名选择保存格式
170
+ const ext = path.extname(absolutePath).toLowerCase();
171
+
172
+ if (ext === '.json') {
173
+ // 保存为JSON格式
174
+ const jsonData = {
175
+ cookies: cookiesToSave,
176
+ updatedAt: new Date().toISOString(),
177
+ count: cookiesToSave.length
178
+ };
179
+ fs.writeFileSync(absolutePath, JSON.stringify(jsonData, null, 2), 'utf8');
180
+ } else {
181
+ // 保存为文本格式,每行一个cookie
182
+ const content = cookiesToSave.join('\n');
183
+ fs.writeFileSync(absolutePath, content, 'utf8');
184
+ }
185
+
186
+ logger.success(`已将 ${cookiesToSave.length} 个cookie保存到文件: ${absolutePath}`);
187
+ return true;
188
+ } catch (error) {
189
+ logger.error(`保存cookie到文件失败: ${error.message}`);
190
+ return false;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * 获取Notion的空间ID和用户ID
196
+ * @param {string} cookie - Notion cookie
197
+ * @returns {Promise<Object>} - 包含ID信息的对象
198
+ */
199
+ async fetchNotionIds(cookie, retryCount = 0) {
200
+ if (!cookie) {
201
+ return { success: false, error: '未提供cookie' };
202
+ }
203
+
204
+ try {
205
+ // 创建JSDOM实例模拟浏览器环境
206
+ const dom = new JSDOM("", {
207
+ url: "https://www.notion.so",
208
+ referrer: "https://www.notion.so/",
209
+ contentType: "text/html",
210
+ includeNodeLocations: true,
211
+ storageQuota: 10000000,
212
+ pretendToBeVisual: true,
213
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
214
+ });
215
+
216
+ // 设置全局对象
217
+ const { window } = dom;
218
+
219
+ // 安全地设置全局对象
220
+ if (!global.window) global.window = window;
221
+ if (!global.document) global.document = window.document;
222
+
223
+ // 设置navigator
224
+ if (!global.navigator) {
225
+ try {
226
+ Object.defineProperty(global, 'navigator', {
227
+ value: window.navigator,
228
+ writable: true,
229
+ configurable: true
230
+ });
231
+ } catch (navError) {
232
+ logger.warning(`无法设置navigator: ${navError.message},继续执行`);
233
+ }
234
+ }
235
+
236
+ // 设置cookie
237
+ document.cookie = cookie;
238
+
239
+ // 创建fetch选项
240
+ const fetchOptions = {
241
+ method: 'POST',
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ 'accept': '*/*',
245
+ 'accept-language': 'en-US,en;q=0.9',
246
+ 'notion-audit-log-platform': 'web',
247
+ 'notion-client-version': '23.13.0.3686',
248
+ 'origin': 'https://www.notion.so',
249
+ 'referer': 'https://www.notion.so/',
250
+ 'user-agent': window.navigator.userAgent,
251
+ 'Cookie': cookie
252
+ },
253
+ body: JSON.stringify({}),
254
+ };
255
+
256
+ // 添加代理配置(如果有)
257
+ if (this.proxyUrl) {
258
+ const { HttpsProxyAgent } = await import('https-proxy-agent');
259
+ fetchOptions.agent = new HttpsProxyAgent(this.proxyUrl);
260
+ logger.info(`使用代理: ${this.proxyUrl}`);
261
+ }
262
+
263
+ // 发送请求
264
+ const response = await fetch("https://www.notion.so/api/v3/getSpaces", fetchOptions);
265
+
266
+ // 检查响应状态
267
+ if (response.status === 401) {
268
+ return { success: false, status: 401, error: '未授权,cookie无效' };
269
+ }
270
+
271
+ if (!response.ok) {
272
+ throw new Error(`HTTP error! status: ${response.status}`);
273
+ }
274
+
275
+ const data = await response.json();
276
+
277
+ // 提取用户ID
278
+ const userIdKey = Object.keys(data)[0];
279
+ if (!userIdKey) {
280
+ throw new Error('无法从响应中提取用户ID');
281
+ }
282
+
283
+ const userId = userIdKey;
284
+
285
+ // 提取空间ID
286
+ const userRoot = data[userIdKey]?.user_root?.[userIdKey];
287
+ const spaceViewPointers = userRoot?.value?.value?.space_view_pointers;
288
+
289
+ if (!spaceViewPointers || !Array.isArray(spaceViewPointers) || spaceViewPointers.length === 0) {
290
+ throw new Error('在响应中找不到space_view_pointers或spaceId');
291
+ }
292
+
293
+ const spaceId = spaceViewPointers[0].spaceId;
294
+
295
+ if (!spaceId) {
296
+ throw new Error('无法从space_view_pointers中提取spaceId');
297
+ }
298
+
299
+ // 清理全局对象
300
+ this.cleanupGlobalObjects();
301
+
302
+ return {
303
+ success: true,
304
+ userId,
305
+ spaceId
306
+ };
307
+
308
+ } catch (error) {
309
+ // 清理全局对象
310
+ this.cleanupGlobalObjects();
311
+
312
+ // 重试逻���
313
+ if (retryCount < this.maxRetries && error.message !== '未授权,cookie无效') {
314
+ logger.warning(`获取Notion ID失败,正在重试 (${retryCount + 1}/${this.maxRetries}): ${error.message}`);
315
+ return await this.fetchNotionIds(cookie, retryCount + 1);
316
+ }
317
+
318
+ return {
319
+ success: false,
320
+ error: error.message
321
+ };
322
+ }
323
+ }
324
+
325
+ /**
326
+ * 清理全局对象
327
+ */
328
+ cleanupGlobalObjects() {
329
+ try {
330
+ if (global.window) delete global.window;
331
+ if (global.document) delete global.document;
332
+
333
+ // 安全地删除navigator
334
+ if (global.navigator) {
335
+ try {
336
+ delete global.navigator;
337
+ } catch (navError) {
338
+ // 如果无法删除,尝试将其设置为undefined
339
+ try {
340
+ Object.defineProperty(global, 'navigator', {
341
+ value: undefined,
342
+ writable: true,
343
+ configurable: true
344
+ });
345
+ } catch (defineError) {
346
+ logger.warning(`无法清理navigator: ${defineError.message}`);
347
+ }
348
+ }
349
+ }
350
+ } catch (cleanupError) {
351
+ logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
352
+ }
353
+ }
354
+
355
+ /**
356
+ * 获取下一个可用的cookie及其ID
357
+ * @returns {Object|null} - cookie及其对应的ID,如果没有可用cookie则返回null
358
+ */
359
+ getNext() {
360
+ if (!this.initialized || this.cookieEntries.length === 0) {
361
+ return null;
362
+ }
363
+
364
+ // 轮询选择下一个cookie
365
+ const entry = this.cookieEntries[this.currentIndex];
366
+
367
+ // 更新索引,实现轮询
368
+ this.currentIndex = (this.currentIndex + 1) % this.cookieEntries.length;
369
+
370
+ // 更新最后使用时间
371
+ entry.lastUsed = Date.now();
372
+
373
+ return {
374
+ cookie: entry.cookie,
375
+ spaceId: entry.spaceId,
376
+ userId: entry.userId
377
+ };
378
+ }
379
+
380
+ /**
381
+ * 标记cookie为无效
382
+ * @param {string} userId - 用户ID
383
+ */
384
+ markAsInvalid(userId) {
385
+ const index = this.cookieEntries.findIndex(entry => entry.userId === userId);
386
+ if (index !== -1) {
387
+ this.cookieEntries[index].valid = false;
388
+ logger.warning(`已将用户ID为 ${userId} 的cookie标记为无效`);
389
+
390
+ // 过滤掉所有无效的cookie
391
+ this.cookieEntries = this.cookieEntries.filter(entry => entry.valid);
392
+
393
+ // 重置当前索引
394
+ if (this.cookieEntries.length > 0) {
395
+ this.currentIndex = 0;
396
+ }
397
+ }
398
+ }
399
+
400
+ /**
401
+ * 获取有效cookie的数量
402
+ * @returns {number} - 有效cookie的数量
403
+ */
404
+ getValidCount() {
405
+ return this.cookieEntries.filter(entry => entry.valid).length;
406
+ }
407
+
408
+ /**
409
+ * 获取所有cookie的状态信息
410
+ * @returns {Array} - cookie状态数组
411
+ */
412
+ getStatus() {
413
+ return this.cookieEntries.map((entry, index) => ({
414
+ index,
415
+ userId: entry.userId.substring(0, 8) + '...',
416
+ spaceId: entry.spaceId.substring(0, 8) + '...',
417
+ valid: entry.valid,
418
+ lastUsed: entry.lastUsed ? new Date(entry.lastUsed).toLocaleString() : 'never'
419
+ }));
420
+ }
421
+ }
422
+
423
+ export const cookieManager = new CookieManager();
src/ProxyPool.js ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ /**
4
+ * 代理池类,用于管理和提供HTTP代理
5
+ */
6
+ class ProxyPool {
7
+ /**
8
+ * 创建代理池实例
9
+ * @param {Object} options - 配置选项
10
+ * @param {number} options.targetCount - 目标代理数量,默认20
11
+ * @param {number} options.batchSize - 每次获取的代理数量,默认20
12
+ * @param {number} options.testTimeout - 测试代理超时时间(毫秒),默认5000
13
+ * @param {number} options.requestTimeout - 请求目标网站超时时间(毫秒),默认10000
14
+ * @param {string} options.targetUrl - 目标网站URL,默认'https://www.notion.so'
15
+ * @param {number} options.concurrentRequests - 并发请求数量,默认10
16
+ * @param {number} options.minThreshold - 可用代理数量低于此阈值时自动补充,默认5
17
+ * @param {number} options.checkInterval - 检查代理池状态的时间间隔(毫秒),默认30000
18
+ * @param {string} options.proxyProtocol - 代理协议,默认'http'
19
+ * @param {number} options.maxRefillAttempts - 最大补充尝试次数,默认20
20
+ * @param {number} options.retryDelay - 重试延迟(毫秒),默认1000
21
+ * @param {boolean} options.useCache - 是否使用缓存,默认true
22
+ * @param {number} options.cacheExpiry - 缓存过期时间(毫秒),默认3600000 (1小时)
23
+ */
24
+ constructor(options = {}) {
25
+ // 配置参数
26
+ this.targetCount = options.targetCount || 20;
27
+ this.batchSize = options.batchSize || 20;
28
+ this.testTimeout = options.testTimeout || 5000;
29
+ this.requestTimeout = options.requestTimeout || 10000;
30
+ this.targetUrl = options.targetUrl || 'https://www.notion.so';
31
+ this.concurrentRequests = options.concurrentRequests || 10;
32
+ this.minThreshold = options.minThreshold || 5;
33
+ this.checkInterval = options.checkInterval || 30000; // 默认30秒检查一次
34
+ this.proxyProtocol = options.proxyProtocol || 'http';
35
+ this.maxRefillAttempts = options.maxRefillAttempts || 20; // 减少最大尝试次数
36
+ this.retryDelay = options.retryDelay || 1000; // 减少重试延迟
37
+ this.useCache = options.useCache !== undefined ? options.useCache : true;
38
+ this.cacheExpiry = options.cacheExpiry || 3600000; // 默认1小时
39
+
40
+ // 内部状态
41
+ this.availableProxies = [];
42
+ this.currentIndex = 0;
43
+ this.isInitialized = false;
44
+ this.isRefilling = false;
45
+ this.checkTimer = null;
46
+ this.proxyCache = new Map(); // 缓存验证过的代理
47
+
48
+ // 绑定方法
49
+ this.getProxy = this.getProxy.bind(this);
50
+ this.removeProxy = this.removeProxy.bind(this);
51
+ this.checkAndRefill = this.checkAndRefill.bind(this);
52
+ }
53
+
54
+ /**
55
+ * 初始化代理池
56
+ * @returns {Promise<void>}
57
+ */
58
+ async initialize() {
59
+ if (this.isInitialized) return;
60
+
61
+ console.log(`初始化代理池,目标数量: ${this.targetCount}`);
62
+ await this.refillProxies();
63
+
64
+ // 设置定时检查
65
+ this.checkTimer = setInterval(this.checkAndRefill, this.checkInterval);
66
+
67
+ this.isInitialized = true;
68
+ console.log(`代理池初始化完成,当前可用代理数量: ${this.availableProxies.length}`);
69
+ }
70
+
71
+ /**
72
+ * 停止代理池服务
73
+ */
74
+ stop() {
75
+ if (this.checkTimer) {
76
+ clearInterval(this.checkTimer);
77
+ this.checkTimer = null;
78
+ }
79
+ console.log('代理池服务已停止');
80
+ }
81
+
82
+ /**
83
+ * 检查并补充代理
84
+ */
85
+ async checkAndRefill() {
86
+ if (this.availableProxies.length <= this.minThreshold && !this.isRefilling) {
87
+ console.log(`可用代理数量(${this.availableProxies.length})低于阈值(${this.minThreshold}),开始补充代理`);
88
+ await this.refillProxies();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 补充代理到目标数量
94
+ * @returns {Promise<void>}
95
+ */
96
+ async refillProxies() {
97
+ if (this.isRefilling) return;
98
+
99
+ this.isRefilling = true;
100
+ console.log(`开始补充代理,当前数量: ${this.availableProxies.length},目标数量: ${this.targetCount}`);
101
+
102
+ let attempts = 0;
103
+
104
+ try {
105
+ // 计算需要补充的代理数量
106
+ const neededProxies = this.targetCount - this.availableProxies.length;
107
+
108
+ // 优先检查缓存中的代理
109
+ if (this.useCache && this.proxyCache.size > 0) {
110
+ await this.tryUsingCachedProxies(neededProxies);
111
+ }
112
+
113
+ // 如果缓存中的代理不足,继续获取新代理
114
+ while (this.availableProxies.length < this.targetCount && attempts < this.maxRefillAttempts) {
115
+ attempts++;
116
+
117
+ console.log(`补充尝试 #${attempts},当前可用代理: ${this.availableProxies.length}/${this.targetCount}`);
118
+
119
+ // 计算本次需要获取的批次大小
120
+ const remainingNeeded = this.targetCount - this.availableProxies.length;
121
+ const batchSizeNeeded = Math.max(this.batchSize, remainingNeeded * 2); // 获取更多代理以提高成功率
122
+
123
+ // 获取代理
124
+ const proxies = await this.getProxiesFromProvider(batchSizeNeeded);
125
+
126
+ if (proxies.length === 0) {
127
+ console.log(`没有获取到代理,等待${this.retryDelay/1000}秒后重试...`);
128
+ await new Promise(resolve => setTimeout(resolve, this.retryDelay));
129
+ continue;
130
+ }
131
+
132
+ // 过滤掉已有的代理
133
+ const newProxies = this.filterExistingProxies(proxies);
134
+
135
+ if (newProxies.length === 0) {
136
+ console.log('所有获取的代理都已存在,继续获取新代理...');
137
+ continue;
138
+ }
139
+
140
+ // 测试代理
141
+ const results = await this.testProxiesConcurrently(newProxies);
142
+
143
+ // 添加可用代理
144
+ this.addValidProxies(results);
145
+
146
+ // 如果已经获取到足够的代理,提前结束
147
+ if (this.availableProxies.length >= this.targetCount) {
148
+ break;
149
+ }
150
+
151
+ // 如果还没补充到足够的代理,等待一段时间再继续
152
+ if (this.availableProxies.length < this.targetCount) {
153
+ await new Promise(resolve => setTimeout(resolve, this.retryDelay));
154
+ }
155
+ }
156
+ } catch (error) {
157
+ console.error('补充代理过程中出错:', error);
158
+ } finally {
159
+ this.isRefilling = false;
160
+
161
+ if (this.availableProxies.length >= this.targetCount) {
162
+ console.log(`代理补充完成,当前可用代理: ${this.availableProxies.length}/${this.targetCount}`);
163
+ } else {
164
+ console.log(`已达到最大尝试次数 ${this.maxRefillAttempts},当前可用代理: ${this.availableProxies.length}/${this.targetCount}`);
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * 尝试使用缓存中的代理
171
+ * @param {number} neededProxies - 需要的代理数量
172
+ */
173
+ async tryUsingCachedProxies(neededProxies) {
174
+ const now = Date.now();
175
+ const cachedProxies = [];
176
+
177
+ // 筛选未过期的缓存代理
178
+ for (const [proxyKey, data] of this.proxyCache.entries()) {
179
+ if (now - data.timestamp < this.cacheExpiry && data.valid) {
180
+ cachedProxies.push(proxyKey);
181
+
182
+ if (cachedProxies.length >= neededProxies) {
183
+ break;
184
+ }
185
+ }
186
+ }
187
+
188
+ if (cachedProxies.length > 0) {
189
+ console.log(`从缓存中找到 ${cachedProxies.length} 个可能可用的代理`);
190
+
191
+ // 验证缓存的代理是否仍然可用
192
+ const results = await this.testProxiesConcurrently(cachedProxies);
193
+ this.addValidProxies(results);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 过滤掉已存在的代理
199
+ * @param {Array<string>} proxies - 代理列表
200
+ * @returns {Array<string>} - 新代理列表
201
+ */
202
+ filterExistingProxies(proxies) {
203
+ return proxies.filter(proxy => {
204
+ const [ip, port] = proxy.split(':');
205
+ return !this.availableProxies.some(p => p.ip === ip && p.port === port);
206
+ });
207
+ }
208
+
209
+ /**
210
+ * 添加有效的代理到代理池
211
+ * @param {Array<{proxy: string, result: boolean}>} results - 测试结果
212
+ */
213
+ addValidProxies(results) {
214
+ for (const { proxy, result } of results) {
215
+ if (result) {
216
+ const [ip, port] = proxy.split(':');
217
+
218
+ // 检查是否已存在
219
+ if (!this.availableProxies.some(p => p.ip === ip && p.port === port)) {
220
+ const proxyObj = {
221
+ ip,
222
+ port,
223
+ protocol: this.proxyProtocol,
224
+ full: `${this.proxyProtocol}://${proxy}`,
225
+ addedAt: new Date().toISOString()
226
+ };
227
+
228
+ this.availableProxies.push(proxyObj);
229
+
230
+ // 添加到缓存
231
+ if (this.useCache) {
232
+ this.proxyCache.set(proxy, {
233
+ valid: true,
234
+ timestamp: Date.now()
235
+ });
236
+ }
237
+
238
+ console.log(`成功添加代理: ${proxyObj.full},当前可用代理: ${this.availableProxies.length}/${this.targetCount}`);
239
+
240
+ if (this.availableProxies.length >= this.targetCount) {
241
+ break;
242
+ }
243
+ }
244
+ } else if (this.useCache) {
245
+ // 记录无效代理到缓存
246
+ this.proxyCache.set(proxy, {
247
+ valid: false,
248
+ timestamp: Date.now()
249
+ });
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 从代理服务获取代理URL
256
+ * @param {number} count - 请求的代理数量
257
+ * @returns {Promise<Array<string>>} - 代理URL列表
258
+ */
259
+ async getProxiesFromProvider(count = null) {
260
+ try {
261
+ const requestCount = count || this.batchSize;
262
+ const url = `https://proxy.scdn.io/api/get_proxy.php?protocol=${this.proxyProtocol}&count=${requestCount}`;
263
+ console.log(`正在获取代理,URL: ${url}`);
264
+
265
+ const response = await axios.get(url, {
266
+ timeout: 10000,
267
+ validateStatus: status => true
268
+ });
269
+
270
+ if (response.data && response.data.code === 200) {
271
+ console.log(`成功获取 ${response.data.data.count} 个代理`);
272
+ return response.data.data.proxies;
273
+ } else {
274
+ console.error('获取代理失败:', response.data ? response.data.message : '未知错误');
275
+ return [];
276
+ }
277
+ } catch (error) {
278
+ console.error('获取代理出错:', error.message);
279
+ return [];
280
+ }
281
+ }
282
+
283
+ /**
284
+ * 并发测试多个代理
285
+ * @param {Array<string>} proxies - 代理列表
286
+ * @returns {Promise<Array<{proxy: string, result: boolean}>>} - 测试结果
287
+ */
288
+ async testProxiesConcurrently(proxies) {
289
+ const results = [];
290
+ const remainingNeeded = this.targetCount - this.availableProxies.length;
291
+
292
+ // 增加并发数以加快处理速度
293
+ const concurrentRequests = Math.min(this.concurrentRequests * 2, 20);
294
+
295
+ // 分批处理代理
296
+ for (let i = 0; i < proxies.length; i += concurrentRequests) {
297
+ const batch = proxies.slice(i, i + concurrentRequests);
298
+ const promises = batch.map(proxy => {
299
+ // 检查缓存中是否有近期验证过的结果
300
+ if (this.useCache && this.proxyCache.has(proxy)) {
301
+ const cachedResult = this.proxyCache.get(proxy);
302
+ const isFresh = (Date.now() - cachedResult.timestamp) < this.cacheExpiry;
303
+
304
+ if (isFresh) {
305
+ // 使用缓存结果,避免重复测试
306
+ return Promise.resolve({ proxy, result: cachedResult.valid });
307
+ }
308
+ }
309
+
310
+ return this.testProxy(proxy)
311
+ .then(result => ({ proxy, result }))
312
+ .catch(() => ({ proxy, result: false }));
313
+ });
314
+
315
+ const batchResults = await Promise.all(promises);
316
+ results.push(...batchResults);
317
+
318
+ // 如果已经找到足够的代理,提前结束测试
319
+ const successCount = results.filter(item => item.result).length;
320
+ if (successCount >= remainingNeeded) {
321
+ break;
322
+ }
323
+ }
324
+
325
+ return results;
326
+ }
327
+
328
+ /**
329
+ * 测试代理是否可用
330
+ * @param {string} proxyUrl - 代理URL
331
+ * @returns {Promise<boolean>} - 代理是否可用
332
+ */
333
+ async testProxy(proxyUrl) {
334
+ try {
335
+ // 创建代理配置
336
+ const proxyConfig = {
337
+ host: proxyUrl.split(':')[0],
338
+ port: parseInt(proxyUrl.split(':')[1]),
339
+ protocol: this.proxyProtocol
340
+ };
341
+
342
+ // 发送请求到目标网站
343
+ const response = await axios.get(this.targetUrl, {
344
+ proxy: proxyConfig,
345
+ headers: {
346
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
347
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
348
+ 'Accept-Language': 'en-US,en;q=0.5',
349
+ 'Connection': 'keep-alive',
350
+ 'Upgrade-Insecure-Requests': '1'
351
+ },
352
+ timeout: this.requestTimeout,
353
+ validateStatus: status => true,
354
+ maxRedirects: 10,
355
+ followRedirect: true
356
+ });
357
+
358
+ // 检查响应是否包含目标网站特有的内容
359
+ const isTargetContent = response.data &&
360
+ (typeof response.data === 'string') &&
361
+ (response.data.includes('notion') ||
362
+ response.data.includes('Notion'));
363
+
364
+ const isValid = response.status === 200 && isTargetContent;
365
+
366
+ if (isValid) {
367
+ console.log(`代理 ${proxyUrl} 请求目标网站成功,状态码: ${response.status}`);
368
+ } else {
369
+ console.log(`代理 ${proxyUrl} 请求目标网站失败,状态码: ${response.status}`);
370
+ }
371
+
372
+ return isValid;
373
+ } catch (error) {
374
+ console.log(`代理 ${proxyUrl} 请求出错: ${error.message}`);
375
+ return false;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * 获取一个可用代理
381
+ * @returns {Object|null} - 代理对象,如果没有可用代理则返回null
382
+ */
383
+ getProxy() {
384
+ if (this.availableProxies.length === 0) {
385
+ console.log('没有可用代理');
386
+ return null;
387
+ }
388
+
389
+ // 轮询方式获取代理
390
+ const proxy = this.availableProxies[this.currentIndex];
391
+ this.currentIndex = (this.currentIndex + 1) % this.availableProxies.length;
392
+
393
+ return proxy;
394
+ }
395
+
396
+ /**
397
+ * 移除指定代理
398
+ * @param {string} ip - 代理IP
399
+ * @param {string|number} port - 代理端口
400
+ * @returns {boolean} - 是否成功移除
401
+ */
402
+ removeProxy(ip, port) {
403
+ const portStr = port.toString();
404
+ const initialLength = this.availableProxies.length;
405
+
406
+ // 找到要移除的代理
407
+ const proxyToRemove = this.availableProxies.find(
408
+ proxy => proxy.ip === ip && proxy.port === portStr
409
+ );
410
+
411
+ if (proxyToRemove) {
412
+ // 更新缓存,标记为无效
413
+ if (this.useCache) {
414
+ const proxyKey = `${ip}:${portStr}`;
415
+ this.proxyCache.set(proxyKey, { valid: false, timestamp: Date.now() });
416
+ }
417
+ }
418
+
419
+ this.availableProxies = this.availableProxies.filter(
420
+ proxy => !(proxy.ip === ip && proxy.port === portStr)
421
+ );
422
+
423
+ // 重置当前索引,确保不会越界
424
+ if (this.currentIndex >= this.availableProxies.length && this.availableProxies.length > 0) {
425
+ this.currentIndex = 0;
426
+ }
427
+
428
+ const removed = initialLength > this.availableProxies.length;
429
+
430
+ if (removed) {
431
+ console.log(`已移除代理 ${ip}:${port},当前可用代理: ${this.availableProxies.length}`);
432
+ } else {
433
+ console.log(`未找到要移除的代理 ${ip}:${port}`);
434
+ }
435
+
436
+ // 如果移除后代理数量低于阈值,触发补充
437
+ this.checkAndRefill();
438
+
439
+ return removed;
440
+ }
441
+
442
+ /**
443
+ * 获取所有可用代理
444
+ * @returns {Array<Object>} - 代理对象数组
445
+ */
446
+ getAllProxies() {
447
+ return [...this.availableProxies];
448
+ }
449
+
450
+ /**
451
+ * 获取可用代理数量
452
+ * @returns {number} - 代理数量
453
+ */
454
+ getCount() {
455
+ return this.availableProxies.length;
456
+ }
457
+
458
+ /**
459
+ * 清理过期的缓存条目
460
+ */
461
+ cleanupCache() {
462
+ if (!this.useCache) return;
463
+
464
+ const now = Date.now();
465
+ let cleanupCount = 0;
466
+
467
+ for (const [key, data] of this.proxyCache.entries()) {
468
+ if (now - data.timestamp > this.cacheExpiry) {
469
+ this.proxyCache.delete(key);
470
+ cleanupCount++;
471
+ }
472
+ }
473
+
474
+ if (cleanupCount > 0) {
475
+ console.log(`清理了 ${cleanupCount} 个过期的缓存代理`);
476
+ }
477
+ }
478
+ }
479
+
480
+ // 使用示例
481
+ async function example() {
482
+ // 创建代理池实例
483
+ const proxyPool = new ProxyPool({
484
+ targetCount: 10, // 目标保持10个代理
485
+ minThreshold: 3, // 当可用代理少于3个时,自动补充
486
+ checkInterval: 60000, // 每60秒检查一次
487
+ targetUrl: 'https://www.notion.so',
488
+ concurrentRequests: 15, // 增加并发请求数
489
+ useCache: true, // 启用缓存
490
+ maxRefillAttempts: 15, // 减少最大尝试次数
491
+ retryDelay: 1000 // 减少重试延迟
492
+ });
493
+
494
+ // 初始化代理池
495
+ await proxyPool.initialize();
496
+
497
+ // 获取一个代理
498
+ const proxy = proxyPool.getProxy();
499
+ console.log('获取到代理:', proxy);
500
+
501
+ // 模拟使用一段时间后,移除一个代理
502
+ setTimeout(() => {
503
+ if (proxy) {
504
+ proxyPool.removeProxy(proxy.ip, proxy.port);
505
+ }
506
+
507
+ // 获取所有代理
508
+ const allProxies = proxyPool.getAllProxies();
509
+ console.log(`当前所有代理(${allProxies.length}):`, allProxies);
510
+
511
+ // 使用完毕后停止服务
512
+ setTimeout(() => {
513
+ proxyPool.stop();
514
+ console.log('代理池示例运行完毕');
515
+ }, 5000);
516
+ }, 5000);
517
+ }
518
+
519
+ // 如果直接运行此文件,则执行示例
520
+ if (typeof require !== 'undefined' && require.main === module) {
521
+ example().catch(err => console.error('示例运行出错:', err));
522
+ }
523
+
524
+ // 导出 ProxyPool 类和实例
525
+ export default ProxyPool;
526
+ export const proxyPool = new ProxyPool();
src/ProxyServer.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { spawn } from 'child_process';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import dotenv from 'dotenv';
7
+ import chalk from 'chalk';
8
+
9
+ // 获取当前文件的目录路径
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ // 加载环境变量
14
+ dotenv.config({ path: join(dirname(__dirname), '.env') });
15
+
16
+ // 日志配置
17
+ const logger = {
18
+ info: (message) => console.log(chalk.blue(`[ProxyServer] ${message}`)),
19
+ error: (message) => console.error(chalk.red(`[ProxyServer] ${message}`)),
20
+ warning: (message) => console.warn(chalk.yellow(`[ProxyServer] ${message}`)),
21
+ success: (message) => console.log(chalk.green(`[ProxyServer] ${message}`)),
22
+ };
23
+
24
+ class ProxyServer {
25
+ constructor() {
26
+ this.proxyProcess = null;
27
+ this.platform = process.env.PROXY_SERVER_PLATFORM || 'auto';
28
+ this.port = process.env.PROXY_SERVER_PORT || 10655;
29
+ this.logPath = process.env.PROXY_SERVER_LOG_PATH || './proxy_server.log';
30
+ this.enabled = process.env.ENABLE_PROXY_SERVER === 'true';
31
+ this.proxyAuthToken = process.env.PROXY_AUTH_TOKEN || 'default_token';
32
+ this.logStream = null;
33
+ }
34
+
35
+ // 获取当前系统平台
36
+ detectPlatform() {
37
+ if (this.platform !== 'auto') {
38
+ return this.platform;
39
+ }
40
+
41
+ const platform = os.platform();
42
+ const arch = os.arch();
43
+
44
+ if (platform === 'win32') {
45
+ return 'windows';
46
+ } else if (platform === 'linux') {
47
+ if (arch === 'arm64') {
48
+ return 'android';
49
+ } else {
50
+ return 'linux';
51
+ }
52
+ } else if (platform === 'android') {
53
+ return 'android';
54
+ } else {
55
+ logger.warning(`未知平台: ${platform}, ${arch}, 默认使用linux版本`);
56
+ return 'linux';
57
+ }
58
+ }
59
+
60
+ // 获取代理服务器可执行文件路径
61
+ getProxyServerPath() {
62
+ const platform = this.detectPlatform();
63
+ const proxyDir = join(__dirname, 'proxy');
64
+
65
+ switch (platform) {
66
+ case 'windows':
67
+ return join(proxyDir, 'chrome_proxy_server_windows_amd64.exe');
68
+ case 'linux':
69
+ return join(proxyDir, 'chrome_proxy_server_linux_amd64');
70
+ case 'android':
71
+ return join(proxyDir, 'chrome_proxy_server_android_arm64');
72
+ default:
73
+ logger.error(`不支持的平台: ${platform}`);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // 启动代理服务器
79
+ async start() {
80
+ if (!this.enabled) {
81
+ logger.info('代理服务器未启用,跳过启动');
82
+ return;
83
+ }
84
+
85
+ if (this.proxyProcess) {
86
+ logger.warning('代理服务器已经在运行中');
87
+ return;
88
+ }
89
+
90
+ const proxyServerPath = this.getProxyServerPath();
91
+ if (!proxyServerPath) {
92
+ logger.error('无法获取代理服务器路径');
93
+ return;
94
+ }
95
+
96
+ try {
97
+ // 确保可执行文件有执行权限(在Linux/Android上)
98
+ if (this.detectPlatform() !== 'windows') {
99
+ try {
100
+ fs.chmodSync(proxyServerPath, 0o755);
101
+ } catch (err) {
102
+ logger.warning(`无法设置执行权限: ${err.message}`);
103
+ }
104
+ }
105
+
106
+ // 创建日志文件
107
+ this.logStream = fs.createWriteStream(this.logPath, { flags: 'a' });
108
+
109
+ // 修复 stdio 参数问题
110
+ // 启动代理服务器进程
111
+ this.proxyProcess = spawn(proxyServerPath, [
112
+ '--port', this.port.toString(),
113
+ '--token', this.proxyAuthToken
114
+ ], {
115
+ stdio: ['ignore', 'pipe', 'pipe'], // 使用pipe而不是直接传递流
116
+ detached: false
117
+ });
118
+
119
+ // 将进程的输出重定向到日志文件
120
+ if (this.proxyProcess.stdout) {
121
+ this.proxyProcess.stdout.pipe(this.logStream);
122
+ }
123
+
124
+ if (this.proxyProcess.stderr) {
125
+ this.proxyProcess.stderr.pipe(this.logStream);
126
+ }
127
+
128
+ // 设置进程事件处理
129
+ this.proxyProcess.on('error', (err) => {
130
+ logger.error(`代理服务器启动失败: ${err.message}`);
131
+ this.proxyProcess = null;
132
+ if (this.logStream) {
133
+ this.logStream.end();
134
+ this.logStream = null;
135
+ }
136
+ });
137
+
138
+ this.proxyProcess.on('exit', (code, signal) => {
139
+ logger.info(`代理服务器已退出,退出码: ${code}, 信号: ${signal}`);
140
+ this.proxyProcess = null;
141
+ if (this.logStream) {
142
+ this.logStream.end();
143
+ this.logStream = null;
144
+ }
145
+ });
146
+
147
+ // 等待一段时间,确保服务器启动
148
+ await new Promise(resolve => setTimeout(resolve, 1000));
149
+
150
+ if (this.proxyProcess && this.proxyProcess.exitCode === null) {
151
+ logger.success(`代理服务器已启动,端口: ${this.port}, 日志文件: ${this.logPath}`);
152
+ return true;
153
+ } else {
154
+ logger.error('代理服务器启动失败');
155
+ if (this.logStream) {
156
+ this.logStream.end();
157
+ this.logStream = null;
158
+ }
159
+ return false;
160
+ }
161
+ } catch (error) {
162
+ logger.error(`启动代理服务器时出错: ${error.message}`);
163
+ if (this.logStream) {
164
+ this.logStream.end();
165
+ this.logStream = null;
166
+ }
167
+ return false;
168
+ }
169
+ }
170
+
171
+ // 停止代理服务器
172
+ stop() {
173
+ if (!this.proxyProcess) {
174
+ //logger.info('代理服务器已关闭');
175
+ return;
176
+ }
177
+
178
+ try {
179
+ // 在Windows上使用taskkill确保子进程也被终止
180
+ if (this.detectPlatform() === 'windows' && this.proxyProcess.pid) {
181
+ spawn('taskkill', ['/pid', this.proxyProcess.pid, '/f', '/t']);
182
+ } else {
183
+ // 在Linux/Android上使用kill信号
184
+ this.proxyProcess.kill('SIGTERM');
185
+ }
186
+
187
+ logger.success('代理服务器已停止');
188
+ } catch (error) {
189
+ logger.error(`停止代理服务器时出错: ${error.message}`);
190
+ } finally {
191
+ this.proxyProcess = null;
192
+ if (this.logStream) {
193
+ this.logStream.end();
194
+ this.logStream = null;
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ // 创建单例
201
+ const proxyServer = new ProxyServer();
202
+
203
+ // 导出
204
+ export { proxyServer };
src/cookie-cli.js ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { cookieManager } from './CookieManager.js';
4
+ import dotenv from 'dotenv';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ import fs from 'fs';
8
+ import readline from 'readline';
9
+ import chalk from 'chalk';
10
+
11
+ // 获取当前文件的目录路径
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ // 加载环境变量
16
+ dotenv.config({ path: join(dirname(__dirname), '.env') });
17
+
18
+ // 日志配置
19
+ const logger = {
20
+ info: (message) => console.log(chalk.blue(`[信息] ${message}`)),
21
+ error: (message) => console.error(chalk.red(`[错误] ${message}`)),
22
+ warning: (message) => console.warn(chalk.yellow(`[警告] ${message}`)),
23
+ success: (message) => console.log(chalk.green(`[成功] ${message}`)),
24
+ };
25
+
26
+ // 创建readline接口
27
+ const rl = readline.createInterface({
28
+ input: process.stdin,
29
+ output: process.stdout
30
+ });
31
+
32
+ // 默认cookie文件路径
33
+ const DEFAULT_COOKIE_FILE = 'cookies.txt';
34
+
35
+ let isSaveCookie = false;
36
+ let isAddCookie = false;
37
+
38
+ // 显示帮助信息
39
+ function showHelp() {
40
+ console.log(chalk.cyan('Notion Cookie 管理工具'));
41
+ console.log(chalk.cyan('===================='));
42
+ console.log('');
43
+ console.log('可用命令:');
44
+ console.log(' help - 显示此帮助信息');
45
+ console.log(' list - 列出所有cookie');
46
+ console.log(' add - 添加新的cookie');
47
+ console.log(' validate - 验证所有cookie');
48
+ console.log(' remove - 删除指定cookie');
49
+ console.log(' save - 保存cookie到文件');
50
+ console.log(' load - 从文件加载cookie');
51
+ console.log(' exit - 退出程序');
52
+ console.log('');
53
+ }
54
+
55
+ // 列出所有cookie
56
+ async function listCookies() {
57
+ if (!cookieManager.initialized || cookieManager.getValidCount() === 0) {
58
+ logger.warning('没有可用的cookie,请先加载或添加cookie');
59
+ return;
60
+ }
61
+
62
+ const status = cookieManager.getStatus();
63
+ console.log(chalk.cyan('\nCookie 列表:'));
64
+ console.log(chalk.cyan('==========='));
65
+
66
+ status.forEach((entry, idx) => {
67
+ const validMark = entry.valid ? chalk.green('✓') : chalk.red('✗');
68
+ console.log(`${idx + 1}. ${validMark} 用户ID: ${entry.userId}, 空间ID: ${entry.spaceId}, 上次使用: ${entry.lastUsed}`);
69
+ });
70
+
71
+ console.log(`\n共有 ${status.length} 个cookie,${cookieManager.getValidCount()} 个有效\n`);
72
+ }
73
+
74
+ // 添加新cookie
75
+ async function addCookie() {
76
+ return new Promise((resolve) => {
77
+ rl.question(chalk.yellow('请输入Notion cookie: '), async (cookie) => {
78
+ if (!cookie || cookie.trim() === '') {
79
+ logger.error('Cookie不能为空');
80
+ resolve();
81
+ return;
82
+ }
83
+
84
+ logger.info('正在验证cookie...');
85
+ const result = await cookieManager.fetchNotionIds(cookie.trim());
86
+
87
+ if (result.success) {
88
+ // 如果cookie管理器尚未初始化,先初始化
89
+ if (!cookieManager.initialized) {
90
+ await cookieManager.initialize(cookie.trim());
91
+ } else {
92
+ // 已初始化,直接添加到现有条目
93
+ cookieManager.cookieEntries.push({
94
+ cookie: cookie.trim(),
95
+ spaceId: result.spaceId,
96
+ userId: result.userId,
97
+ valid: true,
98
+ lastUsed: 0
99
+ });
100
+ }
101
+ isAddCookie = true;
102
+ logger.success(`Cookie添加成功! 用户ID: ${result.userId}, 空间ID: ${result.spaceId}`);
103
+
104
+ } else {
105
+ logger.error(`Cookie验证失败: ${result.error}`);
106
+ }
107
+
108
+ resolve();
109
+ });
110
+ });
111
+ }
112
+
113
+ // 验证所有cookie
114
+ async function validateCookies() {
115
+ if (!cookieManager.initialized || cookieManager.cookieEntries.length === 0) {
116
+ logger.warning('没有可用的cookie,请先加载或添加cookie');
117
+ return;
118
+ }
119
+
120
+ logger.info('开始验证所有cookie...');
121
+
122
+ const originalEntries = [...cookieManager.cookieEntries];
123
+ cookieManager.cookieEntries = [];
124
+
125
+ for (let i = 0; i < originalEntries.length; i++) {
126
+ const entry = originalEntries[i];
127
+ logger.info(`正在验证第 ${i+1}/${originalEntries.length} 个cookie...`);
128
+
129
+ const result = await cookieManager.fetchNotionIds(entry.cookie);
130
+ if (result.success) {
131
+ cookieManager.cookieEntries.push({
132
+ cookie: entry.cookie,
133
+ spaceId: result.spaceId,
134
+ userId: result.userId,
135
+ valid: true,
136
+ lastUsed: entry.lastUsed || 0
137
+ });
138
+ logger.success(`第 ${i+1} 个cookie验证成功`);
139
+ } else {
140
+ logger.error(`第 ${i+1} 个cookie验证失败: ${result.error}`);
141
+ }
142
+ }
143
+
144
+ logger.info(`验证完成,共 ${originalEntries.length} 个cookie,${cookieManager.cookieEntries.length} 个有效`);
145
+ }
146
+
147
+ // 删除指定cookie
148
+ async function removeCookie() {
149
+ if (!cookieManager.initialized || cookieManager.cookieEntries.length === 0) {
150
+ logger.warning('没有可用的cookie,请先加载或添加cookie');
151
+ return;
152
+ }
153
+
154
+ // 先列出所有cookie
155
+ await listCookies();
156
+
157
+ return new Promise((resolve) => {
158
+ rl.question(chalk.yellow('请输入要删除的cookie编号: '), (input) => {
159
+ const index = parseInt(input) - 1;
160
+
161
+ if (isNaN(index) || index < 0 || index >= cookieManager.cookieEntries.length) {
162
+ logger.error('无效的编号');
163
+ resolve();
164
+ return;
165
+ }
166
+
167
+ const removed = cookieManager.cookieEntries.splice(index, 1)[0];
168
+ logger.success(`已删除编号 ${index + 1} 的cookie (用户ID: ${removed.userId.substring(0, 8)}...)`);
169
+
170
+ // 重置当前索引
171
+ if (cookieManager.cookieEntries.length > 0) {
172
+ cookieManager.currentIndex = 0;
173
+ }
174
+
175
+ resolve();
176
+ });
177
+ });
178
+ }
179
+
180
+ // 保存cookie到文件
181
+ async function saveCookies() {
182
+ if (!cookieManager.initialized || cookieManager.cookieEntries.length === 0) {
183
+ logger.warning('没有可用的cookie,请先加载或添加cookie');
184
+ return;
185
+ }
186
+
187
+ return new Promise((resolve) => {
188
+ rl.question(chalk.yellow(`请输入保存文件路径 (默认: ${DEFAULT_COOKIE_FILE}): `), (filePath) => {
189
+ const path = filePath.trim() || DEFAULT_COOKIE_FILE;
190
+
191
+ rl.question(chalk.yellow('是否只保存有效的cookie? (y/n, 默认: y): '), (onlyValidInput) => {
192
+ const onlyValid = onlyValidInput.toLowerCase() !== 'n';
193
+
194
+ const success = cookieManager.saveToFile(path, onlyValid);
195
+ if (success) {
196
+ logger.success(`Cookie已保存到文件: ${path}`);
197
+ isSaveCookie = true;
198
+ }
199
+
200
+ resolve();
201
+ });
202
+ });
203
+ });
204
+ }
205
+
206
+ // 从文件加载cookie
207
+ async function loadCookies() {
208
+ return new Promise((resolve) => {
209
+ rl.question(chalk.yellow(`请输入cookie文件路径 (默认: ${DEFAULT_COOKIE_FILE}): `), async (filePath) => {
210
+ const path = filePath.trim() || DEFAULT_COOKIE_FILE;
211
+
212
+ logger.info(`正在从文件加载cookie: ${path}`);
213
+ const success = await cookieManager.loadFromFile(path);
214
+
215
+ if (success) {
216
+ logger.success(`成功从文件加载cookie,共 ${cookieManager.getValidCount()} 个有效cookie`);
217
+ } else {
218
+ logger.error(`从文件加载cookie失败`);
219
+ }
220
+
221
+ resolve();
222
+ });
223
+ });
224
+ }
225
+
226
+ // 主函数
227
+ async function main() {
228
+ // 显示欢迎信息
229
+ console.log(chalk.cyan('\nNotion Cookie 管理工具'));
230
+ console.log(chalk.cyan('====================\n'));
231
+
232
+ // 检查是否有环境变量中的cookie
233
+ const envCookie = process.env.NOTION_COOKIE;
234
+ if (envCookie) {
235
+ logger.info('检测到环境变量中的NOTION_COOKIE,正在初始化...');
236
+ await cookieManager.initialize(envCookie);
237
+ }
238
+
239
+ // 检查是否有环境变量中的cookie文件
240
+ const envCookieFile = process.env.COOKIE_FILE;
241
+ if (envCookieFile && !cookieManager.initialized) {
242
+ logger.info(`检测到环境变量中的COOKIE_FILE: ${envCookieFile},正在加载...`);
243
+ await cookieManager.loadFromFile(envCookieFile);
244
+ }
245
+
246
+ // 如果没有cookie,检查默认文件
247
+ if (!cookieManager.initialized && fs.existsSync(DEFAULT_COOKIE_FILE)) {
248
+ logger.info(`检测到默认cookie文件: ${DEFAULT_COOKIE_FILE},正在加载...`);
249
+ await cookieManager.loadFromFile(DEFAULT_COOKIE_FILE);
250
+ }
251
+
252
+ showHelp();
253
+
254
+ // 命令循环
255
+ while (true) {
256
+ const command = await new Promise((resolve) => {
257
+ rl.question(chalk.green('> '), (cmd) => {
258
+ resolve(cmd.trim().toLowerCase());
259
+ });
260
+ });
261
+
262
+ switch (command) {
263
+ case 'help':
264
+ showHelp();
265
+ break;
266
+ case 'list':
267
+ await listCookies();
268
+ break;
269
+ case 'add':
270
+ await addCookie();
271
+ break;
272
+ case 'validate':
273
+ await validateCookies();
274
+ break;
275
+ case 'remove':
276
+ await removeCookie();
277
+ break;
278
+ case 'save':
279
+ await saveCookies();
280
+ break;
281
+ case 'load':
282
+ await loadCookies();
283
+ break;
284
+ case 'exit':
285
+ case 'quit':
286
+ case 'q':
287
+ logger.info('感谢使用,再见!');
288
+ rl.close();
289
+ process.exit(0);
290
+ default:
291
+ logger.error(`未知命令: ${command}`);
292
+ logger.info('输入 "help" 查看可用命令');
293
+ }
294
+ }
295
+ }
296
+
297
+ // 启动程序
298
+ main().catch((error) => {
299
+ logger.error(`程序出错: ${error.message}`);
300
+ process.exit(1);
301
+ });
src/lightweight-client-express.js ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import dotenv from 'dotenv';
3
+ import { randomUUID } from 'crypto';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+ import chalk from 'chalk';
7
+ import {
8
+ ChatMessage, ChatCompletionRequest, Choice, ChoiceDelta, ChatCompletionChunk
9
+ } from './models.js';
10
+ import {
11
+ initialize,
12
+ streamNotionResponse,
13
+ buildNotionRequest,
14
+ INITIALIZED_SUCCESSFULLY
15
+ } from './lightweight-client.js';
16
+ import { proxyPool } from './ProxyPool.js';
17
+ import { cookieManager } from './CookieManager.js';
18
+
19
+ // 获取当前文件的目录路径
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ // 加载环境变量
24
+ dotenv.config({ path: join(dirname(__dirname), '.env') });
25
+
26
+ // 日志配置
27
+ const logger = {
28
+ info: (message) => console.log(chalk.blue(`[info] ${message}`)),
29
+ error: (message) => console.error(chalk.red(`[error] ${message}`)),
30
+ warning: (message) => console.warn(chalk.yellow(`[warn] ${message}`)),
31
+ success: (message) => console.log(chalk.green(`[success] ${message}`)),
32
+ request: (method, path, status, time) => {
33
+ const statusColor = status >= 500 ? chalk.red :
34
+ status >= 400 ? chalk.yellow :
35
+ status >= 300 ? chalk.cyan :
36
+ status >= 200 ? chalk.green : chalk.white;
37
+ console.log(`${chalk.magenta(`[${method}]`)} - ${path} ${statusColor(status)} ${chalk.gray(`${time}ms`)}`);
38
+ }
39
+ };
40
+
41
+ // 认证配置
42
+ const EXPECTED_TOKEN = process.env.PROXY_AUTH_TOKEN || "default_token";
43
+
44
+ // 创建Express应用
45
+ const app = express();
46
+ app.use(express.json({ limit: '50mb' }));
47
+ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
48
+
49
+ // 请求日志中间件
50
+ app.use((req, res, next) => {
51
+ const start = Date.now();
52
+
53
+ // 保存原始的 end 方法
54
+ const originalEnd = res.end;
55
+
56
+ // 重写 end 方法以记录请求完成时间
57
+ res.end = function(...args) {
58
+ const duration = Date.now() - start;
59
+ logger.request(req.method, req.path, res.statusCode, duration);
60
+ return originalEnd.apply(this, args);
61
+ };
62
+
63
+ next();
64
+ });
65
+
66
+ // 认证中间件
67
+ function authenticate(req, res, next) {
68
+ const authHeader = req.headers.authorization;
69
+
70
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
71
+ return res.status(401).json({
72
+ error: {
73
+ message: "Authentication required. Please provide a valid Bearer token.",
74
+ type: "authentication_error"
75
+ }
76
+ });
77
+ }
78
+
79
+ const token = authHeader.split(' ')[1];
80
+
81
+ if (token !== EXPECTED_TOKEN) {
82
+ return res.status(401).json({
83
+ error: {
84
+ message: "Invalid authentication credentials",
85
+ type: "authentication_error"
86
+ }
87
+ });
88
+ }
89
+
90
+ next();
91
+ }
92
+
93
+ // API路由
94
+
95
+ // 获取模型列表
96
+ app.get('/v1/models', authenticate, (req, res) => {
97
+ // 返回可用模型列表
98
+ const modelList = {
99
+ data: [
100
+ { id: "openai-gpt-4.1" },
101
+ { id: "anthropic-opus-4" },
102
+ { id: "anthropic-sonnet-4" },
103
+ { id: "anthropic-sonnet-3.x-stable" }
104
+ ]
105
+ };
106
+
107
+ res.json(modelList);
108
+ });
109
+
110
+ // 聊天完成端点
111
+ app.post('/v1/chat/completions', authenticate, async (req, res) => {
112
+ try {
113
+ // 检查是否成功初始化
114
+ if (!INITIALIZED_SUCCESSFULLY) {
115
+ return res.status(500).json({
116
+ error: {
117
+ message: "系统未成功初始化。请检查您的NOTION_COOKIE是否有效。",
118
+ type: "server_error"
119
+ }
120
+ });
121
+ }
122
+
123
+ // 检查是否有可用的cookie
124
+ if (cookieManager.getValidCount() === 0) {
125
+ return res.status(500).json({
126
+ error: {
127
+ message: "没有可用的有效cookie。请检查您的NOTION_COOKIE配置。",
128
+ type: "server_error"
129
+ }
130
+ });
131
+ }
132
+
133
+ // 验证请求数据
134
+ const requestData = req.body;
135
+
136
+ if (!requestData.messages || !Array.isArray(requestData.messages) || requestData.messages.length === 0) {
137
+ return res.status(400).json({
138
+ error: {
139
+ message: "Invalid request: 'messages' field must be a non-empty array.",
140
+ type: "invalid_request_error"
141
+ }
142
+ });
143
+ }
144
+
145
+ // 构建Notion请求
146
+ const notionRequestBody = buildNotionRequest(requestData);
147
+
148
+ // 处理流式响应
149
+ if (requestData.stream) {
150
+ res.setHeader('Content-Type', 'text/event-stream');
151
+ res.setHeader('Cache-Control', 'no-cache');
152
+ res.setHeader('Connection', 'keep-alive');
153
+
154
+ logger.info(`开始流式响应`);
155
+ const stream = await streamNotionResponse(notionRequestBody);
156
+ stream.pipe(res);
157
+
158
+ // 处理客户端断开连接
159
+ req.on('close', () => {
160
+ stream.end();
161
+ });
162
+ } else {
163
+ // 非流式响应
164
+ // 创建一个内部流来收集完整响应
165
+ logger.info(`开始非流式响应`);
166
+ const chunks = [];
167
+ const stream = await streamNotionResponse(notionRequestBody);
168
+
169
+ return new Promise((resolve, reject) => {
170
+ stream.on('data', (chunk) => {
171
+ const chunkStr = chunk.toString();
172
+ if (chunkStr.startsWith('data: ') && !chunkStr.includes('[DONE]')) {
173
+ try {
174
+ const dataJson = chunkStr.substring(6).trim();
175
+ if (dataJson) {
176
+ const chunkData = JSON.parse(dataJson);
177
+ if (chunkData.choices && chunkData.choices[0].delta && chunkData.choices[0].delta.content) {
178
+ chunks.push(chunkData.choices[0].delta.content);
179
+ }
180
+ }
181
+ } catch (error) {
182
+ logger.error(`解析非流式响应块时出错: ${error}`);
183
+ }
184
+ }
185
+ });
186
+
187
+ stream.on('end', () => {
188
+ const fullResponse = {
189
+ id: `chatcmpl-${randomUUID()}`,
190
+ object: "chat.completion",
191
+ created: Math.floor(Date.now() / 1000),
192
+ model: requestData.model,
193
+ choices: [
194
+ {
195
+ index: 0,
196
+ message: {
197
+ role: "assistant",
198
+ content: chunks.join('')
199
+ },
200
+ finish_reason: "stop"
201
+ }
202
+ ],
203
+ usage: {
204
+ prompt_tokens: null,
205
+ completion_tokens: null,
206
+ total_tokens: null
207
+ }
208
+ };
209
+
210
+ res.json(fullResponse);
211
+ resolve();
212
+ });
213
+
214
+ stream.on('error', (error) => {
215
+ logger.error(`非流式响应出错: ${error}`);
216
+ reject(error);
217
+ });
218
+ });
219
+ }
220
+ } catch (error) {
221
+ logger.error(`聊天完成端点错误: ${error}`);
222
+ res.status(500).json({
223
+ error: {
224
+ message: `Internal server error: ${error.message}`,
225
+ type: "server_error"
226
+ }
227
+ });
228
+ }
229
+ });
230
+
231
+ // 健康检查端点
232
+ app.get('/health', (req, res) => {
233
+ res.json({
234
+ status: 'ok',
235
+ timestamp: new Date().toISOString(),
236
+ initialized: INITIALIZED_SUCCESSFULLY,
237
+ valid_cookies: cookieManager.getValidCount()
238
+ });
239
+ });
240
+
241
+ // Cookie状态查询端点
242
+ app.get('/cookies/status', authenticate, (req, res) => {
243
+ res.json({
244
+ total_cookies: cookieManager.getValidCount(),
245
+ cookies: cookieManager.getStatus()
246
+ });
247
+ });
248
+
249
+ // 启动服务器
250
+ const PORT = process.env.PORT || 7860;
251
+
252
+ // 初始化并启动服务器
253
+ initialize().then(() => {
254
+ app.listen(PORT, () => {
255
+ logger.info(`服务已启动 - 端口: ${PORT}`);
256
+ logger.info(`访问地址: http://localhost:${PORT}`);
257
+
258
+ if (INITIALIZED_SUCCESSFULLY) {
259
+ logger.success(`系统初始化状态: ✅`);
260
+ logger.success(`可用cookie数量: ${cookieManager.getValidCount()}`);
261
+ } else {
262
+ logger.warning(`系统初始化状态: ❌`);
263
+ logger.warning(`警告: 系统未成功初始化,API调用将无法正常工作`);
264
+ logger.warning(`请检查NOTION_COOKIE配置是否有效`);
265
+ }
266
+ });
267
+ }).catch((error) => {
268
+ logger.error(`初始化失败: ${error}`);
269
+ });
src/models.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomUUID } from 'crypto';
2
+
3
+ // 输入模型 (OpenAI-like)
4
+ export class ChatMessage {
5
+ constructor({
6
+ id = generateCustomId(),
7
+ role,
8
+ content,
9
+ userId = null,
10
+ createdAt = null,
11
+ traceId = null
12
+ }) {
13
+ this.id = id;
14
+ this.role = role; // "system", "user", "assistant"
15
+ this.content = content;
16
+ this.userId = userId;
17
+ this.createdAt = createdAt;
18
+ this.traceId = traceId;
19
+ }
20
+ }
21
+
22
+ export class ChatCompletionRequest {
23
+ constructor({
24
+ messages,
25
+ model = "notion-proxy",
26
+ stream = false,
27
+ notion_model = "anthropic-opus-4"
28
+ }) {
29
+ this.messages = messages;
30
+ this.model = model;
31
+ this.stream = stream;
32
+ this.notion_model = notion_model;
33
+ }
34
+ }
35
+
36
+ // Notion 模型
37
+ export class NotionTranscriptConfigValue {
38
+ constructor({
39
+ type = "markdown-chat",
40
+ model
41
+ }) {
42
+ this.type = type;
43
+ this.model = model;
44
+ }
45
+ }
46
+
47
+
48
+ export class NotionTranscriptContextValue {
49
+ constructor({
50
+ userId,
51
+ spaceId,
52
+ surface = "home_module",
53
+ timezone = "America/Los_Angeles",
54
+ userName,
55
+ spaceName,
56
+ spaceViewId,
57
+ currentDatetime
58
+ }) {
59
+ this.userId = userId;
60
+ this.spaceId = spaceId;
61
+ this.surface = surface;
62
+ this.timezone = timezone;
63
+ this.userName = userName;
64
+ this.spaceName = spaceName;
65
+ this.spaceViewId = spaceViewId;
66
+ this.currentDatetime = currentDatetime;
67
+ }
68
+ }
69
+
70
+ export class NotionTranscriptItem {
71
+ constructor({
72
+ id = generateCustomId(),
73
+ type,
74
+ value = null,
75
+
76
+ }) {
77
+ this.id = id;
78
+ this.type = type; // "markdown-chat", "agent-integration", "context"
79
+ this.value = value;
80
+ }
81
+ }
82
+
83
+ export class NotionTranscriptItemByuser {
84
+ constructor({
85
+ id = generateCustomId(),
86
+ type,
87
+ value = null,
88
+ userId,
89
+ createdAt
90
+
91
+ }) {
92
+ this.id = id;
93
+ this.type = type; // "config", "user"
94
+ this.value = value;
95
+ this.userId = userId;
96
+ this.createdAt = createdAt;
97
+ }
98
+ }
99
+
100
+ export class NotionDebugOverrides {
101
+ constructor({
102
+ cachedInferences = {},
103
+ annotationInferences = {},
104
+ emitInferences = false
105
+ }) {
106
+ this.cachedInferences = cachedInferences;
107
+ this.annotationInferences = annotationInferences;
108
+ this.emitInferences = emitInferences;
109
+ }
110
+ }
111
+
112
+ export function generateCustomId() {
113
+ // 创建固定部分
114
+ const prefix1 = '2036702a';
115
+ const prefix2 = '4d19';
116
+ const prefix5 = '00aa';
117
+
118
+ // 生成随机十六进制字符
119
+ function randomHex(length) {
120
+ return Array(length).fill(0).map(() =>
121
+ Math.floor(Math.random() * 16).toString(16)
122
+ ).join('');
123
+ }
124
+
125
+ // 组合所有部分
126
+ const part3 = '80' + randomHex(2); // 8xxx
127
+ const part4 = randomHex(4); // xxxx
128
+ const part5 = prefix5 + randomHex(8); // 00aaxxxxxxxx
129
+
130
+ return `${prefix1}-${prefix2}-${part3}-${part4}-${part5}`;
131
+ }
132
+
133
+ export class NotionRequestBody {
134
+ constructor({
135
+ traceId = randomUUID(),
136
+ spaceId,
137
+ transcript,
138
+ createThread = false,
139
+ debugOverrides = new NotionDebugOverrides({}),
140
+ generateTitle = true,
141
+ saveAllThreadOperations = true,
142
+ }) {
143
+ this.traceId = traceId;
144
+ this.spaceId = spaceId;
145
+ this.transcript = transcript;
146
+ this.createThread = createThread;
147
+ this.debugOverrides = debugOverrides;
148
+ this.generateTitle = generateTitle;
149
+ this.saveAllThreadOperations = saveAllThreadOperations;
150
+ }
151
+ }
152
+
153
+ // 输出模型 (OpenAI SSE)
154
+ export class ChoiceDelta {
155
+ constructor({
156
+ content = null
157
+ }) {
158
+ this.content = content;
159
+ }
160
+ }
161
+
162
+ export class Choice {
163
+ constructor({
164
+ index = 0,
165
+ delta,
166
+ finish_reason = null
167
+ }) {
168
+ this.index = index;
169
+ this.delta = delta;
170
+ this.finish_reason = finish_reason;
171
+ }
172
+ }
173
+
174
+ export class ChatCompletionChunk {
175
+ constructor({
176
+ id = `chatcmpl-${randomUUID()}`,
177
+ object = "chat.completion.chunk",
178
+ created = Math.floor(Date.now() / 1000),
179
+ model = "notion-proxy",
180
+ choices
181
+ }) {
182
+ this.id = id;
183
+ this.object = object;
184
+ this.created = created;
185
+ this.model = model;
186
+ this.choices = choices;
187
+ }
188
+ }
189
+
190
+ // 模型列表端点 /v1/models
191
+ export class Model {
192
+ constructor({
193
+ id,
194
+ object = "model",
195
+ created = Math.floor(Date.now() / 1000),
196
+ owned_by = "notion"
197
+ }) {
198
+ this.id = id;
199
+ this.object = object;
200
+ this.created = created;
201
+ this.owned_by = owned_by;
202
+ }
203
+ }
204
+
205
+ export class ModelList {
206
+ constructor({
207
+ object = "list",
208
+ data
209
+ }) {
210
+ this.object = object;
211
+ this.data = data;
212
+ }
213
+ }