> 创建时间:2026-05-06
> 背景:设置模块已从 V2 重构为 V3,但大量页面仍访问旧 V2 `.items` 结构导致运行时 TypeError。
## V3 架构核心概念
| 旧 V2 概念 | 新 V3 概念 |
|---|---|
| `textProviders.items[]` (AiProviderProfile) | `capabilityBindings[]` (CapabilityBinding) + `credentials[]` (ProviderCredential) |
| `AiProviderProfile.id` (即 `profileId`) | `CapabilityBinding.id` (即新的 `profileId`) |
| `profile.providerKind` | `binding.providerKind` |
| `profile.baseUrl/apiKey/accessKeyId/secretAccessKey` | `credential.baseUrl/apiKey/accessKeyId/secretAccessKey` |
| `profile.name` | `credential.name` (或 `binding.id` 兜底) |
| `profileHasImageEndpointCreds(profile)` | V3 版:按 `binding.providerKind` + `credential` 做校验 |
| `effectiveImageReferenceCaps(profile)` | V3 版:接收 `providerKind` + `binding.extra` 做校验 |
| React dep `[appSettings.textProviders.items]` | `[appSettings]` |
## V3 核心数据结构
```typescript
interface AppSettingsV3 {
settingsVersion: 3;
credentials: ProviderCredential[]; // 服务商凭证主档
capabilityBindings: CapabilityBinding[]; // 能力绑定(含 providerKind)
defaults: CategoryDefaultsV3; // 各类默认绑定id
}
interface CapabilityBinding {
id: string;
capability: "text" | "speech" | "image" | "imageToImage" | "video" | "keyframe";
providerKind: string;
credentialId: string;
models: string;
extra?: Record<string, unknown>;
}
interface ProviderCredential {
id: string;
name: string;
vendor: string;
authMode: "apiKey" | "aksk" | "none";
baseUrl: string;
apiKey: string;
accessKeyId: string;
secretAccessKey: string;
extra?: Record<string, unknown>;
}
```
## 第一步:appSettings.ts 新增 V3 工具函数
### 1.1 已添加
- `resolveProviderKindFromBinding(s, bindingId)` — 从绑定 ID 解析 providerKind
- `resolveCredentialFromBinding(s, bindingId)` — 从绑定 ID 查找凭证
### 1.2 需新增
```typescript
/** V3 版:检查某绑定是否具备图像端点凭证(替代 profileHasImageEndpointCreds) */
function bindingHasImageEndpointCreds(s: AppSettingsV3, bindingId: string): boolean;
/** V3 版:检查某绑定是否具备图生图凭证(替代 profileHasImageToImageCreds) */
function bindingHasImageToImageCreds(s: AppSettingsV3, bindingId: string): boolean;
/** V3 版:查找某能力桶下所有 ComfyUI 绑定 ID 集合 */
function findComfyBindingIds(s: AppSettingsV3, capability: "image" | "imageToImage"): Set<string>;
/** V3 版:从 Comfy 绑定 + 工作流列表构造 ImageModelPick[] */
function buildComfyWorkflowPicksFromBindings(
s: AppSettingsV3,
workflows: ComfyWorkflow[],
profileIds: Set<string>,
): ImageModelPick[];
```
## 第二步:imageReferenceCaps.ts 改造
### 新增 V3 版函数
```typescript
/**
* V3 版:查询有效垫图能力(含 extra 覆盖)
* 替代 effectiveImageReferenceCaps(p: AiProviderProfile)
*/
export function effectiveImageReferenceCapsV3(
providerKind: string,
extra?: Record<string, unknown>,
): ImageReferenceCaps;
```
同时保留旧版签名(`@deprecated`)以维持 `sceneImageDisplay.ts` 等文件的向后兼容过渡。
## 第三步:逐文件替换(分类处理)
### 分类 A:简单 providerKind 解析(14 个文件)
**旧代码模式:**
```typescript
const profile = appSettings.textProviders.items.find(x => x.id === pick.profileId);
const pk = profile?.providerKind;
```
**替换为:**
```typescript
const pk = resolveProviderKindFromBinding(appSettings, pick.profileId);
```
**依赖数组:** `[x, appSettings.textProviders.items]` → `[x, appSettings]`
**涉及文件:**
| # | 文件 | 行号 | 能力类型 |
|---|------|------|---------|
| 1 | `ui/src/components/story-tab/StoryTab.tsx` | 134 | text |
| 2 | `ui/src/components/scene-tab/SceneImageGenerateModal.tsx` | 80 | text |
| 3 | `ui/src/components/storyboard/modals/StoryboardGenerateShotsModal.tsx` | 50 | text |
| 4 | `ui/src/features/character-tab/modals/ActorPortraitGenerateModal.tsx` | 69 | text |
| 5 | `ui/src/features/character-tab/modals/ActorSaveExtractModal.tsx` | 35 | text |
| 6 | `ui/src/features/role-tab/modals/GenerateRolePortraitModal.tsx` | 114 | text |
| 7 | `ui/src/features/role-tab/modals/RolePortraitModelPickerModal.tsx` | 117 | text |
| 8 | `ui/src/features/role-tab/modals/GeneratePromptModal.tsx` | 130 | text |
| 9 | `ui/src/components/scene-tab/modals/AutoGenerateScenesModal.tsx` | 138 | text |
| 10 | `ui/src/components/chapter-tab/modals/StoryRolesUpsertModal.tsx` | 163 | text |
| 11 | `ui/src/components/chapter-tab/modals/OptimizeChapterPromptModal.tsx` | 103 | text |
| 12 | `ui/src/components/chapter-tab/modals/ChapterBatchAllChaptersScenesModal.tsx` | 208 | text |
| 13 | `ui/src/features/role-tab/hooks/useRoleBatchOps.ts` | 99, 201, 328 | text ×2, image |
| 14 | `ui/src/features/role-tab/hooks/useRoleBatchAiModals.ts` | 80, 269, 299 | text ×2, image |
| 15 | `ui/src/features/role-tab/ai/useRolePortraitAiPicks.ts` | 142, 154, 172 | text, image, i2i |
### 分类 B:ComfyUI 工作流相关(4 个文件)
**旧代码模式:**
```typescript
const comfyProfileIds = new Set(
appSettings.imageProviders.items
.filter(p => p.providerKind === IMAGE_KIND_COMFYUI && profileHasImageEndpointCreds(p))
.map(p => p.id)
);
for (const p of appSettings.imageProviders.items) {
if (!comfyProfileIds.has(p.id)) continue;
const profileName = p.name;
...
}
```
**替换为:**
```typescript
const comfyProfileIds = findComfyBindingIds(appSettings, "image");
const comfyBindings = appSettings.capabilityBindings.filter(
b => b.providerKind === IMAGE_KIND_COMFYUI && bindingHasImageEndpointCreds(appSettings, b.id)
);
for (const b of comfyBindings) {
const cred = findCredentialById(appSettings, b.credentialId);
const profileName = cred?.name ?? b.id;
...
}
```
**涉及文件:**
| # | 文件 | 替换内容 |
|---|------|---------|
| 1 | `ui/src/hooks/useComfyWorkflowImagePicks.ts` | 全量重写:comfyProfileIdSet → V3 版本 |
| 2 | `ui/src/hooks/useImageT2iModelSurface.ts` | comfyProfileIds 计算 → V3 |
| 3 | `ui/src/hooks/useImageI2iModelSurface.ts` | comfyProfileIds 计算 → V3 |
| 4 | `ui/src/components/storyboard/hooks/useStoryboardModelPicks.ts` | comfyProfileIds + 工作流循环 → V3 |
### 分类 C:垫图能力查询(3 个文件)
**旧代码模式:**
```typescript
const profile = appSettings.imageProviders.items.find(i => i.id === pick.profileId);
if (profile && effectiveImageReferenceCaps(profile).supportsReferenceImages) return pick;
```
**替换为:**
```typescript
const binding = findBindingById(appSettings, pick.profileId);
if (binding && effectiveImageReferenceCapsV3(binding.providerKind, binding.extra).supportsReferenceImages) return pick;
```
**涉及文件:**
| # | 文件 | 行号 |
|---|------|------|
| 1 | `ui/src/domain/sceneImageDisplay.ts` | 9-18 (firstImageToImagePickSupportingPads) |
| 2 | `ui/src/features/character-tab/CharacterTabScreen.tsx` | 237 |
| 3 | `ui/src/components/scene-tab/SceneTabMainLegacyImpl.tsx` | 282, 292 |
### 分类 D:useShotWorkbenchCapabilities 专用
**涉及文件:** `ui/src/components/storyboard/modals/shot-workbench/hooks/useShotWorkbenchCapabilities.ts`
删除本地 `resolveProviderKind` 辅助函数,改用 `resolveProviderKindFromBinding`。
## 第四步:废弃函数清理
标记以下函数为 `@deprecated`(保留空壳仅供编译通过):
- `profileHasImageEndpointCreds` → 替代为 `bindingHasImageEndpointCreds`
- `profileHasImageToImageCreds` → 替代为 `bindingHasImageToImageCreds`
## 执行顺序
1. ✅ `appSettings.ts` 添加 `resolveProviderKindFromBinding` / `resolveCredentialFromBinding`
2. `appSettings.ts` 添加 V3 凭证检查函数 + Comfy 辅助函数
3. `imageReferenceCaps.ts` 添加 `effectiveImageReferenceCapsV3`
4. **分类 A** 文件批量替换(14 文件)
5. **分类 B** 文件重写 ComfyUI 逻辑(4 文件)
6. **分类 C** 文件替换垫图查询(3 文件)
7. **分类 D** 文件替换(1 文件)
8. 全量 ReadLints 检查
## 影响范围统计
| 类别 | 文件数 | 替换点数 |
|------|--------|---------|
| 分类 A(providerKind 解析) | 15 | ~24 |
| 分类 B(ComfyUI) | 4 | ~12 |
| 分类 C(垫图能力) | 3 | ~5 |
| 分类 D(workbench) | 1 | ~6 |
| 基础设施(appSettings + caps) | 2 | ~8 |
| **总计** | **~25** | **~55** |