Add /careerbot base path for www.ityb.me/careerbot deployment
- Add BASE_PATH config, include all routers with prefix
- Inject {{ base }} Jinja2 global for all template URLs
- Add window.BASE_PATH for static JS files
- Update Nginx to proxy /careerbot/ path
- Add OPS_MANUAL.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
96997daed0
commit
501f8985ec
396
OPS_MANUAL.md
Normal file
396
OPS_MANUAL.md
Normal file
@ -0,0 +1,396 @@
|
||||
# CareerBot 线上运维手册
|
||||
|
||||
## 1. 服务器信息
|
||||
|
||||
| 项目 | 详情 |
|
||||
|------|------|
|
||||
| 云服务商 | 阿里云 ECS |
|
||||
| 公网 IP | `39.106.14.107` |
|
||||
| 操作系统 | Alibaba Cloud Linux 3 (OpenAnolis Edition) |
|
||||
| 配置 | 2 核 CPU / 2GB 内存 / 40GB 磁盘 |
|
||||
| Python 版本 | 3.11.13 |
|
||||
| Nginx 版本 | 1.20.1 |
|
||||
| Gitea 版本 | 1.22.6 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 账号信息
|
||||
|
||||
### 2.1 服务器账号
|
||||
|
||||
| 账号 | 用途 | 登录方式 |
|
||||
|------|------|---------|
|
||||
| `root` | 系统管理 | SSH 密钥 (`~/.ssh/id_ed25519`) |
|
||||
| `deploy` | 应用部署和运行 | SSH 密钥(同上),sudo 免密 |
|
||||
| `gitea` | Gitea 服务运行 | 系统用户,不可直接登录 |
|
||||
| `nginx` | Nginx 服务运行 | 系统用户,不可直接登录 |
|
||||
|
||||
**本地 SSH 快捷连接**(已配置在 `~/.ssh/config`):
|
||||
```bash
|
||||
ssh ecs # 以 deploy 用户连接
|
||||
ssh root@ecs # 以 root 用户连接(需将 config 中 User 临时改为 root,或直接用 IP)
|
||||
ssh root@39.106.14.107 # root 直连
|
||||
```
|
||||
|
||||
### 2.2 应用账号
|
||||
|
||||
| 系统 | 地址 | 账号 | 密码 |
|
||||
|------|------|------|------|
|
||||
| CareerBot 管理后台 | `http://39.106.14.107/admin/login` | `ln0422@gmail.com` | `qshs123456` |
|
||||
| Gitea 代码管理 | `http://39.106.14.107:3000` | `ln0422` | `Qshs123456_` |
|
||||
|
||||
### 2.3 ECS 用户密码
|
||||
|
||||
| 用户 | 密码 |
|
||||
|------|------|
|
||||
| `deploy` | `CareerBot2026!` |
|
||||
|
||||
> **注意**:日常运维通过 SSH 密钥登录,密码仅在密钥不可用时作为备用。
|
||||
|
||||
---
|
||||
|
||||
## 3. 服务架构
|
||||
|
||||
```
|
||||
外部访问
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Nginx (:80) │
|
||||
│ 配置: /etc/nginx/sites-enabled/ │
|
||||
│ 日志: /var/log/nginx/ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ http://39.106.14.107/ │
|
||||
│ → proxy_pass http://127.0.0.1:8000 │
|
||||
│ → CareerBot (uvicorn) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ http://39.106.14.107:3000 │
|
||||
│ → Gitea (直接监听,未经 Nginx 代理) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 端口使用
|
||||
|
||||
| 端口 | 服务 | 监听地址 | 防火墙 | 安全组 |
|
||||
|------|------|---------|--------|--------|
|
||||
| 22 | SSH | 0.0.0.0 | 已放行 | 已放行 |
|
||||
| 80 | Nginx → CareerBot | 0.0.0.0 | 已放行 | 已放行 |
|
||||
| 443 | HTTPS (预留) | - | 已放行 | 已放行 |
|
||||
| 3000 | Gitea | 0.0.0.0 | 已放行 | 已放行 |
|
||||
| 8000 | CareerBot (uvicorn) | 127.0.0.1 | 内部 | 无需 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 文件目录结构
|
||||
|
||||
```
|
||||
/home/deploy/apps/CareerBot/ ← 项目根目录
|
||||
├── venv/ ← Python 虚拟环境
|
||||
├── app/ ← 应用代码
|
||||
├── templates/ ← 页面模板
|
||||
├── careerbot.db ← SQLite 数据库(重要数据!)
|
||||
├── uploads/ ← 用户上传文件
|
||||
├── requirements.txt
|
||||
├── init_data.py
|
||||
├── start.bat / stop.bat ← Windows 本地启动脚本(线上不用)
|
||||
├── DESIGN.md ← 设计文档
|
||||
└── OPS_MANUAL.md ← 本文档
|
||||
|
||||
/etc/systemd/system/
|
||||
├── careerbot.service ← CareerBot systemd 服务
|
||||
└── gitea.service ← Gitea systemd 服务
|
||||
|
||||
/etc/nginx/
|
||||
├── nginx.conf ← Nginx 主配置
|
||||
├── sites-available/
|
||||
│ └── careerbot.conf ← CareerBot 站点配置
|
||||
└── sites-enabled/
|
||||
└── careerbot.conf → ../sites-available/careerbot.conf
|
||||
|
||||
/usr/local/bin/gitea ← Gitea 二进制文件
|
||||
/etc/gitea/app.ini ← Gitea 配置文件
|
||||
/var/lib/gitea/ ← Gitea 数据目录(仓库、数据库)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 常用运维命令
|
||||
|
||||
### 5.1 服务管理
|
||||
|
||||
```bash
|
||||
# ── CareerBot ──
|
||||
sudo systemctl status careerbot # 查看状态
|
||||
sudo systemctl restart careerbot # 重启
|
||||
sudo systemctl stop careerbot # 停止
|
||||
sudo systemctl start careerbot # 启动
|
||||
sudo journalctl -u careerbot -f # 实时查看日志
|
||||
sudo journalctl -u careerbot --since "1 hour ago" # 查看最近1小时日志
|
||||
|
||||
# ── Nginx ──
|
||||
sudo systemctl status nginx
|
||||
sudo systemctl reload nginx # 重载配置(不中断连接)
|
||||
sudo systemctl restart nginx # 重启
|
||||
sudo nginx -t # 测试配置文件语法
|
||||
|
||||
# ── Gitea ──
|
||||
sudo systemctl status gitea
|
||||
sudo systemctl restart gitea
|
||||
sudo journalctl -u gitea -f
|
||||
```
|
||||
|
||||
### 5.2 代码更新部署
|
||||
|
||||
**从本地推送代码并部署到线上(标准流程):**
|
||||
|
||||
```bash
|
||||
# 1. 本地:提交并推送到 Gitea
|
||||
cd D:/Files/Projects/VibeCoding/CareerBot
|
||||
git add .
|
||||
git commit -m "描述改动"
|
||||
git push origin main
|
||||
|
||||
# 2. 线上:拉取并重启
|
||||
ssh ecs "cd ~/apps/CareerBot && git pull && sudo systemctl restart careerbot"
|
||||
```
|
||||
|
||||
**一键部署脚本(在本地执行):**
|
||||
```bash
|
||||
ssh ecs "cd ~/apps/CareerBot && git pull origin main && sudo systemctl restart careerbot && sleep 2 && sudo systemctl is-active careerbot"
|
||||
```
|
||||
|
||||
### 5.3 数据库操作
|
||||
|
||||
```bash
|
||||
# 连接到 SQLite 数据库
|
||||
ssh ecs "cd ~/apps/CareerBot && sqlite3 careerbot.db"
|
||||
|
||||
# 常用 SQL
|
||||
.tables # 查看所有表
|
||||
SELECT * FROM admin_user; # 查看管理员
|
||||
SELECT token, note, max_questions, used_questions, is_active FROM access_token; # 查看令牌
|
||||
SELECT * FROM recruiter_message ORDER BY created_at DESC LIMIT 10; # 最近招聘消息
|
||||
SELECT COUNT(*) FROM chat_history; # 对话记录总数
|
||||
.quit # 退出
|
||||
```
|
||||
|
||||
### 5.4 系统监控
|
||||
|
||||
```bash
|
||||
# 查看内存使用
|
||||
ssh ecs "free -h"
|
||||
|
||||
# 查看磁盘使用
|
||||
ssh ecs "df -h /"
|
||||
|
||||
# 查看各服务内存占用
|
||||
ssh ecs "ps aux --sort=-rss | head -10"
|
||||
|
||||
# 查看端口监听
|
||||
ssh ecs "ss -tlnp"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置文件详情
|
||||
|
||||
### 6.1 CareerBot systemd 服务
|
||||
|
||||
文件:`/etc/systemd/system/careerbot.service`
|
||||
```ini
|
||||
[Unit]
|
||||
Description=CareerBot Web Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=deploy
|
||||
WorkingDirectory=/home/deploy/apps/CareerBot
|
||||
ExecStart=/home/deploy/apps/CareerBot/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PATH=/home/deploy/apps/CareerBot/venv/bin:/usr/bin
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
关键参数:
|
||||
- `--host 127.0.0.1`:只监听本地,由 Nginx 代理外部访问
|
||||
- `Restart=always`:崩溃后自动重启
|
||||
- `RestartSec=5`:重启间隔 5 秒
|
||||
|
||||
### 6.2 Nginx 站点配置
|
||||
|
||||
文件:`/etc/nginx/sites-available/careerbot.conf`
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name 39.106.14.107;
|
||||
|
||||
client_max_body_size 10M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# SSE 流式响应支持(关键!关闭缓冲)
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location /uploads/ {
|
||||
alias /home/deploy/apps/CareerBot/uploads/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键配置说明:
|
||||
- `proxy_buffering off` + `proxy_cache off`:**必须关闭**,否则 SSE 流式对话无法实时返回
|
||||
- `proxy_read_timeout 300s`:LLM 长回复可能需要较长时间
|
||||
- `client_max_body_size 10M`:允许上传最大 10MB 文件
|
||||
|
||||
### 6.3 防火墙规则
|
||||
|
||||
```bash
|
||||
# 查看当前规则
|
||||
sudo firewall-cmd --list-ports
|
||||
# 输出: 22/tcp 80/tcp 443/tcp 3000/tcp
|
||||
|
||||
# 添加新端口(如未来部署新项目)
|
||||
sudo firewall-cmd --permanent --add-port=PORT/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
> **注意**:开放端口还需要在阿里云控制台的**安全组**中同步添加入方向规则,否则外部仍无法访问。
|
||||
|
||||
---
|
||||
|
||||
## 7. 备份策略
|
||||
|
||||
### 7.1 数据库备份
|
||||
|
||||
CareerBot 的所有业务数据存储在 SQLite 文件中:
|
||||
|
||||
```bash
|
||||
# 手动备份
|
||||
ssh ecs "cp ~/apps/CareerBot/careerbot.db ~/apps/CareerBot/careerbot.db.bak.$(date +%Y%m%d)"
|
||||
|
||||
# 下载到本地
|
||||
scp ecs:~/apps/CareerBot/careerbot.db ./careerbot_backup_$(date +%Y%m%d).db
|
||||
```
|
||||
|
||||
### 7.2 Gitea 数据备份
|
||||
|
||||
```bash
|
||||
# Gitea 数据和仓库
|
||||
ssh ecs "sudo tar czf /tmp/gitea-backup.tar.gz /var/lib/gitea /etc/gitea"
|
||||
scp ecs:/tmp/gitea-backup.tar.gz ./gitea-backup.tar.gz
|
||||
```
|
||||
|
||||
### 7.3 需要备份的关键文件
|
||||
|
||||
| 文件/目录 | 说明 | 重要程度 |
|
||||
|-----------|------|---------|
|
||||
| `~/apps/CareerBot/careerbot.db` | CareerBot 全部业务数据 | **极高** |
|
||||
| `~/apps/CareerBot/uploads/` | 用户上传的文件 | 高 |
|
||||
| `/var/lib/gitea/` | Gitea 所有仓库和数据 | **极高** |
|
||||
| `/etc/gitea/app.ini` | Gitea 配置 | 中 |
|
||||
| `/etc/nginx/sites-available/` | Nginx 站点配置 | 中 |
|
||||
| `/etc/systemd/system/careerbot.service` | 服务配置 | 低(可重建) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 故障排查
|
||||
|
||||
### 8.1 CareerBot 无法访问
|
||||
|
||||
```bash
|
||||
# 1. 检查服务状态
|
||||
sudo systemctl status careerbot
|
||||
|
||||
# 2. 查看错误日志
|
||||
sudo journalctl -u careerbot --since "10 min ago"
|
||||
|
||||
# 3. 检查端口
|
||||
ss -tlnp | grep 8000
|
||||
|
||||
# 4. 检查 Nginx
|
||||
sudo nginx -t
|
||||
sudo systemctl status nginx
|
||||
|
||||
# 5. 手动启动测试
|
||||
cd ~/apps/CareerBot
|
||||
source venv/bin/activate
|
||||
uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
### 8.2 AI 对话不工作
|
||||
|
||||
- 检查管理后台 LLM Config 是否已配置(API URL、API Key、Model Name)
|
||||
- 点击 "Test Connection" 测试连通性
|
||||
- 确认 ECS 能访问外网:`curl https://api.deepseek.com`
|
||||
|
||||
### 8.3 Gitea 无法访问
|
||||
|
||||
```bash
|
||||
sudo systemctl status gitea
|
||||
sudo journalctl -u gitea --since "10 min ago"
|
||||
# 检查 3000 端口和安全组
|
||||
```
|
||||
|
||||
### 8.4 磁盘/内存不足
|
||||
|
||||
```bash
|
||||
# 查看磁盘
|
||||
df -h /
|
||||
|
||||
# 清理日志
|
||||
sudo journalctl --vacuum-time=7d
|
||||
|
||||
# 查看内存
|
||||
free -h
|
||||
|
||||
# 查看占用最多的进程
|
||||
ps aux --sort=-rss | head -10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 新项目部署指南
|
||||
|
||||
后续在同一台 ECS 上部署新项目的标准流程:
|
||||
|
||||
1. **Gitea 上创建仓库**:`http://39.106.14.107:3000` → New Repository
|
||||
|
||||
2. **本地推送代码**
|
||||
3. **ECS 上克隆并配置**:
|
||||
```bash
|
||||
cd ~/apps
|
||||
git clone http://39.106.14.107:3000/ln0422/新项目.git
|
||||
# 安装依赖、初始化等
|
||||
```
|
||||
|
||||
4. **创建 systemd 服务**(参考 careerbot.service,注意更换端口号)
|
||||
|
||||
5. **添加 Nginx 站点配置**:
|
||||
```bash
|
||||
sudo vim /etc/nginx/sites-available/新项目.conf
|
||||
sudo ln -s /etc/nginx/sites-available/新项目.conf /etc/nginx/sites-enabled/
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
6. **如需新端口**:防火墙 + 阿里云安全组同步放行
|
||||
|
||||
---
|
||||
|
||||
## 10. 安全注意事项
|
||||
|
||||
1. **SSH 密钥**:本地私钥 `~/.ssh/id_ed25519` 请妥善保管,丢失需重新生成并更新服务器
|
||||
2. **Gitea 端口**:3000 端口对外开放,建议设置强密码,或后续通过 Nginx 代理并限制访问
|
||||
3. **数据库文件**:`careerbot.db` 不应对外暴露,uvicorn 仅监听 127.0.0.1 已确保安全
|
||||
4. **定期更新**:定期执行 `sudo dnf update -y` 更新系统安全补丁
|
||||
5. **HTTPS**:后续绑定域名后建议配置 Let's Encrypt SSL 证书
|
||||
@ -8,6 +8,7 @@ class Settings(BaseSettings):
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480
|
||||
UPLOAD_DIR: str = "./uploads"
|
||||
BASE_PATH: str = "/careerbot"
|
||||
|
||||
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
||||
|
||||
|
||||
29
app/main.py
29
app/main.py
@ -1,6 +1,7 @@
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.config import settings
|
||||
@ -9,18 +10,30 @@ from app.models import AdminUser, Profile
|
||||
from app.routers import auth, public, admin, chat
|
||||
from app.routers.auth import hash_password
|
||||
|
||||
BASE = settings.BASE_PATH
|
||||
|
||||
app = FastAPI(title="CareerBot", description="Personal Career Showcase with AI Assistant")
|
||||
|
||||
# Mount static files and uploads
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
# Mount static files and uploads under BASE_PATH
|
||||
app.mount(f"{BASE}/static", StaticFiles(directory="app/static"), name="static")
|
||||
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
|
||||
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
|
||||
app.mount(f"{BASE}/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router)
|
||||
app.include_router(public.router)
|
||||
app.include_router(admin.router)
|
||||
app.include_router(chat.router)
|
||||
# Include routers with BASE_PATH prefix
|
||||
app.include_router(auth.router, prefix=BASE)
|
||||
app.include_router(public.router, prefix=BASE)
|
||||
app.include_router(admin.router, prefix=BASE)
|
||||
app.include_router(chat.router, prefix=BASE)
|
||||
|
||||
# Set Jinja2 template globals for BASE_PATH
|
||||
admin.templates.env.globals["base"] = BASE
|
||||
public.templates.env.globals["base"] = BASE
|
||||
|
||||
|
||||
# Root redirect to CareerBot
|
||||
@app.get("/")
|
||||
def root_redirect():
|
||||
return RedirectResponse(url=f"{BASE}/", status_code=302)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
|
||||
@ -9,6 +9,9 @@ from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
|
||||
BASE = settings.BASE_PATH
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import (
|
||||
Profile, Skill, Education, WorkExperience,
|
||||
@ -95,7 +98,7 @@ async def upload_photo(
|
||||
if not p:
|
||||
p = Profile()
|
||||
db.add(p)
|
||||
p.photo_url = f"/uploads/{filename}"
|
||||
p.photo_url = f"{BASE}/uploads/{filename}"
|
||||
db.commit()
|
||||
return {"photo_url": p.photo_url}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ from app.database import get_db
|
||||
from app.models import AdminUser, AccessToken
|
||||
|
||||
router = APIRouter()
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login", auto_error=False)
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.BASE_PATH}/api/admin/login", auto_error=False)
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
|
||||
@ -3,9 +3,12 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models import Profile, Skill, Education, WorkExperience, AccessToken
|
||||
|
||||
BASE = settings.BASE_PATH
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
@ -14,14 +17,14 @@ templates = Jinja2Templates(directory="templates")
|
||||
def index_page(request: Request, db: Session = Depends(get_db)):
|
||||
token = request.cookies.get("visitor_token")
|
||||
if not token:
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
return RedirectResponse(url=f"{BASE}/login", status_code=302)
|
||||
# Allow both valid tokens and anonymous
|
||||
if token != "__anonymous__":
|
||||
record = db.query(AccessToken).filter(
|
||||
AccessToken.token == token, AccessToken.is_active == True
|
||||
).first()
|
||||
if not record:
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
return RedirectResponse(url=f"{BASE}/login", status_code=302)
|
||||
return templates.TemplateResponse(request, "index.html")
|
||||
|
||||
|
||||
|
||||
@ -1,36 +1,37 @@
|
||||
// ── Admin Panel JavaScript ──
|
||||
const BASE_PATH = window.BASE_PATH || '';
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') };
|
||||
}
|
||||
|
||||
async function apiGet(url) {
|
||||
const resp = await fetch(url, { headers: authHeaders() });
|
||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
||||
const resp = await fetch(BASE_PATH + url, { headers: authHeaders() });
|
||||
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiPost(url, data) {
|
||||
const resp = await fetch(url, {
|
||||
const resp = await fetch(BASE_PATH + url, {
|
||||
method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
||||
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiPut(url, data) {
|
||||
const resp = await fetch(url, {
|
||||
const resp = await fetch(BASE_PATH + url, {
|
||||
method: 'PUT', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
||||
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiDelete(url) {
|
||||
const resp = await fetch(url, { method: 'DELETE', headers: authHeaders() });
|
||||
if (resp.status === 401) { window.location.href = '/admin/login'; return null; }
|
||||
const resp = await fetch(BASE_PATH + url, { method: 'DELETE', headers: authHeaders() });
|
||||
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
@ -68,7 +69,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function adminLogout() {
|
||||
localStorage.removeItem('admin_token');
|
||||
window.location.href = '/admin/login';
|
||||
window.location.href = BASE_PATH + '/admin/login';
|
||||
}
|
||||
|
||||
// ── Dashboard ──
|
||||
@ -113,10 +114,10 @@ async function uploadPhoto() {
|
||||
if (!input.files[0]) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', input.files[0]);
|
||||
const resp = await fetch('/api/admin/profile/photo', {
|
||||
const resp = await fetch(BASE_PATH + '/api/admin/profile/photo', {
|
||||
method: 'POST', headers: authHeaders(), body: formData,
|
||||
});
|
||||
if (resp.status === 401) { window.location.href = '/admin/login'; return; }
|
||||
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return; }
|
||||
const data = await resp.json();
|
||||
if (data.photo_url) {
|
||||
document.getElementById('photo-preview').innerHTML = `<img src="${data.photo_url}" style="max-width:120px;border-radius:8px;">`;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// ── Chat Widget JavaScript ──
|
||||
const BASE_PATH = window.BASE_PATH || '';
|
||||
|
||||
let chatSessionId = sessionStorage.getItem('chat_session');
|
||||
if (!chatSessionId) {
|
||||
@ -66,7 +67,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
async function loadChatConfig() {
|
||||
try {
|
||||
const resp = await fetch('/api/chat/config');
|
||||
const resp = await fetch(BASE_PATH + '/api/chat/config');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
maxQuestions = data.max_questions || 10;
|
||||
@ -96,7 +97,7 @@ async function loadChatConfig() {
|
||||
|
||||
async function loadChatHistory() {
|
||||
try {
|
||||
const resp = await fetch(`/api/chat/history/${chatSessionId}`);
|
||||
const resp = await fetch(BASE_PATH + `/api/chat/history/${chatSessionId}`);
|
||||
if (!resp.ok) return;
|
||||
const history = await resp.json();
|
||||
for (const msg of history) {
|
||||
@ -147,7 +148,7 @@ async function sendMessage() {
|
||||
formData.append('message', message);
|
||||
if (file) formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/chat', {
|
||||
const response = await fetch(BASE_PATH + '/api/chat', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
@ -243,7 +244,7 @@ async function enterToken() {
|
||||
const token = prompt('请输入访问令牌 (Access Token):');
|
||||
if (!token || !token.trim()) return;
|
||||
try {
|
||||
const resp = await fetch('/api/verify-token', {
|
||||
const resp = await fetch(BASE_PATH + '/api/verify-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token.trim() }),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// ── Main Page JavaScript ──
|
||||
const BASE_PATH = window.BASE_PATH || '';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadProfile();
|
||||
@ -8,9 +9,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
async function apiFetch(url) {
|
||||
const resp = await fetch(url);
|
||||
const resp = await fetch(BASE_PATH + url);
|
||||
if (resp.status === 401) {
|
||||
window.location.href = '/login';
|
||||
window.location.href = BASE_PATH + '/login';
|
||||
return null;
|
||||
}
|
||||
return resp.json();
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -47,6 +47,7 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Education</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -60,6 +60,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Experience</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -61,6 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - LLM Configuration</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -76,6 +76,7 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Login</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
@ -18,6 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const BASE = "{{ base }}";
|
||||
document.getElementById('password').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') doLogin();
|
||||
});
|
||||
@ -35,7 +36,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/admin/login', {
|
||||
const resp = await fetch(BASE + '/api/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
@ -43,7 +44,7 @@
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
localStorage.setItem('admin_token', data.access_token);
|
||||
window.location.href = '/admin/dashboard';
|
||||
window.location.href = BASE + '/admin/dashboard';
|
||||
} else {
|
||||
errEl.textContent = 'Invalid email or password';
|
||||
errEl.style.display = 'block';
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Messages</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -37,6 +37,7 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Profile</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -75,6 +75,7 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Resume Generator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
<style>
|
||||
.resume-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
||||
.resume-output {
|
||||
@ -28,15 +28,15 @@
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -78,8 +78,10 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
<script>
|
||||
const BASE = window.BASE_PATH || '';
|
||||
let resumeMarkdown = '';
|
||||
|
||||
async function generateResume() {
|
||||
@ -105,13 +107,13 @@ async function generateResume() {
|
||||
if (file) formData.append('file', file);
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/admin/resume/generate', {
|
||||
const resp = await fetch(BASE + '/api/admin/resume/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (resp.status === 401) { window.location.href = '/admin/login'; return; }
|
||||
if (resp.status === 401) { window.location.href = BASE + '/admin/login'; return; }
|
||||
|
||||
output.innerHTML = '';
|
||||
const reader = resp.body.getReader();
|
||||
@ -159,7 +161,7 @@ async function downloadResume() {
|
||||
formData.append('content', resumeMarkdown);
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/admin/resume/download', {
|
||||
const resp = await fetch(BASE + '/api/admin/resume/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') },
|
||||
body: formData,
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Skills</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -52,6 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot Admin - Access Tokens</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo">CareerBot Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="{{ base }}/admin/dashboard"><span class="icon">■</span> Dashboard</a>
|
||||
<a href="{{ base }}/admin/profile"><span class="icon">☺</span> Profile</a>
|
||||
<a href="{{ base }}/admin/education"><span class="icon">☆</span> Education</a>
|
||||
<a href="{{ base }}/admin/experience"><span class="icon">✎</span> Experience</a>
|
||||
<a href="{{ base }}/admin/skills"><span class="icon">★</span> Skills</a>
|
||||
<a href="{{ base }}/admin/tokens"><span class="icon">⚷</span> Access Tokens</a>
|
||||
<a href="{{ base }}/admin/messages"><span class="icon">✉</span> Messages <span class="nav-badge" id="nav-badge" style="display:none;"></span></a>
|
||||
<a href="{{ base }}/admin/llm-config"><span class="icon">⚙</span> LLM Config</a>
|
||||
<a href="{{ base }}/admin/resume"><span class="icon">✍</span> Resume Gen</a>
|
||||
<a href="#" onclick="adminLogout()"><span class="icon">→</span> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@ -74,6 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/admin.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -74,7 +74,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/chat.js"></script>
|
||||
<script>window.BASE_PATH = "{{ base }}";</script>
|
||||
<script src="{{ base }}/static/js/main.js"></script>
|
||||
<script src="{{ base }}/static/js/chat.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerBot - Access</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
@ -21,6 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const BASE = "{{ base }}";
|
||||
document.getElementById('token-input').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') verifyToken();
|
||||
});
|
||||
@ -31,13 +32,13 @@
|
||||
const errEl = document.getElementById('error-msg');
|
||||
errEl.style.display = 'none';
|
||||
try {
|
||||
const resp = await fetch('/api/verify-token', {
|
||||
const resp = await fetch(BASE + '/api/verify-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/';
|
||||
window.location.href = BASE + '/';
|
||||
} else {
|
||||
errEl.textContent = '无效或已过期的访问令牌';
|
||||
errEl.style.display = 'block';
|
||||
@ -50,9 +51,9 @@
|
||||
|
||||
async function enterAnonymous() {
|
||||
try {
|
||||
const resp = await fetch('/api/anonymous-entry', { method: 'POST' });
|
||||
const resp = await fetch(BASE + '/api/anonymous-entry', { method: 'POST' });
|
||||
if (resp.ok) {
|
||||
window.location.href = '/';
|
||||
window.location.href = BASE + '/';
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('error-msg').textContent = '网络错误,请稍后重试';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user