diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..a87a2ec6f7053816aff52784defe72cd44210037 --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +# 当前配置为默认值,请根据需要修改 + +# 服务器监听端口 +PORT=3000 + +# 路由前缀,必须以 / 开头(如果不为空) +ROUTE_PREFIX= + +# 最高权限的认证令牌,必填 +AUTH_TOKEN= + +# 共享的认证令牌,仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限 +SHARED_AUTH_TOKEN= + +# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次 +ENABLE_STREAM_CHECK=true + +# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块 +INCLUDE_STOP_REASON_STREAM=true + +# 令牌文件路径 +TOKEN_FILE=.token + +# 令牌列表文件路径 +TOKEN_LIST_FILE=.token-list + +# (实验性)是否启用慢速池(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 + +# 默认提示词 +DEFAULT_INSTRUCTIONS="Respond in Chinese by default" + +# 反向代理服务器主机名,你猜怎么用 +REVERSE_PROXY_HOST= + +# 请求体大小限制(单位为MB) +# 默认为2MB (2,097,152 字节) +REQUEST_BODY_LIMIT_MB=2 + +# OpenAI 请求时,token 和 checksum 的分隔符 +TOKEN_DELIMITER=, \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6be81ea7f35146935cac6c702d00b26125654177 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/target +/get-token/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 +/cursor-api +/cursor-api.exe +/release + +/*.py +/logs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..6a849fa0626a24485c1a1ab2035653230886ab4d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2360 @@ +# 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 = "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 = "async-trait" +version = "0.1.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "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.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" + +[[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 = "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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +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", + "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.3.4" +dependencies = [ + "axum", + "base64", + "bytes", + "chrono", + "dotenvy", + "flate2", + "futures", + "gif", + "hex", + "image", + "paste", + "prost", + "prost-build", + "rand", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2", + "sysinfo", + "tokio", + "tokio-stream", + "tower-http", + "urlencoding", + "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", +] + +[[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 = "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 = "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", +] + +[[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.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", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[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 = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +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 = "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.7.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", +] + +[[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 = "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", +] + +[[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", + "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", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost", +] + +[[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 = "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 = "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 = "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-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 = "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.7.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 = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.7.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", +] + +[[package]] +name = "serde_json" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +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 = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[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 = "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 = "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", +] + +[[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.7.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 = "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 = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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", + "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", +] + +[[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-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.7.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 = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +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", + "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", + "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", +] + +[[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", +] + +[[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 = "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", + "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", +] + +[[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", + "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", +] + +[[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..4c01f186f64fe78dd85142d2ccc7f2726551d7d3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "cursor-api" +version = "0.1.3-rc.3.4" +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.7.9", 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"] } +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"] } +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", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } +serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } +serde_json = "1.0.135" +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"] } +tokio-stream = { version = "0.1.17", features = ["time"] } +tower-http = { version = "0.6.2", features = ["cors", "limit"] } +urlencoding = "2.1.3" +uuid = { version = "1.11.1", features = ["v4"] } + +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +opt-level = 3 diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000000000000000000000000000000000000..d7cea176ac68690c4bd07941a0b8b35562de3d2c --- /dev/null +++ b/Cross.toml @@ -0,0 +1,14 @@ +[target.x86_64-unknown-linux-gnu] +pre-build = [ + "set -e", + "apt-get update", + "apt-get install -y --no-install-recommends build-essential protobuf-compiler pkg-config libssl-dev nodejs npm", + "rm -rf /var/lib/apt/lists/*" +] + +[target.x86_64-unknown-freebsd] +pre-build = [ + "pkg update", + "pkg install -y node20 www/npm protobuf ca_root_nss bash gmake pkgconf openssl", + "export SSL_CERT_FILE=/etc/ssl/cert.pem" +] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..012409b5117f5e698a4901db5eb20044833c69d8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# AMD64 构建阶段 +FROM --platform=linux/amd64 rust:1.84.0-slim-bookworm as builder-amd64 +WORKDIR /app +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \ + && rm -rf /var/lib/apt/lists/* +COPY . . +ENV RUSTFLAGS="-C link-arg=-s" +RUN cargo build --release && \ + cp target/release/cursor-api /app/cursor-api + +# ARM64 构建阶段 +FROM --platform=linux/arm64 rust:1.84.0-slim-bookworm as builder-arm64 +WORKDIR /app +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \ + && rm -rf /var/lib/apt/lists/* +COPY . . +ENV RUSTFLAGS="-C link-arg=-s" +RUN cargo build --release && \ + cp target/release/cursor-api /app/cursor-api + +# AMD64 运行阶段 +FROM --platform=linux/amd64 debian:bookworm-slim as run-amd64 +WORKDIR /app +ENV TZ=Asia/Shanghai +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder-amd64 /app/cursor-api . + +# ARM64 运行阶段 +FROM --platform=linux/arm64 debian:bookworm-slim as run-arm64 +WORKDIR /app +ENV TZ=Asia/Shanghai +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder-arm64 /app/cursor-api . + +# 通用配置 +FROM run-${TARGETARCH} +ENV PORT=3000 +EXPOSE ${PORT} +CMD ["./cursor-api"] \ No newline at end of file diff --git a/README.md b/README.md index 2814dd93f08851bc49bb4a852f08e95b7f6a7272..79420a81b5d833f5355beb99bee80af54ba92643 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ colorFrom: gray colorTo: pink sdk: docker pinned: false +app_port: 3000 --- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/build.rs b/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..a6557a5b60dec0ee891f033533943a852942768b --- /dev/null +++ b/build.rs @@ -0,0 +1,165 @@ +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::fs; +use std::io::Result; +use std::path::{Path, PathBuf}; +use std::process::Command; + +// 支持的文件类型 +const SUPPORTED_EXTENSIONS: [&str; 3] = ["html", "js", "css"]; + +fn check_and_install_deps() -> Result<()> { + let scripts_dir = Path::new("scripts"); + let node_modules = scripts_dir.join("node_modules"); + + if !node_modules.exists() { + println!("cargo:warning=Installing minifier dependencies..."); + let status = Command::new("npm") + .current_dir(scripts_dir) + .arg("install") + .status()?; + + if !status.success() { + panic!("Failed to install npm dependencies"); + } + println!("cargo:warning=Dependencies installed successfully"); + } + Ok(()) +} + +fn get_files_hash() -> Result> { + let mut file_hashes = HashMap::new(); + let static_dir = Path::new("static"); + + if static_dir.exists() { + for entry in fs::read_dir(static_dir)? { + let entry = entry?; + let path = entry.path(); + + // 检查是否是支持的文件类型,且不是已经压缩的文件 + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if SUPPORTED_EXTENSIONS.contains(&ext) && !path.to_string_lossy().contains(".min.") + { + let content = fs::read(&path)?; + let mut hasher = Sha256::new(); + hasher.update(&content); + let hash = format!("{:x}", hasher.finalize()); + file_hashes.insert(path, hash); + } + } + } + } + + Ok(file_hashes) +} + +fn load_saved_hashes() -> Result> { + let hash_file = Path::new("scripts/.asset-hashes.json"); + if hash_file.exists() { + let content = fs::read_to_string(hash_file)?; + let hash_map: HashMap = serde_json::from_str(&content)?; + Ok(hash_map + .into_iter() + .map(|(k, v)| (PathBuf::from(k), v)) + .collect()) + } else { + Ok(HashMap::new()) + } +} + +fn save_hashes(hashes: &HashMap) -> Result<()> { + let hash_file = Path::new("scripts/.asset-hashes.json"); + let string_map: HashMap = hashes + .iter() + .map(|(k, v)| (k.to_string_lossy().into_owned(), v.clone())) + .collect(); + let content = serde_json::to_string_pretty(&string_map)?; + fs::write(hash_file, content)?; + Ok(()) +} + +fn minify_assets() -> Result<()> { + // 获取现有文件的哈希 + let current_hashes = get_files_hash()?; + + if current_hashes.is_empty() { + println!("cargo:warning=No files to minify"); + return Ok(()); + } + + // 加载保存的哈希值 + let saved_hashes = load_saved_hashes()?; + + // 找出需要更新的文件 + let files_to_update: Vec<_> = current_hashes + .iter() + .filter(|(path, current_hash)| { + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + let min_path = path.with_file_name(format!( + "{}.min.{}", + path.file_stem().unwrap().to_string_lossy(), + ext + )); + + // 检查压缩后的文件是否存在 + if !min_path.exists() { + return true; + } + + // 检查原始文件是否发生变化 + saved_hashes + .get(*path) + .map_or(true, |saved_hash| saved_hash != *current_hash) + }) + .map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned()) + .collect(); + + if files_to_update.is_empty() { + println!("cargo:warning=No files need to be updated"); + return Ok(()); + } + + println!("cargo:warning=Minifying {} files...", files_to_update.len()); + + // 运行压缩脚本 + let status = Command::new("node") + .arg("scripts/minify.js") + .args(&files_to_update) + .status()?; + + if !status.success() { + panic!("Asset minification failed"); + } + + // 保存新的哈希值 + save_hashes(¤t_hashes)?; + + Ok(()) +} + +fn main() -> Result<()> { + // Proto 文件处理 + println!("cargo:rerun-if-changed=src/chat/aiserver/v1/lite.proto"); + let mut config = prost_build::Config::new(); + // config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]"); + // config.type_attribute( + // "aiserver.v1.ThrowErrorCheckRequest", + // "#[derive(serde::Serialize, serde::Deserialize)]" + // ); + config + .compile_protos(&["src/chat/aiserver/v1/lite.proto"], &["src/chat/aiserver/v1/"]) + .unwrap(); + + // 静态资源文件处理 + println!("cargo:rerun-if-changed=scripts/minify.js"); + println!("cargo:rerun-if-changed=scripts/package.json"); + println!("cargo:rerun-if-changed=static"); + + // 检查并安装依赖 + check_and_install_deps()?; + + // 运行资源压缩 + minify_assets()?; + + Ok(()) +} diff --git a/get-token/Cargo.lock b/get-token/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..decbdf64c45e74ffcc7f6f79bc3c391af7247533 --- /dev/null +++ b/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/get-token/Cargo.toml b/get-token/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..afe081b7a5b8e5f44ff570d83294c62006710086 --- /dev/null +++ b/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/get-token/README.md b/get-token/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dc650b225a78885455fdd3ee23349ea06d4b0896 --- /dev/null +++ b/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/get-token/src/main.rs b/get-token/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..98efd111a78bb950b9cbba23ea124392e0d673be --- /dev/null +++ b/get-token/src/main.rs @@ -0,0 +1,29 @@ +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 { + 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/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..e4c1ed6b3b7026a20f959dce38ca61fcdcee7210 --- /dev/null +++ b/scripts/minify.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +const { minify: minifyHtml } = require('html-minifier-terser'); +const { minify: minifyJs } = require('terser'); +const CleanCSS = require('clean-css'); +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 { + const ext = path.extname(inputPath).toLowerCase(); + const content = fs.readFileSync(inputPath, 'utf8'); + let minified; + + 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) { + const inputPath = path.join(staticDir, file); + const ext = path.extname(file); + const 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..82715b06bf815a5b902ffc98d5b08a822ee0d457 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,267 @@ +{ + "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", + "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/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/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/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/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" + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000000000000000000000000000000000000..c1c08daa7c4adec38812d576805bf0774d737854 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,13 @@ +{ + "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", + "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/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..f990f96c1941ad15235163440dd8150d8eed66a8 --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,229 @@ +use super::{ + constant::AUTHORIZATION_BEARER_PREFIX, + lazy::AUTH_TOKEN, + model::AppConfig, +}; +use crate::common::models::{ + config::{ConfigData, ConfigUpdateRequest}, + ApiStatus, ErrorResponse, NormalResponse, +}; +use axum::{ + http::{header::AUTHORIZATION, HeaderMap, StatusCode}, + Json, +}; + +// 定义处理更新操作的宏 +macro_rules! handle_update { + ($request:expr, $field:ident, $update_fn:expr, $field_name:expr) => { + if let Some($field) = $request.$field { + if let Err(e) = $update_fn($field) { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(500), + error: Some(format!("更新 {} 失败: {}", $field_name, e)), + message: None, + }), + )); + } + } + }; +} + +// 定义处理重置操作的宏 +macro_rules! handle_reset { + ($request:expr, $field:ident, $reset_fn:expr, $field_name:expr) => { + if $request.$field.is_some() { + if let Err(e) = $reset_fn() { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + status: ApiStatus::Failed, + code: Some(500), + error: Some(format!("重置 {} 失败: {}", $field_name, e)), + message: None, + }), + )); + } + } + }; +} + +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), + enable_stream_check: AppConfig::get_stream_check(), + include_stop_stream: AppConfig::get_stop_stream(), + vision_ability: AppConfig::get_vision_ability(), + enable_slow_pool: AppConfig::get_slow_pool(), + enable_all_claude: AppConfig::get_allow_claude(), + check_usage_models: AppConfig::get_usage_check(), + }), + 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_update!( + request, + enable_stream_check, + AppConfig::update_stream_check, + "enable_stream_check" + ); + handle_update!( + request, + include_stop_stream, + AppConfig::update_stop_stream, + "include_stop_stream" + ); + handle_update!( + request, + vision_ability, + AppConfig::update_vision_ability, + "vision_ability" + ); + handle_update!( + request, + enable_slow_pool, + AppConfig::update_slow_pool, + "enable_slow_pool" + ); + handle_update!( + request, + enable_all_claude, + AppConfig::update_allow_claude, + "enable_all_claude" + ); + handle_update!( + request, + check_usage_models, + AppConfig::update_usage_check, + "check_usage_models" + ); + + 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_reset!( + request, + enable_stream_check, + AppConfig::reset_stream_check, + "enable_stream_check" + ); + handle_reset!( + request, + include_stop_stream, + AppConfig::reset_stop_stream, + "include_stop_stream" + ); + handle_reset!( + request, + vision_ability, + AppConfig::reset_vision_ability, + "vision_ability" + ); + handle_reset!( + request, + enable_slow_pool, + AppConfig::reset_slow_pool, + "enable_slow_pool" + ); + handle_reset!( + request, + enable_all_claude, + AppConfig::reset_allow_claude, + "enable_all_claude" + ); + handle_reset!( + request, + check_usage_models, + AppConfig::reset_usage_check, + "check_usage_models" + ); + + 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..3ee795b6fd90e655b695cc2ac2f719d41ae145a5 --- /dev/null +++ b/src/app/constant.rs @@ -0,0 +1,72 @@ +macro_rules! def_pub_const { + ($name:ident, $value:expr) => { + pub const $name: &'static str = $value; + }; +} + +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!(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_TOKENINFO_PATH, "/tokeninfo"); +def_pub_const!(ROUTE_GET_TOKENINFO_PATH, "/get-tokeninfo"); +def_pub_const!(ROUTE_UPDATE_TOKENINFO_PATH, "/update-tokeninfo"); +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!(DEFAULT_TOKEN_FILE_NAME, ".token"); +def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".token-list"); + +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_UPDATE_CONFIG, "无法更新配置"); +def_pub_const!(ERR_RESET_CONFIG, "无法重置配置"); +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..c3cb7bf89fdca15d55e26dcbcc4a5092b563dad3 --- /dev/null +++ b/src/app/lazy.rs @@ -0,0 +1,120 @@ +use crate::{ + app::constant::{ + CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_FILE_NAME, DEFAULT_TOKEN_LIST_FILE_NAME, + EMPTY_STRING, + }, + common::utils::{parse_ascii_char_from_env, parse_string_from_env}, +}; +use std::sync::LazyLock; + +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_FILE, env: "TOKEN_FILE", default: DEFAULT_TOKEN_FILE_NAME); +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); + +def_pub_static!(SHARED_AUTH_TOKEN, env: "SHARED_AUTH_TOKEN", default: EMPTY_STRING); + +pub static USE_SHARE: LazyLock = LazyLock::new(|| !SHARED_AUTH_TOKEN.is_empty()); + +pub static TOKEN_DELIMITER: LazyLock = LazyLock::new(|| { + let delimiter = parse_ascii_char_from_env("TOKEN_DELIMITER", ','); + if delimiter.is_ascii_alphabetic() + || delimiter.is_ascii_digit() + || delimiter == '+' + || delimiter == '/' + { + ',' + } else { + delimiter + } +}); + +pub static TOKEN_DELIMITER_LEN: LazyLock = LazyLock::new(|| TOKEN_DELIMITER.len_utf8()); + +pub static USE_PROXY: LazyLock = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty()); + +pub static CURSOR_API2_CHAT_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_API2_HOST + }; + format!("https://{}/aiserver.v1.AiService/StreamChat", host) +}); + +pub static CURSOR_API2_STRIPE_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_API2_HOST + }; + format!("https://{}/auth/full_stripe_profile", host) +}); + +pub static CURSOR_USAGE_API_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_HOST + }; + format!("https://{}/api/usage", host) +}); + +pub static CURSOR_USER_API_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_HOST + }; + format!("https://{}/api/auth/me", host) +}); + +// pub static DEBUG: LazyLock = LazyLock::new(|| parse_bool_from_env("DEBUG", false)); + +// #[macro_export] +// macro_rules! debug_println { +// ($($arg:tt)*) => { +// if *crate::app::statics::DEBUG { +// println!($($arg)*); +// } +// }; +// } diff --git a/src/app/model.rs b/src/app/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..ef82e6054ca978cd70044cb84f4143c76056ea82 --- /dev/null +++ b/src/app/model.rs @@ -0,0 +1,338 @@ +use crate::{ + app::constant::{ + ERR_INVALID_PATH, ERR_RESET_CONFIG, ERR_UPDATE_CONFIG, ROUTE_ABOUT_PATH, ROUTE_CONFIG_PATH, + ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_SHARED_JS_PATH, + ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENINFO_PATH, ROUTE_API_PATH, + }, + common::models::userinfo::TokenProfile, +}; +use crate::chat::model::Message; +use std::sync::{LazyLock, RwLock}; +use serde::{Deserialize, Serialize}; + +// 页面内容类型枚举 +#[derive(Clone, Serialize, Deserialize)] +#[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 + } +} + +mod usage_check; +pub use usage_check::UsageCheck; + +// 静态配置 +#[derive(Clone)] +pub struct AppConfig { + stream_check: bool, + stop_stream: bool, + vision_ability: VisionAbility, + slow_pool: bool, + allow_claude: bool, + pages: Pages, + usage_check: UsageCheck, +} + +#[derive(Serialize, Deserialize, Clone)] +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(), + } + } +} + +impl Default for VisionAbility { + fn default() -> Self { + Self::Base64 + } +} + +#[derive(Clone, Default)] +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 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()) +}); + +impl Default for AppConfig { + fn default() -> Self { + Self { + stream_check: true, + stop_stream: true, + vision_ability: VisionAbility::Base64, + slow_pool: false, + allow_claude: false, + pages: Pages::default(), + usage_check: UsageCheck::Default, + } + } +} + +macro_rules! config_methods { + ($($field:ident: $type:ty, $default:expr;)*) => { + $( + paste::paste! { + pub fn []() -> $type { + APP_CONFIG + .read() + .map(|config| config.$field.clone()) + .unwrap_or($default) + } + + pub fn [](value: $type) -> Result<(), &'static str> { + if let Ok(mut config) = APP_CONFIG.write() { + config.$field = value; + Ok(()) + } else { + Err(ERR_UPDATE_CONFIG) + } + } + + pub fn []() -> Result<(), &'static str> { + if let Ok(mut config) = APP_CONFIG.write() { + config.$field = $default; + Ok(()) + } else { + Err(ERR_RESET_CONFIG) + } + } + } + )* + }; +} + +impl AppConfig { + pub fn init( + stream_check: bool, + stop_stream: bool, + vision_ability: VisionAbility, + slow_pool: bool, + allow_claude: bool, + ) { + if let Ok(mut config) = APP_CONFIG.write() { + config.stream_check = stream_check; + config.stop_stream = stop_stream; + config.vision_ability = vision_ability; + config.slow_pool = slow_pool; + config.allow_claude = allow_claude; + } + } + + config_methods! { + stream_check: bool, true; + stop_stream: bool, true; + slow_pool: bool, false; + allow_claude: bool, false; + } + + pub fn get_vision_ability() -> VisionAbility { + APP_CONFIG + .read() + .map(|config| config.vision_ability.clone()) + .unwrap_or_default() + } + + pub fn get_page_content(path: &str) -> Option { + APP_CONFIG.read().ok().map(|config| match path { + ROUTE_ROOT_PATH => config.pages.root_content.clone(), + ROUTE_LOGS_PATH => config.pages.logs_content.clone(), + ROUTE_CONFIG_PATH => config.pages.config_content.clone(), + ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content.clone(), + ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content.clone(), + ROUTE_SHARED_JS_PATH => config.pages.shared_js_content.clone(), + ROUTE_ABOUT_PATH => config.pages.about_content.clone(), + ROUTE_README_PATH => config.pages.readme_content.clone(), + ROUTE_API_PATH => config.pages.api_content.clone(), + _ => PageContent::default(), + }) + } + + pub fn get_usage_check() -> UsageCheck { + APP_CONFIG + .read() + .map(|config| config.usage_check.clone()) + .unwrap_or_default() + } + + pub fn update_vision_ability(new_ability: VisionAbility) -> Result<(), &'static str> { + if let Ok(mut config) = APP_CONFIG.write() { + config.vision_ability = new_ability; + Ok(()) + } else { + Err(ERR_UPDATE_CONFIG) + } + } + + pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> { + if let Ok(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_TOKENINFO_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, + _ => return Err(ERR_INVALID_PATH), + } + Ok(()) + } else { + Err(ERR_UPDATE_CONFIG) + } + } + + pub fn update_usage_check(rule: UsageCheck) -> Result<(), &'static str> { + if let Ok(mut config) = APP_CONFIG.write() { + config.usage_check = rule; + Ok(()) + } else { + Err(ERR_UPDATE_CONFIG) + } + } + + pub fn reset_vision_ability() -> Result<(), &'static str> { + if let Ok(mut config) = APP_CONFIG.write() { + config.vision_ability = VisionAbility::Base64; + Ok(()) + } else { + Err(ERR_RESET_CONFIG) + } + } + + pub fn reset_page_content(path: &str) -> Result<(), &'static str> { + if let Ok(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_TOKENINFO_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(), + _ => return Err(ERR_INVALID_PATH), + } + Ok(()) + } else { + Err(ERR_RESET_CONFIG) + } + } + + pub fn reset_usage_check() -> Result<(), &'static str> { + if let Ok(mut config) = APP_CONFIG.write() { + config.usage_check = UsageCheck::default(); + Ok(()) + } else { + Err(ERR_RESET_CONFIG) + } + } +} + +impl AppState { + pub fn new(token_infos: Vec) -> Self { + Self { + total_requests: 0, + active_requests: 0, + error_requests: 0, + request_logs: Vec::new(), + token_infos, + } + } +} + +// 请求日志 +#[derive(Serialize, Clone)] +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: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Serialize, Clone)] +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)] +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, + #[serde(default)] + pub token_list: Option, +} diff --git a/src/app/model/usage_check.rs b/src/app/model/usage_check.rs new file mode 100644 index 0000000000000000000000000000000000000000..b7c4848992ed5885e49010962ee7de6fc311cedb --- /dev/null +++ b/src/app/model/usage_check.rs @@ -0,0 +1,91 @@ +use crate::chat::constant::AVAILABLE_MODELS; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub enum UsageCheck { + None, + Default, + All, + Custom(Vec<&'static str>), +} + +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(","))?; + } + } + 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 = UsageCheckHelper::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(',') + .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) + } + } + }) + } +} diff --git a/src/chat.rs b/src/chat.rs new file mode 100644 index 0000000000000000000000000000000000000000..b55789216dc5afa5154ac3fba5cdda209b88c455 --- /dev/null +++ b/src/chat.rs @@ -0,0 +1,8 @@ +pub mod adapter; +pub mod aiserver; +pub mod constant; +pub mod error; +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..17e53c1f7528ee489e455544fa588e9bb86130e4 --- /dev/null +++ b/src/chat/adapter.rs @@ -0,0 +1,403 @@ +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use image::guess_format; +use prost::Message as _; +use uuid::Uuid; + +use crate::app::{ + constant::EMPTY_STRING, + lazy::DEFAULT_INSTRUCTIONS, + model::{AppConfig, VisionAbility}, +}; + +use super::{ + aiserver::v1::{ + conversation_message, image_proto, AzureState, 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) -> (String, 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, + }], + ); + } + + // 如果第一条是 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 let Some(image_url) = &content.image_url { + let url = image_url.url.clone(); + let result = + tokio::spawn(async move { fetch_image_data(&url).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, + }); + } + + (instructions, messages) +} + +async fn fetch_image_data( + url: &str, +) -> 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).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, +) -> Result<(Vec, Option), Box> { + let response = reqwest::get(url).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, +) -> Result, Box> { + // 在进入异步操作前获取并释放锁 + let enable_slow_pool = { + if AppConfig::get_slow_pool() { + Some(true) + } else { + None + } + }; + + let (instructions, messages) = process_chat_inputs(inputs).await; + + let explicit_context = if !instructions.trim().is_empty() { + Some(ExplicitContext { + context: instructions, + repo_context: None, + }) + } else { + None + }; + + 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: None, + quotes: vec![], + debug_info: None, + workspace_id: None, + external_links: vec![], + 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..f886b8b974c347fee41ad4ac57722bb43c3cfafe --- /dev/null +++ b/src/chat/aiserver/v1.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs")); 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/constant.rs b/src/chat/constant.rs new file mode 100644 index 0000000000000000000000000000000000000000..84a8d6ac7fbcf2c79fe4073cef77449848514052 --- /dev/null +++ b/src/chat/constant.rs @@ -0,0 +1,193 @@ +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!(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"); + +pub const AVAILABLE_MODELS: [Model; 21] = [ + Model { + id: CLAUDE_3_5_SONNET, + created: CREATED, + object: MODEL_OBJECT, + owned_by: ANTHROPIC, + }, + Model { + id: GPT_4, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: GPT_4O, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: CLAUDE_3_OPUS, + created: CREATED, + object: MODEL_OBJECT, + owned_by: ANTHROPIC, + }, + Model { + id: CURSOR_FAST, + created: CREATED, + object: MODEL_OBJECT, + owned_by: CURSOR, + }, + Model { + id: CURSOR_SMALL, + created: CREATED, + object: MODEL_OBJECT, + owned_by: CURSOR, + }, + Model { + id: GPT_3_5_TURBO, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: GPT_4_TURBO_2024_04_09, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: GPT_4O_128K, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: GEMINI_1_5_FLASH_500K, + created: CREATED, + object: MODEL_OBJECT, + owned_by: GOOGLE, + }, + Model { + id: CLAUDE_3_HAIKU_200K, + created: CREATED, + object: MODEL_OBJECT, + owned_by: ANTHROPIC, + }, + Model { + id: CLAUDE_3_5_SONNET_200K, + created: CREATED, + object: MODEL_OBJECT, + owned_by: ANTHROPIC, + }, + Model { + id: CLAUDE_3_5_SONNET_20241022, + created: CREATED, + object: MODEL_OBJECT, + owned_by: ANTHROPIC, + }, + Model { + id: GPT_4O_MINI, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: O1_MINI, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: O1_PREVIEW, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: O1, + created: CREATED, + object: MODEL_OBJECT, + owned_by: OPENAI, + }, + Model { + id: CLAUDE_3_5_HAIKU, + created: CREATED, + object: MODEL_OBJECT, + owned_by: ANTHROPIC, + }, + Model { + id: GEMINI_EXP_1206, + created: CREATED, + object: MODEL_OBJECT, + owned_by: GOOGLE, + }, + Model { + id: GEMINI_2_0_FLASH_THINKING_EXP, + created: CREATED, + object: MODEL_OBJECT, + owned_by: GOOGLE, + }, + Model { + id: GEMINI_2_0_FLASH_EXP, + created: CREATED, + object: MODEL_OBJECT, + owned_by: GOOGLE, + }, +]; + +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, +]; diff --git a/src/chat/error.rs b/src/chat/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..be4db01d99c71c59dc706df48188e90a1aa9ec74 --- /dev/null +++ b/src/chat/error.rs @@ -0,0 +1,169 @@ +use super::aiserver::v1::error_details::Error as ErrorType; +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, +} + +use crate::common::models::{ApiStatus, ErrorResponse as CommonErrorResponse}; + +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, + }; + } + ErrorResponse { + status: self.status_code(), + code: self.error.code.clone(), + error: Some(Error { + message: self.error.details[0].debug.details.title.clone(), + details: self.error.details[0].debug.details.detail.clone(), + value: self.error.details[0].value.clone(), + }), + } + } + + pub fn status_code(&self) -> u16 { + match ErrorType::from_str_name(&self.error.details[0].debug.error) { + Some(error) => match error { + ErrorType::Unspecified => 500, + ErrorType::BadApiKey + | ErrorType::InvalidAuthId + | ErrorType::AuthTokenNotFound + | ErrorType::AuthTokenExpired + | ErrorType::Unauthorized => 401, + ErrorType::NotLoggedIn + | ErrorType::NotHighEnoughPermissions + | ErrorType::AgentRequiresLogin + | ErrorType::ProUserOnly + | ErrorType::TaskNoPermissions => 403, + ErrorType::NotFound + | ErrorType::UserNotFound + | ErrorType::TaskUuidNotFound + | ErrorType::AgentEngineNotFound + | ErrorType::GitgraphNotFound + | ErrorType::FileNotFound => 404, + ErrorType::FreeUserRateLimitExceeded + | ErrorType::ProUserRateLimitExceeded + | ErrorType::OpenaiRateLimitExceeded + | ErrorType::OpenaiAccountLimitExceeded + | ErrorType::GenericRateLimitExceeded + | ErrorType::Gpt4VisionPreviewRateLimit + | ErrorType::ApiKeyRateLimit => 429, + ErrorType::BadRequest + | ErrorType::BadModelName + | ErrorType::SlashEditFileTooLong + | ErrorType::FileUnsupported + | ErrorType::ClaudeImageTooLarge => 400, + ErrorType::Deprecated + | ErrorType::FreeUserUsageLimit + | ErrorType::ProUserUsageLimit + | ErrorType::ResourceExhausted + | ErrorType::Openai + | ErrorType::MaxTokens + | ErrorType::ApiKeyNotSupported + | ErrorType::UserAbortedRequest + | ErrorType::CustomMessage + | ErrorType::OutdatedClient + | ErrorType::Debounced + | ErrorType::RepositoryServiceRepositoryIsNotInitialized => 500, + }, + None => 500, + } + } + + // pub fn is_expected(&self) -> bool { + // self.error.details[0].debug.is_expected.unwrap_or_default() + // } +} + +#[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_or(StatusCode::INTERNAL_SERVER_ERROR) + } + + pub fn native_code(&self) -> String { + self.code.replace("_", " ") + } + + 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, + EmptyMessage, +} + +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.details[0].debug.details.title), + StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"), + StreamError::EmptyMessage => write!(f, "empty message"), + } + } +} diff --git a/src/chat/model.rs b/src/chat/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..0091dc60dc9199c06b5f6505a330fc11416c339c --- /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 crate::app::model::{AppConfig, UsageCheck}; +use super::constant::USAGE_CHECK_MODELS; + +impl Model { + pub fn is_usage_check(&self) -> bool { + match 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..e78523a97ad8fa46b97ae3b35c9c6b2baa3640f7 --- /dev/null +++ b/src/chat/route.rs @@ -0,0 +1,18 @@ +mod logs; +pub use logs::{handle_logs, handle_logs_post}; +mod health; +pub use health::{handle_health, handle_root}; +mod token; +pub use token::{ + handle_basic_calibration, handle_get_checksum, handle_get_hash, handle_get_timestamp_header, + handle_get_tokeninfo, handle_tokeninfo_page, handle_update_tokeninfo, + handle_update_tokeninfo_post, +}; +mod profile; +pub use profile::handle_user_info; +mod config; +pub use config::{ + handle_about, 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..ec209567a773e63fcc52902440d72e0a505f6824 --- /dev/null +++ b/src/chat/route/config.rs @@ -0,0 +1,110 @@ +use crate::app::{ + constant::{ + 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_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, + }, + model::{AppConfig, PageContent}, +}; +use axum::{ + body::Body, + extract::Path, + http::{ + header::{CONTENT_TYPE, LOCATION}, + StatusCode, + }, + response::{IntoResponse, Response}, +}; + +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(), + } +} diff --git a/src/chat/route/health.rs b/src/chat/route/health.rs new file mode 100644 index 0000000000000000000000000000000000000000..91c791733b57daf0dc95a9130a98de3973f0b97a --- /dev/null +++ b/src/chat/route/health.rs @@ -0,0 +1,136 @@ +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_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, + ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, + ROUTE_GET_TOKENINFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, + ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_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::models::{ + 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_TOKENINFO_PATH, + ROUTE_UPDATE_TOKENINFO_PATH, + ROUTE_GET_TOKENINFO_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, + ], + }) +} diff --git a/src/chat/route/logs.rs b/src/chat/route/logs.rs new file mode 100644 index 0000000000000000000000000000000000000000..0076b7eb6be1848b378fd9b34d595bba8bdc97be --- /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::{models::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..07987fa294cdcadb69846537821bd75cdde1d3b9 --- /dev/null +++ b/src/chat/route/profile.rs @@ -0,0 +1,34 @@ +use crate::{ + chat::constant::ERR_NODATA, + common::{models::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}}, +}; +use axum::Json; + +use super::token::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/token.rs b/src/chat/route/token.rs new file mode 100644 index 0000000000000000000000000000000000000000..89eb57f1cdc255936d8627f5593047a6646c8837 --- /dev/null +++ b/src/chat/route/token.rs @@ -0,0 +1,276 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENINFO_PATH, + }, + lazy::{AUTH_TOKEN, TOKEN_FILE, TOKEN_LIST_FILE}, + model::{AppConfig, AppState, PageContent, TokenUpdateRequest}, + }, + common::{ + models::{ApiStatus, NormalResponseNoData}, + utils::{ + extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_repair, generate_hash, generate_timestamp_header, load_tokens, validate_token_and_checksum + }, + }, +}; +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() +} + +// 更新 TokenInfo 处理 +pub async fn handle_update_tokeninfo( + State(state): State>>, +) -> Json { + // 重新加载 tokens + let token_infos = load_tokens(); + + // 更新应用状态 + { + let mut state = state.lock().await; + state.token_infos = token_infos; + } + + Json(NormalResponseNoData { + status: ApiStatus::Success, + message: Some("Token list has been reloaded".to_string()), + }) +} + +// 获取 TokenInfo 处理 +pub async fn handle_get_tokeninfo( + 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 token_file = TOKEN_FILE.as_str(); + let token_list_file = TOKEN_LIST_FILE.as_str(); + + // 读取文件内容 + let tokens = std::fs::read_to_string(&token_file).unwrap_or_else(|_| String::new()); + let token_list = std::fs::read_to_string(&token_list_file).unwrap_or_else(|_| String::new()); + + // 获取 tokens_count + let tokens_count = { + { + tokens.len() + } + }; + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + token_file: token_file.to_string(), + token_list_file: token_list_file.to_string(), + tokens: Some(tokens), + tokens_count: Some(tokens_count), + token_list: Some(token_list), + message: None, + })) +} + +#[derive(Serialize)] +pub struct TokenInfoResponse { + pub status: ApiStatus, + pub token_file: String, + pub token_list_file: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tokens_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub token_list: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +pub async fn handle_update_tokeninfo_post( + 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_file = TOKEN_FILE.as_str(); + let token_list_file = TOKEN_LIST_FILE.as_str(); + + // 写入文件 + std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if let Some(token_list) = &request.token_list { + std::fs::write(&token_list_file, token_list) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + + // 重新加载 tokens + let token_infos = load_tokens(); + let token_infos_len = token_infos.len(); + + // 更新应用状态 + { + let mut state = state.lock().await; + state.token_infos = token_infos; + } + + Ok(Json(TokenInfoResponse { + status: ApiStatus::Success, + token_file: token_file.to_string(), + token_list_file: token_list_file.to_string(), + tokens: None, + tokens_count: Some(token_infos_len), + token_list: None, + message: Some("Token files have been updated and reloaded".to_string()), + })) +} + +pub async fn handle_tokeninfo_page() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_TOKENINFO_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/tokeninfo.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..78f3f388e6caeede5105f6299cf63ad638eb7a6d --- /dev/null +++ b/src/chat/service.rs @@ -0,0 +1,710 @@ +use crate::{ + app::{ + constant::{ + AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION, + OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS, + }, + lazy::{AUTH_TOKEN, SHARED_AUTH_TOKEN, USE_SHARE}, + model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo}, + }, + chat::{ + constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS}, + error::StreamError, + model::{ + ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage, + }, + stream::{parse_stream_data, StreamMessage}, + }, + common::{ + client::build_client, + models::{error::ChatError, userinfo::MembershipType, ErrorResponse}, + utils::{format_time_ms, generate_checksum_with_repair, get_token_profile, validate_token_and_checksum}, + }, +}; +use axum::{ + body::Body, + extract::State, + http::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + HeaderMap, StatusCode, + }, + response::Response, + Json, +}; +use bytes::Bytes; +use futures::{Stream, StreamExt}; +use std::{ + convert::Infallible, + sync::{atomic::AtomicBool, Arc}, +}; +use std::{ + pin::Pin, + sync::atomic::{AtomicUsize, Ordering}, +}; +use tokio::sync::Mutex; +use uuid::Uuid; + +const REQUEST_LOGS_LIMIT: usize = 1000; + +// 模型列表处理 +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 model = AVAILABLE_MODELS.iter().find(|m| m.id == request.model); + 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()), + ))?; + + // 验证认证token并获取token信息 + let (auth_token, checksum) = match auth_header { + // 管理员Token验证逻辑 + token if token == AUTH_TOKEN.as_str() || (*USE_SHARE && token == SHARED_AUTH_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验证逻辑 + token => validate_token_and_checksum(token).ok_or(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + ))?, + }; + + 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(&request.model.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()).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; + // 根据id查找对应的日志 + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == log_id) + { + log.token_info.profile = profile; + } + }); + } + + 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: STATUS_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, &request.model).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 = STATUS_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, &generate_checksum_with_repair(&checksum)); + let response = client.body(hex_data).send().await; + + // 处理请求结果 + let response = match 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 = STATUS_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 = STATUS_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()), + )); + } + }; + + // 释放活动请求计数 + { + let mut state = state.lock().await; + state.active_requests -= 1; + } + + if request.stream { + let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple()); + let full_text = Arc::new(Mutex::new(String::with_capacity(1024))); + 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 stream = { + // 创建新的 stream + let mut stream = response.bytes_stream(); + + let enable_stream_check = AppConfig::get_stream_check(); + + if enable_stream_check { + // 检查第一个 chunk + match stream.next().await { + Some(first_chunk) => { + let chunk = first_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()), + ) + })?; + + match parse_stream_data(&chunk) { + Err(StreamError::ChatError(error)) => { + let error_respone = 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 = STATUS_FAILED; + log.error = Some(error_respone.native_code()); + log.timing.total = + format_time_ms(start_time.elapsed().as_secs_f64()); + state.error_requests += 1; + } + } + return Err(( + error_respone.status_code(), + Json(error_respone.to_common()), + )); + } + Ok(_) | Err(_) => { + // 创建一个包含第一个 chunk 的 stream + Box::pin( + futures::stream::once(async move { Ok(chunk) }).chain(stream), + ) + as Pin< + Box< + dyn Stream> + Send, + >, + > + } + } + } + None => { + // Box::pin(stream) + // as Pin> + Send>> + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_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(), + ), + )); + } + } + } else { + Box::pin(stream) + as Pin> + Send>> + } + } + .then({ + let buffer = Arc::new(Mutex::new(Vec::new())); + let first_chunk_time = first_chunk_time.clone(); + let state = state.clone(); + + move |chunk| { + let buffer = buffer.clone(); + let response_id = response_id.clone(); + let model = request.model.clone(); + let is_start = is_start.clone(); + let full_text = full_text.clone(); + let first_chunk_time = first_chunk_time.clone(); + let state = state.clone(); + + async move { + let chunk = chunk.unwrap_or_default(); + let mut buffer_guard = buffer.lock().await; + buffer_guard.extend_from_slice(&chunk); + + match parse_stream_data(&buffer_guard) { + Ok(StreamMessage::Content(texts)) => { + buffer_guard.clear(); + let mut response_data = String::new(); + + // 记录首字时间(如果还未记录) + if let Ok(mut first_time) = first_chunk_time.try_lock() { + if first_time.is_none() { + *first_time = + Some(format_time_ms(start_time.elapsed().as_secs_f64())); + } + } + + // 处理文本内容 + for text in texts { + let mut text_guard = full_text.lock().await; + text_guard.push_str(&text); + let is_first = is_start.load(Ordering::SeqCst); + + let response = ChatResponse { + id: response_id.clone(), + object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), + created: chrono::Utc::now().timestamp(), + model: if is_first { Some(model.clone()) } else { None }, + choices: vec![Choice { + index: 0, + message: None, + delta: Some(Delta { + role: if is_first { + is_start.store(false, Ordering::SeqCst); + Some(Role::Assistant) + } else { + None + }, + content: Some(text), + }), + finish_reason: None, + }], + usage: None, + }; + + response_data.push_str(&format!( + "data: {}\n\n", + serde_json::to_string(&response).unwrap() + )); + } + + Ok::<_, Infallible>(Bytes::from(response_data)) + } + Ok(StreamMessage::StreamStart) => { + buffer_guard.clear(); + // 发送初始响应,包含模型信息 + let response = ChatResponse { + id: response_id.clone(), + object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), + created: chrono::Utc::now().timestamp(), + model: { + is_start.store(true, Ordering::SeqCst); + Some(model.clone()) + }, + choices: vec![Choice { + index: 0, + message: None, + delta: Some(Delta { + role: Some(Role::Assistant), + content: Some(String::new()), + }), + finish_reason: None, + }], + usage: None, + }; + + Ok(Bytes::from(format!( + "data: {}\n\n", + serde_json::to_string(&response).unwrap() + ))) + } + Ok(StreamMessage::StreamEnd) => { + buffer_guard.clear(); + // 根据配置决定是否发送最后的 finish_reason + let include_finish_reason = AppConfig::get_stop_stream(); + + // 计算总时间和首次片段时间 + let total_time = format_time_ms(start_time.elapsed().as_secs_f64()); + let first_time = first_chunk_time.lock().await.unwrap_or(total_time); + + { + 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 = Some(first_time); + } + } + + if include_finish_reason { + let response = ChatResponse { + id: response_id.clone(), + 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, + }; + Ok(Bytes::from(format!( + "data: {}\n\ndata: [DONE]\n\n", + serde_json::to_string(&response).unwrap() + ))) + } else { + Ok(Bytes::from("data: [DONE]\n\n")) + } + } + Ok(StreamMessage::Incomplete) => { + // 保持buffer中的数据以待下一个chunk + Ok(Bytes::new()) + } + Ok(StreamMessage::Debug(debug_prompt)) => { + buffer_guard.clear(); + if let Ok(mut state) = state.try_lock() { + if let Some(last_log) = state.request_logs.last_mut() { + last_log.prompt = Some(debug_prompt.clone()); + } + } + Ok(Bytes::new()) + } + Err(e) => { + buffer_guard.clear(); + eprintln!("[警告] Stream error: {}", e); + Ok(Bytes::new()) + } + } + } + } + }); + + 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_received = false; + let mut first_chunk_time = 0.0; + let mut full_text = String::with_capacity(1024); + let mut stream = response.bytes_stream(); + let mut prompt = None; + + let mut buffer = Vec::new(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| { + // 更新请求日志为失败 + 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.status = STATUS_FAILED; + log.error = Some(format!("Failed to read response chunk: {}", e)); + state.error_requests += 1; + } + } + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json( + ChatError::RequestFailed(format!("Failed to read response chunk: {}", e)) + .to_json(), + ), + ) + })?; + + buffer.extend_from_slice(&chunk); + + match parse_stream_data(&buffer) { + Ok(StreamMessage::Content(texts)) => { + if !first_chunk_received { + first_chunk_time = format_time_ms(start_time.elapsed().as_secs_f64()); + first_chunk_received = true; + } + for text in texts { + full_text.push_str(&text); + } + buffer.clear(); + } + Ok(StreamMessage::Incomplete) => continue, + Ok(StreamMessage::Debug(debug_prompt)) => { + prompt = Some(debug_prompt); + buffer.clear(); + } + Ok(StreamMessage::StreamStart) | Ok(StreamMessage::StreamEnd) => { + buffer.clear(); + } + Err(StreamError::ChatError(error)) => { + let error = 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 = STATUS_FAILED; + log.error = Some(error.native_code()); + log.timing.total = format_time_ms(start_time.elapsed().as_secs_f64()); + state.error_requests += 1; + } + } + return Err((error.status_code(), Json(error.to_common()))); + } + Err(_) => { + buffer.clear(); + continue; + } + } + } + + // 检查响应是否为空 + 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 = STATUS_FAILED; + log.error = Some("Empty response received".to_string()); + if let Some(p) = prompt { + log.prompt = Some(p); + } + 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), + }), + 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 = Some(first_chunk_time); + log.prompt = prompt; + log.status = STATUS_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..25ac5245d17d7bfc15ed3d1e4002630b25feec5b --- /dev/null +++ b/src/chat/stream.rs @@ -0,0 +1,209 @@ +use super::aiserver::v1::StreamChatResponse; +use flate2::read::GzDecoder; +use prost::Message; +use std::io::Read; + +use super::error::{ChatError, StreamError}; + +// 解压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 enum StreamMessage { + // 未完成 + Incomplete, + // 调试 + Debug(String), + // 流开始标志 b"\0\0\0\0\0" + StreamStart, + // 消息内容 + Content(Vec), + // 流结束标志 b"\x02\0\0\0\x02{}" + StreamEnd, +} + +pub fn parse_stream_data(data: &[u8]) -> Result { + if data.len() < 5 { + return Err(StreamError::DataLengthLessThan5); + } + + // 检查是否为流开始标志 + // if data == b"\0\0\0\0\0" { + // return Ok(StreamMessage::StreamStart); + // } + + // 检查是否为流结束标志 + // if data == b"\x02\0\0\0\x02{}" { + // return Ok(StreamMessage::StreamEnd); + // } + + let mut messages = Vec::new(); + let mut offset = 0; + + while offset + 5 <= data.len() { + // 获取消息类型和长度 + let msg_type = data[offset]; + let msg_len = u32::from_be_bytes([ + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + ]) as usize; + + // 流开始 + if msg_type == 0 && msg_len == 0 { + return Ok(StreamMessage::StreamStart); + } + + // 检查剩余数据长度是否足够 + if offset + 5 + msg_len > data.len() { + return Ok(StreamMessage::Incomplete); + } + + let msg_data = &data[offset + 5..offset + 5 + msg_len]; + + match msg_type { + // 文本消息 + 0 => { + if let Ok(response) = StreamChatResponse::decode(msg_data) { + // crate::debug_println!("[text] StreamChatResponse: {:?}", response); + if !response.text.is_empty() { + messages.push(response.text); + } else { + // println!("[text] StreamChatResponse: {:?}", response); + return Ok(StreamMessage::Debug( + response.filled_prompt.unwrap_or_default(), + // response.is_using_slow_request, + )); + } + } + } + // gzip压缩消息 + 1 => { + if let Some(text) = decompress_gzip(msg_data) { + let response = StreamChatResponse::decode(&text[..]).unwrap_or_default(); + // crate::debug_println!("[gzip] StreamChatResponse: {:?}", response); + if !response.text.is_empty() { + messages.push(response.text); + } else { + // println!("[gzip] StreamChatResponse: {:?}", response); + return Ok(StreamMessage::Debug( + response.filled_prompt.unwrap_or_default(), + // response.is_using_slow_request, + )); + } + } + } + // JSON字符串 + 2 => { + if msg_len == 2 { + return Ok(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)); + } + // 未预计 + // messages.push(text); + } + } + // 其他类型暂不处理 + t => eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t), + } + + offset += 5 + msg_len; + } + + if messages.is_empty() { + Err(StreamError::EmptyMessage) + } else { + Ok(StreamMessage::Content(messages)) + } +} + +#[test] +fn test_parse_stream_data() { + // 使用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(); + + // 辅助函数:找到下一个消息边界 + 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; + 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 parse_stream_data(current_msg_bytes) { + Ok(message) => { + match message { + StreamMessage::Content(messages) => { + print!("消息内容 [hex: {}]:", hex_str); + for msg in messages { + println!(" {}", msg); + } + offset += msg_boundary; + } + StreamMessage::Debug(_) => { + // println!("调试信息 [hex: {}]: {}", hex_str, prompt); + offset += msg_boundary; + } + StreamMessage::StreamEnd => { + println!("流结束 [hex: {}]", hex_str); + break; + } + StreamMessage::StreamStart => { + println!("流开始 [hex: {}]", hex_str); + offset += msg_boundary; + } + StreamMessage::Incomplete => { + println!("数据不完整 [hex: {}]", hex_str); + break; + } + } + } + Err(e) => { + println!("解析错误 [hex: {}]: {}", hex_str, e); + break; + } + } + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000000000000000000000000000000000000..757ddf3f634b96cb6f631e9f0e9bf0f2c66c849b --- /dev/null +++ b/src/common.rs @@ -0,0 +1,3 @@ +pub mod models; +pub mod utils; +pub mod client; diff --git a/src/common/client.rs b/src/common/client.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8f803583068bba30cda458f262243a5ec750188 --- /dev/null +++ b/src/common/client.rs @@ -0,0 +1,222 @@ +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_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, + REVERSE_PROXY_HOST, USE_PROXY, + }, +}; +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 uuid::Uuid; + +use super::utils::generate_hash; + +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"); + +/// 返回预构建的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `auth_token` - 授权令牌 +/// * `checksum` - 校验和 +/// * `endpoint` - API 端点路径 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder { + let trace_id = Uuid::new_v4().to_string(); + + let client = if *USE_PROXY { + Client::new() + .post(&*CURSOR_API2_CHAT_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_API2_HOST) + } else { + Client::new() + .post(&*CURSOR_API2_CHAT_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_PROXY { + Client::new() + .get(&*CURSOR_API2_STRIPE_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_API2_HOST) + } else { + Client::new() + .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_PROXY { + Client::new() + .get(&*CURSOR_USAGE_API_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_HOST) + } else { + Client::new() + .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_PROXY { + Client::new() + .get(&*CURSOR_USER_API_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_HOST) + } else { + Client::new() + .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/models.rs b/src/common/models.rs new file mode 100644 index 0000000000000000000000000000000000000000..9333c5cef10347b7f1b2c5c4e747a616fdd785b9 --- /dev/null +++ b/src/common/models.rs @@ -0,0 +1,71 @@ +pub mod error; +pub mod health; +pub mod config; +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/models/config.rs b/src/common/models/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..8c1444839925a0cb36982b12d99295e80df4a615 --- /dev/null +++ b/src/common/models/config.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +use crate::app::model::{PageContent, UsageCheck, VisionAbility}; + +#[derive(Serialize)] +pub struct ConfigData { + pub page_content: Option, + pub enable_stream_check: bool, + pub include_stop_stream: bool, + pub vision_ability: VisionAbility, + pub enable_slow_pool: bool, + pub enable_all_claude: bool, + pub check_usage_models: UsageCheck, +} + +#[derive(Deserialize)] +pub struct ConfigUpdateRequest { + #[serde(default)] + pub action: String, // "get", "update", "reset" + #[serde(default)] + pub path: String, + #[serde(default)] + pub content: Option, // "default", "text", "html" + #[serde(default)] + pub enable_stream_check: Option, + #[serde(default)] + pub include_stop_stream: Option, + #[serde(default)] + pub vision_ability: Option, + #[serde(default)] + pub enable_slow_pool: Option, + #[serde(default)] + pub enable_all_claude: Option, + #[serde(default)] + pub check_usage_models: Option, +} diff --git a/src/common/models/error.rs b/src/common/models/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..79430db915c5c41deabcde06915d263cd1e7a3cd --- /dev/null +++ b/src/common/models/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/models/health.rs b/src/common/models/health.rs new file mode 100644 index 0000000000000000000000000000000000000000..f74fe090c63846a02f06b1610c0d6d8066b9791e --- /dev/null +++ b/src/common/models/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/models/userinfo.rs b/src/common/models/userinfo.rs new file mode 100644 index 0000000000000000000000000000000000000000..ad8554a6857ed6d81182839c90e17ab9a670e1ca --- /dev/null +++ b/src/common/models/userinfo.rs @@ -0,0 +1,88 @@ +use chrono::{DateTime, Local}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +#[serde(untagged)] +pub enum GetUserInfo { + Usage(TokenProfile), + Error { error: String }, +} + +#[derive(Serialize, Clone)] +pub struct TokenProfile { + pub usage: UsageProfile, + pub user: UserProfile, + pub stripe: StripeProfile, +} + +#[derive(Deserialize, Serialize, PartialEq, Clone)] +pub enum MembershipType { + #[serde(rename = "free")] + Free, + #[serde(rename = "free_trial")] + FreeTrial, + #[serde(rename = "pro")] + Pro, + #[serde(rename = "enterprise")] + Enterprise, +} + +#[derive(Deserialize, Serialize, Clone)] +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)] +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)] +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)] +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..bbe20c807243464787d8cc74f083a08b8766ca55 --- /dev/null +++ b/src/common/utils.rs @@ -0,0 +1,165 @@ +mod checksum; +pub use checksum::*; +mod tokens; +pub use tokens::*; + +use super::models::userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}; +use crate::app::{ + constant::{FALSE, TRUE}, + lazy::{TOKEN_DELIMITER, TOKEN_DELIMITER_LEN}, +}; + +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 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 comma_pos = auth_token.rfind(*TOKEN_DELIMITER)?; + let (token_part, checksum) = auth_token.split_at(comma_pos); + let checksum = &checksum[*TOKEN_DELIMITER_LEN..]; // 跳过逗号 + + // 解析 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(), checksum.to_string())) + } else { + None + } +} + +pub fn extract_token(auth_token: &str) -> Option { + // 解析 token + let token_part = match auth_token.rfind(*TOKEN_DELIMITER) { + 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 +} diff --git a/src/common/utils/checksum.rs b/src/common/utils/checksum.rs new file mode 100644 index 0000000000000000000000000000000000000000..2a62796dfcd46925a2ac1b36bca32ef91b393dc1 --- /dev/null +++ b/src/common/utils/checksum.rs @@ -0,0 +1,291 @@ +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) +} + +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 { + // 预校验:检查字符串是否为空或只包含合法的Base64字符和'/' + if checksum.is_empty() + || !checksum + .chars() + .all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '=')) + { + return generate_checksum_with_default(); + } + + // 尝试修复时间戳头的函数 + fn try_fix_timestamp(timestamp_base64: &str) -> Option { + if let Ok(timestamp_bytes) = BASE64.decode(timestamp_base64) { + if timestamp_bytes.len() == 6 { + let mut fixed_bytes = timestamp_bytes.clone(); + deobfuscate_bytes(&mut fixed_bytes); + + // 检查前3位是否为0 + if fixed_bytes[0..3].iter().all(|&x| x == 0) { + // 从后四位构建时间戳 + let timestamp = ((fixed_bytes[2] as u64) << 24) + | ((fixed_bytes[3] as u64) << 16) + | ((fixed_bytes[4] as u64) << 8) + | (fixed_bytes[5] as u64); + + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + // 修复时间戳字节 + fixed_bytes[0] = fixed_bytes[4]; + fixed_bytes[1] = fixed_bytes[5]; + + obfuscate_bytes(&mut fixed_bytes); + return Some(BASE64.encode(&fixed_bytes)); + } + } + } + } + None + } + + if checksum.len() == 8 { + // 尝试修复时间戳头 + if let Some(fixed_timestamp) = try_fix_timestamp(checksum) { + return format!("{}{}/{}", fixed_timestamp, generate_hash(), generate_hash()); + } + + // 验证原始时间戳 + if let Some(timestamp) = extract_time_ks(checksum) { + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + return format!("{}{}/{}", checksum, generate_hash(), generate_hash()); + } + } + } else if checksum.len() > 8 { + // 处理可能包含hash的情况 + let parts: Vec<&str> = checksum.split('/').collect(); + match parts.len() { + 1 => { + let timestamp_base64 = &checksum[..8]; + let device_id = &checksum[8..]; + + if is_valid_hash(device_id) { + // 先尝试修复时间戳 + if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) { + return format!("{}{}/{}", fixed_timestamp, device_id, generate_hash()); + } + + // 验证原始时间戳 + if let Some(timestamp) = extract_time_ks(timestamp_base64) { + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + return format!( + "{}{}/{}", + generate_timestamp_header(), + device_id, + generate_hash() + ); + } + } + } + } + 2 => { + let first_part = parts[0]; + let mac_hash = parts[1]; + + if is_valid_hash(mac_hash) && first_part.len() == mac_hash.len() + 8 { + let timestamp_base64 = &first_part[..8]; + let device_id = &first_part[8..]; + + if is_valid_hash(device_id) { + // 先尝试修复时间戳 + if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) { + return format!("{}{}/{}", fixed_timestamp, device_id, mac_hash); + } + + // 验证原始时间戳 + if let Some(timestamp) = extract_time_ks(timestamp_base64) { + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + return format!( + "{}{}/{}", + generate_timestamp_header(), + device_id, + mac_hash + ); + } + } + } + } + } + _ => {} + } + } + + // 如果所有修复尝试都失败,返回默认值 + generate_checksum_with_default() +} + +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 { + // 预校验:检查字符串是否为空或只包含合法的Base64字符和'/' + if checksum.is_empty() + || !checksum + .chars() + .all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '=')) + { + return false; + } + // 首先检查是否包含基本的 base64 编码部分和 hash 格式的 device_id + let parts: Vec<&str> = checksum.split('/').collect(); + + match parts.len() { + // 没有 MAC 地址的情况 + 1 => { + if checksum.len() < 72 { + // 8 + 64 = 72 + return false; + } + + // 解码前8个字符的base64时间戳 + let timestamp_base64 = &checksum[..8]; + let timestamp = match extract_time_ks(timestamp_base64) { + Some(ts) => ts, + None => return false, + }; + + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if current_timestamp < timestamp { + return false; + } + + // 验证 device_id hash 部分 + is_valid_hash(&checksum[8..]) + } + // 包含 MAC hash 的情况 + 2 => { + let first_part = parts[0]; + let mac_hash = parts[1]; + + // MAC hash 必须是64字符的十六进制 + if !is_valid_hash(mac_hash) { + return false; + } + + // 检查第一部分比MAC hash多8个字符 + if first_part.len() != mac_hash.len() + 8 { + return false; + } + + // 递归验证第一部分 + validate_checksum(first_part) + } + _ => false, + } +} + +fn is_valid_hash(hash: &str) -> bool { + if hash.len() < 64 { + return false; + } + + // 检查是否都是有效的十六进制字符 + hash.chars().all(|c| c.is_ascii_hexdigit()) +} diff --git a/src/common/utils/tokens.rs b/src/common/utils/tokens.rs new file mode 100644 index 0000000000000000000000000000000000000000..2ea41afadb65e2655267a7d6b89fd31c6d93c86a --- /dev/null +++ b/src/common/utils/tokens.rs @@ -0,0 +1,301 @@ +use crate::{ + app::{ + constant::EMPTY_STRING, + model::TokenInfo, + lazy::{TOKEN_FILE, TOKEN_LIST_FILE}, + }, + common::utils::generate_checksum_with_default, +}; + +// 规范化文件内容并写入 +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 +fn parse_token(token_part: &str) -> Option { + // 查找最后一个:或%3A的位置 + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + match (colon_pos, encoded_colon_pos) { + (None, None) => Some(token_part.to_string()), + (Some(pos1), None) => Some(token_part[(pos1 + 1)..].to_string()), + (None, Some(pos2)) => Some(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 }; + Some(token_part[start..].to_string()) + } + } +} + +// Token 加载函数 +pub fn load_tokens() -> Vec { + let token_file = TOKEN_FILE.as_str(); + let token_list_file = TOKEN_LIST_FILE.as_str(); + + // 确保文件存在 + for file in [&token_file, &token_list_file] { + if !std::path::Path::new(file).exists() { + if let Err(e) = std::fs::write(file, EMPTY_STRING) { + eprintln!("警告: 无法创建文件 '{}': {}", file, e); + } + } + } + + // 读取和规范化 token 文件 + let token_entries = match std::fs::read_to_string(&token_file) { + Ok(content) => { + let normalized = content.replace("\r\n", "\n"); + normalized + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + return None; + } + let parsed = parse_token(line); + if parsed.is_none() || !validate_token(&parsed.as_ref().unwrap()) { + return None; + } + parsed + }) + .collect::>() + } + Err(e) => { + eprintln!("警告: 无法读取token文件 '{}': {}", token_file, e); + Vec::new() + } + }; + + // 读取和规范化 token-list 文件 + let mut 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(',').collect(); + match parts[..] { + [token_part, checksum] => { + let token = parse_token(token_part)?; + Some((token, checksum.to_string())) + } + _ => { + eprintln!("警告: 忽略无效的token-list行: {}", line); + None + } + } + }) + .collect() + } + Err(e) => { + eprintln!("警告: 无法读取token-list文件: {}", e); + std::collections::HashMap::new() + } + }; + + // 更新或添加新token + for token in token_entries { + if !token_map.contains_key(&token) { + // 为新token生成checksum + let checksum = generate_checksum_with_default(); + token_map.insert(token, checksum); + } + } + + // 更新 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() +} + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use chrono::{DateTime, Local, TimeZone}; + +// 验证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] != "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" { + 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, + }; + + // 解析 JSON + let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { + Ok(v) => v, + Err(_) => return false, + }; + + // 验证必要字段是否存在且有效 + let required_fields = ["sub", "time", "randomness", "exp", "iss", "scope", "aud"]; + for field in required_fields { + if !payload_json.get(field).is_some() { + return false; + } + } + + // 验证 time 字段 + if let Some(time) = payload_json["time"].as_str() { + // 验证 time 是否为有效的数字字符串 + if let Ok(time_value) = time.parse::() { + let current_time = chrono::Utc::now().timestamp(); + if time_value > current_time { + return false; + } + } else { + return false; + } + } else { + return false; + } + + // 验证 randomness 长度 + if let Some(randomness) = payload_json["randomness"].as_str() { + if randomness.len() != 18 { + return false; + } + } else { + return false; + } + + // 验证过期时间 + if let Some(exp) = payload_json["exp"].as_i64() { + let current_time = chrono::Utc::now().timestamp(); + if current_time > exp { + return false; + } + } else { + return false; + } + + // 验证发行者 + if payload_json["iss"].as_str() != Some("https://authentication.cursor.sh") { + return false; + } + + // 验证授权范围 + if payload_json["scope"].as_str() != Some("openid profile email offline_access") { + return false; + } + + // 验证受众 + if payload_json["aud"].as_str() != Some("https://cursor.com") { + 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, + }; + + // 解析 JSON + let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { + Ok(v) => v, + Err(_) => return None, + }; + + // 提取 sub 字段 + payload_json["sub"] + .as_str() + .map(|s| s.split('|').nth(1).unwrap_or(s).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, + }; + + // 解析 JSON + let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { + Ok(v) => v, + Err(_) => return None, + }; + + // 提取时间戳并转换为本地时间 + payload_json["time"] + .as_str() + .and_then(|t| t.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..a249f273a16b28635831b69f7bd47be9eeed9f09 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,105 @@ +mod app; +mod chat; +mod common; + +use app::{ + config::handle_config_update, + constant::{ + EMPTY_STRING, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, ROUTE_GET_TOKENINFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_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_api_page, handle_basic_calibration, handle_config_page, handle_env_example, handle_get_checksum, handle_get_hash, handle_get_timestamp_header, handle_get_tokeninfo, handle_health, handle_logs, handle_logs_post, handle_readme, handle_root, handle_static, handle_tokeninfo_page, handle_update_tokeninfo, handle_update_tokeninfo_post, handle_user_info + }, + service::{handle_chat, handle_models}, +}; +use common::utils::{ + load_tokens, parse_bool_from_env, parse_string_from_env, parse_usize_from_env, +}; +use std::sync::Arc; +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( + parse_bool_from_env("ENABLE_STREAM_CHECK", true), + parse_bool_from_env("INCLUDE_STOP_REASON_STREAM", true), + VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)), + parse_bool_from_env("ENABLE_SLOW_POOL", false), + parse_bool_from_env("PASS_ANY_CLAUDE", false), + ); + + // 加载 tokens + let token_infos = load_tokens(); + + // 初始化应用状态 + let state = Arc::new(Mutex::new(AppState::new(token_infos))); + + // 设置路由 + let app = Router::new() + .route(ROUTE_ROOT_PATH, get(handle_root)) + .route(ROUTE_HEALTH_PATH, get(handle_health)) + .route(ROUTE_TOKENINFO_PATH, get(handle_tokeninfo_page)) + .route(ROUTE_MODELS_PATH.as_str(), get(handle_models)) + .route(ROUTE_UPDATE_TOKENINFO_PATH, get(handle_update_tokeninfo)) + .route(ROUTE_GET_TOKENINFO_PATH, post(handle_get_tokeninfo)) + .route( + ROUTE_UPDATE_TOKENINFO_PATH, + post(handle_update_tokeninfo_post), + ) + .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)) + .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); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/static/api.html b/static/api.html new file mode 100644 index 0000000000000000000000000000000000000000..5228d64e644a2163069880140d182a573baa1d20 --- /dev/null +++ b/static/api.html @@ -0,0 +1,362 @@ + + + + + + + + API 管理 + + + + + + +
+
+

