#
tokens: 35372/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── bin
│   ├── libs
│   │   ├── mcpcli.py
│   │   ├── mcpez.py
│   │   └── mcphandler.py
│   └── mcpproxy.py
├── docker-compose.yaml
├── dockerfile
├── imgs
│   ├── image-1.png
│   ├── image-2.png
│   ├── image-3.png
│   ├── image-4.png
│   └── image.png
├── mcpeasy-entrypoint.sh
├── mcpez
│   └── main.py
├── mcpez_ngx.conf
├── README.md
└── webui
    ├── chat.html
    ├── edit.html
    ├── index.html
    ├── js
    │   ├── MCPEditor.js
    │   └── serviceCtl.js
    └── libs
        ├── ai
        │   └── ai.js
        └── mcpcli.js
```

# Files

--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------

```yaml
1 | version: '3.8'
2 | services:
3 |   mcpez:
4 |     image: mcpez
5 |     build:
6 |       context: .
7 |       dockerfile: dockerfile
8 |     ports:
9 |       - "8777:80"
```

--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | 
 2 | FROM alpine:latest
 3 | 
 4 | RUN apk update
 5 | RUN apk add bash openresty openrc python3 py3-pip sqlite pipx nodejs npm 
 6 | RUN pipx install uv 
 7 | RUN pipx ensurepath
 8 | ENV PATH="/root/.local/bin:$PATH"
 9 | RUN mkdir -p /data/app
10 | WORKDIR /data/app
11 | RUN uv venv
12 | RUN uv pip install tornado fastapi uvicorn sqlmodel httpx httpx-sse 
13 | RUN mkdir -p /var/run/mcpez
14 | COPY ./mcpeasy-entrypoint.sh /mcpeasy-entrypoint.sh
15 | COPY mcpez_ngx.conf /etc/nginx/mcpez.conf
16 | COPY ./bin /data/app/bin 
17 | COPY ./mcpez /data/app/mcpez
18 | COPY ./webui /data/app/webui
19 | RUN chmod +x /mcpeasy-entrypoint.sh 
20 | ENTRYPOINT ["/mcpeasy-entrypoint.sh"]
```

--------------------------------------------------------------------------------
/mcpez_ngx.conf:
--------------------------------------------------------------------------------

```
 1 | worker_processes  1;
 2 | daemon on;
 3 | 
 4 | events {
 5 |     worker_connections  102400;
 6 | }
 7 | 
 8 | http {
 9 |     include       mime.types;
10 |     default_type  application/octet-stream;
11 | 
12 |     sendfile        on;
13 |     keepalive_timeout  65;
14 | 
15 |     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
16 |                      '$status $body_bytes_sent "$http_referer" '
17 |                      '"$http_user_agent" "$http_x_forwarded_for"';
18 | 
19 |     server {
20 |         listen       80;
21 | 
22 |         access_log  /var/log/nginx/access.log  main;
23 | 	    error_log   /var/log/nginx/error.log  error;
24 | 
25 |         location / {
26 |             # 界面界面
27 |             root /data/app/webui;
28 |             index index.html;
29 |         }
30 | 
31 |         location /api {
32 |             # API 代理设置
33 |             rewrite ^/api/(.*)$ /$1 break;
34 |             proxy_pass http://127.0.0.1:8000;
35 |             proxy_set_header Host $host;
36 |             proxy_set_header X-Real-IP $remote_addr;
37 |             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38 |             proxy_set_header X-Forwarded-Proto $scheme;
39 |             proxy_connect_timeout 5s;
40 |         }
41 | 
42 | 
43 |         location /mcp/ {
44 |             # MCP 代理设置,真的mcp都会在这里被挂载
45 |             set $mcp_upstream "unix:/tmp";
46 |             set $original_uri $request_uri;
47 | 
48 |             keepalive_timeout 180;
49 | 
50 |             rewrite_by_lua_block {
51 | 
52 |                 local captures, err = ngx.re.match(ngx.var.request_uri, "^/mcp/([a-zA-Z0-9\\-_]+)(/.*)?","jio")
53 |                 if not captures then
54 |                     ngx.status = ngx.HTTP_BAD_REQUEST
55 |                     ngx.log(ngx.ERR, "Invalid request format: ", ngx.var.request_uri)
56 |                     ngx.say("Invalid request format")
57 |                     return
58 |                 end
59 | 
60 |                 local key = captures[1]
61 |                 local path_suffix = captures[2] or "/"
62 | 
63 | 
64 |                 ngx.var.mcp_upstream = "unix:/var/run/mcpez/"..key..".sock"
65 |             }
66 | 
67 |             # 代理设置
68 |             proxy_pass http://$mcp_upstream;
69 |             proxy_set_header Host $host;
70 |             proxy_set_header X-Real-IP $remote_addr;
71 |             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
72 |             proxy_set_header X-Forwarded-Proto $scheme;
73 |             proxy_connect_timeout 5s;
74 |             proxy_buffering off;
75 |         }
76 | 
77 |     }
78 | }
```

--------------------------------------------------------------------------------
/bin/libs/mcpez.py:
--------------------------------------------------------------------------------

```python
 1 | from libs.mcpcli import MCPCli
 2 | from libs.mcphandler import make_mcp_handlers
 3 | import copy
 4 | import os
 5 | 
 6 | class MCPEazy:
 7 | 
 8 |     def __init__(self, name, description=''):
 9 |         self.servers = {}
10 |         self.name = name
11 |         self.description = description
12 |         self.functions = {}
13 |         self.ctxStore = None
14 |         self.pathroute = None
15 |         self.apps = None
16 | 
17 |     async def add_mcp_server(self, name, config):
18 |         subsrv = MCPCli(config)
19 |         await subsrv.init()
20 |         self.servers[name] = subsrv
21 |         return self.servers[name]
22 | 
23 |     async def add_to_server(self, app, pathroute=''):
24 |         self.apps = app
25 |         self.pathroute = pathroute
26 |         make_mcp_handlers(app, self, pathroute=pathroute, name=self.name)
27 | 
28 |     async def stop(self):
29 |         names = list(self.servers.keys())
30 |         for name in names:
31 |             srv = self.servers.pop(name, None)
32 |             srv.close()
33 |         for route in self.apps.default_router.rules[0].target.rules:
34 |             if route.target_kwargs.get('name') == self.name:
35 |                 self.apps.default_router.rules[0].target.rules.remove(route)
36 | 
37 |     def get_tools(self, openai=None):
38 |         tools = []
39 |         self.functions = {}
40 |         for srv in self.servers.values():
41 |             for tool in srv.tools:
42 |                 namehash = os.urandom(5).hex()
43 |                 self.functions[namehash] = {'name':tool['name'], 'srv':srv}
44 |                 _tool = copy.deepcopy(tool)
45 |                 _tool['name'] = namehash
46 |                 tools.append(_tool)
47 |         return tools
48 | 
49 |     async def list_tools(self, req, session):
50 |         await session.write_jsonrpc(req['id'], {'tools':self.get_tools()})
51 | 
52 |     async def call_tools(self, req, session):
53 |         name = req['params'].get('name')
54 |         if name not in self.functions:
55 |             return await session.write_jsonrpc(req['id'], {'error': {'code': -32601, 'message': f"Method {name} not found"}})
56 |         _srv = self.functions[name]
57 |         try:
58 |             req['params']['name'] = _srv['name']
59 |             result = await _srv['srv'].request('tools/call', req['params'])
60 |             return await session.write_jsonrpc(req['id'], {'result': result.get('result')})
61 |         except Exception as e:
62 |             return await session.write_jsonrpc(req['id'], {'error': {'code': -32603, 'message': str(e)}})
```

--------------------------------------------------------------------------------
/bin/mcpproxy.py:
--------------------------------------------------------------------------------

```python
 1 | import asyncio
 2 | import json
 3 | import tornado.web
 4 | import argparse
 5 | import sys, os
 6 | import logging
 7 | 
 8 | from tornado.httpserver import HTTPServer
 9 | from tornado.netutil import bind_unix_socket
10 | 
11 | from libs.mcpez import MCPEazy
12 | from sqlmodel import Field, SQLModel, Session, create_engine
13 | 
14 | logging.basicConfig(level=logging.DEBUG)
15 | 
16 | 
17 | optparser = argparse.ArgumentParser(description='MCP代理服务,支持从文件或数据库加载配置')
18 | optparser.add_argument('-c', '--id', type=int, default=0, help='app id')
19 | optparser.add_argument('-s', '--socketdir', default='/var/run/mcpez', help='服务器端口号,也可以是一个uds路径,')
20 | optparser.add_argument('-n', '--name', default='mcpproxy',help='代理服务名称')
21 | optargs = optparser.parse_args()
22 | 
23 | class AppDB(SQLModel, table=True):
24 |     id: int = Field(default=None, primary_key=True)
25 |     name: str = Field(index=True)
26 |     description: str = Field(default='')
27 |     item_type: str = Field(index=True)
28 |     config: str
29 |     functions: str = None
30 |     create_at: int = None
31 |     modify_at: int = None
32 | 
33 | 
34 | class MCPProxy(MCPEazy):
35 |     def __init__(self, name="MCPProxy"):
36 |         super().__init__(name)
37 |     
38 |     async def load_config_from_db(self, app_id):
39 |         engine = create_engine('sqlite:///./mcpez.db')
40 |         with Session(engine) as session:
41 |             try:
42 |                 app = session.get(AppDB, app_id)
43 |                 if not app: raise Exception('Config is not Found')
44 |                 if app.item_type != 'app': raise Exception('item is not a app')
45 |                 self.name = app.name
46 |                 self.description = app.description
47 |                 return json.loads(app.config)
48 |             except Exception as e:
49 |                 logging.error(e)
50 |                 sys.exit(1)
51 |     
52 |     async def load_config(self):
53 |         try:
54 |             config = await self.load_config_from_db(optargs.id)
55 |             active_servers = []
56 |             for name, server_config in config.get('mcpServers', {}).items():
57 |                 try:
58 |                     await self.add_mcp_server(name, server_config)
59 |                     active_servers.append(name)
60 |                 except Exception as e:
61 |                     logging.error(e)
62 |             
63 |             if not active_servers: raise Exception('no active servers found')
64 |             return active_servers
65 |         except Exception as e:
66 |             logging.error(f"配置加载失败: {str(e)}")
67 |             return []
68 |     
69 |     @staticmethod
70 |     async def start():
71 |         proxy = MCPProxy(name=optargs.name)
72 |         await proxy.load_config()
73 |         app = tornado.web.Application(debug=True)
74 |         await proxy.add_to_server(app, pathroute=f"/mcp/{optargs.id}")
75 |         server = HTTPServer(app)
76 |         socket_file = f"{optargs.socketdir}/{optargs.id}.sock"
77 |         server.add_socket(bind_unix_socket(socket_file))
78 |         os.system(f"chmod 777 {socket_file}")
79 |         logging.info(f"HTTP Server runing at unix:{socket_file}:/mcp/{optargs.id}/sse")
80 |         server.start()
81 | 
82 | 
83 | async def main():
84 |     await MCPProxy.start()
85 |     await asyncio.Event().wait()
86 | 
87 | 
88 | if __name__ == "__main__":
89 |     asyncio.run(main())
```

--------------------------------------------------------------------------------
/webui/libs/mcpcli.js:
--------------------------------------------------------------------------------

```javascript
  1 | (function(global) {
  2 | class MCPCli {
  3 |     constructor(server, {fetcher=fetch, timeout=60000, format='openai'}={}) {
  4 |         this.server = server||'https://mcp-main.toai.chat/sse';
  5 |         this.endpoint = null;
  6 |         this.jsonrpc_id = 0;
  7 |         this.responses = {}
  8 |         this._tools = null
  9 |         this._format = format
 10 |         this.tools = []
 11 |         this.timeout = timeout
 12 |         this.fetcher = fetcher
 13 |         this.stream = null
 14 |     }
 15 | 
 16 |     eventLoop(options) {
 17 |         const { resolve, reject, timeout } = options;
 18 |         const Stream = new EventSource(this.server);
 19 |         this.stream = Stream
 20 | 
 21 |         setTimeout(()=>{
 22 |             if(!this.endpoint) { reject('timeout');Stream.close()}
 23 |         }, timeout||this.timeout||60000)
 24 | 
 25 |         Stream.addEventListener('message', (event) => {
 26 |             const data = JSON.parse(event.data);
 27 |             this.responses[data.id] && this.responses[data.id](data)
 28 |         });
 29 | 
 30 |         Stream.addEventListener('endpoint', (event) => {
 31 |             this.endpoint = event.data.startsWith('/') ? new URL(event.data, new URL(this.server)).toString() : event.data;
 32 |             resolve(this.endpoint)
 33 |         });
 34 |     }
 35 | 
 36 |     async connect(timeout) {
 37 |         await (new Promise((resolve, reject) => { this.eventLoop({resolve, reject, timeout});}))
 38 |         await this.send('initialize', {'protocolVersion':'2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'JSMCPCli', 'version': '0.1.0'}})
 39 |         this.send('notifications/initialized', {}, {idpp:false})
 40 |         this._tools = await this.send('tools/list')
 41 |         this.tools = this.transform(this._format)
 42 |         return this
 43 |     }
 44 | 
 45 |     close() {
 46 |         this.stream.close()
 47 |     }
 48 | 
 49 |     send(method, params={}, args={}) {
 50 |         const { idpp=true, timeout=this.timeout } = args;
 51 |         return new Promise((resolve, reject) => {
 52 |             idpp ? this.jsonrpc_id++ : null
 53 |             const bodyObject = { jsonrpc: '2.0', id: this.jsonrpc_id, method: method, params: params }
 54 |             setTimeout(()=>{
 55 |                 if (!this.responses[this.jsonrpc_id]) return
 56 |                 delete this.responses[this.jsonrpc_id]
 57 |                 reject('timeout')
 58 |             }, timeout);
 59 |             this.responses[this.jsonrpc_id] = (data) => {
 60 |                 if (!this.responses[this.jsonrpc_id]) return
 61 |                 delete this.responses[this.jsonrpc_id]
 62 |                 resolve(data.result)
 63 |             }
 64 |             fetch(this.endpoint, {method: 'POST', body: JSON.stringify(bodyObject),headers: { 'Content-Type': 'application/json' }})
 65 |         })
 66 |     }
 67 | 
 68 |     transform (format) {
 69 |         if (!this._tools) return []
 70 |         if (format === 'claude') return this._tools.tools
 71 |         if (format === 'openai') {
 72 |             return this._tools.tools.map(tool => {
 73 |                 return {
 74 |                     type: 'function',
 75 |                     function: {
 76 |                         name: tool.name,
 77 |                         description: tool.description,
 78 |                         parameters: {
 79 |                             type: 'object',
 80 |                             properties: tool.inputSchema.properties,
 81 |                             required: tool.inputSchema.required,
 82 |                             additionalProperties: false
 83 |                         },
 84 |                         strict: true
 85 |                     },
 86 |                 }
 87 |             })
 88 |         }
 89 |     }
 90 | 
 91 |     execute(name, args) {
 92 |         return this.send('tools/call', {name:name, arguments:args})
 93 |     }
 94 | 
 95 |   }
 96 | 
 97 |   global.MCPCli = MCPCli;
 98 |   if (typeof module !== 'undefined' && module.exports) {
 99 |     module.exports = { MCPCli };
100 |   } else if (typeof define === 'function' && define.amd) {
101 |     define(function() { return { MCPCli }; });
102 |   }
103 |   
104 | })(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);
105 | 
106 | //(new MCPCli()).connect().then(cli=>cli.execute('get_stock_price', {symbol:'AAPL'})).then(console.log).catch(console.error)
107 | 
```

