mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 18:17:11 +08:00
Compare commits
10 Commits
feat-mulit
...
2.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60e9d98d0a | ||
|
|
83f6625e0c | ||
|
|
acc09543b7 | ||
|
|
94d8c7e366 | ||
|
|
ea1a0c8b3d | ||
|
|
7bc88c17e4 | ||
|
|
33cf1bc4c3 | ||
|
|
9402e63fe1 | ||
|
|
da97e948ca | ||
|
|
89a07e8e74 |
@@ -23,7 +23,7 @@
|
||||
> 该项目既是一个可以开箱即用的超级 AI 助理,也是一个支持高扩展的 Agent 框架,可以通过为项目扩展大模型接口、接入渠道、内置工具、Skills 系统来灵活实现各种定制需求。核心能力如下:
|
||||
|
||||
- ✅ **自主任务规划**:能够理解复杂任务并自主规划执行,持续思考和调用工具直到完成目标
|
||||
- ✅ **长期记忆:** 自动将对话记忆持久化至本地文件和数据库中,包括核心记忆和日级记忆,支持关键词及向量检索
|
||||
- ✅ **长期记忆:** 自动将对话记忆持久化至本地文件和数据库中,包括核心记忆、日级记忆和梦境蒸馏,支持关键词及向量检索
|
||||
- ✅ **个人知识库:** 自动整理结构化知识,通过交叉引用构建知识图谱,支持通过对话管理和可视化浏览知识库
|
||||
- ✅ **技能系统:** Skills 安装和运行的引擎,支持从 [Skill Hub](https://skills.cowagent.ai/)、GitHub 等一键安装技能,或通过对话创造 Skills
|
||||
- ✅ **工具系统:** 内置文件读写、终端执行、浏览器操作、定时任务等工具,Agent 自主调用以完成复杂任务
|
||||
@@ -70,6 +70,8 @@
|
||||
|
||||
# 🏷 更新日志
|
||||
|
||||
>**2026.04.14:** [2.0.6版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.6),知识库系统、梦境记忆模块、上下文智能压缩、Web 控制台多会话及多项优化。
|
||||
|
||||
>**2026.04.01:** [2.0.5版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.5),Cow CLI 命令系统、Skill Hub 开源、浏览器工具、企微扫码创建、多项优化和修复。
|
||||
|
||||
>**2026.03.22:** [2.0.4版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.4),新增个人微信通道(微信扫码即用)、新增 MiniMax-M2.7 和 GLM-5-Turbo 模型、run.sh 脚本重构、日文文档及多项修复。
|
||||
@@ -117,7 +119,7 @@ irm https://cdn.link-ai.tech/code/cow/run.ps1 | iex
|
||||
|
||||
### 2.环境安装
|
||||
|
||||
支持 Linux、MacOS、Windows 操作系统,可在个人计算机及服务器上运行,需安装 `Python`,Python 版本需在3.7 ~ 3.12 之间。
|
||||
支持 Linux、MacOS、Windows 操作系统,可在个人计算机及服务器上运行,需安装 `Python`,Python 版本需在 3.7 ~ 3.13 之间。
|
||||
|
||||
> 注意:Agent 模式推荐使用源码运行,若选择 Docker 部署则无需安装 python 环境和下载源码,可直接快进到下一节。
|
||||
|
||||
@@ -203,7 +205,8 @@ cow install-browser
|
||||
"agent_workspace": "~/cow", # Agent 的工作空间路径,用于存储 memory、skills、系统设定等
|
||||
"agent_max_context_tokens": 50000, # Agent 模式下最大上下文 tokens,超出将自动智能压缩处理
|
||||
"agent_max_context_turns": 20, # Agent 模式下最大上下文记忆轮次,一问一答为一轮,超出后智能压缩处理
|
||||
"agent_max_steps": 20 # Agent 模式下单次任务的最大决策步数,超出后将停止继续调用工具
|
||||
"agent_max_steps": 20, # Agent 模式下单次任务的最大决策步数,超出后将停止继续调用工具
|
||||
"enable_thinking": true # 是否启用深度思考,开启后 Web 端展示模型推理过程,关闭后可加速响应
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -401,24 +401,28 @@ class MemoryManager:
|
||||
user_id: Optional[str] = None,
|
||||
reason: str = "threshold",
|
||||
max_messages: int = 10,
|
||||
context_summary_callback=None,
|
||||
) -> bool:
|
||||
"""
|
||||
Flush conversation summary to daily memory file.
|
||||
|
||||
|
||||
Args:
|
||||
messages: Conversation message list
|
||||
user_id: Optional user ID
|
||||
reason: "threshold" | "overflow" | "daily_summary"
|
||||
max_messages: Max recent messages to include (0 = all)
|
||||
|
||||
context_summary_callback: Optional callback(str) invoked with the
|
||||
daily summary text for in-context injection
|
||||
|
||||
Returns:
|
||||
True if content was written
|
||||
True if flush was dispatched
|
||||
"""
|
||||
success = self.flush_manager.flush_from_messages(
|
||||
messages=messages,
|
||||
user_id=user_id,
|
||||
reason=reason,
|
||||
max_messages=max_messages,
|
||||
context_summary_callback=context_summary_callback,
|
||||
)
|
||||
if success:
|
||||
self._dirty = True
|
||||
|
||||
@@ -32,68 +32,80 @@ class MemoryService:
|
||||
# ------------------------------------------------------------------
|
||||
# list — paginated file metadata
|
||||
# ------------------------------------------------------------------
|
||||
def list_files(self, page: int = 1, page_size: int = 20) -> dict:
|
||||
def list_files(self, page: int = 1, page_size: int = 20, category: str = "memory") -> dict:
|
||||
"""
|
||||
List all memory files with metadata (without content).
|
||||
List memory or dream files with metadata (without content).
|
||||
|
||||
Returns::
|
||||
|
||||
{
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 15,
|
||||
"list": [
|
||||
{"filename": "MEMORY.md", "type": "global", "size": 2048, "updated_at": "2026-02-20 10:00:00"},
|
||||
{"filename": "2026-02-20.md", "type": "daily", "size": 512, "updated_at": "2026-02-20 09:30:00"},
|
||||
...
|
||||
]
|
||||
}
|
||||
Args:
|
||||
category: ``"memory"`` (default) — MEMORY.md + daily files;
|
||||
``"dream"`` — dream diary files from memory/dreams/
|
||||
"""
|
||||
if category == "dream":
|
||||
files = self._list_dream_files()
|
||||
else:
|
||||
files = self._list_memory_files()
|
||||
|
||||
total = len(files)
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
|
||||
return {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"list": files[start:end],
|
||||
}
|
||||
|
||||
def _list_memory_files(self) -> List[dict]:
|
||||
"""MEMORY.md + memory/*.md (newest first)."""
|
||||
files: List[dict] = []
|
||||
|
||||
# 1. Global memory — MEMORY.md in workspace root
|
||||
global_path = os.path.join(self.workspace_root, "MEMORY.md")
|
||||
if os.path.isfile(global_path):
|
||||
files.append(self._file_info(global_path, "MEMORY.md", "global"))
|
||||
|
||||
# 2. Daily memory files — memory/*.md (sorted newest first)
|
||||
if os.path.isdir(self.memory_dir):
|
||||
daily_files = []
|
||||
for name in os.listdir(self.memory_dir):
|
||||
full = os.path.join(self.memory_dir, name)
|
||||
if os.path.isfile(full) and name.endswith(".md"):
|
||||
daily_files.append((name, full))
|
||||
# Sort by filename descending (newest date first)
|
||||
daily_files.sort(key=lambda x: x[0], reverse=True)
|
||||
for name, full in daily_files:
|
||||
files.append(self._file_info(full, name, "daily"))
|
||||
|
||||
total = len(files)
|
||||
return files
|
||||
|
||||
# Paginate
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
page_items = files[start:end]
|
||||
def _list_dream_files(self) -> List[dict]:
|
||||
"""memory/dreams/*.md (newest first)."""
|
||||
files: List[dict] = []
|
||||
dreams_dir = os.path.join(self.memory_dir, "dreams")
|
||||
|
||||
return {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"list": page_items,
|
||||
}
|
||||
if os.path.isdir(dreams_dir):
|
||||
entries = []
|
||||
for name in os.listdir(dreams_dir):
|
||||
full = os.path.join(dreams_dir, name)
|
||||
if os.path.isfile(full) and name.endswith(".md"):
|
||||
entries.append((name, full))
|
||||
entries.sort(key=lambda x: x[0], reverse=True)
|
||||
for name, full in entries:
|
||||
files.append(self._file_info(full, name, "dream"))
|
||||
|
||||
return files
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# content — read a single file
|
||||
# ------------------------------------------------------------------
|
||||
def get_content(self, filename: str) -> dict:
|
||||
def get_content(self, filename: str, category: str = "memory") -> dict:
|
||||
"""
|
||||
Read the full content of a memory file.
|
||||
Read the full content of a memory or dream file.
|
||||
|
||||
:param filename: File name, e.g. ``MEMORY.md`` or ``2026-02-20.md``
|
||||
:param filename: File name, e.g. ``MEMORY.md``, ``2026-02-20.md``
|
||||
:param category: ``"memory"`` or ``"dream"``
|
||||
:return: dict with ``filename`` and ``content``
|
||||
:raises FileNotFoundError: if the file does not exist
|
||||
"""
|
||||
path = self._resolve_path(filename)
|
||||
path = self._resolve_path(filename, category)
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"Memory file not found: {filename}")
|
||||
|
||||
@@ -113,7 +125,7 @@ class MemoryService:
|
||||
Dispatch a memory management action.
|
||||
|
||||
:param action: ``list`` or ``content``
|
||||
:param payload: action-specific payload
|
||||
:param payload: action-specific payload (supports ``category``: ``"memory"`` | ``"dream"``)
|
||||
:return: protocol-compatible response dict
|
||||
"""
|
||||
payload = payload or {}
|
||||
@@ -121,14 +133,16 @@ class MemoryService:
|
||||
if action == "list":
|
||||
page = payload.get("page", 1)
|
||||
page_size = payload.get("page_size", 20)
|
||||
result_payload = self.list_files(page=page, page_size=page_size)
|
||||
category = payload.get("category", "memory")
|
||||
result_payload = self.list_files(page=page, page_size=page_size, category=category)
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result_payload}
|
||||
|
||||
elif action == "content":
|
||||
filename = payload.get("filename")
|
||||
if not filename:
|
||||
return {"action": action, "code": 400, "message": "filename is required", "payload": None}
|
||||
result_payload = self.get_content(filename)
|
||||
category = payload.get("category", "memory")
|
||||
result_payload = self.get_content(filename, category=category)
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result_payload}
|
||||
|
||||
else:
|
||||
@@ -145,18 +159,20 @@ class MemoryService:
|
||||
# ------------------------------------------------------------------
|
||||
# internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _resolve_path(self, filename: str) -> str:
|
||||
def _resolve_path(self, filename: str, category: str = "memory") -> str:
|
||||
"""
|
||||
Safely resolve a filename to its absolute path within the allowed directory.
|
||||
|
||||
- ``MEMORY.md`` → ``{workspace_root}/MEMORY.md``
|
||||
- ``2026-02-20.md`` → ``{workspace_root}/memory/2026-02-20.md``
|
||||
- ``2026-02-20.md`` (memory) → ``{workspace_root}/memory/2026-02-20.md``
|
||||
- ``2026-02-20.md`` (dream) → ``{workspace_root}/memory/dreams/2026-02-20.md``
|
||||
|
||||
Raises ValueError if the resolved path escapes the allowed directory
|
||||
(path traversal protection).
|
||||
Raises ValueError if the resolved path escapes the allowed directory.
|
||||
"""
|
||||
if filename == "MEMORY.md":
|
||||
base_dir = self.workspace_root
|
||||
elif category == "dream":
|
||||
base_dir = os.path.join(self.memory_dir, "dreams")
|
||||
else:
|
||||
base_dir = self.memory_dir
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""
|
||||
Memory flush manager (with Light Dream)
|
||||
Memory flush manager with Deep Dream distillation
|
||||
|
||||
Handles memory persistence when conversation context is trimmed or overflows:
|
||||
- Uses LLM to summarize discarded messages into concise key-information entries
|
||||
- Uses LLM to summarize discarded messages into concise daily records
|
||||
- Writes to daily memory files (lazy creation)
|
||||
- Light Dream: extracts long-term memories to MEMORY.md in the same LLM call
|
||||
- Deduplicates trim flushes to avoid repeated writes
|
||||
- Runs summarization asynchronously to avoid blocking normal replies
|
||||
- Provides daily summary interface for scheduler
|
||||
- Deep Dream: periodically distills daily memories → refined MEMORY.md + dream diary
|
||||
"""
|
||||
|
||||
import threading
|
||||
@@ -17,43 +16,77 @@ from datetime import datetime
|
||||
from common.log import logger
|
||||
|
||||
|
||||
SUMMARIZE_SYSTEM_PROMPT = """你是一个记忆提取助手。你的任务是从对话记录中提炼出两种记忆:
|
||||
SUMMARIZE_SYSTEM_PROMPT = """你是一个对话记录助手。请将对话内容归纳为当天的日常记录。
|
||||
|
||||
## 第一部分:日常记录([DAILY])
|
||||
## 要求
|
||||
|
||||
按「事件」维度归纳当天发生的事,不要按对话轮次逐条记录:
|
||||
按「事件」维度归纳发生的事,不要按对话轮次逐条记录:
|
||||
- 每条一行,用 "- " 开头
|
||||
- 合并同一件事的多轮对话
|
||||
- 只记录有意义的事件,忽略闲聊和问候
|
||||
- 保留关键的决策、结论和待办事项
|
||||
|
||||
## 第二部分:长期记忆([MEMORY])
|
||||
当对话没有任何记录价值(仅含问候或无意义内容),直接回复"无"。"""
|
||||
|
||||
提取值得**永久记住**的关键信息,这些信息在未来的对话中仍然有价值:
|
||||
- 用户的偏好、习惯、风格(如"用户偏好中文回复"、"用户喜欢简洁风格")
|
||||
- 重要的决策或约定(如"项目决定使用 PostgreSQL")
|
||||
- 关键人物信息(如"张总是用户的上级")
|
||||
- 用户明确要求记住的内容
|
||||
- 重要的教训或经验总结
|
||||
SUMMARIZE_USER_PROMPT = """请归纳以下对话的日常记录:
|
||||
|
||||
**如果没有值得永久记住的信息,[MEMORY] 部分留空即可。**
|
||||
{conversation}"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deep Dream prompts — distill daily memories → MEMORY.md + dream diary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DREAM_SYSTEM_PROMPT = """你是一个记忆整理助手,负责定期整理用户的长期记忆。
|
||||
|
||||
你将收到两份材料:
|
||||
1. **当前长期记忆** — MEMORY.md 的全部现有内容
|
||||
2. **今日日记** — 当天的日常记录
|
||||
|
||||
MEMORY.md 会注入每次对话的系统提示词中,因此必须保持精炼,只存放有价值和值得记忆的内容。
|
||||
|
||||
**重要:只能基于提供的材料进行整理,严禁编造、推测或添加材料中不存在的信息。**
|
||||
|
||||
## 任务
|
||||
|
||||
### Part 1: 更新后的长期记忆([MEMORY])
|
||||
|
||||
在现有记忆基础上进行整理和提炼,输出完整的更新后内容:
|
||||
- **合并提炼**:将含义相近的多条合并为一条高密度表述,而非简单罗列
|
||||
- **新增萃取**:从今日日记中提取值得永久记住的新信息(偏好、决策、人物、规则、经验)
|
||||
- **冲突更新**:当新信息与旧条目矛盾时,以新信息为准,替换旧条目
|
||||
- **清理无效**:删除临时性记录、空白条目、格式残留、无意义、重复内容等
|
||||
- **删除冗余**:已被更精炼表述涵盖的旧条目应删除,避免信息重复
|
||||
- 每条一行,用 "- " 开头,不带日期前缀
|
||||
- 目标:控制在 50 条以内,每条尽量一句话概括
|
||||
|
||||
### Part 2: 梦境日记([DREAM])
|
||||
|
||||
用简洁的叙事风格写一篇短日记,记录这次整理的发现,保持格式美观易读:
|
||||
- 发现了哪些重复或矛盾
|
||||
- 从日记中提取了什么新洞察
|
||||
- 做了哪些清理和优化
|
||||
- 整体感受和观察
|
||||
|
||||
## 输出格式(严格遵守)
|
||||
|
||||
```
|
||||
[DAILY]
|
||||
- 事件1的摘要
|
||||
- 事件2的摘要
|
||||
|
||||
[MEMORY]
|
||||
- 值得永久记住的信息1
|
||||
- 值得永久记住的信息2
|
||||
```
|
||||
- 记忆条目1
|
||||
- 记忆条目2
|
||||
...
|
||||
|
||||
当对话没有任何记录价值(仅含问候或无意义内容),直接回复"无"。"""
|
||||
[DREAM]
|
||||
梦境日记内容...
|
||||
```"""
|
||||
|
||||
SUMMARIZE_USER_PROMPT = """请从以下对话记录中提取记忆(按 [DAILY] 和 [MEMORY] 两部分输出):
|
||||
DREAM_USER_PROMPT = """## 当前长期记忆(MEMORY.md)
|
||||
|
||||
{memory_content}
|
||||
|
||||
## 近期日记(最近 {days} 天)
|
||||
|
||||
{daily_content}"""
|
||||
|
||||
{conversation}"""
|
||||
|
||||
|
||||
class MemoryFlushManager:
|
||||
@@ -81,6 +114,8 @@ class MemoryFlushManager:
|
||||
self.last_flush_timestamp: Optional[datetime] = None
|
||||
self._trim_flushed_hashes: set = set() # Content hashes of already-flushed messages
|
||||
self._last_flushed_content_hash: str = "" # Content hash at last flush, for daily dedup
|
||||
self._last_dream_input_hash: str = "" # Hash of dream input, for dedup
|
||||
self._last_flush_thread: Optional[threading.Thread] = None
|
||||
|
||||
def get_today_memory_file(self, user_id: Optional[str] = None, ensure_exists: bool = False) -> Path:
|
||||
"""Get today's memory file path: memory/YYYY-MM-DD.md"""
|
||||
@@ -124,21 +159,19 @@ class MemoryFlushManager:
|
||||
user_id: Optional[str] = None,
|
||||
reason: str = "trim",
|
||||
max_messages: int = 0,
|
||||
context_summary_callback: Optional[Callable[[str], None]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Asynchronously summarize and flush messages to daily memory.
|
||||
|
||||
|
||||
Deduplication runs synchronously, then LLM summarization + file write
|
||||
run in a background thread so the main reply flow is never blocked.
|
||||
|
||||
Args:
|
||||
messages: Conversation message list (OpenAI/Claude format)
|
||||
user_id: Optional user ID for user-scoped memory
|
||||
reason: Why flush was triggered ("trim" | "overflow" | "daily_summary")
|
||||
max_messages: Max recent messages to summarize (0 = all)
|
||||
|
||||
Returns:
|
||||
True if flush was dispatched
|
||||
|
||||
If *context_summary_callback* is provided, it is called with the
|
||||
[DAILY] portion of the LLM summary once available. The caller can use
|
||||
this to inject the summary into the live message list for context
|
||||
continuity — one LLM call serves both disk persistence and in-context
|
||||
injection.
|
||||
"""
|
||||
try:
|
||||
import hashlib
|
||||
@@ -153,18 +186,19 @@ class MemoryFlushManager:
|
||||
deduped.append(m)
|
||||
if not deduped:
|
||||
return False
|
||||
|
||||
|
||||
import copy
|
||||
snapshot = copy.deepcopy(deduped)
|
||||
thread = threading.Thread(
|
||||
target=self._flush_worker,
|
||||
args=(snapshot, user_id, reason, max_messages),
|
||||
args=(snapshot, user_id, reason, max_messages, context_summary_callback),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
logger.info(f"[MemoryFlush] Async flush dispatched (reason={reason}, msgs={len(snapshot)})")
|
||||
self._last_flush_thread = thread
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[MemoryFlush] Failed to dispatch flush (reason={reason}): {e}")
|
||||
return False
|
||||
@@ -175,43 +209,41 @@ class MemoryFlushManager:
|
||||
user_id: Optional[str],
|
||||
reason: str,
|
||||
max_messages: int,
|
||||
context_summary_callback: Optional[Callable[[str], None]] = None,
|
||||
):
|
||||
"""Background worker: summarize with LLM, write daily file + MEMORY.md (Light Dream)."""
|
||||
"""Background worker: summarize with LLM, write daily memory file."""
|
||||
try:
|
||||
raw_summary = self._summarize_messages(messages, max_messages)
|
||||
if not raw_summary or not raw_summary.strip() or raw_summary.strip() == "无":
|
||||
logger.info(f"[MemoryFlush] No valuable content to flush (reason={reason})")
|
||||
return
|
||||
|
||||
daily_part, memory_part = self._parse_dual_output(raw_summary)
|
||||
# Strip legacy [DAILY]/[MEMORY] markers if model still outputs them
|
||||
daily_part = self._clean_summary_output(raw_summary)
|
||||
if not daily_part:
|
||||
return
|
||||
|
||||
# --- Write daily memory ---
|
||||
if daily_part:
|
||||
daily_file = ensure_daily_memory_file(self.workspace_dir, user_id)
|
||||
daily_file = ensure_daily_memory_file(self.workspace_dir, user_id)
|
||||
|
||||
if reason == "overflow":
|
||||
header = f"## Context Overflow Recovery ({datetime.now().strftime('%H:%M')})"
|
||||
note = "The following conversation was trimmed due to context overflow:\n"
|
||||
elif reason == "trim":
|
||||
header = f"## Trimmed Context ({datetime.now().strftime('%H:%M')})"
|
||||
note = ""
|
||||
elif reason == "daily_summary":
|
||||
header = f"## Daily Summary ({datetime.now().strftime('%H:%M')})"
|
||||
note = ""
|
||||
else:
|
||||
header = f"## Session Notes ({datetime.now().strftime('%H:%M')})"
|
||||
note = ""
|
||||
headers = {
|
||||
"overflow": f"## Context Overflow Recovery ({datetime.now().strftime('%H:%M')})",
|
||||
"trim": f"## Trimmed Context ({datetime.now().strftime('%H:%M')})",
|
||||
"daily_summary": f"## Daily Summary ({datetime.now().strftime('%H:%M')})",
|
||||
}
|
||||
header = headers.get(reason, f"## Session Notes ({datetime.now().strftime('%H:%M')})")
|
||||
|
||||
flush_entry = f"\n{header}\n\n{note}{daily_part}\n"
|
||||
with open(daily_file, "a", encoding="utf-8") as f:
|
||||
f.write(f"\n{header}\n\n{daily_part}\n")
|
||||
|
||||
with open(daily_file, "a", encoding="utf-8") as f:
|
||||
f.write(flush_entry)
|
||||
logger.info(f"[MemoryFlush] Wrote daily memory to {daily_file.name} (reason={reason}, chars={len(daily_part)})")
|
||||
|
||||
logger.info(f"[MemoryFlush] Wrote daily memory to {daily_file.name} (reason={reason}, chars={len(daily_part)})")
|
||||
|
||||
# --- Light Dream: write long-term memory to MEMORY.md ---
|
||||
if memory_part:
|
||||
self._append_to_main_memory(memory_part, user_id)
|
||||
# --- Inject context summary into live messages (if callback provided) ---
|
||||
if context_summary_callback:
|
||||
try:
|
||||
context_summary_callback(daily_part)
|
||||
except Exception as e:
|
||||
logger.warning(f"[MemoryFlush] Context summary callback failed: {e}")
|
||||
|
||||
self.last_flush_timestamp = datetime.now()
|
||||
|
||||
@@ -219,67 +251,26 @@ class MemoryFlushManager:
|
||||
logger.warning(f"[MemoryFlush] Async flush failed (reason={reason}): {e}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_dual_output(raw: str) -> tuple:
|
||||
"""
|
||||
Parse LLM output into (daily_part, memory_part).
|
||||
Handles both new [DAILY]/[MEMORY] format and legacy single-section format.
|
||||
"""
|
||||
def _clean_summary_output(raw: str) -> str:
|
||||
"""Strip legacy [DAILY]/[MEMORY] markers if present, return clean daily text."""
|
||||
raw = raw.strip()
|
||||
if not raw or raw == "无":
|
||||
return ""
|
||||
|
||||
if "[DAILY]" in raw or "[MEMORY]" in raw:
|
||||
daily_part = ""
|
||||
memory_part = ""
|
||||
# Strip [DAILY] marker
|
||||
if "[DAILY]" in raw:
|
||||
start = raw.index("[DAILY]") + len("[DAILY]")
|
||||
end = raw.index("[MEMORY]") if "[MEMORY]" in raw else len(raw)
|
||||
raw = raw[start:end].strip()
|
||||
|
||||
# Extract [DAILY] section
|
||||
if "[DAILY]" in raw:
|
||||
start = raw.index("[DAILY]") + len("[DAILY]")
|
||||
end = raw.index("[MEMORY]") if "[MEMORY]" in raw else len(raw)
|
||||
daily_part = raw[start:end].strip()
|
||||
# Remove stray [MEMORY] section entirely
|
||||
if "[MEMORY]" in raw:
|
||||
raw = raw[:raw.index("[MEMORY]")].strip()
|
||||
|
||||
# Extract [MEMORY] section
|
||||
if "[MEMORY]" in raw:
|
||||
start = raw.index("[MEMORY]") + len("[MEMORY]")
|
||||
memory_part = raw[start:].strip()
|
||||
# Remove markdown code fences
|
||||
raw = raw.replace("```", "").strip()
|
||||
|
||||
# Filter out empty markers
|
||||
if memory_part and all(
|
||||
not line.strip() or line.strip() == "-"
|
||||
for line in memory_part.split("\n")
|
||||
):
|
||||
memory_part = ""
|
||||
|
||||
return daily_part, memory_part
|
||||
|
||||
# Legacy format: treat entire output as daily, no memory extraction
|
||||
return raw, ""
|
||||
|
||||
def _append_to_main_memory(self, memory_entries: str, user_id: Optional[str] = None):
|
||||
"""Append extracted long-term memories to MEMORY.md with date stamp."""
|
||||
try:
|
||||
main_file = self.get_main_memory_file(user_id)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Add date prefix to each entry line
|
||||
stamped_lines = []
|
||||
for line in memory_entries.strip().split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("- "):
|
||||
stamped_lines.append(f"- ({today}) {line[2:]}")
|
||||
elif line:
|
||||
stamped_lines.append(f"- ({today}) {line}")
|
||||
|
||||
if not stamped_lines:
|
||||
return
|
||||
|
||||
stamped_text = "\n".join(stamped_lines)
|
||||
|
||||
with open(main_file, "a", encoding="utf-8") as f:
|
||||
f.write(f"\n{stamped_text}\n")
|
||||
|
||||
logger.info(f"[LightDream] Appended {len(stamped_lines)} entries to MEMORY.md")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[LightDream] Failed to append to MEMORY.md: {e}")
|
||||
return raw
|
||||
|
||||
def create_daily_summary(
|
||||
self,
|
||||
@@ -306,12 +297,187 @@ class MemoryFlushManager:
|
||||
reason="daily_summary",
|
||||
max_messages=0,
|
||||
)
|
||||
|
||||
|
||||
# ---- Deep Dream (memory distillation) ----
|
||||
|
||||
def deep_dream(self, user_id: Optional[str] = None, lookback_days: int = 1, force: bool = False) -> bool:
|
||||
"""
|
||||
Distill recent daily memories into MEMORY.md and generate a dream diary.
|
||||
|
||||
Args:
|
||||
lookback_days: How many days of daily files to read (default 1 for scheduled, 3 for manual)
|
||||
force: Skip input-hash dedup check (used by manual /memory dream trigger)
|
||||
"""
|
||||
if not self.llm_model:
|
||||
logger.warning("[DeepDream] No LLM model available, skipping")
|
||||
return False
|
||||
|
||||
logger.info(f"[DeepDream] Starting memory distillation (lookback={lookback_days} days)")
|
||||
|
||||
# Collect materials
|
||||
memory_content = self._read_main_memory(user_id)
|
||||
daily_content, has_content = self._read_recent_dailies(user_id, lookback_days)
|
||||
|
||||
if not has_content:
|
||||
logger.info("[DeepDream] No recent daily records, skipping to preserve existing MEMORY.md")
|
||||
return False
|
||||
|
||||
# Dedup: skip if input materials haven't changed since last dream
|
||||
import hashlib
|
||||
input_hash = hashlib.md5((memory_content + daily_content).encode("utf-8")).hexdigest()
|
||||
if not force and input_hash == self._last_dream_input_hash:
|
||||
logger.debug("[DeepDream] Input unchanged since last dream, skipping")
|
||||
return False
|
||||
self._last_dream_input_hash = input_hash
|
||||
|
||||
logger.info(
|
||||
f"[DeepDream] Materials collected: "
|
||||
f"MEMORY.md={len(memory_content)} chars, "
|
||||
f"daily={len(daily_content)} chars"
|
||||
)
|
||||
|
||||
# Call LLM for distillation
|
||||
import time as _time
|
||||
t0 = _time.monotonic()
|
||||
try:
|
||||
user_msg = DREAM_USER_PROMPT.format(
|
||||
memory_content=memory_content or "(empty)",
|
||||
days=lookback_days,
|
||||
daily_content=daily_content or "(no recent daily records)",
|
||||
)
|
||||
from agent.protocol.models import LLMRequest
|
||||
# Scale max_tokens based on input size to avoid truncating large MEMORY.md
|
||||
input_chars = len(memory_content) + len(daily_content)
|
||||
dream_max_tokens = max(2000, min(input_chars, 8000))
|
||||
request = LLMRequest(
|
||||
messages=[{"role": "user", "content": user_msg}],
|
||||
temperature=0.3,
|
||||
max_tokens=dream_max_tokens,
|
||||
stream=False,
|
||||
system=DREAM_SYSTEM_PROMPT,
|
||||
)
|
||||
response = self.llm_model.call(request)
|
||||
raw = self._extract_response_text(response)
|
||||
elapsed = _time.monotonic() - t0
|
||||
if not raw or not raw.strip():
|
||||
logger.warning(f"[DeepDream] LLM returned empty response ({elapsed:.1f}s)")
|
||||
return False
|
||||
logger.info(f"[DeepDream] LLM distillation completed ({elapsed:.1f}s, {len(raw)} chars)")
|
||||
except Exception as e:
|
||||
elapsed = _time.monotonic() - t0
|
||||
logger.warning(f"[DeepDream] LLM call failed ({elapsed:.1f}s): {e}")
|
||||
return False
|
||||
|
||||
# Parse [MEMORY] and [DREAM] sections
|
||||
new_memory, dream_diary = self._parse_dream_output(raw)
|
||||
|
||||
if not new_memory:
|
||||
logger.warning("[DeepDream] No [MEMORY] section in LLM output, skipping overwrite")
|
||||
return False
|
||||
|
||||
# Overwrite MEMORY.md
|
||||
try:
|
||||
main_file = self.get_main_memory_file(user_id)
|
||||
old_size = len(memory_content)
|
||||
main_file.write_text(new_memory + "\n", encoding="utf-8")
|
||||
logger.info(
|
||||
f"[DeepDream] Updated MEMORY.md "
|
||||
f"({old_size} → {len(new_memory)} chars)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[DeepDream] Failed to write MEMORY.md: {e}")
|
||||
return False
|
||||
|
||||
# Write dream diary
|
||||
if dream_diary:
|
||||
try:
|
||||
self._write_dream_diary(dream_diary, user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[DeepDream] Failed to write dream diary: {e}")
|
||||
|
||||
logger.info("[DeepDream] ✅ Deep Dream completed successfully")
|
||||
return True
|
||||
|
||||
def _read_main_memory(self, user_id: Optional[str] = None) -> str:
|
||||
"""Read current MEMORY.md content."""
|
||||
main_file = self.get_main_memory_file(user_id)
|
||||
if main_file.exists():
|
||||
return main_file.read_text(encoding="utf-8").strip()
|
||||
return ""
|
||||
|
||||
def _read_recent_dailies(
|
||||
self, user_id: Optional[str] = None, lookback_days: int = 1
|
||||
) -> tuple:
|
||||
"""
|
||||
Read recent daily memory files.
|
||||
|
||||
Returns:
|
||||
(combined_text, has_content) tuple
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
parts = []
|
||||
has_content = False
|
||||
today = datetime.now().date()
|
||||
|
||||
for offset in range(lookback_days):
|
||||
day = today - timedelta(days=offset)
|
||||
date_str = day.strftime("%Y-%m-%d")
|
||||
if user_id:
|
||||
daily_file = self.memory_dir / "users" / user_id / f"{date_str}.md"
|
||||
else:
|
||||
daily_file = self.memory_dir / f"{date_str}.md"
|
||||
|
||||
if daily_file.exists():
|
||||
content = daily_file.read_text(encoding="utf-8").strip()
|
||||
if content:
|
||||
parts.append(f"### {date_str}\n\n{content}")
|
||||
has_content = True
|
||||
else:
|
||||
parts.append(f"### {date_str}\n\n(no records)")
|
||||
|
||||
return "\n\n".join(parts), has_content
|
||||
|
||||
@staticmethod
|
||||
def _parse_dream_output(raw: str) -> tuple:
|
||||
"""Parse LLM output into (new_memory, dream_diary)."""
|
||||
raw = raw.strip().replace("```", "")
|
||||
new_memory = ""
|
||||
dream_diary = ""
|
||||
|
||||
if "[MEMORY]" in raw:
|
||||
start = raw.index("[MEMORY]") + len("[MEMORY]")
|
||||
end = raw.index("[DREAM]") if "[DREAM]" in raw else len(raw)
|
||||
new_memory = raw[start:end].strip()
|
||||
|
||||
if "[DREAM]" in raw:
|
||||
start = raw.index("[DREAM]") + len("[DREAM]")
|
||||
dream_diary = raw[start:].strip()
|
||||
|
||||
return new_memory, dream_diary
|
||||
|
||||
def _write_dream_diary(self, content: str, user_id: Optional[str] = None):
|
||||
"""Write dream diary to memory/dreams/YYYY-MM-DD.md."""
|
||||
dreams_dir = self.memory_dir / "dreams"
|
||||
if user_id:
|
||||
dreams_dir = self.memory_dir / "users" / user_id / "dreams"
|
||||
dreams_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
diary_file = dreams_dir / f"{today}.md"
|
||||
diary_file.write_text(
|
||||
f"# Dream Diary: {today}\n\n{content}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
logger.info(f"[DeepDream] Wrote dream diary to {diary_file}")
|
||||
|
||||
# ---- Internal helpers ----
|
||||
|
||||
def _summarize_messages(self, messages: List[Dict], max_messages: int = 0) -> str:
|
||||
"""
|
||||
Summarize conversation messages using LLM, with rule-based fallback.
|
||||
Summarize conversation messages using LLM.
|
||||
Returns empty string if LLM deems content not worth recording.
|
||||
Rule-based fallback only used when LLM call raises an exception.
|
||||
"""
|
||||
conversation_text = self._format_conversation_for_summary(messages, max_messages)
|
||||
if not conversation_text.strip():
|
||||
@@ -322,13 +488,14 @@ class MemoryFlushManager:
|
||||
summary = self._call_llm_for_summary(conversation_text)
|
||||
if summary and summary.strip() and summary.strip() != "无":
|
||||
return summary.strip()
|
||||
logger.info(f"[MemoryFlush] LLM returned empty or '无', using fallback")
|
||||
logger.info("[MemoryFlush] LLM returned empty or '无', skipping write")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.warning(f"[MemoryFlush] LLM summarization failed, using fallback: {e}")
|
||||
return self._extract_summary_fallback(messages, max_messages)
|
||||
else:
|
||||
logger.info("[MemoryFlush] No LLM model available, using rule-based fallback")
|
||||
|
||||
return self._extract_summary_fallback(messages, max_messages)
|
||||
return self._extract_summary_fallback(messages, max_messages)
|
||||
|
||||
def _format_conversation_for_summary(self, messages: List[Dict], max_messages: int = 0) -> str:
|
||||
"""Format messages into readable conversation text for LLM summarization."""
|
||||
@@ -346,6 +513,52 @@ class MemoryFlushManager:
|
||||
lines.append(f"助手: {text[:500]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _extract_response_text(response) -> str:
|
||||
"""
|
||||
Extract text from LLM response regardless of format.
|
||||
|
||||
Handles:
|
||||
- Generator (MiniMax _handle_sync_response yields Claude-format dicts)
|
||||
- Claude format: {"role":"assistant","content":[{"type":"text","text":"..."}]}
|
||||
- OpenAI format: {"choices":[{"message":{"content":"..."}}]}
|
||||
- OpenAI SDK response object with .choices attribute
|
||||
"""
|
||||
import types
|
||||
|
||||
# Unwrap generator — consume first yielded item
|
||||
if isinstance(response, types.GeneratorType):
|
||||
try:
|
||||
response = next(response)
|
||||
except StopIteration:
|
||||
return ""
|
||||
|
||||
if not response:
|
||||
return ""
|
||||
|
||||
if isinstance(response, dict):
|
||||
# Check for error
|
||||
if response.get("error"):
|
||||
raise RuntimeError(response.get("message", "LLM call failed"))
|
||||
|
||||
# Claude format: content is a list of blocks
|
||||
content = response.get("content")
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
return block.get("text", "")
|
||||
|
||||
# OpenAI format
|
||||
choices = response.get("choices", [])
|
||||
if choices:
|
||||
return choices[0].get("message", {}).get("content", "")
|
||||
|
||||
# OpenAI SDK response object
|
||||
if hasattr(response, "choices") and response.choices:
|
||||
return response.choices[0].message.content or ""
|
||||
|
||||
return ""
|
||||
|
||||
def _call_llm_for_summary(self, conversation_text: str) -> str:
|
||||
"""Call LLM to generate a concise summary of the conversation."""
|
||||
from agent.protocol.models import LLMRequest
|
||||
@@ -359,27 +572,31 @@ class MemoryFlushManager:
|
||||
)
|
||||
|
||||
response = self.llm_model.call(request)
|
||||
|
||||
if isinstance(response, dict):
|
||||
if response.get("error"):
|
||||
raise RuntimeError(response.get("message", "LLM call failed"))
|
||||
# OpenAI format
|
||||
choices = response.get("choices", [])
|
||||
if choices:
|
||||
return choices[0].get("message", {}).get("content", "")
|
||||
|
||||
# Handle response object with attribute access (e.g. OpenAI SDK response)
|
||||
if hasattr(response, "choices") and response.choices:
|
||||
return response.choices[0].message.content or ""
|
||||
|
||||
return ""
|
||||
return self._extract_response_text(response)
|
||||
|
||||
@staticmethod
|
||||
def _extract_first_meaningful_line(text: str, max_len: int = 120) -> str:
|
||||
"""Extract the first meaningful line from assistant reply, skipping markdown noise."""
|
||||
import re
|
||||
for line in text.split("\n"):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# Skip markdown headings, horizontal rules, code fences, pure emoji/symbols
|
||||
if re.match(r'^(#{1,4}\s|```|---|\*\*\*|[-*]\s*$|[^\w\u4e00-\u9fff]{1,5}$)', line):
|
||||
continue
|
||||
# Strip leading markdown bold/emoji decorations
|
||||
cleaned = re.sub(r'^[\*#>\-\s]+', '', line).strip()
|
||||
cleaned = re.sub(r'^[\U0001f300-\U0001f9ff\u2600-\u27bf\s]+', '', cleaned).strip()
|
||||
if len(cleaned) >= 5:
|
||||
return cleaned[:max_len]
|
||||
return text.split("\n")[0].strip()[:max_len]
|
||||
|
||||
@staticmethod
|
||||
def _extract_summary_fallback(messages: List[Dict], max_messages: int = 0) -> str:
|
||||
"""
|
||||
Rule-based fallback when LLM is unavailable.
|
||||
Groups consecutive user+assistant messages into events instead of
|
||||
listing each message individually.
|
||||
Rule-based summary of discarded messages.
|
||||
Format: "用户问了X; 助手回答了Y" per event, compact and readable.
|
||||
"""
|
||||
msgs = messages if max_messages == 0 else messages[-max_messages * 2:]
|
||||
|
||||
@@ -393,19 +610,19 @@ class MemoryFlushManager:
|
||||
text = text.strip()
|
||||
|
||||
if role == "user":
|
||||
if len(text) <= 5:
|
||||
if len(text) <= 3:
|
||||
continue
|
||||
current_user_text = text[:150]
|
||||
current_user_text = text[:120]
|
||||
elif role == "assistant" and current_user_text:
|
||||
first_line = text.split("\n")[0].strip()
|
||||
if len(first_line) > 10:
|
||||
events.append(f"- {current_user_text} → {first_line[:150]}")
|
||||
reply_summary = MemoryFlushManager._extract_first_meaningful_line(text)
|
||||
if reply_summary:
|
||||
events.append(f"- 用户: {current_user_text} → 回复: {reply_summary}")
|
||||
else:
|
||||
events.append(f"- {current_user_text}")
|
||||
events.append(f"- 用户: {current_user_text}")
|
||||
current_user_text = ""
|
||||
|
||||
if current_user_text:
|
||||
events.append(f"- {current_user_text}")
|
||||
events.append(f"- 用户: {current_user_text}")
|
||||
|
||||
return "\n".join(events[:10])
|
||||
|
||||
|
||||
@@ -291,8 +291,8 @@ def _build_memory_section(memory_manager: Any, tools: Optional[List[Any]], langu
|
||||
"",
|
||||
"### Memory Recall(mandatory)",
|
||||
"",
|
||||
"在回答任何关于过往工作、决策、日期、人物、偏好或待办事项的问题之前,**必须**先检索记忆。",
|
||||
"MEMORY.md 已自动加载在项目上下文中(可能被截断),完整内容和每日记忆需要通过工具检索。",
|
||||
"当用户询问过往事件、引用之前的决定、提到人物关系、偏好、待办、或你对某事不确定时,**必须先检索记忆再回答**。",
|
||||
"如果 MEMORY.md 中已有相关信息则无需重复检索。完整内容和每日记忆需要通过工具检索。",
|
||||
"",
|
||||
"1. 不确定位置 → `memory_search` 关键词/语义检索",
|
||||
"2. 已知位置 → `memory_get` 直接读取对应行",
|
||||
@@ -307,7 +307,7 @@ def _build_memory_section(memory_manager: Any, tools: Optional[List[Any]], langu
|
||||
"",
|
||||
"遇到以下情况时,**主动**将信息写入记忆文件(无需告知用户):",
|
||||
"",
|
||||
"- 用户要求记住某些信息",
|
||||
"- 用户要求记住某些信息,或使用了「记住」「以后」「总是」「不要」「偏好」等表达",
|
||||
"- 用户分享了重要的个人偏好、习惯、决策",
|
||||
"- 对话中产生了重要的结论、方案、约定",
|
||||
"- 完成了复杂任务,值得记录关键步骤和结果",
|
||||
|
||||
@@ -78,6 +78,11 @@ class AgentStreamExecutor:
|
||||
except Exception as e:
|
||||
logger.error(f"Event callback error: {e}")
|
||||
|
||||
def _is_thinking_enabled(self) -> bool:
|
||||
from config import conf
|
||||
channel_type = getattr(self.model, 'channel_type', '') or ''
|
||||
return conf().get("enable_thinking", True) and channel_type == 'web'
|
||||
|
||||
def _filter_think_tags(self, text: str) -> str:
|
||||
"""
|
||||
Remove <think> and </think> tags but keep the content inside.
|
||||
@@ -178,7 +183,10 @@ class AgentStreamExecutor:
|
||||
Final response text
|
||||
"""
|
||||
# Log user message with model info
|
||||
logger.info(f"🤖 {self.model.model} | 👤 {user_message}")
|
||||
|
||||
thinking_enabled = self._is_thinking_enabled()
|
||||
thinking_label = "💭 thinking" if thinking_enabled else "⚡ fast"
|
||||
logger.info(f"🤖 {self.model.model} | {thinking_label} | 👤 {user_message}")
|
||||
|
||||
# Add user message (Claude format - use content blocks for consistency)
|
||||
self.messages.append({
|
||||
@@ -588,7 +596,8 @@ class AgentStreamExecutor:
|
||||
reasoning_delta = delta.get("reasoning_content") or ""
|
||||
if reasoning_delta:
|
||||
full_reasoning += reasoning_delta
|
||||
self._emit_event("reasoning_update", {"delta": reasoning_delta})
|
||||
if self._is_thinking_enabled():
|
||||
self._emit_event("reasoning_update", {"delta": reasoning_delta})
|
||||
|
||||
# Handle text content
|
||||
content_delta = delta.get("content") or ""
|
||||
@@ -1198,6 +1207,56 @@ class AgentStreamExecutor:
|
||||
logger.warning("🔧 Aggressive trim: nothing to trim, will clear history")
|
||||
return False
|
||||
|
||||
def _build_context_summary_callback(self, discarded_turns: list, kept_turns: list):
|
||||
"""
|
||||
Build a callback that injects an LLM summary into the first user
|
||||
message of *kept_turns*. Returns None if no valid injection target.
|
||||
|
||||
The callback is passed to flush_from_messages so that the same LLM
|
||||
call that writes daily memory also provides the in-context summary.
|
||||
"""
|
||||
if not kept_turns:
|
||||
return None
|
||||
|
||||
# Find the first user text block in kept_turns as injection target
|
||||
target_block = None
|
||||
for turn in kept_turns:
|
||||
for msg in turn["messages"]:
|
||||
if msg.get("role") == "user":
|
||||
content = msg.get("content", [])
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
target_block = block
|
||||
break
|
||||
if target_block:
|
||||
break
|
||||
if target_block:
|
||||
break
|
||||
|
||||
if not target_block:
|
||||
return None
|
||||
|
||||
turn_count = len(discarded_turns)
|
||||
original_text = target_block["text"]
|
||||
|
||||
def _on_summary_ready(summary: str):
|
||||
if not summary or not summary.strip():
|
||||
return
|
||||
target_block["text"] = (
|
||||
f"[System: Previous conversation summary — "
|
||||
f"{turn_count} turns were compacted]\n\n"
|
||||
f"{summary.strip()}\n\n"
|
||||
f"The recent conversation continues below.\n\n---\n\n"
|
||||
f"{original_text}"
|
||||
)
|
||||
logger.info(
|
||||
f"📝 Context summary injected "
|
||||
f"({len(summary)} chars, {turn_count} turns)"
|
||||
)
|
||||
|
||||
return _on_summary_ready
|
||||
|
||||
def _trim_messages(self):
|
||||
"""
|
||||
智能清理消息历史,保持对话完整性
|
||||
@@ -1224,25 +1283,28 @@ class AgentStreamExecutor:
|
||||
removed_count = len(turns) // 2
|
||||
keep_count = len(turns) - removed_count
|
||||
|
||||
# Flush discarded turns to daily memory
|
||||
if self.agent.memory_manager:
|
||||
discarded_messages = []
|
||||
for turn in turns[:removed_count]:
|
||||
discarded_messages.extend(turn["messages"])
|
||||
if discarded_messages:
|
||||
user_id = getattr(self.agent, '_current_user_id', None)
|
||||
self.agent.memory_manager.flush_memory(
|
||||
messages=discarded_messages, user_id=user_id,
|
||||
reason="trim", max_messages=0
|
||||
)
|
||||
|
||||
discarded_turns = turns[:removed_count]
|
||||
turns = turns[-keep_count:]
|
||||
|
||||
|
||||
logger.info(
|
||||
f"💾 上下文轮次超限: {keep_count + removed_count} > {self.max_context_turns},"
|
||||
f"裁剪至 {keep_count} 轮(移除 {removed_count} 轮)"
|
||||
)
|
||||
|
||||
# Flush to daily memory + inject context summary (single async LLM call)
|
||||
if self.agent.memory_manager:
|
||||
discarded_messages = []
|
||||
for turn in discarded_turns:
|
||||
discarded_messages.extend(turn["messages"])
|
||||
if discarded_messages:
|
||||
user_id = getattr(self.agent, '_current_user_id', None)
|
||||
cb = self._build_context_summary_callback(discarded_turns, turns)
|
||||
self.agent.memory_manager.flush_memory(
|
||||
messages=discarded_messages, user_id=user_id,
|
||||
reason="trim", max_messages=0,
|
||||
context_summary_callback=cb,
|
||||
)
|
||||
|
||||
# Step 3: Token 限制 - 保留完整轮次
|
||||
# Get context window from agent (based on model)
|
||||
context_window = self.agent._get_model_context_window()
|
||||
@@ -1318,6 +1380,7 @@ class AgentStreamExecutor:
|
||||
# --- Many turns (>=5): discard the older half, keep the newer half ---
|
||||
removed_count = len(turns) // 2
|
||||
keep_count = len(turns) - removed_count
|
||||
discarded_turns = turns[:removed_count]
|
||||
kept_turns = turns[-keep_count:]
|
||||
kept_tokens = sum(self._estimate_turn_tokens(t) for t in kept_turns)
|
||||
|
||||
@@ -1328,13 +1391,15 @@ class AgentStreamExecutor:
|
||||
|
||||
if self.agent.memory_manager:
|
||||
discarded_messages = []
|
||||
for turn in turns[:removed_count]:
|
||||
for turn in discarded_turns:
|
||||
discarded_messages.extend(turn["messages"])
|
||||
if discarded_messages:
|
||||
user_id = getattr(self.agent, '_current_user_id', None)
|
||||
cb = self._build_context_summary_callback(discarded_turns, kept_turns)
|
||||
self.agent.memory_manager.flush_memory(
|
||||
messages=discarded_messages, user_id=user_id,
|
||||
reason="trim", max_messages=0
|
||||
reason="trim", max_messages=0,
|
||||
context_summary_callback=cb,
|
||||
)
|
||||
|
||||
new_messages = []
|
||||
|
||||
@@ -160,13 +160,21 @@ class AgentLLMModel(LLMModel):
|
||||
kwargs['system'] = system_prompt
|
||||
|
||||
# Pass context metadata to bot
|
||||
channel_type = getattr(self, 'channel_type', None)
|
||||
channel_type = getattr(self, 'channel_type', None) or ''
|
||||
if channel_type:
|
||||
kwargs['channel_type'] = channel_type
|
||||
session_id = getattr(self, 'session_id', None)
|
||||
if session_id:
|
||||
kwargs['session_id'] = session_id
|
||||
|
||||
# Determine thinking: respect global config, then channel_type
|
||||
from config import conf
|
||||
global_thinking = conf().get("enable_thinking", True)
|
||||
if not global_thinking:
|
||||
kwargs['thinking'] = {"type": "disabled"}
|
||||
else:
|
||||
kwargs['thinking'] = {"type": "enabled"} if channel_type == "web" else {"type": "disabled"}
|
||||
|
||||
response = self.bot.call_with_tools(**kwargs)
|
||||
return self._format_response(response)
|
||||
else:
|
||||
@@ -205,13 +213,21 @@ class AgentLLMModel(LLMModel):
|
||||
kwargs['system'] = system_prompt
|
||||
|
||||
# Pass context metadata to bot
|
||||
channel_type = getattr(self, 'channel_type', None)
|
||||
channel_type = getattr(self, 'channel_type', None) or ''
|
||||
if channel_type:
|
||||
kwargs['channel_type'] = channel_type
|
||||
session_id = getattr(self, 'session_id', None)
|
||||
if session_id:
|
||||
kwargs['session_id'] = session_id
|
||||
|
||||
# Determine thinking: respect global config, then channel_type
|
||||
from config import conf
|
||||
global_thinking = conf().get("enable_thinking", True)
|
||||
if not global_thinking:
|
||||
kwargs['thinking'] = {"type": "disabled"}
|
||||
else:
|
||||
kwargs['thinking'] = {"type": "enabled"} if channel_type == "web" else {"type": "disabled"}
|
||||
|
||||
stream = self.bot.call_with_tools(**kwargs)
|
||||
|
||||
# Convert stream format to our expected format
|
||||
|
||||
@@ -567,7 +567,7 @@ class AgentInitializer:
|
||||
t.start()
|
||||
|
||||
def _flush_all_agents(self):
|
||||
"""Flush memory for all active agent sessions."""
|
||||
"""Flush memory for all active agent sessions, then run Deep Dream."""
|
||||
agents = []
|
||||
if self.agent_bridge.default_agent:
|
||||
agents.append(("default", self.agent_bridge.default_agent))
|
||||
@@ -577,7 +577,10 @@ class AgentInitializer:
|
||||
if not agents:
|
||||
return
|
||||
|
||||
# Phase 1: flush daily summaries
|
||||
flushed = 0
|
||||
flush_threads = []
|
||||
dream_candidate = None
|
||||
for label, agent in agents:
|
||||
try:
|
||||
if not agent.memory_manager:
|
||||
@@ -589,8 +592,26 @@ class AgentInitializer:
|
||||
result = agent.memory_manager.flush_manager.create_daily_summary(messages)
|
||||
if result:
|
||||
flushed += 1
|
||||
t = agent.memory_manager.flush_manager._last_flush_thread
|
||||
if t:
|
||||
flush_threads.append(t)
|
||||
if dream_candidate is None:
|
||||
dream_candidate = agent.memory_manager.flush_manager
|
||||
except Exception as e:
|
||||
logger.warning(f"[DailyFlush] Failed for session {label}: {e}")
|
||||
|
||||
if flushed:
|
||||
logger.info(f"[DailyFlush] Flushed {flushed}/{len(agents)} agent session(s)")
|
||||
|
||||
# Wait for all flush threads to finish before dreaming
|
||||
for t in flush_threads:
|
||||
t.join(timeout=60)
|
||||
|
||||
# Phase 2: Deep Dream — distill daily memories → MEMORY.md + dream diary
|
||||
if dream_candidate:
|
||||
try:
|
||||
result = dream_candidate.deep_dream()
|
||||
if result:
|
||||
logger.info("[DeepDream] Memory distillation completed successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"[DeepDream] Failed: {e}")
|
||||
|
||||
@@ -540,6 +540,18 @@
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors">
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center gap-1.5 text-sm font-medium text-slate-600 dark:text-slate-400">
|
||||
<span data-i18n="config_enable_thinking">Deep Thinking</span>
|
||||
<span class="cfg-tip" data-tip-key="config_enable_thinking_hint"><i class="fas fa-circle-question"></i></span>
|
||||
</label>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input id="cfg-enable-thinking" type="checkbox" class="sr-only peer" checked>
|
||||
<div class="w-9 h-5 bg-slate-200 dark:bg-slate-700 peer-checked:bg-primary-400 rounded-full
|
||||
after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white
|
||||
after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<span id="cfg-agent-status" class="text-xs text-primary-500 opacity-0 transition-opacity duration-300"></span>
|
||||
<button id="cfg-agent-save"
|
||||
@@ -648,6 +660,16 @@
|
||||
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="memory_title">记忆管理</h2>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="memory_desc">查看 Agent 记忆文件和内容</p>
|
||||
</div>
|
||||
<div class="flex items-center bg-slate-100 dark:bg-white/10 rounded-lg p-0.5">
|
||||
<button id="memory-tab-files" onclick="switchMemoryTab('files')"
|
||||
class="memory-tab px-3 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-colors duration-150 active">
|
||||
<i class="fas fa-file-lines mr-1.5"></i><span data-i18n="memory_tab_files">记忆文件</span>
|
||||
</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-moon mr-1.5"></i><span data-i18n="memory_tab_dreams">梦境日记</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="memory-empty" class="flex flex-col items-center justify-center py-20">
|
||||
<div class="w-16 h-16 rounded-2xl bg-purple-50 dark:bg-purple-900/20 flex items-center justify-center mb-4">
|
||||
|
||||
@@ -455,9 +455,8 @@
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.agent-thinking-step .thinking-header.no-toggle { cursor: default; }
|
||||
.agent-thinking-step .thinking-header:not(.no-toggle):hover { color: #64748b; }
|
||||
.dark .agent-thinking-step .thinking-header:not(.no-toggle):hover { color: #cbd5e1; }
|
||||
.agent-thinking-step .thinking-header:hover { color: #64748b; }
|
||||
.dark .agent-thinking-step .thinking-header:hover { color: #cbd5e1; }
|
||||
.agent-thinking-step .thinking-header i:first-child { font-size: 0.625rem; margin-top: 1px; }
|
||||
.agent-thinking-step .thinking-chevron {
|
||||
font-size: 0.5rem;
|
||||
@@ -488,6 +487,11 @@
|
||||
.agent-thinking-step .thinking-full p { margin: 0.25em 0; }
|
||||
.agent-thinking-step .thinking-full p:first-child { margin-top: 0; }
|
||||
.agent-thinking-step .thinking-full p:last-child { margin-bottom: 0; }
|
||||
.agent-thinking-step .thinking-duration {
|
||||
font-size: 0.625rem;
|
||||
color: #b0b8c4;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
/* Content step - real text output frozen before tool calls */
|
||||
.agent-content-step {
|
||||
@@ -886,15 +890,15 @@
|
||||
============================================================ */
|
||||
|
||||
/* Tab toggle */
|
||||
.knowledge-tab {
|
||||
.knowledge-tab, .memory-tab {
|
||||
color: #64748b;
|
||||
}
|
||||
.knowledge-tab.active {
|
||||
.knowledge-tab.active, .memory-tab.active {
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
}
|
||||
.dark .knowledge-tab.active {
|
||||
.dark .knowledge-tab.active, .dark .memory-tab.active {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ const I18N = {
|
||||
config_max_tokens: '最大上下文 Token', config_max_tokens_hint: '对话中 Agent 能输入的最大 Token 长度,超过后会智能压缩处理',
|
||||
config_max_turns: '最大记忆轮次', config_max_turns_hint: '一问一答为一轮,超过后会智能压缩处理',
|
||||
config_max_steps: '最大执行步数', config_max_steps_hint: '单次对话中 Agent 最多调用工具的次数',
|
||||
config_enable_thinking: '深度思考', config_enable_thinking_hint: '启用后在 Web 端展示模型推理过程',
|
||||
config_channel_type: '通道类型',
|
||||
config_provider: '模型厂商', config_model_name: '模型',
|
||||
config_custom_model_hint: '输入自定义模型名称',
|
||||
@@ -54,6 +55,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_loading: '加载记忆文件中...', memory_loading_desc: '记忆文件将显示在此处',
|
||||
memory_back: '返回列表',
|
||||
memory_col_name: '文件名', memory_col_type: '类型', memory_col_size: '大小', memory_col_updated: '更新时间',
|
||||
@@ -92,6 +94,7 @@ const I18N = {
|
||||
confirm_yes: '确认',
|
||||
confirm_cancel: '取消',
|
||||
error_send: '发送失败,请稍后再试。', error_timeout: '请求超时,请再试一次。',
|
||||
thinking_in_progress: '思考中...', thinking_done: '已深度思考', thinking_duration: '耗时',
|
||||
},
|
||||
en: {
|
||||
console: 'Console',
|
||||
@@ -120,6 +123,7 @@ const I18N = {
|
||||
config_max_tokens: 'Max Context Tokens', config_max_tokens_hint: 'Max tokens the Agent can input per conversation, auto-compressed when exceeded',
|
||||
config_max_turns: 'Max Memory Turns', config_max_turns_hint: 'One Q&A pair = one turn, auto-compressed when exceeded',
|
||||
config_max_steps: 'Max Steps', config_max_steps_hint: 'Max tool calls the Agent can make in a single conversation',
|
||||
config_enable_thinking: 'Deep Thinking', config_enable_thinking_hint: 'Show model reasoning on web console',
|
||||
config_channel_type: 'Channel Type',
|
||||
config_provider: 'Provider', config_model_name: 'Model',
|
||||
config_custom_model_hint: 'Enter custom model name',
|
||||
@@ -136,6 +140,7 @@ const I18N = {
|
||||
skills_section_title: 'Skills', skill_enable: 'Enable', skill_disable: 'Disable',
|
||||
skill_toggle_error: 'Operation failed, please try again',
|
||||
memory_title: 'Memory', memory_desc: 'View agent memory files and contents',
|
||||
memory_tab_files: 'Memory Files', memory_tab_dreams: 'Dream Diary',
|
||||
memory_loading: 'Loading memory files...', memory_loading_desc: 'Memory files will be displayed here',
|
||||
memory_back: 'Back to list',
|
||||
memory_col_name: 'Filename', memory_col_type: 'Type', memory_col_size: 'Size', memory_col_updated: 'Updated',
|
||||
@@ -174,6 +179,7 @@ const I18N = {
|
||||
confirm_yes: 'Confirm',
|
||||
confirm_cancel: 'Cancel',
|
||||
error_send: 'Failed to send. Please try again.', error_timeout: 'Request timeout. Please try again.',
|
||||
thinking_in_progress: 'Thinking...', thinking_done: 'Thought', thinking_duration: 'Duration',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -197,7 +203,7 @@ function applyI18n() {
|
||||
el.setAttribute('data-tooltip', t(el.dataset.tipKey));
|
||||
});
|
||||
const langLabel = document.getElementById('lang-label');
|
||||
if (langLabel) langLabel.textContent = currentLang === 'zh' ? 'EN' : '中文';
|
||||
if (langLabel) langLabel.textContent = currentLang === 'zh' ? '中文' : 'EN';
|
||||
}
|
||||
|
||||
function toggleLanguage() {
|
||||
@@ -421,6 +427,36 @@ const chatInput = document.getElementById('chat-input');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
|
||||
// Intercept internal navigation links in chat messages
|
||||
messagesDiv.addEventListener('click', (e) => {
|
||||
const copyBtn = e.target.closest('.copy-msg-btn');
|
||||
if (copyBtn) {
|
||||
e.preventDefault();
|
||||
const msgRoot = copyBtn.closest('.flex.gap-3');
|
||||
const answerEl = msgRoot && msgRoot.querySelector('.answer-content');
|
||||
const rawMd = answerEl && answerEl.dataset.rawMd;
|
||||
if (rawMd) {
|
||||
navigator.clipboard.writeText(rawMd).then(() => {
|
||||
const icon = copyBtn.querySelector('i');
|
||||
if (icon) { icon.className = 'fas fa-check'; setTimeout(() => { icon.className = 'fas fa-copy'; }, 1500); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const a = e.target.closest('a');
|
||||
if (!a) return;
|
||||
const href = a.getAttribute('href') || '';
|
||||
if (href === '/memory/dreams') {
|
||||
e.preventDefault();
|
||||
navigateTo('memory');
|
||||
setTimeout(() => switchMemoryTab('dreams'), 50);
|
||||
} else if (href === '/memory/MEMORY.md') {
|
||||
e.preventDefault();
|
||||
navigateTo('memory');
|
||||
setTimeout(() => { switchMemoryTab('files'); openMemoryFile('MEMORY.md', 'memory'); }, 50);
|
||||
}
|
||||
});
|
||||
const attachmentPreview = document.getElementById('attachment-preview');
|
||||
|
||||
// Pending attachments: [{file_path, file_name, file_type, preview_url}]
|
||||
@@ -560,6 +596,7 @@ const SLASH_COMMANDS = [
|
||||
{ cmd: '/skill info ', desc: '查看技能详情' },
|
||||
{ cmd: '/skill enable ', desc: '启用技能' },
|
||||
{ cmd: '/skill disable ', desc: '禁用技能' },
|
||||
{ cmd: '/memory dream ', desc: '手动触发记忆蒸馏 (可指定天数, 默认3)' },
|
||||
{ cmd: '/knowledge', desc: '查看知识库统计' },
|
||||
{ cmd: '/knowledge list', desc: '查看知识库文件树' },
|
||||
{ cmd: '/knowledge on', desc: '开启知识库' },
|
||||
@@ -892,6 +929,7 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
let currentToolEl = null;
|
||||
let currentReasoningEl = null; // live reasoning bubble
|
||||
let reasoningText = '';
|
||||
let reasoningStartTime = 0;
|
||||
let done = false;
|
||||
|
||||
const MAX_RECONNECTS = 10;
|
||||
@@ -912,7 +950,12 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
<div class="answer-content sse-streaming"></div>
|
||||
<div class="media-content"></div>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400 dark:text-slate-500 mt-1.5">${formatTime(timestamp)}</div>
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<span class="text-xs text-slate-400 dark:text-slate-500">${formatTime(timestamp)}</span>
|
||||
<button class="copy-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${currentLang === 'zh' ? '复制' : 'Copy'}" style="display:none">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
messagesDiv.appendChild(botEl);
|
||||
@@ -936,28 +979,25 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
ensureBotEl();
|
||||
reasoningText += item.content;
|
||||
if (!currentReasoningEl) {
|
||||
reasoningStartTime = Date.now();
|
||||
currentReasoningEl = document.createElement('div');
|
||||
currentReasoningEl.className = 'agent-step agent-thinking-step';
|
||||
currentReasoningEl.innerHTML = `
|
||||
<div class="thinking-header" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<i class="fas fa-lightbulb text-amber-400 flex-shrink-0"></i>
|
||||
<span class="thinking-summary"></span>
|
||||
<span class="thinking-summary">${t('thinking_in_progress')}</span>
|
||||
<i class="fas fa-chevron-right thinking-chevron"></i>
|
||||
</div>
|
||||
<div class="thinking-full"></div>`;
|
||||
stepsEl.appendChild(currentReasoningEl);
|
||||
}
|
||||
const oneLine = reasoningText.trim().replace(/\n+/g, ' ');
|
||||
currentReasoningEl.querySelector('.thinking-summary').textContent =
|
||||
oneLine.length > 80 ? oneLine.substring(0, 80) + '…' : oneLine;
|
||||
currentReasoningEl.querySelector('.thinking-full').innerHTML = renderMarkdown(reasoningText);
|
||||
scrollChatToBottom();
|
||||
|
||||
} else if (item.type === 'delta') {
|
||||
ensureBotEl();
|
||||
if (currentReasoningEl) {
|
||||
if (reasoningText.trim().replace(/\n+/g, ' ').length <= 80)
|
||||
currentReasoningEl.classList.add('no-expand');
|
||||
finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText);
|
||||
currentReasoningEl = null;
|
||||
reasoningText = '';
|
||||
}
|
||||
@@ -980,8 +1020,7 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
} else if (item.type === 'tool_start') {
|
||||
ensureBotEl();
|
||||
if (currentReasoningEl) {
|
||||
if (reasoningText.trim().replace(/\n+/g, ' ').length <= 80)
|
||||
currentReasoningEl.classList.add('no-expand');
|
||||
finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText);
|
||||
currentReasoningEl = null;
|
||||
reasoningText = '';
|
||||
}
|
||||
@@ -1096,8 +1135,10 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
addBotMessage(finalText, new Date((item.timestamp || Date.now() / 1000) * 1000), requestId);
|
||||
} else if (botEl) {
|
||||
contentEl.classList.remove('sse-streaming');
|
||||
// Only update text content when there is something new to show.
|
||||
if (finalText) contentEl.innerHTML = renderMarkdown(finalText);
|
||||
contentEl.dataset.rawMd = finalText || '';
|
||||
const copyBtn = botEl.querySelector('.copy-msg-btn');
|
||||
if (copyBtn && finalText) copyBtn.style.display = '';
|
||||
applyHighlighting(botEl);
|
||||
}
|
||||
scrollChatToBottom();
|
||||
@@ -1125,8 +1166,7 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
||||
if (done) return;
|
||||
|
||||
if (currentReasoningEl) {
|
||||
if (reasoningText.trim().replace(/\n+/g, ' ').length <= 80)
|
||||
currentReasoningEl.classList.add('no-expand');
|
||||
finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText);
|
||||
currentReasoningEl = null;
|
||||
reasoningText = '';
|
||||
}
|
||||
@@ -1250,28 +1290,24 @@ function renderToolCallsHtml(toolCalls) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function finalizeThinking(el, startTime, text) {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
el.querySelector('.thinking-summary').textContent = t('thinking_done');
|
||||
const fullDiv = el.querySelector('.thinking-full');
|
||||
fullDiv.innerHTML = `<div class="thinking-duration">${t('thinking_duration')} ${elapsed}s</div>` + renderMarkdown(text);
|
||||
}
|
||||
|
||||
function renderThinkingHtml(text) {
|
||||
if (!text || !text.trim()) return '';
|
||||
const full = text.trim();
|
||||
const oneLine = full.replace(/\n+/g, ' ');
|
||||
if (oneLine.length > 80) {
|
||||
const truncated = oneLine.substring(0, 80) + '…';
|
||||
return `
|
||||
return `
|
||||
<div class="agent-step agent-thinking-step">
|
||||
<div class="thinking-header" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<i class="fas fa-lightbulb text-amber-400 flex-shrink-0"></i>
|
||||
<span class="thinking-summary">${escapeHtml(truncated)}</span>
|
||||
<span class="thinking-summary">${t('thinking_done')}</span>
|
||||
<i class="fas fa-chevron-right thinking-chevron"></i>
|
||||
</div>
|
||||
<div class="thinking-full">${renderMarkdown(full)}</div>
|
||||
</div>`;
|
||||
}
|
||||
return `
|
||||
<div class="agent-step agent-thinking-step no-expand">
|
||||
<div class="thinking-header no-toggle">
|
||||
<i class="fas fa-lightbulb text-amber-400 flex-shrink-0"></i>
|
||||
<span>${escapeHtml(oneLine)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1351,9 +1387,15 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
|
||||
${stepsHtml ? `<div class="agent-steps">${stepsHtml}</div>` : ''}
|
||||
<div class="answer-content">${renderMarkdown(displayContent)}</div>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400 dark:text-slate-500 mt-1.5">${formatTime(timestamp)}</div>
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<span class="text-xs text-slate-400 dark:text-slate-500">${formatTime(timestamp)}</span>
|
||||
<button class="copy-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${currentLang === 'zh' ? '复制' : 'Copy'}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
el.querySelector('.answer-content').dataset.rawMd = displayContent;
|
||||
applyHighlighting(el);
|
||||
bindChatKnowledgeLinks(el);
|
||||
return el;
|
||||
@@ -2038,6 +2080,7 @@ function initConfigView(data) {
|
||||
document.getElementById('cfg-max-tokens').value = data.agent_max_context_tokens || 50000;
|
||||
document.getElementById('cfg-max-turns').value = data.agent_max_context_turns || 20;
|
||||
document.getElementById('cfg-max-steps').value = data.agent_max_steps || 20;
|
||||
document.getElementById('cfg-enable-thinking').checked = data.enable_thinking !== false;
|
||||
|
||||
const pwdInput = document.getElementById('cfg-password');
|
||||
const maskedPwd = data.web_password_masked || '';
|
||||
@@ -2272,6 +2315,7 @@ function saveAgentConfig() {
|
||||
agent_max_context_tokens: parseInt(document.getElementById('cfg-max-tokens').value) || 50000,
|
||||
agent_max_context_turns: parseInt(document.getElementById('cfg-max-turns').value) || 20,
|
||||
agent_max_steps: parseInt(document.getElementById('cfg-max-steps').value) || 20,
|
||||
enable_thinking: document.getElementById('cfg-enable-thinking').checked,
|
||||
};
|
||||
|
||||
const btn = document.getElementById('cfg-agent-save');
|
||||
@@ -2497,12 +2541,20 @@ function toggleSkill(name, currentlyEnabled) {
|
||||
// Memory View
|
||||
// =====================================================================
|
||||
let memoryPage = 1;
|
||||
let memoryCategory = 'memory'; // 'memory' | 'dream'
|
||||
const memoryPageSize = 10;
|
||||
|
||||
function switchMemoryTab(tab) {
|
||||
document.querySelectorAll('.memory-tab').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById('memory-tab-' + tab).classList.add('active');
|
||||
memoryCategory = tab === 'dreams' ? 'dream' : 'memory';
|
||||
loadMemoryView(1);
|
||||
}
|
||||
|
||||
function loadMemoryView(page) {
|
||||
page = page || 1;
|
||||
memoryPage = page;
|
||||
fetch(`/api/memory?page=${page}&page_size=${memoryPageSize}`).then(r => r.json()).then(data => {
|
||||
fetch(`/api/memory?page=${page}&page_size=${memoryPageSize}&category=${memoryCategory}`).then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
const emptyEl = document.getElementById('memory-empty');
|
||||
const listEl = document.getElementById('memory-list');
|
||||
@@ -2510,7 +2562,15 @@ function loadMemoryView(page) {
|
||||
const total = data.total || 0;
|
||||
|
||||
if (total === 0) {
|
||||
emptyEl.querySelector('p').textContent = currentLang === 'zh' ? '暂无记忆文件' : 'No memory files';
|
||||
const emptyIcon = emptyEl.querySelector('i');
|
||||
const emptyTitle = emptyEl.querySelector('p');
|
||||
if (memoryCategory === 'dream') {
|
||||
emptyIcon.className = 'fas fa-moon text-purple-400 text-xl';
|
||||
emptyTitle.textContent = currentLang === 'zh' ? '暂无梦境日记' : 'No dream diaries yet';
|
||||
} else {
|
||||
emptyIcon.className = 'fas fa-brain text-purple-400 text-xl';
|
||||
emptyTitle.textContent = currentLang === 'zh' ? '暂无记忆文件' : 'No memory files';
|
||||
}
|
||||
emptyEl.classList.remove('hidden');
|
||||
listEl.classList.add('hidden');
|
||||
return;
|
||||
@@ -2523,10 +2583,15 @@ function loadMemoryView(page) {
|
||||
files.forEach(f => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b border-slate-100 dark:border-white/5 hover:bg-slate-50 dark:hover:bg-white/5 cursor-pointer transition-colors';
|
||||
tr.onclick = () => openMemoryFile(f.filename);
|
||||
const typeLabel = f.type === 'global'
|
||||
? '<span class="px-2 py-0.5 rounded-full text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400">Global</span>'
|
||||
: '<span class="px-2 py-0.5 rounded-full text-xs bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">Daily</span>';
|
||||
tr.onclick = () => openMemoryFile(f.filename, memoryCategory);
|
||||
let typeLabel;
|
||||
if (f.type === 'global') {
|
||||
typeLabel = '<span class="px-2 py-0.5 rounded-full text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400">Global</span>';
|
||||
} else if (f.type === 'dream') {
|
||||
typeLabel = '<span class="px-2 py-0.5 rounded-full text-xs bg-violet-50 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">Dream</span>';
|
||||
} else {
|
||||
typeLabel = '<span class="px-2 py-0.5 rounded-full text-xs bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">Daily</span>';
|
||||
}
|
||||
const sizeStr = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-3 text-sm font-mono text-slate-700 dark:text-slate-200">${escapeHtml(f.filename)}</td>
|
||||
@@ -2548,8 +2613,9 @@ function loadMemoryView(page) {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function openMemoryFile(filename) {
|
||||
fetch(`/api/memory/content?filename=${encodeURIComponent(filename)}`).then(r => r.json()).then(data => {
|
||||
function openMemoryFile(filename, category) {
|
||||
category = category || 'memory';
|
||||
fetch(`/api/memory/content?filename=${encodeURIComponent(filename)}&category=${category}`).then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
document.getElementById('memory-panel-list').classList.add('hidden');
|
||||
const panel = document.getElementById('memory-panel-viewer');
|
||||
@@ -3446,10 +3512,9 @@ navigateTo = function(viewId) {
|
||||
if (viewId === 'config') loadConfigView();
|
||||
else if (viewId === 'skills') loadSkillsView();
|
||||
else if (viewId === 'memory') {
|
||||
// Always start from the list panel when navigating to memory
|
||||
document.getElementById('memory-panel-viewer').classList.add('hidden');
|
||||
document.getElementById('memory-panel-list').classList.remove('hidden');
|
||||
loadMemoryView(1);
|
||||
switchMemoryTab('files');
|
||||
}
|
||||
else if (viewId === 'knowledge') loadKnowledgeView();
|
||||
else if (viewId === 'channels') loadChannelsView();
|
||||
|
||||
@@ -759,7 +759,7 @@ class ConfigHandler:
|
||||
"api_key_field": "minimax_api_key",
|
||||
"api_base_key": None,
|
||||
"api_base_default": None,
|
||||
"models": [const.MINIMAX_M2_7, const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING],
|
||||
"models": [const.MINIMAX_M2_7, const.MINIMAX_M2_7_HIGHSPEED, const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING],
|
||||
}),
|
||||
("zhipu", {
|
||||
"label": "智谱AI",
|
||||
@@ -841,7 +841,7 @@ class ConfigHandler:
|
||||
"zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key",
|
||||
"ark_api_key", "minimax_api_key", "linkai_api_key",
|
||||
"agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps",
|
||||
"web_password",
|
||||
"enable_thinking", "web_password",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -894,6 +894,7 @@ class ConfigHandler:
|
||||
"agent_max_context_tokens": local_config.get("agent_max_context_tokens", 50000),
|
||||
"agent_max_context_turns": local_config.get("agent_max_context_turns", 20),
|
||||
"agent_max_steps": local_config.get("agent_max_steps", 20),
|
||||
"enable_thinking": bool(local_config.get("enable_thinking", True)),
|
||||
"api_bases": api_bases,
|
||||
"api_keys": api_keys_masked,
|
||||
"providers": providers,
|
||||
@@ -919,7 +920,7 @@ class ConfigHandler:
|
||||
continue
|
||||
if key in ("agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps"):
|
||||
value = int(value)
|
||||
if key == "use_linkai":
|
||||
if key in ("use_linkai", "enable_thinking"):
|
||||
value = bool(value)
|
||||
local_config[key] = value
|
||||
applied[key] = value
|
||||
@@ -1537,10 +1538,13 @@ class MemoryHandler:
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.memory.service import MemoryService
|
||||
params = web.input(page='1', page_size='20')
|
||||
params = web.input(page='1', page_size='20', category='memory')
|
||||
workspace_root = _get_workspace_root()
|
||||
service = MemoryService(workspace_root)
|
||||
result = service.list_files(page=int(params.page), page_size=int(params.page_size))
|
||||
result = service.list_files(
|
||||
page=int(params.page), page_size=int(params.page_size),
|
||||
category=params.category,
|
||||
)
|
||||
return json.dumps({"status": "success", **result}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Memory API error: {e}")
|
||||
@@ -1553,12 +1557,12 @@ class MemoryContentHandler:
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.memory.service import MemoryService
|
||||
params = web.input(filename='')
|
||||
params = web.input(filename='', category='memory')
|
||||
if not params.filename:
|
||||
return json.dumps({"status": "error", "message": "filename required"})
|
||||
workspace_root = _get_workspace_root()
|
||||
service = MemoryService(workspace_root)
|
||||
result = service.get_content(params.filename)
|
||||
result = service.get_content(params.filename, category=params.category)
|
||||
return json.dumps({"status": "success", **result}, ensure_ascii=False)
|
||||
except ValueError:
|
||||
return json.dumps({"status": "error", "message": "invalid filename"})
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.0.5
|
||||
2.0.6
|
||||
|
||||
@@ -202,6 +202,7 @@ available_setting = {
|
||||
"agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens
|
||||
"agent_max_context_turns": 20, # Agent模式下最大上下文记忆轮次
|
||||
"agent_max_steps": 20, # Agent模式下单次运行最大决策步数
|
||||
"enable_thinking": True, # Whether to enable deep thinking for web channel
|
||||
"knowledge": True, # 是否开启知识库功能
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
|
||||
```json
|
||||
{
|
||||
"channel_type": "web",
|
||||
"web_port": 9899
|
||||
"web_port": 9899,
|
||||
"web_password": "",
|
||||
"enable_thinking": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -18,6 +20,11 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
|
||||
| --- | --- | --- |
|
||||
| `channel_type` | 设为 `web` | `web` |
|
||||
| `web_port` | Web 服务监听端口 | `9899` |
|
||||
| `web_password` | 访问密码,留空表示不启用密码保护 | `""` |
|
||||
| `web_session_expire_days` | 登录会话有效天数 | `30` |
|
||||
| `enable_thinking` | 是否启用深度思考,开启后 Web 端展示推理过程,关闭可加速响应 | `true` |
|
||||
|
||||
配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。密码也支持在控制台的「配置」页面中在线修改。
|
||||
|
||||
## 访问地址
|
||||
|
||||
@@ -30,30 +37,11 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
|
||||
请确保服务器防火墙和安全组已放行对应端口。
|
||||
</Note>
|
||||
|
||||
## 密码保护
|
||||
|
||||
Web 控制台默认无需密码即可访问。如果部署在公网环境,建议配置访问密码:
|
||||
|
||||
```json
|
||||
{
|
||||
"web_password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| `web_password` | 访问密码,留空表示不启用密码保护 | `""` |
|
||||
| `web_session_expire_days` | 登录会话有效天数 | `30` |
|
||||
|
||||
配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。修改密码后,所有已登录的会话将自动失效。
|
||||
|
||||
密码也支持在控制台的「配置」页面中在线修改。
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 对话界面
|
||||
|
||||
支持流式输出,可实时展示 Agent 的思考过程(Reasoning)和工具调用过程(Tool Calls),更直观地观察 Agent 的决策过程:
|
||||
支持流式输出,可实时展示 Agent 的思考过程(Reasoning)和工具调用过程(Tool Calls),更直观地观察 Agent 的决策过程。深度思考功能可通过配置或控制台的「Agent 配置」开关控制。
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227180120.png" />
|
||||
|
||||
|
||||
@@ -106,45 +106,6 @@ Session: 12 messages | 8 skills loaded
|
||||
/logs 50
|
||||
```
|
||||
|
||||
## knowledge
|
||||
|
||||
查看和管理个人知识库。默认显示知识库统计信息。
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
📚 知识库
|
||||
|
||||
- 状态:已开启
|
||||
- 页面数:12
|
||||
- 总大小:45.2 KB
|
||||
- 分类明细:
|
||||
- concepts/: 5 篇
|
||||
- entities/: 4 篇
|
||||
- sources/: 3 篇
|
||||
```
|
||||
|
||||
**查看目录结构:**
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
**开启 / 关闭知识库:**
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
终端 CLI 中 `cow knowledge` 和 `cow knowledge list` 可用,但 `on|off` 仅支持在对话中使用(需实时生效)。
|
||||
</Note>
|
||||
|
||||
## version
|
||||
|
||||
显示当前 CowAgent 版本号。
|
||||
|
||||
@@ -40,7 +40,8 @@ Service:
|
||||
Skills:
|
||||
skill Manage skills (list / search / install / uninstall ...)
|
||||
|
||||
Knowledge:
|
||||
Memory & Knowledge:
|
||||
memory Memory distillation (dream)
|
||||
knowledge View knowledge base stats and structure
|
||||
|
||||
Others:
|
||||
@@ -58,6 +59,7 @@ Others:
|
||||
| `/status` | 查看服务状态和配置 |
|
||||
| `/config` | 查看或修改运行时配置 |
|
||||
| `/skill` | 管理技能(安装、卸载、启用、禁用等) |
|
||||
| `/memory dream [N]` | 手动触发记忆蒸馏(默认 3 天,最大 30) |
|
||||
| `/knowledge` | 查看知识库统计信息 |
|
||||
| `/knowledge list` | 查看知识库目录结构 |
|
||||
| `/knowledge on\|off` | 开启或关闭知识库 |
|
||||
@@ -82,6 +84,7 @@ Others:
|
||||
| logs | ✓ | ✓ |
|
||||
| config | ✗ | ✓ |
|
||||
| context | — | ✓ |
|
||||
| memory (子命令) | ✗ | ✓ |
|
||||
| knowledge (子命令) | ✓ | ✓ |
|
||||
| skill (子命令) | ✓ | ✓ |
|
||||
| start / stop / restart | ✓ | ✗ |
|
||||
|
||||
77
docs/cli/memory-knowledge.mdx
Normal file
77
docs/cli/memory-knowledge.mdx
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: 记忆与知识库
|
||||
description: 记忆蒸馏和知识库管理命令
|
||||
---
|
||||
|
||||
## memory
|
||||
|
||||
管理 Agent 的长期记忆系统。
|
||||
|
||||
### memory dream
|
||||
|
||||
手动触发记忆蒸馏(Deep Dream),整理近期的天级记忆,蒸馏合并到 MEMORY.md,并生成梦境日记。
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`:整理近 N 天的记忆,默认 3 天,最大 30 天
|
||||
- 蒸馏在后台异步执行,完成后会在对话中通知结果
|
||||
- 无需等待 Agent 初始化,首次对话前即可使用
|
||||
|
||||
**示例:**
|
||||
|
||||
```text
|
||||
/memory dream # 整理近 3 天
|
||||
/memory dream 7 # 整理近 7 天
|
||||
/memory dream 30 # 整理近 30 天(全量)
|
||||
```
|
||||
|
||||
蒸馏完成后,Web 端会收到带有跳转链接的通知,可直接查看更新后的 MEMORY.md 和梦境日记。
|
||||
|
||||
<Tip>
|
||||
系统每天 23:55 会自动执行一次蒸馏(lookback 1 天)。手动触发适用于首次部署后的历史整理,或需要立即更新记忆时使用。
|
||||
</Tip>
|
||||
|
||||
## knowledge
|
||||
|
||||
查看和管理个人知识库。默认显示知识库统计信息。
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
📚 知识库
|
||||
|
||||
- 状态:已开启
|
||||
- 页面数:12
|
||||
- 总大小:45.2 KB
|
||||
- 分类明细:
|
||||
- concepts/: 5 篇
|
||||
- entities/: 4 篇
|
||||
- sources/: 3 篇
|
||||
```
|
||||
|
||||
### knowledge list
|
||||
|
||||
查看知识库目录树结构。
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
### knowledge on / off
|
||||
|
||||
开启或关闭知识库。关闭后不再注入知识提示词和索引知识文件。
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
终端 CLI 中 `cow knowledge` 和 `cow knowledge list` 可用,但 `on|off` 仅支持在对话中使用(需实时生效)。
|
||||
</Note>
|
||||
@@ -142,7 +142,8 @@
|
||||
"group": "记忆系统",
|
||||
"pages": [
|
||||
"memory/index",
|
||||
"memory/context"
|
||||
"memory/context",
|
||||
"memory/deep-dream"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -185,6 +186,7 @@
|
||||
"cli/index",
|
||||
"cli/process",
|
||||
"cli/skill",
|
||||
"cli/memory-knowledge",
|
||||
"cli/general"
|
||||
]
|
||||
}
|
||||
@@ -197,6 +199,7 @@
|
||||
"group": "发布记录",
|
||||
"pages": [
|
||||
"releases/overview",
|
||||
"releases/v2.0.6",
|
||||
"releases/v2.0.5",
|
||||
"releases/v2.0.4",
|
||||
"releases/v2.0.3",
|
||||
@@ -314,7 +317,8 @@
|
||||
"group": "Memory System",
|
||||
"pages": [
|
||||
"en/memory/index",
|
||||
"en/memory/context"
|
||||
"en/memory/context",
|
||||
"en/memory/deep-dream"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -357,6 +361,7 @@
|
||||
"en/cli/index",
|
||||
"en/cli/process",
|
||||
"en/cli/skill",
|
||||
"en/cli/memory-knowledge",
|
||||
"en/cli/chat"
|
||||
]
|
||||
}
|
||||
@@ -369,6 +374,7 @@
|
||||
"group": "Release Notes",
|
||||
"pages": [
|
||||
"en/releases/overview",
|
||||
"en/releases/v2.0.6",
|
||||
"en/releases/v2.0.5",
|
||||
"en/releases/v2.0.4",
|
||||
"en/releases/v2.0.2",
|
||||
@@ -486,7 +492,8 @@
|
||||
"group": "メモリシステム",
|
||||
"pages": [
|
||||
"ja/memory/index",
|
||||
"ja/memory/context"
|
||||
"ja/memory/context",
|
||||
"ja/memory/deep-dream"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -529,6 +536,7 @@
|
||||
"ja/cli/index",
|
||||
"ja/cli/process",
|
||||
"ja/cli/skill",
|
||||
"ja/cli/memory-knowledge",
|
||||
"ja/cli/general"
|
||||
]
|
||||
}
|
||||
@@ -541,6 +549,7 @@
|
||||
"group": "リリースノート",
|
||||
"pages": [
|
||||
"ja/releases/overview",
|
||||
"ja/releases/v2.0.6",
|
||||
"ja/releases/v2.0.5",
|
||||
"ja/releases/v2.0.4",
|
||||
"ja/releases/v2.0.3",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
> CowAgent is both an out-of-the-box AI super assistant and a highly extensible Agent framework. You can extend it with new model interfaces, channels, built-in tools, and the Skills system to flexibly implement various customization needs.
|
||||
|
||||
- ✅ **Autonomous Task Planning**: Understands complex tasks and autonomously plans execution, continuously thinking and invoking tools until goals are achieved.
|
||||
- ✅ **Long-term Memory**: Automatically persists conversation memory to local files and databases, including core memory and daily memory, with keyword and vector retrieval support.
|
||||
- ✅ **Long-term Memory**: Automatically persists conversation memory to local files and databases, including core memory, daily memory, and Deep Dream distillation, with keyword and vector retrieval support.
|
||||
- ✅ **Personal Knowledge Base**: Automatically organizes structured knowledge with cross-references to build a knowledge graph, with web-based visualization and conversational management.
|
||||
- ✅ **Skills System**: Implements a Skills creation and execution engine, supports installing skills from [Skill Hub](https://skills.cowagent.ai), GitHub, etc., or creating custom Skills through conversation.
|
||||
- ✅ **Tool System**: Built-in tools for file I/O, terminal execution, browser automation, scheduled tasks, messaging, and more — autonomously invoked by the Agent.
|
||||
@@ -43,6 +43,8 @@ Try online (no deployment needed): [CowAgent](https://link-ai.tech/cowagent/crea
|
||||
|
||||
## Changelog
|
||||
|
||||
> **2026.04.14:** [v2.0.6](https://github.com/zhayujie/CowAgent/releases/tag/2.0.6) — Knowledge Base, Deep Dream Memory Distillation, Smart Context Compression, Web Console upgrades.
|
||||
|
||||
> **2026.04.01:** [v2.0.5](https://github.com/zhayujie/CowAgent/releases/tag/2.0.5) — Cow CLI, Skill Hub open source, Browser tool, WeCom Bot QR scan, and more.
|
||||
|
||||
> **2026.02.27:** [v2.0.2](https://github.com/zhayujie/CowAgent/releases/tag/2.0.2) — Web console overhaul (streaming chat, model/skill/memory/channel/scheduler/log management), multi-channel concurrent running, session persistence, new models including Gemini 3.1 Pro / Claude 4.6 Sonnet / Qwen3.5 Plus.
|
||||
|
||||
@@ -92,31 +92,6 @@ View recent service logs. Shows the last 20 lines by default, up to 50.
|
||||
/logs 50
|
||||
```
|
||||
|
||||
## knowledge
|
||||
|
||||
View and manage the personal knowledge base. Shows statistics by default.
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
**View directory structure:**
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
**Enable / disable knowledge base:**
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
In the terminal CLI, `cow knowledge` and `cow knowledge list` are available, but `on|off` is only supported in chat (requires runtime effect).
|
||||
</Note>
|
||||
|
||||
## version
|
||||
|
||||
Show the current CowAgent version.
|
||||
|
||||
@@ -40,7 +40,8 @@ Service:
|
||||
Skills:
|
||||
skill Manage skills (list / search / install / uninstall ...)
|
||||
|
||||
Knowledge:
|
||||
Memory & Knowledge:
|
||||
memory Memory distillation (dream)
|
||||
knowledge View knowledge base stats and structure
|
||||
|
||||
Others:
|
||||
@@ -58,6 +59,7 @@ In the Web console or any connected channel, type `/` to see command suggestions
|
||||
| `/status` | View service status and configuration |
|
||||
| `/config` | View or modify runtime configuration |
|
||||
| `/skill` | Manage skills (install, uninstall, enable, disable, etc.) |
|
||||
| `/memory dream [N]` | Manually trigger memory distillation (default 3 days, max 30) |
|
||||
| `/knowledge` | View knowledge base statistics |
|
||||
| `/knowledge list` | View knowledge base directory structure |
|
||||
| `/knowledge on\|off` | Enable or disable knowledge base |
|
||||
@@ -80,6 +82,7 @@ In the Web console or any connected channel, type `/` to see command suggestions
|
||||
| logs | ✓ | ✓ |
|
||||
| config | ✗ | ✓ |
|
||||
| context | — | ✓ |
|
||||
| memory (subcommands) | ✗ | ✓ |
|
||||
| knowledge (subcommands) | ✓ | ✓ |
|
||||
| skill (subcommands) | ✓ | ✓ |
|
||||
| start / stop / restart | ✓ | ✗ |
|
||||
|
||||
63
docs/en/cli/memory-knowledge.mdx
Normal file
63
docs/en/cli/memory-knowledge.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Memory & Knowledge
|
||||
description: Memory distillation and knowledge base management commands
|
||||
---
|
||||
|
||||
## memory
|
||||
|
||||
Manage the Agent's long-term memory system.
|
||||
|
||||
### memory dream
|
||||
|
||||
Manually trigger memory distillation (Deep Dream) — consolidate recent daily memories into MEMORY.md and generate a dream diary.
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`: Consolidate the last N days of memory (default 3, max 30)
|
||||
- Runs asynchronously in the background; you'll be notified in chat when complete
|
||||
- Works without Agent initialization — can be used before the first conversation
|
||||
|
||||
**Examples:**
|
||||
|
||||
```text
|
||||
/memory dream # Consolidate last 3 days
|
||||
/memory dream 7 # Consolidate last 7 days
|
||||
/memory dream 30 # Consolidate last 30 days (full)
|
||||
```
|
||||
|
||||
On the Web console, the completion notification includes clickable links to view the updated MEMORY.md and dream diary.
|
||||
|
||||
<Tip>
|
||||
The system automatically runs distillation daily at 23:55 (lookback 1 day). Manual trigger is useful for consolidating historical memories after first deployment, or when you need an immediate memory update.
|
||||
</Tip>
|
||||
|
||||
## knowledge
|
||||
|
||||
View and manage the personal knowledge base. Shows statistics by default.
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
### knowledge list
|
||||
|
||||
View the knowledge base directory tree.
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
### knowledge on / off
|
||||
|
||||
Enable or disable the knowledge base. When disabled, knowledge prompts and file indexing are not injected.
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
In the terminal CLI, `cow knowledge` and `cow knowledge list` are available, but `on|off` is only supported in chat (requires runtime effect).
|
||||
</Note>
|
||||
@@ -5,16 +5,18 @@ description: CowAgent long-term memory, task planning, skills system, CLI comman
|
||||
|
||||
## 1. Long-term Memory
|
||||
|
||||
The memory system enables the Agent to remember important information over time. The Agent proactively stores information when users share preferences, decisions, or key facts, and automatically extracts summaries when conversations reach a certain length. Memory is divided into core memory and daily memory, with hybrid retrieval supporting both keyword search and vector search.
|
||||
The memory system enables the Agent to remember important information over time, using a three-tier memory flow: conversation context (short-term) → daily memory (mid-term) → MEMORY.md (long-term), forming a complete memory lifecycle.
|
||||
|
||||
On first launch, the Agent proactively asks the user for key information and records it in the workspace (default `~/cow`) — including agent settings, user identity, and memory files.
|
||||
|
||||
In subsequent long-term conversations, the Agent intelligently stores or retrieves memory as needed, continuously updating its own settings, user preferences, and memory files, summarizing experiences and lessons learned — truly achieving autonomous thinking and continuous growth.
|
||||
In subsequent long-term conversations, the Agent intelligently stores or retrieves memory as needed, continuously updating its own settings, user preferences, and memory files. **Deep Dream** distillation runs daily, consolidating scattered daily memories into refined long-term memory and generating a narrative-style dream diary.
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
See [Long-term Memory](/en/memory) and [Deep Dream](/en/memory/deep-dream) for details.
|
||||
|
||||
## 2. Personal Knowledge Base
|
||||
|
||||
> The knowledge base system enables the Agent to continuously accumulate and organize structured knowledge. Unlike memory which records along a timeline, the knowledge base is organized by topics, transforming articles, conversation insights, and learning materials into interconnected Markdown pages that form a continuously growing knowledge network.
|
||||
@@ -26,6 +28,10 @@ The Agent automatically organizes valuable information from conversations into k
|
||||
- **Chat integration**: Knowledge document links referenced in Agent replies can be clicked directly in the Web console for viewing
|
||||
- **CLI management**: Use `/knowledge` commands to view stats, browse directory, and toggle the feature with `/knowledge on|off`
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
See [Personal Knowledge Base](/en/knowledge) for details.
|
||||
|
||||
## 3. Task Planning and Tool Use
|
||||
@@ -47,7 +53,7 @@ Access to the OS terminal and file system is the most fundamental and core capab
|
||||
Combining programming and system access, the Agent can execute the complete **Vibecoding workflow** — from information search, asset generation, coding, testing, deployment, Nginx configuration, to publishing — all triggered by a single command from your phone:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203121008.png" width="800" />
|
||||
<img src="https://cdn.link-ai.tech/doc/20260318211018.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.3 Scheduled Tasks
|
||||
|
||||
@@ -20,7 +20,7 @@ CowAgent can proactively think and plan tasks, operate computers and external re
|
||||
Understands complex tasks and autonomously plans execution, continuously thinking and invoking tools until goals are achieved. Supports accessing file systems, terminals, browsers, schedulers, and other system resources through tools.
|
||||
</Card>
|
||||
<Card title="Long-term Memory" icon="database" href="/en/memory">
|
||||
Automatically persists conversation memory to local files and databases, including core memory and daily memory, with keyword and vector retrieval support.
|
||||
Three-tier memory flow (context → daily memory → global memory) with daily Deep Dream distillation, keyword and vector retrieval support.
|
||||
</Card>
|
||||
<Card title="Knowledge Base" icon="book" href="/en/knowledge">
|
||||
Automatically organizes structured knowledge with knowledge graph visualization, building a continuously growing knowledge network through cross-references.
|
||||
|
||||
@@ -39,14 +39,15 @@ When conversation turns exceed `agent_max_context_turns`:
|
||||
|
||||
- The **oldest half** of complete turns is trimmed (preserving tool call chain integrity)
|
||||
- Trimmed messages are summarized by LLM and **written to the daily memory file**
|
||||
- Remaining turns stay intact
|
||||
- Once the LLM summary is ready, it is also **injected into the first user message** of the retained context, helping the model maintain conversational continuity
|
||||
- Summary injection runs asynchronously in the background and takes effect from the next turn onward
|
||||
|
||||
### 3. Token Budget Trimming
|
||||
|
||||
After turn trimming, if tokens still exceed the budget:
|
||||
|
||||
- **Fewer than 5 turns**: All turns undergo **text compression** — each turn keeps only the first user text and last Agent reply, removing intermediate tool call chains
|
||||
- **5 or more turns**: The **first half** of turns is trimmed again, with discarded content also written to memory
|
||||
- **5 or more turns**: The **first half** of turns is trimmed again, with discarded content written to memory and a context summary injected
|
||||
|
||||
### 4. Overflow Emergency Handling
|
||||
|
||||
|
||||
90
docs/en/memory/deep-dream.mdx
Normal file
90
docs/en/memory/deep-dream.mdx
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Deep Dream
|
||||
description: Deep Dream — automatic distillation from conversations to permanent memory
|
||||
---
|
||||
|
||||
Deep Dream is the core consolidation mechanism of CowAgent's memory system, responsible for distilling scattered daily memories into refined long-term memory and generating dream diaries.
|
||||
|
||||
## Memory Flow
|
||||
|
||||
CowAgent's memory progresses through three stages from short-term to long-term:
|
||||
|
||||
```
|
||||
Conversation context (short-term) → Daily memory (mid-term) → MEMORY.md (long-term)
|
||||
```
|
||||
|
||||
### 1. Conversation → Daily Memory
|
||||
|
||||
When conversation context is trimmed or during the daily scheduled summary, the system uses LLM to summarize conversation content into key events, writing them to the daily memory file `memory/YYYY-MM-DD.md`.
|
||||
|
||||
Triggers:
|
||||
- **Context trimming** — Trimmed content is summarized when turn or token limits are exceeded
|
||||
- **Daily schedule** — Automatically triggered at 23:55
|
||||
- **API overflow** — Emergency save of current conversation summary
|
||||
|
||||
### 2. Daily Memory → MEMORY.md (Distillation)
|
||||
|
||||
After the daily summary completes, Deep Dream automatically runs distillation:
|
||||
|
||||
1. **Read materials** — Current `MEMORY.md` + today's daily memory
|
||||
2. **LLM distillation** — Deduplicate, merge, prune, extract new information
|
||||
3. **Overwrite MEMORY.md** — Output the refined long-term memory
|
||||
4. **Generate dream diary** — Record discoveries and insights from the consolidation
|
||||
|
||||
### 3. Role of MEMORY.md
|
||||
|
||||
`MEMORY.md` is injected into the system prompt for every conversation, keeping the Agent aware of user preferences, decisions, and key facts. Therefore it must stay concise — Deep Dream targets approximately 30 entries or fewer.
|
||||
|
||||
## Distillation Rules
|
||||
|
||||
Deep Dream follows these consolidation rules:
|
||||
|
||||
| Operation | Description |
|
||||
| --- | --- |
|
||||
| **Merge & refine** | Combine similar entries into single high-density statements |
|
||||
| **Extract new** | Pull preferences, decisions, people, experiences from daily memory |
|
||||
| **Conflict update** | When new info contradicts old entries, newer info takes precedence |
|
||||
| **Clean invalid** | Remove temporary records, blank entries, formatting artifacts |
|
||||
| **Remove redundancy** | Delete old entries already covered by more refined statements |
|
||||
|
||||
## Dream Diary
|
||||
|
||||
Each distillation generates a dream diary saved at `memory/dreams/YYYY-MM-DD.md`, written in a narrative style recording:
|
||||
|
||||
- Duplications or contradictions found
|
||||
- New insights extracted from daily memory
|
||||
- Cleanups and optimizations performed
|
||||
- Overall observations
|
||||
|
||||
Dream diaries can be viewed in the Web console under "Memory → Dream Diary" tab.
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414110032.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## Manual Trigger
|
||||
|
||||
In addition to the automatic daily run, you can manually trigger distillation in chat:
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`: Consolidate the last N days of memory (default 3, max 30)
|
||||
- Runs asynchronously in the background; you'll be notified in chat when complete
|
||||
- Web notifications include clickable links to view MEMORY.md and dream diary
|
||||
- Works without Agent initialization — can be used before the first conversation
|
||||
|
||||
<Tip>
|
||||
After first deployment, it's recommended to run `/memory dream 30` once to distill all historical daily memories into MEMORY.md.
|
||||
</Tip>
|
||||
|
||||
## Safety Mechanisms
|
||||
|
||||
| Mechanism | Description |
|
||||
| --- | --- |
|
||||
| **Skip on no content** | Distillation skipped when no daily memory exists, avoiding empty overwrites |
|
||||
| **Input dedup** | In scheduled tasks, automatically skipped when input materials haven't changed |
|
||||
| **Async execution** | Distillation runs in a background thread, never blocking conversation |
|
||||
| **Sequential guarantee** | In scheduled tasks, daily flush completes before distillation starts |
|
||||
| **No fabrication** | Prompt explicitly constrains consolidation to existing materials only |
|
||||
@@ -15,12 +15,17 @@ Stored in `~/cow/MEMORY.md`, containing long-term user preferences, important de
|
||||
|
||||
Stored in `~/cow/memory/` directory, named by date (e.g., `2026-03-08.md`), recording daily conversation summaries and key events. Files are only created on first write to avoid generating empty files.
|
||||
|
||||
### Dream Diary (memory/dreams/YYYY-MM-DD.md)
|
||||
|
||||
A byproduct of the Deep Dream (memory distillation) process, recording discoveries, deduplication operations, and new insights from each consolidation. Stored in `~/cow/memory/dreams/` directory, named by date.
|
||||
|
||||
## Automatic Writing
|
||||
|
||||
The Agent automatically persists conversation content to long-term memory through the following mechanisms:
|
||||
|
||||
- **On context trimming** — When conversation turns or tokens exceed the configured limit, the oldest half of the context is trimmed, and the discarded content is summarized by LLM into key information and written to the daily memory file
|
||||
- **On context trimming** — When conversation turns or tokens exceed the configured limit, the oldest half of the context is trimmed, and the discarded content is summarized by LLM into key information and written to the daily memory file. The summary is also asynchronously injected into the retained context for conversational continuity
|
||||
- **Daily scheduled summary** — A full summary is automatically triggered at 23:55 every day, ensuring memory is preserved even on low-activity days (skipped if content hasn't changed)
|
||||
- **[Deep Dream (memory distillation)](/en/memory/deep-dream)** — Runs automatically after the daily summary, distilling daily memories into MEMORY.md and generating a dream diary
|
||||
- **On API context overflow** — When the model API returns a context overflow error, the current conversation summary is saved as an emergency measure
|
||||
|
||||
All memory writes run asynchronously in a background thread (LLM summarization + file writing), never blocking normal conversation replies.
|
||||
@@ -44,6 +49,7 @@ On first launch, the Agent will proactively ask the user for key information and
|
||||
| `user.md` | User identity information and preferences |
|
||||
| `MEMORY.md` | Core memory (long-term) |
|
||||
| `memory/YYYY-MM-DD.md` | Daily memory (created on demand) |
|
||||
| `memory/dreams/YYYY-MM-DD.md` | Dream diary (auto-generated by Deep Dream) |
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
|
||||
@@ -5,6 +5,7 @@ description: CowAgent version history
|
||||
|
||||
| Version | Date | Description |
|
||||
| --- | --- | --- |
|
||||
| [2.0.6](/en/releases/v2.0.6) | 2026.04.14 | Knowledge Base, Deep Dream Memory Distillation, Smart Context Compression, Web Console upgrades |
|
||||
| [2.0.5](/en/releases/v2.0.5) | 2026.04.01 | Cow CLI, Skill Hub open source, Browser tool, WeCom Bot QR scan, and more |
|
||||
| [2.0.4](/en/releases/v2.0.4) | 2026.03.22 | Personal WeChat channel, new model support, Japanese docs, script refactoring and bug fixes |
|
||||
| [2.0.2](/en/releases/v2.0.2) | 2026.02.27 | Web Console upgrade, multi-channel concurrency, session persistence |
|
||||
|
||||
83
docs/en/releases/v2.0.6.mdx
Normal file
83
docs/en/releases/v2.0.6.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: v2.0.6
|
||||
description: CowAgent 2.0.6 - Knowledge Base, Deep Dream Memory Distillation, Smart Context Compression, Web Console Multi-Session and More
|
||||
---
|
||||
|
||||
## Project Renamed to CowAgent
|
||||
|
||||
The repository has been officially renamed from `chatgpt-on-wechat` to **CowAgent**, evolving into a full-featured AI Agent assistant.
|
||||
|
||||
- New URL: [github.com/zhayujie/CowAgent](https://github.com/zhayujie/CowAgent) — GitHub auto-redirects the old URL
|
||||
- CLI commands, config files, and documentation links remain compatible — no extra steps needed
|
||||
|
||||
## 📚 Knowledge Base
|
||||
|
||||
New personal knowledge base system — Agent can autonomously build and maintain structured knowledge, retrieving it on demand during conversations:
|
||||
|
||||
- **Index-driven self-organizing structure**: Knowledge is stored in `knowledge/` directory, auto-organized by category, with each knowledge page as an independent Markdown file
|
||||
- **Auto-write**: Send files, links, or other knowledge to the Agent, or it will automatically create/update knowledge pages when valuable information is identified in conversation
|
||||
- **Hybrid retrieval**: Supports keyword full-text search and vector semantic retrieval, loading relevant knowledge on demand during conversations
|
||||
- **Visualization**: File tree browsing and knowledge graph visualization, with in-document links for direct navigation
|
||||
- **Command management**: `/knowledge` for stats, `/knowledge list` for directory structure, `/knowledge on|off` to toggle
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="750" />
|
||||
|
||||
|
||||
Docs: [Knowledge Base](https://docs.cowagent.ai/en/knowledge)
|
||||
|
||||
## 🌙 Deep Dream Memory Distillation
|
||||
|
||||
A new memory consolidation mechanism that automatically distills scattered conversation memories into refined long-term memory daily:
|
||||
|
||||
- **Three-tier memory flow**: Conversation context (short-term) → Daily memory (mid-term) → MEMORY.md (long-term), forming a complete memory lifecycle
|
||||
- **Auto-distillation**: Runs daily at 23:55, reads the day's daily memory and MEMORY.md, performs deduplication, merging, and pruning via LLM, outputting a refined MEMORY.md
|
||||
- **Dream diary**: Each distillation generates a narrative-style dream diary recording discoveries and insights, stored in `memory/dreams/`
|
||||
- **Manual trigger**: `/memory dream [N]` to manually trigger with configurable lookback days (default 3, max 30), with chat notification on completion
|
||||
- **Web console**: Memory management page now includes a "Dream Diary" tab for browsing all dream diaries
|
||||
|
||||
Docs: [Deep Dream](https://docs.cowagent.ai/en/memory/deep-dream)
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414120158.png" width="750" />
|
||||
|
||||
## 🧠 Smart Context Compression
|
||||
|
||||
When context exceeds limits, trimmed portions are summarized by LLM and asynchronously injected to maintain conversation continuity:
|
||||
|
||||
- **Async LLM summary**: Trimmed messages are summarized into key information by LLM, written to daily memory files and injected into retained context
|
||||
- **Multi-model compatible**: Uses the primary model for summarization, compatible with Claude, OpenAI, MiniMax and other model message format requirements
|
||||
|
||||
Docs: [Short-term Memory](https://docs.cowagent.ai/en/memory/context)
|
||||
|
||||
## 💬 Web Console Upgrades
|
||||
|
||||
Multiple enhancements to the Web console:
|
||||
|
||||
- **Multi-session management**: Create and switch between independent sessions, sidebar session list with auto-generated and manually editable titles
|
||||
- **Password protection**: Set a login password via `web_console_password` config option
|
||||
- **Deep thinking**: Display model thinking process in Web console, controlled by `enable_thinking` config option
|
||||
- **Scheduled push**: Scheduled task results can be pushed to Web console
|
||||
- **Message copy**: One-click copy of raw Markdown content from AI reply bubbles
|
||||
- **Language toggle**: Top language switch button now shows current language for more intuitive interaction
|
||||
|
||||
## 🤖 Model Updates
|
||||
|
||||
- **Vision optimization**: Image recognition tool prefers the primary model with automatic multi-provider fallback. Docs: [Vision Tool](https://docs.cowagent.ai/en/tools/vision)
|
||||
- **MiniMax new model**: Added MiniMax-M2.7-highspeed model and MiniMax TTS voice synthesis support. Thanks @octo-patch
|
||||
- **Qwen**: Added qwen3.6-plus model support
|
||||
|
||||
## 🐛 Other Improvements & Fixes
|
||||
|
||||
- **Memory prompts**: `MEMORY.md` injected into system prompt by default, with refined memory retrieval and write trigger conditions for enhanced proactive writing
|
||||
- **System prompt**: Optimized system prompt style and tone guidance
|
||||
- **Browser tool**: Enhanced implicit interactive element detection
|
||||
- **File send**: Fixed common file types (tar.gz, zip, etc.) not being sent correctly. Thanks @6vision
|
||||
- **macOS compatibility**: Fixed network pre-check timeout compatibility issue. Thanks @Moliang Zhou
|
||||
- **Windows compatibility**: Fixed PowerShell compatibility, process updates, terminal encoding and other issues on Windows
|
||||
- **Python 3.13+**: Fixed missing `legacy-cgi` dependency for Python 3.13+
|
||||
- **WeChat channel**: Updated personal WeChat channel version
|
||||
|
||||
## 📦 Upgrade
|
||||
|
||||
Run `cow update` or `./run.sh update` to upgrade, or pull the latest code and restart. See [Upgrade Guide](https://docs.cowagent.ai/en/guide/upgrade).
|
||||
|
||||
**Release Date**: 2026.04.14 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.5...master)
|
||||
@@ -69,7 +69,8 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词
|
||||
"agent_workspace": "~/cow",
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
"agent_max_steps": 15,
|
||||
"enable_thinking": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,4 +81,5 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词
|
||||
| `agent_max_context_tokens` | 最大上下文 token 数 | `50000` |
|
||||
| `agent_max_context_turns` | 最大上下文记忆轮次 | `20` |
|
||||
| `agent_max_steps` | 单次任务最大决策步数 | `20` |
|
||||
| `enable_thinking` | 是否启用深度思考,开启后 Web 端展示推理过程,关闭可加速响应 | `true` |
|
||||
| `knowledge` | 是否启用个人知识库 | `true` |
|
||||
|
||||
@@ -5,16 +5,18 @@ description: CowAgent 长期记忆、个人知识库、任务规划、技能系
|
||||
|
||||
## 1. 长期记忆
|
||||
|
||||
> 记忆系统让 Agent 能够长期记住重要信息。Agent 会在用户分享偏好、决策、事实等重要信息时主动存储,也会在对话达到一定长度时自动提取摘要。记忆分为核心记忆、天级记忆,支持语义搜索和向量检索的混合检索模式。
|
||||
> 记忆系统让 Agent 能够长期记住重要信息,采用三层记忆流转架构:对话上下文(短期)→ 天级记忆(中期)→ MEMORY.md(长期),形成完整的记忆生命周期。
|
||||
|
||||
第一次启动 Agent 时,Agent 会主动询问关键信息,并记录至工作空间(默认 `~/cow`)中的智能体设定、用户身份、记忆文件中。
|
||||
|
||||
在后续的长期对话中,Agent 会在需要时智能记录或检索记忆,并对自身设定、用户偏好、记忆文件等进行不断更新,总结和记录经验和教训,真正实现自主思考和不断成长。
|
||||
在后续的长期对话中,Agent 会在需要时智能记录或检索记忆,并对自身设定、用户偏好、记忆文件等进行不断更新。每日自动执行 **梦境蒸馏(Deep Dream)**,将分散的天级记忆整合为精炼的长期记忆,同时生成叙事风格的梦境日记。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
详细说明请参考 [长期记忆](/memory) 和 [梦境蒸馏](/memory/deep-dream)。
|
||||
|
||||
## 2. 个人知识库
|
||||
|
||||
> 知识库系统让 Agent 能够持续积累和组织结构化知识。与按时间线记录的记忆不同,知识库以主题为维度,将文章、对话洞察、学习材料等整理为互相关联的 Markdown 页面,形成持续增长的知识网络。
|
||||
@@ -26,6 +28,10 @@ Agent 会在对话中自动将有价值的信息整理为知识页面,维护
|
||||
- **对话联动**:Agent 回复中引用的知识文档链接可在 Web 控制台中直接点击跳转查看
|
||||
- **CLI 管理**:通过 `/knowledge` 命令查看统计、浏览目录,通过 `/knowledge on|off` 开关功能
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
详细说明请参考 [个人知识库](/knowledge)。
|
||||
|
||||
## 3. 任务规划和工具调用
|
||||
@@ -47,7 +53,7 @@ Agent 会在对话中自动将有价值的信息整理为知识页面,维护
|
||||
基于编程能力和系统访问能力,Agent 可以实现从信息搜索、图片等素材生成、编码、测试、部署、Nginx 配置修改、发布的 **Vibecoding 全流程**,通过手机端简单的一句命令完成应用的快速 demo:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203121008.png" width="800" />
|
||||
<img src="https://cdn.link-ai.tech/doc/20260318211018.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.3 定时任务
|
||||
|
||||
@@ -25,7 +25,7 @@ CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、
|
||||
能够理解复杂任务并自主规划执行,持续思考和调用各类工具和技能直到完成目标。
|
||||
</Card>
|
||||
<Card title="长期记忆" icon="database" href="/memory">
|
||||
自动将对话记忆持久化至本地文件和数据库中,包括全局记忆和天级记忆,支持关键词及向量检索。
|
||||
三层记忆流转(上下文→天级记忆→全局记忆),每日梦境蒸馏整理,支持关键词及向量检索。
|
||||
</Card>
|
||||
<Card title="个人知识库" icon="book" href="/knowledge">
|
||||
自动整理结构化知识,支持知识图谱可视化,通过交叉引用构建持续增长的知识网络。
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
> CowAgentは、すぐに使えるAIスーパーアシスタントであると同時に、高い拡張性を持つAgentフレームワークでもあります。新しいモデルインターフェース、チャネル、組み込みツール、Skillシステムを拡張することで、さまざまなカスタマイズニーズに柔軟に対応できます。
|
||||
|
||||
- ✅ **自律的タスク計画**: 複雑なタスクを理解し、自律的に実行計画を立て、目標達成までツールを呼び出しながら継続的に思考します。
|
||||
- ✅ **長期記憶**: 会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索やベクトル検索に対応しています。
|
||||
- ✅ **長期記憶**: 会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリ、デイリーメモリ、Deep Dream 蒸留を含み、キーワード検索やベクトル検索に対応しています。
|
||||
- ✅ **パーソナルナレッジベース**: 構造化された知識を自動整理し、相互参照によるナレッジグラフを構築。Web での可視化ブラウジングと対話による管理をサポートします。
|
||||
- ✅ **Skillシステム**: Skillの作成・実行エンジンを実装。[Skill Hub](https://skills.cowagent.ai)、GitHubなどからSkillをインストールでき、会話を通じたカスタムSkill作成もサポートしています。
|
||||
- ✅ **ツールシステム**: ファイル読み書き、ターミナル実行、ブラウザ操作、スケジュールタスク、メッセージ送信などの組み込みツールを提供。Agentが自律的に呼び出して複雑なタスクを完了します。
|
||||
@@ -43,6 +43,8 @@
|
||||
|
||||
## 更新履歴
|
||||
|
||||
> **2026.04.14:** [v2.0.6](https://github.com/zhayujie/CowAgent/releases/tag/2.0.6) — ナレッジベース、Deep Dream 記憶蒸留、スマートコンテキスト圧縮、Web コンソールアップグレード。
|
||||
|
||||
> **2026.04.01:** [v2.0.5](https://github.com/zhayujie/CowAgent/releases/tag/2.0.5) — Cow CLI、Skill Hubオープンソース化、ブラウザツール、WeCom Botスキャン作成など。
|
||||
|
||||
> **2026.02.27:** [v2.0.2](https://github.com/zhayujie/CowAgent/releases/tag/2.0.2) — Webコンソールの全面刷新(ストリーミングチャット、モデル/Skill/メモリ/チャネル/スケジューラ/ログ管理)、マルチチャネル同時実行、セッション永続化、Gemini 3.1 Pro / Claude 4.6 Sonnet / Qwen3.5 Plusなど新モデル追加。
|
||||
|
||||
@@ -92,31 +92,6 @@ description: ステータスの確認、設定管理、コンテキスト制御
|
||||
/logs 50
|
||||
```
|
||||
|
||||
## knowledge
|
||||
|
||||
パーソナルナレッジベースの表示と管理を行います。デフォルトでは統計情報を表示します。
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
**ディレクトリ構造を表示:**
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
**ナレッジベースの有効化・無効化:**
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
ターミナル CLI では `cow knowledge` と `cow knowledge list` が利用可能ですが、`on|off` はチャットでのみサポートされます(実行時に即座に反映するため)。
|
||||
</Note>
|
||||
|
||||
## version
|
||||
|
||||
現在の CowAgent のバージョンを表示します。
|
||||
|
||||
@@ -40,7 +40,8 @@ Service:
|
||||
Skills:
|
||||
skill Manage skills (list / search / install / uninstall ...)
|
||||
|
||||
Knowledge:
|
||||
Memory & Knowledge:
|
||||
memory Memory distillation (dream)
|
||||
knowledge View knowledge base stats and structure
|
||||
|
||||
Others:
|
||||
@@ -58,6 +59,7 @@ Web コンソールや接続されたチャネルの会話で `/` を入力す
|
||||
| `/status` | サービスの状態と設定を表示 |
|
||||
| `/config` | 実行時設定の表示・変更 |
|
||||
| `/skill` | スキル管理(インストール、アンインストール、有効化、無効化など) |
|
||||
| `/memory dream [N]` | 記憶蒸留を手動トリガー(デフォルト 3 日、最大 30) |
|
||||
| `/knowledge` | ナレッジベースの統計情報を表示 |
|
||||
| `/knowledge list` | ナレッジベースのディレクトリ構造を表示 |
|
||||
| `/knowledge on\|off` | ナレッジベースの有効化・無効化 |
|
||||
@@ -80,6 +82,7 @@ Web コンソールや接続されたチャネルの会話で `/` を入力す
|
||||
| logs | ✓ | ✓ |
|
||||
| config | ✗ | ✓ |
|
||||
| context | — | ✓ |
|
||||
| memory(サブコマンド) | ✗ | ✓ |
|
||||
| knowledge(サブコマンド) | ✓ | ✓ |
|
||||
| skill(サブコマンド) | ✓ | ✓ |
|
||||
| start / stop / restart | ✓ | ✗ |
|
||||
|
||||
63
docs/ja/cli/memory-knowledge.mdx
Normal file
63
docs/ja/cli/memory-knowledge.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 記憶とナレッジベース
|
||||
description: 記憶蒸留とナレッジベース管理コマンド
|
||||
---
|
||||
|
||||
## memory
|
||||
|
||||
Agent の長期記憶システムを管理します。
|
||||
|
||||
### memory dream
|
||||
|
||||
記憶蒸留(Deep Dream)を手動でトリガーします — 最近の日次記憶を整理し、MEMORY.md に統合し、夢日記を生成します。
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`:直近 N 日間の記憶を整理(デフォルト 3 日、最大 30 日)
|
||||
- バックグラウンドで非同期に実行され、完了するとチャットで通知されます
|
||||
- Agent の初期化不要 — 最初の会話前でも使用可能
|
||||
|
||||
**例:**
|
||||
|
||||
```text
|
||||
/memory dream # 直近 3 日間を整理
|
||||
/memory dream 7 # 直近 7 日間を整理
|
||||
/memory dream 30 # 直近 30 日間を整理(全量)
|
||||
```
|
||||
|
||||
Web コンソールでは、完了通知にクリック可能なリンクが含まれ、更新された MEMORY.md と夢日記を直接確認できます。
|
||||
|
||||
<Tip>
|
||||
システムは毎日 23:55 に自動で蒸留を実行します(lookback 1 日)。手動トリガーは、初回デプロイ後の履歴整理や、即座に記憶を更新したい場合に使用します。
|
||||
</Tip>
|
||||
|
||||
## knowledge
|
||||
|
||||
パーソナルナレッジベースの表示と管理。デフォルトで統計情報を表示します。
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
### knowledge list
|
||||
|
||||
ナレッジベースのディレクトリツリーを表示します。
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
### knowledge on / off
|
||||
|
||||
ナレッジベースの有効化・無効化。無効化すると、ナレッジプロンプトとファイルインデックスが注入されなくなります。
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
ターミナル CLI では `cow knowledge` と `cow knowledge list` が利用可能ですが、`on|off` はチャットでのみサポートされます(ランタイム効果が必要なため)。
|
||||
</Note>
|
||||
@@ -5,16 +5,18 @@ description: CowAgent の長期記憶、タスク計画、Skill システム、C
|
||||
|
||||
## 1. 長期記憶
|
||||
|
||||
記憶システムにより、Agent は重要な情報を長期にわたって記憶できます。ユーザーが好みや決定、重要な事実を共有すると、Agent は自発的に情報を保存し、会話が一定の長さに達すると自動的に要約を抽出します。記憶はコアメモリとデイリーメモリに分かれており、キーワード検索とベクトル検索の両方をサポートするハイブリッド検索が可能です。
|
||||
記憶システムにより、Agent は重要な情報を長期にわたって記憶できます。三層記憶フローを採用:会話コンテキスト(短期)→ デイリーメモリ(中期)→ MEMORY.md(長期)、完全な記憶ライフサイクルを形成します。
|
||||
|
||||
初回起動時、Agent はユーザーに重要な情報を自発的に尋ね、ワークスペース(デフォルト `~/cow`)に記録します。これには Agent の設定、ユーザーの身元情報、記憶ファイルが含まれます。
|
||||
|
||||
その後の長期的な会話において、Agent は必要に応じてインテリジェントに記憶を保存・取得し、自身の設定やユーザーの好み、記憶ファイルを継続的に更新し、経験と教訓を要約します。これにより、真に自律的な思考と継続的な成長を実現しています。
|
||||
その後の長期的な会話において、Agent は必要に応じてインテリジェントに記憶を保存・取得し、自身の設定やユーザーの好み、記憶ファイルを継続的に更新します。毎日 **Deep Dream(夢境蒸留)** が自動実行され、散在するデイリーメモリを精製された長期記憶に統合し、ナラティブスタイルの夢日記を生成します。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
詳細は [長期記憶](/ja/memory) と [Deep Dream](/ja/memory/deep-dream) を参照してください。
|
||||
|
||||
## 2. パーソナルナレッジベース
|
||||
|
||||
> ナレッジベースシステムにより、Agent は構造化された知識を継続的に蓄積・整理できます。時系列で記録されるメモリとは異なり、ナレッジベースはトピック別に整理され、記事、会話からの洞察、学習資料などを相互にリンクされた Markdown ページとして整理し、継続的に成長するナレッジネットワークを形成します。
|
||||
@@ -26,6 +28,10 @@ Agent は会話中に価値ある情報を自動的にナレッジページと
|
||||
- **チャット連携**:Agent の回答で参照されるナレッジドキュメントのリンクを Web コンソールで直接クリックして閲覧可能
|
||||
- **CLI 管理**:`/knowledge` コマンドで統計表示、ディレクトリ閲覧、`/knowledge on|off` で機能の切り替えが可能
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
詳細は [パーソナルナレッジベース](/ja/knowledge) を参照してください。
|
||||
|
||||
## 3. タスク計画とツール活用
|
||||
@@ -47,7 +53,7 @@ OS のターミナルとファイルシステムへのアクセスは、最も
|
||||
プログラミングとシステムアクセスを組み合わせることで、Agent は完全な **Vibecoding ワークフロー** を実行できます。情報検索、アセット生成、コーディング、テスト、デプロイ、Nginx 設定、公開まで、すべてスマートフォンからの一つのコマンドで実行可能です:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203121008.png" width="800" />
|
||||
<img src="https://cdn.link-ai.tech/doc/20260318211018.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.3 スケジュールタスク
|
||||
|
||||
@@ -20,7 +20,7 @@ CowAgent は自ら思考しタスクを計画し、コンピュータや外部
|
||||
複雑なタスクを理解し、自律的に実行計画を立て、目標が達成されるまで思考とツール呼び出しを続けます。ツールを通じてファイルシステム、ターミナル、ブラウザ、スケジューラなどのシステムリソースにアクセスできます。
|
||||
</Card>
|
||||
<Card title="長期記憶" icon="database" href="/ja/memory">
|
||||
会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索とベクトル検索に対応しています。
|
||||
三層記憶フロー(コンテキスト→デイリーメモリ→グローバルメモリ)、毎日 Deep Dream 蒸留で整理、キーワード検索とベクトル検索に対応。
|
||||
</Card>
|
||||
<Card title="ナレッジベース" icon="book" href="/ja/knowledge">
|
||||
構造化された知識を自動整理し、ナレッジグラフの可視化をサポート。相互参照により継続的に成長するナレッジネットワークを構築します。
|
||||
|
||||
@@ -39,14 +39,15 @@ description: 会話コンテキスト — メッセージ管理、圧縮戦略
|
||||
|
||||
- **最も古い半分** の完全なターンがトリミングされます(ツール呼び出しチェーンの完全性を保証)
|
||||
- トリミングされたメッセージは LLM によって要約され、**日次記憶ファイルに書き込まれます**
|
||||
- 残りのターンはそのまま保持されます
|
||||
- LLM 要約が完了すると、保持されたコンテキストの最初のユーザーメッセージの先頭に要約が**注入**され、モデルが会話の文脈を維持できるようにします
|
||||
- 要約注入はバックグラウンドで非同期に実行され、次のターンから有効になります
|
||||
|
||||
### 3. トークン予算のトリミング
|
||||
|
||||
ターンのトリミング後、トークン数がまだ予算を超えている場合:
|
||||
|
||||
- **5 ターン未満の場合**:すべてのターンで**テキスト圧縮**を実行 — 各ターンは最初のユーザーテキストと最後の Agent 返信のみを保持し、中間のツール呼び出しチェーンを削除
|
||||
- **5 ターン以上の場合**:**前半のターン**を再度トリミングし、破棄されたコンテンツも記憶に書き込まれます
|
||||
- **5 ターン以上の場合**:**前半のターン**を再度トリミングし、破棄されたコンテンツも記憶に書き込まれ、コンテキスト要約も注入されます
|
||||
|
||||
### 4. オーバーフロー緊急処理
|
||||
|
||||
|
||||
90
docs/ja/memory/deep-dream.mdx
Normal file
90
docs/ja/memory/deep-dream.mdx
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: 夢境蒸留
|
||||
description: Deep Dream — 会話から永続記憶への自動蒸留メカニズム
|
||||
---
|
||||
|
||||
夢境蒸留(Deep Dream)は CowAgent の記憶システムの中核的な整理メカニズムであり、散在する日次記憶を精錬された長期記憶に蒸留し、夢日記を生成します。
|
||||
|
||||
## 記憶の流れ
|
||||
|
||||
CowAgent の記憶は短期から長期まで 3 つの段階を経ます:
|
||||
|
||||
```
|
||||
会話コンテキスト(短期)→ 日次記憶(中期)→ MEMORY.md(長期)
|
||||
```
|
||||
|
||||
### 1. 会話 → 日次記憶
|
||||
|
||||
会話コンテキストがトリミングされた時、または毎日のスケジュール要約時に、LLM が会話内容を重要イベントに要約し、日次記憶ファイル `memory/YYYY-MM-DD.md` に書き込みます。
|
||||
|
||||
トリガー:
|
||||
- **コンテキストトリミング** — ターン数またはトークン制限を超えた時、トリミングされた内容が要約されます
|
||||
- **毎日のスケジュール** — 23:55 に自動トリガー
|
||||
- **API オーバーフロー** — 現在の会話要約の緊急保存
|
||||
|
||||
### 2. 日次記憶 → MEMORY.md(蒸留)
|
||||
|
||||
毎日の要約完了後、Deep Dream が自動的に蒸留を実行します:
|
||||
|
||||
1. **材料の読み込み** — 現在の `MEMORY.md` + 当日の日次記憶
|
||||
2. **LLM 蒸留** — 重複排除、統合、剪定、新情報の抽出
|
||||
3. **MEMORY.md の上書き** — 精錬された長期記憶を出力
|
||||
4. **夢日記の生成** — 整理過程の発見と洞察を記録
|
||||
|
||||
### 3. MEMORY.md の役割
|
||||
|
||||
`MEMORY.md` は毎回の会話のシステムプロンプトに注入され、Agent がユーザーの好み、決定、重要な事実を常に把握できるようにします。そのため簡潔に保つ必要があり、Deep Dream は約 30 項目以内に制御します。
|
||||
|
||||
## 蒸留ルール
|
||||
|
||||
Deep Dream は以下の整理ルールに従います:
|
||||
|
||||
| 操作 | 説明 |
|
||||
| --- | --- |
|
||||
| **統合・精錬** | 類似する複数の項目を 1 つの高密度な表現に統合 |
|
||||
| **新規抽出** | 日次記憶から好み、決定、人物、経験を抽出 |
|
||||
| **矛盾更新** | 新情報が古い項目と矛盾する場合、新情報を優先 |
|
||||
| **無効クリーン** | 一時的な記録、空白項目、フォーマット残留を削除 |
|
||||
| **冗長削除** | より精錬された表現でカバーされた古い項目を削除 |
|
||||
|
||||
## 夢日記
|
||||
|
||||
各蒸留で夢日記が生成され、`memory/dreams/YYYY-MM-DD.md` に保存されます。ナラティブスタイルで以下を記録します:
|
||||
|
||||
- 発見された重複や矛盾
|
||||
- 日次記憶から抽出された新しい洞察
|
||||
- 実行されたクリーンアップと最適化
|
||||
- 全体的な観察
|
||||
|
||||
夢日記は Web コンソールの「メモリ管理 → 夢日記」タブで確認できます。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414110032.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 手動トリガー
|
||||
|
||||
毎日の自動実行に加えて、チャットで手動トリガーできます:
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`:直近 N 日間の記憶を整理(デフォルト 3 日、最大 30 日)
|
||||
- バックグラウンドで非同期に実行され、完了するとチャットで通知されます
|
||||
- Web 通知にはクリック可能なリンクが含まれ、MEMORY.md と夢日記を直接確認できます
|
||||
- Agent の初期化不要 — 最初の会話前でも使用可能
|
||||
|
||||
<Tip>
|
||||
初回デプロイ後は `/memory dream 30` を一度実行して、すべての履歴日次記憶を MEMORY.md に蒸留することをお勧めします。
|
||||
</Tip>
|
||||
|
||||
## 安全メカニズム
|
||||
|
||||
| メカニズム | 説明 |
|
||||
| --- | --- |
|
||||
| **コンテンツなしでスキップ** | 日次記憶がない場合、蒸留をスキップし空の上書きを回避 |
|
||||
| **入力重複排除** | スケジュールタスクで、入力材料が変更されていない場合自動スキップ |
|
||||
| **非同期実行** | 蒸留はバックグラウンドスレッドで実行、会話をブロックしない |
|
||||
| **順序保証** | スケジュールタスクで、日次フラッシュ完了後に蒸留を開始 |
|
||||
| **捏造禁止** | プロンプトで既存の材料のみに基づく整理を明示的に制約 |
|
||||
@@ -15,12 +15,17 @@ description: CowAgent の長期記憶システム — ファイル永続化、
|
||||
|
||||
`~/cow/memory/` ディレクトリに保存され、日付で命名されます(例:`2026-03-08.md`)。日々の会話の要約と主要なイベントを記録します。空ファイルの生成を避けるため、最初の書き込み時にのみファイルが作成されます。
|
||||
|
||||
### 夢日記(memory/dreams/YYYY-MM-DD.md)
|
||||
|
||||
Deep Dream(記憶蒸留)プロセスの副産物で、各整理で発見された重複、統合操作、新しい洞察を記録します。`~/cow/memory/dreams/` ディレクトリに日付で命名されて保存されます。
|
||||
|
||||
## 自動書き込み
|
||||
|
||||
Agent は以下のメカニズムにより、会話内容を長期記憶に自動的に永続化します:
|
||||
|
||||
- **コンテキストトリミング時** — 会話ターン数またはトークン数が設定上限を超えた場合、最も古い半分のコンテキストがトリミングされ、LLM によって要約されて日次記憶ファイルに書き込まれます
|
||||
- **コンテキストトリミング時** — 会話ターン数またはトークン数が設定上限を超えた場合、最も古い半分のコンテキストがトリミングされ、LLM によって要約されて日次記憶ファイルに書き込まれます。要約は保持されたコンテキストにも非同期で注入され、会話の連続性を維持します
|
||||
- **毎日のスケジュール要約** — 毎日 23:55 に自動的にフル要約がトリガーされ、アクティビティが少ない日でも記憶が保存されます(内容が変更されていない場合はスキップ)
|
||||
- **[夢境蒸留(Deep Dream)](/ja/memory/deep-dream)** — 毎日の要約完了後に自動実行され、日次記憶を MEMORY.md に蒸留し、夢日記を生成します
|
||||
- **API コンテキストオーバーフロー時** — モデル API がコンテキストオーバーフローエラーを返した場合、緊急措置として現在の会話要約が保存されます
|
||||
|
||||
すべての記憶書き込みはバックグラウンドスレッドで非同期に実行され(LLM の要約 + ファイル書き込み)、通常の会話応答をブロックしません。
|
||||
@@ -35,6 +40,7 @@ Agent は以下のメカニズムにより、会話内容を長期記憶に自
|
||||
| `user.md` | ユーザーの身元情報と好み |
|
||||
| `MEMORY.md` | コア記憶(長期) |
|
||||
| `memory/YYYY-MM-DD.md` | 日次記憶(オンデマンドで作成) |
|
||||
| `memory/dreams/YYYY-MM-DD.md` | 夢日記(Deep Dream で自動生成) |
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
|
||||
@@ -5,6 +5,7 @@ description: CowAgent バージョン履歴
|
||||
|
||||
| バージョン | 日付 | 説明 |
|
||||
| --- | --- | --- |
|
||||
| [2.0.6](/ja/releases/v2.0.6) | 2026.04.14 | ナレッジベース、Deep Dream 記憶蒸留、スマートコンテキスト圧縮、Web コンソールアップグレード |
|
||||
| [2.0.5](/ja/releases/v2.0.5) | 2026.04.01 | Cow CLI、Skill Hub オープンソース、ブラウザツール、企業微信スキャン作成、その他改善 |
|
||||
| [2.0.4](/ja/releases/v2.0.4) | 2026.03.22 | 個人WeChatチャネル追加、新モデルサポート、日本語ドキュメント、スクリプトリファクタリングおよび複数修正 |
|
||||
| [2.0.2](/ja/releases/v2.0.2) | 2026.02.27 | Web Console アップグレード、マルチチャネル同時実行、セッション永続化 |
|
||||
|
||||
83
docs/ja/releases/v2.0.6.mdx
Normal file
83
docs/ja/releases/v2.0.6.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: v2.0.6
|
||||
description: CowAgent 2.0.6 - ナレッジベース、Deep Dream 記憶蒸留、スマートコンテキスト圧縮、Web コンソールマルチセッションなど
|
||||
---
|
||||
|
||||
## プロジェクト名を CowAgent に改称
|
||||
|
||||
リポジトリが `chatgpt-on-wechat` から **CowAgent** に正式改称され、フル機能の AI Agent アシスタントへ進化しました。
|
||||
|
||||
- 新アドレス:[github.com/zhayujie/CowAgent](https://github.com/zhayujie/CowAgent)、旧アドレスは GitHub が自動リダイレクト
|
||||
- CLI コマンド、設定ファイル、ドキュメントリンクはすべて互換性を維持、追加操作は不要
|
||||
|
||||
## 📚 ナレッジベース
|
||||
|
||||
新しいパーソナルナレッジベースシステム — Agent が構造化された知識を自律的に構築・維持し、会話中に必要に応じて検索・引用:
|
||||
|
||||
- **インデックス駆動の自己組織構造**:ナレッジは `knowledge/` ディレクトリに保存、カテゴリ別に自動整理、各ナレッジページは独立した Markdown ファイル
|
||||
- **自動書き込み**:Agent にファイルやリンクなどの知識を送信、または会話で価値ある情報を識別した際にナレッジページを自動作成・更新
|
||||
- **ハイブリッド検索**:キーワード全文検索とベクトル意味検索をサポート、会話中に関連ナレッジをオンデマンドで読み込み
|
||||
- **ビジュアライゼーション**:ファイルツリー閲覧とナレッジグラフの可視化、ドキュメント内リンクで直接ナビゲーション
|
||||
- **コマンド管理**:`/knowledge` で統計表示、`/knowledge list` でディレクトリ構造、`/knowledge on|off` でオン・オフ
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="750" />
|
||||
|
||||
|
||||
ドキュメント:[ナレッジベース](https://docs.cowagent.ai/ja/knowledge)
|
||||
|
||||
## 🌙 Deep Dream 記憶蒸留
|
||||
|
||||
散在する会話記憶を毎日自動的に精製された長期記憶へ蒸留する新しい記憶整理メカニズム:
|
||||
|
||||
- **三層記憶フロー**:会話コンテキスト(短期)→ デイリーメモリ(中期)→ MEMORY.md(長期)、完全な記憶ライフサイクルを形成
|
||||
- **自動蒸留**:毎日 23:55 に定期実行、当日のデイリーメモリと MEMORY.md を読み取り、LLM で重複排除・統合・剪定を行い、精製された新しい MEMORY.md を出力
|
||||
- **夢日記**:各蒸留でナラティブスタイルの夢日記を生成、整理過程の発見と洞察を記録、`memory/dreams/` ディレクトリに保存
|
||||
- **手動トリガー**:`/memory dream [N]` で手動トリガー、整理日数を指定可能(デフォルト 3 日、最大 30 日)、完了後にチャットで通知
|
||||
- **Web コンソール**:記憶管理ページに「夢日記」タブを追加、すべての夢日記を閲覧可能
|
||||
|
||||
ドキュメント:[Deep Dream](https://docs.cowagent.ai/ja/memory/deep-dream)
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414120158.png" width="750" />
|
||||
|
||||
## 🧠 スマートコンテキスト圧縮
|
||||
|
||||
コンテキストが上限を超えた場合、トリミング部分を LLM で要約し非同期で注入、会話の連続性を維持:
|
||||
|
||||
- **LLM 非同期要約**:トリミングされたメッセージを LLM がキー情報に要約、デイリーメモリファイルへの書き込みと保持コンテキストへの注入を同時実行
|
||||
- **マルチモデル対応**:メインモデルを優先使用、Claude、OpenAI、MiniMax など異なるモデルのメッセージ形式要件に対応
|
||||
|
||||
ドキュメント:[短期記憶](https://docs.cowagent.ai/ja/memory/context)
|
||||
|
||||
## 💬 Web コンソールアップグレード
|
||||
|
||||
Web コンソールの複数機能を強化:
|
||||
|
||||
- **マルチセッション管理**:独立セッションの作成と切り替え、サイドバーにセッションリスト表示、タイトルの自動生成と手動編集をサポート
|
||||
- **パスワード保護**:`web_console_password` 設定でコンソールにログインパスワードを設定可能
|
||||
- **深い思考**:Web 端でモデルの思考プロセスを表示、`enable_thinking` 設定で制御
|
||||
- **定期プッシュ**:定期タスクの結果を Web コンソールにプッシュ
|
||||
- **メッセージコピー**:AI 回答バブルから元の Markdown コンテンツをワンクリックコピー
|
||||
- **言語切替**:上部の言語切替ボタンが現在の言語を表示するように改善、より直感的な操作
|
||||
|
||||
## 🤖 モデル関連
|
||||
|
||||
- **視覚認識の最適化**:画像認識ツールがメインモデルを優先使用、複数プロバイダーの自動フォールバック対応。ドキュメント:[ビジョンツール](https://docs.cowagent.ai/ja/tools/vision)
|
||||
- **MiniMax 新モデル**:MiniMax-M2.7-highspeed モデルと MiniMax TTS 音声合成サポートを追加。Thanks @octo-patch
|
||||
- **通義千問**:qwen3.6-plus モデルサポートを追加
|
||||
|
||||
## 🐛 その他の改善と修正
|
||||
|
||||
- **記憶プロンプト最適化**:`MEMORY.md` をシステムプロンプトにデフォルト注入、記憶検索と書き込みのトリガー条件を精緻化、主動的な書き込み能力を強化
|
||||
- **システムプロンプト**:システムプロンプトのスタイルとトーンガイダンスを最適化
|
||||
- **ブラウザツール**:暗黙的なインタラクティブ要素の検出を強化
|
||||
- **ファイル送信**:汎用ファイルタイプ(tar.gz、zip 等)が正しく送信されない問題を修正。Thanks @6vision
|
||||
- **macOS 互換性**:ネットワークプリチェックタイムアウトの互換性問題を修正。Thanks @Moliang Zhou
|
||||
- **Windows 互換性**:Windows での PowerShell 互換性、プロセス更新、ターミナルエンコーディングなどの問題を修正
|
||||
- **Python 3.13+**:Python 3.13 以降で `legacy-cgi` 依存関係が不足する問題を修正
|
||||
- **個人 WeChat**:個人 WeChat チャネルバージョンを更新
|
||||
|
||||
## 📦 アップグレード
|
||||
|
||||
`cow update` または `./run.sh update` でアップグレード、またはコードを手動で pull して再起動。詳細は[アップグレードガイド](https://docs.cowagent.ai/ja/guide/upgrade)を参照。
|
||||
|
||||
**リリース日**:2026.04.14 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.5...master)
|
||||
@@ -39,14 +39,15 @@ description: 对话上下文 — 消息管理、压缩策略和上下文操作
|
||||
|
||||
- 裁剪 **最早一半** 的完整轮次(保证工具调用链的完整性)
|
||||
- 被裁剪的消息会通过 LLM 总结后**写入当天的日级记忆文件**
|
||||
- 剩余轮次保持不变
|
||||
- LLM 摘要完成后,同时将摘要**注入到保留消息的第一条用户消息开头**,帮助模型在后续对话中保持上下文连贯性
|
||||
- 摘要注入在后台异步完成,不阻塞当前回复;注入的摘要在下一轮对话时生效
|
||||
|
||||
### 3. Token 预算裁剪
|
||||
|
||||
裁剪轮次后,如果 token 数仍超出预算:
|
||||
|
||||
- **轮次 < 5 时**:对所有轮次进行**文本压缩** — 每轮只保留第一条用户文本和最后一条 Agent 回复,去掉中间的工具调用链
|
||||
- **轮次 ≥ 5 时**:再次裁剪**前半轮次**,被丢弃内容同样写入记忆
|
||||
- **轮次 ≥ 5 时**:再次裁剪**前半轮次**,被丢弃内容同样写入记忆并注入上下文摘要
|
||||
|
||||
### 4. 溢出应急处理
|
||||
|
||||
|
||||
94
docs/memory/deep-dream.mdx
Normal file
94
docs/memory/deep-dream.mdx
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: 梦境蒸馏
|
||||
description: Deep Dream — 从对话到永久记忆的自动蒸馏机制
|
||||
---
|
||||
|
||||
梦境蒸馏(Deep Dream)是 CowAgent 记忆系统的核心整理机制,负责将分散的天级记忆蒸馏为精炼的长期记忆,并生成梦境日记。
|
||||
|
||||
## 记忆流转
|
||||
|
||||
CowAgent 的记忆从短期到长期经历三个阶段:
|
||||
|
||||
```
|
||||
对话上下文(短期)→ 天级记忆(中期)→ MEMORY.md(长期)
|
||||
```
|
||||
|
||||
### 1. 对话 → 天级记忆
|
||||
|
||||
当对话上下文被裁剪或每日定时总结时,系统使用 LLM 将对话内容摘要为关键事件,写入当天的天级记忆文件 `memory/YYYY-MM-DD.md`。
|
||||
|
||||
触发时机:
|
||||
- **上下文裁剪** — 轮次或 token 超限时,裁剪的内容被总结写入
|
||||
- **每日定时** — 23:55 自动触发全量总结
|
||||
- **API 溢出** — 紧急保存当前对话摘要
|
||||
|
||||
### 2. 天级记忆 → MEMORY.md(蒸馏)
|
||||
|
||||
每日总结完成后,Deep Dream 自动执行蒸馏:
|
||||
|
||||
1. **读取材料** — 当前 `MEMORY.md` + 当天的天级记忆
|
||||
2. **LLM 蒸馏** — 去重、合并、修剪、提取新信息
|
||||
3. **覆写 MEMORY.md** — 输出精炼后的长期记忆
|
||||
4. **生成梦境日记** — 记录整理过程的发现和洞察
|
||||
|
||||
### 3. MEMORY.md 的作用
|
||||
|
||||
`MEMORY.md` 会被注入到每次对话的系统提示词中,让 Agent 始终了解用户的偏好、决策和关键事实。因此它必须保持精炼——Deep Dream 会控制在约 30 条以内。
|
||||
|
||||
## 蒸馏规则
|
||||
|
||||
Deep Dream 遵循以下整理规则:
|
||||
|
||||
| 操作 | 说明 |
|
||||
| --- | --- |
|
||||
| **合并提炼** | 含义相近的多条合并为一条高密度表述 |
|
||||
| **新增萃取** | 从天级记忆中提取偏好、决策、人物、经验等 |
|
||||
| **冲突更新** | 新信息与旧条目矛盾时,以新信息为准 |
|
||||
| **清理无效** | 删除临时性记录、空白条目、格式残留 |
|
||||
| **删除冗余** | 已被更精炼表述涵盖的旧条目删除 |
|
||||
|
||||
## 梦境日记
|
||||
|
||||
每次蒸馏会生成一篇梦境日记,保存在 `memory/dreams/YYYY-MM-DD.md`,用叙事风格记录:
|
||||
|
||||
- 发现了哪些重复或矛盾
|
||||
- 从天级记忆中提取了什么新洞察
|
||||
- 做了哪些清理和优化
|
||||
- 整体感受和观察
|
||||
|
||||
梦境日记可在 Web 控制台的「记忆管理 → 梦境日记」tab 中查看。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414110032.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 手动触发
|
||||
|
||||
除了每日自动执行外,也可以在对话中手动触发:
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`:整理近 N 天的记忆(默认 3 天,最大 30 天)
|
||||
- 蒸馏在后台异步执行,完成后在对话中通知结果
|
||||
- Web 端通知包含可点击链接,直接跳转查看 MEMORY.md 和梦境日记
|
||||
- 无需 Agent 初始化,首次对话前即可使用
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414120158.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
首次部署后可以手动执行一次 `/memory dream 30`,将历史天级记忆全量蒸馏到 MEMORY.md。
|
||||
</Tip>
|
||||
|
||||
## 安全机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
| --- | --- |
|
||||
| **无新内容跳过** | 没有天级记忆时不执行蒸馏,避免空覆写 |
|
||||
| **输入去重** | 定时任务中,输入材料未变化时自动跳过 |
|
||||
| **异步执行** | 蒸馏在后台线程运行,不阻塞对话 |
|
||||
| **顺序保证** | 定时任务中,天级 flush 全部完成后才启动蒸馏 |
|
||||
| **禁止编造** | 提示词明确约束只能基于已有材料整理,不得推测或添加 |
|
||||
@@ -15,12 +15,17 @@ description: CowAgent 的长期记忆系统 — 文件持久化、自动写入
|
||||
|
||||
存储在 `~/cow/memory/` 目录下,按日期命名(如 `2026-03-08.md`),记录每天的对话摘要和关键事件。仅在首次写入时创建,避免生成空文件。
|
||||
|
||||
### 梦境日记(memory/dreams/YYYY-MM-DD.md)
|
||||
|
||||
Deep Dream(记忆蒸馏)过程的副产物,记录每次整理的发现、去重合并操作和新洞察。存储在 `~/cow/memory/dreams/` 目录下,按日期命名。
|
||||
|
||||
## 自动写入
|
||||
|
||||
Agent 通过以下机制自动将对话内容持久化为长期记忆:
|
||||
|
||||
- **上下文裁剪时** — 当对话轮次或 token 超出配置上限时,裁剪最早一半的上下文,使用 LLM 将被裁剪的内容总结为关键信息写入当天记忆文件
|
||||
- **上下文裁剪时** — 当对话轮次或 token 超出配置上限时,裁剪最早一半的上下文,使用 LLM 将被裁剪的内容总结为关键信息写入当天记忆文件,并将摘要异步注入到保留的上下文中,帮助模型保持对话连贯性
|
||||
- **每日定时总结** — 每天 23:55 自动触发一次全量总结,防止低活跃日无记忆留存(内容无变化时自动跳过)
|
||||
- **[梦境蒸馏(Deep Dream)](/memory/deep-dream)** — 每日总结完成后自动执行,将天级记忆蒸馏合并到 MEMORY.md,并生成梦境日记
|
||||
- **API 上下文溢出时** — 当模型 API 返回上下文溢出错误时,紧急保存当前对话摘要
|
||||
|
||||
所有记忆写入均在后台异步执行(LLM 总结 + 文件写入),不阻塞正常对话回复。
|
||||
@@ -44,6 +49,7 @@ Agent 会在对话中根据需要自动触发记忆检索,将相关历史信
|
||||
| `user.md` | 用户身份信息和偏好 |
|
||||
| `MEMORY.md` | 核心记忆(长期) |
|
||||
| `memory/YYYY-MM-DD.md` | 日级记忆(按需创建) |
|
||||
| `memory/dreams/YYYY-MM-DD.md` | 梦境日记(Deep Dream 自动生成) |
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
|
||||
@@ -5,6 +5,7 @@ description: CowAgent 版本更新历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| [2.0.6](/releases/v2.0.6) | 2026.04.14 | 项目更名、知识库系统、梦境记忆蒸馏、上下文智能压缩、Web 控制台多会话及多项优化 |
|
||||
| [2.0.5](/releases/v2.0.5) | 2026.04.01 | Cow CLI、Skill Hub 开源、浏览器工具、企微扫码创建、多项优化和修复 |
|
||||
| [2.0.4](/releases/v2.0.4) | 2026.03.22 | 新增个人微信通道、新模型支持、日文文档、脚本重构及多项修复 |
|
||||
| [2.0.3](/releases/v2.0.3) | 2026.03.18 | 新增企微智能机器人和 QQ 通道、支持Coding Plan、新增多个模型、Web端文件处理、记忆系统升级 |
|
||||
|
||||
82
docs/releases/v2.0.6.mdx
Normal file
82
docs/releases/v2.0.6.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: v2.0.6
|
||||
description: CowAgent 2.0.6 - 知识库系统、梦境记忆蒸馏、上下文智能压缩、Web 控制台多会话及多项优化
|
||||
---
|
||||
|
||||
## 项目正式更名为 CowAgent
|
||||
|
||||
项目仓库正式从 `chatgpt-on-wechat` 更名为 **CowAgent**,演进为功能完备的 AI Agent 助理。
|
||||
|
||||
- 新地址:[github.com/zhayujie/CowAgent](https://github.com/zhayujie/CowAgent),旧地址 GitHub 会自动重定向
|
||||
- CLI 命令、配置文件、文档链接均保持兼容,无需额外操作
|
||||
|
||||
## 📚 知识库系统
|
||||
|
||||
新增个人知识库系统,Agent 可自主构建和维护结构化知识,并在对话中按需检索引用:
|
||||
|
||||
- **索引驱动的自组织结构**:知识库采用 `knowledge/` 目录,按分类自动组织,每个知识页面为独立的 Markdown 文件
|
||||
- **自动写入**:向 Agent 发送文件、链接等知识,或在讨论中识别到有价值的知识时,自动创建或更新知识页面
|
||||
- **混合检索**:支持关键词全文搜索和向量语义检索,在对话中按需加载相关知识
|
||||
- **可视化**:支持文件树浏览和知识图谱可视化,文档内链接可直接跳转查看
|
||||
- **命令管理**:`/knowledge` 查看统计、`/knowledge list` 查看目录结构、`/knowledge on|off` 开关知识库
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="750" />
|
||||
|
||||
|
||||
相关文档:[知识库](https://docs.cowagent.ai/knowledge)
|
||||
|
||||
## 🌙 梦境记忆蒸馏(Deep Dream)
|
||||
|
||||
全新的记忆整理机制,每日自动将分散的对话记忆蒸馏为精炼的长期记忆:
|
||||
|
||||
- **三层记忆流转**:对话上下文(短期)→ 天级记忆(中期)→ MEMORY.md(长期),形成完整的记忆生命周期
|
||||
- **自动蒸馏**:每日 23:55 定时执行,读取当天天级记忆和 MEMORY.md,通过 LLM 进行去重、合并、修剪,输出精炼的新版 MEMORY.md
|
||||
- **梦境日记**:每次蒸馏生成一篇叙事风格的梦境日记,记录整理过程的发现和洞察,存储在 `memory/dreams/` 目录
|
||||
- **手动触发**:支持 `/memory dream [N]` 手动触发,可指定整理天数(默认 3 天,最大 30 天),完成后在对话中通知结果
|
||||
- **Web 控制台**:记忆管理页面新增「梦境日记」tab,可浏览和查看所有梦境日记
|
||||
|
||||
相关文档:[梦境蒸馏](https://docs.cowagent.ai/memory/deep-dream)
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414120158.png" width="750" />
|
||||
|
||||
## 🧠 上下文智能压缩
|
||||
|
||||
上下文超出限制时将裁剪的部分通过 LLM 总结后异步注入,保持对话连贯性:
|
||||
|
||||
- **LLM 异步摘要**:裁剪的消息由 LLM 总结为关键信息,同时写入天级记忆文件和注入保留的上下文
|
||||
- **多模型兼容**:优先使用主模型进行摘要,兼容 Claude、OpenAI、MiniMax 等不同模型的消息格式要求
|
||||
|
||||
相关文档:[短期记忆](https://docs.cowagent.ai/memory/context)
|
||||
|
||||
## 💬 Web 控制台升级
|
||||
|
||||
Web 控制台多项功能增强:
|
||||
|
||||
- **多会话管理**:支持创建和切换多个独立会话,侧边栏展示会话列表,支持会话标题自动生成和手动编辑
|
||||
- **密码保护**:支持为控制台设置登录密码,可通过 `web_console_password` 配置项控制
|
||||
- **深度思考**:支持在 Web 端展示模型的思考过程,可通过`enable_thinking` 配置项控制
|
||||
- **定时推送**:支持定时任务结果推送到 Web 控制台
|
||||
- **消息复制**:AI 回复支持一键复制原始 Markdown 内容
|
||||
|
||||
## 🤖 模型相关
|
||||
|
||||
- **视觉识别优化**:图片识别工具优先使用主模型,支持多模型厂商自动降级。相关文档:[视觉工具](https://docs.cowagent.ai/tools/vision)
|
||||
- **MiniMax 新模型**:新增 MiniMax-M2.7-highspeed 模型和 MiniMax TTS 语音合成支持。Thanks @octo-patch
|
||||
- **通义千问**:新增 qwen3.6-plus 模型支持
|
||||
|
||||
## 🐛 其他优化与修复
|
||||
|
||||
- **记忆提示词优化**:`MEMORY.md` 默认注入系统提示词,精细化记忆检索和写入的触发条件,增强主动写入能力
|
||||
- **系统提示词**:优化系统提示词的风格和语气引导
|
||||
- **浏览器工具**:增强隐式交互元素检测
|
||||
- **文件发送**:修复通用文件类型(tar.gz、zip 等)未能正确发送的问题。Thanks @6vision
|
||||
- **macOS 兼容**:修复网络预检超时兼容性问题。Thanks @Moliang Zhou
|
||||
- **Windows 兼容**:修复 Windows 下 PowerShell 兼容性、进程更新、终端编码等多项问题
|
||||
- **Python 3.13+**:修复 Python 3.13 及以上版本缺少 `legacy-cgi` 依赖的问题
|
||||
- **个人微信**:更新个人微信通道版本
|
||||
|
||||
## 📦 升级方式
|
||||
|
||||
源码部署可执行 `cow update` 或 `./run.sh update` 一键升级,或手动拉取代码后重启。详见 [更新升级文档](https://docs.cowagent.ai/guide/upgrade)。
|
||||
|
||||
**发布日期**:2026.04.14 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.5...master)
|
||||
@@ -262,20 +262,17 @@ class DashscopeBot(Bot):
|
||||
if kwargs.get("tool_choice"):
|
||||
parameters["tool_choice"] = kwargs["tool_choice"]
|
||||
|
||||
# Add thinking parameters for Qwen3 models (disabled by default for stability)
|
||||
# Add thinking parameters for Qwen3/QwQ models
|
||||
if "qwen3" in model_name.lower() or "qwq" in model_name.lower():
|
||||
# Only enable thinking mode if explicitly requested
|
||||
enable_thinking = kwargs.get("enable_thinking", False)
|
||||
if enable_thinking:
|
||||
thinking = kwargs.get("thinking", {"type": "enabled"})
|
||||
if thinking.get("type") == "enabled":
|
||||
parameters["enable_thinking"] = True
|
||||
|
||||
# Set thinking budget if specified
|
||||
if kwargs.get("thinking_budget"):
|
||||
parameters["thinking_budget"] = kwargs["thinking_budget"]
|
||||
|
||||
# Qwen3 requires incremental_output=true in thinking mode
|
||||
if stream:
|
||||
parameters["incremental_output"] = True
|
||||
else:
|
||||
parameters["enable_thinking"] = False
|
||||
|
||||
# Always use incremental_output for streaming (for better token-by-token streaming)
|
||||
# This is especially important for tool calling to avoid incomplete responses
|
||||
|
||||
@@ -249,9 +249,7 @@ class DoubaoBot(Bot):
|
||||
request_body["tools"] = converted_tools
|
||||
request_body["tool_choice"] = "auto"
|
||||
|
||||
# Explicitly disable thinking to avoid reasoning_content issues
|
||||
# in multi-turn tool calls
|
||||
request_body["thinking"] = {"type": "disabled"}
|
||||
request_body["thinking"] = kwargs.get("thinking", {"type": "enabled"})
|
||||
|
||||
logger.debug(f"[DOUBAO] API call: model={model}, "
|
||||
f"tools={len(converted_tools) if converted_tools else 0}, stream={stream}")
|
||||
@@ -324,8 +322,17 @@ class DoubaoBot(Bot):
|
||||
choice = chunk["choices"][0]
|
||||
delta = choice.get("delta", {})
|
||||
|
||||
# Skip reasoning_content (thinking) - don't log or forward
|
||||
if delta.get("reasoning_content"):
|
||||
yield {
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"reasoning_content": delta["reasoning_content"]
|
||||
},
|
||||
"finish_reason": None
|
||||
}]
|
||||
}
|
||||
continue
|
||||
|
||||
# Handle text content
|
||||
|
||||
@@ -560,6 +560,10 @@ def _linkai_call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||
body["tools"] = tools
|
||||
body["tool_choice"] = kwargs.get("tool_choice", "auto")
|
||||
|
||||
thinking = kwargs.get("thinking")
|
||||
if thinking:
|
||||
body["thinking"] = thinking
|
||||
|
||||
# Prepare headers
|
||||
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
|
||||
base_url = conf().get("linkai_api_base", "https://api.link-ai.tech")
|
||||
|
||||
@@ -249,10 +249,7 @@ class MoonshotBot(Bot):
|
||||
request_body["tools"] = converted_tools
|
||||
request_body["tool_choice"] = "auto"
|
||||
|
||||
# Explicitly disable thinking to avoid reasoning_content issues in multi-turn tool calls.
|
||||
# kimi-k2.5 may enable thinking by default; without preserving reasoning_content
|
||||
# in conversation history the API will reject subsequent requests.
|
||||
request_body["thinking"] = {"type": "disabled"}
|
||||
request_body["thinking"] = kwargs.get("thinking", {"type": "enabled"})
|
||||
|
||||
logger.debug(f"[MOONSHOT] API call: model={model}, "
|
||||
f"tools={len(converted_tools) if converted_tools else 0}, stream={stream}")
|
||||
@@ -325,8 +322,17 @@ class MoonshotBot(Bot):
|
||||
choice = chunk["choices"][0]
|
||||
delta = choice.get("delta", {})
|
||||
|
||||
# Skip reasoning_content (thinking) – don't log or forward
|
||||
if delta.get("reasoning_content"):
|
||||
yield {
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"reasoning_content": delta["reasoning_content"]
|
||||
},
|
||||
"finish_reason": None
|
||||
}]
|
||||
}
|
||||
continue
|
||||
|
||||
# Handle text content
|
||||
|
||||
@@ -31,7 +31,7 @@ KNOWN_COMMANDS = {
|
||||
"help", "version", "status", "logs",
|
||||
"start", "stop", "restart",
|
||||
"skill", "context", "config",
|
||||
"knowledge",
|
||||
"knowledge", "memory",
|
||||
"install-browser",
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ class CowCliPlugin(Plugin):
|
||||
" /config 查看当前配置",
|
||||
" /config <key> 查看某项配置",
|
||||
" /config <key> <val> 修改配置",
|
||||
" /memory dream [N] 手动触发记忆蒸馏 (整理近N天, 默认3, 最多30)",
|
||||
" /knowledge 查看知识库统计",
|
||||
" /knowledge list 查看知识库文件树",
|
||||
" /knowledge on|off 开启/关闭知识库",
|
||||
@@ -856,6 +857,125 @@ class CowCliPlugin(Plugin):
|
||||
icon = "✅" if enabled else "⬚"
|
||||
return f"{icon} 技能 '{name}' 已{action}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# memory
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _cmd_memory(self, args: str, e_context, session_id: str = "", **_) -> str:
|
||||
parts = args.strip().split()
|
||||
sub = parts[0].lower() if parts else ""
|
||||
|
||||
if sub == "dream":
|
||||
days = 3
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
days = max(1, min(int(parts[1]), 30))
|
||||
return self._memory_dream(days, e_context, session_id)
|
||||
else:
|
||||
return (
|
||||
"用法: /memory <子命令>\n\n"
|
||||
"子命令:\n"
|
||||
" dream [N] 手动触发记忆蒸馏 (整理近N天, 默认3, 最多30)"
|
||||
)
|
||||
|
||||
def _memory_dream(self, days: int, e_context, session_id: str) -> str:
|
||||
session_id = self._get_session_id(e_context, fallback=session_id)
|
||||
agent = self._get_agent(session_id)
|
||||
|
||||
flush_mgr = None
|
||||
if agent and agent.memory_manager:
|
||||
flush_mgr = agent.memory_manager.flush_manager
|
||||
|
||||
# Fallback: construct a temporary MemoryFlushManager when agent is not yet initialized
|
||||
if not flush_mgr:
|
||||
try:
|
||||
flush_mgr = self._create_standalone_flush_manager()
|
||||
except Exception as e:
|
||||
return f"⚠️ 无法初始化记忆蒸馏: {e}"
|
||||
|
||||
if not flush_mgr.llm_model:
|
||||
return "⚠️ 未配置 LLM 模型,无法执行记忆蒸馏"
|
||||
|
||||
is_web = self._is_web_channel(e_context)
|
||||
|
||||
def _run():
|
||||
try:
|
||||
result = flush_mgr.deep_dream(lookback_days=days, force=True)
|
||||
if result:
|
||||
msg = self._build_dream_result(flush_mgr, is_web)
|
||||
self._notify(e_context, msg)
|
||||
else:
|
||||
self._notify(e_context, "💤 记忆蒸馏跳过 — 没有新的记忆内容需要整理")
|
||||
except Exception as e:
|
||||
logger.warning(f"[CowCli] /memory dream failed: {e}")
|
||||
self._notify(e_context, f"❌ 记忆蒸馏失败: {e}")
|
||||
|
||||
thread = threading.Thread(target=_run, daemon=True)
|
||||
thread.start()
|
||||
return f"🌙 记忆蒸馏已启动 (整理近 {days} 天的记忆)\n\n整理在后台执行,完成后会通知你。"
|
||||
|
||||
@staticmethod
|
||||
def _notify(e_context, text: str):
|
||||
"""Push a notification message back to the chat channel."""
|
||||
if e_context is None:
|
||||
logger.info(f"[CowCli] {text}")
|
||||
return
|
||||
try:
|
||||
channel = e_context["channel"]
|
||||
context = e_context["context"]
|
||||
if channel and context:
|
||||
channel.send(Reply(ReplyType.TEXT, text), context)
|
||||
except Exception as e:
|
||||
logger.warning(f"[CowCli] notify failed: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _is_web_channel(e_context) -> bool:
|
||||
if e_context is None:
|
||||
return False
|
||||
try:
|
||||
return e_context["context"].kwargs.get("channel_type") == "web"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _build_dream_result(flush_mgr, is_web: bool) -> str:
|
||||
"""Build dream completion message with diary content."""
|
||||
from datetime import datetime
|
||||
lines = ["✅ 记忆蒸馏完成"]
|
||||
|
||||
# Read today's dream diary
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
diary_file = flush_mgr.memory_dir / "dreams" / f"{today}.md"
|
||||
if diary_file.exists():
|
||||
diary = diary_file.read_text(encoding="utf-8").strip()
|
||||
# Strip the "# Dream Diary: ..." header line
|
||||
diary_lines = diary.split("\n")
|
||||
if diary_lines and diary_lines[0].startswith("# "):
|
||||
diary = "\n".join(diary_lines[1:]).strip()
|
||||
if diary:
|
||||
lines.append(f"\n{diary}")
|
||||
|
||||
if is_web:
|
||||
lines.append("\n[MEMORY.md](/memory/MEMORY.md) | [梦境日记](/memory/dreams)")
|
||||
else:
|
||||
lines.append("\nMEMORY.md 已更新")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _create_standalone_flush_manager():
|
||||
"""Create a MemoryFlushManager without a running agent (for pre-init dream)."""
|
||||
from pathlib import Path
|
||||
from config import conf
|
||||
from common.utils import expand_path
|
||||
from agent.memory.summarizer import MemoryFlushManager
|
||||
from bridge.bridge import Bridge
|
||||
from bridge.agent_bridge import AgentLLMModel
|
||||
|
||||
workspace = Path(expand_path(conf().get("agent_workspace", "~/cow")))
|
||||
flush_mgr = MemoryFlushManager(workspace_dir=workspace)
|
||||
flush_mgr.llm_model = AgentLLMModel(Bridge())
|
||||
return flush_mgr
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# knowledge
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
16
run.sh
16
run.sh
@@ -193,6 +193,16 @@ clone_project() {
|
||||
rm CowAgent.zip
|
||||
else
|
||||
local clone_ok=false
|
||||
# Detect and temporarily disable invalid git proxy settings
|
||||
local _git_proxy_unset=false
|
||||
local _http_proxy=$(git config --global http.proxy 2>/dev/null)
|
||||
local _https_proxy=$(git config --global https.proxy 2>/dev/null)
|
||||
if [ -n "$_http_proxy" ] && ! curl -s --connect-timeout 3 --max-time 5 --proxy "$_http_proxy" https://github.com > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}⚠️ Invalid git proxy detected: $_http_proxy, temporarily disabling...${NC}"
|
||||
git config --global --unset http.proxy
|
||||
[ -n "$_https_proxy" ] && git config --global --unset https.proxy
|
||||
_git_proxy_unset=true
|
||||
fi
|
||||
# Test GitHub connectivity before attempting clone
|
||||
if curl -sI --connect-timeout 5 --max-time 10 https://github.com > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}🌐 GitHub is reachable, cloning from GitHub...${NC}"
|
||||
@@ -204,6 +214,12 @@ clone_project() {
|
||||
fi
|
||||
if [ "$clone_ok" = false ]; then
|
||||
echo -e "${RED}❌ Project clone failed. Please check network connection.${NC}"
|
||||
if git config --global http.proxy &> /dev/null || git config --global https.proxy &> /dev/null || [ -n "$http_proxy" ] || [ -n "$https_proxy" ] || [ -n "$HTTP_PROXY" ] || [ -n "$HTTPS_PROXY" ]; then
|
||||
echo -e "${YELLOW}💡 Detected proxy settings. If proxy is misconfigured, try removing it with:${NC}"
|
||||
echo -e "${YELLOW} git config --global --unset http.proxy${NC}"
|
||||
echo -e "${YELLOW} git config --global --unset https.proxy${NC}"
|
||||
echo -e "${YELLOW} unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY${NC}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user