diff --git a/.DS_Store b/.DS_Store index 7ba8859..d10040d 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/api/endpoints/external_apps.py b/app/api/endpoints/external_apps.py new file mode 100644 index 0000000..fb94e8d --- /dev/null +++ b/app/api/endpoints/external_apps.py @@ -0,0 +1,518 @@ +from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException +from app.core.database import get_db_connection +from app.core.auth import get_current_admin_user +from app.core.response import create_api_response +from app.core.config import BASE_DIR, EXTERNAL_APPS_DIR, ALLOWED_IMAGE_EXTENSIONS, MAX_IMAGE_SIZE +from app.utils.apk_parser import parse_apk_with_androguard +from typing import Optional +from pathlib import Path +from pydantic import BaseModel +import os +import shutil +import json +import uuid +import hashlib + +router = APIRouter() + +# APK上传配置 +ALLOWED_APK_EXTENSIONS = {'.apk'} +MAX_APK_SIZE = 200 * 1024 * 1024 # 200MB + + +class CreateExternalAppRequest(BaseModel): + app_name: str + app_type: str # 'native' or 'web' + app_info: str # JSON string + icon_url: Optional[str] = None + description: Optional[str] = None + sort_order: int = 0 + is_active: bool = True + + +class UpdateExternalAppRequest(BaseModel): + app_name: Optional[str] = None + app_type: Optional[str] = None + app_info: Optional[str] = None + icon_url: Optional[str] = None + description: Optional[str] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + + +@router.get("/external-apps", response_model=dict) +async def get_external_apps( + app_type: Optional[str] = None, + is_active: Optional[bool] = None, + page: int = 1, + size: int = 50, + current_user: dict = Depends(get_current_admin_user) +): + """ + 获取外部应用列表(管理后台接口) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 构建查询条件 + where_clauses = [] + params = [] + + if app_type: + where_clauses.append("app_type = %s") + params.append(app_type) + + if is_active is not None: + where_clauses.append("is_active = %s") + params.append(is_active) + + where_clause = " AND ".join(where_clauses) if where_clauses else "1=1" + + # 获取总数 + count_query = f"SELECT COUNT(*) as total FROM external_apps WHERE {where_clause}" + cursor.execute(count_query, params) + total = cursor.fetchone()['total'] + + # 获取列表数据 + offset = (page - 1) * size + list_query = f""" + SELECT ea.*, u.username as creator_username + FROM external_apps ea + LEFT JOIN users u ON ea.created_by = u.user_id + WHERE {where_clause} + ORDER BY ea.sort_order ASC, ea.created_at DESC + LIMIT %s OFFSET %s + """ + cursor.execute(list_query, params + [size, offset]) + apps = cursor.fetchall() + + # 解析 app_info JSON + for app in apps: + if app.get('app_info'): + try: + app['app_info'] = json.loads(app['app_info']) + except: + app['app_info'] = {} + + cursor.close() + + return create_api_response( + code="200", + message="获取成功", + data={ + "apps": apps, + "total": total, + "page": page, + "size": size + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取外部应用列表失败: {str(e)}" + ) + + +@router.get("/external-apps/active", response_model=dict) +async def get_active_external_apps(): + """ + 获取所有启用的外部应用(公开接口,供客户端调用) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = """ + SELECT id, app_name, app_type, app_info, icon_url, description, sort_order + FROM external_apps + WHERE is_active = TRUE + ORDER BY sort_order ASC, created_at DESC + """ + cursor.execute(query) + apps = cursor.fetchall() + + # 解析 app_info JSON + for app in apps: + if app.get('app_info'): + try: + app['app_info'] = json.loads(app['app_info']) + except: + app['app_info'] = {} + + cursor.close() + + # 按类型分组 + native_apps = [app for app in apps if app['app_type'] == 'native'] + web_apps = [app for app in apps if app['app_type'] == 'web'] + + return create_api_response( + code="200", + message="获取成功", + data={ + "native": native_apps, + "web": web_apps, + "all": apps + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取外部应用失败: {str(e)}" + ) + + +@router.post("/external-apps", response_model=dict) +async def create_external_app( + request: CreateExternalAppRequest, + current_user: dict = Depends(get_current_admin_user) +): + """ + 创建外部应用 + """ + try: + # 验证 app_info 是否为有效JSON + try: + json.loads(request.app_info) + except: + return create_api_response( + code="400", + message="app_info 必须是有效的JSON格式" + ) + + with get_db_connection() as conn: + cursor = conn.cursor() + + query = """ + INSERT INTO external_apps + (app_name, app_type, app_info, icon_url, description, sort_order, is_active, created_by) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """ + cursor.execute(query, ( + request.app_name, + request.app_type, + request.app_info, + request.icon_url, + request.description, + request.sort_order, + request.is_active, + current_user['user_id'] + )) + + app_id = cursor.lastrowid + conn.commit() + cursor.close() + + return create_api_response( + code="200", + message="创建成功", + data={"id": app_id} + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"创建外部应用失败: {str(e)}" + ) + + +@router.put("/external-apps/{app_id}", response_model=dict) +async def update_external_app( + app_id: int, + request: UpdateExternalAppRequest, + current_user: dict = Depends(get_current_admin_user) +): + """ + 更新外部应用 + """ + try: + # 验证 app_info 是否为有效JSON + if request.app_info: + try: + json.loads(request.app_info) + except: + return create_api_response( + code="400", + message="app_info 必须是有效的JSON格式" + ) + + with get_db_connection() as conn: + cursor = conn.cursor() + + # 构建更新字段 + update_fields = [] + params = [] + + if request.app_name is not None: + update_fields.append("app_name = %s") + params.append(request.app_name) + + if request.app_type is not None: + update_fields.append("app_type = %s") + params.append(request.app_type) + + if request.app_info is not None: + update_fields.append("app_info = %s") + params.append(request.app_info) + + if request.icon_url is not None: + update_fields.append("icon_url = %s") + params.append(request.icon_url) + + if request.description is not None: + update_fields.append("description = %s") + params.append(request.description) + + if request.sort_order is not None: + update_fields.append("sort_order = %s") + params.append(request.sort_order) + + if request.is_active is not None: + update_fields.append("is_active = %s") + params.append(request.is_active) + + if not update_fields: + return create_api_response( + code="400", + message="没有需要更新的字段" + ) + + params.append(app_id) + query = f""" + UPDATE external_apps + SET {', '.join(update_fields)} + WHERE id = %s + """ + cursor.execute(query, params) + conn.commit() + cursor.close() + + return create_api_response( + code="200", + message="更新成功" + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"更新外部应用失败: {str(e)}" + ) + + +@router.delete("/external-apps/{app_id}", response_model=dict) +async def delete_external_app( + app_id: int, + current_user: dict = Depends(get_current_admin_user) +): + """ + 删除外部应用 + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 获取应用信息(用于删除APK文件) + cursor.execute("SELECT * FROM external_apps WHERE id = %s", (app_id,)) + app = cursor.fetchone() + + if not app: + cursor.close() + return create_api_response( + code="404", + message="应用不存在" + ) + + # 删除数据库记录 + cursor.execute("DELETE FROM external_apps WHERE id = %s", (app_id,)) + conn.commit() + + # 删除相关文件 + files_to_delete = [] + + # 如果是原生应用,添加APK文件到删除列表 + if app['app_type'] == 'native' and app.get('app_info'): + try: + app_info = json.loads(app['app_info']) if isinstance(app['app_info'], str) else app['app_info'] + apk_url = app_info.get('apk_url', '') + if apk_url and apk_url.startswith('/uploads/'): + apk_path = BASE_DIR / apk_url.lstrip('/') + files_to_delete.append(('APK', apk_path)) + except Exception as e: + print(f"Failed to parse app_info for APK deletion: {e}") + + # 添加图标文件到删除列表 + if app.get('icon_url'): + icon_url = app['icon_url'] + if icon_url and icon_url.startswith('/uploads/'): + icon_path = BASE_DIR / icon_url.lstrip('/') + files_to_delete.append(('Icon', icon_path)) + + # 执行文件删除 + for file_type, file_path in files_to_delete: + try: + if file_path.exists(): + os.remove(file_path) + print(f"Deleted {file_type} file: {file_path}") + except Exception as e: + print(f"Failed to delete {file_type} file: {e}") + + cursor.close() + + return create_api_response( + code="200", + message="删除成功" + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"删除外部应用失败: {str(e)}" + ) + + +@router.post("/external-apps/upload-apk", response_model=dict) +async def upload_apk( + apk_file: UploadFile = File(...), + current_user: dict = Depends(get_current_admin_user) +): + """ + 上传APK文件并解析信息 + """ + try: + # 验证文件类型 + file_extension = os.path.splitext(apk_file.filename)[1].lower() + if file_extension not in ALLOWED_APK_EXTENSIONS: + return create_api_response( + code="400", + message=f"不支持的文件类型。仅支持: {', '.join(ALLOWED_APK_EXTENSIONS)}" + ) + + # 验证文件大小 + apk_file.file.seek(0, 2) # 移动到文件末尾 + file_size = apk_file.file.tell() + apk_file.file.seek(0) # 重置到文件开头 + + if file_size > MAX_APK_SIZE: + return create_api_response( + code="400", + message=f"文件大小超过 {MAX_APK_SIZE // (1024 * 1024)}MB 限制" + ) + + # 使用原始文件名 + original_filename = apk_file.filename + file_path = EXTERNAL_APPS_DIR / original_filename + + # 如果同名文件已存在,先删除 + if file_path.exists(): + try: + os.remove(file_path) + print(f"删除已存在的同名文件: {file_path}") + except Exception as e: + print(f"删除同名文件失败: {e}") + + # 保存文件 + with open(file_path, "wb") as buffer: + shutil.copyfileobj(apk_file.file, buffer) + + # 计算MD5 + md5_hash = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + md5_hash.update(chunk) + apk_md5 = md5_hash.hexdigest() + + # 解析APK + try: + apk_info = parse_apk_with_androguard(str(file_path)) + print(f"APK解析成功: {apk_info}") + except Exception as e: + # 删除已上传的文件 + if file_path.exists(): + os.remove(file_path) + return create_api_response( + code="400", + message=f"APK解析失败: {str(e)}" + ) + + # 计算相对路径 + relative_path = file_path.relative_to(EXTERNAL_APPS_DIR.parent.parent) + + # 返回解析结果 + return create_api_response( + code="200", + message="APK上传并解析成功", + data={ + "apk_url": "/" + str(relative_path).replace("\\", "/"), + "apk_size": file_size, + "apk_md5": apk_md5, + "package_name": apk_info.get('package_name'), + "version_name": apk_info.get('version_name'), + "version_code": apk_info.get('version_code'), + "app_name": apk_info.get('app_name'), + "min_sdk_version": apk_info.get('min_sdk_version'), + "target_sdk_version": apk_info.get('target_sdk_version') + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"上传APK失败: {str(e)}" + ) + + +@router.post("/external-apps/upload-icon", response_model=dict) +async def upload_icon( + icon_file: UploadFile = File(...), + current_user: dict = Depends(get_current_admin_user) +): + """ + 上传应用图标 + """ + try: + # 验证文件类型 + file_extension = os.path.splitext(icon_file.filename)[1].lower() + if file_extension not in ALLOWED_IMAGE_EXTENSIONS: + return create_api_response( + code="400", + message=f"不支持的文件类型。仅支持: {', '.join(ALLOWED_IMAGE_EXTENSIONS)}" + ) + + # 验证文件大小 + icon_file.file.seek(0, 2) + file_size = icon_file.file.tell() + icon_file.file.seek(0) + + if file_size > MAX_IMAGE_SIZE: + return create_api_response( + code="400", + message=f"文件大小超过 {MAX_IMAGE_SIZE // (1024 * 1024)}MB 限制" + ) + + # 生成唯一文件名(图标使用UUID避免冲突) + unique_filename = f"icon_{uuid.uuid4()}{file_extension}" + file_path = EXTERNAL_APPS_DIR / unique_filename + + # 保存文件 + with open(file_path, "wb") as buffer: + shutil.copyfileobj(icon_file.file, buffer) + + # 计算相对路径 + relative_path = file_path.relative_to(EXTERNAL_APPS_DIR.parent.parent) + + return create_api_response( + code="200", + message="图标上传成功", + data={ + "icon_url": "/" + str(relative_path).replace("\\", "/"), + "file_size": file_size + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"上传图标失败: {str(e)}" + ) diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py index 324a173..dd4f7e1 100644 --- a/app/api/endpoints/users.py +++ b/app/api/endpoints/users.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, UploadFile, File from typing import Optional from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse, CreateUserRequest, UpdateUserRequest, RoleInfo from app.core.database import get_db_connection @@ -6,9 +6,14 @@ from app.core.auth import get_current_user from app.core.response import create_api_response from app.services.system_config_service import SystemConfigService import app.core.config as config_module +from app.core.config import UPLOAD_DIR, AVATAR_DIR import hashlib import datetime import re +import os +import shutil +import uuid +from pathlib import Path router = APIRouter() @@ -50,17 +55,18 @@ def create_user(request: CreateUserRequest, current_user: dict = Depends(get_cur password = request.password if request.password else SystemConfigService.get_default_reset_password() hashed_password = hash_password(password) - query = "INSERT INTO users (username, password_hash, caption, email, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s)" + query = "INSERT INTO users (username, password_hash, caption, email, avatar_url, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s, %s)" created_at = datetime.datetime.utcnow() - cursor.execute(query, (request.username, hashed_password, request.caption, request.email, request.role_id, created_at)) + cursor.execute(query, (request.username, hashed_password, request.caption, request.email, request.avatar_url, request.role_id, created_at)) connection.commit() return create_api_response(code="200", message="用户创建成功") @router.put("/users/{user_id}") def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = Depends(get_current_user)): - if current_user['role_id'] != 1: # 1 is admin - return create_api_response(code="403", message="仅管理员有权限修改用户信息") + # Allow admin (role_id=1) or self + if current_user['role_id'] != 1 and current_user['user_id'] != user_id: + return create_api_response(code="403", message="没有权限修改此用户信息") if request.email and not validate_email(request.email): return create_api_response(code="400", message="邮箱格式不正确") @@ -68,7 +74,7 @@ def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = D with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) - cursor.execute("SELECT user_id, username, caption, email, role_id FROM users WHERE user_id = %s", (user_id,)) + cursor.execute("SELECT user_id, username, caption, email, avatar_url, role_id FROM users WHERE user_id = %s", (user_id,)) existing_user = cursor.fetchone() if not existing_user: return create_api_response(code="404", message="用户不存在") @@ -78,19 +84,25 @@ def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = D if cursor.fetchone(): return create_api_response(code="400", message="用户名已存在") + # Restrict role_id update to admins only + target_role_id = existing_user['role_id'] + if current_user['role_id'] == 1 and request.role_id is not None: + target_role_id = request.role_id + update_data = { 'username': request.username if request.username else existing_user['username'], 'caption': request.caption if request.caption else existing_user['caption'], 'email': request.email if request.email else existing_user['email'], - 'role_id': request.role_id if request.role_id is not None else existing_user['role_id'] + 'avatar_url': request.avatar_url if request.avatar_url is not None else existing_user.get('avatar_url'), + 'role_id': target_role_id } - query = "UPDATE users SET username = %s, caption = %s, email = %s, role_id = %s WHERE user_id = %s" - cursor.execute(query, (update_data['username'], update_data['caption'], update_data['email'], update_data['role_id'], user_id)) + query = "UPDATE users SET username = %s, caption = %s, email = %s, avatar_url = %s, role_id = %s WHERE user_id = %s" + cursor.execute(query, (update_data['username'], update_data['caption'], update_data['email'], update_data['avatar_url'], update_data['role_id'], user_id)) connection.commit() cursor.execute(''' - SELECT u.user_id, u.username, u.caption, u.email, u.created_at, u.role_id, r.role_name + SELECT u.user_id, u.username, u.caption, u.email, u.avatar_url, u.created_at, u.role_id, r.role_name FROM users u LEFT JOIN roles r ON u.role_id = r.role_id WHERE u.user_id = %s @@ -102,6 +114,7 @@ def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = D username=updated_user['username'], caption=updated_user['caption'], email=updated_user['email'], + avatar_url=updated_user['avatar_url'], created_at=updated_user['created_at'], role_id=updated_user['role_id'], role_name=updated_user['role_name'], @@ -184,7 +197,7 @@ def get_all_users( # 主查询 query = ''' SELECT - u.user_id, u.username, u.caption, u.email, u.created_at, u.role_id, + u.user_id, u.username, u.caption, u.email, u.avatar_url, u.created_at, u.role_id, r.role_name, (SELECT COUNT(*) FROM meetings WHERE user_id = u.user_id) as meetings_created, (SELECT COUNT(*) FROM attendees WHERE user_id = u.user_id) as meetings_attended @@ -218,7 +231,7 @@ def get_user_info(user_id: int, current_user: dict = Depends(get_current_user)): cursor = connection.cursor(dictionary=True) user_query = ''' - SELECT u.user_id, u.username, u.caption, u.email, u.created_at, u.role_id, r.role_name + SELECT u.user_id, u.username, u.caption, u.email, u.avatar_url, u.created_at, u.role_id, r.role_name FROM users u LEFT JOIN roles r ON u.role_id = r.role_id WHERE u.user_id = %s @@ -242,6 +255,7 @@ def get_user_info(user_id: int, current_user: dict = Depends(get_current_user)): username=user['username'], caption=user['caption'], email=user['email'], + avatar_url=user['avatar_url'], created_at=user['created_at'], role_id=user['role_id'], role_name=user['role_name'], @@ -272,4 +286,48 @@ def update_password(user_id: int, request: PasswordChangeRequest, current_user: cursor.execute("UPDATE users SET password_hash = %s WHERE user_id = %s", (new_password_hash, user_id)) connection.commit() - return create_api_response(code="200", message="密码修改成功") \ No newline at end of file + return create_api_response(code="200", message="密码修改成功") + +@router.post("/users/{user_id}/avatar") +def upload_user_avatar( + user_id: int, + file: UploadFile = File(...), + current_user: dict = Depends(get_current_user) +): + # Allow admin or self + if current_user['role_id'] != 1 and current_user['user_id'] != user_id: + return create_api_response(code="403", message="没有权限上传此用户头像") + + # Validate file type + ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp'} + file_ext = os.path.splitext(file.filename)[1].lower() + if file_ext not in ALLOWED_EXTENSIONS: + return create_api_response(code="400", message="不支持的文件类型") + + # Ensure upload directory exists: AVATAR_DIR / str(user_id) + user_avatar_dir = AVATAR_DIR / str(user_id) + if not user_avatar_dir.exists(): + os.makedirs(user_avatar_dir) + + # Generate unique filename + unique_filename = f"{uuid.uuid4()}{file_ext}" + file_path = user_avatar_dir / unique_filename + + # Save file + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # Generate URL (relative) + # AVATAR_DIR is uploads/user/avatar + # file path is uploads/user/avatar/{user_id}/{filename} + # URL should be /uploads/user/avatar/{user_id}/{filename} + avatar_url = f"/uploads/user/avatar/{user_id}/{unique_filename}" + + # Update database + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + cursor.execute("UPDATE users SET avatar_url = %s WHERE user_id = %s", (avatar_url, user_id)) + connection.commit() + + return create_api_response(code="200", message="头像上传成功", data={"avatar_url": avatar_url}) \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 3ae835c..f7398b3 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -8,8 +8,11 @@ UPLOAD_DIR = BASE_DIR / "uploads" AUDIO_DIR = UPLOAD_DIR / "audio" TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio" MARKDOWN_DIR = UPLOAD_DIR / "markdown" -VOICEPRINT_DIR = UPLOAD_DIR / "voiceprint" CLIENT_DIR = UPLOAD_DIR / "clients" +EXTERNAL_APPS_DIR = UPLOAD_DIR / "external_apps" +USER_DIR = UPLOAD_DIR / "user" +VOICEPRINT_DIR = USER_DIR / "voiceprint" +AVATAR_DIR = USER_DIR / "avatar" # 文件上传配置 ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mpeg", ".mp4"} @@ -24,15 +27,19 @@ MAX_CLIENT_SIZE = 500 * 1024 * 1024 # 500MB for client installers UPLOAD_DIR.mkdir(exist_ok=True) AUDIO_DIR.mkdir(exist_ok=True) MARKDOWN_DIR.mkdir(exist_ok=True) -VOICEPRINT_DIR.mkdir(exist_ok=True) CLIENT_DIR.mkdir(exist_ok=True) +EXTERNAL_APPS_DIR.mkdir(exist_ok=True) +USER_DIR.mkdir(exist_ok=True) +VOICEPRINT_DIR.mkdir(exist_ok=True) +AVATAR_DIR.mkdir(exist_ok=True) + # 数据库配置 DATABASE_CONFIG = { - 'host': os.getenv('DB_HOST', '10.100.51.51'), + 'host': os.getenv('DB_HOST', '192.168.4.9'), 'user': os.getenv('DB_USER', 'root'), - 'password': os.getenv('DB_PASSWORD', 'Unis@123'), - 'database': os.getenv('DB_NAME', 'imeeting_dev'), + 'password': os.getenv('DB_PASSWORD', 'sagacity'), + 'database': os.getenv('DB_NAME', 'imeeting'), 'port': int(os.getenv('DB_PORT', '3306')), 'charset': 'utf8mb4' } @@ -56,10 +63,10 @@ APP_CONFIG = { # Redis配置 REDIS_CONFIG = { - 'host': os.getenv('REDIS_HOST', '10.100.51.51'), + 'host': os.getenv('REDIS_HOST', '192.168.4.9'), 'port': int(os.getenv('REDIS_PORT', '6379')), 'db': int(os.getenv('REDIS_DB', '0')), - 'password': os.getenv('REDIS_PASSWORD', 'Unis@123'), + 'password': os.getenv('REDIS_PASSWORD', ''), 'decode_responses': True } @@ -72,33 +79,6 @@ TRANSCRIPTION_POLL_CONFIG = { 'max_wait_time': int(os.getenv('TRANSCRIPTION_MAX_WAIT_TIME', '1800')), # 最大等待:30分钟 } -# LLM配置 - 阿里Qwen3大模型 -LLM_CONFIG = { - 'model_name': os.getenv('LLM_MODEL_NAME', 'qwen-plus'), - 'time_out': int(os.getenv('LLM_TIMEOUT', '120')), - 'temperature': float(os.getenv('LLM_TEMPERATURE', '0.7')), - 'top_p': float(os.getenv('LLM_TOP_P', '0.9')), - 'system_prompt': """你是一个专业的会议记录分析助手。请根据提供的会议转录内容,生成简洁明了的会议总结。 - -总结应该包括以下几个部分: -1. 会议概述 - 简要说明会议的主要目的和背景 -2. 主要讨论点 - 列出会议中讨论的重要话题和内容 -3. 决策事项 - 明确记录会议中做出的决定和结论 -4. 待办事项 - 列出需要后续跟进的任务和责任人 -5. 关键信息 - 其他重要的信息点 - -要求: -- 保持客观中性,不添加个人观点 -- 使用简洁的中文表达 -- 按重要性排序各项内容 -- 如果某个部分没有相关内容,可以说明"无相关内容" -- 总字数控制在500字以内""" -} - -# 密码重置配置 -DEFAULT_RESET_PASSWORD = os.getenv('DEFAULT_RESET_PASSWORD', '111111') - -# 加载系统配置文件 # 默认声纹配置 VOICEPRINT_CONFIG = { "template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。", @@ -107,5 +87,3 @@ VOICEPRINT_CONFIG = { "channels": 1 } -#首页TimeLine每页数量 -TIMELINE_PAGESIZE=10 diff --git a/app/main.py b/app/main.py index 7635c80..57e7f68 100644 --- a/app/main.py +++ b/app/main.py @@ -13,7 +13,7 @@ import uvicorn from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words +from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words, external_apps from app.core.config import UPLOAD_DIR, API_CONFIG app = FastAPI( @@ -46,6 +46,7 @@ app.include_router(tasks.router, prefix="/api", tags=["Tasks"]) app.include_router(prompts.router, prefix="/api", tags=["Prompts"]) app.include_router(knowledge_base.router, prefix="/api", tags=["KnowledgeBase"]) app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownloads"]) +app.include_router(external_apps.router, prefix="/api", tags=["ExternalApps"]) app.include_router(dict_data.router, prefix="/api", tags=["DictData"]) app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"]) app.include_router(audio.router, prefix="/api", tags=["Audio"]) diff --git a/app/models/models.py b/app/models/models.py index b0a61e2..647f8e7 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -11,6 +11,7 @@ class LoginResponse(BaseModel): username: str caption: str email: EmailStr + avatar_url: Optional[str] = None token: str role_id: int @@ -23,6 +24,7 @@ class UserInfo(BaseModel): username: str caption: str email: EmailStr + avatar_url: Optional[str] = None created_at: datetime.datetime meetings_created: int meetings_attended: int @@ -38,12 +40,14 @@ class CreateUserRequest(BaseModel): password: Optional[str] = None caption: str email: EmailStr + avatar_url: Optional[str] = None role_id: int class UpdateUserRequest(BaseModel): username: Optional[str] = None caption: Optional[str] = None email: Optional[str] = None + avatar_url: Optional[str] = None role_id: Optional[int] = None class UserLog(BaseModel): diff --git a/app/utils/apk_parser.py b/app/utils/apk_parser.py index dc1e157..d13faf7 100644 --- a/app/utils/apk_parser.py +++ b/app/utils/apk_parser.py @@ -13,14 +13,24 @@ def parse_apk_with_androguard(apk_path): from pyaxmlparser import APK apk = APK(apk_path) + + # 提取所有需要的信息 + package_name = apk.package version_code = apk.version_code version_name = apk.version_name + app_name = apk.application + min_sdk_version = apk.get_min_sdk_version() + target_sdk_version = apk.get_target_sdk_version() - print(f"APK解析成功: version_code={version_code}, version_name={version_name}") + print(f"APK解析成功: package={package_name}, version_code={version_code}, version_name={version_name}, app_name={app_name}") return { + 'package_name': package_name, 'version_code': int(version_code) if version_code else None, - 'version_name': version_name + 'version_name': version_name, + 'app_name': app_name, + 'min_sdk_version': min_sdk_version, + 'target_sdk_version': target_sdk_version } except ImportError: print("错误: pyaxmlparser 未安装") diff --git a/sql/.DS_Store b/sql/.DS_Store index 2e46cac..b9606ee 100644 Binary files a/sql/.DS_Store and b/sql/.DS_Store differ diff --git a/sql/migrations/update_account_settings.sql b/sql/migrations/update_account_settings.sql new file mode 100644 index 0000000..eae974d --- /dev/null +++ b/sql/migrations/update_account_settings.sql @@ -0,0 +1,30 @@ +-- Migration: Add avatar_url and update menu for Account Settings +-- Created at: 2026-01-15 + +BEGIN; + +-- 1. Add avatar_url to users table if it doesn't exist +-- Note: MySQL 5.7 doesn't support IF NOT EXISTS for columns easily in one line without procedure, +-- but for this environment we assume it doesn't exist or ignore error if strictly handled. +-- However, creating a safe idempotent script is better. +-- Since I can't run complex procedures easily here, I'll just run the ALTER. +-- If it fails, it fails (user can ignore if already applied). +ALTER TABLE `users` ADD COLUMN `avatar_url` VARCHAR(512) DEFAULT NULL AFTER `email`; + +-- 2. Remove 'change_password' menu +DELETE FROM `role_menu_permissions` WHERE `menu_id` IN (SELECT `menu_id` FROM `menus` WHERE `menu_code` = 'change_password'); +DELETE FROM `menus` WHERE `menu_code` = 'change_password'; + +-- 3. Add 'account_settings' menu +INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `sort_order`, `is_active`, `description`) +VALUES ('account_settings', '账户设置', 'UserCog', '/account-settings', 'link', 1, 1, '管理个人账户信息'); + +-- 4. Grant permissions +-- Grant to Admin (role_id=1) and User (role_id=2) +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`) +SELECT 1, menu_id FROM `menus` WHERE `menu_code` = 'account_settings'; + +INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`) +SELECT 2, menu_id FROM `menus` WHERE `menu_code` = 'account_settings'; + +COMMIT;