Compare commits

..

52 Commits

Author SHA1 Message Date
zhayujie
ce4c0a0aa4 feat: release 2.0.7 2026-04-23 17:18:19 +08:00
zhayujie
64511593c4 feat: release 2.0.7 2026-04-23 17:16:17 +08:00
zhayujie
b0e00dfceb feat: support glm-5.1 2026-04-23 16:43:05 +08:00
zhayujie
fc465b463d feat: support kimi coding plan by temporary solution 2026-04-23 16:24:37 +08:00
zhayujie
68ce2e5232 feat(skill): multi-provider image generation with auto-fallback
- Add Gemini, Seedream (Volcengine Ark), Qwen (DashScope), MiniMax
  providers to image-generation skill with universal sequential
  fallback: OpenAI → Gemini → Seedream → Qwen → MiniMax → LinkAI
- Each provider filters unsupported size tiers to valid values
  (e.g. Seedream 1K→2K, Qwen 3K→2K, Gemini 3K→2K)
- Pinned model only tries its native provider; auto-routing uses
  each provider's default model
- Support skill-namespaced config (config.skill.image-generation.model
  → SKILL_IMAGE_GENERATION_MODEL env var)
- Add image lightbox (click-to-enlarge) in web console
- Add  docs for built-in skills (skill-creator, knowledge-wiki,
  image-generation) under docs/skills/
