feat(webui): settings 页面可编辑工具调用限制与超时时间

- settings.html: 新增「工具调用限制」表单(5个字段)
- settings.html: 新增「消息超时」输入框
- server.py: _handle_set_settings 修复空 api_config 时不清空 API key
- 前后端 format:api_config 和 tool_limits 分开提交,互不影响
This commit is contained in:
root
2026-04-28 20:39:26 +08:00
parent c1dc3f647b
commit 12ab1df7f5
2 changed files with 94 additions and 13 deletions

View File

@ -462,16 +462,16 @@ class BackendServer:
api_config = body.get("api_config", {}) api_config = body.get("api_config", {})
tool_limits = body.get("tool_limits", {}) tool_limits = body.get("tool_limits", {})
api_key = api_config.get("api_key", "") # 仅当 api_config 有值时更新 API 配置(避免单独保存 tool_limits 时清空 API key
base_url = api_config.get("base_url", "https://api.deepseek.com") if api_config:
model = api_config.get("model", "deepseek-v4-flash") api_key = api_config.get("api_key", self._config.get("api_key", ""))
base_url = api_config.get("base_url", self._config.get("base_url", "https://api.deepseek.com"))
self.update_config(api_key, base_url, model) model = api_config.get("model", self._config.get("model", "deepseek-v4-flash"))
self.update_config(api_key, base_url, model)
# 通用配置字段(如 message_timeout # 通用配置字段(如 message_timeout
for key in ["message_timeout"]: for key in ["message_timeout"]:
if key in api_config: if key in api_config:
self._config[key] = int(api_config[key]) self._config[key] = int(api_config[key])
limits_keys = [ limits_keys = [
"persona_update_max", "persona_update_max",

View File

@ -204,10 +204,42 @@
<label for="settings_model">Model</label> <label for="settings_model">Model</label>
<input type="text" id="settings_model" placeholder="deepseek-chat"> <input type="text" id="settings_model" placeholder="deepseek-chat">
</div> </div>
<button type="submit" class="btn" id="saveApiConfigBtn">保 存 API 配 置</button> <div class="form-group">
<label for="settings_timeout">消息超时(秒)</label>
<input type="number" id="settings_timeout" placeholder="1200" min="60" step="60">
</div>
<button type="submit" class="btn" id="saveApiConfigBtn">保 存 配 置</button>
</form> </form>
<div id="apiConfigStatus" style="text-align:center;font-size:12px;color:#666688;margin-top:5px;"></div> <div id="apiConfigStatus" style="text-align:center;font-size:12px;color:#666688;margin-top:5px;"></div>
<!-- 工具调用限制 -->
<div class="section-title"><i class="fas fa-sliders-h"></i> 工具调用限制</div>
<div class="limits-hint" style="color:#7777aa;font-size:12px;margin-bottom:10px;">AI 每轮对话中各类工具的最大调用次数</div>
<form id="toolLimitsForm">
<div class="form-group">
<label for="settings_persona_update">人设更新 <span style="color:#7777aa;font-size:12px;">(persona_update_max)</span></label>
<input type="number" id="settings_persona_update" min="0" max="100">
</div>
<div class="form-group">
<label for="settings_task_update">任务写入 <span style="color:#7777aa;font-size:12px;">(task_update_max)</span></label>
<input type="number" id="settings_task_update" min="1" max="200">
</div>
<div class="form-group">
<label for="settings_task_query">任务查询 <span style="color:#7777aa;font-size:12px;">(task_query_max)</span></label>
<input type="number" id="settings_task_query" min="1" max="200">
</div>
<div class="form-group">
<label for="settings_memory_query">记忆查询 <span style="color:#7777aa;font-size:12px;">(memory_query_max)</span></label>
<input type="number" id="settings_memory_query" min="1" max="200">
</div>
<div class="form-group">
<label for="settings_memory_update">记忆写入 <span style="color:#7777aa;font-size:12px;">(memory_update_max)</span></label>
<input type="number" id="settings_memory_update" min="1" max="200">
</div>
<button type="submit" class="btn" id="saveLimitsBtn">保 存 限 制</button>
</form>
<div id="limitsStatus" style="text-align:center;font-size:12px;color:#666688;margin-top:5px;"></div>
<!-- 修改密码 --> <!-- 修改密码 -->
<div class="section-title"><i class="fas fa-key"></i> 修改密码</div> <div class="section-title"><i class="fas fa-key"></i> 修改密码</div>
<form id="passwordForm"> <form id="passwordForm">
@ -477,9 +509,18 @@
const data = await resp.json(); const data = await resp.json();
if (data.success && data.data) { if (data.success && data.data) {
const cfg = data.data.api_config || {}; const cfg = data.data.api_config || {};
const limits = data.data.tool_limits || {};
// API 配置
document.getElementById('settings_api_key').value = cfg.api_key || ''; document.getElementById('settings_api_key').value = cfg.api_key || '';
document.getElementById('settings_base_url').value = cfg.base_url || 'https://api.deepseek.com'; document.getElementById('settings_base_url').value = cfg.base_url || 'https://api.deepseek.com';
document.getElementById('settings_model').value = cfg.model || 'deepseek-chat'; document.getElementById('settings_model').value = cfg.model || 'deepseek-chat';
document.getElementById('settings_timeout').value = cfg.message_timeout || 1200;
// 工具限制
document.getElementById('settings_persona_update').value = limits.persona_update_max || 1;
document.getElementById('settings_task_update').value = limits.task_update_max || 20;
document.getElementById('settings_task_query').value = limits.task_query_max || 30;
document.getElementById('settings_memory_query').value = limits.memory_query_max || 30;
document.getElementById('settings_memory_update').value = limits.memory_update_max || 15;
document.getElementById('apiConfigStatus').textContent = '✅ 配置已加载'; document.getElementById('apiConfigStatus').textContent = '✅ 配置已加载';
} }
} catch (e) { } catch (e) {
@ -487,6 +528,7 @@
} }
} }
// 保存 API 配置(含超时)
document.getElementById('apiConfigForm').addEventListener('submit', async (e) => { document.getElementById('apiConfigForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const btn = document.getElementById('saveApiConfigBtn'); const btn = document.getElementById('saveApiConfigBtn');
@ -503,7 +545,8 @@
api_config: { api_config: {
api_key: document.getElementById('settings_api_key').value, api_key: document.getElementById('settings_api_key').value,
base_url: document.getElementById('settings_base_url').value, base_url: document.getElementById('settings_base_url').value,
model: document.getElementById('settings_model').value model: document.getElementById('settings_model').value,
message_timeout: parseInt(document.getElementById('settings_timeout').value) || 1200
} }
}) })
}); });
@ -518,7 +561,45 @@
statusEl.textContent = '⚠️ 网络错误'; statusEl.textContent = '⚠️ 网络错误';
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.textContent = '保 存 API 配 置'; btn.textContent = '保 存 配 置';
}
});
// 保存工具调用限制
document.getElementById('toolLimitsForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('saveLimitsBtn');
btn.disabled = true;
btn.textContent = '保存中...';
const statusEl = document.getElementById('limitsStatus');
statusEl.textContent = '🔄 保存中...';
try {
const resp = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool_limits: {
persona_update_max: parseInt(document.getElementById('settings_persona_update').value) || 1,
task_update_max: parseInt(document.getElementById('settings_task_update').value) || 20,
task_query_max: parseInt(document.getElementById('settings_task_query').value) || 30,
memory_query_max: parseInt(document.getElementById('settings_memory_query').value) || 30,
memory_update_max: parseInt(document.getElementById('settings_memory_update').value) || 15
}
})
});
const data = await resp.json();
if (data.success) {
statusEl.textContent = '✅ 工具限制已保存并生效';
setTimeout(() => { statusEl.textContent = ''; }, 3000);
} else {
statusEl.textContent = '⚠️ 保存失败: ' + (data.error || '未知错误');
}
} catch (e) {
statusEl.textContent = '⚠️ 网络错误';
} finally {
btn.disabled = false;
btn.textContent = '保 存 限 制';
} }
}); });