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:
@@ -602,10 +602,10 @@ export async function reportAccountFailure(
|
|||||||
const previousSignature = snapshotSignature(record);
|
const previousSignature = snapshotSignature(record);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (classification === "challenge") {
|
if (classification === "challenge" || classification === "risk_control") {
|
||||||
record.authState = "challenge_required";
|
record.authState = "challenge_required";
|
||||||
record.probeState = "idle";
|
record.probeState = "idle";
|
||||||
record.authReason = "captcha_gate";
|
record.authReason = classification === "risk_control" ? "risk_control" : "captcha_gate";
|
||||||
record.lastAuthFailureAt = now;
|
record.lastAuthFailureAt = now;
|
||||||
record.lastProbeAt = now;
|
record.lastProbeAt = now;
|
||||||
record.nextProbeAt = nextRegularProbeAt(now);
|
record.nextProbeAt = nextRegularProbeAt(now);
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export * from "./doubao";
|
|||||||
export * from "./kimi";
|
export * from "./kimi";
|
||||||
export * from "./qwen";
|
export * from "./qwen";
|
||||||
export * from "./toutiao";
|
export * from "./toutiao";
|
||||||
|
export * from "./wenxin";
|
||||||
export * from "./yuanbao";
|
export * from "./yuanbao";
|
||||||
export * from "./zhihu";
|
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
@@ -17,6 +17,7 @@ export type AccountAuthReason =
|
|||||||
| "login_redirect"
|
| "login_redirect"
|
||||||
| "http_401"
|
| "http_401"
|
||||||
| "captcha_gate"
|
| "captcha_gate"
|
||||||
|
| "risk_control"
|
||||||
| "missing_partition"
|
| "missing_partition"
|
||||||
| "silent_refresh_failed"
|
| "silent_refresh_failed"
|
||||||
| "manual_unbind"
|
| "manual_unbind"
|
||||||
@@ -56,6 +57,6 @@ export interface AuthProbeResult {
|
|||||||
export type PlatformFailureClassification =
|
export type PlatformFailureClassification =
|
||||||
| "auth_failure"
|
| "auth_failure"
|
||||||
| "challenge"
|
| "challenge"
|
||||||
|
| "risk_control"
|
||||||
| "network"
|
| "network"
|
||||||
| "ok";
|
| "ok";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("./account-binder", () => ({
|
||||||
|
probePublishAccountSession: vi.fn(),
|
||||||
|
silentRefreshPublishAccountSession: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let getPlatformAdapter: typeof import("./platform-auth-adapters").getPlatformAdapter;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
({ getPlatformAdapter } = await import("./platform-auth-adapters"));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("platform auth adapters", () => {
|
||||||
|
it("classifies Wenxin environment abnormal messages as risk control", () => {
|
||||||
|
const adapter = getPlatformAdapter("wenxin");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
adapter.classifyFailure({
|
||||||
|
message: "当前访问环境存在异常,请更换浏览器再尝试提问",
|
||||||
|
}),
|
||||||
|
).toBe("risk_control");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,8 +27,10 @@ function normalizeFailureText(input: PlatformFailureInput): string {
|
|||||||
input.summary,
|
input.summary,
|
||||||
input.message,
|
input.message,
|
||||||
typeof input.error?.code === "string" ? input.error.code : null,
|
typeof input.error?.code === "string" ? input.error.code : null,
|
||||||
|
typeof input.error?.reason_code === "string" ? input.error.reason_code : null,
|
||||||
typeof input.error?.message === "string" ? input.error.message : null,
|
typeof input.error?.message === "string" ? input.error.message : null,
|
||||||
typeof input.error?.detail === "string" ? input.error.detail : null,
|
typeof input.error?.detail === "string" ? input.error.detail : null,
|
||||||
|
typeof input.error?.page_error === "string" ? input.error.page_error : null,
|
||||||
typeof input.error?.verify_scene === "string" ? input.error.verify_scene : null,
|
typeof input.error?.verify_scene === "string" ? input.error.verify_scene : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -67,6 +69,38 @@ function classifyFailure(input: PlatformFailureInput): PlatformFailureClassifica
|
|||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function classifyDoubaoFailure(input: PlatformFailureInput): PlatformFailureClassification {
|
||||||
|
const text = normalizeFailureText(input);
|
||||||
|
if (!text) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(rate limited|rate limit|too many requests|request limit|frequency limit|请求过于频繁|频率过快|访问过于频繁|限流|风控|服务繁忙|请稍后|稍后再试)/i
|
||||||
|
.test(text)
|
||||||
|
) {
|
||||||
|
return "risk_control";
|
||||||
|
}
|
||||||
|
|
||||||
|
return classifyFailure(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifyWenxinFailure(input: PlatformFailureInput): PlatformFailureClassification {
|
||||||
|
const text = normalizeFailureText(input);
|
||||||
|
if (!text) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(当前访问环境存在异常|访问环境存在异常|环境存在异常|更换浏览器再尝试提问|当前环境异常|访问受限|环境异常)/i
|
||||||
|
.test(text)
|
||||||
|
) {
|
||||||
|
return "risk_control";
|
||||||
|
}
|
||||||
|
|
||||||
|
return classifyFailure(input);
|
||||||
|
}
|
||||||
|
|
||||||
const genericAdapter: PlatformAdapter = {
|
const genericAdapter: PlatformAdapter = {
|
||||||
async probe(account, partition) {
|
async probe(account, partition) {
|
||||||
return await probePublishAccountSession(account, partition);
|
return await probePublishAccountSession(account, partition);
|
||||||
@@ -77,6 +111,26 @@ const genericAdapter: PlatformAdapter = {
|
|||||||
classifyFailure,
|
classifyFailure,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const doubaoAdapter: PlatformAdapter = {
|
||||||
|
async probe(account, partition) {
|
||||||
|
return await probePublishAccountSession(account, partition);
|
||||||
|
},
|
||||||
|
async silentRefresh(account, partition) {
|
||||||
|
return await silentRefreshPublishAccountSession(account, partition);
|
||||||
|
},
|
||||||
|
classifyFailure: classifyDoubaoFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
const wenxinAdapter: PlatformAdapter = {
|
||||||
|
async probe(account, partition) {
|
||||||
|
return await probePublishAccountSession(account, partition);
|
||||||
|
},
|
||||||
|
async silentRefresh(account, partition) {
|
||||||
|
return await silentRefreshPublishAccountSession(account, partition);
|
||||||
|
},
|
||||||
|
classifyFailure: classifyWenxinFailure,
|
||||||
|
};
|
||||||
|
|
||||||
const adapterRegistry = new Map<string, PlatformAdapter>(
|
const adapterRegistry = new Map<string, PlatformAdapter>(
|
||||||
[
|
[
|
||||||
...aiPlatformCatalog.map((platform) => platform.id),
|
...aiPlatformCatalog.map((platform) => platform.id),
|
||||||
@@ -93,10 +147,16 @@ const adapterRegistry = new Map<string, PlatformAdapter>(
|
|||||||
"weixin_gzh",
|
"weixin_gzh",
|
||||||
"zol",
|
"zol",
|
||||||
"dongchedi",
|
"dongchedi",
|
||||||
].map((platform) => [platform, genericAdapter]),
|
].map((platform) => [
|
||||||
|
platform,
|
||||||
|
platform === "doubao"
|
||||||
|
? doubaoAdapter
|
||||||
|
: platform === "wenxin"
|
||||||
|
? wenxinAdapter
|
||||||
|
: genericAdapter,
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
export function getPlatformAdapter(platformId: string): PlatformAdapter {
|
export function getPlatformAdapter(platformId: string): PlatformAdapter {
|
||||||
return adapterRegistry.get(platformId) ?? genericAdapter;
|
return adapterRegistry.get(platformId) ?? genericAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
kimiAdapter,
|
kimiAdapter,
|
||||||
qwenAdapter,
|
qwenAdapter,
|
||||||
toutiaoAdapter,
|
toutiaoAdapter,
|
||||||
|
wenxinAdapter,
|
||||||
yuanbaoAdapter,
|
yuanbaoAdapter,
|
||||||
zhihuAdapter,
|
zhihuAdapter,
|
||||||
type AdapterExecutionResult,
|
type AdapterExecutionResult,
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ensureAccountReady,
|
ensureAccountReady,
|
||||||
forgetTrackedAccountHealth,
|
forgetTrackedAccountHealth,
|
||||||
|
getAccountHealthSnapshot,
|
||||||
getProjectedAccountHealth,
|
getProjectedAccountHealth,
|
||||||
probeTrackedAccount,
|
probeTrackedAccount,
|
||||||
reportAccountFailure,
|
reportAccountFailure,
|
||||||
@@ -127,7 +129,7 @@ const pullIntervalMs = 60_000;
|
|||||||
const leaseExtendIntervalMs = 60_000;
|
const leaseExtendIntervalMs = 60_000;
|
||||||
const maxActivityItems = 60;
|
const maxActivityItems = 60;
|
||||||
const monitorBusinessTimeZone = "Asia/Shanghai";
|
const monitorBusinessTimeZone = "Asia/Shanghai";
|
||||||
const authRequiredMonitorPlatforms = new Set(["yuanbao", "kimi", "deepseek", "doubao"]);
|
const authRequiredMonitorPlatforms = new Set(["yuanbao", "kimi", "deepseek", "doubao", "wenxin"]);
|
||||||
|
|
||||||
interface RuntimeTaskRecord {
|
interface RuntimeTaskRecord {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -279,6 +281,53 @@ function isDefinitiveAuthState(authState: string): boolean {
|
|||||||
return ["active", "expired", "challenge_required", "revoked"].includes(authState);
|
return ["active", "expired", "challenge_required", "revoked"].includes(authState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runtimePlatformLabel(platform: string): string {
|
||||||
|
return getAIPlatformCatalogItem(platform)?.label ?? platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function accountActionRequiredSummary(
|
||||||
|
platform: string,
|
||||||
|
authReason: string | null | undefined,
|
||||||
|
): string {
|
||||||
|
const label = runtimePlatformLabel(platform);
|
||||||
|
if (authReason === "risk_control") {
|
||||||
|
return `${label} 账号疑似触发风控,请打开平台处理验证或等待限制解除后再继续执行。`;
|
||||||
|
}
|
||||||
|
return `${label} 账号需要完成人机验证后才能继续执行。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRecordAccountAccessAlert(
|
||||||
|
task: RuntimeTaskRecord,
|
||||||
|
accountIdentity: PublishAccountIdentity | null,
|
||||||
|
previous: ReturnType<typeof getAccountHealthSnapshot>,
|
||||||
|
next: ReturnType<typeof getAccountHealthSnapshot>,
|
||||||
|
): void {
|
||||||
|
if (!accountIdentity || !next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous?.authState === next.authState && previous?.authReason === next.authReason) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.authState === "challenge_required" && next.authReason === "risk_control") {
|
||||||
|
recordActivity(
|
||||||
|
"warn",
|
||||||
|
`${runtimePlatformLabel(accountIdentity.platform)} 触发风控`,
|
||||||
|
`${accountIdentity.displayName || task.accountName || "当前账号"} 已被平台限制,请打开平台处理验证或等待限制解除后再刷新状态。`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.authState === "challenge_required") {
|
||||||
|
recordActivity(
|
||||||
|
"warn",
|
||||||
|
`${runtimePlatformLabel(accountIdentity.platform)} 需要人工验证`,
|
||||||
|
`${accountIdentity.displayName || task.accountName || "当前账号"} 需要打开平台完成人工验证后再继续执行任务。`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isoFromTimestamp(value: number | null): string | null {
|
function isoFromTimestamp(value: number | null): string | null {
|
||||||
return value ? new Date(value).toISOString() : null;
|
return value ? new Date(value).toISOString() : null;
|
||||||
}
|
}
|
||||||
@@ -2209,10 +2258,10 @@ function buildAccountBlockedResult(
|
|||||||
if (readiness.authState === "challenge_required") {
|
if (readiness.authState === "challenge_required") {
|
||||||
return {
|
return {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
summary: `${task.accountName} 需要完成人机验证后才能继续执行。`,
|
summary: accountActionRequiredSummary(task.platform, readiness.authReason),
|
||||||
error: {
|
error: {
|
||||||
code: "desktop_account_challenge_required",
|
code: readiness.authReason === "risk_control" ? "desktop_account_risk_control" : "desktop_account_challenge_required",
|
||||||
message: "desktop account challenge required",
|
message: readiness.authReason === "risk_control" ? "desktop account risk control triggered" : "desktop account challenge required",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2247,7 +2296,8 @@ async function maybeReportTaskAuthFailure(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await reportAccountFailure(accountIdentity, {
|
const previousSnapshot = getAccountHealthSnapshot(accountIdentity.id);
|
||||||
|
const nextSnapshot = await reportAccountFailure(accountIdentity, {
|
||||||
summary: result.summary,
|
summary: result.summary,
|
||||||
error: result.error ?? null,
|
error: result.error ?? null,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -2257,7 +2307,10 @@ async function maybeReportTaskAuthFailure(
|
|||||||
platform: accountIdentity.platform,
|
platform: accountIdentity.platform,
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
maybeRecordAccountAccessAlert(task, accountIdentity, previousSnapshot, nextSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePlaywrightTargetURL(
|
function resolvePlaywrightTargetURL(
|
||||||
@@ -2640,6 +2693,9 @@ function selectMonitorAdapter(platform: string): MonitorAdapter | null {
|
|||||||
if (platform === qwenAdapter.provider) {
|
if (platform === qwenAdapter.provider) {
|
||||||
return qwenAdapter;
|
return qwenAdapter;
|
||||||
}
|
}
|
||||||
|
if (platform === wenxinAdapter.provider) {
|
||||||
|
return wenxinAdapter;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface RuntimeAccount {
|
|||||||
| "login_redirect"
|
| "login_redirect"
|
||||||
| "http_401"
|
| "http_401"
|
||||||
| "captcha_gate"
|
| "captcha_gate"
|
||||||
|
| "risk_control"
|
||||||
| "missing_partition"
|
| "missing_partition"
|
||||||
| "silent_refresh_failed"
|
| "silent_refresh_failed"
|
||||||
| "manual_unbind"
|
| "manual_unbind"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function authStateLabel(account: AccountRow): string {
|
|||||||
case "expired":
|
case "expired":
|
||||||
return "授权过期";
|
return "授权过期";
|
||||||
case "attention":
|
case "attention":
|
||||||
return "需人工验证";
|
return account.authReason === "risk_control" ? "触发风控" : "需人工验证";
|
||||||
default:
|
default:
|
||||||
return account.probeState === "network_error" ? "最近校验失败" : "待重新校验";
|
return account.probeState === "network_error" ? "最近校验失败" : "待重新校验";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function authLabel(account: AccountRow | null) {
|
|||||||
case "active":
|
case "active":
|
||||||
return "授权正常";
|
return "授权正常";
|
||||||
case "challenge_required":
|
case "challenge_required":
|
||||||
return "需人工验证";
|
return account.authReason === "risk_control" ? "触发风控" : "需人工验证";
|
||||||
case "expired":
|
case "expired":
|
||||||
return "授权过期";
|
return "授权过期";
|
||||||
case "expiring_soon":
|
case "expiring_soon":
|
||||||
@@ -88,6 +88,22 @@ function authColor(account: AccountRow | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function accountActionAlert(account: AccountRow | null): string | null {
|
||||||
|
if (!account || account.authState !== "challenge_required") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.authReason === "risk_control" && account.platform === "doubao") {
|
||||||
|
return "豆包账号疑似触发风控,监控已暂停。请点击“打开平台”,在前台完成验证或等待限制解除后,再刷新状态。";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.authReason === "risk_control") {
|
||||||
|
return "当前账号疑似触发平台限制,相关任务已暂停。请先打开平台处理验证后,再刷新状态。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "当前账号需要人工验证,相关任务会暂停执行。请先打开平台完成验证后,再刷新状态。";
|
||||||
|
}
|
||||||
|
|
||||||
function verificationLabel(account: AccountRow): string {
|
function verificationLabel(account: AccountRow): string {
|
||||||
if (account.lastVerifiedAt) {
|
if (account.lastVerifiedAt) {
|
||||||
return formatVerifiedAtLabel(account.lastVerifiedAt);
|
return formatVerifiedAtLabel(account.lastVerifiedAt);
|
||||||
@@ -328,6 +344,19 @@ async function unbindPlatform(account: AccountRow) {
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="accountActionAlert(platform.account)" class="card-alert">
|
||||||
|
<a-alert
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
banner
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
{{ accountActionAlert(platform.account) }}
|
||||||
|
</template>
|
||||||
|
<template #icon><WarningOutlined /></template>
|
||||||
|
</a-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="platform.duplicateCount > 0" class="card-alert">
|
<div v-if="platform.duplicateCount > 0" class="card-alert">
|
||||||
<a-alert
|
<a-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ function translatePlatform(platform: string) {
|
|||||||
return map[platform?.toLowerCase()] || titleCaseToken(platform);
|
return map[platform?.toLowerCase()] || titleCaseToken(platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expiredAccountLabel(authState: string) {
|
function blockedAccountLabel(account: { authState: string; authReason: string | null }) {
|
||||||
switch (authState) {
|
switch (account.authState) {
|
||||||
|
case "challenge_required":
|
||||||
|
return account.authReason === "risk_control" ? "触发风控" : "待验证";
|
||||||
case "revoked":
|
case "revoked":
|
||||||
return "已停用";
|
return "已停用";
|
||||||
case "expired":
|
case "expired":
|
||||||
@@ -83,8 +85,8 @@ const tasks = computed(() => snapshot.value?.tasks ?? []);
|
|||||||
const accounts = computed(() => snapshot.value?.accounts ?? []);
|
const accounts = computed(() => snapshot.value?.accounts ?? []);
|
||||||
const activity = computed(() => snapshot.value?.activity ?? []);
|
const activity = computed(() => snapshot.value?.activity ?? []);
|
||||||
|
|
||||||
const expiredAccounts = computed(() =>
|
const blockedAccounts = computed(() =>
|
||||||
accounts.value.filter((item) => ["expired", "revoked"].includes(item.authState)),
|
accounts.value.filter((item) => ["expired", "revoked", "challenge_required"].includes(item.authState)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const subsystemCards = computed(() => {
|
const subsystemCards = computed(() => {
|
||||||
@@ -159,23 +161,23 @@ const subsystemCards = computed(() => {
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>账号健康看板</h3>
|
<h3>账号健康看板</h3>
|
||||||
<p>这里只展示已经确认过期、需要重新授权的账号。</p>
|
<p>这里只展示已经确认过期、被风控或需要人工处理的账号。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="expiredAccounts.length > 0" class="info-list">
|
<div v-if="blockedAccounts.length > 0" class="info-list">
|
||||||
<div v-for="account in expiredAccounts" :key="account.id" class="info-row record-row">
|
<div v-for="account in blockedAccounts" :key="account.id" class="info-row record-row">
|
||||||
<div class="info-stack">
|
<div class="info-stack">
|
||||||
<span class="title">{{ account.displayName }}</span>
|
<span class="title">{{ account.displayName }}</span>
|
||||||
<span class="subtitle">{{ translatePlatform(account.platform) }} · <span class="mono-text">{{ formatUid(account.platformUid) }}</span></span>
|
<span class="subtitle">{{ translatePlatform(account.platform) }} · <span class="mono-text">{{ formatUid(account.platformUid) }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-stack" style="align-items: flex-end;">
|
<div class="info-stack" style="align-items: flex-end;">
|
||||||
<StatusBadge :tone="healthTone(account.health)" :label="expiredAccountLabel(account.authState)" />
|
<StatusBadge :tone="healthTone(account.health)" :label="blockedAccountLabel(account)" />
|
||||||
<span class="subtitle" style="margin-top: 6px;">{{ formatRelativeTime(account.lastVerifiedAt ?? account.lastSyncAt) }}</span>
|
<span class="subtitle" style="margin-top: 6px;">{{ formatRelativeTime(account.lastVerifiedAt ?? account.lastSyncAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<p>当前没有已过期账号。</p>
|
<p>当前没有需要人工处理的阻断账号。</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user