API 管理

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

配置管理

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

请求日志查看

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

总请求数

+
-
+
+
+

活跃请求数

+
-
+
+
+

错误请求数

+
-
+
+
+

最后更新

+
-
+
+
+ +
+ + + + + + + + + + + + + + + +
时间模型Token信息Prompt用时/首字流式响应状态错误信息
+
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/static/readme.html b/static/readme.html new file mode 100644 index 0000000000000000000000000000000000000000..0942e203b6c693f9931fa3fe5cee967fc84576c2 --- /dev/null +++ b/static/readme.html @@ -0,0 +1,651 @@ +

cursor-api

+ +

说明

+ +
    +
  • 当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。
  • +
  • 若发现首字慢,与本程序无关。
  • +
  • 若发现响应出现乱码,也与本程序无关。
  • +
  • 属于官方的问题,请不要像作者反馈。
  • +
  • 本程序拥有堪比客户端原本的速度,甚至可能更快。
  • +
  • 本程序的性能是非常厉害的。
  • +
  • 根据本项目开源协议,Fork的项目不能以作者的名义进行任何形式的宣传、推广或声明。
  • +
+ +

获取key

+ +
    +
  1. 访问 www.cursor.com 并完成注册登录
  2. +
  3. 在浏览器中打开开发者工具(F12)
  4. +
  5. 在 Application-Cookies 中查找名为 WorkosCursorSessionToken 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式,cookie + 的值使用冒号 (:) 进行分隔。
  6. +
