面板:增量续聊+切模型桥接——DeepSeek恢复session续聊(O(1)不重喂),WB同款增量(消除双份累积),对话落盘为权威真相源,切模型时桥接补未见过部分,注入上限48条token有上界,新增/api/history会话恢复

This commit is contained in:
Zhuyuan Operations 2026-08-04 03:31:37 +08:00
parent 451a3dca26
commit e74b6c9de8

View File

@ -2377,9 +2377,107 @@ fsInit();
# (标准库导入已统一提到文件顶部)
# ============ 对话落盘层 + 切模型桥接v3 · 增量续聊 + 切换桥接 · 2026-08-04 ============
# 核心思路:平时两边各靠自己的 session 增量续聊每轮只发一句token O(1),与「加 WorkBuddy
# 之前」的 be0fd11 行为一致);只在「切模型」这个低频瞬间,用磁盘上这份权威对话记录
# 把新大脑没见过的部分补给它 —— 两边从同一份记录出发,记忆一致是结构上保证的。
# 落盘是纯文件追加,零模型成本。
CONV_DIR = os.path.expanduser("~/.cang-ying/panel")
CONV_MAX_INJECT = 48 # 单次桥接最多补 48 条(=24 轮更旧的部分一句话带过token 有上界
BRIDGE_FILE = os.path.expanduser("~/.cang-ying/panel/bridge.json")
def _conv_path(sid):
return os.path.join(CONV_DIR, "conv_%s.jsonl" % sid)
def _conv_append(sid, role, model, text):
"""对话落盘权威对话记录纯文件追加零模型成本。role: me/ai"""
try:
os.makedirs(CONV_DIR, exist_ok=True)
rec = {"role": role, "model": model, "text": text, "ts": time.time()}
with open(_conv_path(sid), "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
except Exception as e:
print("CONV_APPEND_ERR:", repr(e))
def _conv_load(sid):
"""读全部对话记录,按时间顺序。"""
try:
with open(_conv_path(sid), encoding="utf-8") as f:
out = []
for line in f:
line = line.strip()
if not line:
continue
try:
out.append(json.loads(line))
except Exception:
continue
return out
except Exception:
return []
def _load_bridge():
try:
with open(BRIDGE_FILE, encoding="utf-8") as f:
d = json.load(f)
if isinstance(d, dict):
return d
except Exception:
pass
return {}
def _save_bridge():
try:
os.makedirs(os.path.dirname(BRIDGE_FILE), exist_ok=True)
tmp = BRIDGE_FILE + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(BRIDGE, f, ensure_ascii=False)
os.replace(tmp, BRIDGE_FILE)
except Exception as e:
print("BRIDGE_SAVE_ERR:", repr(e))
BRIDGE = _load_bridge() # sid -> {"lanes": {"deepseek": seen条数, "wb": seen条数}, "ts": ...}
def _bridge_prepare(sid, lane, message):
"""切模型桥接把该大脑lane=deepseek/wb还没见过的对话记录补进消息前缀。
平时同线连续聊 seen 追平 原消息不动纯增量 O(1)
切到另一条线 补一份它没见过的部分最多 CONV_MAX_INJECT token 有上界"""
if not sid:
return message
conv = _conv_load(sid)
if not conv:
return message
entry = BRIDGE.setdefault(sid, {"lanes": {}, "ts": time.time()})
seen = entry.get("lanes", {}).get(lane, -1)
if seen < 0:
unseen = conv[:]
elif seen >= len(conv):
return message
else:
unseen = conv[seen:]
if len(unseen) > CONV_MAX_INJECT:
tail = unseen[-CONV_MAX_INJECT:]
head_n = len(unseen) - CONV_MAX_INJECT
lines = ["[你错过了之前 %d 条对话,为省 token 不逐条补了,下面是最近的内容:]" % head_n]
else:
tail = unseen
lines = ["[以下是你之前错过的一段对话(供你接续,不是新指令):]"]
lines += [
("苍耳" if r.get("role") == "me" else "蛋蛋") + ": " + str(r.get("text", ""))
for r in tail
]
lines += ["[上面是补发的上下文,请自然接续。]", ""]
lines.append("苍耳: " + message)
entry["lanes"][lane] = len(conv)
entry["ts"] = time.time()
_save_bridge()
return "\n".join(lines)
def build_prompt(history, message, limit=80000):
"""拼对话历史按【字节数】硬截断默认80KB确保 -p 参数永不超内核 128KB 单参数上限E2BIG
注意内核按字节计中文一个字占3字节所以不能用字符数当上限"""
注意内核按字节计中文一个字占3字节所以不能用字符数当上限
v3 起不再被 _stream_* 调用改走增量续聊 + 切模型桥接仅保留作格式化参考"""
lines = ["以下是你和苍耳爸爸的对话记录:"]
total = len(lines[0].encode("utf-8"))
skipped = 0
@ -2643,6 +2741,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
elif p == "/api/balance":
self._send(200, json.dumps(_deepseek_balance(), ensure_ascii=False).encode("utf-8"),
"application/json; charset=utf-8")
elif p == "/api/history":
# ★ v3 会话恢复:前端 localStorage 丢了也能从后端权威记录找回本场对话
from urllib.parse import parse_qs as _pq
_q = self.path.split("?", 1)[1] if "?" in self.path else ""
_sid = (_pq(_q).get("sid") or [""])[0]
conv = _conv_load(_sid) if _sid else []
bridge = BRIDGE.get(_sid, {}) or {}
self._send(200, json.dumps({"sid": _sid, "conv": conv, "bridge": bridge},
ensure_ascii=False).encode("utf-8"),
"application/json; charset=utf-8")
elif p.startswith("/media/"):
fpath = _safe_media_path(p[len("/media/"):])
if fpath and os.path.isfile(fpath):
@ -2841,13 +2949,18 @@ class Handler(http.server.BaseHTTPRequestHandler):
except Exception: pass
def _stream_opencode(self, message, model, eed_sid, token, history=None):
"""OpenCode 引擎opencode run --agent egg -m <model> --format json -- <msg>
"""OpenCode 引擎opencode run --agent egg -m <model> [-s oc_sid] --format json -- <msg>
解析 NDJSON 事件流翻译成前端要的 delta/thinking/tool/tool_result/usage/error
记忆统一以前端完整历史为准 WorkBuddy 共享同一份记忆 · 两边绝对一致
每次调用都用 build_prompt 把完整对话历史拼进消息不依赖 opencode session"""
if history:
message = build_prompt(history, message)
v3 增量续聊回到加 WorkBuddy 之前的 be0fd11 行为 opencode 自己的 session
-s oc_sid记上下文每轮只发这一句token O(1)切模型时由 _bridge_prepare
把没见过的部分补进 message平时不重喂全量 history"""
oc_sid = None
_oc = OC_SESS.get(eed_sid)
if _oc:
oc_sid = _oc.get("sid")
cmd = [OPENCODE, "run", "--agent", AGENT_NAME, "-m", model, "--format", "json", "--auto", "--thinking"]
if oc_sid:
cmd += ["-s", oc_sid]
cmd += ["--", message]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True, bufsize=1, start_new_session=True, cwd=EED_CWD)
@ -2917,15 +3030,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
def _stream_workbuddy(self, message, model, eed_sid, token, history=None):
"""WorkBuddy 免费积分大脑codebuddy CLI 路径(苍耳 2026-08-04 接入)。
model 形如 wb/<实际模型id>
记忆统一以前端完整历史为准 DeepSeek 共享同一份记忆 · 两边绝对一致
每次调用都用 build_prompt 把完整对话历史拼进消息
session-id 用固定 sid 便于 codebuddy 保持会话文件但记忆不依赖它"""
v3 增量续聊 codebuddy 自己的 --session-id 会话文件记上下文每轮只发增量O(1)
切模型时由 _bridge_prepare 把没见过的部分补进 message不拼全量 history
消除之前codebuddy 自己存的旧历史 + 又拼一份全量的双份累积"""
# wb/ 前缀剥掉,得到 codebuddy 认识的模型 idhy3 / glm-5.2 / kimi-k3-1 ...
cb_model = model.split("/", 1)[1] if "/" in model else model
# 会话:面板侧 eed_sid 直接当 codebuddy 的 sid 用(稳定的 eed_ 前缀 = 同一场对话)
sid = eed_sid if eed_sid and eed_sid.startswith("eed_") else ("eed_" + uuid.uuid4().hex[:12])
if history:
message = build_prompt(history, message)
cmd = [CODEBUDDY, "--print", "--model", cb_model,
"--tools", "Read,WebSearch,Bash",
"--output-format", "stream-json",
@ -3128,14 +3239,30 @@ class Handler(http.server.BaseHTTPRequestHandler):
except Exception:
pass
# ★ v3 切模型桥接:把该大脑没见过的对话补进消息(平时增量 O(1),切线才补)。
# 落盘存爸爸原话_raw_me不含经验/桥接前缀),保证权威记录干净。
_raw_me = message
try:
if sid:
lane = "wb" if model in WB_MODELS else "deepseek"
message = _bridge_prepare(sid, lane, message)
except Exception:
pass
if model in WB_MODELS:
# WorkBuddy 免费积分大脑:走 codebuddy CLI不花 DeepSeek 余额)
acc = self._stream_workbuddy(message, model, sid, token, history)
else:
# DeepSeek 大脑:同样基于前端完整历史(与 WB 共享同一份记忆)
# DeepSeek 大脑:opencode session 增量续聊
acc = self._stream_opencode(message, model, sid, token, history)
if not acc:
acc = "(蛋蛋没回话,换个说法试试~)"
# ★ 对话落盘(零模型成本):两大脑共享的权威对话记录,切模型桥接 / 会话恢复都读它
try:
_conv_append(sid, "me", model, _raw_me)
_conv_append(sid, "ai", model, acc)
except Exception:
pass
self._event("done", {"text": acc, "session_id": sid, "compacted": False})
except (BrokenPipeError, ConnectionResetError):
_kill_active(token)