diff --git a/.env.example b/.env.example index 36a200c..d33989a 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,9 @@ MINERU_API_KEY=your_mineru_api_key LIBRARY_DIR=./library PORT=8000 DATABASE_URL=sqlite://astro_research.db + +# Logging Configuration (Pretty console and rolling file logging) +LOG_LEVEL=info,astroresearch=debug +LOG_FORMAT=pretty +LOG_OUTPUTS=stdout,file +LOG_DIR=./logs diff --git a/.gitignore b/.gitignore index fc7b1bb..df01c9c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # Local literature storage (contains downloaded PDFs, HTML, Markdowns and Translations) /library/ +/logs/ # IDEs and OS files .vscode/ diff --git a/Cargo.lock b/Cargo.lock index ea39267..b34c759 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,23 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", +] + [[package]] name = "ahash" version = "0.8.12" @@ -54,8 +71,9 @@ dependencies = [ "base64 0.22.1", "chrono", "dotenvy", + "flate2", "futures-util", - "hmac", + "hmac 0.12.1", "html2md", "quick-xml", "rand", @@ -63,15 +81,18 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "sqlx", - "thiserror", + "thiserror 1.0.69", "tokio", "tower-http 0.5.2", "tracing", + "tracing-appender", "tracing-subscriber", "url", "urlencoding", + "uuid", + "zip", ] [[package]] @@ -209,6 +230,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", + "zeroize", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -227,6 +258,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "cc" version = "1.2.63" @@ -234,6 +274,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -263,6 +305,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.7" @@ -279,6 +337,18 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "cookie" version = "0.18.1" @@ -334,6 +404,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -343,6 +419,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -358,6 +443,24 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -383,13 +486,37 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -409,12 +536,25 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", + "zeroize", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -504,6 +644,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "flume" version = "0.11.1" @@ -684,10 +835,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -770,7 +923,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -779,7 +932,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -870,6 +1032,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -1093,6 +1264,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1115,7 +1295,7 @@ dependencies = [ "combine", "jni-sys 0.3.1", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -1147,6 +1327,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.99" @@ -1174,6 +1364,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + [[package]] name = "libc" version = "0.2.186" @@ -1242,6 +1438,15 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "lzma-rust2" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02" +dependencies = [ + "sha2 0.11.0", +] + [[package]] name = "mac" version = "0.1.1" @@ -1296,7 +1501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1327,6 +1532,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -1510,6 +1725,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1617,6 +1842,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1841,8 +2072,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2079,8 +2310,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2090,8 +2332,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2125,10 +2378,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "siphasher" version = "1.0.3" @@ -2231,10 +2490,10 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tracing", @@ -2270,7 +2529,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -2294,7 +2553,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -2304,7 +2563,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5", @@ -2314,12 +2573,12 @@ dependencies = [ "rand", "rsa", "serde", - "sha1", - "sha2", + "sha1 0.10.6", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -2344,7 +2603,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", @@ -2354,11 +2613,11 @@ dependencies = [ "rand", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -2435,6 +2694,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -2528,7 +2793,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2542,6 +2816,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -2559,6 +2844,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "js-sys", "num-conv", "powerfmt", "serde_core", @@ -2762,6 +3048,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -2794,6 +3093,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -2804,12 +3113,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -2818,6 +3130,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.20.1" @@ -2911,6 +3229,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3547,8 +3876,81 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.2", + "hmac 0.13.0", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1 0.11.0", + "time", + "typed-path", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 7c1ce46..c8e3c53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,14 @@ path = "src/lib.rs" name = "astroresearch" path = "src/main.rs" +[[bin]] +name = "test_qiniu" +path = "scratch/test_qiniu.rs" + [dependencies] tokio = { version = "1", features = ["full"] } axum = { version = "0.7", features = ["macros"] } -tower-http = { version = "0.5", features = ["cors", "fs"] } +tower-http = { version = "0.5", features = ["cors", "fs", "trace"] } sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -23,7 +27,7 @@ quick-xml = { version = "0.31", features = ["serialize"] } anyhow = "1.0" thiserror = "1.0" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } futures-util = { version = "0.3", features = ["io"] } rand = "0.8" regex = "1.10" @@ -34,3 +38,7 @@ base64 = "0.22" urlencoding = "2.1" url = "2.5" html2md = "0.2" +flate2 = "1.1.9" +zip = "8.6.0" +uuid = { version = "1.23.2", features = ["v4"] } +tracing-appender = "0.2.5" diff --git a/README.md b/README.md index 7375a7f..2e2f735 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ cp .env.example .env - 🏗️ **[架构设计](docs/architecture.md)**:包含系统宏观流程图与序列图。 - 🌐 **[API 接口规范](docs/api.md)**:后端 Axum 路由及 HTTP 接口格式。 - 🗄️ **[数据库设计](docs/database.md)**:SQLite 表结构、ER 图与索引优化。 -- 🎨 **[视觉与交互设计](docs/design.md)**:浅色/深色磨砂玻璃、自研 Canvas 图谱引擎说明。 +- 🎨 **[视觉与交互设计](docs/design.md)**:高对比度浅色中文控制台、自研 Canvas 图谱引擎说明。 - 🛠️ **[排障指南](docs/troubleshooting.md)**:人机校验、解析失败等常见问题解法。 - 🚀 **[编译与部署指南](docs/deployment.md)**:单执行文件打包与发布流程。 - 🤝 **[参与贡献指南](docs/contributing.md)**:开发规范及单元测试。 diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 4801e0c..35123d4 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -17,6 +17,7 @@ "react-dom": "^19.2.6", "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "tailwind-merge": "^3.6.0" }, @@ -3047,6 +3048,15 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3055,6 +3065,32 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", @@ -3078,6 +3114,101 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-math": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", @@ -3285,6 +3416,120 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-math": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", @@ -4028,6 +4273,23 @@ "katex": "cli.js" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-math": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", @@ -4074,6 +4336,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index dc03630..276dc41 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -19,6 +19,7 @@ "react-dom": "^19.2.6", "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "tailwind-merge": "^3.6.0" }, diff --git a/dashboard/public/favicon.svg b/dashboard/public/favicon.svg index 6893eb1..53271d8 100644 --- a/dashboard/public/favicon.svg +++ b/dashboard/public/favicon.svg @@ -1 +1,25 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 0c9f3c2..4b4f06d 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,8 +1,9 @@ // dashboard/src/App.tsx import { useState, useEffect, useCallback } from 'react'; import axios from 'axios'; +import { Loader, Download } from 'lucide-react'; import { Sidebar } from './components/layout/Sidebar'; -import { SearchPanel } from './features/search/SearchPanel'; +import { SearchPanel, getDoctypeBadge } from './features/search/SearchPanel'; import { LibraryPanel } from './features/library/LibraryPanel'; import { ReaderPanel } from './features/reader/ReaderPanel'; import { CitationPanel } from './features/citation/CitationPanel'; @@ -11,15 +12,46 @@ import type { StandardPaper, CitationNetwork, NoteRecord } from './types'; export default function App() { const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'sync'>('search'); + + // 全局对话框弹窗状态 + const [dialog, setDialog] = useState<{ + type: 'alert' | 'confirm'; + title: string; + message: string; + onConfirm: () => void; + onCancel?: () => void; + } | null>(null); + + const showAlert = useCallback((message: string, title = '系统提示') => { + setDialog({ + type: 'alert', + title, + message, + onConfirm: () => {}, + }); + }, []); + + const showConfirm = useCallback((message: string, onConfirm: () => void, title = '确认操作') => { + setDialog({ + type: 'confirm', + title, + message, + onConfirm, + }); + }, []); // 共享数据状态 const [library, setLibrary] = useState([]); const [selectedPaper, setSelectedPaper] = useState(null); + const [detailBibcode, setDetailBibcode] = useState(null); // 检索页状态 const [searchQuery, setSearchQuery] = useState(''); const [searchSource, setSearchSource] = useState<'all' | 'ads' | 'arxiv'>('all'); const [searchResults, setSearchResults] = useState([]); + + // 衍生状态计算 + const detailPaper = library.find(p => p.bibcode === detailBibcode) || searchResults.find(p => p.bibcode === detailBibcode); const [searching, setSearching] = useState(false); const [exportingList, setExportingList] = useState([]); const [bibtexContent, setBibtexContent] = useState(null); @@ -39,6 +71,7 @@ export default function App() { const [citationNetwork, setCitationNetwork] = useState(null); const [loadingCitations, setLoadingCitations] = useState(false); const [citationHistory, setCitationHistory] = useState([]); // 多跳历史 + const [uncachedBibcode, setUncachedBibcode] = useState(null); // 笔记系统状态 const [notes, setNotes] = useState([]); @@ -87,7 +120,7 @@ export default function App() { setSearchCache(prev => ({ ...prev, [cacheKey]: res.data })); } catch (e) { console.error('检索文献失败', e); - alert('检索失败,请确认后端连接及 API 密钥配置。'); + showAlert('检索失败,请确认后端连接及 API 密钥配置。', '检索出错'); } finally { setSearching(false); } @@ -135,7 +168,7 @@ export default function App() { } } catch (e) { console.error('下载文献失败', e); - alert('文献下载失败,请检查 ADS 网络限制与网络代理!'); + showAlert('文献下载失败,请检查 ADS 网络限制与网络代理!', '下载失败'); } finally { setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: false })); } @@ -155,7 +188,7 @@ export default function App() { } } catch (e) { console.error('文献解析失败', e); - alert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。'); + showAlert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', '解析失败'); } finally { setParsing(false); } @@ -174,7 +207,7 @@ export default function App() { } } catch (e) { console.error('文献翻译失败', e); - alert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。'); + showAlert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', '翻译失败'); } finally { setTranslating(false); } @@ -246,7 +279,7 @@ export default function App() { setBibtexContent(res.data.bibtex); } catch (e) { console.error('导出 BibTeX 失败', e); - alert('导出 BibTeX 失败,请检查 ADS Token。'); + showAlert('导出 BibTeX 失败,请检查 ADS Token。', '导出失败'); } finally { setExporting(false); } @@ -299,14 +332,8 @@ export default function App() { }; return ( -
+
- {/* 炫酷淡雅背景装饰 */} -
-
-
-
- {/* 导航左侧栏 */} {/* 主工作区 */} -
- {/* 顶部状态条 */} -
-
- - 后端服务连接正常 -
-
- 文献库: {library.length} 篇 -
-
- +
{/* 选项卡容器 */} -
- {activeTab === 'search' && ( - - )} +
+
+ {activeTab === 'search' && ( + setDetailBibcode(paper.bibcode)} + /> + )} - {activeTab === 'library' && ( - - )} + {activeTab === 'library' && ( + setDetailBibcode(paper.bibcode)} + /> + )} - {activeTab === 'reader' && selectedPaper && ( - - )} + {activeTab === 'reader' && selectedPaper && ( + + )} - {activeTab === 'citation' && ( - - )} + {activeTab === 'citation' && ( + + )} - {activeTab === 'sync' && ( - - )} + {activeTab === 'sync' && ( + + )} +
+ + {/* 全局统一样式弹窗 */} + {dialog && ( +
{ + if (dialog.onCancel) dialog.onCancel(); + setDialog(null); + }} + className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all" + > +
e.stopPropagation()} + className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4" + > +
+