+ +

配置说明

+ +

环境变量

+ +
    +
  • PORT: 服务器端口号(默认:3000)
  • +
  • AUTH_TOKEN: 认证令牌(必须,用于API认证)
  • +
  • ROUTE_PREFIX: 路由前缀(可选)
  • +
  • TOKEN_FILE: token文件路径(默认:.token)
  • +
  • TOKEN_LIST_FILE: token列表文件路径(默认:.token-list)
  • +
+ +

更多请查看 /env-example

+ +

Token文件格式

+ +
    +
  1. +

    .token 文件:每行一个token,支持以下格式:

    + +
    # 这是注释
    +token1
    +# alias与标签的作用差不多
    +alias::token2
    +
    + +

    alias 可以是任意值,用于区分不同的 token,更方便管理,WorkosCursorSessionToken 是相同格式
    + 该文件将自动向.token-list文件中追加token,同时自动生成checksum

    +
  2. + +
  3. +

    .token-list 文件:每行为token和checksum的对应关系:

    + +
    # 这里的#表示这行在下次读取要删除
    +token1,checksum1
    +# alias被舍弃,会自动删除最后一个:或%3A的后一位前的所有内容
    +token2,checksum2
    +
    + +

    该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:

    + +
      +
    • 需要删除某个 token
    • +
    • 需要使用已有 checksum 来对应某一个 token
    • +
    +
  4. +
