feat: 优化前端体验,修复数据存储和文档问题
This commit is contained in:
parent
4540bb9f41
commit
dc70a4a1f2
|
|
@ -29,13 +29,13 @@ db_host: localhost
|
|||
db_port: 3306
|
||||
db_user: root
|
||||
db_password: ""
|
||||
db_name: glm_chat
|
||||
db_name: nano_claw
|
||||
```
|
||||
|
||||
### 3. 初始化数据库
|
||||
|
||||
```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;"
|
||||
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime, timezone
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from . import db
|
||||
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ class Message(db.Model):
|
|||
thinking_content = db.Column(db.Text, default="")
|
||||
|
||||
# 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)
|
||||
name = db.Column(db.String(64)) # Tool name (tool messages)
|
||||
|
||||
|
|
|
|||
|
|
@ -349,7 +349,13 @@ GET /api/models
|
|||
```json
|
||||
{
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ classDiagram
|
|||
class SearchService:
|
||||
"""搜索服务"""
|
||||
def __init__(self, engine=None):
|
||||
from duckduckgo_search import DDGS
|
||||
from ddgs import DDGS
|
||||
self.engine = engine or DDGS()
|
||||
|
||||
def search(self, query: str, max_results: int = 5) -> list:
|
||||
|
|
@ -431,7 +431,7 @@ from .factory import tool
|
|||
def init_tools():
|
||||
"""初始化所有内置工具"""
|
||||
# 导入即自动注册
|
||||
from .builtin import crawler, data, file_ops
|
||||
from .builtin import crawler, data, weather
|
||||
|
||||
# 使用时
|
||||
init_tools()
|
||||
|
|
@ -446,10 +446,10 @@ init_tools()
|
|||
| crawler | `web_search` | 网页搜索 | SearchService |
|
||||
| crawler | `fetch_page` | 单页抓取 | FetchService |
|
||||
| crawler | `crawl_batch` | 批量爬取 | FetchService |
|
||||
| data | `calculator` | 数学计算 | - |
|
||||
| data | `data_analysis` | 数据分析 | - |
|
||||
| file | `file_reader` | 文件读取 | - |
|
||||
| file | `file_writer` | 文件写入 | - |
|
||||
| data | `calculator` | 数学计算 | CalculatorService |
|
||||
| data | `text_process` | 文本处理 | - |
|
||||
| data | `json_process` | JSON处理 | - |
|
||||
| weather | `get_weather` | 天气查询 | - (模拟数据) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
const BASE = '/api'
|
||||
|
||||
// Cache for models list
|
||||
let modelsCache = null
|
||||
|
||||
async function request(url, options = {}) {
|
||||
const res = await fetch(`${BASE}${url}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -17,6 +20,34 @@ export const modelApi = {
|
|||
list() {
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -290,7 +290,8 @@ defineExpose({ scrollToBottom })
|
|||
.message-bubble {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 0;
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.message-bubble .avatar {
|
||||
|
|
@ -312,6 +313,11 @@ defineExpose({ scrollToBottom })
|
|||
flex: 1;
|
||||
min-width: 0;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ function copyContent() {
|
|||
.message-bubble {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 0;
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.message-bubble.user {
|
||||
|
|
@ -108,6 +109,11 @@ function copyContent() {
|
|||
.message-body {
|
||||
flex: 1;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -97,11 +97,6 @@
|
|||
</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">
|
||||
<StatsPanel />
|
||||
</div>
|
||||
|
|
@ -112,7 +107,7 @@
|
|||
|
||||
<script setup>
|
||||
import { reactive, ref, watch, onMounted } from 'vue'
|
||||
import { modelApi } from '../api'
|
||||
import { modelApi, conversationApi } from '../api'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
import StatsPanel from './StatsPanel.vue'
|
||||
|
||||
|
|
@ -137,15 +132,15 @@ const form = reactive({
|
|||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await modelApi.list()
|
||||
const res = await modelApi.getCached()
|
||||
models.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load models:', e)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val && props.conversation) {
|
||||
function syncFormFromConversation() {
|
||||
if (props.conversation) {
|
||||
form.title = props.conversation.title || ''
|
||||
form.model = props.conversation.model || ''
|
||||
form.system_prompt = props.conversation.system_prompt || ''
|
||||
|
|
@ -153,14 +148,33 @@ watch(() => props.visible, (val) => {
|
|||
form.max_tokens = props.conversation.max_tokens ?? 65536
|
||||
form.thinking_enabled = props.conversation.thinking_enabled ?? false
|
||||
}
|
||||
}
|
||||
|
||||
// Sync form when panel opens
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
syncFormFromConversation()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(loadModels)
|
||||
|
||||
function save() {
|
||||
emit('save', { ...form })
|
||||
emit('close')
|
||||
// Auto-save with debounce when form changes
|
||||
watch(form, () => {
|
||||
if (props.visible && props.conversation) {
|
||||
saveChanges()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
async function saveChanges() {
|
||||
if (!props.conversation) return
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -358,45 +372,6 @@ function save() {
|
|||
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 {
|
||||
padding: 16px 24px 24px;
|
||||
border-top: 1px solid var(--border-light);
|
||||
|
|
|
|||
Loading…
Reference in New Issue