#
tokens: 29069/50000 12/12 files
lines: off (toggle) GitHub
raw markdown copy
# 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
version: '3.8'
services:
  mcpez:
    image: mcpez
    build:
      context: .
      dockerfile: dockerfile
    ports:
      - "8777:80"
```

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

```dockerfile

FROM alpine:latest

RUN apk update
RUN apk add bash openresty openrc python3 py3-pip sqlite pipx nodejs npm 
RUN pipx install uv 
RUN pipx ensurepath
ENV PATH="/root/.local/bin:$PATH"
RUN mkdir -p /data/app
WORKDIR /data/app
RUN uv venv
RUN uv pip install tornado fastapi uvicorn sqlmodel httpx httpx-sse 
RUN mkdir -p /var/run/mcpez
COPY ./mcpeasy-entrypoint.sh /mcpeasy-entrypoint.sh
COPY mcpez_ngx.conf /etc/nginx/mcpez.conf
COPY ./bin /data/app/bin 
COPY ./mcpez /data/app/mcpez
COPY ./webui /data/app/webui
RUN chmod +x /mcpeasy-entrypoint.sh 
ENTRYPOINT ["/mcpeasy-entrypoint.sh"]
```

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

```
worker_processes  1;
daemon on;

events {
    worker_connections  102400;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

    server {
        listen       80;

        access_log  /var/log/nginx/access.log  main;
	    error_log   /var/log/nginx/error.log  error;

        location / {
            # 界面界面
            root /data/app/webui;
            index index.html;
        }

        location /api {
            # API 代理设置
            rewrite ^/api/(.*)$ /$1 break;
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_connect_timeout 5s;
        }


        location /mcp/ {
            # MCP 代理设置,真的mcp都会在这里被挂载
            set $mcp_upstream "unix:/tmp";
            set $original_uri $request_uri;

            keepalive_timeout 180;

            rewrite_by_lua_block {

                local captures, err = ngx.re.match(ngx.var.request_uri, "^/mcp/([a-zA-Z0-9\\-_]+)(/.*)?","jio")
                if not captures then
                    ngx.status = ngx.HTTP_BAD_REQUEST
                    ngx.log(ngx.ERR, "Invalid request format: ", ngx.var.request_uri)
                    ngx.say("Invalid request format")
                    return
                end

                local key = captures[1]
                local path_suffix = captures[2] or "/"


                ngx.var.mcp_upstream = "unix:/var/run/mcpez/"..key..".sock"
            }

            # 代理设置
            proxy_pass http://$mcp_upstream;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_connect_timeout 5s;
            proxy_buffering off;
        }

    }
}
```

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

```python
from libs.mcpcli import MCPCli
from libs.mcphandler import make_mcp_handlers
import copy
import os

class MCPEazy:

    def __init__(self, name, description=''):
        self.servers = {}
        self.name = name
        self.description = description
        self.functions = {}
        self.ctxStore = None
        self.pathroute = None
        self.apps = None

    async def add_mcp_server(self, name, config):
        subsrv = MCPCli(config)
        await subsrv.init()
        self.servers[name] = subsrv
        return self.servers[name]

    async def add_to_server(self, app, pathroute=''):
        self.apps = app
        self.pathroute = pathroute
        make_mcp_handlers(app, self, pathroute=pathroute, name=self.name)

    async def stop(self):
        names = list(self.servers.keys())
        for name in names:
            srv = self.servers.pop(name, None)
            srv.close()
        for route in self.apps.default_router.rules[0].target.rules:
            if route.target_kwargs.get('name') == self.name:
                self.apps.default_router.rules[0].target.rules.remove(route)

    def get_tools(self, openai=None):
        tools = []
        self.functions = {}
        for srv in self.servers.values():
            for tool in srv.tools:
                namehash = os.urandom(5).hex()
                self.functions[namehash] = {'name':tool['name'], 'srv':srv}
                _tool = copy.deepcopy(tool)
                _tool['name'] = namehash
                tools.append(_tool)
        return tools

    async def list_tools(self, req, session):
        await session.write_jsonrpc(req['id'], {'tools':self.get_tools()})

    async def call_tools(self, req, session):
        name = req['params'].get('name')
        if name not in self.functions:
            return await session.write_jsonrpc(req['id'], {'error': {'code': -32601, 'message': f"Method {name} not found"}})
        _srv = self.functions[name]
        try:
            req['params']['name'] = _srv['name']
            result = await _srv['srv'].request('tools/call', req['params'])
            return await session.write_jsonrpc(req['id'], {'result': result.get('result')})
        except Exception as e:
            return await session.write_jsonrpc(req['id'], {'error': {'code': -32603, 'message': str(e)}})
```

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

```python
import asyncio
import json
import tornado.web
import argparse
import sys, os
import logging

from tornado.httpserver import HTTPServer
from tornado.netutil import bind_unix_socket

from libs.mcpez import MCPEazy
from sqlmodel import Field, SQLModel, Session, create_engine

logging.basicConfig(level=logging.DEBUG)


optparser = argparse.ArgumentParser(description='MCP代理服务,支持从文件或数据库加载配置')
optparser.add_argument('-c', '--id', type=int, default=0, help='app id')
optparser.add_argument('-s', '--socketdir', default='/var/run/mcpez', help='服务器端口号,也可以是一个uds路径,')
optparser.add_argument('-n', '--name', default='mcpproxy',help='代理服务名称')
optargs = optparser.parse_args()

class AppDB(SQLModel, table=True):
    id: int = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    description: str = Field(default='')
    item_type: str = Field(index=True)
    config: str
    functions: str = None
    create_at: int = None
    modify_at: int = None


class MCPProxy(MCPEazy):
    def __init__(self, name="MCPProxy"):
        super().__init__(name)
    
    async def load_config_from_db(self, app_id):
        engine = create_engine('sqlite:///./mcpez.db')
        with Session(engine) as session:
            try:
                app = session.get(AppDB, app_id)
                if not app: raise Exception('Config is not Found')
                if app.item_type != 'app': raise Exception('item is not a app')
                self.name = app.name
                self.description = app.description
                return json.loads(app.config)
            except Exception as e:
                logging.error(e)
                sys.exit(1)
    
    async def load_config(self):
        try:
            config = await self.load_config_from_db(optargs.id)
            active_servers = []
            for name, server_config in config.get('mcpServers', {}).items():
                try:
                    await self.add_mcp_server(name, server_config)
                    active_servers.append(name)
                except Exception as e:
                    logging.error(e)
            
            if not active_servers: raise Exception('no active servers found')
            return active_servers
        except Exception as e:
            logging.error(f"配置加载失败: {str(e)}")
            return []
    
    @staticmethod
    async def start():
        proxy = MCPProxy(name=optargs.name)
        await proxy.load_config()
        app = tornado.web.Application(debug=True)
        await proxy.add_to_server(app, pathroute=f"/mcp/{optargs.id}")
        server = HTTPServer(app)
        socket_file = f"{optargs.socketdir}/{optargs.id}.sock"
        server.add_socket(bind_unix_socket(socket_file))
        os.system(f"chmod 777 {socket_file}")
        logging.info(f"HTTP Server runing at unix:{socket_file}:/mcp/{optargs.id}/sse")
        server.start()


async def main():
    await MCPProxy.start()
    await asyncio.Event().wait()


if __name__ == "__main__":
    asyncio.run(main())