+ +

模型列表

+ +

写死了,后续也不会会支持自定义模型列表

+ +
claude-3.5-sonnet
+gpt-4
+gpt-4o
+claude-3-opus
+cursor-fast
+cursor-small
+gpt-3.5-turbo
+gpt-4-turbo-2024-04-09
+gpt-4o-128k
+gemini-1.5-flash-500k
+claude-3-haiku-200k
+claude-3-5-sonnet-200k
+claude-3-5-sonnet-20241022
+gpt-4o-mini
+o1-mini
+o1-preview
+o1
+claude-3.5-haiku
+gemini-exp-1206
+gemini-2.0-flash-thinking-exp
+gemini-2.0-flash-exp
+
+ +

接口说明

+ +

基础对话

+ +
    +
  • 接口地址: /v1/chat/completions
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token +
      +
    1. 使用环境变量 AUTH_TOKEN 进行认证
    2. +
    3. 使用 .token 文件中的令牌列表进行轮询认证
    4. +
    5. 在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭
    6. +
    +
  • +
+ +

请求格式

+ +
{
+  "model": "string",
+  "messages": [
+    {
+      "role": "system" | "user" | "assistant", // 也可以是 "developer" | "human" | "ai"
+      "content": "string" | [
+        {
+          "type": "text" | "image_url",
+          "text": "string",
+          "image_url": {
+            "url": "string"
+          }
+        }
+      ]
+    }
+  ],
+  "stream": boolean
+}
+
+ +

