feat(desktop): add Wenxin monitoring adapter and surface platform risk control

Introduces the Wenxin (文心一言) monitoring adapter and registers it in
the runtime controller and adapter index. Adds a new risk_control
failure classification with custom classifiers for Doubao and Wenxin
that recognize rate-limit, 频控 and 访问环境异常 signals, and propagates
this state through account-health, runtime activity alerts, and the
renderer views so users see "触发风控" instead of a generic challenge
message on the Home, Accounts, and AI Platforms screens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 15:26:24 +08:00
parent bd4ee8feef
commit ca40657c5a
12 changed files with 2552 additions and 21 deletions
@@ -3,5 +3,6 @@ export * from "./doubao";
export * from "./kimi";
export * from "./qwen";
export * from "./toutiao";
export * from "./wenxin";
export * from "./yuanbao";
export * from "./zhihu";
@@ -0,0 +1,197 @@
import { describe, expect, it } from "vitest";
import { __wenxinTestUtils } from "./wenxin";
const {
htmlTableToMarkdown,
isObservationComplete,
parseWenxinHistoryResponse,
parseWenxinSSEBody,
} = __wenxinTestUtils;
describe("wenxin adapter helpers", () => {
it("parses conversation stream major and message events", () => {
const sseBody = [
"event:major",
"data:{\"logId\":\"major-log\",\"data\":{\"createChatResponseVoCommonResult\":{\"data\":{\"chat\":{\"id\":\"user-1\",\"message\":[{\"content\":\"问题\"}]},\"botChat\":{\"id\":\"bot-1\",\"message\":[{\"content\":\"正在生成中...\"}],\"messageId\":\"msg-1\",\"modelSign\":\"EB45T\"}}},\"createSessionResponseVoCommonResult\":{\"data\":{\"sessionId\":\"session-1\",\"model\":\"EB45T\"}}}}",
"",
"event:message",
"data:{\"code\":0,\"data\":{\"chat_id\":\"bot-1\",\"content\":\"第一段\",\"is_end\":false}}",
"",
"event:message",
"data:{\"code\":0,\"data\":{\"chat_id\":\"bot-1\",\"tokens_all\":\"第一段\\n第二段\",\"is_end\":true}}",
].join("\n");
expect(parseWenxinSSEBody(sseBody, true, null)).toMatchObject({
sessionId: "session-1",
botChatId: "bot-1",
providerModel: "EB45T",
providerRequestID: "msg-1",
answer: "第一段\n第二段",
isEnded: true,
captureDone: true,
});
});
it("extracts withdraw text from stream", () => {
const sseBody = [
"event:major",
"data:{\"data\":{\"createChatResponseVoCommonResult\":{\"data\":{\"isWithdraw\":true,\"withdrawText\":\"当前访问环境存在异常,请更换浏览器再尝试提问\",\"sessionDel\":true}}}}",
"",
"ERR:AbortError: BodyStreamBuffer was aborted",
].join("\n");
expect(parseWenxinSSEBody(sseBody, true, "AbortError: BodyStreamBuffer was aborted")).toMatchObject({
withdrawText: "当前访问环境存在异常,请更换浏览器再尝试提问",
sessionDel: true,
});
});
it("converts html tables to markdown", () => {
const markdown = htmlTableToMarkdown("<table><tr><th>品牌</th><th>均价</th></tr><tr><td>A</td><td>999</td></tr></table>");
expect(markdown).toBe(
[
"| 品牌 | 均价 |",
"| --- | --- |",
"| A | 999 |",
].join("\n"),
);
});
it("extracts answer text, citations, search results, and tables from history payload", () => {
const history = parseWenxinHistoryResponse({
code: 0,
msg: "success",
data: {
state: 1,
chats: [
{
id: "user-1",
role: "user",
message: [
{
contentType: "text",
content: "问题",
},
],
},
{
id: "bot-1",
role: "robot",
stop: 1,
mode: "history",
modelSign: "EB45T",
messageId: "msg-1",
citations: [
{
url: "https://example.com/article",
title: "示例文章",
siteName: "示例站点",
},
],
searchCitations: {
total: 1,
list: [
{
url: "https://search.example.com/result",
title: "搜索结果",
site: "搜索站点",
},
],
},
message: [
{
contentType: "text",
content: "推荐如下:",
},
{
contentType: "table",
content: "<table><tr><th>品牌</th><th>价格</th></tr><tr><td>A</td><td>1000</td></tr></table>",
},
],
},
],
},
}, "session-1");
expect(history.answer).toContain("推荐如下:");
expect(history.answer).toContain("| 品牌 | 价格 |");
expect(history.citations).toHaveLength(2);
expect(history.searchResults).toHaveLength(1);
expect(history.providerModel).toBe("EB45T");
expect(history.providerRequestID).toBe("msg-1");
expect(history.generating).toBe(false);
});
it("falls back to nested web list sources when search citations are not on the latest chat root", () => {
const history = parseWenxinHistoryResponse({
code: 0,
msg: "success",
data: {
state: 1,
webListData: {
list: [
{
url: "https://fallback.example.com/article",
title: "回退网页",
name: "回退站点",
},
],
},
chats: [
{
id: "bot-1",
role: "robot",
stop: 1,
mode: "history",
modelSign: "EB45T",
messageId: "msg-2",
message: [
{
contentType: "text",
content: "这是答案",
},
],
},
],
},
}, "session-2");
expect(history.searchResults).toHaveLength(1);
expect(history.searchResults[0]?.url).toBe("https://fallback.example.com/article");
expect(history.citations[0]?.url).toBe("https://fallback.example.com/article");
});
it("does not treat error-only observations as complete", () => {
expect(isObservationComplete({
sessionId: "session-1",
providerModel: "EB45T",
providerRequestID: "msg-1",
answer: null,
answerBlocks: [],
citations: [],
searchResults: [],
withdrawText: null,
errorMessage: "temporary stream issue",
historyGenerating: false,
streamDone: false,
signature: "sig-1",
})).toBe(false);
expect(isObservationComplete({
sessionId: "session-1",
providerModel: "EB45T",
providerRequestID: "msg-1",
answer: null,
answerBlocks: [],
citations: [],
searchResults: [],
withdrawText: "内容暂不可用",
errorMessage: null,
historyGenerating: false,
streamDone: false,
signature: "sig-2",
})).toBe(false);
});
});
File diff suppressed because it is too large Load Diff