diff --git a/.github/issues/002-provider-deleted-name-display.md b/.github/issues/002-provider-deleted-name-display.md new file mode 100644 index 0000000..81d1aa5 --- /dev/null +++ b/.github/issues/002-provider-deleted-name-display.md @@ -0,0 +1,147 @@ +# Deleted Provider Shows ID Instead of Name in Agent and Workflow Edit Pages + +## Description + +When a Provider is soft-deleted (`deleted=1`), the Agent edit page and Workflow LLM node edit page display the raw numeric `providerId` instead of the provider name. This happens because: + +1. **Backend**: `@TableLogic` on `BaseEntity.deleted` causes MyBatis-Plus to auto-append `AND deleted=0` to all queries. Both `AgentServiceImpl.toBasicResponse()` (list) and `toFullResponse()` (detail) fail to resolve the provider name for soft-deleted providers. `WorkflowServiceImpl.toNodeDetail()` never resolves provider names at all — raw JSON config is passed through. + +2. **Frontend**: Both edit pages load a live provider list via `getProviderList()` for `` dropdown options. The deleted provider is absent from this list, so the `` bound to the numeric `providerId` cannot find a matching option label and displays the raw ID number. + +**Seven affected code locations:** + +| # | Location | Issue | +|:---|:---|:---| +| 1 | `AgentServiceImpl.toBasicResponse():538` | Never resolves `defaultProviderName` — always null in list response | +| 2 | `AgentServiceImpl.toFullResponse():615` | Silently skips provider info when `providerMapper.selectById()` returns null for deleted provider | +| 3 | `WorkflowServiceImpl.toNodeDetail():292` | Passes raw JSON config through, no provider name resolution | +| 4 | `AgentList.vue` `` :188-198 | Dropdown shows raw ID when provider not in options list | +| 5 | `WorkflowEdit.vue` `` :176-178 | Same issue for LLM node config drawer | +| 6 | `ProviderServiceImpl.list()` | Excludes soft-deleted providers due to `@TableLogic` | +| 7 | `AgentResponse` DTO | Has `defaultProviderName` field but backend never populates it for deleted providers | + +## Steps to Reproduce + +### Scenario: Agent edit page shows raw provider ID + +1. Create a Provider (e.g., "OpenAI", id=5) +2. Create an Agent and set its `defaultProviderId` to 5 +3. Delete the Provider (soft-delete, `deleted=1`) +4. Navigate to Agent list page → see the agent row +5. Click "Edit" on the agent +6. The Provider dropdown displays "5" (raw ID) instead of "OpenAI" + +### Scenario: Workflow LLM node edit shows raw provider ID + +1. Create a Provider (e.g., "Claude", id=3) +2. Create a Workflow with an LLM node, configure it to use providerId=3 +3. Delete the Provider (soft-delete) +4. Navigate to Workflow edit page → open the LLM node config drawer +5. The Provider dropdown displays "3" (raw ID) instead of "Claude" + +## Expected Behavior + +- When a referenced provider is soft-deleted, the UI should still display the provider **name** with a visual indicator that it has been deleted (e.g., "OpenAI (deleted)" or "(Deleted Provider)" in muted text) +- The backend should resolve and return provider names even for soft-deleted records +- Workflow LLM node config should also resolve and display provider names + +## Impact Analysis + +| Call Chain | Risk | +|:---|:---| +| `AgentController.list()` → `AgentServiceImpl.list()` → `toBasicResponse()` | Agent list rows show raw providerId | +| `AgentController.getById()` → `AgentServiceImpl.getById()` → `toFullResponse()` | Agent detail shows no provider info at all | +| `WorkflowController.getById()` → `WorkflowServiceImpl.getById()` → `toNodeDetail()` | LLM node config carries unresolved providerId | +| `ProviderController.list()` → `ProviderServiceImpl.list()` | Excludes deleted providers, frontend dropdown has no matching option | + +## Suggested Fix + +### Backend: Resolve provider name including soft-deleted records + +**Option A — Use custom mapper query that ignores `@TableLogic`:** + +```java +// ProviderMapper.java — add a method that includes deleted records +@Select("SELECT id, name, type, base_url, deleted FROM ai_provider WHERE id = #{id}") +Provider selectByIdIncludeDeleted(@Param("id") Long id); +``` + +**Option B — Use `selectList` with `@TableLogic` override (MyBatis-Plus 3.5.1+):** + +Not reliably supported; prefer Option A. + +**AgentServiceImpl.toFullResponse():** +```java +// After fix +Provider provider = providerMapper.selectByIdIncludeDeleted(agent.getDefaultProviderId()); +if (provider != null) { + response.setDefaultProvider(ProviderBasic.builder() + .id(provider.getId()) + .name(provider.getName()) + .type(provider.getType()) + .baseUrl(provider.getBaseUrl()) + .deleted(provider.getDeleted()) // so frontend knows it's deleted + .build()); + response.setDefaultProviderName(provider.getName()); +} +``` + +**AgentServiceImpl.toBasicResponse():** +```java +// After fix — batch-resolve provider names for all agents in list +Set providerIds = agents.stream() + .map(Agent::getDefaultProviderId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); +Map providerMap = providerMapper.selectByIdsIncludeDeleted(providerIds) + .stream().collect(Collectors.toMap(Provider::getId, Function.identity())); +// In the response builder: +agentResponse.setDefaultProviderName( + Optional.ofNullable(providerMap.get(agent.getDefaultProviderId())) + .map(Provider::getName).orElse(null)); +``` + +**WorkflowServiceImpl.toNodeDetail():** +```java +// After fix — resolve providerId from config JSON +if (n.getConfig() != null && n.getConfig().has("providerId")) { + Long providerId = n.getConfig().get("providerId").asLong(); + Provider provider = providerMapper.selectByIdIncludeDeleted(providerId); + if (provider != null) { + ObjectNode config = (ObjectNode) n.getConfig(); + config.put("providerName", provider.getName()); + config.put("providerDeleted", provider.getDeleted() == 1); + } +} +``` + +### Frontend: Display deleted indicator + +```vue + + + +``` + +## Environment + +| Info | Detail | +|:---|:---| +| **Deployment** | Docker / Local Dev | +| **Affected Version** | 1.0.0-SNAPSHOT (current main branch) | +| **Database** | MySQL 8.x | +| **Discovery Method** | Manual testing — delete provider while referenced by Agent/Workflow | + +## Additional Context + +- The `@TableLogic` annotation in `BaseEntity.java` (line 48) auto-filters `deleted=1` rows from ALL MyBatis-Plus queries, making soft-deleted providers invisible to standard mapper methods +- Referential integrity for soft-deleted references is not currently enforced — Agent and Workflow LLM nodes only store numeric provider IDs with no cascade or constraint +- Similar issue may exist for other soft-deleted entities referenced by Agent (e.g., Knowledge base, MCP Server) — should be verified as follow-up +- The batch-resolution approach in `toBasicResponse()` avoids N+1 queries by collecting all provider IDs first and doing a single `SELECT ... WHERE id IN (...)` query 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 }} + + + +``` + +在 `