响应格式

+ +

如果 streamfalse:

+ +
{
+  "id": "string",
+  "object": "chat.completion",
+  "created": number,
+  "model": "string",
+  "choices": [
+    {
+      "index": number,
+      "message": {
+        "role": "assistant",
+        "content": "string"
+      },
+      "finish_reason": "stop" | "length"
+    }
+  ],
+  "usage": {
+    "prompt_tokens": 0,
+    "completion_tokens": 0,
+    "total_tokens": 0
+  }
+}
+
+ +

不进行 tokens 计算主要是担心性能问题。

+ +

如果 streamtrue:

+ +
data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{"role":"assistant","content":"string"},"finish_reason":null}]}
+
+data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{"content":"string"},"finish_reason":null}]}
+
+data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{},"finish_reason":"stop"}]}
+
+data: [DONE]
+
+ +

Token管理接口

+ +

简易Token信息管理页面

+ +
    +
  • 接口地址: /tokeninfo
  • +
  • 请求方法: GET
  • +
  • 响应格式: HTML页面
  • +
  • 功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容
  • +
+ +

更新Token信息 (GET)

+ +
    +
  • 接口地址: /update-tokeninfo
  • +
  • 请求方法: GET
  • +
  • 认证方式: 不需要
  • +
  • 功能: 重新加载tokens并更新应用状态
  • +
  • 响应格式:
  • +
