diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js index a8c04dd..5778cd7 100644 --- a/dist-electron/main/main.js +++ b/dist-electron/main/main.js @@ -1,2 +1,7 @@ -require('bytenode') -require('./main.jsc') \ No newline at end of file +"use strict"; +require("electron"); +require("./main-Bp9J8VEe.js"); +require("electron-squirrel-startup"); +require("electron-log"); +require("bytenode"); +require("axios"); diff --git a/electron/api/router.ts b/electron/api/router.ts index 04fd779..e8df467 100644 --- a/electron/api/router.ts +++ b/electron/api/router.ts @@ -4,9 +4,9 @@ import { providerApiService } from '@electron/service/provider-api-service'; import type { HostApiContext } from './context'; import type { HostApiRequest } from './route-utils'; import { normalizeRequest } from './route-utils'; -import { handleAgentRoutes } from './routes/agents'; import { handleFileRoutes } from './routes/files'; import { handleGatewayRoutes } from './routes/gateway'; +import { handleModelRoutes } from './routes/models'; import { handleProviderRoutes } from './routes/providers'; import { handleSessionRoutes } from './routes/sessions'; @@ -17,7 +17,7 @@ type RouteHandler = ( const routeHandlers: RouteHandler[] = [ handleProviderRoutes, - handleAgentRoutes, + handleModelRoutes, handleGatewayRoutes, handleFileRoutes, handleSessionRoutes, diff --git a/electron/api/routes/agents.ts b/electron/api/routes/models.ts similarity index 85% rename from electron/api/routes/agents.ts rename to electron/api/routes/models.ts index 6e87179..d8f7363 100644 --- a/electron/api/routes/agents.ts +++ b/electron/api/routes/models.ts @@ -5,7 +5,7 @@ import { buildMainSessionKey, normalizeAgentId, type AgentSummary, -} from '@runtime/lib/agents'; +} from '@runtime/lib/models'; import type { HostApiContext } from '../context'; import type { NormalizedHostApiRequest } 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; } -function buildMainAgent(defaultAccount: ProviderAccount | null): AgentSummary { +function buildMainModel(defaultAccount: ProviderAccount | null): AgentSummary { return { id: DEFAULT_AGENT_ID, - name: 'Main Agent', + name: 'Main Model', isDefault: true, providerAccountId: defaultAccount?.id ?? 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(); const summaries: AgentSummary[] = []; @@ -57,12 +57,12 @@ function buildProviderBackedAgents(accounts: ProviderAccount[]): AgentSummary[] return summaries; } -export async function handleAgentRoutes( +export async function handleModelRoutes( request: NormalizedHostApiRequest, ctx: HostApiContext, ) { const { pathname, method } = request; - if (pathname !== '/api/agents' || method !== 'GET') { + if ((pathname !== '/api/models' && pathname !== '/api/agents') || method !== 'GET') { return null; } @@ -72,14 +72,15 @@ export async function handleAgentRoutes( const defaultAccountId = ctx.providerApiService.getDefault().accountId; const defaultAccount = accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null; - const agents = [ - buildMainAgent(defaultAccount), - ...buildProviderBackedAgents(accounts), + const models = [ + buildMainModel(defaultAccount), + ...buildProviderBackedModels(accounts), ]; return ok({ success: true, - agents, + models, + agents: models, defaultAgentId: DEFAULT_AGENT_ID, defaultProviderAccountId: defaultAccount?.id ?? null, defaultModelRef: defaultAccount?.model ?? null, diff --git a/electron/api/routes/sessions.ts b/electron/api/routes/sessions.ts index 3349c93..3fb6284 100644 --- a/electron/api/routes/sessions.ts +++ b/electron/api/routes/sessions.ts @@ -1,6 +1,9 @@ import { sessionStore } from '@electron/gateway/session-store'; -import { getTranscriptFilePath } from '@electron/utils/token-usage-writer'; -import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/agents'; +import { + getTranscriptFilePath, + getTranscriptPathCandidates, +} from '@electron/utils/token-usage-writer'; +import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/models'; import type { HostApiContext } from '../context'; import type { NormalizedHostApiRequest } from '../route-utils'; import { fail, ok, parseJsonBody } from '../route-utils'; @@ -41,20 +44,45 @@ export async function handleSessionRoutes( try { const fsP = await import('node:fs/promises'); - const transcriptPath = getTranscriptFilePath(identity.sessionKey); - let raw: string; + let transcriptPath = getTranscriptFilePath(identity.sessionKey); + let raw: string | null = null; + let lastError: unknown = null; - try { - raw = await fsP.readFile(transcriptPath, 'utf8'); - } catch (error: any) { - const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || ''; - if (error?.code === 'ENOENT' && requestedSessionKey && requestedSessionKey !== identity.sessionKey) { - raw = await fsP.readFile(getTranscriptFilePath(requestedSessionKey), 'utf8'); - } else { - throw error; + for (const candidatePath of getTranscriptPathCandidates(identity.sessionKey)) { + try { + raw = await fsP.readFile(candidatePath, 'utf8'); + transcriptPath = candidatePath; + break; + } catch (error) { + lastError = error; + if ((error as { code?: string } | null)?.code !== 'ENOENT') { + 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 messages = lines.flatMap((line) => { try { @@ -90,19 +118,18 @@ export async function handleSessionRoutes( sessionStore.deleteSession(sessionKey); - const transcriptPath = getTranscriptFilePath(sessionKey); - try { - const fsP = await import('node:fs/promises'); - await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl')); - } catch { - if (rawSessionKey && rawSessionKey !== sessionKey) { - 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. - } + 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')); + break; + } catch { + continue; } } diff --git a/electron/gateway/handlers/chat.ts b/electron/gateway/handlers/chat.ts index f5e881a..dcf5e71 100644 --- a/electron/gateway/handlers/chat.ts +++ b/electron/gateway/handlers/chat.ts @@ -3,7 +3,7 @@ import { createProvider } from '@electron/providers'; import type { BaseProvider } from '@electron/providers/BaseProvider'; import { providerApiService } from '@electron/service/provider-api-service'; 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 { sessionStore } from '../session-store'; import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types'; diff --git a/electron/gateway/session-store.ts b/electron/gateway/session-store.ts index edd1091..6e71afe 100644 --- a/electron/gateway/session-store.ts +++ b/electron/gateway/session-store.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { app } from 'electron'; 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'; let sessionsFilePath: string | null = null; diff --git a/electron/utils/token-usage-writer.ts b/electron/utils/token-usage-writer.ts index f6903ad..0cd8c1a 100644 --- a/electron/utils/token-usage-writer.ts +++ b/electron/utils/token-usage-writer.ts @@ -1,21 +1,39 @@ import { app } from 'electron'; import * as fs from 'fs'; 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); - let agentId = parsed.isAgentSession ? parsed.agentId : 'default'; + const agentId = parsed.isAgentSession ? parsed.agentId : 'default'; let sessionId = parsed.isAgentSession ? parsed.sessionId : sessionKey; if (!sessionId) { 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`); } +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 { const filePath = getTranscriptFilePath(sessionKey); fs.mkdirSync(path.dirname(filePath), { recursive: true }); diff --git a/electron/utils/token-usage.ts b/electron/utils/token-usage.ts index 502727a..212a64f 100644 --- a/electron/utils/token-usage.ts +++ b/electron/utils/token-usage.ts @@ -14,12 +14,14 @@ export { type TokenUsageHistoryEntry, } from './token-usage-core'; -async function listAgentIdsWithSessionDirs(): Promise { - const agentsDir = join(app.getPath('userData'), 'agents'); +const TRANSCRIPT_ROOT_DIR_NAMES = ['models', 'agents'] as const; + +async function listAgentIdsWithSessionDirs(rootDirName: string): Promise { + const rootDir = join(app.getPath('userData'), rootDirName); const agentIds = new Set(); try { - const agentEntries = await readdir(agentsDir, { withFileTypes: true }); + const agentEntries = await readdir(rootDir, { withFileTypes: true }); for (const entry of agentEntries) { if (entry.isDirectory()) { const normalized = entry.name.trim(); @@ -36,38 +38,45 @@ async function listAgentIdsWithSessionDirs(): Promise { } async function listRecentSessionFiles(): Promise> { - const agentsDir = join(app.getPath('userData'), 'agents'); + const filesBySession = new Map(); try { - const agentEntries = await listAgentIdsWithSessionDirs(); - const files: Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }> = []; + for (const rootDirName of TRANSCRIPT_ROOT_DIR_NAMES) { + const agentEntries = await listAgentIdsWithSessionDirs(rootDirName); - for (const agentId of agentEntries) { - const sessionsDir = join(agentsDir, agentId, 'sessions'); - try { - const sessionEntries = await readdir(sessionsDir); + for (const agentId of agentEntries) { + const sessionsDir = join(app.getPath('userData'), rootDirName, agentId, 'sessions'); + try { + const sessionEntries = await readdir(sessionsDir); - for (const fileName of sessionEntries) { - const sessionId = extractSessionIdFromTranscriptFileName(fileName); - if (!sessionId) continue; - const filePath = join(sessionsDir, fileName); - try { - const fileStat = await stat(filePath); - files.push({ - filePath, - sessionId, - agentId, - mtimeMs: fileStat.mtimeMs, - }); - } catch { - continue; + for (const fileName of sessionEntries) { + const sessionId = extractSessionIdFromTranscriptFileName(fileName); + if (!sessionId) continue; + const filePath = join(sessionsDir, fileName); + try { + const fileStat = await stat(filePath); + const sessionKey = `${agentId}:${sessionId}`; + const existing = filesBySession.get(sessionKey); + + if (!existing || fileStat.mtimeMs > existing.mtimeMs) { + filesBySession.set(sessionKey, { + filePath, + sessionId, + agentId, + mtimeMs: fileStat.mtimeMs, + }); + } + } catch { + continue; + } } + } catch { + continue; } - } catch { - continue; } } + const files = [...filesBySession.values()]; files.sort((a, b) => b.mtimeMs - a.mtimeMs); return files; } catch { diff --git a/runtime-shared/lib/agents.ts b/runtime-shared/lib/agents.ts index da329e7..e9644da 100644 --- a/runtime-shared/lib/agents.ts +++ b/runtime-shared/lib/agents.ts @@ -1,92 +1 @@ -export const DEFAULT_AGENT_ID = 'main'; -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; - channelAccountOwners: Record; -} - -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 * from './models'; diff --git a/runtime-shared/lib/models.ts b/runtime-shared/lib/models.ts new file mode 100644 index 0000000..87a0837 --- /dev/null +++ b/runtime-shared/lib/models.ts @@ -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; + channelAccountOwners: Record; +} + +export interface AgentsSnapshot { + agents: AgentSummary[]; + models?: AgentSummary[]; + defaultAgentId: string; + defaultProviderAccountId: string | null; + defaultModelRef: string | null; + mainSessionSuffix: string; + configuredChannelTypes: string[]; + channelOwners: Record; + channelAccountOwners: Record; +} + +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; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 3eea23d..704b78d 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -8,7 +8,7 @@ import blueLogo from '../../assets/images/login/blue_logo.png'; const MENU_MARKS: Record = { '/home': House, '/knowledge': Book, - '/agents': Cpu, + '/models': Cpu, '/skills': Puzzle, '/cron': Clock, '/scripts': Code, diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 840e582..d6402c5 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -15,15 +15,15 @@ import { import { IPC_EVENTS } from '../../lib/constants'; import { invokeIpc } from '../../lib/host-api'; import { - agentsStore, channelStore, chatStore, getCompletedTasks, getPendingTasks, + modelsStore, taskStore, - useAgentsStore, useChannelStore, useChatStore, + useModelsStore, useTaskStore, type StagedAttachment, } from '../../stores'; @@ -149,7 +149,7 @@ function mapMessages(messages: RawMessage[], streamingMessage: RawMessage | null } export default function HomePage() { - const agentsState = useAgentsStore(); + const modelsState = useModelsStore(); const chat = useChatStore(); const taskState = useTaskStore(); const channelState = useChannelStore(); @@ -161,7 +161,7 @@ export default function HomePage() { const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false); useEffect(() => { - void agentsStore.init(); + void modelsStore.init(); void chatStore.init(); void taskStore.init(); void channelStore.init(); @@ -212,10 +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) + const selectedModelId = modelsState.models.some((model) => model.id === chat.currentAgentId) ? chat.currentAgentId - : agentsState.defaultAgentId; - const currentAgent = agentsState.agents.find((agent) => agent.id === selectedAgentId) || null; + : modelsState.defaultAgentId; + const currentModel = modelsState.models.find((model) => model.id === selectedModelId) || null; async function handleSendMessage(): Promise { const sent = await chatStore.sendMessage(inputMessage, attachments); @@ -288,7 +288,7 @@ export default function HomePage() { loading={!chat.initialized} selectedConversationId={chat.currentSessionKey} onNewChat={() => { - void chatStore.newSession(selectedAgentId || undefined); + void chatStore.newSession(selectedModelId || undefined); }} onSelectConversation={(conversationId) => { chatStore.switchSession(conversationId); @@ -316,23 +316,23 @@ export default function HomePage() {

智能对话

网关状态:{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'} - {currentAgent ? ` · 当前代理:${currentAgent.name}` : ''} + {currentModel ? ` · 当前模型:${currentModel.name}` : ''}