拆分顺序:依赖基线 → 数据模型 → 适配器底层 → 调用编排 → 供应商入口 → 前端 → 验证 每个任务为可独立审查的原子变更;执行时遵循 Red/Green TDD,并在每个 Task 后更新
execution-log.md。
spec.md 已完成 HARD-GATE,确认人和时间已回填;master,且保留用户已有工作区改动;1.1.2.3 release 产物,或开发机已临时安装相同 release;spec.md、tasks.md、test-spec.md、execution-log.md 和 code-copilot/rules/automated-testing-standard.md;1.1.2 与 Alibaba/Extensions 1.1.2.3,只引入 DashScope 核心模型模块。forge-server/pom.xml — 更新 Alibaba 版本、增加 Extensions 版本并导入三个 BOM;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/pom.xml — 移除 Spring AI 依赖显式版本,增加 spring-ai-alibaba-dashscope;code-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/execution-log.md — 记录依赖树证据。xml
<spring-ai.version>1.1.2</spring-ai.version>
<spring-ai-alibaba.version>1.1.2.3</spring-ai-alibaba.version>
<spring-ai-alibaba-extensions.version>1.1.2.3</spring-ai-alibaba-extensions.version>
dependency:tree 并保存变更前 Spring AI 依赖基线;dependencyManagement 依次导入 spring-ai-bom、spring-ai-alibaba-extensions-bom、spring-ai-alibaba-bom;com.alibaba.cloud.ai:spring-ai-alibaba-dashscope,确认没有 spring-ai-alibaba-starter-dashscope;dependency:tree,确认 org.springframework.ai:* 统一为 1.1.2、com.alibaba.cloud.ai:* 统一为 1.1.2.3;3.5.13,classpath 不包含 DashScope 自动配置 Starter。forge-server/db/migration/V1.0.17__add_ai_provider_adapter_code.sql — 新增字段、回填和字典;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/domain/AiProvider.java — 新增 adapterCode;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderAdapterCode.java — 稳定适配器代码常量/枚举。关键签名:
public enum AiProviderAdapterCode {
OPENAI_COMPATIBLE("openai_compatible"),
DASHSCOPE_NATIVE("dashscope_native");
public String getCode();
public static AiProviderAdapterCode require(String code);
}
实施步骤:
AiProviderAdapterCodeTest,验证两个合法值以及 null/blank/unknown 失败关闭;varchar(32) NOT NULL DEFAULT 'openai_compatible';adapter_code IS NULL OR TRIM(adapter_code)='',不得覆盖任何非空值;NOT EXISTS 写入 ai_provider_adapter_type 及两条 tenant_id=1 字典数据;adapterCode,默认值只在服务创建链路和数据库中定义,不依赖前端;验收标准:首次迁移的旧记录均为 Compatible;脚本重跑不覆盖预置的 dashscope_native;新字典可由 useDict 获取;未知代码抛业务异常。
forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/AiModelRuntimeOptions.java — 通用 Chat 运行参数;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderAdapter.java — Adapter SPI;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderAdapterRegistry.java — 注册表和唯一性校验;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderBaseUrlPolicy.java — URI 归一化与 Compatible/Native 双向校验;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/OpenAiCompatibleProviderAdapter.java — 现有模型实现迁移;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/adapter/DashScopeNativeProviderAdapter.java — 原生 DashScope 模型实现。public interface AiProviderAdapter {
String adapterCode();
void validate(AiProvider provider, AiModelRuntimeOptions options);
ChatModel createChatModel(AiProvider provider, AiModelRuntimeOptions options);
}
public final class AiProviderAdapterRegistry {
public AiProviderAdapterRegistry(List<AiProviderAdapter> adapters);
public AiProviderAdapter getRequired(String adapterCode);
public ChatModel createChatModel(AiProvider provider, AiModelRuntimeOptions options);
}
public final class AiProviderBaseUrlPolicy {
public static String normalizeAndValidate(String adapterCode, String baseUrl);
}
- **实施步骤**:
- [x] 先写 Registry Red 测试:合法路由、未知值、空值、重复 code,以及 `createChatModel` 的选择→校验→创建顺序;
- [x] `AiProviderAdapterRegistry#createChatModel` 统一执行 `getRequired → validate → adapter.createChatModel`;校验失败时不得调用创建方法;
- [x] 正式调用、连接测试和 Cache 只依赖 Registry 创建入口,不直接注入具体 Adapter;
- [x] 实现 Registry,错误使用 `BusinessException` 且不包含 API Key;
- [x] 先写 Base URL Policy Red 测试,覆盖 scheme/query/fragment/userInfo、尾斜杠、官方 DashScope 双向错配和自定义代理域名;
- [x] Compatible 在官方 DashScope 域名只接受 `/compatible-mode`;Native 只接受根路径或空值默认;自定义域名只校验不改写;
- [x] 先写 OpenAI Adapter Red 测试,覆盖模型/温度/最大 Token 和 Base URL 必填;
- [x] 将当前 `OpenAiApi/OpenAiChatModel` 构造迁入 OpenAI Adapter;
- [x] 先写 DashScope Adapter Red 测试,覆盖默认 Base URL、原生 URL 校验和 `maxToken` 映射;
- [x] 使用 `DashScopeApi.builder().apiKey().baseUrl().build()` 与 `DashScopeChatModel.builder()` 实现原生 Adapter;
- [x] 验证返回模型的默认 Options 为 `DashScopeChatOptions`,且实现 `ToolCallingChatOptions`。
- **验收标准**:新增供应商只需实现 SPI 并注册 Bean;调用层不需要新增 `if/else`;错误适配器不产生网络请求。
## Task 4: 解耦 AiClient 与 ChatClientCache
- **目标**:统一调用链只依赖通用运行参数和 Adapter Registry,保持 `AiClient` 对外协议不变。
- **涉及文件**:
- `forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/client/AiClientImpl.java` — 用 `AiModelRuntimeOptions` 替换 `OpenAiChatOptions`;
- `forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/client/ChatClientCache.java` — 委托 Registry 创建 `ChatModel`,强化租户缓存键;
- `forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/client/AiInvocationResolver.java` — 保留选择优先级,将供应商特定校验下沉 Adapter;
- `forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/support/AiProviderCacheEvictionScheduler.java` — 事务提交后缓存失效调度;
- `forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/client/AiInvocationResolverTest.java` — 保留并扩展现有回归用例;
- `forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/client/ChatClientCacheTest.java` — 新增缓存复用与失效测试。
- **关键签名**:
```java
public ChatClient getOrCreateBase(
AiProvider provider,
AiModelRuntimeOptions options
);
public void evictByProvider(Long tenantId, Long providerId);
public void scheduleAfterCommit(AiProvider provider);
getOrCreateBase 从 provider.getTenantId() 构造缓存键,不接受独立 tenantId 参数;缓存键只含 tenantId、providerId、adapterCode、model、temperature、maxTokens,不含明文密钥;TransactionSynchronizationManager 注册 after-commit 回调,Scheduler 从 AiProvider 派生 tenantId/providerId;实际事务存在但同步未启用时失败关闭,禁止提前清理;AiClientImpl.call/stream 共用同一 AiModelRuntimeOptions 构建方法;AiClientImpl、ChatClientCache 中所有 org.springframework.ai.openai.* import;MessageChatMemoryAdvisor、同步输出、流式输出和会话持久化逻辑不变;AiClient 接口零变更;OpenAI Compatible 回归通过;DashScope 模型可以进入相同 ChatClient/Memory/Stream 链路。forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/dto/AiProviderSaveDTO.java — 新增/修改请求;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/dto/AiProviderTestDTO.java — ID 或未保存完整配置测试请求;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/vo/AiProviderVO.java — 安全响应对象;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/support/AiProviderSecretMasker.java — 脱敏和同值判断;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/service/AiProviderService.java — 生命周期、Adapter 测试、密钥保留、缓存失效;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/controller/AiProviderController.java — DTO/VO 协议和模板调整。public void createProvider(AiProviderSaveDTO request); public void updateProvider(AiProviderSaveDTO request); public AiProviderVO toSafeView(AiProvider provider); public String testConnection(AiProviderTestDTO request); public void deleteProvider(Long id);
- **实施步骤**:
- [x] 先写 SecretMasker Red 测试:长密钥、短密钥、空值、未变化脱敏值和新明文;
- [x] 先写 Service Red 测试:更新脱敏值保留原密钥、更新明文替换、测试 ID 加载真实密钥、one-of 混合请求拒绝、SDK 敏感异常安全化、持久化成功后调度缓存失效;
- [x] 页面/详情改为 `AiProviderVO`,禁止实体原样返回;
- [x] DTO 的 Adapter 缺失/null 时:新增默认为 `openai_compatible`、更新保留持久化值;显式空字符串/纯空白一律拒绝;
- [x] 更新前加载当前租户实体并执行脱敏回写保护;
- [x] `AiProviderTestDTO` 执行严格 one-of:有 ID 时拒绝任何配置字段,无 ID 时要求 Adapter 所需完整配置,禁止数据库配置与请求字段合并;
- [x] 连接测试通过 Registry 创建 `ChatModel`,固定低 Token Prompt;SDK 异常响应使用固定安全文案,日志仅含 providerId、adapterCode、异常类型;
- [x] 新增、更新和测试接口保持 `@ApiDecrypt/@ApiEncrypt`;
- [x] 修改、删除持久化成功后调用 `AiProviderCacheEvictionScheduler.scheduleAfterCommit(provider)`;失败或回滚不得清除缓存;
- [x] `/templates` 为 DashScope Native 返回 `adapterCode=dashscope_native` 和原生 Base URL,同时保留 Compatible 选择能力。
- **验收标准**:浏览器无法获得真实 API Key;保存未改密钥不会覆盖;连接测试不依赖客户端回传已保存密钥;更新后下一次调用使用新配置。
## Task 6: 改造当前供应商管理页面
- **目标**:让管理员明确选择连接协议,并以安全方式测试已保存供应商。
- **涉及文件**:
- `forge-admin-ui/src/views/ai/provider-model.vue` — 字典、表单、表格、默认值、URL 联动和测试参数;
- `forge-admin-ui/src/api/ai.js` — 补充测试请求语义注释,接口路径保持不变。
- **关键状态**:
```js
const { dict } = useDict(
'ai_provider_type',
'ai_provider_adapter_type',
'ai_model_type',
'ai_status',
'ai_is_default',
)
const providerModal = reactive({
form: {
adapterCode: 'openai_compatible',
},
})
openai_compatible,后端仍提供相同默认兜底;dashscope_native 时,仅在 Base URL 为空或为系统已知 Compatible 默认值时替换为 https://dashscope.aliyuncs.com,不得覆盖用户自定义域名;{ id: row.id };providerAdd/providerTest 使用 postEncrypt;providerUpdate 使用带 encrypt: true 的 PUT 请求,确保敏感配置不会明文发送;provider.vue,并确认后端“新增 null 则 Compatible、更新 null 则保留原值、blank 拒绝”的规则保证旧请求兼容。forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderAdapterCodeTest.java — Adapter Code 单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderAdapterRegistryTest.java — Registry 单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/adapter/AiProviderBaseUrlPolicyTest.java — URL Policy 单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/adapter/OpenAiCompatibleProviderAdapterTest.java — Compatible Adapter 单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/adapter/DashScopeNativeProviderAdapterTest.java — DashScope Adapter 单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/client/ChatClientCacheTest.java — 缓存单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/client/AiClientImplTest.java — Native 供应商同步、流式和 reasoningContent 离线回归;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/support/AiProviderSecretMaskerTest.java — 密钥脱敏单测;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/support/AiProviderCacheEvictionSchedulerTest.java — after-commit/rollback 缓存失效测试;forge-server/forge-framework/forge-plugin-parent/forge-plugin-ai/src/test/java/com/mdframe/forge/plugin/ai/provider/service/AiProviderServiceTest.java — 生命周期与密钥单测;code-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/test-spec.md — 回填本轮增量;code-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/execution-log.md — 记录真实命令和结果。AiClientImplTest,离线证明 Native 供应商的同步、流式与 reasoningContent 进入统一调用链;不得用“可选公网测试”替代;-Penable-tests test;forge-admin-server -am package -DskipTests,验证主应用装配;forge_schema_history 和回填结果;v20.19.0 执行前端 build;AI_DASHSCOPE_API_KEY 后验证一次同步和流式 qwen-plus,未提供时明确记录跳过原因;docs/Forge-AI中枢落地架构与阶段路线图.md — 回填阶段 0 的版本和 Adapter 结论;docs/Forge-AI中枢战略与技术选型方案.md — 明确 Alibaba 是 Spring AI 增强层而非替换;code-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/spec.md — 更新执行日志和审查结论;code-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/execution-log.md — 记录收尾验证;code-copilot/memory/decisions.md 或 pitfalls.md — 仅沉淀经过实现验证的长期决策/踩坑。dashscope_native 时必须先切回 Compatible URL 并通过连接测试,才能回退旧应用;pitfalls.md;git diff --check、链接/路径检查和 Spec-Task-Test 状态一致性检查;/review,未通过审查前不归档。ai_model 中启用且默认的模型,并安全记录供应商状态码/错误码。forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/model/mapper/AiModelMapper.java;forge-plugin-ai/src/main/resources/mapper/AiModelMapper.xml;forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/model/service/AiModelService.java;forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/client/AiInvocationResolver.java;forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/service/AiProviderService.java;forge-plugin-ai/src/main/java/com/mdframe/forge/plugin/ai/provider/support/AiProviderFailureDiagnostics.java;AiModelServiceTest、AiInvocationResolverTest、AiProviderServiceTest、AiProviderFailureDiagnosticsTest。ai_provider.default_model 并读取权威模型;ChatModel;status/is_default/del_flag;AiInvocationResolver 保留显式模型和 Agent 模型优先级,默认分支改用 AiModelService;AiProviderService 与 AiInvocationResolver 中的 gpt-3.5-turbo;httpStatus/errorCode 的诊断工具,日志禁止输出原始异常 message;rg 确认生产代码无通用 gpt-3.5-turbo。forge-admin-ui/src/views/ai/provider-model.vue。pageSize/itemCount/show-size-picker,查询统一为显式提交;itemCount;accept、两类弹窗宽度和 API Key 编辑提示;aria-label 和 reduced-motion。v20.19.0 运行目标 Vue 文件 ESLint 与前端生产构建;xmllint、git diff --check、敏感内容和 gpt-3.5-turbo 扫描;execution-log.md;decisions.md 第 16 条和 pitfalls.md 第 104 条;preferences.md 第 10 条;done,归档到 code-copilot/changes/archive/2026-07-11-spring-ai-alibaba-provider-adapter/。