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,
|
sessionError,
|
||||||
conflict,
|
conflict,
|
||||||
} from '../errors.js'
|
} from '../errors.js'
|
||||||
|
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
||||||
|
|
||||||
function ssePrompt(
|
function ssePrompt(
|
||||||
handle: import('../sessionHandle.js').SessionHandle,
|
handle: import('../sessionHandle.js').SessionHandle,
|
||||||
|
|
@ -164,31 +165,149 @@ export function createSessionRoutes(
|
||||||
throw sessionError(msg)
|
throw sessionError(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get('/session', c => {
|
.get('/session', async c => {
|
||||||
const url = new URL(c.req.url)
|
const url = new URL(c.req.url)
|
||||||
const limit = parseInt(url.searchParams.get('limit') ?? '50', 10)
|
const limit = parseInt(url.searchParams.get('limit') ?? '50', 10)
|
||||||
const offset = parseInt(url.searchParams.get('offset') ?? '0', 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()
|
// 从磁盘读取历史 session 列表
|
||||||
const sessions = handles
|
let historySessions: Awaited<ReturnType<typeof listSessionsImpl>> = []
|
||||||
.slice(offset, offset + limit)
|
try {
|
||||||
.map(h => h.getInfo())
|
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 })
|
return c.json({ sessions })
|
||||||
})
|
})
|
||||||
.get('/session/status', c => {
|
.get('/session/:sessionID', async c => {
|
||||||
return c.json({ sessions: sessionManager.getSessionStatuses() })
|
|
||||||
})
|
|
||||||
.get('/session/:sessionID', c => {
|
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
const handle = sessionManager.getSession(id)
|
const handle = sessionManager.getSession(id)
|
||||||
if (!handle) throw notFound('session not found')
|
if (handle) {
|
||||||
const info = handle.getInfo()
|
const info = handle.getInfo()
|
||||||
return c.json({
|
return c.json({
|
||||||
...info,
|
...info,
|
||||||
message_count: handle.messageCount,
|
message_count: handle.messageCount,
|
||||||
usage: handle.usage,
|
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 => {
|
.patch('/session/:sessionID', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
|
|
@ -298,4 +417,29 @@ export function createSessionRoutes(
|
||||||
if (!body.command) throw badRequest('command is required')
|
if (!body.command) throw badRequest('command is required')
|
||||||
return ssePrompt(handle, id, body.command, c)
|
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