From a84b8617a6055bb4cfd8de44c107d99e3cba83de Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Mon, 13 Apr 2026 08:38:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=88=AC=E8=99=AB?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- asserts/ARCHITECTURE.md | 11 +- config.yaml | 2 +- dashboard/src/components/MessageBubble.vue | 4 +- dashboard/src/components/ProcessBlock.vue | 33 ++- dashboard/src/style.css | 4 + dashboard/src/utils/markdown.js | 4 +- .../src/views/ConversationDetailView.vue | 16 +- dashboard/src/views/SettingsView.vue | 11 +- luxx/models.py | 22 +- luxx/routes/messages.py | 6 +- luxx/services/chat.py | 14 +- luxx/tools/builtin/crawler.py | 207 ++++++--------- luxx/tools/executor.py | 4 +- luxx/tools/services.py | 247 ++++++++++++++++++ pyproject.toml | 7 +- luxx/run.py => run.py | 0 17 files changed, 422 insertions(+), 172 deletions(-) create mode 100644 luxx/tools/services.py rename luxx/run.py => run.py (100%) diff --git a/.gitignore b/.gitignore index 4c91ea5..06b7103 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ !README.md !.gitignore -!luxx/**/*.py +!*.py !asserts/**/*.md # Dashboard diff --git a/asserts/ARCHITECTURE.md b/asserts/ARCHITECTURE.md index e9dfc60..eeffd90 100644 --- a/asserts/ARCHITECTURE.md +++ b/asserts/ARCHITECTURE.md @@ -5,9 +5,13 @@ - **框架**: FastAPI 0.109+ - **数据库**: SQLAlchemy 2.0+ - **认证**: JWT (PyJWT) -- **HTTP客户端**: httpx +- **HTTP客户端**: httpx, requests - **配置**: YAML (PyYAML) - **代码执行**: Python 原生执行 +- **网页爬虫**: + - `httpx` - HTTP 客户端 + - `beautifulsoup4` - HTML 解析 + - `lxml` - XML/HTML 解析器 ## 目录结构 @@ -36,6 +40,7 @@ luxx/ │ ├── crawler.py # 网页爬虫 │ ├── data.py # 数据处理 │ └── weather.py # 天气查询 +│ └── services.py # 工具服务层 └── utils/ # 工具函数 └── helpers.py ``` @@ -205,7 +210,9 @@ classDiagram |------|------|------| | `python_execute` | 执行 Python 代码 | 支持 print 输出、变量访问 | | `python_eval` | 计算表达式 | 快速求值 | -| `web_crawl` | 网页抓取 | BeautifulSoup + httpx | +| `web_search` | DuckDuckGo HTML | DuckDuckGo HTML 搜索 | +| `web_fetch` | 网页抓取 | httpx + BeautifulSoup,支持 text/links/structured | +| `batch_fetch` | 批量抓取 | 并发获取多个页面 | | `get_weather` | 天气查询 | 支持城市名查询 | | `process_data` | 数据处理 | JSON 转换、格式化等 | diff --git a/config.yaml b/config.yaml index f13c4e2..b83a005 100644 --- a/config.yaml +++ b/config.yaml @@ -7,7 +7,7 @@ app: database: type: sqlite - url: sqlite:///../chat.db + url: sqlite:///./chat.db llm: provider: deepseek diff --git a/dashboard/src/components/MessageBubble.vue b/dashboard/src/components/MessageBubble.vue index 34addad..95880fc 100644 --- a/dashboard/src/components/MessageBubble.vue +++ b/dashboard/src/components/MessageBubble.vue @@ -66,7 +66,9 @@ const renderedContent = computed(() => { function formatTime(time) { if (!time) return '' - return new Date(time).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + const date = new Date(time) + // 使用本地时区显示 + return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) } function copyContent() { diff --git a/dashboard/src/components/ProcessBlock.vue b/dashboard/src/components/ProcessBlock.vue index 4be9abd..e345c74 100644 --- a/dashboard/src/components/ProcessBlock.vue +++ b/dashboard/src/components/ProcessBlock.vue @@ -9,10 +9,11 @@ 思考中 {{ item.brief || '正在思考...' }} ... + 已截断
-
{{ item.content }}
+
{{ item.displayContent }}
@@ -25,6 +26,7 @@ ... 成功 失败 + 已截断
@@ -34,7 +36,7 @@
结果 -
{{ item.fullResult || item.resultSummary }}
+
{{ item.displayResult }}
@@ -71,12 +73,14 @@ const allItems = computed(() => { if (props.processSteps && props.processSteps.length > 0) { for (const step of props.processSteps) { if (step.type === 'thinking') { + const content = step.content || '' items.push({ key: step.id || `thinking-${step.index}`, type: 'thinking', index: step.index, - content: step.content || '', - brief: step.content ? step.content.slice(0, 50) + (step.content.length > 50 ? '...' : '') : '', + content: content, + displayContent: content.length > 1024 ? content.slice(0, 1024) + '\n\n[... 内容已截断 ...]' : content, + brief: content.slice(0, 50) + (content.length > 50 ? '...' : ''), }) } else if (step.type === 'tool_call') { items.push({ @@ -97,12 +101,15 @@ const allItems = computed(() => { const toolId = step.id_ref || step.id const match = items.findLast(it => it.type === 'tool_call' && it.id === toolId) if (match) { - match.resultSummary = step.content ? step.content.slice(0, 200) : '' - match.fullResult = step.content || '' + const resultContent = step.content || '' + match.resultSummary = resultContent.slice(0, 200) + match.fullResult = resultContent + match.displayResult = resultContent.length > 1024 ? resultContent.slice(0, 1024) + '\n\n[... 结果已截断 ...]' : resultContent match.isSuccess = step.success !== false match.loading = false } else { // 如果没有找到对应的 tool_call,创建一个占位符 + const placeholderContent = step.content || '' items.push({ key: `result-${step.id || step.index}`, type: 'tool_call', @@ -113,8 +120,9 @@ const allItems = computed(() => { brief: step.name || '工具结果', loading: false, isSuccess: true, - resultSummary: step.content ? step.content.slice(0, 200) : '', - fullResult: step.content || '' + resultSummary: placeholderContent.slice(0, 200), + fullResult: placeholderContent, + displayResult: placeholderContent.length > 1024 ? placeholderContent.slice(0, 1024) + '\n\n[... 结果已截断 ...]' : placeholderContent }) } } else if (step.type === 'text') { @@ -280,6 +288,15 @@ const sparkleIcon = ` -
+
加载中...
@@ -106,6 +106,7 @@ const sending = ref(false) const streamingMessage = ref(null) const messagesContainer = ref(null) const textareaRef = ref(null) +const autoScroll = ref(true) const conversationId = ref(route.params.id) const conversationTitle = ref('') @@ -128,6 +129,7 @@ function onKeydown(e) { } const loadMessages = async () => { + autoScroll.value = true loading.value = true try { const res = await messagesAPI.list(conversationId.value) @@ -191,6 +193,7 @@ const sendMessage = async () => { { conversation_id: conversationId.value, content }, { onProcessStep: (step) => { + autoScroll.value = true // 流式开始时启用自动滚动 if (!streamingMessage.value) return // 按 id 更新或追加步骤 const idx = streamingMessage.value.process_steps.findIndex(s => s.id === step.id) @@ -202,6 +205,7 @@ const sendMessage = async () => { }, onDone: () => { // 完成,添加到消息列表 + autoScroll.value = true if (streamingMessage.value) { messages.value.push({ ...streamingMessage.value, @@ -230,6 +234,7 @@ const sendMessage = async () => { } const scrollToBottom = () => { + if (!autoScroll.value) return nextTick(() => { if (messagesContainer.value) { messagesContainer.value.scrollTo({ @@ -240,6 +245,15 @@ const scrollToBottom = () => { }) } +// 处理滚动事件,检测用户是否手动滚动 +const handleScroll = () => { + if (!messagesContainer.value) return + const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value + const distanceToBottom = scrollHeight - scrollTop - clientHeight + // 距离底部超过50px时停止自动跟随 + autoScroll.value = distanceToBottom < 50 +} + // 监听流式消息变化,自动滚动 watch(() => streamingMessage.value?.process_steps?.length, () => { if (streamingMessage.value) { diff --git a/dashboard/src/views/SettingsView.vue b/dashboard/src/views/SettingsView.vue index 6fd3216..1eb5cd5 100644 --- a/dashboard/src/views/SettingsView.vue +++ b/dashboard/src/views/SettingsView.vue @@ -102,6 +102,11 @@
+
+ + + 单次回复最大 token 数,默认 8192 +