dudu MVP:五端语音输入法初始提交
- 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:
@@ -0,0 +1,139 @@
|
||||
package protocol
|
||||
|
||||
// ─── 认证 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type AuthQrResponse struct {
|
||||
State string `json:"state"`
|
||||
QrURL string `json:"qr_url"`
|
||||
}
|
||||
|
||||
type AuthQrStatusResponse struct {
|
||||
Status string `json:"status"` // pending | confirmed | expired
|
||||
Token string `json:"token,omitempty"`
|
||||
User *UserInfo `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type AuthWechatRequest struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
type AuthTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
User UserInfo `json:"user"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
UserID string `json:"user_id"`
|
||||
NicknameMasked string `json:"nickname_masked"`
|
||||
}
|
||||
|
||||
// ─── 用户与余额 ───────────────────────────────────────────────────────────────
|
||||
|
||||
// 账户状态:余额 > 0 → ok;余额 = 0 且试用未尽 → trial;皆尽 → quota。
|
||||
const (
|
||||
AccountOK = "ok"
|
||||
AccountTrial = "trial"
|
||||
AccountQuota = "quota"
|
||||
)
|
||||
|
||||
type MeResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
NicknameMasked string `json:"nickname_masked"`
|
||||
BalanceSeconds int64 `json:"balance_seconds"`
|
||||
TrialDailyLimit int `json:"trial_daily_limit"`
|
||||
TrialUsedToday int `json:"trial_used_today"`
|
||||
AccountState string `json:"account_state"` // ok | trial | quota
|
||||
}
|
||||
|
||||
// ─── 时长包与订单 ─────────────────────────────────────────────────────────────
|
||||
|
||||
type Pack struct {
|
||||
ID string `json:"id"`
|
||||
Minutes int `json:"minutes"`
|
||||
PriceCents int `json:"price_cents"`
|
||||
UnitDesc string `json:"unit_desc"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
type PacksResponse struct {
|
||||
Packs []Pack `json:"packs"`
|
||||
}
|
||||
|
||||
const (
|
||||
PayChannelNative = "native" // 桌面扫码
|
||||
PayChannelApp = "app" // 移动收银台
|
||||
)
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
PackID string `json:"pack_id" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required"` // native | app
|
||||
}
|
||||
|
||||
type CreateOrderResponse struct {
|
||||
OrderID string `json:"order_id"`
|
||||
CodeURL string `json:"code_url,omitempty"` // native
|
||||
PayParams map[string]any `json:"pay_params,omitempty"` // app
|
||||
}
|
||||
|
||||
type OrderStatusResponse struct {
|
||||
Status string `json:"status"` // pending | paid | closed
|
||||
BalanceSeconds int64 `json:"balance_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// ─── 反馈(v1.1)──────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
FeedbackMaxImages = 3
|
||||
FeedbackMaxImageBytes = 5 << 20 // 5MB
|
||||
FeedbackMaxContentLen = 1000
|
||||
FeedbackDailyLimit = 10
|
||||
)
|
||||
|
||||
type FeedbackResponse struct {
|
||||
FeedbackID string `json:"feedback_id"`
|
||||
}
|
||||
|
||||
// ─── 打点(v1.1)──────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
MetricsMaxBatch = 100
|
||||
MetricsMaxPropBytes = 2048
|
||||
)
|
||||
|
||||
type MetricEvent struct {
|
||||
Event string `json:"event"`
|
||||
Props map[string]any `json:"props,omitempty"`
|
||||
ClientTs int64 `json:"client_ts"`
|
||||
}
|
||||
|
||||
type MetricsBatchRequest struct {
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
Platform string `json:"platform" binding:"required"` // mac | win | ios | android
|
||||
AppVersion string `json:"app_version"`
|
||||
OSVersion string `json:"os_version"`
|
||||
Events []MetricEvent `json:"events" binding:"required"`
|
||||
}
|
||||
|
||||
// MetricWhitelist 事件白名单:未知事件名直接丢弃(见前端文档 9.1 指标清单)。
|
||||
var MetricWhitelist = map[string]bool{
|
||||
"asr.first_partial_ms": true,
|
||||
"asr.release_to_commit_ms": true,
|
||||
"audio.start_ms": true,
|
||||
"ws.connect_ms": true,
|
||||
"ws.reconnect": true,
|
||||
"asr.session": true,
|
||||
"inject.fail": true,
|
||||
"onboarding.step": true,
|
||||
"login.success": true,
|
||||
"purchase.success": true,
|
||||
"kb.enabled": true,
|
||||
}
|
||||
|
||||
// ─── 应用更新 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
type AppLatestResponse struct {
|
||||
Version string `json:"version"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package protocol
|
||||
|
||||
// 错误码(WS error 帧与 REST 错误体共用)。
|
||||
const (
|
||||
ErrQuotaExceeded = "QUOTA_EXCEEDED" // 试用 + 余额均已用尽
|
||||
ErrUnauthorized = "UNAUTHORIZED" // JWT 失效 / 被踢出
|
||||
ErrASRUnavailable = "ASR_UNAVAILABLE" // 上游 ASR 故障(重试/切备用后仍失败)
|
||||
ErrSessionLimit = "SESSION_LIMIT" // 单会话超长,已自动截断定稿
|
||||
ErrRateLimited = "RATE_LIMITED" // 设备 30 分钟滑动窗口超限
|
||||
ErrFeedbackRateLimited = "FEEDBACK_RATE_LIMITED" // 反馈每日条数超限
|
||||
ErrBadRequest = "BAD_REQUEST"
|
||||
ErrInternal = "INTERNAL"
|
||||
)
|
||||
|
||||
// ErrMessage 错误码对应的用户可读文案(客户端可直接展示,遵循文案规范:短句、无句号)。
|
||||
var ErrMessage = map[string]string{
|
||||
ErrQuotaExceeded: "今日试用已用完,去购买时长",
|
||||
ErrUnauthorized: "登录已失效,请重新登录",
|
||||
ErrASRUnavailable: "识别服务暂不可用,稍后再试",
|
||||
ErrSessionLimit: "单次最长 3 分钟,松开后可继续",
|
||||
ErrRateLimited: "操作过于频繁,稍后再试",
|
||||
ErrFeedbackRateLimited: "今天反馈次数已达上限",
|
||||
}
|
||||
|
||||
// APIError REST 统一错误体。
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewAPIError(code string) APIError {
|
||||
return APIError{Code: code, Message: ErrMessage[code]}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Package protocol 定义客户端 ↔ 后端的通信契约,是 5 端(server/desktop/android/ios/web)
|
||||
// 对齐的唯一真相源。设计依据 doc/backend-architecture.html 第四章。
|
||||
package protocol
|
||||
|
||||
// 音频参数:二进制帧为 16kHz / 16bit / mono PCM,每 100ms 一帧。
|
||||
const (
|
||||
SampleRate = 16000
|
||||
FrameMillis = 100
|
||||
FrameBytes = SampleRate * 2 * FrameMillis / 1000 // 3200
|
||||
|
||||
// MaxSessionSeconds 单会话上限,到点服务端自动截断定稿(SESSION_LIMIT)。
|
||||
MaxSessionSeconds = 180
|
||||
|
||||
// 设备维度 30 分钟滑动窗口限制。
|
||||
RateWindowSeconds = 30 * 60
|
||||
RateMaxSessions = 30
|
||||
RateMaxAudioSeconds = 30 * 60
|
||||
MaxConcurrentPerDevice = 1
|
||||
|
||||
// TrialDailySeconds 每日免费试用时长(秒)。
|
||||
TrialDailySeconds = 180
|
||||
)
|
||||
|
||||
// 上行文本帧类型。
|
||||
const (
|
||||
MsgStart = "start"
|
||||
MsgStop = "stop"
|
||||
MsgCancel = "cancel"
|
||||
)
|
||||
|
||||
// 下行文本帧类型。
|
||||
const (
|
||||
MsgPartial = "partial"
|
||||
MsgFinal = "final"
|
||||
MsgUsage = "usage"
|
||||
MsgError = "error"
|
||||
)
|
||||
|
||||
// ClientMsg 客户端上行控制消息(文本帧)。音频走二进制帧。
|
||||
type ClientMsg struct {
|
||||
Type string `json:"type"` // start | stop | cancel
|
||||
SessionID string `json:"session_id"`
|
||||
SampleRate int `json:"sample_rate,omitempty"` // start 必填,当前仅支持 16000
|
||||
Format string `json:"format,omitempty"` // start 必填,当前仅支持 pcm16
|
||||
}
|
||||
|
||||
// ServerMsg 服务端下行消息(文本帧)。
|
||||
type ServerMsg struct {
|
||||
Type string `json:"type"` // partial | final | usage | error
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
|
||||
// partial / final
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
// usage:识别中每 2s 及 stop 后下发,驱动 UI 实时刷新余额
|
||||
SessionSeconds int `json:"session_seconds,omitempty"`
|
||||
BalanceSeconds int64 `json:"balance_seconds,omitempty"`
|
||||
TrialRemaining int `json:"trial_remaining,omitempty"`
|
||||
|
||||
// error
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user