From 9d3c00461751380702a402268d151ac9f2da00c8 Mon Sep 17 00:00:00 2001 From: Liuxinyu176 <1041316040@qq.com> Date: Thu, 8 Oct 2026 22:22:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20=E6=96=B0=E5=A2=9E=E5=8F=8D?= =?UTF-8?q?=E4=BB=A3=E7=BD=91=E5=85=B3=E9=AA=A8=E6=9E=B6=E4=B8=8E=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=B7=AF=E7=BA=BF=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/PROXY-ROADMAP.md:M0-M4 里程碑、Provider 矩阵、风险与参考仓库 - data/proxy/:ProviderKind/ProviderRegion/ProxyModels/ProviderAdapter/AccountPool/RegionRouter/LocalProxyServer 接口骨架 - .gitignore:忽略 _tmp_* 调试文件,防止真实接口响应/账号数据入库 --- .gitignore | 3 + .../com/rainy/token/data/proxy/AccountPool.kt | 44 +++++++ .../token/data/proxy/LocalProxyServer.kt | 27 ++++ .../rainy/token/data/proxy/ProviderAdapter.kt | 21 +++ .../rainy/token/data/proxy/ProviderKind.kt | 33 +++++ .../rainy/token/data/proxy/ProviderRegion.kt | 6 + .../com/rainy/token/data/proxy/ProxyModels.kt | 56 ++++++++ .../rainy/token/data/proxy/RegionRouter.kt | 30 +++++ docs/PROXY-ROADMAP.md | 120 ++++++++++++++++++ 9 files changed, 340 insertions(+) create mode 100644 app/src/main/java/com/rainy/token/data/proxy/AccountPool.kt create mode 100644 app/src/main/java/com/rainy/token/data/proxy/LocalProxyServer.kt create mode 100644 app/src/main/java/com/rainy/token/data/proxy/ProviderAdapter.kt create mode 100644 app/src/main/java/com/rainy/token/data/proxy/ProviderKind.kt create mode 100644 app/src/main/java/com/rainy/token/data/proxy/ProviderRegion.kt create mode 100644 app/src/main/java/com/rainy/token/data/proxy/ProxyModels.kt create mode 100644 app/src/main/java/com/rainy/token/data/proxy/RegionRouter.kt create mode 100644 docs/PROXY-ROADMAP.md diff --git a/.gitignore b/.gitignore index 02f92f9..27e6c16 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ release.jks tmp_cc/ goal.md /reference/ + +# 调试临时文件(可能包含真实接口响应/账号数据,禁止提交) +_tmp_* diff --git a/app/src/main/java/com/rainy/token/data/proxy/AccountPool.kt b/app/src/main/java/com/rainy/token/data/proxy/AccountPool.kt new file mode 100644 index 0000000..ae71135 --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/AccountPool.kt @@ -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>() + private val cursor = mutableMapOf() + + fun register(account: PooledAccount) { + accounts.getOrPut(account.kind) { mutableListOf() }.add(account) + } + + fun registerAll(list: List) { + 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> = mutex.withLock { + accounts.mapValues { it.value.toList() } + } +} diff --git a/app/src/main/java/com/rainy/token/data/proxy/LocalProxyServer.kt b/app/src/main/java/com/rainy/token/data/proxy/LocalProxyServer.kt new file mode 100644 index 0000000..441d361 --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/LocalProxyServer.kt @@ -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 + + fun start(config: ProxyServerConfig): Result + + fun stop() +} diff --git a/app/src/main/java/com/rainy/token/data/proxy/ProviderAdapter.kt b/app/src/main/java/com/rainy/token/data/proxy/ProviderAdapter.kt new file mode 100644 index 0000000..28aaee0 --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/ProviderAdapter.kt @@ -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 + + /** 发起对话,内部处理流式/非流式。 */ + fun chat(request: ProxyChatRequest, accountId: String?): Flow +} diff --git a/app/src/main/java/com/rainy/token/data/proxy/ProviderKind.kt b/app/src/main/java/com/rainy/token/data/proxy/ProviderKind.kt new file mode 100644 index 0000000..21ae760 --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/ProviderKind.kt @@ -0,0 +1,33 @@ +package com.rainy.token.data.proxy + +/** + * 网关支持的 Provider 清单。 + * + * 命名规则:按「产品 + 区域」区分(同一个产品在国内版/国际版使用不同域名与鉴权体系)。 + * supportsCheckin 表示该平台官方是否有每日签到类活动;是否已实测由各自 Repository 决定。 + */ +enum class ProviderKind( + val displayName: String, + val regions: Set = 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()), +} diff --git a/app/src/main/java/com/rainy/token/data/proxy/ProviderRegion.kt b/app/src/main/java/com/rainy/token/data/proxy/ProviderRegion.kt new file mode 100644 index 0000000..c8183fa --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/ProviderRegion.kt @@ -0,0 +1,6 @@ +package com.rainy.token.data.proxy + +/** + * 区域:CN=国内版(中国大陆域名),INTL=国际版(海外域名)。 + */ +enum class ProviderRegion { CN, INTL } diff --git a/app/src/main/java/com/rainy/token/data/proxy/ProxyModels.kt b/app/src/main/java/com/rainy/token/data/proxy/ProxyModels.kt new file mode 100644 index 0000000..8ae399a --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/ProxyModels.kt @@ -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 = emptyList(), + val stream: Boolean = false, + val tools: List? = 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? = 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? = 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, +) diff --git a/app/src/main/java/com/rainy/token/data/proxy/RegionRouter.kt b/app/src/main/java/com/rainy/token/data/proxy/RegionRouter.kt new file mode 100644 index 0000000..5c26f16 --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/RegionRouter.kt @@ -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") + } +} diff --git a/docs/PROXY-ROADMAP.md b/docs/PROXY-ROADMAP.md new file mode 100644 index 0000000..bb4715f --- /dev/null +++ b/docs/PROXY-ROADMAP.md @@ -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