--------------------------------------------------------------------------------
/bin/libs/mcphandler.py:
--------------------------------------------------------------------------------

```python
  1 | from tornado.web import RequestHandler
  2 | import json
  3 | import os
  4 | import logging
  5 | import time
  6 | 
  7 | 
  8 | class SSEServer(RequestHandler):
  9 | 
 10 |     def initialize(self, *args, **kwargs):
 11 |         self._auto_finish = False
 12 |         self.ctxStore = kwargs.get('ctxStore')
 13 |         self.pathroute = kwargs.get('pathroute', '')
 14 | 
 15 |     def set_default_headers(self):
 16 |         self.set_header('Content-Type', 'text/event-stream')
 17 |         self.set_header('Cache-Control', 'no-cache')
 18 |         self.set_header('Connection', 'keep-alive')
 19 |         self.set_header('Access-Control-Allow-Origin', '*')
 20 |         self.set_header('Access-Control-Allow-Headers', 'Content-Type')
 21 |         self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
 22 | 
 23 |     def options(self): self.set_status(204)
 24 | 
 25 |     async def get(self):
 26 |         self.ctxid = os.urandom(16).hex()
 27 |         self.ctxStore[self.ctxid] = self
 28 |         await self.write_sse(self.pathroute+'/messages/?session_id=' + self.ctxid, 'endpoint')
 29 | 
 30 |     async def write_sse(self, data, event='message'):
 31 |         self.write('event: ' + event + '\r\ndata: ' + data + '\r\n\r\n')
 32 |         await self.flush()
 33 | 
 34 |     async def write_jsonrpc(self, req_id, result):
 35 |         response = {'jsonrpc': '2.0', 'id': req_id, 'result': result}
 36 |         await self.write_sse( json.dumps(response) )
 37 | 
 38 |     def on_connection_close(self):
 39 |         if not hasattr(self, 'ctxid'): return
 40 |         self.ctxStore.pop(self.ctxid, None)
 41 | 
 42 | 
 43 | 
 44 | 
 45 | class RPCServer(RequestHandler):
 46 | 
 47 |     def initialize(self, *args, **kwargs):
 48 |         self.executor = kwargs.get('executor')
 49 |         self.ctxStore = kwargs.get('ctxStore')
 50 | 
 51 |     def set_default_headers(self):
 52 |         self.set_header('Content-Type', 'text/event-stream')
 53 |         self.set_header('Cache-Control', 'no-cache')
 54 |         self.set_header('Connection', 'keep-alive')
 55 |         self.set_header('Access-Control-Allow-Origin', '*')
 56 |         self.set_header('Access-Control-Allow-Headers', 'Content-Type')
 57 |         self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
 58 | 
 59 |     def options(self): self.set_status(204)
 60 | 
 61 |     async def post(self):
 62 |         ctxid = self.get_argument('session_id')
 63 |         session = self.ctxStore.get(ctxid)
 64 |         req = json.loads(self.request.body)
 65 |         req_method = req.get('method')
 66 |         func = self.for_name(req_method)
 67 |         if func: await func(req, session)
 68 |         self.set_status(202)
 69 |         self.finish('Accepted')
 70 | 
 71 |     def for_name(self, method):
 72 |         return getattr(self, 'with_' + method.replace('/', '_'), None)
 73 | 
 74 |     async def with_initialize(self, req, session):
 75 |         req_id = req.get('id')
 76 |         result = {"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":False},"resources":{"subscribe":False,"listChanged":False},"tools":{"listChanged":False}},"serverInfo":{"name":"mcpsrv","version":"1.3.0"}}
 77 |         await session.write_jsonrpc(req_id, result)
 78 | 
 79 |     async def with_tools_list(self, req, session):
 80 |         try:
 81 |             self.executor and await self.executor.list_tools(req, session)
 82 |         except Exception as e:
 83 |             await session.write_jsonrpc(req['id'], {'tools': []})
 84 |     
 85 |     async def with_ping(self, req, session):
 86 |         req_id = req.get('id')
 87 |         result = {}
 88 |         await session.write_jsonrpc(req_id, result)
 89 | 
 90 |     async def with_tools_call(self, req, session):
 91 |         try:
 92 |             self.executor and await self.executor.call_tools(req, session)
 93 |         except Exception as e:
 94 |             await session.write_jsonrpc(req['id'], {'result': 'error'})
 95 | 
 96 | 
 97 | 
 98 | 
 99 | 
100 | 
101 | 
102 | class ServerStatus(RequestHandler):
103 |     def initialize(self, *args, **kwargs):
104 |         self.ctxStore = kwargs.get('ctxStore')
105 |         self.init_time = kwargs.get('init_time')
106 |         self.name = kwargs.get('name')
107 |         self.executor = kwargs.get('executor')
108 | 
109 | 
110 |     def set_default_headers(self):
111 |         self.set_header('Content-Type', 'application/json')
112 |         self.set_header('Cache-Control', 'no-cache')
113 |         self.set_header('Connection', 'keep-alive')
114 |         self.set_header('Access-Control-Allow-Origin', '*')
115 |         self.set_header('Access-Control-Allow-Headers', 'Content-Type')
116 |         self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
117 | 
118 |     def options(self): self.set_status(204)
119 | 
120 |     async def get(self):
121 |         self.finish(dict(
122 |             name = self.name,
123 |             description = self.executor.description,
124 |             init_time = self.init_time,
125 |             status = 'ok',
126 |             connection_cnt = len(self.ctxStore),
127 |             tools = self.executor.get_tools()
128 |             ))
129 | 
130 | 
131 | 
132 | 
133 | def make_mcp_handlers(application, executor, pathroute='',name=''):
134 |     ctxStore = {}
135 |     executor.ctxStore = ctxStore
136 |     executor.pathroute = pathroute
137 |     application.add_handlers('.*', [
138 |         (pathroute + '/sse', SSEServer, {'ctxStore': ctxStore, 'pathroute': pathroute, 'name':name}),
139 |         (pathroute + '/messages/', RPCServer, {'executor': executor, 'ctxStore': ctxStore, 'name':name}),
140 |         (pathroute + '/server_status', ServerStatus, {'ctxStore': ctxStore,  'executor': executor, 'name': name, 'init_time': int(time.time())}),
141 |     ])
```

--------------------------------------------------------------------------------
/bin/libs/mcpcli.py:
--------------------------------------------------------------------------------

```python
  1 | import asyncio
  2 | import json
  3 | import logging
  4 | 
  5 | 
  6 | class MCPCli:
  7 | 
  8 |     def __init__(self, config, **kwargs):
  9 |         self.rpcid = 0
 10 |         self.rpc_responses = {}
 11 |         self.config = config
 12 |         self.name = kwargs.get('name')
 13 |         self.timeout = kwargs.get('timeout', 60)
 14 |         self.config['type'] = 'sse' if config.get('baseUrl') else 'stdio'
 15 |         self.write = None
 16 |         self.tools = None
 17 |         self.process = None
 18 |         logging.info('created Mcpcli instance')
 19 | 
 20 | 
 21 |     async def request(self, method, params, with_response=True, with_timeout=None):
 22 |         assert self.write, "Connection not established"
 23 |         
 24 |         if with_response:
 25 |             self.rpcid += 1
 26 |             json_rpc_data = {'method': method, 'params': params, 'jsonrpc': '2.0', 'id': self.rpcid}
 27 |             fut = asyncio.Future()
 28 |             self.rpc_responses[self.rpcid] = fut
 29 |             logging.debug(f"Sending request: {json_rpc_data}")
 30 |             await self.write(json.dumps(json_rpc_data).encode() + b'\n')
 31 |             try:
 32 |                 return await asyncio.wait_for(fut, timeout=with_timeout or self.timeout)
 33 |             except asyncio.TimeoutError:
 34 |                 raise TimeoutError(f"Timeout waiting for response to {method}({params})")
 35 |         else:
 36 |             json_rpc_data = {'method': method, 'params': params, 'jsonrpc': '2.0'}
 37 |             await self.write(json.dumps(json_rpc_data).encode() + b'\n')
 38 | 
 39 | 
 40 |     async def init(self):
 41 |         if self.config['type'] == 'stdio':
 42 |             await self.start_stdio_mcp()
 43 |         elif self.config['type'] == 'sse':
 44 |             await self.start_sse()
 45 | 
 46 | 
 47 |     def close(self):
 48 |         if self.config['type'] == 'stdio':
 49 |             self.process.terminate()
 50 |         elif self.config['type'] == 'sse':
 51 |             asyncio.ensure_future(self.process.aclose())
 52 | 
 53 | 
 54 |     async def start_sse(self):
 55 |         import urllib.parse
 56 |         from httpx_sse import EventSource
 57 |         import httpx
 58 |         client = httpx.AsyncClient(verify=False, timeout=httpx.Timeout(None, connect=10.0))
 59 |         session_addr = None
 60 |         is_connected = asyncio.Future()
 61 | 
 62 |         async def start_loop():
 63 |             try:
 64 |                 async with client.stream('GET', self.config['baseUrl']) as response:
 65 |                     self.process = client
 66 |                     event_source = EventSource(response)
 67 |                     async for event in event_source.aiter_sse():
 68 |                         if client.is_closed: break
 69 |                         if event.event == 'endpoint':
 70 |                             session_addr = event.data if event.data.startswith('http') else urllib.parse.urljoin(self.config['baseUrl'], event.data)
 71 |                             is_connected.set_result(session_addr)
 72 |                         elif event.event == 'message':
 73 |                             try:
 74 |                                 data = json.loads(event.data)
 75 |                                 if data.get('id') and data.get('result'):
 76 |                                     future = self.rpc_responses.get(data['id'])
 77 |                                     if future: future.set_result(data)
 78 |                             except json.JSONDecodeError:
 79 |                                 logging.warning(f"Failed to decode JSON: {repr(event.data)}")
 80 |             except Exception as e:
 81 |                 await client.aclose()
 82 | 
 83 |         def writer(data):
 84 |             return client.post(session_addr, data=data)
 85 | 
 86 |         
 87 |         asyncio.ensure_future(start_loop())
 88 |         session_addr = await is_connected
 89 |         self.write = writer
 90 |         await self.request('initialize', {'protocolVersion':'2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'EzMCPCli', 'version': '0.1.2'}}, with_response=False)
 91 |         await self.request('notifications/initialized', {}, with_response=False)
 92 |         r = await self.request('tools/list', {}, with_timeout=10)
 93 |         self.tools = r.get('result', {}).get('tools', [])
 94 | 
 95 | 
 96 |     
 97 |     async def start_stdio_mcp(self):
 98 |         proc = await asyncio.create_subprocess_exec(
 99 |             self.config['command'],
100 |             *self.config['args'],
101 |             env=self.config.get('env', None),
102 |             stdin=asyncio.subprocess.PIPE,
103 |             stdout=asyncio.subprocess.PIPE,
104 |             stderr=asyncio.subprocess.PIPE
105 |         )
106 |         self.process = proc
107 | 
108 |         async def writer(data):
109 |             proc.stdin.write(data)
110 | 
111 |         self.write = writer
112 | 
113 |         async def start_loop():
114 |             while True:
115 |                 line = await proc.stdout.readline()
116 |                 if not line:
117 |                     break
118 |                 try:
119 |                     data = json.loads(line.decode())
120 |                     if 'id' in data and data['id'] in self.rpc_responses:
121 |                         self.rpc_responses[data['id']].set_result(data)
122 |                         del self.rpc_responses[data['id']]
123 |                     else:
124 |                         logging.debug(f"Received notification: {data}")
125 |                 except json.JSONDecodeError as e:
126 |                     logging.warning(f"Failed to decode JSON: {repr(line)}")
127 | 
128 |         asyncio.ensure_future(start_loop())
129 | 
130 |         await self.request('initialize', {'protocolVersion':'2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'EzMCPCli', 'version': '0.1.2'}}, with_response=False)
131 |         await self.request('notifications/initialized', {}, with_response=False)
132 |         r = await self.request('tools/list', {}, with_timeout=30)
133 |         self.tools = r.get('result', {}).get('tools', [])
134 | 
135 | 
```

--------------------------------------------------------------------------------
/webui/libs/ai/ai.js:
--------------------------------------------------------------------------------

