mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 18:17:11 +08:00
Compare commits
120 Commits
feat-multi
...
2.0.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55aaf60a57 | ||
|
|
a5790d82f6 | ||
|
|
63f99af1e6 | ||
|
|
4eed2568aa | ||
|
|
fb7962c7f2 | ||
|
|
76e6b7b471 | ||
|
|
fccb7ff9ed | ||
|
|
3b12ef2e66 | ||
|
|
f9d099be1b | ||
|
|
c322c0e3a5 | ||
|
|
530fc20596 | ||
|
|
a23b4ed754 | ||
|
|
fc4f5077b0 | ||
|
|
6a553886da | ||
|
|
1065c7e722 | ||
|
|
a9c8a59f58 | ||
|
|
8730f7fd27 | ||
|
|
8f608223d7 | ||
|
|
a7cbd47a2f | ||
|
|
b80c3fe5a8 | ||
|
|
5080051e39 | ||
|
|
23bfc8d0ba | ||
|
|
80e9062041 | ||
|
|
67bd3420ed | ||
|
|
aea081703f | ||
|
|
f300d2a2d5 | ||
|
|
f150d7d83a | ||
|
|
4d1f059c0d | ||
|
|
bc7f953fcc | ||
|
|
f653483eea | ||
|
|
6b200fd36b | ||
|
|
161fc6cdf0 | ||
|
|
6f68ed6bce | ||
|
|
a4592ffdfe | ||
|
|
7cd7bd1a48 | ||
|
|
9eeca70292 | ||
|
|
02bfe30848 | ||
|
|
c9c99de3d9 | ||
|
|
8752f0cc60 | ||
|
|
5c65196e44 | ||
|
|
f5798bfe90 | ||
|
|
0e556b3468 | ||
|
|
31820f56e7 | ||
|
|
fd88828abd | ||
|
|
ae11159918 | ||
|
|
472a8605c0 | ||
|
|
e1760ba211 | ||
|
|
ce4c0a0aa4 | ||
|
|
64511593c4 | ||
|
|
b0e00dfceb | ||
|
|
fc465b463d | ||
|
|
68ce2e5232 | ||
|
|
81e8bb62ae | ||
|
|
2c13e1b923 | ||
|
|
a0748c2e3b | ||
|
|
40599bb751 | ||
|
|
f3c64ceea7 | ||
|
|
15c60de709 | ||
|
|
6dd316547f | ||
|
|
54c7676a44 | ||
|
|
d25b8966ce | ||
|
|
14a119c48c | ||
|
|
c82515a927 | ||
|
|
26e630c2dd | ||
|
|
13370d2056 | ||
|
|
35282db9e0 | ||
|
|
426fb88ce7 | ||
|
|
2384bd0e10 | ||
|
|
ba3f66d3d1 | ||
|
|
7293a0f670 | ||
|
|
9e86d46267 | ||
|
|
848430f062 | ||
|
|
abd21335c4 | ||
|
|
8fa95f058a | ||
|
|
d4e5ecd497 | ||
|
|
3830f76729 | ||
|
|
83f778fec9 | ||
|
|
cabd24605f | ||
|
|
ae20ba1148 | ||
|
|
3a50b64977 | ||
|
|
8692e74536 | ||
|
|
1c18bd9889 | ||
|
|
60e9d98d0a | ||
|
|
83f6625e0c | ||
|
|
acc09543b7 | ||
|
|
94d8c7e366 | ||
|
|
ea1a0c8b3d | ||
|
|
7bc88c17e4 | ||
|
|
33cf1bc4c3 | ||
|
|
9402e63fe1 | ||
|
|
90e4d494b2 | ||
|
|
da97e948ca | ||
|
|
89a07e8e74 | ||
|
|
3f3d0381e5 | ||
|
|
3649499dba | ||
|
|
a989d088fd | ||
|
|
f79a915136 | ||
|
|
12e8c3d449 | ||
|
|
4f7064575e | ||
|
|
070df826f1 | ||
|
|
fbe48a4b4e | ||
|
|
4dd497fb6d | ||
|
|
907882c0a7 | ||
|
|
d36d5aee3f | ||
|
|
c6824e5f5e | ||
|
|
199c21eede | ||
|
|
5162da5654 | ||
|
|
a1d82f6193 | ||
|
|
ea78e3d0c6 | ||
|
|
3497f00cb4 | ||
|
|
5355d45031 | ||
|
|
76e9fef3b2 | ||
|
|
c34308cbd4 | ||
|
|
5a10476010 | ||
|
|
46e80dceec | ||
|
|
90d1835353 | ||
|
|
845fadd0aa | ||
|
|
5748ded52c | ||
|
|
6a737fb734 | ||
|
|
54e81aba11 |
11
.github/workflows/deploy-image-arm.yml
vendored
11
.github/workflows/deploy-image-arm.yml
vendored
@@ -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
|
||||
|
||||
13
.github/workflows/deploy-image.yml
vendored
13
.github/workflows/deploy-image.yml
vendored
@@ -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
|
||||
|
||||
456
README.md
456
README.md
@@ -1,13 +1,13 @@
|
||||
<p align="center"><img src= "https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="Chatgpt-on-Wechat" width="550" /></p>
|
||||
<p align="center"><img src= "https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="550" /></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/chatgpt-on-wechat" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/chatgpt-on-wechat" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
|
||||
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/CowAgent" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square" alt="Stars"></a> <br/>
|
||||
[中文] | [<a href="docs/en/README.md">English</a>] | [<a href="docs/ja/README.md">日本語</a>]
|
||||
</p>
|
||||
|
||||
**CowAgent** 是基于大模型的超级 AI 助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行 Skills、拥有长期记忆并不断成长,比 OpenClaw 更轻量和便捷。CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入微信、飞书、钉钉、企微智能机器人、QQ、企微自建应用、微信公众号、网页中使用,7*24小时运行于你的个人电脑或服务器中。
|
||||
**CowAgent** 是基于大模型的超级 AI 助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行 Skills、拥有长期记忆和知识库并不断成长,比 OpenClaw 更轻量和便捷。CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入微信、飞书、钉钉、企微智能机器人、QQ、企微自建应用、微信公众号、网页中使用,7*24小时运行于你的个人电脑或服务器中。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cowagent.ai/">🌐 官网</a> ·
|
||||
@@ -23,12 +23,13 @@
|
||||
> 该项目既是一个可以开箱即用的超级 AI 助理,也是一个支持高扩展的 Agent 框架,可以通过为项目扩展大模型接口、接入渠道、内置工具、Skills 系统来灵活实现各种定制需求。核心能力如下:
|
||||
|
||||
- ✅ **自主任务规划**:能够理解复杂任务并自主规划执行,持续思考和调用工具直到完成目标
|
||||
- ✅ **长期记忆:** 自动将对话记忆持久化至本地文件和数据库中,包括核心记忆和日级记忆,支持关键词及向量检索
|
||||
- ✅ **长期记忆:** 自动将对话记忆持久化至本地文件和数据库中,包括核心记忆、日级记忆和梦境蒸馏,支持关键词及向量检索
|
||||
- ✅ **个人知识库:** 自动整理结构化知识,通过交叉引用构建知识图谱,支持通过对话管理和可视化浏览知识库
|
||||
- ✅ **技能系统:** Skills 安装和运行的引擎,支持从 [Skill Hub](https://skills.cowagent.ai/)、GitHub 等一键安装技能,或通过对话创造 Skills
|
||||
- ✅ **工具系统:** 内置文件读写、终端执行、浏览器操作、定时任务等工具,Agent 自主调用以完成复杂任务
|
||||
- ✅ **CLI系统:** 提供终端命令和对话命令,支持进程管理、技能安装、配置修改等操作
|
||||
- ✅ **多模态消息:** 支持对文本、图片、语音、文件等多类型消息进行解析、处理、生成、发送等操作
|
||||
- ✅ **多模型支持:** 支持 OpenAI, Claude, Gemini, DeepSeek, MiniMax、GLM、Qwen、Kimi、Doubao 等国内外主流模型厂商
|
||||
- ✅ **多模型支持:** 支持 DeepSeek、MiniMax、Claude、Gemini、OpenAI、GLM、Qwen、Doubao、Kimi 等国内外主流模型厂商
|
||||
- ✅ **多通道接入:** 支持运行在本地计算机或服务器,可集成到微信、飞书、钉钉、企业微信、QQ、微信公众号、网页中使用
|
||||
|
||||
## 声明
|
||||
@@ -69,17 +70,23 @@
|
||||
|
||||
# 🏷 更新日志
|
||||
|
||||
>**2026.04.01:** [2.0.5版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.5),Cow CLI 命令系统、Skill Hub 开源、浏览器工具、企微扫码创建、多项优化和修复。
|
||||
>**2026.05.06:** [2.0.8版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.8),飞书渠道全面升级(语音、流式输出和Markdown、一键扫码接入)、新模型支持(DeepSeek V4、百度千帆)、定时任务工具增强等
|
||||
|
||||
>**2026.03.22:** [2.0.4版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.4),新增个人微信通道(微信扫码即用)、新增 MiniMax-M2.7 和 GLM-5-Turbo 模型、run.sh 脚本重构、日文文档及多项修复。
|
||||
>**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.03.18:** [2.0.3版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.3),新增企微智能机器人和 QQ 通道、支持 Coding Plan、新增多个模型、Web 端文件处理、记忆系统升级。
|
||||
>**2026.04.14:** [2.0.6版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.6),知识库系统、梦境记忆模块、上下文智能压缩、Web 控制台多会话及多项优化。
|
||||
|
||||
>**2026.02.27:** [2.0.2版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.2),Web 控制台全面升级(流式对话、模型/技能/记忆/通道/定时任务/日志管理)、支持多通道同时运行、会话持久化存储、新增多个模型。
|
||||
>**2026.04.01:** [2.0.5版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.5),Cow CLI 命令系统、Skill Hub 开源、浏览器工具、企微扫码创建、多项优化和修复。
|
||||
|
||||
>**2026.02.13:** [2.0.1版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.1),内置 Web Search 工具、智能上下文裁剪策略、运行时信息动态更新、Windows 兼容性适配,修复定时任务记忆丢失、飞书连接等多项问题。
|
||||
>**2026.03.22:** [2.0.4版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.4),新增个人微信通道(微信扫码即用)、新增 MiniMax-M2.7 和 GLM-5-Turbo 模型、run.sh 脚本重构、日文文档及多项修复。
|
||||
|
||||
>**2026.02.03:** [2.0.0版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0),正式升级为超级 Agent 助理,支持多轮任务决策、具备长期记忆、实现多种系统工具、支持 Skills 框架,新增多种模型并优化了接入渠道。
|
||||
>**2026.03.18:** [2.0.3版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.3),新增企微智能机器人和 QQ 通道、支持 Coding Plan、新增多个模型、Web 端文件处理、记忆系统升级。
|
||||
|
||||
>**2026.02.27:** [2.0.2版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.2),Web 控制台全面升级(流式对话、模型/技能/记忆/通道/定时任务/日志管理)、支持多通道同时运行、会话持久化存储、新增多个模型。
|
||||
|
||||
>**2026.02.13:** [2.0.1版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.1),内置 Web Search 工具、智能上下文裁剪策略、运行时信息动态更新、Windows 兼容性适配,修复定时任务记忆丢失、飞书连接等多项问题。
|
||||
|
||||
>**2026.02.03:** [2.0.0版本](https://github.com/zhayujie/CowAgent/releases/tag/2.0.0),正式升级为超级 Agent 助理,支持多轮任务决策、具备长期记忆、实现多种系统工具、支持 Skills 框架,新增多种模型并优化了接入渠道。
|
||||
|
||||
更多更新历史请查看: [更新日志](https://docs.cowagent.ai/releases)
|
||||
|
||||
@@ -110,24 +117,24 @@ 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 模式下推荐使用以下模型,可根据效果及成本综合选择:deepseek-v4-flash、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、ernie-5.0
|
||||
|
||||
同时支持使用 **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 环境和下载源码,可直接快进到下一节。
|
||||
|
||||
**(1) 克隆项目代码:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
git clone https://github.com/zhayujie/CowAgent
|
||||
cd CowAgent/
|
||||
```
|
||||
|
||||
若遇到网络问题可使用国内仓库地址:https://gitee.com/zhayujie/chatgpt-on-wechat
|
||||
若遇到网络问题可使用国内仓库地址:https://gitee.com/zhayujie/CowAgent
|
||||
|
||||
**(2) 安装核心依赖 (必选):**
|
||||
|
||||
@@ -177,7 +184,9 @@ cow install-browser
|
||||
# config.json 文件内容示例
|
||||
{
|
||||
"channel_type": "weixin", # 接入渠道类型,默认为 weixin, 支持修改为 feishu,dingtalk,wecom_bot,qq,wechatcom_app,wechatmp_service,wechatmp,terminal
|
||||
"model": "MiniMax-M2.7", # 模型名称
|
||||
"model": "deepseek-v4-flash", # 模型名称
|
||||
"deepseek_api_key": "", # DeepSeek API Key
|
||||
"deepseek_api_base": "https://api.deepseek.com/v1", # DeepSeek API 地址
|
||||
"minimax_api_key": "", # MiniMax API Key
|
||||
"zhipu_ai_api_key": "", # 智谱 GLM API Key
|
||||
"moonshot_api_key": "", # Kimi/Moonshot API Key
|
||||
@@ -187,8 +196,6 @@ cow install-browser
|
||||
"claude_api_base": "https://api.anthropic.com/v1", # Claude API 地址,修改可接入三方代理平台
|
||||
"gemini_api_key": "", # Gemini API Key
|
||||
"gemini_api_base": "https://generativelanguage.googleapis.com", # Gemini API 地址
|
||||
"deepseek_api_key": "", # DeepSeek API Key
|
||||
"deepseek_api_base": "https://api.deepseek.com/v1", # DeepSeek API 地址,可修改为第三方代理
|
||||
"open_ai_api_key": "", # OpenAI API Key
|
||||
"open_ai_api_base": "https://api.openai.com/v1", # OpenAI API 地址
|
||||
"linkai_api_key": "", # LinkAI API Key
|
||||
@@ -197,11 +204,13 @@ cow install-browser
|
||||
"group_speech_recognition": false, # 是否开启群组语音识别
|
||||
"voice_reply_voice": false, # 是否使用语音回复语音
|
||||
"use_linkai": false, # 是否使用 LinkAI 接口,默认关闭,设置为 true 后可对接 LinkAI 平台模型
|
||||
"web_password": "", # Web 控制台访问密码,留空则不启用密码保护
|
||||
"agent": true, # 是否启用 Agent 模式,启用后拥有多轮工具决策、长期记忆、Skills 能力等
|
||||
"agent_workspace": "~/cow", # Agent 的工作空间路径,用于存储 memory、skills、系统设定等
|
||||
"agent_max_context_tokens": 40000, # Agent 模式下最大上下文 tokens,超出将自动丢弃最早的上下文
|
||||
"agent_max_context_turns": 30, # Agent 模式下最大上下文记忆轮次,每轮包括一次用户提问和 AI 回复
|
||||
"agent_max_steps": 15 # Agent 模式下单次任务的最大决策步数,超出后将停止继续调用工具
|
||||
"agent_max_context_tokens": 50000, # Agent 模式下最大上下文 tokens,超出将自动智能压缩处理
|
||||
"agent_max_context_turns": 20, # Agent 模式下最大上下文记忆轮次,一问一答为一轮,超出后智能压缩处理
|
||||
"agent_max_steps": 20, # Agent 模式下单次任务的最大决策步数,超出后将停止继续调用工具
|
||||
"enable_thinking": false # 是否启用深度思考模式
|
||||
}
|
||||
```
|
||||
|
||||
@@ -213,12 +222,13 @@ cow install-browser
|
||||
+ 添加 `"speech_recognition": true` 将开启语音识别,默认使用 openai 的 whisper 模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
|
||||
+ 添加 `"group_speech_recognition": true` 将开启群组语音识别,默认使用 openai 的 whisper 模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配 group_chat_prefix 和 group_chat_keyword, 支持语音触发画图);
|
||||
+ 添加 `"voice_reply_voice": true` 将开启语音回复语音(同时作用于私聊和群聊)
|
||||
+ 使用 MiniMax TTS:设置 `"text_to_voice": "minimax"`,并配置 `minimax_api_key`;可通过 `"tts_voice_id"` 指定发音人(如 `English_Graceful_Lady`),`"text_to_voice_model"` 指定模型(如 `speech-2.8-hd`、`speech-2.8-turbo`)
|
||||
</details>
|
||||
|
||||
<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/chatgpt-on-wechat/blob/master/common/const.py)文件
|
||||
+ `model`: 模型名称,Agent 模式下推荐使用 `deepseek-v4-flash`、`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>
|
||||
@@ -230,7 +240,7 @@ cow install-browser
|
||||
+ `linkai_api_key`: LinkAI Api Key,可在 [控制台](https://link-ai.tech/console/interface) 创建
|
||||
</details>
|
||||
|
||||
注:全部配置项说明可在 [`config.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py) 文件中查看。
|
||||
注:全部配置项说明可在 [`config.py`](https://github.com/zhayujie/CowAgent/blob/master/config.py) 文件中查看。
|
||||
|
||||
## 三、运行
|
||||
|
||||
@@ -305,6 +315,97 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
|
||||
推荐通过 Web 控制台在线管理模型配置,无需手动编辑文件,详见 [模型文档](https://docs.cowagent.ai/models)。以下是手动修改 `config.json` 配置模型的说明:
|
||||
|
||||
<details>
|
||||
<summary>DeepSeek</summary>
|
||||
|
||||
1. API Key 创建:在 [DeepSeek 平台](https://platform.deepseek.com/api_keys) 创建 API Key
|
||||
|
||||
2. 填写配置
|
||||
|
||||
方式一:官方接入(推荐):
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-v4-flash",
|
||||
"deepseek_api_key": "sk-xxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
- `model`: 推荐填写 `deepseek-v4-flash`、`deepseek-v4-pro`
|
||||
- `deepseek_api_key`: DeepSeek 平台的 API Key
|
||||
- `deepseek_api_base`: 可选,默认为 `https://api.deepseek.com/v1`,可修改为第三方代理地址
|
||||
|
||||
方式二:OpenAI 兼容方式接入:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-v4-flash",
|
||||
"bot_type": "openai",
|
||||
"open_ai_api_key": "sk-xxxxxxxxxxx",
|
||||
"open_ai_api_base": "https://api.deepseek.com/v1"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>MiniMax</summary>
|
||||
|
||||
方式一:官方接入,配置如下(推荐):
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 可填写 `MiniMax-M2.7、MiniMax-M2.7-highspeed、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2、abab6.5-chat` 等
|
||||
- `minimax_api_key`:MiniMax 平台的 API-KEY,在 [控制台](https://platform.minimaxi.com/user-center/basic-information/interface-key) 创建
|
||||
|
||||
方式二:OpenAI 兼容方式接入,配置如下:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.7",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `bot_type`: OpenAI 兼容方式
|
||||
- `model`: 可填 `MiniMax-M2.7、MiniMax-M2.7-highspeed、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2`,参考[API文档](https://platform.minimaxi.com/document/%E5%AF%B9%E8%AF%9D?key=66701d281d57f38758d581d0#QklxsNSbaf6kM4j6wjO5eEek)
|
||||
- `open_ai_api_base`: MiniMax 平台 API 的 BASE URL
|
||||
- `open_ai_api_key`: MiniMax 平台的 API-KEY
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Claude</summary>
|
||||
|
||||
1. API Key 创建:在 [Claude控制台](https://console.anthropic.com/settings/keys) 创建 API Key
|
||||
|
||||
2. 填写配置
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"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-7、claude-opus-4-6、claude-sonnet-4-5、claude-sonnet-4-0、claude-opus-4-0、claude-3-5-sonnet-latest` 等
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Gemini</summary>
|
||||
|
||||
API Key 创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn) 创建 API Key ,配置如下
|
||||
```json
|
||||
{
|
||||
"model": "gemini-3.1-flash-lite-preview",
|
||||
"gemini_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 参考[官方文档-模型列表](https://ai.google.dev/gemini-api/docs/models?hl=zh-cn),支持 `gemini-3.1-flash-lite-preview、gemini-3.1-pro-preview、gemini-3-flash-preview、gemini-3-pro-preview` 等
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>OpenAI</summary>
|
||||
|
||||
@@ -326,55 +427,6 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
- `bot_type`: 使用 OpenAI 相关模型时无需填写。当使用第三方代理接口接入 Claude 等非 OpenAI 官方模型时,该参数设为 `openai`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>LinkAI</summary>
|
||||
|
||||
1. API Key 创建:在 [LinkAI平台](https://link-ai.tech/console/interface) 创建 API Key
|
||||
|
||||
2. 填写配置
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-5.4-mini",
|
||||
"use_linkai": true,
|
||||
"linkai_api_key": "YOUR API KEY"
|
||||
}
|
||||
```
|
||||
|
||||
+ `use_linkai`: 是否使用 LinkAI 接口,默认关闭,设置为 true 后可对接 LinkAI 平台的模型,并使用知识库、工作流、数据库、插件等丰富的 Agent 技能
|
||||
+ `linkai_api_key`: LinkAI 平台的 API Key,可在 [控制台](https://link-ai.tech/console/interface) 中创建
|
||||
+ `model`: [模型列表](https://link-ai.tech/console/models)中的全部模型均可使用
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>MiniMax</summary>
|
||||
|
||||
方式一:官方接入,配置如下(推荐):
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 可填写 `MiniMax-M2.7、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2、abab6.5-chat` 等
|
||||
- `minimax_api_key`:MiniMax 平台的 API-KEY,在 [控制台](https://platform.minimaxi.com/user-center/basic-information/interface-key) 创建
|
||||
|
||||
方式二:OpenAI 兼容方式接入,配置如下:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.7",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `bot_type`: OpenAI 兼容方式
|
||||
- `model`: 可填 `MiniMax-M2.7、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2`,参考[API文档](https://platform.minimaxi.com/document/%E5%AF%B9%E8%AF%9D?key=66701d281d57f38758d581d0#QklxsNSbaf6kM4j6wjO5eEek)
|
||||
- `open_ai_api_base`: MiniMax 平台 API 的 BASE URL
|
||||
- `open_ai_api_key`: MiniMax 平台的 API-KEY
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>智谱AI (GLM)</summary>
|
||||
|
||||
@@ -382,24 +434,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>
|
||||
@@ -433,35 +485,6 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
- `open_ai_api_key`: 通义千问的 API-KEY
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Kimi (Moonshot)</summary>
|
||||
|
||||
方式一:官方接入,配置如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi-k2.5",
|
||||
"moonshot_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 可填写 `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",
|
||||
"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`
|
||||
- `open_ai_api_base`: Moonshot 的 BASE URL
|
||||
- `open_ai_api_key`: Moonshot 的 API-KEY
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>豆包 (Doubao)</summary>
|
||||
|
||||
@@ -481,67 +504,74 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Claude</summary>
|
||||
<summary>Kimi (Moonshot)</summary>
|
||||
|
||||
1. API Key 创建:在 [Claude控制台](https://console.anthropic.com/settings/keys) 创建 API Key
|
||||
方式一:官方接入,配置如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi-k2.6",
|
||||
"moonshot_api_key": ""
|
||||
}
|
||||
```
|
||||
- `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.6",
|
||||
"open_ai_api_base": "https://api.moonshot.cn/v1",
|
||||
"open_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `bot_type`: OpenAI 兼容方式
|
||||
- `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>
|
||||
|
||||
<details>
|
||||
<summary>ModelScope</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "modelscope",
|
||||
"model": "Qwen/QwQ-32B",
|
||||
"modelscope_api_key": "your_api_key",
|
||||
"modelscope_base_url": "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||
"text_to_image": "MusePublic/489_ckpt_FLUX_1"
|
||||
}
|
||||
```
|
||||
|
||||
- `bot_type`: modelscope 接口格式
|
||||
- `model`: 参考[模型列表](https://www.modelscope.cn/models?filter=inference_type&page=1)
|
||||
- `modelscope_api_key`: 参考 [官方文档-访问令牌](https://modelscope.cn/docs/accounts/token) ,在 [控制台](https://modelscope.cn/my/myaccesstoken)
|
||||
- `modelscope_base_url`: modelscope 平台的 BASE URL
|
||||
- `text_to_image`: 图像生成模型,参考[模型列表](https://www.modelscope.cn/models?filter=inference_type&page=1)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>LinkAI</summary>
|
||||
|
||||
1. API Key 创建:在 [LinkAI平台](https://link-ai.tech/console/interface) 创建 API Key
|
||||
|
||||
2. 填写配置
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"claude_api_key": "YOUR_API_KEY"
|
||||
"model": "gpt-5.4-mini",
|
||||
"use_linkai": true,
|
||||
"linkai_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` 等
|
||||
|
||||
+ `use_linkai`: 是否使用 LinkAI 接口,默认关闭,设置为 true 后可对接 LinkAI 平台的模型,并使用知识库、工作流、数据库、插件等丰富的 Agent 技能
|
||||
+ `linkai_api_key`: LinkAI 平台的 API Key,可在 [控制台](https://link-ai.tech/console/interface) 中创建
|
||||
+ `model`: [模型列表](https://link-ai.tech/console/models)中的全部模型均可使用
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Gemini</summary>
|
||||
|
||||
API Key 创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn) 创建 API Key ,配置如下
|
||||
```json
|
||||
{
|
||||
"model": "gemini-3.1-flash-lite-preview",
|
||||
"gemini_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 参考[官方文档-模型列表](https://ai.google.dev/gemini-api/docs/models?hl=zh-cn),支持 `gemini-3.1-flash-lite-preview、gemini-3.1-pro-preview、gemini-3-flash-preview、gemini-3-pro-preview` 等
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DeepSeek</summary>
|
||||
|
||||
1. API Key 创建:在 [DeepSeek 平台](https://platform.deepseek.com/api_keys) 创建 API Key
|
||||
|
||||
2. 填写配置
|
||||
|
||||
方式一:官方接入(推荐):
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"deepseek_api_key": "sk-xxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
- `model`: 可填 `deepseek-chat、deepseek-reasoner`,分别对应的是 DeepSeek-V3.2(非思考模式)和 DeepSeek-R1(思考模式)
|
||||
- `deepseek_api_key`: DeepSeek 平台的 API Key
|
||||
- `deepseek_api_base`: 可选,默认为 `https://api.deepseek.com/v1`,可修改为第三方代理地址
|
||||
|
||||
方式二:OpenAI 兼容方式接入:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"bot_type": "openai",
|
||||
"open_ai_api_key": "sk-xxxxxxxxxxx",
|
||||
"open_ai_api_base": "https://api.deepseek.com/v1"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Azure</summary>
|
||||
|
||||
@@ -569,33 +599,35 @@ API Key 创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>百度文心</summary>
|
||||
方式一:官方 SDK 接入,配置如下:
|
||||
<summary>百度千帆 / ERNIE</summary>
|
||||
|
||||
方式一:官方接入(推荐),配置如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "wenxin-4",
|
||||
"baidu_wenxin_api_key": "IajztZ0bDxgnP9bEykU7lBer",
|
||||
"baidu_wenxin_secret_key": "EDPZn6L24uAS9d8RWFfotK47dPvkjD6G"
|
||||
"model": "ernie-5.0",
|
||||
"qianfan_api_key": "",
|
||||
"qianfan_api_base": "https://qianfan.baidubce.com/v2"
|
||||
}
|
||||
```
|
||||
- `model`: 可填 `wenxin`和`wenxin-4`,对应模型为 文心-3.5 和 文心-4.0
|
||||
- `baidu_wenxin_api_key`:参考 [千帆平台-access_token鉴权](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/dlv4pct3s) 文档获取 API Key
|
||||
- `baidu_wenxin_secret_key`:参考 [千帆平台-access_token鉴权](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/dlv4pct3s) 文档获取 Secret Key
|
||||
|
||||
- `model`: 默认推荐填写 `ernie-5.0`(多模态,可直接识图),也可填写 `ernie-x1.1`、`ernie-4.5-turbo-128k`、`ernie-4.5-turbo-32k`;当主模型为纯文本 ERNIE 时,Vision 工具会自动 fallback 到 `ernie-4.5-turbo-vl`
|
||||
- `qianfan_api_key`: 百度千帆 API Key,通常以 `bce-v3/` 开头,可在百度智能云控制台创建
|
||||
- `qianfan_api_base`: 可选,默认为 `https://qianfan.baidubce.com/v2`
|
||||
|
||||
方式二:OpenAI 兼容方式接入,配置如下:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "ERNIE-4.0-Turbo-8K",
|
||||
"model": "ernie-5.0",
|
||||
"open_ai_api_base": "https://qianfan.baidubce.com/v2",
|
||||
"open_ai_api_key": "bce-v3/ALTxxxxxxd2b"
|
||||
"open_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `bot_type`: OpenAI 兼容方式
|
||||
- `model`: 支持官方所有模型,参考[模型列表](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Wm9cvy6rl)
|
||||
- `open_ai_api_base`: 百度文心 API 的 BASE URL
|
||||
- `open_ai_api_key`: 百度文心的 API-KEY,参考 [官方文档](https://cloud.baidu.com/doc/qianfan-api/s/ym9chdsy5) ,在 [控制台](https://console.bce.baidu.com/iam/#/iam/apikey/list) 创建 API Key
|
||||
- `model`: 支持千帆平台上的 ERNIE 模型
|
||||
- `open_ai_api_base`: 百度千帆 OpenAI 兼容 API 的 BASE URL
|
||||
- `open_ai_api_key`: 百度千帆 API Key
|
||||
|
||||
</details>
|
||||
|
||||
@@ -634,26 +666,6 @@ API Key 创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn
|
||||
- `open_ai_api_key`: 讯飞星火平台的[APIPassword](https://console.xfyun.cn/services/bm3) ,因模型而已
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ModelScope</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "modelscope",
|
||||
"model": "Qwen/QwQ-32B",
|
||||
"modelscope_api_key": "your_api_key",
|
||||
"modelscope_base_url": "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||
"text_to_image": "MusePublic/489_ckpt_FLUX_1"
|
||||
}
|
||||
```
|
||||
|
||||
- `bot_type`: modelscope 接口格式
|
||||
- `model`: 参考[模型列表](https://www.modelscope.cn/models?filter=inference_type&page=1)
|
||||
- `modelscope_api_key`: 参考 [官方文档-访问令牌](https://modelscope.cn/docs/accounts/token) ,在 [控制台](https://modelscope.cn/my/myaccesstoken)
|
||||
- `modelscope_base_url`: modelscope 平台的 BASE URL
|
||||
- `text_to_image`: 图像生成模型,参考[模型列表](https://www.modelscope.cn/models?filter=inference_type&page=1)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Coding Plan</summary>
|
||||
|
||||
@@ -708,6 +720,7 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
```
|
||||
|
||||
- `web_port`: 默认为 9899,可按需更改,需要服务器防火墙和安全组放行该端口
|
||||
- `web_password`: 访问密码,留空则不启用密码保护。部署在公网环境时建议设置
|
||||
- 如本地运行,启动后请访问 `http://localhost:9899/chat` ;如服务器运行,请访问 `http://ip:9899/chat`
|
||||
> 注:请将上述 url 中的 ip 或者 port 替换为实际的值
|
||||
</details>
|
||||
@@ -715,36 +728,26 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
<details>
|
||||
<summary>3. Feishu - 飞书</summary>
|
||||
|
||||
飞书支持两种事件接收模式:WebSocket 长连接(推荐)和 Webhook。
|
||||
飞书使用 WebSocket 长连接模式,无需公网 IP。详细步骤参考 [飞书接入](https://docs.cowagent.ai/channels/feishu)。
|
||||
|
||||
**方式一:WebSocket 模式(推荐,无需公网 IP)**
|
||||
**方式一:扫码一键创建(推荐)**
|
||||
|
||||
启动 Cow 后打开 Web 控制台,**通道** → **接入通道** → 选择 **飞书** → 扫码创建。也支持 CLI 启动时在终端打印二维码。
|
||||
|
||||
**方式二:手动配置**
|
||||
|
||||
在飞书开放平台创建自建应用并配置权限后,将凭据填入 `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "APP_ID",
|
||||
"feishu_app_secret": "APP_SECRET",
|
||||
"feishu_event_mode": "websocket"
|
||||
"feishu_stream_reply": true
|
||||
}
|
||||
```
|
||||
|
||||
**方式二:Webhook 模式(需要公网 IP)**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "APP_ID",
|
||||
"feishu_app_secret": "APP_SECRET",
|
||||
"feishu_token": "VERIFICATION_TOKEN",
|
||||
"feishu_event_mode": "webhook",
|
||||
"feishu_port": 9891
|
||||
}
|
||||
```
|
||||
|
||||
- `feishu_event_mode`: 事件接收模式,`websocket`(推荐)或 `webhook`
|
||||
- WebSocket 模式需安装依赖:`pip3 install lark-oapi`
|
||||
|
||||
详细步骤和参数说明参考 [飞书接入](https://docs.cowagent.ai/channels/feishu)
|
||||
- `feishu_stream_reply`:是否开启流式打字机回复,默认开启(需 `cardkit:card:write` 权限 + 飞书客户端 ≥ 7.20)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -766,7 +769,15 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
<details>
|
||||
<summary>5. WeCom Bot - 企微智能机器人</summary>
|
||||
|
||||
企微智能机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,配置简单:
|
||||
企微智能机器人使用 WebSocket 长连接模式,无需公网 IP 和域名。详细步骤参考 [企微智能机器人接入](https://docs.cowagent.ai/channels/wecom-bot)。
|
||||
|
||||
**方式一:扫码一键创建(推荐)**
|
||||
|
||||
启动 Cow 后打开 Web 控制台,**通道** → **接入通道** → 选择 **企微智能机器人** → 使用企业微信扫码创建。
|
||||
|
||||
**方式二:手动配置**
|
||||
|
||||
在企业微信中创建智能机器人并选择**长连接模式**,记录 Bot ID 和 Secret 后填入 `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -775,7 +786,6 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
"wecom_bot_secret": "YOUR_SECRET"
|
||||
}
|
||||
```
|
||||
详细步骤和参数说明参考 [企微智能机器人接入](https://docs.cowagent.ai/channels/wecom-bot)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -878,18 +888,28 @@ QQ 机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,支
|
||||
|
||||
# 🔎 常见问题
|
||||
|
||||
FAQs: <https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs>
|
||||
FAQs: <https://github.com/zhayujie/CowAgent/wiki/FAQs>
|
||||
|
||||
或直接在线咨询 [项目小助手](https://link-ai.tech/app/Kv2fXJcH) (知识库持续完善中,回复供参考)
|
||||
|
||||
# 🛠️ 开发
|
||||
|
||||
欢迎接入更多应用通道,参考 [飞书通道](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/channel/feishu/feishu_channel.py) 新增自定义通道,实现接收和发送消息逻辑即可完成接入。同时欢迎贡献新的 Skills,向 [Skill Hub](https://skills.cowagent.ai/submit) 提交技能。
|
||||
欢迎接入更多应用通道,参考 [飞书通道](https://github.com/zhayujie/CowAgent/blob/master/channel/feishu/feishu_channel.py) 新增自定义通道,实现接收和发送消息逻辑即可完成接入。同时欢迎贡献新的 Skills,向 [Skill Hub](https://skills.cowagent.ai/submit) 提交技能。
|
||||
|
||||
# ✉ 联系
|
||||
|
||||
欢迎提交PR、Issues进行反馈,以及通过 🌟Star 支持并关注项目更新。项目运行遇到问题可以查看 [常见问题列表](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) ,以及前往 [Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues) 中搜索。个人开发者可加入开源交流群参与更多讨论,企业用户可联系[产品客服](https://cdn.link-ai.tech/portal/linkai-customer-service.png)咨询。
|
||||
欢迎提交PR、Issues进行反馈,以及通过 🌟Star 支持并关注项目更新。项目运行遇到问题可以查看 [常见问题列表](https://github.com/zhayujie/CowAgent/wiki/FAQs) ,以及前往 [Issues](https://github.com/zhayujie/CowAgent/issues) 中搜索。个人开发者可加入开源交流群参与更多讨论,企业用户可联系[产品客服](https://cdn.link-ai.tech/portal/linkai-customer-service.png)咨询。
|
||||
|
||||
# 🌟 贡献者
|
||||
|
||||

|
||||

|
||||
|
||||
# 📌 项目更名说明
|
||||
|
||||
本项目原名 `chatgpt-on-wechat`(GitHub 原地址:https://github.com/zhayujie/chatgpt-on-wechat ),
|
||||
于 2026.04.13 正式更名为 **CowAgent**。GitHub 已自动设置重定向,原有链接仍可正常访问。
|
||||
|
||||
如需更新本地仓库的远程地址(可选):
|
||||
```bash
|
||||
git remote set-url origin https://github.com/zhayujie/CowAgent.git
|
||||
```
|
||||
|
||||
@@ -57,7 +57,16 @@ class ChatService:
|
||||
event_type = event.get("type")
|
||||
data = event.get("data", {})
|
||||
|
||||
if event_type == "message_update":
|
||||
if event_type == "reasoning_update":
|
||||
delta = data.get("delta", "")
|
||||
if delta:
|
||||
send_chunk_fn({
|
||||
"chunk_type": "reasoning",
|
||||
"delta": delta,
|
||||
"segment_id": state.segment_id,
|
||||
})
|
||||
|
||||
elif event_type == "message_update":
|
||||
# Incremental text delta
|
||||
delta = data.get("delta", "")
|
||||
if delta:
|
||||
|
||||
241
agent/chat/session_service.py
Normal file
241
agent/chat/session_service.py
Normal 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}
|
||||
0
agent/knowledge/__init__.py
Normal file
0
agent/knowledge/__init__.py
Normal file
240
agent/knowledge/service.py
Normal file
240
agent/knowledge/service.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Knowledge service for handling knowledge base operations.
|
||||
|
||||
Provides a unified interface for listing, reading, and graphing knowledge files,
|
||||
callable from the web console, API, or CLI.
|
||||
|
||||
Knowledge file layout (under workspace_root):
|
||||
knowledge/index.md
|
||||
knowledge/log.md
|
||||
knowledge/<category>/<slug>.md
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
|
||||
class KnowledgeService:
|
||||
"""
|
||||
High-level service for knowledge base queries.
|
||||
Operates directly on the filesystem.
|
||||
"""
|
||||
|
||||
def __init__(self, workspace_root: str):
|
||||
self.workspace_root = workspace_root
|
||||
self.knowledge_dir = os.path.join(workspace_root, "knowledge")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# list — directory tree with stats
|
||||
# ------------------------------------------------------------------
|
||||
def list_tree(self) -> dict:
|
||||
"""
|
||||
Return the knowledge directory tree grouped by category,
|
||||
supporting arbitrarily nested sub-directories.
|
||||
|
||||
Returns::
|
||||
|
||||
{
|
||||
"tree": [
|
||||
{
|
||||
"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
|
||||
}
|
||||
"""
|
||||
if not os.path.isdir(self.knowledge_dir):
|
||||
return {"tree": [], "stats": {"pages": 0, "size": 0}, "enabled": conf().get("knowledge", True)}
|
||||
|
||||
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": 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
|
||||
# ------------------------------------------------------------------
|
||||
def read_file(self, rel_path: str) -> dict:
|
||||
"""
|
||||
Read a single knowledge markdown file.
|
||||
|
||||
:param rel_path: Relative path within knowledge/, e.g. ``concepts/moe.md``
|
||||
:return: dict with ``content`` and ``path``
|
||||
:raises ValueError: if path is invalid or escapes knowledge dir
|
||||
:raises FileNotFoundError: if file does not exist
|
||||
"""
|
||||
if not rel_path or ".." in rel_path:
|
||||
raise ValueError("invalid path")
|
||||
|
||||
full_path = os.path.normpath(os.path.join(self.knowledge_dir, rel_path))
|
||||
allowed = os.path.normpath(self.knowledge_dir)
|
||||
if not full_path.startswith(allowed + os.sep) and full_path != allowed:
|
||||
raise ValueError("path outside knowledge dir")
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
raise FileNotFoundError(f"file not found: {rel_path}")
|
||||
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
return {"content": content, "path": rel_path}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# graph — nodes and links for visualization
|
||||
# ------------------------------------------------------------------
|
||||
def build_graph(self) -> dict:
|
||||
"""
|
||||
Parse all knowledge pages and extract cross-reference links.
|
||||
|
||||
Returns::
|
||||
|
||||
{
|
||||
"nodes": [
|
||||
{"id": "concepts/moe.md", "label": "MoE", "category": "concepts"},
|
||||
...
|
||||
],
|
||||
"links": [
|
||||
{"source": "concepts/moe.md", "target": "entities/deepseek.md"},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
knowledge_path = Path(self.knowledge_dir)
|
||||
if not knowledge_path.is_dir():
|
||||
return {"nodes": [], "links": []}
|
||||
|
||||
nodes = {}
|
||||
links = []
|
||||
link_re = re.compile(r'\[([^\]]*)\]\(([^)]+\.md)\)')
|
||||
|
||||
for md_file in knowledge_path.rglob("*.md"):
|
||||
rel = str(md_file.relative_to(knowledge_path))
|
||||
if rel in ("index.md", "log.md"):
|
||||
continue
|
||||
parts = rel.split("/")
|
||||
category = parts[0] if len(parts) > 1 else "root"
|
||||
title = md_file.stem.replace("-", " ").title()
|
||||
try:
|
||||
content = md_file.read_text(encoding="utf-8")
|
||||
first_line = content.strip().split("\n")[0]
|
||||
if first_line.startswith("# "):
|
||||
title = first_line[2:].strip()
|
||||
for _, link_target in link_re.findall(content):
|
||||
resolved = (md_file.parent / link_target).resolve()
|
||||
try:
|
||||
target_rel = str(resolved.relative_to(knowledge_path))
|
||||
except ValueError:
|
||||
continue
|
||||
if target_rel != rel:
|
||||
links.append({"source": rel, "target": target_rel})
|
||||
except Exception:
|
||||
pass
|
||||
nodes[rel] = {"id": rel, "label": title, "category": category}
|
||||
|
||||
valid_ids = set(nodes.keys())
|
||||
links = [l for l in links if l["source"] in valid_ids and l["target"] in valid_ids]
|
||||
seen = set()
|
||||
deduped = []
|
||||
for l in links:
|
||||
key = tuple(sorted([l["source"], l["target"]]))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
deduped.append(l)
|
||||
|
||||
return {"nodes": list(nodes.values()), "links": deduped}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# dispatch — single entry point for protocol messages
|
||||
# ------------------------------------------------------------------
|
||||
def dispatch(self, action: str, payload: Optional[dict] = None) -> dict:
|
||||
"""
|
||||
Dispatch a knowledge management action.
|
||||
|
||||
:param action: ``list``, ``read``, or ``graph``
|
||||
:param payload: action-specific payload
|
||||
:return: protocol-compatible response dict
|
||||
"""
|
||||
payload = payload or {}
|
||||
try:
|
||||
if action == "list":
|
||||
result = self.list_tree()
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result}
|
||||
|
||||
elif action == "read":
|
||||
path = payload.get("path")
|
||||
if not path:
|
||||
return {"action": action, "code": 400, "message": "path is required", "payload": None}
|
||||
result = self.read_file(path)
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result}
|
||||
|
||||
elif action == "graph":
|
||||
result = self.build_graph()
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result}
|
||||
|
||||
else:
|
||||
return {"action": action, "code": 400, "message": f"unknown action: {action}", "payload": None}
|
||||
|
||||
except ValueError as e:
|
||||
return {"action": action, "code": 403, "message": str(e), "payload": None}
|
||||
except FileNotFoundError as e:
|
||||
return {"action": action, "code": 404, "message": str(e), "payload": None}
|
||||
except Exception as e:
|
||||
logger.error(f"[KnowledgeService] dispatch error: action={action}, error={e}")
|
||||
return {"action": action, "code": 500, "message": str(e), "payload": None}
|
||||
@@ -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.
|
||||
@@ -188,8 +199,9 @@ def _group_into_display_turns(
|
||||
if text:
|
||||
turns.append({"role": "user", "content": text, "created_at": created_at})
|
||||
|
||||
# Collect all tool_calls and tool_results from the rest of the group
|
||||
all_tool_calls: List[Dict[str, Any]] = []
|
||||
# Build an ordered list of steps preserving the original sequence:
|
||||
# thinking → content → tool_call → content → ...
|
||||
steps: List[Dict[str, Any]] = []
|
||||
tool_results: Dict[str, str] = {}
|
||||
final_text = ""
|
||||
final_ts: Optional[int] = None
|
||||
@@ -198,24 +210,48 @@ def _group_into_display_turns(
|
||||
if role == "user":
|
||||
tool_results.update(_extract_tool_results(content))
|
||||
elif role == "assistant":
|
||||
tcs = _extract_tool_calls(content)
|
||||
all_tool_calls.extend(tcs)
|
||||
t = _extract_display_text(content)
|
||||
if t:
|
||||
final_text = t
|
||||
# Walk content blocks in order to preserve interleaving
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
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})
|
||||
elif btype == "text":
|
||||
txt = block.get("text", "").strip()
|
||||
if txt:
|
||||
steps.append({"type": "content", "content": txt})
|
||||
final_text = txt
|
||||
elif btype == "tool_use":
|
||||
steps.append({
|
||||
"type": "tool",
|
||||
"id": block.get("id", ""),
|
||||
"name": block.get("name", ""),
|
||||
"arguments": block.get("input", {}),
|
||||
})
|
||||
elif isinstance(content, str) and content.strip():
|
||||
steps.append({"type": "content", "content": content.strip()})
|
||||
final_text = content.strip()
|
||||
final_ts = created_at
|
||||
|
||||
# Attach tool results to their matching tool_call entries
|
||||
for tc in all_tool_calls:
|
||||
tc["result"] = tool_results.get(tc.get("id", ""), "")
|
||||
# Attach tool results to tool steps
|
||||
for step in steps:
|
||||
if step["type"] == "tool":
|
||||
step["result"] = tool_results.get(step.get("id", ""), "")
|
||||
|
||||
if final_text or all_tool_calls:
|
||||
turns.append({
|
||||
if steps or final_text:
|
||||
turn = {
|
||||
"role": "assistant",
|
||||
"content": final_text,
|
||||
"tool_calls": all_tool_calls,
|
||||
"steps": steps,
|
||||
"created_at": final_ts or (user_row[1] if user_row else 0),
|
||||
})
|
||||
}
|
||||
turns.append(turn)
|
||||
|
||||
return turns
|
||||
|
||||
@@ -264,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()
|
||||
@@ -279,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
|
||||
@@ -293,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:
|
||||
@@ -312,6 +346,9 @@ class ConversationStore:
|
||||
content = json.loads(raw_content)
|
||||
except Exception:
|
||||
content = raw_content
|
||||
# Strip thinking blocks — they are stored for UI display only
|
||||
if role == "assistant" and isinstance(content, list):
|
||||
content = [b for b in content if b.get("type") != "thinking"]
|
||||
result.append({"role": role, "content": content})
|
||||
return result
|
||||
|
||||
@@ -389,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()
|
||||
|
||||
@@ -407,9 +499,111 @@ class ConversationStore:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def prune_scheduled_messages(
|
||||
self,
|
||||
session_id: str,
|
||||
keep_last_n: int,
|
||||
markers: Optional[List[str]] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Keep at most ``keep_last_n`` scheduler-injected user/assistant pairs in
|
||||
the session, deleting the older ones.
|
||||
|
||||
A scheduler-injected pair is identified by a user message whose first
|
||||
text block starts with one of ``markers``; the immediately following
|
||||
assistant message (next seq) is treated as its paired output.
|
||||
|
||||
Only scheduler-tagged messages are touched; regular user turns are
|
||||
never deleted. Safe to call repeatedly; no-op if nothing to prune.
|
||||
|
||||
Args:
|
||||
session_id: Session to prune.
|
||||
keep_last_n: Maximum scheduler pairs to retain (must be >= 0).
|
||||
markers: Text prefixes that identify scheduler user messages.
|
||||
Defaults to ``["[SCHEDULED]", "Scheduled task"]`` so that
|
||||
pairs written by older versions are also recognised.
|
||||
|
||||
Returns:
|
||||
Number of message rows deleted.
|
||||
"""
|
||||
if keep_last_n < 0:
|
||||
keep_last_n = 0
|
||||
if markers is None:
|
||||
markers = ["[SCHEDULED]", "Scheduled task"]
|
||||
|
||||
def _matches_marker(raw_content: str) -> bool:
|
||||
try:
|
||||
parsed = json.loads(raw_content)
|
||||
except Exception:
|
||||
parsed = raw_content
|
||||
text = _extract_display_text(parsed) if not isinstance(parsed, str) else parsed
|
||||
if not text:
|
||||
return False
|
||||
return any(text.startswith(m) for m in markers)
|
||||
|
||||
with self._lock:
|
||||
conn = self._connect()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT seq, role, content
|
||||
FROM messages
|
||||
WHERE session_id = ?
|
||||
ORDER BY seq ASC
|
||||
""",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
|
||||
# Find scheduler pairs: each is (user_seq, assistant_seq?)
|
||||
pairs: List[tuple] = [] # list of (user_seq, assistant_seq_or_None)
|
||||
for idx, (seq, role, raw_content) in enumerate(rows):
|
||||
if role != "user" or not _matches_marker(raw_content):
|
||||
continue
|
||||
assistant_seq = None
|
||||
# Pair with the very next message if it's an assistant turn.
|
||||
if idx + 1 < len(rows):
|
||||
next_seq, next_role, _ = rows[idx + 1]
|
||||
if next_role == "assistant":
|
||||
assistant_seq = next_seq
|
||||
pairs.append((seq, assistant_seq))
|
||||
|
||||
if len(pairs) <= keep_last_n:
|
||||
return 0
|
||||
|
||||
to_delete_pairs = pairs[: len(pairs) - keep_last_n]
|
||||
seqs_to_delete: List[int] = []
|
||||
for user_seq, assistant_seq in to_delete_pairs:
|
||||
seqs_to_delete.append(user_seq)
|
||||
if assistant_seq is not None:
|
||||
seqs_to_delete.append(assistant_seq)
|
||||
|
||||
if not seqs_to_delete:
|
||||
return 0
|
||||
|
||||
placeholders = ",".join("?" * len(seqs_to_delete))
|
||||
with conn:
|
||||
conn.execute(
|
||||
f"DELETE FROM messages WHERE session_id = ? AND seq IN ({placeholders})",
|
||||
(session_id, *seqs_to_delete),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE sessions
|
||||
SET msg_count = (
|
||||
SELECT COUNT(*) FROM messages WHERE session_id = ?
|
||||
)
|
||||
WHERE session_id = ?
|
||||
""",
|
||||
(session_id, session_id),
|
||||
)
|
||||
return len(seqs_to_delete)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
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.
|
||||
@@ -433,7 +627,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:
|
||||
@@ -492,9 +687,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
|
||||
@@ -504,7 +705,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
|
||||
@@ -513,12 +745,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:
|
||||
@@ -573,6 +891,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)
|
||||
|
||||
@@ -285,6 +285,10 @@ class MemoryManager:
|
||||
# Scan memory directory (including daily summaries)
|
||||
if memory_dir.exists():
|
||||
for file_path in memory_dir.rglob("*.md"):
|
||||
# Skip hidden directories (e.g. .dreams/)
|
||||
if any(part.startswith('.') for part in file_path.relative_to(workspace_dir).parts):
|
||||
continue
|
||||
|
||||
# Determine scope and user_id from path
|
||||
rel_path = file_path.relative_to(workspace_dir)
|
||||
parts = rel_path.parts
|
||||
@@ -312,6 +316,14 @@ class MemoryManager:
|
||||
scope = "shared"
|
||||
|
||||
await self._sync_file(file_path, "memory", scope, user_id)
|
||||
|
||||
# Scan knowledge directory (structured knowledge wiki)
|
||||
from config import conf
|
||||
if conf().get("knowledge", True):
|
||||
knowledge_dir = Path(workspace_dir) / "knowledge"
|
||||
if knowledge_dir.exists():
|
||||
for file_path in knowledge_dir.rglob("*.md"):
|
||||
await self._sync_file(file_path, "knowledge", "shared", None)
|
||||
|
||||
self._dirty = False
|
||||
|
||||
@@ -389,24 +401,28 @@ class MemoryManager:
|
||||
user_id: Optional[str] = None,
|
||||
reason: str = "threshold",
|
||||
max_messages: int = 10,
|
||||
context_summary_callback=None,
|
||||
) -> bool:
|
||||
"""
|
||||
Flush conversation summary to daily memory file.
|
||||
|
||||
|
||||
Args:
|
||||
messages: Conversation message list
|
||||
user_id: Optional user ID
|
||||
reason: "threshold" | "overflow" | "daily_summary"
|
||||
max_messages: Max recent messages to include (0 = all)
|
||||
|
||||
context_summary_callback: Optional callback(str) invoked with the
|
||||
daily summary text for in-context injection
|
||||
|
||||
Returns:
|
||||
True if content was written
|
||||
True if flush was dispatched
|
||||
"""
|
||||
success = self.flush_manager.flush_from_messages(
|
||||
messages=messages,
|
||||
user_id=user_id,
|
||||
reason=reason,
|
||||
max_messages=max_messages,
|
||||
context_summary_callback=context_summary_callback,
|
||||
)
|
||||
if success:
|
||||
self._dirty = True
|
||||
|
||||
@@ -32,68 +32,80 @@ class MemoryService:
|
||||
# ------------------------------------------------------------------
|
||||
# list — paginated file metadata
|
||||
# ------------------------------------------------------------------
|
||||
def list_files(self, page: int = 1, page_size: int = 20) -> dict:
|
||||
def list_files(self, page: int = 1, page_size: int = 20, category: str = "memory") -> dict:
|
||||
"""
|
||||
List all memory files with metadata (without content).
|
||||
List memory or dream files with metadata (without content).
|
||||
|
||||
Returns::
|
||||
|
||||
{
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 15,
|
||||
"list": [
|
||||
{"filename": "MEMORY.md", "type": "global", "size": 2048, "updated_at": "2026-02-20 10:00:00"},
|
||||
{"filename": "2026-02-20.md", "type": "daily", "size": 512, "updated_at": "2026-02-20 09:30:00"},
|
||||
...
|
||||
]
|
||||
}
|
||||
Args:
|
||||
category: ``"memory"`` (default) — MEMORY.md + daily files;
|
||||
``"dream"`` — dream diary files from memory/dreams/
|
||||
"""
|
||||
if category == "dream":
|
||||
files = self._list_dream_files()
|
||||
else:
|
||||
files = self._list_memory_files()
|
||||
|
||||
total = len(files)
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
|
||||
return {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"list": files[start:end],
|
||||
}
|
||||
|
||||
def _list_memory_files(self) -> List[dict]:
|
||||
"""MEMORY.md + memory/*.md (newest first)."""
|
||||
files: List[dict] = []
|
||||
|
||||
# 1. Global memory — MEMORY.md in workspace root
|
||||
global_path = os.path.join(self.workspace_root, "MEMORY.md")
|
||||
if os.path.isfile(global_path):
|
||||
files.append(self._file_info(global_path, "MEMORY.md", "global"))
|
||||
|
||||
# 2. Daily memory files — memory/*.md (sorted newest first)
|
||||
if os.path.isdir(self.memory_dir):
|
||||
daily_files = []
|
||||
for name in os.listdir(self.memory_dir):
|
||||
full = os.path.join(self.memory_dir, name)
|
||||
if os.path.isfile(full) and name.endswith(".md"):
|
||||
daily_files.append((name, full))
|
||||
# Sort by filename descending (newest date first)
|
||||
daily_files.sort(key=lambda x: x[0], reverse=True)
|
||||
for name, full in daily_files:
|
||||
files.append(self._file_info(full, name, "daily"))
|
||||
|
||||
total = len(files)
|
||||
return files
|
||||
|
||||
# Paginate
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
page_items = files[start:end]
|
||||
def _list_dream_files(self) -> List[dict]:
|
||||
"""memory/dreams/*.md (newest first)."""
|
||||
files: List[dict] = []
|
||||
dreams_dir = os.path.join(self.memory_dir, "dreams")
|
||||
|
||||
return {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"list": page_items,
|
||||
}
|
||||
if os.path.isdir(dreams_dir):
|
||||
entries = []
|
||||
for name in os.listdir(dreams_dir):
|
||||
full = os.path.join(dreams_dir, name)
|
||||
if os.path.isfile(full) and name.endswith(".md"):
|
||||
entries.append((name, full))
|
||||
entries.sort(key=lambda x: x[0], reverse=True)
|
||||
for name, full in entries:
|
||||
files.append(self._file_info(full, name, "dream"))
|
||||
|
||||
return files
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# content — read a single file
|
||||
# ------------------------------------------------------------------
|
||||
def get_content(self, filename: str) -> dict:
|
||||
def get_content(self, filename: str, category: str = "memory") -> dict:
|
||||
"""
|
||||
Read the full content of a memory file.
|
||||
Read the full content of a memory or dream file.
|
||||
|
||||
:param filename: File name, e.g. ``MEMORY.md`` or ``2026-02-20.md``
|
||||
:param filename: File name, e.g. ``MEMORY.md``, ``2026-02-20.md``
|
||||
:param category: ``"memory"`` or ``"dream"``
|
||||
:return: dict with ``filename`` and ``content``
|
||||
:raises FileNotFoundError: if the file does not exist
|
||||
"""
|
||||
path = self._resolve_path(filename)
|
||||
path = self._resolve_path(filename, category)
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"Memory file not found: {filename}")
|
||||
|
||||
@@ -113,7 +125,7 @@ class MemoryService:
|
||||
Dispatch a memory management action.
|
||||
|
||||
:param action: ``list`` or ``content``
|
||||
:param payload: action-specific payload
|
||||
:param payload: action-specific payload (supports ``category``: ``"memory"`` | ``"dream"``)
|
||||
:return: protocol-compatible response dict
|
||||
"""
|
||||
payload = payload or {}
|
||||
@@ -121,14 +133,16 @@ class MemoryService:
|
||||
if action == "list":
|
||||
page = payload.get("page", 1)
|
||||
page_size = payload.get("page_size", 20)
|
||||
result_payload = self.list_files(page=page, page_size=page_size)
|
||||
category = payload.get("category", "memory")
|
||||
result_payload = self.list_files(page=page, page_size=page_size, category=category)
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result_payload}
|
||||
|
||||
elif action == "content":
|
||||
filename = payload.get("filename")
|
||||
if not filename:
|
||||
return {"action": action, "code": 400, "message": "filename is required", "payload": None}
|
||||
result_payload = self.get_content(filename)
|
||||
category = payload.get("category", "memory")
|
||||
result_payload = self.get_content(filename, category=category)
|
||||
return {"action": action, "code": 200, "message": "success", "payload": result_payload}
|
||||
|
||||
else:
|
||||
@@ -145,18 +159,20 @@ class MemoryService:
|
||||
# ------------------------------------------------------------------
|
||||
# internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _resolve_path(self, filename: str) -> str:
|
||||
def _resolve_path(self, filename: str, category: str = "memory") -> str:
|
||||
"""
|
||||
Safely resolve a filename to its absolute path within the allowed directory.
|
||||
|
||||
- ``MEMORY.md`` → ``{workspace_root}/MEMORY.md``
|
||||
- ``2026-02-20.md`` → ``{workspace_root}/memory/2026-02-20.md``
|
||||
- ``2026-02-20.md`` (memory) → ``{workspace_root}/memory/2026-02-20.md``
|
||||
- ``2026-02-20.md`` (dream) → ``{workspace_root}/memory/dreams/2026-02-20.md``
|
||||
|
||||
Raises ValueError if the resolved path escapes the allowed directory
|
||||
(path traversal protection).
|
||||
Raises ValueError if the resolved path escapes the allowed directory.
|
||||
"""
|
||||
if filename == "MEMORY.md":
|
||||
base_dir = self.workspace_root
|
||||
elif category == "dream":
|
||||
base_dir = os.path.join(self.memory_dir, "dreams")
|
||||
else:
|
||||
base_dir = self.memory_dir
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""
|
||||
Memory flush manager
|
||||
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)
|
||||
- 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
|
||||
@@ -16,29 +16,79 @@ from datetime import datetime
|
||||
from common.log import logger
|
||||
|
||||
|
||||
SUMMARIZE_SYSTEM_PROMPT = """你是一个记忆提取助手。你的任务是从对话记录中提炼出值得长期记住的关键事件和核心信息。
|
||||
SUMMARIZE_SYSTEM_PROMPT = """你是一个对话记录助手。请将对话内容归纳为当天的日常记录。
|
||||
|
||||
核心原则:
|
||||
- 按「事件」维度归纳,而不是按对话轮次逐条记录
|
||||
- 多轮对话如果围绕同一件事,合并为一条摘要
|
||||
- 只记录有长期价值的信息,忽略闲聊、问候、无意义的短消息
|
||||
## 要求
|
||||
|
||||
输出要求:
|
||||
1. 每条一行,用 "- " 开头,格式为:事件/主题 + 关键结论或结果
|
||||
2. 值得记录的信息类型:用户提出的需求及最终解决方案、重要的事实信息、用户的偏好或决策、关键技术方案或配置变更
|
||||
3. 不值得记录的信息:简单问候、闲聊、无实质内容的短消息、重复的中间过程
|
||||
4. 每条摘要应当简明扼要,一句话概括事件的核心内容和结果
|
||||
5. 直接输出摘要内容,不要加任何前缀说明
|
||||
6. 当对话没有任何记录价值(仅含问候或无意义内容),回复"无"
|
||||
按「事件」维度归纳发生的事,不要按对话轮次逐条记录:
|
||||
- 每条一行,用 "- " 开头
|
||||
- 合并同一件事的多轮对话
|
||||
- 只记录有意义的事件,忽略闲聊和问候
|
||||
- 保留关键的决策、结论和待办事项
|
||||
|
||||
示例(仅供参考格式):
|
||||
- 用户配置了 XX 功能,设置参数为 YY,已生效
|
||||
- 用户反馈了 XX 问题,原因是 YY,通过 ZZ 方式解决"""
|
||||
当对话没有任何记录价值(仅含问候或无意义内容),直接回复"无"。"""
|
||||
|
||||
SUMMARIZE_USER_PROMPT = """请从以下对话记录中,按关键事件维度提炼记忆摘要(合并同一事件的多轮对话,不要逐条列出):
|
||||
SUMMARIZE_USER_PROMPT = """请归纳以下对话的日常记录:
|
||||
|
||||
{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])
|
||||
|
||||
用简洁的叙事风格写一篇短日记,记录这次整理的发现,保持格式美观易读:
|
||||
- 发现了哪些重复或矛盾
|
||||
- 从日记中提取了什么新洞察
|
||||
- 做了哪些清理和优化
|
||||
- 整体感受和观察
|
||||
|
||||
## 输出格式(严格遵守)
|
||||
|
||||
```
|
||||
[MEMORY]
|
||||
- 记忆条目1
|
||||
- 记忆条目2
|
||||
...
|
||||
|
||||
[DREAM]
|
||||
梦境日记内容...
|
||||
```"""
|
||||
|
||||
DREAM_USER_PROMPT = """## 当前长期记忆(MEMORY.md)
|
||||
|
||||
{memory_content}
|
||||
|
||||
## 近期日记(最近 {days} 天)
|
||||
|
||||
{daily_content}"""
|
||||
|
||||
|
||||
|
||||
class MemoryFlushManager:
|
||||
"""
|
||||
@@ -65,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 = "" # "{date}:{daily_hash}" of last dream, 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"""
|
||||
@@ -108,23 +160,30 @@ 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:
|
||||
# Strip scheduler-injected pairs before any further processing.
|
||||
# These messages already serve as short-term context inside the
|
||||
# receiver session; promoting them into long-term daily memory
|
||||
# produces low-value flat logs (e.g. "11:28 price=1013, normal /
|
||||
# 11:58 price=1013, normal / ...") and wastes summarisation tokens.
|
||||
messages = self._strip_scheduler_pairs(messages)
|
||||
if not messages:
|
||||
return False
|
||||
|
||||
import hashlib
|
||||
deduped = []
|
||||
for m in messages:
|
||||
@@ -137,18 +196,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
|
||||
@@ -159,41 +219,69 @@ class MemoryFlushManager:
|
||||
user_id: Optional[str],
|
||||
reason: str,
|
||||
max_messages: int,
|
||||
context_summary_callback: Optional[Callable[[str], None]] = None,
|
||||
):
|
||||
"""Background worker: summarize with LLM and write to daily file."""
|
||||
"""Background worker: summarize with LLM, write daily memory file."""
|
||||
try:
|
||||
summary = self._summarize_messages(messages, max_messages)
|
||||
if not summary or not summary.strip() or summary.strip() == "无":
|
||||
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
|
||||
|
||||
|
||||
# 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 ---
|
||||
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 = ""
|
||||
|
||||
flush_entry = f"\n{header}\n\n{note}{summary}\n"
|
||||
|
||||
|
||||
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')})")
|
||||
|
||||
with open(daily_file, "a", encoding="utf-8") as f:
|
||||
f.write(flush_entry)
|
||||
|
||||
f.write(f"\n{header}\n\n{daily_part}\n")
|
||||
|
||||
logger.info(f"[MemoryFlush] Wrote daily memory to {daily_file.name} (reason={reason}, chars={len(daily_part)})")
|
||||
|
||||
# --- 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()
|
||||
|
||||
logger.info(f"[MemoryFlush] Wrote to {daily_file.name} (reason={reason}, chars={len(summary)})")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[MemoryFlush] Async flush failed (reason={reason}): {e}")
|
||||
|
||||
|
||||
@staticmethod
|
||||
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 ""
|
||||
|
||||
# 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()
|
||||
|
||||
# Remove stray [MEMORY] section entirely
|
||||
if "[MEMORY]" in raw:
|
||||
raw = raw[:raw.index("[MEMORY]")].strip()
|
||||
|
||||
# Remove markdown code fences
|
||||
raw = raw.replace("```", "").strip()
|
||||
|
||||
return raw
|
||||
|
||||
def create_daily_summary(
|
||||
self,
|
||||
messages: List[Dict],
|
||||
@@ -219,12 +307,192 @@ 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 same daily content already dreamed today.
|
||||
# Note: only hash daily_content (not memory_content), because deep_dream
|
||||
# itself rewrites MEMORY.md as a side effect, which would otherwise
|
||||
# invalidate the hash on every subsequent call within the same window.
|
||||
import hashlib
|
||||
daily_hash = hashlib.md5(daily_content.encode("utf-8")).hexdigest()
|
||||
today_str = datetime.now().strftime("%Y-%m-%d")
|
||||
dedup_key = f"{today_str}:{daily_hash}"
|
||||
if not force and dedup_key == self._last_dream_input_hash:
|
||||
logger.info("[DeepDream] Already dreamed today with same daily content, skipping")
|
||||
return False
|
||||
self._last_dream_input_hash = dedup_key
|
||||
|
||||
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():
|
||||
@@ -235,13 +503,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."""
|
||||
@@ -259,6 +528,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
|
||||
@@ -272,27 +587,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:]
|
||||
|
||||
@@ -306,19 +625,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])
|
||||
|
||||
@@ -337,6 +656,40 @@ class MemoryFlushManager:
|
||||
return "\n".join(parts)
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def _strip_scheduler_pairs(cls, messages: List[Dict]) -> List[Dict]:
|
||||
"""Drop scheduler-injected user/assistant pairs from a flush batch.
|
||||
|
||||
A scheduler user message starts with the ``[SCHEDULED]`` marker
|
||||
(written by ``AgentBridge.remember_scheduled_output``); the message
|
||||
immediately following it (if it is an assistant turn) is its paired
|
||||
output and is dropped together. Regular user/assistant turns and
|
||||
any tool_use / tool_result blocks are preserved as-is.
|
||||
"""
|
||||
if not messages:
|
||||
return messages
|
||||
|
||||
SCHEDULED_PREFIX = "[SCHEDULED]"
|
||||
result = []
|
||||
skip_next_assistant = False
|
||||
for msg in messages:
|
||||
if not isinstance(msg, dict):
|
||||
result.append(msg)
|
||||
skip_next_assistant = False
|
||||
continue
|
||||
role = msg.get("role")
|
||||
if skip_next_assistant and role == "assistant":
|
||||
skip_next_assistant = False
|
||||
continue
|
||||
skip_next_assistant = False
|
||||
if role == "user":
|
||||
text = cls._extract_text_from_content(msg.get("content", ""))
|
||||
if text.lstrip().startswith(SCHEDULED_PREFIX):
|
||||
skip_next_assistant = True
|
||||
continue
|
||||
result.append(msg)
|
||||
return result
|
||||
|
||||
|
||||
def create_memory_files_if_needed(workspace_dir: Path, user_id: Optional[str] = None):
|
||||
"""
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -92,10 +93,11 @@ def build_agent_system_prompt(
|
||||
顺序说明(按重要性和逻辑关系排列):
|
||||
1. 工具系统 - 核心能力,最先介绍
|
||||
2. 技能系统 - 紧跟工具,因为技能需要用 read 工具读取
|
||||
3. 记忆系统 - 独立的记忆能力
|
||||
3. 记忆系统 - 记忆检索与写入引导
|
||||
3.5 知识系统 - 结构化知识库(knowledge/index.md 注入)
|
||||
4. 工作空间 - 工作环境说明
|
||||
5. 用户身份 - 用户信息(可选)
|
||||
6. 项目上下文 - AGENT.md, USER.md, RULE.md, BOOTSTRAP.md(定义人格、身份、规则、初始化引导)
|
||||
6. 项目上下文 - AGENT.md, USER.md, RULE.md, MEMORY.md, BOOTSTRAP.md
|
||||
7. 运行时信息 - 元信息(时间、模型等)
|
||||
|
||||
Args:
|
||||
@@ -126,6 +128,10 @@ def build_agent_system_prompt(
|
||||
# 3. 记忆系统(独立的记忆能力)
|
||||
if memory_manager:
|
||||
sections.extend(_build_memory_section(memory_manager, tools, language))
|
||||
|
||||
# 3.5 知识系统(结构化知识库)
|
||||
if conf().get("knowledge", True):
|
||||
sections.extend(_build_knowledge_section(workspace_dir, language))
|
||||
|
||||
# 4. 工作空间(工作环境说明)
|
||||
sections.extend(_build_workspace_section(workspace_dir, language))
|
||||
@@ -268,55 +274,105 @@ def _build_memory_section(memory_manager: Any, tools: Optional[List[Any]], langu
|
||||
"""构建记忆系统section"""
|
||||
if not memory_manager:
|
||||
return []
|
||||
|
||||
# 检查是否有memory工具
|
||||
|
||||
has_memory_tools = False
|
||||
if tools:
|
||||
tool_names = [tool.name if hasattr(tool, 'name') else str(tool) for tool in tools]
|
||||
has_memory_tools = any(name in ['memory_search', 'memory_get'] for name in tool_names)
|
||||
|
||||
|
||||
if not has_memory_tools:
|
||||
return []
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
today_file = datetime.now().strftime("%Y-%m-%d") + ".md"
|
||||
|
||||
|
||||
lines = [
|
||||
"## 🧠 记忆系统",
|
||||
"",
|
||||
"### 检索记忆",
|
||||
"### Memory Recall(mandatory)",
|
||||
"",
|
||||
"在回答关于以前的工作、决定、日期、人物、偏好或待办事项的任何问题之前:",
|
||||
"当用户询问过往事件、引用之前的决定、提到人物关系、偏好、待办、或你对某事不确定时,**必须先检索记忆再回答**。",
|
||||
"如果 MEMORY.md 中已有相关信息则无需重复检索。完整内容和每日记忆需要通过工具检索。",
|
||||
"",
|
||||
"1. 不确定记忆文件位置 → 先用 `memory_search` 通过关键词和语义检索相关内容",
|
||||
"2. 已知文件位置 → 直接用 `memory_get` 读取相应的行 (例如:MEMORY.md, memory/YYYY-MM-DD.md)",
|
||||
"3. search 无结果 → 尝试用 `memory_get` 读取MEMORY.md及最近两天记忆文件",
|
||||
"1. 不确定位置 → `memory_search` 关键词/语义检索",
|
||||
"2. 已知位置 → `memory_get` 直接读取对应行",
|
||||
"3. search 无结果 → `memory_get` 读最近两天记忆",
|
||||
"",
|
||||
"**记忆文件结构**:",
|
||||
f"- `MEMORY.md`: 长期记忆(核心信息、偏好、决策等)",
|
||||
"- `MEMORY.md`: 长期记忆索引(已自动加载到上下文,核心信息、偏好、决策等)",
|
||||
f"- `memory/YYYY-MM-DD.md`: 每日记忆,今天是 `memory/{today_file}`",
|
||||
"- `knowledge/`: 结构化知识库(见下方知识系统)",
|
||||
"",
|
||||
"### 写入记忆",
|
||||
"",
|
||||
"**主动存储**:遇到以下情况时,应主动将信息写入记忆文件(无需告知用户):",
|
||||
"遇到以下情况时,**主动**将信息写入记忆文件(无需告知用户):",
|
||||
"",
|
||||
"- 用户明确要求你记住某些信息",
|
||||
"- 用户要求记住某些信息,或使用了「记住」「以后」「总是」「不要」「偏好」等表达",
|
||||
"- 用户分享了重要的个人偏好、习惯、决策",
|
||||
"- 对话中产生了重要的结论、方案、约定",
|
||||
"- 完成了复杂任务,值得记录关键步骤和结果",
|
||||
"- 发现了用户经常遇到的问题或解决方案",
|
||||
"",
|
||||
"**存储规则**:",
|
||||
f"- 长期有效的核心信息 → `MEMORY.md`(文件保持精简,< 2000 tokens)",
|
||||
f"- 当天的事件、进展、笔记 → `memory/{today_file}`",
|
||||
"- 追加内容 → `edit` 工具,oldText 留空",
|
||||
"- 修改内容 → `edit` 工具,oldText 填写要替换的文本",
|
||||
"- **禁止写入敏感信息**:API密钥、令牌等敏感信息严禁写入记忆文件",
|
||||
f"- 长期核心信息 → `MEMORY.md`",
|
||||
f"- 当天事件/进展 → `memory/{today_file}`",
|
||||
"- 结构化知识 → `knowledge/`(见知识系统)",
|
||||
"- 追加 → `edit` 工具,oldText 留空",
|
||||
"- 修改 → `edit` 工具,oldText 填写要替换的文本",
|
||||
"- **禁止写入敏感信息**(API密钥、令牌等)",
|
||||
"",
|
||||
"**使用原则**: 自然使用记忆,就像你本来就知道;不用刻意提起,除非用户问起。",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_knowledge_section(workspace_dir: str, language: str) -> List[str]:
|
||||
"""Build knowledge wiki section. Injects knowledge/index.md when present."""
|
||||
index_path = os.path.join(workspace_dir, "knowledge", "index.md")
|
||||
if not os.path.exists(index_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(index_path, 'r', encoding='utf-8') as f:
|
||||
index_content = f.read().strip()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
lines = [
|
||||
"## 📚 知识系统",
|
||||
"",
|
||||
"你拥有一个持续积累的个人知识库 `knowledge/`,这是你的长期结构化知识存储。",
|
||||
"",
|
||||
"### 自动写入规则(mandatory)",
|
||||
"",
|
||||
"以下场景**必须**在回复的同时写入知识库,**直接写入,不要询问用户是否需要**:",
|
||||
"",
|
||||
"1. **用户分享了文章/链接/文档** → 阅读理解后,在同一轮回复中将要点写入 `knowledge/sources/<slug>.md`",
|
||||
"2. **深度讨论产生了结论/方案** → 整理为 `knowledge/analysis/<slug>.md`",
|
||||
"3. **对话涉及重要实体**(人物/公司/项目)→ 创建或更新 `knowledge/entities/<name>.md`",
|
||||
"4. **讨论了技术概念/方法论** → 整理为 `knowledge/concepts/<topic>.md`",
|
||||
"",
|
||||
"每次写入知识页面后,**必须同步更新** `knowledge/index.md` 添加一行索引。",
|
||||
"详细的页面格式和操作规范,请读取技能 `knowledge-wiki` 的 SKILL.md。",
|
||||
"",
|
||||
"⚠️ 不要问「要不要存到知识库」——符合上述场景就直接写入,这是你的本能行为。",
|
||||
"",
|
||||
]
|
||||
|
||||
if index_content:
|
||||
lines.extend([
|
||||
"### 当前知识索引",
|
||||
"",
|
||||
index_content,
|
||||
"",
|
||||
])
|
||||
|
||||
lines.extend([
|
||||
"**查询方式**:用 `read` 读取知识页面,或用 `memory_search` 检索(知识已纳入向量索引)。",
|
||||
"",
|
||||
])
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
@@ -375,11 +431,12 @@ def _build_workspace_section(workspace_dir: str, language: str) -> List[str]:
|
||||
"",
|
||||
"**重要说明 - 文件已自动加载**:",
|
||||
"",
|
||||
"以下文件在会话启动时**已经自动加载**到系统提示词的「项目上下文」section 中,你**无需再用 read 工具读取它们**:",
|
||||
"以下文件在会话启动时**已经自动加载**到系统提示词中,你**无需再用 read 工具读取**:",
|
||||
"",
|
||||
"- ✅ `AGENT.md`: 已加载 - 你的人格和灵魂设定,请严格遵循。当你的名字、性格或交流风格发生变化时,主动用 `edit` 更新此文件",
|
||||
"- ✅ `USER.md`: 已加载 - 用户的身份信息。当用户修改称呼、姓名等身份信息时,用 `edit` 更新此文件",
|
||||
"- ✅ `RULE.md`: 已加载 - 工作空间使用指南和规则,请严格遵循",
|
||||
"- ✅ `MEMORY.md`: 已加载 - 长期记忆索引",
|
||||
"",
|
||||
"**💬 交流规范**:",
|
||||
"",
|
||||
|
||||
@@ -67,6 +67,12 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works
|
||||
# 创建websites子目录 (for web pages / sites generated by agent)
|
||||
websites_dir = os.path.join(workspace_dir, "websites")
|
||||
os.makedirs(websites_dir, exist_ok=True)
|
||||
|
||||
from config import conf
|
||||
knowledge_enabled = conf().get("knowledge", True)
|
||||
if knowledge_enabled:
|
||||
knowledge_dir = os.path.join(workspace_dir, "knowledge")
|
||||
os.makedirs(knowledge_dir, exist_ok=True)
|
||||
|
||||
# 如果需要,创建模板文件
|
||||
if create_templates:
|
||||
@@ -74,6 +80,15 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works
|
||||
_create_template_if_missing(user_path, _get_user_template())
|
||||
_create_template_if_missing(rule_path, _get_rule_template())
|
||||
_create_template_if_missing(memory_path, _get_memory_template())
|
||||
if knowledge_enabled:
|
||||
_create_template_if_missing(
|
||||
os.path.join(knowledge_dir, "index.md"),
|
||||
_get_knowledge_index_template()
|
||||
)
|
||||
_create_template_if_missing(
|
||||
os.path.join(knowledge_dir, "log.md"),
|
||||
_get_knowledge_log_template()
|
||||
)
|
||||
|
||||
# Only create BOOTSTRAP.md for brand new workspaces;
|
||||
# agent deletes it after completing onboarding
|
||||
@@ -109,6 +124,7 @@ def load_context_files(workspace_dir: str, files_to_load: Optional[List[str]] =
|
||||
DEFAULT_AGENT_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
DEFAULT_RULE_FILENAME,
|
||||
DEFAULT_MEMORY_FILENAME, # Long-term memory (frozen snapshot)
|
||||
DEFAULT_BOOTSTRAP_FILENAME, # Only exists when onboarding is incomplete
|
||||
]
|
||||
|
||||
@@ -138,6 +154,10 @@ def load_context_files(workspace_dir: str, files_to_load: Optional[List[str]] =
|
||||
# 跳过空文件或只包含模板占位符的文件
|
||||
if not content or _is_template_placeholder(content):
|
||||
continue
|
||||
|
||||
# Truncate MEMORY.md to protect context window (frozen snapshot)
|
||||
if filename == DEFAULT_MEMORY_FILENAME:
|
||||
content = _truncate_memory_content(content)
|
||||
|
||||
context_files.append(ContextFile(
|
||||
path=filename,
|
||||
@@ -163,6 +183,36 @@ def _create_template_if_missing(filepath: str, template_content: str):
|
||||
logger.error(f"[Workspace] Failed to create template {filepath}: {e}")
|
||||
|
||||
|
||||
_MEMORY_MAX_LINES = 200
|
||||
_MEMORY_MAX_BYTES = 25000
|
||||
|
||||
|
||||
def _truncate_memory_content(content: str) -> str:
|
||||
"""Truncate MEMORY.md to keep system prompt manageable.
|
||||
|
||||
Takes the **last** N lines (newest entries are appended at the bottom),
|
||||
subject to 200 lines / 25 KB limits (whichever is hit first).
|
||||
Prepends a hint when truncated so the model knows older content exists.
|
||||
"""
|
||||
lines = content.split('\n')
|
||||
truncated = False
|
||||
|
||||
if len(lines) > _MEMORY_MAX_LINES:
|
||||
lines = lines[-_MEMORY_MAX_LINES:]
|
||||
truncated = True
|
||||
|
||||
result = '\n'.join(lines)
|
||||
if len(result.encode('utf-8')) > _MEMORY_MAX_BYTES:
|
||||
while len(result.encode('utf-8')) > _MEMORY_MAX_BYTES and lines:
|
||||
lines.pop(0)
|
||||
truncated = True
|
||||
result = '\n'.join(lines)
|
||||
|
||||
if truncated:
|
||||
result = "...(older entries truncated, use `memory_search` or `memory_get` for full content)\n\n" + result
|
||||
return result
|
||||
|
||||
|
||||
def _is_template_placeholder(content: str) -> bool:
|
||||
"""检查内容是否为模板占位符"""
|
||||
# 常见的占位符模式
|
||||
@@ -287,39 +337,88 @@ def _get_rule_template() -> str:
|
||||
|
||||
这个文件夹是你的家。好好对待它。
|
||||
|
||||
## 工作空间目录结构
|
||||
|
||||
```
|
||||
~/cow/
|
||||
├── AGENT.md # 你的身份和灵魂设定
|
||||
├── USER.md # 用户基本信息(静态)
|
||||
├── RULE.md # 工作空间规则(本文件)
|
||||
├── MEMORY.md # 长期记忆索引(会话启动时自动加载)
|
||||
│
|
||||
├── memory/ # 每日对话记忆
|
||||
│ └── YYYY-MM-DD.md # 当天事件、进展、笔记
|
||||
│
|
||||
├── knowledge/ # 结构化知识库(持续积累的知识)
|
||||
│ ├── index.md # 知识目录索引(必须维护)
|
||||
│ ├── log.md # 知识操作日志
|
||||
│ └── <子目录>/ # 按需创建,参考 index.md 已有分类
|
||||
│
|
||||
├── skills/ # 技能
|
||||
├── websites/ # 网页产物
|
||||
└── tmp/ # 系统临时文件(自动管理,勿手动存放重要文件)
|
||||
```
|
||||
|
||||
## 记忆系统
|
||||
|
||||
你每次会话都是全新的,记忆文件让你保持连续性:
|
||||
|
||||
### 📝 每日记忆:`memory/YYYY-MM-DD.md`
|
||||
- 原始的对话日志
|
||||
- 记录当天发生的事情
|
||||
- 如果 `memory/` 目录不存在,创建它
|
||||
|
||||
### 🧠 长期记忆:`MEMORY.md`
|
||||
- 你精选的记忆,就像人类的长期记忆
|
||||
- **仅在主会话中加载**(与用户的直接聊天)
|
||||
- **不要在共享上下文中加载**(群聊、与其他人的会话)
|
||||
- 这是为了**安全** - 包含不应泄露给陌生人的个人上下文
|
||||
- 记录重要事件、想法、决定、观点、经验教训
|
||||
- 这是你精选的记忆 - 精华,而不是原始日志
|
||||
- 用 `edit` 工具追加新的记忆内容
|
||||
- 你精选的记忆索引,每次会话启动时**自动加载**到上下文中
|
||||
- 记录核心事实、偏好、决策、重要人物、教训
|
||||
- 保持精简(< 200 行),是精华索引而非原始日志
|
||||
- 用 `edit` 工具追加或修改
|
||||
|
||||
### 📝 每日记忆:`memory/YYYY-MM-DD.md`
|
||||
- 当天的事件、进展、笔记
|
||||
- 原始对话日志的沉淀
|
||||
|
||||
### 📝 写下来 - 不要"记在心里"!
|
||||
- **记忆是有限的** - 如果你想记住某事,写入文件
|
||||
- **记忆是有限的** - 想记住的事就写入文件
|
||||
- "记在心里"不会在会话重启后保留,文件才会
|
||||
- 当有人说"记住这个" → 更新 `MEMORY.md` 或 `memory/YYYY-MM-DD.md`
|
||||
- 当你学到教训 → 更新 RULE.md 或相关技能
|
||||
- 当你犯错 → 记录下来,这样未来的你不会重复,**文字 > 大脑** 📝
|
||||
- 当你犯错 → 记录下来,**文字 > 大脑** 📝
|
||||
|
||||
### 存储规则
|
||||
|
||||
当用户分享信息时,根据类型选择存储位置:
|
||||
|
||||
1. **你的身份设定 → AGENT.md**(你的名字、角色、性格、交流风格——用户修改时必须用 `edit` 更新)
|
||||
2. **用户静态身份 → USER.md**(姓名、称呼、职业、时区、联系方式、生日——用户修改时必须用 `edit` 更新)
|
||||
3. **动态记忆 → MEMORY.md**(爱好、偏好、决策、目标、项目、教训、待办事项)
|
||||
1. **你的身份设定 → AGENT.md**(名字、角色、性格、风格)
|
||||
2. **用户静态身份 → USER.md**(姓名、称呼、职业、联系方式、生日)
|
||||
3. **动态记忆 → MEMORY.md**(偏好、决策、目标、教训、待办)
|
||||
4. **当天对话 → memory/YYYY-MM-DD.md**(今天聊的内容)
|
||||
5. **结构化知识 → knowledge/**(见下方知识系统)
|
||||
|
||||
## 知识系统
|
||||
|
||||
知识库 `knowledge/` 是你持续积累的结构化知识。与记忆不同,知识是经过整理和编译的,有明确的主题和交叉引用。
|
||||
|
||||
### 自动写入(不要询问,直接写入)
|
||||
|
||||
当对话中产生了有沉淀价值的知识——无论是用户分享的资料、讨论的结论、学到的概念、还是重要的决策——你**必须**在回复的同时主动写入知识库,**无需问用户"要不要存到知识库"**。
|
||||
|
||||
**关键原则**:学完就记是你的本能,不要征求确认。回复中可以顺带告知"已存入知识库"。
|
||||
|
||||
### 目录组织
|
||||
|
||||
子目录结构**不是固定的**,由你根据实际内容自主决定:
|
||||
- **首次写入时**:先读 `knowledge/index.md`,如果已有分类则延续;如果为空,根据内容选择合适的目录名
|
||||
- **默认建议**:按信息类型组织(例如sources/、concepts/、entities/、analysis/),如果用户有明确的分类偏好(例如按领域 work/、life/、tech/ 等),则按用户要求调整
|
||||
- **保持一致性**:同一用户的知识库应保持统一的组织风格
|
||||
|
||||
### 交叉引用
|
||||
|
||||
知识的核心价值在于**关联**。每个页面都应通过 markdown 链接引用相关页面,构建知识网络:
|
||||
- 提到已有页面的概念时,添加 `[概念名](../category/page.md)` 链接
|
||||
- 新建页面时,检查是否有已有页面应该反向链接到新页面
|
||||
- **只链接已存在的页面**——不要引用尚未创建的页面。如果某个概念值得单独建页,先创建该页面再添加链接
|
||||
|
||||
### 索引维护
|
||||
|
||||
每次创建或更新知识页面后,**必须同步更新** `knowledge/index.md`。
|
||||
索引格式:每行一个 `[标题](路径) — 一句话摘要`,按分类分组,不要用表格。
|
||||
详细操作规范见技能 `knowledge-wiki`。
|
||||
|
||||
## 安全
|
||||
|
||||
@@ -381,4 +480,12 @@ _你刚刚启动,这是你的第一次对话。_ ✨
|
||||
"""
|
||||
|
||||
|
||||
def _get_knowledge_index_template() -> str:
|
||||
"""Knowledge wiki index template — empty file, agent fills it."""
|
||||
return ""
|
||||
|
||||
|
||||
def _get_knowledge_log_template() -> str:
|
||||
"""Knowledge wiki operation log template — empty file, agent fills it."""
|
||||
return ""
|
||||
|
||||
|
||||
@@ -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,48 @@ class AgentStreamExecutor:
|
||||
except Exception as e:
|
||||
logger.error(f"Event callback error: {e}")
|
||||
|
||||
def _is_thinking_enabled(self) -> bool:
|
||||
"""Whether deep-thinking mode is on at the model layer.
|
||||
|
||||
Mirrors the global toggle used by ``bridge.agent_bridge`` when deciding
|
||||
whether to send ``thinking={"type": "enabled"}`` to the model. Used for
|
||||
logging and reasoning-update event emission across all channels.
|
||||
"""
|
||||
from config import conf
|
||||
return bool(conf().get("enable_thinking", False))
|
||||
|
||||
def _should_render_thinking_inline(self) -> bool:
|
||||
"""Whether ``<think>...</think>`` blocks embedded directly in ``content``
|
||||
(MiniMax, some third-party proxies) should be surfaced to the channel.
|
||||
|
||||
Only the Web console can render them in a collapsible panel. IM channels
|
||||
(WeChat/WeCom/DingTalk/Feishu) must strip them, otherwise users see raw
|
||||
XML tags in their chat.
|
||||
"""
|
||||
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 inline thinking rendering is allowed (Web + thinking enabled):
|
||||
remove only the tags, keep the content inside.
|
||||
- Otherwise (IM channels, or thinking disabled globally): 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._should_render_thinking_inline():
|
||||
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 +239,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 +291,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 +307,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 +339,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:
|
||||
@@ -527,6 +618,7 @@ class AgentStreamExecutor:
|
||||
|
||||
# Streaming response
|
||||
full_content = ""
|
||||
full_reasoning = ""
|
||||
tool_calls_buffer = {} # {index: {id, name, arguments}}
|
||||
gemini_raw_parts = None # Preserve Gemini thoughtSignature for round-trip
|
||||
stop_reason = None # Track why the stream stopped
|
||||
@@ -584,10 +676,11 @@ class AgentStreamExecutor:
|
||||
if finish_reason:
|
||||
stop_reason = finish_reason
|
||||
|
||||
# Skip reasoning_content (internal thinking from models like GLM-5)
|
||||
reasoning_delta = delta.get("reasoning_content") or ""
|
||||
# if reasoning_delta:
|
||||
# logger.debug(f"🧠 [thinking] {reasoning_delta[:100]}...")
|
||||
if reasoning_delta:
|
||||
full_reasoning += reasoning_delta
|
||||
if self._is_thinking_enabled():
|
||||
self._emit_event("reasoning_update", {"delta": reasoning_delta})
|
||||
|
||||
# Handle text content
|
||||
content_delta = delta.get("content") or ""
|
||||
@@ -621,8 +714,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)
|
||||
@@ -788,7 +884,18 @@ class AgentStreamExecutor:
|
||||
# Add assistant message to history (Claude format uses content blocks)
|
||||
assistant_msg = {"role": "assistant", "content": []}
|
||||
|
||||
# Add text content block if present
|
||||
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": stored_reasoning
|
||||
})
|
||||
|
||||
if full_content:
|
||||
assistant_msg["content"].append({
|
||||
"type": "text",
|
||||
@@ -1192,6 +1299,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):
|
||||
"""
|
||||
智能清理消息历史,保持对话完整性
|
||||
@@ -1218,25 +1375,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()
|
||||
@@ -1312,6 +1472,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)
|
||||
|
||||
@@ -1322,13 +1483,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 = []
|
||||
|
||||
@@ -210,6 +210,10 @@ class SkillManager:
|
||||
if not include_disabled:
|
||||
entries = [e for e in entries if self.is_skill_enabled(e.skill.name)]
|
||||
|
||||
from config import conf
|
||||
if not conf().get("knowledge", True):
|
||||
entries = [e for e in entries if e.skill.name != "knowledge-wiki"]
|
||||
|
||||
return entries
|
||||
|
||||
def filter_unavailable_skills(
|
||||
|
||||
@@ -29,7 +29,7 @@ ENVIRONMENT: All API keys from env_config are auto-injected. Use $VAR_NAME direc
|
||||
|
||||
SAFETY:
|
||||
- Freely create/modify/delete files within the workspace
|
||||
- For destructive and out-of-workspace commands, explain and confirm first"""
|
||||
- For destructive commands out of workspace, explain and confirm first"""
|
||||
|
||||
params: dict = {
|
||||
"type": "object",
|
||||
@@ -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
|
||||
@@ -232,48 +238,43 @@ SAFETY:
|
||||
|
||||
def _get_safety_warning(self, command: str) -> str:
|
||||
"""
|
||||
Get safety warning for potentially dangerous commands
|
||||
Only warns about extremely dangerous system-level operations
|
||||
|
||||
Get safety warning for absolutely catastrophic commands only.
|
||||
Keep the blocklist minimal so the agent retains maximum freedom.
|
||||
|
||||
:param command: Command to check
|
||||
:return: Warning message if dangerous, empty string if safe
|
||||
"""
|
||||
cmd_lower = command.lower().strip()
|
||||
# Tokenize to avoid substring false positives (e.g. `rm -rf /tmp/x`
|
||||
# must not match `rm -rf /`).
|
||||
tokens = command.lower().split()
|
||||
|
||||
# Only block extremely dangerous system operations
|
||||
dangerous_patterns = [
|
||||
# System shutdown/reboot
|
||||
("shutdown", "This command will shut down the system"),
|
||||
("reboot", "This command will reboot the system"),
|
||||
("halt", "This command will halt the system"),
|
||||
("poweroff", "This command will power off the system"),
|
||||
# `rm -rf /` or `rm -rf /*` targeting the real root.
|
||||
for i, tok in enumerate(tokens):
|
||||
if tok != "rm":
|
||||
continue
|
||||
has_rf = False
|
||||
for j in range(i + 1, len(tokens)):
|
||||
t = tokens[j]
|
||||
if t.startswith("-") and "r" in t and "f" in t:
|
||||
has_rf = True
|
||||
elif t in ("--recursive", "--force"):
|
||||
continue
|
||||
elif t in ("/", "/*"):
|
||||
if has_rf:
|
||||
return "This command will delete the entire filesystem"
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
# Critical system modifications
|
||||
("rm -rf /", "This command will delete the entire filesystem"),
|
||||
("rm -rf /*", "This command will delete the entire filesystem"),
|
||||
("dd if=/dev/zero", "This command can destroy disk data"),
|
||||
("mkfs", "This command will format a filesystem, destroying all data"),
|
||||
("fdisk", "This command modifies disk partitions"),
|
||||
# Disk wiping
|
||||
if "if=/dev/zero" in command.lower() and "dd " in command.lower():
|
||||
return "This command can destroy disk data"
|
||||
|
||||
# User/system management (only if targeting system users)
|
||||
("userdel root", "This command will delete the root user"),
|
||||
("passwd root", "This command will change the root password"),
|
||||
]
|
||||
# Power control - match only as a standalone word (\b enforces word boundary)
|
||||
if re.search(r'\b(shutdown|reboot|halt|poweroff)\b', command.lower()):
|
||||
return "This command will shut down or restart the system"
|
||||
|
||||
for pattern, warning in dangerous_patterns:
|
||||
if pattern in cmd_lower:
|
||||
return warning
|
||||
|
||||
# Check for recursive deletion outside workspace
|
||||
if "rm" in cmd_lower and "-rf" in cmd_lower:
|
||||
# Allow deletion within current workspace
|
||||
if not any(path in cmd_lower for path in ["./", self.cwd.lower()]):
|
||||
# Check if targeting system directories
|
||||
system_dirs = ["/bin", "/usr", "/etc", "/var", "/home", "/root", "/sys", "/proc"]
|
||||
if any(sysdir in cmd_lower for sysdir in system_dirs):
|
||||
return "This command will recursively delete system directories"
|
||||
|
||||
return "" # No warning needed
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _convert_env_vars_for_windows(command: str, dotenv_vars: dict) -> str:
|
||||
|
||||
@@ -44,6 +44,19 @@ class MemoryGetTool(BaseTool):
|
||||
"""
|
||||
super().__init__()
|
||||
self.memory_manager = memory_manager
|
||||
|
||||
from config import conf
|
||||
if conf().get("knowledge", True):
|
||||
self.description = (
|
||||
"Read specific content from memory or knowledge files. "
|
||||
"Use this to get full context from a memory file, knowledge page, or specific line range."
|
||||
)
|
||||
self.params = {**self.params}
|
||||
self.params["properties"] = {**self.params["properties"]}
|
||||
self.params["properties"]["path"] = {
|
||||
"type": "string",
|
||||
"description": "Relative path to the memory or knowledge file (e.g. 'MEMORY.md', 'memory/2026-01-01.md', 'knowledge/concepts/moe.md')"
|
||||
}
|
||||
|
||||
def execute(self, args: dict):
|
||||
"""
|
||||
@@ -68,11 +81,15 @@ class MemoryGetTool(BaseTool):
|
||||
workspace_dir = self.memory_manager.config.get_workspace()
|
||||
|
||||
# Auto-prepend memory/ if not present and not absolute path
|
||||
# Exception: MEMORY.md is in the root directory
|
||||
if not path.startswith('memory/') and not path.startswith('/') and path != 'MEMORY.md':
|
||||
# Exceptions: MEMORY.md in root, knowledge/ files at workspace root
|
||||
if not path.startswith('memory/') and not path.startswith('knowledge/') and not path.startswith('/') and path != 'MEMORY.md':
|
||||
path = f'memory/{path}'
|
||||
|
||||
file_path = workspace_dir / path
|
||||
file_path = (workspace_dir / path).resolve()
|
||||
workspace_resolved = workspace_dir.resolve()
|
||||
|
||||
if not str(file_path).startswith(str(workspace_resolved) + '/') and file_path != workspace_resolved:
|
||||
return ToolResult.fail(f"Error: Access denied: path outside workspace")
|
||||
|
||||
if not file_path.exists():
|
||||
return ToolResult.fail(f"Error: File not found: {path}")
|
||||
|
||||
@@ -48,6 +48,13 @@ class MemorySearchTool(BaseTool):
|
||||
super().__init__()
|
||||
self.memory_manager = memory_manager
|
||||
self.user_id = user_id
|
||||
|
||||
from config import conf
|
||||
if conf().get("knowledge", True):
|
||||
self.description = (
|
||||
"Search agent's long-term memory and knowledge base using semantic and keyword search. "
|
||||
"Use this to recall past conversations, preferences, and knowledge pages."
|
||||
)
|
||||
|
||||
def execute(self, args: dict):
|
||||
"""
|
||||
|
||||
@@ -84,6 +84,49 @@ def get_scheduler_service():
|
||||
return _scheduler_service
|
||||
|
||||
|
||||
def _remember_delivered_output(
|
||||
agent_bridge,
|
||||
task: dict,
|
||||
channel_type: str,
|
||||
content: str,
|
||||
) -> None:
|
||||
"""Best-effort persistence of the message the scheduler sent to a user.
|
||||
|
||||
Uses notify_session_id (the real chat session_id stored at task creation time)
|
||||
so that group chats correctly associate the output with the user's conversation.
|
||||
Falls back to receiver for backward compatibility with old tasks.
|
||||
|
||||
Per-action-type behaviour:
|
||||
- agent_task / tool_call / skill_call: gated by ``scheduler_inject_to_session``
|
||||
(default True). These produce AI-generated content worth remembering.
|
||||
- send_message: additionally gated by ``scheduler_inject_send_message``
|
||||
(default False). Fixed reminder text rarely benefits follow-up Q&A and
|
||||
would just consume context tokens.
|
||||
"""
|
||||
if not content:
|
||||
return
|
||||
action = task.get("action", {})
|
||||
action_type = action.get("type", "")
|
||||
|
||||
# send_message defaults to NOT being injected; explicit opt-in via config.
|
||||
if action_type == "send_message":
|
||||
if not conf().get("scheduler_inject_send_message", False):
|
||||
return
|
||||
|
||||
session_id = action.get("notify_session_id") or action.get("receiver")
|
||||
if not session_id:
|
||||
return
|
||||
try:
|
||||
remember = getattr(agent_bridge, "remember_scheduled_output", None)
|
||||
if remember:
|
||||
task_desc = action.get("task_description") or action.get("content", "")
|
||||
remember(session_id, str(content), channel_type=channel_type, task_description=task_desc)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[Scheduler] Failed to remember delivered output for {session_id}: {e}"
|
||||
)
|
||||
|
||||
|
||||
def _execute_agent_task(task: dict, agent_bridge):
|
||||
"""
|
||||
Execute an agent_task action - let Agent handle the task
|
||||
@@ -165,6 +208,7 @@ def _execute_agent_task(task: dict, agent_bridge):
|
||||
|
||||
# Send the reply
|
||||
channel.send(reply, context)
|
||||
_remember_delivered_output(agent_bridge, task, channel_type, reply.content)
|
||||
logger.info(f"[Scheduler] Task {task['id']} executed successfully, result sent to {receiver}")
|
||||
else:
|
||||
logger.error(f"[Scheduler] Failed to create channel: {channel_type}")
|
||||
@@ -255,6 +299,7 @@ def _execute_send_message(task: dict, agent_bridge):
|
||||
logger.debug(f"[Scheduler] Registered request_id {request_id} -> session {receiver}")
|
||||
|
||||
channel.send(reply, context)
|
||||
_remember_delivered_output(agent_bridge, task, channel_type, content)
|
||||
logger.info(f"[Scheduler] Task {task['id']} executed: sent message to {receiver}")
|
||||
else:
|
||||
logger.error(f"[Scheduler] Failed to create channel: {channel_type}")
|
||||
@@ -351,6 +396,7 @@ def _execute_tool_call(task: dict, agent_bridge):
|
||||
logger.debug(f"[Scheduler] Registered request_id {request_id} -> session {receiver}")
|
||||
|
||||
channel.send(reply, context)
|
||||
_remember_delivered_output(agent_bridge, task, channel_type, content)
|
||||
logger.info(f"[Scheduler] Task {task['id']} executed: sent tool result to {receiver}")
|
||||
else:
|
||||
logger.error(f"[Scheduler] Failed to create channel: {channel_type}")
|
||||
@@ -429,6 +475,24 @@ def _execute_skill_call(task: dict, agent_bridge):
|
||||
if result_prefix:
|
||||
content = f"{result_prefix}\n\n{content}"
|
||||
|
||||
# Send the result via channel
|
||||
from channel.channel_factory import create_channel
|
||||
|
||||
try:
|
||||
channel = create_channel(channel_type)
|
||||
if channel:
|
||||
# For web channel, register request_id
|
||||
if channel_type == "web" and hasattr(channel, 'request_to_session'):
|
||||
req_id = context.get("request_id")
|
||||
if req_id:
|
||||
channel.request_to_session[req_id] = receiver
|
||||
logger.debug(f"[Scheduler] Registered request_id {req_id} -> session {receiver}")
|
||||
|
||||
channel.send(Reply(ReplyType.TEXT, content), context)
|
||||
_remember_delivered_output(agent_bridge, task, channel_type, content)
|
||||
except Exception as e:
|
||||
logger.error(f"[Scheduler] Failed to send skill result: {e}")
|
||||
|
||||
logger.info(f"[Scheduler] Task {task['id']} executed: skill result sent to {receiver}")
|
||||
else:
|
||||
logger.error(f"[Scheduler] Task {task['id']}: No result from skill execution")
|
||||
|
||||
@@ -158,6 +158,11 @@ class SchedulerTool(BaseTool):
|
||||
# Create task
|
||||
task_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Capture the real chat session_id at task creation time so that scheduler
|
||||
# can later inject the delivered output into the user's actual conversation
|
||||
# (in group chats, session_id != receiver, e.g. "user_id:group_id" on feishu).
|
||||
notify_session_id = context.get("session_id")
|
||||
|
||||
# Build action based on message or ai_task
|
||||
if message:
|
||||
action = {
|
||||
@@ -166,7 +171,8 @@ class SchedulerTool(BaseTool):
|
||||
"receiver": context.get("receiver"),
|
||||
"receiver_name": self._get_receiver_name(context),
|
||||
"is_group": context.get("isgroup", False),
|
||||
"channel_type": self.config.get("channel_type", "unknown")
|
||||
"channel_type": self.config.get("channel_type", "unknown"),
|
||||
"notify_session_id": notify_session_id,
|
||||
}
|
||||
else: # ai_task
|
||||
action = {
|
||||
@@ -175,7 +181,8 @@ class SchedulerTool(BaseTool):
|
||||
"receiver": context.get("receiver"),
|
||||
"receiver_name": self._get_receiver_name(context),
|
||||
"is_group": context.get("isgroup", False),
|
||||
"channel_type": self.config.get("channel_type", "unknown")
|
||||
"channel_type": self.config.get("channel_type", "unknown"),
|
||||
"notify_session_id": notify_session_id,
|
||||
}
|
||||
|
||||
# 针对钉钉单聊,额外存储 sender_staff_id
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
Vision tool - Analyze images using Vision API.
|
||||
Supports local files (auto base64-encoded) and HTTP URLs.
|
||||
|
||||
Provider priority (default):
|
||||
1. Main model via bot.call_vision — zero extra cost
|
||||
2. Other models whose API key is configured — auto-discovered
|
||||
3. OpenAI / LinkAI raw HTTP — reliable fallback
|
||||
When use_linkai=true, LinkAI is promoted to #1.
|
||||
When tool.vision.model is set, that model is used exclusively first.
|
||||
Provider resolution:
|
||||
- tool.vision.model (if set) means "prefer this model first; fall back to
|
||||
other configured providers if it fails". The model name is mapped to its
|
||||
native provider (e.g. doubao-* → Doubao, kimi-* → Moonshot, gpt-* →
|
||||
OpenAI/LinkAI). That provider is tried first, then the standard auto
|
||||
chain runs as fallback (with the preferred provider de-duplicated).
|
||||
- Auto chain priority:
|
||||
1. Main model via bot.call_vision — only when the main bot is known
|
||||
to actually support vision (not just expose a call_vision method).
|
||||
2. Other models whose API key is configured.
|
||||
3. OpenAI / LinkAI raw HTTP.
|
||||
When use_linkai=true, LinkAI is promoted to #1.
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -43,15 +49,35 @@ _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"),
|
||||
("gemini_api_key", const.GEMINI, const.GEMINI_31_FLASH_LITE_PRE, "Gemini"),
|
||||
("qianfan_api_key", const.QIANFAN, const.ERNIE_45_TURBO_VL, "Qianfan"),
|
||||
("zhipu_ai_api_key", const.ZHIPU_AI, const.GLM_4_7, "ZhipuAI"),
|
||||
("minimax_api_key", const.MiniMax, const.MINIMAX_M2_7, "MiniMax"),
|
||||
]
|
||||
|
||||
# Model name prefix → discoverable provider display_name.
|
||||
# Used to auto-route tool.vision.model to its native provider.
|
||||
# Matched case-insensitively; longest prefix wins.
|
||||
_MODEL_PREFIX_TO_PROVIDER = [
|
||||
("doubao-", "Doubao"),
|
||||
("kimi-", "Moonshot"),
|
||||
("moonshot-", "Moonshot"),
|
||||
("qwen", "DashScope"), # qwen-*, qwen3-*, qwen3.6-*, etc.
|
||||
("claude-", "Claude"),
|
||||
("ernie-", "Qianfan"),
|
||||
("gemini-", "Gemini"),
|
||||
("glm-", "ZhipuAI"),
|
||||
("minimax-", "MiniMax"),
|
||||
("abab", "MiniMax"),
|
||||
]
|
||||
|
||||
# Model prefixes that natively belong to OpenAI / LinkAI (raw HTTP providers).
|
||||
_OPENAI_MODEL_PREFIXES = ("gpt-", "o1-", "o3-", "o4-", "chatgpt-")
|
||||
|
||||
|
||||
@dataclass
|
||||
class VisionProvider:
|
||||
@@ -116,7 +142,7 @@ class Vision(BaseTool):
|
||||
"Error: No model available for Vision.\n"
|
||||
"The main model does not support vision and no other API keys are configured.\n"
|
||||
"Options:\n"
|
||||
" 1. Switch to a multimodal model (e.g. qwen3.6-plus, claude-sonnet-4-6, gemini-2.0-flash)\n"
|
||||
" 1. Switch to a multimodal model (e.g. ernie-4.5-turbo-vl, qwen3.6-plus, claude-sonnet-4-6, gemini-2.0-flash)\n"
|
||||
" 2. Configure OPENAI_API_KEY: env_config(action=\"set\", key=\"OPENAI_API_KEY\", value=\"your-key\")\n"
|
||||
" 3. Configure LINKAI_API_KEY: env_config(action=\"set\", key=\"LINKAI_API_KEY\", value=\"your-key\")"
|
||||
)
|
||||
@@ -126,6 +152,9 @@ class Vision(BaseTool):
|
||||
except Exception as e:
|
||||
return ToolResult.fail(f"Error: {e}")
|
||||
|
||||
# Default model is only used as a last-resort placeholder for providers
|
||||
# whose VisionProvider.model_override is None (e.g. raw OpenAI provider
|
||||
# when the user did not configure tool.vision.model).
|
||||
return self._call_with_fallback(providers, DEFAULT_MODEL, question, image_content)
|
||||
|
||||
def _call_with_fallback(self, providers: List[VisionProvider], model: str,
|
||||
@@ -162,29 +191,55 @@ class Vision(BaseTool):
|
||||
|
||||
def _resolve_providers(self) -> List[VisionProvider]:
|
||||
"""
|
||||
Build an ordered list of available providers.
|
||||
Build an ordered list of providers to try.
|
||||
|
||||
Priority:
|
||||
- use_linkai=true → [LinkAI, MainModel, OtherModels…, OpenAI]
|
||||
- default → [MainModel, OtherModels…, OpenAI, LinkAI]
|
||||
Semantics of `tool.vision.model`:
|
||||
"Prefer this model first; fall back to other configured providers
|
||||
if it fails."
|
||||
|
||||
"OtherModels" are auto-discovered from configured API keys.
|
||||
The main model's bot_type is excluded from OtherModels to avoid
|
||||
duplicating the MainModel provider.
|
||||
Order:
|
||||
1. The provider that natively serves `tool.vision.model` (if any
|
||||
and its API key is configured) — using the user-specified model
|
||||
name verbatim.
|
||||
2. Auto-discovery chain as fallback:
|
||||
- use_linkai=true → [LinkAI, MainModel?, OtherModels…, OpenAI]
|
||||
- default → [MainModel?, OtherModels…, OpenAI, LinkAI]
|
||||
MainModel is only included when the main bot is known to support
|
||||
vision (see _main_bot_supports_vision).
|
||||
|
||||
Providers that share the same display name as the preferred provider
|
||||
are de-duplicated to avoid retrying the same endpoint twice.
|
||||
"""
|
||||
use_linkai = conf().get("use_linkai", False) and conf().get("linkai_api_key")
|
||||
user_model = self._resolve_user_vision_model()
|
||||
providers: List[VisionProvider] = []
|
||||
|
||||
# Step 1: preferred provider derived from tool.vision.model
|
||||
if user_model:
|
||||
preferred = self._route_by_model_name(user_model)
|
||||
if preferred:
|
||||
providers.extend(preferred)
|
||||
|
||||
# Step 2: auto-discovery chain as fallback
|
||||
existing = {p.name for p in providers}
|
||||
fallback: List[VisionProvider] = []
|
||||
use_linkai = conf().get("use_linkai", False) and conf().get("linkai_api_key")
|
||||
|
||||
if use_linkai:
|
||||
self._append_provider(providers, self._build_linkai_provider)
|
||||
self._append_provider(providers, self._build_main_model_provider)
|
||||
self._append_other_model_providers(providers)
|
||||
self._append_provider(providers, self._build_openai_provider)
|
||||
self._append_provider(fallback, lambda: self._build_linkai_provider(user_model))
|
||||
self._append_provider(fallback, self._build_main_model_provider)
|
||||
self._append_other_model_providers(fallback, preferred_model=user_model)
|
||||
self._append_provider(fallback, lambda: self._build_openai_provider(user_model))
|
||||
else:
|
||||
self._append_provider(providers, self._build_main_model_provider)
|
||||
self._append_other_model_providers(providers)
|
||||
self._append_provider(providers, self._build_openai_provider)
|
||||
self._append_provider(providers, self._build_linkai_provider)
|
||||
self._append_provider(fallback, self._build_main_model_provider)
|
||||
self._append_other_model_providers(fallback, preferred_model=user_model)
|
||||
self._append_provider(fallback, lambda: self._build_openai_provider(user_model))
|
||||
self._append_provider(fallback, lambda: self._build_linkai_provider(user_model))
|
||||
|
||||
for p in fallback:
|
||||
if p.name in existing:
|
||||
continue
|
||||
providers.append(p)
|
||||
existing.add(p.name)
|
||||
|
||||
return providers
|
||||
|
||||
@@ -194,29 +249,135 @@ class Vision(BaseTool):
|
||||
if p:
|
||||
providers.append(p)
|
||||
|
||||
def _append_other_model_providers(self, providers: List[VisionProvider]) -> None:
|
||||
@staticmethod
|
||||
def _resolve_user_vision_model() -> Optional[str]:
|
||||
"""Read tool.vision.model from config; return None if unset/blank."""
|
||||
tool_conf = conf().get("tool", {})
|
||||
if not isinstance(tool_conf, dict):
|
||||
return None
|
||||
vision_conf = tool_conf.get("vision", {})
|
||||
if not isinstance(vision_conf, dict):
|
||||
return None
|
||||
m = vision_conf.get("model")
|
||||
if isinstance(m, str) and m.strip():
|
||||
return m.strip()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _infer_provider_from_model(model_name: str) -> Optional[str]:
|
||||
"""
|
||||
Infer the provider display name from a model name's prefix.
|
||||
Returns None when no rule matches (or for OpenAI-family names, which
|
||||
are handled separately by the caller).
|
||||
"""
|
||||
if not model_name:
|
||||
return None
|
||||
lower = model_name.lower()
|
||||
# Sort by prefix length desc so e.g. "moonshot-" wins over hypothetical "moo-"
|
||||
for prefix, display_name in sorted(_MODEL_PREFIX_TO_PROVIDER, key=lambda x: -len(x[0])):
|
||||
if lower.startswith(prefix.lower()):
|
||||
return display_name
|
||||
return None
|
||||
|
||||
def _route_by_model_name(self, user_model: str) -> Optional[List[VisionProvider]]:
|
||||
"""
|
||||
Try to build a provider list using the user-specified model name.
|
||||
Returns:
|
||||
- [provider] : matched and the provider's key is configured
|
||||
- [] : matched but key missing → tell caller to surface this
|
||||
as a hard error rather than silently falling back
|
||||
- None : no rule matches → caller should fall through to auto
|
||||
"""
|
||||
lower = user_model.lower()
|
||||
|
||||
# OpenAI / LinkAI family
|
||||
if lower.startswith(_OPENAI_MODEL_PREFIXES):
|
||||
providers: List[VisionProvider] = []
|
||||
# Prefer LinkAI when explicitly enabled, else OpenAI first
|
||||
use_linkai = conf().get("use_linkai", False) and conf().get("linkai_api_key")
|
||||
if use_linkai:
|
||||
self._append_provider(providers, lambda: self._build_linkai_provider(user_model))
|
||||
self._append_provider(providers, lambda: self._build_openai_provider(user_model))
|
||||
else:
|
||||
self._append_provider(providers, lambda: self._build_openai_provider(user_model))
|
||||
self._append_provider(providers, lambda: self._build_linkai_provider(user_model))
|
||||
if providers:
|
||||
return providers
|
||||
logger.warning(f"[Vision] tool.vision.model='{user_model}' looks like an OpenAI "
|
||||
f"model but neither OPENAI_API_KEY nor LINKAI_API_KEY is configured.")
|
||||
return None # fall through to auto
|
||||
|
||||
# Discoverable native providers (Doubao, Moonshot, etc.)
|
||||
target_display = self._infer_provider_from_model(user_model)
|
||||
if not target_display:
|
||||
return None # unknown prefix → auto
|
||||
|
||||
for config_key, bot_type, _default_model, display_name in _DISCOVERABLE_MODELS:
|
||||
if display_name != target_display:
|
||||
continue
|
||||
api_key = conf().get(config_key, "")
|
||||
if not api_key or not api_key.strip():
|
||||
logger.warning(f"[Vision] tool.vision.model='{user_model}' routes to "
|
||||
f"'{display_name}' but '{config_key}' is not configured. "
|
||||
f"Falling back to auto-discovery.")
|
||||
return None # fall through to auto
|
||||
try:
|
||||
from models.bot_factory import create_bot
|
||||
bot = create_bot(bot_type)
|
||||
if not hasattr(bot, 'call_vision'):
|
||||
logger.warning(f"[Vision] '{display_name}' bot does not implement call_vision.")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"[Vision] Failed to create '{display_name}' bot: {e}")
|
||||
return None
|
||||
|
||||
return [VisionProvider(
|
||||
name=display_name,
|
||||
api_key="",
|
||||
api_base="",
|
||||
model_override=user_model,
|
||||
use_bot=True,
|
||||
fallback_bot=bot,
|
||||
)]
|
||||
|
||||
return None
|
||||
|
||||
def _append_other_model_providers(self, providers: List[VisionProvider],
|
||||
preferred_model: Optional[str] = None) -> None:
|
||||
"""
|
||||
Auto-discover other models whose API key is configured.
|
||||
Skip the main model's own bot_type (already covered by MainModel provider).
|
||||
Skip bot_types that already have a provider in the list (e.g. OpenAI).
|
||||
Skip the main model's own bot_type (already covered by MainModel
|
||||
provider), unless the main model itself does not support vision —
|
||||
in that case we still want the vendor's dedicated vision model
|
||||
as a fallback. Also skip bot_types that already appear in the
|
||||
provider list.
|
||||
|
||||
If preferred_model matches a provider's family, use it instead
|
||||
of that provider's hard-coded default model.
|
||||
"""
|
||||
# Determine main model's bot_type so we can skip it
|
||||
main_bot_type = None
|
||||
main_bot_supports_vision = False
|
||||
if self.model and hasattr(self.model, '_resolve_bot_type'):
|
||||
main_bot_type = self.model._resolve_bot_type(conf().get("model", ""))
|
||||
main_bot = getattr(self.model, "bot", None)
|
||||
main_bot_supports_vision = self._main_bot_supports_vision(main_bot)
|
||||
|
||||
existing_names = {p.name for p in providers}
|
||||
preferred_provider = self._infer_provider_from_model(preferred_model) if preferred_model else None
|
||||
|
||||
for config_key, bot_type, default_model, display_name in _DISCOVERABLE_MODELS:
|
||||
if display_name in existing_names:
|
||||
continue
|
||||
if bot_type == main_bot_type:
|
||||
# Same bot_type as the main model is normally handled by the
|
||||
# MainModel provider; only skip it here if the main model
|
||||
# actually supports vision. Otherwise fall through and add
|
||||
# the vendor's dedicated vision model as a fallback.
|
||||
if bot_type == main_bot_type and main_bot_supports_vision:
|
||||
continue
|
||||
api_key = conf().get(config_key, "")
|
||||
if not api_key or not api_key.strip():
|
||||
continue
|
||||
|
||||
# Create a bot instance and check if it supports call_vision
|
||||
try:
|
||||
from models.bot_factory import create_bot
|
||||
bot = create_bot(bot_type)
|
||||
@@ -225,62 +386,105 @@ class Vision(BaseTool):
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
providers.append(VisionProvider(
|
||||
model_for_provider = (preferred_model
|
||||
if preferred_provider == display_name and preferred_model
|
||||
else default_model)
|
||||
|
||||
provider = VisionProvider(
|
||||
name=display_name,
|
||||
api_key="",
|
||||
api_base="",
|
||||
model_override=default_model,
|
||||
model_override=model_for_provider,
|
||||
use_bot=True,
|
||||
fallback_bot=bot,
|
||||
))
|
||||
)
|
||||
|
||||
def _resolve_vision_model(self) -> Optional[str]:
|
||||
"""
|
||||
Determine which model to use for vision.
|
||||
# Same vendor as the main bot is the most natural fallback when
|
||||
# the main model itself does not support vision — promote it to
|
||||
# the front of the list instead of relying on declaration order.
|
||||
if bot_type == main_bot_type:
|
||||
providers.insert(0, provider)
|
||||
else:
|
||||
providers.append(provider)
|
||||
|
||||
1. User explicit config: tool.vision.model in config.json
|
||||
2. Fallback to the main configured model name
|
||||
def _main_bot_supports_vision(self, bot) -> bool:
|
||||
"""
|
||||
tool_conf = conf().get("tool", {})
|
||||
user_vision_model = tool_conf.get("vision", {}).get("model") if isinstance(tool_conf, dict) else None
|
||||
if user_vision_model:
|
||||
return user_vision_model
|
||||
model_name = conf().get("model", "")
|
||||
return model_name or None
|
||||
Whether the main bot is known to natively support vision.
|
||||
|
||||
Having a `call_vision` method is necessary but not sufficient —
|
||||
some bots implement the method against an endpoint that does not
|
||||
actually serve vision models, which causes silent failures when a
|
||||
vendor-foreign model name is forwarded.
|
||||
|
||||
Resolution order:
|
||||
1. If the bot explicitly declares `supports_vision`, trust it.
|
||||
This lets bots opt in or out based on their own runtime
|
||||
configuration (e.g. the currently selected model).
|
||||
2. Otherwise, fall back to a model-name prefix heuristic: trust
|
||||
call_vision when the main model looks like an OpenAI family
|
||||
model or matches a known multimodal vendor prefix.
|
||||
"""
|
||||
if bot is None:
|
||||
return False
|
||||
if hasattr(bot, "supports_vision"):
|
||||
return bool(getattr(bot, "supports_vision"))
|
||||
main_model = (conf().get("model") or "").lower()
|
||||
if not main_model:
|
||||
return False
|
||||
if main_model.startswith(_OPENAI_MODEL_PREFIXES):
|
||||
return True
|
||||
return self._infer_provider_from_model(main_model) is not None
|
||||
|
||||
def _build_main_model_provider(self) -> Optional[VisionProvider]:
|
||||
"""
|
||||
Use the vendor's own model for vision via bot.call_vision.
|
||||
Only available when the bot class has call_vision.
|
||||
Gated by _main_bot_supports_vision so non-vision bots (DeepSeek, etc.)
|
||||
do not get routed vendor-foreign model names.
|
||||
"""
|
||||
if not (self.model and hasattr(self.model, 'bot')):
|
||||
return None
|
||||
try:
|
||||
bot = self.model.bot
|
||||
if not hasattr(bot, 'call_vision'):
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
if not hasattr(bot, 'call_vision'):
|
||||
return None
|
||||
if not self._main_bot_supports_vision(bot):
|
||||
return None
|
||||
|
||||
vision_model = self._resolve_vision_model()
|
||||
# Use the configured main model name; do NOT inject tool.vision.model
|
||||
# here, because by the time we reach this branch the tool.vision.model
|
||||
# routing has already been attempted (and either matched the main bot
|
||||
# or failed to find a provider).
|
||||
main_model_name = conf().get("model") or None
|
||||
|
||||
return VisionProvider(
|
||||
name=_MAIN_MODEL_PROVIDER_NAME,
|
||||
api_key="",
|
||||
api_base="",
|
||||
model_override=vision_model,
|
||||
model_override=main_model_name,
|
||||
use_bot=True,
|
||||
)
|
||||
|
||||
def _build_openai_provider(self) -> Optional[VisionProvider]:
|
||||
def _build_openai_provider(self, preferred_model: Optional[str] = None) -> Optional[VisionProvider]:
|
||||
api_key = conf().get("open_ai_api_key") or os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
return None
|
||||
api_base = (conf().get("open_ai_api_base") or os.environ.get("OPENAI_API_BASE", "")).rstrip("/") \
|
||||
or "https://api.openai.com/v1"
|
||||
return VisionProvider(name="OpenAI", api_key=api_key, api_base=self._ensure_v1(api_base))
|
||||
# Only honor preferred_model when it looks like an OpenAI-family name;
|
||||
# otherwise the OpenAI endpoint would 400 on a vendor-specific name.
|
||||
model_override = preferred_model if (
|
||||
preferred_model and preferred_model.lower().startswith(_OPENAI_MODEL_PREFIXES)
|
||||
) else None
|
||||
return VisionProvider(
|
||||
name="OpenAI",
|
||||
api_key=api_key,
|
||||
api_base=self._ensure_v1(api_base),
|
||||
model_override=model_override,
|
||||
)
|
||||
|
||||
def _build_linkai_provider(self) -> Optional[VisionProvider]:
|
||||
def _build_linkai_provider(self, preferred_model: Optional[str] = None) -> Optional[VisionProvider]:
|
||||
api_key = conf().get("linkai_api_key") or os.environ.get("LINKAI_API_KEY")
|
||||
if not api_key:
|
||||
return None
|
||||
@@ -290,8 +494,15 @@ class Vision(BaseTool):
|
||||
extra = get_cloud_headers(api_key)
|
||||
extra.pop("Authorization", None)
|
||||
extra.pop("Content-Type", None)
|
||||
return VisionProvider(name="LinkAI", api_key=api_key, api_base=self._ensure_v1(api_base),
|
||||
extra_headers=extra)
|
||||
# LinkAI is a multi-vendor proxy and accepts most model names, so we
|
||||
# honor any user-configured model name here.
|
||||
return VisionProvider(
|
||||
name="LinkAI",
|
||||
api_key=api_key,
|
||||
api_base=self._ensure_v1(api_base),
|
||||
extra_headers=extra,
|
||||
model_override=preferred_model,
|
||||
)
|
||||
|
||||
def _call_via_bot(self, model: str, question: str, image_content: dict,
|
||||
provider: Optional[VisionProvider] = None) -> ToolResult:
|
||||
|
||||
36
app.py
36
app.py
@@ -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()
|
||||
|
||||
@@ -14,6 +14,7 @@ from bridge.reply import Reply, ReplyType
|
||||
from common import const
|
||||
from common.log import logger
|
||||
from common.utils import expand_path
|
||||
from config import conf
|
||||
from models.openai_compatible_bot import OpenAICompatibleBot
|
||||
|
||||
|
||||
@@ -68,6 +69,7 @@ class AgentLLMModel(LLMModel):
|
||||
_MODEL_BOT_TYPE_MAP = {
|
||||
"wenxin": const.BAIDU, "wenxin-4": const.BAIDU,
|
||||
"xunfei": const.XUNFEI, const.QWEN: const.QWEN_DASHSCOPE,
|
||||
const.QIANFAN: const.QIANFAN,
|
||||
const.MODELSCOPE: const.MODELSCOPE,
|
||||
}
|
||||
_MODEL_PREFIX_MAP = [
|
||||
@@ -75,10 +77,10 @@ class AgentLLMModel(LLMModel):
|
||||
("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), ("claude", const.CLAUDEAPI),
|
||||
("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT),
|
||||
("doubao", const.DOUBAO), ("deepseek", const.DEEPSEEK),
|
||||
("ernie", const.QIANFAN),
|
||||
]
|
||||
|
||||
def __init__(self, bridge: Bridge, bot_type: str = "chat"):
|
||||
from config import conf
|
||||
super().__init__(model=conf().get("model", const.GPT_41))
|
||||
self.bridge = bridge
|
||||
self.bot_type = bot_type
|
||||
@@ -87,7 +89,6 @@ class AgentLLMModel(LLMModel):
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
from config import conf
|
||||
return conf().get("model", const.GPT_41)
|
||||
|
||||
@model.setter
|
||||
@@ -96,8 +97,6 @@ class AgentLLMModel(LLMModel):
|
||||
|
||||
def _resolve_bot_type(self, model_name: str) -> str:
|
||||
"""Resolve bot type from model name, matching Bridge.__init__ logic."""
|
||||
from config import conf
|
||||
|
||||
if conf().get("use_linkai", False) and conf().get("linkai_api_key"):
|
||||
return const.LINKAI
|
||||
# Support custom bot type configuration
|
||||
@@ -117,8 +116,9 @@ class AgentLLMModel(LLMModel):
|
||||
return const.MOONSHOT
|
||||
if conf().get("bot_type") == "modelscope":
|
||||
return const.MODELSCOPE
|
||||
lowered_model = model_name.lower()
|
||||
for prefix, btype in self._MODEL_PREFIX_MAP:
|
||||
if model_name.startswith(prefix):
|
||||
if lowered_model.startswith(prefix):
|
||||
return btype
|
||||
return const.OPENAI
|
||||
|
||||
@@ -160,13 +160,23 @@ 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
|
||||
|
||||
# Thinking mode is a global toggle independent of the channel.
|
||||
# IM channels (WeChat/WeCom/DingTalk/Feishu) won't render the
|
||||
# reasoning trace, but still benefit from the higher answer
|
||||
# quality the thinking pass produces.
|
||||
from config import conf
|
||||
kwargs['thinking'] = (
|
||||
{"type": "enabled"} if conf().get("enable_thinking", False)
|
||||
else {"type": "disabled"}
|
||||
)
|
||||
|
||||
response = self.bot.call_with_tools(**kwargs)
|
||||
return self._format_response(response)
|
||||
else:
|
||||
@@ -205,13 +215,23 @@ 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
|
||||
|
||||
# Thinking mode is a global toggle independent of the channel.
|
||||
# IM channels (WeChat/WeCom/DingTalk/Feishu) won't render the
|
||||
# reasoning trace, but still benefit from the higher answer
|
||||
# quality the thinking pass produces.
|
||||
from config import conf
|
||||
kwargs['thinking'] = (
|
||||
{"type": "enabled"} if conf().get("enable_thinking", False)
|
||||
else {"type": "disabled"}
|
||||
)
|
||||
|
||||
stream = self.bot.call_with_tools(**kwargs)
|
||||
|
||||
# Convert stream format to our expected format
|
||||
@@ -398,6 +418,18 @@ class AgentBridge:
|
||||
# Store session_id on agent so executor can clear DB on fatal errors
|
||||
agent._current_session_id = session_id
|
||||
|
||||
# Bound the in-memory context for scheduler sessions before each run.
|
||||
# Scheduler sessions are stable per-task and append every trigger,
|
||||
# so without trimming they would grow unbounded across runs and
|
||||
# blow up prompt cost. Regular user chats are not touched here —
|
||||
# the agent's own context manager handles that path.
|
||||
if session_id and session_id.startswith("scheduler_"):
|
||||
from config import conf
|
||||
scheduler_keep_turns = max(
|
||||
1, int(conf().get("agent_max_context_turns", 20)) // 5
|
||||
)
|
||||
self._trim_in_memory_to_turns(agent, scheduler_keep_turns)
|
||||
|
||||
try:
|
||||
# Use agent's run_stream method with event handler
|
||||
response = agent.run_stream(
|
||||
@@ -430,7 +462,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:
|
||||
@@ -499,10 +531,14 @@ class AgentBridge:
|
||||
reply.text_content = text_response
|
||||
return reply
|
||||
|
||||
# For other unknown file types, return text with file info
|
||||
message = text_response or file_info.get("message", "文件已准备")
|
||||
message += f"\n\n[文件: {file_info.get('file_name', file_path)}]"
|
||||
return Reply(ReplyType.TEXT, message)
|
||||
# For all other file types (tar.gz, zip, etc.), also use FILE type
|
||||
file_url = f"file://{file_path}"
|
||||
logger.info(f"[AgentBridge] Sending generic file: {file_url}")
|
||||
reply = Reply(ReplyType.FILE, file_url)
|
||||
reply.file_name = file_info.get("file_name", os.path.basename(file_path))
|
||||
if text_response:
|
||||
reply.text_content = text_response
|
||||
return reply
|
||||
|
||||
def _migrate_config_to_env(self, workspace_root: str):
|
||||
"""
|
||||
@@ -588,18 +624,245 @@ 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}"
|
||||
)
|
||||
|
||||
# Marker used to identify scheduler-injected user messages so we can apply
|
||||
# a sliding window without touching real user turns. The legacy prefix
|
||||
# "Scheduled task" (written by the v2 PR) is also recognised when pruning,
|
||||
# so old data can be aged out instead of leaking forever.
|
||||
_SCHEDULED_MARKER = "[SCHEDULED]"
|
||||
_SCHEDULED_LEGACY_MARKERS = ("Scheduled task",)
|
||||
|
||||
def remember_scheduled_output(
|
||||
self,
|
||||
session_id: str,
|
||||
content: str,
|
||||
channel_type: str = "",
|
||||
task_description: str = "",
|
||||
) -> None:
|
||||
"""Add the visible output of a scheduled task to the receiver's session.
|
||||
|
||||
Scheduled task execution uses an isolated session so internal planning and
|
||||
tool calls do not leak into the user's chat. The final message is still
|
||||
part of the conversation from the user's point of view, so keep a small
|
||||
visible turn in the receiver session for follow-up questions.
|
||||
|
||||
Configuration:
|
||||
scheduler_inject_to_session (bool, default True):
|
||||
Master switch. When False, this method is a no-op.
|
||||
scheduler_inject_max_per_session (int, default 3):
|
||||
Maximum scheduler-injected user/assistant pairs retained per
|
||||
session. Older injections are pruned automatically.
|
||||
|
||||
Content is truncated to 2000 chars to prevent a single high-volume task
|
||||
from bloating one entry.
|
||||
"""
|
||||
from config import conf
|
||||
if not conf().get("scheduler_inject_to_session", True):
|
||||
return
|
||||
if not session_id or not content:
|
||||
return
|
||||
|
||||
max_len = 2000
|
||||
if len(content) > max_len:
|
||||
content = content[:max_len] + "..."
|
||||
|
||||
user_text = self._SCHEDULED_MARKER
|
||||
if task_description:
|
||||
user_text = f"{self._SCHEDULED_MARKER} {task_description}"
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": user_text}]},
|
||||
{"role": "assistant", "content": [{"type": "text", "text": content}]},
|
||||
]
|
||||
|
||||
# Persist first so the new pair gets a stable seq, then prune old
|
||||
# scheduler pairs in DB, then sync the in-memory agent.messages buffer.
|
||||
self._persist_messages(session_id, messages, channel_type)
|
||||
|
||||
keep_last_n = max(int(conf().get("scheduler_inject_max_per_session", 3) or 0), 0)
|
||||
try:
|
||||
from agent.memory import get_conversation_store
|
||||
deleted = get_conversation_store().prune_scheduled_messages(
|
||||
session_id, keep_last_n=keep_last_n
|
||||
)
|
||||
if deleted:
|
||||
logger.debug(
|
||||
f"[AgentBridge] Pruned {deleted} old scheduler messages "
|
||||
f"for session={session_id} (keep_last_n={keep_last_n})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[AgentBridge] Failed to prune scheduled messages "
|
||||
f"for session={session_id}: {e}"
|
||||
)
|
||||
|
||||
agent = self.agents.get(session_id)
|
||||
if agent:
|
||||
try:
|
||||
with agent.messages_lock:
|
||||
agent.messages.extend(messages)
|
||||
self._prune_scheduled_in_memory(agent, keep_last_n)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[AgentBridge] Failed to update in-memory scheduled output "
|
||||
f"for session={session_id}: {e}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _trim_in_memory_to_turns(agent, keep_turns: int) -> None:
|
||||
"""Bound ``agent.messages`` to the most recent ``keep_turns`` real
|
||||
user/assistant turns, dropping older history together with any
|
||||
intermediate tool_use/tool_result blocks that belonged to it.
|
||||
|
||||
A "real" user message is any user message whose content is not solely a
|
||||
tool_result block — matches the heuristic used elsewhere when filtering
|
||||
history (see ``AgentInitializer._filter_text_only_messages``).
|
||||
|
||||
No-op when the session is already within budget. Caller does not need
|
||||
to hold the lock; this method acquires it itself.
|
||||
"""
|
||||
if keep_turns <= 0:
|
||||
return
|
||||
|
||||
def _is_real_user(msg) -> bool:
|
||||
if not isinstance(msg, dict) or msg.get("role") != "user":
|
||||
return False
|
||||
content = msg.get("content")
|
||||
if isinstance(content, list):
|
||||
if any(
|
||||
isinstance(b, dict) and b.get("type") == "tool_result"
|
||||
for b in content
|
||||
):
|
||||
return False
|
||||
return any(
|
||||
isinstance(b, dict) and b.get("type") == "text" and b.get("text")
|
||||
for b in content
|
||||
)
|
||||
if isinstance(content, str):
|
||||
return bool(content.strip())
|
||||
return False
|
||||
|
||||
with agent.messages_lock:
|
||||
msgs = agent.messages
|
||||
real_user_indices = [i for i, m in enumerate(msgs) if _is_real_user(m)]
|
||||
if len(real_user_indices) <= keep_turns:
|
||||
return
|
||||
|
||||
# Cut at the (k-th from the end) real user message; keep everything
|
||||
# from there onwards so the surviving slice is still a valid
|
||||
# user/assistant sequence.
|
||||
cut_idx = real_user_indices[-keep_turns]
|
||||
if cut_idx == 0:
|
||||
return
|
||||
|
||||
kept = msgs[cut_idx:]
|
||||
msgs.clear()
|
||||
msgs.extend(kept)
|
||||
logger.debug(
|
||||
f"[AgentBridge] Trimmed in-memory messages to last "
|
||||
f"{keep_turns} turns ({len(kept)} messages remain)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _prune_scheduled_in_memory(cls, agent, keep_last_n: int) -> None:
|
||||
"""Mirror conversation_store.prune_scheduled_messages on agent.messages.
|
||||
|
||||
Caller must hold ``agent.messages_lock``.
|
||||
"""
|
||||
if keep_last_n < 0:
|
||||
keep_last_n = 0
|
||||
|
||||
markers = (cls._SCHEDULED_MARKER,) + cls._SCHEDULED_LEGACY_MARKERS
|
||||
|
||||
def _is_marker_user(msg) -> bool:
|
||||
if not isinstance(msg, dict) or msg.get("role") != "user":
|
||||
return False
|
||||
content = msg.get("content")
|
||||
text = ""
|
||||
if isinstance(content, str):
|
||||
text = content
|
||||
elif isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text = block.get("text", "")
|
||||
break
|
||||
return any(text.startswith(m) for m in markers)
|
||||
|
||||
msgs = agent.messages
|
||||
pair_indices = [] # list of (user_idx, assistant_idx_or_None)
|
||||
for idx, msg in enumerate(msgs):
|
||||
if not _is_marker_user(msg):
|
||||
continue
|
||||
assistant_idx = None
|
||||
if idx + 1 < len(msgs):
|
||||
nxt = msgs[idx + 1]
|
||||
if isinstance(nxt, dict) and nxt.get("role") == "assistant":
|
||||
assistant_idx = idx + 1
|
||||
pair_indices.append((idx, assistant_idx))
|
||||
|
||||
if len(pair_indices) <= keep_last_n:
|
||||
return
|
||||
|
||||
to_drop = pair_indices[: len(pair_indices) - keep_last_n]
|
||||
drop_set = set()
|
||||
for u_idx, a_idx in to_drop:
|
||||
drop_set.add(u_idx)
|
||||
if a_idx is not None:
|
||||
drop_set.add(a_idx)
|
||||
|
||||
# Rebuild the list in place to keep external references stable.
|
||||
kept = [m for i, m in enumerate(msgs) if i not in drop_set]
|
||||
msgs.clear()
|
||||
msgs.extend(kept)
|
||||
|
||||
@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
|
||||
@@ -685,4 +948,4 @@ class AgentBridge:
|
||||
agent.tools = [t for t in agent.tools if t.name != "web_search"]
|
||||
logger.info("[AgentBridge] web_search tool removed (API key no longer available)")
|
||||
except Exception as e:
|
||||
logger.debug(f"[AgentBridge] Failed to refresh conditional tools: {e}")
|
||||
logger.debug(f"[AgentBridge] Failed to refresh conditional tools: {e}")
|
||||
|
||||
@@ -26,8 +26,7 @@ class AgentEventHandler:
|
||||
if context:
|
||||
self.channel = context.kwargs.get("channel") if hasattr(context, "kwargs") else None
|
||||
|
||||
# Track current thinking for channel output
|
||||
self.current_thinking = ""
|
||||
self.current_content = ""
|
||||
self.turn_number = 0
|
||||
|
||||
def handle_event(self, event):
|
||||
@@ -47,6 +46,8 @@ class AgentEventHandler:
|
||||
self._handle_message_update(data)
|
||||
elif event_type == "message_end":
|
||||
self._handle_message_end(data)
|
||||
elif event_type == "reasoning_update":
|
||||
pass
|
||||
elif event_type == "tool_execution_start":
|
||||
self._handle_tool_execution_start(data)
|
||||
elif event_type == "tool_execution_end":
|
||||
@@ -59,30 +60,26 @@ class AgentEventHandler:
|
||||
def _handle_turn_start(self, data):
|
||||
"""Handle turn start event"""
|
||||
self.turn_number = data.get("turn", 0)
|
||||
self.has_tool_calls_in_turn = False
|
||||
self.current_thinking = ""
|
||||
self.current_content = ""
|
||||
|
||||
def _handle_message_update(self, data):
|
||||
"""Handle message update event (streaming text)"""
|
||||
"""Handle message update event (streaming content text)"""
|
||||
delta = data.get("delta", "")
|
||||
self.current_thinking += delta
|
||||
self.current_content += delta
|
||||
|
||||
def _handle_message_end(self, data):
|
||||
"""Handle message end event"""
|
||||
tool_calls = data.get("tool_calls", [])
|
||||
|
||||
# Only send thinking process if followed by tool calls
|
||||
if tool_calls:
|
||||
if self.current_thinking.strip():
|
||||
logger.info(f"💭 {self.current_thinking.strip()[:200]}{'...' if len(self.current_thinking) > 200 else ''}")
|
||||
# Send thinking process to channel
|
||||
self._send_to_channel(f"{self.current_thinking.strip()}")
|
||||
if self.current_content.strip():
|
||||
logger.info(f"💭 {self.current_content.strip()[:200]}{'...' if len(self.current_content) > 200 else ''}")
|
||||
self._send_to_channel(self.current_content.strip())
|
||||
else:
|
||||
# No tool calls = final response (logged at agent_stream level)
|
||||
if self.current_thinking.strip():
|
||||
logger.debug(f"💬 {self.current_thinking.strip()[:200]}{'...' if len(self.current_thinking) > 200 else ''}")
|
||||
if self.current_content.strip():
|
||||
logger.debug(f"💬 {self.current_content.strip()[:200]}{'...' if len(self.current_content) > 200 else ''}")
|
||||
|
||||
self.current_thinking = ""
|
||||
self.current_content = ""
|
||||
|
||||
def _handle_tool_execution_start(self, data):
|
||||
"""Handle tool execution start event - logged by agent_stream.py"""
|
||||
|
||||
@@ -144,7 +144,15 @@ class AgentInitializer:
|
||||
from agent.memory import get_conversation_store
|
||||
store = get_conversation_store()
|
||||
max_turns = conf().get("agent_max_context_turns", 20)
|
||||
restore_turns = max(3, max_turns // 6)
|
||||
# Scheduler tasks run on a stable isolated session per task and
|
||||
# can fire many times a day; a smaller restore window keeps prompt
|
||||
# cost bounded while still letting the agent see "last few" runs
|
||||
# for trend / dedup style logic. Regular chat sessions keep the
|
||||
# original heuristic so user dialogues feel continuous.
|
||||
if session_id.startswith("scheduler_"):
|
||||
restore_turns = max(1, max_turns // 5)
|
||||
else:
|
||||
restore_turns = max(3, max_turns // 6)
|
||||
saved = store.load_messages(session_id, max_turns=restore_turns)
|
||||
if saved:
|
||||
filtered = self._filter_text_only_messages(saved)
|
||||
@@ -548,17 +556,23 @@ class AgentInitializer:
|
||||
import threading
|
||||
|
||||
def _daily_flush_loop():
|
||||
import random
|
||||
last_run_date = None # Track last successful run date to prevent same-day re-trigger
|
||||
while True:
|
||||
try:
|
||||
now = datetime.datetime.now()
|
||||
target = now.replace(hour=23, minute=55, second=0, microsecond=0)
|
||||
if target <= now:
|
||||
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)
|
||||
# Always schedule for tomorrow if we already ran today, or if target time has passed
|
||||
if target <= now or (last_run_date == now.date()):
|
||||
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()
|
||||
last_run_date = datetime.datetime.now().date()
|
||||
except Exception as e:
|
||||
logger.warning(f"[DailyFlush] Error in daily flush loop: {e}")
|
||||
time.sleep(3600)
|
||||
@@ -567,7 +581,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 +591,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 +606,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}")
|
||||
|
||||
@@ -61,6 +61,11 @@ class Bridge(object):
|
||||
if model_type and model_type.startswith("deepseek"):
|
||||
self.btype["chat"] = const.DEEPSEEK
|
||||
|
||||
if model_type and isinstance(model_type, str):
|
||||
lowered_model_type = model_type.lower()
|
||||
if lowered_model_type == const.QIANFAN or lowered_model_type.startswith("ernie"):
|
||||
self.btype["chat"] = const.QIANFAN
|
||||
|
||||
if model_type in [const.MODELSCOPE]:
|
||||
self.btype["chat"] = const.MODELSCOPE
|
||||
|
||||
|
||||
@@ -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:
|
||||
# 先发送文本
|
||||
|
||||
@@ -55,12 +55,186 @@ def _ensure_lark_imported():
|
||||
return lark
|
||||
|
||||
|
||||
def _print_qr_to_terminal(qr_url: str):
|
||||
"""Render a QR code as ASCII art and emit it via logger.
|
||||
|
||||
走 logger 而非 print 是为了避免 nohup/cow 后台启动场景下 stdout 块缓冲导致
|
||||
二维码滞后输出(看起来像出现了两次)。logger 的 StreamHandler 是行缓冲,
|
||||
既能在前台终端看到,也能进 run.log。
|
||||
"""
|
||||
qr_lines = []
|
||||
try:
|
||||
import qrcode as qr_lib
|
||||
import io
|
||||
qr = qr_lib.QRCode(error_correction=qr_lib.constants.ERROR_CORRECT_L, box_size=1, border=1)
|
||||
qr.add_data(qr_url)
|
||||
qr.make(fit=True)
|
||||
buf = io.StringIO()
|
||||
qr.print_ascii(out=buf, invert=True)
|
||||
qr_lines = buf.getvalue().splitlines()
|
||||
except ImportError:
|
||||
qr_lines = ["(未安装 qrcode 包,无法渲染 ASCII 二维码:pip install qrcode)"]
|
||||
except Exception as e:
|
||||
qr_lines = [f"(渲染二维码失败:{e})"]
|
||||
|
||||
header = "=" * 60
|
||||
banner = [
|
||||
"",
|
||||
header,
|
||||
" 飞书一键创建应用:请使用 飞书 App 扫描下方二维码",
|
||||
" (二维码 10 分钟内有效,仅供一次扫描)",
|
||||
header,
|
||||
]
|
||||
footer = [
|
||||
f" 或点击链接创建: {qr_url}",
|
||||
" 等待扫码...",
|
||||
"",
|
||||
]
|
||||
full = banner + qr_lines + footer
|
||||
logger.info("[FeiShu] One-click 飞书应用创建二维码(请用飞书 App 扫码):\n" + "\n".join(full))
|
||||
|
||||
|
||||
def _persist_feishu_credentials(app_id: str, app_secret: str) -> bool:
|
||||
"""Write feishu_app_id / feishu_app_secret + ensure feishu in channel_type into config.json.
|
||||
|
||||
Returns True on success, False on failure (e.g. config.json missing or unwritable).
|
||||
"""
|
||||
try:
|
||||
config_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
"config.json",
|
||||
)
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
file_cfg = json.load(f)
|
||||
else:
|
||||
file_cfg = {}
|
||||
|
||||
file_cfg["feishu_app_id"] = app_id
|
||||
file_cfg["feishu_app_secret"] = app_secret
|
||||
|
||||
# 保证 channel_type 中包含 feishu(用户可能纯通过 CLI 启动单通道)
|
||||
ch_type = file_cfg.get("channel_type", conf().get("channel_type", "")) or ""
|
||||
existing = [s.strip() for s in ch_type.split(",") if s.strip()]
|
||||
if "feishu" not in existing:
|
||||
existing.append("feishu")
|
||||
file_cfg["channel_type"] = ",".join(existing)
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(file_cfg, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# 同步到内存中的 conf(),让本次启动直接生效
|
||||
conf()["feishu_app_id"] = app_id
|
||||
conf()["feishu_app_secret"] = app_secret
|
||||
if "channel_type" in file_cfg:
|
||||
conf()["channel_type"] = file_cfg["channel_type"]
|
||||
|
||||
try:
|
||||
os.chmod(config_path, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[FeiShu] Failed to persist credentials to config.json: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _register_via_qr_in_terminal() -> bool:
|
||||
"""CLI-side one-click app creation via lark_oapi.register_app.
|
||||
|
||||
Blocks the calling thread (typically the channel startup thread) until the user
|
||||
finishes scanning, the QR code expires, or registration is cancelled.
|
||||
|
||||
Returns True if credentials were obtained AND persisted; False otherwise.
|
||||
The caller should fall back to the original "missing credentials" error in that case.
|
||||
"""
|
||||
if not LARK_SDK_AVAILABLE:
|
||||
logger.error(
|
||||
"[FeiShu] 缺少 feishu_app_id / feishu_app_secret。"
|
||||
"未安装 lark-oapi SDK,无法在终端发起扫码创建。"
|
||||
"请执行 pip install -U 'lark-oapi>=1.5.5' 后重试,或手动在 config.json 中填入凭据。"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
lark_mod = _ensure_lark_imported()
|
||||
except Exception as e:
|
||||
logger.error(f"[FeiShu] Import lark_oapi failed: {e}")
|
||||
return False
|
||||
|
||||
# register_app 是 lark-oapi 1.5.5 才引入的能力,旧版本调用会得到难以理解的
|
||||
# AttributeError。提前显式检查,给出明确的升级提示。
|
||||
if not hasattr(lark_mod, "register_app"):
|
||||
try:
|
||||
from importlib.metadata import version as _pkg_version
|
||||
installed = _pkg_version("lark-oapi")
|
||||
except Exception:
|
||||
installed = "unknown"
|
||||
logger.error(
|
||||
f"[FeiShu] 当前 lark-oapi 版本 ({installed}) 不支持一键创建应用,需要 >= 1.5.5。"
|
||||
"请执行 pip install -U 'lark-oapi>=1.5.5' 后重试,或手动在 config.json 中填入凭据。"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info("[FeiShu] 检测到尚未配置 feishu_app_id / feishu_app_secret,"
|
||||
"正在向飞书申请一键创建应用...")
|
||||
|
||||
def _on_qr(info):
|
||||
url = info.get("url", "")
|
||||
if url:
|
||||
_print_qr_to_terminal(url)
|
||||
|
||||
def _on_status(info):
|
||||
# 过滤 polling 心跳(每 5 秒一次),保留 slow_down / domain_switched 等
|
||||
status = info.get("status")
|
||||
if status == "polling":
|
||||
return
|
||||
logger.info(f"[FeiShu] register_app status: {info}")
|
||||
|
||||
try:
|
||||
result = lark_mod.register_app(
|
||||
on_qr_code=_on_qr,
|
||||
on_status_change=_on_status,
|
||||
source="cowagent",
|
||||
)
|
||||
except Exception as e:
|
||||
err_cls = e.__class__.__name__
|
||||
if "Expired" in err_cls:
|
||||
logger.error("[FeiShu] 二维码已过期,请重启程序后重试。")
|
||||
elif "Denied" in err_cls:
|
||||
logger.error("[FeiShu] 已取消授权。")
|
||||
else:
|
||||
logger.error(f"[FeiShu] 一键创建失败:{e}")
|
||||
return False
|
||||
|
||||
app_id = result.get("client_id", "")
|
||||
app_secret = result.get("client_secret", "")
|
||||
if not app_id or not app_secret:
|
||||
logger.error("[FeiShu] 创建结果缺少 app_id/app_secret,无法继续。")
|
||||
return False
|
||||
|
||||
if not _persist_feishu_credentials(app_id, app_secret):
|
||||
logger.error(
|
||||
"[FeiShu] 应用创建成功但写入 config.json 失败,请手动复制以下值到配置文件:\n"
|
||||
f" feishu_app_id = {app_id}\n"
|
||||
f" feishu_app_secret = {app_secret}"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(f"[FeiShu] 应用创建成功,凭据已写入 config.json (app_id={app_id})。")
|
||||
return True
|
||||
|
||||
|
||||
@singleton
|
||||
class FeiShuChanel(ChatChannel):
|
||||
feishu_app_id = conf().get('feishu_app_id')
|
||||
feishu_app_secret = conf().get('feishu_app_secret')
|
||||
feishu_token = conf().get('feishu_token')
|
||||
feishu_event_mode = conf().get('feishu_event_mode', 'websocket') # webhook 或 websocket
|
||||
# 覆盖父类默认值 [ReplyType.VOICE, ReplyType.IMAGE]。
|
||||
# 飞书原生支持发送音频(opus 格式,通过文件上传接口)和图片,
|
||||
# 所有回复类型均已处理,置为空列表以启用语音和图片回复。
|
||||
NOT_SUPPORT_REPLYTYPE = []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -86,6 +260,20 @@ class FeiShuChanel(ChatChannel):
|
||||
self.feishu_app_secret = conf().get('feishu_app_secret')
|
||||
self.feishu_token = conf().get('feishu_token')
|
||||
self.feishu_event_mode = conf().get('feishu_event_mode', 'websocket')
|
||||
|
||||
# 命令行启动场景:缺少凭据时尝试通过 lark.register_app 在终端弹二维码
|
||||
# 引导用户扫码创建应用。Web 控制台启动同样会走到这里,但控制台用户通常
|
||||
# 已经通过 /api/feishu/register 完成了创建并写回 config.json。
|
||||
if not self.feishu_app_id or not self.feishu_app_secret:
|
||||
if _register_via_qr_in_terminal():
|
||||
self.feishu_app_id = conf().get('feishu_app_id')
|
||||
self.feishu_app_secret = conf().get('feishu_app_secret')
|
||||
else:
|
||||
err = "[FeiShu] feishu_app_id 与 feishu_app_secret 缺失,无法启动通道"
|
||||
logger.error(err)
|
||||
self.report_startup_error(err)
|
||||
return
|
||||
|
||||
self._fetch_bot_open_id()
|
||||
if self.feishu_event_mode == 'websocket':
|
||||
self._startup_websocket()
|
||||
@@ -384,10 +572,22 @@ class FeiShuChanel(ChatChannel):
|
||||
no_need_at=True
|
||||
)
|
||||
if context:
|
||||
# 流式回复模式:向 context 注入 on_event 回调,agent 每产出一段文字时会调用它。
|
||||
# 回调内部先发送一条占位消息获取 message_id,之后通过 PATCH 接口原地更新内容,
|
||||
# 实现打字机效果。回调结束时设置 context["feishu_streamed"]=True,
|
||||
# 让 send() 跳过重复发送,避免最终完整回复再被重复投递一次。
|
||||
# 默认开启流式打字机回复。需机器人开通 cardkit:card:write 权限且飞书客户端 7.20+,
|
||||
# 任意环节失败会自动降级为非流式文本回复。
|
||||
if conf().get("feishu_stream_reply", True):
|
||||
context["on_event"] = self._make_feishu_stream_callback(context, feishu_msg.access_token)
|
||||
self.produce(context)
|
||||
logger.debug(f"[FeiShu] query={feishu_msg.content}, type={feishu_msg.ctype}")
|
||||
|
||||
def send(self, reply: Reply, context: Context):
|
||||
# 如果文本回复已通过流式传输发送,则跳过重复发送
|
||||
if reply.type == ReplyType.TEXT and context.get("feishu_streamed"):
|
||||
logger.debug("[FeiShu] streaming already delivered text reply, skipping send()")
|
||||
return
|
||||
msg = context.get("msg")
|
||||
is_group = context["isgroup"]
|
||||
if msg:
|
||||
@@ -450,6 +650,16 @@ class FeiShuChanel(ChatChannel):
|
||||
msg_type = "file"
|
||||
content_key = "file_key"
|
||||
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
# 语音回复:上传音频文件到飞书,然后发送 audio 类型消息
|
||||
file_key = self._upload_audio(reply.content, access_token)
|
||||
if not file_key:
|
||||
logger.warning("[FeiShu] upload audio failed")
|
||||
return
|
||||
reply_content = file_key
|
||||
msg_type = "audio"
|
||||
content_key = "file_key"
|
||||
|
||||
# Check if we can reply to an existing message (need msg_id)
|
||||
can_reply = is_group and msg and hasattr(msg, 'msg_id') and msg.msg_id
|
||||
|
||||
@@ -481,6 +691,396 @@ class FeiShuChanel(ChatChannel):
|
||||
else:
|
||||
logger.error(f"[FeiShu] send message failed, code={res.get('code')}, msg={res.get('msg')}")
|
||||
|
||||
def _make_feishu_stream_callback(self, context, access_token):
|
||||
"""
|
||||
基于飞书官方"流式更新卡片"API 实现打字机回复。
|
||||
|
||||
流程:
|
||||
1. message_update 首次到达 → POST /cardkit/v1/cards 创建带 streaming_mode 的卡片实体,
|
||||
随后用 POST /im/v1/messages(或 reply)以 card_id 把卡片发出去
|
||||
2. 后续 message_update → PUT /cardkit/v1/cards/{id}/elements/{eid}/content
|
||||
传入"当前轮"的全量文本,飞书平台自动计算增量并以打字机效果上屏
|
||||
(流式模式下不受 10 QPS 限制)
|
||||
3. message_end(一轮 LLM 输出结束,且本轮触发了工具调用)→ 把 current 累计到 committed
|
||||
并加入分隔符;下一轮 message_update 又从空白开始,避免多轮内容串到一起
|
||||
4. agent_end → 用 final_response 强制覆盖卡片,再 PATCH /cardkit/v1/cards/{id}/settings
|
||||
关闭 streaming_mode,标记 context["feishu_streamed"]=True 让 chat_channel 跳过普通 send()
|
||||
|
||||
前提条件:
|
||||
- 机器人已开通 cardkit:card:write 权限
|
||||
- 飞书客户端 7.20+
|
||||
|
||||
失败降级:
|
||||
- 创建卡片实体失败(缺权限、网络等)→ 不设置 feishu_streamed 标记,让 chat_channel
|
||||
走普通文本回复路径,用户收到完整回复但无打字机效果,并打 warning 日志
|
||||
"""
|
||||
# 共享状态(受 lock 保护)
|
||||
# 多轮 agent 模式下,每个"中间过场消息"会作为一张独立卡片发送。
|
||||
# current_text 只承载当前正在流式渲染的那张卡片的内容;message_end / agent_end
|
||||
# 时会把它定型并 reset。
|
||||
current_text = [""] # 当前卡片正在累加的 LLM 输出
|
||||
card_id = [None] # 当前流式卡片的实体 ID(每段独立)
|
||||
message_id = [None] # 当前卡片发送后的消息 ID(仅日志用)
|
||||
# 占位发送是同步进行的,但用一个 in-flight 标记防止并发的多条 message_update
|
||||
# 事件各自触发一次创建+发送,导致发出多张卡片。
|
||||
init_in_flight = [False]
|
||||
# 一旦初始化失败就长期标记为 disabled,本次回复不再尝试任何流式调用
|
||||
disabled = [False]
|
||||
lock = threading.Lock()
|
||||
|
||||
# ---- 异步推送队列 ----------------------------------------------------
|
||||
# 同步 requests.put 单次 100~300ms,会阻塞 LLM stream 线程读下一个 chunk。
|
||||
# 把推送丢给独立 worker 线程消费 queue,回调本身只做内存追加,立即返回。
|
||||
# 队列里只放"最新累积文本"的快照;worker 用 deduplication 避免重复推同一个
|
||||
# 内容(高频 chunk 场景下队列会堆积,只推最后一个就够了)。
|
||||
import queue as _queue
|
||||
push_queue: "_queue.Queue[str | None]" = _queue.Queue()
|
||||
|
||||
def _push_worker():
|
||||
while True:
|
||||
snapshot = push_queue.get()
|
||||
if snapshot is None:
|
||||
push_queue.task_done()
|
||||
return
|
||||
# 合并队列中已堆积的快照:只推最后一个,省 PUT 次数同时降低延迟
|
||||
merged_count = 1
|
||||
stop = False
|
||||
while True:
|
||||
try:
|
||||
nxt = push_queue.get_nowait()
|
||||
except _queue.Empty:
|
||||
break
|
||||
merged_count += 1
|
||||
if nxt is None:
|
||||
stop = True
|
||||
break
|
||||
snapshot = nxt
|
||||
try:
|
||||
_stream_update_text(snapshot)
|
||||
finally:
|
||||
for _ in range(merged_count):
|
||||
push_queue.task_done()
|
||||
if stop:
|
||||
return
|
||||
|
||||
push_thread = threading.Thread(target=_push_worker, daemon=True, name="feishu-stream-push")
|
||||
push_thread.start()
|
||||
|
||||
def _drain_push_queue():
|
||||
"""等当前队列里所有 PUT 都完成。message_end/agent_end 在做最终定型前必须 drain,
|
||||
否则 worker 里堆积的旧快照可能在 final_text PUT 之后到达,把最终内容覆盖掉。"""
|
||||
try:
|
||||
push_queue.join()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
msg = context.get("msg")
|
||||
is_group = context.get("isgroup", False)
|
||||
receiver = context.get("receiver")
|
||||
receive_id_type = context.get("receive_id_type", "open_id")
|
||||
# 客户端打字机渲染参数(飞书 App 侧实际"出字"速度):
|
||||
# - print_freq_ms:每次刷新的间隔
|
||||
# - print_step:每次刷新出多少个字符
|
||||
# 当前 40ms × 4 字 ≈ 100 字/秒,接近 ChatGPT/DeepSeek 网页端的节奏。
|
||||
print_freq_ms = 40
|
||||
print_step = 4
|
||||
print_strategy = "fast"
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + access_token,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
# 卡片中富文本组件的 element_id,后续所有 PUT 流式更新都打到这个组件
|
||||
ELEMENT_ID = "stream_md"
|
||||
# 操作序号,每次 PUT 必须严格递增(飞书要求)
|
||||
sequence = [0]
|
||||
|
||||
def _next_sequence():
|
||||
sequence[0] += 1
|
||||
return sequence[0]
|
||||
|
||||
def _build_card_json():
|
||||
"""卡片 JSON 2.0 结构 + streaming_mode + 单 markdown 组件"""
|
||||
return json.dumps({
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"streaming_mode": True,
|
||||
"summary": {"content": "[正在生成回复...]"},
|
||||
"streaming_config": {
|
||||
"print_frequency_ms": {"default": print_freq_ms},
|
||||
"print_step": {"default": print_step},
|
||||
"print_strategy": print_strategy,
|
||||
},
|
||||
},
|
||||
"body": {
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": "...",
|
||||
"element_id": ELEMENT_ID,
|
||||
}
|
||||
],
|
||||
},
|
||||
# 注意:JSON 2.0 不支持自定义 fallback 字段(传入会报错)。
|
||||
# 客户端 < 7.20 时,飞书会自动展示"请升级客户端"占位,无需配置。
|
||||
}, ensure_ascii=False)
|
||||
|
||||
def _create_and_send_card():
|
||||
"""同步执行:创建卡片实体 → 发送消息。任意一步失败则 disabled=True 触发降级"""
|
||||
try:
|
||||
# 步骤 1: 创建卡片实体
|
||||
create_url = "https://open.feishu.cn/open-apis/cardkit/v1/cards"
|
||||
create_body = {"type": "card_json", "data": _build_card_json()}
|
||||
res = requests.post(
|
||||
create_url, headers=headers, json=create_body, timeout=(5, 10)
|
||||
)
|
||||
res_json = res.json()
|
||||
if res_json.get("code") != 0:
|
||||
logger.warning(
|
||||
f"[FeiShu] Stream: create card failed "
|
||||
f"(code={res_json.get('code')}, msg={res_json.get('msg')}). "
|
||||
f"本次回复已自动降级为普通文本回复(一次性返回完整内容)。"
|
||||
f"如需开启流式打字机效果与完整 Markdown 渲染,请到飞书开放平台 "
|
||||
f"https://open.feishu.cn/app 给机器人开通 cardkit:card:write 权限"
|
||||
f"(创建与更新卡片)并重新发布版本,同时确保飞书客户端 >= 7.20。"
|
||||
)
|
||||
with lock:
|
||||
disabled[0] = True
|
||||
return
|
||||
cid = res_json["data"]["card_id"]
|
||||
with lock:
|
||||
card_id[0] = cid
|
||||
|
||||
# 步骤 2: 通过 card_id 发送消息(群聊优先用 reply,单聊直接 send)
|
||||
content_payload = json.dumps(
|
||||
{"type": "card", "data": {"card_id": cid}}, ensure_ascii=False
|
||||
)
|
||||
can_reply = is_group and msg and hasattr(msg, "msg_id") and msg.msg_id
|
||||
if can_reply:
|
||||
send_url = (
|
||||
f"https://open.feishu.cn/open-apis/im/v1/messages/"
|
||||
f"{msg.msg_id}/reply"
|
||||
)
|
||||
send_body = {"msg_type": "interactive", "content": content_payload}
|
||||
send_res = requests.post(
|
||||
send_url, headers=headers, json=send_body, timeout=(5, 10)
|
||||
)
|
||||
else:
|
||||
send_url = "https://open.feishu.cn/open-apis/im/v1/messages"
|
||||
params = {"receive_id_type": receive_id_type}
|
||||
send_body = {
|
||||
"receive_id": receiver,
|
||||
"msg_type": "interactive",
|
||||
"content": content_payload,
|
||||
}
|
||||
send_res = requests.post(
|
||||
send_url, headers=headers, params=params, json=send_body,
|
||||
timeout=(5, 10),
|
||||
)
|
||||
send_json = send_res.json()
|
||||
if send_json.get("code") != 0:
|
||||
logger.warning(
|
||||
f"[FeiShu] Stream: send card failed: {send_json}. 降级为普通文本。"
|
||||
)
|
||||
with lock:
|
||||
disabled[0] = True
|
||||
return
|
||||
mid = send_json["data"]["message_id"]
|
||||
with lock:
|
||||
message_id[0] = mid
|
||||
logger.info(
|
||||
f"[FeiShu] Stream: card created and sent, "
|
||||
f"card_id={cid}, message_id={mid}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[FeiShu] Stream: create/send card exception: {e}. 降级为普通文本。"
|
||||
)
|
||||
with lock:
|
||||
disabled[0] = True
|
||||
finally:
|
||||
with lock:
|
||||
init_in_flight[0] = False
|
||||
|
||||
def _stream_update_text(full_text):
|
||||
"""PUT 流式更新文本组件。content 必须是当前组件的全量文本。"""
|
||||
with lock:
|
||||
cid = card_id[0]
|
||||
if not cid:
|
||||
return
|
||||
url = (
|
||||
f"https://open.feishu.cn/open-apis/cardkit/v1/cards/"
|
||||
f"{cid}/elements/{ELEMENT_ID}/content"
|
||||
)
|
||||
body = {
|
||||
"content": full_text,
|
||||
"sequence": _next_sequence(),
|
||||
}
|
||||
try:
|
||||
res = requests.put(url, headers=headers, json=body, timeout=(5, 10))
|
||||
res_json = res.json()
|
||||
if res_json.get("code") != 0:
|
||||
logger.warning(
|
||||
f"[FeiShu] Stream: update text failed: {res_json}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[FeiShu] Stream: update text exception: {e}")
|
||||
|
||||
def _close_streaming_mode(final_text: str = ""):
|
||||
"""关闭流式模式(卡片转入"普通"状态,可被转发)。
|
||||
|
||||
同时通过整卡更新接口把 summary 改成最终内容的预览,否则飞书会话列表
|
||||
会一直显示创建卡片时的占位摘要("[正在生成回复...]")。
|
||||
"""
|
||||
with lock:
|
||||
cid = card_id[0]
|
||||
if not cid:
|
||||
return
|
||||
|
||||
# 1) 通过整卡更新接口把 streaming_mode 关掉,并改写 summary
|
||||
# (settings 接口的 config 不接受 summary 字段,会报 code=2200)
|
||||
preview_src = (final_text or "").strip().replace("\n", " ")
|
||||
preview = preview_src[:30] if preview_src else ""
|
||||
full_card = {
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"streaming_mode": False,
|
||||
"summary": {"content": preview or " "},
|
||||
},
|
||||
"body": {
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": final_text or " ",
|
||||
"element_id": ELEMENT_ID,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
put_url = f"https://open.feishu.cn/open-apis/cardkit/v1/cards/{cid}"
|
||||
put_body = {
|
||||
"card": {"type": "card_json", "data": json.dumps(full_card, ensure_ascii=False)},
|
||||
"sequence": _next_sequence(),
|
||||
}
|
||||
try:
|
||||
res = requests.put(put_url, headers=headers, json=put_body, timeout=(5, 10))
|
||||
res_json = res.json()
|
||||
if res_json.get("code") != 0:
|
||||
logger.warning(
|
||||
f"[FeiShu] Stream: finalize card (close+summary) failed: {res_json}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[FeiShu] Stream: finalize card exception: {e}"
|
||||
)
|
||||
|
||||
def on_event(event: dict):
|
||||
event_type = event.get("type")
|
||||
data = event.get("data", {})
|
||||
|
||||
# 一旦降级,本次回复不再做任何流式操作
|
||||
with lock:
|
||||
if disabled[0]:
|
||||
return
|
||||
|
||||
if event_type == "message_update":
|
||||
delta = data.get("delta", "")
|
||||
if not delta:
|
||||
return
|
||||
|
||||
# 第一段:判断是否需要初始化(创建卡片 + 发送)
|
||||
need_init = False
|
||||
with lock:
|
||||
if card_id[0] is None and not init_in_flight[0]:
|
||||
init_in_flight[0] = True
|
||||
need_init = True
|
||||
|
||||
if need_init:
|
||||
_create_and_send_card()
|
||||
# 初始化失败已标记 disabled,下次循环直接 return
|
||||
with lock:
|
||||
if disabled[0]:
|
||||
return
|
||||
|
||||
# 第二段:累加文本,把快照丢给 push worker 异步推送。
|
||||
# 这里不能直接 requests.put,否则会阻塞 LLM stream 线程读下一个 chunk
|
||||
# (实测 DeepSeek 高频小 chunk 场景每个 PUT ~150ms,累积起来非常卡)。
|
||||
snapshot = ""
|
||||
should_push = False
|
||||
with lock:
|
||||
current_text[0] += delta
|
||||
if card_id[0]:
|
||||
snapshot = current_text[0]
|
||||
should_push = True
|
||||
|
||||
if should_push:
|
||||
push_queue.put(snapshot)
|
||||
|
||||
elif event_type == "message_end":
|
||||
# 一轮 LLM 输出结束。如果本轮触发了工具调用,说明当前轮的文本是
|
||||
# "中间过场消息"(如"来看看!"),应该作为独立卡片定型,然后为下一轮
|
||||
# 重新创建一张新卡片。这样最终用户看到的是:
|
||||
# [卡片1: 中间过场1]
|
||||
# [卡片2: 中间过场2]
|
||||
# ...
|
||||
# [卡片N: 最终回复]
|
||||
# 与 wecom_bot 的多消息流式体验对齐。
|
||||
tool_calls = data.get("tool_calls", []) or []
|
||||
if not tool_calls:
|
||||
# 没有工具调用:本轮即最终回复,留给 agent_end 统一处理。
|
||||
return
|
||||
|
||||
with lock:
|
||||
text_to_finalize = current_text[0].rstrip()
|
||||
current_text[0] = ""
|
||||
|
||||
if not text_to_finalize:
|
||||
return
|
||||
|
||||
# 等异步队列里堆积的快照都推完,避免它们晚于 final 文本到达把内容覆盖掉
|
||||
_drain_push_queue()
|
||||
# 用最终文本覆盖当前卡片并关闭流式模式(凝固成普通卡片,
|
||||
# 同时把会话列表的 summary 改成预览,不再显示"正在生成回复...")
|
||||
_stream_update_text(text_to_finalize)
|
||||
_close_streaming_mode(text_to_finalize)
|
||||
|
||||
# 重置卡片状态,下一段 message_update 会触发新卡片的创建
|
||||
with lock:
|
||||
card_id[0] = None
|
||||
message_id[0] = None
|
||||
sequence[0] = 0
|
||||
|
||||
elif event_type == "agent_end":
|
||||
# 最终回复:用 final_response 覆盖当前流式卡片,然后关闭流式模式。
|
||||
final_response = data.get("final_response", "")
|
||||
if not final_response:
|
||||
return
|
||||
final_text = str(final_response)
|
||||
# 标记 streamed 让 chat_channel 跳过 send()
|
||||
context["feishu_streamed"] = True
|
||||
|
||||
with lock:
|
||||
has_card = card_id[0] is not None
|
||||
init_busy = init_in_flight[0]
|
||||
|
||||
# 罕见情况:agent_end 触发时还没创建过卡片(极快返回 / 没有
|
||||
# message_update),主动创建一张承载 final_text。
|
||||
if not has_card and not init_busy:
|
||||
with lock:
|
||||
init_in_flight[0] = True
|
||||
_create_and_send_card()
|
||||
with lock:
|
||||
if disabled[0]:
|
||||
return
|
||||
|
||||
_drain_push_queue()
|
||||
_stream_update_text(final_text)
|
||||
_close_streaming_mode(final_text)
|
||||
# 通知 push worker 退出(本次回复彻底结束)
|
||||
push_queue.put(None)
|
||||
|
||||
return on_event
|
||||
|
||||
def fetch_access_token(self) -> str:
|
||||
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
||||
headers = {
|
||||
@@ -687,6 +1287,66 @@ class FeiShuChanel(ChatChannel):
|
||||
except Exception as e:
|
||||
logger.warning(f"[FeiShu] Failed to remove temp file {temp_file}: {e}")
|
||||
|
||||
def _upload_audio(self, audio_path, access_token):
|
||||
"""
|
||||
Upload a local audio file to Feishu and return file_key.
|
||||
audio_path is a plain local file path (no file:// prefix).
|
||||
Feishu audio messages only support opus format; non-opus files are converted first.
|
||||
"""
|
||||
logger.debug(f"[FeiShu] start upload audio, path={audio_path}")
|
||||
|
||||
if not os.path.exists(audio_path):
|
||||
logger.error(f"[FeiShu] audio file not found: {audio_path}")
|
||||
return None
|
||||
|
||||
# Feishu only plays audio messages in opus format.
|
||||
# Convert if the TTS engine produced a different format (e.g. mp3 from OpenAI TTS).
|
||||
upload_path = audio_path
|
||||
if not audio_path.lower().endswith('.opus'):
|
||||
opus_path = os.path.splitext(audio_path)[0] + '.opus'
|
||||
try:
|
||||
from pydub import AudioSegment
|
||||
audio = AudioSegment.from_file(audio_path)
|
||||
audio.export(opus_path, format='opus')
|
||||
upload_path = opus_path
|
||||
logger.info(f"[FeiShu] Converted audio to opus: {opus_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[FeiShu] Failed to convert audio to opus, uploading original: {e}")
|
||||
upload_path = audio_path
|
||||
|
||||
file_name = os.path.splitext(os.path.basename(upload_path))[0] + '.opus'
|
||||
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
||||
data = {'file_type': 'opus', 'file_name': file_name}
|
||||
headers = {'Authorization': f'Bearer {access_token}'}
|
||||
|
||||
try:
|
||||
with open(upload_path, "rb") as f:
|
||||
upload_response = requests.post(
|
||||
upload_url,
|
||||
files={"file": f},
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=(5, 30)
|
||||
)
|
||||
logger.info(
|
||||
f"[FeiShu] upload audio response, status={upload_response.status_code}, res={upload_response.content}")
|
||||
response_data = upload_response.json()
|
||||
if response_data.get("code") == 0:
|
||||
return response_data.get("data").get("file_key")
|
||||
else:
|
||||
logger.error(f"[FeiShu] upload audio failed: {response_data}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"[FeiShu] upload audio exception: {e}")
|
||||
return None
|
||||
finally:
|
||||
# 无论上传成功与否都清理转换产生的临时 opus 文件,避免失败路径下磁盘堆积。
|
||||
if upload_path != audio_path and os.path.exists(upload_path):
|
||||
try:
|
||||
os.remove(upload_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"[FeiShu] Failed to remove temp opus file {upload_path}: {e}")
|
||||
|
||||
def _upload_file_url(self, file_url, access_token):
|
||||
"""
|
||||
Upload file to Feishu
|
||||
|
||||
@@ -162,6 +162,38 @@ class FeishuMessage(ChatMessage):
|
||||
else:
|
||||
logger.info(f"[FeiShu] Failed to download file, key={file_key}, res={response.text}")
|
||||
self._prepare_fn = _download_file
|
||||
elif msg_type == "audio":
|
||||
# 飞书用户发送的语音消息类型为 "audio",文件为 opus 编码格式。
|
||||
# 映射为 ContextType.VOICE,交由 chat_channel 的语音转文字(STT)流程处理。
|
||||
# 文件通过 _prepare_fn 延迟下载,在 chat_channel 调用 cmsg.prepare() 时才执行。
|
||||
self.ctype = ContextType.VOICE
|
||||
content = json.loads(msg.get("content"))
|
||||
file_key = content.get("file_key")
|
||||
|
||||
self.content = TmpDir().path() + file_key + ".opus"
|
||||
logger.info(f"[FeiShu] audio message: file_key={file_key}, save_path={self.content}")
|
||||
|
||||
def _download_audio():
|
||||
logger.info(f"[FeiShu] downloading audio: file_key={file_key}, msg_id={self.msg_id}")
|
||||
url = f"https://open.feishu.cn/open-apis/im/v1/messages/{self.msg_id}/resources/{file_key}"
|
||||
headers = {
|
||||
"Authorization": "Bearer " + access_token,
|
||||
}
|
||||
params = {
|
||||
"type": "file"
|
||||
}
|
||||
try:
|
||||
response = requests.get(url=url, headers=headers, params=params)
|
||||
logger.info(f"[FeiShu] download audio response: status={response.status_code}, size={len(response.content)} bytes")
|
||||
if response.status_code == 200:
|
||||
with open(self.content, "wb") as f:
|
||||
f.write(response.content)
|
||||
logger.info(f"[FeiShu] audio saved to: {self.content}")
|
||||
else:
|
||||
logger.error(f"[FeiShu] Failed to download audio, key={file_key}, status={response.status_code}, res={response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"[FeiShu] Exception downloading audio, key={file_key}: {e}", exc_info=True)
|
||||
self._prepare_fn = _download_audio
|
||||
else:
|
||||
raise NotImplementedError("Unsupported message type: Type:{} ".format(msg_type))
|
||||
|
||||
|
||||
@@ -50,16 +50,53 @@
|
||||
(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>
|
||||
<body class="h-screen overflow-hidden bg-gray-50 dark:bg-[#111111] text-slate-800 dark:text-slate-200 font-sans">
|
||||
|
||||
<!-- Login Overlay -->
|
||||
<div id="login-overlay" class="fixed inset-0 z-[200] bg-gray-50 dark:bg-[#111111] flex items-center justify-center hidden">
|
||||
<div class="w-full max-w-sm mx-4">
|
||||
<div class="flex flex-col items-center mb-8">
|
||||
<img src="assets/logo.jpg" alt="CowAgent" class="w-16 h-16 rounded-2xl mb-4 shadow-lg">
|
||||
<h1 class="text-xl font-bold text-slate-800 dark:text-slate-100">CowAgent</h1>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" id="login-subtitle">请输入密码以访问控制台</p>
|
||||
</div>
|
||||
<form id="login-form" class="space-y-4" onsubmit="return false;">
|
||||
<div class="relative">
|
||||
<input id="login-password" type="password" autocomplete="current-password"
|
||||
placeholder="Password"
|
||||
class="w-full px-4 py-3 rounded-xl border border-slate-200 dark:border-white/10
|
||||
bg-white dark:bg-[#1A1A1A] text-slate-800 dark:text-slate-200
|
||||
placeholder-slate-400 dark:placeholder-slate-500
|
||||
focus:outline-none focus:ring-2 focus:ring-primary-400/50 focus:border-primary-400
|
||||
transition-all duration-150 text-sm">
|
||||
<button type="button" id="login-toggle-pwd"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600
|
||||
dark:hover:text-slate-300 cursor-pointer transition-colors"
|
||||
onclick="toggleLoginPassword()">
|
||||
<i class="fas fa-eye text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p id="login-error" class="text-sm text-red-500 hidden"></p>
|
||||
<button id="login-btn" type="submit"
|
||||
class="w-full py-3 rounded-xl bg-primary-500 hover:bg-primary-600 text-white font-medium
|
||||
text-sm cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app" class="flex h-screen">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 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 -->
|
||||
@@ -67,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>
|
||||
|
||||
@@ -77,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>
|
||||
@@ -92,33 +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">知识</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>
|
||||
@@ -127,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>
|
||||
@@ -154,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 -->
|
||||
<!-- ================================================================ -->
|
||||
@@ -166,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>
|
||||
@@ -220,26 +288,26 @@
|
||||
<!-- ====================================================== -->
|
||||
<!-- 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 -->
|
||||
<div id="welcome-screen" class="flex flex-col items-center justify-center h-full px-6 py-12">
|
||||
<div id="welcome-screen" class="flex flex-col items-center justify-center h-full px-6 pb-16" style="padding-top: 6vh">
|
||||
<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-1 sm:grid-cols-3 gap-4 w-full max-w-2xl">
|
||||
<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
|
||||
cursor-pointer hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<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">
|
||||
@@ -247,9 +315,9 @@
|
||||
<div class="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<i class="fas fa-clock text-amber-500 text-xs"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_task_title">Smart Task</span>
|
||||
<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">
|
||||
@@ -257,14 +325,57 @@
|
||||
<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">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-7 h-7 rounded-lg bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center">
|
||||
<i class="fas fa-book text-violet-500 text-xs"></i>
|
||||
</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>
|
||||
</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">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-7 h-7 rounded-lg bg-rose-50 dark:bg-rose-900/30 flex items-center justify-center">
|
||||
<i class="fas fa-puzzle-piece text-rose-500 text-xs"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_skill_title">技能系统</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_skill_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"
|
||||
data-send="/help">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-7 h-7 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
|
||||
<i class="fas fa-terminal text-slate-500 text-xs"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm text-slate-700 dark:text-slate-200" data-i18n="example_web_title">指令中心</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed" data-i18n="example_web_text">查看全部命令</p>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
@@ -274,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>
|
||||
@@ -296,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
|
||||
@@ -318,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">
|
||||
@@ -330,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>
|
||||
@@ -343,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>
|
||||
@@ -359,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 -->
|
||||
@@ -394,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>
|
||||
@@ -405,36 +525,86 @@
|
||||
<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="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_max_tokens">Max Context Tokens</label>
|
||||
<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">最大上下文 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"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_max_turns">Max Context Turns</label>
|
||||
<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">最大记忆轮次</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"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_max_steps">Max Steps</label>
|
||||
<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">最大执行步数</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"
|
||||
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">
|
||||
</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>
|
||||
|
||||
<!-- Security Config Card -->
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<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">安全设置</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">访问密码</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">留空则不启用密码保护</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">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,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>
|
||||
@@ -478,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>
|
||||
@@ -505,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>
|
||||
@@ -542,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>
|
||||
@@ -558,6 +738,106 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====================================================== -->
|
||||
<!-- VIEW: Knowledge -->
|
||||
<!-- ====================================================== -->
|
||||
<div id="view-knowledge" class="view">
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-8 lg:p-10">
|
||||
<div class="w-full max-w-[1600px] mx-auto">
|
||||
|
||||
<!-- 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">知识库</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">文档</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">图谱</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div id="knowledge-empty" class="flex flex-col items-center justify-center py-20">
|
||||
<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">加载知识库中...</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">在对话中发送文档、链接或主题给 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">开始对话</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents panel -->
|
||||
<div id="knowledge-panel-docs" class="hidden">
|
||||
<div class="flex flex-col md:flex-row gap-4 md:gap-6" style="min-height: calc(100vh - 220px)">
|
||||
<!-- File tree -->
|
||||
<div id="knowledge-sidebar" class="w-full md:w-72 lg:w-80 flex-shrink-0">
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-slate-200 dark:border-white/10">
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-xs"></i>
|
||||
<input id="knowledge-search" type="text" placeholder="Search..."
|
||||
class="w-full pl-8 pr-3 py-1.5 text-xs bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-lg text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-primary-400/50"
|
||||
oninput="filterKnowledgeTree(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div id="knowledge-tree" class="p-2 overflow-y-auto max-h-[50vh] md:max-h-[calc(100vh-300px)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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">
|
||||
<i class="fas fa-file-lines text-3xl mb-3 opacity-40"></i>
|
||||
<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">
|
||||
<div class="flex items-center gap-3 px-4 md:px-5 py-3 border-b border-slate-200 dark:border-white/10">
|
||||
<button onclick="knowledgeMobileBack()" class="md:hidden p-1 -ml-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer">
|
||||
<i class="fas fa-arrow-left text-xs"></i>
|
||||
</button>
|
||||
<i class="fas fa-file-lines text-slate-400 text-sm hidden md:inline"></i>
|
||||
<span id="knowledge-viewer-title" class="text-sm font-medium text-slate-700 dark:text-slate-200 truncate"></span>
|
||||
<span id="knowledge-viewer-path" class="text-xs text-slate-400 dark:text-slate-500 ml-auto font-mono truncate hidden md:inline"></span>
|
||||
</div>
|
||||
<div id="knowledge-viewer-body"
|
||||
class="p-4 md:p-5 overflow-y-auto text-sm msg-content text-slate-700 dark:text-slate-200"
|
||||
style="max-height: calc(100vh - 280px)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Graph panel -->
|
||||
<div id="knowledge-panel-graph" class="hidden">
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 overflow-hidden">
|
||||
<div id="knowledge-graph-container" class="w-full h-[60vh] md:h-[calc(100vh-220px)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====================================================== -->
|
||||
<!-- VIEW: Channels -->
|
||||
<!-- ====================================================== -->
|
||||
@@ -566,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>
|
||||
@@ -590,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">
|
||||
@@ -613,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 -->
|
||||
@@ -629,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>
|
||||
@@ -670,6 +950,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
||||
<script src="assets/js/console.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -45,7 +392,8 @@
|
||||
.msg-content h1 { font-size: 1.4em; }
|
||||
.msg-content h2 { font-size: 1.25em; }
|
||||
.msg-content h3 { font-size: 1.1em; }
|
||||
.msg-content ul, .msg-content ol { margin: 0.5em 0; padding-left: 1.8em; }
|
||||
.msg-content ul { margin: 0.5em 0; padding-left: 1.8em; list-style: disc; }
|
||||
.msg-content ol { margin: 0.5em 0; padding-left: 1.8em; list-style: decimal; }
|
||||
.msg-content li { margin: 0.25em 0; }
|
||||
.msg-content pre {
|
||||
border-radius: 8px; overflow-x: auto; margin: 0.8em 0;
|
||||
@@ -124,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;
|
||||
@@ -146,7 +493,7 @@
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: #94a3b8;
|
||||
max-height: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dark .agent-thinking-step .thinking-full {
|
||||
@@ -157,6 +504,41 @@
|
||||
.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 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: inherit;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.dark .agent-content-step { border-bottom-color: rgba(255, 255, 255, 0.06); }
|
||||
.agent-content-step .agent-content-body p { margin: 0.25em 0; }
|
||||
.agent-content-step .agent-content-body p:first-child { margin-top: 0; }
|
||||
.agent-content-step .agent-content-body p:last-child { margin-bottom: 0; }
|
||||
|
||||
/* Tool step - collapsible */
|
||||
.agent-tool-step .tool-header {
|
||||
@@ -535,3 +917,193 @@
|
||||
.dark .slash-menu-item .desc {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Knowledge View
|
||||
============================================================ */
|
||||
|
||||
/* Tab toggle */
|
||||
.knowledge-tab, .memory-tab {
|
||||
color: #64748b;
|
||||
}
|
||||
.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 .memory-tab.active {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* File tree */
|
||||
.knowledge-tree-group {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.knowledge-tree-group-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.knowledge-tree-group-btn:hover {
|
||||
background: rgba(0,0,0,0.04);
|
||||
color: #334155;
|
||||
}
|
||||
.dark .knowledge-tree-group-btn:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.knowledge-tree-group-btn i.chevron {
|
||||
font-size: 8px;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.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 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.knowledge-tree-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 8px 5px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.knowledge-tree-file:hover {
|
||||
background: rgba(0,0,0,0.04);
|
||||
color: #334155;
|
||||
}
|
||||
.knowledge-tree-file.active {
|
||||
background: #EDFDF3;
|
||||
color: #228547;
|
||||
}
|
||||
.dark .knowledge-tree-file:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.dark .knowledge-tree-file.active {
|
||||
background: rgba(74, 190, 110, 0.1);
|
||||
color: #4ABE6E;
|
||||
}
|
||||
|
||||
/* Graph legend */
|
||||
.knowledge-graph-legend {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
z-index: 10;
|
||||
}
|
||||
.knowledge-graph-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.knowledge-graph-legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Graph tooltip */
|
||||
.knowledge-graph-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 20;
|
||||
}
|
||||
.dark .knowledge-graph-tooltip {
|
||||
background: #1A1A1A;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Config field tooltip */
|
||||
.cfg-tip {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #94a3b8;
|
||||
cursor: help;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cfg-tip:hover { color: #64748b; }
|
||||
.dark .cfg-tip:hover { color: #cbd5e1; }
|
||||
/* Floating tooltip portal — appended to <body> by JS so it isn't clipped
|
||||
by overflow:hidden ancestors. */
|
||||
.cfg-tip-floating {
|
||||
position: fixed;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
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;
|
||||
z-index: 9999;
|
||||
}
|
||||
.dark .cfg-tip-floating {
|
||||
background: #334155;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.cfg-tip-floating.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Example cards: equal height via flex stretch + fixed 2-line description area */
|
||||
.example-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.example-card > p {
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 2.5em; /* ~2 lines at text-sm leading-relaxed */
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
2.0.5
|
||||
2.0.8
|
||||
|
||||
@@ -6,6 +6,7 @@ from cli.commands.skill import skill
|
||||
from cli.commands.process import start, stop, restart, update, status, logs
|
||||
from cli.commands.context import context
|
||||
from cli.commands.install import install_browser
|
||||
from cli.commands.knowledge import knowledge
|
||||
|
||||
|
||||
HELP_TEXT = """Usage: cow COMMAND [ARGS]...
|
||||
@@ -22,6 +23,7 @@ Commands:
|
||||
status Show CowAgent running status.
|
||||
logs View CowAgent logs.
|
||||
skill Manage CowAgent skills.
|
||||
knowledge Manage knowledge base.
|
||||
install-browser Install browser tool (Playwright + Chromium).
|
||||
|
||||
Tip: You can also send /help, /skill list, etc. in agent chat."""
|
||||
@@ -69,6 +71,7 @@ main.add_command(update)
|
||||
main.add_command(status)
|
||||
main.add_command(logs)
|
||||
main.add_command(context)
|
||||
main.add_command(knowledge)
|
||||
main.add_command(install_browser)
|
||||
|
||||
|
||||
|
||||
121
cli/commands/knowledge.py
Normal file
121
cli/commands/knowledge.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""cow knowledge - Knowledge base management commands."""
|
||||
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
from cli.utils import get_project_root
|
||||
|
||||
|
||||
def _get_knowledge_dir():
|
||||
"""Resolve the knowledge directory path from config or default."""
|
||||
try:
|
||||
import sys
|
||||
sys.path.insert(0, get_project_root())
|
||||
from config import conf
|
||||
from common.utils import expand_path
|
||||
workspace = expand_path(conf().get("agent_workspace", "~/cow"))
|
||||
except Exception:
|
||||
workspace = os.path.expanduser("~/cow")
|
||||
return os.path.join(workspace, "knowledge")
|
||||
|
||||
|
||||
def _get_knowledge_enabled():
|
||||
try:
|
||||
import sys
|
||||
sys.path.insert(0, get_project_root())
|
||||
from config import conf
|
||||
return conf().get("knowledge", True)
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.pass_context
|
||||
def knowledge(ctx):
|
||||
"""Manage CowAgent knowledge base."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(_stats())
|
||||
|
||||
|
||||
@knowledge.command("list")
|
||||
def knowledge_list():
|
||||
"""Display knowledge base file tree."""
|
||||
click.echo(_tree())
|
||||
|
||||
|
||||
def _stats() -> str:
|
||||
knowledge_dir = _get_knowledge_dir()
|
||||
if not os.path.isdir(knowledge_dir):
|
||||
return "Knowledge base directory not found."
|
||||
|
||||
enabled = _get_knowledge_enabled()
|
||||
total_files = 0
|
||||
total_bytes = 0
|
||||
cat_count = {}
|
||||
|
||||
for root, dirs, files in os.walk(knowledge_dir):
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
rel_root = os.path.relpath(root, knowledge_dir)
|
||||
category = rel_root.split(os.sep)[0] if rel_root != "." else "root"
|
||||
for f in files:
|
||||
if f.endswith(".md") and f not in ("index.md", "log.md"):
|
||||
total_files += 1
|
||||
total_bytes += os.path.getsize(os.path.join(root, f))
|
||||
cat_count[category] = cat_count.get(category, 0) + 1
|
||||
|
||||
status_icon = click.style("enabled", fg="green") if enabled else click.style("disabled", fg="red")
|
||||
lines = [
|
||||
f"\n Knowledge Base [{status_icon}]",
|
||||
"",
|
||||
f" Pages: {total_files}",
|
||||
f" Size: {total_bytes / 1024:.1f} KB",
|
||||
"",
|
||||
]
|
||||
if cat_count:
|
||||
lines.append(" Categories:")
|
||||
for cat in sorted(cat_count.keys()):
|
||||
lines.append(f" {cat}/ ({cat_count[cat]} pages)")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f" Path: {knowledge_dir}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _tree() -> str:
|
||||
knowledge_dir = _get_knowledge_dir()
|
||||
if not os.path.isdir(knowledge_dir):
|
||||
return "Knowledge base directory not found."
|
||||
|
||||
tree_lines = [" knowledge/"]
|
||||
|
||||
subdirs = sorted([
|
||||
d for d in os.listdir(knowledge_dir)
|
||||
if os.path.isdir(os.path.join(knowledge_dir, d)) and not d.startswith(".")
|
||||
])
|
||||
|
||||
for i, subdir in enumerate(subdirs):
|
||||
is_last_dir = (i == len(subdirs) - 1)
|
||||
branch = "└── " if is_last_dir else "├── "
|
||||
subdir_path = os.path.join(knowledge_dir, subdir)
|
||||
md_files = sorted([
|
||||
f for f in os.listdir(subdir_path)
|
||||
if f.endswith(".md") and not f.startswith(".")
|
||||
])
|
||||
tree_lines.append(f" {branch}{subdir}/ ({len(md_files)})")
|
||||
|
||||
child_prefix = " " if is_last_dir else " │ "
|
||||
max_show = 15
|
||||
for j, fname in enumerate(md_files[:max_show]):
|
||||
is_last_file = (j == len(md_files[:max_show]) - 1) and len(md_files) <= max_show
|
||||
fb = "└── " if is_last_file else "├── "
|
||||
name = fname.replace(".md", "")
|
||||
tree_lines.append(f"{child_prefix}{fb}{name}")
|
||||
if len(md_files) > max_show:
|
||||
tree_lines.append(f"{child_prefix}└── ... +{len(md_files) - max_show} more")
|
||||
|
||||
if not subdirs:
|
||||
tree_lines.append(" (empty)")
|
||||
|
||||
return "\n" + "\n".join(tree_lines) + "\n"
|
||||
@@ -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):
|
||||
|
||||
@@ -54,7 +54,9 @@ class CloudClient(LinkAIClient):
|
||||
self.channel_mgr = None
|
||||
self._skill_service = None
|
||||
self._memory_service = None
|
||||
self._knowledge_service = None
|
||||
self._chat_service = None
|
||||
self._session_service = None
|
||||
|
||||
@property
|
||||
def skill_service(self):
|
||||
@@ -88,6 +90,21 @@ class CloudClient(LinkAIClient):
|
||||
logger.error(f"[CloudClient] Failed to init MemoryService: {e}")
|
||||
return self._memory_service
|
||||
|
||||
@property
|
||||
def knowledge_service(self):
|
||||
"""Lazy-init KnowledgeService."""
|
||||
if self._knowledge_service is None:
|
||||
try:
|
||||
from agent.knowledge.service import KnowledgeService
|
||||
from config import conf
|
||||
from common.utils import expand_path
|
||||
workspace_root = expand_path(conf().get("agent_workspace", "~/cow"))
|
||||
self._knowledge_service = KnowledgeService(workspace_root)
|
||||
logger.debug("[CloudClient] KnowledgeService initialised")
|
||||
except Exception as e:
|
||||
logger.error(f"[CloudClient] Failed to init KnowledgeService: {e}")
|
||||
return self._knowledge_service
|
||||
|
||||
@property
|
||||
def chat_service(self):
|
||||
"""Lazy-init ChatService (requires AgentBridge via Bridge singleton)."""
|
||||
@@ -102,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
|
||||
# ------------------------------------------------------------------
|
||||
@@ -468,6 +497,27 @@ class CloudClient(LinkAIClient):
|
||||
|
||||
return svc.dispatch(action, payload)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# knowledge callback
|
||||
# ------------------------------------------------------------------
|
||||
def on_knowledge(self, data: dict) -> dict:
|
||||
"""
|
||||
Handle KNOWLEDGE messages from the cloud console.
|
||||
Delegates to KnowledgeService.dispatch for the actual operations.
|
||||
|
||||
:param data: message data with 'action', 'clientId', 'payload'
|
||||
:return: response dict
|
||||
"""
|
||||
action = data.get("action", "")
|
||||
payload = data.get("payload")
|
||||
logger.info(f"[CloudClient] on_knowledge: action={action}")
|
||||
|
||||
svc = self.knowledge_service
|
||||
if svc is None:
|
||||
return {"action": action, "code": 500, "message": "KnowledgeService not available", "payload": None}
|
||||
|
||||
return svc.dispatch(action, payload)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# chat callback
|
||||
# ------------------------------------------------------------------
|
||||
@@ -509,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")
|
||||
@@ -524,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", "")
|
||||
|
||||
@@ -3,6 +3,7 @@ OPEN_AI = "openAI"
|
||||
OPENAI = "openai"
|
||||
CHATGPT = "chatGPT" # legacy alias for OPENAI, kept for backward compatibility
|
||||
BAIDU = "baidu"
|
||||
QIANFAN = "qianfan"
|
||||
XUNFEI = "xunfei"
|
||||
CHATGPTONAZURE = "chatGPTOnAzure"
|
||||
LINKAI = "linkai"
|
||||
@@ -14,6 +15,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 +29,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推荐模型
|
||||
@@ -80,6 +83,17 @@ TTS_1_HD = "tts-1-hd"
|
||||
# DeepSeek
|
||||
DEEPSEEK_CHAT = "deepseek-chat" # DeepSeek-V3对话模型
|
||||
DEEPSEEK_REASONER = "deepseek-reasoner" # DeepSeek-R1模型
|
||||
DEEPSEEK_V4_FLASH = "deepseek-v4-flash" # DeepSeek V4 Flash - 默认推荐 (思考模式 + 工具调用)
|
||||
DEEPSEEK_V4_PRO = "deepseek-v4-pro" # DeepSeek V4 Pro - 复杂任务更强 (思考模式 + 工具调用)
|
||||
|
||||
# Baidu Qianfan / ERNIE
|
||||
ERNIE_5 = "ernie-5.0" # ERNIE 5.0 - default recommendation
|
||||
ERNIE_X1_1 = "ernie-x1.1" # ERNIE X1.1 - reasoning-focused, multimodal
|
||||
ERNIE_45_TURBO_128K = "ernie-4.5-turbo-128k"
|
||||
ERNIE_45_TURBO_32K = "ernie-4.5-turbo-32k"
|
||||
ERNIE_4_TURBO_8K = "ERNIE-4.0-Turbo-8K"
|
||||
ERNIE_45_TURBO_VL = "ernie-4.5-turbo-vl"
|
||||
ERNIE_45_TURBO_VL_32K = "ernie-4.5-turbo-vl-32k"
|
||||
|
||||
# Qwen (通义千问 - 阿里云 DashScope)
|
||||
QWEN_TURBO = "qwen-turbo"
|
||||
@@ -93,6 +107,7 @@ QWQ_PLUS = "qwq-plus"
|
||||
|
||||
# MiniMax
|
||||
MINIMAX_M2_7 = "MiniMax-M2.7" # MiniMax M2.7 - Latest
|
||||
MINIMAX_M2_7_HIGHSPEED = "MiniMax-M2.7-highspeed" # MiniMax M2.7 highspeed
|
||||
MINIMAX_M2_5 = "MiniMax-M2.5" # MiniMax M2.5
|
||||
MINIMAX_M2_1 = "MiniMax-M2.1" # MiniMax M2.1
|
||||
MINIMAX_M2_1_LIGHTNING = "MiniMax-M2.1-lightning" # MiniMax M2.1 极速版
|
||||
@@ -100,7 +115,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"
|
||||
@@ -116,6 +132,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"
|
||||
@@ -149,15 +166,25 @@ MODELSCOPE_MODEL_LIST = ["deepseek-ai/DeepSeek-R1-0528", "deepseek-ai/DeepSeek-R
|
||||
|
||||
|
||||
MODEL_LIST = [
|
||||
# DeepSeek
|
||||
DEEPSEEK_V4_FLASH, DEEPSEEK_V4_PRO, DEEPSEEK_CHAT, DEEPSEEK_REASONER,
|
||||
|
||||
# Baidu Qianfan / ERNIE
|
||||
QIANFAN, ERNIE_5, ERNIE_X1_1, ERNIE_45_TURBO_128K, ERNIE_45_TURBO_32K, ERNIE_4_TURBO_8K,
|
||||
ERNIE_45_TURBO_VL, ERNIE_45_TURBO_VL_32K,
|
||||
|
||||
# MiniMax
|
||||
MiniMax, MINIMAX_M2_7, MINIMAX_M2_7_HIGHSPEED, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,
|
||||
|
||||
# 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,
|
||||
CLAUDE_35_SONNET, CLAUDE_35_SONNET_1022, CLAUDE_35_SONNET_0620, CLAUDE_3_SONNET, CLAUDE_3_HAIKU,
|
||||
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",
|
||||
|
||||
|
||||
# Gemini
|
||||
GEMINI_31_FLASH_LITE_PRE, GEMINI_31_PRO_PRE, GEMINI_3_PRO_PRE, GEMINI_3_FLASH_PRE, GEMINI_25_PRO_PRE, GEMINI_25_FLASH_PRE,
|
||||
GEMINI_20_FLASH, GEMINI_20_flash_exp, GEMINI_15_PRO, GEMINI_15_flash, GEMINI_PRO, GEMINI,
|
||||
|
||||
|
||||
# OpenAI
|
||||
GPT35, GPT35_0125, GPT35_1106, "gpt-3.5-turbo-16k",
|
||||
GPT4, GPT4_06_13, GPT4_32k, GPT4_32k_06_13,
|
||||
@@ -167,31 +194,29 @@ MODEL_LIST = [
|
||||
GPT_5, GPT_5_MINI, GPT_5_NANO,
|
||||
GPT_54, GPT_54_MINI, GPT_54_NANO,
|
||||
O1, O1_MINI,
|
||||
|
||||
# DeepSeek
|
||||
DEEPSEEK_CHAT, DEEPSEEK_REASONER,
|
||||
|
||||
# Qwen
|
||||
QWEN36_PLUS, QWEN35_PLUS, QWEN3_MAX, QWEN_MAX, QWEN_PLUS, QWEN_TURBO, QWEN_LONG,
|
||||
|
||||
# MiniMax
|
||||
MiniMax, MINIMAX_M2_7, 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,
|
||||
# GLM (智谱AI)
|
||||
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,
|
||||
# Qwen (通义千问)
|
||||
QWEN36_PLUS, QWEN35_PLUS, QWEN3_MAX, QWEN_MAX, QWEN_PLUS, QWEN_TURBO, QWEN_LONG,
|
||||
|
||||
# Doubao
|
||||
# Doubao (豆包)
|
||||
DOUBAO, DOUBAO_SEED_2_CODE, DOUBAO_SEED_2_PRO, DOUBAO_SEED_2_LITE, DOUBAO_SEED_2_MINI,
|
||||
|
||||
# Kimi (Moonshot)
|
||||
MOONSHOT, "moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k",
|
||||
KIMI_K2_6, KIMI_K2_5, KIMI_K2,
|
||||
|
||||
# ModelScope
|
||||
MODELSCOPE,
|
||||
|
||||
# LinkAI
|
||||
LINKAI_35, LINKAI_4_TURBO, LINKAI_4o,
|
||||
|
||||
# 其他模型
|
||||
WEN_XIN, WEN_XIN_4, XUNFEI,
|
||||
LINKAI_35, LINKAI_4_TURBO, LINKAI_4o,
|
||||
MODELSCOPE
|
||||
]
|
||||
|
||||
MODEL_LIST = MODEL_LIST + GITEE_AI_MODEL_LIST + MODELSCOPE_MODEL_LIST
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"channel_type": "weixin",
|
||||
"model": "MiniMax-M2.7",
|
||||
"model": "deepseek-v4-flash",
|
||||
"deepseek_api_key": "",
|
||||
"deepseek_api_base": "https://api.deepseek.com/v1",
|
||||
"qianfan_api_key": "",
|
||||
"qianfan_api_base": "https://qianfan.baidubce.com/v2",
|
||||
"minimax_api_key": "",
|
||||
"zhipu_ai_api_key": "",
|
||||
"ark_api_key": "",
|
||||
@@ -22,12 +26,16 @@
|
||||
"linkai_app_code": "",
|
||||
"feishu_app_id": "",
|
||||
"feishu_app_secret": "",
|
||||
"feishu_stream_reply": true,
|
||||
"dingtalk_client_id": "",
|
||||
"dingtalk_client_secret":"",
|
||||
"dingtalk_client_secret": "",
|
||||
"wecom_bot_id": "",
|
||||
"wecom_bot_secret": "",
|
||||
"web_password": "",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_tokens": 50000,
|
||||
"agent_max_context_turns": 20,
|
||||
"agent_max_steps": 15
|
||||
"agent_max_steps": 20,
|
||||
"enable_thinking": false,
|
||||
"knowledge": true
|
||||
}
|
||||
|
||||
80
config.py
80
config.py
@@ -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版本
|
||||
@@ -74,6 +76,9 @@ available_setting = {
|
||||
"baidu_wenxin_api_key": "", # Baidu api key
|
||||
"baidu_wenxin_secret_key": "", # Baidu secret key
|
||||
"baidu_wenxin_prompt_enabled": False, # Enable prompt if you are using ernie character model
|
||||
# Baidu Qianfan / ERNIE OpenAI-compatible API
|
||||
"qianfan_api_key": "", # Baidu Qianfan API key in bce-v3 format
|
||||
"qianfan_api_base": "https://qianfan.baidubce.com/v2", # Qianfan OpenAI-compatible API base
|
||||
# 讯飞星火API
|
||||
"xunfei_app_id": "", # 讯飞应用ID
|
||||
"xunfei_api_key": "", # 讯飞 API key
|
||||
@@ -121,10 +126,13 @@ available_setting = {
|
||||
"chat_start_time": "00:00", # 服务开始时间
|
||||
"chat_stop_time": "24:00", # 服务结束时间
|
||||
# 翻译api
|
||||
"translate": "baidu", # 翻译api,支持baidu
|
||||
"translate": "baidu", # 翻译api,支持baidu, youdao
|
||||
# baidu翻译api的配置
|
||||
"baidu_translate_app_id": "", # 百度翻译api的appid
|
||||
"baidu_translate_app_key": "", # 百度翻译api的秘钥
|
||||
# youdao翻译api的配置
|
||||
"youdao_translate_app_key": "", # 有道翻译api的应用ID
|
||||
"youdao_translate_app_secret": "", # 有道翻译api的应用密钥
|
||||
# wechatmp的配置
|
||||
"wechatmp_token": "", # 微信公众平台的Token
|
||||
"wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443
|
||||
@@ -140,12 +148,13 @@ available_setting = {
|
||||
"wechatcomapp_agent_id": "", # 企业微信app的agent_id
|
||||
"wechatcomapp_aes_key": "", # 企业微信app的aes_key
|
||||
# 飞书配置
|
||||
"feishu_port": 80, # 飞书bot监听端口
|
||||
"feishu_port": 80, # 飞书bot监听端口,仅webhook模式需要
|
||||
"feishu_app_id": "", # 飞书机器人应用APP Id
|
||||
"feishu_app_secret": "", # 飞书机器人APP secret
|
||||
"feishu_token": "", # 飞书 verification token
|
||||
"feishu_bot_name": "", # 飞书机器人的名字
|
||||
"feishu_token": "", # 飞书 verification token,仅webhook模式需要
|
||||
"feishu_event_mode": "websocket", # 飞书事件接收模式: webhook(HTTP服务器) 或 websocket(长连接)
|
||||
# 飞书流式回复(基于官方 cardkit 流式卡片 API,需要机器人开通 cardkit:card:write 权限,且飞书客户端 7.20+)
|
||||
"feishu_stream_reply": True, # 是否开启流式回复(打字机效果)。失败/老客户端自动降级为非流式或升级提示
|
||||
# 钉钉配置
|
||||
"dingtalk_client_id": "", # 钉钉机器人Client ID
|
||||
"dingtalk_client_secret": "", # 钉钉机器人Client Secret
|
||||
@@ -180,26 +189,36 @@ available_setting = {
|
||||
# 豆包(火山方舟) 平台配置
|
||||
"ark_api_key": "",
|
||||
"ark_base_url": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
#魔搭社区 平台配置
|
||||
# 魔搭社区 平台配置
|
||||
"modelscope_api_key": "",
|
||||
"modelscope_base_url": "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||
# LinkAI平台配置
|
||||
"use_linkai": False,
|
||||
"linkai_api_key": "",
|
||||
"linkai_app_code": "",
|
||||
"linkai_api_base": "https://api.link-ai.tech", # linkAI服务地址
|
||||
"linkai_api_base": "https://api.link-ai.tech",
|
||||
"cloud_host": "client.link-ai.tech",
|
||||
"cloud_port": None,
|
||||
"cloud_deployment_id": "",
|
||||
"minimax_api_key": "",
|
||||
"Minimax_group_id": "",
|
||||
"Minimax_base_url": "",
|
||||
"deepseek_api_key": "",
|
||||
"deepseek_api_base": "https://api.deepseek.com/v1",
|
||||
"web_port": 9899,
|
||||
"web_password": "", # Web console password; empty means no authentication required
|
||||
"web_session_expire_days": 30, # Auth session expiry in days
|
||||
"agent": True, # 是否开启Agent模式
|
||||
"agent_workspace": "~/cow", # agent工作空间路径,用于存储skills、memory等
|
||||
"agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens
|
||||
"agent_max_context_turns": 30, # Agent模式下最大上下文记忆轮次
|
||||
"agent_max_steps": 15, # Agent模式下单次运行最大决策步数
|
||||
"agent_max_context_turns": 20, # Agent模式下最大上下文记忆轮次
|
||||
"agent_max_steps": 20, # Agent模式下单次运行最大决策步数
|
||||
"enable_thinking": False, # Enable deep-thinking mode for thinking-capable models
|
||||
"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": {},
|
||||
}
|
||||
|
||||
|
||||
@@ -216,13 +235,13 @@ class Config(dict):
|
||||
def __getitem__(self, key):
|
||||
# 跳过以下划线开头的注释字段
|
||||
if not key.startswith("_") and key not in available_setting:
|
||||
logger.warning("[Config] key '{}' not in available_setting, may not take effect".format(key))
|
||||
logger.debug("[Config] key '{}' not in available_setting, may not take effect".format(key))
|
||||
return super().__getitem__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# 跳过以下划线开头的注释字段
|
||||
if not key.startswith("_") and key not in available_setting:
|
||||
logger.warning("[Config] key '{}' not in available_setting, may not take effect".format(key))
|
||||
logger.debug("[Config] key '{}' not in available_setting, may not take effect".format(key))
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def get(self, key, default=None):
|
||||
@@ -372,12 +391,18 @@ def load_config():
|
||||
"gemini_api_base": "GEMINI_API_BASE",
|
||||
"minimax_api_key": "MINIMAX_API_KEY",
|
||||
"minimax_api_base": "MINIMAX_API_BASE",
|
||||
"deepseek_api_key": "DEEPSEEK_API_KEY",
|
||||
"deepseek_api_base": "DEEPSEEK_API_BASE",
|
||||
"qianfan_api_key": "QIANFAN_API_KEY",
|
||||
"qianfan_api_base": "QIANFAN_API_BASE",
|
||||
"zhipu_ai_api_key": "ZHIPU_AI_API_KEY",
|
||||
"zhipu_ai_api_base": "ZHIPU_AI_API_BASE",
|
||||
"moonshot_api_key": "MOONSHOT_API_KEY",
|
||||
"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",
|
||||
@@ -398,12 +423,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__))
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ services:
|
||||
- "9899:9899"
|
||||
environment:
|
||||
CHANNEL_TYPE: 'weixin'
|
||||
MODEL: 'MiniMax-M2.7'
|
||||
MODEL: 'deepseek-v4-flash'
|
||||
DEEPSEEK_API_KEY: ''
|
||||
DEEPSEEK_API_BASE: 'https://api.deepseek.com/v1'
|
||||
MINIMAX_API_KEY: ''
|
||||
ZHIPU_AI_API_KEY: ''
|
||||
ARK_API_KEY: ''
|
||||
@@ -35,9 +37,10 @@ services:
|
||||
DINGTALK_CLIENT_SECRET: ''
|
||||
WECOM_BOT_ID: ''
|
||||
WECOM_BOT_SECRET: ''
|
||||
WEB_PASSWORD: ''
|
||||
AGENT: 'True'
|
||||
AGENT_MAX_CONTEXT_TOKENS: 40000
|
||||
AGENT_MAX_CONTEXT_TOKENS: 50000
|
||||
AGENT_MAX_CONTEXT_TURNS: 20
|
||||
AGENT_MAX_STEPS: 15
|
||||
AGENT_MAX_STEPS: 20
|
||||
volumes:
|
||||
- ./cow:/home/agent/cow
|
||||
|
||||
@@ -3,67 +3,109 @@ title: 飞书
|
||||
description: 将 CowAgent 接入飞书应用
|
||||
---
|
||||
|
||||
通过自建应用将 CowAgent 接入飞书,需要是飞书企业用户且具有企业管理权限。
|
||||
> 通过飞书自建应用接入 CowAgent,支持单聊与群聊(@机器人),使用 WebSocket 长连接模式,无需公网 IP,支持流式打字机回复、语音消息收发。
|
||||
|
||||
## 一、创建企业自建应用
|
||||
<Note>
|
||||
接入需要是飞书企业用户且具有企业管理权限。
|
||||
</Note>
|
||||
|
||||
### 1. 创建应用
|
||||
## 一、接入方式
|
||||
|
||||
进入 [飞书开发平台](https://open.feishu.cn/app/),点击 **创建企业自建应用**,填写必要信息后点击 **创建**:
|
||||
### 方式一:扫码一键接入(推荐)
|
||||
|
||||
启动 Cow 项目后在终端中即可完成扫码创建。或打开 Web 控制台(本地链接:http://127.0.0.1:9899 ),选择 **通道** 菜单,点击 **接入通道**,选择 **飞书**,点击 **一键创建飞书应用**,使用 **飞书 App** 扫描二维码即可自动完成应用创建并接入:
|
||||
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260505181126.png" width="800"/>
|
||||
|
||||
|
||||
<Note>
|
||||
1. `lark-oapi` 依赖版本需要 >=1.5.5
|
||||
2. 扫码创建出的应用会自动预置全部所需权限(消息收发、卡片读写、群聊事件等)和事件订阅,无需到开发者后台手动配置。
|
||||
</Note>
|
||||
|
||||
|
||||
### 方式二:手动创建接入
|
||||
|
||||
需要先在飞书开放平台创建自建应用并配置权限,再通过 Web 控制台或配置文件接入。
|
||||
|
||||
**步骤一:创建应用**
|
||||
|
||||
1. 进入 [飞书开发平台](https://open.feishu.cn/app/),点击 **创建企业自建应用**:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-create-app.jpg" width="500"/>
|
||||
|
||||
### 2. 添加机器人能力
|
||||
|
||||
在 **添加应用能力** 菜单中,为应用添加 **机器人** 能力:
|
||||
2. 在 **添加应用能力** 中,为应用添加 **机器人** 能力:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-add-bot.jpg" width="800"/>
|
||||
|
||||
### 3. 配置应用权限
|
||||
|
||||
点击 **权限管理**,复制以下权限配置,粘贴到 **权限配置** 下方的输入框内,全选筛选出来的权限,点击 **批量开通** 并确认:
|
||||
3. 在 **权限管理** 中,将以下权限粘贴到输入框,全选并 **批量开通**:
|
||||
|
||||
```
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource,cardkit:card:write
|
||||
```
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/feishu-hosting-add-auth2.png" width="800"/>
|
||||
|
||||
## 二、项目配置
|
||||
|
||||
1. 在 **凭证与基础信息** 中获取 `App ID` 和 `App Secret`:
|
||||
4. 在 **凭证与基础信息** 中获取 `App ID` 和 `App Secret`:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-appid-secret.jpg" width="800"/>
|
||||
|
||||
2. 将以下配置加入项目根目录的 `config.json` 文件:
|
||||
**步骤二:接入 CowAgent**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_bot_name": "YOUR_BOT_NAME"
|
||||
}
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Web 控制台">
|
||||
打开 Web 控制台,选择 **通道** 菜单,点击 **接入通道**,选择 **飞书**,切换到「手动填写」Tab,输入 App ID 和 App Secret,点击接入即可。
|
||||
</Tab>
|
||||
<Tab title="配置文件">
|
||||
在 `config.json` 中添加以下配置后启动程序:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `feishu_app_id` | 飞书机器人应用 App ID |
|
||||
| `feishu_app_secret` | 飞书机器人 App Secret |
|
||||
| `feishu_bot_name` | 飞书机器人名称(创建应用时设置),群聊中使用依赖此配置 |
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_stream_reply": true
|
||||
}
|
||||
```
|
||||
|
||||
配置完成后启动项目。
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| `feishu_app_id` | 飞书应用 App ID | - |
|
||||
| `feishu_app_secret` | 飞书应用 App Secret | - |
|
||||
| `feishu_stream_reply` | 是否开启流式打字机回复 | `true` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 三、配置事件订阅
|
||||
**步骤三:发布应用**
|
||||
|
||||
1. 成功运行项目后,在飞书开放平台点击 **事件与回调**,选择 **长连接** 方式,点击保存:
|
||||
1. 启动 Cow 项目后,在飞书开放平台点击 **事件与回调**,选择 **长连接** 模式并保存:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311731183.png" width="600"/>
|
||||
|
||||
2. 点击下方的 **添加事件**,搜索 "接收消息",选择 "**接收消息v2.0**",确认添加。
|
||||
2. 点击 **添加事件**,搜索 "接收消息",选择 **接收消息 v2.0** 并确认。
|
||||
|
||||
3. 点击 **版本管理与发布**,创建版本并申请 **线上发布**,在飞书客户端查看审批消息并审核通过:
|
||||
3. 点击 **版本管理与发布**,创建版本并申请 **线上发布**,在飞书客户端审核通过:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311807356.png" width="600"/>
|
||||
|
||||
完成后在飞书中搜索机器人名称,即可开始对话。
|
||||
## 二、功能说明
|
||||
|
||||
| 功能 | 支持情况 |
|
||||
| --- | --- |
|
||||
| 单聊 | ✅ |
|
||||
| 群聊(@机器人) | ✅ |
|
||||
| 文本消息 | ✅ 收发 |
|
||||
| 图片消息 | ✅ 收发 |
|
||||
| 语音消息 | ✅ 收发 |
|
||||
| 流式回复 | ✅(通过 `feishu_stream_reply` 配置控制,默认开启) |
|
||||
|
||||
<Note>
|
||||
流式回复需要机器人具备 `cardkit:card:write` 权限(一键创建已默认开通),且接收方飞书客户端版本 ≥ 7.20。低版本客户端会显示升级提示,权限或版本不满足时自动降级为普通文本回复。
|
||||
</Note>
|
||||
|
||||
## 三、使用
|
||||
|
||||
完成接入后,在飞书中搜索机器人名称即可开始单聊对话。
|
||||
|
||||
如需在群聊中使用,将机器人添加到群中,@机器人发送消息即可。
|
||||
|
||||
@@ -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` | 是否启用深度思考模式 | `false` |
|
||||
|
||||
配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。密码也支持在控制台的「配置」页面中在线修改。
|
||||
|
||||
## 访问地址
|
||||
|
||||
@@ -34,10 +41,20 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
|
||||
|
||||
### 对话界面
|
||||
|
||||
支持流式输出,可实时展示 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 生成标题**:新会话在首轮对话完成后,自动调用模型生成简短的会话摘要标题
|
||||
- **新建会话**:点击会话列表顶部的「新对话」按钮或输入区的 `+` 按钮创建新会话
|
||||
- **删除会话**:点击会话项的删除按钮,确认后永久删除该会话及其所有消息
|
||||
- **清除上下文**:点击输入区的清除按钮,在当前会话中插入一条分隔线,分隔线以上的消息仍然展示但不再作为模型的上下文输入
|
||||
|
||||
### 模型管理
|
||||
|
||||
支持在线管理模型配置,无需手动编辑配置文件:
|
||||
|
||||
@@ -58,17 +58,18 @@ Session: 12 messages | 8 skills loaded
|
||||
**修改配置项:**
|
||||
|
||||
```text
|
||||
/config model deepseek-chat
|
||||
/config model deepseek-v4-flash
|
||||
```
|
||||
|
||||
**支持修改的配置项:**
|
||||
|
||||
| 配置项 | 说明 | 示例值 |
|
||||
| --- | --- | --- |
|
||||
| `model` | AI 模型名称 | `deepseek-chat` |
|
||||
| `model` | AI 模型名称 | `deepseek-v4-flash` |
|
||||
| `agent_max_context_tokens` | 最大上下文 tokens | `40000` |
|
||||
| `agent_max_context_turns` | 最大上下文记忆轮次 | `30` |
|
||||
| `agent_max_steps` | 单次任务最大决策步数 | `15` |
|
||||
| `enable_thinking` | 是否启用深度思考模式 | `true` / `false` |
|
||||
|
||||
<Note>
|
||||
修改 `model` 时,系统会自动匹配对应的模型调用方式。配置会写入 `config.json` 并持久保存。
|
||||
|
||||
@@ -40,6 +40,10 @@ Service:
|
||||
Skills:
|
||||
skill Manage skills (list / search / install / uninstall ...)
|
||||
|
||||
Memory & Knowledge:
|
||||
memory Memory distillation (dream)
|
||||
knowledge View knowledge base stats and structure
|
||||
|
||||
Others:
|
||||
help Show this help message
|
||||
version Show version
|
||||
@@ -55,6 +59,10 @@ Others:
|
||||
| `/status` | 查看服务状态和配置 |
|
||||
| `/config` | 查看或修改运行时配置 |
|
||||
| `/skill` | 管理技能(安装、卸载、启用、禁用等) |
|
||||
| `/memory dream [N]` | 手动触发记忆蒸馏(默认 3 天,最大 30) |
|
||||
| `/knowledge` | 查看知识库统计信息 |
|
||||
| `/knowledge list` | 查看知识库目录结构 |
|
||||
| `/knowledge on\|off` | 开启或关闭知识库 |
|
||||
| `/context` | 查看当前会话上下文信息 |
|
||||
| `/context clear` | 清空当前会话上下文 |
|
||||
| `/logs` | 查看最近日志 |
|
||||
@@ -76,6 +84,8 @@ Others:
|
||||
| logs | ✓ | ✓ |
|
||||
| config | ✗ | ✓ |
|
||||
| context | — | ✓ |
|
||||
| memory (子命令) | ✗ | ✓ |
|
||||
| knowledge (子命令) | ✓ | ✓ |
|
||||
| skill (子命令) | ✓ | ✓ |
|
||||
| start / stop / restart | ✓ | ✗ |
|
||||
| update | ✓ | ✗ |
|
||||
|
||||
77
docs/cli/memory-knowledge.mdx
Normal file
77
docs/cli/memory-knowledge.mdx
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: 记忆与知识库
|
||||
description: 记忆蒸馏和知识库管理命令
|
||||
---
|
||||
|
||||
## memory
|
||||
|
||||
管理 Agent 的长期记忆系统。
|
||||
|
||||
### memory dream
|
||||
|
||||
手动触发记忆蒸馏(Deep Dream),整理近期的天级记忆,蒸馏合并到 MEMORY.md,并生成梦境日记。
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`:整理近 N 天的记忆,默认 3 天,最大 30 天
|
||||
- 蒸馏在后台异步执行,完成后会在对话中通知结果
|
||||
- 无需等待 Agent 初始化,首次对话前即可使用
|
||||
|
||||
**示例:**
|
||||
|
||||
```text
|
||||
/memory dream # 整理近 3 天
|
||||
/memory dream 7 # 整理近 7 天
|
||||
/memory dream 30 # 整理近 30 天(全量)
|
||||
```
|
||||
|
||||
蒸馏完成后,Web 端会收到带有跳转链接的通知,可直接查看更新后的 MEMORY.md 和梦境日记。
|
||||
|
||||
<Tip>
|
||||
系统每天 23:55 会自动执行一次蒸馏(lookback 1 天)。手动触发适用于首次部署后的历史整理,或需要立即更新记忆时使用。
|
||||
</Tip>
|
||||
|
||||
## knowledge
|
||||
|
||||
查看和管理个人知识库。默认显示知识库统计信息。
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
📚 知识库
|
||||
|
||||
- 状态:已开启
|
||||
- 页面数:12
|
||||
- 总大小:45.2 KB
|
||||
- 分类明细:
|
||||
- concepts/: 5 篇
|
||||
- entities/: 4 篇
|
||||
- sources/: 3 篇
|
||||
```
|
||||
|
||||
### knowledge list
|
||||
|
||||
查看知识库目录树结构。
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
### knowledge on / off
|
||||
|
||||
开启或关闭知识库。关闭后不再注入知识提示词和索引知识文件。
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
终端 CLI 中 `cow knowledge` 和 `cow knowledge list` 可用,但 `on|off` 仅支持在对话中使用(需实时生效)。
|
||||
</Note>
|
||||
126
docs/docs.json
126
docs/docs.json
@@ -24,13 +24,13 @@
|
||||
},
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/zhayujie/chatgpt-on-wechat"
|
||||
"href": "https://github.com/zhayujie/CowAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"github": "https://github.com/zhayujie/chatgpt-on-wechat"
|
||||
"github": "https://github.com/zhayujie/CowAgent"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
@@ -72,17 +72,19 @@
|
||||
"group": "模型配置",
|
||||
"pages": [
|
||||
"models/index",
|
||||
"models/deepseek",
|
||||
"models/minimax",
|
||||
"models/glm",
|
||||
"models/qwen",
|
||||
"models/kimi",
|
||||
"models/doubao",
|
||||
"models/claude",
|
||||
"models/gemini",
|
||||
"models/openai",
|
||||
"models/deepseek",
|
||||
"models/glm",
|
||||
"models/qwen",
|
||||
"models/doubao",
|
||||
"models/kimi",
|
||||
"models/qianfan",
|
||||
"models/linkai",
|
||||
"models/coding-plan"
|
||||
"models/coding-plan",
|
||||
"models/custom"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -132,6 +134,14 @@
|
||||
"skills/create",
|
||||
"skills/hub"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "内置技能",
|
||||
"pages": [
|
||||
"skills/skill-creator",
|
||||
"skills/knowledge-wiki",
|
||||
"skills/image-generation"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -142,7 +152,19 @@
|
||||
"group": "记忆系统",
|
||||
"pages": [
|
||||
"memory/index",
|
||||
"memory/context"
|
||||
"memory/context",
|
||||
"memory/deep-dream"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "知识",
|
||||
"groups": [
|
||||
{
|
||||
"group": "知识库",
|
||||
"pages": [
|
||||
"knowledge/index"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -174,6 +196,7 @@
|
||||
"cli/index",
|
||||
"cli/process",
|
||||
"cli/skill",
|
||||
"cli/memory-knowledge",
|
||||
"cli/general"
|
||||
]
|
||||
}
|
||||
@@ -186,6 +209,9 @@
|
||||
"group": "发布记录",
|
||||
"pages": [
|
||||
"releases/overview",
|
||||
"releases/v2.0.8",
|
||||
"releases/v2.0.7",
|
||||
"releases/v2.0.6",
|
||||
"releases/v2.0.5",
|
||||
"releases/v2.0.4",
|
||||
"releases/v2.0.3",
|
||||
@@ -233,17 +259,19 @@
|
||||
"group": "Model Configuration",
|
||||
"pages": [
|
||||
"en/models/index",
|
||||
"en/models/deepseek",
|
||||
"en/models/minimax",
|
||||
"en/models/glm",
|
||||
"en/models/qwen",
|
||||
"en/models/kimi",
|
||||
"en/models/doubao",
|
||||
"en/models/claude",
|
||||
"en/models/gemini",
|
||||
"en/models/openai",
|
||||
"en/models/deepseek",
|
||||
"en/models/glm",
|
||||
"en/models/qwen",
|
||||
"en/models/doubao",
|
||||
"en/models/kimi",
|
||||
"en/models/qianfan",
|
||||
"en/models/linkai",
|
||||
"en/models/coding-plan"
|
||||
"en/models/coding-plan",
|
||||
"en/models/custom"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -290,9 +318,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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -303,7 +338,19 @@
|
||||
"group": "Memory System",
|
||||
"pages": [
|
||||
"en/memory/index",
|
||||
"en/memory/context"
|
||||
"en/memory/context",
|
||||
"en/memory/deep-dream"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Knowledge",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Knowledge Base",
|
||||
"pages": [
|
||||
"en/knowledge/index"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -335,6 +382,7 @@
|
||||
"en/cli/index",
|
||||
"en/cli/process",
|
||||
"en/cli/skill",
|
||||
"en/cli/memory-knowledge",
|
||||
"en/cli/chat"
|
||||
]
|
||||
}
|
||||
@@ -347,8 +395,12 @@
|
||||
"group": "Release Notes",
|
||||
"pages": [
|
||||
"en/releases/overview",
|
||||
"en/releases/v2.0.8",
|
||||
"en/releases/v2.0.7",
|
||||
"en/releases/v2.0.6",
|
||||
"en/releases/v2.0.5",
|
||||
"en/releases/v2.0.4",
|
||||
"en/releases/v2.0.3",
|
||||
"en/releases/v2.0.2",
|
||||
"en/releases/v2.0.1",
|
||||
"en/releases/v2.0.0"
|
||||
@@ -394,17 +446,19 @@
|
||||
"group": "モデル設定",
|
||||
"pages": [
|
||||
"ja/models/index",
|
||||
"ja/models/deepseek",
|
||||
"ja/models/minimax",
|
||||
"ja/models/glm",
|
||||
"ja/models/qwen",
|
||||
"ja/models/kimi",
|
||||
"ja/models/doubao",
|
||||
"ja/models/claude",
|
||||
"ja/models/gemini",
|
||||
"ja/models/openai",
|
||||
"ja/models/deepseek",
|
||||
"ja/models/glm",
|
||||
"ja/models/qwen",
|
||||
"ja/models/doubao",
|
||||
"ja/models/kimi",
|
||||
"ja/models/qianfan",
|
||||
"ja/models/linkai",
|
||||
"ja/models/coding-plan"
|
||||
"ja/models/coding-plan",
|
||||
"ja/models/custom"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -454,6 +508,14 @@
|
||||
"ja/skills/create",
|
||||
"ja/skills/hub"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "内蔵スキル",
|
||||
"pages": [
|
||||
"ja/skills/skill-creator",
|
||||
"ja/skills/knowledge-wiki",
|
||||
"ja/skills/image-generation"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -464,7 +526,19 @@
|
||||
"group": "メモリシステム",
|
||||
"pages": [
|
||||
"ja/memory/index",
|
||||
"ja/memory/context"
|
||||
"ja/memory/context",
|
||||
"ja/memory/deep-dream"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "ナレッジ",
|
||||
"groups": [
|
||||
{
|
||||
"group": "ナレッジベース",
|
||||
"pages": [
|
||||
"ja/knowledge/index"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -496,6 +570,7 @@
|
||||
"ja/cli/index",
|
||||
"ja/cli/process",
|
||||
"ja/cli/skill",
|
||||
"ja/cli/memory-knowledge",
|
||||
"ja/cli/general"
|
||||
]
|
||||
}
|
||||
@@ -508,6 +583,9 @@
|
||||
"group": "リリースノート",
|
||||
"pages": [
|
||||
"ja/releases/overview",
|
||||
"ja/releases/v2.0.8",
|
||||
"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",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<p align="center"><img src="https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="550" /></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/chatgpt-on-wechat" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/chatgpt-on-wechat" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
|
||||
[<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/README.md">中文</a>] | [English] | [<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/docs/ja/README.md">日本語</a>]
|
||||
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/CowAgent" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square" alt="Stars"></a> <br/>
|
||||
[<a href="https://github.com/zhayujie/CowAgent/blob/master/README.md">中文</a>] | [English] | [<a href="https://github.com/zhayujie/CowAgent/blob/master/docs/ja/README.md">日本語</a>]
|
||||
</p>
|
||||
|
||||
**CowAgent** is an AI super assistant powered by LLMs, capable of autonomous task planning, operating computers and external resources, creating and executing Skills, and continuously growing with long-term memory. It supports flexible model switching, handles text, voice, images, and files, and can be integrated into WeChat, Web, Feishu, DingTalk, WeCom Bot, WeCom App, and WeChat Official Account — running 7×24 hours on your personal computer or server.
|
||||
**CowAgent** is an AI super assistant powered by LLMs, capable of autonomous task planning, operating computers and external resources, creating and executing Skills, and continuously growing with long-term memory and a personal knowledge base. It supports flexible model switching, handles text, voice, images, and files, and can be integrated into WeChat, Web, Feishu, DingTalk, WeCom Bot, WeCom App, and WeChat Official Account — running 7×24 hours on your personal computer or server.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cowagent.ai/">🌐 Website</a> ·
|
||||
@@ -22,12 +22,13 @@
|
||||
> 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.
|
||||
- ✅ **CLI System**: Provides terminal commands and in-chat commands for process management, skill installation, configuration, and more.
|
||||
- ✅ **Multimodal Messages**: Supports parsing, processing, generating, and sending text, images, voice, files, and other message types.
|
||||
- ✅ **Multiple Model Support**: Supports OpenAI, Claude, Gemini, DeepSeek, MiniMax, GLM, Qwen, Kimi, Doubao, and other mainstream model providers.
|
||||
- ✅ **Multiple Model Support**: Supports DeepSeek, MiniMax, Claude, Gemini, OpenAI, GLM, Qwen, Doubao, Kimi, and other mainstream model providers.
|
||||
- ✅ **Multi-platform Deployment**: Runs on local computers or servers, integrable into WeChat, Web, Feishu, DingTalk, WeChat Official Account, and WeCom applications.
|
||||
|
||||
## Disclaimer
|
||||
@@ -42,19 +43,21 @@ Try online (no deployment needed): [CowAgent](https://link-ai.tech/cowagent/crea
|
||||
|
||||
## Changelog
|
||||
|
||||
> **2026.04.01:** [v2.0.5](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.5) — Cow CLI, Skill Hub open source, Browser tool, WeCom Bot QR scan, and more.
|
||||
> **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.02.27:** [v2.0.2](https://github.com/zhayujie/chatgpt-on-wechat/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.
|
||||
> **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.13:** [v2.0.1](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.1) — Built-in Web Search tool, smart context trimming, runtime info dynamic update, Windows compatibility, fixes for scheduler memory loss, Feishu connection issues, 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.
|
||||
|
||||
> **2026.02.03:** [v2.0.0](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0) — Full upgrade to AI super assistant with multi-step task planning, long-term memory, built-in tools, Skills framework, new models, and optimized channels.
|
||||
> **2026.02.13:** [v2.0.1](https://github.com/zhayujie/CowAgent/releases/tag/2.0.1) — Built-in Web Search tool, smart context trimming, runtime info dynamic update, Windows compatibility, fixes for scheduler memory loss, Feishu connection issues, and more.
|
||||
|
||||
> **2025.05.23:** [v1.7.6](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.6) — Web channel optimization, AgentMesh multi-agent plugin, Baidu TTS, claude-4-sonnet/opus support.
|
||||
> **2026.02.03:** [v2.0.0](https://github.com/zhayujie/CowAgent/releases/tag/2.0.0) — Full upgrade to AI super assistant with multi-step task planning, long-term memory, built-in tools, Skills framework, new models, and optimized channels.
|
||||
|
||||
> **2025.04.11:** [v1.7.5](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.5) — wechatferry protocol, DeepSeek model, Tencent Cloud voice, ModelScope and Gitee-AI support.
|
||||
> **2025.05.23:** [v1.7.6](https://github.com/zhayujie/CowAgent/releases/tag/1.7.6) — Web channel optimization, AgentMesh multi-agent plugin, Baidu TTS, claude-4-sonnet/opus support.
|
||||
|
||||
> **2024.12.13:** [v1.7.4](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.4) — Gemini 2.0 model, Web channel, memory leak fix.
|
||||
> **2025.04.11:** [v1.7.5](https://github.com/zhayujie/CowAgent/releases/tag/1.7.5) — wechatferry protocol, DeepSeek model, Tencent Cloud voice, ModelScope and Gitee-AI support.
|
||||
|
||||
> **2024.12.13:** [v1.7.4](https://github.com/zhayujie/CowAgent/releases/tag/1.7.4) — Gemini 2.0 model, Web channel, memory leak fix.
|
||||
|
||||
Full changelog: [Release Notes](https://docs.cowagent.ai/en/releases/overview)
|
||||
|
||||
@@ -83,8 +86,8 @@ Script usage: [One-click Install](https://docs.cowagent.ai/en/guide/quick-start)
|
||||
**1. Clone the project**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
git clone https://github.com/zhayujie/CowAgent
|
||||
cd CowAgent/
|
||||
```
|
||||
|
||||
**2. Install dependencies**
|
||||
@@ -161,15 +164,15 @@ Supports mainstream model providers. Recommended models for Agent mode:
|
||||
|
||||
| Provider | Recommended Model |
|
||||
| --- | --- |
|
||||
| DeepSeek | `deepseek-v4-flash` |
|
||||
| MiniMax | `MiniMax-M2.7` |
|
||||
| GLM | `glm-5-turbo` |
|
||||
| Kimi | `kimi-k2.5` |
|
||||
| Doubao | `doubao-seed-2-0-code-preview-260215` |
|
||||
| Qwen | `qwen3.6-plus` |
|
||||
| Claude | `claude-sonnet-4-6` |
|
||||
| Gemini | `gemini-3.1-pro-preview` |
|
||||
| OpenAI | `gpt-5.4` |
|
||||
| DeepSeek | `deepseek-chat` |
|
||||
| GLM | `glm-5.1` |
|
||||
| Qwen | `qwen3.6-plus` |
|
||||
| Doubao | `doubao-seed-2-0-code-preview-260215` |
|
||||
| Kimi | `kimi-k2.6` |
|
||||
|
||||
For detailed configuration of each model, see the [Models documentation](https://docs.cowagent.ai/en/models/index).
|
||||
|
||||
@@ -232,16 +235,16 @@ Multiple channels can be enabled simultaneously, separated by commas: `"channel_
|
||||
|
||||
## 🔎 FAQ
|
||||
|
||||
FAQs: <https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs>
|
||||
FAQs: <https://github.com/zhayujie/CowAgent/wiki/FAQs>
|
||||
|
||||
## 🛠️ Contributing
|
||||
|
||||
Welcome to add new channels, referring to the [Feishu channel](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/channel/feishu/feishu_channel.py) as an example. Also welcome to contribute new Skills, see the [Skill Creation docs](https://docs.cowagent.ai/en/skills/create), or submit to [Skill Hub](https://skills.cowagent.ai/submit).
|
||||
Welcome to add new channels, referring to the [Feishu channel](https://github.com/zhayujie/CowAgent/blob/master/channel/feishu/feishu_channel.py) as an example. Also welcome to contribute new Skills, see the [Skill Creation docs](https://docs.cowagent.ai/en/skills/create), or submit to [Skill Hub](https://skills.cowagent.ai/submit).
|
||||
|
||||
## ✉ Contact
|
||||
|
||||
Welcome to submit PRs and Issues, and support the project with a 🌟 Star. For questions, check the [FAQ list](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) or search [Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues).
|
||||
Welcome to submit PRs and Issues, and support the project with a 🌟 Star. For questions, check the [FAQ list](https://github.com/zhayujie/CowAgent/wiki/FAQs) or search [Issues](https://github.com/zhayujie/CowAgent/issues).
|
||||
|
||||
## 🌟 Contributors
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,69 +1,107 @@
|
||||
---
|
||||
title: Feishu (Lark)
|
||||
description: Integrate CowAgent into Feishu application
|
||||
description: Integrate CowAgent into Feishu via a custom enterprise app
|
||||
---
|
||||
|
||||
Integrate CowAgent into Feishu by creating a custom enterprise app. You need to be a Feishu enterprise user with admin privileges.
|
||||
> Integrate CowAgent into Feishu via a custom enterprise app. Supports p2p chat and group chat (@bot), uses WebSocket long connection (no public IP needed), supports streaming typewriter replies and voice messages.
|
||||
|
||||
## 1. Create Enterprise Custom App
|
||||
<Note>
|
||||
You need to be a Feishu enterprise user with admin privileges.
|
||||
</Note>
|
||||
|
||||
### 1.1 Create App
|
||||
## 1. Setup
|
||||
|
||||
Go to [Feishu Developer Platform](https://open.feishu.cn/app/), click **Create Enterprise Custom App**, fill in the required information and click **Create**:
|
||||
### Option 1: One-click Scan to Create (Recommended)
|
||||
|
||||
No need to manually create an app on the Feishu Developer Platform. Start the Cow project, open the web console (default `http://127.0.0.1:9899/`), go to **Channels**, click **Add Channel**, choose **Feishu**, then under the **Scan QR** tab click **One-click Create Feishu App** and scan with the **Feishu App** to complete app creation and connection automatically.
|
||||
|
||||
<Note>
|
||||
The created app comes with all required permissions (messaging, card read/write, group events, etc.) and event subscriptions pre-configured. Currently only the Feishu mainland version is supported (Lark international not yet supported).
|
||||
</Note>
|
||||
|
||||
When starting from CLI without `feishu_app_id` configured, the QR code is also printed to the terminal.
|
||||
|
||||
### Option 2: Manual Setup
|
||||
|
||||
Manually create a custom app on the Feishu Developer Platform, then connect via Web Console or config file.
|
||||
|
||||
**Step 1: Create the App**
|
||||
|
||||
1. Go to [Feishu Developer Platform](https://open.feishu.cn/app/), click **Create Enterprise Custom App**:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-create-app.jpg" width="500"/>
|
||||
|
||||
### 1.2 Add Bot Capability
|
||||
|
||||
In **Add App Capabilities**, add **Bot** capability to the app:
|
||||
2. In **Add App Capabilities**, add the **Bot** capability:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-add-bot.jpg" width="800"/>
|
||||
|
||||
### 1.3 Configure App Permissions
|
||||
|
||||
Click **Permission Management**, paste the following permission string into the input box below **Permission Configuration**, select all filtered permissions, click **Batch Enable** and confirm:
|
||||
3. In **Permission Management**, paste the following permissions and **Batch Enable** all:
|
||||
|
||||
```
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource,cardkit:card:write
|
||||
```
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/feishu-hosting-add-auth2.png" width="800"/>
|
||||
|
||||
## 2. Project Configuration
|
||||
|
||||
1. Get `App ID` and `App Secret` from **Credentials & Basic Info**:
|
||||
4. Get `App ID` and `App Secret` from **Credentials & Basic Info**:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-appid-secret.jpg" width="800"/>
|
||||
|
||||
2. Add the following configuration to `config.json` in the project root:
|
||||
**Step 2: Connect to CowAgent**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_bot_name": "YOUR_BOT_NAME"
|
||||
}
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Web Console">
|
||||
Open the web console, go to **Channels**, click **Add Channel**, choose **Feishu**, switch to the **Manual** tab, enter App ID and App Secret, then click connect.
|
||||
</Tab>
|
||||
<Tab title="Config File">
|
||||
Add the following to `config.json` and start the program:
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `feishu_app_id` | Feishu bot App ID |
|
||||
| `feishu_app_secret` | Feishu bot App Secret |
|
||||
| `feishu_bot_name` | Bot name (set when creating the app), required for group chat usage |
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_stream_reply": true
|
||||
}
|
||||
```
|
||||
|
||||
Start the project after configuration is complete.
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `feishu_app_id` | Feishu app App ID | - |
|
||||
| `feishu_app_secret` | Feishu app App Secret | - |
|
||||
| `feishu_stream_reply` | Enable streaming typewriter reply | `true` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 3. Configure Event Subscription
|
||||
**Step 3: Publish the App**
|
||||
|
||||
1. After the project is running successfully, go to the Feishu Developer Platform, click **Events & Callbacks**, select **Long Connection** mode, and click save:
|
||||
1. After Cow is running, go to **Events & Callbacks** in the Feishu Developer Platform, choose **Long Connection** mode and save:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311731183.png" width="600"/>
|
||||
|
||||
2. Click **Add Event** below, search for "Receive Message", select "**Receive Message v2.0**", and confirm.
|
||||
2. Click **Add Event**, search for "Receive Message" and choose **Receive Message v2.0**.
|
||||
|
||||
3. Click **Version Management & Release**, create a new version and apply for **Production Release**. Check the approval message in the Feishu client and approve:
|
||||
3. Click **Version Management & Release**, create a version and apply for **Production Release**. Approve the request in the Feishu client:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311807356.png" width="600"/>
|
||||
|
||||
Once completed, search for the bot name in Feishu to start chatting.
|
||||
## 2. Features
|
||||
|
||||
| Feature | Status |
|
||||
| --- | --- |
|
||||
| P2P chat | ✅ |
|
||||
| Group chat (@bot) | ✅ |
|
||||
| Text messages | ✅ send/receive |
|
||||
| Image messages | ✅ send/receive |
|
||||
| Voice messages | ✅ send/receive |
|
||||
| Streaming reply | ✅ (powered by Feishu cardkit streaming card) |
|
||||
|
||||
<Note>
|
||||
Streaming reply requires the `cardkit:card:write` permission (already enabled by one-click creation) and Feishu client version ≥ 7.20. Older clients see an upgrade prompt; if the permission or version is not satisfied, replies fall back to plain text automatically.
|
||||
</Note>
|
||||
|
||||
## 3. Usage
|
||||
|
||||
After connection, search for the bot name in Feishu to start a chat.
|
||||
|
||||
To use in groups, add the bot to a group and @-mention it.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -44,17 +44,18 @@ View or modify runtime configuration. Changes take effect immediately without re
|
||||
**Modify a config item:**
|
||||
|
||||
```text
|
||||
/config model deepseek-chat
|
||||
/config model deepseek-v4-flash
|
||||
```
|
||||
|
||||
**Configurable items:**
|
||||
|
||||
| Item | Description | Example |
|
||||
| --- | --- | --- |
|
||||
| `model` | AI model name | `deepseek-chat` |
|
||||
| `model` | AI model name | `deepseek-v4-flash` |
|
||||
| `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 mode | `true` / `false` |
|
||||
|
||||
<Note>
|
||||
When changing `model`, the system automatically matches the corresponding model API. Configuration is persisted to `config.json`.
|
||||
|
||||
@@ -40,6 +40,10 @@ Service:
|
||||
Skills:
|
||||
skill Manage skills (list / search / install / uninstall ...)
|
||||
|
||||
Memory & Knowledge:
|
||||
memory Memory distillation (dream)
|
||||
knowledge View knowledge base stats and structure
|
||||
|
||||
Others:
|
||||
help Show this help message
|
||||
version Show version
|
||||
@@ -55,6 +59,10 @@ 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 |
|
||||
| `/context` | View current session context info |
|
||||
| `/context clear` | Clear current session context |
|
||||
| `/logs` | View recent logs |
|
||||
@@ -74,6 +82,8 @@ 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 | ✓ | ✗ |
|
||||
| update | ✓ | ✗ |
|
||||
|
||||
63
docs/en/cli/memory-knowledge.mdx
Normal file
63
docs/en/cli/memory-knowledge.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Memory & Knowledge
|
||||
description: Memory distillation and knowledge base management commands
|
||||
---
|
||||
|
||||
## memory
|
||||
|
||||
Manage the Agent's long-term memory system.
|
||||
|
||||
### memory dream
|
||||
|
||||
Manually trigger memory distillation (Deep Dream) — consolidate recent daily memories into MEMORY.md and generate a dream diary.
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`: Consolidate the last N days of memory (default 3, max 30)
|
||||
- Runs asynchronously in the background; you'll be notified in chat when complete
|
||||
- Works without Agent initialization — can be used before the first conversation
|
||||
|
||||
**Examples:**
|
||||
|
||||
```text
|
||||
/memory dream # Consolidate last 3 days
|
||||
/memory dream 7 # Consolidate last 7 days
|
||||
/memory dream 30 # Consolidate last 30 days (full)
|
||||
```
|
||||
|
||||
On the Web console, the completion notification includes clickable links to view the updated MEMORY.md and dream diary.
|
||||
|
||||
<Tip>
|
||||
The system automatically runs distillation daily at 23:55 (lookback 1 day). Manual trigger is useful for consolidating historical memories after first deployment, or when you need an immediate memory update.
|
||||
</Tip>
|
||||
|
||||
## knowledge
|
||||
|
||||
View and manage the personal knowledge base. Shows statistics by default.
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
### knowledge list
|
||||
|
||||
View the knowledge base directory tree.
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
### knowledge on / off
|
||||
|
||||
Enable or disable the knowledge base. When disabled, knowledge prompts and file indexing are not injected.
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
In the terminal CLI, `cow knowledge` and `cow knowledge list` are available, but `on|off` is only supported in chat (requires runtime effect).
|
||||
</Note>
|
||||
@@ -8,12 +8,12 @@ description: Deploy CowAgent manually (source code / Docker)
|
||||
### 1. Clone the project
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
git clone https://github.com/zhayujie/CowAgent
|
||||
cd CowAgent/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
For network issues, use the mirror: https://gitee.com/zhayujie/chatgpt-on-wechat
|
||||
For network issues, use the mirror: https://gitee.com/zhayujie/CowAgent
|
||||
</Tip>
|
||||
|
||||
### 2. Install dependencies
|
||||
@@ -121,7 +121,8 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
```json
|
||||
{
|
||||
"channel_type": "web",
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "deepseek-v4-flash",
|
||||
"deepseek_api_key": "",
|
||||
"agent": true,
|
||||
"agent_workspace": "~/cow",
|
||||
"agent_max_context_tokens": 40000,
|
||||
@@ -133,7 +134,7 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `channel_type` | Channel type | `web` |
|
||||
| `model` | Model name | `MiniMax-M2.5` |
|
||||
| `model` | Model name | `deepseek-v4-flash` |
|
||||
| `agent` | Enable Agent mode | `true` |
|
||||
| `agent_workspace` | Agent workspace path | `~/cow` |
|
||||
| `agent_max_context_tokens` | Max context tokens | `40000` |
|
||||
@@ -141,5 +142,5 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
| `agent_max_steps` | Max decision steps per task | `15` |
|
||||
|
||||
<Tip>
|
||||
Full configuration options are in the project [`config.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py).
|
||||
Full configuration options are in the project [`config.py`](https://github.com/zhayujie/CowAgent/blob/master/config.py).
|
||||
</Tip>
|
||||
|
||||
@@ -26,7 +26,7 @@ The script automatically performs these steps:
|
||||
|
||||
1. Check Python environment (requires Python 3.7+)
|
||||
2. Install required tools (git, curl, etc.)
|
||||
3. Clone project to `~/chatgpt-on-wechat`
|
||||
3. Clone project to `~/CowAgent`
|
||||
4. Install Python dependencies and Cow CLI
|
||||
5. Guided configuration for AI model and channel
|
||||
6. Start service
|
||||
|
||||
@@ -9,16 +9,18 @@ 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" />
|
||||
|
||||
### Core Modules
|
||||
<img src="https://cdn.link-ai.tech/doc/cow-agent-arch-en.jpg.jpg" alt="CowAgent Architecture" />
|
||||
|
||||
| Module | Description |
|
||||
| --- | --- |
|
||||
| **Channels** | Message channel layer for receiving and sending messages. Supports Web, Feishu, DingTalk, WeCom, WeChat Official Account, and more |
|
||||
| **Agent Core** | Agent engine including task planning, memory system, and skills engine |
|
||||
| **Tools** | Tool layer for Agent to access OS resources. 10+ built-in tools |
|
||||
| **Models** | Model layer with unified access to mainstream LLMs |
|
||||
| **Plan** | Understands user intent, decomposes complex tasks into multi-step plans, and iteratively invokes tools until the goal is achieved |
|
||||
| **Memory** | Automatically persists important information as core memory and daily memory, with hybrid keyword and vector retrieval for cross-session context continuity |
|
||||
| **Knowledge** | Organizes structured knowledge by topic. The Agent autonomously distills valuable information into Markdown pages, maintaining indexes and cross-references to build a growing knowledge network |
|
||||
| **Tools** | Core capability for Agent to access OS resources. 10+ built-in tools including file read/write, terminal, browser, scheduler, memory search, web search, and more |
|
||||
| **Skills** | Loads and manages Skills. Supports one-click installation from Skill Hub, GitHub, and more, or custom skill creation through conversation |
|
||||
| **Models** | Model layer with unified access to OpenAI, Claude, Gemini, DeepSeek, MiniMax, GLM, Qwen, and other mainstream LLMs |
|
||||
| **Channels** | Message channel layer for receiving and sending messages. Supports Web console, WeChat, Feishu, DingTalk, WeCom, WeChat Official Account, and more with a unified protocol |
|
||||
| **CLI** | Command-line system providing terminal commands (`cow`) and chat commands (`/`) for process management, skill installation, configuration, knowledge base management, and more |
|
||||
|
||||
## Agent Mode Workflow
|
||||
|
||||
@@ -28,7 +30,7 @@ When Agent mode is enabled, CowAgent runs as an autonomous agent with the follow
|
||||
2. **Understand Intent** — Analyze task requirements and context
|
||||
3. **Plan Task** — Break complex tasks into multiple steps
|
||||
4. **Invoke Tools** — Select and execute appropriate tools for each step
|
||||
5. **Update Memory** — Store important information in long-term memory
|
||||
5. **Update Memory & Knowledge** — Store important information in long-term memory and organize structured knowledge into the knowledge base
|
||||
6. **Return Result** — Send execution results back to the user
|
||||
|
||||
## Workspace Directory Structure
|
||||
@@ -39,9 +41,12 @@ The Agent workspace is located at `~/cow` by default and stores system prompts,
|
||||
~/cow/
|
||||
├── system.md # Agent system prompt
|
||||
├── user.md # User profile
|
||||
├── MEMORY.md # Core memory
|
||||
├── memory/ # Long-term memory storage
|
||||
│ ├── core.md # Core memory
|
||||
│ └── daily/ # Daily memory
|
||||
│ └── YYYY-MM-DD.md # Daily memory
|
||||
├── knowledge/ # Personal knowledge base
|
||||
│ ├── index.md # Knowledge index
|
||||
│ └── <category>/ # Topic-based pages
|
||||
└── skills/ # Custom skills
|
||||
├── skill-1/
|
||||
└── skill-2/
|
||||
@@ -75,3 +80,4 @@ Configure Agent mode parameters in `config.json`:
|
||||
| `agent_max_context_tokens` | Max context tokens | `40000` |
|
||||
| `agent_max_context_turns` | Max context turns | `30` |
|
||||
| `agent_max_steps` | Max decision steps per task | `15` |
|
||||
| `knowledge` | Enable personal knowledge base | `true` |
|
||||
|
||||
@@ -5,23 +5,42 @@ 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>
|
||||
|
||||
## 2. Task Planning and Tool Use
|
||||
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.
|
||||
|
||||
The Agent automatically organizes valuable information from conversations into knowledge pages, maintaining cross-references and indexes. The Web console provides document browsing and knowledge graph visualization. Knowledge is stored in `~/cow/knowledge/` within the workspace.
|
||||
|
||||
- **Auto-organization**: The Agent autonomously extracts and organizes structured knowledge during conversations, maintaining indexes and cross-references
|
||||
- **Knowledge graph**: Automatically builds a knowledge graph from cross-references between pages, with interactive graph visualization in the Web console
|
||||
- **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
|
||||
|
||||
Tools are the core of how the Agent accesses operating system resources. The Agent intelligently selects and invokes tools based on task requirements, performing file read/write, command execution, scheduled tasks, and more. Built-in tools are implemented in the project's `agent/tools/` directory.
|
||||
|
||||
**Key tools:** file read/write/edit, Bash terminal, browser, file send, scheduler, memory search, web search, environment config, and more.
|
||||
|
||||
### 2.1 Terminal and File Access
|
||||
### 3.1 Terminal and File Access
|
||||
|
||||
Access to the OS terminal and file system is the most fundamental and core capability. Many other tools and skills build on top of this. Users can interact with the Agent from a mobile device to operate resources on their personal computer or server:
|
||||
|
||||
@@ -29,15 +48,15 @@ Access to the OS terminal and file system is the most fundamental and core capab
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202181130.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.2 Programming Capability
|
||||
### 3.2 Programming Capability
|
||||
|
||||
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>
|
||||
|
||||
### 2.3 Scheduled Tasks
|
||||
### 3.3 Scheduled Tasks
|
||||
|
||||
The `scheduler` tool enables dynamic scheduled tasks, supporting **one-time tasks, fixed intervals, and Cron expressions**. Tasks can be triggered as either a **fixed message send** or an **Agent dynamic task** execution:
|
||||
|
||||
@@ -45,7 +64,7 @@ The `scheduler` tool enables dynamic scheduled tasks, supporting **one-time task
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202195402.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.4 Browser
|
||||
### 3.4 Browser
|
||||
|
||||
The built-in `browser` tool allows the Agent to control a Chromium browser to visit web pages, fill forms, click elements, and take screenshots, with support for dynamic JS-rendered pages. Run `cow install-browser` to install with one command, automatically adapting to server (headless) and desktop environments:
|
||||
|
||||
@@ -53,7 +72,7 @@ The built-in `browser` tool allows the Agent to control a Chromium browser to vi
|
||||
<img src="https://cdn.link-ai.tech/doc/20260401110103.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.5 Environment Variable Management
|
||||
### 3.5 Environment Variable Management
|
||||
|
||||
Secrets required by skills are stored in an environment variable file, managed by the `env_config` tool. You can update secrets through conversation, with built-in security protection and desensitization:
|
||||
|
||||
@@ -61,7 +80,7 @@ Secrets required by skills are stored in an environment variable file, managed b
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234939.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 3. Skills System
|
||||
## 4. Skills System
|
||||
|
||||
The Skills system provides infinite extensibility for the Agent. Each Skill consists of a description file, execution scripts (optional), and resources (optional), describing how to complete specific types of tasks. Skills allow the Agent to follow instructions for complex workflows, invoke tools, or integrate third-party systems.
|
||||
|
||||
@@ -71,7 +90,7 @@ The Skills system provides infinite extensibility for the Agent. Each Skill cons
|
||||
|
||||
Install skills: `/skill install <name>` or `cow skill install <name>`, supporting Skill Hub, GitHub, ClawHub, URL, and more.
|
||||
|
||||
### 3.1 Creating Skills
|
||||
### 4.1 Creating Skills
|
||||
|
||||
The `skill-creator` skill enables rapid skill creation through conversation. You can ask the Agent to codify a workflow as a skill, or send any API documentation and examples for the Agent to complete the integration directly:
|
||||
|
||||
@@ -79,7 +98,7 @@ The `skill-creator` skill enables rapid skill creation through conversation. You
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202202247.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.2 Web Search and Image Recognition
|
||||
### 4.2 Web Search and Image Recognition
|
||||
|
||||
- **Web search:** Built-in `web_search` tool, supports multiple search engines. Configure `BOCHA_API_KEY` or `LINKAI_API_KEY` to enable.
|
||||
- **Image recognition:** Built-in `openai-image-vision` skill, supports `gpt-4.1-mini`, `gpt-4.1`, and other models. Requires `OPENAI_API_KEY`.
|
||||
@@ -88,7 +107,7 @@ The `skill-creator` skill enables rapid skill creation through conversation. You
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202213219.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.3 Skill Hub
|
||||
### 4.3 Skill Hub
|
||||
|
||||
Visit [skills.cowagent.ai](https://skills.cowagent.ai/) to browse all available skills, or use commands in conversation:
|
||||
|
||||
@@ -102,7 +121,7 @@ Also supports installing skills from GitHub, ClawHub, LinkAI, and other third-pa
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260401110103.png" width="750" />
|
||||
|
||||
## 4. CLI Command System
|
||||
## 5. CLI Command System
|
||||
|
||||
CowAgent provides two command interaction methods, covering service management, skill installation, configuration, and more:
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ description: CowAgent - AI Super Assistant powered by LLMs
|
||||
|
||||
CowAgent can proactively think and plan tasks, operate computers and external resources, create and execute Skills, and continuously grow with long-term memory. It supports flexible switching between multiple models, handles text, voice, images, files and other multimodal messages, and can be integrated into WeChat, web, Feishu, DingTalk, WeCom, and WeChat Official Account. It runs 7x24 hours on your personal computer or server.
|
||||
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/chatgpt-on-wechat">
|
||||
github.com/zhayujie/chatgpt-on-wechat
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/CowAgent">
|
||||
github.com/zhayujie/CowAgent
|
||||
</Card>
|
||||
|
||||
## Core Capabilities
|
||||
@@ -20,7 +20,10 @@ 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.
|
||||
</Card>
|
||||
<Card title="Skills System" icon="puzzle-piece" href="/en/skills/index">
|
||||
Implements a Skills creation and execution engine with built-in skills, and supports custom Skills development through natural language conversation.
|
||||
@@ -72,7 +75,7 @@ By default, the Web service starts after running. Access `http://localhost:9899/
|
||||
|
||||
## Disclaimer
|
||||
|
||||
1. This project follows the [MIT License](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE) and is intended for technical research and learning. Users must comply with local laws, regulations, policies, and corporate bylaws. Any illegal or rights-infringing use is prohibited.
|
||||
1. This project follows the [MIT License](https://github.com/zhayujie/CowAgent/blob/master/LICENSE) and is intended for technical research and learning. Users must comply with local laws, regulations, policies, and corporate bylaws. Any illegal or rights-infringing use is prohibited.
|
||||
2. Agent mode consumes more tokens than normal chat mode. Choose models based on effectiveness and cost. Agent has access to the host operating system — deploy with caution.
|
||||
3. CowAgent focuses on open-source development and does not participate in, authorize, or issue any cryptocurrency.
|
||||
|
||||
|
||||
89
docs/en/knowledge/index.mdx
Normal file
89
docs/en/knowledge/index.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Personal Knowledge Base
|
||||
description: CowAgent personal knowledge base — structured knowledge accumulation, automatic organization, and knowledge graph
|
||||
---
|
||||
|
||||
The personal knowledge base is the Agent's long-term structured knowledge store, saved in the `knowledge/` directory within the workspace. Unlike memory, which is organized by timeline, the knowledge base organizes content by topic — articles, conversation insights, and learning materials are structured into interlinked Markdown pages, forming a continuously growing knowledge network.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Knowledge vs Memory
|
||||
|
||||
| Dimension | Knowledge Base (knowledge/) | Long-term Memory (memory/) |
|
||||
| --- | --- | --- |
|
||||
| Organization | By topic, interlinked | By timeline, dated files |
|
||||
| Writing | Agent actively structures content | Auto-summarized on context trimming |
|
||||
| Content | Refined, structured knowledge | Raw conversation summaries |
|
||||
| Use cases | Study notes, tech docs, project knowledge | Conversation history, event records |
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
~/cow/knowledge/
|
||||
├── index.md # Knowledge index, entry point for all pages
|
||||
├── log.md # Change log, records each write
|
||||
├── concepts/ # Conceptual knowledge
|
||||
│ └── machine-learning.md
|
||||
├── entities/ # Entity knowledge (people, orgs, tools)
|
||||
│ └── openai.md
|
||||
└── sources/ # Source knowledge (articles, papers)
|
||||
└── llm-wiki.md
|
||||
```
|
||||
|
||||
The directory structure is flexible — the Agent automatically creates appropriate category directories based on actual content. Users can also customize the organization.
|
||||
|
||||
## Automatic Organization
|
||||
|
||||
Knowledge writing is an autonomous Agent behavior, triggered in these scenarios:
|
||||
|
||||
- **User shares an article or document** — The Agent automatically extracts key information and creates a structured knowledge page
|
||||
- **Conversation produces valuable conclusions** — The Agent organizes insights into knowledge pages and links them to existing knowledge
|
||||
- **User explicitly requests organization** — Users can guide the Agent to organize and update knowledge through conversation
|
||||
|
||||
Each knowledge page includes cross-reference links to related pages, gradually building a knowledge graph.
|
||||
|
||||
<Frame>
|
||||
<img src="https://gist.github.com/user-attachments/assets/3ce92f78-1863-4820-8fa8-660c0f2b7f09" alt="Conversational knowledge ingest" />
|
||||
</Frame>
|
||||
|
||||
## Knowledge Retrieval
|
||||
|
||||
The Agent can retrieve knowledge during conversation through:
|
||||
|
||||
- **Index lookup** — Quickly locate relevant pages via `knowledge/index.md`
|
||||
- **Semantic search** — Search knowledge content via the `memory_search` tool
|
||||
- **Direct read** — Read specific knowledge files via the `memory_get` tool
|
||||
|
||||
## Web Console
|
||||
|
||||
The web console provides a dedicated "Knowledge" module with:
|
||||
|
||||
- **Document browsing** — Tree-style directory structure, searchable and collapsible, click to view content
|
||||
- **Knowledge graph** — Interactive graph visualizing relationships between knowledge pages
|
||||
- **Chat integration** — Knowledge document links referenced in Agent replies are clickable for direct navigation
|
||||
|
||||
<Frame>
|
||||
<img src="https://gist.github.com/user-attachments/assets/b7b9d6be-0ac1-4c65-803b-2c6b36bd59a7" alt="Knowledge document browsing" />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="https://gist.github.com/user-attachments/assets/44ae68ca-96cc-40b9-ab33-cdbec34c2379" alt="Knowledge graph visualization" />
|
||||
</Frame>
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Manage the knowledge base with the `/knowledge` command:
|
||||
|
||||
| Command | Description |
|
||||
| --- | --- |
|
||||
| `/knowledge` | Show knowledge base statistics |
|
||||
| `/knowledge list` | Display file directory as a tree |
|
||||
| `/knowledge on` | Enable the knowledge base feature |
|
||||
| `/knowledge off` | Disable the knowledge base feature |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `knowledge` | Whether to enable the personal knowledge base | `true` |
|
||||
| `agent_workspace` | Workspace path; knowledge is stored under the `knowledge/` subdirectory | `~/cow` |
|
||||
@@ -39,14 +39,15 @@ When conversation turns exceed `agent_max_context_turns`:
|
||||
|
||||
- The **oldest half** of complete turns is trimmed (preserving tool call chain integrity)
|
||||
- Trimmed messages are summarized by LLM and **written to the daily memory file**
|
||||
- Remaining turns stay intact
|
||||
- Once the LLM summary is ready, it is also **injected into the first user message** of the retained context, helping the model maintain conversational continuity
|
||||
- Summary injection runs asynchronously in the background and takes effect from the next turn onward
|
||||
|
||||
### 3. Token Budget Trimming
|
||||
|
||||
After turn trimming, if tokens still exceed the budget:
|
||||
|
||||
- **Fewer than 5 turns**: All turns undergo **text compression** — each turn keeps only the first user text and last Agent reply, removing intermediate tool call chains
|
||||
- **5 or more turns**: The **first half** of turns is trimmed again, with discarded content also written to memory
|
||||
- **5 or more turns**: The **first half** of turns is trimmed again, with discarded content written to memory and a context summary injected
|
||||
|
||||
### 4. Overflow Emergency Handling
|
||||
|
||||
|
||||
90
docs/en/memory/deep-dream.mdx
Normal file
90
docs/en/memory/deep-dream.mdx
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Deep Dream
|
||||
description: Deep Dream — automatic distillation from conversations to permanent memory
|
||||
---
|
||||
|
||||
Deep Dream is the core consolidation mechanism of CowAgent's memory system, responsible for distilling scattered daily memories into refined long-term memory and generating dream diaries.
|
||||
|
||||
## Memory Flow
|
||||
|
||||
CowAgent's memory progresses through three stages from short-term to long-term:
|
||||
|
||||
```
|
||||
Conversation context (short-term) → Daily memory (mid-term) → MEMORY.md (long-term)
|
||||
```
|
||||
|
||||
### 1. Conversation → Daily Memory
|
||||
|
||||
When conversation context is trimmed or during the daily scheduled summary, the system uses LLM to summarize conversation content into key events, writing them to the daily memory file `memory/YYYY-MM-DD.md`.
|
||||
|
||||
Triggers:
|
||||
- **Context trimming** — Trimmed content is summarized when turn or token limits are exceeded
|
||||
- **Daily schedule** — Automatically triggered at 23:55
|
||||
- **API overflow** — Emergency save of current conversation summary
|
||||
|
||||
### 2. Daily Memory → MEMORY.md (Distillation)
|
||||
|
||||
After the daily summary completes, Deep Dream automatically runs distillation:
|
||||
|
||||
1. **Read materials** — Current `MEMORY.md` + today's daily memory
|
||||
2. **LLM distillation** — Deduplicate, merge, prune, extract new information
|
||||
3. **Overwrite MEMORY.md** — Output the refined long-term memory
|
||||
4. **Generate dream diary** — Record discoveries and insights from the consolidation
|
||||
|
||||
### 3. Role of MEMORY.md
|
||||
|
||||
`MEMORY.md` is injected into the system prompt for every conversation, keeping the Agent aware of user preferences, decisions, and key facts. Therefore it must stay concise — Deep Dream targets approximately 30 entries or fewer.
|
||||
|
||||
## Distillation Rules
|
||||
|
||||
Deep Dream follows these consolidation rules:
|
||||
|
||||
| Operation | Description |
|
||||
| --- | --- |
|
||||
| **Merge & refine** | Combine similar entries into single high-density statements |
|
||||
| **Extract new** | Pull preferences, decisions, people, experiences from daily memory |
|
||||
| **Conflict update** | When new info contradicts old entries, newer info takes precedence |
|
||||
| **Clean invalid** | Remove temporary records, blank entries, formatting artifacts |
|
||||
| **Remove redundancy** | Delete old entries already covered by more refined statements |
|
||||
|
||||
## Dream Diary
|
||||
|
||||
Each distillation generates a dream diary saved at `memory/dreams/YYYY-MM-DD.md`, written in a narrative style recording:
|
||||
|
||||
- Duplications or contradictions found
|
||||
- New insights extracted from daily memory
|
||||
- Cleanups and optimizations performed
|
||||
- Overall observations
|
||||
|
||||
Dream diaries can be viewed in the Web console under "Memory → Dream Diary" tab.
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414110032.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## Manual Trigger
|
||||
|
||||
In addition to the automatic daily run, you can manually trigger distillation in chat:
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`: Consolidate the last N days of memory (default 3, max 30)
|
||||
- Runs asynchronously in the background; you'll be notified in chat when complete
|
||||
- Web notifications include clickable links to view MEMORY.md and dream diary
|
||||
- Works without Agent initialization — can be used before the first conversation
|
||||
|
||||
<Tip>
|
||||
After first deployment, it's recommended to run `/memory dream 30` once to distill all historical daily memories into MEMORY.md.
|
||||
</Tip>
|
||||
|
||||
## Safety Mechanisms
|
||||
|
||||
| Mechanism | Description |
|
||||
| --- | --- |
|
||||
| **Skip on no content** | Distillation skipped when no daily memory exists, avoiding empty overwrites |
|
||||
| **Input dedup** | In scheduled tasks, automatically skipped when input materials haven't changed |
|
||||
| **Async execution** | Distillation runs in a background thread, never blocking conversation |
|
||||
| **Sequential guarantee** | In scheduled tasks, daily flush completes before distillation starts |
|
||||
| **No fabrication** | Prompt explicitly constrains consolidation to existing materials only |
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
62
docs/en/models/custom.mdx
Normal 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-v4-flash",
|
||||
"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
|
||||
```
|
||||
@@ -7,26 +7,57 @@ Option 1: Native integration (recommended):
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"model": "deepseek-v4-flash",
|
||||
"deepseek_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `model` | `deepseek-chat` (DeepSeek-V3.2, non-thinking mode), `deepseek-reasoner` (DeepSeek-R1, thinking mode) |
|
||||
| `model` | Supports `deepseek-v4-flash` (default) and `deepseek-v4-pro` |
|
||||
| `deepseek_api_key` | Create at [DeepSeek Platform](https://platform.deepseek.com/api_keys) |
|
||||
| `deepseek_api_base` | Optional, defaults to `https://api.deepseek.com/v1`. Can be changed to a third-party proxy |
|
||||
|
||||
## Model Selection
|
||||
|
||||
| Model | Use Case |
|
||||
| --- | --- |
|
||||
| `deepseek-v4-flash` | Default: fast and cost-effective |
|
||||
| `deepseek-v4-pro` | Stronger on complex tasks |
|
||||
|
||||
## Thinking Mode
|
||||
|
||||
The V4 series (`deepseek-v4-flash` / `deepseek-v4-pro`) supports an explicit "thinking mode": the model emits a chain-of-thought (`reasoning_content`) before the final answer to improve answer quality.
|
||||
|
||||
### Toggle
|
||||
|
||||
Controlled by the global `enable_thinking` setting:
|
||||
|
||||
```json
|
||||
{
|
||||
"enable_thinking": true
|
||||
}
|
||||
```
|
||||
|
||||
- `true`: thinking is on across all channels. The Web console renders the reasoning trace; IM channels (WeChat / WeCom / DingTalk / Feishu) don't render it but still benefit from higher answer quality.
|
||||
- `false`: thinking off, faster responses with lower first-token latency.
|
||||
|
||||
### Notes
|
||||
|
||||
- **Sampling parameters**: under thinking mode, `temperature`, `top_p`, `presence_penalty`, and `frequency_penalty` are silently ignored by the server (no error). CowAgent skips sending them automatically.
|
||||
- **Multi-turn tool calls**: once the history contains any tool-call turn, DeepSeek requires `reasoning_content` on every assistant message. CowAgent handles the round-trip automatically, including across mid-session toggles of the thinking switch.
|
||||
|
||||
<Tip>
|
||||
Start with `deepseek-v4-flash`; switch to `deepseek-v4-pro` for harder tasks; enable `enable_thinking` when you want deeper reasoning.
|
||||
</Tip>
|
||||
|
||||
Option 2: OpenAI-compatible configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"model": "deepseek-v4-flash",
|
||||
"bot_type": "openai",
|
||||
"open_ai_api_key": "YOUR_API_KEY",
|
||||
"open_ai_api_base": "https://api.deepseek.com/v1"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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: deepseek-v4-flash, MiniMax-M2.7, claude-sonnet-4-6, gemini-3.1-pro-preview, glm-5.1, qwen3.6-plus, kimi-k2.6, ernie-5.0
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
@@ -18,21 +18,15 @@ You can also use the [LinkAI](https://link-ai.tech) platform interface to flexib
|
||||
## Supported Models
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="DeepSeek" href="/en/models/deepseek">
|
||||
deepseek-v4-flash, deepseek-v4-pro, and more
|
||||
</Card>
|
||||
<Card title="Baidu Qianfan / ERNIE" href="/en/models/qianfan">
|
||||
ernie-5.0, ernie-4.5-turbo-128k, and more
|
||||
</Card>
|
||||
<Card title="MiniMax" href="/en/models/minimax">
|
||||
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
|
||||
</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
|
||||
</Card>
|
||||
<Card title="Doubao (ByteDance)" href="/en/models/doubao">
|
||||
doubao-seed series models
|
||||
</Card>
|
||||
<Card title="Claude" href="/en/models/claude">
|
||||
claude-sonnet-4-6 and more
|
||||
</Card>
|
||||
@@ -42,8 +36,17 @@ You can also use the [LinkAI](https://link-ai.tech) platform interface to flexib
|
||||
<Card title="OpenAI" href="/en/models/openai">
|
||||
gpt-5.4, gpt-4.1, o-series and more
|
||||
</Card>
|
||||
<Card title="DeepSeek" href="/en/models/deepseek">
|
||||
deepseek-chat, deepseek-reasoner
|
||||
<Card title="GLM (Zhipu AI)" href="/en/models/glm">
|
||||
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="Doubao (ByteDance)" href="/en/models/doubao">
|
||||
doubao-seed series models
|
||||
</Card>
|
||||
<Card title="Kimi" href="/en/models/kimi">
|
||||
kimi-k2.6, kimi-k2.5, kimi-k2 and more
|
||||
</Card>
|
||||
<Card title="LinkAI" href="/en/models/linkai">
|
||||
Unified multi-model interface + knowledge base
|
||||
@@ -51,5 +54,5 @@ You can also use the [LinkAI](https://link-ai.tech) platform interface to flexib
|
||||
</CardGroup>
|
||||
|
||||
<Tip>
|
||||
For a full list of model names, refer to the project's [`common/const.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/common/const.py) file.
|
||||
For a full list of model names, refer to the project's [`common/const.py`](https://github.com/zhayujie/CowAgent/blob/master/common/const.py) file.
|
||||
</Tip>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ title: LinkAI
|
||||
description: Unified access to multiple models via LinkAI platform
|
||||
---
|
||||
|
||||
The [LinkAI](https://link-ai.tech) platform lets you flexibly switch between OpenAI, Claude, Gemini, DeepSeek, Qwen, Kimi, and other models, with support for knowledge base, workflows, plugins, and other Agent capabilities.
|
||||
The [LinkAI](https://link-ai.tech) platform lets you flexibly switch between OpenAI, Claude, Gemini, DeepSeek, MiniMax, Qwen, Kimi, and other models, with support for knowledge base, workflows, plugins, and other Agent capabilities.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
63
docs/en/models/qianfan.mdx
Normal file
63
docs/en/models/qianfan.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Baidu Qianfan / ERNIE
|
||||
description: Baidu Qianfan ERNIE model configuration
|
||||
---
|
||||
|
||||
Option 1: Native integration (recommended):
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "ernie-5.0",
|
||||
"qianfan_api_key": "",
|
||||
"qianfan_api_base": "https://qianfan.baidubce.com/v2"
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `model` | Default recommendation: `ernie-5.0`; also supports `ernie-x1.1`, `ernie-4.5-turbo-128k`, `ernie-4.5-turbo-32k` |
|
||||
| `qianfan_api_key` | Qianfan API key, usually starting with `bce-v3/` |
|
||||
| `qianfan_api_base` | Optional, defaults to `https://qianfan.baidubce.com/v2` |
|
||||
|
||||
## Model Selection
|
||||
|
||||
| Model | Use Case |
|
||||
| --- | --- |
|
||||
| `ernie-5.0` | Default recommendation; latest ERNIE flagship with the strongest overall capability |
|
||||
| `ernie-x1.1` | Deep-thinking reasoning model with lower hallucination and stronger instruction following / tool calling |
|
||||
| `ernie-4.5-turbo-128k` | Long-context and general chat |
|
||||
| `ernie-4.5-turbo-32k` | General chat with a balanced context window and cost |
|
||||
|
||||
## Vision tool
|
||||
|
||||
Once `qianfan_api_key` is configured, Agent mode can auto-discover Qianfan for the Vision tool:
|
||||
|
||||
- When the main model itself is multimodal (e.g. `ernie-5.0`, `ernie-x1.1`, `ernie-4.5-turbo-vl`), images are handled directly by the main model with no extra setup.
|
||||
- When the main model is text-only (e.g. `ernie-4.5-turbo-128k`), the Vision tool automatically falls back to `ernie-4.5-turbo-vl`.
|
||||
|
||||
To force a specific Vision model, set it explicitly in `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool": {
|
||||
"vision": {
|
||||
"model": "ernie-4.5-turbo-vl"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Option 2: OpenAI-compatible configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "ernie-5.0",
|
||||
"bot_type": "openai",
|
||||
"open_ai_api_key": "",
|
||||
"open_ai_api_base": "https://qianfan.baidubce.com/v2"
|
||||
}
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Prefer `qianfan_api_key` for new configurations. Existing `wenxin`, `wenxin-4`, `baidu_wenxin_api_key`, and `baidu_wenxin_secret_key` configurations remain supported.
|
||||
</Tip>
|
||||
@@ -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 |
|
||||
@@ -22,4 +24,4 @@ description: CowAgent version history
|
||||
| 1.5.0 | 2023.11.10 | gpt-4-turbo, dall-e-3, tts multimodal |
|
||||
| 1.0.0 | 2022.12.12 | Project created, first ChatGPT integration |
|
||||
|
||||
See [GitHub Releases](https://github.com/zhayujie/chatgpt-on-wechat/releases) for full history.
|
||||
See [GitHub Releases](https://github.com/zhayujie/CowAgent/releases) for full history.
|
||||
|
||||
@@ -5,7 +5,7 @@ description: CowAgent 2.0 - Full upgrade from chatbot to AI super assistant
|
||||
|
||||
CowAgent 2.0 is a comprehensive upgrade from a chatbot to an **AI super assistant** — capable of autonomous thinking and task planning, long-term memory, operating computers, and creating and executing skills.
|
||||
|
||||
**Release Date**: 2026.02.03 | [GitHub Release](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0)
|
||||
**Release Date**: 2026.02.03 | [GitHub Release](https://github.com/zhayujie/CowAgent/releases/tag/2.0.0)
|
||||
|
||||
## Key Updates
|
||||
|
||||
@@ -60,4 +60,4 @@ CowAgent 2.0 is a comprehensive upgrade from a chatbot to an **AI super assistan
|
||||
|
||||
## Contributing
|
||||
|
||||
Welcome to [submit feedback](https://github.com/zhayujie/chatgpt-on-wechat/issues) and [contribute code](https://github.com/zhayujie/chatgpt-on-wechat/pulls).
|
||||
Welcome to [submit feedback](https://github.com/zhayujie/CowAgent/issues) and [contribute code](https://github.com/zhayujie/CowAgent/pulls).
|
||||
|
||||
@@ -3,34 +3,34 @@ title: v2.0.1
|
||||
description: CowAgent 2.0.1 - Built-in Web Search, smart context management, multiple fixes
|
||||
---
|
||||
|
||||
**Release Date**: 2026.02.27 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.0..2.0.1)
|
||||
**Release Date**: 2026.02.27 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.0..2.0.1)
|
||||
|
||||
## New Features
|
||||
|
||||
- **Built-in Web Search tool**: Integrated web search as a built-in Agent tool, reducing decision cost ([4f0ea5d](https://github.com/zhayujie/chatgpt-on-wechat/commit/4f0ea5d7568d61db91ff69c91c429e785fd1b1c2))
|
||||
- **Claude Opus 4.6 model support**: Added support for Claude Opus 4.6 model ([#2661](https://github.com/zhayujie/chatgpt-on-wechat/pull/2661))
|
||||
- **WeCom image recognition**: Support image message recognition in WeCom channel ([#2667](https://github.com/zhayujie/chatgpt-on-wechat/pull/2667))
|
||||
- **Built-in Web Search tool**: Integrated web search as a built-in Agent tool, reducing decision cost ([4f0ea5d](https://github.com/zhayujie/CowAgent/commit/4f0ea5d7568d61db91ff69c91c429e785fd1b1c2))
|
||||
- **Claude Opus 4.6 model support**: Added support for Claude Opus 4.6 model ([#2661](https://github.com/zhayujie/CowAgent/pull/2661))
|
||||
- **WeCom image recognition**: Support image message recognition in WeCom channel ([#2667](https://github.com/zhayujie/CowAgent/pull/2667))
|
||||
|
||||
## Improvements
|
||||
|
||||
- **Smart context management**: Resolved chat context overflow with intelligent context trimming strategy to prevent token limits ([cea7fb7](https://github.com/zhayujie/chatgpt-on-wechat/commit/cea7fb7490c53454602bf05955a0e9f059bcf0fd), [8acf2db](https://github.com/zhayujie/chatgpt-on-wechat/commit/8acf2dbdfe713b84ad74b761b7f86674b1c1904d)) [#2663](https://github.com/zhayujie/chatgpt-on-wechat/issues/2663)
|
||||
- **Runtime info dynamic update**: Automatic update of timestamps and other runtime info in system prompts via dynamic functions ([#2655](https://github.com/zhayujie/chatgpt-on-wechat/pull/2655), [#2657](https://github.com/zhayujie/chatgpt-on-wechat/pull/2657))
|
||||
- **Skill prompt optimization**: Improved Skill system prompt generation, simplified tool descriptions for better Agent performance ([6c21833](https://github.com/zhayujie/chatgpt-on-wechat/commit/6c218331b1f1208ea8be6bf226936d3b556ade3e))
|
||||
- **GLM custom API Base URL**: Support custom API Base URL for GLM models ([#2660](https://github.com/zhayujie/chatgpt-on-wechat/pull/2660))
|
||||
- **Startup script optimization**: Improved `run.sh` script interaction and configuration flow ([#2656](https://github.com/zhayujie/chatgpt-on-wechat/pull/2656))
|
||||
- **Decision step logging**: Added Agent decision step logging for debugging ([cb303e6](https://github.com/zhayujie/chatgpt-on-wechat/commit/cb303e6109c50c8dfef1f5e6c1ec47223bf3cd11))
|
||||
- **Smart context management**: Resolved chat context overflow with intelligent context trimming strategy to prevent token limits ([cea7fb7](https://github.com/zhayujie/CowAgent/commit/cea7fb7490c53454602bf05955a0e9f059bcf0fd), [8acf2db](https://github.com/zhayujie/CowAgent/commit/8acf2dbdfe713b84ad74b761b7f86674b1c1904d)) [#2663](https://github.com/zhayujie/CowAgent/issues/2663)
|
||||
- **Runtime info dynamic update**: Automatic update of timestamps and other runtime info in system prompts via dynamic functions ([#2655](https://github.com/zhayujie/CowAgent/pull/2655), [#2657](https://github.com/zhayujie/CowAgent/pull/2657))
|
||||
- **Skill prompt optimization**: Improved Skill system prompt generation, simplified tool descriptions for better Agent performance ([6c21833](https://github.com/zhayujie/CowAgent/commit/6c218331b1f1208ea8be6bf226936d3b556ade3e))
|
||||
- **GLM custom API Base URL**: Support custom API Base URL for GLM models ([#2660](https://github.com/zhayujie/CowAgent/pull/2660))
|
||||
- **Startup script optimization**: Improved `run.sh` script interaction and configuration flow ([#2656](https://github.com/zhayujie/CowAgent/pull/2656))
|
||||
- **Decision step logging**: Added Agent decision step logging for debugging ([cb303e6](https://github.com/zhayujie/CowAgent/commit/cb303e6109c50c8dfef1f5e6c1ec47223bf3cd11))
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Scheduler memory loss**: Fixed memory loss caused by Scheduler dispatcher ([a77a874](https://github.com/zhayujie/chatgpt-on-wechat/commit/a77a8741b500a408c6f5c8868856fb4b018fe9db))
|
||||
- **Empty tool calls & long results**: Fixed handling of empty tool calls and excessively long tool results ([0542700](https://github.com/zhayujie/chatgpt-on-wechat/commit/0542700f9091ebb08c1a56103b0f0f45f24aa621))
|
||||
- **OpenAI Function Call**: Fixed function call compatibility with OpenAI models ([158c87a](https://github.com/zhayujie/chatgpt-on-wechat/commit/158c87ab8b05bae054cc1b4eacdbb64fc1062ba9))
|
||||
- **Claude tool name field**: Removed extraneous tool name field from Claude model responses ([eec10cb](https://github.com/zhayujie/chatgpt-on-wechat/commit/eec10cb5db6a3d5bc12ef606606532237d2c5f6e))
|
||||
- **MiniMax reasoning**: Optimized MiniMax model reasoning content handling, hidden thinking process output ([c72cda3](https://github.com/zhayujie/chatgpt-on-wechat/commit/c72cda33864bd1542012ee6e0a8bd8c6c88cb5ed), [72b1cac](https://github.com/zhayujie/chatgpt-on-wechat/commit/72b1cacea1ba0d1f3dedacbab2e088e98fd7e172))
|
||||
- **GLM thinking process**: Hidden GLM model thinking process display ([72b1cac](https://github.com/zhayujie/chatgpt-on-wechat/commit/72b1cacea1ba0d1f3dedacbab2e088e98fd7e172))
|
||||
- **Feishu connection & SSL**: Fixed Feishu channel SSL certificate errors and connection issues ([229b14b](https://github.com/zhayujie/chatgpt-on-wechat/commit/229b14b6fcabe7123d53cab1dea39f38dab26d6d), [8674421](https://github.com/zhayujie/chatgpt-on-wechat/commit/867442155e7f095b4f38b0856f8c1d8312b5fcf7))
|
||||
- **model_type validation**: Fixed `AttributeError` caused by non-string `model_type` ([#2666](https://github.com/zhayujie/chatgpt-on-wechat/pull/2666))
|
||||
- **Scheduler memory loss**: Fixed memory loss caused by Scheduler dispatcher ([a77a874](https://github.com/zhayujie/CowAgent/commit/a77a8741b500a408c6f5c8868856fb4b018fe9db))
|
||||
- **Empty tool calls & long results**: Fixed handling of empty tool calls and excessively long tool results ([0542700](https://github.com/zhayujie/CowAgent/commit/0542700f9091ebb08c1a56103b0f0f45f24aa621))
|
||||
- **OpenAI Function Call**: Fixed function call compatibility with OpenAI models ([158c87a](https://github.com/zhayujie/CowAgent/commit/158c87ab8b05bae054cc1b4eacdbb64fc1062ba9))
|
||||
- **Claude tool name field**: Removed extraneous tool name field from Claude model responses ([eec10cb](https://github.com/zhayujie/CowAgent/commit/eec10cb5db6a3d5bc12ef606606532237d2c5f6e))
|
||||
- **MiniMax reasoning**: Optimized MiniMax model reasoning content handling, hidden thinking process output ([c72cda3](https://github.com/zhayujie/CowAgent/commit/c72cda33864bd1542012ee6e0a8bd8c6c88cb5ed), [72b1cac](https://github.com/zhayujie/CowAgent/commit/72b1cacea1ba0d1f3dedacbab2e088e98fd7e172))
|
||||
- **GLM thinking process**: Hidden GLM model thinking process display ([72b1cac](https://github.com/zhayujie/CowAgent/commit/72b1cacea1ba0d1f3dedacbab2e088e98fd7e172))
|
||||
- **Feishu connection & SSL**: Fixed Feishu channel SSL certificate errors and connection issues ([229b14b](https://github.com/zhayujie/CowAgent/commit/229b14b6fcabe7123d53cab1dea39f38dab26d6d), [8674421](https://github.com/zhayujie/CowAgent/commit/867442155e7f095b4f38b0856f8c1d8312b5fcf7))
|
||||
- **model_type validation**: Fixed `AttributeError` caused by non-string `model_type` ([#2666](https://github.com/zhayujie/CowAgent/pull/2666))
|
||||
|
||||
## Platform Compatibility
|
||||
|
||||
- **Windows compatibility**: Fixed path handling, file encoding, and `os.getuid()` unavailability on Windows across multiple tool modules ([051ffd7](https://github.com/zhayujie/chatgpt-on-wechat/commit/051ffd78a372f71a967fd3259e37fe19131f83cf), [5264f7c](https://github.com/zhayujie/chatgpt-on-wechat/commit/5264f7ce18360ee4db5dcb4ebe67307977d40014))
|
||||
- **Windows compatibility**: Fixed path handling, file encoding, and `os.getuid()` unavailability on Windows across multiple tool modules ([051ffd7](https://github.com/zhayujie/CowAgent/commit/051ffd78a372f71a967fd3259e37fe19131f83cf), [5264f7c](https://github.com/zhayujie/CowAgent/commit/5264f7ce18360ee4db5dcb4ebe67307977d40014))
|
||||
|
||||
@@ -3,7 +3,7 @@ title: v2.0.2
|
||||
description: CowAgent 2.0.2 - Web Console upgrade, multi-channel concurrency, session persistence
|
||||
---
|
||||
|
||||
**Release Date**: 2026.02.27 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.1...master)
|
||||
**Release Date**: 2026.02.27 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.1...master)
|
||||
|
||||
## Highlights
|
||||
|
||||
@@ -53,7 +53,7 @@ View Agent runtime logs in real-time for monitoring and troubleshooting:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173514.png" />
|
||||
|
||||
Related commits: [f1a1413](https://github.com/zhayujie/chatgpt-on-wechat/commit/f1a1413), [c0702c8](https://github.com/zhayujie/chatgpt-on-wechat/commit/c0702c8), [394853c](https://github.com/zhayujie/chatgpt-on-wechat/commit/394853c), [1c71c4e](https://github.com/zhayujie/chatgpt-on-wechat/commit/1c71c4e), [5e3eccb](https://github.com/zhayujie/chatgpt-on-wechat/commit/5e3eccb), [e1dc037](https://github.com/zhayujie/chatgpt-on-wechat/commit/e1dc037), [5edbf4c](https://github.com/zhayujie/chatgpt-on-wechat/commit/5edbf4c), [7d258b5](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d258b5)
|
||||
Related commits: [f1a1413](https://github.com/zhayujie/CowAgent/commit/f1a1413), [c0702c8](https://github.com/zhayujie/CowAgent/commit/c0702c8), [394853c](https://github.com/zhayujie/CowAgent/commit/394853c), [1c71c4e](https://github.com/zhayujie/CowAgent/commit/1c71c4e), [5e3eccb](https://github.com/zhayujie/CowAgent/commit/5e3eccb), [e1dc037](https://github.com/zhayujie/CowAgent/commit/e1dc037), [5edbf4c](https://github.com/zhayujie/CowAgent/commit/5edbf4c), [7d258b5](https://github.com/zhayujie/CowAgent/commit/7d258b5)
|
||||
|
||||
### 🔀 Multi-Channel Concurrency
|
||||
|
||||
@@ -67,24 +67,24 @@ Configuration: Set multiple channels in `config.json` via `channel_type` separat
|
||||
}
|
||||
```
|
||||
|
||||
Related commits: [4694594](https://github.com/zhayujie/chatgpt-on-wechat/commit/4694594), [7cce224](https://github.com/zhayujie/chatgpt-on-wechat/commit/7cce224), [7d258b5](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d258b5), [c9adddb](https://github.com/zhayujie/chatgpt-on-wechat/commit/c9adddb)
|
||||
Related commits: [4694594](https://github.com/zhayujie/CowAgent/commit/4694594), [7cce224](https://github.com/zhayujie/CowAgent/commit/7cce224), [7d258b5](https://github.com/zhayujie/CowAgent/commit/7d258b5), [c9adddb](https://github.com/zhayujie/CowAgent/commit/c9adddb)
|
||||
|
||||
### 💾 Session Persistence
|
||||
|
||||
Session history is now persisted to a local SQLite database. Conversation context is automatically restored after service restarts. Historical conversations in the Web Console are also restored.
|
||||
|
||||
Related commits: [29bfbec](https://github.com/zhayujie/chatgpt-on-wechat/commit/29bfbec), [9917552](https://github.com/zhayujie/chatgpt-on-wechat/commit/9917552), [925d728](https://github.com/zhayujie/chatgpt-on-wechat/commit/925d728)
|
||||
Related commits: [29bfbec](https://github.com/zhayujie/CowAgent/commit/29bfbec), [9917552](https://github.com/zhayujie/CowAgent/commit/9917552), [925d728](https://github.com/zhayujie/CowAgent/commit/925d728)
|
||||
|
||||
## New Models
|
||||
|
||||
- **Gemini 3.1 Pro Preview**: Added `gemini-3.1-pro-preview` model support ([52d7cad](https://github.com/zhayujie/chatgpt-on-wechat/commit/52d7cad))
|
||||
- **Claude 4.6 Sonnet**: Added `claude-4.6-sonnet` model support ([52d7cad](https://github.com/zhayujie/chatgpt-on-wechat/commit/52d7cad))
|
||||
- **Qwen3.5 Plus**: Added `qwen3.5-plus` model support ([e59a289](https://github.com/zhayujie/chatgpt-on-wechat/commit/e59a289))
|
||||
- **MiniMax M2.5**: Added `Minimax-M2.5` model support ([48db538](https://github.com/zhayujie/chatgpt-on-wechat/commit/48db538))
|
||||
- **GLM-5**: Added `glm-5` model support ([48db538](https://github.com/zhayujie/chatgpt-on-wechat/commit/48db538))
|
||||
- **Kimi K2.5**: Added `kimi-k2.5` model support ([48db538](https://github.com/zhayujie/chatgpt-on-wechat/commit/48db538))
|
||||
- **Doubao 2.0 Code**: Added `doubao-2.0-code` coding-specialized model ([ab28ee5](https://github.com/zhayujie/chatgpt-on-wechat/commit/ab28ee5))
|
||||
- **DashScope Models**: Added Alibaba Cloud DashScope model name support ([ce58f23](https://github.com/zhayujie/chatgpt-on-wechat/commit/ce58f23))
|
||||
- **Gemini 3.1 Pro Preview**: Added `gemini-3.1-pro-preview` model support ([52d7cad](https://github.com/zhayujie/CowAgent/commit/52d7cad))
|
||||
- **Claude 4.6 Sonnet**: Added `claude-4.6-sonnet` model support ([52d7cad](https://github.com/zhayujie/CowAgent/commit/52d7cad))
|
||||
- **Qwen3.5 Plus**: Added `qwen3.5-plus` model support ([e59a289](https://github.com/zhayujie/CowAgent/commit/e59a289))
|
||||
- **MiniMax M2.5**: Added `Minimax-M2.5` model support ([48db538](https://github.com/zhayujie/CowAgent/commit/48db538))
|
||||
- **GLM-5**: Added `glm-5` model support ([48db538](https://github.com/zhayujie/CowAgent/commit/48db538))
|
||||
- **Kimi K2.5**: Added `kimi-k2.5` model support ([48db538](https://github.com/zhayujie/CowAgent/commit/48db538))
|
||||
- **Doubao 2.0 Code**: Added `doubao-2.0-code` coding-specialized model ([ab28ee5](https://github.com/zhayujie/CowAgent/commit/ab28ee5))
|
||||
- **DashScope Models**: Added Alibaba Cloud DashScope model name support ([ce58f23](https://github.com/zhayujie/CowAgent/commit/ce58f23))
|
||||
|
||||
## Website & Documentation
|
||||
|
||||
@@ -93,6 +93,6 @@ Related commits: [29bfbec](https://github.com/zhayujie/chatgpt-on-wechat/commit/
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Gemini DingTalk image recognition**: Fixed Gemini unable to process image markers in DingTalk channel ([05a3304](https://github.com/zhayujie/chatgpt-on-wechat/commit/05a3304)) ([#2670](https://github.com/zhayujie/chatgpt-on-wechat/pull/2670)) Thanks [@SgtPepper114](https://github.com/SgtPepper114)
|
||||
- **Startup script dependencies**: Fixed dependency installation issue in `run.sh` script ([b6fc9fa](https://github.com/zhayujie/chatgpt-on-wechat/commit/b6fc9fa))
|
||||
- **Bare except cleanup**: Replaced `bare except` with `except Exception` for better exception handling ([adca89b](https://github.com/zhayujie/chatgpt-on-wechat/commit/adca89b)) ([#2674](https://github.com/zhayujie/chatgpt-on-wechat/pull/2674)) Thanks [@haosenwang1018](https://github.com/haosenwang1018)
|
||||
- **Gemini DingTalk image recognition**: Fixed Gemini unable to process image markers in DingTalk channel ([05a3304](https://github.com/zhayujie/CowAgent/commit/05a3304)) ([#2670](https://github.com/zhayujie/CowAgent/pull/2670)) Thanks [@SgtPepper114](https://github.com/SgtPepper114)
|
||||
- **Startup script dependencies**: Fixed dependency installation issue in `run.sh` script ([b6fc9fa](https://github.com/zhayujie/CowAgent/commit/b6fc9fa))
|
||||
- **Bare except cleanup**: Replaced `bare except` with `except Exception` for better exception handling ([adca89b](https://github.com/zhayujie/CowAgent/commit/adca89b)) ([#2674](https://github.com/zhayujie/CowAgent/pull/2674)) Thanks [@haosenwang1018](https://github.com/haosenwang1018)
|
||||
|
||||
91
docs/en/releases/v2.0.3.mdx
Normal file
91
docs/en/releases/v2.0.3.mdx
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: v2.0.3
|
||||
description: CowAgent 2.0.3 - WeCom Smart Bot and QQ channels, Web Console file handling, memory system upgrade
|
||||
---
|
||||
|
||||
## 🔌 New Channels
|
||||
|
||||
### WeCom Smart Bot
|
||||
|
||||
Added the WeCom Smart Bot (`wecom_bot`) channel with streaming card output, support for receiving and replying to text and image messages, and full configuration through the Web Console.
|
||||
|
||||
Documentation: [WeCom Smart Bot](https://docs.cowagent.ai/en/channels/wecom-bot).
|
||||
|
||||
Related commits: [d4480b6](https://github.com/zhayujie/CowAgent/commit/d4480b6), [a42f31f](https://github.com/zhayujie/CowAgent/commit/a42f31f), [4ecd4df](https://github.com/zhayujie/CowAgent/commit/4ecd4df), [8b45d6c](https://github.com/zhayujie/CowAgent/commit/8b45d6c)
|
||||
|
||||
### QQ Channel
|
||||
|
||||
Added the QQ official bot (`qq`) channel with support for text and image messages in both private chats and group chats.
|
||||
|
||||
Documentation: [QQ Bot](https://docs.cowagent.ai/en/channels/qq).
|
||||
|
||||
Related commits: [005a0e1](https://github.com/zhayujie/CowAgent/commit/005a0e1), [a4d54f5](https://github.com/zhayujie/CowAgent/commit/a4d54f5)
|
||||
|
||||
## 🖥️ Web Console File Input and Processing
|
||||
|
||||
The Web Console chat UI now supports file and image uploads — files can be sent directly to the agent for processing. The Read tool gains parsing support for Office documents (Word, Excel, PPT).
|
||||
|
||||
Related commits: [30c6d9b](https://github.com/zhayujie/CowAgent/commit/30c6d9b)
|
||||
|
||||
## 🤖 New Models
|
||||
|
||||
- **GPT-5.4 Series**: Added `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano` ([1623deb](https://github.com/zhayujie/CowAgent/commit/1623deb))
|
||||
- **Gemini 3.1 Flash Lite Preview**: Added `gemini-3.1-flash-lite-preview` ([ba915f2](https://github.com/zhayujie/CowAgent/commit/ba915f2))
|
||||
|
||||
## 💰 Coding Plan Support
|
||||
|
||||
Added integration with vendor Coding Plan (monthly programming subscription) tiers via the unified OpenAI-compatible path. Supported vendors include Aliyun, MiniMax, Zhipu GLM, Kimi, and Volcengine.
|
||||
|
||||
See [Coding Plan docs](https://docs.cowagent.ai/en/models/coding-plan) for detailed configuration.
|
||||
|
||||
## 🧠 Memory System Upgrade
|
||||
|
||||
Memory flush improvements:
|
||||
|
||||
- Use the LLM to summarize out-of-window conversations into compact daily memory entries
|
||||
- Summarization runs asynchronously on a background thread, never blocking replies
|
||||
- Smarter batch trimming policy reduces flush frequency
|
||||
- Daily scheduled flush as a safety net for low-activity scenarios
|
||||
- Fixed context-memory loss issues
|
||||
|
||||
Related commits: [022c13f](https://github.com/zhayujie/CowAgent/commit/022c13f), [c116235](https://github.com/zhayujie/CowAgent/commit/c116235)
|
||||
|
||||
## 🔧 Tool Refactoring
|
||||
|
||||
- **Image Vision**: Image recognition (Vision) is refactored from a Skill into a built-in Tool with a dedicated Vision Provider configuration, improving stability and maintainability ([a50fafa](https://github.com/zhayujie/CowAgent/commit/a50fafa), [3b8b562](https://github.com/zhayujie/CowAgent/commit/3b8b562))
|
||||
- **Web Fetch**: Web fetch is refactored from a Skill into a built-in Tool with support for downloading and parsing remote documents (PDF, Word, Excel, PPT) ([ccb9030](https://github.com/zhayujie/CowAgent/commit/ccb9030), [fa61744](https://github.com/zhayujie/CowAgent/commit/fa61744))
|
||||
|
||||
## 🐳 Docker Deployment Improvements
|
||||
|
||||
- **Config Template Alignment**: `docker-compose.yml` env vars aligned with `config-template.json`, covering full model API key and Agent settings
|
||||
- **Web Console Port Mapping**: Added `9899` port mapping so the Web Console is reachable in browser after Docker deployment
|
||||
- **Hot Config Reload**: Bot API key and API base are now read at request time — changes from the Web Console take effect without restart
|
||||
- **Workspace Persistence**: Added a `./cow` volume mount so agent workspace data (memories, persona, skills, etc.) persists across container rebuilds and upgrades
|
||||
|
||||
## ⚡ Performance Improvements
|
||||
|
||||
- **Faster Startup**: The Feishu channel imports its dependencies lazily, avoiding a 4–10s startup delay ([924dc79](https://github.com/zhayujie/CowAgent/commit/924dc79))
|
||||
- **Channel Stability**: Improved channel connection stability and added env-var support for channel configuration ([f1c04bc](https://github.com/zhayujie/CowAgent/commit/f1c04bc), [46d97fd](https://github.com/zhayujie/CowAgent/commit/46d97fd))
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **bot_type Propagation**: Fixed `bot_type` propagation under Agent mode ([#2691](https://github.com/zhayujie/CowAgent/pull/2691)) Thanks [@Weikjssss](https://github.com/Weikjssss)
|
||||
- **bot_type Resolution Priority**: Adjusted `bot_type` resolution priority under Agent mode ([#2692](https://github.com/zhayujie/CowAgent/pull/2692)) Thanks [@6vision](https://github.com/6vision)
|
||||
- **Zhipu Config**: Fixed Zhipu `bot_type` naming, Web Console persistence, and regex escaping ([#2693](https://github.com/zhayujie/CowAgent/pull/2693)) Thanks [@6vision](https://github.com/6vision)
|
||||
- **OpenAI-Compat Layer**: Unified error handling via the `openai_compat` layer ([#2688](https://github.com/zhayujie/CowAgent/pull/2688)) Thanks [@JasonOA888](https://github.com/JasonOA888)
|
||||
- **OpenAI-Compat Migration**: Completed the `openai_compat` migration across all model bots ([#2689](https://github.com/zhayujie/CowAgent/pull/2689))
|
||||
- **Gemini Tool Calling**: Fixed tool-call matching for Gemini ([eda82ba](https://github.com/zhayujie/CowAgent/commit/eda82ba))
|
||||
- **Session Concurrency**: Fixed race conditions in concurrent session scenarios ([9879878](https://github.com/zhayujie/CowAgent/commit/9879878))
|
||||
- **History Recovery**: Fixed incomplete history recovery — only user/assistant text messages are restored, tool calls are stripped ([b788a3d](https://github.com/zhayujie/CowAgent/commit/b788a3d), [a33ce97](https://github.com/zhayujie/CowAgent/commit/a33ce97))
|
||||
- **Feishu Group Chat**: Removed the `bot_name` dependency for Feishu group chats ([b641bff](https://github.com/zhayujie/CowAgent/commit/b641bff))
|
||||
- **Safari Compatibility**: Fixed an IME Enter key issue that mistakenly sent messages on Safari ([0687916](https://github.com/zhayujie/CowAgent/commit/0687916))
|
||||
- **Windows Compatibility**: Fixed bash-style `$VAR` to `%VAR%` env-var conversion on Windows ([7c67513](https://github.com/zhayujie/CowAgent/commit/7c67513))
|
||||
- **MiniMax Params**: Added a `max_tokens` cap for MiniMax models ([1767413](https://github.com/zhayujie/CowAgent/commit/1767413))
|
||||
- **.gitignore**: Added Python directory ignore rules ([#2683](https://github.com/zhayujie/CowAgent/pull/2683)) Thanks [@pelioo](https://github.com/pelioo)
|
||||
- **AGENT.md Proactive Evolution**: Improved the system prompt guidance around AGENT.md — instead of waiting for explicit user edits, the agent now proactively detects persona/style shifts in the conversation and updates AGENT.md accordingly
|
||||
|
||||
## 📦 Upgrade
|
||||
|
||||
Run `./run.sh update` for a one-click upgrade, or manually pull the latest code and restart. See [Upgrade Guide](https://docs.cowagent.ai/en/guide/upgrade) for details.
|
||||
|
||||
**Release Date**: 2026.03.18 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.2...2.0.3)
|
||||
@@ -16,40 +16,40 @@ Added personal WeChat (`weixin`) channel — the most important update in this r
|
||||
|
||||
Documentation: [WeChat Channel](https://docs.cowagent.ai/channels/weixin).
|
||||
|
||||
Related commits: [ce89869](https://github.com/zhayujie/chatgpt-on-wechat/commit/ce89869), [a483ec0](https://github.com/zhayujie/chatgpt-on-wechat/commit/a483ec0), [c1421e0](https://github.com/zhayujie/chatgpt-on-wechat/commit/c1421e0)
|
||||
Related commits: [ce89869](https://github.com/zhayujie/CowAgent/commit/ce89869), [a483ec0](https://github.com/zhayujie/CowAgent/commit/a483ec0), [c1421e0](https://github.com/zhayujie/CowAgent/commit/c1421e0)
|
||||
|
||||
## 🤖 New Models
|
||||
|
||||
- **MiniMax-M2.7**: Added MiniMax-M2.7 model support
|
||||
- **GLM-5-Turbo**: Added Zhipu glm-5-turbo model support
|
||||
|
||||
Related commits: [9192f6f](https://github.com/zhayujie/chatgpt-on-wechat/commit/9192f6f)
|
||||
Related commits: [9192f6f](https://github.com/zhayujie/CowAgent/commit/9192f6f)
|
||||
|
||||
## 🔧 Script Refactoring
|
||||
|
||||
- **run.sh Refactoring**: Extracted shared logic and eliminated duplication, reducing from 600+ lines to 177 lines ([49d8707](https://github.com/zhayujie/chatgpt-on-wechat/commit/49d8707))
|
||||
- **Executable Permission**: Fixed `run.sh` file permission issue ([652156e](https://github.com/zhayujie/chatgpt-on-wechat/commit/652156e))
|
||||
- **run.sh Refactoring**: Extracted shared logic and eliminated duplication, reducing from 600+ lines to 177 lines ([49d8707](https://github.com/zhayujie/CowAgent/commit/49d8707))
|
||||
- **Executable Permission**: Fixed `run.sh` file permission issue ([652156e](https://github.com/zhayujie/CowAgent/commit/652156e))
|
||||
|
||||
## ⚡ Improvements
|
||||
|
||||
- **Unified Request Headers**: Added identification headers to external requests across Agent services (Chat, Embedding, Vision, WebSearch, etc.) ([b4e711f](https://github.com/zhayujie/chatgpt-on-wechat/commit/b4e711f))
|
||||
- **Auto-Repair Messages**: Enhanced message protocol fault tolerance with automatic repair of malformed message sequences ([b8b57e3](https://github.com/zhayujie/chatgpt-on-wechat/commit/b8b57e3))
|
||||
- **Unified Request Headers**: Added identification headers to external requests across Agent services (Chat, Embedding, Vision, WebSearch, etc.) ([b4e711f](https://github.com/zhayujie/CowAgent/commit/b4e711f))
|
||||
- **Auto-Repair Messages**: Enhanced message protocol fault tolerance with automatic repair of malformed message sequences ([b8b57e3](https://github.com/zhayujie/CowAgent/commit/b8b57e3))
|
||||
|
||||
## 🌍 Japanese Documentation
|
||||
|
||||
Added complete Japanese documentation covering getting started guide, channel integration, model configuration and other major sections. Thanks [@Ikko Ashimine](https://github.com/ikoamu)
|
||||
|
||||
Related commits: [5487c0b](https://github.com/zhayujie/chatgpt-on-wechat/commit/5487c0b)
|
||||
Related commits: [5487c0b](https://github.com/zhayujie/CowAgent/commit/5487c0b)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **WeCom Bot Compatibility**: Fixed compatibility with older `websocket-client` versions, added unified WebSocket compatibility layer ([bc7f627](https://github.com/zhayujie/chatgpt-on-wechat/commit/bc7f627))
|
||||
- **run.sh PID**: Fixed process PID retrieval error in `run.sh` ([9febb07](https://github.com/zhayujie/chatgpt-on-wechat/commit/9febb07))
|
||||
- **Feishu Encoding**: Fixed message and log encoding issue in Feishu channel ([7d0e156](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d0e156))
|
||||
- **Feishu Config**: Removed redundant `feishu_bot_name` dependency in `run.sh` ([1b5be1b](https://github.com/zhayujie/chatgpt-on-wechat/commit/1b5be1b))
|
||||
- **WeCom Bot Compatibility**: Fixed compatibility with older `websocket-client` versions, added unified WebSocket compatibility layer ([bc7f627](https://github.com/zhayujie/CowAgent/commit/bc7f627))
|
||||
- **run.sh PID**: Fixed process PID retrieval error in `run.sh` ([9febb07](https://github.com/zhayujie/CowAgent/commit/9febb07))
|
||||
- **Feishu Encoding**: Fixed message and log encoding issue in Feishu channel ([7d0e156](https://github.com/zhayujie/CowAgent/commit/7d0e156))
|
||||
- **Feishu Config**: Removed redundant `feishu_bot_name` dependency in `run.sh` ([1b5be1b](https://github.com/zhayujie/CowAgent/commit/1b5be1b))
|
||||
|
||||
## 📦 Upgrade
|
||||
|
||||
Run `./run.sh update` for a one-click upgrade, or manually pull the latest code and restart. See [Upgrade Guide](https://docs.cowagent.ai/guide/upgrade) for details.
|
||||
|
||||
**Release Date**: 2026.03.22 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.3...master)
|
||||
**Release Date**: 2026.03.22 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.3...master)
|
||||
|
||||
@@ -57,21 +57,21 @@ WeCom Bot channel now supports QR code scan for one-click bot creation:
|
||||
|
||||
Docs: [WeCom Bot](https://docs.cowagent.ai/en/channels/wecom-bot)
|
||||
|
||||
PR: [#2735](https://github.com/zhayujie/chatgpt-on-wechat/pull/2735). Thanks [@WecomTeam](https://github.com/WecomTeam)
|
||||
PR: [#2735](https://github.com/zhayujie/CowAgent/pull/2735). Thanks [@WecomTeam](https://github.com/WecomTeam)
|
||||
|
||||
## 🐛 Other Improvements & Fixes
|
||||
|
||||
- **DeepSeek module**: Independent DeepSeek Bot with dedicated `deepseek_api_key` config ([#2719](https://github.com/zhayujie/chatgpt-on-wechat/pull/2719)). Thanks [@6vision](https://github.com/6vision)
|
||||
- **Web console**: Slash command menu, input history, new model options, mobile optimization ([#2731](https://github.com/zhayujie/chatgpt-on-wechat/pull/2731)). Thanks [@zkjqd](https://github.com/zkjqd)
|
||||
- **Context loss**: Fix context loss after trimming ([393f0c0](https://github.com/zhayujie/chatgpt-on-wechat/commit/393f0c0))
|
||||
- **System prompt**: Fix system prompt not rebuilding on every turn ([13f5fde](https://github.com/zhayujie/chatgpt-on-wechat/commit/13f5fde))
|
||||
- **Gemini**: Fix missing model attribute in GoogleGeminiBot ([#2716](https://github.com/zhayujie/chatgpt-on-wechat/pull/2716)). Thanks [@cowagent](https://github.com/cowagent)
|
||||
- **WeChat channel**: Fix file send failures and filename loss ([6d9b7ba](https://github.com/zhayujie/chatgpt-on-wechat/commit/6d9b7ba), [45faa9c](https://github.com/zhayujie/chatgpt-on-wechat/commit/45faa9c))
|
||||
- **Docker**: Fix volume permissions, reduce image size ([3eb8348](https://github.com/zhayujie/chatgpt-on-wechat/commit/3eb8348), [4470d4c](https://github.com/zhayujie/chatgpt-on-wechat/commit/4470d4c))
|
||||
- **DeepSeek module**: Independent DeepSeek Bot with dedicated `deepseek_api_key` config ([#2719](https://github.com/zhayujie/CowAgent/pull/2719)). Thanks [@6vision](https://github.com/6vision)
|
||||
- **Web console**: Slash command menu, input history, new model options, mobile optimization ([#2731](https://github.com/zhayujie/CowAgent/pull/2731)). Thanks [@zkjqd](https://github.com/zkjqd)
|
||||
- **Context loss**: Fix context loss after trimming ([393f0c0](https://github.com/zhayujie/CowAgent/commit/393f0c0))
|
||||
- **System prompt**: Fix system prompt not rebuilding on every turn ([13f5fde](https://github.com/zhayujie/CowAgent/commit/13f5fde))
|
||||
- **Gemini**: Fix missing model attribute in GoogleGeminiBot ([#2716](https://github.com/zhayujie/CowAgent/pull/2716)). Thanks [@cowagent](https://github.com/cowagent)
|
||||
- **WeChat channel**: Fix file send failures and filename loss ([6d9b7ba](https://github.com/zhayujie/CowAgent/commit/6d9b7ba), [45faa9c](https://github.com/zhayujie/CowAgent/commit/45faa9c))
|
||||
- **Docker**: Fix volume permissions, reduce image size ([3eb8348](https://github.com/zhayujie/CowAgent/commit/3eb8348), [4470d4c](https://github.com/zhayujie/CowAgent/commit/4470d4c))
|
||||
- **Security**: Fix Memory Content path traversal risk. Thanks [@August829](https://github.com/August829)
|
||||
|
||||
## 📦 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.01 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.4...master)
|
||||
**Release Date**: 2026.04.01 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.4...master)
|
||||
|
||||
83
docs/en/releases/v2.0.6.mdx
Normal file
83
docs/en/releases/v2.0.6.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: v2.0.6
|
||||
description: CowAgent 2.0.6 - Knowledge Base, Deep Dream Memory Distillation, Smart Context Compression, Web Console Multi-Session and More
|
||||
---
|
||||
|
||||
## Project Renamed to CowAgent
|
||||
|
||||
The repository has been officially renamed from `chatgpt-on-wechat` to **CowAgent**, evolving into a full-featured AI Agent assistant.
|
||||
|
||||
- New URL: [github.com/zhayujie/CowAgent](https://github.com/zhayujie/CowAgent) — GitHub auto-redirects the old URL
|
||||
- CLI commands, config files, and documentation links remain compatible — no extra steps needed
|
||||
|
||||
## 📚 Knowledge Base
|
||||
|
||||
New personal knowledge base system — Agent can autonomously build and maintain structured knowledge, retrieving it on demand during conversations:
|
||||
|
||||
- **Index-driven self-organizing structure**: Knowledge is stored in `knowledge/` directory, auto-organized by category, with each knowledge page as an independent Markdown file
|
||||
- **Auto-write**: Send files, links, or other knowledge to the Agent, or it will automatically create/update knowledge pages when valuable information is identified in conversation
|
||||
- **Hybrid retrieval**: Supports keyword full-text search and vector semantic retrieval, loading relevant knowledge on demand during conversations
|
||||
- **Visualization**: File tree browsing and knowledge graph visualization, with in-document links for direct navigation
|
||||
- **Command management**: `/knowledge` for stats, `/knowledge list` for directory structure, `/knowledge on|off` to toggle
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="750" />
|
||||
|
||||
|
||||
Docs: [Knowledge Base](https://docs.cowagent.ai/en/knowledge)
|
||||
|
||||
## 🌙 Deep Dream Memory Distillation
|
||||
|
||||
A new memory consolidation mechanism that automatically distills scattered conversation memories into refined long-term memory daily:
|
||||
|
||||
- **Three-tier memory flow**: Conversation context (short-term) → Daily memory (mid-term) → MEMORY.md (long-term), forming a complete memory lifecycle
|
||||
- **Auto-distillation**: Runs daily at 23:55, reads the day's daily memory and MEMORY.md, performs deduplication, merging, and pruning via LLM, outputting a refined MEMORY.md
|
||||
- **Dream diary**: Each distillation generates a narrative-style dream diary recording discoveries and insights, stored in `memory/dreams/`
|
||||
- **Manual trigger**: `/memory dream [N]` to manually trigger with configurable lookback days (default 3, max 30), with chat notification on completion
|
||||
- **Web console**: Memory management page now includes a "Dream Diary" tab for browsing all dream diaries
|
||||
|
||||
Docs: [Deep Dream](https://docs.cowagent.ai/en/memory/deep-dream)
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260414120158.png" width="750" />
|
||||
|
||||
## 🧠 Smart Context Compression
|
||||
|
||||
When context exceeds limits, trimmed portions are summarized by LLM and asynchronously injected to maintain conversation continuity:
|
||||
|
||||
- **Async LLM summary**: Trimmed messages are summarized into key information by LLM, written to daily memory files and injected into retained context
|
||||
- **Multi-model compatible**: Uses the primary model for summarization, compatible with Claude, OpenAI, MiniMax and other model message format requirements
|
||||
|
||||
Docs: [Short-term Memory](https://docs.cowagent.ai/en/memory/context)
|
||||
|
||||
## 💬 Web Console Upgrades
|
||||
|
||||
Multiple enhancements to the Web console:
|
||||
|
||||
- **Multi-session management**: Create and switch between independent sessions, sidebar session list with auto-generated and manually editable titles
|
||||
- **Password protection**: Set a login password via `web_console_password` config option
|
||||
- **Deep thinking**: Display model thinking process in Web console, controlled by `enable_thinking` config option
|
||||
- **Scheduled push**: Scheduled task results can be pushed to Web console
|
||||
- **Message copy**: One-click copy of raw Markdown content from AI reply bubbles
|
||||
- **Language toggle**: Top language switch button now shows current language for more intuitive interaction
|
||||
|
||||
## 🤖 Model Updates
|
||||
|
||||
- **Vision optimization**: Image recognition tool prefers the primary model with automatic multi-provider fallback. Docs: [Vision Tool](https://docs.cowagent.ai/en/tools/vision)
|
||||
- **MiniMax new model**: Added MiniMax-M2.7-highspeed model and MiniMax TTS voice synthesis support. Thanks @octo-patch
|
||||
- **Qwen**: Added qwen3.6-plus model support
|
||||
|
||||
## 🐛 Other Improvements & Fixes
|
||||
|
||||
- **Memory prompts**: `MEMORY.md` injected into system prompt by default, with refined memory retrieval and write trigger conditions for enhanced proactive writing
|
||||
- **System prompt**: Optimized system prompt style and tone guidance
|
||||
- **Browser tool**: Enhanced implicit interactive element detection
|
||||
- **File send**: Fixed common file types (tar.gz, zip, etc.) not being sent correctly. Thanks @6vision
|
||||
- **macOS compatibility**: Fixed network pre-check timeout compatibility issue. Thanks @Moliang Zhou
|
||||
- **Windows compatibility**: Fixed PowerShell compatibility, process updates, terminal encoding and other issues on Windows
|
||||
- **Python 3.13+**: Fixed missing `legacy-cgi` dependency for Python 3.13+
|
||||
- **WeChat channel**: Updated personal WeChat channel version
|
||||
|
||||
## 📦 Upgrade
|
||||
|
||||
Run `cow update` or `./run.sh update` to upgrade, or pull the latest code and restart. See [Upgrade Guide](https://docs.cowagent.ai/en/guide/upgrade).
|
||||
|
||||
**Release Date**: 2026.04.14 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.5...master)
|
||||
65
docs/en/releases/v2.0.7.mdx
Normal file
65
docs/en/releases/v2.0.7.mdx
Normal 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/1K–4K), 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)
|
||||
68
docs/en/releases/v2.0.8.mdx
Normal file
68
docs/en/releases/v2.0.8.mdx
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: v2.0.8
|
||||
description: CowAgent 2.0.8 - Major Feishu channel upgrade (voice, streaming typewriter, one-click QR app creation), DeepSeek V4 / Baidu Qianfan ERNIE 5.0 support, scheduler memory enhancements and multiple fixes
|
||||
---
|
||||
|
||||
## 🪶 Major Feishu Channel Upgrade
|
||||
|
||||
### 1. One-click QR-scan App Creation
|
||||
|
||||
No more manual app setup, permission scopes and event subscriptions in the Feishu Open Platform. When `feishu_app_id` is not configured, both the Web Console and CLI startup flow now show a QR-scan entry — scan with Feishu, authorize, and the bot is created and config is filled back automatically. Out-of-the-box.
|
||||
|
||||
Documentation: [Feishu Channel](https://docs.cowagent.ai/en/channels/feishu)
|
||||
|
||||
### 2. Voice Messages
|
||||
|
||||
Receive Feishu voice messages with automatic speech-to-text, and reply in voice via TTS. Recognition accuracy for short Chinese voice messages has been improved.
|
||||
|
||||
### 3. Streaming Typewriter Replies
|
||||
|
||||
Integrated with Feishu CardKit streaming cards, **enabled by default**, matching the Web Console experience:
|
||||
|
||||
- Multi-turn agent flows render intermediate updates and the final reply on separate cards
|
||||
- Tuned for high-throughput models like DeepSeek to keep pace with the Web Console
|
||||
- Falls back to plain text replies automatically when not supported, no manual config needed
|
||||
- Requires Feishu client ≥ 7.20
|
||||
|
||||
The voice and streaming building blocks come from a community contribution #2791. Thanks [@ooaaooaa123](https://github.com/ooaaooaa123)
|
||||
|
||||
## 🤖 New Model Support
|
||||
|
||||
- **DeepSeek V4 series**: Added `deepseek-v4-pro` / `deepseek-v4-flash`, with `deepseek-v4-flash` set as the new default
|
||||
- **Unified thinking-mode toggle**: DeepSeek V4, Qwen3 and other thinking-capable models now share the same `enable_thinking` switch
|
||||
- **Baidu Qianfan / ERNIE first-class integration**: New `qianfan` provider supporting `ernie-5.0` (default recommendation), `ernie-x1.1`, `ernie-4.5-turbo-128k`, `ernie-4.5-turbo-32k`. Dedicated `qianfan_api_key` / `qianfan_api_base` settings keep OpenAI config clean; legacy `wenxin` / `wenxin-4` paths are fully preserved. #2790 Thanks [@jimmyzhuu](https://github.com/jimmyzhuu)
|
||||
|
||||
Documentation: [Baidu Qianfan / ERNIE](https://docs.cowagent.ai/en/models/qianfan)
|
||||
|
||||
## 🌐 Translation Provider
|
||||
|
||||
- **Youdao translator**: Added a Youdao provider to the `translate/` module using the v3 SHA-256 signing scheme, with automatic ISO 639-1 language-code mapping (`zh`, `zh-TW`, etc.) #2797 Thanks [@Zmjjeff7](https://github.com/Zmjjeff7)
|
||||
|
||||
## 🛠 OpenAI Client Refactor
|
||||
|
||||
- **Drop SDK dependency**: The OpenAI bot is reimplemented on a native HTTP client — leaner startup, fewer dependency conflicts
|
||||
- **Web Console hint**: API base inputs in the model config UI now include version-path placeholder hints
|
||||
|
||||
## ⏰ Scheduler Memory Enhancements
|
||||
|
||||
- **Follow-up on task results**: Scheduled task results are automatically injected into the receiver's session history — the next turn can ask follow-up questions without re-stating context. Thanks [@huangrichao2020](https://github.com/huangrichao2020)
|
||||
- **No long-term memory pollution**: Scheduler-injected pairs are excluded from the daily memory flush so high-frequency tasks don't drown the memory store
|
||||
- **Bounded scheduler context**: The scheduler's own session context is automatically capped, so long-running periodic tasks don't accumulate state and slow down replies
|
||||
|
||||
## 🔧 Tools and Safety
|
||||
|
||||
- **Vision model selection**: `tool.vision.model` config now actually takes effect, with automatic fallback when unconfigured #2792
|
||||
- **Bash safety prompt**: The destructive-deletion confirm prompt is now scoped to paths outside the workspace — routine in-workspace operations are no longer interrupted
|
||||
|
||||
## 🐛 Other Fixes
|
||||
|
||||
- Fixed Deep Dream firing duplicate runs in multi-instance setups
|
||||
- Fixed missing `reasoning_content` on some history turns in DeepSeek multi-turn conversations
|
||||
|
||||
## 📦 Upgrade
|
||||
|
||||
Source-code deployments can run `cow update` or `./run.sh update` for a one-click upgrade, or pull the latest code and restart manually. See [Upgrade Guide](https://docs.cowagent.ai/en/guide/upgrade) for details.
|
||||
|
||||
> ⚠️ One-click Feishu app creation requires `lark-oapi>=1.5.5`. `cow update` pulls it automatically; manual deployments must update dependencies.
|
||||
|
||||
**Release Date**: 2026.05.05 | [Full Changelog](https://github.com/zhayujie/CowAgent/compare/2.0.7...2.0.8)
|
||||
@@ -54,5 +54,5 @@ Detailed instructions...
|
||||
| `metadata.always` | Always load (default false) |
|
||||
|
||||
<Tip>
|
||||
See the [Skill Creator documentation](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/skills/skill-creator/SKILL.md) for details.
|
||||
See the [Skill Creator documentation](https://github.com/zhayujie/CowAgent/blob/master/skills/skill-creator/SKILL.md) for details.
|
||||
</Tip>
|
||||
|
||||
158
docs/en/skills/image-generation.mdx
Normal file
158
docs/en/skills/image-generation.mdx
Normal 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 2K–4K, 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 1–5 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>
|
||||
112
docs/en/skills/knowledge-wiki.mdx
Normal file
112
docs/en/skills/knowledge-wiki.mdx
Normal 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
|
||||
180
docs/en/skills/skill-creator.mdx
Normal file
180
docs/en/skills/skill-creator.mdx
Normal 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.
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
title: memory - Memory
|
||||
description: Search and read long-term memory
|
||||
title: memory - Memory & Knowledge
|
||||
description: Search and read long-term memory and knowledge base files
|
||||
---
|
||||
|
||||
The memory tool contains two sub-tools: `memory_search` (search memory) and `memory_get` (read memory files).
|
||||
The memory tool contains two sub-tools: `memory_search` (search memory) and `memory_get` (read memory or knowledge files).
|
||||
|
||||
When the [knowledge base](/en/knowledge) feature is enabled, both tools also support accessing files under the `knowledge/` directory.
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -11,7 +13,7 @@ No extra dependencies, available by default. Managed by the Agent Core memory sy
|
||||
|
||||
## memory_search
|
||||
|
||||
Search historical memory with hybrid keyword and vector retrieval.
|
||||
Search historical memory and knowledge base content with hybrid keyword and vector retrieval.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -19,11 +21,11 @@ Search historical memory with hybrid keyword and vector retrieval.
|
||||
|
||||
## memory_get
|
||||
|
||||
Read the content of a specific memory file.
|
||||
Read the content of a specific memory or knowledge file.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | Yes | Relative path to memory file (e.g. `MEMORY.md`, `memory/2026-01-01.md`) |
|
||||
| `path` | string | Yes | Relative path to the file (e.g. `MEMORY.md`, `memory/2026-01-01.md`, `knowledge/concepts/rag.md`) |
|
||||
| `start_line` | integer | No | Start line number |
|
||||
| `end_line` | integer | No | End line number |
|
||||
|
||||
@@ -34,3 +36,8 @@ The Agent automatically invokes memory tools in these scenarios:
|
||||
- When the user shares important information → stores to memory
|
||||
- When historical context is needed → searches relevant memory
|
||||
- When conversation reaches a certain length → extracts summary for storage
|
||||
- When discussing domain knowledge → retrieves relevant pages from the knowledge base
|
||||
|
||||
<Note>
|
||||
When `knowledge` is set to `false` in config, the tool descriptions and search scope automatically adjust to include only memory files.
|
||||
</Note>
|
||||
|
||||
@@ -23,11 +23,12 @@ If the current provider fails, the tool automatically tries the next one until i
|
||||
| Vendor | Vision Model | Notes |
|
||||
| --- | --- | --- |
|
||||
| OpenAI / Compatible | Main model | All OpenAI-compatible multimodal models |
|
||||
| Baidu Qianfan | Main model | Multimodal main models (e.g. `ernie-5.0`) handle images directly; falls back to `ernie-4.5-turbo-vl` for text-only main models |
|
||||
| Qwen (DashScope) | Main model | Via MultiModalConversation API |
|
||||
| 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 |
|
||||
|
||||
@@ -52,7 +53,7 @@ To specify a particular model for the vision tool, add to `config.json`:
|
||||
{
|
||||
"tool": {
|
||||
"vision": {
|
||||
"model": "gpt-4o"
|
||||
"model": "ernie-4.5-turbo-vl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ description: 手动部署 CowAgent(源码 / Docker)
|
||||
### 1. 克隆项目代码
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
git clone https://github.com/zhayujie/CowAgent
|
||||
cd CowAgent/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
若遇到网络问题可使用国内仓库地址:https://gitee.com/zhayujie/chatgpt-on-wechat
|
||||
若遇到网络问题可使用国内仓库地址:https://gitee.com/zhayujie/CowAgent
|
||||
</Tip>
|
||||
|
||||
### 2. 安装依赖
|
||||
@@ -139,7 +139,8 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
```json
|
||||
{
|
||||
"channel_type": "web",
|
||||
"model": "MiniMax-M2.7",
|
||||
"model": "deepseek-v4-flash",
|
||||
"deepseek_api_key": "",
|
||||
"agent": true,
|
||||
"agent_workspace": "~/cow",
|
||||
"agent_max_context_tokens": 40000,
|
||||
@@ -152,8 +153,9 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
```yaml
|
||||
environment:
|
||||
CHANNEL_TYPE: 'web'
|
||||
MODEL: 'MiniMax-M2.7'
|
||||
MINIMAX_API_KEY: 'your-api-key'
|
||||
MODEL: 'deepseek-v4-flash'
|
||||
DEEPSEEK_API_KEY: 'your-api-key'
|
||||
DEEPSEEK_API_BASE: 'https://api.deepseek.com/v1'
|
||||
AGENT: 'True'
|
||||
AGENT_MAX_CONTEXT_TOKENS: 40000
|
||||
AGENT_MAX_CONTEXT_TURNS: 30
|
||||
@@ -165,7 +167,7 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
| 参数 | 环境变量 | 说明 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| `channel_type` | `CHANNEL_TYPE` | 接入渠道类型 | `web` |
|
||||
| `model` | `MODEL` | 模型名称 | `MiniMax-M2.5` |
|
||||
| `model` | `MODEL` | 模型名称 | `deepseek-v4-flash` |
|
||||
| `agent` | `AGENT` | 是否启用 Agent 模式 | `true` |
|
||||
| `agent_workspace` | - | Agent 工作空间路径 | `~/cow` |
|
||||
| `agent_max_context_tokens` | `AGENT_MAX_CONTEXT_TOKENS` | 最大上下文 tokens | `40000` |
|
||||
@@ -173,5 +175,5 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
| `agent_max_steps` | `AGENT_MAX_STEPS` | 单次任务最大决策步数 | `15` |
|
||||
|
||||
<Tip>
|
||||
全部配置项可在项目 [`config.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py) 文件中查看。Docker 部署时,配置项名称需转为大写环境变量格式。
|
||||
全部配置项可在项目 [`config.py`](https://github.com/zhayujie/CowAgent/blob/master/config.py) 文件中查看。Docker 部署时,配置项名称需转为大写环境变量格式。
|
||||
</Tip>
|
||||
|
||||
@@ -26,7 +26,7 @@ description: 使用脚本一键安装和管理 CowAgent
|
||||
|
||||
1. 检查 Python 环境(需要 Python 3.7+)
|
||||
2. 安装必要工具(git、curl 等)
|
||||
3. 克隆项目代码到 `~/chatgpt-on-wechat`
|
||||
3. 克隆项目代码到 `~/CowAgent`
|
||||
4. 安装 Python 依赖和 Cow CLI
|
||||
5. 引导配置 AI 模型和通信渠道
|
||||
6. 启动服务
|
||||
|
||||
@@ -9,27 +9,29 @@ 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" />
|
||||
|
||||
| 模块 | 说明 |
|
||||
| --- | --- |
|
||||
| **Channels** | 消息通道层,负责接收和发送消息,支持 Web、飞书、钉钉、企微、公众号等 |
|
||||
| **Agent Core** | 智能体核心引擎,包括任务规划、记忆系统和技能引擎 |
|
||||
| **Tools** | 工具层,Agent 通过工具访问操作系统资源,内置 10+ 种工具 |
|
||||
| **Models** | 模型层,支持国内外主流大语言模型的统一接入 |
|
||||
| **Plan** | 理解用户意图,将复杂任务分解为多步骤计划,循环调用工具直到完成目标 |
|
||||
| **Memory** | 自动将重要信息持久化为核心记忆和日级记忆,支持关键词和向量混合检索,跨会话保持上下文连续性 |
|
||||
| **Knowledge** | 以主题维度组织结构化知识,Agent 自主整理有价值信息为 Markdown 页面,维护索引和交叉引用,构建持续增长的知识网络 |
|
||||
| **Tools** | Agent 访问操作系统资源的核心能力,内置文件读写、终端执行、浏览器操作、定时调度、记忆检索、联网搜索等 10+ 种工具 |
|
||||
| **Skills** | 加载和管理 Skills,支持从 Skill Hub、GitHub 等一键安装,或通过对话创建自定义技能 |
|
||||
| **Models** | 模型层,统一接入 OpenAI、Claude、Gemini、DeepSeek、MiniMax、GLM、Qwen 等国内外主流大语言模型 |
|
||||
| **Channels** | 消息通道层,负责接收和发送消息,支持 Web 控制台、微信、飞书、钉钉、企微、公众号等,统一消息协议 |
|
||||
| **CLI** | 命令行系统,提供终端命令(`cow`)和对话命令(`/`),支持进程管理、技能安装、配置修改、知识库管理等操作 |
|
||||
|
||||
## Agent 模式
|
||||
|
||||
启用 Agent 模式后,CowAgent 会以自主智能体的方式运行,核心工作流如下:
|
||||
|
||||
1. **接收消息** - 通过通道接收用户输入
|
||||
2. **理解意图** - 分析任务需求和上下文
|
||||
3. **规划任务** - 将复杂任务分解为多个步骤
|
||||
4. **调用工具** - 选择合适的工具执行每个步骤
|
||||
5. **记忆更新** - 将重要信息存入长期记忆
|
||||
6. **返回结果** - 将执行结果发送回用户
|
||||
1. **接收消息** — 通过通道接收用户输入
|
||||
2. **理解意图** — 分析任务需求和上下文
|
||||
3. **规划任务** — 将复杂任务分解为多个步骤
|
||||
4. **调用工具** — 选择合适的工具执行每个步骤
|
||||
5. **记忆与知识更新** — 将重要信息存入长期记忆,将结构化知识整理至知识库
|
||||
6. **返回结果** — 将执行结果发送回用户
|
||||
|
||||
## 工作空间
|
||||
|
||||
@@ -37,11 +39,14 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词
|
||||
|
||||
```
|
||||
~/cow/
|
||||
├── system.md # Agent system prompt
|
||||
├── user.md # User profile
|
||||
├── SYSTEM.md # Agent system prompt
|
||||
├── USER.md # User profile
|
||||
├── MEMORY.md # Core memory
|
||||
├── memory/ # Long-term memory storage
|
||||
│ ├── core.md # Core memory
|
||||
│ └── daily/ # Daily memory
|
||||
│ └── YYYY-MM-DD.md # Daily memory
|
||||
├── knowledge/ # Personal knowledge base
|
||||
│ ├── index.md # Knowledge index
|
||||
│ └── <category>/ # Topic-based pages
|
||||
└── skills/ # Custom skills
|
||||
├── skill-1/
|
||||
└── skill-2/
|
||||
@@ -64,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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -72,6 +78,8 @@ Agent 的工作空间默认位于 `~/cow` 目录,用于存储系统提示词
|
||||
| --- | --- | --- |
|
||||
| `agent` | 是否启用 Agent 模式 | `true` |
|
||||
| `agent_workspace` | 工作空间路径 | `~/cow` |
|
||||
| `agent_max_context_tokens` | 最大上下文 token 数 | `40000` |
|
||||
| `agent_max_context_turns` | 最大上下文记忆轮次 | `30` |
|
||||
| `agent_max_steps` | 单次任务最大决策步数 | `15` |
|
||||
| `agent_max_context_tokens` | 最大上下文 token 数 | `50000` |
|
||||
| `agent_max_context_turns` | 最大上下文记忆轮次 | `20` |
|
||||
| `agent_max_steps` | 单次任务最大决策步数 | `20` |
|
||||
| `enable_thinking` | 是否启用深度思考模式 | `false` |
|
||||
| `knowledge` | 是否启用个人知识库 | `true` |
|
||||
|
||||
@@ -1,27 +1,46 @@
|
||||
---
|
||||
title: 功能介绍
|
||||
description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、浏览器工具详细说明
|
||||
description: CowAgent 长期记忆、个人知识库、任务规划、技能系统、CLI 命令、浏览器工具详细说明
|
||||
---
|
||||
|
||||
## 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>
|
||||
|
||||
## 2. 任务规划和工具调用
|
||||
详细说明请参考 [长期记忆](/memory) 和 [梦境蒸馏](/memory/deep-dream)。
|
||||
|
||||
## 2. 个人知识库
|
||||
|
||||
> 知识库系统让 Agent 能够持续积累和组织结构化知识。与按时间线记录的记忆不同,知识库以主题为维度,将文章、对话洞察、学习材料等整理为互相关联的 Markdown 页面,形成持续增长的知识网络。
|
||||
|
||||
Agent 会在对话中自动将有价值的信息整理为知识页面,维护交叉引用和索引,通过 Web 控制台可浏览文档和查看知识图谱。知识库存储在工作空间的 `~/cow/knowledge/` 目录下。
|
||||
|
||||
- **自动整理**:Agent 在对话中自主提取和整理结构化知识,维护索引和交叉引用
|
||||
- **知识图谱**:基于页面间的交叉引用自动构建知识图谱,Web 控制台提供可视化关系图浏览
|
||||
- **对话联动**:Agent 回复中引用的知识文档链接可在 Web 控制台中直接点击跳转查看
|
||||
- **CLI 管理**:通过 `/knowledge` 命令查看统计、浏览目录,通过 `/knowledge on|off` 开关功能
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260413105435.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
详细说明请参考 [个人知识库](/knowledge)。
|
||||
|
||||
## 3. 任务规划和工具调用
|
||||
|
||||
工具是 Agent 访问操作系统资源的核心,Agent 会根据任务需求智能选择和调用工具,完成文件读写、命令执行、定时任务等各类操作。内置工具的实现在项目的 `agent/tools/` 目录下。
|
||||
|
||||
**主要工具:** 文件读写编辑、Bash 终端、浏览器操作、文件发送、定时调度、记忆搜索、联网搜索、环境配置等。
|
||||
|
||||
### 2.1 终端和文件访问
|
||||
### 3.1 终端和文件访问
|
||||
|
||||
针对操作系统的终端和文件的访问能力,是最基础和核心的工具,其他很多工具或技能都是基于此进行扩展。用户可通过手机端与 Agent 交互,操作个人电脑或服务器上的资源:
|
||||
|
||||
@@ -29,15 +48,15 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202181130.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.2 编程能力
|
||||
### 3.2 编程能力
|
||||
|
||||
基于编程能力和系统访问能力,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>
|
||||
|
||||
### 2.3 定时任务
|
||||
### 3.3 定时任务
|
||||
|
||||
基于 `scheduler` 工具实现动态定时任务,支持**一次性任务、固定时间间隔、Cron 表达式**三种形式,任务触发可选择**固定消息发送**或 **Agent 动态任务**执行两种模式:
|
||||
|
||||
@@ -45,7 +64,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202195402.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.4 浏览器操作
|
||||
### 3.4 浏览器操作
|
||||
|
||||
内置 `browser` 工具,Agent 可控制浏览器访问网页、填写表单、点击元素、截图,支持动态 JS 渲染页面。运行 `cow install-browser` 一键安装,自动适配服务器(无头模式)和桌面环境:
|
||||
|
||||
@@ -53,7 +72,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260401115728.png" width="750" />
|
||||
</Frame>
|
||||
|
||||
### 2.5 环境变量管理
|
||||
### 3.5 环境变量管理
|
||||
|
||||
技能所需的秘钥存储在环境变量文件中,由 `env_config` 工具进行管理,你可以通过对话的方式更新秘钥,工具内置安全保护和脱敏策略:
|
||||
|
||||
@@ -61,7 +80,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234939.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 3. 技能系统
|
||||
## 4. 技能系统
|
||||
|
||||
技能系统为 Agent 提供无限的扩展性,每个 Skill 由说明文件、运行脚本(可选)、资源(可选)组成,描述如何完成特定类型的任务。通过 Skill 可以让 Agent 遵循说明完成复杂流程、调用各类工具或对接第三方系统。
|
||||
|
||||
@@ -71,7 +90,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
|
||||
安装技能:`/skill install <名称>` 或 `cow skill install <名称>`,支持从 Skill Hub、GitHub、ClawHub、URL 等来源安装。
|
||||
|
||||
### 3.1 创建技能
|
||||
### 4.1 创建技能
|
||||
|
||||
通过 `skill-creator` 技能可以通过对话的方式快速创建技能。你可以让 Agent 将某个工作流程固化为技能,或者把任意接口文档和示例发送给 Agent,让他直接完成对接:
|
||||
|
||||
@@ -79,7 +98,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202202247.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.2 搜索和图像识别
|
||||
### 4.2 搜索和图像识别
|
||||
|
||||
- **联网搜索:** 内置 `web_search` 工具,支持多种搜索引擎,配置 `BOCHA_API_KEY` 或 `LINKAI_API_KEY` 后启用。
|
||||
- **图像识别:** 内置 `openai-image-vision` 技能,可使用 `gpt-4.1-mini`、`gpt-4.1` 等模型,依赖 `OPENAI_API_KEY`。
|
||||
@@ -88,7 +107,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202213219.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.3 技能广场
|
||||
### 4.3 技能广场
|
||||
|
||||
访问 [skills.cowagent.ai](https://skills.cowagent.ai/) 浏览所有可用技能,或在对话中执行:
|
||||
|
||||
@@ -103,7 +122,7 @@ description: CowAgent 长期记忆、任务规划、技能系统、CLI 命令、
|
||||
<img src="https://cdn.link-ai.tech/doc/20260401110103.png" width="750" />
|
||||
|
||||
|
||||
## 4. CLI 命令系统
|
||||
## 5. CLI 命令系统
|
||||
|
||||
CowAgent 提供两种命令交互方式,覆盖服务管理、技能安装、配置调整等日常运维操作:
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ description: CowAgent - 基于大模型的超级AI助理
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/78c5dd674e2c828642ecc0406669fed7.png" alt="CowAgent" width="450px"/>
|
||||
|
||||
**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆并不断成长。
|
||||
**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆和知识库并不断成长。
|
||||
|
||||
CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入微信、飞书、钉钉、企业微信应用、微信公众号、网页中使用,7×24小时运行于你的个人电脑或服务器中。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/chatgpt-on-wechat">
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/CowAgent">
|
||||
开源代码仓库,欢迎 Star 和贡献
|
||||
</Card>
|
||||
<Card title="免部署在线体验" icon="cloud" href="https://link-ai.tech/cowagent/create">
|
||||
@@ -25,14 +25,14 @@ CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、
|
||||
能够理解复杂任务并自主规划执行,持续思考和调用各类工具和技能直到完成目标。
|
||||
</Card>
|
||||
<Card title="长期记忆" icon="database" href="/memory">
|
||||
自动将对话记忆持久化至本地文件和数据库中,包括全局记忆和天级记忆,支持关键词及向量检索。
|
||||
三层记忆流转(上下文→天级记忆→全局记忆),每日梦境蒸馏整理,支持关键词及向量检索。
|
||||
</Card>
|
||||
<Card title="个人知识库" icon="book" href="/knowledge">
|
||||
自动整理结构化知识,支持知识图谱可视化,通过交叉引用构建持续增长的知识网络。
|
||||
</Card>
|
||||
<Card title="技能系统" icon="puzzle-piece" href="/skills/index">
|
||||
实现了Skills创建和运行的引擎,内置多种技能,并支持通过自然语言对话完成自定义Skills开发。
|
||||
</Card>
|
||||
<Card title="多模态消息" icon="image" href="/channels/web">
|
||||
支持对文本、图片、语音、文件等多类型消息进行解析、处理、生成、发送等操作。
|
||||
</Card>
|
||||
<Card title="工具系统" icon="wrench" href="/tools/index">
|
||||
内置文件读写、终端执行、浏览器操作、定时任务、消息发送等工具,Agent 可自主调用工具完成复杂任务。
|
||||
</Card>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<p align="center"><img src="https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="550" /></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/chatgpt-on-wechat" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/chatgpt-on-wechat" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
|
||||
[<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/README.md">中文</a>] | [<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/docs/en/README.md">English</a>] | [日本語]
|
||||
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/CowAgent" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square" alt="Stars"></a> <br/>
|
||||
[<a href="https://github.com/zhayujie/CowAgent/blob/master/README.md">中文</a>] | [<a href="https://github.com/zhayujie/CowAgent/blob/master/docs/en/README.md">English</a>] | [日本語]
|
||||
</p>
|
||||
|
||||
**CowAgent** はLLMを搭載したAIスーパーアシスタントです。自律的なタスク計画、コンピュータや外部リソースの操作、Skillの作成・実行、長期記憶による継続的な成長が可能です。柔軟なモデル切り替えに対応し、テキスト・音声・画像・ファイルを処理でき、WeChat、Web、Feishu(飛書)、DingTalk(釘釘)、WeCom Bot(企業微信ボット)、WeComアプリ、WeChat公式アカウントに統合可能で、個人のPCやサーバー上で24時間365日稼働できます。
|
||||
**CowAgent** はLLMを搭載したAIスーパーアシスタントです。自律的なタスク計画、コンピュータや外部リソースの操作、Skillの作成・実行、長期記憶とパーソナルナレッジベースによる継続的な成長が可能です。柔軟なモデル切り替えに対応し、テキスト・音声・画像・ファイルを処理でき、WeChat、Web、Feishu(飛書)、DingTalk(釘釘)、WeCom Bot(企業微信ボット)、WeComアプリ、WeChat公式アカウントに統合可能で、個人のPCやサーバー上で24時間365日稼働できます。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cowagent.ai/">🌐 ウェブサイト</a> ·
|
||||
@@ -22,12 +22,13 @@
|
||||
> CowAgentは、すぐに使えるAIスーパーアシスタントであると同時に、高い拡張性を持つAgentフレームワークでもあります。新しいモデルインターフェース、チャネル、組み込みツール、Skillシステムを拡張することで、さまざまなカスタマイズニーズに柔軟に対応できます。
|
||||
|
||||
- ✅ **自律的タスク計画**: 複雑なタスクを理解し、自律的に実行計画を立て、目標達成までツールを呼び出しながら継続的に思考します。
|
||||
- ✅ **長期記憶**: 会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索やベクトル検索に対応しています。
|
||||
- ✅ **長期記憶**: 会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリ、デイリーメモリ、Deep Dream 蒸留を含み、キーワード検索やベクトル検索に対応しています。
|
||||
- ✅ **パーソナルナレッジベース**: 構造化された知識を自動整理し、相互参照によるナレッジグラフを構築。Web での可視化ブラウジングと対話による管理をサポートします。
|
||||
- ✅ **Skillシステム**: Skillの作成・実行エンジンを実装。[Skill Hub](https://skills.cowagent.ai)、GitHubなどからSkillをインストールでき、会話を通じたカスタムSkill作成もサポートしています。
|
||||
- ✅ **ツールシステム**: ファイル読み書き、ターミナル実行、ブラウザ操作、スケジュールタスク、メッセージ送信などの組み込みツールを提供。Agentが自律的に呼び出して複雑なタスクを完了します。
|
||||
- ✅ **CLIシステム**: ターミナルコマンドとチャットコマンドを提供し、プロセス管理、Skillインストール、設定変更などの操作をサポートします。
|
||||
- ✅ **マルチモーダルメッセージ**: テキスト、画像、音声、ファイルなど、さまざまなメッセージタイプの解析・処理・生成・送信に対応しています。
|
||||
- ✅ **複数モデル対応**: OpenAI、Claude、Gemini、DeepSeek、MiniMax、GLM、Qwen、Kimi、Doubaoなど、主要なモデルプロバイダーに対応しています。
|
||||
- ✅ **複数モデル対応**: DeepSeek、MiniMax、Claude、Gemini、OpenAI、GLM、Qwen、Doubao、Kimiなど、主要なモデルプロバイダーに対応しています。
|
||||
- ✅ **マルチプラットフォームデプロイ**: ローカルPCやサーバー上で実行でき、WeChat、Web、Feishu、DingTalk、WeChat公式アカウント、WeComアプリケーションに統合可能です。
|
||||
|
||||
## 免責事項
|
||||
@@ -42,19 +43,21 @@
|
||||
|
||||
## 更新履歴
|
||||
|
||||
> **2026.04.01:** [v2.0.5](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.5) — Cow CLI、Skill Hubオープンソース化、ブラウザツール、WeCom Botスキャン作成など。
|
||||
> **2026.04.14:** [v2.0.6](https://github.com/zhayujie/CowAgent/releases/tag/2.0.6) — ナレッジベース、Deep Dream 記憶蒸留、スマートコンテキスト圧縮、Web コンソールアップグレード。
|
||||
|
||||
> **2026.02.27:** [v2.0.2](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.2) — Webコンソールの全面刷新(ストリーミングチャット、モデル/Skill/メモリ/チャネル/スケジューラ/ログ管理)、マルチチャネル同時実行、セッション永続化、Gemini 3.1 Pro / Claude 4.6 Sonnet / Qwen3.5 Plusなど新モデル追加。
|
||||
> **2026.04.01:** [v2.0.5](https://github.com/zhayujie/CowAgent/releases/tag/2.0.5) — Cow CLI、Skill Hubオープンソース化、ブラウザツール、WeCom Botスキャン作成など。
|
||||
|
||||
> **2026.02.13:** [v2.0.1](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.1) — 組み込みWeb検索ツール、スマートコンテキストトリミング、ランタイム情報の動的更新、Windows互換性、スケジューラのメモリ喪失やFeishu接続問題などの修正。
|
||||
> **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など新モデル追加。
|
||||
|
||||
> **2026.02.03:** [v2.0.0](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0) — マルチステップタスク計画、長期記憶、組み込みツール、Skillフレームワーク、新モデル、チャネル最適化を備えたAIスーパーアシスタントへの全面アップグレード。
|
||||
> **2026.02.13:** [v2.0.1](https://github.com/zhayujie/CowAgent/releases/tag/2.0.1) — 組み込みWeb検索ツール、スマートコンテキストトリミング、ランタイム情報の動的更新、Windows互換性、スケジューラのメモリ喪失やFeishu接続問題などの修正。
|
||||
|
||||
> **2025.05.23:** [v1.7.6](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.6) — Webチャネル最適化、AgentMeshマルチエージェントプラグイン、Baidu TTS、claude-4-sonnet/opus対応。
|
||||
> **2026.02.03:** [v2.0.0](https://github.com/zhayujie/CowAgent/releases/tag/2.0.0) — マルチステップタスク計画、長期記憶、組み込みツール、Skillフレームワーク、新モデル、チャネル最適化を備えたAIスーパーアシスタントへの全面アップグレード。
|
||||
|
||||
> **2025.04.11:** [v1.7.5](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.5) — wechatferryプロトコル、DeepSeekモデル、Tencent Cloud音声、ModelScope・Gitee-AI対応。
|
||||
> **2025.05.23:** [v1.7.6](https://github.com/zhayujie/CowAgent/releases/tag/1.7.6) — Webチャネル最適化、AgentMeshマルチエージェントプラグイン、Baidu TTS、claude-4-sonnet/opus対応。
|
||||
|
||||
> **2024.12.13:** [v1.7.4](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.4) — Gemini 2.0モデル、Webチャネル、メモリリーク修正。
|
||||
> **2025.04.11:** [v1.7.5](https://github.com/zhayujie/CowAgent/releases/tag/1.7.5) — wechatferryプロトコル、DeepSeekモデル、Tencent Cloud音声、ModelScope・Gitee-AI対応。
|
||||
|
||||
> **2024.12.13:** [v1.7.4](https://github.com/zhayujie/CowAgent/releases/tag/1.7.4) — Gemini 2.0モデル、Webチャネル、メモリリーク修正。
|
||||
|
||||
全更新履歴: [リリースノート](https://docs.cowagent.ai/en/releases/overview)
|
||||
|
||||
@@ -83,8 +86,8 @@ irm https://cdn.link-ai.tech/code/cow/run.ps1 | iex
|
||||
**1. プロジェクトのクローン**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
git clone https://github.com/zhayujie/CowAgent
|
||||
cd CowAgent/
|
||||
```
|
||||
|
||||
**2. 依存関係のインストール**
|
||||
@@ -161,15 +164,15 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
|
||||
| プロバイダー | 推奨モデル |
|
||||
| --- | --- |
|
||||
| DeepSeek | `deepseek-v4-flash` |
|
||||
| MiniMax | `MiniMax-M2.7` |
|
||||
| GLM | `glm-5-turbo` |
|
||||
| Kimi | `kimi-k2.5` |
|
||||
| Doubao | `doubao-seed-2-0-code-preview-260215` |
|
||||
| Qwen | `qwen3.6-plus` |
|
||||
| Claude | `claude-sonnet-4-6` |
|
||||
| Gemini | `gemini-3.1-pro-preview` |
|
||||
| OpenAI | `gpt-5.4` |
|
||||
| DeepSeek | `deepseek-chat` |
|
||||
| GLM | `glm-5.1` |
|
||||
| Qwen | `qwen3.6-plus` |
|
||||
| Doubao | `doubao-seed-2-0-code-preview-260215` |
|
||||
| Kimi | `kimi-k2.6` |
|
||||
|
||||
各モデルの詳細設定については、[モデルドキュメント](https://docs.cowagent.ai/en/models/index)を参照してください。
|
||||
|
||||
@@ -232,16 +235,16 @@ Coding Planは各プロバイダーが提供する月額サブスクリプショ
|
||||
|
||||
## 🔎 よくある質問
|
||||
|
||||
FAQ: <https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs>
|
||||
FAQ: <https://github.com/zhayujie/CowAgent/wiki/FAQs>
|
||||
|
||||
## 🛠️ コントリビューション
|
||||
|
||||
新しいチャネルの追加を歓迎します。[Feishuチャネル](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/channel/feishu/feishu_channel.py)を参考にしてください。また、新しいSkillのコントリビューションも歓迎します。[Skill作成ドキュメント](https://docs.cowagent.ai/ja/skills/create)を参照するか、[Skill Hub](https://skills.cowagent.ai/submit)に提出してください。
|
||||
新しいチャネルの追加を歓迎します。[Feishuチャネル](https://github.com/zhayujie/CowAgent/blob/master/channel/feishu/feishu_channel.py)を参考にしてください。また、新しいSkillのコントリビューションも歓迎します。[Skill作成ドキュメント](https://docs.cowagent.ai/ja/skills/create)を参照するか、[Skill Hub](https://skills.cowagent.ai/submit)に提出してください。
|
||||
|
||||
## ✉ お問い合わせ
|
||||
|
||||
PRやIssueの提出を歓迎します。🌟 Starでプロジェクトをサポートしてください。ご質問がある場合は、[FAQリスト](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs)を確認するか、[Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues)を検索してください。
|
||||
PRやIssueの提出を歓迎します。🌟 Starでプロジェクトをサポートしてください。ご質問がある場合は、[FAQリスト](https://github.com/zhayujie/CowAgent/wiki/FAQs)を確認するか、[Issues](https://github.com/zhayujie/CowAgent/issues)を検索してください。
|
||||
|
||||
## 🌟 コントリビューター
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,69 +1,107 @@
|
||||
---
|
||||
title: Feishu (Lark)
|
||||
description: CowAgent を Feishu アプリケーションに統合する
|
||||
description: 企業向けカスタムアプリで CowAgent を Feishu に接続
|
||||
---
|
||||
|
||||
企業向けカスタムアプリを作成して、CowAgent を Feishu に統合します。管理者権限を持つ Feishu 企業ユーザーである必要があります。
|
||||
> 飛書(Feishu)の企業向けカスタムアプリを通じて CowAgent を接続。1 対 1 チャット、グループチャット(@メンション)に対応。WebSocket 長接続を使用するため公開 IP 不要、ストリーミングのタイプライター応答や音声メッセージにも対応します。
|
||||
|
||||
## 1. 企業カスタムアプリの作成
|
||||
<Note>
|
||||
接続には管理者権限を持つ Feishu 企業ユーザーが必要です。
|
||||
</Note>
|
||||
|
||||
### 1.1 アプリの作成
|
||||
## 1. 接続方法
|
||||
|
||||
[Feishu 開発者プラットフォーム](https://open.feishu.cn/app/)にアクセスし、**企業カスタムアプリを作成**をクリックして、必要な情報を入力し**作成**をクリックします:
|
||||
### 方式 1: ワンクリック作成(推奨)
|
||||
|
||||
事前に Feishu 開発者プラットフォームでアプリを作成する必要はありません。Cow を起動後、Web コンソール(既定 `http://127.0.0.1:9899/`)を開き、**チャネル** メニュー → **チャネルを追加** → **Feishu** を選択し、**QR スキャン** タブで **ワンクリックで Feishu アプリを作成** をクリック。**Feishu アプリ** で QR コードをスキャンするとアプリ作成と接続が自動完了します。
|
||||
|
||||
<Note>
|
||||
作成されたアプリには必要な権限(メッセージ送受信、カード読み書き、グループイベントなど)とイベント購読がすべて事前設定されています。現在は Feishu 中国版のみ対応で、Lark 国際版は未対応です。
|
||||
</Note>
|
||||
|
||||
CLI から `feishu_app_id` 未設定で起動した場合は、ターミナルにも QR コードが表示されます。
|
||||
|
||||
### 方式 2: 手動作成
|
||||
|
||||
Feishu 開発者プラットフォームで自分でアプリを作成し、Web コンソールまたは設定ファイルから接続します。
|
||||
|
||||
**ステップ 1: アプリ作成**
|
||||
|
||||
1. [Feishu 開発者プラットフォーム](https://open.feishu.cn/app/) にアクセスし、**企業カスタムアプリを作成** をクリック:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-create-app.jpg" width="500"/>
|
||||
|
||||
### 1.2 Bot 機能の追加
|
||||
|
||||
**アプリ機能の追加**で、アプリに **Bot** 機能を追加します:
|
||||
2. **アプリ機能の追加** で **Bot** 機能を追加:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-add-bot.jpg" width="800"/>
|
||||
|
||||
### 1.3 アプリ権限の設定
|
||||
|
||||
**権限管理**をクリックし、**権限設定**の下の入力欄に以下の権限文字列を貼り付け、フィルタされたすべての権限を選択し、**一括有効化**をクリックして確認します:
|
||||
3. **権限管理** で以下の権限を貼り付け、全選択して **一括有効化**:
|
||||
|
||||
```
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource,cardkit:card:write
|
||||
```
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/feishu-hosting-add-auth2.png" width="800"/>
|
||||
|
||||
## 2. プロジェクト設定
|
||||
|
||||
1. **認証情報と基本情報**から `App ID` と `App Secret` を取得します:
|
||||
4. **認証情報と基本情報** から `App ID` と `App Secret` を取得:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-appid-secret.jpg" width="800"/>
|
||||
|
||||
2. プロジェクトルートの `config.json` に以下の設定を追加します:
|
||||
**ステップ 2: CowAgent に接続**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_bot_name": "YOUR_BOT_NAME"
|
||||
}
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Web コンソール">
|
||||
Web コンソールから **チャネル** → **チャネルを追加** → **Feishu** → **手動入力** タブに切り替え、App ID と App Secret を入力して接続。
|
||||
</Tab>
|
||||
<Tab title="設定ファイル">
|
||||
`config.json` に以下を追加して起動:
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `feishu_app_id` | Feishu Bot の App ID |
|
||||
| `feishu_app_secret` | Feishu Bot の App Secret |
|
||||
| `feishu_bot_name` | Bot 名(アプリ作成時に設定)、グループチャットで使用する際に必要 |
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_stream_reply": true
|
||||
}
|
||||
```
|
||||
|
||||
設定完了後、プロジェクトを起動します。
|
||||
| パラメータ | 説明 | デフォルト |
|
||||
| --- | --- | --- |
|
||||
| `feishu_app_id` | Feishu アプリの App ID | - |
|
||||
| `feishu_app_secret` | Feishu アプリの App Secret | - |
|
||||
| `feishu_stream_reply` | ストリーミングタイプライター応答を有効化 | `true` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 3. イベントサブスクリプションの設定
|
||||
**ステップ 3: アプリの公開**
|
||||
|
||||
1. プロジェクトが正常に動作した後、Feishu 開発者プラットフォームに移動し、**イベントとコールバック**をクリックし、**ロングコネクション**モードを選択して保存をクリックします:
|
||||
1. Cow 起動後、Feishu 開発者プラットフォームの **イベントとコールバック** で **ロングコネクション** モードを選択して保存:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311731183.png" width="600"/>
|
||||
|
||||
2. 下の**イベントを追加**をクリックし、「メッセージ受信」を検索して「**メッセージ受信 v2.0**」を選択し、確認します。
|
||||
2. **イベントを追加** で「メッセージ受信」を検索し、**メッセージ受信 v2.0** を選択。
|
||||
|
||||
3. **バージョン管理とリリース**をクリックし、新しいバージョンを作成して**本番リリース**を申請します。Feishu クライアントで承認メッセージを確認し、承認します:
|
||||
3. **バージョン管理とリリース** で新バージョンを作成し **本番リリース** を申請、Feishu クライアントで承認:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311807356.png" width="600"/>
|
||||
|
||||
完了後、Feishu で Bot 名を検索してチャットを開始できます。
|
||||
## 2. 機能一覧
|
||||
|
||||
| 機能 | 対応状況 |
|
||||
| --- | --- |
|
||||
| 1 対 1 チャット | ✅ |
|
||||
| グループチャット(@Bot) | ✅ |
|
||||
| テキストメッセージ | ✅ 送受信 |
|
||||
| 画像メッセージ | ✅ 送受信 |
|
||||
| 音声メッセージ | ✅ 送受信 |
|
||||
| ストリーミング応答 | ✅(Feishu cardkit ストリーミングカードベース) |
|
||||
|
||||
<Note>
|
||||
ストリーミング応答には `cardkit:card:write` 権限(ワンクリック作成では自動付与)と Feishu クライアント 7.20 以上が必要です。古いクライアントではアップグレード案内が表示され、権限/バージョン未充足時は通常テキスト応答に自動フォールバックします。
|
||||
</Note>
|
||||
|
||||
## 3. 使い方
|
||||
|
||||
接続完了後、Feishu で Bot 名を検索してチャットを開始できます。
|
||||
|
||||
グループで使う場合は Bot をグループに追加し、@メンションでメッセージを送ってください。
|
||||
|
||||
@@ -38,6 +38,16 @@ Web コンソールは CowAgent のデフォルトチャネルです。起動後
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227180120.png" />
|
||||
|
||||
#### マルチセッション管理
|
||||
|
||||
チャット画面はマルチセッション管理に対応しています。すべてのセッション記録は SQLite データベースに永続的に保存されます:
|
||||
|
||||
- **セッション一覧**:左側の履歴アイコンをクリックしてセッション一覧パネルを展開/折りたたみでき、スクロールですべての履歴セッションを読み込めます
|
||||
- **AI によるタイトル生成**:新しいセッションの最初のやり取りが完了すると、自動的にモデルを呼び出して短い要約タイトルを生成します
|
||||
- **新規セッション**:セッション一覧上部の「新しい会話」ボタン、または入力エリアの `+` ボタンをクリックして新しいセッションを作成します
|
||||
- **セッション削除**:セッション項目の削除ボタンをクリックし、確認後にそのセッションとすべてのメッセージを完全に削除します
|
||||
- **コンテキストクリア**:入力エリアのクリアボタンをクリックすると、現在のセッションに区切り線が挿入されます。区切り線より上のメッセージは表示されたままですが、モデルのコンテキストには含まれなくなります
|
||||
|
||||
### モデル管理
|
||||
|
||||
設定ファイルを手動で編集せずに、オンラインでモデル設定を管理できます:
|
||||
|
||||
@@ -44,17 +44,18 @@ description: ステータスの確認、設定管理、コンテキスト制御
|
||||
**設定項目を変更:**
|
||||
|
||||
```text
|
||||
/config model deepseek-chat
|
||||
/config model deepseek-v4-flash
|
||||
```
|
||||
|
||||
**変更可能な設定項目:**
|
||||
|
||||
| 項目 | 説明 | 例 |
|
||||
| --- | --- | --- |
|
||||
| `model` | AI モデル名 | `deepseek-chat` |
|
||||
| `model` | AI モデル名 | `deepseek-v4-flash` |
|
||||
| `agent_max_context_tokens` | 最大コンテキストトークン数 | `40000` |
|
||||
| `agent_max_context_turns` | 最大コンテキスト記憶ターン数 | `30` |
|
||||
| `agent_max_steps` | タスクごとの最大判断ステップ数 | `15` |
|
||||
| `enable_thinking` | ディープシンキングモードの有効化 | `true` / `false` |
|
||||
|
||||
<Note>
|
||||
`model` を変更すると、システムが対応するモデル API を自動的にマッチングします。設定は `config.json` に永続的に保存されます。
|
||||
|
||||
@@ -40,6 +40,10 @@ Service:
|
||||
Skills:
|
||||
skill Manage skills (list / search / install / uninstall ...)
|
||||
|
||||
Memory & Knowledge:
|
||||
memory Memory distillation (dream)
|
||||
knowledge View knowledge base stats and structure
|
||||
|
||||
Others:
|
||||
help Show this help message
|
||||
version Show version
|
||||
@@ -55,6 +59,10 @@ Web コンソールや接続されたチャネルの会話で `/` を入力す
|
||||
| `/status` | サービスの状態と設定を表示 |
|
||||
| `/config` | 実行時設定の表示・変更 |
|
||||
| `/skill` | スキル管理(インストール、アンインストール、有効化、無効化など) |
|
||||
| `/memory dream [N]` | 記憶蒸留を手動トリガー(デフォルト 3 日、最大 30) |
|
||||
| `/knowledge` | ナレッジベースの統計情報を表示 |
|
||||
| `/knowledge list` | ナレッジベースのディレクトリ構造を表示 |
|
||||
| `/knowledge on\|off` | ナレッジベースの有効化・無効化 |
|
||||
| `/context` | 現在のセッションのコンテキスト情報を表示 |
|
||||
| `/context clear` | 現在のセッションのコンテキストをクリア |
|
||||
| `/logs` | 最近のログを表示 |
|
||||
@@ -74,6 +82,8 @@ Web コンソールや接続されたチャネルの会話で `/` を入力す
|
||||
| logs | ✓ | ✓ |
|
||||
| config | ✗ | ✓ |
|
||||
| context | — | ✓ |
|
||||
| memory(サブコマンド) | ✗ | ✓ |
|
||||
| knowledge(サブコマンド) | ✓ | ✓ |
|
||||
| skill(サブコマンド) | ✓ | ✓ |
|
||||
| start / stop / restart | ✓ | ✗ |
|
||||
| update | ✓ | ✗ |
|
||||
|
||||
63
docs/ja/cli/memory-knowledge.mdx
Normal file
63
docs/ja/cli/memory-knowledge.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 記憶とナレッジベース
|
||||
description: 記憶蒸留とナレッジベース管理コマンド
|
||||
---
|
||||
|
||||
## memory
|
||||
|
||||
Agent の長期記憶システムを管理します。
|
||||
|
||||
### memory dream
|
||||
|
||||
記憶蒸留(Deep Dream)を手動でトリガーします — 最近の日次記憶を整理し、MEMORY.md に統合し、夢日記を生成します。
|
||||
|
||||
```text
|
||||
/memory dream [N]
|
||||
```
|
||||
|
||||
- `N`:直近 N 日間の記憶を整理(デフォルト 3 日、最大 30 日)
|
||||
- バックグラウンドで非同期に実行され、完了するとチャットで通知されます
|
||||
- Agent の初期化不要 — 最初の会話前でも使用可能
|
||||
|
||||
**例:**
|
||||
|
||||
```text
|
||||
/memory dream # 直近 3 日間を整理
|
||||
/memory dream 7 # 直近 7 日間を整理
|
||||
/memory dream 30 # 直近 30 日間を整理(全量)
|
||||
```
|
||||
|
||||
Web コンソールでは、完了通知にクリック可能なリンクが含まれ、更新された MEMORY.md と夢日記を直接確認できます。
|
||||
|
||||
<Tip>
|
||||
システムは毎日 23:55 に自動で蒸留を実行します(lookback 1 日)。手動トリガーは、初回デプロイ後の履歴整理や、即座に記憶を更新したい場合に使用します。
|
||||
</Tip>
|
||||
|
||||
## knowledge
|
||||
|
||||
パーソナルナレッジベースの表示と管理。デフォルトで統計情報を表示します。
|
||||
|
||||
```text
|
||||
/knowledge
|
||||
```
|
||||
|
||||
### knowledge list
|
||||
|
||||
ナレッジベースのディレクトリツリーを表示します。
|
||||
|
||||
```text
|
||||
/knowledge list
|
||||
```
|
||||
|
||||
### knowledge on / off
|
||||
|
||||
ナレッジベースの有効化・無効化。無効化すると、ナレッジプロンプトとファイルインデックスが注入されなくなります。
|
||||
|
||||
```text
|
||||
/knowledge on
|
||||
/knowledge off
|
||||
```
|
||||
|
||||
<Note>
|
||||
ターミナル CLI では `cow knowledge` と `cow knowledge list` が利用可能ですが、`on|off` はチャットでのみサポートされます(ランタイム効果が必要なため)。
|
||||
</Note>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user