Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b67bbe885 | |||
| b98ab79ae2 | |||
| 0d88d22509 | |||
| 7c8adc0d78 |
@@ -54,6 +54,8 @@ def init_db():
|
||||
model TEXT NOT NULL,
|
||||
max_tokens INTEGER DEFAULT 2048,
|
||||
temperature REAL DEFAULT 0.7,
|
||||
enable_thinking INTEGER DEFAULT 0,
|
||||
enable_vision INTEGER DEFAULT 0,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -221,6 +223,14 @@ def init_db():
|
||||
'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', 10, 1)
|
||||
''')
|
||||
|
||||
# 检测并添加 llm_configs 新字段(兼容旧数据库)
|
||||
cursor.execute("PRAGMA table_info(llm_configs)")
|
||||
llm_columns = [col[1] for col in cursor.fetchall()]
|
||||
if 'enable_thinking' not in llm_columns:
|
||||
cursor.execute("ALTER TABLE llm_configs ADD COLUMN enable_thinking INTEGER DEFAULT 0")
|
||||
if 'enable_vision' not in llm_columns:
|
||||
cursor.execute("ALTER TABLE llm_configs ADD COLUMN enable_vision INTEGER DEFAULT 0")
|
||||
|
||||
# 初始化默认对话配置
|
||||
cursor.execute('SELECT COUNT(*) FROM chat_configs')
|
||||
if cursor.fetchone()[0] == 0:
|
||||
@@ -1005,10 +1015,11 @@ def add_llm_config():
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature, enable_thinking, enable_vision)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
|
||||
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7)))
|
||||
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7),
|
||||
data.get('enable_thinking', 0), data.get('enable_vision', 0)))
|
||||
conn.commit()
|
||||
config_id = cursor.lastrowid
|
||||
conn.close()
|
||||
@@ -1023,9 +1034,10 @@ def update_llm_config(id):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
UPDATE llm_configs SET name=?, provider=?, api_url=?, api_key=?, model=?,
|
||||
max_tokens=?, temperature=?, updated_at=CURRENT_TIMESTAMP WHERE id=?
|
||||
max_tokens=?, temperature=?, enable_thinking=?, enable_vision=?, updated_at=CURRENT_TIMESTAMP WHERE id=?
|
||||
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
|
||||
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7), id))
|
||||
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7),
|
||||
data.get('enable_thinking', 0), data.get('enable_vision', 0), id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True})
|
||||
|
||||
@@ -401,6 +401,54 @@
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 表单行(双列布局) */
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group-half {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 表单提示 */
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 开关按钮 */
|
||||
.toggle-switch {
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
background: #ccc;
|
||||
border-radius: 13px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: left 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.active .toggle-slider {
|
||||
left: 26px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
66
www/admin.js
66
www/admin.js
@@ -559,7 +559,8 @@ async function loadLLMPage(content) {
|
||||
<th>名称</th>
|
||||
<th>提供商</th>
|
||||
<th>模型</th>
|
||||
<th>API URL</th>
|
||||
<th>思考</th>
|
||||
<th>视觉</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@@ -570,7 +571,8 @@ async function loadLLMPage(content) {
|
||||
<td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
|
||||
<td>${c.provider}</td>
|
||||
<td>${c.model}</td>
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td>
|
||||
<td>${c.enable_thinking ? '🧠 支持' : '—'}</td>
|
||||
<td>${c.enable_vision ? '👁️ 支持' : '—'}</td>
|
||||
<td>${c.is_active ? '✅ 启用' : '❌ 禁用'}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
@@ -622,8 +624,28 @@ function showAddLLMModal() {
|
||||
<label class="form-label">Temperature</label>
|
||||
<input type="number" class="form-input" id="llmTemperature" value="0.7" step="0.1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group form-group-half">
|
||||
<label class="form-label">思考模式 🧠</label>
|
||||
<div class="toggle-switch" id="llmThinkingToggle" data-value="0">
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<div class="form-tip">支持深度思考/推理能力</div>
|
||||
</div>
|
||||
<div class="form-group form-group-half">
|
||||
<label class="form-label">视觉能力 👁️</label>
|
||||
<div class="toggle-switch" id="llmVisionToggle" data-value="0">
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<div class="form-tip">支持图片输入/多模态</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="form-submit" onclick="saveLLM()">保存</button>
|
||||
`);
|
||||
|
||||
// 绑定开关事件
|
||||
bindToggleSwitch('llmThinkingToggle');
|
||||
bindToggleSwitch('llmVisionToggle');
|
||||
}
|
||||
|
||||
function showEditLLMModal(id) {
|
||||
@@ -664,8 +686,28 @@ function showEditLLMModal(id) {
|
||||
<label class="form-label">Temperature</label>
|
||||
<input type="number" class="form-input" id="llmTemperature" value="${config.temperature}" step="0.1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group form-group-half">
|
||||
<label class="form-label">思考模式 🧠</label>
|
||||
<div class="toggle-switch ${config.enable_thinking ? 'active' : ''}" id="llmThinkingToggle" data-value="${config.enable_thinking || 0}">
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<div class="form-tip">支持深度思考/推理能力</div>
|
||||
</div>
|
||||
<div class="form-group form-group-half">
|
||||
<label class="form-label">视觉能力 👁️</label>
|
||||
<div class="toggle-switch ${config.enable_vision ? 'active' : ''}" id="llmVisionToggle" data-value="${config.enable_vision || 0}">
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<div class="form-tip">支持图片输入/多模态</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="form-submit" onclick="updateLLM(${id})">保存</button>
|
||||
`);
|
||||
|
||||
// 绑定开关事件
|
||||
bindToggleSwitch('llmThinkingToggle');
|
||||
bindToggleSwitch('llmVisionToggle');
|
||||
}
|
||||
|
||||
async function saveLLM() {
|
||||
@@ -676,7 +718,9 @@ async function saveLLM() {
|
||||
api_key: document.getElementById('llmApiKey').value,
|
||||
model: document.getElementById('llmModel').value,
|
||||
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
|
||||
temperature: parseFloat(document.getElementById('llmTemperature').value)
|
||||
temperature: parseFloat(document.getElementById('llmTemperature').value),
|
||||
enable_thinking: parseInt(document.getElementById('llmThinkingToggle')?.dataset.value || 0),
|
||||
enable_vision: parseInt(document.getElementById('llmVisionToggle')?.dataset.value || 0)
|
||||
};
|
||||
|
||||
if (!data.name || !data.api_url || !data.api_key || !data.model) {
|
||||
@@ -698,7 +742,9 @@ async function updateLLM(id) {
|
||||
api_key: document.getElementById('llmApiKey').value,
|
||||
model: document.getElementById('llmModel').value,
|
||||
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
|
||||
temperature: parseFloat(document.getElementById('llmTemperature').value)
|
||||
temperature: parseFloat(document.getElementById('llmTemperature').value),
|
||||
enable_thinking: parseInt(document.getElementById('llmThinkingToggle')?.dataset.value || 0),
|
||||
enable_vision: parseInt(document.getElementById('llmVisionToggle')?.dataset.value || 0)
|
||||
};
|
||||
|
||||
await fetchAPI(`/api/admin/llm/${id}`, 'PUT', data);
|
||||
@@ -1512,6 +1558,18 @@ function closeModal() {
|
||||
document.getElementById('modal').classList.remove('show');
|
||||
}
|
||||
|
||||
function bindToggleSwitch(id) {
|
||||
const toggle = document.getElementById(id);
|
||||
if (!toggle) return;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const current = parseInt(toggle.dataset.value) || 0;
|
||||
const newValue = current === 0 ? 1 : 0;
|
||||
toggle.dataset.value = newValue;
|
||||
toggle.classList.toggle('active', newValue === 1);
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
|
||||
149
www/app.js
149
www/app.js
@@ -25,6 +25,9 @@ let systemAgents = [];
|
||||
// 用户智能体界面显示的智能体
|
||||
let agents = [];
|
||||
|
||||
// 上传的文件(临时存储)
|
||||
let uploadedFile = null; // { name: string, content: string }
|
||||
|
||||
// 用户添加的智能体(按类别分组)
|
||||
let myAgents = {
|
||||
basic: [],
|
||||
@@ -150,6 +153,8 @@ async function loadBackendConfig() {
|
||||
CONFIG.apiKey = backendConfig.llm.api_key;
|
||||
CONFIG.model = backendConfig.llm.model;
|
||||
CONFIG.maxTokens = backendConfig.llm.max_tokens || 2048;
|
||||
CONFIG.enableThinking = backendConfig.llm.enable_thinking || 0;
|
||||
CONFIG.enableVision = backendConfig.llm.enable_vision || 0;
|
||||
}
|
||||
|
||||
updateAgentsDisplay();
|
||||
@@ -3234,10 +3239,12 @@ function showAgentChatPage() {
|
||||
<!-- 上传选项弹窗 -->
|
||||
<div class="attach-panel" id="attachPanel">
|
||||
<div class="attach-panel-content">
|
||||
${CONFIG.enableVision ? `
|
||||
<div class="attach-item" data-type="image">
|
||||
<div class="attach-icon">📷</div>
|
||||
<div class="attach-label">上传图片</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="attach-item" data-type="file">
|
||||
<div class="attach-icon">📄</div>
|
||||
<div class="attach-label">上传文件</div>
|
||||
@@ -3245,7 +3252,7 @@ function showAgentChatPage() {
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="imageInput" accept="image/*" style="display:none">
|
||||
<input type="file" id="fileInput" accept=".txt,.md,.pdf,.doc,.docx,.json,.csv" style="display:none">
|
||||
<input type="file" id="fileInput" accept=".txt,.md,.json,.csv,.xml,.yaml,.yml,.log,.sql,.html,.css,.js,.py,.java,.c,.cpp,.go,.rs,.sh,.bash,.ini,.conf,.cfg" style="display:none">
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar" id="searchBar">
|
||||
@@ -3763,10 +3770,12 @@ function openConversation(id) {
|
||||
<!-- 上传选项弹窗 -->
|
||||
<div class="attach-panel" id="attachPanel">
|
||||
<div class="attach-panel-content">
|
||||
${CONFIG.enableVision ? `
|
||||
<div class="attach-item" data-type="image">
|
||||
<div class="attach-icon">📷</div>
|
||||
<div class="attach-label">上传图片</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="attach-item" data-type="file">
|
||||
<div class="attach-icon">📄</div>
|
||||
<div class="attach-label">上传文件</div>
|
||||
@@ -3774,7 +3783,7 @@ function openConversation(id) {
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="imageInput" accept="image/*" style="display:none">
|
||||
<input type="file" id="fileInput" accept=".txt,.md,.pdf,.doc,.docx,.json,.csv" style="display:none">
|
||||
<input type="file" id="fileInput" accept=".txt,.md,.json,.csv,.xml,.yaml,.yml,.log,.sql,.html,.css,.js,.py,.java,.c,.cpp,.go,.rs,.sh,.bash,.ini,.conf,.cfg" style="display:none">
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3991,8 +4000,26 @@ async function sendMessage() {
|
||||
// 隐藏欢迎界面
|
||||
welcome.style.display = 'none';
|
||||
|
||||
// 添加用户消息
|
||||
currentConversation.messages.push({ role: 'user', content: text });
|
||||
// 构建用户消息内容
|
||||
let userContent = text;
|
||||
let fileContent = null;
|
||||
|
||||
// 如果有上传的文件,附加文件内容
|
||||
if (uploadedFile) {
|
||||
fileContent = uploadedFile;
|
||||
userContent = text; // 用户输入的问题
|
||||
|
||||
// 发送后清除上传文件标记
|
||||
uploadedFile = null;
|
||||
const fileTagArea = document.getElementById('fileTagArea');
|
||||
if (fileTagArea) fileTagArea.innerHTML = '';
|
||||
}
|
||||
|
||||
// 添加用户消息(界面显示)
|
||||
const displayContent = fileContent
|
||||
? `${text}\n\n📎 已上传文件: ${fileContent.name}`
|
||||
: text;
|
||||
currentConversation.messages.push({ role: 'user', content: displayContent });
|
||||
|
||||
// 更新对话标题(第一条用户消息)
|
||||
if (currentConversation.title === '新对话') {
|
||||
@@ -4027,11 +4054,11 @@ async function sendMessage() {
|
||||
autoResize(userInput);
|
||||
|
||||
// 调用流式生成
|
||||
await streamGenerate(currentConversation.messages.length - 1);
|
||||
await streamGenerate(currentConversation.messages.length - 1, fileContent);
|
||||
}
|
||||
|
||||
// 流式生成 AI 回复
|
||||
async function streamGenerate(userMsgIndex) {
|
||||
async function streamGenerate(userMsgIndex, fileContent = null) {
|
||||
isLoading = true;
|
||||
sendBtn.disabled = true;
|
||||
|
||||
@@ -4076,6 +4103,14 @@ async function streamGenerate(userMsgIndex) {
|
||||
content: m.content
|
||||
}));
|
||||
|
||||
// 如果有上传的文件,添加文件内容作为系统消息
|
||||
if (fileContent) {
|
||||
messagesToSend.push({
|
||||
role: 'system',
|
||||
content: `用户上传了文件 "${fileContent.name}",以下是文件内容:\n\n${fileContent.content}`
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有搜索结果,将搜索内容添加到消息中
|
||||
if (searchResults) {
|
||||
const searchContext = formatSearchResultsForLLM(searchResults);
|
||||
@@ -5293,39 +5328,93 @@ async function handleFileUpload(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
const content = event.target.result;
|
||||
const fileName = file.name;
|
||||
|
||||
// 添加用户消息
|
||||
currentConversation.messages.push({
|
||||
role: 'user',
|
||||
content: `[文件: ${fileName}]\\n\\n${content.slice(0, 500)}${content.length > 500 ? '...' : ''}`
|
||||
});
|
||||
|
||||
currentConversation.updatedAt = Date.now();
|
||||
saveConversations();
|
||||
renderMessages();
|
||||
|
||||
if (welcome) welcome.style.display = 'none';
|
||||
|
||||
// 调用AI生成
|
||||
await streamGenerateWithFile(content, fileName);
|
||||
};
|
||||
// 检查文件类型(只支持文本文件)
|
||||
const allowedExtensions = ['.txt', '.md', '.json', '.csv', '.xml', '.yaml', '.yml', '.log', '.sql', '.html', '.css', '.js', '.py', '.java', '.c', '.cpp', '.go', '.rs', '.sh', '.bash', '.ini', '.conf', '.cfg'];
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isAllowed = allowedExtensions.some(ext => fileName.endsWith(ext));
|
||||
|
||||
// 根据文件类型读取
|
||||
if (file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) {
|
||||
// PDF/Word文件暂时只显示文件名
|
||||
showToast('PDF/Word文件暂不支持解析,请上传文本文件');
|
||||
if (!isAllowed) {
|
||||
showToast('只支持文本类型的文件(txt、md、json、代码文件等)');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小(限制约 10KB,大约 10000 字符)
|
||||
const maxSizeKB = 15; // 留一点余量
|
||||
if (file.size > maxSizeKB * 1024) {
|
||||
showToast(`文件太大,请上传小于 ${maxSizeKB}KB 的文本文件`);
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('正在读取文件...');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
let content = event.target.result;
|
||||
|
||||
// 限制最多 10000 字符
|
||||
if (content.length > 10000) {
|
||||
content = content.slice(0, 10000);
|
||||
}
|
||||
|
||||
// 存储上传的文件
|
||||
uploadedFile = {
|
||||
name: file.name,
|
||||
content: content
|
||||
};
|
||||
|
||||
// 显示文件标记
|
||||
showUploadedFileTag();
|
||||
|
||||
showToast(`已上传: ${file.name}`);
|
||||
userInput.focus();
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
showToast('文件读取失败');
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
// 显示上传文件标记
|
||||
function showUploadedFileTag() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
// 查找或创建文件标记区域
|
||||
let fileTagArea = document.getElementById('fileTagArea');
|
||||
if (!fileTagArea) {
|
||||
fileTagArea = document.createElement('div');
|
||||
fileTagArea.id = 'fileTagArea';
|
||||
fileTagArea.className = 'file-tag-area';
|
||||
// 插入到输入区域前面
|
||||
const inputArea = document.querySelector('.input-area');
|
||||
if (inputArea) {
|
||||
inputArea.insertBefore(fileTagArea, inputArea.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
fileTagArea.innerHTML = `
|
||||
<div class="file-tag">
|
||||
<span class="file-tag-icon">📄</span>
|
||||
<span class="file-tag-name">${uploadedFile.name}</span>
|
||||
<button class="file-tag-remove" onclick="removeUploadedFile()">✕</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 移除上传的文件
|
||||
function removeUploadedFile() {
|
||||
uploadedFile = null;
|
||||
const fileTagArea = document.getElementById('fileTagArea');
|
||||
if (fileTagArea) {
|
||||
fileTagArea.innerHTML = '';
|
||||
}
|
||||
showToast('已移除文件');
|
||||
}
|
||||
|
||||
// 带图片的流式生成
|
||||
async function streamGenerateWithImage(base64, imageName) {
|
||||
isLoading = true;
|
||||
|
||||
@@ -3198,6 +3198,50 @@ body {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* 文件标记区域 */
|
||||
.file-tag-area {
|
||||
padding: 8px 12px;
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file-tag-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-tag-name {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-tag-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.file-tag-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#userInput {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
|
||||
Reference in New Issue
Block a user