feat: release v0.74.0 - Chrome Bridge 浏览器自动化集成

🎉 重大功能
- Chrome Bridge: 完整的浏览器自动化控制系统
- 零配置自动连接:扩展自动加载和配置
- AI 原生集成:作为 MCP 工具支持自然语言控制
- 支持导航、页面读取、元素交互、表单操作等

🐛 Bug 修复
- WebSocket 路由修复 (Axum 路径参数语法)
- Chrome 扩展存储清理
- 扩展重复注入防护
- 剪贴板权限添加

🔧 代码质量
- 修复 33+ Clippy 警告
- 所有 259 个测试通过
- ESLint 无警告

📝 文档
- Chrome Bridge 使用指南
- API 技术文档
- 快速参考卡片
This commit is contained in:
coso
2026-02-28 16:05:36 +08:00
parent 0686737331
commit cd95cac72f
108 changed files with 18916 additions and 1921 deletions
+238
View File
@@ -0,0 +1,238 @@
/**
* 小说编排 API
*
* 对应 src-tauri/src/commands/novel_cmd.rs
*/
import { invoke } from "@tauri-apps/api/core";
import {
normalizeNovelSettingsEnvelope,
type NovelSettingsEnvelope,
type NovelSettingsV1,
} from "@/lib/novel-settings/types";
export interface NovelProject {
id: string;
title: string;
theme?: string | null;
target_words: number;
status: string;
current_word_count: number;
metadata_json?: Record<string, unknown> | null;
created_at: number;
updated_at: number;
}
export interface NovelSettingsRecord {
id: string;
project_id: string;
settings_json: NovelSettingsEnvelope;
version: number;
created_at: number;
}
export interface NovelOutlineRecord {
id: string;
project_id: string;
outline_markdown: string;
outline_json?: Record<string, unknown> | null;
version: number;
created_at: number;
}
export interface NovelCharacterRecord {
id: string;
project_id: string;
name: string;
role_type: string;
card_json: Record<string, unknown>;
version: number;
created_at: number;
updated_at: number;
}
export interface NovelChapterRecord {
id: string;
project_id: string;
chapter_no: number;
title: string;
content: string;
word_count: number;
status: string;
quality_score?: number | null;
metadata_json?: Record<string, unknown> | null;
created_at: number;
updated_at: number;
}
export interface NovelGenerationRun {
id: string;
project_id: string;
mode: string;
input_snapshot_json?: Record<string, unknown> | null;
output_snapshot_json?: Record<string, unknown> | null;
provider?: string | null;
model?: string | null;
latency_ms?: number | null;
token_usage_json?: Record<string, unknown> | null;
result_status: string;
error_message?: string | null;
created_at: number;
}
export interface NovelConsistencyIssue {
level: "info" | "warn" | "error" | string;
code: string;
message: string;
details?: Record<string, unknown>;
}
export interface NovelConsistencyCheck {
id: string;
project_id: string;
chapter_id: string;
issues: NovelConsistencyIssue[];
score: number;
created_at: number;
}
export interface NovelProjectSnapshot {
project: NovelProject;
latest_settings?: NovelSettingsRecord | null;
latest_outline?: NovelOutlineRecord | null;
characters: NovelCharacterRecord[];
chapters: NovelChapterRecord[];
latest_consistency?: NovelConsistencyCheck | null;
}
export interface NovelGenerateResult {
mode: string;
run_id: string;
content: string;
chapter?: NovelChapterRecord | null;
}
export interface CreateNovelProjectRequest {
id?: string;
title: string;
theme?: string;
target_words?: number;
metadata_json?: Record<string, unknown>;
settings_json?: NovelSettingsEnvelope | NovelSettingsV1 | Record<string, unknown>;
}
export interface UpdateNovelSettingsRequest {
project_id: string;
settings_json: NovelSettingsEnvelope | NovelSettingsV1 | Record<string, unknown>;
}
export interface NovelGenerateRequest {
project_id: string;
provider?: string;
model?: string;
temperature?: number;
max_tokens?: number;
}
export interface NovelGenerateChapterRequest extends NovelGenerateRequest {
chapter_no?: number;
force_overwrite?: boolean;
}
export interface NovelRewriteChapterRequest extends NovelGenerateRequest {
chapter_id: string;
instructions?: string;
}
export interface NovelPolishChapterRequest extends NovelGenerateRequest {
chapter_id: string;
focus?: string;
}
export interface NovelCheckConsistencyRequest {
project_id: string;
chapter_id: string;
}
export interface NovelListRunsRequest {
project_id: string;
limit?: number;
}
export async function createNovelProject(
request: CreateNovelProjectRequest,
): Promise<NovelProject> {
const normalizedRequest =
request.settings_json === undefined
? request
: {
...request,
settings_json: normalizeNovelSettingsEnvelope(request.settings_json),
};
return invoke("novel_create_project", { request: normalizedRequest });
}
export async function updateNovelSettings(
request: UpdateNovelSettingsRequest,
): Promise<NovelSettingsRecord> {
return invoke("novel_update_settings", {
request: {
...request,
settings_json: normalizeNovelSettingsEnvelope(request.settings_json),
},
});
}
export async function generateNovelOutline(
request: NovelGenerateRequest,
): Promise<NovelGenerateResult> {
return invoke("novel_generate_outline", { request });
}
export async function generateNovelCharacters(
request: NovelGenerateRequest,
): Promise<NovelGenerateResult> {
return invoke("novel_generate_characters", { request });
}
export async function generateNovelChapter(
request: NovelGenerateChapterRequest,
): Promise<NovelGenerateResult> {
return invoke("novel_generate_chapter", { request });
}
export async function continueNovelChapter(
request: NovelGenerateRequest,
): Promise<NovelGenerateResult> {
return invoke("novel_continue_chapter", { request });
}
export async function rewriteNovelChapter(
request: NovelRewriteChapterRequest,
): Promise<NovelGenerateResult> {
return invoke("novel_rewrite_chapter", { request });
}
export async function polishNovelChapter(
request: NovelPolishChapterRequest,
): Promise<NovelGenerateResult> {
return invoke("novel_polish_chapter", { request });
}
export async function checkNovelConsistency(
request: NovelCheckConsistencyRequest,
): Promise<NovelConsistencyCheck> {
return invoke("novel_check_consistency", { request });
}
export async function getNovelProjectSnapshot(
projectId: string,
): Promise<NovelProjectSnapshot> {
return invoke("novel_get_project_snapshot", { projectId });
}
export async function listNovelRuns(
request: NovelListRunsRequest,
): Promise<NovelGenerationRun[]> {
return invoke("novel_list_runs", { request });
}
+71
View File
@@ -0,0 +1,71 @@
import {
checkNovelConsistency,
continueNovelChapter,
generateNovelChapter,
type NovelChapterRecord,
type NovelConsistencyCheck,
} from "@/lib/api/novel";
export interface GenerateNextChapterFlowParams {
projectId: string;
hasExistingChapters: boolean;
provider?: string;
model?: string;
}
export interface GenerateNextChapterFlowResult {
chapter: NovelChapterRecord;
consistency?: NovelConsistencyCheck;
consistencyError?: string;
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
return "未知错误";
}
/**
* 章节半自动编排:生成下一章并尝试执行一致性检查。
* 一致性检查失败不会回滚章节,交由上层做软告警展示。
*/
export async function generateNextChapterWithConsistency(
params: GenerateNextChapterFlowParams,
): Promise<GenerateNextChapterFlowResult> {
const generationResult = params.hasExistingChapters
? await continueNovelChapter({
project_id: params.projectId,
provider: params.provider,
model: params.model,
})
: await generateNovelChapter({
project_id: params.projectId,
provider: params.provider,
model: params.model,
});
const chapter = generationResult.chapter;
if (!chapter) {
throw new Error("章节生成成功但未返回章节数据");
}
try {
const consistency = await checkNovelConsistency({
project_id: params.projectId,
chapter_id: chapter.id,
});
return {
chapter,
consistency,
};
} catch (error) {
return {
chapter,
consistencyError: getErrorMessage(error),
};
}
}
+166
View File
@@ -0,0 +1,166 @@
import type { NovelProjectSnapshot } from "@/lib/api/novel";
import type { NovelSettingsEnvelope } from "@/lib/novel-settings/types";
export type NovelPipelineStage =
| "setup"
| "outline"
| "characters"
| "chapters"
| "qa"
| "publish";
export type NovelStageStatus = "locked" | "ready" | "done" | "warning";
export type NovelPrimaryActionKey =
| "save-settings"
| "generate-outline"
| "generate-characters"
| "generate-next-chapter"
| "run-consistency"
| "open-publish";
export interface NovelPipelinePrimaryAction {
key: NovelPrimaryActionKey;
label: string;
disabled?: boolean;
}
export interface NovelPipelineState {
currentStage: NovelPipelineStage;
stageStatus: Record<NovelPipelineStage, NovelStageStatus>;
primaryAction: NovelPipelinePrimaryAction;
}
function isSetupReady(settings: NovelSettingsEnvelope | null): boolean {
if (!settings) {
return false;
}
const { data } = settings;
return (
data.genres.length > 0 &&
data.oneLinePitch.trim().length > 0 &&
data.mainCharacter.name.trim().length > 0
);
}
function getCurrentStage(
stageStatus: Record<NovelPipelineStage, NovelStageStatus>,
): NovelPipelineStage {
const order: NovelPipelineStage[] = [
"setup",
"outline",
"characters",
"chapters",
"qa",
"publish",
];
for (const stage of order) {
if (stageStatus[stage] === "ready" || stageStatus[stage] === "warning") {
return stage;
}
}
return "publish";
}
function getPrimaryAction(
stage: NovelPipelineStage,
stageStatus: Record<NovelPipelineStage, NovelStageStatus>,
): NovelPipelinePrimaryAction {
switch (stage) {
case "setup":
return {
key: "save-settings",
label: "保存创作设定",
disabled: stageStatus.setup === "locked",
};
case "outline":
return {
key: "generate-outline",
label: stageStatus.outline === "done" ? "重新生成大纲" : "生成大纲",
disabled: stageStatus.outline === "locked",
};
case "characters":
return {
key: "generate-characters",
label:
stageStatus.characters === "done" ? "更新角色阵列" : "生成角色阵列",
disabled: stageStatus.characters === "locked",
};
case "chapters":
return {
key: "generate-next-chapter",
label: "生成下一章(含自动检查)",
disabled: stageStatus.chapters === "locked",
};
case "qa":
return {
key: "run-consistency",
label: "执行一致性检查",
disabled: stageStatus.qa === "locked",
};
case "publish":
return {
key: "open-publish",
label: "前往发布页",
disabled: stageStatus.publish === "locked",
};
default:
return {
key: "save-settings",
label: "保存创作设定",
};
}
}
export function resolveNovelPipelineState(params: {
snapshot: NovelProjectSnapshot | null;
settings: NovelSettingsEnvelope | null;
}): NovelPipelineState {
const { snapshot, settings } = params;
const setupDone = isSetupReady(settings);
const hasOutline = Boolean(snapshot?.latest_outline);
const hasCharacters = (snapshot?.characters.length ?? 0) > 0;
const hasChapters = (snapshot?.chapters.length ?? 0) > 0;
const latestConsistency = snapshot?.latest_consistency;
const consistencyScore = latestConsistency?.score;
const stageStatus: Record<NovelPipelineStage, NovelStageStatus> = {
setup: setupDone ? "done" : "ready",
outline: setupDone ? (hasOutline ? "done" : "ready") : "locked",
characters:
setupDone && hasOutline
? hasCharacters
? "done"
: "ready"
: "locked",
chapters:
setupDone && hasOutline && hasCharacters
? hasChapters
? "done"
: "ready"
: "locked",
qa:
setupDone && hasOutline && hasCharacters && hasChapters
? consistencyScore === undefined
? "ready"
: consistencyScore < 60
? "warning"
: "done"
: "locked",
publish:
setupDone && hasOutline && hasCharacters && hasChapters ? "ready" : "locked",
};
const currentStage = getCurrentStage(stageStatus);
return {
currentStage,
stageStatus,
primaryAction: getPrimaryAction(currentStage, stageStatus),
};
}
+418
View File
@@ -0,0 +1,418 @@
/**
* 小说创作设定强类型定义(V1)
*
* 参考: /Users/coso/Documents/dev/js/ainovel/src/components/novel-settings/types.ts
*/
export const NOVEL_SETTINGS_SCHEMA_VERSION = 1;
const randomId = () => Math.random().toString(36).slice(2) + Date.now().toString(36);
export const ALL_NOVEL_GENRES = [
"玄幻",
"仙侠",
"奇幻",
"武侠",
"都市",
"言情",
"科幻",
"系统文",
"后宫",
"无限流",
"悬疑",
"历史",
"军事",
"游戏",
"末世",
"穿越",
"重生",
"网游",
"竞技",
"机甲",
"星际",
"灵异",
"恐怖",
"宫斗",
"种田",
"娱乐圈",
"盗墓",
"洪荒",
] as const;
export type NovelGenre = (typeof ALL_NOVEL_GENRES)[number];
export interface MainCharacter {
name: string;
gender: string;
age: string;
personality: string;
}
export interface SideCharacter {
id: string;
name: string;
nickname: string;
gender: string;
age: string;
relationship: string;
relationshipCustom: string;
personalityTags: string[];
background: string;
abilities: string;
role: string;
arc: string;
arcCustom: string;
}
export interface Antagonist extends SideCharacter {
motive: string;
fate: string;
}
export interface WorldDetails {
powerSystem: string;
factions: string;
historyEvents: string;
importantLocations: string;
cultureAndTaboos: string;
}
export interface PlotBeat {
id: string;
title: string;
detail: string;
}
export interface WritingStyle {
narration: string;
tones: string[];
cheatLevel: string;
focusAreas: string[];
wordsPerChapter: number;
temperature: number;
}
export interface TabooRule {
id: string;
content: string;
}
export interface ReferenceWork {
id: string;
title: string;
inspiration: string;
}
export interface NovelSettingsV1 {
genres: NovelGenre[];
oneLinePitch: string;
mainCharacter: MainCharacter;
sideCharacters: SideCharacter[];
antagonists: Antagonist[];
worldSummary: string;
conflictTheme: string;
worldDetails: WorldDetails;
opening: string;
middleBeats: PlotBeat[];
endingType: string;
subplots: PlotBeat[];
writingStyle: WritingStyle;
totalWords: number;
chapterWords: number;
nsfw: boolean;
systemNovel: boolean;
harem: boolean;
taboos: TabooRule[];
references: ReferenceWork[];
}
export interface NovelSettingsEnvelope {
schema_version: number;
data: NovelSettingsV1;
}
export const createEmptySideCharacter = (): SideCharacter => ({
id: randomId(),
name: "",
nickname: "",
gender: "男",
age: "",
relationship: "",
relationshipCustom: "",
personalityTags: [],
background: "",
abilities: "",
role: "",
arc: "",
arcCustom: "",
});
export const createEmptyAntagonist = (): Antagonist => ({
...createEmptySideCharacter(),
motive: "",
fate: "",
});
export const createEmptyPlotBeat = (): PlotBeat => ({
id: randomId(),
title: "",
detail: "",
});
export const createEmptyTaboo = (): TabooRule => ({
id: randomId(),
content: "",
});
export const createEmptyReferenceWork = (): ReferenceWork => ({
id: randomId(),
title: "",
inspiration: "",
});
export const createDefaultNovelSettingsV1 = (): NovelSettingsV1 => ({
genres: [],
oneLinePitch: "",
mainCharacter: {
name: "",
gender: "男",
age: "",
personality: "",
},
sideCharacters: [],
antagonists: [],
worldSummary: "",
conflictTheme: "",
worldDetails: {
powerSystem: "",
factions: "",
historyEvents: "",
importantLocations: "",
cultureAndTaboos: "",
},
opening: "",
middleBeats: [],
endingType: "",
subplots: [],
writingStyle: {
narration: "第三人称有限",
tones: [],
cheatLevel: "稳步成长",
focusAreas: [],
wordsPerChapter: 3000,
temperature: 0.7,
},
totalWords: 100000,
chapterWords: 3000,
nsfw: false,
systemNovel: false,
harem: false,
taboos: [],
references: [],
});
export const createDefaultNovelSettingsEnvelope = (): NovelSettingsEnvelope => ({
schema_version: NOVEL_SETTINGS_SCHEMA_VERSION,
data: createDefaultNovelSettingsV1(),
});
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function asBoolean(value: unknown, fallback = false): boolean {
return typeof value === "boolean" ? value : fallback;
}
function asNumber(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is string => typeof item === "string");
}
function normalizeMainCharacter(value: unknown): MainCharacter {
const base = createDefaultNovelSettingsV1().mainCharacter;
if (!isRecord(value)) {
return base;
}
return {
name: asString(value.name, base.name),
gender: asString(value.gender, base.gender),
age: asString(value.age, base.age),
personality: asString(value.personality, base.personality),
};
}
function normalizeSideCharacter(value: unknown): SideCharacter {
const base = createEmptySideCharacter();
if (!isRecord(value)) {
return base;
}
return {
id: asString(value.id, base.id),
name: asString(value.name, base.name),
nickname: asString(value.nickname, base.nickname),
gender: asString(value.gender, base.gender),
age: asString(value.age, base.age),
relationship: asString(value.relationship, base.relationship),
relationshipCustom: asString(value.relationshipCustom, base.relationshipCustom),
personalityTags: asStringArray(value.personalityTags),
background: asString(value.background, base.background),
abilities: asString(value.abilities, base.abilities),
role: asString(value.role, base.role),
arc: asString(value.arc, base.arc),
arcCustom: asString(value.arcCustom, base.arcCustom),
};
}
function normalizeAntagonist(value: unknown): Antagonist {
const base = createEmptyAntagonist();
if (!isRecord(value)) {
return base;
}
const side = normalizeSideCharacter(value);
return {
...side,
motive: asString(value.motive, base.motive),
fate: asString(value.fate, base.fate),
};
}
function normalizeWorldDetails(value: unknown): WorldDetails {
const base = createDefaultNovelSettingsV1().worldDetails;
if (!isRecord(value)) {
return base;
}
return {
powerSystem: asString(value.powerSystem, base.powerSystem),
factions: asString(value.factions, base.factions),
historyEvents: asString(value.historyEvents, base.historyEvents),
importantLocations: asString(value.importantLocations, base.importantLocations),
cultureAndTaboos: asString(value.cultureAndTaboos, base.cultureAndTaboos),
};
}
function normalizePlotBeat(value: unknown): PlotBeat {
const base = createEmptyPlotBeat();
if (!isRecord(value)) {
return base;
}
return {
id: asString(value.id, base.id),
title: asString(value.title, base.title),
detail: asString(value.detail, base.detail),
};
}
function normalizeWritingStyle(value: unknown): WritingStyle {
const base = createDefaultNovelSettingsV1().writingStyle;
if (!isRecord(value)) {
return base;
}
return {
narration: asString(value.narration, base.narration),
tones: asStringArray(value.tones),
cheatLevel: asString(value.cheatLevel, base.cheatLevel),
focusAreas: asStringArray(value.focusAreas),
wordsPerChapter: asNumber(value.wordsPerChapter, base.wordsPerChapter),
temperature: asNumber(value.temperature, base.temperature),
};
}
function normalizeTaboo(value: unknown): TabooRule {
const base = createEmptyTaboo();
if (typeof value === "string") {
return { ...base, content: value };
}
if (!isRecord(value)) {
return base;
}
return {
id: asString(value.id, base.id),
content: asString(value.content, base.content),
};
}
function normalizeReference(value: unknown): ReferenceWork {
const base = createEmptyReferenceWork();
if (!isRecord(value)) {
return base;
}
return {
id: asString(value.id, base.id),
title: asString(value.title, base.title),
inspiration: asString(value.inspiration, base.inspiration),
};
}
export function normalizeNovelSettings(value: unknown): NovelSettingsV1 {
const base = createDefaultNovelSettingsV1();
if (!isRecord(value)) {
return base;
}
const genres = Array.isArray(value.genres)
? value.genres.filter((item): item is NovelGenre => typeof item === "string")
: base.genres;
return {
genres,
oneLinePitch: asString(value.oneLinePitch, base.oneLinePitch),
mainCharacter: normalizeMainCharacter(value.mainCharacter),
sideCharacters: Array.isArray(value.sideCharacters)
? value.sideCharacters.map(normalizeSideCharacter)
: base.sideCharacters,
antagonists: Array.isArray(value.antagonists)
? value.antagonists.map(normalizeAntagonist)
: base.antagonists,
worldSummary: asString(value.worldSummary, base.worldSummary),
conflictTheme: asString(value.conflictTheme, base.conflictTheme),
worldDetails: normalizeWorldDetails(value.worldDetails),
opening: asString(value.opening, base.opening),
middleBeats: Array.isArray(value.middleBeats)
? value.middleBeats.map(normalizePlotBeat)
: base.middleBeats,
endingType: asString(value.endingType, base.endingType),
subplots: Array.isArray(value.subplots)
? value.subplots.map(normalizePlotBeat)
: base.subplots,
writingStyle: normalizeWritingStyle(value.writingStyle),
totalWords: asNumber(value.totalWords, base.totalWords),
chapterWords: asNumber(value.chapterWords, base.chapterWords),
nsfw: asBoolean(value.nsfw, base.nsfw),
systemNovel: asBoolean(value.systemNovel, base.systemNovel),
harem: asBoolean(value.harem, base.harem),
taboos: Array.isArray(value.taboos)
? value.taboos.map(normalizeTaboo).filter((item) => item.content.trim())
: base.taboos,
references: Array.isArray(value.references)
? value.references
.map(normalizeReference)
.filter((item) => item.title.trim() || item.inspiration.trim())
: base.references,
};
}
export function normalizeNovelSettingsEnvelope(value: unknown): NovelSettingsEnvelope {
if (isRecord(value) && isRecord(value.data)) {
const schemaVersion = asNumber(value.schema_version, NOVEL_SETTINGS_SCHEMA_VERSION);
return {
schema_version: schemaVersion,
data: normalizeNovelSettings(value.data),
};
}
return {
schema_version: NOVEL_SETTINGS_SCHEMA_VERSION,
data: normalizeNovelSettings(value),
};
}
+123
View File
@@ -80,6 +80,9 @@ const defaultMocks: Record<string, any> = {
shortcut: "",
},
},
web_search: {
engine: "google",
},
}),
save_config: (config: any) => {
@@ -122,6 +125,126 @@ const defaultMocks: Record<string, any> = {
lan_ip: "192.168.1.100",
all_ips: ["127.0.0.1", "192.168.1.100"],
}),
get_chrome_profile_sessions: () => [],
close_chrome_profile_session: () => true,
open_chrome_profile_window: () => ({
success: true,
reused: false,
browser_source: "system",
browser_path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
profile_dir: "/tmp/proxycast/chrome_profiles/search_google",
remote_debugging_port: 13001,
pid: 12345,
devtools_http_url: "http://127.0.0.1:13001/json/version",
}),
get_chrome_bridge_endpoint_info: () => ({
server_running: true,
host: "127.0.0.1",
port: 8999,
observer_ws_url:
"ws://127.0.0.1:8999/proxycast-chrome-observer/Proxycast_Key=proxy_cast",
control_ws_url:
"ws://127.0.0.1:8999/proxycast-chrome-control/Proxycast_Key=proxy_cast",
bridge_key: "proxy_cast",
}),
get_chrome_bridge_status: () => ({
observer_count: 0,
control_count: 0,
pending_command_count: 0,
observers: [],
controls: [],
pending_commands: [],
}),
chrome_bridge_execute_command: (args: any) => ({
success: true,
request_id: `mock-${Date.now()}`,
command: args?.request?.command ?? "get_page_info",
message: "mock command result",
page_info: {
title: "Mock Page",
url: "https://example.com",
markdown: "# Mock Page\nURL: https://example.com",
updated_at: new Date().toISOString(),
},
}),
get_browser_backend_policy: () => ({
priority: ["aster_compat", "proxycast_extension_bridge", "cdp_direct"],
auto_fallback: true,
}),
set_browser_backend_policy: (args: any) => ({
priority:
args?.policy?.priority ?? [
"aster_compat",
"proxycast_extension_bridge",
"cdp_direct",
],
auto_fallback: args?.policy?.auto_fallback ?? true,
}),
get_browser_backends_status: () => ({
policy: {
priority: ["aster_compat", "proxycast_extension_bridge", "cdp_direct"],
auto_fallback: true,
},
bridge_observer_count: 1,
bridge_control_count: 0,
running_profile_count: 1,
cdp_alive_profile_count: 1,
aster_native_host_supported: true,
aster_native_host_configured: false,
backends: [
{
backend: "aster_compat",
available: true,
capabilities: ["navigate", "read_page", "tabs_context_mcp"],
},
{
backend: "proxycast_extension_bridge",
available: true,
capabilities: ["open_url", "click", "type", "get_page_info"],
},
{
backend: "cdp_direct",
available: true,
capabilities: ["tabs_context_mcp", "navigate", "read_page"],
},
],
}),
browser_execute_action: (args: any) => ({
success: true,
backend: args?.request?.backend ?? "aster_compat",
action: args?.request?.action ?? "navigate",
request_id: `browser-mock-${Date.now()}`,
data: {
message: "mock browser action executed",
},
attempts: [
{
backend: args?.request?.backend ?? "aster_compat",
success: true,
message: "执行成功",
},
],
}),
get_browser_action_audit_logs: (args: any) => {
const now = new Date().toISOString();
const count = Math.min(Number(args?.limit ?? 20), 200);
return Array.from({ length: Math.max(1, count) }, (_, idx) => ({
id: `audit-mock-${idx + 1}`,
created_at: now,
action: "navigate",
profile_key: "default",
requested_backend: "aster_compat",
selected_backend: "aster_compat",
success: true,
attempts: [
{
backend: "aster_compat",
success: true,
message: "执行成功",
},
],
}));
},
// Agent 相关
list_agent_sessions: () => [],
+269
View File
@@ -48,6 +48,10 @@ export interface CreateWebviewRequest {
width: number;
/** 高度 */
height: number;
/** Profile 隔离键(可选) */
profile_key?: string;
/** 是否启用持久化 profile(可选) */
persistent_profile?: boolean;
}
/**
@@ -62,6 +66,187 @@ export interface CreateWebviewResponse {
error?: string;
}
/**
* 启动外部 Chrome Profile 的请求参数
*/
export interface OpenChromeProfileRequest {
/** Profile 隔离键 */
profile_key: string;
/** 要打开的 URL */
url: string;
}
/**
* 启动外部 Chrome Profile 的响应
*/
export interface OpenChromeProfileResponse {
/** 是否成功 */
success: boolean;
/** 是否复用已存在会话 */
reused?: boolean;
/** 浏览器来源 */
browser_source?: "system" | "playwright";
/** 浏览器可执行文件路径 */
browser_path?: string;
/** Profile 目录 */
profile_dir?: string;
/** 远程调试端口 */
remote_debugging_port?: number;
/** Chrome 进程 PID */
pid?: number;
/** DevTools HTTP URL */
devtools_http_url?: string;
/** 错误信息 */
error?: string;
}
/**
* Chrome Profile 会话信息
*/
export interface ChromeProfileSessionInfo {
profile_key: string;
browser_source: "system" | "playwright";
browser_path: string;
profile_dir: string;
remote_debugging_port: number;
pid: number;
started_at: string;
last_url: string;
}
export interface ChromeBridgeEndpointInfo {
server_running: boolean;
host: string;
port: number;
observer_ws_url: string;
control_ws_url: string;
bridge_key: string;
}
export interface ChromeBridgePageInfo {
title?: string;
url?: string;
markdown: string;
updated_at: string;
}
export interface ChromeBridgeObserverSnapshot {
client_id: string;
profile_key: string;
connected_at: string;
user_agent?: string;
last_heartbeat_at?: string;
last_page_info?: ChromeBridgePageInfo;
}
export interface ChromeBridgeControlSnapshot {
client_id: string;
connected_at: string;
user_agent?: string;
}
export interface ChromeBridgePendingCommandSnapshot {
request_id: string;
source_type: "api" | "control";
command: string;
observer_client_id: string;
wait_for_page_info: boolean;
command_completed: boolean;
created_at: string;
}
export interface ChromeBridgeStatusSnapshot {
observer_count: number;
control_count: number;
pending_command_count: number;
observers: ChromeBridgeObserverSnapshot[];
controls: ChromeBridgeControlSnapshot[];
pending_commands: ChromeBridgePendingCommandSnapshot[];
}
export interface ChromeBridgeCommandRequest {
profile_key?: string;
command: string;
target?: string;
text?: string;
url?: string;
wait_for_page_info?: boolean;
timeout_ms?: number;
}
export interface ChromeBridgeCommandResult {
success: boolean;
request_id: string;
command: string;
message?: string;
error?: string;
page_info?: ChromeBridgePageInfo;
}
export type BrowserBackendType =
| "aster_compat"
| "proxycast_extension_bridge"
| "cdp_direct";
export interface BrowserBackendPolicy {
priority: BrowserBackendType[];
auto_fallback: boolean;
}
export interface BrowserBackendStatusItem {
backend: BrowserBackendType;
available: boolean;
reason?: string;
capabilities: string[];
}
export interface BrowserBackendsStatusSnapshot {
policy: BrowserBackendPolicy;
bridge_observer_count: number;
bridge_control_count: number;
running_profile_count: number;
cdp_alive_profile_count: number;
aster_native_host_supported: boolean;
aster_native_host_configured: boolean;
backends: BrowserBackendStatusItem[];
}
export interface BrowserActionRequest {
profile_key?: string;
backend?: BrowserBackendType;
action: string;
args?: Record<string, unknown>;
timeout_ms?: number;
}
export interface BrowserActionAttempt {
backend: BrowserBackendType;
success: boolean;
message: string;
}
export interface BrowserActionResult {
success: boolean;
backend?: BrowserBackendType;
action: string;
request_id: string;
data?: unknown;
error?: string;
attempts: BrowserActionAttempt[];
}
export interface BrowserActionAuditRecord {
id: string;
created_at: string;
action: string;
profile_key?: string;
requested_backend?: BrowserBackendType;
selected_backend?: BrowserBackendType;
success: boolean;
error?: string;
attempts: BrowserActionAttempt[];
}
/**
* 创建一个新的 webview 窗口来显示外部 URL
*
@@ -74,6 +259,90 @@ export async function createWebviewPanel(
return safeInvoke<CreateWebviewResponse>("create_webview_panel", { request });
}
/**
* 使用外部 Chrome + 独立 Profile 打开 URL
*/
export async function openChromeProfileWindow(
request: OpenChromeProfileRequest,
): Promise<OpenChromeProfileResponse> {
return safeInvoke<OpenChromeProfileResponse>("open_chrome_profile_window", {
request,
});
}
/**
* 获取当前运行中的 Chrome Profile 会话
*/
export async function getChromeProfileSessions(): Promise<ChromeProfileSessionInfo[]> {
return safeInvoke<ChromeProfileSessionInfo[]>("get_chrome_profile_sessions");
}
/**
* 关闭指定的 Chrome Profile 会话
*/
export async function closeChromeProfileSession(
profileKey: string,
): Promise<boolean> {
return safeInvoke<boolean>("close_chrome_profile_session", {
profile_key: profileKey,
});
}
/**
* 获取 ChromeBridge 端点信息(用于扩展配置)
*/
export async function getChromeBridgeEndpointInfo(): Promise<ChromeBridgeEndpointInfo> {
return safeInvoke<ChromeBridgeEndpointInfo>("get_chrome_bridge_endpoint_info");
}
/**
* 获取 ChromeBridge 当前连接状态
*/
export async function getChromeBridgeStatus(): Promise<ChromeBridgeStatusSnapshot> {
return safeInvoke<ChromeBridgeStatusSnapshot>("get_chrome_bridge_status");
}
/**
* 通过 ChromeBridge 发送测试命令
*/
export async function chromeBridgeExecuteCommand(
request: ChromeBridgeCommandRequest,
): Promise<ChromeBridgeCommandResult> {
return safeInvoke<ChromeBridgeCommandResult>("chrome_bridge_execute_command", {
request,
});
}
export async function getBrowserBackendPolicy(): Promise<BrowserBackendPolicy> {
return safeInvoke<BrowserBackendPolicy>("get_browser_backend_policy");
}
export async function setBrowserBackendPolicy(
policy: BrowserBackendPolicy,
): Promise<BrowserBackendPolicy> {
return safeInvoke<BrowserBackendPolicy>("set_browser_backend_policy", {
policy,
});
}
export async function getBrowserBackendsStatus(): Promise<BrowserBackendsStatusSnapshot> {
return safeInvoke<BrowserBackendsStatusSnapshot>("get_browser_backends_status");
}
export async function browserExecuteAction(
request: BrowserActionRequest,
): Promise<BrowserActionResult> {
return safeInvoke<BrowserActionResult>("browser_execute_action", { request });
}
export async function getBrowserActionAuditLogs(
limit?: number,
): Promise<BrowserActionAuditRecord[]> {
return safeInvoke<BrowserActionAuditRecord[]>("get_browser_action_audit_logs", {
limit,
});
}
/**
* 关闭 webview 面板
*