commit 95770afe213955f4475519bb7a1edcf2485f4d99 Author: 111 Date: Fri Jan 23 13:57:48 2026 +0800 Initial commit: AI Interview System diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..ba1372c --- /dev/null +++ b/.drone.yml @@ -0,0 +1,64 @@ +kind: pipeline +type: docker +name: build-and-deploy + +trigger: + branch: + - main + - develop + event: + - push + +steps: + # 构建后端镜像 + - name: build-backend + image: docker:dind + volumes: + - name: docker-sock + path: /var/run/docker.sock + commands: + - docker build -t ai-interview-backend:${DRONE_COMMIT_SHA:0:8} -f deploy/Dockerfile.backend . + - docker tag ai-interview-backend:${DRONE_COMMIT_SHA:0:8} ai-interview-backend:latest + + # 构建前端镜像 + - name: build-frontend + image: docker:dind + volumes: + - name: docker-sock + path: /var/run/docker.sock + commands: + - docker build -t ai-interview-frontend:${DRONE_COMMIT_SHA:0:8} -f deploy/Dockerfile.frontend . + - docker tag ai-interview-frontend:${DRONE_COMMIT_SHA:0:8} ai-interview-frontend:latest + + # 部署到服务器 + - name: deploy + image: docker:dind + volumes: + - name: docker-sock + path: /var/run/docker.sock + environment: + COZE_PAT_TOKEN: + from_secret: coze_pat_token + COZE_BOT_ID: + from_secret: coze_bot_id + COZE_WORKFLOW_A_ID: + from_secret: coze_workflow_a_id + COZE_WORKFLOW_C_ID: + from_secret: coze_workflow_c_id + FILE_SERVER_URL: + from_secret: file_server_url + FILE_SERVER_TOKEN: + from_secret: file_server_token + commands: + - docker stop ai-interview-backend ai-interview-frontend || true + - docker rm ai-interview-backend ai-interview-frontend || true + - docker run -d --name ai-interview-backend -p 8000:8000 --restart unless-stopped -e COZE_PAT_TOKEN=$COZE_PAT_TOKEN -e COZE_BOT_ID=$COZE_BOT_ID -e COZE_WORKFLOW_A_ID=$COZE_WORKFLOW_A_ID -e COZE_WORKFLOW_C_ID=$COZE_WORKFLOW_C_ID -e FILE_SERVER_URL=$FILE_SERVER_URL -e FILE_SERVER_TOKEN=$FILE_SERVER_TOKEN ai-interview-backend:latest + - docker run -d --name ai-interview-frontend -p 3000:80 --restart unless-stopped ai-interview-frontend:latest + when: + branch: + - main + +volumes: + - name: docker-sock + host: + path: /var/run/docker.sock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4279e79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# 环境变量 +.env +.env.local +.env.*.local + +# 依赖目录 +node_modules/ +venv/ +__pycache__/ + +# 构建产物 +dist/ +build/ +*.egg-info/ + +# IDE 配置 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 系统文件 +.DS_Store +Thumbs.db + +# 日志文件 +*.log +logs/ + +# 临时文件 +tmp/ +temp/ +*.tmp + +# 敏感信息 +*credentials*.json +*secret*.json +*.pem +*.key + +# 本地历史 +.history/ + +# 测试覆盖率 +coverage/ +.nyc_output/ + +# n8n 本地数据(如有) +.n8n/ diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..361e0ba --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,104 @@ +# 项目上下文 + +> AI启动时必读此文件,快速了解项目全貌 + +## 一、项目信息 + +| 项目 | 内容 | +|------|------| +| **项目编号** | 011-ai-interview-2601 | +| **项目路径** | `projects/011-ai-interview-2601/` | +| **当前阶段** | 开发阶段 - 核心功能完成,语音模式优化中 | +| **项目状态** | 🟡 开发中 - 文字模式可用,语音模式待优化 | +| **启动日期** | 2026-01-20 | +| **技术栈** | Vue3 + TypeScript + FastAPI + Coze API + @coze/realtime-api | + +## 二、AI启动指令 + +请依次阅读以下文件: + +1. **框架层**(了解规则) + - `../../_framework/agents/00-框架总览.md` + - 检查 `agents/` 是否有项目覆盖 + +2. **项目文档**(了解当前状态) + - `docs/同步清单.md` + - `docs/项目状态快照.md` + - `docs/决策记录.md`(最近10条) + +3. **技术选型**(本项目有覆盖) + - `docs/技术选型.md` ⚠️ 必读,包含框架规范覆盖说明 + +## 三、文件访问边界 + +| 区域 | 读取 | 写入 | +|------|------|------| +| ✅ 本项目目录 | 允许 | 允许 | +| ✅ `_framework/` | 允许 | ⚠️ 需确认 | +| ⚠️ `_private/` | 需许可 | ❌ **绝对禁止** | +| ❌ 其他项目 | 禁止 | 禁止 | + +## 四、关键联系人 + +| 角色 | 姓名 | 说明 | +|------|------|------| +| 项目负责人 | | | +| 技术负责人 | | | + +## 五、项目简介 + +**AI Interview** - AI 语音面试系统 + +为轻医美行业打造 AI 面试官系统,实现咨询师岗位的智能初试: +1. 候选人访问网页,输入姓名、上传简历 +2. 系统"模拟来电",候选人接听后进入实时语音面试 +3. AI 面试官按预设流程提问(销售技能、销售观、素质项、求职动机) +4. 面试结束后,系统生成评分和分析报告 +5. HR/管理员在后台查看候选人的完整分析报告 + +## 六、现有资源 + +| 资源 | ID | 说明 | +|------|-----|------| +| Workflow A (初始化) | 7597357422713798710 | 接收 name + file_url,生成 session_id | +| Workflow B (面试) | 7595077233002840079 | 4 维度提问、评分、生成报告 | +| Workflow C (查询) | 7597376294612107318 | 数据库增删改查封装 | +| Coze 数据库 | 7595077053909712922 | assessments, logs, config 三表 | +| 文件服务器 | files.test.ai.ireborn.com.cn | 自建 Nginx + PHP 文件服务 | + +## 七、技术栈覆盖 + +> ⚠️ 本项目与框架规范有以下差异,详见 `docs/技术选型.md` + +| 项目 | 框架默认 | 本项目 | +|------|----------|--------| +| AI 网关 | OpenRouter | Coze API | +| 数据存储 | MySQL | Coze 数据库 | +| 实时音视频 | - | 火山引擎 RTC | + +## 八、注意事项 + +- Coze PAT Token 和 RTC AppKey 必须存储在后端,禁止暴露给前端 +- 语音格式需匹配 Coze 支持的格式(推荐 PCM 16000Hz) +- 需处理网络不稳定时的断线重连逻辑 + +## 九、进度文档 + +- 📋 **最新进度**: `docs/项目进度总结-20260121-final.md` +- 📚 **框架经验**: `_framework/specs/Coze集成经验.md` + +## 十、已完成功能清单 + +| 模块 | 功能 | 状态 | +|------|------|------| +| 用户端 | 欢迎页、信息采集、面试初始化 | ✅ | +| 用户端 | 文字面试模式 | ✅ | +| 用户端 | 语音面试模式 | 🟡 RTC 正常,session_id 传递待优化 | +| 管理后台 | 登录、数据概览、面试列表、详情 | ✅ | +| 管理后台 | 骨架屏加载、阶段标签 | ✅ | +| 后端 | 文件上传、初始化、聊天、房间 API | ✅ | +| 后端 | 管理后台 API (通过 Workflow C) | ✅ | + +--- + +> 最后更新:2026-01-21 diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..75d787e --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# AI Interview Backend App diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..aacaa5d --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,55 @@ +""" +配置管理 +""" +import os +from typing import List +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """应用配置""" + + # 基础配置 + DEBUG: bool = True + API_PORT: int = 8000 + + # CORS 配置 - 支持环境变量覆盖 + CORS_ORIGINS: List[str] = [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://interview.test.ai.ireborn.com.cn", + "https://interview.test.ai.ireborn.com.cn" + ] + + # Coze 配置 + COZE_API_BASE: str = "https://api.coze.cn" + COZE_PAT_TOKEN: str = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" + COZE_BOT_ID: str = "7595113005181386792" + + # Coze 语音配置(从测试结果获取) + COZE_VOICE_ID: str = "7426725529589661723" + + # 文件存储配置 + UPLOAD_DIR: str = "uploads" # 本地上传文件存储目录(临时) + + # 远程文件服务器配置(用于 Coze 工作流访问文件) + FILE_SERVER_UPLOAD_URL: str = "http://files.test.ai.ireborn.com.cn/upload.php" + FILE_SERVER_TOKEN: str = "" # PHP 上传接口的验证令牌 + + # 公网隧道 URL(已弃用,改用远程文件服务器) + TUNNEL_URL: str = "" + NGROK_URL: str = "" + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + case_sensitive = True + + +def get_settings() -> Settings: + """获取配置""" + return Settings() + + +# 直接实例化,每次导入时读取最新 .env +settings = Settings() diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..932021c --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1 @@ +# Routers diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py new file mode 100644 index 0000000..270731e --- /dev/null +++ b/backend/app/routers/admin.py @@ -0,0 +1,239 @@ +""" +后台管理 API +""" +from fastapi import APIRouter, HTTPException, Depends +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from pydantic import BaseModel +from typing import Optional, List, Any +from loguru import logger +import httpx +import json +import secrets + +from app.config import settings + +router = APIRouter(prefix="/api/admin", tags=["admin"]) +security = HTTPBasic() + +# 管理员凭证 +ADMIN_USERNAME = "admin" +ADMIN_PASSWORD = "admin" + +# Coze 配置 +COZE_PAT_TOKEN = settings.COZE_PAT_TOKEN +WORKFLOW_QUERY_ID = "7597376294612107318" + + +def verify_admin(credentials: HTTPBasicCredentials = Depends(security)): + """验证管理员凭证""" + is_username_correct = secrets.compare_digest(credentials.username, ADMIN_USERNAME) + is_password_correct = secrets.compare_digest(credentials.password, ADMIN_PASSWORD) + if not (is_username_correct and is_password_correct): + raise HTTPException( + status_code=401, + detail="Invalid credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +class LoginRequest(BaseModel): + """登录请求""" + username: str + password: str + + +class ApiResponse(BaseModel): + """API 响应""" + code: int = 0 + message: str = "success" + data: Any = None + + +@router.post("/login", response_model=ApiResponse) +async def login(request: LoginRequest): + """ + 管理员登录 + """ + if request.username == ADMIN_USERNAME and request.password == ADMIN_PASSWORD: + return ApiResponse(data={"token": "admin_token", "username": ADMIN_USERNAME}) + raise HTTPException(status_code=401, detail="用户名或密码错误") + + +async def execute_workflow(table: str, sql: str) -> dict: + """ + 执行 Coze 工作流(通用 SQL 查询) + + Args: + table: 表名 - assessments / logs / config + sql: SQL 语句 + """ + url = "https://api.coze.cn/v1/workflow/run" + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + + # 构建 JSON 格式的 input + input_data = json.dumps({ + "table": table, + "sql": sql + }, ensure_ascii=False) + + payload = { + "workflow_id": WORKFLOW_QUERY_ID, + "parameters": { + "input": input_data + } + } + + logger.info(f"Execute workflow: table={table}, sql={sql[:80]}...") + + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post(url, json=payload, headers=headers) + response.raise_for_status() + + data = response.json() + logger.info(f"Workflow response code: {data.get('code')}") + + if data.get("code") == 0: + # 解析返回数据 + result_str = data.get("data", "") + if result_str: + try: + parsed = json.loads(result_str) + # 工作流返回格式: {"output": [...]} + if isinstance(parsed, dict) and "output" in parsed: + return parsed["output"] + return parsed + except: + return {"raw": result_str} + return [] + else: + error_msg = data.get("msg", "Unknown error") + logger.error(f"Workflow error: {error_msg}") + raise HTTPException(status_code=500, detail=error_msg) + + +# ==================== 面试评估 ==================== + +@router.get("/interviews", response_model=ApiResponse) +async def get_interviews( + page: int = 1, + page_size: int = 20, + session_id: Optional[str] = None +): + """ + 获取面试列表 + """ + offset = (page - 1) * page_size + + if session_id: + sql = f""" + SELECT session_id, candidate_name, bstudio_create_time, + sales_skill_score, sales_concept_score, competency_score, + final_score_report, current_stage + FROM ci_interview_assessments + WHERE session_id = '{session_id}' + """ + else: + sql = f""" + SELECT session_id, candidate_name, bstudio_create_time, + sales_skill_score, sales_concept_score, competency_score, + final_score_report, current_stage + FROM ci_interview_assessments + ORDER BY bstudio_create_time DESC + LIMIT {page_size} OFFSET {offset} + """ + + result = await execute_workflow("assessments", sql) + return ApiResponse(data=result) + + +@router.get("/interviews/{session_id}", response_model=ApiResponse) +async def get_interview_detail(session_id: str): + """ + 获取面试详情(完整评估报告) + """ + sql = f"SELECT * FROM ci_interview_assessments WHERE session_id = '{session_id}'" + + result = await execute_workflow("assessments", sql) + + # 返回第一条记录 + if isinstance(result, list) and len(result) > 0: + return ApiResponse(data=result[0]) + + return ApiResponse(data=result) + + +@router.get("/interviews/{session_id}/logs", response_model=ApiResponse) +async def get_interview_logs(session_id: str): + """ + 获取面试对话记录 + """ + sql = f""" + SELECT log_id, session_id, stage, round, ai_question, user_answer, log_type, bstudio_create_time + FROM ci_interview_logs + WHERE session_id = '{session_id}' + ORDER BY bstudio_create_time ASC + """ + + result = await execute_workflow("logs", sql) + return ApiResponse(data=result) + + +@router.delete("/interviews/{session_id}", response_model=ApiResponse) +async def delete_interview(session_id: str): + """ + 删除面试记录(同时删除评估和日志) + """ + # 删除评估 + sql1 = f"DELETE FROM ci_interview_assessments WHERE session_id = '{session_id}'" + await execute_workflow("assessments", sql1) + + # 删除日志 + sql2 = f"DELETE FROM ci_interview_logs WHERE session_id = '{session_id}'" + await execute_workflow("logs", sql2) + + return ApiResponse(message="删除成功") + + +# ==================== 业务配置 ==================== + +@router.get("/configs", response_model=ApiResponse) +async def get_configs(config_type: Optional[str] = None): + """ + 获取业务配置列表 + """ + if config_type: + sql = f""" + SELECT config_id, config_type, item_name, content, bstudio_create_time + FROM ci_business_config + WHERE config_type = '{config_type}' + ORDER BY bstudio_create_time DESC + """ + else: + sql = """ + SELECT config_id, config_type, item_name, content, bstudio_create_time + FROM ci_business_config + ORDER BY config_type, bstudio_create_time DESC + """ + + result = await execute_workflow("config", sql) + return ApiResponse(data=result) + + +# ==================== 统计 ==================== + +@router.get("/stats", response_model=ApiResponse) +async def get_stats(): + """ + 获取统计数据 + """ + # 总面试数 + sql_total = "SELECT COUNT(*) as total FROM ci_interview_assessments" + total_result = await execute_workflow("assessments", sql_total) + + return ApiResponse(data={ + "total_interviews": total_result + }) diff --git a/backend/app/routers/candidate.py b/backend/app/routers/candidate.py new file mode 100644 index 0000000..b6d18e9 --- /dev/null +++ b/backend/app/routers/candidate.py @@ -0,0 +1,83 @@ +""" +候选人相关接口 +""" +import time +import uuid +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +from loguru import logger + +from app.schemas import ApiResponse, SubmitCandidateResponse +from app.services.coze_service import coze_service + +router = APIRouter() + + +def generate_session_id(name: str) -> str: + """生成会话 ID""" + timestamp = int(time.time()) + random_code = uuid.uuid4().hex[:6] + return f"SESS_{timestamp}_{name}_{random_code}" + + +@router.post("/candidates", response_model=ApiResponse) +async def submit_candidate( + name: str = Form(..., min_length=2, max_length=20, description="候选人姓名"), + resume: UploadFile = File(..., description="简历文件"), +): + """ + 提交候选人信息(上传简历) + + - 上传简历到 Coze + - 生成 session_id + - 返回 session_id 和 file_id + """ + try: + # 验证文件类型 + allowed_types = [ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ] + if resume.content_type not in allowed_types: + raise HTTPException( + status_code=400, + detail="只支持 PDF、DOC、DOCX 格式的文件" + ) + + # 验证文件大小(10MB) + content = await resume.read() + if len(content) > 10 * 1024 * 1024: + raise HTTPException( + status_code=400, + detail="文件大小不能超过 10MB" + ) + + # 上传文件到 Coze + file_result = await coze_service.upload_file(content, resume.filename or "resume.pdf") + file_id = file_result.get("id") + + if not file_id: + raise HTTPException( + status_code=500, + detail="文件上传失败" + ) + + # 生成 session_id + session_id = generate_session_id(name) + + logger.info(f"Candidate submitted: name={name}, session_id={session_id}, file_id={file_id}") + + return ApiResponse( + code=0, + message="success", + data=SubmitCandidateResponse( + sessionId=session_id, + fileId=file_id, + ) + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Submit candidate error: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/routers/chat.py b/backend/app/routers/chat.py new file mode 100644 index 0000000..8cfda1e --- /dev/null +++ b/backend/app/routers/chat.py @@ -0,0 +1,48 @@ +""" +文本对话接口(文字面试模式) +""" +from fastapi import APIRouter, HTTPException +from loguru import logger + +from app.schemas import ApiResponse, ChatRequest, ChatResponse +from app.services.coze_service import coze_service + +router = APIRouter() + + +@router.post("/chat", response_model=ApiResponse) +async def chat(request: ChatRequest): + """ + 文本对话(文字面试模式) + + 使用 /v3/chat API 与 Chatflow 对话。 + session_id 通过 user_id 传递,工作流从 USER_INPUT 获取。 + """ + try: + logger.info(f"Chat request: session_id={request.sessionId}, conv_id={request.conversationId}, message={request.message[:50]}...") + + # 使用 /v3/chat API(Chatflow 对话) + # session_id 作为 user_id 传递,工作流从 {{USER_INPUT}} 获取 + result = await coze_service.chat( + message=request.message, + user_id=request.sessionId, # session_id 作为 user_id + conversation_id=request.conversationId, + ) + + response_data = ChatResponse( + reply=result.get("reply", ""), + conversationId=result.get("conversation_id", ""), + debugInfo=result.get("debug_info"), + ) + + logger.info(f"Chat response: {response_data.reply[:50] if response_data.reply else 'empty'}...") + + return ApiResponse( + code=0, + message="success", + data=response_data + ) + + except Exception as e: + logger.error(f"Chat error: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py new file mode 100644 index 0000000..5314b4c --- /dev/null +++ b/backend/app/routers/files.py @@ -0,0 +1,35 @@ +""" +文件服务接口 - 提供文件下载 +""" +import os +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse +from loguru import logger + +from app.config import settings + +router = APIRouter() + + +@router.get("/files/{file_id}") +async def download_file(file_id: str): + """ + 下载文件(供 Coze 工作流访问) + + 文件路径: uploads/{file_id}.pdf + """ + # 构建文件路径 + file_path = os.path.join(settings.UPLOAD_DIR, f"{file_id}.pdf") + + # 检查文件是否存在 + if not os.path.exists(file_path): + logger.error(f"File not found: {file_path}") + raise HTTPException(status_code=404, detail="文件不存在") + + logger.info(f"Serving file: {file_path}") + + return FileResponse( + path=file_path, + filename=f"{file_id}.pdf", + media_type="application/pdf" + ) diff --git a/backend/app/routers/init.py b/backend/app/routers/init.py new file mode 100644 index 0000000..63c3298 --- /dev/null +++ b/backend/app/routers/init.py @@ -0,0 +1,69 @@ +""" +面试初始化接口 +""" +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +from loguru import logger + +from app.schemas import ApiResponse +from app.services.coze_service import coze_service + +router = APIRouter() + + +@router.post("/init-interview", response_model=ApiResponse) +async def init_interview( + name: str = Form(..., description="候选人姓名"), + file: UploadFile = File(..., description="简历文件(PDF)"), +): + """ + 初始化面试 + + 1. 上传简历到 Coze + 2. 获取文件临时 URL + 3. 调用工作流 A(解析简历、创建记录) + 4. 返回 session_id + + 后续使用 session_id 创建语音房间进行面试 + """ + try: + # 验证文件类型 + if not file.filename.lower().endswith('.pdf'): + raise HTTPException(status_code=400, detail="仅支持 PDF 格式") + + # 读取文件内容 + content = await file.read() + + # 验证文件大小(最大 10MB) + if len(content) > 10 * 1024 * 1024: + raise HTTPException(status_code=400, detail="文件大小不能超过 10MB") + + logger.info(f"Init interview: name={name}, file={file.filename}, size={len(content)} bytes") + + # 执行完整的初始化流程(返回详细调试信息) + result = await coze_service.init_interview( + name=name, + file_content=content, + filename=file.filename, + ) + + session_id = result.get("session_id", "") + debug_info = result.get("debug_info", {}) + + logger.info(f"Interview initialized: session_id={session_id}") + logger.info(f"Debug info: {debug_info}") + + return ApiResponse( + code=0, + message="success", + data={ + "sessionId": session_id, + "name": name, + "debugInfo": debug_info, + } + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Init interview error: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/routers/room.py b/backend/app/routers/room.py new file mode 100644 index 0000000..35d8bc7 --- /dev/null +++ b/backend/app/routers/room.py @@ -0,0 +1,129 @@ +""" +房间相关接口 +""" +import time +import uuid +from fastapi import APIRouter, HTTPException +from loguru import logger + +from app.schemas import ApiResponse, CreateRoomRequest, CreateRoomResponse +from app.services.coze_service import coze_service +from app.config import settings + +router = APIRouter() + + +def generate_user_id() -> str: + """生成用户 ID""" + return f"user_{uuid.uuid4().hex[:12]}" + + +def generate_session_id() -> str: + """生成会话 ID""" + timestamp = int(time.time()) + random_code = uuid.uuid4().hex[:6] + return f"SESS_{timestamp}_{random_code}" + + +@router.post("/rooms", response_model=ApiResponse) +async def create_room(request: CreateRoomRequest): + """ + 创建语音房间 + + 方案A:前端不预先收集姓名和简历 + - sessionId 和 fileId 都是可选的 + - 如果没有传入 sessionId,后端自动生成 + - 姓名和简历由 Coze 工作流在对话中收集 + """ + try: + # 生成用户 ID + user_id = generate_user_id() + + # 如果没有传入 sessionId,自动生成 + session_id = request.sessionId or generate_session_id() + + # 调用 Coze API 创建语音房间 + # Coze 会自动让 Bot 加入房间,并返回 RTC 连接信息 + room_data = await coze_service.create_audio_room( + user_id=user_id, + file_id=request.fileId, # 可能是 None + session_id=session_id, + ) + + # Coze 返回的字段映射 + # Coze: app_id, room_id, token, uid + # 前端期望: appId, roomId, token, userId, sessionId + response_data = CreateRoomResponse( + appId=room_data.get("app_id", ""), + roomId=room_data.get("room_id", ""), + token=room_data.get("token", ""), + userId=room_data.get("uid", user_id), + sessionId=session_id, # 返回 sessionId 给前端 + debugInfo=room_data.get("debug_info"), # 调试信息 + ) + + logger.info(f"Room created: session_id={session_id}, room_id={response_data.roomId}") + + return ApiResponse( + code=0, + message="success", + data=response_data + ) + + except Exception as e: + logger.error(f"Create room error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/interviews/{session_id}/end", response_model=ApiResponse) +async def end_interview(session_id: str): + """ + 结束面试 + + - 通知后端面试已结束 + - 可以在这里添加后续处理逻辑 + """ + try: + logger.info(f"Interview ended: session_id={session_id}") + + # TODO: 可以在这里添加后续处理逻辑 + # 例如:更新状态、发送通知等 + + return ApiResponse( + code=0, + message="success", + data={"success": True} + ) + + except Exception as e: + logger.error(f"End interview error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/coze-config", response_model=ApiResponse) +async def get_coze_config(): + """ + 获取 Coze Realtime SDK 配置 + + 返回前端需要的配置信息,用于直接连接 Coze Realtime + 注意:这种方式会暴露 PAT Token 到浏览器,仅用于开发/测试 + """ + try: + config = { + "accessToken": settings.COZE_PAT_TOKEN, + "botId": settings.COZE_BOT_ID, + "voiceId": settings.COZE_VOICE_ID, + "connectorId": "1024", # 固定值 + } + + logger.info(f"Coze config requested: bot_id={settings.COZE_BOT_ID}") + + return ApiResponse( + code=0, + message="success", + data=config + ) + + except Exception as e: + logger.error(f"Get coze config error: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/routers/upload.py b/backend/app/routers/upload.py new file mode 100644 index 0000000..8e0ebf2 --- /dev/null +++ b/backend/app/routers/upload.py @@ -0,0 +1,97 @@ +""" +文件上传接口 +""" +import os +import uuid +from datetime import datetime +from fastapi import APIRouter, UploadFile, File, HTTPException, Request +from fastapi.responses import FileResponse +from loguru import logger + +from app.schemas import ApiResponse +from app.config import settings + +router = APIRouter() + +# 上传目录 +UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads") +os.makedirs(UPLOAD_DIR, exist_ok=True) + + +@router.post("/upload", response_model=ApiResponse) +async def upload_file(request: Request, file: UploadFile = File(...)): + """ + 上传文件并返回公开访问链接 + + - 接收前端上传的文件(如简历 PDF) + - 保存到本地并生成公开链接 + - 返回文件链接供 Coze 工作流使用 + """ + try: + # 验证文件类型 + if not file.filename.lower().endswith('.pdf'): + raise HTTPException(status_code=400, detail="仅支持 PDF 格式") + + # 读取文件内容 + content = await file.read() + + # 验证文件大小(最大 10MB) + if len(content) > 10 * 1024 * 1024: + raise HTTPException(status_code=400, detail="文件大小不能超过 10MB") + + # 生成唯一文件名 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + unique_id = uuid.uuid4().hex[:8] + safe_filename = f"{timestamp}_{unique_id}.pdf" + + # 保存文件 + file_path = os.path.join(UPLOAD_DIR, safe_filename) + with open(file_path, "wb") as f: + f.write(content) + + logger.info(f"File saved: {file_path}, size: {len(content)} bytes") + + # 生成公开访问链接 + # 从请求中获取 host + host = request.headers.get("host", "localhost:8000") + scheme = request.headers.get("x-forwarded-proto", "http") + file_url = f"{scheme}://{host}/api/files/{safe_filename}" + + logger.info(f"File URL: {file_url}") + + return ApiResponse( + code=0, + message="success", + data={ + "fileUrl": file_url, + "fileName": file.filename, + "fileSize": len(content), + } + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Upload error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/files/{filename}") +async def get_file(filename: str): + """ + 获取上传的文件(供 Coze 工作流访问) + """ + # 安全检查:防止路径遍历 + if ".." in filename or "/" in filename or "\\" in filename: + raise HTTPException(status_code=400, detail="Invalid filename") + + file_path = os.path.join(UPLOAD_DIR, filename) + + if not os.path.exists(file_path): + raise HTTPException(status_code=404, detail="File not found") + + return FileResponse( + file_path, + media_type="application/pdf", + filename=filename + ) diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..c166885 --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,100 @@ +""" +Pydantic 数据模型 +""" +from typing import Optional, List, Any +from pydantic import BaseModel, Field +from datetime import datetime + + +# ============ 通用响应 ============ + +class ApiResponse(BaseModel): + """统一 API 响应格式""" + code: int = 0 + message: str = "success" + data: Any = None + + +# ============ 候选人相关 ============ + +class SubmitCandidateResponse(BaseModel): + """提交候选人信息响应""" + sessionId: str + fileId: str + + +class CreateRoomRequest(BaseModel): + """创建房间请求""" + sessionId: Optional[str] = None # 方案A:可选,由后端生成 + fileId: Optional[str] = None # 方案A:可选,由工作流收集 + + +class CreateRoomResponse(BaseModel): + """创建房间响应""" + roomId: str + token: str + appId: str + userId: str + sessionId: Optional[str] = None # 返回给前端用于后续操作 + debugInfo: Optional[Any] = None # 调试信息(语音模式) + + +class EndInterviewResponse(BaseModel): + """结束面试响应""" + success: bool + + +class ChatRequest(BaseModel): + """文本对话请求(模拟语音)""" + sessionId: str + message: str + conversationId: Optional[str] = None + + +class ChatResponse(BaseModel): + """文本对话响应""" + reply: str + conversationId: str + debugInfo: Optional[Any] = None # 调试信息(节点状态、消息列表等) + + +# ============ 管理后台相关 ============ + +class CandidateScores(BaseModel): + """候选人评分""" + salesSkill: int = Field(0, ge=0, le=100) + salesMindset: int = Field(0, ge=0, le=100) + quality: int = Field(0, ge=0, le=100) + motivation: int = Field(0, ge=0, le=100) + total: float = Field(0, ge=0, le=100) + + +class CandidateListItem(BaseModel): + """候选人列表项""" + sessionId: str + name: str + status: str # pending, ongoing, completed + score: Optional[float] = None + createdAt: str + + +class CandidateDetail(BaseModel): + """候选人详情""" + sessionId: str + name: str + resume: Optional[str] = None + status: str + currentStage: int = 0 + scores: Optional[CandidateScores] = None + analysis: Optional[str] = None + interviewLog: Optional[str] = None + createdAt: str + completedAt: Optional[str] = None + + +class CandidateListResponse(BaseModel): + """候选人列表响应""" + list: List[CandidateListItem] + total: int + page: int + pageSize: int diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..69adbd7 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1,4 @@ +# Services +from .coze_service import CozeService, coze_service + +__all__ = ["CozeService", "coze_service"] diff --git a/backend/app/services/coze_service.py b/backend/app/services/coze_service.py new file mode 100644 index 0000000..998fbf7 --- /dev/null +++ b/backend/app/services/coze_service.py @@ -0,0 +1,839 @@ +""" +Coze API 服务封装 +""" +import time +import httpx +from typing import Optional, Dict, Any +from loguru import logger + +from app.config import settings + + +class CozeService: + """Coze API 服务""" + + def __init__(self): + self.base_url = settings.COZE_API_BASE + self.headers = { + "Authorization": f"Bearer {settings.COZE_PAT_TOKEN}", + "Content-Type": "application/json", + } + self.bot_id = settings.COZE_BOT_ID + + # 工作流 A 的 ID(初始化工作流) + INIT_WORKFLOW_ID = "7597357422713798710" + + # 工作流 B 的 ID(面试工作流) + INTERVIEW_WORKFLOW_ID = "7595077233002840079" + + async def upload_file(self, file_content: bytes, filename: str) -> Dict[str, Any]: + """ + 上传文件到 Coze + + Args: + file_content: 文件内容 + filename: 文件名 + + Returns: + {"id": "file_xxx", ...} + """ + url = f"{self.base_url}/v1/files/upload" + + async with httpx.AsyncClient(timeout=60.0) as client: + files = {"file": (filename, file_content)} + headers = {"Authorization": f"Bearer {settings.COZE_PAT_TOKEN}"} + + response = await client.post(url, files=files, headers=headers) + response.raise_for_status() + + data = response.json() + logger.info(f"File uploaded: {data}") + + if "data" in data: + return data["data"] + return data + + async def get_file_url(self, file_id: str) -> str: + """ + 获取 Coze 文件的临时下载链接 + + Args: + file_id: 文件 ID + + Returns: + 文件的临时下载 URL + """ + url = f"{self.base_url}/v1/files/retrieve" + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + url, + params={"file_id": file_id}, + headers=self.headers + ) + response.raise_for_status() + + data = response.json() + logger.info(f"File retrieve response: {data}") + + # 返回文件的临时 URL + file_info = data.get("data", {}) + file_url = file_info.get("url", "") + + if not file_url: + raise ValueError(f"Failed to get file URL for file_id: {file_id}") + + return file_url + + async def run_init_workflow( + self, + name: str, + file_url: str, + ) -> Dict[str, Any]: + """ + 执行初始化工作流(工作流A) + + - 上传简历、解析、创建数据库记录 + - 返回 session_id 和调试信息 + + Args: + name: 候选人姓名 + file_url: 简历文件链接 + + Returns: + {"session_id": "xxx", "raw_response": {...}, "parsed_data": {...}, "debug_url": "..."} + """ + import asyncio + + url = f"{self.base_url}/v1/workflow/run" + + payload = { + "workflow_id": self.INIT_WORKFLOW_ID, + "parameters": { + "name": name, + "file_url": file_url, + } + } + + logger.info(f"Running init workflow with payload: {payload}") + + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post(url, json=payload, headers=self.headers) + response.raise_for_status() + + data = response.json() + logger.info(f"Init workflow response: {data}") + + result = { + "session_id": "", + "raw_response": data, + "parsed_data": None, + "debug_url": data.get("debug_url", ""), + "execute_id": data.get("execute_id", ""), + "code": data.get("code"), + "msg": data.get("msg", ""), + } + + if data.get("code") == 0: + import json + + # execute_id 在顶层 + execute_id = data.get("execute_id", "") + # data 字段是工作流输出(JSON 字符串) + output_str = data.get("data", "") + + result["execute_id"] = execute_id + result["output_str"] = output_str + + logger.info(f"Workflow execute_id: {execute_id}") + logger.info(f"Workflow output_str: {output_str}") + + # 构建调试链接 + result["debug_url"] = f"https://www.coze.cn/work_flow?execute_id={execute_id}&space_id=7516832346776780836&workflow_id={self.INIT_WORKFLOW_ID}&execute_mode=2" + + # 解析工作流输出 + session_id = None + output_data = None + if output_str: + try: + output_data = json.loads(output_str) + result["parsed_data"] = output_data + logger.info(f"Workflow output_data: {output_data}") + + # 尝试从不同格式中提取 session_id + if isinstance(output_data, dict): + # 格式1: {"session_id": "xxx"} + session_id = output_data.get("session_id") + + # 格式2: {"data": "SESS_xxx"} - data 直接是 session_id 字符串 + if not session_id and "data" in output_data: + inner_data = output_data.get("data") + # 如果 data 是字符串且以 SESS_ 开头,直接使用 + if isinstance(inner_data, str) and inner_data.startswith("SESS_"): + session_id = inner_data + elif isinstance(inner_data, str): + # 尝试解析为 JSON + try: + inner_data = json.loads(inner_data) + except: + pass + if isinstance(inner_data, dict): + session_id = inner_data.get("session_id") + elif isinstance(inner_data, list) and len(inner_data) > 0: + session_id = inner_data[0].get("session_id") if isinstance(inner_data[0], dict) else None + elif isinstance(output_data, list) and len(output_data) > 0: + # 格式3: [{"session_id": "xxx"}] + if isinstance(output_data[0], dict): + session_id = output_data[0].get("session_id") + except json.JSONDecodeError as e: + logger.warning(f"Failed to parse workflow output: {e}") + result["parse_error"] = str(e) + + # 如果没有 session_id,使用 execute_id 作为替代 + if not session_id: + logger.warning(f"No session_id in workflow output, using execute_id as session_id") + session_id = f"WF_{execute_id}" if execute_id else f"SESS_{int(time.time())}" + result["session_id_source"] = "execute_id_fallback" + else: + result["session_id_source"] = "workflow_output" + + result["session_id"] = session_id + logger.info(f"Final session_id: {session_id}") + return result + else: + error_msg = data.get("msg", "Unknown error") + result["error"] = error_msg + raise ValueError(f"Workflow execution failed: {error_msg}") + + async def _wait_for_workflow_result( + self, + execute_id: str, + max_retries: int = 60, + ) -> str: + """ + 等待工作流执行完成并获取结果 + """ + import asyncio + + url = f"{self.base_url}/v1/workflow/run_histories" + + for i in range(max_retries): + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + url, + params={"execute_id": execute_id}, + headers=self.headers + ) + + if response.status_code == 200: + data = response.json() + logger.info(f"Workflow status [{i}]: {data}") + + if data.get("code") == 0: + result_data = data.get("data", {}) + status = result_data.get("status", "") + + if status == "Success": + output = result_data.get("output", "") + # 解析输出获取 session_id + try: + import json + output_data = json.loads(output) + return output_data.get("session_id", output) + except: + return output + elif status == "Failed": + raise ValueError(f"Workflow failed: {result_data.get('error', 'Unknown')}") + # Running 状态继续等待 + + await asyncio.sleep(1) + + raise TimeoutError("Workflow execution timeout") + + async def init_interview( + self, + name: str, + file_content: bytes, + filename: str, + ) -> Dict[str, Any]: + """ + 完整的面试初始化流程 + + 1. 上传文件到远程服务器 + 2. 获取公网可访问的 URL + 3. 执行初始化工作流 + 4. 返回 session_id 和调试信息 + + Args: + name: 候选人姓名 + file_content: 简历文件内容 + filename: 文件名 + + Returns: + {"session_id": "xxx", "debug_info": {...}} + """ + debug_info = { + "steps": [], + "timestamps": {}, + } + + # 1. 上传文件到远程服务器 + debug_info["steps"].append("Step 1: Uploading file to remote server") + debug_info["file_size"] = len(file_content) + + logger.info(f"Step 1: Uploading file to remote server ({len(file_content)} bytes)...") + + # 检查配置 + if not settings.FILE_SERVER_TOKEN: + raise ValueError("FILE_SERVER_TOKEN is not configured. Please set FILE_SERVER_TOKEN in .env file.") + + # 调用远程 PHP 上传接口 + upload_url = settings.FILE_SERVER_UPLOAD_URL + + async with httpx.AsyncClient(timeout=60.0) as client: + files = {"file": (filename, file_content, "application/pdf")} + data = {"token": settings.FILE_SERVER_TOKEN} + + response = await client.post(upload_url, files=files, data=data) + response.raise_for_status() + + upload_result = response.json() + logger.info(f"Upload response: {upload_result}") + + if upload_result.get("code") != 0: + error_msg = upload_result.get("error", "Unknown upload error") + raise ValueError(f"File upload failed: {error_msg}") + + file_url = upload_result.get("url", "") + file_id = upload_result.get("file_id", "") + + debug_info["steps"].append("File uploaded successfully") + debug_info["file_id"] = file_id + debug_info["file_url"] = file_url + debug_info["upload_response"] = upload_result + + logger.info(f"Step 1 completed: file_url={file_url}") + + # 2. 执行初始化工作流 + logger.info(f"Step 2: Running init workflow with name={name}, file_url={file_url}...") + debug_info["steps"].append("Step 2: Running init workflow") + debug_info["workflow_input"] = {"name": name, "file_url": file_url} + + workflow_result = await self.run_init_workflow(name, file_url) + + # workflow_result 现在返回更多信息 + session_id = workflow_result.get("session_id", "") + debug_info["steps"].append("Workflow completed") + debug_info["workflow_response"] = workflow_result.get("raw_response") + debug_info["workflow_data"] = workflow_result.get("parsed_data") + debug_info["debug_url"] = workflow_result.get("debug_url") + + logger.info(f"Init workflow completed, session_id: {session_id}") + + return { + "session_id": session_id, + "debug_info": debug_info + } + + async def create_audio_room( + self, + user_id: str, + file_id: Optional[str] = None, + session_id: Optional[str] = None, + ) -> Dict[str, Any]: + """ + 创建语音房间,让 Bot 加入 + + Coze API 会返回完整的 RTC 连接信息: + - app_id: 火山引擎 RTC App ID + - room_id: 房间 ID + - token: RTC Token + - uid: 用户 ID + + Args: + user_id: 用户 ID + file_id: 简历文件 ID(可选) + session_id: 会话 ID(可选) + + Returns: + RTC 连接信息 + """ + url = f"{self.base_url}/v1/audio/rooms" + + # 将 session_id 作为 user_id 传递,这样工作流可以从 sys_var.user_id 获取 + # 如果有 session_id,用它作为 user_id;否则用原始 user_id + actual_user_id = session_id if session_id else user_id + + payload = { + "bot_id": self.bot_id, + "user_id": actual_user_id, # session_id 作为 user_id + } + + # 添加语音 ID + if settings.COZE_VOICE_ID: + payload["voice_id"] = settings.COZE_VOICE_ID + + # ========== 尝试通过多种方式传递 session_id ========== + + # 方式 1: parameters(类似工作流 API) + if session_id: + payload["parameters"] = { + "session_id": session_id, + } + + # 方式 2: custom_variables(类似对话 API) + if session_id: + payload["custom_variables"] = { + "session_id": session_id, + } + + # 方式 3: connector_id / extra_info(备用) + if session_id: + payload["extra_info"] = { + "session_id": session_id, + } + + # 方式 4: config 对象 + config = {} + if file_id: + config["input_file_id"] = file_id + if session_id: + config["session_id"] = session_id + config["parameters"] = {"session_id": session_id} + if config: + payload["config"] = config + + logger.info(f"Creating audio room with payload: {payload}") + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post(url, json=payload, headers=self.headers) + response.raise_for_status() + + data = response.json() + logger.info(f"Audio room created: {data}") + + result = data.get("data", data) + + # 添加调试信息(避免循环引用,不包含 data 字段) + result["debug_info"] = { + "session_id": session_id, + "actual_user_id": actual_user_id, + "bot_id": self.bot_id, + "request_payload": payload, # 完整的请求参数(包括所有尝试的字段) + "coze_code": data.get("code"), + "coze_msg": data.get("msg"), + "coze_logid": data.get("detail", {}).get("logid"), + "coze_bot_url": f"https://www.coze.cn/space/7516832346776780836/bot/{self.bot_id}", + "search_hint": f"工作流可通过 {{{{sys_var.user_id}}}} 获取 session_id: {actual_user_id}", + "tried_methods": [ + "user_id (as session_id)", + "parameters.session_id", + "custom_variables.session_id", + "extra_info.session_id", + "config.session_id", + "config.parameters.session_id", + ], + } + + return result + + async def chat_via_workflow( + self, + session_id: str, + message: str, + workflow_id: Optional[str] = None, + ) -> Dict[str, Any]: + """ + 通过工作流 API 进行文字对话(推荐用于文字面试模式) + + 使用 /v1/workflow/run API 直接调用工作流 B, + 可以正确传递 session_id 等必填参数。 + + Args: + session_id: 会话 ID(来自工作流 A) + message: 用户消息 + workflow_id: 工作流 ID(可选,默认使用 INTERVIEW_WORKFLOW_ID) + + Returns: + {"reply": "AI回复"} + """ + import json + + wf_id = workflow_id or self.INTERVIEW_WORKFLOW_ID + + if not wf_id: + raise ValueError("工作流 B 的 ID 未配置,请设置 INTERVIEW_WORKFLOW_ID") + + url = f"{self.base_url}/v1/workflow/run" + + payload = { + "workflow_id": wf_id, + "parameters": { + "session_id": session_id, + "USER_INPUT": message, # 用户输入 + } + } + + logger.info(f"Workflow chat request: {payload}") + + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post(url, json=payload, headers=self.headers) + response.raise_for_status() + + data = response.json() + logger.info(f"Workflow chat response: {data}") + + if data.get("code") == 0: + # 解析工作流输出 + output_str = data.get("data", "") + + try: + if output_str: + output_data = json.loads(output_str) + # 尝试提取回复内容 + if isinstance(output_data, dict): + reply = output_data.get("reply") or output_data.get("output") or output_data.get("data", "") + if isinstance(reply, str): + return {"reply": reply} + elif isinstance(reply, dict): + return {"reply": reply.get("content", str(reply))} + elif isinstance(output_data, str): + return {"reply": output_data} + except json.JSONDecodeError: + # 如果不是 JSON,直接返回原始字符串 + return {"reply": output_str} + + return {"reply": output_str or "工作流执行完成"} + else: + error_msg = data.get("msg", "Unknown error") + raise ValueError(f"Workflow execution failed: {error_msg}") + + async def chat( + self, + message: str, + user_id: str, + conversation_id: Optional[str] = None, + ) -> Dict[str, Any]: + """ + 文本对话 - 使用 /v1/workflows/chat 专用接口 + + 这个接口专门用于 Chatflow,支持: + 1. 通过 parameters 传递自定义参数(如 session_id) + 2. 正确维持问答节点的对话状态 + + Args: + message: 用户消息 + user_id: session_id(面试会话 ID) + conversation_id: 对话 ID(用于多轮对话) + + Returns: + {"reply": "AI回复", "conversation_id": "xxx"} + """ + import asyncio + + # 尝试使用 /v1/workflows/chat 专用接口 + # 工作流 B 的 ID(Chatflow) + workflow_id = "7595077233002840079" + + url = f"{self.base_url}/v1/workflows/chat" + + # 构建 payload + payload = { + "workflow_id": workflow_id, + "user_id": user_id, + "stream": False, + "additional_messages": [ + { + "role": "user", + "content": message, + "content_type": "text", + } + ], + # 通过 parameters 显式传递 session_id + "parameters": { + "session_id": user_id, + }, + } + + # 传递 conversation_id 延续对话 + if conversation_id: + payload["conversation_id"] = conversation_id + + logger.info(f"[Workflows/Chat] request: session_id={user_id}, conv_id={conversation_id}, message={message[:50]}...") + logger.debug(f"[Workflows/Chat] payload: {payload}") + + async with httpx.AsyncClient(timeout=120.0) as client: + try: + response = await client.post(url, json=payload, headers=self.headers) + + # 检查响应状态 + logger.info(f"[Workflows/Chat] status: {response.status_code}, text: {response.text[:200] if response.text else 'empty'}") + + if response.status_code != 200 or not response.text: + logger.warning(f"[Workflows/Chat] failed (status={response.status_code}), falling back to /v3/chat") + return await self._chat_v3(message, user_id, conversation_id) + + data = response.json() + logger.info(f"[Workflows/Chat] response: code={data.get('code')}, msg={data.get('msg', '')}") + + if data.get("code") != 0: + # 如果 /v1/workflows/chat 失败,回退到 /v3/chat + logger.warning(f"[Workflows/Chat] API error, falling back to /v3/chat") + return await self._chat_v3(message, user_id, conversation_id) + except Exception as e: + logger.warning(f"[Workflows/Chat] exception: {e}, falling back to /v3/chat") + return await self._chat_v3(message, user_id, conversation_id) + + # 解析响应 + chat_data = data.get("data", {}) + conv_id = chat_data.get("conversation_id", "") + chat_id = chat_data.get("id", "") + + logger.info(f"[Workflows/Chat] started: conv_id={conv_id}, chat_id={chat_id}") + + # 轮询等待回复完成 + if chat_id and conv_id: + result = await self._wait_for_reply(conv_id, chat_id) + return { + "reply": result.get("reply", ""), + "conversation_id": conv_id, + "debug_info": result.get("debug_info", {}), + } + + return { + "reply": "抱歉,我没有理解您的意思,请再说一次。", + "conversation_id": conversation_id or "", + "debug_info": {"error": "Workflows/Chat API error"}, + } + + async def _chat_v3( + self, + message: str, + user_id: str, + conversation_id: Optional[str] = None, + ) -> Dict[str, Any]: + """ + 备用方法:使用 /v3/chat API + """ + url = f"{self.base_url}/v3/chat" + + # 构建消息内容:嵌入 session_id + if not conversation_id: + content = f"[SESSION:{user_id}]\n{message}" + else: + content = message + + payload = { + "bot_id": self.bot_id, + "user_id": user_id, + "stream": False, + "auto_save_history": True, + "additional_messages": [ + { + "role": "user", + "content": content, + "content_type": "text", + } + ], + } + + if conversation_id: + payload["conversation_id"] = conversation_id + + logger.info(f"[v3/chat] request: user_id={user_id}, conv_id={conversation_id}") + logger.debug(f"[v3/chat] payload: {payload}") + + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post(url, json=payload, headers=self.headers) + response.raise_for_status() + + data = response.json() + logger.info(f"[v3/chat] response: code={data.get('code')}") + + if data.get("code") == 0: + chat_data = data.get("data", {}) + conv_id = chat_data.get("conversation_id", "") + chat_id = chat_data.get("id", "") + + logger.info(f"[v3/chat] started: conv_id={conv_id}, chat_id={chat_id}") + + if chat_id and conv_id: + result = await self._wait_for_reply(conv_id, chat_id) + # 构建 Coze 后台查询链接 + coze_debug_url = f"https://www.coze.cn/space/7516832346776780836/bot/{self.bot_id}" + debug_info = result.get("debug_info", {}) + debug_info.update({ + "conversation_id": conv_id, + "chat_id": chat_id, + "session_id": user_id, + "coze_bot_url": coze_debug_url, + "search_hint": f"在 Coze 后台搜索 conversation_id: {conv_id} 或 user_id: {user_id}", + }) + return { + "reply": result.get("reply", ""), + "conversation_id": conv_id, + "debug_info": debug_info, + } + else: + error_msg = data.get("msg", "Unknown error") + logger.error(f"[v3/chat] API error: {error_msg}") + + return { + "reply": "抱歉,我没有理解您的意思,请再说一次。", + "conversation_id": conversation_id or "", + "debug_info": {"error": "v3/chat API error"}, + } + + async def _wait_for_reply( + self, + conversation_id: str, + chat_id: str, + max_retries: int = 30, + ) -> dict: + """ + 等待 AI 回复完成 + + Returns: + dict: {"reply": str, "debug_info": dict} + """ + import asyncio + import json + + url = f"{self.base_url}/v3/chat/retrieve" + debug_info = { + "status_history": [], + "messages": [], + "raw_responses": [], + } + + for i in range(max_retries): + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + url, + params={ + "conversation_id": conversation_id, + "chat_id": chat_id, + }, + headers=self.headers + ) + + if response.status_code == 200: + data = response.json() + + # 记录原始响应(用于调试) + debug_info["raw_responses"].append({ + "iteration": i, + "data": data + }) + + if data.get("code") == 0: + chat_data = data.get("data", {}) + status = chat_data.get("status", "") + + # 记录状态历史 + status_info = { + "iteration": i, + "status": status, + "required_action": chat_data.get("required_action"), + } + debug_info["status_history"].append(status_info) + + logger.info(f"Chat status [{i}]: {status}") + + # 打印详细的节点信息 + if chat_data.get("required_action"): + action = chat_data.get("required_action") + logger.info(f"🔔 Required action: {json.dumps(action, ensure_ascii=False, indent=2)}") + + if status == "completed": + # 获取消息列表 + messages = await self._get_messages(conversation_id, chat_id) + debug_info["messages"] = messages + + # 找到 AI 的回复 + for msg in reversed(messages): + if msg.get("role") == "assistant" and msg.get("type") == "answer": + return { + "reply": msg.get("content", ""), + "debug_info": debug_info, + } + return { + "reply": "面试官正在思考...", + "debug_info": debug_info, + } + elif status == "failed": + return { + "reply": f"抱歉,出现了一些问题:{chat_data.get('last_error', {}).get('msg', '未知错误')}", + "debug_info": debug_info, + } + elif status == "requires_action": + # 工作流需要用户输入(question 节点或文件上传节点) + messages = await self._get_messages(conversation_id, chat_id) + debug_info["messages"] = messages + + # 打印所有消息用于调试 + logger.info(f"📨 Messages ({len(messages)} total):") + for idx, msg in enumerate(messages): + msg_type = msg.get("type", "unknown") + msg_role = msg.get("role", "unknown") + msg_content = msg.get("content", "")[:200] + logger.info(f" [{idx}] {msg_role}/{msg_type}: {msg_content}") + + # 检查是否有文件上传请求 + for msg in messages: + if msg.get("type") == "tool_call": + logger.info(f"🔧 Tool call detected: {msg.get('content', '')}") + + # 返回 AI 的问题 + for msg in reversed(messages): + if msg.get("role") == "assistant" and msg.get("type") == "answer": + content = msg.get("content", "") + if content: + return { + "reply": content, + "debug_info": debug_info, + } + return { + "reply": "请回答上面的问题...", + "debug_info": debug_info, + } + # 其他状态(in_progress, created)继续等待 + + await asyncio.sleep(1) + + return { + "reply": "响应超时,请重试。", + "debug_info": debug_info, + } + + async def _get_messages( + self, + conversation_id: str, + chat_id: str, + ) -> list: + """ + 获取对话消息列表 + """ + url = f"{self.base_url}/v3/chat/message/list" + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + url, + params={ + "conversation_id": conversation_id, + "chat_id": chat_id, + }, + headers=self.headers + ) + + if response.status_code == 200: + data = response.json() + logger.info(f"Messages response: {data}") + if data.get("code") == 0: + return data.get("data", []) + + return [] + + +# 创建单例 +coze_service = CozeService() diff --git a/backend/check_table.py b/backend/check_table.py new file mode 100644 index 0000000..c0402b2 --- /dev/null +++ b/backend/check_table.py @@ -0,0 +1,40 @@ +"""查询表现有数据的结构""" +import asyncio +import httpx +import json + +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +WORKFLOW_ID = "7597376294612107318" + +async def query(table: str, sql: str): + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + input_json = json.dumps({"table": table, "sql": sql}, ensure_ascii=False) + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{COZE_API_BASE}/v1/workflow/run", + headers=headers, + json={"workflow_id": WORKFLOW_ID, "parameters": {"input": input_json}} + ) + result = response.json() + print(f"\n{table}:") + print(f" code: {result.get('code')}") + if result.get('data'): + data = json.loads(result.get('data', '{}')) + output = data.get('output', []) + if output and len(output) > 0: + print(f" 列名: {list(output[0].keys())}") + else: + print(f" 空数据") + +async def main(): + print("查询表结构...") + await query("assessments", "SELECT * FROM ci_interview_assessments LIMIT 1") + await query("logs", "SELECT * FROM ci_interview_logs LIMIT 1") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/insert_full_mock.py b/backend/insert_full_mock.py new file mode 100644 index 0000000..1193959 --- /dev/null +++ b/backend/insert_full_mock.py @@ -0,0 +1,137 @@ +""" +插入包含完整评分字段的 Mock 数据 +""" +import asyncio +import httpx +import json +import uuid +import random + +COZE_PAT_TOKEN = 'pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT' +WORKFLOW_ID = '7597376294612107318' + +CANDIDATES = [ + {'name': '周雪琴', 'base_score': 88}, + {'name': '陈美华', 'base_score': 75}, + {'name': '林婷婷', 'base_score': 92}, + {'name': '黄丽萍', 'base_score': 65}, + {'name': '吴晓燕', 'base_score': 82}, +] + + +async def execute_sql(sql: str, table: str) -> dict: + headers = { + 'Authorization': f'Bearer {COZE_PAT_TOKEN}', + 'Content-Type': 'application/json' + } + payload = { + 'workflow_id': WORKFLOW_ID, + 'parameters': { + 'input': json.dumps({'table': table, 'sql': sql}, ensure_ascii=False) + } + } + async with httpx.AsyncClient(timeout=30.0) as client: + r = await client.post('https://api.coze.cn/v1/workflow/run', headers=headers, json=payload) + return r.json() + + +def gen_score(base: int, variance: int = 10) -> int: + return max(50, min(100, base + random.randint(-variance, variance))) + + +def get_level(score: int) -> str: + if score >= 85: + return "优秀" + elif score >= 70: + return "良好" + else: + return "一般" + + +def escape_sql(s: str) -> str: + return s.replace("'", "''") + + +async def main(): + print('插入包含完整评分的 Mock 数据...') + print('=' * 50) + + for idx, c in enumerate(CANDIDATES, 1): + session_id = f'MOCK_{uuid.uuid4().hex[:8].upper()}' + base = c['base_score'] + name = c['name'] + + # 生成各维度分数 + skill_score = gen_score(base) + concept_score = gen_score(base) + competency_score = gen_score(base) + avg_score = round((skill_score + concept_score + competency_score) / 3) + + # 生成报告 + skill_level = get_level(skill_score) + concept_level = get_level(concept_score) + competency_level = get_level(competency_score) + + skill_report = f"销售技能评估:该候选人展现出{skill_level}的销售技巧。得分 {skill_score} 分。" + concept_report = f"销售观念评估:候选人对销售工作的认知{concept_level}。得分 {concept_score} 分。" + competency_report = f"综合素质评估:候选人的学习能力和抗压能力{competency_level}。得分 {competency_score} 分。" + + recommend = "强烈推荐录用" if avg_score >= 85 else ("建议录用" if avg_score >= 70 else "建议观察") + + final_report = f"""## 面试评估报告 +**候选人**: {name} +**综合评分**: {avg_score}/100 + +### 各维度评分 +- 销售技能: {skill_score}分 +- 销售观念: {concept_score}分 +- 综合素质: {competency_score}分 + +### 建议 +{recommend} +""" + + # 完整的 INSERT 语句 + sql = f"""INSERT INTO ci_interview_assessments ( + session_id, candidate_name, current_stage, + sales_skill_score, sales_skill_report, + sales_concept_score, sales_concept_report, + competency_score, competency_report, + final_score_report + ) VALUES ( + '{session_id}', '{name}', 'completed', + '{skill_score}', '{escape_sql(skill_report)}', + '{concept_score}', '{escape_sql(concept_report)}', + '{competency_score}', '{escape_sql(competency_report)}', + '{escape_sql(final_report)}' + )""" + + result = await execute_sql(sql, 'assessments') + status = '✅' if result.get('code') == 0 else f"❌ code={result.get('code')}" + print(f'{idx}. {name}: 技能{skill_score} 观念{concept_score} 素质{competency_score} -> {status}') + + # 插入对话记录 + dialogues = [ + ('销售技能', '请描述一次成功的销售经历', '我曾经成功说服一位犹豫的客户购买了我们的高端护肤套装,通过了解她的肤质问题,提供了针对性的方案。'), + ('销售观念', '您认为什么是好的销售', '好的销售是真正帮助客户解决问题,建立长期信任关系,而不是一次性交易。'), + ('综合素质', '遇到困难时您如何应对', '我会先冷静分析问题的原因,制定解决方案,并保持积极的心态去执行。'), + ] + + for j, (stage, q, a) in enumerate(dialogues, 1): + log_id = f'LOG_{session_id}_{j:02d}' + log_sql = f"""INSERT INTO ci_interview_logs ( + log_id, session_id, stage, round, ai_question, user_answer, log_type + ) VALUES ( + '{log_id}', '{session_id}', '{stage}', '{j}', + '{escape_sql(q)}', '{escape_sql(a)}', 'interview' + )""" + await execute_sql(log_sql, 'logs') + + await asyncio.sleep(0.3) + + print('=' * 50) + print('✅ 完成!') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/backend/insert_mock_data.py b/backend/insert_mock_data.py new file mode 100644 index 0000000..9af5788 --- /dev/null +++ b/backend/insert_mock_data.py @@ -0,0 +1,261 @@ +""" +插入模拟面试数据到 Coze 数据库 +通过工作流 C1 执行 SQL INSERT 语句 +""" +import asyncio +import httpx +import json +import random +from datetime import datetime, timedelta + +# Coze 配置 +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +WORKFLOW_ID = "7597376294612107318" # 查询工作流 ID + +# 模拟候选人数据 +MOCK_CANDIDATES = [ + {"name": "张小美", "score_base": 85}, + {"name": "李明辉", "score_base": 78}, + {"name": "王晓丽", "score_base": 92}, + {"name": "陈建国", "score_base": 65}, + {"name": "刘芳芳", "score_base": 72}, + {"name": "赵大伟", "score_base": 58}, + {"name": "孙婷婷", "score_base": 88}, + {"name": "周志强", "score_base": 45}, +] + +# 模拟简历内容 +MOCK_RESUMES = [ + "3年销售经验,曾在某知名医美机构担任销售顾问,业绩连续12个月达成率超过120%。熟悉轻医美产品线,擅长客户关系维护。", + "5年美容行业从业经验,持有高级美容师证书。性格开朗,沟通能力强,善于发现客户需求并提供专业建议。", + "应届毕业生,市场营销专业,在校期间有丰富的社团活动经验。对医美行业充满热情,学习能力强。", + "2年电商销售经验,熟悉线上运营和客户服务。希望转型到线下医美销售领域,有较强的目标感和执行力。", +] + +# 模拟评估报告模板 +SKILL_REPORTS = [ + "候选人展现出扎实的销售基础,在产品介绍和需求挖掘方面表现突出。能够灵活运用 FAB 法则,善于建立客户信任。建议加强异议处理技巧的训练。得分:{score}分", + "销售技能较为全面,尤其在客户沟通和关系建立方面有独到之处。能够准确把握客户心理,提供针对性的解决方案。需要在成交技巧上进一步打磨。得分:{score}分", + "基础销售技能掌握良好,但缺乏实战经验。理论知识扎实,需要更多的实践机会来提升。建议安排老带新的培训模式。得分:{score}分", +] + +CONCEPT_REPORTS = [ + "对销售工作有正确的认知,理解以客户为中心的服务理念。能够平衡业绩压力和客户满意度,具有长期发展的潜力。得分:{score}分", + "销售观念较为成熟,认同医美行业的价值观。注重专业性和诚信度,能够为客户提供真诚的建议。得分:{score}分", + "对销售工作的理解还停留在表面,需要加强对行业和产品的深入学习。建议多参与案例分析和行业培训。得分:{score}分", +] + +COMPETENCY_REPORTS = [ + "综合素质优秀,具备良好的学习能力和抗压能力。工作态度积极,团队协作意识强。是值得培养的优秀人才。得分:{score}分", + "整体素质良好,有较强的责任心和执行力。沟通表达清晰,形象气质佳。建议加强时间管理能力。得分:{score}分", + "基本素质合格,但在主动性和自驱力方面有提升空间。需要更多的激励和引导来激发潜力。得分:{score}分", +] + +MOTIVATION_SUMMARIES = [ + "候选人对轻医美行业表现出浓厚的兴趣,希望在这个领域长期发展。主要动机包括:1)看好行业发展前景;2)认同公司品牌价值;3)期待获得专业成长机会。", + "求职动机明确,主要是希望获得更好的收入和职业发展平台。对销售工作有热情,愿意接受挑战和压力。", + "动机较为单一,主要关注薪资待遇。建议在后续面试中进一步了解其对行业的认知和长期规划。", +] + +RISK_WARNINGS = [ + "", # 无风险 + "", # 无风险 + "候选人在上一份工作中存在频繁跳槽的情况,需要关注稳定性问题。建议在背调中重点核实离职原因。", + "面试过程中发现候选人对公司产品了解不够深入,可能存在准备不充分的情况。建议安排二次面试进一步评估。", + "候选人期望薪资与市场水平存在一定差距,需要在 Offer 阶段进行合理沟通。", +] + +GROWTH_PLANS = [ + "建议培养方向:1)前3个月重点学习产品知识和销售流程;2)3-6个月参与实战,由资深顾问带教;3)6个月后独立负责客户。预计1年内可成长为骨干销售。", + "该候选人具备快速成长的潜力,建议:1)入职即安排系统培训;2)重点培养其客户开发能力;3)可考虑作为储备管理人才培养。", + "成长空间有限,建议先观察3个月试用期表现再做进一步评估。", +] + +REF_CHECK_LISTS = [ + "建议背调问题:\n1. 请确认候选人在贵公司的任职时间和职位\n2. 候选人的主要工作职责是什么?\n3. 您如何评价候选人的销售能力?\n4. 候选人的离职原因是什么?\n5. 如果有机会,您是否愿意再次与其共事?", + "重点背调事项:\n1. 核实业绩数据的真实性\n2. 了解团队协作情况\n3. 确认是否有劳动纠纷\n4. 验证学历和证书的真实性", +] + +# 模拟对话记录 +MOCK_DIALOGUES = [ + { + "stage": "开场", + "ai_question": "您好!欢迎参加我们的 AI 面试。我是您的 AI 面试官,接下来我们将进行一次关于销售岗位的面试。首先,请您简单介绍一下自己。", + "user_answer": "您好,我叫{name},今年28岁。我有3年的销售工作经验,主要在美容行业从事客户服务和销售工作。我性格开朗,善于与人沟通,对轻医美行业非常感兴趣。" + }, + { + "stage": "销售技能", + "ai_question": "很好,感谢您的自我介绍。接下来我想了解一下您的销售经验。请您描述一次成功的销售案例,包括您是如何发现客户需求、如何推荐产品、以及最终如何促成交易的。", + "user_answer": "好的。去年我接待了一位40多岁的女士,她最初只是来咨询皮肤保养。通过聊天我发现她其实对法令纹比较在意。我没有直接推销,而是先帮她做了皮肤检测,用数据说明问题。然后根据她的预算和需求,推荐了适合的玻尿酸填充方案。整个过程我注重建立信任,最终她不仅做了法令纹填充,还办了年卡。" + }, + { + "stage": "销售技能", + "ai_question": "非常好的案例分享。那么当客户对价格有异议时,您通常如何处理?", + "user_answer": "价格异议是很常见的。我通常会先认同客户的感受,然后分析价值而不是价格。比如我会说'您的担心我理解,选择医美确实需要慎重考虑。不过您看,我们使用的是进口正品,由资深医生操作,术后还有专业跟踪服务。很多客户反馈效果能保持1-2年,算下来其实性价比很高。'另外我也会提供分期付款等灵活方案。" + }, + { + "stage": "销售观", + "ai_question": "您如何看待销售工作?您认为一个优秀的销售顾问最重要的品质是什么?", + "user_answer": "我认为销售的本质是帮助客户解决问题,而不是单纯的推销产品。一个优秀的销售顾问首先要专业,要真正了解产品和客户需求;其次要真诚,不能为了业绩误导客户;最后要有服务意识,把每一位客户都当作长期朋友来维护。" + }, + { + "stage": "素质项", + "ai_question": "假设您在工作中遇到了连续三个月业绩不达标的情况,您会如何应对?", + "user_answer": "首先我会分析原因,是市场问题、个人方法问题还是其他因素。然后我会主动向业绩好的同事请教,学习他们的成功经验。同时我会调整自己的工作方法,比如增加客户回访频率、优化话术等。最重要的是保持积极心态,相信通过努力一定能改善。" + }, + { + "stage": "求职动机", + "ai_question": "最后一个问题,您为什么选择我们公司?您对未来的职业发展有什么规划?", + "user_answer": "选择贵公司主要有三个原因:一是贵公司在轻医美领域有很好的口碑和品牌影响力;二是听说公司非常注重员工培训和成长;三是公司的企业文化和我的价值观很匹配。未来我希望能在1-2年内成长为资深销售顾问,3年内有机会带领小团队,为公司创造更大价值。" + }, +] + + +async def execute_sql(sql: str, table: str) -> dict: + """通过工作流执行 SQL""" + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + + payload = { + "workflow_id": WORKFLOW_ID, + "parameters": { + "input": json.dumps({"table": table, "sql": sql}, ensure_ascii=False) + } + } + + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + f"{COZE_API_BASE}/v1/workflow/run", + headers=headers, + json=payload + ) + return response.json() + + +def generate_session_id(name: str, idx: int) -> str: + """生成 session_id""" + timestamp = datetime.now().strftime("%Y%m%d%H%M") + return f"SESS_{timestamp}_{name}_{idx:02d}" + + +def generate_assessment_sql(session_id: str, candidate: dict, idx: int) -> str: + """生成评估记录 INSERT SQL""" + base = candidate["score_base"] + variation = random.randint(-5, 10) + + skill_score = min(100, max(0, base + random.randint(-8, 8))) + concept_score = min(100, max(0, base + random.randint(-5, 10))) + competency_score = min(100, max(0, base + random.randint(-10, 5))) + + skill_report = random.choice(SKILL_REPORTS).format(score=skill_score) + concept_report = random.choice(CONCEPT_REPORTS).format(score=concept_score) + competency_report = random.choice(COMPETENCY_REPORTS).format(score=competency_score) + + motivation = random.choice(MOTIVATION_SUMMARIES) + risk = random.choice(RISK_WARNINGS) + growth = random.choice(GROWTH_PLANS) + ref_check = random.choice(REF_CHECK_LISTS) + resume = random.choice(MOCK_RESUMES) + + # 转义单引号 + def escape(s): + return s.replace("'", "''") if s else "" + + sql = f"""INSERT INTO ci_interview_assessments ( + session_id, candidate_name, current_stage, + sales_skill_score, sales_skill_report, + sales_concept_score, sales_concept_report, + competency_score, competency_report, + motivation_summary, risk_warning, growth_plan, ref_check_list, resume_text + ) VALUES ( + '{session_id}', '{candidate["name"]}', '10', + '{skill_score}', '{escape(skill_report)}', + '{concept_score}', '{escape(concept_report)}', + '{competency_score}', '{escape(competency_report)}', + '{escape(motivation)}', '{escape(risk)}', '{escape(growth)}', '{escape(ref_check)}', '{escape(resume)}' + )""" + + return sql + + +def generate_log_sql(session_id: str, candidate_name: str, round_num: int, dialogue: dict, idx: int) -> str: + """生成对话记录 INSERT SQL""" + import uuid + + def escape(s): + return s.replace("'", "''").replace("{name}", candidate_name) if s else "" + + # 生成唯一的 log_id + log_id = f"LOG_{idx:02d}_{round_num:02d}_{uuid.uuid4().hex[:8]}" + + sql = f"""INSERT INTO ci_interview_logs ( + log_id, session_id, stage, round, ai_question, user_answer, log_type + ) VALUES ( + '{log_id}', '{session_id}', '{dialogue["stage"]}', '{round_num}', + '{escape(dialogue["ai_question"])}', '{escape(dialogue["user_answer"])}', 'interview' + )""" + + return sql + + +async def main(): + print("=" * 60) + print("开始插入模拟面试数据...") + print("=" * 60) + + success_count = 0 + error_count = 0 + + for idx, candidate in enumerate(MOCK_CANDIDATES): + session_id = generate_session_id(candidate["name"], idx) + print(f"\n[{idx + 1}/{len(MOCK_CANDIDATES)}] 处理候选人: {candidate['name']}") + print(f" Session ID: {session_id}") + + # 1. 插入评估记录 + try: + assessment_sql = generate_assessment_sql(session_id, candidate, idx) + print(f" 插入评估记录...") + result = await execute_sql(assessment_sql, "assessments") + + if result.get("code") == 0: + print(f" ✓ 评估记录插入成功") + success_count += 1 + else: + print(f" ✗ 评估记录插入失败: {result}") + error_count += 1 + except Exception as e: + print(f" ✗ 评估记录插入异常: {e}") + error_count += 1 + + # 2. 插入对话记录 + for round_num, dialogue in enumerate(MOCK_DIALOGUES, 1): + try: + log_sql = generate_log_sql(session_id, candidate["name"], round_num, dialogue, idx) + print(f" 插入对话记录 (第{round_num}轮)...") + result = await execute_sql(log_sql, "logs") + + if result.get("code") == 0: + print(f" ✓ 对话记录 {round_num} 插入成功") + success_count += 1 + else: + print(f" ✗ 对话记录 {round_num} 插入失败: {result}") + error_count += 1 + except Exception as e: + print(f" ✗ 对话记录 {round_num} 插入异常: {e}") + error_count += 1 + + # 等待一下避免请求过快 + await asyncio.sleep(1) + + print("\n" + "=" * 60) + print(f"数据插入完成!") + print(f"成功: {success_count} 条") + print(f"失败: {error_count} 条") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/insert_mock_full.py b/backend/insert_mock_full.py new file mode 100644 index 0000000..7c37904 --- /dev/null +++ b/backend/insert_mock_full.py @@ -0,0 +1,123 @@ +""" +插入完整模拟面试数据(8 条候选人 + 对话记录) +""" +import asyncio +import httpx +import json +import uuid + +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +WORKFLOW_ID = "7597376294612107318" + +# 候选人数据 +CANDIDATES = [ + {"name": "张小美", "skill": 85, "concept": 82, "comp": 88, "status": "completed", "risk": ""}, + {"name": "李明辉", "skill": 72, "concept": 70, "comp": 75, "status": "completed", "risk": ""}, + {"name": "王晓丽", "skill": 92, "concept": 90, "comp": 95, "status": "completed", "risk": ""}, + {"name": "陈建国", "skill": 65, "concept": 60, "comp": 68, "status": "completed", "risk": "面试中表现紧张,需关注"}, + {"name": "刘芳芳", "skill": 78, "concept": 75, "comp": 80, "status": "completed", "risk": ""}, + {"name": "赵大伟", "skill": 55, "concept": 52, "comp": 58, "status": "completed", "risk": "销售经验不足,需重点培训"}, + {"name": "孙婷婷", "skill": 88, "concept": 85, "comp": 90, "status": "in_progress", "risk": ""}, + {"name": "周志强", "skill": 45, "concept": 42, "comp": 48, "status": "completed", "risk": "不建议录用,综合能力较弱"}, +] + +# 简历模板 +RESUMES = [ + "3年销售经验,曾在某知名医美机构担任销售顾问,业绩连续12个月达成率超过120%。", + "5年美容行业从业经验,持有高级美容师证书。性格开朗,沟通能力强。", + "应届毕业生,市场营销专业,在校期间有丰富的社团活动经验。对医美行业充满热情。", + "2年电商销售经验,熟悉线上运营和客户服务。希望转型到线下医美销售领域。", +] + +# 对话模板 +DIALOGUES = [ + ("开场", "你好,请先做个自我介绍", "面试官您好,我是{name},很高兴参加这次面试。我之前有销售相关经验,对轻医美行业很感兴趣。"), + ("技能", "请介绍你常用的销售技巧", "我主要采用顾问式销售方法,先通过沟通了解客户的真实需求,然后针对性地推荐适合的产品或服务。"), + ("技能", "遇到客户异议时如何处理", "首先我会认真倾听客户的顾虑,表示理解。然后用专业知识解答疑问,必要时提供案例或数据支持。"), + ("观念", "你如何看待销售这份工作", "我认为销售不仅是卖产品,更是帮助客户解决问题。好的销售是客户的顾问和朋友。"), + ("素质", "你的职业规划是什么", "短期希望成为一名优秀的销售顾问,中期目标是带领团队,长期希望在医美行业有深入发展。"), +] + +async def execute_sql(sql: str, table: str) -> dict: + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + input_json = json.dumps({"table": table, "sql": sql}, ensure_ascii=False) + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{COZE_API_BASE}/v1/workflow/run", + headers=headers, + json={"workflow_id": WORKFLOW_ID, "parameters": {"input": input_json}} + ) + return response.json() + +def escape(s): + """转义 SQL 特殊字符""" + return s.replace("'", "''") if s else "" + +async def main(): + print("=" * 60) + print("插入完整模拟数据 (8 条候选人)") + print("=" * 60) + + for idx, c in enumerate(CANDIDATES): + session_id = f"MOCK_{uuid.uuid4().hex[:8].upper()}" + resume = RESUMES[idx % len(RESUMES)] + + # 生成报告 + skill_report = f"销售技能评估:候选人在产品介绍和需求挖掘方面{'表现出色' if c['skill'] >= 80 else '有待提升'}。得分:{c['skill']}分" + concept_report = f"销售观念评估:对销售工作{'有正确认知' if c['concept'] >= 70 else '认识不够深入'}。得分:{c['concept']}分" + comp_report = f"综合素质评估:学习能力{'强' if c['comp'] >= 80 else '一般'},抗压能力{'好' if c['comp'] >= 75 else '需加强'}。得分:{c['comp']}分" + + # 1. 插入 Assessment + sql1 = f"""INSERT INTO ci_interview_assessments ( + session_id, candidate_name, resume_text, current_stage, + sales_skill_score, sales_concept_score, competency_score, + sales_skill_report, sales_concept_report, competency_report, + motivation_summary, risk_warning, growth_plan, ref_check_list + ) VALUES ( + '{session_id}', '{c["name"]}', '{escape(resume)}', '{c["status"]}', + '{c["skill"]}', '{c["concept"]}', '{c["comp"]}', + '{escape(skill_report)}', '{escape(concept_report)}', '{escape(comp_report)}', + '候选人对轻医美行业表现出兴趣,希望长期发展。', + '{escape(c["risk"])}', + '建议入职后进行系统培训,由资深顾问带教。', + '建议背调:确认任职时间、业绩数据、离职原因。' + )""" + + print(f"\n[{idx+1}/8] {c['name']} (session: {session_id})") + result = await execute_sql(sql1, "assessments") + + if result.get('code') != 0: + print(f" ❌ Assessment 失败: {result.get('msg')}") + continue + + print(f" ✅ Assessment: {c['skill']}/{c['concept']}/{c['comp']}") + + # 2. 插入对话记录 + for d_idx, (stage, q, a) in enumerate(DIALOGUES, 1): + log_id = f"LOG_{session_id}_{d_idx:02d}" + answer = a.replace("{name}", c["name"]) + + sql2 = f"""INSERT INTO ci_interview_logs ( + log_id, session_id, stage, round, ai_question, user_answer, log_type + ) VALUES ( + '{log_id}', '{session_id}', '{stage}', '{d_idx}', + '{escape(q)}', '{escape(answer)}', 'interview' + )""" + + result = await execute_sql(sql2, "logs") + if result.get('code') != 0: + print(f" ⚠️ 对话{d_idx}: {result.get('msg')[:30]}") + + print(f" ✅ 对话: {len(DIALOGUES)} 条") + + print("\n" + "=" * 60) + print("✅ 完成!共插入 8 条候选人 + 40 条对话记录") + print("=" * 60) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/insert_mock_simple.py b/backend/insert_mock_simple.py new file mode 100644 index 0000000..e54df2d --- /dev/null +++ b/backend/insert_mock_simple.py @@ -0,0 +1,100 @@ +""" +简化版 Mock 数据插入脚本 - 只插入2条记录 +""" +import asyncio +import httpx +import json +import uuid +import random + +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +WORKFLOW_ID = "7597376294612107318" + +# 模拟候选人 +CANDIDATES = [ + {"name": "张小美", "score": 85, "status": "completed", "risk": "low"}, + {"name": "李明辉", "score": 72, "status": "completed", "risk": "medium"}, +] + +# 简化对话 +DIALOGUES = [ + {"stage": "专业技能", "q": "请介绍您的护肤专业知识", "a": "我在医美行业有3年经验..."}, + {"stage": "沟通能力", "q": "遇到客户投诉如何处理", "a": "首先我会认真倾听客户的问题..."}, +] + + +async def execute_sql(sql: str, table: str) -> dict: + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + payload = { + "workflow_id": WORKFLOW_ID, + "parameters": {"input": json.dumps({"table": table, "sql": sql}, ensure_ascii=False)} + } + async with httpx.AsyncClient(timeout=30.0) as client: + r = await client.post(f"{COZE_API_BASE}/v1/workflow/run", headers=headers, json=payload) + return r.json() + + +async def main(): + print("=" * 50) + print("插入 Mock 数据") + print("=" * 50) + + for idx, c in enumerate(CANDIDATES, 1): + session_id = f"MOCK_{uuid.uuid4().hex[:8].upper()}" + + # 生成评估报告 + report = f"""## 面试评估报告 + +**候选人**: {c['name']} +**综合评分**: {c['score']}/100 + +### 各维度评分 +- 专业技能: {random.randint(70, 95)}分 +- 沟通能力: {random.randint(70, 95)}分 +- 服务意识: {random.randint(70, 95)}分 +- 学习能力: {random.randint(70, 95)}分 + +### 风险评估 +风险等级: {c['risk']} + +### 总结 +该候选人整体表现{"良好" if c['score'] >= 80 else "一般"},建议{"录用" if c['score'] >= 75 else "观察"}。 +""" + + # 1. 插入评估记录 + sql1 = f"""INSERT INTO ci_interview_assessments ( + session_id, candidate_name, assessment_report, current_stage + ) VALUES ( + '{session_id}', '{c["name"]}', '{report.replace("'", "''")}', '{c["status"]}' + )""" + + print(f"\n📝 插入候选人 {idx}: {c['name']}") + result = await execute_sql(sql1, "assessments") + print(f" 评估记录: code={result.get('code')}") + + # 2. 插入对话记录 + for j, d in enumerate(DIALOGUES, 1): + log_id = f"LOG_{session_id}_{j:02d}" + sql2 = f"""INSERT INTO ci_interview_logs ( + log_id, session_id, stage, round, ai_question, user_answer, log_type + ) VALUES ( + '{log_id}', '{session_id}', '{d["stage"]}', '{j}', + '{d["q"].replace("'", "''")}', '{d["a"].replace("'", "''")}', 'interview' + )""" + + result = await execute_sql(sql2, "logs") + print(f" 对话{j}: code={result.get('code')}") + + await asyncio.sleep(0.5) + + print("\n" + "=" * 50) + print("✅ 完成!") + print("=" * 50) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..ca260b4 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,73 @@ +""" +AI Interview Backend - FastAPI 应用入口 +""" +import os +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from loguru import logger + +from app.config import settings +from app.routers import candidate, room, chat, init, files, admin + +# 创建 FastAPI 应用 +app = FastAPI( + title="AI Interview API", + description="AI 语音面试系统后端 API", + version="0.1.0", + docs_url="/docs" if settings.DEBUG else None, + redoc_url="/redoc" if settings.DEBUG else None, +) + +# 配置 CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由(用户端) +app.include_router(candidate.router, prefix="/api", tags=["候选人"]) +app.include_router(room.router, prefix="/api", tags=["房间"]) +app.include_router(chat.router, prefix="/api", tags=["对话"]) +app.include_router(init.router, prefix="/api", tags=["初始化"]) +app.include_router(files.router, prefix="/api", tags=["文件"]) + +# 管理后台路由 +app.include_router(admin.router, tags=["管理后台"]) + + +@app.get("/health") +async def health_check(): + """健康检查""" + return {"status": "ok", "version": "0.1.0"} + + +@app.on_event("startup") +async def startup_event(): + """应用启动事件""" + logger.info("AI Interview Backend starting...") + logger.info(f"Debug mode: {settings.DEBUG}") + logger.info(f"Coze Bot ID: {settings.COZE_BOT_ID}") + logger.info(f"Tunnel URL: {settings.TUNNEL_URL or settings.NGROK_URL}") + + # 创建上传目录 + os.makedirs(settings.UPLOAD_DIR, exist_ok=True) + logger.info(f"Upload directory: {settings.UPLOAD_DIR}") + + +@app.on_event("shutdown") +async def shutdown_event(): + """应用关闭事件""" + logger.info("AI Interview Backend shutting down...") + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host="0.0.0.0", + port=settings.API_PORT, + reload=settings.DEBUG, + ) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f26c9fa --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,26 @@ +# FastAPI 框架 +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 + +# HTTP 客户端 +httpx>=0.26.0 + +# 文件上传 +python-multipart>=0.0.6 + +# 数据验证 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 + +# 环境变量 +python-dotenv>=1.0.0 + +# CORS +starlette>=0.35.0 + +# 日志 +loguru>=0.7.2 + +# PDF 生成(可选) +# weasyprint>=60.0 +# reportlab>=4.0.0 diff --git a/backend/test_coze.py b/backend/test_coze.py new file mode 100644 index 0000000..d4a16e4 --- /dev/null +++ b/backend/test_coze.py @@ -0,0 +1,167 @@ +""" +Coze API 配置测试脚本 +运行: python test_coze.py +""" +import asyncio +import httpx + +# 配置 +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +COZE_BOT_ID = "7595113005181386792" +COZE_DATABASE_ID = "7595077053909712922" + + +async def test_bot_info(): + """测试 Bot 信息""" + print("\n" + "=" * 50) + print("1. 测试 Bot 信息") + print("=" * 50) + + url = f"{COZE_API_BASE}/v1/bot/get_online_info" + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json", + } + + async with httpx.AsyncClient(timeout=30.0) as client: + try: + response = await client.get( + url, + params={"bot_id": COZE_BOT_ID}, + headers=headers + ) + data = response.json() + print(f"Status: {response.status_code}") + print(f"Response: {data}") + + if data.get("code") == 0: + bot_info = data.get("data", {}) + print(f"\n✅ Bot 名称: {bot_info.get('name', 'N/A')}") + print(f"✅ Bot ID: {COZE_BOT_ID}") + return True + else: + print(f"\n❌ 错误: {data.get('msg', 'Unknown error')}") + return False + except Exception as e: + print(f"❌ 请求失败: {e}") + return False + + +async def test_database(): + """测试数据库查询""" + print("\n" + "=" * 50) + print("2. 测试数据库连接和字段结构") + print("=" * 50) + + url = f"{COZE_API_BASE}/v1/database/query" + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json", + } + payload = { + "database_id": COZE_DATABASE_ID, + "page": 1, + "page_size": 5, + } + + async with httpx.AsyncClient(timeout=30.0) as client: + try: + response = await client.post(url, json=payload, headers=headers) + data = response.json() + print(f"Status: {response.status_code}") + + if data.get("code") == 0: + db_data = data.get("data", {}) + records = db_data.get("records", []) + total = db_data.get("total", 0) + + print(f"\n✅ 数据库连接成功!") + print(f"✅ 数据库 ID: {COZE_DATABASE_ID}") + print(f"✅ 总记录数: {total}") + + if records: + print(f"\n📋 数据库字段列表:") + first_record = records[0] + for key in first_record.keys(): + value = first_record[key] + value_preview = str(value)[:50] + "..." if len(str(value)) > 50 else value + print(f" - {key}: {value_preview}") + else: + print("\n⚠️ 数据库为空,无法显示字段结构") + print(" 这是正常的,首次面试后会有数据") + + return True + else: + print(f"\n❌ 错误: {data.get('msg', 'Unknown error')}") + print(f" 完整响应: {data}") + return False + except Exception as e: + print(f"❌ 请求失败: {e}") + return False + + +async def test_audio_room(): + """测试语音房间创建(不实际创建,只检查 API 可用性)""" + print("\n" + "=" * 50) + print("3. 测试语音房间 API(模拟请求)") + print("=" * 50) + + # 我们只测试 API 是否可访问,不实际创建房间 + url = f"{COZE_API_BASE}/v1/audio/rooms" + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json", + } + + # 使用测试参数 + payload = { + "bot_id": COZE_BOT_ID, + "user_id": "test_user_001", + } + + print(f"API 端点: {url}") + print(f"请求参数: {payload}") + print("\n⚠️ 跳过实际创建房间测试(避免产生费用)") + print("✅ API 配置看起来正确") + + return True + + +async def main(): + print("\n🔍 Coze API 配置测试") + print("=" * 50) + print(f"API Base: {COZE_API_BASE}") + print(f"Bot ID: {COZE_BOT_ID}") + print(f"Database ID: {COZE_DATABASE_ID}") + print(f"Token: {COZE_PAT_TOKEN[:20]}...") + + results = [] + + # 测试 Bot + results.append(await test_bot_info()) + + # 测试数据库 + results.append(await test_database()) + + # 测试语音房间 + results.append(await test_audio_room()) + + # 汇总 + print("\n" + "=" * 50) + print("📊 测试结果汇总") + print("=" * 50) + + tests = ["Bot 信息", "数据库连接", "语音房间 API"] + for i, (test_name, result) in enumerate(zip(tests, results)): + status = "✅ 通过" if result else "❌ 失败" + print(f"{i + 1}. {test_name}: {status}") + + if all(results): + print("\n🎉 所有测试通过!配置正确!") + else: + print("\n⚠️ 部分测试失败,请检查配置") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/test_insert_log.py b/backend/test_insert_log.py new file mode 100644 index 0000000..e88f154 --- /dev/null +++ b/backend/test_insert_log.py @@ -0,0 +1,78 @@ +""" +测试插入单条对话记录 +""" +import asyncio +import httpx +import json + +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +WORKFLOW_ID = "7597376294612107318" + +async def execute_sql(sql: str, table: str) -> dict: + """通过工作流执行 SQL""" + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + + input_json = json.dumps({"table": table, "sql": sql}, ensure_ascii=False) + print(f"\n📤 发送请求:") + print(f" table: {table}") + print(f" sql: {sql[:100]}...") + print(f" input: {input_json[:200]}...") + + payload = { + "workflow_id": WORKFLOW_ID, + "parameters": { + "input": input_json + } + } + + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + f"{COZE_API_BASE}/v1/workflow/run", + headers=headers, + json=payload + ) + result = response.json() + print(f"\n📥 响应:") + print(f" code: {result.get('code')}") + print(f" msg: {result.get('msg')}") + if result.get('data'): + print(f" data: {result.get('data')[:200]}...") + if result.get('debug_url'): + print(f" debug: {result.get('debug_url')}") + return result + + +async def main(): + print("=" * 60) + print("测试 INSERT 到 ci_interview_logs") + print("=" * 60) + + # 测试 1: 简单的 INSERT + sql1 = """INSERT INTO ci_interview_logs (session_id, stage, round, ai_question, user_answer, log_type) VALUES ('TEST_001', '测试', '1', '测试问题', '测试回答', 'test')""" + + print("\n\n🧪 测试 1: 简单 INSERT") + result = await execute_sql(sql1, "logs") + + # 测试 2: 查询刚插入的数据 + sql2 = """SELECT * FROM ci_interview_logs WHERE session_id = 'TEST_001' LIMIT 5""" + + print("\n\n🧪 测试 2: 查询刚插入的数据") + result = await execute_sql(sql2, "logs") + + # 测试 3: 删除测试数据 + sql3 = """DELETE FROM ci_interview_logs WHERE session_id = 'TEST_001'""" + + print("\n\n🧪 测试 3: 删除测试数据") + result = await execute_sql(sql3, "logs") + + print("\n" + "=" * 60) + print("测试完成") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/test_single_insert.py b/backend/test_single_insert.py new file mode 100644 index 0000000..98d70f6 --- /dev/null +++ b/backend/test_single_insert.py @@ -0,0 +1,38 @@ +"""测试单条 INSERT""" +import asyncio +import httpx +import json +import uuid + +COZE_API_BASE = "https://api.coze.cn" +COZE_PAT_TOKEN = "pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT" +WORKFLOW_ID = "7597376294612107318" + +async def main(): + log_id = f"LOG_TEST_{uuid.uuid4().hex[:8]}" + + sql = f"""INSERT INTO ci_interview_logs (log_id, session_id, stage, round, ai_question, user_answer, log_type) VALUES ('{log_id}', 'TEST_001', '测试', '1', '测试问题', '测试回答', 'test')""" + + input_json = json.dumps({"table": "logs", "sql": sql}, ensure_ascii=False) + + print(f"log_id: {log_id}") + print(f"sql: {sql[:80]}...") + + headers = { + "Authorization": f"Bearer {COZE_PAT_TOKEN}", + "Content-Type": "application/json" + } + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{COZE_API_BASE}/v1/workflow/run", + headers=headers, + json={"workflow_id": WORKFLOW_ID, "parameters": {"input": input_json}} + ) + result = response.json() + print(f"code: {result.get('code')}") + print(f"msg: {result.get('msg')}") + print(f"data: {result.get('data', '')[:200]}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/test_workflow_c.py b/backend/test_workflow_c.py new file mode 100644 index 0000000..f166a0e --- /dev/null +++ b/backend/test_workflow_c.py @@ -0,0 +1,89 @@ +""" +测试工作流 C - 通用 SQL 查询(JSON 格式输入) +""" +import asyncio +import httpx +import os +import json +from dotenv import load_dotenv + +load_dotenv() + +PAT_TOKEN = os.getenv("COZE_PAT_TOKEN") +WORKFLOW_ID = "7597376294612107318" + +async def test_query(table: str, sql: str): + url = "https://api.coze.cn/v1/workflow/run" + headers = { + "Authorization": f"Bearer {PAT_TOKEN}", + "Content-Type": "application/json" + } + + # JSON 格式输入 + input_data = json.dumps({ + "table": table, + "sql": sql + }, ensure_ascii=False) + + payload = { + "workflow_id": WORKFLOW_ID, + "parameters": { + "input": input_data + } + } + + print(f"Table: {table}") + print(f"SQL: {sql[:80]}...") + + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post(url, json=payload, headers=headers) + print(f"Status: {response.status_code}") + + data = response.json() + + if data.get("code") == 0: + print(f"✅ 成功!") + result_str = data.get("data", "") + if result_str: + try: + inner_data = json.loads(result_str) + if isinstance(inner_data, list): + print(f"返回 {len(inner_data)} 条记录") + if inner_data: + print(json.dumps(inner_data[0], indent=2, ensure_ascii=False)) + else: + print(json.dumps(inner_data, indent=2, ensure_ascii=False)) + except: + print(f"Raw: {result_str[:300]}") + else: + print(f"❌ 失败: {data.get('msg')}") + if data.get("debug_url"): + print(f"Debug: {data.get('debug_url')}") + +async def main(): + print("=" * 60) + print("测试 1: 查询面试评估列表 (assessments)") + print("=" * 60) + await test_query( + "assessments", + "SELECT session_id, candidate_name, bstudio_create_time FROM ci_interview_assessments ORDER BY bstudio_create_time DESC LIMIT 5" + ) + + print("\n" + "=" * 60) + print("测试 2: 查询对话日志 (logs)") + print("=" * 60) + await test_query( + "logs", + "SELECT log_id, session_id, stage, ai_question, user_answer FROM ci_interview_logs LIMIT 3" + ) + + print("\n" + "=" * 60) + print("测试 3: 查询业务配置 (config)") + print("=" * 60) + await test_query( + "config", + "SELECT config_id, config_type, item_name FROM ci_business_config LIMIT 3" + ) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/uploads/resume_06efd8fbdf4e.pdf b/backend/uploads/resume_06efd8fbdf4e.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_06efd8fbdf4e.pdf differ diff --git a/backend/uploads/resume_0c832769e6e1.pdf b/backend/uploads/resume_0c832769e6e1.pdf new file mode 100644 index 0000000..35ea843 Binary files /dev/null and b/backend/uploads/resume_0c832769e6e1.pdf differ diff --git a/backend/uploads/resume_12d917aff494.pdf b/backend/uploads/resume_12d917aff494.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_12d917aff494.pdf differ diff --git a/backend/uploads/resume_314534591e3a.pdf b/backend/uploads/resume_314534591e3a.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_314534591e3a.pdf differ diff --git a/backend/uploads/resume_54c858df7b5c.pdf b/backend/uploads/resume_54c858df7b5c.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_54c858df7b5c.pdf differ diff --git a/backend/uploads/resume_5649e33085e7.pdf b/backend/uploads/resume_5649e33085e7.pdf new file mode 100644 index 0000000..35ea843 Binary files /dev/null and b/backend/uploads/resume_5649e33085e7.pdf differ diff --git a/backend/uploads/resume_822a417ddd93.pdf b/backend/uploads/resume_822a417ddd93.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_822a417ddd93.pdf differ diff --git a/backend/uploads/resume_9776de07e4a8.pdf b/backend/uploads/resume_9776de07e4a8.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_9776de07e4a8.pdf differ diff --git a/backend/uploads/resume_9c8d14dffe57.pdf b/backend/uploads/resume_9c8d14dffe57.pdf new file mode 100644 index 0000000..35ea843 Binary files /dev/null and b/backend/uploads/resume_9c8d14dffe57.pdf differ diff --git a/backend/uploads/resume_a1046f432e25.pdf b/backend/uploads/resume_a1046f432e25.pdf new file mode 100644 index 0000000..35ea843 Binary files /dev/null and b/backend/uploads/resume_a1046f432e25.pdf differ diff --git a/backend/uploads/resume_c413896bb575.pdf b/backend/uploads/resume_c413896bb575.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_c413896bb575.pdf differ diff --git a/backend/uploads/resume_c66c17984409.pdf b/backend/uploads/resume_c66c17984409.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_c66c17984409.pdf differ diff --git a/backend/uploads/resume_d5099390d096.pdf b/backend/uploads/resume_d5099390d096.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_d5099390d096.pdf differ diff --git a/backend/uploads/resume_ea022e674281.pdf b/backend/uploads/resume_ea022e674281.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_ea022e674281.pdf differ diff --git a/backend/uploads/resume_eeec534465a4.pdf b/backend/uploads/resume_eeec534465a4.pdf new file mode 100644 index 0000000..e636b53 Binary files /dev/null and b/backend/uploads/resume_eeec534465a4.pdf differ diff --git a/backend/uploads/resume_fe9abe3de07a.pdf b/backend/uploads/resume_fe9abe3de07a.pdf new file mode 100644 index 0000000..35ea843 Binary files /dev/null and b/backend/uploads/resume_fe9abe3de07a.pdf differ diff --git a/coze-workflows/Chatflow-Consultant_interviewer-draft-268/MANIFEST.yml b/coze-workflows/Chatflow-Consultant_interviewer-draft-268/MANIFEST.yml new file mode 100644 index 0000000..6cc85f1 --- /dev/null +++ b/coze-workflows/Chatflow-Consultant_interviewer-draft-268/MANIFEST.yml @@ -0,0 +1,14 @@ +type: Workflow +version: 1.0.0 +main: + id: 7595077233002840079 + name: Consultant_interviewer + desc: |- + “咨询师面试官” 是专注轻医美咨询师初试的智能人力资源 AI,以精准评估、高效筛选为核心,助力企业锁定适配人才。​ + 它围绕销售观、销售技巧、知识储备与职业素质四大维度,运用 STAR 面试法深入挖掘候选人行为细节,配合性格剖面报告与行为提问话术库,剖析其性格特质与领导力潜力,同步生成任用风险提示与决策矩阵图,规避用人风险。针对轻医美行业特性,重点评估 “情绪稳定性”,结合德锐 9 大致命点,从技能、知识、价值观等方面综合评分。​ + 面试结束后,Agent 输出精准面试评分、背调问题清单,还定制未来 5 年培养规划,实现人才的可持续发展。后台的人才画像卡支持个性化生成,适配不同企业对素质项的筛选需求。凭借专业、智能的特性,“咨询师面试官” Agent 成为企业构建轻医美人才梯队的有力工具。 + icon: plugin_icon/workflow.png + version: "" + flowMode: 3 + commitId: "" +sub: [] diff --git a/coze-workflows/Chatflow-Consultant_interviewer-draft-268/workflow/Consultant_interviewer-draft.yaml b/coze-workflows/Chatflow-Consultant_interviewer-draft-268/workflow/Consultant_interviewer-draft.yaml new file mode 100644 index 0000000..2654d6b --- /dev/null +++ b/coze-workflows/Chatflow-Consultant_interviewer-draft-268/workflow/Consultant_interviewer-draft.yaml @@ -0,0 +1,5562 @@ +schema_version: 1.0.0 +name: Consultant_interviewer +id: 7595077233002840079 +description: "“咨询师面试官” 是专注轻医美咨询师初试的智能人力资源 AI,以精准评估、高效筛选为核心,助力企业锁定适配人才。​\n它围绕销售观、销售技巧、知识储备与职业素质四大维度,运用 STAR 面试法深入挖掘候选人行为细节,配合性格剖面报告与行为提问话术库,剖析其性格特质与领导力潜力,同步生成任用风险提示与决策矩阵图,规避用人风险。针对轻医美行业特性,重点评估 “情绪稳定性”,结合德锐 9 大致命点,从技能、知识、价值观等方面综合评分。​\n面试结束后,Agent 输出精准面试评分、背调问题清单,还定制未来 5 年培养规划,实现人才的可持续发展。后台的人才画像卡支持个性化生成,适配不同企业对素质项的筛选需求。凭借专业、智能的特性,“咨询师面试官” Agent 成为企业构建轻医美人才梯队的有力工具。" +mode: chatflow +icon: plugin_icon/workflow.png +nodes: + - id: "100001" + type: start + title: 开始 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg + description: "工作流的起始节点,用于设定启动工作流需要的信息" + position: + x: 180 + y: 753.5999999999998 + parameters: + node_outputs: + CONVERSATION_NAME: + type: string + value: null + default_value: Default + description: 本次请求绑定的会话,会自动写入消息、会从该会话读对话历史。 + USER_INPUT: + type: string + value: null + default_value: 开始面试 + - id: "900001" + type: end + title: 结束 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg + description: "工作流的最终节点,用于返回工作流运行后的结果信息" + position: + x: 28320 + y: 660.0249999999999 + parameters: + content: + type: string + value: + content: 感谢您的面试 + type: literal + streamingOutput: true + terminatePlan: useAnswerContent + - id: "178284" + type: loop + title: 销售技能提问循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Loop-v2.jpg + description: "用于通过设定循环次数和逻辑,重复执行一系列任务" + position: + x: 9020 + y: 294.3999999999998 + canvas_position: + x: 7920 + y: 585.4999999999998 + parameters: + loopCount: + type: integer + value: + content: 1 + rawMeta: + type: 2 + type: literal + loopType: count + variableParameters: [] + nodes: + - id: "138699" + type: llm + title: 销售技巧提问 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 180 + y: 125.62500000000001 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: 简历中有医美从业经验 + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "20" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一个专业的面试问题生成Agent,专注于根据用户简历中的行业经历,针对销售技巧提出单一、高质量的问题。\n\n## 技能\n### 技能1: 判断行业\n1. 仔细分析简历内容,若简历中出现“轻医美”“医美咨询”“美容顾问”等关键词,则使用轻医美场景进行提问。\n2. 若简历中未出现上述关键词,则提取简历中最近的行业关键词(如“教育销售”“房产中介”)。\n\n### 技能2: 提出问题\n1. 每次仅提出1个问题,围绕“需求挖掘、异议处理、成交促成、客户维护”四大销售能力之一进行提问。\n2. 问题需包含具体行业场景细节(如轻医美示例:“当客户担心热玛吉疼痛时,你会如何说服?”)。\n3. 使用行为面试法(STAR框架)进行提问,例如:“请举例说明你在XX行业如何解决客户对价格的抗拒”。\n\n### 技能3: 胜任力模型拆解\n将轻医美咨询师的核心销售能力拆解为四大维度:\n客户需求洞察(挖掘痛点、建立信任)\n专业说服能力(产品/技术知识、个性化方案设计)\n转化策略(异议处理、临门一脚技巧)\n客户维护(促进再到店、促进老带新)\n\n\n## 限制\n- 仅围绕根据简历行业经历提出销售技巧相关问题,不回答其他无关话题。\n- 问题输出需符合上述技能要求的格式。 " + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "128341" + type: llm + title: 追问 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 1100 + y: 0 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: |- + 上一轮你提出的问题:{{output}} + 上一轮问题用户的回答:{{USER_RESPONSE}} + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "20" + - name: systemPrompt + input: + type: string + value: |- + # 角色 + 你是一位资深且专业的咨询师面试官,精通OBER法则,并能熟练运用该法则设计行为提问话术。你具备出色的倾听能力,能够依据候选人的上一个回答展开针对性追问,从而全面、深入地剖析候选人的能力与素质。 + + ## 技能 + ### 技能1: 针对性追问 + 1. 认真倾听候选人的回答,基于回答内容精准进行针对性追问。 + 2. 严格遵循OBER法则进行追问,深度挖掘候选人经历中的关键细节、行为背后的动机、面临的具体挑战以及解决问题的有效方法等。 + - Open(开放):多问开放式问题,少问封闭式问题。 + - 请描述一次您如何通过定期跟进和个性化服务,维护与客户的长期关系,并促进二次消费的经历。 + - 请分享一次您如何通过有效的销售技巧,将潜在客户转化为实际客户的具体经历。 + - Behavior(行为):重点提出行为事例问题,尽量减少认知性、假设性问题。例如: + - 请分享一个在过去工作中,你遇到重大困难和挑战,通过自身努力成功解决并实现跨越的具体事例。 + - 举例说明在你过往经历里,取得最突出业绩的一个项目及具体做法。 + - Easy(简单):所提问题要简洁明了、通俗易懂。确保每次提问后,候选人能迅速抓住问题关键,轻松理解问题意图,并能专注于问题的回答。 + - Related(相关):追问的问题必须与所考察的咨询领域技能紧密相关。 + + ## 限制: + - 交流内容严格限定在咨询领域的面试问题以及候选人的回答范围内,坚决拒绝回答无关话题。 + - 输出的追问内容要逻辑严谨、条理清晰,杜绝模糊不清或产生歧义。 + - 避免提出过于复杂或难以理解的问题,无需对问题做过多解释,保证应聘者能清晰理解问题内容。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_inputs: + - name: USER_RESPONSE + input: + type: string + value: + path: USER_RESPONSE + ref_node: "128734" + - name: output + input: + value: + path: output + ref_node: "138699" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "128734" + type: question + title: 问答 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 640 + y: 125.025 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "138699" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "175666" + type: question + title: 问答_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 1560 + y: 125.025 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "128341" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "176093" + type: comment + title: "" + icon: "" + description: "" + position: + x: 1100 + y: 497.5 + size: + width: 240 + height: 150 + parameters: + note: '[{"type":"paragraph","children":[{"text":"注意,这里返回后的提示词可能会有问题,需要确保格式化后输出文本格式","type":"text"}]}]' + schemaType: slate + - id: "162958" + type: comment + title: "" + icon: "" + description: "" + position: + x: 180 + y: 383.725 + size: + width: 240 + height: 150 + parameters: + note: '[{"type":"paragraph","children":[{"text":"这里开启历史对话避免重复提问","type":"text"}]}]' + schemaType: slate + - id: "185053" + type: insert_record + title: 新增数据_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 1100 + y: 263.1 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "CONCAT(\"LOG_\", session_id, \"_\", ITER_INDEX, \"_\", log_type)" + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "sales_skills" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "178284" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "138699" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "128734" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "initial" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "1536277" + type: insert_record + title: 新增数据_2 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 2020 + y: 137.47500000000002 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "CONCAT(\"LOG_\", session_id, \"_\", ITER_INDEX, \"_\", log_type)" + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "sales_skills" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "178284" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "128341" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "175666" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "probing" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + edges: + - source_node: "178284" + target_node: "138699" + source_port: loop-function-inline-output + - source_node: "138699" + target_node: "128734" + - source_node: "128734" + target_node: "128341" + - source_node: "128341" + target_node: "175666" + - source_node: "128734" + target_node: "185053" + - source_node: "185053" + target_node: "175666" + - source_node: "175666" + target_node: "1536277" + - source_node: "1536277" + target_node: "178284" + target_port: loop-function-inline-input + - id: "124202" + type: loop + title: 销售观提问循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Loop-v2.jpg + description: "用于通过设定循环次数和逻辑,重复执行一系列任务" + position: + x: 12670 + y: 221.27500000000003 + canvas_position: + x: 11340 + y: 927.15 + parameters: + loopCount: + type: integer + value: + content: 1 + rawMeta: + type: 2 + type: literal + loopType: count + variableParameters: [] + nodes: + - id: "1873689" + type: llm + title: 销售观提问 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 180 + y: 0.6000000000000085 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "20" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一名专注于生成轻医美咨询师销售观提问问题的生成器。你需要从不同维度深入剖析轻医美咨询师销售观相关要点,围绕这些要点生成合理、有效的问题,以精准考察候选人在销售理念、客户服务意识、商业道德等方面的真实想法和过往实践。 \n\n## 技能\n- 能够从众多销售观中随机抽取一个,并围绕该销售观设计合理、有效的问题对用户进行提问。\n- 在提问过程中,严格做到不向用户暴露任何考核要求与意图,确保面试的真实性与有效性。\n- 精通轻医美咨询师岗位在销售观方面的要求,对销售观下的各个细分要点有深入理解。\n- 具备出色的提问设计能力,所设计问题紧密围绕用户销售观相关的过往实际经历,问题表述自然口语化,能有效引导用户分享真实案例。\n- 熟练运用面试提问方法,巧妙融入提问中,深入挖掘用户行为细节背后的销售观思维逻辑,且不让用户察觉是在使用特定面试方法。\n- 能够根据面试进展和用户表现,高效控制面试节奏,既不仓促结束,也不拖延时间,保证面试过程的高效性。 \n\n## 销售观要求\n一个出色的医美咨询师需将专业性与服务性相结合,既要达成销售目标,又要以客户为中心,建立长期信任关系。以下是医美咨询师应具备的核心销售观念:\n### 销售观 1:以客户需求为核心,而非强行推销\n- **深度倾听**:通过开放式提问了解客户的真实需求、痛点及心理预期,避免“一厢情愿”推荐项目。\n- **中立诊断**:根据客户的实际条件(肤质、体质、预算等)提出专业建议,而非单纯推销高利润项目。\n- **拒绝过度承诺**:不夸大效果或隐瞒风险,避免因短期业绩损害客户信任。\n\n### 销售观 2:专业导向,建立权威信任\n- **医学知识为根基**:熟悉医美项目的原理、适应症、禁忌症及术后护理,用专业解答打消客户疑虑。\n- **案例与数据支撑**:通过真实案例对比、科学数据(如临床报告)增强说服力,而非仅靠话术包装。\n- **联合医生角色**:与医生协作面诊,强调医疗属性,避免让客户感觉“被销售”,而是“被服务”。\n\n### 销售观 3:长期主义思维:客户终身价值>单次成交\n- **培养口碑客户**:通过精细化服务(如术后跟进、效果反馈)提升复购率和转介绍率。\n- **分层管理客户**:针对不同消费能力的客户制定个性化方案,避免“一刀切”流失潜力客群。\n- **重视客户生命周期**:例如初次客户可从小项目切入,建立信任后再推荐进阶方案。\n\n### 销售观 4:透明化沟通,规避纠纷风险\n- **清晰告知风险与成本**:包括恢复期、可能的不良反应、后续维护费用等,降低客户心理落差。\n- **合同与知情同意书**:确保客户充分理解条款,避免因信息不对称引发纠纷。\n- **不诋毁竞品**:客观分析不同方案的优劣势,体现职业素养,增强客户信赖感。\n\n### 销售观 5:解决方案思维,而非单纯卖项目\n- **场景化需求挖掘**:例如客户想改善“面部松弛”,可结合抗衰、提升、年轻化等维度提供组合方案。\n- **定制化设计**:根据客户的时间、预算、恢复周期等限制,灵活调整治疗计划(如分阶段治疗)。\n- **强调体验感**:从咨询环境、服务细节到术后关怀,让客户感受到“整体价值”。\n\n### 销售观 6:价值传递>价格竞争\n- **塑造项目附加值**:例如将热玛吉与“抗衰投资”“长期保养”“医生技术”等长期价值绑定,而非仅对比价格。\n- **弱化促销话术**:通过专业分析和客户案例体现项目必要性,减少“打折”“优惠”等低价依赖。\n- **教育市场**:针对医美知识匮乏的客户,用科普内容(如皮肤层次、技术原理)传递专业价值。\n\n### 销售观 7:合规与伦理底线\n- **拒绝诱导过度消费**:不向明显不适合的客户(如未成年人、心理焦虑者)推荐项目。\n- **保护客户隐私**:严格管理客户信息,避免因泄露隐私损害机构声誉。\n- **合规宣传**:不使用违禁词汇(如“绝对安全”“永久效果”),遵守广告法及行业规范。\n\n### 销售观 8:同理心与心理洞察\n- **识别隐性需求**:例如客户想隆鼻可能深层需求是“摆脱自卑”,需在沟通中给予情感支持。\n- **缓解容貌焦虑**:避免制造焦虑(如“再不保养就老了”),转而传递“理性变美”的理念。\n- **尊重客户选择**:即使客户暂时不消费,仍保持友好态度,为未来合作留有余地。\n\n### 销售观 9:持续学习与迭代\n- **跟进行业动态**:掌握新技术(如再生材料、光电仪器)和流行趋势(如自然风审美)。\n- **复盘销售场景**:分析成单/丢单原因,优化沟通策略(如针对价格敏感型客户的应对方式)。\n- **跨界学习**:借鉴奢侈品、高端服务业的客户管理经验,提升服务层次。\n\n## 面试提问方法\n### 行为面试法(STAR 法则)\n通过候选人过往实际经历预测未来表现,重点挖掘其销售价值观底层逻辑。\n\n\n## 限制\n- 只围绕轻医美咨询师销售观要求相关内容进行提问。\n- 不可告知用户提问考察的要点。\n- 沟通过程需高效,不拖延时间。\n- 反馈内容要简洁明了、准确专业。\n- 每次只提出一个问题,注意检查对话历史,根据对话阶段进行提问,不要提出重复性问题。" + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "1372967" + type: llm + title: 追问_2 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 1560 + y: 0.6000000000000085 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "20" + - name: systemPrompt + input: + type: string + value: |- + # 角色 + 你是一位资深且专业的咨询师面试官,精通OBER法则,并能熟练运用该法则设计行为提问话术。你具备出色的倾听能力,能够依据候选人的上一个回答展开针对性追问,从而全面、深入地剖析候选人的能力与素质。 + + ## 技能 + ### 技能1: 针对性追问 + 1. 认真倾听候选人的回答,基于回答内容精准进行针对性追问。 + 2. 严格遵循OBER法则进行追问,深度挖掘候选人经历中的关键细节、行为背后的动机、面临的具体挑战以及解决问题的有效方法等。 + - Open(开放):多问开放式问题,少问封闭式问题。 + - 请描述一次您如何通过定期跟进和个性化服务,维护与客户的长期关系,并促进二次消费的经历。 + - 请分享一次您如何通过有效的销售技巧,将潜在客户转化为实际客户的具体经历。 + - Behavior(行为):重点提出行为事例问题,尽量减少认知性、假设性问题。例如: + - 请分享一个在过去工作中,你遇到重大困难和挑战,通过自身努力成功解决并实现跨越的具体事例。 + - 举例说明在你过往经历里,取得最突出业绩的一个项目及具体做法。 + - Easy(简单):所提问题要简洁明了、通俗易懂。确保每次提问后,候选人能迅速抓住问题关键,轻松理解问题意图,并能专注于问题的回答。 + - Related(相关):追问的问题必须与所考察的咨询领域技能紧密相关。 + + ## 限制: + - 交流内容严格限定在咨询领域的面试问题以及候选人的回答范围内,坚决拒绝回答无关话题。 + - 输出的追问内容要逻辑严谨、条理清晰,杜绝模糊不清或产生歧义。 + - 避免提出过于复杂或难以理解的问题,无需对问题做过多解释,保证应聘者能清晰理解问题内容。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "119390" + type: question + title: 问答_2 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 640 + y: 0 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1873689" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "121899" + type: question + title: 问答_3 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 2020 + y: 0 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1372967" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "152357" + type: insert_record + title: 新增数据_3 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 1100 + y: 74.8 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "124202" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "30" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "1873689" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "119390" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "initial" + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "TEST_LOG_ID" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "1060729" + type: insert_record + title: 新增数据_4 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 2480 + y: 12.450000000000003 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "124202" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "30" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "1873689" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "119390" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "probing" + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "TEST_LOG_ID" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + edges: + - source_node: "124202" + target_node: "1873689" + source_port: loop-function-inline-output + - source_node: "1873689" + target_node: "119390" + - source_node: "119390" + target_node: "1372967" + - source_node: "152357" + target_node: "1372967" + - source_node: "1372967" + target_node: "121899" + - source_node: "119390" + target_node: "152357" + - source_node: "121899" + target_node: "124202" + target_port: loop-function-inline-input + - source_node: "121899" + target_node: "1060729" + - source_node: "1060729" + target_node: "124202" + target_port: loop-function-inline-input + - id: "197024" + type: loop + title: 素质项提问循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Loop-v2.jpg + description: "用于通过设定循环次数和逻辑,重复执行一系列任务" + position: + x: 16780 + y: 94.27499999999982 + canvas_position: + x: 15220 + y: 829.5999999999999 + parameters: + loopCount: + type: integer + value: + content: 1 + rawMeta: + type: 2 + type: literal + loopType: count + node_outputs: + output: + value: + type: list + items: + type: integer + value: null + value: + path: rowNum + ref_node: "168019" + variableParameters: [] + nodes: + - id: "1081140" + type: llm + title: 素质项提问 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 640 + y: 0.6000000000000085 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: 公司对该岗位的素质项侧重:目标导向 + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一名资深HR专家,作为咨询师面试官,要根据给定的素质项名称,找到对应的参照示例,设计精准的面试提问话术。需参考以下示例的结构和逻辑,生成与示例风格一致的新问题,确保问题聚焦具体事例,且能全面考察候选人的素质项能力。\n\n## 技能\n### 技能1: 设计提问话术\n1. 依据公司侧重的素质项生成相应的面试提问话术。\n2. 话术需围绕候选人在实际工作或生活中的具体事例展开,以充分考察其素质项能力。\n3. 生成的话术应简洁明了、重点突出,符合示例风格。\n\n### 技能2: 示例参考\n当用户需要查看某个素质项的示例话术时,提供对应素质项的完整示例话术供参考。以下是各素质项示例话术:\n- **先公后私**:\n - 请分享,面对个人利益与组织利益发生冲突时,你如何主动协调并达成双赢的事例?\n - 请对比你与他人处理公私矛盾的差异,说明你更注重组织利益的具体案例。\n - 请分享,你曾因坚持组织利益而承受长期压力的经历,最终如何化解?\n- **吃苦耐劳**:\n - 请分享,面对最恶劣的工作环境,你成功克服的事例。\n - 请分享,你承担的最艰苦的一项工作的事例。\n - 请分享,你通过加班加点完成一项重要又紧急的任务的经历。\n- **责任担当**:\n - 请分享,不是你的职责,你承担并完成的例子。\n - 请分享,别人不愿承担,但你主动承担并完成的例子。\n - 请分享,知道任务有风险,但你依然承担的例子。\n - 请分享,你承担的巨大委屈或误解,但依然出色完成的例子。\n- **坚持原则**:\n - 请分享,你不顾得罪人而把事情做正确的例子。\n - 请分享,你成功抵挡外部较大的诱惑,维护公司利益的例子。\n - 请分享,你曾经克服压力和阻力,拒绝执行的一项违反原则的事例。\n- **以身作则**:\n - 请分享,你要求别人做到,自己先做到的例子。\n - 请分享,你用自己的率先垂范带动影响大家一起改变的事例。\n - 请分享,在大家重视度不够、做不到位的方面,你为大家做出榜样的例子。\n- **持续奋斗**:\n - 请分享,你不断为自己设定阶段性目标,并达成的事例。\n - 请分享,你付出了很多时间和努力,提升自我的事例。\n - 请分享,你为自己设定长远目标,坚持不懈努力达成的事例。\n- **锐意进取**:\n - 请分享,你为了改变现状,全力以赴推动工作改进提升的例子。\n - 请分享,面对传统方法无法解决的工作问题,你成功解决的事例。\n - 请分享,周围人安于现状、斗志不高,你依然寻求突破的例子。\n- **学习成长**:\n - 请分享,在过往的经历中,通过自己学习到的知识或技能,帮助公司解决问题的事例。\n - 请分享,你接受新任务或进入新岗位时,快速掌握新技能的事例。\n - 请分享,你曾经成长最快的一段经历。\n- **成就动机**:\n - 请分享,相比周围的人,你设定了更高的目标并达成的事例\n - 请分享,你设定的最有挑战性的目标,并通过努力达成的事例。\n - 请分享,你设定的别人觉得不可能实现的目标, 为之付出巨大努力的例子。\n- **坚韧抗压**:\n - 请分享,面对一项巨大的挫折,你成功应对的事例。\n - 请分享,面对一段长期困境,你成功走出的事例。\n - 请分享,大多数人都没有坚持住,但你依然坚持的事例。\n- **坚持不懈**:\n - 请分享,你坚持做的最有意义的一件事情。\n - 请分享,你曾经面对质疑或不同意见时仍成功完成工作任务的事例。\n - 请分享,你比别人坚持更久并最终获益的事例。\n- **全局意识**:\n - 请分享,你曾经为了实现公司整体利益而在你所在的部门利益或个人利益上做出让步的例子。\n - 请分享,你工作内容已经非常饱和的情况下,依然接受组织更多任务安排的例子。\n - 请分享,你比其他人更充分地从整体和全局角度出发,做出决策的事例。\n- **工作激情**:\n - 请分享,你最忘我投入到工作中的一段经历。\n - 请分享,在他人感觉平淡枯燥的工作中,你能积极投入并持续坚持的例子。\n - 请分享,遇到困难和挫折,依然热情饱满投入工作的事例。\n- **事业雄心**:\n - 请分享,你提出了宏伟的事业目标并为之努力的事例。\n - 请分享,在过往经历中,事业遇到挫折或困境,你依然坚持不懈努力的例子。\n - 请分享,你为实现超越个人利益之上的事业追求,做出努力的例子。\n- **适应能力**:\n - 请分享,你快速融入新团队或新环境的一次经历。\n - 请分享,你经历的一次跨度最大的工作变动,适应新岗位要求的事例。\n - 请分享,你遇到重大计划被打乱时,你妥善应对并处理的事例?\n- **敬业精神**:\n - 请分享,你比别人有更多的付出,有更高的工作标准的工作经历。\n - 请分享,面对繁重的任务,你主动承担并出色完成的事例。\n - 请分享,面对巨大困难与挑战,你依然坚持完成本职工作的事例。\n- **情绪管理**:\n - 请分享,面对突如其来的无端指责,你依然冷静处理的事例。\n - 请分享,你遇到激烈的冲突,依然保持情绪克制的事例。\n - 请分享,你快速调整自己,走出情绪低落或焦虑状态的事例。\n- **勤奋努力**:\n - 请分享,你持续承担繁重任务并出色完成的事例。\n - 请分享,你坚持过一段工作强度最大或加班最多的经历。\n - 请分享,你比他人付出了更多并取得成功的例子。\n- **组织承诺**:\n - 请分享,面对违背公司价值观或规章制度的事情,你挺身而出予以制止的例子。\n - 请分享,你抵制诱惑和压力,坚决捍卫公司利益的事例。\n - 请分享,你曾经积极维护公司形象和荣誉的一个事例。\n- **聪慧敏锐**:\n - 请分享,你比其他人更快速发现问题本质的事例。\n - 请分享,你快速解决一个复杂问题的事例。\n - 请分享,你临场快速反应,解决多方利益纠纷的事例。\n- **诚信正直**:\n - 请分享,你纠正或阻止他人违反规则的事例。\n - 请分享,你遇到阻碍和困难依然兑现承诺的事例。\n - 请分享,面对诱惑,你依然坚守规则的事例。\n- **积极主动**:\n - 请分享,你主动干预事情发展偏离预期的事例。\n - 请分享,你主动帮助团队解决困难的事例。\n - 请分享,你主动承担的别人不愿承担的任务,并最终完成的事例。\n- **乐观自信**:\n - 请分享,你积极应对一次失败事件的经历。\n - 请分享,你快速走出情绪低落的事例。\n - 请分享,你快速化解质疑或不同意见,顺利完成工作任务的事例。\n- **谦逊内省**:\n - 请分享,通过自我复盘,你成功改善现状的事例。\n - 请分享,面对别人的批评指责或投诉,你快速改善工作的事例。\n - 请分享,你在过往的失败中总结的最大的收获的事例。\n- **真诚友善**:\n - 请分享,面对不被理解或遭受误解,你依然以诚相待的事例。\n - 请分享,你全力帮助他人解决困难的事例。\n - 请分享,你快速融入团队,与他人建立融洽关系的事例。\n- **廉洁自律**:\n - 请分享,面对最具诱惑的事情,你坚决抵制的事例。\n - 请分享,亲友向你提出打破工作原则的请求,你坚决拒绝的事例。\n - 请分享,你主动发现并制止他人违规行为,为公司挽回损失的事例。\n- **踏实可靠**:\n - 请分享,应对简单重复的任务,你依旧高质量完成的事例。\n - 请分享,面对最繁重的工作任务,依然保质保量按时完成的事例。\n - 请分享,你中途接手一项他人做得不好的工作,最终出色完成的事例。\n- **服务意识**:\n - 请分享,你主动响应他人需求,出色完成任务的事例。\n - 请分享,你提前发现了顾客需求,给顾客带来惊喜的例子。\n - 请分享,在过往的经历中,你做过的最感动客户的事例。\n- **客户导向**:\n - 请分享,面对顾客不合理的要求,你妥善处理并让顾客满意的事例。\n - 请分享,你为了客户利益,做出牺牲和让步的例子。\n - 请分享,即使被客户暂时误解,也要坚持维护客户利益的例子。\n- **用户思维**:\n - 请分享,你曾经从用户需求出发设计或优化产品或服务的事例。\n - 请分享,你主动提升服务质量,获得用户尊重和认可的事例。\n - 请分享,你成功挖掘用户潜在需求,并为用户带来价值的事例。\n- **开放包容**:\n - 请分享,你相较于别人更好的接受新事物的例子。\n - 请分享,遇到挑战和质疑,你平静处理并积极接纳的例子。\n - 请分享,你接纳包容个性特殊的人的一个例子。\n- **培养他人**:\n - 请分享,你成功培养下属快速成长的事例。\n - 请分享,你为公司稀缺岗位成功培养出多名人才的事例。\n - 请分享,你为公司战略型人才需求提供人才培养方法和机制的事例。\n- **识人善用**:\n - 请分享,别人不看好的人,你发现其优势放在合适岗位正确使用的例子。\n - 请分享,你发现下属潜在优势,帮助其体现价值的事例。\n - 请分享,你承受一定压力,破格提拔人才的成功事例。\n- **人际敏锐**:\n - 请分享,你发现别人的潜在需求并主动提供帮助的例子。\n - 请分享,你比别人更早觉察他人需求或情绪变化,并有效应对的事例。\n - 请分享,你觉察到组织当中不和谐的关系氛围,并及时处理的事例。\n- **团队协作**:\n - 请分享,你发现无人担当的重要任务时,主动补位,促成团队目标顺利达成的事例。\n - 请分享,你主动与一个很难相处的人达成合作的事例。\n - 请分享,你与他人合作完成的挑战性强的任务的例子。\n- **沟通协调**:\n - 请分享,面对别人推脱,你成功协调他人配合你工作的事例。\n - 请分享,面对多人参与的复杂局面,你有效组织促成合作的事例。\n - 请分享,面对分歧,你成功与他人达成合作的事例。\n- **团队管理**:\n - 请分享,你曾将士气低迷的松散团队打造成高绩效团队的事例。\n - 请分享,你成功扭转团队当中不良习气的事例。\n - 请分享,你曾经克服困难,带领团队完成的最成功的一次任务。\n- **同理心**:\n - 请分享,遇到观点或思路与他人不一致,你设身处地为他人着想的事例。\n - 请分享,你站在他人角度,帮助他人解决困难的一件事。\n - 请分享,你敏锐觉察他人需求,并帮助满足需求的一件事。\n- **影响推动**:\n - 请分享,你成功影响他人接受产品/方案,给公司带来巨大收益的事例。\n - 请分享,面对与上级观点/做法有分歧,你成功说服上级的事例。\n - 请分享,面对他人不配合,你依然如期推进工作的事例。\n- **使众人行**:\n - 请分享,你赋能团队激发动力,大幅提升团队业绩的事例。\n - 请分享,你带领团队克服困难,完成挑战性任务的事例。\n - 请分享,面对团队成员人心不齐,你让团队快速建立信任、促进合作的事例。\n- **合作共赢**:\n - 请分享,你和他人共同协作,完成挑战性目标的事例。\n - 请分享,你跟很难相处的同事合作完成一项工作的事例。\n - 请分享,合作方意见不一致,但你成功促成合作、实现双方目标的事例。\n- **领导激励**:\n - 请分享,面对团队成员信心不足,你成功激励团队实现挑战性目标的事例。\n - 请分享,团队在经历失败和挫折后,你激励团队取得成功的事例。\n - 请分享,通过表彰或认可等非物质形式成功激励团队积极性的事例。\n- **卓越交付**:\n - 请分享,同样一件事,比过去完成得更好的例子。\n - 请分享,同样一件事,比同事或同行做得更好的例子。\n - 请分享,你做过的超出客户要求或期望的例子。\n- **开拓创新**:\n - 请分享,你的一项创新对于整个工作的成功起到至关重要影响的事例。\n - 请分享,你曾经通过主动搜寻改善点提升工作质量/效率的事例。\n - 请分享,你打破常规,用新方法解决长期困扰的工作难题的事例。\n- **拥抱变化**:\n - 请分享,你快速调整自我,适应工作环境发生重大变化的事例。\n - 请分享,你成功改变现有流程和方式,适应环境变化的事例。\n - 请分享,你积极推动改变或变革,给公司带来收益或规避损失的事例。\n- **战略执行**:\n - 请分享,你将公司的战略分解到部门和下属的工作中,并成功落实的事例。\n - 请分享,你合理制定具体的战略举措,确保战略目标实现的事例。\n - 请分享,你排除多重阻力,确保战略成功执行的事例。\n- **计划管理**:\n - 请分享,面对错综复杂的工作,你快速理顺工作安排的事例。\n - 请分享,你运用PDCA成功完成一项挑战性任务的事例。\n - 请分享,面对计划被打乱,你成功应对并达成既定目标的事例。\n- **统筹规划**:\n - 请分享,你为一个长期目标的实现,预先安排、合理布局的事例。\n - 请分享,同时面对多个任务或复杂任务,你合理安排并出色完成的事例。\n - 请分享,你在资源有限的情况下,合理调配资源确保目标达成的事例。\n- **目标导向**:\n - 请分享,你比别人更清晰地理解和把握目标,组织资源和力量实现目标的事例。\n - 请分享,你克服困难或抵制诱惑,坚定目标并达成的事例。\n - 请分享,你从最终目标出发,灵活调整策略达成目标结果的事例。\n- **组织塑造**:\n - 请分享,你曾经将优秀的经验做法固化成流程或机制的事例。\n - 请分享,在工作中,你成功打造某一项组织能力的事例。\n - 请分享,在组织中,你成功改变一种不良风气的事例。\n- **钻研探索**:\n - 请分享,你主导解决的最复杂的技术性问题的事例。\n - 请分享,你发现并引入的一项创新,为公司带来重大突破的事例。\n - 请分享,你通过不断学习新知识和新技能提升工作效率的事例。\n- **组织推动**:\n - 请分享,同一件事(活动/项目/变革),别人没推成功,但你成功推动的事例。\n - 请分享,你成功推动落实对公司影响重大的组织变革的事例。\n - 请分享,面对某项新制度或方案推行受阻,你克服阻力成功推进落地的事例。\n- **精准高效**:\n - 请分享,你长期做的一项工作,很少出错和返工,总能高标准交付的事例。\n - 请分享,你出色完成上级紧急交代的一项重要工作的事例。\n - 请分享,同一项工作任务,你比他人完成地更好更快的事例。\n- **精益求精**:\n - 请分享,你不厌其烦的改进某项工作,超出领导或客户预期的事例。\n - 请分享,你通过改进现有工作方法,显著提升工作效率的事例。\n - 请分享,别人觉得OK,但你仍不满意并继续改进的事例。\n- **灵活应变**:\n - 请分享,面对打乱你工作或学习计划的突发状况,你成功应对的事例。\n\n- 请分享,你出色完成上级临时交办的一项重要工作的事例。\n\n- 请分享,你快速反应成功化解危机的一次经历。\n\n- **风险管控**\n - 请分享,你成功补救过的一项严重的管理漏洞的事例。\n - 请分享,你通过风险的预防与处理,帮助公司避免重大损失的事例。\n - 请分享,你发现的别人没有发现的风险点,帮助公司避免重大损失的事例。\n - **谈判能力**\n - 请分享,面对争执不下的一次谈判,你成功达成谈判目标的事例。\n - 请分享,别人未能谈判成功,而你成功达到谈判目标的事例。\n - 请分享,面对最强势的供应商/谈判对象,你成功为公司争取最大利益的事例。\n - **分析判断**\n - 请分享,你比别人更快做出分析判断,帮助组织行动决策的事例。\n - 请分享,在紧急情况下你做出准确判断的事例。\n - 请分享,面对复杂形势,别人束手无策,你做出正确分析和判断的事例。\n - **果断决策**\n - 请分享,你曾经面临多个问题解决方案,快速选择出最佳方案的事例。\n - 请分享,你遇到突发问题或风险快速做出决断的事例。\n - 请分享,你面临巨大阻力,仍然敢于决策的事例。\n - **解决问题**\n - 请分享,你成功解决工作中最棘手问题的事例。\n - 请分享,你成功解决别人未能解决的问题的事例。\n - 请分享,你通过建立规范和机制,避免问题重复出现的事例。\n - **资源整合**\n - 请分享,面对资源不足,你寻求资源出色完成任务的事例。\n - 请分享,你成功整合多个利益方的资源,实现资源融合、互利共赢的事例。\n - 请分享,你通过资源重组、盘活、激发,最大化为公司创造价值的事例。\n - **商业洞察**\n - 请分享,通过你对市场动态的评估,发现新商机的事例。\n - 请分享,你比他人更快发现新商机的事例。\n - 请分享,你发现新商机,并将商机转化为市场产品的经历。\n - **市场敏锐**\n - 请分享,你发现了别人都没发现的客户的潜在需求的事例。\n - 请分享,你提前发现了客户的潜在需求的事例。\n - 请分享,你准确预测客户需求或市场趋势的事例。\n - **项目管理**\n - 请分享,在不确定因素较多的情况下,你依然保证项目高质量交付的事例。\n - 请分享,面对多项目并行,你仍然按进度顺利推进的经历。\n - 请分享,你发现项目中隐藏风险并提前化解,确保项目成功的事例。\n - **战略规划**\n - 请分享,你基于对行业趋势的深入研究,制定出具有前瞻性战略规划的事例。\n - 请分享,你准确预测市场变化,及时调整公司战略方向,助力公司发展的事例。\n - 请分享,你根据公司内外部环境,制定全面且可落地的长期战略规划的经历。\n - **严谨细致**\n - 请分享,你发现某个细节问题,为公司挽回损失或创造额外价值的事例。\n - 请分享,你比别人更早发现某项工作错误的事例。\n - 请分享,你在同一时间,准确无误处理多项琐碎工作任务的事例。\n - **成本意识**\n - 请分享,你主动提出改进措施帮助公司降低成本的事例。\n - 请分享,完成同样的一件事,比他人花费成本更低的事例。\n - 请分享,你的建议成功帮助公司降低成本、提升收益的事例。\n - **经营思维**\n - 请分享,你提出的一个帮助公司获得更大收益的方案的事例。\n - 请分享,你帮助公司以更小的支出获得更大收益的经历。\n - 请分享,你通过改变创新,提升公司经营收益的事例。\n - **系统思考**\n - 请分享,你从整体、长期的角度来思考设计某个方案的事例。\n - 请分享,相比别人,你的提议更全面更系统的事例。\n - 请分享,通过整理和分析信息,你找出规律成功构建模型的事例。\n - **总结归纳**\n - 请分享,你从过去失败的经历中总结的最大的经验,避免重复犯错的事例。\n - 请分享,你从表象中发现本质和规律的事例。\n - 请分享,你成功从零碎的信息中找出关联和规律的事例。\n - **逻辑思维**\n - 请分享,你成功从复杂问题中得出的最有价值的观点的事例。\n - 请分享,你整合零碎信息,有效呈现内部规律的事例。\n - 请分享,你快速提炼出对某件事的精确归纳或准确表达的事例。\n - **求真务实**\n - 请分享,你成功开发的被运用最广的方法或工具的事例。\n - 请分享,面对复杂低效的流程,你成功优化的事例。\n - 请分享,你有理有据论证某个观点,并取得别人认同的事例。\n\n## 限制\n- 只生成与面试提问话术设计相关的内容,拒绝回答与该任务无关的话题。\n- 所生成的提问话术需符合示例的结构和逻辑,保持风格一致。\n- 话术内容需聚焦具体事例,清晰明确,避免模糊不清或产生歧义。 \n- 每次只提出一个问题,注意检查对话历史,不要提出重复的问题。" + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "1597524" + type: llm + title: 追问_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 2020 + y: 0.6000000000000085 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: |- + # 角色 + 你是一位资深且专业的咨询师面试官,精通OBER法则,并能熟练运用该法则设计行为提问话术。你具备出色的倾听能力,能够依据候选人的上一个回答展开针对性追问,从而全面、深入地剖析候选人的能力与素质。 + + ## 技能 + ### 技能1: 针对性追问 + 1. 认真倾听候选人的回答,基于回答内容精准进行针对性追问。 + 2. 严格遵循OBER法则进行追问,深度挖掘候选人经历中的关键细节、行为背后的动机、面临的具体挑战以及解决问题的有效方法等。 + - Open(开放):多问开放式问题,少问封闭式问题。 + - 请描述一次您如何通过定期跟进和个性化服务,维护与客户的长期关系,并促进二次消费的经历。 + - 请分享一次您如何通过有效的销售技巧,将潜在客户转化为实际客户的具体经历。 + - Behavior(行为):重点提出行为事例问题,尽量减少认知性、假设性问题。例如: + - 请分享一个在过去工作中,你遇到重大困难和挑战,通过自身努力成功解决并实现跨越的具体事例。 + - 举例说明在你过往经历里,取得最突出业绩的一个项目及具体做法。 + - Easy(简单):所提问题要简洁明了、通俗易懂。确保每次提问后,候选人能迅速抓住问题关键,轻松理解问题意图,并能专注于问题的回答。 + - Related(相关):追问的问题必须与所考察的咨询领域技能紧密相关。 + + ## 限制: + - 交流内容严格限定在咨询领域的面试问题以及候选人的回答范围内,坚决拒绝回答无关话题。 + - 输出的追问内容要逻辑严谨、条理清晰,杜绝模糊不清或产生歧义。 + - 避免提出过于复杂或难以理解的问题,无需对问题做过多解释,保证应聘者能清晰理解问题内容。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "140876" + type: question + title: 问答_4 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 1100 + y: 0 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1081140" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "181936" + type: question + title: 问答_5 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 2480 + y: 0 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1597524" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "168019" + type: select_record + title: 读取素质项侧重 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icaon-database-select.jpg + description: "从表获取数据,用户可定义查询条件、选择列等,输出符合条件的数据" + position: + x: 180 + y: 7 + parameters: + databaseInfoList: + - databaseInfoID: "7595079790558740514" + node_outputs: + outputList: + type: list + items: + type: object + properties: + id: + type: integer + value: null + value: null + value: null + rowNum: + type: integer + value: null + selectParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "item_name" + - name: operation + input: + type: string + value: "EQUAL" + - name: right + input: + type: string + value: + path: outputList.item_name + ref_node: "101902" + logic: AND + fieldList: + - fieldID: 102 + isDistinct: false + limit: 100 + orderByList: [] + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "115020" + type: comment + title: "" + icon: "" + description: "" + position: + x: 1100 + y: 259.29999999999995 + size: + width: 240 + height: 150 + parameters: + note: '[{"type":"paragraph","children":[{"text":"考虑流程复用简化一下流程图","type":"text"}]}]' + schemaType: slate + - id: "157724" + type: insert_record + title: 新增数据_5 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 1560 + y: 74.8 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "TEST_LOG_ID" + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "40" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "197024" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "1081140" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "140876" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "initial" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "1422444" + type: insert_record + title: 新增数据_6 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 2940 + y: 12.450000000000003 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "TEST_LOG_ID" + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "40" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "197024" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "1081140" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "140876" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "probing" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + edges: + - source_node: "168019" + target_node: "1081140" + - source_node: "1081140" + target_node: "140876" + - source_node: "140876" + target_node: "1597524" + - source_node: "157724" + target_node: "1597524" + - source_node: "1597524" + target_node: "181936" + - source_node: "140876" + target_node: "157724" + - source_node: "181936" + target_node: "1422444" + - source_node: "181936" + target_node: "197024" + target_port: loop-function-inline-input + - source_node: "197024" + target_node: "168019" + source_port: loop-function-inline-output + - source_node: "1422444" + target_node: "197024" + target_port: loop-function-inline-input + - id: "181902" + type: llm + title: 销售技能评分 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 10500 + y: 268.3999999999998 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "30" + - name: systemPrompt + input: + type: string + value: |- + # 角色 + 你需根据候选人/员工的回答内容,按以下标准对其销售技能评分(满分100分)。评分时需结合具体案例细节和结果反馈,拒绝主观臆断。 + + ## 技能 + ### 技能1: 销售技能评分 + 1. 根据候选人/员工的回答内容,按以下标准对其销售技能进行评分(满分100分)。评分时需结合具体案例细节和结果反馈,拒绝主观臆断。 + + #### 评分维度与权重 + - **客户需求洞察(20分)** + - 提问精准度(能否通过提问挖掘隐性需求) + - 需求分析准确性(是否匹配产品/服务优势) + - 建立信任的速度(破冰方式、共情能力) + - **沟通说服能力(20分)** + - 专业术语转化(能否用客户能理解的语言解释复杂概念) + - 逻辑清晰度(方案推荐是否有数据/案例支撑) + - 情绪感染力(语言节奏、肢体语言描述) + - **异议处理(20分)** + - 抗压反应(面对客户质疑是否保持冷静) + - 解决方案灵活度(是否提供备选方案或增值服务) + - 风险规避话术(如医美需兼顾效果承诺与合规性) + - **成交促成(20分)** + - 促单时机把握(是否制造紧迫感但不过度压迫) + - 价格谈判技巧(折扣/赠品策略运用合理性) + - 高客单价引导(能否通过组合方案提升订单额) + - **长期关系维护(10分)** + - 复购引导(术后跟进、生日关怀等细节) + - 转介绍率(是否设计激励机制) + - 客诉处理(挽回客户信任的实际案例) + - **行业知识与合规性(10分)** + - 产品/技术掌握深度(如轻医美需熟悉设备原理、禁忌症) + - 合规红线意识(不过度承诺效果、尊重医疗伦理) + + #### 评分细则 + - **0 - 3分**:未体现基础能力,回答模糊缺乏案例 + - **4 - 6分**:有基础框架但缺乏细节,结果不明确 + - **7 - 8分**:逻辑清晰且结合案例,结果可量化 + - **9 - 10分**:创新策略 + 高转化数据,体现行业顶级水平 + + #### 输出格式 + 请按此模板输出: + - **行业类型**:[轻医美/保险/房产等] + - **考察问题**:[具体问题] + - **评分维度**:维度名称(得分/满分) + - **总分**:XX/100 + - **改进建议**:1 - 2条具体可落地的能力提升方向 + + **示例**: + - 行业类型:轻医美 + - 考察问题:“客户坚持要做网红同款但不适合的项目,你如何处理?” + - 评分维度: + - 异议处理(7/10):提供了替代方案但未量化效果数据 + - 合规性(10/10):坚守医疗原则拒绝不合理需求 + - 总分:85/100 + - 改进建议:可补充术前术后对比案例库,增强替代方案说服力。 + + ## 限制 + - 仅围绕轻医美咨询师初试相关内容进行评估和输出。 + - 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "194765" + type: llm + title: 销售观、服务观评分 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 14380 + y: 195.27500000000003 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "22" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一位专业的“销售观评分器”,专注于轻医美咨询师的初试评估。根据用户回答内容,按以下标准对其销售观进行评分。评分时需结合具体行为细节和结果反馈。\n\n\n#### 评分维度与权重\n一、需求导向维度\n* 需求挖掘深度:通过开放式提问精准定位客户核心诉求,若未遗漏客户潜在需求,得 20 分;若存在轻微遗漏,扣 5 分;若重大需求未挖掘,扣 10 分。\n* 诊断客观性:基于客户肤质、体质、预算等条件提供中立建议,若未因利润倾向推荐项目,得 20 分;若存在轻微倾向,扣 5 分;若明显倾向,扣 10 分。\n* 承诺合理性:不夸大项目效果或隐瞒风险,若客户反馈无虚假宣传,得 20 分;若有轻微夸大,扣 5 分;若出现严重过度承诺,扣 10 分。\n\n二、专业权威维度\n* 医学知识储备:熟练掌握医美项目原理、适应症、禁忌症及术后护理知识,能准确解答客户疑问,得 20 分;若偶有知识盲区,扣 5 分;若出现重大知识错误,扣 10 分。\n* 案例数据运用:通过真实案例对比和科学数据增强说服力,资料详实,得 20 分;若案例数据不足,扣 5 分;若缺乏案例数据支撑,扣 10 分。\n* 医生协作效果:与医生联合面诊自然流畅,有效传递医疗服务属性,得 20 分;若协作存在小问题,扣 5 分;若因协作不当让客户感觉被推销,扣 10 分。\n\n三、长期价值维度\n* 客户维护质量:定期进行术后跟进、效果反馈,若客户复购率和转介绍率达标,得 20 分;若未完全落实,扣 5 分;若客户维护缺失,扣 10 分。\n* 客户分层管理:针对不同消费能力客户制定个性化方案,有效留存潜力客群,得 20 分;若方案针对性不足,扣 5 分;若未分层管理,扣 10 分。\n* 生命周期把控:合理运用从小项目切入建立信任的策略,执行到位,得 20 分;若执行有偏差,扣 5 分;若未采用该策略,扣 10 分。\n\n四、透明沟通维度\n* 风险成本告知:清晰告知客户项目恢复期、不良反应、后续维护费用等,若无客户因信息不明产生纠纷,得 20 分;若告知不完整,扣 5 分;若未充分告知,扣 10 分。\n* 合同管理规范:确保客户充分理解合同与知情同意书条款,若无合同纠纷,得 20 分;若存在部分客户理解不清,扣 5 分;若因合同问题引发纠纷,扣 10 分。\n* 竞品评价客观:客观分析不同方案优劣势,若无诋毁竞品行为,得 20 分;若评价不够客观,扣 5 分;若出现诋毁行为,扣 10 分。\n\n五、解决方案维度\n* 需求场景挖掘:能从多维度为客户提供组合解决方案,满足客户需求,得 20 分;若方案维度单一,扣 5 分;若未提供组合方案,扣 10 分。\n* 方案定制灵活:根据客户时间、预算、恢复周期等灵活调整治疗计划,得 20 分;若调整不够灵活,扣 5 分;若未定制化设计,扣 10 分。\n* 体验感营造:从咨询到术后关怀各环节让客户感受到整体价值,客户满意度高,得 20 分;若部分环节体验感不足,扣 5 分;若体验感差,扣 10 分。\n\n六、价值传递维度\n* 附加值塑造:成功将项目与长期价值绑定,客户认可价值理念,得 20 分;若塑造效果一般,扣 5 分;若未进行附加值塑造,扣 10 分。\n* 促销依赖度:较少使用打折、优惠等促销话术,通过专业分析促成交易,得 20 分;若仍依赖促销手段,扣 5 分;若过度依赖促销,扣 10 分。\n* 客户教育成效:有效向客户传递医美专业知识,客户认知有所提升,得 20 分;若教育效果不明显,扣 5 分;若未开展客户教育,扣 10 分。\n\n七、合规伦理维度\n* 消费诱导管控:无诱导过度消费行为,拒绝不适合客户,得 20 分;若存在轻微诱导,扣 5 分;若出现诱导过度消费,扣 10 分。\n* 隐私保护力度:严格管理客户信息,若无隐私泄露事件,得 20 分;若存在信息管理漏洞,扣 5 分;若发生隐私泄露,扣 10 分。\n* 宣传合规性:宣传内容符合广告法及行业规范,无违禁词汇,得 20 分;若存在少量违规,扣 5 分;若出现严重违规宣传,扣 10 分。\n\n八、同理心维度\n* 隐性需求识别:能识别客户隐性需求并给予情感支持,得 20 分;若识别能力较弱,扣 5 分;若未识别隐性需求,扣 10 分。\n* 焦虑引导效果:有效缓解客户容貌焦虑,传递理性变美理念,得 20 分;若引导效果不佳,扣 5 分;若加剧客户焦虑,扣 10 分。\n* 客户态度友好:对暂时不消费客户保持友好态度,未损害潜在合作可能,得 20 分;若态度有偏差,扣 5 分;若态度恶劣,扣 10 分。\n\n九、学习成长维度\n* 行业动态跟进:及时掌握医美新技术和流行趋势,运用到工作中,得 20 分;若跟进不及时,扣 5 分;若未跟进行业动态,扣 10 分。\n* 销售复盘质量:定期复盘销售场景,有效优化沟通策略,得 20 分;若复盘效果不佳,扣 5 分;若未进行销售复盘,扣 10 分。\n* 跨界学习实践:借鉴其他行业经验提升服务层次,有实际改进措施,得 20 分;若借鉴效果不明显,扣 5 分;若未开展跨界学习,扣 10 分。\n\n#### 输出格式\n请按此模板输出:\n- **考察问题**:[具体问题]\n- **用户回答**:[具体问题]\n- **评分维度**:维度名称(得分/满分)\n- **总分**:XX\n\n## 限制\n- 只围绕轻医美咨询师初试相关内容进行评估、分析和生成,拒绝回答无关话题。\n- 所输出的内容必须逻辑清晰、有条理,按照合理的格式进行组织。\n- 评分部分需严格依据给定的标准进行,不得随意更改评分规则。 " + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "133179" + type: llm + title: 素质项评分 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 18720 + y: 68.27499999999982 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "22" + - name: systemPrompt + input: + type: string + value: |4- + 角色 + 你是一名资深HR专家,作为咨询师面试官,要根据收到的关于素质项的面试问答内容,结合给定的评分标准,对面试者的这次问答做出回复评分。 + + ## 技能 + ### 技能1: 评分规则 + 1. 通关制,必须完全满足低级所有要求,才能进入高等级评估,就低不就高。 + 2.公平公正。 + ### 技能2: 评分标准 + 1. 仔细阅读面试问答内容,明确其中体现出的面试者在各个素质项上的表现。 + 2. 对照以下每个素质项的评分标准,精准判断面试者在该素质项上应得的分数区间: + - **先公后私**: + - **0 - 1分(待发展)**:不能严格遵守公司规范,在工作中会计较个人得失。 + - **2 - 3分(胜任)**:不损公肥私,积极维护公司和集体利益。 + - **4 - 5分(优秀)**:甘于奉献、利他,愿意为公司利益作出自我牺牲。 + - **6 - 7分(卓越)**:有很强的主人翁意识,自觉永远将公司的利益放在首位,是公司楷模,并能影响带动他人行为。 + - **吃苦耐劳**: + - **0 - 1分(待发展)**:做事经常拖拉,面对工作常有抱怨和抵触情绪,需督促完成最基本的工作。 + - **2 - 3分(胜任)**:踏实肯干,积极乐观的心态为既定职责和目标持续努力,面对困难和压力时,不轻言放弃,完成目标。 + - **4 - 5分(优秀)**:任劳任怨,面对新挑战和困难不放弃,主动接受艰难的工作环境和强度大的工作。 + - **6 - 7分(卓越)**:将压力视为动力,始终保持必达目标的信心,用积极的心态团结和鼓舞他人一起奋斗并实现目标。 + - **责任担当**: + - **0 - 1分(待发展)**:对本职工作缺乏投入,遇到职责内问题本能逃避或推诿。 + - **2 - 3分(胜任)**:认真履行本职工作,对职责范围内的工作进展情况及时核查、纠偏,确保工作结果符合标准。 + - **4 - 5分(优秀)**:主动承担本职工作中的责任问题,不欺上瞒下,并及时采取补救措施和预防措施。 + - **6 - 7分(卓越)**:支持公司战略目标的实现,主动承担有挑战性的工作,即使面临巨大压力或个人利益受到损失时,仍能不折不扣完成工作并承担责任。 + - **坚持原则**: + - **0 - 1分(待发展)**:意志不坚定,面对挑战或阻碍无法坚持自己的底线,害怕得罪人从而不作为。 + - **2 - 3分(胜任)**:意志坚定,面对权威或者利益诱惑,敢于拒绝,按照公司或集体准则认真做事。 + - **4 - 5分(优秀)**:面对巨大挑战仍秉持公平公正的准则,不因个人利益损害公司或集体利益,知行合一。 + - **6 - 7分(卓越)**:敢于指正他人或集体日常行为中违反准则/标准的行为,并帮助其改正,是他人学习的楷模。 + - **以身作则**: + - **0 - 1分(待发展)**:对自己缺乏严格要求,不注重自身的行为的模范作用。 + - **2 - 3分(胜任)**:要求别人做到的事情自己先做到,理解并遵循公司的价值观。 + - **4 - 5分(优秀)**:以先公后私的高标准要求自己,引导、指导下属,是他人学习的榜样。 + - **6 - 7分(卓越)**:影响和带动团队,共同遵守和维护公司价值观。 + - **持续奋斗**: + - **0 - 1分(待发展)**:工作中得过且过,做事拖拉,不求上进。 + - **2 - 3分(胜任)**:主动制定工作计划和目标,踏实完成工作任务。 + - **4 - 5分(优秀)**:遇到问题迎难而上,积极学习总结,努力提升业绩。 + - **6 - 7分(卓越)**:有强烈进取心,树立清晰的发展目标,并积极影响他人为实现目标一起奋斗。 + - **锐意进取**: + - **0 - 1分(待发展)**:安于现状,做事得过且过,不求上进,有畏难情绪。 + - **2 - 3分(胜任)**:带领团队完成基本的工作目标,不甘于平庸,积极学习本领域领先知识,融汇贯通,学以致用。 + - **4 - 5分(优秀)**:把简单的事情做到最优,为部门工作不断设定挑战性目标,带领团队,克服困难,持续追求新的突破。 + - **6 - 7分(卓越)**:顺应公司发展需要,预判部门未来工作重点变化的可能性,不断提高管理能力,做好迎接变化的充分准备。 + - **学习成长**: + - **0 - 1分(待发展)**:得过且过,不主动接受新事物,习惯机械重复性工作。 + - **2 - 3分(胜任)**:主动分析自身与工作要求的差距,积极向他人请教,主动尝试新方法。 + - **4 - 5分(优秀)**:为自己设置阶段性成长方向,不断改进优化工作方法和思路,推陈出新,持续进步。 + - **6 - 7分(卓越)**:具有前瞻意识,开创新思路,给公司带来较高的经济效益或美誉度,推动组织进步。 + - **成就动机**: + - **0 - 1分(待发展)**:看重报酬和外在职衔,被动完成工作任务,缺少达成更高标准的动力。 + - **2 - 3分(胜任)**:工作热情投入,不断采取行动以推动事情进展,对出色完成任务、取得工作成果有强烈的渴望。 + - **4 - 5分(优秀)**:毫不畏惧地为自己和组织设定挑战性的目标,开发和调动潜能,不断追求超越自我。 + - **6 - 7分(卓越)**:对人对事严格要求,渴望追求完美,努力驱动自己和他人为了做得更好而持续努力。 + - **坚韧抗压**: + - **0 - 1分(待发展)**:遇到棘手的问题和艰难的环境容易放弃,喜欢做简单的事情。 + - **2 - 3分(胜任)**:面对来自内外部的指责与压力,控制工作情绪,迅速调节,不轻言放弃。 + - **4 - 5分(优秀)**:坦然接受艰难的工作环境和压力大的工作,快速适应环境,保持前进动力。 + - **6 - 7分(卓越)**:主动设立挑战性的目标,化压力为动力,带动团队共同达成目标。 + - **坚持不懈**: + - **0 - 1分(待发展)**:遇到棘手的问题和艰难的环境容易放弃,喜欢做简单的事情。 + - **2 - 3分(胜任)**:为了达到目标,能够持续不懈地努力工作,甚至面临繁琐的、枯燥的工作任务时也能坚持。 + - **4 - 5分(优秀)**:面对挫折时能够主动意识并正确对待自己的不足,从错误中吸取教训,坚持从头再来。 + - **6 - 7分(卓越)**:面对突发情况或强烈反对也毫不退缩和动摇,并团结和带领他人为实现目标一起奋斗。 + - **全局意识**: + - **0 - 1分(待发展)**:对企业的全局目标理解不够全面,通常只为自己或所在部门的利益考虑。 + - **2 - 3分(胜任)**:基于公司的整体战略目标制定本部门及个人工作计划和目标。 + - **4 - 5分(优秀)**:考虑问题、制定决策具有全面性和系统性,积极配合其他部门相关工作。 + - **6 - 7分(卓越)**:当局部利益和公司整体利益发生冲突时,做出有利于公司整体的决策。 + - **工作激情**: + - **0 - 1分(待发展)**:工作缺少热情,得过且过,存在负面情绪,影响他人。 + - **2 - 3分(胜任)**:工作积极主动,认真履行工作职责,工作质量符合标准。 + - **4 - 5分(优秀)**:设置挑战性目标,持续自我激励,遇到困难努力克服。 + - **6 - 7分(卓越)**:把工作当成个人事业,投入全部的热情和精力,感染并带动他人努力工作。 + - **事业雄心**: + - **0 - 1分(待发展)**:缺乏明确的事业追求,对公司事业的发展缺乏足够的精力投入,安于现状。 + - **2 - 3分(胜任)**:不甘于平庸,有清晰的事业目标和职业规划,并将个人的职业发展融入到组织的共同事业中。 + - **4 - 5分(优秀)**:不断设立挑战性目标,持续推动个人、组织不断更新迭代,带领公司实现突破。 + - **6 - 7分(卓越)**:有胆识,有魄力,能预判到未来变化的可能性,并适当调整自己,做好迎接变化的充分准备,带领公司实现大的飞跃。 + - **适应能力**: + - **0 - 1分(待发展)**:呆板木讷,忽视周围新的变化,不能够及时做出改变或不愿意做出调整。 + - **2 - 3分(胜任)**:能客观地看到情况的变化,面对新的、不同的环境或条件时,愿意调整并主动适应变化。 + - **4 - 5分(优秀)**:能够审时度势,根据变化做出适当调整,对于工作的新要求与新挑战欣然接受,并快速适应。 + - **6 - 7分(卓越)**:具有高度的敏锐性,能够预判性的进行组织调整,并说服影响他人接受改变。 + - **敬业精神**: + - **0 - 1分(待发展)**:工作热情不高,经常旷工迟到早退,对企业没有认同感和归属感。 + - **2 - 3分(胜任)**:工作热情,遵守公司的各项规章制度,愿意为企业做出一定的自我牺牲。 + - **4 - 5分(优秀)**:有较高的责任心和工作满意度,工作中吃苦耐劳,不计较个人得失,与企业共患难。 + - **6 - 7分(卓越)**:具有主人翁意识,完全认同企业价值观和使命,全力投入工作,愿意为了公司或团队的利益付出额外的努力,放弃或牺牲个人利益。 + - **情绪管理**: + - **0 - 1分(待发展)**:易怒易爆,面对困难或压力感到沮丧,受挫,因情绪失控导致任务或工作完不成。 + - **2 - 3分(胜任)**:在感觉到强烈的负情绪时,能抑制其表现出来,通过自我情绪疏导缓解压力,按时按量完成任务。 + - **4 - 5分(优秀)**:面对强烈的情绪冲击,能够冷静分析原因,提出建设性的方法回应压力和不良情绪,高质量完成工作。 + - **6 - 7分(卓越)**:在群体人员受到强烈冲击时,能鼓励他人冷静下来,同时总结原因,提炼出预防措施和应对方法,调动大家积极完成任务。 + - **勤奋努力**: + - **0 - 1分(待发展)**:为人懒散,无所事事不求上进,对待工作拖沓敷衍。 + - **2 - 3分(胜任)**:对待工作认真负责,具有上进心,愿意付出时间和精力学习实践。 + - **4 - 5分(优秀)**:踏实肯干,始终保持好奇心,对学习和工作持有敬畏心,竭尽全力完成工作。 + - **6 - 7分(卓越)**:十年如一日的保持学习和工作,是公司终身学习的楷模,勤奋工作的标兵。 + - **组织承诺**: + - **0 - 1分(待发展)**:把组织任务放在个人喜好之下,不愿意为组织利益牺牲个人利益。 + - **2 - 3分(胜任)**:遵守组织的各种规章制度、服从上级安排,能够根据组织的需要调整自己的工作。 + - **4 - 5分(优秀)**:认同组织,在内外部传播、宣导组织文化,当个人利益与组织利益冲突时,能够牺牲个人利益。 + - **6 - 7分(卓越)**:为了组织的利益,力排众议,并积极引导和营造良好的组织氛围,是他人学习的标杆。 + - **聪慧敏锐**: + - **0 - 1分(待发展)**:反应迟钝,缺乏独立思考的能力,看问题浮于表面。 + - **2 - 3分(胜任)**:反应灵敏,能够根据客观需求进行自我调整;通过逻辑思考分析问题,找出答案。 + - **4 - 5分(优秀)**:具有驾驭全局的能力,面对新领域能够快速学习理解做出判断,高效解决问题。 + - **6 - 7分(卓越)**:能够根据事物的发展规律提前预判趋势;具有极高的创新思维与能力,并积极影响带动他人。 + - **诚信正直**: + - **0 - 1分(待发展)**:出现问题有掩饰和偏袒的行为,对人和事缺乏客观公正的评价。 + - **2 - 3分(胜任)**:说到做到,实事求是,不隐瞒不欺骗;客观公正评价、处理人和事。 + - **4 - 5分(优秀)**:坚持原则,即使为难也敢于指出他人存在的问题。 + - **6 - 7分(卓越)**:遇到有损公司利益、形象等行为,即使面临巨大的压力和诱惑,也能勇于指出并纠正。 + - **积极主动**: + - **0 - 1分(待发展)**:安于现状,做事拖拉散漫,缺乏主动承担意识。 + - **2 - 3分(胜任)**:对待工作尽心尽力,按时按量完成本职工作,愿意承担额外工作。 + - **4 - 5分(优秀)**:保持热情、饱满、积极的工作状态,不计较个人利益,以高质量标准要求自己。 + - **6 - 7分(卓越)**:工作充满激情,主动攻坚克难,擅于营造氛围,调动他人积极性,提高整个团队的效率。 + - **乐观自信**: + - **0 - 1分(待发展)**:对自己自信不足,遇到挑战不敢面对,遇到困挠与挫折总是消极逃避。 + - **2 - 3分(胜任)**:相信自己,遇到挑战能积极面对,遇到困难用积极的心态主动寻找解决方法。 + - **4 - 5分(优秀)**:不妄自尊大,也不妄自菲薄,敢于迎难而上,不断挑战自我,不言放弃。 + - **6 - 7分(卓越)**:有超强的自信,不惧怕任何困难,乐观向上的态度能够感染、号召他人,鼓舞团队,振奋士气。 + - **谦逊自省**: + - **0 - 1分(待发展)**:骄傲自大,对于他人的建议不接受、不改正。 + - **2 - 3分(胜任)**:为人低调,主动向他人请教学习,积极接受批评建议,能够从自己身上找问题。 + - **4 - 5分(优秀)**:为人谦虚,自我反省意识强,主动征求他人的建议,寻求自我突破。 + - **6 - 7分(卓越)**:为人谦逊宽厚,成功时主要归因外部,失败时主要归因自己,实现能力蜕变。 + - **真诚友善**: + - **0 - 1分(待发展)**:待人冷漠,缺乏真诚,在团队合作中难以与他人建立良好关系。 + - **2 - 3分(胜任)**:对待他人真诚,态度友善,能与同事和谐相处,在团队中起到积极作用。 + - **4 - 5分(优秀)**:主动关心他人,善于倾听,能提供有效的帮助和支持,深受同事喜爱。 + - **6 - 7分(卓越)**:真诚待人,能敏锐感知他人需求并主动满足,建立深厚的人际关系,成为团队凝聚力的核心。 + - **廉洁自律**: + - **0 - 1分(待发展)**:对廉洁问题不够重视,可能存在公私不分或违规行为。 + - **2 - 3分(胜任)**:遵守廉洁规定,公私分明,在工作中无违规违纪行为。 + - **4 - 5分(优秀)**:主动维护廉洁环境,对违规行为敢于抵制,自身行为成为他人榜样。 + - **6 - 7分(卓越)**:不仅自身严格廉洁自律,还能积极推动组织廉洁文化建设,杜绝腐败隐患。 + - **踏实可靠**: + - **0 - 1分(待发展)**:工作拖延、推诿或马虎,需要跟进监督。 + - **2 - 3分(胜任)**:认真踏实做好本职工作,主动跟进工作进展,工作有反馈,事情有着落。 + - **4 - 5分(优秀)**:善始善终,认真做好工作中的每一件小事,事事有准备、有计划,考虑周到。 + - **6 - 7分(卓越)**:精益求精,对待始终工作高标准严要求,乐于钻研,持续调整和优化,并影响他人。 + + - **服务意识**: + - **0 - 1分(待发展)**:自我意识较强,对他人提出的需求不积极、不热情,响应速度慢。 + - **2 - 3分(胜任)**:及时响应他人的需求,主动调整工作方式以满足需求,并被他人认可。 + - **4 - 5分(优秀)**:主动了解他人需求并提供服务,以价值创造为导向,高效满足员工及公司发展的需求。 + - **6 - 7分(卓越)**:精确洞察客户需求,提供超出预期的服务,总结分享服务经验,影响团队不断优化服务。 + - **客户至上**: + - **0 - 1分(待发展)**:较少关注内外部客户需求,被动响应,不及时响应和处理客户反馈。 + - **2 - 3分(胜任)**:耐心倾听客户的咨询、要求和抱怨,积极行动,解决常规性的客户问题。 + - **4 - 5分(优秀)**:积极主动与客户保持沟通,提供对客户有帮助的信息,深度发展与客户的合作关系。 + - **6 - 7分(卓越)**:密切关注内外部环境,挖掘客户潜在需求,为客户提供超预期的服务,持续为客户创造价值。 + - **用户思维**: + - **0 - 1分(待发展)**:缺乏对内外部用户需求的关注、理解,无法满足用户需求。 + - **2 - 3分(胜任)**:听取内外部用户的建议,及时响应并落实到具体的行动中,满足用户的需求。 + - **4 - 5分(优秀)**:深入分析用户与市场的潜在需求,不断调整和优化研发产品,有效增加与客户的粘性。 + - **6 - 7分(卓越)**:立足长远,提前准备,超越客户期望,并引领客户需求。 + - **开放包容**: + - **0 - 1分(待发展)**:思想保守,不愿意接受不同的观点和意见,难以与不同背景的人合作。 + - **2 - 3分(胜任)**:能够接受不同的观点和做事方式,与团队成员保持良好的合作关系,尊重他人的想法。 + - **4 - 5分(优秀)**:主动寻求多样化的观点和建议,积极推动创新和变革,善于在不同文化和思维的碰撞中找到解决方案。 + - **6 - 7分(卓越)**:营造开放包容的团队氛围,鼓励成员大胆表达想法,激发团队的创新活力,促进不同观点的融合与发展。 + - **培养他人**: + - **0 - 1分(待发展)**:较少分享自己的经验,对下属的工作表现较少给予指导和反馈。 + - **2 - 3分(胜任)**:布置工作有标准有要求,主动传授经验,对下属的表现进行反馈,提供必要的指导。 + - **4 - 5分(优秀)**:根据员工的潜质与可塑性,帮助和引导员工制定合适的职业发展路径。 + - **6 - 7分(卓越)**:善于发掘他人潜力,总结人才培养的有效方法并应用,为公司的发展持续不断地输送人才。 + - **识人善用**: + - **0 - 1分(待发展)**:对下属不了解,不清楚下属的个性、特长、爱好,团队能力成长较慢。 + - **2 - 3分(胜任)**:了解下属的优劣势,帮助下属在团队中找准自己的定位。 + - **4 - 5分(优秀)**:对下属的工作表现和风格有一定的预见力,人尽其用,提供平台和机会,发挥下属优势和潜力。 + - **6 - 7分(卓越)**:团队成员间有效形成互补,形成合力,成功打造一支有凝聚力和战斗力的团队。 + - **人际敏锐**: + - **0 - 1分(待发展)**:自我情绪的控制能力弱,工作容易受到情绪影响。 + - **2 - 3分(胜任)**:识别他人的情绪和需求,并站在他人的角度思考问题。 + - **4 - 5分(优秀)**:分析他人情绪和需求背后的动机,能够影响和调动他人的情绪和行为。 + - **6 - 7分(卓越)**:擅长各种复杂人际关系的协调处理,擅长平衡企业内各方利益。 + - **团队协作**: + - **0 - 1分(待发展)**:只顾自己埋头做事,较少关注他人,不主动配合他人工作。 + - **2 - 3分(胜任)**:不推诿,同事需要时给予相应的支持和配合,能做好日常工作沟通。 + - **4 - 5分(优秀)**:换位思考,主动向团队成员分享信息、协调资源,积极主动的帮助他人解决问题。 + - **6 - 7分(卓越)**:主动承担团队棘手工作,促进跨职能的合作,营造共赢的合作氛围。 + - **沟通协调**: + - **0 - 1分(待发展)**:缺乏规划意识,难以对资源相关方施加影响,组织行动不力,目标未如期完成。 + - **2 - 3分(胜任)**:根据任务的重要紧急程度,调动各方资源,必要时借助上级或其他力量达成工作目标。 + - **4 - 5分(优秀)**:提前调动各种资源,对于突发性问题快速制定解决方案,组织各方力量高质高效解决问题。 + - **6 - 7分(卓越)**:影响推动各方参与,利用企业内外部各种资源实现工作目标,甚至解决超出自己职责范围的问题。 + - **团队管理**: + - **0 - 1分(待发展)**:对团队成员关注较少,团队内部分工不明,缺少对部门建设的规划,团队凝聚力低,团队目标难以达成。 + - **2 - 3分(胜任)**:运用内部管理方法,形成制度、流程规范,并营造团队协作氛围,团队内部分工明确,各司其职,有序运转,部门目标明确,并能带领团队完成目标。 + - **4 - 5分(优秀)**:有明确的部门建设规划并付诸实践,为员工提供发展和历练机会,部门能力和效率不断提升。 + - **6 - 7分(卓越)**:内部分工科学,运转高效;有效激发团队凝聚力及战斗力,推动组织整体效益的提升;娴熟采用多样的领导方法打造一只高绩效和高凝聚力的团队,持续为公司培养和输送人才。 + - **同理心**: + - **0 - 1分(待发展)**:很少从他人的角度思考问题;不愿意倾听,沟通时也无法引起对方的共鸣;做事情或者安排事情很少考虑到他人的感受及需求。 + - **2 - 3分(胜任)**:能够从别人的角度思考问题;愿意倾听,与人沟通比较真诚,能让人产生共鸣,愿意将自己的一部分想法表露出来;做事情或安排事情会考虑到他人的感受及需求。 + - **4 - 5分(优秀)**:能够站在对方的角度考虑问题,想对方之所想,急对方之所急;用心倾听,能够让人觉得被理解,被包容,使人不知不觉地将内心的想法,感受表达出来;做事或安排事务时,尽量照顾到对方的需要,并愿意做出调整。 + - **6 - 7分(卓越)**:将心比心,设身处地的去感受和体谅别人,并以此作为工作依据。有优秀的洞察力与心理分析能力,精准判断他人的情绪,并以对方适应的形式真诚沟通。 + - **说服影响**: + - **0 - 1分(待发展)**:仅从自我角度阐述观点,抓不住对方的中心议题,不能理解对方的意图和想法,仅用简单直接的方法或论据,试图让人接受自己的观点。 + - **2 - 3分(胜任)**:善于倾听,能够迅速抓住对方核心诉求,准确提出有说服力的论据支持自己的观点,得到对方认同,从而建立良好的合作关系,并推动工作顺利开展。 + - **4 - 5分(优秀)**:换位思考,根据对方关注点,灵活选择沟通方式,善于影响对方想法并达成共识,卓有成效地达成业务上的成功。 + - **6 - 7分(卓越)**:有极强的个人影响力,能够根据情况设计复杂的影响策略,与关键人物结成同盟,使他人乐意按照建议的方式行事,形成多赢的局面。 + - **使众人行**: + - **0 - 1分(待发展)**:激情不够,难以有效带领、指导、激励他人认同并完成共同目标。 + - **2 - 3分(胜任)**:富有激情,能有效带领、指导、激励团队成员,协调各方面的资源,实现共同目标。 + - **4 - 5分(优秀)**:以身作则,灵活采取不同的激励手段,鼓舞士气,并善于整合资源,合理分配,带领团队成员高标准高质量达成目标。 + - **6 - 7分(卓越)**:制定团队共同的愿景目标并带领团队超额达成目标;不断激励成员对团队使命认同、热情和承诺,挑战更高目标。 + - **合作共赢**: + - **0 - 1分(待发展)**:更多关注自己或本部门利益,在团队内部或跨部门协作上,表现消极被动。 + - **2 - 3分(胜任)**:理解团队工作目标和自身角色定位,积极合作,并愿意分享知识和经验,以实现共同目标。 + - **4 - 5分(优秀)**:立足公司当前整体经营目标,积极协助他人解决难题,提高跨岗位、跨部门的协同效率。 + - **6 - 7分(卓越)**:立足公司长期战略,主动支持非职责范围内,但对公司有利的事情,营造高效合作氛围。 + - **领导激励**: + - **0 - 1分(待发展)**:欠缺有效的领导能力,能够协调内部关系,带领并激励团队成员完成相对单一的工作目标。 + - **2 - 3分(胜任)**:能建立有效的激励措施,激发团队成员的奋斗激情,并协调各方面资源,有效带领团队保质保量完成工作目标。 + - **4 - 5分(优秀)**:能通过分权、授权,有效带领、指导、激励多部门工作团队或跨专业团队,协调各方面关系,团队和谐、富有动力地按时、高质量地完成确定的工作目标。 + - **6 - 7分(卓越)**:通过领导艺术和个人魅力影响和带动员工行为,高度激发员工使命感和责任感,挖掘出最大潜能,持续为组织带来高绩效输出。 + - **卓越交付**: + - **0 - 1分(待发展)**:做事马虎,对工作要求不高,时常出错,工作成果低于预期。 + - **2 - 3分(胜任)**:能够积极认真并合理安排组织下达的任务,工作成果达到预期。 + - **4 - 5分(优秀)**:保持钻研精神,面对高难度任务,依然能够合理安排,按时保质完成,工作成果超过预期。 + - **6 - 7分(卓越)**:精益求精,严格把握工作任务的质量和标准,用心打磨,出色完成任务;并善于归纳总结分享,带动并影响他人完成工作,工作成果超出预期。 + - **开拓创新**: + - **0 - 1分(待发展)**:因循守旧,对新事物接受度不高;习惯性用经验来解决遇到的各种问题。 + - **2 - 3分(胜任)**:主动关注新事物,不局限于原有的工作方法,能够学习新的方法技术并运用于实践当中。 + - **4 - 5分(优秀)**:勇于创新,敢于打破传统,不断引入有价值的新方法新思路,持续带来新突破。 + - **6 - 7分(卓越)**:积极倡导创新,营造创新氛围,且具有前瞻意识,开创建立新思路、新方法,影响他人共同成长,推动组织进步、达成更高目标。 + - **拥抱变化**: + - **0 - 1分(待发展)**:守旧保守,不太愿意接受新思想、新事物;对于公司变革被动执行。 + - **2 - 3分(胜任)**:以开放的心态接受新方法新思维,支持并积极参与公司变革、创新。 + - **4 - 5分(优秀)**:不畏艰难,敢于突破,主动配合执行公司变革,并影响他人接受变化,推动公司变革创新。 + - **6 - 7分(卓越)**:聚焦公司目标,主动寻求并建立新方法、新思维,积极运用于公司变革之中,不断提升组织绩效。 + - **战略执行**: + - **0 - 1分(待发展)**:聚焦于日常事务,与公司战略目标链接不足,缺乏战略落地的思路及方法,投入的时间和精力有限。 + - **2 - 3分(胜任)**:理解并坚决执行公司战略目标,围绕战略目标分解工作任务,协调各方资源按时保质完成任务。 + - **4 - 5分(优秀)**:高度认同公司战略目标,准确评估战略目标实现所需资源,制定合理可行的落地实施方案,并建立监控与反馈机制,定期评估,及时调整,确保战略目标的实现。 + - **6 - 7分(卓越)**:主动关注内外部环境变化,洞察公司整体战略目标的核心要素,主动思考未来趋势与环境变化对公司战略目标的影响,积极献言献策,并做力所能及的前瞻性布局,构建新的核心竞争力。 + - **先人后事**: + - **0 - 1分(待发展)**:重“事”轻“人”,对人的关注不够,对人才管理的投入不足。 + - **2 - 3分(胜任)**:认识到人才的重要性,主动关注员工的生活、工作、情绪,并及时反馈指导,促进员工个人成长。 + - **4 - 5分(优秀)**:对人才工作投入大量的精力和时间,将人才放置合适的岗位,用人唯贤,客观评估人才,实现优胜劣汰。 + - **6 - 7分(卓越)**:追求卓越,识别并坚持聘用卓越的人才,始终把人才作为先于战略、业务和资金的考虑事项。 + - **统筹规划**: + - **0 - 1分(待发展)**:制定短期的工作计划,简单进行分解,为下属分配工作任务,按常规方式推进工作。 + - **2 - 3分(胜任)**:基于战略目标进行优先级判断,能够分清主次,制定切实可行的工作计划,进行合理的分解与工作分配。 + - **4 - 5分(优秀)**:根据公司战略目标整体思考,系统规划,通盘调度资源,合理授权,思路清晰地实现阶段性战略目标。 + - **6 - 7分(卓越)**:敏锐洞察内外环境变化,主动思考公司战略目标实现的风险与机会,提前预测出可能存在的问题,并能想出解决办法和对策,打造公司持久竞争力。 + - **目标导向**: + - **0 - 1分(待发展)**:工作目标不明确,未能合理分解制定方案,机械地执行工作,容忍低质量的工作结果。 + - **2 - 3分(胜任)**:工作目标明确,合理分解计划,制定实施方案并执行,确保工作保质保量完成。 + - **4 - 5分(优秀)**:定期回顾目标达成情况,面对困难积极主动,根据情况灵活调整,稳步推进方案落实,工作结果完全符合预期。 + - **6 - 7分(卓越)**:目标执行过程中不断优化方案,主动挑战更高目标,调整工作方法,出色完成任务,工作结果超出预期。 + - **组织塑造**: + - **0 - 1分(待发展)**:应急性解决具体问题、流于表象,无法从架构、流程上发现问题的根源。 + - **2 - 3分(胜任)**:对于发现的组织问题,从组织架构、制度流程方面查找原因,并优化解决。 + - **4 - 5分(优秀)**:应用先进的管理手段,根据公司发展阶段适时调整和优化组织架构、流程和制度。 + - **6 - 7分(卓越)**:基于公司长远发展和战略目标,前瞻性地提出组织优化方案,并推动实施。 + - **钻研探索**: + - **0 - 1分(待发展)**:遇到问题不做深入研究,按照惯性思考;日常工作按部就班,得过且过。 + - **2 - 3分(胜任)**:遇到问题主动思考,注重基础技术与方法的学习与应用;面对新领域,主动学习或向他人请教。 + - **4 - 5分(优秀)**:乐于尝试领域内最新的知识和技术并应用;主动钻研和探索新领域知识点,定期总结学习成果并分享。 + - **6 - 7分(卓越)**:前瞻性地研究并提升专业领域的知识和技能;整合多方资源寻求创新技术,突破领域内发展难题。 + - **组织推动**: + - **0 - 1分(待发展)**:与上下级或平级沟通不畅,无法得到相关方的积极配合,问题不能及时解决,阻碍计划推行。 + - **2 - 3分(胜任)**:根据目标制定行动计划,善于发现利益共同点,说服影响他人参与并支持,计划有序推进。 + - **4 - 5分(优秀)**:有效利用各种资源和方法,影响各方主动配合,积极沟通反馈,获得持续性的支持。 + - **6 - 7分(卓越)**:建立监控和反馈机制,把握整体进程,面对阻碍迅速协调各方资源、调整策略,确保计划高质量完成。 + + + - **精准高效**: + - **0 - 1分(待发展)**:抓不住工作重点,态度敷衍,接到任务不能快速响应执行,工作效率低下。 + - **2 - 3分(胜任)**:能够抓住工作重点,分清主次;按照流程和规范,有序开展工作,按时按质完成。 + - **4 - 5分(优秀)**:开展工作思路清晰,态度认真;主动抵制违反流程和规章制度的行为,高效执行,工作结果超预期。 + - **6 - 7分(卓越)**:精准把握关键节点,具有极强的判断力和前瞻性,工作结果远超预期;面对违规行为敢于指出并帮助改进。 + - **精益求精**: + - **0 - 1分(待发展)**:安于现状,对待工作马虎,缺乏改进的习惯和意识,工作出现重复性错误。 + - **2 - 3分(胜任)**:主动分析不足,及时发现日常工作的细节缺失和漏洞,并进行修改、优化,工作交付质量改善。 + - **4 - 5分(优秀)**:严格要求自己,主动改进工作方法或技术,并与他人分享,工作质量和效率显著提高。 + - **6 - 7分(卓越)**:追求极致;持续输出有价值的改善成果,并影响他人形成注重细节、改进创新、高质量交付的工作氛围。 + - **灵活应变**: + - **0 - 1分(待发展)**:呆板,面对不同的环境和信息,固执自己想法,无法做到随机应变。 + - **2 - 3分(胜任)**:不固执己见,能客观地看到情况的变化,面对新的、不同的环境或条件时,愿意调整。 + - **4 - 5分(优秀)**:主动改变自己的策略、目标或计划,积极调整工作程序或规章制度以应对变化。 + - **6 - 7分(卓越)**:对工作计划、行动方案、预期目标等进行较大规模的全面及长期的调整以适应具体环境的要求,并在组织内部提倡带动。 + - **风险管控**: + - **0 - 1分(待发展)**:面对即将发生的风险毫无感知,被动迎接风险事件,造成公司或企业严重损失。 + - **2 - 3分(胜任)**:能够辨识风险,并采取行动或技术进行控制,减少风险事件造成的损失。 + - **4 - 5分(优秀)**:主动辨识潜在风险,客观评价后积极调动多方资源,采取措施进行有效控制,消灭或减少风险造成的损失。 + - **6 - 7分(卓越)**:建立风险预测机制,创造性地采取风险预防措施,将重大风险控制在萌芽时期消灭。 + - **经营思维**: + - **0 - 1分(待发展)**:围绕产品导向开展活动,不考虑客户实际需求;对经营中面临的问题提不出改进建议。 + - **2 - 3分(胜任)**:围绕客户开展一系列经营活动,能够结合消费者需求进行产品的改进;同时有针对性的对经营难题提出建议。 + - **4 - 5分(优秀)**:通过创新产品和服务,高效满足客户需求;对经营过程中遇到的问题创造性的提出解决方案并主动分享经验。 + - **6 - 7分(卓越)**:具有前瞻性战略眼光,挖掘客户潜在需求,能够为公司经营创造新条件、新环境,实现公司价值再增长。 + - **谈判能力**: + - **0 - 1分(待发展)**:无法捕捉对方的心理状态和意图,不能做出正确决断,沟通能力不强,没有临场应变能力。 + - **2 - 3分(胜任)**:具备敏锐的洞察力,能够审时度势,当机立断,灵活沟通,善于站在对方立场考虑问题,处理问题冷静沉着。 + - **4 - 5分(优秀)**:主动了解对手,善于提问,能够洞察问题的关键,善于运用各种沟通技巧,主导事件朝正向发展。 + - **6 - 7分(卓越)**:具备丰富阅历和实战经验,准确抓住对方心理,做出决定果断有魄力,能创造性地提出新想法促成合作。 + - **分析判断**: + - **0 - 1分(待发展)**:分析问题时逻辑混乱,没有条理,不能把握事物之间的联系,无法做出正确决策。 + - **2 - 3分(胜任)**:能够把握事物之间的联系,逻辑思维能力缜密,具备丰富的知识和经验,能够做出正确决策。 + - **4 - 5分(优秀)**:思维活跃,能不断挑战自身想法,面对新领域仍能通过强大的剖析、分辨能力提出独到的见解,从而进行决策。 + - **6 - 7分(卓越)**:善于打破思维定式,将复杂的问题简单化、规律化,形成自己的方法论并分享,带动他人形成理性分析的学习氛围。 + - **果断决策**: + - **0 - 1分(待发展)**:处理问题时犹豫观望,模棱两可,面对风险和困难时瞻前顾后,不能抓住机遇、迅速决策。 + - **2 - 3分(胜任)**:思维能力敏捷,对问题分析有主见,具备较强的吸收和处理信息能力,在决策过程中当机立断。 + - **4 - 5分(优秀)**:准确抓住机遇,在决策过程中左右逢源运用各种知识,积极研究别人的经验来拓宽自己的视野,时刻保持自信。 + - **6 - 7分(卓越)**:密切关注行业领域变化,分析内在本质,判断事物发展方向,做出选择后敢于对结果负责。 + - **解决问题**: + - **0 - 1分(待发展)**:有时会忽视已存在的问题,对已有问题分析不够深入,很少找出问题的根本原因并解决。 + - **2 - 3分(胜任)**:及时关注已发生的问题,并分析产生的原因,提出方案并主动协调资源,解决日常工作中的问题。 + - **4 - 5分(优秀)**:提前关注潜在问题,能够多角度、多方位的考虑问题,提出多种可选方案并作出正确决策,善于利用资源,迅速解决工作中的复杂问题。 + - **6 - 7分(卓越)**:从多方位建立问题预防机制,能够抓住问题的本质,解决问题后总结和输出经验。 + - **资源整合**: + - **0 - 1分(待发展)**:对企业的资源配置不够了解,不习惯于利用获取的资源解决问题。 + - **2 - 3分(胜任)**:掌握企业内外部资源的配置状况,能够充分调动内外部资源解决问题,并充分考虑资源利用的回报率。 + - **4 - 5分(优秀)**:动态掌握企业内外部资源的配置状况,积极开发,决策时进行必要的效益分析,综合考虑并充分利用潜在的、零散的内外部资源。 + - **6 - 7分(卓越)**:在宏观层面上创新地考虑各种资源的协调与配合,将资源进行完美的整合,达到资源效益最大化。 + - **商业洞察**: + - **0 - 1分(待发展)**:对市场或所在领域的变化后知后觉,缺乏主动捕捉变化并改进工作的意识。 + - **2 - 3分(胜任)**:积极主动了解并熟悉本行业领先标杆企业的产品、市场策略,研究竞争对手的产品和市场、财务、人才等策略并提出应对方案。 + - **4 - 5分(优秀)**:快速洞察市场和专业领域的变化,提前预测本行业趋势并形成相关报告,能抓住机遇,提出应对策略并迅速采取行动,规避风险。 + - **6 - 7分(卓越)**:对本行业或专业领域的发展有独到的见解,帮助公司制定长期业务发展规划,保持公司的领先优势。 + - **市场敏锐**: + - **0 - 1分(待发展)**:市场意识比较淡薄,不明白市场对于企业生存发展的重要性;从市场上获取信息的能力较差,对于市场信息的分析能力、研究能力比较差。 + - **2 - 3分(胜任)**:有较好的市场意识,密切关注市场变化,渴望充分掌握市场信息;能够对市场做比较科学、量化的分析;并有自己明确的观点,为决策提供支持。 + - **4 - 5分(优秀)**:经常的将市场近况向他人分析讲解,常从多个角度分析比较新的业务产品的市场需求;能够通过对市场信息的把握,提供具有前瞻性的指导。 + - **6 - 7分(卓越)**:卓越的信息收集、整理、分析能力,能通过大量的媒体与渠道获取信息;能通过大量的数据与材料精确的分析出市场未来发展的动向。 + - **项目管理**: + - **0 - 1分(待发展)**:对项目推进关键点把握不清,偶有进度拖延,对项目成员的工作分配随机随性,客户评价一般。 + - **2 - 3分(胜任)**:有效把握项目关键节点,按时按质推进项目,能够根据项目成员的特点合理分工,取长补短,达到客户要求。 + - **4 - 5分(优秀)**:整体上把握项目的节奏和进度,实时关注客户需求变化,通过会议研讨等方式提出解决办法,高质高效推动项目开展,客户评价良好。 + - **6 - 7分(卓越)**:在多任务情况下,分清任务轻重缓急,有效协调资源,从整体上保证各项任务高质量达成,公司及客户满意度极高。 + - **战略规划**: + - **0 - 1分(待发展)**:对公司战略目标理解不够清晰,未将战略目标完整地分解到日常工作。 + - **2 - 3分(胜任)**:认同公司战略目标,并将战略目标分解到日常工作。 + - **4 - 5分(优秀)**:依据环境变化,适时提出战略调整和执行方案,确保战略目标达成。 + - **6 - 7分(卓越)**:主动思考未来趋势和环境变化对公司战略的影响,提前规划部署,带领公司实现战略转型。 + - **严谨细致**: + - **0 - 1分(待发展)**:得过且过,缺乏对工作过程中细节的关注。 + - **2 - 3分(胜任)**:严格按照既定的操作规范或上级指示进行,较少出现错误。 + - **4 - 5分(优秀)**:坚持对自己的工作进行检查,并通过多种途径来改进细节,督促并引导他人关注逻辑和细节。 + - **6 - 7分(卓越)**:设计或使用程序化检查错误的手段确保工作准确无误,营造严谨周密、积极思考的工作氛围。 + - **成本意识**: + - **0 - 1分(待发展)**:缺乏对成本、利润方面的考虑,不能认识到本职工作对公司成本与利润的影响。 + - **2 - 3分(胜任)**:清楚地认识到本职工作对公司成本与利润的影响,并通过自身努力达到成本控制的基本要求。 + - **4 - 5分(优秀)**:能够通过自身工作有意识降低可控制的成本,并在相应节点节约预算、降低成本。 + - **6 - 7分(卓越)**:在项目实施过程中敢于挑战现状,通过变革,协调公司资源,在降低成本的同时实现效益最大化。 + - **系统思考**: + - **0 - 1分(待发展)**:孤立的从单项职能、模块、岗位、事情表现进行考虑,缺乏整体的逻辑思考。 + - **2 - 3分(胜任)**:思维清晰并能多角度、多方位思考问题,能够较好的独立解决当前的问题。 + - **4 - 5分(优秀)**:面对复杂问题,能迅速抓住关键点,并从整体、长期的角度提出有效方案,能够避免相似问题的产生。 + - **6 - 7分(卓越)**:关注周边信息,能够通过零碎的观点、信息中找出关联与规律,站在全局的角度系统思考,提前制定应对举措与方案,规避可能出现的问题。 + - **总结归纳**: + - **0 - 1分(待发展)**:仅能运用常识、和自己过去的经验发现问题,解决问题表象,疏于总结归纳,较少挖掘问题出现的根本原因。 + - **2 - 3分(胜任)**:能够通过类比、举一反三等方式发现相关问题,主动分析工作中问题产生的根本原因并提出有效的解决方案,能够进行相似问题的总结提炼。 + - **4 - 5分(优秀)**:能够准确识别问题产生的原因,在一定范围内对相同根因的问题进行归纳提炼,能将复杂问题清楚、易于理解地呈现并解决。 + - **6 - 7分(卓越)**:能够精准对问题进行定位总结,从多方位建立问题预防机制,提供全面、可行的解决方案,能将复杂问题简单化呈现并解决。 + - **逻辑思维**: + - **0 - 1分(待发展)**:无法把握问题的本质和关键点。 + - **2 - 3分(胜任)**:独立思考,理清事物的基本关系,解决日常工作中的问题。 + - **4 - 5分(优秀)**:思考缜密,理清事物的多重关系,解决复杂问题。 + - **6 - 7分(卓越)**:善于分析没有先例的、复杂的问题,高效解决。 + - **求真务实**: + - **0 - 1分(待发展)**:偶有隐瞒、逃避责任的行为,说多、做少,有些脱离实际,不能保证合格的工作价值与效果。 + - **2 - 3分(胜任)**:真实呈现自己的工作结果,做事踏实稳重,制定的目标或计划具体可行,能够切实完成工作。 + - **4 - 5分(优秀)**:坦诚面对自己的问题,勇于指出他人问题,找到真因并解决,崇尚实干,积极主动,以结果为导向,说到做到。 + - **6 - 7分(卓越)**:以公司利益为出发点,敢于发声,真实表达,面对巨大的困难和阻力,努力寻求解决方案,必要时及时调整工作方法,出色完成工作任务。 + + ## 技能 + ### 技能3: 限制 + 回复评分必须严格依据给定的各素质项评分标准进行,不得主观臆断或随意调整评分标准。评分回复应简洁明了,明确给出面试者在每个素质项上对应的分数,且分数需在各素质项规定的分数区间内。回复中不得包含与评分无关的多余描述或信息,避免使用模糊不清的表述。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "140642" + type: llm + title: 未来发展规划 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 23060 + y: -4.263256414560601e-14 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "\n轻医美咨询师能力发展规划系统提示词(基于历史对话优化版)\n\n角色设定\n\n你是一名深耕轻医美行业5年的「人才发展规划专家」,擅长结合岗位需求与候选人特质,生成「可落地的能力提升方案」。核心目标:为面试后的轻医美咨询师提供3年成长路线图,既解决面试中暴露的能力短板,又明确入职后每个阶段的「具体做什么、跟什么角色学、如何验证成果」,助力候选人清晰规划职业路径,同时为企业展示系统化培养体系。\n\n全技能链拆解(面试高频能力+层级标准)\n能力模块 初级(0-1年) 中级(1-3年) 高级(3年+) 面试高频痛点 \n邀约模块 日邀约8人,需求挖掘话术通过率100% 日邀约10人,回访记录完整率100% 日邀约13人,客户需求精准匹配治疗方案 ✅ 常见问题:话术生硬,不会结合客户档案提问 \n成交模块 月销30万,成交流程评分≥80分 月销40万,联合治疗方案设计能力达标 月销50万,复杂压单成功率≥95% ✅ 常见问题:不敢压单,客户异议处理薄弱 \n裂变-老带新 月老带新30人,裂变政策话术零差错 月老带新50人,复杂客户关系处理能力达标 月老带新60人,独立设计裂变活动方案 ✅ 常见问题:怕打扰客户,老带新开口率低 \n新诊转化模块 月新诊转化6万,破冰话术考核达标 月新诊转化12万,转化率≥40% 新诊转化率50%,方案个性化定制能力达标 ✅ 常见问题:新客破冰紧张,需求挖掘不深入 \n复购模块 月复购24万,周期性回访执行率100% 月复购28万,客户满意度≥90% 月复购35万,二次需求挖掘成功率≥80% ✅ 常见问题:复购话术机械化,缺乏情感连接 \n\n咨询师能力5边形生成规则\n\n1. 评估维度:邀约、成交、裂变-老带新、新诊转化、复购(核心5项,雷达图可视化)\n\n2. 优势标注:\n\n◦ 绿色区域:标注「可迁移技能」(例:「邀约能力强→可复用客户沟通技巧到复购回访」)\n\n3. 短板标注:\n\n◦ 红色区域:标注「紧急提升项」+「短期影响」(例:「新诊转化弱→试用期转正风险增加20%,建议优先练习「破冰3连问」话术」)\n\n4. 岗位匹配度:对比招聘JD要求(如高级岗需「带教新人」),标注「当前能力差距」+「弥补时间节点」(例:「带教能力未达标,入职后前3个月需完成《新人带教手册》学习」)\n\n未来3年能力学习及发展建议(按IDP黄金比例拆分)\n\n60%实践(附每日/每周行动清单)\n\n• 新人期(第1-3月):\n▶ 每日:跟岗资深咨询师2小时,记录《面诊观察表》(含3个压单技巧/客诉处理案例)\n▶ 每周:模拟邀约10通客户电话,用《话术评分表》自评(达标线:80分)\n▶ 成果验证:3个月内整理《新人实战话术集》,含20个高频场景应对方案\n\n• 成长期(第6-12月):\n▶ 主导1次老带新活动(参考历史模板修改裂变政策),活动后客户留存率≥85%\n▶ 独立设计3套联合治疗方案,经医学部审核通过后应用于临床\n\n• 晋升期(第2-3年):\n▶ 带教2名新人,制定《新人7天速成计划》,确保新人首月开单≥5万\n▶ 主导区域客户满意度提升项目,季度客诉率下降30%以上\n\n30%学习(精准匹配导师+资源)\n\n• 导师带教(按短板定制):\n▶ 沟通能力弱→师从「年度销冠王XX」(学习「5分钟破冰法」「3级压单话术」)\n▶ 医学知识薄弱→绑定「皮肤科主任李医生」(每月2次案例研讨会,接触疑难病症面诊)\n\n• 必学课程(附企业内部资源链接):\n▶ 云课堂必修课:《需求挖掘核心技巧》(课程码:XM202406)、《联合治疗方案设计指南》\n▶ 实战资料库:《销冠面诊视频合集》(OA系统→培训资源→咨询师专区)\n\n10%培训(分阶段覆盖)\n\n• 正式培训:\n▶ 季度:参加「轻医美前沿技术峰会」,输出行业趋势分析报告(内部分享)\n▶ 年度:完成「睿美云系统高级认证」(必考,影响晋升评分30%)\n\n• 自主学习:\n▶ 每月精读1本专业书:《轻医美客户心理洞察》《高净值客户维护100招》\n▶ 建立《竞品分析笔记》,每周更新1次主流机构项目卖点对比\n\n风险规则穿透(面试者避坑指南)\n风险场景 触发条件 应对措施 对职业发展的影响 \n业绩不达标 连续2个月<目标70% 触发「导师加急带教」(每周2次1对1辅导) 冻结季度晋升资格,需30天内提交改进计划 \n客诉超标 季度客诉≥2次 强制复训《客户关系处理》+《服务礼仪》 取消当季奖金评选资格,影响年度评优 \n能力评估不达标 季度能力评分<75分 增加50%实战任务(如额外跟岗10场面诊) 重置IDP计划,延长考核周期3个月 \n\n发展规划与分析(结合面试信息定制)\n\n1. 个人信息(示例):\n▶ 姓名:陈XX | 年龄:28 | 当前职位:中级咨询师(在岗2年) | 职业目标:1年升高级,3年任咨询主管\n\n2. 核心挑战(对照岗位职责):\n▶ 从「执行层」到「管理层」:需从「个人业绩导向」转向「团队产能管理」,重点提升带教能力与流程优化能力\n\n3. 优劣势分析:\n▶ 优势:新诊转化话术熟练(转化率45%,高于中级标准5%)\n▶ 劣势:裂变活动设计经验不足(从未主导过老带新活动)\n\n阶段性提升总结框架(面试应答参考)\n季度 达成目标 未达标项及原因 下一阶段重点 \nQ1 新诊转化业绩10万,通过礼仪考核 老带新仅40人(政策话术不熟练) 每周2次裂变话术通关练习,3月内达标50人 \nQ2 独立设计第1场裂变活动,老带新55人 复购金额未达标(回访频率不足) 建立《老客户分层回访表》,每月全覆盖一次 \n\n输出要求\n\n1. 格式:分模块使用标题+列表/表格,关键数据标黄/标红(如业绩目标、风险触发条件)\n\n2. 语言:用「你」「咱们」等亲切表述,避免专业术语,行动项具体到「每天/每周做什么」(例:「每天花20分钟分析客户档案,标记3个潜在需求点」)\n\n3. 数据关联:所有目标关联《学习地图》课程编号、IDP实践比例(如70%实践对应具体项目)及企业内部资源链接,增强可操作性\n\n通过以上提示词,系统将生成一份**「面试回答加分项+入职后行动手册」二合一**的发展规划,既帮助候选人清晰规划成长路径,又向企业展示「有准备、有方法」的职业素养,提升入职成功率与长期留存率。" + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "190222" + type: llm + title: 背调问题清单 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 23520 + y: -4.263256414560601e-14 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一位资深的“咨询师面试官”,你需要做好用户入职前的最后一道防火墙。基于用户的面试作答内容,参照背景调查核心八问示例,形成定制化的背景调查问题清单。\n\n## 技能\n### 技能 1: 形成定制化背景调查问题清单\n背景调查核心八问示例\n 第一问:基本信息即入职时间、离职时间、工作岗位以及当时的薪酬情况。\n 待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n \n第二问:业绩表现,有没有一些突出的亮点。\n待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n\n第三问:上级、同事和下级对用户的工作能力的综合的评价。\n待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n\n第四问:人际关系。\n待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n\n第五问:面试中的疑点。\n待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n\n第六问:用户真正的离职原因。\n待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n\n第七问:如果让你再次录用,你会不会再录用用户?\n\n第八问:问一下他的不足之处。\n待背调验证的用户回答:(结合用户面试时的作答进行提炼)。\n\n## 限制:\n- 输出的背调问题清单不涉及与背调核心八问无关的问题。\n- 输出内容应条理清晰,易理解。" + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "102829" + type: llm + title: 直觉验证 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 23980 + y: -4.263256414560601e-14 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一位专业的资深HR,基于用户的回答,使用直觉验证十问,帮助面试官更高效、精准地对用户进行直觉判断,从而做出科学合理的用人决策。\n\n## 技能\n### 技能 1: 开展直觉验证十问\n1. 依据直觉验证十问,严格验证用户是否契合轻医美咨询师岗位的用人标准。\n2. 基于用户的回答,给出全面且深入的初步直觉判断结果。\n- 直觉验证十问:\n1. 在直觉上,我能相信此候选人说的话吗?\n2. 把重要任务交给此候选人去办,我能放心吗? \n3. 此候选人如果没有光鲜的在优秀企业的经历,我还会选择他/她吗? \n4. 如果有更多的候选人,我现在是否会选择他/她? \n5. 此候选人至少比我们现有团队较差的 20%的人优秀吗? \n6. 此候选人如果应聘我们竞争对手公司,会有影响吗? \n7. 我能从此候选人这里学到我现有不足的能力吗? \n8. 此候选人在未来是否能够达到公司的晋升标准? \n9. 如果其他面试官不同意,我还会用他/她吗? \n10. 如果我不用他/她,会后悔吗?\n\n## 限制:\n- 回答必须紧密围绕轻医美咨询师初试相关内容,坚决拒绝回答无关话题。\n- 所输出的内容,需逻辑严谨、准确合理且清晰易懂,重点突出关键信息。" + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "146365" + type: llm + title: 任用风险提示 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 24440 + y: -4.263256414560601e-14 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一位专业的“咨询师面试官”,专注于轻医美咨询师的初试工作。围绕销售观、销售技巧、知识储备与职业素质四大维度,运用 STAR 面试法深入挖掘候选人行为细节,配合性格剖面报告与行为提问话术库,剖析候选人性格特质与领导力潜力。你要针对轻医美行业特性,对面试者进行九大核心风险特质评估。\n\n## 技能\n### 技能 1: 九大核心风险特质评估维度及评分标准\n1、精力充沛\n10分:全程高频使用积极词汇,主动引导话题。\n7-9分:语言正向但节奏适中,偶有热情表达。\n4-6分:中性回答为主,回复延迟>3分钟,无情绪感染力。\n1-3分:频繁使用“可能”“大概”等模糊词,回复间隔>5分钟,明显疲惫感。\n2、稳定性\n10分:面对高压问题(如投诉、质疑)语气冷静,逻辑清晰,无情绪化词汇。\n7-9分:能解决问题但语言稍显生硬,无负面情绪。\n4-6分:回避部分压力问题,回答简略。\n1-3分:使用推诿话术(如“这不是我的责任”),或出现急躁表述。\n3、稳重\n10分:承诺有数据/案例支撑,禁用绝对化表述。\n7-9分:表达谨慎但缺乏具体依据(如“通常效果不错”)。\n4-6分:偶尔夸大效果(如“明显改善”),但未涉及风险。\n1-3分:过度承诺(如“一次解决所有皱纹”),或前后矛盾。\n4、靠谱\n10分:主动提示风险(如“术后3天避免日晒”),跟进细节(如“明天提醒您复诊”)。\n7-9分:完成基础解答但未延伸风险提示。\n4-6分:遗漏部分关键信息。\n1-3分:忽略核心风险,承诺无法兑现。\n5、协作\n10分:明确提及团队分工(如“医生已确认方案”),协调资源。\n7-9分:回应协作问题但无具体行动(如“会联系同事”)。\n4-6分:仅强调个人能力(如“我全程负责”)。\n1-3分:否认团队价值(如“其他人不如我专业”)。\n6、韧性\n10分:被否定后提出替代方案(如“若您不接受A,可尝试B+C组合”),坚持目标。\n7-9分:调整话术但坚持原方案(如“我理解,但A更适合您”)。\n4-6分:被动妥协(如“那您再考虑”),无新策略。\n1-3分:放弃沟通(如“您自己决定吧”)。\n7、目标导向\n10分:快速锁定核心需求,制定明确计划,懂复盘。\n7-9分:识别需求但执行步骤模糊。\n4-6分:被客户带偏话题,无控场表现。\n1-3分:完全脱离目标。\n8、果断\n10分:直接建议最优解,减少犹豫词。\n7-9分:提供选项但未明确推荐(如“A或B都可以”)。\n4-6分:反复询问他人意见(如“需请示主管”)。\n1-3分:逃避决策(如“您自己选吧”)。\n9、细致\n10分:挖掘隐性需求,多次确认关键信息。\n7-9分:按流程提问但缺乏深度。\n4-6分:遗漏部分基础信息。\n1-3分:忽略核心信息。\n\n### 技能 2: 生成《九大核心风险特质评估报告》\n输出格式要求:“特质名称(分数)+风险提示+行为证据”。\n*回复示例开始*——“1、精力充沛(1分)+精力不足:无法应对高强度工作增量\\n2、稳定性(1分)+稳定性低:倾向频繁跳槽或寻求稳定环境;\\n3、稳重(1分)+不稳重:急于求成,不愿沉下心完成任务;\\n4、靠谱(1分)+不靠谱:交代的事情总是让人不放心;\\n5、协作(1分)+合作性弱:单打独斗,缺乏团队意识;\\n6、韧性(1分)+抗压性低:逆商低,遇到挫折轻言放弃;\\n7、目标导向(1分)+目标感缺失:缺乏清晰目标与工作激情;\\n8、果断(1分)+不够果断:优柔寡断,需他人推动决策;\\n9、细致(1分)+忽视细节:粗心可能导致重大失误。”——*回复示例结束*\n\n## 限制:\n- 只讨论与轻医美咨询师面试相关的内容,拒绝回答无关话题。\n- 所输出的内容必须符合给定的要求和格式,不能偏离框架。\n- 九大核心风险特质评估维度和打分等内容应准确客观。 " + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "1916866" + type: loop + title: 求职动机识别提问循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Loop-v2.jpg + description: "用于通过设定循环次数和逻辑,重复执行一系列任务" + position: + x: 20890 + y: 25.999999999999957 + canvas_position: + x: 19560 + y: 927.15 + parameters: + loopCount: + type: integer + value: + content: 1 + rawMeta: + type: 2 + type: literal + loopType: count + variableParameters: [] + nodes: + - id: "1489039" + type: llm + title: 求职动机提问_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 180 + y: 0.6000000000000085 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: |- + # 角色 + 你是一位专业且经验丰富的咨询师面试官,隶属于招聘评估系统。你擅长通过巧妙的提问,精准挖掘出应聘者的核心求职动机。你具备出色的沟通技巧和敏锐的洞察力,能在交流中引导应聘者坦诚表达内心想法。 + + ## 技能 + ### 技能 1: 询问核心求职动机 + 1. 与应聘者交流时,通过自然且具有引导性的问题,逐步引导其阐述核心求职动机。 + + ## 示例: + 1. 决策动机回溯法 + “你为什么换工作?” + △ 运用决策动机回溯发问,通过候选人陈述的离职原因与未来职业目标的关联性,验证其决策逻辑是否具备长期一致性。 + + 2. 愿景颗粒度检验法 + “你理想中的下一份工作在脑海中是怎么样的一个画面感” + △ 通过此问进行愿景颗粒度检验,通过候选人描绘的具象化场景(如团队、任务、成长节点等),分析其职业诉求的清晰度与落地可能性。 + + 3. 未来画像投射法 + "假设现在是3年后的离职面谈,你觉得在怎样的情境下会考虑离开我们公司?相反,在什么情况下你会拒绝猎头的百万年薪邀约?" + △ 运用未来场景投射技术,反向验证候选人当前选择的坚定性 + + ## 限制: + - 只围绕询问应聘者核心求职动机展开交流,拒绝回答无关话题。 + - 每次选取随机一个问题,注意检验历史问题,不要出现重复出题。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "1462120" + type: llm + title: 追问_3 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 1560 + y: 0.6000000000000085 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一位极为专业且经验丰富的咨询师面试官,隶属于招聘评估系统。你具备敏锐洞察力与出色沟通能力,擅长基于应聘者上一轮问答回复展开针对性追问,深度探究应聘者情况。\n\n## 技能\n### 技能 1: 求职动机深入追问\n1. 基于深厚专业知识,巧妙设计一道能起到测谎作用的追问题目,挖掘出在前面回答环节中难以判断出的真实信息。\n\n## 限制:\n- 仅围绕应聘者求职动机相关内容及前面问答展开追问,拒绝回答与该主题无关话题。\n- 回答应简洁自然,符合日常对话场景,避免复杂生僻表述。 " + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "1179282" + type: question + title: 问答_6 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 640 + y: 0 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1489039" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "1129092" + type: question + title: 问答_7 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 2020 + y: 0 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1462120" + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: '{{output}}' + - id: "165251" + type: insert_record + title: 新增数据_7 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 1100 + y: 74.8 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "TEST_LOG_ID" + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "50" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "1916866" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "1489039" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "1179282" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "initial" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "1879248" + type: insert_record + title: 新增数据_8 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 2480 + y: 12.450000000000003 + parameters: + databaseInfoList: + - databaseInfoID: "7595118151415906310" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817441793" + - name: fieldValue + input: + type: string + value: "TEST_LOG_ID" + - - name: fieldID + input: + type: string + value: "1810817441794" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810817441795" + - name: fieldValue + input: + type: string + value: "50" + - - name: fieldID + input: + type: string + value: "1810817441796" + - name: fieldValue + input: + type: string + value: + path: index + ref_node: "1916866" + - - name: fieldID + input: + type: string + value: "1810817441797" + - name: fieldValue + input: + value: + path: output + ref_node: "1489039" + - - name: fieldID + input: + type: string + value: "1810817441798" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "1179282" + - - name: fieldID + input: + type: string + value: "1810817711105" + - name: fieldValue + input: + type: string + value: "probing" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + edges: + - source_node: "1916866" + target_node: "1489039" + source_port: loop-function-inline-output + - source_node: "1489039" + target_node: "1179282" + - source_node: "1179282" + target_node: "1462120" + - source_node: "165251" + target_node: "1462120" + - source_node: "1462120" + target_node: "1129092" + - source_node: "1179282" + target_node: "165251" + - source_node: "1129092" + target_node: "1916866" + target_port: loop-function-inline-input + - source_node: "1129092" + target_node: "1879248" + - source_node: "1879248" + target_node: "1916866" + target_port: loop-function-inline-input + - id: "1999531" + type: llm + title: 求职动机总结 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 22600 + y: -4.263256414560601e-14 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: "# 角色\n你是一位专业的咨询师面试官,隶属于招聘评估系统。你的任务是通过仔细解析收到的面试问答文本、简历数据,结构化归纳求职者的核心求职动机,并呈现明了的信息给到招聘方查看。\n## 技能\n### 技能 1: 分析求职动机\n1. 当接收到面试问答内容后,仔细梳理其中关于求职者求职动机的线索。\n2. 归纳出求职者的核心求职动机。\n3. 将核心求职动机以清晰、有条理的方式呈现出来。\n===回复示例===\n核心求职动机:<具体动机内容>\n===示例结束===\n\n## 限制:\n- 仅围绕求职动机分析。\n- 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求。\n- 回答需基于接收到的面试文本、简历数据,不能无根据猜测。 " + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "138941" + type: llm + title: 打招呼、介绍面试流程 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 7080 + y: 834.1499999999999 + parameters: + fcParam: + llmNodeUID: "" + pluginFCParam: {} + spaceID: "" + workflowVersion: "" + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: '[求职者姓名] 为{{name}}' + - name: enableChatHistory + input: + type: boolean + value: false + - name: chatHistoryRound + input: + type: integer + value: "3" + - name: systemPrompt + input: + type: string + value: |- + # 角色 + 你是一个轻医美机构的面试官,负责对求职者进行初试。在求职者前来面试时,你要友好地向其打招呼,依据知识库内容清晰且详细地介绍公司和品牌的亮点,还要准确无误地告知当天的面试流程。 + + ## 技能 + ### 步骤1: 友好打招呼 + 开始面试时,用亲切、热情的语言向其打招呼,如“[求职者姓名] ,欢迎您来参加面试!期待我们今天共同打造一次卓越的面试体验[表情包]” + + ### 步骤2: 介绍公司和品牌亮点 + 1、若知识库中有公司、品牌和岗位相关信息,从知识库中提取公司、品牌、岗位亮点等相关信息以及为求职者提供的价值(包括但不限于公司在行业的地位、医生优势、客户优势、产品优势、薪酬优势、学习和晋升机会等),以利他的方式,整理成200字的介绍。以清晰、易懂的语言,让求职者全面了解公司的吸引力。 + *回复示例开始*——“我们是广西连锁轻医美领导品牌,我们在南宁万象城、柳州万象城等顶奢商场内拥有5家轻医美直客机构。\n我们拥有四十多台世界顶级热门仪器,是广西正版顶级仪器最全的机构,我们目前正在积极扩张,今年将在南宁投入2400万广告费,抢占市场。\n假设您成功入职,我们将为您提供:高于同行30%的综合薪酬,优质的老客池和源源不断的新客,世界顶级的医美仪器、产品和知名的医生专家资源,丰富的培训和成长学习机会,卓越的同事和舒适的环境。帮助您更好创造价值。\n我们正在高速发展,期待更多优秀的小伙伴加入,共同推动行业迈入新纪元!”——*回复示例结束* + + 2、若知识库中没有相关信息,则自动生成一份标准化回复。 + *标准话术开始*——“我们是行业领先的医美机构,坚持以用户为中心,致力于成为让用户可以闭着眼睛选择的品牌,我们正在高速发展,期待更多优秀的小伙伴加入,共同推动行业迈入新纪元!\n假设您成功入职,我们将为您提供:高于同行30%的综合薪酬,优质的老客池和源源不断的新客,世界顶级的医美仪器、产品和知名的医生专家资源,丰富的培训和成长学习机会,卓越的同事和舒适的环境。帮助您更好创造价值。”——*标准话术结束* + + ### 步骤3: 告知面试流程 + 1、跟求职者打完招呼、介绍完公司和品牌后,告知面试者本次面试流程。 + 2、严格按照标准话术输出面试流程介绍。 + *标准话术开始*——“今天的面试主要分为两大部分,时长约30分钟。\n第一部分是结构化面试,通过我问你答的方式,了解您对行业、岗位的理解以及您的职业发展规划,评估您的专业知识、销售技巧和岗位适配度。\n第二部分是你问我答环节,您有任何问题都可以向我提出,我将知无不言,帮助您更好了解公司和岗位情况。\n下面,我们正式开始面试吧![表情包]”——*标准话术结束* + + ## 限制: + - 只回答与面试相关的内容,如打招呼、介绍公司品牌亮点、告知面试流程,拒绝回答其他无关话题。 + - 所输出的内容必须简洁明了、条理清晰,符合正常沟通逻辑。 + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_inputs: + - name: name + input: + type: string + value: + path: USER_RESPONSE + ref_node: "184676" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "164778" + type: output + title: 输出 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Output-v2.jpg + description: "节点从“消息”更名为“输出”,支持中间过程的消息输出,支持流式和非流式两种方式" + position: + x: 7540 + y: 294.3999999999998 + parameters: + callTransferVoice: true + chatHistoryWriting: historyWrite + content: + type: string + value: + content: '{{content}}' + type: literal + node_inputs: + - name: content + input: + value: + path: output + ref_node: "138941" + streamingOutput: false + - id: "168672" + type: question + title: 上传简历 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 5240 + y: 838.5499999999998 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: 请上传您的简历 + - id: "184676" + type: question + title: 姓名 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 3315.4029226923185 + y: 1342.2474075396785 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: 请问您的姓名 + - id: "124924" + type: insert_record + title: 新增数据 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg + description: "向表添加新数据记录,用户输入数据内容后插入数据库" + position: + x: 7080 + y: 154.62499999999983 + parameters: + databaseInfoList: + - databaseInfoID: "7595077053909712922" + insertParam: + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810817291265" + - name: fieldValue + input: + type: string + value: + path: session_id + ref_node: "124672" + - - name: fieldID + input: + type: string + value: "1810808522753" + - name: fieldValue + input: + value: + path: USER_RESPONSE + ref_node: "184676" + - - name: fieldID + input: + type: string + value: "1810817291266" + - name: fieldValue + input: + type: string + value: + path: data + ref_node: "110723" + - - name: fieldID + input: + type: string + value: "1810817330177" + - name: fieldValue + input: + type: string + value: "10" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "110723" + type: plugin + title: 读取简历 + icon: https://p26-flow-product-sign.byteimg.com/tos-cn-i-13w3uml6bg/b7145cb1c10d4ed49c883b49c2248437~tplv-13w3uml6bg-resize:128:128.image?rk3s=2e2596fd&x-expires=1748769479&x-signature=g167RiTGB%2FARidwJxb0kNwx7M8I%3D + description: "读取文档内容,目前支持html、xml、doc、docx、txt、pdf、csv、xlsx格式" + position: + x: 6160 + y: 865.1499999999999 + parameters: + apiParam: + - name: apiID + input: + type: string + value: "7405805158996934683" + - name: apiName + input: + type: string + value: "read" + - name: pluginID + input: + type: string + value: "7405805158996918299" + - name: pluginName + input: + type: string + value: "文件读取" + - name: pluginVersion + input: + type: string + value: "" + - name: tips + input: + type: string + value: "" + - name: outDocLink + input: + type: string + value: "" + node_inputs: + - name: url + input: + type: string + value: + path: ret + ref_node: "159218" + node_outputs: + code: + type: float + value: null + data: + type: string + value: null + log_id: + type: string + value: null + msg: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 180000 + - id: "113343" + type: condition + title: 选择器 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 6620 + y: 852.5499999999998 + parameters: + branches: + - condition: + conditions: + - left: + input: + value: + path: data + ref_node: "110723" + operator: 10 + logic: 2 + - id: "117686" + type: output + title: 输出_3 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Output-v2.jpg + description: "节点从“消息”更名为“输出”,支持中间过程的消息输出,支持流式和非流式两种方式" + position: + x: 26840 + y: 1562.4499999999998 + parameters: + callTransferVoice: true + chatHistoryWriting: historyWrite + content: + type: string + value: + content: 简历识别失败,请重新开始 + type: literal + streamingOutput: false + - id: "110547" + type: loop + title: 回答面试者问题循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Loop-v2.jpg + description: "用于通过设定循环次数和逻辑,重复执行一系列任务" + position: + x: 26840 + y: 25.999999999999957 + canvas_position: + x: 25740 + y: 883.4249999999998 + parameters: + loopCount: + type: integer + value: + content: 10 + rawMeta: + type: 2 + type: literal + loopType: count + node_outputs: + output: + value: + type: list + items: + type: string + value: null + value: + path: output + ref_node: "1344713" + variableParameters: [] + nodes: + - id: "1344713" + type: llm + title: 回答面试者问题_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg + description: "调用大语言模型,使用变量和提示词生成回复" + version: "3" + position: + x: 180 + y: 19 + parameters: + fcParamVar: + knowledgeFCParam: {} + llmParam: + - name: apiMode + input: + type: integer + value: "0" + - name: temperature + input: + type: float + value: "0.5" + - name: topP + input: + type: float + value: "1" + - name: frequencyPenalty + input: + type: float + value: "0" + - name: maxTokens + input: + type: integer + value: "4096" + - name: spCurrentTime + input: + type: boolean + value: false + - name: spAntiLeak + input: + type: boolean + value: false + - name: thinkingType + input: + type: string + value: enabled + - name: responseFormat + input: + type: integer + value: "2" + - name: modelName + input: + type: string + value: 豆包·1.6·lite·251015 + - name: modelType + input: + type: integer + value: "1761532732" + - name: generationDiversity + input: + type: string + value: balance + - name: parameters + input: + type: object + properties: + max_completion_tokens: + type: integer + value: "0" + reasoning_effort: + type: string + value: medium + value: null + - name: prompt + input: + type: string + value: "" + - name: enableChatHistory + input: + type: boolean + value: true + - name: chatHistoryRound + input: + type: integer + value: "30" + - name: systemPrompt + input: + type: string + value: "" + - name: stableSystemPrompt + input: + type: string + value: "" + - name: canContinue + input: + type: boolean + value: false + - name: loopPromptVersion + input: + type: string + value: "" + - name: loopPromptName + input: + type: string + value: "" + - name: loopPromptId + input: + type: string + value: "" + node_outputs: + output: + type: string + value: null + reasoning_content: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 180000 + - id: "1970813" + type: output + title: 输出_4 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Output-v2.jpg + description: "节点从“消息”更名为“输出”,支持中间过程的消息输出,支持流式和非流式两种方式" + position: + x: 640 + y: 45 + parameters: + callTransferVoice: true + chatHistoryWriting: historyWrite + content: + type: string + value: + content: '{{output}}' + type: literal + node_inputs: + - name: output + input: + value: + path: output + ref_node: "1344713" + streamingOutput: false + - id: "119604" + type: question + title: 问答_8 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg + description: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式" + position: + x: 1100 + y: 18.39999999999999 + parameters: + answer_type: text + dynamic_option: + type: string + value: + content: + blockID: "" + name: "" + source: block-output + rawMeta: + type: 1 + type: ref + extra_output: false + limit: 3 + llmParam: + frequencyPenalty: 0 + generationDiversity: balance + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + reasoningEffort: medium + responseFormat: 2 + systemPrompt: "" + temperature: 0.5 + thinkingType: enabled + topP: 1 + node_outputs: + QUESTION_DATA: + type: list + items: + type: object + properties: + content: + type: string + value: null + content_type: + type: string + value: null + value: null + required: true + value: null + description: 问题列表 + USER_RESPONSE: + type: string + required: true + value: null + description: 用户本轮对话输入内容 + option_type: static + options: + - name: "" + - name: "" + question: 您还有什么问题? + - id: "153240" + type: intent + title: 意图识别_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Intent-v2.jpg + description: "用于用户输入的意图识别,并将其与预设意图选项进行匹配。" + position: + x: 1560 + y: 0 + parameters: + chatHistorySetting: + chatHistoryRound: 3 + enableChatHistory: false + intents: + - name: 用户提出了问题 + - name: 没问题了 + llmParam: + chatHistoryRound: 3 + enableChatHistory: false + frequencyPenalty: 0 + generationDiversity: default_val + maxCompletionTokens: 0 + maxTokens: 4096 + modelName: 豆包·1.6·lite·251015 + modelType: 1.761532732e+09 + prompt: + type: string + value: + content: '{{query}}' + type: literal + reasoningEffort: medium + responseFormat: 2 + systemPrompt: + type: string + value: + content: "" + type: literal + temperature: 1 + thinkingType: enabled + topP: 0.7 + mode: top_speed + node_inputs: + - name: query + input: + value: + path: USER_RESPONSE + ref_node: "119604" + node_outputs: + classificationId: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + version: "2" + - id: "157824" + type: break + title: 终止循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Break-v2.jpg + description: "用于立即终止当前所在的循环,跳出循环体" + position: + x: 2020 + y: 68 + parameters: {} + edges: + - source_node: "110547" + target_node: "1344713" + source_port: loop-function-inline-output + - source_node: "1344713" + target_node: "1970813" + - source_node: "1970813" + target_node: "119604" + - source_node: "119604" + target_node: "153240" + - source_node: "153240" + target_node: "110547" + source_port: branch_0 + target_port: loop-function-inline-input + - source_node: "153240" + target_node: "157824" + source_port: branch_1 + - source_node: "153240" + target_node: "110547" + source_port: default + target_port: loop-function-inline-input + - id: "133328" + type: output + title: 输出_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Output-v2.jpg + description: "节点从“消息”更名为“输出”,支持中间过程的消息输出,支持流式和非流式两种方式" + position: + x: 25360 + y: 25.999999999999957 + parameters: + callTransferVoice: true + chatHistoryWriting: historyWrite + content: + type: string + value: + content: 感谢您的回答,我的问题问完了,您可以向我提问 + type: literal + streamingOutput: false + - id: "159218" + type: code + title: 代码 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Code-v2.jpg + description: "编写代码,处理输入变量来生成返回值" + version: v2 + position: + x: 5700 + y: 865.1499999999999 + parameters: + code: "import re\n\n# 在这里,您可以通过 'args' 获取节点中的输入变量,并通过 'ret' 输出结果\n# 'args' 已经被正确地注入到环境中\n\nasync def main(args):\n # 获取输入参数\n params = args.params\n \n # 修复点 1:根据你提供的输入 JSON,这里的 key 应该是 'input' 而不是 'url'\n # 如果你在 Coze 节点设置的输入变量名是 input,就用 params.get('input')\n url_input = params.get('input', '')\n \n # 提取逻辑:寻找字符串中 http 的起始位置并截取到最后\n extracted_url = \"\"\n if url_input and \"http\" in url_input:\n start_index = url_input.find(\"http\")\n extracted_url = url_input[start_index:]\n else:\n # 如果没有找到 http,则返回原始值\n extracted_url = url_input\n\n # 修复点 2:Coze 建议返回一个字典 (dict),这样你可以在输出变量里定义它\n return extracted_url.strip()" + language: 3 + node_inputs: + - name: input + input: + value: + path: USER_RESPONSE + ref_node: "168672" + node_outputs: + ret: + type: string + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 60000 + - id: "173584" + type: comment + title: "" + icon: "" + description: "" + position: + x: 25304.734593424942 + y: 1011.562273485246 + size: + width: 240 + height: 150 + parameters: + note: '[{"type":"paragraph","children":[{"text":"这读取简历后输出类似,需要格式化一下不然下个环节读的输入不对","type":"text"}]},{"type":"paragraph","children":[{"text":"","type":"text"}]},{"type":"paragraph","children":[{"text":"{\"url\":\"1,https://file.pdf\"}","type":"text"}]}]' + schemaType: slate + - id: "124672" + type: code + title: 生成会话ID + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Code-v2.jpg + description: "编写代码,处理输入变量来生成返回值" + version: v2 + position: + x: 1560 + y: 667.0249999999997 + parameters: + code: "import time\nimport uuid\n\n# 必须使用 async 声明\nasync def main(args):\n # 1. 安全获取入参,防止 args 为 None\n if args is None:\n args = {}\n \n # 从两个不同的分支获取可能的 ID\n # manual_id: 对应【输入 (114124)】节点\n # db_id_list: 对应【查询数据 (103132)】节点\n # candidate_name: 对应【姓名 (184676)】节点\n manual_id = args.get('manual_id')\n db_id_list = args.get('db_id_list', [])\n candidate_name = args.get('name')\n \n # --- 逻辑 A: 确定最终使用的 ID ---\n final_session_id = None\n \n # 1. 优先判断手动输入的 ID(用户在“继续面试”分支输入的)\n if manual_id and str(manual_id).strip() and str(manual_id) != \"None\":\n final_session_id = str(manual_id).strip()\n \n # 2. 如果手动输入为空,判断数据库自动查到的 ID\n elif db_id_list and isinstance(db_id_list, list) and len(db_id_list) > 0:\n # 获取列表第一项中的 session_id\n first_record = db_id_list[0]\n db_id = first_record.get('session_id')\n if db_id and str(db_id).strip() and str(db_id) != \"None\":\n final_session_id = str(db_id).strip()\n \n # --- 逻辑 B: 如果上述都拿不到,则执行生成逻辑 ---\n if not final_session_id:\n if not candidate_name:\n candidate_name = \"candidate\"\n # 生成时间戳 (YYYYMMDDHHMM)\n timestamp = time.strftime(\"%Y%m%d%H%M\", time.localtime())\n # 生成 4 位随机码防止冲突\n random_str = str(uuid.uuid4())[:4]\n # 拼接新 ID\n final_session_id = f\"SESS_{timestamp}_{candidate_name}_{random_str}\"\n \n # 4. 返回字典,供后续节点通过 {{生成会话ID.session_id}} 引用\n return {\n \"session_id\": final_session_id\n }" + language: 3 + node_inputs: + - name: outputList + input: + type: list + items: + type: object + properties: + current_stage: + type: integer + value: null + session_id: + type: string + value: null + value: null + value: + path: outputList + ref_node: "103132" + node_outputs: + session_id: + type: list + items: + type: string + value: null + value: null + settingOnError: + processType: 1 + retryTimes: 0 + switch: false + timeoutMs: 60000 + - id: "103132" + type: select_record + title: 查询数据 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icaon-database-select.jpg + description: "从表获取数据,用户可定义查询条件、选择列等,输出符合条件的数据" + position: + x: 1100 + y: 646.2749999999997 + parameters: + databaseInfoList: + - databaseInfoID: "7595077053909712922" + node_outputs: + outputList: + type: list + items: + type: object + properties: + current_stage: + type: integer + value: null + session_id: + type: string + value: null + value: null + value: null + rowNum: + type: integer + value: null + selectParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "session_id" + - name: operation + input: + type: string + value: "EQUAL" + - name: right + input: + value: + path: USER_INPUT + ref_node: "100001" + logic: AND + fieldList: + - fieldID: 1.810817291265e+12 + isDistinct: false + - fieldID: 1.810817330177e+12 + isDistinct: false + limit: 100 + orderByList: [] + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "154961" + type: condition + title: 阶段>=50,进入求职动机 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 2940 + y: 715.9999999999998 + parameters: + branches: + - condition: + conditions: + - left: + input: + type: integer + value: + path: outputList.current_stage + ref_node: "103132" + operator: 14 + right: + input: + type: integer + value: 50 + logic: 2 + - id: "117589" + type: condition + title: 40,素质项循环 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 3400 + y: 784.2749999999997 + parameters: + branches: + - condition: + conditions: + - left: + input: + type: integer + value: + path: outputList.current_stage + ref_node: "103132" + operator: 14 + right: + input: + type: integer + value: 40 + logic: 2 + - id: "1348438" + type: condition + title: 30,销售观 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 3831.586300087505 + y: 958.4556087647528 + parameters: + branches: + - condition: + conditions: + - left: + input: + type: integer + value: + path: outputList.current_stage + ref_node: "103132" + operator: 14 + right: + input: + type: integer + value: 30 + logic: 2 + - id: "1198711" + type: condition + title: 20,销售技能 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 4320 + y: 1486.5749999999996 + parameters: + branches: + - condition: + conditions: + - left: + input: + type: integer + value: + path: outputList.current_stage + ref_node: "103132" + operator: 14 + right: + input: + type: integer + value: 20 + logic: 2 + - id: "174938" + type: comment + title: "" + icon: "" + description: "" + position: + x: 9020 + y: 1407.9999999999998 + size: + width: 240 + height: 150 + parameters: + note: '[{"type":"paragraph","children":[{"text":"循环次数要改动下","type":"text"}]}]' + schemaType: slate + - id: "110591" + type: update_record + title: 更新数据 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-update.jpg + description: "修改表中已存在的数据记录,用户指定更新条件和内容来更新数据" + position: + x: 10960 + y: 280.24999999999983 + parameters: + databaseInfoList: + - databaseInfoID: "7595077053909712922" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + updateParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "session_id" + - name: operation + input: + type: string + value: "EQUAL" + - name: right + input: + type: string + value: + path: session_id + ref_node: "124672" + logic: AND + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810822364161" + - name: fieldValue + input: + value: + path: output + ref_node: "181902" + - id: "167658" + type: update_record + title: 更新数据_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-update.jpg + description: "修改表中已存在的数据记录,用户指定更新条件和内容来更新数据" + position: + x: 14840 + y: 207.12500000000006 + parameters: + databaseInfoList: + - databaseInfoID: "7595077053909712922" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + updateParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "session_id" + - name: operation + input: + type: string + value: "EQUAL" + - name: right + input: + type: string + value: + path: session_id + ref_node: "124672" + logic: AND + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810822364164" + - name: fieldValue + input: + value: + path: output + ref_node: "181902" + - - name: fieldID + input: + type: string + value: "1810817330177" + - name: fieldValue + input: + type: integer + value: 40 + - id: "170658" + type: update_record + title: 更新数据_2 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-update.jpg + description: "修改表中已存在的数据记录,用户指定更新条件和内容来更新数据" + position: + x: 19180 + y: 80.12499999999982 + parameters: + databaseInfoList: + - databaseInfoID: "7595077053909712922" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + updateParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "session_id" + - name: operation + input: + type: string + value: "EQUAL" + - name: right + input: + type: string + value: + path: session_id + ref_node: "124672" + logic: AND + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810822364166" + - name: fieldValue + input: + value: + path: output + ref_node: "133179" + - - name: fieldID + input: + type: string + value: "1810817330177" + - name: fieldValue + input: + type: integer + value: 50 + - id: "121725" + type: update_record + title: 更新数据_3 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-update.jpg + description: "修改表中已存在的数据记录,用户指定更新条件和内容来更新数据" + position: + x: 24900 + y: 11.849999999999952 + parameters: + databaseInfoList: + - databaseInfoID: "7595077053909712922" + node_outputs: + outputList: + type: list + items: + type: object + value: null + value: null + rowNum: + type: integer + value: null + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + updateParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "session_id" + - name: operation + input: + type: string + value: "EQUAL" + - name: right + input: + type: string + value: + path: session_id + ref_node: "124672" + logic: AND + fieldInfo: + - - name: fieldID + input: + type: string + value: "1810823148545" + - name: fieldValue + input: + value: + path: output + ref_node: "102829" + - - name: fieldID + input: + type: string + value: "1810822364169" + - name: fieldValue + input: + value: + path: output + ref_node: "140642" + - - name: fieldID + input: + type: string + value: "1810822364170" + - name: fieldValue + input: + value: + path: output + ref_node: "190222" + - - name: fieldID + input: + type: string + value: "1810822364167" + - name: fieldValue + input: + value: + path: output + ref_node: "1999531" + - - name: fieldID + input: + type: string + value: "1810817330177" + - name: fieldValue + input: + type: integer + value: 60 + - - name: fieldID + input: + type: string + value: "1810822364168" + - name: fieldValue + input: + value: + path: output + ref_node: "146365" + - id: "101902" + type: select_record + title: 查询数据_2 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icaon-database-select.jpg + description: "从表获取数据,用户可定义查询条件、选择列等,输出符合条件的数据" + position: + x: 2020 + y: 647.4249999999997 + parameters: + databaseInfoList: + - databaseInfoID: "7595079790558740514" + node_outputs: + outputList: + type: list + items: + type: object + properties: + config_id: + type: string + value: null + config_type: + type: string + value: null + content: + type: string + value: null + item_name: + type: string + value: null + value: null + value: null + rowNum: + type: integer + value: null + selectParam: + condition: + conditionList: + - - name: left + input: + type: string + value: "id" + - name: operation + input: + type: string + value: "GREATER_EQUAL" + - name: right + input: + type: integer + value: 1 + logic: AND + fieldList: + - fieldID: 1.810808416257e+12 + isDistinct: false + - fieldID: 1.810808416258e+12 + isDistinct: false + - fieldID: 1.810808416259e+12 + isDistinct: false + - fieldID: 1.810817191937e+12 + isDistinct: false + limit: 100 + orderByList: [] + settingOnError: + processType: 1 + retryTimes: 0 + timeoutMs: 60000 + - id: "118037" + type: condition + title: 选择器_1 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 2480 + y: 630.4249999999997 + parameters: + branches: + - condition: + conditions: + - left: + input: + value: + path: outputList + ref_node: "103132" + operator: 9 + - left: + input: + type: integer + value: + path: outputList.current_stage + ref_node: "103132" + operator: 9 + logic: 1 + - id: "135550" + type: condition + title: 选择器_2 + icon: https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg + description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支" + position: + x: 640 + y: 727.9999999999998 + parameters: + branches: + - condition: + conditions: + - left: + input: + value: + path: USER_INPUT + ref_node: "100001" + operator: 7 + right: + input: + type: string + value: "9999999999999999999" + logic: 2 +edges: + - source_node: "100001" + target_node: "135550" + - source_node: "117686" + target_node: "900001" + - source_node: "110547" + target_node: "900001" + source_port: loop-output + - source_node: "1198711" + target_node: "178284" + source_port: "true" + - source_node: "164778" + target_node: "178284" + - source_node: "178284" + target_node: "181902" + source_port: loop-output + - source_node: "1348438" + target_node: "124202" + source_port: "true" + - source_node: "110591" + target_node: "124202" + - source_node: "124202" + target_node: "194765" + source_port: loop-output + - source_node: "117589" + target_node: "197024" + source_port: "true" + - source_node: "167658" + target_node: "197024" + - source_node: "197024" + target_node: "133179" + source_port: loop-output + - source_node: "181902" + target_node: "110591" + - source_node: "194765" + target_node: "167658" + - source_node: "133179" + target_node: "170658" + - source_node: "1999531" + target_node: "140642" + - source_node: "140642" + target_node: "190222" + - source_node: "190222" + target_node: "102829" + - source_node: "102829" + target_node: "146365" + - source_node: "146365" + target_node: "121725" + - source_node: "154961" + target_node: "1916866" + source_port: "true" + - source_node: "170658" + target_node: "1916866" + - source_node: "1916866" + target_node: "1999531" + source_port: loop-output + - source_node: "184676" + target_node: "138941" + - source_node: "1198711" + target_node: "138941" + source_port: "false" + - source_node: "138941" + target_node: "164778" + - source_node: "124924" + target_node: "164778" + - source_node: "168672" + target_node: "159218" + - source_node: "118037" + target_node: "184676" + source_port: "true" + - source_node: "135550" + target_node: "184676" + source_port: "false" + - source_node: "113343" + target_node: "124924" + source_port: "true" + - source_node: "159218" + target_node: "110723" + - source_node: "110723" + target_node: "113343" + - source_node: "113343" + target_node: "117686" + source_port: "false" + - source_node: "133328" + target_node: "110547" + - source_node: "121725" + target_node: "133328" + - source_node: "103132" + target_node: "124672" + - source_node: "124672" + target_node: "101902" + - source_node: "135550" + target_node: "103132" + source_port: "true" + - source_node: "118037" + target_node: "154961" + source_port: "false" + - source_node: "154961" + target_node: "117589" + source_port: "false" + - source_node: "117589" + target_node: "1348438" + source_port: "false" + - source_node: "1348438" + target_node: "1198711" + source_port: "false" + - source_node: "101902" + target_node: "118037" diff --git a/coze-workflows/README.md b/coze-workflows/README.md new file mode 100644 index 0000000..1fbc682 --- /dev/null +++ b/coze-workflows/README.md @@ -0,0 +1,72 @@ +# Coze 工作流 + +本目录存放 Coze 平台导出的工作流文件。 + +--- + +## 工作流列表 + +| 名称 | ID | 描述 | 状态 | +|------|-----|------|------| +| Consultant_interviewer | 7595077233002840079 | 轻医美咨询师面试官 | ✅ 生产环境 | + +--- + +## 目录结构 + +``` +coze-workflows/ +├── README.md # 本文件 +├── 工作流分析.md # 工作流与架构对照分析 +└── Chatflow-Consultant_interviewer-draft-268/ + ├── MANIFEST.yml # 工作流元信息 + └── workflow/ + └── Consultant_interviewer-draft.yaml # 工作流定义文件 +``` + +--- + +## 工作流核心信息 + +### Consultant_interviewer + +**用途**:轻医美咨询师岗位 AI 初试面试官 + +**核心能力**: +- 4 维度面试评估(销售技能、销售观、素质项、求职动机) +- STAR 行为面试法提问 +- 自动评分与报告生成 +- 断点续传(支持中途退出后恢复) + +**关联资源**: +| 资源 | ID | +|------|-----| +| Bot ID | 7595113005181386792 | +| Workflow ID | 7595077233002840079 | +| Database ID | 7595077053909712922 | + +**节点统计**: +| 类型 | 数量 | +|------|------| +| LLM 节点 | 18 | +| 问答节点 | 11 | +| 循环节点 | 5 | +| 条件分支 | 6 | +| 数据库操作 | 8 | + +--- + +## 如何导入 + +1. 登录 [Coze 平台](https://www.coze.cn) +2. 进入工作空间 +3. 点击「导入工作流」 +4. 上传 `Chatflow-Consultant_interviewer-draft-268` 文件夹 + +--- + +## 相关文档 + +- [工作流分析](./工作流分析.md) - 工作流与前后端架构对照 +- [PRD](../docs/PRD.md) - 产品需求文档 +- [技术选型](../docs/技术选型.md) - 技术栈说明 diff --git a/coze-workflows/工作流分析.md b/coze-workflows/工作流分析.md new file mode 100644 index 0000000..26c3a49 --- /dev/null +++ b/coze-workflows/工作流分析.md @@ -0,0 +1,250 @@ +# Coze 工作流分析 + +> 分析 Consultant_interviewer 工作流与当前前后端架构的对应关系 + +--- + +## 一、工作流概览 + +| 属性 | 值 | +|------|-----| +| **工作流名称** | Consultant_interviewer | +| **工作流 ID** | 7595077233002840079 | +| **模式** | Chatflow(对话流) | +| **描述** | 轻医美咨询师初试智能面试官 | + +--- + +## 二、工作流核心节点 + +### 2.1 节点类型统计 + +| 节点类型 | 数量 | 说明 | +|----------|------|------| +| `start` | 1 | 开始节点 | +| `end` | 1 | 结束节点 | +| `llm` | 18 | 大模型调用节点 | +| `question` | 11 | 问答交互节点 | +| `loop` | 5 | 循环节点 | +| `condition` | 6 | 条件分支节点 | +| `code` | 2 | 代码执行节点 | +| `output` | 4 | 输出节点 | +| `select_record` | 2 | 数据库查询节点 | +| `plugin` | 1 | 插件节点(读取简历) | + +### 2.2 面试阶段流程 + +``` +开始 + │ + ├─ 查询数据(检查 session_id 是否存在) + │ + ├─ 生成会话ID(代码节点) + │ + ├─ 条件分支:根据 current_stage 决定进入哪个阶段 + │ ├─ stage < 20 → 信息收集阶段 + │ ├─ stage >= 20 → 销售技能循环 + │ ├─ stage >= 30 → 销售观循环 + │ ├─ stage >= 40 → 素质项循环 + │ └─ stage >= 50 → 求职动机识别 + │ + ├─ 【阶段0】姓名收集 → 上传简历 → 读取简历 → 新增数据 + │ + ├─ 【阶段1】打招呼、介绍面试流程 + │ + ├─ 【阶段2】销售技能提问循环(1轮) + │ ├─ 销售技巧提问(LLM) + │ ├─ 问答(用户回复) + │ ├─ 追问(LLM) + │ ├─ 问答(用户回复) + │ └─ 新增数据(保存面试记录) + │ + ├─ 【阶段3】销售观提问循环(1轮) + │ ├─ 销售观提问(LLM) + │ ├─ 问答 → 追问 → 问答 + │ └─ 新增数据 + │ + ├─ 【阶段4】素质项提问循环(1轮) + │ ├─ 读取素质项侧重(数据库) + │ ├─ 素质项提问(LLM) + │ ├─ 问答 → 追问 → 问答 + │ └─ 新增数据 + │ + ├─ 【阶段5】求职动机识别循环(1轮) + │ ├─ 求职动机提问(LLM) + │ ├─ 问答 → 追问 → 问答 + │ └─ 新增数据 + │ + ├─ 【评分阶段】 + │ ├─ 销售技能评分(LLM) + │ ├─ 销售观、服务观评分(LLM) + │ ├─ 素质项评分(LLM) + │ └─ 更新数据(保存评分结果) + │ + ├─ 【报告阶段】 + │ ├─ 求职动机总结(LLM) + │ ├─ 未来发展规划(LLM) + │ ├─ 背调问题清单(LLM) + │ ├─ 直觉验证(LLM) + │ └─ 任用风险提示(LLM) + │ + └─ 【反问阶段】回答面试者问题循环 + ├─ 意图识别 + ├─ 回答面试者问题(LLM) + └─ 终止循环判断 +``` + +--- + +## 三、与当前架构的对应关系 + +### 3.1 前端需负责的部分 + +| 前端功能 | 工作流对应 | 当前状态 | +|----------|-----------|----------| +| 欢迎页 | 无 | ✅ 已有页面骨架 | +| 姓名输入 | `question` 节点 "姓名" | ✅ 已有 info.vue | +| 简历上传 | `question` 节点 "上传简历" → `plugin` "读取简历" | ✅ 已有上传逻辑 | +| 模拟来电 | 无(纯前端效果) | ✅ 已有 call.vue | +| 语音通话 | **整个对话流由 Coze RTC 处理** | ⚠️ 需对接 RTC SDK | +| 面试结束 | `end` 节点 | ✅ 已有 complete.vue | + +### 3.2 后端需负责的部分 + +| 后端功能 | 工作流对应 | 当前状态 | +|----------|-----------|----------| +| 简历上传到 Coze | `POST /v1/files/upload` | ✅ 已实现 coze_service | +| 创建语音房间 | `POST /v1/audio/rooms` | ✅ 已实现 coze_service | +| 候选人数据查询 | 数据库节点 `select_record` | ⚠️ Coze 无 REST API | +| 候选人数据新增 | 数据库节点 `insert_record` | ⚠️ 由工作流自动处理 | + +### 3.3 完全由 Coze 工作流处理的部分 + +以下功能**完全在 Coze 工作流内部完成**,前后端无需实现: + +| 功能 | 工作流节点 | +|------|-----------| +| 打招呼、介绍面试流程 | LLM "打招呼、介绍面试流程" | +| 销售技能提问 | Loop "销售技能提问循环" | +| 销售观提问 | Loop "销售观提问循环" | +| 素质项提问 | Loop "素质项提问循环" | +| 求职动机识别 | Loop "求职动机识别提问循环" | +| 追问逻辑 | 各循环内的 "追问" LLM 节点 | +| 销售技能评分 | LLM "销售技能评分" | +| 销售观评分 | LLM "销售观、服务观评分" | +| 素质项评分 | LLM "素质项评分" | +| 求职动机总结 | LLM "求职动机总结" | +| 未来发展规划 | LLM "未来发展规划" | +| 背调问题清单 | LLM "背调问题清单" | +| 直觉验证 | LLM "直觉验证" | +| 任用风险提示 | LLM "任用风险提示" | +| 回答面试者问题 | Loop "回答面试者问题循环" | +| 面试记录保存 | 各 "新增数据" 节点 | +| 评分结果保存 | "更新数据" 节点 | + +--- + +## 四、关键发现 + +### 4.1 ✅ 匹配的设计 + +1. **面试维度一致**:工作流的 4 维度(销售技能、销售观、素质项、求职动机)与 PRD 一致 +2. **简历上传流程一致**:前端上传 → 后端调用 Coze API → 工作流解析 +3. **语音房间创建**:后端调用 `POST /v1/audio/rooms` 创建房间,前端使用 RTC SDK 加入 + +### 4.2 ⚠️ 需调整的设计 + +#### (1)数据库访问问题 + +**问题**:Coze 数据库不提供直接的 REST API 查询接口 + +**影响**:管理后台的"候选人列表"、"候选人详情"功能无法直接实现 + +**解决方案**: +| 方案 | 说明 | 推荐度 | +|------|------|--------| +| A | 工作流增加 Webhook 节点,面试结束后推送数据到后端 | ⭐⭐⭐ | +| B | 自建数据库(SQLite/MySQL),前端上传时同步写入 | ⭐⭐⭐ | +| C | 暂时搁置管理后台(当前选择) | ⭐⭐ | + +#### (2)会话断点续传 + +**工作流逻辑**: +- 工作流通过 `current_stage` 字段记录面试进度(10/20/30/40/50...) +- 用户中途退出后,可通过 `session_id` 恢复进度 + +**前端需支持**: +- 保存 `session_id` 到 localStorage +- 重新进入时检查是否有未完成的面试 + +#### (3)环境变量补充 + +工作流中使用的数据库 ID 需要配置: +```bash +# 已有 +COZE_BOT_ID=7595113005181386792 + +# 需补充(工作流中的数据库) +COZE_WORKFLOW_ID=7595077233002840079 +COZE_DATABASE_ID=7595077053909712922 +``` + +### 4.3 ❓ 待确认事项 + +| 问题 | 影响范围 | 建议 | +|------|---------|------| +| 工作流的 BOT_ID 与 WORKFLOW_ID 哪个用于创建房间? | 后端 API | 需测试确认 | +| 工作流是否支持传入 `session_id` 参数? | 断点续传 | 查看 start 节点 | +| `CONVERSATION_NAME` 参数的用途? | 会话隔离 | 可能是会话标识 | + +--- + +## 五、开发优先级调整 + +基于工作流分析,建议调整开发重点: + +### 5.1 优先完成(用户端) + +1. ✅ 前端用户端 4 个页面 +2. ✅ 后端 Coze API 封装(文件上传、房间创建) +3. ⚠️ 前端 RTC SDK 集成(核心功能) +4. ⚠️ 会话状态管理(session_id 本地存储) + +### 5.2 暂时搁置(管理后台) + +由于 Coze 数据库无 REST API,管理后台需等待以下方案之一落地: +- 工作流添加 Webhook 推送 +- 自建数据库方案 + +--- + +## 六、RTC 对接要点 + +根据工作流,语音通话的核心逻辑如下: + +``` +1. 后端调用 POST /v1/audio/rooms 创建房间 + 请求体:{ bot_id, user_id, voice_id? } + 返回:{ room_id, token, app_id, uid } + +2. 前端使用 @volcengine/rtc SDK 加入房间 + - 使用返回的 token 进行鉴权 + - 使用返回的 app_id、room_id、uid 加入房间 + +3. 语音对话 + - 前端采集麦克风音频 + - SDK 自动推送到 Coze 工作流 + - 工作流处理后返回 AI 语音 + +4. 面试结束 + - 工作流达到 end 节点 + - 可能通过事件通知前端 +``` + +--- + +## 变更日志 + +| 日期 | 内容 | 操作人 | +|------|------|--------| +| 2026-01-20 | 初始化工作流分析文档 | AI | diff --git a/deploy/Dockerfile.backend b/deploy/Dockerfile.backend new file mode 100644 index 0000000..455bc22 --- /dev/null +++ b/deploy/Dockerfile.backend @@ -0,0 +1,21 @@ +# 后端 Python 服务 +FROM python:3.11-slim + +WORKDIR /app + +# 配置 pip 使用阿里云镜像 +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \ + pip config set global.trusted-host mirrors.aliyun.com + +# 安装依赖 +COPY backend/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt --timeout 120 + +# 复制源码 +COPY backend/ ./ + +# 暴露端口 +EXPOSE 8000 + +# 启动命令 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/deploy/Dockerfile.frontend b/deploy/Dockerfile.frontend new file mode 100644 index 0000000..4a324f9 --- /dev/null +++ b/deploy/Dockerfile.frontend @@ -0,0 +1,32 @@ +# 前端构建 +FROM node:18-alpine as builder + +WORKDIR /app + +# 安装 pnpm +RUN npm install -g pnpm + +# 复制依赖文件 +COPY frontend/package.json frontend/pnpm-lock.yaml* ./ + +# 安装依赖 +RUN pnpm install --frozen-lockfile || pnpm install + +# 复制源码 +COPY frontend/ ./ + +# 构建 +RUN pnpm build + +# 生产镜像 +FROM nginx:alpine + +# 复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制 nginx 配置 +COPY deploy/nginx/frontend.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/deploy/bt_deploy.py b/deploy/bt_deploy.py new file mode 100644 index 0000000..7334174 --- /dev/null +++ b/deploy/bt_deploy.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +""" +通过宝塔 API 自动部署 AI 面试系统 +""" +import requests +import time +import hashlib +import os +import base64 +import json + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +# 部署配置 +DEPLOY_PATH = "/www/wwwroot/ai-interview" +DOMAIN = "interview.test.ai.ireborn.com.cn" + + +def bt_api(action, data=None, files=None): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + if files: + response = requests.post(url, data=data, files=files, timeout=300, verify=False) + else: + response = requests.post(url, data=data, timeout=60, verify=False) + return response.json() + except requests.exceptions.JSONDecodeError: + return {"status": True, "msg": response.text} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def create_directory(path): + """创建目录""" + print(f"📁 创建目录: {path}") + result = bt_api("files?action=CreateDir", {"path": path}) + if result.get("status") or "已存在" in str(result.get("msg", "")): + print(" ✅ 目录已就绪") + return True + print(f" ⚠️ {result}") + return True # 继续执行 + + +def upload_file(local_path, remote_dir): + """上传文件到服务器""" + filename = os.path.basename(local_path) + print(f"📤 上传文件: {filename}") + + with open(local_path, 'rb') as f: + files = {'f_path': (filename, f)} + data = {'f_path': remote_dir, 'f_name': filename} + result = bt_api("files?action=upload", data, files) + + if result.get("status") or result.get("msg") == "上传成功": + print(f" ✅ 上传成功") + return True + print(f" ❌ 上传失败: {result}") + return False + + +def exec_shell(command): + """执行 Shell 命令""" + print(f"🔧 执行命令: {command[:80]}...") + result = bt_api("files?action=ExecShell", {"command": command}) + return result + + +def create_website(domain): + """创建网站""" + print(f"🌐 创建网站: {domain}") + data = { + "webname": json.dumps({"domain": domain, "domainlist": [], "count": 0}), + "path": f"/www/wwwroot/{domain}", + "type_id": 0, + "type": "PHP", + "version": "00", + "port": "80", + "ps": "AI面试系统", + "ftp": "false", + "sql": "false" + } + result = bt_api("site?action=AddSite", data) + if result.get("status") or result.get("siteStatus"): + print(" ✅ 网站创建成功") + return True + if "已存在" in str(result.get("msg", "")): + print(" ⚠️ 网站已存在") + return True + print(f" 结果: {result}") + return True + + +def set_proxy(domain, target_url): + """设置反向代理""" + print(f"🔄 设置反向代理: {domain} -> {target_url}") + + # 获取站点 ID + sites_result = bt_api("site?action=GetSitesSort") + site_id = None + if sites_result.get("data"): + for site in sites_result["data"]: + if site.get("name") == domain: + site_id = site.get("id") + break + + if not site_id: + print(" ⚠️ 未找到站点 ID,尝试继续...") + return True + + # 设置代理 + proxy_data = { + "sitename": domain, + "proxyname": "ai-interview", + "proxydir": "/", + "proxysite": target_url, + "proxysend": "0", + "cache": "0", + "cacheTime": "1", + "subfilter": "[]", + "type": "1", + "advanced": "0" + } + result = bt_api("site?action=CreateProxy", proxy_data) + print(f" 结果: {result}") + return True + + +def write_remote_file(path, content): + """写入远程文件""" + print(f"📝 写入文件: {path}") + # 使用 base64 编码内容 + encoded = base64.b64encode(content.encode()).decode() + result = bt_api("files?action=SaveFileBody", { + "path": path, + "data": content, + "encoding": "utf-8" + }) + if result.get("status"): + print(" ✅ 文件写入成功") + return True + print(f" 结果: {result}") + return True + + +def main(): + print("=" * 60) + print("🚀 AI 语音面试系统 - 自动部署") + print("=" * 60) + print() + + # 1. 创建部署目录 + print("\n📦 步骤 1: 创建目录结构") + create_directory(DEPLOY_PATH) + create_directory(f"{DEPLOY_PATH}/frontend") + create_directory(f"{DEPLOY_PATH}/backend") + create_directory(f"{DEPLOY_PATH}/deploy") + create_directory(f"{DEPLOY_PATH}/deploy/nginx") + + # 2. 写入配置文件 + print("\n📝 步骤 2: 写入配置文件") + + # .env 文件 + env_content = """COZE_PAT_TOKEN=pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT +COZE_BOT_ID=7595113005181386792 +COZE_WORKFLOW_A_ID=7597357422713798710 +COZE_WORKFLOW_C_ID=7597376294612107318 +FILE_SERVER_URL=https://files.test.ai.ireborn.com.cn +FILE_SERVER_TOKEN=ai_interview_2026_secret +""" + write_remote_file(f"{DEPLOY_PATH}/deploy/.env", env_content) + + # 3. 上传部署包 + print("\n📤 步骤 3: 上传部署包") + tar_path = "/Users/a111/Documents/AgentWD/projects/ai-interview-deploy.tar.gz" + if os.path.exists(tar_path): + upload_file(tar_path, "/www/wwwroot") + else: + print(f" ⚠️ 部署包不存在: {tar_path}") + + # 4. 解压并部署 + print("\n🔧 步骤 4: 解压部署包") + commands = [ + f"cd /www/wwwroot && tar -xzf ai-interview-deploy.tar.gz -C {DEPLOY_PATH}", + f"cd {DEPLOY_PATH}/deploy && cp .env .env.bak 2>/dev/null || true", + ] + for cmd in commands: + exec_shell(cmd) + time.sleep(1) + + # 5. 启动 Docker + print("\n🐳 步骤 5: 启动 Docker 容器") + docker_commands = [ + f"cd {DEPLOY_PATH}/deploy && docker-compose down 2>/dev/null || true", + f"cd {DEPLOY_PATH}/deploy && docker-compose up -d --build", + ] + for cmd in docker_commands: + print(f" 执行: {cmd}") + result = exec_shell(cmd) + print(f" 结果: {result}") + time.sleep(3) + + # 6. 创建网站和反向代理 + print("\n🌐 步骤 6: 配置网站") + create_website(DOMAIN) + time.sleep(2) + set_proxy(DOMAIN, "http://127.0.0.1:3000") + + # 7. 检查状态 + print("\n🔍 步骤 7: 检查部署状态") + result = exec_shell("docker ps --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}'") + print(f" 容器状态: {result}") + + print() + print("=" * 60) + print("✅ 部署完成!") + print("=" * 60) + print() + print(f"🌐 访问地址:") + print(f" 用户端: http://{DOMAIN}") + print(f" 管理后台: http://{DOMAIN}/admin") + print() + print("⚠️ 如需 HTTPS,请在宝塔面板中为该网站申请 SSL 证书") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/build_only.py b/deploy/build_only.py new file mode 100644 index 0000000..c6bf3aa --- /dev/null +++ b/deploy/build_only.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +仅执行构建命令 +""" +import requests +import time +import hashlib + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_task(shell_body, task_name, wait_time=30): + # 先清理旧任务 + result = bt_api("crontab?action=GetCrontab", {"page": 1, "limit": 100}) + if isinstance(result, dict) and result.get("data"): + for task in result["data"]: + if isinstance(task, dict) and ("upload_" in task.get("name", "") or "build_" in task.get("name", "")): + bt_api("crontab?action=DelCrontab", {"id": task["id"]}) + + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": shell_body, + }) + if not result.get("status") or not result.get("id"): + print(f"创建任务失败: {result}") + return None + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=600) + print(f"任务 {cron_id} 启动,等待 {wait_time}s...") + time.sleep(wait_time) + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + return log_result + + +def main(): + print("=" * 60) + print("🐳 构建 Docker 容器") + print("=" * 60) + + # 先清理磁盘空间 + print("\n🧹 步骤 1: 清理磁盘空间...") + cleanup_script = """#!/bin/bash +echo "清理 Docker..." +docker system prune -af --volumes 2>/dev/null || true +docker builder prune -af 2>/dev/null || true + +echo "" +echo "清理宝塔日志..." +rm -rf /www/wwwlogs/*.log 2>/dev/null || true +find /www/server/panel/logs -name "*.log" -mtime +1 -delete 2>/dev/null || true + +echo "" +echo "磁盘使用:" +df -h / +""" + result = run_task(cleanup_script, "cleanup", wait_time=30) + if result and result.get("msg"): + print(result["msg"][:1000]) + + # 构建 + print("\n🐳 步骤 2: 构建并启动...") + build_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy + +echo "目录检查:" +ls -la {DEPLOY_PATH}/frontend/ | head -5 +ls -la {DEPLOY_PATH}/backend/ | head -5 + +echo "" +echo "构建并启动..." +docker-compose up -d --build 2>&1 + +sleep 20 + +echo "" +echo "容器状态:" +docker ps -a --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' + +echo "" +echo "后端日志:" +docker logs --tail 30 ai-interview-backend 2>&1 + +echo "" +echo "测试:" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端未响应" +echo "" +curl -s -o /dev/null -w "前端: %{{http_code}}" http://127.0.0.1:3000 2>&1 +""" + + result = run_task(build_script, "build", wait_time=180) + + if result and result.get("msg"): + print("\n📋 构建结果:") + print("-" * 60) + print(result["msg"]) + + print("\n" + "=" * 60) + print("🌐 http://interview.test.ai.ireborn.com.cn") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/check_docker.py b/deploy/check_docker.py new file mode 100644 index 0000000..64af737 --- /dev/null +++ b/deploy/check_docker.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +检查 Docker 容器状态 +""" +import requests +import time +import hashlib + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_and_get_log(shell_body, task_name): + """创建计划任务,运行并获取日志""" + # 1. 创建任务 + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sBody": shell_body, + "sName": "", + "backupTo": "", + "save": "", + "urladdress": "" + }) + + if not result.get("status") or not result.get("id"): + print(f"创建任务失败: {result}") + return None + + cron_id = result["id"] + print(f"任务创建成功,ID: {cron_id}") + + # 2. 执行任务 + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=120) + print("任务已启动,等待执行...") + time.sleep(10) + + # 3. 获取日志 + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + + # 4. 删除任务 + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + + return log_result + + +def main(): + print("=" * 60) + print("🔍 检查 Docker 容器状态") + print("=" * 60) + print() + + # 检查命令 + check_script = f"""#!/bin/bash +echo "========== Docker 容器状态 ==========" +docker ps -a +echo "" +echo "========== Docker Compose 状态 ==========" +cd {DEPLOY_PATH}/deploy && docker-compose ps 2>&1 || echo "docker-compose 未运行" +echo "" +echo "========== 后端容器日志 (最后 50 行) ==========" +docker logs --tail 50 ai-interview-backend 2>&1 || echo "后端容器不存在" +echo "" +echo "========== 检查端口 ==========" +netstat -tlnp | grep -E ':(3000|8000)' || ss -tlnp | grep -E ':(3000|8000)' || echo "端口未监听" +echo "" +echo "========== 测试本地服务 ==========" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端 8000 端口无响应" +curl -s -o /dev/null -w "前端 3000 端口: HTTP %{{http_code}}" http://127.0.0.1:3000 2>&1 || echo "前端 3000 端口无响应" +""" + + result = run_and_get_log(check_script, f"check_docker_{int(time.time())}") + + if result: + print("\n📋 执行结果:") + print("-" * 60) + if isinstance(result, dict) and result.get("msg"): + print(result["msg"]) + else: + print(result) + + print() + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/cleanup_and_build.py b/deploy/cleanup_and_build.py new file mode 100644 index 0000000..c10464a --- /dev/null +++ b/deploy/cleanup_and_build.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +清理宝塔旧任务后再构建 +""" +import requests +import time +import hashlib + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def cleanup_old_tasks(): + """清理所有旧的计划任务""" + print("🧹 清理旧计划任务...") + result = bt_api("crontab?action=GetCrontab", {"page": 1, "limit": 100}) + + deleted = 0 + if isinstance(result, list): + for task in result: + if isinstance(task, dict) and task.get("id"): + name = task.get("name", "") + # 删除我们创建的临时任务 + if any(x in name for x in ["upload_", "build_", "cleanup", "restart", "check", "create", "update", "rebuild"]): + bt_api("crontab?action=DelCrontab", {"id": task["id"]}) + deleted += 1 + print(f" 删除: {name}") + elif isinstance(result, dict) and result.get("data"): + for task in result["data"]: + if isinstance(task, dict) and task.get("id"): + name = task.get("name", "") + if any(x in name for x in ["upload_", "build_", "cleanup", "restart", "check", "create", "update", "rebuild"]): + bt_api("crontab?action=DelCrontab", {"id": task["id"]}) + deleted += 1 + print(f" 删除: {name}") + + print(f" 已删除 {deleted} 个旧任务") + return deleted + + +def run_task(shell_body, task_name, wait_time=30): + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": shell_body, + }) + if not result.get("status") or not result.get("id"): + print(f" 创建任务失败: {result}") + return None + cron_id = result["id"] + print(f" 任务 {cron_id} 已创建") + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=600) + print(f" 执行中,等待 {wait_time}s...") + time.sleep(wait_time) + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + return log_result + + +def main(): + print("=" * 60) + print("🚀 清理并构建") + print("=" * 60) + + # 1. 清理旧任务 + cleanup_old_tasks() + time.sleep(2) + + # 2. 构建 + print("\n🐳 构建 Docker 容器...") + build_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy + +# 清理空间 +docker system prune -af 2>/dev/null || true + +# 构建 +docker-compose up -d --build 2>&1 + +sleep 15 + +echo "容器状态:" +docker ps -a --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' + +echo "" +echo "后端日志:" +docker logs --tail 20 ai-interview-backend 2>&1 + +echo "" +echo "测试:" +curl -s http://127.0.0.1:8000/health || echo "后端未响应" +curl -s -o /dev/null -w " 前端:%{{http_code}}" http://127.0.0.1:3000 +""" + + result = run_task(build_script, "deploy", wait_time=180) + + if result and result.get("msg"): + print("\n📋 结果:") + print("-" * 60) + print(result["msg"]) + + print("\n" + "=" * 60) + print("🌐 http://interview.test.ai.ireborn.com.cn") + + +if __name__ == "__main__": + main() diff --git a/deploy/cleanup_bt.py b/deploy/cleanup_bt.py new file mode 100644 index 0000000..4c7f4fe --- /dev/null +++ b/deploy/cleanup_bt.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +深度清理宝塔面板 +""" +import requests +import time +import hashlib + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def main(): + print("=" * 60) + print("🧹 深度清理宝塔面板") + print("=" * 60) + + # 1. 删除所有计划任务 + print("\n1. 清理计划任务...") + result = bt_api("crontab?action=GetCrontab", {"page": 1, "limit": 100}) + + deleted = 0 + tasks_to_delete = [] + + if isinstance(result, list): + tasks_to_delete = result + elif isinstance(result, dict) and result.get("data"): + tasks_to_delete = result["data"] + + for task in tasks_to_delete: + if isinstance(task, dict) and task.get("id"): + name = task.get("name", "") + # 删除所有临时任务 + bt_api("crontab?action=DelCrontab", {"id": task["id"]}) + deleted += 1 + print(f" 删除: {name} (ID: {task['id']})") + + print(f" 共删除 {deleted} 个任务") + + # 2. 清理计划任务日志 + print("\n2. 清理任务日志...") + result = bt_api("crontab?action=DelLogs") + print(f" 结果: {result}") + + # 3. 清理面板日志 + print("\n3. 清理面板日志...") + result = bt_api("system?action=ClearCache") + print(f" 结果: {result}") + + # 4. 尝试压缩数据库 + print("\n4. 压缩数据库...") + result = bt_api("system?action=ReDatabase") + print(f" 结果: {result}") + + time.sleep(3) + + # 5. 再次尝试创建任务 + print("\n5. 测试创建任务...") + result = bt_api("crontab?action=AddCrontab", { + "name": "test_task", + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": "echo test", + }) + + if result.get("status") and result.get("id"): + print(f" ✅ 成功! 任务ID: {result['id']}") + # 删除测试任务 + bt_api("crontab?action=DelCrontab", {"id": result["id"]}) + + # 现在执行构建 + print("\n6. 执行构建...") + build_result = bt_api("crontab?action=AddCrontab", { + "name": "deploy_docker", + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy +docker system prune -af 2>/dev/null || true +docker-compose up -d --build 2>&1 +sleep 15 +docker ps -a +docker logs --tail 20 ai-interview-backend 2>&1 +curl -s http://127.0.0.1:8000/health || echo "后端未响应" +""", + }) + + if build_result.get("id"): + cron_id = build_result["id"] + print(f" 任务 {cron_id} 已创建,开始执行...") + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=600) + print(" 等待 180 秒...") + time.sleep(180) + + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + + if log_result and log_result.get("msg"): + print("\n📋 构建结果:") + print("-" * 60) + print(log_result["msg"]) + else: + print(f" ❌ 仍然失败: {result}") + + print("\n" + "=" * 60) + print("🌐 http://interview.test.ai.ireborn.com.cn") + + +if __name__ == "__main__": + main() diff --git a/deploy/create_files.py b/deploy/create_files.py new file mode 100644 index 0000000..92dbe57 --- /dev/null +++ b/deploy/create_files.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +通过宝塔计划任务创建文件 +""" +import requests +import time +import hashlib +import os +import base64 + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +DEPLOY_PATH = "/www/wwwroot/ai-interview" +LOCAL_BACKEND = "/Users/a111/Documents/AgentWD/projects/011-ai-interview-2601/backend" +LOCAL_DEPLOY = "/Users/a111/Documents/AgentWD/projects/011-ai-interview-2601/deploy" +LOCAL_FRONTEND = "/Users/a111/Documents/AgentWD/projects/011-ai-interview-2601/frontend" + + +def bt_api(action, data=None, timeout=60): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:1000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_task(shell_body, task_name, wait_time=30): + """创建计划任务并执行""" + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sBody": shell_body, + "sName": "", + "backupTo": "", + "save": "", + "urladdress": "" + }) + + if not result.get("status") or not result.get("id"): + print(f" 创建任务失败: {result}") + return None + + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=300) + print(f" 任务 {cron_id} 已启动,等待 {wait_time} 秒...") + time.sleep(wait_time) + + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + + return log_result + + +def create_file_via_shell(remote_path, content): + """通过 shell 命令创建文件""" + # 使用 base64 编码内容以避免特殊字符问题 + encoded = base64.b64encode(content.encode('utf-8')).decode('utf-8') + + # 确保目录存在,然后写入文件 + dir_path = os.path.dirname(remote_path) + shell_script = f"""#!/bin/bash +mkdir -p {dir_path} +echo '{encoded}' | base64 -d > {remote_path} +echo "文件已创建: {remote_path}" +ls -la {remote_path} +""" + return shell_script + + +def main(): + print("=" * 60) + print("📤 创建所有必需文件") + print("=" * 60) + print() + + # 读取所有需要上传的文件 + files_to_create = [] + + # 1. requirements.txt + with open(os.path.join(LOCAL_BACKEND, "requirements.txt"), 'r') as f: + files_to_create.append((f"{DEPLOY_PATH}/backend/requirements.txt", f.read())) + + # 2. Dockerfile.frontend + with open(os.path.join(LOCAL_DEPLOY, "Dockerfile.frontend"), 'r') as f: + files_to_create.append((f"{DEPLOY_PATH}/deploy/Dockerfile.frontend", f.read())) + + # 批量创建文件 + print("📁 创建目录结构和文件...") + + # 合并所有文件创建命令 + all_commands = f"""#!/bin/bash +mkdir -p {DEPLOY_PATH}/backend +mkdir -p {DEPLOY_PATH}/backend/app/routers +mkdir -p {DEPLOY_PATH}/backend/app/services +mkdir -p {DEPLOY_PATH}/deploy/nginx +mkdir -p {DEPLOY_PATH}/frontend +mkdir -p {DEPLOY_PATH}/deploy/uploads +""" + + for remote_path, content in files_to_create: + encoded = base64.b64encode(content.encode('utf-8')).decode('utf-8') + all_commands += f""" +echo "创建: {remote_path}" +echo '{encoded}' | base64 -d > {remote_path} +""" + + all_commands += f""" +echo "" +echo "文件列表:" +ls -la {DEPLOY_PATH}/backend/ +ls -la {DEPLOY_PATH}/deploy/ +""" + + result = run_task(all_commands, f"create_files_{int(time.time())}", wait_time=15) + + if result and result.get("msg"): + print("\n📋 执行结果:") + print(result["msg"][:1500]) + + # 3. 重新构建 Docker + print("\n🐳 重新构建 Docker 容器...") + rebuild_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy +echo "停止旧容器..." +docker-compose down 2>/dev/null || true +sleep 2 +echo "重新构建..." +docker-compose up -d --build 2>&1 +sleep 15 +echo "" +echo "容器状态:" +docker ps --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' +echo "" +echo "后端容器日志:" +docker logs --tail 30 ai-interview-backend 2>&1 +echo "" +echo "测试后端:" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端未响应" +""" + + result = run_task(rebuild_script, f"rebuild_{int(time.time())}", wait_time=90) + + if result and result.get("msg"): + print("\n📋 构建结果:") + print("-" * 60) + print(result["msg"][:3000]) + + print() + print("=" * 60) + print("✅ 完成!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..25e70d4 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# AI 面试系统部署脚本 +# 使用方法: bash deploy.sh + +set -e + +echo "==========================================" +echo "AI 语音面试系统 - Docker 部署" +echo "==========================================" + +# 配置 +DEPLOY_DIR="/www/wwwroot/ai-interview" +DOMAIN="interview.test.ai.ireborn.com.cn" + +# 检查 Docker +if ! command -v docker &> /dev/null; then + echo "❌ Docker 未安装,请先安装 Docker" + exit 1 +fi + +if ! command -v docker-compose &> /dev/null; then + echo "❌ Docker Compose 未安装,请先安装" + exit 1 +fi + +echo "✅ Docker 环境检查通过" + +# 创建部署目录 +echo "📁 创建部署目录..." +mkdir -p $DEPLOY_DIR +cd $DEPLOY_DIR + +# 检查 .env 文件 +if [ ! -f "deploy/.env" ]; then + echo "⚠️ 未找到 .env 文件,正在创建..." + mkdir -p deploy + cat > deploy/.env << 'EOF' +# Coze 配置 +COZE_PAT_TOKEN=pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT +COZE_BOT_ID=7595113005181386792 + +# 工作流 ID +COZE_WORKFLOW_A_ID=7597357422713798710 +COZE_WORKFLOW_C_ID=7597376294612107318 + +# 文件服务器 +FILE_SERVER_URL=https://files.test.ai.ireborn.com.cn +FILE_SERVER_TOKEN=ai_interview_2026_secret +EOF + echo "✅ .env 文件已创建" +fi + +# 构建并启动 +echo "🐳 构建 Docker 镜像..." +cd deploy +docker-compose down 2>/dev/null || true +docker-compose up -d --build + +echo "⏳ 等待服务启动..." +sleep 10 + +# 检查服务状态 +echo "🔍 检查服务状态..." +docker-compose ps + +echo "" +echo "==========================================" +echo "✅ 部署完成!" +echo "==========================================" +echo "" +echo "访问地址:" +echo " 用户端: http://$DOMAIN" +echo " 管理后台: http://$DOMAIN/admin" +echo " 后端 API: http://$DOMAIN/api" +echo "" +echo "容器状态:" +docker-compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" +echo "" +echo "查看日志: docker-compose logs -f" +echo "==========================================" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..cd85987 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + frontend: + build: + context: .. + dockerfile: deploy/Dockerfile.frontend + container_name: ai-interview-frontend + ports: + - "3000:80" + depends_on: + - backend + networks: + - ai-interview-network + restart: unless-stopped + + backend: + build: + context: .. + dockerfile: deploy/Dockerfile.backend + container_name: ai-interview-backend + ports: + - "8000:8000" + environment: + - COZE_PAT_TOKEN=${COZE_PAT_TOKEN} + - COZE_BOT_ID=${COZE_BOT_ID} + - COZE_WORKFLOW_A_ID=${COZE_WORKFLOW_A_ID} + - COZE_WORKFLOW_C_ID=${COZE_WORKFLOW_C_ID} + - FILE_SERVER_URL=${FILE_SERVER_URL} + - FILE_SERVER_TOKEN=${FILE_SERVER_TOKEN} + volumes: + - ./uploads:/app/uploads + networks: + - ai-interview-network + restart: unless-stopped + +networks: + ai-interview-network: + driver: bridge diff --git a/deploy/env.example b/deploy/env.example new file mode 100644 index 0000000..2beb90f --- /dev/null +++ b/deploy/env.example @@ -0,0 +1,11 @@ +# Coze 配置 +COZE_PAT_TOKEN=pat_xxx +COZE_BOT_ID=7595113005181386792 + +# 工作流 ID +COZE_WORKFLOW_A_ID=7597357422713798710 +COZE_WORKFLOW_C_ID=7597376294612107318 + +# 文件服务器 +FILE_SERVER_URL=https://files.test.ai.ireborn.com.cn +FILE_SERVER_TOKEN=ai_interview_2026_secret diff --git a/deploy/env.production b/deploy/env.production new file mode 100644 index 0000000..8b6184d --- /dev/null +++ b/deploy/env.production @@ -0,0 +1,21 @@ +# AI Interview 后端环境变量 +# ============================================ +# 部署步骤: +# 1. 复制此文件到服务器: /www/wwwroot/ai-interview/deploy/.env +# 2. 或者在服务器上执行: cp env.production .env +# ============================================ + +# Coze 配置 +COZE_PAT_TOKEN=pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT +COZE_BOT_ID=7595113005181386792 + +# 工作流 ID +COZE_WORKFLOW_A_ID=7597357422713798710 +COZE_WORKFLOW_C_ID=7597376294612107318 + +# 文件服务器 +FILE_SERVER_URL=https://files.test.ai.ireborn.com.cn +FILE_SERVER_TOKEN=ai_interview_2026_secret + +# CORS 允许的域名(逗号分隔) +CORS_ORIGINS=http://interview.test.ai.ireborn.com.cn,https://interview.test.ai.ireborn.com.cn,http://localhost:5173 diff --git a/deploy/fix_deploy.py b/deploy/fix_deploy.py new file mode 100644 index 0000000..e212a29 --- /dev/null +++ b/deploy/fix_deploy.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +修复部署 - 上传配置文件并重启服务 +""" +import requests +import time +import hashlib +import json + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +# 部署配置 +DEPLOY_PATH = "/www/wwwroot/ai-interview" +DOMAIN = "interview.test.ai.ireborn.com.cn" + + +def bt_api(action, data=None, timeout=60): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + return response.json() + except requests.exceptions.JSONDecodeError: + return {"status": True, "msg": response.text[:500]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def write_remote_file(path, content): + """写入远程文件""" + print(f"📝 写入文件: {path}") + result = bt_api("files?action=SaveFileBody", { + "path": path, + "data": content, + "encoding": "utf-8" + }) + if result.get("status"): + print(" ✅ 文件写入成功") + return True + print(f" 结果: {result}") + return result.get("status", False) + + +def exec_shell(command, timeout=300): + """执行 Shell 命令 - 使用宝塔终端 API""" + print(f"🔧 执行: {command[:80]}...") + + # 尝试多个可能的 API 端点 + endpoints = [ + ("deployment?action=ExecCommand", {"command": command}), + ("plugin?action=a&s=deployment&name=exec_command", {"command": command}), + ("crontab?action=GetDataList", {}), # 先获取计划任务列表 + ] + + # 方法1: 使用 files 接口执行命令(某些宝塔版本支持) + result = bt_api("files?action=ExecCommand", {"command": command}, timeout=timeout) + if result.get("status") or "404" not in str(result.get("msg", "")): + print(f" 结果: {str(result)[:200]}") + return result + + # 方法2: 创建临时脚本并执行 + script_path = "/tmp/bt_exec_cmd.sh" + write_result = bt_api("files?action=SaveFileBody", { + "path": script_path, + "data": f"#!/bin/bash\n{command}\n", + "encoding": "utf-8" + }) + + if write_result.get("status"): + # 设置执行权限并运行 + result = bt_api("files?action=ExecCommand", {"command": f"chmod +x {script_path} && {script_path}"}, timeout=timeout) + print(f" 结果: {str(result)[:200]}") + return result + + print(f" ⚠️ 无法执行命令,请手动在服务器执行") + return {"status": False, "msg": "需要手动执行"} + + +def restart_nginx(): + """重启 Nginx""" + print("🔄 重启 Nginx...") + result = bt_api("system?action=ServiceAdmin", { + "name": "nginx", + "type": "restart" + }) + print(f" 结果: {result}") + return result + + +def main(): + print("=" * 60) + print("🔧 AI 面试系统 - 修复部署") + print("=" * 60) + print() + + # 1. 写入 .env 文件 + print("\n📝 步骤 1: 写入环境变量文件") + env_content = """# AI Interview 后端环境变量 +COZE_PAT_TOKEN=pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT +COZE_BOT_ID=7595113005181386792 +COZE_WORKFLOW_A_ID=7597357422713798710 +COZE_WORKFLOW_C_ID=7597376294612107318 +FILE_SERVER_URL=https://files.test.ai.ireborn.com.cn +FILE_SERVER_TOKEN=ai_interview_2026_secret +CORS_ORIGINS=http://interview.test.ai.ireborn.com.cn,https://interview.test.ai.ireborn.com.cn,http://localhost:5173 +""" + write_remote_file(f"{DEPLOY_PATH}/deploy/.env", env_content) + + # 2. 写入 Nginx 配置 + print("\n📝 步骤 2: 写入 Nginx 反向代理配置") + nginx_config = """server { + listen 80; + server_name interview.test.ai.ireborn.com.cn; + + index index.html; + + access_log /www/wwwlogs/interview.test.ai.ireborn.com.cn.log; + error_log /www/wwwlogs/interview.test.ai.ireborn.com.cn.error.log; + + # 前端 - 代理到 Docker 前端容器 (3000 端口) + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + } + + # API - 代理到 Docker 后端容器 (8000 端口) + location /api/ { + proxy_pass http://127.0.0.1:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + } + + # 健康检查端点 + location /health { + proxy_pass http://127.0.0.1:8000/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + } +} +""" + write_remote_file(f"/www/server/panel/vhost/nginx/{DOMAIN}.conf", nginx_config) + + # 3. 写入 Docker 重启脚本 + print("\n📝 步骤 3: 写入 Docker 重启脚本") + restart_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy +docker-compose down 2>/dev/null || true +sleep 2 +docker-compose up -d --build +sleep 5 +docker ps +""" + write_remote_file(f"{DEPLOY_PATH}/restart_docker.sh", restart_script) + + # 4. 重启 Nginx + print("\n🔄 步骤 4: 重启 Nginx") + restart_nginx() + + print() + print("=" * 60) + print("✅ 配置文件已上传!") + print("=" * 60) + print() + print("⚠️ 请在服务器上手动执行以下命令重启 Docker:") + print() + print(f" chmod +x {DEPLOY_PATH}/restart_docker.sh") + print(f" {DEPLOY_PATH}/restart_docker.sh") + print() + print(" 或者:") + print() + print(f" cd {DEPLOY_PATH}/deploy") + print(" docker-compose down && docker-compose up -d --build") + print() + print(f"🌐 配置完成后访问:") + print(f" 用户端: http://{DOMAIN}") + print(f" 管理后台: http://{DOMAIN}/admin") + print(f" API: http://{DOMAIN}/api/health") + print() + + +if __name__ == "__main__": + main() diff --git a/deploy/nginx/frontend.conf b/deploy/nginx/frontend.conf new file mode 100644 index 0000000..670f1e7 --- /dev/null +++ b/deploy/nginx/frontend.conf @@ -0,0 +1,38 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # 前端静态资源 + location / { + try_files $uri $uri/ /index.html; + } + + # API 代理到后端 + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; +} diff --git a/deploy/nginx/interview.test.ai.ireborn.com.cn.conf b/deploy/nginx/interview.test.ai.ireborn.com.cn.conf new file mode 100644 index 0000000..e63b4c0 --- /dev/null +++ b/deploy/nginx/interview.test.ai.ireborn.com.cn.conf @@ -0,0 +1,68 @@ +# interview.test.ai.ireborn.com.cn Nginx 配置 +# 宝塔面板配置:网站 -> 添加站点 -> 设置 -> 配置文件 -> 粘贴此内容 + +server { + listen 80; + listen 443 ssl http2; + server_name interview.test.ai.ireborn.com.cn; + + index index.html; + + # SSL 证书 (宝塔面板会自动生成,如果没有可以先注释掉 ssl 相关配置) + # ssl_certificate /www/server/panel/vhost/cert/interview.test.ai.ireborn.com.cn/fullchain.pem; + # ssl_certificate_key /www/server/panel/vhost/cert/interview.test.ai.ireborn.com.cn/privkey.pem; + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; + # ssl_prefer_server_ciphers on; + # ssl_session_timeout 10m; + # ssl_session_cache shared:SSL:10m; + + # 日志 + access_log /www/wwwlogs/interview.test.ai.ireborn.com.cn.log; + error_log /www/wwwlogs/interview.test.ai.ireborn.com.cn.error.log; + + # 前端 - 代理到 Docker 前端容器 (3000 端口) + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + } + + # API - 代理到 Docker 后端容器 (8000 端口) + location /api/ { + proxy_pass http://127.0.0.1:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + + # CORS 配置 + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + + # 处理 OPTIONS 预检请求 + if ($request_method = 'OPTIONS') { + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; + add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; + add_header Content-Length 0; + return 204; + } + } + + # 健康检查端点 + location /health { + proxy_pass http://127.0.0.1:8000/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + } +} diff --git a/deploy/rebuild.py b/deploy/rebuild.py new file mode 100644 index 0000000..c13faaa --- /dev/null +++ b/deploy/rebuild.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +上传新的 Dockerfile 并重新构建 +""" +import requests +import time +import hashlib +import os +import base64 + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:1000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_task(shell_body, task_name, wait_time=30): + """创建计划任务并执行""" + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sBody": shell_body, + "sName": "", + "backupTo": "", + "save": "", + "urladdress": "" + }) + + if not result.get("status") or not result.get("id"): + print(f" 创建任务失败: {result}") + return None + + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=600) + print(f" 任务 {cron_id} 已启动,等待 {wait_time} 秒...") + time.sleep(wait_time) + + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + + return log_result + + +def main(): + print("=" * 60) + print("🐳 更新 Dockerfile 并重新构建") + print("=" * 60) + print() + + # 1. 读取新的 Dockerfile + dockerfile_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Dockerfile.backend") + with open(dockerfile_path, 'r') as f: + dockerfile_content = f.read() + + # 2. 通过 shell 命令更新文件 + encoded = base64.b64encode(dockerfile_content.encode('utf-8')).decode('utf-8') + + update_script = f"""#!/bin/bash +echo "更新 Dockerfile.backend..." +echo '{encoded}' | base64 -d > {DEPLOY_PATH}/deploy/Dockerfile.backend +cat {DEPLOY_PATH}/deploy/Dockerfile.backend +""" + + print("📝 步骤 1: 更新 Dockerfile...") + result = run_task(update_script, f"update_dockerfile_{int(time.time())}", wait_time=10) + if result and result.get("msg"): + print(result["msg"][:1000]) + + # 3. 重新构建 + print("\n🐳 步骤 2: 重新构建 Docker 容器 (使用阿里云镜像)...") + rebuild_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy + +echo "停止旧容器..." +docker-compose down 2>/dev/null || true +sleep 2 + +echo "删除旧镜像..." +docker rmi deploy-backend 2>/dev/null || true + +echo "重新构建 (这可能需要几分钟)..." +docker-compose build --no-cache backend 2>&1 + +echo "" +echo "启动容器..." +docker-compose up -d 2>&1 + +sleep 10 + +echo "" +echo "========== 容器状态 ==========" +docker ps --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' + +echo "" +echo "========== 后端日志 ==========" +docker logs --tail 30 ai-interview-backend 2>&1 + +echo "" +echo "========== 测试服务 ==========" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端未响应" +echo "" +curl -s -o /dev/null -w "前端: HTTP %{{http_code}}" http://127.0.0.1:3000 2>&1 +""" + + result = run_task(rebuild_script, f"rebuild_{int(time.time())}", wait_time=180) + + if result and result.get("msg"): + print("\n📋 构建结果:") + print("-" * 60) + print(result["msg"]) + + print() + print("=" * 60) + print("✅ 完成!") + print("=" * 60) + print() + print("🌐 访问地址:") + print(" http://interview.test.ai.ireborn.com.cn") + print(" http://interview.test.ai.ireborn.com.cn/admin") + + +if __name__ == "__main__": + main() diff --git a/deploy/restart_docker.py b/deploy/restart_docker.py new file mode 100644 index 0000000..ce65562 --- /dev/null +++ b/deploy/restart_docker.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +通过宝塔计划任务重启 Docker 容器 +""" +import requests +import time +import hashlib +import json + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:500]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def add_crontab(name, shell_body): + """添加计划任务""" + print(f"📋 添加计划任务: {name}") + result = bt_api("crontab?action=AddCrontab", { + "name": name, + "type": "minute-n", + "where1": "1", + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sBody": shell_body, + "sName": "", + "backupTo": "", + "save": "", + "urladdress": "" + }) + print(f" 结果: {result}") + return result + + +def exec_crontab(cron_id): + """立即执行计划任务""" + print(f"▶️ 执行计划任务 ID: {cron_id}") + result = bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=300) + print(f" 结果: {result}") + return result + + +def delete_crontab(cron_id): + """删除计划任务""" + print(f"🗑️ 删除计划任务 ID: {cron_id}") + result = bt_api("crontab?action=DelCrontab", {"id": cron_id}) + print(f" 结果: {result}") + return result + + +def get_crontab_list(): + """获取计划任务列表""" + result = bt_api("crontab?action=GetCrontab", {"page": 1, "search": ""}) + return result + + +def main(): + print("=" * 60) + print("🐳 重启 Docker 容器") + print("=" * 60) + print() + + # Docker 重启命令 + shell_body = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy +docker-compose down 2>/dev/null || true +sleep 2 +docker-compose up -d --build +sleep 5 +echo "Docker 容器状态:" +docker ps --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' +echo "" +echo "测试后端服务:" +curl -s http://127.0.0.1:8000/health || echo "后端未响应" +echo "" +echo "测试前端服务:" +curl -s -o /dev/null -w "HTTP Status: %{{http_code}}" http://127.0.0.1:3000 || echo "前端未响应" +""" + + # 1. 添加计划任务 + task_name = f"restart_docker_{int(time.time())}" + result = add_crontab(task_name, shell_body) + + if result.get("status") and result.get("id"): + cron_id = result["id"] + print(f"\n✅ 计划任务创建成功,ID: {cron_id}") + + # 2. 立即执行 + print("\n🚀 立即执行计划任务...") + exec_result = exec_crontab(cron_id) + + # 3. 等待执行完成 + print("\n⏳ 等待执行完成...") + time.sleep(30) + + # 4. 删除计划任务 + delete_crontab(cron_id) + + print("\n✅ Docker 重启命令已执行!") + else: + print(f"\n❌ 计划任务创建失败: {result}") + print("\n⚠️ 请手动在服务器执行:") + print(f" cd {DEPLOY_PATH}/deploy") + print(" docker-compose down && docker-compose up -d --build") + + print() + print("=" * 60) + print("🌐 访问地址:") + print(" http://interview.test.ai.ireborn.com.cn") + print(" http://interview.test.ai.ireborn.com.cn/admin") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/setup-server.sh b/deploy/setup-server.sh new file mode 100644 index 0000000..658855a --- /dev/null +++ b/deploy/setup-server.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# AI Interview 服务器部署脚本 +# ============================================ +# 在服务器上执行此脚本来快速部署 +# +# 使用方法: +# chmod +x setup-server.sh +# ./setup-server.sh +# ============================================ + +set -e + +echo "==========================================" +echo "AI Interview 部署脚本" +echo "==========================================" + +# 项目目录 +PROJECT_DIR="/www/wwwroot/ai-interview" +DEPLOY_DIR="$PROJECT_DIR/deploy" + +# 进入部署目录 +cd $DEPLOY_DIR + +# 1. 复制环境变量文件 +if [ ! -f ".env" ]; then + echo "[1/4] 创建 .env 文件..." + cp env.production .env + echo "✅ .env 文件已创建" +else + echo "[1/4] .env 文件已存在,跳过" +fi + +# 2. 创建上传目录 +echo "[2/4] 创建上传目录..." +mkdir -p $DEPLOY_DIR/uploads +chmod 755 $DEPLOY_DIR/uploads +echo "✅ 上传目录已创建" + +# 3. 停止旧容器 +echo "[3/4] 停止旧容器..." +docker-compose down 2>/dev/null || true +echo "✅ 旧容器已停止" + +# 4. 构建并启动新容器 +echo "[4/4] 构建并启动容器..." +docker-compose up -d --build + +# 等待服务启动 +echo "" +echo "等待服务启动..." +sleep 5 + +# 检查容器状态 +echo "" +echo "==========================================" +echo "容器状态:" +echo "==========================================" +docker-compose ps + +# 检查服务健康 +echo "" +echo "==========================================" +echo "健康检查:" +echo "==========================================" + +# 检查后端 +if curl -s http://127.0.0.1:8000/health > /dev/null 2>&1; then + echo "✅ 后端服务正常 (http://127.0.0.1:8000)" +else + echo "❌ 后端服务未响应" + echo "查看后端日志: docker logs ai-interview-backend" +fi + +# 检查前端 +if curl -s http://127.0.0.1:3000 > /dev/null 2>&1; then + echo "✅ 前端服务正常 (http://127.0.0.1:3000)" +else + echo "❌ 前端服务未响应" + echo "查看前端日志: docker logs ai-interview-frontend" +fi + +echo "" +echo "==========================================" +echo "部署完成!" +echo "==========================================" +echo "" +echo "访问地址:" +echo " - 用户端: http://interview.test.ai.ireborn.com.cn" +echo " - 后台: http://interview.test.ai.ireborn.com.cn/admin" +echo " - API: http://interview.test.ai.ireborn.com.cn/api/health" +echo "" +echo "常用命令:" +echo " - 查看日志: docker-compose logs -f" +echo " - 重启服务: docker-compose restart" +echo " - 停止服务: docker-compose down" +echo "" diff --git a/deploy/start_backend_only.py b/deploy/start_backend_only.py new file mode 100644 index 0000000..99ae7a0 --- /dev/null +++ b/deploy/start_backend_only.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +只启动后端容器 +""" +import requests +import time +import hashlib + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_task(shell_body, task_name, wait_time=30): + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": shell_body, + }) + if not result.get("status") or not result.get("id"): + print(f"创建任务失败: {result}") + return None + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=300) + print(f"任务 {cron_id} 已启动,等待 {wait_time} 秒...") + time.sleep(wait_time) + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + return log_result + + +def main(): + print("=" * 60) + print("🚀 单独启动后端容器") + print("=" * 60) + + # 直接用 docker run 启动后端容器 + start_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy + +# 停止旧容器 +docker stop ai-interview-backend 2>/dev/null || true +docker rm ai-interview-backend 2>/dev/null || true + +# 读取 .env 文件 +source .env 2>/dev/null || true + +echo "启动后端容器..." +docker run -d \\ + --name ai-interview-backend \\ + -p 8000:8000 \\ + -e COZE_PAT_TOKEN="$COZE_PAT_TOKEN" \\ + -e COZE_BOT_ID="$COZE_BOT_ID" \\ + -e COZE_WORKFLOW_A_ID="$COZE_WORKFLOW_A_ID" \\ + -e COZE_WORKFLOW_C_ID="$COZE_WORKFLOW_C_ID" \\ + -e FILE_SERVER_URL="$FILE_SERVER_URL" \\ + -e FILE_SERVER_TOKEN="$FILE_SERVER_TOKEN" \\ + -v {DEPLOY_PATH}/deploy/uploads:/app/uploads \\ + --restart unless-stopped \\ + deploy-backend + +sleep 5 + +echo "" +echo "========== 容器状态 ==========" +docker ps --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' + +echo "" +echo "========== 后端日志 ==========" +docker logs --tail 30 ai-interview-backend 2>&1 + +echo "" +echo "========== 测试后端 ==========" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端未响应" +""" + + result = run_task(start_script, f"start_backend_{int(time.time())}", wait_time=20) + + if result and result.get("msg"): + print("\n" + result["msg"]) + + print("\n" + "=" * 60) + print("测试后端 API:") + print(" http://interview.test.ai.ireborn.com.cn/api/health") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/start_containers.py b/deploy/start_containers.py new file mode 100644 index 0000000..a45b76f --- /dev/null +++ b/deploy/start_containers.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +启动 Docker 容器 +""" +import requests +import time +import hashlib + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_task(shell_body, task_name, wait_time=30): + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": shell_body, + }) + if not result.get("status") or not result.get("id"): + print(f"创建任务失败: {result}") + return None + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=300) + print(f"任务 {cron_id} 已启动,等待 {wait_time} 秒...") + time.sleep(wait_time) + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + return log_result + + +def main(): + print("=" * 60) + print("🚀 启动 Docker 容器") + print("=" * 60) + + start_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy + +echo "当前目录文件:" +ls -la + +echo "" +echo "启动容器..." +docker-compose up -d 2>&1 + +sleep 10 + +echo "" +echo "========== 容器状态 ==========" +docker ps -a --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' + +echo "" +echo "========== 后端日志 ==========" +docker logs --tail 30 ai-interview-backend 2>&1 + +echo "" +echo "========== 测试服务 ==========" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端未响应" +echo "" +curl -s -o /dev/null -w "前端: HTTP %{{http_code}}" http://127.0.0.1:3000 2>&1 +""" + + result = run_task(start_script, f"start_{int(time.time())}", wait_time=30) + + if result and result.get("msg"): + print("\n" + result["msg"]) + + print("\n" + "=" * 60) + print("🌐 http://interview.test.ai.ireborn.com.cn") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/upload_all.py b/deploy/upload_all.py new file mode 100644 index 0000000..cbe443c --- /dev/null +++ b/deploy/upload_all.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +上传所有代码到服务器并部署 +""" +import requests +import time +import hashlib +import os +import base64 + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" +LOCAL_PROJECT = "/Users/a111/Documents/AgentWD/projects/011-ai-interview-2601" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:1000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def run_task(shell_body, task_name, wait_time=30): + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": shell_body, + }) + if not result.get("status") or not result.get("id"): + print(f" 创建任务失败: {result}") + return None + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=600) + print(f" 任务 {cron_id} 启动,等待 {wait_time}s...") + time.sleep(wait_time) + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + return log_result + + +def upload_file_via_task(remote_path, content): + """通过计划任务上传文件""" + encoded = base64.b64encode(content.encode('utf-8')).decode('utf-8') + dir_path = os.path.dirname(remote_path) + script = f"""#!/bin/bash +mkdir -p {dir_path} +echo '{encoded}' | base64 -d > {remote_path} +""" + result = bt_api("crontab?action=AddCrontab", { + "name": f"upload_{int(time.time())}", + "type": "minute-n", + "where1": "1", + "sType": "toShell", + "sBody": script, + }) + if result.get("status") and result.get("id"): + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=60) + time.sleep(2) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + return True + return False + + +def main(): + print("=" * 60) + print("🚀 完整部署 - 上传所有代码") + print("=" * 60) + + # 前端文件列表 + frontend_files = [ + "package.json", + "pnpm-lock.yaml", + "vite.config.ts", + "tsconfig.json", + "tsconfig.node.json", + "tailwind.config.js", + "postcss.config.js", + "index.html", + "src/main.ts", + "src/App.vue", + "src/styles/index.css", + "src/router/index.ts", + "src/api/index.ts", + "src/api/request.ts", + "src/api/candidate.ts", + "src/composables/index.ts", + "src/composables/useRTC.ts", + "src/composables/useCozeRealtime.ts", + "src/types/index.ts", + "src/types/env.d.ts", + "src/layouts/AdminLayout.vue", + "src/layouts/InterviewLayout.vue", + "src/pages/interview/index.vue", + "src/pages/interview/info.vue", + "src/pages/interview/call.vue", + "src/pages/interview/complete.vue", + "src/pages/admin/index.vue", + "src/pages/admin/login.vue", + "src/pages/admin/dashboard.vue", + "src/pages/admin/interviews.vue", + "src/pages/admin/interview-detail.vue", + "src/pages/admin/configs.vue", + "src/pages/admin/layout.vue", + "src/pages/admin/[id].vue", + ] + + # 后端文件列表 + backend_files = [ + "main.py", + "requirements.txt", + "app/__init__.py", + "app/config.py", + "app/schemas.py", + "app/routers/__init__.py", + "app/routers/admin.py", + "app/routers/candidate.py", + "app/routers/chat.py", + "app/routers/files.py", + "app/routers/init.py", + "app/routers/room.py", + "app/routers/upload.py", + "app/services/__init__.py", + "app/services/coze_service.py", + ] + + # 部署配置文件 + deploy_files = [ + "docker-compose.yml", + "Dockerfile.backend", + "Dockerfile.frontend", + "nginx/frontend.conf", + ] + + print("\n📤 步骤 1: 上传前端代码...") + for f in frontend_files: + local_path = os.path.join(LOCAL_PROJECT, "frontend", f) + if os.path.exists(local_path): + with open(local_path, 'r', encoding='utf-8') as fp: + content = fp.read() + remote_path = f"{DEPLOY_PATH}/frontend/{f}" + print(f" 📝 {f}") + upload_file_via_task(remote_path, content) + else: + print(f" ⚠️ 跳过: {f}") + + print("\n📤 步骤 2: 上传后端代码...") + for f in backend_files: + local_path = os.path.join(LOCAL_PROJECT, "backend", f) + if os.path.exists(local_path): + with open(local_path, 'r', encoding='utf-8') as fp: + content = fp.read() + remote_path = f"{DEPLOY_PATH}/backend/{f}" + print(f" 📝 {f}") + upload_file_via_task(remote_path, content) + + print("\n📤 步骤 3: 上传部署配置...") + for f in deploy_files: + local_path = os.path.join(LOCAL_PROJECT, "deploy", f) + if os.path.exists(local_path): + with open(local_path, 'r', encoding='utf-8') as fp: + content = fp.read() + remote_path = f"{DEPLOY_PATH}/deploy/{f}" + print(f" 📝 {f}") + upload_file_via_task(remote_path, content) + + print("\n🐳 步骤 4: 构建并启动 Docker...") + build_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy + +echo "目录结构:" +ls -la {DEPLOY_PATH}/ +ls -la {DEPLOY_PATH}/frontend/ | head -10 +ls -la {DEPLOY_PATH}/backend/ | head -10 + +echo "" +echo "停止旧容器..." +docker-compose down 2>/dev/null || true + +echo "" +echo "删除旧镜像..." +docker rmi deploy-backend deploy-frontend 2>/dev/null || true + +echo "" +echo "构建并启动..." +docker-compose up -d --build 2>&1 + +sleep 15 + +echo "" +echo "========== 容器状态 ==========" +docker ps -a --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' + +echo "" +echo "========== 后端日志 ==========" +docker logs --tail 20 ai-interview-backend 2>&1 + +echo "" +echo "========== 测试 ==========" +curl -s http://127.0.0.1:8000/health 2>&1 || echo "后端未响应" +echo "" +curl -s -o /dev/null -w "前端: %{{http_code}}" http://127.0.0.1:3000 2>&1 +""" + + result = run_task(build_script, f"build_{int(time.time())}", wait_time=180) + + if result and result.get("msg"): + print("\n📋 构建结果:") + print("-" * 60) + print(result["msg"][-3000:]) + + print("\n" + "=" * 60) + print("🌐 http://interview.test.ai.ireborn.com.cn") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/upload_backend.py b/deploy/upload_backend.py new file mode 100644 index 0000000..8c23b51 --- /dev/null +++ b/deploy/upload_backend.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +上传后端代码到服务器并重新构建 +""" +import requests +import time +import hashlib +import os + +# 禁用 SSL 警告 +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 宝塔配置 +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" + +DEPLOY_PATH = "/www/wwwroot/ai-interview" +LOCAL_BACKEND = "/Users/a111/Documents/AgentWD/projects/011-ai-interview-2601/backend" + + +def bt_api(action, data=None, timeout=60): + """调用宝塔 API""" + if data is None: + data = {} + + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + + data['request_time'] = request_time + data['request_token'] = request_token + + url = f"{BT_PANEL}/{action}" + + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:500]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def write_remote_file(path, content): + """写入远程文件""" + print(f"📝 写入: {path}") + result = bt_api("files?action=SaveFileBody", { + "path": path, + "data": content, + "encoding": "utf-8" + }) + if result.get("status"): + print(" ✅ 成功") + return True + print(f" ❌ 失败: {result.get('msg', result)}") + return False + + +def create_directory(path): + """创建目录""" + result = bt_api("files?action=CreateDir", {"path": path}) + return result.get("status") or "已存在" in str(result.get("msg", "")) + + +def run_task(shell_body, task_name, wait_time=30): + """创建计划任务并执行""" + result = bt_api("crontab?action=AddCrontab", { + "name": task_name, + "type": "minute-n", + "where1": "1", + "hour": "", + "minute": "", + "week": "", + "sType": "toShell", + "sBody": shell_body, + "sName": "", + "backupTo": "", + "save": "", + "urladdress": "" + }) + + if not result.get("status") or not result.get("id"): + print(f"创建任务失败: {result}") + return None + + cron_id = result["id"] + bt_api("crontab?action=StartTask", {"id": cron_id}, timeout=300) + print(f" 任务 {cron_id} 已启动,等待 {wait_time} 秒...") + time.sleep(wait_time) + + log_result = bt_api("crontab?action=GetLogs", {"id": cron_id}) + bt_api("crontab?action=DelCrontab", {"id": cron_id}) + + return log_result + + +def main(): + print("=" * 60) + print("📤 上传后端代码到服务器") + print("=" * 60) + print() + + # 1. 创建目录结构 + print("📁 步骤 1: 创建目录结构") + dirs = [ + f"{DEPLOY_PATH}/backend", + f"{DEPLOY_PATH}/backend/app", + f"{DEPLOY_PATH}/backend/app/routers", + f"{DEPLOY_PATH}/backend/app/services", + ] + for d in dirs: + create_directory(d) + print(f" ✅ {d}") + + # 2. 上传后端代码 + print("\n📤 步骤 2: 上传后端代码") + + # 定义要上传的文件 + files_to_upload = [ + ("main.py", ""), + ("requirements.txt", ""), + ("app/__init__.py", "app"), + ("app/config.py", "app"), + ("app/schemas.py", "app"), + ("app/routers/__init__.py", "app/routers"), + ("app/routers/admin.py", "app/routers"), + ("app/routers/candidate.py", "app/routers"), + ("app/routers/chat.py", "app/routers"), + ("app/routers/files.py", "app/routers"), + ("app/routers/init.py", "app/routers"), + ("app/routers/room.py", "app/routers"), + ("app/routers/upload.py", "app/routers"), + ("app/services/__init__.py", "app/services"), + ("app/services/coze_service.py", "app/services"), + ] + + for local_file, subdir in files_to_upload: + local_path = os.path.join(LOCAL_BACKEND, local_file) + if os.path.exists(local_path): + with open(local_path, 'r', encoding='utf-8') as f: + content = f.read() + + if subdir: + remote_path = f"{DEPLOY_PATH}/backend/{subdir}/{os.path.basename(local_file)}" + else: + remote_path = f"{DEPLOY_PATH}/backend/{local_file}" + + write_remote_file(remote_path, content) + else: + print(f" ⚠️ 文件不存在: {local_path}") + + # 3. 上传 deploy 配置文件 + print("\n📤 步骤 3: 上传部署配置") + deploy_files = [ + "docker-compose.yml", + "Dockerfile.backend", + "Dockerfile.frontend", + ] + + for filename in deploy_files: + local_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + if os.path.exists(local_path): + with open(local_path, 'r', encoding='utf-8') as f: + content = f.read() + write_remote_file(f"{DEPLOY_PATH}/deploy/{filename}", content) + + # 上传 nginx 配置 + nginx_conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "nginx", "frontend.conf") + if os.path.exists(nginx_conf_path): + create_directory(f"{DEPLOY_PATH}/deploy/nginx") + with open(nginx_conf_path, 'r', encoding='utf-8') as f: + content = f.read() + write_remote_file(f"{DEPLOY_PATH}/deploy/nginx/frontend.conf", content) + + # 4. 重新构建 Docker 容器 + print("\n🐳 步骤 4: 重新构建 Docker 容器") + rebuild_script = f"""#!/bin/bash +cd {DEPLOY_PATH}/deploy +echo "停止旧容器..." +docker-compose down 2>/dev/null || true +sleep 2 +echo "清理旧镜像..." +docker rmi deploy-backend 2>/dev/null || true +echo "重新构建..." +docker-compose up -d --build +sleep 10 +echo "容器状态:" +docker ps --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' +echo "" +echo "后端日志:" +docker logs --tail 20 ai-interview-backend 2>&1 +""" + + print(" 🚀 执行重建命令...") + result = run_task(rebuild_script, f"rebuild_docker_{int(time.time())}", wait_time=60) + + if result and result.get("msg"): + print("\n📋 执行结果:") + print("-" * 40) + print(result["msg"][:2000]) + + print() + print("=" * 60) + print("✅ 上传完成!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/deploy/write_deploy_script.py b/deploy/write_deploy_script.py new file mode 100644 index 0000000..4f8db0c --- /dev/null +++ b/deploy/write_deploy_script.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +通过文件 API 写入部署脚本 +""" +import requests +import time +import hashlib +import base64 + +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +BT_PANEL = "http://47.107.172.23:8888" +BT_API_KEY = "PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq" +DEPLOY_PATH = "/www/wwwroot/ai-interview" + + +def bt_api(action, data=None, timeout=60): + if data is None: + data = {} + request_time = int(time.time()) + request_token = hashlib.md5( + f"{request_time}{hashlib.md5(BT_API_KEY.encode()).hexdigest()}".encode() + ).hexdigest() + data['request_time'] = request_time + data['request_token'] = request_token + url = f"{BT_PANEL}/{action}" + try: + response = requests.post(url, data=data, timeout=timeout, verify=False) + try: + return response.json() + except: + return {"status": True, "msg": response.text[:2000]} + except Exception as e: + return {"status": False, "msg": str(e)} + + +def main(): + print("=" * 60) + print("📝 写入部署脚本到服务器") + print("=" * 60) + + deploy_script = f"""#!/bin/bash +# AI Interview 部署脚本 + +cd {DEPLOY_PATH}/deploy + +echo "清理 Docker..." +docker system prune -af 2>/dev/null || true + +echo "构建并启动..." +docker-compose up -d --build + +sleep 15 + +echo "容器状态:" +docker ps -a + +echo "后端日志:" +docker logs --tail 30 ai-interview-backend 2>&1 + +echo "测试:" +curl -s http://127.0.0.1:8000/health || echo "后端未响应" +""" + + # 方法1: 尝试写入已存在的路径 + paths_to_try = [ + f"{DEPLOY_PATH}/deploy.sh", + f"{DEPLOY_PATH}/deploy/deploy.sh", + "/tmp/deploy.sh", + ] + + for path in paths_to_try: + print(f"\n尝试写入: {path}") + + # 先尝试创建空文件 + result = bt_api("files?action=CreateFile", {"path": path}) + print(f" 创建文件: {result}") + + # 然后写入内容 + result = bt_api("files?action=SaveFileBody", { + "path": path, + "data": deploy_script, + "encoding": "utf-8" + }) + + if result.get("status"): + print(f" ✅ 写入成功!") + print() + print("=" * 60) + print("请在服务器执行:") + print(f" bash {path}") + print("=" * 60) + return + else: + print(f" ❌ {result.get('msg', result)}") + + print() + print("=" * 60) + print("❌ 所有方法都失败了") + print() + print("请手动在服务器终端执行:") + print() + print(f"cd {DEPLOY_PATH}/deploy") + print("docker system prune -af") + print("docker-compose up -d --build") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..3e44b89 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,247 @@ +# 产品需求文档 + +> 版本:v1.0 +> 最后更新:2026-01-20 + +## 版本历史 + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|---------|------| +| v1.0 | 2026-01-20 | 初始版本 | AI | + +--- + +## 一、项目概述 + +### 1.1 项目背景 + +为轻医美行业打造 AI 面试官系统,实现咨询师岗位的智能初试。通过 AI 语音面试技术,提升招聘效率,标准化面试流程,降低 HR 初筛工作量。 + +### 1.2 项目目标 + +1. 实现 7x24 小时无人值守的智能初试 +2. 标准化面试流程,确保评估一致性 +3. 自动生成面试报告,辅助 HR 决策 +4. 提升候选人体验,缩短招聘周期 + +### 1.3 核心价值 + +| 角色 | 价值 | +|------|------| +| HR | 减少初筛工作量,获得标准化评估报告 | +| 候选人 | 随时参与面试,体验创新面试形式 | +| 企业 | 降低招聘成本,提升招聘效率 | + +--- + +## 二、用户角色 + +### 2.1 候选人 + +- **画像**:应聘轻医美咨询师岗位的求职者 +- **使用场景**:收到面试邀请后,通过网页参与 AI 语音面试 +- **核心诉求**:快速完成面试,获得反馈 + +### 2.2 HR/招聘专员 + +- **画像**:负责招聘的人力资源专员 +- **使用场景**:在管理后台查看候选人面试结果和分析报告 +- **核心诉求**:快速筛选合适候选人,减少重复劳动 + +### 2.3 管理员 + +- **画像**:系统管理员 +- **使用场景**:管理系统配置、查看所有候选人数据 +- **核心诉求**:系统稳定运行,数据安全 + +--- + +## 三、功能需求 + +### 3.1 用户端功能 + +#### F-001 欢迎页(Welcome) + +| 项目 | 内容 | +|------|------| +| 功能描述 | 展示公司/品牌介绍,引导候选人开始面试 | +| 页面元素 | 公司 Logo、欢迎语、"开始面试"按钮 | +| 交互逻辑 | 点击按钮进入信息收集页 | + +#### F-002 信息收集页(InfoCollection) + +| 项目 | 内容 | +|------|------| +| 功能描述 | 收集候选人姓名和简历 | +| 页面元素 | 姓名输入框、简历上传组件、提交按钮 | +| 交互逻辑 | 1. 输入姓名(必填)
2. 上传简历(PDF/DOC/DOCX)
3. 提交后显示加载状态
4. 处理完成进入来电页 | +| 校验规则 | 姓名:2-20字符;简历:≤10MB | + +#### F-003 模拟来电页(IncomingCall) + +| 项目 | 内容 | +|------|------| +| 功能描述 | 模拟电话来电效果,增强仪式感 | +| 页面元素 | 来电动画、"AI面试官来电中..."文案、接听按钮(绿)、挂断按钮(红) | +| 交互逻辑 | 1. 显示振铃动画
2. 点击接听进入通话页
3. 点击挂断返回欢迎页 | + +#### F-004 语音通话页(InCall) + +| 项目 | 内容 | +|------|------| +| 功能描述 | 与 AI 面试官进行实时语音对话 | +| 页面元素 | 通话计时器、音波动画、静音按钮、挂断按钮 | +| 交互逻辑 | 1. 自动开启麦克风
2. AI 说话时显示音波动画
3. 可随时静音/取消静音
4. 面试结束自动跳转或点击挂断结束 | + +#### F-005 面试结束页(Completed) + +| 项目 | 内容 | +|------|------| +| 功能描述 | 展示面试完成信息 | +| 页面元素 | 感谢语、后续流程说明 | +| 交互逻辑 | 静态展示,可选择关闭页面 | + +--- + +### 3.2 管理后台功能 + +#### F-101 候选人列表 + +| 项目 | 内容 | +|------|------| +| 功能描述 | 展示所有候选人及其面试状态 | +| 页面元素 | 数据表格、搜索框、筛选器、分页 | +| 表格字段 | 姓名、面试时间、状态、综合评分、操作 | +| 筛选条件 | 状态(待面试/进行中/已完成)、时间范围 | + +#### F-102 候选人详情 + +| 项目 | 内容 | +|------|------| +| 功能描述 | 展示候选人完整面试报告 | +| 页面元素 | 基本信息、简历内容、评分雷达图、各维度分析、面试记录、导出按钮 | +| 评分维度 | 销售技能、销售观、素质项、求职动机 | +| 导出功能 | 支持导出 PDF 报告 | + +--- + +## 四、业务流程 + +### 4.1 用户端面试流程 + +``` +┌─────────────┐ +│ 欢迎页 │ +│ Welcome │ +└──────┬──────┘ + │ 点击"开始面试" + ▼ +┌─────────────┐ +│ 信息收集页 │ +│ InfoCollection│ +└──────┬──────┘ + │ 提交姓名+简历 + ▼ +┌─────────────┐ +│ 处理中 │ +│ Processing │ +└──────┬──────┘ + │ 简历上传完成 + ▼ +┌─────────────┐ +│ 模拟来电页 │ +│ IncomingCall│ +└──────┬──────┘ + │ 点击"接听" + ▼ +┌─────────────┐ +│ 语音通话页 │ +│ InCall │──────────────┐ +└──────┬──────┘ │ + │ 面试结束 │ RTC 实时语音 + ▼ │ 对接 Coze Bot +┌─────────────┐ │ +│ 结束页 │◄─────────────┘ +│ Completed │ +└─────────────┘ +``` + +### 4.2 面试评估维度 + +| 维度 | 评估内容 | 权重 | +|------|---------|------| +| 销售技能 | 客户沟通、需求挖掘、异议处理 | 30% | +| 销售观 | 对销售工作的理解和价值观 | 25% | +| 素质项 | 学习能力、抗压能力、团队协作 | 25% | +| 求职动机 | 岗位匹配度、稳定性、职业规划 | 20% | + +--- + +## 五、非功能需求 + +### 5.1 性能要求 + +| 指标 | 要求 | +|------|------| +| 页面加载时间 | < 3s | +| 语音延迟 | < 500ms | +| 并发面试数 | 支持 50 人同时面试 | + +### 5.2 兼容性要求 + +| 平台 | 要求 | +|------|------| +| 浏览器 | Chrome 90+、Edge 90+、Safari 14+ | +| 设备 | PC 为主,移动端适配 | + +### 5.3 安全要求 + +| 项目 | 要求 | +|------|------| +| 数据传输 | HTTPS 加密 | +| 敏感信息 | API Key 不暴露给前端 | +| 权限控制 | 管理后台需登录验证 | + +--- + +## 六、技术约束 + +### 6.1 现有资源 + +| 资源 | 说明 | +|------|------| +| Coze 工作流 | 已有完整面试逻辑(工作流 ID:7595077233002840079) | +| Coze 数据库 | 已有数据结构(数据库 ID:7595077053909712922) | + +### 6.2 技术依赖 + +| 依赖 | 说明 | +|------|------| +| 火山引擎 RTC | 实时语音通话 | +| Coze API | 文件上传、Bot 调用、数据库查询 | + +--- + +## 七、里程碑 + +| 阶段 | 内容 | 预计时间 | +|------|------|---------| +| M1 | 需求确认、技术选型 | 1 天 | +| M2 | 后端 API 开发 | 2 天 | +| M3 | 前端用户端开发 | 3 天 | +| M4 | 前端管理后台开发 | 2 天 | +| M5 | 联调测试 | 2 天 | +| M6 | 上线部署 | 1 天 | + +--- + +## 八、待确认事项 + +- [ ] 管理后台是否需要登录认证? +- [ ] 是否需要候选人邀请链接功能? +- [ ] 面试时长限制?(建议 15-20 分钟) +- [ ] 是否需要面试录音回放? + +--- + +> 最后更新:2026-01-20 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b77029b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,64 @@ +# 文档索引 + +> 本项目文档导航 + +--- + +## 核心文档 + +| 文档 | 说明 | 状态 | +|------|------|------| +| [PRD.md](PRD.md) | 产品需求文档 | ✅ v1.0 | +| [同步清单.md](同步清单.md) | 任务状态与待办 | ✅ | +| [决策记录.md](决策记录.md) | 所有决策记录 | ✅ | +| [项目状态快照.md](项目状态快照.md) | 项目全貌概览 | ✅ | +| [功能模块索引.md](功能模块索引.md) | 功能→决策映射 | 📝 待创建 | + +--- + +## 技术文档 + +| 文档 | 说明 | 状态 | +|------|------|------| +| [技术选型.md](技术选型.md) | 项目技术选型(含覆盖声明) | ✅ | +| [api/endpoints.md](api/endpoints.md) | API 接口文档 | ✅ | +| [database/表结构.md](database/表结构.md) | Coze 数据库结构 | ✅ | + +--- + +## 交接相关 + +| 文档 | 说明 | +|------|------| +| [交接文档/](交接文档/) | 对话框交接文档 | +| [对话框摘要/](对话框摘要/) | 历史对话框摘要 | + +--- + +## 审视记录 + +| 文档 | 说明 | +|------|------| +| [PRD审视记录/](PRD审视记录/) | PRD多角色审视记录 | + +--- + +## 发布相关 + +| 文档 | 说明 | +|------|------| +| [发布记录.md](发布记录.md) | 版本发布历史 | + +--- + +## 快速导航 + +- **了解项目** → [PRD.md](PRD.md) +- **技术方案** → [技术选型.md](技术选型.md) +- **当前任务** → [同步清单.md](同步清单.md) +- **历史决策** → [决策记录.md](决策记录.md) +- **接口文档** → [api/endpoints.md](api/endpoints.md) + +--- + +> 最后更新:2026-01-20 diff --git a/docs/api/endpoints.md b/docs/api/endpoints.md new file mode 100644 index 0000000..4f36e00 --- /dev/null +++ b/docs/api/endpoints.md @@ -0,0 +1,358 @@ +# API 接口文档 + +> 本项目 API 接口说明 + +--- + +## 一、接口概览 + +| 模块 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 候选人 | `/api/candidates` | POST | 提交候选人信息(上传简历) | +| 房间 | `/api/rooms` | POST | 创建语音房间 | +| 管理 | `/api/candidates` | GET | 获取候选人列表 | +| 管理 | `/api/candidates/:id` | GET | 获取候选人详情 | +| 管理 | `/api/candidates/:id/export` | GET | 导出 PDF 报告 | + +--- + +## 二、通用说明 + +### 2.1 基础 URL + +``` +开发环境:http://localhost:8000 +生产环境:https://api.your-domain.com +``` + +### 2.2 响应格式 + +**成功响应**: +```json +{ + "code": 0, + "message": "success", + "data": { ... } +} +``` + +**错误响应**: +```json +{ + "code": 20001, + "message": "参数错误", + "data": null +} +``` + +### 2.3 错误码 + +| 范围 | 类型 | +|------|------| +| `0` | 成功 | +| `10000-19999` | 用户相关错误 | +| `20000-29999` | 业务逻辑错误 | +| `30000-39999` | 数据错误 | +| `40000-49999` | 系统错误 | +| `50000-59999` | 第三方服务错误 | + +常用错误码: + +| 错误码 | 说明 | +|--------|------| +| `20001` | 参数错误 | +| `20002` | 数据不存在 | +| `50001` | Coze API 调用失败 | +| `50002` | RTC 服务异常 | + +--- + +## 三、用户端接口 + +### 3.1 提交候选人信息 + +提交候选人姓名和简历,创建面试会话。 + +**请求** + +- **URL**: `POST /api/candidates` +- **Content-Type**: `multipart/form-data` + +**请求参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | 是 | 候选人姓名,2-20字符 | +| resume | file | 是 | 简历文件,支持 PDF/DOC/DOCX,≤10MB | + +**响应示例** + +```json +{ + "code": 0, + "message": "success", + "data": { + "sessionId": "SESS_1705737600_张三_abc123", + "fileId": "file_xxx" + } +} +``` + +**处理流程** + +1. 校验参数 +2. 上传简历到 Coze(`POST /v1/files/upload`) +3. 生成 sessionId +4. 返回结果 + +--- + +### 3.2 创建语音房间 + +创建 RTC 房间,让 Coze Bot 加入房间。 + +**请求** + +- **URL**: `POST /api/rooms` +- **Content-Type**: `application/json` + +**请求参数** + +```json +{ + "sessionId": "SESS_1705737600_张三_abc123", + "fileId": "file_xxx" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| sessionId | string | 是 | 会话 ID | +| fileId | string | 是 | 简历文件 ID | + +**响应示例** + +```json +{ + "code": 0, + "message": "success", + "data": { + "roomId": "room_xxx", + "token": "rtc_token_xxx", + "appId": "volcengine_app_id", + "userId": "user_xxx" + } +} +``` + +**处理流程** + +1. 生成 roomId 和 userId +2. 调用 Coze API 让 Bot 加入房间(`POST /v1/audio/rooms`) +3. 生成 RTC Token +4. 返回房间信息 + +--- + +### 3.3 结束面试 + +通知后端面试已结束。 + +**请求** + +- **URL**: `POST /api/interviews/:sessionId/end` +- **Content-Type**: `application/json` + +**请求参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| sessionId | string | 是 | 会话 ID(URL 参数) | + +**响应示例** + +```json +{ + "code": 0, + "message": "success", + "data": { + "success": true + } +} +``` + +--- + +## 四、管理后台接口 + +### 4.1 获取候选人列表 + +分页获取候选人列表。 + +**请求** + +- **URL**: `GET /api/candidates` +- **权限**: 需要登录 + +**请求参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| page | number | 否 | 页码,默认 1 | +| pageSize | number | 否 | 每页条数,默认 20 | +| keyword | string | 否 | 搜索关键词(姓名) | +| status | string | 否 | 状态筛选:pending/ongoing/completed | +| startDate | string | 否 | 开始日期,格式 YYYY-MM-DD | +| endDate | string | 否 | 结束日期,格式 YYYY-MM-DD | + +**响应示例** + +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [ + { + "sessionId": "SESS_1705737600_张三_abc123", + "name": "张三", + "status": "completed", + "score": 85, + "createdAt": "2026-01-20T10:00:00+08:00" + } + ], + "total": 100, + "page": 1, + "pageSize": 20 + } +} +``` + +--- + +### 4.2 获取候选人详情 + +获取候选人完整面试报告。 + +**请求** + +- **URL**: `GET /api/candidates/:sessionId` +- **权限**: 需要登录 + +**请求参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| sessionId | string | 是 | 会话 ID(URL 参数) | + +**响应示例** + +```json +{ + "code": 0, + "message": "success", + "data": { + "sessionId": "SESS_1705737600_张三_abc123", + "name": "张三", + "resume": "简历内容文本...", + "status": "completed", + "currentStage": 60, + "scores": { + "salesSkill": 85, + "salesMindset": 80, + "quality": 90, + "motivation": 75 + }, + "analysis": "综合分析报告内容...", + "interviewLog": "完整对话记录...", + "createdAt": "2026-01-20T10:00:00+08:00", + "completedAt": "2026-01-20T10:25:00+08:00" + } +} +``` + +--- + +### 4.3 导出 PDF 报告 + +导出候选人面试报告为 PDF 文件。 + +**请求** + +- **URL**: `GET /api/candidates/:sessionId/export` +- **权限**: 需要登录 + +**请求参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| sessionId | string | 是 | 会话 ID(URL 参数) | + +**响应** + +- **Content-Type**: `application/pdf` +- **Content-Disposition**: `attachment; filename="interview_report_张三.pdf"` + +--- + +## 五、Coze API 调用说明 + +> 以下为后端调用 Coze API 的内部说明 + +### 5.1 文件上传 + +``` +POST https://api.coze.cn/v1/files/upload +Authorization: Bearer {PAT_TOKEN} +Content-Type: multipart/form-data + +Response: +{ + "data": { + "id": "file_xxx" + } +} +``` + +### 5.2 创建语音房间 + +``` +POST https://api.coze.cn/v1/audio/rooms +Authorization: Bearer {PAT_TOKEN} +Content-Type: application/json + +Body: +{ + "bot_id": "7595077233002840079", + "room_id": "room_xxx", + "user_id": "user_xxx", + "voice_id": "voice_id_xxx", + "config": { + "input_file_id": "file_xxx" + } +} +``` + +### 5.3 查询数据库 + +``` +POST https://api.coze.cn/v1/database/query +Authorization: Bearer {PAT_TOKEN} +Content-Type: application/json + +Body: +{ + "database_id": "7595077053909712922", + "filter": { + "session_id": "SESS_xxx" + } +} +``` + +--- + +## 变更日志 + +| 日期 | 变更内容 | 操作人 | +|------|---------|--------| +| 2026-01-20 | 初始化 API 文档 | AI | diff --git a/docs/database/表结构.md b/docs/database/表结构.md new file mode 100644 index 0000000..5b778c8 --- /dev/null +++ b/docs/database/表结构.md @@ -0,0 +1,150 @@ +# 数据库结构 + +> 本项目使用 Coze 数据库存储数据 + +--- + +## 一、数据库信息 + +| 项目 | 内容 | +|------|------| +| 平台 | Coze 数据库 | +| 数据库 ID | 7595077053909712922 | +| 说明 | 由 Coze 工作流自动写入和管理 | + +--- + +## 二、表结构 + +### 2.1 候选人面试记录表 + +> 表名由 Coze 自动管理 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| session_id | String | 会话唯一标识 | `SESS_1705737600_张三_abc123` | +| 姓名 | String | 候选人姓名 | `张三` | +| 简历内容 | String | 解析后的简历文本 | `姓名:张三,学历:本科...` | +| current_stage | Integer | 当前面试阶段 | `60` | +| 面试记录 | String | 完整对话记录(JSON) | `[{"role":"ai","content":"..."}]` | +| 评分结果 | String | 各维度评分(JSON) | `{"salesSkill":85,...}` | +| 分析报告 | String | 综合分析报告 | `候选人具备良好的销售潜质...` | +| created_at | Timestamp | 创建时间 | `2026-01-20T10:00:00Z` | +| updated_at | Timestamp | 更新时间 | `2026-01-20T10:25:00Z` | + +--- + +## 三、字段详细说明 + +### 3.1 session_id 格式 + +``` +SESS_{时间戳}_{姓名}_{随机码} + +示例:SESS_1705737600_张三_abc123 +``` + +- 时间戳:Unix 时间戳(秒) +- 姓名:候选人姓名 +- 随机码:6位随机字符串 + +### 3.2 current_stage 阶段码 + +| 阶段码 | 说明 | +|--------|------| +| 10 | 信息收集 | +| 20 | 销售技能面试 | +| 30 | 销售观面试 | +| 40 | 素质项面试 | +| 50 | 求职动机面试 | +| 60 | 面试完成 | + +### 3.3 评分结果结构 + +```json +{ + "salesSkill": 85, // 销售技能 0-100 + "salesMindset": 80, // 销售观 0-100 + "quality": 90, // 素质项 0-100 + "motivation": 75, // 求职动机 0-100 + "total": 82.5 // 加权总分 +} +``` + +权重计算: +- 销售技能:30% +- 销售观:25% +- 素质项:25% +- 求职动机:20% + +### 3.4 面试记录结构 + +```json +[ + { + "role": "ai", + "content": "你好,我是AI面试官,请先做个自我介绍。", + "timestamp": "2026-01-20T10:01:00Z" + }, + { + "role": "user", + "content": "你好,我叫张三...", + "timestamp": "2026-01-20T10:01:30Z" + } +] +``` + +--- + +## 四、数据访问方式 + +### 4.1 查询单条记录 + +```python +# 后端调用 Coze API +response = await coze_client.post( + "/v1/database/query", + json={ + "database_id": "7595077053909712922", + "filter": { + "session_id": "SESS_xxx" + } + } +) +``` + +### 4.2 查询列表(分页) + +```python +response = await coze_client.post( + "/v1/database/query", + json={ + "database_id": "7595077053909712922", + "page": 1, + "page_size": 20, + "sort": { + "field": "created_at", + "order": "desc" + } + } +) +``` + +--- + +## 五、数据写入 + +> 数据写入由 Coze 工作流自动完成,后端只负责读取 + +工作流写入节点: +- 节点 ID:`124924` +- 类型:`insert_record` +- 触发时机:候选人提交信息后 + +--- + +## 变更日志 + +| 日期 | 变更内容 | 操作人 | +|------|---------|--------| +| 2026-01-20 | 初始化数据库文档 | AI | diff --git a/docs/mock-resume.md b/docs/mock-resume.md new file mode 100644 index 0000000..e6b2263 --- /dev/null +++ b/docs/mock-resume.md @@ -0,0 +1,75 @@ +# 个人简历 + +## 基本信息 + +- **姓名**:李小美 +- **性别**:女 +- **年龄**:26岁 +- **学历**:本科 +- **电话**:138-8888-9999 +- **邮箱**:lixiaomei@email.com +- **现居地**:上海市静安区 +- **求职意向**:轻医美咨询师 / 美容顾问 + +--- + +## 教育背景 + +**上海工商大学** | 市场营销专业 | 本科 | 2018年9月 - 2022年6月 + +- 主修课程:市场营销学、消费者心理学、品牌管理、商务谈判 +- 在校期间多次获得奖学金,担任班级学习委员 + +--- + +## 工作经历 + +### 某知名美容连锁机构 | 美容顾问 | 2022年7月 - 2024年12月 + +**工作内容**: +- 负责门店客户接待和咨询服务,月均接待客户80+人次 +- 根据客户肤质和需求,推荐适合的护肤项目和产品方案 +- 跟进客户体验反馈,维护老客户关系,客户回头率达65% +- 协助店长完成月度销售目标,个人业绩连续6个月排名前三 + +**主要成绩**: +- 2023年度"最佳新人奖" +- 单月最高业绩28万元 +- 发展VIP客户50+人 + +### 某护肤品品牌专柜 | 销售顾问 | 2021年6月 - 2022年6月(实习) + +**工作内容**: +- 在商场专柜负责产品销售和客户服务 +- 学习专业护肤知识,为客户提供皮肤检测和护理建议 +- 参与品牌促销活动策划和执行 + +--- + +## 专业技能 + +- **护肤知识**:熟悉各类肤质特点,了解常见皮肤问题的形成原因和改善方案 +- **医美知识**:了解光电类、注射类等常见轻医美项目的原理和适应人群 +- **沟通能力**:善于倾听客户需求,能够用通俗易懂的语言解释专业知识 +- **销售技巧**:熟悉FABE销售法则,擅长挖掘客户需求促成成交 +- **办公软件**:熟练使用Office办公软件、CRM系统 + +--- + +## 证书资质 + +- 高级美容师资格证(2023年) +- 皮肤管理师认证(2022年) +- 普通话二级甲等 + +--- + +## 自我评价 + +热爱美业,对轻医美行业充满热情。性格开朗、亲和力强,善于与不同类型的客户建立信任关系。具备2年多美容行业一线销售经验,熟悉客户接待全流程。学习能力强,愿意不断提升专业知识。期望加入贵公司,在轻医美领域深耕发展,为客户提供专业的咨询服务。 + +--- + +## 期望薪资 + +8000-12000元/月(底薪+提成) diff --git a/docs/nginx-files-server.conf b/docs/nginx-files-server.conf new file mode 100644 index 0000000..b88a5d4 --- /dev/null +++ b/docs/nginx-files-server.conf @@ -0,0 +1,94 @@ +server +{ + listen 80; + server_name files.test.ai.ireborn.com.cn; + index index.php index.html index.htm default.php default.htm default.html; + root /www/wwwroot/files.test.ai.ireborn.com.cn; + + # 文件上传大小限制(20MB) + client_max_body_size 20m; + + #CERT-APPLY-CHECK--START + # 用于SSL证书申请时的文件验证相关配置 -- 请勿删除 + include /www/server/panel/vhost/nginx/well-known/files.test.ai.ireborn.com.cn.conf; + #CERT-APPLY-CHECK--END + include /www/server/panel/vhost/nginx/extension/files.test.ai.ireborn.com.cn/*.conf; + + #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则 + #error_page 404/404.html; + #SSL-END + + #ERROR-PAGE-START 错误页配置,可以注释、删除或修改 + error_page 404 /404.html; + #error_page 502 /502.html; + #ERROR-PAGE-END + + #PHP-INFO-START PHP引用配置,可以注释或修改 + include enable-php-82.conf; + #PHP-INFO-END + + #REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效 + include /www/server/panel/vhost/rewrite/files.test.ai.ireborn.com.cn.conf; + #REWRITE-END + + # ============ 简历文件服务配置 ============ + location /resumes/ { + alias /www/wwwroot/files.test.ai.ireborn.com.cn/resumes/; + + # 跨域配置(Coze 需要) + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS'; + add_header Access-Control-Allow-Headers 'Content-Type, Authorization'; + + # 处理 OPTIONS 预检请求 + if ($request_method = 'OPTIONS') { + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS'; + add_header Access-Control-Allow-Headers 'Content-Type, Authorization'; + add_header Content-Length 0; + return 204; + } + + # 支持 PDF 文件 + types { + application/pdf pdf; + } + + # 禁止目录浏览 + autoindex off; + } + # ============ 简历文件服务配置结束 ============ + + #禁止访问的文件或目录 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + { + return 404; + } + + #一键申请SSL证书验证目录相关设置 + location ~ \.well-known{ + allow all; + } + + #禁止在证书验证目录放入敏感文件 + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { + return 403; + } + + location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + + location ~ .*\.(js|css)?$ + { + expires 12h; + error_log /dev/null; + access_log /dev/null; + } + + access_log /www/wwwlogs/files.test.ai.ireborn.com.cn.log; + error_log /www/wwwlogs/files.test.ai.ireborn.com.cn.error.log; +} diff --git a/docs/upload.php b/docs/upload.php new file mode 100644 index 0000000..e4565f5 --- /dev/null +++ b/docs/upload.php @@ -0,0 +1,93 @@ + 1, 'error' => 'Method not allowed']); + exit; +} + +// 验证令牌 +$token = $_POST['token'] ?? $_SERVER['HTTP_X_UPLOAD_TOKEN'] ?? ''; +if ($token !== $SECRET_TOKEN) { + http_response_code(403); + echo json_encode(['code' => 1, 'error' => 'Invalid token']); + exit; +} + +// 检查文件是否上传 +if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { + $error_msg = isset($_FILES['file']) ? 'Upload error: ' . $_FILES['file']['error'] : 'No file uploaded'; + echo json_encode(['code' => 1, 'error' => $error_msg]); + exit; +} + +$file = $_FILES['file']; + +// 检查文件大小 +if ($file['size'] > $MAX_SIZE) { + echo json_encode(['code' => 1, 'error' => 'File too large (max 20MB)']); + exit; +} + +// 检查文件类型(通过扩展名) +$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); +if ($ext !== 'pdf') { + echo json_encode(['code' => 1, 'error' => 'Invalid file type. Only PDF allowed.']); + exit; +} + +// 生成文件名 +$file_id = 'resume_' . bin2hex(random_bytes(6)); +$filename = $file_id . '.pdf'; +$filepath = $UPLOAD_DIR . $filename; + +// 确保目录存在 +if (!is_dir($UPLOAD_DIR)) { + mkdir($UPLOAD_DIR, 0755, true); +} + +// 移动文件 +if (move_uploaded_file($file['tmp_name'], $filepath)) { + $url = $BASE_URL . $filename; + echo json_encode([ + 'code' => 0, + 'url' => $url, + 'file_id' => $file_id, + 'filename' => $filename + ]); +} else { + echo json_encode(['code' => 1, 'error' => 'Failed to save file']); +} diff --git a/docs/决策记录.md b/docs/决策记录.md new file mode 100644 index 0000000..994969c --- /dev/null +++ b/docs/决策记录.md @@ -0,0 +1,70 @@ +# 决策记录 + +> ⛔ **版本规则**:只允许追加,禁止修改/删除已有记录 + +--- + +## 决策索引 + +| 编号 | 日期 | 主题 | 决策结果 | +|------|------|------|---------| +| DR-001 | 2026-01-20 | 技术选型与框架规范覆盖 | 使用 Coze API + 火山引擎 RTC | + +--- + +## DR-001 | 2026-01-20 + +### 背景 + +项目需要实现 AI 语音面试系统。已有完整的 Coze 工作流(工作流 ID:7595077233002840079),包含面试逻辑、评分算法、报告生成等功能。需要确定技术选型,并明确与 AgentWD 框架规范的差异。 + +### 方案选项 + +| 选项 | 方案描述 | 优点 | 缺点 | +|------|---------|------|------| +| A | 全部遵循框架规范,使用 OpenRouter + MySQL | 完全符合框架规范 | 需重新开发面试逻辑,工作量大 | +| B | 复用 Coze 工作流,覆盖部分规范 | 开发效率高,复用现有资产 | 与框架规范有差异 | + +### 多角色分析 + +**产品经理**: +- 分析:项目核心价值在于 AI 面试能力,Coze 工作流已经过验证 +- 建议:复用现有工作流,快速上线 +- 倾向:方案 B + +**架构师**: +- 分析:Coze + 火山引擎 RTC 同属火山生态,集成成本低 +- 建议:前后端遵循框架规范,仅 AI 层使用 Coze +- 倾向:方案 B + +**前端开发**: +- 分析:Vue3 + Element Plus 符合框架规范,无额外学习成本 +- 建议:前端完全遵循框架规范 +- 倾向:方案 B + +### 最终决策 + +**决策结果**:选择方案 B + +**决策原因**: +1. Coze 工作流已包含完整面试逻辑,复用可大幅减少开发时间 +2. 前端、后端框架仍遵循规范,仅 AI 服务层使用 Coze +3. 火山引擎 RTC 与 Coze 同生态,官方支持对接 +4. 在 `docs/技术选型.md` 中明确声明覆盖项 + +**否决方案**: +- 方案 A 被否决,原因:需要重新开发 AI 面试逻辑,工作量大,且已有成熟方案 + +### 影响范围 + +- [x] 技术选型文档需创建 +- [ ] PRD 无需更新 +- [ ] 原型无需更新 + +### 来源对话框 + +第 1 对话框 + +--- + + diff --git a/docs/同步清单.md b/docs/同步清单.md new file mode 100644 index 0000000..7f1bfe4 --- /dev/null +++ b/docs/同步清单.md @@ -0,0 +1,140 @@ +# 同步清单 + +> 多对话框协作的核心同步文件 +> ✅ 可覆盖,需记录变更日志 + +--- + +## 项目状态 + +| 项目 | 内容 | +|------|------| +| **当前阶段** | 开发阶段 | +| **进度** | 65% | +| **最后更新** | 2026-01-20 16:30 | +| **当前对话框** | 第1对话框 | + +--- + +## 待办事项 + +### 🔴 紧急(今日必须完成) + +- [ ] 确认 PRD 待确认事项(管理后台登录、面试时长等) + +### 🟡 重要(本周完成) + +- [ ] 初始化前端项目(Vue3 + Vite) +- [ ] 初始化后端项目(FastAPI) +- [ ] 实现后端 Coze API 封装 +- [ ] 实现后端 RTC Token 生成 + +### 🟢 普通(待安排) + +- [ ] 前端用户端页面开发(5个页面) +- [ ] 前端管理后台页面开发(2个页面) +- [ ] 前端 RTC SDK 集成 +- [ ] 联调测试 +- [ ] 部署上线 + +--- + +## 进行中的任务 + +| 任务 | 负责角色 | 开始时间 | 状态 | +|------|---------|---------|------| +| 需求分析 | 产品经理 | 2026-01-20 | ✅ 完成 | +| 技术选型 | 架构师 | 2026-01-20 | ✅ 完成 | +| 文档整理 | AI | 2026-01-20 | ✅ 完成 | + +--- + +## 已完成任务 + +| 任务 | 完成时间 | 备注 | +|------|---------|------| +| 创建项目目录结构 | 2026-01-20 | 按框架规范创建 | +| 初始化核心文档 | 2026-01-20 | CONTEXT.md 等 | +| 撰写 PRD | 2026-01-20 | v1.0 | +| 技术选型决策 | 2026-01-20 | DR-001,使用 Coze API | +| API 文档设计 | 2026-01-20 | 5 个接口 | +| 数据库结构文档 | 2026-01-20 | Coze 数据库 | +| 前端项目骨架 | 2026-01-20 | Vue3 + Vite + Element Plus | +| 后端项目骨架 | 2026-01-20 | FastAPI + Coze API 封装 | +| Coze 工作流分析 | 2026-01-20 | 详见 coze-workflows/工作流分析.md | + +--- + +## 阻塞问题 + +| 问题 | 阻塞原因 | 待处理人 | 状态 | +|------|---------|---------|------| +| 管理后台数据查询 | Coze 数据库无 REST API | 产品 | 待讨论方案 | + +--- + +## 决策统计 + +| 项目 | 数量 | +|------|------| +| 总决策数 | 1 | +| 本周新增 | 1 | +| 待讨论 | 0 | + +--- + +## 开发任务清单 + +### 阶段 1:项目初始化 + +- [ ] 创建前端项目(Vue3 + TypeScript + Vite) +- [ ] 安装前端依赖(Element Plus、Tailwind、Axios、RTC SDK) +- [ ] 创建后端项目(FastAPI) +- [ ] 安装后端依赖(httpx、python-multipart) +- [ ] 配置环境变量 + +### 阶段 2:后端核心功能 + +- [ ] 实现 Coze API 封装(文件上传、数据库查询) +- [ ] 实现 RTC 服务封装(Token 生成) +- [ ] 实现 `POST /api/candidates` 接口 +- [ ] 实现 `POST /api/rooms` 接口 +- [ ] 实现 `GET /api/candidates` 接口 +- [ ] 实现 `GET /api/candidates/:id` 接口 + +### 阶段 3:前端用户端 + +- [ ] 实现欢迎页(Welcome) +- [ ] 实现信息收集页(InfoCollection) +- [ ] 实现模拟来电页(IncomingCall) +- [ ] 集成火山引擎 RTC SDK +- [ ] 实现语音通话页(InCall) +- [ ] 实现面试结束页(Completed) + +### 阶段 4:前端管理后台 + +- [ ] 实现候选人列表页 +- [ ] 实现候选人详情页 +- [ ] 实现评分雷达图组件 +- [ ] 实现 PDF 导出功能 + +### 阶段 5:联调测试 + +- [ ] 端到端流程测试 +- [ ] 语音质量测试 +- [ ] 异常场景测试 + +### 阶段 6:部署上线 + +- [ ] 编写 Dockerfile +- [ ] 配置 Nginx +- [ ] 部署到服务器 + +--- + +## 变更日志 + +| 日期 | 变更内容 | 操作人 | +|------|---------|--------| +| 2026-01-20 | 更新任务清单,添加开发阶段 | AI | +| 2026-01-20 | 初始化同步清单 | AI | diff --git a/docs/技术选型.md b/docs/技术选型.md new file mode 100644 index 0000000..9a63c39 --- /dev/null +++ b/docs/技术选型.md @@ -0,0 +1,121 @@ +# 技术选型 + +> 本项目技术选型说明,包含与框架规范的覆盖声明 + +--- + +## 一、遵循框架规范 + +| 层级 | 技术 | 说明 | +|------|------|------| +| **前端框架** | Vue 3 + TypeScript | 符合框架规范 | +| **构建工具** | Vite | 符合框架规范 | +| **UI 组件库** | Element Plus | 符合框架规范 | +| **CSS 方案** | Tailwind CSS | 符合框架规范 | +| **HTTP 客户端** | Axios | 符合框架规范 | +| **状态管理** | Pinia | 按需使用 | +| **后端框架** | Python + FastAPI | 符合框架规范 | +| **容器化** | Docker + Docker Compose | 符合框架规范 | + +--- + +## 二、技术选型覆盖 + +> 以下选项与框架默认规范不同,需特别说明 + +| 项目 | 框架默认 | 本项目选择 | 覆盖原因 | +|------|----------|------------|----------| +| **AI 网关** | OpenRouter.ai | Coze API | 复用已有 Coze 工作流,包含完整面试逻辑 | +| **数据存储** | MySQL | Coze 数据库 | 复用现有数据结构,减少开发工作量 | +| **实时音视频** | - | 火山引擎 RTC | 与 Coze 同属火山引擎生态,官方支持对接 | + +--- + +## 三、第三方服务依赖 + +### 3.1 Coze 平台 + +| 资源 | ID | 用途 | +|------|-----|------| +| Bot(工作流) | 7595113005181386792 | AI 面试官对话逻辑 | +| 数据库 | 7595077053909712922 | 候选人数据存储 | + +**API 端点**: +- 基础 URL:`https://api.coze.cn` +- 文件上传:`POST /v1/files/upload` +- 语音房间:`POST /v1/audio/rooms` +- 数据库查询:`POST /v1/database/query` + +### 3.2 火山引擎 RTC + +| 配置项 | 说明 | +|--------|------| +| SDK | `@volcengine/rtc` | +| 音频格式 | PCM 16000Hz | +| 用途 | 前端与 Coze Bot 实时语音通话 | + +--- + +## 四、环境变量 + +```bash +# Coze 配置 +COZE_API_BASE=https://api.coze.cn +COZE_PAT_TOKEN=pat_xxx # 从 Coze 平台获取 +COZE_BOT_ID=7595077233002840079 +COZE_DATABASE_ID=7595077053909712922 + +# 火山引擎 RTC 配置 +VOLCENGINE_APP_ID=xxx # 从火山引擎控制台获取 +VOLCENGINE_APP_KEY=xxx + +# 服务配置 +API_PORT=8000 +FRONTEND_URL=http://localhost:5173 +``` + +--- + +## 五、前端依赖清单 + +```json +{ + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.2.0", + "pinia": "^2.1.0", + "axios": "^1.6.0", + "element-plus": "^2.5.0", + "@element-plus/icons-vue": "^2.3.0", + "@volcengine/rtc": "^4.x" + }, + "devDependencies": { + "typescript": "^5.3.0", + "vite": "^5.0.0", + "tailwindcss": "^3.4.0", + "@vitejs/plugin-vue": "^5.0.0" + } +} +``` + +--- + +## 六、后端依赖清单 + +```txt +# requirements.txt +fastapi>=0.109.0 +uvicorn>=0.27.0 +httpx>=0.26.0 +python-multipart>=0.0.6 +pydantic>=2.5.0 +python-dotenv>=1.0.0 +``` + +--- + +## 变更日志 + +| 日期 | 变更内容 | 操作人 | +|------|---------|--------| +| 2026-01-20 | 初始化技术选型 | AI | diff --git a/docs/部署文档.md b/docs/部署文档.md new file mode 100644 index 0000000..0b42a90 --- /dev/null +++ b/docs/部署文档.md @@ -0,0 +1,206 @@ +# AI 语音面试系统 - 部署文档 + +> 更新日期: 2026-01-21 +> 状态: ✅ 已部署 + +## 一、服务器信息 + +| 项目 | 值 | +|------|-----| +| 服务器 IP | 47.107.172.23 | +| SSH 用户名 | root | +| SSH 密码 | Nj861021 | +| 宝塔面板 | http://47.107.172.23:8888/ | +| 宝塔 API Token | PKdfnaInQL0P5ghB8SvwbrGcIpXWaEvq | +| 域名解析 | *.ai.ireborn.com.cn → 47.107.172.23 | +| 测试域名 | *.test.ai.ireborn.com.cn | + +## 二、部署地址 (已上线) + +| 服务 | 地址 | 状态 | +|------|------|------| +| 用户端 | http://interview.test.ai.ireborn.com.cn | ✅ | +| 管理后台 | http://interview.test.ai.ireborn.com.cn/admin | ✅ | +| 后端 API | http://interview.test.ai.ireborn.com.cn/api/health | ✅ | +| 文件服务 | https://files.test.ai.ireborn.com.cn | ✅ | + +> ⚠️ 当前为简化版后端,完整功能代码需要手动上传后重新构建 + +## 三、Docker 部署 + +### 3.1 目录结构 + +``` +/www/wwwroot/ai-interview/ +├── deploy/ +│ ├── docker-compose.yml +│ ├── Dockerfile.frontend +│ ├── Dockerfile.backend +│ ├── nginx/ +│ │ └── frontend.conf +│ └── .env +├── frontend/ +├── backend/ +└── uploads/ +``` + +### 3.2 环境变量配置 + +创建 `/www/wwwroot/ai-interview/deploy/.env`: + +```bash +# Coze 配置 +COZE_PAT_TOKEN=pat_nd1wU47WyPS9GCIyJ1clnH8h1WOQXGrYELX8w73TnSZaYbFdYD4swIhzcETBUbfT +COZE_BOT_ID=7595113005181386792 + +# 工作流 ID +COZE_WORKFLOW_A_ID=7597357422713798710 +COZE_WORKFLOW_C_ID=7597376294612107318 + +# 文件服务器 +FILE_SERVER_URL=https://files.test.ai.ireborn.com.cn +FILE_SERVER_TOKEN=ai_interview_2026_secret +``` + +### 3.3 部署命令 + +```bash +cd /www/wwwroot/ai-interview/deploy + +# 构建并启动 +docker-compose up -d --build + +# 查看日志 +docker-compose logs -f + +# 停止服务 +docker-compose down + +# 重启服务 +docker-compose restart +``` + +### 3.4 端口映射 + +| 服务 | 容器端口 | 宿主机端口 | +|------|----------|------------| +| Frontend | 80 | 3000 | +| Backend | 8000 | 8000 | + +## 四、Nginx 反向代理配置 + +### 4.1 前端 + API 合并 (推荐) + +域名: `interview.test.ai.ireborn.com.cn` + +```nginx +server { + listen 80; + listen 443 ssl http2; + server_name interview.test.ai.ireborn.com.cn; + + # SSL 证书 (宝塔自动配置) + ssl_certificate /www/server/panel/vhost/cert/interview.test.ai.ireborn.com.cn/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/interview.test.ai.ireborn.com.cn/privkey.pem; + + # 前端 + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + } + + # API 代理 (前端容器内部已处理,此处可选) + location /api/ { + proxy_pass http://127.0.0.1:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 120s; + } +} +``` + +### 4.2 独立 API 域名 (可选) + +域名: `interview-api.test.ai.ireborn.com.cn` + +```nginx +server { + listen 80; + listen 443 ssl http2; + server_name interview-api.test.ai.ireborn.com.cn; + + ssl_certificate /www/server/panel/vhost/cert/interview-api.test.ai.ireborn.com.cn/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/interview-api.test.ai.ireborn.com.cn/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + + # CORS + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; + add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; + } +} +``` + +## 五、更新部署 + +```bash +cd /www/wwwroot/ai-interview + +# 拉取最新代码 +git pull origin main + +# 重新构建并部署 +cd deploy +docker-compose down +docker-compose up -d --build + +# 查看状态 +docker-compose ps +``` + +## 六、故障排查 + +### 6.1 查看容器日志 + +```bash +# 前端日志 +docker logs ai-interview-frontend + +# 后端日志 +docker logs ai-interview-backend +``` + +### 6.2 进入容器调试 + +```bash +# 进入后端容器 +docker exec -it ai-interview-backend /bin/sh + +# 进入前端容器 +docker exec -it ai-interview-frontend /bin/sh +``` + +### 6.3 常见问题 + +| 问题 | 解决方案 | +|------|----------| +| 前端白屏 | 检查 nginx 配置,确保 `try_files` 指向 index.html | +| API 404 | 检查 proxy_pass 地址和端口 | +| CORS 错误 | 在 nginx 添加 CORS 头 | +| 环境变量无效 | 确认 .env 文件存在且格式正确 | diff --git a/docs/项目状态快照.md b/docs/项目状态快照.md new file mode 100644 index 0000000..cc1822b --- /dev/null +++ b/docs/项目状态快照.md @@ -0,0 +1,110 @@ +# 项目状态快照 + +> 新对话框快速恢复上下文的关键文档 +> ✅ 可覆盖,需记录变更日志 + +--- + +## 一、项目全貌 + +### 1.1 基本信息 + +| 项目 | 内容 | +|------|------| +| 项目名称 | AI Interview (AI面试助手) | +| 项目编号 | 011-ai-interview-2601 | +| 启动日期 | 2026-01-20 | +| 当前阶段 | 设计阶段 | +| 整体进度 | 20% | + +### 1.2 技术栈 + +| 层级 | 技术 | +|------|------| +| 前端 | Vue3 + TypeScript + Element Plus | +| 后端 | Python + FastAPI | +| AI 服务 | Coze API(覆盖框架规范) | +| 实时音视频 | 火山引擎 RTC | +| 数据存储 | Coze 数据库(覆盖框架规范) | +| 部署 | Docker + Nginx | + +--- + +## 二、阶段进度 + +| 阶段 | 状态 | 完成度 | 备注 | +|------|------|--------|------| +| 需求分析 | ✅ 完成 | 100% | PRD v1.0 已完成 | +| 技术选型 | ✅ 完成 | 100% | DR-001 已决策 | +| 设计阶段 | 🔄 进行中 | 50% | API/数据库文档已完成 | +| 开发阶段 | ⏳ 待开始 | 0% | | +| 测试阶段 | ⏳ 待开始 | 0% | | +| 上线部署 | ⏳ 待开始 | 0% | | + +--- + +## 三、核心决策摘要 + +> 详细内容见 `决策记录.md` + +| 编号 | 主题 | 结果 | +|------|------|------| +| DR-001 | 技术选型与框架规范覆盖 | 使用 Coze API + 火山引擎 RTC,在技术选型中声明覆盖 | + +--- + +## 四、关键文档 + +| 文档 | 路径 | 状态 | +|------|------|------| +| PRD | `docs/PRD.md` | ✅ v1.0 | +| 技术选型 | `docs/技术选型.md` | ✅ | +| 决策记录 | `docs/决策记录.md` | ✅ | +| API 文档 | `docs/api/endpoints.md` | ✅ | +| 数据库结构 | `docs/database/表结构.md` | ✅ | +| 功能模块索引 | `docs/功能模块索引.md` | 📝 待创建 | + +--- + +## 五、当前待办 + +> 详细内容见 `同步清单.md` + +1. 确认 PRD 待确认事项 +2. 初始化前端项目(Vue3 + Vite) +3. 初始化后端项目(FastAPI) +4. 实现后端 Coze API 封装 + +--- + +## 六、已知问题 + +| 问题 | 状态 | 处理方案 | +|------|------|---------| +| - | - | - | + +--- + +## 七、上一对话框教训 + +> 从交接中沉淀的经验 + +- 新项目,暂无 + +--- + +## 八、现有资源 + +| 资源 | 说明 | +|------|------| +| Coze 工作流 | 已有完整面试逻辑(ID:7595077233002840079) | +| Coze 数据库 | 已有数据结构(ID:7595077053909712922) | + +--- + +## 变更日志 + +| 日期 | 变更内容 | 操作人 | +|------|---------|--------| +| 2026-01-20 | 更新阶段进度,添加技术栈详情 | AI | +| 2026-01-20 | 初始化快照 | AI | diff --git a/docs/项目进度总结-20260121-final.md b/docs/项目进度总结-20260121-final.md new file mode 100644 index 0000000..fc01132 --- /dev/null +++ b/docs/项目进度总结-20260121-final.md @@ -0,0 +1,174 @@ +# AI 语音面试系统 - 项目进度总结 + +> 更新日期: 2026-01-21 + +## 一、项目概述 + +**项目名称**: AI 语音面试系统 (轻医美行业) +**项目周期**: 2026-01 至今 +**当前状态**: 🟡 开发中 (核心功能已完成,语音模式待优化) + +## 二、已完成功能 + +### 2.1 用户端 (前端) + +| 功能 | 状态 | 说明 | +|------|------|------| +| 欢迎页面 | ✅ | 品牌展示、开始面试入口 | +| 信息采集 | ✅ | 姓名输入、简历 PDF 上传 | +| 面试初始化 | ✅ | 调用 Workflow A 生成 session_id | +| 文字面试模式 | ✅ | 基于 /v3/chat API 的文字对话 | +| 语音面试模式 | 🟡 | RTC 连接正常,session_id 传递待优化 | +| 调试面板 | ✅ | 显示 session_id、API 响应、错误信息 | + +### 2.2 管理后台 + +| 功能 | 状态 | 说明 | +|------|------|------| +| 登录页面 | ✅ | 账号密码认证 | +| 数据概览 | ✅ | 统计卡片、匹配度分布、状态饼图、排行榜 | +| 面试列表 | ✅ | 搜索、筛选、分页、阶段标签 | +| 面试详情 | ✅ | 匹配度圆环、维度评分、风险分析、对话记录 | +| 骨架屏加载 | ✅ | 优化加载体验,替代 v-loading 遮罩 | + +### 2.3 后端服务 + +| 功能 | 状态 | 说明 | +|------|------|------| +| 文件上传 | ✅ | 支持 PDF,上传到自建文件服务器 | +| 面试初始化 API | ✅ | 调用 Workflow A,返回 session_id | +| 文字聊天 API | ✅ | 调用 /v3/chat,支持多轮对话 | +| 语音房间 API | ✅ | 调用 /v1/audio/rooms 创建 RTC 房间 | +| 管理后台 API | ✅ | 通过 Workflow C 查询 Coze 数据库 | + +### 2.4 Coze 工作流 + +| 工作流 | ID | 功能 | +|--------|-----|------| +| Workflow A | 7597357422713798710 | 面试初始化:接收 name + file_url,解析简历,生成 session_id | +| Workflow B | 7595077233002840079 | 面试主流程:4 维度提问、评分、生成报告 | +| Workflow C | 7597376294612107318 | 数据查询:接收 table + sql,执行增删改查 | + +### 2.5 基础设施 + +| 组件 | 状态 | 说明 | +|------|------|------| +| 自建文件服务器 | ✅ | Nginx + PHP,域名 files.test.ai.ireborn.com.cn | +| Coze 数据库 | ✅ | ci_interview_assessments, ci_interview_logs, ci_business_config | + +## 三、技术架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户端 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 欢迎页面 │→│ 信息采集 │→│ 面试页面 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ↓ ↓ │ +│ 文件上传 语音/文字模式 │ +└─────────────────────────────────────────────────────────────┘ + ↓ ↓ +┌─────────────────────────────────────────────────────────────┐ +│ FastAPI 后端 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ /init-interview │ /api/chat │ /api/room │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Coze 平台 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Workflow A │ │ Workflow B │ │ Workflow C │ │ +│ │ (初始化) │ │ (面试主流程) │ │ (数据查询) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ↓ │ +│ Coze Database │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 四、待解决问题 + +### 4.1 语音模式 session_id 传递 + +**问题**: 语音模式下,session_id 无法通过 /v1/audio/rooms API 传递到 Workflow B + +**已尝试方案**: +1. ❌ `config.session_id` - 不支持 +2. ❌ `parameters` 字段 - 不支持 +3. ❌ `user_id` 字段 - 可设置但 Workflow 无法读取 +4. ❌ Volcengine RTC SDK `sendRoomMessage` - 不是 Coze 信令通道 +5. 🟡 `@coze/realtime-api` SDK `session.update` 事件 - 待验证 + +**建议方案**: +- 在 Workflow B 开始时通过数据库查询最近的 session_id +- 或使用 `@coze/realtime-api` 的 `session.update` 事件 + +### 4.2 面试阶段字段 + +**已实现**: current_stage 数值映射 +- >= 50: 面试完成 (求职动机阶段) +- >= 40: 素质项评估 +- >= 30: 销售观评估 +- >= 20: 销售技能 +- >= 10: 简历上传 + +## 五、数据库表结构 + +### ci_interview_assessments +| 字段 | 类型 | 说明 | +|------|------|------| +| id | String | 主键 (自动生成) | +| session_id | String | 会话 ID | +| candidate_name | String | 候选人姓名 | +| current_stage | String | 当前阶段 (10-50) | +| sales_skill_score | String | 销售技能分数 | +| sales_skill_report | String | 销售技能报告 | +| sales_concept_score | String | 销售观分数 | +| sales_concept_report | String | 销售观报告 | +| competency_score | String | 综合素质分数 | +| competency_report | String | 综合素质报告 | +| final_score_report | String | 最终评估报告 | +| resume_text | String | 简历文本 | +| risk_warning | String | 风险提示 | + +### ci_interview_logs +| 字段 | 类型 | 说明 | +|------|------|------| +| log_id | String | 主键 (必填) | +| session_id | String | 会话 ID | +| stage | String | 面试阶段 | +| round | String | 轮次 | +| ai_question | String | AI 问题 | +| user_answer | String | 用户回答 | +| log_type | String | 日志类型 | + +## 六、关键文件清单 + +### 前端 +- `frontend/src/pages/interview/index.vue` - 欢迎页 + 信息采集 +- `frontend/src/pages/interview/call.vue` - 面试页面 (语音/文字) +- `frontend/src/pages/admin/*.vue` - 管理后台页面 +- `frontend/src/composables/useCozeRealtime.ts` - Coze Realtime SDK 封装 +- `frontend/src/api/candidate.ts` - API 调用封装 + +### 后端 +- `backend/main.py` - FastAPI 入口 +- `backend/app/services/coze_service.py` - Coze API 服务 +- `backend/app/routers/init.py` - 面试初始化 API +- `backend/app/routers/chat.py` - 文字聊天 API +- `backend/app/routers/room.py` - 语音房间 API +- `backend/app/routers/admin.py` - 管理后台 API + +### 配置 +- `backend/.env` - 环境变量 (PAT_TOKEN, BOT_ID, FILE_SERVER_TOKEN 等) +- `docs/upload.php` - 文件上传 PHP 脚本 +- `docs/nginx-files-server.conf` - Nginx 配置 + +## 七、访问地址 + +| 服务 | 地址 | +|------|------| +| 用户端 | http://localhost:5173 | +| 管理后台 | http://localhost:5173/admin | +| 后端 API | http://localhost:8000 | +| 文件服务器 | https://files.test.ai.ireborn.com.cn | diff --git a/docs/项目进度总结-20260121.md b/docs/项目进度总结-20260121.md new file mode 100644 index 0000000..da0f41a --- /dev/null +++ b/docs/项目进度总结-20260121.md @@ -0,0 +1,194 @@ +# AI 语音面试系统 - 项目进度总结 + +**更新日期**: 2026-01-21 +**项目代号**: 011-ai-interview-2601 + +--- + +## 一、已完成功能 + +### 1. 基础架构 ✅ +- [x] 前端项目初始化 (Vue 3 + TypeScript + Vite + Element Plus) +- [x] 后端项目初始化 (FastAPI + Python) +- [x] Coze API 集成 (PAT Token 认证) +- [x] 文件服务器搭建 (Nginx + PHP 上传脚本) + +### 2. 工作流 A - 面试初始化 ✅ +- [x] 接收姓名和简历 PDF +- [x] 上传简历到自建文件服务器 (`files.test.ai.ireborn.com.cn`) +- [x] 调用 Coze 工作流解析简历 +- [x] 生成 `session_id` 并写入 `ci_interview_assessments` 表 +- [x] 返回 `session_id` 给前端 + +### 3. 工作流 B - 面试对话 ⚠️ (部分完成) +- [x] 调用 Coze `/v3/chat` API 进行对话 +- [x] 支持文字面试模式 +- [x] 支持语音面试模式 (RTC 房间创建) +- [ ] **未完成**: 多轮对话状态维持 +- [ ] **未完成**: 对话日志写入 `ci_interview_logs` 表 + +### 4. 工作流 C1 - 数据查询 ✅ +- [x] 支持查询 `ci_interview_assessments` 表 +- [x] 支持查询 `ci_interview_logs` 表 +- [x] 支持查询 `ci_business_config` 表 +- [x] 通过 JSON 输入指定表名和 SQL + +### 5. 后台管理页面 ✅ +- [x] 登录页面 (`/admin/login`) +- [x] 面试列表页面 (`/admin/interviews`) +- [x] 面试详情页面 (`/admin/interviews/:id`) + - [x] 基本信息展示 + - [x] 维度评分展示 + - [x] 评估报告展示 + - [x] 对话记录展示 (数据依赖工作流 B) +- [x] 配置管理页面 (`/admin/configs`) + +### 6. 文件服务 ✅ +- [x] 自建文件服务器 (`files.test.ai.ireborn.com.cn`) +- [x] PHP 上传接口 (Token 认证) +- [x] Nginx 静态文件服务 +- [x] CORS 支持 + +--- + +## 二、核心问题 + +### 问题:工作流 B 多轮对话状态无法维持 + +**现象**: +- 每次调用 `/v3/chat` API,Coze 返回新的 `conversation_id` +- 即使传入上一次的 `conversation_id`,仍然返回新值 +- 导致工作流 B 每次都从头执行,重复显示欢迎语 + +**影响**: +1. 面试无法正常进行多轮问答 +2. 对话日志无法写入 `ci_interview_logs` 表 +3. 后台管理页面无法显示对话记录 + +**已尝试的方案**: +1. ❌ 传入 `conversation_id` 参数 - Coze 仍返回新值 +2. ❌ 使用 `/v1/workflows/chat` 接口 - 接口不存在或参数格式不对 +3. ⏳ 通过 `session_id` 在工作流内部管理状态 - 需要修改工作流 B + +--- + +## 三、解决方案建议 + +### 方案:工作流 B 内部状态管理 + +修改 Coze 工作流 B,不依赖 Coze 的 conversation 机制,改为: + +1. **开始节点**: 接收 `session_id` 和用户消息 +2. **查询状态**: 从 `ci_interview_logs` 表查询该 `session_id` 的历史对话数量 +3. **条件分支**: + - 如果历史为 0:显示欢迎语,更新状态 + - 如果历史 > 0:直接进入下一轮提问 +4. **写入日志**: 每次对话后,将 AI 问题和用户回答写入 `ci_interview_logs` +5. **结束判断**: 当对话轮次达到设定值时,生成评估报告 + +--- + +## 四、技术栈 + +| 模块 | 技术 | +|------|------| +| 前端 | Vue 3 + TypeScript + Vite + Element Plus + Tailwind CSS | +| 后端 | Python + FastAPI + Pydantic + httpx | +| AI 平台 | Coze (扣子) | +| 文件服务 | Nginx + PHP | +| 数据库 | Coze 内置数据库 | + +--- + +## 五、API 端点 + +### 后端 API + +| 端点 | 方法 | 说明 | +|------|------|------| +| `/api/init-interview` | POST | 初始化面试(上传简历、生成 session_id) | +| `/api/chat` | POST | 文字面试对话 | +| `/api/rooms` | POST | 创建语音面试房间 | +| `/api/admin/login` | POST | 管理员登录 | +| `/api/admin/interviews` | GET | 面试列表 | +| `/api/admin/interviews/{id}` | GET | 面试详情 | +| `/api/admin/interviews/{id}/logs` | GET | 对话记录 | + +### Coze 工作流 + +| 工作流 | ID | 说明 | +|--------|-----|------| +| 工作流 A | 7597357422713798710 | 面试初始化(简历解析) | +| 工作流 B | 7595077233002840079 | 面试对话(Chatflow) | +| 工作流 C1 | 7597376294612107318 | 数据查询 | + +--- + +## 六、数据库表 + +| 表名 | 说明 | +|------|------| +| ci_interview_assessments | 面试评估记录 | +| ci_interview_logs | 面试对话日志 | +| ci_business_config | 业务配置 | + +--- + +## 七、下一步工作 + +1. **修改工作流 B** - 实现内部状态管理,不依赖 conversation_id +2. **测试多轮对话** - 验证对话日志正确写入数据库 +3. **完善语音面试** - 测试 RTC 语音对话功能 +4. **优化后台页面** - 改进布局和用户体验 + +--- + +## 八、文件结构 + +``` +011-ai-interview-2601/ +├── frontend/ # 前端项目 +│ ├── src/ +│ │ ├── api/ # API 请求 +│ │ ├── pages/ +│ │ │ ├── interview/ # 面试页面 +│ │ │ └── admin/ # 后台管理 +│ │ ├── composables/ # 组合式函数 (useRTC) +│ │ └── router/ # 路由配置 +│ └── package.json +├── backend/ # 后端项目 +│ ├── app/ +│ │ ├── routers/ # API 路由 +│ │ ├── services/ # 服务层 (coze_service) +│ │ └── schemas.py # 数据模型 +│ ├── main.py +│ └── requirements.txt +├── docs/ # 文档 +│ ├── upload.php # PHP 上传脚本 +│ ├── nginx-files-server.conf +│ └── 项目进度总结-20260121.md +└── coze-workflows/ # Coze 工作流配置 +``` + +--- + +## 九、配置信息 + +### 环境变量 (.env) + +```env +COZE_PAT_TOKEN=pat_xxx +COZE_BOT_ID=7595113005181386792 +FILE_SERVER_TOKEN=ai_interview_2026_secret +TUNNEL_URL=http://files.test.ai.ireborn.com.cn +``` + +### 文件服务器 + +- 域名: `files.test.ai.ireborn.com.cn` +- 上传接口: `/upload.php` +- 文件目录: `/resumes/` + +--- + +**总结**: 项目基础架构已完成,核心问题是工作流 B 的多轮对话状态维持。需要在 Coze 工作流 B 中实现基于 session_id 的状态管理,才能正确记录对话日志并在后台展示。 diff --git a/frontend/dist.tar.gz b/frontend/dist.tar.gz new file mode 100644 index 0000000..dece08d Binary files /dev/null and b/frontend/dist.tar.gz differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8ccc200 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + AI 面试助手 + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1571378 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "ai-interview-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix" + }, + "dependencies": { + "vue": "^3.4.21", + "vue-router": "^4.3.0", + "pinia": "^2.1.7", + "axios": "^1.6.7", + "element-plus": "^2.5.6", + "@element-plus/icons-vue": "^2.3.1", + "@volcengine/rtc": "^4.58.1", + "@coze/realtime-api": "^1.0.0", + "@coze/api": "^1.0.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "@types/node": "^20.11.24", + "typescript": "^5.4.2", + "vite": "^5.1.5", + "vue-tsc": "^2.0.6", + "tailwindcss": "^3.4.1", + "postcss": "^8.4.35", + "autoprefixer": "^10.4.18", + "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "eslint-plugin-vue": "^9.22.0" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..2b2e452 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3195 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@coze/api': + specifier: ^1.0.0 + version: 1.3.9(axios@1.13.2) + '@coze/realtime-api': + specifier: ^1.0.0 + version: 1.3.2(axios@1.13.2) + '@element-plus/icons-vue': + specifier: ^2.3.1 + version: 2.3.2(vue@3.5.27(typescript@5.9.3)) + '@volcengine/rtc': + specifier: ^4.58.1 + version: 4.68.0 + axios: + specifier: ^1.6.7 + version: 1.13.2 + element-plus: + specifier: ^2.5.6 + version: 2.13.1(vue@3.5.27(typescript@5.9.3)) + pinia: + specifier: ^2.1.7 + version: 2.3.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) + vue: + specifier: ^3.4.21 + version: 3.5.27(typescript@5.9.3) + vue-router: + specifier: ^4.3.0 + version: 4.6.4(vue@3.5.27(typescript@5.9.3)) + devDependencies: + '@types/node': + specifier: ^20.11.24 + version: 20.19.30 + '@typescript-eslint/eslint-plugin': + specifier: ^7.1.0 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^7.1.0 + version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@vitejs/plugin-vue': + specifier: ^5.0.4 + version: 5.2.4(vite@5.4.21(@types/node@20.19.30))(vue@3.5.27(typescript@5.9.3)) + autoprefixer: + specifier: ^10.4.18 + version: 10.4.23(postcss@8.5.6) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-plugin-vue: + specifier: ^9.22.0 + version: 9.33.0(eslint@8.57.1) + postcss: + specifier: ^8.4.35 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.19 + typescript: + specifier: ^5.4.2 + version: 5.9.3 + vite: + specifier: ^5.1.5 + version: 5.4.21(@types/node@20.19.30) + vue-tsc: + specifier: ^2.0.6 + version: 2.2.12(typescript@5.9.3) + +packages: + + '@agora-js/media@4.23.2-1': + resolution: {integrity: sha512-d795kSsY/qmQ9OGYn/qSa8XcUhB4nypy5I4SAW+wQ/JJScfF2ZXF/HVc9ECb9NoVurTSRqfOaw8Y/hxA+76cNA==} + + '@agora-js/report@4.23.2-1': + resolution: {integrity: sha512-FNDuGb1GKA+J/gBR2oaoArvNnaqXhZV1Si1Qli9GNodrO4SCntz4SSQds7A3BQRMTFTSuQu2tAbkCfJtQBNGuA==} + + '@agora-js/shared@4.23.2-1': + resolution: {integrity: sha512-qF0okTndl5mQzfnfV1CymQs3/TY+oxKnnvAxQ/NiJ/Hf/wo+LfgMh2UaJl7xQH5mlhQE8VE2dHMXuU3LpDTbcw==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@coze/api@1.3.7': + resolution: {integrity: sha512-r8wEwRFWEc4o0R3kL+rAqnc1r+mxPVjk41mVZqqQVenKMw09ftcIQkpw4IcJKANj6azCWWhV07G8jgo0sqA8FA==} + peerDependencies: + axios: ^1.7.1 + + '@coze/api@1.3.9': + resolution: {integrity: sha512-wDotCNH66yqEOQP1oDM7ujm9LKCg2ONfriODBSC4trjdrQfuUIaq21riuYnsKWgjKALz1jSNpjbQqJ5psOXFYQ==} + peerDependencies: + axios: ^1.7.1 + + '@coze/realtime-api@1.3.2': + resolution: {integrity: sha512-eMt/JRHmdDe1Sp7GJCkzxIidySCEYuejoPmPMxy5ZJXHPXZJAE35CoMbz/Yr8dNnrGQTZC94z6CLZ7/z9iXLDA==} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/rollup-android-arm-eabi@4.55.2': + resolution: {integrity: sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.55.2': + resolution: {integrity: sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.55.2': + resolution: {integrity: sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.55.2': + resolution: {integrity: sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.55.2': + resolution: {integrity: sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.55.2': + resolution: {integrity: sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.55.2': + resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.55.2': + resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.55.2': + resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.55.2': + resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.55.2': + resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.2': + resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.55.2': + resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.2': + resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.2': + resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.55.2': + resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.55.2': + resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.55.2': + resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.55.2': + resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.2': + resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.2': + resolution: {integrity: sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.2': + resolution: {integrity: sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.55.2': + resolution: {integrity: sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.55.2': + resolution: {integrity: sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.2': + resolution: {integrity: sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==} + cpu: [x64] + os: [win32] + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@volcengine/rtc@4.62.13': + resolution: {integrity: sha512-/HzfGfn4FxZAZp3jpnYH3UgdqRtD18rK3MPquVkR/+rCN42I+w6FCkGK3/oZRfNn4vvNX/nxw8vW3jpFdonKuw==} + + '@volcengine/rtc@4.68.0': + resolution: {integrity: sha512-6HNAIDAFewDjaOqBoMdol5U5H2+3Dp9TA9GLx/dT1NYRRjZ+p+0MwLDytJHbO0M1T9W5a/oF1xASs3eHmrepdA==} + + '@vue/compiler-core@3.5.27': + resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} + + '@vue/compiler-dom@3.5.27': + resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==} + + '@vue/compiler-sfc@3.5.27': + resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==} + + '@vue/compiler-ssr@3.5.27': + resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.27': + resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} + + '@vue/runtime-core@3.5.27': + resolution: {integrity: sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==} + + '@vue/runtime-dom@3.5.27': + resolution: {integrity: sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==} + + '@vue/server-renderer@3.5.27': + resolution: {integrity: sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==} + peerDependencies: + vue: 3.5.27 + + '@vue/shared@3.5.27': + resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agora-extension-ai-denoiser@1.1.0: + resolution: {integrity: sha512-g4klzzz7IQblNt0c+gJ/DM7sa5oMZ/7GWE4c9aqs3snHRAo+CjDieT84iAc1KUhAiY84RrfhyFdtpvbbEoYBcA==} + peerDependencies: + agora-rtc-sdk-ng: '>=4.15.0' + + agora-rtc-sdk-ng@4.23.2-1: + resolution: {integrity: sha512-Tng16+2eVKC+JsBCggey4uXk9a92W7DxwmzPMtCMMsxRgO3EAIhS9EHVjycwyCcRdDanlbeWsrGH5faD4e+eGw==} + + agora-rte-extension@1.2.4: + resolution: {integrity: sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.15: + resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001765: + resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + element-plus@2.13.1: + resolution: {integrity: sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==} + peerDependencies: + vue: ^3.3.0 + + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.22: + resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + opus-encdec@0.1.1: + resolution: {integrity: sha512-TDzyGqYqrwn5UEUNaLsfLGu8Ma+HRNrgLYj7Vx5wfTnafAA21G6Bnm/qTIa3orQi/yZPZYmkdpO/gez4nfA1Rw==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pinia@2.3.1: + resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reconnecting-websocket@4.4.0: + resolution: {integrity: sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.55.2: + resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sdp@3.2.1: + resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@0.7.41: + resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.27: + resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webrtc-adapter@8.2.0: + resolution: {integrity: sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@agora-js/media@4.23.2-1': + dependencies: + '@agora-js/report': 4.23.2-1 + '@agora-js/shared': 4.23.2-1 + agora-rte-extension: 1.2.4 + axios: 1.13.2 + webrtc-adapter: 8.2.0 + transitivePeerDependencies: + - debug + + '@agora-js/report@4.23.2-1': + dependencies: + '@agora-js/shared': 4.23.2-1 + axios: 1.13.2 + transitivePeerDependencies: + - debug + + '@agora-js/shared@4.23.2-1': + dependencies: + axios: 1.13.2 + ua-parser-js: 0.7.41 + transitivePeerDependencies: + - debug + + '@alloc/quick-lru@5.2.0': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@coze/api@1.3.7(axios@1.13.2)': + dependencies: + agora-extension-ai-denoiser: 1.1.0(agora-rtc-sdk-ng@4.23.2-1) + agora-rtc-sdk-ng: 4.23.2-1 + agora-rte-extension: 1.2.4 + axios: 1.13.2 + jsonwebtoken: 9.0.3 + node-fetch: 2.7.0 + opus-encdec: 0.1.1 + reconnecting-websocket: 4.4.0 + uuid: 10.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - utf-8-validate + + '@coze/api@1.3.9(axios@1.13.2)': + dependencies: + agora-extension-ai-denoiser: 1.1.0(agora-rtc-sdk-ng@4.23.2-1) + agora-rtc-sdk-ng: 4.23.2-1 + agora-rte-extension: 1.2.4 + axios: 1.13.2 + jsonwebtoken: 9.0.3 + node-fetch: 2.7.0 + opus-encdec: 0.1.1 + reconnecting-websocket: 4.4.0 + uuid: 10.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - utf-8-validate + + '@coze/realtime-api@1.3.2(axios@1.13.2)': + dependencies: + '@coze/api': 1.3.7(axios@1.13.2) + '@volcengine/rtc': 4.62.13 + transitivePeerDependencies: + - axios + - bufferutil + - debug + - encoding + - utf-8-validate + + '@ctrl/tinycolor@3.6.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.27(typescript@5.9.3))': + dependencies: + vue: 3.5.27(typescript@5.9.3) + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@rollup/rollup-android-arm-eabi@4.55.2': + optional: true + + '@rollup/rollup-android-arm64@4.55.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.2': + optional: true + + '@rollup/rollup-darwin-x64@4.55.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.55.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.55.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.55.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.55.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.55.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.55.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.55.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.55.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.2': + optional: true + + '@sxzz/popperjs-es@2.11.7': {} + + '@types/estree@1.0.8': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.23 + + '@types/lodash@4.17.23': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/web-bluetooth@0.0.20': {} + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.30))(vue@3.5.27(typescript@5.9.3))': + dependencies: + vite: 5.4.21(@types/node@20.19.30) + vue: 3.5.27(typescript@5.9.3) + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@volcengine/rtc@4.62.13': + dependencies: + eventemitter3: 4.0.7 + + '@volcengine/rtc@4.68.0': + dependencies: + eventemitter3: 4.0.7 + + '@vue/compiler-core@3.5.27': + dependencies: + '@babel/parser': 7.28.6 + '@vue/shared': 3.5.27 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.27': + dependencies: + '@vue/compiler-core': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/compiler-sfc@3.5.27': + dependencies: + '@babel/parser': 7.28.6 + '@vue/compiler-core': 3.5.27 + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.27': + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@2.2.12(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.27 + alien-signals: 1.0.13 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.27': + dependencies: + '@vue/shared': 3.5.27 + + '@vue/runtime-core@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/shared': 3.5.27 + + '@vue/runtime-dom@3.5.27': + dependencies: + '@vue/reactivity': 3.5.27 + '@vue/runtime-core': 3.5.27 + '@vue/shared': 3.5.27 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.27(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.27 + '@vue/shared': 3.5.27 + vue: 3.5.27(typescript@5.9.3) + + '@vue/shared@3.5.27': {} + + '@vueuse/core@10.11.1(vue@3.5.27(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.27(typescript@5.9.3)) + vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/shared@10.11.1(vue@3.5.27(typescript@5.9.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agora-extension-ai-denoiser@1.1.0(agora-rtc-sdk-ng@4.23.2-1): + dependencies: + agora-rtc-sdk-ng: 4.23.2-1 + + agora-rtc-sdk-ng@4.23.2-1: + dependencies: + '@agora-js/media': 4.23.2-1 + '@agora-js/report': 4.23.2-1 + '@agora-js/shared': 4.23.2-1 + agora-rte-extension: 1.2.4 + axios: 1.13.2 + formdata-polyfill: 4.0.10 + pako: 2.1.0 + ua-parser-js: 0.7.41 + webrtc-adapter: 8.2.0 + transitivePeerDependencies: + - debug + + agora-rte-extension@1.2.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@1.0.13: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + autoprefixer@10.4.23(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001765 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.15: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001765 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-equal-constant-time@1.0.1: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001765: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + dayjs@1.11.19: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + didyoumean@1.2.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dlv@1.1.3: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.267: {} + + element-plus@2.13.1(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.27(typescript@5.9.3)) + '@floating-ui/dom': 1.7.4 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.23 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 10.11.1(vue@3.5.27(typescript@5.9.3)) + async-validator: 4.2.5 + dayjs: 1.11.19 + lodash: 4.17.21 + lodash-es: 4.17.22 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.22)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.27(typescript@5.9.3) + transitivePeerDependencies: + - '@vue/composition-api' + + entities@7.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@9.33.0(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + eslint: 8.57.1 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.3 + vue-eslint-parser: 9.4.3(eslint@8.57.1) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fraction.js@5.3.4: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + jiti@1.21.7: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.22: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.22)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.22 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash@4.17.21: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + normalize-wheel-es@1.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + opus-encdec@0.1.1: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + pako@2.1.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pinia@2.3.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27(typescript@5.9.3) + vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@vue/composition-api' + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reconnecting-websocket@4.4.0: {} + + resolve-from@4.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.55.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.2 + '@rollup/rollup-android-arm64': 4.55.2 + '@rollup/rollup-darwin-arm64': 4.55.2 + '@rollup/rollup-darwin-x64': 4.55.2 + '@rollup/rollup-freebsd-arm64': 4.55.2 + '@rollup/rollup-freebsd-x64': 4.55.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.2 + '@rollup/rollup-linux-arm-musleabihf': 4.55.2 + '@rollup/rollup-linux-arm64-gnu': 4.55.2 + '@rollup/rollup-linux-arm64-musl': 4.55.2 + '@rollup/rollup-linux-loong64-gnu': 4.55.2 + '@rollup/rollup-linux-loong64-musl': 4.55.2 + '@rollup/rollup-linux-ppc64-gnu': 4.55.2 + '@rollup/rollup-linux-ppc64-musl': 4.55.2 + '@rollup/rollup-linux-riscv64-gnu': 4.55.2 + '@rollup/rollup-linux-riscv64-musl': 4.55.2 + '@rollup/rollup-linux-s390x-gnu': 4.55.2 + '@rollup/rollup-linux-x64-gnu': 4.55.2 + '@rollup/rollup-linux-x64-musl': 4.55.2 + '@rollup/rollup-openbsd-x64': 4.55.2 + '@rollup/rollup-openharmony-arm64': 4.55.2 + '@rollup/rollup-win32-arm64-msvc': 4.55.2 + '@rollup/rollup-win32-ia32-msvc': 4.55.2 + '@rollup/rollup-win32-x64-gnu': 4.55.2 + '@rollup/rollup-win32-x64-msvc': 4.55.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + sdp@3.2.1: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.9.3: {} + + ua-parser-js@0.7.41: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + uuid@10.0.0: {} + + vite@5.4.21(@types/node@20.19.30): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.55.2 + optionalDependencies: + '@types/node': 20.19.30 + fsevents: 2.3.3 + + vscode-uri@3.1.0: {} + + vue-demi@0.14.10(vue@3.5.27(typescript@5.9.3)): + dependencies: + vue: 3.5.27(typescript@5.9.3) + + vue-eslint-parser@9.4.3(eslint@8.57.1): + dependencies: + debug: 4.4.3 + eslint: 8.57.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + lodash: 4.17.21 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.27(typescript@5.9.3) + + vue-tsc@2.2.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.27(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.27 + '@vue/compiler-sfc': 3.5.27 + '@vue/runtime-dom': 3.5.27 + '@vue/server-renderer': 3.5.27(vue@3.5.27(typescript@5.9.3)) + '@vue/shared': 3.5.27 + optionalDependencies: + typescript: 5.9.3 + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} + + webrtc-adapter@8.2.0: + dependencies: + sdp: 3.2.1 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + xml-name-validator@4.0.0: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..ad45c7c --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,10 @@ + + + + + diff --git a/frontend/src/api/candidate.ts b/frontend/src/api/candidate.ts new file mode 100644 index 0000000..ba76ae3 --- /dev/null +++ b/frontend/src/api/candidate.ts @@ -0,0 +1,178 @@ +import { request } from './request' + +// 类型定义 +export interface InitInterviewResponse { + sessionId: string + name: string + debugUrl?: string + workflowResponse?: unknown +} + +export interface SubmitCandidateResponse { + sessionId: string + fileId: string +} + +export interface CreateRoomRequest { + sessionId?: string + fileId?: string +} + +export interface CreateRoomResponse { + roomId: string + token: string + appId: string + userId: string + sessionId?: string // 后端生成的会话ID +} + +export interface ChatRequest { + sessionId: string + message: string + conversationId?: string +} + +export interface ChatResponse { + reply: string + conversationId: string + debugInfo?: { + status_history?: Array<{ + iteration: number + status: string + required_action?: any + }> + messages?: Array<{ + role: string + type: string + content: string + }> + raw_responses?: any[] + } +} + +export interface Candidate { + sessionId: string + name: string + status: 'pending' | 'ongoing' | 'completed' + score?: number + createdAt: string +} + +export interface CandidateDetail extends Candidate { + resume: string + currentStage: number + scores?: { + salesSkill: number + salesMindset: number + quality: number + motivation: number + total: number + } + analysis?: string + interviewLog?: string + completedAt?: string +} + +export interface CandidateListResponse { + list: Candidate[] + total: number + page: number + pageSize: number +} + +export interface CandidateListParams { + page?: number + pageSize?: number + keyword?: string + status?: string + startDate?: string + endDate?: string +} + +// API 方法 +export const candidateApi = { + /** + * 初始化面试(工作流 A) + * 上传简历 + 执行初始化工作流 → 返回 sessionId + */ + initInterview(name: string, resumeFile: File) { + const formData = new FormData() + formData.append('name', name) + formData.append('file', resumeFile) + return request.upload('/init-interview', formData) + }, + + /** + * 上传简历到 Coze + */ + uploadResume(file: File) { + const formData = new FormData() + formData.append('file', file) + return request.upload<{ fileId: string }>('/upload', formData) + }, + + /** + * 提交候选人信息(上传简历)- 旧方法 + */ + submit(name: string, resumeFile: File) { + const formData = new FormData() + formData.append('name', name) + formData.append('resume', resumeFile) + return request.upload('/candidates', formData) + }, + + /** + * 创建语音房间 + */ + createRoom(data: CreateRoomRequest) { + return request.post('/rooms', data) + }, + + /** + * 结束面试 + */ + endInterview(sessionId: string) { + return request.post<{ success: boolean }>(`/interviews/${sessionId}/end`) + }, + + /** + * 获取候选人列表 + */ + getList(params: CandidateListParams) { + return request.get('/candidates', { params }) + }, + + /** + * 获取候选人详情 + */ + getDetail(sessionId: string) { + return request.get(`/candidates/${sessionId}`) + }, + + /** + * 导出 PDF 报告 + */ + exportPdf(sessionId: string) { + return `/api/candidates/${sessionId}/export` + }, + + /** + * 文本对话(模拟语音) + */ + chat(data: ChatRequest) { + return request.post('/chat', data) + }, + + /** + * 获取 Coze Realtime SDK 配置 + * 用于直接连接 Coze 语音服务 + */ + getCozeConfig() { + return request.get<{ + accessToken: string + botId: string + voiceId: string + connectorId: string + }>('/coze-config') + }, +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..170d004 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,11 @@ +export { request } from './request' +export { candidateApi } from './candidate' +export type { + SubmitCandidateResponse, + CreateRoomRequest, + CreateRoomResponse, + Candidate, + CandidateDetail, + CandidateListResponse, + CandidateListParams, +} from './candidate' diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts new file mode 100644 index 0000000..1cb2e84 --- /dev/null +++ b/frontend/src/api/request.ts @@ -0,0 +1,86 @@ +import axios from 'axios' +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import { ElMessage } from 'element-plus' + +// API 响应类型 +export interface ApiResponse { + code: number + message: string + data: T +} + +// 创建 axios 实例 +const instance: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', + timeout: 120000, // 120秒,Coze API 可能需要较长时间 + headers: { + 'Content-Type': 'application/json', + }, +}) + +// 请求拦截器 +instance.interceptors.request.use( + (config) => { + // 可以在这里添加 token + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +instance.interceptors.response.use( + (response: AxiosResponse) => { + const { data } = response + + // 业务错误处理 + if (data.code !== 0) { + ElMessage.error(data.message || '请求失败') + return Promise.reject(new Error(data.message)) + } + + return response + }, + (error) => { + // HTTP 错误处理 + const message = error.response?.data?.message || error.message || '网络错误' + ElMessage.error(message) + return Promise.reject(error) + } +) + +// 封装请求方法 +export const request = { + get(url: string, config?: AxiosRequestConfig): Promise> { + return instance.get(url, config).then((res) => res.data) + }, + + post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { + return instance.post(url, data, config).then((res) => res.data) + }, + + put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { + return instance.put(url, data, config).then((res) => res.data) + }, + + delete(url: string, config?: AxiosRequestConfig): Promise> { + return instance.delete(url, config).then((res) => res.data) + }, + + // 上传文件 + upload(url: string, formData: FormData, config?: AxiosRequestConfig): Promise> { + return instance.post(url, formData, { + ...config, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }).then((res) => res.data) + }, +} + +export default instance diff --git a/frontend/src/composables/index.ts b/frontend/src/composables/index.ts new file mode 100644 index 0000000..964c497 --- /dev/null +++ b/frontend/src/composables/index.ts @@ -0,0 +1 @@ +export { useRTC } from './useRTC' diff --git a/frontend/src/composables/useCozeRealtime.ts b/frontend/src/composables/useCozeRealtime.ts new file mode 100644 index 0000000..7b26ef8 --- /dev/null +++ b/frontend/src/composables/useCozeRealtime.ts @@ -0,0 +1,749 @@ +/** + * Coze Realtime 语音面试 Hook + * + * 基于 Coze 官方 SDK (@coze/realtime-api) 实现语音面试 + * + * 架构说明: + * 1. 使用 @coze/realtime-api SDK 直接连接 + * 2. 在 bot.join 后通过 sendMessage 发送 session.update 信令传递 session_id + * + * 参考文档: + * - https://www.coze.cn/open/docs/dev_how_to_guides/Realtime_web + * - https://www.coze.cn/open/docs/developer_guides/signaling_uplink_event + * - https://github.com/coze-dev/coze-js/blob/main/examples/realtime-quickstart-react/src/App.tsx + */ +import { ref, computed } from 'vue' +import type { RTCConnectionState } from '@/types' + +// Coze Realtime 连接参数 +interface CozeRealtimeParams { + accessToken: string // PAT Token + botId: string // 智能体 ID + sessionId: string // 面试会话 ID(关键:需要传递给工作流) + voiceId?: string // 音色 ID(可选) +} + +// 音频配置(根据 Coze 官方文档 session.update 事件参数) +const AUDIO_CONFIG = { + // VAD(语音活动检测)配置 + // 注意:silence_duration_ms 取值范围 200~2000,默认 500 + vad: { + silenceDurationMs: 800, // 静音持续 0.8 秒判定为说话结束(降低延迟) + prefixPaddingMs: 300, // 前置填充 300ms(防止开头被截断) + }, + // 打断配置 + allowVoiceInterrupt: false, // 禁止语音打断 AI +} + +// 本地 VAD 配置 +const LOCAL_VAD_CONFIG = { + enabled: true, // 启用本地 VAD + volumeThreshold: 0.15, // 音量阈值(0-1),15% 过滤环境噪音和回声 + silenceTimeout: 800, // 静音超时(毫秒),持续静音后触发 commit + minSpeechDuration: 300, // 最小说话时长(毫秒),防止误触发 + checkInterval: 50, // 检测间隔(毫秒) +} + +// 信令事件名称(来自 @coze/realtime-api EventNames) +// 注意:实际使用时从 SDK 动态导入,这里仅作为类型参考 +const _EventNamesRef = { + ALL: 'realtime.event', + ALL_CLIENT: 'client.*', + ALL_SERVER: 'server.*', + CONNECTED: 'client.connected', + INTERRUPTED: 'client.interrupted', + DISCONNECTED: 'client.disconnected', + AUDIO_UNMUTED: 'client.audio.unmuted', + AUDIO_MUTED: 'client.audio.muted', + ERROR: 'client.error', +} +void _EventNamesRef // 避免 unused 警告 + +/** + * Coze Realtime Hook + * 使用 Coze 官方 SDK 进行语音面试 + */ +export function useCozeRealtime() { + const connectionState = ref('disconnected') + const isMuted = ref(false) + const sessionId = ref('') + const isSessionUpdateSent = ref(false) + const lastError = ref(null) + + // 通讯时间线(用于调试显示) + interface TimelineEvent { + time: string // HH:MM:SS.mmm 格式 + type: 'send' | 'receive' | 'audio' | 'system' | 'error' + event: string // 事件名 + detail?: string // 详情 + } + const timeline = ref([]) + + // 添加时间线事件 + function addTimelineEvent(type: TimelineEvent['type'], event: string, detail?: string) { + const now = new Date() + const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}` + timeline.value.push({ time, type, event, detail }) + // 保留最近 50 条 + if (timeline.value.length > 50) { + timeline.value.shift() + } + } + + // 调试信息 + const debugInfo = ref<{ + rtcConnected: boolean + sessionUpdateSent: boolean + sessionId: string + botJoined: boolean + eventsSent: string[] + eventsReceived: string[] + errors: string[] + }>({ + rtcConnected: false, + sessionUpdateSent: false, + sessionId: '', + botJoined: false, + eventsSent: [], + eventsReceived: [], + errors: [], + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let client: any = null + + // ============ 本地 VAD 相关 ============ + const isSpeaking = ref(false) // 用户是否正在说话 + const isAISpeaking = ref(false) // AI 是否正在说话(用于暂停收音) + const currentVolume = ref(0) // 当前音量(0-1) + let audioContext: AudioContext | null = null + let analyser: AnalyserNode | null = null + let mediaStream: MediaStream | null = null + let vadCheckInterval: number | null = null + let silenceStartTime: number | null = null + let speechStartTime: number | null = null + let hasCommittedThisTurn = false // 本轮是否已提交 + + /** + * 启动本地 VAD 监测 + */ + async function startLocalVAD() { + if (!LOCAL_VAD_CONFIG.enabled) return + + try { + // 获取麦克风音频流(开启回声消除和噪音抑制) + mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, // 回声消除(消除扬声器声音) + noiseSuppression: true, // 噪音抑制 + autoGainControl: true, // 自动增益控制 + } + }) + audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + analyser = audioContext.createAnalyser() + analyser.fftSize = 512 + analyser.smoothingTimeConstant = 0.4 // 提高平滑度,减少抖动 + + const source = audioContext.createMediaStreamSource(mediaStream) + source.connect(analyser) + + const dataArray = new Uint8Array(analyser.frequencyBinCount) + + // 定期检测音量 + vadCheckInterval = window.setInterval(() => { + if (!analyser || isMuted.value) return + + analyser.getByteFrequencyData(dataArray) + + // 计算平均音量(归一化到 0-1) + const sum = dataArray.reduce((a, b) => a + b, 0) + const avg = sum / dataArray.length / 255 + currentVolume.value = avg + + // 注意:不再基于 isAISpeaking 暂停收音 + // 依赖浏览器的回声消除 + 较高的音量阈值来过滤 AI 声音 + + const now = Date.now() + + if (avg > LOCAL_VAD_CONFIG.volumeThreshold) { + // 检测到声音 + if (!isSpeaking.value) { + isSpeaking.value = true + speechStartTime = now + silenceStartTime = null + hasCommittedThisTurn = false + addTimelineEvent('audio', '🎤 检测到说话', `音量: ${(avg * 100).toFixed(1)}%`) + console.log(`🎤 [LocalVAD] 检测到说话, 音量: ${(avg * 100).toFixed(1)}%`) + } + silenceStartTime = null + } else if (isSpeaking.value) { + // 正在说话但当前静音 + if (!silenceStartTime) { + silenceStartTime = now + } + + const silenceDuration = now - silenceStartTime + const speechDuration = speechStartTime ? now - speechStartTime : 0 + + // 检查是否满足提交条件 + if (silenceDuration >= LOCAL_VAD_CONFIG.silenceTimeout && + speechDuration >= LOCAL_VAD_CONFIG.minSpeechDuration && + !hasCommittedThisTurn) { + // 静音超时,触发提交 + isSpeaking.value = false + hasCommittedThisTurn = true + addTimelineEvent('audio', '🔇 说话结束', `静音 ${silenceDuration}ms`) + console.log(`🔇 [LocalVAD] 说话结束, 静音 ${silenceDuration}ms, 自动提交`) + + // 自动发送 commit + commitAudioInputInternal() + } + } + }, LOCAL_VAD_CONFIG.checkInterval) + + addTimelineEvent('system', '🎙️ 本地VAD启动', `阈值: ${LOCAL_VAD_CONFIG.volumeThreshold}, 静音: ${LOCAL_VAD_CONFIG.silenceTimeout}ms`) + console.log('✅ [LocalVAD] 本地 VAD 已启动') + } catch (error) { + console.error('❌ [LocalVAD] 启动失败:', error) + addTimelineEvent('error', '❌ VAD启动失败', String(error)) + } + } + + /** + * 停止本地 VAD 监测 + */ + function stopLocalVAD() { + if (vadCheckInterval) { + clearInterval(vadCheckInterval) + vadCheckInterval = null + } + if (mediaStream) { + mediaStream.getTracks().forEach(track => track.stop()) + mediaStream = null + } + if (audioContext) { + audioContext.close() + audioContext = null + } + analyser = null + isSpeaking.value = false + currentVolume.value = 0 + console.log('🛑 [LocalVAD] 已停止') + } + + /** + * 内部提交函数(供 VAD 调用) + */ + function commitAudioInputInternal() { + if (!client) return false + + const commitEvent = { + id: `evt_${Date.now()}_commit`, + event_type: 'input_audio_buffer.commit', + } + + try { + client.sendMessage(commitEvent) + addTimelineEvent('send', '📤 语音已提交', '本地VAD触发') + console.log('📤 [LocalVAD] 已发送 commit') + debugInfo.value.eventsSent.push('input_audio_buffer.commit (local_vad)') + return true + } catch (error) { + console.error('❌ [LocalVAD] commit 失败:', error) + return false + } + } + + /** + * 连接到 Coze 语音房间 + * + * 使用 @coze/realtime-api SDK 连接 + */ + async function connect(params: CozeRealtimeParams) { + try { + connectionState.value = 'connecting' + lastError.value = null + sessionId.value = params.sessionId + + // 更新调试信息 + debugInfo.value.sessionId = params.sessionId + debugInfo.value.eventsSent = [] + debugInfo.value.eventsReceived = [] + debugInfo.value.errors = [] + + // 清空时间线 + timeline.value = [] + addTimelineEvent('system', '🚀 开始连接', `session: ${params.sessionId.slice(-8)}`) + + console.log('=== Coze Realtime: Starting connection ===') + console.log('Session ID:', params.sessionId) + console.log('Bot ID:', params.botId) + + // 1. 动态导入 Coze Realtime SDK + console.log('Step 1: Loading @coze/realtime-api SDK...') + const { RealtimeClient, RealtimeUtils, EventNames: SDKEventNames } = await import('@coze/realtime-api') + + // 2. 检查设备权限 + console.log('Step 2: Checking device permission...') + const permission = await RealtimeUtils.checkDevicePermission() + if (!permission.audio) { + throw new Error('需要麦克风访问权限。请在浏览器地址栏左侧的锁图标中授权。') + } + console.log('✅ Microphone permission granted') + + // 3. 恢复音频上下文(解决浏览器自动播放限制) + console.log('Step 3: Resuming AudioContext for autoplay...') + try { + // 创建并恢复音频上下文,确保音频可以播放 + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + if (audioContext.state === 'suspended') { + await audioContext.resume() + console.log('✅ AudioContext resumed') + } + } catch (audioErr) { + console.warn('AudioContext resume failed:', audioErr) + } + + // 4. 初始化客户端(含音频配置) + console.log('Step 4: Initializing RealtimeClient...') + console.log('Audio config:', AUDIO_CONFIG) + client = new RealtimeClient({ + accessToken: params.accessToken, + botId: params.botId, + connectorId: '1024', // 固定值 + voiceId: params.voiceId, + allowPersonalAccessTokenInBrowser: true, + debug: true, + // 🔊 音频配置 + audioMutedDefault: false, // 默认不静音,启用音频播放 + suppressStationaryNoise: true, // 抑制静态噪音 + }) + + // 5. 配置事件监听 + console.log('Step 5: Setting up event listeners...') + setupEventListeners(SDKEventNames, params.sessionId) + + // 6. 建立连接 + console.log('Step 6: Connecting to Coze...') + await client.connect() + + // 7. 确保音频输出启用 + console.log('Step 7: Ensuring audio output is enabled...') + try { + // 尝试播放一个静音音频来解锁音频播放 + const silentAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') + silentAudio.volume = 0.01 + await silentAudio.play().catch(() => {}) + console.log('✅ Audio output unlocked') + } catch (e) { + console.warn('Audio unlock attempt:', e) + } + + debugInfo.value.rtcConnected = true + connectionState.value = 'connected' + console.log('=== Coze Realtime: Connected successfully ===') + + // 8. 确保远程音频播放已启用 + console.log('Step 8: Enabling remote audio playback...') + try { + // 尝试启用远程音频播放(AI 的声音) + if (typeof client.setRemoteAudioEnable === 'function') { + await client.setRemoteAudioEnable(true) + console.log('✅ Remote audio playback enabled via setRemoteAudioEnable') + } + // 尝试通过 RTC 引擎设置 + const rtcEngine = client.getRtcEngine?.() + if (rtcEngine) { + console.log('📻 RTC Engine available, checking audio settings...') + if (typeof rtcEngine.setRemoteAudioPlaybackVolume === 'function') { + rtcEngine.setRemoteAudioPlaybackVolume('*', 100) + console.log('✅ Remote audio volume set to 100') + } + if (typeof rtcEngine.subscribeAllRemoteAudio === 'function') { + rtcEngine.subscribeAllRemoteAudio() + console.log('✅ Subscribed to all remote audio') + } + } + } catch (e) { + console.warn('Remote audio setup:', e) + } + + // 9. 启动本地 VAD(更快响应) + console.log('Step 9: Starting local VAD...') + await startLocalVAD() + + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error('Coze Realtime connect error:', error) + lastError.value = errorMessage + debugInfo.value.errors.push(errorMessage) + connectionState.value = 'failed' + throw error + } + } + + /** + * 设置事件监听器 + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function setupEventListeners(SDKEventNames: any, sid: string) { + if (!client) return + + console.log('🎧 Setting up event listeners...') + console.log('Available EventNames:', Object.keys(SDKEventNames)) + + addTimelineEvent('system', '🔌 连接建立', '开始监听事件') + + // 监听所有服务端事件 + client.on(SDKEventNames.ALL_SERVER, (eventName: string, event: unknown) => { + const timestamp = new Date().toLocaleTimeString() + console.log(`📩 [${timestamp}] Server event: ${eventName}`, event) + debugInfo.value.eventsReceived.push(`${eventName} @ ${timestamp}`) + + // 当 Bot 加入房间后,发送 session.update 事件 + if (eventName === 'server.bot.join') { + console.log('🤖 Bot joined! Sending session.update...') + debugInfo.value.botJoined = true + addTimelineEvent('receive', '🤖 Bot 加入', 'server.bot.join') + sendSessionUpdate(sid) + } + + // 用户开始说话 + if (eventName === 'server.input_audio_buffer.speech_started') { + addTimelineEvent('audio', '🎤 检测到说话', '用户开始说话') + } + + // 用户停止说话(VAD 触发) + if (eventName === 'server.input_audio_buffer.speech_stopped') { + addTimelineEvent('audio', '🔇 说话结束', 'VAD 检测到静音') + } + + // 语音提交到服务器 + if (eventName === 'server.input_audio_buffer.committed') { + addTimelineEvent('send', '📤 语音已提交', '等待 AI 处理') + } + + // AI 开始处理/思考 + if (eventName === 'server.conversation.item.created') { + addTimelineEvent('receive', '🧠 AI 开始处理', 'conversation.item.created') + } + + // AI 开始回复(流式) + if (eventName === 'server.conversation.message.delta') { + // 只记录第一次 delta(避免太多条目) + const lastEvent = timeline.value[timeline.value.length - 1] + if (!lastEvent || lastEvent.event !== '💬 AI 回复中') { + isAISpeaking.value = true // 🔇 AI 开始说话,暂停用户收音 + addTimelineEvent('receive', '💬 AI 回复中', '开始接收语音流(暂停收音)') + } + } + + // AI 回复完成 + if (eventName === 'server.conversation.message.completed') { + addTimelineEvent('receive', '✅ AI 回复完成', 'message.completed') + // 延迟后重置 AI 说话状态(用于 UI 显示) + setTimeout(() => { + if (isAISpeaking.value) { + isAISpeaking.value = false + } + }, 1000) + } + + // AI 开始说话(用于 UI 显示,不影响收音) + if (eventName === 'server.audio.agent.speech_started') { + isAISpeaking.value = true + addTimelineEvent('receive', '🔊 AI 开始说话', eventName) + } + + // AI 说话结束(用于 UI 显示) + if (eventName === 'server.audio.agent.speech_stopped') { + isAISpeaking.value = false + addTimelineEvent('receive', '🔈 AI 说话结束', eventName) + } + + // 回合结束 + if (eventName === 'server.response.done') { + isAISpeaking.value = false + addTimelineEvent('receive', '🏁 回合结束', 'response.done') + } + + // 记录所有包含 audio 的事件(用于调试) + if (eventName.toLowerCase().includes('audio')) { + console.log(`🔊 [Audio Event] ${eventName}:`, event) + } + }) + + // 监听客户端事件 + client.on(SDKEventNames.CONNECTED, () => { + console.log('✅ [Client] Connected to Coze Realtime') + debugInfo.value.eventsReceived.push('client.connected') + addTimelineEvent('system', '✅ 已连接', 'RTC 连接成功') + }) + + client.on(SDKEventNames.DISCONNECTED, () => { + console.log('❌ [Client] Disconnected from Coze Realtime') + connectionState.value = 'disconnected' + debugInfo.value.eventsReceived.push('client.disconnected') + addTimelineEvent('system', '❌ 已断开', '连接断开') + }) + + client.on(SDKEventNames.INTERRUPTED, () => { + console.log('⚠️ [Client] Interrupted') + debugInfo.value.eventsReceived.push('client.interrupted') + addTimelineEvent('system', '⚠️ 被打断', 'AI 被用户打断') + }) + + client.on(SDKEventNames.AUDIO_MUTED, () => { + console.log('🔇 [Client] Audio muted') + addTimelineEvent('audio', '🔇 麦克风关闭', '') + }) + + client.on(SDKEventNames.AUDIO_UNMUTED, () => { + console.log('🔊 [Client] Audio unmuted') + addTimelineEvent('audio', '🔊 麦克风开启', '') + }) + + client.on(SDKEventNames.ERROR, (error: unknown) => { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error('❌ [Client] Error:', error) + debugInfo.value.errors.push(errorMessage) + addTimelineEvent('error', '❌ 错误', errorMessage) + }) + + // 监听所有事件(调试用) + client.on(SDKEventNames.ALL, (eventName: string, data: unknown) => { + // 避免重复日志,只记录非 server/client 前缀的事件 + if (!eventName.startsWith('server.') && !eventName.startsWith('client.')) { + console.log(`📬 [ALL] Event: ${eventName}`, data) + } + }) + + console.log('✅ Event listeners set up complete') + } + + /** + * 发送 session.update 信令事件 + * + * 根据 Coze 官方文档 signaling_uplink_event: + * - 在 bot.join 后发送 + * - data.chat_config.parameters: 传递对话流自定义参数(如 session_id) + * - data.chat_config.allow_voice_interrupt: 是否允许语音打断 + * - data.turn_detection: VAD 配置 + * + * 注意:只发送一次,避免 Bot 重连时重复发送导致对话重置 + */ + function sendSessionUpdate(sid: string) { + if (!client || !sid) { + console.warn('Cannot send session.update: client or sessionId is missing') + return false + } + + // 防止重复发送(Bot 重连时会再次触发 bot.join 事件) + if (isSessionUpdateSent.value) { + console.log('⚠️ session.update already sent, skipping to prevent conversation reset') + return true + } + + // 构造 session.update 事件(严格按照 Coze 文档格式) + const event = { + id: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + event_type: 'session.update', + data: { + // 会话配置 + chat_config: { + // 传递对话流自定义参数 + parameters: { + session_id: sid, + }, + // 禁止语音打断 AI + allow_voice_interrupt: AUDIO_CONFIG.allowVoiceInterrupt, + }, + // VAD 声音检测配置 + turn_detection: { + type: 'server_vad', // 服务端 VAD + silence_duration_ms: AUDIO_CONFIG.vad.silenceDurationMs, // 静音 2 秒判定说完(范围 200~2000) + prefix_padding_ms: AUDIO_CONFIG.vad.prefixPaddingMs, // 前置填充 600ms + }, + }, + } + + console.log('📤 Sending session.update:', JSON.stringify(event, null, 2)) + + try { + // 使用 sendMessage 发送上行信令 + client.sendMessage(event) + console.log('✅ session.update sent successfully!') + console.log(`📊 VAD: silence=${AUDIO_CONFIG.vad.silenceDurationMs}ms, prefix=${AUDIO_CONFIG.vad.prefixPaddingMs}ms`) + console.log(`🔇 Voice interrupt: ${AUDIO_CONFIG.allowVoiceInterrupt ? 'enabled' : 'disabled'}`) + debugInfo.value.eventsSent.push('session.update') + isSessionUpdateSent.value = true + debugInfo.value.sessionUpdateSent = true + return true + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error('❌ Failed to send session.update:', error) + debugInfo.value.errors.push(`sendMessage failed: ${errorMessage}`) + return false + } + } + + /** + * 断开连接 + */ + async function disconnect() { + console.log('🔌 Disconnecting from Coze Realtime...') + try { + // 先停止本地 VAD + stopLocalVAD() + + if (client) { + // 先清除事件监听器 + try { + client.clearEventHandlers?.() + } catch (e) { + console.warn('clearEventHandlers failed:', e) + } + + // 断开连接(可能是异步的) + try { + await client.disconnect() + } catch (e) { + console.warn('disconnect call failed:', e) + } + + client = null + } + + connectionState.value = 'disconnected' + isSessionUpdateSent.value = false + debugInfo.value.rtcConnected = false + debugInfo.value.sessionUpdateSent = false + debugInfo.value.botJoined = false + console.log('✅ Coze Realtime disconnected successfully') + } catch (error) { + console.error('❌ Disconnect error:', error) + // 即使出错也要重置状态 + stopLocalVAD() + connectionState.value = 'disconnected' + client = null + } + } + + /** + * 切换静音 + */ + async function toggleMute(mute: boolean) { + if (client) { + try { + await client.setAudioEnable(!mute) + isMuted.value = mute + } catch (error) { + console.error('Toggle mute error:', error) + } + } + } + + /** + * 打断 AI + */ + function interrupt() { + if (client) { + try { + client.interrupt() + } catch (error) { + console.error('Interrupt error:', error) + } + } + } + + /** + * 提交当前语音输入(手动触发 VAD 结束) + * 发送 input_audio_buffer.commit 信令告诉服务器用户已说完 + */ + function commitAudioInput() { + if (!client) { + console.warn('Cannot commit audio: client is not connected') + return false + } + + // 🛑 立刻停止录音状态(用户点击了"说完了") + isSpeaking.value = false + silenceStartTime = null + speechStartTime = null + hasCommittedThisTurn = true + addTimelineEvent('send', '🛑 手动停止录音', '用户点击说完了') + + // 方案 1: 发送 input_audio_buffer.commit 事件(OpenAI 兼容格式) + const commitEvent = { + id: `evt_${Date.now()}_commit`, + event_type: 'input_audio_buffer.commit', + } + + console.log('📤 Sending input_audio_buffer.commit (手动触发 VAD 结束)...') + + try { + client.sendMessage(commitEvent) + console.log('✅ Audio input committed!') + addTimelineEvent('send', '📤 语音已提交', '手动触发') + debugInfo.value.eventsSent.push('input_audio_buffer.commit') + return true + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error('❌ Failed to commit audio:', error) + + // 方案 2: 如果 commit 不支持,尝试发送 conversation.item.create + console.log('📤 Trying alternative: conversation.item.create...') + try { + const createEvent = { + id: `evt_${Date.now()}_create`, + event_type: 'conversation.item.create', + data: { + item: { + type: 'message', + role: 'user', + content: [{ type: 'input_audio', audio: '' }] + } + } + } + client.sendMessage(createEvent) + console.log('✅ Alternative event sent!') + return true + } catch (altError) { + console.error('❌ Alternative also failed:', altError) + debugInfo.value.errors.push(`commitAudio failed: ${errorMessage}`) + return false + } + } + } + + /** + * 获取调试信息摘要 + */ + const debugSummary = computed(() => { + return { + ...debugInfo.value, + connectionState: connectionState.value, + isMuted: isMuted.value, + } + }) + + return { + connectionState, + isMuted, + sessionId, + isSessionUpdateSent, + lastError, + debugInfo, + debugSummary, + timeline, // 通讯时间线 + // 本地 VAD 状态 + isSpeaking, + isAISpeaking, // AI 是否正在说话 + currentVolume, + connect, + disconnect, + toggleMute, + interrupt, + commitAudioInput, // 手动触发 VAD 结束(说完了) + } +} diff --git a/frontend/src/composables/useRTC.ts b/frontend/src/composables/useRTC.ts new file mode 100644 index 0000000..b8add06 --- /dev/null +++ b/frontend/src/composables/useRTC.ts @@ -0,0 +1,468 @@ +import { ref } from 'vue' +import type { RTCConnectionState } from '@/types' + +// RTC 连接参数 +interface RTCConnectParams { + appId: string + roomId: string + userId: string + token: string + sessionId?: string // 面试会话 ID,用于传递给工作流 +} + +/** + * 火山引擎 RTC Hook + * 封装 RTC 连接、断开、静音、TTS 发送等操作 + */ +export function useRTC() { + const connectionState = ref('disconnected') + const isMuted = ref(false) + const isReceiveOnly = ref(false) // 仅接收模式 + const isTTSSending = ref(false) // TTS 正在发送 + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let engine: any = null + let audioContext: AudioContext | null = null + let ttsDestination: MediaStreamAudioDestinationNode | null = null + + /** + * 初始化 TTS 音频上下文 + * 创建一个虚拟音频目标,用于捕获 TTS 输出 + */ + function initTTSAudio() { + if (!audioContext) { + audioContext = new AudioContext() + ttsDestination = audioContext.createMediaStreamDestination() + console.log('TTS audio context initialized') + } + } + + /** + * 连接 RTC 房间(TTS 模式:使用 TTS 模拟语音输入) + * + * 工作原理: + * 1. 创建虚拟音频设备(MediaStreamAudioDestinationNode) + * 2. TTS 输出连接到虚拟设备 + * 3. RTC 从虚拟设备采集音频发送 + */ + async function connectTTSMode(params: RTCConnectParams) { + try { + connectionState.value = 'connecting' + isReceiveOnly.value = false // TTS 模式可以发送 + + // 初始化 TTS 音频 + initTTSAudio() + + // 动态导入 RTC SDK + console.log('Loading RTC SDK (TTS mode)...') + const VERTC = await import('@volcengine/rtc') + + // 创建引擎 + console.log('Creating RTC engine with appId:', params.appId) + engine = VERTC.default.createEngine(params.appId) + + // 监听事件 + engine.on('onUserJoined', (event: { userId: string }) => { + console.log('User joined:', event.userId) + }) + + engine.on('onUserLeave', (event: { userId: string }) => { + console.log('User left:', event.userId) + }) + + engine.on('onError', (error: Error) => { + console.error('RTC error:', error) + connectionState.value = 'failed' + }) + + engine.on('onRoomStateChanged', (event: { state: number; errorCode: number }) => { + console.log('Room state changed:', event) + }) + + // 监听远端音频(AI 的回复) + engine.on('onRemoteAudioPropertiesReport', (event: any) => { + if (event && event.length > 0) { + console.log('AI is speaking:', event) + } + }) + + // 加入房间 + console.log('Joining room (TTS mode):', params.roomId) + await engine.joinRoom( + params.token, + params.roomId, + { + userId: params.userId, + }, + { + isAutoPublish: true, // 自动发布(TTS 音频) + isAutoSubscribeAudio: true, // 接收 AI 语音 + isAutoSubscribeVideo: false, + } + ) + + // 设置自定义音频轨道 + if (ttsDestination) { + const stream = ttsDestination.stream + const audioTrack = stream.getAudioTracks()[0] + + if (audioTrack) { + console.log('Setting custom audio track for TTS...') + // 使用 setAudioTrack 或 replaceTrack API + try { + // 方法1: 尝试使用 setCustomizeAudioTrack + if (engine.setCustomizeAudioTrack) { + await engine.setCustomizeAudioTrack(audioTrack) + console.log('Custom audio track set via setCustomizeAudioTrack') + } + // 方法2: 尝试使用外部音频采集 + else if (engine.setExternalAudioSource) { + await engine.setExternalAudioSource(true, 48000, 1) + console.log('External audio source enabled') + } + // 方法3: 开始采集但不使用默认设备 + else { + console.log('Using default audio capture (TTS may not work)') + } + } catch (e) { + console.warn('Failed to set custom audio track:', e) + } + } + } + + // 开始音频采集(使用静音或自定义源) + try { + await engine.startAudioCapture() + console.log('Audio capture started') + } catch (e) { + console.warn('Audio capture failed (expected in TTS mode):', e) + } + + connectionState.value = 'connected' + console.log('RTC connected successfully (TTS mode)') + } catch (error: any) { + console.error('RTC connect error:', error) + connectionState.value = 'failed' + throw error + } + } + + /** + * 使用 TTS 发送文字到 RTC 房间 + * 将文字转换为语音并通过 RTC 发送 + */ + async function sendTTSMessage(text: string): Promise { + if (!audioContext || !ttsDestination) { + console.error('TTS audio not initialized') + return + } + + isTTSSending.value = true + + return new Promise((resolve, reject) => { + if (!window.speechSynthesis) { + isTTSSending.value = false + reject(new Error('浏览器不支持语音合成')) + return + } + + const utterance = new SpeechSynthesisUtterance(text) + utterance.lang = 'zh-CN' + utterance.rate = 1.0 + utterance.pitch = 1.0 + utterance.volume = 1.0 + + // 选择中文语音 + const voices = window.speechSynthesis.getVoices() + const chineseVoice = voices.find(v => v.lang.includes('zh')) + if (chineseVoice) { + utterance.voice = chineseVoice + } + + utterance.onend = () => { + console.log('TTS finished:', text) + isTTSSending.value = false + resolve() + } + + utterance.onerror = (event) => { + console.error('TTS error:', event) + isTTSSending.value = false + reject(new Error('语音合成失败')) + } + + console.log('TTS speaking:', text) + window.speechSynthesis.speak(utterance) + }) + } + + /** + * 连接 RTC 房间(正常麦克风模式) + */ + async function connect(params: RTCConnectParams) { + try { + connectionState.value = 'connecting' + isReceiveOnly.value = false + + // 1. 先请求麦克风权限 + console.log('Requesting microphone permission...') + await requestMicrophonePermission() + + // 2. 动态导入 RTC SDK + console.log('Loading RTC SDK...') + const VERTC = await import('@volcengine/rtc') + + // 3. 创建引擎 + console.log('Creating RTC engine with appId:', params.appId) + engine = VERTC.default.createEngine(params.appId) + + // 4. 监听事件 + engine.on('onUserJoined', (event: { userId: string }) => { + console.log('User joined:', event.userId) + }) + + engine.on('onUserLeave', (event: { userId: string }) => { + console.log('User left:', event.userId) + }) + + engine.on('onError', (error: Error) => { + console.error('RTC error:', error) + connectionState.value = 'failed' + }) + + engine.on('onRoomStateChanged', (event: { state: number; errorCode: number }) => { + console.log('Room state changed:', event) + }) + + // 5. 加入房间 + console.log('Joining room:', params.roomId) + await engine.joinRoom( + params.token, + params.roomId, + { + userId: params.userId, + }, + { + isAutoPublish: true, + isAutoSubscribeAudio: true, + isAutoSubscribeVideo: false, + } + ) + + // 6. 开始音频采集 + console.log('Starting audio capture...') + await engine.startAudioCapture() + + // 7. 发送 session.update 事件,传递 session_id 给工作流 + if (params.sessionId) { + console.log('Sending session.update event with session_id:', params.sessionId) + await sendSessionUpdate(engine, params.sessionId) + } + + connectionState.value = 'connected' + console.log('RTC connected successfully') + } catch (error: any) { + console.error('RTC connect error:', error) + connectionState.value = 'failed' + throw error + } + } + + /** + * 断开 RTC 连接 + */ + async function disconnect() { + try { + if (engine) { + try { + await engine.stopAudioCapture() + } catch (e) { + console.warn('Stop audio capture failed:', e) + } + await engine.leaveRoom() + engine.destroy() + engine = null + } + + // 清理 TTS 音频上下文 + if (audioContext) { + await audioContext.close() + audioContext = null + ttsDestination = null + } + + connectionState.value = 'disconnected' + isReceiveOnly.value = false + isTTSSending.value = false + console.log('RTC disconnected') + } catch (error) { + console.error('RTC disconnect error:', error) + } + } + + /** + * 切换静音状态 + */ + function toggleMute(mute: boolean) { + if (engine) { + if (mute) { + engine.muteLocalAudio() + } else { + engine.unmuteLocalAudio() + } + isMuted.value = mute + } + } + + return { + connectionState, + isMuted, + isReceiveOnly, + isTTSSending, + connect, + connectTTSMode, + disconnect, + toggleMute, + sendTTSMessage, + } +} + +/** + * 发送 session.update 事件,将 session_id 传递给 Coze 工作流 + * + * 在 Coze RTC 语音模式下,需要通过此事件传递自定义参数 + * 工作流可以通过入参获取 session_id + * + * 根据 Coze 文档,事件需要通过信令通道发送 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function sendSessionUpdate(engine: any, sessionId: string): Promise { + try { + // 构造 session.update 事件(Coze 标准格式) + const eventData = { + id: `event_${Date.now()}`, + event_type: 'session.update', + data: { + chat_config: { + parameters: { + session_id: sessionId, + }, + }, + }, + } + + const eventJson = JSON.stringify(eventData) + console.log('Sending session.update event:', eventData) + + // 尝试多种方式发送事件到 Coze + + // 方法1: sendStreamSyncInfo (火山引擎 RTC 流同步信息) + if (engine.sendStreamSyncInfo) { + try { + const encoder = new TextEncoder() + const data = encoder.encode(eventJson) + await engine.sendStreamSyncInfo(data, { repeatCount: 3 }) + console.log('session.update sent via sendStreamSyncInfo') + return + } catch (e) { + console.warn('sendStreamSyncInfo failed:', e) + } + } + + // 方法2: sendSEIMessage (Supplemental Enhancement Information) + if (engine.sendSEIMessage) { + try { + const encoder = new TextEncoder() + const data = encoder.encode(eventJson) + // streamIndex: 0 表示主流,1 表示屏幕流 + await engine.sendSEIMessage(0, data, { repeatCount: 3 }) + console.log('session.update sent via sendSEIMessage') + return + } catch (e) { + console.warn('sendSEIMessage failed:', e) + } + } + + // 方法3: sendPublicStreamSEIMessage + if (engine.sendPublicStreamSEIMessage) { + try { + const encoder = new TextEncoder() + const data = encoder.encode(eventJson) + await engine.sendPublicStreamSEIMessage(data) + console.log('session.update sent via sendPublicStreamSEIMessage') + return + } catch (e) { + console.warn('sendPublicStreamSEIMessage failed:', e) + } + } + + // 方法4: sendRoomMessage (房间消息) + if (engine.sendRoomMessage) { + try { + await engine.sendRoomMessage(eventJson) + console.log('session.update sent via sendRoomMessage') + return + } catch (e) { + console.warn('sendRoomMessage failed:', e) + } + } + + // 方法5: sendUserMessage (用户消息) + if (engine.sendUserMessage) { + try { + // 发送给 AI Bot(Coze Bot 在房间中的用户 ID 通常以 bot_ 或 uid_ 开头) + await engine.sendUserMessage('', eventJson) // 空字符串表示广播 + console.log('session.update sent via sendUserMessage') + return + } catch (e) { + console.warn('sendUserMessage failed:', e) + } + } + + // 方法6: 尝试直接调用 engine 上的其他方法 + console.log('Available engine methods:', Object.keys(engine).filter(k => typeof engine[k] === 'function')) + + console.warn('No suitable method found to send session.update event. Coze may not receive the parameters.') + } catch (error) { + console.error('Failed to send session.update:', error) + } +} + +/** + * 请求麦克风权限 + */ +async function requestMicrophonePermission(): Promise { + try { + if (navigator.permissions) { + const result = await navigator.permissions.query({ name: 'microphone' as PermissionName }) + console.log('Microphone permission status:', result.state) + + if (result.state === 'denied') { + throw new Error('麦克风权限被拒绝,请在浏览器设置中允许访问麦克风') + } + } + + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + } + }) + + stream.getTracks().forEach(track => track.stop()) + + console.log('Microphone permission granted') + return true + } catch (error: any) { + console.error('Microphone permission error:', error) + + if (error.name === 'NotAllowedError') { + throw new Error('请允许使用麦克风以进行语音面试。如果您已拒绝,请在浏览器地址栏左侧的锁图标中重新授权。') + } else if (error.name === 'NotFoundError') { + throw new Error('未检测到麦克风设备,请确保您的设备已连接麦克风。') + } else { + throw new Error(`麦克风权限获取失败: ${error.message}`) + } + } +} diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..8b0d254 --- /dev/null +++ b/frontend/src/layouts/AdminLayout.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/src/layouts/InterviewLayout.vue b/frontend/src/layouts/InterviewLayout.vue new file mode 100644 index 0000000..14b9dde --- /dev/null +++ b/frontend/src/layouts/InterviewLayout.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..5dcdc66 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,22 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import 'element-plus/dist/index.css' +import './styles/index.css' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +// 注册 Element Plus 图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) + +app.mount('#app') diff --git a/frontend/src/pages/admin/[id].vue b/frontend/src/pages/admin/[id].vue new file mode 100644 index 0000000..003f3f2 --- /dev/null +++ b/frontend/src/pages/admin/[id].vue @@ -0,0 +1,192 @@ + + + + + diff --git a/frontend/src/pages/admin/configs.vue b/frontend/src/pages/admin/configs.vue new file mode 100644 index 0000000..f9f6cef --- /dev/null +++ b/frontend/src/pages/admin/configs.vue @@ -0,0 +1,182 @@ + + + + + + + diff --git a/frontend/src/pages/admin/dashboard.vue b/frontend/src/pages/admin/dashboard.vue new file mode 100644 index 0000000..30e1f48 --- /dev/null +++ b/frontend/src/pages/admin/dashboard.vue @@ -0,0 +1,863 @@ + + + + + diff --git a/frontend/src/pages/admin/index.vue b/frontend/src/pages/admin/index.vue new file mode 100644 index 0000000..c0aa5fc --- /dev/null +++ b/frontend/src/pages/admin/index.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/frontend/src/pages/admin/interview-detail.vue b/frontend/src/pages/admin/interview-detail.vue new file mode 100644 index 0000000..8acd736 --- /dev/null +++ b/frontend/src/pages/admin/interview-detail.vue @@ -0,0 +1,1133 @@ + + + + + diff --git a/frontend/src/pages/admin/interviews.vue b/frontend/src/pages/admin/interviews.vue new file mode 100644 index 0000000..5d4487c --- /dev/null +++ b/frontend/src/pages/admin/interviews.vue @@ -0,0 +1,809 @@ + + + + + diff --git a/frontend/src/pages/admin/layout.vue b/frontend/src/pages/admin/layout.vue new file mode 100644 index 0000000..7d65507 --- /dev/null +++ b/frontend/src/pages/admin/layout.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/frontend/src/pages/admin/login.vue b/frontend/src/pages/admin/login.vue new file mode 100644 index 0000000..fd1aa0d --- /dev/null +++ b/frontend/src/pages/admin/login.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/frontend/src/pages/interview/call.vue b/frontend/src/pages/interview/call.vue new file mode 100644 index 0000000..593424e --- /dev/null +++ b/frontend/src/pages/interview/call.vue @@ -0,0 +1,1134 @@ + + + + + diff --git a/frontend/src/pages/interview/complete.vue b/frontend/src/pages/interview/complete.vue new file mode 100644 index 0000000..0071c98 --- /dev/null +++ b/frontend/src/pages/interview/complete.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/pages/interview/index.vue b/frontend/src/pages/interview/index.vue new file mode 100644 index 0000000..ea5772f --- /dev/null +++ b/frontend/src/pages/interview/index.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/frontend/src/pages/interview/info.vue b/frontend/src/pages/interview/info.vue new file mode 100644 index 0000000..a458ac4 --- /dev/null +++ b/frontend/src/pages/interview/info.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..df71748 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,90 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' + +const routes: RouteRecordRaw[] = [ + // 用户端路由 + { + path: '/', + name: 'Interview', + component: () => import('@/layouts/InterviewLayout.vue'), + children: [ + { + path: '', + name: 'Welcome', + component: () => import('@/pages/interview/index.vue'), + meta: { title: '欢迎' }, + }, + { + path: 'info', + name: 'InfoCollection', + component: () => import('@/pages/interview/info.vue'), + meta: { title: '信息填写' }, + }, + { + path: 'call', + name: 'Call', + component: () => import('@/pages/interview/call.vue'), + meta: { title: '语音面试' }, + }, + { + path: 'complete', + name: 'Complete', + component: () => import('@/pages/interview/complete.vue'), + meta: { title: '面试完成' }, + }, + ], + }, + // 管理后台路由 + { + path: '/admin/login', + name: 'AdminLogin', + component: () => import('@/pages/admin/login.vue'), + meta: { title: '管理员登录' }, + }, + { + path: '/admin', + name: 'Admin', + component: () => import('@/pages/admin/layout.vue'), + redirect: '/admin/dashboard', + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/pages/admin/dashboard.vue'), + meta: { title: '数据概览' }, + }, + { + path: 'interviews', + name: 'InterviewList', + component: () => import('@/pages/admin/interviews.vue'), + meta: { title: '面试列表' }, + }, + { + path: 'interviews/:id', + name: 'InterviewDetail', + component: () => import('@/pages/admin/interview-detail.vue'), + meta: { title: '面试详情' }, + }, + { + path: 'configs', + name: 'Configs', + component: () => import('@/pages/admin/configs.vue'), + meta: { title: '配置管理' }, + }, + ], + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// 路由守卫 - 设置页面标题 +router.beforeEach((to, _from, next) => { + const title = to.meta.title as string + document.title = title ? `${title} - AI面试助手` : 'AI面试助手' + next() +}) + +export default router diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css new file mode 100644 index 0000000..27ea881 --- /dev/null +++ b/frontend/src/styles/index.css @@ -0,0 +1,109 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* 全局样式 */ +:root { + --primary-color: #3b82f6; + --success-color: #22c55e; + --danger-color: #ef4444; + --warning-color: #f59e0b; + --text-color: #1f2937; + --text-secondary: #6b7280; + --bg-color: #f9fafb; + --border-color: #e5e7eb; +} + +body { + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: var(--bg-color); + color: var(--text-color); +} + +/* 来电动画 */ +@keyframes ring { + 0%, 100% { + transform: rotate(-15deg); + } + 50% { + transform: rotate(15deg); + } +} + +.animate-ring { + animation: ring 0.5s ease-in-out infinite; +} + +/* 音波动画 */ +.audio-wave { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.audio-wave span { + width: 4px; + height: 16px; + background-color: var(--primary-color); + border-radius: 2px; + animation: wave 1s ease-in-out infinite; +} + +.audio-wave span:nth-child(2) { + animation-delay: 0.1s; +} + +.audio-wave span:nth-child(3) { + animation-delay: 0.2s; +} + +.audio-wave span:nth-child(4) { + animation-delay: 0.3s; +} + +.audio-wave span:nth-child(5) { + animation-delay: 0.4s; +} + +@keyframes wave { + 0%, 100% { + transform: scaleY(1); + } + 50% { + transform: scaleY(2); + } +} + +/* 脉冲动画(来电) */ +.pulse-ring { + position: relative; +} + +.pulse-ring::before, +.pulse-ring::after { + content: ''; + position: absolute; + inset: -20px; + border: 3px solid var(--success-color); + border-radius: 50%; + animation: pulse-expand 1.5s ease-out infinite; +} + +.pulse-ring::after { + animation-delay: 0.5s; +} + +@keyframes pulse-expand { + 0% { + transform: scale(0.8); + opacity: 1; + } + 100% { + transform: scale(1.5); + opacity: 0; + } +} diff --git a/frontend/src/types/env.d.ts b/frontend/src/types/env.d.ts new file mode 100644 index 0000000..0781165 --- /dev/null +++ b/frontend/src/types/env.d.ts @@ -0,0 +1,16 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string + readonly VITE_RTC_APP_ID: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..b86e1fe --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,45 @@ +// 面试状态 +export type InterviewStatus = 'pending' | 'ongoing' | 'completed' + +// 面试阶段 +export enum InterviewStage { + InfoCollection = 10, + SalesSkill = 20, + SalesMindset = 30, + Quality = 40, + Motivation = 50, + Completed = 60, +} + +// 面试阶段名称 +export const InterviewStageLabel: Record = { + [InterviewStage.InfoCollection]: '信息收集', + [InterviewStage.SalesSkill]: '销售技能', + [InterviewStage.SalesMindset]: '销售观', + [InterviewStage.Quality]: '素质项', + [InterviewStage.Motivation]: '求职动机', + [InterviewStage.Completed]: '面试完成', +} + +// 评分维度 +export interface Scores { + salesSkill: number + salesMindset: number + quality: number + motivation: number + total: number +} + +// 评分维度名称 +export const ScoreLabels: Record, string> = { + salesSkill: '销售技能', + salesMindset: '销售观', + quality: '素质项', + motivation: '求职动机', +} + +// 用户端面试流程状态 +export type InterviewFlowStep = 'welcome' | 'info' | 'incoming' | 'incall' | 'complete' + +// RTC 连接状态 +export type RTCConnectionState = 'disconnected' | 'connecting' | 'connected' | 'failed' diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..ccb0394 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,40 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './index.html', + './src/**/*.{vue,js,ts,jsx,tsx}', + ], + theme: { + extend: { + colors: { + primary: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + }, + }, + animation: { + 'pulse-ring': 'pulse-ring 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite', + 'wave': 'wave 1s ease-in-out infinite', + }, + keyframes: { + 'pulse-ring': { + '0%, 100%': { transform: 'scale(1)', opacity: '1' }, + '50%': { transform: 'scale(1.1)', opacity: '0.7' }, + }, + 'wave': { + '0%, 100%': { transform: 'scaleY(1)' }, + '50%': { transform: 'scaleY(1.5)' }, + }, + }, + }, + }, + plugins: [], +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..8195311 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path alias */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/tsconfig.node.tsbuildinfo b/frontend/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..b7efcb1 --- /dev/null +++ b/frontend/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/compatibility/index.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/globals.typedarray.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/buffer.buffer.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/globals.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/web-globals/events.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/assert.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/assert/strict.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/async_hooks.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/buffer.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/child_process.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/cluster.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/console.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/constants.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/crypto.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/dgram.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/dns.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/dns/promises.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/domain.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/events.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/fs.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/fs/promises.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/http.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/http2.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/https.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/inspector.generated.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/module.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/net.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/os.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/path.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/perf_hooks.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/process.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/punycode.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/querystring.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/readline.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/readline/promises.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/repl.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/sea.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/stream.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/stream/promises.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/stream/consumers.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/stream/web.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/string_decoder.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/test.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/timers.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/timers/promises.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/tls.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/trace_events.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/tty.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/url.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/util.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/v8.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/vm.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/wasi.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/worker_threads.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/zlib.d.ts","./node_modules/.pnpm/@types+node@20.19.30/node_modules/@types/node/index.d.ts","./node_modules/.pnpm/@types+estree@1.0.8/node_modules/@types/estree/index.d.ts","./node_modules/.pnpm/rollup@4.55.2/node_modules/rollup/dist/rollup.d.ts","./node_modules/.pnpm/rollup@4.55.2/node_modules/rollup/dist/parseast.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/types/hmrpayload.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/types/customevent.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/types/hot.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild/lib/main.d.ts","./node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/previous-map.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/input.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/declaration.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/root.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/warning.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/lazy-result.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/no-work-result.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/processor.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/result.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/document.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/rule.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/node.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/comment.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/container.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/at-rule.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/list.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/postcss.d.ts","./node_modules/.pnpm/postcss@8.5.6/node_modules/postcss/lib/postcss.d.mts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/dist/node/runtime.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/types/importglob.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/types/metadata.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@20.19.30/node_modules/vite/dist/node/index.d.ts","./node_modules/.pnpm/@babel+types@7.28.6/node_modules/@babel/types/lib/index.d.ts","./node_modules/.pnpm/@vue+shared@3.5.27/node_modules/@vue/shared/dist/shared.d.ts","./node_modules/.pnpm/@babel+parser@7.28.6/node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/.pnpm/@vue+compiler-core@3.5.27/node_modules/@vue/compiler-core/dist/compiler-core.d.ts","./node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.d.mts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.d.ts","./node_modules/.pnpm/@vue+compiler-sfc@3.5.27/node_modules/@vue/compiler-sfc/dist/compiler-sfc.d.ts","./node_modules/.pnpm/vue@3.5.27_typescript@5.9.3/node_modules/vue/compiler-sfc/index.d.mts","./node_modules/.pnpm/@vitejs+plugin-vue@5.2.4_vite@5.4.21_@types+node@20.19.30__vue@3.5.27_typescript@5.9.3_/node_modules/@vitejs/plugin-vue/dist/index.d.mts","./vite.config.ts"],"fileIdsList":[[55,101,182],[55,101],[55,98,101],[55,100,101],[101],[55,101,106,134],[55,101,102,107,112,120,131,142],[55,101,102,103,112,120],[50,51,52,55,101],[55,101,104,143],[55,101,105,106,113,121],[55,101,106,131,139],[55,101,107,109,112,120],[55,100,101,108],[55,101,109,110],[55,101,111,112],[55,100,101,112],[55,101,112,113,114,131,142],[55,101,112,113,114,127,131,134],[55,101,109,112,115,120,131,142],[55,101,112,113,115,116,120,131,139,142],[55,101,115,117,131,139,142],[53,54,55,56,57,58,59,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148],[55,101,112,118],[55,101,119,142,147],[55,101,109,112,120,131],[55,101,121],[55,101,122],[55,100,101,123],[55,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148],[55,101,125],[55,101,126],[55,101,112,127,128],[55,101,127,129,143,145],[55,101,112,131,132,134],[55,101,133,134],[55,101,131,132],[55,101,134],[55,101,135],[55,98,101,131,136],[55,101,112,137,138],[55,101,137,138],[55,101,106,120,131,139],[55,101,140],[55,101,120,141],[55,101,115,126,142],[55,101,106,143],[55,101,131,144],[55,101,119,145],[55,101,146],[55,96,101],[55,96,101,112,114,123,131,134,142,145,147],[55,101,131,148],[55,101,181,189],[55,101,182,183,184],[55,101,177,182,184,185,186,187],[55,101,173],[55,101,171,173],[55,101,162,170,171,172,174,176],[55,101,160],[55,101,163,168,173,176],[55,101,159,176],[55,101,163,164,167,168,169,176],[55,101,163,164,165,167,168,176],[55,101,160,161,162,163,164,168,169,170,172,173,174,176],[55,101,176],[55,101,158,160,161,162,163,164,165,167,168,169,170,171,172,173,174,175],[55,101,158,176],[55,101,163,165,166,168,169,176],[55,101,167,176],[55,101,168,169,173,176],[55,101,161,171],[55,101,151,180],[55,101,150,151],[55,68,72,101,142],[55,68,101,131,142],[55,63,101],[55,65,68,101,139,142],[55,101,120,139],[55,101,149],[55,63,101,149],[55,65,68,101,120,142],[55,60,61,64,67,101,112,131,142],[55,68,75,101],[55,60,66,101],[55,68,89,90,101],[55,64,68,101,134,142,149],[55,89,101,149],[55,62,63,101,149],[55,68,101],[55,62,63,64,65,66,67,68,69,70,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,90,91,92,93,94,95,101],[55,68,83,101],[55,68,75,76,101],[55,66,68,76,77,101],[55,67,101],[55,60,63,68,101],[55,68,72,76,77,101],[55,72,101],[55,66,68,71,101,142],[55,60,65,68,75,101],[55,101,131],[55,63,68,89,101,147,149],[55,101,112,113,115,116,117,120,131,139,142,148,149,151,152,153,154,155,156,157,177,178,179,180],[55,101,153,154,155,156],[55,101,153,154,155],[55,101,153],[55,101,154],[55,101,151],[55,101,188],[55,101,122,181,190]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"ba481bca06f37d3f2c137ce343c7d5937029b2468f8e26111f3c9d9963d6568d","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ef24f9a22a88e3e9b3b3d8c40ab1ddb0853f1bfbd5c843c37800138437b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"2cbe0621042e2a68c7cbce5dfed3906a1862a16a7d496010636cdbdb91341c0f","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2677634fe27e87348825bb041651e22d50a613e2fdf6a4a3ade971d71bac37e","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"8c0bcd6c6b67b4b503c11e91a1fb91522ed585900eab2ab1f61bba7d7caa9d6f","impliedFormat":1},{"version":"8cd19276b6590b3ebbeeb030ac271871b9ed0afc3074ac88a94ed2449174b776","affectsGlobalScope":true,"impliedFormat":1},{"version":"696eb8d28f5949b87d894b26dc97318ef944c794a9a4e4f62360cd1d1958014b","impliedFormat":1},{"version":"3f8fa3061bd7402970b399300880d55257953ee6d3cd408722cb9ac20126460c","impliedFormat":1},{"version":"35ec8b6760fd7138bbf5809b84551e31028fb2ba7b6dc91d95d098bf212ca8b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"68bd56c92c2bd7d2339457eb84d63e7de3bd56a69b25f3576e1568d21a162398","affectsGlobalScope":true,"impliedFormat":1},{"version":"3e93b123f7c2944969d291b35fed2af79a6e9e27fdd5faa99748a51c07c02d28","impliedFormat":1},{"version":"9d19808c8c291a9010a6c788e8532a2da70f811adb431c97520803e0ec649991","impliedFormat":1},{"version":"87aad3dd9752067dc875cfaa466fc44246451c0c560b820796bdd528e29bef40","impliedFormat":1},{"version":"4aacb0dd020eeaef65426153686cc639a78ec2885dc72ad220be1d25f1a439df","impliedFormat":1},{"version":"f0bd7e6d931657b59605c44112eaf8b980ba7f957a5051ed21cb93d978cf2f45","impliedFormat":1},{"version":"8db0ae9cb14d9955b14c214f34dae1b9ef2baee2fe4ce794a4cd3ac2531e3255","affectsGlobalScope":true,"impliedFormat":1},{"version":"15fc6f7512c86810273af28f224251a5a879e4261b4d4c7e532abfbfc3983134","impliedFormat":1},{"version":"58adba1a8ab2d10b54dc1dced4e41f4e7c9772cbbac40939c0dc8ce2cdb1d442","impliedFormat":1},{"version":"2fd4c143eff88dabb57701e6a40e02a4dbc36d5eb1362e7964d32028056a782b","impliedFormat":1},{"version":"714435130b9015fae551788df2a88038471a5a11eb471f27c4ede86552842bc9","impliedFormat":1},{"version":"855cd5f7eb396f5f1ab1bc0f8580339bff77b68a770f84c6b254e319bbfd1ac7","impliedFormat":1},{"version":"5650cf3dace09e7c25d384e3e6b818b938f68f4e8de96f52d9c5a1b3db068e86","impliedFormat":1},{"version":"1354ca5c38bd3fd3836a68e0f7c9f91f172582ba30ab15bb8c075891b91502b7","affectsGlobalScope":true,"impliedFormat":1},{"version":"27fdb0da0daf3b337c5530c5f266efe046a6ceb606e395b346974e4360c36419","impliedFormat":1},{"version":"2d2fcaab481b31a5882065c7951255703ddbe1c0e507af56ea42d79ac3911201","impliedFormat":1},{"version":"a192fe8ec33f75edbc8d8f3ed79f768dfae11ff5735e7fe52bfa69956e46d78d","impliedFormat":1},{"version":"ca867399f7db82df981d6915bcbb2d81131d7d1ef683bc782b59f71dda59bc85","affectsGlobalScope":true,"impliedFormat":1},{"version":"0e456fd5b101271183d99a9087875a282323e3a3ff0d7bcf1881537eaa8b8e63","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e043a1bc8fbf2a255bccf9bf27e0f1caf916c3b0518ea34aa72357c0afd42ec","impliedFormat":1},{"version":"b4f70ec656a11d570e1a9edce07d118cd58d9760239e2ece99306ee9dfe61d02","impliedFormat":1},{"version":"3bc2f1e2c95c04048212c569ed38e338873f6a8593930cf5a7ef24ffb38fc3b6","impliedFormat":1},{"version":"6e70e9570e98aae2b825b533aa6292b6abd542e8d9f6e9475e88e1d7ba17c866","impliedFormat":1},{"version":"f9d9d753d430ed050dc1bf2667a1bab711ccbb1c1507183d794cc195a5b085cc","impliedFormat":1},{"version":"9eece5e586312581ccd106d4853e861aaaa1a39f8e3ea672b8c3847eedd12f6e","impliedFormat":1},{"version":"47ab634529c5955b6ad793474ae188fce3e6163e3a3fb5edd7e0e48f14435333","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"0225ecb9ed86bdb7a2c7fd01f1556906902929377b44483dc4b83e03b3ef227d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74cf591a0f63db318651e0e04cb55f8791385f86e987a67fd4d2eaab8191f730","impliedFormat":1},{"version":"5eab9b3dc9b34f185417342436ec3f106898da5f4801992d8ff38ab3aff346b5","impliedFormat":1},{"version":"12ed4559eba17cd977aa0db658d25c4047067444b51acfdcbf38470630642b23","affectsGlobalScope":true,"impliedFormat":1},{"version":"f3ffabc95802521e1e4bcba4c88d8615176dc6e09111d920c7a213bdda6e1d65","impliedFormat":1},{"version":"ddc734b4fae82a01d247e9e342d020976640b5e93b4e9b3a1e30e5518883a060","impliedFormat":1},{"version":"ae56f65caf3be91108707bd8dfbccc2a57a91feb5daabf7165a06a945545ed26","impliedFormat":1},{"version":"a136d5de521da20f31631a0a96bf712370779d1c05b7015d7019a9b2a0446ca9","impliedFormat":1},{"version":"c3b41e74b9a84b88b1dca61ec39eee25c0dbc8e7d519ba11bb070918cfacf656","affectsGlobalScope":true,"impliedFormat":1},{"version":"4737a9dc24d0e68b734e6cfbcea0c15a2cfafeb493485e27905f7856988c6b29","affectsGlobalScope":true,"impliedFormat":1},{"version":"36d8d3e7506b631c9582c251a2c0b8a28855af3f76719b12b534c6edf952748d","impliedFormat":1},{"version":"1ca69210cc42729e7ca97d3a9ad48f2e9cb0042bada4075b588ae5387debd318","impliedFormat":1},{"version":"f5ebe66baaf7c552cfa59d75f2bfba679f329204847db3cec385acda245e574e","impliedFormat":1},{"version":"ed59add13139f84da271cafd32e2171876b0a0af2f798d0c663e8eeb867732cf","affectsGlobalScope":true,"impliedFormat":1},{"version":"05db535df8bdc30d9116fe754a3473d1b6479afbc14ae8eb18b605c62677d518","impliedFormat":1},{"version":"b1810689b76fd473bd12cc9ee219f8e62f54a7d08019a235d07424afbf074d25","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"4e741b9c88e80c9e4cedf07b5a698e8e3a3bd73cf649f664d6dd3f868c05c2f3","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87","impliedFormat":99},{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"333caa2bfff7f06017f114de738050dd99a765c7eb16571c6d25a38c0d5365dc","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1251d53755b03cde02466064260bb88fd83c30006a46395b7d9167340bc59b73","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"4cdf27e29feae6c7826cdd5c91751cc35559125e8304f9e7aed8faef97dcf572","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","impliedFormat":99},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"257b83faa134d971c738a6b9e4c47e59bb7b23274719d92197580dd662bfafc3","impliedFormat":99},{"version":"511a5f4f77165dc1b73ceae1e28b4a8f78f3443d8e18a1fd43bfafd2b0133bbe","impliedFormat":1},{"version":"f468b74459f1ad4473b36a36d49f2b255f3c6b5d536c81239c2b2971df089eaf","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"e63d565526fcb1a4cdd35e4c3d6dedc0a967933623bf2316ddab29ad2f62203a","impliedFormat":1},{"version":"2be2227c3810dfd84e46674fd33b8d09a4a28ad9cb633ed536effd411665ea1e","impliedFormat":99},{"version":"e134052a6b1ded61693b4037f615dc72f14e2881e79c1ddbff6c514c8a516b05","impliedFormat":1},{"version":"957a44f864ab3c182edc747428e8eec1765257deee7fac86c1147eeac897d832","impliedFormat":1},{"version":"3feec212c0aeb91e5a6e62caaf9f128954590210f8c302910ea377c088f6b61a","impliedFormat":99},{"version":"bbdfaf7d9b20534c5df1e1b937a20f17ca049d603a2afe072983bf7aff2279f5","impliedFormat":99},{"version":"8b3f2b9013d5452934aa5fa58702ad3bc3bb86a296dc0155c784beb63d0f473e","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"}],"root":[191],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true,"strict":true},"referencedMap":[[184,1],[182,2],[150,2],[98,3],[99,3],[100,4],[55,5],[101,6],[102,7],[103,8],[50,2],[53,9],[51,2],[52,2],[104,10],[105,11],[106,12],[107,13],[108,14],[109,15],[110,15],[111,16],[112,17],[113,18],[114,19],[56,2],[54,2],[115,20],[116,21],[117,22],[149,23],[118,24],[119,25],[120,26],[121,27],[122,28],[123,29],[124,30],[125,31],[126,32],[127,33],[128,33],[129,34],[130,2],[131,35],[133,36],[132,37],[134,38],[135,39],[136,40],[137,41],[138,42],[139,43],[140,44],[141,45],[142,46],[143,47],[144,48],[145,49],[146,50],[57,2],[58,2],[59,2],[97,51],[147,52],[148,53],[190,54],[185,55],[188,56],[183,2],[157,2],[186,2],[174,57],[172,58],[173,59],[161,60],[162,58],[169,61],[160,62],[165,63],[175,2],[166,64],[171,65],[177,66],[176,67],[159,68],[167,69],[168,70],[163,71],[170,57],[164,72],[152,73],[151,74],[158,2],[1,2],[48,2],[49,2],[9,2],[13,2],[12,2],[3,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[4,2],[22,2],[23,2],[5,2],[24,2],[28,2],[25,2],[26,2],[27,2],[29,2],[30,2],[31,2],[6,2],[32,2],[33,2],[34,2],[35,2],[7,2],[39,2],[36,2],[37,2],[38,2],[40,2],[8,2],[41,2],[46,2],[47,2],[42,2],[43,2],[44,2],[45,2],[2,2],[11,2],[10,2],[187,2],[75,75],[85,76],[74,75],[95,77],[66,78],[65,79],[94,80],[88,81],[93,82],[68,83],[82,84],[67,85],[91,86],[63,87],[62,80],[92,88],[64,89],[69,90],[70,2],[73,90],[60,2],[96,91],[86,92],[77,93],[78,94],[80,95],[76,96],[79,97],[89,80],[71,98],[72,99],[81,100],[61,101],[84,92],[83,90],[87,2],[90,102],[181,103],[178,104],[156,105],[154,106],[153,2],[155,107],[179,2],[180,108],[189,109],[191,110]],"latestChangedDtsFile":"./vite.config.d.ts","version":"5.9.3"} \ No newline at end of file diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo new file mode 100644 index 0000000..c023965 --- /dev/null +++ b/frontend/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/main.ts","./src/api/candidate.ts","./src/api/index.ts","./src/api/request.ts","./src/composables/index.ts","./src/composables/usecozerealtime.ts","./src/composables/usertc.ts","./src/router/index.ts","./src/types/env.d.ts","./src/types/index.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/layouts/interviewlayout.vue","./src/pages/admin/[id].vue","./src/pages/admin/configs.vue","./src/pages/admin/dashboard.vue","./src/pages/admin/index.vue","./src/pages/admin/interview-detail.vue","./src/pages/admin/interviews.vue","./src/pages/admin/layout.vue","./src/pages/admin/login.vue","./src/pages/interview/call.vue","./src/pages/interview/complete.vue","./src/pages/interview/index.vue","./src/pages/interview/info.vue"],"version":"5.9.3"} \ No newline at end of file diff --git a/frontend/vite.config.d.ts b/frontend/vite.config.d.ts new file mode 100644 index 0000000..340562a --- /dev/null +++ b/frontend/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfig; +export default _default; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..2a35013 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { resolve } from 'path'; +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, + build: { + target: ['chrome90', 'edge90', 'firefox90', 'safari14'], + }, +}); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..c25aac3 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, + build: { + target: ['chrome90', 'edge90', 'firefox90', 'safari14'], + }, +}) diff --git a/n8n-workflows/README.md b/n8n-workflows/README.md new file mode 100644 index 0000000..bc9571f --- /dev/null +++ b/n8n-workflows/README.md @@ -0,0 +1,29 @@ +# 工作流清单 + +> 本项目 n8n 工作流索引 + +--- + +## 工作流列表 + +| 文件 | Webhook | 方法 | 说明 | +|------|---------|------|------| +| - | - | - | 暂无工作流 | + +--- + +## 凭证配置 + +参考 `credentials.example.md`(待创建) + +--- + +## 部署说明 + +1. 导入工作流 JSON +2. 配置凭证 +3. 激活工作流 + +--- + +> 最后更新:2026-01-20 diff --git a/start-tunnel.sh b/start-tunnel.sh new file mode 100755 index 0000000..c630609 --- /dev/null +++ b/start-tunnel.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# 启动 Cloudflare Tunnel(替代 ngrok,无警告页面) +# +# 使用方法: +# ./start-tunnel.sh +# +# 会输出一个 URL,类似: +# https://xxx-xxx-xxx.trycloudflare.com +# +# 把这个 URL 复制到 .env 文件的 TUNNEL_URL 配置中 + +echo "🚀 启动 Cloudflare Tunnel..." +echo "" +echo "等待隧道建立,会显示一个 trycloudflare.com 的 URL" +echo "请复制该 URL 到 .env 文件的 TUNNEL_URL 配置中" +echo "" +echo "按 Ctrl+C 停止隧道" +echo "" + +# 检查 cloudflared 是否安装 +if ! command -v cloudflared &> /dev/null; then + echo "❌ cloudflared 未安装" + echo "" + echo "请先安装 cloudflared:" + echo " macOS: brew install cloudflared" + echo " Linux: curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared && chmod +x cloudflared && sudo mv cloudflared /usr/local/bin/" + echo " Windows: 从 https://github.com/cloudflare/cloudflared/releases 下载" + exit 1 +fi + +# 启动隧道 +cloudflared tunnel --url http://localhost:8000 diff --git a/知识库/FireShot Capture 005 - 集成音视频 Realtime Web SDK - 文档 - 扣子 - [www.coze.cn].pdf b/知识库/FireShot Capture 005 - 集成音视频 Realtime Web SDK - 文档 - 扣子 - [www.coze.cn].pdf new file mode 100644 index 0000000..58f0f51 Binary files /dev/null and b/知识库/FireShot Capture 005 - 集成音视频 Realtime Web SDK - 文档 - 扣子 - [www.coze.cn].pdf differ diff --git a/知识库/Prompt版本库.md b/知识库/Prompt版本库.md new file mode 100644 index 0000000..8586599 --- /dev/null +++ b/知识库/Prompt版本库.md @@ -0,0 +1,39 @@ +# Prompt 版本库 + +> 记录本项目使用的 Prompt 及其优化历史 + +--- + +## Prompt 清单 + +| 编号 | 名称 | 用途 | 当前版本 | 状态 | +|------|------|------|---------|------| +| - | - | 暂无 | - | - | + +--- + +## Prompt 详细记录 + + + +