From 70d7e52df0611a85e9d050ea540f33f11d51b966 Mon Sep 17 00:00:00 2001 From: Saboteur7 Date: Thu, 22 May 2025 17:31:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96agent=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8F=8AwebUI=E5=AF=B9=E8=AF=9D=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + channel/web/chat.html | 258 ++++++++++++++++---------- channel/web/static/axios.min.js | 2 + channel/web/web_channel.py | 23 ++- plugins/agent/README.md | 77 ++++++++ plugins/agent/__init__.py | 3 + plugins/agent/agent.py | 283 +++++++++++++++++++++++++++++ plugins/agent/config-template.yaml | 49 +++++ plugins/godcmd/godcmd.py | 2 +- requirements.txt | 2 +- 10 files changed, 602 insertions(+), 100 deletions(-) create mode 100644 channel/web/static/axios.min.js create mode 100644 plugins/agent/README.md create mode 100644 plugins/agent/__init__.py create mode 100644 plugins/agent/agent.py create mode 100644 plugins/agent/config-template.yaml diff --git a/.gitignore b/.gitignore index dd4fdeb..b88dc49 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ plugins.json itchat.pkl *.log logs/ +workspace +config.yaml user_datas.pkl chatgpt_tool_hub/ plugins/**/ @@ -31,4 +33,5 @@ plugins/banwords/lib/__pycache__ !plugins/role !plugins/keyword !plugins/linkai +!plugins/agent client_config.json diff --git a/channel/web/chat.html b/channel/web/chat.html index 1b1111c..3670640 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -6,7 +6,7 @@ AI Assistant - + @@ -14,6 +14,7 @@ + @@ -728,6 +782,19 @@ let userId = 'user_' + Math.random().toString(36).substring(2, 10); let currentSessionId = 'default_session'; // 使用固定会话ID + // 添加一个变量来跟踪输入法状态 + let isComposing = false; + + // 监听输入法组合状态开始 + input.addEventListener('compositionstart', function() { + isComposing = true; + }); + + // 监听输入法组合状态结束 + input.addEventListener('compositionend', function() { + isComposing = false; + }); + // 自动调整文本区域高度 input.addEventListener('input', function() { this.style.height = 'auto'; @@ -780,15 +847,19 @@ event.preventDefault(); } - // Enter 键发送消息 - else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey) { + // Enter 键发送消息,但只在不是输入法组合状态时 + else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !isComposing) { sendMessage(); event.preventDefault(); } }); + // 在发送消息函数前添加调试代码 + console.log('Axios loaded:', typeof axios !== 'undefined'); + // 发送消息函数 function sendMessage() { + console.log('Send message function called'); const userMessage = input.value.trim(); if (userMessage) { // 隐藏欢迎屏幕 @@ -811,33 +882,26 @@ sendButton.disabled = true; // 发送到服务器并等待响应 - fetch('/message', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ + axios({ + method: 'post', + url: '/message', + data: { user_id: userId, message: userMessage, timestamp: timestamp.toISOString(), session_id: currentSessionId - }) + }, + timeout: 120000 // 120秒超时 }) .then(response => { - if (!response.ok) { - throw new Error('Failed to send message'); - } - return response.json(); - }) - .then(data => { // 移除加载消息 if (loadingContainer.parentNode) { messagesDiv.removeChild(loadingContainer); } // 添加AI回复 - if (data.reply) { - addBotMessage(data.reply, new Date()); + if (response.data.reply) { + addBotMessage(response.data.reply, new Date()); } }) .catch(error => { @@ -847,7 +911,11 @@ messagesDiv.removeChild(loadingContainer); } // 显示错误消息 - addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); + if (error.code === 'ECONNABORTED') { + addBotMessage("请求超时,请再试一次吧。", new Date()); + } else { + addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); + } }); } } @@ -882,35 +950,30 @@ return botContainer; } - // 格式化消息内容(处理Markdown和代码高亮) + // 替换 formatMessage 函数,使用 markdown-it 替代 marked 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, // 启用换行符转换为
- gfm: true, // 启用 GitHub 风格的 Markdown - headerIds: true, // 为标题生成ID - mangle: false, // 不转义内联HTML - sanitize: false, // 不净化输出 - smartLists: true, // 使用更智能的列表行为 - smartypants: false, // 不使用更智能的标点符号 - xhtml: false // 不使用自闭合标签 - }); - try { - // 使用 marked 解析 Markdown - const parsed = marked.parse(content); - return parsed; + // 初始化 markdown-it 实例 + const md = window.markdownit({ + html: false, // 禁用 HTML 标签 + xhtmlOut: false, // 使用 '/' 关闭单标签 + breaks: true, // 将换行符转换为
+ 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) { console.error('Error parsing markdown:', e); // 如果解析失败,至少确保换行符正确显示 @@ -918,17 +981,30 @@ } } - // 添加消息后应用代码高亮 + // 更新 applyHighlighting 函数 function applyHighlighting() { try { 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)) { try { hljs.highlightBlock(block); } catch (e) { - console.error('Error highlighting block:', e); + console.error('Error highlighting specific language:', e); + hljs.highlightAuto(block); } } else { hljs.highlightAuto(block); diff --git a/channel/web/static/axios.min.js b/channel/web/static/axios.min.js new file mode 100644 index 0000000..79aa153 --- /dev/null +++ b/channel/web/static/axios.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:{},s=i.allOwnKeys,a=void 0!==s&&s;if(null!=t)if("object"!==e(t)&&(t=[t]),l(t))for(r=0,o=t.length;r3&&void 0!==arguments[3]?arguments[3]:{},i=r.allOwnKeys;return S(t,(function(t,r){n&&m(t)?e[r]=o(t,n):e[r]=t}),{allOwnKeys:i}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,s,u={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)s=o[i],r&&!r(s,e,t)||u[s]||(t[s]=e[s],u[s]=!0);e=!1!==n&&a(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:u,kindOfTest:c,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(l(e))return e;var t=e.length;if(!v(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:T,hasOwnProperty:x,hasOwnProp:x,reduceDescriptors:N,freezeMethods:function(e){N(e,(function(t,n){var r=e[n];m(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return l(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t}};function _(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}P.inherits(_,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var B=_.prototype,D={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){D[e]={value:e}})),Object.defineProperties(_,D),Object.defineProperty(B,"isAxiosError",{value:!0}),_.from=function(e,t,n,r,o,i){var s=Object.create(B);return P.toFlatObject(e,s,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),_.call(s,e.message,t,n,r,o),s.cause=e,s.name=e.name,i&&Object.assign(s,i),s};var F="object"==("undefined"==typeof self?"undefined":e(self))?self.FormData:window.FormData;function U(e){return P.isPlainObject(e)||P.isArray(e)}function k(e){return P.endsWith(e,"[]")?e.slice(0,-2):e}function L(e,t,n){return e?e.concat(t).map((function(e,t){return e=k(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var q=P.toFlatObject(P,{},null,(function(e){return/^is[A-Z]/.test(e)}));function z(t,n,r){if(!P.isObject(t))throw new TypeError("target must be an object");n=n||new(F||FormData);var o,i=(r=P.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!P.isUndefined(t[e])}))).metaTokens,s=r.visitor||l,a=r.dots,u=r.indexes,c=(r.Blob||"undefined"!=typeof Blob&&Blob)&&((o=n)&&P.isFunction(o.append)&&"FormData"===o[Symbol.toStringTag]&&o[Symbol.iterator]);if(!P.isFunction(s))throw new TypeError("visitor must be a function");function f(e){if(null===e)return"";if(P.isDate(e))return e.toISOString();if(!c&&P.isBlob(e))throw new _("Blob is not supported. Use a Buffer instead.");return P.isArrayBuffer(e)||P.isTypedArray(e)?c&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function l(t,r,o){var s=t;if(t&&!o&&"object"===e(t))if(P.endsWith(r,"{}"))r=i?r:r.slice(0,-2),t=JSON.stringify(t);else if(P.isArray(t)&&function(e){return P.isArray(e)&&!e.some(U)}(t)||P.isFileList(t)||P.endsWith(r,"[]")&&(s=P.toArray(t)))return r=k(r),s.forEach((function(e,t){!P.isUndefined(e)&&n.append(!0===u?L([r],t,a):null===u?r:r+"[]",f(e))})),!1;return!!U(t)||(n.append(L(o,r,a),f(t)),!1)}var d=[],h=Object.assign(q,{defaultVisitor:l,convertValue:f,isVisitable:U});if(!P.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!P.isUndefined(t)){if(-1!==d.indexOf(t))throw Error("Circular reference detected in "+r.join("."));d.push(t),P.forEach(t,(function(t,o){!0===(!P.isUndefined(t)&&s.call(n,t,P.isString(o)?o.trim():o,r,h))&&e(t,r?r.concat(o):[o])})),d.pop()}}(t),n}function I(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function M(e,t){this._pairs=[],e&&z(e,this,t)}var J=M.prototype;function H(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function V(e,t,n){if(!t)return e;var r=e.indexOf("#");-1!==r&&(e=e.slice(0,r));var o=n&&n.encode||H,i=P.isURLSearchParams(t)?t.toString():new M(t,n).toString(o);return i&&(e+=(-1===e.indexOf("?")?"?":"&")+i),e}J.append=function(e,t){this._pairs.push([e,t])},J.toString=function(e){var t=e?function(t){return e.call(this,t,I)}:I;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var W,K=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){P.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),X={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},$="undefined"!=typeof URLSearchParams?URLSearchParams:M,Q=FormData,G=("undefined"==typeof navigator||"ReactNative"!==(W=navigator.product)&&"NativeScript"!==W&&"NS"!==W)&&"undefined"!=typeof window&&"undefined"!=typeof document,Y={isBrowser:!0,classes:{URLSearchParams:$,FormData:Q,Blob:Blob},isStandardBrowserEnv:G,protocols:["http","https","file","blob","url","data"]};function Z(e){function t(e,n,r,o){var i=e[o++],s=Number.isFinite(+i),a=o>=e.length;return i=!i&&P.isArray(r)?r.length:i,a?(P.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!s):(r[i]&&P.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&P.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t0;)if(t===(n=r[o]).toLowerCase())return n;return null}function le(e,t){e&&this.set(e),this[se]=t||null}function de(e,t){var n=0,r=function(e,t){e=e||10;var n,r=new Array(e),o=new Array(e),i=0,s=0;return t=void 0!==t?t:1e3,function(a){var u=Date.now(),c=o[s];n||(n=u),r[i]=a,o[i]=u;for(var f=s,l=0;f!==i;)l+=r[f++],f%=e;if((i=(i+1)%e)===s&&(s=(s+1)%e),!(u-n-1,i=P.isObject(e);if(i&&P.isHTMLForm(e)&&(e=new FormData(e)),P.isFormData(e))return o&&o?JSON.stringify(Z(e)):e;if(P.isArrayBuffer(e)||P.isBuffer(e)||P.isStream(e)||P.isFile(e)||P.isBlob(e))return e;if(P.isArrayBufferView(e))return e.buffer;if(P.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return z(e,new Y.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return Y.isNode&&P.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=P.isFileList(e))||r.indexOf("multipart/form-data")>-1){var s=this.env&&this.env.FormData;return z(n?{"files[]":e}:e,s&&new s,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(P.isString(e))try{return(t||JSON.parse)(e),P.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||be.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&P.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw _.from(e,_.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:Y.classes.FormData,Blob:Y.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};function ge(e,t){var n=this||be,r=t||n,o=le.from(r.headers),i=r.data;return P.forEach(e,(function(e){i=e.call(n,i,o.normalize(),t?t.status:void 0)})),o.normalize(),i}function Ee(e){return!(!e||!e.__CANCEL__)}function we(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new re}function Oe(e){return we(e),e.headers=le.from(e.headers),e.data=ge.call(e,e.transformRequest),(e.adapter||be.adapter)(e).then((function(t){return we(e),t.data=ge.call(e,e.transformResponse,t),t.headers=le.from(t.headers),t}),(function(t){return Ee(t)||(we(e),t&&t.response&&(t.response.data=ge.call(e,e.transformResponse,t.response),t.response.headers=le.from(t.response.headers))),Promise.reject(t)}))}function Re(e,t){t=t||{};var n={};function r(e,t){return P.isPlainObject(e)&&P.isPlainObject(t)?P.merge(e,t):P.isPlainObject(t)?P.merge({},t):P.isArray(t)?t.slice():t}function o(n){return P.isUndefined(t[n])?P.isUndefined(e[n])?void 0:r(void 0,e[n]):r(e[n],t[n])}function i(e){if(!P.isUndefined(t[e]))return r(void 0,t[e])}function s(n){return P.isUndefined(t[n])?P.isUndefined(e[n])?void 0:r(void 0,e[n]):r(void 0,t[n])}function a(n){return n in t?r(e[n],t[n]):n in e?r(void 0,e[n]):void 0}var u={url:i,method:i,data:i,baseURL:s,transformRequest:s,transformResponse:s,paramsSerializer:s,timeout:s,timeoutMessage:s,withCredentials:s,adapter:s,responseType:s,xsrfCookieName:s,xsrfHeaderName:s,onUploadProgress:s,onDownloadProgress:s,decompress:s,maxContentLength:s,maxBodyLength:s,beforeRedirect:s,transport:s,httpAgent:s,httpsAgent:s,cancelToken:s,socketPath:s,responseEncoding:s,validateStatus:a};return P.forEach(Object.keys(e).concat(Object.keys(t)),(function(e){var t=u[e]||o,r=t(e);P.isUndefined(r)&&t!==a||(n[e]=r)})),n}P.forEach(["delete","get","head"],(function(e){be.headers[e]={}})),P.forEach(["post","put","patch"],(function(e){be.headers[e]=P.merge(ve)}));var Se="1.1.2",Ae={};["object","boolean","number","function","string","symbol"].forEach((function(t,n){Ae[t]=function(r){return e(r)===t||"a"+(n<1?"n ":" ")+t}}));var je={};Ae.transitional=function(e,t,n){function r(e,t){return"[Axios v1.1.2] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,o,i){if(!1===e)throw new _(r(o," has been removed"+(t?" in "+t:"")),_.ERR_DEPRECATED);return t&&!je[o]&&(je[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,i)}};var Te={assertOptions:function(t,n,r){if("object"!==e(t))throw new _("options must be an object",_.ERR_BAD_OPTION_VALUE);for(var o=Object.keys(t),i=o.length;i-- >0;){var s=o[i],a=n[s];if(a){var u=t[s],c=void 0===u||a(u,s,t);if(!0!==c)throw new _("option "+s+" must be "+c,_.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new _("Unknown option "+s,_.ERR_BAD_OPTION)}},validators:Ae},xe=Te.validators,Ce=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new K,response:new K}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n=(t=Re(this.defaults,t)).transitional;void 0!==n&&Te.assertOptions(n,{silentJSONParsing:xe.transitional(xe.boolean),forcedJSONParsing:xe.transitional(xe.boolean),clarifyTimeoutError:xe.transitional(xe.boolean)},!1),t.method=(t.method||this.defaults.method||"get").toLowerCase();var r=t.headers&&P.merge(t.headers.common,t.headers[t.method]);r&&P.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete t.headers[e]})),t.headers=new le(t.headers,r);var o=[],i=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(i=i&&e.synchronous,o.unshift(e.fulfilled,e.rejected))}));var s,a=[];this.interceptors.response.forEach((function(e){a.push(e.fulfilled,e.rejected)}));var u,c=0;if(!i){var f=[Oe.bind(this),void 0];for(f.unshift.apply(f,o),f.push.apply(f,a),u=f.length,s=Promise.resolve(t);c0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new re(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Pe=function e(t){var n=new Ce(t),r=o(Ce.prototype.request,n);return P.extend(r,Ce.prototype,n,{allOwnKeys:!0}),P.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(Re(t,n))},r}(be);return Pe.Axios=Ce,Pe.CanceledError=re,Pe.CancelToken=Ne,Pe.isCancel=Ee,Pe.VERSION=Se,Pe.toFormData=z,Pe.AxiosError=_,Pe.Cancel=Pe.CanceledError,Pe.all=function(e){return Promise.all(e)},Pe.spread=function(e){return function(t){return e.apply(null,t)}},Pe.isAxiosError=function(e){return P.isObject(e)&&!0===e.isAxiosError},Pe.formToJSON=function(e){return Z(P.isHTMLForm(e)?new FormData(e):e)},Pe})); +//# sourceMappingURL=axios.min.js.map diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index aac4f68..c62598a 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -126,7 +126,7 @@ class WebChannel(ChatChannel): # 等待响应,最多等待30秒 try: - response = response_queue.get(timeout=30) + response = response_queue.get(timeout=120) return json.dumps({"status": "success", "reply": response["content"]}) except Empty: return json.dumps({"status": "error", "message": "Response timeout"}) @@ -151,13 +151,27 @@ class WebChannel(ChatChannel): logger.info(f"Created static directory: {static_dir}") urls = ( + '/', 'RootHandler', # 添加根路径处理器 '/message', 'MessageHandler', '/chat', 'ChatHandler', '/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径 ) port = conf().get("web_port", 9899) 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: @@ -185,11 +199,6 @@ class AssetsHandler: current_dir = os.path.dirname(os.path.abspath(__file__)) 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)) # 安全检查:确保请求的文件在static目录内 diff --git a/plugins/agent/README.md b/plugins/agent/README.md new file mode 100644 index 0000000..15e0e77 --- /dev/null +++ b/plugins/agent/README.md @@ -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. 如果需要使用浏览器工具,请确保安装了相关依赖 diff --git a/plugins/agent/__init__.py b/plugins/agent/__init__.py new file mode 100644 index 0000000..75642e0 --- /dev/null +++ b/plugins/agent/__init__.py @@ -0,0 +1,3 @@ +from .agent import AgentPlugin + +__all__ = ["AgentPlugin"] \ No newline at end of file diff --git a/plugins/agent/agent.py b/plugins/agent/agent.py new file mode 100644 index 0000000..0532216 --- /dev/null +++ b/plugins/agent/agent.py @@ -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"]) diff --git a/plugins/agent/config-template.yaml b/plugins/agent/config-template.yaml new file mode 100644 index 0000000..3fd2b43 --- /dev/null +++ b/plugins/agent/config-template.yaml @@ -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 diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index fe35879..ecf519c 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -155,7 +155,7 @@ def get_help_text(isadmin, isgroup): for plugin in plugins: if plugins[plugin].enabled and not plugins[plugin].hidden: 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() if ADMIN_COMMANDS and isadmin: diff --git a/requirements.txt b/requirements.txt index 255172e..06f285a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ Pillow pre-commit web.py linkai>=0.0.6.0 - +agentmesh-sdk>=0.1.2 From 5f7ade20dc88acc9f0a1d4c4d3a8aa13867d4a38 Mon Sep 17 00:00:00 2001 From: Saboteur7 Date: Fri, 23 May 2025 00:43:54 +0800 Subject: [PATCH 2/3] feat: web channel support multiple message and picture display --- channel/web/chat.html | 673 +++++++++++++++++++++++----------- channel/web/static/github.png | Bin 0 -> 3486 bytes channel/web/web_channel.py | 160 +++++--- config-template.json | 2 +- simple_login_form_test.html | 278 ++++++++++++++ 5 files changed, 848 insertions(+), 265 deletions(-) create mode 100644 channel/web/static/github.png create mode 100644 simple_login_form_test.html diff --git a/channel/web/chat.html b/channel/web/chat.html index 3670640..50e81fa 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -170,6 +170,24 @@ flex: 1; } + #github-link { + display: flex; + align-items: center; + margin-left: 15px; + color: var(--text-color); + text-decoration: none; + transition: opacity 0.2s; + } + + #github-link:hover { + opacity: 0.8; + } + + #github-icon { + height: 24px; + width: 24px; + } + #messages { flex: 1; overflow-y: auto; @@ -210,7 +228,7 @@ } .user-container .avatar { - margin-left: 15px; + margin-left: 20px; margin-right: 0; } @@ -219,6 +237,7 @@ } .user-container .message { + padding: 13px 16px; background-color: var(--bot-msg-bg); border-radius: 10px; margin-bottom: 8px; @@ -266,7 +285,7 @@ } .message { - padding: 12px 16px; + padding: 5px 16px; border-radius: 10px; margin-top: 0; margin-bottom: 8px; @@ -400,7 +419,7 @@ .message img { max-width: 100%; height: auto; - margin: 1em 0; + margin: 0 0; } .timestamp { @@ -548,10 +567,12 @@ left: -260px; height: 100%; z-index: 1000; + transition: left 0.3s ease; } #sidebar.active { left: 0; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2); } #menu-toggle { @@ -577,6 +598,23 @@ #header-logo { height: 24px; } + + /* 添加遮罩层,当侧边栏打开时显示 */ + #sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; + cursor: pointer; + } + + #sidebar.active + #sidebar-overlay { + display: block; + } } /* Dark mode support */ @@ -717,7 +755,7 @@ - +
AI Assistant Logo
AI 助手
+ + GitHub +
@@ -778,10 +819,17 @@ const newChatButton = document.getElementById('new-chat'); const chatHistory = document.getElementById('chat-history'); - // 简化变量,只保留用户ID - let userId = 'user_' + Math.random().toString(36).substring(2, 10); - let currentSessionId = 'default_session'; // 使用固定会话ID - + // 生成新的会话ID + function generateSessionId() { + return 'session_' + ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } + + // 生成初始会话ID + let sessionId = generateSessionId(); + console.log('Session ID:', sessionId); + // 添加一个变量来跟踪输入法状态 let isComposing = false; @@ -815,15 +863,18 @@ }); // 处理菜单切换 - menuToggle.addEventListener('click', function() { + menuToggle.addEventListener('click', function(event) { + event.stopPropagation(); // 防止事件冒泡到 main-content sidebar.classList.toggle('active'); }); - // 处理新对话按钮 - 创建新的用户ID和清空当前对话 + // 处理新对话按钮 - 创建新的会话ID和清空当前对话 newChatButton.addEventListener('click', function() { - // 生成新的用户ID - userId = 'user_' + Math.random().toString(36).substring(2, 10); - console.log('New conversation started with user ID:', userId); + // 生成新的会话ID + sessionId = generateSessionId(); + // 将新的会话ID保存到全局变量,供轮询函数使用 + window.sessionId = sessionId; + console.log('New conversation started with new session ID:', sessionId); // 清空聊天记录 clearChat(); @@ -881,27 +932,42 @@ input.style.height = '52px'; sendButton.disabled = true; - // 发送到服务器并等待响应 + // 使用当前的全局会话ID + const currentSessionId = window.sessionId || sessionId; + + // 发送到服务器并获取请求ID axios({ method: 'post', url: '/message', data: { - user_id: userId, + session_id: currentSessionId, // 使用最新的会话ID message: userMessage, - timestamp: timestamp.toISOString(), - session_id: currentSessionId + timestamp: timestamp.toISOString() }, - timeout: 120000 // 120秒超时 + timeout: 10000 // 10秒超时 }) .then(response => { - // 移除加载消息 - if (loadingContainer.parentNode) { - messagesDiv.removeChild(loadingContainer); - } - - // 添加AI回复 - if (response.data.reply) { - addBotMessage(response.data.reply, new Date()); + if (response.data.status === "success") { + // 保存当前请求ID,用于识别响应 + const currentRequestId = response.data.request_id; + + // 如果还没有开始轮询,则开始轮询 + if (!window.isPolling) { + startPolling(currentSessionId); + } + + // 将请求ID和加载容器关联起来 + window.loadingContainers = window.loadingContainers || {}; + window.loadingContainers[currentRequestId] = loadingContainer; + + // 初始化请求的响应容器映射 + window.requestContainers = window.requestContainers || {}; + } else { + // 处理错误 + if (loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + } + addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); } }) .catch(error => { @@ -920,178 +986,108 @@ } } - // 添加加载中的消息 - function addLoadingMessage() { - const botContainer = document.createElement('div'); - botContainer.className = 'bot-container loading-container'; + // 修改轮询函数,确保正确处理多条回复 + function startPolling(sessionId) { + if (window.isPolling) return; - const messageContainer = document.createElement('div'); - messageContainer.className = 'message-container'; + window.isPolling = true; + console.log('Starting polling with session ID:', sessionId); - messageContainer.innerHTML = ` -
- -
-
-
-
- - - -
-
-
- `; - - botContainer.appendChild(messageContainer); - messagesDiv.appendChild(botContainer); - scrollToBottom(); - - return botContainer; - } - - // 替换 formatMessage 函数,使用 markdown-it 替代 marked - function formatMessage(content) { - try { - // 初始化 markdown-it 实例 - const md = window.markdownit({ - html: false, // 禁用 HTML 标签 - xhtmlOut: false, // 使用 '/' 关闭单标签 - breaks: true, // 将换行符转换为
- 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); + function poll() { + if (!window.isPolling) return; + + // 如果页面已关闭或导航离开,停止轮询 + if (document.hidden) { + setTimeout(poll, 5000); // 页面不可见时降低轮询频率 + return; + } + + // 使用当前的会话ID,而不是闭包中的sessionId + const currentSessionId = window.sessionId || sessionId; + + axios({ + method: 'post', + url: '/poll', + data: { + session_id: currentSessionId + }, + timeout: 5000 + }) + .then(response => { + if (response.data.status === "success") { + if (response.data.has_content) { + console.log('Received response:', response.data); + + // 获取请求ID和内容 + const requestId = response.data.request_id; + const content = response.data.content; + const timestamp = new Date(response.data.timestamp * 1000); + + // 检查是否有对应的加载容器 + if (window.loadingContainers && window.loadingContainers[requestId]) { + // 移除加载容器 + const loadingContainer = window.loadingContainers[requestId]; + if (loadingContainer && loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + } + + // 删除已处理的加载容器引用 + delete window.loadingContainers[requestId]; } + + // 始终创建新的消息,无论是否是同一个请求的后续回复 + addBotMessage(content, timestamp, requestId); + + // 滚动到底部 + scrollToBottom(); } - return hljs.highlightAuto(str).value; - } - }); - - // 渲染 Markdown - return md.render(content); - } catch (e) { - console.error('Error parsing markdown:', e); - // 如果解析失败,至少确保换行符正确显示 - return content.replace(/\n/g, '
'); - } - } - - // 更新 applyHighlighting 函数 - function applyHighlighting() { - try { - document.querySelectorAll('pre code').forEach((block) => { - // 确保代码块有正确的类 - 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)) { - try { - hljs.highlightBlock(block); - } catch (e) { - console.error('Error highlighting specific language:', e); - hljs.highlightAuto(block); - } + + // 继续轮询,使用原来的2秒间隔 + setTimeout(poll, 2000); } else { - hljs.highlightAuto(block); + // 处理错误但继续轮询 + console.error('Error in polling response:', response.data.message); + setTimeout(poll, 3000); } + }) + .catch(error => { + console.error('Error polling for response:', error); + // 出错后继续轮询,但间隔更长 + setTimeout(poll, 3000); }); - } catch (e) { - console.error('Error applying code highlighting:', e); } - } - - // 添加用户消息的函数 (保存到localStorage) - function addUserMessage(content, timestamp) { - // 显示消息 - displayUserMessage(content, timestamp); - // 保存到localStorage - saveMessageToLocalStorage({ - role: 'user', - content: content, - timestamp: timestamp.getTime() - }); + // 开始轮询 + poll(); } - // 添加机器人消息的函数 (保存到localStorage) - function addBotMessage(content, timestamp) { + // 添加机器人消息的函数 (保存到localStorage),增加requestId参数 + function addBotMessage(content, timestamp, requestId) { // 显示消息 - displayBotMessage(content, timestamp); + displayBotMessage(content, timestamp, requestId); // 保存到localStorage saveMessageToLocalStorage({ role: 'assistant', content: content, - timestamp: timestamp.getTime() + timestamp: timestamp.getTime(), + requestId: requestId }); } - // 只显示用户消息而不保存到localStorage - function displayUserMessage(content, timestamp) { - const userContainer = document.createElement('div'); - userContainer.className = 'user-container'; - - const messageContainer = document.createElement('div'); - messageContainer.className = 'message-container'; - - // 安全地格式化消息 - let formattedContent; - try { - formattedContent = formatMessage(content); - } catch (e) { - console.error('Error formatting user message:', e); - formattedContent = `

${content.replace(/\n/g, '
')}

`; - } - - messageContainer.innerHTML = ` -
- -
-
-
${formattedContent}
-
${formatTimestamp(timestamp)}
-
- `; - - userContainer.appendChild(messageContainer); - messagesDiv.appendChild(userContainer); - - // 应用代码高亮 - setTimeout(() => { - applyHighlighting(); - }, 0); - - scrollToBottom(); - } - - // 只显示机器人消息而不保存到localStorage - function displayBotMessage(content, timestamp) { + // 修改显示机器人消息的函数,增加requestId参数 + function displayBotMessage(content, timestamp, requestId) { const botContainer = document.createElement('div'); botContainer.className = 'bot-container'; + // 如果有requestId,将其存储在数据属性中 + if (requestId) { + botContainer.dataset.requestId = requestId; + } + const messageContainer = document.createElement('div'); messageContainer.className = 'message-container'; - // 确保时间戳是有效的 Date 对象 - if (!(timestamp instanceof Date) || isNaN(timestamp)) { - timestamp = new Date(); - } - // 安全地格式化消息 let formattedContent; try { @@ -1114,45 +1110,82 @@ botContainer.appendChild(messageContainer); messagesDiv.appendChild(botContainer); - // 使用setTimeout确保DOM已更新,并延长等待时间 + // 应用代码高亮 setTimeout(() => { - try { - // 直接对新添加的消息应用高亮 - const codeBlocks = botContainer.querySelectorAll('pre code'); - codeBlocks.forEach(block => { - // 确保代码块有正确的类 - 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)) { - try { - hljs.highlightBlock(block); - } catch (e) { - console.error('Error highlighting specific language:', e); - hljs.highlightAuto(block); - } - } else { - hljs.highlightAuto(block); - } - }); - } catch (e) { - console.error('Error in delayed highlighting:', e); - } - }, 100); // 增加延迟以确保DOM完全更新 + applyHighlighting(); + }, 0); scrollToBottom(); } + // 处理响应 + function handleResponse(requestId, content) { + // 获取该请求的加载容器 + const loadingContainer = window.loadingContainers && window.loadingContainers[requestId]; + + // 如果有加载容器,移除它 + if (loadingContainer && loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + delete window.loadingContainers[requestId]; + } + + // 为每个请求创建一个新的消息容器 + if (!window.requestContainers[requestId]) { + window.requestContainers[requestId] = createBotMessageContainer(content, new Date()); + } else { + // 更新现有消息容器 + updateBotMessageContent(window.requestContainers[requestId], content); + } + + // 保存消息到localStorage + saveMessageToLocalStorage({ + role: 'assistant', + content: content, + timestamp: new Date().getTime(), + request_id: requestId + }); + } + + // 修改createBotMessageContainer函数,使其返回创建的容器 + function createBotMessageContainer(content, timestamp) { + const botContainer = document.createElement('div'); + botContainer.className = 'bot-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + // 安全地格式化消息 + let formattedContent; + try { + formattedContent = formatMessage(content); + } catch (e) { + console.error('Error formatting bot message:', e); + formattedContent = `

${content.replace(/\n/g, '
')}

`; + } + + messageContainer.innerHTML = ` +
+ +
+
+
${formattedContent}
+
${formatTimestamp(timestamp)}
+
+ `; + + botContainer.appendChild(messageContainer); + messagesDiv.appendChild(botContainer); + + // 应用代码高亮 + setTimeout(() => { + applyHighlighting(); + }, 0); + + scrollToBottom(); + + return botContainer; + } + // 格式化时间戳 function formatTimestamp(date) { return date.toLocaleTimeString(); @@ -1223,8 +1256,8 @@ }); }); - // 清空localStorage中的消息 - 使用用户ID作为键 - localStorage.setItem(`chatMessages_${userId}`, JSON.stringify([])); + // 清空localStorage中的消息 - 使用会话ID作为键 + localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify([])); // 在移动设备上关闭侧边栏 if (window.innerWidth <= 768) { @@ -1232,26 +1265,242 @@ } } - // 从localStorage加载消息 - 使用用户ID作为键 + // 从localStorage加载消息 - 使用会话ID作为键 function loadMessagesFromLocalStorage() { try { - return JSON.parse(localStorage.getItem(`chatMessages_${userId}`) || '[]'); + return JSON.parse(localStorage.getItem(`chatMessages_${sessionId}`) || '[]'); } catch (error) { console.error('Error loading messages from localStorage:', error); return []; } } - // 保存消息到localStorage - 使用用户ID作为键 + // 保存消息到localStorage - 使用会话ID作为键 function saveMessageToLocalStorage(message) { try { const messages = loadMessagesFromLocalStorage(); messages.push(message); - localStorage.setItem(`chatMessages_${userId}`, JSON.stringify(messages)); + localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify(messages)); } catch (error) { console.error('Error saving message to localStorage:', error); } } + + // 添加用户消息的函数 (保存到localStorage) + function addUserMessage(content, timestamp) { + // 显示消息 + displayUserMessage(content, timestamp); + + // 保存到localStorage + saveMessageToLocalStorage({ + role: 'user', + content: content, + timestamp: timestamp.getTime() + }); + } + + // 只显示用户消息而不保存到localStorage + function displayUserMessage(content, timestamp) { + const userContainer = document.createElement('div'); + userContainer.className = 'user-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + // 安全地格式化消息 + let formattedContent; + try { + formattedContent = formatMessage(content); + } catch (e) { + console.error('Error formatting user message:', e); + formattedContent = `

${content.replace(/\n/g, '
')}

`; + } + + messageContainer.innerHTML = ` +
+ +
+
+
${formattedContent}
+
${formatTimestamp(timestamp)}
+
+ `; + + userContainer.appendChild(messageContainer); + messagesDiv.appendChild(userContainer); + + // 应用代码高亮 + setTimeout(() => { + applyHighlighting(); + }, 0); + + scrollToBottom(); + } + + // 添加加载中的消息 + function addLoadingMessage() { + const botContainer = document.createElement('div'); + botContainer.className = 'bot-container loading-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + messageContainer.innerHTML = ` +
+ +
+
+
+
+ + + +
+
+
+ `; + + botContainer.appendChild(messageContainer); + messagesDiv.appendChild(botContainer); + scrollToBottom(); + + return botContainer; + } + + // 自动将链接设置为在新标签页打开 + const externalLinksPlugin = (md) => { + // 保存原始的链接渲染器 + const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + // 重写链接渲染器 + md.renderer.rules.link_open = function(tokens, idx, options, env, self) { + // 为所有链接添加 target="_blank" 和 rel="noopener noreferrer" + const token = tokens[idx]; + + // 添加 target="_blank" 属性 + token.attrPush(['target', '_blank']); + + // 添加 rel="noopener noreferrer" 以提高安全性 + token.attrPush(['rel', 'noopener noreferrer']); + + // 调用默认渲染器 + return defaultRender(tokens, idx, options, env, self); + }; + }; + + // 替换 formatMessage 函数,使用 markdown-it 替代 marked + function formatMessage(content) { + try { + // 初始化 markdown-it 实例 + const md = window.markdownit({ + html: false, // 禁用 HTML 标签 + xhtmlOut: false, // 使用 '/' 关闭单标签 + breaks: true, // 将换行符转换为
+ 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; + } + }); + + // 自动将图片URL转换为图片标签 + const autoImagePlugin = (md) => { + const defaultRender = md.renderer.rules.text || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + md.renderer.rules.text = function(tokens, idx, options, env, self) { + const token = tokens[idx]; + const text = token.content.trim(); + + // 检测是否完全是一个图片链接 (以https://开头,以图片扩展名结尾) + const imageRegex = /^https?:\/\/\S+\.(jpg|jpeg|png|gif|webp)(\?\S*)?$/i; + if (imageRegex.test(text)) { + return `Image`; + } + + // 使用默认渲染 + return defaultRender(tokens, idx, options, env, self); + }; + }; + + // 应用插件 + md.use(autoImagePlugin); + + // 应用外部链接插件 + md.use(externalLinksPlugin); + + // 渲染 Markdown + return md.render(content); + } catch (e) { + console.error('Error parsing markdown:', e); + // 如果解析失败,至少确保换行符正确显示 + return content.replace(/\n/g, '
'); + } + } + + // 更新 applyHighlighting 函数 + function applyHighlighting() { + try { + document.querySelectorAll('pre code').forEach((block) => { + // 确保代码块有正确的类 + 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)) { + try { + hljs.highlightBlock(block); + } catch (e) { + console.error('Error highlighting specific language:', e); + hljs.highlightAuto(block); + } + } else { + hljs.highlightAuto(block); + } + }); + } catch (e) { + console.error('Error applying code highlighting:', e); + } + } + + // 在 #main-content 上添加点击事件,用于关闭侧边栏 + document.getElementById('main-content').addEventListener('click', function(event) { + // 只在移动视图下且侧边栏打开时处理 + if (window.innerWidth <= 768 && sidebar.classList.contains('active')) { + sidebar.classList.remove('active'); + } + }); + + // 阻止侧边栏内部点击事件冒泡到 main-content + document.getElementById('sidebar').addEventListener('click', function(event) { + event.stopPropagation(); + }); + + // 添加遮罩层点击事件,用于关闭侧边栏 + document.getElementById('sidebar-overlay').addEventListener('click', function() { + if (sidebar.classList.contains('active')) { + sidebar.classList.remove('active'); + } + }); \ No newline at end of file diff --git a/channel/web/static/github.png b/channel/web/static/github.png new file mode 100644 index 0000000000000000000000000000000000000000..9122abb52d1df8f48c345f3ec4fb3b5ca90dc15c GIT binary patch literal 3486 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 Mg|&ILnP=jE0bF27B>(^b literal 0 HcmV?d00001 diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index c62598a..057a745 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -2,6 +2,7 @@ import sys import time import web import json +import uuid from queue import Queue, Empty from bridge.context import * from bridge.reply import Reply, ReplyType @@ -12,6 +13,8 @@ from common.singleton import singleton from config import conf import os import mimetypes # 添加这行来处理MIME类型 +import threading +import logging class WebMessage(ChatMessage): def __init__( @@ -43,39 +46,54 @@ class WebChannel(ChatChannel): def __init__(self): super().__init__() - self.message_queues = {} # 为每个用户存储一个消息队列 self.msg_id_counter = 0 # 添加消息ID计数器 + self.session_queues = {} # 存储session_id到队列的映射 + self.request_to_session = {} # 存储request_id到session_id的映射 def _generate_msg_id(self): """生成唯一的消息ID""" self.msg_id_counter += 1 return str(int(time.time())) + str(self.msg_id_counter) + def _generate_request_id(self): + """生成唯一的请求ID""" + return str(uuid.uuid4()) + def send(self, reply: Reply, context: Context): try: if reply.type in self.NOT_SUPPORT_REPLYTYPE: logger.warning(f"Web channel doesn't support {reply.type} yet") return + + if reply.type == ReplyType.IMAGE_URL: + time.sleep(0.5) + + # 获取请求ID和会话ID + request_id = context.get("request_id", None) + + if not request_id: + logger.error("No request_id found in context, cannot send message") + return - # 获取用户ID - user_id = context.get("receiver", None) - if not user_id: - logger.error("No receiver found in context, cannot send message") + # 通过request_id获取session_id + session_id = self.request_to_session.get(request_id) + if not session_id: + logger.error(f"No session_id found for request {request_id}") return - # 检查是否有响应队列 - response_queue = context.get("response_queue", None) - if response_queue: - # 直接将响应放入队列 + # 检查是否有会话队列 + if session_id in self.session_queues: + # 创建响应数据,包含请求ID以区分不同请求的响应 response_data = { "type": str(reply.type), "content": reply.content, - "timestamp": time.time() + "timestamp": time.time(), + "request_id": request_id } - response_queue.put(response_data) - logger.debug(f"Response sent to queue for user {user_id}") + self.session_queues[session_id].put(response_data) + logger.debug(f"Response sent to queue for session {session_id}, request {request_id}") else: - logger.warning(f"No response queue found for user {user_id}, response dropped") + logger.warning(f"No response queue found for session {session_id}, response dropped") except Exception as e: logger.error(f"Error in send method: {e}") @@ -83,57 +101,83 @@ class WebChannel(ChatChannel): def post_message(self): """ Handle incoming messages from users via POST request. + Returns a request_id for tracking this specific request. """ try: data = web.data() # 获取原始POST数据 json_data = json.loads(data) - user_id = json_data.get('user_id', 'default_user') - prompt = json_data.get('message', '') session_id = json_data.get('session_id', f'session_{int(time.time())}') - except json.JSONDecodeError: - return json.dumps({"status": "error", "message": "Invalid JSON"}) - except Exception as e: - return json.dumps({"status": "error", "message": str(e)}) - - if not prompt: - return json.dumps({"status": "error", "message": "No message provided"}) + prompt = json_data.get('message', '') - try: - msg_id = self._generate_msg_id() - web_message = WebMessage( - msg_id=msg_id, - content=prompt, - from_user_id=user_id, - to_user_id="Chatgpt", - other_user_id=user_id - ) + # 生成请求ID + request_id = self._generate_request_id() - context = self._compose_context(ContextType.TEXT, prompt, msg=web_message) - if not context: - return json.dumps({"status": "error", "message": "Failed to process message"}) - - # 创建一个响应队列 - response_queue = Queue() + # 将请求ID与会话ID关联 + self.request_to_session[request_id] = session_id - # 确保上下文包含必要的信息 - context["isgroup"] = False - context["receiver"] = user_id - context["session_id"] = user_id - context["response_queue"] = response_queue - - # 发送消息到处理队列 - self.produce(context) + # 确保会话队列存在 + if session_id not in self.session_queues: + self.session_queues[session_id] = Queue() - # 等待响应,最多等待30秒 - try: - response = response_queue.get(timeout=120) - return json.dumps({"status": "success", "reply": response["content"]}) - except Empty: - return json.dumps({"status": "error", "message": "Response timeout"}) + # 创建消息对象 + msg = WebMessage(self._generate_msg_id(), prompt) + msg.from_user_id = session_id # 使用会话ID作为用户ID + + # 创建上下文 + context = self._compose_context(ContextType.TEXT, prompt, msg=msg) + + # 添加必要的字段 + context["session_id"] = session_id + context["request_id"] = request_id + context["isgroup"] = False # 添加 isgroup 字段 + context["receiver"] = session_id # 添加 receiver 字段 + + # 异步处理消息 - 只传递上下文 + threading.Thread(target=self.produce, args=(context,)).start() + + # 返回请求ID + return json.dumps({"status": "success", "request_id": request_id}) except Exception as e: logger.error(f"Error processing message: {e}") - return json.dumps({"status": "error", "message": "Internal server error"}) + return json.dumps({"status": "error", "message": str(e)}) + + def poll_response(self): + """ + Poll for responses using the session_id. + """ + try: + # 不记录轮询请求的日志 + web.ctx.log_request = False + + data = web.data() + json_data = json.loads(data) + session_id = json_data.get('session_id') + + if not session_id or session_id not in self.session_queues: + return json.dumps({"status": "error", "message": "Invalid session ID"}) + + # 尝试从队列获取响应,不等待 + try: + # 使用peek而不是get,这样如果前端没有成功处理,下次还能获取到 + response = self.session_queues[session_id].get(block=False) + + # 返回响应,包含请求ID以区分不同请求 + return json.dumps({ + "status": "success", + "has_content": True, + "content": response["content"], + "request_id": response["request_id"], + "timestamp": response["timestamp"] + }) + + except Empty: + # 没有新响应 + return json.dumps({"status": "success", "has_content": False}) + + except Exception as e: + logger.error(f"Error polling response: {e}") + return json.dumps({"status": "error", "message": str(e)}) def chat_page(self): """Serve the chat HTML page.""" @@ -153,6 +197,7 @@ class WebChannel(ChatChannel): urls = ( '/', 'RootHandler', # 添加根路径处理器 '/message', 'MessageHandler', + '/poll', 'PollHandler', # 添加轮询处理器 '/chat', 'ChatHandler', '/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径 ) @@ -163,6 +208,12 @@ class WebChannel(ChatChannel): import io from contextlib import redirect_stdout + # 配置web.py的日志级别为ERROR,只显示错误 + logging.getLogger("web").setLevel(logging.ERROR) + + # 禁用web.httpserver的日志 + logging.getLogger("web.httpserver").setLevel(logging.ERROR) + # 临时重定向标准输出,捕获web.py的启动消息 with redirect_stdout(io.StringIO()): web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) @@ -179,6 +230,11 @@ class MessageHandler: return WebChannel().post_message() +class PollHandler: + def POST(self): + return WebChannel().poll_response() + + class ChatHandler: def GET(self): # 正常返回聊天页面 diff --git a/config-template.json b/config-template.json index d0268d3..476a5e0 100644 --- a/config-template.json +++ b/config-template.json @@ -1,5 +1,5 @@ { - "channel_type": "wx", + "channel_type": "web", "model": "", "open_ai_api_key": "YOUR API KEY", "claude_api_key": "YOUR API KEY", diff --git a/simple_login_form_test.html b/simple_login_form_test.html new file mode 100644 index 0000000..286a008 --- /dev/null +++ b/simple_login_form_test.html @@ -0,0 +1,278 @@ + + + + + +登录 + + + +
+

用户登录

+
+ + +
+ + +
+ + +
+
+ + +
+ 忘记密码? +
+
+
+
+ +