+ + {dialog.title} +

+ +
+
+

{dialog.message}

+
+
+ + {dialog.type === 'confirm' && ( + + )} +
+
+
+ )} + + {/* 未入库文献操作弹窗 */} + {uncachedBibcode && ( +
setUncachedBibcode(null)} + className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in" + > +
e.stopPropagation()} + className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4" + > +
+

+ + 文献尚未入库 +

+ +
+
+

+ 文献 {uncachedBibcode} 尚未收录在本地数据库中。 +

+

+ 您可以选择在线拉取该文献元数据并入库,或是直接跳转至 NASA ADS 平台查看其原始页面。 +

+
+
+ + + +
+
+
+ )} + + {/* 文献详情弹窗 */} + {detailPaper && ( +
setDetailBibcode(null)} + className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in cursor-pointer" + > +
e.stopPropagation()} + className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-lg w-full p-6 space-y-4 cursor-default animate-fade-in" + > + {/* 标题 & 关闭 */} +
+
+ 文献详情元数据 +

+ {getDoctypeBadge(detailPaper.doctype)} + {detailPaper.title} +

+
+ +
+ + {/* 详情内容 */} +
+ {/* 作者 */} +
+ 作者列表 +

{detailPaper.authors.join(', ')}

+
+ + {/* 期刊 & 年份 */} +
+
+ 发表期刊 + {detailPaper.pub_journal || '未标注'} +
+
+ 发表年份 + {detailPaper.year} +
+
+ + {/* 摘要 */} +
+ 文献摘要 (Abstract) +

