diff --git a/packages/server/src/db/schema.ts b/packages/server/src/db/schema.ts index 643cf84..ebcfe50 100644 --- a/packages/server/src/db/schema.ts +++ b/packages/server/src/db/schema.ts @@ -62,6 +62,7 @@ export const tasks = sqliteTable('tasks', { repoUrl: text('repo_url'), selectedAgent: text('selected_agent').default('claude'), selectedModel: text('selected_model'), + mode: text('mode').notNull().default('default'), // 'default' | 'coding' installDependencies: integer('install_dependencies', { mode: 'boolean' }).default(false), maxDuration: integer('max_duration').default(parseInt(process.env.MAX_SANDBOX_DURATION || '300', 10)), keepAlive: integer('keep_alive', { mode: 'boolean' }).default(false), diff --git a/packages/server/src/db/types.ts b/packages/server/src/db/types.ts index ba5d712..f3e0ec4 100644 --- a/packages/server/src/db/types.ts +++ b/packages/server/src/db/types.ts @@ -37,6 +37,7 @@ export interface Task { repoUrl: string | null selectedAgent: string | null selectedModel: string | null + mode: string installDependencies: boolean | null maxDuration: number | null keepAlive: boolean | null diff --git a/packages/server/src/routes/acp.ts b/packages/server/src/routes/acp.ts index b433f6d..a28cb7d 100644 --- a/packages/server/src/routes/acp.ts +++ b/packages/server/src/routes/acp.ts @@ -186,35 +186,6 @@ acp.delete('/conversation/:conversationId', async (c) => { return c.json({ status: 'success' }) }) -// ─── Chat Endpoint (SSE) ─────────────────────────────────────────────────── - -/** - * POST /api/agent/chat - * - * 简单的聊天端点,返回 SSE 流式响应 - */ -acp.post('/chat', async (c) => { - const body = await c.req.json<{ prompt: string; conversationId?: string; model?: string }>() - const { prompt, conversationId, model } = body - - const { envId, userId, credentials: userCredentials } = c.get('userEnv')! - if (!envId) { - return c.json({ error: 'CloudBase environment not bound' }, 400) - } - - const actualConversationId = conversationId || uuidv4() - - return observeStreamWithLiveCallback(c, null, actualConversationId, envId, userId, async (callback) => { - return cloudbaseAgentService.chatStream(prompt, callback, { - conversationId: actualConversationId, - envId, - userId, - userCredentials, - model, - }) - }) -}) - // ─── ACP JSON-RPC 2.0 Endpoint ───────────────────────────────────────────── /** @@ -384,6 +355,14 @@ async function handleSessionPrompt(c: any, id: number | string, params: SessionP } catch { // write failure doesn't affect main flow } + // Resolve task mode + let taskMode: 'default' | 'coding' | undefined + try { + const task = await getDb().tasks.findById(sessionId) + if (task?.mode === 'coding') taskMode = 'coding' + } catch { + // ignore + } // Launch agent with liveCallback for real-time SSE push return observeStreamWithLiveCallback(c, id, sessionId, envId, userId, async (callback) => { @@ -395,6 +374,7 @@ async function handleSessionPrompt(c: any, id: number | string, params: SessionP model: selectedModel, askAnswers: params.askAnswers, toolConfirmation: params.toolConfirmation, + mode: taskMode, }) }) } diff --git a/packages/server/src/routes/tasks.ts b/packages/server/src/routes/tasks.ts index 0b3ed2e..454719c 100644 --- a/packages/server/src/routes/tasks.ts +++ b/packages/server/src/routes/tasks.ts @@ -289,6 +289,7 @@ tasksRouter.post('/', async (c) => { repoUrl, selectedAgent = 'claude', selectedModel, + mode = 'default', installDependencies = false, maxDuration = 300, keepAlive = false, @@ -321,6 +322,7 @@ tasksRouter.post('/', async (c) => { repoUrl: repoUrl || null, selectedAgent, selectedModel: selectedModel || null, + mode, installDependencies, maxDuration, keepAlive, @@ -2331,4 +2333,56 @@ tasksRouter.post('/:taskId/file-operation', requireUserEnv, async (c) => { } }) +// --------------------------------------------------------------------------- +// Preview proxy — forward requests to dev server inside sandbox +// --------------------------------------------------------------------------- + +tasksRouter.all('/:taskId/preview/*', async (c) => { + const authErr = requireAuth(c) + if (authErr) return authErr + const session = c.get('session')! + const taskId = c.req.param('taskId') + + const task = await getDb().tasks.findById(taskId) + if (!task || task.userId !== session.user.id) { + return c.json({ error: 'Task not found' }, 404) + } + + const envId = (c.get('userEnv') as any)?.envId || process.env.TCB_ENV_ID || '' + const sandbox = await getScfSandbox(task, envId) + if (!sandbox) { + return c.json({ error: 'Sandbox not available' }, 503) + } + + // Extract the path after /preview/ + const fullPath = c.req.path + const previewIdx = fullPath.indexOf('/preview/') + const proxyPath = previewIdx >= 0 ? fullPath.slice(previewIdx + '/preview'.length) : '/' + + try { + const res = await sandbox.request(`/proxy/5173${proxyPath}`, { + method: c.req.method, + headers: { + Accept: c.req.header('accept') || '*/*', + 'Accept-Encoding': c.req.header('accept-encoding') || '', + }, + signal: AbortSignal.timeout(30_000), + }) + + // Forward response headers and body + const contentType = res.headers.get('content-type') + const body = await res.arrayBuffer() + + return new Response(body, { + status: res.status, + headers: { + ...(contentType ? { 'Content-Type': contentType } : {}), + 'Cache-Control': 'no-cache', + }, + }) + } catch { + return c.json({ error: 'Dev server not responding' }, 502) + } +}) + export default tasksRouter diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index 8c42068..47c5875 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -467,5 +467,6 @@ export interface AgentOptions { } /** 指定模型 */ model?: string - mode?: string + /** 任务模式 */ + mode?: 'default' | 'coding' } diff --git a/packages/shared/src/types/task.ts b/packages/shared/src/types/task.ts index 2e7214e..598ae75 100644 --- a/packages/shared/src/types/task.ts +++ b/packages/shared/src/types/task.ts @@ -19,6 +19,7 @@ export const insertTaskSchema = z.object({ repoUrl: z.string().url().optional(), selectedAgent: z.enum(['claude', 'codex', 'copilot', 'cursor', 'gemini', 'opencode']).default('claude'), selectedModel: z.string().optional(), + mode: z.enum(['default', 'coding']).default('default'), installDependencies: z.boolean().default(false), maxDuration: z.number().default(300), keepAlive: z.boolean().default(false), @@ -54,6 +55,7 @@ export const selectTaskSchema = z.object({ repoUrl: z.string().nullable(), selectedAgent: z.string().nullable(), selectedModel: z.string().nullable(), + mode: z.enum(['default', 'coding']).nullable(), installDependencies: z.boolean().nullable(), maxDuration: z.number().nullable(), keepAlive: z.boolean().nullable(), diff --git a/packages/web/src/components/home-page-content.tsx b/packages/web/src/components/home-page-content.tsx index 3347c0e..822884b 100644 --- a/packages/web/src/components/home-page-content.tsx +++ b/packages/web/src/components/home-page-content.tsx @@ -437,6 +437,7 @@ export function HomePageContent({ maxDuration: number keepAlive: boolean enableBrowser: boolean + mode: 'default' | 'coding' }) => { console.log( '[TaskSubmit] called, isSubmitting:', diff --git a/packages/web/src/components/task-chat.tsx b/packages/web/src/components/task-chat.tsx index d8fd42a..362d65a 100644 --- a/packages/web/src/components/task-chat.tsx +++ b/packages/web/src/components/task-chat.tsx @@ -571,9 +571,24 @@ export function TaskChat({ ), } + const isCodingMode = task.mode === 'coding' + // ─── Tab content ─────────────────────────────────────────────────── const renderTabContent = () => { + if (activeTab === 'preview' && isCodingMode) { + return ( +