diff --git "a/DOC/CODEX_DOC/02_\350\256\276\350\256\241\350\257\264\346\230\216/P3_\350\275\257\344\273\266\350\256\276\350\256\241\347\263\273\347\273\237/P3-\350\275\257\344\273\266\350\256\276\350\256\241\347\263\273\347\273\237\350\256\276\350\256\241-260629-\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\350\241\245\345\205\205\346\241\210.md" "b/DOC/CODEX_DOC/02_\350\256\276\350\256\241\350\257\264\346\230\216/P3_\350\275\257\344\273\266\350\256\276\350\256\241\347\263\273\347\273\237/P3-\350\275\257\344\273\266\350\256\276\350\256\241\347\263\273\347\273\237\350\256\276\350\256\241-260629-\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\350\241\245\345\205\205\346\241\210.md" new file mode 100644 index 0000000..3ff41e4 --- /dev/null +++ "b/DOC/CODEX_DOC/02_\350\256\276\350\256\241\350\257\264\346\230\216/P3_\350\275\257\344\273\266\350\256\276\350\256\241\347\263\273\347\273\237/P3-\350\275\257\344\273\266\350\256\276\350\256\241\347\263\273\347\273\237\350\256\276\350\256\241-260629-\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\350\241\245\345\205\205\346\241\210.md" @@ -0,0 +1,314 @@ +# P3 软件设计系统设计补充:转换器可用性增强 + +**日期:** 2026-06-29 +**所属系统:** P3 软件设计系统 +**对应基础设计:** `DOC/CODEX_DOC/02_设计说明/P3_软件设计系统/P3-软件设计系统设计-260515-1715-需规转软设基础转换补充案.md` +**依赖转换器设计:** `DOC/CODEX_DOC/02_设计说明/P3_软件设计系统/P3-软件设计系统设计-260518-0041-插件式转换器落地补充案.md` + +## 1. 设计目标 + +本文补充 P3 软件设计系统中插件式转换器的可用性增强设计。目标是在 P3 Design Lab 执行 `需规 -> 软设文档` 基础转换前,提前暴露默认转换器的外部配置状态,避免用户在缺少 Dify API Key 的环境中点击执行后才得到失败结果。 + +本文不把转换器可用性设计为新的业务事实源。P3 的权威业务事实仍归属输入包、软设会话、软件设计说明草稿、设计基线、工单投影、评审与冻结等对象。转换器 readiness 只表达当前运行环境是否满足转换器执行前置条件。 + +## 2. 当前问题 + +P3 插件式转换器已经支持通过默认 Dify workflow 生成软件设计说明草稿。当前默认转换器为: + +```text +requirement-to-sdd-dify-workflow +``` + +该转换器运行前依赖环境变量提供 Dify API Key: + +```text +CODEFACTORY_P3_DIFY_API_KEY +或兼容配置 DIFY_API_KEY +``` + +原有设计存在以下问题: + +1. 转换器发现接口只描述转换器能力,不描述当前环境是否满足运行条件。 +2. P3 Design Lab 可以展示“执行基础转换”入口,但无法提前提示缺少 Dify API Key。 +3. 用户只有点击执行后,才在转换失败状态中看到配置缺失原因。 +4. 前端缺少结构化状态,只能通过 conversion 失败结果反推转换器不可用。 +5. 后端虽然能够在调用 Dify 时失败,但缺少独立的配置前置检查语义。 + +## 3. 总体方案 + +首版采用“转换器 readiness 模型 + 前端执行入口约束 + 后端强制阻断”的方式: + +```mermaid +flowchart TB + UI["P3DesignLabPage"] + Inspector["StageRelationInspector"] + API["/api/software-design-v2"] + Service["SoftwareDesignV2Service"] + Registry["Converter Registry"] + Dify["Dify Workflow Adapter"] + Env["Runtime Environment"] + + UI --> Inspector + Inspector --> API + API --> Service + Service --> Registry + Registry --> Env + Service --> Dify + Dify --> Env +``` + +P3 服务负责计算转换器 readiness、写入会话转换状态、执行前再次校验 readiness,并把缺配置场景记录为明确的 conversion 失败。前端负责展示 readiness,并在缺配置时禁用“执行基础转换”入口。 + +## 4. Readiness 模型 + +首版在转换器描述中增加 `readiness` 字段。该字段只表达当前环境下转换器是否可执行,不表达转换结果,也不替代服务健康检查。 + +建议结构: + +```json +{ + "ready": false, + "status": "missing_configuration", + "message": "DIFY_API_KEY is not configured for requirement-to-sdd-dify-workflow", + "required_config_keys": [ + "CODEFACTORY_P3_DIFY_API_KEY", + "DIFY_API_KEY" + ], + "missing_config_keys": [ + "CODEFACTORY_P3_DIFY_API_KEY", + "DIFY_API_KEY" + ], + "configured": { + "dify_api_key": false + }, + "operator_hint": "请在本地或部署环境配置 CODEFACTORY_P3_DIFY_API_KEY,或兼容配置 DIFY_API_KEY。" +} +``` + +字段说明: + +| 字段 | 含义 | +| --- | --- | +| `ready` | 当前转换器是否满足执行前置条件 | +| `status` | 当前状态,首版使用 `ready`、`missing_configuration` | +| `message` | 面向接口调用方和调试人员的状态说明 | +| `required_config_keys` | 该转换器需要检查的配置项名称 | +| `missing_config_keys` | 当前环境缺失的配置项名称 | +| `configured` | 配置状态摘要,不包含真实密钥 | +| `operator_hint` | 面向本地部署或运维人员的处理提示 | + +readiness 不应包含真实 API Key、Dify 工作台登录信息、完整 Prompt 或内部敏感调用日志。 + +## 5. 服务改造设计 + +### 5.1 Converter Registry 和 Service + +P3 不在前端硬编码 Dify 配置状态,而是由后端统一计算 readiness。 + +建议职责划分: + +| 模块 | 职责 | +| --- | --- | +| `software_design_v2.service` | 生成转换器描述、创建会话、执行转换、记录失败 | +| Dify converter adapter | 执行真实 Dify workflow 调用 | +| 运行环境配置 | 提供 `CODEFACTORY_P3_DIFY_API_KEY` 或 `DIFY_API_KEY` | + +### 5.2 转换器发现 + +`list_converters()` 返回转换器基础信息时同步返回 readiness: + +```text +list_converters() + -> load registered converters + -> inspect runtime configuration + -> append readiness to each converter + -> return converters +``` + +对 Dify workflow 型转换器,readiness 至少检查 API Key 是否存在。非 Dify 型转换器可以返回 `ready=true` 且 `required_config_keys=[]`。 + +### 5.3 创建会话 + +创建 P3 Design Lab 会话时,应把默认转换器 readiness 写入 conversion 状态: + +```text +create_session(payload) + -> build P3DesignLabSession + -> resolve default converter + -> compute readiness + -> set conversion.converter.readiness + -> return session +``` + +这样页面拿到会话后,不需要额外推断转换器是否可执行。 + +### 5.4 运行转换 + +运行转换时后端必须再次检查 readiness。前端禁用按钮只是用户体验保护,不能作为唯一防线。 + +```text +run_conversion(session_id) + -> load session + -> resolve converter readiness + -> if not ready: record conversion_failed and return 400 + -> mark conversion_running + -> call Dify converter + -> write design_document / design_baseline / workorder_projection + -> mark draft_ready + -> return session +``` + +该流程保证调用方绕过前端直接请求 conversion 时,后端仍不会调用已知不可用的外部 workflow。 + +### 5.5 失败记录 + +缺配置导致的转换失败应写入会话转换状态: + +```text +conversion.status = conversion_failed +conversion.error = readiness.message +conversion.converter.readiness = current readiness +``` + +失败记录不应伪造软设草稿,也不应让会话进入 `draft_ready`。 + +### 5.6 成功转换后的状态保留 + +转换成功后仍应保留 converter 与 readiness 摘要,便于页面展示转换来源,也便于后续运行日志、排障和测试断言。 + +## 6. 前端展示设计 + +### 6.1 ViewModel + +前端 API 类型和工作台 ViewModel 需要承接后端 readiness: + +```text +P3DesignConverterReadiness +P3DesignConverter +StageConversionViewModel.converter.readiness +``` + +adapter 层负责把后端 snake_case 字段转换为前端 camelCase 字段,页面不直接拼装原始 API 对象。 + +### 6.2 转换控制区 + +P3 Design Lab 的阶段关系 Inspector 中,`需规文档 -> 软设文档` 关系已经有基础转换控制区。readiness 展示应放在该控制区内,与转换策略和执行按钮保持同一上下文。 + +缺少 Dify API Key 时: + +1. 展示“转换器未就绪”提示。 +2. 展示可读的配置缺失原因。 +3. 禁用“执行基础转换”按钮。 +4. 不发起 conversion POST 请求。 + +转换器已就绪时: + +1. 展示“转换器已就绪”提示。 +2. 保持原有执行入口。 +3. conversion 结果仍进入原有草稿、基线和投影展示链路。 + +### 6.3 页面动作边界 + +前端点击处理函数仍应保留二次 guard: + +```text +if converter.readiness.ready is false + -> do not call runConversion +``` + +该 guard 用于防止页面状态异步更新或按钮状态异常时发出无效请求。 + +## 7. API 契约设计 + +转换器发现接口应返回 readiness: + +```text +GET /api/software-design-v2/converters +``` + +P3 会话详情中的 conversion 状态应包含 converter readiness: + +```json +{ + "conversion": { + "status": "conversion_pending", + "converter": { + "converter_id": "requirement-to-sdd-dify-workflow", + "name": "P3 requirement to SDD Dify workflow", + "readiness": { + "ready": false, + "status": "missing_configuration" + } + } + } +} +``` + +执行 conversion 时,如果 readiness 不满足,应返回业务错误而不是调用 Dify: + +```text +POST /api/software-design-v2/sessions/{session_id}/conversion + -> 400 + -> session.conversion.status = conversion_failed +``` + +## 8. 与服务健康检查的关系 + +readiness 不并入 `/api/health`。 + +服务健康检查表达 API 进程是否可访问、基础依赖是否正常。转换器 readiness 表达某个可选转换能力在当前环境中是否具备执行前置条件。缺少 Dify API Key 时,API 服务仍可以健康,但 Dify workflow 型转换器不可执行。 + +## 9. 兼容策略 + +为了减少改造风险,首版采用增量字段和双重防线: + +1. `readiness` 是新增字段,不移除既有 converter 字段。 +2. 旧前端即使不读取 readiness,后端也会在 conversion 前阻断缺配置调用。 +3. 新前端读取 readiness 后,可以提前展示未就绪状态并禁用按钮。 +4. 已有 `conversion_failed` 状态继续复用,不新增独立业务状态。 +5. readiness 状态只记录配置摘要,不记录真实密钥值。 + +## 10. 错误处理 + +| 场景 | 后端行为 | +| --- | --- | +| 缺少 Dify API Key | 返回 400,记录 `conversion_failed`,不调用 Dify | +| Dify 网络失败 | 返回转换失败,记录远端调用错误摘要 | +| Dify 返回非 JSON | 返回结构错误,保留转换失败状态 | +| Dify 输出缺字段 | 返回输出校验错误 | +| 转换器不存在 | 返回不支持的转换器 | +| 前端绕过禁用状态直接请求 | 后端按 readiness 再次阻断 | + +## 11. 测试设计 + +后端测试: + +1. converters 接口返回默认转换器 readiness。 +2. 有 Dify API Key 时,默认转换器 readiness 为可执行。 +3. 缺少 Dify API Key 时,默认转换器 readiness 为 `missing_configuration`。 +4. 创建 P3 会话后,`conversion.converter.readiness` 可见。 +5. 缺少 Dify API Key 时执行 conversion 返回 400。 +6. 缺配置失败后 session 进入 `conversion_failed`。 + +前端测试: + +1. 页面拿到 missing configuration readiness 后展示“转换器未就绪”。 +2. 缺配置时“执行基础转换”按钮禁用。 +3. 缺配置时点击处理不发送 conversion 请求。 +4. readiness 可用时不影响原有转换控制区渲染。 + +## 12. 设计风险 + +| 风险 | 处理 | +| --- | --- | +| 前端把 readiness 当作唯一防线 | 后端 conversion 前必须再次检查 readiness | +| readiness 暴露敏感配置 | 只返回布尔值、配置项名称和提示,不返回真实密钥 | +| 服务健康与转换器可用性混淆 | readiness 不并入 `/api/health` | +| 不同转换器配置规则不同 | readiness 由转换器注册信息和 adapter 约定分别计算 | +| 缺配置失败被误认为业务转换失败 | 使用 `missing_configuration` 状态和明确错误文案 | + +## 13. 设计结论 + +P3 转换器可用性增强应以转换器 readiness 为核心模型,以转换器发现接口和 P3 会话 conversion 状态为承载位置,以前端提示和后端阻断形成双重保护。 + +该设计将“点击后才失败”的 Dify 配置问题前移为“执行前可见”的转换器状态,同时保持 P3 业务事实、Dify 外部配置和服务健康检查之间的边界。 diff --git "a/DOC/CODEX_DOC/06_\346\265\213\350\257\225\346\226\207\346\241\243/03_\346\234\272\346\265\213\350\256\260\345\275\225/2026-06\346\265\213\350\257\225/260629-P3-\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\351\252\214\350\257\201\350\256\260\345\275\225.md" "b/DOC/CODEX_DOC/06_\346\265\213\350\257\225\346\226\207\346\241\243/03_\346\234\272\346\265\213\350\256\260\345\275\225/2026-06\346\265\213\350\257\225/260629-P3-\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\351\252\214\350\257\201\350\256\260\345\275\225.md" new file mode 100644 index 0000000..25d427e --- /dev/null +++ "b/DOC/CODEX_DOC/06_\346\265\213\350\257\225\346\226\207\346\241\243/03_\346\234\272\346\265\213\350\256\260\345\275\225/2026-06\346\265\213\350\257\225/260629-P3-\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\351\252\214\350\257\201\350\256\260\345\275\225.md" @@ -0,0 +1,51 @@ +# 2026-06-29 P3 转换器可用性增强自测记录 + +## 1. 自测范围 + +本轮验证范围: + +- `P3` 设计转换器 readiness 模型。 +- `/api/software-design-v2/converters` 默认转换器可用性状态返回。 +- `software_design_v2` 新建 P3 会话时写入 `conversion.converter.readiness`。 +- 缺少 Dify API Key 时,后端 conversion 前置阻断并记录 `conversion_failed`。 +- P3 Design Lab 基础转换控制区展示“转换器未就绪”,并禁用 `执行基础转换`。 +- P3 相关后端、前端、工作台适配和设计形态模型回归。 + +本轮不验证真实 Dify workflow 联调,不写入真实 Dify API Key,也不覆盖 Dify 工作台创建、编辑或发布。页面实际展示效果待人工验收确认。 + +## 2. 自测命令与结果 + +| 命令 | 结果 | +| --- | --- | +| P2/P3 输入包、P3 转换器插件、阶段产物、工作区布局、Software Design V2 API 相关回归测试集 | `44 passed, 5 warnings in 3.21s` | +| `uv run pytest apps/api/tests/test_platform_exchange_p2_p3_api.py apps/api/tests/test_design_converter_plugins.py apps/api/tests/test_stage_artifacts_api.py apps/api/tests/test_workspace_layouts_api.py apps/api/tests/test_software_design_v2_api.py -q` | 通过;覆盖 converters readiness、session readiness、缺配置 conversion 400 和 `conversion_failed` | +| `pnpm --dir apps/web exec vitest run src/test/P3DesignLabPage.test.tsx src/test/DesignMorphCanvasPlatform.test.tsx src/test/P3DesignLabWorkbenchAdapter.test.ts src/test/P3DesignMorphModel.test.ts` | `4 passed test files`,`54 passed tests`;覆盖 readiness 提示、按钮禁用、不发送无效 conversion、工作台适配和设计形态模型 | +| `curl http://127.0.0.1:8120/api/health` | `{"status":"ok"}` | +| `curl http://127.0.0.1:8120/api/software-design-v2/converters` | 默认转换器返回 `readiness.ready=false`、`readiness.status=missing_configuration`、缺失 `CODEFACTORY_P3_DIFY_API_KEY` 和 `DIFY_API_KEY` | +| `curl http://127.0.0.1:8120/api/software-design-v2/input-packages` | 返回可用于 P3 Design Lab 的输入包,样例 `input_package_id=art-7dbe0b44835446b0`,`p3_consumable=true` | +| `curl -o /dev/null -w 'p3-design-lab=%{http_code}\n' http://127.0.0.1:5273/p3-design-lab` | `p3-design-lab=200` | +| 直接执行缺配置 P3 conversion | HTTP `400`,会话记录 `conversion.status=conversion_failed`,错误来源为 `design_converter` | +| `git diff --check` | 通过,无空白错误 | + +## 2.1 转换器可用性补充 + +原有 P3 Design Lab 转换控制区只展示 `执行基础转换` 操作,不展示默认 Dify 转换器的配置状态。当本地环境缺少 Dify API Key 时,用户点击后才进入失败路径。 + +本轮增加 readiness 后,状态暴露位置调整为: + +```text +/api/software-design-v2/converters +P3DesignLabSession.conversion.converter.readiness +StageConversionViewModel.converter.readiness +P3 Design Lab 转换控制区 +``` + +缺少 Dify API Key 时,页面提前展示“转换器未就绪”,并禁用 `执行基础转换`。如果调用方绕过前端直接请求 conversion,后端仍会返回 400,并把会话记录为 `conversion_failed`。 + +## 3. 回归观察 + +本轮没有重新执行 `uv run pytest apps/api/tests -q` 全量 API 测试。相关后端回归使用 P2/P3 输入包、P3 转换器、阶段产物、工作区布局和 Software Design V2 API 组合测试集;相关前端回归使用 P3 页面、工作台适配和设计形态模型测试集。 + +## 4. 结论 + +本轮 P3 转换器可用性增强已通过相关后端测试、前端测试和运行态接口验证。当前状态为:已自测,待人工验收。 diff --git "a/DOC/CODEX_DOC/07_\350\277\207\347\250\213\346\226\207\346\241\243/01_\344\274\232\350\257\235\344\272\244\346\216\245/2026-06-29-P3\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\344\272\244\344\273\230\350\257\264\346\230\216.md" "b/DOC/CODEX_DOC/07_\350\277\207\347\250\213\346\226\207\346\241\243/01_\344\274\232\350\257\235\344\272\244\346\216\245/2026-06-29-P3\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\344\272\244\344\273\230\350\257\264\346\230\216.md" new file mode 100644 index 0000000..1b98c41 --- /dev/null +++ "b/DOC/CODEX_DOC/07_\350\277\207\347\250\213\346\226\207\346\241\243/01_\344\274\232\350\257\235\344\272\244\346\216\245/2026-06-29-P3\350\275\254\346\215\242\345\231\250\345\217\257\347\224\250\346\200\247\345\242\236\345\274\272\344\272\244\344\273\230\350\257\264\346\230\216.md" @@ -0,0 +1,111 @@ +# 2026-06-29 P3 转换器可用性增强交接 + +## 1. 本轮目标 + +在 `codex/p3-converter-readiness-delivery` 分支上,为 P3 Design Lab 默认 Dify 转换器补充 readiness 状态。目标是把缺少 Dify API Key 时的失败路径从“用户点击执行后才暴露”前移为“进入转换前可见”,并保留后端执行前阻断。 + +## 2. 已完成内容 + +- 新增 P3 转换器可用性增强设计补充文档。 +- 新增 P3 转换器可用性增强自测记录。 +- `software_design_v2` 已在转换器发现接口返回 readiness。 +- 新建 P3 软设会话时,`conversion.converter.readiness` 随会话返回。 +- conversion 执行前增加 Dify Key 配置检查。 +- 缺少 Dify Key 时,后端返回 `400`,并记录 `conversion_failed`。 +- P3 Design Lab 已展示“转换器已就绪 / 转换器未就绪”。 +- 缺少 Dify Key 时,前端禁用 `执行基础转换`。 +- 前端点击处理函数保留二次 guard,避免异常状态下发出无效 conversion 请求。 +- 后端和前端测试已覆盖缺配置路径。 + +涉及代码: + +- `apps/api/app/software_design_v2/service.py` +- `apps/api/tests/test_software_design_v2_api.py` +- `apps/web/src/lib/api.ts` +- `apps/web/src/components/stageWorkbench/models.ts` +- `apps/web/src/pages/adapters/p3DesignLabWorkbenchAdapter.ts` +- `apps/web/src/pages/P3DesignLabPage.tsx` +- `apps/web/src/pages/P3DesignLabPage.css` +- `apps/web/src/test/P3DesignLabPage.test.tsx` + +涉及文档: + +- `DOC/CODEX_DOC/02_设计说明/P3_软件设计系统/P3-软件设计系统设计-260629-转换器可用性增强补充案.md` +- `DOC/CODEX_DOC/06_测试文档/03_机测记录/2026-06测试/260629-P3-转换器可用性增强验证记录.md` +- `DOC/CODEX_DOC/07_过程文档/01_会话交接/2026-06-29-P3转换器可用性增强交付说明.md` + +## 3. 关键边界 + +当前分支负责: + +- 转换器 readiness 计算。 +- converters 接口和 P3 会话状态暴露 readiness。 +- Dify Key 缺失时的前端提示和按钮禁用。 +- Dify Key 缺失时的后端 conversion 阻断。 +- 缺配置路径的后端、前端测试。 + +当前分支不负责: + +- 登录、编辑或发布 Dify 工作台 workflow。 +- 保存真实 Dify API Key。 +- 维护 Dify 内部节点状态。 +- 改变 P3 冻结、发布或 P4 投影门禁。 +- 重构 P3 插件式转换器协议。 +- 把 readiness 合并进 `/api/health`。 + +## 4. 验证情况 + +相关后端测试: + +```text +P2/P3 输入包、P3 转换器插件、阶段产物、工作区布局、Software Design V2 API 相关回归测试集 +``` + +结果: + +```text +44 passed, 5 warnings in 3.21s +``` + +相关前端测试: + +```text +P3 页面、工作台适配和设计形态模型测试集 +``` + +结果: + +```text +4 passed test files +54 passed tests +``` + +运行态验证: + +```text +/api/health -> {"status":"ok"} +/api/software-design-v2/converters -> readiness.ready=false when Dify Key unset +/p3-design-lab -> HTTP 200 +conversion -> 400 + conversion_failed when Dify Key unset +``` + +详见: + +```text +DOC/CODEX_DOC/06_测试文档/03_机测记录/2026-06测试/260629-P3-转换器可用性增强验证记录.md +``` + +## 5. 后续建议 + +1. 当前 readiness 展示更适合开发者、测试人员、本地部署人员和内部交付人员使用。该场景下,直接提示缺少 `CODEFACTORY_P3_DIFY_API_KEY` 或 `DIFY_API_KEY` 有助于快速定位环境配置问题。 +2. 如果 P3 Design Lab 后续面向外部普通用户开放,建议收敛前端可见文案,不直接展示环境变量名。普通用户侧可改为“转换服务暂不可用,请联系管理员或稍后重试”,具体缺失配置只保留在后端日志、管理员视图或调试接口中。 +3. 当前缺少 Dify API Key 时仍沿用原项目 `ValueError -> HTTP 400` 的处理机制。若进入产品化阶段,建议单独拆分转换器配置异常,例如 `ConverterConfigurationError` 或 `ConverterUnavailableError`,并将服务端配置缺失映射为 `503 Service Unavailable`。 +4. readiness 机制本身建议继续保留。即使未来用户侧文案脱敏,后端仍应在 converters 接口、P3 会话 conversion 状态和 conversion 执行入口保留同一套转换器可用性判断,避免只依赖前端按钮状态。 +5. 后续接入真实 Dify Key 后,应补充 `readiness.ready=true` 的真实联调记录,覆盖 converters 接口、P3 会话初始化、真实 conversion 执行、草稿生成和失败回滚路径。 +6. 若后续扩展多个转换器或非 Dify adapter,建议把 readiness 计算下沉到转换器注册信息或 adapter 元数据中,避免在 `software_design_v2` service 中硬编码不同转换器的配置规则。 +7. readiness 仍不建议并入 `/api/health`。API 进程健康和某个可选转换器是否满足外部配置,是两个不同层级的状态;后续如需统一观测,可另建管理员健康检查或转换器诊断接口。 +8. 真实 Dify API Key 仍不得写入仓库、测试记录或交接文档。测试和文档中只保留配置项名称、布尔状态和错误摘要。 + +## 6. 当前状态 + +已自测,待人工验收。 diff --git a/apps/api/app/software_design_v2/service.py b/apps/api/app/software_design_v2/service.py index 35c32e8..5484138 100644 --- a/apps/api/app/software_design_v2/service.py +++ b/apps/api/app/software_design_v2/service.py @@ -100,7 +100,7 @@ def list_input_packages(self) -> dict: def list_converters(self) -> dict: registry = get_design_converter_plugin_registry() - return {"items": [converter.to_api() for converter in registry.list_converters()]} + return {"items": [self._converter_to_api(converter) for converter in registry.list_converters()]} def create_session(self, payload: P3DesignSessionCreate) -> dict: input_package = self._get_input_package(payload.input_package_id) @@ -125,7 +125,13 @@ def create_session(self, payload: P3DesignSessionCreate) -> dict: "output_style": payload.generation_policy.get("output_style", "按标准软设正文写,不写聊天语气"), }, "status": "conversion_pending", - "conversion": self._build_conversion_state("conversion_pending", "standard_sdd_draft", None, None), + "conversion": self._build_conversion_state( + "conversion_pending", + "standard_sdd_draft", + None, + None, + converter=self._default_converter_api(), + ), "design_document": None, "design_baseline": None, "workorder_projection": None, @@ -171,14 +177,27 @@ def run_conversion(self, session_id: str, payload: P3DesignConversionRun) -> dic def _run_design_converter(self, design_session: dict, payload: P3DesignConversionRun, strategy: str) -> dict: registry = get_design_converter_plugin_registry() manifest = registry.require(payload.converter_id.strip() if payload.converter_id else registry.default_converter().converter_id) - design_session["conversion"] = self._build_conversion_state("conversion_running", strategy, None, None, converter=manifest.to_api()) + converter_api = self._converter_to_api(manifest) + readiness = dict(converter_api.get("readiness") or {}) + if readiness and readiness.get("ready") is False: + message = str(readiness.get("message") or "P3 design converter is not ready") + self._record_conversion_failure(design_session, strategy, converter_api, message) + raise ValueError(message) + + design_session["conversion"] = self._build_conversion_state( + "conversion_running", + strategy, + None, + None, + converter=converter_api, + ) request = self._build_converter_request(design_session, payload, strategy) adapter = load_design_converter_adapter(manifest) try: result = adapter.run(request) except ValueError as exc: - self._record_conversion_failure(design_session, strategy, manifest.to_api(), str(exc)) + self._record_conversion_failure(design_session, strategy, converter_api, str(exc)) raise design_document = self._normalize_converter_design_document(result, design_session) design_baseline = self._build_design_baseline_from_converter_result(result, design_session) @@ -193,7 +212,7 @@ def _run_design_converter(self, design_session: dict, payload: P3DesignConversio strategy, design_document, design_baseline, - converter=result.converter, + converter=self._merge_converter_readiness(result.converter, converter_api), process_output=result.process_output, ) design_session["updated_at"] = self._now() @@ -229,6 +248,62 @@ def _record_conversion_failure(self, design_session: dict, strategy: str, conver ] self._persist_design_session(design_session) + def _default_converter_api(self) -> dict | None: + try: + registry = get_design_converter_plugin_registry() + return self._converter_to_api(registry.default_converter()) + except ValueError: + return None + + def _converter_to_api(self, manifest) -> dict: + converter = manifest.to_api() + converter["readiness"] = self._build_converter_readiness(converter) + return converter + + def _merge_converter_readiness(self, converter: dict, fallback_converter: dict) -> dict: + normalized = dict(converter or {}) + fallback = dict(fallback_converter or {}) + if "readiness" not in normalized and fallback.get("readiness"): + normalized["readiness"] = fallback["readiness"] + if "requires" not in normalized and fallback.get("requires"): + normalized["requires"] = fallback["requires"] + if "name" not in normalized and fallback.get("name"): + normalized["name"] = fallback["name"] + return normalized + + @staticmethod + def _build_converter_readiness(converter: dict) -> dict: + requires = dict(converter.get("requires") or {}) + if requires.get("dify_api") is not True: + return { + "ready": True, + "status": "ready", + "message": "转换器不依赖外部 Dify API 配置。", + "required_config_keys": [], + "missing_config_keys": [], + } + + api_key_configured = bool(_env("CODEFACTORY_P3_DIFY_API_KEY", "DIFY_API_KEY")) + required_config_keys = ["CODEFACTORY_P3_DIFY_API_KEY", "DIFY_API_KEY"] + if api_key_configured: + return { + "ready": True, + "status": "ready", + "message": "P3 Dify 转换器已检测到 API Key,可执行需规转软设转换。", + "required_config_keys": required_config_keys, + "missing_config_keys": [], + "configured": {"dify_api_key": True}, + } + return { + "ready": False, + "status": "missing_configuration", + "message": "DIFY_API_KEY is not configured for requirement-to-sdd-dify-workflow", + "required_config_keys": required_config_keys, + "missing_config_keys": required_config_keys, + "configured": {"dify_api_key": False}, + "operator_hint": "请在本地或部署环境配置 CODEFACTORY_P3_DIFY_API_KEY,或兼容配置 DIFY_API_KEY。", + } + def _build_converter_request( self, design_session: dict, diff --git a/apps/api/tests/test_software_design_v2_api.py b/apps/api/tests/test_software_design_v2_api.py index 2fbb1d3..737b86e 100644 --- a/apps/api/tests/test_software_design_v2_api.py +++ b/apps/api/tests/test_software_design_v2_api.py @@ -21,6 +21,7 @@ def fake_design_converter_loader(monkeypatch): "CODEFACTORY_P3_SCOPED_DIFY_RESPONSE_MODE", ): monkeypatch.delenv(env_name, raising=False) + monkeypatch.setenv("CODEFACTORY_P3_DIFY_API_KEY", "test-p3-dify-key") class FakeDesignConverterAdapter: def __init__(self, converter_id: str) -> None: @@ -361,6 +362,54 @@ def test_software_design_v2_lists_available_design_converters() -> None: assert items[0]["protocol"] == "p3-design-converter-protocol@1" assert items[0]["observability_level"] == "limited" assert items[0]["capabilities"]["design_document"] is True + assert items[0]["readiness"]["ready"] is True + assert items[0]["readiness"]["status"] == "ready" + assert "CODEFACTORY_P3_DIFY_API_KEY" in items[0]["readiness"]["required_config_keys"] + + +def test_software_design_v2_blocks_dify_conversion_when_api_key_missing(monkeypatch) -> None: + monkeypatch.delenv("CODEFACTORY_P3_DIFY_API_KEY", raising=False) + monkeypatch.delenv("DIFY_API_KEY", raising=False) + client = TestClient(create_app()) + _create_frozen_requirement_authoring_document(client) + + converters = client.get("/api/software-design-v2/converters") + assert converters.status_code == 200 + converter = converters.json()["items"][0] + assert converter["readiness"]["ready"] is False + assert converter["readiness"]["status"] == "missing_configuration" + assert "DIFY_API_KEY" in converter["readiness"]["message"] + + input_package_id = client.get("/api/software-design-v2/input-packages").json()["items"][0]["input_package_id"] + session_response = client.post( + "/api/software-design-v2/sessions", + json={ + "input_package_id": input_package_id, + "design_title": "空域协同规划软件设计说明 - 未配置转换器", + "version_label": "v0.1", + "generation_policy": { + "architecture_preference": "统一服务优先,保留拆分点", + "module_granularity": "3-5 个业务模块,不拆太细", + "output_style": "按标准软设正文写,不写聊天语气", + }, + }, + ) + assert session_response.status_code == 200 + session = session_response.json() + assert session["conversion"]["converter"]["readiness"]["ready"] is False + + converted = client.post( + f"/api/software-design-v2/sessions/{session['session_id']}/conversion", + json={"strategy": "standard_sdd_draft"}, + ) + assert converted.status_code == 400 + assert "DIFY_API_KEY" in converted.json()["detail"] + + failed_session = client.get(f"/api/software-design-v2/sessions/{session['session_id']}").json() + assert failed_session["status"] == "conversion_failed" + assert failed_session["conversion"]["status"] == "conversion_failed" + assert failed_session["conversion"]["converter"]["readiness"]["ready"] is False + assert failed_session["conversion"]["process_output"]["error"]["source"] == "design_converter" def test_software_design_v2_applies_scoped_patch_proposal_to_design_document() -> None: diff --git a/apps/web/src/components/stageWorkbench/models.ts b/apps/web/src/components/stageWorkbench/models.ts index d65c233..4d665b2 100644 --- a/apps/web/src/components/stageWorkbench/models.ts +++ b/apps/web/src/components/stageWorkbench/models.ts @@ -177,6 +177,19 @@ export type StageConversionViewModel = { elapsedSeconds: number; progressNote?: string; strategy: string; + converter?: { + converterId: string; + name?: string; + converterType?: string; + readiness?: { + ready: boolean; + status: string; + message: string; + requiredConfigKeys: string[]; + missingConfigKeys: string[]; + operatorHint?: string; + }; + } | null; strategyOptions: Array<{ value: string; label: string; diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index f78adec..8cbd907 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -2605,9 +2605,34 @@ export type P3DesignLabConversionStep = { status: "pending" | "running" | "done" | "failed" | string; }; +export type P3DesignConverterReadiness = { + ready: boolean; + status: "ready" | "missing_configuration" | "unavailable" | string; + message: string; + required_config_keys?: string[]; + missing_config_keys?: string[]; + configured?: Record; + operator_hint?: string; +}; + +export type P3DesignConverter = { + converter_id: string; + name?: string; + converter_type?: string; + document_type?: string; + protocol?: string; + status?: string; + priority?: number; + capabilities?: Record; + requires?: Record; + observability_level?: string; + readiness?: P3DesignConverterReadiness; +}; + export type P3DesignLabConversionState = { status: "conversion_pending" | "conversion_running" | "conversion_failed" | "draft_ready" | string; strategy: string; + converter?: P3DesignConverter | null; strategy_options: Array<{ value: string; label: string; description: string }>; steps: P3DesignLabConversionStep[]; draft_preview?: { diff --git a/apps/web/src/pages/P3DesignLabPage.css b/apps/web/src/pages/P3DesignLabPage.css index 06dd62a..d58728c 100644 --- a/apps/web/src/pages/P3DesignLabPage.css +++ b/apps/web/src/pages/P3DesignLabPage.css @@ -407,6 +407,22 @@ font-weight: 850; } +.p3-design-lab-converter-readiness { + border-radius: 0; +} + +.p3-design-lab-converter-readiness .ant-alert-message { + color: #1f3330; + font-size: 12px; + font-weight: 950; +} + +.p3-design-lab-converter-readiness .ant-alert-description { + color: #4e625d; + font-size: 11px; + line-height: 1.45; +} + .p3-design-lab-conversion-strategy-picker { display: grid; gap: 7px; diff --git a/apps/web/src/pages/P3DesignLabPage.tsx b/apps/web/src/pages/P3DesignLabPage.tsx index 72c5409..8f11b0f 100644 --- a/apps/web/src/pages/P3DesignLabPage.tsx +++ b/apps/web/src/pages/P3DesignLabPage.tsx @@ -121,6 +121,28 @@ function getApiErrorDetail(error: unknown) { return ""; } +function getConversionReadinessNotice(workbench: StageDocumentWorkbenchViewModel) { + const readiness = workbench.conversion.converter?.readiness; + if (!readiness) { + return null; + } + const missingKeys = readiness.missingConfigKeys.length ? readiness.missingConfigKeys.join(" / ") : ""; + if (readiness.ready) { + return { + ready: true, + title: "转换器已就绪", + description: readiness.message || "当前转换器已通过本地配置检查。", + }; + } + return { + ready: false, + title: "转换器未就绪", + description: + readiness.operatorHint || + (missingKeys ? `缺少配置:${missingKeys}` : readiness.message || "当前转换器缺少必要运行配置。"), + }; +} + export function P3DesignLabPage() { const location = useLocation(); const navigate = useNavigate(); @@ -417,6 +439,11 @@ export function P3DesignLabPage() { if (conversionInFlightSessionId === designSession.session_id) { return; } + const readinessNotice = getConversionReadinessNotice(workbench); + if (readinessNotice && !readinessNotice.ready) { + setError(`${readinessNotice.title}:${readinessNotice.description}`); + return; + } const runningSessionId = designSession.session_id; try { setSubmitting(true); @@ -1546,6 +1573,8 @@ function StageRelationInspector({ }) { const relationType = typeof selection.payload?.relationType === "string" ? selection.payload.relationType : ""; const isBasicConversion = selection.objectId === "reqdoc"; + const readinessNotice = getConversionReadinessNotice(workbench); + const converterUnavailable = Boolean(hasSession && readinessNotice && !readinessNotice.ready); return ( <> @@ -1571,6 +1600,16 @@ function StageRelationInspector({ 转换为 {toInspectorText(selection.payload?.outputSummary)} + {readinessNotice ? ( + + ) : null}
转换策略 @@ -1588,7 +1627,7 @@ function StageRelationInspector({