feat: release v0.84.0 with full pending changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
coso
2026-03-12 00:34:23 +08:00
co-authored by Claude Opus 4.6
parent bfacf764b7
commit 2aa83ee7fc
268 changed files with 21727 additions and 6006 deletions
+232 -269
View File
@@ -3,11 +3,33 @@
*
* 基于 Aster 框架的 Agent 状态管理
* 参考 Claude-Cowork 的设计模式
*
* @deprecated 当前仓库已迁移到 `useAgentChat` / `useAsterAgentChat` 主链路;
* 请不要在新代码中继续依赖这个遗留 store。
*/
import { create } from "zustand";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { safeListen } from "@/lib/dev-bridge";
import {
confirmAsterAction,
createAsterSession,
deleteAsterSession,
getAsterSession,
initAsterAgent,
listAsterSessions,
sendAsterMessageStream,
stopAsterSession,
submitAsterElicitationResponse,
type AsterSessionDetail,
} from "@/lib/api/agentRuntime";
import {
parseStreamEvent,
type StreamEvent,
type ToolExecutionResult,
type TokenUsage,
} from "@/lib/api/agentStream";
import { requireDefaultProjectId } from "@/lib/api/project";
import {
isAsterSessionNotFoundError,
resolveRestorableSessionId,
@@ -22,11 +44,8 @@ export interface MessageImage {
}
/** 工具调用结果 */
export interface ToolResult {
success: boolean;
output?: string;
error?: string;
}
export type ToolResult = ToolExecutionResult;
export type { TokenUsage } from "@/lib/api/agentStream";
/** 工具调用状态 */
export interface ToolCallState {
@@ -39,13 +58,6 @@ export interface ToolCallState {
endTime?: Date;
}
/** Token 使用量 */
export interface TokenUsage {
input_tokens: number;
output_tokens: number;
total_tokens: number;
}
/** 内容片段类型 */
export type ContentPart =
| { type: "text"; text: string }
@@ -106,48 +118,18 @@ export interface ConfirmResponse {
// ============ Tauri 事件类型 ============
/** Tauri Agent 事件 */
export type TauriAgentEvent =
| { type: "text_delta"; text: string }
| { type: "thinking_delta"; text: string }
| {
type: "tool_start";
tool_name: string;
tool_id: string;
arguments?: string;
}
| { type: "tool_end"; tool_id: string; result: ToolResult }
| {
type: "action_required";
request_id: string;
action_type: string;
data: Record<string, unknown>;
}
| { type: "model_change"; model: string; mode: string }
| { type: "done"; usage?: TokenUsage }
| { type: "final_done"; usage?: TokenUsage }
| { type: "warning"; code?: string; message: string }
| { type: "error"; message: string }
| { type: "message"; message: unknown };
export type TauriAgentEvent = StreamEvent;
// ============ Store 状态类型 ============
interface AgentState {
// 会话状态
currentSessionId: string | null;
sessions: SessionInfo[];
messages: Message[];
// 流式状态
isStreaming: boolean;
currentAssistantMsgId: string | null;
// 权限确认
pendingActions: ActionRequired[];
// 配置
isInitialized: boolean;
// Actions
initialize: () => Promise<void>;
sendMessage: (content: string, images?: MessageImage[]) => Promise<void>;
stopStreaming: () => Promise<void>;
@@ -157,19 +139,46 @@ interface AgentState {
deleteSession: (sessionId: string) => Promise<void>;
clearMessages: () => void;
loadSessions: () => Promise<void>;
// 内部方法
_handleEvent: (event: TauriAgentEvent) => void;
_handleEvent: (event: StreamEvent) => void;
_cleanup: () => void;
}
// ============ Store 实现 ============
// 事件监听器引用
let eventUnlisten: UnlistenFn | null = null;
const resolveDefaultWorkspaceId = async (): Promise<string> => {
return requireDefaultProjectId("未找到默认工作区,请先创建并设为默认工作区");
};
const toRuntimeImages = (images?: MessageImage[]) =>
images?.map((image) => ({
data: image.data,
media_type: image.mediaType,
}));
const extractSessionMessageText = (
content: AsterSessionDetail["messages"][number]["content"],
): string => {
const segments = content
.map((block) => {
if (typeof block.text === "string" && block.text.trim()) {
return block.text.trim();
}
if (typeof block.output === "string" && block.output.trim()) {
return block.output.trim();
}
if (typeof block.error === "string" && block.error.trim()) {
return `错误: ${block.error.trim()}`;
}
return "";
})
.filter(Boolean);
return segments.join("\n\n");
};
export const useAgentStore = create<AgentState>((set, get) => ({
// 初始状态
currentSessionId: null,
sessions: [],
messages: [],
@@ -178,14 +187,11 @@ export const useAgentStore = create<AgentState>((set, get) => ({
pendingActions: [],
isInitialized: false,
// 初始化 Agent
initialize: async () => {
try {
await invoke("aster_agent_init");
await initAsterAgent();
set({ isInitialized: true });
console.log("[AgentStore] Agent 初始化成功");
// 加载会话列表
await get().loadSessions();
} catch (error) {
console.error("[AgentStore] Agent 初始化失败:", error);
@@ -193,22 +199,18 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 发送消息
sendMessage: async (content: string, images?: MessageImage[]) => {
const state = get();
// 确保已初始化
if (!state.isInitialized) {
await state.initialize();
}
// 确保有会话
let sessionId = state.currentSessionId;
if (!sessionId) {
sessionId = await state.createSession();
}
// 创建用户消息
const userMsg: Message = {
id: crypto.randomUUID(),
role: "user",
@@ -217,7 +219,6 @@ export const useAgentStore = create<AgentState>((set, get) => ({
timestamp: new Date(),
};
// 创建助手消息占位符
const assistantMsgId = crypto.randomUUID();
const assistantMsg: Message = {
id: assistantMsgId,
@@ -229,47 +230,45 @@ export const useAgentStore = create<AgentState>((set, get) => ({
contentParts: [],
};
// 更新状态
set((s) => ({
messages: [...s.messages, userMsg, assistantMsg],
set((currentState) => ({
messages: [...currentState.messages, userMsg, assistantMsg],
isStreaming: true,
currentAssistantMsgId: assistantMsgId,
}));
// 创建唯一事件名称
const eventName = `aster_stream_${assistantMsgId}`;
try {
// 设置事件监听器
eventUnlisten = await listen<TauriAgentEvent>(eventName, (event) => {
get()._handleEvent(event.payload);
const workspaceId = await resolveDefaultWorkspaceId();
get()._cleanup();
eventUnlisten = await safeListen<unknown>(eventName, (event) => {
const parsedEvent = parseStreamEvent(event.payload);
if (!parsedEvent) {
return;
}
get()._handleEvent(parsedEvent);
});
// 发送请求
await invoke("aster_agent_chat_stream", {
request: {
message: content,
session_id: sessionId,
event_name: eventName,
images: images?.map((img) => ({
data: img.data,
media_type: img.mediaType,
})),
},
});
await sendAsterMessageStream(
content,
sessionId,
eventName,
workspaceId,
toRuntimeImages(images),
);
} catch (error) {
console.error("[AgentStore] 发送消息失败:", error);
// 更新消息状态为错误
set((s) => ({
messages: s.messages.map((msg) =>
msg.id === assistantMsgId
set((currentState) => ({
messages: currentState.messages.map((message) =>
message.id === assistantMsgId
? {
...msg,
...message,
isThinking: false,
content: `错误: ${error}`,
}
: msg,
: message,
),
isStreaming: false,
currentAssistantMsgId: null,
@@ -280,29 +279,25 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 停止流式响应
stopStreaming: async () => {
const state = get();
if (!state.currentSessionId) return;
try {
await invoke("aster_agent_stop", {
sessionId: state.currentSessionId,
});
await stopAsterSession(state.currentSessionId);
} catch (error) {
console.error("[AgentStore] 停止失败:", error);
}
// 更新消息状态
set((s) => ({
messages: s.messages.map((msg) =>
msg.id === s.currentAssistantMsgId
set((currentState) => ({
messages: currentState.messages.map((message) =>
message.id === currentState.currentAssistantMsgId
? {
...msg,
...message,
isThinking: false,
content: msg.content || "(已停止生成)",
content: message.content || "(已停止生成)",
}
: msg,
: message,
),
isStreaming: false,
currentAssistantMsgId: null,
@@ -311,14 +306,14 @@ export const useAgentStore = create<AgentState>((set, get) => ({
get()._cleanup();
},
// 确认权限请求
confirmAction: async (response: ConfirmResponse) => {
try {
const state = get();
const actionType =
response.actionType ||
state.pendingActions.find((a) => a.requestId === response.requestId)
?.actionType;
state.pendingActions.find(
(item) => item.requestId === response.requestId,
)?.actionType;
if (actionType === "elicitation" || actionType === "ask_user") {
if (!state.currentSessionId) {
@@ -345,27 +340,22 @@ export const useAgentStore = create<AgentState>((set, get) => ({
userData = "";
}
await invoke("aster_agent_submit_elicitation_response", {
sessionId: state.currentSessionId,
request: {
request_id: response.requestId,
user_data: userData,
},
});
await submitAsterElicitationResponse(
state.currentSessionId,
response.requestId,
userData,
);
} else {
await invoke("aster_agent_confirm", {
request: {
request_id: response.requestId,
confirmed: response.confirmed,
response: response.response,
},
});
await confirmAsterAction(
response.requestId,
response.confirmed,
response.response,
);
}
// 移除已处理的请求
set((s) => ({
pendingActions: s.pendingActions.filter(
(a) => a.requestId !== response.requestId,
set((currentState) => ({
pendingActions: currentState.pendingActions.filter(
(item) => item.requestId !== response.requestId,
),
}));
} catch (error) {
@@ -374,43 +364,28 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 切换会话
switchSession: async (sessionId: string) => {
try {
const detail = await invoke<{
id: string;
name?: string;
messages: Array<{
role: string;
content: string;
timestamp: string;
}>;
}>("aster_session_get", { sessionId });
// 转换消息格式
const messages: Message[] = detail.messages.map((msg, index) => ({
const detail = await getAsterSession(sessionId);
const messages: Message[] = detail.messages.map((message, index) => ({
id: `${sessionId}-${index}`,
role: msg.role as "user" | "assistant",
content: msg.content,
timestamp: new Date(msg.timestamp),
role: message.role === "user" ? "user" : "assistant",
content: extractSessionMessageText(message.content),
timestamp: new Date(message.timestamp),
}));
set({
currentSessionId: sessionId,
messages,
pendingActions: [],
isStreaming: false,
currentAssistantMsgId: null,
});
} catch (error) {
console.error("[AgentStore] 切换会话失败:", error);
if (isAsterSessionNotFoundError(error)) {
try {
const sessions = await invoke<
Array<{
id: string;
created_at: number;
updated_at: number;
}>
>("aster_session_list");
const sessions = await listAsterSessions();
const recoveredSessionId = resolveRestorableSessionId({
candidateSessionId: null,
sessions: sessions.map((item) => ({
@@ -429,6 +404,8 @@ export const useAgentStore = create<AgentState>((set, get) => ({
set({
messages: [],
pendingActions: [],
isStreaming: false,
currentAssistantMsgId: null,
});
return;
} catch (recoveryError) {
@@ -439,17 +416,17 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 创建会话
createSession: async (name?: string) => {
try {
const sessionId = await invoke<string>("aster_session_create", {
workingDir: null,
name,
});
const workspaceId = await resolveDefaultWorkspaceId();
const sessionId = await createAsterSession(workspaceId, undefined, name);
set((s) => ({
set((currentState) => ({
currentSessionId: sessionId,
messages: [],
pendingActions: [],
isStreaming: false,
currentAssistantMsgId: null,
sessions: [
{
id: sessionId,
@@ -458,7 +435,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
updatedAt: new Date(),
messagesCount: 0,
},
...s.sessions,
...currentState.sessions,
],
}));
@@ -469,46 +446,45 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 删除会话
deleteSession: async (sessionId: string) => {
// TODO: 实现后端删除接口
set((s) => ({
sessions: s.sessions.filter((sess) => sess.id !== sessionId),
...(s.currentSessionId === sessionId
? { currentSessionId: null, messages: [] }
await deleteAsterSession(sessionId);
set((currentState) => ({
sessions: currentState.sessions.filter(
(session) => session.id !== sessionId,
),
...(currentState.currentSessionId === sessionId
? {
currentSessionId: null,
messages: [],
pendingActions: [],
isStreaming: false,
currentAssistantMsgId: null,
}
: {}),
}));
},
// 清空消息
clearMessages: () => {
set({
messages: [],
currentSessionId: null,
pendingActions: [],
isStreaming: false,
currentAssistantMsgId: null,
});
},
// 加载会话列表
loadSessions: async () => {
try {
const sessions = await invoke<
Array<{
id: string;
name?: string;
created_at: string;
updated_at: string;
messages_count: number;
}>
>("aster_session_list");
const sessions = await listAsterSessions();
set({
sessions: sessions.map((s) => ({
id: s.id,
name: s.name,
createdAt: new Date(s.created_at),
updatedAt: new Date(s.updated_at),
messagesCount: s.messages_count,
sessions: sessions.map((session) => ({
id: session.id,
name: session.name,
createdAt: new Date(session.created_at),
updatedAt: new Date(session.updated_at),
messagesCount: session.messages_count || 0,
})),
});
} catch (error) {
@@ -516,8 +492,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 处理事件
_handleEvent: (event: TauriAgentEvent) => {
_handleEvent: (event: StreamEvent) => {
const state = get();
const msgId = state.currentAssistantMsgId;
if (!msgId) return;
@@ -526,55 +501,55 @@ export const useAgentStore = create<AgentState>((set, get) => ({
switch (event.type) {
case "text_delta":
set((s) => ({
messages: s.messages.map((msg) => {
if (msg.id !== msgId) return msg;
set((currentState) => ({
messages: currentState.messages.map((message) => {
if (message.id !== msgId) return message;
const newContent = msg.content + event.text;
const newParts = [...(msg.contentParts || [])];
const nextContent = message.content + event.text;
const nextParts = [...(message.contentParts || [])];
const lastPart = nextParts[nextParts.length - 1];
// 追加到最后一个 text 类型,或创建新的
const lastPart = newParts[newParts.length - 1];
if (lastPart && lastPart.type === "text") {
newParts[newParts.length - 1] = {
nextParts[nextParts.length - 1] = {
type: "text",
text: lastPart.text + event.text,
};
} else {
newParts.push({ type: "text", text: event.text });
nextParts.push({ type: "text", text: event.text });
}
return {
...msg,
content: newContent,
...message,
content: nextContent,
isThinking: false,
thinkingContent: undefined,
contentParts: newParts,
contentParts: nextParts,
};
}),
}));
break;
case "thinking_delta":
set((s) => ({
messages: s.messages.map((msg) => {
if (msg.id !== msgId) return msg;
set((currentState) => ({
messages: currentState.messages.map((message) => {
if (message.id !== msgId) return message;
const nextParts = [...(message.contentParts || [])];
const lastPart = nextParts[nextParts.length - 1];
const newParts = [...(msg.contentParts || [])];
const lastPart = newParts[newParts.length - 1];
if (lastPart && lastPart.type === "thinking") {
newParts[newParts.length - 1] = {
nextParts[nextParts.length - 1] = {
type: "thinking",
text: lastPart.text + event.text,
};
} else {
newParts.push({ type: "thinking", text: event.text });
nextParts.push({ type: "thinking", text: event.text });
}
return {
...msg,
thinkingContent: (msg.thinkingContent || "") + event.text,
contentParts: newParts,
...message,
thinkingContent: (message.thinkingContent || "") + event.text,
contentParts: nextParts,
};
}),
}));
@@ -589,21 +564,23 @@ export const useAgentStore = create<AgentState>((set, get) => ({
startTime: new Date(),
};
set((s) => ({
messages: s.messages.map((msg) => {
if (msg.id !== msgId) return msg;
// 检查是否已存在
if (msg.toolCalls?.find((tc) => tc.id === event.tool_id)) {
return msg;
set((currentState) => ({
messages: currentState.messages.map((message) => {
if (message.id !== msgId) return message;
if (
message.toolCalls?.find(
(toolCall) => toolCall.id === event.tool_id,
)
) {
return message;
}
return {
...msg,
toolCalls: [...(msg.toolCalls || []), newToolCall],
...message,
toolCalls: [...(message.toolCalls || []), newToolCall],
contentParts: [
...(msg.contentParts || []),
{ type: "tool_use" as const, toolCall: newToolCall },
...(message.contentParts || []),
{ type: "tool_use", toolCall: newToolCall },
],
};
}),
@@ -612,75 +589,63 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
case "tool_end":
set((s) => ({
messages: s.messages.map((msg) => {
if (msg.id !== msgId) return msg;
set((currentState) => ({
messages: currentState.messages.map((message) => {
if (message.id !== msgId) return message;
const updatedToolCalls = (msg.toolCalls || []).map((tc) =>
tc.id === event.tool_id
const nextToolCalls = (message.toolCalls || []).map((toolCall) =>
toolCall.id === event.tool_id
? {
...tc,
status: event.result.success
? ("completed" as const)
: ("failed" as const),
...toolCall,
status: event.result.success ? "completed" : "failed",
result: event.result,
endTime: new Date(),
}
: tc,
: toolCall,
);
const updatedContentParts = (msg.contentParts || []).map((part) => {
if (
part.type === "tool_use" &&
part.toolCall.id === event.tool_id
) {
const nextContentParts = (message.contentParts || []).map(
(part) => {
if (
part.type !== "tool_use" ||
part.toolCall.id !== event.tool_id
) {
return part;
}
return {
...part,
toolCall: {
...part.toolCall,
status: event.result.success
? ("completed" as const)
: ("failed" as const),
status: event.result.success ? "completed" : "failed",
result: event.result,
endTime: new Date(),
},
};
}
return part;
});
},
);
return {
...msg,
toolCalls: updatedToolCalls,
contentParts: updatedContentParts,
...message,
toolCalls: nextToolCalls,
contentParts: nextContentParts,
};
}),
}));
break;
case "action_required":
set((s) => ({
set((currentState) => ({
pendingActions: [
...s.pendingActions,
...currentState.pendingActions,
{
requestId: event.request_id,
actionType: event.action_type as ActionRequired["actionType"],
toolName: event.data.tool_name as string | undefined,
arguments: event.data.arguments as
| Record<string, unknown>
| undefined,
prompt:
(event.data.prompt as string | undefined) ||
(event.data.message as string | undefined),
requestedSchema: event.data.requested_schema as
| Record<string, unknown>
| undefined,
options: event.data.options as
| Array<{
label: string;
description?: string;
}>
| undefined,
toolName: event.tool_name,
arguments: event.arguments,
prompt: event.prompt || event.questions?.[0]?.question,
requestedSchema: event.requested_schema,
options: event.questions?.[0]?.options,
timestamp: new Date(),
},
],
@@ -688,20 +653,19 @@ export const useAgentStore = create<AgentState>((set, get) => ({
break;
case "done":
// 单次响应完成,但工具循环可能继续
console.log("[AgentStore] done 事件,等待 final_done...");
break;
case "final_done":
set((s) => ({
messages: s.messages.map((msg) =>
msg.id === msgId
set((currentState) => ({
messages: currentState.messages.map((message) =>
message.id === msgId
? {
...msg,
...message,
isThinking: false,
usage: event.usage,
}
: msg,
: message,
),
isStreaming: false,
currentAssistantMsgId: null,
@@ -710,15 +674,15 @@ export const useAgentStore = create<AgentState>((set, get) => ({
break;
case "error":
set((s) => ({
messages: s.messages.map((msg) =>
msg.id === msgId
set((currentState) => ({
messages: currentState.messages.map((message) =>
message.id === msgId
? {
...msg,
...message,
isThinking: false,
content: msg.content || `错误: ${event.message}`,
content: message.content || `错误: ${event.message}`,
}
: msg,
: message,
),
isStreaming: false,
currentAssistantMsgId: null,
@@ -732,17 +696,16 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
},
// 清理资源
_cleanup: () => {
if (eventUnlisten) {
eventUnlisten();
eventUnlisten = null;
}
const cleanup = eventUnlisten;
eventUnlisten = null;
cleanup?.();
},
}));
// 导出便捷 hooks
export const useAgentMessages = () => useAgentStore((s) => s.messages);
export const useAgentStreaming = () => useAgentStore((s) => s.isStreaming);
export const useAgentSessions = () => useAgentStore((s) => s.sessions);
export const usePendingActions = () => useAgentStore((s) => s.pendingActions);
export const useAgentMessages = () => useAgentStore((state) => state.messages);
export const useAgentStreaming = () =>
useAgentStore((state) => state.isStreaming);
export const useAgentSessions = () => useAgentStore((state) => state.sessions);
export const usePendingActions = () =>
useAgentStore((state) => state.pendingActions);
+2
View File
@@ -1,5 +1,7 @@
/**
* Stores 导出
*
* @deprecated `agentStore` 属于遗留导出,请不要在新代码中继续依赖。
*/
// Aster Agent Store