mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-06 07:57:12 +08:00
添加扫码登录功能 (#601)
* feat: 添加扫码登录功能,支持生成二维码并轮询登录状态 * feat: 增强扫码登录功能的测试,完善二维码生成与状态轮询的文档注释 * refactor: 后端改动之:1)拆分 login 到 credential 中;2)扫码登录和刷新凭据时复用相同的 extract 函数;3)精简注释。 * refactor: 前端改动之:1)扫码在单独的弹窗页处理;2)不同 status 下采用相同布局,避免状态变化导致布局跳动 * format --------- Co-authored-by: zkl <i@zkl2333.com>
This commit is contained in:
@@ -123,3 +123,8 @@ pub struct UpdateVideoSourceRequest {
|
|||||||
pub struct DefaultPathRequest {
|
pub struct DefaultPathRequest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct PollQrcodeRequest {
|
||||||
|
pub qrcode_key: String,
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use bili_sync_entity::*;
|
|||||||
use sea_orm::{DerivePartialModel, FromQueryResult};
|
use sea_orm::{DerivePartialModel, FromQueryResult};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::bilibili::{PollStatus, Qrcode};
|
||||||
use crate::utils::status::{PageStatus, VideoStatus};
|
use crate::utils::status::{PageStatus, VideoStatus};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -213,3 +214,7 @@ pub struct VideoSourceDetail {
|
|||||||
pub struct UpdateVideoSourceResponse {
|
pub struct UpdateVideoSourceResponse {
|
||||||
pub rule_display: Option<String>,
|
pub rule_display: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type GenerateQrcodeResponse = Qrcode;
|
||||||
|
|
||||||
|
pub type PollQrcodeResponse = PollStatus;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::{Extension, Query};
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
|
||||||
|
use crate::api::request::PollQrcodeRequest;
|
||||||
|
use crate::api::response::{GenerateQrcodeResponse, PollQrcodeResponse};
|
||||||
|
use crate::api::wrapper::{ApiError, ApiResponse};
|
||||||
|
use crate::bilibili::{BiliClient, Credential};
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/login/qrcode/generate", post(generate_qrcode))
|
||||||
|
.route("/login/qrcode/poll", get(poll_qrcode))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成扫码登录二维码
|
||||||
|
pub async fn generate_qrcode(
|
||||||
|
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||||
|
) -> Result<ApiResponse<GenerateQrcodeResponse>, ApiError> {
|
||||||
|
Ok(ApiResponse::ok(Credential::generate_qrcode(&bili_client.client).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 轮询扫码登录状态
|
||||||
|
pub async fn poll_qrcode(
|
||||||
|
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||||
|
Query(params): Query<PollQrcodeRequest>,
|
||||||
|
) -> Result<ApiResponse<PollQrcodeResponse>, ApiError> {
|
||||||
|
Ok(ApiResponse::ok(
|
||||||
|
Credential::poll_qrcode(&bili_client.client, ¶ms.qrcode_key).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ use crate::config::VersionedConfig;
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod dashboard;
|
mod dashboard;
|
||||||
|
mod login;
|
||||||
mod me;
|
mod me;
|
||||||
mod task;
|
mod task;
|
||||||
mod video_sources;
|
mod video_sources;
|
||||||
@@ -25,6 +26,7 @@ pub fn router() -> Router {
|
|||||||
"/api",
|
"/api",
|
||||||
config::router()
|
config::router()
|
||||||
.merge(me::router())
|
.merge(me::router())
|
||||||
|
.merge(login::router())
|
||||||
.merge(video_sources::router())
|
.merge(video_sources::router())
|
||||||
.merge(videos::router())
|
.merge(videos::router())
|
||||||
.merge(dashboard::router())
|
.merge(dashboard::router())
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rsa::sha2::Sha256;
|
|||||||
use rsa::{Oaep, RsaPublicKey};
|
use rsa::{Oaep, RsaPublicKey};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::bilibili::{Client, Validate};
|
use crate::bilibili::{BiliError, Client, Validate};
|
||||||
|
|
||||||
const MIXIN_KEY_ENC_TAB: [usize; 64] = [
|
const MIXIN_KEY_ENC_TAB: [usize; 64] = [
|
||||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38,
|
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38,
|
||||||
@@ -17,6 +17,13 @@ const MIXIN_KEY_ENC_TAB: [usize; 64] = [
|
|||||||
20, 34, 44, 52,
|
20, 34, 44, 52,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
mod qrcode_status_code {
|
||||||
|
pub const SUCCESS: i64 = 0;
|
||||||
|
pub const NOT_SCANNED: i64 = 86101;
|
||||||
|
pub const SCANNED_UNCONFIRMED: i64 = 86090;
|
||||||
|
pub const EXPIRED: i64 = 86038;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Credential {
|
pub struct Credential {
|
||||||
pub sessdata: String,
|
pub sessdata: String,
|
||||||
@@ -32,6 +39,28 @@ pub struct WbiImg {
|
|||||||
pub(crate) sub_url: String,
|
pub(crate) sub_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Qrcode {
|
||||||
|
pub url: String,
|
||||||
|
pub qrcode_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "status", rename_all = "snake_case")]
|
||||||
|
pub enum PollStatus {
|
||||||
|
Success {
|
||||||
|
credential: Credential,
|
||||||
|
},
|
||||||
|
Pending {
|
||||||
|
message: String,
|
||||||
|
#[serde(default)]
|
||||||
|
scanned: bool,
|
||||||
|
},
|
||||||
|
Expired {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
impl WbiImg {
|
impl WbiImg {
|
||||||
pub fn into_mixin_key(self) -> Option<String> {
|
pub fn into_mixin_key(self) -> Option<String> {
|
||||||
let key = match (get_filename(self.img_url.as_str()), get_filename(self.sub_url.as_str())) {
|
let key = match (get_filename(self.img_url.as_str()), get_filename(self.sub_url.as_str())) {
|
||||||
@@ -56,6 +85,78 @@ impl Credential {
|
|||||||
Ok(serde_json::from_value(res["data"]["wbi_img"].take())?)
|
Ok(serde_json::from_value(res["data"]["wbi_img"].take())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn generate_qrcode(client: &Client) -> Result<Qrcode> {
|
||||||
|
let mut res = client
|
||||||
|
.request(
|
||||||
|
Method::GET,
|
||||||
|
"https://passport.bilibili.com/x/passport-login/web/qrcode/generate",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await?
|
||||||
|
.validate()?;
|
||||||
|
Ok(serde_json::from_value(res["data"].take())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn poll_qrcode(client: &Client, qrcode_key: &str) -> Result<PollStatus> {
|
||||||
|
let mut resp = client
|
||||||
|
.request(
|
||||||
|
Method::GET,
|
||||||
|
"https://passport.bilibili.com/x/passport-login/web/qrcode/poll",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.query(&[("qrcode_key", qrcode_key)])
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
let headers = std::mem::take(resp.headers_mut());
|
||||||
|
let json = resp.json::<serde_json::Value>().await?.validate()?;
|
||||||
|
let code = json["data"]["code"].as_i64().context("missing 'code' field in data")?;
|
||||||
|
|
||||||
|
match code {
|
||||||
|
qrcode_status_code::SUCCESS => {
|
||||||
|
let mut credential = Self::extract(headers, json)?;
|
||||||
|
credential.buvid3 = Self::get_buvid3(client).await?;
|
||||||
|
Ok(PollStatus::Success { credential })
|
||||||
|
}
|
||||||
|
qrcode_status_code::NOT_SCANNED => Ok(PollStatus::Pending {
|
||||||
|
message: "未扫描".to_owned(),
|
||||||
|
scanned: false,
|
||||||
|
}),
|
||||||
|
qrcode_status_code::SCANNED_UNCONFIRMED => Ok(PollStatus::Pending {
|
||||||
|
message: "已扫描,请在手机上确认登录".to_owned(),
|
||||||
|
scanned: true,
|
||||||
|
}),
|
||||||
|
qrcode_status_code::EXPIRED => Ok(PollStatus::Expired {
|
||||||
|
message: "二维码已过期".to_owned(),
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
bail!(BiliError::InvalidResponse(json.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 buvid3 浏览器指纹
|
||||||
|
///
|
||||||
|
/// 参考 https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/buvid3_4.md
|
||||||
|
async fn get_buvid3(client: &Client) -> Result<String> {
|
||||||
|
let resp = client
|
||||||
|
.request(Method::GET, "https://api.bilibili.com/x/web-frontend/getbuvid", None)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await?
|
||||||
|
.validate()?;
|
||||||
|
resp["data"]["buvid"]
|
||||||
|
.as_str()
|
||||||
|
.context("missing 'buvid' field in data")
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// 检查凭据是否有效
|
/// 检查凭据是否有效
|
||||||
pub async fn need_refresh(&self, client: &Client) -> Result<bool> {
|
pub async fn need_refresh(&self, client: &Client) -> Result<bool> {
|
||||||
let res = client
|
let res = client
|
||||||
@@ -124,7 +225,7 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> {
|
async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> {
|
||||||
let mut res = client
|
let mut resp = client
|
||||||
.request(
|
.request(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"https://passport.bilibili.com/x/passport-login/web/cookie/refresh",
|
"https://passport.bilibili.com/x/passport-login/web/cookie/refresh",
|
||||||
@@ -141,37 +242,10 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
|||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
// 必须在 .json 前取出 headers,否则 res 会被消耗
|
let headers = std::mem::take(resp.headers_mut());
|
||||||
let headers = std::mem::take(res.headers_mut());
|
let json = resp.json::<serde_json::Value>().await?.validate()?;
|
||||||
let res = res.json::<serde_json::Value>().await?.validate()?;
|
let mut credential = Self::extract(headers, json)?;
|
||||||
let set_cookies = headers.get_all(header::SET_COOKIE);
|
credential.buvid3 = self.buvid3.clone();
|
||||||
let mut credential = Self {
|
|
||||||
buvid3: self.buvid3.clone(),
|
|
||||||
..Self::default()
|
|
||||||
};
|
|
||||||
let required_cookies = HashSet::from(["SESSDATA", "bili_jct", "DedeUserID"]);
|
|
||||||
let cookies: Vec<Cookie> = set_cookies
|
|
||||||
.iter()
|
|
||||||
.filter_map(|x| x.to_str().ok())
|
|
||||||
.filter_map(|x| Cookie::parse(x).ok())
|
|
||||||
.filter(|x| required_cookies.contains(x.name()))
|
|
||||||
.collect();
|
|
||||||
ensure!(
|
|
||||||
cookies.len() == required_cookies.len(),
|
|
||||||
"not all required cookies found"
|
|
||||||
);
|
|
||||||
for cookie in cookies {
|
|
||||||
match cookie.name() {
|
|
||||||
"SESSDATA" => credential.sessdata = cookie.value().to_string(),
|
|
||||||
"bili_jct" => credential.bili_jct = cookie.value().to_string(),
|
|
||||||
"DedeUserID" => credential.dedeuserid = cookie.value().to_string(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match res["data"]["refresh_token"].as_str() {
|
|
||||||
Some(token) => credential.ac_time_value = token.to_string(),
|
|
||||||
None => bail!("refresh_token not found"),
|
|
||||||
}
|
|
||||||
Ok(credential)
|
Ok(credential)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +269,36 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
|||||||
.validate()?;
|
.validate()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 解析 header 和 json,获取除 buvid3 字段外全部填充的 Credential
|
||||||
|
fn extract(headers: header::HeaderMap, json: serde_json::Value) -> Result<Credential> {
|
||||||
|
let mut credential = Credential::default();
|
||||||
|
let required_cookies = HashSet::from(["SESSDATA", "bili_jct", "DedeUserID"]);
|
||||||
|
let cookies: Vec<Cookie> = headers
|
||||||
|
.get_all(header::SET_COOKIE)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.to_str().ok())
|
||||||
|
.filter_map(|x| Cookie::parse(x).ok())
|
||||||
|
.filter(|x| required_cookies.contains(x.name()))
|
||||||
|
.collect();
|
||||||
|
ensure!(
|
||||||
|
cookies.len() == required_cookies.len(),
|
||||||
|
"not all required cookies found"
|
||||||
|
);
|
||||||
|
for cookie in cookies {
|
||||||
|
match cookie.name() {
|
||||||
|
"SESSDATA" => credential.sessdata = cookie.value().to_string(),
|
||||||
|
"bili_jct" => credential.bili_jct = cookie.value().to_string(),
|
||||||
|
"DedeUserID" => credential.dedeuserid = cookie.value().to_string(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match json["data"]["refresh_token"].as_str() {
|
||||||
|
Some(token) => credential.ac_time_value = token.to_string(),
|
||||||
|
None => bail!("refresh_token not found"),
|
||||||
|
}
|
||||||
|
Ok(credential)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用指定的 pattern 正则表达式在 doc 中查找,返回第一个匹配的捕获组
|
// 用指定的 pattern 正则表达式在 doc 中查找,返回第一个匹配的捕获组
|
||||||
@@ -246,4 +350,94 @@ mod tests {
|
|||||||
"bar=%E4%BA%94%E4%B8%80%E5%9B%9B&baz=1919810&foo=one%20one%20four"
|
"bar=%E4%BA%94%E4%B8%80%E5%9B%9B&baz=1919810&foo=one%20one%20four"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_credential_success() {
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
headers.append(
|
||||||
|
header::SET_COOKIE,
|
||||||
|
"SESSDATA=test_sessdata; Path=/; Domain=bilibili.com".parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers.append(
|
||||||
|
header::SET_COOKIE,
|
||||||
|
"bili_jct=test_jct; Path=/; Domain=bilibili.com".parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers.append(
|
||||||
|
header::SET_COOKIE,
|
||||||
|
"DedeUserID=123456; Path=/; Domain=bilibili.com".parse().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"data": {
|
||||||
|
"refresh_token": "test_refresh_token"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let credential = Credential::extract(headers, json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(credential.sessdata, "test_sessdata");
|
||||||
|
assert_eq!(credential.bili_jct, "test_jct");
|
||||||
|
assert_eq!(credential.dedeuserid, "123456");
|
||||||
|
assert_eq!(credential.ac_time_value, "test_refresh_token");
|
||||||
|
assert!(credential.buvid3.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_credential_missing_sessdata() {
|
||||||
|
let headers = header::HeaderMap::new();
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"data": {
|
||||||
|
"refresh_token": "test_refresh_token"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(Credential::extract(headers, json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_credential_missing_refresh_token() {
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
headers.append(header::SET_COOKIE, "SESSDATA=test_sessdata".parse().unwrap());
|
||||||
|
headers.append(header::SET_COOKIE, "bili_jct=test_jct".parse().unwrap());
|
||||||
|
headers.append(header::SET_COOKIE, "DedeUserID=123456".parse().unwrap());
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"data": {}
|
||||||
|
});
|
||||||
|
assert!(Credential::extract(headers, json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore = "requires manual testing with real QR code scan"]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_qrcode_login_flow() -> Result<()> {
|
||||||
|
let client = Client::new();
|
||||||
|
// 1. 生成二维码
|
||||||
|
let qr_response = Credential::generate_qrcode(&client).await?;
|
||||||
|
println!("二维码 URL: {}", qr_response.url);
|
||||||
|
println!("qrcode_key: {}", qr_response.qrcode_key);
|
||||||
|
println!("\n请使用 B 站 APP 扫描二维码...\n");
|
||||||
|
// 2. 轮询登录状态(最多轮询 90 次,每 2 秒一次,共 180 秒)
|
||||||
|
for i in 1..=90 {
|
||||||
|
println!("第 {} 次轮询...", i);
|
||||||
|
let status = Credential::poll_qrcode(&client, &qr_response.qrcode_key).await?;
|
||||||
|
match status {
|
||||||
|
PollStatus::Success { credential } => {
|
||||||
|
println!("\n登录成功!");
|
||||||
|
println!("SESSDATA: {}", credential.sessdata);
|
||||||
|
println!("bili_jct: {}", credential.bili_jct);
|
||||||
|
println!("buvid3: {}", credential.buvid3);
|
||||||
|
println!("DedeUserID: {}", credential.dedeuserid);
|
||||||
|
println!("ac_time_value: {}", credential.ac_time_value);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
PollStatus::Pending { message, scanned } => {
|
||||||
|
println!("状态: {}, 已扫描: {}", message, scanned);
|
||||||
|
}
|
||||||
|
PollStatus::Expired { message } => {
|
||||||
|
println!("\n二维码已过期: {}", message);
|
||||||
|
anyhow::bail!("二维码过期");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
bail!("轮询超时")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use thiserror::Error;
|
|||||||
pub enum BiliError {
|
pub enum BiliError {
|
||||||
#[error("response missing 'code' or 'message' field, full response: {0}")]
|
#[error("response missing 'code' or 'message' field, full response: {0}")]
|
||||||
InvalidResponse(String),
|
InvalidResponse(String),
|
||||||
#[error("API returned error code {0}, message: {1}, full response: {2}")]
|
#[error("API returned error code {0}, full response: {1}")]
|
||||||
ErrorResponse(i64, String, String),
|
ErrorResponse(i64, String),
|
||||||
#[error("risk control triggered by server, full response: {0}")]
|
#[error("risk control triggered by server, full response: {0}")]
|
||||||
RiskControlOccurred(String),
|
RiskControlOccurred(String),
|
||||||
#[error("no video streams available (may indicate risk control)")]
|
#[error("no video streams available (may indicate risk control)")]
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ use std::borrow::Cow;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use analyzer::{BestStream, FilterOption};
|
pub use analyzer::{BestStream, FilterOption};
|
||||||
use anyhow::{Result, bail, ensure};
|
use anyhow::{Context, Result, bail, ensure};
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
use chrono::serde::ts_seconds;
|
use chrono::serde::ts_seconds;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
pub use client::{BiliClient, Client};
|
pub use client::{BiliClient, Client};
|
||||||
pub use collection::{Collection, CollectionItem, CollectionType};
|
pub use collection::{Collection, CollectionItem, CollectionType};
|
||||||
pub use credential::Credential;
|
pub use credential::{Credential, PollStatus, Qrcode};
|
||||||
pub use danmaku::DanmakuOption;
|
pub use danmaku::DanmakuOption;
|
||||||
pub use dynamic::Dynamic;
|
pub use dynamic::Dynamic;
|
||||||
pub use error::BiliError;
|
pub use error::BiliError;
|
||||||
@@ -51,17 +51,13 @@ impl Validate for serde_json::Value {
|
|||||||
type Output = serde_json::Value;
|
type Output = serde_json::Value;
|
||||||
|
|
||||||
fn validate(self) -> Result<Self::Output> {
|
fn validate(self) -> Result<Self::Output> {
|
||||||
let (code, msg) = match (self["code"].as_i64(), self["message"].as_str()) {
|
let code = self["code"]
|
||||||
(Some(code), Some(msg)) => (code, msg),
|
.as_i64()
|
||||||
_ => bail!(BiliError::InvalidResponse(self.to_string())),
|
.with_context(|| BiliError::InvalidResponse(self.to_string()))?;
|
||||||
};
|
|
||||||
if code == -352 || !self["data"]["v_voucher"].is_null() {
|
if code == -352 || !self["data"]["v_voucher"].is_null() {
|
||||||
bail!(BiliError::RiskControlOccurred(self.to_string()));
|
bail!(BiliError::RiskControlOccurred(self.to_string()));
|
||||||
}
|
}
|
||||||
ensure!(
|
ensure!(code == 0, BiliError::ErrorResponse(code, self.to_string()));
|
||||||
code == 0,
|
|
||||||
BiliError::ErrorResponse(code, msg.to_owned(), self.to_string())
|
|
||||||
);
|
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ pub async fn fetch_video_details(
|
|||||||
"获取视频 {} - {} 的详细信息失败,错误为:{:#}",
|
"获取视频 {} - {} 的详细信息失败,错误为:{:#}",
|
||||||
&video_model.bvid, &video_model.name, e
|
&video_model.bvid, &video_model.name, e
|
||||||
);
|
);
|
||||||
if let Some(BiliError::ErrorResponse(-404, _, _)) = e.downcast_ref::<BiliError>() {
|
if let Some(BiliError::ErrorResponse(-404, _)) = e.downcast_ref::<BiliError>() {
|
||||||
let mut video_active_model: bili_sync_entity::video::ActiveModel = video_model.into();
|
let mut video_active_model: bili_sync_entity::video::ActiveModel = video_model.into();
|
||||||
video_active_model.valid = Set(false);
|
video_active_model.valid = Set(false);
|
||||||
video_active_model.save(connection).await?;
|
video_active_model.save(connection).await?;
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bili-sync-web",
|
"name": "bili-sync-web",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.1",
|
"@eslint/compat": "^1.4.1",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
@@ -262,6 +266,10 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="],
|
||||||
|
|
||||||
|
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="],
|
||||||
@@ -288,6 +296,8 @@
|
|||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
@@ -304,10 +314,14 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
|
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
@@ -374,6 +388,8 @@
|
|||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
@@ -386,6 +402,10 @@
|
|||||||
|
|
||||||
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
||||||
|
|
||||||
|
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||||
@@ -434,6 +454,8 @@
|
|||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||||
@@ -456,6 +478,8 @@
|
|||||||
|
|
||||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
@@ -546,6 +570,8 @@
|
|||||||
|
|
||||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
|
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||||
|
|
||||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
@@ -556,6 +582,8 @@
|
|||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
|
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
|
||||||
@@ -576,8 +604,14 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||||
@@ -594,6 +628,8 @@
|
|||||||
|
|
||||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
@@ -604,6 +640,10 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
@@ -646,6 +686,8 @@
|
|||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.51.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.51.0", "@typescript-eslint/parser": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA=="],
|
"typescript-eslint": ["typescript-eslint@8.51.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.51.0", "@typescript-eslint/parser": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
@@ -656,10 +698,20 @@
|
|||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
||||||
|
|
||||||
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||||
@@ -700,6 +752,8 @@
|
|||||||
|
|
||||||
"tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="],
|
"tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="],
|
||||||
|
|
||||||
|
"yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||||
@@ -707,5 +761,11 @@
|
|||||||
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||||
|
|
||||||
"mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
|
"mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
|
||||||
|
|
||||||
|
"yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
|
|
||||||
|
"yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
|
|
||||||
|
"yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5204
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -47,5 +47,9 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint ."
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -26,7 +26,9 @@ import type {
|
|||||||
Notifier,
|
Notifier,
|
||||||
UpdateFilteredVideoStatusRequest,
|
UpdateFilteredVideoStatusRequest,
|
||||||
UpdateFilteredVideoStatusResponse,
|
UpdateFilteredVideoStatusResponse,
|
||||||
ResetFilteredVideoStatusRequest
|
ResetFilteredVideoStatusRequest,
|
||||||
|
QrcodeGenerateResponse as GenerateQrcodeResponse,
|
||||||
|
QrcodePollResponse as PollQrcodeResponse
|
||||||
} from './types';
|
} from './types';
|
||||||
import { wsManager } from './ws';
|
import { wsManager } from './ws';
|
||||||
|
|
||||||
@@ -266,6 +268,14 @@ class ApiClient {
|
|||||||
return this.post<boolean>('/task/download');
|
return this.post<boolean>('/task/download');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateQrcode(): Promise<ApiResponse<GenerateQrcodeResponse>> {
|
||||||
|
return this.post<GenerateQrcodeResponse>('/login/qrcode/generate');
|
||||||
|
}
|
||||||
|
|
||||||
|
async pollQrcode(qrcodeKey: string): Promise<ApiResponse<PollQrcodeResponse>> {
|
||||||
|
return this.get<PollQrcodeResponse>('/login/qrcode/poll', { qrcode_key: qrcodeKey });
|
||||||
|
}
|
||||||
|
|
||||||
subscribeToLogs(onMessage: (data: string) => void) {
|
subscribeToLogs(onMessage: (data: string) => void) {
|
||||||
return wsManager.subscribeToLogs(onMessage);
|
return wsManager.subscribeToLogs(onMessage);
|
||||||
}
|
}
|
||||||
@@ -313,6 +323,8 @@ const api = {
|
|||||||
updateConfig: (config: Config) => apiClient.updateConfig(config),
|
updateConfig: (config: Config) => apiClient.updateConfig(config),
|
||||||
getDashboard: () => apiClient.getDashboard(),
|
getDashboard: () => apiClient.getDashboard(),
|
||||||
triggerDownloadTask: () => apiClient.triggerDownloadTask(),
|
triggerDownloadTask: () => apiClient.triggerDownloadTask(),
|
||||||
|
generateQrcode: () => apiClient.generateQrcode(),
|
||||||
|
pollQrcode: (qrcodeKey: string) => apiClient.pollQrcode(qrcodeKey),
|
||||||
subscribeToSysInfo: (onMessage: (data: SysInfo) => void) =>
|
subscribeToSysInfo: (onMessage: (data: SysInfo) => void) =>
|
||||||
apiClient.subscribeToSysInfo(onMessage),
|
apiClient.subscribeToSysInfo(onMessage),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import api from '$lib/api';
|
||||||
|
import type { Credential, ApiError } from '$lib/types';
|
||||||
|
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||||
|
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录组件
|
||||||
|
*
|
||||||
|
* 状态流转:
|
||||||
|
* loading -> showing -> (success | expired | error)
|
||||||
|
* success 会调用 onSuccess 回调,由父组件关闭弹窗,不需要内部做处理
|
||||||
|
*
|
||||||
|
* @prop onSuccess - 登录成功回调,接收完整的凭证对象
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 常量配置
|
||||||
|
const QR_EXPIRE_TIME = 180; // 二维码有效期(秒)
|
||||||
|
const POLL_INTERVAL = 2000; // 轮询间隔(毫秒)
|
||||||
|
const COUNTDOWN_INTERVAL = 1000; // 倒计时更新间隔(毫秒)
|
||||||
|
const QR_SIZE = 256; // 二维码图片尺寸(像素)
|
||||||
|
const QR_MARGIN = 2; // 二维码边距
|
||||||
|
|
||||||
|
export let onSuccess: (credential: Credential) => void;
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
generateQrcode();
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status = 'loading' | 'showing' | 'expired' | 'error';
|
||||||
|
|
||||||
|
let status: Status = 'loading';
|
||||||
|
let qrcodeUrl = ''; // B站返回的二维码 URL(需要转换为图片)
|
||||||
|
let qrcodeKey = ''; // 用于轮询的认证 token
|
||||||
|
let qrcodeDataUrl = ''; // 生成的二维码图片 Data URL
|
||||||
|
let countdown = QR_EXPIRE_TIME; // 倒计时
|
||||||
|
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let countdownInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let scanned = false; // 是否已扫描
|
||||||
|
let errorMessage = '';
|
||||||
|
let isPolling = false; // 轮询标志,确保轮询排他性
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成二维码
|
||||||
|
*
|
||||||
|
* 1. 停止之前的轮询和倒计时(确保排他性)
|
||||||
|
* 2. 调用后端 API 获取二维码信息
|
||||||
|
* 3. 将 URL 转换为二维码图片
|
||||||
|
* 4. 开始轮询登录状态
|
||||||
|
*/
|
||||||
|
async function generateQrcode() {
|
||||||
|
// 先停止之前的轮询和倒计时(排他性)
|
||||||
|
stopPolling();
|
||||||
|
stopCountdown();
|
||||||
|
status = 'loading';
|
||||||
|
errorMessage = '';
|
||||||
|
scanned = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.generateQrcode();
|
||||||
|
qrcodeUrl = response.data.url;
|
||||||
|
qrcodeKey = response.data.qrcode_key;
|
||||||
|
countdown = QR_EXPIRE_TIME;
|
||||||
|
|
||||||
|
// 将 URL 转换为二维码图片
|
||||||
|
qrcodeDataUrl = await QRCode.toDataURL(qrcodeUrl, {
|
||||||
|
width: QR_SIZE,
|
||||||
|
margin: QR_MARGIN,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#FFFFFF'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
status = 'showing';
|
||||||
|
|
||||||
|
// 开始轮询和倒计时
|
||||||
|
startPolling();
|
||||||
|
startCountdown();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成二维码失败:', error);
|
||||||
|
status = 'error';
|
||||||
|
errorMessage = (error as ApiError).message || '生成二维码失败';
|
||||||
|
toast.error('生成二维码失败', {
|
||||||
|
description: (error as ApiError).message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询登录状态
|
||||||
|
*
|
||||||
|
* 每次调用前检查 isPolling 标志,确保轮询排他性。
|
||||||
|
* 异步请求后再次检查,防止在请求过程中状态已改变。
|
||||||
|
*/
|
||||||
|
async function pollStatus() {
|
||||||
|
// 如果已经停止轮询,直接返回
|
||||||
|
if (!qrcodeKey || !isPolling) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.pollQrcode(qrcodeKey);
|
||||||
|
const pollResult = response.data;
|
||||||
|
|
||||||
|
// 再次检查是否还在轮询(防止在请求过程中状态改变)
|
||||||
|
if (!isPolling) return;
|
||||||
|
|
||||||
|
if (pollResult.status === 'success') {
|
||||||
|
stopPolling();
|
||||||
|
stopCountdown();
|
||||||
|
onSuccess(pollResult.credential);
|
||||||
|
} else if (pollResult.status === 'pending') {
|
||||||
|
scanned = pollResult.scanned || false;
|
||||||
|
} else if (pollResult.status === 'expired') {
|
||||||
|
stopPolling();
|
||||||
|
stopCountdown();
|
||||||
|
status = 'expired';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('轮询登录状态失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动轮询
|
||||||
|
*
|
||||||
|
* 设置轮询标志并启动定时器
|
||||||
|
*/
|
||||||
|
function startPolling() {
|
||||||
|
isPolling = true;
|
||||||
|
pollInterval = setInterval(pollStatus, POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止轮询
|
||||||
|
*
|
||||||
|
* 立即设置轮询标志为 false,清除定时器
|
||||||
|
*/
|
||||||
|
function stopPolling() {
|
||||||
|
isPolling = false; // 立即设置标志为 false
|
||||||
|
if (pollInterval) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
pollInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动倒计时
|
||||||
|
*
|
||||||
|
* 每秒减少倒计时,到期后自动停止轮询并标记为过期
|
||||||
|
*/
|
||||||
|
function startCountdown() {
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
countdown--;
|
||||||
|
if (countdown <= 0) {
|
||||||
|
stopPolling();
|
||||||
|
stopCountdown();
|
||||||
|
status = 'expired';
|
||||||
|
}
|
||||||
|
}, COUNTDOWN_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止倒计时
|
||||||
|
*
|
||||||
|
* 清除倒计时定时器
|
||||||
|
*/
|
||||||
|
function stopCountdown() {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
stopPolling();
|
||||||
|
stopCountdown();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="qr-login-container">
|
||||||
|
<Card.Root class="border-0 shadow-none">
|
||||||
|
<Card.Content class="p-4">
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<!-- 二维码容器 - 始终显示边框 -->
|
||||||
|
<div class="border-border relative rounded-lg border-2 bg-white p-3">
|
||||||
|
{#if status === 'loading'}
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div class="flex h-48 w-48 items-center justify-center">
|
||||||
|
<LoaderCircle class="text-muted-foreground h-8 w-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
{:else if status === 'showing'}
|
||||||
|
<!-- 显示二维码 -->
|
||||||
|
<img src={qrcodeDataUrl} alt="登录二维码" class="h-48 w-48" />
|
||||||
|
{:else}
|
||||||
|
<!-- 过期或错误状态 - 显示占位图标 -->
|
||||||
|
<div class="flex h-48 w-48 items-center justify-center">
|
||||||
|
<RefreshCw class="text-muted-foreground h-12 w-12" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态提示文本 -->
|
||||||
|
<div class="text-muted-foreground space-y-2 text-center text-sm">
|
||||||
|
{#if status === 'loading'}
|
||||||
|
<p>正在生成二维码...</p>
|
||||||
|
{:else if status === 'showing'}
|
||||||
|
{#if scanned}
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<LoaderCircle class="h-4 w-4 animate-spin" />
|
||||||
|
<p>已扫描,请在手机上确认登录</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>请使用哔哩哔哩 APP 扫描二维码</p>
|
||||||
|
{/if}
|
||||||
|
{:else if status === 'expired'}
|
||||||
|
<p>二维码已过期</p>
|
||||||
|
{:else if status === 'error'}
|
||||||
|
<p class="text-destructive">{errorMessage}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- 倒计时 - 始终显示 -->
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<span class="text-muted-foreground text-xs">有效时间:</span>
|
||||||
|
<span
|
||||||
|
class="font-mono text-sm font-bold"
|
||||||
|
class:text-primary={countdown > 0}
|
||||||
|
class:text-muted-foreground={countdown <= 0}
|
||||||
|
>
|
||||||
|
{#if status === 'showing'}
|
||||||
|
{Math.floor(countdown / 60)}:{String(countdown % 60).padStart(2, '0')}
|
||||||
|
{:else}
|
||||||
|
-:--
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 - 根据状态变化 -->
|
||||||
|
{#if status === 'loading'}
|
||||||
|
<Button variant="outline" size="sm" class="w-full" disabled>
|
||||||
|
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
加载中...
|
||||||
|
</Button>
|
||||||
|
{:else if status === 'showing'}
|
||||||
|
<Button variant="outline" size="sm" onclick={generateQrcode} class="w-full">
|
||||||
|
<RefreshCw class="mr-2 h-4 w-4" />
|
||||||
|
刷新二维码
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button variant="outline" size="sm" onclick={generateQrcode} class="w-full">
|
||||||
|
<RefreshCw class="mr-2 h-4 w-4" />
|
||||||
|
重新获取二维码
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qr-login-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -349,3 +349,24 @@ export interface TaskStatus {
|
|||||||
export interface UpdateVideoSourceResponse {
|
export interface UpdateVideoSourceResponse {
|
||||||
ruleDisplay: string;
|
ruleDisplay: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扫码登录相关类型
|
||||||
|
export interface QrcodeGenerateResponse {
|
||||||
|
url: string;
|
||||||
|
qrcode_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QrcodePollResponse =
|
||||||
|
| {
|
||||||
|
status: 'success';
|
||||||
|
credential: Credential;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'pending';
|
||||||
|
message: string;
|
||||||
|
scanned?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'expired';
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
@@ -10,12 +10,14 @@
|
|||||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
import PasswordInput from '$lib/components/custom/password-input.svelte';
|
import PasswordInput from '$lib/components/custom/password-input.svelte';
|
||||||
|
import QrLogin from '$lib/components/custom/qr-login.svelte';
|
||||||
import NotifierDialog from './NotifierDialog.svelte';
|
import NotifierDialog from './NotifierDialog.svelte';
|
||||||
import InfoIcon from '@lucide/svelte/icons/info';
|
import InfoIcon from '@lucide/svelte/icons/info';
|
||||||
|
import QrCodeIcon from '@lucide/svelte/icons/qr-code';
|
||||||
import api from '$lib/api';
|
import api from '$lib/api';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||||
import type { Config, ApiError, Notifier } from '$lib/types';
|
import type { Config, ApiError, Notifier, Credential } from '$lib/types';
|
||||||
|
|
||||||
let frontendToken = ''; // 前端认证token
|
let frontendToken = ''; // 前端认证token
|
||||||
let config: Config | null = null;
|
let config: Config | null = null;
|
||||||
@@ -31,6 +33,10 @@
|
|||||||
let editingNotifierIndex: number | null = null;
|
let editingNotifierIndex: number | null = null;
|
||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
|
|
||||||
|
// QR 登录 Dialog 相关
|
||||||
|
let showQrLoginDialog = false;
|
||||||
|
let qrLoginComponent: QrLogin;
|
||||||
|
|
||||||
function openAddNotifierDialog() {
|
function openAddNotifierDialog() {
|
||||||
editingNotifier = null;
|
editingNotifier = null;
|
||||||
editingNotifierIndex = null;
|
editingNotifierIndex = null;
|
||||||
@@ -168,6 +174,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleQrLoginSuccess(credential: Credential) {
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
// 自动填充凭证到 formData
|
||||||
|
formData.credential = credential;
|
||||||
|
|
||||||
|
toast.success('扫码登录成功,已填充凭据');
|
||||||
|
|
||||||
|
// 自动保存配置
|
||||||
|
saveConfig();
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
showQrLoginDialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setBreadcrumb([{ label: '设置' }]);
|
setBreadcrumb([{ label: '设置' }]);
|
||||||
|
|
||||||
@@ -349,6 +370,27 @@
|
|||||||
|
|
||||||
<!-- B站认证 -->
|
<!-- B站认证 -->
|
||||||
<Tabs.Content value="auth" class="mt-6 space-y-6">
|
<Tabs.Content value="auth" class="mt-6 space-y-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label class="text-base font-semibold">快速登录</Label>
|
||||||
|
<p class="text-muted-foreground text-sm">使用哔哩哔哩 APP 扫码登录,自动填充凭据</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onclick={() => {
|
||||||
|
showQrLoginDialog = true;
|
||||||
|
tick().then(() => {
|
||||||
|
qrLoginComponent!.init();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<QrCodeIcon class="mr-2 h-4 w-4" />
|
||||||
|
扫码登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- 原有的手动输入 Cookie 表单 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="sessdata">SESSDATA</Label>
|
<Label for="sessdata">SESSDATA</Label>
|
||||||
@@ -964,3 +1006,20 @@
|
|||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<!-- QR 登录弹窗 -->
|
||||||
|
<Dialog.Root bind:open={showQrLoginDialog}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="bg-background/80 fixed inset-0 z-50 backdrop-blur-sm" />
|
||||||
|
<Dialog.Content
|
||||||
|
class="bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-md translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg"
|
||||||
|
>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>扫码登录</Dialog.Title>
|
||||||
|
<Dialog.Description>使用哔哩哔哩 APP 扫描二维码登录</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<QrLogin bind:this={qrLoginComponent} onSuccess={handleQrLoginSuccess} />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
|||||||
Reference in New Issue
Block a user