feat: AI 控制台历史会话(DeepSeek 式)——①agent_conversation 表:会话持久化(单表 JSON 消息),一轮 run 完成后 user/assistant 自动落库、新会话以首条消息作标题;②会话 API:列表/新建/详情/删除/重命名;③run 绑定 conversation_id:history 从会话加载(多轮上下文延续)、完成写回;④前端三栏布局:左侧会话栏(新建/列表/高亮/悬停删除/轮数时间)+ 聊天 + 实时画面,切换会话即切换上下文,刷新后自动恢复当前会话与消息
This commit is contained in:
+199
-22
@@ -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/<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"])
|
||||
@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 关流,用户就看不到「已写入经验」的提示了。
|
||||
|
||||
Reference in New Issue
Block a user