From 122f5d8c52cde4dc9d2703389fa4b0131e861b9a Mon Sep 17 00:00:00 2001 From: yanweidong Date: Tue, 21 Jul 2026 19:19:36 +0800 Subject: [PATCH] feat: align controlled AI sessions and MVP controls --- apps/web_v1/scripts/structure-check.mjs | 31 +- apps/web_v1/src/api/ai.ts | 30 ++ apps/web_v1/src/app/App.tsx | 15 +- apps/web_v1/src/pages/projects/project-ai.tsx | 178 +++++++---- .../pages/projects/project-channel-page.tsx | 7 +- .../src/pages/projects/project-cron.tsx | 2 +- .../pages/projects/project-new-channel.tsx | 48 +-- .../src/pages/projects/project-statusbar.tsx | 120 +------- .../src/pages/projects/project-topbar.tsx | 20 -- apps/web_v1/src/pages/workspace-explore.tsx | 218 +------------- apps/web_v1/src/pages/workspace-home.tsx | 9 +- backend/cmd/api/main.go | 4 + backend/internal/logic/ai/gateway.go | 16 +- backend/internal/logic/ai/gateway_test.go | 18 +- backend/internal/logic/ai/handlers.go | 120 ++++++++ backend/internal/logic/ai/handlers_test.go | 278 ++++++++++++++++++ backend/internal/logic/ai/sessions.go | 111 ++++++- backend/internal/models/ai_session.go | 1 + 18 files changed, 758 insertions(+), 468 deletions(-) create mode 100644 apps/web_v1/src/api/ai.ts create mode 100644 backend/internal/logic/ai/handlers.go create mode 100644 backend/internal/logic/ai/handlers_test.go diff --git a/apps/web_v1/scripts/structure-check.mjs b/apps/web_v1/scripts/structure-check.mjs index dd5a009..2097376 100644 --- a/apps/web_v1/scripts/structure-check.mjs +++ b/apps/web_v1/scripts/structure-check.mjs @@ -24,6 +24,7 @@ const requiredFiles = [ 'src/api/mappers.tsx', 'src/api/search.ts', 'src/api/inbox.ts', + 'src/api/ai.ts', 'scripts/api-client.test.mjs', ] @@ -51,7 +52,7 @@ if (!projectRailSource.includes('projectNameLabel(project.name)')) { failures.push('project rail labels must derive from the project name') } -const apiFiles = ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts'].map((name) => `src/api/${name}`) +const apiFiles = ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts', 'ai.ts'].map((name) => `src/api/${name}`) const apiSource = apiFiles.map((file) => readFileSync(file, 'utf8')).join('\n') for (const forbidden of [ { pattern: /\bID\s*\?\s*:/, label: 'optional PascalCase ID compatibility field' }, @@ -101,6 +102,34 @@ if (!inboxSource.includes('body: { suggestionIds }')) failures.push('inbox confi const inboxPageSource = readFileSync('src/pages/projects/project-inbox.tsx', 'utf8') if (!inboxPageSource.includes('确认创建')) failures.push('project inbox must expose the single confirmation action') +const aiApiSource = readFileSync('src/api/ai.ts', 'utf8') +for (const required of ['/api/v1/projects/', '/ai-sessions', 'listAISessions', 'createAISession']) { + if (!aiApiSource.includes(required)) failures.push(`AI API must include ${required}`) +} +if (/\b(?:task|note|source)Id\b/.test(aiApiSource)) failures.push('AI session responses must not expose automatic formal object IDs') + +const aiPageSource = readFileSync('src/pages/projects/project-ai.tsx', 'utf8') +for (const required of ['AI 助手', '创建会话', 'loading', 'error']) { + if (!aiPageSource.includes(required)) failures.push(`project AI page must include ${required}`) +} +for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息', 'IconAttachment', 'agent-send-button']) { + if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains unsupported chat control ${forbidden}`) +} + +const unsupportedControls = [ + { file: 'src/pages/workspace-explore.tsx', required: '暂未开放', forbidden: ['同步数据源', '添加数据源', 'IconRefresh', 'IconEdit', 'IconDelete'] }, + { file: 'src/pages/projects/project-new-channel.tsx', required: '暂未开放', forbidden: ['保存频道', '(`/api/v1/projects/${projectId}/ai-sessions`, { + token: session.token, + }) +} + +export async function createAISession(session: ApiSession, projectId: string, input: CreateAISessionInput) { + return apiRequest(`/api/v1/projects/${projectId}/ai-sessions`, { + method: 'POST', + token: session.token, + body: input, + }) +} diff --git a/apps/web_v1/src/app/App.tsx b/apps/web_v1/src/app/App.tsx index df6399a..31bbe1a 100644 --- a/apps/web_v1/src/app/App.tsx +++ b/apps/web_v1/src/app/App.tsx @@ -1,8 +1,9 @@ -import { useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { ConfigProvider, Message, Spin } from '@arco-design/web-react' import '@arco-design/web-react/dist/css/arco.css' import '../App.css' import { ApiError, login, setApiBaseUrl, type ApiSession } from '../api/client' +import { createAISession, listAISessions, type CreateAISessionInput } from '../api/ai' import { analyzeInboxItem, confirmInboxItem } from '../api/inbox' import { mapWorkspace } from '../api/mappers' import { @@ -43,6 +44,16 @@ function App() { const [searchResultPreview, setSearchResultPreview] = useState(null) const workspaceSearch = useWorkbenchSearch(session) + const handleListAISessions = useCallback((projectId: string) => { + if (!session) return Promise.reject(new Error('未登录')) + return listAISessions(session, projectId) + }, [session]) + + const handleCreateAISession = useCallback((projectId: string, input: CreateAISessionInput) => { + if (!session) return Promise.reject(new Error('未登录')) + return createAISession(session, projectId, input) + }, [session]) + const dark = theme === 'dark' const activeWorkspace = workspaces.find((workspace) => workspace.project.id === activeProjectID) ?? workspaces[0] const activeTagOptions = activeWorkspace?.tags.filter((tag) => tag !== 'all' && tag !== '全部') ?? [] @@ -346,6 +357,8 @@ function App() { onSelectSearchResult={handleSelectSearchResult} onAnalyzeInbox={handleAnalyzeInbox} onConfirmInbox={handleConfirmInbox} + onListAISessions={handleListAISessions} + onCreateAISession={handleCreateAISession} /> ) : ( diff --git a/apps/web_v1/src/pages/projects/project-ai.tsx b/apps/web_v1/src/pages/projects/project-ai.tsx index e3bd151..0b55a11 100644 --- a/apps/web_v1/src/pages/projects/project-ai.tsx +++ b/apps/web_v1/src/pages/projects/project-ai.tsx @@ -1,80 +1,134 @@ -import { Button, Card, Input, Space, Typography } from '@arco-design/web-react' -import { - IconArrowUp, - IconAttachment, - IconPlusCircle, - IconRobot, -} from '@arco-design/web-react/icon' -import type { AISession, ProjectWorkspace } from './project-types' +import { useEffect, useState } from 'react' +import { Alert, Button, Card, Empty, Input, Space, Spin, Tag, Typography } from '@arco-design/web-react' +import { IconPlusCircle, IconRobot } from '@arco-design/web-react/icon' +import type { AISessionDTO, CreateAISessionInput } from '../../api/ai' +import type { ProjectWorkspace } from './project-types' const { Title, Text } = Typography -export function ProjectAi({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) { - const { aiSessions } = activeWorkspace - const groupedSessions = groupSessions(aiSessions) +export function ProjectAi({ + activeWorkspace, + onSelectItem, + onListSessions, + onCreateSession, +}: { + activeWorkspace: ProjectWorkspace + onSelectItem: (title: string) => void + onListSessions: (projectId: string) => Promise + onCreateSession: (projectId: string, input: CreateAISessionInput) => Promise +}) { + const [sessions, setSessions] = useState([]) + const [title, setTitle] = useState('') + const [context, setContext] = useState('') + const [loading, setLoading] = useState(true) + const [creating, setCreating] = useState(false) + const [error, setError] = useState('') + const projectId = activeWorkspace.project.id + + useEffect(() => { + let current = true + setLoading(true) + setError('') + void onListSessions(projectId) + .then((items) => { + if (current) setSessions(items) + }) + .catch((requestError: unknown) => { + if (current) setError(requestError instanceof Error ? requestError.message : 'AI 会话加载失败,请稍后重试') + }) + .finally(() => { + if (current) setLoading(false) + }) + return () => { + current = false + } + }, [onListSessions, projectId]) + + const createSession = async () => { + const trimmedTitle = title.trim() + if (!trimmedTitle) { + setError('请输入 AI 会话标题') + return + } + setCreating(true) + setError('') + try { + const created = await onCreateSession(projectId, { + title: trimmedTitle, + context: context.trim(), + }) + setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)]) + setTitle('') + setContext('') + onSelectItem(created.title) + } catch (requestError) { + setError(requestError instanceof Error ? requestError.message : 'AI 会话创建失败,请稍后重试') + } finally { + setCreating(false) + } + } return (
-
- - +
+
+ AI 助手 + 创建项目内普通会话;不会自动生成任务、笔记或资料。 +
+
-
- {groupedSessions.length ? ( - groupedSessions.map((group) => ( -
- {group.label} - {group.items.map((session, index) => ( - - ))} -
- )) - ) : ( - 暂无对话 - )} -
+ {error ? setError('')} /> : null} + +
+ + + {sessions.length ? ( +
+ {sessions.map((session) => ( + + ))} +
+ ) : loading ? null : } +
-
- 选择专家,开始对话 -
-
- -
- - -
+ + + + AI 输出如需转为正式对象,必须另行确认。 +
) } -function groupSessions(sessions: AISession[]) { - const labels = ['今天', '昨天', '7 天内'] - return labels.map((label) => ({ - label, - items: sessions.filter((session, index) => session.time.includes(label) || (!labels.some((item) => session.time.includes(item)) && labels.indexOf(label) === fallbackGroupIndex(index))), - })).filter((group) => group.items.length > 0) -} - -function fallbackGroupIndex(index: number) { - if (index === 0) return 0 - if (index === 1) return 1 - return 2 +function sessionStatusLabel(status: string) { + if (status === 'ready') return '待开始' + if (status === 'failed') return '创建失败' + return status || '未知状态' } diff --git a/apps/web_v1/src/pages/projects/project-channel-page.tsx b/apps/web_v1/src/pages/projects/project-channel-page.tsx index 5e64aa2..6809187 100644 --- a/apps/web_v1/src/pages/projects/project-channel-page.tsx +++ b/apps/web_v1/src/pages/projects/project-channel-page.tsx @@ -8,6 +8,7 @@ import type { ProjectTaskUpdate } from './project-task-edit-modal' import { ProjectTasks } from './project-tasks' import type { ChannelKey, InboxConfirmationOutcome, ProjectWorkspace } from './project-types' import type { InboxSuggestionDTO } from '../../api/inbox' +import type { AISessionDTO, CreateAISessionInput } from '../../api/ai' export function ProjectChannelPage({ activeChannel, @@ -23,6 +24,8 @@ export function ProjectChannelPage({ onUpdateTask, onAnalyzeInbox, onConfirmInbox, + onListAISessions, + onCreateAISession, }: { activeChannel: ChannelKey activeWorkspace: ProjectWorkspace @@ -37,6 +40,8 @@ export function ProjectChannelPage({ onUpdateTask: (update: ProjectTaskUpdate) => void onAnalyzeInbox: (inboxId: string) => Promise onConfirmInbox: (inboxId: string, suggestionIds: string[]) => Promise + onListAISessions: (projectId: string) => Promise + onCreateAISession: (projectId: string, input: CreateAISessionInput) => Promise }) { switch (activeChannel) { case 'inbox': @@ -44,7 +49,7 @@ export function ProjectChannelPage({ case 'tasks': return case 'ai': - return + return case 'notes': return case 'cron': diff --git a/apps/web_v1/src/pages/projects/project-cron.tsx b/apps/web_v1/src/pages/projects/project-cron.tsx index f65547b..e33853e 100644 --- a/apps/web_v1/src/pages/projects/project-cron.tsx +++ b/apps/web_v1/src/pages/projects/project-cron.tsx @@ -14,7 +14,7 @@ export function ProjectCron({ activeWorkspace, onSelectItem, onCreateCronPlan }:
计划任务 - {project.name} 的定时任务、自动检查和周期性 AI 工作。 + {project.name} 的计划任务元数据与提醒安排;当前不会自主执行 AI 工作。
diff --git a/apps/web_v1/src/pages/projects/project-new-channel.tsx b/apps/web_v1/src/pages/projects/project-new-channel.tsx index 8d97662..8cb4215 100644 --- a/apps/web_v1/src/pages/projects/project-new-channel.tsx +++ b/apps/web_v1/src/pages/projects/project-new-channel.tsx @@ -1,9 +1,7 @@ -import { Button, Card, Input, Select, Space, Tag, Typography } from '@arco-design/web-react' -import { IconApps, IconLink, IconPlus } from '@arco-design/web-react/icon' +import { Card, Empty, Typography } from '@arco-design/web-react' import type { ProjectWorkspace } from './project-types' const { Title, Text } = Typography -const { TextArea } = Input export function ProjectNewChannel({ activeWorkspace }: { activeWorkspace: ProjectWorkspace }) { return ( @@ -11,51 +9,11 @@ export function ProjectNewChannel({ activeWorkspace }: { activeWorkspace: Projec
新建频道 - {activeWorkspace.project.name} 的频道入口配置,用于接入外部页面、资料集合或项目流程。 + {activeWorkspace.project.name} 暂不支持自定义频道或外部链接接入。
-
- - - - - - - -