1
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
.DS_Store
|
||||
.langgraph_api/
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/tasks.json" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="langgraph_env" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/simple-agent-template.iml" filepath="$PROJECT_DIR$/.idea/simple-agent-template.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PyProjectModelSettings">
|
||||
<option name="showConfigurationNotification" value="false" />
|
||||
<option name="usePyprojectToml" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="langgraph_env" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+1324
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
.PHONY: help install dev run test integration-tests lint format
|
||||
|
||||
help:
|
||||
@echo 'Targets:'
|
||||
@echo ' install Sync runtime dependencies with uv'
|
||||
@echo ' dev Sync project + dev dependencies with uv'
|
||||
@echo ' run Start the local LangGraph dev server'
|
||||
@echo ' test Run unit tests'
|
||||
@echo ' integration-tests Run integration tests'
|
||||
@echo ' lint Run Ruff checks'
|
||||
@echo ' format Format with Ruff'
|
||||
|
||||
install:
|
||||
uv sync --no-dev
|
||||
|
||||
dev:
|
||||
uv sync
|
||||
|
||||
run:
|
||||
uv run langgraph dev
|
||||
|
||||
test:
|
||||
uv run python -m pytest tests/unit_tests -q
|
||||
|
||||
integration-tests:
|
||||
uv run python -m pytest tests/integration_tests -q
|
||||
|
||||
lint:
|
||||
uv run python -m ruff check src tests
|
||||
|
||||
format:
|
||||
uv run python -m ruff format src tests
|
||||
@@ -0,0 +1,64 @@
|
||||
# Agent 智能助手
|
||||
|
||||
基于 LangGraph 的多智能体系统,支持文件操作、代码执行、知识管理、定时任务、MCP 外部服务、技能插件。
|
||||
|
||||
## 功能
|
||||
|
||||
- 对话交互(Web 前端,Markdown 渲染,代码高亮)
|
||||
- 文件读写(txt/log/json/csv/docx/xlsx/pptx/pdf/zip/mindmap)
|
||||
- HTTP 请求、代码沙箱(安全执行 Python)
|
||||
- 定时任务调度(cron)
|
||||
- 知识库 + 上下文记忆(SQLite)
|
||||
- 技能插件系统(SKILL.md 热加载)
|
||||
- MCP 外部服务(高德地图等)
|
||||
- 安全写入二次确认 + 审计日志
|
||||
- 登录认证(PBKDF2 加盐哈希)
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
双击 start.bat
|
||||
|
||||
# Linux / Kylin / Ubuntu
|
||||
bash start.sh
|
||||
```
|
||||
|
||||
访问 `http://127.0.0.1:8765/static/chat.html`,账号 `admin` / `Ascii2013!`
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
├── start.sh / start.bat # 启动脚本
|
||||
├── start_all.py # 统一入口(HTTP + LangGraph + 调度器)
|
||||
├── prepare_deps.sh # 离线依赖准备(有网机器跑一次)
|
||||
├── deps/ # Python 离线包
|
||||
├── fonts/ # 中文字体(PDF用)
|
||||
├── static/lib/ # 前端 JS(已内嵌,无需外网)
|
||||
├── src/simple_agent/
|
||||
│ ├── graph.py # 主 Agent 图
|
||||
│ ├── agents/ # 子智能体(Writer/MCP)
|
||||
│ ├── tools/ # 34 个工具
|
||||
│ ├── skills/ # 技能系统
|
||||
│ ├── prompts/ # 系统提示词
|
||||
│ └── utils/ # 审计/日志/路径安全
|
||||
├── deploy_tool/ # 独立部署监控程序
|
||||
└── deploy_config.json # 部署配置
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `LLM_MODEL` | MiniMax-M2.7 | 模型名称 |
|
||||
| `LLM_API_KEY` | - | API Key |
|
||||
| `LLM_BASE_URL` | api.minimax.chat/v1 | API 地址 |
|
||||
| `AGENT_HOST` | 127.0.0.1 | 绑定地址(远程访问设 0.0.0.0) |
|
||||
| `WRITER_WORKSPACE` | /opt/app/AgentWorkspace | 安全写入目录 |
|
||||
|
||||
## 安全
|
||||
|
||||
- 文件写入:仅 `/opt/app/AgentWorkspace/` 和 `knowledge/` 免确认,其他路径弹窗审批
|
||||
- 审计日志:所有写入操作记录到 SQLite(`AgentWorklogs/audit/`)
|
||||
- 认证:PBKDF2 加盐哈希,24h 会话超时,5 次失败锁定
|
||||
- 代码沙箱:AST 安全检查 + 模块白名单
|
||||
@@ -0,0 +1,44 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
echo ========================================
|
||||
echo Agent Windows 打包
|
||||
echo ========================================
|
||||
|
||||
:: 确保虚拟环境
|
||||
if not exist ".venv\Scripts\python.exe" (
|
||||
echo [ERROR] 未找到 .venv,请先创建虚拟环境
|
||||
pause & exit /b 1
|
||||
)
|
||||
|
||||
.venv\Scripts\python.exe -m pip install pyinstaller -q
|
||||
|
||||
.venv\Scripts\python.exe -m PyInstaller ^
|
||||
--onedir ^
|
||||
--name agent ^
|
||||
--add-data "src/simple_agent/prompts;simple_agent/prompts" ^
|
||||
--add-data "prompts;prompts" ^
|
||||
--add-data "static;static" ^
|
||||
--add-data "skills;skills" ^
|
||||
--add-data "workspace;workspace" ^
|
||||
--add-data "langgraph.json;." ^
|
||||
--add-data "tasks.json;." ^
|
||||
--add-data "fonts;fonts" ^
|
||||
--hidden-import langchain_openai ^
|
||||
--hidden-import pypdf ^
|
||||
--hidden-import fpdf ^
|
||||
--hidden-import openpyxl ^
|
||||
--hidden-import docx ^
|
||||
--hidden-import pptx ^
|
||||
--hidden-import psutil ^
|
||||
--collect-all langgraph ^
|
||||
--collect-all langchain ^
|
||||
--collect-all langchain_core ^
|
||||
--collect-all langchain_openai ^
|
||||
--noconfirm ^
|
||||
start_all.py
|
||||
|
||||
echo ========================================
|
||||
echo 打包完成: dist\agent\
|
||||
echo ========================================
|
||||
pause
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# ================================================================
|
||||
# Agent 打包脚本 (在有网的 Linux 上运行一次)
|
||||
# 生成 dist/agent/ 独立目录,复制到服务器直接运行
|
||||
# ================================================================
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "========================================"
|
||||
echo " Agent 打包中..."
|
||||
echo "========================================"
|
||||
|
||||
# 确保虚拟环境
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
source .venv/bin/activate
|
||||
|
||||
# 安装依赖 + pyinstaller
|
||||
pip install -r requirements-linux.txt -q
|
||||
pip install pyinstaller -q
|
||||
|
||||
# 打包
|
||||
pyinstaller \
|
||||
--onedir \
|
||||
--name agent \
|
||||
--add-data "src/simple_agent/prompts:simple_agent/prompts" \
|
||||
--add-data "prompts:prompts" \
|
||||
--add-data "static:static" \
|
||||
--add-data "skills:skills" \
|
||||
--add-data "workspace:workspace" \
|
||||
--add-data "langgraph.json:." \
|
||||
--add-data "tasks.json:." \
|
||||
--hidden-import langchain_openai \
|
||||
--hidden-import langchain.agents \
|
||||
--hidden-import pypdf \
|
||||
--hidden-import fpdf \
|
||||
--hidden-import openpyxl \
|
||||
--hidden-import docx \
|
||||
--hidden-import pptx \
|
||||
--hidden-import psutil \
|
||||
--collect-all langgraph \
|
||||
--collect-all langchain \
|
||||
--collect-all langchain_core \
|
||||
--collect-all langchain_openai \
|
||||
--noconfirm \
|
||||
start_all.py
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 打包完成: dist/agent/"
|
||||
echo " 复制到服务器: scp -r dist/agent/ user@host:/opt/"
|
||||
echo " 启动: /opt/agent/agent"
|
||||
echo "========================================"
|
||||
@@ -0,0 +1,381 @@
|
||||
# PAM Agent API 接口文档
|
||||
|
||||
## 服务概览
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| Chat Server | `8765` | 聊天界面、文件服务、技能管理、MCP 查询 |
|
||||
| LangGraph API | `2024` | Agent 对话图执行(LangGraph 标准 API) |
|
||||
|
||||
---
|
||||
|
||||
## 鉴权
|
||||
|
||||
Chat Server(8765)部分接口需 Bearer Token。
|
||||
|
||||
### POST /api/login — 登录
|
||||
|
||||
取得 Token,有效期 24 小时。
|
||||
|
||||
**请求**
|
||||
```
|
||||
POST http://127.0.0.1:8765/api/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "Ascii2013!"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"token": "e63f3f2e16a73db3...", "user": "admin"}
|
||||
```
|
||||
|
||||
**后续请求带 Token**
|
||||
```
|
||||
Authorization: Bearer e63f3f2e16a73db3...
|
||||
```
|
||||
|
||||
默认用户名 `admin`,密码 `Ascii2013!`。可在 `.env` 中配置 `AGENT_ADMIN_PASS_HASH` + `AGENT_ADMIN_PASS_SALT` 修改。
|
||||
|
||||
---
|
||||
|
||||
## 一、主 Agent 对话(LangGraph API,2024)
|
||||
|
||||
### POST /threads — 创建对话线程
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:2024/threads
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"thread_id": "019e8702-..."}
|
||||
```
|
||||
|
||||
### POST /threads/search — 查询所有线程
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:2024/threads/search
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
[
|
||||
{"thread_id": "019e8702-...", ...},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### GET /threads/{thread_id}/state — 获取线程状态(含历史消息)
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:2024/threads/{thread_id}/state
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"values": {
|
||||
"messages": [
|
||||
{"type": "human", "content": "查询天气"},
|
||||
{"type": "ai", "content": "...", "tool_calls": [...]},
|
||||
{"type": "tool", "name": "delegate_to_mcpmanageragent", "content": "..."}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /threads/{thread_id}/runs/stream — 发送消息并流式获取回复
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:2024/threads/{thread_id}/runs/stream
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"assistant_id": "simple_agent",
|
||||
"thread_id": "{thread_id}",
|
||||
"config": {"configurable": {"temperature": 0.7}},
|
||||
"input": {
|
||||
"messages": [{"role": "user", "content": "查询北京天气"}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应(SSE 流)**
|
||||
```
|
||||
event: metadata
|
||||
data: {"run_id":"...","attempt":1}
|
||||
|
||||
event: values
|
||||
data: {"messages":[{"type":"ai","content":"...","tool_calls":[...]}]}
|
||||
```
|
||||
|
||||
消息类型:`human`(用户) | `ai`(Agent) | `tool`(工具结果)
|
||||
|
||||
### POST /runs/stream — 无状态运行(调度器用)
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:2024/runs/stream
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"assistant_id": "simple_agent",
|
||||
"input": {
|
||||
"messages": [{"role": "user", "content": "[定时任务执行] 任务描述"}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /ok — 健康检查
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:2024/ok
|
||||
```
|
||||
→ `200 {"ok": true}`
|
||||
|
||||
---
|
||||
|
||||
## 二、MCP 查询(8765)
|
||||
|
||||
### POST /api/mcp_query — 直接调用高德地图等 MCP 服务
|
||||
|
||||
不经过 LLM,直接查询。依赖 `.env` 中的 `AMAP_KEY`。
|
||||
|
||||
**请求**
|
||||
```
|
||||
POST http://127.0.0.1:8765/api/mcp_query
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{"query": "查询北京天气"}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"result": "{\"city\":\"北京市\",\"forecasts\":[{\"date\":\"2026-06-03\",\"dayweather\":\"多云\",\"daytemp_float\":\"34.0\",...}]}"
|
||||
}
|
||||
```
|
||||
|
||||
**支持的查询类型**:天气(自动识别城市)、地理编码、周边搜索、IP 定位、默认天气。
|
||||
|
||||
### POST /api/mcp_reconnect — 强制 MCP 重连
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/api/mcp_reconnect
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"success": true, "message": "MCP 重连成功"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、文件与工作空间(8765)
|
||||
|
||||
### GET /workspace/{filename} — 获取工作空间文件(免鉴权)
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:8765/workspace/weather_chart.svg
|
||||
```
|
||||
|
||||
**响应**:文件内容 + 对应 Content-Type(`.svg` → `image/svg+xml`,`.png` → `image/png` 等)
|
||||
|
||||
支持中文文件名(自动 URL 解码)。搜索路径:桌面 AgentWorkspace → 项目 workspace 目录。
|
||||
|
||||
### POST /upload?type=knowledge|memory — 上传文件
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/upload?type=knowledge
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <binary>
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"status": "ok", "path": "D:\\Desktop\\AgentWorkspace\\knowledge\\file.txt"}
|
||||
```
|
||||
|
||||
### POST /api/write_file — 写入文件
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/api/write_file
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{"path": "test.txt", "content": "Hello World"}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"success": true, "path": "D:\\Desktop\\AgentWorkspace\\test.txt", "size": 11}
|
||||
```
|
||||
|
||||
### 安全确认流程
|
||||
|
||||
```
|
||||
GET /api/confirm_info?confirm_id={uuid} # 查询确认状态
|
||||
POST /api/confirm_write # 批准或拒绝
|
||||
{"confirm_id": "{uuid}", "choice": "approve|reject"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、技能管理(8765)
|
||||
|
||||
### GET /skills/list — 列出已安装技能
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:8765/skills/list
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
[
|
||||
{"name": "WriterAgent", "description": "负责文件创建、写入..."},
|
||||
{"name": "MCPManagerAgent", "description": "访问高德地图等外部服务..."}
|
||||
]
|
||||
```
|
||||
|
||||
### GET /skills/trash — 列出已删除技能
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:8765/skills/trash
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### POST /skills/reload — 热更新全部技能
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/skills/reload
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"status": "ok", "message": "所有配置和技能已从磁盘重新加载。"}
|
||||
```
|
||||
|
||||
**注意**:此端点已升级为全量热重载,包括 `.env`、`mcp_config.json`、`remote_skills.json`、`prompts/*.md`。
|
||||
|
||||
### POST /skills/delete?name={name} — 删除技能(移入垃圾桶)
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/skills/delete?name=roll-dice
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### POST /skills/recover?name={name} — 从垃圾桶恢复
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/skills/recover?name=roll-dice
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### POST /skills/permanent_delete?name={name} — 永久删除
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/skills/permanent_delete?name=roll-dice
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### POST /skills/upload — 上传技能包
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/skills/upload
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <skill.zip or SKILL.md>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、系统管理(8765)
|
||||
|
||||
### POST /api/restart — 应用配置并重启
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/api/restart
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"m_model": "deepseek-v4-pro",
|
||||
"m_url": "https://api.deepseek.com/v1",
|
||||
"m_key": "sk-xxx"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"success": true, "message": "配置已应用并备份,服务正在重启..."}
|
||||
```
|
||||
|
||||
支持写入 `.env` 的字段:`m_model`, `m_url`, `m_key`, `w_model`, `w_url`, `w_key`, `mcp_model`, `mcp_url`, `mcp_key`。
|
||||
|
||||
### POST /api/deploy — JAR 部署
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8765/api/deploy
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
jar_file: <binary>
|
||||
target_dir: "/opt/app"
|
||||
health_url: "http://127.0.0.1:8080/health"
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"backup": "app.jar.bak.20260604_120000",
|
||||
"saved": "/opt/app/app.jar",
|
||||
"started": true,
|
||||
"returncode": 0,
|
||||
"health_ok": true,
|
||||
"rolled_back": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、热更新机制
|
||||
|
||||
修改以下文件后 **2-3 秒自动生效**,无需重启:
|
||||
|
||||
| 文件 | 效果 |
|
||||
|------|------|
|
||||
| `.env` | 模型/Key/URL 即时切换 |
|
||||
| `mcp_config.json` | MCP 服务增删自动重连 |
|
||||
| `remote_skills.json` | 远端子智能体即时上/下线 |
|
||||
| `prompts/*.md` | 提示词即时生效 |
|
||||
|
||||
也可手动触发:`POST /skills/reload`
|
||||
|
||||
---
|
||||
|
||||
## 七、静态文件
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:8765/static/chat.html # 主聊天界面
|
||||
GET http://127.0.0.1:8765/static/mcp.html # MCP 只读查询界面(暂定)
|
||||
GET http://127.0.0.1:8765/static/chat.js # 前端 JS
|
||||
GET http://127.0.0.1:8765/static/lib/marked.min.js # Markdown 渲染
|
||||
GET http://127.0.0.1:8765/static/lib/highlight.min.js # 代码高亮
|
||||
```
|
||||
@@ -0,0 +1,364 @@
|
||||
# chat.html API 接口文档
|
||||
|
||||
## 服务地址
|
||||
|
||||
| 服务 | 变量名 | 实际地址 |
|
||||
|------|--------|----------|
|
||||
| LangGraph API | `API_BASE` | `http://服务器IP:2024` |
|
||||
| 聊天管理 | `CHAT_SERVER` | `http://服务器IP:8765` |
|
||||
|
||||
---
|
||||
|
||||
## Auth 机制
|
||||
|
||||
```
|
||||
POST /api/login → 获取 token
|
||||
后续请求: Authorization: Bearer <token>
|
||||
X-User: <currentUser>
|
||||
```
|
||||
|
||||
token 存 `sessionStorage`,24h 过期。内嵌模式可 URL 传参 `?user=xxx&token=xxx` 跳过登录。
|
||||
|
||||
---
|
||||
|
||||
## 一、LangGraph API(2024)
|
||||
|
||||
### POST /threads — 创建线程
|
||||
|
||||
```
|
||||
POST http://服务器IP:2024/threads
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"thread_id": "019e8702-..."}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /threads/search — 查询已有线程
|
||||
|
||||
```
|
||||
POST http://服务器IP:2024/threads/search
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
[
|
||||
{"thread_id": "019e8702-...", ...}
|
||||
]
|
||||
```
|
||||
|
||||
**说明**:与本地 localStorage 线程列表合并,仅保留服务器上存在的线程。
|
||||
|
||||
---
|
||||
|
||||
### GET /threads/{thread_id}/state — 获取线程状态(历史消息)
|
||||
|
||||
```
|
||||
GET http://服务器IP:2024/threads/{thread_id}/state
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"values": {
|
||||
"messages": [
|
||||
{"type": "human", "content": "你好"},
|
||||
{"type": "ai", "content": "你好!有什么可以帮助你的?", "tool_calls": [...]},
|
||||
{"type": "tool", "name": "delegate_to_mcpmanageragent", "content": "..."}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:切换线程时调用,优先展示 localStorage 缓存,后台静默同步。消息缓存到 `msg_cache_{thread_id}`。
|
||||
|
||||
---
|
||||
|
||||
### POST /threads/{thread_id}/runs/stream — 发送消息(SSE 流)
|
||||
|
||||
**正常发送**
|
||||
```
|
||||
POST http://服务器IP:2024/threads/{thread_id}/runs/stream
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"assistant_id": "simple_agent",
|
||||
"thread_id": "{thread_id}",
|
||||
"config": {"configurable": {"temperature": 0.7}},
|
||||
"input": {
|
||||
"messages": [{"role": "user", "content": "查询北京天气"}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**中断恢复**
|
||||
```json
|
||||
{
|
||||
"assistant_id": "simple_agent",
|
||||
"thread_id": "{thread_id}",
|
||||
"input": null,
|
||||
"command": {
|
||||
"resume": {
|
||||
"__interrupt_id__": "{id}",
|
||||
"value": "approve"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应(SSE 流)**
|
||||
```
|
||||
event: metadata
|
||||
data: {"run_id":"...","attempt":1}
|
||||
|
||||
event: values
|
||||
data: {"messages":[{"type":"ai","content":"...","tool_calls":[...]}]}
|
||||
|
||||
event: values
|
||||
data: {"messages":[...,{"type":"tool","name":"...","content":"..."}]}
|
||||
```
|
||||
|
||||
**特殊事件**
|
||||
```json
|
||||
{"__interrupt__": {"message": "...", "__interrupt_id__": "..."}}
|
||||
```
|
||||
触发前端弹窗等待用户确认。
|
||||
|
||||
**消息类型**
|
||||
|
||||
| type | 角色 | 说明 |
|
||||
|------|------|------|
|
||||
| `human` | 用户 | 前端忽略(不发气泡) |
|
||||
| `ai` | Agent | Markdown 渲染 |
|
||||
| `ai` (带 tool_calls) | Agent | 显示工具调用 |
|
||||
| `tool` | 工具结果 | 含 `` 时用 Markdown 渲染 |
|
||||
|
||||
---
|
||||
|
||||
## 二、聊天管理 API(8765)
|
||||
|
||||
### POST /api/login — 登录
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/api/login
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "admin", "password": "Ascii2013!"}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"token": "e63f3f2e...", "user": "admin"}
|
||||
```
|
||||
401: `{"error": "用户名或密码错误"}`
|
||||
429: `{"error": "登录尝试次数过多"}`(5次/15分钟)
|
||||
|
||||
---
|
||||
|
||||
### POST /api/save_config — 保存配置(热更新,推荐)
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/api/save_config
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"temperature": 0.7,
|
||||
"m_model": "deepseek-v4-pro",
|
||||
"m_url": "https://api.deepseek.com/v1",
|
||||
"m_key": "sk-xxx",
|
||||
"w_model": "deepseek-v4-pro",
|
||||
"w_url": "https://api.deepseek.com/v1",
|
||||
"w_key": "sk-xxx",
|
||||
"mcp_model": "deepseek-v4-pro",
|
||||
"mcp_url": "https://api.deepseek.com/v1",
|
||||
"mcp_key": "sk-xxx"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"success": true, "message": "配置已保存,热更新自动生效"}
|
||||
```
|
||||
|
||||
**说明**:所有字段可选。写 `.env` 后文件监听自动重载,无需重启。
|
||||
|
||||
---
|
||||
|
||||
### POST /api/restart — 应用并重启(已弃用,推荐 /api/save_config)
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/api/restart
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{ ... 同 /api/save_config ... }
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"success": true, "message": "配置已应用并备份,服务正在重启..."}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 安全确认流程
|
||||
|
||||
**查询确认状态**
|
||||
```
|
||||
GET http://服务器IP:8765/api/confirm_info?confirm_id={uuid}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
**响应**
|
||||
```json
|
||||
{"confirm_id": "{uuid}", "target_path": "...", "result": "pending", "risk_analysis": "..."}
|
||||
```
|
||||
|
||||
**批准/拒绝**
|
||||
```
|
||||
POST http://服务器IP:8765/api/confirm_write
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"confirm_id": "{uuid}", "choice": "approve"}
|
||||
```
|
||||
|
||||
**响应(批准)**
|
||||
```json
|
||||
{"success": true, "message": "写入已确认并执行", "path": "...", "size": 123}
|
||||
```
|
||||
|
||||
**响应(拒绝)**
|
||||
```json
|
||||
{"success": true, "message": "用户拒绝写入"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /upload?type=knowledge|memory — 上传文件
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/upload?type=knowledge
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <binary>
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"status": "ok", "path": "/absolute/path"}
|
||||
```
|
||||
|
||||
**type 说明**:
|
||||
- `knowledge`:`.txt .md .json .csv .yml .xml .html .css .js .py .java .cpp .c .h`
|
||||
- `memory`:其他所有文件
|
||||
|
||||
---
|
||||
|
||||
## 三、技能管理 API(8765)
|
||||
|
||||
### GET /skills/list — 已安装技能
|
||||
|
||||
```
|
||||
GET http://服务器IP:8765/skills/list
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
[{"name": "WriterAgent", "description": "负责文件创建、写入..."}, ...]
|
||||
```
|
||||
|
||||
### GET /skills/trash — 已删除技能
|
||||
|
||||
```
|
||||
GET http://服务器IP:8765/skills/trash
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
[{"name": "roll-dice", "deleted_at": 1717482000000, "description": "已删除"}]
|
||||
```
|
||||
|
||||
### POST /skills/reload — 热更新全部配置
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/skills/reload
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"status": "ok", "message": "所有配置和技能已从磁盘重新加载。"}
|
||||
```
|
||||
|
||||
### POST /skills/delete?name={name} — 删除(移至垃圾桶)
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/skills/delete?name=roll-dice
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### POST /skills/recover?name={name} — 恢复
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/skills/recover?name=roll-dice
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### POST /skills/permanent_delete?name={name} — 永久删除
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/skills/permanent_delete?name=roll-dice
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### POST /skills/upload — 上传安装技能
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/skills/upload
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <skill.zip 或 SKILL.md>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、外部 API
|
||||
|
||||
### GET {base_url}/models — 获取模型列表
|
||||
|
||||
```
|
||||
GET https://api.deepseek.com/v1/models
|
||||
Authorization: Bearer <用户输入的 API Key>
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"data": [{"id": "deepseek-v4-pro"}, {"id": "deepseek-v4-flash"}]}
|
||||
```
|
||||
|
||||
**说明**:前端设置面板点"获取"触发。用用户手动填入的 Key,不是会话 token。
|
||||
|
||||
---
|
||||
|
||||
## 五、静态资源
|
||||
|
||||
```
|
||||
GET /static/chat.html 主聊天界面
|
||||
GET /static/chat.js 前端逻辑
|
||||
GET /static/lib/marked.min.js Markdown 渲染
|
||||
GET /static/lib/highlight.min.js 代码高亮
|
||||
GET /workspace/{filename} 工作空间文件(免鉴权)
|
||||
```
|
||||
@@ -0,0 +1,164 @@
|
||||
# mcp.html API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
mcp.html 是只读 MCP 查询页面——只能通过 Agent 调用 MCP 工具,无文件系统/技能管理权限。API 调用是 chat.html 的子集,**多了 `mcp_mode: true` 配置标记**。
|
||||
|
||||
## 服务地址
|
||||
|
||||
| 服务 | 变量名 | 实际地址 |
|
||||
|------|--------|----------|
|
||||
| LangGraph API | `API_BASE` | `http://服务器IP:2024` |
|
||||
| 聊天管理 | `CHAT_SERVER` | `http://服务器IP:8765` |
|
||||
|
||||
---
|
||||
|
||||
## Auth 机制
|
||||
|
||||
与 chat.html 完全一致:`POST /api/login` → `Bearer <token>` + `X-User: <value>`。
|
||||
|
||||
---
|
||||
|
||||
## 一、LangGraph API(2024)
|
||||
|
||||
### POST /threads — 创建线程
|
||||
|
||||
```
|
||||
POST http://服务器IP:2024/threads
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"thread_id": "019e8702-..."}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /threads/search — 查询线程
|
||||
|
||||
```
|
||||
POST http://服务器IP:2024/threads/search
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /threads/{thread_id}/state — 获取线程状态
|
||||
|
||||
```
|
||||
GET http://服务器IP:2024/threads/{thread_id}/state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /threads/{thread_id}/runs/stream — 发送消息(SSE 流)
|
||||
|
||||
**与 chat.html 的唯一区别**:`config.configurable.mcp_mode = true`
|
||||
|
||||
```
|
||||
POST http://服务器IP:2024/threads/{thread_id}/runs/stream
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"assistant_id": "simple_agent",
|
||||
"thread_id": "{thread_id}",
|
||||
"config": {"configurable": {"mcp_mode": true, "temperature": 0.7}},
|
||||
"input": {
|
||||
"messages": [{"role": "user", "content": "查询北京天气"}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**中断恢复**(也带 `mcp_mode: true`)
|
||||
```json
|
||||
{
|
||||
"assistant_id": "simple_agent",
|
||||
"thread_id": "{thread_id}",
|
||||
"input": null,
|
||||
"config": {"configurable": {"mcp_mode": true}},
|
||||
"command": {
|
||||
"resume": {
|
||||
"__interrupt_id__": "{id}",
|
||||
"value": "approve"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:SSE 流,与 chat.html 完全相同。
|
||||
|
||||
---
|
||||
|
||||
## 二、聊天管理 API(8765)
|
||||
|
||||
### POST /api/login — 登录
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/api/login
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "admin", "password": "Ascii2013!"}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{"token": "e63f3f2e...", "user": "admin"}
|
||||
```
|
||||
|
||||
### GET /skills/list — 鉴权验证
|
||||
|
||||
仅用于验证 token 有效性,不渲染技能列表。
|
||||
|
||||
```
|
||||
GET http://服务器IP:8765/skills/list
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 安全确认流程
|
||||
|
||||
**查询确认状态**
|
||||
```
|
||||
GET http://服务器IP:8765/api/confirm_info?confirm_id={uuid}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**批准/拒绝**
|
||||
```
|
||||
POST http://服务器IP:8765/api/confirm_write
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"confirm_id": "{uuid}", "choice": "approve"}
|
||||
```
|
||||
|
||||
### POST /upload?type=knowledge|memory — 上传文件
|
||||
|
||||
```
|
||||
POST http://服务器IP:8765/upload?type=knowledge
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <binary>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## mcp.html 与 chat.html 对比
|
||||
|
||||
| 功能 | chat.html | mcp.html |
|
||||
|------|-----------|----------|
|
||||
| Agent 对话 | ✅ | ✅ |
|
||||
| 对话历史 | ✅(可删除) | ✅(禁止删除) |
|
||||
| 文件上传 | ✅ | ✅ |
|
||||
| 安全确认弹窗 | ✅ | ✅ |
|
||||
| 技能管理面板 | ✅ | ❌ |
|
||||
| 设置面板 | ✅ | ❌ |
|
||||
| 保存配置/重启 | ✅ | ❌ |
|
||||
| 获取模型列表 | ✅ | ❌ |
|
||||
| 导出对话 | ✅ | ❌ |
|
||||
| `mcp_mode` 标记 | ❌ | ✅ |
|
||||
@@ -0,0 +1,88 @@
|
||||
# PAM Agent - Linux RedHat 部署
|
||||
|
||||
## 前置要求
|
||||
- RedHat 8+/CentOS 8+/Rocky 8+
|
||||
- root 权限
|
||||
- 网络连接(安装 Python 和 pip 依赖)
|
||||
|
||||
## 快速部署(用户级,无需 root)
|
||||
```bash
|
||||
# 1. 上传并解压
|
||||
scp pam-aiagent.tar.gz prouser@your-server:/opt/app/
|
||||
ssh prouser@your-server
|
||||
cd /opt/app
|
||||
mkdir -p pam-aiagent
|
||||
tar xzf pam-aiagent.tar.gz -C pam-aiagent --strip-components=1
|
||||
|
||||
# 2. 安装(全程不联网,无需 root)
|
||||
cd /opt/app/pam-aiagent/deploy_linux
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
|
||||
# 3. 配置 API Key
|
||||
vim /opt/app/pam-aiagent/.env
|
||||
|
||||
# 4. 启动
|
||||
/opt/app/pam-aiagent/start.sh
|
||||
|
||||
# 5. 访问
|
||||
浏览器打开 http://服务器IP:8765/static/chat.html
|
||||
```
|
||||
|
||||
## 启停
|
||||
```bash
|
||||
/opt/app/pam-aiagent/start.sh # 启动
|
||||
/opt/app/pam-aiagent/stop.sh # 停止
|
||||
tail -f /opt/app/pam-aiagent/logs/server.log # 查看日志
|
||||
```
|
||||
|
||||
## 依赖
|
||||
服务器需预装(可用 RedHat ISO 离线装):
|
||||
```bash
|
||||
# 编译 Python 需要(如果用 dnf 直接装 python3.12 则不需要)
|
||||
openssl-devel bzip2-devel libffi-devel zlib-devel readline-devel sqlite-devel
|
||||
```
|
||||
如服务器已有 python3.12 含 SSL,脚本会自动跳过编译。
|
||||
|
||||
## 离线部署(无网络环境)
|
||||
在**有网络的机器**上下载依赖包:
|
||||
```bash
|
||||
pip download -r requirements-linux.txt -d wheels/
|
||||
```
|
||||
然后将 wheels 目录和项目文件一起传到服务器,手动安装:
|
||||
```bash
|
||||
pip install --no-index --find-links=wheels/ -r requirements-linux.txt
|
||||
```
|
||||
|
||||
## 手动安装 Python 3.12(含 SSL)
|
||||
|
||||
如果 `dnf install python3.12` 不可用或需要源码编译:
|
||||
|
||||
```bash
|
||||
# 先装 SSL 编译依赖(必须,否则 pip 无法联网)
|
||||
dnf install -y openssl-devel bzip2-devel libffi-devel zlib-devel readline-devel sqlite-devel
|
||||
|
||||
# 方案一:yum/dnf
|
||||
dnf install -y epel-release
|
||||
dnf module enable python312 -y
|
||||
dnf install -y python312 python312-devel python312-pip
|
||||
|
||||
# 方案二:源码编译
|
||||
wget https://www.python.org/ftp/python/3.12.9/Python-3.12.9.tgz
|
||||
tar xzf Python-3.12.9.tgz && cd Python-3.12.9
|
||||
./configure --enable-optimizations --with-ssl --prefix=/usr/local/python3.12
|
||||
make -j$(nproc) && make install
|
||||
ln -sf /usr/local/python3.12/bin/python3.12 /usr/bin/python3.12
|
||||
|
||||
# 验证 SSL
|
||||
python3.12 -c "import ssl; print(ssl.OPENSSL_VERSION)"
|
||||
# 应输出: OpenSSL 3.x.x ...
|
||||
```
|
||||
|
||||
> ⚠️ **必须安装 SSL**:没有 `openssl-devel` 编译的 Python 无法 `pip install`,也无法调用 HTTPS API。
|
||||
|
||||
## 端口
|
||||
| 服务 | 端口 |
|
||||
|------|------|
|
||||
| 聊天界面 | 8765 |
|
||||
| LangGraph API | 2024 |
|
||||
Binary file not shown.
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# PAM Agent 用户级部署脚本(无需 root)
|
||||
# 用法: chmod +x install.sh && ./install.sh
|
||||
set -e
|
||||
|
||||
echo "=== PAM Agent 部署 (prouser 用户级) ==="
|
||||
APP_DIR="/opt/app/pam-aiagent"
|
||||
PY_DIR="/opt/app/python3.12"
|
||||
|
||||
# 1. 安装 Python 3.12
|
||||
echo "[1/4] 安装 Python 3.12..."
|
||||
PY_DIR="/opt/app/python3.12"
|
||||
export PATH="$PY_DIR/bin:$PATH"
|
||||
|
||||
# 先检查系统是否已有 Python 3.12 + SSL
|
||||
if command -v python3.12 &>/dev/null && python3.12 -c "import ssl" 2>/dev/null; then
|
||||
echo " 系统 Python 3.12 可用: $(python3.12 --version)"
|
||||
PY_DIR=$(dirname $(dirname $(which python3.12)))
|
||||
# 再检查预编译二进制
|
||||
elif [ -f "$APP_DIR/deploy_linux/python-3.12-linux.tar.gz" ]; then
|
||||
echo " 解压预编译 Python 3.12(免编译)..."
|
||||
mkdir -p "$PY_DIR"
|
||||
tar xzf "$APP_DIR/deploy_linux/python-3.12-linux.tar.gz" -C "$PY_DIR" --strip-components=1
|
||||
echo " Python 3.12 已就绪: $($PY_DIR/bin/python3.12 --version)"
|
||||
else
|
||||
echo " [ERROR] 无系统 Python 3.12,也无预编译包"
|
||||
echo " 请执行: dnf install -y gcc openssl-devel(从 RedHat ISO 离线装)"
|
||||
echo " 然后重试编译: tar xzf Python-3.12.9.tgz && cd Python-3.12.9 && ./configure && make && make install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证 SSL
|
||||
if ! "$PY_DIR/bin/python3.12" -c "import ssl" 2>/dev/null; then
|
||||
echo " [ERROR] Python SSL 不可用"; exit 1
|
||||
fi
|
||||
|
||||
# 2. 创建虚拟环境
|
||||
echo "[2/4] 创建虚拟环境..."
|
||||
"$PY_DIR/bin/python3.12" -m venv "$APP_DIR/venv" --without-pip
|
||||
source "$APP_DIR/venv/bin/activate"
|
||||
# 离线安装 pip
|
||||
"$PY_DIR/bin/python3.12" -m ensurepip 2>/dev/null
|
||||
python -m pip install --no-index --find-links="$APP_DIR/deploy_linux/wheels/" pip setuptools wheel 2>/dev/null || true
|
||||
|
||||
# 3. 安装依赖(仅本地wheels)
|
||||
echo "[3/4] 安装 Python 依赖..."
|
||||
|
||||
# tiktoken 无 Linux wheel,装假包(不影响功能)
|
||||
rm -f "$APP_DIR/deploy_linux/wheels/tiktoken"*
|
||||
mkdir -p /tmp/faketiktoken/tiktoken
|
||||
# 空模块让 import tiktoken 不报错
|
||||
cat > /tmp/faketiktoken/tiktoken/__init__.py << 'INIT'
|
||||
"""Fake tiktoken for offline deployment"""
|
||||
def get_encoding(name='cl100k_base'):
|
||||
class FakeEncoding:
|
||||
def encode(self, text): return [1]*len(text)
|
||||
def decode(self, tokens): return ''
|
||||
return FakeEncoding()
|
||||
def encoding_for_model(model): return get_encoding()
|
||||
INIT
|
||||
cat > /tmp/faketiktoken/setup.py << 'SETUP'
|
||||
from setuptools import setup, find_packages
|
||||
setup(name='tiktoken', version='0.7.0', packages=find_packages(), install_requires=[])
|
||||
SETUP
|
||||
pip install --no-index /tmp/faketiktoken/
|
||||
rm -rf /tmp/faketiktoken
|
||||
|
||||
pip install --no-index --find-links="$APP_DIR/deploy_linux/wheels/" -r "$APP_DIR/requirements-linux.txt"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo " [ERROR] 依赖安装失败"
|
||||
exit 1
|
||||
fi
|
||||
deactivate
|
||||
|
||||
# 4. 配置
|
||||
echo "[4/4] 配置文件..."
|
||||
|
||||
# .env
|
||||
if [ ! -f "$APP_DIR/.env" ]; then
|
||||
cat > "$APP_DIR/.env" << 'EOF'
|
||||
# === 模型配置 ===
|
||||
LLM_MODEL=deepseek-v4-pro
|
||||
LLM_BASE_URL=https://api.deepseek.com/v1
|
||||
LLM_API_KEY=你的KEY
|
||||
LLM_MODEL_W=deepseek-v4-pro
|
||||
LLM_BASE_URL_W=https://api.deepseek.com/v1
|
||||
LLM_API_KEY_W=你的KEY
|
||||
LLM_MODEL_M=deepseek-v4-pro
|
||||
LLM_BASE_URL_M=https://api.deepseek.com/v1
|
||||
LLM_API_KEY_M=你的KEY
|
||||
|
||||
# === 高德地图 ===
|
||||
AMAP_KEY=你的KEY
|
||||
|
||||
# === 服务配置 ===
|
||||
AGENT_HOST=0.0.0.0
|
||||
AGENT_ADMIN_USER=admin
|
||||
AGENT_ADMIN_PASS_HASH=43d5f493c42c3d3ad6cd692ea9f87e69e427c7f95aab3f76209dfa5ad602920d
|
||||
AGENT_ADMIN_PASS_SALT=8293ce07ae02df60e4300e21802f8ffed9561b0f7679acda2bd1a407ad108282
|
||||
LANGCHAIN_TRACING_V2=false
|
||||
|
||||
# === 工作空间 ===
|
||||
WRITER_WORKSPACE=/opt/app/pam-aiagent/workspace
|
||||
EOF
|
||||
fi
|
||||
|
||||
mkdir -p "$APP_DIR/workspace/knowledge" "$APP_DIR/workspace/memory"
|
||||
mkdir -p "$APP_DIR/logs"
|
||||
|
||||
# 启动脚本
|
||||
cat > "$APP_DIR/start.sh" << EOF
|
||||
#!/bin/bash
|
||||
cd "$APP_DIR"
|
||||
export PATH="$PY_DIR/bin:\$PATH"
|
||||
export PYTHONUTF8=1
|
||||
export PYTHONPATH="$APP_DIR/src:\$PYTHONPATH"
|
||||
nohup "$APP_DIR/venv/bin/python" start_all.py > "$APP_DIR/logs/server.log" 2>&1 &
|
||||
echo \$! > "$APP_DIR/logs/server.pid"
|
||||
echo "PAM Agent started (PID: \$!)"
|
||||
EOF
|
||||
|
||||
# 停止脚本
|
||||
cat > "$APP_DIR/stop.sh" << EOF
|
||||
#!/bin/bash
|
||||
if [ -f "$APP_DIR/logs/server.pid" ]; then
|
||||
PID=\$(cat "$APP_DIR/logs/server.pid")
|
||||
kill \$PID 2>/dev/null && echo "PAM Agent stopped (PID: \$PID)"
|
||||
rm -f "$APP_DIR/logs/server.pid"
|
||||
else
|
||||
echo "No PID file found. Try: pkill -f start_all.py"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x "$APP_DIR/start.sh" "$APP_DIR/stop.sh"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " PAM Agent 部署完成(用户级)"
|
||||
echo "========================================"
|
||||
echo " 安装目录: $APP_DIR"
|
||||
echo " Python: $PY_DIR"
|
||||
echo " 配置 Key: vim $APP_DIR/.env"
|
||||
echo ""
|
||||
echo " 启动: $APP_DIR/start.sh"
|
||||
echo " 停止: $APP_DIR/stop.sh"
|
||||
echo " 日志: tail -f $APP_DIR/logs/server.log"
|
||||
echo " 前端: http://服务器IP:8765/static/chat.html"
|
||||
echo ""
|
||||
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Agent 对话</title>
|
||||
<script src="lib/marked.min.js"></script>
|
||||
<script src="lib/highlight.min.js"></script>
|
||||
<style>
|
||||
:root { --bg: #f0f2f5; --chat-bg: #ffffff; --accent: #4f6ef7; --accent-dark: #3b54d4; --text: #1a1a2e; --text-secondary: #6b7280; --border: #e5e7eb; --sidebar-w: 220px; --skillbar-w: 240px; --radius: 12px; --radius-sm: 8px; --shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); display:flex; height:100vh; }
|
||||
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:#d1d5db; border-radius:3px; }
|
||||
.app-container { display:flex; width:100%; height:100vh; background:var(--chat-bg); overflow:hidden; }
|
||||
.sidebar { width:var(--sidebar-w); min-width:var(--sidebar-w); max-width:var(--sidebar-w); background:#f8f9fb; border-right:1px solid var(--border); display:flex; flex-direction:column; transition:width 0.25s ease; overflow:hidden; flex-shrink:0; }
|
||||
.sidebar.collapsed { width:0; min-width:0; max-width:0; border-right:none; }
|
||||
.sidebar-header { padding:16px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); }
|
||||
.sidebar-header span { font-weight:600; font-size:15px; color:var(--text); }
|
||||
.new-chat-btn { background:var(--accent); color:white; border:none; padding:7px 14px; border-radius:var(--radius-sm); cursor:pointer; font-size:13px; font-weight:500; }
|
||||
.new-chat-btn:hover { background:var(--accent-dark); }
|
||||
.thread-list { flex:1; overflow-y:auto; padding:6px; }
|
||||
.thread-item { padding:10px 12px; border-radius:var(--radius-sm); cursor:pointer; font-size:13px; display:flex; align-items:center; justify-content:space-between; margin:2px 0; color:var(--text-secondary); }
|
||||
.thread-item:hover { background:#eef0f5; color:var(--text); }
|
||||
.thread-item.active { background:#e8edff; color:var(--accent); font-weight:500; }
|
||||
.thread-item .del-btn { background:none; border:none; color:#d1d5db; cursor:pointer; font-size:14px; padding:2px 4px; border-radius:4px; }
|
||||
.thread-item .del-btn:hover { color:#ef4444; background:#fee; }
|
||||
.chat-container { flex:1; min-width:0; display:flex; flex-direction:column; background:var(--chat-bg); }
|
||||
.chat-header { padding:14px 20px; background:linear-gradient(135deg,var(--accent) 0%,#6366f1 100%); color:white; font-size:16px; font-weight:600; display:flex; align-items:center; gap:8px; }
|
||||
.chat-header .title { flex:1; }
|
||||
.toggle-btn { background:rgba(255,255,255,0.15); border:none; color:white; padding:6px 10px; border-radius:6px; cursor:pointer; font-size:14px; }
|
||||
.toggle-btn:hover { background:rgba(255,255,255,0.25); }
|
||||
.export-btn { background:rgba(255,255,255,0.15); border:none; color:white; cursor:pointer; font-size:13px; padding:6px 10px; border-radius:6px; }
|
||||
.export-btn:hover,.settings-btn:hover { background:rgba(255,255,255,0.25); }
|
||||
.settings-btn { background:rgba(255,255,255,0.15); border:none; color:white; cursor:pointer; font-size:16px; padding:6px 10px; border-radius:6px; }
|
||||
.messages { flex:1; overflow-y:auto; padding:20px 24px; display:flex; flex-direction:column; gap:16px; }
|
||||
.message { display:flex; flex-direction:column; max-width:85%; animation:fadeIn 0.2s ease; }
|
||||
@keyframes fadeIn { from{opacity:0;transform:translateY(4px);} to{opacity:1;transform:translateY(0);} }
|
||||
.message.user { align-self:flex-end; }
|
||||
.message.agent,.message.tool,.message.system { align-self:flex-start; }
|
||||
.message .bubble { padding:12px 16px; border-radius:var(--radius); word-break:break-word; overflow-wrap:break-word; white-space:pre-wrap; line-height:1.7; font-size:14.5px; position:relative; box-shadow:var(--shadow); }
|
||||
.user .bubble { background:linear-gradient(135deg,#e8edff 0%,#dce4ff 100%); color:var(--text); border-bottom-right-radius:4px; }
|
||||
.agent .bubble { background:#fff; color:var(--text); border:1px solid #f0f0f0; border-bottom-left-radius:4px; }
|
||||
.tool .bubble { background:#fffdf5; color:#8b6914; font-size:13px; border:1px solid #fef3c7; }
|
||||
.system .bubble { background:#f9fafb; color:var(--text-secondary); font-size:13px; border:1px solid #f3f4f6; }
|
||||
.message .label { font-size:11px; margin-bottom:4px; color:var(--text-secondary); padding-left:4px; font-weight:500; }
|
||||
.agent .bubble h1,.agent .bubble h2,.agent .bubble h3 { margin:16px 0 8px; font-weight:700; color:var(--text); }
|
||||
.agent .bubble h1 { font-size:1.3em; border-bottom:1px solid #eee; padding-bottom:6px; }
|
||||
.agent .bubble h2 { font-size:1.15em; } .agent .bubble h3 { font-size:1.05em; }
|
||||
.agent .bubble p { margin:0 0 8px; } .agent .bubble ul,.agent .bubble ol { padding-left:20px; margin:8px 0; }
|
||||
.agent .bubble li { margin:3px 0; }
|
||||
.agent .bubble a { color:var(--accent); text-decoration:none; font-weight:500; }
|
||||
.agent .bubble a:hover { text-decoration:underline; }
|
||||
.agent .bubble blockquote { border-left:3px solid var(--accent); margin:10px 0; padding:6px 14px; color:var(--text-secondary); background:#f8f9ff; border-radius:0 6px 6px 0; font-style:italic; }
|
||||
.agent .bubble :not(pre)>code { background:#f1f3f5; color:#e53e3e; padding:2px 6px; border-radius:4px; font-size:0.88em; font-weight:500; }
|
||||
.agent .bubble pre { background:#1a1b26; border-radius:var(--radius-sm); margin:12px 0; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.12); }
|
||||
.agent .bubble pre .code-header { display:flex; justify-content:space-between; align-items:center; padding:8px 14px; background:#252636; color:#9ca3af; font-size:11.5px; border-bottom:1px solid #313244; }
|
||||
.agent .bubble pre .code-header .lang { text-transform:uppercase; }
|
||||
.agent .bubble pre .code-header .copy-code { background:rgba(255,255,255,0.06); border:none; color:#9ca3af; cursor:pointer; font-size:11px; padding:4px 8px; border-radius:4px; }
|
||||
.agent .bubble pre .code-header .copy-code:hover { color:#fff; background:rgba(255,255,255,0.12); }
|
||||
.agent .bubble pre>code { display:block; padding:14px 16px; color:#cdd6f4; line-height:1.65; font-size:13.5px; white-space:pre-wrap; word-break:break-word; }
|
||||
.agent .bubble table { border-collapse:collapse; margin:12px 0; width:100%; font-size:13.5px; border-radius:var(--radius-sm); overflow:hidden; box-shadow:var(--shadow); }
|
||||
.agent .bubble th { background:#f0f3ff; color:var(--text); font-weight:600; padding:10px 14px; border:1px solid #e2e6f0; text-align:left; }
|
||||
.agent .bubble td { padding:9px 14px; border:1px solid #eef1f7; }
|
||||
.agent .bubble tr:nth-child(even) td { background:#fafbff; }
|
||||
.agent .bubble tr:hover td { background:#f0f3ff; }
|
||||
.agent .bubble hr { border:none; border-top:1px solid #e5e7eb; margin:16px 0; }
|
||||
.agent .bubble img { max-width:100%; border-radius:6px; }
|
||||
.agent .bubble strong { font-weight:700; }
|
||||
.input-area { display:flex; padding:14px 20px; background:var(--chat-bg); border-top:1px solid var(--border); gap:8px; align-items:center; }
|
||||
#user-input { flex:1; padding:11px 16px; border:1.5px solid #e5e7eb; border-radius:24px; background:#f8f9fb; color:var(--text); outline:none; font-size:14px; }
|
||||
#user-input:focus { border-color:var(--accent); background:#fff; box-shadow:0 0 0 3px rgba(79,110,247,0.1); }
|
||||
.send-btn { padding:10px 18px; background:var(--accent); border:none; border-radius:20px; color:white; font-weight:600; cursor:pointer; font-size:13.5px; }
|
||||
.send-btn:hover { background:var(--accent-dark); box-shadow:0 2px 8px rgba(79,110,247,0.3); }
|
||||
.send-btn:disabled { opacity:0.5; cursor:not-allowed; }
|
||||
.stop-btn { padding:10px 16px; background:#ef4444; border:none; border-radius:20px; color:white; font-weight:600; cursor:pointer; font-size:13.5px; display:none; }
|
||||
.stop-btn:hover { background:#dc2626; }
|
||||
.copy-btn { background:none; border:none; color:#cbd5e0; cursor:pointer; font-size:11px; padding:3px 6px; position:absolute; top:8px; right:10px; opacity:0; transition:all 0.2s; border-radius:4px; }
|
||||
.message:hover .copy-btn { opacity:1; }
|
||||
.copy-btn:hover { color:var(--accent); background:#f0f3ff; }
|
||||
.drop-zone { border:2px dashed #d1d5db; border-radius:var(--radius-sm); padding:18px; text-align:center; color:#9ca3af; margin:0 20px 8px; display:none; }
|
||||
.drop-zone.active { border-color:var(--accent); background:#f0f4ff; color:var(--accent); }
|
||||
.skillbar { width:var(--skillbar-w); min-width:var(--skillbar-w); max-width:var(--skillbar-w); background:#f8f9fb; border-left:1px solid var(--border); display:flex; flex-direction:column; transition:width 0.25s; overflow:hidden; flex-shrink:0; }
|
||||
.skillbar.collapsed { width:0; min-width:0; max-width:0; border-left:none; }
|
||||
.skillbar-header { padding:16px; font-weight:600; font-size:15px; border-bottom:1px solid var(--border); background:linear-gradient(135deg,var(--accent) 0%,#6366f1 100%); color:white; display:flex; justify-content:space-between; align-items:center; }
|
||||
.skill-actions { padding:10px; border-bottom:1px solid var(--border); display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.skill-actions button,.skill-actions label.btn { background:var(--accent); color:white; border:none; padding:6px 10px; border-radius:6px; cursor:pointer; font-size:12px; }
|
||||
.skill-actions button:hover,.skill-actions label.btn:hover { background:var(--accent-dark); }
|
||||
.skill-item { padding:9px 12px; border-bottom:1px solid #f3f4f6; font-size:13px; display:flex; flex-wrap:wrap; align-items:flex-start; }
|
||||
.skill-item:hover { background:#f0f2f5; }
|
||||
.skill-item .skill-info { flex:1; min-width:0; }
|
||||
.skill-item .skill-name { font-weight:600; color:var(--text); }
|
||||
.skill-item .skill-desc { font-size:11.5px; color:var(--text-secondary); word-break:break-word; margin-top:2px; }
|
||||
.skill-item button { background:none; border:none; color:#d1d5db; cursor:pointer; font-size:12px; margin-left:8px; flex-shrink:0; padding:2px 4px; border-radius:4px; }
|
||||
.skill-item button:hover { color:#ef4444; background:#fee; }
|
||||
.confirm-popup { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); display:flex; align-items:center; justify-content:center; z-index:9999; backdrop-filter:blur(2px); }
|
||||
.confirm-box { background:white; padding:28px; border-radius:var(--radius); max-width:500px; width:90%; box-shadow:0 8px 30px rgba(0,0,0,0.15); }
|
||||
.confirm-text { white-space:pre-wrap; word-break:break-word; margin-bottom:20px; max-height:300px; overflow-y:auto; font-size:14.5px; color:var(--text); }
|
||||
.confirm-buttons { display:flex; gap:10px; justify-content:flex-end; }
|
||||
.confirm-buttons button { padding:9px 22px; border:none; border-radius:var(--radius-sm); cursor:pointer; font-weight:600; font-size:14px; }
|
||||
.btn-approve { background:#10b981; color:white; } .btn-approve:hover { background:#059669; }
|
||||
.btn-reject { background:#ef4444; color:white; } .btn-reject:hover { background:#dc2626; }
|
||||
.timeout-note { font-size:12px; color:#9ca3af; margin-top:12px; text-align:right; }
|
||||
.spinner { border:3px solid #e5e7eb; border-top:3px solid var(--accent); border-radius:50%; width:20px; height:20px; animation:spin 0.8s linear infinite; }
|
||||
@keyframes spin { to{transform:rotate(360deg);} }
|
||||
.login-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); display:flex; align-items:center; justify-content:center; z-index:10000; }
|
||||
.login-box { background:white; padding:36px; border-radius:16px; width:360px; text-align:center; box-shadow:0 16px 48px rgba(0,0,0,0.2); animation:fadeIn 0.3s ease; }
|
||||
.login-box h2 { margin-bottom:24px; color:var(--text); font-size:22px; }
|
||||
.login-box input { width:100%; padding:11px 14px; margin:8px 0; border:1.5px solid #e5e7eb; border-radius:10px; font-size:15px; outline:none; }
|
||||
.login-box input:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(79,110,247,0.1); }
|
||||
.login-box .login-btn { width:100%; padding:11px; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); color:white; border:none; border-radius:10px; font-size:16px; font-weight:600; cursor:pointer; margin-top:14px; }
|
||||
.login-box .login-btn:hover { box-shadow:0 4px 16px rgba(102,126,234,0.4); }
|
||||
.login-error { color:#ef4444; font-size:13px; margin-top:10px; min-height:20px; }
|
||||
.settings-panel { position:fixed; top:0; right:0; width:340px; height:100vh; background:white; box-shadow:-4px 0 20px rgba(0,0,0,0.12); z-index:9998; transform:translateX(100%); transition:transform 0.25s ease; padding:20px; overflow-y:auto; }
|
||||
.settings-panel.open { transform:translateX(0); }
|
||||
.settings-panel h3 { font-size:16px; margin-bottom:12px; color:var(--text); }
|
||||
.settings-panel label { display:block; font-size:12px; color:var(--text-secondary); margin-bottom:2px; margin-top:8px; }
|
||||
.settings-panel input { width:100%; padding:7px 10px; border:1.5px solid #e5e7eb; border-radius:6px; font-size:13px; outline:none; }
|
||||
.settings-panel input:focus { border-color:var(--accent); }
|
||||
.settings-panel .save-btn { margin-top:16px; width:100%; padding:10px; background:var(--accent); color:white; border:none; border-radius:8px; font-weight:600; cursor:pointer; }
|
||||
.settings-panel .close-btn { position:absolute; top:12px; right:12px; background:none; border:none; font-size:20px; cursor:pointer; color:#999; }
|
||||
.settings-panel .agent-section { border:1px solid #e5e7eb; border-radius:8px; padding:10px; margin-bottom:10px; }
|
||||
.settings-panel .agent-section h4 { font-size:13px; margin-bottom:6px; color:var(--accent); }
|
||||
.settings-panel .row { display:flex; gap:6px; }
|
||||
.settings-panel .row input { flex:1; }
|
||||
.settings-panel .eye-btn { background:var(--accent); border:none; cursor:pointer; font-size:12px; padding:6px 8px; color:white; border-radius:4px; white-space:nowrap; }
|
||||
.settings-panel .eye-btn:hover { background:var(--accent-dark); }.model-select { width:100%; margin-top:4px; padding:6px; border:1px solid var(--border); border-radius:4px; font-size:12px; background:white; }
|
||||
.hljs { color:#cdd6f4; background:#1a1b26; }
|
||||
.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-section,.hljs-link { color:#cba6f7; }
|
||||
.hljs-string,.hljs-title,.hljs-name,.hljs-type,.hljs-attr,.hljs-symbol,.hljs-bullet,.hljs-addition,.hljs-variable,.hljs-template-tag,.hljs-template-variable { color:#a6e3a1; }
|
||||
.hljs-comment,.hljs-quote,.hljs-deletion,.hljs-meta { color:#6c7086; font-style:italic; }
|
||||
.hljs-number,.hljs-regexp,.hljs-built_in,.hljs-builtin-name { color:#fab387; }
|
||||
.hljs-attribute,.hljs-tag,.hljs-selector-class,.hljs-selector-id { color:#89b4fa; }
|
||||
.hljs-params { color:#cdd6f4; }
|
||||
.hljs-function .hljs-title,.hljs-class .hljs-title { color:#89b4fa; }
|
||||
.hljs-property,.hljs-selector-pseudo { color:#89dceb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header"><span>对话记录</span><button class="new-chat-btn" onclick="createNewChat()">+ 新对话</button></div>
|
||||
<div class="thread-list" id="threadList"></div>
|
||||
</div>
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<button class="toggle-btn" onclick="toggleSidebar('sidebar')" id="toggle-sidebar-btn">◀</button>
|
||||
<span class="title">Agent 对话</span>
|
||||
<button class="export-btn" onclick="exportChat()">↓ 导出</button>
|
||||
<button class="settings-btn" onclick="toggleSettings()">⚙</button>
|
||||
<button class="toggle-btn" onclick="toggleSidebar('skillbar')" id="toggle-skillbar-btn">▶</button>
|
||||
</div>
|
||||
<div class="messages" id="messages"></div>
|
||||
<div id="loading-indicator" style="display:none;margin:0 20px 8px;align-items:center;"><div class="spinner"></div><span style="color:#666;margin-left:8px;">思考中...</span></div>
|
||||
<div class="drop-zone" id="drop-zone">拖拽文件到此处上传</div>
|
||||
<div class="input-area">
|
||||
<input type="text" id="user-input" placeholder="输入消息..." />
|
||||
<input type="file" id="file-input" multiple style="display:none" />
|
||||
<button class="send-btn" onclick="document.getElementById('file-input').click()">📎</button>
|
||||
<button class="send-btn" id="send-btn" onclick="sendMessage()">发送</button>
|
||||
<button class="stop-btn" id="stop-btn" onclick="stopGeneration()">⏹ 停止</button>
|
||||
</div>
|
||||
<div style="padding:4px 20px;font-size:11px;color:var(--text-secondary);text-align:center;" id="token-info"></div>
|
||||
</div>
|
||||
<div class="skillbar" id="skillbar">
|
||||
<div class="skillbar-header">技能管理</div>
|
||||
<div class="skill-actions">
|
||||
<label class="btn" for="skill-zip-upload">上传技能包</label>
|
||||
<input type="file" id="skill-zip-upload" accept=".zip,.md" style="display:none" onchange="uploadSkillZip(this)">
|
||||
<button onclick="reloadSkills()">热更新</button>
|
||||
</div>
|
||||
<div style="flex:1;overflow-y:auto;" id="skillList"><div style="padding:12px;color:#666;">加载中...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panel" id="settings-panel">
|
||||
<button class="close-btn" onclick="toggleSettings()">x</button>
|
||||
<h3>Agent 配置</h3>
|
||||
<p style="font-size:11px;color:#999;margin-bottom:10px;">Temperature 即时生效。模型/URL/Key 对应 .env 变量,修改后需重启。</p>
|
||||
<div class="agent-section">
|
||||
<h4>主 Agent (LLM_MODEL / LLM_API_KEY / LLM_BASE_URL)</h4>
|
||||
<label>模型</label><div class="row"><input type="text" class="model-select" id="cfg-m-model" placeholder="输入模型名或点击获取" onclick="showModelOptions('cfg-m-model','dl-m-model')" /><datalist id="dl-m-model" style="display:none"></datalist><button class="eye-btn" onclick="fetchModels('cfg-m-model','dl-m-model','cfg-m-url','cfg-m-key')" title="获取模型列表">获取</button></div>
|
||||
<label>API URL</label><input type="text" id="cfg-m-url" placeholder="https://open.bigmodel.cn/api/paas/v4" />
|
||||
<label>API Key</label><input type="password" id="cfg-m-key" placeholder="sk-xxx" />
|
||||
<label>Temperature</label><input type="range" id="cfg-m-temp" min="0" max="2" step="0.1" value="0.7" style="width:100%" /><span id="cfg-m-temp-val" style="font-size:12px;color:var(--text-secondary)">0.7</span>
|
||||
<label>上下文窗口 (tokens)</label><input type="number" id="cfg-m-maxtk" value="256000" min="0" style="width:100%" />
|
||||
<button class="save-btn" onclick="testAgent('main')" style="background:#10b981;margin-top:8px;">测试连接</button>
|
||||
</div>
|
||||
<div class="agent-section">
|
||||
<h4>Writer Agent (LLM_MODEL_W / LLM_API_KEY_W / LLM_BASE_URL_W)</h4>
|
||||
<label>模型</label><div class="row"><input type="text" class="model-select" id="cfg-w-model" placeholder="输入模型名或点击获取" onclick="showModelOptions('cfg-w-model','dl-w-model')" /><datalist id="dl-w-model" style="display:none"></datalist><button class="eye-btn" onclick="fetchModels('cfg-w-model','dl-w-model','cfg-w-url','cfg-w-key')" title="获取模型列表">获取</button></div>
|
||||
<label>API URL</label><input type="text" id="cfg-w-url" placeholder="https://api.minimax.chat/v1" />
|
||||
<label>API Key</label><input type="password" id="cfg-w-key" placeholder="sk-xxx" />
|
||||
<button class="save-btn" onclick="testAgent('writer')" style="background:#10b981;margin-top:8px;">测试连接</button>
|
||||
</div>
|
||||
<div class="agent-section">
|
||||
<h4>MCP Agent (LLM_MODEL_M / LLM_API_KEY_M / LLM_BASE_URL_M)</h4>
|
||||
<label>模型</label><div class="row"><input type="text" class="model-select" id="cfg-mcp-model" placeholder="输入模型名或点击获取" onclick="showModelOptions('cfg-mcp-model','dl-mcp-model')" /><datalist id="dl-mcp-model" style="display:none"></datalist><button class="eye-btn" onclick="fetchModels('cfg-mcp-model','dl-mcp-model','cfg-mcp-url','cfg-mcp-key')" title="获取模型列表">获取</button></div>
|
||||
<label>API URL</label><input type="text" id="cfg-mcp-url" placeholder="https://api.minimax.chat/v1" />
|
||||
<label>API Key</label><input type="password" id="cfg-mcp-key" placeholder="sk-xxx" />
|
||||
<button class="save-btn" onclick="testAgent('mcp')" style="background:#10b981;margin-top:8px;">测试连接</button>
|
||||
</div>
|
||||
<button class="save-btn" onclick="saveSettings()">保存全部</button>
|
||||
<button class="save-btn" onclick="applyAndRestart()" style="background:#f59e0b;margin-top:8px;">应用并重启</button>
|
||||
</div>
|
||||
<div class="login-overlay" id="login-overlay">
|
||||
<div class="login-box">
|
||||
<h2>登录</h2>
|
||||
<input type="text" id="login-username" placeholder="用户名" autocomplete="username" />
|
||||
<input type="password" id="login-password" placeholder="密码" autocomplete="current-password" />
|
||||
<button class="login-btn" onclick="doLogin()">登 录</button>
|
||||
<div class="login-error" id="login-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="chat.js?v=20260611"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,570 @@
|
||||
// Agent Chat - 所有前端逻辑
|
||||
const ASSISTANT_ID = 'simple_agent';
|
||||
const API_BASE = window.location.protocol + '//' + window.location.hostname + ':2024';
|
||||
const CHAT_SERVER = window.location.protocol + '//' + window.location.hostname + ':8765';
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const threadListDiv = document.getElementById('threadList');
|
||||
const skillListDiv = document.getElementById('skillList');
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
let currentThreadId = null, threads = [], authToken = sessionStorage.getItem('auth_token') || '';
|
||||
let abortController = null;
|
||||
const messageMap = new Map(), processedConfirms = new Set(), pendingConfirms = new Map(), existingMsgTexts = new Set();
|
||||
var pendingCharts = [], streamMsgIndex = -1; // -1=不跳过,>=0=跳过该索引之前的消息
|
||||
|
||||
// URL user identity
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var embedUser = urlParams.get('user') || 'default';
|
||||
var embedToken = urlParams.get('token') || '';
|
||||
var currentUser = embedUser || 'default';
|
||||
// 迁移旧的无前缀 localStorage 数据
|
||||
if (!localStorage.getItem(userKey('agent_threads')) && localStorage.getItem('agent_threads')) {
|
||||
var keysToMigrate = [];
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var k = localStorage.key(i);
|
||||
if (k && (k === 'agent_threads' || k === 'agent_config' || k.startsWith('msg_cache_') || k === 'last_thread')) {
|
||||
keysToMigrate.push(k);
|
||||
}
|
||||
}
|
||||
keysToMigrate.forEach(function(k) {
|
||||
localStorage.setItem(userKey(k), localStorage.getItem(k));
|
||||
localStorage.removeItem(k);
|
||||
});
|
||||
}
|
||||
if (embedUser && embedToken) {
|
||||
authToken = embedToken;
|
||||
sessionStorage.setItem('auth_token', embedToken);
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
function userKey(k) { return currentUser + '_' + k; }
|
||||
function localGet(k) { return localStorage.getItem(userKey(k)); }
|
||||
function localSet(k, v) { localStorage.setItem(userKey(k), v); }
|
||||
|
||||
// ====== UI Helpers ======
|
||||
function toggleSidebar(id) {
|
||||
var el = document.getElementById(id);
|
||||
el.classList.toggle('collapsed');
|
||||
if (id === 'sidebar') document.getElementById('toggle-sidebar-btn').textContent = el.classList.contains('collapsed') ? '\u25b6' : '\u25c0';
|
||||
else document.getElementById('toggle-skillbar-btn').textContent = el.classList.contains('collapsed') ? '\u25c0' : '\u25b6';
|
||||
}
|
||||
function escapeHtml(text) {
|
||||
var map = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
||||
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||
}
|
||||
function showError(msg) {
|
||||
addMessage('system', msg, '\u7cfb\u7edf');
|
||||
loadingIndicator.style.display = 'none';
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('send-btn').disabled = false;
|
||||
}
|
||||
|
||||
// ====== Markdown ======
|
||||
if (typeof marked !== 'undefined') {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
if (typeof hljs !== 'undefined') {
|
||||
marked.setOptions({ highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (e) {} }
|
||||
return code;
|
||||
}});
|
||||
}
|
||||
var renderer = new marked.Renderer();
|
||||
renderer.code = function(code, lang) {
|
||||
var langLabel = lang || 'code';
|
||||
var escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
var highlighted = (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) ? hljs.highlight(code, { language: lang }).value : escaped;
|
||||
return '<pre><div class="code-header"><span class="lang">' + langLabel + '</span><button class="copy-code" onclick="copyCodeBlock(this)">\u590d\u5236</button></div><code class="hljs">' + highlighted + '</code></pre>';
|
||||
};
|
||||
marked.setOptions({ renderer: renderer });
|
||||
}
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
try { return marked.parse(text); } catch (e) { return escapeHtml(text).replace(/\n/g, '<br>'); }
|
||||
}
|
||||
function copyCodeBlock(btn) {
|
||||
var code = btn.parentElement.nextElementSibling;
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code.textContent).then(function() {
|
||||
btn.textContent = '\u5df2\u590d\u5236';
|
||||
setTimeout(function() { btn.textContent = '\u590d\u5236'; }, 1500);
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Message Rendering ======
|
||||
function addMessage(role, content, label, msgId) {
|
||||
msgId = msgId || ('msg-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6));
|
||||
var div = document.createElement('div');
|
||||
div.className = 'message ' + role;
|
||||
var html = label ? '<div class="label">' + label + '</div>' : '';
|
||||
var body;
|
||||
if (role === 'agent') body = renderMarkdown(content);
|
||||
else if (role === 'tool') body = '<pre><code>' + escapeHtml(content) + '</code></pre>';
|
||||
else body = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
html += '<div class="bubble" id="' + msgId + '">' + body + '<button class="copy-btn" onclick="copyMsg(\'' + msgId + '\')">\ud83d\udccb</button></div>';
|
||||
div.innerHTML = html;
|
||||
messagesDiv.appendChild(div);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
function copyMsg(id) {
|
||||
var el = document.getElementById(id); if (!el) return;
|
||||
var clone = el.cloneNode(true); var cb = clone.querySelector('.copy-btn'); if (cb) cb.remove();
|
||||
navigator.clipboard.writeText(clone.innerText).catch(function() {});
|
||||
}
|
||||
function addMessagePlain(role, content, label) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'message ' + role;
|
||||
var html = label ? '<div class="label">' + label + '</div>' : '';
|
||||
html += '<div class="bubble">' + escapeHtml(content).replace(/\n/g, '<br>') + '</div>';
|
||||
div.innerHTML = html;
|
||||
messagesDiv.appendChild(div);
|
||||
}
|
||||
function renderHistoryMessage(msg) {
|
||||
// 写去重表,防止 stream 重放
|
||||
var dk = currentThreadId + '|' + (msg.type||'') + '|' + (msg.content||'').substring(0, 100);
|
||||
messageMap.set(dk, true);
|
||||
if (msg.type === 'human' || msg.role === 'user') addMessagePlain('user', msg.content, '\u4f60');
|
||||
else if (msg.type === 'ai') {
|
||||
var c = (msg.content || '').replace(/<think>.*?<\/think>/gs, '').trim();
|
||||
if (c) addMessage('agent', c, 'Agent');
|
||||
if (msg.tool_calls) {
|
||||
for (var i = 0; i < msg.tool_calls.length; i++) {
|
||||
var tc = msg.tool_calls[i];
|
||||
addMessagePlain('tool', tc.name + '(' + JSON.stringify(tc.args) + ')', '\u5de5\u5177\u8c03\u7528');
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'tool') {
|
||||
var tc = msg.name + ': ' + (msg.content || '');
|
||||
if ((msg.content||'').indexOf('![图表]') >= 0) {
|
||||
addMessage('agent', msg.content, msg.name || '\u5de5\u5177\u7ed3\u679c');
|
||||
} else {
|
||||
addMessagePlain('tool', tc, '\u5de5\u5177\u7ed3\u679c');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Thread Management ======
|
||||
function loadThreads() {
|
||||
var stored = localGet('agent_threads');
|
||||
threads = stored ? JSON.parse(stored) : [];
|
||||
renderThreadList();
|
||||
fetch(API_BASE + '/threads/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
var serverIds = new Set();
|
||||
data.forEach(function(t) { serverIds.add(t.thread_id);
|
||||
if (!threads.find(function(l) { return l.id === t.thread_id; })) {
|
||||
threads.unshift({ id: t.thread_id, title: '', createdAt: Date.now() });
|
||||
}
|
||||
});
|
||||
threads = threads.filter(function(t) { return serverIds.has(t.id); });
|
||||
saveThreads();
|
||||
renderThreadList();
|
||||
}
|
||||
}).catch(function() {
|
||||
// 无法连接服务器时,保留本地缓存的历史记录
|
||||
});
|
||||
}
|
||||
function saveThreads() { localSet('agent_threads', JSON.stringify(threads)); }
|
||||
function renderThreadList() {
|
||||
threadListDiv.innerHTML = '';
|
||||
threads.forEach(function(t) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'thread-item' + (t.id === currentThreadId ? ' active' : '');
|
||||
div.innerHTML = '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;" onclick="switchThread(\'' + t.id + '\')">' + escapeHtml(t.title || t.id.substring(0, 8)) + '</span><button class="del-btn" onclick="deleteThread(event,\'' + t.id + '\')">\u00d7</button>';
|
||||
threadListDiv.appendChild(div);
|
||||
});
|
||||
}
|
||||
function deleteThread(e, threadId) {
|
||||
e.stopPropagation();
|
||||
if (!confirm('\u786e\u5b9a\u5220\u9664\uff1f')) return;
|
||||
threads = threads.filter(function(t) { return t.id !== threadId; });
|
||||
saveThreads();
|
||||
if (currentThreadId === threadId) { messagesDiv.innerHTML = ''; messageMap.clear(); currentThreadId = null;
|
||||
threads.length > 0 ? switchThread(threads[0].id) : createNewChat(); }
|
||||
else renderThreadList();
|
||||
}
|
||||
async function createThread() {
|
||||
var resp = await apiFetch(API_BASE + '/threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!resp.ok) throw new Error('\u521b\u5efa\u5931\u8d25');
|
||||
return (await resp.json()).thread_id;
|
||||
}
|
||||
async function createNewChat() {
|
||||
try { var threadId = await createThread(); threads.unshift({ id: threadId, title: '', createdAt: Date.now() });
|
||||
saveThreads(); currentThreadId = threadId; renderThreadList(); messagesDiv.innerHTML = ''; messageMap.clear(); }
|
||||
catch (err) { addMessage('system', '\u521b\u5efa\u65b0\u5bf9\u8bdd\u5931\u8d25: ' + err.message, '\u7cfb\u7edf'); }
|
||||
}
|
||||
async function switchThread(threadId) {
|
||||
currentThreadId = threadId; localSet('last_thread', threadId);
|
||||
renderThreadList(); messagesDiv.innerHTML = ''; existingMsgTexts.clear(); streamMsgIndex = -1;
|
||||
// 优先从本地缓存秒出
|
||||
var cached = localGet('msg_cache_' + threadId) || localStorage.getItem('msg_cache_' + threadId);
|
||||
var hasCache = false;
|
||||
if (cached) {
|
||||
try {
|
||||
var cmsgs = JSON.parse(cached);
|
||||
if (cmsgs.length > 0) {
|
||||
for (var j = 0; j < cmsgs.length; j++) renderHistoryMessage(cmsgs[j]);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
hasCache = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!hasCache) {
|
||||
messagesDiv.innerHTML = '<div class="message system"><div class="bubble" style="display:flex;align-items:center;gap:8px;"><div class="spinner"></div><span>加载中...</span></div></div>';
|
||||
// 3秒超时后显示空状态
|
||||
setTimeout(function() {
|
||||
if (currentThreadId === threadId && messagesDiv.children.length <= 1) {
|
||||
messagesDiv.innerHTML = '<div class="message system"><div class="bubble">暂无对话内容</div></div>';
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
// 后台静默同步服务器最新数据
|
||||
apiFetch(API_BASE + '/threads/' + threadId + '/state').then(async function(resp) {
|
||||
if (!resp.ok) return;
|
||||
var data = await resp.json();
|
||||
var msgs = data && data.values ? data.values.messages || [] : [];
|
||||
if (msgs.length === 0) return;
|
||||
localSet('msg_cache_' + threadId, JSON.stringify(msgs));
|
||||
if (!hasCache || msgs.length !== JSON.parse(cached).length) {
|
||||
messagesDiv.innerHTML = ''; messageMap.clear();
|
||||
for (var i = 0; i < msgs.length; i++) renderHistoryMessage(msgs[i]);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
// ====== Confirm Flow ======
|
||||
async function checkConfirmPending(confirmId) {
|
||||
if (pendingConfirms.has(confirmId)) return pendingConfirms.get(confirmId);
|
||||
var p = apiFetch(CHAT_SERVER + '/api/confirm_info?confirm_id=' + encodeURIComponent(confirmId))
|
||||
.then(async function(r) { if (r.ok) { var d = await r.json(); return d.result === 'pending'; } return true; })
|
||||
.catch(function() { return true; });
|
||||
pendingConfirms.set(confirmId, p); return p;
|
||||
}
|
||||
async function processMessage(msg) {
|
||||
if (msg.type === 'human' || msg.role === 'user') return;
|
||||
var content = msg.content || '';
|
||||
var confirmMatch = content.match(/\[NEED_USER_CONFIRM_FILE\|([a-f0-9\-]+)\]/);
|
||||
if (confirmMatch) { var cid = confirmMatch[1]; if (processedConfirms.has(cid)) return; processedConfirms.add(cid);
|
||||
var isPending = await checkConfirmPending(cid); if (!isPending) return; showConfirmPopupForWrite(cid); throw { type: 'CONFIRM_PENDING' }; }
|
||||
// 内容去重:用内容前100字符+thread做key,防止LangGraph重放历史消息
|
||||
var dedupKey = currentThreadId + '|' + (msg.type||'') + '|' + content.substring(0, 100);
|
||||
if (messageMap.has(dedupKey)) return; messageMap.set(dedupKey, true);
|
||||
var txt = (msg.content || '').substring(0, 100);
|
||||
if (existingMsgTexts.has(txt)) return;
|
||||
if (msg.type === 'ai') {
|
||||
var c = (msg.content || '').replace(/<think>.*?<\/think>/gs, '').trim();
|
||||
// 如果有待展示的图表,插入到总结之后
|
||||
if (c && pendingCharts.length > 0) {
|
||||
var parts = c.split(/(?<=[。!?\n])/); // 按句号/感叹号/疑问号/换行分割
|
||||
var summary = parts.slice(0, Math.min(2, parts.length)).join('').trim();
|
||||
var rest = parts.slice(Math.min(2, parts.length)).join('').trim();
|
||||
if (summary) addMessage('agent', summary, 'Agent');
|
||||
pendingCharts.forEach(function(chart) { addMessage('agent', chart, '\u56fe\u8868'); });
|
||||
pendingCharts = [];
|
||||
if (rest) addMessage('agent', rest, 'Agent');
|
||||
} else if (c) {
|
||||
addMessage('agent', c, 'Agent');
|
||||
}
|
||||
if (msg.tool_calls) { for (var i = 0; i < msg.tool_calls.length; i++) { var tc = msg.tool_calls[i]; addMessage('tool', tc.name + '(' + JSON.stringify(tc.args) + ')', '\u5de5\u5177\u8c03\u7528'); } }
|
||||
} else if (msg.type === 'tool') {
|
||||
var tc = msg.content || '';
|
||||
if (tc.indexOf('![图表]') >= 0) {
|
||||
// 图表不立即渲染,暂存队列等待 Agent 回复时插入
|
||||
var imgs = tc.match(/!\[.*?\]\(\/workspace\/.+?\.svg\)/g);
|
||||
if (imgs) imgs.forEach(function(img) { pendingCharts.push(img); });
|
||||
} else {
|
||||
addMessage('tool', msg.name + ': ' + tc, '\u5de5\u5177\u7ed3\u679c');
|
||||
}
|
||||
}
|
||||
}
|
||||
function showConfirmPopupForWrite(confirmId) {
|
||||
var popup = document.createElement('div'); popup.id = 'confirm-popup'; popup.className = 'confirm-popup';
|
||||
popup.innerHTML = '<div class="confirm-box"><div class="confirm-text">\u6587\u4ef6\u4e0d\u5728\u5b89\u5168\u533a\u57df\u5185\uff0c\u662f\u5426\u5199\u5165\uff1f</div><div class="confirm-buttons"><button class="btn-approve">\u540c\u610f\u5199\u5165</button><button class="btn-reject">\u62d2\u7edd</button></div></div>';
|
||||
document.body.appendChild(popup);
|
||||
popup.querySelector('.btn-approve').addEventListener('click', async function() { popup.remove();
|
||||
try { var r = await apiFetch(CHAT_SERVER + '/api/confirm_write', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({confirm_id:confirmId, choice:'approve'}) });
|
||||
var d = await r.json(); addMessage('system', d.success ? '\u2705 \u5df2\u5199\u5165: ' + (d.path||'') : '\u5199\u5165\u5931\u8d25: ' + (d.error||d.message), '\u7cfb\u7edf'); }
|
||||
catch(e) { addMessage('system', '\u8bf7\u6c42\u5931\u8d25: ' + e.message, '\u7cfb\u7edf'); }
|
||||
});
|
||||
popup.querySelector('.btn-reject').addEventListener('click', async function() { popup.remove();
|
||||
try { await apiFetch(CHAT_SERVER + '/api/confirm_write', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({confirm_id:confirmId, choice:'reject'}) }); } catch(e){}
|
||||
addMessage('system', '\u5df2\u62d2\u7edd\u5199\u5165', '\u7cfb\u7edf');
|
||||
});
|
||||
document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') popup.remove(); }, { once: true });
|
||||
}
|
||||
|
||||
// ====== Stream Processing ======
|
||||
async function processStream(response) {
|
||||
var reader = response.body.getReader(); var decoder = new TextDecoder(); var buffer = '', first = false;
|
||||
try { while (true) { var result = await reader.read(); if (result.done) break;
|
||||
buffer += decoder.decode(result.value, { stream: true }); var lines = buffer.split('\n'); buffer = lines.pop();
|
||||
for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith('data: ')) continue;
|
||||
try { var data = JSON.parse(line.slice(6));
|
||||
if (data.__interrupt__) { showInterruptPopup(data.__interrupt__); reader.cancel(); loadingIndicator.style.display='none'; stopUI(); return; }
|
||||
if (data.messages) { if (!first) { loadingIndicator.style.display='none'; first=true; }
|
||||
for (var j=0;j<data.messages.length;j++) { try { await processMessage(data.messages[j]); } catch(e) { if (e&&e.type==='CONFIRM_PENDING') { reader.cancel(); loadingIndicator.style.display='none'; stopUI(); return; } throw e; } }
|
||||
}
|
||||
} catch(e) { if (e&&e.type==='CONFIRM_PENDING') { reader.cancel(); loadingIndicator.style.display='none'; stopUI(); return; } }
|
||||
}
|
||||
}
|
||||
} catch(e) { if (e.name!=='AbortError') console.error('Stream error:',e); }
|
||||
loadingIndicator.style.display='none'; stopUI();
|
||||
// 如果还有未展示的图表,直接显示
|
||||
if (pendingCharts.length > 0) {
|
||||
pendingCharts.forEach(function(chart) { addMessage('agent', chart, '\u56fe\u8868'); });
|
||||
pendingCharts = [];
|
||||
}
|
||||
// 缓存最新消息到本地
|
||||
if (currentThreadId) {
|
||||
var allBubbles = messagesDiv.querySelectorAll('.message');
|
||||
var cachedMsgs = [];
|
||||
allBubbles.forEach(function(m) {
|
||||
if (m.classList.contains('user')) cachedMsgs.push({type:'human',role:'user',content:m.querySelector('.bubble').innerText.trim()});
|
||||
else if (m.classList.contains('agent')) cachedMsgs.push({type:'ai',role:'assistant',content:m.querySelector('.bubble').innerText.trim()});
|
||||
else if (m.classList.contains('tool')) cachedMsgs.push({type:'tool',role:'tool',name:'',content:m.querySelector('.bubble').innerText.trim()});
|
||||
});
|
||||
localSet('msg_cache_' + currentThreadId, JSON.stringify(cachedMsgs));
|
||||
}
|
||||
}
|
||||
function showInterruptPopup(data) {
|
||||
var popup = document.createElement('div'); popup.id='confirm-popup'; popup.className='confirm-popup';
|
||||
popup.innerHTML = '<div class="confirm-box"><div class="confirm-text">' + escapeHtml(data.message||'\u662f\u5426\u7ee7\u7eed\uff1f') + '</div><div class="confirm-buttons"><button class="btn-approve">\u540c\u610f</button><button class="btn-reject">\u62d2\u7edd</button></div><div class="timeout-note">30\u5206\u949f\u540e\u8d85\u65f6</div></div>';
|
||||
document.body.appendChild(popup);
|
||||
popup.querySelectorAll('button').forEach(function(b) { b.addEventListener('click', async function() { popup.remove(); await resumeWithCommand(data, b.classList.contains('btn-approve')?'approve':'reject'); }); });
|
||||
document.addEventListener('keydown', function esc(e) { if (e.key==='Escape') popup.remove(); }, { once:true });
|
||||
}
|
||||
async function resumeWithCommand(data, val) {
|
||||
var payload = { assistant_id:ASSISTANT_ID, thread_id:currentThreadId, input:null, command:{resume:{__interrupt_id__:data.__interrupt_id__,value:val}} };
|
||||
startUI(); try { var resp = await apiFetch(API_BASE+'/threads/'+currentThreadId+'/runs/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); await processStream(resp); } catch(e) { showError('\u6062\u590d\u5931\u8d25: '+e.message); }
|
||||
}
|
||||
function stopGeneration() { if (abortController) { abortController.abort(); abortController=null; } stopUI(); }
|
||||
function startUI() { loadingIndicator.style.display='flex'; document.getElementById('stop-btn').style.display='inline-block'; document.getElementById('send-btn').disabled=true; }
|
||||
function stopUI() { loadingIndicator.style.display='none'; document.getElementById('stop-btn').style.display='none'; document.getElementById('send-btn').disabled=false; }
|
||||
|
||||
// ====== Send Message ======
|
||||
async function sendMessage() {
|
||||
if (!currentThreadId) { try { currentThreadId=await createThread(); threads.unshift({id:currentThreadId,title:'',createdAt:Date.now()}); saveThreads(); renderThreadList(); messagesDiv.innerHTML=''; messageMap.clear(); } catch(err) { addMessage('system','\u521b\u5efa\u5bf9\u8bdd\u5931\u8d25','\u7cfb\u7edf'); return; } }
|
||||
var input = document.getElementById('user-input'); var text = input.value.trim(); if (!text) return;
|
||||
addMessage('user', text, '\u4f60'); input.value='';
|
||||
pendingCharts = []; // 新消息清空图表队列
|
||||
var ct = threads.find(function(t){return t.id===currentThreadId;}); if (ct&&!ct.title){ct.title=text.substring(0,30);saveThreads();renderThreadList();}
|
||||
existingMsgTexts.clear();
|
||||
messagesDiv.querySelectorAll('.bubble').forEach(function(b){ existingMsgTexts.add(b.innerText.trim().substring(0,100)); });
|
||||
startUI(); abortController = new AbortController();
|
||||
var cfg = JSON.parse(localGet('agent_config')||'{}'); var config={configurable:{}}; if(cfg.temperature) config.configurable.temperature=cfg.temperature;
|
||||
try {
|
||||
var resp = await fetch(API_BASE+'/threads/'+currentThreadId+'/runs/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({assistant_id:ASSISTANT_ID,thread_id:currentThreadId,config:config,input:{messages:[{role:'user',content:text}]}}),signal:abortController.signal});
|
||||
await processStream(resp);
|
||||
} catch(e) { if (e.name==='AbortError') addMessage('system','\u5df2\u505c\u6b62\u751f\u6210','\u7cfb\u7edf'); else showError('\u8bf7\u6c42\u5931\u8d25: '+e.message); }
|
||||
abortController=null;
|
||||
}
|
||||
|
||||
// ====== Export ======
|
||||
function exportChat() {
|
||||
var msgs=[]; messagesDiv.querySelectorAll('.message').forEach(function(m){var bubble=m.querySelector('.bubble');if(!bubble)return;var clone=bubble.cloneNode(true);var cb=clone.querySelector('.copy-btn');if(cb)cb.remove();var role=m.classList.contains('user')?'\u7528\u6237':m.classList.contains('agent')?'Agent':m.classList.contains('tool')?'\u5de5\u5177':'\u7cfb\u7edf';msgs.push('## '+role+'\n\n'+clone.innerText+'\n');});
|
||||
var text='# Agent \u5bf9\u8bdd\u8bb0\u5f55\n\n'+new Date().toLocaleString()+'\n\n---\n\n'+msgs.join('\n---\n\n');
|
||||
var blob=new Blob([text],{type:'text/markdown;charset=utf-8'}); var a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='chat-'+new Date().toISOString().slice(0,10)+'.md'; a.click(); URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
// ====== File Upload ======
|
||||
var dropZone=document.getElementById('drop-zone');
|
||||
['dragenter','dragover'].forEach(function(e){document.addEventListener(e,function(ev){ev.preventDefault();dropZone.style.display='block';dropZone.classList.add('active');});});
|
||||
['dragleave','drop'].forEach(function(e){document.addEventListener(e,function(ev){ev.preventDefault();if(e==='dragleave'){dropZone.style.display='none';dropZone.classList.remove('active');}});});
|
||||
dropZone.addEventListener('drop',function(e){e.preventDefault();dropZone.style.display='none';dropZone.classList.remove('active');uploadFiles(e.dataTransfer.files);});
|
||||
document.getElementById('file-input').addEventListener('change',function(){uploadFiles(this.files);this.value='';});
|
||||
async function uploadFiles(files){for(var i=0;i<files.length;i++){var f=files[i];var fd=new FormData();fd.append('file',f);var type=f.name.match(/\.(txt|log|md|json|csv|yml|yaml|xml|html|css|js|py|java|cpp|c|h)$/i)?'knowledge':'memory';try{var r=await apiFetch(CHAT_SERVER+'/upload?type='+type,{method:'POST',body:fd});if(r.ok)addMessage('system','\u5df2\u4e0a\u4f20: '+f.name,'\u7cfb\u7edf');}catch(e){addMessage('system','\u4e0a\u4f20\u5931\u8d25: '+f.name,'\u7cfb\u7edf');}}}
|
||||
|
||||
// ====== Shortcuts ======
|
||||
document.addEventListener('keydown',function(e){if(e.target.tagName==='INPUT'&&e.target.id==='user-input'&&e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}else if(e.ctrlKey&&e.key==='n'){e.preventDefault();createNewChat();}else if(e.ctrlKey&&e.key==='e'){e.preventDefault();exportChat();}else if(e.key==='Escape'){var popup=document.getElementById('confirm-popup');if(popup){popup.remove();stopUI();}}});
|
||||
|
||||
// ====== API + Auth ======
|
||||
async function apiFetch(url, options) {
|
||||
options = options || {};
|
||||
if (authToken) { options.headers = options.headers || {}; options.headers['Authorization'] = 'Bearer ' + authToken; options.headers['X-User'] = currentUser; }
|
||||
return fetch(url, options);
|
||||
}
|
||||
function checkLogin() {
|
||||
if (authToken) {
|
||||
apiFetch(CHAT_SERVER + '/skills/list').then(function(r) {
|
||||
if (r.ok) {
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
loadThreads();
|
||||
var lt = localGet('last_thread');
|
||||
if (lt && threads.find(function(t) { return t.id === lt; })) switchThread(lt);
|
||||
else if (threads.length === 0) createNewChat();
|
||||
fetchSkills();
|
||||
} else { authToken = ''; sessionStorage.removeItem('auth_token'); }
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
async function doLogin() {
|
||||
var username = document.getElementById('login-username').value.trim();
|
||||
var password = document.getElementById('login-password').value;
|
||||
var errorEl = document.getElementById('login-error');
|
||||
if (!username || !password) { errorEl.textContent = '\u8bf7\u8f93\u5165\u7528\u6237\u540d\u548c\u5bc6\u7801'; return; }
|
||||
try {
|
||||
var resp = await fetch(CHAT_SERVER + '/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, password: password }) });
|
||||
var data = await resp.json();
|
||||
if (resp.ok && data.token) {
|
||||
authToken = data.token;
|
||||
sessionStorage.setItem('auth_token', data.token);
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
loadThreads();
|
||||
if (threads.length === 0) createNewChat();
|
||||
fetchSkills();
|
||||
} else { errorEl.textContent = data.error || '\u767b\u5f55\u5931\u8d25'; }
|
||||
} catch (e) { errorEl.textContent = '\u8fde\u63a5\u5931\u8d25: ' + e.message; }
|
||||
}
|
||||
|
||||
// ====== Skills ======
|
||||
async function fetchSkills() {
|
||||
try { var results = await Promise.all([apiFetch(CHAT_SERVER+'/skills/list'), apiFetch(CHAT_SERVER+'/skills/trash')]); var normal = await results[0].json(); var trash = []; if (results[1].ok) trash = await results[1].json(); renderSkillList(normal, trash); }
|
||||
catch (e) { skillListDiv.innerHTML = '<div style="padding:12px;color:#d666;">\u83b7\u53d6\u5931\u8d25</div>'; }
|
||||
}
|
||||
function renderSkillList(normal, trash) {
|
||||
var html = '<div style="font-weight:bold;padding:8px 12px;border-bottom:1px solid #ddd;">\u5df2\u5b89\u88c5\u6280\u80fd</div>';
|
||||
if (!normal || !normal.length) html += '<div style="padding:12px;color:#666;">\u6682\u65e0</div>';
|
||||
else normal.forEach(function(s) { html += '<div class="skill-item"><div class="skill-info"><div class="skill-name">' + escapeHtml(s.name) + '</div><div class="skill-desc">' + escapeHtml(s.description || '') + '</div></div><button class="btn-delete-skill" data-skill-name="' + escapeHtml(s.name) + '">\u5220\u9664</button></div>'; });
|
||||
html += '<div style="font-weight:bold;padding:8px 12px;border-bottom:1px solid #ddd;margin-top:10px;">\u5783\u573e\u6876</div>';
|
||||
if (!trash || !trash.length) html += '<div style="padding:12px;color:#666;">\u7a7a</div>';
|
||||
else trash.forEach(function(s) { html += '<div class="skill-item" style="opacity:0.7;"><div class="skill-name">' + escapeHtml(s.name) + ' <span style="font-size:11px;color:#999;">(\u5df2\u5220\u9664)</span></div><button class="btn-recover-skill" data-skill-name="' + escapeHtml(s.name) + '">\u6062\u590d</button><button class="btn-delete-permanent" data-skill-name="' + escapeHtml(s.name) + '">\u6c38\u4e45\u5220\u9664</button></div>'; });
|
||||
skillListDiv.innerHTML = html;
|
||||
skillListDiv.querySelectorAll('.btn-delete-skill').forEach(function(b) { b.addEventListener('click', function() { deleteSkill(b.dataset.skillName); }); });
|
||||
skillListDiv.querySelectorAll('.btn-recover-skill').forEach(function(b) { b.addEventListener('click', function() { recoverSkill(b.dataset.skillName); }); });
|
||||
skillListDiv.querySelectorAll('.btn-delete-permanent').forEach(function(b) { b.addEventListener('click', function() { permanentDeleteSkill(b.dataset.skillName); }); });
|
||||
}
|
||||
async function reloadSkills() { try { var r = await apiFetch(CHAT_SERVER+'/skills/reload',{method:'POST'}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '\u7cfb\u7edf'); fetchSkills(); } } catch (e) { addMessage('system', '\u70ed\u66f4\u65b0\u5931\u8d25', '\u7cfb\u7edf'); } }
|
||||
async function deleteSkill(name) { if (!confirm('\u5c06 \"' + name + '\" \u79fb\u81f3\u5783\u573e\u6876\uff1f')) return;
|
||||
try { var r = await apiFetch(CHAT_SERVER+'/skills/delete?name='+encodeURIComponent(name),{method:'POST'}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '\u7cfb\u7edf'); fetchSkills(); } } catch (e) { addMessage('system', '\u5220\u9664\u5931\u8d25', '\u7cfb\u7edf'); } }
|
||||
async function recoverSkill(name) { try { var r = await apiFetch(CHAT_SERVER+'/skills/recover?name='+encodeURIComponent(name),{method:'POST'}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '\u7cfb\u7edf'); fetchSkills(); } } catch (e) { addMessage('system', '\u6062\u590d\u5931\u8d25', '\u7cfb\u7edf'); } }
|
||||
async function permanentDeleteSkill(name) { if (!confirm('\u6c38\u4e45\u5220\u9664 \"' + name + '\"\uff1f\u4e0d\u53ef\u6062\u590d\uff01')) return;
|
||||
try { var r = await apiFetch(CHAT_SERVER+'/skills/permanent_delete?name='+encodeURIComponent(name),{method:'POST'}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '\u7cfb\u7edf'); fetchSkills(); } } catch (e) { addMessage('system', '\u5220\u9664\u5931\u8d25', '\u7cfb\u7edf'); } }
|
||||
async function uploadSkillZip(input) { var file = input.files[0]; if (!file) return; var fd = new FormData(); fd.append('file', file);
|
||||
try { var r = await apiFetch(CHAT_SERVER+'/skills/upload',{method:'POST',body:fd}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '\u7cfb\u7edf'); fetchSkills(); } } catch (e) { addMessage('system', '\u4e0a\u4f20\u5931\u8d25', '\u7cfb\u7edf'); } input.value = ''; }
|
||||
|
||||
// ====== Settings ======
|
||||
function toggleSettings() {
|
||||
var p = document.getElementById('settings-panel'); p.classList.toggle('open');
|
||||
if (p.classList.contains('open')) {
|
||||
var cfg = JSON.parse(localGet('agent_config') || '{}');
|
||||
var mSel = document.getElementById('cfg-m-model'); ensureOption(mSel, cfg.m_model);
|
||||
document.getElementById('cfg-m-url').value = cfg.m_url || ''; document.getElementById('cfg-m-key').value = cfg.m_key || '';
|
||||
document.getElementById('cfg-m-temp').value = cfg.temperature || 0.7; document.getElementById('cfg-m-temp-val').textContent = cfg.temperature || 0.7;
|
||||
var wSel = document.getElementById('cfg-w-model'); ensureOption(wSel, cfg.w_model);
|
||||
document.getElementById('cfg-w-url').value = cfg.w_url || ''; document.getElementById('cfg-w-key').value = cfg.w_key || '';
|
||||
var mcpSel = document.getElementById('cfg-mcp-model'); ensureOption(mcpSel, cfg.mcp_model);
|
||||
document.getElementById('cfg-mcp-url').value = cfg.mcp_url || ''; document.getElementById('cfg-mcp-key').value = cfg.mcp_key || '';
|
||||
document.getElementById('cfg-m-maxtk').value = cfg.max_tokens || 256000;
|
||||
}
|
||||
}
|
||||
function ensureOption(inp, val) {
|
||||
if (!val || !inp) return;
|
||||
if (!inp.value) inp.value = val;
|
||||
}
|
||||
document.getElementById('cfg-m-temp').addEventListener('input', function() { document.getElementById('cfg-m-temp-val').textContent = this.value; });
|
||||
function saveSettings() {
|
||||
var cfg = { temperature: parseFloat(document.getElementById('cfg-m-temp').value), m_model: document.getElementById('cfg-m-model').value.trim(), m_url: document.getElementById('cfg-m-url').value.trim(), m_key: document.getElementById('cfg-m-key').value.trim(), w_model: document.getElementById('cfg-w-model').value.trim(), w_url: document.getElementById('cfg-w-url').value.trim(), w_key: document.getElementById('cfg-w-key').value.trim(), mcp_model: document.getElementById('cfg-mcp-model').value.trim(), mcp_url: document.getElementById('cfg-mcp-url').value.trim(), mcp_key: document.getElementById('cfg-mcp-key').value.trim() };
|
||||
Object.keys(cfg).forEach(function(k) { if (!cfg[k] && cfg[k] !== 0) delete cfg[k]; });
|
||||
localSet('agent_config', JSON.stringify(cfg));
|
||||
document.getElementById('settings-panel').classList.remove('open');
|
||||
// 写 .env 触发文件监听热更新
|
||||
apiFetch(CHAT_SERVER + '/api/save_config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }).then(function(r) { return r.json(); }).then(function(d) {
|
||||
addMessage('system', d.success ? '\u914d\u7f6e\u5df2\u4fdd\u5b58\u5e76\u70ed\u66f4\u65b0\u751f\u6548' : (d.message || d.error || '\u4fdd\u5b58\u5931\u8d25'), '\u7cfb\u7edf');
|
||||
}).catch(function(e) { addMessage('system', '\u4fdd\u5b58\u5931\u8d25: ' + e.message, '\u7cfb\u7edf'); });
|
||||
}
|
||||
function applyAndRestart() { saveSettings(); if (!confirm('\u5c06\u628a\u914d\u7f6e\u5199\u5165.env\u5e76\u91cd\u542f\u670d\u52a1\uff0c\u786e\u8ba4\uff1f')) return;
|
||||
var cfg = JSON.parse(localGet('agent_config') || '{}'); addMessage('system', '\u6b63\u5728\u5e94\u7528\u914d\u7f6e\u5e76\u91cd\u542f...', '\u7cfb\u7edf');
|
||||
apiFetch(CHAT_SERVER + '/api/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }).then(function(r) { return r.json(); }).then(function(d) { addMessage('system', d.message || '\u91cd\u542f\u4e2d', '\u7cfb\u7edf'); setTimeout(function() { location.reload(); }, 5000); }).catch(function(e) { addMessage('system', '\u91cd\u542f\u8bf7\u6c42\u5931\u8d25: ' + e.message, '\u7cfb\u7edf'); });
|
||||
}
|
||||
function fetchModels(mid, did, uid, kid) {
|
||||
var url = document.getElementById(uid).value.trim(); var key = document.getElementById(kid).value.trim();
|
||||
if (!url || !key) { addMessage('system', '\u8bf7\u5148\u586b\u5199API URL\u548cKey', '\u7cfb\u7edf'); return; }
|
||||
url = url.replace(/\/+$/, '') + '/models'; addMessage('system', '\u6b63\u5728\u83b7\u53d6\u6a21\u578b\u5217\u8868...', '\u7cfb\u7edf');
|
||||
fetch(url, { headers: { 'Authorization': 'Bearer ' + key } }).then(function(r) {
|
||||
if (!r.ok) { addMessage('system', 'HTTP ' + r.status + ' \u8bf7\u624b\u52a8\u8f93\u5165', '\u7cfb\u7edf'); return; }
|
||||
return r.text();
|
||||
}).then(function(t) {
|
||||
if (!t) return;
|
||||
try { var d = JSON.parse(t); var models = d.data || d.models || []; if (!models.length) { addMessage('system', '\u672a\u83b7\u53d6\u5230\u6a21\u578b', '\u7cfb\u7edf'); return; }
|
||||
var names = models.map(function(m) { return m.id; }).filter(function(v,i,a){ return a.indexOf(v)===i; }).sort();
|
||||
var dl = document.getElementById(did);
|
||||
if (dl) { dl.innerHTML = ''; names.forEach(function(n) { var o = document.createElement('option'); o.value = n; dl.appendChild(o); }); }
|
||||
var inp = document.getElementById(mid);
|
||||
if (!inp.value && names.length > 0) inp.value = names[0];
|
||||
inp.placeholder = names.length + ' 个模型可选,点击输入框查看';
|
||||
addMessage('system', '\u5df2\u52a0\u8f7d ' + names.length + ' \u4e2a\u6a21\u578b\uff0c\u70b9\u51fb\u8f93\u5165\u6846\u67e5\u770b', '\u7cfb\u7edf');
|
||||
} catch (e) { addMessage('system', '\u54cd\u5e94\u975eJSON: ' + t.substring(0, 80), '\u7cfb\u7edf'); }
|
||||
}).catch(function(e) { addMessage('system', '\u83b7\u53d6\u5931\u8d25: ' + e.message, '\u7cfb\u7edf'); });
|
||||
}
|
||||
function toggleKey(inputId, btn) { var inp = document.getElementById(inputId);
|
||||
if (inp.type === 'password') { var raw = inp.value || ''; var masked = raw.length > 8 ? raw.substring(0, 4) + '****' + raw.substring(raw.length - 4) : raw; inp.type = 'text'; inp.value = masked; inp.dataset.raw = raw; btn.textContent = '\u{1f648}'; }
|
||||
else { inp.type = 'password'; inp.value = inp.dataset.raw || inp.value; btn.textContent = '\u{1f441}'; }
|
||||
}
|
||||
|
||||
// ====== Agent Test ======
|
||||
async function testAgent(type) { saveSettings(); var label = type === 'main' ? '\u4e3bAgent' : type === 'writer' ? 'WriterAgent' : 'MCPAgent';
|
||||
var testMsg = type === 'main' ? 'Hi, reply OK' : type === 'writer' ? 'Use utc_now to get time' : 'Use utc_now to get time';
|
||||
addMessage('system', 'Testing ' + label + '...', '\u7cfb\u7edf');
|
||||
try { var r = await fetch(API_BASE + '/threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!r.ok) { addMessage('system', '\u274c ' + label + ': Cannot create thread HTTP ' + r.status, '\u7cfb\u7edf'); return; }
|
||||
var d = await r.json(); var tid = d.thread_id;
|
||||
var r2 = await fetch(API_BASE + '/threads/' + tid + '/runs/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assistant_id: ASSISTANT_ID, thread_id: tid, input: { messages: [{ role: 'user', content: testMsg }] } }) });
|
||||
if (!r2.ok) { addMessage('system', '\u274c ' + label + ': Stream failed HTTP ' + r2.status, '\u7cfb\u7edf'); return; }
|
||||
var reader = r2.body.getReader(), decoder = new TextDecoder(), buf = '', found = false;
|
||||
var timeout = setTimeout(function() { if (!found) addMessage('system', '\u26a0\ufe0f ' + label + ': Timeout, API reachable', '\u7cfb\u7edf'); }, 30000);
|
||||
try { while (true) { var chunk = await reader.read(); if (chunk.done) break; buf += decoder.decode(chunk.value, { stream: true });
|
||||
var lines = buf.split('\n'); buf = lines.pop();
|
||||
for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith('data: ')) continue;
|
||||
try { var msgData = JSON.parse(line.slice(6)); if (msgData.messages) { for (var j = 0; j < msgData.messages.length; j++) { var msg = msgData.messages[j]; if (msg.content && msg.content.trim()) { found = true; clearTimeout(timeout); reader.cancel(); addMessage('system', '\u2705 ' + label + ': Connected, model responded', '\u7cfb\u7edf'); return; } } } } catch (e) {} }
|
||||
}
|
||||
} catch (e) { if (!found) { clearTimeout(timeout); addMessage('system', '\u274c ' + label + ': Read error - ' + e.message, '\u7cfb\u7edf'); } }
|
||||
if (!found) { clearTimeout(timeout); addMessage('system', '\u26a0\ufe0f ' + label + ': No response, API reachable', '\u7cfb\u7edf'); }
|
||||
} catch (e) { addMessage('system', '\u274c ' + label + ': ' + e.message, '\u7cfb\u7edf'); }
|
||||
}
|
||||
|
||||
// ====== Token Display ======
|
||||
function estimateTokens(text) { var cn = (text.match(/[\u4e00-\u9fff]/g) || []).length; var en = text.length - cn; return Math.ceil(cn * 0.6 + en * 0.25); }
|
||||
function updateTokenDisplay() { var total = 0; messagesDiv.querySelectorAll('.message .bubble').forEach(function(b) { total += estimateTokens(b.innerText || ''); });
|
||||
var cfg = JSON.parse(localGet('agent_config') || '{}'); var max = cfg.max_tokens || 256000; var el = document.getElementById('token-info');
|
||||
if (max > 0) { el.textContent = '\u5df2\u7528 ~' + total + ' / ' + max + ' tokens (' + Math.round(total / max * 100) + '%)'; el.style.color = total > max * 0.8 ? '#ef4444' : 'var(--text-secondary)'; }
|
||||
else { el.textContent = '~' + total + ' tokens'; }
|
||||
}
|
||||
setInterval(updateTokenDisplay, 3000);
|
||||
|
||||
// ====== Model Selector ======
|
||||
function showModelOptions(inpId, dlId) {
|
||||
var dl = document.getElementById(dlId);
|
||||
if (!dl || dl.options.length === 0) return;
|
||||
// 移除已有面板
|
||||
var old = document.getElementById('model-options-panel');
|
||||
if (old) old.remove();
|
||||
var inp = document.getElementById(inpId);
|
||||
var rect = inp.getBoundingClientRect();
|
||||
var panel = document.createElement('div');
|
||||
panel.id = 'model-options-panel';
|
||||
panel.style.cssText = 'position:fixed;z-index:99999;background:white;border:1px solid #ccc;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.15);max-height:200px;overflow-y:auto;min-width:200px';
|
||||
panel.style.left = rect.left + 'px';
|
||||
panel.style.top = (rect.bottom + 4) + 'px';
|
||||
panel.style.width = Math.max(rect.width, 200) + 'px';
|
||||
for (var i = 0; i < dl.options.length; i++) {
|
||||
var opt = dl.options[i];
|
||||
var item = document.createElement('div');
|
||||
item.textContent = opt.value;
|
||||
item.style.cssText = 'padding:6px 10px;cursor:pointer;font-size:12px;color:#333';
|
||||
item.onmouseover = function() { this.style.background = '#e8edff'; };
|
||||
item.onmouseout = function() { this.style.background = ''; };
|
||||
item.onclick = (function(v) { return function() { inp.value = v; panel.remove(); }; })(opt.value);
|
||||
panel.appendChild(item);
|
||||
}
|
||||
document.body.appendChild(panel);
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', function rm() { panel.remove(); document.removeEventListener('click', rm); }, {once: true});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
// ====== Init ======
|
||||
checkLogin();
|
||||
document.getElementById('login-password').addEventListener('keydown', function(e) { if (e.key === 'Enter') doLogin(); });
|
||||
@@ -0,0 +1,739 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>MCP 查询</title>
|
||||
<script src="lib/marked.min.js"></script>
|
||||
<script src="lib/highlight.min.js"></script>
|
||||
<style>
|
||||
:root { --bg: #f0f2f5; --chat-bg: #ffffff; --accent: #4f6ef7; --accent-dark: #3b54d4; --text: #1a1a2e; --text-secondary: #6b7280; --border: #e5e7eb; --sidebar-w: 220px; --skillbar-w: 240px; --radius: 12px; --radius-sm: 8px; --shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); display:flex; height:100vh; }
|
||||
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:#d1d5db; border-radius:3px; }
|
||||
.app-container { display:flex; width:100%; height:100vh; background:var(--chat-bg); overflow:hidden; }
|
||||
.sidebar { width:var(--sidebar-w); min-width:var(--sidebar-w); max-width:var(--sidebar-w); background:#f8f9fb; border-right:1px solid var(--border); display:flex; flex-direction:column; transition:width 0.25s ease; overflow:hidden; flex-shrink:0; }
|
||||
.sidebar.collapsed { width:0; min-width:0; max-width:0; border-right:none; }
|
||||
.sidebar-header { padding:16px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); }
|
||||
.sidebar-header span { font-weight:600; font-size:15px; color:var(--text); }
|
||||
.new-chat-btn { background:var(--accent); color:white; border:none; padding:7px 14px; border-radius:var(--radius-sm); cursor:pointer; font-size:13px; font-weight:500; }
|
||||
.new-chat-btn:hover { background:var(--accent-dark); }
|
||||
.thread-list { flex:1; overflow-y:auto; padding:6px; }
|
||||
.thread-item { padding:10px 12px; border-radius:var(--radius-sm); cursor:pointer; font-size:13px; display:flex; align-items:center; justify-content:space-between; margin:2px 0; color:var(--text-secondary); }
|
||||
.thread-item:hover { background:#eef0f5; color:var(--text); }
|
||||
.thread-item.active { background:#e8edff; color:var(--accent); font-weight:500; }
|
||||
.chat-container { flex:1; min-width:0; display:flex; flex-direction:column; background:var(--chat-bg); }
|
||||
.chat-header { padding:14px 20px; background:linear-gradient(135deg,var(--accent) 0%,#6366f1 100%); color:white; font-size:16px; font-weight:600; display:flex; align-items:center; gap:8px; flex-shrink:0; }
|
||||
.chat-header .title { flex:1; }
|
||||
.toggle-btn { background:rgba(255,255,255,0.15); border:none; color:white; padding:6px 10px; border-radius:6px; cursor:pointer; font-size:14px; }
|
||||
.toggle-btn:hover { background:rgba(255,255,255,0.25); }
|
||||
.export-btn { background:rgba(255,255,255,0.15); border:none; color:white; cursor:pointer; font-size:13px; padding:6px 10px; border-radius:6px; }
|
||||
.export-btn:hover,.settings-btn:hover { background:rgba(255,255,255,0.25); }
|
||||
.settings-btn { background:rgba(255,255,255,0.15); border:none; color:white; cursor:pointer; font-size:16px; padding:6px 10px; border-radius:6px; }
|
||||
.messages { flex:1; overflow-y:auto; padding:20px 24px; display:flex; flex-direction:column; gap:16px; }
|
||||
.message { display:flex; flex-direction:column; max-width:85%; animation:fadeIn 0.2s ease; }
|
||||
@keyframes fadeIn { from{opacity:0;transform:translateY(4px);} to{opacity:1;transform:translateY(0);} }
|
||||
.message.user { align-self:flex-end; }
|
||||
.message.agent,.message.tool,.message.system { align-self:flex-start; }
|
||||
.message .bubble { padding:12px 16px; border-radius:var(--radius); word-break:break-word; overflow-wrap:break-word; white-space:pre-wrap; line-height:1.7; font-size:14.5px; position:relative; box-shadow:var(--shadow); }
|
||||
.user .bubble { background:linear-gradient(135deg,#e8edff 0%,#dce4ff 100%); color:var(--text); border-bottom-right-radius:4px; }
|
||||
.agent .bubble { background:#fff; color:var(--text); border:1px solid #f0f0f0; border-bottom-left-radius:4px; }
|
||||
.tool .bubble { background:#fffdf5; color:#8b6914; font-size:13px; border:1px solid #fef3c7; }
|
||||
.system .bubble { background:#f9fafb; color:var(--text-secondary); font-size:13px; border:1px solid #f3f4f6; }
|
||||
.message .label { font-size:11px; margin-bottom:4px; color:var(--text-secondary); padding-left:4px; font-weight:500; }
|
||||
.agent .bubble h1,.agent .bubble h2,.agent .bubble h3 { margin:16px 0 8px; font-weight:700; color:var(--text); }
|
||||
.agent .bubble h1 { font-size:1.3em; border-bottom:1px solid #eee; padding-bottom:6px; }
|
||||
.agent .bubble h2 { font-size:1.15em; } .agent .bubble h3 { font-size:1.05em; }
|
||||
.agent .bubble p { margin:0 0 8px; } .agent .bubble ul,.agent .bubble ol { padding-left:20px; margin:8px 0; }
|
||||
.agent .bubble li { margin:3px 0; }
|
||||
.agent .bubble a { color:var(--accent); text-decoration:none; font-weight:500; }
|
||||
.agent .bubble a:hover { text-decoration:underline; }
|
||||
.agent .bubble blockquote { border-left:3px solid var(--accent); margin:10px 0; padding:6px 14px; color:var(--text-secondary); background:#f8f9ff; border-radius:0 6px 6px 0; font-style:italic; }
|
||||
.agent .bubble :not(pre)>code { background:#f1f3f5; color:#e53e3e; padding:2px 6px; border-radius:4px; font-size:0.88em; font-weight:500; }
|
||||
.agent .bubble pre { background:#1a1b26; border-radius:var(--radius-sm); margin:12px 0; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.12); }
|
||||
.agent .bubble pre .code-header { display:flex; justify-content:space-between; align-items:center; padding:8px 14px; background:#252636; color:#9ca3af; font-size:11.5px; border-bottom:1px solid #313244; }
|
||||
.agent .bubble pre .code-header .lang { text-transform:uppercase; }
|
||||
.agent .bubble pre .code-header .copy-code { background:rgba(255,255,255,0.06); border:none; color:#9ca3af; cursor:pointer; font-size:11px; padding:4px 8px; border-radius:4px; }
|
||||
.agent .bubble pre .code-header .copy-code:hover { color:#fff; background:rgba(255,255,255,0.12); }
|
||||
.agent .bubble pre>code { display:block; padding:14px 16px; color:#cdd6f4; line-height:1.65; font-size:13.5px; white-space:pre-wrap; word-break:break-word; }
|
||||
.agent .bubble table { border-collapse:collapse; margin:12px 0; width:100%; font-size:13.5px; border-radius:var(--radius-sm); overflow:hidden; box-shadow:var(--shadow); }
|
||||
.agent .bubble th { background:#f0f3ff; color:var(--text); font-weight:600; padding:10px 14px; border:1px solid #e2e6f0; text-align:left; }
|
||||
.agent .bubble td { padding:9px 14px; border:1px solid #eef1f7; }
|
||||
.agent .bubble tr:nth-child(even) td { background:#fafbff; }
|
||||
.agent .bubble tr:hover td { background:#f0f3ff; }
|
||||
.agent .bubble hr { border:none; border-top:1px solid #e5e7eb; margin:16px 0; }
|
||||
.agent .bubble img { max-width:100%; border-radius:6px; }
|
||||
.agent .bubble strong { font-weight:700; }
|
||||
.input-area { display:flex; padding:14px 20px; background:var(--chat-bg); border-top:1px solid var(--border); gap:8px; align-items:center; flex-shrink:0; }
|
||||
#user-input { flex:1; padding:11px 16px; border:1.5px solid #e5e7eb; border-radius:24px; background:#f8f9fb; color:var(--text); outline:none; font-size:14px; }
|
||||
#user-input:focus { border-color:var(--accent); background:#fff; box-shadow:0 0 0 3px rgba(79,110,247,0.1); }
|
||||
.send-btn { padding:10px 18px; background:var(--accent); border:none; border-radius:20px; color:white; font-weight:600; cursor:pointer; font-size:13.5px; }
|
||||
.send-btn:hover { background:var(--accent-dark); box-shadow:0 2px 8px rgba(79,110,247,0.3); }
|
||||
.send-btn:disabled { opacity:0.5; cursor:not-allowed; }
|
||||
.stop-btn { padding:10px 16px; background:#ef4444; border:none; border-radius:20px; color:white; font-weight:600; cursor:pointer; font-size:13.5px; display:none; }
|
||||
.stop-btn:hover { background:#dc2626; }
|
||||
.copy-btn { background:none; border:none; color:#cbd5e0; cursor:pointer; font-size:11px; padding:3px 6px; position:absolute; top:8px; right:10px; opacity:0; transition:all 0.2s; border-radius:4px; }
|
||||
.message:hover .copy-btn { opacity:1; }
|
||||
.copy-btn:hover { color:var(--accent); background:#f0f3ff; }
|
||||
.drop-zone { border:2px dashed #d1d5db; border-radius:var(--radius-sm); padding:18px; text-align:center; color:#9ca3af; margin:0 20px 8px; display:none; }
|
||||
.drop-zone.active { border-color:var(--accent); background:#f0f4ff; color:var(--accent); }
|
||||
.skillbar { width:var(--skillbar-w); min-width:var(--skillbar-w); max-width:var(--skillbar-w); background:#f8f9fb; border-left:1px solid var(--border); display:flex; flex-direction:column; transition:width 0.25s; overflow:hidden; flex-shrink:0; }
|
||||
.skillbar.collapsed { width:0; min-width:0; max-width:0; border-left:none; }
|
||||
.skillbar-header { padding:16px; font-weight:600; font-size:15px; border-bottom:1px solid var(--border); background:linear-gradient(135deg,var(--accent) 0%,#6366f1 100%); color:white; display:flex; justify-content:space-between; align-items:center; }
|
||||
.skill-actions { padding:10px; border-bottom:1px solid var(--border); display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.skill-actions button,.skill-actions label.btn { background:var(--accent); color:white; border:none; padding:6px 10px; border-radius:6px; cursor:pointer; font-size:12px; }
|
||||
.skill-actions button:hover,.skill-actions label.btn:hover { background:var(--accent-dark); }
|
||||
.skill-item { padding:9px 12px; border-bottom:1px solid #f3f4f6; font-size:13px; display:flex; flex-wrap:wrap; align-items:flex-start; }
|
||||
.skill-item:hover { background:#f0f2f5; }
|
||||
.skill-item .skill-info { flex:1; min-width:0; }
|
||||
.skill-item .skill-name { font-weight:600; color:var(--text); }
|
||||
.skill-item .skill-desc { font-size:11.5px; color:var(--text-secondary); word-break:break-word; margin-top:2px; }
|
||||
.confirm-popup { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); display:flex; align-items:center; justify-content:center; z-index:9999; backdrop-filter:blur(2px); }
|
||||
.confirm-box { background:white; padding:28px; border-radius:var(--radius); max-width:500px; width:90%; box-shadow:0 8px 30px rgba(0,0,0,0.15); }
|
||||
.confirm-text { white-space:pre-wrap; word-break:break-word; margin-bottom:20px; max-height:300px; overflow-y:auto; font-size:14.5px; color:var(--text); }
|
||||
.confirm-buttons { display:flex; gap:10px; justify-content:flex-end; }
|
||||
.confirm-buttons button { padding:9px 22px; border:none; border-radius:var(--radius-sm); cursor:pointer; font-weight:600; font-size:14px; }
|
||||
.btn-approve { background:#10b981; color:white; } .btn-approve:hover { background:#059669; }
|
||||
.btn-reject { background:#ef4444; color:white; } .btn-reject:hover { background:#dc2626; }
|
||||
.timeout-note { font-size:12px; color:#9ca3af; margin-top:12px; text-align:right; }
|
||||
.spinner { border:3px solid #e5e7eb; border-top:3px solid var(--accent); border-radius:50%; width:20px; height:20px; animation:spin 0.8s linear infinite; }
|
||||
@keyframes spin { to{transform:rotate(360deg);} }
|
||||
.login-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); display:flex; align-items:center; justify-content:center; z-index:10000; }
|
||||
.login-box { background:white; padding:36px; border-radius:16px; width:360px; text-align:center; box-shadow:0 16px 48px rgba(0,0,0,0.2); animation:fadeIn 0.3s ease; }
|
||||
.login-box h2 { margin-bottom:24px; color:var(--text); font-size:22px; }
|
||||
.login-box input { width:100%; padding:11px 14px; margin:8px 0; border:1.5px solid #e5e7eb; border-radius:10px; font-size:15px; outline:none; }
|
||||
.login-box input:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(79,110,247,0.1); }
|
||||
.login-box .login-btn { width:100%; padding:11px; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); color:white; border:none; border-radius:10px; font-size:16px; font-weight:600; cursor:pointer; margin-top:14px; }
|
||||
.login-box .login-btn:hover { box-shadow:0 4px 16px rgba(102,126,234,0.4); }
|
||||
.login-error { color:#ef4444; font-size:13px; margin-top:10px; min-height:20px; }
|
||||
.settings-panel { position:fixed; top:0; right:0; width:340px; height:100vh; background:white; box-shadow:-4px 0 20px rgba(0,0,0,0.12); z-index:9998; transform:translateX(100%); transition:transform 0.25s ease; padding:20px; overflow-y:auto; }
|
||||
.settings-panel.open { transform:translateX(0); }
|
||||
.settings-panel h3 { font-size:16px; margin-bottom:12px; color:var(--text); }
|
||||
.settings-panel label { display:block; font-size:12px; color:var(--text-secondary); margin-bottom:2px; margin-top:8px; }
|
||||
.settings-panel input { width:100%; padding:7px 10px; border:1.5px solid #e5e7eb; border-radius:6px; font-size:13px; outline:none; }
|
||||
.settings-panel input:focus { border-color:var(--accent); }
|
||||
.settings-panel .save-btn { margin-top:16px; width:100%; padding:10px; background:var(--accent); color:white; border:none; border-radius:8px; font-weight:600; cursor:pointer; }
|
||||
.settings-panel .close-btn { position:absolute; top:12px; right:12px; background:none; border:none; font-size:20px; cursor:pointer; color:#999; }
|
||||
.settings-panel .agent-section { border:1px solid #e5e7eb; border-radius:8px; padding:10px; margin-bottom:10px; }
|
||||
.settings-panel .agent-section h4 { font-size:13px; margin-bottom:6px; color:var(--accent); }
|
||||
.settings-panel .row { display:flex; gap:6px; }
|
||||
.settings-panel .row input { flex:1; }
|
||||
.settings-panel .eye-btn { background:var(--accent); border:none; cursor:pointer; font-size:12px; padding:6px 8px; color:white; border-radius:4px; white-space:nowrap; }
|
||||
.settings-panel .eye-btn:hover { background:var(--accent-dark); }
|
||||
.model-select { width:100%; margin-top:4px; padding:6px; border:1px solid var(--border); border-radius:4px; font-size:12px; background:white; }
|
||||
.subtitle { font-size:12px; color:rgba(255,255,255,0.7); margin-left:8px; font-weight:400; }
|
||||
.hljs { color:#cdd6f4; background:#1a1b26; }
|
||||
.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-section,.hljs-link { color:#cba6f7; }
|
||||
.hljs-string,.hljs-title,.hljs-name,.hljs-type,.hljs-attr,.hljs-symbol,.hljs-bullet,.hljs-addition,.hljs-variable,.hljs-template-tag,.hljs-template-variable { color:#a6e3a1; }
|
||||
.hljs-comment,.hljs-quote,.hljs-deletion,.hljs-meta { color:#6c7086; font-style:italic; }
|
||||
.hljs-number,.hljs-regexp,.hljs-built_in,.hljs-builtin-name { color:#fab387; }
|
||||
.hljs-attribute,.hljs-tag,.hljs-selector-class,.hljs-selector-id { color:#89b4fa; }
|
||||
.hljs-params { color:#cdd6f4; }
|
||||
.hljs-function .hljs-title,.hljs-class .hljs-title { color:#89b4fa; }
|
||||
.hljs-property,.hljs-selector-pseudo { color:#89dceb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header"><span>查询记录</span><button class="new-chat-btn" onclick="createNewChat()">+ 新查询</button></div>
|
||||
<div class="thread-list" id="threadList"></div>
|
||||
</div>
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<button class="toggle-btn" onclick="toggleSidebar('sidebar')" id="toggle-sidebar-btn">◀</button>
|
||||
<span class="title">MCP 查询</span>
|
||||
<span class="subtitle">可读写 · 禁止删除</span>
|
||||
<button class="export-btn" onclick="exportChat()">↓ 导出</button>
|
||||
<button class="settings-btn" onclick="toggleSettings()">⚙</button>
|
||||
<button class="toggle-btn" onclick="toggleSidebar('skillbar')" id="toggle-skillbar-btn">▶</button>
|
||||
</div>
|
||||
<div class="messages" id="messages"></div>
|
||||
<div id="loading-indicator" style="display:none;margin:0 20px 8px;align-items:center;"><div class="spinner"></div><span style="color:#666;margin-left:8px;">思考中...</span></div>
|
||||
<div class="drop-zone" id="drop-zone">拖拽文件到此处上传</div>
|
||||
<div class="input-area">
|
||||
<input type="text" id="user-input" placeholder="输入消息..." />
|
||||
<input type="file" id="file-input" multiple style="display:none" />
|
||||
<button class="send-btn" onclick="document.getElementById('file-input').click()">📎</button>
|
||||
<button class="send-btn" id="send-btn" onclick="sendMessage()">发送</button>
|
||||
<button class="stop-btn" id="stop-btn" onclick="stopGeneration()">⏹ 停止</button>
|
||||
</div>
|
||||
<div style="padding:4px 20px;font-size:11px;color:var(--text-secondary);text-align:center;" id="token-info"></div>
|
||||
</div>
|
||||
<div class="skillbar" id="skillbar">
|
||||
<div class="skillbar-header">技能管理</div>
|
||||
<div class="skill-actions">
|
||||
<label class="btn" for="skill-zip-upload">上传技能包</label>
|
||||
<input type="file" id="skill-zip-upload" accept=".zip,.md" style="display:none" onchange="uploadSkillZip(this)">
|
||||
<button onclick="reloadSkills()">热更新</button>
|
||||
</div>
|
||||
<div style="flex:1;overflow-y:auto;" id="skillList"><div style="padding:12px;color:#666;">加载中...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panel" id="settings-panel">
|
||||
<button class="close-btn" onclick="toggleSettings()">x</button>
|
||||
<h3>Agent 配置</h3>
|
||||
<p style="font-size:11px;color:#999;margin-bottom:10px;">Temperature 即时生效。模型/URL/Key 对应 .env 变量,修改后需重启。</p>
|
||||
<div class="agent-section">
|
||||
<h4>主 Agent (LLM_MODEL / LLM_API_KEY / LLM_BASE_URL)</h4>
|
||||
<label>模型</label><div class="row"><input type="text" class="model-select" id="cfg-m-model" placeholder="输入模型名或点击获取" onclick="showModelOptions('cfg-m-model','dl-m-model')" /><datalist id="dl-m-model" style="display:none"></datalist><button class="eye-btn" onclick="fetchModels('cfg-m-model','dl-m-model','cfg-m-url','cfg-m-key')" title="获取模型列表">获取</button></div>
|
||||
<label>API URL</label><input type="text" id="cfg-m-url" placeholder="https://open.bigmodel.cn/api/paas/v4" />
|
||||
<label>API Key</label><input type="password" id="cfg-m-key" placeholder="sk-xxx" />
|
||||
<label>Temperature</label><input type="range" id="cfg-m-temp" min="0" max="2" step="0.1" value="0.7" style="width:100%" /><span id="cfg-m-temp-val" style="font-size:12px;color:var(--text-secondary)">0.7</span>
|
||||
<label>上下文窗口 (tokens)</label><input type="number" id="cfg-m-maxtk" value="256000" min="0" style="width:100%" />
|
||||
<button class="save-btn" onclick="testAgent('main')" style="background:#10b981;margin-top:8px;">测试连接</button>
|
||||
</div>
|
||||
<div class="agent-section">
|
||||
<h4>Writer Agent (LLM_MODEL_W / LLM_API_KEY_W / LLM_BASE_URL_W)</h4>
|
||||
<label>模型</label><div class="row"><input type="text" class="model-select" id="cfg-w-model" placeholder="输入模型名或点击获取" onclick="showModelOptions('cfg-w-model','dl-w-model')" /><datalist id="dl-w-model" style="display:none"></datalist><button class="eye-btn" onclick="fetchModels('cfg-w-model','dl-w-model','cfg-w-url','cfg-w-key')" title="获取模型列表">获取</button></div>
|
||||
<label>API URL</label><input type="text" id="cfg-w-url" placeholder="https://api.minimax.chat/v1" />
|
||||
<label>API Key</label><input type="password" id="cfg-w-key" placeholder="sk-xxx" />
|
||||
<button class="save-btn" onclick="testAgent('writer')" style="background:#10b981;margin-top:8px;">测试连接</button>
|
||||
</div>
|
||||
<div class="agent-section">
|
||||
<h4>MCP Agent (LLM_MODEL_M / LLM_API_KEY_M / LLM_BASE_URL_M)</h4>
|
||||
<label>模型</label><div class="row"><input type="text" class="model-select" id="cfg-mcp-model" placeholder="输入模型名或点击获取" onclick="showModelOptions('cfg-mcp-model','dl-mcp-model')" /><datalist id="dl-mcp-model" style="display:none"></datalist><button class="eye-btn" onclick="fetchModels('cfg-mcp-model','dl-mcp-model','cfg-mcp-url','cfg-mcp-key')" title="获取模型列表">获取</button></div>
|
||||
<label>API URL</label><input type="text" id="cfg-mcp-url" placeholder="https://api.minimax.chat/v1" />
|
||||
<label>API Key</label><input type="password" id="cfg-mcp-key" placeholder="sk-xxx" />
|
||||
<button class="save-btn" onclick="testAgent('mcp')" style="background:#10b981;margin-top:8px;">测试连接</button>
|
||||
</div>
|
||||
<button class="save-btn" onclick="saveSettings()">保存全部</button>
|
||||
<button class="save-btn" onclick="applyAndRestart()" style="background:#f59e0b;margin-top:8px;">应用并重启</button>
|
||||
</div>
|
||||
<div class="login-overlay" id="login-overlay">
|
||||
<div class="login-box">
|
||||
<h2>MCP 查询登录</h2>
|
||||
<input type="text" id="login-username" placeholder="用户名" autocomplete="username" />
|
||||
<input type="password" id="login-password" placeholder="密码" autocomplete="current-password" />
|
||||
<button class="login-btn" onclick="doLogin()">登 录</button>
|
||||
<div class="login-error" id="login-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ASSISTANT_ID = 'simple_agent';
|
||||
const API_BASE = window.location.protocol + '//' + window.location.hostname + ':2024';
|
||||
const CHAT_SERVER = window.location.protocol + '//' + window.location.hostname + ':8765';
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const threadListDiv = document.getElementById('threadList');
|
||||
const skillListDiv = document.getElementById('skillList');
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
let currentThreadId = null, threads = [], authToken = sessionStorage.getItem('auth_token') || '';
|
||||
let abortController = null;
|
||||
const messageMap = new Map(), processedConfirms = new Set(), pendingConfirms = new Map(), existingMsgTexts = new Set();
|
||||
var pendingCharts = [];
|
||||
|
||||
// URL user identity
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var embedUser = urlParams.get('user') || 'default';
|
||||
var embedToken = urlParams.get('token') || '';
|
||||
var currentUser = embedUser || 'default';
|
||||
// 迁移旧存储
|
||||
if (!localStorage.getItem(userKey('mcp_threads')) && localStorage.getItem('mcp_threads')) {
|
||||
var keysToMigrate = [];
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var k = localStorage.key(i);
|
||||
if (k && (k === 'mcp_threads' || k === 'mcp_config' || k.startsWith('mcp_msg_cache_') || k === 'mcp_last_thread')) {
|
||||
keysToMigrate.push(k);
|
||||
}
|
||||
}
|
||||
keysToMigrate.forEach(function(k) { localStorage.setItem(userKey(k), localStorage.getItem(k)); localStorage.removeItem(k); });
|
||||
}
|
||||
if (embedUser && embedToken) {
|
||||
authToken = embedToken;
|
||||
sessionStorage.setItem('auth_token', embedToken);
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
function userKey(k) { return currentUser + '_mcp_' + k; }
|
||||
function localGet(k) { return localStorage.getItem(userKey(k)); }
|
||||
function localSet(k, v) { localStorage.setItem(userKey(k), v); }
|
||||
|
||||
// ====== UI Helpers ======
|
||||
function toggleSidebar(id) {
|
||||
var el = document.getElementById(id);
|
||||
el.classList.toggle('collapsed');
|
||||
if (id === 'sidebar') document.getElementById('toggle-sidebar-btn').textContent = el.classList.contains('collapsed') ? '\u25b6' : '\u25c0';
|
||||
else document.getElementById('toggle-skillbar-btn').textContent = el.classList.contains('collapsed') ? '\u25c0' : '\u25b6';
|
||||
}
|
||||
function escapeHtml(text) {
|
||||
var map = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
||||
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||
}
|
||||
function showError(msg) {
|
||||
addMessage('system', msg, '系统');
|
||||
loadingIndicator.style.display = 'none';
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('send-btn').disabled = false;
|
||||
}
|
||||
|
||||
// ====== Markdown ======
|
||||
if (typeof marked !== 'undefined') {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
if (typeof hljs !== 'undefined') {
|
||||
marked.setOptions({ highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (e) {} }
|
||||
return code;
|
||||
}});
|
||||
}
|
||||
var renderer = new marked.Renderer();
|
||||
renderer.code = function(code, lang) {
|
||||
var langLabel = lang || 'code';
|
||||
var escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
var highlighted = (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) ? hljs.highlight(code, { language: lang }).value : escaped;
|
||||
return '<pre><div class="code-header"><span class="lang">' + langLabel + '</span><button class="copy-code" onclick="copyCodeBlock(this)">复制</button></div><code class="hljs">' + highlighted + '</code></pre>';
|
||||
};
|
||||
marked.setOptions({ renderer: renderer });
|
||||
}
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
try { return marked.parse(text); } catch (e) { return escapeHtml(text).replace(/\n/g, '<br>'); }
|
||||
}
|
||||
function copyCodeBlock(btn) {
|
||||
var code = btn.parentElement.nextElementSibling;
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code.textContent).then(function() {
|
||||
btn.textContent = '已复制';
|
||||
setTimeout(function() { btn.textContent = '复制'; }, 1500);
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Message Rendering ======
|
||||
function addMessage(role, content, label, msgId) {
|
||||
msgId = msgId || ('msg-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6));
|
||||
var div = document.createElement('div');
|
||||
div.className = 'message ' + role;
|
||||
var html = label ? '<div class="label">' + label + '</div>' : '';
|
||||
var body;
|
||||
if (role === 'agent') body = renderMarkdown(content);
|
||||
else if (role === 'tool') body = '<pre><code>' + escapeHtml(content) + '</code></pre>';
|
||||
else body = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
html += '<div class="bubble" id="' + msgId + '">' + body + '<button class="copy-btn" onclick="copyMsg(\'' + msgId + '\')">\ud83d\udccb</button></div>';
|
||||
div.innerHTML = html;
|
||||
messagesDiv.appendChild(div);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
function copyMsg(id) {
|
||||
var el = document.getElementById(id); if (!el) return;
|
||||
var clone = el.cloneNode(true); var cb = clone.querySelector('.copy-btn'); if (cb) cb.remove();
|
||||
navigator.clipboard.writeText(clone.innerText).catch(function() {});
|
||||
}
|
||||
function addMessagePlain(role, content, label) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'message ' + role;
|
||||
var html = label ? '<div class="label">' + label + '</div>' : '';
|
||||
html += '<div class="bubble">' + escapeHtml(content).replace(/\n/g, '<br>') + '</div>';
|
||||
div.innerHTML = html;
|
||||
messagesDiv.appendChild(div);
|
||||
}
|
||||
function renderHistoryMessage(msg) {
|
||||
if (msg.type === 'human' || msg.role === 'user') addMessagePlain('user', msg.content, '你');
|
||||
else if (msg.type === 'ai') {
|
||||
var c = (msg.content || '').replace(/<think>.*?<\/think>/gs, '').trim();
|
||||
if (c) addMessage('agent', c, 'Agent');
|
||||
if (msg.tool_calls) {
|
||||
for (var i = 0; i < msg.tool_calls.length; i++) {
|
||||
var tc = msg.tool_calls[i];
|
||||
addMessagePlain('tool', tc.name + '(' + JSON.stringify(tc.args) + ')', '工具调用');
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'tool') {
|
||||
var tc = msg.name + ': ' + (msg.content || '');
|
||||
if ((msg.content||'').indexOf('![图表]') >= 0) {
|
||||
addMessage('agent', msg.content, msg.name || '工具结果');
|
||||
} else {
|
||||
addMessagePlain('tool', tc, '工具结果');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Thread Management ======
|
||||
function loadThreads() {
|
||||
var stored = localGet('mcp_threads');
|
||||
threads = stored ? JSON.parse(stored) : [];
|
||||
renderThreadList();
|
||||
fetch(API_BASE + '/threads/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
var serverIds = new Set();
|
||||
data.forEach(function(t) { serverIds.add(t.thread_id);
|
||||
if (!threads.find(function(l) { return l.id === t.thread_id; })) {
|
||||
threads.unshift({ id: t.thread_id, title: '', createdAt: Date.now() });
|
||||
}
|
||||
});
|
||||
threads = threads.filter(function(t) { return serverIds.has(t.id); });
|
||||
saveThreads();
|
||||
renderThreadList();
|
||||
}
|
||||
}).catch(function() {});
|
||||
}
|
||||
function saveThreads() { localSet('mcp_threads', JSON.stringify(threads)); }
|
||||
function renderThreadList() {
|
||||
threadListDiv.innerHTML = '';
|
||||
threads.forEach(function(t) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'thread-item' + (t.id === currentThreadId ? ' active' : '');
|
||||
// 不显示删除按钮——MCP页面禁止删除
|
||||
div.innerHTML = '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;" onclick="switchThread(\'' + t.id + '\')">' + escapeHtml(t.title || t.id.substring(0, 8)) + '</span>';
|
||||
threadListDiv.appendChild(div);
|
||||
});
|
||||
}
|
||||
async function createThread() {
|
||||
var resp = await apiFetch(API_BASE + '/threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!resp.ok) throw new Error('创建失败');
|
||||
return (await resp.json()).thread_id;
|
||||
}
|
||||
async function createNewChat() {
|
||||
try { var threadId = await createThread(); threads.unshift({ id: threadId, title: '', createdAt: Date.now() });
|
||||
saveThreads(); currentThreadId = threadId; renderThreadList(); messagesDiv.innerHTML = ''; messageMap.clear(); }
|
||||
catch (err) { addMessage('system', '创建新对话失败: ' + err.message, '系统'); }
|
||||
}
|
||||
async function switchThread(threadId) {
|
||||
currentThreadId = threadId; localSet('mcp_last_thread', threadId);
|
||||
renderThreadList(); messagesDiv.innerHTML = ''; messageMap.clear(); existingMsgTexts.clear();
|
||||
var cached = localGet('mcp_msg_cache_' + threadId);
|
||||
var hasCache = false;
|
||||
if (cached) {
|
||||
try {
|
||||
var cmsgs = JSON.parse(cached);
|
||||
if (cmsgs.length > 0) {
|
||||
for (var j = 0; j < cmsgs.length; j++) renderHistoryMessage(cmsgs[j]);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
hasCache = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!hasCache) {
|
||||
messagesDiv.innerHTML = '<div class="message system"><div class="bubble" style="display:flex;align-items:center;gap:8px;"><div class="spinner"></div><span>加载中...</span></div></div>';
|
||||
setTimeout(function() {
|
||||
if (currentThreadId === threadId && messagesDiv.children.length <= 1) {
|
||||
messagesDiv.innerHTML = '<div class="message system"><div class="bubble">暂无对话内容</div></div>';
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
apiFetch(API_BASE + '/threads/' + threadId + '/state').then(async function(resp) {
|
||||
if (!resp.ok) return;
|
||||
var data = await resp.json();
|
||||
var msgs = data && data.values ? data.values.messages || [] : [];
|
||||
if (msgs.length === 0) return;
|
||||
localSet('mcp_msg_cache_' + threadId, JSON.stringify(msgs));
|
||||
if (!hasCache || msgs.length !== JSON.parse(cached).length) {
|
||||
messagesDiv.innerHTML = ''; messageMap.clear();
|
||||
for (var i = 0; i < msgs.length; i++) renderHistoryMessage(msgs[i]);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
// ====== Confirm Flow ======
|
||||
async function checkConfirmPending(confirmId) {
|
||||
if (pendingConfirms.has(confirmId)) return pendingConfirms.get(confirmId);
|
||||
var p = apiFetch(CHAT_SERVER + '/api/confirm_info?confirm_id=' + encodeURIComponent(confirmId))
|
||||
.then(async function(r) { if (r.ok) { var d = await r.json(); return d.result === 'pending'; } return true; })
|
||||
.catch(function() { return true; });
|
||||
pendingConfirms.set(confirmId, p); return p;
|
||||
}
|
||||
async function processMessage(msg) {
|
||||
if (msg.type === 'human' || msg.role === 'user') return;
|
||||
var content = msg.content || '';
|
||||
var confirmMatch = content.match(/\[NEED_USER_CONFIRM_FILE\|([a-f0-9\-]+)\]/);
|
||||
if (confirmMatch) { var cid = confirmMatch[1]; if (processedConfirms.has(cid)) return; processedConfirms.add(cid);
|
||||
var isPending = await checkConfirmPending(cid); if (!isPending) return; showConfirmPopupForWrite(cid); throw { type: 'CONFIRM_PENDING' }; }
|
||||
var dedupKey = (msg.type||'') + '|' + content.substring(0, 100);
|
||||
if (messageMap.has(dedupKey)) return; messageMap.set(dedupKey, true);
|
||||
var txt = (msg.content || '').substring(0, 100);
|
||||
if (existingMsgTexts.has(txt)) return;
|
||||
if (msg.type === 'ai') {
|
||||
var c = (msg.content || '').replace(/<think>.*?<\/think>/gs, '').trim();
|
||||
if (c && pendingCharts.length > 0) {
|
||||
var parts = c.split(/(?<=[。!?\n])/);
|
||||
var summary = parts.slice(0, Math.min(2, parts.length)).join('').trim();
|
||||
var rest = parts.slice(Math.min(2, parts.length)).join('').trim();
|
||||
if (summary) addMessage('agent', summary, 'Agent');
|
||||
pendingCharts.forEach(function(chart) { addMessage('agent', chart, '图表'); });
|
||||
pendingCharts = [];
|
||||
if (rest) addMessage('agent', rest, 'Agent');
|
||||
} else if (c) {
|
||||
addMessage('agent', c, 'Agent');
|
||||
}
|
||||
if (msg.tool_calls) { for (var i = 0; i < msg.tool_calls.length; i++) { var tc = msg.tool_calls[i]; addMessage('tool', tc.name + '(' + JSON.stringify(tc.args) + ')', '工具调用'); } }
|
||||
} else if (msg.type === 'tool') {
|
||||
var tc = msg.content || '';
|
||||
if (tc.indexOf('![图表]') >= 0) {
|
||||
var imgs = tc.match(/!\[.*?\]\(\/workspace\/.+?\.svg\)/g);
|
||||
if (imgs) imgs.forEach(function(img) { pendingCharts.push(img); });
|
||||
} else {
|
||||
addMessage('tool', msg.name + ': ' + tc, '工具结果');
|
||||
}
|
||||
}
|
||||
}
|
||||
function showConfirmPopupForWrite(confirmId) {
|
||||
var popup = document.createElement('div'); popup.id = 'confirm-popup'; popup.className = 'confirm-popup';
|
||||
popup.innerHTML = '<div class="confirm-box"><div class="confirm-text">文件不在安全区域内,是否写入?</div><div class="confirm-buttons"><button class="btn-approve">同意写入</button><button class="btn-reject">拒绝</button></div></div>';
|
||||
document.body.appendChild(popup);
|
||||
popup.querySelector('.btn-approve').addEventListener('click', async function() { popup.remove();
|
||||
try { var r = await apiFetch(CHAT_SERVER + '/api/confirm_write', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({confirm_id:confirmId, choice:'approve'}) });
|
||||
var d = await r.json(); addMessage('system', d.success ? '\u2705 已写入: ' + (d.path||'') : '写入失败: ' + (d.error||d.message), '系统'); }
|
||||
catch(e) { addMessage('system', '请求失败: ' + e.message, '系统'); }
|
||||
});
|
||||
popup.querySelector('.btn-reject').addEventListener('click', async function() { popup.remove();
|
||||
try { await apiFetch(CHAT_SERVER + '/api/confirm_write', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({confirm_id:confirmId, choice:'reject'}) }); } catch(e){}
|
||||
addMessage('system', '已拒绝写入', '系统');
|
||||
});
|
||||
document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') popup.remove(); }, { once: true });
|
||||
}
|
||||
|
||||
// ====== Stream Processing ======
|
||||
async function processStream(response) {
|
||||
var reader = response.body.getReader(); var decoder = new TextDecoder(); var buffer = '', first = false;
|
||||
try { while (true) { var result = await reader.read(); if (result.done) break;
|
||||
buffer += decoder.decode(result.value, { stream: true }); var lines = buffer.split('\n'); buffer = lines.pop();
|
||||
for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith('data: ')) continue;
|
||||
try { var data = JSON.parse(line.slice(6));
|
||||
if (data.__interrupt__) { showInterruptPopup(data.__interrupt__); reader.cancel(); loadingIndicator.style.display='none'; stopUI(); return; }
|
||||
if (data.messages) { if (!first) { loadingIndicator.style.display='none'; first=true; }
|
||||
for (var j=0;j<data.messages.length;j++) { try { await processMessage(data.messages[j]); } catch(e) { if (e&&e.type==='CONFIRM_PENDING') { reader.cancel(); loadingIndicator.style.display='none'; stopUI(); return; } throw e; } }
|
||||
}
|
||||
} catch(e) { if (e&&e.type==='CONFIRM_PENDING') { reader.cancel(); loadingIndicator.style.display='none'; stopUI(); return; } }
|
||||
}
|
||||
}
|
||||
} catch(e) { if (e.name!=='AbortError') console.error('Stream error:',e); }
|
||||
loadingIndicator.style.display='none'; stopUI();
|
||||
if (pendingCharts.length > 0) {
|
||||
pendingCharts.forEach(function(chart) { addMessage('agent', chart, '图表'); });
|
||||
pendingCharts = [];
|
||||
}
|
||||
if (currentThreadId) {
|
||||
var allBubbles = messagesDiv.querySelectorAll('.message');
|
||||
var cachedMsgs = [];
|
||||
allBubbles.forEach(function(m) {
|
||||
if (m.classList.contains('user')) cachedMsgs.push({type:'human',role:'user',content:m.querySelector('.bubble').innerText.trim()});
|
||||
else if (m.classList.contains('agent')) cachedMsgs.push({type:'ai',role:'assistant',content:m.querySelector('.bubble').innerText.trim()});
|
||||
else if (m.classList.contains('tool')) cachedMsgs.push({type:'tool',role:'tool',name:'',content:m.querySelector('.bubble').innerText.trim()});
|
||||
});
|
||||
localSet('mcp_msg_cache_' + currentThreadId, JSON.stringify(cachedMsgs));
|
||||
}
|
||||
}
|
||||
function showInterruptPopup(data) {
|
||||
var popup = document.createElement('div'); popup.id='confirm-popup'; popup.className='confirm-popup';
|
||||
popup.innerHTML = '<div class="confirm-box"><div class="confirm-text">' + escapeHtml(data.message||'是否继续?') + '</div><div class="confirm-buttons"><button class="btn-approve">同意</button><button class="btn-reject">拒绝</button></div><div class="timeout-note">30分钟后超时</div></div>';
|
||||
document.body.appendChild(popup);
|
||||
popup.querySelectorAll('button').forEach(function(b) { b.addEventListener('click', async function() { popup.remove(); await resumeWithCommand(data, b.classList.contains('btn-approve')?'approve':'reject'); }); });
|
||||
document.addEventListener('keydown', function esc(e) { if (e.key==='Escape') popup.remove(); }, { once:true });
|
||||
}
|
||||
async function resumeWithCommand(data, val) {
|
||||
var payload = { assistant_id:ASSISTANT_ID, thread_id:currentThreadId, input:null, config:{configurable:{mcp_mode:true}}, command:{resume:{__interrupt_id__:data.__interrupt_id__,value:val}} };
|
||||
startUI(); try { var resp = await apiFetch(API_BASE+'/threads/'+currentThreadId+'/runs/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); await processStream(resp); } catch(e) { showError('恢复失败: '+e.message); }
|
||||
}
|
||||
function stopGeneration() { if (abortController) { abortController.abort(); abortController=null; } stopUI(); }
|
||||
function startUI() { loadingIndicator.style.display='flex'; document.getElementById('stop-btn').style.display='inline-block'; document.getElementById('send-btn').disabled=true; }
|
||||
function stopUI() { loadingIndicator.style.display='none'; document.getElementById('stop-btn').style.display='none'; document.getElementById('send-btn').disabled=false; }
|
||||
|
||||
// ====== Send Message ======
|
||||
async function sendMessage() {
|
||||
if (!currentThreadId) { try { currentThreadId=await createThread(); threads.unshift({id:currentThreadId,title:'',createdAt:Date.now()}); saveThreads(); renderThreadList(); messagesDiv.innerHTML=''; messageMap.clear(); } catch(err) { addMessage('system','创建对话失败','系统'); return; } }
|
||||
var input = document.getElementById('user-input'); var text = input.value.trim(); if (!text) return;
|
||||
addMessage('user', text, '你'); input.value='';
|
||||
pendingCharts = [];
|
||||
var ct = threads.find(function(t){return t.id===currentThreadId;}); if (ct&&!ct.title){ct.title=text.substring(0,30);saveThreads();renderThreadList();}
|
||||
existingMsgTexts.clear();
|
||||
messagesDiv.querySelectorAll('.bubble').forEach(function(b){ existingMsgTexts.add(b.innerText.trim().substring(0,100)); });
|
||||
startUI(); abortController = new AbortController();
|
||||
var cfg = JSON.parse(localGet('mcp_config')||'{}'); var config={configurable:{mcp_mode:true}}; if(cfg.temperature) config.configurable.temperature=cfg.temperature;
|
||||
try {
|
||||
var resp = await fetch(API_BASE+'/threads/'+currentThreadId+'/runs/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({assistant_id:ASSISTANT_ID,thread_id:currentThreadId,config:config,input:{messages:[{role:'user',content:text}]}}),signal:abortController.signal});
|
||||
await processStream(resp);
|
||||
} catch(e) { if (e.name==='AbortError') addMessage('system','已停止生成','系统'); else showError('请求失败: '+e.message); }
|
||||
abortController=null;
|
||||
}
|
||||
|
||||
// ====== Export ======
|
||||
function exportChat() {
|
||||
var msgs=[]; messagesDiv.querySelectorAll('.message').forEach(function(m){var bubble=m.querySelector('.bubble');if(!bubble)return;var clone=bubble.cloneNode(true);var cb=clone.querySelector('.copy-btn');if(cb)cb.remove();var role=m.classList.contains('user')?'用户':m.classList.contains('agent')?'Agent':m.classList.contains('tool')?'工具':'系统';msgs.push('## '+role+'\n\n'+clone.innerText+'\n');});
|
||||
var text='# MCP 查询记录\n\n'+new Date().toLocaleString()+'\n\n---\n\n'+msgs.join('\n---\n\n');
|
||||
var blob=new Blob([text],{type:'text/markdown;charset=utf-8'}); var a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='mcp-query-'+new Date().toISOString().slice(0,10)+'.md'; a.click(); URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
// ====== File Upload ======
|
||||
var dropZone=document.getElementById('drop-zone');
|
||||
['dragenter','dragover'].forEach(function(e){document.addEventListener(e,function(ev){ev.preventDefault();dropZone.style.display='block';dropZone.classList.add('active');});});
|
||||
['dragleave','drop'].forEach(function(e){document.addEventListener(e,function(ev){ev.preventDefault();if(e==='dragleave'){dropZone.style.display='none';dropZone.classList.remove('active');}});});
|
||||
dropZone.addEventListener('drop',function(e){e.preventDefault();dropZone.style.display='none';dropZone.classList.remove('active');uploadFiles(e.dataTransfer.files);});
|
||||
document.getElementById('file-input').addEventListener('change',function(){uploadFiles(this.files);this.value='';});
|
||||
async function uploadFiles(files){for(var i=0;i<files.length;i++){var f=files[i];var fd=new FormData();fd.append('file',f);var type=f.name.match(/\.(txt|log|md|json|csv|yml|yaml|xml|html|css|js|py|java|cpp|c|h)$/i)?'knowledge':'memory';try{var r=await apiFetch(CHAT_SERVER+'/upload?type='+type,{method:'POST',body:fd});if(r.ok)addMessage('system','已上传: '+f.name,'系统');}catch(e){addMessage('system','上传失败: '+f.name,'系统');}}}
|
||||
|
||||
// ====== Shortcuts ======
|
||||
document.addEventListener('keydown',function(e){if(e.target.tagName==='INPUT'&&e.target.id==='user-input'&&e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}else if(e.ctrlKey&&e.key==='n'){e.preventDefault();createNewChat();}else if(e.ctrlKey&&e.key==='e'){e.preventDefault();exportChat();}else if(e.key==='Escape'){var popup=document.getElementById('confirm-popup');if(popup){popup.remove();stopUI();}}});
|
||||
|
||||
// ====== API + Auth ======
|
||||
async function apiFetch(url, options) {
|
||||
options = options || {};
|
||||
if (authToken) { options.headers = options.headers || {}; options.headers['Authorization'] = 'Bearer ' + authToken; options.headers['X-User'] = currentUser; }
|
||||
return fetch(url, options);
|
||||
}
|
||||
function checkLogin() {
|
||||
if (authToken) {
|
||||
apiFetch(CHAT_SERVER + '/skills/list').then(function(r) {
|
||||
if (r.ok) {
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
loadThreads();
|
||||
var lt = localGet('mcp_last_thread');
|
||||
if (lt && threads.find(function(t) { return t.id === lt; })) switchThread(lt);
|
||||
else if (threads.length === 0) createNewChat();
|
||||
fetchSkills();
|
||||
} else { authToken = ''; sessionStorage.removeItem('auth_token'); }
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
async function doLogin() {
|
||||
var username = document.getElementById('login-username').value.trim();
|
||||
var password = document.getElementById('login-password').value;
|
||||
var errorEl = document.getElementById('login-error');
|
||||
if (!username || !password) { errorEl.textContent = '请输入用户名和密码'; return; }
|
||||
try {
|
||||
var resp = await fetch(CHAT_SERVER + '/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, password: password }) });
|
||||
var data = await resp.json();
|
||||
if (resp.ok && data.token) {
|
||||
authToken = data.token;
|
||||
sessionStorage.setItem('auth_token', data.token);
|
||||
document.getElementById('login-overlay').style.display = 'none';
|
||||
loadThreads();
|
||||
if (threads.length === 0) createNewChat();
|
||||
fetchSkills();
|
||||
} else { errorEl.textContent = data.error || '登录失败'; }
|
||||
} catch (e) { errorEl.textContent = '连接失败: ' + e.message; }
|
||||
}
|
||||
|
||||
// ====== Skills (只展示,不显示删除/恢复按钮 —— MCP页面禁止删除) ======
|
||||
async function fetchSkills() {
|
||||
try { var results = await Promise.all([apiFetch(CHAT_SERVER+'/skills/list')]); var normal = await results[0].json(); renderSkillList(normal); }
|
||||
catch (e) { skillListDiv.innerHTML = '<div style="padding:12px;color:#d666;">获取失败</div>'; }
|
||||
}
|
||||
function renderSkillList(normal) {
|
||||
var html = '<div style="font-weight:bold;padding:8px 12px;border-bottom:1px solid #ddd;">已安装技能</div>';
|
||||
if (!normal || !normal.length) html += '<div style="padding:12px;color:#666;">暂无</div>';
|
||||
else normal.forEach(function(s) { html += '<div class="skill-item"><div class="skill-info"><div class="skill-name">' + escapeHtml(s.name) + '</div><div class="skill-desc">' + escapeHtml(s.description || '') + '</div></div></div>'; });
|
||||
skillListDiv.innerHTML = html;
|
||||
}
|
||||
async function reloadSkills() { try { var r = await apiFetch(CHAT_SERVER+'/skills/reload',{method:'POST'}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '系统'); fetchSkills(); } } catch (e) { addMessage('system', '热更新失败', '系统'); } }
|
||||
async function uploadSkillZip(input) { var file = input.files[0]; if (!file) return; var fd = new FormData(); fd.append('file', file);
|
||||
try { var r = await apiFetch(CHAT_SERVER+'/skills/upload',{method:'POST',body:fd}); var d = await r.json(); if (r.ok) { addMessage('system', d.message, '系统'); fetchSkills(); } } catch (e) { addMessage('system', '上传失败', '系统'); } input.value = ''; }
|
||||
|
||||
// ====== Settings ======
|
||||
function toggleSettings() {
|
||||
var p = document.getElementById('settings-panel'); p.classList.toggle('open');
|
||||
if (p.classList.contains('open')) {
|
||||
var cfg = JSON.parse(localGet('mcp_config') || '{}');
|
||||
var mSel = document.getElementById('cfg-m-model'); ensureOption(mSel, cfg.m_model);
|
||||
document.getElementById('cfg-m-url').value = cfg.m_url || ''; document.getElementById('cfg-m-key').value = cfg.m_key || '';
|
||||
document.getElementById('cfg-m-temp').value = cfg.temperature || 0.7; document.getElementById('cfg-m-temp-val').textContent = cfg.temperature || 0.7;
|
||||
var wSel = document.getElementById('cfg-w-model'); ensureOption(wSel, cfg.w_model);
|
||||
document.getElementById('cfg-w-url').value = cfg.w_url || ''; document.getElementById('cfg-w-key').value = cfg.w_key || '';
|
||||
var mcpSel = document.getElementById('cfg-mcp-model'); ensureOption(mcpSel, cfg.mcp_model);
|
||||
document.getElementById('cfg-mcp-url').value = cfg.mcp_url || ''; document.getElementById('cfg-mcp-key').value = cfg.mcp_key || '';
|
||||
document.getElementById('cfg-m-maxtk').value = cfg.max_tokens || 256000;
|
||||
}
|
||||
}
|
||||
function ensureOption(inp, val) { if (!val || !inp) return; if (!inp.value) inp.value = val; }
|
||||
document.getElementById('cfg-m-temp').addEventListener('input', function() { document.getElementById('cfg-m-temp-val').textContent = this.value; });
|
||||
function saveSettings() {
|
||||
var cfg = { temperature: parseFloat(document.getElementById('cfg-m-temp').value), m_model: document.getElementById('cfg-m-model').value.trim(), m_url: document.getElementById('cfg-m-url').value.trim(), m_key: document.getElementById('cfg-m-key').value.trim(), w_model: document.getElementById('cfg-w-model').value.trim(), w_url: document.getElementById('cfg-w-url').value.trim(), w_key: document.getElementById('cfg-w-key').value.trim(), mcp_model: document.getElementById('cfg-mcp-model').value.trim(), mcp_url: document.getElementById('cfg-mcp-url').value.trim(), mcp_key: document.getElementById('cfg-mcp-key').value.trim() };
|
||||
Object.keys(cfg).forEach(function(k) { if (!cfg[k] && cfg[k] !== 0) delete cfg[k]; });
|
||||
localSet('mcp_config', JSON.stringify(cfg));
|
||||
document.getElementById('settings-panel').classList.remove('open');
|
||||
apiFetch(CHAT_SERVER + '/api/save_config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }).then(function(r) { return r.json(); }).then(function(d) {
|
||||
addMessage('system', d.success ? '配置已保存并热更新生效' : (d.message || d.error || '保存失败'), '系统');
|
||||
}).catch(function(e) { addMessage('system', '保存失败: ' + e.message, '系统'); });
|
||||
}
|
||||
function applyAndRestart() { saveSettings(); if (!confirm('将把配置写入.env并重启服务,确认?')) return;
|
||||
var cfg = JSON.parse(localGet('mcp_config') || '{}'); addMessage('system', '正在应用配置并重启...', '系统');
|
||||
apiFetch(CHAT_SERVER + '/api/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }).then(function(r) { return r.json(); }).then(function(d) { addMessage('system', d.message || '重启中', '系统'); setTimeout(function() { location.reload(); }, 5000); }).catch(function(e) { addMessage('system', '重启请求失败: ' + e.message, '系统'); });
|
||||
}
|
||||
function fetchModels(mid, did, uid, kid) {
|
||||
var url = document.getElementById(uid).value.trim(); var key = document.getElementById(kid).value.trim();
|
||||
if (!url || !key) { addMessage('system', '请先填写API URL和Key', '系统'); return; }
|
||||
url = url.replace(/\/+$/, '') + '/models'; addMessage('system', '正在获取模型列表...', '系统');
|
||||
fetch(url, { headers: { 'Authorization': 'Bearer ' + key } }).then(function(r) {
|
||||
if (!r.ok) { addMessage('system', 'HTTP ' + r.status + ' 请手动输入', '系统'); return; }
|
||||
return r.text();
|
||||
}).then(function(t) {
|
||||
if (!t) return;
|
||||
try { var d = JSON.parse(t); var models = d.data || d.models || []; if (!models.length) { addMessage('system', '未获取到模型', '系统'); return; }
|
||||
var names = models.map(function(m) { return m.id; }).filter(function(v,i,a){ return a.indexOf(v)===i; }).sort();
|
||||
var dl = document.getElementById(did);
|
||||
if (dl) { dl.innerHTML = ''; names.forEach(function(n) { var o = document.createElement('option'); o.value = n; dl.appendChild(o); }); }
|
||||
var inp = document.getElementById(mid);
|
||||
if (!inp.value && names.length > 0) inp.value = names[0];
|
||||
inp.placeholder = names.length + ' 个模型可选,点击输入框查看';
|
||||
addMessage('system', '已加载 ' + names.length + ' 个模型,点击输入框查看', '系统');
|
||||
} catch (e) { addMessage('system', '响应非JSON: ' + t.substring(0, 80), '系统'); }
|
||||
}).catch(function(e) { addMessage('system', '获取失败: ' + e.message, '系统'); });
|
||||
}
|
||||
|
||||
// ====== Agent Test ======
|
||||
async function testAgent(type) { saveSettings(); var label = type === 'main' ? '主Agent' : type === 'writer' ? 'WriterAgent' : 'MCPAgent';
|
||||
var testMsg = type === 'main' ? 'Hi, reply OK' : type === 'writer' ? 'Use utc_now to get time' : 'Use utc_now to get time';
|
||||
addMessage('system', 'Testing ' + label + '...', '系统');
|
||||
try { var r = await fetch(API_BASE + '/threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!r.ok) { addMessage('system', '✗ ' + label + ': Cannot create thread HTTP ' + r.status, '系统'); return; }
|
||||
var d = await r.json(); var tid = d.thread_id;
|
||||
var r2 = await fetch(API_BASE + '/threads/' + tid + '/runs/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assistant_id: ASSISTANT_ID, thread_id: tid, input: { messages: [{ role: 'user', content: testMsg }] } }) });
|
||||
if (!r2.ok) { addMessage('system', '✗ ' + label + ': Stream failed HTTP ' + r2.status, '系统'); return; }
|
||||
var reader = r2.body.getReader(), decoder = new TextDecoder(), buf = '', found = false;
|
||||
var timeout = setTimeout(function() { if (!found) addMessage('system', '✗ ' + label + ': Timeout, API reachable', '系统'); }, 30000);
|
||||
try { while (true) { var chunk = await reader.read(); if (chunk.done) break; buf += decoder.decode(chunk.value, { stream: true });
|
||||
var lines = buf.split('\n'); buf = lines.pop();
|
||||
for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith('data: ')) continue;
|
||||
try { var msgData = JSON.parse(line.slice(6)); if (msgData.messages) { for (var j = 0; j < msgData.messages.length; j++) { var msg = msgData.messages[j]; if (msg.content && msg.content.trim()) { found = true; clearTimeout(timeout); reader.cancel(); addMessage('system', '✓ ' + label + ': Connected, model responded', '系统'); return; } } } } catch (e) {} }
|
||||
}
|
||||
} catch (e) { if (!found) { clearTimeout(timeout); addMessage('system', '✗ ' + label + ': Read error - ' + e.message, '系统'); } }
|
||||
if (!found) { clearTimeout(timeout); addMessage('system', '✗ ' + label + ': No response, API reachable', '系统'); }
|
||||
} catch (e) { addMessage('system', '✗ ' + label + ': ' + e.message, '系统'); }
|
||||
}
|
||||
|
||||
// ====== Token Display ======
|
||||
function estimateTokens(text) { var cn = (text.match(/[\u4e00-\u9fff]/g) || []).length; var en = text.length - cn; return Math.ceil(cn * 0.6 + en * 0.25); }
|
||||
function updateTokenDisplay() { var total = 0; messagesDiv.querySelectorAll('.message .bubble').forEach(function(b) { total += estimateTokens(b.innerText || ''); });
|
||||
var cfg = JSON.parse(localGet('mcp_config') || '{}'); var max = cfg.max_tokens || 256000; var el = document.getElementById('token-info');
|
||||
if (max > 0) { el.textContent = '已用 ~' + total + ' / ' + max + ' tokens (' + Math.round(total / max * 100) + '%)'; el.style.color = total > max * 0.8 ? '#ef4444' : 'var(--text-secondary)'; }
|
||||
else { el.textContent = '~' + total + ' tokens'; }
|
||||
}
|
||||
setInterval(updateTokenDisplay, 3000);
|
||||
|
||||
// ====== Model Selector ======
|
||||
function showModelOptions(inpId, dlId) {
|
||||
var dl = document.getElementById(dlId);
|
||||
if (!dl || dl.options.length === 0) return;
|
||||
var old = document.getElementById('model-options-panel');
|
||||
if (old) old.remove();
|
||||
var inp = document.getElementById(inpId);
|
||||
var rect = inp.getBoundingClientRect();
|
||||
var panel = document.createElement('div');
|
||||
panel.id = 'model-options-panel';
|
||||
panel.style.cssText = 'position:fixed;z-index:99999;background:white;border:1px solid #ccc;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.15);max-height:200px;overflow-y:auto;min-width:200px';
|
||||
panel.style.left = rect.left + 'px';
|
||||
panel.style.top = (rect.bottom + 4) + 'px';
|
||||
panel.style.width = Math.max(rect.width, 200) + 'px';
|
||||
for (var i = 0; i < dl.options.length; i++) {
|
||||
var opt = dl.options[i];
|
||||
var item = document.createElement('div');
|
||||
item.textContent = opt.value;
|
||||
item.style.cssText = 'padding:6px 10px;cursor:pointer;font-size:12px;color:#333';
|
||||
item.onmouseover = function() { this.style.background = '#e8edff'; };
|
||||
item.onmouseout = function() { this.style.background = ''; };
|
||||
item.onclick = (function(v) { return function() { inp.value = v; panel.remove(); }; })(opt.value);
|
||||
panel.appendChild(item);
|
||||
}
|
||||
document.body.appendChild(panel);
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', function rm() { panel.remove(); document.removeEventListener('click', rm); }, {once: true});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ====== Init ======
|
||||
checkLogin();
|
||||
document.getElementById('login-password').addEventListener('keydown', function(e) { if (e.key === 'Enter') doLogin(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
# 远程 MCP 查询服务(通过 HTTP 调用外部 MCP server)
|
||||
# {
|
||||
# "name": "airport_monitor",
|
||||
# "type": "mcp",
|
||||
# "url": "http://192.168.1.100:8080/mcp",
|
||||
# "description": "机场软件运行状态查询",
|
||||
# "timeout": 30,
|
||||
# "health_timeout": 5,
|
||||
# "max_retries": 3,
|
||||
# "recovery_check_interval": 60
|
||||
# },
|
||||
|
||||
# 远程 LangGraph 智能体
|
||||
# {
|
||||
# "name": "data_analyst",
|
||||
# "type": "langgraph",
|
||||
# "url": "http://192.168.1.200:2024",
|
||||
# "assistant_id": "data_analyst",
|
||||
# "description": "数据分析专家",
|
||||
# "timeout": 60,
|
||||
# "health_timeout": 5,
|
||||
# "max_retries": 3,
|
||||
# "recovery_check_interval": 120
|
||||
# },
|
||||
|
||||
# 通用 HTTP API
|
||||
# {
|
||||
# "name": "log_collector",
|
||||
# "type": "http",
|
||||
# "url": "http://192.168.1.50:3000/api/query",
|
||||
# "health_url": "http://192.168.1.50:3000/health",
|
||||
# "description": "日志收集与检索服务",
|
||||
# "timeout": 30,
|
||||
# "health_timeout": 5,
|
||||
# "max_retries": 3,
|
||||
# "recovery_check_interval": 60
|
||||
# }
|
||||
@@ -0,0 +1,84 @@
|
||||
# 远端子智能体配置说明
|
||||
|
||||
## 配置文件: `remote_skills.json`
|
||||
|
||||
放在项目根目录(与 `mcp_config.json` 同级),格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"name": "skill_name", // 唯一名称,会生成 delegate_to_skill_name 工具
|
||||
"type": "http", // http / langgraph / mcp
|
||||
"url": "http://host:port/path",
|
||||
"health_url": "http://host:port/health", // 可选,健康检查端点
|
||||
"description": "描述", // 给 LLM 看的工具描述
|
||||
"timeout": 30, // 调用超时(秒)
|
||||
"health_timeout": 5, // 健康检查超时(秒)
|
||||
"max_retries": 3, // 连续失败多少次后标记不可用
|
||||
"recovery_check_interval": 60 // 不可用后多久重试健康检查(秒)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 三种连接方式
|
||||
|
||||
### 1. HTTP API
|
||||
```json
|
||||
{
|
||||
"name": "log_collector",
|
||||
"type": "http",
|
||||
"url": "http://192.168.1.50:3000/api/query",
|
||||
"health_url": "http://192.168.1.50:3000/health",
|
||||
"description": "日志收集与检索服务"
|
||||
}
|
||||
```
|
||||
- POST `url` 发送 `{"instruction": "..."}`
|
||||
- 返回 `{"result": "..."}` 或 `{"content": "..."}`
|
||||
- health_url 返回 200-399 即为健康
|
||||
|
||||
### 2. LangGraph Remote
|
||||
```json
|
||||
{
|
||||
"name": "data_analyst",
|
||||
"type": "langgraph",
|
||||
"url": "http://192.168.1.200:2024",
|
||||
"assistant_id": "data_analyst",
|
||||
"description": "数据分析专家"
|
||||
}
|
||||
```
|
||||
- 通过 langgraph-sdk 调用远程 LangGraph 服务
|
||||
- 需要 `pip install langgraph-sdk`
|
||||
- 健康检查调用 `/ok` 端点
|
||||
|
||||
### 3. MCP Server
|
||||
```json
|
||||
{
|
||||
"name": "airport_monitor",
|
||||
"type": "mcp",
|
||||
"url": "http://192.168.1.100:8080/mcp",
|
||||
"description": "机场软件运行状态查询"
|
||||
}
|
||||
```
|
||||
- 通过 langchain-mcp-adapters 连接 MCP server
|
||||
- 健康检查:尝试获取 tools 列表
|
||||
- 成功则返回可用工具,失败则标记不可用
|
||||
|
||||
## 健康检查机制
|
||||
|
||||
| 状态 | 行为 |
|
||||
|------|------|
|
||||
| 健康 | 正常调用,30秒内不重复检查 |
|
||||
| 首次失败 | 重试,每次调用前检查 |
|
||||
| 连续失败 ≥ max_retries | 标记不可用,返回"当前不可用" |
|
||||
| 不可用后 recovery_check_interval 秒 | 重新检查,成功则自动恢复 |
|
||||
|
||||
## 使用方式
|
||||
|
||||
1. 配置 `remote_skills.json`
|
||||
2. 重启服务
|
||||
3. 主 Agent 工具列表自动增加 `delegate_to_<skill_name>` 工具
|
||||
4. LLM 根据描述自动选择合适的远端技能
|
||||
5. 技能不可用时返回"该功能当前不可用"
|
||||
6. 恢复后自动重新可用
|
||||
@@ -0,0 +1,119 @@
|
||||
aiosqlite==0.22.1
|
||||
altgraph==0.17.5
|
||||
annotated-types==0.7.0
|
||||
anthropic==0.100.0
|
||||
anyio==4.13.0
|
||||
APScheduler==3.11.2
|
||||
attrs==26.1.0
|
||||
blockbuster==1.5.26
|
||||
certifi==2026.4.22
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.7
|
||||
click==8.3.3
|
||||
cloudpickle==3.1.2
|
||||
colorama==0.4.6
|
||||
croniter==6.2.2
|
||||
cryptography==46.0.7
|
||||
defusedxml==0.7.1
|
||||
distro==1.9.0
|
||||
docstring_parser==0.18.0
|
||||
drawpyo==0.2.5
|
||||
et_xmlfile==2.0.0
|
||||
fonttools==4.63.0
|
||||
fpdf2==2.8.7
|
||||
googleapis-common-protos==1.74.0
|
||||
grpcio==1.78.0
|
||||
grpcio-health-checking==1.78.0
|
||||
grpcio-tools==1.78.0
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
httpx-sse==0.4.3
|
||||
idna==3.13
|
||||
importlib_metadata==8.7.1
|
||||
iniconfig==2.3.0
|
||||
jiter==0.14.0
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.1.1
|
||||
jsonschema==4.26.0
|
||||
jsonschema-specifications==2025.9.1
|
||||
jsonschema_rs==0.44.1
|
||||
langchain==1.2.17
|
||||
langchain-anthropic==1.4.3
|
||||
langchain-core==1.3.3
|
||||
langchain-mcp-adapters==0.2.2
|
||||
langchain-openai==1.2.1
|
||||
langchain-protocol==0.0.15
|
||||
langgraph==1.1.10
|
||||
langgraph-api==0.8.7
|
||||
langgraph-checkpoint==4.0.3
|
||||
langgraph-checkpoint-sqlite==3.0.3
|
||||
langgraph-cli==0.4.24
|
||||
langgraph-prebuilt==1.0.13
|
||||
langgraph-runtime-inmem==0.28.0
|
||||
langgraph-sdk==0.3.14
|
||||
langsmith==0.8.2
|
||||
lxml==6.1.0
|
||||
mcp==1.27.0
|
||||
openai==2.35.1
|
||||
openpyxl==3.1.5
|
||||
opentelemetry-api==1.41.1
|
||||
opentelemetry-exporter-otlp-proto-common==1.41.1
|
||||
opentelemetry-exporter-otlp-proto-http==1.41.1
|
||||
opentelemetry-proto==1.41.1
|
||||
opentelemetry-sdk==1.41.1
|
||||
opentelemetry-semantic-conventions==0.62b1
|
||||
orjson==3.11.9
|
||||
ormsgpack==1.12.2
|
||||
packaging==26.2
|
||||
pathspec==1.1.1
|
||||
pefile==2024.8.26
|
||||
pillow==12.2.0
|
||||
pluggy==1.6.0
|
||||
protobuf==6.33.6
|
||||
psutil==7.2.2
|
||||
pycparser==3.0
|
||||
pydantic==2.13.4
|
||||
pydantic-settings==2.14.0
|
||||
pydantic_core==2.46.4
|
||||
Pygments==2.20.0
|
||||
pyinstaller==6.20.0
|
||||
pyinstaller-hooks-contrib==2026.5
|
||||
PyJWT==2.12.1
|
||||
pypdf==6.12.2
|
||||
pytest==9.0.3
|
||||
python-dateutil==2.9.0.post0
|
||||
python-docx==1.2.0
|
||||
python-dotenv==1.2.2
|
||||
python-multipart==0.0.27
|
||||
python-pptx==1.0.2
|
||||
PyYAML==6.0.3
|
||||
referencing==0.37.0
|
||||
regex==2026.4.4
|
||||
requests==2.33.1
|
||||
requests-toolbelt==1.0.0
|
||||
rpds-py==0.30.0
|
||||
ruff==0.15.12
|
||||
setuptools==82.0.1
|
||||
# Editable Git install with no remote (simple-agent-template==0.1.0)
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
sqlite-vec==0.1.9
|
||||
sse-starlette==3.3.4
|
||||
starlette==1.0.0
|
||||
structlog==25.5.0
|
||||
tenacity==9.1.4
|
||||
tqdm==4.67.3
|
||||
truststore==0.10.4
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2026.2
|
||||
tzlocal==5.3.1
|
||||
urllib3==2.6.3
|
||||
uuid_utils==0.14.1
|
||||
uvicorn==0.46.0
|
||||
watchfiles==1.1.1
|
||||
xlsxwriter==3.2.9
|
||||
xxhash==3.7.0
|
||||
zipp==3.23.1
|
||||
zstandard==0.25.0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user