```

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

```javascript
(function(global) {
class MCPCli {
    constructor(server, {fetcher=fetch, timeout=60000, format='openai'}={}) {
        this.server = server||'https://mcp-main.toai.chat/sse';
        this.endpoint = null;
        this.jsonrpc_id = 0;
        this.responses = {}
        this._tools = null
        this._format = format
        this.tools = []
        this.timeout = timeout
        this.fetcher = fetcher
        this.stream = null
    }

    eventLoop(options) {
        const { resolve, reject, timeout } = options;
        const Stream = new EventSource(this.server);
        this.stream = Stream

        setTimeout(()=>{
            if(!this.endpoint) { reject('timeout');Stream.close()}
        }, timeout||this.timeout||60000)

        Stream.addEventListener('message', (event) => {
            const data = JSON.parse(event.data);
            this.responses[data.id] && this.responses[data.id](data)
        });

        Stream.addEventListener('endpoint', (event) => {
            this.endpoint = event.data.startsWith('/') ? new URL(event.data, new URL(this.server)).toString() : event.data;
            resolve(this.endpoint)
        });
    }

    async connect(timeout) {
        await (new Promise((resolve, reject) => { this.eventLoop({resolve, reject, timeout});}))
        await this.send('initialize', {'protocolVersion':'2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'JSMCPCli', 'version': '0.1.0'}})
        this.send('notifications/initialized', {}, {idpp:false})
        this._tools = await this.send('tools/list')
        this.tools = this.transform(this._format)
        return this
    }

    close() {
        this.stream.close()
    }

    send(method, params={}, args={}) {
        const { idpp=true, timeout=this.timeout } = args;
        return new Promise((resolve, reject) => {
            idpp ? this.jsonrpc_id++ : null
            const bodyObject = { jsonrpc: '2.0', id: this.jsonrpc_id, method: method, params: params }
            setTimeout(()=>{
                if (!this.responses[this.jsonrpc_id]) return
                delete this.responses[this.jsonrpc_id]
                reject('timeout')
            }, timeout);
            this.responses[this.jsonrpc_id] = (data) => {
                if (!this.responses[this.jsonrpc_id]) return
                delete this.responses[this.jsonrpc_id]
                resolve(data.result)
            }
            fetch(this.endpoint, {method: 'POST', body: JSON.stringify(bodyObject),headers: { 'Content-Type': 'application/json' }})
        })
    }

    transform (format) {
        if (!this._tools) return []
        if (format === 'claude') return this._tools.tools
        if (format === 'openai') {
            return this._tools.tools.map(tool => {
                return {
                    type: 'function',
                    function: {
                        name: tool.name,
                        description: tool.description,
                        parameters: {
                            type: 'object',
                            properties: tool.inputSchema.properties,
                            required: tool.inputSchema.required,
                            additionalProperties: false
                        },
                        strict: true
                    },
                }
            })
        }
    }

    execute(name, args) {
        return this.send('tools/call', {name:name, arguments:args})
    }

  }

  global.MCPCli = MCPCli;
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = { MCPCli };
  } else if (typeof define === 'function' && define.amd) {
    define(function() { return { MCPCli }; });
  }
  
})(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);

//(new MCPCli()).connect().then(cli=>cli.execute('get_stock_price', {symbol:'AAPL'})).then(console.log).catch(console.error)

```

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

```python
from tornado.web import RequestHandler
import json
import os
import logging
import time


class SSEServer(RequestHandler):

    def initialize(self, *args, **kwargs):
        self._auto_finish = False
        self.ctxStore = kwargs.get('ctxStore')
        self.pathroute = kwargs.get('pathroute', '')

    def set_default_headers(self):
        self.set_header('Content-Type', 'text/event-stream')
        self.set_header('Cache-Control', 'no-cache')
        self.set_header('Connection', 'keep-alive')
        self.set_header('Access-Control-Allow-Origin', '*')
        self.set_header('Access-Control-Allow-Headers', 'Content-Type')
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')

    def options(self): self.set_status(204)

    async def get(self):
        self.ctxid = os.urandom(16).hex()
        self.ctxStore[self.ctxid] = self
        await self.write_sse(self.pathroute+'/messages/?session_id=' + self.ctxid, 'endpoint')

    async def write_sse(self, data, event='message'):
        self.write('event: ' + event + '\r\ndata: ' + data + '\r\n\r\n')
        await self.flush()

    async def write_jsonrpc(self, req_id, result):
        response = {'jsonrpc': '2.0', 'id': req_id, 'result': result}
        await self.write_sse( json.dumps(response) )

    def on_connection_close(self):
        if not hasattr(self, 'ctxid'): return
        self.ctxStore.pop(self.ctxid, None)




class RPCServer(RequestHandler):

    def initialize(self, *args, **kwargs):
        self.executor = kwargs.get('executor')
        self.ctxStore = kwargs.get('ctxStore')

    def set_default_headers(self):
        self.set_header('Content-Type', 'text/event-stream')
        self.set_header('Cache-Control', 'no-cache')
        self.set_header('Connection', 'keep-alive')
        self.set_header('Access-Control-Allow-Origin', '*')
        self.set_header('Access-Control-Allow-Headers', 'Content-Type')
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')

    def options(self): self.set_status(204)

    async def post(self):
        ctxid = self.get_argument('session_id')
        session = self.ctxStore.get(ctxid)
        req = json.loads(self.request.body)
        req_method = req.get('method')
        func = self.for_name(req_method)
        if func: await func(req, session)
        self.set_status(202)
        self.finish('Accepted')

    def for_name(self, method):
        return getattr(self, 'with_' + method.replace('/', '_'), None)

    async def with_initialize(self, req, session):
        req_id = req.get('id')
        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"}}
        await session.write_jsonrpc(req_id, result)

    async def with_tools_list(self, req, session):
        try:
            self.executor and await self.executor.list_tools(req, session)
        except Exception as e:
            await session.write_jsonrpc(req['id'], {'tools': []})
    
    async def with_ping(self, req, session):
        req_id = req.get('id')
        result = {}
        await session.write_jsonrpc(req_id, result)

    async def with_tools_call(self, req, session):
        try:
            self.executor and await self.executor.call_tools(req, session)
        except Exception as e:
            await session.write_jsonrpc(req['id'], {'result': 'error'})







class ServerStatus(RequestHandler):
    def initialize(self, *args, **kwargs):
        self.ctxStore = kwargs.get('ctxStore')
        self.init_time = kwargs.get('init_time')
        self.name = kwargs.get('name')
        self.executor = kwargs.get('executor')


    def set_default_headers(self):
        self.set_header('Content-Type', 'application/json')
        self.set_header('Cache-Control', 'no-cache')
        self.set_header('Connection', 'keep-alive')
        self.set_header('Access-Control-Allow-Origin', '*')
        self.set_header('Access-Control-Allow-Headers', 'Content-Type')
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')

    def options(self): self.set_status(204)

    async def get(self):
        self.finish(dict(
            name = self.name,
            description = self.executor.description,
            init_time = self.init_time,
            status = 'ok',
            connection_cnt = len(self.ctxStore),
            tools = self.executor.get_tools()
            ))




def make_mcp_handlers(application, executor, pathroute='',name=''):
    ctxStore = {}
    executor.ctxStore = ctxStore
    executor.pathroute = pathroute
    application.add_handlers('.*', [
        (pathroute + '/sse', SSEServer, {'ctxStore': ctxStore, 'pathroute': pathroute, 'name':name}),
        (pathroute + '/messages/', RPCServer, {'executor': executor, 'ctxStore': ctxStore, 'name':name}),
        (pathroute + '/server_status', ServerStatus, {'ctxStore': ctxStore,  'executor': executor, 'name': name, 'init_time': int(time.time())}),
    ])
```

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

```python
import asyncio
import json
import logging


class MCPCli:

    def __init__(self, config, **kwargs):
        self.rpcid = 0
        self.rpc_responses = {}
        self.config = config
        self.name = kwargs.get('name')
        self.timeout = kwargs.get('timeout', 60)
        self.config['type'] = 'sse' if config.get('baseUrl') else 'stdio'
        self.write = None
        self.tools = None
        self.process = None
        logging.info('created Mcpcli instance')


    async def request(self, method, params, with_response=True, with_timeout=None):
        assert self.write, "Connection not established"
        
        if with_response:
            self.rpcid += 1
            json_rpc_data = {'method': method, 'params': params, 'jsonrpc': '2.0', 'id': self.rpcid}
            fut = asyncio.Future()
            self.rpc_responses[self.rpcid] = fut
            logging.debug(f"Sending request: {json_rpc_data}")
            await self.write(json.dumps(json_rpc_data).encode() + b'\n')
            try:
                return await asyncio.wait_for(fut, timeout=with_timeout or self.timeout)
            except asyncio.TimeoutError:
                raise TimeoutError(f"Timeout waiting for response to {method}({params})")
        else:
            json_rpc_data = {'method': method, 'params': params, 'jsonrpc': '2.0'}
            await self.write(json.dumps(json_rpc_data).encode() + b'\n')


    async def init(self):
        if self.config['type'] == 'stdio':
            await self.start_stdio_mcp()
        elif self.config['type'] == 'sse':
            await self.start_sse()


    def close(self):
        if self.config['type'] == 'stdio':
            self.process.terminate()
        elif self.config['type'] == 'sse':
            asyncio.ensure_future(self.process.aclose())


    async def start_sse(self):
        import urllib.parse
        from httpx_sse import EventSource
        import httpx
        client = httpx.AsyncClient(verify=False, timeout=httpx.Timeout(None, connect=10.0))
        session_addr = None
        is_connected = asyncio.Future()

        async def start_loop():
            try:
                async with client.stream('GET', self.config['baseUrl']) as response:
                    self.process = client
                    event_source = EventSource(response)
                    async for event in event_source.aiter_sse():
                        if client.is_closed: break
                        if event.event == 'endpoint':
                            session_addr = event.data if event.data.startswith('http') else urllib.parse.urljoin(self.config['baseUrl'], event.data)
                            is_connected.set_result(session_addr)
                        elif event.event == 'message':
                            try:
                                data = json.loads(event.data)
                                if data.get('id') and data.get('result'):
                                    future = self.rpc_responses.get(data['id'])
                                    if future: future.set_result(data)
                            except json.JSONDecodeError:
                                logging.warning(f"Failed to decode JSON: {repr(event.data)}")
            except Exception as e:
                await client.aclose()

        def writer(data):
            return client.post(session_addr, data=data)

        
        asyncio.ensure_future(start_loop())
        session_addr = await is_connected
        self.write = writer
        await self.request('initialize', {'protocolVersion':'2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'EzMCPCli', 'version': '0.1.2'}}, with_response=False)
        await self.request('notifications/initialized', {}, with_response=False)
        r = await self.request('tools/list', {}, with_timeout=10)
        self.tools = r.get('result', {}).get('tools', [])


    
    async def start_stdio_mcp(self):
        proc = await asyncio.create_subprocess_exec(
            self.config['command'],
            *self.config['args'],
            env=self.config.get('env', None),
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        self.process = proc

        async def writer(data):
            proc.stdin.write(data)

        self.write = writer

        async def start_loop():
            while True:
                line = await proc.stdout.readline()
                if not line:
                    break
                try:
                    data = json.loads(line.decode())
                    if 'id' in data and data['id'] in self.rpc_responses:
                        self.rpc_responses[data['id']].set_result(data)
                        del self.rpc_responses[data['id']]
                    else:
                        logging.debug(f"Received notification: {data}")
                except json.JSONDecodeError as e:
                    logging.warning(f"Failed to decode JSON: {repr(line)}")

        asyncio.ensure_future(start_loop())

        await self.request('initialize', {'protocolVersion':'2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'EzMCPCli', 'version': '0.1.2'}}, with_response=False)
        await self.request('notifications/initialized', {}, with_response=False)
        r = await self.request('tools/list', {}, with_timeout=30)
        self.tools = r.get('result', {}).get('tools', [])


```

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

```javascript
// Version: 1.1.0
(function(global) {
  class AI {
    constructor(options = {}) {
      this.api_key = options.api_key || options.apiKey || '';
      this.baseURL = options.baseURL || options.endpoint || 'https://porky.toai.chat/to/openrouter';
      this.completionsURI = options.completionsURI === undefined ? '/chat/completions' : '';
      this.model = options.model || 'deepseek/deepseek-r1-distill-qwen-32b:free';
      this.messages = options.messages || [];
      this.system_prompt = options.system_prompt || options.sysprompt || '';
      this.abortController = null;
      this.toolFn = options.tool_fn || options.mcpserver || null;
      this.opts = { temperature: 0.7, max_tokens: 128000, top_p: 1, stream: true, ...options.opts };
      this.completions = { create: this.create.bind(this) };
      this.filters = {}; 

      this.add_response_filter('reasoning', data=>!!data.choices?.[0]?.delta?.reasoning);
      
      return this;
    }
    
    add_response_filter(queue_name, filterFn) {
      if (!this.filters[queue_name]) this.filters[queue_name] = [];
      this.filters[queue_name].push(filterFn);
      return this;
    }
    
    cancel() { 
      if (this.abortController) {
        this.abortController.abort();
        this.abortController = null;
      }
    }
    
    async create(prompt, args) {
      const { options={}, images=[], audio=[], files=[] } = args || {};

      const messages = [...(options.messages || this.messages || [])];
      if (this.system_prompt) messages.unshift({ role: 'system', content: this.system_prompt });
      if (prompt || images?.length || audio?.length || files?.length) 
        messages.push(this.prepareUserMessage(prompt, { images, audio, files }));
      
      const reqOptions = {
        model: options.model || this.model,
        messages,
        temperature: options.temperature || this.opts.temperature,
        max_tokens: options.max_tokens || this.opts.max_tokens,
        top_p: options.top_p || this.opts.top_p,
        stream: options.stream !== undefined ? options.stream : this.opts.stream,
        ...(this.toolFn?.tools && { tools: this.toolFn.tools, tool_choice: options.tool_choice || "auto" })
      };
      
      this.abortController = new AbortController();
      
      try {
        const response = await fetch(`${this.baseURL}${this.completionsURI}`, {
          method: 'POST', 
          headers: {
            'Content-Type': 'application/json',
            ...(this.api_key ? { 'Authorization': `Bearer ${this.api_key}` } : {})
          },
          body: JSON.stringify(reqOptions),
          signal: this.abortController.signal
        });
        
        if (!response.ok) {
          const error = await response.json().catch(() => ({}));
          throw new Error(`API错误: ${response.status} ${error.error?.message || ''}`);
        }
        
        if (!reqOptions.stream) {
          const result = await response.json();
          return new ResponseIterator({ 
            type: 'final', 
            message: result.choices?.[0]?.message || { role: 'assistant', content: '' }, 
            ai: this, final: true,
            filters: this.filters
          });
        }
        
        const iter = new ResponseIterator({ ai: this, reqOptions, filters: this.filters });
        this.processStream(response, iter);
        return iter;
      } catch (error) { throw error; }
    }
    
    async processStream(response, iter) {
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      const msg = { role: 'assistant', content: '' }; // 移除 reasoning_content 字段
      const toolCalls = [];
      let buffer = '';
      
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          
          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';
          
          for (const line of lines) {
            const event = this.parseSSELine(line);
            if (!event) continue;
            
            if (event.isDone) {
              await this.finalizeResponse(msg, toolCalls, iter);
              return;
            }
            
            await this.processEvent(event, msg, toolCalls, iter);
          }
        }
        
        // 处理最后可能的缓冲数据
        if (buffer.trim()) {
          const event = this.parseSSELine(buffer);
          if (event && !event.isDone) {
            await this.processEvent(event, msg, toolCalls, iter);
          }
        }
        
        await this.finalizeResponse(msg, toolCalls, iter);
      } catch (error) {
        iter.error(error);
      } finally {
        reader.releaseLock();
        this.abortController = null;
      }
    }
    
    parseSSELine(line) {
      line = line.trim();
      if (!line) return null;
      
      // 检测[DONE]标记
      if (line === 'data: [DONE]') {
        return { isDone: true };
      }
      
      // 解析数据
      let eventType = '';
      let data = '';
      
      if (line.startsWith('event:')) {
        eventType = line.slice(6).trim();
      } else if (line.startsWith('data:')) {
        data = line.slice(5).trim();
        try {
          return { type: eventType || 'content', data: JSON.parse(data), isDone: false };
        } catch (e) {
          return { type: eventType || 'content', data, isDone: false, raw: true };
        }
      } else {
        try {
          return { type: 'content', data: JSON.parse(line), isDone: false };
        } catch (e) {
          return { type: 'content', data: line, isDone: false, raw: true };
        }
      }
      
      return null;
    }
    
    async processEvent(event, msg, toolCalls, iter) {
      const { type, data, raw } = event;

      // 应用过滤器并推送到队列
      Object.keys(this.filters).forEach((filter_name)=>{
        const filter = this.filters[filter_name];
        const shouldpush = filter.some(f=>f(data))
        if (shouldpush) {
          iter.push(filter_name, { type: filter_name, message: {...msg}, delta: data });
        }
      })

      // 处理对象数据
      if (!raw && typeof data === 'object') {
        // 处理工具调用
        if (data.choices && data.choices[0]?.delta?.tool_calls) {
          this.updateToolCalls(data.choices[0].delta.tool_calls, toolCalls);
          iter.push('tool_calls', { type: 'tool_calls', toolCalls: [...toolCalls], message: {...msg}, delta: data });

          if (data.choices[0].finish_reason === 'tool_calls') {
            msg.tool_calls = [...toolCalls];
            iter.push('tool_request', { type: 'tool_request', toolCalls: [...toolCalls], message: {...msg} });
            return await this.handleToolCalls(msg, toolCalls, iter);
          }
          return;
        }

        // 处理内容更新
        const content = data.choices?.[0]?.delta?.content || '';
        if (content) {
          msg.content += content;
          iter.push('content', { type: 'content', content, message: {...msg}, delta: content });
          return;
        }
      } else {
        // 处理字符串和其他类型的数据
        switch (type) {
          case 'tool_calls':
            if (Array.isArray(data)) {
              this.updateToolCalls(data, toolCalls);
              iter.push('tool_calls', { type, toolCalls: [...toolCalls], message: {...msg}, delta: data });

              if (toolCalls.every(t => t.function?.arguments)) {
                msg.tool_calls = [...toolCalls];
                iter.push('tool_request', { type: 'tool_request', toolCalls: [...toolCalls], message: {...msg} });
                return await this.handleToolCalls(msg, toolCalls, iter);
              }
            }
            break;
          case 'content':
            if (data) {
              msg.content += data;
              iter.push('content', { type, content: data, message: {...msg}, delta: data });
            }
            break;
        }
      }
    }
    
    async finalizeResponse(msg, toolCalls, iter) {
      if (toolCalls.length > 0 && toolCalls.every(t => t.function?.name)) {
        msg.tool_calls = [...toolCalls];
        iter.push('tool_request', { type: 'tool_request', toolCalls: [...toolCalls], message: {...msg} });
        return await this.handleToolCalls(msg, toolCalls, iter);
      } else {
        iter.push('final', { type: 'final', message: msg });
        iter.complete();
      }
    }
    
    async handleToolCalls(msg, toolCalls, iter) {
      if (!this.toolFn) {
        iter.push('final', { type: 'final', message: msg });
        iter.complete();
        return;
      }
      
      const results = await Promise.all(toolCalls.map(async tool => {
        try {
          const args = JSON.parse(tool.function.arguments);
          const result = await this.toolFn.execute(tool.function.name, args);
          return {
            tool_call_id: tool.id, 
            role: "tool",
            content: typeof result === 'string' ? result : JSON.stringify(result)
          };
        } catch (err) { 
          return { 
            tool_call_id: tool.id, 
            role: "tool", 
            content: JSON.stringify({ error: err.message }) 
          }; 
        }
      }));
      
      iter.push('tool_response', { type: 'tool_response', toolResults: results, toolCalls });
      
      try {
        // 继续对话,将工具结果添加到消息历史中
        const nextResponse = await this.create("", { 
          options: {
            ...iter.reqOptions,
            messages: [
              ...iter.reqOptions.messages, 
              {...msg},
              ...results.map(r => ({ 
                role: "tool", 
                tool_call_id: r.tool_call_id, 
                content: r.content 
              }))
            ],
            tool_choice: "auto"
          }
        });
        iter.chainWith(nextResponse);
      } catch (error) { iter.error(error); }
    }
    
    prepareUserMessage(message, { images, audio, files } = {}) {
      if ((!images?.length) && (!audio?.length) && (!files?.length)) 
        return { role: "user", content: message };
      
      const content = [];
      if (message?.trim()) content.push({ type: "text", text: message });
      
      if (images?.length) images.forEach(img => content.push({
        type: "image_url", image_url: { url: img.url || img, detail: img.detail || "auto" }
      }));
      
      if (audio?.length) audio.forEach(clip => 
        content.push({ type: "audio", audio: { url: clip.url || clip } }));
      
      if (files?.length) files.forEach(file => 
        content.push({ type: "file", file: { url: file.url || file } }));
      
      return { role: "user", content };
    }
    
    updateToolCalls(deltas, toolCalls) {
      if (!Array.isArray(deltas)) return;
      
      deltas.forEach(d => {
        const idx = d.index;
        if (!toolCalls[idx]) toolCalls[idx] = { 
          id: d.id || '', type: 'function', function: { name: '', arguments: '' } 
        };
        
        if (d.id) toolCalls[idx].id = d.id;
        if (d.function?.name) toolCalls[idx].function.name = d.function.name;
        if (d.function?.arguments) toolCalls[idx].function.arguments += d.function.arguments;
      });
    }
  }

  class ResponseIterator {
    constructor({ ai, reqOptions, type, message, final = false, filters = {} }) {
      this.ai = ai;
      this.reqOptions = reqOptions;
      this.queues = new Map();
      this.waiters = new Map();
      this.chained = null;
      this.completed = false;
      this.filters = filters;
      
      // 创建所有队列 - 移除 reasoning_content
      ['content', 'tool_calls', 'tool_request', 
       'tool_response', 'streaming', 'final', ...Object.keys(filters)].forEach(t => {
        this.queues.set(t, []);
        this.waiters.set(t, []);
      });
      
      if (final && type && message) {
        this.push(type, { type, message });
        this.complete();
      }
    }
    
    push(type, data) {
      if (this.completed) return;
      // 仅处理基本队列
      const queue = this.queues.get(type) || [];
      const waiters = this.waiters.get(type) || [];
      queue.push(data);
      if (waiters.length) waiters.shift()();
    }
    
    complete() {
      this.completed = true;
      for (const waiters of this.waiters.values()) waiters.forEach(r => r());
    }
    
    chainWith(r) { this.chained = r; this.complete(); }
    error(e) { this.errorObj = e; this.complete(); }
    
    on(type) {
      if (!this.queues.has(type)) throw new Error(`未知事件类型: ${type}`);
      const self = this;
      
      return {
        async *[Symbol.asyncIterator]() {
          try {
            if (self.errorObj) throw self.errorObj;
            let queue = self.queues.get(type), index = 0;
            
            while (true) {
              if (index >= queue.length) {
                if (self.completed && !self.chained) break;
                if (self.chained) { 
                  for await (const item of self.chained.on(type)) yield item;
                  break;
                }
                
                await new Promise(r => self.waiters.get(type).push(r));
                if (self.errorObj) throw self.errorObj;
                queue = self.queues.get(type);
              } else yield queue[index++];
            }
          } catch (e) { throw e; }
        }
      };
    }
    
    async *[Symbol.asyncIterator]() { yield* this.on('streaming'); }
  }

  global.AI = AI;
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = { AI };
  } else if (typeof define === 'function' && define.amd) {
    define(function() { return { AI }; });
  }
})(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);
```

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

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MCP服务状态管理</title>
    <!-- 引入TailwindCSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: {
                            50: '#e3f2fd',
                            100: '#bbdefb',
                            200: '#90caf9',
                            300: '#64b5f6',
                            400: '#42a5f5',
                            500: '#2196f3',
                            600: '#1e88e5',
                            700: '#1976d2',
                            800: '#1565c0',
                            900: '#0d47a1'
                        },
                        secondary: '#64748b'
                    }
                }
            }
        }
    </script>
    <!-- 引入UIkit -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>
    <style>
        .uk-card {
            border-radius: 1rem;
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
        }
        .uk-button {
            border-radius: 0.5rem;
            transition: all 0.2s ease;
        }
        .uk-button-primary {
            background: linear-gradient(145deg, #1e88e5, #1976d2);
            box-shadow: 0 4px 6px -1px rgba(30, 136, 229, 0.2);
        }
        .uk-button-primary:hover {
            transform: translateY(-1px);
            box-shadow: 0 10px 15px -3px rgba(30, 136, 229, 0.3);
        }
        .uk-input, .uk-select, .uk-textarea {
            border-radius: 0.5rem;
            transition: all 0.2s ease;
        }
        .uk-input:focus, .uk-select:focus, .uk-textarea:focus {
            box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.2);
        }
        .uk-form-label {
            font-weight: 500;
            margin-bottom: 0.5rem;
            color: #334155;
        }
        .service-table tr {
            transition: all 0.2s ease;
        }
        .service-table tr:hover {
            background-color: rgba(30, 136, 229, 0.05);
        }
        .service-table tr.active {
            background-color: rgba(30, 136, 229, 0.1);
            border-left: 4px solid #1e88e5;
        }
        .badge {
            padding: 0.25rem 0.75rem;
            border-radius: 9999px;
            font-size: 0.75rem;
            font-weight: 500;
        }
        /* 定制滚动条 */
        ::-webkit-scrollbar {
            width: 6px;
            height: 6px;
        }
        ::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 3px;
        }
        ::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 3px;
        }
        ::-webkit-scrollbar-thumb:hover {
            background: #a8a8a8;
        }
        @media (max-width: 959px) {
            .mobile-full-width {
                width: 100% !important;
            }
        }
    </style>
</head>
<body class="bg-slate-100 min-h-screen">
    <!-- 导航栏 -->
    <nav class="bg-gradient-to-r from-primary-700 to-primary-500 shadow-lg">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16">
                <div class="flex items-center text-white">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                        <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" />
                    </svg>                      
                    <span class="ml-2 text-xl font-bold">MCP服务状态管理</span>
                </div>
                <div class="flex items-center gap-6">
                    <div class="relative">
                        <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="搜索应用...">
                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                            <svg class="w-5 h-5 text-gray-300" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <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"/>
                            </svg>
                        </div>
                    </div>
                    <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">
                        <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">
                            <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" />
                        </svg>                          
                        PlayGround
                    </a>
                </div>
            </div>
        </div>
    </nav>

    <!-- 主界面 -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        <div class="bg-white rounded-2xl shadow-xl p-6 overflow-hidden">
            <div class="flex justify-between items-center mb-6">
                <h2 class="text-2xl font-bold text-gray-800">应用服务列表</h2>
                <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>
            </div>

            <!-- 加载指示器 -->
            <div id="statusLoading" class="flex justify-center items-center py-8 hidden">
                <div uk-spinner></div>
                <span class="ml-3">加载服务状态...</span>
            </div>

            <div class="overflow-x-auto max-h-[600px] overflow-y-auto">
                <table class="w-full text-left service-table">
                    <thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider sticky top-0 z-10">
                        <tr>
                            <th class="px-4 py-3 rounded-tl-lg">应用ID</th>
                            <th class="px-4 py-3">名称</th>
                            <th class="px-4 py-3">描述</th>
                            <th class="px-4 py-3">状态</th>
                            <th class="px-4 py-3">详情</th>
                            <th class="px-4 py-3 rounded-tr-lg text-right">操作</th>
                        </tr>
                    </thead>
                    <tbody id="serviceTableBody" class="divide-y divide-gray-100">
                        <!-- 应用列表项会动态添加到这里 -->
                    </tbody>
                </table>
            </div>

            <div id="noServicesMessage" class="text-center py-10 text-gray-500">
                还没有可运行的MCP服务,请开始 <a href="edit.html" target="_blank">[新建服务]</a>
            </div>
            
        </div>
    </div>

    <!-- 应用详情模态框 -->
    <div id="app-details-modal" uk-modal>
        <div class="uk-modal-dialog uk-modal-body rounded-xl w-[80%]">
            <button class="uk-modal-close-default" type="button" uk-close></button>
            <h2 class="text-2xl font-bold mb-4" id="appModalTitle">应用详情</h2>
            
            <div class="space-y-4">
                <div>
                    <h3 class="text-sm font-medium text-gray-500">应用描述</h3>
                    <p id="appModalDescription" class="mt-1 text-gray-900">加载中...</p>
                </div>
                
                <div>
                    <h3 class="text-sm font-medium text-gray-500">服务配置</h3>
                    <div class="mt-2 bg-gray-50 p-4 rounded-lg">
                        <pre id="appModalConfig" class="whitespace-pre-wrap overflow-x-auto text-xs">加载中...</pre>
                    </div>
                </div>
            </div>
            
            <div class="flex justify-end space-x-4 mt-6">
                <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">
                    <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M8 5v14l11-7-11-7z" fill="currentColor"/>
                    </svg>
                    启动服务
                </button>
                <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">
                    <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M6 18L18 6M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                    </svg>
                    关闭
                </button>
            </div>
        </div>
    </div>

    <!-- 服务详情模态框 -->
    <div id="service-details-modal" uk-modal>
        <div class="uk-modal-dialog uk-modal-body rounded-xl w-[80%]">
            <button class="uk-modal-close-default" type="button" uk-close></button>
            <h2 class="text-2xl font-bold mb-4">服务状态详情</h2>
            
            <div id="serviceStatusLoading" class="flex justify-center items-center py-8">
                <div uk-spinner></div>
                <span class="ml-3">加载服务状态...</span>
            </div>
            
            <div id="serviceStatusContent" class="space-y-4 hidden">
                <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
                    <div>
                        <h3 class="text-sm font-medium text-gray-500">服务ID</h3>
                        <p id="serviceModalId" class="mt-1 text-gray-900">-</p>
                    </div>
                    
                    <div>
                        <h3 class="text-sm font-medium text-gray-500">当前状态</h3>
                        <div id="serviceModalStatus" class="mt-1">-</div>
                    </div>
                    
                    <div>
                        <h3 class="text-sm font-medium text-gray-500">服务地址</h3>
                        <p id="serviceModalAddress" class="mt-1 text-gray-900 break-all">-</p>
                    </div>
                </div>
                
                <div>
                    <h3 class="text-sm font-medium text-gray-500">详细信息</h3>
                    <div class="mt-2 bg-gray-50 p-4 rounded-lg">
                        <pre id="serviceModalDetails" class="whitespace-pre-wrap overflow-x-auto text-xs">无可用信息</pre>
                    </div>
                </div>
            </div>
            
            <div id="serviceStatusError" class="bg-red-50 text-red-600 p-4 rounded-lg mb-4 hidden">
                无法获取服务状态信息
            </div>
            
            <div class="flex justify-end space-x-4 mt-6">
                <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">
                    <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M6 6h12v12H6z" fill="currentColor"/>
                    </svg>
                    停止服务
                </button>
                <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">
                    <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <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"/>
                    </svg>
                    刷新
                </button>
                <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">
                    关闭
                </button>
            </div>
        </div>
    </div>

    <!-- 引入控制器脚本 -->
    <script src="js/serviceCtl.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 初始化服务控制器
            const controller = new ServiceController();
            controller.init();
        });
    </script>
</body>
</html>

```

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

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MCP应用编辑器</title>
    <!-- TailwindCSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: {
                            50: '#e3f2fd',
                            100: '#bbdefb',
                            200: '#90caf9',
                            300: '#64b5f6',
                            400: '#42a5f5',
                            500: '#2196f3',
                            600: '#1e88e5',
                            700: '#1976d2',
                            800: '#1565c0',
                            900: '#0d47a1'
                        },
                        secondary: '#64748b'
                    }
                }
            }
        }
    </script>
    <!-- UIkit CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css" />
    <!-- UIkit JS -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>
    <style>
        .uk-card {
            border-radius: 1rem;
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
        }
        .uk-button {
            border-radius: 0.5rem;
            transition: all 0.2s ease;
        }
        .uk-button-primary {
            background: linear-gradient(145deg, #1e88e5, #1976d2);
            box-shadow: 0 4px 6px -1px rgba(30, 136, 229, 0.2);
        }
        .uk-button-primary:hover {
            transform: translateY(-1px);
            box-shadow: 0 10px 15px -3px rgba(30, 136, 229, 0.3);
        }
        .uk-input, .uk-select, .uk-textarea {
            border-radius: 0.5rem;
            transition: all 0.2s ease;
        }
        .uk-input:focus, .uk-select:focus, .uk-textarea:focus {
            box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.2);
        }
        .uk-form-label {
            font-weight: 500;
            margin-bottom: 0.5rem;
            color: #334155;
        }
    </style>
