mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
Merge pull request #2762 from zhayujie/feat-web-console-upgrade
feat(web): add password protection for web console
This commit is contained in:
@@ -198,11 +198,12 @@ 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 模式下单次任务的最大决策步数,超出后将停止继续调用工具
|
||||
}
|
||||
```
|
||||
|
||||
@@ -710,6 +711,7 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
```
|
||||
|
||||
- `web_port`: 默认为 9899,可按需更改,需要服务器防火墙和安全组放行该端口
|
||||
- `web_password`: 访问密码,留空则不启用密码保护。部署在公网环境时建议设置
|
||||
- 如本地运行,启动后请访问 `http://localhost:9899/chat` ;如服务器运行,请访问 `http://ip:9899/chat`
|
||||
> 注:请将上述 url 中的 ip 或者 port 替换为实际的值
|
||||
</details>
|
||||
|
||||
@@ -54,6 +54,41 @@
|
||||
</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">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
@@ -414,21 +449,30 @@
|
||||
</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">Max Context Tokens</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">Max Memory 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">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
|
||||
@@ -444,6 +488,35 @@
|
||||
</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">Security</h3>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_password">Password</label>
|
||||
<input id="cfg-password" type="password" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors
|
||||
cfg-key-masked"
|
||||
data-masked="1">
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-1.5" data-i18n="config_password_hint">Leave empty to disable password protection</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<span id="cfg-password-status" class="text-xs text-primary-500 opacity-0 transition-opacity duration-300"></span>
|
||||
<button id="cfg-password-save"
|
||||
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
|
||||
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick="savePasswordConfig()" data-i18n="config_save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -689,3 +689,42 @@
|
||||
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; }
|
||||
.cfg-tip::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 6px);
|
||||
transform: translateX(-50%);
|
||||
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: 50;
|
||||
}
|
||||
.dark .cfg-tip::after {
|
||||
background: #334155;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.cfg-tip:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -31,14 +31,20 @@ const I18N = {
|
||||
config_title: '配置管理', config_desc: '管理模型和 Agent 配置',
|
||||
config_model: '模型配置', config_agent: 'Agent 配置',
|
||||
config_channel: '通道配置',
|
||||
config_agent_enabled: 'Agent 模式', config_max_tokens: '最大 Token',
|
||||
config_max_turns: '最大轮次', config_max_steps: '最大步数',
|
||||
config_agent_enabled: 'Agent 模式',
|
||||
config_max_tokens: '最大上下文 Token', config_max_tokens_hint: '对话中 Agent 能输入的最大 Token 长度,超过后会智能压缩处理',
|
||||
config_max_turns: '最大记忆轮次', config_max_turns_hint: '一问一答为一轮,超过后会智能压缩处理',
|
||||
config_max_steps: '最大执行步数', config_max_steps_hint: '单次对话中 Agent 最多调用工具的次数',
|
||||
config_channel_type: '通道类型',
|
||||
config_provider: '模型厂商', config_model_name: '模型',
|
||||
config_custom_model_hint: '输入自定义模型名称',
|
||||
config_save: '保存', config_saved: '已保存',
|
||||
config_save_error: '保存失败',
|
||||
config_custom_option: '自定义...',
|
||||
config_security: '安全设置', config_password: '访问密码',
|
||||
config_password_hint: '留空则不启用密码保护',
|
||||
config_password_changed: '密码已更新,请重新登录',
|
||||
config_password_cleared: '密码已清除',
|
||||
skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 技能', skills_hub_btn: '探索技能广场',
|
||||
skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处',
|
||||
tools_section_title: '内置工具', tools_loading: '加载工具中...',
|
||||
@@ -92,14 +98,20 @@ const I18N = {
|
||||
config_title: 'Configuration', config_desc: 'Manage model and agent settings',
|
||||
config_model: 'Model Configuration', config_agent: 'Agent Configuration',
|
||||
config_channel: 'Channel Configuration',
|
||||
config_agent_enabled: 'Agent Mode', config_max_tokens: 'Max Tokens',
|
||||
config_max_turns: 'Max Turns', config_max_steps: 'Max Steps',
|
||||
config_agent_enabled: 'Agent Mode',
|
||||
config_max_tokens: 'Max Context Tokens', config_max_tokens_hint: 'Max tokens the Agent can input per conversation, auto-compressed when exceeded',
|
||||
config_max_turns: 'Max Memory Turns', config_max_turns_hint: 'One Q&A pair = one turn, auto-compressed when exceeded',
|
||||
config_max_steps: 'Max Steps', config_max_steps_hint: 'Max tool calls the Agent can make in a single conversation',
|
||||
config_channel_type: 'Channel Type',
|
||||
config_provider: 'Provider', config_model_name: 'Model',
|
||||
config_custom_model_hint: 'Enter custom model name',
|
||||
config_save: 'Save', config_saved: 'Saved',
|
||||
config_save_error: 'Save failed',
|
||||
config_custom_option: 'Custom...',
|
||||
config_security: 'Security', config_password: 'Password',
|
||||
config_password_hint: 'Leave empty to disable password protection',
|
||||
config_password_changed: 'Password updated, please re-login',
|
||||
config_password_cleared: 'Password cleared',
|
||||
skills_title: 'Skills', skills_desc: 'View, enable, or disable agent skills', skills_hub_btn: 'Skill Hub',
|
||||
skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading',
|
||||
tools_section_title: 'Built-in Tools', tools_loading: 'Loading tools...',
|
||||
@@ -151,6 +163,9 @@ function applyI18n() {
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
el.placeholder = t(el.dataset['i18nPlaceholder']);
|
||||
});
|
||||
document.querySelectorAll('[data-tip-key]').forEach(el => {
|
||||
el.setAttribute('data-tooltip', t(el.dataset.tipKey));
|
||||
});
|
||||
document.getElementById('lang-label').textContent = currentLang === 'zh' ? 'EN' : '中文';
|
||||
}
|
||||
|
||||
@@ -1572,8 +1587,35 @@ function initConfigView(data) {
|
||||
syncModelSelection(configCurrentModel);
|
||||
|
||||
document.getElementById('cfg-max-tokens').value = data.agent_max_context_tokens || 50000;
|
||||
document.getElementById('cfg-max-turns').value = data.agent_max_context_turns || 30;
|
||||
document.getElementById('cfg-max-steps').value = data.agent_max_steps || 15;
|
||||
document.getElementById('cfg-max-turns').value = data.agent_max_context_turns || 20;
|
||||
document.getElementById('cfg-max-steps').value = data.agent_max_steps || 20;
|
||||
|
||||
const pwdInput = document.getElementById('cfg-password');
|
||||
const maskedPwd = data.web_password_masked || '';
|
||||
pwdInput.value = maskedPwd;
|
||||
pwdInput.dataset.masked = maskedPwd ? '1' : '';
|
||||
pwdInput.dataset.maskedVal = maskedPwd;
|
||||
pwdInput.classList.toggle('cfg-key-masked', !!maskedPwd);
|
||||
|
||||
if (maskedPwd) {
|
||||
pwdInput.placeholder = '••••••••';
|
||||
} else {
|
||||
pwdInput.placeholder = '';
|
||||
}
|
||||
|
||||
if (!pwdInput._cfgBound) {
|
||||
pwdInput.addEventListener('focus', function() {
|
||||
if (this.dataset.masked === '1') {
|
||||
this.value = '';
|
||||
this.dataset.masked = '';
|
||||
this.classList.remove('cfg-key-masked');
|
||||
}
|
||||
});
|
||||
pwdInput.addEventListener('input', function() {
|
||||
this.dataset.masked = '';
|
||||
});
|
||||
pwdInput._cfgBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
function detectProvider(model) {
|
||||
@@ -1779,8 +1821,8 @@ function saveModelConfig() {
|
||||
function saveAgentConfig() {
|
||||
const updates = {
|
||||
agent_max_context_tokens: parseInt(document.getElementById('cfg-max-tokens').value) || 50000,
|
||||
agent_max_context_turns: parseInt(document.getElementById('cfg-max-turns').value) || 30,
|
||||
agent_max_steps: parseInt(document.getElementById('cfg-max-steps').value) || 15,
|
||||
agent_max_context_turns: parseInt(document.getElementById('cfg-max-turns').value) || 20,
|
||||
agent_max_steps: parseInt(document.getElementById('cfg-max-steps').value) || 20,
|
||||
};
|
||||
|
||||
const btn = document.getElementById('cfg-agent-save');
|
||||
@@ -1802,6 +1844,40 @@ function saveAgentConfig() {
|
||||
.finally(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function savePasswordConfig() {
|
||||
const input = document.getElementById('cfg-password');
|
||||
if (input.dataset.masked === '1') {
|
||||
showStatus('cfg-password-status', 'config_saved', false);
|
||||
return;
|
||||
}
|
||||
const newPwd = input.value.trim();
|
||||
const btn = document.getElementById('cfg-password-save');
|
||||
btn.disabled = true;
|
||||
fetch('/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ updates: { web_password: newPwd } })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
if (newPwd) {
|
||||
showStatus('cfg-password-status', 'config_password_changed', false);
|
||||
setTimeout(() => { window.location.reload(); }, 1500);
|
||||
} else {
|
||||
input.dataset.masked = '';
|
||||
input.dataset.maskedVal = '';
|
||||
input.classList.remove('cfg-key-masked');
|
||||
showStatus('cfg-password-status', 'config_password_cleared', false);
|
||||
}
|
||||
} else {
|
||||
showStatus('cfg-password-status', 'config_save_error', true);
|
||||
}
|
||||
})
|
||||
.catch(() => showStatus('cfg-password-status', 'config_save_error', true))
|
||||
.finally(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function loadConfigView() {
|
||||
fetch('/config').then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
@@ -3342,29 +3418,120 @@ function renderKnowledgeGraph(container, nodes, links) {
|
||||
container.appendChild(legendDiv);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Authentication
|
||||
// =====================================================================
|
||||
function toggleLoginPassword() {
|
||||
const input = document.getElementById('login-password');
|
||||
const icon = document.querySelector('#login-toggle-pwd i');
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
icon.classList.replace('fa-eye', 'fa-eye-slash');
|
||||
} else {
|
||||
input.type = 'password';
|
||||
icon.classList.replace('fa-eye-slash', 'fa-eye');
|
||||
}
|
||||
}
|
||||
window.toggleLoginPassword = toggleLoginPassword;
|
||||
|
||||
function showLoginScreen() {
|
||||
const overlay = document.getElementById('login-overlay');
|
||||
if (!overlay) return;
|
||||
overlay.classList.remove('hidden');
|
||||
document.getElementById('app').classList.add('hidden');
|
||||
|
||||
const subtitle = document.getElementById('login-subtitle');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
if (currentLang === 'en') {
|
||||
subtitle.textContent = 'Enter password to access the console';
|
||||
loginBtn.textContent = 'Login';
|
||||
} else {
|
||||
subtitle.textContent = '请输入密码以访问控制台';
|
||||
loginBtn.textContent = '登录';
|
||||
}
|
||||
|
||||
const form = document.getElementById('login-form');
|
||||
const pwdInput = document.getElementById('login-password');
|
||||
pwdInput.focus();
|
||||
|
||||
form.onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
const pwd = pwdInput.value;
|
||||
if (!pwd) return;
|
||||
const btn = document.getElementById('login-btn');
|
||||
const errEl = document.getElementById('login-error');
|
||||
btn.disabled = true;
|
||||
errEl.classList.add('hidden');
|
||||
|
||||
fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({password: pwd})
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') {
|
||||
overlay.classList.add('hidden');
|
||||
document.getElementById('app').classList.remove('hidden');
|
||||
initApp();
|
||||
} else {
|
||||
errEl.textContent = currentLang === 'zh' ? '密码错误' : 'Wrong password';
|
||||
errEl.classList.remove('hidden');
|
||||
pwdInput.value = '';
|
||||
pwdInput.focus();
|
||||
}
|
||||
btn.disabled = false;
|
||||
}).catch(() => {
|
||||
errEl.textContent = currentLang === 'zh' ? '网络错误,请重试' : 'Network error, please retry';
|
||||
errEl.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
});
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Intercept 401 responses globally to show login screen on session expiry
|
||||
const _originalFetch = window.fetch;
|
||||
window.fetch = function(...args) {
|
||||
return _originalFetch.apply(this, args).then(response => {
|
||||
if (response.status === 401) {
|
||||
const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '');
|
||||
if (!url.startsWith('/auth/')) {
|
||||
showLoginScreen();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
function initApp() {
|
||||
fetch('/api/knowledge/list').then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') _knowledgeTreeData = data.tree || [];
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/api/version').then(r => r.json()).then(data => {
|
||||
APP_VERSION = `v${data.version}`;
|
||||
document.getElementById('sidebar-version').textContent = `CowAgent ${APP_VERSION}`;
|
||||
}).catch(() => {
|
||||
document.getElementById('sidebar-version').textContent = 'CowAgent';
|
||||
});
|
||||
chatInput.focus();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Initialization
|
||||
// =====================================================================
|
||||
applyTheme();
|
||||
applyI18n();
|
||||
|
||||
// Pre-fetch knowledge tree for chat link resolution
|
||||
fetch('/api/knowledge/list').then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') _knowledgeTreeData = data.tree || [];
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/api/version').then(r => r.json()).then(data => {
|
||||
APP_VERSION = `v${data.version}`;
|
||||
document.getElementById('sidebar-version').textContent = `CowAgent ${APP_VERSION}`;
|
||||
fetch('/auth/check').then(r => r.json()).then(data => {
|
||||
if (data.auth_required && !data.authenticated) {
|
||||
showLoginScreen();
|
||||
} else {
|
||||
initApp();
|
||||
}
|
||||
}).catch(() => {
|
||||
document.getElementById('sidebar-version').textContent = 'CowAgent';
|
||||
initApp();
|
||||
});
|
||||
chatInput.focus();
|
||||
|
||||
// Re-enable color transition AFTER first paint so the theme applied in <head>
|
||||
// doesn't produce an animated flash on load. The class is missing from the
|
||||
// body initially; adding it here means transitions only fire on user-triggered
|
||||
// theme toggles, not on page load.
|
||||
requestAnimationFrame(() => {
|
||||
document.body.classList.add('transition-colors', 'duration-200');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
@@ -23,6 +25,62 @@ from config import conf
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"}
|
||||
VIDEO_EXTENSIONS = {".mp4", ".webm", ".avi", ".mov", ".mkv"}
|
||||
|
||||
def _is_password_enabled():
|
||||
return bool(conf().get("web_password", ""))
|
||||
|
||||
|
||||
def _session_expire_seconds():
|
||||
return int(conf().get("web_session_expire_days", 30)) * 86400
|
||||
|
||||
|
||||
def _create_auth_token():
|
||||
"""Create a stateless signed token: ``<timestamp_hex>.<hmac_hex>``."""
|
||||
ts = format(int(time.time()), "x")
|
||||
sig = hmac.new(
|
||||
conf().get("web_password", "").encode(),
|
||||
ts.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return f"{ts}.{sig}"
|
||||
|
||||
|
||||
def _verify_auth_token(token):
|
||||
"""Verify a signed token is valid and not expired.
|
||||
|
||||
The token is derived from the password, so it survives server restarts
|
||||
and automatically invalidates when the password changes.
|
||||
"""
|
||||
if not token or "." not in token:
|
||||
return False
|
||||
ts_hex, sig = token.split(".", 1)
|
||||
try:
|
||||
ts = int(ts_hex, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
if time.time() - ts > _session_expire_seconds():
|
||||
return False
|
||||
expected = hmac.new(
|
||||
conf().get("web_password", "").encode(),
|
||||
ts_hex.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(sig, expected)
|
||||
|
||||
|
||||
def _check_auth():
|
||||
"""Return True if request is authenticated or password not enabled."""
|
||||
if not _is_password_enabled():
|
||||
return True
|
||||
return _verify_auth_token(web.cookies().get("cow_auth_token", ""))
|
||||
|
||||
|
||||
def _require_auth():
|
||||
"""Raise 401 if not authenticated. Call at the top of protected handlers."""
|
||||
if not _check_auth():
|
||||
raise web.HTTPError("401 Unauthorized",
|
||||
{"Content-Type": "application/json; charset=utf-8"},
|
||||
json.dumps({"status": "error", "message": "Unauthorized"}))
|
||||
|
||||
|
||||
def _get_upload_dir() -> str:
|
||||
from common.utils import expand_path
|
||||
@@ -440,6 +498,9 @@ class WebChannel(ChatChannel):
|
||||
|
||||
urls = (
|
||||
'/', 'RootHandler',
|
||||
'/auth/login', 'AuthLoginHandler',
|
||||
'/auth/check', 'AuthCheckHandler',
|
||||
'/auth/logout', 'AuthLogoutHandler',
|
||||
'/message', 'MessageHandler',
|
||||
'/upload', 'UploadHandler',
|
||||
'/uploads/(.*)', 'UploadsHandler',
|
||||
@@ -502,24 +563,62 @@ class WebChannel(ChatChannel):
|
||||
|
||||
class RootHandler:
|
||||
def GET(self):
|
||||
# 重定向到/chat
|
||||
raise web.seeother('/chat')
|
||||
|
||||
|
||||
class AuthCheckHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
if not _is_password_enabled():
|
||||
return json.dumps({"status": "success", "auth_required": False})
|
||||
if _check_auth():
|
||||
return json.dumps({"status": "success", "auth_required": True, "authenticated": True})
|
||||
return json.dumps({"status": "success", "auth_required": True, "authenticated": False})
|
||||
|
||||
|
||||
class AuthLoginHandler:
|
||||
def POST(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
if not _is_password_enabled():
|
||||
return json.dumps({"status": "success"})
|
||||
try:
|
||||
data = json.loads(web.data())
|
||||
except Exception:
|
||||
return json.dumps({"status": "error", "message": "Invalid request"})
|
||||
password = data.get("password", "")
|
||||
expected = conf().get("web_password", "")
|
||||
if not hmac.compare_digest(password, expected):
|
||||
logger.warning("[WebChannel] Invalid login attempt")
|
||||
return json.dumps({"status": "error", "message": "Wrong password"})
|
||||
token = _create_auth_token()
|
||||
web.setcookie("cow_auth_token", token, expires=_session_expire_seconds(),
|
||||
path="/", httponly=True, samesite="Lax")
|
||||
return json.dumps({"status": "success"})
|
||||
|
||||
|
||||
class AuthLogoutHandler:
|
||||
def POST(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
web.setcookie("cow_auth_token", "", expires=-1, path="/")
|
||||
return json.dumps({"status": "success"})
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
def POST(self):
|
||||
_require_auth()
|
||||
return WebChannel().post_message()
|
||||
|
||||
|
||||
class UploadHandler:
|
||||
def POST(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
return WebChannel().upload_file()
|
||||
|
||||
|
||||
class UploadsHandler:
|
||||
def GET(self, file_name):
|
||||
"""Serve uploaded files from workspace/tmp/ for preview."""
|
||||
_require_auth()
|
||||
try:
|
||||
upload_dir = _get_upload_dir()
|
||||
full_path = os.path.normpath(os.path.join(upload_dir, file_name))
|
||||
@@ -541,7 +640,7 @@ class UploadsHandler:
|
||||
|
||||
class FileServeHandler:
|
||||
def GET(self):
|
||||
"""Serve a local file by absolute path (for agent send tool)."""
|
||||
_require_auth()
|
||||
try:
|
||||
params = web.input(path="")
|
||||
file_path = params.path
|
||||
@@ -567,11 +666,13 @@ class FileServeHandler:
|
||||
|
||||
class PollHandler:
|
||||
def POST(self):
|
||||
_require_auth()
|
||||
return WebChannel().poll_response()
|
||||
|
||||
|
||||
class StreamHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
params = web.input(request_id='')
|
||||
request_id = params.request_id
|
||||
if not request_id:
|
||||
@@ -695,6 +796,7 @@ class ConfigHandler:
|
||||
"zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key",
|
||||
"ark_api_key", "minimax_api_key", "linkai_api_key",
|
||||
"agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps",
|
||||
"web_password",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -705,7 +807,7 @@ class ConfigHandler:
|
||||
return value[:4] + "*" * (len(value) - 8) + value[-4:]
|
||||
|
||||
def GET(self):
|
||||
"""Return configuration info and provider/model metadata."""
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
local_config = conf()
|
||||
@@ -733,6 +835,9 @@ class ConfigHandler:
|
||||
"api_key_field": p.get("api_key_field"),
|
||||
}
|
||||
|
||||
raw_pwd = local_config.get("web_password", "")
|
||||
masked_pwd = ("*" * len(raw_pwd)) if raw_pwd else ""
|
||||
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"use_agent": use_agent,
|
||||
@@ -743,17 +848,18 @@ class ConfigHandler:
|
||||
"channel_type": local_config.get("channel_type", ""),
|
||||
"agent_max_context_tokens": local_config.get("agent_max_context_tokens", 50000),
|
||||
"agent_max_context_turns": local_config.get("agent_max_context_turns", 20),
|
||||
"agent_max_steps": local_config.get("agent_max_steps", 15),
|
||||
"agent_max_steps": local_config.get("agent_max_steps", 20),
|
||||
"api_bases": api_bases,
|
||||
"api_keys": api_keys_masked,
|
||||
"providers": providers,
|
||||
"web_password_masked": masked_pwd,
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting config: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
"""Update configuration values in memory and persist to config.json."""
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
data = json.loads(web.data())
|
||||
@@ -902,6 +1008,7 @@ class ChannelsHandler:
|
||||
return set(cls._parse_channel_list(conf().get("channel_type", "")))
|
||||
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
local_config = conf()
|
||||
@@ -939,6 +1046,7 @@ class ChannelsHandler:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
body = json.loads(web.data())
|
||||
@@ -1192,6 +1300,7 @@ class WeixinQrHandler:
|
||||
return None
|
||||
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
running_ch = self._get_running_channel()
|
||||
@@ -1224,6 +1333,7 @@ class WeixinQrHandler:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
body = json.loads(web.data())
|
||||
@@ -1311,6 +1421,7 @@ def _get_workspace_root():
|
||||
|
||||
class ToolsHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.tools.tool_manager import ToolManager
|
||||
@@ -1335,6 +1446,7 @@ class ToolsHandler:
|
||||
|
||||
class SkillsHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.skills.service import SkillService
|
||||
@@ -1349,6 +1461,7 @@ class SkillsHandler:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.skills.service import SkillService
|
||||
@@ -1375,6 +1488,7 @@ class SkillsHandler:
|
||||
|
||||
class MemoryHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.memory.service import MemoryService
|
||||
@@ -1390,6 +1504,7 @@ class MemoryHandler:
|
||||
|
||||
class MemoryContentHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.memory.service import MemoryService
|
||||
@@ -1411,6 +1526,7 @@ class MemoryContentHandler:
|
||||
|
||||
class SchedulerHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.tools.scheduler.task_store import TaskStore
|
||||
@@ -1426,14 +1542,7 @@ class SchedulerHandler:
|
||||
|
||||
class HistoryHandler:
|
||||
def GET(self):
|
||||
"""
|
||||
Return paginated conversation history for a session.
|
||||
|
||||
Query params:
|
||||
session_id (required)
|
||||
page int, default 1 (1 = most recent messages)
|
||||
page_size int, default 20
|
||||
"""
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
web.header('Access-Control-Allow-Origin', '*')
|
||||
try:
|
||||
@@ -1457,7 +1566,7 @@ class HistoryHandler:
|
||||
|
||||
class LogsHandler:
|
||||
def GET(self):
|
||||
"""Stream the last N lines of run.log as SSE, then tail new lines."""
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
web.header('Cache-Control', 'no-cache')
|
||||
web.header('X-Accel-Buffering', 'no')
|
||||
@@ -1545,6 +1654,7 @@ class AssetsHandler:
|
||||
|
||||
class KnowledgeListHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.knowledge.service import KnowledgeService
|
||||
@@ -1558,6 +1668,7 @@ class KnowledgeListHandler:
|
||||
|
||||
class KnowledgeReadHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.knowledge.service import KnowledgeService
|
||||
@@ -1574,6 +1685,7 @@ class KnowledgeReadHandler:
|
||||
|
||||
class KnowledgeGraphHandler:
|
||||
def GET(self):
|
||||
_require_auth()
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.knowledge.service import KnowledgeService
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
"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,
|
||||
"knowledge": true
|
||||
}
|
||||
|
||||
@@ -195,11 +195,13 @@ available_setting = {
|
||||
"Minimax_group_id": "",
|
||||
"Minimax_base_url": "",
|
||||
"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模式下单次运行最大决策步数
|
||||
"knowledge": True, # 是否开启知识库功能
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,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
|
||||
|
||||
@@ -30,6 +30,25 @@ Web 控制台是 CowAgent 的默认通道,启动后会自动运行,通过浏
|
||||
请确保服务器防火墙和安全组已放行对应端口。
|
||||
</Note>
|
||||
|
||||
## 密码保护
|
||||
|
||||
Web 控制台默认无需密码即可访问。如果部署在公网环境,建议配置访问密码:
|
||||
|
||||
```json
|
||||
{
|
||||
"web_password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| `web_password` | 访问密码,留空表示不启用密码保护 | `""` |
|
||||
| `web_session_expire_days` | 登录会话有效天数 | `30` |
|
||||
|
||||
配置密码后,访问控制台时需先输入密码完成登录。登录状态默认保持 30 天,期间重启服务也无需重新登录。修改密码后,所有已登录的会话将自动失效。
|
||||
|
||||
密码也支持在控制台的「配置」页面中在线修改。
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 对话界面
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[<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> ·
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
- ✅ **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.
|
||||
- ✅ **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.
|
||||
|
||||
@@ -42,6 +42,10 @@ Knowledge writing is an autonomous Agent behavior, triggered in these scenarios:
|
||||
|
||||
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:
|
||||
@@ -55,9 +59,17 @@ The Agent can retrieve knowledge during conversation through:
|
||||
The web console provides a dedicated "Knowledge" module with:
|
||||
|
||||
- **Document browsing** — Tree-style directory structure, searchable and collapsible, click to view content
|
||||
- **Knowledge graph** — D3.js force-directed graph visualizing relationships between knowledge pages
|
||||
- **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:
|
||||
|
||||
@@ -77,7 +77,7 @@ 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` |
|
||||
| `knowledge` | 是否启用个人知识库 | `true` |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[<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> ·
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
- ✅ **自律的タスク計画**: 複雑なタスクを理解し、自律的に実行計画を立て、目標達成までツールを呼び出しながら継続的に思考します。
|
||||
- ✅ **長期記憶**: 会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索やベクトル検索に対応しています。
|
||||
- ✅ **パーソナルナレッジベース**: 構造化された知識を自動整理し、相互参照によるナレッジグラフを構築。Web での可視化ブラウジングと対話による管理をサポートします。
|
||||
- ✅ **Skillシステム**: Skillの作成・実行エンジンを実装。[Skill Hub](https://skills.cowagent.ai)、GitHubなどからSkillをインストールでき、会話を通じたカスタムSkill作成もサポートしています。
|
||||
- ✅ **ツールシステム**: ファイル読み書き、ターミナル実行、ブラウザ操作、スケジュールタスク、メッセージ送信などの組み込みツールを提供。Agentが自律的に呼び出して複雑なタスクを完了します。
|
||||
- ✅ **CLIシステム**: ターミナルコマンドとチャットコマンドを提供し、プロセス管理、Skillインストール、設定変更などの操作をサポートします。
|
||||
|
||||
@@ -42,6 +42,10 @@ description: CowAgent のパーソナルナレッジベース — 構造化さ
|
||||
|
||||
各ナレッジページには関連ページへの相互参照リンクが含まれ、ナレッジグラフを段階的に構築します。
|
||||
|
||||
<Frame>
|
||||
<img src="https://gist.github.com/user-attachments/assets/3ce92f78-1863-4820-8fa8-660c0f2b7f09" alt="会話によるナレッジの取り込み" />
|
||||
</Frame>
|
||||
|
||||
## ナレッジ検索
|
||||
|
||||
Agent は会話中に以下の方法でナレッジを検索できます:
|
||||
@@ -55,9 +59,17 @@ Agent は会話中に以下の方法でナレッジを検索できます:
|
||||
Web コンソールには専用の「ナレッジ」モジュールがあり、以下をサポートします:
|
||||
|
||||
- **ドキュメント閲覧** — ツリー形式のディレクトリ構造、検索・折りたたみ可能、クリックでコンテンツ表示
|
||||
- **ナレッジグラフ** — D3.js フォースダイレクテッドグラフによるナレッジ間の関係を可視化
|
||||
- **ナレッジグラフ** — インタラクティブなグラフによるナレッジ間の関係を可視化
|
||||
- **チャット連携** — Agent の返信で参照されたナレッジドキュメントリンクはクリックで直接ナビゲーション
|
||||
|
||||
<Frame>
|
||||
<img src="https://gist.github.com/user-attachments/assets/b7b9d6be-0ac1-4c65-803b-2c6b36bd59a7" alt="ナレッジドキュメント閲覧" />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="https://gist.github.com/user-attachments/assets/44ae68ca-96cc-40b9-ab33-cdbec34c2379" alt="ナレッジグラフの可視化" />
|
||||
</Frame>
|
||||
|
||||
## CLI コマンド
|
||||
|
||||
`/knowledge` コマンドでナレッジベースを管理:
|
||||
|
||||
Reference in New Issue
Block a user