mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(web): support parallel sessions; fix lost/duplicate in-flight replies
This commit is contained in:
@@ -269,7 +269,9 @@ def run_evolution_for_session(
|
||||
new_messages = all_messages[done:]
|
||||
transcript = _build_transcript(new_messages)
|
||||
if not transcript.strip():
|
||||
logger.info(f"[Evolution] session={session_id}: no new messages, skip")
|
||||
# Routine no-op: the per-minute scan hits every idle session, so keep
|
||||
# this at debug to avoid spamming the log.
|
||||
logger.debug(f"[Evolution] session={session_id}: no new messages, skip")
|
||||
# Advance the cursor anyway so we don't re-scan the same tail.
|
||||
agent._evo_done_msg_count = total_msgs
|
||||
return False
|
||||
|
||||
@@ -120,11 +120,12 @@ them. When their signal is clear, act; do not be shy here.
|
||||
|
||||
- Nothing worth evolving -> output exactly `[SILENT]` and nothing else.
|
||||
- Otherwise, after performing the edits, output a short user-facing summary in
|
||||
the SAME LANGUAGE the user used in the conversation. Tell the user, briefly:
|
||||
the SAME LANGUAGE the user used. Write it for an ordinary user, in plain
|
||||
everyday words — NOT a developer report. No need to expose internal details
|
||||
(file names/paths, system mechanics, etc.). Tell the user, briefly:
|
||||
1) that you just did a self-learning pass,
|
||||
2) what you learned and what you changed (in plain terms — no need to cite
|
||||
exact file paths; "remembered X" / "improved the weekly-report skill" is
|
||||
enough).
|
||||
2) what you learned and what you changed in THIS pass ("remembered X" /
|
||||
"improved the <name> skill" / "finished <task>").
|
||||
Keep it to 1-3 lines. Generic shape (do not copy domain words):
|
||||
"I just did a self-learning pass.
|
||||
- Learned: <what you learned>
|
||||
|
||||
@@ -515,6 +515,15 @@ class AgentBridge:
|
||||
)
|
||||
self._trim_in_memory_to_turns(agent, scheduler_keep_turns)
|
||||
|
||||
# Eagerly persist the user message BEFORE running the agent so the
|
||||
# session and the user's bubble are immediately visible — even if
|
||||
# the user switches away or refreshes before the reply finishes.
|
||||
# The reply (assistant/tool messages) is appended once the run
|
||||
# completes; the final persist skips this already-stored user turn.
|
||||
pre_persisted = self._pre_persist_user_message(
|
||||
session_id, query, context, clear_history
|
||||
)
|
||||
|
||||
try:
|
||||
# Use agent's run_stream method with event handler
|
||||
response = agent.run_stream(
|
||||
@@ -541,7 +550,11 @@ class AgentBridge:
|
||||
# Persist new messages generated during this run
|
||||
if session_id:
|
||||
channel_type = (context.get("channel_type") or "") if context else ""
|
||||
new_messages = getattr(agent, '_last_run_new_messages', [])
|
||||
new_messages = list(getattr(agent, '_last_run_new_messages', []))
|
||||
# The leading user turn was already persisted eagerly above;
|
||||
# drop it here so it isn't stored twice.
|
||||
if pre_persisted and new_messages and new_messages[0].get("role") == "user":
|
||||
new_messages = new_messages[1:]
|
||||
if new_messages:
|
||||
self._persist_messages(session_id, list(new_messages), channel_type)
|
||||
else:
|
||||
@@ -757,6 +770,48 @@ class AgentBridge:
|
||||
except Exception as e:
|
||||
logger.warning(f"[AgentBridge] Failed to sync API keys: {e}")
|
||||
|
||||
def _pre_persist_user_message(
|
||||
self, session_id: str, query: str, context: Context, clear_history: bool
|
||||
) -> bool:
|
||||
"""Persist the user's message before the agent runs.
|
||||
|
||||
This makes a brand-new session (and the user's bubble) visible even if
|
||||
the reply hasn't finished — switching away or refreshing no longer
|
||||
loses the in-flight session. Returns True when the user turn was
|
||||
stored, so the caller can skip it in the post-run persist.
|
||||
|
||||
Best-effort: any failure is swallowed and reported as not-persisted.
|
||||
"""
|
||||
if not session_id or not query:
|
||||
return False
|
||||
# Only real user turns: skip scheduler-injected / scheduled-task runs.
|
||||
if session_id.startswith("scheduler_") or (
|
||||
context and context.get("is_scheduled_task")
|
||||
):
|
||||
return False
|
||||
try:
|
||||
from config import conf
|
||||
if not conf().get("conversation_persistence", True):
|
||||
return False
|
||||
from agent.memory import get_conversation_store
|
||||
store = get_conversation_store()
|
||||
# clear_history starts a fresh transcript: wipe the store first so
|
||||
# the eager user turn becomes seq 0, matching in-memory state.
|
||||
if clear_history:
|
||||
store.clear_session(session_id)
|
||||
channel_type = (context.get("channel_type") or "") if context else ""
|
||||
user_msg = {
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": query}],
|
||||
}
|
||||
store.append_messages(session_id, [user_msg], channel_type=channel_type)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[AgentBridge] Failed to pre-persist user message for session={session_id}: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def _persist_messages(
|
||||
self, session_id: str, new_messages: list, channel_type: str = ""
|
||||
) -> None:
|
||||
|
||||
@@ -772,7 +772,7 @@
|
||||
</button>
|
||||
<button id="memory-tab-dreams" onclick="switchMemoryTab('dreams')"
|
||||
class="memory-tab px-3 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-colors duration-150">
|
||||
<i class="fas fa-seedling mr-1.5"></i><span data-i18n="memory_tab_dreams">自主进化</span>
|
||||
<i class="fas fa-seedling mr-1.5"></i><span data-i18n="memory_tab_dreams">自我进化</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,7 @@ const I18N = {
|
||||
config_max_turns: '最大记忆轮次', config_max_turns_hint: '一问一答为一轮,超过后会智能压缩处理',
|
||||
config_max_steps: '最大执行步数', config_max_steps_hint: '单次对话中 Agent 最多调用工具的次数',
|
||||
config_enable_thinking: '深度思考', config_enable_thinking_hint: '是否启用深度思考模式',
|
||||
config_self_evolution: '自主进化', config_self_evolution_hint: '会话空闲后自动复盘,沉淀记忆、优化技能、处理未完成事项',
|
||||
config_self_evolution: '自我进化', config_self_evolution_hint: '会话空闲后自动复盘,沉淀记忆、优化技能、处理未完成事项',
|
||||
evolution_badge: '自主学习',
|
||||
config_channel_type: '通道类型',
|
||||
config_provider: '模型厂商', config_model_name: '模型',
|
||||
@@ -142,7 +142,7 @@ const I18N = {
|
||||
skills_section_title: '技能', skill_enable: '启用', skill_disable: '禁用',
|
||||
skill_toggle_error: '操作失败,请稍后再试',
|
||||
memory_title: '记忆管理', memory_desc: '查看 Agent 记忆文件和内容',
|
||||
memory_tab_files: '记忆文件', memory_tab_dreams: '自主进化',
|
||||
memory_tab_files: '记忆文件', memory_tab_dreams: '自我进化',
|
||||
memory_loading: '加载记忆文件中...', memory_loading_desc: '记忆文件将显示在此处',
|
||||
memory_back: '返回列表',
|
||||
memory_col_name: '文件名', memory_col_type: '类型', memory_col_size: '大小', memory_col_updated: '更新时间',
|
||||
@@ -883,6 +883,7 @@ let isPolling = false;
|
||||
let pollGeneration = 0; // incremented on each restart to cancel stale poll loops
|
||||
let loadingContainers = {};
|
||||
let activeStreams = {}; // request_id -> EventSource
|
||||
let sessionActiveRequest = {}; // session_id -> request_id (in-flight stream per session)
|
||||
let isComposing = false;
|
||||
let appConfig = { use_agent: false, title: 'CowAgent', subtitle: '', providers: {}, api_bases: {} };
|
||||
|
||||
@@ -2242,6 +2243,27 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
let reasoningStartTime = 0;
|
||||
let done = false;
|
||||
|
||||
// The session this stream belongs to. Sessions run in parallel: the user
|
||||
// may switch to another session while this one is still streaming. When
|
||||
// that happens the stream keeps running in the background (so the reply
|
||||
// still completes and persists) but must NOT touch the now-foreign view.
|
||||
// On return, the completed reply is loaded from the DB via loadHistory.
|
||||
const ownerSession = sessionId;
|
||||
// Once the user navigates away, this stream is permanently "detached":
|
||||
// its bubble was wiped from the DOM by the switch, so it must never render
|
||||
// again — even if the user returns to this session. It keeps running to
|
||||
// finish + persist the reply; the completed reply is then surfaced via
|
||||
// history reload / polling. This avoids re-creating a mangled bubble that
|
||||
// starts mid-stream.
|
||||
let detached = false;
|
||||
const isActive = () => ownerSession === sessionId && !detached;
|
||||
sessionActiveRequest[ownerSession] = requestId;
|
||||
const clearOwnerRequest = () => {
|
||||
if (sessionActiveRequest[ownerSession] === requestId) {
|
||||
delete sessionActiveRequest[ownerSession];
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_RECONNECTS = 10;
|
||||
const RECONNECT_BASE_MS = 1000;
|
||||
let reconnectCount = 0;
|
||||
@@ -2294,6 +2316,23 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
// Successful data received, reset reconnect counter
|
||||
reconnectCount = 0;
|
||||
|
||||
// Background session: keep the stream alive so the reply finishes
|
||||
// and persists, but skip all UI rendering for the foreign view.
|
||||
// Once a stream has rendered into a view that later got cleared
|
||||
// (the user switched away), mark it detached for good so it never
|
||||
// tries to render into a rebuilt/foreign view — switching back
|
||||
// shows the finished reply via history reload / polling instead.
|
||||
if (ownerSession !== sessionId || detached) {
|
||||
detached = true;
|
||||
if (item.type === 'done' || item.type === 'error' || item.type === 'voice_attach') {
|
||||
done = true;
|
||||
es.close();
|
||||
delete activeStreams[requestId];
|
||||
clearOwnerRequest();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.type === 'reasoning') {
|
||||
ensureBotEl();
|
||||
reasoningText += item.content;
|
||||
@@ -2507,6 +2546,7 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
// TTS audio (`voice_attach`). It will close the stream on
|
||||
// its own via onerror once the tail expires.
|
||||
done = true;
|
||||
clearOwnerRequest();
|
||||
resetSendBtnSendMode();
|
||||
|
||||
const finalTextRaw = item.content || accumulatedText;
|
||||
@@ -2564,11 +2604,13 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
}
|
||||
es.close();
|
||||
delete activeStreams[requestId];
|
||||
clearOwnerRequest();
|
||||
|
||||
} else if (item.type === 'error') {
|
||||
done = true;
|
||||
es.close();
|
||||
delete activeStreams[requestId];
|
||||
clearOwnerRequest();
|
||||
if (loadingEl) { loadingEl.remove(); loadingEl = null; }
|
||||
addBotMessage(t('error_send'), new Date());
|
||||
resetSendBtnSendMode();
|
||||
@@ -2598,7 +2640,10 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Exhausted retries, show whatever we have
|
||||
// Exhausted retries. Only surface the failure in the owning view —
|
||||
// a background session must not mutate the currently shown chat.
|
||||
clearOwnerRequest();
|
||||
if (!isActive()) return;
|
||||
if (loadingEl) { loadingEl.remove(); loadingEl = null; }
|
||||
if (!botEl) {
|
||||
addBotMessage(t('error_send'), new Date());
|
||||
@@ -2641,10 +2686,19 @@ function startPolling() {
|
||||
loadingContainers[rid].remove();
|
||||
delete loadingContainers[rid];
|
||||
}
|
||||
const welcomeScreen = document.getElementById('welcome-screen');
|
||||
if (welcomeScreen) welcomeScreen.remove();
|
||||
addBotMessage(data.content, new Date(data.timestamp * 1000), rid);
|
||||
scrollChatToBottom();
|
||||
// Skip if this reply is already on screen. Happens when a reply
|
||||
// arrives via both the SSE stream and the poll queue (e.g. the
|
||||
// user switched away mid-run, leaving the queued reply to be
|
||||
// re-fetched on return) — render it only once.
|
||||
const already = rid && messagesDiv.querySelector(
|
||||
`[data-request-id="${rid}"]`
|
||||
);
|
||||
if (!already) {
|
||||
const welcomeScreen = document.getElementById('welcome-screen');
|
||||
if (welcomeScreen) welcomeScreen.remove();
|
||||
addBotMessage(data.content, new Date(data.timestamp * 1000), rid);
|
||||
scrollChatToBottom();
|
||||
}
|
||||
}
|
||||
const delay = (data.status === 'success' && data.has_content) ? 5000 : 10000;
|
||||
setTimeout(poll, delay);
|
||||
@@ -2889,8 +2943,11 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
|
||||
}
|
||||
|
||||
// Self-evolution bubbles get a small badge so the user can feel the agent
|
||||
// learned something on its own (text itself stays clean).
|
||||
const evolutionBadge = (msg && msg.kind === 'evolution')
|
||||
// learned something on its own (text itself stays clean). History replay
|
||||
// carries msg.kind; live pushes are identified by the evolution_ request id.
|
||||
const isEvolution = (msg && msg.kind === 'evolution')
|
||||
|| (typeof requestId === 'string' && requestId.startsWith('evolution_'));
|
||||
const evolutionBadge = isEvolution
|
||||
? `<div class="flex items-center gap-1 mb-1.5 text-xs text-slate-400 dark:text-slate-500">
|
||||
<i class="fas fa-seedling text-[11px]"></i>
|
||||
<span>${t('evolution_badge')}</span>
|
||||
@@ -3205,14 +3262,17 @@ function addLoadingIndicator() {
|
||||
}
|
||||
|
||||
function newChat() {
|
||||
// Close all active SSE connections for the current session
|
||||
Object.values(activeStreams).forEach(es => { try { es.close(); } catch (_) {} });
|
||||
activeStreams = {};
|
||||
// Do NOT close active streams: other sessions keep streaming in the
|
||||
// background (each stream self-guards against the foreign view) and their
|
||||
// replies still complete and persist.
|
||||
|
||||
// Stop any pending-reply poller from the previous session.
|
||||
if (_pendingReplyTimer) { clearTimeout(_pendingReplyTimer); _pendingReplyTimer = null; }
|
||||
|
||||
// Generate a fresh session and persist it so the next page load also starts clean
|
||||
sessionId = generateSessionId();
|
||||
localStorage.setItem(SESSION_ID_KEY, sessionId);
|
||||
loadingContainers = {};
|
||||
resetSendBtnSendMode(); // fresh session has no in-flight reply
|
||||
startPolling(); // bump generation so old loop self-cancels, new loop uses fresh sessionId
|
||||
messagesDiv.innerHTML = '';
|
||||
const ws = document.createElement('div');
|
||||
@@ -3547,15 +3607,48 @@ function _onSessionListScroll() {
|
||||
}
|
||||
})();
|
||||
|
||||
// When returning to a session whose reply is still streaming in the
|
||||
// background, the reply isn't in history yet. Show a loading indicator and
|
||||
// reload history until the reply has persisted (stream cleared) or the user
|
||||
// navigates away. Stable bubbles are de-duplicated by request_id on reload.
|
||||
let _pendingReplyTimer = null;
|
||||
function _waitForPendingReply(sid) {
|
||||
if (_pendingReplyTimer) { clearTimeout(_pendingReplyTimer); _pendingReplyTimer = null; }
|
||||
// The user turn is already in history (persisted eagerly); show a loading
|
||||
// hint beneath it while the reply streams in the background.
|
||||
addLoadingIndicator();
|
||||
|
||||
function tick() {
|
||||
// User switched away — abandon; the other session manages its own view.
|
||||
if (sid !== sessionId) return;
|
||||
if (sessionActiveRequest[sid]) {
|
||||
// Still streaming — keep waiting without touching the view.
|
||||
_pendingReplyTimer = setTimeout(tick, 1500);
|
||||
return;
|
||||
}
|
||||
// Reply finished and persisted: rebuild the view from history once.
|
||||
// loadHistory only prepends, so clear first to avoid duplicates.
|
||||
historyPage = 0;
|
||||
historyLoading = false;
|
||||
messagesDiv.innerHTML = '';
|
||||
loadHistory(1);
|
||||
}
|
||||
_pendingReplyTimer = setTimeout(tick, 1500);
|
||||
}
|
||||
|
||||
function switchSession(newSessionId) {
|
||||
if (newSessionId === sessionId) {
|
||||
if (currentView !== 'chat') navigateTo('chat');
|
||||
return;
|
||||
}
|
||||
|
||||
Object.values(activeStreams).forEach(es => { try { es.close(); } catch (_) {} });
|
||||
activeStreams = {};
|
||||
loadingContainers = {};
|
||||
// Stop any pending-reply poller from the session we're leaving.
|
||||
if (_pendingReplyTimer) { clearTimeout(_pendingReplyTimer); _pendingReplyTimer = null; }
|
||||
|
||||
// Do NOT close active streams here: sessions run in parallel, so any
|
||||
// in-flight reply for another session must keep streaming in the
|
||||
// background (it self-guards against rendering into the foreign view).
|
||||
// The reply completes and persists; switching back loads it from history.
|
||||
|
||||
sessionId = newSessionId;
|
||||
localStorage.setItem(SESSION_ID_KEY, sessionId);
|
||||
@@ -3568,6 +3661,18 @@ function switchSession(newSessionId) {
|
||||
loadHistory(1);
|
||||
startPolling();
|
||||
|
||||
// Restore the send button to match this session's stream state: if it has
|
||||
// an in-flight reply, show Cancel; otherwise show Send. A reply that is
|
||||
// still streaming in the background won't be in history yet, so poll the
|
||||
// history until it lands.
|
||||
const pendingReq = sessionActiveRequest[sessionId];
|
||||
if (pendingReq) {
|
||||
setSendBtnCancelMode(pendingReq);
|
||||
_waitForPendingReply(sessionId);
|
||||
} else {
|
||||
resetSendBtnSendMode();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.session-item').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.sessionId === sessionId);
|
||||
});
|
||||
|
||||
@@ -360,6 +360,13 @@ class WebChannel(ChatChannel):
|
||||
):
|
||||
logger.debug(f"Polling skipped duplicate file reply for session {session_id}")
|
||||
return
|
||||
# SSE-enabled requests already stream the text reply to the
|
||||
# client. Do NOT also enqueue it for polling: if the user
|
||||
# switched away mid-run, the queued copy would resurface as a
|
||||
# duplicate bubble when they return and poll the session.
|
||||
if reply.type == ReplyType.TEXT and context.get("on_event") is not None:
|
||||
logger.debug(f"Polling skipped SSE text reply for session {session_id}")
|
||||
return
|
||||
response_data = {
|
||||
"type": str(reply.type),
|
||||
"content": content,
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
"group": "发布记录",
|
||||
"pages": [
|
||||
"zh/releases/overview",
|
||||
"zh/releases/v2.1.1",
|
||||
"zh/releases/v2.1.0",
|
||||
"zh/releases/v2.0.9",
|
||||
"zh/releases/v2.0.8",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 自主进化
|
||||
title: 自我进化
|
||||
description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优化技能、处理未完成事项
|
||||
---
|
||||
|
||||
@@ -7,13 +7,13 @@ description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优
|
||||
|
||||
### 简介
|
||||
|
||||
自主进化(Self-Evolution)让 Agent 不止于"完成单次任务",而是能在与你的相处中持续成长。每段对话告一段落后,它会自动"回头复盘"一次:把值得记住的沉淀为长期记忆、把使用中暴露的问题修进技能、把没做完的事情接着推进。久而久之,Agent 会越来越懂你的偏好、越来越少重复犯错、越来越主动地把事情收尾,而这一切都在后台静默完成,只有真正做了事情时才会简短地告诉你。
|
||||
自我进化(Self-Evolution)让 Agent 不止于"完成单次任务",而是能在与你的相处中持续成长。每段对话告一段落后,它会自动"回头复盘"一次:把值得记住的沉淀为长期记忆、把使用中暴露的问题修进技能、把没做完的事情接着推进。久而久之,Agent 会越来越懂你的偏好、越来越少重复犯错、越来越主动地把事情收尾,而这一切都在后台静默完成,只有真正做了事情时才会简短地告诉你。
|
||||
|
||||
> 它与[梦境蒸馏](/zh/memory/deep-dream)互补:梦境蒸馏负责整理记忆本身,自主进化则在记忆之外,进一步优化技能、推进未完成的任务,让 Agent 的能力随使用不断打磨。
|
||||
> 它与[梦境蒸馏](/zh/memory/deep-dream)互补:梦境蒸馏负责整理记忆本身,自我进化则在记忆之外,进一步优化技能、推进未完成的任务,让 Agent 的能力随使用不断打磨。
|
||||
|
||||
### 三个目标
|
||||
|
||||
自主进化围绕三件事工作:
|
||||
自我进化围绕三件事工作:
|
||||
|
||||
| 目标 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -27,7 +27,7 @@ description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优
|
||||
|
||||
### 触发时机
|
||||
|
||||
自主进化不是定时执行,而是在**一段对话自然结束、进入空闲后**才触发,避免打断正在进行的交流。需要同时满足:
|
||||
自我进化不是定时执行,而是在**一段对话自然结束、进入空闲后**才触发,避免打断正在进行的交流。需要同时满足:
|
||||
|
||||
- **对话已空闲** — 距离最后一次互动超过设定的空闲时长(默认 15 分钟)
|
||||
- **对话有足够内容** — 自上次进化以来累积了足够轮次,或上下文已接近容量上限
|
||||
@@ -36,11 +36,11 @@ description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优
|
||||
|
||||
### 相关配置
|
||||
|
||||
自主进化默认关闭,可在 Web 控制台「配置 → Agent 配置」中通过开关启用(位于"深度思考"下方),也可在配置文件中调整:
|
||||
自我进化默认关闭,可在 Web 控制台「配置 → Agent 配置」中通过开关启用(位于"深度思考"下方),也可在配置文件中调整:
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| `self_evolution_enabled` | 是否启用自主进化 | `false` |
|
||||
| `self_evolution_enabled` | 是否启用自我进化 | `false` |
|
||||
| `self_evolution_idle_minutes` | 对话空闲多久后触发(分钟) | `15` |
|
||||
| `self_evolution_min_turns` | 触发所需的最少对话轮次 | `6` |
|
||||
|
||||
@@ -50,7 +50,7 @@ description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优
|
||||
|
||||
### 进化记录
|
||||
|
||||
每次进化的过程和结果会按日期记录在 `memory/evolution/YYYY-MM-DD.md` 中,可在 Web 控制台的「记忆管理 → 自主进化」tab 中查看。该 tab 同时汇总了自主进化记录与梦境日记,方便统一回顾 Agent 的成长轨迹。
|
||||
每次进化的过程和结果会按日期记录在 `memory/evolution/YYYY-MM-DD.md` 中,可在 Web 控制台的「记忆管理 → 自我进化」tab 中查看。该 tab 同时汇总了自我进化记录与梦境日记,方便统一回顾 Agent 的成长轨迹。
|
||||
|
||||
### 如何回滚
|
||||
|
||||
@@ -58,7 +58,7 @@ description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优
|
||||
|
||||
## 实现设计
|
||||
|
||||
自主进化复用了系统已有的能力,保持轻量:
|
||||
自我进化复用了系统已有的能力,保持轻量:
|
||||
|
||||
- **隔离执行**:每次复盘都启动一个独立的、临时的复盘任务,使用与主对话相同的模型,但拥有受限的工具集(只能读上下文、改记忆与技能文件)。它不会污染主对话的上下文,也不会影响主对话的性能。
|
||||
- **基于备份的撤销**:进化前对相关文件做快照备份,撤销时按备份还原,因此每一次改动都可追溯、可逆。
|
||||
@@ -66,7 +66,7 @@ description: Self-Evolution — 会话空闲后自动复盘,沉淀记忆、优
|
||||
|
||||
### 克制与安全
|
||||
|
||||
自主进化的设计原则是"必要时执行,减少打扰":
|
||||
自我进化的设计原则是"必要时执行,减少打扰":
|
||||
|
||||
| 机制 | 说明 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -5,6 +5,7 @@ description: CowAgent 版本更新历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| [2.1.1](/zh/releases/v2.1.1) | 2026.06.08 | 自进化能力、Web 控制台消息管理升级、MCP 跨平台增强与并发调用、新模型接入(MiniMax-M3、qwen3.7-plus 等)、多项优化 |
|
||||
| [2.1.0](/zh/releases/v2.1.0) | 2026.06.01 | 国际化支持、新增 Telegram / Discord / Slack / 微信客服通道、命令行交互升级(流式输出、命令模糊匹配、任务取消)、MCP Streamable HTTP、新模型接入 |
|
||||
| [2.0.9](/zh/releases/v2.0.9) | 2026.05.22 | 新增模型管理、MCP 协议支持、浏览器登录态持久化、新模型接入(gpt-5.5、gemini-3.5-flash、qwen3.7-max 等)、部署安全加固 |
|
||||
| [2.0.8](/zh/releases/v2.0.8) | 2026.05.06 | 飞书渠道全面升级(语音、流式输出和Markdown、扫码一键接入)、DeepSeek V4和百度模型新增、定时任务工具增强 |
|
||||
|
||||
61
docs/zh/releases/v2.1.1.mdx
Normal file
61
docs/zh/releases/v2.1.1.mdx
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: v2.1.1
|
||||
description: CowAgent 2.1.1 - 自进化能力、Web 控制台消息管理升级、MCP 跨平台增强、多模型接入与优化
|
||||
---
|
||||
|
||||
🌐 [English](https://docs.cowagent.ai/releases/v2.1.1) | [中文](https://docs.cowagent.ai/zh/releases/v2.1.1)
|
||||
|
||||
## 🧬 自进化能力
|
||||
|
||||
CowAgent 新增**自进化(Self-Evolution)**能力,让 Agent 不止于完成单次任务,而是在与你的日常协作中持续成长:
|
||||
|
||||
- **自动复盘成长**:一段对话自然结束并进入空闲后,Agent 会在后台悄悄回顾刚刚的对话,沉淀值得记住的偏好和事实、修复技能中暴露的问题、并补做未完成的任务
|
||||
- **越用越懂你**:随着使用,Agent 会逐渐记住你的习惯、减少重复犯错,在收尾上做得更好
|
||||
- **不打扰原则**:只有在确实做出了改动时,才用一句话告诉你它学到了什么、调整了什么,没有变化时全程静默
|
||||
- **安全可回退**:每次复盘前都会自动备份,对结果不满意时只需在对话中让它撤销即可恢复;内置技能受保护、所有改动均限制在工作空间内
|
||||
|
||||
默认关闭,可在 Web 控制台 **设置 → Agent 配置** 中一键开启。
|
||||
|
||||
相关文档:[自进化](https://docs.cowagent.ai/zh/memory/self-evolution)
|
||||
|
||||
## 💬 Web 控制台消息管理升级
|
||||
|
||||
Web 控制台的聊天体验进一步增强,操作更接近主流聊天产品:
|
||||
|
||||
- **消息编辑与重发**:用户与机器人的消息均支持编辑、删除、重新生成
|
||||
- **代码块更易用**:代码块新增语言标签和一键复制按钮
|
||||
- **拖拽体验优化**:支持将文件拖拽到整个聊天区域
|
||||
|
||||
Thanks @core-power (#2865)
|
||||
|
||||
## 🧩 MCP 跨平台增强
|
||||
|
||||
- **Windows 兼容修复**:修复 MCP 在 Windows 下 `stdio` 通信不可用的问题,并支持通过 `mcp.json` 配置服务超时时间
|
||||
- **并发调用支持**:`sse` 与 `streamable-http` 传输支持跨会话并发调用,多工具响应更快
|
||||
|
||||
Thanks @xliu123321 (#2859)
|
||||
|
||||
相关文档:[MCP 工具](https://docs.cowagent.ai/zh/tools/mcp)
|
||||
|
||||
## 🤖 模型新增与优化
|
||||
|
||||
- **MiniMax-M3**:新增并设为默认模型,保留 M2.7 系列作为可选项。Thanks @octo-patch (#2855)
|
||||
- **通义千问 qwen3.7-plus**:支持多模态对话
|
||||
- **语音识别模型可选**:Web 控制台支持选择 ASR(语音识别)模型并持久化保存。Thanks @nightwhite (#2857)
|
||||
- **安装菜单简化**:一键安装脚本精简模型选择菜单,新增小米 MiMo 选项
|
||||
|
||||
相关文档:[模型概览](https://docs.cowagent.ai/zh/models)
|
||||
|
||||
## 🛠 体验优化与修复
|
||||
|
||||
- **国际化体验**:通道列表按界面语言排序展示;优化 `auto` 模式下的语言自动回退逻辑
|
||||
- **任务取消更可靠**:修复部分场景下流式回复无法中断的问题
|
||||
- **CLI 增强**:`cow status` 新增显示当前项目路径
|
||||
- **部署安全加固**:凭证文件拦截范围收敛至 `~/.cow/.env`,不再误拦其他目录(Thanks @orbisai0security #2863);微信公众号在 `wechatmp_token` 为空时拒绝 Webhook 请求
|
||||
- **群任务看板插件**:内置群任务看板插件源。Thanks @Wyh-max-star (#2853)
|
||||
|
||||
## 📦 升级方式
|
||||
|
||||
源码部署可执行 `cow update` 一键升级,或手动拉取代码后重启。详见 [更新升级文档](https://docs.cowagent.ai/zh/guide/upgrade)。
|
||||
|
||||
**发布日期**:2026.06.08 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.1.0...2.1.1)
|
||||
Reference in New Issue
Block a user