feat: release v0.85.0 with full pending changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
coso
2026-03-13 02:40:20 +08:00
co-authored by Claude Opus 4.6
parent 621ab3ee73
commit 8995ef9c68
259 changed files with 24283 additions and 18539 deletions
+1
View File
@@ -16,6 +16,7 @@
- `develop/`:开发流程与协作规范
- `plugins/`:插件与扩展相关文档
- `tests/`:测试策略与用例文档
- `iteration-notes/`:迭代备忘与下版本建议(暂不进入当前发布范围的问题)
- `images/`:文档图片资源
- `TECH_SPEC.md`:技术规格文档
- `develop/execution-tracker-technical-plan.md`:统一执行追踪(Execution Tracker)专项技术规划
+41 -37
View File
@@ -8,13 +8,13 @@ ProxyCast 是一个桌面端 AI API 代理工具,将各种大模型客户端 A
基于 pubcast 项目技术栈:
| 类别 | 技术 |
|------|------|
| 框架 | Tauri 2.0 (Rust + Web) |
| 前端 | React 18 + TypeScript |
| 构建 | Vite 5 |
| UI | Tailwind CSS + Radix UI |
| 图标 | Lucide React |
| 类别 | 技术 |
| ---- | ----------------------- |
| 框架 | Tauri 2.0 (Rust + Web) |
| 前端 | React 18 + TypeScript |
| 构建 | Vite 5 |
| UI | Tailwind CSS + Radix UI |
| 图标 | Lucide React |
### 核心依赖
@@ -32,15 +32,15 @@ ProxyCast 是一个桌面端 AI API 代理工具,将各种大模型客户端 A
参考 AIClient-2-API,需支持以下渠道:
| Provider | 协议 | 说明 |
|----------|------|------|
| `claude-kiro-oauth` | OpenAI/Claude | Kiro OAuth 访问 Claude Sonnet 4.5 |
| `gemini-cli-oauth` | OpenAI/Claude/Gemini | Gemini CLI OAuth |
| `openai-qwen-oauth` | OpenAI/Claude | 通义千问 OAuth |
| `openai-custom` | OpenAI | 自定义 OpenAI 兼容 API |
| `claude-custom` | Claude | 自定义 Claude API |
| `gemini-antigravity` | Gemini | Antigravity 协议 |
| `openaiResponses-custom` | OpenAI Responses | 结构化对话 |
| Provider | 协议 | 说明 |
| ------------------------ | -------------------- | --------------------------------- |
| `claude-kiro-oauth` | OpenAI/Claude | Kiro OAuth 访问 Claude Sonnet 4.5 |
| `gemini-cli-oauth` | OpenAI/Claude/Gemini | Gemini CLI OAuth |
| `openai-qwen-oauth` | OpenAI/Claude | 通义千问 OAuth |
| `openai-custom` | OpenAI | 自定义 OpenAI 兼容 API |
| `claude-custom` | Claude | 自定义 Claude API |
| `gemini-antigravity` | Gemini | Antigravity 协议 |
| `openaiResponses-custom` | OpenAI Responses | 结构化对话 |
## 核心功能模块
@@ -74,9 +74,9 @@ src/
│ ├── TokenManager.tsx # Token 管理
│ ├── LogViewer.tsx # 日志查看
│ └── ui/ # 通用 UI 组件
├── hooks/
│ └── useTauri.ts # Tauri API hooks
├── hooks/ # 领域 Hook(不再承载统一 Tauri 聚合层)
└── lib/
├── api/ # 前端 API 网关
└── utils.ts
```
@@ -91,12 +91,12 @@ http://localhost:8999/{provider}/v1/messages
### 支持的端点
| 端点 | 协议 | 说明 |
|------|------|------|
| `/v1/chat/completions` | OpenAI | 聊天补全 |
| `/v1/messages` | Claude | Anthropic 消息 |
| `/v1/models` | OpenAI | 模型列表 |
| `/health` | - | 健康检查 |
| 端点 | 协议 | 说明 |
| ---------------------- | ------ | -------------- |
| `/v1/chat/completions` | OpenAI | 聊天补全 |
| `/v1/messages` | Claude | Anthropic 消息 |
| `/v1/models` | OpenAI | 模型列表 |
| `/health` | - | 健康检查 |
## 配置文件结构
@@ -139,12 +139,12 @@ http://localhost:8999/{provider}/v1/messages
## Token 凭证路径
| 服务 | 默认路径 |
|------|----------|
| Kiro | `~/.aws/sso/cache/kiro-auth-token.json` |
| Gemini | `~/.gemini/oauth_creds.json` |
| Qwen | `~/.qwen/oauth_creds.json` |
| Antigravity | `~/.antigravity/oauth_creds.json` |
| 服务 | 默认路径 |
| ----------- | --------------------------------------- |
| Kiro | `~/.aws/sso/cache/kiro-auth-token.json` |
| Gemini | `~/.gemini/oauth_creds.json` |
| Qwen | `~/.qwen/oauth_creds.json` |
| Antigravity | `~/.antigravity/oauth_creds.json` |
## 协议转换
@@ -156,13 +156,13 @@ OpenAI <---> Claude <---> Gemini
### 转换矩阵
| 输入协议 | 输出 Provider | 说明 |
|----------|---------------|------|
| OpenAI | kiro | OpenAI -> CodeWhisperer |
| OpenAI | gemini | OpenAI -> Gemini |
| Claude | kiro | Claude -> CodeWhisperer |
| Claude | gemini | Claude -> Gemini |
| Claude | openai | Claude -> OpenAI |
| 输入协议 | 输出 Provider | 说明 |
| -------- | ------------- | ----------------------- |
| OpenAI | kiro | OpenAI -> CodeWhisperer |
| OpenAI | gemini | OpenAI -> Gemini |
| Claude | kiro | Claude -> CodeWhisperer |
| Claude | gemini | Claude -> Gemini |
| Claude | openai | Claude -> OpenAI |
## UI 功能
@@ -175,21 +175,25 @@ OpenAI <---> Claude <---> Gemini
## 开发计划
### Phase 1: 基础框架
- [ ] Tauri 项目初始化
- [ ] 基础 UI 布局
- [ ] 配置管理
### Phase 2: Kiro Provider
- [ ] Kiro OAuth Token 读取
- [ ] CodeWhisperer API 调用
- [ ] OpenAI/Claude 协议支持
### Phase 3: 其他 Provider
- [ ] Gemini CLI OAuth
- [ ] Qwen OAuth
- [ ] OpenAI/Claude Custom
### Phase 4: 高级功能
- [ ] Provider Pool 管理
- [ ] 自动 Token 刷新
- [ ] 请求日志/统计
+7
View File
@@ -10,6 +10,7 @@ AI Agent 专用文档目录,提供模块级别的详细说明。
## 文件索引
### 核心系统
- `overview.md` - 项目架构概览
- `governance.md` - **治理第一原则**(新旧并存、迁移收口、禁止回流)
- `providers.md` - Provider 系统(OAuth/API Key 认证)
@@ -18,26 +19,31 @@ AI Agent 专用文档目录,提供模块级别的详细说明。
- `server.md` - HTTP 服务器(API 端点)
### 前端模块
- `components.md` - React 组件系统
- `hooks.md` - 自定义 React Hooks
- `lib.md` - 工具库和 API 封装
### 后端模块
- `services.md` - 业务服务层
- `commands.md` - Tauri 命令
- `database.md` - 数据库层(SQLite)
### 功能模块
- `terminal.md` - 内置终端
- `mcp.md` - MCP 服务器管理
- `plugins.md` - 插件系统
- `playwright-e2e.md` - Playwright MCP 续测与 E2E 指南
### Aster 集成
- `aster-integration.md` - **Aster 框架集成方案**
- `workspace.md` - **Workspace 设计文档**(工作目录管理)
### 内容创作
- `content-creator.md` - **内容创作系统**(write_file 标签、画布联动)
## 使用方式
@@ -50,6 +56,7 @@ AI Agent 在处理特定模块时,应先阅读对应的 aiprompts 文档:
# 处理新旧并存、迁移、重构、架构收口
→ 先读 docs/aiprompts/governance.md
→ 再执行 npm run governance:legacy-report
# 处理凭证池相关任务
→ 先读 docs/aiprompts/credential-pool.md
+1 -1
View File
@@ -82,7 +82,7 @@ ProxyCast 已完整集成 aster-rust 框架,包括凭证池桥接。
### 使用方式
> 治理约定:前端业务层不要直接 `invoke('aster_*')`,统一通过 `src/lib/api/agentRuntime.ts` 调用现役 Aster API。
> 治理约定:前端业务层不要直接 `invoke('aster_*')`,统一通过 `src/lib/api/agentRuntime.ts` 调用现役 Aster API。历史 `src/lib/api/agentCompat.ts` 已删除。
```typescript
import {
+57 -13
View File
@@ -2,7 +2,50 @@
## 概述
Tauri 命令是前端与 Rust 后端通信的桥梁,通过 `invoke` 调用。
Tauri 命令是前端与 Rust 后端通信的边界,但前端业务代码**不应直接散落 `invoke`**。
推荐路径是:
`组件 / Hook -> src/lib/api/* 网关 -> safeInvoke -> Rust command`
这样做的目的不是“多包一层”,而是确保:
- 前端只有一个可治理的调用出口
- Rust 命令可以按 `current / compat / deprecated` 分类演进
- 新旧命令并存时,迁移边界清晰,不会继续扩散
## 治理约束
- 新的前端功能,禁止在页面、组件、普通 Hook 中直接调用 `invoke`。
- 新的 Rust 命令,必须同时落一个对应的 `src/lib/api/*` 网关文件或收口到现有网关。
- 旧命令如果暂时不能删,必须明确标记为 `compat` 或 `deprecated`,只允许保兼容,不允许继续长新逻辑。
- 当前端已经迁到新网关后,要继续用 ESLint、脚本或日志告警封住旧入口,避免 AI 回流。
## 当前事实源
- 聊天主命令:`chat_*`
- 旧 `general_chat_*` 前端 compat 网关与 Rust 命令已删除
- 当前剩余治理重点:统计、记忆等旁路仍在读取 `general_chat_*` 历史表
## 治理案例:记忆系统
以当前仓库里的记忆能力为例:
- `unified_memory_*`:现役统一记忆主链路,后续功能优先往这里收
- `memory_runtime_*`:现役 runtime / 上下文记忆主入口
- `memory_get_*` / `memory_toggle_auto`:当前仍在使用的治理配置入口
- `switch_prompt`:旧 prompt 切换命令已移除,统一使用 `enable_prompt`
- `get_legacy_api_key_credentials` 等迁移命令:前端与 Tauri 入口都已移除,避免 UI/AI 再接入历史迁移链路
这类场景下,AI 不应该再做一套“第三套记忆命令”,而应该:
1. 先判断当前需求属于主链路、兼容层,还是治理配置
2. 如果是统一沉淀记忆,优先补到 `unified_memory_*`
3. 如果是 runtime / 上下文记忆视图,优先补到 `memory_runtime_*`
4. 如果存在旧命令又无任何调用,就直接删掉命令注册、桥接和 mock,不要继续保留空兼容壳
同理,对话系统也不应该重新引回已经删除的 `general_chat_*` 命令;
后续如需扩展聊天能力,应继续收敛到 `chat_*` 与对应网关。
## 目录结构
@@ -73,22 +116,23 @@ async fn clear_flow_records(before: Option<i64>) -> Result<u64, String>;
## 前端调用
推荐写法不是在业务层直接 `invoke`,而是在 API 网关里集中调用:
```typescript
import { invoke } from '@tauri-apps/api/core';
// src/lib/api/serverRuntime.ts
import { safeInvoke } from "@/lib/dev-bridge";
// 添加凭证
const credential = await invoke<CredentialInfo>('add_credential', {
provider: 'kiro',
filePath: '/path/to/credential.json',
});
export async function getServerStatus() {
return safeInvoke<ServerStatus>("get_server_status");
}
```
// 获取服务器状态
const status = await invoke<ServerStatus>('get_server_status');
业务层只消费 API 网关:
// 查询流量记录
const records = await invoke<PagedResult<FlowRecord>>('get_flow_records', {
query: { page: 1, pageSize: 20 },
});
```typescript
import { getServerStatus } from "@/lib/api/serverRuntime";
const status = await getServerStatus();
```
## 错误处理
+36 -40
View File
@@ -11,7 +11,7 @@ src/components/
├── ui/ # 基础 UI 组件 (shadcn/ui)
├── provider-pool/ # 凭证池管理
├── flow-monitor/ # 流量监控
├── general-chat/ # 通用对话
├── general-chat/ # 兼容画布桥接(非对话主入口)
├── terminal/ # 内置终端
├── mcp/ # MCP 服务器
├── settings/ # 设置页面
@@ -27,15 +27,15 @@ src/components/
```tsx
// src/components/AppSidebar.tsx
export function AppSidebar() {
return (
<aside className="w-14 bg-sidebar">
<nav className="flex flex-col items-center gap-2">
<SidebarItem icon={Home} to="/" />
<SidebarItem icon={MessageSquare} to="/chat" />
<SidebarItem icon={Settings} to="/settings" />
</nav>
</aside>
);
return (
<aside className="w-14 bg-sidebar">
<nav className="flex flex-col items-center gap-2">
<SidebarItem icon={Home} to="/" />
<SidebarItem icon={MessageSquare} to="/chat" />
<SidebarItem icon={Settings} to="/settings" />
</nav>
</aside>
);
}
```
@@ -46,14 +46,14 @@ export function AppSidebar() {
```tsx
// src/components/provider-pool/ProviderPoolPanel.tsx
export function ProviderPoolPanel() {
const { credentials, addCredential, removeCredential } = useProviderPool();
return (
<div className="space-y-4">
<CredentialList credentials={credentials} onRemove={removeCredential} />
<AddCredentialDialog onAdd={addCredential} />
</div>
);
const { credentials, addCredential, removeCredential } = useProviderPool();
return (
<div className="space-y-4">
<CredentialList credentials={credentials} onRemove={removeCredential} />
<AddCredentialDialog onAdd={addCredential} />
</div>
);
}
```
@@ -64,15 +64,15 @@ export function ProviderPoolPanel() {
```tsx
// src/components/flow-monitor/FlowMonitorPanel.tsx
export function FlowMonitorPanel() {
const { records, stats, query } = useFlowMonitor();
return (
<div className="flex flex-col h-full">
<FlowStats stats={stats} />
<FlowTable records={records} />
<FlowPagination query={query} />
</div>
);
const { records, stats, query } = useFlowMonitor();
return (
<div className="flex flex-col h-full">
<FlowStats stats={stats} />
<FlowTable records={records} />
<FlowPagination query={query} />
</div>
);
}
```
@@ -89,22 +89,18 @@ export function FlowMonitorPanel() {
```tsx
// 标准组件结构
interface Props {
// props 定义
// props 定义
}
export function ComponentName({ prop1, prop2 }: Props) {
// hooks
const [state, setState] = useState();
// handlers
const handleClick = () => {};
// render
return (
<div>
{/* JSX */}
</div>
);
// hooks
const [state, setState] = useState();
// handlers
const handleClick = () => {};
// render
return <div>{/* JSX */}</div>;
}
```
+9 -3
View File
@@ -17,7 +17,7 @@ AI 返回带 <write_file> 标签的响应
↓
StreamingRenderer 解析标签 → 调用 onWriteFile
↓
AgentChatPage.handleWriteFile → 更新画布状态
AgentChatPage.handleWriteFile → 映射为社媒 harness 产物 / 版本链
↓
右侧画布自动打开,显示文档内容
```
@@ -44,8 +44,10 @@ src/components/
│ │ └── MessageList.tsx # 消息列表
│ └── index.tsx # AgentChatPage
└── general-chat/
└── store/
└── useGeneralChatStore.ts # 通用对话 Store
├── bridge.ts # 兼容桥接层
├── canvas/
│ └── CanvasPanel.tsx # 复用画布面板
└── types.ts # CanvasState / DEFAULT_CANVAS_STATE
```
## 核心组件
@@ -217,6 +219,10 @@ const handleWriteFile = useCallback(
## 注意事项
- 社媒主题已不再把 `write_file` 仅视为“文件覆盖”,而是映射为带阶段语义的版本链产物
- `brief / draft / polished / platform variant / publish package` 应分别作为不同产物语义处理
- 日志、运行轨迹、正文产物三层分离:`harness` 产生命名事件,日志只做投影,正文仍由画布/产物承载
### Aster 框架限制
Aster 框架的 `SessionConfig` 不支持 session 级别的 system prompt,因此采用**消息注入**方案:
+28
View File
@@ -83,6 +83,32 @@
- CI 阻止新代码继续引用废弃路径
- 脚本扫描旧表、旧 DAO、旧命令、旧 Hook 的新增使用点
当前仓库可直接运行:
```bash
npm run governance:legacy-report
```
它会扫描:
- 已经被判定为 `deprecated` / `dead-candidate` 的前端入口(按真实 import 解析 `@/` 与相对路径)
- 旧 Tauri 命令是否仍然只收口在指定 API 网关
- 哪些兼容壳层已经零引用,可以进入删除候选
当前项目的最新治理状态可以概括为:
- 旧 `components/chat`、`general-chat` 页面 / Hook / Store / compat API 已删除
- Rust `general_chat_*` 兼容命令已删除
- `src/lib/api/agentCompat.ts` 已删除,Aster 前端只保留现役 runtime / stream API
- General Chat 历史数据迁移已接入数据库初始化;新治理优先推动“启动期迁移”,而不是长期保留运行时 fallback
- 下一阶段重点不再是页面和命令,而是统计、记忆等旁路对 `general_chat_*` 历史表的依赖
进一步治理时,建议坚持一个更细的边界规则:
- “迁移是否完成”的判断收口在 `Repository / Database` 边界
- 业务服务层只消费 `pending_*` 语义接口
- 不要在多个 service 里重复写 `is_migrated` 分支
原则只有一句:
**不是鼓励走新路,而是封住老路。**
@@ -184,6 +210,8 @@
3. 必须显式说明当前改动属于 `current`、`compat`、`deprecated`、`dead` 中哪一类。
4. 如果发现主链路与旁路系统割裂,必须指出,不得假装治理已经完成。
5. 如果无法在本次改动中完成收口,至少要建立守卫,阻止问题继续扩散。
6. 一旦历史数据迁移已接入启动流程,运行时必须按“迁移完成标记”短路旧表读取;旧表只允许服务迁移、审计与回放,不再参与主链路查询。
7. 过渡期对外暴露的命名必须体现“迁移态”语义,例如 `pending_*`,不要继续让业务层直接看见 `legacy_*` 模块名与函数名。
## 一句话总结
+91 -59
View File
@@ -2,7 +2,7 @@
## 概述
自定义 Hooks 封装业务逻辑,通过 Tauri invoke 与后端通信。
自定义 Hooks 封装业务逻辑;新代码应优先通过 `src/lib/api/*` 网关与后端通信,而不是直接在 Hook 中散落 `invoke`。
## 目录结构
@@ -15,10 +15,15 @@ src/hooks/
├── useFlowEvents.ts # 流量事件
├── useMcpServers.ts # MCP 服务器
├── useDeepLink.ts # Deep Link 处理
├── useSound.ts # 音效管理
└── useTauri.ts # Tauri 通用
└── useSound.ts # 音效管理
```
## 治理约束
- 新的前端能力优先落在 `src/lib/api/*`,再由 Hook 或组件消费。
- 历史 `useTauri.ts` 兼容聚合层已删除,不要重新引入新的“大一统 API Hook”。
- 旧聊天链路优先迁移到 `@/hooks/useUnifiedChat`,不要继续扩散 `useChat` / compat Hook。
## 核心 Hooks
### useUnifiedChat(统一对话)
@@ -39,8 +44,22 @@ const { messages, sendMessage, stopGeneration } = useUnifiedChat({
const creatorChat = useUnifiedChat({
mode: "creator",
systemPrompt: "你是内容创作助手...",
onCanvasUpdate: (path, content) => { /* 更新画布 */ },
onWriteFile: (content, fileName) => { /* 文件写入 */ },
harnessConfig: {
theme: "social-media",
artifactMode: "version-chain",
},
onHarnessEvent: (event) => {
/* 接收阶段推进、产物创建等语义事件 */
},
onArtifactUpdate: (artifact) => {
/* 接收产物快照 */
},
onCanvasUpdate: (path, content) => {
/* 更新画布 */
},
onWriteFile: (content, fileName) => {
/* 文件写入 */
},
});
// General 模式 - 纯文本对话
@@ -48,6 +67,7 @@ const generalChat = useUnifiedChat({ mode: "general" });
```
**返回值**:
- `session` - 当前会话
- `messages` - 消息列表
- `isLoading` / `isSending` - 状态
@@ -55,7 +75,13 @@ const generalChat = useUnifiedChat({ mode: "general" });
- `sendMessage()` / `stopGeneration()` - 消息操作
- `configureProvider()` - Provider 配置
**补充说明**:
- Creator 模式现在支持 `harnessConfig`、`onHarnessEvent`、`onArtifactUpdate`
- 社媒内容推荐把 `<write_file>` 结果投影为“版本链产物”,而不是只按文件名覆盖
**相关文件**:
- 类型定义:`src/types/chat.ts`
- API 封装:`src/lib/api/unified-chat.ts`
- 架构文档:`docs/prd/chat-architecture-redesign.md`
@@ -64,29 +90,31 @@ const generalChat = useUnifiedChat({ mode: "general" });
```typescript
export function useProviderPool() {
const [credentials, setCredentials] = useState<Credential[]>([]);
const [loading, setLoading] = useState(false);
const refresh = async () => {
setLoading(true);
const list = await invoke<Credential[]>('list_credentials');
setCredentials(list);
setLoading(false);
};
const addCredential = async (provider: string, path: string) => {
await invoke('add_credential', { provider, filePath: path });
await refresh();
};
const removeCredential = async (id: string) => {
await invoke('remove_credential', { id });
await refresh();
};
useEffect(() => { refresh(); }, []);
return { credentials, loading, addCredential, removeCredential, refresh };
const [credentials, setCredentials] = useState<Credential[]>([]);
const [loading, setLoading] = useState(false);
const refresh = async () => {
setLoading(true);
const list = await invoke<Credential[]>("list_credentials");
setCredentials(list);
setLoading(false);
};
const addCredential = async (provider: string, path: string) => {
await invoke("add_credential", { provider, filePath: path });
await refresh();
};
const removeCredential = async (id: string) => {
await invoke("remove_credential", { id });
await refresh();
};
useEffect(() => {
refresh();
}, []);
return { credentials, loading, addCredential, removeCredential, refresh };
}
```
@@ -94,17 +122,19 @@ export function useProviderPool() {
```typescript
export function useFlowEvents() {
const [records, setRecords] = useState<FlowRecord[]>([]);
useEffect(() => {
const unlisten = listen<FlowEvent>('flow-event', (event) => {
setRecords(prev => [event.payload.data, ...prev].slice(0, 100));
});
return () => { unlisten.then(fn => fn()); };
}, []);
return { records };
const [records, setRecords] = useState<FlowRecord[]>([]);
useEffect(() => {
const unlisten = listen<FlowEvent>("flow-event", (event) => {
setRecords((prev) => [event.payload.data, ...prev].slice(0, 100));
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
return { records };
}
```
@@ -112,17 +142,19 @@ export function useFlowEvents() {
```typescript
export function useDeepLink() {
useEffect(() => {
const unlisten = listen<string>('deep-link', async (event) => {
const url = new URL(event.payload);
if (url.pathname === '/oauth/callback') {
await handleOAuthCallback(url.searchParams);
}
});
return () => { unlisten.then(fn => fn()); };
}, []);
useEffect(() => {
const unlisten = listen<string>("deep-link", async (event) => {
const url = new URL(event.payload);
if (url.pathname === "/oauth/callback") {
await handleOAuthCallback(url.searchParams);
}
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
}
```
@@ -138,15 +170,15 @@ export function useDeepLink() {
```typescript
// 返回对象,包含状态和操作
return {
// 状态
data,
loading,
error,
// 操作
refresh,
add,
remove,
// 状态
data,
loading,
error,
// 操作
refresh,
add,
remove,
};
```
+4 -1
View File
@@ -17,11 +17,14 @@ src-tauri/src/services/
├── usage_service.rs # 使用量统计
├── backup_service.rs # 备份服务
├── update_check_service.rs # 自动更新检查
└── general_chat/ # 通用对话服务
```
## 核心服务
> 注意:`general_chat/` 兼容壳已删除。
> 新功能与新治理都应直接落到 unified chat / `chat_*` 体系,不要重新引回旧入口。
> `ProviderPoolService::select_credential_with_fallback_legacy` 也已删除,凭证选择统一走现役 `select_credential_with_fallback`。
### ProviderPoolService
```rust