isididiidid commited on
Commit
de9786a
·
verified ·
1 Parent(s): cfb0647

Upload 7 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,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 简化的代理池类,支持 HTTP 和 SOCKS5 代理
3
+ */
4
+ class ProxyPool {
5
+ constructor() {
6
+ // 从环境变量读取完整的代理URL
7
+ this.proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || '';
8
+ this.isInitialized = false;
9
+
10
+ // 解析代理URL
11
+ this.parseProxyUrl();
12
+ }
13
+
14
+ /**
15
+ * 解析代理URL
16
+ */
17
+ parseProxyUrl() {
18
+ if (!this.proxyUrl) {
19
+ console.log('未设置代理URL环境变量 (HTTP_PROXY)');
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const url = new URL(this.proxyUrl);
25
+ this.proxyInfo = {
26
+ protocol: url.protocol.replace(':', ''),
27
+ host: url.hostname,
28
+ port: parseInt(url.port),
29
+ username: url.username,
30
+ password: url.password,
31
+ full: this.proxyUrl
32
+ };
33
+
34
+ console.log(`代理配置解析成功: ${this.proxyInfo.protocol}://${this.proxyInfo.host}:${this.proxyInfo.port}`);
35
+ } catch (error) {
36
+ console.error(`代理URL解析失败: ${error.message}`);
37
+ this.proxyInfo = null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 初始化代理池
43
+ */
44
+ async initialize() {
45
+ if (this.isInitialized) return true;
46
+
47
+ if (!this.proxyUrl) {
48
+ console.log('未配置代理,将使用直连模式');
49
+ this.isInitialized = true;
50
+ return true;
51
+ }
52
+
53
+ if (!this.proxyInfo) {
54
+ console.error('代理配置无效');
55
+ return false;
56
+ }
57
+
58
+ // 测试代理连接
59
+ const isValid = await this.testProxyConnection();
60
+ if (isValid) {
61
+ this.isInitialized = true;
62
+ console.log('代理池初始化成功');
63
+ return true;
64
+ } else {
65
+ console.error('代理连接测试失败');
66
+ // 即使测试失败,也标记为已初始化,让实际使用时验证
67
+ this.isInitialized = true;
68
+ console.log('跳过测试失败,继续使用配置的代理');
69
+ return true;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 测试代理连接
75
+ */
76
+ async testProxyConnection() {
77
+ if (!this.proxyInfo) return false;
78
+
79
+ try {
80
+ let agent;
81
+
82
+ // 根据协议选择不同的代理 Agent
83
+ if (this.proxyUrl.startsWith('socks5://')) {
84
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
85
+ agent = new SocksProxyAgent(this.proxyUrl);
86
+ console.log('使用 SOCKS5 代理进行测试...');
87
+ } else if (this.proxyUrl.startsWith('socks4://')) {
88
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
89
+ agent = new SocksProxyAgent(this.proxyUrl);
90
+ console.log('使用 SOCKS4 代理进行测试...');
91
+ } else {
92
+ const HttpProxyAgent = (await import('http-proxy-agent')).default;
93
+ agent = new HttpProxyAgent(this.proxyUrl);
94
+ console.log('使用 HTTP 代理进行测试...');
95
+ }
96
+
97
+ const fetch = (await import('node-fetch')).default;
98
+
99
+ // 尝试多个测试URL
100
+ const testUrls = [
101
+ 'http://httpbin.org/ip',
102
+ 'https://api.ipify.org?format=json',
103
+ 'http://icanhazip.com'
104
+ ];
105
+
106
+ for (const testUrl of testUrls) {
107
+ try {
108
+ console.log(`测试代理连接到: ${testUrl}`);
109
+
110
+ const response = await fetch(testUrl, {
111
+ agent,
112
+ timeout: 15000,
113
+ headers: {
114
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
115
+ 'Accept': '*/*',
116
+ 'Connection': 'close'
117
+ }
118
+ });
119
+
120
+ if (response.ok) {
121
+ const data = await response.text();
122
+ let ip = data.trim();
123
+
124
+ // 尝试解析JSON格式的响应
125
+ try {
126
+ const jsonData = JSON.parse(data);
127
+ ip = jsonData.ip || jsonData.origin || ip;
128
+ } catch (e) {
129
+ // 如果不是JSON,使用原始文本
130
+ }
131
+
132
+ console.log(`${this.proxyInfo.protocol.toUpperCase()} 代理连接测试成功!`);
133
+ console.log(`测试URL: ${testUrl}`);
134
+ console.log(`出口IP: ${ip}`);
135
+ return true;
136
+ } else {
137
+ console.log(`测试URL ${testUrl} 失败,状态码: ${response.status}`);
138
+ }
139
+ } catch (urlError) {
140
+ console.log(`测试URL ${testUrl} 出错: ${urlError.message}`);
141
+ }
142
+ }
143
+
144
+ console.error('所有测试URL都失败了');
145
+ return false;
146
+ } catch (error) {
147
+ console.error(`代理连接测试失败: ${error.message}`);
148
+ return false;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * 获取代理(每次调用返回相同的代理)
154
+ */
155
+ getProxy() {
156
+ if (!this.proxyInfo) {
157
+ return null;
158
+ }
159
+
160
+ return {
161
+ ip: this.proxyInfo.host,
162
+ port: this.proxyInfo.port,
163
+ protocol: this.proxyInfo.protocol,
164
+ full: this.proxyInfo.full
165
+ };
166
+ }
167
+
168
+ /**
169
+ * 移除代理(记录错误)
170
+ */
171
+ removeProxy(ip, port) {
172
+ console.log(`记录代理问题: ${ip}:${port}`);
173
+ return true;
174
+ }
175
+
176
+ /**
177
+ * 获取代理数量
178
+ */
179
+ getValidCount() {
180
+ return this.proxyInfo ? 1 : 0;
181
+ }
182
+
183
+ /**
184
+ * 获取状态信息
185
+ */
186
+ getStatus() {
187
+ if (!this.proxyInfo) {
188
+ return [];
189
+ }
190
+
191
+ return [{
192
+ index: 0,
193
+ host: this.proxyInfo.host,
194
+ port: this.proxyInfo.port,
195
+ protocol: this.proxyInfo.protocol,
196
+ active: true
197
+ }];
198
+ }
199
+
200
+ /**
201
+ * 停止代理池
202
+ */
203
+ stop() {
204
+ console.log('代理池已停止');
205
+ }
206
+ }
207
+
208
+ // 导出
209
+ export default ProxyPool;
210
+ export const proxyPool = new ProxyPool();
211
+
212
+
213
+
214
+
src/ProxyServer.js ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dotenv from 'dotenv';
2
+ import chalk from 'chalk';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ dotenv.config({ path: join(dirname(__dirname), '.env') });
10
+
11
+ const logger = {
12
+ info: (message) => console.log(chalk.blue(`[ProxyServer] ${message}`)),
13
+ error: (message) => console.error(chalk.red(`[ProxyServer] ${message}`)),
14
+ warning: (message) => console.warn(chalk.yellow(`[ProxyServer] ${message}`)),
15
+ success: (message) => console.log(chalk.green(`[ProxyServer] ${message}`)),
16
+ };
17
+
18
+ /**
19
+ * 简化的代理服务器管理类
20
+ */
21
+ class ProxyServer {
22
+ constructor() {
23
+ this.enabled = process.env.ENABLE_PROXY_SERVER === 'true';
24
+ this.proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || '';
25
+ this.isConnected = false;
26
+ }
27
+
28
+ /**
29
+ * 启动代理服务器
30
+ */
31
+ async start() {
32
+ if (!this.enabled) {
33
+ logger.info('代理服务器未启用,跳过启动');
34
+ return false;
35
+ }
36
+
37
+ if (!this.proxyUrl) {
38
+ logger.warning('未设置代理URL (HTTP_PROXY),跳过代理验证');
39
+ return false;
40
+ }
41
+
42
+ logger.info('正在验证代理连接...');
43
+
44
+ try {
45
+ const isValid = await this.validateProxyConnection();
46
+
47
+ if (isValid) {
48
+ this.isConnected = true;
49
+ logger.success('代理服务连接成功');
50
+ return true;
51
+ } else {
52
+ logger.error('代理服务连接失败');
53
+ return false;
54
+ }
55
+ } catch (error) {
56
+ logger.error(`连接代理服务时出错: ${error.message}`);
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 验证代理连接
63
+ */
64
+ async validateProxyConnection() {
65
+ try {
66
+ // 修复导入方式
67
+ const HttpProxyAgent = (await import('http-proxy-agent')).default;
68
+ const fetch = (await import('node-fetch')).default;
69
+
70
+ const agent = new HttpProxyAgent(this.proxyUrl);
71
+
72
+ const response = await fetch('https://httpbin.org/ip', {
73
+ agent,
74
+ timeout: 10000
75
+ });
76
+
77
+ if (response.ok) {
78
+ const data = await response.json();
79
+ logger.info(`代理连接测试成功,出口IP: ${data.origin}`);
80
+ return true;
81
+ } else {
82
+ logger.error(`代理连接测试失败,状态码: ${response.status}`);
83
+ return false;
84
+ }
85
+ } catch (error) {
86
+ logger.error(`代理连接验证失败: ${error.message}`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 停止代理服务器
93
+ */
94
+ stop() {
95
+ this.isConnected = false;
96
+ logger.success('代理服务已断开');
97
+ }
98
+
99
+ /**
100
+ * 获取代理状态
101
+ */
102
+ getStatus() {
103
+ return {
104
+ enabled: this.enabled,
105
+ connected: this.isConnected,
106
+ proxyConfigured: !!this.proxyUrl
107
+ };
108
+ }
109
+ }
110
+
111
+ const proxyServer = new ProxyServer();
112
+
113
+ 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/lightweight-client.js ADDED
@@ -0,0 +1,843 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fetch from 'node-fetch';
2
+ import { JSDOM } from 'jsdom';
3
+ import dotenv from 'dotenv';
4
+ import { randomUUID } from 'crypto';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ import { PassThrough } from 'stream';
8
+ import chalk from 'chalk';
9
+ import fs from 'fs';
10
+ import {
11
+ NotionTranscriptConfigValue,
12
+ NotionTranscriptContextValue, NotionTranscriptItem, NotionDebugOverrides,
13
+ NotionRequestBody, ChoiceDelta, Choice, ChatCompletionChunk, NotionTranscriptItemByuser
14
+ } from './models.js';
15
+ import { proxyPool } from './ProxyPool.js';
16
+ import { proxyServer } from './ProxyServer.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
+ };
33
+
34
+ // 配置
35
+ const NOTION_API_URL = "https://www.notion.so/api/v3/runInferenceTranscript";
36
+ let currentCookieData = null;
37
+ const USE_NATIVE_PROXY_POOL = process.env.USE_NATIVE_PROXY_POOL === 'true';
38
+ const ENABLE_PROXY_SERVER = process.env.ENABLE_PROXY_SERVER === 'true';
39
+ let proxy = null;
40
+
41
+ // 代理配置
42
+ const PROXY_URL = process.env.PROXY_URL || "";
43
+
44
+ // 标记是否成功初始化
45
+ let INITIALIZED_SUCCESSFULLY = false;
46
+
47
+ // 安全的流写入函数
48
+ function safeWriteToStream(stream, data) {
49
+ if (stream && !stream.destroyed && !stream.writableEnded && stream.writable) {
50
+ try {
51
+ return stream.write(data);
52
+ } catch (error) {
53
+ logger.error(`写入流时出错: ${error.message}`);
54
+ return false;
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+
60
+ // 安全的流结束函数
61
+ function safeEndStream(stream) {
62
+ if (stream && !stream.destroyed && !stream.writableEnded) {
63
+ try {
64
+ stream.end();
65
+ } catch (error) {
66
+ logger.error(`结束流时出错: ${error.message}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ // 注册进程退出事件,确保代理服务器在程序退出时关闭
72
+ process.on('exit', () => {
73
+ try {
74
+ if (proxyServer) {
75
+ proxyServer.stop();
76
+ }
77
+ } catch (error) {
78
+ logger.error(`程序退出时关闭代理服务器出错: ${error.message}`);
79
+ }
80
+ });
81
+
82
+ // 捕获意外退出信号
83
+ ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
84
+ process.on(signal, () => {
85
+ logger.info(`收到${signal}信号,正在关闭代理服务器...`);
86
+ try {
87
+ if (proxyServer) {
88
+ proxyServer.stop();
89
+ }
90
+ } catch (error) {
91
+ logger.error(`关闭代理服务器出错: ${error.message}`);
92
+ }
93
+ process.exit(0);
94
+ });
95
+ });
96
+
97
+ // 构建Notion请求
98
+ function buildNotionRequest(requestData) {
99
+ // 确保我们有当前的cookie数据
100
+ if (!currentCookieData) {
101
+ currentCookieData = cookieManager.getNext();
102
+ if (!currentCookieData) {
103
+ throw new Error('没有可用的cookie');
104
+ }
105
+ }
106
+
107
+ // 当前时间
108
+ const now = new Date();
109
+ // 格式化为ISO字符串,确保包含毫秒和时区
110
+ const isoString = now.toISOString();
111
+
112
+ // 生成随机名称,类似于Python版本
113
+ const randomWords = ["Project", "Workspace", "Team", "Studio", "Lab", "Hub", "Zone", "Space"];
114
+ const userName = `User${Math.floor(Math.random() * 900) + 100}`; // 生成100-999之间的随机数
115
+ const spaceName = `${randomWords[Math.floor(Math.random() * randomWords.length)]} ${Math.floor(Math.random() * 99) + 1}`;
116
+
117
+ // 创建transcript数组
118
+ const transcript = [];
119
+
120
+ // 添加配置项
121
+ if(requestData.model === 'anthropic-sonnet-3.x-stable'){
122
+ transcript.push(new NotionTranscriptItem({
123
+ type: "config",
124
+ value: new NotionTranscriptConfigValue({
125
+ })
126
+ }));
127
+ }else{
128
+ transcript.push(new NotionTranscriptItem({
129
+ type: "config",
130
+ value: new NotionTranscriptConfigValue({
131
+ model: requestData.model
132
+ })
133
+ }));
134
+ }
135
+
136
+
137
+ // 添加上下文项
138
+ transcript.push(new NotionTranscriptItem({
139
+ type: "context",
140
+ value: new NotionTranscriptContextValue({
141
+ userId: currentCookieData.userId,
142
+ spaceId: currentCookieData.spaceId,
143
+ surface: "home_module",
144
+ timezone: "America/Los_Angeles",
145
+ userName: userName,
146
+ spaceName: spaceName,
147
+ spaceViewId: randomUUID(),
148
+ currentDatetime: isoString
149
+ })
150
+ }));
151
+
152
+ // 添加agent-integration项
153
+ transcript.push(new NotionTranscriptItem({
154
+ type: "agent-integration"
155
+ }));
156
+
157
+ // 添加消息
158
+ for (const message of requestData.messages) {
159
+ // 处理消息内容,确保格式一致
160
+ let content = message.content;
161
+
162
+ // 处理内容为数组的情况
163
+ if (Array.isArray(content)) {
164
+ let textContent = "";
165
+ for (const part of content) {
166
+ if (part && typeof part === 'object' && part.type === 'text') {
167
+ if (typeof part.text === 'string') {
168
+ textContent += part.text;
169
+ }
170
+ }
171
+ }
172
+ content = textContent || ""; // 使用提取的文本或空字符串
173
+ } else if (typeof content !== 'string') {
174
+ content = ""; // 如果不是字符串或数组,则默认为空字符串
175
+ }
176
+
177
+ if (message.role === "system") {
178
+ // 系统消息作为用户消息添加
179
+ transcript.push(new NotionTranscriptItemByuser({
180
+ type: "user",
181
+ value: [[content]],
182
+ userId: currentCookieData.userId,
183
+ createdAt: message.createdAt || isoString
184
+ }));
185
+ } else if (message.role === "user") {
186
+ // 用户消息
187
+ transcript.push(new NotionTranscriptItemByuser({
188
+ type: "user",
189
+ value: [[content]],
190
+ userId: currentCookieData.userId,
191
+ createdAt: message.createdAt || isoString
192
+ }));
193
+ } else if (message.role === "assistant") {
194
+ // 助手消息
195
+ transcript.push(new NotionTranscriptItem({
196
+ type: "markdown-chat",
197
+ value: content,
198
+ traceId: message.traceId || randomUUID(),
199
+ createdAt: message.createdAt || isoString
200
+ }));
201
+ }
202
+ }
203
+
204
+ // 创建请求体
205
+ const requestBody = new NotionRequestBody({
206
+ spaceId: currentCookieData.spaceId,
207
+ transcript: transcript,
208
+ createThread: true,
209
+ traceId: randomUUID(),
210
+ debugOverrides: new NotionDebugOverrides({
211
+ cachedInferences: {},
212
+ annotationInferences: {},
213
+ emitInferences: false
214
+ }),
215
+ generateTitle: false,
216
+ saveAllThreadOperations: false
217
+ });
218
+
219
+ // 添加调试日志(仅在开发环境)
220
+ if (process.env.NODE_ENV !== 'production') {
221
+ logger.info(`构建的请求体transcript长度: ${transcript.length}`);
222
+ }
223
+
224
+ return requestBody;
225
+ }
226
+
227
+ // 流式处理Notion响应
228
+ async function streamNotionResponse(notionRequestBody) {
229
+ // 确保我们有当前的cookie数据
230
+ if (!currentCookieData) {
231
+ currentCookieData = cookieManager.getNext();
232
+ if (!currentCookieData) {
233
+ throw new Error('没有可用的cookie');
234
+ }
235
+ }
236
+
237
+ // 创建流
238
+ const stream = new PassThrough();
239
+
240
+ // 添加初始数据,确保连接建立
241
+ safeWriteToStream(stream, ':\n\n'); // 发送一个空注释行,保持连接活跃
242
+
243
+ // 设置HTTP头模板
244
+ const headers = {
245
+ 'Content-Type': 'application/json',
246
+ 'accept': 'application/x-ndjson',
247
+ 'accept-language': 'en-US,en;q=0.9',
248
+ 'notion-audit-log-platform': 'web',
249
+ 'notion-client-version': '23.13.0.3686',
250
+ 'origin': 'https://www.notion.so',
251
+ 'referer': 'https://www.notion.so/chat',
252
+ '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',
253
+ 'x-notion-active-user-header': currentCookieData.userId,
254
+ 'x-notion-space-id': currentCookieData.spaceId
255
+ };
256
+
257
+ // 设置超时处理,确保流不会无限等待
258
+ const timeoutId = setTimeout(() => {
259
+ logger.warning(`请求超时,30秒内未收到响应`);
260
+ try {
261
+ // 发送结束消息
262
+ const endChunk = new ChatCompletionChunk({
263
+ choices: [
264
+ new Choice({
265
+ delta: new ChoiceDelta({ content: "请求超时,未收到Notion响应。" }),
266
+ finish_reason: "timeout"
267
+ })
268
+ ]
269
+ });
270
+ safeWriteToStream(stream, `data: ${JSON.stringify(endChunk)}\n\n`);
271
+ safeWriteToStream(stream, 'data: [DONE]\n\n');
272
+ safeEndStream(stream);
273
+ } catch (error) {
274
+ logger.error(`发送超时消息时出错: ${error}`);
275
+ safeEndStream(stream);
276
+ }
277
+ }, 30000); // 30秒超时
278
+
279
+ // 启动fetch处理
280
+ fetchNotionResponseWithRetry(
281
+ stream,
282
+ notionRequestBody,
283
+ headers,
284
+ NOTION_API_URL,
285
+ currentCookieData.cookie,
286
+ timeoutId
287
+ ).catch((error) => {
288
+ logger.error(`流处理出错: ${error}`);
289
+ clearTimeout(timeoutId); // 清除超时计时器
290
+
291
+ try {
292
+ // 发送错误消息
293
+ const errorChunk = new ChatCompletionChunk({
294
+ choices: [
295
+ new Choice({
296
+ delta: new ChoiceDelta({ content: `处理请求时出错: ${error.message}` }),
297
+ finish_reason: "error"
298
+ })
299
+ ]
300
+ });
301
+ safeWriteToStream(stream, `data: ${JSON.stringify(errorChunk)}\n\n`);
302
+ safeWriteToStream(stream, 'data: [DONE]\n\n');
303
+ } catch (e) {
304
+ logger.error(`发送错误消息时出错: ${e}`);
305
+ } finally {
306
+ safeEndStream(stream);
307
+ }
308
+ });
309
+
310
+ return stream;
311
+ }
312
+
313
+ // 带重试的fetch函数
314
+ async function fetchNotionResponseWithRetry(chunkQueue, notionRequestBody, headers, notionApiUrl, notionCookie, timeoutId, retryCount = 0) {
315
+ const maxRetries = 2;
316
+
317
+ try {
318
+ return await fetchNotionResponse(chunkQueue, notionRequestBody, headers, notionApiUrl, notionCookie, timeoutId, retryCount);
319
+ } catch (error) {
320
+ if (retryCount < maxRetries && (error.message.includes('400') || error.message.includes('timeout') || error.message.includes('EPROTO') || error.message.includes('SSL'))) {
321
+ logger.warning(`请求失败,正在重试 (${retryCount + 1}/${maxRetries}): ${error.message}`);
322
+ await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1))); // 递增等待时间
323
+ return fetchNotionResponseWithRetry(chunkQueue, notionRequestBody, headers, notionApiUrl, notionCookie, timeoutId, retryCount + 1);
324
+ }
325
+ throw error;
326
+ }
327
+ }
328
+
329
+ // 使用fetch调用Notion API并处理流式响应
330
+ async function fetchNotionResponse(chunkQueue, notionRequestBody, headers, notionApiUrl, notionCookie, timeoutId, retryCount = 0) {
331
+ let responseReceived = false;
332
+ let dom = null;
333
+
334
+ try {
335
+ // 创建JSDOM实例模拟浏览器环境
336
+ dom = new JSDOM("", {
337
+ url: "https://www.notion.so",
338
+ referrer: "https://www.notion.so/chat",
339
+ contentType: "text/html",
340
+ includeNodeLocations: true,
341
+ storageQuota: 10000000,
342
+ pretendToBeVisual: true,
343
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
344
+ });
345
+
346
+ // 设置全局对象
347
+ const { window } = dom;
348
+
349
+ // 使用更安全的方式设置全局对象
350
+ try {
351
+ if (!global.window) {
352
+ global.window = window;
353
+ }
354
+
355
+ if (!global.document) {
356
+ global.document = window.document;
357
+ }
358
+
359
+ // 安全地设置navigator
360
+ if (!global.navigator) {
361
+ try {
362
+ Object.defineProperty(global, 'navigator', {
363
+ value: window.navigator,
364
+ writable: true,
365
+ configurable: true
366
+ });
367
+ } catch (navError) {
368
+ logger.warning(`无法设置navigator: ${navError.message},继续执行`);
369
+ // 继续执行,不会中断流程
370
+ }
371
+ }
372
+ } catch (globalError) {
373
+ logger.warning(`设置全局对象时出错: ${globalError.message}`);
374
+ }
375
+
376
+ // 设置cookie
377
+ document.cookie = notionCookie;
378
+
379
+ // 创建fetch选项
380
+ const fetchOptions = {
381
+ method: 'POST',
382
+ headers: {
383
+ ...headers,
384
+ 'user-agent': window.navigator.userAgent,
385
+ 'Cookie': notionCookie
386
+ },
387
+ body: JSON.stringify(notionRequestBody),
388
+ };
389
+
390
+ // 支持多种代理协议的配置处理
391
+ if (USE_NATIVE_PROXY_POOL) {
392
+ proxy = proxyPool.getProxy();
393
+ if (proxy !== null) {
394
+ try {
395
+ let agent;
396
+
397
+ // 根据代理协议选择合适的 Agent
398
+ if (proxy.full.startsWith('socks5://')) {
399
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
400
+ agent = new SocksProxyAgent(proxy.full);
401
+ logger.info(`使用 SOCKS5 代理: ${proxy.ip}:${proxy.port}`);
402
+ } else if (proxy.full.startsWith('socks4://')) {
403
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
404
+ agent = new SocksProxyAgent(proxy.full);
405
+ logger.info(`使用 SOCKS4 代理: ${proxy.ip}:${proxy.port}`);
406
+ } else {
407
+ const HttpProxyAgent = (await import('http-proxy-agent')).default;
408
+ agent = new HttpProxyAgent(proxy.full);
409
+ logger.info(`使用 HTTP 代理: ${proxy.ip}:${proxy.port}`);
410
+ }
411
+
412
+ fetchOptions.agent = agent;
413
+ } catch (proxyError) {
414
+ logger.error(`代理配置失败: ${proxyError.message}`);
415
+ logger.warning(`跳过代理,使用直连`);
416
+ delete fetchOptions.agent;
417
+
418
+ // 标记这个代理为无效
419
+ if (proxy && proxy.ip && proxy.port) {
420
+ proxyPool.removeProxy(proxy.ip, proxy.port);
421
+ }
422
+ }
423
+ } else {
424
+ logger.warning(`没有可用代理,使用直连`);
425
+ }
426
+ } else if (PROXY_URL) {
427
+ // 如果有环境变量配置的代理
428
+ try {
429
+ let agent;
430
+
431
+ if (PROXY_URL.startsWith('socks5://')) {
432
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
433
+ agent = new SocksProxyAgent(PROXY_URL);
434
+ logger.info(`使用环境变量 SOCKS5 代理: ${PROXY_URL}`);
435
+ } else if (PROXY_URL.startsWith('socks4://')) {
436
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
437
+ agent = new SocksProxyAgent(PROXY_URL);
438
+ logger.info(`使用环境变量 SOCKS4 代理: ${PROXY_URL}`);
439
+ } else {
440
+ const HttpProxyAgent = (await import('http-proxy-agent')).default;
441
+ agent = new HttpProxyAgent(PROXY_URL);
442
+ logger.info(`使用环境变量 HTTP 代理: ${PROXY_URL}`);
443
+ }
444
+
445
+ fetchOptions.agent = agent;
446
+ } catch (envProxyError) {
447
+ logger.error(`环境变量代理配置失败: ${envProxyError.message}`);
448
+ }
449
+ }
450
+
451
+ let response = null;
452
+ // 发送请求
453
+ if (ENABLE_PROXY_SERVER){
454
+ response = await fetch('http://127.0.0.1:10655/proxy', {
455
+ method: 'POST',
456
+ body: JSON.stringify({
457
+ method: 'POST',
458
+ url: notionApiUrl,
459
+ headers: fetchOptions.headers,
460
+ body: fetchOptions.body,
461
+ stream:true
462
+ }),
463
+ });
464
+ }else{
465
+ response = await fetch(notionApiUrl, fetchOptions);
466
+ }
467
+
468
+ // 检查是否收到401错误(未授权)
469
+ if (response.status === 401) {
470
+ logger.error(`收到401未授权错误,cookie可能已失效`);
471
+ // 标记当前cookie为无效
472
+ cookieManager.markAsInvalid(currentCookieData.userId);
473
+ // 尝试获取下一个cookie
474
+ currentCookieData = cookieManager.getNext();
475
+
476
+ if (!currentCookieData) {
477
+ throw new Error('所有cookie均已失效,无法继续请求');
478
+ }
479
+
480
+ // 使用新cookie重新构建请求体
481
+ const newRequestBody = buildNotionRequest({
482
+ model: notionRequestBody.transcript[0]?.value?.model || '',
483
+ messages: [] // 这里应该根据实际情况重构消息
484
+ });
485
+
486
+ // 使用新cookie重试请求
487
+ return fetchNotionResponse(
488
+ chunkQueue,
489
+ newRequestBody,
490
+ {
491
+ ...headers,
492
+ 'x-notion-active-user-header': currentCookieData.userId,
493
+ 'x-notion-space-id': currentCookieData.spaceId
494
+ },
495
+ notionApiUrl,
496
+ currentCookieData.cookie,
497
+ timeoutId,
498
+ retryCount
499
+ );
500
+ }
501
+
502
+ if (!response.ok) {
503
+ let errorDetails = '';
504
+ try {
505
+ const errorText = await response.text();
506
+ errorDetails = errorText;
507
+ logger.error(`Notion API 错误详情: ${response.status} - ${errorText}`);
508
+ } catch (e) {
509
+ logger.error(`无法读取错误响应: ${e.message}`);
510
+ }
511
+
512
+ // 添加请求调试信息
513
+ logger.error(`请求URL: ${notionApiUrl}`);
514
+ logger.error(`请求头: ${JSON.stringify(fetchOptions.headers, null, 2)}`);
515
+ logger.error(`请求体长度: ${fetchOptions.body.length}`);
516
+
517
+ throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
518
+ }
519
+
520
+ // 处理流式响应
521
+ if (!response.body) {
522
+ throw new Error("Response body is null");
523
+ }
524
+
525
+ // 创建流读取器
526
+ const reader = response.body;
527
+ let buffer = '';
528
+
529
+ // 处理数据块
530
+ reader.on('data', (chunk) => {
531
+ try {
532
+ // 标记已收到响应
533
+ if (!responseReceived) {
534
+ responseReceived = true;
535
+ logger.info(`已连接Notion API`);
536
+ clearTimeout(timeoutId); // 清除超时计时器
537
+ }
538
+
539
+ // 解码数据
540
+ const text = chunk.toString('utf8');
541
+ buffer += text;
542
+
543
+ // 按行分割并处理完整的JSON对象
544
+ const lines = buffer.split('\n');
545
+ buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
546
+
547
+ for (const line of lines) {
548
+ if (!line.trim()) continue;
549
+
550
+ try {
551
+ const jsonData = JSON.parse(line);
552
+
553
+ // 提取内容
554
+ if (jsonData?.type === "markdown-chat" && typeof jsonData?.value === "string") {
555
+ const content = jsonData.value;
556
+ if (!content) continue;
557
+
558
+ // 创建OpenAI格式的块
559
+ const chunk = new ChatCompletionChunk({
560
+ choices: [
561
+ new Choice({
562
+ delta: new ChoiceDelta({ content }),
563
+ finish_reason: null
564
+ })
565
+ ]
566
+ });
567
+
568
+ // 添加到队列
569
+ const dataStr = `data: ${JSON.stringify(chunk)}\n\n`;
570
+ safeWriteToStream(chunkQueue, dataStr);
571
+ } else if (jsonData?.recordMap) {
572
+ // 忽略recordMap响应
573
+ } else {
574
+ // 忽略其他类型响应
575
+ }
576
+ } catch (jsonError) {
577
+ logger.error(`解析JSON出错: ${jsonError}`);
578
+ }
579
+ }
580
+ } catch (error) {
581
+ logger.error(`处理数据块出错: ${error}`);
582
+ }
583
+ });
584
+
585
+ // 处理流结束
586
+ reader.on('end', () => {
587
+ try {
588
+ logger.info(`响应完成`);
589
+ if (cookieManager.getValidCount() > 1){
590
+ // 尝试切换到下一个cookie
591
+ currentCookieData = cookieManager.getNext();
592
+ logger.info(`切换到下一个cookie: ${currentCookieData.userId}`);
593
+ }
594
+
595
+ // 如果没有收到任何响应,发送一个提示消息
596
+ if (!responseReceived) {
597
+ logger.warning(`未从Notion收到内容响应,请更换ip重试`);
598
+ if (USE_NATIVE_PROXY_POOL && proxy) {
599
+ proxyPool.removeProxy(proxy.ip, proxy.port);
600
+ }
601
+
602
+ const noContentChunk = new ChatCompletionChunk({
603
+ choices: [
604
+ new Choice({
605
+ delta: new ChoiceDelta({ content: "未从Notion收到内容响应,请更换ip重试。" }),
606
+ finish_reason: "no_content"
607
+ })
608
+ ]
609
+ });
610
+ safeWriteToStream(chunkQueue, `data: ${JSON.stringify(noContentChunk)}\n\n`);
611
+ }
612
+
613
+ // 创建结束块
614
+ const endChunk = new ChatCompletionChunk({
615
+ choices: [
616
+ new Choice({
617
+ delta: new ChoiceDelta({ content: null }),
618
+ finish_reason: "stop"
619
+ })
620
+ ]
621
+ });
622
+
623
+ // 添加到队列
624
+ safeWriteToStream(chunkQueue, `data: ${JSON.stringify(endChunk)}\n\n`);
625
+ safeWriteToStream(chunkQueue, 'data: [DONE]\n\n');
626
+
627
+ // 清除超时计时器(如果尚未清除)
628
+ if (timeoutId) clearTimeout(timeoutId);
629
+
630
+ // 清理全局对象
631
+ cleanupGlobalObjects();
632
+
633
+ // 结束流
634
+ safeEndStream(chunkQueue);
635
+ } catch (error) {
636
+ logger.error(`Error in stream end handler: ${error}`);
637
+ if (timeoutId) clearTimeout(timeoutId);
638
+
639
+ // 清理全局对象
640
+ cleanupGlobalObjects();
641
+
642
+ safeEndStream(chunkQueue);
643
+ }
644
+ });
645
+
646
+ // 处理错误
647
+ reader.on('error', (error) => {
648
+ logger.error(`Stream error: ${error}`);
649
+ if (timeoutId) clearTimeout(timeoutId);
650
+
651
+ // 清理全局对象
652
+ cleanupGlobalObjects();
653
+
654
+ try {
655
+ const errorChunk = new ChatCompletionChunk({
656
+ choices: [
657
+ new Choice({
658
+ delta: new ChoiceDelta({ content: `流读取错误: ${error.message}` }),
659
+ finish_reason: "error"
660
+ })
661
+ ]
662
+ });
663
+ safeWriteToStream(chunkQueue, `data: ${JSON.stringify(errorChunk)}\n\n`);
664
+ safeWriteToStream(chunkQueue, 'data: [DONE]\n\n');
665
+ } catch (e) {
666
+ logger.error(`Error sending error message: ${e}`);
667
+ } finally {
668
+ safeEndStream(chunkQueue);
669
+ }
670
+ });
671
+ } catch (error) {
672
+ logger.error(`Notion API请求失败: ${error}`);
673
+
674
+ // 如果是代理相关的错误,尝试移除当前代理并重试
675
+ if (USE_NATIVE_PROXY_POOL && proxy &&
676
+ (error.message.includes('EPROTO') ||
677
+ error.message.includes('ECONNREFUSED') ||
678
+ error.message.includes('timeout') ||
679
+ error.message.includes('SSL') ||
680
+ error.message.includes('wrong version number'))) {
681
+
682
+ logger.warning(`代理 ${proxy.ip}:${proxy.port} 出现错误,移除此代理`);
683
+ proxyPool.removeProxy(proxy.ip, proxy.port);
684
+
685
+ // 如果还有其他代理可用,可以考虑重试
686
+ const nextProxy = proxyPool.getProxy();
687
+ if (nextProxy && retryCount < 1) { // 限制重试次数
688
+ logger.info(`尝试使用下一个代理: ${nextProxy.ip}:${nextProxy.port}`);
689
+ // 重新抛出错误让上层重试机制处理
690
+ throw new Error(`代理错误,需要重试: ${error.message}`);
691
+ }
692
+ }
693
+
694
+ // 清理全局对象
695
+ cleanupGlobalObjects();
696
+
697
+ if (timeoutId) clearTimeout(timeoutId);
698
+
699
+ // 确保在错误情况下也触发流结束
700
+ try {
701
+ if (!responseReceived) {
702
+ const errorChunk = new ChatCompletionChunk({
703
+ choices: [
704
+ new Choice({
705
+ delta: new ChoiceDelta({ content: `Notion API请求失败: ${error.message}` }),
706
+ finish_reason: "error"
707
+ })
708
+ ]
709
+ });
710
+ safeWriteToStream(chunkQueue, `data: ${JSON.stringify(errorChunk)}\n\n`);
711
+ safeWriteToStream(chunkQueue, 'data: [DONE]\n\n');
712
+ }
713
+ } catch (e) {
714
+ logger.error(`发送错误消息时出错: ${e}`);
715
+ } finally {
716
+ safeEndStream(chunkQueue);
717
+ }
718
+
719
+ throw error; // 重新抛出错误以便上层捕获
720
+ }
721
+ }
722
+
723
+ // 清理全局对象的函数
724
+ function cleanupGlobalObjects() {
725
+ try {
726
+ if (global.window) delete global.window;
727
+ if (global.document) delete global.document;
728
+
729
+ // 安全地删除navigator
730
+ if (global.navigator) {
731
+ try {
732
+ delete global.navigator;
733
+ } catch (navError) {
734
+ // 如果无法删除,尝试将其设置为undefined
735
+ try {
736
+ Object.defineProperty(global, 'navigator', {
737
+ value: undefined,
738
+ writable: true,
739
+ configurable: true
740
+ });
741
+ } catch (defineError) {
742
+ logger.warning(`无法清理navigator: ${defineError.message}`);
743
+ }
744
+ }
745
+ }
746
+ } catch (cleanupError) {
747
+ logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
748
+ }
749
+ }
750
+
751
+ // ====================================================================
752
+ // 【核心修改】应用初始化函数,以适应Hugging Face Secrets
753
+ // ====================================================================
754
+ async function initialize() {
755
+ logger.info(`初始化Notion配置...`);
756
+
757
+ // 如果启用了完整的代理功能,则启动代理服务器
758
+ if (ENABLE_PROXY_SERVER) {
759
+ try {
760
+ await proxyServer.start();
761
+ } catch (error) {
762
+ logger.error(`启动代理服务器失败: ${error.message}`);
763
+ }
764
+ }
765
+
766
+ let initResult = false;
767
+ const cookieFileContent = process.env.COOKIE_FILE_CONTENT;
768
+ const cookieFilePath = process.env.COOKIE_FILE_PATH || 'cookies.txt';
769
+
770
+ // 1. 优先从环境变量 COOKIE_FILE_CONTENT 加载 (Hugging Face部署模式)
771
+ if (cookieFileContent) {
772
+ logger.info(`检测到 COOKIE_FILE_CONTENT,正在从环境变量内容加载...`);
773
+ try {
774
+ // 将Secret内容写入容器内的一个临时文件
775
+ fs.writeFileSync(cookieFilePath, cookieFileContent, 'utf8');
776
+ // 让CookieManager从这个刚创建的文件加载
777
+ initResult = await cookieManager.loadFromFile(cookieFilePath);
778
+ } catch (e) {
779
+ logger.error(`从Secret创建或加载cookie失败: ${e.message}`);
780
+ initResult = false;
781
+ }
782
+ }
783
+ // 2. 如果没有,则回退到从环境变量 COOKIE_FILE 指定的路径加载 (本地开发模式1)
784
+ else if (process.env.COOKIE_FILE) {
785
+ logger.info(`检测到COOKIE_FILE配置: ${process.env.COOKIE_FILE}`);
786
+ initResult = await cookieManager.loadFromFile(process.env.COOKIE_FILE);
787
+ }
788
+
789
+ // 3. 如果以上都失败或未配置,则回退到从 NOTION_COOKIE 字符串加载 (本地开发模式2)
790
+ if (!initResult) {
791
+ const cookiesString = process.env.NOTION_COOKIE;
792
+ if (!cookiesString) {
793
+ logger.error(`错误: 未在Secrets或环境变量中设置 COOKIE_FILE_CONTENT, COOKIE_FILE 或 NOTION_COOKIE。应用无法启动。`);
794
+ INITIALIZED_SUCCESSFULLY = false;
795
+ return;
796
+ }
797
+ logger.info(`正在从环境变量 NOTION_COOKIE 初始化...`);
798
+ initResult = await cookieManager.initialize(cookiesString);
799
+ }
800
+
801
+ // 检查最终初始化结果
802
+ if (!initResult) {
803
+ logger.error(`初始化cookie管理器失败,应用无法正常工作。`);
804
+ INITIALIZED_SUCCESSFULLY = false;
805
+ return;
806
+ }
807
+
808
+ // 获取第一个可用的cookie数据用于日志记录
809
+ currentCookieData = cookieManager.getNext();
810
+ if (!currentCookieData) {
811
+ logger.error(`没有可用的有效cookie,应用无法正常工作。`);
812
+ INITIALIZED_SUCCESSFULLY = false;
813
+ return;
814
+ }
815
+
816
+ logger.success(`成功初始化cookie管理器,共有 ${cookieManager.getValidCount()} 个有效cookie`);
817
+ logger.info(`当前使用的cookie对应的用户ID: ${currentCookieData.userId}`);
818
+ logger.info(`当前使用的cookie对应的空间ID: ${currentCookieData.spaceId}`);
819
+
820
+ // 如果启用了代理池,则初始化代理池
821
+ if (USE_NATIVE_PROXY_POOL) {
822
+ logger.info(`正在初始化代理池...`);
823
+ try {
824
+ await proxyPool.initialize();
825
+ logger.success(`代理池初始化完成,可用代理数量: ${proxyPool.getValidCount ? proxyPool.getValidCount() : '未知'}`);
826
+ } catch (proxyError) {
827
+ logger.error(`代理池初始化失败: ${proxyError.message}`);
828
+ logger.warning(`将在没有代理池的情况下继续运行`);
829
+ }
830
+ }
831
+
832
+ INITIALIZED_SUCCESSFULLY = true;
833
+ }
834
+
835
+ // 导出函数
836
+ export {
837
+ initialize,
838
+ streamNotionResponse,
839
+ buildNotionRequest,
840
+ INITIALIZED_SUCCESSFULLY
841
+ };
842
+
843
+
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
+ }