+ {detailPaper.abstract_text || '该文献暂无摘要数据。'} +

+
+ + {/* 关键字 */} + {detailPaper.keywords && detailPaper.keywords.length > 0 && ( +
+ 学科关键字 (Keywords) +
+ {detailPaper.keywords.map(kw => ( + + {kw} + + ))} +
+
+ )} + + {/* 标识符 */} +
+
+ BIBCODE + + {detailPaper.bibcode === detailPaper.arxiv_id ? '暂无' : ( + + {detailPaper.bibcode} + + )} + +
+
+ DOI + + {detailPaper.doi ? ( + + {detailPaper.doi} + + ) : '无'} + +
+
+ ARXIV ID + + {detailPaper.arxiv_id ? ( + + {detailPaper.arxiv_id} + + ) : '无'} + +
+
+
+ + {/* 底部操作:整合所有动作(阅读、图谱、下载) */} +
+ {detailPaper.is_downloaded ? ( + <> + + + + + ) : ( + <> + + + + )} +
+
+
+ )}
); } diff --git a/dashboard/src/components/CitationGalaxyCanvas.tsx b/dashboard/src/components/CitationGalaxyCanvas.tsx index 886065a..c422c4c 100644 --- a/dashboard/src/components/CitationGalaxyCanvas.tsx +++ b/dashboard/src/components/CitationGalaxyCanvas.tsx @@ -5,6 +5,7 @@ import type { CitationNetwork } from '../types'; interface CanvasProps { networks: CitationNetwork[]; activeNetwork: CitationNetwork; + nodeLimit: number; onNodeClick: (bibcode: string) => void; } @@ -18,6 +19,7 @@ interface Node { radius: number; color: string; type: 'center' | 'reference' | 'citation'; + inDb: boolean; } interface Link { @@ -25,7 +27,7 @@ interface Link { target: string; } -export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: CanvasProps) { +export function CitationGalaxyCanvas({ networks, activeNetwork, nodeLimit, onNodeClick }: CanvasProps) { const canvasRef = useRef(null); useEffect(() => { @@ -34,24 +36,26 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C const ctx = canvas.getContext('2d'); if (!ctx) return; - // 适配高清屏幕像素比 const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); - // 合并所有 networks 的节点,去重,最多 50 个 - const MAX_NODES = 50; + const MAX_NODES = nodeLimit; const allIds = new Set(); const nodes: Node[] = []; const links: Link[] = []; networks.forEach((net, netIdx) => { const isActive = net.bibcode === activeNetwork.bibcode; - // 添加中心节点 if (!allIds.has(net.bibcode) && nodes.length < MAX_NODES) { allIds.add(net.bibcode); + // 根据被引用数量决定中心节点大小 (起步半径 16,按被引量上限 32 比例缩放) + const citeCount = net.citation_count || 0; + const radius = isActive + ? Math.min(32, Math.max(18, 16 + citeCount / 50)) + : Math.min(24, Math.max(12, 11 + citeCount / 100)); nodes.push({ id: net.bibcode, label: net.bibcode, @@ -59,20 +63,21 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C y: rect.height / 2 + (netIdx === 0 ? 0 : (Math.random() - 0.5) * 200), vx: 0, vy: 0, - radius: isActive ? 24 : 16, - color: isActive ? '#a855f7' : '#6366f1', + radius, + color: isActive ? '#0284c7' : '#475569', type: 'center', + inDb: true, }); } - // 添加参考文献节点 net.references.forEach((ref, idx) => { if (nodes.length >= MAX_NODES) return; if (!allIds.has(ref)) { allIds.add(ref); const angle = (idx / Math.max(1, net.references.length)) * Math.PI * 2; - const dist = 140 + Math.random() * 30; + const dist = 120 + Math.random() * 30; const centerNode = nodes.find(n => n.id === net.bibcode); + const inDb = activeNetwork.citation_counts ? Object.prototype.hasOwnProperty.call(activeNetwork.citation_counts, ref) : false; nodes.push({ id: ref, label: ref, @@ -80,9 +85,10 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C y: (centerNode?.y ?? rect.height / 2) + Math.sin(angle) * dist, vx: 0, vy: 0, - radius: 12, + radius: 8, // 初始值,稍后按连线数重算 color: '#d97706', type: 'reference', + inDb, }); } if (allIds.has(ref)) { @@ -90,14 +96,14 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C } }); - // 添加被引文献节点 net.citations.forEach((cit, idx) => { if (nodes.length >= MAX_NODES) return; if (!allIds.has(cit)) { allIds.add(cit); const angle = (idx / Math.max(1, net.citations.length)) * Math.PI * 2 + Math.PI; - const dist = 160 + Math.random() * 40; + const dist = 140 + Math.random() * 40; const centerNode = nodes.find(n => n.id === net.bibcode); + const inDb = activeNetwork.citation_counts ? Object.prototype.hasOwnProperty.call(activeNetwork.citation_counts, cit) : false; nodes.push({ id: cit, label: cit, @@ -105,9 +111,10 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C y: (centerNode?.y ?? rect.height / 2) + Math.sin(angle) * dist, vx: 0, vy: 0, - radius: 12, - color: '#4f46e5', + radius: 8, // 初始值,稍后按连线数重算 + color: '#0891b2', type: 'citation', + inDb, }); } if (allIds.has(cit)) { @@ -116,24 +123,46 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C }); }); + // 计算外围节点在当前渲染网络中的度数(连线数),动态微调其半径大小,凸显网络枢纽节点 + const degrees: Record = {}; + links.forEach(l => { + degrees[l.source] = (degrees[l.source] || 0) + 1; + degrees[l.target] = (degrees[l.target] || 0) + 1; + }); + + nodes.forEach(n => { + if (n.type !== 'center') { + const deg = degrees[n.id] || 0; + const citeCount = activeNetwork.citation_counts?.[n.id] || 0; + // 融合数据库中该文献的被引数量 (起步+citeCount/40) 与当前渲染网格连线度数,动态决定半径 (最大限制 18) + n.radius = Math.min(18, Math.max(6, 6 + citeCount / 40 + Math.min(6, deg * 1.5))); + } + }); + + let offsetX = 0; + let offsetY = 0; + let scale = 1.0; + let isDragging = false; + let dragStartX = 0; + let dragStartY = 0; + let hasDragged = false; + let animationFrameId: number; let hoveredNode: Node | null = null; + let frameCount = 0; - // 经典力导向算法迭代 const updatePhysics = () => { - // 1. 斥力:任何两个节点之间均产生反向推力 for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { let dx = nodes[j].x - nodes[i].x; let dy = nodes[j].y - nodes[i].y; let dist = Math.sqrt(dx * dx + dy * dy) || 1; - let minDist = nodes[i].radius + nodes[j].radius + 50; + let minDist = nodes[i].radius + nodes[j].radius + 60; if (dist < minDist) { let force = (minDist - dist) * 0.08; let fx = (dx / dist) * force; let fy = (dy / dist) * force; - // 节点不强行推动中心大节点 if (nodes[i].type !== 'center' || nodes[i].id !== activeNetwork.bibcode) { nodes[i].vx -= fx; nodes[i].vy -= fy; @@ -146,7 +175,6 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C } } - // 2. 引力与向心力:被连线连接的节点之间产生向中心靠拢力 links.forEach(link => { const sourceNode = nodes.find(n => n.id === link.source); const targetNode = nodes.find(n => n.id === link.target); @@ -154,7 +182,7 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C let dx = targetNode.x - sourceNode.x; let dy = targetNode.y - sourceNode.y; let dist = Math.sqrt(dx * dx + dy * dy) || 1; - let force = dist * 0.003; // 弹性系数 + let force = dist * 0.003; let fx = (dx / dist) * force; let fy = (dy / dist) * force; @@ -169,23 +197,54 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C } }); - // 3. 应用阻尼阻力,限制极限加速 nodes.forEach(node => { if (node.id !== activeNetwork.bibcode) { node.x += node.vx; node.y += node.vy; - node.vx *= 0.85; // 阻尼 + node.vx *= 0.85; node.vy *= 0.85; } }); }; - // 画布渲染渲染循环 const render = () => { + frameCount++; updatePhysics(); ctx.clearRect(0, 0, rect.width, rect.height); + ctx.save(); + // 应用拖拽和缩放的坐标变换 + const cx = rect.width / 2; + const cy = rect.height / 2; + ctx.translate(cx + offsetX, cy + offsetY); + ctx.scale(scale, scale); + ctx.translate(-cx, -cy); + + // 绘制背景宇宙引力线 & 刻度圈 + const centerNode = nodes.find(n => n.id === activeNetwork.bibcode); + if (centerNode) { + ctx.strokeStyle = 'rgba(148, 163, 184, 0.15)'; + ctx.lineWidth = 1; + ctx.setLineDash([4, 6]); + + ctx.beginPath(); + ctx.arc(centerNode.x, centerNode.y, 130, 0, Math.PI * 2); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(centerNode.x, centerNode.y, 180, 0, Math.PI * 2); + ctx.stroke(); + + // 动态圈 + ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)'; + ctx.setLineDash([]); + const pulseRadius = 130 + (frameCount % 120) * 0.4; + ctx.beginPath(); + ctx.arc(centerNode.x, centerNode.y, pulseRadius, 0, Math.PI * 2); + ctx.stroke(); + } + // 绘制连线 ctx.lineWidth = 1; links.forEach(link => { @@ -195,7 +254,7 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C ctx.beginPath(); ctx.moveTo(sourceNode.x, sourceNode.y); ctx.lineTo(targetNode.x, targetNode.y); - ctx.strokeStyle = sourceNode.type === 'reference' ? 'rgba(245, 158, 11, 0.25)' : 'rgba(129, 140, 248, 0.25)'; + ctx.strokeStyle = 'rgba(148, 163, 184, 0.25)'; ctx.stroke(); } }); @@ -203,62 +262,137 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, onNodeClick }: C // 绘制节点 nodes.forEach(node => { const isHovered = hoveredNode?.id === node.id; + + ctx.save(); + ctx.globalAlpha = node.inDb ? 1.0 : 0.35; // 未入库文献透明度降为 0.35 + ctx.beginPath(); - ctx.arc(node.x, node.y, node.radius + (isHovered ? 4 : 0), 0, Math.PI * 2); + ctx.arc(node.x, node.y, node.radius + (isHovered ? 6 : 3), 0, Math.PI * 2); + ctx.fillStyle = node.color + (isHovered ? '25' : '0f'); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2); ctx.fillStyle = node.color; ctx.fill(); - // 绘制光晕环绕 ctx.beginPath(); - ctx.arc(node.x, node.y, node.radius + (isHovered ? 8 : 4), 0, Math.PI * 2); - ctx.strokeStyle = node.color + '40'; // 附加透明度光晕 - ctx.lineWidth = 2; + ctx.arc(node.x, node.y, node.radius + (isHovered ? 4 : 2), 0, Math.PI * 2); + ctx.strokeStyle = node.color + '40'; + ctx.lineWidth = 1; ctx.stroke(); - // 绘制 bibcode 文本说明 - ctx.fillStyle = isHovered ? '#0f172a' : '#64748b'; - ctx.font = isHovered ? 'bold 10px monospace' : '9px monospace'; + ctx.fillStyle = isHovered ? '#0284c7' : '#334155'; + ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(node.label, node.x, node.y + node.radius + (isHovered ? 18 : 14)); + ctx.fillText(node.label, node.x, node.y + node.radius + (isHovered ? 16 : 12)); + + ctx.restore(); }); + ctx.restore(); + animationFrameId = requestAnimationFrame(render); }; render(); - // 交互鼠标监听 + const handleMouseDown = (e: MouseEvent) => { + isDragging = true; + dragStartX = e.clientX - offsetX; + dragStartY = e.clientY - offsetY; + hasDragged = false; + }; + const handleMouseMove = (e: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; + if (isDragging) { + const dx = e.clientX - dragStartX; + const dy = e.clientY - dragStartY; + if (Math.sqrt((dx - offsetX) ** 2 + (dy - offsetY) ** 2) > 3) { + hasDragged = true; + } + offsetX = dx; + offsetY = dy; + } + + const cx = rect.width / 2; + const cy = rect.height / 2; + const gx = (mouseX - cx - offsetX) / scale + cx; + const gy = (mouseY - cy - offsetY) / scale + cy; + let found: Node | null = null; for (const node of nodes) { - let dx = node.x - mouseX; - let dy = node.y - mouseY; + let dx = node.x - gx; + let dy = node.y - gy; let dist = Math.sqrt(dx * dx + dy * dy); - if (dist < node.radius + 5) { + if (dist < node.radius + 6) { found = node; break; } } hoveredNode = found; - canvas.style.cursor = found ? 'pointer' : 'default'; + + if (isDragging) { + canvas.style.cursor = 'grabbing'; + } else { + canvas.style.cursor = found ? 'pointer' : 'grab'; + } + }; + + const handleMouseUp = () => { + isDragging = false; + }; + + const handleMouseLeave = () => { + isDragging = false; }; const handleCanvasClick = () => { + if (hasDragged) return; if (hoveredNode && hoveredNode.id !== activeNetwork.bibcode) { onNodeClick(hoveredNode.id); } }; + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + const cx = rect.width / 2; + const cy = rect.height / 2; + const gx = (mouseX - cx - offsetX) / scale + cx; + const gy = (mouseY - cy - offsetY) / scale + cy; + + if (e.deltaY < 0) { + scale = Math.min(5.0, scale * 1.1); + } else { + scale = Math.max(0.15, scale / 1.1); + } + + offsetX = mouseX - cx - (gx - cx) * scale; + offsetY = mouseY - cy - (gy - cy) * scale; + }; + + canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); + canvas.addEventListener('mouseup', handleMouseUp); + canvas.addEventListener('mouseleave', handleMouseLeave); canvas.addEventListener('click', handleCanvasClick); + canvas.addEventListener('wheel', handleWheel, { passive: false }); return () => { cancelAnimationFrame(animationFrameId); + canvas.removeEventListener('mousedown', handleMouseDown); canvas.removeEventListener('mousemove', handleMouseMove); + canvas.removeEventListener('mouseup', handleMouseUp); + canvas.removeEventListener('mouseleave', handleMouseLeave); canvas.removeEventListener('click', handleCanvasClick); + canvas.removeEventListener('wheel', handleWheel); }; }, [networks, activeNetwork, onNodeClick]); diff --git a/dashboard/src/components/layout/Sidebar.tsx b/dashboard/src/components/layout/Sidebar.tsx index 3d3f98c..898081b 100644 --- a/dashboard/src/components/layout/Sidebar.tsx +++ b/dashboard/src/components/layout/Sidebar.tsx @@ -11,25 +11,38 @@ interface SidebarProps { export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations }: SidebarProps) { return ( -