修复编译问题

This commit is contained in:
coso
2026-04-15 07:38:01 +08:00
parent d5902c9380
commit 302e7524d2
14 changed files with 167 additions and 93 deletions
@@ -289,19 +289,19 @@ impl ProviderCredential {
/// 解析当前凭证应采用的 Prompt Cache 模式。
pub fn effective_prompt_cache_mode(&self) -> Option<ProviderPromptCacheMode> {
self.prompt_cache_mode_override.or_else(|| {
if self.provider_type.supports_anthropic_prompt_cache() {
Some(ProviderPromptCacheMode::Automatic)
} else if matches!(self.provider_type, PoolProviderType::AnthropicCompatible)
&& is_known_automatic_anthropic_compatible_host(
if matches!(self.provider_type, PoolProviderType::AnthropicCompatible) {
return if is_known_automatic_anthropic_compatible_host(
get_base_url(&self.credential).as_deref(),
)
{
Some(ProviderPromptCacheMode::Automatic)
} else if matches!(self.provider_type, PoolProviderType::AnthropicCompatible) {
Some(ProviderPromptCacheMode::ExplicitOnly)
} else {
None
) {
Some(ProviderPromptCacheMode::Automatic)
} else {
Some(ProviderPromptCacheMode::ExplicitOnly)
};
}
self.provider_type
.supports_anthropic_prompt_cache()
.then_some(ProviderPromptCacheMode::Automatic)
})
}
@@ -144,10 +144,6 @@ impl WorkspaceSandboxedBashTool {
}
}
fn format_output(stdout: &str, stderr: &str, exit_code: i32) -> String {
Self::format_output_with_message(stdout, stderr, exit_code, None)
}
fn format_output_with_message(
stdout: &str,
stderr: &str,
@@ -598,7 +598,7 @@ function createAlignedRuntimeToolInventory(): AgentRuntimeToolInventory {
caller_allowed: true,
visible_in_context: true,
},
...base.runtime_tools.filter((entry) => entry.name !== "Agent"),
...(base.runtime_tools ?? []).filter((entry) => entry.name !== "Agent"),
],
};
}
@@ -417,6 +417,36 @@ describe("MarkdownRenderer", () => {
).not.toBeNull();
});
it("标题后的正文应保持聊天正文排版,不应缩小变灰", () => {
const container = document.createElement("div");
container.style.setProperty("--foreground", "17 24 39");
container.style.setProperty("--muted-foreground", "100 116 139");
container.style.fontSize = "15px";
container.style.lineHeight = "1.7";
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(
<MarkdownRenderer content={"## 小结\n\n这段正文应该和聊天正文保持同一字号与主色。"} />,
);
});
mountedRoots.push({ container, root });
const heading = container.querySelector(
'h2[data-markdown-heading-level="2"]',
);
const paragraph = container.querySelector("p");
expect(heading).not.toBeNull();
expect(paragraph).not.toBeNull();
expect(getComputedStyle(paragraph as Element).fontSize).toBe("1em");
expect(document.head.textContent).not.toContain("h1 + p");
expect(document.head.textContent).not.toContain("h2 + p");
expect(document.head.textContent).not.toContain("h3 + p");
});
it("非流式时应保留 raw html 渲染能力", () => {
const content = [
"前置文本",
@@ -39,8 +39,8 @@ const CODE_BLOCK_BUTTON_HOVER_SURFACE = "rgba(248, 250, 252, 0.98)";
// 收紧正文与代码块表面,让消息正文更接近单列执行流的阅读节奏。
const MarkdownContainer = styled.div`
font-size: 14px;
line-height: 1.76;
font-size: inherit;
line-height: inherit;
color: hsl(var(--foreground));
overflow-wrap: break-word;
word-break: break-word;
@@ -57,14 +57,8 @@ const MarkdownContainer = styled.div`
p {
margin: 0 0 0.95em;
color: hsl(var(--foreground));
}
h1 + p,
h2 + p,
h3 + p {
color: hsl(var(--muted-foreground));
font-size: 1.02em;
line-height: 1.8;
font-size: 1em;
line-height: inherit;
}
h1,
@@ -74,9 +68,9 @@ const MarkdownContainer = styled.div`
h5,
h6 {
font-weight: 700;
margin: 1.34em 0 0.58em;
line-height: 1.32;
letter-spacing: -0.01em;
margin: 1.08em 0 0.5em;
line-height: 1.42;
letter-spacing: 0;
color: hsl(var(--foreground));
}
@@ -87,21 +81,20 @@ const MarkdownContainer = styled.div`
}
h1 {
font-size: 1.54em;
font-size: 1.16em;
}
h2 {
font-size: 1.28em;
font-size: 1.1em;
}
h3 {
font-size: 1.12em;
font-size: 1.04em;
}
h4 {
font-size: 1.03em;
font-size: 1em;
}
h5,
h6 {
font-size: 0.96em;
color: hsl(var(--muted-foreground));
font-size: 0.98em;
}
ul,
@@ -1653,7 +1653,7 @@ describe("MessageList", () => {
content: "正在分析依赖关系。",
timestamp: now,
runtimeStatus: {
phase: "reasoning",
phase: "routing",
title: "处理中",
detail: "正在读取多个 crate 的依赖。",
},
@@ -288,11 +288,19 @@ describe("agentStreamTurnEventBinding", () => {
setIsSending: noopDispatch<boolean>(),
});
streamHandler?.({
if (!streamHandler) {
throw new Error("expected stream handler to be registered");
}
const activeStreamHandler = streamHandler as (event: {
payload: unknown;
}) => void;
activeStreamHandler({
payload: {
type: "runtime_status",
status: {
phase: "reasoning",
phase: "routing",
title: "分析中",
detail: "正在整理仓库结构",
},
@@ -1,9 +1,14 @@
import type { AgentRuntimeToolInventory } from "@/lib/api/agentRuntime";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
deriveRuntimeToolAvailability,
RUNTIME_TOOL_AVAILABILITY_OVERRIDE_STORAGE_KEY,
} from "./runtimeToolAvailability";
function asToolInventory(value: unknown): AgentRuntimeToolInventory {
return value as AgentRuntimeToolInventory;
}
describe("runtime tool surface 派生", () => {
beforeEach(() => {
window.localStorage.clear();
@@ -14,24 +19,26 @@ describe("runtime tool surface 派生", () => {
});
it("runtime tool surface 应优先从 runtime_tools 派生 current capability", () => {
const availability = deriveRuntimeToolAvailability({
agent_initialized: true,
runtime_tools: [
{ name: "WebSearch" },
{ name: "Agent" },
{ name: "SendMessage" },
{ name: "TeamCreate" },
{ name: "TeamDelete" },
{ name: "ListPeers" },
{ name: "TaskCreate" },
{ name: "TaskGet" },
{ name: "TaskList" },
{ name: "TaskUpdate" },
{ name: "TaskOutput" },
{ name: "TaskStop" },
],
registry_tools: [{ name: "registry-only" }],
} as Parameters<typeof deriveRuntimeToolAvailability>[0]);
const availability = deriveRuntimeToolAvailability(
asToolInventory({
agent_initialized: true,
runtime_tools: [
{ name: "WebSearch" },
{ name: "Agent" },
{ name: "SendMessage" },
{ name: "TeamCreate" },
{ name: "TeamDelete" },
{ name: "ListPeers" },
{ name: "TaskCreate" },
{ name: "TaskGet" },
{ name: "TaskList" },
{ name: "TaskUpdate" },
{ name: "TaskOutput" },
{ name: "TaskStop" },
],
registry_tools: [],
}),
);
expect(availability).toMatchObject({
source: "runtime_tools",
@@ -62,11 +69,13 @@ describe("runtime tool surface 派生", () => {
}),
);
const availability = deriveRuntimeToolAvailability({
agent_initialized: true,
runtime_tools: [{ name: "WebSearch" }],
registry_tools: [],
} as Parameters<typeof deriveRuntimeToolAvailability>[0]);
const availability = deriveRuntimeToolAvailability(
asToolInventory({
agent_initialized: true,
runtime_tools: [{ name: "WebSearch" }],
registry_tools: [],
}),
);
expect(availability).toMatchObject({
source: "runtime_tools",
@@ -6,6 +6,7 @@ import {
normalizeToolNameKey,
parseToolCallArguments,
resolveToolFilePath,
type ToolCallArgumentValue,
} from "./toolDisplayInfo";
export type ToolBatchKind = "exploration" | "browser";
@@ -39,11 +40,16 @@ interface ToolBatchAccumulator {
interface ToolLikeDescriptor {
toolName: string;
argumentsValue?: string | Record<string, unknown>;
argumentsValue?: string | Record<string, ToolCallArgumentValue>;
command?: string | null;
query?: string | null;
}
type ThreadProcessBatchItem = Extract<
AgentThreadItem,
{ type: "tool_call" | "command_execution" | "web_search" }
>;
function shorten(value: string | null | undefined, maxLength = 72): string | null {
const normalized = value?.trim();
if (!normalized) {
@@ -55,7 +61,7 @@ function shorten(value: string | null | undefined, maxLength = 72): string | nul
return `${normalized.slice(0, maxLength - 1).trimEnd()}…`;
}
function asRecord(value: unknown): Record<string, unknown> {
function asRecord(value: unknown): Record<string, ToolCallArgumentValue> {
if (!value) {
return {};
}
@@ -63,18 +69,28 @@ function asRecord(value: unknown): Record<string, unknown> {
if (typeof value === "string") {
const parsed = parseToolCallArguments(value);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
return parsed;
}
return {};
}
if (typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
return value as Record<string, ToolCallArgumentValue>;
}
return {};
}
function isThreadProcessBatchItem(
item: AgentThreadItem,
): item is ThreadProcessBatchItem {
return (
item.type === "tool_call" ||
item.type === "command_execution" ||
item.type === "web_search"
);
}
function readString(
record: Record<string, unknown>,
keys: string[],
@@ -378,32 +394,32 @@ export function summarizeStreamingToolBatch(
export function summarizeThreadProcessBatch(
items: AgentThreadItem[],
): ToolBatchSummaryDescriptor | null {
if (
items.length < 2 ||
items.some(
(item) =>
item.type !== "tool_call" &&
item.type !== "command_execution" &&
item.type !== "web_search",
)
) {
const processItems = items.filter(isThreadProcessBatchItem);
if (processItems.length < 2 || processItems.length !== items.length) {
return null;
}
const descriptors: ToolLikeDescriptor[] = items.map((item) => {
const descriptors: ToolLikeDescriptor[] = processItems.map((item) => {
if (item.type === "command_execution") {
const argumentsValue: Record<string, ToolCallArgumentValue> = {
command: item.command,
cwd: item.cwd,
};
return {
toolName: "exec_command",
command: item.command,
argumentsValue: { command: item.command, cwd: item.cwd },
argumentsValue,
};
}
if (item.type === "web_search") {
const argumentsValue: Record<string, ToolCallArgumentValue> = {
query: item.query || item.action || "",
};
return {
toolName: item.action || "web_search",
query: item.query || item.action || null,
argumentsValue: { query: item.query || item.action || "" },
argumentsValue,
};
}
@@ -411,7 +427,7 @@ export function summarizeThreadProcessBatch(
toolName: item.tool_name,
argumentsValue:
item.arguments && typeof item.arguments === "object"
? (item.arguments as Record<string, unknown>)
? (item.arguments as Record<string, ToolCallArgumentValue>)
: item.arguments === undefined
? undefined
: String(item.arguments),
@@ -36,10 +36,7 @@ import { WorkspaceConversationScene } from "./WorkspaceConversationScene";
type InputbarScene = Pick<
ReturnType<typeof useWorkspaceInputbarSceneRuntime>,
| "inputbarNode"
| "generalWorkbenchDialog"
| "teamWorkbenchSurfaceProps"
| "runtimeToolAvailability"
"inputbarNode" | "generalWorkbenchDialog" | "teamWorkbenchSurfaceProps"
>;
type CanvasScene = Pick<
ReturnType<typeof useWorkspaceCanvasSceneRuntime>,
@@ -723,7 +720,6 @@ export function useWorkspaceConversationSceneRuntime({
setAccessMode,
onManageProviders: navigationActions.handleManageProviders,
toolPreferences: chatToolPreferences,
runtimeToolAvailability: inputbarScene.runtimeToolAvailability,
onToolPreferenceChange: (key, enabled) =>
setChatToolPreferences((previous) => ({
...previous,
+16 -4
View File
@@ -336,6 +336,17 @@ const MEMORY_SCOPE_CARD_META: Array<{
},
];
type MemoryAvailabilityStatus = "loaded" | "exists" | "missing";
interface MemoryScopeCardView {
key: "user" | "project" | "local" | "auto" | "durable" | "team";
label: string;
description: string;
status: MemoryAvailabilityStatus;
detail: string;
helper: string;
}
const MEMORY_DO_NOT_SAVE = [
"代码模式、约定、架构、文件路径或项目结构,这些应直接从当前仓库读取。",
"Git 历史、最近改动、谁改了什么,`git log` 和 `git blame` 才是事实源。",
@@ -637,7 +648,7 @@ function buildCreationPrompt(
return lines.join("\n");
}
function getMemoryAvailabilityBadge(status: "loaded" | "exists" | "missing"): {
function getMemoryAvailabilityBadge(status: MemoryAvailabilityStatus): {
label: string;
className: string;
} {
@@ -932,10 +943,11 @@ export function MemoryPage({ onNavigate, pageParams }: MemoryPageProps) {
[sourceBuckets],
);
const memoryScopeCards = useMemo(() => {
const memoryScopeCards = useMemo<MemoryScopeCardView[]>(() => {
return MEMORY_SCOPE_CARD_META.map((card) => {
if (card.key === "team") {
const status = teamSnapshots.length > 0 ? "loaded" : "missing";
const status: MemoryAvailabilityStatus =
teamSnapshots.length > 0 ? "loaded" : "missing";
return {
...card,
status,
@@ -961,7 +973,7 @@ export function MemoryPage({ onNavigate, pageParams }: MemoryPageProps) {
? "durable"
: "user";
const bucket = sourceBucketMap.get(bucketKey);
const status = bucket?.status || "missing";
const status: MemoryAvailabilityStatus = bucket?.status || "missing";
if (card.key === "auto") {
return {
@@ -14,7 +14,6 @@ import type {
BaseSetupPackage,
BaseSetupPolicyProfile,
BaseSetupProjectionIndex,
BaseSetupScorecardProfile,
BaseSetupSlotProfile,
} from "../types";
+7 -2
View File
@@ -179,7 +179,7 @@ describe("http-client", () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("事件流在已建立连接后断开时应主动关闭并停止重复告警", async () => {
it("事件流在已建立连接后断开时应保留连接并停止重复告警", async () => {
class MockEventSource {
static instances: MockEventSource[] = [];
@@ -226,10 +226,15 @@ describe("http-client", () => {
source.emitError();
source.emitError();
expect(source.close).toHaveBeenCalledTimes(1);
const secondUnlisten = await listenViaHttpEvent("config-changed", vi.fn());
expect(MockEventSource.instances).toHaveLength(1);
expect(source.close).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledTimes(1);
unlisten();
secondUnlisten();
expect(source.close).toHaveBeenCalledTimes(1);
});
it("事件流在建立后结束不应把整个桥接误标记为 unavailable", async () => {
+13 -3
View File
@@ -336,6 +336,7 @@ export async function listenViaHttpEvent<T = unknown>(
let hasOpened = false;
let settleOpen: ((value: void | PromiseLike<void>) => void) | null = null;
let settleOpenError: ((reason?: unknown) => void) | null = null;
let reconnectWarningShown = false;
const openPromise = new Promise<void>((resolve, reject) => {
settleOpen = resolve;
settleOpenError = reject;
@@ -354,6 +355,10 @@ export async function listenViaHttpEvent<T = unknown>(
}, DEV_BRIDGE_EVENT_CONNECT_TIMEOUT_MS);
source.onmessage = (messageEvent) => {
if (reconnectWarningShown) {
reconnectWarningShown = false;
markBridgeHealthy();
}
const parsed = parseBridgeEventPayload<unknown>(messageEvent.data);
if (!parsed) {
return;
@@ -375,6 +380,7 @@ export async function listenViaHttpEvent<T = unknown>(
return;
}
hasOpened = true;
reconnectWarningShown = false;
markBridgeHealthy();
window.clearTimeout(connectTimeout);
settleOpen?.();
@@ -386,13 +392,17 @@ export async function listenViaHttpEvent<T = unknown>(
if (!hubActive) {
return;
}
if (hasOpened) {
if (!reconnectWarningShown) {
reconnectWarningShown = true;
console.warn(`[DevBridge] 事件流异常: ${normalizedEvent}`, error);
}
return;
}
console.warn(`[DevBridge] 事件流异常: ${normalizedEvent}`, error);
hubActive = false;
bridgeEventHubs.delete(normalizedEvent);
source.close();
if (hasOpened) {
return;
}
markBridgeUnavailable();
window.clearTimeout(connectTimeout);
settleOpenError?.(