feat: AI 控制台历史会话(DeepSeek 式)——①agent_conversation 表:会话持久化(单表 JSON 消息),一轮 run 完成后 user/assistant 自动落库、新会话以首条消息作标题;②会话 API:列表/新建/详情/删除/重命名;③run 绑定 conversation_id:history 从会话加载(多轮上下文延续)、完成写回;④前端三栏布局:左侧会话栏(新建/列表/高亮/悬停删除/轮数时间)+ 聊天 + 实时画面,切换会话即切换上下文,刷新后自动恢复当前会话与消息
This commit is contained in:
+129
-47
@@ -19,7 +19,9 @@ function initAgentChat(){
|
|||||||
loadLiveDevices();
|
loadLiveDevices();
|
||||||
loadAgentTargetDevices();
|
loadAgentTargetDevices();
|
||||||
startRunPoll();
|
startRunPoll();
|
||||||
restoreAgentView();
|
renderConvList(); // 会话列表(决定当前会话)
|
||||||
|
loadCurrentConvMessages(); // 渲染当前会话历史
|
||||||
|
restoreRunningFlow(); // 若任务运行中:重订阅事件流
|
||||||
// 输入框快捷键
|
// 输入框快捷键
|
||||||
const inp = document.getElementById('agent-input');
|
const inp = document.getElementById('agent-input');
|
||||||
inp.addEventListener('keydown', ev=>{
|
inp.addEventListener('keydown', ev=>{
|
||||||
@@ -30,39 +32,22 @@ function initAgentChat(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== 页面刷新/重进恢复 ==================
|
// ================== 页面刷新/重进恢复(运行中任务续流) ==================
|
||||||
// 刷新后聊天区是空的,但后端任务可能还在跑、历史还在——从这里恢复:
|
// 历史轮次由 loadCurrentConvMessages 从会话渲染;这里只处理「任务仍在后台跑」:
|
||||||
// 1) 渲染会话历史(user/assistant 文本轮)
|
// 重新订阅事件流(服务端队列保留积压,重连后补发 delta/step/done)。
|
||||||
// 2) 任务运行中 → 重新订阅事件流(服务端队列保留积压,重连后补发)
|
function restoreRunningFlow(){
|
||||||
function restoreAgentView(){
|
|
||||||
const chat = document.getElementById('agent-chat');
|
|
||||||
if(!chat) return;
|
|
||||||
const hasContent = chat.children.length > 0 && !chat.querySelector('.agent-empty');
|
|
||||||
if(hasContent) return; // 页面未刷新(切 Tab 回来 DOM 还在),无需恢复
|
|
||||||
apiGet('/api/agent/run').then(r=>{
|
apiGet('/api/agent/run').then(r=>{
|
||||||
if(!r||!r.ok) return;
|
if(!r||!r.ok) return;
|
||||||
const hist = r.history || [];
|
|
||||||
hist.forEach(h=>{
|
|
||||||
const txt = (h.content || '').trim();
|
|
||||||
if(!txt) return;
|
|
||||||
if(h.role === 'user'){ addUserMsg(txt); }
|
|
||||||
else{
|
|
||||||
const div = newAssistantMsg();
|
|
||||||
div.querySelector('.agent-text').textContent = txt;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(r.state === 'running' && r.run_id){
|
if(r.state === 'running' && r.run_id){
|
||||||
const div = newAssistantMsg();
|
|
||||||
div.querySelector('.agent-text').textContent = '↻ 已恢复连接,任务仍在后台运行(此前的内容可在上方查看)…';
|
|
||||||
setRunning(true);
|
setRunning(true);
|
||||||
listenStream(r.run_id);
|
listenStream(r.run_id);
|
||||||
}else if(r.state === 'error' && r.error){
|
}else if(r.state === 'error' && r.error && _currentConvId){
|
||||||
const div = newAssistantMsg();
|
// 错误提示仅当本会话相关时展示
|
||||||
div.querySelector('.agent-text').textContent = '⚠ ' + r.error;
|
const chat = document.getElementById('agent-chat');
|
||||||
}else if(!hist.length && r.state === 'idle'){
|
if(chat && !chat.children.length){
|
||||||
clearEmpty();
|
const div = newAssistantMsg();
|
||||||
document.getElementById('agent-chat').innerHTML =
|
div.querySelector('.agent-text').textContent = '⚠ ' + r.error;
|
||||||
'<div class="agent-empty">👋 给 AI 下达指令…(先选 🎯 目标设备)</div>';
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -200,18 +185,95 @@ function bindChatScroll(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function clearAgentChat(){
|
function clearAgentChat(){
|
||||||
document.getElementById('agent-chat').innerHTML =
|
document.getElementById('agent-chat').innerHTML = '';
|
||||||
'<div class="agent-empty">对话已清空。给 AI 下达新指令…</div>';
|
|
||||||
}
|
}
|
||||||
// 新建会话:清后端历史 + 清前端对话区(像 DeepSeek 的新建会话)
|
|
||||||
function newAgentSession(){
|
// ================== 历史会话(DeepSeek 式:左侧列表,多会话持久化) ==================
|
||||||
|
let _currentConvId = localStorage.getItem('agent_conv_id') || '';
|
||||||
|
function saveConvId(id){
|
||||||
|
_currentConvId = id || '';
|
||||||
|
if(id) localStorage.setItem('agent_conv_id', id);
|
||||||
|
else localStorage.removeItem('agent_conv_id');
|
||||||
|
}
|
||||||
|
function createAgentSession(){
|
||||||
if(_agentBusy){showToast('运行中不能新建会话','error');return;}
|
if(_agentBusy){showToast('运行中不能新建会话','error');return;}
|
||||||
if(!confirm('新建会话将清空当前对话历史,确定?'))return;
|
apiPost('/api/agent/conversations',{}).then(r=>{
|
||||||
apiPost('/api/agent/clear',{}).then(r=>{
|
|
||||||
if(r&&r.ok){
|
if(r&&r.ok){
|
||||||
|
saveConvId(r.id);
|
||||||
clearAgentChat();
|
clearAgentChat();
|
||||||
showToast('已新建会话','success');
|
showToast('已新建会话','success');
|
||||||
}else showToast('操作失败','error');
|
renderConvList();
|
||||||
|
}else showToast('新建失败: '+((r&&r.error)||''),'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function selectAgentSession(id){
|
||||||
|
if(_agentBusy){showToast('运行中不能切换会话','error');return;}
|
||||||
|
if(id === _currentConvId) return;
|
||||||
|
saveConvId(id);
|
||||||
|
clearAgentChat();
|
||||||
|
loadCurrentConvMessages();
|
||||||
|
renderConvList();
|
||||||
|
}
|
||||||
|
function deleteAgentSession(id){
|
||||||
|
if(!confirm('删除该会话?历史消息将不可恢复。')) return;
|
||||||
|
apiDelete('/api/agent/conversations/'+id).then(r=>{
|
||||||
|
if(r&&r.ok){
|
||||||
|
if(_currentConvId === id) saveConvId('');
|
||||||
|
showToast('会话已删除','success');
|
||||||
|
renderConvList();
|
||||||
|
loadCurrentConvMessages();
|
||||||
|
}else showToast('删除失败: '+((r&&r.error)||''),'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderConvList(){
|
||||||
|
apiGet('/api/agent/conversations').then(r=>{
|
||||||
|
if(!r||!r.ok)return;
|
||||||
|
const list = document.getElementById('agent-sess-list');
|
||||||
|
if(!list)return;
|
||||||
|
const convs = r.conversations || [];
|
||||||
|
let cur = _currentConvId;
|
||||||
|
if(!convs.some(c=>c.id===cur)) cur = convs.length ? convs[0].id : '';
|
||||||
|
if(cur !== _currentConvId){ saveConvId(cur); clearAgentChat(); loadCurrentConvMessages(); }
|
||||||
|
if(!convs.length){
|
||||||
|
list.innerHTML = '<div class="agent-empty" style="padding:20px 10px;font-size:12px">'
|
||||||
|
+ '暂无历史会话<br><br>点「+ 新建会话」开始</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = convs.map(c=>{
|
||||||
|
const t = c.title || '新会话';
|
||||||
|
return '<div class="agent-sess-item'+(c.id===cur?' active':'')
|
||||||
|
+'" onclick="selectAgentSession(\''+c.id+'\')">'
|
||||||
|
+'<button class="agent-sess-del" title="删除会话" '
|
||||||
|
+'onclick="event.stopPropagation();deleteAgentSession(\''+c.id+'\')">✕</button>'
|
||||||
|
+'<div class="t">'+esc(t)+'</div>'
|
||||||
|
+'<div class="m">'+esc(c.updated_at||'')+(c.count ? ' · '+c.count+' 轮' : '')+'</div></div>';
|
||||||
|
}).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function loadCurrentConvMessages(){
|
||||||
|
const chat = document.getElementById('agent-chat');
|
||||||
|
if(!chat) return;
|
||||||
|
if(!_currentConvId){
|
||||||
|
chat.innerHTML = '<div class="agent-empty">👋 点左侧「+ 新建会话」开始,'
|
||||||
|
+ '给 AI 下达指令(先选 🎯 目标设备)。</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiGet('/api/agent/conversations/'+_currentConvId).then(r=>{
|
||||||
|
if(!r||!r.ok)return;
|
||||||
|
chat.innerHTML = '';
|
||||||
|
(r.messages||[]).forEach(m=>{
|
||||||
|
const txt = (m.content||'').trim();
|
||||||
|
if(!txt) return;
|
||||||
|
if(m.role==='user'){ addUserMsg(txt); }
|
||||||
|
else{
|
||||||
|
const div = newAssistantMsg();
|
||||||
|
div.querySelector('.agent-text').textContent = txt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!chat.children.length){
|
||||||
|
chat.innerHTML = '<div class="agent-empty">新会话。给 AI 下达指令'
|
||||||
|
+ '(先选 🎯 目标设备)…</div>';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,17 +306,36 @@ function sendAgentMsg(){
|
|||||||
inp.value = '';
|
inp.value = '';
|
||||||
addUserMsg(text);
|
addUserMsg(text);
|
||||||
|
|
||||||
apiPost('/api/agent/run', {prompt: text, serial: serial}).then(r=>{
|
// 会话:无当前会话时自动新建(DeepSeek 式:消息总归属于一个会话)
|
||||||
if(!r||!r.ok){
|
const doRun = (convId)=>{
|
||||||
const msg = r ? (r.error||'启动失败') : '请求失败';
|
apiPost('/api/agent/run', {prompt: text, serial: serial,
|
||||||
const div = newAssistantMsg();
|
conversation_id: convId}).then(r=>{
|
||||||
div.querySelector('.agent-text').textContent = '⚠ ' + msg;
|
if(!r||!r.ok){
|
||||||
showToast(msg,'error');
|
const msg = r ? (r.error||'启动失败') : '请求失败';
|
||||||
return;
|
const div = newAssistantMsg();
|
||||||
}
|
div.querySelector('.agent-text').textContent = '⚠ ' + msg;
|
||||||
setRunning(true);
|
showToast(msg,'error');
|
||||||
listenStream(r.run_id);
|
return;
|
||||||
});
|
}
|
||||||
|
setRunning(true);
|
||||||
|
listenStream(r.run_id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if(_currentConvId){
|
||||||
|
doRun(_currentConvId);
|
||||||
|
}else{
|
||||||
|
apiPost('/api/agent/conversations',{}).then(r=>{
|
||||||
|
if(r&&r.ok){
|
||||||
|
saveConvId(r.id);
|
||||||
|
renderConvList();
|
||||||
|
doRun(r.id);
|
||||||
|
}else{
|
||||||
|
const div = newAssistantMsg();
|
||||||
|
div.querySelector('.agent-text').textContent = '⚠ 新建会话失败';
|
||||||
|
showToast('新建会话失败','error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function listenStream(runId){
|
function listenStream(runId){
|
||||||
@@ -309,6 +390,7 @@ function listenStream(runId){
|
|||||||
msgEl.querySelector('.agent-text').textContent = d.answer;
|
msgEl.querySelector('.agent-text').textContent = d.answer;
|
||||||
}
|
}
|
||||||
endRun();
|
endRun();
|
||||||
|
renderConvList(); // 落库已完成:刷新标题/时间/轮数
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('error', ev=>{
|
es.addEventListener('error', ev=>{
|
||||||
|
|||||||
@@ -306,6 +306,17 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
|||||||
|
|
||||||
/* ===== AI 控制台(聊天界面,浅色内容区——DeepSeek 风格,图片文字清晰) ===== */
|
/* ===== AI 控制台(聊天界面,浅色内容区——DeepSeek 风格,图片文字清晰) ===== */
|
||||||
.agent-body{display:flex;flex:1;min-height:0}
|
.agent-body{display:flex;flex:1;min-height:0}
|
||||||
|
.agent-col-sessions{width:236px;border-right:1px solid #e5e7eb;background:#f6f7f9;display:flex;flex-direction:column;min-width:0}
|
||||||
|
.agent-sess-head{padding:10px;border-bottom:1px solid #e5e7eb}
|
||||||
|
.agent-sess-list{flex:1;overflow-y:auto;padding:6px}
|
||||||
|
.agent-sess-item{padding:8px 10px;border-radius:9px;cursor:pointer;margin-bottom:2px;border:1px solid transparent;position:relative}
|
||||||
|
.agent-sess-item:hover{background:#eceff3}
|
||||||
|
.agent-sess-item.active{background:#e0f2fe;border-color:#bae6fd}
|
||||||
|
.agent-sess-item .t{font-size:12.5px;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:20px}
|
||||||
|
.agent-sess-item .m{font-size:10.5px;color:#9ca3af;margin-top:2px}
|
||||||
|
.agent-sess-del{position:absolute;right:6px;top:6px;visibility:hidden;background:transparent;border:none;color:#9ca3af;cursor:pointer;font-size:12px;padding:2px 5px;border-radius:5px}
|
||||||
|
.agent-sess-item:hover .agent-sess-del{visibility:visible}
|
||||||
|
.agent-sess-del:hover{background:#fecaca;color:#b91c1c}
|
||||||
.agent-col-chat{flex:1;display:flex;flex-direction:column;min-width:0}
|
.agent-col-chat{flex:1;display:flex;flex-direction:column;min-width:0}
|
||||||
.agent-col-live{width:300px;border-left:1px solid #e5e7eb;background:#fff;display:flex;flex-direction:column;padding:10px}
|
.agent-col-live{width:300px;border-left:1px solid #e5e7eb;background:#fff;display:flex;flex-direction:column;padding:10px}
|
||||||
.agent-live-head{font-size:12.5px;font-weight:600;color:#374151;display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
.agent-live-head{font-size:12.5px;font-weight:600;color:#374151;display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
||||||
@@ -467,12 +478,17 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
|||||||
<div class="agent-title">🤖 AI 控制台<span id="agent-model-tag" class="agent-model-tag"></span></div>
|
<div class="agent-title">🤖 AI 控制台<span id="agent-model-tag" class="agent-model-tag"></span></div>
|
||||||
<div class="agent-top-actions">
|
<div class="agent-top-actions">
|
||||||
<span id="agent-running-tag" class="agent-running-tag" style="display:none">● 运行中</span>
|
<span id="agent-running-tag" class="agent-running-tag" style="display:none">● 运行中</span>
|
||||||
<button class="btn btn-xs" onclick="newAgentSession()" title="清空当前对话并开启新会话">+ 新建会话</button>
|
|
||||||
<button class="btn btn-xs" onclick="openExpLib()" title="查看/管理自进化经验库(每日 AI 巡检建议,删除需确认)">🧠 经验库</button>
|
<button class="btn btn-xs" onclick="openExpLib()" title="查看/管理自进化经验库(每日 AI 巡检建议,删除需确认)">🧠 经验库</button>
|
||||||
<button class="btn btn-xs" onclick="openAgentConfig()">⚙ 配置</button>
|
<button class="btn btn-xs" onclick="openAgentConfig()">⚙ 配置</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-body">
|
<div class="agent-body">
|
||||||
|
<div class="agent-col-sessions">
|
||||||
|
<div class="agent-sess-head">
|
||||||
|
<button class="btn btn-primary" style="width:100%" onclick="createAgentSession()">+ 新建会话</button>
|
||||||
|
</div>
|
||||||
|
<div id="agent-sess-list" class="agent-sess-list"></div>
|
||||||
|
</div>
|
||||||
<div class="agent-col-chat">
|
<div class="agent-col-chat">
|
||||||
<div id="agent-chat" class="agent-chat"></div>
|
<div id="agent-chat" class="agent-chat"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+199
-22
@@ -460,39 +460,156 @@ def agent_experience_keep():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({"ok": True, "msg": f"经验 #{exp_id} 已保留"})
|
return jsonify({"ok": True, "msg": f"经验 #{exp_id} 已保留"})
|
||||||
|
|
||||||
@bp.route("/api/agent/config", methods=["GET"])
|
# ================== 历史会话(DeepSeek 式:多会话持久化) ==================
|
||||||
@admin_required
|
# 会话 = 消息序列(JSON),一轮 run 的 user/assistant 文本完成后追加落库。
|
||||||
def agent_config_get():
|
# 单表 JSON 存储(每会话几十 KB 内,无需独立消息表)。
|
||||||
"""读 Agent 配置(key 打码返回)。"""
|
_CONV_TABLE = """
|
||||||
cfg = _read_cfg()
|
CREATE TABLE IF NOT EXISTS agent_conversation (
|
||||||
if cfg["api_key"]:
|
id VARCHAR(20) PRIMARY KEY,
|
||||||
k = cfg["api_key"]
|
title VARCHAR(100) DEFAULT '',
|
||||||
cfg["api_key_masked"] = k[:6] + "***" + k[-4:]
|
messages TEXT DEFAULT '[]',
|
||||||
return jsonify({"ok": True, **cfg})
|
created_at VARCHAR(20) DEFAULT '',
|
||||||
|
updated_at VARCHAR(20) DEFAULT '')"""
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/agent/config", methods=["POST"])
|
def _ensure_conv_table():
|
||||||
|
try:
|
||||||
|
db.session.execute(db.text(_CONV_TABLE))
|
||||||
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _conv_msgs(conv_id):
|
||||||
|
"""读会话消息列表 [{role, content}]。会话不存在返回 None。"""
|
||||||
|
try:
|
||||||
|
row = db.session.execute(db.text(
|
||||||
|
"SELECT messages FROM agent_conversation WHERE id=:i"),
|
||||||
|
{"i": conv_id}).fetchone()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(row[0] or "[]")
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _conv_save_messages(conv_id, messages, title=None):
|
||||||
|
"""整段写回会话消息 + 时间戳(尽力而为,失败不影响主流程)。"""
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
msgs_json = json.dumps(messages[-60:], ensure_ascii=False)
|
||||||
|
if title:
|
||||||
|
db.session.execute(db.text(
|
||||||
|
"UPDATE agent_conversation SET messages=:m, title=:t, "
|
||||||
|
"updated_at=:u WHERE id=:i"),
|
||||||
|
{"m": msgs_json, "t": title[:100], "u": now, "i": conv_id})
|
||||||
|
else:
|
||||||
|
db.session.execute(db.text(
|
||||||
|
"UPDATE agent_conversation SET messages=:m, updated_at=:u "
|
||||||
|
"WHERE id=:i"),
|
||||||
|
{"m": msgs_json, "u": now, "i": conv_id})
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
_log.warning(f"会话保存失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/agent/conversations", methods=["GET"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def agent_config_save():
|
def agent_conversations_list():
|
||||||
"""保存 Agent 配置:{api_base?, model?, api_key?, default_serial?} 部分更新。"""
|
"""会话列表(按最近更新倒序):{id, title, updated_at, count}。"""
|
||||||
data = request.json or {}
|
_ensure_conv_table()
|
||||||
for key, meta_key in _CFG_KEYS.items():
|
rows = db.session.execute(db.text(
|
||||||
if key in data and data[key] is not None:
|
"SELECT id, title, messages, updated_at FROM agent_conversation "
|
||||||
_meta_put(meta_key, str(data[key]).strip())
|
"ORDER BY updated_at DESC, created_at DESC")).fetchall()
|
||||||
|
out = []
|
||||||
|
for cid, title, messages, updated_at in rows:
|
||||||
|
try:
|
||||||
|
count = len(json.loads(messages or "[]")) // 2
|
||||||
|
except Exception:
|
||||||
|
count = 0
|
||||||
|
out.append({"id": cid, "title": title or "新会话",
|
||||||
|
"updated_at": updated_at or "", "count": count})
|
||||||
|
return jsonify({"ok": True, "conversations": out})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/agent/conversations", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def agent_conversations_create():
|
||||||
|
"""新建会话(空消息)。返回 {id}。"""
|
||||||
|
_ensure_conv_table()
|
||||||
|
from datetime import datetime
|
||||||
|
cid = uuid.uuid4().hex[:10]
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
db.session.execute(db.text(
|
||||||
|
"INSERT INTO agent_conversation(id, title, messages, created_at, updated_at) "
|
||||||
|
"VALUES(:i, '', '[]', :c, :c)"), {"i": cid, "c": now})
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({"ok": True, "msg": "已保存"})
|
return jsonify({"ok": True, "id": cid, "title": "新会话"})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/agent/conversations/<conv_id>", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def agent_conversations_detail(conv_id):
|
||||||
|
"""会话详情(全部消息文本)。"""
|
||||||
|
_ensure_conv_table()
|
||||||
|
msgs = _conv_msgs(conv_id)
|
||||||
|
if msgs is None:
|
||||||
|
return jsonify({"ok": False, "error": "会话不存在"}), 404
|
||||||
|
row = db.session.execute(db.text(
|
||||||
|
"SELECT title, created_at, updated_at FROM agent_conversation "
|
||||||
|
"WHERE id=:i"), {"i": conv_id}).fetchone()
|
||||||
|
return jsonify({"ok": True, "id": conv_id,
|
||||||
|
"title": (row[0] if row else "") or "新会话",
|
||||||
|
"messages": msgs,
|
||||||
|
"created_at": row[1] if row else "",
|
||||||
|
"updated_at": row[2] if row else ""})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/agent/conversations/<conv_id>", methods=["DELETE"])
|
||||||
|
@admin_required
|
||||||
|
def agent_conversations_delete(conv_id):
|
||||||
|
"""删除会话(消息一并删除,不可恢复)。"""
|
||||||
|
_ensure_conv_table()
|
||||||
|
db.session.execute(db.text(
|
||||||
|
"DELETE FROM agent_conversation WHERE id=:i"), {"i": conv_id})
|
||||||
|
db.session.commit()
|
||||||
|
_log.info(f"删除会话 {conv_id}")
|
||||||
|
return jsonify({"ok": True, "msg": "会话已删除"})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/agent/conversations/<conv_id>/rename", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def agent_conversations_rename(conv_id):
|
||||||
|
"""重命名会话。"""
|
||||||
|
data = request.json or {}
|
||||||
|
title = (data.get("title") or "").strip()[:100]
|
||||||
|
if not title:
|
||||||
|
return jsonify({"ok": False, "error": "标题不能为空"}), 400
|
||||||
|
_ensure_conv_table()
|
||||||
|
db.session.execute(db.text(
|
||||||
|
"UPDATE agent_conversation SET title=:t WHERE id=:i"),
|
||||||
|
{"t": title, "i": conv_id})
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"ok": True, "msg": "已重命名"})
|
||||||
|
|
||||||
|
|
||||||
# ================== 运行 ==================
|
# ================== 运行 ==================
|
||||||
@bp.route("/api/agent/run", methods=["POST"])
|
@bp.route("/api/agent/run", methods=["POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def agent_run():
|
def agent_run():
|
||||||
"""启动 Agent:{prompt, serial?}。运行中返回 409。"""
|
"""启动 Agent:{prompt, serial?, conversation_id?}。运行中返回 409。"""
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
prompt = (data.get("prompt") or "").strip()
|
prompt = (data.get("prompt") or "").strip()
|
||||||
if not prompt:
|
if not prompt:
|
||||||
return jsonify({"ok": False, "error": "请输入指令"}), 400
|
return jsonify({"ok": False, "error": "请输入指令"}), 400
|
||||||
serial = (data.get("serial") or "").strip()
|
serial = (data.get("serial") or "").strip()
|
||||||
|
conv_id = (data.get("conversation_id") or "").strip()
|
||||||
cfg = _read_cfg()
|
cfg = _read_cfg()
|
||||||
if not cfg.get("api_key"):
|
if not cfg.get("api_key"):
|
||||||
return jsonify({"ok": False, "error": "请先在配置区填写 API Key"}), 400
|
return jsonify({"ok": False, "error": "请先在配置区填写 API Key"}), 400
|
||||||
@@ -517,6 +634,20 @@ def agent_run():
|
|||||||
f"AI 不与任务抢设备,任务结束后才能操作(或在任务页先停止)"}), 409
|
f"AI 不与任务抢设备,任务结束后才能操作(或在任务页先停止)"}), 409
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # 状态服务异常不阻塞(MCP busy 锁兜底)
|
pass # 状态服务异常不阻塞(MCP busy 锁兜底)
|
||||||
|
# 会话校验:不存在则自动新建(标题=首条消息截断)
|
||||||
|
if conv_id:
|
||||||
|
_ensure_conv_table()
|
||||||
|
if _conv_msgs(conv_id) is None:
|
||||||
|
try:
|
||||||
|
from datetime import datetime as _dt2
|
||||||
|
now = _dt2.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
db.session.execute(db.text(
|
||||||
|
"INSERT OR IGNORE INTO agent_conversation(id, messages, "
|
||||||
|
"created_at, updated_at) VALUES(:i, '[]', :c, :c)"),
|
||||||
|
{"i": conv_id, "c": now})
|
||||||
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
with _lock:
|
with _lock:
|
||||||
if _run["state"] == "running":
|
if _run["state"] == "running":
|
||||||
return jsonify({"ok": False, "error":
|
return jsonify({"ok": False, "error":
|
||||||
@@ -525,17 +656,38 @@ def agent_run():
|
|||||||
run_id = uuid.uuid4().hex[:8]
|
run_id = uuid.uuid4().hex[:8]
|
||||||
from datetime import datetime as _dt
|
from datetime import datetime as _dt
|
||||||
_run.update(id=run_id, state="running", prompt=prompt,
|
_run.update(id=run_id, state="running", prompt=prompt,
|
||||||
serial=serial,
|
serial=serial, conv_id=conv_id,
|
||||||
started=_dt.now().strftime("%H:%M:%S"),
|
started=_dt.now().strftime("%H:%M:%S"),
|
||||||
answer="", error="")
|
answer="", error="")
|
||||||
# history 保留(同会话多轮对话),由前端「清空对话」调用 clear 重置
|
# history 保留(多轮上下文),由会话/「新建会话」管理
|
||||||
_queues[run_id] = queue.Queue()
|
_queues[run_id] = queue.Queue()
|
||||||
_stop_events[run_id] = threading.Event()
|
_stop_events[run_id] = threading.Event()
|
||||||
_log.info(f"Agent 启动: {prompt[:60]} @ {serial or 'default'}")
|
_log.info(f"Agent 启动: {prompt[:60]} @ {serial or 'default'} conv={conv_id or '-'}")
|
||||||
threading.Thread(target=_agent_thread,
|
threading.Thread(target=_agent_thread,
|
||||||
args=(run_id, prompt, serial, cfg),
|
args=(run_id, prompt, serial, cfg),
|
||||||
daemon=True).start()
|
daemon=True).start()
|
||||||
return jsonify({"ok": True, "run_id": run_id})
|
return jsonify({"ok": True, "run_id": run_id})
|
||||||
|
@bp.route("/api/agent/config", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def agent_config_get():
|
||||||
|
"""读 Agent 配置(key 打码返回)。"""
|
||||||
|
cfg = _read_cfg()
|
||||||
|
if cfg["api_key"]:
|
||||||
|
k = cfg["api_key"]
|
||||||
|
cfg["api_key_masked"] = k[:6] + "***" + k[-4:]
|
||||||
|
return jsonify({"ok": True, **cfg})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/agent/config", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def agent_config_save():
|
||||||
|
"""保存 Agent 配置:{api_base?, model?, api_key?, default_serial?} 部分更新。"""
|
||||||
|
data = request.json or {}
|
||||||
|
for key, meta_key in _CFG_KEYS.items():
|
||||||
|
if key in data and data[key] is not None:
|
||||||
|
_meta_put(meta_key, str(data[key]).strip())
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"ok": True, "msg": "已保存"})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/agent/run", methods=["GET"])
|
@bp.route("/api/agent/run", methods=["GET"])
|
||||||
@@ -691,7 +843,19 @@ def _agent_thread(run_id, prompt, serial, cfg):
|
|||||||
pass # 非法值用默认 40
|
pass # 非法值用默认 40
|
||||||
|
|
||||||
with _lock:
|
with _lock:
|
||||||
history = list(_run.get("history") or [])
|
conv_id = _run.get("conv_id") or ""
|
||||||
|
# 历史来源:绑定了会话 → 从会话读(多轮上下文延续,DeepSeek 式);
|
||||||
|
# 无会话(CLI/兼容)→ 用运行态 history
|
||||||
|
if conv_id:
|
||||||
|
msgs = _conv_msgs(conv_id)
|
||||||
|
history = [{"role": m.get("role"), "content": m.get("content", "")[:4000]}
|
||||||
|
for m in (msgs or []) if m.get("role") in ("user", "assistant")]
|
||||||
|
history = history[-24:]
|
||||||
|
with _lock:
|
||||||
|
_run["history"] = list(history)
|
||||||
|
else:
|
||||||
|
with _lock:
|
||||||
|
history = list(_run.get("history") or [])
|
||||||
target = serial or cfg.get("default_serial") or ""
|
target = serial or cfg.get("default_serial") or ""
|
||||||
stop_evt = _stop_events.get(run_id)
|
stop_evt = _stop_events.get(run_id)
|
||||||
|
|
||||||
@@ -722,6 +886,19 @@ def _agent_thread(run_id, prompt, serial, cfg):
|
|||||||
hist.append({"role": "user", "content": prompt[:2000]})
|
hist.append({"role": "user", "content": prompt[:2000]})
|
||||||
hist.append({"role": "assistant", "content": (answer or "")[:4000]})
|
hist.append({"role": "assistant", "content": (answer or "")[:4000]})
|
||||||
_run["history"] = hist[-24:]
|
_run["history"] = hist[-24:]
|
||||||
|
# 会话落库:本轮追加写回(新会话自动用首条消息作标题)
|
||||||
|
if conv_id:
|
||||||
|
try:
|
||||||
|
saved = list(_run["history"])
|
||||||
|
title = None
|
||||||
|
if not db.session.execute(db.text(
|
||||||
|
"SELECT title FROM agent_conversation WHERE id=:i"),
|
||||||
|
{"i": conv_id}).scalar():
|
||||||
|
title = "".join(prompt.split())[:30] or "新会话"
|
||||||
|
_conv_save_messages(conv_id, saved, title=title)
|
||||||
|
_log.info(f"会话已保存: {conv_id}({len(saved)} 条消息)")
|
||||||
|
except Exception as e:
|
||||||
|
_log.warning(f"会话落库失败: {e}")
|
||||||
|
|
||||||
# 自进化:成功执行过工具则提炼配方写入经验。必须在 done 之前完成——
|
# 自进化:成功执行过工具则提炼配方写入经验。必须在 done 之前完成——
|
||||||
# done 发出后 SSE 关流,用户就看不到「已写入经验」的提示了。
|
# done 发出后 SSE 关流,用户就看不到「已写入经验」的提示了。
|
||||||
|
|||||||
Reference in New Issue
Block a user