Merge pull request #103 from y574444354/fix/serve

Fix/serve
This commit is contained in:
DoSun 2026-05-15 10:38:57 +08:00 committed by GitHub
commit 22c5c7a1bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 128 additions and 54 deletions

View File

@ -8,15 +8,19 @@ const outdir = 'dist'
const { rmSync } = await import('fs')
rmSync(outdir, { recursive: true, force: true })
// Step 1.5: Generate review builtin files
console.log('Generating review builtin files...')
const { spawnSync: genSpawnSync } = await import('child_process')
const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], {
stdio: 'inherit',
cwd: process.cwd(),
})
if (genResult.status !== 0) {
console.warn('Warning: generate-review-builtin.ts failed, using existing files')
// Step 1.5: Generate review builtin files (skip with SKIP_REVIEW_BUILTIN=1)
if (process.env.SKIP_REVIEW_BUILTIN) {
console.log('Skipping review builtin generation (SKIP_REVIEW_BUILTIN is set)')
} else {
console.log('Generating review builtin files...')
const { spawnSync: genSpawnSync } = await import('child_process')
const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], {
stdio: 'inherit',
cwd: process.cwd(),
})
if (genResult.status !== 0) {
console.warn('Warning: generate-review-builtin.ts failed, using existing files')
}
}
// Default features that match the official CLI build.

View File

@ -73,14 +73,18 @@ const inspectArgs = process.env.BUN_INSPECT
// npm, etc.) and on all platforms.
const bunCmd = process.execPath;
// Generate review builtin files before dev launch
console.log('[dev] Generating review builtin files...');
const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], {
stdio: ["inherit", "inherit", "inherit"],
cwd: projectRoot,
});
if (!genResult.success) {
console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files');
// Generate review builtin files before dev launch (skip with SKIP_REVIEW_BUILTIN=1)
if (process.env.SKIP_REVIEW_BUILTIN) {
console.log('[dev] Skipping review builtin generation (SKIP_REVIEW_BUILTIN is set)');
} else {
console.log('[dev] Generating review builtin files...');
const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], {
stdio: ["inherit", "inherit", "inherit"],
cwd: projectRoot,
});
if (!genResult.success) {
console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files');
}
}
const args = [bunCmd, ...inspectArgs, "run", ...defineArgs, ...featureArgs, cliPath, ...process.argv.slice(2)];

View File

