feat: 优化前端体验,修复数据存储和文档问题

This commit is contained in:
ViperEkura 2026-03-24 23:17:02 +08:00
parent 4540bb9f41
commit dc70a4a1f2
8 changed files with 89 additions and 64 deletions

View File

@ -29,13 +29,13 @@ db_host: localhost
db_port: 3306 db_port: 3306
db_user: root db_user: root
db_password: "" db_password: ""
db_name: glm_chat db_name: nano_claw
``` ```
### 3. 初始化数据库 ### 3. 初始化数据库
```bash ```bash
mysql -u root -p -e "CREATE DATABASE glm_chat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p -e "CREATE DATABASE nano_claw CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
``` ```

View File

@ -1,4 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy.dialects.mysql import LONGTEXT
from . import db from . import db
@ -45,7 +46,7 @@ class Message(db.Model):
thinking_content = db.Column(db.Text, default="") thinking_content = db.Column(db.Text, default="")
# Tool call support # Tool call support
tool_calls = db.Column(db.Text) # JSON string: tool call requests (assistant messages) tool_calls = db.Column(LONGTEXT) # JSON string: tool call requests (assistant messages)
tool_call_id = db.Column(db.String(64)) # Tool call ID (tool messages) tool_call_id = db.Column(db.String(64)) # Tool call ID (tool messages)
name = db.Column(db.String(64)) # Tool name (tool messages) name = db.Column(db.String(64)) # Tool name (tool messages)

View File

@ -349,7 +349,13 @@ GET /api/models
```json ```json
{ {
"code": 0, "code": 0,
"data": ["glm-5", "glm-4", "glm-3-turbo"] "data": [
{"id": "glm-5", "name": "GLM-5"},
{"id": "glm-5-turbo", "name": "GLM-5 Turbo"},
{"id": "glm-4.5", "name": "GLM-4.5"},
{"id": "glm-4.6", "name": "GLM-4.6"},
{"id": "glm-4.7", "name": "GLM-4.7"}
]
} }
``` ```

View File

@ -361,7 +361,7 @@ classDiagram
class SearchService: class SearchService:
"""搜索服务""" """搜索服务"""
def __init__(self, engine=None): def __init__(self, engine=None):
from duckduckgo_search import DDGS from ddgs import DDGS
self.engine = engine or DDGS() self.engine = engine or DDGS()
def search(self, query: str, max_results: int = 5) -> list: def search(self, query: str, max_results: int = 5) -> list:
@ -431,7 +431,7 @@ from .factory import tool
def init_tools(): def init_tools():
"""初始化所有内置工具""" """初始化所有内置工具"""
# 导入即自动注册 # 导入即自动注册
from .builtin import crawler, data, file_ops from .builtin import crawler, data, weather
# 使用时 # 使用时
init_tools() init_tools()
@ -446,10 +446,10 @@ init_tools()
| crawler | `web_search` | 网页搜索 | SearchService | | crawler | `web_search` | 网页搜索 | SearchService |
| crawler | `fetch_page` | 单页抓取 | FetchService | | crawler | `fetch_page` | 单页抓取 | FetchService |
| crawler | `crawl_batch` | 批量爬取 | FetchService | | crawler | `crawl_batch` | 批量爬取 | FetchService |
| data | `calculator` | 数学计算 | - | | data | `calculator` | 数学计算 | CalculatorService |
| data | `data_analysis` | 数据分析 | - | | data | `text_process` | 文本处理 | - |
| file | `file_reader` | 文件读取 | - | | data | `json_process` | JSON处理 | - |
| file | `file_writer` | 文件写入 | - | | weather | `get_weather` | 天气查询 | - (模拟数据) |
--- ---

View File