</head>
<body class="bg-slate-100 min-h-screen">
    <!-- 导航栏 -->
    <nav class="bg-gradient-to-r from-primary-700 to-primary-500 shadow-lg">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16">
                <div class="flex items-center text-white">
        
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                        <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" />
                    </svg>                      
                    <span class="ml-2 text-xl font-bold">MCP应用编辑器</span>
                </div>
                <div class="flex items-center gap-6">
                    <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">
                        <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">
                            <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" />
                        </svg>
                        导出JSON
                    </button>

                </div>
            </div>
        </div>
    </nav>

    <!-- 主内容区域 -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        <div class="grid grid-cols-12 gap-6">
            <!-- 侧边栏 -->
            <div class="col-span-12 md:col-span-3 space-y-6">
                <!-- 基本信息卡片 -->
                <div class="bg-white rounded-xl shadow-sm p-6">
                    <h2 class="text-lg font-bold text-gray-800 mb-4">应用信息</h2>
                    <div class="space-y-4">
                        <div>
                            <label class="block text-sm font-medium text-gray-700 mb-1">应用名称</label>
                            <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="输入应用名称">
                        </div>
                        <div>
                            <label class="block text-sm font-medium text-gray-700 mb-1">应用描述</label>
                            <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>
                        </div>
                    </div>
                </div>

                <!-- 导入/导出卡片 -->
                <div class="bg-white rounded-xl shadow-sm p-6">
                    <h2 class="text-lg font-bold text-gray-800 mb-4">配置管理</h2>
                    <div class="space-y-3 mb-3">
                        <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">
                            <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">
                                <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" />
                            </svg>
                            保存配置
                        </button>
                    </div>
                    <div class="space-y-3">
                        <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">
                            <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <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"/>
                            </svg>
                            导入JSON
                        </button>
                        <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">
                            <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <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"/>
                            </svg>
                            使用模板
                        </button>
                        <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">
                            <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <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"/>
                            </svg>
                            删除应用
                        </button>
                    </div>
                </div>
            </div>

            <!-- 主要内容区域 -->
            <div class="col-span-12 md:col-span-9 space-y-6">
                <!-- 服务器列表 -->
                <div class="bg-white rounded-xl shadow-sm p-6">
                    <div class="flex justify-between items-center mb-6">
                        <h2 class="text-lg font-bold text-gray-800">服务器配置</h2>
                        <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">
                            <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                            </svg>
                            MCP
                        </button>
                    </div>

                    <div class="overflow-x-auto">
                        <table class="w-full text-left">
                            <thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
                                <tr>
                                    <th class="px-4 py-3 rounded-tl-lg">名称</th>
                                    <th class="px-4 py-3">类型</th>
                                    <th class="px-4 py-3">命令</th>
                                    <th class="px-4 py-3 rounded-tr-lg text-right">操作</th>
                                </tr>
                            </thead>
                            <tbody id="serverTableBody" class="divide-y divide-gray-100">
                                <!-- 服务器列表项会动态添加到这里 -->
                            </tbody>
                        </table>
                    </div>

                    <div id="emptyServerMessage" class="text-center py-8 text-gray-500 hidden">
                        还没有添加任何服务器配置
                    </div>
                </div>

                <!-- JSON预览区域 -->
                <div class="bg-white rounded-xl shadow-sm p-6">
                    <h2 class="text-lg font-bold text-gray-800 mb-4">JSON预览</h2>
                    <div class="relative">
                        <pre id="jsonPreview" class="bg-gray-50 p-4 rounded-lg text-sm font-mono overflow-x-auto"></pre>
                        <button id="copyJsonBtn" class="absolute top-2 right-2 p-2 text-gray-500 hover:text-gray-700 bg-white rounded-lg shadow-sm">
                            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <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"/>
                            </svg>
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- 服务器配置模态框 -->
    <div id="server-modal" uk-modal>
        <div class="uk-modal-dialog uk-modal-body rounded-xl">
            <button class="uk-modal-close-default" type="button" uk-close></button>
            <h2 class="text-2xl font-bold mb-6" id="serverModalTitle">添加服务器</h2>
            
            <form id="serverForm" class="space-y-6">
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-1">服务器名称</label>
                        <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="输入服务器名称">
                    </div>
                    
                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-1">服务器类型</label>
                        <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">
                            <option value="sse">SSE (远程服务)</option>
                            <option value="stdio">STDIO (本地进程)</option>
                        </select>
                    </div>
                </div>

                <div>
                    <label class="block text-sm font-medium text-gray-700 mb-1">服务器描述</label>
                    <textarea  id="serverDescription" class="border w-full min-h-[48px] p-2 rounded-lg focus:border-sky-200"></textarea>
                </div>

                <!-- SSE配置 -->
                <div id="sseConfig" class="space-y-4">
                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-1">Base URL</label>
                        <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">
                    </div>

                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-3">Headers</label>
                        <div id="headersContainer" class="space-y-2">
                            <!-- Headers会动态添加到这里 -->
                        </div>
                        <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">
                            <svg class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none">
                                <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                            </svg>
                            添加Header
                        </button>
                    </div>
                </div>

                <!-- STDIO配置 -->
                <div id="stdioConfig" class="space-y-4 hidden">
                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-1">Command</label>
                        <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="执行的命令">
                    </div>

                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-3">参数列表</label>
                        <div id="argsContainer" class="space-y-2">
                            <!-- 参数会动态添加到这里 -->
                        </div>
                        <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">
                            <svg class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none">
                                <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                            </svg>
                            添加参数
                        </button>
                    </div>

                    <div>
                        <label class="block text-sm font-medium text-gray-700 mb-3">环境变量</label>
                        <div id="envContainer" class="space-y-2">
                            <!-- 环境变量会动态添加到这里 -->
                        </div>
                        <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">
                            <svg class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none">
                                <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                            </svg>
                            添加环境变量
                        </button>
                    </div>
                </div>

                <div class="flex justify-end space-x-4 pt-4">
                    <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">
                        <svg class="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="none">
                            <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                        </svg>
                        保存配置
                    </button>
                    <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">
                        取消
                    </button>
                </div>
            </form>
        </div>
    </div>

    <!-- 模板选择模态框 -->
    <div id="template-modal" uk-modal>
        <div class="uk-modal-dialog uk-modal-body rounded-xl w-2/5">
            <button class="uk-modal-close-default" type="button" uk-close></button>
            <h2 class="text-2xl font-bold mb-4 ">选择模板</h2>
            
            <div id="templateLoading" class="flex justify-center items-center py-8">
                <div uk-spinner></div>
                <span class="ml-3">加载模板列表...</span>
            </div>
            
            <div id="templateTableContainer" class="hidden">
                <div class="flex justify-between items-center mb-4">
                    <div class="flex items-center">
                        <input type="checkbox" id="selectAllTools" class="mr-2 h-4 w-4 rounded text-primary-600">
                        <label for="selectAllTools" class="text-sm font-medium text-gray-700">全选</label>
                    </div>
                    <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">
                        <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none">
                            <path d="M12 6v12M18 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                        </svg>
                        添加选中的模板
                    </button>
                </div>
                
                <div class="overflow-x-auto">
                    <table class="w-full text-left">
                        <thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
                            <tr>
                                <th class="px-4 py-3 rounded-tl-lg w-10"></th>
                                <th class="px-4 py-3">名称</th>
                                <th class="px-4 py-3">类型</th>
                                <th class="px-4 py-3 rounded-tr-lg">描述</th>
                            </tr>
                        </thead>
                        <tbody id="templateTableBody" class="divide-y divide-gray-100">
                            <!-- 模板列表项会动态添加到这里 -->
                        </tbody>
                    </table>
                </div>
            </div>
            
            <div id="noTemplatesMessage" class="text-center py-8 text-gray-500 hidden">
    <div id="json-modal" uk-modal>
        <div class="uk-modal-dialog uk-modal-body rounded-xl">
            <button class="uk-modal-close-default" type="button" uk-close></button>
            <h2 class="text-2xl font-bold mb-4">导入JSON配置</h2>
            
            <div class="mb-4">
                <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>
            </div>
            
            <div class="flex justify-end">
                <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">
                    <svg class="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="none">
                        <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    导入配置
                </button>
            </div>
        </div>
    </div>

    <script src="js/MCPEditor.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const editor = new MCPEditor();
            editor.init();
        });
    </script>