```javascript
  1 | // Version: 1.1.0
  2 | (function(global) {
  3 |   class AI {
  4 |     constructor(options = {}) {
  5 |       this.api_key = options.api_key || options.apiKey || '';
  6 |       this.baseURL = options.baseURL || options.endpoint || 'https://porky.toai.chat/to/openrouter';
  7 |       this.completionsURI = options.completionsURI === undefined ? '/chat/completions' : '';
  8 |       this.model = options.model || 'deepseek/deepseek-r1-distill-qwen-32b:free';
  9 |       this.messages = options.messages || [];
 10 |       this.system_prompt = options.system_prompt || options.sysprompt || '';
 11 |       this.abortController = null;
 12 |       this.toolFn = options.tool_fn || options.mcpserver || null;
 13 |       this.opts = { temperature: 0.7, max_tokens: 128000, top_p: 1, stream: true, ...options.opts };
 14 |       this.completions = { create: this.create.bind(this) };
 15 |       this.filters = {}; 
 16 | 
 17 |       this.add_response_filter('reasoning', data=>!!data.choices?.[0]?.delta?.reasoning);
 18 |       
 19 |       return this;
 20 |     }
 21 |     
 22 |     add_response_filter(queue_name, filterFn) {
 23 |       if (!this.filters[queue_name]) this.filters[queue_name] = [];
 24 |       this.filters[queue_name].push(filterFn);
 25 |       return this;
 26 |     }
 27 |     
 28 |     cancel() { 
 29 |       if (this.abortController) {
 30 |         this.abortController.abort();
 31 |         this.abortController = null;
 32 |       }
 33 |     }
 34 |     
 35 |     async create(prompt, args) {
 36 |       const { options={}, images=[], audio=[], files=[] } = args || {};
 37 | 
 38 |       const messages = [...(options.messages || this.messages || [])];
 39 |       if (this.system_prompt) messages.unshift({ role: 'system', content: this.system_prompt });
 40 |       if (prompt || images?.length || audio?.length || files?.length) 
 41 |         messages.push(this.prepareUserMessage(prompt, { images, audio, files }));
 42 |       
 43 |       const reqOptions = {
 44 |         model: options.model || this.model,
 45 |         messages,
 46 |         temperature: options.temperature || this.opts.temperature,
 47 |         max_tokens: options.max_tokens || this.opts.max_tokens,
 48 |         top_p: options.top_p || this.opts.top_p,
 49 |         stream: options.stream !== undefined ? options.stream : this.opts.stream,
 50 |         ...(this.toolFn?.tools && { tools: this.toolFn.tools, tool_choice: options.tool_choice || "auto" })
 51 |       };
 52 |       
 53 |       this.abortController = new AbortController();
 54 |       
 55 |       try {
 56 |         const response = await fetch(`${this.baseURL}${this.completionsURI}`, {
 57 |           method: 'POST', 
 58 |           headers: {
 59 |             'Content-Type': 'application/json',
 60 |             ...(this.api_key ? { 'Authorization': `Bearer ${this.api_key}` } : {})
 61 |           },
 62 |           body: JSON.stringify(reqOptions),
 63 |           signal: this.abortController.signal
 64 |         });
 65 |         
 66 |         if (!response.ok) {
 67 |           const error = await response.json().catch(() => ({}));
 68 |           throw new Error(`API错误: ${response.status} ${error.error?.message || ''}`);
 69 |         }
 70 |         
 71 |         if (!reqOptions.stream) {
 72 |           const result = await response.json();
 73 |           return new ResponseIterator({ 
 74 |             type: 'final', 
 75 |             message: result.choices?.[0]?.message || { role: 'assistant', content: '' }, 
 76 |             ai: this, final: true,
 77 |             filters: this.filters
 78 |           });
 79 |         }
 80 |         
 81 |         const iter = new ResponseIterator({ ai: this, reqOptions, filters: this.filters });
 82 |         this.processStream(response, iter);
 83 |         return iter;
 84 |       } catch (error) { throw error; }
 85 |     }
 86 |     
 87 |     async processStream(response, iter) {
 88 |       const reader = response.body.getReader();
 89 |       const decoder = new TextDecoder();
 90 |       const msg = { role: 'assistant', content: '' }; // 移除 reasoning_content 字段
 91 |       const toolCalls = [];
 92 |       let buffer = '';
 93 |       
 94 |       try {
 95 |         while (true) {
 96 |           const { done, value } = await reader.read();
 97 |           if (done) break;
 98 |           
 99 |           buffer += decoder.decode(value, { stream: true });
100 |           const lines = buffer.split('\n');
101 |           buffer = lines.pop() || '';
102 |           
103 |           for (const line of lines) {
104 |             const event = this.parseSSELine(line);
105 |             if (!event) continue;
106 |             
107 |             if (event.isDone) {
108 |               await this.finalizeResponse(msg, toolCalls, iter);
109 |               return;
110 |             }
111 |             
112 |             await this.processEvent(event, msg, toolCalls, iter);
113 |           }
114 |         }
115 |         
116 |         // 处理最后可能的缓冲数据
117 |         if (buffer.trim()) {
118 |           const event = this.parseSSELine(buffer);
119 |           if (event && !event.isDone) {
120 |             await this.processEvent(event, msg, toolCalls, iter);
121 |           }
122 |         }
123 |         
124 |         await this.finalizeResponse(msg, toolCalls, iter);
125 |       } catch (error) {
126 |         iter.error(error);
127 |       } finally {
128 |         reader.releaseLock();
129 |         this.abortController = null;
130 |       }
131 |     }
132 |     
133 |     parseSSELine(line) {
134 |       line = line.trim();
135 |       if (!line) return null;
136 |       
137 |       // 检测[DONE]标记
138 |       if (line === 'data: [DONE]') {
139 |         return { isDone: true };
140 |       }
141 |       
142 |       // 解析数据
143 |       let eventType = '';
144 |       let data = '';
145 |       
146 |       if (line.startsWith('event:')) {
147 |         eventType = line.slice(6).trim();
148 |       } else if (line.startsWith('data:')) {
149 |         data = line.slice(5).trim();
150 |         try {
151 |           return { type: eventType || 'content', data: JSON.parse(data), isDone: false };
152 |         } catch (e) {
153 |           return { type: eventType || 'content', data, isDone: false, raw: true };
154 |         }
155 |       } else {
156 |         try {
157 |           return { type: 'content', data: JSON.parse(line), isDone: false };
158 |         } catch (e) {
159 |           return { type: 'content', data: line, isDone: false, raw: true };
160 |         }
161 |       }
162 |       
163 |       return null;
164 |     }
165 |     
166 |     async processEvent(event, msg, toolCalls, iter) {
167 |       const { type, data, raw } = event;
168 | 
169 |       // 应用过滤器并推送到队列
170 |       Object.keys(this.filters).forEach((filter_name)=>{
171 |         const filter = this.filters[filter_name];
172 |         const shouldpush = filter.some(f=>f(data))
173 |         if (shouldpush) {
174 |           iter.push(filter_name, { type: filter_name, message: {...msg}, delta: data });
175 |         }
176 |       })
177 | 
178 |       // 处理对象数据
179 |       if (!raw && typeof data === 'object') {
180 |         // 处理工具调用
181 |         if (data.choices && data.choices[0]?.delta?.tool_calls) {
182 |           this.updateToolCalls(data.choices[0].delta.tool_calls, toolCalls);
183 |           iter.push('tool_calls', { type: 'tool_calls', toolCalls: [...toolCalls], message: {...msg}, delta: data });
184 | 
185 |           if (data.choices[0].finish_reason === 'tool_calls') {
186 |             msg.tool_calls = [...toolCalls];
187 |             iter.push('tool_request', { type: 'tool_request', toolCalls: [...toolCalls], message: {...msg} });
188 |             return await this.handleToolCalls(msg, toolCalls, iter);
189 |           }
190 |           return;
191 |         }
192 | 
193 |         // 处理内容更新
194 |         const content = data.choices?.[0]?.delta?.content || '';
195 |         if (content) {
196 |           msg.content += content;
197 |           iter.push('content', { type: 'content', content, message: {...msg}, delta: content });
198 |           return;
199 |         }
200 |       } else {
201 |         // 处理字符串和其他类型的数据
202 |         switch (type) {
203 |           case 'tool_calls':
204 |             if (Array.isArray(data)) {
205 |               this.updateToolCalls(data, toolCalls);
206 |               iter.push('tool_calls', { type, toolCalls: [...toolCalls], message: {...msg}, delta: data });
207 | 
208 |               if (toolCalls.every(t => t.function?.arguments)) {
209 |                 msg.tool_calls = [...toolCalls];
210 |                 iter.push('tool_request', { type: 'tool_request', toolCalls: [...toolCalls], message: {...msg} });
211 |                 return await this.handleToolCalls(msg, toolCalls, iter);
212 |               }
213 |             }
214 |             break;
215 |           case 'content':
216 |             if (data) {
217 |               msg.content += data;
218 |               iter.push('content', { type, content: data, message: {...msg}, delta: data });
219 |             }
220 |             break;
221 |         }
222 |       }
223 |     }
224 |     
225 |     async finalizeResponse(msg, toolCalls, iter) {
226 |       if (toolCalls.length > 0 && toolCalls.every(t => t.function?.name)) {
227 |         msg.tool_calls = [...toolCalls];
228 |         iter.push('tool_request', { type: 'tool_request', toolCalls: [...toolCalls], message: {...msg} });
229 |         return await this.handleToolCalls(msg, toolCalls, iter);
230 |       } else {
231 |         iter.push('final', { type: 'final', message: msg });
232 |         iter.complete();
233 |       }
234 |     }
235 |     
236 |     async handleToolCalls(msg, toolCalls, iter) {
237 |       if (!this.toolFn) {
238 |         iter.push('final', { type: 'final', message: msg });
239 |         iter.complete();
240 |         return;
241 |       }
242 |       
243 |       const results = await Promise.all(toolCalls.map(async tool => {
244 |         try {
245 |           const args = JSON.parse(tool.function.arguments);
246 |           const result = await this.toolFn.execute(tool.function.name, args);
247 |           return {
248 |             tool_call_id: tool.id, 
249 |             role: "tool",
250 |             content: typeof result === 'string' ? result : JSON.stringify(result)
251 |           };
252 |         } catch (err) { 
253 |           return { 
254 |             tool_call_id: tool.id, 
255 |             role: "tool", 
256 |             content: JSON.stringify({ error: err.message }) 
257 |           }; 
258 |         }
259 |       }));
260 |       
261 |       iter.push('tool_response', { type: 'tool_response', toolResults: results, toolCalls });
262 |       
263 |       try {
264 |         // 继续对话,将工具结果添加到消息历史中
265 |         const nextResponse = await this.create("", { 
266 |           options: {
267 |             ...iter.reqOptions,
268 |             messages: [
269 |               ...iter.reqOptions.messages, 
270 |               {...msg},
271 |               ...results.map(r => ({ 
272 |                 role: "tool", 
273 |                 tool_call_id: r.tool_call_id, 
274 |                 content: r.content 
275 |               }))
276 |             ],
277 |             tool_choice: "auto"
278 |           }
279 |         });
280 |         iter.chainWith(nextResponse);
281 |       } catch (error) { iter.error(error); }
282 |     }
283 |     
284 |     prepareUserMessage(message, { images, audio, files } = {}) {
285 |       if ((!images?.length) && (!audio?.length) && (!files?.length)) 
286 |         return { role: "user", content: message };
287 |       
288 |       const content = [];
289 |       if (message?.trim()) content.push({ type: "text", text: message });
290 |       
291 |       if (images?.length) images.forEach(img => content.push({
292 |         type: "image_url", image_url: { url: img.url || img, detail: img.detail || "auto" }
293 |       }));
294 |       
295 |       if (audio?.length) audio.forEach(clip => 
296 |         content.push({ type: "audio", audio: { url: clip.url || clip } }));
297 |       
298 |       if (files?.length) files.forEach(file => 
299 |         content.push({ type: "file", file: { url: file.url || file } }));
300 |       
301 |       return { role: "user", content };
302 |     }
303 |     
304 |     updateToolCalls(deltas, toolCalls) {
305 |       if (!Array.isArray(deltas)) return;
306 |       
307 |       deltas.forEach(d => {
308 |         const idx = d.index;
309 |         if (!toolCalls[idx]) toolCalls[idx] = { 
310 |           id: d.id || '', type: 'function', function: { name: '', arguments: '' } 
311 |         };
312 |         
313 |         if (d.id) toolCalls[idx].id = d.id;
314 |         if (d.function?.name) toolCalls[idx].function.name = d.function.name;
315 |         if (d.function?.arguments) toolCalls[idx].function.arguments += d.function.arguments;
316 |       });
317 |     }
318 |   }
319 | 
320 |   class ResponseIterator {
321 |     constructor({ ai, reqOptions, type, message, final = false, filters = {} }) {
322 |       this.ai = ai;
323 |       this.reqOptions = reqOptions;
324 |       this.queues = new Map();
325 |       this.waiters = new Map();
326 |       this.chained = null;
327 |       this.completed = false;
328 |       this.filters = filters;
329 |       
330 |       // 创建所有队列 - 移除 reasoning_content
331 |       ['content', 'tool_calls', 'tool_request', 
332 |        'tool_response', 'streaming', 'final', ...Object.keys(filters)].forEach(t => {
333 |         this.queues.set(t, []);
334 |         this.waiters.set(t, []);
335 |       });
336 |       
337 |       if (final && type && message) {
338 |         this.push(type, { type, message });
339 |         this.complete();
340 |       }
341 |     }
342 |     
343 |     push(type, data) {
344 |       if (this.completed) return;
345 |       // 仅处理基本队列
346 |       const queue = this.queues.get(type) || [];
347 |       const waiters = this.waiters.get(type) || [];
348 |       queue.push(data);
349 |       if (waiters.length) waiters.shift()();
350 |     }
351 |     
352 |     complete() {
353 |       this.completed = true;
354 |       for (const waiters of this.waiters.values()) waiters.forEach(r => r());
355 |     }
356 |     
357 |     chainWith(r) { this.chained = r; this.complete(); }
358 |     error(e) { this.errorObj = e; this.complete(); }
359 |     
360 |     on(type) {
361 |       if (!this.queues.has(type)) throw new Error(`未知事件类型: ${type}`);
362 |       const self = this;
363 |       
364 |       return {
365 |         async *[Symbol.asyncIterator]() {
366 |           try {
367 |             if (self.errorObj) throw self.errorObj;
368 |             let queue = self.queues.get(type), index = 0;
369 |             
370 |             while (true) {
371 |               if (index >= queue.length) {
372 |                 if (self.completed && !self.chained) break;
373 |                 if (self.chained) { 
374 |                   for await (const item of self.chained.on(type)) yield item;
375 |                   break;
376 |                 }
377 |                 
378 |                 await new Promise(r => self.waiters.get(type).push(r));
379 |                 if (self.errorObj) throw self.errorObj;
380 |                 queue = self.queues.get(type);
381 |               } else yield queue[index++];
382 |             }
383 |           } catch (e) { throw e; }
384 |         }
385 |       };
386 |     }
387 |     
388 |     async *[Symbol.asyncIterator]() { yield* this.on('streaming'); }
389 |   }
390 | 
391 |   global.AI = AI;
392 |   if (typeof module !== 'undefined' && module.exports) {
393 |     module.exports = { AI };
394 |   } else if (typeof define === 'function' && define.amd) {
395 |     define(function() { return { AI }; });
396 |   }
397 | })(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);
```

