} JSON object
+ */
+ Content.prototype.toJSON = function toJSON() {
+ return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+ };
+
+ /**
+ * Gets the default type url for Content
+ * @function getTypeUrl
+ * @memberof StreamUnifiedChatWithToolsResponse.Message.Unknown12.Content
+ * @static
+ * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
+ * @returns {string} The default type url
+ */
+ Content.getTypeUrl = function getTypeUrl(typeUrlPrefix) {
+ if (typeUrlPrefix === undefined) {
+ typeUrlPrefix = "type.googleapis.com";
+ }
+ return typeUrlPrefix + "/StreamUnifiedChatWithToolsResponse.Message.Unknown12.Content";
+ };
+
+ return Content;
+ })();
+
+ return Unknown12;
+ })();
+
+ return Message;
+ })();
+
+ return StreamUnifiedChatWithToolsResponse;
+})();
+
+module.exports = $root;
diff --git a/src/proto/message.proto b/src/proto/message.proto
new file mode 100644
index 0000000000000000000000000000000000000000..c5da418e65929b285b90739965ecf928c4b9e2cb
--- /dev/null
+++ b/src/proto/message.proto
@@ -0,0 +1,133 @@
+syntax = "proto3";
+
+message AvailableModelsResponse {
+ message AvailableModel {
+ string name = 1;
+ bool defaultOn = 2;
+ optional bool isLongContextOnly = 3;
+ optional bool isChatOnly = 4;
+ }
+ repeated AvailableModel models = 2;
+ repeated string modelNames = 1;
+}
+
+message MessageSummary {
+ string content = 1;
+ string summaryId1 = 2;
+ string summaryId2 = 3; // uuid, equal to summaryId1
+ string previousSummaryId = 4;
+}
+
+message MessageThinking {
+ string content = 1;
+}
+message StreamUnifiedChatWithToolsRequest {
+ message Request {
+ message Message {
+ message Image {
+ message Metadata {
+ int32 width = 1;
+ int32 height = 2;
+ }
+ bytes data = 1;
+ Metadata metadata = 2;
+ }
+ string content = 1;
+ int32 role = 2;
+ Image image = 10;
+ string messageId = 13;
+ string unknown29 = 29; // 1, only for user message
+ string summaryId = 32;
+ MessageSummary summary = 39;
+ MessageThinking thinking = 45;
+ int32 chatModeEnum = 47; // 1 for ask, 2 for agent, 3 for edit
+ }
+ message Instruction {
+ string instruction = 1;
+ }
+ message Model {
+ string name = 1;
+ bytes empty = 4;
+ }
+ message CursorSetting {
+ message Unknown6 {
+ bytes unknown1 = 1;
+ bytes unknown2 = 2;
+ }
+ string name = 1;
+ bytes unknown3 = 3;
+ Unknown6 unknown6 = 6;
+ int32 unknown8 = 8;
+ int32 unknown9 = 9;
+ }
+ message Metadata {
+ string os = 1; // win32
+ string arch = 2; // x64
+ string version = 3; // 10.0.22631
+ string path = 4; // C:\Program Files\PowerShell\7\pwsh.exe
+ string timestamp = 5; // 2025-03-03T13:10:08.590Z
+ }
+ message MessageId {
+ string messageId = 1;
+ string summaryId = 2;
+ int32 role = 3;
+ }
+
+ repeated Message messages = 1;
+ int32 unknown2 = 2; // 1
+ Instruction instruction = 3;
+ int32 unknown4 = 4; // 1
+ Model model = 5;
+ repeated string wikiTool = 7; // one url one item
+ string webTool = 8; // "full search"
+ int32 unknown13 = 13;
+ CursorSetting cursorSetting = 15;
+ int32 unknown19 = 19; // 1
+ int32 unknown22 = 22; // 1
+ string conversationId = 23; // uuid
+ Metadata metadata = 26;
+ int32 unknown27 = 27; // 1
+ string unknown29 = 29;
+ repeated MessageId messageIds = 30;
+ int32 largeContext = 35; // 1
+ int32 unknown38 = 38; // 0
+ int32 chatModeEnum = 46; // 1 for ask, 2 for agent, 3 for edit
+ string unknown47 = 47;
+ int32 unknown48 = 48; // 0
+ int32 unknown49 = 49; // 0
+ int32 unknown51 = 51; // 0
+ int32 unknown53 = 53; // 0
+ string chatMode = 54;
+ }
+
+ Request request = 1;
+}
+
+message StreamUnifiedChatWithToolsResponse {
+ message Message {
+ message WebTool {
+ message WebPage {
+ string url = 1;
+ string title = 2;
+ string content = 3;
+ }
+ repeated WebPage webPage = 1;
+ }
+ message Unknown12 {
+ message Content {
+ string content = 1;
+ }
+ Content content = 1;
+ }
+ string content = 1;
+ WebTool webtool = 11;
+ Unknown12 unknown12 = 12;
+ string unknown22 = 22; // uuid
+ string unknown23 = 23;
+ string unknown27 = 27; // uuid
+ MessageThinking thinking = 25;
+ }
+
+ Message message = 2;
+ MessageSummary summary = 3;
+}
diff --git a/src/proxy/cursor_proxy_server_linux_amd64 b/src/proxy/cursor_proxy_server_linux_amd64
new file mode 100644
index 0000000000000000000000000000000000000000..e39ebbdacd50019bc9d3ef2f9aa6a7cf8e17ff42
--- /dev/null
+++ b/src/proxy/cursor_proxy_server_linux_amd64
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:985fa643f2a5b793102a4e97b058f14fc1e58cfd280c0bcc70e8a8455e932a4e
+size 12790492
diff --git a/src/proxy/others/cursor_proxy_server_linux_amd64 b/src/proxy/others/cursor_proxy_server_linux_amd64
new file mode 100644
index 0000000000000000000000000000000000000000..8baf7cb9ea49303215509a2c4552b74a2cb1ca46
--- /dev/null
+++ b/src/proxy/others/cursor_proxy_server_linux_amd64
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6fe0a95e158d753f3f179b88690d5e76cdb4a5b6408ca024bc544d2a84c6a919
+size 12780345
diff --git a/src/public/index.html b/src/public/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..65d5472e22ac5171e42d600f07873512b74fa27b
--- /dev/null
+++ b/src/public/index.html
@@ -0,0 +1,326 @@
+
+
+
+
+
+ Cursor To OpenAI - API Key 管理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
添加/更新 API Key
+
+
+
+
+
+
+
+
+
现有 API Key
+
+
+
+
+
+
+ | API KEY |
+ COOKIE 数量 |
+ 操作 |
+
+
+
+
+
+
+
+
+
+
+
+
+
使用说明
+
+
+ - 添加自定义 API Key 和对应的 Cursor Cookie 值。
+ - API Key 可以不添加 Cookie,但不会有任何功能,可以后续再添加 Cookie。
+ - 使用自定义 API Key 作为 OpenAI API 的认证凭证。
+ - 系统将自动在多个 Cookie 之间进行轮询。
+
+ /v1/models - 模型列表
+ /v1/chat/completions - 聊天补全
+
+
+
+
+
+
+
+
+
+
不建议使用
+
+
+
+
Cookie自动刷新 (不建议使用)
+
+
+
+
+
+
+
+
+
+
获取Cursor Cookie
+
+
+
+
+
+
+
+
+
×
+
+
+
修改 API Key 的 Cookie
+
+
+
+
+
+
+
+
+
+
×
+
+
+
管理无效Cookie
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/public/login.html b/src/public/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..39516c3b5577d8aedc3abea641d04198fc0f84ad
--- /dev/null
+++ b/src/public/login.html
@@ -0,0 +1,282 @@
+
+
+
+
+
+ Cursor To OpenAI - 管理员登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
管理员登录
+
+
+
+
+ 还没有账号?点击注册
+
+
+
+
+
+
+
+
注册管理员账号
+
+
+
+
+ 已有账号?点击登录
+
+
+
+
+
+
+
系统信息
+
+
+
+ Cursor To OpenAI 是一个管理自定义 API Key 与 Cursor Cookie 映射关系的高效工具。登录后可以进行API Key的管理和配置。
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/public/logs.html b/src/public/logs.html
new file mode 100644
index 0000000000000000000000000000000000000000..fa38d29744dc5004b613c1dd10f2c494879d7adc
--- /dev/null
+++ b/src/public/logs.html
@@ -0,0 +1,714 @@
+
+
+
+
+
+ Cursor To OpenAI - 日志查看
+
+
+
+
+
+
+
+
+
日志筛选
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已开启
+
+
+
+
+
+
+
日志列表
+
+
+
+
+
+ | 时间 |
+ 级别 |
+ 内容 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/public/scripts.js b/src/public/scripts.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd2d4e17cfd22f5839c47ae1ac59303f0e1a7c38
--- /dev/null
+++ b/src/public/scripts.js
@@ -0,0 +1,1342 @@
+// 模态框相关功能
+document.addEventListener('DOMContentLoaded', function() {
+ // 获取所有模态框和关闭按钮
+ const modals = document.querySelectorAll('.modal');
+ const closeBtns = document.querySelectorAll('.close');
+
+ // 关闭所有模态框的函数
+ function closeAllModals() {
+ modals.forEach(modal => {
+ modal.style.display = 'none';
+ });
+ document.body.classList.remove('modal-open');
+ }
+
+ // 为每个关闭按钮添加事件
+ closeBtns.forEach(btn => {
+ btn.onclick = closeAllModals;
+ });
+
+ // 点击模态框外部关闭
+ window.onclick = function(event) {
+ modals.forEach(modal => {
+ if (event.target == modal) {
+ closeAllModals();
+ }
+ });
+ }
+
+ // 页面加载时获取 API Key 列表和无效Cookie列表
+ checkAuth();
+ loadApiKeys();
+ renderInvalidCookies();
+ populateRefreshApiKeySelect();
+ populateCookieApiKeySelect();
+
+ // 初始化添加Cookie的标签容器
+ renderAddCookieTags([]);
+
+ // 绑定事件监听器
+ bindEventListeners();
+
+ // 处理日志按钮点击事件
+ document.getElementById('logsBtn')?.addEventListener('click', function() {
+ window.location.href = '/logs.html';
+ });
+});
+
+// 绑定各种事件监听器
+function bindEventListeners() {
+ // 表单提交
+ document.getElementById('addKeyForm').addEventListener('submit', handleAddKeyForm);
+ document.getElementById('editCookieForm').addEventListener('submit', handleEditCookieForm);
+ document.getElementById('invalidCookieForm').addEventListener('submit', handleInvalidCookieForm);
+
+ // 按钮点击
+ // 注意:testApiBtn可能在页面上出现两次,需要检查元素是否存在
+ const testApiButtons = document.querySelectorAll('#testApiBtn');
+ testApiButtons.forEach(btn => {
+ if(btn) btn.addEventListener('click', testApiConnection);
+ });
+
+ const clearCacheButtons = document.querySelectorAll('#clearCacheBtn');
+ clearCacheButtons.forEach(btn => {
+ if(btn) btn.addEventListener('click', clearCacheAndRefresh);
+ });
+
+ // 其他按钮
+ if(document.getElementById('addNewCookieBtn')) document.getElementById('addNewCookieBtn').addEventListener('click', handleAddNewCookie);
+ if(document.getElementById('addCookieBtn')) document.getElementById('addCookieBtn').addEventListener('click', handleAddCookie);
+ if(document.getElementById('addInvalidCookieBtn')) document.getElementById('addInvalidCookieBtn').addEventListener('click', handleAddInvalidCookie);
+ if(document.getElementById('closeInvalidCookieModal')) document.getElementById('closeInvalidCookieModal').addEventListener('click', closeInvalidCookieModal);
+
+ // 修复刷新Cookie和生成链接按钮的事件绑定
+ const refreshCookieBtn = document.getElementById('refreshCookieBtn');
+ if(refreshCookieBtn) {
+ console.log('为refreshCookieBtn绑定事件');
+ refreshCookieBtn.addEventListener('click', handleRefreshCookie);
+ }
+
+ const generateLinkBtn = document.getElementById('generateLinkBtn');
+ if(generateLinkBtn) {
+ console.log('为generateLinkBtn绑定事件');
+ generateLinkBtn.addEventListener('click', handleGenerateLink);
+ }
+
+ if(document.getElementById('logoutBtn')) document.getElementById('logoutBtn').addEventListener('click', handleLogout);
+}
+
+// API Key 管理相关函数
+// 加载现有 API Key
+async function loadApiKeys() {
+ try {
+ console.log('开始加载API Keys...');
+ const response = await fetch('/v1/api-keys', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ console.log('API响应状态:', response.status);
+ const data = await response.json();
+ console.log('获取到的数据:', data);
+
+ const keyList = document.getElementById('keyList');
+ keyList.innerHTML = '';
+
+ if (data.success && data.apiKeys.length > 0) {
+ data.apiKeys.forEach(key => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${key.key} |
+ ${key.cookieCount} |
+
+
+
+ |
+ `;
+ keyList.appendChild(row);
+ });
+ } else {
+ keyList.innerHTML = '| 暂无 API Key |
';
+ }
+ } catch (error) {
+ console.error('加载 API Key 失败:', error);
+ document.getElementById('keyListMessage').innerHTML = `
+ 加载 API Key 失败: ${error.message}
+ `;
+ }
+}
+
+// 处理添加/更新 API Key 表单提交
+async function handleAddKeyForm(e) {
+ e.preventDefault();
+
+ const apiKey = document.getElementById('apiKey').value.trim();
+ const cookieValuesText = document.getElementById('cookieValues').value.trim();
+
+ if (!apiKey) {
+ document.getElementById('addKeyMessage').innerHTML = `
+ API Key 不能为空
+ `;
+ return;
+ }
+
+ // 将逗号分隔的 Cookie 值转换为数组
+ const cookieValues = cookieValuesText ?
+ cookieValuesText.split(',').map(cookie => cookie.trim()).filter(cookie => cookie) :
+ [];
+
+ try {
+ const response = await fetch('/v1/api-keys', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ apiKey,
+ cookieValues,
+ }),
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('addKeyMessage').innerHTML = `
+ API Key 添加/更新成功
+ `;
+ // 等待3秒后再刷新页面
+ setTimeout(() => {
+ window.location.reload();
+ }, 3000);
+ } else {
+ document.getElementById('addKeyMessage').innerHTML = `
+ API Key 添加/更新失败: ${data.error}
+ `;
+ }
+ } catch (error) {
+ console.error('添加/更新 API Key 失败:', error);
+ document.getElementById('addKeyMessage').innerHTML = `
+ 添加/更新 API Key 失败: ${error.message}
+ `;
+ }
+}
+
+// 删除 API Key
+async function deleteApiKey(apiKey) {
+ if (!confirm(`确定要删除 API Key "${apiKey}" 吗?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/v1/api-keys/${encodeURIComponent(apiKey)}`, {
+ method: 'DELETE',
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('keyListMessage').innerHTML = `
+ API Key 删除成功
+ `;
+ loadApiKeys();
+ } else {
+ document.getElementById('keyListMessage').innerHTML = `
+ API Key 删除失败: ${data.error}
+ `;
+ }
+ } catch (error) {
+ console.error('删除 API Key 失败:', error);
+ document.getElementById('keyListMessage').innerHTML = `
+ 删除 API Key 失败: ${error.message}
+ `;
+ }
+}
+
+// 获取API Key的Cookie值
+async function getCookiesForApiKey(apiKey) {
+ try {
+ const response = await fetch(`/v1/api-keys/${encodeURIComponent(apiKey)}/cookies`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.cookies;
+ } catch (error) {
+ console.error(`获取 ${apiKey} 的Cookie值失败:`, error);
+ throw error;
+ }
+}
+
+// 修改 API Key
+async function editApiKey(apiKey) {
+ try {
+ document.getElementById('editModalMessage').innerHTML = '';
+ document.getElementById('editApiKey').value = apiKey;
+
+ // 获取当前Cookie值
+ const cookies = await getCookiesForApiKey(apiKey);
+
+ // 更新隐藏的textarea
+ document.getElementById('editCookieValues').value = cookies.join(',');
+
+ // 更新Cookie标签容器
+ renderCookieTags(cookies);
+
+ // 清空新Cookie输入框
+ document.getElementById('newCookie').value = '';
+
+ // 显示模态框
+ const modal = document.getElementById('editModal');
+ modal.style.display = 'block';
+ document.body.classList.add('modal-open');
+
+ } catch (error) {
+ console.error('打开修改模态框失败:', error);
+ document.getElementById('editModalMessage').innerHTML = `
+ 无法加载Cookie数据: ${error.message}
+ `;
+ const modal = document.getElementById('editModal');
+ modal.style.display = 'block'; // 即使出错也显示模态框,以便显示错误信息
+ document.body.classList.add('modal-open');
+ }
+}
+
+// 获取API Keys的辅助函数
+async function getApiKeys() {
+ const response = await fetch('/v1/api-keys', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.success ? data.apiKeys : [];
+}
+
+// 复制文本到剪贴板的通用函数
+async function copyTextToClipboard(text) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch (err) {
+ console.error('复制到剪贴板失败:', err);
+
+ // 如果navigator.clipboard不可用,使用备用方法
+ try {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+
+ // 将元素设置为看不见
+ textArea.style.position = 'fixed';
+ textArea.style.top = '0';
+ textArea.style.left = '0';
+ textArea.style.width = '2em';
+ textArea.style.height = '2em';
+ textArea.style.padding = '0';
+ textArea.style.border = 'none';
+ textArea.style.outline = 'none';
+ textArea.style.boxShadow = 'none';
+ textArea.style.background = 'transparent';
+
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ const successful = document.execCommand('copy');
+ document.body.removeChild(textArea);
+ return successful;
+ } catch (fallbackErr) {
+ console.error('备用复制方法失败:', fallbackErr);
+ return false;
+ }
+ }
+}
+
+// 显示复制成功弹窗提示
+function showCopyToast(success) {
+ const toast = document.createElement('div');
+ toast.style.position = 'fixed';
+ toast.style.bottom = '20px';
+ toast.style.left = '50%';
+ toast.style.transform = 'translateX(-50%)';
+ toast.style.padding = '8px 16px';
+ toast.style.borderRadius = '4px';
+ toast.style.zIndex = '9999';
+ toast.style.fontSize = '14px';
+
+ if (success) {
+ toast.style.backgroundColor = '#28a745';
+ toast.style.color = 'white';
+ toast.textContent = '复制成功!';
+ } else {
+ toast.style.backgroundColor = '#dc3545';
+ toast.style.color = 'white';
+ toast.textContent = '复制失败,请手动复制';
+ }
+
+ document.body.appendChild(toast);
+
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ toast.style.transition = 'opacity 0.5s';
+ setTimeout(() => {
+ document.body.removeChild(toast);
+ }, 500);
+ }, 2000);
+}
+
+// 处理复制Cookie按钮点击
+async function handleCopyCookie(cookie) {
+ const success = await copyTextToClipboard(cookie);
+ showCopyToast(success);
+}
+
+// Cookie管理相关函数
+// 渲染Cookie标签
+function renderCookieTags(cookies) {
+ const container = document.getElementById('cookieTagsContainer');
+ container.innerHTML = '';
+
+ if (cookies.length === 0) {
+ container.innerHTML = '暂无Cookie,请添加
';
+ return;
+ }
+
+ cookies.forEach((cookie, index) => {
+ // 创建标签
+ const tag = document.createElement('span');
+ tag.className = 'cookie-tag';
+
+ // 对短文本添加特殊类
+ if (cookie.length < 5) {
+ tag.classList.add('short-cookie');
+ }
+
+ // 截断Cookie显示
+ const displayText = cookie.length > 20 ?
+ cookie.substring(0, 8) + '...' + cookie.substring(cookie.length - 8) :
+ cookie;
+
+ tag.title = cookie; // 完整Cookie作为工具提示
+
+ // 增加对移动端友好的结构,添加复制按钮
+ tag.innerHTML = `
+ ${displayText}
+
+
+
+
+ `;
+ container.appendChild(tag);
+ });
+
+ // 添加删除按钮的事件监听
+ document.querySelectorAll('.delete-cookie').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const index = parseInt(this.getAttribute('data-index'));
+ deleteCookieTag(index);
+ });
+ });
+
+ // 添加复制按钮的事件监听
+ document.querySelectorAll('.copy-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const cookie = this.getAttribute('data-cookie');
+ handleCopyCookie(cookie);
+ });
+ });
+}
+
+// 删除Cookie标签
+function deleteCookieTag(index) {
+ // 从隐藏的textarea中获取当前的cookies
+ const cookieValuesElem = document.getElementById('editCookieValues');
+ let cookies = cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c);
+
+ // 删除指定索引的cookie
+ cookies.splice(index, 1);
+
+ // 更新隐藏的textarea
+ cookieValuesElem.value = cookies.join(',');
+
+ // 重新渲染标签
+ renderCookieTags(cookies);
+}
+
+// 处理添加新Cookie
+function handleAddCookie() {
+ const newCookieInput = document.getElementById('newCookie');
+ const newCookie = newCookieInput.value.trim();
+
+ if (!newCookie) {
+ return;
+ }
+
+ // 获取当前的cookies
+ const cookieValuesElem = document.getElementById('editCookieValues');
+ let cookies = cookieValuesElem.value ?
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
+ [];
+
+ // 添加新cookie
+ cookies.push(newCookie);
+
+ // 更新隐藏的textarea
+ cookieValuesElem.value = cookies.join(',');
+
+ // 重新渲染标签
+ renderCookieTags(cookies);
+
+ // 清空输入框
+ newCookieInput.value = '';
+}
+
+// 处理编辑表单提交
+async function handleEditCookieForm(e) {
+ e.preventDefault();
+
+ const apiKey = document.getElementById('editApiKey').value.trim();
+ const cookieValuesText = document.getElementById('editCookieValues').value.trim();
+
+ if (!apiKey) {
+ document.getElementById('editModalMessage').innerHTML = `
+ API Key不能为空
+ `;
+ return;
+ }
+
+ // 将逗号分隔的 Cookie 值转换为数组
+ const cookieValues = cookieValuesText ?
+ cookieValuesText.split(',').map(cookie => cookie.trim()).filter(cookie => cookie) :
+ [];
+
+ try {
+ const response = await fetch('/v1/api-keys', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ apiKey,
+ cookieValues,
+ }),
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('editModalMessage').innerHTML = `
+ Cookie 修改成功
+ `;
+ setTimeout(() => {
+ document.getElementById('editModal').style.display = 'none';
+ loadApiKeys();
+ }, 1500);
+ } else {
+ document.getElementById('editModalMessage').innerHTML = `
+ Cookie 修改失败: ${data.error}
+ `;
+ }
+ } catch (error) {
+ console.error('修改 Cookie 失败:', error);
+ document.getElementById('editModalMessage').innerHTML = `
+ 修改 Cookie 失败: ${error.message}
+ `;
+ }
+}
+
+// 渲染添加API Key表单中的Cookie标签
+function renderAddCookieTags(cookies) {
+ const container = document.getElementById('addCookieTagsContainer');
+ container.innerHTML = '';
+
+ if (cookies.length === 0) {
+ container.innerHTML = '暂无Cookie,请添加
';
+ return;
+ }
+
+ cookies.forEach((cookie, index) => {
+ const tag = document.createElement('span');
+ tag.className = 'cookie-tag';
+
+ // 对短文本添加特殊类
+ if (cookie.length < 5) {
+ tag.classList.add('short-cookie');
+ }
+
+ const displayText = cookie.length > 20 ?
+ cookie.substring(0, 8) + '...' + cookie.substring(cookie.length - 8) :
+ cookie;
+
+ tag.title = cookie;
+
+ // 增加对移动端友好的结构,添加复制按钮
+ tag.innerHTML = `
+ ${displayText}
+
+
+
+
+ `;
+ container.appendChild(tag);
+ });
+
+ document.querySelectorAll('.delete-add-cookie').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const index = parseInt(this.getAttribute('data-index'));
+ deleteAddCookieTag(index);
+ });
+ });
+
+ // 添加复制按钮的事件监听
+ document.querySelectorAll('.copy-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const cookie = this.getAttribute('data-cookie');
+ handleCopyCookie(cookie);
+ });
+ });
+}
+
+// 删除添加表单中的Cookie标签
+function deleteAddCookieTag(index) {
+ const cookieValuesElem = document.getElementById('cookieValues');
+ let cookies = cookieValuesElem.value ?
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
+ [];
+
+ cookies.splice(index, 1);
+ cookieValuesElem.value = cookies.join(',');
+ renderAddCookieTags(cookies);
+}
+
+// 处理添加新Cookie标签到添加表单
+function handleAddNewCookie() {
+ const newCookieInput = document.getElementById('addNewCookie');
+ const newCookie = newCookieInput.value.trim();
+
+ if (!newCookie) {
+ return;
+ }
+
+ const cookieValuesElem = document.getElementById('cookieValues');
+ let cookies = cookieValuesElem.value ?
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
+ [];
+
+ cookies.push(newCookie);
+ cookieValuesElem.value = cookies.join(',');
+ renderAddCookieTags(cookies);
+ newCookieInput.value = '';
+}
+
+// 无效Cookie管理相关函数
+// 获取无效Cookie列表
+async function getInvalidCookies() {
+ try {
+ const response = await fetch('/v1/invalid-cookies', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.invalidCookies;
+ } catch (error) {
+ console.error('获取无效Cookie失败:', error);
+ throw error;
+ }
+}
+
+// 清除特定无效Cookie
+async function clearInvalidCookie(cookie) {
+ try {
+ const response = await fetch(`/v1/invalid-cookies/${encodeURIComponent(cookie)}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.success;
+ } catch (error) {
+ console.error(`清除无效Cookie失败:`, error);
+ throw error;
+ }
+}
+
+// 清除所有无效Cookie
+async function clearAllInvalidCookies() {
+ try {
+ const response = await fetch('/v1/invalid-cookies', {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.success;
+ } catch (error) {
+ console.error('清除所有无效Cookie失败:', error);
+ throw error;
+ }
+}
+
+// 渲染无效Cookie列表
+async function renderInvalidCookies() {
+ const container = document.getElementById('invalidCookiesContainer');
+
+ try {
+ const invalidCookies = await getInvalidCookies();
+
+ if (invalidCookies.length === 0) {
+ container.innerHTML = '没有检测到无效Cookie
';
+ return;
+ }
+
+ let html = '| 无效Cookie | 数量 | 操作 |
';
+
+ // 展示为一行,类似于API Key列表
+ html += `
+
+ | 无效Cookie |
+ ${invalidCookies.length} |
+
+
+
+ |
+
+ `;
+
+ html += '
';
+ container.innerHTML = html;
+
+ // 添加按钮事件监听
+ document.getElementById('editInvalidCookiesBtn').addEventListener('click', openInvalidCookieModal);
+ document.getElementById('clearAllInvalidCookiesInTable').addEventListener('click', handleClearAllInvalidCookies);
+
+ } catch (error) {
+ container.innerHTML = `加载失败: ${error.message}
`;
+ }
+}
+
+// 处理清除所有无效Cookie按钮事件
+async function handleClearAllInvalidCookies() {
+ try {
+ await clearAllInvalidCookies();
+ showMessage('invalidCookiesContainer', '所有无效Cookie已清除', 'info');
+ renderInvalidCookies(); // 重新渲染列表
+ } catch (error) {
+ showMessage('invalidCookiesContainer', `清除失败: ${error.message}`, 'error');
+ }
+}
+
+// API 测试相关函数
+// 测试API连接
+async function testApiConnection() {
+ const resultDiv = document.getElementById('testApiResult');
+ resultDiv.innerHTML = '正在测试API连接...
';
+
+ try {
+ const response = await fetch('/v1/api-keys', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ resultDiv.innerHTML = `API响应状态: ${response.status}
`;
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ resultDiv.innerHTML += `获取到的数据: ${JSON.stringify(data)}
`;
+ } catch (error) {
+ console.error('测试API失败:', error);
+ resultDiv.innerHTML = `测试API失败: ${error.message}
`;
+ }
+}
+
+// 清除缓存并刷新
+function clearCacheAndRefresh() {
+ // 清除缓存
+ if ('caches' in window) {
+ caches.keys().then(function(names) {
+ for (let name of names) {
+ caches.delete(name);
+ }
+ });
+ }
+
+ // 强制刷新页面(绕过缓存)
+ window.location.reload(true);
+}
+
+// 显示消息的通用函数
+function showMessage(containerId, message, type) {
+ const container = document.getElementById(containerId);
+ container.innerHTML = `${message}
`;
+}
+
+// Cookie刷新相关函数
+// 填充刷新API Key的下拉选择框
+async function populateRefreshApiKeySelect() {
+ try {
+ const apiKeys = await getApiKeys();
+ const select = document.getElementById('refreshApiKey');
+
+ // 清空现有选项(保留"所有API Key"选项)
+ while (select.options.length > 1) {
+ select.remove(1);
+ }
+
+ // 添加API Key选项
+ apiKeys.forEach(key => {
+ const option = document.createElement('option');
+ option.value = key.key;
+ option.textContent = `${key.key} (${key.cookieCount} 个Cookie)`;
+ select.appendChild(option);
+ });
+ } catch (error) {
+ console.error('加载API Key选项失败:', error);
+ }
+}
+
+// 处理刷新Cookie按钮事件
+async function handleRefreshCookie() {
+ console.log('刷新Cookie按钮被点击');
+ const refreshBtn = document.getElementById('refreshCookieBtn');
+ const apiKey = document.getElementById('refreshApiKey').value;
+ const statusContainer = document.getElementById('refreshStatusContainer');
+ const statusText = document.getElementById('refreshStatus');
+ const progressBar = document.getElementById('refreshProgress');
+
+ // 显示调试信息
+ showMessage('refreshCookieMessage', '正在准备发送请求...', 'info');
+
+ // 禁用按钮,显示状态容器
+ refreshBtn.disabled = true;
+ statusContainer.style.display = 'block';
+ statusText.textContent = '正在发送刷新请求...';
+ progressBar.value = 10;
+
+ try {
+ // 构建请求URL
+ let url = '/v1/refresh-cookies';
+ if (apiKey) {
+ url += `?apiKey=${encodeURIComponent(apiKey)}`;
+ }
+
+ // 发送刷新请求
+ statusText.textContent = '正在发送刷新请求...';
+ progressBar.value = 20;
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ // 显示长时间等待提示
+ statusText.textContent = '刷新请求已发送,请耐心等待2-12分钟...';
+ progressBar.value = 50;
+ showMessage('refreshCookieMessage', '刷新请求已发送,由于需要访问Cursor官网获取新Cookie,整个过程可能需要2-12分钟,请耐心等待。您可以关闭此页面,稍后再来查看结果。', 'info');
+
+ // 启动定时检查刷新状态
+ let checkInterval = setInterval(async () => {
+ try {
+ const statusResponse = await fetch('/v1/refresh-status', {
+ method: 'GET',
+ headers: {
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!statusResponse.ok) {
+ throw new Error(`HTTP错误: ${statusResponse.status} ${statusResponse.statusText}`);
+ }
+
+ const statusData = await statusResponse.json();
+ const refreshData = statusData.data;
+
+ // 更新状态信息
+ statusText.textContent = refreshData.message || '正在刷新...';
+
+ // 根据状态更新进度条和UI
+ if (refreshData.status === 'completed') {
+ // 刷新完成
+ progressBar.value = 100;
+ statusText.textContent = `刷新完成: ${refreshData.message}`;
+ clearInterval(checkInterval);
+
+ // 重新加载API Key列表
+ await loadApiKeys();
+ await populateRefreshApiKeySelect();
+
+ // 显示成功消息
+ showMessage('refreshCookieMessage', `刷新完成: ${refreshData.message}`, 'success');
+
+ // 启用按钮
+ refreshBtn.disabled = false;
+
+ // 3秒后隐藏状态容器
+ setTimeout(() => {
+ statusContainer.style.display = 'none';
+ }, 3000);
+ } else if (refreshData.status === 'failed') {
+ // 刷新失败
+ progressBar.value = 0;
+ statusText.textContent = `刷新失败: ${refreshData.message}`;
+ clearInterval(checkInterval);
+
+ // 显示错误消息
+ showMessage('refreshCookieMessage', `刷新失败: ${refreshData.message}`, 'error');
+
+ // 启用按钮
+ refreshBtn.disabled = false;
+ } else if (refreshData.status === 'running') {
+ // 正在刷新
+ progressBar.value = 75;
+ } else if (!refreshData.isRunning) {
+ // 未知状态但不在运行
+ clearInterval(checkInterval);
+ refreshBtn.disabled = false;
+ }
+ } catch (error) {
+ console.error('检查刷新状态失败:', error);
+ }
+ }, 5000); // 每5秒检查一次
+
+ // 设置超时检查,12分钟后如果还没完成就停止检查
+ setTimeout(() => {
+ if (checkInterval) {
+ clearInterval(checkInterval);
+ refreshBtn.disabled = false;
+ statusContainer.style.display = 'none';
+ }
+ }, 720000);
+ } catch (error) {
+ console.error('刷新Cookie失败:', error);
+ statusText.textContent = '刷新请求发送失败';
+ progressBar.value = 0;
+ showMessage('refreshCookieMessage', `刷新请求发送失败: ${error.message}`, 'error');
+ refreshBtn.disabled = false;
+ }
+}
+
+// 获取Cookie相关函数
+// 为Cookie获取功能填充API Key下拉框
+function populateCookieApiKeySelect() {
+ populateRefreshApiKeySelect().then(() => {
+ // 复制refreshApiKey的选项到targetApiKey
+ const sourceSelect = document.getElementById('refreshApiKey');
+ const targetSelect = document.getElementById('targetApiKey');
+
+ // 保留第一个选项("所有API Key")
+ while (targetSelect.options.length > 1) {
+ targetSelect.remove(1);
+ }
+
+ // 复制选项
+ for (let i = 1; i < sourceSelect.options.length; i++) {
+ const option = document.createElement('option');
+ option.value = sourceSelect.options[i].value;
+ option.textContent = sourceSelect.options[i].textContent;
+ targetSelect.appendChild(option);
+ }
+ });
+}
+
+// 处理生成登录链接
+async function handleGenerateLink() {
+ console.log('生成登录链接按钮被点击');
+ const messageContainer = document.getElementById('getCookieMessage');
+ const linkContainer = document.getElementById('loginLinkContainer');
+ const loginLink = document.getElementById('loginLink');
+ const pollStatusText = document.getElementById('pollStatusText');
+ const pollProgress = document.getElementById('pollProgress');
+ const targetApiKey = document.getElementById('targetApiKey').value;
+
+ try {
+ // 显示加载状态
+ messageContainer.innerHTML = '正在生成登录链接...
';
+
+ // 请求生成登录链接
+ const response = await fetch('/v1/generate-cookie-link', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ },
+ body: JSON.stringify({ apiKey: targetApiKey })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ if (!data.success) {
+ throw new Error(data.message || '生成链接失败');
+ }
+
+ // 显示链接
+ loginLink.href = data.url;
+ loginLink.textContent = data.url;
+ linkContainer.style.display = 'block';
+
+ // 更新状态
+ pollStatusText.textContent = '等待用户登录...';
+ pollProgress.value = 10;
+
+ // 开始轮询获取cookie状态
+ messageContainer.innerHTML = '链接已生成,请点击链接登录Cursor账号并授权
';
+
+ // 开始轮询cookie状态
+ pollForCookieStatus(data.uuid);
+
+ } catch (error) {
+ console.error('生成登录链接失败:', error);
+ messageContainer.innerHTML = `生成链接失败: ${error.message}
`;
+ }
+}
+
+// 轮询Cookie获取状态
+function pollForCookieStatus(uuid) {
+ const messageContainer = document.getElementById('getCookieMessage');
+ const pollStatusText = document.getElementById('pollStatusText');
+ const pollProgress = document.getElementById('pollProgress');
+ const maxAttempts = 300; // 最多尝试300次,相当于5分钟(原来是120次,2分钟)
+ let attempt = 0;
+
+ // 更新状态显示
+ pollStatusText.textContent = '等待用户登录...';
+
+ const interval = setInterval(function() {
+ attempt++;
+
+ try {
+ // 更新进度条(保持在10%-90%之间,表示等待中)
+ pollProgress.value = 10 + Math.min(80, attempt / 3.75); // 调整进度条速度以适应5分钟
+
+ // 请求检查状态
+ fetch(`/v1/check-cookie-status?uuid=${encodeURIComponent(uuid)}`, {
+ method: 'GET',
+ headers: {
+ 'Cache-Control': 'no-cache'
+ }
+ }).then(function(response) {
+ if (!response.ok) {
+ pollStatusText.textContent = `请求失败: ${response.status}`;
+ return;
+ }
+
+ return response.json();
+ }).then(function(data) {
+ if (data.success) {
+ // Cookie获取成功
+ clearInterval(interval);
+ pollProgress.value = 100;
+ pollStatusText.textContent = '获取Cookie成功!';
+ messageContainer.innerHTML = `成功获取并添加Cookie!${data.message || ''}
`;
+
+ // 刷新API Keys列表
+ loadApiKeys();
+ populateCookieApiKeySelect();
+
+ } else if (data.status === 'waiting') {
+ // 继续等待
+ pollStatusText.textContent = '等待用户登录...';
+ } else if (data.status === 'failed') {
+ // 获取失败
+ clearInterval(interval);
+ pollStatusText.textContent = '获取失败';
+ pollProgress.value = 0;
+ messageContainer.innerHTML = `获取Cookie失败: ${data.message || '未知错误'}
`;
+ }
+ }).catch(function(error) {
+ console.error('轮询Cookie状态失败:', error);
+ pollStatusText.textContent = `轮询出错: ${error.message}`;
+ });
+
+ } catch (error) {
+ console.error('轮询Cookie状态出错:', error);
+ pollStatusText.textContent = `轮询出错: ${error.message}`;
+ }
+
+ // 达到最大尝试次数后停止
+ if (attempt >= maxAttempts) {
+ clearInterval(interval);
+ pollStatusText.textContent = '超时,请重试';
+ pollProgress.value = 0;
+ messageContainer.innerHTML = '获取Cookie超时,请重新尝试
';
+ }
+
+ }, 1000); // 每秒轮询一次
+}
+
+// 授权相关函数
+// 检查登录状态
+function checkAuth() {
+ const token = localStorage.getItem('adminToken');
+ if (!token) {
+ window.location.href = '/login.html';
+ return;
+ }
+
+ // 验证token
+ fetch('/v1/admin/verify', {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (!data.success) {
+ localStorage.removeItem('adminToken');
+ window.location.href = '/login.html';
+ } else {
+ // 更新为新的用户名显示方式
+ const usernameElem = document.getElementById('usernameText');
+ if (usernameElem) {
+ usernameElem.textContent = data.username;
+ } else {
+ // 兼容旧版模板,可能没有usernameText元素
+ const adminElem = document.getElementById('adminUsername');
+ if (adminElem) {
+ adminElem.textContent = `管理员:${data.username}`;
+ }
+ }
+ }
+ })
+ .catch(error => {
+ console.error('验证失败:', error);
+ localStorage.removeItem('adminToken');
+ window.location.href = '/login.html';
+ });
+}
+
+// 处理退出登录
+function handleLogout() {
+ localStorage.removeItem('adminToken');
+ window.location.href = '/login.html';
+}
+
+// 添加token到所有API请求
+function addAuthHeader(headers = {}) {
+ const token = localStorage.getItem('adminToken');
+ return {
+ ...headers,
+ 'Authorization': `Bearer ${token}`
+ };
+}
+
+// 修改所有fetch请求,添加token
+(function() {
+ const originalFetch = window.fetch;
+ window.fetch = function(url, options = {}) {
+ // 只对管理页面的API请求添加token
+ if (url.includes('/v1/api-keys') ||
+ url.includes('/v1/invalid-cookies') ||
+ url.includes('/v1/refresh-cookies') ||
+ url.includes('/v1/generate-cookie-link') ||
+ url.includes('/v1/check-cookie-status') ||
+ url.includes('/v1/logs')) {
+ options.headers = addAuthHeader(options.headers);
+ }
+ return originalFetch(url, options);
+ };
+})();
+
+// 无效Cookie模态窗口相关函数
+// 打开无效Cookie模态窗口
+async function openInvalidCookieModal() {
+ try {
+ document.getElementById('invalidCookieModalMessage').innerHTML = '';
+ const invalidCookies = await getInvalidCookies();
+ renderInvalidCookieTags(invalidCookies);
+ document.getElementById('invalidCookiesValues').value = invalidCookies.join(',');
+ document.getElementById('newInvalidCookie').value = '';
+ const modal = document.getElementById('invalidCookieModal');
+ modal.style.display = 'block';
+ document.body.classList.add('modal-open');
+ } catch (error) {
+ console.error('打开无效Cookie模态框失败:', error);
+ showMessage('invalidCookiesContainer', `加载无效Cookie失败: ${error.message}`, 'error');
+ }
+}
+
+// 关闭无效Cookie模态框
+function closeInvalidCookieModal() {
+ const modal = document.getElementById('invalidCookieModal');
+ modal.style.display = 'none';
+ document.body.classList.remove('modal-open');
+}
+
+// 渲染无效Cookie标签
+function renderInvalidCookieTags(cookies) {
+ const container = document.getElementById('invalidCookieTagsContainer');
+ container.innerHTML = '';
+
+ if (cookies.length === 0) {
+ container.innerHTML = '暂无无效Cookie
';
+ return;
+ }
+
+ cookies.forEach((cookie, index) => {
+ // 创建标签
+ const tag = document.createElement('span');
+ tag.className = 'cookie-tag';
+
+ // 对短文本添加特殊类
+ if (cookie.length < 5) {
+ tag.classList.add('short-cookie');
+ }
+
+ // 截断Cookie显示
+ const displayText = cookie.length > 20 ?
+ cookie.substring(0, 8) + '...' + cookie.substring(cookie.length - 8) :
+ cookie;
+
+ tag.title = cookie; // 完整Cookie作为工具提示
+
+ // 修改样式,使用与API Key相同的删除按钮样式
+ tag.innerHTML = `
+ ${displayText}
+
+
+
+
+ `;
+ container.appendChild(tag);
+ });
+
+ // 添加删除按钮的事件监听
+ document.querySelectorAll('#invalidCookieTagsContainer .delete-cookie').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const index = parseInt(this.getAttribute('data-index'));
+ deleteInvalidCookieTag(index);
+ });
+ });
+
+ // 添加复制按钮的事件监听
+ document.querySelectorAll('#invalidCookieTagsContainer .copy-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const cookie = this.getAttribute('data-cookie');
+ handleCopyCookie(cookie);
+ });
+ });
+}
+
+// 删除无效Cookie标签
+function deleteInvalidCookieTag(index) {
+ // 从隐藏的textarea中获取当前的cookies
+ const cookieValuesElem = document.getElementById('invalidCookiesValues');
+ let cookies = cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c);
+
+ // 删除指定索引的cookie
+ cookies.splice(index, 1);
+
+ // 更新隐藏的textarea
+ cookieValuesElem.value = cookies.join(',');
+
+ // 重新渲染标签
+ renderInvalidCookieTags(cookies);
+}
+
+// 处理添加新无效Cookie
+function handleAddInvalidCookie() {
+ const newCookieInput = document.getElementById('newInvalidCookie');
+ const newCookie = newCookieInput.value.trim();
+
+ if (!newCookie) {
+ return;
+ }
+
+ // 获取当前的cookies
+ const cookieValuesElem = document.getElementById('invalidCookiesValues');
+ let cookies = cookieValuesElem.value ?
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
+ [];
+
+ // 添加新cookie
+ cookies.push(newCookie);
+
+ // 更新隐藏的textarea
+ cookieValuesElem.value = cookies.join(',');
+
+ // 重新渲染标签
+ renderInvalidCookieTags(cookies);
+
+ // 清空输入框
+ newCookieInput.value = '';
+}
+
+// 处理无效Cookie编辑表单提交
+async function handleInvalidCookieForm(e) {
+ e.preventDefault();
+
+ const cookieValuesText = document.getElementById('invalidCookiesValues').value.trim();
+
+ // 将逗号分隔的 Cookie 值转换为数组
+ const invalidCookies = cookieValuesText ?
+ cookieValuesText.split(',').map(cookie => cookie.trim()).filter(cookie => cookie) :
+ [];
+
+ try {
+ // 先清除所有无效Cookie
+ await clearAllInvalidCookies();
+
+ // 如果有新的无效Cookie,逐个添加
+ if (invalidCookies.length > 0) {
+ // 假设API提供了批量添加无效Cookie的接口
+ const response = await fetch('/v1/invalid-cookies', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ invalidCookies,
+ }),
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
+ 无效Cookie修改成功
+ `;
+ setTimeout(() => {
+ closeInvalidCookieModal();
+ renderInvalidCookies(); // 重新渲染列表
+ }, 1500);
+ } else {
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
+ 无效Cookie修改失败: ${data.error}
+ `;
+ }
+ } else {
+ // 如果清空了所有无效Cookie
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
+ 已清空所有无效Cookie
+ `;
+ setTimeout(() => {
+ closeInvalidCookieModal();
+ renderInvalidCookies(); // 重新渲染列表
+ }, 1500);
+ }
+ } catch (error) {
+ console.error('修改无效Cookie失败:', error);
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
+ 修改无效Cookie失败: ${error.message}
+ `;
+ }
+}
\ No newline at end of file
diff --git a/src/public/styles.css b/src/public/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..41f4ad111b12c9ed502f9f512d8db480b4e66141
--- /dev/null
+++ b/src/public/styles.css
@@ -0,0 +1,1270 @@
+/* iOS风格现代化样式 */
+:root {
+ --ios-background: #f2f2f7;
+ --ios-card-background: #ffffff;
+ --ios-blue: #007aff;
+ --ios-green: #34c759;
+ --ios-red: #ff3b30;
+ --ios-yellow: #ffcc00;
+ --ios-gray: #8e8e93;
+ --ios-light-gray: #d1d1d6;
+ --ios-text-primary: #000000;
+ --ios-text-secondary: #6c6c70;
+ --ios-border-radius: 10px;
+ --ios-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+ --ios-padding: 16px;
+}
+
+/* 夜间模式变量 */
+[data-theme="dark"] {
+ --ios-background: #000000;
+ --ios-card-background: #1c1c1e;
+ --ios-blue: #0a84ff;
+ --ios-green: #30d158;
+ --ios-red: #ff453a;
+ --ios-yellow: #ffd60a;
+ --ios-gray: #8e8e93;
+ --ios-light-gray: #38383a;
+ --ios-text-primary: #ffffff;
+ --ios-text-secondary: #adadb5;
+ --ios-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+/* 防止初始加载时主题切换闪烁 */
+.no-transition * {
+ transition: none !important;
+}
+
+/* 主题切换时的平滑过渡效果 */
+.theme-transition {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+.theme-transition * {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+/* 主题切换按钮样式 */
+.theme-switch {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: var(--ios-card-background);
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: var(--ios-shadow);
+ z-index: 1000;
+ border: 1px solid rgba(142, 142, 147, 0.1);
+ transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+ overflow: hidden;
+}
+
+/* 主题切换按钮动画 */
+.theme-switch-animate {
+ transform: rotate(180deg) scale(1.1);
+ box-shadow: 0 0 20px rgba(var(--ios-blue-rgb, 0, 122, 255), 0.5);
+}
+
+/* 主题图标动画 */
+.theme-icon {
+ width: 24px;
+ height: 24px;
+ color: var(--ios-text-primary);
+ transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1),
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+.theme-icon.dark-mode {
+ transform: rotate(360deg) scale(1.1);
+}
+
+.theme-switch:hover {
+ transform: scale(1.05);
+ box-shadow: 0 0 15px rgba(var(--ios-blue-rgb), 0.3);
+}
+
+.theme-switch:active {
+ transform: scale(0.95);
+}
+
+/* 提取颜色的RGB值用于动画和透明度 */
+:root {
+ --ios-blue-rgb: 0, 122, 255;
+ --ios-green-rgb: 52, 199, 89;
+ --ios-red-rgb: 255, 59, 48;
+ --ios-yellow-rgb: 255, 204, 0;
+}
+
+[data-theme="dark"] {
+ --ios-blue-rgb: 10, 132, 255;
+ --ios-green-rgb: 48, 209, 88;
+ --ios-red-rgb: 255, 69, 58;
+ --ios-yellow-rgb: 255, 214, 10;
+}
+
+/* 适配夜间模式的样式调整 */
+[data-theme="dark"] .header-card {
+ background: linear-gradient(-45deg, #30d158, #0a84ff, #5e5ce6, #64d2ff);
+ background-size: 300% 300%;
+ animation: gradientAnimation 12s ease infinite;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ border: none;
+}
+
+[data-theme="dark"] .header-card::after {
+ background: linear-gradient(120deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 60%);
+}
+
+[data-theme="dark"] .header-card,
+[data-theme="dark"] .header-card h1,
+[data-theme="dark"] .header-card p {
+ color: white;
+}
+
+[data-theme="dark"] .cookie-tag {
+ background-color: rgba(10, 132, 255, 0.15);
+ border: 1px solid rgba(10, 132, 255, 0.2);
+}
+
+[data-theme="dark"] .cookie-tag:hover {
+ background-color: rgba(10, 132, 255, 0.25);
+}
+
+[data-theme="dark"] .delete-cookie,
+[data-theme="dark"] .delete-add-cookie {
+ background-color: rgba(255, 69, 58, 0.2);
+}
+
+[data-theme="dark"] .delete-cookie:hover,
+[data-theme="dark"] .delete-add-cookie:hover {
+ background-color: rgba(255, 69, 58, 0.3);
+}
+
+[data-theme="dark"] .copy-btn {
+ background: rgba(142, 142, 147, 0.2);
+}
+
+[data-theme="dark"] .copy-btn:hover {
+ background-color: rgba(10, 132, 255, 0.2);
+}
+
+[data-theme="dark"] .sticky-actions {
+ background: rgba(28, 28, 30, 0.8);
+}
+
+[data-theme="dark"] .modal-content {
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+}
+
+[data-theme="dark"] input,
+[data-theme="dark"] textarea,
+[data-theme="dark"] select {
+ background-color: rgba(142, 142, 147, 0.12);
+}
+
+[data-theme="dark"] input:focus,
+[data-theme="dark"] textarea:focus,
+[data-theme="dark"] select:focus {
+ background-color: rgba(142, 142, 147, 0.18);
+ box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.25);
+}
+
+/* 主题切换动画 */
+body {
+ transition: background-color 0.3s ease;
+}
+
+.card, input, textarea, select, button, th, td, .modal-content, .cookie-tag, .sticky-actions {
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+html {
+ scroll-behavior: smooth;
+ height: -webkit-fill-available;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
+ line-height: 1.5;
+ margin: 0;
+ padding: 16px;
+ color: var(--ios-text-primary);
+ max-width: 960px;
+ margin: 0 auto;
+ background-color: var(--ios-background);
+ font-size: 16px;
+ font-weight: 400;
+}
+
+h1, h2 {
+ color: var(--ios-text-primary);
+ margin-top: 0;
+ font-weight: 600;
+ letter-spacing: -0.5px;
+}
+
+h1 {
+ font-size: 28px;
+ margin-bottom: 8px;
+}
+
+h2 {
+ font-size: 22px;
+ margin-bottom: 16px;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.card {
+ background: var(--ios-card-background);
+ border-radius: var(--ios-border-radius);
+ box-shadow: var(--ios-shadow);
+ padding: var(--ios-padding);
+ transition: transform 0.2s, box-shadow 0.2s;
+ border: 1px solid rgba(0, 0, 0, 0.04);
+ overflow: hidden;
+}
+
+.card:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: var(--ios-text-primary);
+ font-size: 15px;
+}
+
+input, textarea, select {
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid var(--ios-light-gray);
+ border-radius: 8px;
+ font-size: 16px;
+ transition: all 0.2s;
+ box-sizing: border-box;
+ background-color: rgba(142, 142, 147, 0.06);
+ color: var(--ios-text-primary);
+ appearance: none;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+}
+
+input:focus, textarea:focus, select:focus {
+ border-color: var(--ios-blue);
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
+ background-color: var(--ios-card-background);
+}
+
+textarea {
+ min-height: 100px;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+ line-height: 1.5;
+}
+
+button {
+ background: var(--ios-blue);
+ color: white;
+ border: none;
+ padding: 12px 18px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 16px;
+ transition: all 0.2s;
+ font-weight: 500;
+ -webkit-tap-highlight-color: transparent;
+ text-align: center;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+button:hover {
+ background: #0062cc;
+ transform: translateY(-1px);
+}
+
+button:active {
+ transform: translateY(1px);
+ opacity: 0.9;
+}
+
+table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ margin-bottom: 16px;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid var(--ios-light-gray);
+}
+
+th, td {
+ padding: 14px 16px;
+ text-align: left;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+th {
+ background-color: rgba(142, 142, 147, 0.06);
+ font-weight: 600;
+ color: var(--ios-text-primary);
+ font-size: 14px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+tr:last-child td {
+ border-bottom: none;
+}
+
+tr:hover {
+ background-color: rgba(0, 122, 255, 0.03);
+}
+
+.action-btn {
+ background: var(--ios-red);
+ margin-right: 8px;
+ font-size: 14px;
+ padding: 8px 12px;
+}
+
+.action-btn:hover {
+ background: #e02e24;
+}
+
+.edit-btn {
+ background: var(--ios-blue);
+ margin-right: 8px;
+ font-size: 14px;
+ padding: 8px 12px;
+}
+
+.edit-btn:hover {
+ background: #0062cc;
+}
+
+.info {
+ background-color: rgba(52, 199, 89, 0.1);
+ color: var(--ios-text-primary);
+ padding: 14px;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ border-left: 3px solid var(--ios-green);
+ font-size: 15px;
+}
+
+.error {
+ background-color: rgba(255, 59, 48, 0.1);
+ color: var(--ios-text-primary);
+ padding: 14px;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ border-left: 3px solid var(--ios-red);
+ font-size: 15px;
+}
+
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(5px);
+ -webkit-backdrop-filter: blur(5px);
+}
+
+.modal-content {
+ background-color: var(--ios-card-background);
+ margin: 10% auto;
+ padding: 24px;
+ border: none;
+ width: 85%;
+ max-width: 500px;
+ border-radius: 14px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ animation: modalFadeIn 0.3s;
+}
+
+@keyframes modalFadeIn {
+ from { opacity: 0; transform: translateY(-20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.close {
+ color: var(--ios-gray);
+ float: right;
+ font-size: 24px;
+ font-weight: 300;
+ cursor: pointer;
+ margin-top: -5px;
+ transition: all 0.2s;
+}
+
+.close:hover,
+.close:focus {
+ color: var(--ios-text-primary);
+ text-decoration: none;
+}
+
+.cookie-text {
+ word-break: break-all;
+ font-size: 14px;
+ font-family: monospace;
+}
+
+.cookie-tag {
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: rgba(0, 122, 255, 0.08);
+ padding: 8px 12px;
+ border-radius: 8px;
+ margin: 0 0 8px 0;
+ width: 100%;
+ position: relative;
+ transition: all 0.2s;
+ border: 1px solid rgba(0, 122, 255, 0.12);
+}
+
+.cookie-tag:hover {
+ background-color: rgba(0, 122, 255, 0.12);
+}
+
+.delete-cookie, .delete-add-cookie {
+ background-color: rgba(255, 59, 48, 0.1);
+ color: var(--ios-red);
+ border: none;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ margin-left: 8px;
+ padding: 0;
+ line-height: 1;
+ transition: all 0.2s;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.delete-cookie:hover, .delete-add-cookie:hover {
+ background-color: rgba(255, 59, 48, 0.2);
+ transform: scale(1.05);
+}
+
+.delete-cookie:active, .delete-add-cookie:active {
+ transform: scale(0.95);
+ opacity: 0.9;
+}
+
+.cookie-text-content {
+ max-width: calc(100% - 70px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--ios-text-primary);
+}
+
+.cookies-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 15px;
+ padding: 5px 0;
+}
+
+.add-cookie-btn {
+ padding: 10px 14px;
+ background-color: var(--ios-blue);
+ transition: all 0.2s ease;
+}
+
+.add-cookie-btn:hover {
+ background-color: #0062cc;
+}
+
+progress {
+ width: 100%;
+ height: 8px;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+progress::-webkit-progress-bar {
+ background-color: rgba(142, 142, 147, 0.2);
+ border-radius: 4px;
+}
+
+progress::-webkit-progress-value {
+ background-color: var(--ios-blue);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+}
+
+/* 梦幻动态渐变背景动画 */
+@keyframes gradientAnimation {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ 100% {
+ background-position: 0% 50%;
+ }
+}
+
+.header-card {
+ background: linear-gradient(-45deg, #4CD964, #5ac8fa, #34C759, #5ac8fa);
+ background-size: 300% 300%;
+ animation: gradientAnimation 12s ease infinite;
+ color: var(--ios-text-primary);
+ position: relative;
+ overflow: hidden;
+ padding: 20px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border: none;
+}
+
+.header-card::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(120deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 60%);
+ pointer-events: none;
+}
+
+.header-card h1, .header-card p {
+ color: var(--ios-text-primary);
+}
+
+.sticky-actions {
+ position: sticky;
+ bottom: 20px;
+ background: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ padding: 15px;
+ border-radius: 12px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
+ margin-top: 20px;
+ z-index: 100;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.cookie-tag.short-cookie {
+ max-width: 180px;
+}
+
+.copy-btn {
+ background: rgba(142, 142, 147, 0.1);
+ color: var(--ios-blue);
+ border: none;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ margin-left: 6px;
+ padding: 0;
+ transition: all 0.2s;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.copy-btn:hover {
+ background-color: rgba(0, 122, 255, 0.1);
+ transform: scale(1.05);
+}
+
+.copy-btn:active {
+ transform: scale(0.95);
+ opacity: 0.9;
+}
+
+.delete-cookie, .delete-add-cookie {
+ background-color: rgba(255, 59, 48, 0.1);
+ color: var(--ios-red);
+ border: none;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ margin-left: 8px;
+ padding: 0;
+ line-height: 1;
+ transition: all 0.2s;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.delete-cookie:hover, .delete-add-cookie:hover {
+ background-color: rgba(255, 59, 48, 0.2);
+ transform: scale(1.05);
+}
+
+.delete-cookie:active, .delete-add-cookie:active {
+ transform: scale(0.95);
+ opacity: 0.9;
+}
+
+.cookie-buttons {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+/* 适配移动端的样式 */
+@media screen and (max-width: 768px) {
+ body {
+ padding: 12px;
+ font-size: 15px;
+ }
+
+ h1 {
+ font-size: 24px;
+ }
+
+ h2 {
+ font-size: 20px;
+ }
+
+ .card {
+ padding: 16px;
+ }
+
+ input, textarea, select {
+ padding: 10px 12px;
+ font-size: 16px;
+ }
+
+ button {
+ padding: 12px 16px;
+ width: 100%;
+ margin-bottom: 8px;
+ }
+
+ /* 优化添加按钮在移动端的宽度 */
+ .add-cookie-btn {
+ width: auto;
+ min-width: 42px;
+ max-width: 42px;
+ padding: 10px;
+ margin-bottom: 0;
+ flex: 0 0 auto;
+ }
+
+ .add-cookie-btn i {
+ margin: 0;
+ }
+
+ /* 让输入框占据更多宽度 */
+ #addNewCookie,
+ #newCookie,
+ #newInvalidCookie {
+ flex: 1;
+ }
+
+ /* 优化表单布局 */
+ div[style*="display: flex; gap: 10px;"] {
+ gap: 8px !important;
+ }
+
+ /* 优化Cookie标签在移动端的显示 */
+ .cookie-tag {
+ font-size: 13px;
+ padding: 6px 10px;
+ margin: 0 0 8px 0;
+ width: 100%;
+ }
+
+ /* 让Cookie内容区域尽可能宽 */
+ .cookie-text-content {
+ max-width: calc(100% - 68px);
+ }
+
+ /* 优化复制与删除按钮 */
+ .cookie-buttons {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+ }
+
+ .copy-btn,
+ .delete-cookie,
+ .delete-add-cookie {
+ width: 28px;
+ height: 28px;
+ margin-left: 4px;
+ background-color: rgba(142, 142, 147, 0.15);
+ transition: all 0.15s ease;
+ }
+
+ .copy-btn:active,
+ .delete-cookie:active,
+ .delete-add-cookie:active {
+ transform: scale(0.92);
+ opacity: 0.8;
+ }
+
+ .cookie-tag.short-cookie {
+ max-width: calc(100% - 16px);
+ }
+
+ th, td {
+ padding: 12px;
+ font-size: 14px;
+ }
+
+ .modal-content {
+ width: 92%;
+ padding: 20px;
+ margin: 15% auto 5%;
+ }
+
+ /* 在小屏幕上重新排列表格 */
+ table, thead, tbody, th, td, tr {
+ display: block;
+ }
+
+ thead tr {
+ position: absolute;
+ top: -9999px;
+ left: -9999px;
+ }
+
+ tr {
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ margin-bottom: 10px;
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ td {
+ border: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ position: relative;
+ padding-left: 50%;
+ white-space: normal;
+ text-align: left;
+ }
+
+ td:before {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ width: 45%;
+ padding-right: 10px;
+ white-space: nowrap;
+ text-align: left;
+ font-weight: 600;
+ content: attr(data-title);
+ color: var(--ios-text-secondary);
+ font-size: 13px;
+ }
+
+ /* 设置每个单元格的标题 */
+ #keyTable td:nth-of-type(1):before { content: "API Key"; }
+ #keyTable td:nth-of-type(2):before { content: "Cookie 数量"; }
+ #keyTable td:nth-of-type(3):before { content: "操作"; }
+
+ .action-btn, .edit-btn {
+ margin-right: 8px;
+ font-size: 14px;
+ padding: 8px 12px;
+ width: auto;
+ display: inline-flex;
+ }
+
+ .form-input-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .form-input-group input {
+ width: 100%;
+ }
+
+ .form-input-group button {
+ width: 100%;
+ }
+
+ .header-card div[style*="display: flex; justify-content: space-between;"] {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .header-card div[style*="display: flex; justify-content: space-between;"] div {
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .header-card div[style*="display: flex; justify-content: space-between;"] button {
+ flex: 1;
+ min-width: 0;
+ }
+
+ #adminUsername {
+ display: block;
+ width: 100%;
+ margin-bottom: 8px;
+ text-align: center;
+ }
+}
+
+/* 为header-card内的按钮添加特殊样式 */
+.header-card button {
+ background-color: rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(5px);
+ -webkit-backdrop-filter: blur(5px);
+ color: var(--ios-text-primary);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.header-card button:hover {
+ background-color: rgba(255, 255, 255, 0.25);
+ transform: translateY(-2px);
+}
+
+.header-card button.danger {
+ background-color: #ff3b30;
+ color: white;
+ border: none;
+}
+
+.header-card button.danger:hover {
+ background-color: #ff2d20;
+}
+
+[data-theme="dark"] .header-card button {
+ background-color: rgba(255, 255, 255, 0.15);
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+[data-theme="dark"] .header-card button:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+/* 用户信息样式 */
+.header-card .user-info {
+ display: flex;
+ align-items: center;
+ margin: 15px 0;
+}
+
+.header-card .user-info i {
+ margin-right: 8px;
+ opacity: 0.9;
+}
+
+/* 退出按钮特殊样式 */
+.header-card .logout-btn {
+ background-color: rgba(255, 59, 48, 0.9);
+ color: white;
+ border: none;
+ width: 100%;
+ padding: 12px;
+ margin-top: 10px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.header-card .logout-btn:hover {
+ background-color: rgba(255, 59, 48, 1);
+ transform: translateY(-2px);
+}
+
+[data-theme="dark"] .header-card .logout-btn {
+ background-color: rgba(255, 69, 58, 0.8);
+}
+
+[data-theme="dark"] .header-card .logout-btn:hover {
+ background-color: rgba(255, 69, 58, 0.9);
+}
+
+/* 按钮组样式 */
+.header-card .button-group {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+}
+
+.header-card .button-group button {
+ flex: 1;
+}
+
+/* 用户名颜色随主题切换 */
+#adminUsername {
+ color: var(--ios-text-primary) !important;
+}
+
+/* 优化主题切换动画 - 添加在文件末尾 */
+/* 提取颜色的RGB值用于动画和透明度 */
+:root {
+ --ios-blue-rgb: 0, 122, 255;
+ --ios-green-rgb: 52, 199, 89;
+ --ios-red-rgb: 255, 59, 48;
+ --ios-yellow-rgb: 255, 204, 0;
+}
+
+[data-theme="dark"] {
+ --ios-blue-rgb: 10, 132, 255;
+ --ios-green-rgb: 48, 209, 88;
+ --ios-red-rgb: 255, 69, 58;
+ --ios-yellow-rgb: 255, 214, 10;
+}
+
+/* 防止初始加载时主题切换闪烁 */
+.no-transition * {
+ transition: none !important;
+}
+
+/* 主题切换时的平滑过渡效果 */
+.theme-transition {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+.theme-transition * {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+/* 主题切换按钮动画 */
+.theme-switch-animate {
+ transform: rotate(180deg) scale(1.1);
+ box-shadow: 0 0 20px rgba(var(--ios-blue-rgb), 0.5);
+}
+
+/* 主题图标动画 */
+.theme-icon {
+ width: 24px;
+ height: 24px;
+ color: var(--ios-text-primary);
+ transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1),
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+.theme-icon.dark-mode {
+ transform: rotate(360deg) scale(1.1);
+}
+
+.theme-switch:hover {
+ transform: scale(1.05);
+ box-shadow: 0 0 15px rgba(var(--ios-blue-rgb), 0.3);
+}
+
+.theme-switch:active {
+ transform: scale(0.95);
+}
+
+/* 优化特定元素的过渡效果 */
+.card {
+ transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
+ background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+button {
+ transition: background-color 0.3s cubic-bezier(0.25, 1, 0.5, 1),
+ transform 0.2s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.3s cubic-bezier(0.25, 1, 0.5, 1),
+ opacity 0.2s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+input, textarea, select {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+.cookie-tag, .delete-cookie, .delete-add-cookie, .copy-btn {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ transform 0.2s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+/* 添加主题切换时的特殊动画效果 */
+.theme-transition .card {
+ animation: card-theme-shift 0.8s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+@keyframes card-theme-shift {
+ 0% { transform: translateY(0); }
+ 50% { transform: translateY(-5px); }
+ 100% { transform: translateY(0); }
+}
+
+/* 确保元素在滚动和动画期间的流畅渲染 */
+.card, .theme-switch, .modal-content, input, button {
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+ transform-style: preserve-3d;
+ -webkit-transform-style: preserve-3d;
+ will-change: transform, opacity;
+}
+
+/* 调整主题切换动画效果 */
+.theme-transition body {
+ animation: bg-fade 0.8s cubic-bezier(0.25, 1, 0.5, 1);
+}
+
+@keyframes bg-fade {
+ 0% { opacity: 0.98; }
+ 50% { opacity: 0.95; }
+ 100% { opacity: 1; }
+}
+
+/* 侧边导航栏样式 */
+.side-nav-trigger {
+ position: fixed;
+ right: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1000;
+ width: 12px;
+ height: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+ border-radius: 50%;
+ background-color: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ box-shadow: 0 0 5px rgba(0, 122, 255, 0.2);
+}
+
+.trigger-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: rgba(88, 166, 255, 0.8);
+ box-shadow: 0 0 4px rgba(88, 166, 255, 0.5);
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+}
+
+.side-nav-trigger:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.side-nav-trigger:hover .trigger-dot {
+ background-color: var(--ios-blue);
+ box-shadow: 0 0 8px rgba(0, 122, 255, 0.7);
+}
+
+.side-nav-menu {
+ position: fixed;
+ right: 40px;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 999;
+ background-color: rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(25px);
+ -webkit-backdrop-filter: blur(25px);
+ border-radius: 16px;
+ padding: 15px 10px;
+ width: 200px;
+ max-height: 80vh;
+ overflow-y: auto;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.15);
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+}
+
+.side-nav-trigger:hover + .side-nav-menu,
+.side-nav-menu:hover {
+ opacity: 1;
+ visibility: visible;
+}
+
+.side-nav-content {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 10px;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.nav-item:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.nav-item.active {
+ background-color: rgba(0, 122, 255, 0.1);
+}
+
+.nav-item-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: rgba(158, 158, 158, 0.8);
+ margin-right: 10px;
+ transition: all 0.2s ease;
+}
+
+.nav-item.active .nav-item-dot {
+ background-color: var(--ios-blue);
+ box-shadow: 0 0 5px rgba(0, 122, 255, 0.5);
+}
+
+/* 非活动条目使用浅灰色文字 */
+.nav-item-title {
+ color: var(--ios-gray);
+ font-size: 14px;
+ font-weight: 400;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ letter-spacing: -0.2px;
+ transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
+}
+
+/* 当前激活条目使用主要文字颜色并加粗 */
+.nav-item.active .nav-item-title {
+ color: var(--ios-blue);
+ font-weight: 600;
+}
+
+/* 鼠标悬停时字体放大加粗 */
+.nav-item:hover .nav-item-title {
+ transform: scale(1.05);
+ font-weight: 500;
+ color: var(--ios-text-primary);
+}
+
+@media (max-width: 768px) {
+ .side-nav-trigger {
+ width: 14px;
+ height: 14px;
+ }
+
+ .trigger-dot {
+ width: 7px;
+ height: 7px;
+ }
+
+ .side-nav-menu {
+ right: 40px;
+ width: 180px;
+ }
+
+ /* 移动端点击显示目录 */
+ .side-nav-trigger.touch-active + .side-nav-menu,
+ .side-nav-menu.touch-active {
+ opacity: 1;
+ visibility: visible;
+ }
+}
+
+/* 暗色模式自适应 */
+@media (prefers-color-scheme: dark) {
+ .side-nav-trigger {
+ background-color: rgba(30, 30, 30, 0.3);
+ }
+
+ .side-nav-trigger:hover {
+ background-color: rgba(30, 30, 30, 0.5);
+ }
+
+ .side-nav-menu {
+ background-color: rgba(30, 30, 30, 0.08);
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.08);
+ }
+
+ .nav-item:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+ }
+
+ .nav-item.active {
+ background-color: rgba(10, 132, 255, 0.15);
+ }
+}
+
+[data-theme="dark"] .nav-item-title {
+ color: var(--ios-gray);
+}
+
+[data-theme="dark"] .nav-item.active .nav-item-title {
+ color: var(--ios-blue);
+}
+
+[data-theme="dark"] .nav-item:hover .nav-item-title {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+input, textarea, select {
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
+ box-shadow 0.3s cubic-bezier(0.25, 1, 0.5, 1);
+}
\ No newline at end of file
diff --git a/src/public/theme.js b/src/public/theme.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7053f456ce5ee50253555009afa037bf1b4c6b2
--- /dev/null
+++ b/src/public/theme.js
@@ -0,0 +1,290 @@
+// 主题管理系统
+
+// 在HTML解析前应用主题,防止初始闪烁
+(function() {
+ // 尝试从localStorage读取用户主题偏好
+ const savedTheme = localStorage.getItem('userThemePreference');
+
+ // 如果有保存的主题偏好,立即应用
+ if (savedTheme) {
+ document.documentElement.setAttribute('data-theme', savedTheme);
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ // 检查系统主题
+ document.documentElement.setAttribute('data-theme', 'dark');
+ } else {
+ // 检查当前时间
+ const currentHour = new Date().getHours();
+ if (currentHour >= 19 || currentHour < 7) {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ }
+ }
+
+ // 添加类以防止过渡效果在页面加载时触发
+ document.documentElement.classList.add('no-transition');
+})();
+
+document.addEventListener('DOMContentLoaded', () => {
+ // 创建主题切换按钮
+ createThemeToggle();
+
+ // 初始化主题
+ initTheme();
+
+ // 监听系统主题变化
+ listenForSystemThemeChanges();
+
+ // 移除阻止过渡效果的类
+ setTimeout(() => {
+ document.documentElement.classList.remove('no-transition');
+ }, 100);
+});
+
+// 创建主题切换按钮
+function createThemeToggle() {
+ const themeSwitch = document.createElement('div');
+ themeSwitch.className = 'theme-switch';
+ themeSwitch.setAttribute('title', '切换亮/暗主题');
+ themeSwitch.innerHTML = `
+
+ `;
+
+ // 更新为当前主题的图标
+ const currentTheme = document.documentElement.getAttribute('data-theme');
+ updateThemeIcon(currentTheme);
+
+ // 添加点击事件监听
+ themeSwitch.addEventListener('click', toggleTheme);
+
+ // 添加到页面
+ document.body.appendChild(themeSwitch);
+}
+
+// 初始化主题
+function initTheme() {
+ // 首先检查用户的主题偏好
+ const savedTheme = localStorage.getItem('userThemePreference');
+ if (savedTheme) {
+ applyTheme(savedTheme);
+ return;
+ }
+
+ // 如果没有用户偏好,检查系统主题
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ applyTheme('dark');
+ return;
+ }
+
+ // 检查当前时间
+ const currentHour = new Date().getHours();
+ if (currentHour >= 19 || currentHour < 7) {
+ applyTheme('dark');
+ return;
+ }
+
+ // 如果没有特殊情况,使用亮色主题
+ applyTheme('light');
+}
+
+// 应用主题
+function applyTheme(theme) {
+ document.documentElement.setAttribute('data-theme', theme);
+ updateThemeIcon(theme);
+ localStorage.setItem('userThemePreference', theme);
+}
+
+// 更新主题图标
+function updateThemeIcon(theme) {
+ const themeIcon = document.querySelector('.theme-icon');
+
+ if (!themeIcon) return;
+
+ if (theme === 'dark') {
+ // 使用CSS类切换动画而不是直接修改innerHTML
+ themeIcon.classList.add('dark-mode');
+ themeIcon.innerHTML = `
+
+ `;
+ } else {
+ themeIcon.classList.remove('dark-mode');
+ themeIcon.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+// 切换主题
+function toggleTheme() {
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
+
+ // 添加过渡类,启用平滑动画
+ document.documentElement.classList.add('theme-transition');
+
+ // 应用新主题
+ applyTheme(newTheme);
+
+ // 切换动画效果
+ const themeSwitch = document.querySelector('.theme-switch');
+ if (themeSwitch) {
+ themeSwitch.classList.add('theme-switch-animate');
+ setTimeout(() => {
+ themeSwitch.classList.remove('theme-switch-animate');
+ }, 700);
+ }
+}
+
+// 监听系统主题变化
+function listenForSystemThemeChanges() {
+ if (window.matchMedia) {
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
+ // 只有当用户没有手动设置主题时才跟随系统
+ if (!localStorage.getItem('userThemePreference')) {
+ applyTheme(e.matches ? 'dark' : 'light');
+ }
+ });
+ }
+}
+
+// 基于时间自动切换主题的功能
+function scheduleThemeChange() {
+ // 只有当用户没有手动设置主题时才自动切换
+ if (!localStorage.getItem('userThemePreference')) {
+ const currentHour = new Date().getHours();
+ if (currentHour >= 19 || currentHour < 7) {
+ applyTheme('dark');
+ } else {
+ applyTheme('light');
+ }
+ }
+
+ // 每小时检查一次
+ setTimeout(scheduleThemeChange, 3600000);
+}
+
+// 启动基于时间的主题切换
+scheduleThemeChange();
+
+// 侧边导航功能
+document.addEventListener('DOMContentLoaded', function() {
+ initSideNavigation();
+});
+
+// 初始化侧边导航
+function initSideNavigation() {
+ // 获取所有卡片
+ const cards = document.querySelectorAll('.card');
+ const navContent = document.querySelector('.side-nav-content');
+ const trigger = document.querySelector('.side-nav-trigger');
+ const menu = document.querySelector('.side-nav-menu');
+
+ if (!cards.length || !navContent) return;
+
+ // 为每个卡片创建导航项
+ cards.forEach((card, index) => {
+ // 尝试获取卡片标题
+ let title = '';
+ const h2 = card.querySelector('h2');
+ const h1 = card.querySelector('h1');
+
+ if (h2) {
+ title = h2.textContent.trim();
+ } else if (h1) {
+ title = h1.textContent.trim();
+ } else {
+ title = `部分 ${index + 1}`;
+ }
+
+ // 创建导航项
+ const navItem = document.createElement('div');
+ navItem.className = 'nav-item';
+ navItem.setAttribute('data-target', index);
+
+ // 创建导航点
+ const dot = document.createElement('div');
+ dot.className = 'nav-item-dot';
+
+ // 创建标题
+ const titleSpan = document.createElement('div');
+ titleSpan.className = 'nav-item-title';
+ titleSpan.textContent = title;
+
+ navItem.appendChild(dot);
+ navItem.appendChild(titleSpan);
+ navContent.appendChild(navItem);
+
+ // 点击事件:滚动到对应卡片
+ navItem.addEventListener('click', (e) => {
+ e.preventDefault();
+ card.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ });
+
+ // 使用 Intersection Observer 检测当前可见的卡片
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const index = Array.from(cards).indexOf(entry.target);
+ updateActiveNavItem(index);
+ }
+ });
+ }, { threshold: 0.3 });
+
+ // 观察所有卡片
+ cards.forEach(card => {
+ observer.observe(card);
+ });
+
+ // 更新活动导航项
+ function updateActiveNavItem(index) {
+ const navItems = document.querySelectorAll('.nav-item');
+ navItems.forEach(item => {
+ item.classList.remove('active');
+ });
+
+ const activeItem = document.querySelector(`.nav-item[data-target="${index}"]`);
+ if (activeItem) {
+ activeItem.classList.add('active');
+
+ // 确保活动项在可视区域内
+ if (activeItem.offsetTop < navContent.scrollTop ||
+ activeItem.offsetTop > navContent.scrollTop + navContent.clientHeight) {
+ activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+ }
+
+ // 移动端触摸事件处理
+ if (trigger && menu) {
+ trigger.addEventListener('touchstart', function(e) {
+ e.preventDefault();
+ this.classList.toggle('touch-active');
+ menu.classList.toggle('touch-active');
+ });
+
+ // 点击其他区域关闭移动端目录
+ document.addEventListener('touchstart', function(e) {
+ if (!e.target.closest('.side-nav-trigger') && !e.target.closest('.side-nav-menu')) {
+ trigger.classList.remove('touch-active');
+ menu.classList.remove('touch-active');
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/routes/index.js b/src/routes/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..f536308381c9e304e36e7fab1fe0f2b11eab674c
--- /dev/null
+++ b/src/routes/index.js
@@ -0,0 +1,8 @@
+const express = require('express');
+const router = express.Router();
+const v1Routes = require('./v1');
+
+// OpenAI v1 API routes
+router.use('/v1', v1Routes);
+
+module.exports = router;
diff --git a/src/routes/v1.js b/src/routes/v1.js
new file mode 100644
index 0000000000000000000000000000000000000000..55f7f1894e914d3cfd6fdf9822900d5455bfb7f5
--- /dev/null
+++ b/src/routes/v1.js
@@ -0,0 +1,1924 @@
+const express = require('express');
+const router = express.Router();
+const { fetch, ProxyAgent, Agent } = require('undici');
+
+const $root = require('../proto/message.js');
+const { v4: uuidv4, v5: uuidv5 } = require('uuid');
+const { generateCursorBody, chunkToUtf8String, generateHashed64Hex, generateCursorChecksum } = require('../utils/utils.js');
+const keyManager = require('../utils/keyManager.js');
+const { spawn } = require('child_process');
+const path = require('path');
+const admin = require('../models/admin');
+const config = require('../config/config');
+const crypto = require('crypto');
+const logger = require('../utils/logger');
+
+const activeRequestControllers = new Map(); // 用于存储 API Key -> AbortController 的映射
+
+// 存储刷新状态的变量
+let refreshStatus = {
+ isRunning: false,
+ status: 'idle', // idle, running, completed, failed
+ message: '',
+ startTime: null,
+ endTime: null,
+ error: null
+};
+
+// 储存当前正在处理的Cookie获取请求
+const pendingCookieRequests = new Map();
+
+// 检查是否已有管理员账号
+router.get('/admin/check', (req, res) => {
+ try {
+ return res.json({
+ success: true,
+ exists: admin.hasAdmin()
+ });
+ } catch (error) {
+ logger.error('检查管理员账号失败:', error);
+ return res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+});
+
+// 注册管理员
+router.post('/admin/register', (req, res) => {
+ try {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).json({
+ success: false,
+ message: '用户名和密码不能为空'
+ });
+ }
+
+ const token = admin.register(username, password);
+
+ return res.json({
+ success: true,
+ message: '注册成功',
+ token
+ });
+ } catch (error) {
+ logger.error('注册管理员失败:', error);
+ return res.status(400).json({
+ success: false,
+ message: error.message
+ });
+ }
+});
+
+// 管理员登录
+router.post('/admin/login', (req, res) => {
+ try {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).json({
+ success: false,
+ message: '用户名和密码不能为空'
+ });
+ }
+
+ const token = admin.login(username, password);
+
+ return res.json({
+ success: true,
+ message: '登录成功',
+ token
+ });
+ } catch (error) {
+ logger.error('登录失败:', error);
+ return res.status(400).json({
+ success: false,
+ message: error.message
+ });
+ }
+});
+
+// 验证token
+router.get('/admin/verify', (req, res) => {
+ try {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({
+ success: false,
+ message: '未提供认证token'
+ });
+ }
+
+ const token = authHeader.split(' ')[1];
+ const result = admin.verifyToken(token);
+
+ return res.json(result);
+ } catch (error) {
+ logger.error('验证token失败:', error);
+ return res.status(401).json({
+ success: false,
+ message: error.message
+ });
+ }
+});
+
+// 添加API key管理路由
+router.post("/api-keys", async (req, res) => {
+ try {
+ const { apiKey, cookieValues } = req.body;
+
+ if (!apiKey || !cookieValues) {
+ return res.status(400).json({
+ error: 'API key and cookie values are required',
+ });
+ }
+
+ keyManager.addOrUpdateApiKey(apiKey, cookieValues);
+
+ return res.json({
+ success: true,
+ message: 'API key added or updated successfully',
+ });
+ } catch (error) {
+ logger.error(error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ });
+ }
+});
+
+// 获取所有API Keys
+router.get("/api-keys", async (req, res) => {
+ try {
+ logger.info('收到获取API Keys请求');
+ const apiKeys = keyManager.getAllApiKeys();
+ logger.info('获取到的API Keys:', apiKeys);
+
+ const result = {
+ success: true,
+ apiKeys: apiKeys.map(apiKey => ({
+ key: apiKey,
+ cookieCount: keyManager.getAllCookiesForApiKey(apiKey).length,
+ })),
+ };
+ logger.info('返回结果:', result);
+
+ return res.json(result);
+ } catch (error) {
+ logger.error('获取API Keys失败:', error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// 删除API key
+router.delete("/api-keys/:apiKey", async (req, res) => {
+ try {
+ const { apiKey } = req.params;
+
+ keyManager.removeApiKey(apiKey);
+
+ return res.json({
+ success: true,
+ message: 'API key removed successfully',
+ });
+ } catch (error) {
+ logger.error(error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ });
+ }
+});
+
+// 获取特定API Key的Cookie值
+router.get("/api-keys/:apiKey/cookies", async (req, res) => {
+ try {
+ const { apiKey } = req.params;
+ logger.info(`收到获取API Key ${apiKey}的Cookie值请求`);
+
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey);
+ logger.info(`API Key ${apiKey}的Cookie值:`, cookies);
+
+ return res.json({
+ success: true,
+ cookies: cookies
+ });
+ } catch (error) {
+ logger.error(`获取API Key ${req.params.apiKey}的Cookie值失败:`, error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// 获取所有无效的cookie
+router.get("/invalid-cookies", async (req, res) => {
+ try {
+ const invalidCookies = keyManager.getInvalidCookies();
+
+ return res.json({
+ success: true,
+ invalidCookies: Array.from(invalidCookies)
+ });
+ } catch (error) {
+ logger.error('获取无效cookie失败:', error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// 清除特定的无效cookie
+router.delete("/invalid-cookies/:cookie", async (req, res) => {
+ try {
+ const { cookie } = req.params;
+ const success = keyManager.clearInvalidCookie(cookie);
+
+ return res.json({
+ success: success,
+ message: success ? '无效cookie已清除' : '未找到指定的无效cookie'
+ });
+ } catch (error) {
+ logger.error('清除无效cookie失败:', error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// 清除所有无效cookie
+router.delete("/invalid-cookies", async (req, res) => {
+ try {
+ keyManager.clearAllInvalidCookies();
+
+ return res.json({
+ success: true,
+ message: '所有无效cookie已清除'
+ });
+ } catch (error) {
+ logger.error('清除所有无效cookie失败:', error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// 批量添加无效cookie
+router.post("/invalid-cookies", async (req, res) => {
+ try {
+ const { invalidCookies } = req.body;
+
+ if (!Array.isArray(invalidCookies)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Invalid request',
+ message: 'invalidCookies必须是一个数组'
+ });
+ }
+
+ // 获取当前无效cookie集合
+ const currentInvalidCookies = keyManager.getInvalidCookies();
+
+ // 添加新的无效cookie
+ for (const cookie of invalidCookies) {
+ if (typeof cookie === 'string' && cookie.trim()) {
+ currentInvalidCookies.add(cookie.trim());
+ }
+ }
+
+ // 保存到文件
+ keyManager.saveInvalidCookiesToFile();
+
+ return res.json({
+ success: true,
+ message: `已添加${invalidCookies.length}个无效cookie`
+ });
+ } catch (error) {
+ logger.error('添加无效cookie失败:', error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// 获取可用模型列表
+router.get("/models", async (req, res) => {
+ try{
+ let bearerToken = req.headers.authorization?.replace('Bearer ', '');
+
+ // 使用keyManager获取实际的cookie
+ let authToken = keyManager.getCookieForApiKey(bearerToken);
+
+ if (authToken && authToken.includes('%3A%3A')) {
+ authToken = authToken.split('%3A%3A')[1];
+ }
+ else if (authToken && authToken.includes('::')) {
+ authToken = authToken.split('::')[1];
+ }
+
+ const checksum = req.headers['x-cursor-checksum']
+ ?? process.env['x-cursor-checksum']
+ ?? generateCursorChecksum(authToken.trim());
+ //const cursorClientVersion = "0.45.11"
+ const cursorClientVersion = "0.50.4";
+
+ const availableModelsResponse = await fetch("https://api2.cursor.sh/aiserver.v1.AiService/AvailableModels", {
+ method: 'POST',
+ headers: {
+ 'accept-encoding': 'gzip',
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'Host': 'api2.cursor.sh',
+ },
+ })
+ const data = await availableModelsResponse.arrayBuffer();
+ const buffer = Buffer.from(data);
+ try{
+ const models = $root.AvailableModelsResponse.decode(buffer).models;
+
+ // 生成带前缀的模型列表
+ const prefixedModels = models.map(model => ({
+ id: `[auto]-${model.name}`,
+ created: Date.now(),
+ object: 'model',
+ owned_by: 'cursor'
+ }));
+
+ // 合并原始模型和带前缀的模型
+ const combinedModels = models.map(model => ({
+ id: model.name,
+ created: Date.now(),
+ object: 'model',
+ owned_by: 'cursor'
+ })).concat(prefixedModels);
+
+ return res.json({
+ object: "list",
+ data: combinedModels
+ })
+ } catch (error) {
+ const text = buffer.toString('utf-8');
+ throw new Error(text);
+ }
+ }
+ catch (error) {
+ logger.error(error);
+ return res.status(500).json({
+ error: 'Internal server error',
+ });
+ }
+})
+
+
+router.post('/chat/completions', async (req, res) => {
+ // 检查请求体是否存在
+ if (!req.body) {
+ return res.status(400).json({
+ error: '请求体不能为空',
+ });
+ }
+
+ // 检查模型属性是否存在
+ if (!req.body.model) {
+ return res.status(400).json({
+ error: '缺少必要参数: model',
+ });
+ }
+
+ // 检查未支持的模型和流式传输 (对原始模型进行检查)
+ if (typeof req.body.model === 'string' && req.body.model.replace('[auto]-', '').startsWith('o1-') && req.body.stream) {
+ return res.status(400).json({
+ error: 'Model not supported stream',
+ });
+ }
+
+ try {
+ const { model, messages, stream = false } = req.body;
+ let extractedStopTokens = [];
+ let processedMessages = JSON.parse(JSON.stringify(messages)); // 复制一份,避免修改原始请求体
+ let foundStopStringPattern = false;
+
+ let actualModel = model; // 实际发送给Cursor的模型名称
+ const autoPrefix = '[auto]-';
+
+ // 检查并处理带前缀的模型
+ if (typeof model === 'string' && model.startsWith(autoPrefix)) {
+ actualModel = model.substring(autoPrefix.length); // 移除前缀
+ logger.info(`检测到预定模板模型: ${model}, 实际使用模型: ${actualModel}`);
+
+ // 定义模板和随机标签
+ const template = `
+<|Stop-String|>::::::::::::<|Stop-String|>
+###Please ensure to output the following stop string wrapped in xml tag {{random}} at the end of each reply:
+
+Ending this round of conversation: Ten, nine, eight, seven, six, five, four, three, two, one. This round of replies has been perfectly completed!
+`;
+ const tags = ['', '', '', '', '', '', ''];
+
+ // 1. 随机选择一个标签,确保本次请求中所有注入都使用这一个
+ const randomTag = tags[Math.floor(Math.random() * tags.length)];
+
+ // 2. 构建注入系统消息的指令
+ const processedTemplate = template.replace('{{random}}', randomTag);
+
+ // 3. 构建追加到assistant消息的声明
+ const declarationString = `
+${randomTag}
+Ending this round of conversation: Ten, nine, eight, seven, six, five, four, three, two, one. This round of replies has been perfectly completed!
+${randomTag.replace('<', '')}
+`;
+
+ // 4. 将指令注入到系统消息中 (不存在则创建)
+ let systemMessage = processedMessages.find(m => m.role === 'system');
+ if (systemMessage) {
+ systemMessage.content = (systemMessage.content || '') + '\n\n' + processedTemplate;
+ logger.debug('已将模板追加到现有系统消息');
+ } else {
+ processedMessages.unshift({ role: 'system', content: processedTemplate });
+ logger.debug('未找到系统消息,已创建并添加新的系统消息');
+ }
+
+ // 5. 将声明追加到最后5条assistant消息
+ let assistantMessagesToModify = 5;
+ for (let i = processedMessages.length - 1; i >= 0 && assistantMessagesToModify > 0; i--) {
+ if (processedMessages[i].role === 'assistant') {
+ processedMessages[i].content = (processedMessages[i].content || '') + declarationString;
+ assistantMessagesToModify--;
+ }
+ }
+ logger.debug(`已将声明追加到 ${5 - assistantMessagesToModify} 条assistant消息`);
+
+ // 在处理完预设模板后,确保foundStopStringPattern为false,以便后续的停止字符串提取逻辑能够运行在processedMessages上
+ foundStopStringPattern = false; // 重置foundStopStringPattern
+ }
+
+ // 从messages中提取停止字符串并移除标记 (现在会作用于可能修改过的processedMessages)
+ for (const message of processedMessages) {
+ let content = message.content || '';
+ const stopStringPattern = /<\|Stop-String\|>(.*?)<\|Stop-String\|>/s;
+ const match = content.match(stopStringPattern);
+
+ if (match && match[1] && !foundStopStringPattern) {
+ // 只处理找到的第一个匹配
+ const stopStrings = match[1].split('::').map(s => s.trim()).filter(s => s.length > 0);
+ extractedStopTokens = stopStrings;
+ foundStopStringPattern = true;
+
+ // 移除content中的停止字符串标记
+ content = content.replace(stopStringPattern, '').trim();
+
+ // 如果移除后内容为空,考虑删除该消息或保留角色信息
+ if (content === '') {
+ // Option 1: Keep role but empty content, prevents removing valid turn.
+ message.content = ''; // 直接修改processedMessages中的对象
+ // Option 2: Remove message entirely if content becomes empty.
+ // continue; // 这需要重建processedMessages数组
+ } else {
+ message.content = content; // 直接修改processedMessages中的对象
+ }
+ } else if (foundStopStringPattern) {
+ // 如果已经找到模式,直接使用原始内容,不再进行移除操作
+ // message.content 保持不变
+ }
+ }
+
+ // 如果没有找到停止字符串模式,返回错误 (现在只有在没有[auto]-前缀模型且没有找到标记时才会触发)
+ // 对于[auto]-前缀模型,由于模板中包含了标记,foundStopStringPattern会被设置为true
+ if (!foundStopStringPattern) {
+ return res.json({
+ id: `chatcmpl-${uuidv4()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model: model || 'unknown',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: '预设错误,请使用指定预设结构',
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0,
+ },
+ });
+ }
+
+ // 使用提取的停止字符串
+ const stopTokens = extractedStopTokens;
+
+ // 记录本次回复的所有停止字符串
+ logger.info(`本次回复使用的停止字符串: [${stopTokens.join(', ')}]`);
+
+ let bearerToken = req.headers.authorization?.replace('Bearer ', '');
+
+ // 使用keyManager获取实际的cookie
+ let authToken = keyManager.getCookieForApiKey(bearerToken);
+ // 保存原始cookie,用于后续可能的错误处理
+ const originalAuthToken = authToken;
+ //console.log('原始cookie:', originalAuthToken);
+
+ if (authToken && authToken.includes('%3A%3A')) {
+ authToken = authToken.split('%3A%3A')[1];
+ }
+ else if (authToken && authToken.includes('::')) {
+ authToken = authToken.split('::')[1];
+ }
+
+ // 使用processedMessages (可能包含追加的模板)
+ if (!processedMessages || processedMessages.length === 0 || !authToken) {
+ return res.status(400).json({
+ error: 'Invalid request. Messages should be a non-empty array and authorization is required',
+ });
+ }
+
+ const checksum = req.headers['x-cursor-checksum']
+ ?? process.env['x-cursor-checksum']
+ ?? generateCursorChecksum(authToken.trim());
+
+ const sessionid = uuidv5(authToken, uuidv5.DNS);
+ const clientKey = generateHashed64Hex(authToken);
+ const cursorClientVersion = "0.50.4";
+
+ // 在请求聊天接口前,依次调用6个接口
+ if (process.env.USE_OTHERS === 'true') {
+ try{
+ others(authToken, clientKey, checksum, cursorClientVersion, sessionid).then( () => {
+ logger.info("其它接口异步调用成功");
+ });
+ } catch (error) {
+ logger.error(error.message);
+ }
+ }
+
+ // 使用实际发送给Cursor的模型名称 (不带前缀)
+ logger.info('发送给Cursor的完整消息上下文:', JSON.stringify(processedMessages, null, 2));
+ logger.info('发送给Cursor的实际模型:', actualModel);
+ const cursorBody = generateCursorBody(processedMessages, actualModel);
+
+ // 添加代理支持
+ const dispatcher = config.proxy && config.proxy.enabled
+ ? new ProxyAgent(config.proxy.url, { allowH2: true })
+ : new Agent({ allowH2: true });
+
+ // 根据.env配置决定是否使用TLS代理
+ const useTlsProxy = process.env.USE_TLS_PROXY === 'true';
+
+ // 创建AbortController用于能够中止请求
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ let response;
+
+ try {
+ if (useTlsProxy) {
+ // 使用JA3指纹伪造代理服务器
+ logger.info(`使用TLS代理服务器`);
+ response = await fetch('http://localhost:8080/proxy', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ url: 'https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools',
+ method: 'POST',
+ headers: {
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-accept-encoding': 'gzip',
+ 'connect-content-encoding': 'gzip',
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/connect+proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'x-request-id': uuidv4(),
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ body: cursorBody,
+ stream: true // 启用流式响应
+ }),
+ timeout: {
+ connect: 5000,
+ read: 30000
+ },
+ signal // 添加AbortController的signal
+ });
+ } else {
+ // 直接调用API,不使用TLS代理
+ logger.info('不使用TLS代理服务器,直接请求API');
+ response = await fetch('https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools', {
+ method: 'POST',
+ headers: {
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-accept-encoding': 'gzip',
+ 'connect-content-encoding': 'gzip',
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/connect+proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Shanghai',
+ 'x-ghost-mode': 'true',
+ 'x-request-id': uuidv4(),
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ body: cursorBody,
+ dispatcher: dispatcher,
+ timeout: {
+ connect: 5000,
+ read: 30000
+ },
+ signal // 添加AbortController的signal
+ });
+ }
+ } catch (fetchError) {
+ logger.error(`Fetch错误: ${fetchError.message}`);
+
+ // 处理连接超时错误
+ const isConnectTimeout = fetchError.cause &&
+ (fetchError.cause.code === 'UND_ERR_CONNECT_TIMEOUT' ||
+ fetchError.message.includes('Connect Timeout Error'));
+
+ // 构建错误响应
+ const errorMessage = isConnectTimeout
+ ? `⚠️ 连接超时 ⚠️\\n\\n无法连接到API服务器(api2.cursor.sh),请检查您的网络连接或尝试使用代理。`
+ : `⚠️ 请求失败 ⚠️\\n\\n错误: ${fetchError.message}`;
+
+ if (stream) {
+ // 流式响应格式的错误
+ const responseId = `chatcmpl-${uuidv4()}`;
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model || 'unknown',
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: errorMessage,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ res.write('data: [DONE]\n\n');
+ res.end();
+ } else {
+ // 非流式响应格式的错误
+ res.json({
+ id: `chatcmpl-${uuidv4()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model || 'unknown',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: errorMessage,
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0,
+ },
+ });
+ }
+ return; // 重要:提前返回
+ }
+
+ // 处理响应
+ if (stream) {
+ // 如果存在此 API Key 的旧请求,则中止它
+ if (bearerToken && activeRequestControllers.has(bearerToken)) {
+ const oldController = activeRequestControllers.get(bearerToken);
+ logger.info(`API Key [${bearerToken}] 的新流式请求到达,正在中止旧请求...`);
+ oldController.abort();
+ // activeRequestControllers.delete(bearerToken); // 旧的会被新的覆盖,或在旧请求的清理逻辑中移除
+ }
+ // 存储当前请求的 AbortController
+ if (bearerToken) {
+ activeRequestControllers.set(bearerToken, controller);
+ }
+
+ // 清理当前请求的 AbortController 的辅助函数
+ const cleanupCurrentController = () => {
+ if (bearerToken && activeRequestControllers.get(bearerToken) === controller) {
+ activeRequestControllers.delete(bearerToken);
+ logger.debug(`API Key [${bearerToken}] 的流式请求处理完毕,已清理 AbortController。`);
+ }
+ };
+ res.on('finish', cleanupCurrentController); // 响应正常结束时清理
+ res.on('close', cleanupCurrentController); // 响应因任何原因关闭时清理 (包括客户端断开)
+
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+
+ // 监听客户端断开连接事件
+ req.on('close', () => {
+ if (!responseEnded) {
+ logger.warn(`客户端已断开连接 (API Key: [${bearerToken}]), 正在中止对Cursor服务端的请求...`);
+ controller.abort();
+ responseEnded = true;
+ // cleanupCurrentController 会在 res 'close' 时被调用
+ }
+ });
+
+ const responseId = `chatcmpl-${uuidv4()}`;
+
+ let isThinking_status = 0; //0为没有思考,1为处于思考状态
+ try {
+ let responseEnded = false; // 添加标志,标记响应是否已结束
+ let hasWrittenThinkingStart = false; // 标记是否已发送thinking开始标签
+ let hasWrittenThinkingEnd = false; // 标记是否已发送thinking结束标签
+ let hasWrittenContent = false; // 标记是否已发送content
+ let accumulatedThinking = ''; // 累积thinking内容
+ let accumulatedContent = ''; // 累积content内容
+
+ for await (const chunk of response.body) {
+ // 如果响应已结束,不再处理后续数据
+ if (responseEnded) {
+ continue;
+ }
+
+ let result = {};
+ try {
+ result = chunkToUtf8String(chunk);
+ } catch (error) {
+ logger.error('解析响应块失败:', error);
+ // 提供默认的空结果,避免后续处理出错
+ result = {
+ isThink: false,
+ thinkingContent: '',
+ content: '',
+ error: `解析错误: ${error.message}`
+ };
+ }
+
+ // 检查是否返回了错误对象
+ if (result && typeof result === 'object' && result.error) {
+ // 检查是否包含特定的无效cookie错误信息
+ const errorStr = typeof result.error === 'string' ? result.error : JSON.stringify(result.error);
+
+ // 处理错误并获取结果
+ const errorResult = handleCursorError(errorStr, bearerToken, originalAuthToken);
+
+ // 如果是需要移除的cookie,从API Key中移除
+ if (errorResult.shouldRemoveCookie) {
+ const removed = keyManager.removeCookieFromApiKey(bearerToken, originalAuthToken);
+ logger.info(`Cookie移除${removed ? '成功' : '失败'}`);
+
+ // 如果成功移除,在错误消息中添加明确提示
+ if (removed) {
+ errorResult.message = `⚠️ 目前Cookie已从API Key中移除 ⚠️\\n\\n${errorResult.message}`;
+ }
+ }
+
+ // 返回错误信息给客户端,作为assistant消息
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: errorResult.message,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+
+ res.write('data: [DONE]\n\n');
+ responseEnded = true; // 标记响应已结束
+ break; // 跳出循环,不再处理后续数据
+ }
+
+ // 处理thinking内容
+ if (result.isThink && result.thinkingContent && result.thinkingContent.length > 0) {
+ // 累积thinking内容
+ accumulatedThinking += result.thinkingContent;
+
+ // 如果没有发送thinking开始标记,则发送
+ if (!hasWrittenThinkingStart) {
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: "\\n",
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ hasWrittenThinkingStart = true;
+ }
+
+ // 发送accumulated thinking内容片段
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: result.thinkingContent,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ }
+
+ // 处理常规内容
+ if (result.content && result.content.length > 0) {
+ // 累积content内容
+ accumulatedContent += result.content;
+
+ // 如果已经有thinking内容,且尚未发送thinking结束标记,则发送
+ if (hasWrittenThinkingStart && !hasWrittenThinkingEnd) {
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: "\\n\\n",
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ hasWrittenThinkingEnd = true;
+ }
+
+ // 检查是否遇到停止字符串
+ let shouldStop = false;
+ let contentToSend = result.content;
+
+ // 检查停止字符串
+ if (stopTokens.length > 0) {
+ for (const stopToken of stopTokens) {
+ const stopIndex = accumulatedContent.indexOf(stopToken);
+ if (stopIndex !== -1) {
+ // 记录检测到停止字符串的日志
+ logger.info(`检测到停止字符串: "${stopToken}" 在位置 ${stopIndex},累积内容长度: ${accumulatedContent.length}`);
+
+ // 如果找到停止字符串,立即停止,不管停止字符串在哪个chunk中
+ const lastChunkIndex = accumulatedContent.length - result.content.length;
+
+ if (stopIndex >= lastChunkIndex) {
+ // 停止字符串在当前块中,只发送到停止字符串之前的内容
+ contentToSend = result.content.substring(0, stopIndex - lastChunkIndex);
+ } else {
+ // 停止字符串在之前的chunks中,不发送当前chunk的任何内容
+ contentToSend = '';
+ }
+
+ shouldStop = true;
+ break;
+ }
+ }
+ }
+
+ // 只有当有内容要发送时才发送
+ if (contentToSend.length > 0) {
+ // 发送content内容
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: contentToSend,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ hasWrittenContent = true;
+ }
+
+ // 如果需要停止,发送[DONE]并结束响应
+ if (shouldStop) {
+ res.write('data: [DONE]\n\n');
+ res.end();
+ responseEnded = true;
+
+ try {
+ controller.abort();
+ } catch (abortError) {
+ logger.error('中止Cursor连接时出错 (停止字符串):', abortError);
+ }
+ // cleanupCurrentController 会在 res 'finish' 或 'close' 时被调用
+ break;
+ }
+ }
+ }
+
+ // 处理结束逻辑:确保thinking标签被正确关闭
+ if (!responseEnded) {
+ // 如果有thinking内容但没有发送结束标记,则发送
+ if (hasWrittenThinkingStart && !hasWrittenThinkingEnd) {
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: "\\n\\n",
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ }
+
+ res.write('data: [DONE]\n\n');
+ res.end();
+ // cleanupCurrentController 会在 res 'finish' 时被调用
+ }
+ } catch (streamError) {
+ // 区分正常的中止操作和真正的错误
+ if (streamError.name === 'AbortError') {
+ logger.info(`流处理被中止 (API Key: [${bearerToken}]), 原因可能为: 新请求覆盖, 客户端断开, 或停止字符串触发。`);
+ } else {
+ logger.error(`Stream error (API Key: [${bearerToken}]):`, streamError);
+ }
+
+ if (!res.writableEnded) {
+ if (streamError.name === 'AbortError') {
+ // AbortError 通常意味着我们主动中止,响应可能已处理或将由 'close' 事件处理
+ // 但为确保万无一失,如果响应未结束,尝试结束它
+ if (!res.headersSent) { // 避免在已发送头后再次发送
+ res.status(500).json({ error: 'Stream aborted' });
+ } else {
+ res.end(); //尝试结束流
+ }
+ return; // AbortError 处理完毕
+ } else if (streamError.name === 'TimeoutError') {
+ // 将超时错误作为assistant消息发送
+ const errorMessage = `⚠️ 请求超时 ⚠️\\n\\n错误:服务器响应超时,请稍后重试。`;
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: errorMessage,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ } else {
+ // 将处理错误作为assistant消息发送
+ const errorMessage = `⚠️ 处理错误 ⚠️\\n\\n错误:流处理出错,请稍后重试。\\n\\n${streamError.message || ''}`;
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: errorMessage,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ }
+ res.write('data: [DONE]\n\n');
+ res.end();
+ }
+ }
+ } else {
+ try {
+ let text = '';
+ let thinkingText = '';
+ let hasThinking = false;
+ let responseEnded = false; // 添加标志,标记响应是否已结束
+
+ for await (const chunk of response.body) {
+ // 如果响应已结束,不再处理后续数据
+ if (responseEnded) {
+ continue;
+ }
+
+ let result = {};
+ try {
+ result = chunkToUtf8String(chunk);
+ } catch (error) {
+ logger.error('非流式响应解析块失败:', error);
+ // 提供默认的空结果,避免后续处理出错
+ result = {
+ thinkingContent: '',
+ content: '',
+ error: `解析错误: ${error.message}`
+ };
+ }
+ // 输出完整的result内容和类型,便于调试
+ //console.log("收到的非流式响应:", typeof result, result && typeof result === 'object' ? JSON.stringify(result) : result);
+
+ // 检查是否返回了错误对象
+ if (result && typeof result === 'object' && result.error) {
+ //console.error('检测到错误响应:', result.error);
+
+ // 检查是否包含特定的无效cookie错误信息
+ const errorStr = typeof result.error === 'string' ? result.error : JSON.stringify(result.error);
+
+ // 处理错误并获取结果
+ const errorResult = handleCursorError(errorStr, bearerToken, originalAuthToken);
+
+ // 如果是需要移除的cookie,从API Key中移除
+ if (errorResult.shouldRemoveCookie) {
+ const removed = keyManager.removeCookieFromApiKey(bearerToken, originalAuthToken);
+ logger.info(`Cookie移除${removed ? '成功' : '失败'}`);
+
+ // 如果成功移除,在错误消息中添加明确提示
+ if (removed) {
+ errorResult.message = `⚠️ 目前Cookie已从API Key中移除 ⚠️\\n\\n${errorResult.message}`;
+ }
+ }
+
+ // 无效cookie错误,格式化为assistant消息
+ res.json({
+ id: `chatcmpl-${uuidv4()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model,
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: errorResult.message,
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0,
+ },
+ });
+
+ responseEnded = true; // 标记响应已结束
+ break; // 跳出循环,不再处理后续数据
+ }
+
+ // 处理thinking内容
+ if (result.thinkingContent && result.thinkingContent.length > 0) {
+ thinkingText += result.thinkingContent;
+ hasThinking = true;
+ }
+
+ // 处理正常文本内容
+ if (result.content && typeof result.content === 'string') {
+ text += result.content;
+ }
+ }
+
+ // 只有在响应尚未结束的情况下,才处理和返回结果
+ if (!responseEnded) {
+ // 对解析后的字符串进行进一步处理
+ text = text.replace(/^.*<\|END_USER\|>/s, '');
+ text = text.replace(/^\n[a-zA-Z]?/, '').trim();
+
+ // 检查停止字符串并截断内容
+ if (stopTokens.length > 0) {
+ for (const stopToken of stopTokens) {
+ const stopIndex = text.indexOf(stopToken);
+ if (stopIndex !== -1) {
+ // 记录检测到停止字符串的日志
+ logger.info(`非流式响应检测到停止字符串: "${stopToken}" 在位置 ${stopIndex}`);
+
+ // 截断到停止字符串之前的内容
+ text = text.substring(0, stopIndex);
+ break;
+ }
+ }
+ }
+
+ // 如果存在thinking内容,添加标签
+ let finalContent = text;
+ if (hasThinking && thinkingText.length > 0) {
+ finalContent = `\\n${thinkingText}\\n\\n${text}`;
+ }
+
+ res.json({
+ id: `chatcmpl-${uuidv4()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model,
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: finalContent,
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0,
+ },
+ });
+ }
+ } catch (error) {
+ logger.error('Non-stream error:', error);
+ // 确保在发送错误信息前检查响应是否已结束
+ if (!res.headersSent) {
+ if (error.name === 'TimeoutError') {
+ // 使用统一的错误格式
+ const errorMessage = `⚠️ 请求超时 ⚠️\\n\\n错误:服务器响应超时,请稍后重试。`;
+ return res.json({
+ id: `chatcmpl-${uuidv4()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model || 'unknown',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: errorMessage,
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0,
+ },
+ });
+ }
+ throw error;
+ }
+ }
+ }
+ } catch (error) {
+ logger.error('Error:', error);
+ if (!res.headersSent) {
+ const errorText = error.name === 'TimeoutError' ? '请求超时' : '服务器内部错误';
+
+ if (req.body.stream) {
+ // 流式响应格式的错误
+ const responseId = `chatcmpl-${uuidv4()}`;
+ // 添加清晰的错误提示
+ const errorMessage = `⚠️ 请求失败 ⚠️\\n\\n错误:${errorText},请稍后重试。\\n\\n${error.message || ''}`;
+ res.write(
+ `data: ${JSON.stringify({
+ id: responseId,
+ object: 'chat.completion.chunk',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model || 'unknown',
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: errorMessage,
+ },
+ },
+ ],
+ })}\n\n`
+ );
+ res.write('data: [DONE]\n\n');
+ res.end();
+ } else {
+ // 非流式响应格式的错误
+ // 添加清晰的错误提示
+ const errorMessage = `⚠️ 请求失败 ⚠️\\n\\n错误:${errorText},请稍后重试。\\n\\n${error.message || ''}`;
+ res.json({
+ id: `chatcmpl-${uuidv4()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model: req.body.model || 'unknown',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: errorMessage,
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0,
+ },
+ });
+ }
+ }
+ }
+});
+
+// 触发Cookie刷新
+router.post("/refresh-cookies", async (req, res) => {
+ try {
+ // 如果已经有刷新进程在运行,则返回错误
+ if (refreshStatus.isRunning) {
+ return res.status(409).json({
+ success: false,
+ message: '已有刷新进程在运行,请等待完成后再试'
+ });
+ }
+
+ // 获取请求参数
+ const apiKey = req.query.apiKey || '';
+
+ // 重置刷新状态
+ refreshStatus = {
+ isRunning: true,
+ status: 'running',
+ message: '正在启动刷新进程...',
+ startTime: new Date(),
+ endTime: null,
+ error: null
+ };
+
+ logger.info(`收到刷新Cookie请求,API Key: ${apiKey || '所有'}`);
+
+ // 构建命令行参数
+ const args = [];
+ if (apiKey) {
+ args.push(apiKey);
+ }
+
+ // 获取auto-refresh-cookies.js的绝对路径
+ const scriptPath = path.resolve(__dirname, '../../auto-refresh-cookies.js');
+
+ // 启动子进程执行刷新脚本
+ const refreshProcess = spawn('node', [scriptPath, ...args], {
+ stdio: ['ignore', 'pipe', 'pipe']
+ });
+
+ // 收集输出
+ let output = '';
+
+ refreshProcess.stdout.on('data', (data) => {
+ const text = data.toString();
+ output += text;
+ logger.info(`刷新进程输出: ${text}`);
+
+ // 更新状态消息
+ if (text.includes('开始自动刷新')) {
+ refreshStatus.message = '正在刷新Cookie...';
+ } else if (text.includes('刷新结果:')) {
+ refreshStatus.message = text.trim();
+ }
+ });
+
+ refreshProcess.stderr.on('data', (data) => {
+ const text = data.toString();
+ output += text;
+ logger.error(`刷新进程错误: ${text}`);
+
+ // 更新错误信息
+ refreshStatus.error = text.trim();
+ refreshStatus.message = `发生错误: ${text.trim()}`;
+ });
+
+ refreshProcess.on('close', (code) => {
+ logger.info(`刷新进程退出,代码: ${code}`);
+
+ refreshStatus.isRunning = false;
+ refreshStatus.endTime = new Date();
+
+ if (code === 0) {
+ refreshStatus.status = 'completed';
+
+ // 提取成功信息
+ const successMatch = output.match(/成功刷新 (\d+) 个/);
+ if (successMatch) {
+ refreshStatus.message = `成功刷新 ${successMatch[1]} 个API Key的Cookie`;
+ } else {
+ refreshStatus.message = '刷新完成';
+ }
+
+ // 子进程执行完成后,重新初始化API Keys来加载新的Cookie
+ try {
+ const keyManager = require('../utils/keyManager');
+ logger.info('子进程刷新Cookie完成,重新初始化主进程中的API Keys...');
+ keyManager.initializeApiKeys();
+ logger.info('主进程API Keys重新加载完成');
+ } catch (initError) {
+ logger.error('重新初始化API Keys失败:', initError);
+ }
+ } else {
+ refreshStatus.status = 'failed';
+ refreshStatus.message = refreshStatus.error || '刷新失败,请查看服务器日志';
+ }
+ });
+
+ // 立即返回响应,不等待刷新完成
+ return res.json({
+ success: true,
+ message: '刷新请求已接受,正在后台处理'
+ });
+ } catch (error) {
+ logger.error('触发刷新Cookie失败:', error);
+
+ // 更新刷新状态
+ refreshStatus.isRunning = false;
+ refreshStatus.status = 'failed';
+ refreshStatus.endTime = new Date();
+ refreshStatus.error = error.message;
+ refreshStatus.message = `触发刷新失败: ${error.message}`;
+
+ return res.status(500).json({
+ success: false,
+ message: `触发刷新失败: ${error.message}`
+ });
+ }
+});
+
+// 查询Cookie刷新状态
+router.get("/refresh-status", (req, res) => {
+ try {
+ // 返回当前刷新状态
+ return res.json({
+ success: true,
+ data: {
+ ...refreshStatus,
+ isRunning: refreshStatus.isRunning || false,
+ status: refreshStatus.status || 'unknown',
+ message: refreshStatus.message || '未触发刷新',
+ startTime: refreshStatus.startTime || null,
+ endTime: refreshStatus.endTime || null
+ }
+ });
+ } catch (error) {
+ logger.error('获取刷新状态失败:', error);
+ return res.status(500).json({
+ success: false,
+ message: `获取刷新状态失败: ${error.message}`
+ });
+ }
+});
+
+// 生成获取Cookie的链接
+router.post('/generate-cookie-link', async (req, res) => {
+ try {
+ // 验证管理员权限
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({
+ success: false,
+ message: '未提供认证token'
+ });
+ }
+
+ const token = authHeader.split(' ')[1];
+ const authResult = admin.verifyToken(token);
+
+ if (!authResult.success) {
+ return res.status(401).json({
+ success: false,
+ message: '认证失败'
+ });
+ }
+
+ // 生成UUID和PKCE验证器
+ const uuid = uuidv4();
+ const verifier = crypto.randomBytes(32).toString('base64url');
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
+
+ // 生成登录链接
+ const loginUrl = `https://www.cursor.com/ja/loginDeepControl?challenge=${challenge}&uuid=${uuid}&mode=login`;
+
+ // 记录请求信息
+ pendingCookieRequests.set(uuid, {
+ uuid,
+ verifier,
+ status: 'waiting',
+ created: Date.now(),
+ apiKey: req.body.apiKey || '', // 目标API Key,空字符串表示所有API Key
+ lastCheck: Date.now(),
+ cookie: null
+ });
+
+ // 设置60分钟后自动清理
+ setTimeout(() => {
+ if (pendingCookieRequests.has(uuid)) {
+ pendingCookieRequests.delete(uuid);
+ }
+ }, 60 * 60 * 1000);
+
+ return res.json({
+ success: true,
+ url: loginUrl,
+ uuid: uuid
+ });
+ } catch (error) {
+ logger.error('生成Cookie链接失败:', error);
+ return res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+});
+
+// 查询Cookie获取状态
+router.get('/check-cookie-status', async (req, res) => {
+ try {
+ const { uuid } = req.query;
+
+ if (!uuid || !pendingCookieRequests.has(uuid)) {
+ return res.json({
+ success: false,
+ status: 'failed',
+ message: '无效的UUID或请求已过期'
+ });
+ }
+
+ const request = pendingCookieRequests.get(uuid);
+ request.lastCheck = Date.now();
+
+ // 检查状态
+ if (request.status === 'waiting') {
+ // 检查Cursor API获取token
+ try {
+ const apiUrl = `https://api2.cursor.sh/auth/poll?uuid=${uuid}&verifier=${request.verifier}`;
+ const response = await fetch(apiUrl, {
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.210 Safari/537.36',
+ 'Accept': '*/*',
+ 'Origin': 'vscode-file://vscode-app',
+ 'x-ghost-mode': 'true'
+ },
+ timeout: 5000
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+
+ if (data && data.accessToken) {
+ // 获取到了Cookie
+ request.cookie = data.accessToken;
+ request.status = 'success';
+
+ // 将Cookie添加到目标API Key
+ let message = '';
+
+ if (request.apiKey) {
+ // 添加到特定API Key
+ const apiKey = request.apiKey;
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
+ cookies.push(request.cookie);
+ keyManager.addOrUpdateApiKey(apiKey, cookies);
+ message = `Cookie已添加到API Key: ${apiKey}`;
+ } else {
+ // 添加到所有API Key
+ const apiKeys = keyManager.getAllApiKeys();
+ for (const apiKey of apiKeys) {
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
+ cookies.push(request.cookie);
+ keyManager.addOrUpdateApiKey(apiKey, cookies);
+ }
+ message = `Cookie已添加到所有API Key,共${apiKeys.length}个`;
+ }
+
+ // 完成后从等待列表中移除
+ pendingCookieRequests.delete(uuid);
+
+ return res.json({
+ success: true,
+ message: message
+ });
+ }
+ }
+
+ // 如果没有获取到Cookie,继续等待
+ return res.json({
+ success: false,
+ status: 'waiting'
+ });
+
+ } catch (error) {
+ logger.error('查询Cursor API失败:', error);
+ // 发生错误但继续等待,不改变状态
+ return res.json({
+ success: false,
+ status: 'waiting',
+ message: '轮询过程中出现错误,继续等待'
+ });
+ }
+ } else if (request.status === 'success') {
+ // 已成功,返回结果
+ const message = request.apiKey
+ ? `Cookie已添加到API Key: ${request.apiKey}`
+ : `Cookie已添加到所有API Key`;
+
+ // 完成后从等待列表中移除
+ pendingCookieRequests.delete(uuid);
+
+ return res.json({
+ success: true,
+ message: message
+ });
+ } else {
+ // 失败
+ pendingCookieRequests.delete(uuid);
+ return res.json({
+ success: false,
+ status: 'failed',
+ message: '获取Cookie失败'
+ });
+ }
+ } catch (error) {
+ logger.error('检查Cookie状态失败:', error);
+ return res.status(500).json({
+ success: false,
+ status: 'failed',
+ message: error.message
+ });
+ }
+});
+
+// 获取日志API
+router.get("/logs", (req, res) => {
+ try {
+ // 获取查询参数
+ const level = req.query.level;
+ const search = req.query.search;
+ const page = parseInt(req.query.page) || 1;
+ const pageSize = parseInt(req.query.pageSize) || 100;
+ const startTime = req.query.startTime;
+ const endTime = req.query.endTime;
+
+ // 过滤参数
+ const filter = {
+ level,
+ search,
+ page,
+ pageSize,
+ startTime,
+ endTime
+ };
+
+ // 获取日志
+ const logs = logger.getLogs(filter);
+
+ return res.json({
+ success: true,
+ data: logs
+ });
+ } catch (error) {
+ logger.error('获取日志失败:', error);
+ return res.status(500).json({
+ success: false,
+ message: `获取日志失败: ${error.message}`
+ });
+ }
+});
+
+// 清除内存日志
+router.delete("/logs", (req, res) => {
+ try {
+ logger.clearMemoryLogs();
+ return res.json({
+ success: true,
+ message: '日志已清除'
+ });
+ } catch (error) {
+ logger.error('清除日志失败:', error);
+ return res.status(500).json({
+ success: false,
+ message: `清除日志失败: ${error.message}`
+ });
+ }
+});
+async function others(authToken, clientKey, checksum, cursorClientVersion, sessionid){
+ try {
+ // 定义所有API端点配置
+ const endpoints = [
+ {
+ url: 'https://api2.cursor.sh/aiserver.v1.AiService/CheckFeatureStatus',
+ method: 'POST',
+ headers: {
+ 'accept-encoding': 'gzip',
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'x-new-onboarding-completed': 'false',
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ body: '', // 实际长度为23字节
+ timeout: {
+ connect: 5000,
+ read: 30000
+ }
+ },
+ {
+ url: 'https://api2.cursor.sh/aiserver.v1.AiService/AvailableDocs',
+ method: 'POST',
+ headers: {
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-accept-encoding': 'gzip',
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'x-request-id': uuidv4(),
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ timeout: {
+ connect: 5000,
+ read: 30000
+ }
+ },
+ {
+ url: 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetTeams',
+ method: 'POST',
+ headers: {
+ 'accept-encoding': 'gzip',
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'x-new-onboarding-completed': 'false',
+ 'x-request-id': uuidv4(),
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ body: '',
+ timeout: {
+ connect: 5000,
+ read: 30000
+ }
+ },
+ {
+ url: 'https://api2.cursor.sh/auth/full_stripe_profile',
+ method: 'GET',
+ headers: {
+ 'Host': 'api2.cursor.sh',
+ 'Connection': 'keep-alive',
+ 'Authorization': `Bearer ${authToken}`,
+ 'x-new-onboarding-completed': 'false',
+ 'x-ghost-mode': 'true',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.49.4 Chrome/132.0.6834.210 Electron/34.3.4 Safari/537.36',
+ 'Accept': '*/*',
+ 'Origin': 'vscode-file://vscode-app',
+ 'Sec-Fetch-Site': 'cross-site',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Sec-Fetch-Dest': 'empty',
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
+ 'Accept-Language': 'zh-CN'
+ },
+ timeout: {
+ connect: 5000,
+ read: 30000
+ }
+ },
+ {
+ url: 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests',
+ method: 'POST',
+ headers: {
+ 'accept-encoding': 'gzip',
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'x-new-onboarding-completed': 'false',
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ body: '',
+ timeout: {
+ connect: 5000,
+ read: 30000
+ }
+ },
+ {
+ url: 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetHardLimit',
+ method: 'POST',
+ headers: {
+ 'accept-encoding': 'gzip',
+ 'authorization': `Bearer ${authToken}`,
+ 'connect-protocol-version': '1',
+ 'content-type': 'application/proto',
+ 'user-agent': 'connect-es/1.6.1',
+ 'x-client-key': clientKey,
+ 'x-cursor-checksum': checksum,
+ 'x-cursor-client-version': cursorClientVersion,
+ 'x-cursor-config-version': uuidv4(),
+ 'x-cursor-timezone': 'Asia/Tokyo',
+ 'x-ghost-mode': 'true',
+ 'x-new-onboarding-completed': 'false',
+ 'x-session-id': sessionid,
+ 'Host': 'api2.cursor.sh',
+ },
+ body: '',
+ timeout: {
+ connect: 5000,
+ read: 30000
+ }
+ }
+ ];
+
+ // 随机选择2-4个接口调用
+ const minApis = 2;
+ const maxApis = 4;
+ const numApisToCall = Math.floor(Math.random() * (maxApis - minApis + 1)) + minApis;
+
+ // 随机打乱数组并取前几个元素
+ const shuffledEndpoints = [...endpoints].sort(() => 0.5 - Math.random()).slice(0, numApisToCall);
+
+ // 使用Promise.allSettled确保即使一个请求失败也不会影响其他请求
+ const results = await Promise.allSettled(shuffledEndpoints.map(async (endpoint) => {
+ try {
+ const response = await fetch(endpoint.url, {
+ method: endpoint.method,
+ headers: endpoint.headers,
+ body: endpoint.body || undefined,
+ timeout: endpoint.timeout
+ });
+
+ return {
+ url: endpoint.url,
+ status: response.status,
+ success: true
+ };
+ } catch (error) {
+ // 记录单个请求的错误,但不中断整体流程
+ logger.debug(`其它API调用失败 (${endpoint.url}): ${error.message}`);
+ return {
+ url: endpoint.url,
+ success: false,
+ error: error.message
+ };
+ }
+ }));
+
+ // 记录请求结果统计
+ const successCount = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
+ logger.debug(`其它API调用完成: 成功 ${successCount}/${results.length}`);
+
+ return true;
+ } catch (error) {
+ // 记录整体错误,但不影响主流程
+ logger.error(`others函数执行出错: ${error.message}`);
+ return false;
+ }
+}
+// 在文件末尾添加错误处理函数
+function handleCursorError(errorStr, bearerToken, originalAuthToken) {
+ let message = '';
+ let shouldRemoveCookie = false;
+
+ if (errorStr.includes('Not logged in')) {
+ // 更明确的错误日志
+ if (originalAuthToken === bearerToken) {
+ logger.error(`检测到API Key "${bearerToken}" 中没有可用Cookie,正在尝试以向后兼容模式使用API Key本身`);
+ message = `错误:API Key "${bearerToken}" 中没有可用的Cookie。请添加有效的Cookie到此API Key,或使用其他有效的API Key。\\n\\n详细信息:${errorStr}`;
+ } else {
+ logger.error('检测到无效cookie:', originalAuthToken);
+ message = `错误:Cookie无效或已过期,请更新Cookie。\\n\\n详细信息:${errorStr}`;
+ }
+ shouldRemoveCookie = true;
+ } else if (errorStr.includes('You\'ve reached your trial request limit') || errorStr.includes('You\'ve reached the usage limit for free usage')) {
+ logger.error('检测到额度用尽cookie:', originalAuthToken);
+ message = `错误:Cookie使用额度已用完,请更换Cookie或等待刷新。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = true;
+ } else if (errorStr.includes('User is unauthorized')) {
+ logger.error('检测到未授权cookie:', originalAuthToken);
+ message = `错误:Cookie已被封禁或失效,请更换Cookie。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = true;
+ } else if (errorStr.includes('suspicious activity checks')) {
+ logger.error('检测到IP黑名单:', originalAuthToken);
+ message = `错误:IP可能被列入黑名单,请尝试更换网络环境或使用代理。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = false;
+ } else if (errorStr.includes('Too many computers')) {
+ logger.error('检测到账户暂时被封禁:', originalAuthToken);
+ message = `错误:账户因在多台设备登录而暂时被封禁,请稍后再试或更换账户。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = true;
+ } else if (errorStr.includes('Login expired') || errorStr.includes('login expired')) {
+ logger.error('检测到登录过期cookie:', originalAuthToken);
+ message = `错误:Cookie登录已过期,请更新Cookie。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = true;
+ } else if(errorStr.includes('your request has been blocked due to the use of a temporary email service for this account')) {
+ logger.error('检测到临时邮箱:', originalAuthToken);
+ message = `错误:请求被阻止,检测到临时邮箱服务,请更换邮箱。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = true;
+ } else if (errorStr.includes('Your request has been blocked as our system has detected suspicious activity from your account')) {
+ logger.error('检测到账户异常:', originalAuthToken);
+ message = `错误:请求被阻止,可能是假ban,多重试几次/更换cookie/更换设备。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = false;
+ } else {
+ // 非Cookie相关错误
+ logger.error('检测到其他错误:', errorStr);
+ message = `错误:请求失败。\\n\\n详细信息:${errorStr}`;
+ shouldRemoveCookie = false;
+ }
+
+ return {
+ message,
+ shouldRemoveCookie
+ };
+}
+
+module.exports = router;
diff --git a/src/utils/cookieRefresher.js b/src/utils/cookieRefresher.js
new file mode 100644
index 0000000000000000000000000000000000000000..c32fc9c02d07af80e44ce5e0abf013aadd48ecaf
--- /dev/null
+++ b/src/utils/cookieRefresher.js
@@ -0,0 +1,818 @@
+const fs = require('fs');
+const path = require('path');
+const csv = require('csv-parser');
+const axios = require('axios');
+const AdmZip = require('adm-zip');
+const { Octokit } = require('@octokit/rest');
+const keyManager = require('./keyManager');
+const config = require('../config/config');
+const { extractCookiesFromCsv } = require('./extractCookieFromCsv');
+const logger = require('./logger');
+
+// GitHub 仓库信息从环境变量中获取
+const GITHUB_OWNER = process.env.GITHUB_OWNER || 'liuw1535';
+const GITHUB_REPO = process.env.GITHUB_REPO || 'Cursor-Register';
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN; // 需要在环境变量中设置
+const GITHUB_WORKFLOW_ID = process.env.GITHUB_WORKFLOW_ID || 'register.yml';
+const TRIGGER_WORKFLOW = process.env.TRIGGER_WORKFLOW === 'true';
+
+// 下载目录
+const DOWNLOAD_DIR = path.join(__dirname, '../../downloads');
+const EXTRACT_DIR = path.join(__dirname, '../../extracted');
+
+// 确保目录存在
+function ensureDirectoryExists(dir) {
+ if (!fs.existsSync(dir)) {
+ try {
+ fs.mkdirSync(dir, { recursive: true });
+ logger.info(`创建目录成功: ${dir}`);
+ } catch (err) {
+ logger.error(`创建目录失败: ${dir}`, err);
+ throw err;
+ }
+ }
+}
+
+// 触发 GitHub Actions 工作流
+async function triggerWorkflow() {
+ try {
+ if (!GITHUB_TOKEN) {
+ logger.error('未设置 GITHUB_TOKEN,无法触发工作流');
+ return null;
+ }
+
+ logger.info(`正在触发 GitHub Actions 工作流: ${GITHUB_WORKFLOW_ID}...`);
+ const octokit = new Octokit({
+ auth: GITHUB_TOKEN
+ });
+
+ // 从环境变量获取工作流参数
+ const number = process.env.REGISTER_NUMBER || '2';
+ const maxWorkers = process.env.REGISTER_MAX_WORKERS || '1';
+ const emailServer = process.env.REGISTER_EMAIL_SERVER || 'TempEmail';
+ const ingestToOneapi = process.env.REGISTER_INGEST_TO_ONEAPI === 'true';
+ const uploadArtifact = process.env.REGISTER_UPLOAD_ARTIFACT !== 'false'; // 默认为true
+ const useConfigFile = process.env.REGISTER_USE_CONFIG_FILE !== 'false'; // 默认为true
+ const emailConfigs = process.env.REGISTER_EMAIL_CONFIGS || '[]';
+
+ logger.info(`工作流参数: number=${number}, maxWorkers=${maxWorkers}, emailServer=${emailServer}, ingestToOneapi=${ingestToOneapi}, uploadArtifact=${uploadArtifact}, useConfigFile=${useConfigFile}`);
+
+ // 获取触发前的最新工作流ID,用于后续识别新触发的工作流
+ const { data: beforeWorkflowRuns } = await octokit.actions.listWorkflowRuns({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ workflow_id: GITHUB_WORKFLOW_ID,
+ per_page: 1
+ });
+
+ const latestWorkflowIdBefore = beforeWorkflowRuns.workflow_runs && beforeWorkflowRuns.workflow_runs.length > 0
+ ? beforeWorkflowRuns.workflow_runs[0].id
+ : 0;
+
+ logger.info(`触发前最新工作流ID: ${latestWorkflowIdBefore}`);
+
+ // 触发工作流
+ const response = await octokit.actions.createWorkflowDispatch({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ workflow_id: GITHUB_WORKFLOW_ID,
+ ref: 'main', // 默认使用 main 分支,可以根据需要修改
+ inputs: {
+ number: number,
+ max_workers: maxWorkers,
+ email_server: emailServer,
+ ingest_to_oneapi: ingestToOneapi.toString(),
+ upload_artifact: uploadArtifact.toString(),
+ use_config_file: useConfigFile.toString(),
+ email_configs: emailConfigs
+ }
+ });
+
+ logger.info('工作流触发成功,等待工作流开始运行...');
+
+ // 等待新工作流出现并获取其ID
+ let newWorkflowRunId = null;
+ let findAttempts = 0;
+ const maxFindAttempts = 30; // 最多等待30次,每次5秒
+
+ while (findAttempts < maxFindAttempts && !newWorkflowRunId) {
+ findAttempts++;
+ logger.info(`查找新触发的工作流,尝试 ${findAttempts}/${maxFindAttempts}...`);
+
+ try {
+ const { data: afterWorkflowRuns } = await octokit.actions.listWorkflowRuns({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ workflow_id: GITHUB_WORKFLOW_ID,
+ per_page: 5
+ });
+
+ if (afterWorkflowRuns.workflow_runs && afterWorkflowRuns.workflow_runs.length > 0) {
+ // 查找ID大于之前最新工作流ID的工作流(即新触发的工作流)
+ const newWorkflow = afterWorkflowRuns.workflow_runs.find(run => run.id > latestWorkflowIdBefore);
+ if (newWorkflow) {
+ newWorkflowRunId = newWorkflow.id;
+ logger.info(`找到新触发的工作流,ID: ${newWorkflowRunId}, 状态: ${newWorkflow.status}`);
+ }
+ }
+ } catch (error) {
+ logger.error(`查找工作流时出错 (尝试 ${findAttempts}/${maxFindAttempts}): ${error.message}`);
+ // 出错时继续尝试,不中断循环
+ }
+
+ if (!newWorkflowRunId) {
+ // 等待5秒后再次检查
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ }
+ }
+
+ if (!newWorkflowRunId) {
+ logger.info('未能找到新触发的工作流,可能触发失败');
+ return null;
+ }
+
+ // 等待工作流完成
+ let attempts = 0;
+ const maxAttempts = 120; // 最多等待120次,每次30秒,总共60分钟
+ let consecutiveErrors = 0;
+ const maxConsecutiveErrors = 5; // 最多允许连续5次错误
+
+ while (attempts < maxAttempts) {
+ attempts++;
+ logger.info(`等待工作流完成,尝试 ${attempts}/${maxAttempts}...`);
+
+ try {
+ // 获取工作流状态
+ const { data: workflowRun } = await octokit.actions.getWorkflowRun({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ run_id: newWorkflowRunId
+ });
+
+ // 重置连续错误计数
+ consecutiveErrors = 0;
+
+ logger.info(`工作流状态: ${workflowRun.status}, 结果: ${workflowRun.conclusion || '进行中'}`);
+
+ // 检查工作流是否完成
+ if (workflowRun.status === 'completed') {
+ if (workflowRun.conclusion === 'success') {
+ logger.info(`工作流运行成功,ID: ${newWorkflowRunId}`);
+ return workflowRun;
+ } else {
+ logger.info(`工作流运行失败,结果: ${workflowRun.conclusion}`);
+ return null;
+ }
+ }
+ } catch (error) {
+ consecutiveErrors++;
+ logger.error(`获取工作流状态时出错 (尝试 ${attempts}/${maxAttempts}, 连续错误 ${consecutiveErrors}/${maxConsecutiveErrors}): ${error.message}`);
+
+ // 如果连续错误次数超过阈值,则放弃
+ if (consecutiveErrors >= maxConsecutiveErrors) {
+ logger.error(`连续错误次数超过阈值 (${maxConsecutiveErrors}),放弃等待`);
+ throw new Error(`连续 ${maxConsecutiveErrors} 次获取工作流状态失败: ${error.message}`);
+ }
+
+ // 错误后等待时间稍微延长
+ await new Promise(resolve => setTimeout(resolve, 10000));
+ // 继续循环,不中断
+ continue;
+ }
+
+ // 等待30秒后再次检查
+ await new Promise(resolve => setTimeout(resolve, 30000));
+ }
+
+ logger.info('等待工作流完成超时');
+ return null;
+ } catch (error) {
+ logger.error('触发工作流失败:', error);
+ throw error; // 重新抛出错误,让调用者处理
+ }
+}
+
+// 从 GitHub Actions 获取最新的 Artifact
+async function getLatestArtifact() {
+ try {
+ logger.info('正在连接 GitHub API...');
+ const octokit = new Octokit({
+ auth: GITHUB_TOKEN
+ });
+
+ // 如果配置了自动触发工作流,则先触发工作流
+ let workflowRun = null;
+ if (TRIGGER_WORKFLOW) {
+ logger.info('配置了自动触发工作流,正在触发...');
+ try {
+ workflowRun = await triggerWorkflow();
+ } catch (error) {
+ logger.error('触发工作流过程中出现错误:', error.message);
+ logger.info('尝试继续使用已找到的工作流ID...');
+
+ // 尝试获取最新的工作流,看是否有正在运行的工作流
+ const { data: runningWorkflows } = await octokit.actions.listWorkflowRuns({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ workflow_id: GITHUB_WORKFLOW_ID,
+ status: 'in_progress',
+ per_page: 5
+ });
+
+ if (runningWorkflows.workflow_runs && runningWorkflows.workflow_runs.length > 0) {
+ // 找到正在运行的工作流
+ const runningWorkflow = runningWorkflows.workflow_runs[0];
+ logger.info(`找到正在运行的工作流,ID: ${runningWorkflow.id}, 状态: ${runningWorkflow.status}`);
+
+ // 等待工作流完成
+ let attempts = 0;
+ const maxAttempts = 120; // 最多等待120次,每次30秒,总共60分钟
+ let consecutiveErrors = 0;
+ const maxConsecutiveErrors = 5; // 最多允许连续5次错误
+
+ while (attempts < maxAttempts) {
+ attempts++;
+ logger.info(`等待工作流完成,尝试 ${attempts}/${maxAttempts}...`);
+
+ try {
+ // 获取工作流状态
+ const { data: currentWorkflow } = await octokit.actions.getWorkflowRun({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ run_id: runningWorkflow.id
+ });
+
+ // 重置连续错误计数
+ consecutiveErrors = 0;
+
+ logger.info(`工作流状态: ${currentWorkflow.status}, 结果: ${currentWorkflow.conclusion || '进行中'}`);
+
+ // 检查工作流是否完成
+ if (currentWorkflow.status === 'completed') {
+ if (currentWorkflow.conclusion === 'success') {
+ logger.info(`工作流运行成功,ID: ${currentWorkflow.id}`);
+ workflowRun = currentWorkflow;
+ break;
+ } else {
+ logger.info(`工作流运行失败,结果: ${currentWorkflow.conclusion}`);
+ break;
+ }
+ }
+ } catch (err) {
+ consecutiveErrors++;
+ logger.error(`获取工作流状态时出错 (尝试 ${attempts}/${maxAttempts}, 连续错误 ${consecutiveErrors}/${maxConsecutiveErrors}): ${err.message}`);
+
+ // 如果连续错误次数超过阈值,则放弃
+ if (consecutiveErrors >= maxConsecutiveErrors) {
+ logger.error(`连续错误次数超过阈值 (${maxConsecutiveErrors}),放弃等待`);
+ break;
+ }
+
+ // 错误后等待时间稍微延长
+ await new Promise(resolve => setTimeout(resolve, 10000));
+ // 继续循环,不中断
+ continue;
+ }
+
+ // 等待30秒后再次检查
+ await new Promise(resolve => setTimeout(resolve, 30000));
+ }
+ }
+ }
+
+ if (!workflowRun) {
+ logger.info('触发工作流失败或等待超时,尝试获取最新的工作流运行');
+ }
+ }
+
+ // 如果没有触发工作流或触发失败,则获取最新的工作流运行
+ if (!workflowRun) {
+ logger.info('获取最新的工作流运行...');
+ const { data: workflowRuns } = await octokit.actions.listWorkflowRunsForRepo({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ status: 'success',
+ per_page: 5
+ });
+
+ if (!workflowRuns.workflow_runs || workflowRuns.workflow_runs.length === 0) {
+ logger.info('没有找到成功的工作流运行');
+ return null;
+ }
+
+ // 获取最新成功运行的 Artifacts
+ workflowRun = workflowRuns.workflow_runs[0];
+ }
+
+ logger.info(`找到最新的工作流运行: ${workflowRun.id}`);
+
+ // 等待一段时间,确保Artifact已经上传完成
+ logger.info('等待Artifact上传完成...');
+ await new Promise(resolve => setTimeout(resolve, 10000));
+
+ // 获取工作流的Artifacts
+ let artifacts = null;
+ let artifactAttempts = 0;
+ const maxArtifactAttempts = 10; // 最多尝试10次,每次10秒
+
+ while (artifactAttempts < maxArtifactAttempts && (!artifacts || !artifacts.artifacts || artifacts.artifacts.length === 0)) {
+ artifactAttempts++;
+ logger.info(`尝试获取Artifacts,尝试 ${artifactAttempts}/${maxArtifactAttempts}...`);
+
+ try {
+ const response = await octokit.actions.listWorkflowRunArtifacts({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ run_id: workflowRun.id
+ });
+
+ artifacts = response.data;
+ } catch (error) {
+ logger.error(`获取Artifacts时出错 (尝试 ${artifactAttempts}/${maxArtifactAttempts}): ${error.message}`);
+ // 出错时继续尝试,不中断循环
+ }
+
+ if (!artifacts || !artifacts.artifacts || artifacts.artifacts.length === 0) {
+ logger.info('暂时没有找到Artifacts,等待10秒后重试...');
+ await new Promise(resolve => setTimeout(resolve, 10000));
+ }
+ }
+
+ if (!artifacts || !artifacts.artifacts || artifacts.artifacts.length === 0) {
+ logger.info('没有找到Artifacts,可能工作流没有生成Artifact');
+ return null;
+ }
+
+ logger.info(`找到 ${artifacts.artifacts.length} 个Artifacts`);
+
+ // 查找 Account info Artifact
+ const accountInfoArtifact = artifacts.artifacts.find(artifact =>
+ artifact.name.toLowerCase().includes('account info'));
+
+ if (!accountInfoArtifact) {
+ logger.info('没有找到 Account info Artifact');
+ return null;
+ }
+
+ logger.info(`找到 Account info Artifact: ${accountInfoArtifact.id}`);
+ return accountInfoArtifact;
+ } catch (error) {
+ logger.error('获取 Artifact 失败:', error);
+ return null;
+ }
+}
+
+// 下载 Artifact
+async function downloadArtifact(artifact) {
+ let downloadAttempts = 0;
+ const maxDownloadAttempts = 5; // 最多尝试5次下载
+
+ while (downloadAttempts < maxDownloadAttempts) {
+ downloadAttempts++;
+ try {
+ logger.info(`开始下载 Artifact: ${artifact.id}... (尝试 ${downloadAttempts}/${maxDownloadAttempts})`);
+ ensureDirectoryExists(DOWNLOAD_DIR);
+
+ const octokit = new Octokit({
+ auth: GITHUB_TOKEN
+ });
+
+ // 获取下载 URL
+ const { url } = await octokit.actions.downloadArtifact({
+ owner: GITHUB_OWNER,
+ repo: GITHUB_REPO,
+ artifact_id: artifact.id,
+ archive_format: 'zip'
+ });
+
+ // 下载 zip 文件
+ const zipFilePath = path.join(DOWNLOAD_DIR, `${artifact.id}.zip`);
+ const response = await axios({
+ method: 'get',
+ url: url,
+ responseType: 'arraybuffer',
+ timeout: 60000 // 设置60秒超时
+ });
+
+ fs.writeFileSync(zipFilePath, response.data);
+ logger.info(`Artifact 下载完成: ${zipFilePath}`);
+ return zipFilePath;
+ } catch (error) {
+ logger.error(`下载 Artifact 失败 (尝试 ${downloadAttempts}/${maxDownloadAttempts}): ${error.message}`);
+
+ if (downloadAttempts >= maxDownloadAttempts) {
+ logger.error('达到最大尝试次数,放弃下载');
+ return null;
+ }
+
+ // 等待一段时间后重试
+ const retryDelay = 10000; // 10秒
+ logger.info(`等待 ${retryDelay/1000} 秒后重试...`);
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
+ }
+ }
+
+ return null;
+}
+
+// 解压 Artifact
+async function extractArtifact(zipFilePath) {
+ let extractAttempts = 0;
+ const maxExtractAttempts = 3; // 最多尝试3次解压
+
+ while (extractAttempts < maxExtractAttempts) {
+ extractAttempts++;
+ try {
+ logger.info(`开始解压 Artifact: ${zipFilePath}... (尝试 ${extractAttempts}/${maxExtractAttempts})`);
+ ensureDirectoryExists(EXTRACT_DIR);
+
+ const zip = new AdmZip(zipFilePath);
+ zip.extractAllTo(EXTRACT_DIR, true);
+ logger.info(`Artifact 解压完成: ${EXTRACT_DIR}`);
+
+ // 查找 token CSV 文件
+ const files = fs.readdirSync(EXTRACT_DIR);
+ const tokenFile = files.find(file => file.startsWith('token_') && file.endsWith('.csv'));
+
+ if (!tokenFile) {
+ logger.info('没有找到 token CSV 文件');
+
+ if (extractAttempts >= maxExtractAttempts) {
+ return null;
+ }
+
+ // 等待一段时间后重试
+ const retryDelay = 5000; // 5秒
+ logger.info(`等待 ${retryDelay/1000} 秒后重试...`);
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
+ continue;
+ }
+
+ logger.info(`找到 token CSV 文件: ${tokenFile}`);
+ return path.join(EXTRACT_DIR, tokenFile);
+ } catch (error) {
+ logger.error(`解压 Artifact 失败 (尝试 ${extractAttempts}/${maxExtractAttempts}): ${error.message}`);
+
+ if (extractAttempts >= maxExtractAttempts) {
+ logger.error('达到最大尝试次数,放弃解压');
+ return null;
+ }
+
+ // 等待一段时间后重试
+ const retryDelay = 5000; // 5秒
+ logger.info(`等待 ${retryDelay/1000} 秒后重试...`);
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
+ }
+ }
+
+ return null;
+}
+
+/**
+ * 从CSV文件中提取cookies
+ * @param {string} csvFilePath - CSV文件路径
+ * @returns {Promise} - 提取到的cookie数组
+ */
+async function extractCookiesFromCsvFile(csvFilePath) {
+ const maxExtractAttempts = 3;
+ let attempt = 1;
+
+ while (attempt <= maxExtractAttempts) {
+ logger.info(`尝试从CSV文件提取cookies (尝试 ${attempt}/${maxExtractAttempts})...`);
+
+ try {
+ // 读取文件内容
+ if (!fs.existsSync(csvFilePath)) {
+ logger.error(`CSV文件不存在: ${csvFilePath}`);
+ return [];
+ }
+
+ // 读取文件内容并处理可能的换行符
+ let fileContent = fs.readFileSync(csvFilePath, 'utf8');
+ fileContent = fileContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+
+ // 首先尝试直接从文件内容中提取所有可能的cookie
+ const cookies = [];
+
+ // 1. 检查是否有JWT格式的token (新格式)
+ const jwtRegex = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
+ const jwtMatches = fileContent.match(jwtRegex);
+
+ if (jwtMatches && jwtMatches.length > 0) {
+ logger.info(`直接从文件内容中提取到 ${jwtMatches.length} 个JWT token格式的Cookie`);
+ jwtMatches.forEach(match => {
+ if (!cookies.includes(match)) {
+ cookies.push(match);
+ }
+ });
+ }
+
+ // 2. 检查是否有旧格式的cookie
+ if (fileContent.includes('user_')) {
+ logger.info('文件包含旧格式cookie标识"user_"');
+
+ // 使用旧的提取函数尝试提取
+ try {
+ const oldFormatCookies = await extractCookiesFromCsv(csvFilePath);
+ if (oldFormatCookies && oldFormatCookies.length > 0) {
+ logger.info(`通过提取模块获取到 ${oldFormatCookies.length} 个cookie`);
+ oldFormatCookies.forEach(cookie => {
+ if (!cookies.includes(cookie)) {
+ cookies.push(cookie);
+ }
+ });
+ }
+ } catch (e) {
+ logger.warn('通过提取模块获取cookie失败:', e.message);
+ }
+ }
+
+ // 3. 如果找到了cookie,返回结果
+ if (cookies.length > 0) {
+ const newFormatCount = cookies.filter(c => c.startsWith('ey')).length;
+ const oldFormatCount = cookies.filter(c => c.includes('%3A%3A')).length;
+
+ logger.info(`总共找到 ${cookies.length} 个cookie`);
+ logger.info(`新格式cookie(ey开头): ${newFormatCount}个`);
+ logger.info(`旧格式cookie(包含%3A%3A): ${oldFormatCount}个`);
+ logger.info(`其他格式cookie: ${cookies.length - newFormatCount - oldFormatCount}个`);
+
+ return cookies;
+ }
+
+ logger.warn(`未能从文件中提取到任何cookie (尝试 ${attempt}/${maxExtractAttempts})`);
+ } catch (error) {
+ logger.error(`从CSV文件提取cookies时出错 (尝试 ${attempt}/${maxExtractAttempts}):`, error);
+ }
+
+ attempt++;
+ if (attempt <= maxExtractAttempts) {
+ logger.info(`等待5秒后重试...`);
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ }
+ }
+
+ logger.error(`在 ${maxExtractAttempts} 次尝试后未能从CSV文件提取到cookies`);
+ return [];
+}
+
+// 将新的有效cookie添加到系统中
+function addNewCookiesToSystem(apiKey, newCookies) {
+ try {
+ logger.info(`准备添加 ${newCookies.length} 个新cookie到系统中`);
+
+ // 获取当前的cookies
+ const currentCookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
+ logger.info(`当前API密钥 ${apiKey} 有 ${currentCookies.length} 个cookies`);
+
+ // 获取无效的cookies
+ const invalidCookies = keyManager.getInvalidCookies() || [];
+ logger.info(`系统中有 ${invalidCookies.length || 0} 个无效cookies`);
+
+ // 过滤出新的有效cookie
+ let newValidCookies = [];
+
+ // 检查invalidCookies的类型并相应处理
+ if (invalidCookies instanceof Set) {
+ newValidCookies = newCookies.filter(cookie =>
+ !currentCookies.includes(cookie) && !invalidCookies.has(cookie)
+ );
+ } else if (Array.isArray(invalidCookies)) {
+ newValidCookies = newCookies.filter(cookie =>
+ !currentCookies.includes(cookie) && !invalidCookies.includes(cookie)
+ );
+ } else if (invalidCookies && typeof invalidCookies === 'object') {
+ // 如果是普通对象,检查cookie是否作为键存在
+ newValidCookies = newCookies.filter(cookie =>
+ !currentCookies.includes(cookie) && !(cookie in invalidCookies)
+ );
+ } else {
+ // 如果invalidCookies不是预期的类型,只过滤当前cookies
+ newValidCookies = newCookies.filter(cookie => !currentCookies.includes(cookie));
+ }
+
+ logger.info(`过滤后有 ${newValidCookies.length} 个新的有效cookies`);
+
+ // 验证cookie是否完整
+ const validatedCookies = newValidCookies.filter(cookie => {
+ // 检查是否是新格式的JWT token (ey开头)
+ if (cookie.startsWith('ey') && cookie.includes('.')) {
+ const parts = cookie.split('.');
+ // 检查JWT是否包含三个部分
+ if (parts.length !== 3) {
+ logger.warn(`跳过不完整的JWT cookie (新格式): ${cookie}`);
+ return false;
+ }
+ return true;
+ }
+ // 检查旧格式cookie是否包含JWT的三个部分
+ else if (cookie.includes('%3A%3A')) {
+ const parts = cookie.split('%3A%3A');
+ if (parts.length === 2) {
+ const jwt = parts[1];
+ // 检查JWT是否包含点(表示JWT的三个部分)
+ if (!jwt.includes('.') || jwt.split('.').length !== 3) {
+ logger.warn(`跳过不完整的cookie (旧格式): ${cookie}`);
+ return false;
+ }
+ }
+ }
+ return true;
+ });
+
+ logger.info(`验证完整性后有 ${validatedCookies.length} 个有效cookies`);
+
+ if (validatedCookies.length > 0) {
+ // 添加新的有效cookie到系统
+ keyManager.addOrUpdateApiKey(apiKey, [...currentCookies, ...validatedCookies]);
+ logger.info(`成功添加 ${validatedCookies.length} 个新cookie到API密钥 ${apiKey}`);
+ return validatedCookies.length; // 返回添加的cookie数量
+ } else {
+ logger.info(`没有新的有效cookie需要添加到API密钥 ${apiKey}`);
+ return 0; // 没有添加cookie,返回0
+ }
+ } catch (error) {
+ logger.error('添加新cookie到系统时出错:', error);
+ return 0; // 出错时返回0
+ }
+}
+
+// 清理临时文件
+function cleanupTempFiles() {
+ try {
+ logger.info('开始清理临时文件...');
+
+ // 清理下载目录
+ if (fs.existsSync(DOWNLOAD_DIR)) {
+ fs.readdirSync(DOWNLOAD_DIR).forEach(file => {
+ fs.unlinkSync(path.join(DOWNLOAD_DIR, file));
+ });
+ }
+
+ // 清理解压目录
+ if (fs.existsSync(EXTRACT_DIR)) {
+ fs.readdirSync(EXTRACT_DIR).forEach(file => {
+ fs.unlinkSync(path.join(EXTRACT_DIR, file));
+ });
+ }
+
+ logger.info('临时文件清理完成');
+ } catch (error) {
+ logger.error('清理临时文件失败:', error);
+ }
+}
+
+// 检查 API Key 是否需要补充 Cookie
+function checkApiKeyNeedRefresh(apiKey, minCookieCount = config.refresh.minCookieCount) {
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey);
+ return cookies.length < minCookieCount;
+}
+
+// 将现有cookie全部设为无效并从API Key中移除
+function markExistingCookiesAsInvalid(apiKey) {
+ try {
+ // 获取当前API Key的所有cookie
+ const currentCookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
+ logger.info(`正在将API Key ${apiKey} 的 ${currentCookies.length} 个现有cookie标记为无效...`);
+
+ // 如果没有cookie,直接返回
+ if (currentCookies.length === 0) {
+ logger.info(`API Key ${apiKey} 没有现有cookie,无需标记为无效`);
+ return 0;
+ }
+
+ // 获取无效cookie列表
+ const invalidCookies = keyManager.getInvalidCookies();
+ let markedCount = 0;
+
+ // 遍历cookie并添加到无效列表
+ for (const cookie of currentCookies) {
+ // 将cookie添加到无效集合中
+ if (invalidCookies instanceof Set) {
+ invalidCookies.add(cookie);
+ }
+ markedCount++;
+ }
+
+ // 保存无效cookie到文件
+ keyManager.saveInvalidCookiesToFile();
+
+ // 清空当前API Key的cookie列表
+ keyManager.addOrUpdateApiKey(apiKey, []);
+
+ // 保存更新后的API Keys
+ keyManager.saveApiKeysToFile();
+
+ logger.info(`已将API Key ${apiKey} 的 ${markedCount} 个cookie标记为无效并从API Key中移除`);
+ return markedCount;
+ } catch (error) {
+ logger.error(`标记现有cookie为无效时出错:`, error);
+ return 0;
+ }
+}
+
+// 主函数:自动刷新 Cookie
+async function autoRefreshCookies(apiKey, minCookieCount = config.refresh.minCookieCount) {
+ logger.info(`开始自动刷新 Cookie,目标 API Key: ${apiKey},最小 Cookie 数量: ${minCookieCount}`);
+
+ try {
+ // 检查是否需要刷新
+ if (!checkApiKeyNeedRefresh(apiKey, minCookieCount)) {
+ logger.info(`API Key ${apiKey} 的 Cookie 数量足够,不需要刷新`);
+ return {
+ success: true,
+ message: '当前 Cookie 数量足够,不需要刷新',
+ refreshed: 0
+ };
+ }
+
+ // 获取最新的 Artifact
+ const artifact = await getLatestArtifact();
+ if (!artifact) {
+ return {
+ success: false,
+ message: '获取 Artifact 失败',
+ refreshed: 0
+ };
+ }
+
+ // 下载 Artifact
+ const zipFilePath = await downloadArtifact(artifact);
+ if (!zipFilePath) {
+ return {
+ success: false,
+ message: '下载 Artifact 失败',
+ refreshed: 0
+ };
+ }
+
+ // 解压 Artifact
+ const csvFilePath = await extractArtifact(zipFilePath);
+ if (!csvFilePath) {
+ return {
+ success: false,
+ message: '解压 Artifact 失败',
+ refreshed: 0
+ };
+ }
+
+ // 提取 Cookie
+ const cookies = await extractCookiesFromCsvFile(csvFilePath);
+ if (cookies.length === 0) {
+ return {
+ success: false,
+ message: '没有找到有效的 Cookie',
+ refreshed: 0
+ };
+ }
+
+ // 分析提取到的cookie格式
+ const newFormatCookies = cookies.filter(cookie => cookie.startsWith('ey'));
+ const oldFormatCookies = cookies.filter(cookie => cookie.includes('%3A%3A'));
+ logger.info(`提取到 ${newFormatCookies.length} 个新格式cookie(ey开头)`);
+ logger.info(`提取到 ${oldFormatCookies.length} 个旧格式cookie(包含%3A%3A)`);
+
+ // 根据配置决定是否将现有cookie标记为无效
+ const refreshMode = process.env.COOKIE_REFRESH_MODE || 'append';
+
+ if (refreshMode === 'replace') {
+ // 将现有cookie标记为无效并从API Key中移除
+ logger.info('使用替换模式: 将现有cookie标记为无效');
+ markExistingCookiesAsInvalid(apiKey);
+ } else {
+ logger.info('使用追加模式: 保留现有cookie,只添加新cookie');
+ }
+
+ // 添加新的 Cookie 到系统
+ const addedCount = addNewCookiesToSystem(apiKey, cookies);
+
+ // 清理临时文件
+ cleanupTempFiles();
+
+ return {
+ success: true,
+ message: `成功添加 ${addedCount} 个新 Cookie (新格式: ${newFormatCookies.length}, 旧格式: ${oldFormatCookies.length})`,
+ refreshed: addedCount
+ };
+ } catch (error) {
+ logger.error('自动刷新 Cookie 失败:', error);
+ return {
+ success: false,
+ message: `刷新失败: ${error.message}`,
+ refreshed: 0
+ };
+ }
+}
+
+module.exports = {
+ autoRefreshCookies,
+ checkApiKeyNeedRefresh,
+ getLatestArtifact,
+ downloadArtifact,
+ extractArtifact,
+ extractCookiesFromCsvFile,
+ addNewCookiesToSystem,
+ cleanupTempFiles,
+ triggerWorkflow,
+ markExistingCookiesAsInvalid
+};
\ No newline at end of file
diff --git a/src/utils/envChecker.js b/src/utils/envChecker.js
new file mode 100644
index 0000000000000000000000000000000000000000..8554ceb550e008660efeb85883f466faede1cce4
--- /dev/null
+++ b/src/utils/envChecker.js
@@ -0,0 +1,89 @@
+const fs = require('fs');
+const path = require('path');
+
+// 添加自己的简单日志函数,防止循环依赖
+function log(level, message) {
+ // 只在控制台输出,不写入文件
+ const timestamp = new Date().toISOString();
+ if (level === 'ERROR') {
+ console.error(`[ERROR] ${timestamp} ${message}`);
+ } else if (level === 'WARN') {
+ console.warn(`[WARN] ${timestamp} ${message}`);
+ } else {
+ console.log(`[INFO] ${timestamp} ${message}`);
+ }
+}
+
+/**
+ * 检查 .env 文件是否存在
+ * @returns {boolean} 文件是否存在
+ */
+function checkEnvFileExists() {
+ const envPath = path.resolve(process.cwd(), '.env');
+ return fs.existsSync(envPath);
+}
+
+/**
+ * 检查必要的环境变量是否已设置
+ * @returns {Object} 检查结果,包含是否通过和缺失的变量列表
+ */
+function checkRequiredEnvVars() {
+ // 定义必要的环境变量列表
+ const requiredVars = [
+ 'API_KEYS', // API Keys 配置
+ ];
+
+ // 如果启用了自动刷新,则需要检查相关配置
+ if (process.env.ENABLE_AUTO_REFRESH === 'true') {
+ requiredVars.push(
+ 'GITHUB_TOKEN',
+ 'GITHUB_OWNER',
+ 'GITHUB_REPO',
+ 'GITHUB_WORKFLOW_ID',
+ 'TRIGGER_WORKFLOW'
+ );
+ }
+
+ // 检查每个必要的环境变量
+ const missingVars = requiredVars.filter(varName => !process.env[varName]);
+
+ return {
+ passed: missingVars.length === 0,
+ missingVars
+ };
+}
+
+/**
+ * 执行环境检查,如果不符合要求则退出程序
+ */
+function enforceEnvCheck() {
+ log('INFO', '正在检查环境配置...');
+
+ // 检查 .env 文件是否存在
+ const envFileExists = checkEnvFileExists();
+ if (!envFileExists) {
+ log('ERROR', '\n错误: 未找到 .env 文件!');
+ log('ERROR', '请根据 .env.example 创建 .env 文件并配置必要的环境变量。');
+ log('ERROR', '执行以下命令复制示例文件: cp .env.example .env,或执行npm run setup\n');
+ process.exit(1); // 退出程序,状态码 1 表示错误
+ }
+
+ // 检查必要的环境变量
+ const { passed, missingVars } = checkRequiredEnvVars();
+ if (!passed) {
+ log('ERROR', '\n错误: 以下必要的环境变量未在 .env 文件中设置:');
+ missingVars.forEach(varName => {
+ log('ERROR', ` - ${varName}`);
+ });
+ log('ERROR', '\n请在 .env 文件中配置这些变量后重新启动程序。\n');
+ process.exit(1); // 退出程序,状态码 1 表示错误
+ }
+
+ log('INFO', '环境检查通过,继续启动程序...');
+}
+
+module.exports = {
+ checkEnvFileExists,
+ checkRequiredEnvVars,
+ enforceEnvCheck
+};
\ No newline at end of file
diff --git a/src/utils/extractCookieFromCsv.js b/src/utils/extractCookieFromCsv.js
new file mode 100644
index 0000000000000000000000000000000000000000..8be2bafbe6b9e2336bc1bc62d4a4410a68e1f7b3
--- /dev/null
+++ b/src/utils/extractCookieFromCsv.js
@@ -0,0 +1,269 @@
+const fs = require('fs');
+const path = require('path');
+const csv = require('csv-parser');
+
+/**
+ * 从CSV文件中提取完整的cookie
+ * @param {string} csvFilePath - CSV文件路径
+ * @returns {Promise} - 提取到的cookie数组
+ */
+async function extractCookiesFromCsv(csvFilePath) {
+ return new Promise((resolve, reject) => {
+ try {
+ // 检查文件是否存在
+ if (!fs.existsSync(csvFilePath)) {
+ console.error(`CSV文件不存在: ${csvFilePath}`);
+ return resolve([]);
+ }
+
+ // 读取文件内容
+ const fileContent = fs.readFileSync(csvFilePath, 'utf8');
+ console.log(`文件内容前200个字符: ${fileContent.substring(0, 200)}`);
+
+ // 检查文件是否为空
+ if (!fileContent || fileContent.trim() === '') {
+ console.error('CSV文件为空');
+ return resolve([]);
+ }
+
+ // 首先尝试直接从文件内容中提取所有可能的cookie
+ const cookies = [];
+
+ // 检查是否有JWT格式的token (新格式)
+ const jwtRegex = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
+ const jwtMatches = fileContent.match(jwtRegex);
+
+ if (jwtMatches && jwtMatches.length > 0) {
+ console.log(`直接从文件内容中提取到 ${jwtMatches.length} 个JWT token格式的Cookie`);
+ jwtMatches.forEach(match => {
+ if (!cookies.includes(match)) {
+ cookies.push(match);
+ }
+ });
+ }
+
+ // 检查文件内容是否包含关键字
+ const hasTokenKeyword = fileContent.includes('token');
+ const hasUserPrefix = fileContent.includes('user_');
+ console.log(`文件包含"token"关键字: ${hasTokenKeyword}`);
+ console.log(`文件包含"user_"前缀: ${hasUserPrefix}`);
+
+ // 如果文件包含user_前缀,尝试提取旧格式cookie
+ if (hasUserPrefix) {
+ const oldFormatCookies = extractCookiesFromText(fileContent);
+ if (oldFormatCookies.length > 0) {
+ console.log(`从文件内容中提取到 ${oldFormatCookies.length} 个旧格式Cookie`);
+ oldFormatCookies.forEach(cookie => {
+ if (!cookies.includes(cookie)) {
+ cookies.push(cookie);
+ }
+ });
+ }
+ }
+
+ // 如果已经找到cookie,返回结果
+ if (cookies.length > 0) {
+ console.log(`总共提取到 ${cookies.length} 个Cookie`);
+ return resolve(validateCookies(cookies));
+ }
+
+ // 使用csv-parser解析CSV文件
+ const possibleTokenFields = ['token', 'cookie', 'value', 'Token', 'Cookie', 'Value', 'jwt', 'JWT'];
+
+ fs.createReadStream(csvFilePath)
+ .pipe(csv())
+ .on('data', (row) => {
+ // 检查所有可能的字段名
+ for (const field of possibleTokenFields) {
+ if (row[field]) {
+ // 检查是否是JWT格式
+ if (row[field].startsWith('ey') && row[field].includes('.')) {
+ if (!cookies.includes(row[field])) {
+ cookies.push(row[field]);
+ }
+ break;
+ }
+ // 检查是否是旧格式
+ else if (row[field].includes('user_')) {
+ if (!cookies.includes(row[field])) {
+ cookies.push(row[field]);
+ }
+ break;
+ }
+ }
+ }
+
+ // 如果没有找到预定义的字段,遍历所有字段
+ if (cookies.length === 0) {
+ for (const field in row) {
+ if (row[field] && typeof row[field] === 'string') {
+ // 检查是否是JWT格式
+ if (row[field].startsWith('ey') && row[field].includes('.')) {
+ if (!cookies.includes(row[field])) {
+ cookies.push(row[field]);
+ }
+ break;
+ }
+ // 检查是否是旧格式
+ else if (row[field].includes('user_')) {
+ if (!cookies.includes(row[field])) {
+ cookies.push(row[field]);
+ }
+ break;
+ }
+ }
+ }
+ }
+ })
+ .on('end', () => {
+ console.log(`从CSV解析中提取到 ${cookies.length} 个Cookie`);
+
+ // 如果通过CSV解析没有找到cookie,尝试按行读取
+ if (cookies.length === 0) {
+ console.log('尝试按行读取文件...');
+ const lines = fileContent.split('\n');
+ for (const line of lines) {
+ // 检查是否有JWT格式token
+ if (line.includes('ey')) {
+ const jwtMatches = line.match(jwtRegex);
+ if (jwtMatches) {
+ jwtMatches.forEach(match => {
+ if (!cookies.includes(match)) {
+ cookies.push(match);
+ }
+ });
+ }
+ }
+
+ // 检查是否有旧格式cookie
+ if (line.includes('user_')) {
+ const extractedCookies = extractCookiesFromText(line);
+ extractedCookies.forEach(cookie => {
+ if (!cookies.includes(cookie)) {
+ cookies.push(cookie);
+ }
+ });
+ }
+ }
+ console.log(`按行读取后提取到 ${cookies.length} 个Cookie`);
+ }
+
+ // 验证提取的cookie是否完整
+ const validatedCookies = validateCookies(cookies);
+
+ resolve(validatedCookies);
+ })
+ .on('error', (error) => {
+ console.error('解析CSV文件时出错:', error);
+
+ // 如果已经提取到cookie,直接返回
+ if (cookies.length > 0) {
+ console.log(`解析出错但已提取到 ${cookies.length} 个Cookie,进行验证后返回`);
+ resolve(validateCookies(cookies));
+ } else {
+ // 否则尝试其他方法提取
+ console.log('尝试其他方法提取Cookie...');
+
+ // 尝试提取JWT格式token
+ const jwtMatches = fileContent.match(jwtRegex);
+ if (jwtMatches) {
+ jwtMatches.forEach(match => {
+ if (!cookies.includes(match)) {
+ cookies.push(match);
+ }
+ });
+ }
+
+ // 尝试提取旧格式cookie
+ const oldFormatCookies = extractCookiesFromText(fileContent);
+ oldFormatCookies.forEach(cookie => {
+ if (!cookies.includes(cookie)) {
+ cookies.push(cookie);
+ }
+ });
+
+ console.log(`通过其他方法提取到 ${cookies.length} 个Cookie`);
+ resolve(validateCookies(cookies));
+ }
+ });
+ } catch (error) {
+ console.error('提取Cookie时出错:', error);
+ reject(error);
+ }
+ });
+}
+
+/**
+ * 从文本中提取cookie
+ * @param {string} text - 要提取cookie的文本
+ * @returns {string[]} - 提取到的cookie数组
+ */
+function extractCookiesFromText(text) {
+ const cookies = [];
+
+ // 使用正则表达式匹配user_开头的cookie(旧格式)
+ const oldFormatRegex = /user_[a-zA-Z0-9%]+%3A%3A[a-zA-Z0-9%\.\_\-]+/g;
+ const oldFormatMatches = text.match(oldFormatRegex);
+
+ if (oldFormatMatches) {
+ oldFormatMatches.forEach(match => {
+ if (!cookies.includes(match)) {
+ cookies.push(match);
+ }
+ });
+ }
+
+ // 使用正则表达式匹配以ey开头的JWT格式cookie(新格式)
+ const jwtRegex = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
+ const jwtMatches = text.match(jwtRegex);
+
+ if (jwtMatches) {
+ jwtMatches.forEach(match => {
+ if (!cookies.includes(match)) {
+ cookies.push(match);
+ }
+ });
+ }
+
+ return cookies;
+}
+
+/**
+ * 验证cookie是否完整
+ * @param {string[]} cookies - 要验证的cookie数组
+ * @returns {string[]} - 验证后的cookie数组
+ */
+function validateCookies(cookies) {
+ return cookies.filter(cookie => {
+ // 检查是否是新格式的JWT token (ey开头)
+ if (cookie.startsWith('ey') && cookie.includes('.')) {
+ const parts = cookie.split('.');
+ // 检查JWT是否包含三个部分
+ if (parts.length === 3) {
+ return true; // cookie有效
+ } else {
+ console.warn(`检测到不完整的JWT(新格式): ${cookie}`);
+ return false;
+ }
+ }
+ // 检查旧格式cookie是否完整
+ else if (cookie.includes('%3A%3A')) {
+ const parts = cookie.split('%3A%3A');
+ if (parts.length === 2) {
+ const jwt = parts[1];
+ // 检查JWT是否包含两个点(表示三个部分)
+ if (jwt.includes('.') && jwt.split('.').length === 3) {
+ return true; // cookie完整
+ } else {
+ console.warn(`检测到不完整的JWT(旧格式): ${cookie}`);
+ return false;
+ }
+ }
+ }
+ return true; // 对于无法识别的格式,默认保留
+ });
+}
+
+module.exports = {
+ extractCookiesFromCsv
+};
\ No newline at end of file
diff --git a/src/utils/keyManager.js b/src/utils/keyManager.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f63326fec21e74b3c97ae353916065be118a5a1
--- /dev/null
+++ b/src/utils/keyManager.js
@@ -0,0 +1,410 @@
+const config = require('../config/config');
+const fs = require('fs');
+const path = require('path');
+const logger = require('./logger');
+
+// 定义无效cookie的存储文件路径
+const INVALID_COOKIES_FILE = path.join(__dirname, '../../data/invalid_cookies.json');
+// 定义API Keys的存储文件路径
+const API_KEYS_FILE = path.join(__dirname, '../../data/api_keys.json');
+
+// 确保data目录存在
+function ensureDataDirExists() {
+ const dataDir = path.join(__dirname, '../../data');
+ if (!fs.existsSync(dataDir)) {
+ try {
+ fs.mkdirSync(dataDir, { recursive: true });
+ logger.info(`创建data目录: ${dataDir}`);
+ } catch (err) {
+ logger.error('创建data目录失败:', err);
+ }
+ }
+}
+
+// 存储API key与Cursor cookie的映射关系
+let apiKeyMap = new Map();
+
+// 存储每个API key对应的cookie轮询索引
+let rotationIndexes = new Map();
+
+// 存储被标记为无效的cookie
+let invalidCookies = new Set();
+
+// 从文件加载无效cookie
+function loadInvalidCookiesFromFile() {
+ ensureDataDirExists();
+
+ try {
+ if (fs.existsSync(INVALID_COOKIES_FILE)) {
+ const data = fs.readFileSync(INVALID_COOKIES_FILE, 'utf8');
+ const cookiesArray = JSON.parse(data);
+
+ // 清空当前集合并添加从文件加载的cookie
+ invalidCookies.clear();
+ cookiesArray.forEach(cookie => invalidCookies.add(cookie));
+
+ logger.info(`从文件加载了 ${cookiesArray.length} 个无效cookie`);
+ } else {
+ saveInvalidCookiesToFile(); // 如果文件不存在,创建新文件
+ }
+ } catch (err) {
+ logger.error('加载无效cookie文件失败:', err);
+ saveInvalidCookiesToFile(); // 如果加载失败,尝试创建新文件
+ }
+}
+
+// 将无效cookie保存到文件
+function saveInvalidCookiesToFile() {
+ ensureDataDirExists();
+
+ try {
+ const cookiesArray = Array.from(invalidCookies);
+ fs.writeFileSync(INVALID_COOKIES_FILE, JSON.stringify(cookiesArray, null, 2), 'utf8');
+ logger.info(`已将 ${cookiesArray.length} 个无效cookie保存到文件`);
+ } catch (err) {
+ logger.error('保存无效cookie文件失败:', err);
+ }
+}
+
+// 从文件加载API Keys
+function loadApiKeysFromFile() {
+ ensureDataDirExists();
+
+ try {
+ if (fs.existsSync(API_KEYS_FILE)) {
+ const data = fs.readFileSync(API_KEYS_FILE, 'utf8');
+ const apiKeysObj = JSON.parse(data);
+
+ // 清空现有映射
+ apiKeyMap.clear();
+ rotationIndexes.clear();
+
+ // 统计总cookie数量
+ let totalCookies = 0;
+
+ // 添加从文件加载的API Keys
+ for (const [apiKey, cookies] of Object.entries(apiKeysObj)) {
+ if (Array.isArray(cookies)) {
+ apiKeyMap.set(apiKey, cookies);
+ rotationIndexes.set(apiKey, 0);
+ totalCookies += cookies.length;
+ } else {
+ logger.error(`API Key ${apiKey} 的cookies不是数组,跳过`);
+ }
+ }
+
+ const apiKeyCount = Object.keys(apiKeysObj).length;
+ logger.info(`从文件加载了 ${apiKeyCount} 个API Key,共 ${totalCookies} 个Cookie`);
+ return apiKeyCount > 0;
+ } else {
+ logger.info('API Keys文件不存在,将使用配置中的API Keys');
+ return false;
+ }
+ } catch (err) {
+ logger.error('加载API Keys文件失败:', err);
+ return false;
+ }
+}
+
+// 将API Keys保存到文件
+function saveApiKeysToFile() {
+ ensureDataDirExists();
+
+ try {
+ // 将Map转换为普通对象
+ const apiKeysObj = {};
+ for (const [apiKey, cookies] of apiKeyMap.entries()) {
+ apiKeysObj[apiKey] = cookies;
+ }
+
+ // 使用JSON.stringify时避免特殊字符处理问题
+ const jsonString = JSON.stringify(apiKeysObj, null, 2);
+ fs.writeFileSync(API_KEYS_FILE, jsonString, 'utf8');
+ logger.info(`已将 ${Object.keys(apiKeysObj).length} 个API Key保存到文件`);
+
+ // 简化验证过程
+ try {
+ const savedContent = fs.readFileSync(API_KEYS_FILE, 'utf8');
+ JSON.parse(savedContent); // 只验证JSON格式是否正确
+ logger.info('验证通过: 所有cookie都被完整保存');
+ } catch (verifyErr) {
+ logger.error('验证保存内容时出错:', verifyErr);
+ }
+ } catch (err) {
+ logger.error('保存API Keys文件失败:', err);
+ }
+}
+
+// API Keys初始化函数
+function initializeApiKeys() {
+ // 首先从文件加载现有的API Keys
+ const loadedFromFile = loadApiKeysFromFile();
+
+ // 检查环境变量中是否有API Keys配置
+ const configApiKeys = config.apiKeys;
+ const hasEnvApiKeys = Object.keys(configApiKeys).length > 0;
+
+ if (hasEnvApiKeys) {
+ logger.info('从环境变量检测到API Keys配置,将合并到现有配置...');
+
+ // 记录合并前的Cookie数量
+ let beforeMergeCookies = 0;
+ for (const cookies of apiKeyMap.values()) {
+ beforeMergeCookies += cookies.length;
+ }
+
+ // 合并环境变量中的API Keys到现有映射
+ for (const [apiKey, cookieValue] of Object.entries(configApiKeys)) {
+ // 获取现有的cookies(如果有)
+ const existingCookies = apiKeyMap.get(apiKey) || [];
+
+ // 准备要添加的新cookies
+ let newCookies = [];
+ if (typeof cookieValue === 'string') {
+ newCookies = [cookieValue];
+ } else if (Array.isArray(cookieValue)) {
+ newCookies = cookieValue;
+ }
+
+ // 合并cookies,确保不重复
+ const mergedCookies = [...existingCookies];
+ for (const cookie of newCookies) {
+ if (!mergedCookies.includes(cookie)) {
+ mergedCookies.push(cookie);
+ }
+ }
+
+ // 更新映射
+ apiKeyMap.set(apiKey, mergedCookies);
+
+ // 确保轮询索引存在
+ if (!rotationIndexes.has(apiKey)) {
+ rotationIndexes.set(apiKey, 0);
+ }
+ }
+
+ // 记录合并后的Cookie数量
+ let afterMergeCookies = 0;
+ for (const cookies of apiKeyMap.values()) {
+ afterMergeCookies += cookies.length;
+ }
+
+ logger.info(`合并前共有 ${beforeMergeCookies} 个Cookie,合并后共有 ${afterMergeCookies} 个Cookie`);
+
+ // 保存合并后的结果到文件
+ saveApiKeysToFile();
+ } else if (!loadedFromFile) {
+ logger.warn('警告: 未能从文件加载API Keys,且环境变量中也没有配置API Keys');
+ }
+
+ // 统计API Keys和Cookies数量
+ let totalCookies = 0;
+ for (const cookies of apiKeyMap.values()) {
+ totalCookies += cookies.length;
+ }
+
+ logger.info(`API Keys初始化完成,共有 ${apiKeyMap.size} 个API Key,${totalCookies} 个Cookie`);
+
+ // 加载无效cookie
+ loadInvalidCookiesFromFile();
+
+ // 从API Key中移除已知的无效cookie
+ logger.info('开始从API Keys中移除无效cookie...');
+ removeInvalidCookiesFromApiKeys();
+}
+
+// 从所有API Key中移除已知的无效cookie
+function removeInvalidCookiesFromApiKeys() {
+ let totalRemoved = 0;
+
+ for (const [apiKey, cookies] of apiKeyMap.entries()) {
+ const initialLength = cookies.length;
+
+ // 过滤掉无效的cookie
+ const filteredCookies = cookies.filter(cookie => !invalidCookies.has(cookie));
+
+ // 如果有cookie被移除,更新API Key的cookie列表
+ if (filteredCookies.length < initialLength) {
+ const removedCount = initialLength - filteredCookies.length;
+ totalRemoved += removedCount;
+
+ apiKeyMap.set(apiKey, filteredCookies);
+ rotationIndexes.set(apiKey, 0);
+
+ logger.info(`从API Key ${apiKey} 中移除了 ${removedCount} 个无效cookie,剩余 ${filteredCookies.length} 个`);
+ }
+ }
+
+ logger.info(`总共从API Keys中移除了 ${totalRemoved} 个无效cookie`);
+
+ // 如果有cookie被移除,保存更新后的API Keys
+ if (totalRemoved > 0) {
+ saveApiKeysToFile();
+ }
+}
+
+// 添加或更新API key映射
+function addOrUpdateApiKey(apiKey, cookieValues) {
+ if (!Array.isArray(cookieValues)) {
+ cookieValues = [cookieValues];
+ }
+
+ // 过滤掉已知的无效cookie
+ const validCookies = cookieValues.filter(cookie => !invalidCookies.has(cookie));
+
+ if (validCookies.length < cookieValues.length) {
+ logger.info(`API Key ${apiKey} 中有 ${cookieValues.length - validCookies.length} 个无效cookie被过滤`);
+ }
+
+ apiKeyMap.set(apiKey, validCookies);
+ rotationIndexes.set(apiKey, 0);
+
+ // 保存更新后的API Keys
+ saveApiKeysToFile();
+}
+
+// 删除API key映射
+function removeApiKey(apiKey) {
+ apiKeyMap.delete(apiKey);
+ rotationIndexes.delete(apiKey);
+
+ // 保存更新后的API Keys
+ saveApiKeysToFile();
+}
+
+// 获取API key对应的cookie值(根据轮询策略)
+function getCookieForApiKey(apiKey, strategy = config.defaultRotationStrategy) {
+ // 如果API key不存在,也许是cookie本身,直接返回API key本身(向后兼容)
+ if (!apiKeyMap.has(apiKey)) {
+ return apiKey;
+ }
+ const cookies = apiKeyMap.get(apiKey);
+
+ if (!cookies || cookies.length === 0) {
+ return apiKey;
+ }
+
+ if (cookies.length === 1) {
+ return cookies[0];
+ }
+
+ // 根据策略选择cookie
+ if (strategy === 'random') {
+ // 随机策略
+ const randomIndex = Math.floor(Math.random() * cookies.length);
+ return cookies[randomIndex];
+ } else if(strategy === 'round-robin') {
+ // 轮询策略(round-robin)
+ let currentIndex = rotationIndexes.get(apiKey) || 0;
+ const cookie = cookies[currentIndex];
+
+ // 更新索引
+ currentIndex = (currentIndex + 1) % cookies.length;
+ rotationIndexes.set(apiKey, currentIndex);
+
+ return cookie;
+ } else {
+ // 默认策略(default)
+ return cookies[0];
+ }
+}
+
+// 获取所有API key
+function getAllApiKeys() {
+ return Array.from(apiKeyMap.keys());
+}
+
+// 获取API key对应的所有cookie
+function getAllCookiesForApiKey(apiKey) {
+ return apiKeyMap.get(apiKey) || [];
+}
+
+// 从API key的cookie列表中移除特定cookie
+function removeCookieFromApiKey(apiKey, cookieToRemove) {
+ if (!apiKeyMap.has(apiKey)) {
+ logger.info(`API Key ${apiKey} 不存在,无法移除cookie`);
+ return false;
+ }
+
+ const cookies = apiKeyMap.get(apiKey);
+ const initialLength = cookies.length;
+
+ // 检查是否尝试移除与API Key相同的值(可能是向后兼容模式)
+ if (cookieToRemove === apiKey && initialLength === 0) {
+ logger.info(`API Key ${apiKey} 中没有任何cookie,系统正在尝试以向后兼容模式使用API Key本身`);
+ return false;
+ }
+
+ // 过滤掉要移除的cookie
+ const filteredCookies = cookies.filter(cookie => cookie !== cookieToRemove);
+
+ // 如果长度没变,说明没有找到要移除的cookie
+ if (filteredCookies.length === initialLength) {
+ logger.info(`未找到要移除的cookie: ${cookieToRemove}`);
+ return false;
+ }
+
+ // 更新cookie列表
+ apiKeyMap.set(apiKey, filteredCookies);
+
+ // 重置轮询索引
+ rotationIndexes.set(apiKey, 0);
+
+ // 将移除的cookie添加到无效cookie集合中
+ invalidCookies.add(cookieToRemove);
+
+ // 保存无效cookie到文件
+ saveInvalidCookiesToFile();
+
+ // 保存更新后的API Keys
+ saveApiKeysToFile();
+
+ logger.info(`已从API Key ${apiKey} 中移除cookie: ${cookieToRemove}`);
+ logger.info(`剩余cookie数量: ${filteredCookies.length}`);
+
+ return true;
+}
+
+// 获取所有被标记为无效的cookie
+function getInvalidCookies() {
+ return invalidCookies;
+}
+
+// 清除特定的无效cookie记录
+function clearInvalidCookie(cookie) {
+ const result = invalidCookies.delete(cookie);
+
+ if (result) {
+ // 保存更新后的无效cookie到文件
+ saveInvalidCookiesToFile();
+ }
+
+ return result;
+}
+
+// 清除所有无效cookie记录
+function clearAllInvalidCookies() {
+ invalidCookies.clear();
+
+ // 保存更新后的无效cookie到文件
+ saveInvalidCookiesToFile();
+
+ return true;
+}
+
+module.exports = {
+ addOrUpdateApiKey,
+ removeApiKey,
+ getCookieForApiKey,
+ getAllApiKeys,
+ getAllCookiesForApiKey,
+ initializeApiKeys,
+ removeCookieFromApiKey,
+ getInvalidCookies,
+ clearInvalidCookie,
+ clearAllInvalidCookies,
+ loadInvalidCookiesFromFile,
+ saveInvalidCookiesToFile,
+ loadApiKeysFromFile,
+ saveApiKeysToFile
+};
\ No newline at end of file
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 0000000000000000000000000000000000000000..3e0b3ad3d0e22562889071dc16fc3523dd8b5284
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,392 @@
+// logger.js - 统一的日志系统模块
+const fs = require('fs');
+const path = require('path');
+
+// 避免循环依赖
+let config = null;
+// 延迟加载配置
+function getConfig() {
+ if (!config) {
+ try {
+ config = require('../config/config');
+ } catch (err) {
+ console.error('加载配置文件失败:', err.message);
+ config = { log: { level: 'INFO', format: 'colored' } };
+ }
+ }
+ return config;
+}
+
+const LOG_LEVELS = {
+ ERROR: 0,
+ WARN: 1,
+ INFO: 2,
+ DEBUG: 3,
+ TRACE: 4,
+ HTTP: 2 // HTTP日志级别与INFO相同
+};
+
+// 默认日志级别
+let currentLogLevel = LOG_LEVELS.INFO;
+
+// 日志格式
+let logFormat = 'colored'; // colored, json, text
+
+// 带颜色的控制台输出
+const COLORS = {
+ RESET: '\x1b[0m',
+ RED: '\x1b[31m',
+ YELLOW: '\x1b[33m',
+ GREEN: '\x1b[32m',
+ BLUE: '\x1b[34m',
+ CYAN: '\x1b[36m'
+};
+
+// 日志文件配置
+const LOG_DIR = path.join(__dirname, '../../logs');
+const LOG_FILE = path.join(LOG_DIR, 'app.log');
+const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
+let logToFile = false;
+
+// 内存中存储的日志(用于网页显示)
+const memoryLogs = [];
+const MAX_MEMORY_LOGS = 1000; // 内存中最多保存的日志条数
+
+// 确保日志目录存在
+function ensureLogDirExists() {
+ try {
+ if (!fs.existsSync(LOG_DIR)) {
+ fs.mkdirSync(LOG_DIR, { recursive: true });
+ }
+ return true;
+ } catch (err) {
+ console.error(`创建日志目录失败: ${err.message}`);
+ return false;
+ }
+}
+
+// 初始化文件日志
+function initFileLogging() {
+ const conf = getConfig();
+ if (process.env.LOG_TO_FILE === 'true' || (conf.log && conf.log.toFile)) {
+ if (ensureLogDirExists()) {
+ logToFile = true;
+ // 检查日志文件大小,如果超过最大值则进行轮转
+ if (fs.existsSync(LOG_FILE)) {
+ const stats = fs.statSync(LOG_FILE);
+ if (stats.size > MAX_LOG_SIZE) {
+ rotateLogFile();
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+// 日志文件轮转
+function rotateLogFile() {
+ try {
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+ const newLogFile = path.join(LOG_DIR, `app-${timestamp}.log`);
+ if (fs.existsSync(LOG_FILE)) {
+ fs.renameSync(LOG_FILE, newLogFile);
+ }
+ // 清理旧日志文件,保留最近10个
+ const logFiles = fs.readdirSync(LOG_DIR)
+ .filter(file => file.startsWith('app-') && file.endsWith('.log'))
+ .sort()
+ .reverse();
+
+ if (logFiles.length > 10) {
+ logFiles.slice(10).forEach(file => {
+ try {
+ fs.unlinkSync(path.join(LOG_DIR, file));
+ } catch (err) {
+ console.error(`删除旧日志文件失败: ${err.message}`);
+ }
+ });
+ }
+ } catch (err) {
+ console.error(`日志文件轮转失败: ${err.message}`);
+ logToFile = false;
+ }
+}
+
+// 添加日志到内存
+function addLogToMemory(level, timestamp, ...args) {
+ // 将日志对象添加到内存数组
+ const logEntry = {
+ level,
+ timestamp,
+ message: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')
+ };
+
+ memoryLogs.unshift(logEntry); // 新日志添加到数组开头
+
+ // 保持数组在最大长度以内
+ if (memoryLogs.length > MAX_MEMORY_LOGS) {
+ memoryLogs.pop(); // 移除最旧的日志
+ }
+}
+
+// 将日志写入文件
+function writeLogToFile(level, timestamp, ...args) {
+ if (!logToFile) return;
+
+ try {
+ let logEntry;
+
+ if (logFormat === 'json') {
+ // JSON格式
+ const data = args.map(arg => typeof arg === 'object' ? arg : String(arg));
+ const logObject = {
+ level,
+ timestamp,
+ message: data.length === 1 ? data[0] : data
+ };
+ logEntry = JSON.stringify(logObject) + '\n';
+ } else {
+ // 文本格式
+ logEntry = `[${level}] ${timestamp} ${args.map(arg =>
+ typeof arg === 'object' ? JSON.stringify(arg) : arg
+ ).join(' ')}\n`;
+ }
+
+ fs.appendFileSync(LOG_FILE, logEntry);
+
+ // 检查文件大小,必要时进行轮转
+ const stats = fs.statSync(LOG_FILE);
+ if (stats.size > MAX_LOG_SIZE) {
+ rotateLogFile();
+ }
+ } catch (err) {
+ console.error(`写入日志文件失败: ${err.message}`);
+ logToFile = false;
+ }
+}
+
+// 获取时间戳
+function getTimestamp() {
+ return new Date().toISOString();
+}
+
+// 设置日志级别
+function setLogLevel(level) {
+ if (typeof level === 'string') {
+ level = level.toUpperCase();
+ if (LOG_LEVELS[level] !== undefined) {
+ currentLogLevel = LOG_LEVELS[level];
+ } else {
+ error(`无效的日志级别: ${level}`);
+ }
+ } else if (typeof level === 'number' && level >= 0 && level <= 4) {
+ currentLogLevel = level;
+ } else {
+ error(`无效的日志级别: ${level}`);
+ }
+}
+
+// 设置日志格式
+function setLogFormat(format) {
+ const validFormats = ['colored', 'json', 'text'];
+ if (validFormats.includes(format)) {
+ logFormat = format;
+ return true;
+ } else {
+ error(`无效的日志格式: ${format}`);
+ return false;
+ }
+}
+
+// 格式化控制台日志
+function formatConsoleLog(level, timestamp, color, ...args) {
+ if (logFormat === 'json') {
+ // JSON格式
+ const data = args.map(arg => typeof arg === 'object' ? arg : String(arg));
+ return JSON.stringify({
+ level,
+ timestamp,
+ message: data.length === 1 ? data[0] : data
+ });
+ } else if (logFormat === 'text') {
+ // 纯文本格式(无颜色)
+ return `[${level}] ${timestamp} ${args.join(' ')}`;
+ } else {
+ // 默认:带颜色格式
+ return `${color}[${level}] ${timestamp}${COLORS.RESET} ${args.join(' ')}`;
+ }
+}
+
+// 错误日志
+function error(...args) {
+ if (currentLogLevel >= LOG_LEVELS.ERROR) {
+ const timestamp = getTimestamp();
+ const formattedLog = formatConsoleLog('ERROR', timestamp, COLORS.RED, ...args);
+ console.error(formattedLog);
+ writeLogToFile('ERROR', timestamp, ...args);
+ addLogToMemory('ERROR', timestamp, ...args);
+ }
+}
+
+// 警告日志
+function warn(...args) {
+ if (currentLogLevel >= LOG_LEVELS.WARN) {
+ const timestamp = getTimestamp();
+ const formattedLog = formatConsoleLog('WARN', timestamp, COLORS.YELLOW, ...args);
+ console.warn(formattedLog);
+ writeLogToFile('WARN', timestamp, ...args);
+ addLogToMemory('WARN', timestamp, ...args);
+ }
+}
+
+// 信息日志
+function info(...args) {
+ if (currentLogLevel >= LOG_LEVELS.INFO) {
+ const timestamp = getTimestamp();
+ const formattedLog = formatConsoleLog('INFO', timestamp, COLORS.GREEN, ...args);
+ console.log(formattedLog);
+ writeLogToFile('INFO', timestamp, ...args);
+ addLogToMemory('INFO', timestamp, ...args);
+ }
+}
+
+// 调试日志
+function debug(...args) {
+ if (currentLogLevel >= LOG_LEVELS.DEBUG) {
+ const timestamp = getTimestamp();
+ const formattedLog = formatConsoleLog('DEBUG', timestamp, COLORS.BLUE, ...args);
+ console.log(formattedLog);
+ writeLogToFile('DEBUG', timestamp, ...args);
+ addLogToMemory('DEBUG', timestamp, ...args);
+ }
+}
+
+// 跟踪日志
+function trace(...args) {
+ if (currentLogLevel >= LOG_LEVELS.TRACE) {
+ const timestamp = getTimestamp();
+ const formattedLog = formatConsoleLog('TRACE', timestamp, COLORS.CYAN, ...args);
+ console.log(formattedLog);
+ writeLogToFile('TRACE', timestamp, ...args);
+ addLogToMemory('TRACE', timestamp, ...args);
+ }
+}
+
+// HTTP请求日志 (特殊处理,方便筛选)
+function http(...args) {
+ if (currentLogLevel >= LOG_LEVELS.INFO) {
+ const timestamp = getTimestamp();
+ const formattedLog = formatConsoleLog('HTTP', timestamp, COLORS.CYAN, ...args);
+ console.log(formattedLog);
+ writeLogToFile('HTTP', timestamp, ...args);
+ addLogToMemory('HTTP', timestamp, ...args);
+ }
+}
+
+// 获取内存中的日志
+function getLogs(filter = {}) {
+ let filteredLogs = [...memoryLogs];
+
+ // 按日志级别筛选
+ if (filter.level) {
+ filteredLogs = filteredLogs.filter(log => log.level === filter.level);
+ }
+
+ // 按时间范围筛选
+ if (filter.startTime) {
+ filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= new Date(filter.startTime));
+ }
+
+ if (filter.endTime) {
+ filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) <= new Date(filter.endTime));
+ }
+
+ // 按关键词搜索
+ if (filter.search) {
+ const searchTerm = filter.search.toLowerCase();
+ filteredLogs = filteredLogs.filter(log =>
+ log.message.toLowerCase().includes(searchTerm) ||
+ log.level.toLowerCase().includes(searchTerm)
+ );
+ }
+
+ // 分页
+ const page = filter.page || 1;
+ const pageSize = filter.pageSize || 100;
+ const start = (page - 1) * pageSize;
+ const end = start + pageSize;
+
+ return {
+ logs: filteredLogs.slice(start, end),
+ total: filteredLogs.length,
+ page,
+ pageSize
+ };
+}
+
+// 清除内存日志
+function clearMemoryLogs() {
+ memoryLogs.length = 0;
+ info('内存日志已清除');
+}
+
+// 初始化配置
+function initialize() {
+ try {
+ const conf = getConfig();
+
+ // 初始化日志级别
+ const envLevel = process.env.LOG_LEVEL;
+ if (envLevel) {
+ setLogLevel(envLevel);
+ } else if (conf && conf.log && conf.log.level) {
+ setLogLevel(conf.log.level);
+ }
+
+ // 初始化日志格式
+ const envFormat = process.env.LOG_FORMAT;
+ if (envFormat) {
+ setLogFormat(envFormat);
+ } else if (conf && conf.log && conf.log.format) {
+ setLogFormat(conf.log.format);
+ }
+
+ // 初始化文件日志
+ initFileLogging();
+ } catch (err) {
+ console.error(`初始化日志系统出错: ${err.message}`);
+ }
+}
+
+// 初始化
+initialize();
+
+module.exports = {
+ LOG_LEVELS,
+ setLogLevel,
+ setLogFormat,
+ error,
+ warn,
+ info,
+ debug,
+ trace,
+ http,
+ // 暴露文件日志相关方法
+ enableFileLogging: () => {
+ if (ensureLogDirExists()) {
+ logToFile = true;
+ info('文件日志已启用');
+ return true;
+ }
+ return false;
+ },
+ disableFileLogging: () => {
+ logToFile = false;
+ info('文件日志已禁用');
+ },
+ rotateLogFile,
+ // 添加内存日志相关方法
+ getLogs,
+ clearMemoryLogs
+};
\ No newline at end of file
diff --git a/src/utils/proxyLauncher.js b/src/utils/proxyLauncher.js
new file mode 100644
index 0000000000000000000000000000000000000000..24fc2146f65d8d37d95c3b09ac8c35a54debe785
--- /dev/null
+++ b/src/utils/proxyLauncher.js
@@ -0,0 +1,317 @@
+const { spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+const logger = require('./logger');
+
+let mainProxyProcess = null;
+let othersProxyProcess = null;
+let mainProxyLogStream = null;
+let othersProxyLogStream = null;
+
+/**
+ * 获取当前系统平台
+ * @returns {string} 平台标识
+ */
+function detectPlatform() {
+ const platform = os.platform();
+ const arch = os.arch();
+
+ if (platform === 'win32' && arch === 'x64') {
+ return 'windows_x64';
+ } else if (platform === 'linux' && arch === 'x64') {
+ return 'linux_x64';
+ } else if ((platform === 'android' || platform === 'linux') && (arch === 'arm64' || arch === 'aarch64')) {
+ return 'android_arm64';
+ }
+
+ // 默认返回linux版本
+ logger.warn(`未识别的平台: ${platform} ${arch},将使用linux_x64代理`);
+ return 'linux_x64';
+}
+
+/**
+ * 获取代理服务器可执行文件路径
+ * @param {string} platform 平台类型
+ * @param {string} proxyType 代理类型 ('main' 或 'others')
+ * @returns {string} 可执行文件路径
+ */
+function getProxyExecutablePath(platform, proxyType = 'main') {
+ let proxyDir;
+
+ if (proxyType === 'others') {
+ proxyDir = path.join(process.cwd(), 'src', 'proxy', 'others');
+ } else {
+ proxyDir = path.join(process.cwd(), 'src', 'proxy');
+ }
+
+ // 根据平台选择可执行文件
+ switch (platform) {
+ case 'windows_x64':
+ return path.join(proxyDir, 'cursor_proxy_server_windows_amd64.exe');
+ case 'linux_x64':
+ return path.join(proxyDir, 'cursor_proxy_server_linux_amd64');
+ case 'android_arm64':
+ return path.join(proxyDir, 'cursor_proxy_server_android_arm64');
+ default:
+ logger.warn(`未知平台: ${platform},将使用linux_x64代理`);
+ return path.join(proxyDir, 'cursor_proxy_server_linux_amd64');
+ }
+}
+
+/**
+ * 创建并打开代理服务器日志文件
+ * @param {string} platform 平台类型
+ * @param {string} proxyType 代理类型 ('main' 或 'others')
+ * @returns {fs.WriteStream} 日志文件写入流
+ */
+function createProxyLogFile(platform, proxyType = 'main') {
+ try {
+ // 确保logs目录存在
+ const logsDir = path.join(process.cwd(), 'logs');
+ if (!fs.existsSync(logsDir)) {
+ fs.mkdirSync(logsDir, { recursive: true });
+ }
+
+ // 创建日志文件名,包含日期和平台信息
+ const now = new Date();
+ const dateStr = now.toISOString().split('T')[0];
+ const logFileName = `proxy_server_${proxyType}_${platform}_${dateStr}.log`;
+ const logFilePath = path.join(logsDir, logFileName);
+
+ // 创建日志文件流
+ const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
+
+ // 写入日志文件头
+ const headerLine = `\n\n========== ${proxyType}代理服务器日志 - ${platform} - ${now.toISOString()} ==========\n\n`;
+ logStream.write(headerLine);
+
+ logger.info(`${proxyType}代理服务器详细日志将记录到: ${logFilePath}`);
+
+ return logStream;
+ } catch (error) {
+ logger.error(`创建${proxyType}代理服务器日志文件失败: ${error.message}`);
+ return null;
+ }
+}
+
+/**
+ * 写入日志到代理服务器日志文件
+ * @param {fs.WriteStream} logStream 日志文件流
+ * @param {string} message 日志消息
+ * @param {string} type 日志类型 (stdout 或 stderr)
+ */
+function writeToProxyLog(logStream, message, type = 'stdout') {
+ if (!logStream) return;
+
+ try {
+ const timestamp = new Date().toISOString();
+ const logLine = `[${timestamp}] [${type}] ${message}\n`;
+ logStream.write(logLine);
+ } catch (error) {
+ logger.error(`写入代理服务器日志失败: ${error.message}`);
+ }
+}
+
+/**
+ * 启动单个代理服务器
+ * @param {string} platform 平台类型
+ * @param {string} proxyType 代理类型 ('main' 或 'others')
+ * @param {number} port 代理服务器端口
+ * @returns {object} 包含进程和日志流的对象
+ */
+function startSingleProxyServer(platform, proxyType, port) {
+ try {
+ // 获取可执行文件路径
+ const execPath = getProxyExecutablePath(platform, proxyType);
+
+ // 检查文件是否存在
+ if (!fs.existsSync(execPath)) {
+ logger.error(`${proxyType}代理服务器可执行文件不存在: ${execPath}`);
+ return { process: null, logStream: null };
+ }
+
+ // 在Linux/Android上,设置可执行权限
+ if (platform !== 'windows_x64') {
+ try {
+ fs.chmodSync(execPath, '755');
+ } catch (err) {
+ logger.warn(`无法设置${proxyType}代理服务器可执行权限: ${err.message}`);
+ }
+ }
+
+ // 创建代理服务器日志文件
+ const logStream = createProxyLogFile(platform, proxyType);
+
+ // 启动代理服务器进程
+ logger.info(`正在启动${platform}平台的${proxyType}代理服务器: ${execPath},端口: ${port}`);
+
+ // 添加端口参数
+ const args = port ? [`--port=${port}`] : [];
+
+ const proxyProcess = spawn(execPath, args, {
+ detached: false,
+ stdio: ['ignore', 'pipe', 'pipe']
+ });
+
+ // 记录代理服务器的详细日志到文件
+ proxyProcess.stdout.on('data', (data) => {
+ const output = data.toString().trim();
+ writeToProxyLog(logStream, output, 'stdout');
+ });
+
+ proxyProcess.stderr.on('data', (data) => {
+ const errorOutput = data.toString().trim();
+ writeToProxyLog(logStream, errorOutput, 'stderr');
+
+ // 只在启动失败时记录错误信息到控制台
+ if (!proxyProcess.startSuccessful && errorOutput.includes('error')) {
+ logger.error(`${proxyType}代理服务器启动错误: ${errorOutput.split('\n')[0]}`);
+ }
+ });
+
+ proxyProcess.on('error', (err) => {
+ logger.error(`${proxyType}代理服务器启动失败: ${err.message}`);
+ writeToProxyLog(logStream, `启动失败: ${err.message}`, 'error');
+ return { process: null, logStream: null };
+ });
+
+ proxyProcess.on('close', (code) => {
+ // 只有在非正常退出时记录到控制台
+ if (code !== 0) {
+ logger.warn(`${proxyType}代理服务器已退出,代码: ${code}`);
+ }
+
+ writeToProxyLog(logStream, `进程已退出,退出代码: ${code}`, 'info');
+
+ // 关闭日志文件
+ if (logStream) {
+ logStream.end();
+ }
+ });
+
+ // 等待一段时间确保启动成功
+ setTimeout(() => {
+ if (proxyProcess && proxyProcess.exitCode === null) {
+ proxyProcess.startSuccessful = true;
+ logger.info(`${proxyType}代理服务器已成功启动`);
+ writeToProxyLog(logStream, `${proxyType}代理服务器已成功启动`, 'info');
+ } else {
+ logger.error(`${proxyType}代理服务器启动失败或异常退出`);
+ writeToProxyLog(logStream, `${proxyType}代理服务器启动失败或异常退出`, 'error');
+ }
+ }, 1000);
+
+ return { process: proxyProcess, logStream };
+ } catch (error) {
+ logger.error(`启动${proxyType}代理服务器出错: ${error.message}`);
+ return { process: null, logStream: null };
+ }
+}
+
+/**
+ * 启动代理服务器
+ * @returns {boolean} 是否成功启动
+ */
+function startProxyServer() {
+ try {
+ // 检查是否启用代理
+ const useTlsProxy = process.env.USE_TLS_PROXY === 'true';
+ if (!useTlsProxy) {
+ logger.warn('TLS代理服务器未启用,跳过启动');
+ return true;
+ }
+
+ // 检查是否启用辅助代理服务器
+ const useOthersProxy = process.env.USE_OTHERS_PROXY === 'true';
+
+ // 确定要使用的平台
+ let platform = process.env.PROXY_PLATFORM || 'auto';
+ if (platform === 'auto') {
+ platform = detectPlatform();
+ }
+
+ // 启动主代理服务器(默认使用8080端口)
+ const mainProxy = startSingleProxyServer(platform, 'main', 8080);
+ mainProxyProcess = mainProxy.process;
+ mainProxyLogStream = mainProxy.logStream;
+
+ // 根据配置决定是否启动辅助代理服务器
+ if (useOthersProxy) {
+ logger.info('辅助代理服务器已启用,正在启动...');
+ // 启动others代理服务器(端口 10654)
+ const othersProxy = startSingleProxyServer(platform, 'others', 10654);
+ othersProxyProcess = othersProxy.process;
+ othersProxyLogStream = othersProxy.logStream;
+
+ // 如果辅助代理启动失败,记录警告
+ if (!othersProxyProcess) {
+ logger.warn('辅助代理服务器启动失败');
+ } else {
+ logger.info('辅助代理服务器启动成功');
+ }
+ } else {
+ logger.warn('辅助代理服务器未启用,跳过启动');
+ }
+
+ // 如果主代理启动失败,记录警告
+ if (!mainProxyProcess) {
+ logger.warn('主代理服务器启动失败');
+ return false;
+ }
+
+ return true;
+ } catch (error) {
+ logger.error(`启动代理服务器出错: ${error.message}`);
+ return false;
+ }
+}
+
+/**
+ * 停止代理服务器
+ */
+function stopProxyServer() {
+ const stopSingleProxy = (proxyProcess, logStream, proxyType) => {
+ if (proxyProcess) {
+ logger.info(`正在停止${proxyType}代理服务器...`);
+ writeToProxyLog(logStream, `正在停止${proxyType}代理服务器`, 'info');
+
+ // 在Windows上,使用taskkill强制终止
+ if (os.platform() === 'win32') {
+ try {
+ spawn('taskkill', ['/pid', proxyProcess.pid, '/f', '/t']);
+ } catch (err) {
+ logger.error(`使用taskkill终止${proxyType}代理进程失败: ${err.message}`);
+ writeToProxyLog(logStream, `使用taskkill终止${proxyType}代理进程失败: ${err.message}`, 'error');
+ }
+ } else {
+ // 在Linux/Mac上直接kill
+ proxyProcess.kill('SIGTERM');
+ }
+
+ // 允许一些时间写入最后的日志
+ setTimeout(() => {
+ // 关闭日志文件
+ if (logStream) {
+ logStream.end();
+ }
+ }, 500);
+ }
+ };
+
+ // 停止主代理服务器
+ stopSingleProxy(mainProxyProcess, mainProxyLogStream, 'main');
+ mainProxyProcess = null;
+ mainProxyLogStream = null;
+
+ // 停止others代理服务器
+ stopSingleProxy(othersProxyProcess, othersProxyLogStream, 'others');
+ othersProxyProcess = null;
+ othersProxyLogStream = null;
+}
+
+// 导出模块
+module.exports = {
+ startProxyServer,
+ stopProxyServer
+};
\ No newline at end of file
diff --git a/src/utils/utils.js b/src/utils/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..d683885580ac4d32c2485038a99b9aae9e4a80c8
--- /dev/null
+++ b/src/utils/utils.js
@@ -0,0 +1,206 @@
+const os = require('os');
+const zlib = require('zlib');
+const crypto = require('crypto');
+const { v4: uuidv4 } = require('uuid');
+const $root = require('../proto/message.js');
+
+function generateCursorBody(messages, modelName) {
+
+ const instruction = messages
+ .filter(msg => msg.role === 'system')
+ .map(msg => msg.content)
+ .join('\n')
+
+ const formattedMessages = messages
+ .filter(msg => msg.role !== 'system')
+ .map(msg => ({
+ content: msg.content,
+ role: msg.role === 'user' ? 1 : 2,
+ messageId: uuidv4(),
+ ...(msg.role === 'user' ? { chatModeEnum: 1 } : {})
+ //...(msg.role !== 'user' ? { summaryId: uuidv4() } : {})
+ }));
+
+ const messageIds = formattedMessages.map(msg => {
+ const { role, messageId, summaryId } = msg;
+ return summaryId ? { role, messageId, summaryId } : { role, messageId };
+ });
+
+ const body = {
+ request:{
+ messages: formattedMessages,
+ unknown2: 1,
+ instruction: {
+ instruction: instruction
+ },
+ unknown4: 1,
+ model: {
+ name: modelName,
+ empty: '',
+ },
+ webTool: "",
+ unknown13: 1,
+ cursorSetting: {
+ name: "cursor\\aisettings",
+ unknown3: "",
+ unknown6: {
+ unknwon1: "",
+ unknown2: ""
+ },
+ unknown8: 1,
+ unknown9: 1
+ },
+ unknown19: 1,
+ //unknown22: 1,
+ conversationId: uuidv4(),
+ metadata: {
+ os: "win32",
+ arch: "x64",
+ version: "10.0.22631",
+ path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
+ timestamp: new Date().toISOString(),
+ },
+ unknown27: 0,
+ //unknown29: "",
+ messageIds: messageIds,
+ largeContext: 0,
+ unknown38: 0,
+ chatModeEnum: 1,
+ unknown47: "",
+ unknown48: 0,
+ unknown49: 0,
+ unknown51: 0,
+ unknown53: 1,
+ chatMode: "Ask"
+ }
+ };
+
+ const errMsg = $root.StreamUnifiedChatWithToolsRequest.verify(body);
+ if (errMsg) throw Error(errMsg);
+ const instance = $root.StreamUnifiedChatWithToolsRequest.create(body);
+ let buffer = $root.StreamUnifiedChatWithToolsRequest.encode(instance).finish();
+ let magicNumber = 0x00
+ if (formattedMessages.length >= 3){
+ buffer = zlib.gzipSync(buffer)
+ magicNumber = 0x01
+ }
+
+ const finalBody = Buffer.concat([
+ Buffer.from([magicNumber]),
+ Buffer.from(buffer.length.toString(16).padStart(8, '0'), 'hex'),
+ buffer
+ ])
+
+ return finalBody
+}
+
+function chunkToUtf8String(chunk) {
+ const results = []
+ const thinkingResults = []
+ const contentResults = []
+ const errorResults = { hasError: false, errorMessage: '' }
+ const buffer = Buffer.from(chunk, 'hex');
+ //console.log("Chunk buffer:", buffer.toString('hex'))
+
+ try {
+ for(let i = 0; i < buffer.length; i++){
+ const magicNumber = parseInt(buffer.subarray(i, i + 1).toString('hex'), 16)
+ const dataLength = parseInt(buffer.subarray(i + 1, i + 5).toString('hex'), 16)
+ const data = buffer.subarray(i + 5, i + 5 + dataLength)
+ //console.log("Parsed buffer:", magicNumber, dataLength, data.toString('hex'))
+
+ if (magicNumber == 0 || magicNumber == 1) {
+ const gunzipData = magicNumber == 0 ? data : zlib.gunzipSync(data)
+ const response = $root.StreamUnifiedChatWithToolsResponse.decode(gunzipData);
+ const thinking = response?.message?.thinking?.content
+ if (thinking !== undefined && thinking.length > 0){
+ thinkingResults.push(thinking);
+ // console.log('[DEBUG] 收到 thinking:', thinking);
+ }
+ const content = response?.message?.content
+ if (content !== undefined && content.length > 0){
+ contentResults.push(content)
+ // console.log('[DEBUG] 收到 content:', content);
+ }
+ }
+ else if (magicNumber == 2 || magicNumber == 3) {
+ // Json message
+ const gunzipData = magicNumber == 2 ? data : zlib.gunzipSync(data)
+ const utf8 = gunzipData.toString('utf-8')
+ const message = JSON.parse(utf8)
+
+ if (message != null && (typeof message !== 'object' ||
+ (Array.isArray(message) ? message.length > 0 : Object.keys(message).length > 0))){
+ //results.push(utf8)
+ console.error(utf8)
+
+ // 检查是否为错误消息
+ if (message && message.error) {
+ errorResults.hasError = true;
+ errorResults.errorMessage = utf8;
+ }
+ }
+ }
+ else {
+ //console.log('Unknown magic number when parsing chunk response: ' + magicNumber)
+ }
+
+ i += 5 + dataLength - 1
+ }
+ } catch (err) {
+ console.log('Error parsing chunk response:', err)
+ }
+
+ // 如果存在错误,返回错误对象
+ if (errorResults.hasError) {
+ return { error: errorResults.errorMessage };
+ }
+
+ // 分别返回thinking和content内容
+ return {
+ reasoning_content: thinkingResults.join(''),
+ content: contentResults.join('')
+ };
+}
+
+function generateHashed64Hex(input, salt = '') {
+ const hash = crypto.createHash('sha256');
+ hash.update(input + salt);
+ return hash.digest('hex');
+}
+
+function obfuscateBytes(byteArray) {
+ let t = 165;
+ for (let r = 0; r < byteArray.length; r++) {
+ byteArray[r] = (byteArray[r] ^ t) + (r % 256);
+ t = byteArray[r];
+ }
+ return byteArray;
+}
+
+function generateCursorChecksum(token) {
+ const machineId = generateHashed64Hex(token, 'machineId');
+ const macMachineId = generateHashed64Hex(token, 'macMachineId');
+
+ const timestamp = Math.floor(Date.now() / 1e6);
+ const byteArray = new Uint8Array([
+ (timestamp >> 40) & 255,
+ (timestamp >> 32) & 255,
+ (timestamp >> 24) & 255,
+ (timestamp >> 16) & 255,
+ (timestamp >> 8) & 255,
+ 255 & timestamp,
+ ]);
+
+ const obfuscatedBytes = obfuscateBytes(byteArray);
+ const encodedChecksum = Buffer.from(obfuscatedBytes).toString('base64');
+
+ return `${encodedChecksum}${machineId}/${macMachineId}`;
+}
+
+module.exports = {
+ generateCursorBody,
+ chunkToUtf8String,
+ generateHashed64Hex,
+ generateCursorChecksum
+};
diff --git a/start.bat b/start.bat
new file mode 100644
index 0000000000000000000000000000000000000000..4a12a321a6440c551df8615db74d13212ddd8b13
--- /dev/null
+++ b/start.bat
@@ -0,0 +1,17 @@
+@echo off
+chcp 65001 >nul
+REM 编码:UTF-8
+
+REM 安装依赖
+echo install dependencies...
+call npm install --no-fund --quiet --no-audit
+
+REM 检查上一个命令的退出状态
+if %ERRORLEVEL% neq 0 (
+ echo dependencies installation failed,maybe start application failed
+)
+
+REM 启动应用
+echo start application...
+call npm start
+pause
\ No newline at end of file
diff --git a/start.sh b/start.sh
new file mode 100644
index 0000000000000000000000000000000000000000..ec76f1ed687a64439eb9c3b6b5f9e413dbb33fab
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# 编码:UTF-8
+
+# 安装依赖
+echo "install dependencies..."
+npm install --no-fund --quiet --no-audit
+
+# 检查上一个命令的退出状态
+if [ $? -ne 0 ]; then
+ echo "dependencies installation failed,maybe start application failed"
+fi
+
+# 启动应用
+echo "start application..."
+npm start
\ No newline at end of file
diff --git a/update.bat b/update.bat
new file mode 100644
index 0000000000000000000000000000000000000000..8365cd05f39eeec7a3dc6565d828f0b37ec53f01
--- /dev/null
+++ b/update.bat
@@ -0,0 +1,33 @@
+@echo off
+chcp 65001 >nul
+REM 编码:UTF-8
+
+echo start update process...
+
+REM 还原特定的代理服务器文件
+git checkout HEAD -- src/proxy/cursor_proxy_server_android_arm64 ^
+ src/proxy/cursor_proxy_server_linux_amd64 ^
+ src/proxy/cursor_proxy_server_windows_amd64.exe
+
+if %ERRORLEVEL% neq 0 (
+ echo error:restore proxy server file failed
+ goto fail
+)
+
+REM 拉取远程更新,保留服务器端更改
+git pull -X theirs
+
+if %ERRORLEVEL% neq 0 (
+ echo error:pull update failed,maybe network problem or conflict
+ goto fail
+)
+
+echo update success
+goto end
+
+:fail
+echo update failed,please check network connection or solve conflict
+exit /b 1
+
+:end
+pause
\ No newline at end of file
diff --git a/update.sh b/update.sh
new file mode 100644
index 0000000000000000000000000000000000000000..2ad938c733204e4fe4a16f33186c84985cc8c1b3
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+echo "start update process..."
+
+# 还原特定的代理服务器文件
+git checkout HEAD -- src/proxy/cursor_proxy_server_android_arm64 \
+ src/proxy/cursor_proxy_server_linux_amd64 \
+ src/proxy/cursor_proxy_server_windows_amd64.exe
+
+if [ $? -ne 0 ]; then
+ echo "error:restore proxy server file failed"
+ exit 1
+fi
+
+# 拉取远程更新,保留服务器端更改
+git pull -X theirs
+
+if [ $? -ne 0 ]; then
+ echo "error:pull update failed,maybe network problem or conflict"
+ exit 1
+fi
+
+echo "update success"