mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: sync agent chat updates and remove pr gate
This commit is contained in:
@@ -1,136 +0,0 @@
|
||||
name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "src/**"
|
||||
- "src-tauri/**"
|
||||
- "scripts/**"
|
||||
- "eslint.config.js"
|
||||
- "tsconfig.json"
|
||||
- "tsconfig.node.json"
|
||||
- "tailwind.config.js"
|
||||
- "postcss.config.js"
|
||||
- "vite.config.ts"
|
||||
- "index.html"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "src/**"
|
||||
- "src-tauri/**"
|
||||
- "scripts/**"
|
||||
- "eslint.config.js"
|
||||
- "tsconfig.json"
|
||||
- "tsconfig.node.json"
|
||||
- "tailwind.config.js"
|
||||
- "postcss.config.js"
|
||||
- "vite.config.ts"
|
||||
- "index.html"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_NET_RETRY: 10
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
name: Frontend Verify
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Run Vitest
|
||||
run: npm test
|
||||
|
||||
bridge:
|
||||
name: Bridge & Contracts Verify
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run bridge tests
|
||||
run: npm run test:bridge
|
||||
|
||||
- name: Run command contracts
|
||||
run: npm run test:contracts
|
||||
|
||||
rust:
|
||||
name: Rust Verify
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libasound2-dev
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Setup Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
shared-key: pr-gate-rust-ubuntu
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Run cargo test
|
||||
run: cargo test --manifest-path "src-tauri/Cargo.toml"
|
||||
|
||||
- name: Run cargo clippy
|
||||
run: cargo clippy --manifest-path "src-tauri/Cargo.toml"
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
- **Team 配置与发布稳定性收尾**:补齐 Team Selector 自定义 Team 配置链路、当前 Team 展示与相关测试,并修复通知、Provider Runtime 与前端类型兼容问题,确保 `v0.92.0` 可稳定构建发布
|
||||
- **运行态与工具可视化增强**:`ToolCallDisplay`、Harness 状态面板、Runtime Strip、执行日志与子代理时间线继续增强,工具调用与运行态反馈更完整
|
||||
- **Aster Agent 运行时继续收口**:Rust 侧补齐 session store、subagent control、agent tools inventory / execution、runtime queue 及命令桥接,统一现役 Agent Runtime 路径
|
||||
- **治理与测试基建升级**:新增 `pr-gate`、本地校验脚本、命令契约检查、workspace smoke 与治理报告更新,发布前自检链路更清晰
|
||||
- **治理与测试基建升级**:本地校验脚本、命令契约检查、workspace smoke 与治理报告继续完善,发布前自检链路更清晰
|
||||
- **Provider / 模型兼容性继续补强**:补充 Novita 与多种 OpenAI/Claude 兼容 provider 细节,推理内容与工具调用适配继续完善
|
||||
|
||||
### ⚠️ 兼容性说明
|
||||
|
||||
@@ -112,7 +112,7 @@ npm run bridge:health -- --timeout-ms 120000
|
||||
- 假设 `tauri-driver` 仍是推荐路径
|
||||
- 假设浏览器 E2E 已进入 CI 标准门禁
|
||||
|
||||
当前 PR 门禁以 `.github/workflows/pr-gate.yml` 为准;完整浏览器主链路 smoke 仍属于后续建设项,详见 `docs/test/testing-strategy-2026.md`。
|
||||
当前浏览器主链路 smoke 仍属于后续建设项,详见 `docs/test/testing-strategy-2026.md`。
|
||||
|
||||
## 7. 给后续 Agent 的交接要求
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
- `package.json`:当前统一测试命令入口
|
||||
- `scripts/local-ci.mjs`:当前本地智能校验入口
|
||||
- `scripts/report-legacy-surfaces.mjs`:当前 legacy / compat 回流护栏
|
||||
- `.github/workflows/pr-gate.yml`:当前 PR 自动门禁入口
|
||||
|
||||
### compat
|
||||
|
||||
@@ -39,11 +38,10 @@
|
||||
- 桥接基础测试已经存在:`src/lib/dev-bridge/safeInvoke.test.ts`、`src/lib/tauri-mock/core.test.ts`
|
||||
- legacy 治理护栏已经存在:`npm run governance:legacy-report`
|
||||
- 旧权限表面治理护栏已经补齐:`src/lib/governance/legacyToolPermissionGuard.test.ts` + `npm run governance:legacy-report`
|
||||
- 跨层命令契约检查基础版已经落地:`npm run test:contracts` 已进入 `scripts/local-ci.mjs` 与 `.github/workflows/pr-gate.yml`
|
||||
- 跨层命令契约检查基础版已经落地:`npm run test:contracts` 已进入 `scripts/local-ci.mjs`
|
||||
- 命令契约延期例外已经收口:`agent_terminal_command_response`、`agent_term_scrollback_response` 已退出 `runtimeGatewayCommands`,改为 `dead-candidate` 治理监控
|
||||
- 首条自包含 smoke 已落地:`npm run smoke:workspace-ready` 可自动校验 DevBridge 就绪、默认 workspace 获取、目录修复与路径回查
|
||||
- 测试文档事实源已经收口:`docs/test/README.md`、`docs/test/e2e-tests.md`、`docs/aiprompts/playwright-e2e.md` 已按“索引 / 总览 / 详细事实源”分层
|
||||
- PR 自动门禁已经补齐:`.github/workflows/pr-gate.yml` 已覆盖前端、bridge、Rust 三类基础检查
|
||||
|
||||
## 3. 当前仍未解决的问题优先级
|
||||
|
||||
|
||||
@@ -30,16 +30,7 @@ import {
|
||||
} from "./utils/chatToolPreferences";
|
||||
import { isTeamRuntimeRecommendation } from "./utils/contextualRecommendations";
|
||||
import { normalizeProjectId } from "./utils/topicProjectResolution";
|
||||
import {
|
||||
createTeamDefinitionFromPreset,
|
||||
listBuiltinTeamDefinitions,
|
||||
type TeamDefinition,
|
||||
} from "./utils/teamDefinitions";
|
||||
import {
|
||||
loadCustomTeams,
|
||||
loadSelectedTeamReference,
|
||||
persistSelectedTeam,
|
||||
} from "./utils/teamStorage";
|
||||
import { useSelectedTeamPreference } from "./hooks/useSelectedTeamPreference";
|
||||
|
||||
const SUPPORTED_ENTRY_THEMES: ThemeType[] = [
|
||||
"general",
|
||||
@@ -161,22 +152,6 @@ function normalizeInitialTheme(value?: string): ThemeType {
|
||||
return "general";
|
||||
}
|
||||
|
||||
function resolvePersistedSelectedTeam(theme?: string | null): TeamDefinition | null {
|
||||
const selection = loadSelectedTeamReference(theme);
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selection.source === "builtin") {
|
||||
return (
|
||||
listBuiltinTeamDefinitions().find((team) => team.id === selection.id) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return loadCustomTeams().find((team) => team.id === selection.id) || null;
|
||||
}
|
||||
|
||||
function scheduleDeferredHomeEnhancement(task: () => void): () => void {
|
||||
if (typeof window === "undefined") {
|
||||
return () => undefined;
|
||||
@@ -302,9 +277,6 @@ export function AgentChatHomeShell({
|
||||
const [creationMode, setCreationMode] = useState<CreationMode>(
|
||||
initialCreationMode ?? "guided",
|
||||
);
|
||||
const [selectedTeam, setSelectedTeam] = useState<TeamDefinition | null>(() =>
|
||||
resolvePersistedSelectedTeam(initialTheme),
|
||||
);
|
||||
const [chatToolPreferences, setChatToolPreferences] =
|
||||
useState<ChatToolPreferences>(() =>
|
||||
loadChatToolPreferences(normalizedEntryTheme),
|
||||
@@ -329,6 +301,11 @@ export function AgentChatHomeShell({
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [skillsLoading, setSkillsLoading] = useState(false);
|
||||
const [browserAssistLoading, setBrowserAssistLoading] = useState(false);
|
||||
const {
|
||||
selectedTeam,
|
||||
setSelectedTeam: handleSelectTeam,
|
||||
enableSuggestedTeam: handleEnableSuggestedTeam,
|
||||
} = useSelectedTeamPreference(activeTheme);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTheme(normalizeInitialTheme(initialTheme));
|
||||
@@ -365,14 +342,6 @@ export function AgentChatHomeShell({
|
||||
saveChatToolPreferences(chatToolPreferences, activeTheme);
|
||||
}, [activeTheme, chatToolPreferences, chatToolPreferencesTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTeam(resolvePersistedSelectedTeam(activeTheme));
|
||||
}, [activeTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
persistSelectedTeam(selectedTeam, activeTheme);
|
||||
}, [activeTheme, selectedTeam]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextPreferences = resolvePersistedProviderModel(currentProjectId);
|
||||
setProviderTypeState(nextPreferences.providerType);
|
||||
@@ -615,27 +584,6 @@ export function AgentChatHomeShell({
|
||||
[activeTheme, chatToolPreferences, handleEnterWorkspace],
|
||||
);
|
||||
|
||||
const handleEnableSuggestedTeam = useCallback((suggestedPresetId?: string) => {
|
||||
const resolvedPresetId = suggestedPresetId?.trim();
|
||||
if (!resolvedPresetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestedTeam = createTeamDefinitionFromPreset(resolvedPresetId);
|
||||
if (suggestedTeam) {
|
||||
persistSelectedTeam(suggestedTeam, activeTheme);
|
||||
setSelectedTeam(suggestedTeam);
|
||||
}
|
||||
}, [activeTheme]);
|
||||
|
||||
const handleSelectTeam = useCallback(
|
||||
(team: TeamDefinition | null) => {
|
||||
persistSelectedTeam(team, activeTheme);
|
||||
setSelectedTeam(team);
|
||||
},
|
||||
[activeTheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<MainArea>
|
||||
|
||||
@@ -273,18 +273,7 @@ import {
|
||||
buildGeneralAgentSystemPrompt,
|
||||
resolveAgentChatMode,
|
||||
} from "./utils/generalAgentPrompt";
|
||||
import {
|
||||
buildTeamDefinitionLabel,
|
||||
buildTeamDefinitionSummary,
|
||||
createTeamDefinitionFromPreset,
|
||||
listBuiltinTeamDefinitions,
|
||||
type TeamDefinition,
|
||||
} from "./utils/teamDefinitions";
|
||||
import {
|
||||
loadCustomTeams,
|
||||
loadSelectedTeamReference,
|
||||
persistSelectedTeam,
|
||||
} from "./utils/teamStorage";
|
||||
import { useSelectedTeamPreference } from "./hooks/useSelectedTeamPreference";
|
||||
import {
|
||||
areBrowserAssistSessionStatesEqual,
|
||||
clearBrowserAssistSessionState,
|
||||
@@ -362,22 +351,6 @@ function normalizeInitialTheme(value?: string): ThemeType {
|
||||
return "general";
|
||||
}
|
||||
|
||||
function resolvePersistedSelectedTeam(theme?: string | null): TeamDefinition | null {
|
||||
const selection = loadSelectedTeamReference(theme);
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selection.source === "builtin") {
|
||||
return (
|
||||
listBuiltinTeamDefinitions().find((team) => team.id === selection.id) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return loadCustomTeams().find((team) => team.id === selection.id) || null;
|
||||
}
|
||||
|
||||
function shouldPreserveGeneralArtifact(artifact: Artifact): boolean {
|
||||
return artifact.meta.persistOutsideMessages === true;
|
||||
}
|
||||
@@ -2591,9 +2564,6 @@ export function AgentChatWorkspace({
|
||||
|
||||
// 内容创作相关状态
|
||||
const [activeTheme, setActiveTheme] = useState<string>(normalizedEntryTheme);
|
||||
const [selectedTeam, setSelectedTeam] = useState<TeamDefinition | null>(() =>
|
||||
resolvePersistedSelectedTeam(normalizedEntryTheme),
|
||||
);
|
||||
const [creationMode, setCreationMode] = useState<CreationMode>(
|
||||
initialCreationMode ?? "guided",
|
||||
);
|
||||
@@ -2741,6 +2711,14 @@ export function AgentChatWorkspace({
|
||||
setThemeWorkbenchCreationTaskEvents,
|
||||
] = useState<ThemeWorkbenchCreationTaskEvent[]>([]);
|
||||
const documentEditorFocusedRef = useRef(false);
|
||||
const {
|
||||
selectedTeam,
|
||||
setSelectedTeam: handleSelectTeam,
|
||||
enableSuggestedTeam: handleEnableSuggestedTeam,
|
||||
preferredTeamPresetId,
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
} = useSelectedTeamPreference(activeTheme);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveContentTarget(projectId, contentId, canvasState?.type ?? null);
|
||||
@@ -2850,52 +2828,6 @@ export function AgentChatWorkspace({
|
||||
|
||||
// 工作流状态(仅在内容创作模式下使用)
|
||||
const mappedTheme = activeTheme as ThemeType;
|
||||
const preferredTeamPresetId = useMemo(
|
||||
() =>
|
||||
selectedTeam?.presetId?.trim() ||
|
||||
(selectedTeam?.source === "builtin" ? selectedTeam.id : undefined),
|
||||
[selectedTeam],
|
||||
);
|
||||
const selectedTeamLabel = useMemo(
|
||||
() => buildTeamDefinitionLabel(selectedTeam) || undefined,
|
||||
[selectedTeam],
|
||||
);
|
||||
const selectedTeamSummary = useMemo(
|
||||
() => buildTeamDefinitionSummary(selectedTeam) || undefined,
|
||||
[selectedTeam],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTeam(resolvePersistedSelectedTeam(activeTheme));
|
||||
}, [activeTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
persistSelectedTeam(selectedTeam, activeTheme);
|
||||
}, [activeTheme, selectedTeam]);
|
||||
|
||||
const handleSelectTeam = useCallback(
|
||||
(team: TeamDefinition | null) => {
|
||||
persistSelectedTeam(team, activeTheme);
|
||||
setSelectedTeam(team);
|
||||
},
|
||||
[activeTheme],
|
||||
);
|
||||
|
||||
const handleEnableSuggestedTeam = useCallback(
|
||||
(suggestedPresetId?: string) => {
|
||||
const resolvedPresetId = suggestedPresetId?.trim();
|
||||
if (!resolvedPresetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestedTeam = createTeamDefinitionFromPreset(resolvedPresetId);
|
||||
if (suggestedTeam) {
|
||||
persistSelectedTeam(suggestedTeam, activeTheme);
|
||||
setSelectedTeam(suggestedTeam);
|
||||
}
|
||||
},
|
||||
[activeTheme],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRuntimeStyleSelection({
|
||||
|
||||
@@ -400,6 +400,24 @@ describe("HarnessStatusPanel", () => {
|
||||
expect(document.body.textContent).toContain("等待首个模型事件");
|
||||
});
|
||||
|
||||
it("runtimeStatus 为 failed 时应展示失败阶段与失败详情", () => {
|
||||
renderPanel({
|
||||
harnessState: createHarnessState({
|
||||
runtimeStatus: {
|
||||
phase: "failed",
|
||||
title: "当前执行失败",
|
||||
detail: "429 rate limit",
|
||||
checkpoints: ["已保留当前回合过程"],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain("执行阶段");
|
||||
expect(document.body.textContent).toContain("失败");
|
||||
expect(document.body.textContent).toContain("当前执行失败");
|
||||
expect(document.body.textContent).toContain("429 rate limit");
|
||||
});
|
||||
|
||||
it("存在 selectedTeam 时应在工作台展示当前 Team 配置", () => {
|
||||
renderPanel({
|
||||
selectedTeamLabel: "前端联调团队",
|
||||
|
||||
@@ -544,6 +544,8 @@ function formatRuntimePhaseLabel(
|
||||
return "建回合中";
|
||||
case "context":
|
||||
return "装载上下文";
|
||||
case "failed":
|
||||
return "失败";
|
||||
default:
|
||||
return runtimeStatus.phase;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import React, { memo, useMemo, useState, useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
@@ -567,37 +568,88 @@ const RUNTIME_PHASE_LABELS: Record<AgentRuntimeStatus["phase"], string> = {
|
||||
preparing: "准备中",
|
||||
routing: "回合建立中",
|
||||
context: "上下文装载中",
|
||||
failed: "执行失败",
|
||||
};
|
||||
|
||||
const AgentRuntimeStatusBlock: React.FC<{ status: AgentRuntimeStatus }> = ({
|
||||
status,
|
||||
}) => (
|
||||
<div className="rounded-2xl border border-border/70 bg-muted/30 px-4 py-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
}) => {
|
||||
const failed = status.phase === "failed";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl border px-4 py-3",
|
||||
failed
|
||||
? "border-rose-200 bg-rose-50/90"
|
||||
: "border-border/70 bg-muted/30",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-7 w-7 items-center justify-center rounded-full",
|
||||
failed ? "bg-rose-100 text-rose-600" : "bg-primary/10 text-primary",
|
||||
)}
|
||||
>
|
||||
{failed ? (
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
failed ? "text-rose-900" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{status.title}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"ml-auto inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs",
|
||||
failed
|
||||
? "border-rose-200 bg-white text-rose-700"
|
||||
: "border-border/70 bg-background/80 text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{failed ? (
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
) : (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
)}
|
||||
{RUNTIME_PHASE_LABELS[status.phase]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">{status.title}</div>
|
||||
<div className="ml-auto inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/80 px-2.5 py-0.5 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
{RUNTIME_PHASE_LABELS[status.phase]}
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm",
|
||||
failed ? "text-rose-700" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status.detail}
|
||||
</div>
|
||||
{status.checkpoints && status.checkpoints.length > 0 ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{status.checkpoints.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-xs",
|
||||
failed
|
||||
? "border-rose-200 bg-white text-rose-700"
|
||||
: "border-border/60 bg-background/80 text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{status.detail}</div>
|
||||
{status.checkpoints && status.checkpoints.length > 0 ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{status.checkpoints.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-full border border-border/60 bg-background/80 px-2.5 py-1 text-xs text-muted-foreground"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 流式消息渲染组件
|
||||
|
||||
@@ -28,6 +28,11 @@ import {
|
||||
handleToolStartEvent,
|
||||
} from "./agentStreamEventProcessor";
|
||||
import type { AgentRuntimeAdapter } from "./agentRuntimeAdapter";
|
||||
import {
|
||||
buildFailedAgentMessageContent,
|
||||
buildFailedAgentRuntimeStatus,
|
||||
formatAgentRuntimeStatusSummary,
|
||||
} from "../utils/agentRuntimeStatus";
|
||||
|
||||
type MessageParts = NonNullable<Message["contentParts"]>;
|
||||
|
||||
@@ -156,6 +161,49 @@ export function handleTurnStreamEvent({
|
||||
appendThinkingToParts,
|
||||
} = callbacks;
|
||||
|
||||
const markFailedTimelineState = (errorMessage: string) => {
|
||||
const failedAt = new Date().toISOString();
|
||||
const failedRuntimeStatus = buildFailedAgentRuntimeStatus(errorMessage);
|
||||
|
||||
setThreadTurns((prev) => {
|
||||
const runningTurn =
|
||||
prev.find((turn) => turn.id === optimisticTurnId) ||
|
||||
[...prev]
|
||||
.reverse()
|
||||
.find(
|
||||
(turn) =>
|
||||
turn.thread_id === activeSessionId && turn.status === "running",
|
||||
);
|
||||
|
||||
if (!runningTurn) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return upsertThreadTurnState(prev, {
|
||||
...runningTurn,
|
||||
status: "failed",
|
||||
error_message: errorMessage,
|
||||
completed_at: runningTurn.completed_at || failedAt,
|
||||
updated_at: failedAt,
|
||||
});
|
||||
});
|
||||
|
||||
setThreadItems((prev) => {
|
||||
const optimisticItem = prev.find((item) => item.id === optimisticItemId);
|
||||
if (!optimisticItem || optimisticItem.type !== "turn_summary") {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return upsertThreadItemState(prev, {
|
||||
...optimisticItem,
|
||||
status: "failed",
|
||||
completed_at: optimisticItem.completed_at || failedAt,
|
||||
updated_at: failedAt,
|
||||
text: formatAgentRuntimeStatusSummary(failedRuntimeStatus),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
switch (data.type) {
|
||||
case "thread_started":
|
||||
break;
|
||||
@@ -411,9 +459,8 @@ export function handleTurnStreamEvent({
|
||||
break;
|
||||
}
|
||||
|
||||
case "error":
|
||||
clearOptimisticItem();
|
||||
clearOptimisticTurn();
|
||||
case "error": {
|
||||
markFailedTimelineState(data.message);
|
||||
removeQueuedTurnState(requestState.queuedTurnId ? [requestState.queuedTurnId] : []);
|
||||
finishRequestLog(requestState, {
|
||||
eventType: "chat_request_error",
|
||||
@@ -421,6 +468,7 @@ export function handleTurnStreamEvent({
|
||||
error: data.message,
|
||||
});
|
||||
observer?.onError?.(data.message);
|
||||
const failedRuntimeStatus = buildFailedAgentRuntimeStatus(data.message);
|
||||
if (
|
||||
data.message.includes("429") ||
|
||||
data.message.toLowerCase().includes("rate limit")
|
||||
@@ -435,9 +483,11 @@ export function handleTurnStreamEvent({
|
||||
? {
|
||||
...updateMessageArtifactsStatus(msg, "error"),
|
||||
isThinking: false,
|
||||
content:
|
||||
requestState.accumulatedContent || `错误: ${data.message}`,
|
||||
runtimeStatus: undefined,
|
||||
content: buildFailedAgentMessageContent(
|
||||
data.message,
|
||||
requestState.accumulatedContent || msg.content,
|
||||
),
|
||||
runtimeStatus: failedRuntimeStatus,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
@@ -445,6 +495,7 @@ export function handleTurnStreamEvent({
|
||||
clearActiveStreamIfMatch(eventName);
|
||||
disposeListener();
|
||||
break;
|
||||
}
|
||||
|
||||
case "warning": {
|
||||
if (data.code === WORKSPACE_PATH_AUTO_CREATED_WARNING_CODE) {
|
||||
|
||||
@@ -42,8 +42,11 @@ import {
|
||||
upsertThreadTurnState,
|
||||
} from "./agentThreadState";
|
||||
import {
|
||||
buildFailedAgentMessageContent,
|
||||
buildFailedAgentRuntimeStatus,
|
||||
buildInitialAgentRuntimeStatus,
|
||||
buildWaitingAgentRuntimeStatus,
|
||||
formatAgentRuntimeStatusSummary,
|
||||
} from "../utils/agentRuntimeStatus";
|
||||
import { handleTurnStreamEvent } from "./agentStreamRuntimeHandler";
|
||||
|
||||
@@ -210,22 +213,6 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
[],
|
||||
);
|
||||
|
||||
const buildRuntimeStatusSummary = useCallback(
|
||||
(status?: Message["runtimeStatus"]): string => {
|
||||
if (!status?.title) {
|
||||
return "Agent 正在准备执行";
|
||||
}
|
||||
|
||||
const lines = [status.title.trim()];
|
||||
if (status.detail?.trim()) {
|
||||
lines.push(status.detail.trim());
|
||||
}
|
||||
|
||||
return lines.join("\n\n");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (
|
||||
content: string,
|
||||
@@ -436,6 +423,45 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
setCurrentTurnId((prev) => (prev === optimisticTurnId ? null : prev));
|
||||
};
|
||||
|
||||
const markOptimisticFailure = (errorMessage: string) => {
|
||||
if (expectingQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const failedAt = new Date().toISOString();
|
||||
const failedRuntimeStatus = buildFailedAgentRuntimeStatus(errorMessage);
|
||||
|
||||
setThreadTurns((prev) => {
|
||||
const currentTurn = prev.find((turn) => turn.id === optimisticTurnId);
|
||||
if (!currentTurn) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return upsertThreadTurnState(prev, {
|
||||
...currentTurn,
|
||||
status: "failed",
|
||||
error_message: errorMessage,
|
||||
completed_at: currentTurn.completed_at || failedAt,
|
||||
updated_at: failedAt,
|
||||
});
|
||||
});
|
||||
|
||||
setThreadItems((prev) => {
|
||||
const currentItem = prev.find((item) => item.id === optimisticItemId);
|
||||
if (!currentItem || currentItem.type !== "turn_summary") {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return upsertThreadItemState(prev, {
|
||||
...currentItem,
|
||||
status: "failed",
|
||||
completed_at: currentItem.completed_at || failedAt,
|
||||
updated_at: failedAt,
|
||||
text: formatAgentRuntimeStatusSummary(failedRuntimeStatus),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const disposeListener = () => {
|
||||
const registered = listenerMapRef.current.get(eventName);
|
||||
if (registered) {
|
||||
@@ -469,7 +495,7 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
started_at: optimisticStartedAt,
|
||||
updated_at: optimisticStartedAt,
|
||||
type: "turn_summary",
|
||||
text: buildRuntimeStatusSummary(assistantMsg.runtimeStatus),
|
||||
text: formatAgentRuntimeStatusSummary(assistantMsg.runtimeStatus),
|
||||
}),
|
||||
);
|
||||
setCurrentTurnId(optimisticTurnId);
|
||||
@@ -534,7 +560,7 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
started_at: optimisticStartedAt,
|
||||
updated_at: new Date().toISOString(),
|
||||
type: "turn_summary",
|
||||
text: buildRuntimeStatusSummary(waitingRuntimeStatus),
|
||||
text: formatAgentRuntimeStatusSummary(waitingRuntimeStatus),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -653,6 +679,7 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
}
|
||||
console.error("[AsterChat] 发送失败:", error);
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
const failedRuntimeStatus = buildFailedAgentRuntimeStatus(errMsg);
|
||||
observer?.onError?.(errMsg);
|
||||
if (
|
||||
errMsg.includes("429") ||
|
||||
@@ -664,16 +691,20 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
} else {
|
||||
toast.error(`发送失败: ${error}`);
|
||||
}
|
||||
clearOptimisticItem();
|
||||
clearOptimisticTurn();
|
||||
markOptimisticFailure(errMsg);
|
||||
removeQueuedTurnState(
|
||||
requestState.queuedTurnId ? [requestState.queuedTurnId] : [],
|
||||
);
|
||||
setMessages((prev) =>
|
||||
prev.filter(
|
||||
(msg) =>
|
||||
msg.id !== assistantMsgId &&
|
||||
(!expectingQueue || !userMsgId || msg.id !== userMsgId),
|
||||
prev.map((msg) =>
|
||||
msg.id === assistantMsgId
|
||||
? {
|
||||
...updateMessageArtifactsStatus(msg, "error"),
|
||||
isThinking: false,
|
||||
content: buildFailedAgentMessageContent(errMsg, msg.content),
|
||||
runtimeStatus: failedRuntimeStatus,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
);
|
||||
clearActiveStreamIfMatch(eventName);
|
||||
@@ -686,7 +717,6 @@ export function useAgentStream(options: UseAgentStreamOptions) {
|
||||
[
|
||||
activeStreamRef,
|
||||
buildQueuedRuntimeStatus,
|
||||
buildRuntimeStatusSummary,
|
||||
clearActiveStreamIfMatch,
|
||||
ensureSession,
|
||||
executionStrategy,
|
||||
|
||||
@@ -920,6 +920,48 @@ describe("useAsterAgentChat thread timeline", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("submitTurn 失败时应保留失败回合与失败消息,而不是清空当前过程", async () => {
|
||||
const workspaceId = "ws-thread-submit-failed";
|
||||
seedSession(workspaceId, "session-thread-submit-failed");
|
||||
mockSubmitAgentRuntimeTurn.mockRejectedValueOnce(new Error("429 rate limit"));
|
||||
const harness = mountHook(workspaceId);
|
||||
|
||||
try {
|
||||
await flushEffects();
|
||||
|
||||
await act(async () => {
|
||||
await harness
|
||||
.getValue()
|
||||
.sendMessage("帮我开始执行", [], false, false, false, "react");
|
||||
});
|
||||
|
||||
const assistantMessage = [...harness.getValue().messages]
|
||||
.reverse()
|
||||
.find((msg) => msg.role === "assistant");
|
||||
|
||||
expect(assistantMessage?.content).toContain("执行失败:429 rate limit");
|
||||
expect(assistantMessage?.runtimeStatus).toMatchObject({
|
||||
phase: "failed",
|
||||
title: "当前执行失败",
|
||||
});
|
||||
expect(harness.getValue().turns).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "failed",
|
||||
error_message: "429 rate limit",
|
||||
}),
|
||||
]);
|
||||
expect(harness.getValue().threadItems).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "turn_summary",
|
||||
status: "failed",
|
||||
}),
|
||||
]);
|
||||
expect(mockToast.warning).toHaveBeenCalledWith("请求过于频繁,请稍后重试");
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("应接收 turn/item 生命周期事件并写入运行态", async () => {
|
||||
const workspaceId = "ws-thread-timeline";
|
||||
seedSession(workspaceId, "session-thread-timeline");
|
||||
@@ -1005,6 +1047,70 @@ describe("useAsterAgentChat thread timeline", () => {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("stream error 事件时应保留失败消息与失败回合", async () => {
|
||||
const workspaceId = "ws-thread-stream-error";
|
||||
seedSession(workspaceId, "session-thread-stream-error");
|
||||
const harness = mountHook(workspaceId);
|
||||
const stream = captureTurnStream();
|
||||
|
||||
try {
|
||||
await flushEffects();
|
||||
|
||||
await act(async () => {
|
||||
await harness
|
||||
.getValue()
|
||||
.sendMessage("请开始处理", [], false, false, false, "react");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stream.emit({
|
||||
type: "turn_started",
|
||||
turn: {
|
||||
id: "turn-stream-error-1",
|
||||
thread_id: "session-thread-stream-error",
|
||||
prompt_text: "请开始处理",
|
||||
status: "running",
|
||||
started_at: "2026-03-20T10:00:00.000Z",
|
||||
created_at: "2026-03-20T10:00:00.000Z",
|
||||
updated_at: "2026-03-20T10:00:00.000Z",
|
||||
},
|
||||
});
|
||||
stream.emit({
|
||||
type: "error",
|
||||
message: "模型执行失败",
|
||||
});
|
||||
});
|
||||
|
||||
const assistantMessage = [...harness.getValue().messages]
|
||||
.reverse()
|
||||
.find((msg) => msg.role === "assistant");
|
||||
|
||||
expect(assistantMessage?.content).toContain("执行失败:模型执行失败");
|
||||
expect(assistantMessage?.runtimeStatus).toMatchObject({
|
||||
phase: "failed",
|
||||
title: "当前执行失败",
|
||||
});
|
||||
expect(harness.getValue().turns).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "turn-stream-error-1",
|
||||
status: "failed",
|
||||
error_message: "模型执行失败",
|
||||
}),
|
||||
]);
|
||||
expect(harness.getValue().threadItems).toEqual([
|
||||
expect.objectContaining({
|
||||
id: expect.stringContaining("turn-summary:"),
|
||||
type: "turn_summary",
|
||||
status: "failed",
|
||||
turn_id: "turn-stream-error-1",
|
||||
}),
|
||||
]);
|
||||
expect(mockToast.error).toHaveBeenCalledWith("响应错误: 模型执行失败");
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("useAsterAgentChat runtime routing", () => {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { TeamDefinition } from "../utils/teamDefinitions";
|
||||
import { createTeamDefinitionFromPreset } from "../utils/teamDefinitions";
|
||||
import { useSelectedTeamPreference } from "./useSelectedTeamPreference";
|
||||
import {
|
||||
loadSelectedTeamReference,
|
||||
persistSelectedTeam,
|
||||
} from "../utils/teamStorage";
|
||||
|
||||
interface HookHarness {
|
||||
getValue: () => ReturnType<typeof useSelectedTeamPreference>;
|
||||
rerender: (theme?: string | null) => void;
|
||||
unmount: () => void;
|
||||
}
|
||||
|
||||
function mountHook(initialTheme?: string | null): HookHarness {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let hookValue: ReturnType<typeof useSelectedTeamPreference> | null = null;
|
||||
|
||||
function TestComponent({ theme }: { theme?: string | null }) {
|
||||
hookValue = useSelectedTeamPreference(theme);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = (theme?: string | null) => {
|
||||
act(() => {
|
||||
root.render(<TestComponent theme={theme} />);
|
||||
});
|
||||
};
|
||||
|
||||
render(initialTheme);
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!hookValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return hookValue;
|
||||
},
|
||||
rerender: render,
|
||||
unmount: () => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function flushEffects() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe("useSelectedTeamPreference", () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("切换 theme 时应读取对应 Team,而不是把旧主题选择写回新主题", async () => {
|
||||
const engineeringTeam = createTeamDefinitionFromPreset(
|
||||
"code-triage-team",
|
||||
) as TeamDefinition;
|
||||
const researchTeam = createTeamDefinitionFromPreset(
|
||||
"research-team",
|
||||
) as TeamDefinition;
|
||||
|
||||
persistSelectedTeam(engineeringTeam, "general");
|
||||
persistSelectedTeam(researchTeam, "knowledge");
|
||||
|
||||
const harness = mountHook("general");
|
||||
|
||||
try {
|
||||
await flushEffects();
|
||||
expect(harness.getValue().selectedTeam?.id).toBe("code-triage-team");
|
||||
|
||||
harness.rerender("knowledge");
|
||||
await flushEffects();
|
||||
|
||||
expect(harness.getValue().selectedTeam?.id).toBe("research-team");
|
||||
expect(loadSelectedTeamReference("general")).toEqual({
|
||||
id: "code-triage-team",
|
||||
source: "builtin",
|
||||
});
|
||||
expect(loadSelectedTeamReference("knowledge")).toEqual({
|
||||
id: "research-team",
|
||||
source: "builtin",
|
||||
});
|
||||
|
||||
act(() => {
|
||||
harness.getValue().setSelectedTeam(null);
|
||||
});
|
||||
|
||||
expect(loadSelectedTeamReference("knowledge")).toBeNull();
|
||||
expect(loadSelectedTeamReference("general")).toEqual({
|
||||
id: "code-triage-team",
|
||||
source: "builtin",
|
||||
});
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { TeamDefinition } from "../utils/teamDefinitions";
|
||||
import {
|
||||
buildTeamDefinitionLabel,
|
||||
buildTeamDefinitionSummary,
|
||||
createTeamDefinitionFromPreset,
|
||||
} from "../utils/teamDefinitions";
|
||||
import {
|
||||
persistSelectedTeam,
|
||||
resolvePersistedSelectedTeam,
|
||||
} from "../utils/teamStorage";
|
||||
|
||||
export function useSelectedTeamPreference(theme?: string | null) {
|
||||
const [selectedTeam, setSelectedTeamState] = useState<TeamDefinition | null>(
|
||||
() => resolvePersistedSelectedTeam(theme),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTeamState(resolvePersistedSelectedTeam(theme));
|
||||
}, [theme]);
|
||||
|
||||
const setSelectedTeam = useCallback(
|
||||
(team: TeamDefinition | null) => {
|
||||
persistSelectedTeam(team, theme);
|
||||
setSelectedTeamState(team);
|
||||
},
|
||||
[theme],
|
||||
);
|
||||
|
||||
const enableSuggestedTeam = useCallback(
|
||||
(suggestedPresetId?: string) => {
|
||||
const resolvedPresetId = suggestedPresetId?.trim();
|
||||
if (!resolvedPresetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestedTeam = createTeamDefinitionFromPreset(resolvedPresetId);
|
||||
if (suggestedTeam) {
|
||||
setSelectedTeam(suggestedTeam);
|
||||
}
|
||||
},
|
||||
[setSelectedTeam],
|
||||
);
|
||||
|
||||
const preferredTeamPresetId = useMemo(
|
||||
() =>
|
||||
selectedTeam?.presetId?.trim() ||
|
||||
(selectedTeam?.source === "builtin" ? selectedTeam.id : undefined),
|
||||
[selectedTeam],
|
||||
);
|
||||
const selectedTeamLabel = useMemo(
|
||||
() => buildTeamDefinitionLabel(selectedTeam) || undefined,
|
||||
[selectedTeam],
|
||||
);
|
||||
const selectedTeamSummary = useMemo(
|
||||
() => buildTeamDefinitionSummary(selectedTeam) || undefined,
|
||||
[selectedTeam],
|
||||
);
|
||||
|
||||
return {
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
enableSuggestedTeam,
|
||||
preferredTeamPresetId,
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
mockUseThemeContextWorkspace,
|
||||
mockUseTopicBranchBoard,
|
||||
mockUseTeamWorkspaceRuntime,
|
||||
mockUseCompatSubagentRuntime,
|
||||
mockGetProject,
|
||||
mockGetDefaultProject,
|
||||
mockGetOrCreateDefaultProject,
|
||||
@@ -48,6 +49,7 @@ const {
|
||||
mockUseThemeContextWorkspace: vi.fn(),
|
||||
mockUseTopicBranchBoard: vi.fn(),
|
||||
mockUseTeamWorkspaceRuntime: vi.fn(),
|
||||
mockUseCompatSubagentRuntime: vi.fn(),
|
||||
mockGetProject: vi.fn(),
|
||||
mockGetDefaultProject: vi.fn(),
|
||||
mockGetOrCreateDefaultProject: vi.fn(),
|
||||
@@ -148,6 +150,7 @@ vi.mock("./hooks", () => ({
|
||||
useThemeContextWorkspace: mockUseThemeContextWorkspace,
|
||||
useTopicBranchBoard: mockUseTopicBranchBoard,
|
||||
useTeamWorkspaceRuntime: mockUseTeamWorkspaceRuntime,
|
||||
useCompatSubagentRuntime: mockUseCompatSubagentRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("./hooks/useSessionFiles", () => ({
|
||||
@@ -983,6 +986,15 @@ beforeEach(() => {
|
||||
liveActivityBySessionId: {},
|
||||
activityRefreshVersionBySessionId: {},
|
||||
});
|
||||
mockUseCompatSubagentRuntime.mockReturnValue({
|
||||
isRunning: false,
|
||||
progress: null,
|
||||
events: [],
|
||||
result: null,
|
||||
error: null,
|
||||
recentActivity: [],
|
||||
hasSignals: false,
|
||||
});
|
||||
mockCanvasWorkbenchLayoutState.renderPreview = false;
|
||||
|
||||
mockJotaiState.artifacts = [];
|
||||
@@ -4357,4 +4369,94 @@ describe("AgentChatPage legacy 问卷 A2UI", () => {
|
||||
"action-request-req-action-required",
|
||||
);
|
||||
});
|
||||
|
||||
it("真实 action_required 已提交后,输入区应显示补充信息确认提示而不是继续停留在表单态", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
return {
|
||||
providerType: "kiro",
|
||||
setProviderType: vi.fn(),
|
||||
model: "mock-model",
|
||||
setModel: vi.fn(),
|
||||
executionStrategy: "auto",
|
||||
setExecutionStrategy: vi.fn(),
|
||||
messages: [
|
||||
{
|
||||
id: "msg-user-submitted",
|
||||
role: "user",
|
||||
content: "继续推进当前任务",
|
||||
timestamp: new Date("2026-03-15T09:02:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-assistant-submitted",
|
||||
role: "assistant",
|
||||
content: "已收到补充信息,正在继续推进。",
|
||||
timestamp: new Date("2026-03-15T09:02:10.000Z"),
|
||||
actionRequests: [
|
||||
{
|
||||
requestId: "req-submitted-action",
|
||||
actionType: "ask_user",
|
||||
prompt: "请选择执行模式",
|
||||
questions: [{ question: "你希望如何执行?" }],
|
||||
status: "submitted",
|
||||
submittedResponse: '{"answer":"自动执行(Auto)"}',
|
||||
submittedUserData: {
|
||||
answer: "自动执行(Auto)",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
isSending: false,
|
||||
sendMessage: sharedSendMessageMock,
|
||||
stopSending: vi.fn(async () => undefined),
|
||||
clearMessages: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
editMessage: vi.fn(),
|
||||
handlePermissionResponse: vi.fn(),
|
||||
triggerAIGuide: sharedTriggerAIGuideMock,
|
||||
topics: [
|
||||
{
|
||||
id: "topic-a",
|
||||
title: "话题 A",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
],
|
||||
sessionId: "session-1",
|
||||
switchTopic: sharedSwitchTopicMock,
|
||||
deleteTopic: vi.fn(),
|
||||
renameTopic: vi.fn(),
|
||||
workspacePathMissing: false,
|
||||
fixWorkspacePathAndRetry: vi.fn(),
|
||||
dismissWorkspacePathError: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
renderPage({
|
||||
projectId: "project-action-required-submitted",
|
||||
theme: "general",
|
||||
lockTheme: true,
|
||||
});
|
||||
await flushEffects(10);
|
||||
|
||||
const latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
pendingA2UIForm?: {
|
||||
id?: string;
|
||||
} | null;
|
||||
a2uiSubmissionNotice?: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
} | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(latestInputbarProps?.pendingA2UIForm ?? null).toBeNull();
|
||||
expect(latestInputbarProps?.a2uiSubmissionNotice).toMatchObject({
|
||||
title: "补充信息已确认",
|
||||
summary: "自动执行(Auto)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +142,7 @@ export interface WriteArtifactContext {
|
||||
}
|
||||
|
||||
export interface AgentRuntimeStatus {
|
||||
phase: "preparing" | "routing" | "context";
|
||||
phase: "preparing" | "routing" | "context" | "failed";
|
||||
title: string;
|
||||
detail: string;
|
||||
checkpoints?: string[];
|
||||
|
||||
@@ -15,6 +15,11 @@ function buildExecutionLabel(strategy: AsterExecutionStrategy): string {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRuntimeErrorDetail(errorMessage: string): string {
|
||||
const detail = errorMessage.trim();
|
||||
return detail || "执行链路返回失败,请查看详情后重试。";
|
||||
}
|
||||
|
||||
export function buildInitialAgentRuntimeStatus(options: {
|
||||
executionStrategy: AsterExecutionStrategy;
|
||||
webSearch?: boolean;
|
||||
@@ -82,3 +87,40 @@ export function buildActionResumeRuntimeStatus(): AgentRuntimeStatus {
|
||||
checkpoints: ["补充信息已确认", "已唤醒当前执行链路", "等待下一条执行事件"],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFailedAgentRuntimeStatus(
|
||||
errorMessage: string,
|
||||
): AgentRuntimeStatus {
|
||||
return {
|
||||
phase: "failed",
|
||||
title: "当前执行失败",
|
||||
detail: normalizeRuntimeErrorDetail(errorMessage),
|
||||
checkpoints: ["已保留当前回合过程", "可修正问题后重试", "如需继续可补充更明确的输入"],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFailedAgentMessageContent(
|
||||
errorMessage: string,
|
||||
partialContent?: string,
|
||||
): string {
|
||||
const failureText = `执行失败:${normalizeRuntimeErrorDetail(errorMessage)}`;
|
||||
const trimmedPartialContent = partialContent?.trim();
|
||||
return trimmedPartialContent
|
||||
? `${trimmedPartialContent}\n\n${failureText}`
|
||||
: failureText;
|
||||
}
|
||||
|
||||
export function formatAgentRuntimeStatusSummary(
|
||||
status?: AgentRuntimeStatus | null,
|
||||
): string {
|
||||
if (!status?.title) {
|
||||
return "Agent 正在准备执行";
|
||||
}
|
||||
|
||||
const lines = [status.title.trim()];
|
||||
if (status.detail?.trim()) {
|
||||
lines.push(status.detail.trim());
|
||||
}
|
||||
|
||||
return lines.join("\n\n");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TeamDefinition } from "./teamDefinitions";
|
||||
import {
|
||||
createTeamDefinitionFromPreset,
|
||||
buildTeamSelectionReference,
|
||||
normalizeTeamDefinition,
|
||||
type TeamSelectionReference,
|
||||
@@ -109,3 +110,18 @@ export function loadSelectedTeamReference(
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePersistedSelectedTeam(
|
||||
theme?: string | null,
|
||||
): TeamDefinition | null {
|
||||
const selection = loadSelectedTeamReference(theme);
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selection.source === "builtin") {
|
||||
return createTeamDefinitionFromPreset(selection.id);
|
||||
}
|
||||
|
||||
return loadCustomTeams().find((team) => team.id === selection.id) || null;
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ export interface StreamEventContextTrace {
|
||||
}
|
||||
|
||||
export interface StreamRuntimeStatusPayload {
|
||||
phase: "preparing" | "routing" | "context";
|
||||
phase: "preparing" | "routing" | "context" | "failed";
|
||||
title: string;
|
||||
detail: string;
|
||||
checkpoints?: string[];
|
||||
|
||||
Reference in New Issue
Block a user