feat: add models management and usage history components
- Introduced RequestContentDialog for displaying request content details. - Added UsageBarChart for visualizing token usage data. - Implemented UsageHistorySection to manage and display usage history with filtering and pagination. - Created provider-types for managing provider-related types. - Developed ModelsPage to encapsulate models configuration, providers, and usage history. - Defined usage-history types and utility functions for managing usage data. - Updated routing to include models page and redirect agents to models. - Refactored chat store to integrate models instead of agents. - Established models store for managing model-related state and data fetching.
This commit is contained in:
@@ -1,2 +1,7 @@
|
|||||||
require('bytenode')
|
"use strict";
|
||||||
require('./main.jsc')
|
require("electron");
|
||||||
|
require("./main-Bp9J8VEe.js");
|
||||||
|
require("electron-squirrel-startup");
|
||||||
|
require("electron-log");
|
||||||
|
require("bytenode");
|
||||||
|
require("axios");
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { providerApiService } from '@electron/service/provider-api-service';
|
|||||||
import type { HostApiContext } from './context';
|
import type { HostApiContext } from './context';
|
||||||
import type { HostApiRequest } from './route-utils';
|
import type { HostApiRequest } from './route-utils';
|
||||||
import { normalizeRequest } from './route-utils';
|
import { normalizeRequest } from './route-utils';
|
||||||
import { handleAgentRoutes } from './routes/agents';
|
|
||||||
import { handleFileRoutes } from './routes/files';
|
import { handleFileRoutes } from './routes/files';
|
||||||
import { handleGatewayRoutes } from './routes/gateway';
|
import { handleGatewayRoutes } from './routes/gateway';
|
||||||
|
import { handleModelRoutes } from './routes/models';
|
||||||
import { handleProviderRoutes } from './routes/providers';
|
import { handleProviderRoutes } from './routes/providers';
|
||||||
import { handleSessionRoutes } from './routes/sessions';
|
import { handleSessionRoutes } from './routes/sessions';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ type RouteHandler = (
|
|||||||
|
|
||||||
const routeHandlers: RouteHandler[] = [
|
const routeHandlers: RouteHandler[] = [
|
||||||
handleProviderRoutes,
|
handleProviderRoutes,
|
||||||
handleAgentRoutes,
|
handleModelRoutes,
|
||||||
handleGatewayRoutes,
|
handleGatewayRoutes,
|
||||||
handleFileRoutes,
|
handleFileRoutes,
|
||||||
handleSessionRoutes,
|
handleSessionRoutes,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
buildMainSessionKey,
|
buildMainSessionKey,
|
||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
type AgentSummary,
|
type AgentSummary,
|
||||||
} from '@runtime/lib/agents';
|
} from '@runtime/lib/models';
|
||||||
import type { HostApiContext } from '../context';
|
import type { HostApiContext } from '../context';
|
||||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||||
import { ok } from '../route-utils';
|
import { ok } from '../route-utils';
|
||||||
@@ -18,10 +18,10 @@ function formatModelDisplay(modelRef: string | null | undefined, fallbackLabel:
|
|||||||
return parts[parts.length - 1] || trimmed;
|
return parts[parts.length - 1] || trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMainAgent(defaultAccount: ProviderAccount | null): AgentSummary {
|
function buildMainModel(defaultAccount: ProviderAccount | null): AgentSummary {
|
||||||
return {
|
return {
|
||||||
id: DEFAULT_AGENT_ID,
|
id: DEFAULT_AGENT_ID,
|
||||||
name: 'Main Agent',
|
name: 'Main Model',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
providerAccountId: defaultAccount?.id ?? null,
|
providerAccountId: defaultAccount?.id ?? null,
|
||||||
modelRef: defaultAccount?.model ?? null,
|
modelRef: defaultAccount?.model ?? null,
|
||||||
@@ -32,7 +32,7 @@ function buildMainAgent(defaultAccount: ProviderAccount | null): AgentSummary {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProviderBackedAgents(accounts: ProviderAccount[]): AgentSummary[] {
|
function buildProviderBackedModels(accounts: ProviderAccount[]): AgentSummary[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const summaries: AgentSummary[] = [];
|
const summaries: AgentSummary[] = [];
|
||||||
|
|
||||||
@@ -57,12 +57,12 @@ function buildProviderBackedAgents(accounts: ProviderAccount[]): AgentSummary[]
|
|||||||
return summaries;
|
return summaries;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleAgentRoutes(
|
export async function handleModelRoutes(
|
||||||
request: NormalizedHostApiRequest,
|
request: NormalizedHostApiRequest,
|
||||||
ctx: HostApiContext,
|
ctx: HostApiContext,
|
||||||
) {
|
) {
|
||||||
const { pathname, method } = request;
|
const { pathname, method } = request;
|
||||||
if (pathname !== '/api/agents' || method !== 'GET') {
|
if ((pathname !== '/api/models' && pathname !== '/api/agents') || method !== 'GET') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +72,15 @@ export async function handleAgentRoutes(
|
|||||||
const defaultAccountId = ctx.providerApiService.getDefault().accountId;
|
const defaultAccountId = ctx.providerApiService.getDefault().accountId;
|
||||||
const defaultAccount = accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null;
|
const defaultAccount = accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null;
|
||||||
|
|
||||||
const agents = [
|
const models = [
|
||||||
buildMainAgent(defaultAccount),
|
buildMainModel(defaultAccount),
|
||||||
...buildProviderBackedAgents(accounts),
|
...buildProviderBackedModels(accounts),
|
||||||
];
|
];
|
||||||
|
|
||||||
return ok({
|
return ok({
|
||||||
success: true,
|
success: true,
|
||||||
agents,
|
models,
|
||||||
|
agents: models,
|
||||||
defaultAgentId: DEFAULT_AGENT_ID,
|
defaultAgentId: DEFAULT_AGENT_ID,
|
||||||
defaultProviderAccountId: defaultAccount?.id ?? null,
|
defaultProviderAccountId: defaultAccount?.id ?? null,
|
||||||
defaultModelRef: defaultAccount?.model ?? null,
|
defaultModelRef: defaultAccount?.model ?? null,
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { sessionStore } from '@electron/gateway/session-store';
|
import { sessionStore } from '@electron/gateway/session-store';
|
||||||
import { getTranscriptFilePath } from '@electron/utils/token-usage-writer';
|
import {
|
||||||
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/agents';
|
getTranscriptFilePath,
|
||||||
|
getTranscriptPathCandidates,
|
||||||
|
} from '@electron/utils/token-usage-writer';
|
||||||
|
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/models';
|
||||||
import type { HostApiContext } from '../context';
|
import type { HostApiContext } from '../context';
|
||||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||||
import { fail, ok, parseJsonBody } from '../route-utils';
|
import { fail, ok, parseJsonBody } from '../route-utils';
|
||||||
@@ -41,19 +44,44 @@ export async function handleSessionRoutes(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fsP = await import('node:fs/promises');
|
const fsP = await import('node:fs/promises');
|
||||||
const transcriptPath = getTranscriptFilePath(identity.sessionKey);
|
let transcriptPath = getTranscriptFilePath(identity.sessionKey);
|
||||||
let raw: string;
|
let raw: string | null = null;
|
||||||
|
let lastError: unknown = null;
|
||||||
|
|
||||||
|
for (const candidatePath of getTranscriptPathCandidates(identity.sessionKey)) {
|
||||||
try {
|
try {
|
||||||
raw = await fsP.readFile(transcriptPath, 'utf8');
|
raw = await fsP.readFile(candidatePath, 'utf8');
|
||||||
} catch (error: any) {
|
transcriptPath = candidatePath;
|
||||||
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
|
break;
|
||||||
if (error?.code === 'ENOENT' && requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
|
} catch (error) {
|
||||||
raw = await fsP.readFile(getTranscriptFilePath(requestedSessionKey), 'utf8');
|
lastError = error;
|
||||||
} else {
|
if ((error as { code?: string } | null)?.code !== 'ENOENT') {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw === null) {
|
||||||
|
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
|
||||||
|
if (requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
|
||||||
|
for (const candidatePath of getTranscriptPathCandidates(requestedSessionKey)) {
|
||||||
|
try {
|
||||||
|
raw = await fsP.readFile(candidatePath, 'utf8');
|
||||||
|
transcriptPath = candidatePath;
|
||||||
|
break;
|
||||||
|
} catch (candidateError) {
|
||||||
|
lastError = candidateError;
|
||||||
|
if ((candidateError as { code?: string } | null)?.code !== 'ENOENT') {
|
||||||
|
throw candidateError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw === null) {
|
||||||
|
throw lastError ?? Object.assign(new Error('Transcript not found'), { code: 'ENOENT' });
|
||||||
|
}
|
||||||
|
|
||||||
const lines = raw.split(/\r?\n/).filter(Boolean);
|
const lines = raw.split(/\r?\n/).filter(Boolean);
|
||||||
const messages = lines.flatMap((line) => {
|
const messages = lines.flatMap((line) => {
|
||||||
@@ -90,19 +118,18 @@ export async function handleSessionRoutes(
|
|||||||
|
|
||||||
sessionStore.deleteSession(sessionKey);
|
sessionStore.deleteSession(sessionKey);
|
||||||
|
|
||||||
const transcriptPath = getTranscriptFilePath(sessionKey);
|
|
||||||
try {
|
|
||||||
const fsP = await import('node:fs/promises');
|
const fsP = await import('node:fs/promises');
|
||||||
|
const transcriptPathCandidates = Array.from(new Set([
|
||||||
|
...getTranscriptPathCandidates(sessionKey),
|
||||||
|
...(rawSessionKey && rawSessionKey !== sessionKey ? getTranscriptPathCandidates(rawSessionKey) : []),
|
||||||
|
]));
|
||||||
|
|
||||||
|
for (const transcriptPath of transcriptPathCandidates) {
|
||||||
|
try {
|
||||||
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
|
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
|
||||||
|
break;
|
||||||
} catch {
|
} catch {
|
||||||
if (rawSessionKey && rawSessionKey !== sessionKey) {
|
continue;
|
||||||
try {
|
|
||||||
const fsP = await import('node:fs/promises');
|
|
||||||
const legacyTranscriptPath = getTranscriptFilePath(rawSessionKey);
|
|
||||||
await fsP.rename(legacyTranscriptPath, legacyTranscriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
|
|
||||||
} catch {
|
|
||||||
// Best effort: transcript may not exist yet.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createProvider } from '@electron/providers';
|
|||||||
import type { BaseProvider } from '@electron/providers/BaseProvider';
|
import type { BaseProvider } from '@electron/providers/BaseProvider';
|
||||||
import { providerApiService } from '@electron/service/provider-api-service';
|
import { providerApiService } from '@electron/service/provider-api-service';
|
||||||
import logManager from '@electron/service/logger';
|
import logManager from '@electron/service/logger';
|
||||||
import { normalizeAgentSessionKey } from '@runtime/lib/agents';
|
import { normalizeAgentSessionKey } from '@runtime/lib/models';
|
||||||
import type { RawMessage } from '@runtime/shared/chat-model';
|
import type { RawMessage } from '@runtime/shared/chat-model';
|
||||||
import { sessionStore } from '../session-store';
|
import { sessionStore } from '../session-store';
|
||||||
import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types';
|
import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import logManager from '@electron/service/logger';
|
import logManager from '@electron/service/logger';
|
||||||
import { normalizeAgentSessionKey } from '@runtime/lib/agents';
|
import { normalizeAgentSessionKey } from '@runtime/lib/models';
|
||||||
import type { RawMessage } from '@runtime/shared/chat-model';
|
import type { RawMessage } from '@runtime/shared/chat-model';
|
||||||
|
|
||||||
let sessionsFilePath: string | null = null;
|
let sessionsFilePath: string | null = null;
|
||||||
|
|||||||
@@ -1,21 +1,39 @@
|
|||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { parseSessionKey } from '@runtime/lib/agents';
|
import { parseSessionKey } from '@runtime/lib/models';
|
||||||
|
|
||||||
export function getTranscriptFilePath(sessionKey: string): string {
|
const PRIMARY_TRANSCRIPT_ROOT_DIR = 'models';
|
||||||
|
const LEGACY_TRANSCRIPT_ROOT_DIR = 'agents';
|
||||||
|
|
||||||
|
function buildTranscriptFilePath(sessionKey: string, rootDirName: string): string {
|
||||||
const parsed = parseSessionKey(sessionKey);
|
const parsed = parseSessionKey(sessionKey);
|
||||||
let agentId = parsed.isAgentSession ? parsed.agentId : 'default';
|
const agentId = parsed.isAgentSession ? parsed.agentId : 'default';
|
||||||
let sessionId = parsed.isAgentSession ? parsed.sessionId : sessionKey;
|
let sessionId = parsed.isAgentSession ? parsed.sessionId : sessionKey;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
sessionId = 'unknown';
|
sessionId = 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseDir = path.join(app.getPath('userData'), 'agents', agentId, 'sessions');
|
const baseDir = path.join(app.getPath('userData'), rootDirName, agentId, 'sessions');
|
||||||
return path.join(baseDir, `${sessionId}.jsonl`);
|
return path.join(baseDir, `${sessionId}.jsonl`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTranscriptFilePath(sessionKey: string): string {
|
||||||
|
return buildTranscriptFilePath(sessionKey, PRIMARY_TRANSCRIPT_ROOT_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLegacyTranscriptFilePath(sessionKey: string): string {
|
||||||
|
return buildTranscriptFilePath(sessionKey, LEGACY_TRANSCRIPT_ROOT_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranscriptPathCandidates(sessionKey: string): string[] {
|
||||||
|
return Array.from(new Set([
|
||||||
|
getTranscriptFilePath(sessionKey),
|
||||||
|
getLegacyTranscriptFilePath(sessionKey),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
export function appendTranscriptLine(sessionKey: string, lineObject: any): void {
|
export function appendTranscriptLine(sessionKey: string, lineObject: any): void {
|
||||||
const filePath = getTranscriptFilePath(sessionKey);
|
const filePath = getTranscriptFilePath(sessionKey);
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ export {
|
|||||||
type TokenUsageHistoryEntry,
|
type TokenUsageHistoryEntry,
|
||||||
} from './token-usage-core';
|
} from './token-usage-core';
|
||||||
|
|
||||||
async function listAgentIdsWithSessionDirs(): Promise<string[]> {
|
const TRANSCRIPT_ROOT_DIR_NAMES = ['models', 'agents'] as const;
|
||||||
const agentsDir = join(app.getPath('userData'), 'agents');
|
|
||||||
|
async function listAgentIdsWithSessionDirs(rootDirName: string): Promise<string[]> {
|
||||||
|
const rootDir = join(app.getPath('userData'), rootDirName);
|
||||||
const agentIds = new Set<string>();
|
const agentIds = new Set<string>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const agentEntries = await readdir(agentsDir, { withFileTypes: true });
|
const agentEntries = await readdir(rootDir, { withFileTypes: true });
|
||||||
for (const entry of agentEntries) {
|
for (const entry of agentEntries) {
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const normalized = entry.name.trim();
|
const normalized = entry.name.trim();
|
||||||
@@ -36,14 +38,14 @@ async function listAgentIdsWithSessionDirs(): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }>> {
|
async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }>> {
|
||||||
const agentsDir = join(app.getPath('userData'), 'agents');
|
const filesBySession = new Map<string, { filePath: string; sessionId: string; agentId: string; mtimeMs: number }>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const agentEntries = await listAgentIdsWithSessionDirs();
|
for (const rootDirName of TRANSCRIPT_ROOT_DIR_NAMES) {
|
||||||
const files: Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }> = [];
|
const agentEntries = await listAgentIdsWithSessionDirs(rootDirName);
|
||||||
|
|
||||||
for (const agentId of agentEntries) {
|
for (const agentId of agentEntries) {
|
||||||
const sessionsDir = join(agentsDir, agentId, 'sessions');
|
const sessionsDir = join(app.getPath('userData'), rootDirName, agentId, 'sessions');
|
||||||
try {
|
try {
|
||||||
const sessionEntries = await readdir(sessionsDir);
|
const sessionEntries = await readdir(sessionsDir);
|
||||||
|
|
||||||
@@ -53,12 +55,17 @@ async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessi
|
|||||||
const filePath = join(sessionsDir, fileName);
|
const filePath = join(sessionsDir, fileName);
|
||||||
try {
|
try {
|
||||||
const fileStat = await stat(filePath);
|
const fileStat = await stat(filePath);
|
||||||
files.push({
|
const sessionKey = `${agentId}:${sessionId}`;
|
||||||
|
const existing = filesBySession.get(sessionKey);
|
||||||
|
|
||||||
|
if (!existing || fileStat.mtimeMs > existing.mtimeMs) {
|
||||||
|
filesBySession.set(sessionKey, {
|
||||||
filePath,
|
filePath,
|
||||||
sessionId,
|
sessionId,
|
||||||
agentId,
|
agentId,
|
||||||
mtimeMs: fileStat.mtimeMs,
|
mtimeMs: fileStat.mtimeMs,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -67,7 +74,9 @@ async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [...filesBySession.values()];
|
||||||
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
return files;
|
return files;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,92 +1 @@
|
|||||||
export const DEFAULT_AGENT_ID = 'main';
|
export * from './models';
|
||||||
export const DEFAULT_MAIN_SESSION_SUFFIX = 'main';
|
|
||||||
|
|
||||||
export interface AgentSummary {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isDefault: boolean;
|
|
||||||
providerAccountId: string | null;
|
|
||||||
modelRef: string | null;
|
|
||||||
modelDisplay: string;
|
|
||||||
mainSessionKey: string;
|
|
||||||
vendorId?: string | null;
|
|
||||||
source?: 'synthetic-main' | 'provider-account';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AgentsSnapshot {
|
|
||||||
agents: AgentSummary[];
|
|
||||||
defaultAgentId: string;
|
|
||||||
defaultProviderAccountId: string | null;
|
|
||||||
defaultModelRef: string | null;
|
|
||||||
mainSessionSuffix: string;
|
|
||||||
configuredChannelTypes: string[];
|
|
||||||
channelOwners: Record<string, string>;
|
|
||||||
channelAccountOwners: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParsedSessionKey {
|
|
||||||
sessionKey: string;
|
|
||||||
agentId: string;
|
|
||||||
sessionId: string;
|
|
||||||
isAgentSession: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeAgentId(value: string | null | undefined): string {
|
|
||||||
const normalized = String(value ?? '').trim().toLowerCase();
|
|
||||||
return normalized || DEFAULT_AGENT_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeSessionSuffix(value: string | null | undefined): string {
|
|
||||||
const normalized = String(value ?? '').trim().toLowerCase();
|
|
||||||
return normalized || DEFAULT_MAIN_SESSION_SUFFIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAgentSessionKey(agentId: string, sessionId: string): string {
|
|
||||||
return `agent:${normalizeAgentId(agentId)}:${normalizeSessionSuffix(sessionId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildMainSessionKey(
|
|
||||||
agentId: string,
|
|
||||||
sessionId = DEFAULT_MAIN_SESSION_SUFFIX,
|
|
||||||
): string {
|
|
||||||
return buildAgentSessionKey(agentId, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseSessionKey(sessionKey: string): ParsedSessionKey {
|
|
||||||
const trimmed = String(sessionKey ?? '').trim();
|
|
||||||
|
|
||||||
if (trimmed.startsWith('agent:')) {
|
|
||||||
const parts = trimmed.split(':');
|
|
||||||
const agentId = normalizeAgentId(parts[1]);
|
|
||||||
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
|
|
||||||
return {
|
|
||||||
sessionKey: buildAgentSessionKey(agentId, sessionId),
|
|
||||||
agentId,
|
|
||||||
sessionId,
|
|
||||||
isAgentSession: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed.startsWith('local:')) {
|
|
||||||
const parts = trimmed.split(':');
|
|
||||||
const agentId = normalizeAgentId(parts[1]);
|
|
||||||
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
|
|
||||||
return {
|
|
||||||
sessionKey: buildAgentSessionKey(agentId, sessionId),
|
|
||||||
agentId,
|
|
||||||
sessionId,
|
|
||||||
isAgentSession: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sessionKey: trimmed,
|
|
||||||
agentId: DEFAULT_AGENT_ID,
|
|
||||||
sessionId: normalizeSessionSuffix(trimmed),
|
|
||||||
isAgentSession: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeAgentSessionKey(sessionKey: string): string {
|
|
||||||
return parseSessionKey(sessionKey).sessionKey;
|
|
||||||
}
|
|
||||||
|
|||||||
112
runtime-shared/lib/models.ts
Normal file
112
runtime-shared/lib/models.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
export const DEFAULT_AGENT_ID = 'main';
|
||||||
|
export const DEFAULT_MAIN_SESSION_SUFFIX = 'main';
|
||||||
|
export const DEFAULT_MODEL_ID = DEFAULT_AGENT_ID;
|
||||||
|
|
||||||
|
export interface AgentSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
providerAccountId: string | null;
|
||||||
|
modelRef: string | null;
|
||||||
|
modelDisplay: string;
|
||||||
|
mainSessionKey: string;
|
||||||
|
vendorId?: string | null;
|
||||||
|
source?: 'synthetic-main' | 'provider-account';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelSummary = AgentSummary;
|
||||||
|
|
||||||
|
export interface ModelsSnapshot {
|
||||||
|
models: ModelSummary[];
|
||||||
|
agents?: ModelSummary[];
|
||||||
|
defaultAgentId: string;
|
||||||
|
defaultProviderAccountId: string | null;
|
||||||
|
defaultModelRef: string | null;
|
||||||
|
mainSessionSuffix: string;
|
||||||
|
configuredChannelTypes: string[];
|
||||||
|
channelOwners: Record<string, string>;
|
||||||
|
channelAccountOwners: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentsSnapshot {
|
||||||
|
agents: AgentSummary[];
|
||||||
|
models?: AgentSummary[];
|
||||||
|
defaultAgentId: string;
|
||||||
|
defaultProviderAccountId: string | null;
|
||||||
|
defaultModelRef: string | null;
|
||||||
|
mainSessionSuffix: string;
|
||||||
|
configuredChannelTypes: string[];
|
||||||
|
channelOwners: Record<string, string>;
|
||||||
|
channelAccountOwners: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedSessionKey {
|
||||||
|
sessionKey: string;
|
||||||
|
agentId: string;
|
||||||
|
sessionId: string;
|
||||||
|
isAgentSession: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAgentId(value: string | null | undefined): string {
|
||||||
|
const normalized = String(value ?? '').trim().toLowerCase();
|
||||||
|
return normalized || DEFAULT_AGENT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSessionSuffix(value: string | null | undefined): string {
|
||||||
|
const normalized = String(value ?? '').trim().toLowerCase();
|
||||||
|
return normalized || DEFAULT_MAIN_SESSION_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAgentSessionKey(agentId: string, sessionId: string): string {
|
||||||
|
return `agent:${normalizeAgentId(agentId)}:${normalizeSessionSuffix(sessionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMainSessionKey(
|
||||||
|
agentId: string,
|
||||||
|
sessionId = DEFAULT_MAIN_SESSION_SUFFIX,
|
||||||
|
): string {
|
||||||
|
return buildAgentSessionKey(agentId, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSessionKey(sessionKey: string): ParsedSessionKey {
|
||||||
|
const trimmed = String(sessionKey ?? '').trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('agent:')) {
|
||||||
|
const parts = trimmed.split(':');
|
||||||
|
const agentId = normalizeAgentId(parts[1]);
|
||||||
|
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
|
||||||
|
return {
|
||||||
|
sessionKey: buildAgentSessionKey(agentId, sessionId),
|
||||||
|
agentId,
|
||||||
|
sessionId,
|
||||||
|
isAgentSession: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('local:')) {
|
||||||
|
const parts = trimmed.split(':');
|
||||||
|
const agentId = normalizeAgentId(parts[1]);
|
||||||
|
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
|
||||||
|
return {
|
||||||
|
sessionKey: buildAgentSessionKey(agentId, sessionId),
|
||||||
|
agentId,
|
||||||
|
sessionId,
|
||||||
|
isAgentSession: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionKey: trimmed,
|
||||||
|
agentId: DEFAULT_AGENT_ID,
|
||||||
|
sessionId: normalizeSessionSuffix(trimmed),
|
||||||
|
isAgentSession: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAgentSessionKey(sessionKey: string): string {
|
||||||
|
return parseSessionKey(sessionKey).sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeModelId = normalizeAgentId;
|
||||||
|
export const buildModelSessionKey = buildAgentSessionKey;
|
||||||
|
export const normalizeModelSessionKey = normalizeAgentSessionKey;
|
||||||
@@ -8,7 +8,7 @@ import blueLogo from '../../assets/images/login/blue_logo.png';
|
|||||||
const MENU_MARKS: Record<string, typeof House> = {
|
const MENU_MARKS: Record<string, typeof House> = {
|
||||||
'/home': House,
|
'/home': House,
|
||||||
'/knowledge': Book,
|
'/knowledge': Book,
|
||||||
'/agents': Cpu,
|
'/models': Cpu,
|
||||||
'/skills': Puzzle,
|
'/skills': Puzzle,
|
||||||
'/cron': Clock,
|
'/cron': Clock,
|
||||||
'/scripts': Code,
|
'/scripts': Code,
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ import {
|
|||||||
import { IPC_EVENTS } from '../../lib/constants';
|
import { IPC_EVENTS } from '../../lib/constants';
|
||||||
import { invokeIpc } from '../../lib/host-api';
|
import { invokeIpc } from '../../lib/host-api';
|
||||||
import {
|
import {
|
||||||
agentsStore,
|
|
||||||
channelStore,
|
channelStore,
|
||||||
chatStore,
|
chatStore,
|
||||||
getCompletedTasks,
|
getCompletedTasks,
|
||||||
getPendingTasks,
|
getPendingTasks,
|
||||||
|
modelsStore,
|
||||||
taskStore,
|
taskStore,
|
||||||
useAgentsStore,
|
|
||||||
useChannelStore,
|
useChannelStore,
|
||||||
useChatStore,
|
useChatStore,
|
||||||
|
useModelsStore,
|
||||||
useTaskStore,
|
useTaskStore,
|
||||||
type StagedAttachment,
|
type StagedAttachment,
|
||||||
} from '../../stores';
|
} from '../../stores';
|
||||||
@@ -149,7 +149,7 @@ function mapMessages(messages: RawMessage[], streamingMessage: RawMessage | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const agentsState = useAgentsStore();
|
const modelsState = useModelsStore();
|
||||||
const chat = useChatStore();
|
const chat = useChatStore();
|
||||||
const taskState = useTaskStore();
|
const taskState = useTaskStore();
|
||||||
const channelState = useChannelStore();
|
const channelState = useChannelStore();
|
||||||
@@ -161,7 +161,7 @@ export default function HomePage() {
|
|||||||
const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false);
|
const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void agentsStore.init();
|
void modelsStore.init();
|
||||||
void chatStore.init();
|
void chatStore.init();
|
||||||
void taskStore.init();
|
void taskStore.init();
|
||||||
void channelStore.init();
|
void channelStore.init();
|
||||||
@@ -212,10 +212,10 @@ export default function HomePage() {
|
|||||||
const latestTask = currentTaskSource[0];
|
const latestTask = currentTaskSource[0];
|
||||||
|
|
||||||
const visibleMessages = mapMessages(chat.messages, chat.streamingMessage);
|
const visibleMessages = mapMessages(chat.messages, chat.streamingMessage);
|
||||||
const selectedAgentId = agentsState.agents.some((agent) => agent.id === chat.currentAgentId)
|
const selectedModelId = modelsState.models.some((model) => model.id === chat.currentAgentId)
|
||||||
? chat.currentAgentId
|
? chat.currentAgentId
|
||||||
: agentsState.defaultAgentId;
|
: modelsState.defaultAgentId;
|
||||||
const currentAgent = agentsState.agents.find((agent) => agent.id === selectedAgentId) || null;
|
const currentModel = modelsState.models.find((model) => model.id === selectedModelId) || null;
|
||||||
|
|
||||||
async function handleSendMessage(): Promise<void> {
|
async function handleSendMessage(): Promise<void> {
|
||||||
const sent = await chatStore.sendMessage(inputMessage, attachments);
|
const sent = await chatStore.sendMessage(inputMessage, attachments);
|
||||||
@@ -288,7 +288,7 @@ export default function HomePage() {
|
|||||||
loading={!chat.initialized}
|
loading={!chat.initialized}
|
||||||
selectedConversationId={chat.currentSessionKey}
|
selectedConversationId={chat.currentSessionKey}
|
||||||
onNewChat={() => {
|
onNewChat={() => {
|
||||||
void chatStore.newSession(selectedAgentId || undefined);
|
void chatStore.newSession(selectedModelId || undefined);
|
||||||
}}
|
}}
|
||||||
onSelectConversation={(conversationId) => {
|
onSelectConversation={(conversationId) => {
|
||||||
chatStore.switchSession(conversationId);
|
chatStore.switchSession(conversationId);
|
||||||
@@ -316,23 +316,23 @@ export default function HomePage() {
|
|||||||
<h2 className="text-base font-semibold text-[#171717] dark:text-gray-100">智能对话</h2>
|
<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">
|
<div className="mt-1 text-xs text-[#99A0AE] dark:text-gray-500">
|
||||||
网关状态:{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'}
|
网关状态:{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'}
|
||||||
{currentAgent ? ` · 当前代理:${currentAgent.name}` : ''}
|
{currentModel ? ` · 当前模型:${currentModel.name}` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300">
|
<label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300">
|
||||||
<span>代理</span>
|
<span>模型</span>
|
||||||
<select
|
<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"
|
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}
|
disabled={modelsState.loading || modelsState.models.length === 0}
|
||||||
value={selectedAgentId}
|
value={selectedModelId}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
chatStore.selectAgent(event.target.value);
|
chatStore.selectAgent(event.target.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{agentsState.agents.map((agent) => (
|
{modelsState.models.map((model) => (
|
||||||
<option key={agent.id} value={agent.id}>
|
<option key={model.id} value={model.id}>
|
||||||
{agent.name}
|
{model.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -341,7 +341,7 @@ export default function HomePage() {
|
|||||||
type="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"
|
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={() => {
|
onClick={() => {
|
||||||
void agentsStore.load();
|
void modelsStore.load();
|
||||||
void chatStore.loadSessions();
|
void chatStore.loadSessions();
|
||||||
void chatStore.loadHistory();
|
void chatStore.loadHistory();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { agentsStore, useAgentsStore } from '../../../stores';
|
import { modelsStore, useModelsStore } from '../../../stores';
|
||||||
|
|
||||||
const CHIP_CLASS_NAME = [
|
const CHIP_CLASS_NAME = [
|
||||||
'rounded-full border px-2.5 py-1 text-[11px] leading-none',
|
'rounded-full border px-2.5 py-1 text-[11px] leading-none',
|
||||||
'border-[#E5E8EE] text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300',
|
'border-[#E5E8EE] text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export default function AgentsSection() {
|
export default function ModelsSection() {
|
||||||
const agentState = useAgentsStore();
|
const modelsState = useModelsStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void agentsStore.init();
|
void modelsStore.init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,43 +18,43 @@ export default function AgentsSection() {
|
|||||||
<div className="flex items-end justify-between gap-4">
|
<div className="flex items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[18px] font-semibold leading-[24px] text-[#171717] dark:text-gray-100">
|
<h3 className="text-[18px] font-semibold leading-[24px] text-[#171717] dark:text-gray-100">
|
||||||
Agents Snapshot
|
Models Snapshot
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-[13px] leading-[20px] text-[#99A0AE] dark:text-gray-500">
|
<p className="mt-1 text-[13px] leading-[20px] text-[#99A0AE] dark:text-gray-500">
|
||||||
当前本地 `agents` 契约与 `mainSessionKey` 映射。
|
当前本地 `/api/models` 快照与 `mainSessionKey` 映射。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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={() => {
|
onClick={() => {
|
||||||
void agentsStore.load();
|
void modelsStore.load();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
刷新 Agents
|
刷新 Models
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{agentState.error ? (
|
{modelsState.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]">
|
<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}
|
{modelsState.error}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{agentState.agents.map((agent) => (
|
{modelsState.models.map((model) => (
|
||||||
<article
|
<article
|
||||||
key={agent.id}
|
key={model.id}
|
||||||
className="rounded-[16px] border border-[#E5E8EE] bg-[#FAFBFC] p-4 dark:border-[#2a2a2d] dark:bg-[#202024]"
|
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="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-[15px] font-semibold text-[#171717] dark:text-gray-100">
|
<div className="truncate text-[15px] font-semibold text-[#171717] dark:text-gray-100">
|
||||||
{agent.name}
|
{model.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[12px] text-[#99A0AE] dark:text-gray-500">{agent.id}</div>
|
<div className="mt-1 text-[12px] text-[#99A0AE] dark:text-gray-500">{model.id}</div>
|
||||||
</div>
|
</div>
|
||||||
{agent.isDefault ? (
|
{model.isDefault ? (
|
||||||
<span className="rounded-full bg-[#EFF6FF] px-2.5 py-1 text-[11px] font-medium text-[#2B7FFF] dark:bg-[#1d2633]">
|
<span className="rounded-full bg-[#EFF6FF] px-2.5 py-1 text-[11px] font-medium text-[#2B7FFF] dark:bg-[#1d2633]">
|
||||||
默认
|
默认
|
||||||
</span>
|
</span>
|
||||||
@@ -62,20 +62,20 @@ export default function AgentsSection() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
<span className={CHIP_CLASS_NAME}>Provider: {agent.providerAccountId || '--'}</span>
|
<span className={CHIP_CLASS_NAME}>Provider: {model.providerAccountId || '--'}</span>
|
||||||
<span className={CHIP_CLASS_NAME}>Model: {agent.modelDisplay || '--'}</span>
|
<span className={CHIP_CLASS_NAME}>Model: {model.modelDisplay || '--'}</span>
|
||||||
</div>
|
</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="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="font-medium text-[#171717] dark:text-gray-100">mainSessionKey</div>
|
||||||
<div className="mt-1 break-all">{agent.mainSessionKey}</div>
|
<div className="mt-1 break-all">{model.mainSessionKey}</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!agentState.loading && agentState.agents.length === 0 ? (
|
{!modelsState.loading && modelsState.models.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">
|
<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。
|
当前还没有可用模型。先在下方配置 provider 账号后,这里会自动生成可路由的 model snapshot。
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import AgentsSection from './components/AgentsSection';
|
import ModelsSection from './components/ModelsSection';
|
||||||
import ProvidersSection from './components/ProvidersSection';
|
import ProvidersSection from './components/ProvidersSection';
|
||||||
import UsageHistorySection from './components/UsageHistorySection';
|
import UsageHistorySection from './components/UsageHistorySection';
|
||||||
|
|
||||||
export default function AgentsPage() {
|
export default function ModelsPage() {
|
||||||
return (
|
return (
|
||||||
<section className="h-full w-full min-h-0">
|
<section className="h-full w-full min-h-0">
|
||||||
<div className="flex h-full w-full min-h-0 flex-col rounded-[16px] bg-white p-[20px] dark:bg-[#1b1b1d]">
|
<div className="flex h-full w-full min-h-0 flex-col rounded-[16px] bg-white p-[20px] dark:bg-[#1b1b1d]">
|
||||||
@@ -18,7 +18,7 @@ export default function AgentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 space-y-12 overflow-y-auto pb-10 pr-2">
|
<div className="min-h-0 flex-1 space-y-12 overflow-y-auto pb-10 pr-2">
|
||||||
<AgentsSection />
|
<ModelsSection />
|
||||||
<ProvidersSection />
|
<ProvidersSection />
|
||||||
<UsageHistorySection />
|
<UsageHistorySection />
|
||||||
</div>
|
</div>
|
||||||
@@ -3,7 +3,7 @@ import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-
|
|||||||
import MainLayout from '../components/layout/MainLayout';
|
import MainLayout from '../components/layout/MainLayout';
|
||||||
import HomePage from '../pages/Home';
|
import HomePage from '../pages/Home';
|
||||||
import LoginPage from '../pages/Login';
|
import LoginPage from '../pages/Login';
|
||||||
import AgentsPage from '../pages/agents';
|
import ModelsPage from '../pages/Models';
|
||||||
import SkillsPage from '../pages/Skills';
|
import SkillsPage from '../pages/Skills';
|
||||||
import CronPage from '../pages/Cron';
|
import CronPage from '../pages/Cron';
|
||||||
import ScriptsPage from '../pages/Scripts';
|
import ScriptsPage from '../pages/Scripts';
|
||||||
@@ -45,7 +45,8 @@ export function AppRouter() {
|
|||||||
<Route element={<RequireAuth />}>
|
<Route element={<RequireAuth />}>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route path="/home" element={<HomePage />} />
|
<Route path="/home" element={<HomePage />} />
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Route path="/models" element={<ModelsPage />} />
|
||||||
|
<Route path="/agents" element={<Navigate to="/models" replace />} />
|
||||||
<Route path="/skills" element={<SkillsPage />} />
|
<Route path="/skills" element={<SkillsPage />} />
|
||||||
<Route path="/cron" element={<CronPage />} />
|
<Route path="/cron" element={<CronPage />} />
|
||||||
<Route path="/scripts" element={<ScriptsPage />} />
|
<Route path="/scripts" element={<ScriptsPage />} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type AppPath =
|
export type AppPath =
|
||||||
| '/home'
|
| '/home'
|
||||||
| '/agents'
|
| '/models'
|
||||||
| '/skills'
|
| '/skills'
|
||||||
| '/cron'
|
| '/cron'
|
||||||
| '/scripts'
|
| '/scripts'
|
||||||
@@ -27,7 +27,7 @@ export const DEFAULT_PATH: WorkspacePath = '/home';
|
|||||||
export const NAV_ITEMS: NavItem[] = [
|
export const NAV_ITEMS: NavItem[] = [
|
||||||
{ path: '/home', labelKey: 'sidebar.home' },
|
{ path: '/home', labelKey: 'sidebar.home' },
|
||||||
{ path: '/knowledge', labelKey: 'sidebar.knowledge' },
|
{ path: '/knowledge', labelKey: 'sidebar.knowledge' },
|
||||||
{ path: '/agents', labelKey: 'sidebar.models' },
|
{ path: '/models', labelKey: 'sidebar.models' },
|
||||||
{ path: '/skills', labelKey: 'sidebar.skills' },
|
{ path: '/skills', labelKey: 'sidebar.skills' },
|
||||||
{ path: '/cron', labelKey: 'sidebar.cron' },
|
{ path: '/cron', labelKey: 'sidebar.cron' },
|
||||||
{ path: '/scripts', labelKey: 'sidebar.scripts' },
|
{ path: '/scripts', labelKey: 'sidebar.scripts' },
|
||||||
@@ -37,12 +37,13 @@ export const NAV_ITEMS: NavItem[] = [
|
|||||||
export function normalizeWorkspacePath(pathname: string): WorkspacePath {
|
export function normalizeWorkspacePath(pathname: string): WorkspacePath {
|
||||||
switch (pathname) {
|
switch (pathname) {
|
||||||
case '/knowledge':
|
case '/knowledge':
|
||||||
|
case '/models':
|
||||||
case '/agents':
|
case '/agents':
|
||||||
case '/skills':
|
case '/skills':
|
||||||
case '/cron':
|
case '/cron':
|
||||||
case '/scripts':
|
case '/scripts':
|
||||||
case '/setting':
|
case '/setting':
|
||||||
return pathname;
|
return pathname === '/agents' ? '/models' : pathname;
|
||||||
case '/home':
|
case '/home':
|
||||||
default:
|
default:
|
||||||
return DEFAULT_PATH;
|
return DEFAULT_PATH;
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import {
|
|||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
normalizeAgentSessionKey,
|
normalizeAgentSessionKey,
|
||||||
parseSessionKey,
|
parseSessionKey,
|
||||||
} from '@runtime/lib/agents';
|
} from '@runtime/lib/models';
|
||||||
import type { ChatSession, RawMessage, ToolStatus } from '../shared/chat-model';
|
import type { ChatSession, RawMessage, ToolStatus } from '../shared/chat-model';
|
||||||
import { extractText, isToolOnlyMessage } from '../shared/chat-model';
|
import { extractText, isToolOnlyMessage } from '../shared/chat-model';
|
||||||
import { gatewayRpc, onGatewayEvent } from '../lib/gateway-client';
|
import { gatewayRpc, onGatewayEvent } from '../lib/gateway-client';
|
||||||
import { hostApiFetch } from '../lib/host-api';
|
import { hostApiFetch } from '../lib/host-api';
|
||||||
import type { GatewayEvent } from '../types/runtime';
|
import type { GatewayEvent } from '../types/runtime';
|
||||||
import { agentsStore } from './agents';
|
import { modelsStore } from './models';
|
||||||
|
|
||||||
const SESSION_LOAD_MIN_INTERVAL_MS = 1200;
|
const SESSION_LOAD_MIN_INTERVAL_MS = 1200;
|
||||||
const HISTORY_LOAD_MIN_INTERVAL_MS = 800;
|
const HISTORY_LOAD_MIN_INTERVAL_MS = 800;
|
||||||
@@ -101,19 +101,19 @@ function patchState(patch: Partial<ChatStoreState>): ChatStoreState {
|
|||||||
function getAgentIdFromSessionKey(sessionKey: string): string {
|
function getAgentIdFromSessionKey(sessionKey: string): string {
|
||||||
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
|
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
|
||||||
if (parsed.isAgentSession) return parsed.agentId;
|
if (parsed.isAgentSession) return parsed.agentId;
|
||||||
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
return modelsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultAgentId(): string {
|
function getDefaultAgentId(): string {
|
||||||
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
return modelsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultMainSessionKey(): string {
|
function getDefaultMainSessionKey(): string {
|
||||||
return agentsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY;
|
return modelsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMainSessionKeyForAgent(agentId: string | null | undefined): string {
|
function resolveMainSessionKeyForAgent(agentId: string | null | undefined): string {
|
||||||
return agentsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey();
|
return modelsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNewSessionKey(agentId: string | null | undefined): string {
|
function buildNewSessionKey(agentId: string | null | undefined): string {
|
||||||
@@ -208,7 +208,7 @@ async function resolveDefaultAccountId(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveProviderAccountIdForAgent(agentId: string | null | undefined): Promise<string | null> {
|
async function resolveProviderAccountIdForAgent(agentId: string | null | undefined): Promise<string | null> {
|
||||||
const mappedAccountId = agentsStore.resolveProviderAccountId(agentId);
|
const mappedAccountId = modelsStore.resolveProviderAccountId(agentId);
|
||||||
if (mappedAccountId) {
|
if (mappedAccountId) {
|
||||||
return mappedAccountId;
|
return mappedAccountId;
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ async function subscribeToGateway(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSessions(): Promise<void> {
|
async function loadSessions(): Promise<void> {
|
||||||
await agentsStore.init();
|
await modelsStore.init();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (loadSessionsInFlight) {
|
if (loadSessionsInFlight) {
|
||||||
await loadSessionsInFlight;
|
await loadSessionsInFlight;
|
||||||
@@ -850,7 +850,7 @@ function clearError(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initChatStore(): Promise<void> {
|
async function initChatStore(): Promise<void> {
|
||||||
await agentsStore.init();
|
await modelsStore.init();
|
||||||
await subscribeToGateway();
|
await subscribeToGateway();
|
||||||
await loadSessions();
|
await loadSessions();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './agents';
|
export * from './models';
|
||||||
export * from './chat';
|
export * from './chat';
|
||||||
export * from './task';
|
export * from './task';
|
||||||
export * from './channel';
|
export * from './channel';
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import {
|
|||||||
buildMainSessionKey,
|
buildMainSessionKey,
|
||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
type AgentSummary,
|
type AgentSummary,
|
||||||
type AgentsSnapshot,
|
type ModelsSnapshot,
|
||||||
} from '@runtime/lib/agents';
|
} from '@runtime/lib/models';
|
||||||
import { hostApiFetch } from '../lib/host-api';
|
import { hostApiFetch } from '../lib/host-api';
|
||||||
|
|
||||||
export interface AgentsStoreState {
|
export interface ModelsStoreState {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
agents: AgentSummary[];
|
models: AgentSummary[];
|
||||||
defaultAgentId: string;
|
defaultAgentId: string;
|
||||||
defaultProviderAccountId: string | null;
|
defaultProviderAccountId: string | null;
|
||||||
defaultModelRef: string | null;
|
defaultModelRef: string | null;
|
||||||
@@ -22,12 +22,12 @@ export interface AgentsStoreState {
|
|||||||
|
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
let loadAgentsInFlight: Promise<void> | null = null;
|
let loadModelsInFlight: Promise<void> | null = null;
|
||||||
let state: AgentsStoreState = {
|
let state: ModelsStoreState = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
agents: [],
|
models: [],
|
||||||
defaultAgentId: DEFAULT_AGENT_ID,
|
defaultAgentId: DEFAULT_AGENT_ID,
|
||||||
defaultProviderAccountId: null,
|
defaultProviderAccountId: null,
|
||||||
defaultModelRef: null,
|
defaultModelRef: null,
|
||||||
@@ -40,49 +40,51 @@ function emit(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchState(patch: Partial<AgentsStoreState>): AgentsStoreState {
|
function patchState(patch: Partial<ModelsStoreState>): ModelsStoreState {
|
||||||
state = { ...state, ...patch };
|
state = { ...state, ...patch };
|
||||||
emit();
|
emit();
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeAgent(agent: AgentSummary): AgentSummary {
|
function sanitizeModel(model: AgentSummary): AgentSummary {
|
||||||
const normalizedId = normalizeAgentId(agent.id);
|
const normalizedId = normalizeAgentId(model.id);
|
||||||
const normalizedMainSessionKey = agent.mainSessionKey || buildMainSessionKey(normalizedId);
|
const normalizedMainSessionKey = model.mainSessionKey || buildMainSessionKey(normalizedId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: normalizedId,
|
id: normalizedId,
|
||||||
name: agent.name || normalizedId,
|
name: model.name || normalizedId,
|
||||||
isDefault: Boolean(agent.isDefault),
|
isDefault: Boolean(model.isDefault),
|
||||||
providerAccountId: agent.providerAccountId ?? null,
|
providerAccountId: model.providerAccountId ?? null,
|
||||||
modelRef: agent.modelRef ?? null,
|
modelRef: model.modelRef ?? null,
|
||||||
modelDisplay: agent.modelDisplay || agent.modelRef || agent.name || normalizedId,
|
modelDisplay: model.modelDisplay || model.modelRef || model.name || normalizedId,
|
||||||
mainSessionKey: normalizedMainSessionKey,
|
mainSessionKey: normalizedMainSessionKey,
|
||||||
vendorId: agent.vendorId ?? null,
|
vendorId: model.vendorId ?? null,
|
||||||
source: agent.source,
|
source: model.source,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAgents(): Promise<void> {
|
async function loadModels(): Promise<void> {
|
||||||
if (loadAgentsInFlight) {
|
if (loadModelsInFlight) {
|
||||||
await loadAgentsInFlight;
|
await loadModelsInFlight;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAgentsInFlight = (async () => {
|
loadModelsInFlight = (async () => {
|
||||||
patchState({ loading: true, error: null });
|
patchState({ loading: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const snapshot = await hostApiFetch<AgentsSnapshot & { success?: boolean }>('/api/agents');
|
const snapshot = await hostApiFetch<ModelsSnapshot & { success?: boolean }>('/api/models');
|
||||||
const agents = Array.isArray(snapshot?.agents)
|
const models = Array.isArray(snapshot?.models)
|
||||||
? snapshot.agents.map((agent) => sanitizeAgent(agent))
|
? snapshot.models.map((model) => sanitizeModel(model))
|
||||||
|
: Array.isArray(snapshot?.agents)
|
||||||
|
? snapshot.agents.map((model) => sanitizeModel(model))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
patchState({
|
patchState({
|
||||||
initialized: true,
|
initialized: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
agents,
|
models,
|
||||||
defaultAgentId: snapshot?.defaultAgentId ? normalizeAgentId(snapshot.defaultAgentId) : DEFAULT_AGENT_ID,
|
defaultAgentId: snapshot?.defaultAgentId ? normalizeAgentId(snapshot.defaultAgentId) : DEFAULT_AGENT_ID,
|
||||||
defaultProviderAccountId: snapshot?.defaultProviderAccountId ?? null,
|
defaultProviderAccountId: snapshot?.defaultProviderAccountId ?? null,
|
||||||
defaultModelRef: snapshot?.defaultModelRef ?? null,
|
defaultModelRef: snapshot?.defaultModelRef ?? null,
|
||||||
@@ -98,9 +100,9 @@ async function loadAgents(): Promise<void> {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadAgentsInFlight;
|
await loadModelsInFlight;
|
||||||
} finally {
|
} finally {
|
||||||
loadAgentsInFlight = null;
|
loadModelsInFlight = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,36 +111,36 @@ function subscribe(listener: () => void): () => void {
|
|||||||
return () => listeners.delete(listener);
|
return () => listeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnapshot(): AgentsStoreState {
|
function getSnapshot(): ModelsStoreState {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAgentById(agentId: string | null | undefined): AgentSummary | undefined {
|
function getModelById(modelId: string | null | undefined): AgentSummary | undefined {
|
||||||
const normalizedId = normalizeAgentId(agentId);
|
const normalizedId = normalizeAgentId(modelId);
|
||||||
return state.agents.find((agent) => agent.id === normalizedId);
|
return state.models.find((model) => model.id === normalizedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMainSessionKey(agentId: string | null | undefined): string {
|
function resolveMainSessionKey(agentId: string | null | undefined): string {
|
||||||
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
||||||
return getAgentById(normalizedId)?.mainSessionKey || buildMainSessionKey(normalizedId, state.mainSessionSuffix);
|
return getModelById(normalizedId)?.mainSessionKey || buildMainSessionKey(normalizedId, state.mainSessionSuffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveProviderAccountId(agentId: string | null | undefined): string | null {
|
function resolveProviderAccountId(agentId: string | null | undefined): string | null {
|
||||||
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
||||||
return getAgentById(normalizedId)?.providerAccountId ?? state.defaultProviderAccountId;
|
return getModelById(normalizedId)?.providerAccountId ?? state.defaultProviderAccountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const agentsStore = {
|
export const modelsStore = {
|
||||||
subscribe,
|
subscribe,
|
||||||
getSnapshot,
|
getSnapshot,
|
||||||
getState: () => state,
|
getState: () => state,
|
||||||
init: loadAgents,
|
init: loadModels,
|
||||||
load: loadAgents,
|
load: loadModels,
|
||||||
getAgentById,
|
getModelById,
|
||||||
resolveMainSessionKey,
|
resolveMainSessionKey,
|
||||||
resolveProviderAccountId,
|
resolveProviderAccountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useAgentsStore(): AgentsStoreState {
|
export function useModelsStore(): ModelsStoreState {
|
||||||
return useSyncExternalStore(agentsStore.subscribe, agentsStore.getSnapshot, agentsStore.getSnapshot);
|
return useSyncExternalStore(modelsStore.subscribe, modelsStore.getSnapshot, modelsStore.getSnapshot);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user