feat(proxy): 新增反代网关骨架与开发路线图
- docs/PROXY-ROADMAP.md:M0-M4 里程碑、Provider 矩阵、风险与参考仓库 - data/proxy/:ProviderKind/ProviderRegion/ProxyModels/ProviderAdapter/AccountPool/RegionRouter/LocalProxyServer 接口骨架 - .gitignore:忽略 _tmp_* 调试文件,防止真实接口响应/账号数据入库
This commit is contained in:
parent
79f99f5dee
commit
9d3c004617
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@ release.jks
|
|||||||
tmp_cc/
|
tmp_cc/
|
||||||
goal.md
|
goal.md
|
||||||
/reference/
|
/reference/
|
||||||
|
|
||||||
|
# 调试临时文件(可能包含真实接口响应/账号数据,禁止提交)
|
||||||
|
_tmp_*
|
||||||
|
|||||||
44
app/src/main/java/com/rainy/token/data/proxy/AccountPool.kt
Normal file
44
app/src/main/java/com/rainy/token/data/proxy/AccountPool.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号池:按 Provider 保存账号,负责轮询/冷却/会话粘性。
|
||||||
|
*
|
||||||
|
* 当前为骨架(round-robin)。后续把 workbuddy2api-panel 的
|
||||||
|
* 三因子加权、429 熔断冷却、会话粘性搬进来。
|
||||||
|
*/
|
||||||
|
class AccountPool {
|
||||||
|
|
||||||
|
data class PooledAccount(
|
||||||
|
val kind: ProviderKind,
|
||||||
|
val region: ProviderRegion? = null,
|
||||||
|
val accountId: String? = null,
|
||||||
|
val label: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private val accounts = mutableMapOf<ProviderKind, MutableList<PooledAccount>>()
|
||||||
|
private val cursor = mutableMapOf<ProviderKind, Int>()
|
||||||
|
|
||||||
|
fun register(account: PooledAccount) {
|
||||||
|
accounts.getOrPut(account.kind) { mutableListOf() }.add(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerAll(list: List<PooledAccount>) {
|
||||||
|
list.forEach(::register)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun next(kind: ProviderKind, sessionKey: String? = null): PooledAccount? = mutex.withLock {
|
||||||
|
val list = accounts[kind]?.takeIf { it.isNotEmpty() } ?: return null
|
||||||
|
// TODO: 会话粘性:命中 sessionKey 对应账号时优先返回
|
||||||
|
val idx = cursor[kind] ?: 0
|
||||||
|
cursor[kind] = (idx + 1) % list.size
|
||||||
|
list[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun snapshot(): Map<ProviderKind, List<PooledAccount>> = mutex.withLock {
|
||||||
|
accounts.mapValues { it.value.toList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地 HTTP 反代服务配置。
|
||||||
|
*/
|
||||||
|
data class ProxyServerConfig(
|
||||||
|
val port: Int = 8787,
|
||||||
|
/** 客户端必须携带的 Bearer Key;null = 关闭鉴权(仅本机建议)。 */
|
||||||
|
val apiKey: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地 HTTP 反代服务。
|
||||||
|
*
|
||||||
|
* 具体实现建议用 Ktor CIO(协程友好,方便 SSE 流式转发);
|
||||||
|
* 若 APK 体积敏感,可换 NanoHTTPD,但流式转发需要额外桥接。
|
||||||
|
*/
|
||||||
|
interface LocalProxyServer {
|
||||||
|
val isRunning: StateFlow<Boolean>
|
||||||
|
|
||||||
|
fun start(config: ProxyServerConfig): Result<Unit>
|
||||||
|
|
||||||
|
fun stop()
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的上游 Provider 适配器。
|
||||||
|
*
|
||||||
|
* 实现类负责把 [ProxyChatRequest] 翻译成各家私有协议
|
||||||
|
* (llm_utils_chat / v2/plugin / ACP 等),并把上游响应/SSE 流翻译回 [Flow]。
|
||||||
|
*
|
||||||
|
* 账号池、熔断、会话粘性由网关上层统一处理,不在适配器内重复实现。
|
||||||
|
*/
|
||||||
|
interface ProviderAdapter {
|
||||||
|
val kind: ProviderKind
|
||||||
|
|
||||||
|
/** 该适配器支持的模型列表;空列表表示暂未加载/不支持。 */
|
||||||
|
suspend fun models(): List<ProxyModel>
|
||||||
|
|
||||||
|
/** 发起对话,内部处理流式/非流式。 */
|
||||||
|
fun chat(request: ProxyChatRequest, accountId: String?): Flow<ProxyChatChunk>
|
||||||
|
}
|
||||||
33
app/src/main/java/com/rainy/token/data/proxy/ProviderKind.kt
Normal file
33
app/src/main/java/com/rainy/token/data/proxy/ProviderKind.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关支持的 Provider 清单。
|
||||||
|
*
|
||||||
|
* 命名规则:按「产品 + 区域」区分(同一个产品在国内版/国际版使用不同域名与鉴权体系)。
|
||||||
|
* supportsCheckin 表示该平台官方是否有每日签到类活动;是否已实测由各自 Repository 决定。
|
||||||
|
*/
|
||||||
|
enum class ProviderKind(
|
||||||
|
val displayName: String,
|
||||||
|
val regions: Set<ProviderRegion> = ProviderRegion.entries.toSet(),
|
||||||
|
val supportsChat: Boolean = true,
|
||||||
|
val supportsCheckin: Boolean = false,
|
||||||
|
) {
|
||||||
|
WORKBUDDY_CN("WorkBuddy 国内版", setOf(ProviderRegion.CN), supportsCheckin = true),
|
||||||
|
WORKBUDDY_INTL("WorkBuddy 国际版", setOf(ProviderRegion.INTL)),
|
||||||
|
TRAE_CN("Trae CN", setOf(ProviderRegion.CN), supportsCheckin = true),
|
||||||
|
TRAE_INTL("Trae INTL", setOf(ProviderRegion.INTL)),
|
||||||
|
TRAE_WORK("TraeWork 桌面版", setOf(ProviderRegion.CN)),
|
||||||
|
QODER_CN("Qoder CN", setOf(ProviderRegion.CN), supportsCheckin = true),
|
||||||
|
RACCOON_CN("小浣熊(商汤)", setOf(ProviderRegion.CN), supportsCheckin = true),
|
||||||
|
CATPAW_CN("CatPaw(美团)", setOf(ProviderRegion.CN)),
|
||||||
|
AUTOCLAW_CN("AutoClaw 国内版", setOf(ProviderRegion.CN), supportsCheckin = true),
|
||||||
|
AUTOCLAW_INTL("AutoClaw 国际版", setOf(ProviderRegion.INTL)),
|
||||||
|
CLINE("Cline", setOf(ProviderRegion.INTL)),
|
||||||
|
ACCIO_CN("Accio 国内版", setOf(ProviderRegion.CN)),
|
||||||
|
ACCIO_INTL("Accio 国际版", setOf(ProviderRegion.INTL)),
|
||||||
|
ZCODE_CN("ZCode 国内版", setOf(ProviderRegion.CN)),
|
||||||
|
ZCODE_INTL("ZCode 国际版", setOf(ProviderRegion.INTL)),
|
||||||
|
CODEARTS_CN("CodeArts(华为云)", setOf(ProviderRegion.CN)),
|
||||||
|
LOOMY_CN("Loomy(讯飞)", setOf(ProviderRegion.CN), supportsCheckin = true),
|
||||||
|
OPENAI_COMPATIBLE("OpenAI 兼容自定义", ProviderRegion.entries.toSet()),
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域:CN=国内版(中国大陆域名),INTL=国际版(海外域名)。
|
||||||
|
*/
|
||||||
|
enum class ProviderRegion { CN, INTL }
|
||||||
56
app/src/main/java/com/rainy/token/data/proxy/ProxyModels.kt
Normal file
56
app/src/main/java/com/rainy/token/data/proxy/ProxyModels.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关对外的最小 OpenAI 兼容模型。
|
||||||
|
* 后续按协议(Chat Completions / Responses / Anthropic)扩展。
|
||||||
|
*/
|
||||||
|
data class ProxyChatRequest(
|
||||||
|
val model: String,
|
||||||
|
val messages: List<ProxyChatMessage> = emptyList(),
|
||||||
|
val stream: Boolean = false,
|
||||||
|
val tools: List<JsonElement>? = null,
|
||||||
|
val toolChoice: JsonElement? = null,
|
||||||
|
val reasoningEffort: String? = null,
|
||||||
|
val maxTokens: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ProxyChatMessage(
|
||||||
|
val role: String,
|
||||||
|
val content: String? = null,
|
||||||
|
val toolCalls: List<JsonElement>? = null,
|
||||||
|
val toolCallId: String? = null,
|
||||||
|
val name: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ProxyModel(
|
||||||
|
val id: String,
|
||||||
|
val provider: ProviderKind,
|
||||||
|
val region: ProviderRegion? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 流式/非流式统一输出块,由 StreamNormalizer 归一。 */
|
||||||
|
sealed interface ProxyChatChunk {
|
||||||
|
data class Done(
|
||||||
|
val text: String? = null,
|
||||||
|
val usage: ProxyUsage? = null,
|
||||||
|
) : ProxyChatChunk
|
||||||
|
|
||||||
|
data class Delta(
|
||||||
|
val text: String? = null,
|
||||||
|
val reasoning: String? = null,
|
||||||
|
val toolCalls: List<JsonElement>? = null,
|
||||||
|
) : ProxyChatChunk
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
val code: Int? = null,
|
||||||
|
) : ProxyChatChunk
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ProxyUsage(
|
||||||
|
val promptTokens: Long? = null,
|
||||||
|
val completionTokens: Long? = null,
|
||||||
|
val totalTokens: Long? = null,
|
||||||
|
)
|
||||||
30
app/src/main/java/com/rainy/token/data/proxy/RegionRouter.kt
Normal file
30
app/src/main/java/com/rainy/token/data/proxy/RegionRouter.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型名 → Provider + Region 路由。
|
||||||
|
*
|
||||||
|
* 骨架策略:按模型家族前缀映射;后续可用 agent2api / proxy-hub 的远端模型目录替换。
|
||||||
|
*/
|
||||||
|
class RegionRouter {
|
||||||
|
|
||||||
|
data class Route(
|
||||||
|
val kind: ProviderKind,
|
||||||
|
val region: ProviderRegion,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun route(model: String): Route? {
|
||||||
|
val normalized = model.lowercase()
|
||||||
|
return when {
|
||||||
|
// 国内/字节/智谱系模型默认 CN
|
||||||
|
CN_MODEL_FAMILIES.any { normalized.contains(it) } -> Route(ProviderKind.OPENAI_COMPATIBLE, ProviderRegion.CN)
|
||||||
|
// 国际模型默认 INTL
|
||||||
|
INTL_MODEL_FAMILIES.any { normalized.contains(it) } -> Route(ProviderKind.OPENAI_COMPATIBLE, ProviderRegion.INTL)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val CN_MODEL_FAMILIES = listOf("glm", "deepseek", "kimi", "qwen", "doubao", "hunyuan", "minimax")
|
||||||
|
private val INTL_MODEL_FAMILIES = listOf("gpt", "o1", "o3", "o4", "claude", "gemini", "grok")
|
||||||
|
}
|
||||||
|
}
|
||||||
120
docs/PROXY-ROADMAP.md
Normal file
120
docs/PROXY-ROADMAP.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# RainyToken 反代网关开发路线图(PROXY-ROADMAP)
|
||||||
|
|
||||||
|
> 分支:dev-proxy
|
||||||
|
> 目标:在雨晴Token App 内嵌一个多提供商 OpenAI 兼容反代网关,支持双区、签到、反代、多账号池,并尽量覆盖 agent2api / fleet-kit 等开源项目已支持的平台。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 最终目标
|
||||||
|
|
||||||
|
App 内启动一个本地 HTTP 服务(默认 `127.0.0.1:8787`),对外暴露:
|
||||||
|
|
||||||
|
- `GET /v1/models`
|
||||||
|
- `POST /v1/chat/completions`(OpenAI 兼容,流式/非流式)
|
||||||
|
- `POST /v1/responses`(Codex Responses API)
|
||||||
|
- `POST /v1/messages`(Anthropic Messages,Claude Code 直连)
|
||||||
|
|
||||||
|
请求按「模型名 → Provider + Region + 账号」路由,复用现有 `CredentialRepository` / `SecureStorage` / 各 Repository。
|
||||||
|
|
||||||
|
## 2. 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
LocalProxyServer (127.0.0.1:8787)
|
||||||
|
│
|
||||||
|
├── /v1/chat/completions
|
||||||
|
├── /v1/responses
|
||||||
|
├── /v1/messages
|
||||||
|
└── /v1/models
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ProviderRouter(模型名 → Provider + Region + Account)
|
||||||
|
│
|
||||||
|
┌──────┼──────────┬─────────────┬──────────────┐
|
||||||
|
▼ ▼ ▼ ▼ ▼
|
||||||
|
WBAdpt TraeAdpt QoderAdpt RaccoonAdpt OfficialPassthrough
|
||||||
|
(CN/INTL)(CN/INTL)(CN) (CN) (DeepSeek/Gemini/Kimi…)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AccountPool(多账号轮询 + 熔断冷却 + 会话粘性)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
StreamNormalizer(各家上游 SSE 分片 → 标准 OpenAI/Anthropic 流)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 已编码骨架(data/proxy)
|
||||||
|
|
||||||
|
- `ProviderKind.kt`:全部计划支持的 Provider 枚举
|
||||||
|
- `ProviderRegion.kt`:CN / INTL
|
||||||
|
- `ProxyModels.kt`:统一 Chat 请求 / 响应 / 模型 / 用量
|
||||||
|
- `ProviderAdapter.kt`:统一 Provider 适配器接口
|
||||||
|
- `AccountPool.kt`:账号池骨架(round-robin,后续加权)
|
||||||
|
- `RegionRouter.kt`:模型 → Provider/Region 路由骨架
|
||||||
|
- `LocalProxyServer.kt`:本地服务接口 + 配置
|
||||||
|
|
||||||
|
## 4. 里程碑
|
||||||
|
|
||||||
|
### M0:地基(当前)
|
||||||
|
- 新增 `data/proxy` 骨架,能编译
|
||||||
|
- 新增本路线图
|
||||||
|
- 验收:`./gradlew :app:assembleDebug` 绿
|
||||||
|
|
||||||
|
### M1:先打通“已经有凭据体系”的三家
|
||||||
|
- WorkBuddy 国内版 Chat 适配器(参考 workbuddy2api / workbuddy-gateway)
|
||||||
|
- Trae CN Chat 适配器(参考 Trae2api-cn / trae2api)
|
||||||
|
- Sub2API 透传(本身是 OpenAI 兼容协议)
|
||||||
|
- 本地 HTTP 服务可用,任意 OpenAI 客户端可连
|
||||||
|
- 验收:真机 + 真凭据,WorkBuddy / Trae / Sub2API 各能回复一条消息
|
||||||
|
|
||||||
|
### M2:双区 + 多账号治理
|
||||||
|
- WorkBuddy 国际版、Trae INTL/Solo/Work 适配器
|
||||||
|
- RegionRouter 按模型家族自动选区(glm/deepseek/kimi → CN,gpt/claude/gemini → INTL)
|
||||||
|
- AccountPool 加权轮询 + 429 熔断 + 会话粘性
|
||||||
|
- 验收:国内/国际账号混挂,同模型自动走对应区,单号故障自动换号
|
||||||
|
|
||||||
|
### M3:覆盖 agent2api 全家桶
|
||||||
|
- Qoder CN、小浣熊、CatPaw、AutoClaw CN/INTL、Cline、Accio CN/INTL、ZCode CN/INTL、CodeArts、Loomy
|
||||||
|
- 每家一个 Adapter,抄 agent2api `providers/` 目录
|
||||||
|
- 验收:每家至少能在有凭据的情况下完成一次对话;签到/余额尽量复用现有页
|
||||||
|
|
||||||
|
### M4:长尾 + 官方直连
|
||||||
|
- 灵犀、团结AI、TokenDance、Kimi Code、MiniMax、Gemini
|
||||||
|
- 官方 OpenAI 兼容平台统一走 `OfficialPassthrough`(配置 base_url + key)
|
||||||
|
- 设置页新增「代理网关」管理:端口、启动/停止、API Key、运行状态
|
||||||
|
- 验收:所有支持平台可在 UI 中启用/禁用
|
||||||
|
|
||||||
|
## 5. Provider 支持矩阵(规划)
|
||||||
|
|
||||||
|
| Provider | 区域 | Chat | 签到 | 参考实现 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| WorkBuddy | CN / INTL | ✅ | CN 有 | agent2api / fleet-kit / proxy-hub |
|
||||||
|
| Trae / SOLO / Work | CN / INTL | ✅ | CN 有 | agent2api / proxy-hub |
|
||||||
|
| Qoder | CN | ✅ | CN 有 | agent2api / proxy-hub |
|
||||||
|
| 小浣熊 | CN | ✅ | 桌面登录积分 | agent2api / fleet-kit |
|
||||||
|
| CatPaw | CN | ✅ | - | agent2api |
|
||||||
|
| AutoClaw | CN / INTL | ✅ | CN 有 | agent2api |
|
||||||
|
| Cline | INTL | ✅ | - | agent2api |
|
||||||
|
| Accio | CN / INTL | ✅ | - | agent2api |
|
||||||
|
| ZCode | CN / INTL | ✅ | - | agent2api |
|
||||||
|
| CodeArts | CN | ✅ | 每日福利 | agent2api |
|
||||||
|
| Loomy | CN | ✅ | 每日积分 | agent2api / fleet-kit |
|
||||||
|
| 灵犀 / 团结AI / TokenDance | CN | ✅ | 待确认 | fleet-kit |
|
||||||
|
| Kimi Code / MiniMax / Gemini | INTL/CN | ✅ | - | 官方 API 透传 |
|
||||||
|
|
||||||
|
## 6. 关键风险与对策
|
||||||
|
|
||||||
|
1. **上游协议会变**:所有解析做防御式多候选扫描(项目现有惯例);接口变动时快速适配。
|
||||||
|
2. **设备指纹/风控**:Trae 签到 9074、WorkBuddy 设备指纹;沿用现有 `checkinDeviceId` / `derive_id` 思路。
|
||||||
|
3. **Token 单次轮换**:WorkBuddy refresh 单次消费,沿用 `RefreshWriteSession` 落库再用的机制。
|
||||||
|
4. **SSE 分片差异**:各家流式事件格式不同,用统一 `StreamNormalizer` 归一。
|
||||||
|
5. **Android 后台**:若只给本机内客户端用,App 前台运行即可;若希望后台常驻,需前台服务 + 通知。
|
||||||
|
|
||||||
|
## 7. 参考仓库
|
||||||
|
|
||||||
|
- agent2api:https://github.com/aimod-cc/agent2api
|
||||||
|
- workbuddy2api-hub:https://github.com/ardeyouxipianyi/workbuddy2api-hub
|
||||||
|
- workbuddy-gateway:https://github.com/CangShui/workbuddy-gateway
|
||||||
|
- workbuddy2api-panel:https://github.com/linguo2625469/workbuddy2api-panel
|
||||||
|
- Trae2api-cn:https://github.com/autumnsentiment/Trae2api-cn
|
||||||
|
- trae2api (Go):https://github.com/JeffHu0912/trae2api
|
||||||
|
- proxy-hub:https://github.com/1416277987/proxy-hub
|
||||||
|
- fleet-kit:https://github.com/cpufreestyle/fleet-kit
|
||||||
Loading…
Reference in New Issue
Block a user