mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-20 21:57:14 +08:00
feat: 优化agent插件及webUI对话页面
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,6 +15,8 @@ plugins.json
|
|||||||
itchat.pkl
|
itchat.pkl
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
|
workspace
|
||||||
|
config.yaml
|
||||||
user_datas.pkl
|
user_datas.pkl
|
||||||
chatgpt_tool_hub/
|
chatgpt_tool_hub/
|
||||||
plugins/**/
|
plugins/**/
|
||||||
@@ -31,4 +33,5 @@ plugins/banwords/lib/__pycache__
|
|||||||
!plugins/role
|
!plugins/role
|
||||||
!plugins/keyword
|
!plugins/keyword
|
||||||
!plugins/linkai
|
!plugins/linkai
|
||||||
|
!plugins/agent
|
||||||
client_config.json
|
client_config.json
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>AI Assistant</title>
|
<title>AI Assistant</title>
|
||||||
<link rel="icon" href="assets/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="assets/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/python.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/python.min.js"></script>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/java.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/java.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/go.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/go.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/cpp.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/cpp.min.js"></script>
|
||||||
|
<script src="assets/axios.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #10a37f;
|
--primary-color: #10a37f;
|
||||||
@@ -200,7 +201,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
padding: 0 15px;
|
padding: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-container .message-container {
|
.user-container .message-container {
|
||||||
@@ -267,68 +268,139 @@
|
|||||||
.message {
|
.message {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: 0; /* 移除顶部外边距 */
|
margin-top: 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message p {
|
.message p {
|
||||||
margin-top: 0.5em;
|
margin: 0.8em 0;
|
||||||
margin-bottom: 0.5em;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message p:empty {
|
.message p:first-child {
|
||||||
margin: 0;
|
margin-top: 0;
|
||||||
line-height: 0.7em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message p:empty::before {
|
.message p:last-child {
|
||||||
content: "";
|
margin-bottom: 0;
|
||||||
display: inline-block;
|
}
|
||||||
height: 0.7em;
|
|
||||||
|
.message h1, .message h2, .message h3, .message h4, .message h5, .message h6 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message h3 {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message h4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message ul, .message ol {
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message li {
|
||||||
|
margin: 0.3em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message li > p {
|
||||||
|
margin: 0.3em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message pre {
|
.message pre {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message code {
|
.message code {
|
||||||
font-family: monospace;
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message pre code {
|
.message pre code {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message blockquote {
|
.message blockquote {
|
||||||
border-left: 4px solid #ddd;
|
border-left: 4px solid #ddd;
|
||||||
padding-left: 10px;
|
padding: 0 0 0 1em;
|
||||||
|
margin: 1em 0;
|
||||||
color: #777;
|
color: #777;
|
||||||
margin: 10px 0;
|
}
|
||||||
|
|
||||||
|
.message blockquote > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message blockquote > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message table {
|
.message table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 10px 0;
|
margin: 1em 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message th, .message td {
|
.message th, .message td {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
padding: 8px;
|
padding: 8px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message th {
|
.message th {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message hr {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border: none;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
@@ -487,7 +559,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-container {
|
.message-container {
|
||||||
padding: 10px;
|
padding: 10px 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-container {
|
.bot-container {
|
||||||
@@ -621,28 +693,10 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 基础消息样式 */
|
/* 确保代码块内的文本不会溢出 */
|
||||||
.message {
|
.hljs {
|
||||||
border-radius: 10px;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-break: break-all;
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 用户消息样式 */
|
|
||||||
.user-container .message {
|
|
||||||
background-color: var(--bot-msg-bg);
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 机器人消息样式 */
|
|
||||||
.bot-container .message {
|
|
||||||
background-color: var(--bot-msg-bg);
|
|
||||||
border-radius: 10px 10px 10px 0;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 0px 16px 12px 16px;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -728,6 +782,19 @@
|
|||||||
let userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
let userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
||||||
let currentSessionId = 'default_session'; // 使用固定会话ID
|
let currentSessionId = 'default_session'; // 使用固定会话ID
|
||||||
|
|
||||||
|
// 添加一个变量来跟踪输入法状态
|
||||||
|
let isComposing = false;
|
||||||
|
|
||||||
|
// 监听输入法组合状态开始
|
||||||
|
input.addEventListener('compositionstart', function() {
|
||||||
|
isComposing = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听输入法组合状态结束
|
||||||
|
input.addEventListener('compositionend', function() {
|
||||||
|
isComposing = false;
|
||||||
|
});
|
||||||
|
|
||||||
// 自动调整文本区域高度
|
// 自动调整文本区域高度
|
||||||
input.addEventListener('input', function() {
|
input.addEventListener('input', function() {
|
||||||
this.style.height = 'auto';
|
this.style.height = 'auto';
|
||||||
@@ -780,15 +847,19 @@
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
// Enter 键发送消息
|
// Enter 键发送消息,但只在不是输入法组合状态时
|
||||||
else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey) {
|
else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !isComposing) {
|
||||||
sendMessage();
|
sendMessage();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 在发送消息函数前添加调试代码
|
||||||
|
console.log('Axios loaded:', typeof axios !== 'undefined');
|
||||||
|
|
||||||
// 发送消息函数
|
// 发送消息函数
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
|
console.log('Send message function called');
|
||||||
const userMessage = input.value.trim();
|
const userMessage = input.value.trim();
|
||||||
if (userMessage) {
|
if (userMessage) {
|
||||||
// 隐藏欢迎屏幕
|
// 隐藏欢迎屏幕
|
||||||
@@ -811,33 +882,26 @@
|
|||||||
sendButton.disabled = true;
|
sendButton.disabled = true;
|
||||||
|
|
||||||
// 发送到服务器并等待响应
|
// 发送到服务器并等待响应
|
||||||
fetch('/message', {
|
axios({
|
||||||
method: 'POST',
|
method: 'post',
|
||||||
headers: {
|
url: '/message',
|
||||||
'Content-Type': 'application/json'
|
data: {
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
message: userMessage,
|
message: userMessage,
|
||||||
timestamp: timestamp.toISOString(),
|
timestamp: timestamp.toISOString(),
|
||||||
session_id: currentSessionId
|
session_id: currentSessionId
|
||||||
})
|
},
|
||||||
|
timeout: 120000 // 120秒超时
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to send message');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
// 移除加载消息
|
// 移除加载消息
|
||||||
if (loadingContainer.parentNode) {
|
if (loadingContainer.parentNode) {
|
||||||
messagesDiv.removeChild(loadingContainer);
|
messagesDiv.removeChild(loadingContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加AI回复
|
// 添加AI回复
|
||||||
if (data.reply) {
|
if (response.data.reply) {
|
||||||
addBotMessage(data.reply, new Date());
|
addBotMessage(response.data.reply, new Date());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -847,7 +911,11 @@
|
|||||||
messagesDiv.removeChild(loadingContainer);
|
messagesDiv.removeChild(loadingContainer);
|
||||||
}
|
}
|
||||||
// 显示错误消息
|
// 显示错误消息
|
||||||
addBotMessage("抱歉,发生了错误,请稍后再试。", new Date());
|
if (error.code === 'ECONNABORTED') {
|
||||||
|
addBotMessage("请求超时,请再试一次吧。", new Date());
|
||||||
|
} else {
|
||||||
|
addBotMessage("抱歉,发生了错误,请稍后再试。", new Date());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -882,35 +950,30 @@
|
|||||||
return botContainer;
|
return botContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化消息内容(处理Markdown和代码高亮)
|
// 替换 formatMessage 函数,使用 markdown-it 替代 marked
|
||||||
function formatMessage(content) {
|
function formatMessage(content) {
|
||||||
// 配置 marked 以使用 highlight.js
|
|
||||||
marked.setOptions({
|
|
||||||
highlight: function(code, language) {
|
|
||||||
if (language && hljs.getLanguage(language)) {
|
|
||||||
try {
|
|
||||||
return hljs.highlight(code, { language: language }).value;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error highlighting code:', e);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
},
|
|
||||||
breaks: true, // 启用换行符转换为 <br>
|
|
||||||
gfm: true, // 启用 GitHub 风格的 Markdown
|
|
||||||
headerIds: true, // 为标题生成ID
|
|
||||||
mangle: false, // 不转义内联HTML
|
|
||||||
sanitize: false, // 不净化输出
|
|
||||||
smartLists: true, // 使用更智能的列表行为
|
|
||||||
smartypants: false, // 不使用更智能的标点符号
|
|
||||||
xhtml: false // 不使用自闭合标签
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 marked 解析 Markdown
|
// 初始化 markdown-it 实例
|
||||||
const parsed = marked.parse(content);
|
const md = window.markdownit({
|
||||||
return parsed;
|
html: false, // 禁用 HTML 标签
|
||||||
|
xhtmlOut: false, // 使用 '/' 关闭单标签
|
||||||
|
breaks: true, // 将换行符转换为 <br>
|
||||||
|
linkify: true, // 自动将 URL 转换为链接
|
||||||
|
typographer: true, // 启用一些语言中性的替换和引号美化
|
||||||
|
highlight: function(str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs.highlight(str, { language: lang }).value;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error highlighting code:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(str).value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染 Markdown
|
||||||
|
return md.render(content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing markdown:', e);
|
console.error('Error parsing markdown:', e);
|
||||||
// 如果解析失败,至少确保换行符正确显示
|
// 如果解析失败,至少确保换行符正确显示
|
||||||
@@ -918,17 +981,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加消息后应用代码高亮
|
// 更新 applyHighlighting 函数
|
||||||
function applyHighlighting() {
|
function applyHighlighting() {
|
||||||
try {
|
try {
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll('pre code').forEach((block) => {
|
||||||
// 手动应用高亮
|
// 确保代码块有正确的类
|
||||||
const language = block.className.replace('language-', '');
|
if (!block.classList.contains('hljs')) {
|
||||||
|
block.classList.add('hljs');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取语言
|
||||||
|
let language = '';
|
||||||
|
block.classList.forEach(cls => {
|
||||||
|
if (cls.startsWith('language-')) {
|
||||||
|
language = cls.replace('language-', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用高亮
|
||||||
if (language && hljs.getLanguage(language)) {
|
if (language && hljs.getLanguage(language)) {
|
||||||
try {
|
try {
|
||||||
hljs.highlightBlock(block);
|
hljs.highlightBlock(block);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error highlighting block:', e);
|
console.error('Error highlighting specific language:', e);
|
||||||
|
hljs.highlightAuto(block);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hljs.highlightAuto(block);
|
hljs.highlightAuto(block);
|
||||||
|
|||||||
2
channel/web/static/axios.min.js
vendored
Normal file
2
channel/web/static/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -126,7 +126,7 @@ class WebChannel(ChatChannel):
|
|||||||
|
|
||||||
# 等待响应,最多等待30秒
|
# 等待响应,最多等待30秒
|
||||||
try:
|
try:
|
||||||
response = response_queue.get(timeout=30)
|
response = response_queue.get(timeout=120)
|
||||||
return json.dumps({"status": "success", "reply": response["content"]})
|
return json.dumps({"status": "success", "reply": response["content"]})
|
||||||
except Empty:
|
except Empty:
|
||||||
return json.dumps({"status": "error", "message": "Response timeout"})
|
return json.dumps({"status": "error", "message": "Response timeout"})
|
||||||
@@ -151,13 +151,27 @@ class WebChannel(ChatChannel):
|
|||||||
logger.info(f"Created static directory: {static_dir}")
|
logger.info(f"Created static directory: {static_dir}")
|
||||||
|
|
||||||
urls = (
|
urls = (
|
||||||
|
'/', 'RootHandler', # 添加根路径处理器
|
||||||
'/message', 'MessageHandler',
|
'/message', 'MessageHandler',
|
||||||
'/chat', 'ChatHandler',
|
'/chat', 'ChatHandler',
|
||||||
'/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径
|
'/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径
|
||||||
)
|
)
|
||||||
port = conf().get("web_port", 9899)
|
port = conf().get("web_port", 9899)
|
||||||
app = web.application(urls, globals(), autoreload=False)
|
app = web.application(urls, globals(), autoreload=False)
|
||||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
|
||||||
|
# 禁用web.py的默认日志输出
|
||||||
|
import io
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
|
||||||
|
# 临时重定向标准输出,捕获web.py的启动消息
|
||||||
|
with redirect_stdout(io.StringIO()):
|
||||||
|
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||||
|
|
||||||
|
|
||||||
|
class RootHandler:
|
||||||
|
def GET(self):
|
||||||
|
# 重定向到/chat
|
||||||
|
raise web.seeother('/chat')
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler:
|
class MessageHandler:
|
||||||
@@ -185,11 +199,6 @@ class AssetsHandler:
|
|||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
static_dir = os.path.join(current_dir, 'static')
|
static_dir = os.path.join(current_dir, 'static')
|
||||||
|
|
||||||
# 打印调试信息
|
|
||||||
logger.info(f"Current directory: {current_dir}")
|
|
||||||
logger.info(f"Static directory: {static_dir}")
|
|
||||||
logger.info(f"Requested file: {file_path}")
|
|
||||||
|
|
||||||
full_path = os.path.normpath(os.path.join(static_dir, file_path))
|
full_path = os.path.normpath(os.path.join(static_dir, file_path))
|
||||||
|
|
||||||
# 安全检查:确保请求的文件在static目录内
|
# 安全检查:确保请求的文件在static目录内
|
||||||
|
|||||||
77
plugins/agent/README.md
Normal file
77
plugins/agent/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# AgentMesh Plugin
|
||||||
|
|
||||||
|
这个插件集成了 AgentMesh 多智能体框架,允许用户通过简单的命令使用多智能体团队来完成各种任务。
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
AgentMesh 是一个开源的多智能体平台,提供开箱即用的 Agent 开发框架、多 Agent 间的协同策略、任务规划和自主决策能力。通过这个插件,你可以:
|
||||||
|
|
||||||
|
- 使用预配置的智能体团队处理复杂任务
|
||||||
|
- 利用多智能体协作能力解决问题
|
||||||
|
- 访问各种工具,如搜索引擎、浏览器、文件系统等
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
1. 确保已安装 AgentMesh SDK:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install agentmesh-sdk>=0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 如需使用浏览器工具,还需安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install browser-use>=0.1.40
|
||||||
|
playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
插件从项目根目录的 `config.yaml` 文件中读取配置。请确保该文件包含正确的团队配置。
|
||||||
|
|
||||||
|
配置示例:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
teams:
|
||||||
|
general_team:
|
||||||
|
description: "通用智能体团队,擅长于搜索、研究和执行各种任务"
|
||||||
|
model: "gpt-4o"
|
||||||
|
max_steps: 20
|
||||||
|
agents:
|
||||||
|
- name: "通用助手"
|
||||||
|
description: "全能的通用智能体"
|
||||||
|
system_prompt: "你是全能的通用智能体,可以帮助用户解决工作、生活、学习上的任何问题,以及使用工具解决各类复杂问题"
|
||||||
|
tools: ["google_search", "calculator", "current_time"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
使用 `$agent` 前缀触发插件,支持以下命令:
|
||||||
|
|
||||||
|
- `$agent teams` - 列出可用的团队
|
||||||
|
- `$agent use [team_name] [task]` - 使用特定团队执行任务
|
||||||
|
- `$agent [task]` - 使用默认团队执行任务
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
```
|
||||||
|
$agent teams
|
||||||
|
$agent use general_team 帮我分析多智能体技术发展趋势
|
||||||
|
$agent 帮我查看当前文件夹路径
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具支持
|
||||||
|
|
||||||
|
AgentMesh 支持多种工具,包括但不限于:
|
||||||
|
|
||||||
|
- `calculator`: 数学计算工具
|
||||||
|
- `current_time`: 获取当前时间
|
||||||
|
- `browser`: 浏览器操作工具
|
||||||
|
- `google_search`: 搜索引擎
|
||||||
|
- `file_save`: 文件保存工具
|
||||||
|
- `terminal`: 终端命令执行工具
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保 `config.yaml` 文件中包含正确的团队配置
|
||||||
|
2. 如果需要使用浏览器工具,请确保安装了相关依赖
|
||||||
3
plugins/agent/__init__.py
Normal file
3
plugins/agent/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .agent import AgentPlugin
|
||||||
|
|
||||||
|
__all__ = ["AgentPlugin"]
|
||||||
283
plugins/agent/agent.py
Normal file
283
plugins/agent/agent.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from agentmesh import AgentTeam, Agent, LLMModel
|
||||||
|
from agentmesh.models import ClaudeModel
|
||||||
|
from agentmesh.tools import ToolManager
|
||||||
|
from config import conf
|
||||||
|
|
||||||
|
import plugins
|
||||||
|
from plugins import Plugin, Event, EventContext, EventAction
|
||||||
|
from bridge.context import ContextType
|
||||||
|
from bridge.reply import Reply, ReplyType
|
||||||
|
from common.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
@plugins.register(
|
||||||
|
name="agent",
|
||||||
|
desc="Use AgentMesh framework to process tasks with multi-agent teams",
|
||||||
|
version="0.1.0",
|
||||||
|
author="Saboteur7",
|
||||||
|
desire_priority=1,
|
||||||
|
)
|
||||||
|
class AgentPlugin(Plugin):
|
||||||
|
"""Plugin for integrating AgentMesh framework."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
|
||||||
|
self.name = "agent"
|
||||||
|
self.description = "Use AgentMesh framework to process tasks with multi-agent teams"
|
||||||
|
self.config = self._load_config()
|
||||||
|
self.tool_manager = ToolManager()
|
||||||
|
self.tool_manager.load_tools(config_dict=self.config.get("tools"))
|
||||||
|
logger.info("[agent] inited")
|
||||||
|
|
||||||
|
def _load_config(self) -> Dict:
|
||||||
|
"""Load configuration from config.yaml file."""
|
||||||
|
config_path = os.path.join(self.path, "config.yaml")
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
logger.warning(f"Config file not found at {config_path}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
def get_help_text(self, verbose=False, **kwargs):
|
||||||
|
"""Return help message for the agent plugin."""
|
||||||
|
help_text = "AgentMesh插件: 使用多智能体团队处理任务和回答问题,支持多种工具和智能体协作能力。"
|
||||||
|
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
|
||||||
|
|
||||||
|
if not verbose:
|
||||||
|
return help_text
|
||||||
|
|
||||||
|
teams = self.get_available_teams()
|
||||||
|
teams_str = ", ".join(teams) if teams else "未配置任何团队"
|
||||||
|
|
||||||
|
help_text += "\n\n使用说明:\n"
|
||||||
|
help_text += f"{trigger_prefix}agent teams - 列出可用的团队\n"
|
||||||
|
help_text += f"{trigger_prefix}agent use [team_name] [task] - 使用特定团队执行任务\n"
|
||||||
|
help_text += f"{trigger_prefix}agent [task] - 使用默认团队执行任务\n\n"
|
||||||
|
help_text += f"可用团队: {teams_str}\n\n"
|
||||||
|
help_text += f"示例:\n"
|
||||||
|
help_text += f"{trigger_prefix}agent use general_team 帮我分析多智能体技术发展趋势\n"
|
||||||
|
help_text += f"{trigger_prefix}agent 帮我查看当前文件夹路径"
|
||||||
|
|
||||||
|
return help_text
|
||||||
|
|
||||||
|
def get_available_teams(self) -> List[str]:
|
||||||
|
"""Get list of available teams from configuration."""
|
||||||
|
teams_config = self.config.get("teams", {})
|
||||||
|
return list(teams_config.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def create_team_from_config(self, team_name: str) -> Optional[AgentTeam]:
|
||||||
|
"""Create a team from configuration."""
|
||||||
|
# Get teams configuration
|
||||||
|
teams_config = self.config.get("teams", {})
|
||||||
|
|
||||||
|
# Check if the specified team exists
|
||||||
|
if team_name not in teams_config:
|
||||||
|
logger.error(f"Team '{team_name}' not found in configuration.")
|
||||||
|
available_teams = list(teams_config.keys())
|
||||||
|
logger.info(f"Available teams: {', '.join(available_teams)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get team configuration
|
||||||
|
team_config = teams_config[team_name]
|
||||||
|
|
||||||
|
# Get team's model
|
||||||
|
team_model_name = team_config.get("model", "gpt-4.1-mini")
|
||||||
|
team_model = self.create_llm_model(team_model_name)
|
||||||
|
|
||||||
|
# Get team's max_steps (default to 20 if not specified)
|
||||||
|
team_max_steps = team_config.get("max_steps", 20)
|
||||||
|
|
||||||
|
# Create team with the model
|
||||||
|
team = AgentTeam(
|
||||||
|
name=team_name,
|
||||||
|
description=team_config.get("description", ""),
|
||||||
|
rule=team_config.get("rule", ""),
|
||||||
|
model=team_model,
|
||||||
|
max_steps=team_max_steps
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and add agents to the team
|
||||||
|
agents_config = team_config.get("agents", [])
|
||||||
|
for agent_config in agents_config:
|
||||||
|
# Check if agent has a specific model
|
||||||
|
if agent_config.get("model"):
|
||||||
|
agent_model = self.create_llm_model(agent_config.get("model"))
|
||||||
|
else:
|
||||||
|
agent_model = team_model
|
||||||
|
|
||||||
|
# Get agent's max_steps
|
||||||
|
agent_max_steps = agent_config.get("max_steps")
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
name=agent_config.get("name", ""),
|
||||||
|
system_prompt=agent_config.get("system_prompt", ""),
|
||||||
|
model=agent_model, # Use agent's model if specified, otherwise will use team's model
|
||||||
|
description=agent_config.get("description", ""),
|
||||||
|
max_steps=agent_max_steps
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add tools to the agent if specified
|
||||||
|
tool_names = agent_config.get("tools", [])
|
||||||
|
for tool_name in tool_names:
|
||||||
|
tool = self.tool_manager.create_tool(tool_name)
|
||||||
|
if tool:
|
||||||
|
agent.add_tool(tool)
|
||||||
|
else:
|
||||||
|
if tool_name == "browser":
|
||||||
|
logger.warning(
|
||||||
|
"Tool 'Browser' loaded failed, "
|
||||||
|
"please install the required dependency with: \n"
|
||||||
|
"'pip install browser-use>=0.1.40' or 'pip install agentmesh-sdk[full]'\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Tool '{tool_name}' not found for agent '{agent.name}'\n")
|
||||||
|
|
||||||
|
# Add agent to team
|
||||||
|
team.add(agent)
|
||||||
|
|
||||||
|
return team
|
||||||
|
|
||||||
|
def on_handle_context(self, e_context: EventContext):
|
||||||
|
"""Handle the message context."""
|
||||||
|
if e_context['context'].type != ContextType.TEXT:
|
||||||
|
return
|
||||||
|
content = e_context['context'].content
|
||||||
|
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
|
||||||
|
|
||||||
|
if not content.startswith(f"{trigger_prefix}agent "):
|
||||||
|
e_context.action = EventAction.CONTINUE
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.config:
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.ERROR
|
||||||
|
reply.content = "未找到插件配置,请在 plugins/agent 目录下创建 config.yaml 配置文件,可根据 config-template.yml 模板文件复制"
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract the actual task
|
||||||
|
task = content[len(f"{trigger_prefix}agent "):].strip()
|
||||||
|
|
||||||
|
# If task is empty, return help message
|
||||||
|
if not task:
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.TEXT
|
||||||
|
reply.content = self.get_help_text(verbose=True)
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if task is asking for available teams
|
||||||
|
if task.lower() in ["teams", "list teams", "show teams"]:
|
||||||
|
teams = self.get_available_teams()
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.TEXT
|
||||||
|
|
||||||
|
if not teams:
|
||||||
|
reply.content = "未配置任何团队。请检查 config.yaml 文件。"
|
||||||
|
else:
|
||||||
|
reply.content = f"可用团队: {', '.join(teams)}"
|
||||||
|
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if task specifies a team
|
||||||
|
team_name = None
|
||||||
|
if task.startswith("use "):
|
||||||
|
parts = task[4:].split(" ", 1)
|
||||||
|
if len(parts) > 0:
|
||||||
|
team_name = parts[0]
|
||||||
|
if len(parts) > 1:
|
||||||
|
task = parts[1].strip()
|
||||||
|
else:
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.TEXT
|
||||||
|
reply.content = f"已选择团队 '{team_name}'。请输入您想执行的任务。"
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
if not team_name:
|
||||||
|
team_name = self.config.get("team")
|
||||||
|
|
||||||
|
# If no team specified, use default or first available
|
||||||
|
if not team_name:
|
||||||
|
teams = self.configself.get_available_teams()
|
||||||
|
if not teams:
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.TEXT
|
||||||
|
reply.content = "未配置任何团队。请检查 config.yaml 文件。"
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
team_name = teams[0]
|
||||||
|
|
||||||
|
# Create team
|
||||||
|
team = self.create_team_from_config(team_name)
|
||||||
|
if not team:
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.TEXT
|
||||||
|
reply.content = f"创建团队 '{team_name}' 失败。请检查配置。"
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run the task
|
||||||
|
try:
|
||||||
|
logger.info(f"[agent] Running task '{task}' with team '{team_name}', team_model={team.model.model}")
|
||||||
|
result = team.run_async(task=task)
|
||||||
|
for agent_result in result:
|
||||||
|
res_text = f"🤖 {agent_result.get('agent_name')}\n\n{agent_result.get('final_answer')}"
|
||||||
|
_send_text(e_context, content=res_text)
|
||||||
|
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.TEXT
|
||||||
|
reply.content = ""
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error running task with team '{team_name}'")
|
||||||
|
|
||||||
|
reply = Reply()
|
||||||
|
reply.type = ReplyType.ERROR
|
||||||
|
reply.content = f"执行任务时出错: {str(e)}"
|
||||||
|
e_context['reply'] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
|
return
|
||||||
|
|
||||||
|
def create_llm_model(self, model_name) -> LLMModel:
|
||||||
|
if conf().get("use_linkai"):
|
||||||
|
api_base = "https://api.link-ai.tech/v1"
|
||||||
|
api_key = conf().get("linkai_api_key")
|
||||||
|
elif model_name.startswith(("gpt", "text-davinci", "o1", "o3")):
|
||||||
|
api_base = conf().get("open_ai_api_base") or "https://api.openai.com/v1"
|
||||||
|
api_key = conf().get("open_ai_api_key")
|
||||||
|
elif model_name.startswith("claude"):
|
||||||
|
return ClaudeModel(model=model_name, api_key=conf().get("claude_api_key"))
|
||||||
|
elif model_name.startswith("moonshot"):
|
||||||
|
api_base = "https://api.moonshot.cn/v1"
|
||||||
|
api_key = conf().get("moonshot_api_key")
|
||||||
|
elif model_name.startswith("qwen"):
|
||||||
|
api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
|
api_key = conf().get("dashscope_api_key")
|
||||||
|
else:
|
||||||
|
api_base = conf().get("open_ai_api_base") or "https://api.openai.com/v1"
|
||||||
|
api_key = conf().get("open_ai_api_key")
|
||||||
|
|
||||||
|
llm_model = LLMModel(model=model_name, api_key=api_key, api_base=api_base)
|
||||||
|
return llm_model
|
||||||
|
|
||||||
|
|
||||||
|
def _send_text(e_context: EventContext, content: str):
|
||||||
|
reply = Reply(ReplyType.TEXT, content)
|
||||||
|
channel = e_context["channel"]
|
||||||
|
channel.send(reply, e_context["context"])
|
||||||
49
plugins/agent/config-template.yaml
Normal file
49
plugins/agent/config-template.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 选中的Agent Team
|
||||||
|
team: general_team
|
||||||
|
|
||||||
|
tools:
|
||||||
|
google_search:
|
||||||
|
# get your apikey from https://serper.dev/
|
||||||
|
api_key: "e7a21d840d6bb0ba832d850bb5aa4dee337415c4"
|
||||||
|
|
||||||
|
# Team config
|
||||||
|
teams:
|
||||||
|
general_team:
|
||||||
|
model: "qwen-plus"
|
||||||
|
description: "A versatile research and information agent team"
|
||||||
|
max_steps: 5
|
||||||
|
agents:
|
||||||
|
- name: "通用智能助手"
|
||||||
|
description: "Universal assistant specializing in research, information synthesis, and task execution"
|
||||||
|
system_prompt: "You are a versatile assistant who answers questions and completes tasks using available tools. Reply in a clearly structured, attractive and easy to read format."
|
||||||
|
tools:
|
||||||
|
- time
|
||||||
|
- calculator
|
||||||
|
- google_search
|
||||||
|
- browser
|
||||||
|
- terminal
|
||||||
|
|
||||||
|
software_team:
|
||||||
|
model: "gpt-4.1-mini"
|
||||||
|
description: "A software development team with product manager, developer and tester."
|
||||||
|
rule: "A normal R&D process should be that Product Manager writes PRD, Developer writes code based on PRD, and Finally, Tester performs testing."
|
||||||
|
max_steps: 10
|
||||||
|
agents:
|
||||||
|
- name: "Product-Manager"
|
||||||
|
description: "Responsible for product requirements and documentation"
|
||||||
|
system_prompt: "You are an experienced product manager who creates concise PRDs, focusing on user needs and feature specifications. You always format your responses in Markdown."
|
||||||
|
tools:
|
||||||
|
- time
|
||||||
|
- file_save
|
||||||
|
- name: "Developer"
|
||||||
|
description: "Implements code based on PRD"
|
||||||
|
system_prompt: "You are a skilled developer. When developing web application, you creates single-page website based on user needs, you deliver HTML files with embedded JavaScript and CSS that are visually appealing, responsive, and user-friendly, featuring a grand layout and beautiful background. The HTML, CSS, and JavaScript code should be well-structured and effectively organized."
|
||||||
|
tools:
|
||||||
|
- file_save
|
||||||
|
- name: "Tester"
|
||||||
|
description: "Tests code and verifies functionality"
|
||||||
|
system_prompt: "You are a tester who validates code against requirements. For HTML applications, use browser tools to test functionality. For Python or other client-side applications, use the terminal tool to run and test. You only need to test a few core cases."
|
||||||
|
tools:
|
||||||
|
- file_save
|
||||||
|
- browser
|
||||||
|
- terminal
|
||||||
@@ -155,7 +155,7 @@ def get_help_text(isadmin, isgroup):
|
|||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
if plugins[plugin].enabled and not plugins[plugin].hidden:
|
if plugins[plugin].enabled and not plugins[plugin].hidden:
|
||||||
namecn = plugins[plugin].namecn
|
namecn = plugins[plugin].namecn
|
||||||
help_text += "\n%s:" % namecn
|
help_text += "\n%s: " % namecn
|
||||||
help_text += PluginManager().instances[plugin].get_help_text(verbose=False).strip()
|
help_text += PluginManager().instances[plugin].get_help_text(verbose=False).strip()
|
||||||
|
|
||||||
if ADMIN_COMMANDS and isadmin:
|
if ADMIN_COMMANDS and isadmin:
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ Pillow
|
|||||||
pre-commit
|
pre-commit
|
||||||
web.py
|
web.py
|
||||||
linkai>=0.0.6.0
|
linkai>=0.0.6.0
|
||||||
|
agentmesh-sdk>=0.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user