diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..235777da767de595de79b7348bbcc2d71b56f21c --- /dev/null +++ b/.env.example @@ -0,0 +1,100 @@ +# 当前配置为默认值,请根据需要修改 + +# 服务器监听端口 +PORT=3000 + +# 路由前缀,必须以 / 开头(如果不为空) +ROUTE_PREFIX= + +# 最高权限的认证令牌,必填 +AUTH_TOKEN= + +# 共享的认证令牌,仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限 +SHARED_TOKEN= + +# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次(已弃用) +# 新版本已经完成优化 +# ENABLE_STREAM_CHECK=true + +# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块(已弃用) +# INCLUDE_STOP_REASON_STREAM=true + +# 令牌文件路径(已弃用) +# TOKEN_FILE=.token + +# 令牌列表文件路径 +TOKEN_LIST_FILE=.tokens + +# (实验性)是否启用慢速池(true/false) +ENABLE_SLOW_POOL=false + +# 允许claude开头的模型请求绕过内置模型限制(true/false) +PASS_ANY_CLAUDE=false + +# 图片处理能力配置 +# 可选值: +# - none 或 disabled:禁用图片功能 +# - base64 或 base64-only:仅支持 base64 编码的图片 +# - all 或 base64-http:支持 base64 和 HTTP 图片 +# 注意:启用 HTTP 支持可能会暴露服务器 IP +VISION_ABILITY=base64 + +# 额度检查配置 +# 可选值: +# - none 或 disabled:禁用额度检查 +# - default:详见 README +# - all 或 everything:额度无条件检查 +# - 以,分隔的模型列表,为空时使用默认值 +USAGE_CHECK=default + +# 是否允许使用动态(自定义)配置的 API Key +DYNAMIC_KEY=false + +# 动态 Key 的标识前缀 +KEY_PREFIX=sk- + +# 默认提示词 +DEFAULT_INSTRUCTIONS="Respond in Chinese by default" + +# 反向代理服务器主机名 +REVERSE_PROXY_HOST= + +# 代理地址配置说明 +# - 留空或 `no`: 不使用任何代理 +# - `system`: 使用系统代理(变量不存在时的默认值) +# - 代理地址: 支持以下格式 +# - 多个代理: `http://localhost:7890,https://username:password@localhost:1234` +# 没有轮询,只是选择第一个格式正确的 +# - 支持的协议: http, https, socks4, socks5, socks5h +PROXIES= + +# 请求体大小限制(单位为MB) +# 默认为2MB (2,097,152 字节) +REQUEST_BODY_LIMIT_MB=2 + +# OpenAI 请求时,token 和 checksum 的分隔符 +TOKEN_DELIMITER=, + +# 同时兼容默认的,作为分隔符 +USE_COMMA_DELIMITER=true + +# 调试 +DEBUG=false + +# 调试文件 +DEBUG_LOG_FILE=debug.log + +# 日志储存条数(最大值2000) +REQUEST_LOGS_LIMIT=100 + +# Cursor 服务超时(秒)(最大值600) +SERVICE_TIMEOUT=30 + +# 包含网络引用 +INCLUDE_WEB_REFERENCES=false + +# 持久化日志文件路径 +LOGS_FILE_PATH=logs.bin + +# 持久化页面配置文件路径 +PAGES_FILE_PATH=pages.bin \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..78032e87e692934217e8ab3e260f390bc0be3eb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +/target +/tools/*/target +/*.log +/*.env +/static/*.min.html +/static/*.min.css +/static/*.min.js +/scripts/.asset-hashes.json +node_modules +.DS_Store +/.vscode +/.cargo +/.token +/.token-list +/.tokens +/cursor-api +/cursor-api.exe +/release + +/*.py +/logs +/dev* +/build* +/*.bin +/result.txt +tools/tokenizer/ + +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..55973c1428fe876f67f211b5678ed818c5099d24 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2798 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "async-compression" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta 0.1.4", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "rkyv 0.7.45", + "serde", + "windows-targets", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cursor-api" +version = "0.1.3-rc.4.3" +dependencies = [ + "axum", + "base64", + "bytes", + "chrono", + "dotenvy", + "flate2", + "futures", + "gif", + "hex", + "image", + "memmap2", + "parking_lot", + "paste", + "prost", + "prost-build", + "rand", + "regex", + "reqwest", + "rkyv 0.7.45", + "serde", + "serde_json", + "sha2", + "sonic-rs", + "sysinfo", + "tokio", + "tokio-stream", + "tower-http", + "url", + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "faststr" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9154486833a83cb5d99de8c4d831314b8ae810dd4ef18d89ceb7a9c7c728dd74" +dependencies = [ + "bytes", + "rkyv 0.8.10", + "serde", + "simdutf8", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "munge" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64142d38c84badf60abf06ff9bd80ad2174306a5b11bd4706535090a30a419df" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb5c1d8184f13f7d0ccbeeca0def2f9a181bce2624302793005f5ca8aa62e5e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.96", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.96", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive 0.1.4", +] + +[[package]] +name = "ptr_meta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" +dependencies = [ + "ptr_meta_derive 0.3.0", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rancor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" +dependencies = [ + "ptr_meta 0.3.0", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rend" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" + +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "async-compression", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta 0.1.4", + "rend 0.4.2", + "rkyv_derive 0.7.45", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e147371c75553e1e2fcdb483944a8540b8438c31426279553b9a8182a9b7b65" +dependencies = [ + "bytes", + "hashbrown 0.15.2", + "indexmap", + "munge", + "ptr_meta 0.3.0", + "rancor", + "rend 0.5.2", + "rkyv_derive 0.8.10", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.8.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_json" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "sonic-number" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a74044c092f4f43ca7a6cfd62854cf9fb5ac8502b131347c990bf22bef1dfe" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "sonic-rs" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0275f9f2f07d47556fe60c2759da8bc4be6083b047b491b2d476aa0bfa558eb1" +dependencies = [ + "bumpalo", + "bytes", + "cfg-if", + "faststr", + "itoa", + "ref-cast", + "ryu", + "serde", + "simdutf8", + "sonic-number", + "sonic-simd", + "thiserror 2.0.11", +] + +[[package]] +name = "sonic-simd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940a24e82c9a97483ef66cef06b92160a8fa5cd74042c57c10b24d99d169d2fc" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.8.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.8.0", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result 0.1.2", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8989bd2fba1bbed65765509516d5f5a3f4b4f4ca --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "cursor-api" +version = "0.1.3-rc.4.3" +edition = "2021" +authors = ["wisdgod "] +description = "OpenAI format compatibility layer for the Cursor API" +repository = "https://github.com/wisdgod/cursor-api" + +[build-dependencies] +prost-build = "0.13.4" +sha2 = { version = "0.10.8", default-features = false } +serde_json = "1.0.134" + +[dependencies] +axum = { version = "0.8.1", features = ["json"] } +base64 = { version = "0.22.1", default-features = false, features = ["std"] } +# brotli = { version = "7.0.0", default-features = false, features = ["std"] } +bytes = "1.9.0" +chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde", "rkyv-64"] } +dotenvy = "0.15.7" +flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] } +futures = { version = "0.3.31", default-features = false, features = ["std"] } +gif = { version = "0.13.1", default-features = false, features = ["std"] } +hex = { version = "0.4.3", default-features = false, features = ["std"] } +image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] } +memmap2 = "0.9.5" +# openssl = { version = "0.10.68", features = ["vendored"] } +parking_lot = "0.12.3" +paste = "1.0.15" +prost = "0.13.4" +rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } +regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] } +reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } +rkyv = { version = "0.7.45", default-features = false, features = ["alloc", "std", "bytecheck", "size_64", "validation", "std"] } +serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } +serde_json = { package = "sonic-rs", version = "0.3.17" } +# serde_json = "1.0.137" +sha2 = { version = "0.10.8", default-features = false } +sysinfo = { version = "0.33.1", default-features = false, features = ["system"] } +tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs", "signal"] } +tokio-stream = { version = "0.1.17", features = ["time"] } +tower-http = { version = "0.6.2", features = ["cors", "limit"] } +url = { version = "2.5.4", default-features = false } +uuid = { version = "1.12.1", features = ["v4"] } + +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +opt-level = 3 + +[features] +default = [] +use-minified = [] diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000000000000000000000000000000000000..259aa0d4abea7329325904aa290b4bd3292ee74f --- /dev/null +++ b/Cross.toml @@ -0,0 +1,5 @@ +[target.x86_64-unknown-linux-gnu] +dockerfile = "Dockerfile.cross" + +[target.aarch64-unknown-linux-gnu] +dockerfile = "Dockerfile.cross.arm64" diff --git a/Cursor API.md b/Cursor API.md new file mode 100644 index 0000000000000000000000000000000000000000..501511063e80432efc0cd7c6502e0953fec4b16d --- /dev/null +++ b/Cursor API.md @@ -0,0 +1,179 @@ +# Cursor API + +## 项目说明 + +### 版本声明 +- 当前版本已进入稳定阶段 +- 以下问题与程序无关,请勿反馈: + - 响应缺字漏字 + - 首字延迟现象 + - 响应出现乱码 +- 性能优势: + - 达到原生客户端响应速度 + - 部分场景下表现更优 +- 开源协议要求: + - Fork 项目禁止以原作者名义进行宣传推广 + - 禁止发布任何形式的官方声明 + +![Cursor API 架构示意图](https://via.placeholder.com/800x400.png?text=Cursor+API+Architecture) + +## 快速入门 + +### 密钥获取 +1. 访问 [Cursor 官网](https://www.cursor.com) 完成注册登录 +2. 开启浏览器开发者工具 (F12) +3. 在 Application → Cookies 中定位 `WorkosCursorSessionToken` +4. 复制第三个字段值(注意:`%3A%3A` 为 `::` 的 URL 编码形式) + +## 配置指南 + +### 环境变量 +| 变量名 | 类型 | 默认值 | 说明 | +|--------|------|--------|-----| +| PORT | int | 3000 | 服务端口号 | +| AUTH_TOKEN | string | 无 | 认证令牌(必需) | +| ROUTE_PREFIX | string | 无 | 路由前缀 | +| TOKEN_LIST_FILE | string | .tokens | Token 存储文件 | + +完整配置参见 [env-example](/env-example) + +### Token 文件规范 +`.tokens` 文件格式: +```plaintext +# 注释行将在下次读取时自动删除 +token1,checksum1 +token2,checksum2 +``` + +文件管理原则: +- 系统自动维护文件内容 +- 仅以下情况需要手动编辑: + - 删除特定 token + - 绑定已有 checksum 到指定 token + +## 模型支持列表 +```json +[ + "claude-3.5-sonnet", + "gpt-4", + "gpt-4o", + "cursor-fast", + "gpt-4o-mini", + "deepseek-v3" +] +``` +*注:模型列表为固定配置,暂不支持自定义扩展* + +## API 文档 + +### 基础对话接口 +**Endpoint** +`POST /v1/chat/completions` + +**认证方式** +`Bearer Token` 三级认证机制: +1. 环境变量 `AUTH_TOKEN` +2. `.token` 文件轮询 +3. 直接 token,checksum 认证(v0.1.3-rc.3+) + +**请求示例** +```json +{ + "model": "gpt-4", + "messages": [ + { + "role": "user", + "content": "解释量子计算的基本原理" + } + ], + "stream": false +} +``` + +**响应示例(非流式)** +```json +{ + "id": "chatcmpl-9Xy...", + "object": "chat.completion", + "created": 1628063500, + "model": "gpt-4", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "量子计算基于量子比特..." + }, + "finish_reason": "stop" + }] +} +``` + +### Token 管理接口 +| 端点 | 方法 | 功能 | +|------|------|-----| +| `/tokens` | GET | Token 信息管理界面 | +| `/tokens/update` | POST | 批量更新 Token 列表 | +| `/tokens/add` | POST | 增量添加 Token | +| `/tokens/delete` | POST | 删除指定 Token | + +```mermaid +sequenceDiagram + participant Client + participant API + Client->>API: POST /tokens/add + API->>API: 验证Token有效性 + API->>File: 写入.tokens + API-->>Client: 返回更新结果 +``` + +## 高级功能 + +### 动态密钥生成 +**Endpoint** +`POST /build-key` + +**优势对比** +| 特性 | 传统模式 | 动态密钥 | +|------|---------|---------| +| 密钥长度 | 较长 | 优化缩短 | +| 配置扩展 | 无 | 支持自定义 | +| 安全等级 | 基础 | 增强编码 | +| 验证效率 | 预校验耗时 | 即时验证 | + +## 系统监控 + +### 健康检查 +**Endpoint** +`GET /health` + +**响应示例** +```json +{ + "status": "success", + "version": "1.2.0", + "uptime": 86400, + "models": ["gpt-4", "claude-3.5"], + "endpoints": ["/v1/chat", "/tokens"] +} +``` + +## 生态工具 + +### 开发辅助工具 +- [Token 获取工具](https://github.com/wisdgod/cursor-api/tree/main/tools/get-token) + 支持 Windows/Linux/macOS 系统 +- [遥测数据重置工具](https://github.com/wisdgod/cursor-api/tree/main/tools/reset-telemetry) + 清除用户使用数据记录 + +## 致谢声明 +本项目的发展离不开以下开源项目的启发: +- [zhx47/cursor-api](https://github.com/zhx47/cursor-api) - 基础架构参考 +- [cursorToApi](https://github.com/luolazyandlazy/cursorToApi) - 认证机制优化方案 + +--- + +> **项目维护说明** +> 我们欢迎社区贡献,但请注意: +> 1. 功能请求需附带使用场景说明 +> 2. Bug 报告请提供复现步骤和环境信息 +> 3. 重要变更需通过 CI/CD 测试流程 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..da4219b506528c0b5eca337519aaaa88bda7b215 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +ARG TARGETARCH +FROM --platform=linux/${TARGETARCH} rust:1.84.0-slim-bookworm as builder + +ARG TARGETARCH + +WORKDIR /app +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential protobuf-compiler pkg-config libssl-dev nodejs npm openssl \ + && rm -rf /var/lib/apt/lists/* + +COPY . . +RUN case "$TARGETARCH" in \ + amd64) TARGET_CPU="x86-64-v3" ;; \ + arm64) TARGET_CPU="neoverse-n1" ;; \ + *) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \ + esac && \ + RUSTFLAGS="-C link-arg=-s -C target-cpu=$TARGET_CPU" cargo build --release && \ + cp target/release/cursor-api /app/cursor-api + +# 运行阶段 +FROM --platform=linux/${TARGETARCH} debian:bookworm-slim + +WORKDIR /app +ENV TZ=Asia/Shanghai + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates tzdata openssl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/cursor-api . + +ENV PORT=3000 +EXPOSE ${PORT} +CMD ["./cursor-api"] \ No newline at end of file diff --git a/Dockerfile.cross b/Dockerfile.cross new file mode 100644 index 0000000000000000000000000000000000000000..d93a73107a4345588bed22ce9766d50751ca3a85 --- /dev/null +++ b/Dockerfile.cross @@ -0,0 +1,31 @@ +# Dockerfile.cross + +FROM --platform=linux/amd64 rust:1.84.0-slim-bookworm + +WORKDIR /app + +# 安装必要的软件包 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libssl-dev \ + protobuf-compiler \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +# 设置环境变量 (如果需要) +# ENV RUSTFLAGS="-C link-arg=-s" + +# 设置 PROTOC 环境变量 (因为你的 build.rs 需要) +ENV PROTOC=/usr/bin/protoc + +# 安装特定版本的 protoc (如果你需要特定版本,例如 29.3;否则可以删除这部分) +# ENV PROTOC_VERSION=29.3 +# ENV PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-x86_64.zip +# RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP} -O /tmp/${PROTOC_ZIP} && \ +# unzip /tmp/${PROTOC_ZIP} -d /usr && \ +# rm /tmp/${PROTOC_ZIP} + +# 验证安装 +RUN protoc --version \ No newline at end of file diff --git a/Dockerfile.cross.arm64 b/Dockerfile.cross.arm64 new file mode 100644 index 0000000000000000000000000000000000000000..6ec6d19977cb1add4be940aef3f1dfe0531c9296 --- /dev/null +++ b/Dockerfile.cross.arm64 @@ -0,0 +1,31 @@ +# Dockerfile.cross + +FROM --platform=linux/arm64 rust:1.84.0-slim-bookworm + +WORKDIR /app + +# 安装必要的软件包 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libssl-dev \ + protobuf-compiler \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +# 设置环境变量 (如果需要) +# ENV RUSTFLAGS="-C link-arg=-s" + +# 设置 PROTOC 环境变量 (因为你的 build.rs 需要) +ENV PROTOC=/usr/bin/protoc + +# 安装特定版本的 protoc (如果你需要特定版本,例如 29.3;否则可以删除这部分) +# ENV PROTOC_VERSION=29.3 +# ENV PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-x86_64.zip +# RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP} -O /tmp/${PROTOC_ZIP} && \ +# unzip /tmp/${PROTOC_ZIP} -d /usr && \ +# rm /tmp/${PROTOC_ZIP} + +# 验证安装 +RUN protoc --version \ No newline at end of file diff --git a/README.md b/README.md index 5dbcc04169b6dd1baa42c55063c79f9410248335..cc3e0a10ee9605b7a884b47776a14786f2c442b9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ colorFrom: purple colorTo: gray sdk: docker pinned: false +app_port: 3000 --- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..0b9105f6d99d5c57e64ef6378cfaeb32eab52839 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,126 @@ +# 参数处理 +param( + [switch]$Static, + [switch]$Help, + [ValidateSet("x86_64", "aarch64", "i686")] + [string]$Architecture +) + +# 设置错误时停止执行 +$ErrorActionPreference = "Stop" + +# 颜色输出函数 +function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue } +function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow } +function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 } + +# 检查必要的工具 +function Check-Requirements { + $tools = @("cargo", "protoc", "npm", "node") + $missing = @() + + foreach ($tool in $tools) { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { + $missing += $tool + } + } + + if ($missing.Count -gt 0) { + Write-Error "缺少必要工具: $($missing -join ', ')" + } +} + +# 帮助信息 +function Show-Help { + Write-Host @" +用法: $(Split-Path $MyInvocation.ScriptName -Leaf) [选项] + +选项: + -Static 使用静态链接(默认动态链接) + -Help 显示此帮助信息 + +不带参数时使用默认配置构建 +"@ +} + +# 构建函数 +function Build-Target { + param ( + [string]$Target, + [string]$RustFlags + ) + + Write-Info "正在构建 $Target..." + + # 设置环境变量 + $env:RUSTFLAGS = $RustFlags + + # 构建 + if ($Target -ne (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) { + cargo build --target $Target --release + } else { + cargo build --release + } + + # 移动编译产物到 release 目录 + $binaryName = "cursor-api" + if ($Static) { + $binaryName += "-static" + } + + $binaryPath = if ($Target -eq (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) { + "target/release/cursor-api.exe" + } else { + "target/$Target/release/cursor-api.exe" + } + + if (Test-Path $binaryPath) { + Copy-Item $binaryPath "release/$binaryName-$Target.exe" + Write-Info "完成构建 $Target" + } else { + Write-Warn "构建产物未找到: $Target" + Write-Warn "查找路径: $binaryPath" + Write-Warn "当前目录内容:" + Get-ChildItem -Recurse target/ + return $false + } + + return $true +} + +if ($Help) { + Show-Help + exit 0 +} + +# 检查依赖 +Check-Requirements + +# 创建 release 目录 +New-Item -ItemType Directory -Force -Path release | Out-Null + +# 设置静态链接标志 +$rustFlags = "" +if ($Static) { + $rustFlags = "-C target-feature=+crt-static" +} + +# 获取目标架构 +$arch = if ($Architecture) { + $Architecture +} else { + switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "x86_64" } + "ARM64" { "aarch64" } + "X86" { "i686" } + default { Write-Error "不支持的架构: $env:PROCESSOR_ARCHITECTURE" } + } +} +$target = "$arch-pc-windows-msvc" + +Write-Info "开始构建..." +if (-not (Build-Target -Target $target -RustFlags $rustFlags)) { + Write-Error "构建失败" +} + +Write-Info "构建完成!" \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..3eb23088e847a6c2b2842cb9b5f2fc4bb5d4f38e --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,194 @@ +#!/bin/bash +set -euo pipefail + +# 颜色输出函数 +info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } +error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; } + +# 检查必要的工具 +check_requirements() { + local missing_tools=() + + # 基础工具检查 + for tool in cargo protoc npm node; do + if ! command -v "$tool" &>/dev/null; then + missing_tools+=("$tool") + fi + done + + if [[ ${#missing_tools[@]} -gt 0 ]]; then + error "缺少必要工具: ${missing_tools[*]}" + fi +} + +# 解析参数 +USE_STATIC=false + +while [[ $# -gt 0 ]]; do + case $1 in + --static) USE_STATIC=true ;; + --help) show_help; exit 0 ;; + *) error "未知参数: $1" ;; + esac + shift +done + +# 帮助信息 +show_help() { + cat << EOF +用法: $(basename "$0") [选项] + +选项: + --static 使用静态链接(默认动态链接) + --help 显示此帮助信息 + +不带参数时只编译当前平台 +EOF +} + +# 并行构建函数 +build_target() { + local target=$1 + local extension="" + local rustflags="${2:-}" + + info "正在构建 $target..." + + # 确定文件后缀 + [[ $target == *"windows"* ]] && extension=".exe" + + # 构建 + if [[ $target != "$CURRENT_TARGET" ]]; then + env RUSTFLAGS="$rustflags" cargo build --target "$target" --release + else + env RUSTFLAGS="$rustflags" cargo build --release + fi + + # 移动编译产物到 release 目录 + local binary_name="cursor-api" + [[ $USE_STATIC == true ]] && binary_name+="-static" + + local binary_path + if [[ $target == "$CURRENT_TARGET" ]]; then + binary_path="target/release/cursor-api$extension" + else + binary_path="target/$target/release/cursor-api$extension" + fi + + if [[ -f "$binary_path" ]]; then + cp "$binary_path" "release/${binary_name}-$target$extension" + info "完成构建 $target" + else + warn "构建产物未找到: $target" + warn "查找路径: $binary_path" + warn "当前目录内容:" + ls -R target/ + return 1 + fi +} + +# 获取 CPU 架构和操作系统 +ARCH=$(uname -m | sed 's/^aarch64\|arm64$/aarch64/;s/^x86_64\|x86-64\|x64\|amd64$/x86_64/') +OS=$(uname -s) + +# 确定当前系统的目标平台 +get_target() { + local arch=$1 + local os=$2 + case "$os" in + "Darwin") echo "${arch}-apple-darwin" ;; + "Linux") + if [[ $USE_STATIC == true ]]; then + echo "${arch}-unknown-linux-musl" + else + echo "${arch}-unknown-linux-gnu" + fi + ;; + "MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;; + "FreeBSD") echo "${arch}-unknown-freebsd" ;; + *) error "不支持的系统: $os" ;; + esac +} + +# 设置当前目标平台 +CURRENT_TARGET=$(get_target "$ARCH" "$OS") + +# 检查是否成功获取目标平台 +[ -z "$CURRENT_TARGET" ] && error "无法确定当前系统的目标平台" + +# 获取系统对应的所有目标 +get_targets() { + case "$1" in + "linux") + # Linux 只构建当前架构 + echo "$CURRENT_TARGET" + ;; + "freebsd") + # FreeBSD 只构建当前架构 + echo "$CURRENT_TARGET" + ;; + "windows") + # Windows 只构建当前架构 + echo "$CURRENT_TARGET" + ;; + "macos") + # macOS 构建所有 macOS 目标 + echo "x86_64-apple-darwin aarch64-apple-darwin" + ;; + *) error "不支持的系统组: $1" ;; + esac +} + +# 检查依赖 +check_requirements + +# 确定要构建的目标 +case "$OS" in + Darwin) + TARGETS=($(get_targets "macos")) + ;; + Linux) + TARGETS=($(get_targets "linux")) + ;; + FreeBSD) + TARGETS=($(get_targets "freebsd")) + ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) + TARGETS=($(get_targets "windows")) + ;; + *) error "不支持的系统: $OS" ;; +esac + +# 创建 release 目录 +mkdir -p release + +# 设置静态链接标志 +RUSTFLAGS="-C link-arg=-s" +[[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-s" + +# 并行构建所有目标 +info "开始构建..." +for target in "${TARGETS[@]}"; do + build_target "$target" "$RUSTFLAGS" & +done + +# 等待所有构建完成 +wait + +# 为 macOS 平台创建通用二进制 +if [[ "$OS" == "Darwin" ]] && [[ ${#TARGETS[@]} -gt 1 ]]; then + binary_suffix="" + [[ $USE_STATIC == true ]] && binary_suffix="-static" + + if [[ -f "release/cursor-api${binary_suffix}-x86_64-apple-darwin" ]] && \ + [[ -f "release/cursor-api${binary_suffix}-aarch64-apple-darwin" ]]; then + info "创建 macOS 通用二进制..." + lipo -create \ + "release/cursor-api${binary_suffix}-x86_64-apple-darwin" \ + "release/cursor-api${binary_suffix}-aarch64-apple-darwin" \ + -output "release/cursor-api${binary_suffix}-universal-apple-darwin" + fi +fi + +info "构建完成!" \ No newline at end of file diff --git a/scripts/minify.js b/scripts/minify.js new file mode 100644 index 0000000000000000000000000000000000000000..a7061474605a97419a1dcbbf98a4e3cc99ba9c9e --- /dev/null +++ b/scripts/minify.js @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +const { minify: minifyHtml } = require('html-minifier-terser'); +const { minify: minifyJs } = require('terser'); +const CleanCSS = require('clean-css'); +const MarkdownIt = require('markdown-it'); +const fs = require('fs'); +const path = require('path'); + +// 配置选项 +const options = { + collapseWhitespace: true, + removeComments: true, + removeEmptyAttributes: true, + removeOptionalTags: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + minifyCSS: true, + minifyJS: true, + processScripts: ['application/json'], +}; + +// CSS 压缩选项 +const cssOptions = { + level: 2 +}; + +// 处理文件 +async function minifyFile(inputPath, outputPath) { + try { + let ext = path.extname(inputPath).toLowerCase(); + if (ext === '.md') ext = '.html'; + const filename = path.basename(inputPath); + let content = fs.readFileSync(inputPath, 'utf8'); + let minified; + + // 特殊处理 readme.html + if (filename.toLowerCase() === 'readme.md') { + const md = new MarkdownIt({ + html: true, + linkify: true, + typographer: true + }); + const readmeMdPath = path.join(__dirname, '..', 'README.md'); + const markdownContent = fs.readFileSync(readmeMdPath, 'utf8'); + // 添加基本的 markdown 样式 + const htmlContent = ` + + + + + + README + + + + ${md.render(markdownContent)} + + + `; + content = htmlContent; + } + + switch (ext) { + case '.html': + minified = await minifyHtml(content, options); + break; + case '.js': + const result = await minifyJs(content); + minified = result.code; + break; + case '.css': + minified = new CleanCSS(cssOptions).minify(content).styles; + break; + default: + throw new Error(`Unsupported file type: ${ext}`); + } + + fs.writeFileSync(outputPath, minified); + console.log(`✓ Minified ${path.basename(inputPath)} -> ${path.basename(outputPath)}`); + } catch (err) { + console.error(`✗ Error processing ${inputPath}:`, err); + process.exit(1); + } +} + +// 主函数 +async function main() { + // 获取命令行参数,跳过前两个参数(node和脚本路径) + const files = process.argv.slice(2); + + if (files.length === 0) { + console.error('No input files specified'); + process.exit(1); + } + + const staticDir = path.join(__dirname, '..', 'static'); + + for (const file of files) { + // 特殊处理 README.md 的输入路径 + let inputPath; + let outputPath; + + if (file.toLowerCase() === 'readme.md') { + inputPath = path.join(__dirname, '..', 'README.md'); + outputPath = path.join(staticDir, 'readme.min.html'); + } else { + inputPath = path.join(staticDir, file); + const ext = path.extname(file); + outputPath = path.join( + staticDir, + file.replace(ext, `.min${ext}`) + ); + } + + await minifyFile(inputPath, outputPath); + } +} + +main(); \ No newline at end of file diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..4b0405b0827608e288ce1ca2393ea8569cf540fa --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,321 @@ +{ + "name": "html-minifier-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "html-minifier-scripts", + "version": "1.0.0", + "dependencies": { + "clean-css": "^5.3.3", + "html-minifier-terser": "^7.2.0", + "markdown-it": "^14.1.0", + "terser": "^5.37.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000000000000000000000000000000000000..b12a16239517eb0e4c807ddc47bd2d84480a7dca --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,14 @@ +{ + "name": "html-minifier-scripts", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=14.0.0" + }, + "dependencies": { + "clean-css": "^5.3.3", + "html-minifier-terser": "^7.2.0", + "markdown-it": "^14.1.0", + "terser": "^5.37.0" + } +} diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..3687fdaa643b6d6fb550bed72613c60e817a0cdd --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,179 @@ +# ôʱִֹͣ +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" # ӿٶ + +# ɫ +function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue } +function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow } +function Write-Success { param($Message) Write-Host "[SUCCESS] $Message" -ForegroundColor Green } +function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 } + +# ԱȨ +function Test-Administrator { + $user = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal $user + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +if (-not (Test-Administrator)) { + Write-Error "ԹԱȨд˽ű" +} + +# Ϣ +function Show-Help { + Write-Host @" +÷: $(Split-Path $MyInvocation.ScriptName -Leaf) [ѡ] + +ѡ: + -NoVS װ Visual Studio Build Tools + -NoRust װ Rust + -NoNode װ Node.js + -Help ʾ˰Ϣ + +ʾ: + .\setup.ps1 + .\setup.ps1 -NoVS + .\setup.ps1 -NoRust -NoNode +"@ +} + +# +param( + [switch]$NoVS, + [switch]$NoRust, + [switch]$NoNode, + [switch]$Help +) + +if ($Help) { + Show-Help + exit 0 +} + +# 鲢װ Chocolatey +function Install-Chocolatey { + Write-Info " Chocolatey..." + if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Info "װ Chocolatey..." + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + try { + Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + } + catch { + Write-Error "װ Chocolatey ʧ: $_" + } + # ˢ» + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + } +} + +# װ Visual Studio Build Tools +function Install-VSBuildTools { + if ($NoVS) { + Write-Info " Visual Studio Build Tools װ" + return + } + + Write-Info " Visual Studio Build Tools..." + $vsPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (-not (Test-Path $vsPath)) { + Write-Info "װ Visual Studio Build Tools..." + try { + # ذװ + $vsInstallerUrl = "https://aka.ms/vs/17/release/vs_BuildTools.exe" + $vsInstallerPath = "$env:TEMP\vs_BuildTools.exe" + Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath + + # װ + $process = Start-Process -FilePath $vsInstallerPath -ArgumentList ` + "--quiet", "--wait", "--norestart", "--nocache", ` + "--installPath", "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools", ` + "--add", "Microsoft.VisualStudio.Workload.VCTools" ` + -NoNewWindow -Wait -PassThru + + if ($process.ExitCode -ne 0) { + Write-Error "Visual Studio Build Tools װʧ" + } + + Remove-Item $vsInstallerPath -Force + } + catch { + Write-Error "װ Visual Studio Build Tools ʧ: $_" + } + } + else { + Write-Info "Visual Studio Build Tools Ѱװ" + } +} + +# װ Rust +function Install-Rust { + if ($NoRust) { + Write-Info " Rust װ" + return + } + + Write-Info " Rust..." + if (-not (Get-Command rustc -ErrorAction SilentlyContinue)) { + Write-Info "װ Rust..." + try { + $rustupInit = "$env:TEMP\rustup-init.exe" + Invoke-WebRequest -Uri "https://win.rustup.rs" -OutFile $rustupInit + Start-Process -FilePath $rustupInit -ArgumentList "-y" -Wait + Remove-Item $rustupInit -Force + + # ˢ» + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + } + catch { + Write-Error "װ Rust ʧ: $_" + } + } + + # Ŀƽ̨ + Write-Info " Rust Ŀƽ̨..." + $arch = if ([Environment]::Is64BitOperatingSystem) { "x86_64" } else { "i686" } + rustup target add "$arch-pc-windows-msvc" +} + +# װ +function Install-Tools { + Write-Info "װҪ..." + + # װ protoc + if (-not (Get-Command protoc -ErrorAction SilentlyContinue)) { + Write-Info "װ Protocol Buffers..." + choco install -y protoc + } + + # װ Git + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Info "װ Git..." + choco install -y git + } + + # װ Node.js + if (-not $NoNode -and -not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Info "װ Node.js..." + choco install -y nodejs + } + + # ˢ» + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") +} + +# +try { + Write-Info "ʼװҪ..." + + Install-Chocolatey + Install-VSBuildTools + Install-Rust + Install-Tools + + Write-Success "װɣ" +} +catch { + Write-Error "װгִ: $_" +} \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000000000000000000000000000000000000..6a42bdac3e66048b1cb72b051ac6278a045d2fb1 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# 设置错误时退出 +set -e + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +info() { + echo -e "${BLUE}[INFO] $1${NC}" +} + +error() { + echo -e "${RED}[ERROR] $1${NC}" + exit 1 +} + +# 检查是否为 root 用户(FreeBSD 和 Linux) +if [ "$(uname)" != "Darwin" ] && [ "$EUID" -ne 0 ]; then + error "请使用 root 权限运行此脚本 (sudo ./setup.sh)" +fi + +# 检测包管理器 +if command -v brew &> /dev/null; then + PKG_MANAGER="brew" + info "检测到 macOS/Homebrew 系统" +elif command -v pkg &> /dev/null; then + PKG_MANAGER="pkg" + info "检测到 FreeBSD 系统" +elif command -v apt-get &> /dev/null; then + PKG_MANAGER="apt-get" + info "检测到 Debian/Ubuntu 系统" +elif command -v dnf &> /dev/null; then + PKG_MANAGER="dnf" + info "检测到 Fedora/RHEL 系统" +elif command -v yum &> /dev/null; then + PKG_MANAGER="yum" + info "检测到 CentOS 系统" +else + error "未检测到支持的包管理器" +fi + +# 更新包管理器缓存 +info "更新包管理器缓存..." +case $PKG_MANAGER in + "brew") + brew update + ;; + "pkg") + pkg update + ;; + *) + $PKG_MANAGER update -y + ;; +esac + +# 安装基础构建工具 +info "安装基础构建工具..." +case $PKG_MANAGER in + "brew") + brew install \ + protobuf \ + pkg-config \ + openssl \ + curl \ + git \ + node + ;; + "pkg") + pkg install -y \ + gmake \ + protobuf \ + pkgconf \ + openssl \ + curl \ + git \ + node + ;; + "apt-get") + $PKG_MANAGER install -y --no-install-recommends \ + build-essential \ + protobuf-compiler \ + pkg-config \ + libssl-dev \ + ca-certificates \ + curl \ + tzdata \ + git + ;; + *) + $PKG_MANAGER install -y \ + gcc \ + gcc-c++ \ + make \ + protobuf-compiler \ + pkg-config \ + openssl-devel \ + ca-certificates \ + curl \ + tzdata \ + git + ;; +esac + +# 安装 Node.js 和 npm(如果还没有通过包管理器安装) +if ! command -v node &> /dev/null && [ "$PKG_MANAGER" != "brew" ] && [ "$PKG_MANAGER" != "pkg" ]; then + info "安装 Node.js 和 npm..." + if [ "$PKG_MANAGER" = "apt-get" ]; then + curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - + $PKG_MANAGER install -y nodejs + else + curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - + $PKG_MANAGER install -y nodejs + fi +fi + +# 安装 Rust(如果未安装) +if ! command -v rustc &> /dev/null; then + info "安装 Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + . "$HOME/.cargo/env" +fi + +# 添加目标平台 +info "添加 Rust 目标平台..." +case "$(uname)" in + "FreeBSD") + rustup target add x86_64-unknown-freebsd + ;; + "Darwin") + rustup target add x86_64-apple-darwin aarch64-apple-darwin + ;; + *) + rustup target add x86_64-unknown-linux-gnu + ;; +esac + +# 清理包管理器缓存 +case $PKG_MANAGER in + "apt-get") + rm -rf /var/lib/apt/lists/* + ;; + "pkg") + pkg clean -y + ;; +esac + +# 设置时区(除了 macOS) +if [ "$(uname)" != "Darwin" ]; then + info "设置时区为 Asia/Shanghai..." + ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +fi + +echo -e "${GREEN}安装完成!${NC}" \ No newline at end of file diff --git a/serve.ts b/serve.ts new file mode 100644 index 0000000000000000000000000000000000000000..31bc485adc52e2ae4e33d57905d0429150c543af --- /dev/null +++ b/serve.ts @@ -0,0 +1 @@ +Deno.serve(async(r:Request)=>{const rs=(s:number,m:string)=>new Response(m,{status:s,headers:{"Access-Control-Allow-Origin":"*"}});const h=r.headers.get("x-co");if(!h)return rs(400,"Missing header");const a=["api2.cursor.sh","www.cursor.com"];if(!a.includes(h))return rs(403,"Host denied");const u=new URL(r.url),p=["/aiserver.v1.AiService/StreamChat","/aiserver.v1.AiService/StreamChatWeb","/auth/full_stripe_profile","/api/usage","/api/auth/me"];if(!p.includes(u.pathname))return rs(404,"Path invalid");const hd=new Headers(r.headers);hd.delete("x-co");hd.set("Host",h);try{const f=await fetch(`https://${h}${u.pathname}${u.search}`,{method:r.method,headers:hd,body:r.body});const fh=new Headers(f.headers);fh.set("Access-Control-Allow-Origin","*");return new Response(f.body,{status:f.status,headers:fh})}catch(e){return rs(500,"Server error")}}); \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000000000000000000000000000000000000..a08e33dacdc10f9d4ff961f2cf8943b596976f90 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod constant; +pub mod model; +pub mod lazy; diff --git a/src/app/config.rs b/src/app/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..2154fa556eab7feaff6c67526c8ac983eb5c9214 --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,159 @@ +use super::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN, model::AppConfig}; +use crate::common::model::{ + config::{ConfigData, ConfigUpdateRequest}, + ApiStatus, ErrorResponse, NormalResponse, +}; +use axum::{ + http::{header::AUTHORIZATION, HeaderMap, StatusCode}, + Json, +}; + +// 定义处理更新操作的宏 +macro_rules! handle_updates { + ($request:expr, $($field:ident => $update_fn:expr),* $(,)?) => { + $( + if let Some(value) = $request.$field { + $update_fn(value); + } + )* + }; +} + +// 定义处理重置操作的宏 +macro_rules! handle_resets { + ($request:expr, $($field:ident => $reset_fn:expr),* $(,)?) => { + $( + if $request.$field.is_some() { + $reset_fn(); + } + )* + }; +} + +pub async fn handle_config_update( + headers: HeaderMap, + Json(request): Json, +) -> Result>, (StatusCode, Json)> { + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(401), + error: Some("未提供认证令牌".to_string()), + message: None, + }), + ))?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(401), + error: Some("无效的认证令牌".to_string()), + message: None, + }), + )); + } + + match request.action.as_str() { + "get" => Ok(Json(NormalResponse { + status: ApiStatus::Success, + data: Some(ConfigData { + page_content: AppConfig::get_page_content(&request.path), + vision_ability: AppConfig::get_vision_ability(), + enable_slow_pool: AppConfig::get_slow_pool(), + enable_all_claude: AppConfig::get_allow_claude(), + usage_check_models: AppConfig::get_usage_check(), + enable_dynamic_key: AppConfig::get_dynamic_key(), + share_token: AppConfig::get_share_token(), + proxies: AppConfig::get_proxies(), + include_web_references: AppConfig::get_web_refs(), + }), + message: None, + })), + + "update" => { + // 处理页面内容更新 + if !request.path.is_empty() && request.content.is_some() { + let content = request.content.unwrap(); + if let Err(e) = AppConfig::update_page_content(&request.path, content) { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(500), + error: Some(format!("更新页面内容失败: {}", e)), + message: None, + }), + )); + } + } + + handle_updates!(request, + vision_ability => AppConfig::update_vision_ability, + enable_slow_pool => AppConfig::update_slow_pool, + enable_all_claude => AppConfig::update_allow_claude, + usage_check_models => AppConfig::update_usage_check, + enable_dynamic_key => AppConfig::update_dynamic_key, + share_token => AppConfig::update_share_token, + proxies => AppConfig::update_proxies, + include_web_references => AppConfig::update_web_refs, + ); + + Ok(Json(NormalResponse { + status: ApiStatus::Success, + data: None, + message: Some("配置已更新".to_string()), + })) + } + + "reset" => { + // 重置页面内容 + if !request.path.is_empty() { + if let Err(e) = AppConfig::reset_page_content(&request.path) { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(500), + error: Some(format!("重置页面内容失败: {}", e)), + message: None, + }), + )); + } + } + + handle_resets!(request, + vision_ability => AppConfig::reset_vision_ability, + enable_slow_pool => AppConfig::reset_slow_pool, + enable_all_claude => AppConfig::reset_allow_claude, + usage_check_models => AppConfig::reset_usage_check, + enable_dynamic_key => AppConfig::reset_dynamic_key, + share_token => AppConfig::reset_share_token, + proxies => AppConfig::reset_proxies, + include_web_references => AppConfig::reset_web_refs, + ); + + Ok(Json(NormalResponse { + status: ApiStatus::Success, + data: None, + message: Some("配置已重置".to_string()), + })) + } + + _ => Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(400), + error: Some("无效的操作类型".to_string()), + message: None, + }), + )), + } +} diff --git a/src/app/constant.rs b/src/app/constant.rs new file mode 100644 index 0000000000000000000000000000000000000000..7b3d3dbbcb8cc310eeb6e7b9c3a0e993073e0b9b --- /dev/null +++ b/src/app/constant.rs @@ -0,0 +1,83 @@ +macro_rules! def_pub_const { + ($name:ident, $value:expr) => { + pub const $name: &'static str = $value; + }; +} + +pub const COMMA: char = ','; + +def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION")); +// def_pub_const!(PKG_NAME, env!("CARGO_PKG_NAME")); +// def_pub_const!(PKG_DESCRIPTION, env!("CARGO_PKG_DESCRIPTION")); +// def_pub_const!(PKG_AUTHORS, env!("CARGO_PKG_AUTHORS")); +// def_pub_const!(PKG_REPOSITORY, env!("CARGO_PKG_REPOSITORY")); + +def_pub_const!(EMPTY_STRING, ""); + +def_pub_const!(COMMA_STRING, ","); + +def_pub_const!(ROUTE_ROOT_PATH, "/"); +def_pub_const!(ROUTE_HEALTH_PATH, "/health"); +def_pub_const!(ROUTE_GET_HASH, "/get-hash"); +def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum"); +def_pub_const!(ROUTE_GET_TIMESTAMP_HEADER, "/get-tsheader"); +def_pub_const!(ROUTE_USER_INFO_PATH, "/userinfo"); +def_pub_const!(ROUTE_API_PATH, "/api"); +def_pub_const!(ROUTE_LOGS_PATH, "/logs"); +def_pub_const!(ROUTE_CONFIG_PATH, "/config"); +def_pub_const!(ROUTE_TOKENS_PATH, "/tokens"); +def_pub_const!(ROUTE_TOKENS_GET_PATH, "/tokens/get"); +def_pub_const!(ROUTE_TOKENS_RELOAD_PATH, "/tokens/reload"); +def_pub_const!(ROUTE_TOKENS_UPDATE_PATH, "/tokens/update"); +def_pub_const!(ROUTE_TOKENS_ADD_PATH, "/tokens/add"); +def_pub_const!(ROUTE_TOKENS_DELETE_PATH, "/tokens/delete"); +def_pub_const!(ROUTE_ENV_EXAMPLE_PATH, "/env-example"); +def_pub_const!(ROUTE_STATIC_PATH, "/static/{path}"); +def_pub_const!(ROUTE_SHARED_STYLES_PATH, "/static/shared-styles.css"); +def_pub_const!(ROUTE_SHARED_JS_PATH, "/static/shared.js"); +def_pub_const!(ROUTE_ABOUT_PATH, "/about"); +def_pub_const!(ROUTE_README_PATH, "/readme"); +def_pub_const!(ROUTE_BASIC_CALIBRATION_PATH, "/basic-calibration"); +def_pub_const!(ROUTE_BUILD_KEY_PATH, "/build-key"); + +def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".tokens"); + +def_pub_const!(STATUS_PENDING, "pending"); +def_pub_const!(STATUS_SUCCESS, "success"); +def_pub_const!(STATUS_FAILED, "failed"); + +def_pub_const!(HEADER_NAME_GHOST_MODE, "x-ghost-mode"); + +def_pub_const!(TRUE, "true"); +def_pub_const!(FALSE, "false"); + +// def_pub_const!(CONTENT_TYPE_PROTO, "application/proto"); +def_pub_const!(CONTENT_TYPE_CONNECT_PROTO, "application/connect+proto"); +def_pub_const!(CONTENT_TYPE_TEXT_HTML_WITH_UTF8, "text/html;charset=utf-8"); +def_pub_const!( + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, + "text/plain;charset=utf-8" +); +def_pub_const!(CONTENT_TYPE_TEXT_CSS_WITH_UTF8, "text/css;charset=utf-8"); +def_pub_const!( + CONTENT_TYPE_TEXT_JS_WITH_UTF8, + "text/javascript;charset=utf-8" +); + +def_pub_const!(AUTHORIZATION_BEARER_PREFIX, "Bearer "); + +def_pub_const!(CURSOR_API2_HOST, "api2.cursor.sh"); +def_pub_const!(CURSOR_HOST, "www.cursor.com"); +def_pub_const!(CURSOR_SETTINGS_URL, "https://www.cursor.com/settings"); + +def_pub_const!(OBJECT_CHAT_COMPLETION, "chat.completion"); +def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk"); + +// def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat"); +// def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo"); + +def_pub_const!(FINISH_REASON_STOP, "stop"); + +def_pub_const!(ERR_INVALID_PATH, "无效的路径"); + +// def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good"); diff --git a/src/app/lazy.rs b/src/app/lazy.rs new file mode 100644 index 0000000000000000000000000000000000000000..8f659dd4b4f5751e07d0ee703721e5eb06a9e30d --- /dev/null +++ b/src/app/lazy.rs @@ -0,0 +1,189 @@ +use super::constant::{ + COMMA, CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_LIST_FILE_NAME, EMPTY_STRING, +}; +use crate::common::utils::{ + parse_ascii_char_from_env, parse_bool_from_env, parse_string_from_env, parse_usize_from_env, +}; +use std::sync::LazyLock; +use tokio::sync::{Mutex, OnceCell}; + +macro_rules! def_pub_static { + // 基础版本:直接存储 String + ($name:ident, $value:expr) => { + pub static $name: LazyLock = LazyLock::new(|| $value); + }; + + // 环境变量版本 + ($name:ident, env: $env_key:expr, default: $default:expr) => { + pub static $name: LazyLock = + LazyLock::new(|| parse_string_from_env($env_key, $default).trim().to_string()); + }; +} + +// macro_rules! def_pub_static_getter { +// ($name:ident) => { +// paste::paste! { +// pub fn []() -> String { +// (*$name).clone() +// } +// } +// }; +// } + +def_pub_static!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING); +def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", default: EMPTY_STRING); +def_pub_static!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME); +def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX)); +def_pub_static!( + ROUTE_CHAT_PATH, + format!("{}/v1/chat/completions", *ROUTE_PREFIX) +); + +pub static START_TIME: LazyLock> = + LazyLock::new(chrono::Local::now); + +pub fn get_start_time() -> chrono::DateTime { + *START_TIME +} + +def_pub_static!(DEFAULT_INSTRUCTIONS, env: "DEFAULT_INSTRUCTIONS", default: "Respond in Chinese by default"); + +def_pub_static!(REVERSE_PROXY_HOST, env: "REVERSE_PROXY_HOST", default: EMPTY_STRING); + +const DEFAULT_KEY_PREFIX: &str = "sk-"; + +pub static KEY_PREFIX: LazyLock = LazyLock::new(|| { + let value = parse_string_from_env("KEY_PREFIX", DEFAULT_KEY_PREFIX) + .trim() + .to_string(); + if value.is_empty() { + DEFAULT_KEY_PREFIX.to_string() + } else { + value + } +}); + +pub static KEY_PREFIX_LEN: LazyLock = LazyLock::new(|| KEY_PREFIX.len()); + +pub static TOKEN_DELIMITER: LazyLock = LazyLock::new(|| { + let delimiter = parse_ascii_char_from_env("TOKEN_DELIMITER", COMMA); + if delimiter.is_ascii_alphabetic() + || delimiter.is_ascii_digit() + || delimiter == '+' + || delimiter == '/' + { + COMMA + } else { + delimiter + } +}); + +pub static USE_COMMA_DELIMITER: LazyLock = LazyLock::new(|| { + let enable = parse_bool_from_env("USE_COMMA_DELIMITER", true); + if enable && *TOKEN_DELIMITER == COMMA { + false + } else { + enable + } +}); + +pub static USE_REVERSE_PROXY: LazyLock = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty()); + +macro_rules! def_cursor_api_url { + ($name:ident, $api_host:expr, $path:expr) => { + pub static $name: LazyLock = LazyLock::new(|| { + let host = if *USE_REVERSE_PROXY { + &*REVERSE_PROXY_HOST + } else { + $api_host + }; + format!("https://{}{}", host, $path) + }); + }; +} + +def_cursor_api_url!( + CURSOR_API2_CHAT_URL, + CURSOR_API2_HOST, + "/aiserver.v1.AiService/StreamChat" +); + +def_cursor_api_url!( + CURSOR_API2_CHAT_WEB_URL, + CURSOR_API2_HOST, + "/aiserver.v1.AiService/StreamChatWeb" +); + +def_cursor_api_url!( + CURSOR_API2_STRIPE_URL, + CURSOR_API2_HOST, + "/auth/full_stripe_profile" +); + +def_cursor_api_url!(CURSOR_USAGE_API_URL, CURSOR_HOST, "/api/usage"); + +def_cursor_api_url!(CURSOR_USER_API_URL, CURSOR_HOST, "/api/auth/me"); + +pub(super) static LOGS_FILE_PATH: LazyLock = + LazyLock::new(|| parse_string_from_env("LOGS_FILE_PATH", "logs.bin")); + +pub(super) static PAGES_FILE_PATH: LazyLock = + LazyLock::new(|| parse_string_from_env("PAGES_FILE_PATH", "pages.bin")); + +pub static DEBUG: LazyLock = LazyLock::new(|| parse_bool_from_env("DEBUG", false)); + +// 使用环境变量 "DEBUG_LOG_FILE" 来指定日志文件路径,默认值为 "debug.log" +static DEBUG_LOG_FILE: LazyLock = + LazyLock::new(|| parse_string_from_env("DEBUG_LOG_FILE", "debug.log")); + +// 使用 OnceCell 结合 Mutex 来异步初始化 LOG_FILE +static LOG_FILE: OnceCell> = OnceCell::const_new(); + +pub(crate) async fn get_log_file() -> &'static Mutex { + LOG_FILE + .get_or_init(|| async { + Mutex::new( + tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&*DEBUG_LOG_FILE) + .await + .expect("无法打开日志文件"), + ) + }) + .await +} + +#[macro_export] +macro_rules! debug_println { + ($($arg:tt)*) => { + if *crate::app::lazy::DEBUG { + let time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let log_message = format!("{} - {}", time, format!($($arg)*)); + use tokio::io::AsyncWriteExt as _; + + // 使用 tokio 的 spawn 在后台异步写入日志 + tokio::spawn(async move { + let log_file = crate::app::lazy::get_log_file().await; + // 使用 MutexGuard 获取可变引用 + let mut file = log_file.lock().await; + if let Err(err) = file.write_all(log_message.as_bytes()).await { + eprintln!("写入日志文件失败: {}", err); + } + if let Err(err) = file.write_all(b"\n").await { + eprintln!("写入换行符失败: {}", err); + } + // 可以选择在写入失败时 panic,或者忽略 + // panic!("写入日志文件失败: {}", err); + }); + } + }; +} + +pub static REQUEST_LOGS_LIMIT: LazyLock = + LazyLock::new(|| std::cmp::min(parse_usize_from_env("REQUEST_LOGS_LIMIT", 100), 2000)); + +pub static SERVICE_TIMEOUT: LazyLock = LazyLock::new(|| { + let timeout = parse_usize_from_env("SERVICE_TIMEOUT", 30); + u64::try_from(timeout).map(|t| t.min(600)).unwrap_or(30) +}); diff --git a/src/app/model.rs b/src/app/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..1bf836bdab7f91cad4bb61a25a67455bf475de57 --- /dev/null +++ b/src/app/model.rs @@ -0,0 +1,486 @@ +use crate::{ + app::constant::{ + EMPTY_STRING, ERR_INVALID_PATH, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BUILD_KEY_PATH, + ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, + ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENS_PATH, + }, + chat::model::Message, + common::{ + client::rebuild_http_client, + model::{userinfo::TokenProfile, ApiStatus}, + utils::{generate_checksum_with_repair, parse_bool_from_env, parse_string_from_env}, + }, +}; +use parking_lot::RwLock; +use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; +use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; + +mod usage_check; +pub use usage_check::UsageCheck; +mod config; +mod proxies; +pub use proxies::Proxies; +mod build_key; +pub use build_key::*; + +use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS}; + +// 页面内容类型枚举 +#[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)] +#[serde(tag = "type", content = "content")] +pub enum PageContent { + #[serde(rename = "default")] + Default, // 默认行为 + #[serde(rename = "text")] + Text(String), // 纯文本 + #[serde(rename = "html")] + Html(String), // HTML 内容 +} + +impl Default for PageContent { + fn default() -> Self { + Self::Default + } +} + +// 静态配置 +#[derive(Default, Clone)] +pub struct AppConfig { + vision_ability: VisionAbility, + slow_pool: bool, + allow_claude: bool, + pages: Pages, + usage_check: UsageCheck, + dynamic_key: bool, + share_token: String, + is_share: bool, + proxies: Proxies, + web_refs: bool, +} + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)] +pub enum VisionAbility { + #[serde(rename = "none", alias = "disabled")] + None, + #[serde(rename = "base64", alias = "base64-only")] + Base64, + #[serde(rename = "all", alias = "base64-http")] + All, +} + +impl VisionAbility { + pub fn from_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "none" | "disabled" => Self::None, + "base64" | "base64-only" => Self::Base64, + "all" | "base64-http" => Self::All, + _ => Self::default(), + } + } + + pub fn is_none(&self) -> bool { + matches!(self, VisionAbility::None) + } +} + +impl Default for VisionAbility { + fn default() -> Self { + Self::Base64 + } +} + +#[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct Pages { + pub root_content: PageContent, + pub logs_content: PageContent, + pub config_content: PageContent, + pub tokeninfo_content: PageContent, + pub shared_styles_content: PageContent, + pub shared_js_content: PageContent, + pub about_content: PageContent, + pub readme_content: PageContent, + pub api_content: PageContent, + pub build_key_content: PageContent, +} + +// 运行时状态 +pub struct AppState { + pub total_requests: u64, + pub active_requests: u64, + pub error_requests: u64, + pub request_logs: Vec, + pub token_infos: Vec, +} + +// 全局配置实例 +pub static APP_CONFIG: LazyLock> = + LazyLock::new(|| RwLock::new(AppConfig::default())); + +macro_rules! config_methods { + ($($field:ident: $type:ty, $default:expr;)*) => { + $( + paste::paste! { + pub fn []() -> $type + where + $type: Copy + PartialEq, + { + APP_CONFIG.read().$field + } + + pub fn [](value: $type) + where + $type: Copy + PartialEq, + { + let current = Self::[](); + if current != value { + APP_CONFIG.write().$field = value; + } + } + + pub fn []() + where + $type: Copy + PartialEq, + { + let default_value = $default; + let current = Self::[](); + if current != default_value { + APP_CONFIG.write().$field = default_value; + } + } + } + )* + }; +} + +macro_rules! config_methods_clone { + ($($field:ident: $type:ty, $default:expr;)*) => { + $( + paste::paste! { + pub fn []() -> $type + where + $type: Clone + PartialEq, + { + APP_CONFIG.read().$field.clone() + } + + pub fn [](value: $type) + where + $type: Clone + PartialEq, + { + let current = Self::[](); + if current != value { + APP_CONFIG.write().$field = value; + } + } + + pub fn []() + where + $type: Clone + PartialEq, + { + let default_value = $default; + let current = Self::[](); + if current != default_value { + APP_CONFIG.write().$field = default_value; + } + } + } + )* + }; +} + +impl AppConfig { + pub fn init() { + let mut config = APP_CONFIG.write(); + config.vision_ability = + VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)); + config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false); + config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false); + config.usage_check = + UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING)); + config.dynamic_key = parse_bool_from_env("DYNAMIC_KEY", false); + config.share_token = parse_string_from_env("SHARED_TOKEN", EMPTY_STRING); + config.is_share = !config.share_token.is_empty(); + config.proxies = match std::env::var("PROXIES") { + Ok(proxies) => Proxies::from_str(proxies.as_str()), + Err(_) => Proxies::default(), + }; + config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false) + } + + config_methods! { + slow_pool: bool, false; + allow_claude: bool, false; + dynamic_key: bool, false; + web_refs: bool, false; + } + + config_methods_clone! { + vision_ability: VisionAbility, VisionAbility::default(); + usage_check: UsageCheck, UsageCheck::default(); + } + + pub fn get_share_token() -> String { + APP_CONFIG.read().share_token.clone() + } + + pub fn update_share_token(value: String) { + let current = Self::get_share_token(); + if current != value { + let mut config = APP_CONFIG.write(); + config.share_token = value; + config.is_share = !config.share_token.is_empty(); + } + } + + pub fn reset_share_token() { + let current = Self::get_share_token(); + if !current.is_empty() { + let mut config = APP_CONFIG.write(); + config.share_token = String::new(); + config.is_share = false; + } + } + + pub fn get_proxies() -> Proxies { + APP_CONFIG.read().proxies.clone() + } + + pub fn update_proxies(value: Proxies) { + let current = Self::get_proxies(); + if current != value { + let mut config = APP_CONFIG.write(); + config.proxies = value; + rebuild_http_client(); + } + } + + pub fn reset_proxies() { + let default_value = Proxies::default(); + let current = Self::get_proxies(); + if current != default_value { + let mut config = APP_CONFIG.write(); + config.proxies = default_value; + rebuild_http_client(); + } + } + + pub fn get_page_content(path: &str) -> Option { + match path { + ROUTE_ROOT_PATH => Some(APP_CONFIG.read().pages.root_content.clone()), + ROUTE_LOGS_PATH => Some(APP_CONFIG.read().pages.logs_content.clone()), + ROUTE_CONFIG_PATH => Some(APP_CONFIG.read().pages.config_content.clone()), + ROUTE_TOKENS_PATH => Some(APP_CONFIG.read().pages.tokeninfo_content.clone()), + ROUTE_SHARED_STYLES_PATH => Some(APP_CONFIG.read().pages.shared_styles_content.clone()), + ROUTE_SHARED_JS_PATH => Some(APP_CONFIG.read().pages.shared_js_content.clone()), + ROUTE_ABOUT_PATH => Some(APP_CONFIG.read().pages.about_content.clone()), + ROUTE_README_PATH => Some(APP_CONFIG.read().pages.readme_content.clone()), + ROUTE_API_PATH => Some(APP_CONFIG.read().pages.api_content.clone()), + ROUTE_BUILD_KEY_PATH => Some(APP_CONFIG.read().pages.build_key_content.clone()), + _ => None, + } + } + + pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> { + let mut config = APP_CONFIG.write(); + match path { + ROUTE_ROOT_PATH => config.pages.root_content = content, + ROUTE_LOGS_PATH => config.pages.logs_content = content, + ROUTE_CONFIG_PATH => config.pages.config_content = content, + ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = content, + ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content, + ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content, + ROUTE_ABOUT_PATH => config.pages.about_content = content, + ROUTE_README_PATH => config.pages.readme_content = content, + ROUTE_API_PATH => config.pages.api_content = content, + ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = content, + _ => return Err(ERR_INVALID_PATH), + } + Ok(()) + } + + pub fn reset_page_content(path: &str) -> Result<(), &'static str> { + let mut config = APP_CONFIG.write(); + match path { + ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(), + ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(), + ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(), + ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = PageContent::default(), + ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = PageContent::default(), + ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(), + ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(), + ROUTE_README_PATH => config.pages.readme_content = PageContent::default(), + ROUTE_API_PATH => config.pages.api_content = PageContent::default(), + ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = PageContent::default(), + _ => return Err(ERR_INVALID_PATH), + } + Ok(()) + } + + pub fn is_share() -> bool { + APP_CONFIG.read().is_share + } +} + +impl AppState { + pub fn new(token_infos: Vec) -> Self { + // 尝试加载保存的日志 + let request_logs = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { Self::load_saved_logs().await.unwrap_or_default() }) + }); + + Self { + total_requests: request_logs.len() as u64, + active_requests: 0, + error_requests: request_logs + .iter() + .filter(|log| matches!(log.status, LogStatus::Failed)) + .count() as u64, + request_logs, + token_infos, + } + } + + pub fn update_checksum(&mut self) { + for token_info in self.token_infos.iter_mut() { + token_info.checksum = generate_checksum_with_repair(&token_info.checksum); + } + } +} + +#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub enum LogStatus { + Pending, + Success, + Failed, +} + +impl Serialize for LogStatus { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str_name()) + } +} + +impl LogStatus { + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Pending => STATUS_PENDING, + Self::Success => STATUS_SUCCESS, + Self::Failed => STATUS_FAILED, + } + } + + pub fn from_str_name(s: &str) -> Option { + match s { + STATUS_PENDING => Some(Self::Pending), + STATUS_SUCCESS => Some(Self::Success), + STATUS_FAILED => Some(Self::Failed), + _ => None, + } + } +} + +// 请求日志 +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct RequestLog { + pub id: u64, + pub timestamp: chrono::DateTime, + pub model: String, + pub token_info: TokenInfo, + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt: Option, + pub timing: TimingInfo, + pub stream: bool, + pub status: LogStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct TimingInfo { + pub total: f64, // 总用时(秒) + #[serde(skip_serializing_if = "Option::is_none")] + pub first: Option, // 首字时间(秒) +} + +// 聊天请求 +#[derive(Deserialize)] +pub struct ChatRequest { + pub model: String, + pub messages: Vec, + #[serde(default)] + pub stream: bool, +} + +// 用于存储 token 信息 +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct TokenInfo { + pub token: String, + pub checksum: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option, +} + +// TokenUpdateRequest 结构体 +#[derive(Deserialize)] +pub struct TokenUpdateRequest { + pub tokens: String, +} + +#[derive(Deserialize)] +pub struct TokenAddRequestTokenInfo { + pub token: String, + #[serde(default)] + pub checksum: Option, +} + +// TokensDeleteRequest 结构体 +#[derive(Deserialize)] +pub struct TokensDeleteRequest { + #[serde(default)] + pub tokens: Vec, + #[serde(default)] + pub expectation: TokensDeleteResponseExpectation, +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum TokensDeleteResponseExpectation { + #[default] + Simple, + UpdatedTokens, + FailedTokens, + Detailed, +} + +impl TokensDeleteResponseExpectation { + pub fn needs_updated_tokens(&self) -> bool { + matches!( + self, + TokensDeleteResponseExpectation::UpdatedTokens + | TokensDeleteResponseExpectation::Detailed + ) + } + + pub fn needs_failed_tokens(&self) -> bool { + matches!( + self, + TokensDeleteResponseExpectation::FailedTokens + | TokensDeleteResponseExpectation::Detailed + ) + } +} + +// TokensDeleteResponse 结构体 +#[derive(Serialize)] +pub struct TokensDeleteResponse { + pub status: ApiStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_tokens: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub failed_tokens: Option>, +} diff --git a/src/app/model/build_key.rs b/src/app/model/build_key.rs new file mode 100644 index 0000000000000000000000000000000000000000..46f3f31b1cc6d9fbbaa1ebdb3f05f1d37109158f --- /dev/null +++ b/src/app/model/build_key.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; + +use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS}; + +#[derive(Deserialize)] +pub struct BuildKeyRequest { + pub auth_token: String, + #[serde(default)] + pub disable_vision: Option, + #[serde(default)] + pub enable_slow_pool: Option, + #[serde(default)] + pub usage_check_models: Option, + #[serde(default)] + pub include_web_references: Option, +} +pub struct UsageCheckModelConfig { + pub model_type: UsageCheckModelType, + pub model_ids: Vec<&'static str>, +} + +impl<'de> Deserialize<'de> for UsageCheckModelConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + #[serde(rename = "type")] + model_type: UsageCheckModelType, + #[serde(default)] + model_ids: String, + } + + let helper = Helper::deserialize(deserializer)?; + + let model_ids = if helper.model_ids.is_empty() { + Vec::new() + } else { + helper + .model_ids + .split(COMMA) + .filter_map(|model| { + let model = model.trim(); + AVAILABLE_MODELS + .iter() + .find(|m| m.id == model) + .map(|m| m.id) + }) + .collect() + }; + + Ok(UsageCheckModelConfig { + model_type: helper.model_type, + model_ids, + }) + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum UsageCheckModelType { + Default, + Disabled, + All, + Custom, +} + +#[derive(Serialize)] +#[serde(rename_all = "lowercase")] +pub enum BuildKeyResponse { + Key(String), + Error(String), +} diff --git a/src/app/model/config.rs b/src/app/model/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..f4f52444b4060cc1ed89b1696baa47c41a834b63 --- /dev/null +++ b/src/app/model/config.rs @@ -0,0 +1,114 @@ +use memmap2::{MmapMut, MmapOptions}; +use rkyv::{archived_root, Deserialize as _}; +use std::fs::OpenOptions; + +use crate::app::lazy::{LOGS_FILE_PATH, PAGES_FILE_PATH}; + +use super::{AppConfig, AppState, Pages, RequestLog, APP_CONFIG}; + +impl AppState { + // 保存日志的方法 + pub(crate) async fn save_logs(&self) -> Result<(), Box> { + // 序列化日志 + let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?; + + // 创建或打开文件 + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(LOGS_FILE_PATH.as_str())?; + + // 添加大小检查 + if bytes.len() > usize::MAX / 2 { + return Err("日志数据过大".into()); + } + + // 设置文件大小 + file.set_len(bytes.len() as u64)?; + + // 创建可写入的内存映射 + let mut mmap = unsafe { MmapMut::map_mut(&file)? }; + + // 写入数据 + mmap.copy_from_slice(&bytes); + + // 同步到磁盘 + mmap.flush()?; + + Ok(()) + } + + // 加载日志的方法 + pub(super) async fn load_saved_logs() -> Result, Box> { + let file = match OpenOptions::new().read(true).open(LOGS_FILE_PATH.as_str()) { + Ok(file) => file, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(e) => return Err(Box::new(e)), + }; + + // 添加文件大小检查 + if file.metadata()?.len() > usize::MAX as u64 { + return Err("日志文件过大".into()); + } + + // 创建只读内存映射 + let mmap = unsafe { MmapOptions::new().map(&file)? }; + + // 验证并反序列化数据 + let archived = unsafe { archived_root::>(&mmap) }; + Ok(archived.deserialize(&mut rkyv::Infallible)?) + } +} + +impl AppConfig { + pub fn save_config() -> Result<(), Box> { + let pages = APP_CONFIG.read().pages.clone(); + let bytes = rkyv::to_bytes::<_, 256>(&pages)?; + + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(PAGES_FILE_PATH.as_str())?; + + // 添加大小检查 + if bytes.len() > usize::MAX / 2 { + return Err("配置数据过大".into()); + } + + file.set_len(bytes.len() as u64)?; + + let mut mmap = unsafe { MmapMut::map_mut(&file)? }; + mmap.copy_from_slice(&bytes); + mmap.flush()?; + + Ok(()) + } + + pub fn load_saved_config() -> Result<(), Box> { + let file = match OpenOptions::new().read(true).open(PAGES_FILE_PATH.as_str()) { + Ok(file) => file, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(()); + } + Err(e) => return Err(Box::new(e)), + }; + + // 添加文件大小检查 + if file.metadata()?.len() > usize::MAX as u64 { + return Err("配置文件过大".into()); + } + + let mmap = unsafe { MmapOptions::new().map(&file)? }; + + let archived = unsafe { archived_root::(&mmap) }; + let pages = archived.deserialize(&mut rkyv::Infallible)?; + let mut config = APP_CONFIG.write(); + config.pages = pages; + + Ok(()) + } +} diff --git a/src/app/model/proxies.rs b/src/app/model/proxies.rs new file mode 100644 index 0000000000000000000000000000000000000000..b117e4a6b0ba157058ca9adf337b06a2300097f6 --- /dev/null +++ b/src/app/model/proxies.rs @@ -0,0 +1,81 @@ +use reqwest::{Client, Proxy}; +use serde::{Serialize, Serializer}; +use serde::{Deserialize, Deserializer}; +// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; + +use crate::app::constant::COMMA_STRING; + +#[derive(Clone, Default, PartialEq)] +pub enum Proxies { + No, + #[default] + System, + List(Vec), +} + +impl Serialize for Proxies { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Proxies::No => serializer.serialize_str(""), + Proxies::System => serializer.serialize_str("system"), + Proxies::List(urls) => serializer.serialize_str(&urls.join(COMMA_STRING)), + } + } +} + +impl<'de> Deserialize<'de> for Proxies { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + Ok(Proxies::from_str(&s)) + } +} + +impl Proxies { + /// 从字符串创建 Proxies + /// + /// # Arguments + /// * `s` - 代理字符串: + /// - "" 或 "no": 不使用代理 + /// - "system": 使用系统代理 + /// - 其他: 尝试解析为代理列表,无效则返回 System + pub fn from_str(s: &str) -> Self { + match s.trim() { + "" | "no" => Self::No, + "system" => Self::System, + urls => { + let valid_proxies: Vec = urls + .split(',') + .filter_map(|url| { + let trimmed = url.trim(); + (!trimmed.is_empty() && Proxy::all(trimmed).is_ok()) + .then(|| trimmed.to_string()) + }) + .collect(); + + if valid_proxies.is_empty() { + Self::default() + } else { + Self::List(valid_proxies) + } + } + } + } + + pub fn get_client(&self) -> Client { + match self { + Proxies::No => Client::builder().no_proxy().build().unwrap(), + Proxies::System => Client::new(), + Proxies::List(list) => { + // 使用第一个代理(已经确保是有效的) + let proxy = Proxy::all(list[0].clone()).unwrap(); + Client::builder().proxy(proxy).build().unwrap() + } + } + } +} diff --git a/src/app/model/usage_check.rs b/src/app/model/usage_check.rs new file mode 100644 index 0000000000000000000000000000000000000000..7e04498ea25fa61959ec65e8d050deb448c129fc --- /dev/null +++ b/src/app/model/usage_check.rs @@ -0,0 +1,172 @@ +use crate::{ + app::constant::{COMMA, COMMA_STRING}, + chat::{config::key_config, constant::AVAILABLE_MODELS}, +}; +use serde::{Deserialize, Serialize}; +// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; + +#[derive(Clone, PartialEq)] +pub enum UsageCheck { + None, + Default, + All, + Custom(Vec<&'static str>), +} + +impl UsageCheck { + pub fn from_proto(model: Option<&key_config::UsageCheckModel>) -> Option { + model.map(|model| { + use key_config::usage_check_model::Type; + match Type::try_from(model.r#type).unwrap_or(Type::Default) { + Type::Default | Type::Disabled => Self::None, + Type::All => Self::All, + Type::Custom => { + let models: Vec<&'static str> = model + .model_ids + .iter() + .filter_map(|id| AVAILABLE_MODELS.iter().find(|m| m.id == id).map(|m| m.id)) + .collect(); + if models.is_empty() { + Self::None + } else { + Self::Custom(models) + } + } + } + }) + } + + // pub fn to_proto(&self) -> key_config::UsageCheckModel { + // use key_config::usage_check_model::Type; + // match self { + // Self::None => key_config::UsageCheckModel { + // r#type: Type::Disabled.into(), + // model_ids: vec![], + // }, + // Self::Default => key_config::UsageCheckModel { + // r#type: Type::Default.into(), + // model_ids: vec![], + // }, + // Self::All => key_config::UsageCheckModel { + // r#type: Type::All.into(), + // model_ids: vec![], + // }, + // Self::Custom(models) => key_config::UsageCheckModel { + // r#type: Type::Custom.into(), + // model_ids: models.iter().map(|&s| s.to_string()).collect(), + // }, + // } + // } +} + +impl Default for UsageCheck { + fn default() -> Self { + Self::Default + } +} + +impl Serialize for UsageCheck { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("UsageCheck", 1)?; + match self { + UsageCheck::None => { + state.serialize_field("type", "none")?; + } + UsageCheck::Default => { + state.serialize_field("type", "default")?; + } + UsageCheck::All => { + state.serialize_field("type", "all")?; + } + UsageCheck::Custom(models) => { + state.serialize_field("type", "list")?; + state.serialize_field("content", &models.join(COMMA_STRING))?; + } + } + state.end() + } +} + +impl<'de> Deserialize<'de> for UsageCheck { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(tag = "type", content = "content")] + enum UsageCheckHelper { + #[serde(rename = "none")] + None, + #[serde(rename = "default")] + Default, + #[serde(rename = "all")] + All, + #[serde(rename = "list")] + Custom(String), + } + + let helper = ::deserialize(deserializer)?; + Ok(match helper { + UsageCheckHelper::None => UsageCheck::None, + UsageCheckHelper::Default => UsageCheck::Default, + UsageCheckHelper::All => UsageCheck::All, + UsageCheckHelper::Custom(list) => { + if list.is_empty() { + return Ok(UsageCheck::None); + } + + let models: Vec<&'static str> = list + .split(COMMA) + .filter_map(|model| { + let model = model.trim(); + AVAILABLE_MODELS + .iter() + .find(|m| m.id == model) + .map(|m| m.id) + }) + .collect(); + + if models.is_empty() { + UsageCheck::None + } else { + UsageCheck::Custom(models) + } + } + }) + } +} + +impl UsageCheck { + pub fn from_str(s: &str) -> Self { + match s.trim().to_lowercase().as_str() { + "none" | "disabled" => Self::None, + "default" => Self::Default, + "all" | "everything" => Self::All, + list => { + if list.is_empty() { + return Self::default(); + } + let models: Vec<&'static str> = list + .split(COMMA) + .filter_map(|model| { + let model = model.trim(); + AVAILABLE_MODELS + .iter() + .find(|m| m.id == model) + .map(|m| m.id) + }) + .collect(); + + if models.is_empty() { + Self::default() + } else { + Self::Custom(models) + } + } + } + } +} diff --git a/src/chat.rs b/src/chat.rs new file mode 100644 index 0000000000000000000000000000000000000000..f46dddb629977f021a1c679764e33040ba98655f --- /dev/null +++ b/src/chat.rs @@ -0,0 +1,10 @@ +pub mod adapter; +pub mod aiserver; +pub mod config; +pub mod constant; +pub mod error; +// pub mod middleware; +pub mod model; +pub mod route; +pub mod service; +pub mod stream; diff --git a/src/chat/adapter.rs b/src/chat/adapter.rs new file mode 100644 index 0000000000000000000000000000000000000000..2499451536995ce80fb24528cd6cf0560de89e2b --- /dev/null +++ b/src/chat/adapter.rs @@ -0,0 +1,473 @@ +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use image::guess_format; +use prost::Message as _; +use reqwest::Client; +use uuid::Uuid; + +use crate::{ + app::{ + constant::EMPTY_STRING, + lazy::DEFAULT_INSTRUCTIONS, + model::{AppConfig, VisionAbility}, + }, + common::client::HTTP_CLIENT, +}; + +use super::{ + aiserver::v1::{ + conversation_message, image_proto, AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails + }, + constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS}, + model::{Message, MessageContent, Role}, +}; + +async fn process_chat_inputs( + inputs: Vec, + disable_vision: bool, +) -> (String, Vec, Vec) { + // 收集 system 指令 + let instructions = inputs + .iter() + .filter(|input| input.role == Role::System) + .map(|input| match &input.content { + MessageContent::Text(text) => text.clone(), + MessageContent::Vision(contents) => contents + .iter() + .filter_map(|content| { + if content.content_type == "text" { + content.text.clone() + } else { + None + } + }) + .collect::>() + .join("\n"), + }) + .collect::>() + .join("\n\n"); + + // 使用默认指令或收集到的指令 + let instructions = if instructions.is_empty() { + DEFAULT_INSTRUCTIONS.clone() + } else { + instructions + }; + + // 过滤出 user 和 assistant 对话 + let mut chat_inputs: Vec = inputs + .into_iter() + .filter(|input| input.role == Role::User || input.role == Role::Assistant) + .collect(); + + // 处理空对话情况 + if chat_inputs.is_empty() { + return ( + instructions, + vec![ConversationMessage { + text: EMPTY_STRING.into(), + r#type: conversation_message::MessageType::Human as i32, + attached_code_chunks: vec![], + codebase_context_chunks: vec![], + commits: vec![], + pull_requests: vec![], + git_diffs: vec![], + assistant_suggested_diffs: vec![], + interpreter_results: vec![], + images: vec![], + attached_folders: vec![], + approximate_lint_errors: vec![], + bubble_id: Uuid::new_v4().to_string(), + server_bubble_id: None, + attached_folders_new: vec![], + lints: vec![], + user_responses_to_suggested_code_blocks: vec![], + relevant_files: vec![], + tool_results: vec![], + notepads: vec![], + is_capability_iteration: Some(false), + capabilities: vec![], + edit_trail_contexts: vec![], + suggested_code_blocks: vec![], + diffs_for_compressing_files: vec![], + multi_file_linter_errors: vec![], + diff_histories: vec![], + recently_viewed_files: vec![], + recent_locations_history: vec![], + is_agentic: false, + file_diff_trajectories: vec![], + conversation_summary: None, + }], + vec![], + ); + } + + // 处理 WebReferences 开头的 assistant 消息 + chat_inputs = chat_inputs + .into_iter() + .map(|mut input| { + if let (Role::Assistant, MessageContent::Text(text)) = (&input.role, &input.content) { + if text.starts_with("WebReferences:") { + if let Some(pos) = text.find("\n\n") { + input.content = MessageContent::Text(text[pos + 2..].to_owned()); + } + } + } + input + }) + .collect(); + + // 如果第一条是 assistant,插入空的 user 消息 + if chat_inputs + .first() + .map_or(false, |input| input.role == Role::Assistant) + { + chat_inputs.insert( + 0, + Message { + role: Role::User, + content: MessageContent::Text(EMPTY_STRING.into()), + }, + ); + } + + // 处理连续相同角色的情况 + let mut i = 1; + while i < chat_inputs.len() { + if chat_inputs[i].role == chat_inputs[i - 1].role { + let insert_role = if chat_inputs[i].role == Role::User { + Role::Assistant + } else { + Role::User + }; + chat_inputs.insert( + i, + Message { + role: insert_role, + content: MessageContent::Text(EMPTY_STRING.into()), + }, + ); + } + i += 1; + } + + // 确保最后一条是 user + if chat_inputs + .last() + .map_or(false, |input| input.role == Role::Assistant) + { + chat_inputs.push(Message { + role: Role::User, + content: MessageContent::Text(EMPTY_STRING.into()), + }); + } + + // 转换为 proto messages + let mut messages = Vec::new(); + for input in chat_inputs { + let (text, images) = match input.content { + MessageContent::Text(text) => (text, vec![]), + MessageContent::Vision(contents) => { + let mut text_parts = Vec::new(); + let mut images = Vec::new(); + + for content in contents { + match content.content_type.as_str() { + "text" => { + if let Some(text) = content.text { + text_parts.push(text); + } + } + "image_url" => { + if !disable_vision { + if let Some(image_url) = &content.image_url { + let url = image_url.url.clone(); + let client = HTTP_CLIENT.read().clone(); + let result = tokio::spawn(async move { + fetch_image_data(&url, client).await + }); + if let Ok(Ok((image_data, dimensions))) = result.await { + images.push(ImageProto { + data: image_data, + dimension: dimensions, + }); + } + } + } + } + _ => {} + } + } + (text_parts.join("\n"), images) + } + }; + + messages.push(ConversationMessage { + text, + r#type: if input.role == Role::User { + conversation_message::MessageType::Human as i32 + } else { + conversation_message::MessageType::Ai as i32 + }, + attached_code_chunks: vec![], + codebase_context_chunks: vec![], + commits: vec![], + pull_requests: vec![], + git_diffs: vec![], + assistant_suggested_diffs: vec![], + interpreter_results: vec![], + images, + attached_folders: vec![], + approximate_lint_errors: vec![], + bubble_id: Uuid::new_v4().to_string(), + server_bubble_id: None, + attached_folders_new: vec![], + lints: vec![], + user_responses_to_suggested_code_blocks: vec![], + relevant_files: vec![], + tool_results: vec![], + notepads: vec![], + is_capability_iteration: None, + capabilities: vec![], + edit_trail_contexts: vec![], + suggested_code_blocks: vec![], + diffs_for_compressing_files: vec![], + multi_file_linter_errors: vec![], + diff_histories: vec![], + recently_viewed_files: vec![], + recent_locations_history: vec![], + is_agentic: false, + file_diff_trajectories: vec![], + conversation_summary: None, + }); + } + + let mut urls = Vec::new(); + if let Some(last_msg) = messages.last() { + if last_msg.r#type == conversation_message::MessageType::Human as i32 { + let text = &last_msg.text; + let mut chars = text.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '@' { + let mut url = String::new(); + while let Some(&next_char) = chars.peek() { + if next_char.is_whitespace() { + break; + } + url.push(chars.next().unwrap()); + } + if let Ok(parsed_url) = url::Url::parse(&url) { + if parsed_url.scheme() == "http" || parsed_url.scheme() == "https" { + urls.push(url); + } + } + } + } + } + } + + (instructions, messages, urls) +} + +async fn fetch_image_data( + url: &str, + client: Client, +) -> Result<(Vec, Option), Box> { + // 在进入异步操作前获取并释放锁 + let vision_ability = AppConfig::get_vision_ability(); + + match vision_ability { + VisionAbility::None => Err("图片功能已禁用".into()), + + VisionAbility::Base64 => { + if !url.starts_with("data:image/") { + return Err("仅支持 base64 编码的图片".into()); + } + process_base64_image(url) + } + + VisionAbility::All => { + if url.starts_with("data:image/") { + process_base64_image(url) + } else { + process_http_image(url, client).await + } + } + } +} + +// 处理 base64 编码的图片 +fn process_base64_image( + url: &str, +) -> Result<(Vec, Option), Box> { + let parts: Vec<&str> = url.split("base64,").collect(); + if parts.len() != 2 { + return Err("无效的 base64 图片格式".into()); + } + + // 检查图片格式 + let format = parts[0].to_lowercase(); + if !format.contains("png") + && !format.contains("jpeg") + && !format.contains("jpg") + && !format.contains("webp") + && !format.contains("gif") + { + return Err(ERR_UNSUPPORTED_IMAGE_FORMAT.into()); + } + + let image_data = BASE64.decode(parts[1])?; + + // 检查是否为动态 GIF + if format.contains("gif") { + if let Ok(frames) = gif::DecodeOptions::new().read_info(std::io::Cursor::new(&image_data)) { + if frames.into_iter().count() > 1 { + return Err(ERR_UNSUPPORTED_GIF.into()); + } + } + } + + // 获取图片尺寸 + let dimensions = if let Ok(img) = image::load_from_memory(&image_data) { + Some(image_proto::Dimension { + width: img.width() as i32, + height: img.height() as i32, + }) + } else { + None + }; + + Ok((image_data, dimensions)) +} + +// 处理 HTTP 图片 URL +async fn process_http_image( + url: &str, + client: Client, +) -> Result<(Vec, Option), Box> { + let response = client.get(url).send().await?; + let image_data = response.bytes().await?.to_vec(); + let format = guess_format(&image_data)?; + + // 检查图片格式 + match format { + image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::WebP => { + // 这些格式都支持 + } + image::ImageFormat::Gif => { + if let Ok(frames) = + gif::DecodeOptions::new().read_info(std::io::Cursor::new(&image_data)) + { + if frames.into_iter().count() > 1 { + return Err(ERR_UNSUPPORTED_GIF.into()); + } + } + } + _ => return Err(ERR_UNSUPPORTED_IMAGE_FORMAT.into()), + } + + // 获取图片尺寸 + let dimensions = if let Ok(img) = image::load_from_memory_with_format(&image_data, format) { + Some(image_proto::Dimension { + width: img.width() as i32, + height: img.height() as i32, + }) + } else { + None + }; + + Ok((image_data, dimensions)) +} + +pub async fn encode_chat_message( + inputs: Vec, + model_name: &str, + disable_vision: bool, + enable_slow_pool: bool, + is_search: bool, +) -> Result, Box> { + // 在进入异步操作前获取并释放锁 + let enable_slow_pool = { + if enable_slow_pool { + Some(true) + } else { + None + } + }; + + let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await; + + let explicit_context = if !instructions.trim().is_empty() { + Some(ExplicitContext { + context: instructions, + repo_context: None, + }) + } else { + None + }; + + let base_uuid = rand::random::(); + let external_links = urls.into_iter().enumerate().map(|(i, url)| { + let uuid = base_uuid.wrapping_add(i as u16); + ChatExternalLink { + url, + uuid: uuid.to_string(), + } + }).collect(); + + let chat = GetChatRequest { + current_file: None, + conversation: messages, + repositories: vec![], + explicit_context, + workspace_root_path: None, + code_blocks: vec![], + model_details: Some(ModelDetails { + model_name: Some(model_name.to_string()), + api_key: None, + enable_ghost_mode: None, + azure_state: Some(AzureState { + api_key: String::new(), + base_url: String::new(), + deployment: String::new(), + use_azure: false, + }), + enable_slow_pool, + openai_api_base_url: None, + }), + documentation_identifiers: vec![], + request_id: Uuid::new_v4().to_string(), + linter_errors: None, + summary: None, + summary_up_until_index: None, + allow_long_file_scan: Some(false), + is_bash: Some(false), + conversation_id: Uuid::new_v4().to_string(), + can_handle_filenames_after_language_ids: Some(true), + use_web: if is_search { + Some("full_search".to_string()) + } else { + None + }, + quotes: vec![], + debug_info: None, + workspace_id: None, + external_links, + commit_notes: vec![], + long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)), + is_eval: Some(false), + desired_max_tokens: None, + context_ast: None, + is_composer: None, + runnable_code_blocks: Some(false), + should_cache: Some(false), + }; + + let mut encoded = Vec::new(); + chat.encode(&mut encoded)?; + + let len_prefix = format!("{:010x}", encoded.len()).to_uppercase(); + let content = hex::encode_upper(&encoded); + + Ok(hex::decode(len_prefix + &content)?) +} diff --git a/src/chat/aiserver.rs b/src/chat/aiserver.rs new file mode 100644 index 0000000000000000000000000000000000000000..a3a6d96c3f59dd67ed1c89948bc4d13c42504661 --- /dev/null +++ b/src/chat/aiserver.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/src/chat/aiserver/v1.rs b/src/chat/aiserver/v1.rs new file mode 100644 index 0000000000000000000000000000000000000000..5660b2dbe9c0daf1f46e1c6e7e557a37674525bc --- /dev/null +++ b/src/chat/aiserver/v1.rs @@ -0,0 +1,57 @@ +include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs")); +use error_details::Error; + +impl ErrorDetails { + pub fn status_code(&self) -> u16 { + match Error::try_from(self.error) { + Ok(error) => match error { + Error::Unspecified => 500, + Error::BadApiKey + | Error::InvalidAuthId + | Error::AuthTokenNotFound + | Error::AuthTokenExpired + | Error::Unauthorized => 401, + Error::NotLoggedIn + | Error::NotHighEnoughPermissions + | Error::AgentRequiresLogin + | Error::ProUserOnly + | Error::TaskNoPermissions => 403, + Error::NotFound + | Error::UserNotFound + | Error::TaskUuidNotFound + | Error::AgentEngineNotFound + | Error::GitgraphNotFound + | Error::FileNotFound => 404, + Error::FreeUserRateLimitExceeded + | Error::ProUserRateLimitExceeded + | Error::OpenaiRateLimitExceeded + | Error::OpenaiAccountLimitExceeded + | Error::GenericRateLimitExceeded + | Error::Gpt4VisionPreviewRateLimit + | Error::ApiKeyRateLimit => 429, + Error::BadRequest + | Error::BadModelName + | Error::SlashEditFileTooLong + | Error::FileUnsupported + | Error::ClaudeImageTooLarge => 400, + Error::Deprecated + | Error::FreeUserUsageLimit + | Error::ProUserUsageLimit + | Error::ResourceExhausted + | Error::Openai + | Error::MaxTokens + | Error::ApiKeyNotSupported + | Error::UserAbortedRequest + | Error::CustomMessage + | Error::OutdatedClient + | Error::Debounced + | Error::RepositoryServiceRepositoryIsNotInitialized => 500, + }, + Err(_) => 500, + } + } + + // pub fn is_expected(&self) -> bool { + // self.is_expected.unwrap_or_default() + // } +} diff --git a/src/chat/aiserver/v1/lite.proto b/src/chat/aiserver/v1/lite.proto new file mode 100644 index 0000000000000000000000000000000000000000..7a6cc24b586a3d7138cbcc9d8e8238bfe69a2050 --- /dev/null +++ b/src/chat/aiserver/v1/lite.proto @@ -0,0 +1,1156 @@ +syntax = "proto3"; +package aiserver.v1; +enum ClientSideToolV2 { // aiserver.v1.ClientSideToolV2 + CLIENT_SIDE_TOOL_V2_UNSPECIFIED = 0; + CLIENT_SIDE_TOOL_V2_READ_SEMSEARCH_FILES = 1; + CLIENT_SIDE_TOOL_V2_READ_FILE_FOR_IMPORTS = 2; + CLIENT_SIDE_TOOL_V2_RIPGREP_SEARCH = 3; + CLIENT_SIDE_TOOL_V2_RUN_TERMINAL_COMMAND = 4; + CLIENT_SIDE_TOOL_V2_READ_FILE = 5; + CLIENT_SIDE_TOOL_V2_LIST_DIR = 6; + CLIENT_SIDE_TOOL_V2_EDIT_FILE = 7; + CLIENT_SIDE_TOOL_V2_FILE_SEARCH = 8; + CLIENT_SIDE_TOOL_V2_SEMANTIC_SEARCH_FULL = 9; + CLIENT_SIDE_TOOL_V2_CREATE_FILE = 10; + CLIENT_SIDE_TOOL_V2_DELETE_FILE = 11; +} +enum EmbeddingModel { // aiserver.v1.EmbeddingModel + EMBEDDING_MODEL_UNSPECIFIED = 0; + EMBEDDING_MODEL_VOYAGE_CODE_2 = 1; + EMBEDDING_MODEL_TEXT_EMBEDDINGS_LARGE_3 = 2; + EMBEDDING_MODEL_QWEN_1_5B_CUSTOM = 3; +} +enum ChunkType { // aiserver.v1.ChunkType + CHUNK_TYPE_UNSPECIFIED = 0; + CHUNK_TYPE_CODEBASE = 1; + CHUNK_TYPE_LONG_FILE = 2; + CHUNK_TYPE_DOCS = 3; +} +enum FastApplySource { // aiserver.v1.FastApplySource + FAST_APPLY_SOURCE_UNSPECIFIED = 0; + FAST_APPLY_SOURCE_COMPOSER = 1; + FAST_APPLY_SOURCE_CLICKED_APPLY = 2; + FAST_APPLY_SOURCE_CACHED_APPLY = 3; +} +enum BuiltinTool { // aiserver.v1.BuiltinTool + BUILTIN_TOOL_UNSPECIFIED = 0; + BUILTIN_TOOL_SEARCH = 1; + BUILTIN_TOOL_READ_CHUNK = 2; + BUILTIN_TOOL_GOTODEF = 3; + BUILTIN_TOOL_EDIT = 4; + BUILTIN_TOOL_UNDO_EDIT = 5; + BUILTIN_TOOL_END = 6; + BUILTIN_TOOL_NEW_FILE = 7; + BUILTIN_TOOL_ADD_TEST = 8; + BUILTIN_TOOL_RUN_TEST = 9; + BUILTIN_TOOL_DELETE_TEST = 10; + BUILTIN_TOOL_SAVE_FILE = 11; + BUILTIN_TOOL_GET_TESTS = 12; + BUILTIN_TOOL_GET_SYMBOLS = 13; + BUILTIN_TOOL_SEMANTIC_SEARCH = 14; + BUILTIN_TOOL_GET_PROJECT_STRUCTURE = 15; + BUILTIN_TOOL_CREATE_RM_FILES = 16; + BUILTIN_TOOL_RUN_TERMINAL_COMMANDS = 17; + BUILTIN_TOOL_NEW_EDIT = 18; + BUILTIN_TOOL_READ_WITH_LINTER = 19; +} +enum FeatureType { // aiserver.v1.FeatureType + FEATURE_TYPE_UNSPECIFIED = 0; + FEATURE_TYPE_EDIT = 1; + FEATURE_TYPE_GENERATE = 2; + FEATURE_TYPE_INLINE_LONG_COMPLETION = 3; +} +enum TaskStatus { // aiserver.v1.TaskStatus + TASK_STATUS_UNSPECIFIED = 0; + TASK_STATUS_RUNNING = 1; + TASK_STATUS_PAUSED = 2; + TASK_STATUS_DONE = 3; + TASK_STATUS_NOT_STARTED = 4; +} +enum RerankerAlgorithm { // aiserver.v1.RerankerAlgorithm + RERANKER_ALGORITHM_UNSPECIFIED = 0; + RERANKER_ALGORITHM_LULEA = 1; + RERANKER_ALGORITHM_UMEA = 2; + RERANKER_ALGORITHM_NONE = 3; + RERANKER_ALGORITHM_LLAMA = 4; + RERANKER_ALGORITHM_STARCODER_V1 = 5; + RERANKER_ALGORITHM_GPT_3_5_LOGPROBS = 6; + RERANKER_ALGORITHM_LULEA_HAIKU = 7; + RERANKER_ALGORITHM_COHERE = 8; + RERANKER_ALGORITHM_VOYAGE = 9; + RERANKER_ALGORITHM_VOYAGE_EMBEDS = 10; + RERANKER_ALGORITHM_IDENTITY = 11; + RERANKER_ALGORITHM_ADA_EMBEDS = 12; +} +enum RechunkerChoice { // aiserver.v1.RechunkerChoice + RECHUNKER_CHOICE_UNSPECIFIED = 0; + RECHUNKER_CHOICE_IDENTITY = 1; + RECHUNKER_CHOICE_600_TOKS = 2; + RECHUNKER_CHOICE_2400_TOKS = 3; + RECHUNKER_CHOICE_4000_TOKS = 4; +} +enum LintGenerator { // aiserver.v1.LintGenerator + LINT_GENERATOR_UNSPECIFIED = 0; + LINT_GENERATOR_NAIVE = 1; + LINT_GENERATOR_COMMENT_PIPELINE = 2; + LINT_GENERATOR_SIMPLE_BUG = 3; + LINT_GENERATOR_SIMPLE_LINT_RULES = 4; +} +enum LintDiscriminator { // aiserver.v1.LintDiscriminator + LINT_DISCRIMINATOR_UNSPECIFIED = 0; + LINT_DISCRIMINATOR_SPECIFIC_RULES = 1; + LINT_DISCRIMINATOR_COMPILE_ERRORS = 2; + LINT_DISCRIMINATOR_CHANGE_BEHAVIOR = 3; + LINT_DISCRIMINATOR_RELEVANCE = 4; + LINT_DISCRIMINATOR_USER_AWARENESS = 5; + LINT_DISCRIMINATOR_CORRECTNESS = 6; + LINT_DISCRIMINATOR_CHUNKING = 7; + LINT_DISCRIMINATOR_TYPO = 8; + LINT_DISCRIMINATOR_CONFIDENCE = 9; + LINT_DISCRIMINATOR_DISMISSED_BUGS = 10; +} +enum CppSource { // aiserver.v1.CppSource + CPP_SOURCE_UNSPECIFIED = 0; + CPP_SOURCE_LINE_CHANGE = 1; + CPP_SOURCE_TYPING = 2; + CPP_SOURCE_OPTION_HOLD = 3; + CPP_SOURCE_LINTER_ERRORS = 4; + CPP_SOURCE_PARAMETER_HINTS = 5; + CPP_SOURCE_CURSOR_PREDICTION = 6; + CPP_SOURCE_MANUAL_TRIGGER = 7; + CPP_SOURCE_EDITOR_CHANGE = 8; +} +enum ChunkingStrategy { // aiserver.v1.ChunkingStrategy + CHUNKING_STRATEGY_UNSPECIFIED = 0; + CHUNKING_STRATEGY_DEFAULT = 1; +} +message ErrorDetails { // aiserver.v1.ErrorDetails + enum Error { // aiserver.v1.ErrorDetails.Error + ERROR_UNSPECIFIED = 0; + ERROR_BAD_API_KEY = 1; + ERROR_NOT_LOGGED_IN = 2; + ERROR_INVALID_AUTH_ID = 3; + ERROR_NOT_HIGH_ENOUGH_PERMISSIONS = 4; + ERROR_BAD_MODEL_NAME = 5; + ERROR_USER_NOT_FOUND = 6; + ERROR_FREE_USER_RATE_LIMIT_EXCEEDED = 7; + ERROR_PRO_USER_RATE_LIMIT_EXCEEDED = 8; + ERROR_FREE_USER_USAGE_LIMIT = 9; + ERROR_PRO_USER_USAGE_LIMIT = 10; + ERROR_AUTH_TOKEN_NOT_FOUND = 11; + ERROR_AUTH_TOKEN_EXPIRED = 12; + ERROR_OPENAI = 13; + ERROR_OPENAI_RATE_LIMIT_EXCEEDED = 14; + ERROR_OPENAI_ACCOUNT_LIMIT_EXCEEDED = 15; + ERROR_TASK_UUID_NOT_FOUND = 16; + ERROR_TASK_NO_PERMISSIONS = 17; + ERROR_AGENT_REQUIRES_LOGIN = 18; + ERROR_AGENT_ENGINE_NOT_FOUND = 19; + ERROR_MAX_TOKENS = 20; + ERROR_USER_ABORTED_REQUEST = 21; + ERROR_GENERIC_RATE_LIMIT_EXCEEDED = 22; + ERROR_PRO_USER_ONLY = 23; + ERROR_API_KEY_NOT_SUPPORTED = 24; + ERROR_SLASH_EDIT_FILE_TOO_LONG = 26; + ERROR_FILE_UNSUPPORTED = 27; + ERROR_GPT_4_VISION_PREVIEW_RATE_LIMIT = 28; + ERROR_CUSTOM_MESSAGE = 29; + ERROR_OUTDATED_CLIENT = 30; + ERROR_CLAUDE_IMAGE_TOO_LARGE = 31; + ERROR_GITGRAPH_NOT_FOUND = 32; + ERROR_FILE_NOT_FOUND = 33; + ERROR_API_KEY_RATE_LIMIT = 34; + ERROR_DEBOUNCED = 35; + ERROR_BAD_REQUEST = 36; + ERROR_REPOSITORY_SERVICE_REPOSITORY_IS_NOT_INITIALIZED = 37; + ERROR_UNAUTHORIZED = 38; + ERROR_NOT_FOUND = 39; + ERROR_DEPRECATED = 40; + ERROR_RESOURCE_EXHAUSTED = 41; + } + Error error = 1; + CustomErrorDetails details = 2; + optional bool is_expected = 3; +} + +message CustomErrorDetails { // aiserver.v1.CustomErrorDetails + string title = 1; + string detail = 2; + optional bool allow_command_links_potentially_unsafe_please_only_use_for_handwritten_trusted_markdown = 3; + optional bool is_retryable = 4; + optional bool show_request_id = 5; +} + +message GetChatRequest { // aiserver.v1.GetChatRequest + CurrentFileInfo current_file = 1; + repeated ConversationMessage conversation = 2; + repeated RepositoryInfo repositories = 3; + ExplicitContext explicit_context = 4; + optional string workspace_root_path = 5; + repeated CodeBlock code_blocks = 6; + ModelDetails model_details = 7; + repeated string documentation_identifiers = 8; + string request_id = 9; + LinterErrors linter_errors = 10; + optional string summary = 11; + optional int32 summary_up_until_index = 12; + optional bool allow_long_file_scan = 13; + optional bool is_bash = 14; + string conversation_id = 15; + optional bool can_handle_filenames_after_language_ids = 16; + optional string use_web = 17; + repeated ChatQuote quotes = 18; + optional DebugInfo debug_info = 19; + optional string workspace_id = 20; + repeated ChatExternalLink external_links = 21; + repeated CommitNote commit_notes = 23; + optional bool long_context_mode = 22; + optional bool is_eval = 24; + optional int32 desired_max_tokens = 26; + ContextAST context_ast = 25; + optional bool is_composer = 27; + optional bool runnable_code_blocks = 28; + optional bool should_cache = 29; +} +message CurrentFileInfo { // aiserver.v1.CurrentFileInfo + message NotebookCell { // aiserver.v1.CurrentFileInfo.NotebookCell + } + string relative_workspace_path = 1; + string contents = 2; + bool rely_on_filesync = 18; + optional string sha_256_hash = 17; + repeated NotebookCell cells = 16; + repeated BM25Chunk top_chunks = 10; + int32 contents_start_at_line = 9; + CursorPosition cursor_position = 3; + repeated DataframeInfo dataframes = 4; + int32 total_number_of_lines = 8; + string language_id = 5; + CursorRange selection = 6; + optional int32 alternative_version_id = 11; + repeated Diagnostic diagnostics = 7; + optional int32 file_version = 14; + repeated int32 cell_start_lines = 15; + string workspace_root_path = 19; +} +message BM25Chunk { // aiserver.v1.BM25Chunk + string content = 1; + SimplestRange range = 2; + int32 score = 3; + string relative_path = 4; +} +message SimplestRange { // aiserver.v1.SimplestRange + int32 start_line = 1; + int32 end_line_inclusive = 2; +} +message CursorPosition { // aiserver.v1.CursorPosition + int32 line = 1; + int32 column = 2; +} +message DataframeInfo { // aiserver.v1.DataframeInfo + message Column { // aiserver.v1.DataframeInfo.Column + string key = 1; + string type = 2; + } + string name = 1; + string shape = 2; + int32 data_dimensionality = 3; + repeated Column columns = 6; + int32 row_count = 7; + string index_column = 8; +} +message CursorRange { // aiserver.v1.CursorRange + CursorPosition start_position = 1; + CursorPosition end_position = 2; +} +message Diagnostic { // aiserver.v1.Diagnostic + enum DiagnosticSeverity { // aiserver.v1.Diagnostic.DiagnosticSeverity + DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0; + DIAGNOSTIC_SEVERITY_ERROR = 1; + DIAGNOSTIC_SEVERITY_WARNING = 2; + DIAGNOSTIC_SEVERITY_INFORMATION = 3; + DIAGNOSTIC_SEVERITY_HINT = 4; + } + message RelatedInformation { // aiserver.v1.Diagnostic.RelatedInformation + string message = 1; + CursorRange range = 2; + } + string message = 1; + CursorRange range = 2; + DiagnosticSeverity severity = 3; + repeated RelatedInformation related_information = 4; +} +message ConversationMessage { // aiserver.v1.ConversationMessage + enum MessageType { // aiserver.v1.ConversationMessage.MessageType + MESSAGE_TYPE_UNSPECIFIED = 0; + MESSAGE_TYPE_HUMAN = 1; + MESSAGE_TYPE_AI = 2; + } + message CodeChunk { // aiserver.v1.ConversationMessage.CodeChunk + enum SummarizationStrategy { // aiserver.v1.ConversationMessage.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.ConversationMessage.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + INTENT_RECENTLY_VIEWED_FILE = 3; + INTENT_OUTLINE = 4; + INTENT_MENTIONED_FILE = 5; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; + optional bool contents_are_missing = 9; + } + message ApproximateLintError { // aiserver.v1.ConversationMessage.ApproximateLintError + string message = 1; + string value = 2; + int32 start_line = 3; + int32 end_line = 4; + int32 start_column = 5; + int32 end_column = 6; + } + message Lints { // aiserver.v1.ConversationMessage.Lints + GetLintsForChangeResponse lints = 1; + string chat_codeblock_model_value = 2; + } + message ToolResult { // aiserver.v1.ConversationMessage.ToolResult + message CodeChunk { // aiserver.v1.ConversationMessage.CodeChunk + enum SummarizationStrategy { // aiserver.v1.ConversationMessage.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.ConversationMessage.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + INTENT_RECENTLY_VIEWED_FILE = 3; + INTENT_OUTLINE = 4; + INTENT_MENTIONED_FILE = 5; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; + optional bool contents_are_missing = 9; + } + string tool_call_id = 1; + string tool_name = 2; + uint32 tool_index = 3; + string args = 4; + string raw_args = 5; + repeated CodeChunk attached_code_chunks = 6; + optional string content = 7; + ClientSideToolV2Result result = 8; + optional ToolResultError error = 9; + } + message NotepadContext { // aiserver.v1.ConversationMessage.NotepadContext + message CodeChunk { // aiserver.v1.ConversationMessage.CodeChunk + enum SummarizationStrategy { // aiserver.v1.ConversationMessage.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.ConversationMessage.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + INTENT_RECENTLY_VIEWED_FILE = 3; + INTENT_OUTLINE = 4; + INTENT_MENTIONED_FILE = 5; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; + optional bool contents_are_missing = 9; + } + string name = 1; + string text = 2; + repeated CodeChunk attached_code_chunks = 3; + repeated string attached_folders = 4; + repeated Commit commits = 5; + repeated PullRequest pull_requests = 6; + repeated GitDiff git_diffs = 7; + repeated ImageProto images = 8; + } + message EditTrailContext { // aiserver.v1.ConversationMessage.EditTrailContext + message EditLocation { // aiserver.v1.ConversationMessage.EditLocation + string relative_workspace_path = 1; + SimplestRange range = 3; + SimplestRange initial_range = 4; + string context_lines = 5; + string text = 6; + SimplestRange text_range = 7; + } + string unique_id = 1; + repeated EditLocation edit_trail_sorted = 2; + } + message RecentLocation { // aiserver.v1.ConversationMessage.RecentLocation + string relative_workspace_path = 1; + int32 line_number = 2; + } + string text = 1; + MessageType type = 2; + repeated CodeChunk attached_code_chunks = 3; + repeated CodeBlock codebase_context_chunks = 4; + repeated Commit commits = 5; + repeated PullRequest pull_requests = 6; + repeated GitDiff git_diffs = 7; + repeated SimpleFileDiff assistant_suggested_diffs = 8; + repeated InterpreterResult interpreter_results = 9; + repeated ImageProto images = 10; + repeated string attached_folders = 11; + repeated ApproximateLintError approximate_lint_errors = 12; + string bubble_id = 13; + optional string server_bubble_id = 32; + repeated FolderInfo attached_folders_new = 14; + repeated Lints lints = 15; + repeated UserResponseToSuggestedCodeBlock user_responses_to_suggested_code_blocks = 16; + repeated string relevant_files = 17; + repeated ToolResult tool_results = 18; + repeated NotepadContext notepads = 19; + optional bool is_capability_iteration = 20; + repeated ComposerCapabilityRequest capabilities = 21; + repeated EditTrailContext edit_trail_contexts = 22; + repeated SuggestedCodeBlock suggested_code_blocks = 23; + repeated RedDiff diffs_for_compressing_files = 24; + repeated LinterErrorsWithoutFileContents multi_file_linter_errors = 25; + repeated DiffHistoryData diff_histories = 26; + repeated CodeChunk recently_viewed_files = 27; + repeated RecentLocation recent_locations_history = 28; + bool is_agentic = 29; + repeated ComposerFileDiffHistory file_diff_trajectories = 30; + optional ConversationSummary conversation_summary = 31; +} +message CodeBlock { // aiserver.v1.CodeBlock + message Signatures { // aiserver.v1.CodeBlock.Signatures + repeated CursorRange ranges = 1; + } + string relative_workspace_path = 1; + optional string file_contents = 2; + CursorRange range = 3; + string contents = 4; + Signatures signatures = 5; + optional string override_contents = 6; + optional string original_contents = 7; + repeated DetailedLine detailed_lines = 8; +} +message DetailedLine { // aiserver.v1.DetailedLine + string text = 1; + float line_number = 2; + bool is_signature = 3; +} +message Commit { // aiserver.v1.Commit + string sha = 1; + string message = 2; + string description = 3; + repeated FileDiff diff = 4; + string author = 5; + string date = 6; +} +message FileDiff { // aiserver.v1.FileDiff + message Chunk { // aiserver.v1.FileDiff.Chunk + string content = 1; + repeated string lines = 2; + int32 old_start = 3; + int32 old_lines = 4; + int32 new_start = 5; + int32 new_lines = 6; + } + string from = 1; + string to = 2; + repeated Chunk chunks = 3; +} +message PullRequest { // aiserver.v1.PullRequest + string title = 1; + string body = 2; + repeated FileDiff diff = 3; +} +message GitDiff { // aiserver.v1.GitDiff + enum DiffType { // aiserver.v1.GitDiff.DiffType + DIFF_TYPE_UNSPECIFIED = 0; + DIFF_TYPE_DIFF_TO_HEAD = 1; + DIFF_TYPE_DIFF_FROM_BRANCH_TO_MAIN = 2; + } + repeated FileDiff diffs = 1; + DiffType diff_type = 2; +} +message SimpleFileDiff { // aiserver.v1.SimpleFileDiff + message Chunk { // aiserver.v1.SimpleFileDiff.Chunk + repeated string old_lines = 1; + repeated string new_lines = 2; + LineRange old_range = 3; + LineRange new_range = 4; + } + string relative_workspace_path = 1; + repeated Chunk chunks = 3; +} +message LineRange { // aiserver.v1.LineRange + int32 start_line_number = 1; + int32 end_line_number_inclusive = 2; +} +message InterpreterResult { // aiserver.v1.InterpreterResult + string output = 1; + bool success = 2; +} +message ImageProto { // aiserver.v1.ImageProto + message Dimension { // aiserver.v1.ImageProto.Dimension + int32 width = 1; + int32 height = 2; + } + bytes data = 1; + Dimension dimension = 2; +} +message FolderInfo { // aiserver.v1.FolderInfo + string relative_path = 1; + repeated FolderFileInfo files = 2; +} +message FolderFileInfo { // aiserver.v1.FolderFileInfo + string relative_path = 1; + string content = 2; + bool truncated = 3; + float score = 4; +} +message GetLintsForChangeResponse { // aiserver.v1.GetLintsForChangeResponse + message Lint { // aiserver.v1.GetLintsForChangeResponse.Lint + message QuickFix { // aiserver.v1.GetLintsForChangeResponse.Lint.QuickFix + message Edit { // aiserver.v1.GetLintsForChangeResponse.Lint.QuickFix.Edit + string relative_workspace_path = 1; + string text = 2; + int32 start_line_number_one_indexed = 3; + int32 start_column_one_indexed = 4; + int32 end_line_number_inclusive_one_indexed = 5; + int32 end_column_one_indexed = 6; + } + string message = 1; + string kind = 2; + bool is_preferred = 3; + repeated Edit edits = 4; + } + string message = 1; + string severity = 2; + string relative_workspace_path = 3; + int32 start_line_number_one_indexed = 4; + int32 start_column_one_indexed = 5; + int32 end_line_number_inclusive_one_indexed = 6; + int32 end_column_one_indexed = 7; + repeated QuickFix quick_fixes = 9; + } + repeated Lint lints = 1; +} +message UserResponseToSuggestedCodeBlock { // aiserver.v1.UserResponseToSuggestedCodeBlock + enum UserResponseType { // aiserver.v1.UserResponseToSuggestedCodeBlock.UserResponseType + USER_RESPONSE_TYPE_UNSPECIFIED = 0; + USER_RESPONSE_TYPE_ACCEPT = 1; + USER_RESPONSE_TYPE_REJECT = 2; + USER_RESPONSE_TYPE_MODIFY = 3; + } + UserResponseType user_response_type = 1; + string file_path = 2; + optional FileDiff user_modifications_to_suggested_code_blocks = 3; +} +message ClientSideToolV2Result { // aiserver.v1.ClientSideToolV2Result + ClientSideToolV2 tool = 1; + ReadSemsearchFilesResult read_semsearch_files_result = 2; + ReadFileForImportsResult read_file_for_imports_result = 3; + RipgrepSearchResult ripgrep_search_result = 4; + RunTerminalCommandResult run_terminal_command_result = 5; + ReadFileResult read_file_result = 6; + ListDirResult list_dir_result = 9; + EditFileResult edit_file_result = 10; + ToolCallFileSearchResult file_search_result = 11; + SemanticSearchFullResult semantic_search_full_result = 18; + CreateFileResult create_file_result = 19; + DeleteFileResult delete_file_result = 20; + optional ToolResultError error = 8; +} +message ReadSemsearchFilesResult { // aiserver.v1.ReadSemsearchFilesResult + repeated CodeResult code_results = 1; +} +message CodeResult { // aiserver.v1.CodeResult + CodeBlock code_block = 1; + float score = 2; +} +message ReadFileForImportsResult { // aiserver.v1.ReadFileForImportsResult + string contents = 1; +} +message RipgrepSearchResult { // aiserver.v1.RipgrepSearchResult + RipgrepSearchResultInternal internal = 1; +} +message RipgrepSearchResultInternal { // aiserver.v1.RipgrepSearchResultInternal + message IFileMatch { // aiserver.v1.RipgrepSearchResultInternal.IFileMatch + message ITextSearchResult { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchResult + message ITextSearchMatch { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchMatch + message ISearchRangeSetPairing { // aiserver.v1.RipgrepSearchResultInternal.ISearchRangeSetPairing + message ISearchRange { // aiserver.v1.RipgrepSearchResultInternal.ISearchRange + int32 start_line_number = 1; + int32 start_column = 2; + int32 end_line_number = 3; + int32 end_column = 4; + } + ISearchRange source = 1; + ISearchRange preview = 2; + } + optional string uri = 1; + repeated ISearchRangeSetPairing range_locations = 2; + string preview_text = 3; + optional int32 webview_index = 4; + optional string cell_fragment = 5; + } + message ITextSearchContext { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchContext + optional string uri = 1; + string text = 2; + int32 line_number = 3; + } + ITextSearchMatch match = 1; + ITextSearchContext context = 2; + } + string resource = 1; + repeated ITextSearchResult results = 2; + } + enum SearchCompletionExitCode { // aiserver.v1.RipgrepSearchResultInternal.SearchCompletionExitCode + SEARCH_COMPLETION_EXIT_CODE_UNSPECIFIED = 0; + SEARCH_COMPLETION_EXIT_CODE_NORMAL = 1; + SEARCH_COMPLETION_EXIT_CODE_NEW_SEARCH_STARTED = 2; + } + message ITextSearchCompleteMessage { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchCompleteMessage + enum TextSearchCompleteMessageType { // aiserver.v1.RipgrepSearchResultInternal.TextSearchCompleteMessageType + TEXT_SEARCH_COMPLETE_MESSAGE_TYPE_UNSPECIFIED = 0; + TEXT_SEARCH_COMPLETE_MESSAGE_TYPE_INFORMATION = 1; + TEXT_SEARCH_COMPLETE_MESSAGE_TYPE_WARNING = 2; + } + string text = 1; + TextSearchCompleteMessageType type = 2; + optional bool trusted = 3; + } + message IFileSearchStats { // aiserver.v1.RipgrepSearchResultInternal.IFileSearchStats + message ISearchEngineStats { // aiserver.v1.RipgrepSearchResultInternal.ISearchEngineStats + int32 file_walk_time = 1; + int32 directories_walked = 2; + int32 files_walked = 3; + int32 cmd_time = 4; + optional int32 cmd_result_count = 5; + } + message ICachedSearchStats { // aiserver.v1.RipgrepSearchResultInternal.ICachedSearchStats + bool cache_was_resolved = 1; + int32 cache_lookup_time = 2; + int32 cache_filter_time = 3; + int32 cache_entry_count = 4; + } + message IFileSearchProviderStats { // aiserver.v1.RipgrepSearchResultInternal.IFileSearchProviderStats + int32 provider_time = 1; + int32 post_process_time = 2; + } + enum FileSearchProviderType { // aiserver.v1.RipgrepSearchResultInternal.IFileSearchStats.FileSearchProviderType + FILE_SEARCH_PROVIDER_TYPE_UNSPECIFIED = 0; + FILE_SEARCH_PROVIDER_TYPE_FILE_SEARCH_PROVIDER = 1; + FILE_SEARCH_PROVIDER_TYPE_SEARCH_PROCESS = 2; + } + bool from_cache = 1; + ISearchEngineStats search_engine_stats = 2; + ICachedSearchStats cached_search_stats = 3; + IFileSearchProviderStats file_search_provider_stats = 4; + int32 result_count = 5; + FileSearchProviderType type = 6; + optional int32 sorting_time = 7; + } + message ITextSearchStats { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchStats + enum TextSearchProviderType { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchStats.TextSearchProviderType + TEXT_SEARCH_PROVIDER_TYPE_UNSPECIFIED = 0; + TEXT_SEARCH_PROVIDER_TYPE_TEXT_SEARCH_PROVIDER = 1; + TEXT_SEARCH_PROVIDER_TYPE_SEARCH_PROCESS = 2; + TEXT_SEARCH_PROVIDER_TYPE_AI_TEXT_SEARCH_PROVIDER = 3; + } + TextSearchProviderType type = 1; + } + repeated IFileMatch results = 1; + optional SearchCompletionExitCode exit = 2; + optional bool limit_hit = 3; + repeated ITextSearchCompleteMessage messages = 4; + IFileSearchStats file_search_stats = 5; + ITextSearchStats text_search_stats = 6; +} +message RunTerminalCommandResult { // aiserver.v1.RunTerminalCommandResult + string output = 1; + int32 exit_code = 2; + optional bool rejected = 3; + bool popped_out_into_background = 4; +} +message ReadFileResult { // aiserver.v1.ReadFileResult + string contents = 1; + bool did_downgrade_to_line_range = 2; + bool did_shorten_line_range = 3; + bool did_set_default_line_range = 4; + optional string full_file_contents = 5; + optional string outline = 6; + optional int32 start_line_one_indexed = 7; + optional int32 end_line_one_indexed_inclusive = 8; + string relative_workspace_path = 9; + bool did_shorten_char_range = 10; +} +message ListDirResult { // aiserver.v1.ListDirResult + message File { // aiserver.v1.ListDirResult.File + message Timestamp { // google.protobuf.Timestamp + int64 seconds = 1; + int32 nanos = 2; + } + string name = 1; + bool is_directory = 2; + optional int64 size = 3; + optional Timestamp last_modified = 4; + optional int32 num_children = 5; + optional int32 num_lines = 6; + } + repeated File files = 1; + string directory_relative_workspace_path = 2; +} +message EditFileResult { // aiserver.v1.EditFileResult + message FileDiff { // aiserver.v1.EditFileResult.FileDiff + message ChunkDiff { // aiserver.v1.EditFileResult.FileDiff.ChunkDiff + string diff_string = 1; + int32 old_start = 2; + int32 new_start = 3; + int32 old_lines = 4; + int32 new_lines = 5; + int32 lines_removed = 6; + int32 lines_added = 7; + } + enum Editor { // aiserver.v1.EditFileResult.FileDiff.Editor + EDITOR_UNSPECIFIED = 0; + EDITOR_AI = 1; + EDITOR_HUMAN = 2; + } + repeated ChunkDiff chunks = 1; + Editor editor = 2; + bool hit_timeout = 3; + } + FileDiff diff = 1; + bool is_applied = 2; + bool apply_failed = 3; +} +message ToolCallFileSearchResult { // aiserver.v1.ToolCallFileSearchResult + message File { // aiserver.v1.ToolCallFileSearchResult.File + string uri = 1; + } + repeated File files = 1; + optional bool limit_hit = 2; + int32 num_results = 3; +} +message SemanticSearchFullResult { // aiserver.v1.SemanticSearchFullResult + repeated CodeResult code_results = 1; +} +message CreateFileResult { // aiserver.v1.CreateFileResult + bool file_created_successfully = 1; + bool file_already_exists = 2; +} +message DeleteFileResult { // aiserver.v1.DeleteFileResult + bool rejected = 1; + bool file_non_existent = 2; + bool file_deleted_successfully = 3; +} +message ToolResultError { // aiserver.v1.ToolResultError + string client_visible_error_message = 1; + string model_visible_error_message = 2; +} +message ComposerCapabilityRequest { // aiserver.v1.ComposerCapabilityRequest + enum ComposerCapabilityType { // aiserver.v1.ComposerCapabilityRequest.ComposerCapabilityType + COMPOSER_CAPABILITY_TYPE_UNSPECIFIED = 0; + COMPOSER_CAPABILITY_TYPE_LOOP_ON_LINTS = 1; + COMPOSER_CAPABILITY_TYPE_LOOP_ON_TESTS = 2; + COMPOSER_CAPABILITY_TYPE_MEGA_PLANNER = 3; + COMPOSER_CAPABILITY_TYPE_LOOP_ON_COMMAND = 4; + COMPOSER_CAPABILITY_TYPE_TOOL_CALL = 5; + COMPOSER_CAPABILITY_TYPE_DIFF_REVIEW = 6; + COMPOSER_CAPABILITY_TYPE_CONTEXT_PICKING = 7; + COMPOSER_CAPABILITY_TYPE_EDIT_TRAIL = 8; + COMPOSER_CAPABILITY_TYPE_AUTO_CONTEXT = 9; + COMPOSER_CAPABILITY_TYPE_CONTEXT_PLANNER = 10; + COMPOSER_CAPABILITY_TYPE_DIFF_HISTORY = 11; + COMPOSER_CAPABILITY_TYPE_REMEMBER_THIS = 12; + COMPOSER_CAPABILITY_TYPE_DECOMPOSER = 13; + COMPOSER_CAPABILITY_TYPE_USES_CODEBASE = 14; + COMPOSER_CAPABILITY_TYPE_TOOL_FORMER = 15; + } + message LoopOnLintsCapability { // aiserver.v1.ComposerCapabilityRequest.LoopOnLintsCapability + repeated LinterErrors linter_errors = 1; + optional string custom_instructions = 2; + } + message LoopOnTestsCapability { // aiserver.v1.ComposerCapabilityRequest.LoopOnTestsCapability + repeated string test_names = 1; + optional string custom_instructions = 2; + } + message MegaPlannerCapability { // aiserver.v1.ComposerCapabilityRequest.MegaPlannerCapability + optional string custom_instructions = 1; + } + message LoopOnCommandCapability { // aiserver.v1.ComposerCapabilityRequest.LoopOnCommandCapability + string command = 1; + optional string custom_instructions = 2; + optional string output = 3; + optional int32 exit_code = 4; + } + message ToolCallCapability { // aiserver.v1.ComposerCapabilityRequest.ToolCallCapability + message ToolSchema { // aiserver.v1.ComposerCapabilityRequest.ToolSchema + enum ToolType { // aiserver.v1.ComposerCapabilityRequest.ToolType + TOOL_TYPE_UNSPECIFIED = 0; + TOOL_TYPE_ADD_FILE_TO_CONTEXT = 1; + TOOL_TYPE_RUN_TERMINAL_COMMAND = 2; + TOOL_TYPE_ITERATE = 3; + TOOL_TYPE_REMOVE_FILE_FROM_CONTEXT = 4; + TOOL_TYPE_SEMANTIC_SEARCH_CODEBASE = 5; + } + ToolType type = 1; + string name = 2; + repeated string required = 4; + } + optional string custom_instructions = 1; + repeated ToolSchema tool_schemas = 2; + repeated string relevant_files = 3; + repeated string files_in_context = 4; + repeated string semantic_search_files = 5; + } + message DiffReviewCapability { // aiserver.v1.ComposerCapabilityRequest.DiffReviewCapability + message SimpleFileDiff { // aiserver.v1.ComposerCapabilityRequest.DiffReviewCapability.SimpleFileDiff + message Chunk { // aiserver.v1.ComposerCapabilityRequest.DiffReviewCapability.SimpleFileDiff.Chunk + repeated string old_lines = 1; + repeated string new_lines = 2; + LineRange old_range = 3; + LineRange new_range = 4; + } + string relative_workspace_path = 1; + repeated Chunk chunks = 3; + } + optional string custom_instructions = 1; + repeated SimpleFileDiff diffs = 2; + } + message ContextPickingCapability { // aiserver.v1.ComposerCapabilityRequest.ContextPickingCapability + optional string custom_instructions = 1; + repeated string potential_context_files = 2; + repeated CodeChunk potential_context_code_chunks = 3; + repeated string files_in_context = 4; + } + message EditTrailCapability { // aiserver.v1.ComposerCapabilityRequest.EditTrailCapability + optional string custom_instructions = 1; + } + message AutoContextCapability { // aiserver.v1.ComposerCapabilityRequest.AutoContextCapability + optional string custom_instructions = 1; + repeated string additional_files = 2; + } + message ContextPlannerCapability { // aiserver.v1.ComposerCapabilityRequest.ContextPlannerCapability + optional string custom_instructions = 1; + repeated CodeChunk attached_code_chunks = 2; + } + message RememberThisCapability { // aiserver.v1.ComposerCapabilityRequest.RememberThisCapability + optional string custom_instructions = 1; + string memory = 2; + } + message DecomposerCapability { // aiserver.v1.ComposerCapabilityRequest.DecomposerCapability + optional string custom_instructions = 1; + } + ComposerCapabilityType type = 1; + LoopOnLintsCapability loop_on_lints = 2; + LoopOnTestsCapability loop_on_tests = 3; + MegaPlannerCapability mega_planner = 4; + LoopOnCommandCapability loop_on_command = 5; + ToolCallCapability tool_call = 6; + DiffReviewCapability diff_review = 7; + ContextPickingCapability context_picking = 8; + EditTrailCapability edit_trail = 9; + AutoContextCapability auto_context = 10; + ContextPlannerCapability context_planner = 11; + RememberThisCapability remember_this = 12; + DecomposerCapability decomposer = 13; +} +message LinterErrors { // aiserver.v1.LinterErrors + string relative_workspace_path = 1; + repeated LinterError errors = 2; + string file_contents = 3; +} +message LinterError { // aiserver.v1.LinterError + message RelatedInformation { // aiserver.v1.Diagnostic.RelatedInformation + string message = 1; + CursorRange range = 2; + } + enum DiagnosticSeverity { // aiserver.v1.Diagnostic.DiagnosticSeverity + DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0; + DIAGNOSTIC_SEVERITY_ERROR = 1; + DIAGNOSTIC_SEVERITY_WARNING = 2; + DIAGNOSTIC_SEVERITY_INFORMATION = 3; + DIAGNOSTIC_SEVERITY_HINT = 4; + } + string message = 1; + CursorRange range = 2; + optional string source = 3; + repeated RelatedInformation related_information = 4; + optional DiagnosticSeverity severity = 5; +} +message CodeChunk { // aiserver.v1.CodeChunk + enum SummarizationStrategy { // aiserver.v1.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; +} +message SuggestedCodeBlock { // aiserver.v1.SuggestedCodeBlock + string relative_workspace_path = 1; +} +message RedDiff { // aiserver.v1.RedDiff + string relative_workspace_path = 1; + repeated SimplestRange red_ranges = 2; + repeated SimplestRange red_ranges_reversed = 3; + string start_hash = 4; + string end_hash = 5; +} +message LinterErrorsWithoutFileContents { // aiserver.v1.LinterErrorsWithoutFileContents + string relative_workspace_path = 1; + repeated LinterError errors = 2; +} +message DiffHistoryData { // aiserver.v1.DiffHistoryData + string relative_workspace_path = 1; + repeated ComposerFileDiff diffs = 2; + double timestamp = 3; + string unique_id = 4; + ComposerFileDiff start_to_end_diff = 5; +} +message ComposerFileDiff { // aiserver.v1.ComposerFileDiff + message ChunkDiff { // aiserver.v1.ComposerFileDiff.ChunkDiff + string diff_string = 1; + int32 old_start = 2; + int32 new_start = 3; + int32 old_lines = 4; + int32 new_lines = 5; + int32 lines_removed = 6; + int32 lines_added = 7; + } + enum Editor { // aiserver.v1.ComposerFileDiff.Editor + EDITOR_UNSPECIFIED = 0; + EDITOR_AI = 1; + EDITOR_HUMAN = 2; + } + repeated ChunkDiff chunks = 1; + Editor editor = 2; + bool hit_timeout = 3; +} +message ComposerFileDiffHistory { // aiserver.v1.ComposerFileDiffHistory + string file_name = 1; + repeated string diff_history = 2; + repeated double diff_history_timestamps = 3; +} +message ConversationSummary { // aiserver.v1.ConversationSummary + string summary = 1; + string truncation_last_bubble_id_inclusive = 2; + string client_should_start_sending_from_inclusive_bubble_id = 3; +} +message RepositoryInfo { // aiserver.v1.RepositoryInfo + string relative_workspace_path = 1; + repeated string remote_urls = 2; + repeated string remote_names = 3; + string repo_name = 4; + string repo_owner = 5; + bool is_tracked = 6; + bool is_local = 7; + optional int32 num_files = 8; + optional double orthogonal_transform_seed = 9; + optional EmbeddingModel preferred_embedding_model = 10; +} +message ExplicitContext { // aiserver.v1.ExplicitContext + string context = 1; + optional string repo_context = 2; +} +message ModelDetails { // aiserver.v1.ModelDetails + optional string model_name = 1; + optional string api_key = 2; + optional bool enable_ghost_mode = 3; + optional AzureState azure_state = 4; + optional bool enable_slow_pool = 5; + optional string openai_api_base_url = 6; +} +message AzureState { // aiserver.v1.AzureState + string api_key = 1; + string base_url = 2; + string deployment = 3; + bool use_azure = 4; +} +message ChatQuote { // aiserver.v1.ChatQuote + string markdown = 1; + string bubble_id = 2; + int32 section_index = 3; +} +message DebugInfo { // aiserver.v1.DebugInfo + message Breakpoint { // aiserver.v1.DebugInfo.Breakpoint + string relative_workspace_path = 1; + int32 line_number = 2; + repeated string lines_before_breakpoint = 3; + repeated string lines_after_breakpoint = 4; + optional string exception_info = 5; + } + message CallStackFrame { // aiserver.v1.DebugInfo.CallStackFrame + message Scope { // aiserver.v1.DebugInfo.Scope + message Variable { // aiserver.v1.DebugInfo.Variable + string name = 1; + string value = 2; + optional string type = 3; + } + string name = 1; + repeated Variable variables = 2; + } + string relative_workspace_path = 1; + int32 line_number = 2; + string function_name = 3; + repeated Scope scopes = 4; + } + Breakpoint breakpoint = 1; + repeated CallStackFrame call_stack = 2; + repeated CodeBlock history = 3; +} +message ChatExternalLink { // aiserver.v1.ChatExternalLink + string url = 1; + string uuid = 2; +} +message CommitNote { // aiserver.v1.CommitNote + string note = 1; + string commit_hash = 2; +} +message ContextAST { // aiserver.v1.ContextAST + repeated ContainerTree files = 1; +} +message ContainerTree { // aiserver.v1.ContainerTree + string relative_workspace_path = 1; + repeated ContainerTreeNode nodes = 2; +} +message ContainerTreeNode { // aiserver.v1.ContainerTreeNode + message Container { // aiserver.v1.ContainerTreeNode.Container + message Reference { // aiserver.v1.ContainerTreeNode.Reference + string value = 1; + string relative_workspace_path = 2; + } + string doc_string = 1; + string header = 2; + string trailer = 3; + repeated ContainerTreeNode children = 5; + repeated Reference references = 6; + double score = 7; + } + message Blob { // aiserver.v1.ContainerTreeNode.Blob + optional string value = 1; + } + message Symbol { // aiserver.v1.ContainerTreeNode.Symbol + message Reference { // aiserver.v1.ContainerTreeNode.Reference + string value = 1; + string relative_workspace_path = 2; + } + string doc_string = 1; + string value = 2; + repeated Reference references = 6; + double score = 7; + } + Container container = 1; + Blob blob = 2; + Symbol symbol = 3; +} +message StreamChatResponse { // aiserver.v1.StreamChatResponse + message ChunkIdentity { // aiserver.v1.StreamChatResponse.ChunkIdentity + string file_name = 1; + int32 start_line = 2; + int32 end_line = 3; + string text = 4; + ChunkType chunk_type = 5; + } + string text = 1; + optional string server_bubble_id = 22; + optional string debugging_only_chat_prompt = 2; + optional int32 debugging_only_token_count = 3; + DocumentationCitation document_citation = 4; + optional string filled_prompt = 5; + optional bool is_big_file = 6; + optional string intermediate_text = 7; + optional bool is_using_slow_request = 10; + optional ChunkIdentity chunk_identity = 8; + optional DocsReference docs_reference = 9; + optional WebCitation web_citation = 11; + optional StatusUpdates status_updates = 12; + optional ServerTimingInfo timing_info = 13; + optional SymbolLink symbol_link = 14; + optional FileLink file_link = 15; + optional ConversationSummary conversation_summary = 16; + optional ServiceStatusUpdate service_status_update = 17; +} +message DocumentationCitation { // aiserver.v1.DocumentationCitation + repeated DocumentationChunk chunks = 1; +} +message DocumentationChunk { // aiserver.v1.DocumentationChunk + string doc_name = 1; + string page_url = 2; + string documentation_chunk = 3; + float score = 4; + string page_title = 5; +} +message DocsReference { // aiserver.v1.DocsReference + string title = 1; + string url = 2; +} +message WebCitation { // aiserver.v1.WebCitation + repeated WebReference references = 1; +} +message WebReference { // aiserver.v1.WebReference + string title = 2; + string url = 1; +} +message StatusUpdates { // aiserver.v1.StatusUpdates + repeated StatusUpdate updates = 1; +} +message StatusUpdate { // aiserver.v1.StatusUpdate + string message = 1; + optional string metadata = 2; +} +message ServerTimingInfo { // aiserver.v1.ServerTimingInfo + double server_start_time = 1; + double server_first_token_time = 2; + double server_request_sent_time = 3; + double server_end_time = 4; +} +message SymbolLink { // aiserver.v1.SymbolLink + string symbol_name = 1; + string symbol_search_string = 2; + string relative_workspace_path = 3; + int32 rough_line_number = 4; +} +message FileLink { // aiserver.v1.FileLink + string display_name = 1; + string relative_workspace_path = 2; +} +message ServiceStatusUpdate { // aiserver.v1.ServiceStatusUpdate + string message = 1; + string codicon = 2; + optional bool allow_command_links_potentially_unsafe_please_only_use_for_handwritten_trusted_markdown = 3; +} diff --git a/src/chat/config.rs b/src/chat/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..e649b7b4acd6fddd07c2aca08183da78093a9662 --- /dev/null +++ b/src/chat/config.rs @@ -0,0 +1,30 @@ +use crate::AppConfig; + +include!(concat!(env!("OUT_DIR"), "/key.rs")); + +impl KeyConfig { + pub fn new_with_global() -> Self { + Self { + auth_token: None, + disable_vision: Some(AppConfig::get_vision_ability().is_none()), + enable_slow_pool: Some(AppConfig::get_slow_pool()), + usage_check_models: None, + include_web_references: Some(AppConfig::get_web_refs()), + } + } + + pub fn copy_without_auth_token(&self, config: &mut Self) { + if self.disable_vision.is_some() { + config.disable_vision = self.disable_vision; + } + if self.enable_slow_pool.is_some() { + config.enable_slow_pool = self.enable_slow_pool; + } + if self.usage_check_models.is_some() { + config.usage_check_models = self.usage_check_models.clone(); + } + if self.include_web_references.is_some() { + config.include_web_references = self.include_web_references; + } + } +} diff --git a/src/chat/config/key.proto b/src/chat/config/key.proto new file mode 100644 index 0000000000000000000000000000000000000000..2542c7df99b4f4353e73bdb3b1d71b9831b0fc43 --- /dev/null +++ b/src/chat/config/key.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +package key; + +// 动态配置的 API KEY +message KeyConfig { + // 认证令牌信息 + message TokenInfo { + string sub = 1; // 用户标识符 + int64 exp = 2; // 过期时间(Unix 时间戳) + string randomness = 3; // 随机字符串 + string signature = 4; // 签名 + bytes machine_id = 5; // 机器ID的SHA256哈希值 + bytes mac_id = 6; // MAC地址的SHA256哈希值 + } + + // 认证令牌(必需) + TokenInfo auth_token = 1; + + // 是否禁用图片处理能力 + optional bool disable_vision = 4; + + // 是否启用慢速池 + optional bool enable_slow_pool = 5; + + // 使用量检查模型规则 + message UsageCheckModel { + // 检查类型 + enum Type { + TYPE_DEFAULT = 0; // 未指定 + TYPE_DISABLED = 1; // 禁用 + TYPE_ALL = 2; // 全部 + TYPE_CUSTOM = 3; // 自定义列表 + } + Type type = 1; // 检查类型 + repeated string model_ids = 2; // 模型 ID 列表,当 type 为 TYPE_CUSTOM 时生效 + } + // 使用量检查模型规则 + optional UsageCheckModel usage_check_models = 6; + + // 包含网络引用 + optional bool include_web_references = 7; + + // 密码SHA256哈希值 + // bytes secret = 2; +} \ No newline at end of file diff --git a/src/chat/constant.rs b/src/chat/constant.rs new file mode 100644 index 0000000000000000000000000000000000000000..b003cf57ddd96e477130937e6c3f752fd5b689e1 --- /dev/null +++ b/src/chat/constant.rs @@ -0,0 +1,204 @@ +use super::model::Model; + +macro_rules! def_pub_const { + ($name:ident, $value:expr) => { + pub const $name: &'static str = $value; + }; +} +def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF"); +def_pub_const!( + ERR_UNSUPPORTED_IMAGE_FORMAT, + "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF" +); +def_pub_const!(ERR_NODATA, "No data"); + +const MODEL_OBJECT: &str = "model"; +const CREATED: &i64 = &1706659200; + +def_pub_const!(ANTHROPIC, "anthropic"); +def_pub_const!(CURSOR, "cursor"); +def_pub_const!(GOOGLE, "google"); +def_pub_const!(OPENAI, "openai"); +def_pub_const!(DEEPSEEK, "deepseek"); + +def_pub_const!(CLAUDE_3_5_SONNET, "claude-3.5-sonnet"); +def_pub_const!(GPT_4, "gpt-4"); +def_pub_const!(GPT_4O, "gpt-4o"); +def_pub_const!(CLAUDE_3_OPUS, "claude-3-opus"); +def_pub_const!(CURSOR_FAST, "cursor-fast"); +def_pub_const!(CURSOR_SMALL, "cursor-small"); +def_pub_const!(GPT_3_5_TURBO, "gpt-3.5-turbo"); +def_pub_const!(GPT_4_TURBO_2024_04_09, "gpt-4-turbo-2024-04-09"); +def_pub_const!(GPT_4O_128K, "gpt-4o-128k"); +def_pub_const!(GEMINI_1_5_FLASH_500K, "gemini-1.5-flash-500k"); +def_pub_const!(CLAUDE_3_HAIKU_200K, "claude-3-haiku-200k"); +def_pub_const!(CLAUDE_3_5_SONNET_200K, "claude-3-5-sonnet-200k"); +def_pub_const!(CLAUDE_3_5_SONNET_20241022, "claude-3-5-sonnet-20241022"); +def_pub_const!(GPT_4O_MINI, "gpt-4o-mini"); +def_pub_const!(O1_MINI, "o1-mini"); +def_pub_const!(O1_PREVIEW, "o1-preview"); +def_pub_const!(O1, "o1"); +def_pub_const!(CLAUDE_3_5_HAIKU, "claude-3.5-haiku"); +def_pub_const!(GEMINI_EXP_1206, "gemini-exp-1206"); +def_pub_const!( + GEMINI_2_0_FLASH_THINKING_EXP, + "gemini-2.0-flash-thinking-exp" +); +def_pub_const!(GEMINI_2_0_FLASH_EXP, "gemini-2.0-flash-exp"); +def_pub_const!(DEEPSEEK_V3, "deepseek-v3"); +def_pub_const!(DEEPSEEK_R1, "deepseek-r1"); + +// #[derive(Clone, PartialEq, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] +// pub enum ModelType { +// Claude35Sonnet, +// Gpt4, +// Gpt4o, +// Claude3Opus, +// CursorFast, +// CursorSmall, +// Gpt35Turbo, +// Gpt4Turbo202404, +// Gpt4o128k, +// Gemini15Flash500k, +// Claude3Haiku200k, +// Claude35Sonnet200k, +// Claude35Sonnet20241022, +// Gpt4oMini, +// O1Mini, +// O1Preview, +// O1, +// Claude35Haiku, +// GeminiExp1206, +// Gemini20FlashThinkingExp, +// Gemini20FlashExp, +// DeepseekV3, +// DeepseekR1, +// } + +macro_rules! create_model { + ($($id:expr, $owner:expr),* $(,)?) => { + pub const AVAILABLE_MODELS: [Model; count!($( ($id, $owner) )*)] = [ + $( + Model { + id: $id, + created: CREATED, + object: MODEL_OBJECT, + owned_by: $owner, + }, + )* + ]; + }; +} + +macro_rules! count { + () => (0); + (($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*)); +} + +// impl ModelType { +// pub fn as_str_name(&self) -> &'static str { +// match self { +// ModelType::Claude35Sonnet => CLAUDE_3_5_SONNET, +// ModelType::Gpt4 => GPT_4, +// ModelType::Gpt4o => GPT_4O, +// ModelType::Claude3Opus => CLAUDE_3_OPUS, +// ModelType::CursorFast => CURSOR_FAST, +// ModelType::CursorSmall => CURSOR_SMALL, +// ModelType::Gpt35Turbo => GPT_3_5_TURBO, +// ModelType::Gpt4Turbo202404 => GPT_4_TURBO_2024_04_09, +// ModelType::Gpt4o128k => GPT_4O_128K, +// ModelType::Gemini15Flash500k => GEMINI_1_5_FLASH_500K, +// ModelType::Claude3Haiku200k => CLAUDE_3_HAIKU_200K, +// ModelType::Claude35Sonnet200k => CLAUDE_3_5_SONNET_200K, +// ModelType::Claude35Sonnet20241022 => CLAUDE_3_5_SONNET_20241022, +// ModelType::Gpt4oMini => GPT_4O_MINI, +// ModelType::O1Mini => O1_MINI, +// ModelType::O1Preview => O1_PREVIEW, +// ModelType::O1 => O1, +// ModelType::Claude35Haiku => CLAUDE_3_5_HAIKU, +// ModelType::GeminiExp1206 => GEMINI_EXP_1206, +// ModelType::Gemini20FlashThinkingExp => GEMINI_2_0_FLASH_THINKING_EXP, +// ModelType::Gemini20FlashExp => GEMINI_2_0_FLASH_EXP, +// ModelType::DeepseekV3 => DEEPSEEK_V3, +// ModelType::DeepseekR1 => DEEPSEEK_R1, +// } +// } + +// pub fn from_str_name(id :&str) -> Option { +// match id { +// CLAUDE_3_5_SONNET => Some(ModelType::Claude35Sonnet), +// GPT_4 => Some(ModelType::Gpt4), +// GPT_4O => Some(ModelType::Gpt4o), +// CLAUDE_3_OPUS => Some(ModelType::Claude3Opus), +// CURSOR_FAST => Some(ModelType::CursorFast), +// CURSOR_SMALL => Some(ModelType::CursorSmall), +// GPT_3_5_TURBO => Some(ModelType::Gpt35Turbo), +// GPT_4_TURBO_2024_04_09 => Some(ModelType::Gpt4Turbo202404), +// GPT_4O_128K => Some(ModelType::Gpt4o128k), +// GEMINI_1_5_FLASH_500K => Some(ModelType::Gemini15Flash500k), +// CLAUDE_3_HAIKU_200K => Some(ModelType::Claude3Haiku200k), +// CLAUDE_3_5_SONNET_200K => Some(ModelType::Claude35Sonnet200k), +// CLAUDE_3_5_SONNET_20241022 => Some(ModelType::Claude35Sonnet20241022), +// GPT_4O_MINI => Some(ModelType::Gpt4oMini), +// O1_MINI => Some(ModelType::O1Mini), +// O1_PREVIEW => Some(ModelType::O1Preview), +// O1 => Some(ModelType::O1), +// CLAUDE_3_5_HAIKU => Some(ModelType::Claude35Haiku), +// GEMINI_EXP_1206 => Some(ModelType::GeminiExp1206), +// GEMINI_2_0_FLASH_THINKING_EXP => Some(ModelType::Gemini20FlashThinkingExp), +// GEMINI_2_0_FLASH_EXP => Some(ModelType::Gemini20FlashExp), +// DEEPSEEK_V3 => Some(ModelType::DeepseekV3), +// DEEPSEEK_R1 => Some(ModelType::DeepseekR1), +// _ => None, +// } +// } +// } + +create_model!( + CLAUDE_3_5_SONNET, ANTHROPIC, + GPT_4, OPENAI, + GPT_4O, OPENAI, + CLAUDE_3_OPUS, ANTHROPIC, + CURSOR_FAST, CURSOR, + CURSOR_SMALL, CURSOR, + GPT_3_5_TURBO, OPENAI, + GPT_4_TURBO_2024_04_09, OPENAI, + GPT_4O_128K, OPENAI, + GEMINI_1_5_FLASH_500K, GOOGLE, + CLAUDE_3_HAIKU_200K, ANTHROPIC, + CLAUDE_3_5_SONNET_200K, ANTHROPIC, + CLAUDE_3_5_SONNET_20241022, ANTHROPIC, + GPT_4O_MINI, OPENAI, + O1_MINI, OPENAI, + O1_PREVIEW, OPENAI, + O1, OPENAI, + CLAUDE_3_5_HAIKU, ANTHROPIC, + GEMINI_EXP_1206, GOOGLE, + GEMINI_2_0_FLASH_THINKING_EXP, GOOGLE, + GEMINI_2_0_FLASH_EXP, GOOGLE, + DEEPSEEK_V3, DEEPSEEK, + DEEPSEEK_R1, DEEPSEEK, +); + +pub const USAGE_CHECK_MODELS: [&str; 11] = [ + CLAUDE_3_5_SONNET_20241022, + CLAUDE_3_5_SONNET, + GEMINI_EXP_1206, + GPT_4, + GPT_4_TURBO_2024_04_09, + GPT_4O, + CLAUDE_3_5_HAIKU, + GPT_4O_128K, + GEMINI_1_5_FLASH_500K, + CLAUDE_3_HAIKU_200K, + CLAUDE_3_5_SONNET_200K, +]; + +pub const LONG_CONTEXT_MODELS: [&str; 4] = [ + GPT_4O_128K, + GEMINI_1_5_FLASH_500K, + CLAUDE_3_HAIKU_200K, + CLAUDE_3_5_SONNET_200K, +]; + +// include!("constant/models.rs"); diff --git a/src/chat/constant/models.rs b/src/chat/constant/models.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf010b496167c7cb2e35745eb7165f6322440132 --- /dev/null +++ b/src/chat/constant/models.rs @@ -0,0 +1,118 @@ +pub struct DefaultModel { + pub default_on: bool, + pub is_long_context_only: Option, + pub name: &'static str, +} + +pub const AVAILABLE_MODELS2: [DefaultModel; 22] = [ + DefaultModel { + default_on: true, + is_long_context_only: Some(false), + name: CLAUDE_3_5_SONNET, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: GPT_4, + }, + DefaultModel { + default_on: true, + is_long_context_only: None, + name: GPT_4O, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: CLAUDE_3_OPUS, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: CURSOR_FAST, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: CURSOR_SMALL, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: GPT_3_5_TURBO, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: GPT_4_TURBO_2024_04_09, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(true), + name: GPT_4O_128K, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(true), + name: GEMINI_1_5_FLASH_500K, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(true), + name: CLAUDE_3_HAIKU_200K, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(true), + name: CLAUDE_3_5_SONNET_200K, + }, + DefaultModel { + default_on: false, + is_long_context_only: Some(false), + name: CLAUDE_3_5_SONNET_20241022, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(false), + name: GPT_4O_MINI, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(false), + name: O1_MINI, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(false), + name: O1_PREVIEW, + }, + DefaultModel { + default_on: true, + is_long_context_only: Some(false), + name: O1, + }, + DefaultModel { + default_on: false, + is_long_context_only: Some(false), + name: CLAUDE_3_5_HAIKU, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: GEMINI_EXP_1206, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: GEMINI_2_0_FLASH_THINKING_EXP, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: GEMINI_2_0_FLASH_EXP, + }, + DefaultModel { + default_on: false, + is_long_context_only: None, + name: DEEPSEEK_V3, + }, +]; \ No newline at end of file diff --git a/src/chat/error.rs b/src/chat/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..c159ba1c5b946e7fdaf1466aa22c86dfba1e3a1c --- /dev/null +++ b/src/chat/error.rs @@ -0,0 +1,139 @@ +use super::aiserver::v1::ErrorDetails; +use crate::common::model::{ApiStatus, ErrorResponse as CommonErrorResponse}; +use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; +use prost::Message as _; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct ChatError { + error: ErrorBody, +} + +#[derive(Deserialize)] +pub struct ErrorBody { + code: String, + // message: String, always: Error + details: Vec, +} + +#[derive(Deserialize)] +pub struct ErrorDetail { + // #[serde(rename = "type")] + // error_type: String, always: aiserver.v1.ErrorDetails + // debug: ErrorDebug, + value: String, +} + +// #[derive(Deserialize)] +// pub struct ErrorDebug { +// error: String, +// details: ErrorDetails, +// // #[serde(rename = "isExpected")] +// // is_expected: Option, +// } + +// #[derive(Deserialize)] +// pub struct ErrorDetails { +// title: String, +// detail: String, +// // #[serde(rename = "isRetryable")] +// // is_retryable: Option, +// } + +impl ChatError { + pub fn to_error_response(self) -> ErrorResponse { + if self.error.details.is_empty() { + return ErrorResponse { + status: 500, + code: "unknown".to_string(), + error: None, + }; + } + + let error_details = self.error.details.first().and_then(|detail| { + STANDARD_NO_PAD + .decode(&detail.value) + .ok() + .map(bytes::Bytes::from) + .and_then(|buf| ErrorDetails::decode(buf).ok()) + }); + + let status = error_details + .as_ref() + .map(|details| details.status_code()) + .unwrap_or(500); + + ErrorResponse { + status, + code: self.error.code, + error: error_details + .and_then(|details| details.details) + .map(|custom_details| Error { + message: custom_details.title, + details: custom_details.detail, + }), + } + } +} + +#[derive(Serialize)] +pub struct ErrorResponse { + pub status: u16, + pub code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Serialize)] +pub struct Error { + pub message: String, + pub details: String, + // pub value: String, +} + +impl ErrorResponse { + // pub fn to_json(&self) -> serde_json::Value { + // serde_json::to_value(self).unwrap() + // } + + pub fn status_code(&self) -> StatusCode { + StatusCode::from_u16(self.status).unwrap() + } + + pub fn native_code(&self) -> String { + self.error.as_ref().map_or_else( + || self.code.replace("_", " "), + |error| error.message.clone(), + ) + } + + pub fn to_common(self) -> CommonErrorResponse { + CommonErrorResponse { + status: ApiStatus::Error, + code: Some(self.status), + error: self + .error + .as_ref() + .map(|error| error.message.clone()) + .or(Some(self.code.clone())), + message: self.error.as_ref().map(|error| error.details.clone()), + } + } +} + +pub enum StreamError { + ChatError(ChatError), + DataLengthLessThan5, + EmptyStream, +} + +impl std::fmt::Display for StreamError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StreamError::ChatError(error) => write!(f, "{}", error.error.code), + StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"), + StreamError::EmptyStream => write!(f, "empty stream"), + } + } +} diff --git a/src/chat/middleware.rs b/src/chat/middleware.rs new file mode 100644 index 0000000000000000000000000000000000000000..da149834c3a1e779df37d7ab3868238917be10f2 --- /dev/null +++ b/src/chat/middleware.rs @@ -0,0 +1,2 @@ +mod auth; +pub use auth::*; diff --git a/src/chat/middleware/auth.rs b/src/chat/middleware/auth.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba1bd7ff82ca08774a48f8edc1365da9df1b51ee --- /dev/null +++ b/src/chat/middleware/auth.rs @@ -0,0 +1,23 @@ +use crate::app::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN}; +use axum::{ + body::Body, + http::{header::AUTHORIZATION, Request, StatusCode}, + middleware::Next, + response::Response, +}; + +// 认证中间件函数 +pub async fn auth_middleware(request: Request, next: Next) -> Result { + let auth_header = request + .headers() + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(StatusCode::UNAUTHORIZED)?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(StatusCode::UNAUTHORIZED); + } + + Ok(next.run(request).await) +} diff --git a/src/chat/model.rs b/src/chat/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..24576c098ca2843db33c400a675975743bfd7001 --- /dev/null +++ b/src/chat/model.rs @@ -0,0 +1,107 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum MessageContent { + Text(String), + Vision(Vec), +} + +#[derive(Serialize, Deserialize)] +pub struct VisionMessageContent { + #[serde(rename = "type")] + pub content_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub image_url: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct ImageUrl { + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct Message { + pub role: Role, + pub content: MessageContent, +} + +#[derive(Serialize, Deserialize, PartialEq)] +pub enum Role { + #[serde(rename = "system", alias = "developer")] + System, + #[serde(rename = "user", alias = "human")] + User, + #[serde(rename = "assistant", alias = "ai")] + Assistant, +} + +#[derive(Serialize)] +pub struct ChatResponse { + pub id: String, + pub object: String, + pub created: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + pub choices: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub usage: Option, +} + +#[derive(Serialize)] +pub struct Choice { + pub index: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delta: Option, + pub finish_reason: Option, +} + +#[derive(Serialize)] +pub struct Delta { + #[serde(skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, +} + +#[derive(Serialize)] +pub struct Usage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} + +// 模型定义 +#[derive(Serialize, Clone)] +pub struct Model { + pub id: &'static str, + pub created: &'static i64, + pub object: &'static str, + pub owned_by: &'static str, +} + +use super::constant::USAGE_CHECK_MODELS; +use crate::app::model::{AppConfig, UsageCheck}; + +impl Model { + pub fn is_usage_check(&self, usage_check: Option) -> bool { + match usage_check.unwrap_or(AppConfig::get_usage_check()) { + UsageCheck::None => false, + UsageCheck::Default => USAGE_CHECK_MODELS.contains(&self.id), + UsageCheck::All => true, + UsageCheck::Custom(models) => models.contains(&self.id), + } + } +} + +#[derive(Serialize)] +pub struct ModelsResponse { + pub object: &'static str, + pub data: &'static [Model], +} diff --git a/src/chat/route.rs b/src/chat/route.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf124734f6497b4918b732f2407756aa6c5f163c --- /dev/null +++ b/src/chat/route.rs @@ -0,0 +1,19 @@ +mod logs; +pub use logs::{handle_logs, handle_logs_post}; +mod health; +pub use health::{handle_health, handle_root}; +mod tokens; +pub use tokens::{ + handle_add_tokens, handle_basic_calibration, handle_delete_tokens, handle_get_checksum, + handle_get_hash, handle_get_timestamp_header, handle_get_tokens, handle_reload_tokens, + handle_tokens_page, handle_update_tokens, +}; +mod profile; +pub use profile::handle_user_info; +mod config; +pub use config::{ + handle_about, handle_build_key, handle_build_key_page, handle_config_page, handle_env_example, + handle_readme, handle_static, +}; +mod api; +pub use api::handle_api_page; diff --git a/src/chat/route/api.rs b/src/chat/route/api.rs new file mode 100644 index 0000000000000000000000000000000000000000..109d5f91ec16248f66887bd691dc9d1c5b46c15d --- /dev/null +++ b/src/chat/route/api.rs @@ -0,0 +1,26 @@ +use axum::response::{IntoResponse, Response}; +use reqwest::header::CONTENT_TYPE; + +use crate::{ + app::constant::{ + CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_API_PATH, + }, + AppConfig, PageContent, +}; + +pub async fn handle_api_page() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/api.min.html").to_string()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(content.clone()) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } +} diff --git a/src/chat/route/config.rs b/src/chat/route/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..eb94dc2ac7d14724e964e976f8318d2ced4d9ee0 --- /dev/null +++ b/src/chat/route/config.rs @@ -0,0 +1,204 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_CSS_WITH_UTF8, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_JS_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_ABOUT_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH + }, + lazy::{AUTH_TOKEN, KEY_PREFIX}, + model::{AppConfig, BuildKeyRequest, BuildKeyResponse, PageContent, UsageCheckModelType}, + }, + chat::config::{key_config, KeyConfig}, + common::utils::{to_base64, token_to_tokeninfo}, +}; +use axum::{ + body::Body, + extract::Path, + http::{ + header::{AUTHORIZATION, CONTENT_TYPE, LOCATION}, + HeaderMap, StatusCode, + }, + response::{IntoResponse, Response}, + Json, +}; +use prost::Message as _; + +pub async fn handle_env_example() -> impl IntoResponse { + Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(include_str!("../../../.env.example").to_string()) + .unwrap() +} + +// 配置页面处理函数 +pub async fn handle_config_page() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_CONFIG_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/config.min.html").to_string()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(content.clone()) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } +} + +pub async fn handle_static(Path(path): Path) -> impl IntoResponse { + match path.as_str() { + "shared-styles.css" => { + match AppConfig::get_page_content(ROUTE_SHARED_STYLES_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8) + .body(include_str!("../../../static/shared-styles.min.css").to_string()) + .unwrap(), + PageContent::Text(content) | PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } + } + "shared.js" => { + match AppConfig::get_page_content(ROUTE_SHARED_JS_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8) + .body(include_str!("../../../static/shared.min.js").to_string()) + .unwrap(), + PageContent::Text(content) | PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } + } + _ => Response::builder() + .status(StatusCode::NOT_FOUND) + .body("Not found".to_string()) + .unwrap(), + } +} + +pub async fn handle_readme() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/readme.min.html").to_string()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(content.clone()) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } +} + +pub async fn handle_about() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_ABOUT_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header(LOCATION, ROUTE_README_PATH) + .body(Body::empty()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(Body::from(content.clone())) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(Body::from(content.clone())) + .unwrap(), + } +} + +pub async fn handle_build_key_page() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_BUILD_KEY_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/build_key.min.html").to_string()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(content.clone()) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } +} + +pub async fn handle_build_key( + headers: HeaderMap, + Json(request): Json, +) -> (StatusCode, Json) { + // 验证认证令牌 + if AppConfig::is_share() { + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)); + + if auth_header.map_or(true, |h| h != AppConfig::get_share_token().as_str() && h != AUTH_TOKEN.as_str()) { + return ( + StatusCode::UNAUTHORIZED, + Json(BuildKeyResponse::Error("Unauthorized".to_owned())), + ); + } + } + + // 验证并解析 auth_token + let token_info = match token_to_tokeninfo(&request.auth_token) { + Some(info) => info, + None => { + return ( + StatusCode::BAD_REQUEST, + Json(BuildKeyResponse::Error("Invalid auth token".to_owned())), + ) + } + }; + + // 构建 proto 消息 + let mut key_config = KeyConfig { + auth_token: Some(token_info), + disable_vision: request.disable_vision, + enable_slow_pool: request.enable_slow_pool, + usage_check_models: None, + include_web_references: request.include_web_references, + }; + + if let Some(usage_check_models) = request.usage_check_models { + let usage_check = key_config::UsageCheckModel { + r#type: match usage_check_models.model_type { + UsageCheckModelType::Default => { + key_config::usage_check_model::Type::Default as i32 + } + UsageCheckModelType::Disabled => { + key_config::usage_check_model::Type::Disabled as i32 + } + UsageCheckModelType::All => key_config::usage_check_model::Type::All as i32, + UsageCheckModelType::Custom => key_config::usage_check_model::Type::Custom as i32, + }, + model_ids: if matches!(usage_check_models.model_type, UsageCheckModelType::Custom) { + usage_check_models + .model_ids + .iter() + .map(|s| s.to_string()) + .collect() + } else { + Vec::new() + }, + }; + key_config.usage_check_models = Some(usage_check); + } + + // 序列化 + let encoded = key_config.encode_to_vec(); + + let key = format!("{}{}", *KEY_PREFIX, to_base64(&encoded)); + + (StatusCode::OK, Json(BuildKeyResponse::Key(key))) +} diff --git a/src/chat/route/health.rs b/src/chat/route/health.rs new file mode 100644 index 0000000000000000000000000000000000000000..4623af6923343f6519d7a1626b14fac6e92a2ff0 --- /dev/null +++ b/src/chat/route/health.rs @@ -0,0 +1,140 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, + ROUTE_BASIC_CALIBRATION_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, + ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, + ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, + ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_DELETE_PATH, + ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH, ROUTE_TOKENS_UPDATE_PATH, + ROUTE_USER_INFO_PATH, + }, + lazy::{get_start_time, AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH}, + model::{AppConfig, AppState, PageContent}, + }, + chat::constant::AVAILABLE_MODELS, + common::model::{ + health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats}, + ApiStatus, + }, +}; +use axum::{ + body::Body, + extract::State, + http::{ + header::{CONTENT_TYPE, LOCATION}, + HeaderMap, StatusCode, + }, + response::{IntoResponse, Response}, + Json, +}; +use chrono::Local; +use reqwest::header::AUTHORIZATION; +use std::sync::Arc; +use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; +use tokio::sync::Mutex; + +pub async fn handle_root() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_ROOT_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header(LOCATION, ROUTE_HEALTH_PATH) + .body(Body::empty()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(Body::from(content.clone())) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(Body::from(content.clone())) + .unwrap(), + } +} + +pub async fn handle_health( + State(state): State>>, + headers: HeaderMap, +) -> Json { + let start_time = get_start_time(); + let uptime = (Local::now() - start_time).num_seconds(); + + // 先检查 headers 是否包含有效的认证信息 + let stats = if headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .map_or(false, |token| token == AUTH_TOKEN.as_str()) + { + // 只有在需要系统信息时才创建实例 + let mut sys = System::new_with_specifics( + RefreshKind::nothing() + .with_memory(MemoryRefreshKind::everything()) + .with_cpu(CpuRefreshKind::everything()), + ); + + std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); + + // 刷新 CPU 和内存信息 + sys.refresh_memory(); + sys.refresh_cpu_usage(); + + let pid = std::process::id() as usize; + let process = sys.process(pid.into()); + + // 获取内存信息 + let memory = process.map(|p| p.memory()).unwrap_or(0); + + // 获取 CPU 使用率 + let cpu_usage = sys.global_cpu_usage(); + + let state = state.lock().await; + + Some(SystemStats { + started: start_time.to_string(), + total_requests: state.total_requests, + active_requests: state.active_requests, + system: SystemInfo { + memory: MemoryInfo { + rss: memory, // 物理内存使用量(字节) + }, + cpu: CpuInfo { + usage: cpu_usage, // CPU 使用率(百分比) + }, + }, + }) + } else { + None + }; + + Json(HealthCheckResponse { + status: ApiStatus::Healthy, + version: PKG_VERSION, + uptime, + stats, + models: AVAILABLE_MODELS.iter().map(|m| m.id).collect::>(), + endpoints: vec![ + ROUTE_CHAT_PATH.as_str(), + ROUTE_MODELS_PATH.as_str(), + ROUTE_TOKENS_PATH, + ROUTE_TOKENS_GET_PATH, + ROUTE_TOKENS_UPDATE_PATH, + ROUTE_TOKENS_ADD_PATH, + ROUTE_TOKENS_DELETE_PATH, + ROUTE_LOGS_PATH, + ROUTE_ENV_EXAMPLE_PATH, + ROUTE_CONFIG_PATH, + ROUTE_STATIC_PATH, + ROUTE_ABOUT_PATH, + ROUTE_README_PATH, + ROUTE_API_PATH, + ROUTE_GET_HASH, + ROUTE_GET_CHECKSUM, + ROUTE_GET_TIMESTAMP_HEADER, + ROUTE_BASIC_CALIBRATION_PATH, + ROUTE_USER_INFO_PATH, + ROUTE_BUILD_KEY_PATH, + ], + }) +} diff --git a/src/chat/route/logs.rs b/src/chat/route/logs.rs new file mode 100644 index 0000000000000000000000000000000000000000..2e6d18f0fc43f3e6070b5d75a9fa9385beb5778c --- /dev/null +++ b/src/chat/route/logs.rs @@ -0,0 +1,109 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_LOGS_PATH, + }, + lazy::AUTH_TOKEN, + model::{AppConfig, AppState, PageContent, RequestLog}, + }, + common::{model::ApiStatus, utils::extract_token}, +}; +use axum::{ + body::Body, + extract::State, + http::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + HeaderMap, StatusCode, + }, + response::{IntoResponse, Response}, + Json, +}; +use chrono::Local; +use std::sync::Arc; +use tokio::sync::Mutex; + +// 日志处理 +pub async fn handle_logs() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_LOGS_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(Body::from( + include_str!("../../../static/logs.min.html").to_string(), + )) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(Body::from(content.clone())) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(Body::from(content.clone())) + .unwrap(), + } +} + +pub async fn handle_logs_post( + State(state): State>>, + headers: HeaderMap, +) -> Result, StatusCode> { + let auth_token = AUTH_TOKEN.as_str(); + + // 获取认证头 + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(StatusCode::UNAUTHORIZED)?; + + let state = state.lock().await; + + // 如果是管理员token,返回所有日志 + if auth_header == auth_token { + return Ok(Json(LogsResponse { + status: ApiStatus::Success, + total: state.total_requests, + active: Some(state.active_requests), + error: Some(state.error_requests), + logs: state.request_logs.clone(), + timestamp: Local::now().to_string(), + })); + } + + // 解析 token + let token_part = extract_token(auth_header).ok_or(StatusCode::UNAUTHORIZED)?; + + // 否则筛选出token匹配的日志 + let filtered_logs: Vec = state + .request_logs + .iter() + .filter(|log| log.token_info.token == token_part) + .cloned() + .collect(); + + // 如果没有匹配的日志,返回未授权错误 + if filtered_logs.is_empty() { + return Err(StatusCode::UNAUTHORIZED); + } + + Ok(Json(LogsResponse { + status: ApiStatus::Success, + total: filtered_logs.len() as u64, + active: None, + error: None, + logs: filtered_logs, + timestamp: Local::now().to_string(), + })) +} + +#[derive(serde::Serialize)] +pub struct LogsResponse { + pub status: ApiStatus, + pub total: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub active: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + pub logs: Vec, + pub timestamp: String, +} diff --git a/src/chat/route/profile.rs b/src/chat/route/profile.rs new file mode 100644 index 0000000000000000000000000000000000000000..6d3af8b747e472dd98d63ab3630a5a3e633c2be1 --- /dev/null +++ b/src/chat/route/profile.rs @@ -0,0 +1,34 @@ +use crate::{ + chat::constant::ERR_NODATA, + common::{model::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}}, +}; +use axum::Json; + +use super::tokens::TokenRequest; + +pub async fn handle_user_info(Json(request): Json) -> Json { + let auth_token = match request.token { + Some(token) => token, + None => { + return Json(GetUserInfo::Error { + error: ERR_NODATA.to_string(), + }) + } + }; + + let token = match extract_token(&auth_token) { + Some(token) => token, + None => { + return Json(GetUserInfo::Error { + error: ERR_NODATA.to_string(), + }) + } + }; + + match get_token_profile(&token).await { + Some(usage) => Json(GetUserInfo::Usage(usage)), + None => Json(GetUserInfo::Error { + error: ERR_NODATA.to_string(), + }), + } +} diff --git a/src/chat/route/tokens.rs b/src/chat/route/tokens.rs new file mode 100644 index 0000000000000000000000000000000000000000..b5ce309263575cbd139921de47d16fce1a3f90b9 --- /dev/null +++ b/src/chat/route/tokens.rs @@ -0,0 +1,481 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENS_PATH, + }, + lazy::{AUTH_TOKEN, TOKEN_LIST_FILE}, + model::{ + AppConfig, AppState, PageContent, TokenAddRequestTokenInfo, TokenInfo, + TokenUpdateRequest, TokensDeleteRequest, TokensDeleteResponse, + }, + }, + common::{ + model::{error::ChatError, ApiStatus, ErrorResponse}, + utils::{ + extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, + generate_checksum_with_repair, generate_hash, generate_timestamp_header, load_tokens, + parse_token, validate_token, validate_token_and_checksum, write_tokens, + }, + }, +}; +use axum::{ + extract::{Query, State}, + http::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + HeaderMap, + }, + response::{IntoResponse, Response}, + Json, +}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub async fn handle_get_hash() -> Response { + let hash = generate_hash(); + + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(), + ); + + (headers, hash).into_response() +} + +#[derive(Deserialize)] +pub struct ChecksumQuery { + #[serde(default)] + pub checksum: Option, +} + +pub async fn handle_get_checksum(Query(query): Query) -> Response { + let checksum = match query.checksum { + None => generate_checksum_with_default(), + Some(checksum) => generate_checksum_with_repair(&checksum), + }; + + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(), + ); + + (headers, checksum).into_response() +} + +pub async fn handle_get_timestamp_header() -> Response { + let timestamp_header = generate_timestamp_header(); + + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(), + ); + + (headers, timestamp_header).into_response() +} + +pub async fn handle_get_tokens( + State(state): State>>, + headers: HeaderMap, +) -> Result, StatusCode> { + // 验证 AUTH_TOKEN + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(StatusCode::UNAUTHORIZED)?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(StatusCode::UNAUTHORIZED); + } + + let tokens = state.lock().await.token_infos.clone(); + let tokens_count = tokens.len(); + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + tokens: Some(tokens), + tokens_count, + message: None, + })) +} + +#[derive(Serialize)] +pub struct TokenInfoResponse { + pub status: ApiStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub tokens: Option>, + pub tokens_count: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +pub async fn handle_reload_tokens( + State(state): State>>, + headers: HeaderMap, +) -> Result, StatusCode> { + // 验证 AUTH_TOKEN + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(StatusCode::UNAUTHORIZED)?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(StatusCode::UNAUTHORIZED); + } + + // 重新加载 tokens + let tokens = load_tokens(); + let tokens_count = tokens.len(); + + // 更新应用状态 + { + let mut state = state.lock().await; + state.token_infos = tokens; + } + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + tokens: None, + tokens_count, + message: Some("Token list has been reloaded".to_string()), + })) +} + +pub async fn handle_update_tokens( + State(state): State>>, + headers: HeaderMap, + Json(request): Json, +) -> Result, StatusCode> { + // 验证 AUTH_TOKEN + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(StatusCode::UNAUTHORIZED)?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(StatusCode::UNAUTHORIZED); + } + + let token_list_file = TOKEN_LIST_FILE.as_str(); + + std::fs::write(&token_list_file, &request.tokens) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // 重新加载 tokens + let token_infos = load_tokens(); + let tokens_count = token_infos.len(); + + // 更新应用状态 + { + let mut state = state.lock().await; + state.token_infos = token_infos; + } + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + tokens: None, + tokens_count, + message: Some("Token files have been updated and reloaded".to_string()), + })) +} + +pub async fn handle_add_tokens( + State(state): State>>, + headers: HeaderMap, + Json(request): Json>, +) -> Result, (StatusCode, Json)> { + // 验证 AUTH_TOKEN + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + ))?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + )); + } + + let token_list_file = TOKEN_LIST_FILE.as_str(); + + // 获取当前的 tokens 并创建新的 token_infos + let mut token_infos = { + let state = state.lock().await; + state.token_infos.clone() + }; + + // 创建现有token的集合 + let existing_tokens: std::collections::HashSet<_> = + token_infos.iter().map(|info| info.token.as_str()).collect(); + + // 预分配容量 + let mut new_tokens = Vec::with_capacity(request.len()); + + // 处理新的tokens + for token_info in request { + let parsed_token = parse_token(&token_info.token); + if !existing_tokens.contains(parsed_token.as_str()) && validate_token(&parsed_token) { + new_tokens.push(TokenInfo { + token: parsed_token, + // 如果提供了checksum就使用提供的,否则生成新的 + checksum: token_info + .checksum + .as_deref() + .map(generate_checksum_with_repair) + .unwrap_or_else(generate_checksum_with_default), + profile: None, + }); + } + } + + // 如果有新tokens才进行后续操作 + if !new_tokens.is_empty() { + // 预分配足够的容量 + token_infos.reserve(new_tokens.len()); + token_infos.extend(new_tokens); + + // 写入文件 + write_tokens(&token_infos, token_list_file).map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + status: ApiStatus::Error, + code: None, + error: Some("Failed to update token list file".to_string()), + message: Some("无法更新token list文件".to_string()), + }), + ) + })?; + + // 获取最终的tokens数量(在更新状态之前) + let tokens_count = token_infos.len(); + + // 更新应用状态 + { + let mut state = state.lock().await; + state.token_infos = token_infos; + } + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + tokens: None, + tokens_count, + message: Some("New tokens have been added and reloaded".to_string()), + })) + } else { + // 如果没有新tokens,使用原始数量 + let tokens_count = token_infos.len(); + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + tokens: None, + tokens_count, + message: Some("No new tokens were added".to_string()), + })) + } +} + +pub async fn handle_delete_tokens( + State(state): State>>, + headers: HeaderMap, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + // 验证 AUTH_TOKEN + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + ))?; + + if auth_header != AUTH_TOKEN.as_str() { + return Err(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + )); + } + + let token_infos = state.lock().await.token_infos.clone(); + let original_count = token_infos.len(); // 提前存储原始长度 + + // 获取token_list文件路径 + let token_list_file = TOKEN_LIST_FILE.as_str(); + + // 创建要删除的tokens的HashSet,提高查找效率 + let tokens_to_delete: std::collections::HashSet<_> = request.tokens.iter().collect(); + + // 如果需要的话计算 failed_tokens + let failed_tokens = if request.expectation.needs_failed_tokens() { + Some( + request + .tokens + .iter() + .filter(|token| !token_infos.iter().any(|info| &info.token == *token)) + .cloned() + .collect::>(), + ) + } else { + None + }; + + // 预分配容量并过滤掉要删除的tokens + let estimated_capacity = original_count.saturating_sub(tokens_to_delete.len()); + let mut filtered_token_infos = Vec::with_capacity(estimated_capacity); + + // 一次性过滤tokens + for info in token_infos { + if !tokens_to_delete.contains(&info.token) { + filtered_token_infos.push(info); + } + } + + // 如果有tokens被删除才进行更新操作 + if filtered_token_infos.len() < original_count { + // 写入文件 + write_tokens(&filtered_token_infos, token_list_file).map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + status: ApiStatus::Error, + code: None, + error: Some("Failed to update token list file".to_string()), + message: Some("无法更新token list文件".to_string()), + }), + ) + })?; + + // 如果需要的话计算 updated_tokens + let updated_tokens = if request.expectation.needs_updated_tokens() { + Some( + filtered_token_infos + .iter() + .map(|info| info.token.clone()) + .collect(), + ) + } else { + None + }; + + // 更新状态 + { + let mut state = state.lock().await; + state.token_infos = filtered_token_infos; + } + + Ok(Json(TokensDeleteResponse { + status: ApiStatus::Success, + updated_tokens, + failed_tokens, + })) + } else { + // 如果没有tokens被删除 + Ok(Json(TokensDeleteResponse { + status: ApiStatus::Success, + updated_tokens: if request.expectation.needs_updated_tokens() { + Some( + filtered_token_infos + .iter() + .map(|info| info.token.clone()) + .collect(), + ) + } else { + None + }, + failed_tokens, + })) + } +} + +pub async fn handle_tokens_page() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_TOKENS_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/tokens.min.html").to_string()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(content.clone()) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } +} + +#[derive(Deserialize)] +pub struct TokenRequest { + pub token: Option, +} + +#[derive(Serialize)] +pub struct BasicCalibrationResponse { + pub status: ApiStatus, + pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub create_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub checksum_time: Option, +} + +pub async fn handle_basic_calibration( + Json(request): Json, +) -> Json { + // 从请求头中获取并验证 auth token + let auth_token = match request.token { + Some(token) => token, + None => { + return Json(BasicCalibrationResponse { + status: ApiStatus::Error, + message: Some("未提供授权令牌".to_string()), + user_id: None, + create_at: None, + checksum_time: None, + }) + } + }; + + // 校验 token 和 checksum + let (token, checksum) = match validate_token_and_checksum(&auth_token) { + Some(parts) => parts, + None => { + return Json(BasicCalibrationResponse { + status: ApiStatus::Error, + message: Some("无效令牌或无效校验和".to_string()), + user_id: None, + create_at: None, + checksum_time: None, + }) + } + }; + + // 提取用户ID和创建时间 + let user_id = extract_user_id(&token); + let create_at = extract_time(&token).map(|dt| dt.to_string()); + let checksum_time = extract_time_ks(&checksum[..8]); + + // 返回校验结果 + Json(BasicCalibrationResponse { + status: ApiStatus::Success, + message: Some("校验成功".to_string()), + user_id, + create_at, + checksum_time, + }) +} diff --git a/src/chat/service.rs b/src/chat/service.rs new file mode 100644 index 0000000000000000000000000000000000000000..dffc972878f9a1ffd92e07edf31dbb405e7c69fe --- /dev/null +++ b/src/chat/service.rs @@ -0,0 +1,766 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION, + OBJECT_CHAT_COMPLETION_CHUNK, + }, + lazy::{AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT}, + model::{ + AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo, + UsageCheck, + }, + }, + chat::{ + config::KeyConfig, + constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS}, + error::StreamError, + model::{ + ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage, + }, + stream::{StreamDecoder, StreamMessage}, + }, + common::{ + client::build_client, + model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse}, + utils::{ + format_time_ms, from_base64, get_token_profile, tokeninfo_to_token, + validate_token_and_checksum, TrimNewlines as _, + }, + }, +}; +use axum::{ + body::Body, + extract::State, + http::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + HeaderMap, StatusCode, + }, + response::Response, + Json, +}; +use bytes::Bytes; +use futures::StreamExt; +use prost::Message as _; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::{ + convert::Infallible, + sync::{atomic::AtomicBool, Arc}, +}; +use tokio::sync::Mutex; +use uuid::Uuid; + +// 模型列表处理 +pub async fn handle_models() -> Json { + Json(ModelsResponse { + object: "list", + data: &AVAILABLE_MODELS, + }) +} + +// 聊天处理函数的签名 +pub async fn handle_chat( + State(state): State>>, + headers: HeaderMap, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + let allow_claude = AppConfig::get_allow_claude(); + + let is_search = request.model.ends_with("-online"); + let model_name = if is_search { + request.model[..request.model.len() - 7].to_string() + } else { + request.model.clone() + }; + + // 验证模型是否支持并获取模型信息 + let model = AVAILABLE_MODELS.iter().find(|m| m.id == model_name); + let model_supported = model.is_some(); + + if !(model_supported || allow_claude && request.model.starts_with("claude")) { + return Err(( + StatusCode::BAD_REQUEST, + Json(ChatError::ModelNotSupported(request.model).to_json()), + )); + } + + let request_time = chrono::Local::now(); + + // 验证请求 + if request.messages.is_empty() { + return Err(( + StatusCode::BAD_REQUEST, + Json(ChatError::EmptyMessages.to_json()), + )); + } + + // 获取并处理认证令牌 + let auth_header = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) + .ok_or(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + ))?; + + let mut current_config = KeyConfig::new_with_global(); + + // 验证认证token并获取token信息 + let (auth_token, checksum) = match auth_header { + // 管理员Token验证逻辑 + token + if token == AUTH_TOKEN.as_str() + || (AppConfig::is_share() && token == AppConfig::get_share_token().as_str()) => + { + static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0); + let state_guard = state.lock().await; + let token_infos = &state_guard.token_infos; + + // 检查是否存在可用的token + if token_infos.is_empty() { + return Err(( + StatusCode::SERVICE_UNAVAILABLE, + Json(ChatError::NoTokens.to_json()), + )); + } + + // 轮询选择token + let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len(); + let token_info = &token_infos[index]; + (token_info.token.clone(), token_info.checksum.clone()) + } + + token if AppConfig::get_dynamic_key() && token.starts_with(&*KEY_PREFIX) => { + from_base64(&token[*KEY_PREFIX_LEN..]) + .and_then(|decoded_bytes| KeyConfig::decode(&decoded_bytes[..]).ok()) + .and_then(|key_config| { + key_config.copy_without_auth_token(&mut current_config); + key_config.auth_token + }) + .and_then(|token_info| tokeninfo_to_token(&token_info)) + .ok_or(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + ))? + } + + // 普通用户Token验证逻辑 + token => validate_token_and_checksum(token).ok_or(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + ))?, + }; + + let current_config = current_config; + + let current_id: u64; + + // 更新请求日志 + { + let state_clone = state.clone(); + let mut state = state.lock().await; + state.total_requests += 1; + state.active_requests += 1; + + // 查找最新的相同token的日志,检查使用情况 + let need_profile_check = state + .request_logs + .iter() + .rev() + .find(|log| log.token_info.token == auth_token && log.token_info.profile.is_some()) + .and_then(|log| log.token_info.profile.as_ref()) + .map(|profile| { + if profile.stripe.membership_type != MembershipType::Free { + return false; + } + + let is_premium = USAGE_CHECK_MODELS.contains(&model_name.as_str()); + let standard = &profile.usage.standard; + let premium = &profile.usage.premium; + + if is_premium { + premium + .max_requests + .map_or(false, |max| premium.num_requests >= max) + } else { + standard + .max_requests + .map_or(false, |max| standard.num_requests >= max) + } + }) + .unwrap_or(false); + + // 如果达到限制,直接返回未授权错误 + if need_profile_check { + state.active_requests -= 1; + state.error_requests += 1; + return Err(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + )); + } + + let next_id = state.request_logs.last().map_or(1, |log| log.id + 1); + current_id = next_id; + + // 如果需要获取用户使用情况,创建后台任务获取profile + if model + .map(|m| { + m.is_usage_check(UsageCheck::from_proto( + current_config.usage_check_models.as_ref(), + )) + }) + .unwrap_or(false) + { + let auth_token_clone = auth_token.clone(); + let state_clone = state_clone.clone(); + let log_id = next_id; + + tokio::spawn(async move { + let profile = get_token_profile(&auth_token_clone).await; + let mut state = state_clone.lock().await; + + // 先找到所有需要更新的位置的索引 + let token_info_idx = state + .token_infos + .iter() + .position(|info| info.token == auth_token_clone); + + let log_idx = state.request_logs.iter().rposition(|log| log.id == log_id); + + // 根据索引更新 + match (token_info_idx, log_idx) { + (Some(t_idx), Some(l_idx)) => { + state.token_infos[t_idx].profile = profile.clone(); + state.request_logs[l_idx].token_info.profile = profile; + } + (Some(t_idx), None) => { + state.token_infos[t_idx].profile = profile; + } + (None, Some(l_idx)) => { + state.request_logs[l_idx].token_info.profile = profile; + } + (None, None) => {} + } + }); + } + + state.request_logs.push(RequestLog { + id: next_id, + timestamp: request_time, + model: request.model.clone(), + token_info: TokenInfo { + token: auth_token.clone(), + checksum: checksum.clone(), + profile: None, + }, + prompt: None, + timing: TimingInfo { + total: 0.0, + first: None, + }, + stream: request.stream, + status: LogStatus::Pending, + error: None, + }); + + if state.request_logs.len() > *REQUEST_LOGS_LIMIT { + state.request_logs.remove(0); + } + } + + // 将消息转换为hex格式 + let hex_data = match super::adapter::encode_chat_message( + request.messages, + &model_name, + current_config.disable_vision(), + current_config.enable_slow_pool(), + is_search, + ) + .await + { + Ok(data) => data, + Err(e) => { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Failed; + log.error = Some(e.to_string()); + } + state.active_requests -= 1; + state.error_requests += 1; + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json( + ChatError::RequestFailed("Failed to encode chat message".to_string()).to_json(), + ), + )); + } + }; + + // 构建请求客户端 + let client = build_client(&auth_token, &checksum, is_search); + // 添加超时设置 + let response = tokio::time::timeout( + std::time::Duration::from_secs(*SERVICE_TIMEOUT), + client.body(hex_data).send(), + ) + .await; + + // 处理请求结果 + let response = match response { + Ok(inner_response) => match inner_response { + Ok(resp) => { + // 更新请求日志为成功 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Success; + } + } + resp + } + Err(e) => { + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Failed; + log.error = Some(e.to_string()); + } + state.active_requests -= 1; + state.error_requests += 1; + } + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ChatError::RequestFailed(e.to_string()).to_json()), + )); + } + }, + Err(_) => { + // 处理超时错误 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Failed; + log.error = Some("Request timeout".to_string()); + } + state.active_requests -= 1; + state.error_requests += 1; + } + return Err(( + StatusCode::GATEWAY_TIMEOUT, + Json(ChatError::RequestFailed("Request timeout".to_string()).to_json()), + )); + } + }; + + // 释放活动请求计数 + { + let mut state = state.lock().await; + state.active_requests -= 1; + } + + let convert_web_ref = current_config.include_web_references(); + + if request.stream { + let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple()); + let is_start = Arc::new(AtomicBool::new(true)); + let start_time = std::time::Instant::now(); + let first_chunk_time = Arc::new(Mutex::new(None::)); + let decoder = Arc::new(Mutex::new(StreamDecoder::new())); + + // 定义消息处理器的上下文结构体 + struct MessageProcessContext<'a> { + response_id: &'a str, + model: &'a str, + is_start: &'a AtomicBool, + first_chunk_time: &'a Mutex>, + start_time: std::time::Instant, + state: &'a Mutex, + current_id: u64, + } + + // 处理消息并生成响应数据的辅助函数 + async fn process_messages( + messages: Vec, + ctx: &MessageProcessContext<'_>, + ) -> String { + let mut response_data = String::new(); + + for message in messages { + match message { + StreamMessage::Content(text) => { + let is_first = ctx.is_start.load(Ordering::SeqCst); + if is_first { + if let Ok(mut first_time) = ctx.first_chunk_time.try_lock() { + *first_time = Some(ctx.start_time.elapsed().as_secs_f64()); + } + } + + let response = ChatResponse { + id: ctx.response_id.to_string(), + object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), + created: chrono::Utc::now().timestamp(), + model: if is_first { + Some(ctx.model.to_string()) + } else { + None + }, + choices: vec![Choice { + index: 0, + message: None, + delta: Some(Delta { + role: if is_first { + Some(Role::Assistant) + } else { + None + }, + content: if is_first { + ctx.is_start.store(false, Ordering::SeqCst); + Some(text.trim_leading_newlines()) + } else { + Some(text) + }, + }), + finish_reason: None, + }], + usage: None, + }; + + response_data.push_str(&format!( + "data: {}\n\n", + serde_json::to_string(&response).unwrap() + )); + } + StreamMessage::StreamEnd => { + // 计算总时间和首次片段时间 + let total_time = ctx.start_time.elapsed().as_secs_f64(); + let first_time = ctx.first_chunk_time.lock().await.unwrap_or(total_time); + + { + let mut state = ctx.state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == ctx.current_id) + { + log.timing.total = format_time_ms(total_time); + log.timing.first = Some(format_time_ms(first_time)); + } + } + + let response = ChatResponse { + id: ctx.response_id.to_string(), + object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), + created: chrono::Utc::now().timestamp(), + model: None, + choices: vec![Choice { + index: 0, + message: None, + delta: Some(Delta { + role: None, + content: None, + }), + finish_reason: Some(FINISH_REASON_STOP.to_string()), + }], + usage: None, + }; + response_data.push_str(&format!( + "data: {}\n\ndata: [DONE]\n\n", + serde_json::to_string(&response).unwrap() + )); + } + StreamMessage::Debug(debug_prompt) => { + if let Ok(mut state) = ctx.state.try_lock() { + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == ctx.current_id) + { + log.prompt = Some(debug_prompt); + } + } + } + _ => {} // 忽略其他消息类型 + } + } + + response_data + } + + // 首先处理stream直到获得第一个结果 + let mut stream = response.bytes_stream(); + while !decoder.lock().await.is_first_result_ready() { + match stream.next().await { + Some(Ok(chunk)) => { + if let Err(StreamError::ChatError(error)) = + decoder.lock().await.decode(&chunk, convert_web_ref) + { + let error_response = error.to_error_response(); + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Failed; + log.error = Some(error_response.native_code()); + log.timing.total = + format_time_ms(start_time.elapsed().as_secs_f64()); + state.error_requests += 1; + } + } + return Err(( + error_response.status_code(), + Json(error_response.to_common()), + )); + } + } + Some(Err(e)) => { + let error_message = format!("Failed to read response chunk: {}", e); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ChatError::RequestFailed(error_message).to_json()), + )); + } + None => { + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Failed; + log.error = Some("Empty stream response".to_string()); + state.error_requests += 1; + } + } + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json( + ChatError::RequestFailed("Empty stream response".to_string()).to_json(), + ), + )); + } + } + } + + // 处理后续的stream + let stream = stream.then({ + let decoder = decoder.clone(); + let response_id = response_id.clone(); + let model = request.model.clone(); + let is_start = is_start.clone(); + let first_chunk_time = first_chunk_time.clone(); + let state = state.clone(); + + move |chunk| { + let decoder = decoder.clone(); + let response_id = response_id.clone(); + let model = model.clone(); + let is_start = is_start.clone(); + let first_chunk_time = first_chunk_time.clone(); + let state = state.clone(); + + async move { + let chunk = chunk.unwrap_or_default(); + + let ctx = MessageProcessContext { + response_id: &response_id, + model: &model, + is_start: &is_start, + first_chunk_time: &first_chunk_time, + start_time, + state: &state, + current_id, + }; + + // 使用decoder处理chunk + let messages = match decoder.lock().await.decode(&chunk, convert_web_ref) { + Ok(msgs) => msgs, + Err(e) => { + eprintln!("[警告] Stream error: {}", e); + return Ok::<_, Infallible>(Bytes::new()); + } + }; + + let mut response_data = String::new(); + + if let Some(first_msg) = decoder.lock().await.take_first_result() { + let first_response = process_messages(first_msg, &ctx).await; + response_data.push_str(&first_response); + } + + let current_response = process_messages(messages, &ctx).await; + if !current_response.is_empty() { + response_data.push_str(¤t_response); + } + + Ok(Bytes::from(response_data)) + } + } + }); + + Ok(Response::builder() + .header("Cache-Control", "no-cache") + .header("Connection", "keep-alive") + .header(CONTENT_TYPE, "text/event-stream") + .body(Body::from_stream(stream)) + .unwrap()) + } else { + // 非流式响应 + let start_time = std::time::Instant::now(); + let mut first_chunk_time = None::; + let mut decoder = StreamDecoder::new(); + let mut full_text = String::with_capacity(1024); + let mut stream = response.bytes_stream(); + + // 逐个处理chunks + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| { + let error_message = format!("Failed to read response chunk: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ChatError::RequestFailed(error_message).to_json()), + ) + })?; + + // 立即处理当前chunk + match decoder.decode(&chunk, convert_web_ref) { + Ok(messages) => { + for message in messages { + match message { + StreamMessage::Content(text) => { + if first_chunk_time.is_none() { + first_chunk_time = Some(start_time.elapsed().as_secs_f64()); + } + full_text.push_str(&text); + } + StreamMessage::Debug(debug_prompt) => { + if let Ok(mut state) = state.try_lock() { + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.prompt = Some(debug_prompt); + } + } + } + _ => {} + } + } + } + Err(StreamError::ChatError(error)) => { + let error_response = error.to_error_response(); + return Err(( + error_response.status_code(), + Json(error_response.to_common()), + )); + } + Err(e) => { + let error_response = ErrorResponse { + status: ApiStatus::Error, + code: Some(500), + error: Some(e.to_string()), + message: None, + }; + return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))); + } + } + } + + // 检查响应是否为空 + if full_text.is_empty() { + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = LogStatus::Failed; + log.error = Some("Empty response received".to_string()); + state.error_requests += 1; + } + } + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ChatError::RequestFailed("Empty response received".to_string()).to_json()), + )); + } + + let response_data = ChatResponse { + id: format!("chatcmpl-{}", Uuid::new_v4().simple()), + object: OBJECT_CHAT_COMPLETION.to_string(), + created: chrono::Utc::now().timestamp(), + model: Some(request.model), + choices: vec![Choice { + index: 0, + message: Some(Message { + role: Role::Assistant, + content: MessageContent::Text(full_text.trim_leading_newlines()), + }), + delta: None, + finish_reason: Some(FINISH_REASON_STOP.to_string()), + }], + usage: Some(Usage { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }), + }; + + { + // 更新请求日志时间信息和状态 + let total_time = format_time_ms(start_time.elapsed().as_secs_f64()); + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.timing.total = total_time; + log.timing.first = first_chunk_time; + log.status = LogStatus::Success; + } + } + + Ok(Response::builder() + .header(CONTENT_TYPE, "application/json") + .body(Body::from(serde_json::to_string(&response_data).unwrap())) + .unwrap()) + } +} diff --git a/src/chat/stream.rs b/src/chat/stream.rs new file mode 100644 index 0000000000000000000000000000000000000000..953526d51e07bc5fbc68fc26148762863b786a65 --- /dev/null +++ b/src/chat/stream.rs @@ -0,0 +1,2 @@ +mod decoder; +pub use decoder::*; diff --git a/src/chat/stream/decoder.rs b/src/chat/stream/decoder.rs new file mode 100644 index 0000000000000000000000000000000000000000..f2926d441ec46892c45c15738fa6bb156e6ea6cd --- /dev/null +++ b/src/chat/stream/decoder.rs @@ -0,0 +1,406 @@ +use crate::chat::{ + aiserver::v1::StreamChatResponse, + error::{ChatError, StreamError}, +}; +use flate2::read::GzDecoder; +use prost::Message; +use std::{collections::BTreeMap, io::Read}; + +// 解压gzip数据 +fn decompress_gzip(data: &[u8]) -> Option> { + let mut decoder = GzDecoder::new(data); + let mut decompressed = Vec::new(); + + match decoder.read_to_end(&mut decompressed) { + Ok(_) => Some(decompressed), + Err(_) => { + // println!("gzip解压失败: {}", e); + None + } + } +} + +pub trait ToMarkdown { + fn to_markdown(&self) -> String; +} + +impl ToMarkdown for BTreeMap { + fn to_markdown(&self) -> String { + if self.is_empty() { + return String::new(); + } + + let mut result = String::from("WebReferences:\n"); + for (i, (url, title)) in self.iter().enumerate() { + result.push_str(&format!("{}. [{}]({})\n", i + 1, title, url)); + } + result.push_str("\n"); + result + } +} + +#[derive(PartialEq, Clone, Debug)] +pub enum StreamMessage { + // 调试 + Debug(String), + // 网络引用 + WebReference(BTreeMap), + // 内容开始标志 + ContentStart, + // 消息内容 + Content(String), + // 流结束标志 + StreamEnd, +} + +impl StreamMessage { + fn convert_web_ref_to_content(self) -> Self { + match self { + StreamMessage::WebReference(refs) => StreamMessage::Content(refs.to_markdown()), + other => other, + } + } +} + +pub struct StreamDecoder { + buffer: Vec, + first_result: Option>, + first_result_ready: bool, + first_result_taken: bool, +} + +impl StreamDecoder { + pub fn new() -> Self { + Self { + buffer: Vec::new(), + first_result: None, + first_result_ready: false, + first_result_taken: false, + } + } + + pub fn take_first_result(&mut self) -> Option> { + if !self.buffer.is_empty() { + return None; + } + if self.first_result.is_some() { + self.first_result_taken = true; + } + self.first_result.take() + } + + #[cfg(test)] + fn is_incomplete(&self) -> bool { + !self.buffer.is_empty() + } + + pub fn is_first_result_ready(&self) -> bool { + self.first_result_ready + } + + pub fn decode(&mut self, data: &[u8], convert_web_ref: bool) -> Result, StreamError> { + self.buffer.extend_from_slice(data); + + if self.buffer.len() < 5 { + if self.buffer.len() == 0 { + return Err(StreamError::EmptyStream); + } + crate::debug_println!("数据长度小于5字节,当前数据: {}", hex::encode(&self.buffer)); + return Err(StreamError::DataLengthLessThan5); + } + + let mut messages = Vec::new(); + let mut offset = 0; + + while offset + 5 <= self.buffer.len() { + let msg_type = self.buffer[offset]; + let msg_len = u32::from_be_bytes([ + self.buffer[offset + 1], + self.buffer[offset + 2], + self.buffer[offset + 3], + self.buffer[offset + 4], + ]) as usize; + + if msg_len == 0 { + offset += 5; + messages.push(StreamMessage::ContentStart); + continue; + } + + if offset + 5 + msg_len > self.buffer.len() { + break; + } + + let msg_data = &self.buffer[offset + 5..offset + 5 + msg_len]; + + match self.process_message(msg_type, msg_data)? { + Some(msg) => { + if convert_web_ref { + messages.push(msg.convert_web_ref_to_content()); + } else { + messages.push(msg); + } + } + _ => {} + } + + offset += 5 + msg_len; + } + + self.buffer.drain(..offset); + + if !self.first_result_taken && !messages.is_empty() { + if self.first_result.is_none() { + self.first_result = Some(messages.clone()); + } else if !self.first_result_ready { + self.first_result.as_mut().unwrap().extend(messages.clone()); + } + } + if !self.first_result_ready { + self.first_result_ready = self.first_result.is_some() && self.buffer.is_empty() && !self.first_result_taken; + } + Ok(messages) + } + + fn process_message( + &self, + msg_type: u8, + msg_data: &[u8], + ) -> Result, StreamError> { + match msg_type { + 0 => self.handle_text_message(msg_data), + 1 => self.handle_gzip_message(msg_data), + 2 => self.handle_json_message(msg_data), + 3 => self.handle_gzip_json_message(msg_data), + t => { + eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t); + crate::debug_println!("消息类型: {},消息内容: {}", t, hex::encode(msg_data)); + Ok(None) + } + } + } + + fn handle_text_message(&self, msg_data: &[u8]) -> Result, StreamError> { + if let Ok(response) = StreamChatResponse::decode(msg_data) { + // crate::debug_println!("[text] StreamChatResponse [hex: {}]: {:?}", hex::encode(msg_data), response); + if !response.text.is_empty() { + Ok(Some(StreamMessage::Content(response.text))) + } else if let Some(filled_prompt) = response.filled_prompt { + Ok(Some(StreamMessage::Debug(filled_prompt))) + } else if let Some(web_citation) = response.web_citation { + let mut refs = BTreeMap::new(); + for reference in web_citation.references { + refs.insert(reference.url, reference.title); + } + Ok(Some(StreamMessage::WebReference(refs))) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + fn handle_gzip_message(&self, msg_data: &[u8]) -> Result, StreamError> { + if let Some(text) = decompress_gzip(msg_data) { + if let Ok(response) = StreamChatResponse::decode(&text[..]) { + // crate::debug_println!("[gzip] StreamChatResponse [hex: {}]: {:?}", hex::encode(msg_data), response); + if !response.text.is_empty() { + Ok(Some(StreamMessage::Content(response.text))) + } else if let Some(filled_prompt) = response.filled_prompt { + Ok(Some(StreamMessage::Debug(filled_prompt))) + } else if let Some(web_citation) = response.web_citation { + let mut refs = BTreeMap::new(); + for reference in web_citation.references { + refs.insert(reference.url, reference.title); + } + Ok(Some(StreamMessage::WebReference(refs))) + } else { + Ok(None) + } + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + fn handle_json_message(&self, msg_data: &[u8]) -> Result, StreamError> { + if msg_data.len() == 2 { + return Ok(Some(StreamMessage::StreamEnd)); + } + if let Ok(text) = String::from_utf8(msg_data.to_vec()) { + // println!("JSON消息: {}", text); + if let Ok(error) = serde_json::from_str::(&text) { + return Err(StreamError::ChatError(error)); + } + } + Ok(None) + } + + fn handle_gzip_json_message( + &self, + msg_data: &[u8], + ) -> Result, StreamError> { + if let Some(text) = decompress_gzip(msg_data) { + if text.len() == 2 { + return Ok(Some(StreamMessage::StreamEnd)); + } + if let Ok(text) = String::from_utf8(text) { + // println!("JSON消息: {}", text); + if let Ok(error) = serde_json::from_str::(&text) { + return Err(StreamError::ChatError(error)); + } + } + } + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_single_chunk() { + // 使用include_str!加载测试数据文件 + let stream_data = include_str!("../../../tests/data/stream_data.txt"); + + // 将整个字符串按每两个字符分割成字节 + let bytes: Vec = stream_data + .as_bytes() + .chunks(2) + .map(|chunk| { + let hex_str = std::str::from_utf8(chunk).unwrap(); + u8::from_str_radix(hex_str, 16).unwrap() + }) + .collect(); + + // 创建解码器 + let mut decoder = StreamDecoder::new(); + + match decoder.decode(&bytes, false) { + Ok(messages) => { + for message in messages { + match message { + StreamMessage::StreamEnd => { + println!("流结束"); + break; + } + StreamMessage::Content(msg) => { + println!("消息内容: {}", msg); + } + StreamMessage::WebReference(refs) => { + println!("网页引用:"); + for (i, (url, title)) in refs.iter().enumerate() { + println!("{}. {} - {}", i, url, title); + } + } + StreamMessage::Debug(prompt) => { + println!("调试信息: {}", prompt); + } + StreamMessage::ContentStart => { + println!("流开始"); + } + } + } + } + Err(e) => { + println!("解析错误: {}", e); + } + } + if decoder.is_incomplete() { + println!("数据不完整"); + } + } + + #[test] + fn test_multiple_chunks() { + // 使用include_str!加载测试数据文件 + let stream_data = include_str!("../../../tests/data/stream_data.txt"); + + // 将整个字符串按每两个字符分割成字节 + let bytes: Vec = stream_data + .as_bytes() + .chunks(2) + .map(|chunk| { + let hex_str = std::str::from_utf8(chunk).unwrap(); + u8::from_str_radix(hex_str, 16).unwrap() + }) + .collect(); + + // 创建解码器 + let mut decoder = StreamDecoder::new(); + + // 辅助函数:找到下一个消息边界 + fn find_next_message_boundary(bytes: &[u8]) -> usize { + if bytes.len() < 5 { + return bytes.len(); + } + let msg_len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize; + 5 + msg_len + } + + // 辅助函数:将字节转换为hex字符串 + fn bytes_to_hex(bytes: &[u8]) -> String { + bytes + .iter() + .map(|b| format!("{:02X}", b)) + .collect::>() + .join("") + } + + // 多次解析数据 + let mut offset = 0; + let mut should_break = false; + + while offset < bytes.len() { + let remaining_bytes = &bytes[offset..]; + let msg_boundary = find_next_message_boundary(remaining_bytes); + let current_msg_bytes = &remaining_bytes[..msg_boundary]; + let hex_str = bytes_to_hex(current_msg_bytes); + + match decoder.decode(current_msg_bytes, false) { + Ok(messages) => { + for message in messages { + match message { + StreamMessage::StreamEnd => { + println!("流结束 [hex: {}]", hex_str); + should_break = true; + break; + } + StreamMessage::Content(msg) => { + println!("消息内容 [hex: {}]: {}", hex_str, msg); + } + StreamMessage::WebReference(refs) => { + println!("网页引用 [hex: {}]:", hex_str); + for (i, (url, title)) in refs.iter().enumerate() { + println!("{}. {} - {}", i, url, title); + } + } + StreamMessage::Debug(prompt) => { + println!("调试信息 [hex: {}]: {}", hex_str, prompt); + } + StreamMessage::ContentStart => { + println!("流开始 [hex: {}]", hex_str); + } + } + } + if should_break { + break; + } + if decoder.is_incomplete() { + println!("数据不完整 [hex: {}]", hex_str); + break; + } + offset += msg_boundary; + } + Err(e) => { + println!("解析错误 [hex: {}]: {}", hex_str, e); + break; + } + } + } + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1c74dac8c6a6f2976394fe2320335a7e8601ca5 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,3 @@ +pub mod model; +pub mod utils; +pub mod client; diff --git a/src/common/client.rs b/src/common/client.rs new file mode 100644 index 0000000000000000000000000000000000000000..202888578655133c132fe7e3781b324b3d80890b --- /dev/null +++ b/src/common/client.rs @@ -0,0 +1,246 @@ +use super::utils::generate_hash; +use crate::{app::{ + constant::{ + CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL, + HEADER_NAME_GHOST_MODE, TRUE, + }, + lazy::{ + CURSOR_API2_CHAT_URL, CURSOR_API2_CHAT_WEB_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, REVERSE_PROXY_HOST, USE_REVERSE_PROXY + }, +}, AppConfig}; +use reqwest::header::{ + ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, + DNT, HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT, + }; +use reqwest::{Client, RequestBuilder}; +use std::sync::LazyLock; +use uuid::Uuid; + +macro_rules! def_const { + ($name:ident, $value:expr) => { + const $name: &'static str = $value; + }; +} + +def_const!(SEC_FETCH_DEST, "sec-fetch-dest"); +def_const!(SEC_FETCH_MODE, "sec-fetch-mode"); +def_const!(SEC_FETCH_SITE, "sec-fetch-site"); +def_const!(SEC_GPC, "sec-gpc"); +def_const!(PRIORITY, "priority"); + +def_const!(ONE, "1"); +def_const!(ENCODINGS, "gzip,br"); +def_const!(VALUE_ACCEPT, "*/*"); +def_const!(VALUE_LANGUAGE, "zh-CN"); +def_const!(EMPTY, "empty"); +def_const!(CORS, "cors"); +def_const!(NO_CACHE, "no-cache"); +def_const!(UA_WIN, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"); +def_const!(SAME_ORIGIN, "same-origin"); +def_const!(KEEP_ALIVE, "keep-alive"); +def_const!(TRAILERS, "trailers"); +def_const!(U_EQ_4, "u=4"); + +def_const!(PROXY_HOST, "x-co"); + +pub(crate) static HTTP_CLIENT: LazyLock> = + LazyLock::new(|| parking_lot::RwLock::new(AppConfig::get_proxies().get_client())); + +/// 重新构建 HTTP 客户端 +/// +/// 当需要更新代理设置时,可以调用此方法重新创建客户端 +pub fn rebuild_http_client() { + let new_client = AppConfig::get_proxies().get_client(); + let mut client = HTTP_CLIENT.write(); + *client = new_client; +} + +/// 返回预构建的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `auth_token` - 授权令牌 +/// * `checksum` - 校验和 +/// * `endpoint` - API 端点路径 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_client(auth_token: &str, checksum: &str, is_search: bool) -> RequestBuilder { + let trace_id = Uuid::new_v4().to_string(); + let url = if is_search { + &*CURSOR_API2_CHAT_WEB_URL + } else { + &*CURSOR_API2_CHAT_URL + }; + + let client = if *USE_REVERSE_PROXY { + HTTP_CLIENT + .read() + .post(url) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_API2_HOST) + } else { + HTTP_CLIENT + .read() + .post(url) + .header(HOST, CURSOR_API2_HOST) + }; + + client + .header(CONTENT_TYPE, CONTENT_TYPE_CONNECT_PROTO) + .bearer_auth(auth_token) + .header("connect-accept-encoding", ENCODINGS) + .header("connect-protocol-version", ONE) + .header(USER_AGENT, "connect-es/1.6.1") + .header("x-amzn-trace-id", format!("Root={}", trace_id)) + .header("x-client-key", generate_hash()) + .header("x-cursor-checksum", checksum) + .header("x-cursor-client-version", "0.42.5") + .header("x-cursor-timezone", "Asia/Shanghai") + .header(HEADER_NAME_GHOST_MODE, TRUE) + .header("x-request-id", trace_id) + .header(CONNECTION, KEEP_ALIVE) + .header(TRANSFER_ENCODING, "chunked") +} + +/// 返回预构建的获取 Stripe 账户信息的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `auth_token` - 授权令牌 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_profile_client(auth_token: &str) -> RequestBuilder { + let client = if *USE_REVERSE_PROXY { + HTTP_CLIENT + .read() + .get(&*CURSOR_API2_STRIPE_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_API2_HOST) + } else { + HTTP_CLIENT + .read() + .get(&*CURSOR_API2_STRIPE_URL) + .header(HOST, CURSOR_API2_HOST) + }; + + client + .header("sec-ch-ua", "\"Not-A.Brand\";v=\"99\", \"Chromium\";v=\"124\"") + .header(HEADER_NAME_GHOST_MODE, TRUE) + .header("sec-ch-ua-mobile", "?0") + .bearer_auth(auth_token) + .header( + USER_AGENT, + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.42.5 Chrome/124.0.6367.243 Electron/30.4.0 Safari/537.36", + ) + .header("sec-ch-ua-platform", "\"Windows\"") + .header(ACCEPT, VALUE_ACCEPT) + .header(ORIGIN, "vscode-file://vscode-app") + .header(SEC_FETCH_SITE, "cross-site") + .header(SEC_FETCH_MODE, CORS) + .header(SEC_FETCH_DEST, EMPTY) + .header(ACCEPT_ENCODING, ENCODINGS) + .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) + .header(PRIORITY, "u=1, i") +} + +/// 返回预构建的获取使用情况的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `user_id` - 用户 ID +/// * `auth_token` - 授权令牌 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_usage_client(user_id: &str, auth_token: &str) -> RequestBuilder { + let session_token = format!("{}%3A%3A{}", user_id, auth_token); + + let client = if *USE_REVERSE_PROXY { + HTTP_CLIENT + .read() + .get(&*CURSOR_USAGE_API_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_HOST) + } else { + HTTP_CLIENT + .read() + .get(&*CURSOR_USAGE_API_URL) + .header(HOST, CURSOR_HOST) + }; + + client + .header(USER_AGENT, UA_WIN) + .header(ACCEPT, VALUE_ACCEPT) + .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) + .header(ACCEPT_ENCODING, ENCODINGS) + .header(REFERER, CURSOR_SETTINGS_URL) + .header(DNT, ONE) + .header(SEC_GPC, ONE) + .header(SEC_FETCH_DEST, EMPTY) + .header(SEC_FETCH_MODE, CORS) + .header(SEC_FETCH_SITE, SAME_ORIGIN) + .header(CONNECTION, KEEP_ALIVE) + .header(PRAGMA, NO_CACHE) + .header(CACHE_CONTROL, NO_CACHE) + .header(TE, TRAILERS) + .header(PRIORITY, U_EQ_4) + .header( + COOKIE, + &format!("WorkosCursorSessionToken={}", session_token), + ) + .query(&[("user", user_id)]) +} + +/// 返回预构建的获取用户信息的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `user_id` - 用户 ID +/// * `auth_token` - 授权令牌 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_userinfo_client(user_id: &str, auth_token: &str) -> RequestBuilder { + let session_token = format!("{}%3A%3A{}", user_id, auth_token); + + let client = if *USE_REVERSE_PROXY { + HTTP_CLIENT + .read() + .get(&*CURSOR_USER_API_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_HOST) + } else { + HTTP_CLIENT + .read() + .get(&*CURSOR_USER_API_URL) + .header(HOST, CURSOR_HOST) + }; + + client + .header(USER_AGENT, UA_WIN) + .header(ACCEPT, VALUE_ACCEPT) + .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) + .header(ACCEPT_ENCODING, ENCODINGS) + .header(REFERER, CURSOR_SETTINGS_URL) + .header(DNT, ONE) + .header(SEC_GPC, ONE) + .header(SEC_FETCH_DEST, EMPTY) + .header(SEC_FETCH_MODE, CORS) + .header(SEC_FETCH_SITE, SAME_ORIGIN) + .header(CONNECTION, KEEP_ALIVE) + .header(PRAGMA, NO_CACHE) + .header(CACHE_CONTROL, NO_CACHE) + .header(TE, TRAILERS) + .header(PRIORITY, U_EQ_4) + .header( + COOKIE, + &format!("WorkosCursorSessionToken={}", session_token), + ) + .query(&[("user", user_id)]) +} diff --git a/src/common/model.rs b/src/common/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..2662a2825f9392d239295eb01da9428da4459882 --- /dev/null +++ b/src/common/model.rs @@ -0,0 +1,72 @@ +pub mod error; +pub mod health; +pub mod config; +pub mod token; +pub mod userinfo; + +use config::ConfigData; + +use serde::Serialize; + +#[derive(Serialize)] +pub enum ApiStatus { + #[serde(rename = "healthy")] + Healthy, + #[serde(rename = "success")] + Success, + #[serde(rename = "error")] + Error, + #[serde(rename = "failed")] + Failed, +} + +// #[derive(Serialize)] +// #[serde(untagged)] +// pub enum ApiResponse { +// HealthCheck(HealthCheckResponse), +// ConfigData(NormalResponse), +// Error(ErrorResponse), +// } + +// impl ApiResponse { +// pub fn to_string(&self) -> String { +// serde_json::to_string(self).unwrap() +// } +// } + +#[derive(Serialize)] +pub struct NormalResponse { + pub status: ApiStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl std::fmt::Display for NormalResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap()) + } +} + +// #[derive(Serialize)] +// pub struct NormalResponseNoData { +// pub status: ApiStatus, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub message: Option, +// } + +#[derive(Serialize)] +pub struct ErrorResponse { + // status -> 成功 / 失败 + pub status: ApiStatus, + // HTTP 请求的状态码 + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + // HTTP 请求的错误码 + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + // 错误详情 + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} diff --git a/src/common/model/config.rs b/src/common/model/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..2112f359bd88a59c813622f8073bae085f0159b9 --- /dev/null +++ b/src/common/model/config.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies}; + +#[derive(Serialize)] +pub struct ConfigData { + pub page_content: Option, + pub vision_ability: VisionAbility, + pub enable_slow_pool: bool, + pub enable_all_claude: bool, + pub usage_check_models: UsageCheck, + pub enable_dynamic_key: bool, + #[serde(skip_serializing_if = "String::is_empty")] + pub share_token: String, + pub proxies: Proxies, + pub include_web_references: bool, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct ConfigUpdateRequest { + pub action: String, // "get", "update", "reset" + pub path: String, + pub content: Option, // "default", "text", "html" + pub vision_ability: Option, + pub enable_slow_pool: Option, + pub enable_all_claude: Option, + pub usage_check_models: Option, + pub enable_dynamic_key: Option, + pub share_token: Option, + pub proxies: Option, + pub include_web_references: Option, +} diff --git a/src/common/model/error.rs b/src/common/model/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..79430db915c5c41deabcde06915d263cd1e7a3cd --- /dev/null +++ b/src/common/model/error.rs @@ -0,0 +1,34 @@ +use super::ErrorResponse; + +pub enum ChatError { + ModelNotSupported(String), + EmptyMessages, + NoTokens, + RequestFailed(String), + Unauthorized, +} + +impl ChatError { + pub fn to_json(&self) -> ErrorResponse { + let (error, message) = match self { + ChatError::ModelNotSupported(model) => ( + "model_not_supported", + format!("Model '{}' is not supported", model), + ), + ChatError::EmptyMessages => ( + "empty_messages", + "Message array cannot be empty".to_string(), + ), + ChatError::NoTokens => ("no_tokens", "No available tokens".to_string()), + ChatError::RequestFailed(err) => ("request_failed", format!("Request failed: {}", err)), + ChatError::Unauthorized => ("unauthorized", "Invalid authorization token".to_string()), + }; + + ErrorResponse { + status: super::ApiStatus::Error, + code: None, + error: Some(error.to_string()), + message: Some(message), + } + } +} diff --git a/src/common/model/health.rs b/src/common/model/health.rs new file mode 100644 index 0000000000000000000000000000000000000000..f74fe090c63846a02f06b1610c0d6d8066b9791e --- /dev/null +++ b/src/common/model/health.rs @@ -0,0 +1,38 @@ +use serde::Serialize; + +use super::ApiStatus; + +#[derive(Serialize)] +pub struct HealthCheckResponse { + pub status: ApiStatus, + pub version: &'static str, + pub uptime: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub stats: Option, + pub models: Vec<&'static str>, + pub endpoints: Vec<&'static str>, +} + +#[derive(Serialize)] +pub struct SystemStats { + pub started: String, + pub total_requests: u64, + pub active_requests: u64, + pub system: SystemInfo, +} + +#[derive(Serialize)] +pub struct SystemInfo { + pub memory: MemoryInfo, + pub cpu: CpuInfo, +} + +#[derive(Serialize)] +pub struct MemoryInfo { + pub rss: u64, // 物理内存使用量(字节) +} + +#[derive(Serialize)] +pub struct CpuInfo { + pub usage: f32, // CPU 使用率(百分比) +} diff --git a/src/common/model/token.rs b/src/common/model/token.rs new file mode 100644 index 0000000000000000000000000000000000000000..14242f3a538f080b28a7aab5c3f25f00faf5fddd --- /dev/null +++ b/src/common/model/token.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct TokenPayload { + pub sub: String, + pub time: String, + pub randomness: String, + pub exp: i64, + pub iss: String, + pub scope: String, + pub aud: String, +} diff --git a/src/common/model/userinfo.rs b/src/common/model/userinfo.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a1466b905f5da1a953f4b8767abf3e28e22cb4a --- /dev/null +++ b/src/common/model/userinfo.rs @@ -0,0 +1,89 @@ +use chrono::{DateTime, Local}; +use serde::{Deserialize, Serialize}; +use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; + +#[derive(Serialize)] +#[serde(untagged)] +pub enum GetUserInfo { + Usage(TokenProfile), + Error { error: String }, +} + +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct TokenProfile { + pub usage: UsageProfile, + pub user: UserProfile, + pub stripe: StripeProfile, +} + +#[derive(Deserialize, Serialize, PartialEq, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub enum MembershipType { + #[serde(rename = "free")] + Free, + #[serde(rename = "free_trial")] + FreeTrial, + #[serde(rename = "pro")] + Pro, + #[serde(rename = "enterprise")] + Enterprise, +} + +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct StripeProfile { + #[serde(rename(deserialize = "membershipType"))] + pub membership_type: MembershipType, + #[serde( + rename(deserialize = "paymentId"), + default, + skip_serializing_if = "Option::is_none" + )] + pub payment_id: Option, + #[serde(rename(deserialize = "daysRemainingOnTrial"))] + pub days_remaining_on_trial: u32, +} + +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct ModelUsage { + #[serde(rename(deserialize = "numRequests", serialize = "requests"))] + pub num_requests: u32, + #[serde( + rename(deserialize = "numRequestsTotal"), + default, + skip_serializing_if = "Option::is_none" + )] + pub requests_total: Option, + #[serde(rename(deserialize = "numTokens", serialize = "tokens"))] + pub num_tokens: u32, + #[serde( + rename(deserialize = "maxRequestUsage"), + skip_serializing_if = "Option::is_none" + )] + pub max_requests: Option, + #[serde( + rename(deserialize = "maxTokenUsage"), + skip_serializing_if = "Option::is_none" + )] + pub max_tokens: Option, +} + +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct UsageProfile { + #[serde(rename(deserialize = "gpt-4"))] + pub premium: ModelUsage, + #[serde(rename(deserialize = "gpt-3.5-turbo"))] + pub standard: ModelUsage, + #[serde(rename(deserialize = "gpt-4-32k"))] + pub unknown: ModelUsage, +} + +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub struct UserProfile { + pub email: String, + // pub email_verified: bool, + pub name: String, + #[serde(rename(serialize = "id"))] + pub sub: String, + pub updated_at: DateTime, + // Image link, rendered in /logs? + // pub picture: Option, +} diff --git a/src/common/utils.rs b/src/common/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..1adc30dcb50ba8b842937ee3a59b12740b06fa36 --- /dev/null +++ b/src/common/utils.rs @@ -0,0 +1,281 @@ +mod checksum; +use ::base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +pub use checksum::*; +mod token; +pub use token::*; +mod base64; +pub use base64::*; + +use super::model::{token::TokenPayload, userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}}; +use crate::app::{ + constant::{COMMA, FALSE, TRUE}, + lazy::{TOKEN_DELIMITER, USE_COMMA_DELIMITER}, +}; + +pub fn parse_bool_from_env(key: &str, default: bool) -> bool { + std::env::var(key) + .ok() + .map(|v| match v.to_lowercase().as_str() { + TRUE | "1" => true, + FALSE | "0" => false, + _ => default, + }) + .unwrap_or(default) +} + +pub fn parse_string_from_env(key: &str, default: &str) -> String { + std::env::var(key).unwrap_or_else(|_| default.to_string()) +} + +pub fn parse_ascii_char_from_env(key: &str, default: char) -> char { + std::env::var(key) + .ok() + .and_then(|v| { + let chars: Vec = v.chars().collect(); + if chars.len() == 1 && chars[0].is_ascii() { + Some(chars[0]) + } else { + None + } + }) + .unwrap_or(default) +} + +pub fn parse_usize_from_env(key: &str, default: usize) -> usize { + std::env::var(key) + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(default) +} + +pub trait TrimNewlines { + fn trim_leading_newlines(self) -> Self; +} + +impl TrimNewlines for String { + #[inline(always)] + fn trim_leading_newlines(mut self) -> Self { + let bytes = self.as_bytes(); + if bytes.len() >= 2 && bytes[0] == b'\n' && bytes[1] == b'\n' { + unsafe { + let start_ptr = self.as_mut_ptr(); + let new_len = self.len() - 2; + std::ptr::copy(start_ptr.add(2), start_ptr, new_len); + self.as_mut_vec().set_len(new_len); + } + } + self + } +} + +pub async fn get_token_profile(auth_token: &str) -> Option { + let user_id = extract_user_id(auth_token)?; + + // 构建请求客户端 + let client = super::client::build_usage_client(&user_id, auth_token); + + // 发送请求并获取响应 + // let response = client.send().await.ok()?; + // let bytes = response.bytes().await?; + // println!("Raw response bytes: {:?}", bytes); + // let usage = serde_json::from_str::(&text).ok()?; + let usage = client + .send() + .await + .ok()? + .json::() + .await + .ok()?; + + let user = get_user_profile(auth_token).await?; + + // 从 Stripe 获取用户资料 + let stripe = get_stripe_profile(auth_token).await?; + + // 映射响应数据到 TokenProfile + Some(TokenProfile { + usage, + user, + stripe, + }) +} + +pub async fn get_stripe_profile(auth_token: &str) -> Option { + let client = super::client::build_profile_client(auth_token); + let response = client + .send() + .await + .ok()? + .json::() + .await + .ok()?; + Some(response) +} + +pub async fn get_user_profile(auth_token: &str) -> Option { + let user_id = extract_user_id(auth_token)?; + + // 构建请求客户端 + let client = super::client::build_userinfo_client(&user_id, auth_token); + + // 发送请求并获取响应 + let user_profile = client.send().await.ok()?.json::().await.ok()?; + + Some(user_profile) +} + +pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> { + // 尝试使用自定义分隔符查找 + let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER); + + // 如果自定义分隔符未找到,并且 USE_COMMA_DELIMITER 为 true,则尝试使用逗号 + if delimiter_pos.is_none() && *USE_COMMA_DELIMITER { + delimiter_pos = auth_token.rfind(COMMA); + } + + // 如果最终都没有找到分隔符,则返回 None + let comma_pos = delimiter_pos?; + + // 使用找到的分隔符位置分割字符串 + let (token_part, checksum) = auth_token.split_at(comma_pos); + let checksum = &checksum[1..]; // 跳过逗号 + + // 解析 token - 为了向前兼容,忽略最后一个:或%3A前的内容 + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + let token = match (colon_pos, encoded_colon_pos) { + (None, None) => token_part, // 最简单的构成: token,checksum + (Some(pos1), None) => &token_part[(pos1 + 1)..], + (None, Some(pos2)) => &token_part[(pos2 + 3)..], + (Some(pos1), Some(pos2)) => { + let pos = pos1.max(pos2); + let start = if pos == pos2 { pos + 3 } else { pos + 1 }; + &token_part[start..] + } + }; + + // 验证 token 和 checksum 有效性 + if validate_token(token) && validate_checksum(checksum) { + Some((token.to_string(), generate_checksum_with_repair(checksum))) + } else { + None + } +} + +pub fn extract_token(auth_token: &str) -> Option { + // 尝试使用自定义分隔符查找 + let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER); + + // 如果自定义分隔符未找到,并且 USE_COMMA_DELIMITER 为 true,则尝试使用逗号 + if delimiter_pos.is_none() && *USE_COMMA_DELIMITER { + delimiter_pos = auth_token.rfind(COMMA); + } + + // 根据是否找到分隔符来确定 token_part + let token_part = match delimiter_pos { + Some(pos) => &auth_token[..pos], + None => auth_token, + }; + + // 向前兼容 + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + let token = match (colon_pos, encoded_colon_pos) { + (None, None) => token_part, + (Some(pos1), None) => &token_part[(pos1 + 1)..], + (None, Some(pos2)) => &token_part[(pos2 + 3)..], + (Some(pos1), Some(pos2)) => { + let pos = pos1.max(pos2); + let start = if pos == pos2 { pos + 3 } else { pos + 1 }; + &token_part[start..] + } + }; + + // 验证 token 有效性 + if validate_token(token) { + Some(token.to_string()) + } else { + None + } +} + +pub fn format_time_ms(seconds: f64) -> f64 { + (seconds * 1000.0).round() / 1000.0 +} + +use crate::chat::config::key_config; + +/// 将 JWT token 转换为 TokenInfo +pub fn token_to_tokeninfo(auth_token: &str) -> Option { + let (token, checksum) = validate_token_and_checksum(auth_token)?; + + // JWT token 由3部分组成,用 . 分隔 + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return None; + } + + // 解码 payload (第二部分) + let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { + Ok(decoded) => decoded, + Err(_) => return None, + }; + + // 将 payload 转换为字符串 + let payload_str = match String::from_utf8(payload) { + Ok(s) => s, + Err(_) => return None, + }; + + // 解析为 TokenPayload + let payload: TokenPayload = match serde_json::from_str(&payload_str) { + Ok(p) => p, + Err(_) => return None, + }; + + let (machine_id_hash, mac_id_hash) = extract_hashes(&checksum)?; + + // 构建 TokenInfo + Some(key_config::TokenInfo { + sub: payload.sub, + exp: payload.exp, + randomness: payload.randomness, + signature: parts[2].to_string(), + machine_id: machine_id_hash, + mac_id: mac_id_hash, + }) +} + +/// 将 TokenInfo 转换为 JWT token +pub fn tokeninfo_to_token(info: &key_config::TokenInfo) -> Option<(String, String)> { + // 构建 payload + let payload = TokenPayload { + sub: info.sub.clone(), + exp: info.exp, + randomness: info.randomness.clone(), + time: (info.exp - 2592000000).to_string(), // exp - 30000天 + iss: ISSUER.to_string(), + scope: SCOPE.to_string(), + aud: AUDIENCE.to_string(), + }; + + let payload_str = match serde_json::to_string(&payload) { + Ok(s) => s, + Err(_) => return None, + }; + + let payload_b64 = URL_SAFE_NO_PAD.encode(payload_str.as_bytes()); + + // 从 TokenInfo 中获取 machine_id 和 mac_id 的 hex 字符串 + let device_id = hex::encode(&info.machine_id); + let mac_addr = if !info.mac_id.is_empty() { + Some(hex::encode(&info.mac_id)) + } else { + None + }; + + // 组合 token + Some((format!("{}.{}.{}", HEADER_B64, payload_b64, info.signature), generate_checksum(&device_id, mac_addr.as_deref()))) +} diff --git a/src/common/utils/base64.rs b/src/common/utils/base64.rs new file mode 100644 index 0000000000000000000000000000000000000000..59bfe9ad2169728e84e0ec8f1a06ae1547d7b40f --- /dev/null +++ b/src/common/utils/base64.rs @@ -0,0 +1,148 @@ +// Base64 字符集 (a-z, A-Z, 0-9, -, _) +const BASE64_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + +// 预计算的 Base64 查找表,用于快速解码 +const BASE64_LOOKUP: [i8; 256] = { + let mut lookup = [-1i8; 256]; + let mut i = 0; + while i < BASE64_CHARS.len() { + lookup[BASE64_CHARS[i] as usize] = i as i8; + i += 1; + } + lookup +}; + +/// 将字节切片编码为 Base64 字符串。 +/// +/// # Arguments +/// +/// * `bytes`: 要编码的字节切片 +/// +/// # Returns +/// +/// 编码后的 Base64 字符串 +pub fn to_base64(bytes: &[u8]) -> String { + // 预分配足够容量,避免多次分配内存 + let capacity = (bytes.len() + 2) / 3 * 4; + let mut result = Vec::with_capacity(capacity); + + // 每三个字节为一组进行处理 + for chunk in bytes.chunks(3) { + // 将三个字节合并为一个 u32 + let b1 = chunk[0] as u32; + let b2 = chunk.get(1).map_or(0, |&b| b as u32); + let b3 = chunk.get(2).map_or(0, |&b| b as u32); + + let n = (b1 << 16) | (b2 << 8) | b3; + + // 将 u32 拆分成四个 6 位的值,并根据查找表转换为 Base64 字符 + result.push(BASE64_CHARS[(n >> 18) as usize]); + result.push(BASE64_CHARS[((n >> 12) & 0x3F) as usize]); + + // 如果 chunk 长度大于 1,则需要处理第二个字符 + if chunk.len() > 1 { + result.push(BASE64_CHARS[((n >> 6) & 0x3F) as usize]); + // 如果 chunk 长度大于 2,则需要处理第三个字符 + if chunk.len() > 2 { + result.push(BASE64_CHARS[(n & 0x3F) as usize]); + } + } + } + + // 使用 from_utf8_unchecked 提高性能,因为 BASE64_CHARS 都是有效的 ASCII 字符 + unsafe { String::from_utf8_unchecked(result) } +} + +/// 将 Base64 字符串解码为字节数组。 +/// +/// # Arguments +/// +/// * `input`: 要解码的 Base64 字符串 +/// +/// # Returns +/// +/// 如果解码成功,返回 Some(解码后的字节数组);如果输入无效,返回 None +pub fn from_base64(input: &str) -> Option> { + let input = input.as_bytes(); + + // 检查输入长度,Base64 编码的长度必须是 4 的倍数或余 2/3 + if input.is_empty() || input.len() % 4 == 1 { + return None; + } + + // 检查是否包含无效字符,无效字符直接返回None + if input.iter().any(|&b| BASE64_LOOKUP[b as usize] == -1) { + return None; + } + + // 预分配足够容量,避免多次分配内存 + let capacity = input.len() / 4 * 3; + let mut result = Vec::with_capacity(capacity); + + // 每四个字符为一组进行处理 + let mut chunks = input.chunks_exact(4); + for chunk in &mut chunks { + // 使用查找表将 Base64 字符转换为 6 位的值 + let n1 = BASE64_LOOKUP[chunk[0] as usize] as u32; + let n2 = BASE64_LOOKUP[chunk[1] as usize] as u32; + let n3 = BASE64_LOOKUP[chunk[2] as usize] as u32; + let n4 = BASE64_LOOKUP[chunk[3] as usize] as u32; + + // 将四个 6 位的值合并为一个 u32,并拆分成三个字节 + let n = (n1 << 18) | (n2 << 12) | (n3 << 6) | n4; + result.push((n >> 16) as u8); + result.push(((n >> 8) & 0xFF) as u8); + result.push((n & 0xFF) as u8); + } + + // 处理剩余的字符 + let remainder = chunks.remainder(); + if !remainder.is_empty() { + let n1 = BASE64_LOOKUP[remainder[0] as usize] as u32; + let n2 = BASE64_LOOKUP[remainder[1] as usize] as u32; + + let mut n = (n1 << 18) | (n2 << 12); + result.push((n >> 16) as u8); + + // 如果剩余字符长度大于 2,则需要处理第二个字节 + if remainder.len() > 2 { + let n3 = BASE64_LOOKUP[remainder[2] as usize] as u32; + n |= n3 << 6; + result.push(((n >> 8) & 0xFF) as u8); + } + } + + Some(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base64_roundtrip() { + let test_cases = vec![ + vec![0u8, 1, 2, 3], + vec![255u8, 254, 253], + vec![0u8], + vec![0u8, 1], + vec![0u8, 1, 2], + vec![255u8; 1000], + ]; + + for case in test_cases { + let encoded = to_base64(&case); + let decoded = from_base64(&encoded).unwrap(); + assert_eq!(case, decoded); + } + } + + #[test] + fn test_invalid_input() { + assert_eq!(from_base64(""), None); // 空字符串 + assert_eq!(from_base64("a"), None); // 长度为 1 + assert_eq!(from_base64("!@#$"), None); // 无效字符 + assert_eq!(from_base64("YWJj!"), None); // 包含无效字符 + assert!(from_base64("YWJj").is_some()); // 有效输入 + } +} diff --git a/src/common/utils/checksum.rs b/src/common/utils/checksum.rs new file mode 100644 index 0000000000000000000000000000000000000000..1616c2016d22abf3c29ee976d6ba3d308913ecfd --- /dev/null +++ b/src/common/utils/checksum.rs @@ -0,0 +1,217 @@ +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use rand::Rng; +use sha2::{Digest, Sha256}; + +pub fn generate_hash() -> String { + let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); + let mut hasher = Sha256::new(); + hasher.update(random_bytes); + hex::encode(hasher.finalize()) +} + +fn obfuscate_bytes(bytes: &mut [u8]) { + let mut prev: u8 = 165; + for (idx, byte) in bytes.iter_mut().enumerate() { + let old_value = *byte; + *byte = (old_value ^ prev).wrapping_add((idx % 256) as u8); + prev = *byte; + } +} + +fn deobfuscate_bytes(bytes: &mut [u8]) { + let mut prev: u8 = 165; + for (idx, byte) in bytes.iter_mut().enumerate() { + let temp = *byte; + *byte = (*byte).wrapping_sub((idx % 256) as u8) ^ prev; + prev = temp; + } +} + +pub fn generate_timestamp_header() -> String { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + let mut timestamp_bytes = vec![ + ((timestamp >> 8) & 0xFF) as u8, + (0xFF & timestamp) as u8, + ((timestamp >> 24) & 0xFF) as u8, + ((timestamp >> 16) & 0xFF) as u8, + ((timestamp >> 8) & 0xFF) as u8, + (0xFF & timestamp) as u8, + ]; + + obfuscate_bytes(&mut timestamp_bytes); + BASE64.encode(×tamp_bytes) +} + +pub fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String { + let encoded = generate_timestamp_header(); + match mac_addr { + Some(mac) => format!("{}{}/{}", encoded, device_id, mac), + None => format!("{}{}", encoded, device_id), + } +} + +pub fn generate_checksum_with_default() -> String { + generate_checksum(&generate_hash(), Some(&generate_hash())) +} + +pub fn generate_checksum_with_repair(checksum: &str) -> String { + let bytes = checksum.as_bytes(); + let len = bytes.len(); + + // 长度快速检查 + if len != 72 && len != 129 && len != 137 { + return generate_checksum_with_default(); + } + + // 单次遍历完成所有字符校验 + for (i, &b) in bytes.iter().enumerate() { + let valid = match (len, i) { + // 通用字符校验(排除非法字符) + (_, _) if !b.is_ascii_alphanumeric() && b != b'/' && b != b'+' && b != b'=' => false, + + // 72字节格式:时间戳(8) + 设备哈希(64) + (72, 8..=71) => b.is_ascii_hexdigit(), + + // 129字节格式:设备哈希(64) + '/' + MAC哈希(64) + (129, 0..=63) => b.is_ascii_hexdigit(), + (129, 64) => b == b'/', + (129, 65..=128) => b.is_ascii_hexdigit(), + + // 137字节格式:时间戳(8) + 设备哈希(64) + '/' + MAC哈希(64) + (137, 8..=71) => b.is_ascii_hexdigit(), + (137, 72) => b == b'/', + (137, 73..=136) => b.is_ascii_hexdigit(), + + // 时间戳部分不需要校验 + (72 | 137, 0..=7) => true, + + _ => unreachable!(), + }; + + if !valid { + return generate_checksum_with_default(); + } + } + + // 校验通过后构造结果 + match len { + 72 => format!( + "{}{}/{}", + generate_timestamp_header(), + unsafe { std::str::from_utf8_unchecked(&bytes[8..]) }, + generate_hash() + ), + 129 => format!( + "{}{}/{}", + generate_timestamp_header(), + unsafe { std::str::from_utf8_unchecked(&bytes[..64]) }, + unsafe { std::str::from_utf8_unchecked(&bytes[65..]) } + ), + 137 => format!( + "{}{}/{}", + generate_timestamp_header(), + unsafe { std::str::from_utf8_unchecked(&bytes[8..72]) }, + unsafe { std::str::from_utf8_unchecked(&bytes[73..]) } + ), + _ => unreachable!(), + } +} + +pub fn extract_time_ks(timestamp_base64: &str) -> Option { + let mut timestamp_bytes = BASE64.decode(timestamp_base64).ok()?; + + if timestamp_bytes.len() != 6 { + return None; + } + + deobfuscate_bytes(&mut timestamp_bytes); + + if timestamp_bytes[0] != timestamp_bytes[4] || timestamp_bytes[1] != timestamp_bytes[5] { + return None; + } + + // 使用后四位还原 timestamp + Some( + ((timestamp_bytes[2] as u64) << 24) + | ((timestamp_bytes[3] as u64) << 16) + | ((timestamp_bytes[4] as u64) << 8) + | (timestamp_bytes[5] as u64), + ) +} + +pub fn validate_checksum(checksum: &str) -> bool { + let bytes = checksum.as_bytes(); + let len = bytes.len(); + + // 长度门控 + if len != 72 && len != 137 { + return false; + } + + // 单次遍历完成所有字符校验 + for (i, &b) in bytes.iter().enumerate() { + let valid = match (len, i) { + // 通用字符校验(排除非法字符) + (_, _) if !b.is_ascii_alphanumeric() && b != b'/' && b != b'+' && b != b'=' => false, + + // 格式校验 + (72, 0..=7) => true, // 时间戳部分(由extract_time_ks验证) + (72, 8..=71) => b.is_ascii_hexdigit(), + + (137, 0..=7) => true, // 时间戳 + (137, 8..=71) => b.is_ascii_hexdigit(), // 设备哈希 + (137, 72) => b == b'/', // 分割符(索引72是第73个字符) + (137, 73..=136) => b.is_ascii_hexdigit(), // MAC哈希 + + _ => unreachable!(), + }; + + if !valid { + return false; + } + } + + // 统一时间戳验证(无需分层) + let time_valid = extract_time_ks(&checksum[..8]).is_some(); + + // 附加MAC哈希长度校验(仅137字符需要) + let mac_hash_valid = if len == 137 { + checksum[73..].len() == 64 // 确保MAC哈希长度为64 + } else { + true // 72字符无需此检查 + }; + + time_valid && mac_hash_valid +} + +/// 从校验通过的checksum中提取哈希值(需先通过validate_checksum验证) +/// 返回 (device_hash, mac_hash) ,mac_hash可能为空Vec +pub fn extract_hashes(checksum: &str) -> Option<(Vec, Vec)> { + // 前置条件:必须通过校验(确保长度和格式正确) + if !validate_checksum(checksum) { + return None; + } + + // 根据长度直接切割,无需字符级验证(validate_checksum已保证) + match checksum.len() { + 72 => { + // 格式:8字节时间戳 + 64字节设备哈希 + let device_hash = hex::decode(&checksum[8..]).ok()?; // 8..72 + Some((device_hash, Vec::new())) + } + 137 => { + // 格式:8时间戳 + 64设备哈希 + '/' + 64MAC哈希 + // 直接按固定位置切割(validate_checksum已确保索引72是'/') + let device_hash = hex::decode(&checksum[8..72]).ok()?; + let mac_hash = hex::decode(&checksum[73..]).ok()?; // 73..137 + Some((device_hash, mac_hash)) + } + // validate_checksum已过滤其他长度,此处应为不可达代码 + _ => unreachable!("Invalid length after validation: {}", checksum.len()), + } +} diff --git a/src/common/utils/token.rs b/src/common/utils/token.rs new file mode 100644 index 0000000000000000000000000000000000000000..65fe25961bde062cf4ffc639e5b5e3beecf583c6 --- /dev/null +++ b/src/common/utils/token.rs @@ -0,0 +1,276 @@ +use super::generate_checksum_with_repair; +use crate::app::{ + constant::{COMMA, EMPTY_STRING}, + lazy::TOKEN_LIST_FILE, + model::TokenInfo, +}; +use crate::common::model::token::TokenPayload; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use chrono::{DateTime, Local, TimeZone}; + +// 规范化文件内容并写入 +fn normalize_and_write(content: &str, file_path: &str) -> String { + let normalized = content.replace("\r\n", "\n"); + if normalized != content { + if let Err(e) = std::fs::write(file_path, &normalized) { + eprintln!("警告: 无法更新规范化的文件: {}", e); + } + } + normalized +} + +// 解析token +pub fn parse_token(token_part: &str) -> String { + // 查找最后一个:或%3A的位置 + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + match (colon_pos, encoded_colon_pos) { + (None, None) => token_part.to_string(), + (Some(pos1), None) => token_part[(pos1 + 1)..].to_string(), + (None, Some(pos2)) => token_part[(pos2 + 3)..].to_string(), + (Some(pos1), Some(pos2)) => { + // 取较大的位置作为分隔点 + let pos = pos1.max(pos2); + let start = if pos == pos2 { pos + 3 } else { pos + 1 }; + token_part[start..].to_string() + } + } +} + +// Token 加载函数 +pub fn load_tokens() -> Vec { + let token_list_file = TOKEN_LIST_FILE.as_str(); + + // 确保文件存在 + if !std::path::Path::new(&token_list_file).exists() { + if let Err(e) = std::fs::write(&token_list_file, EMPTY_STRING) { + eprintln!("警告: 无法创建文件 '{}': {}", &token_list_file, e); + } + } + + // 读取和规范化 token-list 文件 + let token_map: std::collections::HashMap = + match std::fs::read_to_string(&token_list_file) { + Ok(content) => { + let normalized = normalize_and_write(&content, &token_list_file); + normalized + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + return None; + } + + let parts: Vec<&str> = line.split(COMMA).collect(); + match parts[..] { + [token_part, checksum] => { + let token = parse_token(token_part); + Some((token, generate_checksum_with_repair(checksum))) + } + _ => { + eprintln!("警告: 忽略无效的token-list行: {}", line); + None + } + } + }) + .collect() + } + Err(e) => { + eprintln!("警告: 无法读取token-list文件: {}", e); + std::collections::HashMap::new() + } + }; + + // 更新 token-list 文件 + let token_list_content = token_map + .iter() + .map(|(token, checksum)| format!("{},{}", token, checksum)) + .collect::>() + .join("\n"); + + if let Err(e) = std::fs::write(&token_list_file, token_list_content) { + eprintln!("警告: 无法更新token-list文件: {}", e); + } + + // 转换为 TokenInfo vector + token_map + .into_iter() + .map(|(token, checksum)| TokenInfo { + token: token.clone(), + checksum, + profile: None, + }) + .collect() +} + +pub fn write_tokens(token_infos: &[TokenInfo], file_path: &str) -> std::io::Result<()> { + let content = token_infos + .iter() + .map(|info| format!("{},{}", info.token, info.checksum)) + .collect::>() + .join("\n"); + + std::fs::write(file_path, content) +} + +pub(super) const HEADER_B64: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; +pub(super) const ISSUER: &str = "https://authentication.cursor.sh"; +pub(super) const SCOPE: &str = "openid profile email offline_access"; +pub(super) const AUDIENCE: &str = "https://cursor.com"; + +// 验证jwt token是否有效 +pub fn validate_token(token: &str) -> bool { + // 检查 token 格式 + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return false; + } + + if parts[0] != HEADER_B64 { + return false; + } + + // 解码 payload + let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { + Ok(decoded) => decoded, + Err(_) => return false, + }; + + // 转换为字符串 + let payload_str = match String::from_utf8(payload) { + Ok(s) => s, + Err(_) => return false, + }; + + // 解析为 TokenPayload + let payload: TokenPayload = match serde_json::from_str(&payload_str) { + Ok(p) => p, + Err(_) => return false, + }; + + // 验证 time 字段 + if let Ok(time_value) = payload.time.parse::() { + let current_time = chrono::Utc::now().timestamp(); + if time_value > current_time { + return false; + } + } else { + return false; + } + + // 验证 randomness 格式 + let bytes = payload.randomness.as_bytes(); + if bytes.len() != 18 { + return false; + } + + // 单次遍历完成所有字符校验 + for (i, &b) in bytes.iter().enumerate() { + let valid = match i { + // 16进制数字部分 + 0..=7 | 9..=12 | 14..=17 => b.is_ascii_hexdigit(), + // 连字符部分 + 8 | 13 => b == b'-', + _ => unreachable!(), + }; + + if !valid { + return false; + } + } + + // 验证过期时间 + let current_time = chrono::Utc::now().timestamp(); + if current_time > payload.exp { + return false; + } + + // 验证发行者 + if payload.iss != ISSUER { + return false; + } + + // 验证授权范围 + if payload.scope != SCOPE { + return false; + } + + // 验证受众 + if payload.aud != AUDIENCE { + return false; + } + + true +} + +// 从 JWT token 中提取用户 ID +pub fn extract_user_id(token: &str) -> Option { + // JWT token 由3部分组成,用 . 分隔 + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return None; + } + + // 解码 payload (第二部分) + let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { + Ok(decoded) => decoded, + Err(_) => return None, + }; + + // 将 payload 转换为字符串 + let payload_str = match String::from_utf8(payload) { + Ok(s) => s, + Err(_) => return None, + }; + + // 解析为 TokenPayload + let payload: TokenPayload = match serde_json::from_str(&payload_str) { + Ok(p) => p, + Err(_) => return None, + }; + + // 提取 sub 字段 + Some( + payload + .sub + .split('|') + .nth(1) + .unwrap_or(&payload.sub) + .to_string(), + ) +} + +// 从 JWT token 中提取 time 字段 +pub fn extract_time(token: &str) -> Option> { + // JWT token 由3部分组成,用 . 分隔 + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return None; + } + + // 解码 payload (第二部分) + let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { + Ok(decoded) => decoded, + Err(_) => return None, + }; + + // 将 payload 转换为字符串 + let payload_str = match String::from_utf8(payload) { + Ok(s) => s, + Err(_) => return None, + }; + + // 解析为 TokenPayload + let payload: TokenPayload = match serde_json::from_str(&payload_str) { + Ok(p) => p, + Err(_) => return None, + }; + + // 提取时间戳并转换为本地时间 + payload + .time + .parse::() + .ok() + .and_then(|timestamp| Local.timestamp_opt(timestamp, 0).single()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..2d0658ae3c2c069c749a63b8c8bf0152c96bb2be --- /dev/null +++ b/src/main.rs @@ -0,0 +1,197 @@ +mod app; +mod chat; +mod common; + +use app::{ + config::handle_config_update, + constant::{ + PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, + ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, + ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, + ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH, + ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH, + ROUTE_TOKENS_RELOAD_PATH, ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH, + }, + lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH}, + model::*, +}; +use axum::{ + routing::{get, post}, + Router, +}; +use chat::{ + route::{ + handle_about, handle_add_tokens, handle_api_page, handle_basic_calibration, + handle_build_key, handle_build_key_page, handle_config_page, handle_delete_tokens, + handle_env_example, handle_get_checksum, handle_get_hash, handle_get_timestamp_header, + handle_get_tokens, handle_health, handle_logs, handle_logs_post, handle_readme, + handle_reload_tokens, handle_root, handle_static, handle_tokens_page, handle_update_tokens, + handle_user_info, + }, + service::{handle_chat, handle_models}, +}; +use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env}; +use std::sync::Arc; +use tokio::signal; +use tokio::sync::Mutex; +use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer}; + +#[tokio::main] +async fn main() { + // 设置自定义 panic hook + std::panic::set_hook(Box::new(|info| { + // std::env::set_var("RUST_BACKTRACE", "1"); + if let Some(msg) = info.payload().downcast_ref::() { + eprintln!("{}", msg); + } else if let Some(msg) = info.payload().downcast_ref::<&str>() { + eprintln!("{}", msg); + } + })); + + // 加载环境变量 + dotenvy::dotenv().ok(); + + if AUTH_TOKEN.is_empty() { + panic!("AUTH_TOKEN must be set") + }; + + // 初始化全局配置 + AppConfig::init(); + + // 加载 tokens + let token_infos = load_tokens(); + + // 初始化应用状态 + let state = Arc::new(Mutex::new(AppState::new(token_infos))); + + // 尝试加载保存的配置 + if let Err(e) = AppConfig::load_saved_config() { + eprintln!("加载保存的配置失败: {}", e); + } + + // 创建一个克隆用于后台任务 + let state_for_reload = state.clone(); + + // 启动后台任务在每个整1000秒时更新 checksum + tokio::spawn(async move { + loop { + // 获取当前时间戳 + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + // 计算距离下一个整1000秒的等待时间 + let next_reload = (now / 1000 + 1) * 1000; + let wait_duration = next_reload - now; + + // 等待到下一个整1000秒 + tokio::time::sleep(std::time::Duration::from_secs(wait_duration)).await; + + let mut app_state = state_for_reload.lock().await; + app_state.update_checksum(); + // debug_println!("checksum 自动刷新: {}", next_reload); + } + }); + + // 创建一个克隆用于信号处理 + let state_for_shutdown = state.clone(); + + // 设置关闭信号处理 + let shutdown_signal = async move { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + println!("正在关闭服务器..."); + + // 保存配置 + if let Err(e) = AppConfig::save_config() { + eprintln!("保存配置失败: {}", e); + } else { + println!("配置已保存"); + } + + // 保存日志 + let state = state_for_shutdown.lock().await; + if let Err(e) = state.save_logs().await { + eprintln!("保存日志失败: {}", e); + } else { + println!("日志已保存"); + } + }; + + // 设置路由 + let app = Router::new() + .route(ROUTE_ROOT_PATH, get(handle_root)) + .route(ROUTE_HEALTH_PATH, get(handle_health)) + .route(ROUTE_TOKENS_PATH, get(handle_tokens_page)) + .route(ROUTE_MODELS_PATH.as_str(), get(handle_models)) + .route(ROUTE_TOKENS_GET_PATH, post(handle_get_tokens)) + .route(ROUTE_TOKENS_RELOAD_PATH, post(handle_reload_tokens)) + .route(ROUTE_TOKENS_UPDATE_PATH, post(handle_update_tokens)) + .route(ROUTE_TOKENS_ADD_PATH, post(handle_add_tokens)) + .route(ROUTE_TOKENS_DELETE_PATH, post(handle_delete_tokens)) + .route(ROUTE_CHAT_PATH.as_str(), post(handle_chat)) + .route(ROUTE_LOGS_PATH, get(handle_logs)) + .route(ROUTE_LOGS_PATH, post(handle_logs_post)) + .route(ROUTE_ENV_EXAMPLE_PATH, get(handle_env_example)) + .route(ROUTE_CONFIG_PATH, get(handle_config_page)) + .route(ROUTE_CONFIG_PATH, post(handle_config_update)) + .route(ROUTE_STATIC_PATH, get(handle_static)) + .route(ROUTE_ABOUT_PATH, get(handle_about)) + .route(ROUTE_README_PATH, get(handle_readme)) + .route(ROUTE_API_PATH, get(handle_api_page)) + .route(ROUTE_GET_HASH, get(handle_get_hash)) + .route(ROUTE_GET_CHECKSUM, get(handle_get_checksum)) + .route(ROUTE_GET_TIMESTAMP_HEADER, get(handle_get_timestamp_header)) + .route(ROUTE_BASIC_CALIBRATION_PATH, post(handle_basic_calibration)) + .route(ROUTE_USER_INFO_PATH, post(handle_user_info)) + .route(ROUTE_BUILD_KEY_PATH, get(handle_build_key_page)) + .route(ROUTE_BUILD_KEY_PATH, post(handle_build_key)) + .layer(RequestBodyLimitLayer::new( + 1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2), + )) + .layer(CorsLayer::permissive()) + .with_state(state); + + // 启动服务器 + let port = parse_string_from_env("PORT", "3000"); + let addr = format!("0.0.0.0:{}", port); + println!("服务器运行在端口 {}", port); + println!("当前版本: v{}", PKG_VERSION); + // if PKG_VERSION.contains("pre") { + // println!("当前是测试版,有问题及时反馈哦~"); + // } + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + let server = axum::serve(listener, app); + tokio::select! { + result = server => { + if let Err(e) = result { + eprintln!("服务器错误: {}", e); + } + } + _ = shutdown_signal => { + println!("服务器已关闭"); + } + } +} diff --git a/static/api.html b/static/api.html new file mode 100644 index 0000000000000000000000000000000000000000..850ed6d063dc1a5609cfed1991eead384dccc4f0 --- /dev/null +++ b/static/api.html @@ -0,0 +1,382 @@ + + + + + + + + API 管理 + + + + + + +
+
+