+ +
{
+  "status": "success",
+  "message": "Token list has been reloaded"
+}
+
+ +

更新Token信息 (POST)

+ +
    +
  • 接口地址: /update-tokeninfo
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 请求格式:
  • +
+ +
{
+  "tokens": "string",
+  "token_list": "string"
+}
+
+ +
    +
  • 响应格式:
  • +
+ +
{
+  "status": "success",
+  "token_file": "string",
+  "token_list_file": "string",
+  "tokens_count": number,
+  "message": "Token files have been updated and reloaded"
+}
+
+ +

获取Token信息

+ +
    +
  • 接口地址: /get-tokeninfo
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 响应格式:
  • +
+ +
{
+  "status": "success",
+  "token_file": "string",
+  "token_list_file": "string",
+  "tokens": "string",
+  "tokens_count": number,
+  "token_list": "string"
+}
+
+ +

配置管理接口

+ +

配置页面

+ +
    +
  • 接口地址: /config
  • +
  • 请求方法: GET
  • +
  • 响应格式: HTML页面
  • +
  • 功能: 提供配置管理界面,可以修改页面内容和系统配置
  • +
+ +

更新配置

+ +
    +
  • 接口地址: /config
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 请求格式:
  • +
+ +
{
+  "action": "get" | "update" | "reset",
+  "path": "string",
+  "content": {
+    "type": "default" | "text" | "html",
+    "content": "string"
+  },
+  "enable_stream_check": boolean,
+  "include_stop_stream": boolean,
+  "vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
+  "enable_slow_pool": boolean,
+  "enable_all_claude": boolean,
+  "check_usage_models": {
+    "type": "none" | "default" | "all" | "list",
+    "content": "string"
+  }
+}
+
+ +
    +
  • 响应格式:
  • +
