Spaces:
Paused
Paused
Upload 6 files
Browse files- Dockerfile +20 -0
- chat_history.html +1712 -0
- docker-compose.yml +27 -0
- gemini.py +0 -0
- index.html +2025 -0
- requirements.txt +15 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 5 |
+
PYTHONUNBUFFERED=1 \
|
| 6 |
+
PIP_NO_CACHE_DIR=1
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
EXPOSE 8000
|
| 16 |
+
|
| 17 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 18 |
+
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5)"
|
| 19 |
+
|
| 20 |
+
CMD ["python", "-u", "gemini.py"]
|
chat_history.html
ADDED
|
@@ -0,0 +1,1712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Business Gemini 智能对话</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* ==================== 复用原有 CSS 变量 ==================== */
|
| 9 |
+
:root {
|
| 10 |
+
--primary: #2563eb;
|
| 11 |
+
--primary-hover: #1d4ed8;
|
| 12 |
+
--primary-light: rgba(37, 99, 235, 0.1);
|
| 13 |
+
--success: #10b981;
|
| 14 |
+
--danger: #ef4444;
|
| 15 |
+
--warning: #f59e0b;
|
| 16 |
+
--radius: 8px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
[data-theme="light"] {
|
| 20 |
+
--bg-color: #f1f5f9;
|
| 21 |
+
--card-bg: #ffffff;
|
| 22 |
+
--text-main: #1e293b;
|
| 23 |
+
--text-muted: #64748b;
|
| 24 |
+
--border: #e2e8f0;
|
| 25 |
+
--hover-bg: #f8fafc;
|
| 26 |
+
--input-bg: #ffffff;
|
| 27 |
+
--bubble-user: #2563eb;
|
| 28 |
+
--bubble-ai: #ffffff;
|
| 29 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 30 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 31 |
+
--bg-gradient: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
[data-theme="dark"] {
|
| 35 |
+
--bg-color: #0f172a;
|
| 36 |
+
--card-bg: #1e293b;
|
| 37 |
+
--text-main: #e2e8f0;
|
| 38 |
+
--text-muted: #94a3b8;
|
| 39 |
+
--border: #334155;
|
| 40 |
+
--hover-bg: rgba(255, 255, 255, 0.05);
|
| 41 |
+
--input-bg: #0f172a;
|
| 42 |
+
--bubble-user: #2563eb;
|
| 43 |
+
--bubble-ai: #1e293b;
|
| 44 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
| 45 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
| 46 |
+
--bg-gradient: radial-gradient(circle at 10% 20%, rgba(37, 99, 235, 0.1) 0%, transparent 20%);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
* { margin: 0; padding: 0; box-sizing: border-box; transition: background-color 0.3s, border-color 0.3s; }
|
| 50 |
+
|
| 51 |
+
body {
|
| 52 |
+
font-family: 'Inter', -apple-system, sans-serif;
|
| 53 |
+
background-color: var(--bg-color);
|
| 54 |
+
background-image: var(--bg-gradient);
|
| 55 |
+
color: var(--text-main);
|
| 56 |
+
height: 100vh;
|
| 57 |
+
display: flex;
|
| 58 |
+
flex-direction: column;
|
| 59 |
+
overflow: hidden;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* ==================== 布局结构 ==================== */
|
| 63 |
+
.header {
|
| 64 |
+
padding: 20px 30px;
|
| 65 |
+
background: rgba(255, 255, 255, 0.8);
|
| 66 |
+
backdrop-filter: blur(10px);
|
| 67 |
+
border-bottom: 1px solid var(--border);
|
| 68 |
+
display: flex;
|
| 69 |
+
justify-content: space-between;
|
| 70 |
+
align-items: center;
|
| 71 |
+
flex-shrink: 0;
|
| 72 |
+
z-index: 10;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
[data-theme="dark"] .header { background: rgba(15, 23, 42, 0.8); }
|
| 76 |
+
|
| 77 |
+
.header h1 {
|
| 78 |
+
font-size: 20px;
|
| 79 |
+
font-weight: 700;
|
| 80 |
+
display: flex;
|
| 81 |
+
align-items: center;
|
| 82 |
+
gap: 10px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.header h1::before {
|
| 86 |
+
content: ''; width: 4px; height: 20px; background: var(--primary); border-radius: 2px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.header-controls {
|
| 90 |
+
display: flex;
|
| 91 |
+
gap: 10px;
|
| 92 |
+
align-items: center;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.chat-container {
|
| 96 |
+
flex: 1;
|
| 97 |
+
max-width: 1000px;
|
| 98 |
+
width: 100%;
|
| 99 |
+
margin: 0 auto;
|
| 100 |
+
padding: 30px 20px 120px 20px;
|
| 101 |
+
overflow-y: auto;
|
| 102 |
+
scroll-behavior: smooth;
|
| 103 |
+
display: flex;
|
| 104 |
+
flex-direction: column;
|
| 105 |
+
gap: 24px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* ==================== 对话气泡样式 ==================== */
|
| 109 |
+
.message-row {
|
| 110 |
+
display: flex;
|
| 111 |
+
align-items: flex-start;
|
| 112 |
+
gap: 16px;
|
| 113 |
+
animation: slideIn 0.3s ease-out;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
@keyframes slideIn {
|
| 117 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 118 |
+
to { opacity: 1; transform: translateY(0); }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.message-row.user {
|
| 122 |
+
flex-direction: row-reverse;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.avatar {
|
| 126 |
+
width: 40px;
|
| 127 |
+
height: 40px;
|
| 128 |
+
border-radius: 50%;
|
| 129 |
+
display: flex;
|
| 130 |
+
align-items: center;
|
| 131 |
+
justify-content: center;
|
| 132 |
+
font-size: 18px;
|
| 133 |
+
flex-shrink: 0;
|
| 134 |
+
box-shadow: var(--shadow-sm);
|
| 135 |
+
background-color: var(--card-bg);
|
| 136 |
+
border: 1px solid var(--border);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.avatar.ai { color: var(--primary); background: var(--primary-light); border: none; }
|
| 140 |
+
.avatar.user { background: var(--card-bg); color: var(--text-muted); }
|
| 141 |
+
|
| 142 |
+
.message-content {
|
| 143 |
+
display: flex;
|
| 144 |
+
flex-direction: column;
|
| 145 |
+
max-width: 70%;
|
| 146 |
+
gap: 4px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.message-row.user .message-content {
|
| 150 |
+
align-items: flex-end;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.bubble {
|
| 154 |
+
padding: 12px 16px;
|
| 155 |
+
border-radius: 12px;
|
| 156 |
+
font-size: 14px;
|
| 157 |
+
line-height: 1.6;
|
| 158 |
+
position: relative;
|
| 159 |
+
box-shadow: var(--shadow-sm);
|
| 160 |
+
word-wrap: break-word;
|
| 161 |
+
white-space: pre-wrap;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.message-row.user .bubble {
|
| 165 |
+
background: var(--bubble-user);
|
| 166 |
+
color: white;
|
| 167 |
+
border-top-right-radius: 2px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.message-row.ai .bubble {
|
| 171 |
+
background: var(--bubble-ai);
|
| 172 |
+
color: var(--text-main);
|
| 173 |
+
border: 1px solid var(--border);
|
| 174 |
+
border-top-left-radius: 2px;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.timestamp {
|
| 178 |
+
font-size: 11px;
|
| 179 |
+
color: var(--text-muted);
|
| 180 |
+
margin: 0 4px;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* ==================== 模型选择器 ==================== */
|
| 184 |
+
/* 主布局容器 */
|
| 185 |
+
.main-container {
|
| 186 |
+
display: flex;
|
| 187 |
+
flex: 1;
|
| 188 |
+
overflow: hidden;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* 左侧会话列表 */
|
| 192 |
+
.session-sidebar {
|
| 193 |
+
width: 260px;
|
| 194 |
+
background: var(--bg-secondary);
|
| 195 |
+
border-right: 1px solid var(--border);
|
| 196 |
+
display: flex;
|
| 197 |
+
flex-direction: column;
|
| 198 |
+
flex-shrink: 0;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.session-header {
|
| 202 |
+
padding: 16px;
|
| 203 |
+
border-bottom: 1px solid var(--border);
|
| 204 |
+
display: flex;
|
| 205 |
+
justify-content: space-between;
|
| 206 |
+
align-items: center;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.session-header h3 {
|
| 210 |
+
margin: 0;
|
| 211 |
+
font-size: 14px;
|
| 212 |
+
color: var(--text-main);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.new-session-btn {
|
| 216 |
+
background: var(--primary);
|
| 217 |
+
color: white;
|
| 218 |
+
border: none;
|
| 219 |
+
padding: 6px 12px;
|
| 220 |
+
border-radius: 6px;
|
| 221 |
+
cursor: pointer;
|
| 222 |
+
font-size: 13px;
|
| 223 |
+
transition: background 0.2s;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.new-session-btn:hover {
|
| 227 |
+
background: var(--primary-dark);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.session-list {
|
| 231 |
+
flex: 1;
|
| 232 |
+
overflow-y: auto;
|
| 233 |
+
padding: 8px;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.session-item {
|
| 237 |
+
padding: 12px;
|
| 238 |
+
border-radius: 8px;
|
| 239 |
+
cursor: pointer;
|
| 240 |
+
margin-bottom: 4px;
|
| 241 |
+
display: flex;
|
| 242 |
+
justify-content: space-between;
|
| 243 |
+
align-items: center;
|
| 244 |
+
transition: background 0.2s;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.session-item:hover {
|
| 248 |
+
background: var(--bg-main);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.session-item.active {
|
| 252 |
+
background: var(--primary-light);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.session-name {
|
| 256 |
+
flex: 1;
|
| 257 |
+
overflow: hidden;
|
| 258 |
+
text-overflow: ellipsis;
|
| 259 |
+
white-space: nowrap;
|
| 260 |
+
font-size: 14px;
|
| 261 |
+
color: var(--text-main);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.session-actions {
|
| 265 |
+
display: flex;
|
| 266 |
+
gap: 4px;
|
| 267 |
+
visibility: hidden;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.session-item:hover .session-actions {
|
| 271 |
+
visibility: visible;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.session-action-btn {
|
| 275 |
+
background: none;
|
| 276 |
+
border: none;
|
| 277 |
+
cursor: pointer;
|
| 278 |
+
padding: 4px;
|
| 279 |
+
font-size: 12px;
|
| 280 |
+
opacity: 0.6;
|
| 281 |
+
transition: opacity 0.2s;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.session-action-btn:hover {
|
| 285 |
+
opacity: 1;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* 聊天主区域 */
|
| 289 |
+
.chat-main {
|
| 290 |
+
flex: 1;
|
| 291 |
+
display: flex;
|
| 292 |
+
flex-direction: column;
|
| 293 |
+
overflow: hidden;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.model-selector {
|
| 297 |
+
display: flex;
|
| 298 |
+
align-items: center;
|
| 299 |
+
gap: 8px;
|
| 300 |
+
font-size: 13px;
|
| 301 |
+
color: var(--text-muted);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.model-selector label {
|
| 305 |
+
white-space: nowrap;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.model-selector select {
|
| 309 |
+
padding: 6px 12px;
|
| 310 |
+
border-radius: 8px;
|
| 311 |
+
border: 1px solid var(--border);
|
| 312 |
+
background: var(--bg-main);
|
| 313 |
+
color: var(--text-main);
|
| 314 |
+
font-size: 13px;
|
| 315 |
+
cursor: pointer;
|
| 316 |
+
outline: none;
|
| 317 |
+
min-width: 150px;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.model-selector select:hover {
|
| 321 |
+
border-color: var(--primary);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.model-selector select:focus {
|
| 325 |
+
border-color: var(--primary);
|
| 326 |
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/* ==================== 模式切换开关 ==================== */
|
| 330 |
+
.mode-switch {
|
| 331 |
+
display: flex;
|
| 332 |
+
align-items: center;
|
| 333 |
+
gap: 8px;
|
| 334 |
+
font-size: 13px;
|
| 335 |
+
color: var(--text-muted);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.switch {
|
| 339 |
+
position: relative;
|
| 340 |
+
width: 44px;
|
| 341 |
+
height: 24px;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.switch input {
|
| 345 |
+
opacity: 0;
|
| 346 |
+
width: 0;
|
| 347 |
+
height: 0;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.slider {
|
| 351 |
+
position: absolute;
|
| 352 |
+
cursor: pointer;
|
| 353 |
+
top: 0;
|
| 354 |
+
left: 0;
|
| 355 |
+
right: 0;
|
| 356 |
+
bottom: 0;
|
| 357 |
+
background-color: var(--border);
|
| 358 |
+
transition: 0.3s;
|
| 359 |
+
border-radius: 24px;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.slider:before {
|
| 363 |
+
position: absolute;
|
| 364 |
+
content: "";
|
| 365 |
+
height: 18px;
|
| 366 |
+
width: 18px;
|
| 367 |
+
left: 3px;
|
| 368 |
+
bottom: 3px;
|
| 369 |
+
background-color: white;
|
| 370 |
+
transition: 0.3s;
|
| 371 |
+
border-radius: 50%;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
input:checked + .slider {
|
| 375 |
+
background-color: var(--primary);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
input:checked + .slider:before {
|
| 379 |
+
transform: translateX(20px);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
/* 在CSS部分添加文件上传相关样式,在 .error-message 样式后面添加 -->
|
| 383 |
+
.error-message {
|
| 384 |
+
background: rgba(239, 68, 68, 0.1);
|
| 385 |
+
border: 1px solid var(--danger);
|
| 386 |
+
color: var(--danger);
|
| 387 |
+
padding: 8px 12px;
|
| 388 |
+
border-radius: 8px;
|
| 389 |
+
font-size: 13px;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
/* 底部输入区 */
|
| 393 |
+
.input-area {
|
| 394 |
+
position: fixed;
|
| 395 |
+
bottom: 0;
|
| 396 |
+
left: 0;
|
| 397 |
+
right: 0;
|
| 398 |
+
background: var(--card-bg);
|
| 399 |
+
border-top: 1px solid var(--border);
|
| 400 |
+
padding: 16px 20px;
|
| 401 |
+
z-index: 100;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.input-wrapper {
|
| 405 |
+
max-width: 1000px;
|
| 406 |
+
margin: 0 auto;
|
| 407 |
+
display: flex;
|
| 408 |
+
gap: 12px;
|
| 409 |
+
align-items: flex-end;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.input-wrapper textarea {
|
| 413 |
+
flex: 1;
|
| 414 |
+
padding: 12px 16px;
|
| 415 |
+
border: 1px solid var(--border);
|
| 416 |
+
border-radius: 20px;
|
| 417 |
+
background: var(--input-bg);
|
| 418 |
+
color: var(--text-main);
|
| 419 |
+
font-size: 15px;
|
| 420 |
+
resize: none;
|
| 421 |
+
min-height: 44px;
|
| 422 |
+
max-height: 120px;
|
| 423 |
+
outline: none;
|
| 424 |
+
transition: border-color 0.2s;
|
| 425 |
+
font-family: inherit;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.input-wrapper textarea:focus {
|
| 429 |
+
border-color: var(--primary);
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.input-wrapper textarea::placeholder {
|
| 433 |
+
color: var(--text-muted);
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.send-btn {
|
| 437 |
+
width: 50px;
|
| 438 |
+
height: 50px;
|
| 439 |
+
border-radius: 12px;
|
| 440 |
+
border: none;
|
| 441 |
+
background: var(--primary);
|
| 442 |
+
color: white;
|
| 443 |
+
font-size: 18px;
|
| 444 |
+
cursor: pointer;
|
| 445 |
+
display: flex;
|
| 446 |
+
align-items: center;
|
| 447 |
+
justify-content: center;
|
| 448 |
+
transition: all 0.2s;
|
| 449 |
+
flex-shrink: 0;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.send-btn:hover:not(:disabled) {
|
| 453 |
+
background: var(--primary-hover);
|
| 454 |
+
transform: scale(1.05);
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.send-btn:disabled {
|
| 458 |
+
opacity: 0.6;
|
| 459 |
+
cursor: not-allowed;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
/* 主题切换按钮 */
|
| 463 |
+
.theme-toggle {
|
| 464 |
+
background: var(--card-bg);
|
| 465 |
+
border: 1px solid var(--border);
|
| 466 |
+
border-radius: 8px;
|
| 467 |
+
padding: 8px 12px;
|
| 468 |
+
cursor: pointer;
|
| 469 |
+
font-size: 14px;
|
| 470 |
+
color: var(--text-main);
|
| 471 |
+
transition: all 0.2s;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.theme-toggle:hover {
|
| 475 |
+
background: var(--hover-bg);
|
| 476 |
+
border-color: var(--primary);
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
/* 加载动画 */
|
| 480 |
+
.typing-indicator {
|
| 481 |
+
display: flex;
|
| 482 |
+
gap: 4px;
|
| 483 |
+
padding: 12px 16px;
|
| 484 |
+
background: var(--bubble-ai);
|
| 485 |
+
border: 1px solid var(--border);
|
| 486 |
+
border-radius: 12px;
|
| 487 |
+
border-top-left-radius: 2px;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.typing-indicator span {
|
| 491 |
+
width: 8px;
|
| 492 |
+
height: 8px;
|
| 493 |
+
background: var(--text-muted);
|
| 494 |
+
border-radius: 50%;
|
| 495 |
+
animation: typing 1.4s infinite ease-in-out;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
| 499 |
+
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
| 500 |
+
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
| 501 |
+
|
| 502 |
+
@keyframes typing {
|
| 503 |
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.6; }
|
| 504 |
+
30% { transform: translateY(-10px); opacity: 1; }
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
/* ==================== 文件上传相关样式 ==================== */
|
| 508 |
+
.file-upload-btn {
|
| 509 |
+
width: 60px;
|
| 510 |
+
height: 60px;
|
| 511 |
+
border-radius: 12px;
|
| 512 |
+
background: var(--card-bg);
|
| 513 |
+
color: var(--text-main);
|
| 514 |
+
border: 1px solid var(--border);
|
| 515 |
+
cursor: pointer;
|
| 516 |
+
display: flex;
|
| 517 |
+
align-items: center;
|
| 518 |
+
justify-content: center;
|
| 519 |
+
font-size: 20px;
|
| 520 |
+
transition: transform 0.2s, background 0.2s, border-color 0.2s;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.file-upload-btn:hover {
|
| 524 |
+
background: var(--hover-bg);
|
| 525 |
+
border-color: var(--primary);
|
| 526 |
+
transform: scale(1.05);
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.file-upload-btn:active {
|
| 530 |
+
transform: scale(0.95);
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.file-upload-btn:disabled {
|
| 534 |
+
background: var(--hover-bg);
|
| 535 |
+
cursor: not-allowed;
|
| 536 |
+
transform: none;
|
| 537 |
+
opacity: 0.6;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.file-upload-btn.has-files {
|
| 541 |
+
background: var(--primary-light);
|
| 542 |
+
border-color: var(--primary);
|
| 543 |
+
color: var(--primary);
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
#fileInput {
|
| 547 |
+
display: none;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.uploaded-files-container {
|
| 551 |
+
max-width: 1000px;
|
| 552 |
+
width: 100%;
|
| 553 |
+
margin: 0 auto 10px auto;
|
| 554 |
+
padding: 0 20px;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.uploaded-files {
|
| 558 |
+
display: flex;
|
| 559 |
+
flex-wrap: wrap;
|
| 560 |
+
gap: 8px;
|
| 561 |
+
padding: 10px;
|
| 562 |
+
background: var(--hover-bg);
|
| 563 |
+
border-radius: 12px;
|
| 564 |
+
border: 1px solid var(--border);
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.file-tag {
|
| 568 |
+
display: flex;
|
| 569 |
+
align-items: center;
|
| 570 |
+
gap: 6px;
|
| 571 |
+
padding: 6px 12px;
|
| 572 |
+
background: var(--card-bg);
|
| 573 |
+
border: 1px solid var(--border);
|
| 574 |
+
border-radius: 20px;
|
| 575 |
+
font-size: 13px;
|
| 576 |
+
color: var(--text-main);
|
| 577 |
+
animation: fadeIn 0.3s ease;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
@keyframes fadeIn {
|
| 581 |
+
from { opacity: 0; transform: scale(0.9); }
|
| 582 |
+
to { opacity: 1; transform: scale(1); }
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.file-tag .file-icon {
|
| 586 |
+
font-size: 14px;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.file-tag .file-name {
|
| 590 |
+
max-width: 150px;
|
| 591 |
+
overflow: hidden;
|
| 592 |
+
text-overflow: ellipsis;
|
| 593 |
+
white-space: nowrap;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.file-tag .remove-file {
|
| 597 |
+
width: 18px;
|
| 598 |
+
height: 18px;
|
| 599 |
+
border-radius: 50%;
|
| 600 |
+
background: var(--danger);
|
| 601 |
+
color: white;
|
| 602 |
+
border: none;
|
| 603 |
+
cursor: pointer;
|
| 604 |
+
display: flex;
|
| 605 |
+
align-items: center;
|
| 606 |
+
justify-content: center;
|
| 607 |
+
font-size: 12px;
|
| 608 |
+
line-height: 1;
|
| 609 |
+
padding: 0;
|
| 610 |
+
transition: transform 0.2s;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.file-tag .remove-file:hover {
|
| 614 |
+
transform: scale(1.1);
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.file-uploading {
|
| 618 |
+
opacity: 0.6;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.file-uploading .file-name::after {
|
| 622 |
+
content: ' (上传中...)';
|
| 623 |
+
color: var(--text-muted);
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.upload-progress {
|
| 627 |
+
position: absolute;
|
| 628 |
+
bottom: 0;
|
| 629 |
+
left: 0;
|
| 630 |
+
height: 3px;
|
| 631 |
+
background: var(--primary);
|
| 632 |
+
border-radius: 0 0 20px 20px;
|
| 633 |
+
transition: width 0.3s;
|
| 634 |
+
}
|
| 635 |
+
</style>
|
| 636 |
+
</head>
|
| 637 |
+
<body>
|
| 638 |
+
|
| 639 |
+
<!-- 顶部栏 -->
|
| 640 |
+
<div class="header">
|
| 641 |
+
<h1>Business Gemini <span style="font-weight: 400; color: var(--text-muted); font-size: 0.8em; margin-left: 8px;">智能对话</span></h1>
|
| 642 |
+
<div class="header-controls">
|
| 643 |
+
<div class="model-selector">
|
| 644 |
+
<label for="modelSelect">模型:</label>
|
| 645 |
+
<select id="modelSelect">
|
| 646 |
+
<option value="gemini-enterprise">加载中...</option>
|
| 647 |
+
</select>
|
| 648 |
+
</div>
|
| 649 |
+
<div class="model-selector">
|
| 650 |
+
<label for="accountSelect">指定账号:</label>
|
| 651 |
+
<select id="accountSelect">
|
| 652 |
+
<option value="">自动轮询</option>
|
| 653 |
+
</select>
|
| 654 |
+
</div>
|
| 655 |
+
<div class="mode-switch">
|
| 656 |
+
<span>非流式</span>
|
| 657 |
+
<label class="switch">
|
| 658 |
+
<input type="checkbox" id="streamMode" checked>
|
| 659 |
+
<span class="slider"></span>
|
| 660 |
+
</label>
|
| 661 |
+
<span>流式</span>
|
| 662 |
+
</div>
|
| 663 |
+
<button class="theme-toggle clear" onclick="clearChat()" title="清空对话">
|
| 664 |
+
🗑️ 清空
|
| 665 |
+
</button>
|
| 666 |
+
<button class="theme-toggle home" onclick="window.location.href='./'" title="返回首页">
|
| 667 |
+
<span>🏠 返回首页</span>
|
| 668 |
+
</button>
|
| 669 |
+
<button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
|
| 670 |
+
<span id="themeIcon">☀️</span>
|
| 671 |
+
</button>
|
| 672 |
+
</div>
|
| 673 |
+
</div>
|
| 674 |
+
|
| 675 |
+
<!-- 主容器 -->
|
| 676 |
+
<div class="main-container">
|
| 677 |
+
<!-- 左侧会话列表 -->
|
| 678 |
+
<div class="session-sidebar">
|
| 679 |
+
<div class="session-header">
|
| 680 |
+
<h3>会话列表</h3>
|
| 681 |
+
<button class="new-session-btn" onclick="createNewSession()">+ 新建</button>
|
| 682 |
+
</div>
|
| 683 |
+
<div class="session-list" id="sessionList">
|
| 684 |
+
<!-- 会话项会动态插入 -->
|
| 685 |
+
</div>
|
| 686 |
+
</div>
|
| 687 |
+
|
| 688 |
+
<!-- 聊天主区域 -->
|
| 689 |
+
<div class="chat-main">
|
| 690 |
+
<!-- 聊天内容区 -->
|
| 691 |
+
<div class="chat-container" id="chatContainer">
|
| 692 |
+
<!-- 消息会通过 JS 动态插入到这里 -->
|
| 693 |
+
</div>
|
| 694 |
+
|
| 695 |
+
<!-- 修改底部输入区,添加文件上传按钮 -->
|
| 696 |
+
<div class="input-area">
|
| 697 |
+
<div class="uploaded-files-container" id="uploadedFilesContainer" style="display: none;">
|
| 698 |
+
<div class="uploaded-files" id="uploadedFiles">
|
| 699 |
+
<!-- 已上传的文件标签会动态插入这里 -->
|
| 700 |
+
</div>
|
| 701 |
+
</div>
|
| 702 |
+
<div class="input-wrapper">
|
| 703 |
+
<input type="file" id="fileInput" multiple accept="*">
|
| 704 |
+
<button class="file-upload-btn" id="uploadBtn" onclick="document.getElementById('fileInput').click()" title="上传文件">
|
| 705 |
+
📎
|
| 706 |
+
</button>
|
| 707 |
+
<textarea id="userInput" placeholder="输入消息与 Business Gemini 对话..." onkeydown="handleKeyDown(event)"></textarea>
|
| 708 |
+
<button class="send-btn" id="sendBtn" onclick="sendMessage()">➤</button>
|
| 709 |
+
</div>
|
| 710 |
+
</div>
|
| 711 |
+
</div>
|
| 712 |
+
</div>
|
| 713 |
+
|
| 714 |
+
<script>
|
| 715 |
+
console.log('JavaScript 开始加载...');
|
| 716 |
+
// API 基础 URL
|
| 717 |
+
const API_BASE = '.';
|
| 718 |
+
|
| 719 |
+
// ==================== 全局状态 ====================
|
| 720 |
+
let chatHistory = [];
|
| 721 |
+
let isLoading = false;
|
| 722 |
+
let currentAIBubble = null;
|
| 723 |
+
let abortController = null;
|
| 724 |
+
let uploadedFiles = []; // 存储已上传的文件信息 {id, name, gemini_file_id}
|
| 725 |
+
|
| 726 |
+
// ==================== 会话管理状态 ====================
|
| 727 |
+
let sessions = []; // 所有会话列表
|
| 728 |
+
let currentSessionId = null; // 当前会话ID
|
| 729 |
+
const SESSIONS_STORAGE_KEY = 'chat_sessions';
|
| 730 |
+
const CURRENT_SESSION_KEY = 'current_session_id';
|
| 731 |
+
|
| 732 |
+
// ==================== 会话管理函数 ====================
|
| 733 |
+
// 生成唯一ID
|
| 734 |
+
function generateId() {
|
| 735 |
+
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
// 加载所有会话
|
| 739 |
+
function loadSessions() {
|
| 740 |
+
try {
|
| 741 |
+
const saved = localStorage.getItem(SESSIONS_STORAGE_KEY);
|
| 742 |
+
sessions = saved ? JSON.parse(saved) : [];
|
| 743 |
+
currentSessionId = localStorage.getItem(CURRENT_SESSION_KEY);
|
| 744 |
+
|
| 745 |
+
// 如果没有会话,创建一个默认会话
|
| 746 |
+
if (sessions.length === 0) {
|
| 747 |
+
createNewSession(true);
|
| 748 |
+
} else {
|
| 749 |
+
// 如果当前会话ID无效,选择第一个会话
|
| 750 |
+
if (!currentSessionId || !sessions.find(s => s.id === currentSessionId)) {
|
| 751 |
+
currentSessionId = sessions[0].id;
|
| 752 |
+
localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
renderSessionList();
|
| 757 |
+
loadCurrentSessionHistory();
|
| 758 |
+
} catch (error) {
|
| 759 |
+
console.error('加载会话失败:', error);
|
| 760 |
+
sessions = [];
|
| 761 |
+
createNewSession(true);
|
| 762 |
+
}
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
// 保存所有会话
|
| 766 |
+
function saveSessions() {
|
| 767 |
+
try {
|
| 768 |
+
localStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(sessions));
|
| 769 |
+
localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
|
| 770 |
+
} catch (error) {
|
| 771 |
+
console.error('保存会话失败:', error);
|
| 772 |
+
}
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
// 创建新会话
|
| 776 |
+
function createNewSession(isInit = false) {
|
| 777 |
+
const newSession = {
|
| 778 |
+
id: generateId(),
|
| 779 |
+
name: `新会话 ${sessions.length + 1}`,
|
| 780 |
+
history: [],
|
| 781 |
+
createdAt: Date.now(),
|
| 782 |
+
updatedAt: Date.now()
|
| 783 |
+
};
|
| 784 |
+
sessions.unshift(newSession);
|
| 785 |
+
currentSessionId = newSession.id;
|
| 786 |
+
chatHistory = [];
|
| 787 |
+
|
| 788 |
+
if (!isInit) {
|
| 789 |
+
saveSessions();
|
| 790 |
+
renderSessionList();
|
| 791 |
+
renderChatHistory();
|
| 792 |
+
}
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
// 切换会话
|
| 796 |
+
function switchSession(sessionId) {
|
| 797 |
+
if (sessionId === currentSessionId) return;
|
| 798 |
+
|
| 799 |
+
// 保存当前会话的历史
|
| 800 |
+
saveCurrentSessionHistory();
|
| 801 |
+
|
| 802 |
+
currentSessionId = sessionId;
|
| 803 |
+
localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
|
| 804 |
+
|
| 805 |
+
loadCurrentSessionHistory();
|
| 806 |
+
renderSessionList();
|
| 807 |
+
renderChatHistory();
|
| 808 |
+
|
| 809 |
+
// 滚动到底部
|
| 810 |
+
setTimeout(() => {
|
| 811 |
+
const container = document.getElementById('chatContainer');
|
| 812 |
+
container.scrollTop = container.scrollHeight;
|
| 813 |
+
}, 100);
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
// 保存当前会话历史
|
| 817 |
+
function saveCurrentSessionHistory() {
|
| 818 |
+
const session = sessions.find(s => s.id === currentSessionId);
|
| 819 |
+
if (session) {
|
| 820 |
+
session.history = [...chatHistory];
|
| 821 |
+
session.updatedAt = Date.now();
|
| 822 |
+
// 如果有消息,用第一条用户消息作为会话名称
|
| 823 |
+
if (chatHistory.length > 0 && session.name.startsWith('新会话')) {
|
| 824 |
+
const firstUserMsg = chatHistory.find(m => m.role === 'user');
|
| 825 |
+
if (firstUserMsg) {
|
| 826 |
+
const content = typeof firstUserMsg.content === 'string'
|
| 827 |
+
? firstUserMsg.content
|
| 828 |
+
: firstUserMsg.content.find(c => c.type === 'text')?.text || '';
|
| 829 |
+
session.name = content.substring(0, 20) + (content.length > 20 ? '...' : '');
|
| 830 |
+
}
|
| 831 |
+
}
|
| 832 |
+
saveSessions();
|
| 833 |
+
}
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
// 加载当前会话历史
|
| 837 |
+
function loadCurrentSessionHistory() {
|
| 838 |
+
const session = sessions.find(s => s.id === currentSessionId);
|
| 839 |
+
if (session) {
|
| 840 |
+
chatHistory = [...session.history];
|
| 841 |
+
} else {
|
| 842 |
+
chatHistory = [];
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
// 渲染会话列表
|
| 847 |
+
function renderSessionList() {
|
| 848 |
+
const listContainer = document.getElementById('sessionList');
|
| 849 |
+
listContainer.innerHTML = '';
|
| 850 |
+
|
| 851 |
+
sessions.forEach(session => {
|
| 852 |
+
const item = document.createElement('div');
|
| 853 |
+
item.className = `session-item ${session.id === currentSessionId ? 'active' : ''}`;
|
| 854 |
+
item.innerHTML = `
|
| 855 |
+
<span class="session-name" title="${escapeHtml(session.name)}">${escapeHtml(session.name)}</span>
|
| 856 |
+
<div class="session-actions">
|
| 857 |
+
<button class="session-action-btn" onclick="event.stopPropagation(); renameSession('${session.id}')" title="重命名">✏️</button>
|
| 858 |
+
<button class="session-action-btn" onclick="event.stopPropagation(); deleteSession('${session.id}')" title="删除">🗑️</button>
|
| 859 |
+
</div>
|
| 860 |
+
`;
|
| 861 |
+
item.onclick = () => switchSession(session.id);
|
| 862 |
+
listContainer.appendChild(item);
|
| 863 |
+
});
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
// 重命名会话
|
| 867 |
+
function renameSession(sessionId) {
|
| 868 |
+
const session = sessions.find(s => s.id === sessionId);
|
| 869 |
+
if (!session) return;
|
| 870 |
+
|
| 871 |
+
const newName = prompt('请输入新的会话名称:', session.name);
|
| 872 |
+
if (newName && newName.trim()) {
|
| 873 |
+
session.name = newName.trim();
|
| 874 |
+
session.updatedAt = Date.now();
|
| 875 |
+
saveSessions();
|
| 876 |
+
renderSessionList();
|
| 877 |
+
}
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
// 删除会话
|
| 881 |
+
function deleteSession(sessionId) {
|
| 882 |
+
if (sessions.length <= 1) {
|
| 883 |
+
alert('至少需要保留一个会话');
|
| 884 |
+
return;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
if (!confirm('确定要删除这个会话吗?')) return;
|
| 888 |
+
|
| 889 |
+
const index = sessions.findIndex(s => s.id === sessionId);
|
| 890 |
+
if (index === -1) return;
|
| 891 |
+
|
| 892 |
+
sessions.splice(index, 1);
|
| 893 |
+
|
| 894 |
+
// 如果删除的是当前会话,切换到第一个会话
|
| 895 |
+
if (sessionId === currentSessionId) {
|
| 896 |
+
currentSessionId = sessions[0].id;
|
| 897 |
+
loadCurrentSessionHistory();
|
| 898 |
+
renderChatHistory();
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
saveSessions();
|
| 902 |
+
renderSessionList();
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
// ==================== 获取模型列表 ====================
|
| 906 |
+
async function loadModelList() {
|
| 907 |
+
try {
|
| 908 |
+
const response = await fetch(`${API_BASE}/api/models`);
|
| 909 |
+
if (!response.ok) {
|
| 910 |
+
throw new Error('获取模型列表失败');
|
| 911 |
+
}
|
| 912 |
+
const data = await response.json();
|
| 913 |
+
const models = data.models || [];
|
| 914 |
+
|
| 915 |
+
const select = document.getElementById('modelSelect');
|
| 916 |
+
select.innerHTML = ''; // 清空现有选项
|
| 917 |
+
|
| 918 |
+
if (models.length === 0) {
|
| 919 |
+
select.innerHTML = '<option value="gemini-enterprise">gemini-enterprise</option>';
|
| 920 |
+
} else {
|
| 921 |
+
models.forEach(model => {
|
| 922 |
+
const option = document.createElement('option');
|
| 923 |
+
option.value = model.id || model.name;
|
| 924 |
+
option.textContent = model.name || model.id;
|
| 925 |
+
select.appendChild(option);
|
| 926 |
+
});
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
// 从localStorage恢复上次选择的模型
|
| 930 |
+
const savedModel = localStorage.getItem('selectedModel');
|
| 931 |
+
if (savedModel && select.querySelector(`option[value="${savedModel}"]`)) {
|
| 932 |
+
select.value = savedModel;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
// 监听模型选择变化,保存到localStorage
|
| 936 |
+
select.addEventListener('change', () => {
|
| 937 |
+
localStorage.setItem('selectedModel', select.value);
|
| 938 |
+
});
|
| 939 |
+
} catch (error) {
|
| 940 |
+
console.error('加载模型��表失败:', error);
|
| 941 |
+
// 失败时使用默认模型
|
| 942 |
+
const select = document.getElementById('modelSelect');
|
| 943 |
+
select.innerHTML = '<option value="gemini-enterprise">gemini-enterprise</option>';
|
| 944 |
+
}
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
// ==================== 获取当前选中的模型 ====================
|
| 948 |
+
function getSelectedModel() {
|
| 949 |
+
return document.getElementById('modelSelect').value || 'gemini-enterprise';
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
// ==================== 获取账号列表 ====================
|
| 953 |
+
async function loadAccountList() {
|
| 954 |
+
try {
|
| 955 |
+
const response = await fetch(`${API_BASE}/api/accounts`);
|
| 956 |
+
if (!response.ok) throw new Error('获取账号列表失败');
|
| 957 |
+
const data = await response.json();
|
| 958 |
+
const accounts = data.accounts || [];
|
| 959 |
+
const select = document.getElementById('accountSelect');
|
| 960 |
+
select.innerHTML = '<option value="">自动轮询</option>';
|
| 961 |
+
accounts.filter(a => a.available).forEach(account => {
|
| 962 |
+
const option = document.createElement('option');
|
| 963 |
+
option.value = account.id;
|
| 964 |
+
option.textContent = account.csesidx ? `账号${account.id} (${account.csesidx})` : `账号${account.id}`;
|
| 965 |
+
select.appendChild(option);
|
| 966 |
+
});
|
| 967 |
+
} catch (error) {
|
| 968 |
+
console.error('加载账号列表失败:', error);
|
| 969 |
+
}
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
// ==================== 获取当前选中的账号 ====================
|
| 973 |
+
function getSelectedAccount() {
|
| 974 |
+
return document.getElementById('accountSelect').value || null;
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
// ==================== 初始化 ====================
|
| 978 |
+
window.onload = () => {
|
| 979 |
+
console.log('页面加载完成,开始初始化...');
|
| 980 |
+
loadSessions(); // 加载会话列表(会自动加载当前会话历史)
|
| 981 |
+
loadModelList(); // 加载模型列表
|
| 982 |
+
loadAccountList(); // 加载账号列表
|
| 983 |
+
if (chatHistory.length === 0) {
|
| 984 |
+
addMessage('ai', '你好!有什么我可以帮你的吗?');
|
| 985 |
+
} else {
|
| 986 |
+
renderChatHistory();
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
// 初始化文件上传事件监听
|
| 990 |
+
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
| 991 |
+
|
| 992 |
+
// 确保页面加载后滚动到底部
|
| 993 |
+
setTimeout(() => {
|
| 994 |
+
const container = document.getElementById('chatContainer');
|
| 995 |
+
container.scrollTop = container.scrollHeight;
|
| 996 |
+
}, 100);
|
| 997 |
+
};
|
| 998 |
+
|
| 999 |
+
// ==================== 主题切换 ====================
|
| 1000 |
+
function toggleTheme() {
|
| 1001 |
+
const html = document.documentElement;
|
| 1002 |
+
const currentTheme = html.getAttribute('data-theme');
|
| 1003 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
| 1004 |
+
html.setAttribute('data-theme', newTheme);
|
| 1005 |
+
document.getElementById('themeIcon').textContent = newTheme === 'dark' ? '🌙' : '☀️';
|
| 1006 |
+
localStorage.setItem('theme', newTheme);
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 1010 |
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
| 1011 |
+
document.getElementById('themeIcon').textContent = savedTheme === 'dark' ? '🌙' : '☀️';
|
| 1012 |
+
|
| 1013 |
+
// ==================== 键盘事件处理 ====================
|
| 1014 |
+
function handleKeyDown(event) {
|
| 1015 |
+
if (event.keyCode === 13 && !event.shiftKey) {
|
| 1016 |
+
event.preventDefault();
|
| 1017 |
+
sendMessage();
|
| 1018 |
+
}
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
// ==================== 发送消息 ====================
|
| 1022 |
+
async function sendMessage() {
|
| 1023 |
+
console.log('sendMessage 被调用');
|
| 1024 |
+
const input = document.getElementById('userInput');
|
| 1025 |
+
const text = input.value.trim();
|
| 1026 |
+
console.log('输入内容:', text, '加载状态:', isLoading);
|
| 1027 |
+
if (!text || isLoading) {
|
| 1028 |
+
console.log('条件不满足,返回');
|
| 1029 |
+
return;
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
// 获取已上传的文件信息
|
| 1033 |
+
const attachments = uploadedFiles.map(f => ({
|
| 1034 |
+
name: f.name,
|
| 1035 |
+
isImage: f.isImage,
|
| 1036 |
+
previewUrl: f.previewUrl || null
|
| 1037 |
+
}));
|
| 1038 |
+
|
| 1039 |
+
// 添加用户消息(包含附件)
|
| 1040 |
+
addMessage('user', text, attachments);
|
| 1041 |
+
input.value = '';
|
| 1042 |
+
|
| 1043 |
+
// 设置加载状态
|
| 1044 |
+
setLoading(true);
|
| 1045 |
+
|
| 1046 |
+
// 获取流式模式设置
|
| 1047 |
+
const isStream = document.getElementById('streamMode').checked;
|
| 1048 |
+
|
| 1049 |
+
try {
|
| 1050 |
+
if (isStream) {
|
| 1051 |
+
await sendStreamRequest(text);
|
| 1052 |
+
} else {
|
| 1053 |
+
await sendNonStreamRequest(text);
|
| 1054 |
+
}
|
| 1055 |
+
} catch (error) {
|
| 1056 |
+
console.error('请求失败:', error);
|
| 1057 |
+
if (error.name !== 'AbortError') {
|
| 1058 |
+
addErrorMessage('请求失败: ' + error.message);
|
| 1059 |
+
}
|
| 1060 |
+
} finally {
|
| 1061 |
+
setLoading(false);
|
| 1062 |
+
// 发送成功后清空已上传的文件
|
| 1063 |
+
clearUploadedFiles();
|
| 1064 |
+
}
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
// ==================== 流式请求 ====================
|
| 1068 |
+
async function sendStreamRequest(text) {
|
| 1069 |
+
// 显示等待动画
|
| 1070 |
+
const typingId = showTypingIndicator();
|
| 1071 |
+
|
| 1072 |
+
let aiMessageId = null;
|
| 1073 |
+
let fullContent = '';
|
| 1074 |
+
|
| 1075 |
+
abortController = new AbortController();
|
| 1076 |
+
console.log('开始发送流式请求...');
|
| 1077 |
+
|
| 1078 |
+
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
| 1079 |
+
method: 'POST',
|
| 1080 |
+
headers: {
|
| 1081 |
+
'Content-Type': 'application/json'
|
| 1082 |
+
},
|
| 1083 |
+
body: JSON.stringify({
|
| 1084 |
+
model: getSelectedModel(),
|
| 1085 |
+
messages: buildMessages(text),
|
| 1086 |
+
stream: true,
|
| 1087 |
+
account_id: getSelectedAccount()
|
| 1088 |
+
}),
|
| 1089 |
+
signal: abortController.signal
|
| 1090 |
+
});
|
| 1091 |
+
|
| 1092 |
+
if (!response.ok) {
|
| 1093 |
+
const errorData = await response.json();
|
| 1094 |
+
throw new Error(errorData.error || '请求失败');
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
const reader = response.body.getReader();
|
| 1098 |
+
const decoder = new TextDecoder();
|
| 1099 |
+
|
| 1100 |
+
while (true) {
|
| 1101 |
+
const { done, value } = await reader.read();
|
| 1102 |
+
if (done) break;
|
| 1103 |
+
|
| 1104 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 1105 |
+
const lines = chunk.split('\n');
|
| 1106 |
+
|
| 1107 |
+
for (const line of lines) {
|
| 1108 |
+
if (line.startsWith('data: ')) {
|
| 1109 |
+
const data = line.slice(6);
|
| 1110 |
+
if (data === '[DONE]') {
|
| 1111 |
+
// 流式结束
|
| 1112 |
+
break;
|
| 1113 |
+
}
|
| 1114 |
+
try {
|
| 1115 |
+
const parsed = JSON.parse(data);
|
| 1116 |
+
const content = parsed.choices?.[0]?.delta?.content;
|
| 1117 |
+
const accountCsesidx = parsed.account_csesidx;
|
| 1118 |
+
if (content) {
|
| 1119 |
+
// 收到第一个内容时,移除等待动画并创建AI消息气泡
|
| 1120 |
+
if (!aiMessageId) {
|
| 1121 |
+
removeTypingIndicator(typingId);
|
| 1122 |
+
aiMessageId = createAIBubble();
|
| 1123 |
+
}
|
| 1124 |
+
fullContent += content;
|
| 1125 |
+
updateAIBubble(aiMessageId, fullContent);
|
| 1126 |
+
}
|
| 1127 |
+
// 更新账号信息
|
| 1128 |
+
if (accountCsesidx && aiMessageId) {
|
| 1129 |
+
updateAIBubbleAccount(aiMessageId, accountCsesidx);
|
| 1130 |
+
}
|
| 1131 |
+
} catch (e) {
|
| 1132 |
+
// 忽略解析错误
|
| 1133 |
+
}
|
| 1134 |
+
}
|
| 1135 |
+
}
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
// 如果没有收到任何内容,移除等待动画
|
| 1139 |
+
if (!aiMessageId) {
|
| 1140 |
+
removeTypingIndicator(typingId);
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
// 保存到历史记录
|
| 1144 |
+
if (fullContent) {
|
| 1145 |
+
chatHistory.push({ role: 'ai', content: fullContent, time: new Date().toISOString() });
|
| 1146 |
+
saveChatHistory();
|
| 1147 |
+
}
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
// ==================== 非流式请求 ====================
|
| 1151 |
+
async function sendNonStreamRequest(text) {
|
| 1152 |
+
// 显示加载指示器
|
| 1153 |
+
const loadingId = showTypingIndicator();
|
| 1154 |
+
|
| 1155 |
+
abortController = new AbortController();
|
| 1156 |
+
|
| 1157 |
+
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
| 1158 |
+
method: 'POST',
|
| 1159 |
+
headers: {
|
| 1160 |
+
'Content-Type': 'application/json'
|
| 1161 |
+
},
|
| 1162 |
+
body: JSON.stringify({
|
| 1163 |
+
model: getSelectedModel(),
|
| 1164 |
+
messages: buildMessages(text),
|
| 1165 |
+
stream: false,
|
| 1166 |
+
account_id: getSelectedAccount()
|
| 1167 |
+
}),
|
| 1168 |
+
signal: abortController.signal
|
| 1169 |
+
});
|
| 1170 |
+
|
| 1171 |
+
// 移除加载指示器
|
| 1172 |
+
removeTypingIndicator(loadingId);
|
| 1173 |
+
|
| 1174 |
+
if (!response.ok) {
|
| 1175 |
+
const errorData = await response.json();
|
| 1176 |
+
throw new Error(errorData.error || '请求失败');
|
| 1177 |
+
}
|
| 1178 |
+
|
| 1179 |
+
const data = await response.json();
|
| 1180 |
+
const content = data.choices?.[0]?.message?.content;
|
| 1181 |
+
const accountCsesidx = data.account_csesidx;
|
| 1182 |
+
|
| 1183 |
+
if (content) {
|
| 1184 |
+
// 使用createAIBubble以支持显示账号信息
|
| 1185 |
+
const aiMessageId = createAIBubble();
|
| 1186 |
+
updateAIBubble(aiMessageId, content);
|
| 1187 |
+
if (accountCsesidx) {
|
| 1188 |
+
updateAIBubbleAccount(aiMessageId, accountCsesidx);
|
| 1189 |
+
}
|
| 1190 |
+
// 保存到历史记录
|
| 1191 |
+
chatHistory.push({ role: 'ai', content: content, time: new Date().toISOString() });
|
| 1192 |
+
saveChatHistory();
|
| 1193 |
+
} else {
|
| 1194 |
+
addErrorMessage('未收到有效响应');
|
| 1195 |
+
}
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
// ==================== 构建消息列表 ====================
|
| 1199 |
+
function buildMessages(currentText) {
|
| 1200 |
+
const messages = [];
|
| 1201 |
+
|
| 1202 |
+
// 添加历史消息(最近10条)
|
| 1203 |
+
const recentHistory = chatHistory.slice(-10);
|
| 1204 |
+
for (const msg of recentHistory) {
|
| 1205 |
+
messages.push({
|
| 1206 |
+
role: msg.role === 'ai' ? 'assistant' : 'user',
|
| 1207 |
+
content: msg.content
|
| 1208 |
+
});
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
// 构建当前用户消息(支持文件)
|
| 1212 |
+
const fileIds = getUploadedFileIds();
|
| 1213 |
+
if (fileIds.length > 0) {
|
| 1214 |
+
// 使用OpenAI格式的content数组
|
| 1215 |
+
const contentParts = [];
|
| 1216 |
+
|
| 1217 |
+
// 添加文件引用
|
| 1218 |
+
for (const fileId of fileIds) {
|
| 1219 |
+
contentParts.push({
|
| 1220 |
+
type: 'file',
|
| 1221 |
+
file: { id: fileId }
|
| 1222 |
+
});
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
// 添加文本内容
|
| 1226 |
+
contentParts.push({
|
| 1227 |
+
type: 'text',
|
| 1228 |
+
text: currentText
|
| 1229 |
+
});
|
| 1230 |
+
|
| 1231 |
+
messages.push({
|
| 1232 |
+
role: 'user',
|
| 1233 |
+
content: contentParts
|
| 1234 |
+
});
|
| 1235 |
+
} else {
|
| 1236 |
+
messages.push({
|
| 1237 |
+
role: 'user',
|
| 1238 |
+
content: currentText
|
| 1239 |
+
});
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
return messages;
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
// ==================== UI 操作函数 ====================
|
| 1246 |
+
function addMessage(role, content, attachments = []) {
|
| 1247 |
+
const container = document.getElementById('chatContainer');
|
| 1248 |
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 1249 |
+
|
| 1250 |
+
const rowDiv = document.createElement('div');
|
| 1251 |
+
rowDiv.className = `message-row ${role}`;
|
| 1252 |
+
|
| 1253 |
+
const avatarDiv = document.createElement('div');
|
| 1254 |
+
avatarDiv.className = `avatar ${role}`;
|
| 1255 |
+
avatarDiv.innerHTML = role === 'ai' ? '🤖' : '👤';
|
| 1256 |
+
|
| 1257 |
+
const contentWrapper = document.createElement('div');
|
| 1258 |
+
contentWrapper.className = 'message-content';
|
| 1259 |
+
|
| 1260 |
+
// 如果有附件,先显示附件
|
| 1261 |
+
if (attachments && attachments.length > 0) {
|
| 1262 |
+
const attachmentsContainer = document.createElement('div');
|
| 1263 |
+
attachmentsContainer.className = 'message-attachments';
|
| 1264 |
+
attachmentsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;';
|
| 1265 |
+
|
| 1266 |
+
for (const attachment of attachments) {
|
| 1267 |
+
if (attachment.isImage && attachment.previewUrl) {
|
| 1268 |
+
// 图片附件
|
| 1269 |
+
const img = document.createElement('img');
|
| 1270 |
+
img.src = attachment.previewUrl;
|
| 1271 |
+
img.style.cssText = 'max-width: 200px; max-height: 200px; border-radius: 8px; cursor: pointer; object-fit: cover;';
|
| 1272 |
+
img.title = attachment.name;
|
| 1273 |
+
img.onclick = function() {
|
| 1274 |
+
window.open(attachment.previewUrl, '_blank');
|
| 1275 |
+
};
|
| 1276 |
+
attachmentsContainer.appendChild(img);
|
| 1277 |
+
} else {
|
| 1278 |
+
// 非图片文件附件
|
| 1279 |
+
const fileTag = document.createElement('div');
|
| 1280 |
+
fileTag.style.cssText = 'display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--primary-light); border-radius: 6px; font-size: 13px; color: var(--text-main);';
|
| 1281 |
+
fileTag.innerHTML = `<span>📄</span><span>${attachment.name}</span>`;
|
| 1282 |
+
attachmentsContainer.appendChild(fileTag);
|
| 1283 |
+
}
|
| 1284 |
+
}
|
| 1285 |
+
contentWrapper.appendChild(attachmentsContainer);
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
const bubbleDiv = document.createElement('div');
|
| 1289 |
+
bubbleDiv.className = 'bubble';
|
| 1290 |
+
bubbleDiv.textContent = content;
|
| 1291 |
+
|
| 1292 |
+
const timeDiv = document.createElement('div');
|
| 1293 |
+
timeDiv.className = 'timestamp';
|
| 1294 |
+
timeDiv.innerText = time;
|
| 1295 |
+
|
| 1296 |
+
contentWrapper.appendChild(bubbleDiv);
|
| 1297 |
+
contentWrapper.appendChild(timeDiv);
|
| 1298 |
+
|
| 1299 |
+
rowDiv.appendChild(avatarDiv);
|
| 1300 |
+
rowDiv.appendChild(contentWrapper);
|
| 1301 |
+
|
| 1302 |
+
container.appendChild(rowDiv);
|
| 1303 |
+
container.scrollTop = container.scrollHeight;
|
| 1304 |
+
|
| 1305 |
+
// 保存到历史记录(包含附件)
|
| 1306 |
+
chatHistory.push({ role, content, attachments: attachments || [], time: new Date().toISOString() });
|
| 1307 |
+
saveChatHistory();
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
function createAIBubble() {
|
| 1311 |
+
const container = document.getElementById('chatContainer');
|
| 1312 |
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 1313 |
+
const messageId = 'ai-msg-' + Date.now();
|
| 1314 |
+
|
| 1315 |
+
const rowDiv = document.createElement('div');
|
| 1316 |
+
rowDiv.className = 'message-row ai';
|
| 1317 |
+
rowDiv.id = messageId;
|
| 1318 |
+
|
| 1319 |
+
const avatarDiv = document.createElement('div');
|
| 1320 |
+
avatarDiv.className = 'avatar ai';
|
| 1321 |
+
avatarDiv.innerHTML = '🤖';
|
| 1322 |
+
|
| 1323 |
+
const contentWrapper = document.createElement('div');
|
| 1324 |
+
contentWrapper.className = 'message-content';
|
| 1325 |
+
|
| 1326 |
+
const bubbleDiv = document.createElement('div');
|
| 1327 |
+
bubbleDiv.className = 'bubble';
|
| 1328 |
+
bubbleDiv.id = messageId + '-bubble';
|
| 1329 |
+
bubbleDiv.textContent = '';
|
| 1330 |
+
|
| 1331 |
+
// 账号信息显示区域
|
| 1332 |
+
const accountDiv = document.createElement('div');
|
| 1333 |
+
accountDiv.className = 'account-info';
|
| 1334 |
+
accountDiv.id = messageId + '-account';
|
| 1335 |
+
accountDiv.style.cssText = 'font-size: 11px; color: var(--text-muted); margin-top: 4px;';
|
| 1336 |
+
accountDiv.textContent = '';
|
| 1337 |
+
|
| 1338 |
+
const timeDiv = document.createElement('div');
|
| 1339 |
+
timeDiv.className = 'timestamp';
|
| 1340 |
+
timeDiv.innerText = time;
|
| 1341 |
+
|
| 1342 |
+
contentWrapper.appendChild(bubbleDiv);
|
| 1343 |
+
contentWrapper.appendChild(accountDiv);
|
| 1344 |
+
contentWrapper.appendChild(timeDiv);
|
| 1345 |
+
|
| 1346 |
+
rowDiv.appendChild(avatarDiv);
|
| 1347 |
+
rowDiv.appendChild(contentWrapper);
|
| 1348 |
+
|
| 1349 |
+
container.appendChild(rowDiv);
|
| 1350 |
+
container.scrollTop = container.scrollHeight;
|
| 1351 |
+
|
| 1352 |
+
return messageId;
|
| 1353 |
+
}
|
| 1354 |
+
|
| 1355 |
+
// 更新AI消息的账号信息
|
| 1356 |
+
function updateAIBubbleAccount(messageId, accountCsesidx) {
|
| 1357 |
+
const accountDiv = document.getElementById(messageId + '-account');
|
| 1358 |
+
if (accountDiv && accountCsesidx) {
|
| 1359 |
+
accountDiv.textContent = '账号: ' + accountCsesidx;
|
| 1360 |
+
}
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
function updateAIBubble(messageId, content) {
|
| 1364 |
+
const bubble = document.getElementById(messageId + '-bubble');
|
| 1365 |
+
if (bubble) {
|
| 1366 |
+
// 解析内容,将图片URL转换为图片元素
|
| 1367 |
+
bubble.innerHTML = parseContentWithImages(content);
|
| 1368 |
+
const container = document.getElementById('chatContainer');
|
| 1369 |
+
container.scrollTop = container.scrollHeight;
|
| 1370 |
+
}
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
// 解析内容中的图片URL并转换为HTML
|
| 1374 |
+
function parseContentWithImages(content) {
|
| 1375 |
+
// 匹配图片URL的正则表达式(支持常见图片格式)
|
| 1376 |
+
const imageUrlRegex = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))/gi;
|
| 1377 |
+
|
| 1378 |
+
// 将内容按行分割处理
|
| 1379 |
+
const lines = content.split('\n');
|
| 1380 |
+
const processedLines = lines.map(line => {
|
| 1381 |
+
// 检查该行是否是纯图片URL
|
| 1382 |
+
const trimmedLine = line.trim();
|
| 1383 |
+
if (imageUrlRegex.test(trimmedLine) && trimmedLine.match(imageUrlRegex)?.[0] === trimmedLine) {
|
| 1384 |
+
// 重置正则表达式的lastIndex
|
| 1385 |
+
imageUrlRegex.lastIndex = 0;
|
| 1386 |
+
// 该行是纯图片URL,转换为图片元素
|
| 1387 |
+
return `<div class="ai-image-container"><img src="${escapeHtml(trimmedLine)}" alt="AI生成的图片" style="max-width: 300px; max-height: 300px; border-radius: 8px; cursor: pointer; margin: 8px 0;" onclick="window.open('${escapeHtml(trimmedLine)}', '_blank')" onerror="this.style.display='none'; this.nextSibling.style.display='inline';"><span style="display:none;">${escapeHtml(trimmedLine)}</span></div>`;
|
| 1388 |
+
}
|
| 1389 |
+
// 重置正则表达式的lastIndex
|
| 1390 |
+
imageUrlRegex.lastIndex = 0;
|
| 1391 |
+
// 普通文本行,转义HTML
|
| 1392 |
+
return escapeHtml(line);
|
| 1393 |
+
});
|
| 1394 |
+
|
| 1395 |
+
return processedLines.join('<br>');
|
| 1396 |
+
}
|
| 1397 |
+
|
| 1398 |
+
// HTML转义函数
|
| 1399 |
+
function escapeHtml(text) {
|
| 1400 |
+
const div = document.createElement('div');
|
| 1401 |
+
div.textContent = text;
|
| 1402 |
+
return div.innerHTML;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
function showTypingIndicator() {
|
| 1406 |
+
const container = document.getElementById('chatContainer');
|
| 1407 |
+
const indicatorId = 'typing-' + Date.now();
|
| 1408 |
+
|
| 1409 |
+
const rowDiv = document.createElement('div');
|
| 1410 |
+
rowDiv.className = 'message-row ai';
|
| 1411 |
+
rowDiv.id = indicatorId;
|
| 1412 |
+
|
| 1413 |
+
const avatarDiv = document.createElement('div');
|
| 1414 |
+
avatarDiv.className = 'avatar ai';
|
| 1415 |
+
avatarDiv.innerHTML = '🤖';
|
| 1416 |
+
|
| 1417 |
+
const contentWrapper = document.createElement('div');
|
| 1418 |
+
contentWrapper.className = 'message-content';
|
| 1419 |
+
|
| 1420 |
+
const indicator = document.createElement('div');
|
| 1421 |
+
indicator.className = 'typing-indicator';
|
| 1422 |
+
indicator.innerHTML = '<span></span><span></span><span></span>';
|
| 1423 |
+
|
| 1424 |
+
contentWrapper.appendChild(indicator);
|
| 1425 |
+
rowDiv.appendChild(avatarDiv);
|
| 1426 |
+
rowDiv.appendChild(contentWrapper);
|
| 1427 |
+
|
| 1428 |
+
container.appendChild(rowDiv);
|
| 1429 |
+
container.scrollTop = container.scrollHeight;
|
| 1430 |
+
|
| 1431 |
+
return indicatorId;
|
| 1432 |
+
}
|
| 1433 |
+
|
| 1434 |
+
function removeTypingIndicator(indicatorId) {
|
| 1435 |
+
const indicator = document.getElementById(indicatorId);
|
| 1436 |
+
if (indicator) {
|
| 1437 |
+
indicator.remove();
|
| 1438 |
+
}
|
| 1439 |
+
}
|
| 1440 |
+
|
| 1441 |
+
function addErrorMessage(message) {
|
| 1442 |
+
const container = document.getElementById('chatContainer');
|
| 1443 |
+
|
| 1444 |
+
const rowDiv = document.createElement('div');
|
| 1445 |
+
rowDiv.className = 'message-row ai';
|
| 1446 |
+
|
| 1447 |
+
const avatarDiv = document.createElement('div');
|
| 1448 |
+
avatarDiv.className = 'avatar ai';
|
| 1449 |
+
avatarDiv.innerHTML = '⚠️';
|
| 1450 |
+
|
| 1451 |
+
const contentWrapper = document.createElement('div');
|
| 1452 |
+
contentWrapper.className = 'message-content';
|
| 1453 |
+
|
| 1454 |
+
const errorDiv = document.createElement('div');
|
| 1455 |
+
errorDiv.className = 'error-message';
|
| 1456 |
+
errorDiv.textContent = message;
|
| 1457 |
+
|
| 1458 |
+
contentWrapper.appendChild(errorDiv);
|
| 1459 |
+
rowDiv.appendChild(avatarDiv);
|
| 1460 |
+
rowDiv.appendChild(contentWrapper);
|
| 1461 |
+
|
| 1462 |
+
container.appendChild(rowDiv);
|
| 1463 |
+
container.scrollTop = container.scrollHeight;
|
| 1464 |
+
}
|
| 1465 |
+
|
| 1466 |
+
function setLoading(loading) {
|
| 1467 |
+
isLoading = loading;
|
| 1468 |
+
const input = document.getElementById('userInput');
|
| 1469 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 1470 |
+
|
| 1471 |
+
input.disabled = loading;
|
| 1472 |
+
sendBtn.disabled = loading;
|
| 1473 |
+
sendBtn.innerHTML = loading ? '⏳' : '➤';
|
| 1474 |
+
}
|
| 1475 |
+
|
| 1476 |
+
// ==================== 对话历史管理 ====================
|
| 1477 |
+
function saveChatHistory() {
|
| 1478 |
+
// 保存到当前会话
|
| 1479 |
+
saveCurrentSessionHistory();
|
| 1480 |
+
}
|
| 1481 |
+
|
| 1482 |
+
function loadChatHistory() {
|
| 1483 |
+
// 从当前会话加载(由loadSessions调用)
|
| 1484 |
+
const session = sessions.find(s => s.id === currentSessionId);
|
| 1485 |
+
if (session && session.history) {
|
| 1486 |
+
chatHistory = session.history;
|
| 1487 |
+
} else {
|
| 1488 |
+
chatHistory = [];
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
|
| 1492 |
+
function renderChatHistory() {
|
| 1493 |
+
const container = document.getElementById('chatContainer');
|
| 1494 |
+
container.innerHTML = '';
|
| 1495 |
+
|
| 1496 |
+
for (const msg of chatHistory) {
|
| 1497 |
+
const time = new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 1498 |
+
|
| 1499 |
+
const rowDiv = document.createElement('div');
|
| 1500 |
+
rowDiv.className = `message-row ${msg.role}`;
|
| 1501 |
+
|
| 1502 |
+
const avatarDiv = document.createElement('div');
|
| 1503 |
+
avatarDiv.className = `avatar ${msg.role}`;
|
| 1504 |
+
avatarDiv.innerHTML = msg.role === 'ai' ? '🤖' : '👤';
|
| 1505 |
+
|
| 1506 |
+
const contentWrapper = document.createElement('div');
|
| 1507 |
+
contentWrapper.className = 'message-content';
|
| 1508 |
+
|
| 1509 |
+
// 如果有附件,先显示附件(兼容旧的images字段)
|
| 1510 |
+
const attachments = msg.attachments || (msg.images ? msg.images.map(url => ({ isImage: true, previewUrl: url, name: '图片' })) : []);
|
| 1511 |
+
if (attachments && attachments.length > 0) {
|
| 1512 |
+
const attachmentsContainer = document.createElement('div');
|
| 1513 |
+
attachmentsContainer.className = 'message-attachments';
|
| 1514 |
+
attachmentsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;';
|
| 1515 |
+
|
| 1516 |
+
for (const attachment of attachments) {
|
| 1517 |
+
if (attachment.isImage && attachment.previewUrl) {
|
| 1518 |
+
// 图片附件
|
| 1519 |
+
const img = document.createElement('img');
|
| 1520 |
+
img.src = attachment.previewUrl;
|
| 1521 |
+
img.style.cssText = 'max-width: 200px; max-height: 200px; border-radius: 8px; cursor: pointer; object-fit: cover;';
|
| 1522 |
+
img.title = attachment.name || '图片';
|
| 1523 |
+
img.onclick = function() {
|
| 1524 |
+
window.open(attachment.previewUrl, '_blank');
|
| 1525 |
+
};
|
| 1526 |
+
attachmentsContainer.appendChild(img);
|
| 1527 |
+
} else {
|
| 1528 |
+
// 非图片文件附件
|
| 1529 |
+
const fileTag = document.createElement('div');
|
| 1530 |
+
fileTag.style.cssText = 'display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--primary-light); border-radius: 6px; font-size: 13px; color: var(--text-main);';
|
| 1531 |
+
fileTag.innerHTML = `<span>📄</span><span>${attachment.name || '文件'}</span>`;
|
| 1532 |
+
attachmentsContainer.appendChild(fileTag);
|
| 1533 |
+
}
|
| 1534 |
+
}
|
| 1535 |
+
contentWrapper.appendChild(attachmentsContainer);
|
| 1536 |
+
}
|
| 1537 |
+
|
| 1538 |
+
const bubbleDiv = document.createElement('div');
|
| 1539 |
+
bubbleDiv.className = 'bubble';
|
| 1540 |
+
// AI消息需要解析图片URL
|
| 1541 |
+
if (msg.role === 'ai') {
|
| 1542 |
+
bubbleDiv.innerHTML = parseContentWithImages(msg.content);
|
| 1543 |
+
} else {
|
| 1544 |
+
bubbleDiv.textContent = msg.content;
|
| 1545 |
+
}
|
| 1546 |
+
|
| 1547 |
+
const timeDiv = document.createElement('div');
|
| 1548 |
+
timeDiv.className = 'timestamp';
|
| 1549 |
+
timeDiv.innerText = time;
|
| 1550 |
+
|
| 1551 |
+
contentWrapper.appendChild(bubbleDiv);
|
| 1552 |
+
contentWrapper.appendChild(timeDiv);
|
| 1553 |
+
|
| 1554 |
+
rowDiv.appendChild(avatarDiv);
|
| 1555 |
+
rowDiv.appendChild(contentWrapper);
|
| 1556 |
+
|
| 1557 |
+
container.appendChild(rowDiv);
|
| 1558 |
+
}
|
| 1559 |
+
|
| 1560 |
+
container.scrollTop = container.scrollHeight;
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
function clearChat() {
|
| 1564 |
+
if (confirm('确定要清空当前会话的对话记录吗?')) {
|
| 1565 |
+
chatHistory = [];
|
| 1566 |
+
saveChatHistory(); // 保存到当前会话
|
| 1567 |
+
document.getElementById('chatContainer').innerHTML = '';
|
| 1568 |
+
addMessage('ai', '对话已清空。有什么我可以帮你的吗?');
|
| 1569 |
+
}
|
| 1570 |
+
}
|
| 1571 |
+
|
| 1572 |
+
// ==================== 文件上传功能 ====================
|
| 1573 |
+
function handleFileSelect(event) {
|
| 1574 |
+
const files = event.target.files;
|
| 1575 |
+
if (!files || files.length === 0) return;
|
| 1576 |
+
|
| 1577 |
+
for (const file of files) {
|
| 1578 |
+
uploadFile(file);
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
// 清空input以便可以重复选择同一文件
|
| 1582 |
+
event.target.value = '';
|
| 1583 |
+
}
|
| 1584 |
+
|
| 1585 |
+
async function uploadFile(file) {
|
| 1586 |
+
const uploadBtn = document.getElementById('uploadBtn');
|
| 1587 |
+
const filesContainer = document.getElementById('uploadedFilesContainer');
|
| 1588 |
+
const filesList = document.getElementById('uploadedFiles');
|
| 1589 |
+
|
| 1590 |
+
// 显示文件容器
|
| 1591 |
+
filesContainer.style.display = 'block';
|
| 1592 |
+
|
| 1593 |
+
// 创建文件标签(上传中状态)
|
| 1594 |
+
const fileTag = document.createElement('div');
|
| 1595 |
+
fileTag.className = 'file-tag file-uploading';
|
| 1596 |
+
fileTag.id = 'file-' + Date.now();
|
| 1597 |
+
fileTag.innerHTML = `
|
| 1598 |
+
<span class="file-icon">📄</span>
|
| 1599 |
+
<span class="file-name">${file.name}</span>
|
| 1600 |
+
`;
|
| 1601 |
+
filesList.appendChild(fileTag);
|
| 1602 |
+
|
| 1603 |
+
try {
|
| 1604 |
+
const formData = new FormData();
|
| 1605 |
+
formData.append('file', file);
|
| 1606 |
+
formData.append('purpose', 'assistants');
|
| 1607 |
+
|
| 1608 |
+
const response = await fetch(`${API_BASE}/v1/files`, {
|
| 1609 |
+
method: 'POST',
|
| 1610 |
+
body: formData
|
| 1611 |
+
});
|
| 1612 |
+
|
| 1613 |
+
if (!response.ok) {
|
| 1614 |
+
const errorData = await response.json();
|
| 1615 |
+
throw new Error(errorData.error?.message || '上传失败');
|
| 1616 |
+
}
|
| 1617 |
+
|
| 1618 |
+
const data = await response.json();
|
| 1619 |
+
|
| 1620 |
+
// 更新文件标签为成功状态
|
| 1621 |
+
fileTag.className = 'file-tag';
|
| 1622 |
+
fileTag.innerHTML = `
|
| 1623 |
+
<span class="file-icon">📄</span>
|
| 1624 |
+
<span class="file-name">${file.name}</span>
|
| 1625 |
+
<button class="remove-file" onclick="removeFile('${fileTag.id}', '${data.id}')">×</button>
|
| 1626 |
+
`;
|
| 1627 |
+
|
| 1628 |
+
// 保存文件信息(包含图片预览)
|
| 1629 |
+
const fileInfo = {
|
| 1630 |
+
tagId: fileTag.id,
|
| 1631 |
+
id: data.id,
|
| 1632 |
+
name: file.name,
|
| 1633 |
+
gemini_file_id: data.gemini_file_id,
|
| 1634 |
+
isImage: file.type.startsWith('image/'),
|
| 1635 |
+
previewUrl: null
|
| 1636 |
+
};
|
| 1637 |
+
|
| 1638 |
+
// 如果是图片,生成预览URL(使用Promise确保同步完成)
|
| 1639 |
+
if (fileInfo.isImage) {
|
| 1640 |
+
await new Promise((resolve) => {
|
| 1641 |
+
const reader = new FileReader();
|
| 1642 |
+
reader.onload = function(e) {
|
| 1643 |
+
fileInfo.previewUrl = e.target.result;
|
| 1644 |
+
resolve();
|
| 1645 |
+
};
|
| 1646 |
+
reader.readAsDataURL(file);
|
| 1647 |
+
});
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
+
uploadedFiles.push(fileInfo);
|
| 1651 |
+
|
| 1652 |
+
// 更新上传按钮状态
|
| 1653 |
+
updateUploadBtnState();
|
| 1654 |
+
|
| 1655 |
+
} catch (error) {
|
| 1656 |
+
console.error('文件上传失败:', error);
|
| 1657 |
+
fileTag.remove();
|
| 1658 |
+
alert('文件上传失败: ' + error.message);
|
| 1659 |
+
|
| 1660 |
+
// 如果没有文件了,隐藏容器
|
| 1661 |
+
if (uploadedFiles.length === 0) {
|
| 1662 |
+
filesContainer.style.display = 'none';
|
| 1663 |
+
}
|
| 1664 |
+
}
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
+
function removeFile(tagId, fileId) {
|
| 1668 |
+
// 从DOM中移除
|
| 1669 |
+
const fileTag = document.getElementById(tagId);
|
| 1670 |
+
if (fileTag) {
|
| 1671 |
+
fileTag.remove();
|
| 1672 |
+
}
|
| 1673 |
+
|
| 1674 |
+
// 从数组中移除
|
| 1675 |
+
uploadedFiles = uploadedFiles.filter(f => f.tagId !== tagId);
|
| 1676 |
+
|
| 1677 |
+
// 更新UI状态
|
| 1678 |
+
updateUploadBtnState();
|
| 1679 |
+
|
| 1680 |
+
// 如果没有文件了,隐藏容器
|
| 1681 |
+
if (uploadedFiles.length === 0) {
|
| 1682 |
+
document.getElementById('uploadedFilesContainer').style.display = 'none';
|
| 1683 |
+
}
|
| 1684 |
+
|
| 1685 |
+
// 可选:调用删除API
|
| 1686 |
+
fetch(`${API_BASE}/v1/files/${fileId}`, { method: 'DELETE' }).catch(console.error);
|
| 1687 |
+
}
|
| 1688 |
+
|
| 1689 |
+
function getUploadedFileIds() {
|
| 1690 |
+
return uploadedFiles.map(f => f.id);
|
| 1691 |
+
}
|
| 1692 |
+
|
| 1693 |
+
function clearUploadedFiles() {
|
| 1694 |
+
uploadedFiles = [];
|
| 1695 |
+
document.getElementById('uploadedFiles').innerHTML = '';
|
| 1696 |
+
document.getElementById('uploadedFilesContainer').style.display = 'none';
|
| 1697 |
+
updateUploadBtnState();
|
| 1698 |
+
}
|
| 1699 |
+
|
| 1700 |
+
function updateUploadBtnState() {
|
| 1701 |
+
const uploadBtn = document.getElementById('uploadBtn');
|
| 1702 |
+
if (uploadedFiles.length > 0) {
|
| 1703 |
+
uploadBtn.classList.add('has-files');
|
| 1704 |
+
uploadBtn.title = `已上传 ${uploadedFiles.length} 个文件`;
|
| 1705 |
+
} else {
|
| 1706 |
+
uploadBtn.classList.remove('has-files');
|
| 1707 |
+
uploadBtn.title = '上传文件';
|
| 1708 |
+
}
|
| 1709 |
+
}
|
| 1710 |
+
</script>
|
| 1711 |
+
</body>
|
| 1712 |
+
</html>
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: business-gemini-new
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
app:
|
| 5 |
+
build: .
|
| 6 |
+
container_name: business-gemini-new
|
| 7 |
+
restart: unless-stopped
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
volumes:
|
| 11 |
+
- ./business_gemini_session.json:/app/business_gemini_session.json
|
| 12 |
+
- ./index.html:/app/index.html
|
| 13 |
+
- ./gemini.py:/app/gemini.py
|
| 14 |
+
environment:
|
| 15 |
+
- PYTHONUNBUFFERED=1
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: [ "CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health', timeout=5)" ]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
networks:
|
| 23 |
+
- gemini-network
|
| 24 |
+
|
| 25 |
+
networks:
|
| 26 |
+
gemini-network:
|
| 27 |
+
driver: bridge
|
gemini.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
ADDED
|
@@ -0,0 +1,2025 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Business Gemini Pool 管理控制台</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* [OPTIMIZATION] 1. 全局样式优化与变量调整 */
|
| 9 |
+
:root {
|
| 10 |
+
/* 核心颜色保持不变 */
|
| 11 |
+
--primary: #4285f4;
|
| 12 |
+
--primary-hover: #3367d6;
|
| 13 |
+
--primary-light: rgba(66, 133, 244, 0.1);
|
| 14 |
+
--success: #34a853;
|
| 15 |
+
--success-light: rgba(52, 168, 83, 0.1);
|
| 16 |
+
--danger: #ea4335;
|
| 17 |
+
--danger-light: rgba(234, 67, 53, 0.1);
|
| 18 |
+
--warning: #fbbc04;
|
| 19 |
+
--warning-light: rgba(251, 188, 4, 0.1);
|
| 20 |
+
|
| 21 |
+
/* [NEW] 引入更精细的变量控制 */
|
| 22 |
+
--radius-sm: 6px;
|
| 23 |
+
--radius-md: 12px; /* 增大圆角,更柔和 */
|
| 24 |
+
--radius-lg: 16px;
|
| 25 |
+
--transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); /* [NEW] 现代化的缓动函数 */
|
| 26 |
+
|
| 27 |
+
--font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; /* [NEW] 引入更适合UI的字体 */
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* [OPTIMIZATION] 2. Light & Dark Theme 优化,增强对比度和质感 */
|
| 31 |
+
[data-theme="light"] {
|
| 32 |
+
--bg-color: #f7f8fc; /* 更柔和的背景色 */
|
| 33 |
+
--card-bg: #ffffff;
|
| 34 |
+
--text-main: #1f2328;
|
| 35 |
+
--text-muted: #656d76;
|
| 36 |
+
--border: #e4e7eb; /* 更浅的边框色 */
|
| 37 |
+
--hover-bg: #f2f3f5;
|
| 38 |
+
--input-bg: #ffffff;
|
| 39 |
+
--shadow-sm: 0 1px 2px 0 rgba(27, 31, 35, 0.04);
|
| 40 |
+
--shadow-md: 0 4px 8px 0 rgba(27, 31, 35, 0.06), 0 1px 2px 0 rgba(27, 31, 35, 0.05); /* 更柔和的阴影 */
|
| 41 |
+
--shadow-lg: 0 10px 20px 0 rgba(27, 31, 35, 0.07), 0 3px 6px 0 rgba(27, 31, 35, 0.05);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
[data-theme="dark"] {
|
| 45 |
+
--bg-color: #1a1b1e;
|
| 46 |
+
--card-bg: #242528;
|
| 47 |
+
--text-main: #e8eaed;
|
| 48 |
+
--text-muted: #9aa0a6;
|
| 49 |
+
--border: #3a3c40;
|
| 50 |
+
--hover-bg: #303134;
|
| 51 |
+
--input-bg: #2f3033;
|
| 52 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
|
| 53 |
+
--shadow-md: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 1px 2px 0 rgba(0, 0, 0, 0.1);
|
| 54 |
+
--shadow-lg: 0 10px 20px 0 rgba(0, 0, 0, 0.2), 0 3px 6px 0 rgba(0, 0, 0, 0.15);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
* {
|
| 58 |
+
margin: 0;
|
| 59 |
+
padding: 0;
|
| 60 |
+
box-sizing: border-box;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
body {
|
| 64 |
+
font-family: var(--font-main);
|
| 65 |
+
background-color: var(--bg-color);
|
| 66 |
+
color: var(--text-main);
|
| 67 |
+
min-height: 100vh;
|
| 68 |
+
transition: background-color 0.3s, color 0.3s;
|
| 69 |
+
-webkit-font-smoothing: antialiased;
|
| 70 |
+
-moz-osx-font-smoothing: grayscale;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.container {
|
| 74 |
+
max-width: 1400px;
|
| 75 |
+
margin: 0 auto;
|
| 76 |
+
padding: 32px; /* 增加页面内边距 */
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* [OPTIMIZATION] 3. Header 重新设计,更简洁大气 */
|
| 80 |
+
.header {
|
| 81 |
+
display: flex;
|
| 82 |
+
justify-content: space-between;
|
| 83 |
+
align-items: center;
|
| 84 |
+
margin-bottom: 32px;
|
| 85 |
+
/* 移除背景和阴影,使其融入页面 */
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.header-left { display: flex; align-items: center; gap: 16px; }
|
| 89 |
+
|
| 90 |
+
.logo {
|
| 91 |
+
width: 44px;
|
| 92 |
+
height: 44px;
|
| 93 |
+
background: linear-gradient(135deg, #4285f4, #34a853, #fbbc04, #ea4335);
|
| 94 |
+
border-radius: var(--radius-md);
|
| 95 |
+
display: flex;
|
| 96 |
+
align-items: center;
|
| 97 |
+
justify-content: center;
|
| 98 |
+
color: white;
|
| 99 |
+
font-weight: 600;
|
| 100 |
+
font-size: 22px;
|
| 101 |
+
transform: rotate(-10deg); /* [NEW] 增加一点趣味性 */
|
| 102 |
+
transition: var(--transition-ease);
|
| 103 |
+
}
|
| 104 |
+
.logo:hover { transform: rotate(0deg) scale(1.05); }
|
| 105 |
+
|
| 106 |
+
.header h1 {
|
| 107 |
+
font-size: 26px; /* 增大标题字号 */
|
| 108 |
+
font-weight: 600;
|
| 109 |
+
color: var(--text-main);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.header h1 span {
|
| 113 |
+
color: var(--text-muted);
|
| 114 |
+
font-weight: 400;
|
| 115 |
+
font-size: 16px;
|
| 116 |
+
margin-left: 10px;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.header-right { display: flex; align-items: center; gap: 16px; }
|
| 120 |
+
|
| 121 |
+
.status-indicator {
|
| 122 |
+
display: flex;
|
| 123 |
+
align-items: center;
|
| 124 |
+
gap: 8px;
|
| 125 |
+
padding: 8px 16px;
|
| 126 |
+
background: var(--success-light);
|
| 127 |
+
border: 1px solid rgba(52, 168, 83, 0.2);
|
| 128 |
+
border-radius: 50px; /* 改为胶囊形状 */
|
| 129 |
+
font-size: 14px;
|
| 130 |
+
color: var(--success);
|
| 131 |
+
font-weight: 500;
|
| 132 |
+
}
|
| 133 |
+
.status-indicator::before {
|
| 134 |
+
content: ''; width: 8px; height: 8px;
|
| 135 |
+
background: var(--success); border-radius: 50%;
|
| 136 |
+
animation: pulse 2s infinite;
|
| 137 |
+
}
|
| 138 |
+
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.9); } }
|
| 139 |
+
|
| 140 |
+
.theme-toggle {
|
| 141 |
+
width: 44px; height: 44px; border: 1px solid var(--border);
|
| 142 |
+
background: var(--card-bg); border-radius: var(--radius-md);
|
| 143 |
+
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
| 144 |
+
font-size: 20px; transition: var(--transition-ease);
|
| 145 |
+
}
|
| 146 |
+
.theme-toggle:hover { background: var(--hover-bg); border-color: var(--primary); transform: translateY(-2px); }
|
| 147 |
+
|
| 148 |
+
/* [OPTIMIZATION] 4. Tabs 重新设计,更现代、更 subtle */
|
| 149 |
+
.tabs {
|
| 150 |
+
display: flex;
|
| 151 |
+
gap: 16px;
|
| 152 |
+
border-bottom: 1px solid var(--border); /* 底部线条导航 */
|
| 153 |
+
margin-bottom: 32px;
|
| 154 |
+
}
|
| 155 |
+
.tab {
|
| 156 |
+
padding: 14px 4px; /* 减少水平padding,通过gap控制间距 */
|
| 157 |
+
border: none; border-bottom: 2px solid transparent;
|
| 158 |
+
background: transparent; color: var(--text-muted);
|
| 159 |
+
font-size: 15px; font-weight: 500;
|
| 160 |
+
cursor: pointer; border-radius: 0;
|
| 161 |
+
transition: var(--transition-ease);
|
| 162 |
+
display: flex; align-items: center; justify-content: center;
|
| 163 |
+
gap: 8px;
|
| 164 |
+
}
|
| 165 |
+
.tab:hover { color: var(--primary); }
|
| 166 |
+
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
| 167 |
+
.tab-icon { font-size: 20px; }
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
/* Status Badge */
|
| 171 |
+
.badge {
|
| 172 |
+
display: inline-flex;
|
| 173 |
+
align-items: center;
|
| 174 |
+
gap: 6px;
|
| 175 |
+
padding: 6px 12px;
|
| 176 |
+
border-radius: 20px;
|
| 177 |
+
font-size: 12px;
|
| 178 |
+
font-weight: 500;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.badge::before {
|
| 182 |
+
content: '';
|
| 183 |
+
width: 6px;
|
| 184 |
+
height: 6px;
|
| 185 |
+
border-radius: 50%;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.badge-success {
|
| 189 |
+
background: var(--success-light);
|
| 190 |
+
color: var(--success);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.badge-success::before {
|
| 194 |
+
background: var(--success);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.badge-danger {
|
| 198 |
+
background: var(--danger-light);
|
| 199 |
+
color: var(--danger);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.badge-danger::before {
|
| 203 |
+
background: var(--danger);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.cooldown-hint {
|
| 207 |
+
display: block;
|
| 208 |
+
color: var(--text-muted);
|
| 209 |
+
font-size: 12px;
|
| 210 |
+
margin-top: 4px;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.log-level-control {
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 8px;
|
| 217 |
+
background: var(--card-bg);
|
| 218 |
+
border: 1px solid var(--border);
|
| 219 |
+
border-radius: var(--radius-md);
|
| 220 |
+
padding: 6px 10px;
|
| 221 |
+
}
|
| 222 |
+
.log-level-control label {
|
| 223 |
+
font-size: 12px;
|
| 224 |
+
color: var(--text-muted);
|
| 225 |
+
}
|
| 226 |
+
.log-level-select {
|
| 227 |
+
border: 1px solid var(--border);
|
| 228 |
+
background: var(--input-bg);
|
| 229 |
+
color: var(--text-main);
|
| 230 |
+
border-radius: var(--radius-sm);
|
| 231 |
+
padding: 6px 8px;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.token-actions {
|
| 235 |
+
display: flex;
|
| 236 |
+
gap: 8px;
|
| 237 |
+
flex-wrap: wrap;
|
| 238 |
+
margin-bottom: 12px;
|
| 239 |
+
}
|
| 240 |
+
.token-input {
|
| 241 |
+
flex: 1;
|
| 242 |
+
min-width: 240px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.badge-warning {
|
| 246 |
+
background: var(--warning-light);
|
| 247 |
+
color: #b06000;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.badge-warning::before {
|
| 251 |
+
background: var(--warning);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* [OPTIMIZATION] 5. 动画效果增强 */
|
| 255 |
+
.tab-content { display: none; }
|
| 256 |
+
.tab-content.active { display: block; animation: contentFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
|
| 257 |
+
@keyframes contentFadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
| 258 |
+
|
| 259 |
+
/* [OPTIMIZATION] 6. Card 样式优化 */
|
| 260 |
+
.card {
|
| 261 |
+
background: var(--card-bg);
|
| 262 |
+
border-radius: var(--radius-lg);
|
| 263 |
+
box-shadow: var(--shadow-md);
|
| 264 |
+
border: 1px solid var(--border);
|
| 265 |
+
margin-bottom: 32px;
|
| 266 |
+
overflow: hidden;
|
| 267 |
+
transition: var(--transition-ease);
|
| 268 |
+
}
|
| 269 |
+
.card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-lg); }
|
| 270 |
+
|
| 271 |
+
.card-header {
|
| 272 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 273 |
+
padding: 20px 24px; border-bottom: 1px solid var(--border);
|
| 274 |
+
}
|
| 275 |
+
.card-title {
|
| 276 |
+
font-size: 18px; font-weight: 600; color: var(--text-main);
|
| 277 |
+
display: flex; align-items: center; gap: 12px;
|
| 278 |
+
}
|
| 279 |
+
.card-title-icon { font-size: 22px; color: var(--text-muted); }
|
| 280 |
+
.card-body { padding: 24px; }
|
| 281 |
+
|
| 282 |
+
/* [OPTIMIZATION] 7. Button 样式优化 */
|
| 283 |
+
.btn {
|
| 284 |
+
padding: 10px 20px; border: none; border-radius: var(--radius-md);
|
| 285 |
+
cursor: pointer; font-size: 14px; font-weight: 500;
|
| 286 |
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
| 287 |
+
transition: var(--transition-ease); text-decoration: none;
|
| 288 |
+
}
|
| 289 |
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 290 |
+
.btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
| 291 |
+
.btn-primary { background: var(--primary); color: white; }
|
| 292 |
+
.btn-primary:hover:not(:disabled) { background: var(--primary-hover); }
|
| 293 |
+
|
| 294 |
+
.btn-outline {
|
| 295 |
+
background: transparent; color: var(--text-muted);
|
| 296 |
+
border: 1px solid var(--border);
|
| 297 |
+
}
|
| 298 |
+
.btn-outline:hover:not(:disabled) { border-color: var(--text-main); color: var(--text-main); }
|
| 299 |
+
/* 其他按钮颜色保持 */
|
| 300 |
+
.btn-success { background: var(--success-light); color: var(--success); border: 1px solid rgba(52, 168, 83, 0.2); }
|
| 301 |
+
.btn-success:hover:not(:disabled) { background: var(--success); color: white; border-color: var(--success); }
|
| 302 |
+
.btn-danger { background: var(--danger-light); color: var(--danger); border: 1px solid rgba(234, 67, 53, 0.2); }
|
| 303 |
+
.btn-danger:hover:not(:disabled) { background: var(--danger); color: white; border-color: var(--danger); }
|
| 304 |
+
|
| 305 |
+
.btn-sm { padding: 6px 14px; font-size: 13px; border-radius: var(--radius-sm); }
|
| 306 |
+
.btn-icon { width: 32px; height: 32px; padding: 0; border-radius: var(--radius-md); display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; }
|
| 307 |
+
.btn-warning { background: #fff3cd; color: #856404; border: 1px solid rgba(133, 100, 4, 0.2); }
|
| 308 |
+
.btn-warning:hover:not(:disabled) { background: #ffc107; color: #212529; border-color: #ffc107; }
|
| 309 |
+
|
| 310 |
+
/* [OPTIMIZATION] 8. Table 样式优化,增强可读性 */
|
| 311 |
+
.table-container { overflow-x: auto; }
|
| 312 |
+
table { width: 100%; border-collapse: collapse; }
|
| 313 |
+
th {
|
| 314 |
+
text-align: left; padding: 16px 24px; font-size: 13px;
|
| 315 |
+
font-weight: 500; color: var(--text-muted); text-transform: uppercase;
|
| 316 |
+
letter-spacing: 0.5px; background: transparent; /* 移除背景色 */
|
| 317 |
+
border-bottom: 2px solid var(--border); /* 加粗底部边框 */
|
| 318 |
+
}
|
| 319 |
+
td {
|
| 320 |
+
padding: 18px 24px; border-bottom: 1px solid var(--border);
|
| 321 |
+
font-size: 14px; color: var(--text-main);
|
| 322 |
+
transition: background-color 0.2s;
|
| 323 |
+
}
|
| 324 |
+
tr:last-child td { border-bottom: none; }
|
| 325 |
+
tr:hover td { background: var(--hover-bg); }
|
| 326 |
+
|
| 327 |
+
/* [OPTIMIZATION] 9. Form 样式优化 */
|
| 328 |
+
.form-group {
|
| 329 |
+
display: flex;
|
| 330 |
+
flex-direction: column;
|
| 331 |
+
margin-bottom: 20px;
|
| 332 |
+
}
|
| 333 |
+
.form-group label,
|
| 334 |
+
.form-label {
|
| 335 |
+
display: block;
|
| 336 |
+
margin-bottom: 12px;
|
| 337 |
+
font-size: 14px;
|
| 338 |
+
font-weight: 600;
|
| 339 |
+
color: var(--text-main);
|
| 340 |
+
letter-spacing: 0.2px;
|
| 341 |
+
}
|
| 342 |
+
.form-group input, .form-group textarea, .form-group select,
|
| 343 |
+
.form-input,
|
| 344 |
+
.form-textarea {
|
| 345 |
+
width: 100%;
|
| 346 |
+
padding: 14px 16px;
|
| 347 |
+
border-radius: var(--radius-md);
|
| 348 |
+
border: 1px solid var(--border);
|
| 349 |
+
background: var(--bg);
|
| 350 |
+
color: var(--text-main);
|
| 351 |
+
font-size: 14px;
|
| 352 |
+
transition: var(--transition-ease);
|
| 353 |
+
box-sizing: border-box;
|
| 354 |
+
line-height: 1.5;
|
| 355 |
+
}
|
| 356 |
+
.form-textarea {
|
| 357 |
+
min-height: 90px;
|
| 358 |
+
resize: vertical;
|
| 359 |
+
font-family: inherit;
|
| 360 |
+
}
|
| 361 |
+
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
|
| 362 |
+
outline: none;
|
| 363 |
+
border-color: var(--primary);
|
| 364 |
+
box-shadow: 0 0 0 3px var(--primary-light), 0 1px 2px rgba(0,0,0,0.05) inset;
|
| 365 |
+
}
|
| 366 |
+
.form-group input:disabled {
|
| 367 |
+
background: var(--hover-bg);
|
| 368 |
+
color: var(--text-muted);
|
| 369 |
+
cursor: not-allowed;
|
| 370 |
+
}
|
| 371 |
+
.form-group small {
|
| 372 |
+
display: block;
|
| 373 |
+
margin-top: 6px;
|
| 374 |
+
font-size: 13px;
|
| 375 |
+
color: var(--text-muted);
|
| 376 |
+
}
|
| 377 |
+
.form-row {
|
| 378 |
+
display: grid;
|
| 379 |
+
grid-template-columns: 1fr 1fr;
|
| 380 |
+
gap: 24px;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* Settings Section 样式 */
|
| 384 |
+
.settings-section {
|
| 385 |
+
background: var(--bg);
|
| 386 |
+
border: 1px solid var(--border);
|
| 387 |
+
border-radius: var(--radius-lg);
|
| 388 |
+
padding: 28px;
|
| 389 |
+
margin-bottom: 28px;
|
| 390 |
+
}
|
| 391 |
+
.settings-section:last-child {
|
| 392 |
+
margin-bottom: 0;
|
| 393 |
+
}
|
| 394 |
+
.settings-section h3 {
|
| 395 |
+
font-size: 17px;
|
| 396 |
+
font-weight: 600;
|
| 397 |
+
color: var(--text-main);
|
| 398 |
+
margin-bottom: 24px;
|
| 399 |
+
padding-bottom: 16px;
|
| 400 |
+
border-bottom: 1px solid var(--border);
|
| 401 |
+
display: flex;
|
| 402 |
+
align-items: center;
|
| 403 |
+
}
|
| 404 |
+
.settings-section .form-group {
|
| 405 |
+
margin-bottom: 24px;
|
| 406 |
+
}
|
| 407 |
+
.settings-section .form-group:last-of-type {
|
| 408 |
+
margin-bottom: 20px;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
/* [OPTIMIZATION] 10. Modal 动画与样式优化 */
|
| 412 |
+
.modal {
|
| 413 |
+
display: flex; /* 改为flex,便于控制 */
|
| 414 |
+
align-items: center; justify-content: center;
|
| 415 |
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
| 416 |
+
background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px);
|
| 417 |
+
z-index: 1000; opacity: 0; visibility: hidden;
|
| 418 |
+
transition: opacity 0.3s, visibility 0.3s;
|
| 419 |
+
}
|
| 420 |
+
.modal.show { opacity: 1; visibility: visible; }
|
| 421 |
+
.modal-content {
|
| 422 |
+
background: var(--card-bg); border-radius: var(--radius-lg);
|
| 423 |
+
width: 600px; max-width: 90vw; max-height: 90vh;
|
| 424 |
+
overflow-y: auto; box-shadow: var(--shadow-lg);
|
| 425 |
+
transform: translateY(20px) scale(0.98);
|
| 426 |
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 427 |
+
}
|
| 428 |
+
.modal.show .modal-content { transform: translateY(0) scale(1); }
|
| 429 |
+
.modal-header { padding: 24px; border-bottom: 1px solid var(--border); }
|
| 430 |
+
.modal-header h3 { font-size: 20px; font-weight: 600; display: inline-block; }
|
| 431 |
+
.modal-close {
|
| 432 |
+
width: 36px; height: 36px; border: none; background: transparent;
|
| 433 |
+
color: var(--text-muted); cursor: pointer; border-radius: 50%;
|
| 434 |
+
display: flex; align-items: center; justify-content: center;
|
| 435 |
+
font-size: 22px; transition: var(--transition-ease);
|
| 436 |
+
float: right;
|
| 437 |
+
}
|
| 438 |
+
.modal-close:hover { background: var(--hover-bg); color: var(--text-main); transform: rotate(90deg); }
|
| 439 |
+
.modal-body { padding: 24px; }
|
| 440 |
+
.modal-footer {
|
| 441 |
+
display: flex; justify-content: flex-end; gap: 12px;
|
| 442 |
+
padding: 20px 24px; border-top: 1px solid var(--border);
|
| 443 |
+
background: var(--hover-bg);
|
| 444 |
+
border-bottom-left-radius: var(--radius-lg);
|
| 445 |
+
border-bottom-right-radius: var(--radius-lg);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
/* [OPTIMIZATION] 11. Stats Card 优化 */
|
| 450 |
+
.stats-grid {
|
| 451 |
+
display: grid;
|
| 452 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 453 |
+
gap: 24px;
|
| 454 |
+
margin-bottom: 32px;
|
| 455 |
+
}
|
| 456 |
+
.stat-card {
|
| 457 |
+
background: var(--card-bg); border: 1px solid var(--border);
|
| 458 |
+
border-radius: var(--radius-lg); padding: 24px;
|
| 459 |
+
display: flex; flex-direction: column; /* 垂直布局 */
|
| 460 |
+
align-items: flex-start; gap: 16px;
|
| 461 |
+
transition: var(--transition-ease);
|
| 462 |
+
/* [NEW] 入场动画 */
|
| 463 |
+
opacity: 0;
|
| 464 |
+
transform: translateY(20px);
|
| 465 |
+
animation: fadeIn-up 0.5s ease-out forwards;
|
| 466 |
+
}
|
| 467 |
+
/* [NEW] Staggered Animation for Stats Cards */
|
| 468 |
+
.stat-card:nth-child(1) { animation-delay: 0.1s; }
|
| 469 |
+
.stat-card:nth-child(2) { animation-delay: 0.2s; }
|
| 470 |
+
.stat-card:nth-child(3) { animation-delay: 0.3s; }
|
| 471 |
+
.stat-card:nth-child(4) { animation-delay: 0.4s; }
|
| 472 |
+
|
| 473 |
+
@keyframes fadeIn-up {
|
| 474 |
+
to {
|
| 475 |
+
opacity: 1;
|
| 476 |
+
transform: translateY(0);
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
.stat-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-md); border-color: var(--primary); }
|
| 480 |
+
|
| 481 |
+
.stat-info-top { display: flex; justify-content: space-between; align-items: center; width: 100%; }
|
| 482 |
+
.stat-info-top p { font-size: 14px; font-weight: 500; color: var(--text-muted); }
|
| 483 |
+
|
| 484 |
+
.stat-icon {
|
| 485 |
+
width: 40px; height: 40px; border-radius: var(--radius-md);
|
| 486 |
+
display: flex; align-items: center; justify-content: center; font-size: 20px;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.stat-info-bottom h3 { font-size: 32px; font-weight: 600; color: var(--text-main); }
|
| 490 |
+
.stat-icon.blue { background: var(--primary-light); color: var(--primary); }
|
| 491 |
+
.stat-icon.green { background: var(--success-light); color: var(--success); }
|
| 492 |
+
.stat-icon.red { background: var(--danger-light); color: var(--danger); }
|
| 493 |
+
.stat-icon.yellow { background: var(--warning-light); color: #b06000; }
|
| 494 |
+
|
| 495 |
+
/* 其他样式保持或微调 */
|
| 496 |
+
.badge {
|
| 497 |
+
padding: 5px 12px; border-radius: 50px;
|
| 498 |
+
font-size: 12px; font-weight: 500;
|
| 499 |
+
}
|
| 500 |
+
.empty-state { text-align: center; padding: 80px 20px; color: var(--text-muted); }
|
| 501 |
+
.empty-state-icon { font-size: 56px; margin-bottom: 20px; opacity: 0.4; }
|
| 502 |
+
|
| 503 |
+
.toast {
|
| 504 |
+
position: fixed; bottom: 32px; left: 50%;
|
| 505 |
+
transform: translateX(-50%) translateY(100px);
|
| 506 |
+
background: var(--card-bg); border: 1px solid var(--border);
|
| 507 |
+
border-radius: var(--radius-md); padding: 16px 24px;
|
| 508 |
+
box-shadow: var(--shadow-lg); min-width: 320px;
|
| 509 |
+
z-index: 2000; opacity: 0; visibility: hidden;
|
| 510 |
+
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 511 |
+
display: flex; align-items: center; gap: 12px;
|
| 512 |
+
}
|
| 513 |
+
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; visibility: visible; }
|
| 514 |
+
|
| 515 |
+
/* [NEW] SVG Icon Styles */
|
| 516 |
+
.icon {
|
| 517 |
+
width: 1em;
|
| 518 |
+
height: 1em;
|
| 519 |
+
stroke-width: 2;
|
| 520 |
+
fill: none;
|
| 521 |
+
stroke: currentColor;
|
| 522 |
+
stroke-linecap: round;
|
| 523 |
+
stroke-linejoin: round;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
/* Responsive */
|
| 527 |
+
@media (max-width: 768px) {
|
| 528 |
+
.container { padding: 24px 16px; }
|
| 529 |
+
.header { flex-direction: column; gap: 24px; text-align: center; }
|
| 530 |
+
.tabs {
|
| 531 |
+
gap: 8px;
|
| 532 |
+
/* [NEW] 允许在移动端横向滚动 */
|
| 533 |
+
overflow-x: auto;
|
| 534 |
+
white-space: nowrap;
|
| 535 |
+
-ms-overflow-style: none; /* IE and Edge */
|
| 536 |
+
scrollbar-width: none; /* Firefox */
|
| 537 |
+
}
|
| 538 |
+
.tabs::-webkit-scrollbar { display: none; } /* Chrome, Safari, and Opera */
|
| 539 |
+
.tab { flex-shrink: 0; }
|
| 540 |
+
.form-row { grid-template-columns: 1fr; }
|
| 541 |
+
.stats-grid { gap: 16px; }
|
| 542 |
+
}
|
| 543 |
+
</style>
|
| 544 |
+
</head>
|
| 545 |
+
<body>
|
| 546 |
+
<!-- [NEW] SVG Icon Definitions -->
|
| 547 |
+
<svg width="0" height="0" style="display: none;">
|
| 548 |
+
<symbol id="icon-users" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></symbol>
|
| 549 |
+
<symbol id="icon-robot" viewBox="0 0 24 24"><path d="M12 8V4H8"></path><rect x="4" y="12" width="16" height="8" rx="2"></rect><path d="M2 12h20"></path><path d="M12 12V8a4 4 0 0 0-4-4"></path></symbol>
|
| 550 |
+
<symbol id="icon-settings" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 0 2l-.15.08a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l-.22-.38a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1 0-2l.15-.08a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
|
| 551 |
+
<symbol id="icon-server" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></symbol>
|
| 552 |
+
<symbol id="icon-list" viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></symbol>
|
| 553 |
+
<symbol id="icon-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></symbol>
|
| 554 |
+
<symbol id="icon-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></symbol>
|
| 555 |
+
<symbol id="icon-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
|
| 556 |
+
<symbol id="icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></symbol>
|
| 557 |
+
<symbol id="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></symbol>
|
| 558 |
+
<symbol id="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></symbol>
|
| 559 |
+
<symbol id="icon-message" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></symbol>
|
| 560 |
+
<symbol id="icon-play" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></symbol>
|
| 561 |
+
<symbol id="icon-pause" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></symbol>
|
| 562 |
+
<symbol id="icon-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></symbol>
|
| 563 |
+
<symbol id="icon-key" viewBox="0 0 24 24"><path d="M21 2l-2 2"></path><path d="M9 6l-2 2"></path><circle cx="7.5" cy="15.5" r="5.5"></circle><path d="M21 2l-9.6 9.6"></path><path d="M15.5 7.5l3 3"></path><path d="M16 13l-3-3"></path></symbol>
|
| 564 |
+
</svg>
|
| 565 |
+
|
| 566 |
+
<div class="container">
|
| 567 |
+
<!-- Header -->
|
| 568 |
+
<header class="header">
|
| 569 |
+
<div class="header-left">
|
| 570 |
+
<div class="logo">G</div>
|
| 571 |
+
<h1>Business Gemini Pool <span>管理控制台</span></h1>
|
| 572 |
+
</div>
|
| 573 |
+
<div class="header-right">
|
| 574 |
+
<div class="status-indicator" id="serviceStatus">服务运行中</div>
|
| 575 |
+
<div class="log-level-control">
|
| 576 |
+
<label for="logLevelSelect">日志</label>
|
| 577 |
+
<select id="logLevelSelect" class="log-level-select" onchange="updateLogLevel(this.value)">
|
| 578 |
+
<option value="DEBUG">DEBUG</option>
|
| 579 |
+
<option value="INFO" selected>INFO</option>
|
| 580 |
+
<option value="ERROR">ERROR</option>
|
| 581 |
+
</select>
|
| 582 |
+
</div>
|
| 583 |
+
<button class="btn btn-outline" id="loginButton" style="padding: 8px 12px;" onclick="showLoginModal()">登录</button>
|
| 584 |
+
<a href="chat_history.html" class="btn btn-primary" style="padding: 8px 16px; font-size: 14px; text-decoration: none; display: flex; align-items: center; gap: 6px;" title="进入在线对话">
|
| 585 |
+
<svg class="icon" style="width: 16px; height: 16px;"><use xlink:href="#icon-message"></use></svg>
|
| 586 |
+
在线对话
|
| 587 |
+
</a>
|
| 588 |
+
<button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
|
| 589 |
+
<span id="themeIconContainer">
|
| 590 |
+
<svg class="icon"><use xlink:href="#icon-sun"></use></svg>
|
| 591 |
+
</span>
|
| 592 |
+
</button>
|
| 593 |
+
</div>
|
| 594 |
+
</header>
|
| 595 |
+
|
| 596 |
+
<!-- Tabs -->
|
| 597 |
+
<div class="tabs">
|
| 598 |
+
<button class="tab active" onclick="switchTab('accounts')">
|
| 599 |
+
<svg class="icon tab-icon"><use xlink:href="#icon-users"></use></svg>
|
| 600 |
+
账号管理
|
| 601 |
+
</button>
|
| 602 |
+
<button class="tab" onclick="switchTab('models')">
|
| 603 |
+
<svg class="icon tab-icon"><use xlink:href="#icon-robot"></use></svg>
|
| 604 |
+
模型管理
|
| 605 |
+
</button>
|
| 606 |
+
<button class="tab" onclick="switchTab('settings')">
|
| 607 |
+
<svg class="icon tab-icon"><use xlink:href="#icon-settings"></use></svg>
|
| 608 |
+
系统设置
|
| 609 |
+
</button>
|
| 610 |
+
<button class="tab" onclick="switchTab('tokens')">
|
| 611 |
+
<svg class="icon tab-icon"><use xlink:href="#icon-key"></use></svg>
|
| 612 |
+
Token 管理
|
| 613 |
+
</button>
|
| 614 |
+
</div>
|
| 615 |
+
|
| 616 |
+
<!-- 账号管理 -->
|
| 617 |
+
<div id="accounts" class="tab-content active">
|
| 618 |
+
<!-- Stats -->
|
| 619 |
+
<div class="stats-grid">
|
| 620 |
+
<div class="stat-card">
|
| 621 |
+
<div class="stat-info-top">
|
| 622 |
+
<p>总账号数</p>
|
| 623 |
+
<div class="stat-icon blue"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div>
|
| 624 |
+
</div>
|
| 625 |
+
<div class="stat-info-bottom">
|
| 626 |
+
<h3 id="totalAccounts">0</h3>
|
| 627 |
+
</div>
|
| 628 |
+
</div>
|
| 629 |
+
<div class="stat-card">
|
| 630 |
+
<div class="stat-info-top">
|
| 631 |
+
<p>可用账号</p>
|
| 632 |
+
<div class="stat-icon green"><svg class="icon"><use xlink:href="#icon-check"></use></svg></div>
|
| 633 |
+
</div>
|
| 634 |
+
<div class="stat-info-bottom">
|
| 635 |
+
<h3 id="availableAccounts">0</h3>
|
| 636 |
+
</div>
|
| 637 |
+
</div>
|
| 638 |
+
<div class="stat-card">
|
| 639 |
+
<div class="stat-info-top">
|
| 640 |
+
<p>不可用账号</p>
|
| 641 |
+
<div class="stat-icon red"><svg class="icon"><use xlink:href="#icon-x"></use></svg></div>
|
| 642 |
+
</div>
|
| 643 |
+
<div class="stat-info-bottom">
|
| 644 |
+
<h3 id="unavailableAccounts">0</h3>
|
| 645 |
+
</div>
|
| 646 |
+
</div>
|
| 647 |
+
<div class="stat-card">
|
| 648 |
+
<div class="stat-info-top">
|
| 649 |
+
<p>当前轮训索引</p>
|
| 650 |
+
<div class="stat-icon yellow"><svg class="icon"><use xlink:href="#icon-refresh"></use></svg></div>
|
| 651 |
+
</div>
|
| 652 |
+
<div class="stat-info-bottom">
|
| 653 |
+
<h3 id="currentIndex">0</h3>
|
| 654 |
+
</div>
|
| 655 |
+
</div>
|
| 656 |
+
</div>
|
| 657 |
+
|
| 658 |
+
<div class="card">
|
| 659 |
+
<div class="card-header">
|
| 660 |
+
<div class="card-title">
|
| 661 |
+
<svg class="icon card-title-icon"><use xlink:href="#icon-list"></use></svg>
|
| 662 |
+
账号列表
|
| 663 |
+
</div>
|
| 664 |
+
<button class="btn btn-primary" onclick="showAddAccountModal()">
|
| 665 |
+
<svg class="icon"><use xlink:href="#icon-plus"></use></svg>
|
| 666 |
+
添加账号
|
| 667 |
+
</button>
|
| 668 |
+
</div>
|
| 669 |
+
<div class="table-container">
|
| 670 |
+
<table id="accountsTable">
|
| 671 |
+
<thead>
|
| 672 |
+
<tr>
|
| 673 |
+
<th>序号</th>
|
| 674 |
+
<th>Team ID</th>
|
| 675 |
+
<th>csesidx</th>
|
| 676 |
+
<th>User Agent</th>
|
| 677 |
+
<th>状态</th>
|
| 678 |
+
<th>操作</th>
|
| 679 |
+
</tr>
|
| 680 |
+
</thead>
|
| 681 |
+
<tbody id="accountsTableBody"></tbody>
|
| 682 |
+
</table>
|
| 683 |
+
</div>
|
| 684 |
+
</div>
|
| 685 |
+
</div>
|
| 686 |
+
|
| 687 |
+
<!-- 模型管理 (HTML结构类似,图标已替换) -->
|
| 688 |
+
<div id="models" class="tab-content">
|
| 689 |
+
<div class="card">
|
| 690 |
+
<div class="card-header">
|
| 691 |
+
<div class="card-title">
|
| 692 |
+
<svg class="icon card-title-icon"><use xlink:href="#icon-robot"></use></svg>
|
| 693 |
+
模型列表
|
| 694 |
+
</div>
|
| 695 |
+
<button class="btn btn-primary" onclick="showAddModelModal()">
|
| 696 |
+
<svg class="icon"><use xlink:href="#icon-plus"></use></svg>
|
| 697 |
+
添加模型
|
| 698 |
+
</button>
|
| 699 |
+
</div>
|
| 700 |
+
<div class="table-container">
|
| 701 |
+
<table id="modelsTable">
|
| 702 |
+
<thead>
|
| 703 |
+
<tr>
|
| 704 |
+
<th>模型ID</th>
|
| 705 |
+
<th>名称</th>
|
| 706 |
+
<th>描述</th>
|
| 707 |
+
<th>上下文长度</th>
|
| 708 |
+
<th>最大Token</th>
|
| 709 |
+
<th>状态</th>
|
| 710 |
+
<th>操作</th>
|
| 711 |
+
</tr>
|
| 712 |
+
</thead>
|
| 713 |
+
<tbody id="modelsTableBody"></tbody>
|
| 714 |
+
</table>
|
| 715 |
+
</div>
|
| 716 |
+
</div>
|
| 717 |
+
</div>
|
| 718 |
+
|
| 719 |
+
<!-- 系统设置 (HTML结构类似,图标已替换) -->
|
| 720 |
+
<div id="settings" class="tab-content">
|
| 721 |
+
<div class="card">
|
| 722 |
+
<div class="card-header">
|
| 723 |
+
<div class="card-title">
|
| 724 |
+
<svg class="icon card-title-icon"><use xlink:href="#icon-settings"></use></svg>
|
| 725 |
+
系统配置
|
| 726 |
+
</div>
|
| 727 |
+
</div>
|
| 728 |
+
<div class="card-body">
|
| 729 |
+
<form id="settingsForm">
|
| 730 |
+
<div class="settings-section">
|
| 731 |
+
<h3>代理设置</h3>
|
| 732 |
+
<div class="form-group">
|
| 733 |
+
<label class="form-label" for="proxyUrl">代理地址</label>
|
| 734 |
+
<input type="text" class="form-input" id="proxyUrl" placeholder="http://127.0.0.1:7890">
|
| 735 |
+
<small>用于访问Google API的代理服务器地址</small>
|
| 736 |
+
<div class="proxy-status" id="proxyStatus"></div>
|
| 737 |
+
</div>
|
| 738 |
+
<div class="form-group">
|
| 739 |
+
<label class="form-label" for="imageOutputMode">图片输出模式</label>
|
| 740 |
+
<select class="form-input" id="imageOutputMode">
|
| 741 |
+
<option value="url">图片URL(默认)</option>
|
| 742 |
+
<option value="base64">Base64 Data URL</option>
|
| 743 |
+
</select>
|
| 744 |
+
<small>控制聊天接口返回的图片是以URL形式还是以 data:image/...;base64,... 形式输出</small>
|
| 745 |
+
</div>
|
| 746 |
+
<div style="display: flex; gap: 12px;">
|
| 747 |
+
<button type="button" class="btn btn-outline" onclick="testProxy()">
|
| 748 |
+
测试代理
|
| 749 |
+
</button>
|
| 750 |
+
<button type="button" class="btn btn-primary" onclick="saveSettings()">
|
| 751 |
+
保存设置
|
| 752 |
+
</button>
|
| 753 |
+
</div>
|
| 754 |
+
</div>
|
| 755 |
+
|
| 756 |
+
<div class="settings-section">
|
| 757 |
+
<h3><svg class="icon" style="width: 1em; height: 1em; vertical-align: -2px; margin-right: 8px;"><use xlink:href="#icon-server"></use></svg>服务信息</h3>
|
| 758 |
+
<div class="form-row">
|
| 759 |
+
<div class="form-group">
|
| 760 |
+
<label class="form-label">服务端口</label>
|
| 761 |
+
<input type="text" class="form-input" value="8000" disabled>
|
| 762 |
+
</div>
|
| 763 |
+
<div class="form-group">
|
| 764 |
+
<label class="form-label">API地址</label>
|
| 765 |
+
<input type="text" class="form-input" value="http://localhost:8000/v1" disabled>
|
| 766 |
+
</div>
|
| 767 |
+
</div>
|
| 768 |
+
</div>
|
| 769 |
+
|
| 770 |
+
<div class="settings-section">
|
| 771 |
+
<h3>配置文件</h3>
|
| 772 |
+
<div class="form-group">
|
| 773 |
+
<label class="form-label" for="configJson">当前配置 (JSON)</label>
|
| 774 |
+
<textarea class="form-textarea" id="configJson" rows="15" readonly></textarea>
|
| 775 |
+
<small>配置文件路径: business_gemini_session.json</small>
|
| 776 |
+
</div>
|
| 777 |
+
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
| 778 |
+
<button type="button" class="btn btn-outline" onclick="refreshConfig()">
|
| 779 |
+
刷新配置
|
| 780 |
+
</button>
|
| 781 |
+
<button type="button" class="btn btn-outline" onclick="downloadConfig()">
|
| 782 |
+
下载配置
|
| 783 |
+
</button>
|
| 784 |
+
<button type="button" class="btn btn-primary" onclick="uploadConfig()">
|
| 785 |
+
导入配置
|
| 786 |
+
</button>
|
| 787 |
+
<input type="file" id="configFileInput" accept=".json" style="display: none;" onchange="handleConfigUpload(event)">
|
| 788 |
+
</div>
|
| 789 |
+
</div>
|
| 790 |
+
</form>
|
| 791 |
+
</div>
|
| 792 |
+
</div>
|
| 793 |
+
</div>
|
| 794 |
+
|
| 795 |
+
<!-- Token 管理 -->
|
| 796 |
+
<div id="tokens" class="tab-content">
|
| 797 |
+
<div class="card">
|
| 798 |
+
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
| 799 |
+
<div class="card-title" style="display:flex; align-items:center; gap:8px;">
|
| 800 |
+
<svg class="icon card-title-icon"><use xlink:href="#icon-key"></use></svg>
|
| 801 |
+
Token 管理
|
| 802 |
+
</div>
|
| 803 |
+
<div class="token-actions">
|
| 804 |
+
<input id="manualToken" class="form-input token-input" placeholder="手动输入 Token(留空自动生成)">
|
| 805 |
+
<button class="btn btn-outline" type="button" onclick="generateToken()">生成 Token</button>
|
| 806 |
+
<button class="btn btn-primary" type="button" onclick="addToken()">添加 Token</button>
|
| 807 |
+
</div>
|
| 808 |
+
</div>
|
| 809 |
+
<table class="table">
|
| 810 |
+
<thead>
|
| 811 |
+
<tr>
|
| 812 |
+
<th style="width:70%;">Token</th>
|
| 813 |
+
<th>操作</th>
|
| 814 |
+
</tr>
|
| 815 |
+
</thead>
|
| 816 |
+
<tbody id="tokensTableBody">
|
| 817 |
+
<tr><td colspan="2" class="empty-state">加载中...</td></tr>
|
| 818 |
+
</tbody>
|
| 819 |
+
</table>
|
| 820 |
+
</div>
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
|
| 824 |
+
<!-- 模态框 (已优化关闭按钮) -->
|
| 825 |
+
<div class="modal" id="addAccountModal">
|
| 826 |
+
<div class="modal-content">
|
| 827 |
+
<div class="modal-header">
|
| 828 |
+
<h3>添加账号</h3>
|
| 829 |
+
<button class="modal-close" onclick="closeModal('addAccountModal')" title="关闭">×</button>
|
| 830 |
+
</div>
|
| 831 |
+
<!-- Modal Body and Footer ... (No functional changes needed) -->
|
| 832 |
+
<div class="modal-body">
|
| 833 |
+
<div class="form-group">
|
| 834 |
+
<label class="form-label" for="newAccountJson">粘贴账号JSON(可直接复制工具输出)</label>
|
| 835 |
+
<textarea class="form-textarea" id="newAccountJson" placeholder='{"team_id":"...","secure_c_ses":"...","host_c_oses":"...","csesidx":"...","user_agent":"..."}' rows="4"></textarea>
|
| 836 |
+
<div style="display:flex; gap:8px; margin-top:8px;">
|
| 837 |
+
<button class="btn btn-outline btn-sm" type="button" onclick="parseAccountJson()">解析填充</button>
|
| 838 |
+
<button class="btn btn-outline btn-sm" type="button" onclick="pasteAccountJson()">从剪贴板读取并填充</button>
|
| 839 |
+
</div>
|
| 840 |
+
</div>
|
| 841 |
+
<div class="form-group">
|
| 842 |
+
<label class="form-label" for="newTeamId">Team ID</label>
|
| 843 |
+
<input type="text" class="form-input" id="newTeamId" placeholder="输入Team ID">
|
| 844 |
+
</div>
|
| 845 |
+
<div class="form-group">
|
| 846 |
+
<label class="form-label" for="newSecureCses">Cookie中的__Secure-C_SES</label>
|
| 847 |
+
<textarea class="form-textarea" id="newSecureCses" placeholder="输入Cookie中的__Secure-C_SES" rows="3"></textarea>
|
| 848 |
+
</div>
|
| 849 |
+
<div class="form-group">
|
| 850 |
+
<label class="form-label" for="newHostCoses">Cookie中的__Host-C_OSES</label>
|
| 851 |
+
<textarea class="form-textarea" id="newHostCoses" placeholder="输入Cookie中的__Host-C_OSES" rows="3"></textarea>
|
| 852 |
+
</div>
|
| 853 |
+
<div class="form-group">
|
| 854 |
+
<label class="form-label" for="newCsesidx">CSESIDX</label>
|
| 855 |
+
<input type="text" class="form-input" id="newCsesidx" placeholder="输入CSESIDX">
|
| 856 |
+
</div>
|
| 857 |
+
<div class="form-group">
|
| 858 |
+
<label class="form-label" for="newUserAgent">User Agent</label>
|
| 859 |
+
<input type="text" class="form-input" id="newUserAgent" placeholder="输入User Agent">
|
| 860 |
+
</div>
|
| 861 |
+
</div>
|
| 862 |
+
<div class="modal-footer">
|
| 863 |
+
<button class="btn btn-outline" onclick="closeModal('addAccountModal')">取消</button>
|
| 864 |
+
<button class="btn btn-primary" onclick="saveNewAccount()">保存</button>
|
| 865 |
+
</div>
|
| 866 |
+
</div>
|
| 867 |
+
</div>
|
| 868 |
+
<!-- 编辑账号模态框 -->
|
| 869 |
+
<div class="modal" id="editAccountModal">
|
| 870 |
+
<div class="modal-content">
|
| 871 |
+
<div class="modal-header">
|
| 872 |
+
<h3>编辑账号</h3>
|
| 873 |
+
<button class="modal-close" onclick="closeModal('editAccountModal')" title="关闭">×</button>
|
| 874 |
+
</div>
|
| 875 |
+
<div class="modal-body">
|
| 876 |
+
<input type="hidden" id="editAccountId">
|
| 877 |
+
<div class="form-group">
|
| 878 |
+
<label class="form-label" for="editTeamId">Team ID</label>
|
| 879 |
+
<input type="text" class="form-input" id="editTeamId" placeholder="输入Team ID">
|
| 880 |
+
</div>
|
| 881 |
+
<div class="form-group">
|
| 882 |
+
<label class="form-label" for="editSecureCses">Cookie中的__Secure-C_SES</label>
|
| 883 |
+
<textarea class="form-textarea" id="editSecureCses" placeholder="输入Secure C Ses" rows="3"></textarea>
|
| 884 |
+
</div>
|
| 885 |
+
<div class="form-group">
|
| 886 |
+
<label class="form-label" for="editHostCoses">Cookie中的__Host-C_OSES</label>
|
| 887 |
+
<textarea class="form-textarea" id="editHostCoses" placeholder="输入Host C Oses" rows="3"></textarea>
|
| 888 |
+
</div>
|
| 889 |
+
<div class="form-group">
|
| 890 |
+
<label class="form-label" for="editCsesidx">CSESIDX</label>
|
| 891 |
+
<input type="text" class="form-input" id="editCsesidx" placeholder="输入CSESIDX">
|
| 892 |
+
</div>
|
| 893 |
+
<div class="form-group">
|
| 894 |
+
<label class="form-label" for="editUserAgent">User Agent</label>
|
| 895 |
+
<input type="text" class="form-input" id="editUserAgent" placeholder="输入User Agent">
|
| 896 |
+
</div>
|
| 897 |
+
</div>
|
| 898 |
+
<div class="modal-footer">
|
| 899 |
+
<button class="btn btn-outline" onclick="closeModal('editAccountModal')">取消</button>
|
| 900 |
+
<button class="btn btn-primary" onclick="updateAccount()">保存</button>
|
| 901 |
+
</div>
|
| 902 |
+
</div>
|
| 903 |
+
</div>
|
| 904 |
+
|
| 905 |
+
<!-- 刷新Cookie模态框 -->
|
| 906 |
+
<div class="modal" id="refreshCookieModal">
|
| 907 |
+
<div class="modal-content">
|
| 908 |
+
<div class="modal-header">
|
| 909 |
+
<h3>刷新账号Cookie</h3>
|
| 910 |
+
<button class="modal-close" onclick="closeModal('refreshCookieModal')" title="关闭">×</button>
|
| 911 |
+
</div>
|
| 912 |
+
<div class="modal-body">
|
| 913 |
+
<input type="hidden" id="refreshAccountId">
|
| 914 |
+
<p class="text-muted" style="margin-bottom: 16px;">请输入新的Cookie值来刷新账号认证信息。刷新后将清除JWT缓存。</p>
|
| 915 |
+
<div class="form-group">
|
| 916 |
+
<label class="form-label" for="refreshSecureCses">Cookie中的__Secure-C_SES <span style="color: var(--danger);">*</span></label>
|
| 917 |
+
<textarea class="form-textarea" id="refreshSecureCses" placeholder="输入新的__Secure-C_SES值" rows="3"></textarea>
|
| 918 |
+
</div>
|
| 919 |
+
<div class="form-group">
|
| 920 |
+
<label class="form-label" for="refreshHostCoses">Cookie中的__Host-C_OSES <span style="color: var(--danger);">*</span></label>
|
| 921 |
+
<textarea class="form-textarea" id="refreshHostCoses" placeholder="输入新的__Host-C_OSES值" rows="3"></textarea>
|
| 922 |
+
</div>
|
| 923 |
+
<div class="form-group">
|
| 924 |
+
<label class="form-label" for="refreshCsesidx">CSESIDX (可选)</label>
|
| 925 |
+
<input type="text" class="form-input" id="refreshCsesidx" placeholder="输入CSESIDX值">
|
| 926 |
+
</div>
|
| 927 |
+
<div class="form-group">
|
| 928 |
+
<label class="form-label">从JSON粘贴 (可选)</label>
|
| 929 |
+
<textarea class="form-textarea" id="refreshCookieJson" placeholder="粘贴Cookie JSON数据" rows="3"></textarea>
|
| 930 |
+
<div style="display:flex; gap:8px; margin-top:8px;">
|
| 931 |
+
<button class="btn btn-outline btn-sm" type="button" onclick="parseRefreshCookieJson()">解析填充</button>
|
| 932 |
+
<button class="btn btn-outline btn-sm" type="button" onclick="pasteRefreshCookieJson()">📋 粘贴并解析</button>
|
| 933 |
+
</div>
|
| 934 |
+
</div>
|
| 935 |
+
</div>
|
| 936 |
+
<div class="modal-footer">
|
| 937 |
+
<button class="btn btn-outline" onclick="closeModal('refreshCookieModal')">取消</button>
|
| 938 |
+
<button class="btn btn-primary" onclick="refreshAccountCookie()">刷新Cookie</button>
|
| 939 |
+
</div>
|
| 940 |
+
</div>
|
| 941 |
+
</div>
|
| 942 |
+
|
| 943 |
+
<!-- 添加模型模态框 -->
|
| 944 |
+
<div class="modal" id="addModelModal">
|
| 945 |
+
<div class="modal-content">
|
| 946 |
+
<div class="modal-header">
|
| 947 |
+
<h3>添加模型</h3>
|
| 948 |
+
<button class="modal-close" onclick="closeModal('addModelModal')" title="关闭">×</button>
|
| 949 |
+
</div>
|
| 950 |
+
<div class="modal-body">
|
| 951 |
+
<div class="form-row">
|
| 952 |
+
<div class="form-group">
|
| 953 |
+
<label class="form-label" for="newModelId">模型ID</label>
|
| 954 |
+
<input type="text" class="form-input" id="newModelId" placeholder="如: gemini-pro">
|
| 955 |
+
</div>
|
| 956 |
+
<div class="form-group">
|
| 957 |
+
<label class="form-label" for="newModelName">模型名称</label>
|
| 958 |
+
<input type="text" class="form-input" id="newModelName" placeholder="如: Gemini Pro">
|
| 959 |
+
</div>
|
| 960 |
+
</div>
|
| 961 |
+
<div class="form-group">
|
| 962 |
+
<label class="form-label" for="newModelDesc">描述</label>
|
| 963 |
+
<input type="text" class="form-input" id="newModelDesc" placeholder="模型描述">
|
| 964 |
+
</div>
|
| 965 |
+
<div class="form-row">
|
| 966 |
+
<div class="form-group">
|
| 967 |
+
<label class="form-label" for="newContextLength">上下文长度</label>
|
| 968 |
+
<input type="number" class="form-input" id="newContextLength" value="32768">
|
| 969 |
+
</div>
|
| 970 |
+
<div class="form-group">
|
| 971 |
+
<label class="form-label" for="newMaxTokens">最大Token</label>
|
| 972 |
+
<input type="number" class="form-input" id="newMaxTokens" value="8192">
|
| 973 |
+
</div>
|
| 974 |
+
</div>
|
| 975 |
+
</div>
|
| 976 |
+
<div class="modal-footer">
|
| 977 |
+
<button class="btn btn-outline" onclick="closeModal('addModelModal')">取消</button>
|
| 978 |
+
<button class="btn btn-primary" onclick="saveNewModel()">保存</button>
|
| 979 |
+
</div>
|
| 980 |
+
</div>
|
| 981 |
+
</div>
|
| 982 |
+
|
| 983 |
+
<!-- 登录模态框 -->
|
| 984 |
+
<div class="modal" id="loginModal">
|
| 985 |
+
<div class="modal-content">
|
| 986 |
+
<div class="modal-header">
|
| 987 |
+
<h3>管理员登录</h3>
|
| 988 |
+
<button class="modal-close" onclick="closeModal('loginModal')" title="关闭">×</button>
|
| 989 |
+
</div>
|
| 990 |
+
<div class="modal-body">
|
| 991 |
+
<div class="form-group">
|
| 992 |
+
<label class="form-label" for="loginPassword">后台密码</label>
|
| 993 |
+
<input type="password" class="form-input" id="loginPassword" placeholder="输入后台密码">
|
| 994 |
+
</div>
|
| 995 |
+
<p class="text-muted" style="font-size: 12px;">首次登录将设置当前密码为后台密码。</p>
|
| 996 |
+
</div>
|
| 997 |
+
<div class="modal-footer">
|
| 998 |
+
<button class="btn btn-outline" onclick="closeModal('loginModal')">取消</button>
|
| 999 |
+
<button class="btn btn-primary" onclick="submitLogin()">登录</button>
|
| 1000 |
+
</div>
|
| 1001 |
+
</div>
|
| 1002 |
+
</div>
|
| 1003 |
+
|
| 1004 |
+
<!-- 编辑模型模态框 -->
|
| 1005 |
+
<div class="modal" id="editModelModal">
|
| 1006 |
+
<div class="modal-content">
|
| 1007 |
+
<div class="modal-header">
|
| 1008 |
+
<h3>编辑模型</h3>
|
| 1009 |
+
<button class="modal-close" onclick="closeModal('editModelModal')" title="关闭">×</button>
|
| 1010 |
+
</div>
|
| 1011 |
+
<div class="modal-body">
|
| 1012 |
+
<input type="hidden" id="editModelOriginalId">
|
| 1013 |
+
<div class="form-row">
|
| 1014 |
+
<div class="form-group">
|
| 1015 |
+
<label class="form-label" for="editModelId">模型ID</label>
|
| 1016 |
+
<input type="text" class="form-input" id="editModelId" placeholder="如: gemini-pro" readonly style="background-color: var(--bg-tertiary); cursor: not-allowed;">
|
| 1017 |
+
</div>
|
| 1018 |
+
<div class="form-group">
|
| 1019 |
+
<label class="form-label" for="editModelName">模型名称</label>
|
| 1020 |
+
<input type="text" class="form-input" id="editModelName" placeholder="如: Gemini Pro">
|
| 1021 |
+
</div>
|
| 1022 |
+
</div>
|
| 1023 |
+
<div class="form-group">
|
| 1024 |
+
<label class="form-label" for="editModelDesc">描述</label>
|
| 1025 |
+
<input type="text" class="form-input" id="editModelDesc" placeholder="模型描述">
|
| 1026 |
+
</div>
|
| 1027 |
+
<div class="form-row">
|
| 1028 |
+
<div class="form-group">
|
| 1029 |
+
<label class="form-label" for="editContextLength">上下文长度</label>
|
| 1030 |
+
<input type="number" class="form-input" id="editContextLength">
|
| 1031 |
+
</div>
|
| 1032 |
+
<div class="form-group">
|
| 1033 |
+
<label class="form-label" for="editMaxTokens">最大Token</label>
|
| 1034 |
+
<input type="number" class="form-input" id="editMaxTokens">
|
| 1035 |
+
</div>
|
| 1036 |
+
</div>
|
| 1037 |
+
</div>
|
| 1038 |
+
<div class="modal-footer">
|
| 1039 |
+
<button class="btn btn-outline" onclick="closeModal('editModelModal')">取消</button>
|
| 1040 |
+
<button class="btn btn-primary" onclick="updateModel()">保存</button>
|
| 1041 |
+
</div>
|
| 1042 |
+
</div>
|
| 1043 |
+
</div>
|
| 1044 |
+
|
| 1045 |
+
|
| 1046 |
+
<!-- Toast通知 -->
|
| 1047 |
+
<div id="toastContainer" class="toast-container">
|
| 1048 |
+
<!-- Toasts will be injected here by JS -->
|
| 1049 |
+
</div>
|
| 1050 |
+
<div class="toast" id="toast"></div>
|
| 1051 |
+
|
| 1052 |
+
<script>
|
| 1053 |
+
// [OPTIMIZATION] 1. 脚本微调以适应新的图标
|
| 1054 |
+
function updateThemeIcon(theme) {
|
| 1055 |
+
const iconContainer = document.getElementById('themeIconContainer');
|
| 1056 |
+
if (iconContainer) {
|
| 1057 |
+
const iconId = theme === 'dark' ? 'icon-sun' : 'icon-moon';
|
| 1058 |
+
iconContainer.innerHTML = `<svg class="icon"><use xlink:href="#${iconId}"></use></svg>`;
|
| 1059 |
+
}
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
// [OPTIMIZATION] 2. 改进Toast通知
|
| 1063 |
+
let toastTimeout;
|
| 1064 |
+
function showToast(message, type = 'info') {
|
| 1065 |
+
const toast = document.getElementById('toast');
|
| 1066 |
+
if (!toast) return;
|
| 1067 |
+
|
| 1068 |
+
let icon = '';
|
| 1069 |
+
let borderType = type; // 'success', 'error', 'info'
|
| 1070 |
+
switch(type) {
|
| 1071 |
+
case 'success':
|
| 1072 |
+
icon = '<svg class="icon" style="color: var(--success);"><use xlink:href="#icon-check"></use></svg>';
|
| 1073 |
+
break;
|
| 1074 |
+
case 'error':
|
| 1075 |
+
icon = '<svg class="icon" style="color: var(--danger);"><use xlink:href="#icon-x"></use></svg>';
|
| 1076 |
+
break;
|
| 1077 |
+
default:
|
| 1078 |
+
icon = '<svg class="icon" style="color: var(--primary);"><use xlink:href="#icon-server"></use></svg>';
|
| 1079 |
+
borderType = 'primary';
|
| 1080 |
+
break;
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
toast.innerHTML = `${icon} <span class="toast-message">${message}</span>`;
|
| 1084 |
+
toast.className = `toast show`;
|
| 1085 |
+
toast.style.borderLeft = `4px solid var(--${borderType})`;
|
| 1086 |
+
|
| 1087 |
+
clearTimeout(toastTimeout);
|
| 1088 |
+
toastTimeout = setTimeout(() => {
|
| 1089 |
+
toast.classList.remove('show');
|
| 1090 |
+
}, 3500);
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
// =======================================================
|
| 1094 |
+
// [FULL SCRIPT] 以下是完整的、未删减的功能性 JavaScript 代码
|
| 1095 |
+
// =======================================================
|
| 1096 |
+
|
| 1097 |
+
// API 基础 URL
|
| 1098 |
+
const API_BASE = '.';
|
| 1099 |
+
|
| 1100 |
+
// 全局数据缓存
|
| 1101 |
+
let accountsData = [];
|
| 1102 |
+
let modelsData = [];
|
| 1103 |
+
let configData = {};
|
| 1104 |
+
let currentEditAccountId = null;
|
| 1105 |
+
let currentEditModelId = null;
|
| 1106 |
+
const ADMIN_TOKEN_KEY = 'admin_token';
|
| 1107 |
+
let tokensData = [];
|
| 1108 |
+
|
| 1109 |
+
// --- 初始化 ---
|
| 1110 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1111 |
+
initTheme();
|
| 1112 |
+
loadAllData();
|
| 1113 |
+
setInterval(checkServerStatus, 30000); // 每30秒检查一次服务状态
|
| 1114 |
+
updateLoginButton();
|
| 1115 |
+
});
|
| 1116 |
+
|
| 1117 |
+
// --- 核心加载与渲染 ---
|
| 1118 |
+
async function loadAllData() {
|
| 1119 |
+
await Promise.all([
|
| 1120 |
+
loadAccounts(),
|
| 1121 |
+
loadModels(),
|
| 1122 |
+
loadConfig(),
|
| 1123 |
+
checkServerStatus(),
|
| 1124 |
+
loadLogLevel(),
|
| 1125 |
+
loadTokens()
|
| 1126 |
+
]);
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
function getAuthHeaders() {
|
| 1130 |
+
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
|
| 1131 |
+
return token ? { 'X-Admin-Token': token } : {};
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
function updateLoginButton() {
|
| 1135 |
+
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
|
| 1136 |
+
const btn = document.getElementById('loginButton');
|
| 1137 |
+
if (!btn) return;
|
| 1138 |
+
if (token) {
|
| 1139 |
+
btn.textContent = '注销';
|
| 1140 |
+
btn.disabled = false;
|
| 1141 |
+
btn.classList.remove('btn-disabled');
|
| 1142 |
+
btn.title = '注销登录';
|
| 1143 |
+
btn.onclick = logoutAdmin;
|
| 1144 |
+
} else {
|
| 1145 |
+
btn.textContent = '登录';
|
| 1146 |
+
btn.disabled = false;
|
| 1147 |
+
btn.classList.remove('btn-disabled');
|
| 1148 |
+
btn.title = '管理员登录';
|
| 1149 |
+
btn.onclick = showLoginModal;
|
| 1150 |
+
}
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
async function apiFetch(url, options = {}) {
|
| 1154 |
+
const headers = Object.assign({}, options.headers || {}, getAuthHeaders());
|
| 1155 |
+
const res = await fetch(url, { ...options, headers });
|
| 1156 |
+
if (res.status === 401 || res.status === 403) {
|
| 1157 |
+
showLoginModal();
|
| 1158 |
+
updateLoginButton();
|
| 1159 |
+
throw new Error('需要登录');
|
| 1160 |
+
}
|
| 1161 |
+
return res;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
// --- 主题控制 ---
|
| 1165 |
+
function initTheme() {
|
| 1166 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 1167 |
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
| 1168 |
+
updateThemeIcon(savedTheme);
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
function toggleTheme() {
|
| 1172 |
+
const current = document.documentElement.getAttribute('data-theme');
|
| 1173 |
+
const newTheme = current === 'dark' ? 'light' : 'dark';
|
| 1174 |
+
document.documentElement.setAttribute('data-theme', newTheme);
|
| 1175 |
+
localStorage.setItem('theme', newTheme);
|
| 1176 |
+
updateThemeIcon(newTheme);
|
| 1177 |
+
}
|
| 1178 |
+
|
| 1179 |
+
// --- 标签页控制 ---
|
| 1180 |
+
function switchTab(tabName) {
|
| 1181 |
+
document.querySelectorAll('.tab').forEach(btn => btn.classList.remove('active'));
|
| 1182 |
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
| 1183 |
+
|
| 1184 |
+
const tabBtn = document.querySelector(`[onclick="switchTab('${tabName}')"]`);
|
| 1185 |
+
const tabContent = document.getElementById(tabName);
|
| 1186 |
+
|
| 1187 |
+
if (tabBtn) tabBtn.classList.add('active');
|
| 1188 |
+
if (tabContent) tabContent.classList.add('active');
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
// --- 状态检查 ---
|
| 1192 |
+
async function checkServerStatus() {
|
| 1193 |
+
const indicator = document.getElementById('serviceStatus');
|
| 1194 |
+
if (!indicator) return;
|
| 1195 |
+
try {
|
| 1196 |
+
const res = await apiFetch(`${API_BASE}/api/status`);
|
| 1197 |
+
console.log('Server Status Response:', res);
|
| 1198 |
+
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
| 1199 |
+
const data = await res.json();
|
| 1200 |
+
indicator.textContent = '服务运行中';
|
| 1201 |
+
indicator.classList.remove('offline');
|
| 1202 |
+
indicator.title = '服务连接正常 - ' + new Date().toLocaleString();
|
| 1203 |
+
} catch (e) {
|
| 1204 |
+
indicator.textContent = '服务离线';
|
| 1205 |
+
indicator.classList.add('offline');
|
| 1206 |
+
indicator.title = '无法连接到后端服务';
|
| 1207 |
+
}
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
// --- 账号管理 (Accounts) ---
|
| 1211 |
+
async function loadAccounts() {
|
| 1212 |
+
try {
|
| 1213 |
+
const res = await apiFetch(`${API_BASE}/api/accounts`);
|
| 1214 |
+
const data = await res.json();
|
| 1215 |
+
accountsData = data.accounts || [];
|
| 1216 |
+
document.getElementById('currentIndex').textContent = data.current_index || 0;
|
| 1217 |
+
renderAccounts();
|
| 1218 |
+
updateAccountStats();
|
| 1219 |
+
} catch (e) {
|
| 1220 |
+
showToast('加载账号列表失败: ' + e.message, 'error');
|
| 1221 |
+
}
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
function renderAccounts() {
|
| 1225 |
+
const tbody = document.getElementById('accountsTableBody');
|
| 1226 |
+
if (!tbody) return;
|
| 1227 |
+
|
| 1228 |
+
if (accountsData.length === 0) {
|
| 1229 |
+
tbody.innerHTML = `<tr><td colspan="6" class="empty-state">
|
| 1230 |
+
<div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div>
|
| 1231 |
+
<h3>暂无账号</h3><p>点击 "添加账号" 按钮来创建一个。</p>
|
| 1232 |
+
</td></tr>`;
|
| 1233 |
+
return;
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
tbody.innerHTML = accountsData.map((acc, index) => `
|
| 1237 |
+
<tr>
|
| 1238 |
+
<td>${index + 1}</td>
|
| 1239 |
+
<td><code>${acc.team_id || '-'}</code></td>
|
| 1240 |
+
<td><code>${acc.csesidx || '-'}</code></td>
|
| 1241 |
+
<td title="${acc.user_agent}">${acc.user_agent ? acc.user_agent.substring(0, 30) + '...' : '-'}</td>
|
| 1242 |
+
<td>
|
| 1243 |
+
<span class="badge ${acc.available ? 'badge-success' : 'badge-danger'}">${acc.available ? '可用' : '不可用'}</span>
|
| 1244 |
+
${renderNextRefresh(acc)}
|
| 1245 |
+
</td>
|
| 1246 |
+
<td style="white-space: nowrap;">
|
| 1247 |
+
<button class="btn btn-sm ${acc.enabled !== false ? 'btn-warning' : 'btn-success'} btn-icon" onclick="toggleAccount(${acc.id})" title="${acc.enabled !== false ? '停用' : '启用'}"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-${acc.enabled !== false ? 'pause' : 'play'}"></use></svg></button>
|
| 1248 |
+
<button class="btn btn-sm btn-outline btn-icon" onclick="testAccount(${acc.id})" title="测试连接"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-zap"></use></svg></button>
|
| 1249 |
+
<button class="btn btn-sm btn-outline btn-icon" onclick="showRefreshCookieModal(${acc.id})" title="刷新Cookie"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-refresh"></use></svg></button>
|
| 1250 |
+
<button class="btn btn-sm btn-outline btn-icon" onclick="showEditAccountModal(${acc.id})" title="编辑"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-settings"></use></svg></button>
|
| 1251 |
+
<button class="btn btn-sm btn-danger btn-icon" onclick="deleteAccount(${acc.id})" title="删除"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-x"></use></svg></button>
|
| 1252 |
+
</td>
|
| 1253 |
+
</tr>
|
| 1254 |
+
`).join('');
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
function updateAccountStats() {
|
| 1258 |
+
document.getElementById('totalAccounts').textContent = accountsData.length;
|
| 1259 |
+
document.getElementById('availableAccounts').textContent = accountsData.filter(a => a.available).length;
|
| 1260 |
+
document.getElementById('unavailableAccounts').textContent = accountsData.length - accountsData.filter(a => a.available).length;
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
function renderNextRefresh(acc) {
|
| 1264 |
+
if (!acc || !acc.cooldown_until) return '';
|
| 1265 |
+
const now = Date.now();
|
| 1266 |
+
const ts = acc.cooldown_until * 1000;
|
| 1267 |
+
if (ts <= now) return '';
|
| 1268 |
+
const next = new Date(ts);
|
| 1269 |
+
const remaining = Math.max(0, ts - now);
|
| 1270 |
+
const minutes = Math.floor(remaining / 60000);
|
| 1271 |
+
const label = minutes >= 60
|
| 1272 |
+
? `${Math.floor(minutes / 60)}小时${minutes % 60}分`
|
| 1273 |
+
: `${minutes}分`;
|
| 1274 |
+
return `<span class="cooldown-hint">下次恢复: ${next.toLocaleString()}(约${label})</span>`;
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
function showAddAccountModal() {
|
| 1278 |
+
// 清空表单字段
|
| 1279 |
+
document.getElementById('newAccountJson').value = '';
|
| 1280 |
+
document.getElementById('newTeamId').value = '';
|
| 1281 |
+
document.getElementById('newSecureCses').value = '';
|
| 1282 |
+
document.getElementById('newHostCoses').value = '';
|
| 1283 |
+
document.getElementById('newCsesidx').value = '';
|
| 1284 |
+
document.getElementById('newUserAgent').value = '';
|
| 1285 |
+
openModal('addAccountModal');
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
function showEditAccountModal(id) {
|
| 1289 |
+
const acc = accountsData.find(a => a.id === id);
|
| 1290 |
+
if (!acc) return;
|
| 1291 |
+
|
| 1292 |
+
document.getElementById('editAccountId').value = id;
|
| 1293 |
+
document.getElementById('editTeamId').value = acc.team_id || '';
|
| 1294 |
+
document.getElementById('editSecureCses').value = acc.secure_c_ses || '';
|
| 1295 |
+
document.getElementById('editHostCoses').value = acc.host_c_oses || '';
|
| 1296 |
+
document.getElementById('editCsesidx').value = acc.csesidx || '';
|
| 1297 |
+
document.getElementById('editUserAgent').value = acc.user_agent ? acc.user_agent.replace('...', '') : '';
|
| 1298 |
+
|
| 1299 |
+
openModal('editAccountModal');
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
async function updateAccount() {
|
| 1303 |
+
const id = document.getElementById('editAccountId').value;
|
| 1304 |
+
const account = {};
|
| 1305 |
+
|
| 1306 |
+
const teamId = document.getElementById('editTeamId').value;
|
| 1307 |
+
const secureCses = document.getElementById('editSecureCses').value;
|
| 1308 |
+
const hostCoses = document.getElementById('editHostCoses').value;
|
| 1309 |
+
const csesidx = document.getElementById('editCsesidx').value;
|
| 1310 |
+
const userAgent = document.getElementById('editUserAgent').value;
|
| 1311 |
+
|
| 1312 |
+
if (teamId) account.team_id = teamId;
|
| 1313 |
+
if (secureCses) account.secure_c_ses = secureCses;
|
| 1314 |
+
if (hostCoses) account.host_c_oses = hostCoses;
|
| 1315 |
+
if (csesidx) account.csesidx = csesidx;
|
| 1316 |
+
if (userAgent) account.user_agent = userAgent;
|
| 1317 |
+
|
| 1318 |
+
try {
|
| 1319 |
+
const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, {
|
| 1320 |
+
method: 'PUT',
|
| 1321 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1322 |
+
body: JSON.stringify(account)
|
| 1323 |
+
});
|
| 1324 |
+
const data = await res.json();
|
| 1325 |
+
|
| 1326 |
+
if (data.success) {
|
| 1327 |
+
showToast('账号更新成功', 'success');
|
| 1328 |
+
closeModal('editAccountModal');
|
| 1329 |
+
loadAccounts();
|
| 1330 |
+
} else {
|
| 1331 |
+
showToast('更新失败: ' + (data.error || '未知错误'), 'error');
|
| 1332 |
+
}
|
| 1333 |
+
} catch (e) {
|
| 1334 |
+
showToast('更新失败: ' + e.message, 'error');
|
| 1335 |
+
}
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
async function saveNewAccount() {
|
| 1339 |
+
const teamId = document.getElementById('newTeamId').value;
|
| 1340 |
+
const secureCses = document.getElementById('newSecureCses').value;
|
| 1341 |
+
const hostCoses = document.getElementById('newHostCoses').value;
|
| 1342 |
+
const csesidx = document.getElementById('newCsesidx').value;
|
| 1343 |
+
const userAgent = document.getElementById('newUserAgent').value;
|
| 1344 |
+
|
| 1345 |
+
try {
|
| 1346 |
+
const res = await apiFetch(`${API_BASE}/api/accounts`, {
|
| 1347 |
+
method: 'POST',
|
| 1348 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1349 |
+
body: JSON.stringify({
|
| 1350 |
+
team_id: teamId,
|
| 1351 |
+
"secure_c_ses": secureCses,
|
| 1352 |
+
"host_c_oses": hostCoses,
|
| 1353 |
+
"csesidx": csesidx,
|
| 1354 |
+
"user_agent": userAgent })
|
| 1355 |
+
});
|
| 1356 |
+
const data = await res.json();
|
| 1357 |
+
if (!res.ok || data.error) throw new Error(data.error || data.detail || '添加失败');
|
| 1358 |
+
showToast('账号添加成功!', 'success');
|
| 1359 |
+
closeModal('addAccountModal');
|
| 1360 |
+
loadAccounts();
|
| 1361 |
+
} catch (e) {
|
| 1362 |
+
showToast('添加失败: ' + e.message, 'error');
|
| 1363 |
+
}
|
| 1364 |
+
}
|
| 1365 |
+
|
| 1366 |
+
function parseAccountJson(text) {
|
| 1367 |
+
const textarea = document.getElementById('newAccountJson');
|
| 1368 |
+
const raw = (typeof text === 'string' ? text : textarea.value || '').trim();
|
| 1369 |
+
if (!raw) {
|
| 1370 |
+
showToast('请先粘贴账号JSON', 'warning');
|
| 1371 |
+
return;
|
| 1372 |
+
}
|
| 1373 |
+
let acc;
|
| 1374 |
+
try {
|
| 1375 |
+
const parsed = JSON.parse(raw);
|
| 1376 |
+
acc = Array.isArray(parsed) ? parsed[0] : parsed;
|
| 1377 |
+
if (!acc || typeof acc !== 'object') throw new Error('格式不正确');
|
| 1378 |
+
} catch (err) {
|
| 1379 |
+
showToast('解析失败: ' + err.message, 'error');
|
| 1380 |
+
return;
|
| 1381 |
+
}
|
| 1382 |
+
|
| 1383 |
+
document.getElementById('newTeamId').value = acc.team_id || '';
|
| 1384 |
+
document.getElementById('newSecureCses').value = acc.secure_c_ses || '';
|
| 1385 |
+
document.getElementById('newHostCoses').value = acc.host_c_oses || '';
|
| 1386 |
+
document.getElementById('newCsesidx').value = acc.csesidx || '';
|
| 1387 |
+
document.getElementById('newUserAgent').value = acc.user_agent || '';
|
| 1388 |
+
showToast('已填充账号信息', 'success');
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
async function pasteAccountJson() {
|
| 1392 |
+
try {
|
| 1393 |
+
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
| 1394 |
+
showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning');
|
| 1395 |
+
return;
|
| 1396 |
+
}
|
| 1397 |
+
const text = await navigator.clipboard.readText();
|
| 1398 |
+
document.getElementById('newAccountJson').value = text;
|
| 1399 |
+
parseAccountJson(text);
|
| 1400 |
+
} catch (e) {
|
| 1401 |
+
showToast('无法读取剪贴板: ' + e.message, 'error');
|
| 1402 |
+
}
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
async function deleteAccount(id) {
|
| 1406 |
+
if (!confirm('确定要删除这个账号吗?')) return;
|
| 1407 |
+
try {
|
| 1408 |
+
const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, { method: 'DELETE' });
|
| 1409 |
+
if (!res.ok) throw new Error((await res.json()).detail);
|
| 1410 |
+
showToast('账号删除成功!', 'success');
|
| 1411 |
+
loadAccounts();
|
| 1412 |
+
} catch (e) {
|
| 1413 |
+
showToast('删除失败: ' + e.message, 'error');
|
| 1414 |
+
}
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
async function testAccount(id) {
|
| 1418 |
+
showToast(`正在测试账号ID: ${id}...`, 'info');
|
| 1419 |
+
try {
|
| 1420 |
+
const res = await apiFetch(`${API_BASE}/api/accounts/${id}/test`);
|
| 1421 |
+
const data = await res.json();
|
| 1422 |
+
if (res.ok && data.success) {
|
| 1423 |
+
showToast(`账号 ${id} 测试成功!`, 'success');
|
| 1424 |
+
} else {
|
| 1425 |
+
throw new Error(data.detail || '未知错误');
|
| 1426 |
+
}
|
| 1427 |
+
loadAccounts();
|
| 1428 |
+
} catch (e) {
|
| 1429 |
+
showToast(`账号 ${id} 测试失败: ${e.message}`, 'error');
|
| 1430 |
+
}
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
async function toggleAccount(id) {
|
| 1434 |
+
const acc = accountsData.find(a => a.id === id);
|
| 1435 |
+
const action = acc && acc.enabled !== false ? '停用' : '启用';
|
| 1436 |
+
try {
|
| 1437 |
+
const res = await apiFetch(`${API_BASE}/api/accounts/${id}/toggle`, {
|
| 1438 |
+
method: 'POST',
|
| 1439 |
+
headers: { 'Content-Type': 'application/json' }
|
| 1440 |
+
});
|
| 1441 |
+
const data = await res.json();
|
| 1442 |
+
if (res.ok && data.success) {
|
| 1443 |
+
showToast(`账号 ${id} ${action}成功!`, 'success');
|
| 1444 |
+
loadAccounts();
|
| 1445 |
+
} else {
|
| 1446 |
+
throw new Error(data.error || data.detail || '未知错误');
|
| 1447 |
+
}
|
| 1448 |
+
} catch (e) {
|
| 1449 |
+
showToast(`账号 ${id} ${action}失败: ${e.message}`, 'error');
|
| 1450 |
+
}
|
| 1451 |
+
}
|
| 1452 |
+
|
| 1453 |
+
/**
|
| 1454 |
+
* 显示刷新Cookie的模态框
|
| 1455 |
+
* @param {number} id - 账号ID
|
| 1456 |
+
*/
|
| 1457 |
+
function showRefreshCookieModal(id) {
|
| 1458 |
+
const acc = accountsData.find(a => a.id === id);
|
| 1459 |
+
if (!acc) {
|
| 1460 |
+
showToast('账号不存在', 'error');
|
| 1461 |
+
return;
|
| 1462 |
+
}
|
| 1463 |
+
|
| 1464 |
+
document.getElementById('refreshAccountId').value = id;
|
| 1465 |
+
document.getElementById('refreshSecureCses').value = '';
|
| 1466 |
+
document.getElementById('refreshHostCoses').value = '';
|
| 1467 |
+
document.getElementById('refreshCsesidx').value = '';
|
| 1468 |
+
document.getElementById('refreshCookieJson').value = '';
|
| 1469 |
+
|
| 1470 |
+
openModal('refreshCookieModal');
|
| 1471 |
+
}
|
| 1472 |
+
|
| 1473 |
+
/**
|
| 1474 |
+
* 从JSON解析并填充刷新Cookie表单
|
| 1475 |
+
* @param {string} text - JSON字符串
|
| 1476 |
+
*/
|
| 1477 |
+
function parseRefreshCookieJson(text) {
|
| 1478 |
+
const textarea = document.getElementById('refreshCookieJson');
|
| 1479 |
+
const raw = (typeof text === 'string' ? text : textarea.value || '').trim();
|
| 1480 |
+
if (!raw) {
|
| 1481 |
+
showToast('请先粘贴Cookie JSON', 'warning');
|
| 1482 |
+
return;
|
| 1483 |
+
}
|
| 1484 |
+
let acc;
|
| 1485 |
+
try {
|
| 1486 |
+
const parsed = JSON.parse(raw);
|
| 1487 |
+
acc = Array.isArray(parsed) ? parsed[0] : parsed;
|
| 1488 |
+
if (!acc || typeof acc !== 'object') throw new Error('格式不正确');
|
| 1489 |
+
} catch (err) {
|
| 1490 |
+
showToast('解析失败: ' + err.message, 'error');
|
| 1491 |
+
return;
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
document.getElementById('refreshSecureCses').value = acc.secure_c_ses || '';
|
| 1495 |
+
document.getElementById('refreshHostCoses').value = acc.host_c_oses || '';
|
| 1496 |
+
document.getElementById('refreshCsesidx').value = acc.csesidx || '';
|
| 1497 |
+
showToast('已填充Cookie信息', 'success');
|
| 1498 |
+
}
|
| 1499 |
+
|
| 1500 |
+
/**
|
| 1501 |
+
* 从剪贴板粘贴并解析刷新Cookie JSON
|
| 1502 |
+
*/
|
| 1503 |
+
async function pasteRefreshCookieJson() {
|
| 1504 |
+
try {
|
| 1505 |
+
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
| 1506 |
+
showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning');
|
| 1507 |
+
return;
|
| 1508 |
+
}
|
| 1509 |
+
const text = await navigator.clipboard.readText();
|
| 1510 |
+
document.getElementById('refreshCookieJson').value = text;
|
| 1511 |
+
parseRefreshCookieJson(text);
|
| 1512 |
+
} catch (e) {
|
| 1513 |
+
showToast('无法读取剪贴板: ' + e.message, 'error');
|
| 1514 |
+
}
|
| 1515 |
+
}
|
| 1516 |
+
|
| 1517 |
+
/**
|
| 1518 |
+
* 刷新账号Cookie
|
| 1519 |
+
* 调用后端API更新账号的Cookie信息
|
| 1520 |
+
*/
|
| 1521 |
+
async function refreshAccountCookie() {
|
| 1522 |
+
const id = document.getElementById('refreshAccountId').value;
|
| 1523 |
+
const secureCses = document.getElementById('refreshSecureCses').value.trim();
|
| 1524 |
+
const hostCoses = document.getElementById('refreshHostCoses').value.trim();
|
| 1525 |
+
const csesidx = document.getElementById('refreshCsesidx').value.trim();
|
| 1526 |
+
|
| 1527 |
+
// 验证必填字段
|
| 1528 |
+
if (!secureCses || !hostCoses) {
|
| 1529 |
+
showToast('secure_c_ses 和 host_c_oses 为必填项', 'warning');
|
| 1530 |
+
return;
|
| 1531 |
+
}
|
| 1532 |
+
|
| 1533 |
+
try {
|
| 1534 |
+
const res = await apiFetch(`${API_BASE}/api/accounts/${id}/refresh-cookie`, {
|
| 1535 |
+
method: 'POST',
|
| 1536 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1537 |
+
body: JSON.stringify({
|
| 1538 |
+
secure_c_ses: secureCses,
|
| 1539 |
+
host_c_oses: hostCoses,
|
| 1540 |
+
csesidx: csesidx || undefined
|
| 1541 |
+
})
|
| 1542 |
+
});
|
| 1543 |
+
const data = await res.json();
|
| 1544 |
+
|
| 1545 |
+
if (res.ok && data.success) {
|
| 1546 |
+
showToast('Cookie刷新成功!', 'success');
|
| 1547 |
+
closeModal('refreshCookieModal');
|
| 1548 |
+
loadAccounts();
|
| 1549 |
+
} else {
|
| 1550 |
+
throw new Error(data.error || data.detail || '未知错误');
|
| 1551 |
+
}
|
| 1552 |
+
} catch (e) {
|
| 1553 |
+
showToast('Cookie刷新失败: ' + e.message, 'error');
|
| 1554 |
+
}
|
| 1555 |
+
}
|
| 1556 |
+
|
| 1557 |
+
// --- 模型管理 (Models) ---
|
| 1558 |
+
async function loadModels() {
|
| 1559 |
+
try {
|
| 1560 |
+
const res = await apiFetch(`${API_BASE}/api/models`);
|
| 1561 |
+
const data = await res.json();
|
| 1562 |
+
modelsData = data.models || [];
|
| 1563 |
+
renderModels();
|
| 1564 |
+
} catch (e) {
|
| 1565 |
+
showToast('加载模型列表失败: ' + e.message, 'error');
|
| 1566 |
+
}
|
| 1567 |
+
}
|
| 1568 |
+
|
| 1569 |
+
function escapeHtml(str) {
|
| 1570 |
+
if (!str) return '';
|
| 1571 |
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
| 1572 |
+
}
|
| 1573 |
+
|
| 1574 |
+
function renderModels() {
|
| 1575 |
+
const tbody = document.getElementById('modelsTableBody');
|
| 1576 |
+
if (!tbody) return;
|
| 1577 |
+
if (modelsData.length === 0) {
|
| 1578 |
+
tbody.innerHTML = `<tr><td colspan="7" class="empty-state">
|
| 1579 |
+
<div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-robot"></use></svg></div>
|
| 1580 |
+
<h3>暂无模型</h3><p>点击 "添加模型" 按钮来创建一个。</p>
|
| 1581 |
+
</td></tr>`;
|
| 1582 |
+
return;
|
| 1583 |
+
}
|
| 1584 |
+
tbody.innerHTML = modelsData.map((model, index) => {
|
| 1585 |
+
const safeId = escapeHtml(model.id);
|
| 1586 |
+
const safeName = escapeHtml(model.name);
|
| 1587 |
+
const safeDesc = escapeHtml(model.description);
|
| 1588 |
+
return `
|
| 1589 |
+
<tr>
|
| 1590 |
+
<td><code>${safeId}</code></td>
|
| 1591 |
+
<td>${safeName}</td>
|
| 1592 |
+
<td title="${safeDesc}">${model.description ? safeDesc.substring(0, 40) + '...' : ''}</td>
|
| 1593 |
+
<td>${model.context_length}</td>
|
| 1594 |
+
<td>${model.max_tokens}</td>
|
| 1595 |
+
<td><span class="badge ${model.is_public ? 'badge-success' : 'badge-warning'}">${model.is_public ? '公共' : '私有'}</span></td>
|
| 1596 |
+
<td>
|
| 1597 |
+
<button class="btn btn-sm btn-outline btn-icon" onclick="showEditModelModalByIndex(${index})" title="编辑">✏️</button>
|
| 1598 |
+
<button class="btn btn-sm btn-danger btn-icon" onclick="deleteModelByIndex(${index})" title="删除">🗑️</button>
|
| 1599 |
+
</td>
|
| 1600 |
+
</tr>
|
| 1601 |
+
`;
|
| 1602 |
+
}).join('');
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
function showAddModelModal() {
|
| 1606 |
+
openModal('addModelModal');
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
function showEditModelModalByIndex(index) {
|
| 1610 |
+
const model = modelsData[index];
|
| 1611 |
+
if (!model) return;
|
| 1612 |
+
|
| 1613 |
+
document.getElementById('editModelOriginalId').value = model.id;
|
| 1614 |
+
document.getElementById('editModelId').value = model.id;
|
| 1615 |
+
document.getElementById('editModelName').value = model.name || '';
|
| 1616 |
+
document.getElementById('editModelDesc').value = model.description || '';
|
| 1617 |
+
document.getElementById('editContextLength').value = model.context_length || '';
|
| 1618 |
+
document.getElementById('editMaxTokens').value = model.max_tokens || '';
|
| 1619 |
+
|
| 1620 |
+
openModal('editModelModal');
|
| 1621 |
+
}
|
| 1622 |
+
|
| 1623 |
+
async function updateModel() {
|
| 1624 |
+
const originalId = document.getElementById('editModelOriginalId').value;
|
| 1625 |
+
const model = {
|
| 1626 |
+
name: document.getElementById('editModelName').value,
|
| 1627 |
+
description: document.getElementById('editModelDesc').value,
|
| 1628 |
+
context_length: parseInt(document.getElementById('editContextLength').value) || 32000,
|
| 1629 |
+
max_tokens: parseInt(document.getElementById('editMaxTokens').value) || 8096
|
| 1630 |
+
};
|
| 1631 |
+
|
| 1632 |
+
try {
|
| 1633 |
+
const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(originalId)}`, {
|
| 1634 |
+
method: 'PUT',
|
| 1635 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1636 |
+
body: JSON.stringify(model)
|
| 1637 |
+
});
|
| 1638 |
+
const data = await res.json();
|
| 1639 |
+
|
| 1640 |
+
if (data.success) {
|
| 1641 |
+
showToast('模型更新成功', 'success');
|
| 1642 |
+
closeModal('editModelModal');
|
| 1643 |
+
loadModels();
|
| 1644 |
+
} else {
|
| 1645 |
+
showToast('更新失败: ' + (data.error || '未知错误'), 'error');
|
| 1646 |
+
}
|
| 1647 |
+
} catch (e) {
|
| 1648 |
+
showToast('更新失败: ' + e.message, 'error');
|
| 1649 |
+
}
|
| 1650 |
+
}
|
| 1651 |
+
|
| 1652 |
+
/**
|
| 1653 |
+
* 保存新模型
|
| 1654 |
+
* 从添加模型模态框获取数据并调用API创建新模型
|
| 1655 |
+
*/
|
| 1656 |
+
async function saveNewModel() {
|
| 1657 |
+
const modelId = document.getElementById('newModelId').value.trim();
|
| 1658 |
+
const modelName = document.getElementById('newModelName').value.trim();
|
| 1659 |
+
const modelDesc = document.getElementById('newModelDesc').value.trim();
|
| 1660 |
+
const contextLength = parseInt(document.getElementById('newContextLength').value) || 32000;
|
| 1661 |
+
const maxTokens = parseInt(document.getElementById('newMaxTokens').value) || 8096;
|
| 1662 |
+
|
| 1663 |
+
// 验证必填字段
|
| 1664 |
+
if (!modelId) {
|
| 1665 |
+
showToast('请输入模型ID', 'warning');
|
| 1666 |
+
return;
|
| 1667 |
+
}
|
| 1668 |
+
if (!modelName) {
|
| 1669 |
+
showToast('请输入模型名称', 'warning');
|
| 1670 |
+
return;
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
const model = {
|
| 1674 |
+
id: modelId,
|
| 1675 |
+
name: modelName,
|
| 1676 |
+
description: modelDesc,
|
| 1677 |
+
context_length: contextLength,
|
| 1678 |
+
max_tokens: maxTokens
|
| 1679 |
+
};
|
| 1680 |
+
|
| 1681 |
+
try {
|
| 1682 |
+
const res = await apiFetch(`${API_BASE}/api/models`, {
|
| 1683 |
+
method: 'POST',
|
| 1684 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1685 |
+
body: JSON.stringify(model)
|
| 1686 |
+
});
|
| 1687 |
+
const data = await res.json();
|
| 1688 |
+
|
| 1689 |
+
if (res.ok && (data.success || !data.error)) {
|
| 1690 |
+
showToast('模型添加成功', 'success');
|
| 1691 |
+
closeModal('addModelModal');
|
| 1692 |
+
// 清空表单
|
| 1693 |
+
document.getElementById('newModelId').value = '';
|
| 1694 |
+
document.getElementById('newModelName').value = '';
|
| 1695 |
+
document.getElementById('newModelDesc').value = '';
|
| 1696 |
+
document.getElementById('newContextLength').value = '';
|
| 1697 |
+
document.getElementById('newMaxTokens').value = '';
|
| 1698 |
+
loadModels();
|
| 1699 |
+
} else {
|
| 1700 |
+
throw new Error(data.error || '添加失败');
|
| 1701 |
+
}
|
| 1702 |
+
} catch (e) {
|
| 1703 |
+
showToast('添加模型失败: ' + e.message, 'error');
|
| 1704 |
+
}
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
/**
|
| 1708 |
+
* 删除模型
|
| 1709 |
+
* @param {string} id - 模型ID
|
| 1710 |
+
*/
|
| 1711 |
+
async function deleteModelByIndex(index) {
|
| 1712 |
+
const model = modelsData[index];
|
| 1713 |
+
if (!model) return;
|
| 1714 |
+
const id = model.id;
|
| 1715 |
+
if (!confirm(`确定要删除模型 "${id}" 吗?此操作不可恢复。`)) {
|
| 1716 |
+
return;
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
try {
|
| 1720 |
+
const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(id)}`, {
|
| 1721 |
+
method: 'DELETE'
|
| 1722 |
+
});
|
| 1723 |
+
const data = await res.json();
|
| 1724 |
+
|
| 1725 |
+
if (res.ok && (data.success || !data.error)) {
|
| 1726 |
+
showToast('模型删除成功', 'success');
|
| 1727 |
+
loadModels();
|
| 1728 |
+
} else {
|
| 1729 |
+
throw new Error(data.error || '删除失败');
|
| 1730 |
+
}
|
| 1731 |
+
} catch (e) {
|
| 1732 |
+
showToast('删除模型失败: ' + e.message, 'error');
|
| 1733 |
+
}
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
+
// --- 系统设置 (Settings) ---
|
| 1737 |
+
async function loadConfig() {
|
| 1738 |
+
try {
|
| 1739 |
+
const res = await apiFetch(`${API_BASE}/api/config`);
|
| 1740 |
+
configData = await res.json();
|
| 1741 |
+
document.getElementById('proxyUrl').value = configData.proxy || '';
|
| 1742 |
+
const imageModeSelect = document.getElementById('imageOutputMode');
|
| 1743 |
+
if (imageModeSelect) {
|
| 1744 |
+
const mode = (configData.image_output_mode || 'url');
|
| 1745 |
+
imageModeSelect.value = mode === 'base64' ? 'base64' : 'url';
|
| 1746 |
+
}
|
| 1747 |
+
document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
|
| 1748 |
+
} catch (e) {
|
| 1749 |
+
showToast('加载配置失败: ' + e.message, 'error');
|
| 1750 |
+
}
|
| 1751 |
+
}
|
| 1752 |
+
|
| 1753 |
+
async function loadLogLevel() {
|
| 1754 |
+
try {
|
| 1755 |
+
const res = await apiFetch(`${API_BASE}/api/logging`);
|
| 1756 |
+
const data = await res.json();
|
| 1757 |
+
const select = document.getElementById('logLevelSelect');
|
| 1758 |
+
if (select && data.level) {
|
| 1759 |
+
select.value = data.level;
|
| 1760 |
+
}
|
| 1761 |
+
} catch (e) {
|
| 1762 |
+
console.warn('日志级别加载失败', e);
|
| 1763 |
+
}
|
| 1764 |
+
}
|
| 1765 |
+
|
| 1766 |
+
async function updateLogLevel(level) {
|
| 1767 |
+
try {
|
| 1768 |
+
const res = await apiFetch(`${API_BASE}/api/logging`, {
|
| 1769 |
+
method: 'POST',
|
| 1770 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1771 |
+
body: JSON.stringify({ level })
|
| 1772 |
+
});
|
| 1773 |
+
const data = await res.json();
|
| 1774 |
+
if (!res.ok || data.error) {
|
| 1775 |
+
throw new Error(data.error || '设置失败');
|
| 1776 |
+
}
|
| 1777 |
+
showToast(`日志级别已切换为 ${data.level}`, 'success');
|
| 1778 |
+
} catch (e) {
|
| 1779 |
+
showToast('日志级别设置失败: ' + e.message, 'error');
|
| 1780 |
+
}
|
| 1781 |
+
}
|
| 1782 |
+
|
| 1783 |
+
// --- Token 管理 ---
|
| 1784 |
+
async function loadTokens() {
|
| 1785 |
+
try {
|
| 1786 |
+
const res = await apiFetch(`${API_BASE}/api/tokens`);
|
| 1787 |
+
const data = await res.json();
|
| 1788 |
+
tokensData = data.tokens || [];
|
| 1789 |
+
renderTokens();
|
| 1790 |
+
} catch (e) {
|
| 1791 |
+
showToast('加载 Token 失败: ' + e.message, 'error');
|
| 1792 |
+
}
|
| 1793 |
+
}
|
| 1794 |
+
|
| 1795 |
+
function renderTokens() {
|
| 1796 |
+
const tbody = document.getElementById('tokensTableBody');
|
| 1797 |
+
if (!tbody) return;
|
| 1798 |
+
if (!tokensData.length) {
|
| 1799 |
+
tbody.innerHTML = `<tr><td colspan="2" class="empty-state">暂无 Token</td></tr>`;
|
| 1800 |
+
return;
|
| 1801 |
+
}
|
| 1802 |
+
tbody.innerHTML = tokensData.map(token => `
|
| 1803 |
+
<tr>
|
| 1804 |
+
<td><code>${token}</code></td>
|
| 1805 |
+
<td style="white-space: nowrap;">
|
| 1806 |
+
<button class="btn btn-outline btn-sm" data-token="${token}" onclick="copyToken(this.dataset.token)" title="复制Token">复制</button>
|
| 1807 |
+
<button class="btn btn-danger btn-sm" data-token="${token}" onclick="deleteToken(this.dataset.token)" title="删除Token">删除</button>
|
| 1808 |
+
</td>
|
| 1809 |
+
</tr>
|
| 1810 |
+
`).join('');
|
| 1811 |
+
}
|
| 1812 |
+
|
| 1813 |
+
async function addToken() {
|
| 1814 |
+
const manual = document.getElementById('manualToken').value.trim();
|
| 1815 |
+
try {
|
| 1816 |
+
const res = await apiFetch(`${API_BASE}/api/tokens`, {
|
| 1817 |
+
method: 'POST',
|
| 1818 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1819 |
+
body: JSON.stringify(manual ? { token: manual } : {})
|
| 1820 |
+
});
|
| 1821 |
+
const data = await res.json();
|
| 1822 |
+
if (!res.ok || data.error) throw new Error(data.error || '创建失败');
|
| 1823 |
+
document.getElementById('manualToken').value = data.token;
|
| 1824 |
+
showToast('Token 创建成功', 'success');
|
| 1825 |
+
loadTokens();
|
| 1826 |
+
} catch (e) {
|
| 1827 |
+
showToast('创建 Token 失败: ' + e.message, 'error');
|
| 1828 |
+
}
|
| 1829 |
+
}
|
| 1830 |
+
|
| 1831 |
+
function generateToken() {
|
| 1832 |
+
if (window.crypto && crypto.randomUUID) {
|
| 1833 |
+
document.getElementById('manualToken').value = crypto.randomUUID().replace(/-/g, '');
|
| 1834 |
+
} else {
|
| 1835 |
+
document.getElementById('manualToken').value = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
| 1836 |
+
}
|
| 1837 |
+
}
|
| 1838 |
+
|
| 1839 |
+
async function deleteToken(token) {
|
| 1840 |
+
if (!confirm('确定删除该 Token 吗?')) return;
|
| 1841 |
+
try {
|
| 1842 |
+
const res = await apiFetch(`${API_BASE}/api/tokens/${token}`, { method: 'DELETE' });
|
| 1843 |
+
const data = await res.json();
|
| 1844 |
+
if (!res.ok || data.error) throw new Error(data.error || '删除失败');
|
| 1845 |
+
showToast('Token 删除成功', 'success');
|
| 1846 |
+
loadTokens();
|
| 1847 |
+
} catch (e) {
|
| 1848 |
+
showToast('删除 Token 失败: ' + e.message, 'error');
|
| 1849 |
+
}
|
| 1850 |
+
}
|
| 1851 |
+
|
| 1852 |
+
function copyToken(token) {
|
| 1853 |
+
if (!token) {
|
| 1854 |
+
showToast('无效的 Token', 'warning');
|
| 1855 |
+
return;
|
| 1856 |
+
}
|
| 1857 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 1858 |
+
navigator.clipboard.writeText(token).then(() => {
|
| 1859 |
+
showToast('已复制', 'success');
|
| 1860 |
+
}).catch(() => {
|
| 1861 |
+
fallbackCopy(token);
|
| 1862 |
+
});
|
| 1863 |
+
} else {
|
| 1864 |
+
fallbackCopy(token);
|
| 1865 |
+
}
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
function fallbackCopy(text) {
|
| 1869 |
+
try {
|
| 1870 |
+
const textarea = document.createElement('textarea');
|
| 1871 |
+
textarea.value = text;
|
| 1872 |
+
document.body.appendChild(textarea);
|
| 1873 |
+
textarea.select();
|
| 1874 |
+
document.execCommand('copy');
|
| 1875 |
+
document.body.removeChild(textarea);
|
| 1876 |
+
showToast('已复制', 'success');
|
| 1877 |
+
} catch (err) {
|
| 1878 |
+
showToast('复制失败', 'error');
|
| 1879 |
+
}
|
| 1880 |
+
}
|
| 1881 |
+
|
| 1882 |
+
function logoutAdmin() {
|
| 1883 |
+
localStorage.removeItem(ADMIN_TOKEN_KEY);
|
| 1884 |
+
document.cookie = 'admin_token=; Max-Age=0; path=/';
|
| 1885 |
+
showToast('已注销', 'success');
|
| 1886 |
+
updateLoginButton();
|
| 1887 |
+
}
|
| 1888 |
+
|
| 1889 |
+
function showLoginModal() {
|
| 1890 |
+
document.getElementById('loginPassword').value = '';
|
| 1891 |
+
openModal('loginModal');
|
| 1892 |
+
}
|
| 1893 |
+
|
| 1894 |
+
async function submitLogin() {
|
| 1895 |
+
const pwd = document.getElementById('loginPassword').value;
|
| 1896 |
+
if (!pwd) {
|
| 1897 |
+
showToast('请输入密码', 'warning');
|
| 1898 |
+
return;
|
| 1899 |
+
}
|
| 1900 |
+
try {
|
| 1901 |
+
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
| 1902 |
+
method: 'POST',
|
| 1903 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1904 |
+
body: JSON.stringify({ password: pwd })
|
| 1905 |
+
});
|
| 1906 |
+
const data = await res.json();
|
| 1907 |
+
if (!res.ok || data.error) {
|
| 1908 |
+
throw new Error(data.error || '登录失败');
|
| 1909 |
+
}
|
| 1910 |
+
localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
|
| 1911 |
+
showToast('登录成功', 'success');
|
| 1912 |
+
closeModal('loginModal');
|
| 1913 |
+
loadAllData();
|
| 1914 |
+
updateLoginButton();
|
| 1915 |
+
} catch (e) {
|
| 1916 |
+
showToast('登录失败: ' + e.message, 'error');
|
| 1917 |
+
}
|
| 1918 |
+
}
|
| 1919 |
+
|
| 1920 |
+
async function saveSettings() {
|
| 1921 |
+
const proxyUrl = document.getElementById('proxyUrl').value;
|
| 1922 |
+
const imageModeSelect = document.getElementById('imageOutputMode');
|
| 1923 |
+
const imageOutputMode = imageModeSelect ? imageModeSelect.value : 'url';
|
| 1924 |
+
try {
|
| 1925 |
+
const res = await apiFetch(`${API_BASE}/api/config`, {
|
| 1926 |
+
method: 'PUT',
|
| 1927 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1928 |
+
body: JSON.stringify({ proxy: proxyUrl, image_output_mode: imageOutputMode })
|
| 1929 |
+
});
|
| 1930 |
+
if (!res.ok) throw new Error((await res.json()).detail);
|
| 1931 |
+
showToast('设置保存成功!', 'success');
|
| 1932 |
+
loadConfig();
|
| 1933 |
+
} catch (e) {
|
| 1934 |
+
showToast('保存失败: ' + e.message, 'error');
|
| 1935 |
+
}
|
| 1936 |
+
}
|
| 1937 |
+
|
| 1938 |
+
async function testProxy() {
|
| 1939 |
+
const proxyUrl = document.getElementById('proxyUrl').value;
|
| 1940 |
+
const proxyStatus = document.getElementById('proxyStatus');
|
| 1941 |
+
proxyStatus.textContent = '测试中...';
|
| 1942 |
+
proxyStatus.style.color = 'var(--text-muted)';
|
| 1943 |
+
try {
|
| 1944 |
+
const res = await apiFetch(`${API_BASE}/api/proxy/test`, {
|
| 1945 |
+
method: 'POST',
|
| 1946 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1947 |
+
body: JSON.stringify({ proxy: proxyUrl })
|
| 1948 |
+
});
|
| 1949 |
+
const data = await res.json();
|
| 1950 |
+
if (res.ok && data.success) {
|
| 1951 |
+
proxyStatus.textContent = `测试成功! (${data.delay_ms}ms)`;
|
| 1952 |
+
proxyStatus.style.color = 'var(--success)';
|
| 1953 |
+
} else {
|
| 1954 |
+
throw new Error(data.detail);
|
| 1955 |
+
}
|
| 1956 |
+
} catch (e) {
|
| 1957 |
+
proxyStatus.textContent = `测试失败: ${e.message}`;
|
| 1958 |
+
proxyStatus.style.color = 'var(--danger)';
|
| 1959 |
+
}
|
| 1960 |
+
}
|
| 1961 |
+
|
| 1962 |
+
function refreshConfig() {
|
| 1963 |
+
loadConfig();
|
| 1964 |
+
showToast('配置已刷新', 'info');
|
| 1965 |
+
}
|
| 1966 |
+
|
| 1967 |
+
function downloadConfig() {
|
| 1968 |
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(configData, null, 2));
|
| 1969 |
+
const downloadAnchorNode = document.createElement('a');
|
| 1970 |
+
downloadAnchorNode.setAttribute("href", dataStr);
|
| 1971 |
+
downloadAnchorNode.setAttribute("download", "business_gemini_session.json");
|
| 1972 |
+
document.body.appendChild(downloadAnchorNode);
|
| 1973 |
+
downloadAnchorNode.click();
|
| 1974 |
+
downloadAnchorNode.remove();
|
| 1975 |
+
showToast('配置文件已开始下载', 'success');
|
| 1976 |
+
}
|
| 1977 |
+
|
| 1978 |
+
function uploadConfig() {
|
| 1979 |
+
document.getElementById('configFileInput').click();
|
| 1980 |
+
}
|
| 1981 |
+
|
| 1982 |
+
function handleConfigUpload(event) {
|
| 1983 |
+
const file = event.target.files[0];
|
| 1984 |
+
if (!file) return;
|
| 1985 |
+
const reader = new FileReader();
|
| 1986 |
+
reader.onload = async (e) => {
|
| 1987 |
+
try {
|
| 1988 |
+
const newConfig = JSON.parse(e.target.result);
|
| 1989 |
+
const res = await apiFetch(`${API_BASE}/api/config/import`, {
|
| 1990 |
+
method: 'POST',
|
| 1991 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1992 |
+
body: JSON.stringify(newConfig)
|
| 1993 |
+
});
|
| 1994 |
+
if (!res.ok) throw new Error((await res.json()).detail);
|
| 1995 |
+
showToast('配置导入成功!', 'success');
|
| 1996 |
+
loadAllData();
|
| 1997 |
+
} catch (err) {
|
| 1998 |
+
showToast('导入失败: ' + err.message, 'error');
|
| 1999 |
+
}
|
| 2000 |
+
};
|
| 2001 |
+
reader.readAsText(file);
|
| 2002 |
+
}
|
| 2003 |
+
|
| 2004 |
+
// --- 模态框控制 ---
|
| 2005 |
+
function openModal(modalId) {
|
| 2006 |
+
const modal = document.getElementById(modalId);
|
| 2007 |
+
if (modal) modal.classList.add('show');
|
| 2008 |
+
}
|
| 2009 |
+
|
| 2010 |
+
function closeModal(modalId) {
|
| 2011 |
+
const modal = document.getElementById(modalId);
|
| 2012 |
+
if (modal) modal.classList.remove('show');
|
| 2013 |
+
}
|
| 2014 |
+
|
| 2015 |
+
document.querySelectorAll('.modal').forEach(modal => {
|
| 2016 |
+
modal.addEventListener('click', (e) => {
|
| 2017 |
+
if (e.target.classList.contains('modal')) {
|
| 2018 |
+
closeModal(modal.id);
|
| 2019 |
+
}
|
| 2020 |
+
});
|
| 2021 |
+
});
|
| 2022 |
+
</script>
|
| 2023 |
+
|
| 2024 |
+
</body>
|
| 2025 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Business Gemini OpenAPI 兼容服务依赖
|
| 2 |
+
# Python 3.8+
|
| 3 |
+
|
| 4 |
+
# Web框架
|
| 5 |
+
flask>=2.0.0
|
| 6 |
+
flask-cors>=3.0.0
|
| 7 |
+
|
| 8 |
+
# HTTP请求
|
| 9 |
+
requests>=2.25.0
|
| 10 |
+
|
| 11 |
+
# SSL警告处理(requests依赖)
|
| 12 |
+
urllib3>=1.26.0
|
| 13 |
+
|
| 14 |
+
# 可选:用于从浏览器获取 Cookie (CDP)
|
| 15 |
+
websocket-client>=1.0.0
|