notionnodejs / src /lightweight-client.js
clash-linux's picture
Upload 16 files
720114a verified
import fetch from 'node-fetch';
import { JSDOM } from 'jsdom';
import dotenv from 'dotenv';
import { randomUUID } from 'crypto';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { PassThrough } from 'stream';
import chalk from 'chalk';
import {
NotionTranscriptConfigValue,
NotionTranscriptContextValue, NotionTranscriptItem, NotionDebugOverrides,
NotionRequestBody, ChoiceDelta, Choice, ChatCompletionChunk, NotionTranscriptItemByuser
} from './models.js';
import { proxyPool } from './ProxyPool.js';
import { proxyServer } from './ProxyServer.js';
import { cookieManager } from './CookieManager.js';
// 获取当前文件的目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 加载环境变量
dotenv.config({ path: join(dirname(__dirname), '.env') });
// 日志配置
const logger = {
info: (message) => console.log(chalk.blue(`[info] ${message}`)),
error: (message) => console.error(chalk.red(`[error] ${message}`)),
warning: (message) => console.warn(chalk.yellow(`[warn] ${message}`)),
success: (message) => console.log(chalk.green(`[success] ${message}`)),
};
// 配置
const NOTION_API_URL = "https://www.notion.so/api/v3/runInferenceTranscript";
// 这些变量将由cookieManager动态提供
let currentCookieData = null;
const USE_NATIVE_PROXY_POOL = process.env.USE_NATIVE_PROXY_POOL === 'true';
const ENABLE_PROXY_SERVER = process.env.ENABLE_PROXY_SERVER === 'true';
let proxy = null;
// 代理配置
const PROXY_URL = process.env.PROXY_URL || "";
// 标记是否成功初始化
let INITIALIZED_SUCCESSFULLY = false;
// 注册进程退出事件,确保代理服务器在程序退出时关闭
process.on('exit', () => {
try {
if (proxyServer) {
proxyServer.stop();
}
} catch (error) {
logger.error(`程序退出时关闭代理服务器出错: ${error.message}`);
}
});
// 捕获意外退出信号
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
process.on(signal, () => {
logger.info(`收到${signal}信号,正在关闭代理服务器...`);
try {
if (proxyServer) {
proxyServer.stop();
}
} catch (error) {
logger.error(`关闭代理服务器出错: ${error.message}`);
}
process.exit(0);
});
});
// 构建Notion请求
function buildNotionRequest(requestData) {
// 确保我们有当前的cookie数据
if (!currentCookieData) {
currentCookieData = cookieManager.getNext();
if (!currentCookieData) {
throw new Error('没有可用的cookie');
}
}
// 当前时间
const now = new Date();
// 格式化为ISO字符串,确保包含毫秒和时区
const isoString = now.toISOString();
// 生成随机名称,类似于Python版本
const randomWords = ["Project", "Workspace", "Team", "Studio", "Lab", "Hub", "Zone", "Space"];
const userName = `User${Math.floor(Math.random() * 900) + 100}`; // 生成100-999之间的随机数
const spaceName = `${randomWords[Math.floor(Math.random() * randomWords.length)]} ${Math.floor(Math.random() * 99) + 1}`;
// 创建transcript数组
const transcript = [];
// 添加配置项
if(requestData.model === 'anthropic-sonnet-3.x-stable'){
transcript.push(new NotionTranscriptItem({
type: "config",
value: new NotionTranscriptConfigValue({
})
}));
} else if(requestData.model === 'google-gemini-2.5-pro'){
transcript.push(new NotionTranscriptItem({
type: "config",
value: new NotionTranscriptConfigValue({
model: 'vertex-gemini-2.5-pro'
})
}));
} else if (requestData.model === 'google-gemini-2.5-flash'){
transcript.push(new NotionTranscriptItem({
type: "config",
value: new NotionTranscriptConfigValue({
model: 'vertex-gemini-2.5-flash'
})
}));
}
else{
transcript.push(new NotionTranscriptItem({
type: "config",
value: new NotionTranscriptConfigValue({
model: requestData.model
})
}));
}
// 添加上下文项
transcript.push(new NotionTranscriptItem({
type: "context",
value: new NotionTranscriptContextValue({
userId: currentCookieData.userId,
spaceId: currentCookieData.spaceId,
surface: "home_module",
timezone: "America/Los_Angeles",
userName: userName,
spaceName: spaceName,
spaceViewId: randomUUID(),
currentDatetime: isoString
})
}));
// 添加agent-integration项
transcript.push(new NotionTranscriptItem({
type: "agent-integration"
}));
// 添加消息
for (const message of requestData.messages) {
// 处理消息内容,确保格式一致
let content = message.content;
// 处理内容为数组的情况
if (Array.isArray(content)) {
let textContent = "";
for (const part of content) {
if (part && typeof part === 'object' && part.type === 'text') {
if (typeof part.text === 'string') {
textContent += part.text;
}
}
}
content = textContent || ""; // 使用提取的文本或空字符串
} else if (typeof content !== 'string') {
content = ""; // 如果不是字符串或数组,则默认为空字符串
}
if (message.role === "system") {
// 系统消息作为用户消息添加
transcript.push(new NotionTranscriptItemByuser({
type: "user",
value: [[content]],
userId: currentCookieData.userId,
createdAt: message.createdAt || isoString
}));
} else if (message.role === "user") {
// 用户消息
transcript.push(new NotionTranscriptItemByuser({
type: "user",
value: [[content]],
userId: currentCookieData.userId,
createdAt: message.createdAt || isoString
}));
} else if (message.role === "assistant") {
// 助手消息
transcript.push(new NotionTranscriptItem({
type: "markdown-chat",
value: content,
traceId: message.traceId || randomUUID(),
createdAt: message.createdAt || isoString
}));
}
}
// 创建请求体
return new NotionRequestBody({
spaceId: currentCookieData.spaceId,
transcript: transcript,
createThread: true,
traceId: randomUUID(),
debugOverrides: new NotionDebugOverrides({
cachedInferences: {},
annotationInferences: {},
emitInferences: false
}),
generateTitle: false,
saveAllThreadOperations: false
});
}
// 流式处理Notion响应
async function streamNotionResponse(notionRequestBody) {
// 确保我们有当前的cookie数据
if (!currentCookieData) {
currentCookieData = cookieManager.getNext();
if (!currentCookieData) {
throw new Error('没有可用的cookie');
}
}
// 创建流
const stream = new PassThrough();
// 标记流状态
let streamClosed = false;
// 重写stream.end方法,确保安全关闭
const originalEnd = stream.end;
stream.end = function(...args) {
if (streamClosed) return; // 避免重复关闭
streamClosed = true;
return originalEnd.apply(this, args);
};
// 添加初始数据,确保连接建立
stream.write(':\n\n'); // 发送一个空注释行,保持连接活跃
// 设置HTTP头模板
const headers = {
'Content-Type': 'application/json',
'accept': 'application/x-ndjson',
'accept-language': 'en-US,en;q=0.9',
'notion-audit-log-platform': 'web',
'notion-client-version': '23.13.0.3686',
'origin': 'https://www.notion.so',
'referer': 'https://www.notion.so/chat',
'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',
'x-notion-active-user-header': currentCookieData.userId,
'x-notion-space-id': currentCookieData.spaceId
};
// 设置超时处理,确保流不会无限等待
const timeoutId = setTimeout(() => {
if (streamClosed) return;
logger.warning(`请求超时,30秒内未收到响应`);
try {
// 发送结束消息
const endChunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content: "请求超时,未收到Notion响应。" }),
finish_reason: "timeout"
})
]
});
stream.write(`data: ${JSON.stringify(endChunk)}\n\n`);
stream.write('data: [DONE]\n\n');
stream.end();
} catch (error) {
logger.error(`发送超时消息时出错: ${error}`);
if (!streamClosed) stream.end();
}
}, 30000); // 30秒超时
// 启动fetch处理
fetchNotionResponse(
stream,
notionRequestBody,
headers,
NOTION_API_URL,
currentCookieData.cookie,
timeoutId
).catch((error) => {
if (streamClosed) return;
logger.error(`流处理出错: ${error}`);
clearTimeout(timeoutId); // 清除超时计时器
try {
// 发送错误消息
const errorChunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content: `处理请求时出错: ${error.message}` }),
finish_reason: "error"
})
]
});
stream.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
stream.write('data: [DONE]\n\n');
} catch (e) {
logger.error(`发送错误消息时出错: ${e}`);
} finally {
if (!streamClosed) stream.end();
}
});
return stream;
}
// 使用fetch调用Notion API并处理流式响应
async function fetchNotionResponse(chunkQueue, notionRequestBody, headers, notionApiUrl, notionCookie, timeoutId) {
let responseReceived = false;
let dom = null;
// 检查流是否已关闭的辅助函数
const isStreamClosed = () => {
return chunkQueue.destroyed || (typeof chunkQueue.closed === 'boolean' && chunkQueue.closed);
};
// 安全写入函数,确保只向开启的流写入数据
const safeWrite = (data) => {
if (!isStreamClosed()) {
try {
return chunkQueue.write(data);
} catch (error) {
logger.error(`流写入错误: ${error.message}`);
return false;
}
}
return false;
};
try {
// 创建JSDOM实例模拟浏览器环境
dom = new JSDOM("", {
url: "https://www.notion.so",
referrer: "https://www.notion.so/chat",
contentType: "text/html",
includeNodeLocations: true,
storageQuota: 10000000,
pretendToBeVisual: true,
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
});
// 设置全局对象
const { window } = dom;
// 使用更安全的方式设置全局对象
try {
if (!global.window) {
global.window = window;
}
if (!global.document) {
global.document = window.document;
}
// 安全地设置navigator
if (!global.navigator) {
try {
Object.defineProperty(global, 'navigator', {
value: window.navigator,
writable: true,
configurable: true
});
} catch (navError) {
logger.warning(`无法设置navigator: ${navError.message},继续执行`);
// 继续执行,不会中断流程
}
}
} catch (globalError) {
logger.warning(`设置全局对象时出错: ${globalError.message}`);
}
// 设置cookie
document.cookie = notionCookie;
// 创建fetch选项
const fetchOptions = {
method: 'POST',
headers: {
...headers,
'user-agent': window.navigator.userAgent,
'Cookie': notionCookie
},
body: JSON.stringify(notionRequestBody),
};
// 添加代理配置(如果有)
if (USE_NATIVE_PROXY_POOL && ENABLE_PROXY_SERVER && !PROXY_URL) {
proxy = proxyPool.getProxy();
if (proxy !== null)
{
logger.info(`使用代理: ${proxy.full}`);
}
else{
logger.warning(`没有可用代理`);
}
} else if(USE_NATIVE_PROXY_POOL&&!PROXY_URL&&!ENABLE_PROXY_SERVER) {
const { HttpsProxyAgent } = await import('https-proxy-agent');
proxy = proxyPool.getProxy();
fetchOptions.agent = new HttpsProxyAgent(proxy.full);
logger.info(`使用代理: ${proxy.full}`);
}else if(PROXY_URL){
const { HttpsProxyAgent } = await import('https-proxy-agent');
fetchOptions.agent = new HttpsProxyAgent(PROXY_URL);
logger.info(`使用代理: ${PROXY_URL}`);
}
let response = null;
// 发送请求
if (ENABLE_PROXY_SERVER && USE_NATIVE_PROXY_POOL){
response = await fetch('http://127.0.0.1:10655/proxy', {
method: 'POST',
body: JSON.stringify({
method: 'POST',
url: notionApiUrl,
headers: fetchOptions.headers,
body: fetchOptions.body,
stream:true,
proxy:proxy.full
}),
});
}
else if (ENABLE_PROXY_SERVER && !USE_NATIVE_PROXY_POOL && PROXY_URL){
response = await fetch('http://127.0.0.1:10655/proxy', {
method: 'POST',
body: JSON.stringify({
method: 'POST',
url: notionApiUrl,
headers: fetchOptions.headers,
body: fetchOptions.body,
proxy: PROXY_URL,
stream:true,
}),
});
}
else if(ENABLE_PROXY_SERVER && !USE_NATIVE_PROXY_POOL){
response = await fetch('http://127.0.0.1:10655/proxy', {
method: 'POST',
body: JSON.stringify({
method: 'POST',
url: notionApiUrl,
headers: fetchOptions.headers,
body: fetchOptions.body,
stream:true,
}),
});
}
else{
response = await fetch(notionApiUrl, fetchOptions);
}
// 检查是否收到401错误(未授权)
if (response.status === 401) {
logger.error(`收到401未授权错误,cookie可能已失效`);
// 标记当前cookie为无效
cookieManager.markAsInvalid(currentCookieData.userId);
// 尝试获取下一个cookie
currentCookieData = cookieManager.getNext();
if (!currentCookieData) {
throw new Error('所有cookie均已失效,无法继续请求');
}
// 使用新cookie重新构建请求体
const newRequestBody = buildNotionRequest({
model: notionRequestBody.transcript[0]?.value?.model || '',
messages: [] // 这里应该根据实际情况重构消息
});
// 使用新cookie重试请求
return fetchNotionResponse(
chunkQueue,
newRequestBody,
{
...headers,
'x-notion-active-user-header': currentCookieData.userId,
'x-notion-space-id': currentCookieData.spaceId
},
notionApiUrl,
currentCookieData.cookie,
timeoutId
);
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 处理流式响应
if (!response.body) {
throw new Error("Response body is null");
}
// 创建流读取器
const reader = response.body;
let buffer = '';
// 处理数据块
reader.on('data', (chunk) => {
// 检查流是否已关闭
if (isStreamClosed()) {
try {
reader.destroy();
} catch (error) {
logger.error(`销毁reader时出错: ${error.message}`);
}
return;
}
try {
// 标记已收到响应
if (!responseReceived) {
responseReceived = true;
logger.info(`已连接Notion API`);
clearTimeout(timeoutId); // 清除超时计时器
}
// 解码数据
const text = chunk.toString('utf8');
buffer += text;
// 按行分割并处理完整的JSON对象
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
for (const line of lines) {
if (!line.trim()) continue;
try {
const jsonData = JSON.parse(line);
// 提取内容
if (jsonData?.type === "markdown-chat" && typeof jsonData?.value === "string") {
const content = jsonData.value;
if (!content) continue;
// 创建OpenAI格式的块
const chunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content }),
finish_reason: null
})
]
});
// 添加到队列
const dataStr = `data: ${JSON.stringify(chunk)}\n\n`;
if (!safeWrite(dataStr)) {
// 如果写入失败,结束处理
try {
reader.destroy();
} catch (error) {
logger.error(`写入失败后销毁reader时出错: ${error.message}`);
}
return;
}
} else if (jsonData?.recordMap) {
// 忽略recordMap响应
} else {
// 忽略其他类型响应
}
} catch (jsonError) {
logger.error(`解析JSON出错: ${jsonError}`);
}
}
} catch (error) {
logger.error(`处理数据块出错: ${error}`);
}
});
// 处理流结束
reader.on('end', () => {
try {
logger.info(`响应完成`);
if (cookieManager.getValidCount() > 1){
// 尝试切换到下一个cookie
currentCookieData = cookieManager.getNext();
logger.info(`切换到下一个cookie: ${currentCookieData.userId}`);
}
// 如果没有收到任何响应,发送一个提示消息
if (!responseReceived) {
if (!ENABLE_PROXY_SERVER){
logger.warning(`未从Notion收到内容响应,请尝试启用tls代理服务`)
}else if (USE_NATIVE_PROXY_POOL){
logger.warning(`未从Notion收到内容响应,请重roll,或者切换cookie`)
}else{
logger.warning(`未从Notion收到内容响应,请更换ip重试`);
}
if (USE_NATIVE_PROXY_POOL) {
proxyPool.removeProxy(proxy.ip, proxy.port);
}
const noContentChunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content: "未从Notion收到内容响应,请更换ip重试。" }),
finish_reason: "no_content"
})
]
});
safeWrite(`data: ${JSON.stringify(noContentChunk)}\n\n`);
}
// 创建结束块
const endChunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content: null }),
finish_reason: "stop"
})
]
});
// 添加到队列
safeWrite(`data: ${JSON.stringify(endChunk)}\n\n`);
safeWrite('data: [DONE]\n\n');
// 清除超时计时器(如果尚未清除)
if (timeoutId) clearTimeout(timeoutId);
// 清理全局对象
try {
if (global.window) delete global.window;
if (global.document) delete global.document;
// 安全地删除navigator
if (global.navigator) {
try {
delete global.navigator;
} catch (navError) {
// 如果无法删除,尝试将其设置为undefined
try {
Object.defineProperty(global, 'navigator', {
value: undefined,
writable: true,
configurable: true
});
} catch (defineError) {
logger.warning(`无法清理navigator: ${defineError.message}`);
}
}
}
} catch (cleanupError) {
logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
}
// 结束流
if (!isStreamClosed()) {
chunkQueue.end();
}
} catch (error) {
logger.error(`Error in stream end handler: ${error}`);
if (timeoutId) clearTimeout(timeoutId);
// 清理全局对象
try {
if (global.window) delete global.window;
if (global.document) delete global.document;
// 安全地删除navigator
if (global.navigator) {
try {
delete global.navigator;
} catch (navError) {
// 如果无法删除,尝试将其设置为undefined
try {
Object.defineProperty(global, 'navigator', {
value: undefined,
writable: true,
configurable: true
});
} catch (defineError) {
logger.warning(`无法清理navigator: ${defineError.message}`);
}
}
}
} catch (cleanupError) {
logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
}
if (!isStreamClosed()) {
chunkQueue.end();
}
}
});
// 处理错误
reader.on('error', (error) => {
logger.error(`Stream error: ${error}`);
if (timeoutId) clearTimeout(timeoutId);
// 清理全局对象
try {
if (global.window) delete global.window;
if (global.document) delete global.document;
// 安全地删除navigator
if (global.navigator) {
try {
delete global.navigator;
} catch (navError) {
// 如果无法删除,尝试将其设置为undefined
try {
Object.defineProperty(global, 'navigator', {
value: undefined,
writable: true,
configurable: true
});
} catch (defineError) {
logger.warning(`无法清理navigator: ${defineError.message}`);
}
}
}
} catch (cleanupError) {
logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
}
try {
const errorChunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content: `流读取错误: ${error.message}` }),
finish_reason: "error"
})
]
});
safeWrite(`data: ${JSON.stringify(errorChunk)}\n\n`);
safeWrite('data: [DONE]\n\n');
} catch (e) {
logger.error(`Error sending error message: ${e}`);
} finally {
if (!isStreamClosed()) {
chunkQueue.end();
}
}
});
} catch (error) {
logger.error(`Notion API请求失败: ${error}`);
// 清理全局对象
try {
if (global.window) delete global.window;
if (global.document) delete global.document;
// 安全地删除navigator
if (global.navigator) {
try {
delete global.navigator;
} catch (navError) {
// 如果无法删除,尝试将其设置为undefined
try {
Object.defineProperty(global, 'navigator', {
value: undefined,
writable: true,
configurable: true
});
} catch (defineError) {
logger.warning(`无法清理navigator: ${defineError.message}`);
}
}
}
} catch (cleanupError) {
logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
}
if (timeoutId) clearTimeout(timeoutId);
// 确保在错误情况下也触发流结束
try {
if (!responseReceived && !isStreamClosed()) {
const errorChunk = new ChatCompletionChunk({
choices: [
new Choice({
delta: new ChoiceDelta({ content: `Notion API请求失败: ${error.message}` }),
finish_reason: "error"
})
]
});
safeWrite(`data: ${JSON.stringify(errorChunk)}\n\n`);
safeWrite('data: [DONE]\n\n');
}
} catch (e) {
logger.error(`发送错误消息时出错: ${e}`);
}
if (!isStreamClosed()) {
chunkQueue.end();
}
throw error; // 重新抛出错误以便上层捕获
}
}
// 应用初始化
async function initialize() {
logger.info(`初始化Notion配置...`);
// 启动代理服务器
try {
await proxyServer.start();
} catch (error) {
logger.error(`启动代理服务器失败: ${error.message}`);
}
// 初始化cookie管理器
let initResult = false;
// 检查是否配置了cookie文件
const cookieFilePath = process.env.COOKIE_FILE;
if (cookieFilePath) {
logger.info(`检测到COOKIE_FILE配置: ${cookieFilePath}`);
initResult = await cookieManager.loadFromFile(cookieFilePath);
if (!initResult) {
logger.error(`从文件加载cookie失败,尝试使用环境变量中的NOTION_COOKIE`);
}
}
// 如果文件加载失败或未配置文件,尝试从环境变量加载
if (!initResult) {
const cookiesString = process.env.NOTION_COOKIE;
if (!cookiesString) {
logger.error(`错误: 未设置NOTION_COOKIE环境变量或COOKIE_FILE路径,应用无法正常工作`);
logger.error(`请在.env文件中设置有效的NOTION_COOKIE值或COOKIE_FILE路径`);
INITIALIZED_SUCCESSFULLY = false;
return;
}
logger.info(`正在从环境变量初始化cookie管理器...`);
initResult = await cookieManager.initialize(cookiesString);
if (!initResult) {
logger.error(`初始化cookie管理器失败,应用无法正常工作`);
INITIALIZED_SUCCESSFULLY = false;
return;
}
}
// 获取第一个可用的cookie数据
currentCookieData = cookieManager.getNext();
if (!currentCookieData) {
logger.error(`没有可用的cookie,应用无法正常工作`);
INITIALIZED_SUCCESSFULLY = false;
return;
}
logger.success(`成功初始化cookie管理器,共有 ${cookieManager.getValidCount()} 个有效cookie`);
logger.info(`当前使用的cookie对应的用户ID: ${currentCookieData.userId}`);
logger.info(`当前使用的cookie对应的空间ID: ${currentCookieData.spaceId}`);
if (process.env.USE_NATIVE_PROXY_POOL === 'true') {
logger.info(`正在初始化本地代理池...`);
// 设置代理池的日志级别为warn,减少详细日志输出
proxyPool.logLevel = 'error';
// 启用进度条显示
proxyPool.showProgressBar = true;
if (['us', 'uk', 'jp', 'de', 'fr', 'ca'].includes(process.env.PROXY_COUNTRY)) {
proxyPool.setCountry(process.env.PROXY_COUNTRY);
} else {
logger.warning(`未设置正确PROXY_COUNTRY,使用默认代理国家: us`);
proxyPool.setCountry('us');
}
await proxyPool.initialize();
await new Promise(resolve => setTimeout(resolve, 1000));
logger.success(`代理池初始化完成,当前代理国家: ${proxyPool.proxyCountry}`);
}
INITIALIZED_SUCCESSFULLY = true;
}
// 导出函数
export {
initialize,
streamNotionResponse,
buildNotionRequest,
INITIALIZED_SUCCESSFULLY
};