+ +
{
+  "status": "success",
+  "message": "string",
+  "data": {
+    "page_content": {
+      "type": "default" | "text" | "html", // 对于js和css后两者是一样的
+      "content": "string"
+    },
+    "enable_stream_check": boolean,
+    "include_stop_stream": boolean,
+    "vision_ability": "none" | "base64" | "all",
+    "enable_slow_pool": boolean,
+    "enable_all_claude": boolean,
+    "check_usage_models": {
+      "type": "none" | "default" | "all" | "list",
+      "content": "string"
+    }
+  }
+}
+
+ +

注意:check_usage_models 字段的默认值为:

+ +
{
+  "type": "default",
+  "content": "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"
+}
+ +

这些模型将默认进行使用量检查。您可以通过配置接口修改此设置。

+ +

路径修改注意:选择类型再修改文本,否则选择默认时内容的修改无效,在更新配置后自动被覆盖导致内容丢失,自行改进。

+ +

静态资源接口

+ +

获取共享样式

+ +
    +
  • 接口地址: /static/shared-styles.css
  • +
  • 请求方法: GET
  • +
  • 响应格式: CSS文件
  • +
  • 功能: 获取共享样式表
  • +
+ +

获取共享脚本

+ +
    +
  • 接口地址: /static/shared.js
  • +
  • 请求方法: GET
  • +
  • 响应格式: JavaScript文件
  • +
  • 功能: 获取共享JavaScript代码
  • +
+ +

环境变量示例

+ +
    +
  • 接口地址: /env-example
  • +
  • 请求方法: GET
  • +
  • 响应格式: 文本文件
  • +
  • 功能: 获取环境变量配置示例
  • +
+ +

其他接口

+ +

获取模型列表

+ +
    +
  • 接口地址: /v1/models
  • +
  • 请求方法: GET
  • +
  • 响应格式:
  • +
+ +
{
+  "object": "list",
+  "data": [
+    {
+      "id": "string",
+      "object": "model",
+      "created": number,
+      "owned_by": "string"
+    }
+  ]
+}
+
+ +

获取一个随机hash

+ +
    +
  • 接口地址: /get-hash
  • +
  • 请求方法: GET
  • +
  • 响应格式:
  • +
+ +
string
+ +

获取或修复checksum

+ +
    +
  • 接口地址: /get-checksum
  • +
  • 请求方法: GET
  • +
  • 请求参数: +
      +
    • checksum: 可选,用于修复的旧版本生成的checksum,也可只传入前8个字符;可用来自动刷新时间戳头
    • +
    +
  • +
  • 响应格式:
  • +
+ +
string
+ +

说明:

+
    +
  • 如果不提供checksum参数,将生成一个新的随机checksum
  • +
  • 如果提供checksum参数,将尝试修复旧版本的checksum以适配v0.1.3-rc.3之后的版本使用,修复失败会返回新的checksum;若输入的checksum本来就有效,则返回更新tsheader后的checksum
  • +
+ +

获取当前的tsheader

+ +
    +
  • 接口地址: /get-tsheader
  • +
  • 请求方法: GET
  • +
  • 响应格式:
  • +
+ +
string
+ +

健康检查接口

+ +
    +
  • 接口地址: /health/(重定向)
  • +
  • 请求方法: GET
  • +
  • 认证方式: Bearer Token(可选)
  • +
  • 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML),默认JSON
  • +
+ +
{
+  "status": "success",
+  "version": "string",
+  "uptime": number,
+  "stats": {
+    "started": "string",
+    "total_requests": number,
+    "active_requests": number,
+    "system": {
+      "memory": {
+        "rss": number
+      },
+      "cpu": {
+        "usage": number
+      }
+    }
+  },
+  "models": ["string"],
+  "endpoints": ["string"]
+}
+
+ +

注意:stats 字段仅在请求头中包含正确的 AUTH_TOKEN 时才会返回。否则,该字段将被省略。

+ +

获取日志接口

+ +
    +
  • 接口地址: /logs
  • +
  • 请求方法: GET
  • +
  • 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)
  • +
+ +

获取日志数据

+ +
    +
  • 接口地址: /logs
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 响应格式:
  • +
+ +
{
+  "total": number,
+  "logs": [
+    {
+      "id": number,
+      "timestamp": "string",
+      "model": "string",
+      "token_info": {
+        "token": "string",
+        "checksum": "string",
+        "profile": {
+          "usage": {
+            "premium": {
+              "requests": number,
+              "requests_total": number,
+              "tokens": number,
+              "max_requests": number,
+              "max_tokens": number
+            },
+            "standard": {
+              "requests": number,
+              "requests_total": number,
+              "tokens": number,
+              "max_requests": number,
+              "max_tokens": number
+            },
+            "unknown": {
+              "requests": number,
+              "requests_total": number,
+              "tokens": number,
+              "max_requests": number,
+              "max_tokens": number
+            }
+          },
+          "user": {
+            "email": "string",
+            "name": "string",
+            "id": "string",
+            "updated_at": "string"
+          },
+          "stripe": {
+            "membership_type": "free" | "free_trial" | "pro" | "enterprise",
+            "payment_id": "string",
+            "days_remaining_on_trial": number
+          }
+        }
+      },
+      "prompt": "string",
+      "timing": {
+        "total": number,
+        "first": number
+      },
+      "stream": boolean,
+      "status": "string",
+      "error": "string"
+    }
+  ],
+  "timestamp": "string",
+  "status": "success"
+}
+
+ +

获取用户信息

+ +
    +
  • 接口地址: /userinfo
  • +
  • 请求方法: POST
  • +
  • 认证方式: 请求体中包含token
  • +
  • 请求格式:
  • +
+ +
{
+  "token": "string"
+}
+
+ +
    +
  • 响应格式:
  • +
+ +
{
+  "usage": {
+    "premium": {
+      "requests": number,
+      "requests_total": number,
+      "tokens": number,
+      "max_requests": number,
+      "max_tokens": number
+    },
+    "standard": {
+      "requests": number,
+      "requests_total": number,
+      "tokens": number,
+      "max_requests": number,
+      "max_tokens": number
+    },
+    "unknown": {
+      "requests": number,
+      "requests_total": number,
+      "tokens": number,
+      "max_requests": number,
+      "max_tokens": number
+    }
+  },
+  "user": {
+    "email": "string",
+    "name": "string",
+    "id": "string",
+    "updated_at": "string"
+  },
+  "stripe": {
+    "membership_type": "free" | "free_trial" | "pro" | "enterprise",
+    "payment_id": "string",
+    "days_remaining_on_trial": number
+  }
+}
+
+ +

如果发生错误,响应格式为:

+ +
{
+  "error": "string"
+}
+
+ +

基础校准

+ +
    +
  • 接口地址: /basic-calibration
  • +
  • 请求方法: POST
  • +
  • 认证方式: 请求体中包含token
  • +
  • 请求格式:
  • +
+ +
{
+  "token": "string"
+}
+
+ +
    +
  • 响应格式:
  • +
