feat: implement OpenClaw process owner and runtime path utilities
- Add OpenClawProcessOwner class to manage the lifecycle of the OpenClaw process. - Introduce utility functions for managing OpenClaw runtime paths. - Update session store to normalize agent session keys and migrate existing keys. - Refactor main process to handle local provider API routing through a new dispatch function. - Enhance token usage writer to utilize a new session key parsing function. - Create agents management store to handle agent data and interactions. - Update chat store to integrate agent selection and session management. - Introduce AgentsSection component for displaying agent information in the UI. - Refactor HomePage to support agent selection and display current agent. - Update routing to reflect new agents page structure.
This commit is contained in:
@@ -15,11 +15,13 @@ import {
|
||||
import { IPC_EVENTS } from '../../lib/constants';
|
||||
import { invokeIpc } from '../../lib/host-api';
|
||||
import {
|
||||
agentsStore,
|
||||
channelStore,
|
||||
chatStore,
|
||||
getCompletedTasks,
|
||||
getPendingTasks,
|
||||
taskStore,
|
||||
useAgentsStore,
|
||||
useChannelStore,
|
||||
useChatStore,
|
||||
useTaskStore,
|
||||
@@ -147,6 +149,7 @@ function mapMessages(messages: RawMessage[], streamingMessage: RawMessage | null
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const agentsState = useAgentsStore();
|
||||
const chat = useChatStore();
|
||||
const taskState = useTaskStore();
|
||||
const channelState = useChannelStore();
|
||||
@@ -158,6 +161,7 @@ export default function HomePage() {
|
||||
const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void agentsStore.init();
|
||||
void chatStore.init();
|
||||
void taskStore.init();
|
||||
void channelStore.init();
|
||||
@@ -208,6 +212,10 @@ export default function HomePage() {
|
||||
const latestTask = currentTaskSource[0];
|
||||
|
||||
const visibleMessages = mapMessages(chat.messages, chat.streamingMessage);
|
||||
const selectedAgentId = agentsState.agents.some((agent) => agent.id === chat.currentAgentId)
|
||||
? chat.currentAgentId
|
||||
: agentsState.defaultAgentId;
|
||||
const currentAgent = agentsState.agents.find((agent) => agent.id === selectedAgentId) || null;
|
||||
|
||||
async function handleSendMessage(): Promise<void> {
|
||||
const sent = await chatStore.sendMessage(inputMessage, attachments);
|
||||
@@ -279,7 +287,7 @@ export default function HomePage() {
|
||||
loading={!chat.initialized}
|
||||
selectedConversationId={chat.currentSessionKey}
|
||||
onNewChat={() => {
|
||||
void chatStore.newSession();
|
||||
void chatStore.newSession(selectedAgentId || undefined);
|
||||
}}
|
||||
onSelectConversation={(conversationId) => {
|
||||
chatStore.switchSession(conversationId);
|
||||
@@ -307,18 +315,39 @@ export default function HomePage() {
|
||||
<h2 className="text-base font-semibold text-[#171717] dark:text-gray-100">智能对话</h2>
|
||||
<div className="mt-1 text-xs text-[#99A0AE] dark:text-gray-500">
|
||||
网关状态:{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'}
|
||||
{currentAgent ? ` · 当前代理:${currentAgent.name}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
|
||||
onClick={() => {
|
||||
void chatStore.loadSessions();
|
||||
void chatStore.loadHistory();
|
||||
}}
|
||||
>
|
||||
刷新会话
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300">
|
||||
<span>代理</span>
|
||||
<select
|
||||
className="rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] outline-none transition-colors hover:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300"
|
||||
disabled={agentsState.loading || agentsState.agents.length === 0}
|
||||
value={selectedAgentId}
|
||||
onChange={(event) => {
|
||||
chatStore.selectAgent(event.target.value);
|
||||
}}
|
||||
>
|
||||
{agentsState.agents.map((agent) => (
|
||||
<option key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
|
||||
onClick={() => {
|
||||
void agentsStore.load();
|
||||
void chatStore.loadSessions();
|
||||
void chatStore.loadHistory();
|
||||
}}
|
||||
>
|
||||
刷新会话
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
|
||||
84
src/pages/agents/components/AgentsSection.tsx
Normal file
84
src/pages/agents/components/AgentsSection.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useEffect } from 'react';
|
||||
import { agentsStore, useAgentsStore } from '../../../stores';
|
||||
|
||||
const CHIP_CLASS_NAME = [
|
||||
'rounded-full border px-2.5 py-1 text-[11px] leading-none',
|
||||
'border-[#E5E8EE] text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300',
|
||||
].join(' ');
|
||||
|
||||
export default function AgentsSection() {
|
||||
const agentState = useAgentsStore();
|
||||
|
||||
useEffect(() => {
|
||||
void agentsStore.init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="space-y-5">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-[18px] font-semibold leading-[24px] text-[#171717] dark:text-gray-100">
|
||||
Agents Snapshot
|
||||
</h3>
|
||||
<p className="mt-1 text-[13px] leading-[20px] text-[#99A0AE] dark:text-gray-500">
|
||||
当前本地 `agents` 契约与 `mainSessionKey` 映射。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-[12px] text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
|
||||
onClick={() => {
|
||||
void agentsStore.load();
|
||||
}}
|
||||
>
|
||||
刷新 Agents
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{agentState.error ? (
|
||||
<div className="rounded-[14px] border border-[#F1D4D4] bg-[#FFF5F5] px-4 py-3 text-sm text-[#A53A3A] dark:border-[#4b2a2a] dark:bg-[#2a1f1f] dark:text-[#f7b8b8]">
|
||||
{agentState.error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{agentState.agents.map((agent) => (
|
||||
<article
|
||||
key={agent.id}
|
||||
className="rounded-[16px] border border-[#E5E8EE] bg-[#FAFBFC] p-4 dark:border-[#2a2a2d] dark:bg-[#202024]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[15px] font-semibold text-[#171717] dark:text-gray-100">
|
||||
{agent.name}
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] text-[#99A0AE] dark:text-gray-500">{agent.id}</div>
|
||||
</div>
|
||||
{agent.isDefault ? (
|
||||
<span className="rounded-full bg-[#EFF6FF] px-2.5 py-1 text-[11px] font-medium text-[#2B7FFF] dark:bg-[#1d2633]">
|
||||
默认
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<span className={CHIP_CLASS_NAME}>Provider: {agent.providerAccountId || '--'}</span>
|
||||
<span className={CHIP_CLASS_NAME}>Model: {agent.modelDisplay || '--'}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-[12px] border border-dashed border-[#DCE5F1] bg-white px-3 py-2 text-[12px] leading-[18px] text-[#525866] dark:border-[#2a2a2d] dark:bg-[#17171a] dark:text-gray-300">
|
||||
<div className="font-medium text-[#171717] dark:text-gray-100">mainSessionKey</div>
|
||||
<div className="mt-1 break-all">{agent.mainSessionKey}</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
{!agentState.loading && agentState.agents.length === 0 ? (
|
||||
<div className="rounded-[16px] border border-dashed border-[#DCE5F1] bg-[#FAFBFC] px-4 py-6 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-300">
|
||||
当前还没有可用 agent。先在下方配置 provider 账号后,这里会自动生成可路由的 agent snapshot。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import AgentsSection from './components/AgentsSection';
|
||||
import ProvidersSection from './components/ProvidersSection';
|
||||
import UsageHistorySection from './components/UsageHistorySection';
|
||||
|
||||
@@ -17,6 +18,7 @@ export default function AgentsPage() {
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-12 overflow-y-auto pb-10 pr-2">
|
||||
<AgentsSection />
|
||||
<ProvidersSection />
|
||||
<UsageHistorySection />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-
|
||||
import MainLayout from '../components/layout/MainLayout';
|
||||
import HomePage from '../pages/Home';
|
||||
import LoginPage from '../pages/Login';
|
||||
import AgentsPage from '../pages/Agents';
|
||||
import AgentsPage from '../pages/agents';
|
||||
import SkillsPage from '../pages/Skills';
|
||||
import CronPage from '../pages/Cron';
|
||||
import ScriptsPage from '../pages/Scripts';
|
||||
|
||||
144
src/stores/agents.ts
Normal file
144
src/stores/agents.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import {
|
||||
DEFAULT_AGENT_ID,
|
||||
DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
buildMainSessionKey,
|
||||
normalizeAgentId,
|
||||
type AgentSummary,
|
||||
type AgentsSnapshot,
|
||||
} from '@runtime/lib/agents';
|
||||
import { hostApiFetch } from '../lib/host-api';
|
||||
|
||||
export interface AgentsStoreState {
|
||||
initialized: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
agents: AgentSummary[];
|
||||
defaultAgentId: string;
|
||||
defaultProviderAccountId: string | null;
|
||||
defaultModelRef: string | null;
|
||||
mainSessionSuffix: string;
|
||||
}
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
let loadAgentsInFlight: Promise<void> | null = null;
|
||||
let state: AgentsStoreState = {
|
||||
initialized: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
agents: [],
|
||||
defaultAgentId: DEFAULT_AGENT_ID,
|
||||
defaultProviderAccountId: null,
|
||||
defaultModelRef: null,
|
||||
mainSessionSuffix: DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
};
|
||||
|
||||
function emit(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function patchState(patch: Partial<AgentsStoreState>): AgentsStoreState {
|
||||
state = { ...state, ...patch };
|
||||
emit();
|
||||
return state;
|
||||
}
|
||||
|
||||
function sanitizeAgent(agent: AgentSummary): AgentSummary {
|
||||
const normalizedId = normalizeAgentId(agent.id);
|
||||
const normalizedMainSessionKey = agent.mainSessionKey || buildMainSessionKey(normalizedId);
|
||||
|
||||
return {
|
||||
id: normalizedId,
|
||||
name: agent.name || normalizedId,
|
||||
isDefault: Boolean(agent.isDefault),
|
||||
providerAccountId: agent.providerAccountId ?? null,
|
||||
modelRef: agent.modelRef ?? null,
|
||||
modelDisplay: agent.modelDisplay || agent.modelRef || agent.name || normalizedId,
|
||||
mainSessionKey: normalizedMainSessionKey,
|
||||
vendorId: agent.vendorId ?? null,
|
||||
source: agent.source,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAgents(): Promise<void> {
|
||||
if (loadAgentsInFlight) {
|
||||
await loadAgentsInFlight;
|
||||
return;
|
||||
}
|
||||
|
||||
loadAgentsInFlight = (async () => {
|
||||
patchState({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshot & { success?: boolean }>('/api/agents');
|
||||
const agents = Array.isArray(snapshot?.agents)
|
||||
? snapshot.agents.map((agent) => sanitizeAgent(agent))
|
||||
: [];
|
||||
|
||||
patchState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
agents,
|
||||
defaultAgentId: snapshot?.defaultAgentId ? normalizeAgentId(snapshot.defaultAgentId) : DEFAULT_AGENT_ID,
|
||||
defaultProviderAccountId: snapshot?.defaultProviderAccountId ?? null,
|
||||
defaultModelRef: snapshot?.defaultModelRef ?? null,
|
||||
mainSessionSuffix: snapshot?.mainSessionSuffix || DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
});
|
||||
} catch (error) {
|
||||
patchState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
await loadAgentsInFlight;
|
||||
} finally {
|
||||
loadAgentsInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
function getSnapshot(): AgentsStoreState {
|
||||
return state;
|
||||
}
|
||||
|
||||
function getAgentById(agentId: string | null | undefined): AgentSummary | undefined {
|
||||
const normalizedId = normalizeAgentId(agentId);
|
||||
return state.agents.find((agent) => agent.id === normalizedId);
|
||||
}
|
||||
|
||||
function resolveMainSessionKey(agentId: string | null | undefined): string {
|
||||
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
||||
return getAgentById(normalizedId)?.mainSessionKey || buildMainSessionKey(normalizedId, state.mainSessionSuffix);
|
||||
}
|
||||
|
||||
function resolveProviderAccountId(agentId: string | null | undefined): string | null {
|
||||
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
||||
return getAgentById(normalizedId)?.providerAccountId ?? state.defaultProviderAccountId;
|
||||
}
|
||||
|
||||
export const agentsStore = {
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
getState: () => state,
|
||||
init: loadAgents,
|
||||
load: loadAgents,
|
||||
getAgentById,
|
||||
resolveMainSessionKey,
|
||||
resolveProviderAccountId,
|
||||
};
|
||||
|
||||
export function useAgentsStore(): AgentsStoreState {
|
||||
return useSyncExternalStore(agentsStore.subscribe, agentsStore.getSnapshot, agentsStore.getSnapshot);
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import {
|
||||
DEFAULT_AGENT_ID as FALLBACK_AGENT_ID,
|
||||
DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
buildAgentSessionKey,
|
||||
buildMainSessionKey,
|
||||
normalizeAgentId,
|
||||
normalizeAgentSessionKey,
|
||||
parseSessionKey,
|
||||
} from '@runtime/lib/agents';
|
||||
import type { ChatSession, RawMessage, ToolStatus } from '../shared/chat-model';
|
||||
import { extractText, isToolOnlyMessage } from '../shared/chat-model';
|
||||
import { gatewayRpc, onGatewayEvent } from '../lib/gateway-client';
|
||||
import { hostApiFetch } from '../lib/host-api';
|
||||
import type { GatewayEvent } from '../types/runtime';
|
||||
import { agentsStore } from './agents';
|
||||
|
||||
const DEFAULT_SESSION_KEY = 'agent:main:main';
|
||||
const DEFAULT_AGENT_ID = '1953462165250859011';
|
||||
const SESSION_LOAD_MIN_INTERVAL_MS = 1200;
|
||||
const HISTORY_LOAD_MIN_INTERVAL_MS = 800;
|
||||
const CHAT_EVENT_DEDUPE_TTL_MS = 30000;
|
||||
const FALLBACK_MAIN_SESSION_KEY = buildMainSessionKey(FALLBACK_AGENT_ID, DEFAULT_MAIN_SESSION_SUFFIX);
|
||||
|
||||
export interface StagedAttachment {
|
||||
fileName: string;
|
||||
@@ -19,6 +28,18 @@ export interface StagedAttachment {
|
||||
preview: string | null;
|
||||
}
|
||||
|
||||
interface TranscriptMessageLike extends Omit<RawMessage, 'timestamp'> {
|
||||
timestamp?: string | number;
|
||||
}
|
||||
|
||||
interface SessionTranscriptResponse {
|
||||
sessionKey: string;
|
||||
sessionId: string;
|
||||
agentId: string;
|
||||
transcriptPath: string;
|
||||
messages: TranscriptMessageLike[];
|
||||
}
|
||||
|
||||
export interface ChatStoreState {
|
||||
initialized: boolean;
|
||||
messages: RawMessage[];
|
||||
@@ -58,8 +79,8 @@ let state: ChatStoreState = {
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
sessions: [],
|
||||
currentSessionKey: DEFAULT_SESSION_KEY,
|
||||
currentAgentId: DEFAULT_AGENT_ID,
|
||||
currentSessionKey: FALLBACK_MAIN_SESSION_KEY,
|
||||
currentAgentId: FALLBACK_AGENT_ID,
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
gatewayStatus: 'disconnected',
|
||||
@@ -78,9 +99,25 @@ function patchState(patch: Partial<ChatStoreState>): ChatStoreState {
|
||||
}
|
||||
|
||||
function getAgentIdFromSessionKey(sessionKey: string): string {
|
||||
if (!sessionKey.startsWith('agent:')) return DEFAULT_AGENT_ID;
|
||||
const parts = sessionKey.split(':');
|
||||
return parts[1] || DEFAULT_AGENT_ID;
|
||||
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
|
||||
if (parsed.isAgentSession) return parsed.agentId;
|
||||
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||
}
|
||||
|
||||
function getDefaultAgentId(): string {
|
||||
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||
}
|
||||
|
||||
function getDefaultMainSessionKey(): string {
|
||||
return agentsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY;
|
||||
}
|
||||
|
||||
function resolveMainSessionKeyForAgent(agentId: string | null | undefined): string {
|
||||
return agentsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey();
|
||||
}
|
||||
|
||||
function buildNewSessionKey(agentId: string | null | undefined): string {
|
||||
return buildAgentSessionKey(agentId || getDefaultAgentId(), `session-${Date.now()}`);
|
||||
}
|
||||
|
||||
function clearSessionEntryFromMap<T extends Record<string, unknown>>(entries: T, sessionKey: string): T {
|
||||
@@ -170,6 +207,14 @@ async function resolveDefaultAccountId(): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveProviderAccountIdForAgent(agentId: string | null | undefined): Promise<string | null> {
|
||||
const mappedAccountId = agentsStore.resolveProviderAccountId(agentId);
|
||||
if (mappedAccountId) {
|
||||
return mappedAccountId;
|
||||
}
|
||||
return resolveDefaultAccountId();
|
||||
}
|
||||
|
||||
async function stageBuffer(base64: string, fileName: string, mimeType: string): Promise<StagedAttachment> {
|
||||
try {
|
||||
const result = await hostApiFetch<StagedAttachment>('/api/files/stage-buffer', {
|
||||
@@ -194,6 +239,31 @@ async function stageBuffer(base64: string, fileName: string, mimeType: string):
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTranscriptMessage(message: TranscriptMessageLike): RawMessage {
|
||||
const rawTimestamp = message.timestamp;
|
||||
let timestamp: number | undefined;
|
||||
|
||||
if (typeof rawTimestamp === 'number') {
|
||||
timestamp = rawTimestamp;
|
||||
} else if (typeof rawTimestamp === 'string') {
|
||||
const parsed = Date.parse(rawTimestamp);
|
||||
timestamp = Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadTranscriptHistory(sessionKey: string): Promise<RawMessage[]> {
|
||||
const query = new URLSearchParams({ sessionKey });
|
||||
const response = await hostApiFetch<SessionTranscriptResponse>(`/api/sessions/transcript?${query.toString()}`);
|
||||
return Array.isArray(response?.messages)
|
||||
? response.messages.map((message) => normalizeTranscriptMessage(message))
|
||||
: [];
|
||||
}
|
||||
|
||||
async function subscribeToGateway(): Promise<void> {
|
||||
if (gatewaySubscribed) return;
|
||||
|
||||
@@ -213,6 +283,7 @@ async function subscribeToGateway(): Promise<void> {
|
||||
}
|
||||
|
||||
async function loadSessions(): Promise<void> {
|
||||
await agentsStore.init();
|
||||
const now = Date.now();
|
||||
if (loadSessionsInFlight) {
|
||||
await loadSessionsInFlight;
|
||||
@@ -225,19 +296,46 @@ async function loadSessions(): Promise<void> {
|
||||
loadSessionsInFlight = (async () => {
|
||||
try {
|
||||
const localKeys = await gatewayRpc<string[]>('session.list', {});
|
||||
let sessions: ChatSession[] = localKeys.map((key) => ({
|
||||
const normalizedKeys = Array.from(
|
||||
new Set(localKeys.map((key) => normalizeAgentSessionKey(key)).filter(Boolean)),
|
||||
);
|
||||
const canonicalBySuffix = new Map<string, string>();
|
||||
for (const key of normalizedKeys) {
|
||||
const parsed = parseSessionKey(key);
|
||||
if (parsed.isAgentSession && parsed.sessionId && !canonicalBySuffix.has(parsed.sessionId)) {
|
||||
canonicalBySuffix.set(parsed.sessionId, parsed.sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
let sessions: ChatSession[] = normalizedKeys.map((key) => ({
|
||||
key,
|
||||
displayName: state.sessionLabels[key] || 'New Chat',
|
||||
updatedAt: state.sessionLastActivity[key] || Date.now(),
|
||||
}));
|
||||
|
||||
const existingNonLocal = state.sessions.filter((session) => !session.key.startsWith('local:'));
|
||||
sessions = [...existingNonLocal, ...sessions];
|
||||
const existingTransient = state.sessions.filter((session) => {
|
||||
const parsed = parseSessionKey(session.key);
|
||||
return !parsed.isAgentSession;
|
||||
});
|
||||
sessions = [...existingTransient, ...sessions];
|
||||
|
||||
let nextSessionKey = state.currentSessionKey || DEFAULT_SESSION_KEY;
|
||||
let nextSessionKey = state.currentSessionKey || getDefaultMainSessionKey();
|
||||
const normalizedCurrentSessionKey = normalizeAgentSessionKey(nextSessionKey);
|
||||
if (normalizedCurrentSessionKey.startsWith('agent:')) {
|
||||
nextSessionKey = normalizedCurrentSessionKey;
|
||||
}
|
||||
if (!nextSessionKey.startsWith('agent:')) {
|
||||
const canonicalMatch = canonicalBySuffix.get(nextSessionKey);
|
||||
if (canonicalMatch) {
|
||||
nextSessionKey = canonicalMatch;
|
||||
}
|
||||
}
|
||||
if (!sessions.find((session) => session.key === nextSessionKey) && sessions.length > 0) {
|
||||
nextSessionKey = sessions[0].key;
|
||||
}
|
||||
if (!nextSessionKey) {
|
||||
nextSessionKey = getDefaultMainSessionKey();
|
||||
}
|
||||
|
||||
const sessionsWithCurrent =
|
||||
!sessions.find((session) => session.key === nextSessionKey) && nextSessionKey
|
||||
@@ -261,7 +359,7 @@ async function loadSessions(): Promise<void> {
|
||||
},
|
||||
});
|
||||
|
||||
if (nextSessionKey && nextSessionKey !== DEFAULT_SESSION_KEY) {
|
||||
if (nextSessionKey) {
|
||||
await loadHistory(nextSessionKey, true);
|
||||
}
|
||||
|
||||
@@ -316,7 +414,7 @@ async function loadSessions(): Promise<void> {
|
||||
}
|
||||
|
||||
async function loadHistory(sessionKey = state.currentSessionKey, quiet = false): Promise<void> {
|
||||
if (!sessionKey || sessionKey === DEFAULT_SESSION_KEY) {
|
||||
if (!sessionKey) {
|
||||
patchState({
|
||||
messages: [],
|
||||
loading: false,
|
||||
@@ -341,10 +439,24 @@ async function loadHistory(sessionKey = state.currentSessionKey, quiet = false):
|
||||
|
||||
const loadPromise = (async () => {
|
||||
try {
|
||||
const messages = await gatewayRpc<RawMessage[]>('chat.history', {
|
||||
sessionKey,
|
||||
limit: 50,
|
||||
});
|
||||
let messages: RawMessage[] = [];
|
||||
|
||||
try {
|
||||
messages = await gatewayRpc<RawMessage[]>('chat.history', {
|
||||
sessionKey,
|
||||
limit: 50,
|
||||
});
|
||||
} catch {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
try {
|
||||
messages = await loadTranscriptHistory(sessionKey);
|
||||
} catch {
|
||||
messages = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (state.currentSessionKey !== sessionKey) return;
|
||||
|
||||
@@ -414,20 +526,14 @@ async function loadHistory(sessionKey = state.currentSessionKey, quiet = false):
|
||||
}
|
||||
}
|
||||
|
||||
async function newSession(): Promise<void> {
|
||||
const defaultAccountId = await resolveDefaultAccountId();
|
||||
if (!defaultAccountId) {
|
||||
patchState({ error: '请先前往模型管理页面配置并设置一个默认模型' });
|
||||
return;
|
||||
}
|
||||
|
||||
async function newSession(agentId = state.currentAgentId): Promise<void> {
|
||||
const leavingEmpty =
|
||||
!state.currentSessionKey.endsWith(':main') &&
|
||||
state.messages.length === 0 &&
|
||||
!state.sessionLastActivity[state.currentSessionKey] &&
|
||||
!state.sessionLabels[state.currentSessionKey];
|
||||
|
||||
const newKey = `local:${defaultAccountId}:${crypto.randomUUID()}`;
|
||||
const newKey = buildNewSessionKey(agentId || getDefaultAgentId());
|
||||
const nextSessions = leavingEmpty
|
||||
? state.sessions.filter((session) => session.key !== state.currentSessionKey)
|
||||
: state.sessions;
|
||||
@@ -458,6 +564,16 @@ function switchSession(sessionKey: string): void {
|
||||
void loadHistory(sessionKey);
|
||||
}
|
||||
|
||||
function selectAgent(agentId: string): void {
|
||||
const normalizedAgentId = normalizeAgentId(agentId);
|
||||
const mainSessionKey = resolveMainSessionKeyForAgent(normalizedAgentId);
|
||||
if (mainSessionKey === state.currentSessionKey) {
|
||||
patchState({ currentAgentId: normalizedAgentId });
|
||||
return;
|
||||
}
|
||||
switchSession(mainSessionKey);
|
||||
}
|
||||
|
||||
async function deleteSession(sessionKey: string): Promise<void> {
|
||||
try {
|
||||
await gatewayRpc('session.delete', { sessionKey });
|
||||
@@ -473,7 +589,7 @@ async function deleteSession(sessionKey: string): Promise<void> {
|
||||
};
|
||||
|
||||
if (state.currentSessionKey === sessionKey) {
|
||||
const nextSession = remaining[0]?.key ?? DEFAULT_SESSION_KEY;
|
||||
const nextSession = remaining[0]?.key ?? getDefaultMainSessionKey();
|
||||
patchState({
|
||||
...basePatch,
|
||||
currentSessionKey: nextSession,
|
||||
@@ -487,7 +603,7 @@ async function deleteSession(sessionKey: string): Promise<void> {
|
||||
lastUserMessageAt: null,
|
||||
});
|
||||
|
||||
if (nextSession !== DEFAULT_SESSION_KEY) {
|
||||
if (nextSession) {
|
||||
await loadHistory(nextSession);
|
||||
}
|
||||
return;
|
||||
@@ -512,17 +628,18 @@ async function sendMessage(text: string, attachments: StagedAttachment[] = []):
|
||||
const trimmedText = text.trim();
|
||||
if (!trimmedText && attachments.length === 0) return false;
|
||||
|
||||
const defaultAccountId = await resolveDefaultAccountId();
|
||||
if (!defaultAccountId) {
|
||||
let targetSessionKey = state.currentSessionKey || resolveMainSessionKeyForAgent(state.currentAgentId);
|
||||
targetSessionKey = normalizeAgentSessionKey(targetSessionKey);
|
||||
if (!targetSessionKey) {
|
||||
targetSessionKey = getDefaultMainSessionKey();
|
||||
}
|
||||
const targetAgentId = getAgentIdFromSessionKey(targetSessionKey);
|
||||
const providerAccountId = await resolveProviderAccountIdForAgent(targetAgentId);
|
||||
if (!providerAccountId) {
|
||||
patchState({ error: '请先前往模型管理页面配置并设置一个默认模型' });
|
||||
return false;
|
||||
}
|
||||
|
||||
let targetSessionKey = state.currentSessionKey;
|
||||
if (!targetSessionKey || targetSessionKey === DEFAULT_SESSION_KEY) {
|
||||
targetSessionKey = `local:${defaultAccountId}:${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
const nowMs = Date.now();
|
||||
const userMessage: RawMessage = {
|
||||
role: 'user',
|
||||
@@ -559,7 +676,7 @@ async function sendMessage(text: string, attachments: StagedAttachment[] = []):
|
||||
lastUserMessageAt: nowMs,
|
||||
sessions: nextSessions,
|
||||
currentSessionKey: targetSessionKey,
|
||||
currentAgentId: getAgentIdFromSessionKey(targetSessionKey),
|
||||
currentAgentId: targetAgentId,
|
||||
sessionLabels: nextLabels,
|
||||
sessionLastActivity: {
|
||||
...state.sessionLastActivity,
|
||||
@@ -583,7 +700,7 @@ async function sendMessage(text: string, attachments: StagedAttachment[] = []):
|
||||
content: messageContent,
|
||||
},
|
||||
options: {
|
||||
providerAccountId: defaultAccountId,
|
||||
providerAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -617,7 +734,7 @@ async function abortRun(): Promise<void> {
|
||||
lastUserMessageAt: null,
|
||||
});
|
||||
|
||||
if (!sessionKey || sessionKey === DEFAULT_SESSION_KEY) return;
|
||||
if (!sessionKey) return;
|
||||
|
||||
try {
|
||||
await gatewayRpc('chat.abort', { sessionKey });
|
||||
@@ -733,6 +850,7 @@ function clearError(): void {
|
||||
}
|
||||
|
||||
async function initChatStore(): Promise<void> {
|
||||
await agentsStore.init();
|
||||
await subscribeToGateway();
|
||||
await loadSessions();
|
||||
}
|
||||
@@ -775,6 +893,7 @@ export const chatStore = {
|
||||
loadHistory,
|
||||
switchSession,
|
||||
newSession,
|
||||
selectAgent,
|
||||
deleteSession,
|
||||
renameSession,
|
||||
sendMessage,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './settings';
|
||||
export * from './agents';
|
||||
export * from './chat';
|
||||
export * from './task';
|
||||
export * from './channel';
|
||||
|
||||
Reference in New Issue
Block a user