@ -161,7 +161,7 @@ import {
DEFAULT_OUTPUT_STYLE_NAME,
getAllOutputStyles,
} from 'src/constants/outputStyles.js'
import { TEAMMATE_MESSAGE_TAG, TICK_TAG } from 'src/constants/xml.js'
import { TASK_NOTIFICATION_TAG, TEAMMATE_MESSAGE_TAG, TICK_TAG } from 'src/constants/xml.js'
import {
getSettings_DEPRECATED,
getSettingsWithSources,
@ -4360,29 +4360,44 @@ function runHeadlessStreaming(
trackReceivedMessageUuid(userMsg.uuid as UUID)
}
enqueue({
mode: 'prompt' as const,
// file_attachments rides the protobuf catchall from the web composer.
// Same-ref no-op when absent (no 'file_attachments' key).
value: await resolveAndPrepend(
userMsg,
(userMsg.message as { content: ContentBlockParam[] }).content,
),
uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`,
priority: (userMsg as { priority?: string })
.priority as import('src/types/textInputTypes.js').QueuePriority,
})
// Increment prompt count for attribution tracking and save snapshot
// The snapshot persists promptCount so it survives compaction
if (feature('COMMIT_ATTRIBUTION')) {
setAppState(prev => ({
...prev,
attribution: incrementPromptCount(prev.attribution, snapshot => {
void recordAttributionSnapshot(snapshot).catch(error => {
logForDebugging(`Attribution: Failed to save snapshot: ${error}`)
})
}),
}))
const rawContent = typeof userMsg.content === 'string' ? userMsg.content : ''
const isTaskNotification = rawContent.trimStart().startsWith(
`<${TASK_NOTIFICATION_TAG}>`,
)
if (isTaskNotification) {
enqueue({
mode: 'task-notification' as const,
value: rawContent,
uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`,
priority: (userMsg as { priority?: string })
.priority as import('src/types/textInputTypes.js').QueuePriority,
})
} else {
enqueue({
mode: 'prompt' as const,
// file_attachments rides the protobuf catchall from the web composer.
// Same-ref no-op when absent (no 'file_attachments' key).
value: await resolveAndPrepend(
userMsg,
(userMsg.message as { content: ContentBlockParam[] }).content,
),
uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`,
priority: (userMsg as { priority?: string })
.priority as import('src/types/textInputTypes.js').QueuePriority,
})
// Increment prompt count for attribution tracking and save snapshot
// The snapshot persists promptCount so it survives compaction
if (feature('COMMIT_ATTRIBUTION')) {
setAppState(prev => ({
...prev,
attribution: incrementPromptCount(prev.attribution, snapshot => {
void recordAttributionSnapshot(snapshot).catch(error => {
logForDebugging(`Attribution: Failed to save snapshot: ${error}`)
})
}),
}))
}
}
void run()
}

View File

@ -74,7 +74,7 @@ export class EventBus {
const sid = dataObj?.session_id ?? dataObj?.sessionID
if (typeof sid === 'string') {
const sessionCwd = this.sessionCwds.get(sid)
if (sessionCwd && sessionCwd !== client.cwdFilter) {
if (sessionCwd !== client.cwdFilter) {
continue
}
}

View File

@ -221,14 +221,12 @@ export function createSessionRoutes(
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)
// 优先从请求头 x-csc-directory 取工作目录cs-cloud 通过此头传递),
// fallback 到 query string 的 dir 参数
const headerDir = c.req.header('x-csc-directory')
const dir = (headerDir ? decodeURIComponent(headerDir) : undefined)
?? url.searchParams.get('dir')
?? undefined
// roots=true 时只返回没有 parentID 的顶层 sessioncsc 无 parent 概念,全部视为 root
// const rootsOnly = url.searchParams.get('roots') === 'true'
const canonicalDir = dir ? await canonicalizePath(dir).catch(() => dir) : undefined
// 从磁盘读取历史 session 列表
let historySessions: Awaited<ReturnType<typeof listSessionsImpl>> = []
@ -271,7 +269,6 @@ export function createSessionRoutes(
// 补充内存中有但磁盘还没落盘的活跃 session刚创建还没写过消息的
const historyIds = new Set(historySessions.map(s => s.sessionId))
const canonicalDir = dir ? await canonicalizePath(dir).catch(() => dir) : undefined
for (const handle of sessionManager.getAllSessions()) {
if (!historyIds.has(handle.sessionId)) {
if (canonicalDir) {
@ -283,7 +280,31 @@ export function createSessionRoutes(
}
}
// 过滤掉已知的无意义管理命令会话(用户直接输入后立即退出,没有实际对话内容)
// Pre-canonicalize all session cwds for consistent cwd filtering
const cwdCache = new Map<string, string>()
const getCwd = async (p: string) => {
const hit = cwdCache.get(p)
if (hit !== undefined) return hit
const c = await canonicalizePath(p).catch(() => p)
cwdCache.set(p, c)
return c
}
// Filter by cwd to prevent cross-workspace leakage
const cwdFiltered: typeof merged = []
for (const s of merged) {
if (canonicalDir) {
const sessionCwd = s.cwd ?? s.directory ?? ''
if (sessionCwd) {
const c = await getCwd(sessionCwd)
if (c !== canonicalDir) continue
} else {
continue
}
}
cwdFiltered.push(s)
}
const BORING_COMMANDS = new Set([
'/exit', '/quit', '/bye',
'/clear', '/reset',
@ -292,7 +313,7 @@ export function createSessionRoutes(
'/help', '/version',
'/compact',
])
const filtered = merged.filter(s => {
const filtered = cwdFiltered.filter(s => {
const t = s.title.trim()
if (!t) return false
if (BORING_COMMANDS.has(t)) return false
@ -308,7 +329,10 @@ export function createSessionRoutes(
.get('/session/status', async c => {
const headerDir = c.req.header('x-csc-directory')
const dir = headerDir ? decodeURIComponent(headerDir) : undefined
const statuses = sessionManager.getSessionStatuses(dir)
const activeCount = sessionManager.getActiveCount()
const indexCount = sessionManager.getLoadedIndexCount()
const statuses = await sessionManager.getSessionStatuses(dir)
process.stderr.write(`[status] dir=${dir ?? '(none)'} active=${activeCount} index=${indexCount} result=${JSON.stringify(statuses)}\n`)
return c.json(statuses)
})
.get('/session/:sessionID', async c => {
@ -318,6 +342,7 @@ export function createSessionRoutes(
const info = handle.getInfo()
return c.json({
...info,
busy_status: handle.getEffectiveBusyStatus(),
message_count: handle.messageCount,
usage: handle.usage,
})

View File

@ -141,6 +141,16 @@ export class SessionHandle implements DisposableChildProcess {
return this._lastMessageUuid
}
getEffectiveBusyStatus(): SessionBusyStatus {
if (this._prompting) {
return { type: 'busy' }
}
if (this._status === 'stopped') {
return { type: 'idle' }
}
return this._busyStatus
}
get ready(): boolean {
return this._status === 'running'
}

View File

@ -147,6 +147,10 @@ export class SessionManager {
return this.sessions.size
}
getLoadedIndexCount(): number {
return this.loadedIndex.size
}
getSession(id: string): SessionHandle | undefined {
return this.sessions.get(id)
}
@ -163,7 +167,20 @@ export class SessionManager {
const handleCwd = await canonicalizePath(handle.cwd)
if (handleCwd !== canonicalCwd) continue
}
result[id] = handle.busyStatus
result[id] = handle.getEffectiveBusyStatus()
}
// Supplement with persisted sessions that are no longer in memory
// (evicted by idle timeout / LRU). They are always idle.
if (canonicalCwd) {
for (const [id, entry] of this.loadedIndex) {
if (result[id]) continue
try {
const entryCwd = await canonicalizePath(entry.cwd)
if (entryCwd === canonicalCwd) {
result[id] = { type: 'idle' }
}
} catch {}
}
}
return result
}
@ -220,9 +237,8 @@ export class SessionManager {
this.sessions.set(sessionId, handle)
if (!opts.silent) {
void canonicalizePath(cwd).then(canonical => {
this.eventBus.registerSessionCwd(sessionId, canonical)
})
const canonical = await canonicalizePath(cwd).catch(() => cwd)
this.eventBus.registerSessionCwd(sessionId, canonical)
this.eventBus.publishSessionEvent(sessionId, 'created', {
status: 'starting',
created_at: Date.now(),