feat: 优化前端体验,修复数据存储和文档问题
This commit is contained in:
parent
4540bb9f41
commit
dc70a4a1f2
|
|
@ -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;"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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` | 天气查询 | - (模拟数据) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue