optimize: WeChat QR code auth flow and channel reliability

- Generate QR code server-side when iLink API returns SPA page URL
  (added qrcode + image crates for PNG generation)
- Strip bot_token from frontend response (new WeixinQrcodeStatusPublic type)
- Add request timeouts and shared HTTP client for QR code endpoints
- Fix TOCTOU race on reply_context double-lock (single lock scope)
- Extract do_send() helper to deduplicate sendmessage logic; resend now
  checks ret field for context expiry instead of HTTP status only
- Cap pending_messages buffer at 50 to prevent unbounded memory growth
- Generate stable X-WECHAT-UIN per backend instance instead of per request
- Extract ILINK_CHANNEL_VERSION constant (was hardcoded in 4 places)
- Add 5-minute client-side QR expiry timeout in frontend dialog
- Track consecutive polling errors and show warning after 3 failures
- Stabilise onAuthSuccess/onClose callback refs to prevent polling restarts
- Replace dead i18n key weixinOpenQrcode with weixinPollError

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-02 11:22:51 +08:00
parent 0ef36ee918
commit d0e0aad525
17 changed files with 379 additions and 207 deletions

67
src-tauri/Cargo.lock generated
View File

@@ -615,6 +615,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -860,12 +866,14 @@ dependencies = [
"flate2",
"futures",
"futures-util",
"image",
"keyring",
"kill_tree",
"mac-notification-sys",
"notify",
"portable-pty",
"prost",
"qrcode",
"rand 0.8.5",
"regex",
"reqwest 0.12.28",
@@ -2465,7 +2473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [
"byteorder",
"png",
"png 0.17.16",
]
[[package]]
@@ -2576,6 +2584,19 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -3196,6 +3217,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "muda"
version = "0.17.1"
@@ -3211,7 +3242,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-foundation",
"once_cell",
"png",
"png 0.17.16",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",
@@ -4119,6 +4150,19 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags 2.10.0",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "polling"
version = "3.11.0"
@@ -4340,6 +4384,21 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "pxfm"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]]
name = "qrcode"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
dependencies = [
"image",
]
[[package]]
name = "quick-xml"
version = "0.37.5"
@@ -6263,7 +6322,7 @@ dependencies = [
"ico",
"json-patch",
"plist",
"png",
"png 0.17.16",
"proc-macro2",
"quote",
"semver",
@@ -7014,7 +7073,7 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
"png",
"png 0.17.16",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",

View File

@@ -82,6 +82,8 @@ tokio-tungstenite = { version = "0.26", features = ["native-tls"] }
futures-util = "0.3"
prost = "0.13"
rand = "0.8"
qrcode = "0.14"
image = { version = "0.25", default-features = false, features = ["png"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-window-state = { version = "2", optional = true }

View File

@@ -13,6 +13,24 @@ use crate::chat_channel::traits::ChatChannelBackend;
use crate::chat_channel::types::*;
const ILINK_BASE_URL: &str = "https://ilinkai.weixin.qq.com";
const ILINK_CHANNEL_VERSION: &str = "1.0.2";
/// Maximum number of messages buffered while context_token is expired.
const MAX_PENDING_MESSAGES: usize = 50;
/// Shared HTTP client for QR code auth requests (avoids re-creating TLS state).
fn qr_client() -> reqwest::Client {
use std::sync::OnceLock;
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT
.get_or_init(|| {
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(15))
.build()
.unwrap_or_default()
})
.clone()
}
// ── QR code auth types (public, used by commands) ──
@@ -25,16 +43,24 @@ pub struct WeixinQrcodeInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeixinQrcodeStatus {
pub status: String,
/// bot_token and base_url are consumed by the _core command layer and
/// stripped before the response reaches the frontend.
#[serde(skip_serializing_if = "Option::is_none")]
pub bot_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
}
/// Frontend-safe subset of [`WeixinQrcodeStatus`] — no credentials.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeixinQrcodeStatusPublic {
pub status: String,
}
// ── QR code auth functions (called before backend exists) ──
pub async fn weixin_get_qrcode() -> Result<WeixinQrcodeInfo, ChatChannelError> {
let client = reqwest::Client::new();
let client = qr_client();
let resp = client
.get(format!("{ILINK_BASE_URL}/ilink/bot/get_bot_qrcode"))
.query(&[("bot_type", "3")])
@@ -64,14 +90,15 @@ pub async fn weixin_get_qrcode() -> Result<WeixinQrcodeInfo, ChatChannelError> {
));
}
// If the image content is a URL, fetch the actual image bytes and
// convert to a data-URI so the frontend can display it without CORS issues.
// If the image content is a URL, try to fetch the actual image bytes.
// If the URL points to an HTML SPA (which renders the QR code via JS),
// generate the QR code ourselves — the SPA simply encodes the page URL.
let qrcode_img_content = if raw_img.starts_with("http://") || raw_img.starts_with("https://") {
match fetch_image_as_data_uri(&client, &raw_img).await {
Ok(data_uri) => data_uri,
Err(e) => {
eprintln!("[Weixin] failed to proxy QR image: {e}, falling back to URL");
raw_img
Err(_) => {
eprintln!("[Weixin] URL is an SPA page, generating QR code from URL");
generate_qrcode_data_uri(&raw_img)?
}
}
} else {
@@ -85,6 +112,9 @@ pub async fn weixin_get_qrcode() -> Result<WeixinQrcodeInfo, ChatChannelError> {
}
/// Fetch an image from a URL and return it as a `data:<mime>;base64,...` string.
///
/// Returns an error if the URL points to an HTML page (SPA) rather than a
/// raw image — the caller will generate a QR code from the URL instead.
async fn fetch_image_as_data_uri(
client: &reqwest::Client,
url: &str,
@@ -107,11 +137,10 @@ async fn fetch_image_as_data_uri(
.unwrap_or("image/png")
.to_string();
// If the server returned HTML instead of an image, bail out
if content_type.contains("text/html") || content_type.contains("text/plain") {
return Err(ChatChannelError::ConnectionFailed(format!(
"Expected image but got {content_type}"
)));
return Err(ChatChannelError::ConnectionFailed(
"QR code URL is an SPA page".into(),
));
}
let bytes = resp
@@ -119,16 +148,48 @@ async fn fetch_image_as_data_uri(
.await
.map_err(|e| ChatChannelError::ConnectionFailed(format!("Image read failed: {e}")))?;
if bytes.is_empty() {
return Err(ChatChannelError::ConnectionFailed(
"Empty image response".into(),
));
}
let b64 = B64.encode(&bytes);
// Normalize content-type: strip parameters like charset
let mime = content_type.split(';').next().unwrap_or("image/png").trim();
Ok(format!("data:{mime};base64,{b64}"))
}
/// Generate a QR code image encoding the given text and return as a PNG data URI.
///
/// The iLink QR page is a SPA that renders `window.location.href` as a QR code.
/// We replicate that logic server-side so the frontend can display it directly.
fn generate_qrcode_data_uri(content: &str) -> Result<String, ChatChannelError> {
use image::{codecs::png::PngEncoder, ImageEncoder, Luma};
use qrcode::QrCode;
let code = QrCode::new(content.as_bytes()).map_err(|e| {
ChatChannelError::ConnectionFailed(format!("QR code generation failed: {e}"))
})?;
let img = code
.render::<Luma<u8>>()
.quiet_zone(true)
.min_dimensions(250, 250)
.build();
let (w, h) = (img.width(), img.height());
let mut png_buf: Vec<u8> = Vec::new();
PngEncoder::new(&mut png_buf)
.write_image(img.as_raw(), w, h, image::ExtendedColorType::L8)
.map_err(|e| ChatChannelError::ConnectionFailed(format!("PNG encoding failed: {e}")))?;
let b64 = B64.encode(&png_buf);
Ok(format!("data:image/png;base64,{b64}"))
}
pub async fn weixin_check_qrcode(
qrcode: &str,
) -> Result<WeixinQrcodeStatus, ChatChannelError> {
let client = reqwest::Client::new();
let client = qr_client();
let resp = client
.get(format!("{ILINK_BASE_URL}/ilink/bot/get_qrcode_status"))
.query(&[("qrcode", qrcode)])
@@ -181,10 +242,15 @@ pub struct WeixinBackend {
reply_context: Arc<Mutex<Option<WeixinReplyContext>>>,
/// Messages that failed due to expired context_token, resend on next refresh.
pending_messages: Arc<Mutex<Vec<String>>>,
/// Stable X-WECHAT-UIN value for this backend instance.
wechat_uin: String,
}
impl WeixinBackend {
pub fn new(channel_id: i32, bot_token: String, base_url: String) -> Self {
let uin_raw = rand::thread_rng().gen::<u32>().to_string();
let wechat_uin = B64.encode(uin_raw.as_bytes());
Self {
bot_token,
base_url,
@@ -198,10 +264,11 @@ impl WeixinBackend {
shutdown_tx: Arc::new(Mutex::new(None)),
reply_context: Arc::new(Mutex::new(None)),
pending_messages: Arc::new(Mutex::new(Vec::new())),
wechat_uin,
}
}
fn build_headers(bot_token: &str) -> HeaderMap {
fn build_headers(bot_token: &str, wechat_uin: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
headers.insert(
@@ -209,9 +276,7 @@ impl WeixinBackend {
HeaderValue::from_static("ilink_bot_token"),
);
let uin_raw = rand::thread_rng().gen::<u32>().to_string();
let uin_b64 = B64.encode(uin_raw.as_bytes());
if let Ok(val) = HeaderValue::from_str(&uin_b64) {
if let Ok(val) = HeaderValue::from_str(wechat_uin) {
headers.insert("X-WECHAT-UIN", val);
}
@@ -223,6 +288,95 @@ impl WeixinBackend {
headers
}
/// Build the JSON body for the iLink sendmessage API.
fn build_send_body(
to_user_id: &str,
context_token: &str,
text: &str,
) -> serde_json::Value {
serde_json::json!({
"msg": {
"from_user_id": "",
"to_user_id": to_user_id,
"client_id": format!("codeg-{}", uuid::Uuid::new_v4()),
"message_type": 2,
"message_state": 2,
"context_token": context_token,
"item_list": [{
"type": 1,
"text_item": { "text": text }
}]
},
"base_info": { "channel_version": ILINK_CHANNEL_VERSION }
})
}
/// Send a message via the iLink API and handle the response.
/// Returns `Ok(true)` if sent, `Ok(false)` if buffered due to expired context.
async fn do_send(
client: &reqwest::Client,
base_url: &str,
bot_token: &str,
wechat_uin: &str,
to_user_id: &str,
context_token: &str,
text: &str,
reply_context: &Mutex<Option<WeixinReplyContext>>,
pending_messages: &Mutex<Vec<String>>,
) -> Result<bool, ChatChannelError> {
let body = Self::build_send_body(to_user_id, context_token, text);
let url = format!("{base_url}/ilink/bot/sendmessage");
let resp = client
.post(&url)
.headers(Self::build_headers(bot_token, wechat_uin))
.json(&body)
.send()
.await
.map_err(|e| ChatChannelError::SendFailed(e.to_string()))?;
let status_code = resp.status();
let resp_text = resp.text().await.unwrap_or_default();
if !status_code.is_success() {
return Err(ChatChannelError::SendFailed(format!(
"HTTP {status_code}: {resp_text}"
)));
}
// Check for ret errors in response (e.g. -2 = context expired)
if let Ok(resp_json) = serde_json::from_str::<serde_json::Value>(&resp_text) {
if let Some(ret) = resp_json.get("ret").and_then(|v| v.as_i64()) {
if ret != 0 {
let errmsg = resp_json
.get("errmsg")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
eprintln!("[Weixin] sendmessage ret={ret}, errmsg={errmsg}");
if ret == -2 {
// Context token expired — mark stale and buffer
if let Some(ref mut c) = *reply_context.lock().await {
c.expired = true;
}
let mut buf = pending_messages.lock().await;
if buf.len() < MAX_PENDING_MESSAGES {
buf.push(text.to_string());
}
eprintln!("[Weixin] context_token expired (ret=-2), buffered message");
return Ok(false);
}
return Err(ChatChannelError::SendFailed(format!(
"ret={ret}: {errmsg}"
)));
}
}
}
Ok(true)
}
async fn send_text(
&self,
text: &str,
@@ -249,79 +403,33 @@ impl WeixinBackend {
"[Weixin] context expired, buffering message (len={})",
text.len()
);
self.pending_messages.lock().await.push(text.to_string());
let mut buf = self.pending_messages.lock().await;
if buf.len() < MAX_PENDING_MESSAGES {
buf.push(text.to_string());
} else {
eprintln!("[Weixin] pending buffer full, dropping message");
}
return Ok(SentMessageId(String::new()));
}
let client_id = format!("codeg-{}", uuid::Uuid::new_v4());
let body = serde_json::json!({
"msg": {
"from_user_id": "",
"to_user_id": to_user_id,
"client_id": client_id,
"message_type": 2,
"message_state": 2,
"context_token": context_token,
"item_list": [{
"type": 1,
"text_item": { "text": text }
}]
},
"base_info": { "channel_version": "1.0.2" }
});
let url = format!("{}/ilink/bot/sendmessage", self.base_url);
eprintln!(
"[Weixin] sendmessage to={to_user_id}, context_token_len={}, text_len={}",
context_token.len(),
text.len()
);
let resp = self
.client
.post(&url)
.headers(Self::build_headers(&self.bot_token))
.json(&body)
.send()
.await
.map_err(|e| ChatChannelError::SendFailed(e.to_string()))?;
let status_code = resp.status();
let resp_text = resp.text().await.unwrap_or_default();
eprintln!("[Weixin] sendmessage response: status={status_code}, body={resp_text}");
if !status_code.is_success() {
return Err(ChatChannelError::SendFailed(format!(
"HTTP {status_code}: {resp_text}"
)));
}
// Check for ret errors in response (e.g. -2 = context expired)
if let Ok(resp_json) = serde_json::from_str::<serde_json::Value>(&resp_text) {
if let Some(ret) = resp_json.get("ret").and_then(|v| v.as_i64()) {
if ret != 0 {
let errmsg = resp_json
.get("errmsg")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
eprintln!("[Weixin] sendmessage ret={ret}, errmsg={errmsg}");
if ret == -2 {
// Context token expired — mark stale and buffer
if let Some(ref mut c) = *self.reply_context.lock().await {
c.expired = true;
}
self.pending_messages.lock().await.push(text.to_string());
eprintln!("[Weixin] context_token expired (ret=-2), buffered message");
return Ok(SentMessageId(String::new()));
}
return Err(ChatChannelError::SendFailed(format!(
"ret={ret}: {errmsg}"
)));
}
}
}
Self::do_send(
&self.client,
&self.base_url,
&self.bot_token,
&self.wechat_uin,
&to_user_id,
&context_token,
text,
&self.reply_context,
&self.pending_messages,
)
.await?;
Ok(SentMessageId(String::new()))
}
@@ -348,7 +456,7 @@ impl ChatChannelBackend for WeixinBackend {
// Verify auth by doing a quick getupdates with empty cursor
let verify_body = serde_json::json!({
"get_updates_buf": "",
"base_info": { "channel_version": "1.0.2" }
"base_info": { "channel_version": ILINK_CHANNEL_VERSION }
});
let url = format!("{}/ilink/bot/getupdates", self.base_url);
eprintln!("[Weixin] verify POST {url}");
@@ -356,7 +464,7 @@ impl ChatChannelBackend for WeixinBackend {
let resp = self
.client
.post(&url)
.headers(Self::build_headers(&self.bot_token))
.headers(Self::build_headers(&self.bot_token, &self.wechat_uin))
.json(&verify_body)
.send()
.await
@@ -380,6 +488,13 @@ impl ChatChannelBackend for WeixinBackend {
.and_then(|v| v.as_i64())
.unwrap_or(-1);
// Check for known auth-failure codes
if ret == -14 {
return Err(ChatChannelError::AuthenticationFailed(
"Session expired (ret=-14), please re-authenticate".into(),
));
}
// The iLink API may omit the `ret` field or return non-zero on the first
// call. Always extract the cursor if present — it's needed for polling.
let initial_cursor = verify_result
@@ -404,6 +519,7 @@ impl ChatChannelBackend for WeixinBackend {
let client = self.client.clone();
let bot_token = self.bot_token.clone();
let base_url = self.base_url.clone();
let wechat_uin = self.wechat_uin.clone();
let channel_id = self.channel_id;
let status = self.status.clone();
let reply_context = self.reply_context.clone();
@@ -420,13 +536,13 @@ impl ChatChannelBackend for WeixinBackend {
let body = serde_json::json!({
"get_updates_buf": cursor,
"base_info": { "channel_version": "1.0.2" }
"base_info": { "channel_version": ILINK_CHANNEL_VERSION }
});
let result = tokio::select! {
r = client
.post(format!("{base_url}/ilink/bot/getupdates"))
.headers(WeixinBackend::build_headers(&bot_token))
.headers(WeixinBackend::build_headers(&bot_token, &wechat_uin))
.json(&body)
.send() => r,
_ = shutdown_rx.changed() => break,
@@ -521,19 +637,21 @@ impl ChatChannelBackend for WeixinBackend {
.unwrap_or_default();
// Store reply context for outbound messages
// Single lock scope to avoid TOCTOU
if !from_user_id.is_empty() && !context_token.is_empty() {
let was_expired = reply_context
.lock()
.await
.as_ref()
.map(|c| c.expired)
.unwrap_or(false);
*reply_context.lock().await = Some(WeixinReplyContext {
to_user_id: from_user_id.to_string(),
context_token: context_token.to_string(),
expired: false,
});
let was_expired = {
let mut guard = reply_context.lock().await;
let was = guard
.as_ref()
.map(|c| c.expired)
.unwrap_or(false);
*guard = Some(WeixinReplyContext {
to_user_id: from_user_id.to_string(),
context_token: context_token.to_string(),
expired: false,
});
was
};
// Resend buffered messages with fresh context
if was_expired {
@@ -545,52 +663,29 @@ impl ChatChannelBackend for WeixinBackend {
buffered.len()
);
for pending_text in &buffered {
let cid =
format!("codeg-{}", uuid::Uuid::new_v4());
let send_body = serde_json::json!({
"msg": {
"from_user_id": "",
"to_user_id": from_user_id,
"client_id": cid,
"message_type": 2,
"message_state": 2,
"context_token": context_token,
"item_list": [{
"type": 1,
"text_item": { "text": pending_text }
}]
},
"base_info": { "channel_version": "1.0.2" }
});
let send_ok = match client
.post(format!(
"{base_url}/ilink/bot/sendmessage"
))
.headers(WeixinBackend::build_headers(
&bot_token,
))
.json(&send_body)
.send()
.await
{
Ok(r) => {
let ok = r.status().is_success();
if !ok {
eprintln!("[Weixin] resend failed: HTTP {}", r.status());
}
ok
let ok = WeixinBackend::do_send(
&client,
&base_url,
&bot_token,
&wechat_uin,
from_user_id,
context_token,
pending_text,
&reply_context,
&pending_messages,
)
.await;
if let Err(e) = ok {
eprintln!("[Weixin] resend error: {e}");
// Re-buffer remaining on hard error
let mut buf =
pending_messages.lock().await;
if buf.len() < MAX_PENDING_MESSAGES {
buf.push(pending_text.clone());
}
Err(e) => {
eprintln!("[Weixin] resend error: {e}");
false
}
};
if !send_ok {
// Re-buffer remaining messages
pending_messages.lock().await.push(
pending_text.clone(),
);
}
// If do_send returned Ok(false), it
// already re-buffered internally.
}
}
}
@@ -662,14 +757,14 @@ impl ChatChannelBackend for WeixinBackend {
async fn test_connection(&self) -> Result<(), ChatChannelError> {
let body = serde_json::json!({
"get_updates_buf": "",
"base_info": { "channel_version": "1.0.2" }
"base_info": { "channel_version": ILINK_CHANNEL_VERSION }
});
let url = format!("{}/ilink/bot/getupdates", self.base_url);
let resp = self
.client
.post(&url)
.headers(Self::build_headers(&self.bot_token))
.headers(Self::build_headers(&self.bot_token, &self.wechat_uin))
.json(&body)
.send()
.await
@@ -683,9 +778,7 @@ impl ChatChannelBackend for WeixinBackend {
eprintln!("[Weixin] test_connection: status={status_code}, body={resp_text}");
// As long as we got a valid JSON response from the server, treat it as reachable.
// The iLink API may return ret != 0 on first empty-cursor call.
let _: serde_json::Value = serde_json::from_str(&resp_text).map_err(|e| {
let resp_json: serde_json::Value = serde_json::from_str(&resp_text).map_err(|e| {
ChatChannelError::ConnectionFailed(format!("Not valid JSON: {e}"))
})?;
@@ -695,6 +788,15 @@ impl ChatChannelBackend for WeixinBackend {
)));
}
// Check for known auth-failure codes
if let Some(ret) = resp_json.get("ret").and_then(|v| v.as_i64()) {
if ret == -14 {
return Err(ChatChannelError::AuthenticationFailed(
"Session expired (ret=-14)".into(),
));
}
}
Ok(())
}
}

View File

@@ -1,5 +1,5 @@
use crate::app_error::AppCommandError;
use crate::chat_channel::backends::weixin::{WeixinQrcodeInfo, WeixinQrcodeStatus};
use crate::chat_channel::backends::weixin::{WeixinQrcodeInfo, WeixinQrcodeStatusPublic};
use crate::chat_channel::manager::ChatChannelManager;
use crate::chat_channel::types::ChannelType;
use crate::db::service::{chat_channel_message_log_service, chat_channel_service};
@@ -351,7 +351,7 @@ pub async fn weixin_check_qrcode_core(
db: &AppDatabase,
channel_id: i32,
qrcode: &str,
) -> Result<WeixinQrcodeStatus, AppCommandError> {
) -> Result<WeixinQrcodeStatusPublic, AppCommandError> {
let result = crate::chat_channel::backends::weixin::weixin_check_qrcode(qrcode)
.await
.map_err(AppCommandError::from)?;
@@ -386,7 +386,10 @@ pub async fn weixin_check_qrcode_core(
}
}
Ok(result)
// Return only the status — never expose bot_token to the frontend
Ok(WeixinQrcodeStatusPublic {
status: result.status,
})
}
// ---------------------------------------------------------------------------
@@ -569,6 +572,6 @@ pub async fn weixin_check_qrcode(
db: tauri::State<'_, AppDatabase>,
channel_id: i32,
qrcode: String,
) -> Result<WeixinQrcodeStatus, AppCommandError> {
) -> Result<WeixinQrcodeStatusPublic, AppCommandError> {
weixin_check_qrcode_core(&db, channel_id, &qrcode).await
}

View File

@@ -6,7 +6,7 @@ use serde::Deserialize;
use crate::app_error::AppCommandError;
use crate::app_state::AppState;
use crate::commands::chat_channel as cc_commands;
use crate::chat_channel::backends::weixin::{WeixinQrcodeInfo, WeixinQrcodeStatus};
use crate::chat_channel::backends::weixin::{WeixinQrcodeInfo, WeixinQrcodeStatusPublic};
use crate::models::chat_channel::{ChannelStatusInfo, ChatChannelInfo, ChatChannelMessageLogInfo};
// ---------------------------------------------------------------------------
@@ -268,7 +268,7 @@ pub struct WeixinCheckQrcodeParams {
pub async fn weixin_check_qrcode(
Extension(state): Extension<Arc<AppState>>,
Json(params): Json<WeixinCheckQrcodeParams>,
) -> Result<Json<WeixinQrcodeStatus>, AppCommandError> {
) -> Result<Json<WeixinQrcodeStatusPublic>, AppCommandError> {
let result =
cc_commands::weixin_check_qrcode_core(&state.db, params.channel_id, &params.qrcode)
.await?;