@ -1,5 +1,8 @@
const BASE = '/api' const BASE = '/api'
// Cache for models list
let modelsCache = null
async function request(url, options = {}) { async function request(url, options = {}) {
const res = await fetch(`${BASE}${url}`, { const res = await fetch(`${BASE}${url}`, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -17,6 +20,34 @@ export const modelApi = {
list() { list() {
return request('/models') return request('/models')
}, },
// Get cached models or fetch from server
async getCached() {
if (modelsCache) {
return { data: modelsCache }
}
// Try localStorage cache first
const cached = localStorage.getItem('models_cache')
if (cached) {
try {
modelsCache = JSON.parse(cached)
return { data: modelsCache }
} catch (_) {}
}
// Fetch from server
const res = await this.list()
modelsCache = res.data
localStorage.setItem('models_cache', JSON.stringify(modelsCache))
return res
},
// Clear cache (e.g., when models changed on server)
clearCache() {
modelsCache = null
localStorage.removeItem('models_cache')
}
} }
export const statsApi = { export const statsApi = {

View File

@ -290,7 +290,8 @@ defineExpose({ scrollToBottom })
.message-bubble { .message-bubble {
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 16px 0; padding: 0;
margin-bottom: 16px;
} }
.message-bubble .avatar { .message-bubble .avatar {
@ -312,6 +313,11 @@ defineExpose({ scrollToBottom })
flex: 1; flex: 1;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
padding: 16px;
border: 1px solid var(--border-light);
border-radius: 12px;
background: var(--bg-primary);
transition: background 0.2s, border-color 0.2s;
} }
.streaming-content { .streaming-content {

View File

@ -70,7 +70,8 @@ function copyContent() {
.message-bubble { .message-bubble {
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 16px 0; padding: 0;
margin-bottom: 16px;
} }
.message-bubble.user { .message-bubble.user {
@ -108,6 +109,11 @@ function copyContent() {
.message-body { .message-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 16px;
border: 1px solid var(--border-light);
border-radius: 12px;
background: var(--bg-primary);
transition: background 0.2s, border-color 0.2s;
} }
.message-content { .message-content {

View File

@ -97,11 +97,6 @@
</div> </div>
</div> </div>
<div class="settings-footer">
<button class="btn-cancel" @click="$emit('close')">取消</button>
<button class="btn-save" @click="save">保存</button>
</div>
<div class="settings-stats"> <div class="settings-stats">
<StatsPanel /> <StatsPanel />
</div> </div>
@ -112,7 +107,7 @@
<script setup> <script setup>
import { reactive, ref, watch, onMounted } from 'vue' import { reactive, ref, watch, onMounted } from 'vue'
import { modelApi } from '../api' import { modelApi, conversationApi } from '../api'
import { useTheme } from '../composables/useTheme' import { useTheme } from '../composables/useTheme'
import StatsPanel from './StatsPanel.vue' import StatsPanel from './StatsPanel.vue'
@ -137,15 +132,15 @@ const form = reactive({
async function loadModels() { async function loadModels() {
try { try {
const res = await modelApi.list() const res = await modelApi.getCached()
models.value = res.data || [] models.value = res.data || []
} catch (e) { } catch (e) {
console.error('Failed to load models:', e) console.error('Failed to load models:', e)
} }
} }
watch(() => props.visible, (val) => { function syncFormFromConversation() {
if (val && props.conversation) { if (props.conversation) {
form.title = props.conversation.title || '' form.title = props.conversation.title || ''
form.model = props.conversation.model || '' form.model = props.conversation.model || ''
form.system_prompt = props.conversation.system_prompt || '' form.system_prompt = props.conversation.system_prompt || ''
@ -153,14 +148,33 @@ watch(() => props.visible, (val) => {
form.max_tokens = props.conversation.max_tokens ?? 65536 form.max_tokens = props.conversation.max_tokens ?? 65536
form.thinking_enabled = props.conversation.thinking_enabled ?? false form.thinking_enabled = props.conversation.thinking_enabled ?? false
} }
}
// Sync form when panel opens
watch(() => props.visible, (visible) => {
if (visible) {
syncFormFromConversation()
}
}) })
onMounted(loadModels) // Auto-save with debounce when form changes
watch(form, () => {
if (props.visible && props.conversation) {
saveChanges()
}
}, { deep: true })
function save() { async function saveChanges() {
emit('save', { ...form }) if (!props.conversation) return
emit('close') try {
const res = await conversationApi.update(props.conversation.id, { ...form })
emit('save', res.data)
} catch (e) {
console.error('Failed to save settings:', e)
}
} }
onMounted(loadModels)
</script> </script>
<style scoped> <style scoped>
@ -358,45 +372,6 @@ function save() {
margin: 24px 0; margin: 24px 0;
} }
.settings-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-light);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn-cancel {
padding: 8px 20px;
border-radius: 8px;
border: 1px solid var(--border-medium);
background: none;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 0.15s;
}
.btn-cancel:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.btn-save {
padding: 8px 20px;
border-radius: 8px;
border: none;
background: var(--accent-primary);
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.15s;
}
.btn-save:hover {
background: var(--accent-primary-hover);
}
.settings-stats { .settings-stats {
padding: 16px 24px 24px; padding: 16px 24px 24px;
border-top: 1px solid var(--border-light); border-top: 1px solid var(--border-light);