feat(monitoring): dispatch monitoring tasks to desktop via AMQP outbox

Replace SSE /desktop/events with priority AMQP dispatch for monitoring
runs, and add phase1/phase2 infrastructure so desktop workers can lease,
resume, report, skip, and cancel monitoring tasks over the existing
dispatch WebSocket.

- Add monitoring collect outbox worker and phase2 desktop task fields
- Add /desktop/monitoring/tasks/{lease,resume,result,skip,cancel} routes
- Introduce execution-devtools and network-observer on desktop runtime
- Refactor runtime-controller, LoginView, and doubao adapter for the new flow
- Add channelName column to tracking views

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 00:24:21 +08:00
parent 749b6b99cd
commit 4142c53fa6
57 changed files with 7897 additions and 985 deletions
+577 -110
View File
@@ -1,10 +1,16 @@
import type { JsonValue, MonitoringSourceItem } from "@geo/shared-types";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { ensureViewLoaded, normalizeText } from "./common";
import type { JsonValue, MonitoringSourceItem } from "@geo/shared-types";
import type { Session } from "electron/main";
import type { Page as PlaywrightPage } from "playwright-core";
import { normalizeText } from "./common";
import type { MonitorAdapter } from "./base";
const DOUBAO_APP_ID = "497858";
const DOUBAO_BOT_ID = "7338286299411103781";
const DOUBAO_HOME_URL = "https://www.doubao.com/";
const DOUBAO_CHAT_URL = "https://www.doubao.com/chat/completion";
const DOUBAO_REFERER = "https://www.doubao.com/chat/";
@@ -63,6 +69,7 @@ type DoubaoStreamEvent = {
type DoubaoStreamSummary = {
answer: string | null;
reasoning: string | null;
provider_request_id: string | null;
request_id: string | null;
conversation_id: string | null;
@@ -72,6 +79,12 @@ type DoubaoStreamSummary = {
error_message: string | null;
};
const ANSWER_EVENT_WHITELIST = new Set([
"STREAM_CHUNK",
"STREAM_MSG_NOTIFY",
"FULL_MSG_NOTIFY",
]);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -326,13 +339,30 @@ function collectSources(
}
}
function collectAnswerFragments(payload: unknown, result: string[] = []): string[] {
type AnswerFragment = {
text: string;
kind: "answer" | "reasoning";
};
type AnswerWalkContext = {
inReasoning: boolean;
};
function isReasoningIcon(value: unknown): boolean {
return typeof value === "string" && value.includes("Deep_Think");
}
function collectAnswerFragments(
payload: unknown,
result: AnswerFragment[] = [],
context: AnswerWalkContext = { inReasoning: false },
): AnswerFragment[] {
if (payload == null) {
return result;
}
if (Array.isArray(payload)) {
for (const item of payload) {
collectAnswerFragments(item, result);
collectAnswerFragments(item, result, context);
}
return result;
}
@@ -340,19 +370,32 @@ function collectAnswerFragments(payload: unknown, result: string[] = []): string
return result;
}
const textBlockText = readNestedString(payload, ["text_block", "text"]);
if (textBlockText) {
result.push(textBlockText);
let nextContext = context;
if (typeof payload.block_type === "number" && payload.block_type !== 10000) {
// Non-answer blocks (search queries, status cards, etc.) — skip entirely.
return result;
}
if (isReasoningIcon((payload as { icon_url?: unknown }).icon_url)) {
nextContext = { inReasoning: true };
}
const textBlock = payload.text_block;
if (isRecord(textBlock)) {
const text = normalizeOptionalString(textBlock.text);
if (text) {
const reasoning = context.inReasoning || isReasoningIcon(textBlock.icon_url);
result.push({ text, kind: reasoning ? "reasoning" : "answer" });
}
}
const deltaText = normalizeOptionalString(payload.delta);
if (deltaText) {
result.push(deltaText);
result.push({ text: deltaText, kind: nextContext.inReasoning ? "reasoning" : "answer" });
}
const nestedDeltaText = readNestedString(payload, ["delta", "text"]);
if (nestedDeltaText) {
result.push(nestedDeltaText);
result.push({ text: nestedDeltaText, kind: nextContext.inReasoning ? "reasoning" : "answer" });
}
const assistantText =
@@ -360,12 +403,13 @@ function collectAnswerFragments(payload: unknown, result: string[] = []): string
normalizeOptionalString(payload.message_type) === "assistant") &&
normalizeOptionalString(payload.text);
if (assistantText) {
result.push(assistantText);
result.push({ text: assistantText, kind: nextContext.inReasoning ? "reasoning" : "answer" });
}
for (const value of Object.values(payload)) {
for (const [key, value] of Object.entries(payload)) {
if (key === "text_block") continue;
if (Array.isArray(value) || isRecord(value)) {
collectAnswerFragments(value, result);
collectAnswerFragments(value, result, nextContext);
}
}
@@ -423,6 +467,7 @@ function formatDoubaoStreamError(payload: unknown): string {
async function parseDoubaoStream(response: Response): Promise<DoubaoStreamSummary> {
const summary: DoubaoStreamSummary = {
answer: null,
reasoning: null,
provider_request_id: null,
request_id: null,
conversation_id: null,
@@ -466,9 +511,15 @@ async function parseDoubaoStream(response: Response): Promise<DoubaoStreamSummar
summary.request_id = findFirstStringByKey(parsed.payload, ["message_id", "local_message_id", "reply_id"]);
}
const fragments = collectAnswerFragments(parsed.payload);
for (const fragment of fragments) {
summary.answer = mergeAnswerText(summary.answer, fragment);
if (ANSWER_EVENT_WHITELIST.has(parsed.event.toUpperCase())) {
const fragments = collectAnswerFragments(parsed.payload);
for (const fragment of fragments) {
if (fragment.kind === "answer") {
summary.answer = mergeAnswerText(summary.answer, fragment.text);
} else {
summary.reasoning = mergeAnswerText(summary.reasoning, fragment.text);
}
}
}
collectSources(parsed.payload, summary.search_results, summary.citations);
@@ -573,101 +624,496 @@ function buildDoubaoRequestBody(questionText: string, runtimeState: DoubaoRuntim
};
}
function parseRuntimeState(value: unknown): DoubaoRuntimeState | null {
const DOUBAO_RUNTIME_STATE_FIELDS: Array<keyof DoubaoRuntimeState> = [
"fp",
"ms_token",
"device_id",
"web_id",
"tea_uuid",
];
const DOUBAO_RUNTIME_STATE_TIMEOUT_MS = 15_000;
const DOUBAO_RUNTIME_STATE_POLL_INTERVAL_MS = 500;
const DOUBAO_RUNTIME_STATE_DISK_SCAN_FILE_LIMIT = 8;
const DOUBAO_RUNTIME_STATE_DISK_SCAN_MAX_BYTES = 512_000;
const DOUBAO_XMST_PATTERN = /xmst[\x00-\x20\x7f-\xff]{0,8}([A-Za-z0-9_-]{20,}={0,2})/g;
const DOUBAO_TEA_TOKENS_PATTERN = /__tea_cache_tokens_497858(?:u)?[\x00-\x20\x7f-\xff]{0,8}(\{[^{}]{0,500}\})/g;
const DOUBAO_SAMANTHA_WEB_ID_PATTERN = /samantha_web_web_id[\x00-\x20\x7f-\xff]{0,8}(\{[^{}]{0,500}\})/g;
const DOUBAO_SLARDAR_FLOW_PATTERN = /SLARDARflow_web[\x00-\x20\x7f-\xff]{0,16}(JTdC[A-Za-z0-9+/=_-]{40,}JTdE)/g;
function parseRuntimeState(value: unknown): {
state: DoubaoRuntimeState | null;
missing: Array<keyof DoubaoRuntimeState>;
readError: string | null;
} {
if (!isRecord(value)) {
return null;
return { state: null, missing: [...DOUBAO_RUNTIME_STATE_FIELDS], readError: null };
}
const readError = normalizeOptionalString(value.error);
const fp = normalizeOptionalString(value.fp);
const msToken = normalizeOptionalString(value.ms_token);
const deviceID = normalizeOptionalString(value.device_id);
const webID = normalizeOptionalString(value.web_id);
const teaUUID = normalizeOptionalString(value.tea_uuid);
if (!fp || !msToken || !deviceID || !webID || !teaUUID) {
return null;
const missing: Array<keyof DoubaoRuntimeState> = [];
if (!fp) missing.push("fp");
if (!msToken) missing.push("ms_token");
if (!deviceID) missing.push("device_id");
if (!webID) missing.push("web_id");
if (!teaUUID) missing.push("tea_uuid");
if (missing.length > 0 || !fp || !msToken || !deviceID || !webID || !teaUUID) {
return { state: null, missing, readError };
}
return {
fp,
ms_token: msToken,
device_id: deviceID,
web_id: webID,
tea_uuid: teaUUID,
state: {
fp,
ms_token: msToken,
device_id: deviceID,
web_id: webID,
tea_uuid: teaUUID,
},
missing,
readError,
};
}
async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
await new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(new Error("adapter_aborted"));
return;
}
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(new Error("adapter_aborted"));
};
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function ensurePageOnDoubaoChat(
page: PlaywrightPage,
signal: AbortSignal,
): Promise<void> {
if (signal.aborted) {
throw new Error("adapter_aborted");
}
const currentURL = safePlaywrightPageURL(page);
if (!currentURL.startsWith("https://www.doubao.com/")) {
await page.goto(DOUBAO_HOME_URL, { waitUntil: "domcontentloaded" });
if (signal.aborted) {
throw new Error("adapter_aborted");
}
}
if (!safePlaywrightPageURL(page).startsWith("https://www.doubao.com/chat")) {
await page.goto(DOUBAO_REFERER, { waitUntil: "domcontentloaded" });
}
await page.waitForLoadState("domcontentloaded");
await page.waitForLoadState("networkidle", { timeout: 3_000 }).catch(() => undefined);
if (signal.aborted) {
throw new Error("adapter_aborted");
}
}
function safePlaywrightPageURL(page: PlaywrightPage): string {
try {
return page.url();
} catch {
return "";
}
}
const DOUBAO_READ_RUNTIME_STATE_SCRIPT = () => {
try {
const safeParse = (value) => {
if (!value) return null;
try {
return JSON.parse(value);
} catch {
return value;
}
};
const readStorage = (name) => {
try {
return window.localStorage.getItem(name) || window.sessionStorage.getItem(name);
} catch {
return null;
}
};
const decodeFlowState = (value) => {
if (typeof value !== "string" || !value.trim()) {
return null;
}
let current = value.trim();
try {
current = window.atob(current);
} catch {
// Keep original text when it is not base64-encoded.
}
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const next = decodeURIComponent(current);
if (next === current) {
break;
}
current = next;
} catch {
break;
}
}
return safeParse(current);
};
const samanthaState = safeParse(window.localStorage.getItem("samantha_web_web_id"));
const teaState = safeParse(window.localStorage.getItem("__tea_cache_tokens_497858"));
const teaSDKState = safeParse(window.localStorage.getItem("__tea_sdk_ab_version_497858"));
const flowState = decodeFlowState(readStorage("SLARDARflow_web"));
const pick = (source, paths) => {
for (const path of paths) {
let current = source;
let ok = true;
for (const key of path) {
if (!current || typeof current !== "object" || !(key in current)) {
ok = false;
break;
}
current = current[key];
}
if (ok && typeof current === "string" && current.trim()) {
return current.trim();
}
}
return null;
};
const readCookie = (name) => {
const segments = document.cookie.split(";");
for (const segment of segments) {
const [rawName, ...rest] = segment.trim().split("=");
if (rawName === name) {
const joined = rest.join("=").trim();
return joined || null;
}
}
return null;
};
return {
fp: readCookie("s_v_web_id"),
ms_token: readStorage("xmst") || readStorage("msToken"),
device_id:
pick(flowState, [["deviceId"], ["device_id"], ["id"]]) ||
pick(samanthaState, [["device_id"], ["deviceId"], ["web_id"], ["webId"], ["id"]]),
web_id:
pick(teaState, [["web_id"], ["webId"], ["id"]]) ||
pick(samanthaState, [["web_id"], ["webId"], ["device_id"], ["deviceId"], ["id"]]),
tea_uuid:
pick(teaState, [["user_unique_id"], ["userUniqueId"], ["tea_uuid"], ["teaUuid"], ["uuid"], ["web_id"]]) ||
pick(teaSDKState, [["uuid"], ["user_unique_id"], ["userUniqueId"]]),
};
} catch (error) {
return {
error: error instanceof Error ? error.message : "runtime_state_read_failed",
};
}
};
function lastPatternMatch(text: string, pattern: RegExp): string | null {
pattern.lastIndex = 0;
let last: string | null = null;
let matched: RegExpExecArray | null = null;
while ((matched = pattern.exec(text)) !== null) {
const candidate = normalizeOptionalString(matched[1]);
if (candidate) {
last = candidate;
}
}
pattern.lastIndex = 0;
return last;
}
function readFileTailAsLatin1(filePath: string, maxBytes = DOUBAO_RUNTIME_STATE_DISK_SCAN_MAX_BYTES): string {
const buffer = readFileSync(filePath);
if (buffer.length <= maxBytes) {
return buffer.toString("latin1");
}
return buffer.subarray(buffer.length - maxBytes).toString("latin1");
}
function parseDoubaoRecord(value: string | null): Record<string, unknown> | null {
if (!value) {
return null;
}
const parsed = safeParseJSON(value);
return isRecord(parsed) ? parsed : null;
}
function decodeDoubaoFlowState(value: string | null): Record<string, unknown> | null {
const raw = normalizeOptionalString(value);
if (!raw) {
return null;
}
try {
let decoded = Buffer.from(raw, "base64").toString("utf8");
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const next = decodeURIComponent(decoded);
if (next === decoded) {
break;
}
decoded = next;
} catch {
break;
}
}
const parsed = safeParseJSON(decoded);
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
async function flushDoubaoSessionPersistence(session: Session): Promise<void> {
const maybeFlush = (session as Session & {
flushStorageData?: () => Promise<void> | void;
}).flushStorageData;
if (typeof maybeFlush === "function") {
try {
await Promise.resolve(maybeFlush.call(session));
} catch {
// Ignore persistence flush errors and continue with the best available state.
}
}
try {
await Promise.resolve(session.cookies.flushStore());
} catch {
// Ignore cookie flush errors and continue with the best available state.
}
}
async function readDoubaoCookieValue(
session: Session,
name: string,
): Promise<string | null> {
let cookies: Awaited<ReturnType<Session["cookies"]["get"]>> = [];
try {
cookies = await Promise.resolve(session.cookies.get({ url: DOUBAO_HOME_URL }));
} catch {
cookies = [];
}
for (const cookie of cookies) {
if (cookie.name !== name) {
continue;
}
const value = normalizeOptionalString(cookie.value);
if (value) {
return value;
}
}
return null;
}
function readDoubaoPersistedStorageState(storagePath: string | null): Partial<DoubaoRuntimeState> {
if (!storagePath) {
return {};
}
const leveldbPath = join(storagePath, "Local Storage", "leveldb");
let candidates: string[] = [];
try {
candidates = readdirSync(leveldbPath)
.map((entry) => join(leveldbPath, entry))
.filter((entry) => /\.(?:log|ldb)$/i.test(entry))
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)
.slice(0, DOUBAO_RUNTIME_STATE_DISK_SCAN_FILE_LIMIT);
} catch {
return {};
}
let xmst: string | null = null;
let teaState: Record<string, unknown> | null = null;
let samanthaState: Record<string, unknown> | null = null;
let flowState: Record<string, unknown> | null = null;
for (const filePath of candidates) {
let text = "";
try {
text = readFileTailAsLatin1(filePath);
} catch {
continue;
}
if (!text) {
continue;
}
xmst ||= lastPatternMatch(text, DOUBAO_XMST_PATTERN);
teaState ||= parseDoubaoRecord(lastPatternMatch(text, DOUBAO_TEA_TOKENS_PATTERN));
samanthaState ||= parseDoubaoRecord(lastPatternMatch(text, DOUBAO_SAMANTHA_WEB_ID_PATTERN));
flowState ||= decodeDoubaoFlowState(lastPatternMatch(text, DOUBAO_SLARDAR_FLOW_PATTERN));
if (xmst && teaState && samanthaState && flowState) {
break;
}
}
const deviceID =
getDirectString(flowState ?? {}, ["deviceId", "device_id", "id"]) ??
getDirectString(samanthaState ?? {}, ["device_id", "deviceId", "web_id", "webId", "id"]);
const webID =
getDirectString(teaState ?? {}, ["web_id", "webId", "id"]) ??
getDirectString(samanthaState ?? {}, ["web_id", "webId", "device_id", "deviceId", "id"]);
const teaUUID =
getDirectString(teaState ?? {}, ["user_unique_id", "userUniqueId", "tea_uuid", "teaUuid", "uuid", "web_id", "webId"]) ??
webID;
return {
ms_token: xmst ?? undefined,
device_id: deviceID ?? undefined,
web_id: webID ?? undefined,
tea_uuid: teaUUID ?? undefined,
};
}
function mergeDoubaoRuntimeStateSources(...sources: Array<Record<string, unknown> | null | undefined>): Record<string, unknown> {
const merged: Record<string, unknown> = {};
for (const field of DOUBAO_RUNTIME_STATE_FIELDS) {
for (const source of sources) {
const value = normalizeOptionalString(source?.[field]);
if (value) {
merged[field] = value;
break;
}
}
}
for (const source of sources) {
const error = normalizeOptionalString(source?.error);
if (error) {
merged.error = error;
break;
}
}
return merged;
}
async function readDoubaoRuntimeStateFallback(session: Session): Promise<Record<string, unknown>> {
const [fp, persisted] = await Promise.all([
readDoubaoCookieValue(session, "s_v_web_id"),
Promise.resolve(readDoubaoPersistedStorageState(session.storagePath)),
]);
return {
...(fp ? { fp } : {}),
...persisted,
};
}
async function loadDoubaoRuntimeState(context: Parameters<MonitorAdapter["query"]>[0]): Promise<DoubaoRuntimeState> {
if (!context.view) {
throw new Error("doubao_view_required");
if (!context.playwright) {
throw new Error("doubao_playwright_required");
}
const view = context.view;
const page = context.playwright.page;
context.reportProgress("doubao.bootstrap_view");
await ensureViewLoaded(view, DOUBAO_REFERER, context.signal);
await ensurePageOnDoubaoChat(page, context.signal);
await flushDoubaoSessionPersistence(context.session);
context.reportProgress("doubao.read_runtime_state");
const state = await view.webContents.executeJavaScript(
`(() => {
try {
const safeParse = (value) => {
if (!value) return null;
try {
return JSON.parse(value);
} catch {
return value;
}
};
const samanthaState = safeParse(window.localStorage.getItem("samantha_web_web_id"));
const teaState = safeParse(window.localStorage.getItem("__tea_cache_tokens_497858"));
const pick = (source, paths) => {
for (const path of paths) {
let current = source;
let ok = true;
for (const key of path) {
if (!current || typeof current !== "object" || !(key in current)) {
ok = false;
break;
}
current = current[key];
}
if (ok && typeof current === "string" && current.trim()) {
return current.trim();
}
}
return null;
};
const readCookie = (name) => {
const segments = document.cookie.split(";");
for (const segment of segments) {
const [rawName, ...rest] = segment.trim().split("=");
if (rawName === name) {
const joined = rest.join("=").trim();
return joined || null;
}
}
return null;
};
return {
fp: readCookie("s_v_web_id"),
ms_token: window.localStorage.getItem("xmst"),
device_id: pick(samanthaState, [["web_id"], ["device_id"], ["deviceId"], ["id"]]),
web_id: pick(teaState, [["web_id"], ["webId"], ["id"]]),
tea_uuid: pick(teaState, [["web_id"], ["tea_uuid"], ["teaUuid"], ["user_unique_id"], ["uuid"]]),
};
} catch (error) {
return {
error: error instanceof Error ? error.message : "runtime_state_read_failed",
};
}
})();`,
true,
);
const runtimeState = parseRuntimeState(state);
if (!runtimeState) {
throw new Error("doubao_runtime_state_missing");
const deadline = Date.now() + DOUBAO_RUNTIME_STATE_TIMEOUT_MS;
let lastMissing: Array<keyof DoubaoRuntimeState> = [...DOUBAO_RUNTIME_STATE_FIELDS];
let lastReadError: string | null = null;
const persistedFallback = await readDoubaoRuntimeStateFallback(context.session).catch(() => ({}));
while (true) {
if (context.signal?.aborted) {
throw new Error("adapter_aborted");
}
const raw = await page.evaluate(DOUBAO_READ_RUNTIME_STATE_SCRIPT);
const merged = mergeDoubaoRuntimeStateSources(raw, persistedFallback);
const { state, missing, readError } = parseRuntimeState(merged);
if (state) {
return state;
}
lastMissing = missing;
lastReadError = readError;
if (Date.now() >= deadline) {
break;
}
await sleep(DOUBAO_RUNTIME_STATE_POLL_INTERVAL_MS, context.signal);
}
return runtimeState;
const detail = lastReadError
? `read_error=${lastReadError}`
: `missing=${lastMissing.join(",") || "unknown"}`;
throw new Error(`doubao_runtime_state_missing:${detail}`);
}
const DOUBAO_COMPLETION_FETCH_SCRIPT = async (
{ url, body, referer }: { url: string; body: Record<string, unknown>; referer: string },
) => {
try {
const res = await fetch(url, {
method: "POST",
credentials: "include",
referrer: referer,
headers: {
Accept: "text/event-stream",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const text = await res.text();
return { ok: res.ok, status: res.status, body: text };
} catch (error) {
return {
ok: false,
status: 0,
body: "",
error: error instanceof Error ? error.message : "doubao_completion_fetch_failed",
};
}
};
type DoubaoCompletionResult = {
ok: boolean;
status: number;
body: string;
error?: string;
};
async function fetchDoubaoCompletion(
page: PlaywrightPage,
url: string,
body: Record<string, unknown>,
): Promise<DoubaoCompletionResult> {
let result: unknown;
try {
result = await page.evaluate(DOUBAO_COMPLETION_FETCH_SCRIPT, {
url,
body,
referer: DOUBAO_REFERER,
});
} catch (error) {
return {
ok: false,
status: 0,
body: "",
error: error instanceof Error ? error.message : "doubao_completion_fetch_eval_failed",
};
}
if (!isRecord(result)) {
return { ok: false, status: 0, body: "", error: "doubao_completion_fetch_malformed" };
}
return {
ok: Boolean(result.ok),
status: typeof result.status === "number" ? result.status : 0,
body: typeof result.body === "string" ? result.body : "",
error: normalizeOptionalString(result.error) ?? undefined,
};
}
function extractQuestionText(payload: Record<string, unknown>): string {
@@ -716,50 +1162,70 @@ function buildAdapterError(
export const doubaoAdapter: MonitorAdapter = {
provider: "doubao",
executionMode: "view",
executionMode: "playwright",
async query(context, payload) {
if (!context.view) {
if (!context.playwright) {
return {
status: "failed",
summary: "豆包监测缺少页面执行上下文。",
summary: "豆包监测缺少 Playwright 执行上下文。",
error: buildAdapterError(
"doubao_view_required",
"doubao_view_required",
"doubao_playwright_required",
"doubao_playwright_required",
),
};
}
const questionText = extractQuestionText(payload);
const runtimeState = await loadDoubaoRuntimeState(context);
let runtimeState: DoubaoRuntimeState;
try {
runtimeState = await loadDoubaoRuntimeState(context);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.startsWith("doubao_runtime_state_missing")) {
const detail = message.slice("doubao_runtime_state_missing:".length) || "unknown";
return {
status: "failed",
summary: `豆包运行时上下文未就绪(${detail}),请稍后重试或在桌面端打开豆包页面完成初始化。`,
error: buildAdapterError(
"doubao_runtime_state_missing",
`doubao_runtime_state_missing:${detail}`,
),
};
}
throw error;
}
const requestBody = buildDoubaoRequestBody(questionText, runtimeState);
context.reportProgress("doubao.query");
const response = await context.session.fetch(buildDoubaoRequestUrl(runtimeState), {
method: "POST",
headers: {
Accept: "text/event-stream",
"Content-Type": "application/json",
referer: DOUBAO_REFERER,
},
body: JSON.stringify(requestBody),
});
const fetchResult = await fetchDoubaoCompletion(
context.playwright.page,
buildDoubaoRequestUrl(runtimeState),
requestBody,
);
if (!response.ok) {
const bodyText = await response.text().catch(() => "");
const detail = normalizeText(bodyText)?.slice(0, 240);
if (fetchResult.error) {
return {
status: "failed",
summary: detail ? `豆包请求失败:${detail}` : `豆包请求失败${response.status})。`,
summary: `豆包请求发送失败${fetchResult.error}`,
error: buildAdapterError("doubao_request_failed", fetchResult.error),
};
}
if (!fetchResult.ok) {
const detail = normalizeText(fetchResult.body)?.slice(0, 240);
return {
status: "failed",
summary: detail ? `豆包请求失败:${detail}` : `豆包请求失败(${fetchResult.status})。`,
error: buildAdapterError(
"doubao_request_failed",
detail || `doubao_request_failed_${response.status}`,
{ http_status: response.status },
detail || `doubao_request_failed_${fetchResult.status}`,
{ http_status: fetchResult.status },
),
};
}
context.reportProgress("doubao.parse_stream");
const streamSummary = await parseDoubaoStream(response);
const streamSummary = await parseDoubaoStream(new Response(fetchResult.body));
if (streamSummary.error_message) {
return {
status: "failed",
@@ -786,6 +1252,7 @@ export const doubaoAdapter: MonitorAdapter = {
request_id: streamSummary.request_id ?? streamSummary.conversation_id,
conversation_id: streamSummary.conversation_id,
answer: streamSummary.answer,
reasoning: streamSummary.reasoning,
citation_count: streamSummary.citations.length,
search_result_count: streamSummary.search_results.length,
event_count: streamSummary.event_count,