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:
duanshuwen
2026-04-17 21:32:06 +08:00
parent eca70425cf
commit e9f3a29886
33 changed files with 1526 additions and 2428 deletions

View File

@@ -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">

View 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>
);
}

View File

@@ -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>