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/dingtalk.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""钉钉API客户端"""
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DingTalkClient:
|
|
12
|
+
"""钉钉开放平台API客户端"""
|
|
13
|
+
|
|
14
|
+
# API基础地址
|
|
15
|
+
BASE_URL = "https://oapi.dingtalk.com"
|
|
16
|
+
NEW_API_URL = "https://api.dingtalk.com"
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
app_key: str,
|
|
21
|
+
app_secret: str,
|
|
22
|
+
corp_id: str = "",
|
|
23
|
+
agent_id: str = ""
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
初始化钉钉客户端
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
app_key: 应用AppKey
|
|
30
|
+
app_secret: 应用AppSecret
|
|
31
|
+
corp_id: 企业CorpId(工作台免登需要)
|
|
32
|
+
agent_id: 应用AgentId(工作台免登需要)
|
|
33
|
+
"""
|
|
34
|
+
self.app_key = app_key
|
|
35
|
+
self.app_secret = app_secret
|
|
36
|
+
self.corp_id = corp_id
|
|
37
|
+
self.agent_id = agent_id
|
|
38
|
+
|
|
39
|
+
# Access Token缓存
|
|
40
|
+
self._access_token: Optional[str] = None
|
|
41
|
+
self._token_expire_time: float = 0
|
|
42
|
+
|
|
43
|
+
# HTTP客户端
|
|
44
|
+
self._client = httpx.AsyncClient(timeout=30.0)
|
|
45
|
+
|
|
46
|
+
async def close(self):
|
|
47
|
+
"""关闭HTTP客户端"""
|
|
48
|
+
await self._client.aclose()
|
|
49
|
+
|
|
50
|
+
async def get_access_token(self) -> str:
|
|
51
|
+
"""
|
|
52
|
+
获取企业内部应用的Access Token
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Access Token字符串
|
|
56
|
+
"""
|
|
57
|
+
# 检查缓存
|
|
58
|
+
if self._access_token and time.time() < self._token_expire_time - 60:
|
|
59
|
+
return self._access_token
|
|
60
|
+
|
|
61
|
+
# 请求新Token
|
|
62
|
+
url = f"{self.BASE_URL}/gettoken"
|
|
63
|
+
params = {
|
|
64
|
+
"appkey": self.app_key,
|
|
65
|
+
"appsecret": self.app_secret
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
response = await self._client.get(url, params=params)
|
|
70
|
+
data = response.json()
|
|
71
|
+
|
|
72
|
+
if data.get("errcode") == 0:
|
|
73
|
+
self._access_token = data["access_token"]
|
|
74
|
+
# Token有效期7200秒
|
|
75
|
+
self._token_expire_time = time.time() + data.get("expires_in", 7200)
|
|
76
|
+
logger.info("获取钉钉Access Token成功")
|
|
77
|
+
return self._access_token
|
|
78
|
+
else:
|
|
79
|
+
logger.error(f"获取钉钉Access Token失败: {data}")
|
|
80
|
+
raise Exception(f"获取Access Token失败: {data.get('errmsg')}")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"请求钉钉API异常: {e}")
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
async def get_user_info_by_code(self, auth_code: str) -> Dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
通过授权码获取用户信息(新版OAuth2扫码登录)
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
auth_code: 授权码
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
用户信息字典
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# 1. 通过授权码获取用户access_token(新版OAuth2 API)
|
|
97
|
+
url = f"{self.NEW_API_URL}/v1.0/oauth2/userAccessToken"
|
|
98
|
+
body = {
|
|
99
|
+
"clientId": self.app_key,
|
|
100
|
+
"clientSecret": self.app_secret,
|
|
101
|
+
"code": auth_code,
|
|
102
|
+
"grantType": "authorization_code"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
response = await self._client.post(url, json=body)
|
|
106
|
+
data = response.json()
|
|
107
|
+
|
|
108
|
+
if "accessToken" not in data:
|
|
109
|
+
logger.error(f"获取用户Token失败: {data}")
|
|
110
|
+
raise Exception(f"获取用户Token失败: {data.get('message', data)}")
|
|
111
|
+
|
|
112
|
+
user_access_token = data["accessToken"]
|
|
113
|
+
logger.info("获取用户access_token成功")
|
|
114
|
+
|
|
115
|
+
# 2. 使用用户access_token获取用户信息
|
|
116
|
+
user_info_url = f"{self.NEW_API_URL}/v1.0/contact/users/me"
|
|
117
|
+
headers = {"x-acs-dingtalk-access-token": user_access_token}
|
|
118
|
+
|
|
119
|
+
user_response = await self._client.get(user_info_url, headers=headers)
|
|
120
|
+
user_data = user_response.json()
|
|
121
|
+
|
|
122
|
+
if "unionId" not in user_data:
|
|
123
|
+
logger.error(f"获取用户信息失败: {user_data}")
|
|
124
|
+
raise Exception(f"获取用户信息失败: {user_data.get('message', user_data)}")
|
|
125
|
+
|
|
126
|
+
unionid = user_data.get("unionId")
|
|
127
|
+
logger.info(f"获取用户信息成功: unionId={unionid}")
|
|
128
|
+
|
|
129
|
+
# 3. 通过unionId获取企业内用户userid
|
|
130
|
+
userid = await self.get_userid_by_unionid(unionid)
|
|
131
|
+
|
|
132
|
+
if userid:
|
|
133
|
+
# 获取企业内用户详细信息
|
|
134
|
+
user_detail = await self.get_user_detail(userid)
|
|
135
|
+
return {
|
|
136
|
+
"userid": userid,
|
|
137
|
+
"unionid": unionid,
|
|
138
|
+
"name": user_detail.get("name", user_data.get("nick", "")),
|
|
139
|
+
"avatar": user_detail.get("avatar", user_data.get("avatarUrl", "")),
|
|
140
|
+
"mobile": user_detail.get("mobile", user_data.get("mobile", "")),
|
|
141
|
+
"email": user_detail.get("email", user_data.get("email", "")),
|
|
142
|
+
"department": self._format_department(user_detail.get("dept_id_list", [])),
|
|
143
|
+
"title": user_detail.get("title", ""),
|
|
144
|
+
}
|
|
145
|
+
else:
|
|
146
|
+
# 非企业内用户,使用OAuth返回的基本信息
|
|
147
|
+
return {
|
|
148
|
+
"userid": user_data.get("openId", unionid),
|
|
149
|
+
"unionid": unionid,
|
|
150
|
+
"name": user_data.get("nick", ""),
|
|
151
|
+
"avatar": user_data.get("avatarUrl", ""),
|
|
152
|
+
"mobile": user_data.get("mobile", ""),
|
|
153
|
+
"email": user_data.get("email", ""),
|
|
154
|
+
"department": "",
|
|
155
|
+
"title": "",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.error(f"通过授权码获取用户信息异常: {e}")
|
|
160
|
+
raise
|
|
161
|
+
|
|
162
|
+
async def get_user_info_by_code_internal(self, auth_code: str) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
通过免登授权码获取用户信息(企业内部应用/H5免登)
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
auth_code: 免登授权码(通过钉钉JSAPI获取)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
用户信息字典
|
|
171
|
+
"""
|
|
172
|
+
access_token = await self.get_access_token()
|
|
173
|
+
|
|
174
|
+
# 通过免登授权码获取用户userid
|
|
175
|
+
url = f"{self.BASE_URL}/topapi/v2/user/getuserinfo"
|
|
176
|
+
params = {"access_token": access_token}
|
|
177
|
+
body = {"code": auth_code}
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
response = await self._client.post(url, params=params, json=body)
|
|
181
|
+
data = response.json()
|
|
182
|
+
|
|
183
|
+
if data.get("errcode") != 0:
|
|
184
|
+
logger.error(f"获取用户信息失败: {data}")
|
|
185
|
+
raise Exception(f"获取用户信息失败: {data.get('errmsg')}")
|
|
186
|
+
|
|
187
|
+
result = data.get("result", {})
|
|
188
|
+
userid = result.get("userid")
|
|
189
|
+
unionid = result.get("unionid")
|
|
190
|
+
|
|
191
|
+
if not userid:
|
|
192
|
+
raise Exception("未获取到用户userid")
|
|
193
|
+
|
|
194
|
+
# 获取用户详细信息
|
|
195
|
+
user_detail = await self.get_user_detail(userid)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"userid": userid,
|
|
199
|
+
"unionid": unionid,
|
|
200
|
+
"name": user_detail.get("name", ""),
|
|
201
|
+
"avatar": user_detail.get("avatar", ""),
|
|
202
|
+
"mobile": user_detail.get("mobile", ""),
|
|
203
|
+
"email": user_detail.get("email", ""),
|
|
204
|
+
"department": self._format_department(user_detail.get("dept_id_list", [])),
|
|
205
|
+
"title": user_detail.get("title", ""),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(f"通过免登授权码获取用户信息异常: {e}")
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
async def get_user_detail(self, userid: str) -> Dict[str, Any]:
|
|
213
|
+
"""
|
|
214
|
+
获取用户详细信息
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
userid: 用户ID
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
用户详细信息
|
|
221
|
+
"""
|
|
222
|
+
access_token = await self.get_access_token()
|
|
223
|
+
|
|
224
|
+
url = f"{self.BASE_URL}/topapi/v2/user/get"
|
|
225
|
+
params = {"access_token": access_token}
|
|
226
|
+
body = {"userid": userid}
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
response = await self._client.post(url, params=params, json=body)
|
|
230
|
+
data = response.json()
|
|
231
|
+
|
|
232
|
+
if data.get("errcode") != 0:
|
|
233
|
+
logger.error(f"获取用户详情失败: {data}")
|
|
234
|
+
raise Exception(f"获取用户详情失败: {data.get('errmsg')}")
|
|
235
|
+
|
|
236
|
+
return data.get("result", {})
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(f"获取用户详情异常: {e}")
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
async def get_userid_by_unionid(self, unionid: str) -> Optional[str]:
|
|
243
|
+
"""
|
|
244
|
+
通过UnionId获取UserId
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
unionid: 用户UnionId
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
用户UserId
|
|
251
|
+
"""
|
|
252
|
+
access_token = await self.get_access_token()
|
|
253
|
+
|
|
254
|
+
url = f"{self.BASE_URL}/topapi/user/getbyunionid"
|
|
255
|
+
params = {"access_token": access_token}
|
|
256
|
+
body = {"unionid": unionid}
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
response = await self._client.post(url, params=params, json=body)
|
|
260
|
+
data = response.json()
|
|
261
|
+
|
|
262
|
+
if data.get("errcode") != 0:
|
|
263
|
+
logger.warning(f"通过UnionId获取UserId失败: {data}")
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
return data.get("result", {}).get("userid")
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"通过UnionId获取UserId异常: {e}")
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
async def get_jsapi_ticket(self) -> str:
|
|
273
|
+
"""
|
|
274
|
+
获取JSAPI Ticket(H5页面调用钉钉JS API需要)
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
JSAPI Ticket
|
|
278
|
+
"""
|
|
279
|
+
access_token = await self.get_access_token()
|
|
280
|
+
|
|
281
|
+
url = f"{self.BASE_URL}/get_jsapi_ticket"
|
|
282
|
+
params = {"access_token": access_token}
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
response = await self._client.get(url, params=params)
|
|
286
|
+
data = response.json()
|
|
287
|
+
|
|
288
|
+
if data.get("errcode") != 0:
|
|
289
|
+
logger.error(f"获取JSAPI Ticket失败: {data}")
|
|
290
|
+
raise Exception(f"获取JSAPI Ticket失败: {data.get('errmsg')}")
|
|
291
|
+
|
|
292
|
+
return data.get("ticket", "")
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f"获取JSAPI Ticket异常: {e}")
|
|
296
|
+
raise
|
|
297
|
+
|
|
298
|
+
def _format_department(self, dept_ids: list) -> str:
|
|
299
|
+
"""格式化部门信息"""
|
|
300
|
+
if not dept_ids:
|
|
301
|
+
return ""
|
|
302
|
+
# 简单返回部门ID列表,实际可以查询部门名称
|
|
303
|
+
return ",".join(str(d) for d in dept_ids)
|
|
304
|
+
|
|
305
|
+
def generate_qrcode_url(self, redirect_uri: str, state: str = "") -> str:
|
|
306
|
+
"""
|
|
307
|
+
生成钉钉扫码登录URL(新版OAuth2)
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
redirect_uri: 回调地址
|
|
311
|
+
state: 状态参数
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
扫码登录URL
|
|
315
|
+
"""
|
|
316
|
+
import urllib.parse
|
|
317
|
+
|
|
318
|
+
params = {
|
|
319
|
+
"client_id": self.app_key,
|
|
320
|
+
"response_type": "code",
|
|
321
|
+
"scope": "openid",
|
|
322
|
+
"redirect_uri": redirect_uri,
|
|
323
|
+
"state": state or "dingtalk_login",
|
|
324
|
+
"prompt": "consent"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
query = urllib.parse.urlencode(params)
|
|
328
|
+
return f"https://login.dingtalk.com/oauth2/auth?{query}"
|
|
329
|
+
|
|
330
|
+
def generate_h5_auth_url(self, redirect_uri: str, state: str = "") -> str:
|
|
331
|
+
"""
|
|
332
|
+
生成H5页面授权URL(用于钉钉内H5应用)
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
redirect_uri: 回调地址
|
|
336
|
+
state: 状态参数
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
H5授权URL
|
|
340
|
+
"""
|
|
341
|
+
import urllib.parse
|
|
342
|
+
|
|
343
|
+
params = {
|
|
344
|
+
"appid": self.app_key,
|
|
345
|
+
"response_type": "code",
|
|
346
|
+
"scope": "snsapi_auth",
|
|
347
|
+
"redirect_uri": redirect_uri,
|
|
348
|
+
"state": state or "dingtalk_h5"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
query = urllib.parse.urlencode(params)
|
|
352
|
+
return f"https://login.dingtalk.com/oauth2/auth?{query}"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# 全局客户端实例
|
|
356
|
+
_dingtalk_client: Optional[DingTalkClient] = None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def init_dingtalk_client(
|
|
360
|
+
app_key: str,
|
|
361
|
+
app_secret: str,
|
|
362
|
+
corp_id: str = "",
|
|
363
|
+
agent_id: str = ""
|
|
364
|
+
) -> DingTalkClient:
|
|
365
|
+
"""初始化全局钉钉客户端"""
|
|
366
|
+
global _dingtalk_client
|
|
367
|
+
_dingtalk_client = DingTalkClient(app_key, app_secret, corp_id, agent_id)
|
|
368
|
+
return _dingtalk_client
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def get_dingtalk_client() -> Optional[DingTalkClient]:
|
|
372
|
+
"""获取全局钉钉客户端"""
|
|
373
|
+
return _dingtalk_client
|
src/auth/jwt_handler.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""JWT Token 处理"""
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
from jose import jwt, JWTError
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JWTHandler:
|
|
12
|
+
"""JWT Token 处理器"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
secret_key: str,
|
|
17
|
+
algorithm: str = "HS256",
|
|
18
|
+
expire_hours: int = 24
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
初始化JWT处理器
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
secret_key: 密钥
|
|
25
|
+
algorithm: 加密算法
|
|
26
|
+
expire_hours: Token过期时间(小时)
|
|
27
|
+
"""
|
|
28
|
+
self.secret_key = secret_key
|
|
29
|
+
self.algorithm = algorithm
|
|
30
|
+
self.expire_hours = expire_hours
|
|
31
|
+
|
|
32
|
+
def create_token(
|
|
33
|
+
self,
|
|
34
|
+
user_id: int,
|
|
35
|
+
dingtalk_userid: str,
|
|
36
|
+
name: str,
|
|
37
|
+
extra_data: Optional[Dict[str, Any]] = None
|
|
38
|
+
) -> str:
|
|
39
|
+
"""
|
|
40
|
+
创建JWT Token
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
user_id: 用户ID
|
|
44
|
+
dingtalk_userid: 钉钉用户ID
|
|
45
|
+
name: 用户名称
|
|
46
|
+
extra_data: 额外数据
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
JWT Token字符串
|
|
50
|
+
"""
|
|
51
|
+
expire = datetime.utcnow() + timedelta(hours=self.expire_hours)
|
|
52
|
+
|
|
53
|
+
payload = {
|
|
54
|
+
"sub": str(user_id),
|
|
55
|
+
"dingtalk_userid": dingtalk_userid,
|
|
56
|
+
"name": name,
|
|
57
|
+
"exp": expire,
|
|
58
|
+
"iat": datetime.utcnow(),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if extra_data:
|
|
62
|
+
payload.update(extra_data)
|
|
63
|
+
|
|
64
|
+
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
65
|
+
return token
|
|
66
|
+
|
|
67
|
+
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
|
|
68
|
+
"""
|
|
69
|
+
验证JWT Token
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
token: JWT Token字符串
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
解码后的payload,验证失败返回None
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
payload = jwt.decode(
|
|
79
|
+
token,
|
|
80
|
+
self.secret_key,
|
|
81
|
+
algorithms=[self.algorithm]
|
|
82
|
+
)
|
|
83
|
+
return payload
|
|
84
|
+
except JWTError as e:
|
|
85
|
+
logger.warning(f"JWT验证失败: {e}")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def get_user_id(self, token: str) -> Optional[int]:
|
|
89
|
+
"""
|
|
90
|
+
从Token中获取用户ID
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
token: JWT Token字符串
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
用户ID,验证失败返回None
|
|
97
|
+
"""
|
|
98
|
+
payload = self.verify_token(token)
|
|
99
|
+
if payload and "sub" in payload:
|
|
100
|
+
try:
|
|
101
|
+
return int(payload["sub"])
|
|
102
|
+
except (ValueError, TypeError):
|
|
103
|
+
return None
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def refresh_token(self, token: str) -> Optional[str]:
|
|
107
|
+
"""
|
|
108
|
+
刷新Token(延长过期时间)
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
token: 原JWT Token
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
新的JWT Token,验证失败返回None
|
|
115
|
+
"""
|
|
116
|
+
payload = self.verify_token(token)
|
|
117
|
+
if not payload:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# 创建新Token
|
|
121
|
+
return self.create_token(
|
|
122
|
+
user_id=int(payload["sub"]),
|
|
123
|
+
dingtalk_userid=payload.get("dingtalk_userid", ""),
|
|
124
|
+
name=payload.get("name", ""),
|
|
125
|
+
extra_data={
|
|
126
|
+
k: v for k, v in payload.items()
|
|
127
|
+
if k not in ("sub", "dingtalk_userid", "name", "exp", "iat")
|
|
128
|
+
}
|
|
129
|
+
)
|