--------------------------------------------------------------------------------
/webui/index.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="zh-CN">
  3 | <head>
  4 |     <meta charset="UTF-8">
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6 |     <title>MCP服务状态管理</title>
  7 |     <!-- 引入TailwindCSS -->
  8 |     <script src="https://cdn.tailwindcss.com"></script>
  9 |     <script>
 10 |         tailwind.config = {
 11 |             theme: {
 12 |                 extend: {
 13 |                     colors: {
 14 |                         primary: {
 15 |                             50: '#e3f2fd',
 16 |                             100: '#bbdefb',
 17 |                             200: '#90caf9',
 18 |                             300: '#64b5f6',
 19 |                             400: '#42a5f5',
 20 |                             500: '#2196f3',
 21 |                             600: '#1e88e5',
 22 |                             700: '#1976d2',
 23 |                             800: '#1565c0',
 24 |                             900: '#0d47a1'
 25 |                         },
 26 |                         secondary: '#64748b'
 27 |                     }
 28 |                 }
 29 |             }
 30 |         }
 31 |     </script>
 32 |     <!-- 引入UIkit -->
 33 |     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css" />
 34 |     <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
 35 |     <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>
 36 |     <style>
 37 |         .uk-card {
 38 |             border-radius: 1rem;
 39 |             box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
 40 |         }
 41 |         .uk-button {
 42 |             border-radius: 0.5rem;
 43 |             transition: all 0.2s ease;
 44 |         }
 45 |         .uk-button-primary {
 46 |             background: linear-gradient(145deg, #1e88e5, #1976d2);
 47 |             box-shadow: 0 4px 6px -1px rgba(30, 136, 229, 0.2);
 48 |         }
 49 |         .uk-button-primary:hover {
 50 |             transform: translateY(-1px);
 51 |             box-shadow: 0 10px 15px -3px rgba(30, 136, 229, 0.3);
 52 |         }
 53 |         .uk-input, .uk-select, .uk-textarea {
 54 |             border-radius: 0.5rem;
 55 |             transition: all 0.2s ease;
 56 |         }
 57 |         .uk-input:focus, .uk-select:focus, .uk-textarea:focus {
 58 |             box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.2);
 59 |         }
 60 |         .uk-form-label {
 61 |             font-weight: 500;
 62 |             margin-bottom: 0.5rem;
 63 |             color: #334155;
 64 |         }
 65 |         .service-table tr {
 66 |             transition: all 0.2s ease;
 67 |         }
 68 |         .service-table tr:hover {
 69 |             background-color: rgba(30, 136, 229, 0.05);
 70 |         }
 71 |         .service-table tr.active {
 72 |             background-color: rgba(30, 136, 229, 0.1);
 73 |             border-left: 4px solid #1e88e5;
 74 |         }
 75 |         .badge {
 76 |             padding: 0.25rem 0.75rem;
 77 |             border-radius: 9999px;
 78 |             font-size: 0.75rem;
 79 |             font-weight: 500;
 80 |         }
 81 |         /* 定制滚动条 */
 82 |         ::-webkit-scrollbar {
 83 |             width: 6px;
 84 |             height: 6px;
 85 |         }
 86 |         ::-webkit-scrollbar-track {
 87 |             background: #f1f1f1;
 88 |             border-radius: 3px;
 89 |         }
 90 |         ::-webkit-scrollbar-thumb {
 91 |             background: #c1c1c1;
 92 |             border-radius: 3px;
 93 |         }
 94 |         ::-webkit-scrollbar-thumb:hover {
 95 |             background: #a8a8a8;
 96 |         }
 97 |         @media (max-width: 959px) {
 98 |             .mobile-full-width {
 99 |                 width: 100% !important;
100 |             }
101 |         }
102 |     </style>
103 | </head>
104 | <body class="bg-slate-100 min-h-screen">
105 |     <!-- 导航栏 -->
106 |     <nav class="bg-gradient-to-r from-primary-700 to-primary-500 shadow-lg">
107 |         <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
108 |             <div class="flex justify-between h-16">
109 |                 <div class="flex items-center text-white">
110 |                     <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
111 |                         <path stroke-linecap="round" stroke-linejoin="round" d="m20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525" />
112 |                     </svg>                      
113 |                     <span class="ml-2 text-xl font-bold">MCP服务状态管理</span>
114 |                 </div>
115 |                 <div class="flex items-center gap-6">
116 |                     <div class="relative">
117 |                         <input type="text" id="searchApp" class="border pl-10 h-10 rounded-xl border-gray-200 bg-white bg-opacity-20 text-white placeholder-gray-300 focus:border-white focus:ring focus:ring-white focus:ring-opacity-20 transition-all" placeholder="搜索应用...">
118 |                         <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
119 |                             <svg class="w-5 h-5 text-gray-300" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
120 |                                 <path d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
121 |                             </svg>
122 |                         </div>
123 |                     </div>
124 |                     <a class="hover:text-white hover:no-underline bg-white bg-opacity-20 hover:bg-opacity-30 text-white rounded-lg px-4 py-2 flex items-center transition-all" href="chat.html" target="_blank">
125 |                         <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 mr-2">
126 |                             <path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" />
127 |                         </svg>                          
128 |                         PlayGround
129 |                     </a>
130 |                 </div>
131 |             </div>
132 |         </div>
133 |     </nav>
134 | 
135 |     <!-- 主界面 -->
136 |     <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
137 |         <div class="bg-white rounded-2xl shadow-xl p-6 overflow-hidden">
138 |             <div class="flex justify-between items-center mb-6">
139 |                 <h2 class="text-2xl font-bold text-gray-800">应用服务列表</h2>
140 |                 <div><a href="edit.html" target="_blank" class="uk-button-small border rounded-lg py-2 hover:text-gray-900 hover:no-underline"> 新建服务 </a> </div>
141 |             </div>
142 | 
143 |             <!-- 加载指示器 -->
144 |             <div id="statusLoading" class="flex justify-center items-center py-8 hidden">
145 |                 <div uk-spinner></div>
146 |                 <span class="ml-3">加载服务状态...</span>
147 |             </div>
148 | 
149 |             <div class="overflow-x-auto max-h-[600px] overflow-y-auto">
150 |                 <table class="w-full text-left service-table">
151 |                     <thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider sticky top-0 z-10">
152 |                         <tr>
153 |                             <th class="px-4 py-3 rounded-tl-lg">应用ID</th>
154 |                             <th class="px-4 py-3">名称</th>
155 |                             <th class="px-4 py-3">描述</th>
156 |                             <th class="px-4 py-3">状态</th>
157 |                             <th class="px-4 py-3">详情</th>
158 |                             <th class="px-4 py-3 rounded-tr-lg text-right">操作</th>
159 |                         </tr>
160 |                     </thead>
161 |                     <tbody id="serviceTableBody" class="divide-y divide-gray-100">
162 |                         <!-- 应用列表项会动态添加到这里 -->
163 |                     </tbody>
164 |                 </table>
165 |             </div>
166 | 
167 |             <div id="noServicesMessage" class="text-center py-10 text-gray-500">
168 |                 还没有可运行的MCP服务,请开始 <a href="edit.html" target="_blank">[新建服务]</a>
169 |             </div>
170 |             
171 |         </div>
172 |     </div>
173 | 
174 |     <!-- 应用详情模态框 -->
175 |     <div id="app-details-modal" uk-modal>
176 |         <div class="uk-modal-dialog uk-modal-body rounded-xl w-[80%]">
177 |             <button class="uk-modal-close-default" type="button" uk-close></button>
178 |             <h2 class="text-2xl font-bold mb-4" id="appModalTitle">应用详情</h2>
179 |             
180 |             <div class="space-y-4">
181 |                 <div>
182 |                     <h3 class="text-sm font-medium text-gray-500">应用描述</h3>
183 |                     <p id="appModalDescription" class="mt-1 text-gray-900">加载中...</p>
184 |                 </div>
185 |                 
186 |                 <div>
187 |                     <h3 class="text-sm font-medium text-gray-500">服务配置</h3>
188 |                     <div class="mt-2 bg-gray-50 p-4 rounded-lg">
189 |                         <pre id="appModalConfig" class="whitespace-pre-wrap overflow-x-auto text-xs">加载中...</pre>
190 |                     </div>
191 |                 </div>
192 |             </div>
193 |             
194 |             <div class="flex justify-end space-x-4 mt-6">
195 |                 <button type="button" id="startServiceBtn" class="inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
196 |                     <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
197 |                         <path d="M8 5v14l11-7-11-7z" fill="currentColor"/>
198 |                     </svg>
199 |                     启动服务
200 |                 </button>
201 |                 <button type="button" class="uk-modal-close inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium rounded-lg transition-colors">
202 |                     <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
203 |                         <path d="M6 18L18 6M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
204 |                     </svg>
205 |                     关闭
206 |                 </button>
207 |             </div>
208 |         </div>
209 |     </div>
210 | 
211 |     <!-- 服务详情模态框 -->
212 |     <div id="service-details-modal" uk-modal>
213 |         <div class="uk-modal-dialog uk-modal-body rounded-xl w-[80%]">
214 |             <button class="uk-modal-close-default" type="button" uk-close></button>
215 |             <h2 class="text-2xl font-bold mb-4">服务状态详情</h2>
216 |             
217 |             <div id="serviceStatusLoading" class="flex justify-center items-center py-8">
218 |                 <div uk-spinner></div>
219 |                 <span class="ml-3">加载服务状态...</span>
220 |             </div>
221 |             
222 |             <div id="serviceStatusContent" class="space-y-4 hidden">
223 |                 <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
224 |                     <div>
225 |                         <h3 class="text-sm font-medium text-gray-500">服务ID</h3>
226 |                         <p id="serviceModalId" class="mt-1 text-gray-900">-</p>
227 |                     </div>
228 |                     
229 |                     <div>
230 |                         <h3 class="text-sm font-medium text-gray-500">当前状态</h3>
231 |                         <div id="serviceModalStatus" class="mt-1">-</div>
232 |                     </div>
233 |                     
234 |                     <div>
235 |                         <h3 class="text-sm font-medium text-gray-500">服务地址</h3>
236 |                         <p id="serviceModalAddress" class="mt-1 text-gray-900 break-all">-</p>
237 |                     </div>
238 |                 </div>
239 |                 
240 |                 <div>
241 |                     <h3 class="text-sm font-medium text-gray-500">详细信息</h3>
242 |                     <div class="mt-2 bg-gray-50 p-4 rounded-lg">
243 |                         <pre id="serviceModalDetails" class="whitespace-pre-wrap overflow-x-auto text-xs">无可用信息</pre>
244 |                     </div>
245 |                 </div>
246 |             </div>
247 |             
248 |             <div id="serviceStatusError" class="bg-red-50 text-red-600 p-4 rounded-lg mb-4 hidden">
249 |                 无法获取服务状态信息
250 |             </div>
251 |             
252 |             <div class="flex justify-end space-x-4 mt-6">
253 |                 <button type="button" id="stopServiceDetailsBtn" class="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">
254 |                     <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
255 |                         <path d="M6 6h12v12H6z" fill="currentColor"/>
256 |                     </svg>
257 |                     停止服务
258 |                 </button>
259 |                 <button type="button" id="refreshServiceDetailsBtn" class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors">
260 |                     <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
261 |                         <path d="M4 4V9H4.58152M19.9381 11C19.446 7.05369 16.0796 4 12 4C8.64262 4 5.76829 6.06817 4.58152 9M4.58152 9H9M20 20V15H19.4185M19.4185 15C18.2317 17.9318 15.3574 20 12 20C7.92038 20 4.55399 16.9463 4.06189 13M19.4185 15H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
262 |                     </svg>
263 |                     刷新
264 |                 </button>
265 |                 <button type="button" class="uk-modal-close inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium rounded-lg transition-colors">
266 |                     关闭
267 |                 </button>
268 |             </div>
269 |         </div>
270 |     </div>
271 | 
272 |     <!-- 引入控制器脚本 -->
273 |     <script src="js/serviceCtl.js"></script>
274 |     <script>
275 |         document.addEventListener('DOMContentLoaded', function() {
276 |             // 初始化服务控制器
277 |             const controller = new ServiceController();
278 |             controller.init();
279 |         });
280 |     </script>
281 | </body>
282 | </html>
283 | 
```

--------------------------------------------------------------------------------
/webui/edit.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="zh-CN">
  3 | <head>
  4 |     <meta charset="UTF-8">
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6 |     <title>MCP应用编辑器</title>
  7 |     <!-- TailwindCSS -->
  8 |     <script src="https://cdn.tailwindcss.com"></script>
  9 |     <script>
 10 |         tailwind.config = {
 11 |             theme: {
 12 |                 extend: {
 13 |                     colors: {
 14 |                         primary: {
 15 |                             50: '#e3f2fd',
 16 |                             100: '#bbdefb',
 17 |                             200: '#90caf9',
 18 |                             300: '#64b5f6',
 19 |                             400: '#42a5f5',
 20 |                             500: '#2196f3',
 21 |                             600: '#1e88e5',
 22 |                             700: '#1976d2',
 23 |                             800: '#1565c0',
 24 |                             900: '#0d47a1'
 25 |                         },
 26 |                         secondary: '#64748b'
 27 |                     }
 28 |                 }
 29 |             }
 30 |         }
 31 |     </script>
 32 |     <!-- UIkit CSS -->
 33 |     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css" />
 34 |     <!-- UIkit JS -->
 35 |     <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
 36 |     <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>
 37 |     <style>
 38 |         .uk-card {
 39 |             border-radius: 1rem;
 40 |             box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
 41 |         }
 42 |         .uk-button {
 43 |             border-radius: 0.5rem;
 44 |             transition: all 0.2s ease;
 45 |         }
 46 |         .uk-button-primary {
 47 |             background: linear-gradient(145deg, #1e88e5, #1976d2);
 48 |             box-shadow: 0 4px 6px -1px rgba(30, 136, 229, 0.2);
 49 |         }
 50 |         .uk-button-primary:hover {
 51 |             transform: translateY(-1px);
 52 |             box-shadow: 0 10px 15px -3px rgba(30, 136, 229, 0.3);
 53 |         }
 54 |         .uk-input, .uk-select, .uk-textarea {
 55 |             border-radius: 0.5rem;
 56 |             transition: all 0.2s ease;
 57 |         }
 58 |         .uk-input:focus, .uk-select:focus, .uk-textarea:focus {
 59 |             box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.2);
 60 |         }
 61 |         .uk-form-label {
 62 |             font-weight: 500;
 63 |             margin-bottom: 0.5rem;
 64 |             color: #334155;
 65 |         }
 66 |     </style>
 67 | </head>
 68 | <body class="bg-slate-100 min-h-screen">
 69 |     <!-- 导航栏 -->
 70 |     <nav class="bg-gradient-to-r from-primary-700 to-primary-500 shadow-lg">
 71 |         <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
 72 |             <div class="flex justify-between h-16">
 73 |                 <div class="flex items-center text-white">
 74 |         
 75 |                     <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
 76 |                         <path stroke-linecap="round" stroke-linejoin="round" d="m20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525" />
 77 |                     </svg>                      
 78 |                     <span class="ml-2 text-xl font-bold">MCP应用编辑器</span>
 79 |                 </div>
 80 |                 <div class="flex items-center gap-6">
 81 |                     <button class="bg-white bg-opacity-20 hover:bg-opacity-30 text-white rounded-lg px-4 py-2 flex items-center transition-all" id="exportConfigBtn">
 82 |                         <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 mr-2">
 83 |                             <path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z" />
 84 |                         </svg>
 85 |                         导出JSON
 86 |                     </button>
 87 | 
 88 |                 </div>
 89 |             </div>
 90 |         </div>
 91 |     </nav>
 92 | 
 93 |     <!-- 主内容区域 -->
 94 |     <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
 95 |         <div class="grid grid-cols-12 gap-6">
 96 |             <!-- 侧边栏 -->
 97 |             <div class="col-span-12 md:col-span-3 space-y-6">
 98 |                 <!-- 基本信息卡片 -->
 99 |                 <div class="bg-white rounded-xl shadow-sm p-6">
100 |                     <h2 class="text-lg font-bold text-gray-800 mb-4">应用信息</h2>
101 |                     <div class="space-y-4">
102 |                         <div>
103 |                             <label class="block text-sm font-medium text-gray-700 mb-1">应用名称</label>
104 |                             <input type="text" id="appName" class="border w-full h-10 px-3 rounded-lg border-gray-200 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20" placeholder="输入应用名称">
105 |                         </div>
106 |                         <div>
107 |                             <label class="block text-sm font-medium text-gray-700 mb-1">应用描述</label>
108 |                             <textarea id="appDescription" rows="3" class="border w-full px-3 py-2 rounded-lg border-gray-200 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20" placeholder="描述此应用的用途"></textarea>
109 |                         </div>
110 |                     </div>
111 |                 </div>
112 | 
113 |                 <!-- 导入/导出卡片 -->
114 |                 <div class="bg-white rounded-xl shadow-sm p-6">
115 |                     <h2 class="text-lg font-bold text-gray-800 mb-4">配置管理</h2>
116 |                     <div class="space-y-3 mb-3">
117 |                         <button class="w-full flex items-center justify-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors" id="saveConfigBtn">
118 |                             <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 mr-2">
119 |                                 <path stroke-linecap="round" stroke-linejoin="round" d="M9 3.75H6.912a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h3.218a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859M12 3v8.25m0 0-3-3m3 3 3-3" />
120 |                             </svg>
121 |                             保存配置
122 |                         </button>
123 |                     </div>
124 |                     <div class="space-y-3">
125 |                         <button class="w-full flex items-center justify-center px-4 py-2 bg-violet-600 bg-opacity-80 hover:bg-violet-700 text-white text-sm font-medium rounded-lg transition-colors" id="importJsonBtn">
126 |                             <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
127 |                                 <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
128 |                             </svg>
129 |                             导入JSON
130 |                         </button>
131 |                         <button class="w-full flex items-center justify-center px-4 py-2 text-primary-700 bg-primary-50 hover:bg-primary-100 text-sm font-medium rounded-lg transition-colors" id="useTemplateBtn">
132 |                             <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
133 |                                 <path d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
134 |                             </svg>
135 |                             使用模板
136 |                         </button>
137 |                         <button class="w-full flex items-center justify-center px-4 py-2 text-red-700 bg-red-50 hover:bg-red-100 text-sm font-medium rounded-lg transition-colors" id="deleteAppBtn">
138 |                             <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
139 |                                 <path d="M19 7L18.1327 19.1425C18.0579 20.1891 17.187 21 16.1378 21H7.86224C6.81296 21 5.94208 20.1891 5.86732 19.1425L5 7M10 11V17M14 11V17M15 7V4C15 3.44772 14.5523 3 14 3H10C9.44772 3 9 3.44772 9 4V7M4 7H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
140 |                             </svg>
141 |                             删除应用
142 |                         </button>
143 |                     </div>
144 |                 </div>
145 |             </div>
146 | 
147 |             <!-- 主要内容区域 -->
148 |             <div class="col-span-12 md:col-span-9 space-y-6">
149 |                 <!-- 服务器列表 -->
150 |                 <div class="bg-white rounded-xl shadow-sm p-6">
151 |                     <div class="flex justify-between items-center mb-6">
152 |                         <h2 class="text-lg font-bold text-gray-800">服务器配置</h2>
153 |                         <button class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors" id="addServerBtn">
154 |                             <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
155 |                                 <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
156 |                             </svg>
157 |                             MCP
158 |                         </button>
159 |                     </div>
160 | 
161 |                     <div class="overflow-x-auto">
162 |                         <table class="w-full text-left">
163 |                             <thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
164 |                                 <tr>
165 |                                     <th class="px-4 py-3 rounded-tl-lg">名称</th>
166 |                                     <th class="px-4 py-3">类型</th>
167 |                                     <th class="px-4 py-3">命令</th>
168 |                                     <th class="px-4 py-3 rounded-tr-lg text-right">操作</th>
169 |                                 </tr>
170 |                             </thead>
171 |                             <tbody id="serverTableBody" class="divide-y divide-gray-100">
172 |                                 <!-- 服务器列表项会动态添加到这里 -->
173 |                             </tbody>
174 |                         </table>
175 |                     </div>
176 | 
177 |                     <div id="emptyServerMessage" class="text-center py-8 text-gray-500 hidden">
178 |                         还没有添加任何服务器配置
179 |                     </div>
180 |                 </div>
181 | 
182 |                 <!-- JSON预览区域 -->
183 |                 <div class="bg-white rounded-xl shadow-sm p-6">
184 |                     <h2 class="text-lg font-bold text-gray-800 mb-4">JSON预览</h2>
185 |                     <div class="relative">
186 |                         <pre id="jsonPreview" class="bg-gray-50 p-4 rounded-lg text-sm font-mono overflow-x-auto"></pre>
187 |                         <button id="copyJsonBtn" class="absolute top-2 right-2 p-2 text-gray-500 hover:text-gray-700 bg-white rounded-lg shadow-sm">
188 |                             <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
189 |                                 <path d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
190 |                             </svg>
191 |                         </button>
192 |                     </div>
193 |                 </div>
194 |             </div>
195 |         </div>
196 |     </div>
197 | 
198 |     <!-- 服务器配置模态框 -->
199 |     <div id="server-modal" uk-modal>
200 |         <div class="uk-modal-dialog uk-modal-body rounded-xl">
201 |             <button class="uk-modal-close-default" type="button" uk-close></button>
202 |             <h2 class="text-2xl font-bold mb-6" id="serverModalTitle">添加服务器</h2>
203 |             
204 |             <form id="serverForm" class="space-y-6">
205 |                 <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
206 |                     <div>
207 |                         <label class="block text-sm font-medium text-gray-700 mb-1">服务器名称</label>
208 |                         <input type="text" id="serverName" required class="border w-full h-10 px-3 rounded-lg border-gray-200 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20" placeholder="输入服务器名称">
209 |                     </div>
210 |                     
211 |                     <div>
212 |                         <label class="block text-sm font-medium text-gray-700 mb-1">服务器类型</label>
213 |                         <select id="serverType" class="border w-full h-10 px-3 rounded-lg border-gray-200 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20">
214 |                             <option value="sse">SSE (远程服务)</option>
215 |                             <option value="stdio">STDIO (本地进程)</option>
216 |                         </select>
217 |                     </div>
218 |                 </div>
219 | 
220 |                 <div>
221 |                     <label class="block text-sm font-medium text-gray-700 mb-1">服务器描述</label>
222 |                     <textarea  id="serverDescription" class="border w-full min-h-[48px] p-2 rounded-lg focus:border-sky-200"></textarea>
223 |                 </div>
224 | 
225 |                 <!-- SSE配置 -->
226 |                 <div id="sseConfig" class="space-y-4">
227 |                     <div>
228 |                         <label class="block text-sm font-medium text-gray-700 mb-1">Base URL</label>
229 |                         <input type="text" id="baseUrl" class="border w-full h-10 px-3 rounded-lg border-gray-200 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20" placeholder="http://example.com/api">
230 |                     </div>
231 | 
232 |                     <div>
233 |                         <label class="block text-sm font-medium text-gray-700 mb-3">Headers</label>
234 |                         <div id="headersContainer" class="space-y-2">
235 |                             <!-- Headers会动态添加到这里 -->
236 |                         </div>
237 |                         <button type="button" id="addHeaderBtn" class="mt-2 inline-flex items-center px-3 py-2 text-sm font-medium text-primary-600 bg-primary-50 rounded-lg hover:bg-primary-100 transition-colors">
238 |                             <svg class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none">
239 |                                 <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
240 |                             </svg>
241 |                             添加Header
242 |                         </button>
243 |                     </div>
244 |                 </div>
245 | 
246 |                 <!-- STDIO配置 -->
247 |                 <div id="stdioConfig" class="space-y-4 hidden">
248 |                     <div>
249 |                         <label class="block text-sm font-medium text-gray-700 mb-1">Command</label>
250 |                         <input type="text" id="command" class="border w-full h-10 px-3 rounded-lg border-gray-200 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20" placeholder="执行的命令">
251 |                     </div>
252 | 
253 |                     <div>
254 |                         <label class="block text-sm font-medium text-gray-700 mb-3">参数列表</label>
255 |                         <div id="argsContainer" class="space-y-2">
256 |                             <!-- 参数会动态添加到这里 -->
257 |                         </div>
258 |                         <button type="button" id="addArgBtn" class="mt-2 inline-flex items-center px-3 py-2 text-sm font-medium text-primary-600 bg-primary-50 rounded-lg hover:bg-primary-100 transition-colors">
259 |                             <svg class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none">
260 |                                 <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
261 |                             </svg>
262 |                             添加参数
263 |                         </button>
264 |                     </div>
265 | 
266 |                     <div>
267 |                         <label class="block text-sm font-medium text-gray-700 mb-3">环境变量</label>
268 |                         <div id="envContainer" class="space-y-2">
269 |                             <!-- 环境变量会动态添加到这里 -->
270 |                         </div>
271 |                         <button type="button" id="addEnvBtn" class="mt-2 inline-flex items-center px-3 py-2 text-sm font-medium text-primary-600 bg-primary-50 rounded-lg hover:bg-primary-100 transition-colors">
272 |                             <svg class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none">
273 |                                 <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
274 |                             </svg>
275 |                             添加环境变量
276 |                         </button>
277 |                     </div>
278 |                 </div>
279 | 
280 |                 <div class="flex justify-end space-x-4 pt-4">
281 |                     <button type="submit" class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow hover:shadow-lg transition-all">
282 |                         <svg class="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="none">
283 |                             <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
284 |                         </svg>
285 |                         保存配置
286 |                     </button>
287 |                     <button type="button" class="uk-modal-close inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium rounded-lg transition-colors">
288 |                         取消
289 |                     </button>
290 |                 </div>
291 |             </form>
292 |         </div>
293 |     </div>
294 | 
295 |     <!-- 模板选择模态框 -->
296 |     <div id="template-modal" uk-modal>
297 |         <div class="uk-modal-dialog uk-modal-body rounded-xl w-2/5">
298 |             <button class="uk-modal-close-default" type="button" uk-close></button>
299 |             <h2 class="text-2xl font-bold mb-4 ">选择模板</h2>
300 |             
301 |             <div id="templateLoading" class="flex justify-center items-center py-8">
302 |                 <div uk-spinner></div>
303 |                 <span class="ml-3">加载模板列表...</span>
304 |             </div>
305 |             
306 |             <div id="templateTableContainer" class="hidden">
307 |                 <div class="flex justify-between items-center mb-4">
308 |                     <div class="flex items-center">
309 |                         <input type="checkbox" id="selectAllTools" class="mr-2 h-4 w-4 rounded text-primary-600">
310 |                         <label for="selectAllTools" class="text-sm font-medium text-gray-700">全选</label>
311 |                     </div>
312 |                     <button id="addSelectedToolsBtn" class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors">
313 |                         <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none">
314 |                             <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
315 |                         </svg>
316 |                         添加选中的模板
317 |                     </button>
318 |                 </div>
319 |                 
320 |                 <div class="overflow-x-auto">
321 |                     <table class="w-full text-left">
322 |                         <thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
323 |                             <tr>
324 |                                 <th class="px-4 py-3 rounded-tl-lg w-10"></th>
325 |                                 <th class="px-4 py-3">名称</th>
326 |                                 <th class="px-4 py-3">类型</th>
327 |                                 <th class="px-4 py-3 rounded-tr-lg">描述</th>
328 |                             </tr>
329 |                         </thead>
330 |                         <tbody id="templateTableBody" class="divide-y divide-gray-100">
331 |                             <!-- 模板列表项会动态添加到这里 -->
332 |                         </tbody>
333 |                     </table>
334 |                 </div>
335 |             </div>
336 |             
337 |             <div id="noTemplatesMessage" class="text-center py-8 text-gray-500 hidden">
338 |     <div id="json-modal" uk-modal>
339 |         <div class="uk-modal-dialog uk-modal-body rounded-xl">
340 |             <button class="uk-modal-close-default" type="button" uk-close></button>
341 |             <h2 class="text-2xl font-bold mb-4">导入JSON配置</h2>
342 |             
343 |             <div class="mb-4">
344 |                 <textarea id="jsonInput" class="w-full h-96 px-4 py-3 font-mono text-sm border rounded-xl focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-20" placeholder="在此粘贴JSON配置..."></textarea>
345 |             </div>
346 |             
347 |             <div class="flex justify-end">
348 |                 <button type="button" id="importBtn" class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors">
349 |                     <svg class="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="none">
350 |                         <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
351 |                     </svg>
352 |                     导入配置
353 |                 </button>
354 |             </div>
355 |         </div>
356 |     </div>
357 | 
358 |     <script src="js/MCPEditor.js"></script>
359 |     <script>
360 |         document.addEventListener('DOMContentLoaded', function() {
361 |             const editor = new MCPEditor();
362 |             editor.init();
363 |         });
364 |     </script>
365 | </body>
366 | </html>
367 | 
```

