feat(ai): S7d 生成 provider 切 Dify 的 fail-closed 硬化 + 超时对齐(代码层)
分支①第四步代码/配置层:RoutingMuseAiRuntimeClient 补 Dify 装配为占位 Unavailable 时的 fail-closed ——dify 选中但未装配/凭据缺 → 返回 Dify 专属 AI_DIFY_UNAVAILABLE 并拒绝,绝不回退 New-API(此前会 误报 AI_NEW_API_UNAVAILABLE);dify 分支三出口(requiresSourceRefs→REF_INVALID / !difyAvailable→ UNAVAILABLE / 否则 difyClient.execute)均不触碰 newApiClient(亲验)。Dify non-stream-read 超时默认 90→180s 对齐总预算(180≤180<SSE 死线 240);单人 solo-compose.env.example 启用 Dify 块(凭据取自 既有 S1 DIFY 块,未新造);New-API adapter 与配置保留(S8 才摘)。 在用 Agent 的 runtimeProvider 存于 muse_ai_agent_version.config(运行时 DB JSON 列)非静态种子,故真正 把在用 Agent 切 Dify 属 live 数据操作,归 M3 由主会话执行(本提交不含 DB 改动、不切换 live provider, 超时改动在 Dify 未启用前休眠)。 验证:ai 模块 530 用例全绿(新增 fail-closed 不回退 + Dify 完整正文形态 2 例);P1rDifyChatLiveAcceptanceIT 真连 100.64.0.8:18080 验证 wiring/契约/失败映射通过。**M3 live happy-path 阻断**:Dify「muse-写作透传」 app(75f105e8)/chat-messages 返 500(/info 200)= 该 app 模型后端未配好(Dify 侧配置,非 Muse 代码), 代码正确映射 AI_DIFY_PROVIDER_5XX 可重试+fail-closed;待所有者在 Dify 侧修复 app 模型配置后跑 M3。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2ac42a0628
commit
b583bd6dc6
@ -19,10 +19,15 @@ public class RoutingMuseAiRuntimeClient implements MuseAiRuntimeClient {
|
||||
|
||||
private final MuseAiRuntimeClient newApiClient;
|
||||
private final MuseAiRuntimeClient difyClient;
|
||||
private final boolean difyAvailable;
|
||||
|
||||
public RoutingMuseAiRuntimeClient(MuseAiRuntimeClient newApiClient, MuseAiRuntimeClient difyClient) {
|
||||
this.newApiClient = newApiClient == null ? new UnavailableMuseAiRuntimeClient() : newApiClient;
|
||||
this.difyClient = difyClient == null ? null : difyClient;
|
||||
this.difyClient = difyClient;
|
||||
// Dify 槽位未装配(null)或仅装配为占位 Unavailable adapter 时,一律视为 Dify 不可用。
|
||||
// WHY 单列 Unavailable:占位 Unavailable 会返回 New-API 语义错误码(AI_NEW_API_UNAVAILABLE),
|
||||
// 对 Dify 路径具误导性;S7d 要求选中 dify 必须失败关闭并抛 Dify 专属错误,绝不回退 New-API 伪成功。
|
||||
this.difyAvailable = difyClient != null && !(difyClient instanceof UnavailableMuseAiRuntimeClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -38,7 +43,9 @@ public class RoutingMuseAiRuntimeClient implements MuseAiRuntimeClient {
|
||||
return failure(command, "provider_bad_request", "AI_AGENT_DIFY_REF_INVALID",
|
||||
"Dify runtime cannot satisfy Muse source attribution contract", false);
|
||||
}
|
||||
if (difyClient == null) {
|
||||
// fail-closed:Dify 未装配/装配为 Unavailable/凭据缺失 → 拒绝,绝不回退 New-API。
|
||||
// (凭据/appId 缺失等细分错误由 difyClient 内部 validateConfig 失败关闭并返回 Dify 专属错误码。)
|
||||
if (!difyAvailable) {
|
||||
return failure(command, "runtime_unavailable", "AI_DIFY_UNAVAILABLE",
|
||||
"AI Dify runtime unavailable", false);
|
||||
}
|
||||
|
||||
@ -267,7 +267,9 @@ public class MuseAiProperties {
|
||||
private List<Credential> credentials = List.of();
|
||||
|
||||
private Integer connectTimeoutSeconds = 5;
|
||||
private Integer nonStreamReadTimeoutSeconds = 90;
|
||||
// 生成主链切 Dify(S7d)默认对齐 180s 总预算:实际请求超时取 min(该值, 总预算),
|
||||
// 若低于总预算会把长篇生成截短,故默认 180(须 < SSE 死线 240s)。
|
||||
private Integer nonStreamReadTimeoutSeconds = 180;
|
||||
private Integer totalTimeoutSeconds = 180;
|
||||
|
||||
@Data
|
||||
|
||||
@ -248,7 +248,9 @@ muse:
|
||||
- ref: ${MUSE_AI_DIFY_PARSER_CREDENTIAL_REF:dify-parser-s1}
|
||||
api-key: ${MUSE_AI_DIFY_PARSER_API_KEY:}
|
||||
connect-timeout-seconds: ${MUSE_AI_DIFY_CONNECT_TIMEOUT_SECONDS:5}
|
||||
non-stream-read-timeout-seconds: ${MUSE_AI_DIFY_NON_STREAM_READ_TIMEOUT_SECONDS:90}
|
||||
# 生成主链切 Dify(S7d):短读超时对齐 180s 总预算。WHY 不是 90:实际请求超时取 min(非流式短读, 总预算),
|
||||
# 90 会把长篇生成截到 90s;须 ≥ 总预算(180s)且 < SSE 死线(240s)才能让上游先出话、后端不提前关门。
|
||||
non-stream-read-timeout-seconds: ${MUSE_AI_DIFY_NON_STREAM_READ_TIMEOUT_SECONDS:180}
|
||||
total-timeout-seconds: ${MUSE_AI_DIFY_TOTAL_TIMEOUT_SECONDS:180}
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
@ -525,6 +525,54 @@ class MuseAiRuntimeClientTest {
|
||||
assertTrue(response.failure().retryable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_failClosedForDifyWhenAssembledUnavailableWithoutFallbackToNewApi() {
|
||||
// S7d fail-closed 反向:Dify 槽位装配为占位 Unavailable(非 null)时,选中 dify 仍必须失败关闭,
|
||||
// 返回 Dify 专属不可用错误码,且绝不回退 New-API —— newApi lambda 若被命中会返回 wrong 结果使断言失败。
|
||||
RoutingMuseAiRuntimeClient client = new RoutingMuseAiRuntimeClient(
|
||||
command -> new MuseAiRuntimeClient.RuntimeResponse(
|
||||
new MuseAiRuntimeClient.RuntimeResult("corr-real-1", "success", Map.of("summary", "wrong"),
|
||||
Map.of(), null, "new-api-should-not-run", "stop", Instant.now()), null),
|
||||
new UnavailableMuseAiRuntimeClient());
|
||||
|
||||
MuseAiRuntimeClient.RuntimeResponse response = client.execute(command(difyInputSummary("agent")));
|
||||
|
||||
assertNull(response.result());
|
||||
assertEquals("runtime_unavailable", response.failure().failureType());
|
||||
assertEquals("AI_DIFY_UNAVAILABLE", response.failure().errorCode());
|
||||
assertFalse(response.failure().retryable());
|
||||
}
|
||||
|
||||
// S7d Dify 形态:完整正文(>80 字、句末标点)经护栏通过后,fullOutput 承载完整正文(仅内存供 S7b),
|
||||
// 落库摘要 content 仍只 80 字截断,证明「审的是完整正文、主权摘要不含全文」。
|
||||
private static final String COMPLETE_DIFY_OUTPUT_SAMPLE =
|
||||
"夜色像一层薄薄的墨慢慢浸透了整座城市,街灯在雨里晕开成一团团柔和的光,"
|
||||
+ "她合上笔记本望向窗外那条空荡荡的长街,心里第一次生出一种前所未有的笃定,"
|
||||
+ "仿佛所有漂泊多年的疑问都在这一刻悄然有了答案。";
|
||||
|
||||
@Test
|
||||
void should_routeDifyCompleteOutputWithFullOutputWhileSummaryTruncated() throws Exception {
|
||||
startServer(exchange -> writeJson(exchange, 200,
|
||||
"{\"message_id\":\"dify-msg-3\",\"answer\":\"" + COMPLETE_DIFY_OUTPUT_SAMPLE + "\"}", null));
|
||||
RoutingMuseAiRuntimeClient client = new RoutingMuseAiRuntimeClient(
|
||||
new RealNewApiMuseAiRuntimeClient(newApiProperties(true)),
|
||||
new RealDifyMuseAiRuntimeClient(difyProperties(true)));
|
||||
|
||||
MuseAiRuntimeClient.RuntimeResponse response = client.execute(command(difyInputSummary("agent")));
|
||||
|
||||
assertNull(response.failure());
|
||||
assertEquals("success", response.result().status());
|
||||
assertEquals("dify", response.result().outputSummary().get("provider"));
|
||||
assertEquals("untraceable_provider_context", response.result().outputSummary().get("traceability"));
|
||||
// fullOutput = 完整正文(仅内存,供 S7b 瞬态通路);落库摘要 content 只 80 字、不等于完整正文。
|
||||
assertEquals(COMPLETE_DIFY_OUTPUT_SAMPLE, response.result().fullOutput());
|
||||
String summary = String.valueOf(response.result().outputSummary().get("content"));
|
||||
assertTrue(summary.length() <= 80, "落库摘要不得超过 80 字快照");
|
||||
assertTrue(response.result().fullOutput().length() > summary.length(),
|
||||
"完整正文长度须大于落库摘要,证明摘要为截断快照而非全文");
|
||||
assertFalse(summary.equals(response.result().fullOutput()), "落库摘要不得等于完整正文");
|
||||
}
|
||||
|
||||
private MuseAiRuntimeClient.RuntimeCommand command(Map<String, Object> inputSummary) {
|
||||
return new MuseAiRuntimeClient.RuntimeCommand(
|
||||
"cmd-real-1", "3001", "4001", "corr-real-1", 1, "model-a", "prompt-v9",
|
||||
|
||||
@ -42,7 +42,8 @@ MUSE_AI_DIFY_PARSER_API_KEY=app-It4V7WukHohP8rhWV0gOn2H5
|
||||
MUSE_AI_DIFY_PARSER_CREDENTIAL_REF=dify-parser-s1
|
||||
MUSE_AI_DIFY_CONNECT_TIMEOUT_SECONDS=5
|
||||
MUSE_AI_DIFY_FIRST_BYTE_TIMEOUT_SECONDS=15
|
||||
MUSE_AI_DIFY_NON_STREAM_READ_TIMEOUT_SECONDS=90
|
||||
# S7d 生成主链切 Dify:短读超时对齐 180s 总预算(实际请求超时取 min(短读,总预算),90 会截短长篇生成)。
|
||||
MUSE_AI_DIFY_NON_STREAM_READ_TIMEOUT_SECONDS=180
|
||||
MUSE_AI_DIFY_STREAM_IDLE_TIMEOUT_SECONDS=30
|
||||
MUSE_AI_DIFY_TOTAL_TIMEOUT_SECONDS=180
|
||||
MUSE_KNOWLEDGE_DIFY_BASE_URL=http://100.64.0.8:18080/v1
|
||||
|
||||
@ -10,6 +10,13 @@ MUSE_ACCOUNT_EVENTS_PUBLISH_WORKER_ENABLED=false
|
||||
MUSE_ACCOUNT_EVENTS_ATTRIBUTION_WORKER_ENABLED=false
|
||||
MUSE_ACCOUNT_EVENTS_QUOTA_REQUEST_WORKER_ENABLED=false
|
||||
|
||||
# AI 运行时后续由 S7/S8 切到 Dify;S4 保持 New-API/Dify adapter 未配置即 fail-closed。
|
||||
# AI 运行时:S7d 生成主链切 Dify,启用 Dify adapter;New-API adapter 代码保留但配置默认关闭(S8 验收后才摘)。
|
||||
MUSE_AI_NEW_API_ENABLED=false
|
||||
MUSE_AI_DIFY_ENABLED=false
|
||||
MUSE_AI_DIFY_ENABLED=true
|
||||
# Dify 生成 app 的真实凭据不在样例提交(见文件开头);base-url / writing app-id / api-key 从
|
||||
# scripts/dev/p1r-external-acceptance.env 的 DIFY_S1 块(Muse 专属 workspace)注入,勿在此新造。
|
||||
# credentialRef 仅为引用名(非密钥),须与注入的 Dify 凭据 ref 一致。
|
||||
MUSE_AI_DIFY_WRITING_CREDENTIAL_REF=dify-writing-s1
|
||||
# 生成正文时延上限对齐 180s 总预算(< SSE 死线 240s);短读超时须 ≥ 总预算,否则被 min() 截到 90s。
|
||||
MUSE_AI_DIFY_NON_STREAM_READ_TIMEOUT_SECONDS=180
|
||||
MUSE_AI_DIFY_TOTAL_TIMEOUT_SECONDS=180
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user