ForcomeBot 2.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forcomebot-2.2.4.dist-info/METADATA +342 -0
- forcomebot-2.2.4.dist-info/RECORD +36 -0
- forcomebot-2.2.4.dist-info/WHEEL +4 -0
- forcomebot-2.2.4.dist-info/entry_points.txt +4 -0
- src/__init__.py +68 -0
- src/__main__.py +487 -0
- src/api/__init__.py +21 -0
- src/api/routes.py +775 -0
- src/api/websocket.py +280 -0
- src/auth/__init__.py +33 -0
- src/auth/database.py +87 -0
- src/auth/dingtalk.py +373 -0
- src/auth/jwt_handler.py +129 -0
- src/auth/middleware.py +260 -0
- src/auth/models.py +107 -0
- src/auth/routes.py +385 -0
- src/clients/__init__.py +7 -0
- src/clients/langbot.py +710 -0
- src/clients/qianxun.py +388 -0
- src/core/__init__.py +19 -0
- src/core/config_manager.py +411 -0
- src/core/log_collector.py +167 -0
- src/core/message_queue.py +364 -0
- src/core/state_store.py +242 -0
- src/handlers/__init__.py +8 -0
- src/handlers/message_handler.py +833 -0
- src/handlers/message_parser.py +325 -0
- src/handlers/scheduler.py +822 -0
- src/models.py +77 -0
- src/static/assets/index-B4i68B5_.js +50 -0
- src/static/assets/index-BPXisDkw.css +2 -0
- src/static/index.html +14 -0
- src/static/vite.svg +1 -0
- src/utils/__init__.py +13 -0
- src/utils/text_processor.py +166 -0
- src/utils/xml_parser.py +215 -0
src/auth/middleware.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""认证中间件"""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, Callable, List
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from fastapi import Request, HTTPException, Depends
|
|
7
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from .database import get_db
|
|
12
|
+
from .models import User, OperationLog
|
|
13
|
+
from .jwt_handler import JWTHandler
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# HTTP Bearer认证
|
|
18
|
+
security = HTTPBearer(auto_error=False)
|
|
19
|
+
|
|
20
|
+
# 全局JWT处理器
|
|
21
|
+
_jwt_handler: Optional[JWTHandler] = None
|
|
22
|
+
|
|
23
|
+
# 认证开关
|
|
24
|
+
_auth_enabled: bool = True
|
|
25
|
+
|
|
26
|
+
# 白名单路由(不需要认证)
|
|
27
|
+
AUTH_WHITELIST = [
|
|
28
|
+
"/health",
|
|
29
|
+
"/qianxun/callback",
|
|
30
|
+
"/api/auth/dingtalk/login",
|
|
31
|
+
"/api/auth/dingtalk/h5-login",
|
|
32
|
+
"/api/auth/dingtalk/qrcode-url",
|
|
33
|
+
"/api/auth/dingtalk/config",
|
|
34
|
+
"/docs",
|
|
35
|
+
"/openapi.json",
|
|
36
|
+
"/redoc",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def init_auth(jwt_handler: JWTHandler, auth_enabled: bool = True):
|
|
41
|
+
"""初始化认证模块"""
|
|
42
|
+
global _jwt_handler, _auth_enabled
|
|
43
|
+
_jwt_handler = jwt_handler
|
|
44
|
+
_auth_enabled = auth_enabled
|
|
45
|
+
logger.info(f"认证模块已初始化, 认证开关: {auth_enabled}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_auth_enabled() -> bool:
|
|
49
|
+
"""检查认证是否启用"""
|
|
50
|
+
return _auth_enabled
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_jwt_handler() -> JWTHandler:
|
|
54
|
+
"""获取JWT处理器"""
|
|
55
|
+
if _jwt_handler is None:
|
|
56
|
+
raise RuntimeError("JWT处理器未初始化")
|
|
57
|
+
return _jwt_handler
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AuthMiddleware:
|
|
61
|
+
"""认证中间件"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, app):
|
|
64
|
+
self.app = app
|
|
65
|
+
|
|
66
|
+
async def __call__(self, scope, receive, send):
|
|
67
|
+
if scope["type"] == "http":
|
|
68
|
+
# 检查是否需要认证
|
|
69
|
+
path = scope.get("path", "")
|
|
70
|
+
|
|
71
|
+
# 白名单路由跳过认证
|
|
72
|
+
if self._is_whitelisted(path):
|
|
73
|
+
await self.app(scope, receive, send)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# 静态文件跳过认证
|
|
77
|
+
if path.startswith("/app/") or path.startswith("/static/"):
|
|
78
|
+
await self.app(scope, receive, send)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# 认证未启用时跳过
|
|
82
|
+
if not _auth_enabled:
|
|
83
|
+
await self.app(scope, receive, send)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
await self.app(scope, receive, send)
|
|
87
|
+
|
|
88
|
+
def _is_whitelisted(self, path: str) -> bool:
|
|
89
|
+
"""检查路径是否在白名单中"""
|
|
90
|
+
for whitelist_path in AUTH_WHITELIST:
|
|
91
|
+
if path == whitelist_path or path.startswith(whitelist_path + "/"):
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def get_current_user(
|
|
97
|
+
request: Request,
|
|
98
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
|
99
|
+
) -> User:
|
|
100
|
+
"""
|
|
101
|
+
获取当前登录用户(必须登录)
|
|
102
|
+
|
|
103
|
+
用于需要强制登录的接口
|
|
104
|
+
"""
|
|
105
|
+
# 认证未启用时返回模拟用户
|
|
106
|
+
if not _auth_enabled:
|
|
107
|
+
return _create_mock_user()
|
|
108
|
+
|
|
109
|
+
if not credentials:
|
|
110
|
+
raise HTTPException(status_code=401, detail="未提供认证凭证")
|
|
111
|
+
|
|
112
|
+
if not _jwt_handler:
|
|
113
|
+
raise HTTPException(status_code=500, detail="认证服务未初始化")
|
|
114
|
+
|
|
115
|
+
# 验证Token
|
|
116
|
+
payload = _jwt_handler.verify_token(credentials.credentials)
|
|
117
|
+
if not payload:
|
|
118
|
+
raise HTTPException(status_code=401, detail="无效的认证凭证")
|
|
119
|
+
|
|
120
|
+
# 获取用户ID
|
|
121
|
+
user_id = payload.get("sub")
|
|
122
|
+
if not user_id:
|
|
123
|
+
raise HTTPException(status_code=401, detail="无效的用户信息")
|
|
124
|
+
|
|
125
|
+
# 从数据库获取用户
|
|
126
|
+
db = get_db()
|
|
127
|
+
async with db.session_factory() as session:
|
|
128
|
+
result = await session.execute(
|
|
129
|
+
select(User).where(User.id == int(user_id))
|
|
130
|
+
)
|
|
131
|
+
user = result.scalar_one_or_none()
|
|
132
|
+
|
|
133
|
+
if not user:
|
|
134
|
+
raise HTTPException(status_code=401, detail="用户不存在")
|
|
135
|
+
|
|
136
|
+
if not user.is_active:
|
|
137
|
+
raise HTTPException(status_code=403, detail="用户已被禁用")
|
|
138
|
+
|
|
139
|
+
return user
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def get_optional_user(
|
|
143
|
+
request: Request,
|
|
144
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
|
145
|
+
) -> Optional[User]:
|
|
146
|
+
"""
|
|
147
|
+
获取当前登录用户(可选)
|
|
148
|
+
|
|
149
|
+
用于不强制登录但需要用户信息的接口
|
|
150
|
+
"""
|
|
151
|
+
# 认证未启用时返回模拟用户
|
|
152
|
+
if not _auth_enabled:
|
|
153
|
+
return _create_mock_user()
|
|
154
|
+
|
|
155
|
+
if not credentials:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
if not _jwt_handler:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# 验证Token
|
|
162
|
+
payload = _jwt_handler.verify_token(credentials.credentials)
|
|
163
|
+
if not payload:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
# 获取用户ID
|
|
167
|
+
user_id = payload.get("sub")
|
|
168
|
+
if not user_id:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# 从数据库获取用户
|
|
172
|
+
try:
|
|
173
|
+
db = get_db()
|
|
174
|
+
async with db.session_factory() as session:
|
|
175
|
+
result = await session.execute(
|
|
176
|
+
select(User).where(User.id == int(user_id))
|
|
177
|
+
)
|
|
178
|
+
user = result.scalar_one_or_none()
|
|
179
|
+
return user if user and user.is_active else None
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning(f"获取用户信息失败: {e}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _create_mock_user() -> User:
|
|
186
|
+
"""创建模拟用户(认证关闭时使用)"""
|
|
187
|
+
user = User(
|
|
188
|
+
id=0,
|
|
189
|
+
dingtalk_userid="mock_user",
|
|
190
|
+
name="系统用户",
|
|
191
|
+
is_active=True,
|
|
192
|
+
is_admin=True
|
|
193
|
+
)
|
|
194
|
+
return user
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def log_operation(
|
|
198
|
+
user: Optional[User],
|
|
199
|
+
action: str,
|
|
200
|
+
resource: str,
|
|
201
|
+
resource_id: Optional[str] = None,
|
|
202
|
+
detail: Optional[dict] = None,
|
|
203
|
+
request: Optional[Request] = None,
|
|
204
|
+
status: str = "success",
|
|
205
|
+
error_message: Optional[str] = None
|
|
206
|
+
):
|
|
207
|
+
"""
|
|
208
|
+
记录操作日志
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
user: 操作用户
|
|
212
|
+
action: 操作类型
|
|
213
|
+
resource: 操作资源
|
|
214
|
+
resource_id: 资源ID
|
|
215
|
+
detail: 操作详情
|
|
216
|
+
request: 请求对象
|
|
217
|
+
status: 操作状态
|
|
218
|
+
error_message: 错误信息
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
db = get_db()
|
|
222
|
+
async with db.session_factory() as session:
|
|
223
|
+
log = OperationLog(
|
|
224
|
+
user_id=user.id if user and user.id else None,
|
|
225
|
+
user_name=user.name if user else None,
|
|
226
|
+
action=action,
|
|
227
|
+
resource=resource,
|
|
228
|
+
resource_id=resource_id,
|
|
229
|
+
detail=detail,
|
|
230
|
+
method=request.method if request else None,
|
|
231
|
+
path=str(request.url.path) if request else None,
|
|
232
|
+
ip_address=_get_client_ip(request) if request else None,
|
|
233
|
+
user_agent=request.headers.get("user-agent", "")[:512] if request else None,
|
|
234
|
+
status=status,
|
|
235
|
+
error_message=error_message,
|
|
236
|
+
created_at=datetime.now()
|
|
237
|
+
)
|
|
238
|
+
session.add(log)
|
|
239
|
+
await session.commit()
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"记录操作日志失败: {e}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _get_client_ip(request: Request) -> str:
|
|
245
|
+
"""获取客户端IP"""
|
|
246
|
+
# 优先从X-Forwarded-For获取
|
|
247
|
+
forwarded = request.headers.get("x-forwarded-for")
|
|
248
|
+
if forwarded:
|
|
249
|
+
return forwarded.split(",")[0].strip()
|
|
250
|
+
|
|
251
|
+
# 从X-Real-IP获取
|
|
252
|
+
real_ip = request.headers.get("x-real-ip")
|
|
253
|
+
if real_ip:
|
|
254
|
+
return real_ip
|
|
255
|
+
|
|
256
|
+
# 从连接获取
|
|
257
|
+
if request.client:
|
|
258
|
+
return request.client.host
|
|
259
|
+
|
|
260
|
+
return "unknown"
|
src/auth/models.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""数据库模型定义"""
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import String, Boolean, DateTime, Integer, Text, JSON, ForeignKey
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
|
+
|
|
8
|
+
from .database import Base
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class User(Base):
|
|
12
|
+
"""用户表"""
|
|
13
|
+
__tablename__ = "users"
|
|
14
|
+
|
|
15
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
16
|
+
|
|
17
|
+
# 钉钉用户信息
|
|
18
|
+
dingtalk_userid: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="钉钉用户ID")
|
|
19
|
+
dingtalk_unionid: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="钉钉UnionID")
|
|
20
|
+
|
|
21
|
+
# 基本信息
|
|
22
|
+
name: Mapped[str] = mapped_column(String(64), comment="姓名")
|
|
23
|
+
avatar: Mapped[Optional[str]] = mapped_column(String(512), nullable=True, comment="头像URL")
|
|
24
|
+
mobile: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="手机号")
|
|
25
|
+
email: Mapped[Optional[str]] = mapped_column(String(128), nullable=True, comment="邮箱")
|
|
26
|
+
department: Mapped[Optional[str]] = mapped_column(String(256), nullable=True, comment="部门")
|
|
27
|
+
title: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="职位")
|
|
28
|
+
|
|
29
|
+
# 状态
|
|
30
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
|
|
31
|
+
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否管理员")
|
|
32
|
+
|
|
33
|
+
# 时间戳
|
|
34
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间")
|
|
35
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
|
36
|
+
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="最后登录时间")
|
|
37
|
+
|
|
38
|
+
# 关联
|
|
39
|
+
operation_logs: Mapped[list["OperationLog"]] = relationship("OperationLog", back_populates="user")
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict:
|
|
42
|
+
"""转换为字典"""
|
|
43
|
+
return {
|
|
44
|
+
"id": self.id,
|
|
45
|
+
"dingtalk_userid": self.dingtalk_userid,
|
|
46
|
+
"name": self.name,
|
|
47
|
+
"avatar": self.avatar,
|
|
48
|
+
"mobile": self.mobile,
|
|
49
|
+
"email": self.email,
|
|
50
|
+
"department": self.department,
|
|
51
|
+
"title": self.title,
|
|
52
|
+
"is_active": self.is_active,
|
|
53
|
+
"is_admin": self.is_admin,
|
|
54
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
55
|
+
"last_login": self.last_login.isoformat() if self.last_login else None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OperationLog(Base):
|
|
60
|
+
"""操作日志表"""
|
|
61
|
+
__tablename__ = "operation_logs"
|
|
62
|
+
|
|
63
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
64
|
+
|
|
65
|
+
# 用户信息
|
|
66
|
+
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, comment="用户ID")
|
|
67
|
+
user_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="用户名称(冗余存储)")
|
|
68
|
+
|
|
69
|
+
# 操作信息
|
|
70
|
+
action: Mapped[str] = mapped_column(String(64), index=True, comment="操作类型")
|
|
71
|
+
resource: Mapped[str] = mapped_column(String(128), comment="操作资源")
|
|
72
|
+
resource_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="资源ID")
|
|
73
|
+
detail: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="操作详情")
|
|
74
|
+
|
|
75
|
+
# 请求信息
|
|
76
|
+
method: Mapped[Optional[str]] = mapped_column(String(10), nullable=True, comment="请求方法")
|
|
77
|
+
path: Mapped[Optional[str]] = mapped_column(String(256), nullable=True, comment="请求路径")
|
|
78
|
+
ip_address: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="IP地址")
|
|
79
|
+
user_agent: Mapped[Optional[str]] = mapped_column(String(512), nullable=True, comment="浏览器UA")
|
|
80
|
+
|
|
81
|
+
# 结果
|
|
82
|
+
status: Mapped[str] = mapped_column(String(16), default="success", comment="操作状态: success/failed")
|
|
83
|
+
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="错误信息")
|
|
84
|
+
|
|
85
|
+
# 时间戳
|
|
86
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, index=True, comment="操作时间")
|
|
87
|
+
|
|
88
|
+
# 关联
|
|
89
|
+
user: Mapped[Optional["User"]] = relationship("User", back_populates="operation_logs")
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict:
|
|
92
|
+
"""转换为字典"""
|
|
93
|
+
return {
|
|
94
|
+
"id": self.id,
|
|
95
|
+
"user_id": self.user_id,
|
|
96
|
+
"user_name": self.user_name,
|
|
97
|
+
"action": self.action,
|
|
98
|
+
"resource": self.resource,
|
|
99
|
+
"resource_id": self.resource_id,
|
|
100
|
+
"detail": self.detail,
|
|
101
|
+
"method": self.method,
|
|
102
|
+
"path": self.path,
|
|
103
|
+
"ip_address": self.ip_address,
|
|
104
|
+
"status": self.status,
|
|
105
|
+
"error_message": self.error_message,
|
|
106
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
107
|
+
}
|