diff --git a/static/admin/agent.js b/static/admin/agent.js
index 2a64793..8790483 100644
--- a/static/admin/agent.js
+++ b/static/admin/agent.js
@@ -19,7 +19,9 @@ function initAgentChat(){
loadLiveDevices();
loadAgentTargetDevices();
startRunPoll();
- restoreAgentView();
+ renderConvList(); // 会话列表(决定当前会话)
+ loadCurrentConvMessages(); // 渲染当前会话历史
+ restoreRunningFlow(); // 若任务运行中:重订阅事件流
// 输入框快捷键
const inp = document.getElementById('agent-input');
inp.addEventListener('keydown', ev=>{
@@ -30,39 +32,22 @@ function initAgentChat(){
});
}
-// ================== 页面刷新/重进恢复 ==================
-// 刷新后聊天区是空的,但后端任务可能还在跑、历史还在——从这里恢复:
-// 1) 渲染会话历史(user/assistant 文本轮)
-// 2) 任务运行中 → 重新订阅事件流(服务端队列保留积压,重连后补发)
-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 还在),无需恢复
+// ================== 页面刷新/重进恢复(运行中任务续流) ==================
+// 历史轮次由 loadCurrentConvMessages 从会话渲染;这里只处理「任务仍在后台跑」:
+// 重新订阅事件流(服务端队列保留积压,重连后补发 delta/step/done)。
+function restoreRunningFlow(){
apiGet('/api/agent/run').then(r=>{
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){
- const div = newAssistantMsg();
- div.querySelector('.agent-text').textContent = '↻ 已恢复连接,任务仍在后台运行(此前的内容可在上方查看)…';
setRunning(true);
listenStream(r.run_id);
- }else if(r.state === 'error' && r.error){
- const div = newAssistantMsg();
- div.querySelector('.agent-text').textContent = '⚠ ' + r.error;
- }else if(!hist.length && r.state === 'idle'){
- clearEmpty();
- document.getElementById('agent-chat').innerHTML =
- '
+
diff --git a/web/agent_api.py b/web/agent_api.py
index 7e80371..be0449c 100644
--- a/web/agent_api.py
+++ b/web/agent_api.py
@@ -460,39 +460,156 @@ def agent_experience_keep():
db.session.commit()
return jsonify({"ok": True, "msg": f"经验 #{exp_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})
+# ================== 历史会话(DeepSeek 式:多会话持久化) ==================
+# 会话 = 消息序列(JSON),一轮 run 的 user/assistant 文本完成后追加落库。
+# 单表 JSON 存储(每会话几十 KB 内,无需独立消息表)。
+_CONV_TABLE = """
+CREATE TABLE IF NOT EXISTS agent_conversation (
+ id VARCHAR(20) PRIMARY KEY,
+ title VARCHAR(100) DEFAULT '',
+ messages TEXT DEFAULT '[]',
+ 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
-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())
+def agent_conversations_list():
+ """会话列表(按最近更新倒序):{id, title, updated_at, count}。"""
+ _ensure_conv_table()
+ rows = db.session.execute(db.text(
+ "SELECT id, title, messages, updated_at FROM agent_conversation "
+ "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()
- return jsonify({"ok": True, "msg": "已保存"})
+ return jsonify({"ok": True, "id": cid, "title": "新会话"})
+
+
+@bp.route("/api/agent/conversations/
", 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/", 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//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"])
@admin_required
def agent_run():
- """启动 Agent:{prompt, serial?}。运行中返回 409。"""
+ """启动 Agent:{prompt, serial?, conversation_id?}。运行中返回 409。"""
data = request.json or {}
prompt = (data.get("prompt") or "").strip()
if not prompt:
return jsonify({"ok": False, "error": "请输入指令"}), 400
serial = (data.get("serial") or "").strip()
+ conv_id = (data.get("conversation_id") or "").strip()
cfg = _read_cfg()
if not cfg.get("api_key"):
return jsonify({"ok": False, "error": "请先在配置区填写 API Key"}), 400
@@ -517,6 +634,20 @@ def agent_run():
f"AI 不与任务抢设备,任务结束后才能操作(或在任务页先停止)"}), 409
except Exception:
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:
if _run["state"] == "running":
return jsonify({"ok": False, "error":
@@ -525,17 +656,38 @@ def agent_run():
run_id = uuid.uuid4().hex[:8]
from datetime import datetime as _dt
_run.update(id=run_id, state="running", prompt=prompt,
- serial=serial,
+ serial=serial, conv_id=conv_id,
started=_dt.now().strftime("%H:%M:%S"),
answer="", error="")
- # history 保留(同会话多轮对话),由前端「清空对话」调用 clear 重置
+ # history 保留(多轮上下文),由会话/「新建会话」管理
_queues[run_id] = queue.Queue()
_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,
args=(run_id, prompt, serial, cfg),
daemon=True).start()
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"])
@@ -691,7 +843,19 @@ def _agent_thread(run_id, prompt, serial, cfg):
pass # 非法值用默认 40
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 ""
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": "assistant", "content": (answer or "")[:4000]})
_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 发出后 SSE 关流,用户就看不到「已写入经验」的提示了。