+ +
{
+  "status": "success" | "error",
+  "message": "string",
+  "user_id": "string",
+  "create_at": "string",
+  "checksum_time": number
+}
+
+ +

注意: user_id, create_at, 和 checksum_time 字段在校验失败时可能不存在。

+ +

偷偷写在最后的话

+ +

虽然作者觉得收点钱合理,但不强求,要是主动自愿发我我肯定收(因为真有人这么做,虽然不是赞助),赞助很合理吧

+ +

不是主动自愿就算了,不是很缺,给了会很感动罢了。

+ +

虽然不是很建议你赞助,但如果你赞助了,大概可以:

+ +
    +
  • 测试版更新
  • +
  • 要求功能
  • +
  • 问题更快解决
  • +
+ +

即使如此,我也保留可以拒绝赞助和拒绝要求的权利。

+ +

求赞助还是有点不要脸了,接下来是吐槽:

+ +

辛辛苦苦做这个也不知道是为了谁,好累。其实还有很多功能可以做,比如直接传token支持配置(其实这个要专门做一个页面),这个作为rc.4的计划之一吧。

+ +

主要没想做用户管理,所以不存在是否接入LinuxDo的问题。虽然那个半成品公益版做好了就是了。

+ +

就说这么多,没啥可说的,不管那么多,做就完了。[doge] 自己想象吧。

+ +

为什么一直说要跑路呢?主要是有时Cursor的Claude太假了,堪比gpt-4o-mini,我对比发现真没啥差别,比以前差远了,无力了,所以不太想做了。我也感觉很奇怪。

+ +

查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。

+ +

有人说少个二维码来着,还是算了。如果觉得好用,给点支持。其实没啥大不了的,没兴趣就不做了。不想那么多了。

\ No newline at end of file diff --git a/static/shared-styles.css b/static/shared-styles.css new file mode 100644 index 0000000000000000000000000000000000000000..7bcd7ed92de6d26bf82c834c140b283db3bced75 --- /dev/null +++ b/static/shared-styles.css @@ -0,0 +1,350 @@ +: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: var(--text-secondary); +} + +/* 按钮组 */ +.button-group { + display: flex; + gap: 10px; + margin: var(--spacing) 0; +} + +/* 消息提示 */ +.message { + padding: 12px; + border-radius: var(--border-radius); + margin: 10px 0; + border: 1px solid transparent; +} + +.success { + background: var(--success-color); + color: #fff; +} + +.error { + background: var(--error-color); + color: #fff; +} + +/* 表格样式 */ +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..5cf6974f4a2f3841c3d749e57e10ce913bd3aa5d --- /dev/null +++ b/static/shared.js @@ -0,0 +1,241 @@ +// Token 管理功能 +function saveAuthToken(token) { + const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期 + localStorage.setItem('authToken', token); + localStorage.setItem('authTokenExpiry', expiryTime); +} + +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; +} + +// 消息显示功能 +function showMessage(elementId, text, isError = false) { + const msg = document.getElementById(elementId); + msg.className = `message ${isError ? 'error' : 'success'}`; + msg.textContent = text; +} + +function showGlobalMessage(text, isError = false) { + showMessage('message', text, isError); + // 3秒后自动清除消息 + setTimeout(() => { + const msg = document.getElementById('message'); + msg.textContent = ''; + msg.className = 'message'; + }, 3000); +} + +// 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); + } +} \ No newline at end of file diff --git a/static/tokeninfo.html b/static/tokeninfo.html new file mode 100644 index 0000000000000000000000000000000000000000..a285e0f8b68764af4c5482e9a0b7b8148eb17b91 --- /dev/null +++ b/static/tokeninfo.html @@ -0,0 +1,127 @@ + + + + + + + + Token 信息管理 + + + + + + + +

Token 信息管理

+ +
+
+ + +
+
+ +
+
+

Token 配置

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ 快捷键: Ctrl + S 保存更改 +
+
+
+ +
+ + + + + \ 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..4fc1df7f764fe68c686956ebda4e19f936ef258a --- /dev/null +++ b/tests/data/stream_data.txt @@ -0,0 +1 @@ +01000005231F8B080000000000000385553D8F1C451025EE981F509060A3F5AC6C0C81395932F6D95C6063F9CEB21CF96A676A669AEDA91EF7C7EE0DBE937046CE0F3004C4C40810093FE50422E22FA0EA9EB9DD8593894E37DBF5EAEBBD57EFBFF3E1EFEFEE9D7EB6FFE0E0D18BC3E78747FB0F4F6FABE736023A0264D01CC818DD1007E89D6D1C761DB919F4764D8E2A580CF0E0F1D1B59B163ACDBA8029B4C5BE1F205868C9F480ECD7E400798097917CD0963D8416038496207A72D0A2872BD147346690AF03ACB531B020C0858D014A5B696EAE164A5D2FE0594BBC09D51ED02F3537505B0754E9E0257368493B89A319F486D013D818FA1800C1EBAE37BAD654C18A9CD796C1D60951DEC3C2D872990B6C75D31ADDB482293FB7C80D79602AC97B740320578055E5A1B45D479C736BAE74898160DD9223889CE3AA0C2FBD2E8818FC52F73D5505DC97C24FB0EB0DDD52C7C7C706B989D8D0AD1E433B0F765E6B436A3E87A228804EB40FD26E022B8A42BD7A95DA7E711DCECEFEFFD58DB7BE3A3E3E5647D3684B64F044A975E2A01D815432036FF3927A47353969D9B219C0115679317D8581A63D24F402BEA843DA9BF679B91D216F68E003BA3027AEA65548A60B168CA39AC1228614F38107BBC4E13D78426BA7C37F6ACC05E91A7C4FA5AE759998E5281150667EC7AC71F0C2EA95AE0810164E530D74D21B640C5B9C189B99416443DE6F987719B4CF892FBA56EA4601F72CB00D60348175D0E15240A1C632F842A98F0A38A801336627BC128A0D368266406135E9866122C5059D1DF9DE7225AFD214A70785523713A73A0C02E3C6979EE469876E59D935174A7D3C4A4906282410A131ADB744E02F92E55E7363532238B807580721402BD83A6834B0C07219748A357A49E0ED2D105AF543682D0B0D13FED959E29AFAE49222B60A48A246DE30351370AC0A8DB73BA5A5D58B6ADE565812AD231F70E44D47A1B515CCA134E87D9E99D4908D6041C6722364DE74B4A3526F3B9ADBD092CB42AD23978940770EEEB6183ED73E58375CB90AAF14002499C9DFAD494C9FF34CF64EF71FDDDBF8B19A1CFAE9E1FE93D3DBEAF156EFB535C6AEA589B45D1F5C2CB3BF0A778C110EEF50C08B261C195A2107D167974CD90D053CB2C044957CC472C9766DA86AE832EC4A3B2A83E88B77D10BB557461F6CF762FBFD6DF56443D5BBAD66015C0C50518DD104A5F61E3E3D3CBA682C7ACAA81D9EE82E7662AD7D0C499368F2ED492B0C76490C46773A649A80D7DC18DA940347ADE625B4E82AF26196A0D3C2AD0F1074470927FD979350B62C09AA887AF2A150EA482C4BFB4D64EF6C6DA3383F5729D0D0C9C569836B133B276F49CDC825211FE06544A3C3301E45A9901C89C66749F2D306881B5198E654472E49733393DB423D71252779F3B514F59468B63EA5DA1C61D02BBAF85C28F58CA08FCE47023A098E3A828AFAD0824361B05809838F3DB95A974936E2EAA1FD14D624C76009E4BD182D1A2145BE8DBBB1AEC692AE195A9101E2D8914BBB4B08636ACD6C57BBA5ED80B4B8D021A20147A5CEAB2F60E4871424E7D9D9D8B42047174D66417AE767D0D98536FA2B4A0A18D5BC4D2147DE46578AA7CB942AEAACD055EC20BD0E2E8A7D37621C2BCAD8E2D4F7AD6B68F4D4DE9A3C723340699D08426E18973403479D5D4D58537B65CEA1398C6917E8A902CBD0C60E1918437499904234DBA7B17103BDD35CEADE909F2ED2DA3A53CDB66E97233429D22EBEA432153DF24BEDCDB3B8F6E6972A73349BD15AFE6D354A9DFFFADD9F6F7EF8EBF5B7E73F7F7DFED337E7BF7DFFC7EB1FFFFEE5CD6EDC3FD3E84DBB490A0000000000000000000000050A03E6889100000000080A06E68BA5E69C8900000000080A06E585B3E4BA8E00000000050A03E7BC9600000000050A03E7A88B00000000050A03E3808100000000080A06E8BDAFE4BBB600000000080A06E5BC80E58F9100000000050A03E3808100000000080A06E7AE97E6B39500000000050A03E3808100000000080A06E695B0E68DAE00000000080A06E7BB93E69E8400000000050A03E3808100000000080A06E69CBAE599A800000000080A06E5ADA6E4B9A000000000050A03E3808100000000080A06E4BABAE5B7A500000000080A06E699BAE883BD00000000050A03E7AD8900000000080A06E5A49AE4B8AA00000000080A06E9A286E59F9F000000000B0A09E79A84E4BFA1E681AF00000000050A03E3808200000000080A06E68891E79A8400000000080A06E79FA5E8AF8600000000050A03E6B6B500000000050A03E79B9600000000050A03E4BA8600000000050A03E5A49A00000000050A03E7A78D00000000050A03E7BC9600000000050A03E7A88B00000000080A06E8AFADE8A88000000000050A03EFBC8800000000050A03E5A68200000000080A06507974686F6E00000000050A03E3808100000000060A044A61766100000000050A03E3808100000000060A044A61766100000000080A0653637269707400000000050A03E3808100000000030A014300000000040A022B2B00000000050A03E7AD8900000000080A06EFBC89E3808100000000080A06E5BC80E58F9100000000050A03E6A18600000000050A03E69EB600000000050A03E3808100000000080A06E5B7A5E585B700000000050A03E5928C00000000080A06E69C80E4BDB300000000080A06E5AE9EE8B7B5000000000B0A09E38082E6ADA4E5A49600000000080A06EFBC8CE6889100000000050A03E8BF9800000000080A06E58FAFE4BBA500000000080A06E68F90E4BE9B00000000080A06E585B3E4BA8E00000000080A06E8AEA1E7AE9700000000050A03E69CBA00000000080A06E7A791E5ADA600000000050A03E79A8400000000080A06E79086E8AEBA00000000080A06E79FA5E8AF8600000000050A03E3808100000000080A06E68A80E69CAF00000000080A06E8B68BE58ABF00000000050A03E5928C00000000080A06E8A18CE4B89A00000000080A06E58AA8E68081000000000B0A09E79A84E4BFA1E681AF00000000070A05E380820A0A00000000080A06E5A682E69E9C00000000050A03E4BDA000000000050A03E69C8900000000080A06E585B7E4BD93000000000B0A09E79A84E997AEE9A29800000000050A03E6889600000000080A06E99C80E8A68100000000080A06E5B8AEE58AA900000000050A03E79A8400000000080A06E59CB0E696B900000000080A06EFBC8CE8AFB700000000080A06E5918AE8AF8900000000050A03E6889100000000080A06EFBC8CE6889100000000050A03E4BC9A00000000050A03E5B0BD00000000050A03E58A9B00000000080A06E68F90E4BE9B00000000080A06E8AFA6E7BB8600000000050A03E5928C00000000080A06E6B7B1E585A500000000050A03E79A8400000000050A03E8A7A300000000050A03E7AD9400000000050A03E3808202000000027B7D \ No newline at end of file