Merge pull request #49 from Askhz/feat/server-add-session

Feat/server add session
This commit is contained in:
DoSun 2026-05-08 18:34:17 +08:00 committed by GitHub
commit 8261385cbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -9,6 +9,7 @@ import {
sessionError,
conflict,
} from '../errors.js'
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
function ssePrompt(
handle: import('../sessionHandle.js').SessionHandle,
@ -164,31 +165,149 @@ export function createSessionRoutes(
throw sessionError(msg)
}
})
.get('/session', c => {
.get('/session', async c => {
const url = new URL(c.req.url)
const limit = parseInt(url.searchParams.get('limit') ?? '50', 10)
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10)
const dir = url.searchParams.get('dir') ?? undefined
// roots=true 时只返回没有 parentID 的顶层 sessioncsc 无 parent 概念,全部视为 root
// const rootsOnly = url.searchParams.get('roots') === 'true'
const handles = sessionManager.getAllSessions()
const sessions = handles
.slice(offset, offset + limit)
.map(h => h.getInfo())
// 从磁盘读取历史 session 列表
let historySessions: Awaited<ReturnType<typeof listSessionsImpl>> = []
try {
historySessions = await listSessionsImpl({ dir, limit: limit + offset })
process.stderr.write(`[server:session] listSessionsImpl dir=${dir ?? 'all'} found=${historySessions.length}\n`)
} catch (err) {
process.stderr.write(`[server:session] listSessionsImpl error: ${err}\n`)
}
// 内存中活跃的 handle用于覆盖运行时状态
const handleMap = new Map(
sessionManager.getAllSessions().map(h => [h.sessionId, h])
)
// 把磁盘历史会话转成统一格式,如果内存中有对应 handle 则合并运行时字段
const merged = historySessions.map(s => {
const handle = handleMap.get(s.sessionId)
const info = handle?.getInfo()
return {
session_id: s.sessionId,
status: info?.status ?? 'stopped',
cwd: info?.cwd ?? s.cwd ?? '',
title: (info?.title ?? s.customTitle ?? s.firstPrompt ?? s.summary) ?? '',
model: info?.model,
permission_mode: info?.permission_mode,
created_at: s.createdAt ?? info?.created_at ?? 0,
last_active_at: s.lastModified ?? info?.last_active_at ?? 0,
cost_usd: info?.cost_usd ?? 0,
input_tokens: info?.input_tokens ?? 0,
output_tokens: info?.output_tokens ?? 0,
}
})
// 补充内存中有但磁盘还没落盘的活跃 session刚创建还没写过消息的
const historyIds = new Set(historySessions.map(s => s.sessionId))
for (const handle of sessionManager.getAllSessions()) {
if (!historyIds.has(handle.sessionId)) {
const info = handle.getInfo()
merged.push({ ...info, title: info.title ?? '' })
}
}
// 过滤掉已知的无意义管理命令会话(用户直接输入后立即退出,没有实际对话内容)
const BORING_COMMANDS = new Set([
'/exit', '/quit', '/bye',
'/clear', '/reset',
'/model', '/models',
'/login', '/logout',
'/help', '/version',
'/compact',
])
const filtered = merged.filter(s => {
const t = s.title.trim()
if (!t) return false
if (BORING_COMMANDS.has(t)) return false
return true
})
// 按最后活跃时间倒序
filtered.sort((a, b) => (b.last_active_at ?? 0) - (a.last_active_at ?? 0))
const sessions = filtered.slice(offset, offset + limit)
process.stderr.write(`[server:session] GET /session -> history=${historySessions.length} active=${handleMap.size} merged=${merged.length} returned=${sessions.length}\n`)
return c.json({ sessions })
})
.get('/session/status', async c => {
// 内存中活跃 session 的状态
const activeStatuses = sessionManager.getSessionStatuses()
// 补充磁盘历史 session全部视为 idle
let historySessions: Awaited<ReturnType<typeof listSessionsImpl>> = []
try {
historySessions = await listSessionsImpl({ limit: 200 })
} catch {}
const sessions: Record<string, { status: string; state: string; has_pending_permission: boolean; type: string }> = {}
// 先把历史 session 全部标为 idle/stopped
for (const s of historySessions) {
sessions[s.sessionId] = {
status: 'stopped',
state: 'stopped',
has_pending_permission: false,
type: 'idle',
}
}
// 用内存中活跃的 handle 状态覆盖
for (const [id, st] of Object.entries(activeStatuses)) {
sessions[id] = {
status: st.status,
state: st.status,
has_pending_permission: st.has_pending_permission,
type: st.status === 'running' ? 'busy' : 'idle',
}
}
return c.json({ sessions })
})
.get('/session/status', c => {
return c.json({ sessions: sessionManager.getSessionStatuses() })
})
.get('/session/:sessionID', c => {
.get('/session/:sessionID', async c => {
const id = c.req.param('sessionID')
const handle = sessionManager.getSession(id)
if (!handle) throw notFound('session not found')
const info = handle.getInfo()
return c.json({
...info,
message_count: handle.messageCount,
usage: handle.usage,
})
if (handle) {
const info = handle.getInfo()
return c.json({
...info,
message_count: handle.messageCount,
usage: handle.usage,
})
}
// 内存中没有,从磁盘历史记录查找
try {
const history = await listSessionsImpl({ limit: 1000 })
const s = history.find(h => h.sessionId === id)
if (s) {
return c.json({
session_id: s.sessionId,
status: 'stopped',
cwd: s.cwd ?? '',
title: (s.customTitle ?? s.firstPrompt ?? s.summary) ?? '',
model: undefined,
permission_mode: undefined,
created_at: s.createdAt ?? 0,
last_active_at: s.lastModified ?? 0,
cost_usd: 0,
input_tokens: 0,
output_tokens: 0,
message_count: 0,
usage: { input_tokens: 0, output_tokens: 0 },
})
}
} catch {}
throw notFound('session not found')
})
.patch('/session/:sessionID', async c => {
const id = c.req.param('sessionID')
@ -298,4 +417,29 @@ export function createSessionRoutes(
if (!body.command) throw badRequest('command is required')
return ssePrompt(handle, id, body.command, c)
})
.post('/session/:sessionID/command_async', async c => {
const id = c.req.param('sessionID')
const handle = sessionManager.getSession(id)
if (!handle) throw notFound('session not found')
const body = await c.req.json<{ command: string }>()
if (!body.command) throw badRequest('command is required')
if (handle.prompting) throw conflict('session is already processing a prompt')
handle.prompt(body.command).catch(() => {})
return new Response(null, { status: 204 })
})
.post('/session/:sessionID/revert', async c => {
const id = c.req.param('sessionID')
const handle = sessionManager.getSession(id)
if (!handle) throw notFound('session not found')
return c.json(handle.getInfo())
})
.post('/session/:sessionID/summarize', async c => {
const id = c.req.param('sessionID')
const handle = sessionManager.getSession(id)
if (!handle) throw notFound('session not found')
return c.json({ ok: true })
})
}