Merge pull request #49 from Askhz/feat/server-add-session
Feat/server add session
This commit is contained in:
commit
8261385cbb
|
|
@ -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 的顶层 session(csc 无 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 })
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user