From 777f61bb2c07f6aa38b0257b274f04f6f76e81b7 Mon Sep 17 00:00:00 2001 From: mingming Date: Mon, 25 May 2026 08:43:22 +0800 Subject: [PATCH 01/16] docs: add provider deleted display fix spec Co-Authored-By: Claude Opus 4.7 --- ...-25-provider-deleted-display-fix-design.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/specs/2026-05-25-provider-deleted-display-fix-design.md diff --git a/docs/specs/2026-05-25-provider-deleted-display-fix-design.md b/docs/specs/2026-05-25-provider-deleted-display-fix-design.md new file mode 100644 index 0000000..e842f75 --- /dev/null +++ b/docs/specs/2026-05-25-provider-deleted-display-fix-design.md @@ -0,0 +1,117 @@ +# Provider 删除后展示修复设计 + +## Status + +Approved + +## Date + +2026-05-25 + +## Owner + +mingming + +## Deciders + +mingming + +## Context + +当 Provider 被软删除后,Agent 编辑页和 Workflow LLM 节点编辑页展示的是裸数字 `providerId` 而非 provider 名称。根因有两层: + +1. **后端**:`@TableLogic` 自动过滤 `deleted=1` 记录,`AgentServiceImpl` 查不到已删除 provider 时跳过名称填充;`WorkflowServiceImpl` 从来不解析 provider 名称 +2. **前端**:`` 下拉选项来自 `getProviderList()`(自动排除已删除),找不到匹配选项时就显示裸 ID + +此外,从业务可用性角度,被引用的 Provider 不应允许删除。 + +## Decision + +采用**删除前校验(主力防线)+ 展示兜底(安全网)**的策略。 + +### 删除前校验 + +`ProviderServiceImpl.delete()` 增加两层检查: + +1. **Agent 引用**:查询 `ai_agent` 表 `default_provider_id = providerId AND deleted = 0`,有记录则抛 `PROVIDER_IN_USE` +2. **Workflow LLM 节点引用**:查询 `workflow_node` 表,对 `config` JSON 中 `providerId` 做匹配(`deleted=0` 的 workflow),有记录则抛 `PROVIDER_IN_USE_BY_WORKFLOW` + +级联删除 `ai_model` 和 `provider_model_config` 保持现有逻辑不变。 + +### 展示兜底 + +后端不绕过 `@TableLogic`,只在查不到 provider 时标记不可用: + +- `AgentResponse` 新增 `defaultProviderAvailable` 字段(`Boolean`,默认 `true`) +- `toBasicResponse()`:批量查 provider 名称,查不到的设 `defaultProviderAvailable=false`、`defaultProviderName=null` +- `toFullResponse()`:查不到 provider 时设 `defaultProviderAvailable=false`、`defaultProviderName=null` +- `WorkflowServiceImpl.toNodeDetail()`:解析 config JSON 中 `providerId`,查不到则注入 `providerAvailable: false` + +前端检查标记: + +- `AgentList.vue`:`defaultProviderAvailable === false` 时显示 `t('provider.unavailable')` +- `WorkflowEdit.vue`:LLM 节点 provider 下拉同理 +- i18n key:中文 `(不可用)` / 英文 `(Unavailable)` + +## Considered Options + +| 方案 | 描述 | 拒绝原因 | +|:---|:---|:---| +| A. 反范式化存储 | `ai_agent` 加 `default_provider_name` 列 | 需改表结构,provider 改名需同步 | +| B. 后端占位兜底 | Service 层查不到时直接填占位文本 | **采用** | +| C. 绕过 @TableLogic | 自定义 SQL 查全部记录 | 破坏 ORM 一致性,两套查询路径 | + +## Details + +### 后端变更清单 + +| 文件 | 变更 | +|:---|:---| +| `eify-common/.../error/ErrorCode.java` | 新增 `PROVIDER_IN_USE`、`PROVIDER_IN_USE_BY_WORKFLOW` | +| `eify-common/.../i18n/messages.properties` | 新增中文错误消息 | +| `eify-common/.../i18n/messages_en_US.properties` | 新增英文错误消息 | +| `eify-provider/.../service/impl/ProviderServiceImpl.java` | `delete()` 增加 Agent + Workflow 引用校验 | +| `eify-agent/.../dto/AgentResponse.java` | 新增 `defaultProviderAvailable` 字段 | +| `eify-agent/.../service/impl/AgentServiceImpl.java` | `toBasicResponse()` / `toFullResponse()` 标记 provider 可用性 | +| `eify-workflow/.../service/impl/WorkflowServiceImpl.java` | `toNodeDetail()` 解析 providerId,注入 `providerAvailable` | + +### 前端变更清单 + +| 文件 | 变更 | +|:---|:---| +| `eify-web/src/i18n/locales/zh-CN.json` | 新增 `provider.unavailable: "(不可用)"` | +| `eify-web/src/i18n/locales/en-US.json` | 新增 `provider.unavailable: "(Unavailable)"` | +| `eify-web/src/views/AgentList.vue` | provider 列检查 `defaultProviderAvailable` 显示占位文本 | +| `eify-web/src/views/WorkflowEdit.vue` | LLM 节点 provider 下拉处理不可用情况 | + +### 验证场景 + +#### 删除校验 + +| # | 场景 | 预期 | +|:---|:---|:---| +| 1 | 删除未被任何 Agent/Workflow 引用的 provider | 成功,级联删除 model 和 config | +| 2 | 删除被 1 个 Agent 引用的 provider | 阻止,返回 `PROVIDER_IN_USE` | +| 3 | 删除被多个 Agent 引用的 provider | 阻止,错误信息包含 Agent 数量 | +| 4 | 删除被 Workflow LLM 节点引用的 provider | 阻止,返回 `PROVIDER_IN_USE_BY_WORKFLOW` | +| 5 | 删除被 Agent 和 Workflow 同时引用的 provider | 阻止 | +| 6 | 删除被已软删除 Agent(`deleted=1`)引用的 provider | 成功 | + +#### 展示兜底 + +| # | 场景 | 预期 | +|:---|:---|:---| +| 7 | Agent 列表 — provider 正常存在 | 显示 provider 名称 | +| 8 | Agent 列表 — provider 已删除 | 显示 `(不可用)` / `(Unavailable)` | +| 9 | Agent 列表 — defaultProviderId 为 null | 显示 `—` | +| 10 | Agent 列表 — 混合(部分正常、部分已删) | 各自正确显示 | +| 11 | Agent 详情 — provider 已删除 | `defaultProviderName=null`, `defaultProviderAvailable=false` | +| 12 | Workflow LLM 节点 — provider 已删除 | config JSON 注入 `providerAvailable: false` | +| 13 | Agent 编辑页 — provider 已删除 | `` 显示 `(不可用)` | +| 14 | Workflow 编辑页 — provider 已删除 | LLM 节点 provider 下拉显示 `(不可用)` | + +## Consequences + +- **正面**:用户可以清楚看到 provider 不可用状态,不会困惑于裸数字 ID;被引用的 provider 不会被误删 +- **负面**:删除时多两次数据库查询(Agent + WorkflowNode 引用计数) +- **风险**:WorkflowNode config JSON 中 `providerId` 的查询依赖 JSON 函数(`JSON_EXTRACT`),大数据量时需关注索引;目前 workflow 数量有限,风险可控 From bea7fe873e8b744778b0d87726a482c7c4ba2dad Mon Sep 17 00:00:00 2001 From: mingming Date: Mon, 25 May 2026 08:49:04 +0800 Subject: [PATCH 02/16] docs: add provider deleted display fix implementation plan Co-Authored-By: Claude Opus 4.7 --- ...2026-05-25-provider-deleted-display-fix.md | 662 ++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 docs/plans/2026-05-25-provider-deleted-display-fix.md diff --git a/docs/plans/2026-05-25-provider-deleted-display-fix.md b/docs/plans/2026-05-25-provider-deleted-display-fix.md new file mode 100644 index 0000000..6d1e430 --- /dev/null +++ b/docs/plans/2026-05-25-provider-deleted-display-fix.md @@ -0,0 +1,662 @@ +# Provider 删除后展示修复 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 修复 Provider 软删除后 Agent 和 Workflow 编辑页显示裸 ID 的问题,同时增加删除前引用校验。 + +**Architecture:** 两层防线——删除前校验(ProviderServiceImpl.delete 中检查 Agent/Workflow 引用,阻止删除) + 展示兜底(后端标记 provider 不可用,前端通过 i18n 显示占位文本)。不绕过 @TableLogic,不新增数据库字段。 + +**Tech Stack:** Java 17, Spring Boot 3, MyBatis-Plus, Vue 3, vue-i18n, Element Plus + +--- + +### 文件结构 + +| 文件 | 角色 | 变更类型 | +|:---|:---|:---| +| `eify-common/.../error/ErrorCode.java` | 新增错误码 | 修改 | +| `eify-common/.../i18n/messages.properties` | 中文错误消息 | 修改 | +| `eify-common/.../i18n/messages_en_US.properties` | 英文错误消息 | 修改 | +| `eify-provider/.../mapper/ProviderMapper.java` | 自定义引用计数查询 | 修改 | +| `eify-provider/.../service/impl/ProviderServiceImpl.java` | 删除前校验 | 修改 | +| `eify-agent/.../dto/AgentResponse.java` | 新增 defaultProviderAvailable | 修改 | +| `eify-agent/.../service/impl/AgentServiceImpl.java` | 批量解析 provider 名称+标记可用性 | 修改 | +| `eify-workflow/.../service/impl/WorkflowServiceImpl.java` | LLM 节点注入 providerAvailable | 修改 | +| `eify-web/src/i18n/locales/zh-CN.json` | 前端中文翻译 | 修改 | +| `eify-web/src/i18n/locales/en-US.json` | 前端英文翻译 | 修改 | +| `eify-web/src/views/AgentList.vue` | 编辑对话框 provider 下拉兜底 | 修改 | +| `eify-web/src/views/WorkflowEdit.vue` | LLM 节点 provider 下拉兜底 | 修改 | +| `eify-web/src/api/agent.ts` | 新增 defaultProviderAvailable 类型 | 修改 | + +--- + +### Task 1: ErrorCode + i18n 消息 + +**Files:** +- Modify: `eify-common/src/main/java/com/eify/common/error/ErrorCode.java` +- Modify: `eify-common/src/main/resources/i18n/messages.properties` +- Modify: `eify-common/src/main/resources/i18n/messages_en_US.properties` + +- [ ] **Step 1: 在 ErrorCode 中新增两条错误码** + +在 `ErrorCode.java` 的 Provider 段(2000-2999),`MODEL_NOT_SUPPORTED` 之后添加: + +```java +PROVIDER_IN_USE(2007, "供应商已被 Agent 使用,无法删除"), +PROVIDER_IN_USE_BY_WORKFLOW(2008, "供应商已被工作流使用,无法删除"), +``` + +- [ ] **Step 2: 在 messages.properties 新增中文错误消息** + +在 Provider 段末尾添加: + +```properties +PROVIDER_IN_USE_zh=供应商已被 Agent 使用,无法删除 +PROVIDER_IN_USE_BY_WORKFLOW_zh=供应商已被工作流使用,无法删除 +``` + +- [ ] **Step 3: 在 messages_en_US.properties 新增英文错误消息** + +在 Provider 段末尾添加: + +```properties +PROVIDER_IN_USE_zh=Provider is in use by Agent(s) and cannot be deleted +PROVIDER_IN_USE_BY_WORKFLOW_zh=Provider is in use by Workflow(s) and cannot be deleted +``` + +- [ ] **Step 4: 编译验证** + +```bash +mvn compile -q -pl eify-common +``` + +- [ ] **Step 5: Commit** + +```bash +git add eify-common/src/main/java/com/eify/common/error/ErrorCode.java \ + eify-common/src/main/resources/i18n/messages.properties \ + eify-common/src/main/resources/i18n/messages_en_US.properties +git commit -m "feat: add PROVIDER_IN_USE error codes for provider delete validation" +``` + +--- + +### Task 2: ProviderMapper — 自定义引用计数查询 + +**Files:** +- Modify: `eify-provider/src/main/java/com/eify/provider/mapper/ProviderMapper.java` + +- [ ] **Step 1: 添加两个自定义查询方法** + +```java +package com.eify.provider.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.eify.provider.domain.entity.Provider; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface ProviderMapper extends BaseMapper { + + @Select("SELECT COUNT(*) FROM ai_agent WHERE default_provider_id = #{providerId} AND deleted = 0") + int countAgentReferences(@Param("providerId") Long providerId); + + @Select("SELECT COUNT(*) FROM ai_workflow_node n " + + "INNER JOIN ai_workflow w ON n.workflow_id = w.id " + + "WHERE n.type = 'llm' " + + "AND JSON_EXTRACT(n.config, '$.providerId') = #{providerId} " + + "AND n.deleted = 0 " + + "AND w.deleted = 0") + int countWorkflowLlmReferences(@Param("providerId") Long providerId); +} +``` + +- [ ] **Step 2: 编译验证** + +```bash +mvn compile -q -pl eify-provider -am +``` + +- [ ] **Step 3: Commit** + +```bash +git add eify-provider/src/main/java/com/eify/provider/mapper/ProviderMapper.java +git commit -m "feat: add reference count queries to ProviderMapper" +``` + +--- + +### Task 3: ProviderServiceImpl.delete() — 删除前校验 + +**Files:** +- Modify: `eify-provider/src/main/java/com/eify/provider/service/impl/ProviderServiceImpl.java:269-281` + +- [ ] **Step 1: 在 delete() 方法中添加引用校验** + +将 `delete()` 方法替换为: + +```java +@Override +@Transactional(rollbackFor = Exception.class) +@CacheEvict(value = "provider-cache", key = "#id") +public void delete(Long id) { + Provider existing = WorkspaceGuard.requireInWorkspace( + providerMapper.selectById(id), ErrorCode.NOT_FOUND); + + int agentRefs = providerMapper.countAgentReferences(id); + if (agentRefs > 0) { + throw new BusinessException(ErrorCode.PROVIDER_IN_USE); + } + + int workflowRefs = providerMapper.countWorkflowLlmReferences(id); + if (workflowRefs > 0) { + throw new BusinessException(ErrorCode.PROVIDER_IN_USE_BY_WORKFLOW); + } + + modelConfigMapper.delete(new LambdaQueryWrapper() + .eq(ModelConfig::getProviderId, id) + .eq(ModelConfig::getWorkspaceId, CurrentContext.getWorkspaceId())); + + providerMapper.deleteById(id); + log.info("删除供应商成功,id: {}, name: {}", id, existing.getName()); +} +``` + +- [ ] **Step 2: 编译验证** + +```bash +mvn compile -q -pl eify-provider -am +``` + +- [ ] **Step 3: Commit** + +```bash +git add eify-provider/src/main/java/com/eify/provider/service/impl/ProviderServiceImpl.java +git commit -m "feat: add Agent/Workflow reference check before provider deletion" +``` + +--- + +### Task 4: AgentResponse DTO — 新增 defaultProviderAvailable + +**Files:** +- Modify: `eify-agent/src/main/java/com/eify/agent/domain/dto/AgentResponse.java` + +- [ ] **Step 1: 添加字段** + +在 `defaultProviderType` 字段之后添加: + +```java +/** + * 默认供应商是否可用(false 表示已被删除或禁用) + */ +private Boolean defaultProviderAvailable; +``` + +- [ ] **Step 2: 编译验证** + +```bash +mvn compile -q -pl eify-agent -am +``` + +- [ ] **Step 3: Commit** + +```bash +git add eify-agent/src/main/java/com/eify/agent/domain/dto/AgentResponse.java +git commit -m "feat: add defaultProviderAvailable field to AgentResponse" +``` + +--- + +### Task 5: AgentServiceImpl.toBasicResponse() — 列表批量解析 provider 名称 + +**Files:** +- Modify: `eify-agent/src/main/java/com/eify/agent/service/impl/AgentServiceImpl.java` + - `list()` (line 80-102) + - `list(page, pageSize, name, enabled)` (line 104-133) + - `toBasicResponse()` (line 538-567) + +- [ ] **Step 1: 添加批量解析 provider 名称的方法** + +在 `AgentServiceImpl` 类中添加新方法(放在 `toBasicResponse` 附近): + +```java +/** + * 批量加载 Agent 的 Provider 名称和可用状态 + */ +private void batchLoadProviderNames(List agents) { + if (agents == null || agents.isEmpty()) return; + + Set providerIds = agents.stream() + .map(Agent::getDefaultProviderId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (providerIds.isEmpty()) return; + + List providers = providerMapper.selectBatchIds(providerIds); + Map providerMap = providers.stream() + .collect(Collectors.toMap(Provider::getId, p -> p)); + + for (Agent agent : agents) { + if (agent.getDefaultProviderId() != null) { + Provider p = providerMap.get(agent.getDefaultProviderId()); + if (p != null) { + agent.setTempProviderName(p.getName()); + agent.setTempProviderAvailable(true); + } else { + agent.setTempProviderAvailable(false); + } + } + } +} +``` + +Wait — Agent entity doesn't have `tempProviderName` / `tempProviderAvailable` fields. I should use a different approach. Let me add transient helper fields or restructure. + +Actually, the cleanest approach: modify `toBasicResponse()` to accept a `Map` parameter and resolve there. Keep it stateless. + +- [ ] **Step 1 (Revised): 修改 list 方法和 toBasicResponse** + +修改两个 `list()` 方法,在调用 `toBasicResponse` 前批量加载 provider: + +```java +@Override +public PageResult list(Integer page, Integer pageSize) { + // ... existing pagination setup ... + IPage result = agentMapper.selectPage(pageObj, wrapper); + + batchLoadKnowledgeIds(result.getRecords()); + batchLoadMcpToolIds(result.getRecords()); + + // 批量加载 Provider 名称 + Map providerMap = batchLoadProvidersForAgents(result.getRecords()); + + List responses = result.getRecords().stream() + .map(a -> toBasicResponse(a, providerMap)) + .collect(Collectors.toList()); + + return PageResult.of(responses, result.getTotal(), page, pageSize); +} +``` + +同样修改带筛选的 `list(page, pageSize, name, enabled)` 方法。 + +添加批量加载方法: + +```java +private Map batchLoadProvidersForAgents(List agents) { + Set providerIds = agents.stream() + .map(Agent::getDefaultProviderId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (providerIds.isEmpty()) return Collections.emptyMap(); + return providerMapper.selectBatchIds(providerIds).stream() + .collect(Collectors.toMap(Provider::getId, p -> p)); +} +``` + +然后修改 `toBasicResponse` 签名和实现: + +```java +private AgentResponse toBasicResponse(Agent agent, Map providerMap) { + Provider provider = providerMap.get(agent.getDefaultProviderId()); + + return AgentResponse.builder() + .id(agent.getId()) + .name(agent.getName()) + .description(agent.getDescription()) + .avatar(agent.getAvatar()) + .defaultProviderId(agent.getDefaultProviderId()) + .defaultProviderName(provider != null ? provider.getName() : null) + .defaultProviderType(provider != null ? provider.getType().toString() : null) + .defaultProviderAvailable(provider != null) + .defaultModel(agent.getDefaultModel()) + .systemPrompt(agent.getSystemPrompt()) + .userMessagePrefix(agent.getUserMessagePrefix()) + .welcomeMessage(agent.getWelcomeMessage()) + .temperature(agent.getTemperature()) + .maxTokens(agent.getMaxTokens()) + .topP(agent.getTopP()) + .frequencyPenalty(agent.getFrequencyPenalty()) + .presencePenalty(agent.getPresencePenalty()) + .maxHistoryRounds(agent.getMaxHistoryRounds()) + .streamEnabled(agent.getStreamEnabled()) + .agentConfig(agent.getAgentConfig()) + .enabled(agent.getEnabled()) + .createdAt(agent.getCreatedAt().toString()) + .updatedAt(agent.getUpdatedAt().toString()) + .creatorId(agent.getCreatorId()) + .knowledgeIds(agent.getKnowledgeIds()) + .mcpToolIds(agent.getMcpToolIds()) + .ragEnabled(agent.getRagEnabled()) + .ragTopK(agent.getRagTopK()) + .ragStrategy(agent.getRagStrategy()) + .build(); +} +``` + +- [ ] **Step 2: 编译验证** + +```bash +mvn compile -q -pl eify-agent -am +``` + +- [ ] **Step 3: Commit** + +```bash +git add eify-agent/src/main/java/com/eify/agent/service/impl/AgentServiceImpl.java +git commit -m "feat: batch-resolve provider names in Agent list response" +``` + +--- + +### Task 6: AgentServiceImpl.toFullResponse() — 标记 provider 可用性 + +**Files:** +- Modify: `eify-agent/src/main/java/com/eify/agent/service/impl/AgentServiceImpl.java:572-625` + +- [ ] **Step 1: 修改 toFullResponse 中 provider 处理逻辑** + +将 `toFullResponse()` 中 provider 相关部分(line 574, 615-622)替换为: + +```java +// 查询关联的 Provider(可能为 null,表示已被软删除) +Provider provider = providerMapper.selectById(agent.getDefaultProviderId()); + +// ... (middle section unchanged) ... + +// 添加 Provider 信息 +if (provider != null) { + builder.defaultProvider(AgentResponse.ProviderInfo.builder() + .id(provider.getId()) + .name(provider.getName()) + .type(provider.getType().toString()) + .baseUrl(provider.getBaseUrl()) + .build()) + .defaultProviderName(provider.getName()) + .defaultProviderType(provider.getType().toString()) + .defaultProviderAvailable(true); +} else { + builder.defaultProviderAvailable(false); +} +``` + +- [ ] **Step 2: 编译验证** + +```bash +mvn compile -q -pl eify-agent -am +``` + +- [ ] **Step 3: Commit** + +```bash +git add eify-agent/src/main/java/com/eify/agent/service/impl/AgentServiceImpl.java +git commit -m "feat: mark provider availability in Agent detail response" +``` + +--- + +### Task 7: WorkflowServiceImpl.toNodeDetail() — LLM 节点注入 providerAvailable + +**Files:** +- Modify: `eify-workflow/src/main/java/com/eify/workflow/service/impl/WorkflowServiceImpl.java:292-303` +- May need to add ProviderMapper import and field + +- [ ] **Step 1: 添加 ProviderMapper 依赖注入** + +在类顶部添加 import: + +```java +import com.eify.provider.domain.entity.Provider; +import com.eify.provider.mapper.ProviderMapper; +import tools.jackson.databind.node.ObjectNode; +``` + +在构造函数参数中添加 `ProviderMapper`(该类使用 `@RequiredArgsConstructor`,但为确保可见性,显式声明): + +```java +private final ProviderMapper providerMapper; +``` + +- [ ] **Step 2: 修改 toNodeDetail 方法** + +```java +private NodeDetail toNodeDetail(WorkflowNode n) { + JsonNode config = n.getConfig(); + + // 为 LLM 节点注入 provider 可用性信息 + if ("llm".equals(n.getType()) && config != null && config.has("providerId")) { + Long providerId = config.get("providerId").asLong(); + Provider provider = providerMapper.selectById(providerId); + ObjectNode mutableConfig = (ObjectNode) config; + mutableConfig.put("providerAvailable", provider != null); + if (provider != null) { + mutableConfig.put("providerName", provider.getName()); + } + } + + return NodeDetail.builder() + .id(n.getId()) + .workflowId(n.getWorkflowId()) + .nodeKey(n.getNodeKey()) + .type(n.getType()) + .name(n.getLabel()) + .positionX(n.getPositionX()) + .positionY(n.getPositionY()) + .config(config) + .build(); +} +``` + +- [ ] **Step 3: 编译验证** + +```bash +mvn compile -q -pl eify-workflow -am +``` + +- [ ] **Step 4: Commit** + +```bash +git add eify-workflow/src/main/java/com/eify/workflow/service/impl/WorkflowServiceImpl.java +git commit -m "feat: inject provider availability into workflow LLM node config" +``` + +--- + +### Task 8: 前端 i18n 翻译键 + +**Files:** +- Modify: `eify-web/src/i18n/locales/zh-CN.json` +- Modify: `eify-web/src/i18n/locales/en-US.json` + +- [ ] **Step 1: 在 provider 段添加翻译** + +在 `zh-CN.json` 的 `provider` 对象中添加(若没有则创建 `provider` 段): + +```json +"provider": { + "unavailable": "(不可用)", + "unsyncedHint": "暂无可用供应商,请先在「供应商管理」中添加供应商" +} +``` + +在 `en-US.json` 的 `provider` 对象中添加: + +```json +"provider": { + "unavailable": "(Unavailable)", + "unsyncedHint": "No providers available. Please add a provider first in Provider Management" +} +``` + +> 注意:`provider.unsyncedHint` 已在现有代码中使用,需确认 key 存在。如已存在则只需添加 `unavailable`。 + +- [ ] **Step 2: 验证 i18n key 一致性** + +```bash +cd eify-web && npx ts-node scripts/validate-i18n.ts && cd .. +``` + +- [ ] **Step 3: Commit** + +```bash +git add eify-web/src/i18n/locales/zh-CN.json eify-web/src/i18n/locales/en-US.json +git commit -m "feat: add provider.unavailable i18n key" +``` + +--- + +### Task 9: AgentList.vue — 编辑对话框 provider 下拉兜底 + +**Files:** +- Modify: `eify-web/src/views/AgentList.vue` +- Modify: `eify-web/src/api/agent.ts` + +- [ ] **Step 1: 在 agent.ts 的 AgentResponse 接口中添加字段** + +在 `AgentResponse` 接口中,`defaultProviderName` 之后添加: + +```typescript +defaultProviderAvailable?: boolean +``` + +- [ ] **Step 2: 修改 AgentList.vue 的 handleEdit 方法** + +在 `handleEdit` 方法中,加载完 providers 后,检查被编辑 agent 的 provider 是否可用。找到 `handleEdit` 方法(~line 1090),在 `loadProviders()` 返回的数据加载流程中加入检查。 + +当前 `handleEdit` 直接设置 `defaultProviderId: row.defaultProviderId`。需要在 `` 中为不可用的 provider 添加一个禁用的选项。 + +修改模板中 `` 的 `` 循环之后,添加一个条件选项: + +```vue + + + {{ provider.name }} + {{ provider.type }} + + + +``` + +在 `