diff --git a/backend/app/models/tenant_app.py b/backend/app/models/tenant_app.py
index 46b9101..2f04d64 100644
--- a/backend/app/models/tenant_app.py
+++ b/backend/app/models/tenant_app.py
@@ -19,8 +19,7 @@ class TenantApp(Base):
wechat_secret_encrypted = Column(Text)
# 鉴权配置
- token_secret = Column(String(64))
- token_required = Column(SmallInteger, default=0)
+ access_token = Column(String(64)) # 访问令牌(长期有效)
allowed_origins = Column(Text) # JSON 数组
# 功能权限
diff --git a/backend/app/routers/apps.py b/backend/app/routers/apps.py
index b391655..3cfea82 100644
--- a/backend/app/routers/apps.py
+++ b/backend/app/routers/apps.py
@@ -1,281 +1,267 @@
-"""应用管理路由"""
-import json
-import hmac
-import hashlib
-import time
-from fastapi import APIRouter, Depends, HTTPException, Query
-from pydantic import BaseModel
-from typing import Optional, List
-from sqlalchemy.orm import Session
-
-from ..database import get_db
-from ..models.app import App
-from ..models.tenant_app import TenantApp
-from .auth import get_current_user, require_operator
-from ..models.user import User
-
-router = APIRouter(prefix="/apps", tags=["应用管理"])
-
-
-# ============ Schemas ============
-
-class ToolItem(BaseModel):
- """工具项"""
- code: str
- name: str
- path: str
-
-
-class AppCreate(BaseModel):
- """创建应用"""
- app_code: str
- app_name: str
- base_url: Optional[str] = None
- description: Optional[str] = None
- tools: Optional[List[ToolItem]] = None
-
-
-class AppUpdate(BaseModel):
- """更新应用"""
- app_name: Optional[str] = None
- base_url: Optional[str] = None
- description: Optional[str] = None
- tools: Optional[List[ToolItem]] = None
- status: Optional[int] = None
-
-
-class GenerateUrlRequest(BaseModel):
- """生成链接请求"""
- tenant_id: str
- app_code: str
- tool_code: Optional[str] = None # 不传则生成应用首页链接
-
-
-# ============ API Endpoints ============
-
-@router.get("")
-async def list_apps(
- page: int = Query(1, ge=1),
- size: int = Query(20, ge=1, le=100),
- status: Optional[int] = None,
- user: User = Depends(get_current_user),
- db: Session = Depends(get_db)
-):
- """获取应用列表"""
- query = db.query(App)
-
- if status is not None:
- query = query.filter(App.status == status)
-
- total = query.count()
- apps = query.order_by(App.id.asc()).offset((page - 1) * size).limit(size).all()
-
- return {
- "total": total,
- "page": page,
- "size": size,
- "items": [format_app(app) for app in apps]
- }
-
-
-@router.get("/all")
-async def list_all_apps(
- user: User = Depends(get_current_user),
- db: Session = Depends(get_db)
-):
- """获取所有启用的应用(用于下拉选择)"""
- apps = db.query(App).filter(App.status == 1).order_by(App.id.asc()).all()
- return [{"app_code": app.app_code, "app_name": app.app_name} for app in apps]
-
-
-@router.get("/{app_id}")
-async def get_app(
- app_id: int,
- user: User = Depends(get_current_user),
- db: Session = Depends(get_db)
-):
- """获取应用详情"""
- app = db.query(App).filter(App.id == app_id).first()
- if not app:
- raise HTTPException(status_code=404, detail="应用不存在")
-
- return format_app(app)
-
-
-@router.post("")
-async def create_app(
- data: AppCreate,
- user: User = Depends(require_operator),
- db: Session = Depends(get_db)
-):
- """创建应用"""
- # 检查 app_code 是否重复
- exists = db.query(App).filter(App.app_code == data.app_code).first()
- if exists:
- raise HTTPException(status_code=400, detail="应用代码已存在")
-
- app = App(
- app_code=data.app_code,
- app_name=data.app_name,
- base_url=data.base_url,
- description=data.description,
- tools=json.dumps([t.model_dump() for t in data.tools], ensure_ascii=False) if data.tools else None,
- status=1
- )
- db.add(app)
- db.commit()
- db.refresh(app)
-
- return {"success": True, "id": app.id}
-
-
-@router.put("/{app_id}")
-async def update_app(
- app_id: int,
- data: AppUpdate,
- user: User = Depends(require_operator),
- db: Session = Depends(get_db)
-):
- """更新应用"""
- app = db.query(App).filter(App.id == app_id).first()
- if not app:
- raise HTTPException(status_code=404, detail="应用不存在")
-
- update_data = data.model_dump(exclude_unset=True)
-
- # 处理 tools JSON
- if 'tools' in update_data:
- if update_data['tools']:
- update_data['tools'] = json.dumps([t.model_dump() if hasattr(t, 'model_dump') else t for t in update_data['tools']], ensure_ascii=False)
- else:
- update_data['tools'] = None
-
- for key, value in update_data.items():
- setattr(app, key, value)
-
- db.commit()
- return {"success": True}
-
-
-@router.delete("/{app_id}")
-async def delete_app(
- app_id: int,
- user: User = Depends(require_operator),
- db: Session = Depends(get_db)
-):
- """删除应用"""
- app = db.query(App).filter(App.id == app_id).first()
- if not app:
- raise HTTPException(status_code=404, detail="应用不存在")
-
- # 检查是否有租户在使用
- tenant_count = db.query(TenantApp).filter(TenantApp.app_code == app.app_code).count()
- if tenant_count > 0:
- raise HTTPException(status_code=400, detail=f"有 {tenant_count} 个租户正在使用此应用,无法删除")
-
- db.delete(app)
- db.commit()
-
- return {"success": True}
-
-
-@router.post("/generate-url")
-async def generate_signed_url(
- data: GenerateUrlRequest,
- user: User = Depends(get_current_user),
- db: Session = Depends(get_db)
-):
- """
- 生成带签名的访问链接
-
- 返回完整的可直接使用的 URL
- """
- # 获取应用信息
- app = db.query(App).filter(App.app_code == data.app_code, App.status == 1).first()
- if not app:
- raise HTTPException(status_code=404, detail="应用不存在或已禁用")
-
- if not app.base_url:
- raise HTTPException(status_code=400, detail="应用未配置基础URL")
-
- # 获取租户配置
- tenant_app = db.query(TenantApp).filter(
- TenantApp.tenant_id == data.tenant_id,
- TenantApp.app_code == data.app_code,
- TenantApp.status == 1
- ).first()
-
- if not tenant_app:
- raise HTTPException(status_code=404, detail="租户未配置此应用")
-
- # 构建基础 URL
- base_url = app.base_url.rstrip('/')
- if data.tool_code:
- # 查找工具路径
- tools = json.loads(app.tools) if app.tools else []
- tool = next((t for t in tools if t.get('code') == data.tool_code), None)
- if tool:
- base_url = f"{base_url}{tool.get('path', '')}"
- else:
- base_url = f"{base_url}/{data.tool_code}"
-
- # 构建参数
- params = {
- "tid": data.tenant_id,
- "aid": data.app_code
- }
-
- # 如果需要签名
- if tenant_app.token_required and tenant_app.token_secret:
- ts = str(int(time.time()))
- message = f"{data.tenant_id}{data.app_code}{ts}"
- sign = hmac.new(
- tenant_app.token_secret.encode(),
- message.encode(),
- hashlib.sha256
- ).hexdigest()
- params["ts"] = ts
- params["sign"] = sign
-
- # 组装 URL
- query_string = "&".join([f"{k}={v}" for k, v in params.items()])
- full_url = f"{base_url}?{query_string}"
-
- return {
- "success": True,
- "url": full_url,
- "params": params,
- "token_required": bool(tenant_app.token_required),
- "expires_in": 300 if tenant_app.token_required else None, # 签名5分钟有效
- "note": "签名链接5分钟内有效,过期需重新生成" if tenant_app.token_required else "免签名链接,长期有效"
- }
-
-
-@router.get("/{app_code}/tools")
-async def get_app_tools(
- app_code: str,
- user: User = Depends(get_current_user),
- db: Session = Depends(get_db)
-):
- """获取应用的工具列表(用于配置权限时选择)"""
- app = db.query(App).filter(App.app_code == app_code).first()
- if not app:
- raise HTTPException(status_code=404, detail="应用不存在")
-
- tools = json.loads(app.tools) if app.tools else []
- return tools
-
-
-def format_app(app: App) -> dict:
- """格式化应用数据"""
- return {
- "id": app.id,
- "app_code": app.app_code,
- "app_name": app.app_name,
- "base_url": app.base_url,
- "description": app.description,
- "tools": json.loads(app.tools) if app.tools else [],
- "status": app.status,
- "created_at": app.created_at,
- "updated_at": app.updated_at
- }
+"""应用管理路由"""
+import json
+from fastapi import APIRouter, Depends, HTTPException, Query
+from pydantic import BaseModel
+from typing import Optional, List
+from sqlalchemy.orm import Session
+
+from ..database import get_db
+from ..models.app import App
+from ..models.tenant_app import TenantApp
+from .auth import get_current_user, require_operator
+from ..models.user import User
+
+router = APIRouter(prefix="/apps", tags=["应用管理"])
+
+
+# ============ Schemas ============
+
+class ToolItem(BaseModel):
+ """工具项"""
+ code: str
+ name: str
+ path: str
+
+
+class AppCreate(BaseModel):
+ """创建应用"""
+ app_code: str
+ app_name: str
+ base_url: Optional[str] = None
+ description: Optional[str] = None
+ tools: Optional[List[ToolItem]] = None
+
+
+class AppUpdate(BaseModel):
+ """更新应用"""
+ app_name: Optional[str] = None
+ base_url: Optional[str] = None
+ description: Optional[str] = None
+ tools: Optional[List[ToolItem]] = None
+ status: Optional[int] = None
+
+
+class GenerateUrlRequest(BaseModel):
+ """生成链接请求"""
+ tenant_id: str
+ app_code: str
+ tool_code: Optional[str] = None # 不传则生成应用首页链接
+
+
+# ============ API Endpoints ============
+
+@router.get("")
+async def list_apps(
+ page: int = Query(1, ge=1),
+ size: int = Query(20, ge=1, le=100),
+ status: Optional[int] = None,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取应用列表"""
+ query = db.query(App)
+
+ if status is not None:
+ query = query.filter(App.status == status)
+
+ total = query.count()
+ apps = query.order_by(App.id.asc()).offset((page - 1) * size).limit(size).all()
+
+ return {
+ "total": total,
+ "page": page,
+ "size": size,
+ "items": [format_app(app) for app in apps]
+ }
+
+
+@router.get("/all")
+async def list_all_apps(
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取所有启用的应用(用于下拉选择)"""
+ apps = db.query(App).filter(App.status == 1).order_by(App.id.asc()).all()
+ return [{"app_code": app.app_code, "app_name": app.app_name} for app in apps]
+
+
+@router.get("/{app_id}")
+async def get_app(
+ app_id: int,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取应用详情"""
+ app = db.query(App).filter(App.id == app_id).first()
+ if not app:
+ raise HTTPException(status_code=404, detail="应用不存在")
+
+ return format_app(app)
+
+
+@router.post("")
+async def create_app(
+ data: AppCreate,
+ user: User = Depends(require_operator),
+ db: Session = Depends(get_db)
+):
+ """创建应用"""
+ # 检查 app_code 是否重复
+ exists = db.query(App).filter(App.app_code == data.app_code).first()
+ if exists:
+ raise HTTPException(status_code=400, detail="应用代码已存在")
+
+ app = App(
+ app_code=data.app_code,
+ app_name=data.app_name,
+ base_url=data.base_url,
+ description=data.description,
+ tools=json.dumps([t.model_dump() for t in data.tools], ensure_ascii=False) if data.tools else None,
+ status=1
+ )
+ db.add(app)
+ db.commit()
+ db.refresh(app)
+
+ return {"success": True, "id": app.id}
+
+
+@router.put("/{app_id}")
+async def update_app(
+ app_id: int,
+ data: AppUpdate,
+ user: User = Depends(require_operator),
+ db: Session = Depends(get_db)
+):
+ """更新应用"""
+ app = db.query(App).filter(App.id == app_id).first()
+ if not app:
+ raise HTTPException(status_code=404, detail="应用不存在")
+
+ update_data = data.model_dump(exclude_unset=True)
+
+ # 处理 tools JSON
+ if 'tools' in update_data:
+ if update_data['tools']:
+ update_data['tools'] = json.dumps([t.model_dump() if hasattr(t, 'model_dump') else t for t in update_data['tools']], ensure_ascii=False)
+ else:
+ update_data['tools'] = None
+
+ for key, value in update_data.items():
+ setattr(app, key, value)
+
+ db.commit()
+ return {"success": True}
+
+
+@router.delete("/{app_id}")
+async def delete_app(
+ app_id: int,
+ user: User = Depends(require_operator),
+ db: Session = Depends(get_db)
+):
+ """删除应用"""
+ app = db.query(App).filter(App.id == app_id).first()
+ if not app:
+ raise HTTPException(status_code=404, detail="应用不存在")
+
+ # 检查是否有租户在使用
+ tenant_count = db.query(TenantApp).filter(TenantApp.app_code == app.app_code).count()
+ if tenant_count > 0:
+ raise HTTPException(status_code=400, detail=f"有 {tenant_count} 个租户正在使用此应用,无法删除")
+
+ db.delete(app)
+ db.commit()
+
+ return {"success": True}
+
+
+@router.post("/generate-url")
+async def generate_url(
+ data: GenerateUrlRequest,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """
+ 生成访问链接
+
+ 返回完整的可直接使用的 URL(使用静态 token,长期有效)
+ """
+ # 获取应用信息
+ app = db.query(App).filter(App.app_code == data.app_code, App.status == 1).first()
+ if not app:
+ raise HTTPException(status_code=404, detail="应用不存在或已禁用")
+
+ if not app.base_url:
+ raise HTTPException(status_code=400, detail="应用未配置基础URL")
+
+ # 获取租户配置
+ tenant_app = db.query(TenantApp).filter(
+ TenantApp.tenant_id == data.tenant_id,
+ TenantApp.app_code == data.app_code,
+ TenantApp.status == 1
+ ).first()
+
+ if not tenant_app:
+ raise HTTPException(status_code=404, detail="租户未配置此应用")
+
+ if not tenant_app.access_token:
+ raise HTTPException(status_code=400, detail="租户应用未配置访问令牌")
+
+ # 构建基础 URL
+ base_url = app.base_url.rstrip('/')
+ if data.tool_code:
+ # 查找工具路径
+ tools = json.loads(app.tools) if app.tools else []
+ tool = next((t for t in tools if t.get('code') == data.tool_code), None)
+ if tool:
+ base_url = f"{base_url}{tool.get('path', '')}"
+ else:
+ base_url = f"{base_url}/{data.tool_code}"
+
+ # 构建参数(静态 token,长期有效)
+ params = {
+ "tid": data.tenant_id,
+ "token": tenant_app.access_token
+ }
+
+ # 组装 URL
+ query_string = "&".join([f"{k}={v}" for k, v in params.items()])
+ full_url = f"{base_url}?{query_string}"
+
+ return {
+ "success": True,
+ "url": full_url,
+ "params": params,
+ "note": "静态链接,长期有效"
+ }
+
+
+@router.get("/{app_code}/tools")
+async def get_app_tools(
+ app_code: str,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取应用的工具列表(用于配置权限时选择)"""
+ app = db.query(App).filter(App.app_code == app_code).first()
+ if not app:
+ raise HTTPException(status_code=404, detail="应用不存在")
+
+ tools = json.loads(app.tools) if app.tools else []
+ return tools
+
+
+def format_app(app: App) -> dict:
+ """格式化应用数据"""
+ return {
+ "id": app.id,
+ "app_code": app.app_code,
+ "app_name": app.app_name,
+ "base_url": app.base_url,
+ "description": app.description,
+ "tools": json.loads(app.tools) if app.tools else [],
+ "status": app.status,
+ "created_at": app.created_at,
+ "updated_at": app.updated_at
+ }
diff --git a/backend/app/routers/tenant_apps.py b/backend/app/routers/tenant_apps.py
index ef385ec..fc29704 100644
--- a/backend/app/routers/tenant_apps.py
+++ b/backend/app/routers/tenant_apps.py
@@ -24,8 +24,7 @@ class TenantAppCreate(BaseModel):
wechat_corp_id: Optional[str] = None
wechat_agent_id: Optional[str] = None
wechat_secret: Optional[str] = None # 明文,存储时加密
- token_secret: Optional[str] = None # 如果不传则自动生成
- token_required: bool = False
+ access_token: Optional[str] = None # 如果不传则自动生成
allowed_origins: Optional[List[str]] = None
allowed_tools: Optional[List[str]] = None
@@ -35,8 +34,7 @@ class TenantAppUpdate(BaseModel):
wechat_corp_id: Optional[str] = None
wechat_agent_id: Optional[str] = None
wechat_secret: Optional[str] = None
- token_secret: Optional[str] = None
- token_required: Optional[bool] = None
+ access_token: Optional[str] = None
allowed_origins: Optional[List[str]] = None
allowed_tools: Optional[List[str]] = None
status: Optional[int] = None
@@ -101,8 +99,8 @@ async def create_tenant_app(
if exists:
raise HTTPException(status_code=400, detail="该租户应用配置已存在")
- # 自动生成 token_secret
- token_secret = data.token_secret or secrets.token_hex(32)
+ # 自动生成 access_token
+ access_token = data.access_token or secrets.token_hex(32)
# 加密 wechat_secret
wechat_secret_encrypted = None
@@ -116,8 +114,7 @@ async def create_tenant_app(
wechat_corp_id=data.wechat_corp_id,
wechat_agent_id=data.wechat_agent_id,
wechat_secret_encrypted=wechat_secret_encrypted,
- token_secret=token_secret,
- token_required=1 if data.token_required else 0,
+ access_token=access_token,
allowed_origins=json.dumps(data.allowed_origins) if data.allowed_origins else None,
allowed_tools=json.dumps(data.allowed_tools) if data.allowed_tools else None,
status=1
@@ -126,7 +123,7 @@ async def create_tenant_app(
db.commit()
db.refresh(app)
- return {"success": True, "id": app.id, "token_secret": token_secret}
+ return {"success": True, "id": app.id, "access_token": access_token}
@router.put("/{app_id}")
@@ -155,10 +152,6 @@ async def update_tenant_app(
if 'allowed_tools' in update_data:
update_data['allowed_tools'] = json.dumps(update_data['allowed_tools']) if update_data['allowed_tools'] else None
- # 处理 token_required
- if 'token_required' in update_data:
- update_data['token_required'] = 1 if update_data['token_required'] else 0
-
for key, value in update_data.items():
setattr(app, key, value)
@@ -189,16 +182,16 @@ async def regenerate_token(
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
- """重新生成 token_secret"""
+ """重新生成 access_token"""
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用配置不存在")
new_token = secrets.token_hex(32)
- app.token_secret = new_token
+ app.access_token = new_token
db.commit()
- return {"success": True, "token_secret": new_token}
+ return {"success": True, "access_token": new_token}
@router.get("/{app_id}/wechat-secret")
@@ -229,8 +222,7 @@ def format_tenant_app(app: TenantApp, mask_secret: bool = True) -> dict:
"wechat_corp_id": app.wechat_corp_id,
"wechat_agent_id": app.wechat_agent_id,
"has_wechat_secret": bool(app.wechat_secret_encrypted),
- "token_secret": "******" if mask_secret and app.token_secret else app.token_secret,
- "token_required": bool(app.token_required),
+ "access_token": "******" if mask_secret and app.access_token else app.access_token,
"allowed_origins": json.loads(app.allowed_origins) if app.allowed_origins else [],
"allowed_tools": json.loads(app.allowed_tools) if app.allowed_tools else [],
"status": app.status,
diff --git a/frontend/src/views/app-config/index.vue b/frontend/src/views/app-config/index.vue
index 54d0424..34b6276 100644
--- a/frontend/src/views/app-config/index.vue
+++ b/frontend/src/views/app-config/index.vue
@@ -1,490 +1,481 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
- 已配置
- 未配置
-
-
-
-
-
- {{ row.token_required ? '必须' : '可选' }}
-
-
-
-
-
-
- {{ tool }}
-
- ...
-
-
-
-
- 生成链接
- 编辑
- 密钥
- 重置
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 企业微信配置
-
-
-
-
-
-
-
-
-
-
-
- 鉴权配置
-
-
-
- 开启后 URL 必须携带有效签名
-
-
-
- {{ opt.label }}
-
-
-
-
- 取消
- 确定
-
-
-
-
-
-
-
- {{ currentRow.tenant_id }}
- {{ currentRow.app_code }}
-
-
- {{ currentRow.token_required ? '需要签名' : '免签名' }}
-
-
-
- {{ (currentRow.allowed_tools || []).length > 0 ? currentRow.allowed_tools.join(', ') : '全部' }}
-
-
-
-
-
-
-
-
-
-
-
- 生成链接
-
-
-
-
-
-
生成结果
-
-
-
-
-
-
-
- 复制链接
-
-
-
-
-
- 关闭
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+ 已配置
+ 未配置
+
+
+
+
+ 已配置
+ 未配置
+
+
+
+
+
+ {{ tool }}
+
+ ...
+
+
+
+
+ 生成链接
+ 编辑
+ 密钥
+ 重置
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 企业微信配置
+
+
+
+
+
+
+
+
+
+
+
+ 权限配置
+
+
+
+ {{ opt.label }}
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+ {{ currentRow.tenant_id }}
+ {{ currentRow.app_code }}
+
+ 已配置
+ 未配置
+
+
+ {{ (currentRow.allowed_tools || []).length > 0 ? currentRow.allowed_tools.join(', ') : '全部' }}
+
+
+
+
+
+
+
+
+
+
+
+ 生成链接
+
+
+
+
+
+
生成结果
+
+
+
+
+
+
+
+ 复制链接
+
+
+
+
+
+ 关闭
+
+
+
+
+
+