dudu MVP:五端语音输入法初始提交
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s

- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-12 00:38:37 +08:00
commit 40760aa884
252 changed files with 40789 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
//! 客户端打点(10H):内存队列,满 20 条或 60s 批量上报;失败丢弃不影响功能。
use parking_lot::Mutex;
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::Duration;
use tauri::{AppHandle, Manager};
#[derive(Clone)]
pub struct Metrics {
queue: Arc<Mutex<Vec<Value>>>,
}
impl Metrics {
pub fn record(&self, event: &str, props: Value) {
let mut q = self.queue.lock();
q.push(json!({
"event": event,
"props": props,
"client_ts": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0),
}));
if q.len() > 1000 {
let overflow = q.len() - 1000;
q.drain(..overflow); // 上限丢最旧
}
}
}
pub fn spawn(app: AppHandle) -> Metrics {
let m = Metrics {
queue: Arc::new(Mutex::new(Vec::new())),
};
let queue = m.queue.clone();
tauri::async_runtime::spawn(async move {
let client = reqwest::Client::new();
let mut tick = tokio::time::interval(Duration::from_secs(10));
loop {
tick.tick().await;
let (events, url, device_id) = {
let mut q = queue.lock();
if q.is_empty() || (q.len() < 20 && tickcount_skip(&q)) {
continue;
}
let events: Vec<Value> = q.drain(..).collect();
let s = app.state::<crate::settings::SettingsStore>().get();
(events, format!("{}/v1/metrics/batch", s.server_url), s.device_id)
};
let body = json!({
"device_id": device_id,
"platform": std::env::consts::OS,
"app_version": env!("CARGO_PKG_VERSION"),
"os_version": "",
"events": events,
});
let _ = client.post(&url).json(&body).send().await; // 失败即丢弃
}
});
m
}
// 不满 20 条时按 60s 节奏(tick 10s × 6 次取模近似)上报;保持实现极简。
fn tickcount_skip(_q: &[Value]) -> bool {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
N.fetch_add(1, Ordering::Relaxed) % 6 != 0
}