Compare commits
4 Commits
ca2a48c081
...
128f2481c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
128f2481c0 | ||
|
|
84ba209b3f | ||
|
|
eba7d89006 | ||
|
|
765ff2c51d |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
/target
|
||||
config.yaml
|
||||
config.hera.yaml
|
||||
config.*.yaml
|
||||
state.json
|
||||
noc.service
|
||||
state.*.json
|
||||
*.db
|
||||
|
||||
524
Cargo.lock
generated
524
Cargo.lock
generated
@@ -2,6 +2,18 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -39,6 +51,12 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[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.5.0"
|
||||
@@ -51,6 +69,12 @@ version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[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"
|
||||
@@ -246,6 +270,18 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[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 = "fastrand"
|
||||
version = "2.4.0"
|
||||
@@ -382,6 +418,26 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
@@ -406,7 +462,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
@@ -414,6 +470,34 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.4.0",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -429,6 +513,15 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -452,6 +545,16 @@ dependencies = [
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
@@ -459,7 +562,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 1.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -485,9 +611,9 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
@@ -499,6 +625,43 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2 0.4.13",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http 1.4.0",
|
||||
"hyper 1.9.0",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
@@ -506,12 +669,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"hyper 0.14.32",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[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 1.9.0",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.9.0",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"system-configuration 0.7.0",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
@@ -669,6 +873,16 @@ version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.9.0"
|
||||
@@ -714,6 +928,17 @@ version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[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 = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -811,8 +1036,13 @@ name = "noc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"dptree",
|
||||
"libc",
|
||||
"pulldown-cmark",
|
||||
"reqwest 0.12.28",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
@@ -1004,6 +1234,25 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"pulldown-cmark-escape",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-escape"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -1060,16 +1309,16 @@ version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper-tls 0.5.0",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -1083,8 +1332,8 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"sync_wrapper 0.1.2",
|
||||
"system-configuration 0.5.1",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
@@ -1097,6 +1346,76 @@ dependencies = [
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.13",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.9.0",
|
||||
"hyper-rustls",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -1119,13 +1438,46 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1339,6 +1691,12 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -1367,6 +1725,15 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[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.2"
|
||||
@@ -1386,7 +1753,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
"system-configuration-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1399,6 +1777,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[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 = "take_mut"
|
||||
version = "0.2.2"
|
||||
@@ -1455,7 +1843,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"pin-project",
|
||||
"rc-box",
|
||||
"reqwest",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@@ -1487,7 +1875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -1570,6 +1958,16 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
@@ -1594,6 +1992,45 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"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"
|
||||
@@ -1679,6 +2116,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@@ -1691,6 +2134,12 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[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.8"
|
||||
@@ -1716,7 +2165,7 @@ version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
@@ -1926,6 +2375,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -2219,6 +2679,26 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.7"
|
||||
@@ -2240,6 +2720,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
|
||||
@@ -5,11 +5,16 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
dptree = "0.3"
|
||||
libc = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
pulldown-cmark = "0.12"
|
||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
teloxide = { version = "0.12", features = ["macros"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
uuid = { version = "1", features = ["v5"] }
|
||||
|
||||
10
Makefile
10
Makefile
@@ -2,18 +2,22 @@ REPO := $(shell pwd)
|
||||
HERA := heradev
|
||||
HERA_DIR := noc
|
||||
|
||||
.PHONY: build deploy deploy-hera
|
||||
.PHONY: build test deploy deploy-hera
|
||||
|
||||
build:
|
||||
cargo build --release
|
||||
|
||||
test:
|
||||
cargo clippy -- -D warnings
|
||||
cargo test -- --nocapture
|
||||
|
||||
noc.service: noc.service.in
|
||||
sed -e 's|@REPO@|$(REPO)|g' -e 's|@PATH@|$(PATH)|g' $< > $@
|
||||
|
||||
deploy: build noc.service
|
||||
deploy: test build noc.service
|
||||
mkdir -p ~/bin ~/.config/systemd/user
|
||||
systemctl --user stop noc 2>/dev/null || true
|
||||
cp target/release/noc ~/bin/
|
||||
install target/release/noc ~/bin/noc
|
||||
cp noc.service ~/.config/systemd/user/
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now noc
|
||||
|
||||
52
doc/todo.md
52
doc/todo.md
@@ -1,10 +1,44 @@
|
||||
# TODO
|
||||
# noc roadmap
|
||||
|
||||
- [ ] Streaming responses — edit message as claude output arrives instead of waiting for full completion
|
||||
- [ ] Markdown formatting — parse claude output and send with TG MarkdownV2
|
||||
- [ ] Timeout handling — kill claude if it hangs beyond a threshold
|
||||
- [ ] Graceful shutdown on SIGTERM
|
||||
- [ ] `/reset` command to force new session without waiting for 5am
|
||||
- [ ] Rate limiting per chat
|
||||
- [ ] Voice message support — STT (whisper.cpp) → text → claude
|
||||
- [ ] Video/audio file transcription
|
||||
## "会呼吸的助手" — 让 noc 活着
|
||||
|
||||
核心理念:noc 不应该只在收到消息时才被唤醒,而是一个持续运行、有自己节奏的存在。
|
||||
|
||||
### 主动行为
|
||||
- [ ] 定时任务 (cron):LLM 可以自己设置提醒、定期检查
|
||||
- [ ] 事件驱动:监控文件变化、git push、CI 状态等,主动通知
|
||||
- [ ] 晨间/晚间报告:每天自动汇总待办、提醒重要事项
|
||||
- [ ] 情境感知:根据时间、地点、日历自动调整行为
|
||||
|
||||
### 记忆与成长
|
||||
- [ ] 长期记忆 (MEMORY.md):跨 session 的持久化记忆
|
||||
- [ ] 语义搜索:基于 embedding 的记忆检索
|
||||
- [ ] 自我反思:定期回顾对话质量,优化自己的行为
|
||||
|
||||
### 感知能力
|
||||
- [x] 图片理解:multimodal vision input
|
||||
- [ ] 语音转录:whisper API 转文字
|
||||
- [ ] 屏幕/截图分析
|
||||
- [ ] 链接预览/摘要
|
||||
|
||||
### 交互体验
|
||||
- [x] 群组支持:独立上下文
|
||||
- [x] 流式输出:sendMessageDraft + editMessageText
|
||||
- [x] Markdown 渲染
|
||||
- [ ] Typing indicator
|
||||
- [ ] Inline keyboard 交互
|
||||
- [ ] 语音回复 (TTS)
|
||||
|
||||
### 工具生态
|
||||
- [x] 脚本工具发现 (tools/ + --schema)
|
||||
- [x] 异步子代理 (spawn_agent)
|
||||
- [x] 飞书待办管理
|
||||
- [ ] Web search / fetch
|
||||
- [ ] 更多脚本工具
|
||||
- [ ] MCP 协议支持
|
||||
|
||||
### 可靠性
|
||||
- [ ] API 重试策略 (指数退避)
|
||||
- [ ] 用量追踪
|
||||
- [ ] Context pruning (只裁工具输出)
|
||||
- [ ] Model failover
|
||||
|
||||
@@ -10,6 +10,7 @@ ExecStart=%h/bin/noc
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=RUST_LOG=noc=info
|
||||
Environment=RUST_BACKTRACE=1
|
||||
Environment=NOC_CONFIG=@REPO@/config.yaml
|
||||
Environment=NOC_STATE=@REPO@/state.json
|
||||
Environment=PATH=@PATH@
|
||||
|
||||
1693
src/main.rs
1693
src/main.rs
File diff suppressed because it is too large
Load Diff
361
tests/tool_call.rs
Normal file
361
tests/tool_call.rs
Normal file
@@ -0,0 +1,361 @@
|
||||
//! Integration test: verify tool call round-trip with Ollama's OpenAI-compatible API.
|
||||
//! Requires Ollama running at OLLAMA_URL (default: http://100.84.7.49:11434).
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
const OLLAMA_URL: &str = "http://100.84.7.49:11434/v1";
|
||||
const MODEL: &str = "gemma4:31b";
|
||||
|
||||
fn tools() -> serde_json::Value {
|
||||
json!([{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"description": "Calculate a math expression",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {"type": "string", "description": "Math expression to evaluate"}
|
||||
},
|
||||
"required": ["expression"]
|
||||
}
|
||||
}
|
||||
}])
|
||||
}
|
||||
|
||||
/// Test non-streaming tool call round-trip
|
||||
#[tokio::test]
|
||||
async fn test_tool_call_roundtrip_non_streaming() {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{OLLAMA_URL}/chat/completions");
|
||||
|
||||
// Round 1: ask the model to use the calculator
|
||||
let body = json!({
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is 2+2? Use the calculator tool."}
|
||||
],
|
||||
"tools": tools(),
|
||||
});
|
||||
|
||||
let resp = client.post(&url).json(&body).send().await.unwrap();
|
||||
assert!(resp.status().is_success(), "Round 1 failed: {}", resp.status());
|
||||
|
||||
let result: serde_json::Value = resp.json().await.unwrap();
|
||||
let choice = &result["choices"][0];
|
||||
assert_eq!(
|
||||
choice["finish_reason"].as_str().unwrap(),
|
||||
"tool_calls",
|
||||
"Expected tool_calls finish_reason, got: {choice}"
|
||||
);
|
||||
|
||||
let tool_calls = choice["message"]["tool_calls"].as_array().unwrap();
|
||||
assert!(!tool_calls.is_empty(), "No tool calls returned");
|
||||
|
||||
let tc = &tool_calls[0];
|
||||
let call_id = tc["id"].as_str().unwrap();
|
||||
let func_name = tc["function"]["name"].as_str().unwrap();
|
||||
assert_eq!(func_name, "calculator");
|
||||
|
||||
// Round 2: send tool result back
|
||||
let body2 = json!({
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is 2+2? Use the calculator tool."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func_name,
|
||||
"arguments": tc["function"]["arguments"].as_str().unwrap()
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": call_id,
|
||||
"content": "4"
|
||||
}
|
||||
],
|
||||
"tools": tools(),
|
||||
});
|
||||
|
||||
let resp2 = client.post(&url).json(&body2).send().await.unwrap();
|
||||
let status2 = resp2.status();
|
||||
let body2_text = resp2.text().await.unwrap();
|
||||
assert!(
|
||||
status2.is_success(),
|
||||
"Round 2 failed ({status2}): {body2_text}"
|
||||
);
|
||||
|
||||
let result2: serde_json::Value = serde_json::from_str(&body2_text).unwrap();
|
||||
let content = result2["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
assert!(!content.is_empty(), "Expected content in round 2 response");
|
||||
println!("Round 2 response: {content}");
|
||||
}
|
||||
|
||||
/// Test tool call with conversation history (simulates real scenario)
|
||||
#[tokio::test]
|
||||
async fn test_tool_call_with_history() {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{OLLAMA_URL}/chat/completions");
|
||||
|
||||
// Simulate real message history with system prompt
|
||||
let body = json!({
|
||||
"model": MODEL,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个AI助手。你可以使用提供的工具来完成任务。当需要执行命令、运行代码或启动复杂子任务时,直接调用对应的工具,不要只是描述你会怎么做。"},
|
||||
{"role": "user", "content": "hi"},
|
||||
{"role": "assistant", "content": "Hello!"},
|
||||
{"role": "user", "content": "What is 3+4? Use the calculator."}
|
||||
],
|
||||
"tools": tools(),
|
||||
});
|
||||
|
||||
// Round 1: expect tool call
|
||||
let mut resp = client.post(&url).json(&body).send().await.unwrap();
|
||||
assert!(resp.status().is_success(), "Round 1 failed: {}", resp.status());
|
||||
|
||||
let mut buffer = String::new();
|
||||
let mut tc_id = String::new();
|
||||
let mut tc_name = String::new();
|
||||
let mut tc_args = String::new();
|
||||
let mut has_tc = false;
|
||||
|
||||
while let Some(chunk) = resp.chunk().await.unwrap() {
|
||||
buffer.push_str(&String::from_utf8_lossy(&chunk));
|
||||
while let Some(pos) = buffer.find('\n') {
|
||||
let line = buffer[..pos].to_string();
|
||||
buffer = buffer[pos + 1..].to_string();
|
||||
if let Some(data) = line.trim().strip_prefix("data: ") {
|
||||
if data.trim() == "[DONE]" { break; }
|
||||
if let Ok(j) = serde_json::from_str::<serde_json::Value>(data) {
|
||||
if let Some(arr) = j["choices"][0]["delta"]["tool_calls"].as_array() {
|
||||
has_tc = true;
|
||||
for tc in arr {
|
||||
if let Some(id) = tc["id"].as_str() { tc_id = id.into(); }
|
||||
if let Some(n) = tc["function"]["name"].as_str() { tc_name = n.into(); }
|
||||
if let Some(a) = tc["function"]["arguments"].as_str() { tc_args.push_str(a); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_tc, "Expected tool call, got content only");
|
||||
println!("Tool: {tc_name}({tc_args}) id={tc_id}");
|
||||
|
||||
// Round 2: tool result → expect content
|
||||
let body2 = json!({
|
||||
"model": MODEL,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个AI助手。"},
|
||||
{"role": "user", "content": "hi"},
|
||||
{"role": "assistant", "content": "Hello!"},
|
||||
{"role": "user", "content": "What is 3+4? Use the calculator."},
|
||||
{"role": "assistant", "content": "", "tool_calls": [{"id": tc_id, "type": "function", "function": {"name": tc_name, "arguments": tc_args}}]},
|
||||
{"role": "tool", "tool_call_id": tc_id, "content": "7"}
|
||||
],
|
||||
"tools": tools(),
|
||||
});
|
||||
|
||||
let resp2 = client.post(&url).json(&body2).send().await.unwrap();
|
||||
let status = resp2.status();
|
||||
if !status.is_success() {
|
||||
let err = resp2.text().await.unwrap();
|
||||
panic!("Round 2 failed ({status}): {err}");
|
||||
}
|
||||
|
||||
let mut resp2 = client.post(&url).json(&body2).send().await.unwrap();
|
||||
let mut content = String::new();
|
||||
let mut buf2 = String::new();
|
||||
while let Some(chunk) = resp2.chunk().await.unwrap() {
|
||||
buf2.push_str(&String::from_utf8_lossy(&chunk));
|
||||
while let Some(pos) = buf2.find('\n') {
|
||||
let line = buf2[..pos].to_string();
|
||||
buf2 = buf2[pos + 1..].to_string();
|
||||
if let Some(data) = line.trim().strip_prefix("data: ") {
|
||||
if data.trim() == "[DONE]" { break; }
|
||||
if let Ok(j) = serde_json::from_str::<serde_json::Value>(data) {
|
||||
if let Some(c) = j["choices"][0]["delta"]["content"].as_str() {
|
||||
content.push_str(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Final response: {content}");
|
||||
assert!(!content.is_empty(), "Expected non-empty content in round 2");
|
||||
}
|
||||
|
||||
/// Test multimodal image input
|
||||
#[tokio::test]
|
||||
async fn test_image_multimodal() {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{OLLAMA_URL}/chat/completions");
|
||||
|
||||
// 2x2 red PNG generated by PIL
|
||||
let b64 = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVR4nGP8z8DAwMDAxMDAwMDAAAANHQEDasKb6QAAAABJRU5ErkJggg==";
|
||||
|
||||
let body = json!({
|
||||
"model": MODEL,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What color is this image? Reply with just the color name."},
|
||||
{"type": "image_url", "image_url": {"url": format!("data:image/png;base64,{b64}")}}
|
||||
]
|
||||
}],
|
||||
});
|
||||
|
||||
let resp = client.post(&url).json(&body).send().await.unwrap();
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap();
|
||||
assert!(status.is_success(), "Multimodal request failed ({status}): {text}");
|
||||
|
||||
let result: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
let content = result["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
println!("Image description: {content}");
|
||||
assert!(!content.is_empty(), "Expected non-empty response for image");
|
||||
}
|
||||
|
||||
/// Test streaming tool call round-trip (matches our actual code path)
|
||||
#[tokio::test]
|
||||
async fn test_tool_call_roundtrip_streaming() {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{OLLAMA_URL}/chat/completions");
|
||||
|
||||
// Round 1: streaming, get tool calls
|
||||
let body = json!({
|
||||
"model": MODEL,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is 7*6? Use the calculator tool."}
|
||||
],
|
||||
"tools": tools(),
|
||||
});
|
||||
|
||||
let mut resp = client.post(&url).json(&body).send().await.unwrap();
|
||||
assert!(resp.status().is_success(), "Round 1 streaming failed");
|
||||
|
||||
// Parse SSE to extract tool calls
|
||||
let mut buffer = String::new();
|
||||
let mut tool_call_id = String::new();
|
||||
let mut tool_call_name = String::new();
|
||||
let mut tool_call_args = String::new();
|
||||
let mut has_tool_calls = false;
|
||||
|
||||
while let Some(chunk) = resp.chunk().await.unwrap() {
|
||||
buffer.push_str(&String::from_utf8_lossy(&chunk));
|
||||
|
||||
while let Some(pos) = buffer.find('\n') {
|
||||
let line = buffer[..pos].to_string();
|
||||
buffer = buffer[pos + 1..].to_string();
|
||||
|
||||
let trimmed = line.trim();
|
||||
if let Some(data) = trimmed.strip_prefix("data: ") {
|
||||
if data.trim() == "[DONE]" {
|
||||
break;
|
||||
}
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(data) {
|
||||
let delta = &json["choices"][0]["delta"];
|
||||
if let Some(tc_arr) = delta["tool_calls"].as_array() {
|
||||
has_tool_calls = true;
|
||||
for tc in tc_arr {
|
||||
if let Some(id) = tc["id"].as_str() {
|
||||
tool_call_id = id.to_string();
|
||||
}
|
||||
if let Some(name) = tc["function"]["name"].as_str() {
|
||||
tool_call_name = name.to_string();
|
||||
}
|
||||
if let Some(args) = tc["function"]["arguments"].as_str() {
|
||||
tool_call_args.push_str(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_tool_calls, "No tool calls in streaming response");
|
||||
assert_eq!(tool_call_name, "calculator");
|
||||
println!("Tool call: {tool_call_name}({tool_call_args}) id={tool_call_id}");
|
||||
|
||||
// Round 2: send tool result, streaming
|
||||
let body2 = json!({
|
||||
"model": MODEL,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is 7*6? Use the calculator tool."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call_name,
|
||||
"arguments": tool_call_args
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": "42"
|
||||
}
|
||||
],
|
||||
"tools": tools(),
|
||||
});
|
||||
|
||||
let resp2 = client.post(&url).json(&body2).send().await.unwrap();
|
||||
let status2 = resp2.status();
|
||||
if !status2.is_success() {
|
||||
let err = resp2.text().await.unwrap();
|
||||
panic!("Round 2 streaming failed ({status2}): {err}");
|
||||
}
|
||||
|
||||
// Collect content from streaming response
|
||||
let mut resp2 = client
|
||||
.post(&url)
|
||||
.json(&body2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let mut content = String::new();
|
||||
let mut buffer2 = String::new();
|
||||
|
||||
while let Some(chunk) = resp2.chunk().await.unwrap() {
|
||||
buffer2.push_str(&String::from_utf8_lossy(&chunk));
|
||||
while let Some(pos) = buffer2.find('\n') {
|
||||
let line = buffer2[..pos].to_string();
|
||||
buffer2 = buffer2[pos + 1..].to_string();
|
||||
let trimmed = line.trim();
|
||||
if let Some(data) = trimmed.strip_prefix("data: ") {
|
||||
if data.trim() == "[DONE]" {
|
||||
break;
|
||||
}
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(data) {
|
||||
if let Some(c) = json["choices"][0]["delta"]["content"].as_str() {
|
||||
content.push_str(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!content.is_empty(), "Expected content in round 2 streaming");
|
||||
println!("Round 2 streaming content: {content}");
|
||||
}
|
||||
187
tools/manage_todo
Executable file
187
tools/manage_todo
Executable file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["requests"]
|
||||
# ///
|
||||
"""Feishu Bitable todo manager.
|
||||
|
||||
Usage:
|
||||
./fam-todo.py list-undone List open todos
|
||||
./fam-todo.py list-done List completed todos
|
||||
./fam-todo.py add <title> Add a new todo
|
||||
./fam-todo.py mark-done <record_id> Mark as done
|
||||
./fam-todo.py mark-undone <record_id> Mark as undone
|
||||
./fam-todo.py --schema Print tool schema JSON
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import requests
|
||||
|
||||
APP_ID = "cli_a7f042e93d385013"
|
||||
APP_SECRET = "ht4FCjQ8JJ65ZPUWlff6ldFBmaP0mxqY"
|
||||
APP_TOKEN = "SSoGbmGFoazJkUs7bbfcaSG8n7f"
|
||||
TABLE_ID = "tblIA2biceDpvr35"
|
||||
BASE_URL = "https://open.feishu.cn/open-apis"
|
||||
|
||||
ACTIONS = ["list-undone", "list-done", "add", "mark-done", "mark-undone"]
|
||||
|
||||
SCHEMA = {
|
||||
"name": "fam_todo",
|
||||
"description": "管理 Fam 的飞书待办事项表格。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ACTIONS,
|
||||
"description": "操作类型",
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "待办标题 (add 时必填)",
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "记录ID (mark-done/mark-undone 时必填)",
|
||||
},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_token():
|
||||
r = requests.post(
|
||||
f"{BASE_URL}/auth/v3/tenant_access_token/internal/",
|
||||
json={"app_id": APP_ID, "app_secret": APP_SECRET},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()["tenant_access_token"]
|
||||
|
||||
|
||||
def headers():
|
||||
return {"Authorization": f"Bearer {get_token()}", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
def api(method, path, **kwargs):
|
||||
url = f"{BASE_URL}/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}{path}"
|
||||
r = requests.request(method, url, headers=headers(), **kwargs)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def format_field(v):
|
||||
if isinstance(v, list):
|
||||
return "".join(
|
||||
seg.get("text", str(seg)) if isinstance(seg, dict) else str(seg)
|
||||
for seg in v
|
||||
)
|
||||
return str(v)
|
||||
|
||||
|
||||
def list_records(done_filter):
|
||||
"""List records. done_filter: True=done only, False=undone only."""
|
||||
data = api("GET", "/records", params={"page_size": 500})
|
||||
items = data.get("data", {}).get("items", [])
|
||||
if not items:
|
||||
return "No records found."
|
||||
|
||||
lines = []
|
||||
for item in items:
|
||||
fields = item.get("fields", {})
|
||||
is_done = bool(fields.get("Done"))
|
||||
if is_done != done_filter:
|
||||
continue
|
||||
|
||||
rid = item["record_id"]
|
||||
title = format_field(fields.get("Item", ""))
|
||||
priority = fields.get("Priority", "")
|
||||
notes = format_field(fields.get("Notes", ""))
|
||||
|
||||
parts = [f"[{rid}] {title}"]
|
||||
if priority:
|
||||
parts.append(f" P: {priority}")
|
||||
if notes:
|
||||
preview = notes[:80].replace("\n", " ")
|
||||
parts.append(f" Note: {preview}")
|
||||
lines.append("\n".join(parts))
|
||||
|
||||
if not lines:
|
||||
label = "completed" if done_filter else "open"
|
||||
return f"No {label} todos."
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def add_record(title):
|
||||
data = api("POST", "/records", json={"fields": {"Item": title}})
|
||||
rid = data.get("data", {}).get("record", {}).get("record_id", "?")
|
||||
return f"Added [{rid}]: {title}"
|
||||
|
||||
|
||||
def mark_done(record_id):
|
||||
api("PUT", f"/records/{record_id}", json={"fields": {"Done": True}})
|
||||
return f"Marked [{record_id}] as done"
|
||||
|
||||
|
||||
def mark_undone(record_id):
|
||||
api("PUT", f"/records/{record_id}", json={"fields": {"Done": False}})
|
||||
return f"Marked [{record_id}] as undone"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h"):
|
||||
print(__doc__.strip())
|
||||
sys.exit(0)
|
||||
|
||||
if sys.argv[1] == "--schema":
|
||||
print(json.dumps(SCHEMA, ensure_ascii=False))
|
||||
sys.exit(0)
|
||||
|
||||
arg = sys.argv[1]
|
||||
if not arg.startswith("{"):
|
||||
args = {"action": arg}
|
||||
if len(sys.argv) > 2:
|
||||
args["title"] = " ".join(sys.argv[2:])
|
||||
args["record_id"] = sys.argv[2] # also set record_id for mark-*
|
||||
else:
|
||||
try:
|
||||
args = json.loads(arg)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Invalid JSON: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
action = args.get("action", "")
|
||||
try:
|
||||
if action == "list-undone":
|
||||
print(list_records(done_filter=False))
|
||||
elif action == "list-done":
|
||||
print(list_records(done_filter=True))
|
||||
elif action == "add":
|
||||
title = args.get("title", "")
|
||||
if not title:
|
||||
print("Error: title is required")
|
||||
sys.exit(1)
|
||||
print(add_record(title))
|
||||
elif action == "mark-done":
|
||||
rid = args.get("record_id", "")
|
||||
if not rid:
|
||||
print("Error: record_id is required")
|
||||
sys.exit(1)
|
||||
print(mark_done(rid))
|
||||
elif action == "mark-undone":
|
||||
rid = args.get("record_id", "")
|
||||
if not rid:
|
||||
print("Error: record_id is required")
|
||||
sys.exit(1)
|
||||
print(mark_undone(rid))
|
||||
else:
|
||||
print(f"Unknown action: {action}. Valid: {', '.join(ACTIONS)}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user