</body>
</html>

```

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

```html
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth overflow-hidden">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <title>Playground/toai.chat</title>
  
  <!-- 引入UIKit和TailwindCSS -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css" />
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>

  <!-- 引入marked和highlight.js -->
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.css" id="markdown-light">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/github-markdown-dark.css" id="markdown-dark" disabled>

  <script src="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/a11y-light.min.css" id="code-light">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/a11y-dark.min.css" id="code-dark" disabled>

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.css" id="mermaid-light">
  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>

  <script src="https://cdn.tailwindcss.com"></script>
  
  <script src="ai/ai.js"></script>
  <script src="mcp/mcpcli.js"></script>
  
  <script>
    tailwind.config = {
      darkMode: 'class',
      theme: {
        extend: {
          colors: {
            'ai-green': '#10a37f',
            'ai-green-dark': '#0e8a6e',
            'chat-bg': '#f7f7f8',
            'chat-dark': '#444654',
            'text-light': '#222222',
            'text-dark': '#ececf1',
            'user-bg': '#eeeeee'
          },
          boxShadow: {
            'send-btn': '0 2px 5px rgba(0,0,0,0.1)'
          }
        }
      }
    }
  </script>
  <style>
    .markdown-body { background-color: transparent; h-* { color: #eee; } }
    ::-webkit-scrollbar { width: 0px; }
  </style>
</head>
<body class="dark:bg-gray-900 dark:text-gray-100">
  <!-- 添加UIKit Offcanvas组件作为左侧抽屉 -->
  <div id="offcanvas-nav" uk-offcanvas="overlay: true; mode: push">
    <div class="uk-offcanvas-bar bg-gray-50 text-gray-600 dark:bg-gray-600 dark:text-gray-100">
      <button class="uk-offcanvas-close" type="button" uk-close></button>
      <div class="font-bold text-lg text-gray-800 dark:text-gray-100 mb-4">Topic History</div>
      
      <!-- 改为黑白配色的新对话按钮 -->
      <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">
        <span uk-icon="icon: plus" class="text-white"></span>
        <span class="ml-2 text-sm font-medium">New Topic</span>
      </div>
      
      <div class="conversation-list mt-4" id="conversation-list"></div>
    </div>
  </div>


  <div id="app" class="flex flex-col h-screen">
    <!-- 顶部导航栏 - 固定定位 -->
    <header class="fixed top-0 left-0 w-full bg-white dark:bg-gray-900 z-40">
      <div class="uk-container uk-container-expand">
        <nav class="uk-navbar" uk-navbar>
          <div class="uk-navbar-left">
            <!-- 添加菜单按钮 -->
            <a class="uk-navbar-toggle" href="#" uk-toggle="target: #offcanvas-nav">
              <span uk-icon="icon: menu"></span>
            </a>
            <!-- 将固定标题改为可编辑区域 -->
            <div class="uk-navbar-item dark:text-gray-100">
              <span id="conversation-title" contenteditable="true"></span>
            </div>
          </div>
          <div class="uk-navbar-right">
            <ul class="uk-navbar-nav">
              <li>
                <a href="#" id="theme-toggle" class="theme-icon flex items-center justify-center dark:text-gray-100">
                  <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">
                    <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" />
                  </svg>
                </a>
              </li>
              <li>
                <a href="#" uk-icon="icon: cog"></a>
                <div class="uk-navbar-dropdown" uk-dropdown="mode: click; animation: uk-animation-slide-top-small; duration: 100">
                  <ul class="uk-nav uk-navbar-dropdown-nav line-height-2">
                    <li style="margin:16px;"><a id="clear-chat" uk-icon="icon: refresh">清空记录</a></li>
                    <li style="margin:16px;"><a id="remove-chat" uk-icon="icon: trash">删除对话</a></li>
                    <li style="margin:16px;"><a id="api-settings" uk-icon="icon: cog">对话设置</a></li>
                  </ul>
                </div>
              </li>
            </ul>
          </div>
        </nav>
      </div>
    </header>
    
    <!-- 聊天区域 - 调整顶部边距以避免被固定header覆盖 -->
    <main class="flex-grow pt-[80px] overflow-y-auto">
      <div class="uk-container uk-container-expand h-full">
        <div id="chat-container" class="chat-container flex flex-col mb-2.5 p-5 pb-[240px]"></div>
      </div>
    </main>
    
    <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">

        <div class="message-toolbar uk-margin-small-bottom flex items-center gap-8 px-4">

            <div class="uk-inline">
                <div class="js-upload cursor-hand" uk-form-custom>
                    <button class="toolbar-btn" type="button" uk-tooltip="上传文件">
                        <span uk-icon="icon: image"></span>
                        <input type="file" id="image-upload-input" accept="image/*" multiple>
                    </button>

                </div>
            </div>
            
            <div class="uk-inline">
              <button class="toolbar-btn" id="mcp-toggle" uk-tooltip="MCP">
                <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">
                  <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" />
                </svg>
                </span>      

                          
              </button>
            </div>
        
        </div>

        <div class="flex items-start gap-2.5 relative mx-auto my-0">
          <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>
          
          <!-- 重写发送按钮,添加两种状态 -->
          <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>
            <!-- 正常状态 -->
            <span id="send-icon" class="block">
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                <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" />
              </svg>
            </span>
            <!-- loading状态 -->
            <span id="loading-icon" class="hidden">
              <svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                <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>
              </svg>
            </span>
          </button>
        </div>

        <div id="image-preview-container" class="flex flex-wrap gap-2.5 mt-2.5">
            <ul class="uk-thumbnav" uk-margin id="image-preview-container"></ul>
        </div>
    </div>

  </div>
  
  <div id="settings-modal" uk-modal>
    <div class="uk-modal-dialog uk-modal-body">
      <h2 class="uk-modal-title">对话设置</h2>
      <form class="uk-form-stacked">
        <div class="uk-margin">
          <label class="uk-form-label">API Key</label>
          <div class="uk-form-controls"><input class="uk-input" id="apiKey" type="password" placeholder="输入你的API Key"></div>
        </div>
        <div class="uk-margin">
            <label class="uk-form-label">MCP(SSE)</label>
            <div class="uk-form-controls"><input class="uk-input" id="mcp" type="text" placeholder="请输入MCP服务器地址"></div>
          </div>        
        <div class="uk-margin">
          <label class="uk-form-label">API baseURL</label>
          <div class="uk-form-controls"><input class="uk-input" id="baseURL" type="text" placeholder="https://porky.toai.chat/pollination/v1"></div>
        </div>
        <div class="uk-margin">
          <label class="uk-form-label">模型</label>
          <div class="uk-form-controls">
            <input class="uk-input" id="model" type="text" placeholder="模型名称" value="openai">
          </div>
          <a href="https://text.pollinations.ai/models" target="_blank">see the models if you are using pollinations</a>
        </div>
        <div class="uk-margin">
          <label class="uk-form-label">系统提示 (System Prompt)</label>
          <div class="uk-form-controls">
            <textarea class="uk-textarea" id="systemPrompt" rows="3" placeholder="输入系统提示,指导AI的行为和角色"></textarea>
          </div>
        </div>
        <div class="uk-margin">
          <label class="uk-form-label">温度 <span id="temperature-value">0.7</span></label>
          <div class="uk-form-controls"><input class="uk-range" id="temperature" type="range" min="0" max="2" step="0.01" value="0.7"></div>
        </div>
        <div class="uk-margin">
          <label class="uk-form-label">最大令牌数 (Max Tokens)</label>
          <div class="uk-form-controls">
            <input class="uk-input" id="maxTokens" type="number" placeholder="最大令牌数" value="128000">
          </div>
        </div>
        <div class="uk-margin">
          <label class="uk-form-label">Top P <span id="top-p-value">1.0</span></label>
          <div class="uk-form-controls"><input class="uk-range" id="topP" type="range" min="0" max="1" step="0.01" value="1.0"></div>
        </div>
      </form>
      <div class="uk-text-right">
        <button class="uk-button uk-button-default uk-modal-close rounded-md">取消</button>
        <button class="uk-button uk-button-primary rounded-md" id="save-settings">保存</button>
      </div>
    </div>
  </div>

  <script>

    const appState = {
      darkMode: localStorage.getItem('darkMode') === 'true',
      currentChat: null,

      togglekMode() {
        this.darkMode = !this.darkMode;
        localStorage.setItem('darkMode', this.darkMode);
        document.documentElement.classList.toggle('dark')
        document.getElementById('markdown-light').disabled = this.darkMode;
        document.getElementById('markdown-dark').disabled = !this.darkMode;
        document.getElementById('code-light').disabled = this.darkMode;
        document.getElementById('code-dark').disabled = !this.darkMode;
      }

    }

    document.getElementById('theme-toggle').addEventListener('click', appState.togglekMode)
    mermaid.initialize({startOnLoad:true, theme: appState.darkMode ? 'neutral' : 'neutral', securityLevel: 'loose'});


    class DataBase {
      constructor(dbname, structure={}) {
        this.dbName = dbname
        this.structure = structure;
        this.db = null;
        this.ready = this.initDatabase();
      }

      async initDatabase() {
        return new Promise((resolve, reject) => {
          const request = indexedDB.open(this.dbName, 1);
          
          request.onupgradeneeded = (event) => {
            const db = event.target.result;
            for (const storeName in this.structure) {
              if (!db.objectStoreNames.contains(storeName)) {
                const store = db.createObjectStore(storeName, { keyPath: this.structure[storeName].keyPath });
                for (const index of this.structure[storeName].indexes) {
                  store.createIndex(index.name, index.keyPath, { unique: index.unique||false });
                }
              }
            }
          };
    
          request.onsuccess = (event) => { this.db = event.target.result; resolve(true);};
          request.onerror = (event) => { reject(event.target.error);};
        });
      }

      async getCollectionData(collection, key, filter) {
        await this.ready;
        return new Promise((resolve, reject) => {
          const transaction = this.db.transaction([collection], 'readonly');
          const store = transaction.objectStore(collection);
          const index = store.index(key);
          const request = !filter ? index.getAll() : index.getAll(filter);
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
      }

      async setCollectionData(collection, data) {
        await this.ready;
        return new Promise((resolve, reject) => {
          const transaction = this.db.transaction([collection], 'readwrite');
          const store = transaction.objectStore(collection);
          const request = store.put(data);
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
      }

      async deleteCollectionData(collection, key) {
        await this.ready;
        return new Promise((resolve, reject) => {
          const transaction = this.db.transaction([collection], 'readwrite');
          const store = transaction.objectStore(collection);
          const request = store.delete(key);
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
      }
    }
      
    class MainDatabase extends DataBase {
        constructor() {
          super('MainDB', {
            conversations: {
              keyPath: 'id',
              indexes: [
                { name: 'title', keyPath: 'title' },
                { name: 'lastMessage', keyPath: 'lastMessage' },
                { name: 'id', keyPath: 'id' },
                { name: 'timestamp', keyPath: 'timestamp' }
              ]
            }
          });
        }
        async getAllConversations(id=null) {
          if (id) return await this.getCollectionData('conversations', 'id', id);
          return await this.getCollectionData('conversations', 'timestamp');
        }
        async getOrCreateLastChat() {
          const conversations = await this.getAllConversations();
          if (conversations.length > 0) {
            const conversation = conversations[conversations.length - 1];
            console.log('获取最后一个对话:', conversation);
            return conversation;
          }
          return this.createNewConversation(Date.now(), '');
        }
        async createNewConversation(id, title) {
          const conversation = { id: id, title: title,timestamp: Date.now(), lastMessage: ''};
          await this.setCollectionData('conversations', conversation);
          return conversation;
        }
      }

    class ChatDatabase extends DataBase {
      constructor(chatId) {
        if(!chatId) {
          throw new Error('Chat ID is required');
        }
        super(`ChatDB-${chatId}`, {
          messages: {
            keyPath: 'id',
            indexes: [
              { name: 'id', keyPath: 'id' },
              { name: 'content', keyPath: 'content' },
              { name: 'role', keyPath: 'role' },
              { name: 'timestamp', keyPath: 'timestamp' },
              { name: 'images', keyPath: 'images' }
            ]
          },
          settings: {
            keyPath: 'key',
            indexes: [
              { name: 'key', keyPath: 'key' },
              { name: 'value', keyPath: 'value' }
            ]
          }
        });
      }
      async getSettings() {return await this.getCollectionData('settings', 'key')}
      async saveMessage(message) { await this.setCollectionData('messages', message)}
      async getAllMessages() { return await this.getCollectionData('messages', 'timestamp') }
    }

    window.MainDatabase = new MainDatabase()


    class AdvancedConversation {
      constructor() {
        this.db = window.MainDatabase
        this.conversationList = document.getElementById('conversation-list');
        this.newChatButton = document.getElementById('new-chat-btn');
        this.deleteChatButton = document.getElementById('remove-chat');
        this.topicTitle = document.getElementById('conversation-title');
        this.init();
      }
      async init() {
        this.newChatButton.addEventListener('click', this.createNewConversation.bind(this));
        await this.loadConversations();
        this.deleteChatButton.addEventListener('click', this.deleteConversation.bind(this));
        this.topicTitle.addEventListener('blur', async (e) => {
          const title = e.target.innerText;
          if (title) { await this.setConversationTitle(window.aiui.chatId, title);}
        });
        this.topicTitle.addEventListener('keydown', (e) => {
          if (e.key === 'Enter') { e.preventDefault(); this.topicTitle.blur();}
        });
      }
      async loadConversations() {
        const conversations = await this.db.getAllConversations();
        this.conversationList.innerHTML = ''; // 清空列表
        for (const conversation of conversations) {
          const item = document.createElement('div');
          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';
          item.id = 'conversation-' + conversation.id;
          item.setAttribute('data-id', conversation.id);
          const title = document.createElement('span')
          title.className = 'conversation-title truncate whitespace-nowrap'; title.innerText = conversation.title || 'Untitled Topic';
          item.appendChild(title);
          item.addEventListener('click', (e) => {this.loadConversation(conversation.id);});
          this.conversationList.appendChild(item);
        }
        return conversations;
      }
      async createNewConversation() {
        const chat = await this.db.createNewConversation(Date.now(), '');
        await this.loadConversations();
        await this.loadConversation(chat.id);
      }
      async loadConversation(id) {
        localStorage.setItem('last_chat_id', id);
        const conversations = await this.db.getAllConversations(id);
        if (conversations.length > 0) {
          const conversation = conversations[0];
          this.conversationList.querySelectorAll('.conversation-item').forEach(item => item.classList.remove('active'));
          this.conversationList.querySelector(`#conversation-${conversation.id}`).classList.add('active');
          window.aiui = new AIUI( {chatId: conversation.id} );
          await window.aiui.ready;
          this.topicTitle.innerText = conversation.title;
          UIkit.offcanvas('#offcanvas-nav').hide();
        }
      }
      async deleteConversation(event) {
        const id = window.aiui.chatId;
        await UIkit.modal.confirm('确定要删除此对话吗?', {labels: {ok: '删除', cancel: '取消'}}).then(async () => {
          await this.db.deleteCollectionData('conversations', id);
          await indexedDB.deleteDatabase(`ChatDB-${id}`);
          await this.loadConversations();
          window.aiui = new AIUI();
        })
      }
      async setConversationTitle(id, title) {
        const conversation = await this.db.getCollectionData('conversations', 'id', id);
        if (conversation.length > 0) {
          conversation[0].title = title;
          await this.db.setCollectionData('conversations', conversation[0]);
          this.topicTitle.innerText = title;
        }
        await this.loadConversations();
      }
    }


    class AdvancedContainer {
      // 聊天消息容器类,主要控制chat-container的消息
      static instance = null;
      
      static getInstance(db) {
        if (!AdvancedContainer.instance) {
          AdvancedContainer.instance = new AdvancedContainer(db);
        } else if (db && AdvancedContainer.instance.db !== db) {
          // 当数据库引用变化时,更新数据库引用并重置容器
          AdvancedContainer.instance.db = db;
          AdvancedContainer.instance.resetContainer();
          AdvancedContainer.instance.loadMessages(); // 自动加载新数据库的消息
        }
        return AdvancedContainer.instance;
      }
      
      constructor(db) {
        // 如果已经有实例,直接返回
        if (AdvancedContainer.instance) {
          return AdvancedContainer.instance;
        }
        
        this.container = document.getElementById('chat-container');
        this.clearChatButton = document.getElementById('clear-chat');
        this.container.innerHTML = ''; // 初始化的时候,清空容器
        this.db = db;
        this.initialized = false;
        this.ready = this.init();
        
        AdvancedContainer.instance = this;
      }

      // 重置容器,清空内容
      resetContainer() {
        this.container.innerHTML = '';
      }

      async init() {
        // 确保初始化和事件绑定只执行一次
        if (this.initialized) return;
        
        this.clearChatButton.addEventListener('click', async() => {
          await UIkit.modal.confirm('确定要清空聊天记录吗?', {labels: {ok: '清空', cancel: '取消'}}).then(async () => {
            await this.clearChat();
            if (window.aiui && window.aiui.ai) {
              window.aiui.ai.messages = [];
            }
          });
        });
        
        this.initialized = true;
      }

      scrollToBottom() {
        // 修改滚动函数,滚动整个窗口到底部
        window.scrollTo(0, document.body.scrollHeight);
      }
      
      async newMessage(role) {
        const msgId = `msg-${Date.now()}`;
        const messageDiv = document.createElement('div');
        messageDiv.className = 'max-w-fit mb-4 p-3 rounded-lg relative break-words shadow-sm';
        
        // 使用Tailwind类替代自定义CSS类
        if (role === 'user') {
          messageDiv.classList.add('bg-user-bg', 'text-gray-900', 'ml-auto', 'text-right','uk-animation-slide-bottom-small','uk-animation-fade');
        } else if (role === 'assistant') {
          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');
        }
        
        messageDiv.id = msgId;

        const messageContent = document.createElement('div');
        messageContent.className = 'message-content';
        messageContent.setAttribute('raw', '');
        if (role === 'assistant') {
            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');
        }

        const messageReason = document.createElement('div');
        messageReason.className = 'message-reason opacity-30 markdown-body max-h-[32px] overflow-hidden hover:cursor-pointer hover:opacity-60 hover:font-weight-light';
        messageReason.setAttribute('raw', '');
        messageReason.addEventListener('click', (e)=>{ messageReason.classList.toggle("max-h-[48px]")} )
        messageDiv.appendChild(messageReason)

        messageDiv.appendChild(messageReason);
        messageDiv.appendChild(messageContent);

        const add = () => { this.container.appendChild(messageDiv); this.scrollToBottom();};
        const update = async(content, addons={}) => { 
          const { scroll=false, images=[], spin=false } = addons;
          messageContent.setAttribute('raw', content);
          if (spin) {
            content = `<div uk-spinner='ratio: 0.5'></div>`;
          } 
          if (role === 'user') {
            messageContent.innerHTML = content;
            if (images.length) {
                messageContent.setAttribute('images', JSON.stringify(images));
                const imgContainer = document.createElement('div'); imgContainer.className = 'flex';
                images.forEach((image, index) => {
                    const img = document.createElement('img');
                    img.src = image; img.style.height = '80px'; img.className = 'rounded-md mx-0.5'; 
                    imgContainer.appendChild(img);
                });
                messageContent.appendChild(imgContainer);
            }
          } else if (role==='assistant') {
            messageContent.innerHTML = marked.parse(content);
          }

          if (role === 'assistant') {setTimeout(() => mermaid.init(undefined, messageContent.querySelectorAll('.mermaid')), 0);}
          if (scroll) this.scrollToBottom();
        };
        const updateReason = async(reason) => {
          messageReason.classList.add('p-4')
          messageReason.innerHTML = marked.parse(reason); 
        }; 
        const save = async( ) => { 
            const content = messageContent.getAttribute('raw');
            const images = messageContent.getAttribute('images');
            if(!content && !images) return;
            await this.db.saveMessage({ id: msgId,  role: role, content: messageContent.getAttribute('raw'), timestamp: Date.now(), images: messageContent.getAttribute('images') });
        };
        window.rup = updateReason;
        return {msgId, messageDiv, add, update, save, updateReason };
      }

      async clearChat() {
        const messages = await this.db.getAllMessages();
        for (const message of messages) { await this.db.deleteCollectionData('messages', message.id); }
        this.container.innerHTML = ''; 
      }

      async loadMessages() {
        const messages = await this.db.getAllMessages();
        for (const message of messages) {
          const { msgId, messageDiv, add, update } = await this.newMessage(message.role);
          await update(message.content, {scroll:false, images:message.images ? JSON.parse(message.images) : []});
          add();
        }
      }
    }

    class AdvancedInput {
      // 聊天输入框类,主要控制message-input的输入, 以及发送按钮的状态, 还有点击发送按钮后的回调
      constructor() {
        this.input = document.getElementById('message-input');
        this.sendButton = document.getElementById('send-button');
        this.sendIcon = document.getElementById('send-icon');
        this.loadingIcon = document.getElementById('loading-icon');
        this.imageUploadInput = document.getElementById('image-upload-input');
        this.imagePreviewContainer = document.getElementById('image-preview-container');
        this.mcpToggle = document.getElementById('mcp-toggle');
        this.mcpStatus = 0;  // 0=>未链接, 2=>连接中, 1=>链接成功
        this.mcpcli = null;
        this.isProcessing = false;
        this.init()
      }

      init() {
        this.mcpToggle.addEventListener('click', this.mcpClient.bind(this));
        this.input.addEventListener('input', () => {this.sendButton.disabled = !this.input.value.trim();});
        this.input.addEventListener('keydown', (e) => {if (e.key === 'Enter' && e.ctrlKey) {e.preventDefault();this.sendButton.click();}});
        this.imageUploadInput.addEventListener('change', (e)=>{
            const files = e.target.files;
            if (files.length > 0) {
                for (const file of files) {
                  const reader = new FileReader();
                  reader.onload = (event) => {
                      const a = document.createElement('a'); a.className = 'uk-position-relative';
                      const img = document.createElement('img'); img.src = event.target.result; img.classList.add('rounded-md'); img.style.height='64px'
                      a.appendChild(img);
                      const deleteBtn = document.createElement('button');deleteBtn.className = 'uk-icon-button uk-position-top-right uk-position-small';
                      deleteBtn.setAttribute('uk-icon', 'trash');deleteBtn.style.backgroundColor = 'rgba(255,255,255,0.4)';
                      deleteBtn.onclick = (e) => {e.preventDefault(); a.remove();};
                      a.appendChild(deleteBtn);
                      this.imagePreviewContainer.appendChild(a);
                  };
                  reader.readAsDataURL(file);
                }
            }
        })
        this.sendButton.onclick = () => {
          if (this.isProcessing) return;
          if(!this.input.value.trim()) { return }
          
          // 显示loading状态
          this.isProcessing = true;
          this.sendButton.disabled = false;
          this.sendIcon.classList.add('hidden');
          this.loadingIcon.classList.remove('hidden');
          this.input.setAttribute('disabled', 'disabled');
          this.imageUploadInput.setAttribute('disabled', 'disabled');
          
          return new Promise((resolve, reject) => {
            const message = { content: this.input.value, images: [...document.getElementById('image-preview-container').querySelectorAll('img')].map(x=>x.src) };
            window.aiui.onSendMessage(message, resolve, reject);
          }).catch((error) => {
            console.error('Error:', error);
          }).finally(() => {
            this.isProcessing = false;
            this.input.value = '';
            this.imagePreviewContainer.innerHTML = '';
            this.imageUploadInput.value = '';
            this.input.style.height = 'auto';
            
            // 恢复正常状态
            this.sendIcon.classList.remove('hidden');
            this.loadingIcon.classList.add('hidden');
            this.input.removeAttribute('disabled');
            this.imageUploadInput.removeAttribute('disabled');
            this.input.focus();
            this.sendButton.disabled = !this.input.value.trim();
          });
        };
      }

      async mcpClient() {
          if(this.mcpStatus===1) {
            this.mcpcli.close()
            this.mcpcli = null
            window.aiui.ai.toolFn = null
            this.mcpStatus = false
            this.mcpToggle.classList.remove('uk-spinner')
            this.mcpToggle.classList.remove('text-cyan-400')
            this.mcpToggle.setAttribute('uk-tooltip', 'MCP已断开。请重新链接')
            return
          }else if(this.mcpStatus===2) {
            UIkit.notification({message: '正在连接MCP,请稍等',status: 'warning',pos: 'top-center',timeout: 1000});
            return
          }
          const mcpaddress = window.aiui.settings.settings.mcp;
          if(!mcpaddress) {
            UIkit.notification({message: '请先设置MCP服务器地址',status: 'warning',pos: 'top-center',timeout: 5000});
            window.aiui.settings.open()
            return 
          }
          const mcpcli = new MCPCli(mcpaddress)
          this.mcpToggle.classList.add('uk-spinner')
          this.mcpToggle.setAttribute('uk-tooltip', '连接中...')
          this.mcpStatus = 2
          mcpcli.connect()
          .then(cli=>{
            this.mcpcli = cli
            this.mcpStatus = 1
            this.mcpToggle.setAttribute('uk-tooltip', 'MCP已连接')
            //this.mcpToggle.classList.remove('uk-spinner')
            this.mcpToggle.classList.add('text-cyan-400')
            UIkit.notification({message: 'MCP连接成功',status: 'success',pos: 'top-center',timeout: 5000});
            window.aiui.ai.toolFn = cli
          })
          .catch(err=>{
            console.error(err)
            UIkit.notification({message: 'MCP连接失败',status: 'danger',pos: 'top-center',timeout: 5000});
            this.mcpToggle.classList.remove('uk-spinner')
            this.mcpToggle.removeAttribute('uk-tooltip')
            this.mcpStatus = 0
          })
          

      }
    }

    class AdvancedSettings {
      static instance = null;
      
      static getInstance(db) {
        if (!AdvancedSettings.instance) {
          AdvancedSettings.instance = new AdvancedSettings(db);
        } else if (db) {
          // 如果已有实例但传入了新的db,则更新db
          AdvancedSettings.instance.db = db;
        }
        return AdvancedSettings.instance;
      }

      constructor(db) {
        // 如果已经有实例,则直接返回该实例
        if (AdvancedSettings.instance) {
          return AdvancedSettings.instance;
        }
        
        this.db = db;
        this.settingsButton = document.getElementById('api-settings');
        this.saveButton = document.getElementById('save-settings');
        this.settingsModal = document.getElementById('settings-modal');
        this.settings = {};
        this.initialized = false;
        this.init();
        
        AdvancedSettings.instance = this;
      }

      async init() {
        if (this.initialized) return;
        
        this.settingsButton.addEventListener('click', this.open.bind(this));
        this.saveButton.addEventListener('click', this.save.bind(this));
        document.getElementById('temperature').addEventListener('input', (e) => {
          document.getElementById('temperature-value').textContent = e.target.value;
        });
        document.getElementById('topP').addEventListener('input', (e) => {
          document.getElementById('top-p-value').textContent = e.target.value;
        });
        
        this.initialized = true;
        await this.get();
      }
      
      async open() {
        const settings = await this.get();
        document.getElementById('apiKey').value = settings.apiKey || '';
        document.getElementById('mcp').value = settings.mcp || '';
        document.getElementById('baseURL').value = settings.baseURL || 'https://text.pollinations.ai/openai';
        document.getElementById('model').value = settings.model || 'openai';
        document.getElementById('systemPrompt').value = settings.systemPrompt || '';
        document.getElementById('temperature').value = settings.temperature || 0.7;
        document.getElementById('temperature-value').textContent = settings.temperature || 0.7;
        document.getElementById('maxTokens').value = settings.maxTokens || 128000;
        document.getElementById('topP').value = settings.topP || 1.0;
        document.getElementById('top-p-value').textContent = settings.topP || 1.0;
        UIkit.modal(this.settingsModal).show();
      }
      
      close() { UIkit.modal(this.settingsModal).hide();}

      async save() {
        for (const setting of ['apiKey', 'mcp', 'baseURL', 'model', 'systemPrompt', 'temperature', 'maxTokens', 'topP']) {
          await this.db.setCollectionData('settings', { key: setting, value: document.getElementById(setting).value });
        }
        UIkit.notification({message: '设置已保存',status: 'success',pos: 'top-center',timeout: 2000});
        this.close();
        await this.get()
        window.dispatchEvent(new CustomEvent('settingsUpdate', {detail: this.settings}));
        return this.settings;
      }

      async get() {
        const default_settings = {
            'apiKey': '', 'baseURL': 'https://text.pollinations.ai/openai','mcp':'',
            'model': 'openai', 'systemPrompt': '',
            'temperature': 0.61, 'maxTokens': 8192, 'topP': 0.95
        }
        const saved_settings = (await this.db.getSettings()).reduce((acc, setting) => {acc[setting.key] = setting.value;return acc;}, {});
        this.settings = {...default_settings, ...saved_settings};
        return this.settings;
      }
    }

    // AIUI类
    class AIUI {
      constructor(options = {}) {        
        this.chatId = options.chatId
        this.ready = this.init()
      }

      async init() {
        if (!this.chatId) {
          if (localStorage.getItem('last_chat_id')) {
            await window.conversations.loadConversation(parseInt(localStorage.getItem('last_chat_id')));
            return
          } else {
            const chat = window.MainDatabase.getOrCreateLastChat();
            await window.conversations.loadConversation(chat.id);
            return
          }
        }

        this.db = new ChatDatabase(this.chatId);
        // 使用单例模式获取容器实例
        this.container = AdvancedContainer.getInstance(this.db);
        this.input = new AdvancedInput();
        // 使用单例模式获取设置实例
        this.settings = AdvancedSettings.getInstance(this.db);
        await this.settings.get();

        const messages = (await this.db.getAllMessages()).map(m => ({role:m.role, content:m.content}));
        this.ai = new AI({
          messages: messages,
          system_prompt: this.settings.settings.systemPrompt || '',
          model: this.settings.settings.model,
          apiKey: this.settings.settings.apiKey || '',
          baseURL: this.settings.settings.baseURL,
          completionsURI: '',
          opts: {
            temperature: parseFloat(this.settings.settings.temperature) || 0.7,
            max_tokens: parseInt(this.settings.settings.maxTokens) || 128000,
            top_p: parseFloat(this.settings.settings.topP) || 1.0
          }
        });

        window.addEventListener('settingsUpdate', async (e) => {
          this.ai = new AI({
            messages: this.ai.messages,
            system_prompt: e.detail.systemPrompt || '',
            model: e.detail.model,
            apiKey: e.detail.apiKey || '',
            baseURL: e.detail.baseURL,
            completionsURI: '',
            opts: {
              temperature: parseFloat(e.detail.temperature) || 0.7,
              max_tokens: parseInt(e.detail.maxTokens) || 128000,
              top_p: parseFloat(e.detail.topP) || 1.0
            }
          });
        });

        await this.container.loadMessages();
      }
      
      async onSendMessage(message, resolve, reject) {
        const { content, images } = message;
        if (!content.trim()) {reject('消息不能为空');return}
        if (window.conversations.topicTitle.innerText === '') {
          await window.conversations.setConversationTitle(this.chatId, content.slice(0, 30));
        }
        const userMessage = await this.container.newMessage('user');
        await userMessage.add();
        await userMessage.update(content.trim(), true, images);
        await userMessage.save();

        const assistantMessage = await this.container.newMessage('assistant');
        await assistantMessage.add();
        await assistantMessage.update('',{spin:true});
      
        try {

          // 调用AI生成回复
          this.currentResponse = await this.ai.create(content.trim(), {images: images});
          let fullResponse = '';
          let fullReasoning = ''

          // 同时处理推理和内容流
          await Promise.all([

            // 实现同时处理内容流和推理流,上次有个人讽刺说openAI在推理流时不出内容流,而实际上确实没人这么做。这个实现无非是预留效果罢了。。。。。

            // 处理推理流
            (async() => {
              console.log('reasoning.....');
              for await (const reasoning_chunk of this.currentResponse.on('reasoning')) {
                fullReasoning += reasoning_chunk.delta?.choices?.[0]?.delta?.reasoning || '';
                await assistantMessage.updateReason(fullReasoning);
              }
              //为了不增加token,推理流不写入数据库
            })(),
            
            // 处理内容流
            (async ()=> {
              for await (const chunk of this.currentResponse.on('content')) {
                if (chunk.delta) {
                  fullResponse += chunk.delta;
                  await assistantMessage.update(fullResponse, true);
                }
              }
              await assistantMessage.save();
            })()

          ])
          
          // 添加AI回复到消息历史
          this.ai.messages.push({
            role: 'assistant',
            content: fullResponse
          });
          
          resolve(fullResponse);
        } catch (error) {
          await assistantMessage.update('抱歉,哪里出现了错误了。 请稍后再试。');
          reject(error);
        } 
      }
    }

    document.addEventListener('DOMContentLoaded', async function() {
      
        const { markedHighlight } = window.markedHighlight;
        marked.use(markedHighlight({
            langPrefix: 'hljs language-',
            highlight: function(code, lang) {
                if (lang === 'mermaid') { return '<div class="mermaid">' + code + '</div>';}
                const language = hljs.getLanguage(lang) ? lang : 'plaintext'
                return hljs.highlight(code, {language: language}).value;
            }
        }));

        window.conversations = new AdvancedConversation();
        await window.conversations.loadConversations();
        window.aiui = new AIUI();
        await window.aiui.ready

        window.scrollTo(0, 1);
        if (/()iPhone|iPad|iPod/.test(navigator.userAgent)) {
            document.body.style.height = '100vh';
            document.body.overflow = 'hidden';
            document.body.requestFullscreen()
        }

    });
  </script>
</body>
</html>

```