2026-04-23 12:39:39 +08:00
zhayujie
81e8bb62ae feat(skill): support gpt-image-2 in image generation skill 2026-04-22 20:39:49 +08:00
zhayujie
2c13e1b923 feat(models): support kimi-k2.6 2026-04-22 12:01:40 +08:00
zhayujie
a0748c2e3b fix(web): cap reasoning content to 4KB across stream/storage/display 2026-04-21 20:31:38 +08:00
zhayujie
40599bb751 fix(web): smart auto-scroll for chat #2775 2026-04-20 21:43:21 +08:00
zhayujie
f3c64ceea7 fix: refresh skill manager on /skill 2026-04-19 19:50:16 +08:00
zhayujie
15c60de709 fix: improve skill installation to support multiple source formats and ensure target directory 2026-04-19 19:05:51 +08:00
zhayujie
6dd316547f fix(web): fix session title generation fallback and reset Bridge on config change 2026-04-19 18:43:48 +08:00
zhayujie
54c7676a44 docs: update architecture diagram 2026-04-18 23:08:36 +08:00
zhayujie
d25b8966ce fix(web): prevent duplicate image previews 2026-04-18 22:32:34 +08:00
zhayujie
14a119c48c fix(gemini): solving the problem of tool call not returnings 2026-04-18 21:18:27 +08:00
zhayujie
c82515a927 fix(agent): don't drop tool_calls from empty-response retry 2026-04-18 20:50:40 +08:00
zhayujie
26e630c2dd feat(cli): /config support set enable_thinking 2026-04-17 16:09:43 +08:00
zhayujie
13370d2056 fix: thinking display is disabled by default 2026-04-17 15:31:59 +08:00
zhayujie
35282db9e0 feat(models): support claude-opus-4-7 2026-04-16 23:24:16 +08:00
zhayujie
426fb88ce7 fix(knowledge): exclude root-level files from knowledge stats to preserve empty state 2026-04-16 22:55:46 +08:00
zhayujie
2384bd0e10 fix: update CI workflows for repo rename and add latest tag 2026-04-16 21:57:20 +08:00
zhayujie
ba3f66d3d1 feat: show root-level files (index.md, log.md) in knowledge tree 2026-04-16 21:47:44 +08:00
zhayujie
7293a0f670 fix: modify repo name in github workflow 2026-04-16 21:38:58 +08:00
zhayujie
9e86d46267 fix: sync env vars when updating config in docker env 2026-04-16 21:32:07 +08:00
zhayujie
848430f062 feat(knowledge): support nested directories in knowledge base listing and display 2026-04-16 12:28:18 +08:00
zhayujie
abd21335c4 Merge pull request #2772 from 6vision/master
fix: bot_type change notification never shown after model switch
2026-04-16 10:43:41 +08:00
6vision
8fa95f058a fix: bot_type change notification never shown after model switch
Made-with: Cursor
2026-04-15 21:48:50 +08:00
zhayujie
d4e5ecd497 fix: compatible with Python 3.7 by deferring Literal import in truncate.py 2026-04-15 12:29:09 +08:00
zhayujie
3830f76729 feat: add custom model provider 2026-04-15 12:26:05 +08:00
zhayujie
83f778fec9 feat(dream): structured organization of dream memories 2026-04-15 11:27:46 +08:00
zhayujie
cabd24605f fix: add random jitter to daily dream schedule 2026-04-15 00:33:33 +08:00
zhayujie
ae20ba1148 Merge branch 'master' of github.com:zhayujie/chatgpt-on-wechat 2026-04-14 22:58:59 +08:00
zhayujie
3a50b64977 feat: web multi session interface 2026-04-14 22:58:25 +08:00
zhayujie
8692e74536 fix(web): hide session panel by default on mobile and support overlay dismiss 2026-04-14 21:09:01 +08:00
zhayujie
1c18bd9889 docs(memory): update long-term memory docs 2026-04-14 17:14:28 +08:00
zhayujie
60e9d98d0a feat: release 2.0.6 2026-04-14 12:37:53 +08:00
zhayujie
83f6625e0c feat: release 2.0.6 2026-04-14 12:08:57 +08:00
zhayujie
acc09543b7 feat(dream): add memory dream cli and docs
- New memory/deep-dream.mdx (zh/en/ja): memory flow, distillation rules, dream diary, manual trigger, safety mechanisms
- Simplify long-term memory page, link to deep-dream for details
- New cli/memory-knowledge.mdx (zh/en/ja): memory and knowledge commands
- Move knowledge commands from general.mdx to memory-knowledge.mdx
- Register new pages in docs.json navigation for all languages
- Add /memory dream to cli/index.mdx command tables
2026-04-14 11:03:53 +08:00
zhayujie
94d8c7e366 feat(dream): add Dream Diary tab to memory management page
- Backend: MemoryService supports category param (memory/dream), lists memory/dreams/*.md
- Backend: MemoryContentHandler resolves dream files from memory/dreams/ directory
- Frontend: add tab switcher (Memory Files / Dream Diary) matching knowledge tab style
- Frontend: dream entries show purple "Dream" badge, empty state with moon icon
- Cloud dispatch passes category param for consistency
2026-04-13 22:08:15 +08:00
zhayujie
ea1a0c8b3d feat(memory): add Deep Dream module for daily memory distillation
- Add Deep Dream: nightly distill daily memories → refined MEMORY.md + dream diary
- Simplify flush prompt to daily-only, defer MEMORY.md maintenance to Deep Dream
- Remove dead code (_append_to_main_memory) and fix fallback summary logic
- Add shrinkage protection and input dedup for dream process
- Ensure flush threads complete before dream starts
- Update docs (zh/en/ja) with dream diary and distillation mechanism
2026-04-13 21:32:52 +08:00
zhayujie
7bc88c17e4 Merge branch 'master' of github.com:zhayujie/chatgpt-on-wechat 2026-04-13 20:13:30 +08:00
zhayujie
33cf1bc4c3 feat(memory): async LLM context summary injection on trim
- Unified flush + context injection into a single async LLM call
  (flush_from_messages accepts context_summary_callback)
- Fixed response parsing bug: handle generator returns and Claude-format
  dicts from bot.call_with_tools, which previously caused all LLM
  summaries to silently fail (falling back to rule-based extraction)
- Removed standalone context summary prompts and methods; reuse the
  existing [DAILY]/[MEMORY] summarization pipeline
- Updated docs (zh/en/ja) to reflect the new injection behavior
2026-04-13 20:13:05 +08:00
zhayujie
9402e63fe1 Merge pull request #2766 from zhayujie/feat-mulit-session
feat(web): add multi-session management for web console
2026-04-13 18:51:07 +08:00
zhayujie
90e4d494b2 feat(web): add multi-session management for web console 2026-04-13 18:50:31 +08:00
zhayujie
da97e948ca feat: refine memory recall/write prompts for better precision and proactivity 2026-04-13 18:02:06 +08:00
zhayujie
89a07e8e74 feat: add enable_thinking config to control deep reasoning on web console 2026-04-13 16:06:28 +08:00
zhayujie
3f3d0381e5 feat: update knowledge docs and fix claude error 2026-04-13 11:16:26 +08:00
zhayujie
3649499dba fix: optimize the stability of network pre-checks 2026-04-13 10:35:38 +08:00
zhayujie
a989d088fd Merge pull request #2764 from WilliamOnVoyage/fix/macos-timeout-fallback
fix: Fix run.sh for MacOS via add timeout fallback
2026-04-13 10:21:44 +08:00
Moliang Zhou
f79a915136 fix: add timeout fallback for macOS compatibility
The `timeout` command (GNU Coreutils) is not available by default on macOS,
causing the installation script to fail with 'timeout: command not found'
during git clone.

This adds a shell function fallback that:
- Uses `gtimeout` if Homebrew coreutils is installed
- Otherwise skips the timeout and runs the command directly
2026-04-12 11:18:44 -07:00
zhayujie
12e8c3d449 Merge pull request #2763 from zhayujie/feat-web-console-upgrade
feat(web): support scheduler push messages and enrich welcome screen
2026-04-12 21:20:34 +08:00
zhayujie
070df826f1 Merge pull request #2762 from zhayujie/feat-web-console-upgrade
feat(web): add password protection for web console
2026-04-12 20:38:45 +08:00
117 changed files with 7577 additions and 854 deletions

View File

@@ -19,7 +19,7 @@ env:
jobs:
build-and-push-image:
if: github.repository == 'zhayujie/chatgpt-on-wechat'
if: github.repository == 'zhayujie/CowAgent'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -51,7 +51,12 @@ jobs:
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.REGISTRY }}/zhayujie/chatgpt-on-wechat
${{ env.REGISTRY }}/zhayujie/cowagent
tags: |
type=raw,value=latest-arm64,enable={{is_default_branch}}
type=ref,event=branch,suffix=-arm64
type=ref,event=tag,suffix=-arm64
- name: Build and push Docker image
uses: docker/build-push-action@v3
@@ -60,7 +65,7 @@ jobs:
push: true
file: ./docker/Dockerfile.latest
platforms: linux/arm64
tags: ${{ steps.meta.outputs.tags }}-arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- uses: actions/delete-package-versions@v4

View File

@@ -16,10 +16,11 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_IMAGE: zhayujie/chatgpt-on-wechat
jobs:
build-and-push-image:
if: github.repository == 'zhayujie/chatgpt-on-wechat'
if: github.repository == 'zhayujie/CowAgent'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -47,8 +48,14 @@ jobs:
uses: docker/metadata-action@v4
with:
images: |
${{ env.IMAGE_NAME }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
zhayujie/chatgpt-on-wechat
zhayujie/cowagent
${{ env.REGISTRY }}/zhayujie/chatgpt-on-wechat
${{ env.REGISTRY }}/zhayujie/cowagent
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=ref,event=tag
- name: Build and push Docker image
uses: docker/build-push-action@v3

View File

@@ -23,7 +23,7 @@
> 该项目既是一个可以开箱即用的超级 AI 助理,也是一个支持高扩展的 Agent 框架可以通过为项目扩展大模型接口、接入渠道、内置工具、Skills 系统来灵活实现各种定制需求。核心能力如下:
-**自主任务规划**:能够理解复杂任务并自主规划执行,持续思考和调用工具直到完成目标
-**长期记忆:** 自动将对话记忆持久化至本地文件和数据库中,包括核心记忆日级记忆,支持关键词及向量检索
-**长期记忆:** 自动将对话记忆持久化至本地文件和数据库中,包括核心记忆日级记忆和梦境蒸馏,支持关键词及向量检索
-**个人知识库:** 自动整理结构化知识,通过交叉引用构建知识图谱,支持通过对话管理和可视化浏览知识库
-**技能系统:** Skills 安装和运行的引擎,支持从 [Skill Hub](https://skills.cowagent.ai/)、GitHub 等一键安装技能,或通过对话创造 Skills
-**工具系统:** 内置文件读写、终端执行、浏览器操作、定时任务等工具Agent 自主调用以完成复杂任务
@@ -70,6 +70,10 @@
# 🏷 更新日志
>**2026.04.22** [2.0.7版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.7)图像生成内置技能GPT Image 2、Nano Banana 等、新模型支持Kimi K2.6、Claude Opus 4.7、GLM 5.1、知识库和记忆增强、Web 控制台优化
>**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 脚本重构、日文文档及多项修复。
@@ -111,13 +115,13 @@ irm https://cdn.link-ai.tech/code/cow/run.ps1 | iex
项目支持国内外主流厂商的模型接口,可选模型及配置说明参考:[模型说明](#模型说明)。
> Agent 模式下推荐使用以下模型可根据效果及成本综合选择MiniMax-M2.7、glm-5-turbo、kimi-k2.5、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview、gpt-5.4、gpt-5.4-mini
> Agent 模式下推荐使用以下模型可根据效果及成本综合选择MiniMax-M2.7、glm-5.1、kimi-k2.6、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview、gpt-5.4、gpt-5.4-mini
同时支持使用 **LinkAI 平台** 接口,支持上述全部模型,并支持知识库、工作流、插件等 Agent 技能,参考 [接口文档](https://docs.link-ai.tech/platform/api)。
### 2.环境安装
支持 Linux、MacOS、Windows 操作系统,可在个人计算机及服务器上运行,需安装 `Python`Python 版本需在3.7 ~ 3.12 之间。
支持 Linux、MacOS、Windows 操作系统,可在个人计算机及服务器上运行,需安装 `Python`Python 版本需在 3.7 ~ 3.13 之间。
> 注意Agent 模式推荐使用源码运行,若选择 Docker 部署则无需安装 python 环境和下载源码,可直接快进到下一节。
@@ -203,7 +207,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": false # 是否启用深度思考,开启后 Web 端展示模型推理过程,关闭后可加速响应
}
```
@@ -221,7 +226,7 @@ cow install-browser
<details>
<summary>2. 其他配置</summary>
+ `model`: 模型名称Agent 模式下推荐使用 `MiniMax-M2.7``glm-5-turbo``kimi-k2.5``qwen3.6-plus``claude-sonnet-4-6``gemini-3.1-pro-preview`,全部模型名称参考[common/const.py](https://github.com/zhayujie/CowAgent/blob/master/common/const.py)文件
+ `model`: 模型名称Agent 模式下推荐使用 `MiniMax-M2.7``glm-5.1``kimi-k2.6``qwen3.6-plus``claude-sonnet-4-6``gemini-3.1-pro-preview`,全部模型名称参考[common/const.py](https://github.com/zhayujie/CowAgent/blob/master/common/const.py)文件
+ `character_desc`:普通对话模式下的机器人系统提示词。在 Agent 模式下该配置不生效,由工作空间中的文件内容构成。
+ `subscribe_msg`:订阅消息,公众号和企业微信 channel 中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成 bot 的触发词。
</details>
@@ -385,24 +390,24 @@ sudo docker logs -f chatgpt-on-wechat
```json
{
"model": "glm-5-turbo",
"model": "glm-5.1",
"zhipu_ai_api_key": ""
}
```
- `model`: 可填 `glm-5-turbo、glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long` 等, 参考 [glm 系列模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4)
- `model`: 可填 `glm-5.1、glm-5-turbo、glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long` 等, 参考 [glm 系列模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4)
- `zhipu_ai_api_key`: 智谱AI 平台的 API KEY在 [控制台](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys) 创建
方式二OpenAI 兼容方式接入,配置如下:
```json
{
"bot_type": "openai",
"model": "glm-5-turbo",
"model": "glm-5.1",
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
"open_ai_api_key": ""
}
```
- `bot_type`: OpenAI 兼容方式
- `model`: 可填 `glm-5-turbo、glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long`
- `model`: 可填 `glm-5.1、glm-5-turbo、glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long`
- `open_ai_api_base`: 智谱AI 平台的 BASE URL
- `open_ai_api_key`: 智谱AI 平台的 API KEY
</details>
@@ -443,24 +448,24 @@ sudo docker logs -f chatgpt-on-wechat
```json
{
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"moonshot_api_key": ""
}
```
- `model`: 可填写 `kimi-k2.5、kimi-k2、moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k`
- `model`: 可填写 `kimi-k2.6、kimi-k2.5、kimi-k2、moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k`
- `moonshot_api_key`: Moonshot 的 API-KEY在 [控制台](https://platform.moonshot.cn/console/api-keys) 创建
方式二OpenAI 兼容方式接入,配置如下:
```json
{
"bot_type": "openai",
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"open_ai_api_base": "https://api.moonshot.cn/v1",
"open_ai_api_key": ""
}
```
- `bot_type`: OpenAI 兼容方式
- `model`: 可填写 `kimi-k2.5、kimi-k2、moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k`
- `model`: 可填写 `kimi-k2.6、kimi-k2.5、kimi-k2、moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k`
- `open_ai_api_base`: Moonshot 的 BASE URL
- `open_ai_api_key`: Moonshot 的 API-KEY
</details>
@@ -496,7 +501,7 @@ sudo docker logs -f chatgpt-on-wechat
"claude_api_key": "YOUR_API_KEY"
}
```
- `model`: 参考 [官方模型ID](https://docs.anthropic.com/en/docs/about-claude/models/overview#model-aliases) ,支持 `claude-sonnet-4-6、claude-opus-4-6、claude-sonnet-4-5、claude-sonnet-4-0、claude-opus-4-0、claude-3-5-sonnet-latest`
- `model`: 参考 [官方模型ID](https://docs.anthropic.com/en/docs/about-claude/models/overview#model-aliases) ,支持 `claude-sonnet-4-6、claude-opus-4-7、claude-opus-4-6、claude-sonnet-4-5、claude-sonnet-4-0、claude-opus-4-0、claude-3-5-sonnet-latest`
</details>
<details>

View File

@@ -0,0 +1,241 @@
"""
SessionService - Manages multi-session lifecycle for both web channel and cloud client.
Provides a unified interface for listing, deleting, renaming, clearing context,
and generating AI titles for conversation sessions. Backed by ConversationStore
(SQLite) and AgentBridge (in-memory agent instances).
"""
import re
from typing import Optional
from common.log import logger
def _truncate_fallback_title(user_message: str, max_len: int = 30) -> str:
"""Pick the first non-empty line of the user message and truncate it."""
if not user_message:
return "New Chat"
first_line = ""
for line in user_message.splitlines():
line = line.strip()
if line:
first_line = line
break
if not first_line:
return "New Chat"
if len(first_line) > max_len:
first_line = first_line[:max_len].rstrip() + "..."
return first_line
def generate_session_title(user_message: str, assistant_reply: str = "") -> str:
"""
Generate a short session title by calling the current bot's reply_text.
Falls back to the first line of the user message if the LLM call fails
or returns an obvious error sentinel.
"""
fallback = _truncate_fallback_title(user_message)
try:
from bridge.bridge import Bridge
from models.session_manager import Session
bot = Bridge().get_bot("chat")
prompt_parts = [f"User: {user_message[:300]}"]
if assistant_reply:
prompt_parts.append(f"Assistant: {assistant_reply[:300]}")
session = Session("__title_gen__", system_prompt="")
session.messages = [
{"role": "user", "content": (
"Generate a very short title (max 15 characters for Chinese, max 6 words for English) "
"summarizing this conversation. Return ONLY the title text, nothing else.\n\n"
+ "\n".join(prompt_parts)
)}
]
result = bot.reply_text(session) or {}
# When bots fail (network error, auth error, rate limit, etc.) they
# typically return completion_tokens=0 with a sentinel content like
# "请再问我一次吧" / "我现在有点累了". Treat that as failure.
completion_tokens = result.get("completion_tokens", 0) or 0
raw = (result.get("content") or "").strip()
if completion_tokens <= 0:
logger.warning(
f"[SessionService] Title generation got empty completion "
f"(completion_tokens={completion_tokens}, content='{raw[:50]}'), "
f"using fallback")
return fallback
title = re.sub(r'<think>.*?</think>', '', raw, flags=re.DOTALL).strip().strip('"\'')
logger.info(f"[SessionService] Title generation result: '{title}' (len={len(title)})")
if title and len(title) <= 50:
return title
except Exception as e:
logger.warning(f"[SessionService] Title generation failed: {e}")
return fallback
class SessionService:
"""
High-level service for session lifecycle management.
Usage:
svc = SessionService()
result = svc.dispatch("list", {"channel_type": "web", "page": 1})
"""
def _get_store(self):
from agent.memory import get_conversation_store
return get_conversation_store()
def _remove_agent(self, session_id: str):
"""Remove the in-memory Agent instance for a session if it exists."""
try:
from bridge.bridge import Bridge
ab = Bridge().get_agent_bridge()
if session_id in ab.agents:
del ab.agents[session_id]
logger.info(f"[SessionService] Removed agent instance: {session_id}")
except Exception:
pass
@staticmethod
def _normalize_sid(session_id: str) -> str:
if session_id and not session_id.startswith("session_"):
return f"session_{session_id}"
return session_id
# ------------------------------------------------------------------
# actions
# ------------------------------------------------------------------
def list_sessions(self, channel_type: Optional[str] = None,
page: int = 1, page_size: int = 50) -> dict:
store = self._get_store()
return store.list_sessions(
channel_type=channel_type,
page=page,
page_size=page_size,
)
def delete_session(self, session_id: str) -> None:
if not session_id:
raise ValueError("session_id required")
session_id = self._normalize_sid(session_id)
store = self._get_store()
store.clear_session(session_id)
self._remove_agent(session_id)
logger.info(f"[SessionService] Session deleted: {session_id}")
def rename_session(self, session_id: str, title: str) -> None:
if not session_id:
raise ValueError("session_id required")
if not title:
raise ValueError("title required")
session_id = self._normalize_sid(session_id)
store = self._get_store()
found = store.rename_session(session_id, title)
if not found:
raise ValueError("session not found")
def clear_context(self, session_id: str) -> int:
"""
Set context boundary. Returns the new context_start_seq value.
"""
if not session_id:
raise ValueError("session_id required")
session_id = self._normalize_sid(session_id)
store = self._get_store()
new_seq = store.clear_context(session_id)
self._remove_agent(session_id)
return new_seq
def gen_title(self, session_id: str, user_message: str,
assistant_reply: str = "") -> str:
"""
Generate an AI title and persist it. Returns the generated title.
"""
if not session_id:
raise ValueError("session_id required")
if not user_message:
raise ValueError("user_message required")
session_id = self._normalize_sid(session_id)
title = generate_session_title(user_message, assistant_reply)
store = self._get_store()
updated = store.rename_session(session_id, title)
logger.info(f"[SessionService] Title set: sid={session_id}, "
f"title='{title}', db_updated={updated}")
return title
# ------------------------------------------------------------------
# dispatch — single entry point for protocol messages
# ------------------------------------------------------------------
def dispatch(self, action: str, payload: Optional[dict] = None) -> dict:
"""
Dispatch a session management action and return a protocol-compatible
response dict.
Action names use a ``*_session`` / session-prefixed convention so they
can coexist with history actions (e.g. ``query``) on the same HISTORY
message channel without ambiguity.
Supported actions:
- list_sessions: list sessions with pagination
- delete_session: delete a session
- rename_session: rename a session title
- clear_context: set context boundary
- generate_title: AI-generate a session title
:param action: one of the above action names
:param payload: action-specific payload
:return: dict with action, code, message, payload
"""
payload = payload or {}
try:
if action == "list_sessions":
result = self.list_sessions(
channel_type=payload.get("channel_type"),
page=int(payload.get("page", 1)),
page_size=int(payload.get("page_size", 50)),
)
return {"action": action, "code": 200, "message": "success", "payload": result}
elif action == "delete_session":
self.delete_session(payload.get("session_id", ""))
return {"action": action, "code": 200, "message": "success", "payload": None}
elif action == "rename_session":
self.rename_session(
payload.get("session_id", ""),
payload.get("title", "").strip(),
)
return {"action": action, "code": 200, "message": "success", "payload": None}
elif action == "clear_context":
new_seq = self.clear_context(payload.get("session_id", ""))
return {"action": action, "code": 200, "message": "success",
"payload": {"context_start_seq": new_seq}}
elif action == "generate_title":
title = self.gen_title(
payload.get("session_id", ""),
payload.get("user_message", ""),
payload.get("assistant_reply", ""),
)
return {"action": action, "code": 200, "message": "success",
"payload": {"title": title}}
else:
return {"action": action, "code": 400,
"message": f"unknown action: {action}", "payload": None}
except ValueError as e:
return {"action": action, "code": 400, "message": str(e), "payload": None}
except Exception as e:
logger.error(f"[SessionService] dispatch error: action={action}, error={e}")
return {"action": action, "code": 500, "message": str(e), "payload": None}

View File

@@ -34,7 +34,8 @@ class KnowledgeService:
# ------------------------------------------------------------------
def list_tree(self) -> dict:
"""
Return the knowledge directory tree grouped by category.
Return the knowledge directory tree grouped by category,
supporting arbitrarily nested sub-directories.
Returns::
@@ -44,10 +45,20 @@ class KnowledgeService:
"dir": "concepts",
"files": [
{"name": "moe.md", "title": "MoE", "size": 1234},
...
],
"children": []
},
{
"dir": "platform",
"files": [],
"children": [
{
"dir": "analysis",
"files": [{"name": "perf.md", ...}],
"children": []
}
]
},
...
],
"stats": {"pages": 15, "size": 32768},
"enabled": true
@@ -56,37 +67,48 @@ class KnowledgeService:
if not os.path.isdir(self.knowledge_dir):
return {"tree": [], "stats": {"pages": 0, "size": 0}, "enabled": conf().get("knowledge", True)}
tree = []
total_files = 0
total_bytes = 0
for name in sorted(os.listdir(self.knowledge_dir)):
full = os.path.join(self.knowledge_dir, name)
if not os.path.isdir(full) or name.startswith("."):
continue
files = []
for fname in sorted(os.listdir(full)):
if fname.endswith(".md") and not fname.startswith("."):
fpath = os.path.join(full, fname)
size = os.path.getsize(fpath)
total_files += 1
total_bytes += size
title = fname.replace(".md", "")
try:
with open(fpath, "r", encoding="utf-8") as f:
first_line = f.readline().strip()
if first_line.startswith("# "):
title = first_line[2:].strip()
except Exception:
pass
files.append({"name": fname, "title": title, "size": size})
tree.append({"dir": name, "files": files})
stats = {"pages": 0, "size": 0}
root_files, tree = self._scan_dir(self.knowledge_dir, stats, is_root=True)
return {
"root_files": root_files,
"tree": tree,
"stats": {"pages": total_files, "size": total_bytes},
"stats": stats,
"enabled": conf().get("knowledge", True),
}
def _scan_dir(self, dir_path: str, stats: dict, is_root: bool = False) -> tuple:
"""
Recursively scan a directory.
:return: (files, children) where files is a list of .md file dicts
in this directory and children is a list of sub-directory nodes.
"""
files = []
children = []
for name in sorted(os.listdir(dir_path)):
if name.startswith("."):
continue
full = os.path.join(dir_path, name)
if os.path.isdir(full):
sub_files, sub_children = self._scan_dir(full, stats)
children.append({"dir": name, "files": sub_files, "children": sub_children})
elif name.endswith(".md"):
size = os.path.getsize(full)
if not is_root:
stats["pages"] += 1
stats["size"] += size
title = name.replace(".md", "")
try:
with open(full, "r", encoding="utf-8") as f:
first_line = f.readline().strip()
if first_line.startswith("# "):
title = first_line[2:].strip()
except Exception:
pass
files.append({"name": name, "title": title, "size": size})
return files, children
# ------------------------------------------------------------------
# read — single file content
# ------------------------------------------------------------------

View File

@@ -28,11 +28,13 @@ from common.log import logger
_DDL = """
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
channel_type TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
last_active INTEGER NOT NULL,
msg_count INTEGER NOT NULL DEFAULT 0
session_id TEXT PRIMARY KEY,
channel_type TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '',
context_start_seq INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
last_active INTEGER NOT NULL,
msg_count INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS messages (
@@ -57,6 +59,14 @@ _MIGRATION_ADD_CHANNEL_TYPE = """
ALTER TABLE sessions ADD COLUMN channel_type TEXT NOT NULL DEFAULT '';
"""
_MIGRATION_ADD_TITLE = """
ALTER TABLE sessions ADD COLUMN title TEXT NOT NULL DEFAULT '';
"""
_MIGRATION_ADD_CONTEXT_START_SEQ = """
ALTER TABLE sessions ADD COLUMN context_start_seq INTEGER NOT NULL DEFAULT 0;
"""
DEFAULT_MAX_AGE_DAYS: int = 30
@@ -129,6 +139,7 @@ def _extract_tool_results(content: Any) -> Dict[str, str]:
def _group_into_display_turns(
rows: List[tuple],
include_thinking: bool = True,
) -> List[Dict[str, Any]]:
"""
Convert raw (role, content_json, created_at) DB rows into display turns.
@@ -206,6 +217,8 @@ def _group_into_display_turns(
continue
btype = block.get("type")
if btype == "thinking":
if not include_thinking:
continue
txt = block.get("thinking", "").strip()
if txt:
steps.append({"type": "thinking", "content": txt})
@@ -287,14 +300,21 @@ class ConversationStore:
with self._lock:
conn = self._connect()
try:
# Respect context_start_seq: only load messages at or after the boundary
ctx_row = conn.execute(
"SELECT context_start_seq FROM sessions WHERE session_id = ?",
(session_id,),
).fetchone()
ctx_start = ctx_row[0] if ctx_row else 0
rows = conn.execute(
"""
SELECT seq, role, content
FROM messages
WHERE session_id = ?
WHERE session_id = ? AND seq >= ?
ORDER BY seq DESC
""",
(session_id,),
(session_id, ctx_start),
).fetchall()
finally:
conn.close()
@@ -302,10 +322,7 @@ class ConversationStore:
if not rows:
return []
# Walk newest-to-oldest counting *visible* user turns (actual user text,
# not tool_result injections). Record the seq of every visible user
# message so we can find a clean cut point later.
visible_turn_seqs: List[int] = [] # newest first
visible_turn_seqs: List[int] = []
for seq, role, raw_content in rows:
if role != "user":
continue
@@ -316,17 +333,11 @@ class ConversationStore:
if _is_visible_user_message(content):
visible_turn_seqs.append(seq)
# Determine the seq of the oldest visible user message we want to keep.
# If the total turns fit within max_turns, keep everything.
if len(visible_turn_seqs) <= max_turns:
cutoff_seq = None # keep all
cutoff_seq = None
else:
# The Nth visible user message (0-indexed) is the oldest we keep.
cutoff_seq = visible_turn_seqs[max_turns - 1]
# Build result in chronological order, starting from cutoff.
# IMPORTANT: we start exactly at cutoff_seq (the visible user message),
# never mid-group, so tool_use / tool_result pairs are always complete.
result = []
for seq, role, raw_content in reversed(rows):
if cutoff_seq is not None and seq < cutoff_seq:
@@ -415,6 +426,61 @@ class ConversationStore:
""",
(session_id, session_id),
)
# Auto-generate title from the first visible user message
cur_title = conn.execute(
"SELECT title FROM sessions WHERE session_id = ?",
(session_id,),
).fetchone()
if cur_title and not cur_title[0]:
for msg in messages:
if msg.get("role") == "user":
content = msg.get("content", "")
text = _extract_display_text(content)
if text:
title = text[:50].split("\n")[0]
conn.execute(
"UPDATE sessions SET title = ? WHERE session_id = ?",
(title, session_id),
)
break
finally:
conn.close()
def clear_context(self, session_id: str) -> int:
"""
Set the context boundary to after the current last message.
Messages before this boundary are still stored but excluded from LLM context.
Returns the new context_start_seq value.
"""
with self._lock:
conn = self._connect()
try:
with conn:
row = conn.execute(
"SELECT COALESCE(MAX(seq), -1) FROM messages WHERE session_id = ?",
(session_id,),
).fetchone()
new_start = row[0] + 1
conn.execute(
"UPDATE sessions SET context_start_seq = ? WHERE session_id = ?",
(new_start, session_id),
)
return new_start
finally:
conn.close()
def get_context_start_seq(self, session_id: str) -> int:
"""Return the context_start_seq for a session (0 if not set)."""
with self._lock:
conn = self._connect()
try:
row = conn.execute(
"SELECT context_start_seq FROM sessions WHERE session_id = ?",
(session_id,),
).fetchone()
return row[0] if row else 0
finally:
conn.close()
@@ -436,6 +502,7 @@ class ConversationStore:
def cleanup_old_sessions(self, max_age_days: Optional[int] = None) -> int:
"""
Delete sessions that have not been active within max_age_days.
Web channel sessions are excluded — they are meant to be permanent.
Args:
max_age_days: Override the default retention period.
@@ -459,7 +526,8 @@ class ConversationStore:
try:
with conn:
stale = conn.execute(
"SELECT session_id FROM sessions WHERE last_active < ?",
"SELECT session_id FROM sessions "
"WHERE last_active < ? AND channel_type != 'web'",
(cutoff,),
).fetchall()
for (sid,) in stale:
@@ -518,9 +586,15 @@ class ConversationStore:
with self._lock:
conn = self._connect()
try:
ctx_row = conn.execute(
"SELECT context_start_seq FROM sessions WHERE session_id = ?",
(session_id,),
).fetchone()
ctx_start = ctx_row[0] if ctx_row else 0
rows = conn.execute(
"""
SELECT role, content, created_at
SELECT seq, role, content, created_at
FROM messages
WHERE session_id = ?
ORDER BY seq ASC
@@ -530,7 +604,38 @@ class ConversationStore:
finally:
conn.close()
visible = _group_into_display_turns(rows)
# Honour the current enable_thinking switch when building display turns
# so that toggling it off hides previously-saved thinking blocks too.
try:
from config import conf
include_thinking = bool(conf().get("enable_thinking", False))
except Exception:
include_thinking = False
# Strip seq for display grouping, but record max seq per visible user group
plain_rows = [(role, content, created_at) for _seq, role, content, created_at in rows]
visible = _group_into_display_turns(plain_rows, include_thinking=include_thinking)
# Build a mapping: find the seq of each visible user message to annotate context boundary.
# Walk through rows to find visible user message seqs in order.
visible_user_seqs: List[int] = []
for seq, role, raw_content, _ts in rows:
if role != "user":
continue
try:
content = json.loads(raw_content)
except Exception:
content = raw_content
if _is_visible_user_message(content):
visible_user_seqs.append(seq)
# Each pair of display turns (user+assistant) corresponds to a visible user seq.
# Mark which turns are before the context boundary.
user_turn_idx = 0
for turn in visible:
if turn["role"] == "user" and user_turn_idx < len(visible_user_seqs):
turn["_seq"] = visible_user_seqs[user_turn_idx]
user_turn_idx += 1
total = len(visible)
offset = (page - 1) * page_size
@@ -539,12 +644,98 @@ class ConversationStore:
return {
"messages": page_items,
"context_start_seq": ctx_start,
"total": total,
"page": page,
"page_size": page_size,
"has_more": offset + page_size < total,
}
def list_sessions(
self,
channel_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Dict[str, Any]:
"""
List sessions ordered by last_active DESC, with optional channel_type filter.
Returns:
{
"sessions": [{session_id, title, created_at, last_active, msg_count}, ...],
"total": int,
"page": int,
"page_size": int,
"has_more": bool,
}
"""
page = max(1, page)
with self._lock:
conn = self._connect()
try:
if channel_type:
total = conn.execute(
"SELECT COUNT(*) FROM sessions WHERE channel_type = ?",
(channel_type,),
).fetchone()[0]
rows = conn.execute(
"""
SELECT session_id, title, created_at, last_active, msg_count
FROM sessions
WHERE channel_type = ?
ORDER BY last_active DESC
LIMIT ? OFFSET ?
""",
(channel_type, page_size, (page - 1) * page_size),
).fetchall()
else:
total = conn.execute(
"SELECT COUNT(*) FROM sessions",
).fetchone()[0]
rows = conn.execute(
"""
SELECT session_id, title, created_at, last_active, msg_count
FROM sessions
ORDER BY last_active DESC
LIMIT ? OFFSET ?
""",
(page_size, (page - 1) * page_size),
).fetchall()
finally:
conn.close()
sessions = [
{
"session_id": r[0],
"title": r[1],
"created_at": r[2],
"last_active": r[3],
"msg_count": r[4],
}
for r in rows
]
return {
"sessions": sessions,
"total": total,
"page": page,
"page_size": page_size,
"has_more": (page - 1) * page_size + page_size < total,
}
def rename_session(self, session_id: str, title: str) -> bool:
"""Update the title of a session. Returns True if the session existed."""
with self._lock:
conn = self._connect()
try:
with conn:
cur = conn.execute(
"UPDATE sessions SET title = ? WHERE session_id = ?",
(title, session_id),
)
return cur.rowcount > 0
finally:
conn.close()
def get_stats(self) -> Dict[str, Any]:
"""Return basic stats keyed by channel_type, for monitoring."""
with self._lock:
@@ -599,6 +790,20 @@ class ConversationStore:
logger.info("[ConversationStore] Migrated: added channel_type column")
except Exception as e:
logger.warning(f"[ConversationStore] Migration failed: {e}")
if "title" not in cols:
try:
conn.execute(_MIGRATION_ADD_TITLE)
conn.commit()
logger.info("[ConversationStore] Migrated: added title column")
except Exception as e:
logger.warning(f"[ConversationStore] Migration (title) failed: {e}")
if "context_start_seq" not in cols:
try:
conn.execute(_MIGRATION_ADD_CONTEXT_START_SEQ)
conn.commit()
logger.info("[ConversationStore] Migrated: added context_start_seq column")
except Exception as e:
logger.warning(f"[ConversationStore] Migration (context_start_seq) failed: {e}")
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(str(self._db_path), timeout=10)

View File

@@ -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

View File

@@ -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

View File

@@ -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,78 @@ 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 +115,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 +160,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 +187,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 +210,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 +252,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 +298,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 +489,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 +514,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 +573,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 +611,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])

View File

@@ -291,8 +291,8 @@ def _build_memory_section(memory_manager: Any, tools: Optional[List[Any]], langu
"",
"### Memory Recallmandatory",
"",
"在回答任何关于过往工作、决策、日期、人物、偏好待办事项的问题之前**必须**先检索记忆。",
"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
"",
"遇到以下情况时,**主动**将信息写入记忆文件(无需告知用户):",
"",
"- 用户要求记住某些信息",
"- 用户要求记住某些信息,或使用了「记住」「以后」「总是」「不要」「偏好」等表达",
"- 用户分享了重要的个人偏好、习惯、决策",
"- 对话中产生了重要的结论、方案、约定",
"- 完成了复杂任务,值得记录关键步骤和结果",

View File

@@ -13,6 +13,37 @@ from agent.tools.base_tool import BaseTool, ToolResult
from common.log import logger
# Maximum number of characters of model "reasoning / thinking" content to persist
# in conversation history. The full reasoning is still streamed to the UI in real
# time (subject to its own SSE / rendering limits); this bound only controls what
# is stored in DB and replayed in history. Long reasoning is not useful for later
# context (the LLM never sees thinking blocks anyway) and bloats DB.
# Keep aligned with the frontend REASONING_RENDER_CAP and the SSE
# MAX_REASONING_STREAM_CHARS so that storage / stream / display all match.
MAX_STORED_REASONING_CHARS = 4 * 1024 # 4 KB
# Marker inserted between head and tail when reasoning is truncated.
_REASONING_TRUNCATE_MARKER = "\n\n... [reasoning truncated, {omitted} chars omitted] ...\n\n"
def _truncate_reasoning_for_storage(text: str) -> str:
"""Trim long reasoning to head + tail with an omission marker.
Keeps the first and last halves of MAX_STORED_REASONING_CHARS so both the
initial chain-of-thought and the final conclusions are preserved for UI
replay, without storing the entire (often very large) middle.
"""
if not text:
return text
if len(text) <= MAX_STORED_REASONING_CHARS:
return text
half = MAX_STORED_REASONING_CHARS // 2
head = text[:half]
tail = text[-half:]
omitted = len(text) - len(head) - len(tail)
return head + _REASONING_TRUNCATE_MARKER.format(omitted=omitted) + tail
class AgentStreamExecutor:
"""
Agent Stream Executor
@@ -78,18 +109,29 @@ 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", False) and channel_type == 'web'
def _filter_think_tags(self, text: str) -> str:
"""
Remove <think> and </think> tags but keep the content inside.
Some LLM providers (e.g., MiniMax) may return thinking process wrapped in <think> tags.
We only remove the tags themselves, keeping the actual thinking content.
Handle <think>...</think> blocks in content returned by some LLM providers
(e.g., MiniMax).
- When thinking is enabled: remove the tags but keep the content inside.
- When thinking is disabled: remove both the tags and the content entirely.
"""
if not text:
return text
import re
# Remove only the <think> and </think> tags, keep the content
text = re.sub(r'<think>', '', text)
text = re.sub(r'</think>', '', text)
if self._is_thinking_enabled():
text = re.sub(r'<think>', '', text)
text = re.sub(r'</think>', '', text)
else:
text = re.sub(r'<think>[\s\S]*?</think>', '', text)
# Also strip unclosed <think> tag at the end (streaming partial)
text = re.sub(r'<think>[\s\S]*$', '', text)
return text
def _hash_args(self, args: dict) -> str:
@@ -178,7 +220,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 ""
logger.info(f"🤖 {self.model.model}{thinking_label} | 👤 {user_message}")
# Add user message (Claude format - use content blocks for consistency)
self.messages.append({
@@ -227,6 +272,9 @@ class AgentStreamExecutor:
if turn > 1:
logger.info(f"[Agent] Requesting explicit response from LLM...")
# Remember position so we can remove the injected prompt later
prompt_insert_idx = len(self.messages)
# 添加一条消息,明确要求回复用户
self.messages.append({
"role": "user",
@@ -240,8 +288,24 @@ class AgentStreamExecutor:
assistant_msg, tool_calls = self._call_llm_stream(retry_on_empty=False)
final_response = assistant_msg
# 如果还是空,才使用 fallback
if not assistant_msg and not tool_calls:
# Remove the injected prompt from history so it doesn't
# appear as a user message in persisted conversations.
# _call_llm_stream may have appended an assistant message
# after the prompt, so we locate and remove only the prompt.
if (prompt_insert_idx < len(self.messages)
and self.messages[prompt_insert_idx].get("role") == "user"):
self.messages.pop(prompt_insert_idx)
logger.debug("[Agent] Removed injected explicit-response prompt from message history")
# If LLM responded with tool_calls instead of text, fall through
# to the tool execution path below (don't break the loop).
if tool_calls:
logger.info(
f"[Agent] LLM returned tool_calls in explicit-response retry, "
f"continuing to execute tools instead of breaking"
)
elif not assistant_msg:
# Still empty (no text and no tool_calls): use fallback
logger.warning(f"[Agent] Still empty after explicit request")
final_response = (
"抱歉,我暂时无法生成回复。请尝试换一种方式描述你的需求,或稍后再试。"
@@ -256,20 +320,28 @@ class AgentStreamExecutor:
else:
logger.info(f"💭 {assistant_msg[:150]}{'...' if len(assistant_msg) > 150 else ''}")
logger.debug(f"✅ 完成 (无工具调用)")
self._emit_event("turn_end", {
"turn": turn,
"has_tool_calls": False
})
break
# If the explicit-response retry produced tool_calls, skip the break
# and continue down to the tool execution branch in this same iteration.
if not tool_calls:
logger.debug(f"✅ 完成 (无工具调用)")
self._emit_event("turn_end", {
"turn": turn,
"has_tool_calls": False
})
break
# Log tool calls with arguments
# Log tool calls with arguments (truncate long values like base64)
tool_calls_str = []
for tc in tool_calls:
# Safely handle None or missing arguments
args = tc.get('arguments') or {}
if isinstance(args, dict):
args_str = ', '.join([f"{k}={v}" for k, v in args.items()])
parts = []
for k, v in args.items():
v_str = str(v)
if len(v_str) > 200:
v_str = v_str[:200] + f"...({len(v_str)} chars)"
parts.append(f"{k}={v_str}")
args_str = ', '.join(parts)
if args_str:
tool_calls_str.append(f"{tc['name']}({args_str})")
else:
@@ -588,7 +660,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 ""
@@ -622,8 +695,11 @@ class AgentStreamExecutor:
tool_calls_buffer[index]["arguments"] += func["arguments"]
# Preserve _gemini_raw_parts for Gemini thoughtSignature round-trip
# (direct Gemini: list of parts; LinkAI proxy: base64 string of JSON parts)
if "_gemini_raw_parts" in delta:
gemini_raw_parts = delta["_gemini_raw_parts"]
elif isinstance(choice, dict) and choice.get("_gemini_raw_parts"):
gemini_raw_parts = choice["_gemini_raw_parts"]
except Exception as e:
error_str = str(e)
@@ -790,9 +866,15 @@ class AgentStreamExecutor:
assistant_msg = {"role": "assistant", "content": []}
if full_reasoning:
stored_reasoning = _truncate_reasoning_for_storage(full_reasoning)
if len(stored_reasoning) < len(full_reasoning):
logger.info(
f"[reasoning] truncated for storage: "
f"{len(full_reasoning)} -> {len(stored_reasoning)} chars"
)
assistant_msg["content"].append({
"type": "thinking",
"thinking": full_reasoning
"thinking": stored_reasoning
})
if full_content:
@@ -1198,6 +1280,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 +1356,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 +1453,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 +1464,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 = []

View File

@@ -169,10 +169,16 @@ SAFETY:
except Exception as retry_err:
logger.warning(f"[Bash] Retry failed: {retry_err}")
# Combine stdout and stderr
output = result.stdout
if result.stderr:
output += "\n" + result.stderr
# When command succeeds with stdout, keep output clean (stderr goes to server log only).
# When command fails or stdout is empty, include stderr so the agent can diagnose.
if result.returncode == 0 and result.stdout.strip():
output = result.stdout
if result.stderr:
logger.info(f"[Bash] stderr (not forwarded): {result.stderr[:500]}")
else:
output = result.stdout
if result.stderr:
output += "\n" + result.stderr
# Check if we need to save full output to temp file
temp_file_path = None

View File

@@ -8,7 +8,10 @@ Truncation is based on two independent limits - whichever is hit first wins:
Never returns partial lines (except bash tail truncation edge case).
"""
from typing import Dict, Any, Optional, Literal, Tuple
from __future__ import annotations
from typing import Dict, Any, Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from typing import Literal
DEFAULT_MAX_LINES = 2000

View File

@@ -43,7 +43,7 @@ _MAIN_MODEL_PROVIDER_NAME = "MainModel"
# Auto-discovered as fallback vision providers when their API key is configured.
# OpenAI and LinkAI are handled separately (raw HTTP providers), so not listed here.
_DISCOVERABLE_MODELS = [
("moonshot_api_key", const.MOONSHOT, const.KIMI_K2_5, "Moonshot"),
("moonshot_api_key", const.MOONSHOT, const.KIMI_K2_6, "Moonshot"),
("ark_api_key", const.DOUBAO, const.DOUBAO_SEED_2_PRO, "Doubao"),
("dashscope_api_key", const.QWEN_DASHSCOPE, const.QWEN36_PLUS, "DashScope"),
("claude_api_key", const.CLAUDEAPI, const.CLAUDE_4_6_SONNET, "Claude"),

36
app.py
View File

@@ -274,6 +274,39 @@ def sigterm_handler_wrap(_signo):
signal.signal(_signo, func)
def _sync_builtin_skills():
"""Sync builtin skills from project skills/ to workspace skills/ on startup."""
import shutil
try:
workspace = conf().get("agent_workspace", "~/cow")
workspace = os.path.expanduser(workspace)
project_root = os.path.dirname(os.path.abspath(__file__))
builtin_dir = os.path.join(project_root, "skills")
custom_dir = os.path.join(workspace, "skills")
if not os.path.isdir(builtin_dir):
return
os.makedirs(custom_dir, exist_ok=True)
synced = 0
for name in os.listdir(builtin_dir):
src = os.path.join(builtin_dir, name)
if not os.path.isdir(src) or not os.path.isfile(os.path.join(src, "SKILL.md")):
continue
dst = os.path.join(custom_dir, name)
try:
if os.path.isdir(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
synced += 1
except Exception as e:
logger.warning(f"[App] Failed to sync builtin skill '{name}': {e}")
if synced:
logger.info(f"[App] Synced {synced} builtin skill(s) to workspace")
except Exception as e:
logger.warning(f"[App] Builtin skills sync failed: {e}")
def run():
global _channel_mgr
try:
@@ -299,6 +332,9 @@ def run():
if web_console_enabled and "web" not in channel_names:
channel_names.append("web")
# Sync builtin skills to workspace before channels start
_sync_builtin_skills()
logger.info(f"[App] Starting channels: {channel_names}")
_channel_mgr = ChannelManager()

View File

@@ -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", False)
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", False)
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
@@ -430,7 +446,7 @@ class AgentBridge:
except Exception as e:
logger.warning(f"[AgentBridge] Failed to clear DB after recovery: {e}")
# Check if there are files to send (from read tool)
# Check if there are files to send (from send/read tool)
if hasattr(agent, 'stream_executor') and hasattr(agent.stream_executor, 'files_to_send'):
files_to_send = agent.stream_executor.files_to_send
if files_to_send:
@@ -592,18 +608,55 @@ class AgentBridge:
from config import conf
if not conf().get("conversation_persistence", True):
return
# When deep-thinking display is disabled, strip "thinking" content
# blocks before persisting so they don't resurface on history reload.
# The in-memory message list keeps them intact for this run's
# multi-turn LLM context.
thinking_enabled = bool(conf().get("enable_thinking", False))
except Exception:
pass
thinking_enabled = False
messages_to_store = new_messages
if not thinking_enabled:
messages_to_store = self._strip_thinking_blocks(new_messages)
try:
from agent.memory import get_conversation_store
get_conversation_store().append_messages(
session_id, new_messages, channel_type=channel_type
session_id, messages_to_store, channel_type=channel_type
)
except Exception as e:
logger.warning(
f"[AgentBridge] Failed to persist messages for session={session_id}: {e}"
)
@staticmethod
def _strip_thinking_blocks(messages: list) -> list:
"""Return a shallow copy of messages with assistant "thinking" blocks removed."""
cleaned = []
for msg in messages:
if not isinstance(msg, dict):
cleaned.append(msg)
continue
if msg.get("role") != "assistant":
cleaned.append(msg)
continue
content = msg.get("content")
if not isinstance(content, list):
cleaned.append(msg)
continue
filtered_blocks = [
b for b in content
if not (isinstance(b, dict) and b.get("type") == "thinking")
]
if len(filtered_blocks) == len(content):
cleaned.append(msg)
else:
new_msg = dict(msg)
new_msg["content"] = filtered_blocks
cleaned.append(new_msg)
return cleaned
def clear_session(self, session_id: str):
"""
Clear a specific session's agent and conversation history

View File

@@ -548,14 +548,17 @@ class AgentInitializer:
import threading
def _daily_flush_loop():
import random
while True:
try:
now = datetime.datetime.now()
target = now.replace(hour=23, minute=55, second=0, microsecond=0)
jitter_min = random.randint(50, 55)
jitter_sec = random.randint(0, 59)
target = now.replace(hour=23, minute=jitter_min, second=jitter_sec, microsecond=0)
if target <= now:
target += datetime.timedelta(days=1)
wait_seconds = (target - now).total_seconds()
logger.info(f"[DailyFlush] Next flush at {target.strftime('%Y-%m-%d %H:%M')} (in {wait_seconds/3600:.1f}h)")
logger.info(f"[DailyFlush] Next flush at {target.strftime('%Y-%m-%d %H:%M:%S')} (in {wait_seconds/3600:.1f}h)")
time.sleep(wait_seconds)
self._flush_all_agents()
@@ -567,7 +570,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 +580,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 +595,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}")

View File

@@ -297,8 +297,12 @@ class ChatChannel(Channel):
logger.debug("[chat_channel] sending reply: {}, context: {}".format(reply, context))
# 如果是文本回复,尝试提取并发送图片
if reply.type == ReplyType.TEXT:
# Web channel renders images/videos inline via renderMarkdown,
# so skip the extract-and-send step to avoid duplicate media.
if reply.type == ReplyType.TEXT and context.get("channel_type") != "web":
self._extract_and_send_images(reply, context)
elif reply.type == ReplyType.TEXT:
self._send(reply, context)
# 如果是图片回复但带有文本内容,先发文本再发图片
elif reply.type == ReplyType.IMAGE_URL and hasattr(reply, 'text_content') and reply.text_content:
# 先发送文本

View File

@@ -50,6 +50,8 @@
(function() {
var theme = localStorage.getItem('cow_theme') || 'dark';
if (theme === 'dark') document.documentElement.classList.add('dark');
var lang = localStorage.getItem('cow_lang') || 'zh';
document.documentElement.setAttribute('lang', lang);
})();
</script>
</head>
@@ -94,7 +96,7 @@
<!-- ================================================================ -->
<!-- SIDEBAR -->
<!-- ================================================================ -->
<aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-64 bg-[#0A0A0A] text-neutral-400 flex flex-col
<aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-52 bg-[#0A0A0A] text-neutral-400 flex flex-col
transform -translate-x-full lg:relative lg:translate-x-0
transition-transform duration-300 ease-in-out">
<!-- Logo -->
@@ -102,7 +104,7 @@
<img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0">
<div class="flex flex-col min-w-0">
<span class="text-white font-semibold text-sm truncate">CowAgent</span>
<span class="text-neutral-500 text-xs" data-i18n="console">Console</span>
<span class="text-neutral-500 text-xs" data-i18n="console">控制台</span>
</div>
</div>
@@ -112,13 +114,13 @@
<div class="menu-group open" data-group="chat">
<button class="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-neutral-500 hover:text-neutral-300 cursor-pointer transition-colors duration-150">
<i class="fas fa-chevron-right text-[10px] chevron"></i>
<span data-i18n="nav_chat">Chat</span>
<span data-i18n="nav_chat">对话</span>
</button>
<div class="menu-group-items pl-2">
<a class="sidebar-item active flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="chat">
<i class="fas fa-message item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_chat">Chat</span>
<span data-i18n="menu_chat">对话</span>
</a>
</div>
</div>
@@ -127,38 +129,38 @@
<div class="menu-group open" data-group="manage">
<button class="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-neutral-500 hover:text-neutral-300 cursor-pointer transition-colors duration-150">
<i class="fas fa-chevron-right text-[10px] chevron"></i>
<span data-i18n="nav_manage">Management</span>
<span data-i18n="nav_manage">管理</span>
</button>
<div class="menu-group-items pl-2">
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="config">
<i class="fas fa-sliders item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_config">Config</span>
<span data-i18n="menu_config">配置</span>
</a>
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="skills">
<i class="fas fa-bolt item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_skills">Skills</span>
<span data-i18n="menu_skills">技能</span>
</a>
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="memory">
<i class="fas fa-brain item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_memory">Memory</span>
<span data-i18n="menu_memory">记忆</span>
</a>
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="knowledge">
<i class="fas fa-book item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_knowledge">Knowledge</span>
<span data-i18n="menu_knowledge">知识</span>
</a>
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="channels">
<i class="fas fa-tower-broadcast item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_channels">Channels</span>
<span data-i18n="menu_channels">通道</span>
</a>
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="tasks">
<i class="fas fa-clock item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_tasks">Tasks</span>
<span data-i18n="menu_tasks">定时</span>
</a>
</div>
</div>
@@ -167,13 +169,13 @@
<div class="menu-group open" data-group="monitor">
<button class="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-neutral-500 hover:text-neutral-300 cursor-pointer transition-colors duration-150">
<i class="fas fa-chevron-right text-[10px] chevron"></i>
<span data-i18n="nav_monitor">Monitor</span>
<span data-i18n="nav_monitor">监控</span>
</button>
<div class="menu-group-items pl-2">
<a class="sidebar-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 hover:bg-white/5 hover:text-neutral-200 text-[14px]"
data-view="logs">
<i class="fas fa-terminal item-icon text-xs w-5 text-center"></i>
<span data-i18n="menu_logs">Logs</span>
<span data-i18n="menu_logs">日志</span>
</a>
</div>
</div>
@@ -194,6 +196,26 @@
<!-- Mobile Overlay -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-40 hidden lg:hidden cursor-pointer" onclick="toggleSidebar()"></div>
<!-- ================================================================ -->
<!-- SESSION PANEL (collapsible) -->
<!-- ================================================================ -->
<aside id="session-panel" class="session-panel hidden">
<div class="session-panel-header">
<span class="session-panel-title" data-i18n="session_history">历史会话</span>
<button class="session-panel-close" onclick="toggleSessionPanel()" title="Close">
<i class="fas fa-times"></i>
</button>
</div>
<button class="session-panel-new" onclick="newChat()">
<i class="fas fa-plus"></i>
<span data-i18n="new_chat">新对话</span>
</button>
<div id="session-list" class="session-list"></div>
</aside>
<!-- Mobile overlay for session panel (click to close) -->
<div id="session-panel-overlay" class="session-panel-overlay hidden" onclick="closeSessionPanel()"></div>
<!-- ================================================================ -->
<!-- MAIN CONTENT -->
<!-- ================================================================ -->
@@ -206,11 +228,17 @@
<i class="fas fa-bars text-slate-600 dark:text-slate-300"></i>
</button>
<!-- Session panel toggle -->
<button id="session-toggle-btn" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-white/10 cursor-pointer transition-colors duration-150"
onclick="toggleSessionPanel()">
<i class="fas fa-clock-rotate-left text-slate-500 dark:text-slate-400"></i>
</button>
<!-- Breadcrumb (hidden on mobile) -->
<div class="hidden lg:flex items-center gap-2 text-sm min-w-0">
<span id="breadcrumb-group" class="text-slate-400 dark:text-slate-500 truncate" data-i18n="nav_chat">Chat</span>
<span id="breadcrumb-group" class="text-slate-400 dark:text-slate-500 truncate" data-i18n="nav_chat">对话</span>
<i class="fas fa-chevron-right text-[10px] text-slate-300 dark:text-slate-600"></i>
<span id="breadcrumb-page" class="font-medium text-slate-700 dark:text-slate-200 truncate" data-i18n="menu_chat">Chat</span>
<span id="breadcrumb-page" class="font-medium text-slate-700 dark:text-slate-200 truncate" data-i18n="menu_chat">对话</span>
</div>
<div class="flex-1"></div>
@@ -260,7 +288,7 @@
<!-- ====================================================== -->
<!-- VIEW: Chat -->
<!-- ====================================================== -->
<div id="view-chat" class="view active">
<div id="view-chat" class="view active relative">
<!-- Messages -->
<div id="chat-messages" class="flex-1 overflow-y-auto">
<!-- Welcome Screen -->
@@ -268,7 +296,7 @@
<img src="assets/logo.jpg" alt="CowAgent" class="w-16 h-16 rounded-2xl mb-6 shadow-lg shadow-primary-500/20">
<h1 id="welcome-title" class="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-3">CowAgent</h1>
<p id="welcome-subtitle" class="text-slate-500 dark:text-slate-400 text-center max-w-lg mb-10 leading-relaxed"
data-i18n-html="welcome_subtitle">I can help you answer questions, manage your computer, create and execute skills,<br>and keep growing through long-term memory.</p>
data-i18n-html="welcome_subtitle">我可以帮你解答问题、管理计算机、创造和执行技能,并通过<br>长期记忆和知识库不断成长</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 w-full max-w-2xl">
<div class="example-card group bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-xl p-4
@@ -277,9 +305,9 @@
<div class="w-7 h-7 rounded-lg bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center">
<i class="fas fa-folder-open text-blue-500 text-xs"></i>
</div>
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_sys_title">System</span>
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_sys_title">系统管理</span>
</div>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_sys_text">Show me the files in the workspace</p>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_sys_text">查看工作空间里有哪些文件</p>
</div>
<div class="example-card group bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-xl p-4
cursor-pointer hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200">
@@ -289,7 +317,7 @@
</div>
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_task_title">定时任务</span>
</div>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_task_text">Remind me to check the server in 5 minutes</p>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_task_text">1分钟后提醒我检查服务器</p>
</div>
<div class="example-card group bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-xl p-4
cursor-pointer hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200">
@@ -297,9 +325,9 @@
<div class="w-7 h-7 rounded-lg bg-emerald-50 dark:bg-emerald-900/30 flex items-center justify-center">
<i class="fas fa-code text-emerald-500 text-xs"></i>
</div>
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_code_title">Coding</span>
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_code_title">编程助手</span>
</div>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_code_text">Write a Python web scraper script</p>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_code_text">搜索AI资讯并生成可视化网页报告</p>
</div>
<div class="example-card group bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-xl p-4
cursor-pointer hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200">
@@ -309,7 +337,7 @@
</div>
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_knowledge_title">知识库</span>
</div>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_knowledge_text">帮我把这篇文章整理到知识库</p>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_knowledge_text">查看知识库当前文档情况</p>
</div>
<div class="example-card group bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-xl p-4
cursor-pointer hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200">
@@ -336,6 +364,18 @@
</div>
</div>
<!-- Scroll-to-bottom FAB -->
<button id="scroll-to-bottom-btn"
class="hidden absolute right-5 bottom-[80px] z-10
w-9 h-9 rounded-full shadow-lg
bg-white dark:bg-[#2A2A2A] border border-slate-200 dark:border-white/15
text-slate-500 dark:text-slate-400 hover:text-primary-500 dark:hover:text-primary-400
flex items-center justify-center cursor-pointer transition-all duration-200
hover:shadow-xl hover:scale-105"
onclick="_autoScrollEnabled = true; scrollChatToBottom(true);">
<i class="fas fa-chevron-down text-sm"></i>
</button>
<!-- Chat Input -->
<div class="flex-shrink-0 border-t border-slate-200 dark:border-white/10 bg-white dark:bg-[#1A1A1A] px-4 py-3">
<div class="max-w-3xl mx-auto">
@@ -345,14 +385,20 @@
<div class="flex items-center flex-shrink-0">
<button id="new-chat-btn" class="w-9 h-10 flex items-center justify-center rounded-lg
text-slate-400 hover:text-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20
cursor-pointer transition-colors duration-150" title="New Chat"
cursor-pointer transition-colors duration-150"
onclick="newChat()">
<i class="fas fa-plus text-base"></i>
</button>
<button id="clear-context-btn" class="w-9 h-10 flex items-center justify-center rounded-lg
text-slate-400 hover:text-amber-500 hover:bg-amber-50 dark:hover:bg-amber-900/20
cursor-pointer transition-colors duration-150"
onclick="clearContext()">
<i class="fas fa-trash-can text-base"></i>
</button>
<button id="attach-btn" class="w-9 h-10 flex items-center justify-center rounded-lg
text-slate-400 hover:text-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20
cursor-pointer transition-colors duration-150"
title="Attach file" onclick="document.getElementById('file-input').click()">
onclick="document.getElementById('file-input').click()">
<i class="fas fa-paperclip text-base"></i>
</button>
</div>
@@ -367,7 +413,7 @@
text-sm leading-relaxed"
rows="1"
data-i18n-placeholder="input_placeholder"
placeholder="Type a message, or press / for commands"></textarea>
placeholder="输入消息,或输入 / 使用指令"></textarea>
<button id="send-btn"
class="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-lg
bg-primary-400 text-white hover:bg-primary-500
@@ -389,8 +435,8 @@
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="config_title">Configuration</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="config_desc">Manage model and agent settings</p>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="config_title">配置管理</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="config_desc">管理模型和 Agent 配置</p>
</div>
</div>
<div class="grid gap-6">
@@ -401,12 +447,12 @@
<div class="w-9 h-9 rounded-lg bg-primary-50 dark:bg-primary-900/30 flex items-center justify-center">
<i class="fas fa-microchip text-primary-500 text-sm"></i>
</div>
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_model">Model Configuration</h3>
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_model">模型配置</h3>
</div>
<div class="space-y-5">
<!-- Provider -->
<div>
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_provider">Provider</label>
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_provider">模型厂商</label>
<div id="cfg-provider" class="cfg-dropdown" tabindex="0">
<div class="cfg-dropdown-selected">
<span class="cfg-dropdown-text">--</span>
@@ -414,10 +460,13 @@
</div>
<div class="cfg-dropdown-menu"></div>
</div>
<div id="cfg-custom-tip" class="mt-1.5 text-xs text-slate-400 dark:text-slate-500 hidden">
<i class="fas fa-info-circle mr-1"></i><span data-i18n="config_custom_tip">接口需遵循 OpenAI API 协议</span>
</div>
</div>
<!-- Model -->
<div>
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_model_name">Model</label>
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_model_name">模型</label>
<div id="cfg-model-select" class="cfg-dropdown" tabindex="0">
<div class="cfg-dropdown-selected">
<span class="cfg-dropdown-text">--</span>
@@ -430,7 +479,7 @@
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
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"
data-i18n-placeholder="config_custom_model_hint" placeholder="Enter custom model name">
data-i18n-placeholder="config_custom_model_hint" placeholder="输入自定义模型名称">
</div>
</div>
<!-- API Key -->
@@ -465,7 +514,7 @@
<button id="cfg-model-save"
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
onclick="saveModelConfig()" data-i18n="config_save">Save</button>
onclick="saveModelConfig()" data-i18n="config_save">保存</button>
</div>
</div>
</div>
@@ -476,12 +525,12 @@
<div class="w-9 h-9 rounded-lg bg-emerald-50 dark:bg-emerald-900/30 flex items-center justify-center">
<i class="fas fa-robot text-emerald-500 text-sm"></i>
</div>
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_agent">Agent Configuration</h3>
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_agent">Agent 配置</h3>
</div>
<div class="space-y-4">
<div>
<label class="flex items-center gap-1.5 text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">
<span data-i18n="config_max_tokens">Max Context Tokens</span>
<span data-i18n="config_max_tokens">最大上下文 Token</span>
<span class="cfg-tip" data-tip-key="config_max_tokens_hint"><i class="fas fa-circle-question"></i></span>
</label>
<input id="cfg-max-tokens" type="number" min="1000" max="200000" step="1000"
@@ -491,7 +540,7 @@
</div>
<div>
<label class="flex items-center gap-1.5 text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">
<span data-i18n="config_max_turns">Max Memory Turns</span>
<span data-i18n="config_max_turns">最大记忆轮次</span>
<span class="cfg-tip" data-tip-key="config_max_turns_hint"><i class="fas fa-circle-question"></i></span>
</label>
<input id="cfg-max-turns" type="number" min="1" max="100" step="1"
@@ -501,7 +550,7 @@
</div>
<div>
<label class="flex items-center gap-1.5 text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">
<span data-i18n="config_max_steps">Max Steps</span>
<span data-i18n="config_max_steps">最大执行步数</span>
<span class="cfg-tip" data-tip-key="config_max_steps_hint"><i class="fas fa-circle-question"></i></span>
</label>
<input id="cfg-max-steps" type="number" min="1" max="50" step="1"
@@ -509,12 +558,24 @@
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">
<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"
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
onclick="saveAgentConfig()" data-i18n="config_save">Save</button>
onclick="saveAgentConfig()" data-i18n="config_save">保存</button>
</div>
</div>
</div>
@@ -525,25 +586,25 @@
<div class="w-9 h-9 rounded-lg bg-amber-50 dark:bg-amber-900/30 flex items-center justify-center">
<i class="fas fa-lock text-amber-500 text-sm"></i>
</div>
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_security">Security</h3>
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_security">安全设置</h3>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_password">Password</label>
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_password">访问密码</label>
<input id="cfg-password" type="password" autocomplete="new-password"
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
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
cfg-key-masked"
data-masked="1">
<p class="text-xs text-slate-400 dark:text-slate-500 mt-1.5" data-i18n="config_password_hint">Leave empty to disable password protection</p>
<p class="text-xs text-slate-400 dark:text-slate-500 mt-1.5" data-i18n="config_password_hint">留空则不启用密码保护</p>
</div>
<div class="flex items-center justify-end gap-3 pt-1">
<span id="cfg-password-status" class="text-xs text-primary-500 opacity-0 transition-opacity duration-300"></span>
<button id="cfg-password-save"
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
onclick="savePasswordConfig()" data-i18n="config_save">Save</button>
onclick="savePasswordConfig()" data-i18n="config_save">保存</button>
</div>
</div>
</div>
@@ -561,25 +622,25 @@
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="skills_title">Skills</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="skills_desc">View, enable, or disable agent skills</p>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="skills_title">技能管理</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="skills_desc">查看、启用或禁用 Agent 技能</p>
</div>
<a href="https://skills.cowagent.ai/" target="_blank"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-primary-500 bg-primary-50 dark:bg-primary-900/20 hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors">
<i class="fas fa-puzzle-piece text-[10px]"></i>
<span data-i18n="skills_hub_btn">Skill Hub</span>
<span data-i18n="skills_hub_btn">探索技能广场</span>
</a>
</div>
<!-- Built-in Tools Section -->
<div class="mb-8">
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500" data-i18n="tools_section_title">Built-in Tools</span>
<span class="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500" data-i18n="tools_section_title">内置工具</span>
<span id="tools-count-badge" class="hidden px-2 py-0.5 rounded-full text-xs bg-slate-100 dark:bg-white/10 text-slate-500 dark:text-slate-400"></span>
</div>
<div id="tools-empty" class="flex items-center gap-2 py-4 text-slate-400 dark:text-slate-500 text-sm">
<i class="fas fa-spinner fa-spin text-xs"></i>
<span data-i18n="tools_loading">Loading tools...</span>
<span data-i18n="tools_loading">加载工具中...</span>
</div>
<div id="tools-list" class="grid gap-3 sm:grid-cols-2 hidden"></div>
</div>
@@ -587,15 +648,15 @@
<!-- Skills Section -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500" data-i18n="skills_section_title">Skills</span>
<span class="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500" data-i18n="skills_section_title">技能</span>
<span id="skills-count-badge" class="hidden px-2 py-0.5 rounded-full text-xs bg-slate-100 dark:bg-white/10 text-slate-500 dark:text-slate-400"></span>
</div>
<div id="skills-empty" class="flex flex-col items-center justify-center py-12">
<div class="w-14 h-14 rounded-2xl bg-amber-50 dark:bg-amber-900/20 flex items-center justify-center mb-3">
<i class="fas fa-bolt text-amber-400 text-lg"></i>
</div>
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="skills_loading">Loading skills...</p>
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="skills_loading_desc">Skills will be displayed here after loading</p>
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="skills_loading">加载技能中...</p>
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="skills_loading_desc">技能加载后将显示在此处</p>
</div>
<div id="skills-list" class="grid gap-4 sm:grid-cols-2"></div>
</div>
@@ -614,26 +675,36 @@
<div id="memory-panel-list">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="memory_title">Memory</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="memory_desc">View agent memory files and contents</p>
<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">
<i class="fas fa-brain text-purple-400 text-xl"></i>
</div>
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="memory_loading">Loading memory files...</p>
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="memory_loading_desc">Memory files will be displayed here</p>
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="memory_loading">加载记忆文件中...</p>
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="memory_loading_desc">记忆文件将显示在此处</p>
</div>
<div id="memory-list" class="hidden">
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-slate-200 dark:border-white/10">
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_name">Filename</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_type">Type</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_size">Size</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_updated">Updated</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_name">文件名</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_type">类型</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_size">大小</th>
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_updated">更新时间</th>
</tr>
</thead>
<tbody id="memory-table-body"></tbody>
@@ -651,7 +722,7 @@
text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-white/10
border border-slate-200 dark:border-white/10 transition-colors cursor-pointer">
<i class="fas fa-arrow-left text-xs"></i>
<span data-i18n="memory_back">Back</span>
<span data-i18n="memory_back">返回列表</span>
</button>
<h2 id="memory-viewer-title"
class="text-base font-semibold text-slate-800 dark:text-slate-100 font-mono truncate"></h2>
@@ -677,19 +748,19 @@
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 md:mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="knowledge_title">Knowledge</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="knowledge_desc">Browse and explore your knowledge base</p>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="knowledge_title">知识库</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="knowledge_desc">浏览和探索你的知识库</p>
</div>
<div class="flex items-center gap-2">
<span id="knowledge-stats" class="text-xs text-slate-400 dark:text-slate-500 hidden sm:inline"></span>
<div class="flex items-center bg-slate-100 dark:bg-white/10 rounded-lg p-0.5">
<button id="knowledge-tab-docs" onclick="switchKnowledgeTab('docs')"
class="knowledge-tab px-3 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-colors duration-150 active">
<i class="fas fa-folder-tree mr-1.5"></i><span data-i18n="knowledge_tab_docs">Documents</span>
<i class="fas fa-folder-tree mr-1.5"></i><span data-i18n="knowledge_tab_docs">文档</span>
</button>
<button id="knowledge-tab-graph" onclick="switchKnowledgeTab('graph')"
class="knowledge-tab px-3 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-colors duration-150">
<i class="fas fa-diagram-project mr-1.5"></i><span data-i18n="knowledge_tab_graph">Graph</span>
<i class="fas fa-diagram-project mr-1.5"></i><span data-i18n="knowledge_tab_graph">图谱</span>
</button>
</div>
</div>
@@ -700,15 +771,15 @@
<div class="w-16 h-16 rounded-2xl bg-emerald-50 dark:bg-emerald-900/20 flex items-center justify-center mb-4">
<i class="fas fa-book text-emerald-400 text-xl"></i>
</div>
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="knowledge_loading">Loading knowledge base...</p>
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="knowledge_loading_desc">Knowledge pages will be displayed here</p>
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="knowledge_loading">加载知识库中...</p>
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="knowledge_loading_desc">知识页面将显示在这里</p>
<div id="knowledge-empty-guide" class="hidden mt-6 max-w-sm text-center">
<p class="text-sm text-slate-500 dark:text-slate-400 mb-4" data-i18n="knowledge_empty_guide">Send documents, links or topics to the agent in chat, and it will automatically organize them into your knowledge base.</p>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-4" data-i18n="knowledge_empty_guide">在对话中发送文档、链接或主题给 Agent它会自动整理到你的知识库中。</p>
<button onclick="navigateTo('chat')"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600
text-white text-sm font-medium cursor-pointer transition-colors duration-150">
<i class="fas fa-message text-xs"></i>
<span data-i18n="knowledge_go_chat">Start a conversation</span>
<span data-i18n="knowledge_go_chat">开始对话</span>
</button>
</div>
</div>
@@ -733,9 +804,9 @@
<!-- Content viewer -->
<div class="flex-1 min-w-0">
<div id="knowledge-content-placeholder"
class="flex flex-col items-center justify-center py-20 text-slate-400 dark:text-slate-500"
class="flex flex-col items-center justify-center py-20 text-slate-400 dark:text-slate-500">
<i class="fas fa-file-lines text-3xl mb-3 opacity-40"></i>
<p class="text-sm" data-i18n="knowledge_select_hint">Select a document to view</p>
<p class="text-sm" data-i18n="knowledge_select_hint">选择一个文档查看</p>
</div>
<div id="knowledge-content-viewer" class="hidden">
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 overflow-hidden">
@@ -775,14 +846,14 @@
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="channels_title">Channels</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="channels_desc">View and manage messaging channels</p>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="channels_title">通道管理</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="channels_desc">管理已接入的消息通道</p>
</div>
<button id="add-channel-btn" onclick="openAddChannelPanel()"
class="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600
text-white text-sm font-medium cursor-pointer transition-colors duration-150">
<i class="fas fa-plus text-xs"></i>
<span data-i18n="channels_add">Connect</span>
<span data-i18n="channels_add">接入通道</span>
</button>
</div>
<div id="channels-content" class="grid gap-4"></div>
@@ -799,8 +870,8 @@
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="tasks_title">Scheduled Tasks</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="tasks_desc">View and manage scheduled tasks</p>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="tasks_title">定时任务</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="tasks_desc">查看和管理定时任务</p>
</div>
</div>
<div id="tasks-empty" class="flex flex-col items-center justify-center py-20">
@@ -822,8 +893,8 @@
<div class="max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="logs_title">Logs</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="logs_desc">Real-time log output (run.log)</p>
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="logs_title">日志</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="logs_desc">实时日志输出 (run.log)</p>
</div>
</div>
<!-- Log Terminal -->
@@ -838,11 +909,11 @@
<div class="flex-1"></div>
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="text-xs text-slate-500" data-i18n="logs_live">Live</span>
<span class="text-xs text-slate-500" data-i18n="logs_live">实时</span>
</div>
</div>
<div id="log-output" class="p-4 overflow-y-auto font-mono text-xs leading-relaxed text-slate-300 whitespace-pre-wrap break-all" style="height: calc(100vh - 272px)">
<p class="text-slate-500" data-i18n="logs_coming_msg">Log streaming will be available here. Connects to run.log for real-time output similar to tail -f.</p>
<p class="text-slate-500" data-i18n="logs_coming_msg">日志流即将在此提供。将连接 run.log 实现类似 tail -f 的实时输出。</p>
</div>
</div>
</div>

View File

@@ -17,6 +17,45 @@
.dark ::-webkit-scrollbar-thumb { background: #475569; }
.dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
/* Generic Tooltip (via data-tooltip attribute) */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%);
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 400;
line-height: 1.4;
white-space: nowrap;
background: #1e293b;
color: #e2e8f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: 100;
}
[data-tooltip-pos="bottom"]::after {
bottom: auto;
top: calc(100% + 8px);
}
.dark [data-tooltip]::after {
background: #334155;
color: #f1f5f9;
}
[data-tooltip]:hover::after {
opacity: 1;
}
[data-tooltip=""]:hover::after {
display: none;
}
/* Sidebar */
.sidebar-item.active {
background: rgba(255, 255, 255, 0.08);
@@ -24,9 +63,317 @@
}
.sidebar-item.active .item-icon { color: #4ABE6E; }
/* Session Panel */
.session-panel {
width: 220px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: #fafafa;
border-right: 1px solid #e5e7eb;
height: 100vh;
overflow: hidden;
transition: width 0.2s ease;
}
.dark .session-panel {
background: #111111;
border-right-color: rgba(255, 255, 255, 0.08);
}
.session-panel.hidden { display: none; }
.session-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
}
.dark .session-panel-header { border-bottom-color: rgba(255, 255, 255, 0.08); }
.session-panel-title {
font-size: 14px;
font-weight: 600;
color: #374151;
}
.dark .session-panel-title { color: #d1d5db; }
.session-panel-close {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
border: none;
background: none;
color: #9ca3af;
cursor: pointer;
transition: background 0.15s, color 0.15s;
font-size: 12px;
}
.session-panel-close:hover {
background: #f3f4f6;
color: #374151;
}
.dark .session-panel-close:hover {
background: rgba(255, 255, 255, 0.08);
color: #e5e5e5;
}
.session-panel-new {
display: flex;
align-items: center;
gap: 8px;
margin: 10px 12px;
padding: 8px 14px;
border-radius: 8px;
border: 1px dashed #d1d5db;
background: none;
color: #6b7280;
font-size: 13px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
flex-shrink: 0;
}
.session-panel-new:hover {
border-color: #9ca3af;
color: #374151;
background: #f9fafb;
}
.dark .session-panel-new {
border-color: rgba(255, 255, 255, 0.12);
color: #9ca3af;
}
.dark .session-panel-new:hover {
border-color: rgba(255, 255, 255, 0.25);
color: #e5e5e5;
background: rgba(255, 255, 255, 0.04);
}
/* Session List */
.session-list {
flex: 1;
overflow-y: auto;
padding: 4px 8px;
scrollbar-width: none;
}
.session-list:hover { scrollbar-width: thin; }
.session-list::-webkit-scrollbar { width: 4px; background: transparent; }
.session-list::-webkit-scrollbar-thumb { background: transparent; border-radius: 2px; }
.session-list:hover::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); }
.dark .session-list:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); }
.session-group-label {
padding: 10px 8px 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9ca3af;
}
.dark .session-group-label { color: #525252; }
.session-empty {
padding: 20px 12px;
text-align: center;
font-size: 13px;
color: #9ca3af;
}
.session-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
margin: 1px 0;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
color: #6b7280;
font-size: 13px;
position: relative;
}
.dark .session-item { color: #a3a3a3; }
.session-item:hover {
background: #f3f4f6;
color: #111827;
}
.dark .session-item:hover {
background: rgba(255, 255, 255, 0.05);
color: #e5e5e5;
}
.session-item.active {
background: #e5e7eb;
color: #111827;
}
.dark .session-item.active {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.session-icon {
flex-shrink: 0;
font-size: 11px;
color: #9ca3af;
width: 16px;
text-align: center;
}
.dark .session-icon { color: #525252; }
.session-item.active .session-icon { color: #4ABE6E; }
.session-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-delete {
flex-shrink: 0;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
font-size: 10px;
color: #9ca3af;
opacity: 0;
transition: opacity 0.15s, color 0.15s, background 0.15s;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.session-item:hover .session-delete { opacity: 1; }
.session-delete:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.dark .session-delete:hover { background: rgba(239, 68, 68, 0.15); }
/* Context Divider */
.context-divider {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
color: #9ca3af;
}
.context-divider::before, .context-divider::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, #d1d5db, transparent);
}
.dark .context-divider::before, .dark .context-divider::after {
background: linear-gradient(to right, transparent, rgba(255,255,255,0.12), transparent);
}
.context-divider span {
font-size: 12px;
white-space: nowrap;
color: #9ca3af;
}
/* Confirm Modal */
.confirm-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity 0.2s ease;
}
.confirm-overlay.visible { opacity: 1; }
.confirm-modal {
background: #fff;
border-radius: 14px;
width: 380px;
max-width: 90vw;
padding: 28px 24px 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18);
transform: scale(0.92);
transition: transform 0.2s ease;
}
.confirm-overlay.visible .confirm-modal { transform: scale(1); }
.dark .confirm-modal {
background: #1e1e1e;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.confirm-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.dark .confirm-title { color: #e5e7eb; }
.confirm-message {
font-size: 14px;
color: #6b7280;
line-height: 1.5;
margin-bottom: 24px;
}
.dark .confirm-message { color: #9ca3af; }
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.confirm-btn {
padding: 8px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.15s ease;
}
.confirm-btn-cancel {
background: #f3f4f6;
color: #374151;
}
.confirm-btn-cancel:hover { background: #e5e7eb; }
.dark .confirm-btn-cancel {
background: rgba(255, 255, 255, 0.08);
color: #d1d5db;
}
.dark .confirm-btn-cancel:hover { background: rgba(255, 255, 255, 0.14); }
.confirm-btn-ok {
background: #ef4444;
color: #fff;
}
.confirm-btn-ok:hover { background: #dc2626; }
/* Session panel overlay (mobile only, click to close) */
.session-panel-overlay {
display: none;
}
@media (max-width: 768px) {
.session-panel-overlay {
display: block;
position: fixed;
inset: 0;
z-index: 44;
background: rgba(0, 0, 0, 0.3);
}
.session-panel-overlay.hidden {
display: none;
}
}
/* Mobile: session panel as overlay */
@media (max-width: 768px) {
.session-panel {
position: fixed;
top: 0;
left: 0;
z-index: 45;
width: 220px;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
}
.dark .session-panel {
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.4);
}
}
/* Menu Groups */
.menu-group-items { max-height: 0; overflow: hidden; transition: max-height 0.25s ease-out; }
.menu-group.open .menu-group-items { max-height: 500px; transition: max-height 0.35s ease-in; }
.menu-group.open .menu-group-items { max-height: 2000px; transition: max-height 0.35s ease-in; }
.menu-group .chevron { transition: transform 0.25s ease; }
.menu-group.open .chevron { transform: rotate(90deg); }
@@ -125,9 +472,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;
@@ -158,6 +504,27 @@
.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;
}
/* Streaming reasoning: render as plain pre to avoid expensive markdown
re-parsing on every chunk. Wrap long lines so the bubble width is
respected and use the same font size/color as the rendered version. */
.agent-thinking-step .thinking-stream-pre {
margin: 0;
padding: 0;
background: transparent;
border: 0;
font-family: inherit;
font-size: inherit;
line-height: 1.5;
color: inherit;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
/* Content step - real text output frozen before tool calls */
.agent-content-step {
@@ -556,15 +923,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;
}
@@ -601,13 +968,13 @@
font-size: 8px;
transition: transform 0.15s;
}
.knowledge-tree-group.open .chevron {
.knowledge-tree-group.open > .knowledge-tree-group-btn .chevron {
transform: rotate(90deg);
}
.knowledge-tree-group-items {
display: none;
}
.knowledge-tree-group.open .knowledge-tree-group-items {
.knowledge-tree-group.open > .knowledge-tree-group-items {
display: block;
}

File diff suppressed because it is too large Load Diff

View File

@@ -90,6 +90,12 @@ def _get_upload_dir() -> str:
return tmp_dir
def _generate_session_title(user_message: str, assistant_reply: str = "") -> str:
"""Delegate to the shared SessionService implementation."""
from agent.chat.session_service import generate_session_title
return generate_session_title(user_message, assistant_reply)
class WebMessage(ChatMessage):
def __init__(
self,
@@ -202,9 +208,24 @@ class WebChannel(ChatChannel):
# Fallback: polling mode
if session_id in self.session_queues:
content = reply.content if reply.content is not None else ""
# Skip file:// IMAGE_URL/FILE replies originating from an SSE-enabled
# request: they were already pushed via the `file_to_send` event during
# agent execution. By the time the chat_channel sends the IMAGE_URL reply,
# the SSE stream has typically closed (after the text "done") and the
# request_id is gone from sse_queues, so we'd otherwise duplicate the file
# as a polling bubble. Scheduler/push tasks have no on_event and must
# still go through polling normally.
if (
reply.type in (ReplyType.IMAGE_URL, ReplyType.FILE)
and content.startswith("file://")
and context.get("on_event") is not None
):
logger.debug(f"Polling skipped duplicate file reply for session {session_id}")
return
response_data = {
"type": str(reply.type),
"content": reply.content,
"content": content,
"timestamp": time.time(),
"request_id": request_id
}
@@ -219,6 +240,17 @@ class WebChannel(ChatChannel):
def _make_sse_callback(self, request_id: str):
"""Build an on_event callback that pushes agent stream events into the SSE queue."""
# Cap reasoning bytes pushed to the frontend per request to avoid
# browser stalls / crashes on very long chains-of-thought. Anything
# beyond the cap is dropped from the stream (DB still persists a
# truncated copy via _truncate_reasoning_for_storage).
# Keep aligned with frontend REASONING_RENDER_CAP and backend
# MAX_STORED_REASONING_CHARS.
MAX_REASONING_STREAM_CHARS = 4 * 1024 # 4 KB
# Use a single-element list as a mutable counter accessible from closure.
reasoning_chars_sent = [0]
reasoning_capped_notified = [False]
def on_event(event: dict):
if request_id not in self.sse_queues:
return
@@ -228,8 +260,21 @@ class WebChannel(ChatChannel):
if event_type == "reasoning_update":
delta = data.get("delta", "")
if delta:
q.put({"type": "reasoning", "content": delta})
if not delta:
return
remaining = MAX_REASONING_STREAM_CHARS - reasoning_chars_sent[0]
if remaining <= 0:
if not reasoning_capped_notified[0]:
reasoning_capped_notified[0] = True
q.put({
"type": "reasoning",
"content": "\n\n... [reasoning truncated for display] ...",
})
return
if len(delta) > remaining:
delta = delta[:remaining]
reasoning_chars_sent[0] += len(delta)
q.put({"type": "reasoning", "content": delta})
elif event_type == "message_update":
delta = data.get("delta", "")
@@ -263,6 +308,25 @@ class WebChannel(ChatChannel):
if tool_calls:
q.put({"type": "message_end", "has_tool_calls": True})
elif event_type == "agent_end":
# Safety net: if the agent finishes with an empty final_response,
# chat_channel skips _send_reply (because reply.content is empty),
# which means no "done" event is ever emitted and the SSE stream
# would hang until the 10-min idle timeout. Push a fallback "done"
# here so the frontend always gets closure.
final_response = data.get("final_response", "")
if not final_response or not str(final_response).strip():
logger.warning(
f"[WebChannel] agent_end with empty final_response for "
f"request {request_id}, sending fallback done"
)
q.put({
"type": "done",
"content": "(模型未返回任何内容,请重试或换一种方式描述你的需求)",
"request_id": request_id,
"timestamp": time.time(),
})
elif event_type == "file_to_send":
file_path = data.get("path", "")
file_name = data.get("file_name", os.path.basename(file_path))
@@ -519,6 +583,10 @@ class WebChannel(ChatChannel):
'/api/knowledge/read', 'KnowledgeReadHandler',
'/api/knowledge/graph', 'KnowledgeGraphHandler',
'/api/scheduler', 'SchedulerHandler',
'/api/sessions', 'SessionsHandler',
'/api/sessions/(.*)/generate_title', 'SessionTitleHandler',
'/api/sessions/(.*)/clear_context', 'SessionClearContextHandler',
'/api/sessions/(.*)', 'SessionDetailHandler',
'/api/history', 'HistoryHandler',
'/api/logs', 'LogsHandler',
'/api/version', 'VersionHandler',
@@ -688,21 +756,26 @@ class StreamHandler:
class ChatHandler:
def GET(self):
# 正常返回聊天页面
web.header('Cache-Control', 'no-cache, no-store, must-revalidate')
web.header('Pragma', 'no-cache')
file_path = os.path.join(os.path.dirname(__file__), 'chat.html')
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
html = f.read()
cache_bust = str(int(time.time()))
html = html.replace('assets/js/console.js', f'assets/js/console.js?v={cache_bust}')
html = html.replace('assets/css/console.css', f'assets/css/console.css?v={cache_bust}')
return html
class ConfigHandler:
_RECOMMENDED_MODELS = [
const.MINIMAX_M2_7, const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING,
const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7,
const.MINIMAX_M2_7_HIGHSPEED, const.MINIMAX_M2_7, const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING,
const.GLM_5_1, const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7,
const.QWEN36_PLUS, const.QWEN35_PLUS, const.QWEN3_MAX,
const.KIMI_K2_5, const.KIMI_K2,
const.KIMI_K2_6, const.KIMI_K2_5, const.KIMI_K2,
const.DOUBAO_SEED_2_PRO, const.DOUBAO_SEED_2_CODE,
const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS, const.CLAUDE_4_5_SONNET,
const.CLAUDE_4_6_SONNET, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_OPUS, const.CLAUDE_4_5_SONNET,
const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE,
const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o,
const.DEEPSEEK_CHAT, const.DEEPSEEK_REASONER,
@@ -714,14 +787,14 @@ 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",
"api_key_field": "zhipu_ai_api_key",
"api_base_key": "zhipu_ai_api_base",
"api_base_default": "https://open.bigmodel.cn/api/paas/v4",
"models": [const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7],
"models": [const.GLM_5_1, const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7],
}),
("dashscope", {
"label": "通义千问",
@@ -735,7 +808,7 @@ class ConfigHandler:
"api_key_field": "moonshot_api_key",
"api_base_key": "moonshot_base_url",
"api_base_default": "https://api.moonshot.cn/v1",
"models": [const.KIMI_K2_5, const.KIMI_K2],
"models": [const.KIMI_K2_6, const.KIMI_K2_5, const.KIMI_K2],
}),
("doubao", {
"label": "豆包",
@@ -749,7 +822,7 @@ class ConfigHandler:
"api_key_field": "claude_api_key",
"api_base_key": "claude_api_base",
"api_base_default": "https://api.anthropic.com/v1",
"models": [const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS, const.CLAUDE_4_5_SONNET],
"models": [const.CLAUDE_4_6_SONNET, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_OPUS, const.CLAUDE_4_5_SONNET],
}),
("gemini", {
"label": "Gemini",
@@ -786,17 +859,24 @@ class ConfigHandler:
"api_base_default": None,
"models": _RECOMMENDED_MODELS,
}),
("custom", {
"label": "自定义",
"api_key_field": "custom_api_key",
"api_base_key": "custom_api_base",
"api_base_default": "",
"models": [],
}),
])
EDITABLE_KEYS = {
"model", "bot_type", "use_linkai",
"open_ai_api_base", "deepseek_api_base", "claude_api_base", "gemini_api_base",
"zhipu_ai_api_base", "moonshot_base_url", "ark_base_url",
"zhipu_ai_api_base", "moonshot_base_url", "ark_base_url", "custom_api_base",
"open_ai_api_key", "deepseek_api_key", "claude_api_key", "gemini_api_key",
"zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key",
"ark_api_key", "minimax_api_key", "linkai_api_key",
"ark_api_key", "minimax_api_key", "linkai_api_key", "custom_api_key",
"agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps",
"web_password",
"enable_thinking", "web_password",
}
@staticmethod
@@ -849,6 +929,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", False)),
"api_bases": api_bases,
"api_keys": api_keys_masked,
"providers": providers,
@@ -874,7 +955,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
@@ -894,6 +975,19 @@ class ConfigHandler:
json.dump(file_cfg, f, indent=4, ensure_ascii=False)
logger.info(f"[WebChannel] Config updated: {list(applied.keys())}")
# Reset Bridge so that bot routing reflects the new config.
# Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot)
# even after the user switches bot_type / use_linkai / model in UI.
bridge_routing_keys = {"bot_type", "use_linkai", "model"}
if any(k in applied for k in bridge_routing_keys):
try:
from bridge.bridge import Bridge
Bridge().reset_bot()
logger.info("[WebChannel] Bridge bot routing reset due to config change")
except Exception as reset_err:
logger.warning(f"[WebChannel] Failed to reset bridge: {reset_err}")
return json.dumps({"status": "success", "applied": applied}, ensure_ascii=False)
except Exception as e:
logger.error(f"Error updating config: {e}")
@@ -1492,10 +1586,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}")
@@ -1508,12 +1605,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"})
@@ -1540,6 +1637,135 @@ class SchedulerHandler:
return json.dumps({"status": "error", "message": str(e)})
class SessionsHandler:
def GET(self):
_require_auth()
web.header('Content-Type', 'application/json; charset=utf-8')
try:
params = web.input(page='1', page_size='50')
from agent.memory import get_conversation_store
store = get_conversation_store()
result = store.list_sessions(
channel_type="web",
page=int(params.page),
page_size=int(params.page_size),
)
return json.dumps({"status": "success", **result}, ensure_ascii=False)
except Exception as e:
logger.error(f"[WebChannel] Sessions API error: {e}")
return json.dumps({"status": "error", "message": str(e)})
class SessionDetailHandler:
def DELETE(self, session_id: str):
_require_auth()
web.header('Content-Type', 'application/json; charset=utf-8')
logger.info(f"[WebChannel] DELETE session request: {session_id}")
try:
if not session_id:
return json.dumps({"status": "error", "message": "session_id required"})
from agent.memory import get_conversation_store
store = get_conversation_store()
store.clear_session(session_id)
# Also remove the Agent instance from AgentBridge if exists
try:
from bridge.bridge import Bridge
ab = Bridge().get_agent_bridge()
if session_id in ab.agents:
del ab.agents[session_id]
logger.info(f"[WebChannel] Removed agent instance for session {session_id}")
except Exception:
pass
channel = WebChannel()
channel.session_queues.pop(session_id, None)
logger.info(f"[WebChannel] Session deleted: {session_id}")
return json.dumps({"status": "success"})
except Exception as e:
logger.error(f"[WebChannel] Session delete error: {e}")
return json.dumps({"status": "error", "message": str(e)})
def PUT(self, session_id: str):
_require_auth()
web.header('Content-Type', 'application/json; charset=utf-8')
try:
if not session_id:
return json.dumps({"status": "error", "message": "session_id required"})
body = json.loads(web.data())
title = body.get("title", "").strip()
if not title:
return json.dumps({"status": "error", "message": "title required"})
from agent.memory import get_conversation_store
store = get_conversation_store()
found = store.rename_session(session_id, title)
if not found:
return json.dumps({"status": "error", "message": "session not found"})
return json.dumps({"status": "success"})
except Exception as e:
logger.error(f"[WebChannel] Session rename error: {e}")
return json.dumps({"status": "error", "message": str(e)})
class SessionTitleHandler:
def POST(self, session_id: str):
_require_auth()
web.header('Content-Type', 'application/json; charset=utf-8')
try:
if not session_id:
return json.dumps({"status": "error", "message": "session_id required"})
body = json.loads(web.data())
user_message = body.get("user_message", "")
assistant_reply = body.get("assistant_reply", "")
if not user_message:
return json.dumps({"status": "error", "message": "user_message required"})
title = _generate_session_title(user_message, assistant_reply)
from agent.memory import get_conversation_store
store = get_conversation_store()
updated = store.rename_session(session_id, title)
logger.info(f"[WebChannel] Session title set: sid={session_id}, title='{title}', db_updated={updated}")
return json.dumps({"status": "success", "title": title}, ensure_ascii=False)
except Exception as e:
logger.error(f"[WebChannel] Title generation error: {e}")
return json.dumps({"status": "error", "message": str(e)})
class SessionClearContextHandler:
def POST(self, session_id: str):
_require_auth()
web.header('Content-Type', 'application/json; charset=utf-8')
try:
if not session_id:
return json.dumps({"status": "error", "message": "session_id required"})
from agent.memory import get_conversation_store
store = get_conversation_store()
new_seq = store.clear_context(session_id)
# Delete the agent instance so a fresh one is created on the next message
try:
from bridge.bridge import Bridge
bridge = Bridge()
ab = bridge.get_agent_bridge()
if session_id in ab.agents:
del ab.agents[session_id]
logger.info(f"[WebChannel] Cleared agent instance for session {session_id}")
except Exception:
pass
return json.dumps({"status": "success", "context_start_seq": new_seq})
except Exception as e:
logger.error(f"[WebChannel] Clear context error: {e}")
return json.dumps({"status": "error", "message": str(e)})
class HistoryHandler:
def GET(self):
_require_auth()

View File

@@ -1 +1 @@
2.0.5
2.0.6

View File

@@ -644,32 +644,52 @@ def _list_local():
skills_dir = get_skills_dir()
builtin_dir = get_builtin_skills_dir()
# Merge builtin skills that are on disk but missing from config
_merge_builtin_into_config(config, builtin_dir, skills_dir)
if not config:
# Fallback: scan directories directly
entries = []
for d in [builtin_dir, skills_dir]:
if not os.path.isdir(d):
continue
source = "builtin" if d == builtin_dir else "custom"
for name in sorted(os.listdir(d)):
skill_path = os.path.join(d, name)
if os.path.isdir(skill_path) and not name.startswith("."):
has_skill_md = os.path.exists(os.path.join(skill_path, "SKILL.md"))
if has_skill_md:
entries.append({"name": name, "source": source, "enabled": True, "description": ""})
if not entries:
click.echo("No skills installed.")
return
_print_skill_table(entries)
click.echo("No skills installed.")
return
entries = sorted(config.values(), key=lambda x: x.get("name", ""))
if not entries:
click.echo("No skills installed.")
return
_print_skill_table(entries)
def _merge_builtin_into_config(config: dict, builtin_dir: str, skills_dir: str):
"""Scan builtin and custom dirs, add any new skills into config dict."""
dirty = False
for d, source in [(builtin_dir, "builtin"), (skills_dir, "custom")]:
if not os.path.isdir(d):
continue
for name in os.listdir(d):
if name.startswith(".") or name in ("skills_config.json",):
continue
skill_path = os.path.join(d, name)
if not os.path.isdir(skill_path):
continue
if not os.path.isfile(os.path.join(skill_path, "SKILL.md")):
continue
if name in config:
continue
desc = _read_skill_description(skill_path)
config[name] = {
"name": name,
"description": desc,
"source": source,
"enabled": True,
"category": "skill",
}
dirty = True
if dirty:
config_path = os.path.join(skills_dir, "skills_config.json")
try:
os.makedirs(skills_dir, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4, ensure_ascii=False)
except Exception:
pass
def _print_skill_table(entries):
"""Print skills as a formatted table."""
def _display_label(e):

View File

@@ -56,6 +56,7 @@ class CloudClient(LinkAIClient):
self._memory_service = None
self._knowledge_service = None
self._chat_service = None
self._session_service = None
@property
def skill_service(self):
@@ -118,6 +119,18 @@ class CloudClient(LinkAIClient):
logger.error(f"[CloudClient] Failed to init ChatService: {e}")
return self._chat_service
@property
def session_service(self):
"""Lazy-init SessionService."""
if self._session_service is None:
try:
from agent.chat.session_service import SessionService
self._session_service = SessionService()
logger.debug("[CloudClient] SessionService initialised")
except Exception as e:
logger.error(f"[CloudClient] Failed to init SessionService: {e}")
return self._session_service
# ------------------------------------------------------------------
# message push callback
# ------------------------------------------------------------------
@@ -546,12 +559,23 @@ class CloudClient(LinkAIClient):
# ------------------------------------------------------------------
# history callback
# ------------------------------------------------------------------
# Session-related actions handled via the HISTORY channel
_SESSION_ACTIONS = {
"list_sessions", "delete_session", "rename_session",
"clear_context", "generate_title",
}
def on_history(self, data: dict) -> dict:
"""
Handle HISTORY messages from the cloud console.
Returns paginated conversation history for a session.
:param data: message data with 'action' and 'payload' (session_id, page, page_size)
Supports both history query and session management actions
through a unified HISTORY message channel:
- query: paginated conversation history
- list_sessions / delete_session / rename_session /
clear_context / generate_title: session lifecycle
:param data: message data with 'action' and 'payload'
:return: response dict
"""
action = data.get("action", "query")
@@ -561,8 +585,19 @@ class CloudClient(LinkAIClient):
if action == "query":
return self._query_history(payload)
if action in self._SESSION_ACTIONS:
return self._dispatch_session(action, payload)
return {"action": action, "code": 404, "message": f"unknown action: {action}", "payload": None}
def _dispatch_session(self, action: str, payload: dict) -> dict:
"""Delegate session actions to SessionService."""
svc = self.session_service
if svc is None:
return {"action": action, "code": 500,
"message": "SessionService not available", "payload": None}
return svc.dispatch(action, payload)
def _query_history(self, payload: dict) -> dict:
"""Query paginated conversation history using ConversationStore."""
session_id = payload.get("session_id", "")

View File

@@ -14,6 +14,7 @@ ZHIPU_AI = "zhipu"
MOONSHOT = "moonshot"
MiniMax = "minimax"
DEEPSEEK = "deepseek"
CUSTOM = "custom" # custom OpenAI-compatible API, bot_type won't auto-switch on model change
MODELSCOPE = "modelscope"
# 模型列表
@@ -27,6 +28,7 @@ CLAUDE_35_SONNET = "claude-3-5-sonnet-latest" # 带 latest 标签的模型名
CLAUDE_35_SONNET_1022 = "claude-3-5-sonnet-20241022" # 带具体日期的模型名称,会固定为该日期发布的模型
CLAUDE_35_SONNET_0620 = "claude-3-5-sonnet-20240620"
CLAUDE_4_OPUS = "claude-opus-4-0"
CLAUDE_4_7_OPUS = "claude-opus-4-7" # Claude Opus 4.7
CLAUDE_4_6_OPUS = "claude-opus-4-6" # Claude Opus 4.6 - Agent推荐模型
CLAUDE_4_SONNET = "claude-sonnet-4-0" # Claude Sonnet 4.0
CLAUDE_4_5_SONNET = "claude-sonnet-4-5" # Claude Sonnet 4.5 - Agent推荐模型
@@ -101,7 +103,8 @@ MINIMAX_M2 = "MiniMax-M2" # MiniMax M2
MINIMAX_ABAB6_5 = "abab6.5-chat" # MiniMax abab6.5
# GLM (智谱AI)
GLM_5_TURBO = "glm-5-turbo" # 智谱 GLM-5-Turbo - Latest
GLM_5_1 = "glm-5.1" # 智谱 GLM-5.1 - Agent recommended model (default)
GLM_5_TURBO = "glm-5-turbo" # 智谱 GLM-5-Turbo
GLM_5 = "glm-5" # 智谱 GLM-5
GLM_4 = "glm-4"
GLM_4_PLUS = "glm-4-plus"
@@ -117,6 +120,7 @@ GLM_4_7 = "glm-4.7" # 智谱 GLM-4.7 - Agent推荐模型
MOONSHOT = "moonshot"
KIMI_K2 = "kimi-k2"
KIMI_K2_5 = "kimi-k2.5"
KIMI_K2_6 = "kimi-k2.6" # Kimi K2.6 - Agent recommended model (default)
# Doubao (Volcengine Ark)
DOUBAO = "doubao"
@@ -151,7 +155,7 @@ MODELSCOPE_MODEL_LIST = ["deepseek-ai/DeepSeek-R1-0528", "deepseek-ai/DeepSeek-R
MODEL_LIST = [
# Claude
CLAUDE3, CLAUDE_4_6_SONNET, CLAUDE_4_6_OPUS, CLAUDE_4_OPUS, CLAUDE_4_5_SONNET, CLAUDE_4_SONNET, CLAUDE_3_OPUS, CLAUDE_3_OPUS_0229,
CLAUDE3, CLAUDE_4_6_SONNET, CLAUDE_4_7_OPUS, CLAUDE_4_6_OPUS, CLAUDE_4_OPUS, CLAUDE_4_5_SONNET, CLAUDE_4_SONNET, CLAUDE_3_OPUS, CLAUDE_3_OPUS_0229,
CLAUDE_35_SONNET, CLAUDE_35_SONNET_1022, CLAUDE_35_SONNET_0620, CLAUDE_3_SONNET, CLAUDE_3_HAIKU,
"claude", "claude-3-haiku", "claude-3-sonnet", "claude-3-opus", "claude-3.5-sonnet",
@@ -179,12 +183,12 @@ MODEL_LIST = [
MiniMax, MINIMAX_M2_7, MINIMAX_M2_7_HIGHSPEED, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,
# GLM
ZHIPU_AI, GLM_5_TURBO, GLM_5, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS,
ZHIPU_AI, GLM_5_1, GLM_5_TURBO, GLM_5, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS,
GLM_4_0520, GLM_4_AIR, GLM_4_AIRX, GLM_4_7,
# Kimi
MOONSHOT, "moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k",
KIMI_K2, KIMI_K2_5,
KIMI_K2_6, KIMI_K2_5, KIMI_K2,
# Doubao
DOUBAO, DOUBAO_SEED_2_CODE, DOUBAO_SEED_2_PRO, DOUBAO_SEED_2_LITE, DOUBAO_SEED_2_MINI,

View File

@@ -17,10 +17,12 @@ available_setting = {
"open_ai_api_base": "https://api.openai.com/v1",
"claude_api_base": "https://api.anthropic.com/v1", # claude api base
"gemini_api_base": "https://generativelanguage.googleapis.com", # gemini api base
"custom_api_key": "", # custom OpenAI-compatible provider api key (used when bot_type is "custom")
"custom_api_base": "", # custom OpenAI-compatible provider api base (used when bot_type is "custom")
"proxy": "", # openai使用的代理
# chatgpt模型 当use_azure_chatgpt为true时其名称为Azure上model deployment名称
"model": "gpt-3.5-turbo", # 可选择: gpt-4o, pt-4o-mini, gpt-4-turbo, claude-3-sonnet, wenxin, moonshot, qwen-turbo, xunfei, glm-4, minimax, gemini等模型全部可选模型详见common/const.py文件
"bot_type": "", # 可选配置使用兼容openai格式的三方服务时候需填"openai"(历史值"chatGPT"仍兼容。bot具体名称详见common/const.py文件如不填根据model名称判断
"bot_type": "", # 可选配置使用兼容openai格式的三方服务时候需填"openai"或"custom"custom模式下切换模型不会自动切换bot_type。bot具体名称详见common/const.py文件如不填根据model名称判断
"use_azure_chatgpt": False, # 是否使用azure的chatgpt
"azure_deployment_id": "", # azure 模型部署名称
"azure_api_version": "", # azure api版本
@@ -202,7 +204,12 @@ available_setting = {
"agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens
"agent_max_context_turns": 20, # Agent模式下最大上下文记忆轮次
"agent_max_steps": 20, # Agent模式下单次运行最大决策步数
"enable_thinking": False, # Whether to enable deep thinking for web channel
"knowledge": True, # 是否开启知识库功能
# Per-skill runtime config. Nested keys are flattened to env vars at startup
# using the rule: skill[<name>][<key>] -> SKILL_<NAME>_<KEY>
# (e.g. skill["image-generation"].model -> SKILL_IMAGE_GENERATION_MODEL).
"skill": {},
}
@@ -381,6 +388,8 @@ def load_config():
"moonshot_api_base": "MOONSHOT_API_BASE",
"ark_api_key": "ARK_API_KEY",
"ark_api_base": "ARK_API_BASE",
"dashscope_api_key": "DASHSCOPE_API_KEY",
"dashscope_api_base": "DASHSCOPE_API_BASE",
# Channel credentials (used by skills that check env vars)
"feishu_app_id": "FEISHU_APP_ID",
"feishu_app_secret": "FEISHU_APP_SECRET",
@@ -401,12 +410,45 @@ def load_config():
if val:
os.environ[env_key] = str(val)
injected += 1
injected += _sync_skill_config_to_env(config.get("skill", {}))
if injected:
logger.info("[INIT] Synced {} config values to environment variables".format(injected))
config.load_user_datas()
def _sync_skill_config_to_env(skill_section) -> int:
"""Flatten skill-namespaced config into environment variables.
Mapping rule: ``config["skill"][<name>][<key>]`` -> ``SKILL_<NAME>_<KEY>``
(e.g. ``skill["image-generation"].model`` -> ``SKILL_IMAGE_GENERATION_MODEL``).
This lets subprocess-based skill scripts read their own settings without
importing project code. Existing env vars are NOT overwritten so the
real environment always wins.
Returns the number of variables actually injected.
"""
if not isinstance(skill_section, dict):
return 0
injected = 0
for skill_name, skill_conf in skill_section.items():
if not isinstance(skill_conf, dict):
continue
name_part = str(skill_name).replace("-", "_").upper()
for key, val in skill_conf.items():
if val is None or val == "":
continue
env_key = "SKILL_{}_{}".format(name_part, str(key).upper())
if env_key in os.environ:
continue
os.environ[env_key] = str(val)
injected += 1
return injected
def get_root():
return os.path.dirname(os.path.abspath(__file__))

View File

@@ -10,7 +10,9 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
```json
{
"channel_type": "web",
"web_port": 9899
"web_port": 9899,
"web_password": "",
"enable_thinking": false
}
```
@@ -18,6 +20,11 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
| --- | --- | --- |
| `channel_type` | 设为 `web` | `web` |
| `web_port` | Web 服务监听端口 | `9899` |
| `web_password` | 访问密码,留空表示不启用密码保护 | `""` |
| `web_session_expire_days` | 登录会话有效天数 | `30` |
| `enable_thinking` | 是否启用深度思考,开启后 Web 端展示推理过程,关闭可加速响应 | `false` |
配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。密码也支持在控制台的「配置」页面中在线修改。
## 访问地址
@@ -30,33 +37,24 @@ 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" />
#### 多会话管理
对话界面支持多会话Session管理所有会话记录持久化存储在数据库中
- **会话列表**:点击左侧历史会话图标可展开/收起会话列表面板,支持滚动加载全部历史会话
- **AI 生成标题**:新会话在首轮对话完成后,自动调用模型生成简短的会话摘要标题
- **新建会话**:点击会话列表顶部的「新对话」按钮或输入区的 `+` 按钮创建新会话
- **删除会话**:点击会话项的删除按钮,确认后永久删除该会话及其所有消息
- **清除上下文**:点击输入区的清除按钮,在当前会话中插入一条分隔线,分隔线以上的消息仍然展示但不再作为模型的上下文输入
### 模型管理
支持在线管理模型配置,无需手动编辑配置文件:

View File

@@ -69,6 +69,7 @@ Session: 12 messages | 8 skills loaded
| `agent_max_context_tokens` | 最大上下文 tokens | `40000` |
| `agent_max_context_turns` | 最大上下文记忆轮次 | `30` |
| `agent_max_steps` | 单次任务最大决策步数 | `15` |
| `enable_thinking` | 是否启用深度思考 | `true` / `false` |
<Note>
修改 `model` 时,系统会自动匹配对应的模型调用方式。配置会写入 `config.json` 并持久保存。
@@ -106,45 +107,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 版本号。

View File

@@ -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 | ✓ | ✗ |

View 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>

View File

@@ -82,7 +82,8 @@
"models/openai",
"models/deepseek",
"models/linkai",
"models/coding-plan"
"models/coding-plan",
"models/custom"
]
}
]
@@ -132,6 +133,14 @@
"skills/create",
"skills/hub"
]
},
{
"group": "内置技能",
"pages": [
"skills/skill-creator",
"skills/knowledge-wiki",
"skills/image-generation"
]
}
]
},
@@ -142,7 +151,8 @@
"group": "记忆系统",
"pages": [
"memory/index",
"memory/context"
"memory/context",
"memory/deep-dream"
]
}
]
@@ -185,6 +195,7 @@
"cli/index",
"cli/process",
"cli/skill",
"cli/memory-knowledge",
"cli/general"
]
}
@@ -197,6 +208,8 @@
"group": "发布记录",
"pages": [
"releases/overview",
"releases/v2.0.7",
"releases/v2.0.6",
"releases/v2.0.5",
"releases/v2.0.4",
"releases/v2.0.3",
@@ -254,7 +267,8 @@
"en/models/openai",
"en/models/deepseek",
"en/models/linkai",
"en/models/coding-plan"
"en/models/coding-plan",
"en/models/custom"
]
}
]
@@ -301,9 +315,16 @@
"pages": [
"en/skills/index",
"en/skills/install",
"en/skills/skill-creator",
"en/skills/hub"
]
},
{
"group": "Built-in Skills",
"pages": [
"en/skills/skill-creator",
"en/skills/knowledge-wiki",
"en/skills/image-generation"
]
}
]
},
@@ -314,7 +335,8 @@
"group": "Memory System",
"pages": [
"en/memory/index",
"en/memory/context"
"en/memory/context",
"en/memory/deep-dream"
]
}
]
@@ -357,6 +379,7 @@
"en/cli/index",
"en/cli/process",
"en/cli/skill",
"en/cli/memory-knowledge",
"en/cli/chat"
]
}
@@ -369,6 +392,8 @@
"group": "Release Notes",
"pages": [
"en/releases/overview",
"en/releases/v2.0.7",
"en/releases/v2.0.6",
"en/releases/v2.0.5",
"en/releases/v2.0.4",
"en/releases/v2.0.2",
@@ -426,7 +451,8 @@
"ja/models/openai",
"ja/models/deepseek",
"ja/models/linkai",
"ja/models/coding-plan"
"ja/models/coding-plan",
"ja/models/custom"
]
}
]
@@ -476,6 +502,14 @@
"ja/skills/create",
"ja/skills/hub"
]
},
{
"group": "内蔵スキル",
"pages": [
"ja/skills/skill-creator",
"ja/skills/knowledge-wiki",
"ja/skills/image-generation"
]
}
]
},
@@ -486,7 +520,8 @@
"group": "メモリシステム",
"pages": [
"ja/memory/index",
"ja/memory/context"
"ja/memory/context",
"ja/memory/deep-dream"
]
}
]
@@ -529,6 +564,7 @@
"ja/cli/index",
"ja/cli/process",
"ja/cli/skill",
"ja/cli/memory-knowledge",
"ja/cli/general"
]
}
@@ -541,6 +577,8 @@
"group": "リリースノート",
"pages": [
"ja/releases/overview",
"ja/releases/v2.0.7",
"ja/releases/v2.0.6",
"ja/releases/v2.0.5",
"ja/releases/v2.0.4",
"ja/releases/v2.0.3",

View File

@@ -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.
@@ -163,8 +165,8 @@ Supports mainstream model providers. Recommended models for Agent mode:
| Provider | Recommended Model |
| --- | --- |
| MiniMax | `MiniMax-M2.7` |
| GLM | `glm-5-turbo` |
| Kimi | `kimi-k2.5` |
| GLM | `glm-5.1` |
| Kimi | `kimi-k2.6` |
| Doubao | `doubao-seed-2-0-code-preview-260215` |
| Qwen | `qwen3.6-plus` |
| Claude | `claude-sonnet-4-6` |

View File

@@ -38,6 +38,16 @@ Supports streaming output with real-time display of the Agent's reasoning proces
<img width="850" src="https://cdn.link-ai.tech/doc/20260227180120.png" />
#### Multi-Session Management
The chat interface supports multi-session management. All session records are persistently stored in a SQLite database:
- **Session List**: Click the history icon on the left to expand/collapse the session list panel, with scroll-to-load support for all historical sessions
- **AI-Generated Titles**: After the first exchange in a new session, the model is automatically called to generate a short summary title
- **New Session**: Click the "New Chat" button at the top of the session list or the `+` button in the input area to create a new session
- **Delete Session**: Click the delete button on a session item and confirm to permanently delete the session and all its messages
- **Clear Context**: Click the clear button in the input area to insert a divider in the current session. Messages above the divider are still displayed but no longer included as context for the model
### Model Management
Manage model configurations online without manually editing config files:

View File

@@ -55,6 +55,7 @@ View or modify runtime configuration. Changes take effect immediately without re
| `agent_max_context_tokens` | Max context tokens | `40000` |
| `agent_max_context_turns` | Max context memory turns | `30` |
| `agent_max_steps` | Max decision steps per task | `15` |
| `enable_thinking` | Enable deep thinking | `true` / `false` |
<Note>
When changing `model`, the system automatically matches the corresponding model API. Configuration is persisted to `config.json`.
@@ -92,31 +93,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.

View File

@@ -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 | ✓ | ✗ |

View 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>

View File

@@ -9,7 +9,7 @@ CowAgent 2.0 has evolved from a simple chatbot into a super intelligent assistan
CowAgent's architecture consists of the following core modules:
<img src="https://cdn.link-ai.tech/doc/68ef7b212c6f791e0e74314b912149f9-sz_5847990.png" alt="CowAgent Architecture" />
<img src="https://cdn.link-ai.tech/doc/cow-agent-arch-en.jpg.jpg" alt="CowAgent Architecture" />
| Module | Description |
| --- | --- |

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View 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 |

View File

@@ -5,6 +5,8 @@ description: CowAgent long-term memory system — file persistence, automatic wr
Long-term memory is stored in workspace files, persisting across sessions. The Agent loads historical memory on demand via retrieval tools during conversation, and automatically writes conversation summaries to long-term memory when context is trimmed.
<img src="https://cdn.link-ai.tech/doc/memory-architecture-en.jpg" alt="Memory Architecture" />
## Memory Types
### Core Memory (MEMORY.md)
@@ -15,12 +17,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.
@@ -34,19 +41,25 @@ The memory system supports hybrid retrieval modes:
The Agent automatically triggers memory retrieval during conversation as needed, incorporating relevant historical information into context. Results are ranked by a combined score (default: 0.7 vector weight + 0.3 keyword weight). Daily memory scores decay over time (30-day half-life), while core memory does not decay.
## First Launch
## Related Files
On first launch, the Agent will proactively ask the user for key information and save it to the workspace (default `~/cow`):
Files related to memory in the workspace (default `~/cow`):
| File | Description |
| --- | --- |
| `system.md` | Agent system prompt and behavior settings |
| `user.md` | User identity information and preferences |
| `AGENT.md` | Agent personality and behavior settings |
| `USER.md` | User identity information and preferences |
| `RULE.md` | Custom rules and constraints |
| `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) |
## Web Console
The memory management page in the Web console allows browsing memory files and dream diaries, with tab switching support:
<Frame>
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
<img src="https://cdn.link-ai.tech/doc/20260414171014.png" width="800" />
</Frame>
## Configuration

View File

@@ -12,6 +12,6 @@ description: Claude model configuration
| Parameter | Description |
| --- | --- |
| `model` | Options include `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4-0`, `claude-3-5-sonnet-latest`, etc. See [official models](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `model` | Options include `claude-sonnet-4-6`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4-0`, `claude-3-5-sonnet-latest`, etc. See [official models](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `claude_api_key` | Create at [Claude Console](https://console.anthropic.com/settings/keys) |
| `claude_api_base` | Optional. Defaults to `https://api.anthropic.com/v1`. Change to use third-party proxy |

View File

@@ -102,18 +102,18 @@ Reference: [China Quick Start](https://docs.bigmodel.cn/cn/coding-plan/quick-sta
```json
{
"bot_type": "openai",
"bot_type": "moonshot",
"model": "kimi-for-coding",
"open_ai_api_base": "https://api.kimi.com/coding/v1",
"open_ai_api_key": "YOUR_API_KEY"
"moonshot_base_url": "https://api.kimi.com/coding/v1",
"moonshot_api_key": "YOUR_API_KEY"
}
```
| Parameter | Description |
| --- | --- |
| `model` | `kimi-for-coding` |
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
| `open_ai_api_key` | Coding Plan specific key (not shared with pay-as-you-go) |
| `model` | Use `kimi-for-coding` for auto-updating model, or specify a model such as `kimi-k2.6` |
| `moonshot_base_url` | `https://api.kimi.com/coding/v1` |
| `moonshot_api_key` | Coding Plan specific key (not shared with pay-as-you-go) |
Reference: [Key & Docs](https://www.kimi.com/code/docs/)

62
docs/en/models/custom.mdx Normal file
View File

@@ -0,0 +1,62 @@
---
title: Custom
description: Custom provider for third-party APIs and local models
---
For models accessed via OpenAI-compatible APIs, such as:
- **Third-party API proxies**: Use a unified API Base to call multiple models
- **Local models**: Models deployed locally via Ollama, vLLM, LocalAI, etc.
- **Private deployments**: Self-hosted model services within your organization
<Note>
Unlike the `openai` provider, switching models under the Custom provider will not auto-switch the provider type. Your custom API address is always preserved.
</Note>
## Configuration
### Third-party API Proxy
```json
{
"bot_type": "custom",
"model": "deepseek-chat",
"custom_api_key": "YOUR_API_KEY",
"custom_api_base": "https://{your-proxy.com}/v1"
}
```
| Parameter | Description |
| --- | --- |
| `bot_type` | Must be set to `custom` |
| `model` | Model name, any model supported by your proxy service |
| `custom_api_key` | API key provided by your proxy service |
| `custom_api_base` | API base URL, must be OpenAI-compatible |
### Local Models
Local models typically don't require an API key — just set the API base:
```json
{
"bot_type": "custom",
"model": "qwen3.5:27b",
"custom_api_base": "http://localhost:11434/v1"
}
```
Common local deployment tools and their default addresses:
| Tool | Default API Base |
| --- | --- |
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
| [LocalAI](https://localai.io) | `http://localhost:8080/v1` |
## Switching Models
Under the Custom provider, switching models only changes `model` without affecting `bot_type` or the API address:
```
/config model qwen3.5:27b
```

View File

@@ -5,14 +5,14 @@ description: Zhipu AI GLM model configuration
```json
{
"model": "glm-5-turbo",
"model": "glm-5.1",
"zhipu_ai_api_key": "YOUR_API_KEY"
}
```
| Parameter | Description |
| --- | --- |
| `model` | Options include `glm-5-turbo`, `glm-5`, `glm-4.7`, `glm-4-plus`, `glm-4-flash`, `glm-4-air`, etc. See [model codes](https://bigmodel.cn/dev/api/normal-model/glm-4) |
| `model` | Options include `glm-5.1`, `glm-5-turbo`, `glm-5`, `glm-4.7`, `glm-4-plus`, `glm-4-flash`, `glm-4-air`, etc. See [model codes](https://bigmodel.cn/dev/api/normal-model/glm-4) |
| `zhipu_ai_api_key` | Create at [Zhipu AI Console](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
OpenAI-compatible configuration is also supported:
@@ -20,7 +20,7 @@ OpenAI-compatible configuration is also supported:
```json
{
"bot_type": "openai",
"model": "glm-5-turbo",
"model": "glm-5.1",
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
"open_ai_api_key": "YOUR_API_KEY"
}

View File

@@ -6,7 +6,7 @@ description: Supported models and recommended choices for CowAgent
CowAgent supports mainstream LLMs from domestic and international providers. Model interfaces are implemented in the project's `models/` directory.
<Note>
For Agent mode, the following models are recommended based on quality and cost: MiniMax-M2.7, glm-5-turbo, kimi-k2.5, qwen3.6-plus, claude-sonnet-4-6, gemini-3.1-pro-preview
For Agent mode, the following models are recommended based on quality and cost: MiniMax-M2.7, glm-5.1, kimi-k2.6, qwen3.6-plus, claude-sonnet-4-6, gemini-3.1-pro-preview
</Note>
## Configuration
@@ -22,13 +22,13 @@ You can also use the [LinkAI](https://link-ai.tech) platform interface to flexib
MiniMax-M2.7 and other series models
</Card>
<Card title="GLM (Zhipu AI)" href="/en/models/glm">
glm-5-turbo, glm-5 and other series models
glm-5.1, glm-5-turbo, glm-5 and other series models
</Card>
<Card title="Qwen (Tongyi Qianwen)" href="/en/models/qwen">
qwen3.6-plus, qwen3-max and more
</Card>
<Card title="Kimi" href="/en/models/kimi">
kimi-k2.5, kimi-k2 and more
kimi-k2.6, kimi-k2.5, kimi-k2 and more
</Card>
<Card title="Doubao (ByteDance)" href="/en/models/doubao">
doubao-seed series models

View File

@@ -5,14 +5,14 @@ description: Kimi (Moonshot) model configuration
```json
{
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"moonshot_api_key": "YOUR_API_KEY"
}
```
| Parameter | Description |
| --- | --- |
| `model` | Options include `kimi-k2.5`, `kimi-k2`, `moonshot-v1-8k`, `moonshot-v1-32k`, `moonshot-v1-128k` |
| `model` | Options include `kimi-k2.6`, `kimi-k2.5`, `kimi-k2`, `moonshot-v1-8k`, `moonshot-v1-32k`, `moonshot-v1-128k` |
| `moonshot_api_key` | Create at [Moonshot Console](https://platform.moonshot.cn/console/api-keys) |
OpenAI-compatible configuration is also supported:
@@ -20,7 +20,7 @@ OpenAI-compatible configuration is also supported:
```json
{
"bot_type": "openai",
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"open_ai_api_base": "https://api.moonshot.cn/v1",
"open_ai_api_key": "YOUR_API_KEY"
}

View File

@@ -5,6 +5,8 @@ description: CowAgent version history
| Version | Date | Description |
| --- | --- | --- |
| [2.0.7](/en/releases/v2.0.7) | 2026.04.22 | Image Generation Skill (6-provider auto-routing), new models (Kimi K2.6, Claude Opus 4.7, GLM 5.1), knowledge base and Web Console improvements |
| [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 |

View 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)

View File

@@ -0,0 +1,65 @@
---
title: v2.0.7
description: CowAgent 2.0.7 - Image Generation Skill (6-provider auto-routing), new models, knowledge base enhancements, Web Console improvements and bug fixes
---
## 🎨 Image Generation Skill
New built-in `image-generation` skill supporting text-to-image, image-to-image, and multi-image fusion across six major providers:
- **6-provider auto-routing**: OpenAI (GPT-Image-2) → Gemini (Nano Banana) → Seedream (Volcengine Ark) → Qwen (DashScope) → MiniMax → LinkAI — automatically selects from configured providers in fixed priority order, with automatic fallback on failure
- **Zero model selection**: Just configure an API key and it works — no need to manually specify a model. You can also name a specific model in conversation (e.g. "draw a cat with seedream")
- **Flexible control**: Supports `quality`, `size` (512/1K4K), and `aspect_ratio` parameters, with each provider automatically mapping to its supported values
- **Image editing**: Pass existing images for editing, style transfer, or multi-image fusion (Seedream supports up to 14 reference images)
- **Skill-level config**: Pin a default model via `skill.image-generation.model` in `config.json`
- **Image lightbox**: All images in the Web console now support click-to-enlarge preview
Docs: [Image Generation Skill](https://docs.cowagent.ai/en/skills/image-generation)
## 🤖 New Model Support
- **Kimi K2.6**: Added `kimi-k2.6` model support
- **Claude Opus 4.7**: Added `claude-opus-4-7` model support
- **GLM 5.1**: Added `glm-5.1` model support
- **Kimi Coding Plan**: Support for Kimi Coding Plan mode
- **Custom model providers**: New custom model provider configuration for easier integration with additional vendors
## 💬 Web Console Improvements
- **Smart auto-scroll**: Improved chat scroll behaviour — no longer forces scroll to bottom while the user is reading earlier messages
- **Reasoning content cap**: Deep thinking content capped at 4 KB to prevent frontend lag
- **Mobile optimisation**: Session sidebar hidden by default on mobile, with overlay dismiss support
- **Session title fix**: Fixed title auto-generation fallback logic and Bridge reset on config change
- **Image preview dedup**: Fixed duplicate image rendering within the same message
## 📚 Knowledge Base Enhancements
- **Nested directory support**: Knowledge base listing and display now support multi-level nested directories
- **Root-level file display**: Show `index.md`, `log.md` and other root-level files in the knowledge tree
- **Empty state stats fix**: Root-level files no longer interfere with empty-state detection
## 🌙 Dream Memory Improvements
- **Structured organisation**: Dream memory files are now auto-archived by date with a cleaner directory structure
- **Schedule jitter**: Daily dream trigger includes random jitter to avoid concurrency conflicts in cluster deployments
## 🛠 Skill System Improvements
- **Skill manager refresh**: `/skill` commands now automatically refresh the skill manager to keep state in sync
- **Installation sources**: Skill installation supports multiple source formats (URL, zip, local file, etc.) with automatic target directory handling
## 🐛 Other Fixes
- **Gemini fix**: Fixed Gemini tool calls not returning results
- **Agent retry**: Empty-response retries no longer drop `tool_calls`
- **Docker env sync**: Fixed environment variables not syncing after config update in Docker environments
- **Python 3.7 compat**: Deferred `Literal` import for Python 3.7 compatibility
- **Model switch notification**: Fixed bot_type change notification not showing after model switch. Thanks @6vision
- **Config command**: `/config` now supports setting `enable_thinking`
- **Thinking display**: Deep thinking display disabled by default
## 📦 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.22 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.6...master)

View File

@@ -0,0 +1,158 @@
---
title: image-generation - Image Generation
description: Text-to-image / image-to-image / multi-image fusion with automatic multi-provider routing and fallback
---
A general-purpose image generation and editing skill supporting six providers: OpenAI, Gemini, Seedream (Volcengine Ark), Qwen (DashScope), MiniMax, and LinkAI. No need to choose a model manually — the script automatically selects a configured provider based on a fixed priority order.
## Model Selection
`image-generation` uses a "fixed priority + automatic fallback" strategy — just configure your keys and it works:
1. **Priority order**: `OpenAI → Gemini → Seedream → Qwen → MiniMax → LinkAI`
2. **Unconfigured providers are skipped**: only providers with an API key participate
3. **Automatic fallback on failure**: on errors like 401, model not enabled, or network issues, the next provider is tried
4. **Specified model goes first**: if a specific model name is provided, its provider is promoted to the front
### Supported Models
| Provider | Models / Aliases | Notes |
| --- | --- | --- |
| OpenAI | `gpt-image-2`, `gpt-image-1` | General-purpose, high quality, supports `quality` parameter |
| Gemini Nano Banana | `nano-banana-2`, `nano-banana-pro`, `nano-banana` | Corresponds to `gemini-3.1-flash`, `gemini-3-pro`, `gemini-2.5-flash` image variants |
| Seedream (Volcengine Ark) | `seedream-5.0-lite`, `seedream-4.5` | Native 2K4K, up to 14 reference images for fusion |
| Qwen (DashScope) | `qwen-image-2.0`, `qwen-image-2.0-pro` | Strong with Chinese text rendering and text-image layouts |
| MiniMax | `image-01` | Fast and simple image generation |
| LinkAI | Any model | Universal proxy, used as fallback |
<Note>
By default, the Agent does not pick a model — it uses automatic routing. If you want a specific model, just say so in the conversation, e.g. "use seedream to draw a cat" or "generate a poster with gpt-image-2". You can also pin a default model via the "Custom Configuration" section below.
</Note>
## Custom Configuration
### API Key Setup
You need **at least one** provider key. Configuring multiple providers enables automatic fallback. There are three ways to set up keys:
#### Option 1: Automatic Reuse of Existing Keys
If you have already configured model keys in the web console or `config.json` (e.g. `openai_api_key`, `gemini_api_key`, etc.), these keys are **automatically synced** to the corresponding environment variables at startup. In other words, if your chat model works, image generation can use the same key with zero extra configuration.
#### Option 2: Configure in config.json
Add the key fields directly to `config.json`:
```json
{
"openai_api_key": "sk-xxx",
"openai_api_base": "https://api.openai.com/v1",
"gemini_api_key": "AIza-xxx",
"ark_api_key": "xxx",
"dashscope_api_key": "sk-xxx",
"minimax_api_key": "xxx",
"linkai_api_key": "xxx"
}
```
A restart is required after changes. Each key also has a corresponding `*_api_base` field for custom endpoints.
#### Option 3: Configure via Conversation
Send an API key in the chat and the Agent will save it to `~/cow/.env` using the `env_config` tool — **no restart needed**. For example:
```
Set OPENAI_API_KEY to sk-xxx
```
Or:
```
Configure ARK_API_KEY as xxx
```
### API Key Reference
| Environment Variable | config.json Field | Provider | Default Base URL |
| --- | --- | --- | --- |
| `OPENAI_API_KEY` | `openai_api_key` | OpenAI | `https://api.openai.com/v1` |
| `GEMINI_API_KEY` | `gemini_api_key` | Gemini | `https://generativelanguage.googleapis.com` |
| `ARK_API_KEY` | `ark_api_key` | Volcengine Ark (Seedream) | `https://ark.cn-beijing.volces.com/api/v3` |
| `DASHSCOPE_API_KEY` | `dashscope_api_key` | Alibaba DashScope (Qwen) | `https://dashscope.aliyuncs.com` |
| `MINIMAX_API_KEY` | `minimax_api_key` | MiniMax | `https://api.minimaxi.com` |
| `LINKAI_API_KEY` | `linkai_api_key` | LinkAI | `https://api.link-ai.tech` |
### Pinning a Default Model
To force all image generation through a specific provider's model, add this to `config.json`:
```json
"skill": {
"image-generation": {
"model": "seedream-5.0-lite"
}
}
```
At startup, this is automatically converted to the environment variable `SKILL_IMAGE_GENERATION_MODEL`, and the script will always use this model's provider for generation.
## Enabling and Disabling
`image-generation` is a built-in skill that **automatically adjusts its status based on API keys**:
- **Key configured**: the skill is active — the Agent will invoke it when asked to draw
- **Key not configured**: the skill still appears in context (marked as "needs configuration") — the Agent will guide the user to set up a key rather than failing silently
To control it manually:
```text
/skill disable image-generation # Disable (won't be invoked even if keys are present)
/skill enable image-generation # Re-enable
```
In the terminal: `cow skill disable image-generation` / `cow skill enable image-generation`.
## Parameters
| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `prompt` | string | Yes | — | Image description |
| `image_url` | string / list | No | null | Input image(s) for editing — local path or URL. Pass multiple for multi-image fusion |
| `quality` | string | No | auto | `low` / `medium` / `high` — only some providers support this |
| `size` | string | No | auto | `512` / `1K` / `2K` / `3K` / `4K`, or pixel value like `1024x1024` |
| `aspect_ratio` | string | No | null | `1:1` / `3:2` / `2:3` / `16:9` / `9:16` / `21:9`; Gemini also supports `1:4` / `4:1` / `1:8` / `8:1` |
<Warning>
**Higher quality and larger size cost more and take longer.**
- For everyday conversations and quick previews, use the defaults (`auto`) or `quality=low` + `size=1K` — roughly 20 seconds
- For posters or when the user explicitly asks for high resolution, use `quality=high` + `size=2K/4K` — may take 15 minutes depending on the model
</Warning>
## Output
On success:
```json
{
"model": "doubao-seedream-5-0-260128",
"images": [
{"url": "/path/to/output.png"}
]
}
```
On failure: `{ "error": "..." }`. After an error, **do not retry directly** — it is almost always a configuration issue (wrong key, incorrect API base, model not enabled). Have the user fix the configuration first.
## Common Use Cases
- **Text-to-image**: generate illustrations, posters, icons, avatars, storyboards, etc. from a description
- **Image-to-image**: change styles, swap elements, add decorations or text on an existing image
- **Multi-image fusion**: combine multiple reference images into one (outfit swaps, character group photos, etc.)
<Note>
- Bash timeout should be set to 600 seconds. Each provider has a 300-second HTTP timeout, but the script may try multiple providers sequentially
- Input images are automatically compressed to ≤ 4 MB with the longest edge ≤ 4096 px
- Gemini / Seedream / Qwen / MiniMax do not support the `quality` parameter — passing it has no effect
- Seedream defaults to 2K; `seedream-5.0-lite` supports up to 3K; `seedream-4.5` supports up to 4K
</Note>

View File

@@ -0,0 +1,112 @@
---
title: knowledge-wiki - Knowledge Base
description: Maintain a local structured knowledge base with automatic archiving, categorisation, and cross-referencing
---
Organises notes, insights, and reference materials from your conversations into a structured local knowledge base, automatically maintaining an index and cross-references between pages.
`knowledge-wiki` maintains a `knowledge/` directory in your workspace — essentially the Agent's "second brain". The skill is marked `always: true`, so it is **always loaded** and requires no external dependencies.
## When It Triggers
- You share an article, document, or URL that you want to keep for future reference
- A conversation produces conclusions worth retaining long-term
- You want to look up something you accumulated earlier
## Directory Structure
```
knowledge/
├── index.md # Global index (must be maintained)
├── log.md # Operation log (append-only)
└── <category>/ # Category subdirectories (grouped by content)
└── <slug>.md # Knowledge page (lowercase-hyphenated filename)
```
## Three Core Operations
### 1. Ingest
When you share some material, the Agent will:
1. Read and understand the original content, extracting key information
2. Decide which category it belongs to — check `index.md` first; create a new category if none fits
3. Generate a knowledge page at `knowledge/<category>/<slug>.md`
4. Update the index `index.md` and the log `log.md`
### 2. Synthesise
When a conversation produces new conclusions or insights:
1. Create a new knowledge page under an appropriate category
2. Add cross-links to and from related existing pages
3. Update the index and log
### 3. Query
When you ask about previously accumulated knowledge:
1. Search `index.md` for potentially relevant pages
2. Open specific pages with the `read` tool
3. Supplement with `memory_search` if needed
4. Include links to knowledge pages in the answer so you can click through to the source
## Page Format
```markdown
# Page Title
> Source: <source URL or brief description>
Body content. Link between pages using relative paths:
[Related Page](../category/related-page.md)
## Key Points
- ...
## Related Pages
- [Page A](../category/page-a.md) — why it's related
```
<Note>
- `> Source:` records where this knowledge came from. Always include it when there is a clear source
- Cross-references are important: when creating or updating a page, remember to add back-links in the related pages too
- **Only link to pages that already exist.** If a concept deserves its own page, create it first, then add the link
</Note>
## Index Format
`knowledge/index.md` uses a flat list grouped by category, one knowledge page per line:
```markdown
# Knowledge Index
## Category A
- [Page Title](category-a/page-slug.md) — one-line summary
## Category B
- [Page Title](category-b/page-slug.md) — one-line summary
```
No tables, no emojis. Category names and organisation can be adjusted freely.
## Log Format
`knowledge/log.md` is append-only — newest entries go at the bottom:
```markdown
## [YYYY-MM-DD] ingest | Page Title
## [YYYY-MM-DD] synthesize | Page Title
```
## Writing Guidelines
- **Filenames**: lowercase with hyphens, e.g. `machine-learning.md`
- **One topic per page** — link related content across pages
- **Update, don't duplicate** — if a page already exists, update it rather than creating a new one
- **Always update the index** `knowledge/index.md` after any change
- **Distill, don't copy** — capture the key points, not the entire source
- **Use full paths when referencing knowledge pages in conversations**, e.g. `[Title](knowledge/<category>/<slug>.md)`. Use relative paths only for inter-page links
- **Include links when answering questions based on knowledge pages** so users can dig deeper

View File

@@ -0,0 +1,180 @@
---
title: skill-creator - Skill Creator
description: Create, install, and update skills — standardises SKILL.md format and directory structure
---
`skill-creator` is a "meta-skill" that helps the Agent create, install, and update other skills, ensuring every skill follows a consistent `SKILL.md` format and directory layout.
## When It Triggers
- The user wants to install a skill from a URL or remote repository
- The user wants to create a brand-new skill from scratch
- An existing skill needs upgrading or restructuring
## What Is a Skill?
A skill is a reusable instruction set plus optional scripts and assets. It injects domain expertise into the Agent so it can handle specific tasks like a specialist.
A skill typically contains:
1. **Specialised workflow** — step-by-step instructions for a category of tasks
2. **Tool usage** — how to call a particular API or process a particular file format
3. **Domain knowledge** — team conventions, business rules, data schemas, etc.
4. **Attached resources** — scripts, reference docs, templates, etc.
<Note>
**Core principle: less is more.** Only write what the Agent wouldn't figure out on its own. For every line you add, ask yourself: is it worth the tokens?
</Note>
## Directory Structure
```
skill-name/
├── SKILL.md # Required: skill definition
│ ├── YAML frontmatter (name / description are mandatory)
│ └── Markdown body (instructions + examples)
└── Optional resources
├── scripts/ # Executable scripts (Python / Bash, etc.)
├── references/ # Large reference docs the Agent reads on demand
└── assets/ # Templates, icons, etc. used directly in output
```
## SKILL.md Specification
Frontmatter fields in the SKILL.md header:
| Field | Description |
| --- | --- |
| `name` | Skill name — lowercase with hyphens, must match the directory name |
| `description` | **The most important field.** Clearly state what the skill does and when to use it. The Agent reads this to decide whether to invoke it. All trigger-related descriptions go here, not in the body |
| `metadata.cowagent.requires.bins` | System CLI tools that must be installed |
| `metadata.cowagent.requires.env` | Required environment variables (all must be present) |
| `metadata.cowagent.requires.anyEnv` | Multiple API keys — at least one must be set |
| `metadata.cowagent.requires.anyBins` | Multiple tools — at least one must be installed |
| `metadata.cowagent.always` | Set to `true` to always load, skipping dependency checks |
| `metadata.cowagent.emoji` | Display emoji (optional) |
| `metadata.cowagent.os` | OS restriction, e.g. `["darwin", "linux"]` |
<Note>
The `category` field does not need to be set manually — the system automatically sets it to `skill`.
</Note>
Two ways to declare API key dependencies:
```yaml
metadata:
cowagent:
requires:
env: ["MYAPI_KEY"] # Must be present
```
```yaml
metadata:
cowagent:
requires:
anyEnv: ["OPENAI_API_KEY", "LINKAI_API_KEY"] # At least one
```
**Skills are auto-enabled/disabled based on dependencies**: they activate when all required environment variables are present and deactivate when any are missing — no need for manual `/skill enable`.
## Resource Directories
| Directory | What goes here | What does NOT go here |
| --- | --- | --- |
| `scripts/` | Code that needs to run repeatedly, or scripts that produce deterministic results | Demo-only code snippets |
| `references/` | Documents **over 500 lines** that genuinely won't fit in SKILL.md (e.g. a full DB schema) | General API docs, tutorials, examples |
| `assets/` | Files that appear in the final output (templates, icons, boilerplate, etc.) | Explanatory documentation |
<Warning>
**In principle, everything goes in `SKILL.md`** — only split into resource directories when it truly won't fit.
Do not add `README.md`, `CHANGELOG.md`, or `INSTALLATION_GUIDE.md` to a skill — put everything in `SKILL.md`. Resource directories should only contain scripts that actually run or assets that are actually used.
</Warning>
## Installing External Skills
After installation, the skill lands in `<workspace>/skills/<name>/`.
| Source | How to install |
| --- | --- |
| URL (single file) | curl / web_fetch |
| URL (zip archive) | Download and extract |
| Local SKILL.md | Read directly |
| Local zip archive | Extract |
Installation steps:
1. Locate the `SKILL.md` (may be at the root or in a subdirectory of the archive)
2. Read the `name` from the frontmatter
3. Copy the **entire skill directory** (including `SKILL.md`, `scripts/`, `assets/`, etc.) to `<workspace>/skills/<name>/`
4. If the archive contains an `INSTALL.md` or similar setup script, run it — but the final result must still reside under `<workspace>/skills/<name>/`
## Creating a Skill from Scratch
Recommended order:
1. **Clarify requirements** — ask the user for a few concrete use cases (don't ask too many at once)
2. **Plan the structure** — does this skill need scripts? Reference docs? Template assets?
3. **Scaffold** — use the init script:
```bash
scripts/init_skill.py <skill-name> --path <workspace>/skills [--resources scripts,references,assets] [--examples]
```
4. **Fill in content** — write SKILL.md, add scripts and resources. Always test scripts after writing them
5. **Validate** (optional):
```bash
scripts/quick_validate.py <workspace>/skills/<skill-name>
```
6. **Iterate** — keep improving based on real-world usage feedback
## Naming Conventions
- Use only lowercase letters, digits, and hyphens. Normalise user-given names, e.g. `Plan Mode` → `plan-mode`
- Maximum 64 characters
- Keep it short, start with a verb, make it self-explanatory
- Use tool names as prefixes when appropriate, e.g. `gh-address-comments`, `linear-address-issue`
- The directory name and the `name` field must match exactly
## Three-Level Loading
Skills are not loaded into context all at once — they use a three-level progressive loading mechanism:
1. **Metadata** (`name` + `description`) — always in context (~100 words). The Agent uses this to decide whether to invoke the skill
2. **SKILL.md body** — loaded only when the skill is activated; keep it under 500 lines
3. **Resource files** — read on demand by the Agent
For skills with multiple variants (e.g. multi-cloud deployment), organise like this:
```
cloud-deploy/
├── SKILL.md # Main workflow and provider selection logic
└── references/
├── aws.md
├── gcp.md
└── azure.md
```
When the user picks AWS, the Agent only reads `aws.md` — no need to load all three providers.
## Common Design Patterns
**Step-by-step**: numbered steps with corresponding scripts.
```markdown
1. Analyse form structure (run analyze_form.py)
2. Generate field mappings (edit fields.json)
3. Auto-fill the form (run fill_form.py)
```
**Branching**: different flows based on user intent.
```markdown
1. Determine operation type:
**Creating new content?** → follow the "Create" workflow
**Editing existing content?** → follow the "Edit" workflow
```
**Template-based**: when output format has strict requirements, include a template in SKILL.md for the Agent to follow.

View File

@@ -27,7 +27,7 @@ If the current provider fails, the tool automatically tries the next one until i
| Claude | Main model | Anthropic native image format |
| Gemini | Main model | inlineData format |
| Doubao | Main model | doubao-seed-2-0 series natively supported |
| Kimi (Moonshot) | Main model | kimi-k2.5 natively supported |
| Kimi (Moonshot) | Main model | kimi-k2.6, kimi-k2.5 natively supported |
| ZhipuAI | glm-5v-turbo | Always uses dedicated vision model |
| MiniMax | MiniMax-Text-01 | Always uses dedicated vision model |

View File

@@ -9,7 +9,7 @@ CowAgent 2.0 从简单的聊天机器人全面升级为超级智能助理,采
CowAgent 的整体架构由以下核心模块组成:
<img src="https://cdn.link-ai.tech/doc/68ef7b212c6f791e0e74314b912149f9-sz_5847990.png" alt="CowAgent Architecture" />
<img src="https://cdn.link-ai.tech/doc/cow-agent-arch-zh.jpg" alt="CowAgent Architecture" />
| 模块 | 说明 |
| --- | --- |
@@ -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": false
}
```
@@ -80,4 +81,5 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词
| `agent_max_context_tokens` | 最大上下文 token 数 | `50000` |
| `agent_max_context_turns` | 最大上下文记忆轮次 | `20` |
| `agent_max_steps` | 单次任务最大决策步数 | `20` |
| `enable_thinking` | 是否启用深度思考,开启后 Web 端展示推理过程,关闭可加速响应 | `false` |
| `knowledge` | 是否启用个人知识库 | `true` |

View File

@@ -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 定时任务

View File

@@ -25,7 +25,7 @@ CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、
能够理解复杂任务并自主规划执行,持续思考和调用各类工具和技能直到完成目标。
</Card>
<Card title="长期记忆" icon="database" href="/memory">
自动将对话记忆持久化至本地文件和数据库中,包括全局记忆和天级记忆,支持关键词及向量检索。
三层记忆流转(上下文→天级记忆→全局记忆),每日梦境蒸馏整理,支持关键词及向量检索。
</Card>
<Card title="个人知识库" icon="book" href="/knowledge">
自动整理结构化知识,支持知识图谱可视化,通过交叉引用构建持续增长的知识网络。

View File

@@ -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など新モデル追加。
@@ -163,8 +165,8 @@ sudo docker logs -f chatgpt-on-wechat
| プロバイダー | 推奨モデル |
| --- | --- |
| MiniMax | `MiniMax-M2.7` |
| GLM | `glm-5-turbo` |
| Kimi | `kimi-k2.5` |
| GLM | `glm-5.1` |
| Kimi | `kimi-k2.6` |
| Doubao | `doubao-seed-2-0-code-preview-260215` |
| Qwen | `qwen3.6-plus` |
| Claude | `claude-sonnet-4-6` |

View File

@@ -38,6 +38,16 @@ Web コンソールは CowAgent のデフォルトチャネルです。起動後
<img width="850" src="https://cdn.link-ai.tech/doc/20260227180120.png" />
#### マルチセッション管理
チャット画面はマルチセッション管理に対応しています。すべてのセッション記録は SQLite データベースに永続的に保存されます:
- **セッション一覧**:左側の履歴アイコンをクリックしてセッション一覧パネルを展開/折りたたみでき、スクロールですべての履歴セッションを読み込めます
- **AI によるタイトル生成**:新しいセッションの最初のやり取りが完了すると、自動的にモデルを呼び出して短い要約タイトルを生成します
- **新規セッション**:セッション一覧上部の「新しい会話」ボタン、または入力エリアの `+` ボタンをクリックして新しいセッションを作成します
- **セッション削除**:セッション項目の削除ボタンをクリックし、確認後にそのセッションとすべてのメッセージを完全に削除します
- **コンテキストクリア**:入力エリアのクリアボタンをクリックすると、現在のセッションに区切り線が挿入されます。区切り線より上のメッセージは表示されたままですが、モデルのコンテキストには含まれなくなります
### モデル管理
設定ファイルを手動で編集せずに、オンラインでモデル設定を管理できます:

View File

@@ -55,6 +55,7 @@ description: ステータスの確認、設定管理、コンテキスト制御
| `agent_max_context_tokens` | 最大コンテキストトークン数 | `40000` |
| `agent_max_context_turns` | 最大コンテキスト記憶ターン数 | `30` |
| `agent_max_steps` | タスクごとの最大判断ステップ数 | `15` |
| `enable_thinking` | ディープシンキングの有効化 | `true` / `false` |
<Note>
`model` を変更すると、システムが対応するモデル API を自動的にマッチングします。設定は `config.json` に永続的に保存されます。
@@ -92,31 +93,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 のバージョンを表示します。

View File

@@ -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 | ✓ | ✗ |

View 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>

View File

@@ -9,7 +9,7 @@ CowAgent 2.0 は、シンプルなチャットボットから、自律的な思
CowAgent のアーキテクチャは以下のコアモジュールで構成されています:
<img src="https://cdn.link-ai.tech/doc/68ef7b212c6f791e0e74314b912149f9-sz_5847990.png" alt="CowAgent Architecture" />
<img src="https://cdn.link-ai.tech/doc/cow-agent-arch-en.jpg.jpg" alt="CowAgent Architecture" />
| モジュール | 説明 |
| --- | --- |

View File

@@ -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 スケジュールタスク

View File

@@ -20,7 +20,7 @@ CowAgent は自ら思考しタスクを計画し、コンピュータや外部
複雑なタスクを理解し、自律的に実行計画を立て、目標が達成されるまで思考とツール呼び出しを続けます。ツールを通じてファイルシステム、ターミナル、ブラウザ、スケジューラなどのシステムリソースにアクセスできます。
</Card>
<Card title="長期記憶" icon="database" href="/ja/memory">
会話の記憶ローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索とベクトル検索に対応しています
三層記憶ロー(コンテキスト→デイリーメモリ→グローバルメモリ)、毎日 Deep Dream 蒸留で整理、キーワード検索とベクトル検索に対応。
</Card>
<Card title="ナレッジベース" icon="book" href="/ja/knowledge">
構造化された知識を自動整理し、ナレッジグラフの可視化をサポート。相互参照により継続的に成長するナレッジネットワークを構築します。

View File

@@ -39,14 +39,15 @@ description: 会話コンテキスト — メッセージ管理、圧縮戦略
- **最も古い半分** の完全なターンがトリミングされます(ツール呼び出しチェーンの完全性を保証)
- トリミングされたメッセージは LLM によって要約され、**日次記憶ファイルに書き込まれます**
- 残りのターンはそのまま保持されます
- LLM 要約が完了すると、保持されたコンテキストの最初のユーザーメッセージの先頭に要約が**注入**され、モデルが会話の文脈を維持できるようにします
- 要約注入はバックグラウンドで非同期に実行され、次のターンから有効になります
### 3. トークン予算のトリミング
ターンのトリミング後、トークン数がまだ予算を超えている場合:
- **5 ターン未満の場合**:すべてのターンで**テキスト圧縮**を実行 — 各ターンは最初のユーザーテキストと最後の Agent 返信のみを保持し、中間のツール呼び出しチェーンを削除
- **5 ターン以上の場合****前半のターン**を再度トリミングし、破棄されたコンテンツも記憶に書き込まれます
- **5 ターン以上の場合****前半のターン**を再度トリミングし、破棄されたコンテンツも記憶に書き込まれ、コンテキスト要約も注入されます
### 4. オーバーフロー緊急処理

View 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>
## 安全メカニズム
| メカニズム | 説明 |
| --- | --- |
| **コンテンツなしでスキップ** | 日次記憶がない場合、蒸留をスキップし空の上書きを回避 |
| **入力重複排除** | スケジュールタスクで、入力材料が変更されていない場合自動スキップ |
| **非同期実行** | 蒸留はバックグラウンドスレッドで実行、会話をブロックしない |
| **順序保証** | スケジュールタスクで、日次フラッシュ完了後に蒸留を開始 |
| **捏造禁止** | プロンプトで既存の材料のみに基づく整理を明示的に制約 |

View File

@@ -5,6 +5,8 @@ description: CowAgent の長期記憶システム — ファイル永続化、
長期記憶はワークスペースのファイルに保存され、セッション間で永続化されます。Agent は会話中に検索ツールを通じて過去の記憶をオンデマンドで読み込み、コンテキストのトリミング時に会話の要約を自動的に長期記憶に書き込みます。
<img src="https://cdn.link-ai.tech/doc/memory-architecture-en.jpg" alt="Memory Architecture" />
## 記憶の種類
### コア記憶MEMORY.md
@@ -15,29 +17,40 @@ 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 の要約 + ファイル書き込み)、通常の会話応答をブロックしません。
## 初回起動
## 関連ファイル
初回起動時に、Agent はユーザーに主要な情報を積極的に尋ね、ワークスペース(デフォルト `~/cow`に保存します
ワークスペース(デフォルト `~/cow`内の記憶関連ファイル
| ファイル | 説明 |
| --- | --- |
| `system.md` | Agent のシステムプロンプトと動作設定 |
| `user.md` | ユーザーの身元情報と好み |
| `AGENT.md` | Agent のパーソナリティと動作設定 |
| `USER.md` | ユーザーの身元情報と好み |
| `RULE.md` | カスタムルールと制約 |
| `MEMORY.md` | コア記憶(長期) |
| `memory/YYYY-MM-DD.md` | 日次記憶(オンデマンドで作成) |
| `memory/dreams/YYYY-MM-DD.md` | 夢日記Deep Dream で自動生成) |
## Web コンソール
Web コンソールの記憶管理ページで、記憶ファイルと夢日記を閲覧できます。タブ切り替えに対応:
<Frame>
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
<img src="https://cdn.link-ai.tech/doc/20260414171014.png" width="800" />
</Frame>
## 設定

View File

@@ -12,6 +12,6 @@ description: Claudeモデルの設定
| パラメータ | 説明 |
| --- | --- |
| `model` | `claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest`などから選択可能。[公式モデル一覧](https://docs.anthropic.com/en/docs/about-claude/models/overview)を参照 |
| `model` | `claude-sonnet-4-6`、`claude-opus-4-7`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest`などから選択可能。[公式モデル一覧](https://docs.anthropic.com/en/docs/about-claude/models/overview)を参照 |
| `claude_api_key` | [Claude Console](https://console.anthropic.com/settings/keys)で作成 |
| `claude_api_base` | 任意。デフォルトは`https://api.anthropic.com/v1`。サードパーティプロキシを使用する場合に変更 |

View File

@@ -102,18 +102,18 @@ description: Coding Planモデルの設定
```json
{
"bot_type": "openai",
"bot_type": "moonshot",
"model": "kimi-for-coding",
"open_ai_api_base": "https://api.kimi.com/coding/v1",
"open_ai_api_key": "YOUR_API_KEY"
"moonshot_base_url": "https://api.kimi.com/coding/v1",
"moonshot_api_key": "YOUR_API_KEY"
}
```
| パラメータ | 説明 |
| --- | --- |
| `model` | `kimi-for-coding` |
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
| `open_ai_api_key` | Coding Plan専用キー従量課金とは共有不可 |
| `model` | `kimi-for-coding`で自動更新モデル、または`kimi-k2.6`などのモデルを指定 |
| `moonshot_base_url` | `https://api.kimi.com/coding/v1` |
| `moonshot_api_key` | Coding Plan専用キー従量課金とは共有不可 |
参考: [キー & ドキュメント](https://www.kimi.com/code/docs/)

62
docs/ja/models/custom.mdx Normal file
View File

@@ -0,0 +1,62 @@
---
title: カスタム
description: サードパーティAPIやローカルモデル向けのカスタムプロバイダー設定
---
OpenAI互換プロトコルでアクセスするモデルサービスに適用します
- **サードパーティAPIプロキシ**統一APIベースで複数モデルを呼び出し
- **ローカルモデル**Ollama、vLLM、LocalAIなどでローカルにデプロイされたモデル
- **プライベートデプロイ**:組織内でホストされたモデルサービス
<Note>
`openai` プロバイダーとの違い:カスタムプロバイダーでは `/config model` でモデルを切り替えてもプロバイダータイプは自動切り替えされず、カスタムAPIアドレスが常に保持されます。
</Note>
## 設定方法
### サードパーティAPIプロキシ
```json
{
"bot_type": "custom",
"model": "deepseek-chat",
"custom_api_key": "YOUR_API_KEY",
"custom_api_base": "https://{your-proxy.com}/v1"
}
```
| パラメータ | 説明 |
| --- | --- |
| `bot_type` | `custom` に設定必須 |
| `model` | モデル名、プロキシサービスがサポートする任意のモデル名 |
| `custom_api_key` | プロキシサービスが提供するAPIキー |
| `custom_api_base` | APIアドレス、OpenAI互換プロトコルが必要 |
### ローカルモデル
ローカルモデルは通常APIキー不要で、APIベースのみ設定します
```json
{
"bot_type": "custom",
"model": "qwen3.5:27b",
"custom_api_base": "http://localhost:11434/v1"
}
```
一般的なローカルデプロイツールとデフォルトアドレス:
| ツール | デフォルトAPIベース |
| --- | --- |
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
| [LocalAI](https://localai.io) | `http://localhost:8080/v1` |
## モデル切り替え
カスタムプロバイダーではモデル切り替え時に `model` のみ変更され、`bot_type` やAPIアドレスは変わりません
```
/config model qwen3.5:27b
```

View File

@@ -5,14 +5,14 @@ description: 智谱AI GLMモデルの設定
```json
{
"model": "glm-5-turbo",
"model": "glm-5.1",
"zhipu_ai_api_key": "YOUR_API_KEY"
}
```
| パラメータ | 説明 |
| --- | --- |
| `model` | `glm-5-turbo`、`glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air`などから選択可能。[モデルコード](https://bigmodel.cn/dev/api/normal-model/glm-4)を参照 |
| `model` | `glm-5.1`、`glm-5-turbo`、`glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air`などから選択可能。[モデルコード](https://bigmodel.cn/dev/api/normal-model/glm-4)を参照 |
| `zhipu_ai_api_key` | [智谱AI Console](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys)で作成 |
OpenAI互換の設定もサポートしています:
@@ -20,7 +20,7 @@ OpenAI互換の設定もサポートしています:
```json
{
"bot_type": "openai",
"model": "glm-5-turbo",
"model": "glm-5.1",
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
"open_ai_api_key": "YOUR_API_KEY"
}

View File

@@ -6,7 +6,7 @@ description: CowAgentがサポートするモデルとおすすめの選択肢
CowAgentは国内外の主要なLLMをサポートしています。モデルインターフェースはプロジェクトの`models/`ディレクトリに実装されています。
<Note>
Agent モードでは、品質とコストのバランスから以下のモデルをおすすめします: MiniMax-M2.7、glm-5-turbo、kimi-k2.5、qwen3.6-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
Agent モードでは、品質とコストのバランスから以下のモデルをおすすめします: MiniMax-M2.7、glm-5.1、kimi-k2.6、qwen3.6-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
</Note>
## 設定
@@ -22,13 +22,13 @@ CowAgentは国内外の主要なLLMをサポートしています。モデルイ
MiniMax-M2.7およびその他のシリーズモデル
</Card>
<Card title="GLM (智谱AI)" href="/ja/models/glm">
glm-5-turbo、glm-5およびその他のシリーズモデル
glm-5.1、glm-5-turbo、glm-5およびその他のシリーズモデル
</Card>
<Card title="Qwen (通义千问)" href="/ja/models/qwen">
qwen3.6-plus、qwen3-maxなど
</Card>
<Card title="Kimi" href="/ja/models/kimi">
kimi-k2.5、kimi-k2など
kimi-k2.6、kimi-k2.5、kimi-k2など
</Card>
<Card title="Doubao (ByteDance)" href="/ja/models/doubao">
doubao-seedシリーズモデル

View File

@@ -5,14 +5,14 @@ description: Kimi (Moonshot) モデルの設定
```json
{
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"moonshot_api_key": "YOUR_API_KEY"
}
```
| パラメータ | 説明 |
| --- | --- |
| `model` | `kimi-k2.5`、`kimi-k2`、`moonshot-v1-8k`、`moonshot-v1-32k`、`moonshot-v1-128k`から選択可能 |
| `model` | `kimi-k2.6`、`kimi-k2.5`、`kimi-k2`、`moonshot-v1-8k`、`moonshot-v1-32k`、`moonshot-v1-128k`から選択可能 |
| `moonshot_api_key` | [Moonshot Console](https://platform.moonshot.cn/console/api-keys)で作成 |
OpenAI互換の設定もサポートしています:
@@ -20,7 +20,7 @@ OpenAI互換の設定もサポートしています:
```json
{
"bot_type": "openai",
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"open_ai_api_base": "https://api.moonshot.cn/v1",
"open_ai_api_key": "YOUR_API_KEY"
}

View File

@@ -5,6 +5,8 @@ description: CowAgent バージョン履歴
| バージョン | 日付 | 説明 |
| --- | --- | --- |
| [2.0.7](/ja/releases/v2.0.7) | 2026.04.22 | 画像生成スキル6プロバイダー自動ルーティング、新モデルKimi K2.6、Claude Opus 4.7、GLM 5.1)、ナレッジベースと Web コンソールの改善 |
| [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 アップグレード、マルチチャネル同時実行、セッション永続化 |

View 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)

View File

@@ -0,0 +1,65 @@
---
title: v2.0.7
description: CowAgent 2.0.7 - 画像生成スキル6プロバイダー自動ルーティング、新モデルサポート、ナレッジベース強化、Web コンソール改善およびバグ修正
---
## 🎨 画像生成スキル
新しい内蔵スキル `image-generation` を追加。テキストから画像生成、画像編集、複数画像の融合に対応し、6 社の主要プロバイダーをカバー:
- **6 プロバイダー自動ルーティング**OpenAI (GPT-Image-2) → Gemini (Nano Banana) → Seedream (Volcengine Ark) → Qwen (DashScope) → MiniMax → LinkAI — 固定の優先順位で設定済みプロバイダーを自動選択、失敗時は次のプロバイダーへ自動フォールバック
- **モデル選択不要**API Key を設定するだけで使用可能、モデルを手動で指定する必要なし。会話で特定モデルを指名することも可能「seedream で猫を描いて」)
- **柔軟な制御**`quality`(画質)、`size`解像度、512/1K〜4K、`aspect_ratio`(アスペクト比)パラメータ対応、各プロバイダーが自動的に有効な値にマッピング
- **画像編集**既存の画像を渡して編集・スタイル変換・複数画像融合が可能Seedream は最大 14 枚の参照画像をサポート)
- **スキルレベル設定**`config.json` の `skill.image-generation.model` でデフォルトモデルを固定可能
- **画像ライトボックス**Web コンソールのすべての画像がクリックで拡大プレビュー対応
ドキュメント:[画像生成スキル](https://docs.cowagent.ai/ja/skills/image-generation)
## 🤖 新モデルサポート
- **Kimi K2.6**`kimi-k2.6` モデルサポートを追加
- **Claude Opus 4.7**`claude-opus-4-7` モデルサポートを追加
- **GLM 5.1**`glm-5.1` モデルサポートを追加
- **Kimi Coding Plan**Kimi Coding Plan モードをサポート
- **カスタムモデルプロバイダー**:新しいカスタムモデルプロバイダー設定により、追加ベンダーとの統合が容易に
## 💬 Web コンソール改善
- **スマート自動スクロール**:チャットスクロールの動作を改善 — ユーザーが過去のメッセージを閲覧中に強制的に最下部にスクロールしなくなりました
- **推論コンテンツ制限**:深い思考コンテンツを 4KB に制限し、フロントエンドのラグを防止
- **モバイル最適化**:セッションサイドバーをモバイルではデフォルトで非表示、オーバーレイタップで閉じることが可能
- **セッションタイトル修正**:タイトル自動生成のフォールバックロジックと設定変更時の Bridge リセットを修正
- **画像プレビュー重複排除**:同一メッセージ内での画像の重複レンダリングを修正
## 📚 ナレッジベース強化
- **ネストディレクトリ対応**:ナレッジベースの一覧表示が多階層のネストディレクトリに対応
- **ルートレベルファイル表示**:ナレッジツリーにルートディレクトリの `index.md`、`log.md` などを表示
- **空状態統計の修正**:ルートレベルファイルが空状態検出に干渉しなくなりました
## 🌙 夢の記憶改善
- **構造化整理**:夢の記憶ファイルが日付別に自動アーカイブされ、ディレクトリ構造がより整理されました
- **スケジュールジッター**:毎日の夢トリガーにランダムジッターを追加し、クラスター環境での同時実行の競合を回避
## 🛠 スキルシステム改善
- **スキルマネージャーの更新**`/skill` コマンド実行後にスキルマネージャーを自動リフレッシュし、状態の同期を確保
- **インストールソース拡張**スキルインストールが複数のソース形式URL、zip、ローカルファイルなどに対応し、ターゲットディレクトリを自動的に確保
## 🐛 その他の修正
- **Gemini 修正**Gemini の tool call が結果を返さない問題を修正
- **Agent リトライ**:空レスポンスのリトライ時に `tool_calls` が破棄されなくなりました
- **Docker 環境変数同期**Docker 環境で設定更新後に環境変数が同期されない問題を修正
- **Python 3.7 互換**Python 3.7 互換性のために `Literal` のインポートを遅延
- **モデル切替通知**:モデル切替後に bot_type 変更通知が表示されない問題を修正。Thanks @6vision
- **設定コマンド**`/config` で `enable_thinking` の設定が可能に
- **思考表示**:深い思考の表示がデフォルトで無効に
## 📦 アップグレード
`cow update` または `./run.sh update` でアップグレード、またはコードを手動で pull して再起動。詳細は[アップグレードガイド](https://docs.cowagent.ai/ja/guide/upgrade)を参照。
**リリース日**2026.04.22 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.6...master)

View File

@@ -0,0 +1,158 @@
---
title: image-generation - 画像生成
description: テキストから画像生成 / 画像編集 / 複数画像の融合、複数プロバイダーの自動ルーティングとフォールバック対応
---
汎用の画像生成・編集スキルです。OpenAI、Gemini、SeedreamVolcengine Ark、QwenDashScope、MiniMax、LinkAI の 6 社に対応。モデルを手動で選ぶ必要はなく、固定の優先順位に従って、設定済みのプロバイダーを自動的に選択します。
## モデル選択
`image-generation` は「固定優先度 + 自動フォールバック」のストラテジーを採用しています。API Key を設定するだけで使えます:
1. **優先順位**: `OpenAI → Gemini → Seedream → Qwen → MiniMax → LinkAI`
2. **未設定のプロバイダーはスキップ**: API Key が設定されているプロバイダーのみが参加
3. **失敗時は自動で次へ**: 401、モデル未開通、ネットワークエラーなどの場合、次のプロバイダーを試行
4. **モデル指定時は前置**: 特定のモデル名を渡すと、そのプロバイダーが最前列に昇格
### 対応モデル
| プロバイダー | モデル / エイリアス | 特徴 |
| --- | --- | --- |
| OpenAI | `gpt-image-2`、`gpt-image-1` | 汎用テキスト→画像、高品質、`quality` パラメータ対応 |
| Gemini Nano Banana | `nano-banana-2`、`nano-banana-pro`、`nano-banana` | `gemini-3.1-flash`、`gemini-3-pro`、`gemini-2.5-flash` の画像バージョン |
| SeedreamVolcengine Ark | `seedream-5.0-lite`、`seedream-4.5` | ネイティブ 2K4K、最大 14 枚の参照画像を融合 |
| QwenDashScope | `qwen-image-2.0`、`qwen-image-2.0-pro` | 中国語テキスト描画やテキスト・画像レイアウトに強い |
| MiniMax | `image-01` | シンプルで高速な画像生成 |
| LinkAI | 任意のモデル | 汎用プロキシ、フォールバック用 |
<Note>
デフォルトでは Agent はモデルを選ばず、自動ルーティングを使用します。特定のモデルを使いたい場合は、会話で直接指定してください「seedream で猫を描いて」「gpt-image-2 でポスターを作って」)。下記の「カスタム設定」でデフォルトモデルを固定することもできます。
</Note>
## カスタム設定
### API Key の設定
**少なくとも 1 つ**のプロバイダーの Key が必要です。複数設定すると自動フォールバックが有効になります。設定方法は 3 通り:
#### 方法 1既存のモデル Key を自動再利用
Web コンソールや `config.json` で対話モデルの Key`openai_api_key`、`gemini_api_key` など)を設定済みの場合、起動時にこれらの Key は対応する環境変数に**自動同期**されます。つまり、対話モデルが使えていれば、画像生成も同じ Key で追加設定なしに利用できます。
#### 方法 2config.json で設定
`config.json` に Key フィールドを直接記述:
```json
{
"openai_api_key": "sk-xxx",
"openai_api_base": "https://api.openai.com/v1",
"gemini_api_key": "AIza-xxx",
"ark_api_key": "xxx",
"dashscope_api_key": "sk-xxx",
"minimax_api_key": "xxx",
"linkai_api_key": "xxx"
}
```
変更後は再起動が必要です。各 Key には対応する `*_api_base` フィールドがあり、カスタムエンドポイントを指定できます。
#### 方法 3会話で直接設定
チャットで API Key を送信すると、Agent が `env_config` ツールで `~/cow/.env` に保存します。**再起動不要**でただちに反映されます。例:
```
OPENAI_API_KEY を sk-xxx に設定して
```
または:
```
ARK_API_KEY を xxx に設定して
```
### API Key 一覧
| 環境変数 | config.json フィールド | プロバイダー | デフォルト Base URL |
| --- | --- | --- | --- |
| `OPENAI_API_KEY` | `openai_api_key` | OpenAI | `https://api.openai.com/v1` |
| `GEMINI_API_KEY` | `gemini_api_key` | Gemini | `https://generativelanguage.googleapis.com` |
| `ARK_API_KEY` | `ark_api_key` | Volcengine ArkSeedream | `https://ark.cn-beijing.volces.com/api/v3` |
| `DASHSCOPE_API_KEY` | `dashscope_api_key` | Alibaba DashScopeQwen | `https://dashscope.aliyuncs.com` |
| `MINIMAX_API_KEY` | `minimax_api_key` | MiniMax | `https://api.minimaxi.com` |
| `LINKAI_API_KEY` | `linkai_api_key` | LinkAI | `https://api.link-ai.tech` |
### デフォルトモデルの固定
すべての画像生成を特定のプロバイダーのモデルで固定したい場合、`config.json` に以下を追加:
```json
"skill": {
"image-generation": {
"model": "seedream-5.0-lite"
}
}
```
起動時にこの設定は環境変数 `SKILL_IMAGE_GENERATION_MODEL` に自動変換され、スクリプトはこのモデルのプロバイダーを常に使用します。
## 有効化と無効化
`image-generation` は内蔵スキルで、**API Key に基づいてステータスが自動調整**されます:
- **Key 設定済み**:スキルはアクティブ — Agent は画像生成リクエストを受けると呼び出す
- **Key 未設定**:スキルはコンテキストに表示される(「設定が必要」とマーク)— Agent は呼び出し失敗の代わりに Key の設定を案内する
手動で制御する場合:
```text
/skill disable image-generation # 無効化Key があっても呼び出されない)
/skill enable image-generation # 再有効化
```
ターミナルでは `cow skill disable image-generation` / `cow skill enable image-generation`。
## パラメータ
| パラメータ | 型 | 必須 | デフォルト | 説明 |
| --- | --- | --- | --- | --- |
| `prompt` | string | はい | — | 画像の説明 |
| `image_url` | string / list | いいえ | null | 編集用の入力画像。ローカルパスまたは URL。複数指定で複数画像融合 |
| `quality` | string | いいえ | auto | `low` / `medium` / `high` — 一部のプロバイダーのみ対応 |
| `size` | string | いいえ | auto | `512` / `1K` / `2K` / `3K` / `4K`、またはピクセル値(例: `1024x1024` |
| `aspect_ratio` | string | いいえ | null | `1:1` / `3:2` / `2:3` / `16:9` / `9:16` / `21:9`Gemini は `1:4` / `4:1` / `1:8` / `8:1` にも対応 |
<Warning>
**品質が高いほど・解像度が大きいほど、コストが高く、時間がかかります。**
- 日常の会話やプレビューにはデフォルト(`auto`)、または `quality=low` + `size=1K` を使用 — 約 20 秒で生成
- ポスターやユーザーが高解像度を明示的に要求した場合は `quality=high` + `size=2K/4K` — モデルによって 1〜5 分かかる場合があります
</Warning>
## 出力
成功時:
```json
{
"model": "doubao-seedream-5-0-260128",
"images": [
{"url": "/path/to/output.png"}
]
}
```
失敗時:`{ "error": "..." }`。エラー後は**直接リトライしないでください** — ほぼ確実に設定の問題ですKey の誤り、API ベース URL の不一致、モデル未開通など)。まず設定を修正してから再試行してください。
## よくある使い方
- **テキスト→画像**:説明からイラスト、ポスター、アイコン、アバター、絵コンテなどを生成
- **画像→画像**:既存の画像のスタイル変更、要素の入れ替え、装飾やテキストの追加
- **複数画像の融合**:複数の参照画像を 1 枚に合成(着せ替え、キャラクター集合写真など)
<Note>
- bash タイムアウトは 600 秒に設定してください。各プロバイダーの HTTP タイムアウトは 300 秒ですが、スクリプトが複数のプロバイダーを順番に試行する場合があります
- 入力画像は自動的に 4 MB 以下・最長辺 4096 px 以下に圧縮されます
- Gemini / Seedream / Qwen / MiniMax は `quality` パラメータに対応していません(渡しても無視されます)
- Seedream のデフォルトは 2K。`seedream-5.0-lite` は 3K まで、`seedream-4.5` は 4K まで対応
</Note>

View File

@@ -0,0 +1,112 @@
---
title: knowledge-wiki - ナレッジベース
description: ローカルの構造化ナレッジベースを管理し、自動でアーカイブ・分類・相互参照を行う
---
会話で生まれた資料、アイデア、メモをローカルの構造化ナレッジベースに整理し、インデックスとページ間の相互参照を自動で維持します。
`knowledge-wiki` はワークスペース内の `knowledge/` ディレクトリを管理します。Agent の「外部メモリ」のようなものです。`always: true` が設定されているため**常にコンテキストにロード**され、外部依存は不要です。
## いつ起動するか
- 記事、ドキュメント、URL を共有して、後で参照できるように残したいとき
- 会話の中で長期保存に値する結論が出たとき
- 以前蓄積したナレッジを調べたいとき
## ディレクトリ構成
```
knowledge/
├── index.md # グローバルインデックス(必ずメンテナンスする)
├── log.md # 操作ログ(追記のみ)
└── <category>/ # カテゴリサブディレクトリ(内容ごとにグループ化)
└── <slug>.md # ナレッジページ(小文字ハイフン区切りのファイル名)
```
## 3 つの基本操作
### 1. 収録Ingest
資料を共有すると、Agent は:
1. 原文を読んで理解し、重要な情報を抽出
2. どのカテゴリに属するか判断 — まず `index.md` をチェックし、適切なカテゴリがなければ新規作成
3. `knowledge/<category>/<slug>.md` にナレッジページを生成
4. インデックス `index.md` とログ `log.md` を更新
### 2. 統合Synthesize
会話の中で新しい結論やインサイトが生まれたとき:
1. 適切なカテゴリの下に新しいナレッジページを作成
2. 関連する既存ページに相互リンクを追加
3. インデックスとログを更新
### 3. 検索Query
以前蓄積したナレッジについて質問されたとき:
1. `index.md` から関連しそうなページを探す
2. `read` ツールで具体的なページを開く
3. 必要に応じて `memory_search` で補完検索
4. 回答にナレッジページへのリンクを含め、ユーザーが原文を確認できるようにする
## ページの書き方
```markdown
# ページタイトル
> Source: <ソース URL または簡単な説明>
本文。ページ間は相対パスでリンク:
[関連ページ](../category/related-page.md)
## 要点
- ...
## 関連ページ
- [ページ A](../category/page-a.md) — 関連する理由
```
<Note>
- `> Source:` はこのナレッジの出典を記録します。明確な出典がある場合は必ず記載してください
- 相互参照は重要です:ページを作成・更新したら、関連ページにも逆リンクを追加してください
- **既に存在するページにのみリンクしてください**。ある概念が独立ページに値する場合は、先にページを作成してからリンクを追加してください
</Note>
## インデックス形式
`knowledge/index.md` はフラットリスト形式で、カテゴリごとにグループ化し、各ナレッジページを 1 行で表します:
```markdown
# Knowledge Index
## カテゴリ A
- [ページタイトル](category-a/page-slug.md) — 一行の要約
## カテゴリ B
- [ページタイトル](category-b/page-slug.md) — 一行の要約
```
テーブルや絵文字は使いません。カテゴリ名や構成は柔軟に調整できます。
## ログ形式
`knowledge/log.md` は追記のみ、最新のエントリが一番下:
```markdown
## [YYYY-MM-DD] ingest | ページタイトル
## [YYYY-MM-DD] synthesize | ページタイトル
```
## 執筆ガイドライン
- **ファイル名**は小文字+ハイフン(例: `machine-learning.md`
- **1 ページ 1 トピック** — 関連コンテンツはリンクで繋ぐ
- **重複ページを作らず、既存ページを更新する**
- **変更のたびにインデックスを更新する**`knowledge/index.md`
- **要点を抽出し、全文をコピーしない**
- **会話中にナレッジページを参照する際はフルパスを使用**(例: `[タイトル](knowledge/<category>/<slug>.md)`)。ページ間の相互リンクのみ相対パスを使用
- **ナレッジページに基づいて回答する際はリンクを含める** — ユーザーが詳細を確認できるように

View File

@@ -0,0 +1,180 @@
---
title: skill-creator - スキル作成
description: スキルの作成・インストール・更新、SKILL.md の書き方とディレクトリ構成の標準化
---
`skill-creator` は「メタスキル」です。Agent が他のスキルを作成・インストール・更新する際に呼び出され、すべてのスキルの `SKILL.md` の書き方とディレクトリ構成を統一します。
## いつ起動するか
- ユーザーが URL やリモートリポジトリからスキルをインストールしたいとき
- ユーザーが新しいスキルをゼロから作成したいとき
- 既存のスキルをアップグレード・リファクタリングする必要があるとき
## スキルとは
スキルは「再利用可能な説明書」にオプションのスクリプトやリソースを加えたものです。特定のドメインの専門知識を Agent に注入し、該当タスクをスペシャリストのように処理できるようにします。
スキルには通常、以下が含まれます:
1. **専門ワークフロー** — ある種のタスクの完全な手順
2. **ツールの使い方** — 特定の API やファイル形式の処理方法
3. **ドメイン知識** — チームの規約、ビジネスルール、データ構造など
4. **付属リソース** — スクリプト、参考ドキュメント、テンプレートなど
<Note>
**基本原則:省けるものは省く。** Agent が自力で推測できない内容だけを書きましょう。1 行追加するたびに「このトークンコストに見合うか?」と自問してください。
</Note>
## ディレクトリ構成
```
skill-name/
├── SKILL.md # 必須:スキル定義
│ ├── YAML frontmattername / description は必須)
│ └── Markdown 本文(説明 + 例)
└── オプションリソース
├── scripts/ # 実行可能スクリプトPython / Bash など)
├── references/ # 分量が多い参考ドキュメントAgent が必要時に読む)
└── assets/ # テンプレート、アイコンなど(出力に直接使われるもの)
```
## SKILL.md 仕様
SKILL.md ヘッダーの `frontmatter` フィールド:
| フィールド | 説明 |
| --- | --- |
| `name` | スキル名。小文字+ハイフン、ディレクトリ名と一致させる |
| `description` | **最も重要なフィールド**。「このスキルが何をするか」「いつ使うべきか」を明記する。Agent はこれを見て呼び出すかどうかを判断する。トリガーに関する記述はすべてここに書き、本文には書かない |
| `metadata.cowagent.requires.bins` | システムに必要な CLI ツール |
| `metadata.cowagent.requires.env` | 必要な環境変数(すべて揃っている必要がある) |
| `metadata.cowagent.requires.anyEnv` | 複数の API Key のうち 1 つあればよい |
| `metadata.cowagent.requires.anyBins` | 複数のツールのうち 1 つあればよい |
| `metadata.cowagent.always` | `true` にすると常にロードされ、依存チェックをスキップ |
| `metadata.cowagent.emoji` | 表示用の絵文字(任意) |
| `metadata.cowagent.os` | OS 制限、例: `["darwin", "linux"]` |
<Note>
`category` フィールドは手動で設定する必要はありません。システムが自動的に `skill` に設定します。
</Note>
API Key 依存の宣言方法は 2 通り:
```yaml
metadata:
cowagent:
requires:
env: ["MYAPI_KEY"] # 必須
```
```yaml
metadata:
cowagent:
requires:
anyEnv: ["OPENAI_API_KEY", "LINKAI_API_KEY"] # いずれか 1 つ
```
**スキルは依存関係に基づいて自動的に有効/無効になります**:環境変数が揃えば自動有効、不足すれば自動無効。手動で `/skill enable` する必要はありません。
## リソースディレクトリの使い方
| ディレクトリ | 入れるもの | 入れないもの |
| --- | --- | --- |
| `scripts/` | 繰り返し実行するコード、確定的な結果が必要なスクリプト | デモ用のコード片 |
| `references/` | **500 行超**で SKILL.md に収まらない大きなドキュメント(完全な DB スキーマなど) | 一般的な API ドキュメント、チュートリアル |
| `assets/` | 最終出力に含まれるファイル(テンプレート、アイコン、ボイラープレートなど) | 説明用ドキュメント |
<Warning>
**原則としてすべての内容を `SKILL.md` に書きます** — リソースディレクトリに分割するのは本当に収まらない場合だけです。
`README.md`、`CHANGELOG.md`、`INSTALLATION_GUIDE.md` などをスキルに追加しないでください。すべて `SKILL.md` に入れましょう。リソースディレクトリには実際に実行するスクリプトや実際に使う素材だけを配置してください。
</Warning>
## 外部スキルのインストール
インストール後、スキルは `<workspace>/skills/<name>/` に配置されます。
| ソース | インストール方法 |
| --- | --- |
| URL単一ファイル | curl / web_fetch |
| URLzip アーカイブ) | ダウンロードして展開 |
| ローカル SKILL.md | 直接読み込み |
| ローカル zip アーカイブ | 展開 |
インストール手順:
1. `SKILL.md` を見つける(アーカイブのルートまたはサブディレクトリにある場合がある)
2. frontmatter から `name` を読み取る
3. **スキルディレクトリ全体**`SKILL.md`、`scripts/`、`assets/` など)を `<workspace>/skills/<name>/` にコピー
4. アーカイブに `INSTALL.md` などのセットアップスクリプトがあれば実行するが、最終的に `<workspace>/skills/<name>/` に収まっている必要がある
## スキルをゼロから作成
推奨手順:
1. **要件を明確にする** — ユーザーに具体的なユースケースをいくつか挙げてもらう(一度に多く聞きすぎない)
2. **構成を計画する** — スクリプトは必要か?参考ドキュメントは?テンプレートは?
3. **スキャフォールド** — 初期化スクリプトを使用:
```bash
scripts/init_skill.py <skill-name> --path <workspace>/skills [--resources scripts,references,assets] [--examples]
```
4. **内容を埋める** — SKILL.md を書き、スクリプトとリソースを追加。スクリプトは必ず実行テストする
5. **バリデーション**(任意):
```bash
scripts/quick_validate.py <workspace>/skills/<skill-name>
```
6. **イテレーション** — 実際の使用フィードバックに基づいて継続的に改善
## 命名規則
- 小文字、数字、ハイフンのみ使用。ユーザーの入力は正規化する(例: `Plan Mode` → `plan-mode`
- 64 文字以内
- 短く、動詞で始め、一目で何をするか分かるように
- 必要に応じてツール名をプレフィックスにする(例: `gh-address-comments`、`linear-address-issue`
- ディレクトリ名と `name` フィールドは完全に一致させる
## 3 段階ローディング
スキルは一度にすべてコンテキストに読み込まれるわけではなく、3 段階で必要に応じてロードされます:
1. **メタ情報**`name` + `description` — 常にコンテキスト内(約 100 語。Agent がスキルを使うかどうかの判断に使用
2. **SKILL.md 本文** — スキルが有効化されたときだけロード。500 行以内を推奨
3. **リソースファイル** — Agent が必要なときに読み込む
複数のバリエーション(例: マルチクラウドデプロイ)を持つスキルは次のように整理:
```
cloud-deploy/
├── SKILL.md # メインワークフローとプロバイダー選択ロジック
└── references/
├── aws.md
├── gcp.md
└── azure.md
```
ユーザーが AWS を選んだら、Agent は `aws.md` だけを読みます。3 社分のドキュメントをすべてロードする必要はありません。
## よくあるデザインパターン
**ステップ式**:番号付きの手順と対応スクリプト。
```markdown
1. フォーム構造を分析analyze_form.py を実行)
2. フィールドマッピングを生成fields.json を編集)
3. フォームを自動入力fill_form.py を実行)
```
**分岐式**:ユーザーの意図に応じて異なるフローへ。
```markdown
1. 操作タイプを判定:
**新規作成?** → 「作成フロー」へ
**既存の編集?** → 「編集フロー」へ
```
**テンプレート式**出力形式に厳密な要件がある場合、SKILL.md にテンプレートを含め、Agent にそれに従って出力させる。

View File

@@ -27,7 +27,7 @@ Vision ツールは多段階の自動選択+自動フォールバック戦略
| Claude | メインモデル | Anthropic ネイティブ画像形式 |
| Gemini | メインモデル | inlineData 形式 |
| 豆包 (Doubao) | メインモデル | doubao-seed-2-0 シリーズがネイティブ対応 |
| Kimi (Moonshot) | メインモデル | kimi-k2.5 がネイティブ対応 |
| Kimi (Moonshot) | メインモデル | kimi-k2.6、kimi-k2.5 がネイティブ対応 |
| 智谱 AI | glm-5v-turbo | 常にビジョン専用モデルを使用 |
| MiniMax | MiniMax-Text-01 | 常にビジョン専用モデルを使用 |

View File

@@ -5,6 +5,10 @@ description: CowAgent 的个人知识库系统 — 结构化知识沉淀、自
个人知识库是 Agent 的长期结构化知识存储,保存在工作空间的 `knowledge/` 目录下。与按时间线组织的记忆不同,知识库以主题为维度,将用户分享的文章、对话中的洞察、学习材料等整理为互相关联的 Markdown 页面,形成可持续增长的知识网络。
<Frame>
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="800" />
</Frame>
## 核心概念
### 知识 vs 记忆
@@ -30,7 +34,12 @@ description: CowAgent 的个人知识库系统 — 结构化知识沉淀、自
└── llm-wiki.md
```
目录结构是灵活的 — Agent 会根据实际内容自动创建合适的分类目录。用户也可以通过对话自定义目录组织方式。
目录结构是灵活的 — Agent 会根据实际内容自动创建合适的分类目录。用户也可以通过对话的方式自定义目录组织方式。
<Frame>
<img src="https://cdn.link-ai.tech/doc/20260413110104.png" width="800" />
</Frame>
## 自动整理
@@ -40,6 +49,11 @@ description: CowAgent 的个人知识库系统 — 结构化知识沉淀、自
- **对话产生有价值的结论** — Agent 将洞察整理为知识页面,并与已有知识建立关联
- **用户主动要求整理** — 用户可以通过对话指导 Agent 组织和更新知识
<Frame>
<img src="https://cdn.link-ai.tech/doc/17aad553d3e9e428c52ff9dc31726fda.png" width="800" />
</Frame>
每个知识页面都包含与其他页面的交叉引用链接,逐步构建起一个知识图谱。
## 知识检索
@@ -55,9 +69,14 @@ Agent 在对话中可以通过以下方式检索知识:
Web 控制台提供了专用的「知识」模块,支持:
- **文档浏览** — 树状目录结构,可搜索、可折叠,点击查看文档内容
- **知识图谱** — 基于 D3.js 的力导向图,可视化展示知识之间的关联关系
- **知识图谱** — 可视化展示知识之间的关联关系,节点可直接跳转至文档
- **对话联动** — Agent 回复中引用的知识文档链接可直接点击跳转查看
<Frame>
<img src="https://cdn.link-ai.tech/doc/20260413105402.png" width="800" />
</Frame>
## CLI 命令
通过 `/knowledge` 命令管理知识库:

View File

@@ -39,14 +39,15 @@ description: 对话上下文 — 消息管理、压缩策略和上下文操作
- 裁剪 **最早一半** 的完整轮次(保证工具调用链的完整性)
- 被裁剪的消息会通过 LLM 总结后**写入当天的日级记忆文件**
- 剩余轮次保持不变
- LLM 摘要完成后,同时将摘要**注入到保留消息的第一条用户消息开头**,帮助模型在后续对话中保持上下文连贯性
- 摘要注入在后台异步完成,不阻塞当前回复;注入的摘要在下一轮对话时生效
### 3. Token 预算裁剪
裁剪轮次后,如果 token 数仍超出预算:
- **轮次 < 5 时**:对所有轮次进行**文本压缩** — 每轮只保留第一条用户文本和最后一条 Agent 回复,去掉中间的工具调用链
- **轮次 ≥ 5 时**:再次裁剪**前半轮次**,被丢弃内容同样写入记忆
- **轮次 ≥ 5 时**:再次裁剪**前半轮次**,被丢弃内容同样写入记忆并注入上下文摘要
### 4. 溢出应急处理

View 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 全部完成后才启动蒸馏 |
| **禁止编造** | 提示词明确约束只能基于已有材料整理,不得推测或添加 |

View File

@@ -5,6 +5,8 @@ description: CowAgent 的长期记忆系统 — 文件持久化、自动写入
长期记忆保存在工作空间文件中跨会话持久存在。Agent 在对话中通过检索工具按需加载历史记忆,也会在上下文裁剪时自动将对话摘要写入长期记忆。
<img src="https://cdn.link-ai.tech/doc/memory-architecture-zh.jpeg" alt="Memory Architecture" />
## 记忆类型
### 核心记忆MEMORY.md
@@ -15,12 +17,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 总结 + 文件写入),不阻塞正常对话回复。
@@ -34,19 +41,25 @@ Agent 通过以下机制自动将对话内容持久化为长期记忆:
Agent 会在对话中根据需要自动触发记忆检索,将相关历史信息纳入上下文。检索结果按混合评分排序(默认向量权重 0.7、关键词权重 0.3),日级记忆会随时间衰减(半衰期 30 天),核心记忆不衰减。
## 首次启动
## 相关文件
首次启动 Agent 时Agent 会主动向用户询问关键信息,并记录至工作空间(默认 `~/cow`)中:
工作空间(默认 `~/cow`)中与记忆相关的文件
| 文件 | 说明 |
| --- | --- |
| `system.md` | Agent 的系统提示词和行为设定 |
| `user.md` | 用户身份信息和偏好 |
| `AGENT.md` | Agent 的人格和行为设定 |
| `USER.md` | 用户身份信息和偏好 |
| `RULE.md` | 自定义规则和约束 |
| `MEMORY.md` | 核心记忆(长期) |
| `memory/YYYY-MM-DD.md` | 日级记忆(按需创建) |
| `memory/dreams/YYYY-MM-DD.md` | 梦境日记Deep Dream 自动生成) |
## Web 控制台
在 Web 控制台的记忆管理页面中,可浏览记忆文件和梦境日记,支持通过 Tab 切换查看:
<Frame>
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
<img src="https://cdn.link-ai.tech/doc/20260414171014.png" width="800" />
</Frame>
## 相关配置

View File

@@ -12,6 +12,6 @@ description: Claude 模型配置
| 参数 | 说明 |
| --- | --- |
| `model` | 支持 `claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest` 等,参考 [官方模型](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `model` | 支持 `claude-sonnet-4-6`、`claude-opus-4-7`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest` 等,参考 [官方模型](https://docs.anthropic.com/en/docs/about-claude/models/overview) |
| `claude_api_key` | 在 [Claude 控制台](https://console.anthropic.com/settings/keys) 创建 |
| `claude_api_base` | 可选,默认为 `https://api.anthropic.com/v1`,修改可接入第三方代理 |

View File

@@ -99,27 +99,6 @@ description: Coding Plan 模式模型配置
---
## Kimi
```json
{
"bot_type": "openai",
"model": "kimi-for-coding",
"open_ai_api_base": "https://api.kimi.com/coding/v1",
"open_ai_api_key": "YOUR_API_KEY"
}
```
| 参数 | 说明 |
| --- | --- |
| `model` | `kimi-for-coding` |
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
| `open_ai_api_key` | Coding Plan 专用 Key与按量计费接口不通用 |
官方文档:[Key 获取](https://www.kimi.com/code/docs/)
---
## 火山引擎
```json
@@ -138,3 +117,24 @@ description: Coding Plan 模式模型配置
| `open_ai_api_key` | API Key 与普通接口通用 |
官方文档:[快速开始](https://www.volcengine.com/docs/82379/1928261?lang=zh)
---
## Kimi
```json
{
"bot_type": "moonshot",
"model": "kimi-for-coding",
"moonshot_base_url": "https://api.kimi.com/coding/v1",
"moonshot_api_key": "YOUR_API_KEY"
}
```
| 参数 | 说明 |
| --- | --- |
| `model` | 填写 `kimi-for-coding` 会自动更新模型,或指定模型例如 `kimi-k2.6` |
| `moonshot_base_url` | `https://api.kimi.com/coding/v1` |
| `moonshot_api_key` | Coding Plan 专用 Key与按量计费接口不通用 |
官方文档:[Key 获取](https://www.kimi.com/code/docs/)

62
docs/models/custom.mdx Normal file
View File

@@ -0,0 +1,62 @@
---
title: 自定义
description: 自定义厂商配置,适用于第三方 API 代理和本地模型
---
适用于通过 OpenAI 兼容协议接入的第三方模型服务或本地部署的模型,例如:
- **第三方 API 代理**:使用统一的 API Base 调用多种模型
- **本地模型**:通过 Ollama、vLLM、LocalAI 等工具在本地部署的模型
- **私有化部署**:企业内部部署的模型服务
<Note>
与 `openai` 厂商的区别:选择自定义厂商后,通过 `/config model` 切换模型时,不会自动切换厂商类型,始终使用自定义的 API 地址。
</Note>
## 配置方式
### 第三方 API 代理
```json
{
"bot_type": "custom",
"model": "",
"custom_api_key": "YOUR_API_KEY",
"custom_api_base": "https://{your-proxy.com}/v1"
}
```
| 参数 | 说明 |
| --- | --- |
| `bot_type` | 必须设为 `custom` |
| `model` | 模型名称,填写代理服务支持的任意模型名 |
| `custom_api_key` | API 密钥,由代理服务提供 |
| `custom_api_base` | API 地址,由代理服务提供,需兼容 OpenAI 协议 |
### 本地模型
本地模型通常不需要 API Key只需填写 API Base 即可:
```json
{
"bot_type": "custom",
"model": "qwen3.5:27b",
"custom_api_base": "http://localhost:11434/v1"
}
```
常见的本地部署工具及默认地址:
| 工具 | 默认 API Base |
| --- | --- |
| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
| [LocalAI](https://localai.io) | `http://localhost:8080/v1` |
## 切换模型
自定义厂商下切换模型时,只会修改 `model`,不会改变 `bot_type` 和 API 地址:
```
/config model qwen3.5:27b
```

View File

@@ -5,14 +5,14 @@ description: 智谱AI GLM 模型配置
```json
{
"model": "glm-5-turbo",
"model": "glm-5.1",
"zhipu_ai_api_key": "YOUR_API_KEY"
}
```
| 参数 | 说明 |
| --- | --- |
| `model` | 可填 `glm-5-turbo`、`glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air` 等,参考 [模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4) |
| `model` | 可填 `glm-5.1`、`glm-5-turbo`、`glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air` 等,参考 [模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4) |
| `zhipu_ai_api_key` | 在 [智谱AI 控制台](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys) 创建 |
也支持 OpenAI 兼容方式接入:
@@ -20,7 +20,7 @@ description: 智谱AI GLM 模型配置
```json
{
"bot_type": "openai",
"model": "glm-5-turbo",
"model": "glm-5.1",
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
"open_ai_api_key": "YOUR_API_KEY"
}

View File

@@ -6,7 +6,7 @@ description: CowAgent 支持的模型及推荐选择
CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在项目的 `models/` 目录下。
<Note>
Agent 模式下推荐使用以下模型可根据效果及成本综合选择MiniMax-M2.7、glm-5-turbo、kimi-k2.5、qwen3.6-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
Agent 模式下推荐使用以下模型可根据效果及成本综合选择MiniMax-M2.7、glm-5.1、kimi-k2.6、qwen3.6-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
同时支持使用 [LinkAI](https://link-ai.tech) 平台接口,可灵活切换多种模型,并支持知识库、工作流、插件等 Agent 能力。
</Note>
@@ -27,13 +27,13 @@ CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在
MiniMax-M2.7 等系列模型
</Card>
<Card title="智谱 GLM" href="/models/glm">
glm-5-turbo、glm-5 等系列模型
glm-5.1、glm-5-turbo、glm-5 等系列模型
</Card>
<Card title="通义千问 Qwen" href="/models/qwen">
qwen3.6-plus、qwen3-max 等
</Card>
<Card title="Kimi" href="/models/kimi">
kimi-k2.5、kimi-k2 等
kimi-k2.6、kimi-k2.5、kimi-k2 等
</Card>
<Card title="豆包 Doubao" href="/models/doubao">
doubao-seed 系列模型
@@ -53,6 +53,9 @@ CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在
<Card title="LinkAI" href="/models/linkai">
多模型统一接口 + 知识库
</Card>
<Card title="自定义" href="/models/custom">
第三方代理、本地模型等
</Card>
</CardGroup>

View File

@@ -5,14 +5,14 @@ description: Kimi (Moonshot) 模型配置
```json
{
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"moonshot_api_key": "YOUR_API_KEY"
}
```
| 参数 | 说明 |
| --- | --- |
| `model` | 可填 `kimi-k2.5`、`kimi-k2`、`moonshot-v1-8k`、`moonshot-v1-32k`、`moonshot-v1-128k` |
| `model` | 可填 `kimi-k2.6`、`kimi-k2.5`、`kimi-k2`、`moonshot-v1-8k`、`moonshot-v1-32k`、`moonshot-v1-128k` |
| `moonshot_api_key` | 在 [Moonshot 控制台](https://platform.moonshot.cn/console/api-keys) 创建 |
也支持 OpenAI 兼容方式接入:
@@ -20,7 +20,7 @@ description: Kimi (Moonshot) 模型配置
```json
{
"bot_type": "openai",
"model": "kimi-k2.5",
"model": "kimi-k2.6",
"open_ai_api_base": "https://api.moonshot.cn/v1",
"open_ai_api_key": "YOUR_API_KEY"
}

View File

@@ -5,6 +5,8 @@ description: CowAgent 版本更新历史
| 版本 | 日期 | 说明 |
| --- | --- | --- |
| [2.0.7](/releases/v2.0.7) | 2026.04.22 | 图像生成技能六厂商自动路由、新模型支持Kimi K2.6、Claude Opus 4.7、GLM 5.1、知识库增强、Web 控制台优化 |
| [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端文件处理、记忆系统升级 |

83
docs/releases/v2.0.6.mdx Normal file
View File

@@ -0,0 +1,83 @@
---
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)
Inspired by Karpathy's [LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f).
## 🌙 梦境记忆蒸馏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)

64
docs/releases/v2.0.7.mdx Normal file
View File

@@ -0,0 +1,64 @@
---
title: v2.0.7
description: CowAgent 2.0.7 - 图像生成技能六厂商自动路由、新模型支持、知识库增强、Web 控制台优化及多项修复
---
## 🎨 图像生成技能
新增图像生成内置技能,支持文生图、图生图、多图融合,支持 `GPT-Image-2`、`Nano Banana` 等多种模型:
- **自动路由**支持六种模型厂商自动切换OpenAI (GPT-Image-2) → Gemini (Nano Banana) → Seedream (火山方舟) → Qwen (百炼) → MiniMax → LinkAI
- **开箱即用**:配置 API Key 即可使用,无需手动指定模型。也支持在对话中指定特定模型
- **灵活控制**:支持 `quality`(画质)、`size`分辨率512/1K~4K、`aspect_ratio`(宽高比)等参数,各厂商自动适配有效值
- **图片编辑**:传入已有图片即可进行编辑、风格迁移、多图融合
- **Skill 级配置**:支持通过 `config.json` 中的 `skill.image-generation.model` 固定默认模型
相关文档:[图像生成技能](https://docs.cowagent.ai/skills/image-generation)
## 🤖 新模型支持
- **Kimi K2.6**:新增 `kimi-k2.6` 模型支持
- **Claude Opus 4.7**:新增 `claude-opus-4-7` 模型支持
- **GLM 5.1**:新增 `glm-5.1` 模型支持
- **Kimi Coding Plan**:支持 Kimi Coding Plan 模式
- **自定义模型厂商**:新增[自定义模型](https://docs.cowagent.ai/models/custom)提供方配置,方便接入本地模型及更多厂商
## 📚 知识库增强
- **嵌套目录支持**:知识库列表和展示支持多级嵌套目录
- **根级文件展示**:知识树中显示根目录下的 `index.md`、`log.md` 等文件
- **空状态统计修复**:排除根级文件对知识库统计的干扰,正确保持空状态
## 🌙 梦境记忆优化
- **结构化组织**:梦境记忆文件按日期自动归档,目录结构更清晰
- **定时抖动**:每日定时触发增加随机抖动,避免集群场景下的并发冲突
## 🛠 技能系统改进
- **技能管理刷新**`/skill` 命令执行后自动加载最新技能,确保状态同步
- **安装来源扩展**技能安装支持多种来源格式URL、zip、本地文件等
## 💬 Web 控制台优化
- **智能自动滚动**:优化聊天窗口滚动逻辑,用户手动翻阅时不再强制跳到底部 Thanks @colin2060
- **移动端适配**:侧边栏默认隐藏,支持点击遮罩关闭
- **图片预览去重**:修复同一消息中图片重复渲染的问题
- **推理内容截断**:深度思考内容超出阶段,解决前端卡顿问题
- **会话标题修复**:修复标题自动生成的回退逻辑
## 🐛 其他修复
- **Gemini 修复**:修复 Gemini tool call 不返回结果的问题
- **Agent 重试**:空响应重试时不再丢弃 tool_calls
- **Docker 环境变量**:修复 Docker 环境下更新配置后环境变量未同步的问题 Thanks @sunboy0523
- **Python 3.7 兼容**:延迟导入 `Literal` 以兼容 Python 3.7
- **模型切换通知**:修复切换模型后 bot_type 变更通知未显示的问题。Thanks @6vision
- **配置命令增强**`/config` 支持设置 `enable_thinking`
## 📦 升级方式
源码部署可执行 `cow update` 或 `./run.sh update` 一键升级,或手动拉取代码后重启。详见 [更新升级文档](https://docs.cowagent.ai/guide/upgrade)。
**发布日期**2026.04.22 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.6...2.0.7)

View File

@@ -0,0 +1,160 @@
---
title: image-generation - 图像生成
description: 文生图 / 图生图 / 多图融合,支持多家厂商自动路由与回退
---
通用的图像生成与编辑技能,支持 OpenAI、Gemini、Seedream火山方舟、Qwen百炼、MiniMax、LinkAI 共六家厂商。不需要手动选模型,脚本会按固定优先级自动挑选已配置的厂商来出图。
## 模型选择
`image-generation` 采用「固定优先级 + 自动回退」的策略,配好 Key 就能用:
1. **优先级顺序**`OpenAI → Gemini → Seedream → Qwen → MiniMax → LinkAI`
2. **没配 Key 的跳过**:只有设了 API Key 的厂商才会参与
3. **失败自动切下一家**:遇到 401、模型未开通、网络异常等错误时会自动试下一个
4. **指定模型时前置**:如果明确传了某个模型名,对应厂商会被提到最前面先试
### 支持的模型
| 厂商 | 模型 / 别名 | 特点 |
| --- | --- | --- |
| OpenAI | `gpt-image-2`、`gpt-image-1` | 通用文生图,高质量、高智能,支持 `quality` 参数控制画质 |
| Gemini Nano Banana | `nano-banana-2`、`nano-banana-pro`、`nano-banana` | 对应 `gemini-3.1-flash`、`gemini-3-pro`、`gemini-2.5-flash` 的图像版本 |
| Seedream火山方舟 | `seedream-5.0-lite`、`seedream-4.5` | 原生 2K4K最多 14 张图融合 |
| Qwen百炼 | `qwen-image-2.0`、`qwen-image-2.0-pro` | 擅长中文排版和图文融合 |
| MiniMax | `image-01` | 简单快速的图片生成 |
| LinkAI | 任意模型 | 通用代理,兜底用 |
<Note>
默认情况下 Agent 不会主动选模型,而是走自动路由。如果你想用某个特定模型,直接在对话里说就行,比如「用 seedream 画一只猫」或「用 gpt-image-2 生成海报」。也可以通过下面的「自定义配置」固定默认模型。
</Note>
## 自定义配置
### API Key 配置
至少需要配**一个**厂商的 Key配多个就能享受自动回退能力。有三种配置方式
#### 方式一:已有模型 Key 自动复用
如果你在 web控制台 或 `config.json` 中配置了对话模型的 Key比如 `openai_api_key`、`gemini_api_key` 等),启动时这些 Key 会被**自动同步**到对应的环境变量。也就是说,只要你的对话模型能用,图像生成就能直接用同一个 Key不需要额外配置。
#### 方式二:在 config.json 中配置
在 `config.json` 中直接写对应的 Key 字段即可,支持的字段如下:
```json
{
"openai_api_key": "sk-xxx",
"openai_api_base": "https://api.openai.com/v1",
"gemini_api_key": "AIza-xxx",
"ark_api_key": "xxx",
"dashscope_api_key": "sk-xxx",
"minimax_api_key": "xxx",
"linkai_api_key": "xxx"
}
```
修改后需要重启生效。每个 Key 还有对应的 `*_api_base` 字段可以自定义接口地址。
#### 方式三:对话中直接配置
在对话里发送 API KeyAgent 会通过 `env_config` 工具自动保存到 `~/cow/.env`**不需要重启**就能生效。例如:
```
帮我配置 OPENAI_API_KEY 为 sk-xxx
```
或者:
```
设置 ARK_API_KEY 为 xxx
```
### API Key 一览
| 环境变量 | config.json 字段 | 对应厂商 | 默认 Base URL |
| --- | --- | --- | --- |
| `OPENAI_API_KEY` | `openai_api_key` | OpenAI | `https://api.openai.com/v1` |
| `GEMINI_API_KEY` | `gemini_api_key` | Gemini | `https://generativelanguage.googleapis.com` |
| `ARK_API_KEY` | `ark_api_key` | 火山方舟Seedream | `https://ark.cn-beijing.volces.com/api/v3` |
| `DASHSCOPE_API_KEY` | `dashscope_api_key` | 阿里百炼Qwen | `https://dashscope.aliyuncs.com` |
| `MINIMAX_API_KEY` | `minimax_api_key` | MiniMax | `https://api.minimaxi.com` |
| `LINKAI_API_KEY` | `linkai_api_key` | LinkAI | `https://api.link-ai.tech` |
### 指定默认模型
如果想让所有图像生成固定走某个厂商的模型,可以在 `config.json` 里加:
```json
"skill": {
"image-generation": {
"model": "seedream-5.0-lite"
}
}
```
启动时这段配置会被自动转成环境变量 `SKILL_IMAGE_GENERATION_MODEL`,脚本读到后会固定使用这个模型所在的厂商进行生成。
## 开启和关闭
`image-generation` 是内置技能,**会根据 API Key 自动调整状态**
- **Key 已配置**技能正常可用Agent 收到画图请求时会直接调用
- **Key 未配置**技能仍然会出现在上下文中标记为「需要配置」Agent 会引导用户去配 Key而不是直接调用失败
如果想手动控制,也可以用命令:
```text
/skill disable image-generation # 手动关闭(即使有 Key 也不会被调用)
/skill enable image-generation # 重新开启
```
终端里对应的命令是 `cow skill disable image-generation` / `cow skill enable image-generation`。
## 参数
| 参数 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `prompt` | string | 是 | — | 图像描述 |
| `image_url` | string / list | 否 | null | 编辑用的输入图,支持本地路径或 URL。传多个就是多图融合 |
| `quality` | string | 否 | auto | `low` / `medium` / `high`,只有部分厂商支持 |
| `size` | string | 否 | auto | `512` / `1K` / `2K` / `3K` / `4K`,也可以写像素值如 `1024x1024` |
| `aspect_ratio` | string | 否 | null | `1:1` / `3:2` / `2:3` / `16:9` / `9:16` / `21:9`Gemini 还支持 `1:4` / `4:1` / `1:8` / `8:1` |
<Warning>
**质量越高、分辨率越大,花的钱越多、等的时间越长。**
- 日常对话和快速预览直接用默认(`auto`),或者 `quality=low` + `size=1K`,大概 20 秒出图
- 做海报、用户明确要高清的时候再上 `quality=high` + `size=2K/4K`,可能要等 15 分钟,取决于不同模型的速度
</Warning>
## 输出
成功时返回:
```json
{
"model": "doubao-seedream-5-0-260128",
"images": [
{"url": "/path/to/output.png"}
]
}
```
失败时返回 `{ "error": "..." }`。出错后**不要直接重试**——大概率是配置问题Key 填错、API 地址不对、模型没开通),让用户修好配置再试。
## 常见用法
- **文生图**:根据描述生成插画、海报、图标、头像、分镜图等
- **图生图**:在已有图片上改风格、换元素、加装饰、加文字等
- **多图融合**:把多张参考图合成一张(换装、角色合影等)
<Note>
- bash 超时建议设 600 秒。单个厂商的 HTTP 超时是 300 秒,但脚本可能依次尝试多个厂商
- 输入的图片会自动压缩到 4MB 以内、最长边不超过 4096px
- Gemini / Seedream / Qwen / MiniMax 不支持 `quality` 参数,传了也没用
- Seedream 默认出 2K 图,`seedream-5.0-lite` 支持到 3K`seedream-4.5` 支持到 4K
</Note>

View File

@@ -34,7 +34,7 @@ CowAgent 支持通过统一的 `install` 命令安装来自 **[Cow 技能广场]
## 从 LinkAI 安装
[LinkAI](https://link-ai.tech/console) 上的所有公开资源 (1w+个插件/应用/工作流) ,以及自己创建的资源 (应用/工作流/知识库/数据库/插件) 都可以通过命令一键安装:
[LinkAI](https://link-ai.tech/console) 上的所有公开资源 (1w+个应用/工作流/插件) ,以及自己创建的资源 (应用/工作流/知识库/数据库/插件) 都可以通过命令一键安装:
```text
/skill install linkai:<code>

View File

@@ -0,0 +1,112 @@
---
title: knowledge-wiki - 知识库
description: 维护本地结构化知识库,自动归档、分类和交叉引用
---
帮你把对话中产生的资料、灵感和零散笔记整理成结构化的本地知识库,自动维护索引和页面之间的交叉引用。
`knowledge-wiki` 在工作空间下维护一个 `knowledge/` 目录,相当于 Agent 的「外脑」。技能设置了 `always: true`,会**常驻上下文**,不需要任何外部依赖。
## 什么时候会触发
- 你分享了一篇文章、一份文档或一个 URL想要沉淀下来
- 聊天过程中聊出了值得长期保留的结论
- 你想查一下之前积累过的知识
## 目录结构
```
knowledge/
├── index.md # 全局索引(必须维护)
├── log.md # 操作日志(只追加)
└── <category>/ # 分类子目录(按内容自由分组)
└── <slug>.md # 知识页(文件名用小写加中划线)
```
## 三个核心操作
### 1. 收录Ingest
你分享了一段资料时Agent 会:
1. 读懂原文,提取关键信息
2. 按内容决定放到哪个分类下——先看 `index.md` 里有没有合适的分类,没有就新建一个
3. 生成知识页 `knowledge/<category>/<slug>.md`
4. 更新索引 `index.md` 和日志 `log.md`
### 2. 综合Synthesize
聊天中产生了新的结论或洞见时:
1. 在合适的分类下创建新知识页
2. 给相关的已有页面加上互相指向的链接
3. 更新索引和日志
### 3. 查询Query
你问到以前积累的知识时:
1. 先从 `index.md` 里找可能相关的页面
2. 用 `read` 工具打开具体页面
3. 需要时再用 `memory_search` 补充检索
4. 回答里会带上知识页的链接,方便你点过去看原文
## 知识页怎么写
```markdown
# 页面标题
> Source: <来源 URL 或简要说明>
正文内容。页面之间用相对路径链接:
[相关页](../category/related-page.md)
## 要点
- ...
## 相关页面
- [页面 A](../category/page-a.md) — 为什么相关
```
<Note>
- `> Source:` 用来记录这条知识的来源。有明确来源时一定要写
- 交叉引用很重要:创建或更新某页时,记得也去关联页面里补上反向链接
- **只链接已经存在的页面**。如果某个概念值得单独成页,先建好再加链接
</Note>
## 索引格式
`knowledge/index.md` 采用扁平列表,按分类分组,每个知识页占一行:
```markdown
# Knowledge Index
## 分类 A
- [页面标题](category-a/page-slug.md) — 一句话摘要
## 分类 B
- [页面标题](category-b/page-slug.md) — 一句话摘要
```
不用表格,不加 emoji。分类怎么起名、怎么组织都可以灵活调整。
## 日志格式
`knowledge/log.md` 只追加、不修改,最新的写在最下面:
```markdown
## [YYYY-MM-DD] ingest | 页面标题
## [YYYY-MM-DD] synthesize | 页面标题
```
## 写作约定
- **文件名**用小写加中划线,比如 `machine-learning.md`
- **一页只讲一件事**,需要关联的内容通过链接串起来
- **有了就更新,不要重复建页**
- **每次改完都要更新索引** `knowledge/index.md`
- **写精华别抄全文**,抓住要点就行
- **对话里引用知识页时用完整路径**,比如 `[标题](knowledge/<category>/<slug>.md)`。页面之间互相链接才用相对路径
- **基于知识页回答问题时附上链接**,方便深入查阅

View File

@@ -0,0 +1,180 @@
---
title: skill-creator - 技能创建
description: 创建、安装、更新技能,规范 SKILL.md 写法与目录结构
---
`skill-creator` 是一个「元技能」,专门用来帮助 Agent 创建、安装和更新其他技能,确保所有技能的 `SKILL.md` 写法和目录结构保持一致。
## 什么时候会触发
- 用户想从 URL 或远程仓库安装一个技能
- 用户想从头创建一个全新的技能
- 需要升级或重构已有技能
## 技能是什么
简单来说,技能就是一份「可复用的说明书」加上可选的脚本和资源。它给 Agent 注入了某个领域的专业知识,让 Agent 在遇到对应任务时能像专家一样处理。
一个技能通常包含以下内容:
1. **专项工作流** — 某类任务的完整步骤
2. **工具用法** — 怎么调某种 API 或处理某种文件
3. **领域知识** — 团队约定、业务规则、数据结构之类
4. **附带资源** — 脚本、参考文档、模板等
<Note>
**核心原则:能省则省**。只写 Agent 自己想不到的内容,每加一行都要问自己:值不值得占这些 token
</Note>
## 目录结构
```
skill-name/
├── SKILL.md # 必需:技能定义
│ ├── YAML frontmatter必填 name / description
│ └── Markdown 正文(说明 + 示例)
└── 可选资源
├── scripts/ # 可执行脚本Python / Bash 等)
├── references/ # 内容较多的参考文档Agent 按需读取
└── assets/ # 模板、图标等,会直接用在输出里
```
## SKILL.md 规范定义
SKILL.md 文件头部的 `frontmatter` 字段:
| 字段 | 说明 |
| --- | --- |
| `name` | 技能名,小写加中划线,必须和目录名一致 |
| `description` | **最关键的字段**。写清楚「这个技能干什么」和「什么情况下该用它」Agent 看到这段来决定要不要调它。注意:所有触发相关的描述都放在这里,不要写到正文里 |
| `metadata.cowagent.requires.bins` | 系统里必须装了哪些命令行工具 |
| `metadata.cowagent.requires.env` | 需要哪些环境变量(全部满足才行) |
| `metadata.cowagent.requires.anyEnv` | 多个 API Key 满足一个就行 |
| `metadata.cowagent.requires.anyBins` | 多个工具满足一个就行 |
| `metadata.cowagent.always` | 设为 `true` 会始终加载,不检查依赖 |
| `metadata.cowagent.emoji` | 展示用的 emoji可选 |
| `metadata.cowagent.os` | 限定系统,如 `["darwin", "linux"]` |
<Note>
`category` 字段不需要手写,系统会自动设成 `skill`。
</Note>
声明 API Key 依赖有两种写法:
```yaml
metadata:
cowagent:
requires:
env: ["MYAPI_KEY"] # 必须有
```
```yaml
metadata:
cowagent:
requires:
anyEnv: ["OPENAI_API_KEY", "LINKAI_API_KEY"] # 有一个就行
```
**技能会自动按依赖启禁用**:环境变量齐了就自动启用,缺了就自动禁用,不需要手动 `/skill enable`。
## 资源目录怎么用
| 目录 | 放什么 | 不要放 |
| --- | --- | --- |
| `scripts/` | 需要反复执行的代码,或需要确定性结果的脚本 | 纯演示用的代码片段 |
| `references/` | **超过 500 行**、SKILL.md 实在塞不下的大文档(比如完整的数据库 Schema | 普通 API 文档、示例、教程 |
| `assets/` | 会出现在最终产物里的文件(模板、图标、样板代码等) | 说明性文档 |
<Warning>
**原则上所有内容都写在 `SKILL.md` 里**,只有确实放不下才拆到资源目录。
不要给技能加 `README.md`、`CHANGELOG.md`、`INSTALLATION_GUIDE.md` 之类的文件——全部放进 `SKILL.md`。资源目录里只放真正要跑的脚本或真正要用的素材。
</Warning>
## 安装外部技能
安装后最终落在 `<workspace>/skills/<name>/` 目录。
| 来源 | 怎么装 |
| --- | --- |
| URL单文件 | curl / web_fetch 直接拉 |
| URLzip 包) | 下载解压 |
| 本地 SKILL.md | 直接读 |
| 本地 zip 包 | 解压 |
安装步骤:
1. 找到 `SKILL.md`(可能在包的根目录或某个子目录里)
2. 从 frontmatter 里读出 `name`
3. 把**整个技能目录**(包括 `SKILL.md`、`scripts/`、`assets/` 等)复制到 `<workspace>/skills/<name>/`
4. 如果包里有 `INSTALL.md` 之类的安装脚本,照着跑一遍,但最终结果仍然要落在 `<workspace>/skills/<name>/` 下
## 从头创建技能
推荐按这个顺序来:
1. **搞清楚需求** — 让用户举几个具体的使用场景,一次别问太多
2. **想好结构** — 这个技能需要脚本吗?需要参考文档吗?需要模板素材吗?
3. **生成骨架** — 用初始化脚本:
```bash
scripts/init_skill.py <skill-name> --path <workspace>/skills [--resources scripts,references,assets] [--examples]
```
4. **填充内容** — 写好 SKILL.md、补上脚本和资源。脚本写完一定要实际跑一遍
5. **格式校验**(可选):
```bash
scripts/quick_validate.py <workspace>/skills/<skill-name>
```
6. **迭代完善** — 实际用起来之后根据反馈持续改进
## 命名规则
- 只用小写字母、数字和中划线。用户给的名字需要做标准化处理,比如 `Plan Mode` → `plan-mode`
- 长度别超过 64 个字符
- 尽量短、用动词开头、一看就知道干什么
- 必要时用工具名做前缀,比如 `gh-address-comments`、`linear-address-issue`
- 目录名和 `name` 字段必须完全一致
## 三级加载机制
技能不会一次性全部塞进上下文,而是分三级按需加载:
1. **元信息**`name` + `description`)— 常驻上下文,约 100 词。Agent 靠它判断「要不要用这个技能」
2. **SKILL.md 正文** — 确定要用了才加载,建议控制在 500 行以内
3. **资源文件** — Agent 需要的时候再读
如果一个技能涉及多个变体(比如多云厂商部署),建议这样组织:
```
cloud-deploy/
├── SKILL.md # 主流程和厂商选择逻辑
└── references/
├── aws.md
├── gcp.md
└── azure.md
```
用户选了 AWSAgent 只需要读 `aws.md`,不用把三家的文档全加载进来。
## 常见设计模式
**步骤式**:按编号列出操作步骤和对应脚本。
```markdown
1. 分析表单结构(运行 analyze_form.py
2. 生成字段映射(编辑 fields.json
3. 自动填充表单(运行 fill_form.py
```
**分支式**:根据用户意图走不同流程。
```markdown
1. 判断操作类型:
**新建内容?** → 走「创建流程」
**编辑已有内容?** → 走「编辑流程」
```
**模板式**:输出格式有严格要求时,在 SKILL.md 里直接给一个样板,让 Agent 照着写。

Some files were not shown because too many files have changed in this diff Show More