API 管理

+
Healthy
+
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/static/build_key.html b/static/build_key.html new file mode 100644 index 0000000000000000000000000000000000000000..f3dbbc1f4c04a6cd6fd423eaf5c183b5bef9d8dc --- /dev/null +++ b/static/build_key.html @@ -0,0 +1,242 @@ + + + + + + + + Key 构建 + + + + + + + +

Key 构建

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/static/config.html b/static/config.html new file mode 100644 index 0000000000000000000000000000000000000000..d60cd1bb7292699e9722dc0e375f99fd7cd173a4 --- /dev/null +++ b/static/config.html @@ -0,0 +1,332 @@ + + + + + + + + 配置管理 + + + + + + +

配置管理

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/static/logs.html b/static/logs.html new file mode 100644 index 0000000000000000000000000000000000000000..fe6a5e6395adbd565aa4d2ce074085f38c563233 --- /dev/null +++ b/static/logs.html @@ -0,0 +1,767 @@ + + + + + + + + 请求日志查看 + + + + + + + +

请求日志查看

+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+
+

总请求数

+
-
+
+
+

活跃请求数

+
-
+
+
+

错误请求数

+
-
+
+
+

最后更新

+
-
+
+
+ +
+ + + + + + + + + + + + + + + +
时间模型Token信息Prompt用时/首字流式响应状态错误信息
+
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/static/shared-styles.css b/static/shared-styles.css new file mode 100644 index 0000000000000000000000000000000000000000..e76f1113793af731a02264029635228ca4eb3f30 --- /dev/null +++ b/static/shared-styles.css @@ -0,0 +1,453 @@ +:root { + /* 基础颜色变量 */ + --primary-color: #2196F3; + --primary-dark: #1976D2; + --primary-color-alpha: rgba(33, 150, 243, 0.1); + --success-color: #4CAF50; + --error-color: #F44336; + --background-color: #F5F5F5; + --card-background: #FFFFFF; + --text-primary: #333333; + --text-secondary: #757575; + --border-color: #e0e0e0; + --disabled-bg: #f5f5f5; + + /* 布局变量 */ + --border-radius: 8px; + --spacing: 20px; + + /* 动画变量 */ + --transition-fast: 0.2s; + --transition-slow: 0.3s; +} + +/* 暗色模式 */ +@media (prefers-color-scheme: dark) { + :root { + --primary-color: #90CAF9; + --primary-dark: #64B5F6; + --background-color: #121212; + --card-background: #1e1e1e; + --text-primary: #e0e0e0; + --text-secondary: #9e9e9e; + --border-color: #404040; + --disabled-bg: #2d2d2d; + color-scheme: dark; + } +} + +/* 基础样式 */ +html { + scroll-behavior: smooth; + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + max-width: 1200px; + margin: 0 auto; + padding: var(--spacing); + background: var(--background-color); + color: var(--text-primary); + line-height: 1.6; +} + +/* 容器样式 */ +.container { + background: var(--card-background); + padding: var(--spacing); + border-radius: var(--border-radius); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: var(--spacing); + transition: transform var(--transition-fast); +} + +.container:hover { + transform: translateY(-2px); +} + +/* 标题样式 */ +h1, +h2, +h3 { + color: var(--text-primary); + margin-top: 0; + line-height: 1.2; +} + +/* 表单元素样式 */ +.form-group { + margin-bottom: 20px; +} + +/* 标签样式 */ +label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: var(--text-primary); +} + +input, +select, +textarea, +.form-control { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--card-background); + color: var(--text-primary); + font-size: 14px; + line-height: 1.5; + transition: all var(--transition-fast); + appearance: none; +} + +input[type="checkbox"] { + width: auto; + margin-right: 8px; + cursor: pointer; + appearance: auto; +} + +input[type="checkbox"]+label { + cursor: pointer; + color: var(--text-primary); + user-select: none; +} + +input:hover, +select:hover, +textarea:hover, +.form-control:hover { + border-color: var(--primary-color); +} + +input:focus, +select:focus, +textarea:focus, +.form-control:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px var(--primary-color-alpha); + outline: none; +} + +/* 禁用状态 */ +input:disabled, +select:disabled, +textarea:disabled, +.form-control:disabled { + background-color: var(--disabled-bg); + border-color: var(--border-color); + cursor: not-allowed; + opacity: 0.7; +} + +/* 错误状态 */ +input.error, +select.error, +textarea.error, +.form-control.error { + border-color: var(--error-color); +} + +/* Select 特殊样式 */ +select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23757575'%3E%3Cpath d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 20px; + padding-right: 36px; +} + +/* Textarea 特殊样式 */ +textarea { + min-height: 150px; + resize: vertical; + font-family: monospace; + line-height: 1.4; +} + +/* 按钮基础样式 */ +button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 8px 24px; + border: none; + border-radius: var(--border-radius); + background: var(--primary-color); + color: white; + font-size: 16px; + font-weight: 500; + text-align: center; + text-decoration: none; + cursor: pointer; + transition: all var(--transition-fast); + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +/* 按钮状态 */ +button:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--primary-color-alpha); +} + +button:active { + transform: translateY(1px); +} + +button:disabled { + background: var(--disabled-bg); + color: var(--text-secondary); + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* 次要按钮样式 */ +button.secondary { + background: transparent; + border: 1px solid var(--primary-color); + color: var(--primary-color); +} + +button.secondary:hover { + background: var(--primary-color-alpha); + border-color: var(--primary-dark); + color: var(--primary-dark); +} + +button.danger { + background: var(--error-color); + border: none; +} + +button.danger:hover { + background: #d32f2f; + /* 深红色 */ + box-shadow: 0 4px 12px rgba(244, 67, 54, 0.2); +} + +/* 激活状态的按钮 */ +button.active { + background: var(--primary-dark); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); + transform: translateY(1px); +} + +button.secondary.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-dark); +} + +/* 按钮组 */ +.button-group { + display: flex; + gap: 10px; + margin: var(--spacing) 0; +} + +/* 按钮组中的按钮间距调整 */ +.button-group button { + flex: 1; + min-width: 120px; +} + +/* 消息容器 - 固定在顶部中间 */ +.message-container { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + display: flex; + flex-direction: column; + align-items: center; + pointer-events: none; + /* 允许点击穿透 */ +} + +/* 单个消息样式 */ +.message { + padding: 12px 20px; + border-radius: 4px; + background: var(--card-background); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + margin-bottom: 10px; + pointer-events: auto; + /* 允许消息本身可以交互 */ + min-width: 300px; + max-width: 500px; + display: flex; + align-items: center; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + animation: messageIn 0.3s ease-in-out; +} + +.message.success { + background: #f0f9eb; + border: 1px solid #e1f3d8; +} + +.message.error { + background: #fef0f0; + border: 1px solid #fde2e2; +} + +@keyframes messageIn { + 0% { + opacity: 0; + transform: translateY(-20px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes messageOut { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(-20px); + } +} + +/* 深色模式适配 */ +@media (prefers-color-scheme: dark) { + .message { + background: #2c2c2c; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + .message.success { + background: #294929; + border-color: #1c321c; + } + + .message.error { + background: #4d2c2c; + border-color: #321c1c; + } +} + +/* 表格样式 */ +table { + width: 100%; + border-collapse: collapse; + margin-top: var(--spacing); + background: var(--card-background); + border-radius: var(--border-radius); + overflow: hidden; +} + +th, +td { + padding: 12px; + text-align: left; + border-bottom: 1px solid var(--text-secondary); +} + +th { + background: var(--primary-color); + color: white; + font-weight: 500; +} + +tr:nth-child(even) { + background: rgba(0, 0, 0, 0.02); +} + +tr:hover { + background: rgba(0, 0, 0, 0.04); +} + +/* 辅助类 */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.text-center { + text-align: center; +} + +.help-text { + margin-top: 4px; + font-size: 14px; + color: var(--text-secondary); +} + +.error-text { + color: var(--error-color); +} + +.mt-0 { + margin-top: 0; +} + +.mb-0 { + margin-bottom: 0; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + :root { + --spacing: 16px; + } + + body { + padding: 10px; + } + + .button-group { + flex-direction: column; + } + + button { + width: 100%; + padding: 12px 20px; + } + + input, + select, + textarea, + .form-control { + font-size: 16px; + padding: 14px 16px; + } + + table { + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + th, + td { + white-space: nowrap; + } +} \ No newline at end of file diff --git a/static/shared.js b/static/shared.js new file mode 100644 index 0000000000000000000000000000000000000000..4af8e6dbb08ef0408a97f61586df5827ac58f3c4 --- /dev/null +++ b/static/shared.js @@ -0,0 +1,323 @@ +// Token 管理功能 +/** + * 保存认证令牌到本地存储 + * @param {string} token - 要保存的认证令牌 + * @returns {void} + */ +function saveAuthToken(token) { + const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期 + localStorage.setItem('authToken', token); + localStorage.setItem('authTokenExpiry', expiryTime); +} + +/** + * 获取存储的认证令牌 + * @returns {string|null} 如果令牌有效则返回令牌,否则返回 null + */ +function getAuthToken() { + const token = localStorage.getItem('authToken'); + const expiry = localStorage.getItem('authTokenExpiry'); + + if (!token || !expiry) { + return null; + } + + if (new Date().getTime() > parseInt(expiry)) { + localStorage.removeItem('authToken'); + localStorage.removeItem('authTokenExpiry'); + return null; + } + + return token; +} + +// 消息显示功能 +/** + * 在指定元素中显示消息 + * @param {string} elementId - 目标元素的 ID + * @param {string} text - 要显示的消息文本 + * @param {boolean} [isError=false] - 是否为错误消息 + * @returns {void} + */ +function showMessage(elementId, text, isError = false) { + let msg = document.getElementById(elementId); + + // 如果消息元素不存在,创建一个新的 + if (!msg) { + msg = document.createElement('div'); + msg.id = elementId; + document.body.appendChild(msg); + } + + msg.className = `floating-message ${isError ? 'error' : 'success'}`; + msg.innerHTML = text.replace(/\n/g, '
'); +} + +// 确保消息容器存在 +/** + * 确保消息容器存在于 DOM 中 + * @returns {HTMLElement} 消息容器元素 + */ +function ensureMessageContainer() { + let container = document.querySelector('.message-container'); + if (!container) { + container = document.createElement('div'); + container.className = 'message-container'; + document.body.appendChild(container); + } + return container; +} + +/** + * 显示全局消息提示 + * @param {string} text - 要显示的消息文本 + * @param {boolean} [isError=false] - 是否为错误消息 + * @param {number} [timeout=3000] - 消息显示时长(毫秒) + * @returns {void} + */ +function showGlobalMessage(text, isError = false, timeout = 3000) { + const container = ensureMessageContainer(); + + const msgElement = document.createElement('div'); + msgElement.className = `message ${isError ? 'error' : 'success'}`; + msgElement.textContent = text; + + container.appendChild(msgElement); + + // 设置淡出动画和移除 + setTimeout(() => { + msgElement.style.animation = 'messageOut 0.3s ease-in-out'; + setTimeout(() => { + msgElement.remove(); + // 如果容器为空,也移除容器 + if (container.children.length === 0) { + container.remove(); + } + }, 300); + }, timeout); +} + +// Token 输入框自动填充和事件绑定 +function initializeTokenHandling(inputId) { + document.addEventListener('DOMContentLoaded', () => { + const authToken = getAuthToken(); + if (authToken) { + document.getElementById(inputId).value = authToken; + } + }); + + document.getElementById(inputId).addEventListener('change', (e) => { + if (e.target.value) { + saveAuthToken(e.target.value); + } else { + localStorage.removeItem('authToken'); + localStorage.removeItem('authTokenExpiry'); + } + }); +} + +// API 请求通用处理 +async function makeAuthenticatedRequest(url, options = {}) { + const tokenId = options.tokenId || 'authToken'; + const token = document.getElementById(tokenId).value; + + if (!token) { + showGlobalMessage('请输入 AUTH_TOKEN', true); + return null; + } + + const defaultOptions = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + try { + const response = await fetch(url, { ...defaultOptions, ...options }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + showGlobalMessage(`请求失败: ${error.message}`, true); + return null; + } +} + +/** + * 从字符串解析布尔值 + * @param {string} str - 要解析的字符串 + * @param {boolean|null} defaultValue - 解析失败时的默认值 + * @returns {boolean|null} 解析结果,如果无法解析则返回默认值 + */ +function parseBooleanFromString(str, defaultValue = null) { + if (typeof str !== 'string') { + return defaultValue; + } + + const lowercaseStr = str.toLowerCase().trim(); + + if (lowercaseStr === 'true' || lowercaseStr === '1') { + return true; + } else if (lowercaseStr === 'false' || lowercaseStr === '0') { + return false; + } else { + return defaultValue; + } +} + +/** + * 将布尔值转换为字符串 + * @param {boolean|undefined|null} value - 要转换的布尔值 + * @param {string} defaultValue - 转换失败时的默认值 + * @returns {string} 转换结果,如果输入无效则返回默认值 + */ +function parseStringFromBoolean(value, defaultValue = null) { + if (typeof value !== 'boolean') { + return defaultValue; + } + + return value ? 'true' : 'false'; +} + +/** + * 解析对话内容 + * @param {string} promptStr - 原始prompt字符串 + * @returns {Array<{role: string, content: string}>} 解析后的对话数组 + */ +function parsePrompt(promptStr) { + if (!promptStr) return []; + + const messages = []; + const lines = promptStr.split('\n'); + let currentRole = ''; + let currentContent = ''; + + const roleMap = { + 'BEGIN_SYSTEM': 'system', + 'BEGIN_USER': 'user', + 'BEGIN_ASSISTANT': 'assistant' + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // 检查是否是角色标记行 + let foundRole = false; + for (const [marker, role] of Object.entries(roleMap)) { + if (line.includes(marker)) { + // 保存之前的消息(如果有) + if (currentRole && currentContent.trim()) { + messages.push({ + role: currentRole, + content: currentContent.trim() + }); + } + // 设置新角色 + currentRole = role; + currentContent = ''; + foundRole = true; + break; + } + } + + // 如果不是角色标记行且不是END标记行,则添加到当前内容 + if (!foundRole && !line.includes('END_')) { + currentContent += line + '\n'; + } + } + + // 添加最后一条消息 + if (currentRole && currentContent.trim()) { + messages.push({ + role: currentRole, + content: currentContent.trim() + }); + } + + return messages; +} + +/** + * 格式化对话内容为HTML表格 + * @param {Array<{role: string, content: string}>} messages - 对话消息数组 + * @returns {string} HTML表格字符串 + */ +function formatPromptToTable(messages) { + if (!messages || messages.length === 0) { + return '

无对话内容

'; + } + + const roleLabels = { + 'system': '系统', + 'user': '用户', + 'assistant': '助手' + }; + + function escapeHtml(content) { + // 先转义HTML特殊字符 + const escaped = content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + + // 将HTML标签文本用引号包裹,使其更易读 + // return escaped.replace(/<(\/?[^>]+)>/g, '"<$1>"'); + return escaped; + } + + return `${messages.map(msg => ``).join('')}
角色内容
${roleLabels[msg.role] || msg.role}${escapeHtml(msg.content).replace(/\n/g, '
')}
`; +} + +/** + * 安全地显示prompt对话框 + * @param {string} promptStr - 原始prompt字符串 + */ +function showPromptModal(promptStr) { + try { + const modal = document.getElementById('promptModal'); + const content = document.getElementById('promptContent'); + + if (!modal || !content) { + console.error('Modal elements not found'); + return; + } + + const messages = parsePrompt(promptStr); + content.innerHTML = formatPromptToTable(messages); + modal.style.display = 'block'; + } catch (e) { + console.error('显示prompt对话框失败:', e); + console.error('原始prompt:', promptStr); + } +} + +/** + * 将会员类型代码转换为显示名称 + * @param {string|null} type - 会员类型代码,如 'free_trial', 'pro', 'free', 'enterprise' 等 + * @returns {string} 格式化后的会员类型显示名称 + * @example + * formatMembershipType('free_trial') // 返回 'Pro Trial' + * formatMembershipType('pro') // 返回 'Pro' + * formatMembershipType(null) // 返回 '-' + * formatMembershipType('custom_type') // 返回 'Custom Type' + */ +function formatMembershipType(type) { + if (!type) return '-'; + switch (type) { + case 'free_trial': return 'Pro Trial'; + case 'pro': return 'Pro'; + case 'free': return 'Free'; + case 'enterprise': return 'Business'; + default: return type + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } +} diff --git a/static/tokens.html b/static/tokens.html new file mode 100644 index 0000000000000000000000000000000000000000..2225d9d8e9f4877158436e004b82c9a28500fdf9 --- /dev/null +++ b/static/tokens.html @@ -0,0 +1,689 @@ + + + + + + + + Token 信息管理 + + + + + + + +

Token 信息管理

+ +
+
+ + +
+
+ +
+
+

Token 管理

+
+ + + + +
+ +
+ + +
添加模式: 输入要添加的token,每行一个 + 删除模式: 输入要删除的token,每行一个
+
+ +
+
+ + +
+ + + + + + + + + + + + + + +
TokenChecksum邮箱会员类型Premium用量试用剩余操作
+
+ +
+ 快捷键: Ctrl + Enter 执行当前操作 +
+
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/stream_data.txt b/tests/data/stream_data.txt new file mode 100644 index 0000000000000000000000000000000000000000..9318aca4b58c006b31d54beaded1f7be9c9d7f16 --- /dev/null +++ b/tests/data/stream_data.txt @@ -0,0 +1 @@ +000000001362110A0F0A0D52656164696E67206C696E6B730100000F021F8B0800000000000003D55A4F731BC96EBFB32A971CDF09555BA9DDD1CA3239FCCFCB2B59966DAD2D2F234AEBEC562EE00C346C6BA69BDBDD439AFA00EF9C4F91AA7CAD7C8A1C5340CF0CA97F96F4D6FB52B9D844FF01D0C00F68A047BFFDD7AFADFFFCB5F59785F74B3779F9D279F4A53B304BD2A80E1253BCFCE77FFA7949FAF0046632F597FFF9B7296604B9C90CB45AF085FF794B1EA8409583365E5DAA04BD32DAC17A419A5664A162B1B79758424F6E6F6F1FF6F6CA651A083016F6F62C3993AF84460D4A272A25ED0F5AAD63618D696AC9B909B45AC7DA33D3F3291367E448A74C81D213004789D1A963BD5EAB547FEFC152426A45E017C4CBFEBAB3A3D582F38572E09427500E96D6784A3CA530DF80A5A3C3E9F9D1BB43409DCAEEB7C66439C1D4AA15261B989A5C251B993D275B3830973023BB5209012E97F9E6A0328EA72F1E0A728E4DF7641B3D629623536A6F37909894D8108797D902B5721E35FCF0E3B81BC1613E47AD107EF8B1DBEF3399911532EEF06C415625A8618685E1D14E04873A35D6CA8EE180C9CCE4B2A1D713AA5479DE2CF52A2B514EFF0AEDBC4CEB099B91F64A33D9E76DB6A04A8BA190E55C588E87111C96CE5BCC15BE3C3289712F8F1656395FA083139733E71F7E1C74AA65C2A2C78A5F939DA3FA1CCE39EE45F00A17C89B44FE2B5C58543237EC32A9B31C53720BF8E1C7D1A81D89B6989A7A39E5684B27EAF585CC545930190BA5AE890FD296A55AF8C6F198095B34677E6572B51205FB63211D9F980FF08EEC35656615ECD11D0D79D6BB350A1D0F98B678AD72DECBF26DA949F1B945F932CFD0562E64A1A5BD624E6FD01951642063A54E794BDC1F467084C5DCA4B265C40C8FB0206B8CE8DD95798DB5DA47B824F8856C4A323BE2914D81BAB27E65A123D2EC2438BC64BCC8CA4104470B4CE537CB58A85CAC24E3E1A823FE6D7253CC835D58B4298C15BBC78331933A9353F4E28AD887D754C0192D65B4CBA3CE239C05B1FD36B3B4067D30C888256F9695F344C035250BDE5FCE7395305CE27604AF491768AF98EC47F0FAB39A9BD2077B7523786D0AA58380CE96D2BB5C3A111C679BA5E72DED088E739861BEC2D458D1AA1BC1F1EF257A6315E6F0B6549A42CCF05AE74D0DFE388263BF50661962B0DF89E00D5A433BD68EC7A308DEA8CF010063FEAD79EEE5A144C37661B7CF0B2DEA84EDDEED32413A59B0F800AD3E0746353835F9469313B983D13882B7380F88E87598A87C14F3E9DE92B19990E3719F495BA0DEB0F178DFA2C22D4B7CABE616738F56F4E1AD9628E85311BA8AE1783C96812DF2DE9698526ECAA5C066DC9691A299F354601E7C1E473B268D7B4C6DEA13C611BC4325BEECB7C711BC333A2DAD24827EBB277406EFF91F0E065E5DEA0C2D9F86217C9250A5609713D5890E61C3117CA253D3582C8EE0C4E2EF3C35E07576BBAD1BC189B348B9A41B5EE83117FEE3087EC2021B64FD844B495823FE6D6C1AB217F37E4F7A1310D18BE0BDB184FB5BF4994BDEC3AB8C332B23B8EF46F0BE5CA3F2C2A11FC1FB8DCD36D775E61F0F22F8801267A3BEFCF6AB0A829D083ED01CB509C28574C62F240C07BC56CDEB3BA2DB1132A8D6193141C9C293769E240DF6E22E0FFA4559DF32C376041FCA2F54CC4D693331501CC12926684499AE1094D621C1503CC514337489C02866954E31C7B5C4279F8DA94DE588B690A95A9113FD032D4B63E19DFB902A074C58AFB4FABD0C089391D22A5FE91AC7713DA22481C40CDA53FAA212D655F4361A85E80E874265260F00E1BBF1D4684F9A321B7C12D67B47D6A20F1E3F35D6248979F9899C27CBF7EC026DB0A6ACBEE6B80BFAC51CCE1FB150552032CC3ED21203AC864CF805D926FA3B3CB286DF08EB5B9237A8042DF29DCCF0EFF3404662D5785811956779B1B16B94B01E46F07351A17114C114AF9A1A22663227C78644CD458E55DE48FC8CD9D753D458047143A644FA46ECDD8F604AB6E4DF9D08A60B95ABE5526971DCA01B714A0A9AF758A6B1BECCE4B45D595E92F586F3BE09A6FC570C6966CC15C419955A19FDF21437C67B311F87D199292ADF72E23D2B9D53F8F23D5EE3D5A23ACF3082B335EA34045B3B6A2A9F015F2533D40C1AA583CB473C52A60A0E2D06AF8C394066EC73D1348E3B4CDA798565A136C982F25C4E19F3C9668A01011FC8E870C7C63CA6335C1ACB038CF0596E56781534179EB959D51513DFA23353FAC5CEED3B8C60B60C150E7B7266157C407D252A32EDE1BDF2DE49F9F19156AABAC6671E3E9489C286FA45E984B4AF1273E51B9E2A43728A39E3CF4AAB3416218858F29A8B952AAD335E666B4A49D201ABBA56FE3AE094473A119CA35A87BCC74739C7CF6A0B2FC6D739EAEB3A24B95C395F60CD9ECD7D6E4289108FF8B7CEC45B1C7EE7566995621AAA6F33C7AC82CA79A995ABAA5C5E57DA2B12C0B60311EC72842A31EE669D739135D818447071C555A4DC665C345E68C56D0183018E0B65B9441740769AB9F74A67A9E14B8CABE56A90DBA6DAB017B66CE26314C1C5F59C768C318AE017D2745D52B8F99856E4B55C8BA31E5336534D6DB60FAF38774959DBB93B7971303B0833BF524121BA8711FCD6DCF59C3C7F53C51CE7EB1042DD085AADE98281AACB624E969B8AA305EAAC1E80AF355EDDF6735BAFD3AA21627FA4E81182510BDC54DD13BCDA802BE72EB16AAE74061B5302669608BC1196873E470E737DA7FFDA6F7AB6ED9A9B6DDBC13FA4F3ABFB35A89A5DD952A0E2BB438AB7D071D7CDA103A56196637275D06ACDAAA313F0F52DA3D06A3DDD2647B929EFD34FA780C99536EB9CD26C67C3F7EEB689AA43AC69BE30E6EA199DEB4EA77AB3D3BF35717F731B1ADF44A0275390986269344F552F14ADD6A74AA98BB30F8CC6F305F14F5813B88529F31404696C914A7D07DEDCF398F089BECFF3B0986D19DE31D425131648A74BA3B4874B54B9FB07BC16FCA29CF2C0A25DB95C1AEB4558F38E20FE3505BC214AD94A67B399FCE60587790EB38DF354387689153761CED178B1F4AA2030EC2DE6B144E761DC861437EE80B3CC1A16CA710393600EA5AC3EE08D87D313F82BDC62576D04CC4CAB351E1FF48700FF52ED6AB5CE4D8A9B568BFB43FF767AFEF8F66EE7BEED3363F1F1BD837BF74E73DC64D6943A7D9CC368701F870F38777FEFDE9D3D9C0829B39852CAD5D0A5E15E2A215691AB53CCE1E7D263C603A7F8D9D85DB24911CDA49149D8D9BD1DF96820356B2D6EB69418CB228D06CF784D51B0C56B38CDD217E5BC834B636F4C733F8F50EC88DA8E2E6F083C68B5BEFBEE3B38A31C3DA5ADD647D384AE6354EEE6371B1685ECC4C22A2D0F5AAD29A3F0A4D9D86AFD841AE2C13EC4EDB87F8BAB250E06E113D495B5FDAFAEAD57F59EB4AA5BAF3AD19CC71CA540D61A1B0C55A3B9D5DADB3B0B492BDDDB837F7F2109413957122CD081366B981369A8325B7A00AFC8AF79A43789BB5848FC0F279D3616309D9DEF43523A6F0AB20EE8CB92AC229DB0E89C5662B74A07A31B15005A506BDC1E4DDA7DE6238A5D489A15B53E11A025480CBFC4957C51780385D15CBECB81B8A7BF2C2D7715417D777093F370D2E9349C4FC34EA533E17E0897EA8B9C57CEAA8A65CE4506EBCBC75B07E145B349928E2557E6FEAE98F6F8C907581B7BC5A64091DF0058F4BFC57730E9F61ABE27EC7275A92A9F7D625FAD0854331C1434C64382A5234ECD5BCEA196106D4A6B49FB7C239A544A35A70F6ADDD2A33FE975B77AE815375319FADA92F5311BC60D0898FB4D1454F5D10E1C0FE0CE7EB52BE28E2E9DB8D2E5B8E67C2C9CCFD0D30D9CF38939F9FF01BC7727DD2E1C9E0A2BC63EFF7E32E2ABB3EEC0BEA9E7A627B78ED5DB35F19F87D4DEA4DB7E08515C7D6C8D12446DC125F01145144FB2981D45EE8889074F0E88ADB7E9E150E84DDAED6FCBB13BE90FBF2DA895DE3AF75998EE4EFA759CCB585CA7F19929084AC730E38E620B35FE76A57495F41861CB1C3D5FCD3B9FBD1EC27D5DA0D64EDEA2BE512ADE87CE70D2E9FF3988BC900355156E083EC0792ED57F38972B93849CBB2CF37C53F704D3FA8437D51C4C7AFD8710FDC7CC775B4C9D76DEF0778272991B4CA5A4E6F371DAE154236D7915EC41F0234E583CE285CEA43DFA635ED84270EB0FC1E75AE53997FF2B95724355757760D83EEBEA76498CBE54B6A82E975BD9E17E95DBA3491C3F27B06E4486B4502266BD204B70B935B6937DBB16778D7F6FA9309CF4C7BB01D5A903EA9DCA16772EA2E3624E69AA74E6BE765D3CC5639D7DE8F427FD073DD65CDABBAE122FD6B7F34ECC34F152FB4AFABB67384AB987748C9F53713DB160A9F876BE09DF9B6C6F04C1B753B7DDDEE6DDFBEAABAFE4FFC5E3386AEE80DBF84EABC833CEEF78131D38FEC2898E679C9AE7F7681BEF82BAFD9496A0337E00FAA271CF84CAC6BCE02F86509894F27B52565D0D0D26FD3E4C4335148A76A986AA4CB7E6787DFC965C2BBFA8EF49F6D3BD3A88F1ACC973EE004BBF1324CA33AEF9D6B88BED70D8DD7BEB594E4DEB3677B96D732B00D157D4FC7A36FB3BBCCDC7E88CB7D8BCCF73AF31CF5FD01FC85522A47FA3C1786EAABE2FAE846D6FD2EFEE20B5337A1252874F5AF5F5D6BA5ED534D527E18A57C51293CA236E4DD6EDDFB8594277242396BC55B4C2FC668FF2580FF19095FBFBD0694F7A4FAF5D9FDADF569C3BFF8FAA83BB2A3FA790E7E7185B28EF19D8D58B0226D63877A3937C40EF9D80C39BC1D6DF87F6F8D15EC0CC1DD915A5E012F49E2CA5A0C9CB5D13CE5DA992AACB4BB2525EF303CC7E089242650B5F811030CF6FD688FB55ADC346DCA951AB87A690939A4CE33C1FF0816E420E339AB46F445FF374540B550E4A5D17DC736BD68EEA3766815DB97CE1CD8B34185D1221BF043E18034F49363DE972E2F69FDBE256623ACFEAEB6EE734BA93D2986BBCADEA64AC79693B944EA56E25729365D21AEB70CFCD663F3FF6F4F694C0E986DA6AF0CDCC77A7B6BC753FD561CE7F44544579D0AB6428F0FBA8BADC09FA2559C7EFB1B754EEEC3E6A3CAF69A7DD67AB67C77457124DF7FF4C787B3C89475F05E1F79600D728518F7EA79311E516B812143D0D54F7A6F38732447870ED0D76C11C3F76A7FEF7DFFEA379E48677F29585BF05AD250FCE373BDFEBC25FD82EC38BFEE18ABF83CF55AEFC060ABE589370BEE6251C0574986596C4F639AD28AF7329274AAF244186722B382454427EB3E4EB90FFDE69A5D212F3E6490EF08658DCC08AFF5C2AA52569AECEC3270552B6FE06B9144CB3A4094C0F7F7DBB0FB304737A71AEC8EECB172AAA92FF112E31517EB3CF6E5F539EF3FF8C17B7A4843F2B06459B97C74B425FDAF034543A3A80FF05840B634FAC2C00000000000002620001000014201F8B0800000000000003D55BCD6E244772F6B99E222CC1D03487D3EC5F92DD1076CDE15023EEFCB5D91C095A181845574557A7589559CACCEA9E9E95005DBCF0CD7BDB8B0DFBE23DAECF8BB5E1771124C03EF9E0173022B2AABAC92167382BED2E3C074E6756666464C41791115159EFFDC5CEFF7EF9E157F74F1E9E3E7D31FD6C7A7EF2E4AB9F449F9912D012A006A53D65994A497B28AC492DE639D95D28CC8A2C25305BC3C3C9F9BD81815C69D5867AEA028B620DDEC082B20250BB155940BD862F4B725E19EDC02FD0835F10948E2C2CD0C19DD29598656BEE5DC34A6519CC0870664A0FB149944E5BED28EAB6E1D305E9CD54E500DD85D229CC8D054A9477BCB25F90B23C8F76A1C8081D81297D517A40702A2F32355794C092AC534683990B451E0FB3CCC41781C1854A17994A174C931F2F50A7E440534CCEA15D03EA0430491CC426CF4987B5954E548C9E60B5204B50EA302F09E479AF33220DEE421505256DF888197F897991D138FAFCF3CF33D46989298D0BF48B3D6FF6E62AA3686F0FDAED36D04BE53C6F5788B5DBEDE817BF906DBFE8C2D75FBF7D54EF8DA33EFFFCF3E8BC166D8C1A1C916C9DB457968039D9056782920A4B73B2BC65A3B33558C22428A648D053AD07A1DE8667732F7A532E283727D41B18388FD6EF914E6A55F04A0D0A2A51EDC2ACF432E70307E602D77F0967B4B2CABFC6636048CDC11514ABB98A05599604802CF3A36C856BC7A85EAA8400616615CD815E16196AF45B98A836B30BA5CEC8B90DF2AE23EDC2C2CDAE2396A62311BDF408B85C30B0CC9920B3D91A1084F8B6C5D53080DC2494ED426CB2CC7C592A598F57A5206D2C8A6C5D0FF2A662B811621BCE2B430C0C07EE84A14A3F9708AC4DD9887D499657B2342F3306B38694345914CCD47BACCCC580369EF9B726296302CC672A2D955F07B720246B0630CBB6ACC2522A2EE18EA0902DAA36A6D61530F8057CFBCD3FEEEDC1B7DFFCE60A78BFFDE637DF7EF34FF54CC8D15E90756D386FF046DA9596AE6E383C63D613CAC8D386EC15B3357663E3220A96602D8720E7669F4C8E47328AAEACD78EA25E1B1E0469654AE8E678C1308339C6DEB5A3A8DF86D339604059CE9E869D0E2B466940F673A452DDE0A37170965C6174C2A3C4AEEA01ED281A8897C9D133195B8D74C44359548959E976140D2BE7CA26C55B63D7AB69B5E5165DB3D836981AA49E3E009C7B76090BA6ADBCC20C66185F782573337541E0CC18D8D1146BBF309A1D93D0FFFA6BF13ED1FE354C6C31206E1EF5464FC125555C894D6DB326C8613FFA26C60474969CAFED2627BF3009EC419CA17341661BACCF28333A65F7B6D9D125BFED4C4E7BC62FC806D73D2F752C60383A3D5EA0FF58396FECFA4E0B7E110180385EFE7F4B127577259303418423B4F182192D331F7C48E5BF9260B76E61CA2C6970808E3DA0785CA5E7A27E664239B8D066A5D966D7A66CC319CD2D2A0D736B7270B866A9BE770A9843A97196110F9C59B3725489CF93D5E4DFDBE55189D11F7858E09200E3581CA4B93ACC5878EFF4836D72B5DFB584D93DAF72629CB9DAD7B6DF8349AD4F71D331BB780E2032A51902CCA89C1A972522A80F7612676522F0BE11EF1024D28E3EFCEAE4E9834DFC13D511D1F3E9C9D9573F89265BC89AB30B5EF1D242CB795BC6219E61CBCC32F6579716140D58CA68895AFC6D2E41905DB7E1A9014DECC20D60CC0AC92849E93ADA89B2147B3ECFAE6CA71D7D1897CE9BFCC5F6F89F44671B4770BC509A09CED690D01CCBCC471FEE5D3BA99243B5EBAB52781F4E75517A1745EFBFFF3E4C8C67F72627D159BDBB4F6956E9E32CE8239258F20650865D6E50C968FE716019E0760B60BE332EDFABCE1355B9849AD2151CB6DF8C5E06A8044EA4634A3660C69959BE06E95B2138BA17FE4511EB80577B7EF6780C0BEF0B37DEDB63BF56BAB62948A36AC726DF6BC6C546B326C7D184DD7766520351042FF9CF43F240392A39CE24CE0968E4C38F9664E15941FAE8147676624B2C9D9D9D5DD8D9A944B5B3C3D2DDD9B1E44CB694B62414B14A4833CB27421A93C492736388A21316263C3B9F70E38C1CC782CFCE27A0F418C0516C74E298AF072A61C55A8A492D83EA9F9D4F7EBA35238A829A64874A823C4FB10F098BA5E3A3C9F9F1C747E2F479F64363D28C6062D512E3354C4CA6E210D99F93CD1DDBF494EC52C5D541DEAE84E3E9A5AF8FE7DBCBE82D623936A5F61C759984581047F374815A398F1AEEDC1DF55B7094CD502B843B77FBC3213753B2D2EC75F9694E5671E03EC5DC706FB705473A31D6CA8C837D6EA62693098381B44A9565CD50AFD21265F7F7D1CECAA47E603926559A9B439E6673AAB8389066391392A383161C95CE5BCC14EE1D9BD8B8BDE38555CEE7E8E0D4654CF9CEDDFD6E354C480C98F1576467A8BE08FB1C0D5A701F17C89364FDFBB8106770E7EEE8A0CF4D9D6698905BC09DBB87879D96708B89A9875386B674C2DE509AA92A736EF6A4A55E116FA42343B5D0EDF546DCB079B3E7FB26534B61703892A6E31DF3063E26FB8A52B30CF2E81F1EF053EF5628EDDE3EB72DBE5219CFE5F56DA949F1BE85F9324BD1562AE4454B7BC1943E426784917DE92B75C2537AC383161C633E33894C396482C798933546F8EECB738D35DBC758107C423621797AC83DEB1C7525FD4A42C7A45949703467BCC8C8FD161C2F3091DFBCC68263A73B7787D21FB67AC8BF4D66F259900B2F6D726345EEBDFD1137752ABB18F4AAC62E3CA01CCEA890DE3EF73A8F7016961D7698A435E883400E79E57551294F16784572A614E52C5331C3A5D769C103D2EC07B9396CC1832F14170A82BCFA2D7860B822115712A95B7A9B4AB70527E9BAF03CA5D382930CA6982D313156B8EAB7E0E4CB12BDB11C2A3E2C95A660333CD6795383BFD78213BF50A6083638ECB6E023B486B6A4DD1B1DB6E023F54500C0887F6B7EB67724D6B019D81FF2408B3A66B9F7FBDC201D2F78F900AD211B46D53931D95A939375F70F472D7888B3808841971B958E7ABCBB87646C2ACDD168C84D9BF38174E7EE80E72D2ADCF28A0FD5CC62E6D10A3F3CD512057EAA86AE6CB8371A49C706790F4B4C28336521B01975A4276F9E79CA310B3AEFB5B644DA1B706B5DEFB0D7828F51892E879D510B3E363A29AD3882616720ED141EF11F36061E5DEA948B3177EE32844F63AA18ECB3A33AD5C16CD8824F75621A89F55A706AF14B7EB4CFE3EC665ABF05A7CE2265E26E78A0C74CE88F5AF033CCB141D6CFB0108775C8BF8D4D82F762DA8F48AF0322062D78642CE1EE067D66CE73789471666904F7FD163C2A57A8BC5018B6E0D1DAA6EB57B5E71FEDB7E0318A9D1D0EE5B75F5610ECB6E031CD509BB0B8349DF10B31C37D1EAB66F519D1EF4A33B0D63DE406C50B4FDA79123738E8F5B9D32FCAFA9439E8B4E071F992F299296D2A02EAB5E009C6688499BE3428A94D82A1F804134CD1C502A31EB3F404335C897DF2DEB8B5AE14D19166A296E484FFD096A13DA19DF9E02AF7B961BDD2EACB32204C7A4AAB7CC56BAFD7AB7B9438901E83F609BD5431F32A7C1B8DD2E81F1C482B355900089F8D4F241EA2D4069D84F1DE91B5E883C69F186BE2D8EC7DCA8524CBE7EC026D90A68C7EC57617F8EBB1393FC55C5586C8307B4A0506581D708373C4C6FABBDCB3829F13D6A7244F50315AE43399E13FE48E9444AABD83AA516996071BBB4231EB83163CCB2B341EB66082174D0CD1E366C68558AD5073906315E7A3C255871F6ACCC37207DC92D5D722EF610B26644BFEDD6DC164A13255149C6030B3FD16BBA4C0F980D734D697A9ECB62FC34BB2DEB0DF3741947F83C1CD8C388238A3522BA3F79EE0DA782FE263333A3379A55B76BC67A5730AF71EE12BBC5854FB3968C1D90A75128CADD36A229F7D3E4AA6A819344A07951F724F992838B218B432620399B2CE85D35EAFCB4D3BABB02CAD75BCA02C935DF6786753C58080C7647438637BDCA7532C8CE50E46F834334BBC089C0BCDCC2CEB88894FD1A929FD62EBF43D68C1B408110E6B726A153C467D212C72DBC323E57D28553CA5A5AA8EF1A987C765ACB0697DA274CC15B0E0982BDDF0A33238A71E7BFC696995C63C1811AFBCE260A572EB8C97E98A121277C0ACAE947F1570CA3DDD169CA35A05BFC75B39C72FD4065E8CAF73D4AF6A93E470E57C81357916F7B9092142EF907FEB54B4C5E6776E9556092621FA36334C2BA89C975AB92ACAE571A5BD20016C2734825C8E51C5C65D8E739EA70D36F65BF0FC82A34839CD38687CAE15A7050C0638C99595AA3503B2DB3C7BA4749A183EC4385AAE3AA7BE1AC9E36CD9D8C7610B9EBF9AD196300E5BF009697A555238F9B8ADC86B39160F07DCB2A96A62B35DB8CFBE4BC2DAEEEB0F9FB7A7EDF0E433CA2958F7410B7EDE9CF5EC3C7FAEF219CE56C184FA2D88A2C98281AACB7C4696938A63296C561DF0A6C4ABDF79D7D4EB499510B13E12F40841A839AEABEC09EEAFC19533175B35E3949ED3614C2D519D9F1F792EBA55AEE952FEB5DBE46C9B3197D3B6F69F24F3ABF3B5E645074FC991CB015A82B790716F6AB74AC334C3F8A21D45D36AEB047C7C4B2F44D1ED65729C99F23AFEB8B4B255466A267CE0AE8AA8DAC48A660B632EDE2173DDCA542F67FA571E5C9FDC86C4B77A85C68FB89E5E18CD8FAA0A85D42F8429A95FC8CB13FE092BAACB4882349648C53E5775AE29267C4A1FF0FB061ECCB20C750C5515E748278551DAC31C55E6FE04D5824F94531E7869571685B15E166BEA08A25F93C3475C0E3416CEA653F9CD038EB20CA66BE72977AC122B6AC28CADF17921052AB3ACCADA053A0FA30E24B8766DF6322B5848A5995F514129A3DB3CF168720A3F852BE4AA8980A989A2D1A83D3C00F8AB6A56149D9B04D751C4F9A17F38397FFBF47EF7BAE95363F1ED73F7AF9D3BC9709D5A53EAE4ED140EF7AFA3F01867EE0F9DBB35871D21A516134A381A92229C8E8959E4E8143378567A4CB9E3097E61EC76B37111CD43230F616BF6A6E7A901AEF3899A2DC5C6F292A67A899AA0608BC7B09B95F721E1EDC8F663CEE711F2ADA536BDC5A505DBA1B47B46197A4AA2E8A9694CD785B7541BFF66C3A0E09D78B18ACB76144D1885A7CDC428FA196AE8EDEF42AFD31B5EA16A898D41E8047665ECF08D63EB51835B8DEAD7A34E35FB31470990B5C60641D5688EA29D9DB3E0B4929D1DF8DB7B5595D795E175BD36ABF0CABEF26C491BEE935F71CF60DCEB632EF67F30EE763087C9F47C1742859DACE337CA645528F54AA5DC6F7830BA610120829AE3CEE1B833643AC2D87371B3C2D6A72465722EDA2A5DCACB4703B9D11CBE57EFC5D6302F2D6715817DD7BE4CF960DCED36949F84994AA742FD08E6EAE5E67A025F92E02083F9E5EDADC2E27933499C4E53F2BEB24C6774EB0DAC8CBD6051A0ACDF0058F8BF42777FDC1F34744F59E5728DA3A62DC57ED57407068DF11063C91740E65B94432C21DC94D692E6F72BCC49C554B3FBC0D6153E86E3417FC3875E723295CA6BF14BDB6C08372060EA975150C5475B706CC36BF3D5F612AFF1D2ED55BC9CD4944F84F219BFCEDCC639EF989DFF0FC07B7FDCEFC3D11321C5D8E7DFB7467CB5D72DD837F1DCE4F4CAB606DB22FEE3217530EE776E4214471F1BA184A536E012F808238A1FF2325B8CBCB64C6FFFD606B1D136DD6C0A8371A7F3E352EC8F87073F2EA895DE28F79D30DD1F0F6B3B97BE5EEDC6A7260FB76F4246B1811ABFBB52BA727A8CB02243CF47F3D66BAF9B705F07A8B59237A86F98EAED42F760DC1DFE7110F95C365445B8C1F8A07E2319F6E54A7985392FE5825CC80926F50E2FB3B93F1E0C6F42F40F13DFD5656AB7F311BF27288BCC602221757D198F5D8DA4E595B18785DFA284C55BB4D01D770E7F98163610DCE843F02977779A1B61BABE4865583EABEA74898D9E2B9B5787CB15EF703DCB9DC371AFF72E8675C93224859265C26DC2F946D8E10AC8B6C45DA3DF2B2C1C8C87A36D83EAD606F5B14A17AF1D4427F98C12BE71E9DE745CDC4663DD5DE80EC7C31B35D61CDADBAA122DD6A7F396CD34F652EB4AF2BB7750947237F1D87B9788EB96014B45B7FBA3D0BD4CF69211FC78EC763A1BBF7B5D7CF506FFBF783B8E9A33E02ABE93CAF28CF35BDA44078EDF70A2E3274ECDB26BB8ED6D83BA739B94A03BBA01FAC2F1C084C8C6DCE33786E1CADE352EAB8E86F6C7C3214C423414827689862A4FC797A46F714ACABDC6EA9C643D5DCB8308CF1AB9F4694ABF6524CA33AEF9D4781DDB61B3DBE7D63B2935A9D3DC6293E65600A237B0F9666FF607689BB7D11D6DB0799DE61E6096DDA31FE0AB6491E1A504E35D5DF57576256407E3617F0BA9DDC35B21F5E056A3DE9C5AD7A39AA4FA341CF12A2F30AE34C2D7F3DDEEA593256447D263C95B454BCC2EE7286FCB216E92F27017BA9DF1E0F6B1EB6DF3DB8A72F7FF5174F03ACBEF12C8CBEDB35C79BEC055571430B6C6B94B99E40D7C6F191C5E36B6E12E74466FCD05CCCC915D52022E46EFE573104D5ECE9AB0EF8A9544CDE5AA9BE7DDA0DB0D4692F347151508E5E2E4A51871B78A75E4A2FA2646AD0A4DC127359EC679DEE00DD9846CE670DCB9647D4DE9A85E54B9EBAF14AE6826B02B8B7BDEDC4B82D09B2B8B37DAC06D9CCD40B29C5EE78F9BE256CB74DF29AFBBEAD3E83597C6547B9BA84EFA9A4ADB51B86C59A512994953498D7538E7A6D3676F2BBDDDC670FA21B6DAFFD1C4F75A6C79E57CAACD9C2F1155561EF82AC3B722B6BAD55AF15EF07745EE922AFA12B76D1535DE2D69A7EDB2D53BDB745F1C4DFFCFB6786734EE1DBE11841FF017312B14AB47BF95C9545F942D0545B703D5B5EEFC260F110AAE83FD6D30F7DE76A67EFBCB5F35456EA8EEF3C364F359DCE67DDD545E6D15A1A27FB4E4F7E0339529BFE68F0CAC8AC3FE9A4A380AE8304D2D89EC335A5256FB5276945E89830CE15650488884FCBAE0E390EF3B2D555262D694E4002F2D8B6B58F275A9840AD2897C62A1ABEFE5AA779045F87245F1DBE2C9D1670F77611A6346F7CE157FFDC79545AA9CFF3116182BBFDE65B5AF28CBF87FB9995F7D1C557D6553571EE784BEB4A134543A7E1DF5E2C58B1711FF7BE3F5F328FAEE3FFEE5FB7FF8B7EF7EFFAFFFF5DBFFFCEF5FFFF6AF6FBEDC0CDFFFEAD7FFF3EFFFFCDDEFBFF9EE777FFFFD2FFFEEFBDFFEEE32F1FF03DD0AD4E6F8380000000000000000000000050A03E6889100000000080A06E697A0E6B39500000000080A06E79BB4E68EA500000000080A06E8AEBFE997AE00000000080A06E7BD91E7AB9900000000080A06EFBC8CE4BD8600000000050A03E6889100000000080A06E58FAFE4BBA500000000080A06E5918AE8AF8900000000050A03E4BDA000000000080A06E585B3E4BA8E00000000050A03E8AFA500000000080A06E7BD91E7AB9900000000080A06E79A84E4B88000000000050A03E4BA9B00000000080A06E4BFA1E681AF00000000050A03E3808200000000080A06E6A0B9E68DAE00000000050A03E6889100000000050A03E6898000000000050A03E79FA500000000050A03EFBC8C00000000060A044F70656E00000000040A02414900000000050A03E79A8400000000080A06E78AB6E6808100000000080A06E9A1B5E99DA200000000050A03EFBC8800000000030A015B00000000080A0673746174757300000000070A052E6F70656E00000000040A02616900000000060A042E636F6D00000000040A025D2800000000070A05687474707300000000050A033A2F2F00000000080A0673746174757300000000070A052E6F70656E00000000040A02616900000000060A042E636F6D00000000040A022F2900000000050A03EFBC8900000000080A06E68F90E4BE9B00000000050A03E4BA8600000000080A06E69C89E585B300000000060A044F70656E00000000040A02414900000000080A06E69C8DE58AA100000000050A03E79A8400000000080A06E8BF90E8A18C00000000080A06E78AB6E6808100000000050A03E5928C00000000080A06E7BBB4E68AA400000000080A06E4BFA1E681AF00000000050A03E3808200000000080A06E794A8E688B700000000080A06E58FAFE4BBA500000000080A06E69FA5E79C8B00000000080A06E69C8DE58AA100000000050A03E79A8400000000080A06E6ADA3E5B8B800000000080A06E8BF90E8A18C00000000080A06E697B6E997B400000000050A03E3808100000000050A03E6958500000000050A03E99A9C00000000080A06E68AA5E5918A00000000080A06E4BBA5E58F8A00000000080A06E7BBB4E68AA400000000080A06E9809AE79FA500000000050A03E7AD8900000000080A06E58685E5AEB9000000000B0A09E38082E6ADA4E5A49600000000050A03EFBC8C00000000080A06E794A8E688B700000000050A03E8BF9800000000080A06E58FAFE4BBA500000000050A03E8AEA200000000050A03E9988500000000080A06E794B5E5AD9000000000080A06E982AEE4BBB600000000050A03E6889600000000080A06E79FADE4BFA100000000080A06E9809AE79FA500000000080A06EFBC8CE4BBA500000000050A03E4BEBF00000000050A03E59CA800000000080A06E58F91E7949F00000000080A06E4BA8BE4BBB600000000050A03E697B600000000080A06E58F8AE697B600000000080A06E88EB7E58F9600000000080A06E69BB4E696B000000000050A03E3808202000000027B7D \ No newline at end of file diff --git a/tools/get-token/Cargo.lock b/tools/get-token/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..decbdf64c45e74ffcc7f6f79bc3c391af7247533 --- /dev/null +++ b/tools/get-token/Cargo.lock @@ -0,0 +1,189 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cc" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "get-token" +version = "0.1.0" +dependencies = [ + "rusqlite", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/tools/get-token/Cargo.toml b/tools/get-token/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..afe081b7a5b8e5f44ff570d83294c62006710086 --- /dev/null +++ b/tools/get-token/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "get-token" +version = "0.1.0" +edition = "2021" + +[dependencies] +rusqlite = { version = "0.32.1", default-features = false, features = ["bundled"] } + +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +opt-level = 3 diff --git a/tools/get-token/README.md b/tools/get-token/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dc650b225a78885455fdd3ee23349ea06d4b0896 --- /dev/null +++ b/tools/get-token/README.md @@ -0,0 +1,58 @@ +# Cursor Token 获取工具 + +这个工具用于从 Cursor 编辑器的本地数据库中获取访问令牌。 + +## 系统要求 + +- Rust 编程环境 +- Cargo 包管理器 + +## 构建说明 + +### Windows + +1. 安装 Rust + ```powershell + winget install Rustlang.Rust + # 或访问 https://rustup.rs/ 下载安装程序 + ``` + +2. 克隆项目并构建 + ```powershell + git clone + cd get-token + cargo build --release + ``` + +3. 构建完成后,可执行文件位于 `target/release/get-token.exe` + +### macOS + +1. 安装 Rust + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + +2. 克隆项目并构建 + ```bash + git clone + cd get-token + cargo build --release + ``` + +3. 构建完成后,可执行文件位于 `target/release/get-token` + +## 使用方法 + +直接运行编译好的可执行文件即可: + +- Windows: `.\target\release\get-token.exe` +- macOS: `./target/release/get-token` + +程序将自动查找并显示 Cursor 编辑器的访问令牌。 + +## 注意事项 + +- 确保 Cursor 编辑器已经安装并且至少登录过一次 +- Windows 数据库路径:`%USERPROFILE%\AppData\Roaming\Cursor\User\globalStorage\state.vscdb` +- macOS 数据库路径:`~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` \ No newline at end of file diff --git a/tools/get-token/src/main.rs b/tools/get-token/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..305ea2f4d9bf6d4c4bb774b6db080fd2d724a8b3 --- /dev/null +++ b/tools/get-token/src/main.rs @@ -0,0 +1,31 @@ +use rusqlite::Connection; +use std::env; +use std::path::PathBuf; + +fn main() { + let home_dir = env::var("HOME") + .or_else(|_| env::var("USERPROFILE")) + .unwrap(); + let db_path = if cfg!(target_os = "windows") { + PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\User\globalStorage\state.vscdb") + } else if cfg!(target_os = "linux") { + PathBuf::from(home_dir).join(".config/Cursor/User/globalStorage/state.vscdb") + } else { + PathBuf::from(home_dir) + .join("Library/Application Support/Cursor/User/globalStorage/state.vscdb") + }; + + match Connection::open(&db_path) { + Ok(conn) => { + match conn.query_row( + "SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'", + [], + |row| row.get::<_, String>(0), + ) { + Ok(token) => println!("访问令牌: {}", token.trim()), + Err(err) => eprintln!("获取令牌时出错: {}", err), + } + } + Err(err) => eprintln!("无法打开数据库: {}", err), + } +} diff --git a/tools/reset-telemetry/Cargo.lock b/tools/reset-telemetry/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..c03053d93085333c13313adec43db6c0195548d0 --- /dev/null +++ b/tools/reset-telemetry/Cargo.lock @@ -0,0 +1,273 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "reset-telemetry" +version = "0.1.0" +dependencies = [ + "rand", + "serde_json", + "sha2", + "uuid", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "uuid" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/tools/reset-telemetry/Cargo.toml b/tools/reset-telemetry/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..b4fd46f53de10e47bd0d41ca01e29ce633a4e91e --- /dev/null +++ b/tools/reset-telemetry/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "reset-telemetry" +version = "0.1.0" +edition = "2021" + +[dependencies] +rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } +serde_json = "1.0.137" +sha2 = { version = "0.10.8", default-features = false } +uuid = { version = "1.12.1", features = ["v4"] } + +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +opt-level = 3 diff --git a/tools/reset-telemetry/src/main.rs b/tools/reset-telemetry/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..87529cc85db1af98e15f8b0988184a40ec414315 --- /dev/null +++ b/tools/reset-telemetry/src/main.rs @@ -0,0 +1,73 @@ +use rand::RngCore; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::env; +use std::fs; +use std::path::PathBuf; +use uuid::Uuid; + +fn main() -> std::io::Result<()> { + // 获取用户主目录路径 + let home_dir = env::var("HOME") + .or_else(|_| env::var("USERPROFILE")) + .unwrap(); + + // 构建storage.json的路径 + let db_path = if cfg!(target_os = "windows") { + PathBuf::from(home_dir.clone()) + .join(r"AppData\Roaming\Cursor\User\globalStorage\storage.json") + } else if cfg!(target_os = "linux") { + PathBuf::from(home_dir.clone()).join(".config/Cursor/User/globalStorage/storage.json") + } else { + PathBuf::from(home_dir.clone()) + .join("Library/Application Support/Cursor/User/globalStorage/storage.json") + }; + + // 构建machineid文件的路径 + let machine_id_path = if cfg!(target_os = "windows") { + PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\machineid") + } else if cfg!(target_os = "linux") { + PathBuf::from(home_dir).join(".config/Cursor/machineid") + } else { + PathBuf::from(home_dir).join("Library/Application Support/Cursor/machineid") + }; + + // 读取并更新storage.json + let mut content: Value = if db_path.exists() { + let content = fs::read_to_string(&db_path)?; + serde_json::from_str(&content)? + } else { + json!({}) + }; + + // 生成新的遥测ID + content["telemetry.macMachineId"] = json!(generate_sha256_hash()); + content["telemetry.sqmId"] = json!(generate_sqm_id()); + content["telemetry.machineId"] = json!(generate_sha256_hash()); + content["telemetry.devDeviceId"] = json!(generate_device_id()); + + // 写入更新后的storage.json + fs::write(&db_path, serde_json::to_string_pretty(&content)?)?; + + // 更新machineid文件 + fs::write(&machine_id_path, generate_device_id())?; + + println!("遥测ID已重置成功!"); + Ok(()) +} + +fn generate_sha256_hash() -> String { + let mut rng = rand::thread_rng(); + let mut bytes = [0u8; 32]; + rng.fill_bytes(&mut bytes); + let hash = Sha256::digest(&bytes); + format!("{:x}", hash) +} + +fn generate_sqm_id() -> String { + format!("{{{}}}", Uuid::new_v4().to_string().to_uppercase()) +} + +fn generate_device_id() -> String { + Uuid::new_v4().to_string() +} diff --git a/worker.js b/worker.js new file mode 100644 index 0000000000000000000000000000000000000000..bf733a334ea87f03c3e6d11b32dd4fdf9a1eb77a --- /dev/null +++ b/worker.js @@ -0,0 +1,59 @@ +addEventListener('fetch', e => { + e.respondWith(handleRequest(e.request)); +}); + +async function handleRequest(request) { + try { + // 获取目标主机 + const targetHost = request.headers.get("x-co"); + + // 允许的主机和路径列表 + const allowedHosts = ["api2.cursor.sh", "www.cursor.com"]; + const allowedPaths = [ + "/aiserver.v1.AiService/StreamChat", + "/aiserver.v1.AiService/StreamChatWeb", + "/auth/full_stripe_profile", + "/api/usage", + "/api/auth/me" + ]; + + const url = new URL(request.url); + + // 验证请求 + if (!targetHost || !allowedHosts.includes(targetHost) || !allowedPaths.includes(url.pathname)) { + return new Response(null, { status: 403 }); + } + + // 处理请求头 + const headers = new Headers(request.headers); + headers.delete("x-co"); + headers.set("Host", targetHost); + + // 转发请求 + const response = await fetch( + `https://${targetHost}${url.pathname}${url.search}`, + { + method: request.method, + headers: headers, + body: request.body + } + ); + + // 处理响应 + const responseHeaders = new Headers(response.headers); + responseHeaders.set("Access-Control-Allow-Origin", "*"); + + return new Response(response.body, { + status: response.status, + headers: responseHeaders + }); + + } catch (error) { + // 错误处理 + console.error('Request failed:', error); + return new Response("Internal Server Error", { + status: 500, + headers: { "Content-Type": "text/plain" } + }); + } +}