--------------------------------------------------------------------------------
/webui/chat.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="zh-CN" class="scroll-smooth overflow-hidden">
  3 | <head>
  4 |   <meta charset="UTF-8">
  5 |   <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  6 |   <meta name="mobile-web-app-capable" content="yes">
  7 |   <title>Playground/toai.chat</title>
  8 |   
  9 |   <!-- 引入UIKit和TailwindCSS -->
 10 |   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css" />
 11 |   <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
 12 |   <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>
 13 | 
 14 |   <!-- 引入marked和highlight.js -->
 15 |   <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
 16 |   <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
 17 |   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.css" id="markdown-light">
 18 |   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/github-markdown-dark.css" id="markdown-dark" disabled>
 19 | 
 20 |   <script src="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js"></script>
 21 |   <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/a11y-light.min.css" id="code-light">
 22 |   <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/a11y-dark.min.css" id="code-dark" disabled>
 23 | 
 24 |   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.css" id="mermaid-light">
 25 |   <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
 26 | 
 27 |   <script src="https://cdn.tailwindcss.com"></script>
 28 |   
 29 |   <script src="ai/ai.js"></script>
 30 |   <script src="mcp/mcpcli.js"></script>
 31 |   
 32 |   <script>
 33 |     tailwind.config = {
 34 |       darkMode: 'class',
 35 |       theme: {
 36 |         extend: {
 37 |           colors: {
 38 |             'ai-green': '#10a37f',
 39 |             'ai-green-dark': '#0e8a6e',
 40 |             'chat-bg': '#f7f7f8',
 41 |             'chat-dark': '#444654',
 42 |             'text-light': '#222222',
 43 |             'text-dark': '#ececf1',
 44 |             'user-bg': '#eeeeee'
 45 |           },
 46 |           boxShadow: {
 47 |             'send-btn': '0 2px 5px rgba(0,0,0,0.1)'
 48 |           }
 49 |         }
 50 |       }
 51 |     }
 52 |   </script>
 53 |   <style>
 54 |     .markdown-body { background-color: transparent; h-* { color: #eee; } }
 55 |     ::-webkit-scrollbar { width: 0px; }
 56 |   </style>
 57 | </head>
 58 | <body class="dark:bg-gray-900 dark:text-gray-100">
 59 |   <!-- 添加UIKit Offcanvas组件作为左侧抽屉 -->
 60 |   <div id="offcanvas-nav" uk-offcanvas="overlay: true; mode: push">
 61 |     <div class="uk-offcanvas-bar bg-gray-50 text-gray-600 dark:bg-gray-600 dark:text-gray-100">
 62 |       <button class="uk-offcanvas-close" type="button" uk-close></button>
 63 |       <div class="font-bold text-lg text-gray-800 dark:text-gray-100 mb-4">Topic History</div>
 64 |       
 65 |       <!-- 改为黑白配色的新对话按钮 -->
 66 |       <div class="flex items-center justify-center mx-4 my-3 p-2.5 rounded bg-gray-500 text-white cursor-pointer transition-colors" id="new-chat-btn">
 67 |         <span uk-icon="icon: plus" class="text-white"></span>
 68 |         <span class="ml-2 text-sm font-medium">New Topic</span>
 69 |       </div>
 70 |       
 71 |       <div class="conversation-list mt-4" id="conversation-list"></div>
 72 |     </div>
 73 |   </div>
 74 | 
 75 | 
 76 |   <div id="app" class="flex flex-col h-screen">
 77 |     <!-- 顶部导航栏 - 固定定位 -->
 78 |     <header class="fixed top-0 left-0 w-full bg-white dark:bg-gray-900 z-40">
 79 |       <div class="uk-container uk-container-expand">
 80 |         <nav class="uk-navbar" uk-navbar>
 81 |           <div class="uk-navbar-left">
 82 |             <!-- 添加菜单按钮 -->
 83 |             <a class="uk-navbar-toggle" href="#" uk-toggle="target: #offcanvas-nav">
 84 |               <span uk-icon="icon: menu"></span>
 85 |             </a>
 86 |             <!-- 将固定标题改为可编辑区域 -->
 87 |             <div class="uk-navbar-item dark:text-gray-100">
 88 |               <span id="conversation-title" contenteditable="true"></span>
 89 |             </div>
 90 |           </div>
 91 |           <div class="uk-navbar-right">
 92 |             <ul class="uk-navbar-nav">
 93 |               <li>
 94 |                 <a href="#" id="theme-toggle" class="theme-icon flex items-center justify-center dark:text-gray-100">
 95 |                   <svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
 96 |                     <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
 97 |                   </svg>
 98 |                 </a>
 99 |               </li>
100 |               <li>
101 |                 <a href="#" uk-icon="icon: cog"></a>
102 |                 <div class="uk-navbar-dropdown" uk-dropdown="mode: click; animation: uk-animation-slide-top-small; duration: 100">
103 |                   <ul class="uk-nav uk-navbar-dropdown-nav line-height-2">
104 |                     <li style="margin:16px;"><a id="clear-chat" uk-icon="icon: refresh">清空记录</a></li>
105 |                     <li style="margin:16px;"><a id="remove-chat" uk-icon="icon: trash">删除对话</a></li>
106 |                     <li style="margin:16px;"><a id="api-settings" uk-icon="icon: cog">对话设置</a></li>
107 |                   </ul>
108 |                 </div>
109 |               </li>
110 |             </ul>
111 |           </div>
112 |         </nav>
113 |       </div>
114 |     </header>
115 |     
116 |     <!-- 聊天区域 - 调整顶部边距以避免被固定header覆盖 -->
117 |     <main class="flex-grow pt-[80px] overflow-y-auto">
118 |       <div class="uk-container uk-container-expand h-full">
119 |         <div id="chat-container" class="chat-container flex flex-col mb-2.5 p-5 pb-[240px]"></div>
120 |       </div>
121 |     </main>
122 |     
123 |     <div class="fixed bottom-0 left-0 w-full bg-white dark:bg-gray-900 p-4 border-gray-200 dark:border-gray-700 z-30 shadow-md">
124 | 
125 |         <div class="message-toolbar uk-margin-small-bottom flex items-center gap-8 px-4">
126 | 
127 |             <div class="uk-inline">
128 |                 <div class="js-upload cursor-hand" uk-form-custom>
129 |                     <button class="toolbar-btn" type="button" uk-tooltip="上传文件">
130 |                         <span uk-icon="icon: image"></span>
131 |                         <input type="file" id="image-upload-input" accept="image/*" multiple>
132 |                     </button>
133 | 
134 |                 </div>
135 |             </div>
136 |             
137 |             <div class="uk-inline">
138 |               <button class="toolbar-btn" id="mcp-toggle" uk-tooltip="MCP">
139 |                 <span uk-icon><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
140 |                   <path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
141 |                 </svg>
142 |                 </span>      
143 | 
144 |                           
145 |               </button>
146 |             </div>
147 |         
148 |         </div>
149 | 
150 |         <div class="flex items-start gap-2.5 relative mx-auto my-0">
151 |           <textarea id="message-input"  class="rounded-lg p-3 resize-none overflow-y-auto max-h-[120px] flex-grow shadow-sm border border-gray-200 transition-all focus:shadow-md focus:border-blue-400  dark:focus:border-blue-200 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100 uk-textarea h-[48px]" rows="1" placeholder="输入信息..." style="overflow:hidden"></textarea>
152 |           
153 |           <!-- 重写发送按钮,添加两种状态 -->
154 |           <button id="send-button" class="w-10 h-10 rounded-full bg-ai-green text-white flex items-center justify-center cursor-pointer border-0 shadow-md transition-all duration-300 ease-in-out flex-shrink-0 mt-1.5 disabled:bg-gray-300 disabled:cursor-not-allowed" disabled>
155 |             <!-- 正常状态 -->
156 |             <span id="send-icon" class="block">
157 |               <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
158 |                 <path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
159 |               </svg>
160 |             </span>
161 |             <!-- loading状态 -->
162 |             <span id="loading-icon" class="hidden">
163 |               <svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
164 |                 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
165 |                 <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
166 |               </svg>
167 |             </span>
168 |           </button>
169 |         </div>
170 | 
171 |         <div id="image-preview-container" class="flex flex-wrap gap-2.5 mt-2.5">
172 |             <ul class="uk-thumbnav" uk-margin id="image-preview-container"></ul>
173 |         </div>
174 |     </div>
175 | 
176 |   </div>
177 |   
178 |   <div id="settings-modal" uk-modal>
179 |     <div class="uk-modal-dialog uk-modal-body">
180 |       <h2 class="uk-modal-title">对话设置</h2>
181 |       <form class="uk-form-stacked">
182 |         <div class="uk-margin">
183 |           <label class="uk-form-label">API Key</label>
184 |           <div class="uk-form-controls"><input class="uk-input" id="apiKey" type="password" placeholder="输入你的API Key"></div>
185 |         </div>
186 |         <div class="uk-margin">
187 |             <label class="uk-form-label">MCP(SSE)</label>
188 |             <div class="uk-form-controls"><input class="uk-input" id="mcp" type="text" placeholder="请输入MCP服务器地址"></div>
189 |           </div>        
190 |         <div class="uk-margin">
191 |           <label class="uk-form-label">API baseURL</label>
192 |           <div class="uk-form-controls"><input class="uk-input" id="baseURL" type="text" placeholder="https://porky.toai.chat/pollination/v1"></div>
193 |         </div>
194 |         <div class="uk-margin">
195 |           <label class="uk-form-label">模型</label>
196 |           <div class="uk-form-controls">
197 |             <input class="uk-input" id="model" type="text" placeholder="模型名称" value="openai">
198 |           </div>
199 |           <a href="https://text.pollinations.ai/models" target="_blank">see the models if you are using pollinations</a>
200 |         </div>
201 |         <div class="uk-margin">
202 |           <label class="uk-form-label">系统提示 (System Prompt)</label>
203 |           <div class="uk-form-controls">
204 |             <textarea class="uk-textarea" id="systemPrompt" rows="3" placeholder="输入系统提示,指导AI的行为和角色"></textarea>
205 |           </div>
206 |         </div>
207 |         <div class="uk-margin">
208 |           <label class="uk-form-label">温度 <span id="temperature-value">0.7</span></label>
209 |           <div class="uk-form-controls"><input class="uk-range" id="temperature" type="range" min="0" max="2" step="0.01" value="0.7"></div>
210 |         </div>
211 |         <div class="uk-margin">
212 |           <label class="uk-form-label">最大令牌数 (Max Tokens)</label>
213 |           <div class="uk-form-controls">
214 |             <input class="uk-input" id="maxTokens" type="number" placeholder="最大令牌数" value="128000">
215 |           </div>
216 |         </div>
217 |         <div class="uk-margin">
218 |           <label class="uk-form-label">Top P <span id="top-p-value">1.0</span></label>
219 |           <div class="uk-form-controls"><input class="uk-range" id="topP" type="range" min="0" max="1" step="0.01" value="1.0"></div>
220 |         </div>
221 |       </form>
222 |       <div class="uk-text-right">
223 |         <button class="uk-button uk-button-default uk-modal-close rounded-md">取消</button>
224 |         <button class="uk-button uk-button-primary rounded-md" id="save-settings">保存</button>
225 |       </div>
226 |     </div>
227 |   </div>
228 | 
229 |   <script>
230 | 
231 |     const appState = {
232 |       darkMode: localStorage.getItem('darkMode') === 'true',
233 |       currentChat: null,
234 | 
235 |       togglekMode() {
236 |         this.darkMode = !this.darkMode;
237 |         localStorage.setItem('darkMode', this.darkMode);
238 |         document.documentElement.classList.toggle('dark')
239 |         document.getElementById('markdown-light').disabled = this.darkMode;
240 |         document.getElementById('markdown-dark').disabled = !this.darkMode;
241 |         document.getElementById('code-light').disabled = this.darkMode;
242 |         document.getElementById('code-dark').disabled = !this.darkMode;
243 |       }
244 | 
245 |     }
246 | 
247 |     document.getElementById('theme-toggle').addEventListener('click', appState.togglekMode)
248 |     mermaid.initialize({startOnLoad:true, theme: appState.darkMode ? 'neutral' : 'neutral', securityLevel: 'loose'});
249 | 
250 | 
251 |     class DataBase {
252 |       constructor(dbname, structure={}) {
253 |         this.dbName = dbname
254 |         this.structure = structure;
255 |         this.db = null;
256 |         this.ready = this.initDatabase();
257 |       }
258 | 
259 |       async initDatabase() {
260 |         return new Promise((resolve, reject) => {
261 |           const request = indexedDB.open(this.dbName, 1);
262 |           
263 |           request.onupgradeneeded = (event) => {
264 |             const db = event.target.result;
265 |             for (const storeName in this.structure) {
266 |               if (!db.objectStoreNames.contains(storeName)) {
267 |                 const store = db.createObjectStore(storeName, { keyPath: this.structure[storeName].keyPath });
268 |                 for (const index of this.structure[storeName].indexes) {
269 |                   store.createIndex(index.name, index.keyPath, { unique: index.unique||false });
270 |                 }
271 |               }
272 |             }
273 |           };
274 |     
275 |           request.onsuccess = (event) => { this.db = event.target.result; resolve(true);};
276 |           request.onerror = (event) => { reject(event.target.error);};
277 |         });
278 |       }
279 | 
280 |       async getCollectionData(collection, key, filter) {
281 |         await this.ready;
282 |         return new Promise((resolve, reject) => {
283 |           const transaction = this.db.transaction([collection], 'readonly');
284 |           const store = transaction.objectStore(collection);
285 |           const index = store.index(key);
286 |           const request = !filter ? index.getAll() : index.getAll(filter);
287 |           request.onsuccess = () => resolve(request.result);
288 |           request.onerror = () => reject(request.error);
289 |         });
290 |       }
291 | 
292 |       async setCollectionData(collection, data) {
293 |         await this.ready;
294 |         return new Promise((resolve, reject) => {
295 |           const transaction = this.db.transaction([collection], 'readwrite');
296 |           const store = transaction.objectStore(collection);
297 |           const request = store.put(data);
298 |           request.onsuccess = () => resolve(request.result);
299 |           request.onerror = () => reject(request.error);
300 |         });
301 |       }
302 | 
303 |       async deleteCollectionData(collection, key) {
304 |         await this.ready;
305 |         return new Promise((resolve, reject) => {
306 |           const transaction = this.db.transaction([collection], 'readwrite');
307 |           const store = transaction.objectStore(collection);
308 |           const request = store.delete(key);
309 |           request.onsuccess = () => resolve(request.result);
310 |           request.onerror = () => reject(request.error);
311 |         });
312 |       }
313 |     }
314 |       
315 |     class MainDatabase extends DataBase {
316 |         constructor() {
317 |           super('MainDB', {
318 |             conversations: {
319 |               keyPath: 'id',
320 |               indexes: [
321 |                 { name: 'title', keyPath: 'title' },
322 |                 { name: 'lastMessage', keyPath: 'lastMessage' },
323 |                 { name: 'id', keyPath: 'id' },
324 |                 { name: 'timestamp', keyPath: 'timestamp' }
325 |               ]
326 |             }
327 |           });
328 |         }
329 |         async getAllConversations(id=null) {
330 |           if (id) return await this.getCollectionData('conversations', 'id', id);
331 |           return await this.getCollectionData('conversations', 'timestamp');
332 |         }
333 |         async getOrCreateLastChat() {
334 |           const conversations = await this.getAllConversations();
335 |           if (conversations.length > 0) {
336 |             const conversation = conversations[conversations.length - 1];
337 |             console.log('获取最后一个对话:', conversation);
338 |             return conversation;
339 |           }
340 |           return this.createNewConversation(Date.now(), '');
341 |         }
342 |         async createNewConversation(id, title) {
343 |           const conversation = { id: id, title: title,timestamp: Date.now(), lastMessage: ''};
344 |           await this.setCollectionData('conversations', conversation);
345 |           return conversation;
346 |         }
347 |       }
348 | 
349 |     class ChatDatabase extends DataBase {
350 |       constructor(chatId) {
351 |         if(!chatId) {
352 |           throw new Error('Chat ID is required');
353 |         }
354 |         super(`ChatDB-${chatId}`, {
355 |           messages: {
356 |             keyPath: 'id',
357 |             indexes: [
358 |               { name: 'id', keyPath: 'id' },
359 |               { name: 'content', keyPath: 'content' },
360 |               { name: 'role', keyPath: 'role' },
361 |               { name: 'timestamp', keyPath: 'timestamp' },
362 |               { name: 'images', keyPath: 'images' }
363 |             ]
364 |           },
365 |           settings: {
366 |             keyPath: 'key',
367 |             indexes: [
368 |               { name: 'key', keyPath: 'key' },
369 |               { name: 'value', keyPath: 'value' }
370 |             ]
371 |           }
372 |         });
373 |       }
374 |       async getSettings() {return await this.getCollectionData('settings', 'key')}
375 |       async saveMessage(message) { await this.setCollectionData('messages', message)}
376 |       async getAllMessages() { return await this.getCollectionData('messages', 'timestamp') }
377 |     }
378 | 
379 |     window.MainDatabase = new MainDatabase()
380 | 
381 | 
382 |     class AdvancedConversation {
383 |       constructor() {
384 |         this.db = window.MainDatabase
385 |         this.conversationList = document.getElementById('conversation-list');
386 |         this.newChatButton = document.getElementById('new-chat-btn');
387 |         this.deleteChatButton = document.getElementById('remove-chat');
388 |         this.topicTitle = document.getElementById('conversation-title');
389 |         this.init();
390 |       }
391 |       async init() {
392 |         this.newChatButton.addEventListener('click', this.createNewConversation.bind(this));
393 |         await this.loadConversations();
394 |         this.deleteChatButton.addEventListener('click', this.deleteConversation.bind(this));
395 |         this.topicTitle.addEventListener('blur', async (e) => {
396 |           const title = e.target.innerText;
397 |           if (title) { await this.setConversationTitle(window.aiui.chatId, title);}
398 |         });
399 |         this.topicTitle.addEventListener('keydown', (e) => {
400 |           if (e.key === 'Enter') { e.preventDefault(); this.topicTitle.blur();}
401 |         });
402 |       }
403 |       async loadConversations() {
404 |         const conversations = await this.db.getAllConversations();
405 |         this.conversationList.innerHTML = ''; // 清空列表
406 |         for (const conversation of conversations) {
407 |           const item = document.createElement('div');
408 |           item.className = 'conversation-item flex items-center justify-between p-2 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors';
409 |           item.id = 'conversation-' + conversation.id;
410 |           item.setAttribute('data-id', conversation.id);
411 |           const title = document.createElement('span')
412 |           title.className = 'conversation-title truncate whitespace-nowrap'; title.innerText = conversation.title || 'Untitled Topic';
413 |           item.appendChild(title);
414 |           item.addEventListener('click', (e) => {this.loadConversation(conversation.id);});
415 |           this.conversationList.appendChild(item);
416 |         }
417 |         return conversations;
418 |       }
419 |       async createNewConversation() {
420 |         const chat = await this.db.createNewConversation(Date.now(), '');
421 |         await this.loadConversations();
422 |         await this.loadConversation(chat.id);
423 |       }
424 |       async loadConversation(id) {
425 |         localStorage.setItem('last_chat_id', id);
426 |         const conversations = await this.db.getAllConversations(id);
427 |         if (conversations.length > 0) {
428 |           const conversation = conversations[0];
429 |           this.conversationList.querySelectorAll('.conversation-item').forEach(item => item.classList.remove('active'));
430 |           this.conversationList.querySelector(`#conversation-${conversation.id}`).classList.add('active');
431 |           window.aiui = new AIUI( {chatId: conversation.id} );
432 |           await window.aiui.ready;
433 |           this.topicTitle.innerText = conversation.title;
434 |           UIkit.offcanvas('#offcanvas-nav').hide();
435 |         }
436 |       }
437 |       async deleteConversation(event) {
438 |         const id = window.aiui.chatId;
439 |         await UIkit.modal.confirm('确定要删除此对话吗?', {labels: {ok: '删除', cancel: '取消'}}).then(async () => {
440 |           await this.db.deleteCollectionData('conversations', id);
441 |           await indexedDB.deleteDatabase(`ChatDB-${id}`);
442 |           await this.loadConversations();
443 |           window.aiui = new AIUI();
444 |         })
445 |       }
446 |       async setConversationTitle(id, title) {
447 |         const conversation = await this.db.getCollectionData('conversations', 'id', id);
448 |         if (conversation.length > 0) {
449 |           conversation[0].title = title;
450 |           await this.db.setCollectionData('conversations', conversation[0]);
451 |           this.topicTitle.innerText = title;
452 |         }
453 |         await this.loadConversations();
454 |       }
455 |     }
456 | 
457 | 
458 |     class AdvancedContainer {
459 |       // 聊天消息容器类,主要控制chat-container的消息
460 |       static instance = null;
461 |       
462 |       static getInstance(db) {
463 |         if (!AdvancedContainer.instance) {
464 |           AdvancedContainer.instance = new AdvancedContainer(db);
465 |         } else if (db && AdvancedContainer.instance.db !== db) {
466 |           // 当数据库引用变化时,更新数据库引用并重置容器
467 |           AdvancedContainer.instance.db = db;
468 |           AdvancedContainer.instance.resetContainer();
469 |           AdvancedContainer.instance.loadMessages(); // 自动加载新数据库的消息
470 |         }
471 |         return AdvancedContainer.instance;
472 |       }
473 |       
474 |       constructor(db) {
475 |         // 如果已经有实例,直接返回
476 |         if (AdvancedContainer.instance) {
477 |           return AdvancedContainer.instance;
478 |         }
479 |         
480 |         this.container = document.getElementById('chat-container');
481 |         this.clearChatButton = document.getElementById('clear-chat');
482 |         this.container.innerHTML = ''; // 初始化的时候,清空容器
483 |         this.db = db;
484 |         this.initialized = false;
485 |         this.ready = this.init();
486 |         
487 |         AdvancedContainer.instance = this;
488 |       }
489 | 
490 |       // 重置容器,清空内容
491 |       resetContainer() {
492 |         this.container.innerHTML = '';
493 |       }
494 | 
495 |       async init() {
496 |         // 确保初始化和事件绑定只执行一次
497 |         if (this.initialized) return;
498 |         
499 |         this.clearChatButton.addEventListener('click', async() => {
500 |           await UIkit.modal.confirm('确定要清空聊天记录吗?', {labels: {ok: '清空', cancel: '取消'}}).then(async () => {
501 |             await this.clearChat();
502 |             if (window.aiui && window.aiui.ai) {
503 |               window.aiui.ai.messages = [];
504 |             }
505 |           });
506 |         });
507 |         
508 |         this.initialized = true;
509 |       }
510 | 
511 |       scrollToBottom() {
512 |         // 修改滚动函数,滚动整个窗口到底部
513 |         window.scrollTo(0, document.body.scrollHeight);
514 |       }
515 |       
516 |       async newMessage(role) {
517 |         const msgId = `msg-${Date.now()}`;
518 |         const messageDiv = document.createElement('div');
519 |         messageDiv.className = 'max-w-fit mb-4 p-3 rounded-lg relative break-words shadow-sm';
520 |         
521 |         // 使用Tailwind类替代自定义CSS类
522 |         if (role === 'user') {
523 |           messageDiv.classList.add('bg-user-bg', 'text-gray-900', 'ml-auto', 'text-right','uk-animation-slide-bottom-small','uk-animation-fade');
524 |         } else if (role === 'assistant') {
525 |           messageDiv.classList.add('bg-chat-bg', 'text-text-light', 'dark:bg-chat-dark', 'dark:text-text-dark','uk-animation-fade','uk-animation-slide-bottom-small');
526 |         }
527 |         
528 |         messageDiv.id = msgId;
529 | 
530 |         const messageContent = document.createElement('div');
531 |         messageContent.className = 'message-content';
532 |         messageContent.setAttribute('raw', '');
533 |         if (role === 'assistant') {
534 |             messageContent.setAttribute('class','message-content p-2 markdown-body rounded-md dark:[&>h4]:text-gray-100  dark:[&>h3]:text-gray-100  dark:[&>h2]:text-gray-100  dark:[&>h1]:text-gray-100');
535 |         }
536 | 
537 |         const messageReason = document.createElement('div');
538 |         messageReason.className = 'message-reason opacity-30 markdown-body max-h-[32px] overflow-hidden hover:cursor-pointer hover:opacity-60 hover:font-weight-light';
539 |         messageReason.setAttribute('raw', '');
540 |         messageReason.addEventListener('click', (e)=>{ messageReason.classList.toggle("max-h-[48px]")} )
541 |         messageDiv.appendChild(messageReason)
542 | 
543 |         messageDiv.appendChild(messageReason);
544 |         messageDiv.appendChild(messageContent);
545 | 
546 |         const add = () => { this.container.appendChild(messageDiv); this.scrollToBottom();};
547 |         const update = async(content, addons={}) => { 
548 |           const { scroll=false, images=[], spin=false } = addons;
549 |           messageContent.setAttribute('raw', content);
550 |           if (spin) {
551 |             content = `<div uk-spinner='ratio: 0.5'></div>`;
552 |           } 
553 |           if (role === 'user') {
554 |             messageContent.innerHTML = content;
555 |             if (images.length) {
556 |                 messageContent.setAttribute('images', JSON.stringify(images));
557 |                 const imgContainer = document.createElement('div'); imgContainer.className = 'flex';
558 |                 images.forEach((image, index) => {
559 |                     const img = document.createElement('img');
560 |                     img.src = image; img.style.height = '80px'; img.className = 'rounded-md mx-0.5'; 
561 |                     imgContainer.appendChild(img);
562 |                 });
563 |                 messageContent.appendChild(imgContainer);
564 |             }
565 |           } else if (role==='assistant') {
566 |             messageContent.innerHTML = marked.parse(content);
567 |           }
568 | 
569 |           if (role === 'assistant') {setTimeout(() => mermaid.init(undefined, messageContent.querySelectorAll('.mermaid')), 0);}
570 |           if (scroll) this.scrollToBottom();
571 |         };
572 |         const updateReason = async(reason) => {
573 |           messageReason.classList.add('p-4')
574 |           messageReason.innerHTML = marked.parse(reason); 
575 |         }; 
576 |         const save = async( ) => { 
577 |             const content = messageContent.getAttribute('raw');
578 |             const images = messageContent.getAttribute('images');
579 |             if(!content && !images) return;
580 |             await this.db.saveMessage({ id: msgId,  role: role, content: messageContent.getAttribute('raw'), timestamp: Date.now(), images: messageContent.getAttribute('images') });
581 |         };
582 |         window.rup = updateReason;
583 |         return {msgId, messageDiv, add, update, save, updateReason };
584 |       }
585 | 
586 |       async clearChat() {
587 |         const messages = await this.db.getAllMessages();
588 |         for (const message of messages) { await this.db.deleteCollectionData('messages', message.id); }
589 |         this.container.innerHTML = ''; 
590 |       }
591 | 
592 |       async loadMessages() {
593 |         const messages = await this.db.getAllMessages();
594 |         for (const message of messages) {
595 |           const { msgId, messageDiv, add, update } = await this.newMessage(message.role);
596 |           await update(message.content, {scroll:false, images:message.images ? JSON.parse(message.images) : []});
597 |           add();
598 |         }
599 |       }
600 |     }
601 | 
602 |     class AdvancedInput {
603 |       // 聊天输入框类,主要控制message-input的输入, 以及发送按钮的状态, 还有点击发送按钮后的回调
604 |       constructor() {
605 |         this.input = document.getElementById('message-input');
606 |         this.sendButton = document.getElementById('send-button');
607 |         this.sendIcon = document.getElementById('send-icon');
608 |         this.loadingIcon = document.getElementById('loading-icon');
609 |         this.imageUploadInput = document.getElementById('image-upload-input');
610 |         this.imagePreviewContainer = document.getElementById('image-preview-container');
611 |         this.mcpToggle = document.getElementById('mcp-toggle');
612 |         this.mcpStatus = 0;  // 0=>未链接, 2=>连接中, 1=>链接成功
613 |         this.mcpcli = null;
614 |         this.isProcessing = false;
615 |         this.init()
616 |       }
617 | 
618 |       init() {
619 |         this.mcpToggle.addEventListener('click', this.mcpClient.bind(this));
620 |         this.input.addEventListener('input', () => {this.sendButton.disabled = !this.input.value.trim();});
621 |         this.input.addEventListener('keydown', (e) => {if (e.key === 'Enter' && e.ctrlKey) {e.preventDefault();this.sendButton.click();}});
622 |         this.imageUploadInput.addEventListener('change', (e)=>{
623 |             const files = e.target.files;
624 |             if (files.length > 0) {
625 |                 for (const file of files) {
626 |                   const reader = new FileReader();
627 |                   reader.onload = (event) => {
628 |                       const a = document.createElement('a'); a.className = 'uk-position-relative';
629 |                       const img = document.createElement('img'); img.src = event.target.result; img.classList.add('rounded-md'); img.style.height='64px'
630 |                       a.appendChild(img);
631 |                       const deleteBtn = document.createElement('button');deleteBtn.className = 'uk-icon-button uk-position-top-right uk-position-small';
632 |                       deleteBtn.setAttribute('uk-icon', 'trash');deleteBtn.style.backgroundColor = 'rgba(255,255,255,0.4)';
633 |                       deleteBtn.onclick = (e) => {e.preventDefault(); a.remove();};
634 |                       a.appendChild(deleteBtn);
635 |                       this.imagePreviewContainer.appendChild(a);
636 |                   };
637 |                   reader.readAsDataURL(file);
638 |                 }
639 |             }
640 |         })
641 |         this.sendButton.onclick = () => {
642 |           if (this.isProcessing) return;
643 |           if(!this.input.value.trim()) { return }
644 |           
645 |           // 显示loading状态
646 |           this.isProcessing = true;
647 |           this.sendButton.disabled = false;
648 |           this.sendIcon.classList.add('hidden');
649 |           this.loadingIcon.classList.remove('hidden');
650 |           this.input.setAttribute('disabled', 'disabled');
651 |           this.imageUploadInput.setAttribute('disabled', 'disabled');
652 |           
653 |           return new Promise((resolve, reject) => {
654 |             const message = { content: this.input.value, images: [...document.getElementById('image-preview-container').querySelectorAll('img')].map(x=>x.src) };
655 |             window.aiui.onSendMessage(message, resolve, reject);
656 |           }).catch((error) => {
657 |             console.error('Error:', error);
658 |           }).finally(() => {
659 |             this.isProcessing = false;
660 |             this.input.value = '';
661 |             this.imagePreviewContainer.innerHTML = '';
662 |             this.imageUploadInput.value = '';
663 |             this.input.style.height = 'auto';
664 |             
665 |             // 恢复正常状态
666 |             this.sendIcon.classList.remove('hidden');
667 |             this.loadingIcon.classList.add('hidden');
668 |             this.input.removeAttribute('disabled');
669 |             this.imageUploadInput.removeAttribute('disabled');
670 |             this.input.focus();
671 |             this.sendButton.disabled = !this.input.value.trim();
672 |           });
673 |         };
674 |       }
675 | 
676 |       async mcpClient() {
677 |           if(this.mcpStatus===1) {
678 |             this.mcpcli.close()
679 |             this.mcpcli = null
680 |             window.aiui.ai.toolFn = null
681 |             this.mcpStatus = false
682 |             this.mcpToggle.classList.remove('uk-spinner')
683 |             this.mcpToggle.classList.remove('text-cyan-400')
684 |             this.mcpToggle.setAttribute('uk-tooltip', 'MCP已断开。请重新链接')
685 |             return
686 |           }else if(this.mcpStatus===2) {
687 |             UIkit.notification({message: '正在连接MCP,请稍等',status: 'warning',pos: 'top-center',timeout: 1000});
688 |             return
689 |           }
690 |           const mcpaddress = window.aiui.settings.settings.mcp;
691 |           if(!mcpaddress) {
692 |             UIkit.notification({message: '请先设置MCP服务器地址',status: 'warning',pos: 'top-center',timeout: 5000});
693 |             window.aiui.settings.open()
694 |             return 
695 |           }
696 |           const mcpcli = new MCPCli(mcpaddress)
697 |           this.mcpToggle.classList.add('uk-spinner')
698 |           this.mcpToggle.setAttribute('uk-tooltip', '连接中...')
699 |           this.mcpStatus = 2
700 |           mcpcli.connect()
701 |           .then(cli=>{
702 |             this.mcpcli = cli
703 |             this.mcpStatus = 1
704 |             this.mcpToggle.setAttribute('uk-tooltip', 'MCP已连接')
705 |             //this.mcpToggle.classList.remove('uk-spinner')
706 |             this.mcpToggle.classList.add('text-cyan-400')
707 |             UIkit.notification({message: 'MCP连接成功',status: 'success',pos: 'top-center',timeout: 5000});
708 |             window.aiui.ai.toolFn = cli
709 |           })
710 |           .catch(err=>{
711 |             console.error(err)
712 |             UIkit.notification({message: 'MCP连接失败',status: 'danger',pos: 'top-center',timeout: 5000});
713 |             this.mcpToggle.classList.remove('uk-spinner')
714 |             this.mcpToggle.removeAttribute('uk-tooltip')
715 |             this.mcpStatus = 0
716 |           })
717 |           
718 | 
719 |       }
720 |     }
721 | 
722 |     class AdvancedSettings {
723 |       static instance = null;
724 |       
725 |       static getInstance(db) {
726 |         if (!AdvancedSettings.instance) {
727 |           AdvancedSettings.instance = new AdvancedSettings(db);
728 |         } else if (db) {
729 |           // 如果已有实例但传入了新的db,则更新db
730 |           AdvancedSettings.instance.db = db;
731 |         }
732 |         return AdvancedSettings.instance;
733 |       }
734 | 
735 |       constructor(db) {
736 |         // 如果已经有实例,则直接返回该实例
737 |         if (AdvancedSettings.instance) {
738 |           return AdvancedSettings.instance;
739 |         }
740 |         
741 |         this.db = db;
742 |         this.settingsButton = document.getElementById('api-settings');
743 |         this.saveButton = document.getElementById('save-settings');
744 |         this.settingsModal = document.getElementById('settings-modal');
745 |         this.settings = {};
746 |         this.initialized = false;
747 |         this.init();
748 |         
749 |         AdvancedSettings.instance = this;
750 |       }
751 | 
752 |       async init() {
753 |         if (this.initialized) return;
754 |         
755 |         this.settingsButton.addEventListener('click', this.open.bind(this));
756 |         this.saveButton.addEventListener('click', this.save.bind(this));
757 |         document.getElementById('temperature').addEventListener('input', (e) => {
758 |           document.getElementById('temperature-value').textContent = e.target.value;
759 |         });
760 |         document.getElementById('topP').addEventListener('input', (e) => {
761 |           document.getElementById('top-p-value').textContent = e.target.value;
762 |         });
763 |         
764 |         this.initialized = true;
765 |         await this.get();
766 |       }
767 |       
768 |       async open() {
769 |         const settings = await this.get();
770 |         document.getElementById('apiKey').value = settings.apiKey || '';
771 |         document.getElementById('mcp').value = settings.mcp || '';
772 |         document.getElementById('baseURL').value = settings.baseURL || 'https://text.pollinations.ai/openai';
773 |         document.getElementById('model').value = settings.model || 'openai';
774 |         document.getElementById('systemPrompt').value = settings.systemPrompt || '';
775 |         document.getElementById('temperature').value = settings.temperature || 0.7;
776 |         document.getElementById('temperature-value').textContent = settings.temperature || 0.7;
777 |         document.getElementById('maxTokens').value = settings.maxTokens || 128000;
778 |         document.getElementById('topP').value = settings.topP || 1.0;
779 |         document.getElementById('top-p-value').textContent = settings.topP || 1.0;
780 |         UIkit.modal(this.settingsModal).show();
781 |       }
782 |       
783 |       close() { UIkit.modal(this.settingsModal).hide();}
784 | 
785 |       async save() {
786 |         for (const setting of ['apiKey', 'mcp', 'baseURL', 'model', 'systemPrompt', 'temperature', 'maxTokens', 'topP']) {
787 |           await this.db.setCollectionData('settings', { key: setting, value: document.getElementById(setting).value });
788 |         }
789 |         UIkit.notification({message: '设置已保存',status: 'success',pos: 'top-center',timeout: 2000});
790 |         this.close();
791 |         await this.get()
792 |         window.dispatchEvent(new CustomEvent('settingsUpdate', {detail: this.settings}));
793 |         return this.settings;
794 |       }
795 | 
796 |       async get() {
797 |         const default_settings = {
798 |             'apiKey': '', 'baseURL': 'https://text.pollinations.ai/openai','mcp':'',
799 |             'model': 'openai', 'systemPrompt': '',
800 |             'temperature': 0.61, 'maxTokens': 8192, 'topP': 0.95
801 |         }
802 |         const saved_settings = (await this.db.getSettings()).reduce((acc, setting) => {acc[setting.key] = setting.value;return acc;}, {});
803 |         this.settings = {...default_settings, ...saved_settings};
804 |         return this.settings;
805 |       }
806 |     }
807 | 
808 |     // AIUI类
809 |     class AIUI {
810 |       constructor(options = {}) {        
811 |         this.chatId = options.chatId
812 |         this.ready = this.init()
813 |       }
814 | 
815 |       async init() {
816 |         if (!this.chatId) {
817 |           if (localStorage.getItem('last_chat_id')) {
818 |             await window.conversations.loadConversation(parseInt(localStorage.getItem('last_chat_id')));
819 |             return
820 |           } else {
821 |             const chat = window.MainDatabase.getOrCreateLastChat();
822 |             await window.conversations.loadConversation(chat.id);
823 |             return
824 |           }
825 |         }
826 | 
827 |         this.db = new ChatDatabase(this.chatId);
828 |         // 使用单例模式获取容器实例
829 |         this.container = AdvancedContainer.getInstance(this.db);
830 |         this.input = new AdvancedInput();
831 |         // 使用单例模式获取设置实例
832 |         this.settings = AdvancedSettings.getInstance(this.db);
833 |         await this.settings.get();
834 | 
835 |         const messages = (await this.db.getAllMessages()).map(m => ({role:m.role, content:m.content}));
836 |         this.ai = new AI({
837 |           messages: messages,
838 |           system_prompt: this.settings.settings.systemPrompt || '',
839 |           model: this.settings.settings.model,
840 |           apiKey: this.settings.settings.apiKey || '',
841 |           baseURL: this.settings.settings.baseURL,
842 |           completionsURI: '',
843 |           opts: {
844 |             temperature: parseFloat(this.settings.settings.temperature) || 0.7,
845 |             max_tokens: parseInt(this.settings.settings.maxTokens) || 128000,
846 |             top_p: parseFloat(this.settings.settings.topP) || 1.0
847 |           }
848 |         });
849 | 
850 |         window.addEventListener('settingsUpdate', async (e) => {
851 |           this.ai = new AI({
852 |             messages: this.ai.messages,
853 |             system_prompt: e.detail.systemPrompt || '',
854 |             model: e.detail.model,
855 |             apiKey: e.detail.apiKey || '',
856 |             baseURL: e.detail.baseURL,
857 |             completionsURI: '',
858 |             opts: {
859 |               temperature: parseFloat(e.detail.temperature) || 0.7,
860 |               max_tokens: parseInt(e.detail.maxTokens) || 128000,
861 |               top_p: parseFloat(e.detail.topP) || 1.0
862 |             }
863 |           });
864 |         });
865 | 
866 |         await this.container.loadMessages();
867 |       }
868 |       
869 |       async onSendMessage(message, resolve, reject) {
870 |         const { content, images } = message;
871 |         if (!content.trim()) {reject('消息不能为空');return}
872 |         if (window.conversations.topicTitle.innerText === '') {
873 |           await window.conversations.setConversationTitle(this.chatId, content.slice(0, 30));
874 |         }
875 |         const userMessage = await this.container.newMessage('user');
876 |         await userMessage.add();
877 |         await userMessage.update(content.trim(), true, images);
878 |         await userMessage.save();
879 | 
880 |         const assistantMessage = await this.container.newMessage('assistant');
881 |         await assistantMessage.add();
882 |         await assistantMessage.update('',{spin:true});
883 |       
884 |         try {
885 | 
886 |           // 调用AI生成回复
887 |           this.currentResponse = await this.ai.create(content.trim(), {images: images});
888 |           let fullResponse = '';
889 |           let fullReasoning = ''
890 | 
891 |           // 同时处理推理和内容流
892 |           await Promise.all([
893 | 
894 |             // 实现同时处理内容流和推理流,上次有个人讽刺说openAI在推理流时不出内容流,而实际上确实没人这么做。这个实现无非是预留效果罢了。。。。。
895 | 
896 |             // 处理推理流
897 |             (async() => {
898 |               console.log('reasoning.....');
899 |               for await (const reasoning_chunk of this.currentResponse.on('reasoning')) {
900 |                 fullReasoning += reasoning_chunk.delta?.choices?.[0]?.delta?.reasoning || '';
901 |                 await assistantMessage.updateReason(fullReasoning);
902 |               }
903 |               //为了不增加token,推理流不写入数据库
904 |             })(),
905 |             
906 |             // 处理内容流
907 |             (async ()=> {
908 |               for await (const chunk of this.currentResponse.on('content')) {
909 |                 if (chunk.delta) {
910 |                   fullResponse += chunk.delta;
911 |                   await assistantMessage.update(fullResponse, true);
912 |                 }
913 |               }
914 |               await assistantMessage.save();
915 |             })()
916 | 
917 |           ])
918 |           
919 |           // 添加AI回复到消息历史
920 |           this.ai.messages.push({
921 |             role: 'assistant',
922 |             content: fullResponse
923 |           });
924 |           
925 |           resolve(fullResponse);
926 |         } catch (error) {
927 |           await assistantMessage.update('抱歉,哪里出现了错误了。 请稍后再试。');
928 |           reject(error);
929 |         } 
930 |       }
931 |     }
932 | 
933 |     document.addEventListener('DOMContentLoaded', async function() {
934 |       
935 |         const { markedHighlight } = window.markedHighlight;
936 |         marked.use(markedHighlight({
937 |             langPrefix: 'hljs language-',
938 |             highlight: function(code, lang) {
939 |                 if (lang === 'mermaid') { return '<div class="mermaid">' + code + '</div>';}
940 |                 const language = hljs.getLanguage(lang) ? lang : 'plaintext'
941 |                 return hljs.highlight(code, {language: language}).value;
942 |             }
943 |         }));
944 | 
945 |         window.conversations = new AdvancedConversation();
946 |         await window.conversations.loadConversations();
947 |         window.aiui = new AIUI();
948 |         await window.aiui.ready
949 | 
950 |         window.scrollTo(0, 1);
951 |         if (/()iPhone|iPad|iPod/.test(navigator.userAgent)) {
952 |             document.body.style.height = '100vh';
953 |             document.body.overflow = 'hidden';
954 |             document.body.requestFullscreen()
955 |         }
956 | 
957 |     });
958 |   </script>
959 | </body>
960 | </html>
961 | 
```