Compare commits
10 Commits
0418f913c7
...
e37d807d20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e37d807d20 | ||
|
|
904c20bb62 | ||
|
|
0b59bc4a0a | ||
|
|
41a2bd9358 | ||
|
|
1f7dedf866 | ||
|
|
537a3d49e1 | ||
|
|
1d8bf58014 | ||
|
|
b476e034dc | ||
|
|
8ad2ec91f1 | ||
|
|
a0b4d2d44e |
@@ -476,6 +476,65 @@ def api_login_qr_status():
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@robot_bp.route('/api/login_qr_mode', methods=['POST'])
|
||||
@login_required
|
||||
def api_switch_login_qr_mode():
|
||||
"""切换 864 登录二维码模式,并重启登录流程。"""
|
||||
try:
|
||||
server = current_app.dashboard_server
|
||||
robot = getattr(server, "robot", None)
|
||||
if robot is None:
|
||||
return jsonify({"success": False, "error": "机器人实例不可用"}), 500
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
login_qr_api = str(payload.get("login_qr_api", "") or "").strip()
|
||||
login_way = str(payload.get("login_way", "") or "").strip()
|
||||
result = robot.switch_server_864_login_entry(
|
||||
login_qr_api=login_qr_api,
|
||||
login_way=login_way or None,
|
||||
# 切换二维码模式只调整后续登录入口,不主动踢掉服务端当前在线会话:
|
||||
# 1. 用户已经明确要求“服务端已登录时,除非手动执行,否则不要做 logout”;
|
||||
# 2. 这里改为仅重启本地 provider 线程,让 ABOT 重新接管当前会话或等待下次手动退出后再使用新模式;
|
||||
# 3. 真正的远端退出动作只保留在 `/api/logout_864`,语义更清晰。
|
||||
do_logout=False,
|
||||
)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "864 登录二维码模式已切换,新的登录线程正在启动",
|
||||
"data": {
|
||||
**result,
|
||||
"login_qr_state": _serialize_login_qr_state(server),
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
LOG.error(f"切换 864 登录二维码模式失败: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@robot_bp.route('/api/logout_864', methods=['POST'])
|
||||
@login_required
|
||||
def api_logout_864():
|
||||
"""退出当前 864 登录态,并重新进入二维码登录引导。"""
|
||||
try:
|
||||
server = current_app.dashboard_server
|
||||
robot = getattr(server, "robot", None)
|
||||
if robot is None:
|
||||
return jsonify({"success": False, "error": "机器人实例不可用"}), 500
|
||||
|
||||
result = robot.logout_server_864_and_restart_login()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "864 已退出当前登录态,新的二维码登录流程正在启动",
|
||||
"data": {
|
||||
**result,
|
||||
"login_qr_state": _serialize_login_qr_state(server),
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
LOG.error(f"退出 864 登录态失败: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# API路由
|
||||
@robot_bp.route('/api/groups')
|
||||
@login_required
|
||||
|
||||
@@ -124,12 +124,13 @@
|
||||
|
||||
<el-dialog
|
||||
title="微信登录二维码"
|
||||
:visible.sync="loginQrDialog.visible"
|
||||
:visible="shouldShowLoginQrDialog"
|
||||
width="760px"
|
||||
class="login-qr-dialog"
|
||||
:show-close="false"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false">
|
||||
:close-on-press-escape="false"
|
||||
@update:visible="handleLoginQrDialogVisibleChange">
|
||||
<div class="login-qr-dialog__body" v-loading="loginQrDialog.loading">
|
||||
<div class="login-qr-dialog__hero">
|
||||
<div class="login-qr-dialog__preview">
|
||||
@@ -171,6 +172,10 @@
|
||||
<span>UUID</span>
|
||||
<strong>{% raw %}{{ loginQrCurrent.uuid || '-' }}{% endraw %}</strong>
|
||||
</div>
|
||||
<div class="login-qr-dialog__meta-item">
|
||||
<span>登录方式</span>
|
||||
<strong>{% raw %}{{ loginQrWayText }}{% endraw %}</strong>
|
||||
</div>
|
||||
<div class="login-qr-dialog__meta-item">
|
||||
<span>原始状态</span>
|
||||
<strong>{% raw %}{{ formatLoginQrRawState(loginQrCurrent.raw_state) }}{% endraw %}</strong>
|
||||
@@ -182,6 +187,28 @@
|
||||
</div>
|
||||
<div class="login-qr-dialog__actions">
|
||||
<el-button size="mini" @click="loadLoginQrStatus(true)">立即刷新状态</el-button>
|
||||
<el-button
|
||||
v-if="isServer864Login"
|
||||
size="mini"
|
||||
:loading="loginQrActionLoading === 'harmony_api'"
|
||||
@click="switch864LoginMode('harmony_api', 'harmony')">
|
||||
鸿蒙二维码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isServer864Login"
|
||||
size="mini"
|
||||
:loading="loginQrActionLoading === 'new'"
|
||||
@click="switch864LoginMode('new')">
|
||||
标准New二维码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isServer864Login"
|
||||
size="mini"
|
||||
type="danger"
|
||||
:loading="loginQrActionLoading === 'logout_864'"
|
||||
@click="logout864Login">
|
||||
退出864登录
|
||||
</el-button>
|
||||
<el-button v-if="loginQrCurrent.scan_url" type="text" @click="copyLoginQrScanUrl">
|
||||
复制扫码链接
|
||||
</el-button>
|
||||
@@ -581,6 +608,7 @@
|
||||
server_now: 0
|
||||
},
|
||||
loginQrCountdownSeconds: 0,
|
||||
loginQrActionLoading: '',
|
||||
groups: [],
|
||||
selectedGroupForHourlyTrend: '',
|
||||
hourlyTrendDays: 1,
|
||||
@@ -617,9 +645,23 @@
|
||||
loginQrCurrent() {
|
||||
return this.loginQrDialog.current || {};
|
||||
},
|
||||
isServer864Login() {
|
||||
const providerName = String(this.loginQrDialog.provider_name || '').trim().toLowerCase();
|
||||
return providerName === 'server_864' || providerName === '864';
|
||||
},
|
||||
showLoginQrBanner() {
|
||||
return !this.loginQrDialog.logged_in;
|
||||
},
|
||||
shouldShowLoginQrDialog() {
|
||||
// 首页登录弹窗的展示规则尽量简单且稳定:
|
||||
// 1. 只要当前还没登录成功,就必须强制显示二维码弹窗,避免前端局部状态抖动把弹窗意外关掉;
|
||||
// 2. 登录成功后再退回到本地 visible 控制,兼容后续如果需要保留“手动再次查看”的空间;
|
||||
// 3. 这样能直接满足“没有可用账号,就一直要求扫码登录”的目标,不再依赖多处代码同步开关。
|
||||
if (!this.loginQrDialog.logged_in) {
|
||||
return true;
|
||||
}
|
||||
return !!this.loginQrDialog.visible;
|
||||
},
|
||||
loginQrStatusTone() {
|
||||
if (this.loginQrDialog.provider_stage === 'connection_pending') {
|
||||
return 'soft';
|
||||
@@ -696,6 +738,21 @@
|
||||
}
|
||||
return '登录引导中';
|
||||
},
|
||||
loginQrWayText() {
|
||||
const normalizedWay = String(this.loginQrCurrent.login_way || '').trim().toLowerCase();
|
||||
const wayMap = {
|
||||
mac: 'mac',
|
||||
win: 'win',
|
||||
harmony: 'harmony',
|
||||
car: 'car',
|
||||
watch: 'watch'
|
||||
};
|
||||
// 弹窗里直接显示当前实际使用的登录终端形态:
|
||||
// 1. 864 的二维码接口会根据 `Way` 走不同的登录链路,联调时很容易忘记当前到底用的是哪一档;
|
||||
// 2. 用户已经明确在 `.env` 中切到了 `WECHAT_LOGIN_WAY=harmony`,这里展示出来能立刻确认配置有没有真的生效;
|
||||
// 3. 若后续 provider 回传了别的兼容值,也保留原文,避免前端把未知新值错误折叠掉。
|
||||
return wayMap[normalizedWay] || normalizedWay || '-';
|
||||
},
|
||||
loginQrCountdownText() {
|
||||
if (this.loginQrDialog.logged_in) {
|
||||
return '已登录';
|
||||
@@ -882,6 +939,17 @@
|
||||
openLoginQrDialog() {
|
||||
this.loginQrDialog.visible = true;
|
||||
},
|
||||
handleLoginQrDialogVisibleChange(nextVisible) {
|
||||
// 未登录时弹窗不允许被真正关闭:
|
||||
// 1. Element UI 仍可能在内部触发 `update:visible` 事件;
|
||||
// 2. 如果这里直接接受 false,就会再次出现“业务要求必须常驻,但组件自己收起”的问题;
|
||||
// 3. 因此只有在已登录成功后,才允许同步关闭状态。
|
||||
if (!this.loginQrDialog.logged_in) {
|
||||
this.loginQrDialog.visible = true;
|
||||
return;
|
||||
}
|
||||
this.loginQrDialog.visible = !!nextVisible;
|
||||
},
|
||||
applyLoginQrState(state) {
|
||||
const nextState = state || {};
|
||||
const current = nextState.current || {};
|
||||
@@ -930,6 +998,63 @@
|
||||
this.loginQrDialog.loading = false;
|
||||
});
|
||||
},
|
||||
switch864LoginMode(loginQrApi, loginWay = '') {
|
||||
if (!this.isServer864Login) {
|
||||
this.$message.warning('当前不是 864 登录模式,暂不支持切换二维码入口');
|
||||
return;
|
||||
}
|
||||
this.loginQrActionLoading = loginQrApi;
|
||||
axios.post('/robot/api/login_qr_mode', {
|
||||
login_qr_api: loginQrApi,
|
||||
login_way: loginWay
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const state = (((response.data || {}).data || {}).login_qr_state) || {};
|
||||
if (state && Object.keys(state).length > 0) {
|
||||
this.applyLoginQrState(state);
|
||||
}
|
||||
this.$message.success(response.data.message || '864 登录二维码入口已切换');
|
||||
setTimeout(() => this.loadLoginQrStatus(true), 1200);
|
||||
return;
|
||||
}
|
||||
this.$message.error(response.data.error || '切换 864 登录二维码入口失败');
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMessage = (((error || {}).response || {}).data || {}).error || '切换 864 登录二维码入口失败';
|
||||
this.$message.error(errorMessage);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loginQrActionLoading = '';
|
||||
});
|
||||
},
|
||||
logout864Login() {
|
||||
if (!this.isServer864Login) {
|
||||
this.$message.warning('当前不是 864 登录模式,暂不支持此操作');
|
||||
return;
|
||||
}
|
||||
this.loginQrActionLoading = 'logout_864';
|
||||
axios.post('/robot/api/logout_864')
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const state = (((response.data || {}).data || {}).login_qr_state) || {};
|
||||
if (state && Object.keys(state).length > 0) {
|
||||
this.applyLoginQrState(state);
|
||||
}
|
||||
this.$message.success(response.data.message || '864 已退出当前登录态');
|
||||
setTimeout(() => this.loadLoginQrStatus(true), 1200);
|
||||
return;
|
||||
}
|
||||
this.$message.error(response.data.error || '退出 864 登录态失败');
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMessage = (((error || {}).response || {}).data || {}).error || '退出 864 登录态失败';
|
||||
this.$message.error(errorMessage);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loginQrActionLoading = '';
|
||||
});
|
||||
},
|
||||
tickLoginQrCountdown() {
|
||||
if (this.loginQrDialog.logged_in) {
|
||||
this.loginQrCountdownSeconds = 0;
|
||||
|
||||
141
robot.py
141
robot.py
@@ -525,8 +525,13 @@ class Robot:
|
||||
provider_stage = str(self.ipad_login_qr_state.get("provider_stage", "bootstrap") or "bootstrap").strip()
|
||||
connection_ready = bool(self.ipad_login_qr_state.get("connection_ready", False))
|
||||
login_required = bool(self.ipad_login_qr_state.get("login_required", False))
|
||||
# 首页弹窗是否关闭,必须优先以“当前二维码运行态”本身为准:
|
||||
# 1. 之前这里把 `self.wxid` 也算进了 logged_in,导致旧身份缓存会把新一轮扫码流程误判成“已经登录”;
|
||||
# 2. 864 收口失败后虽然 provider 已经回到扫码引导态,但 Robot 上残留的旧 wxid 仍会让前端直接收起弹窗;
|
||||
# 3. 因此这里收紧为只认当前二维码状态机明确给出的 logged_in / confirmed 信号,不再混入历史缓存身份。
|
||||
logged_in = login_state_flag or (qr_status in {"confirmed", "logged_in"} and not login_required)
|
||||
state = {
|
||||
"logged_in": bool(self.wxid) or login_state_flag or qr_status in {"confirmed", "logged_in"},
|
||||
"logged_in": logged_in,
|
||||
"active": bool(self.ipad_login_qr_state.get("active", False)),
|
||||
"status": qr_status,
|
||||
"provider_name": provider_name,
|
||||
@@ -577,6 +582,12 @@ class Robot:
|
||||
head_img_url = str((payload or {}).get("head_img_url", "") or "").strip()
|
||||
expires_in = (payload or {}).get("expires_in")
|
||||
expires_in = None if expires_in in (None, "") else max(0, int(expires_in))
|
||||
# 一旦重新进入扫码链路,就主动清掉 Robot 侧残留的账号身份:
|
||||
# 1. 864 登录收口失败时,provider 已明确告诉前端“当前没有可用账号”,此时继续保留旧 wxid 会误导首页状态;
|
||||
# 2. Dashboard 的当前账号、健康卡片、二维码弹窗都共享这份运行态缓存,必须确保未登录时不再展示历史账号;
|
||||
# 3. 这里只在非成功态下清空,不影响真正登录完成后的账号信息展示。
|
||||
if status not in {"confirmed", "logged_in"}:
|
||||
self._clear_ipad_identity_cache()
|
||||
current_record = {
|
||||
"uuid": uuid_value,
|
||||
"scan_url": scan_url,
|
||||
@@ -615,6 +626,24 @@ class Robot:
|
||||
"updated_at": now_ts,
|
||||
}
|
||||
|
||||
def _clear_ipad_identity_cache(self) -> None:
|
||||
"""清理 Robot 侧缓存的 wechat 账号身份,避免未登录时误显示旧号。"""
|
||||
self.wxid = ""
|
||||
self.nickname = ""
|
||||
self.alias = ""
|
||||
self.phone = ""
|
||||
self.signature = ""
|
||||
# `ipad_bot` 可能还未初始化完成,因此这里做一次存在性保护:
|
||||
# 1. 登录引导状态会在 provider 创建早期就被推给 Dashboard;
|
||||
# 2. 这时 bot 对象不一定已经完整可用,不能因为清理缓存反而引入新的 AttributeError;
|
||||
# 3. 所以只在对象存在时尽量同步清空,做成纯兜底动作。
|
||||
if self.ipad_bot is not None:
|
||||
self.ipad_bot.wxid = ""
|
||||
self.ipad_bot.nickname = ""
|
||||
self.ipad_bot.alias = ""
|
||||
self.ipad_bot.phone = ""
|
||||
self.ipad_bot.signature = ""
|
||||
|
||||
async def _handle_ipad_login_qr_cleared(self, payload: dict | None = None) -> None:
|
||||
"""在登录完成或识别到已有登录态后关闭首页二维码引导。"""
|
||||
now_ts = time.time()
|
||||
@@ -652,6 +681,13 @@ class Robot:
|
||||
2. provider 不应该知道本项目有哪些数据库表、后台缓存或插件系统;
|
||||
3. 因此登录“流程”放到 provider,登录后的“业务初始化”继续留在 Robot。
|
||||
"""
|
||||
# 这里再做一次项目侧兜底校验:
|
||||
# 1. provider 已经会尽量保证只有“拿到可用身份”才会调进来;
|
||||
# 2. 但 Robot 这一层承接的是联系人同步、插件注入、消息归档等重业务动作,不能接受空账号继续执行;
|
||||
# 3. 因此只要 `wxid/nickname` 都为空,就立刻阻断后台初始化,强制回到扫码登录流程。
|
||||
if not str(login_identity.get("wxid", "") or "").strip() and not str(login_identity.get("nickname", "") or "").strip():
|
||||
raise RuntimeError("当前未拿到可用登录账号身份,已阻止进入后台初始化流程")
|
||||
|
||||
self.wxid = login_identity.get("wxid", "")
|
||||
self.nickname = login_identity.get("nickname", "")
|
||||
self.alias = login_identity.get("alias", "")
|
||||
@@ -891,9 +927,110 @@ class Robot:
|
||||
if hasattr(self, "ipad_bot") and self.ipad_bot and hasattr(self.ipad_bot, "stop_runtime"):
|
||||
self.ipad_bot.stop_runtime()
|
||||
if self.ipad_loop:
|
||||
self.ipad_loop.stop()
|
||||
try:
|
||||
# 事件循环运行在独立线程里,主线程这里需要走线程安全停止:
|
||||
# 1. 直接 `loop.stop()` 在某些时机会留下竞态,导致旧线程迟迟不退出;
|
||||
# 2. Dashboard 现在需要支持“不重启主进程,直接切换 864 登录模式”;
|
||||
# 3. 因此这里改成 `call_soon_threadsafe`,让旧 provider 能更稳地收尾退出。
|
||||
self.ipad_loop.call_soon_threadsafe(self.ipad_loop.stop)
|
||||
except Exception:
|
||||
self.ipad_loop.stop()
|
||||
if self.ipad_thread and self.ipad_thread.is_alive():
|
||||
self.ipad_thread.join(timeout=5)
|
||||
self.ipad_thread = None
|
||||
self.ipad_loop = None
|
||||
self.ipad_bot = None
|
||||
self.LOG.info("wechat_ipad客户端已停止")
|
||||
|
||||
def switch_server_864_login_entry(self, *, login_qr_api: str, login_way: str | None = None, do_logout: bool = True) -> dict:
|
||||
"""切换 864 登录入口并重启登录流程。
|
||||
|
||||
设计说明:
|
||||
1. 用户希望在后台直接切换“鸿蒙专用二维码”和“标准 New 二维码”,而不是改 `.env` 后重启整套服务;
|
||||
2. 当前 864 runtime 在独立线程里长驻运行,最稳妥的切换方式是“更新配置 -> 退出旧登录态 -> 重启 provider 登录线程”;
|
||||
3. 这样实现虽然比纯运行时热切换更直接,但代码层次更浅,也更便于后续继续接其他 server 变体。
|
||||
"""
|
||||
if not isinstance(self.ipad_config, dict):
|
||||
raise RuntimeError("wechat_ipad 运行时配置尚未初始化,暂时无法切换 864 登录入口")
|
||||
|
||||
current_server_type = self._normalize_wechat_provider_key(self.ipad_config.get("server_type", "legacy_855"))
|
||||
if current_server_type != "server_864":
|
||||
raise RuntimeError("当前仅支持在 server_864 模式下切换登录二维码入口")
|
||||
|
||||
normalized_login_qr_api = str(login_qr_api or "").strip().lower()
|
||||
if normalized_login_qr_api not in {"harmony_api", "new", "new_x"}:
|
||||
raise ValueError(f"不支持的 864 登录入口模式: {login_qr_api}")
|
||||
|
||||
normalized_login_way = str(login_way or self.ipad_config.get("login_way", "mac") or "mac").strip().lower()
|
||||
if normalized_login_qr_api == "harmony_api":
|
||||
normalized_login_way = "harmony"
|
||||
|
||||
if do_logout and self.ipad_bot and self.ipad_loop:
|
||||
try:
|
||||
logout_future = asyncio.run_coroutine_threadsafe(self.ipad_bot.log_out(), self.ipad_loop)
|
||||
logout_future.result(timeout=20)
|
||||
except Exception as e:
|
||||
# 切换登录模式时,退出旧会话失败不应阻断后续重启:
|
||||
# 1. 某些 864 版本在会话已失效时会直接返回错误;
|
||||
# 2. 用户真正关心的是“后台能否尽快切到新的二维码入口”;
|
||||
# 3. 因此这里记录日志后继续向下执行重启流程。
|
||||
self.LOG.warning(f"切换 864 登录入口前执行退出请求失败,继续重启登录流程: {e}")
|
||||
# `do_logout=False` 时只重启 ABOT 本地 provider,不会主动登出远端在线账号:
|
||||
# 1. 这样切换二维码模式不会误把当前服务端现有会话踢下线;
|
||||
# 2. 若远端本来就已在线,新的 provider 线程会优先尝试接管现有登录态;
|
||||
# 3. 真正的远端退出只保留给手动点击“退出864登录”的显式动作。
|
||||
|
||||
self.ipad_config["login_qr_api"] = normalized_login_qr_api
|
||||
self.ipad_config["login_way"] = normalized_login_way
|
||||
if isinstance(getattr(self.config, "wechat_ipad", None), dict):
|
||||
self.config.wechat_ipad["login_qr_api"] = normalized_login_qr_api
|
||||
self.config.wechat_ipad["login_way"] = normalized_login_way
|
||||
|
||||
self._clear_ipad_identity_cache()
|
||||
switch_label_map = {
|
||||
"harmony_api": "864 鸿蒙专用二维码",
|
||||
"new": "864 标准 New 二维码",
|
||||
"new_x": f"864 NewX 二维码({normalized_login_way})",
|
||||
}
|
||||
with self._ipad_login_qr_lock:
|
||||
self.ipad_login_qr_state = {
|
||||
"logged_in": False,
|
||||
"active": True,
|
||||
"status": "waiting",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "login_required",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"status_text": f"正在切换到{switch_label_map.get(normalized_login_qr_api, normalized_login_qr_api)},请稍候刷新二维码",
|
||||
"current": {
|
||||
"login_qr_api": normalized_login_qr_api,
|
||||
"login_way": normalized_login_way,
|
||||
"updated_at": time.time(),
|
||||
"updated_at_text": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
},
|
||||
"history": [],
|
||||
"updated_at": time.time(),
|
||||
}
|
||||
|
||||
self.stop_wechat_ipad()
|
||||
if not self.init_wechat_ipad():
|
||||
raise RuntimeError("重启 wechat_ipad 登录线程失败,请检查 864 服务端与配置")
|
||||
|
||||
return {
|
||||
"server_type": "server_864",
|
||||
"login_qr_api": normalized_login_qr_api,
|
||||
"login_way": normalized_login_way,
|
||||
"message": switch_label_map.get(normalized_login_qr_api, normalized_login_qr_api),
|
||||
}
|
||||
|
||||
def logout_server_864_and_restart_login(self) -> dict:
|
||||
"""退出当前 864 登录态,并按现有二维码入口重新进入登录引导。"""
|
||||
return self.switch_server_864_login_entry(
|
||||
login_qr_api=str(self.ipad_config.get("login_qr_api", "new_x") or "new_x"),
|
||||
login_way=str(self.ipad_config.get("login_way", "mac") or "mac"),
|
||||
do_logout=True,
|
||||
)
|
||||
|
||||
def keep_running_and_block_process(self) -> None:
|
||||
"""
|
||||
保持机器人运行,不让进程退出
|
||||
|
||||
@@ -64,7 +64,19 @@ class Server864APIClientBase:
|
||||
params=merged_params,
|
||||
json=json_body,
|
||||
) as response:
|
||||
payload = await response.json(content_type=None)
|
||||
try:
|
||||
payload = await response.json(content_type=None)
|
||||
except Exception:
|
||||
raw_text = await response.text()
|
||||
# 864 少数接口会直接返回纯文本而不是标准 DTO:
|
||||
# 1. 当前已在 `LogOut` 链路里碰到这种情况,旧逻辑会先炸 JSON 解析,再把真正状态信息吞掉;
|
||||
# 2. 对 2xx 响应来说,这通常只是“接口风格不统一”,不应该直接视为致命协议错误;
|
||||
# 3. 因此这里统一包成兼容 DTO,保留原始文本给上层按需处理。
|
||||
payload = {
|
||||
"Code": 200 if response.status < 400 else response.status,
|
||||
"Data": None,
|
||||
"Text": str(raw_text or "").strip(),
|
||||
}
|
||||
return self._validate_payload(payload)
|
||||
|
||||
async def _request_data(
|
||||
@@ -96,6 +108,12 @@ class Server864APIClientBase:
|
||||
return payload
|
||||
|
||||
message = str(payload.get("Text") or payload.get("Message") or "server_864 请求失败").strip()
|
||||
# 864 某些登录接口会用非 200 编码表达“当前 key 已经在线”:
|
||||
# 1. 对二维码申请链路来说,这更接近一种状态回执,而不是硬失败;
|
||||
# 2. 如果这里直接抛异常,上层就会把“已经在线”误判成致命错误并退出线程;
|
||||
# 3. 因此先保留 payload 原样放行,让 runtime 再决定是复用现有登录态还是继续走扫码。
|
||||
if "该链接已绑定微信号" in message and "在线状态良好" in message:
|
||||
return payload
|
||||
lowered_message = message.lower()
|
||||
if any(keyword in lowered_message for keyword in ("重新登录", "已退出登录", "离线", "账号需要重新登录")):
|
||||
raise UserLoggedOut(message)
|
||||
|
||||
@@ -7,6 +7,70 @@ from wechat_ipad.providers.server_864.base import Server864APIClientBase
|
||||
class LoginMixin(Server864APIClientBase):
|
||||
"""864 登录相关接口。"""
|
||||
|
||||
@staticmethod
|
||||
def _normalize_online_text(value) -> str:
|
||||
"""把 864 各种登录态字段压平成便于判断的字符串。"""
|
||||
return str(value or "").strip().lower()
|
||||
|
||||
def _is_online_from_login_status_payload(self, data: dict | None) -> bool:
|
||||
"""根据 `GetLoginStatus` 的返回结构判断当前是否在线。
|
||||
|
||||
设计说明:
|
||||
1. 864 不同版本对在线态字段命名并不统一,可能是 `loginState/status/state/isLogin` 中的任意一种;
|
||||
2. 这里集中做一次宽松识别,避免上层 runtime 到处散落同类判断;
|
||||
3. 后续如果 864 新版本再补字段,只需要在这里扩展,不必改多处业务逻辑。
|
||||
"""
|
||||
payload = dict(data or {})
|
||||
normalized_login_state = self._normalize_online_text(
|
||||
self._pick_first(payload, "loginState", "LoginState", "status_text", "statusText")
|
||||
)
|
||||
raw_login_state = self._pick_first(payload, "loginState", "LoginState")
|
||||
normalized_status = self._normalize_online_text(
|
||||
self._pick_first(payload, "status", "Status", "state_text", "stateText")
|
||||
)
|
||||
state_value = self._pick_first(payload, "state", "State")
|
||||
login_flag = self._pick_first(payload, "isLogin", "IsLogin", "online", "Online", "isOnline", "IsOnline")
|
||||
login_err_msg = self._normalize_online_text(
|
||||
self._pick_first(payload, "loginErrMsg", "LoginErrMsg", "msg", "message")
|
||||
)
|
||||
|
||||
if normalized_login_state in {"online", "已登录", "在线"}:
|
||||
return True
|
||||
# 864 的 `GetLoginStatus` 在你当前这版 server 里会直接返回 `loginState: 1`:
|
||||
# 1. 这不是扫码阶段的 `CheckLoginStatus.state`,而是服务端自身维护的登录态枚举;
|
||||
# 2. 之前这里只识别到了字符串 `"online"`,导致明明已经在线却仍被判成未登录;
|
||||
# 3. 这里把常见数字态一并纳入在线判定,避免后续资料拉取与前端显示被卡住。
|
||||
try:
|
||||
normalized_login_state_value = int(raw_login_state or 0)
|
||||
except (TypeError, ValueError):
|
||||
normalized_login_state_value = 0
|
||||
if normalized_login_state_value in {1, 2}:
|
||||
return True
|
||||
if normalized_status in {"online", "已登录", "在线"}:
|
||||
return True
|
||||
if "在线状态良好" in login_err_msg or "账号在线" in login_err_msg:
|
||||
return True
|
||||
if isinstance(login_flag, bool):
|
||||
return login_flag
|
||||
if str(login_flag or "").strip().lower() in {"true", "1", "online"}:
|
||||
return True
|
||||
try:
|
||||
normalized_state_value = int(state_value or 0)
|
||||
except (TypeError, ValueError):
|
||||
normalized_state_value = 0
|
||||
return normalized_state_value in {1, 2}
|
||||
|
||||
def _extract_login_identity_from_status(self, data: dict | None) -> dict:
|
||||
"""从 `GetLoginStatus` 返回中提取尽可能多的账号身份字段。"""
|
||||
payload = dict(data or {})
|
||||
return {
|
||||
"wxid": str(self._pick_first(payload, "wxid", "Wxid", "UserName", "userName") or "").strip(),
|
||||
"nickname": str(self._pick_first(payload, "nick_name", "nickName", "NickName", "nickname") or "").strip(),
|
||||
"alias": str(self._pick_first(payload, "alias", "Alias", "wechatId", "WeChatId") or "").strip(),
|
||||
"phone": str(self._pick_first(payload, "mobile", "Mobile", "phone", "Phone") or "").strip(),
|
||||
"signature": str(self._pick_first(payload, "signature", "Signature") or "").strip(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_login_way(login_way: str) -> str:
|
||||
"""标准化 864 `GetLoginQrCodeNewX` 的 way 参数。"""
|
||||
@@ -57,7 +121,19 @@ class LoginMixin(Server864APIClientBase):
|
||||
normalized_login_qr_api = str(login_qr_api or "new_x").strip().lower()
|
||||
normalized_login_way = self._normalize_login_way(login_way)
|
||||
|
||||
if normalized_login_qr_api in {"new_x", "x", "newx"}:
|
||||
if normalized_login_qr_api in {"harmony_api", "harmony", "harmony_login_api"}:
|
||||
# `HarmonyLoginApi` 是 864 服务端单独暴露的一条鸿蒙二维码链路:
|
||||
# 1. 用户当前希望在 Dashboard 上显式切换“鸿蒙专用二维码”和“标准 New 二维码”;
|
||||
# 2. 该接口的请求模型与 `GetLoginQrCodeNew` 一致,仍然只需要 `Proxy/Check`;
|
||||
# 3. 因此这里单独加一个模式键,不去复用 `new_x + way=harmony`,避免两条链路在运维上混淆。
|
||||
data = await self._request_data(
|
||||
"post",
|
||||
"/login/HarmonyLoginApi",
|
||||
json_body={"Proxy": proxy_value, "Check": False},
|
||||
timeout=30,
|
||||
)
|
||||
uuid, qr_url = self._extract_qr_response(data)
|
||||
elif normalized_login_qr_api in {"new_x", "x", "newx"}:
|
||||
try:
|
||||
# NewX 是当前 864 联调里更完整的一条登录链路:
|
||||
# 1. 它支持 `Way` 指定登录端形态,兼容更多 server 变体;
|
||||
@@ -78,6 +154,14 @@ class LoginMixin(Server864APIClientBase):
|
||||
timeout=30,
|
||||
)
|
||||
uuid, qr_url = self._extract_qr_response(data)
|
||||
elif normalized_login_qr_api in {"new", "legacy_new"}:
|
||||
data = await self._request_data(
|
||||
"post",
|
||||
"/login/GetLoginQrCodeNew",
|
||||
json_body={"Proxy": proxy_value, "Check": False},
|
||||
timeout=30,
|
||||
)
|
||||
uuid, qr_url = self._extract_qr_response(data)
|
||||
else:
|
||||
data = await self._request_data(
|
||||
"post",
|
||||
@@ -146,14 +230,29 @@ class LoginMixin(Server864APIClientBase):
|
||||
|
||||
async def get_login_status(self, auto_login: bool = True) -> dict:
|
||||
"""获取 864 在线状态。"""
|
||||
return await self._request_data(
|
||||
data = await self._request_data(
|
||||
"get",
|
||||
"/login/GetLoginStatus",
|
||||
params={"autoLogin": str(bool(auto_login)).lower()},
|
||||
timeout=20,
|
||||
)
|
||||
normalized = dict(data or {})
|
||||
# 把最常用的在线态判断和身份字段提前归一化:
|
||||
# 1. 这样上层只要消费 `is_online` / `wxid` / `nickname` 这些统一键,不必感知原始 swagger 字段差异;
|
||||
# 2. 与 `CheckLoginStatus` 一样,Dashboard 和 runtime 都能共享同一份兼容结果;
|
||||
# 3. 也方便后续把 864 的不同 server 版本收敛到更薄的一层 provider 适配。
|
||||
normalized["is_online"] = self._is_online_from_login_status_payload(normalized)
|
||||
normalized.update(self._extract_login_identity_from_status(normalized))
|
||||
return normalized
|
||||
|
||||
async def log_out(self) -> bool:
|
||||
"""退出当前 864 登录态。"""
|
||||
await self._request_data("get", "/login/LogOutRequest", timeout=15)
|
||||
try:
|
||||
# 不同 864 版本里退出接口命名存在差异:
|
||||
# 1. 当前项目早期接的是 `/login/LogOutRequest`;
|
||||
# 2. 用户本地 864 源码中实际注册的是 `/login/LogOut`;
|
||||
# 3. 因此这里优先尝试较新的 `/LogOut`,失败后再回退到旧路径,降低版本切换成本。
|
||||
await self._request_data("get", "/login/LogOut", timeout=15)
|
||||
except Exception:
|
||||
await self._request_data("get", "/login/LogOutRequest", timeout=15)
|
||||
return True
|
||||
|
||||
@@ -28,6 +28,57 @@ class Server864RuntimeMixin:
|
||||
def is_runtime_running(self) -> bool:
|
||||
return bool(getattr(self, "_runtime_running", False))
|
||||
|
||||
async def _wait_login_identity_ready(
|
||||
self,
|
||||
*,
|
||||
logger,
|
||||
on_login_qr_update: AsyncCallback | None = None,
|
||||
uuid: str = "",
|
||||
url: str = "",
|
||||
scan_url: str = "",
|
||||
login_qr_api: str = "new_x",
|
||||
login_way: str = "mac",
|
||||
retry_times: int = 12,
|
||||
retry_interval_seconds: int = 5,
|
||||
) -> bool:
|
||||
"""在 864 已判定在线后,继续等待资料接口把账号身份补齐。"""
|
||||
for attempt in range(1, retry_times + 1):
|
||||
if await self._refresh_identity_from_profile(logger=logger):
|
||||
return True
|
||||
|
||||
try:
|
||||
login_status = await self.get_login_status(auto_login=False)
|
||||
except Exception as e:
|
||||
logger.warning(f"server_864 等待账号资料就绪时获取登录状态失败: {e}")
|
||||
else:
|
||||
if self._is_online_from_login_status_payload(login_status):
|
||||
# 服务端已在线但资料还没取到时,前端不应继续停在“未登录”语义:
|
||||
# 1. 用户当前最困惑的点正是“明明在线了,为什么页面还不显示登录成功”;
|
||||
# 2. 这类场景更接近“服务端在线,账号资料同步中”,需要一个更准确的中间态提示;
|
||||
# 3. 因此这里主动把状态更新到“资料同步中”,让首页知道并非扫码失败。
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"status": "confirmed",
|
||||
"status_text": f"864 服务端已在线,正在同步账号资料(第 {attempt}/{retry_times} 次)",
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "login_finalizing",
|
||||
"connection_ready": True,
|
||||
"login_required": False,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
if attempt < retry_times:
|
||||
await asyncio.sleep(retry_interval_seconds)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _normalize_login_runtime_message(message: str) -> str:
|
||||
"""把 864 登录阶段抛出的原始错误整理成更适合前端展示的短文本。"""
|
||||
@@ -101,7 +152,14 @@ class Server864RuntimeMixin:
|
||||
login_way=login_way,
|
||||
)
|
||||
|
||||
await on_login_ready(self.get_login_identity())
|
||||
login_identity = self.get_login_identity()
|
||||
# 在 provider 侧再做一次硬校验:
|
||||
# 1. `_ensure_login()` 已经负责等待真正可用的登录态,但运行期代码后续可能继续演进;
|
||||
# 2. 这里补一道收口保护,可以避免未来某次重构把“空身份”再次漏进 Robot 业务层;
|
||||
# 3. 一旦这里失败,说明当前 provider 还没有拿到稳定账号身份,必须继续停留在登录阶段。
|
||||
if not self._has_login_identity(login_identity):
|
||||
raise RuntimeError("server_864 登录流程未拿到可用账号身份,已阻止进入后台业务初始化")
|
||||
await on_login_ready(login_identity)
|
||||
logger.info("server_864 登录成功")
|
||||
|
||||
await self._set_runtime_running(True, on_runtime_state_change=on_runtime_state_change, logger=logger)
|
||||
@@ -146,7 +204,14 @@ class Server864RuntimeMixin:
|
||||
) -> None:
|
||||
"""确保 864 已完成登录。"""
|
||||
if await self.is_logged_in():
|
||||
await self._refresh_identity_from_profile(logger=logger)
|
||||
identity_ready = await self._wait_login_identity_ready(
|
||||
logger=logger,
|
||||
on_login_qr_update=on_login_qr_update,
|
||||
login_qr_api=login_qr_api,
|
||||
login_way=login_way,
|
||||
)
|
||||
if not identity_ready:
|
||||
raise RuntimeError("当前未拿到可用账号身份,请重新扫码登录")
|
||||
# 864 在“服务端已经在线、ABOT 只是后启动”的场景下会直接走这里:
|
||||
# 1. 之前这条分支只清理二维码态,没有补写本地 runtime_state;
|
||||
# 2. 这会让用户误以为“明明已经登录成功,却没有生成 provider 状态文件”;
|
||||
@@ -173,50 +238,299 @@ class Server864RuntimeMixin:
|
||||
)
|
||||
return
|
||||
|
||||
# 先探测一次 864 当前阶段,让 Dashboard 能直接区分“等服务端准备”和“需要扫码”:
|
||||
# 1. 864 的未登录态并不只有一种,部分场景其实是远端连接对象还没建好;
|
||||
# 2. 若首页始终只显示“未登录”,运维很难判断下一步是等服务端还是去扫码;
|
||||
# 3. 这里把差异压缩成轻量阶段字段,供前端直接展示,不改动核心登录流程。
|
||||
login_stage_snapshot = await self._probe_login_stage()
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
login_stage_snapshot,
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
uuid, url = await self.get_qr_code(print_qr=True, login_qr_api=login_qr_api, login_way=login_way)
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": None,
|
||||
"status": "waiting",
|
||||
"status_text": "等待扫码登录",
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "waiting_scan",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
while True:
|
||||
# 这里把“申请二维码 -> 轮询扫码 -> 登录收口”包成一轮完整会话:
|
||||
# 1. 864 某些失败不是用户操作问题,而是服务端在扫码后主动中断当前连接;
|
||||
# 2. 对这类场景,最合理的行为不是让线程退出,而是把错误留在前端并回到下一轮扫码;
|
||||
# 3. 这样可以满足“没登录成功就不要进入后台,但页面持续引导重新登录”的产品预期。
|
||||
login_stage_snapshot = await self._probe_login_stage()
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
login_stage_snapshot,
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
try:
|
||||
is_logged_in, login_status = await self.check_login_status()
|
||||
uuid, url = await self.get_qr_code(print_qr=True, login_qr_api=login_qr_api, login_way=login_way)
|
||||
except Exception as e:
|
||||
error_message = self._normalize_login_runtime_message(str(e))
|
||||
# 登录轮询阶段一旦出现明确错误,应立刻同步到 Dashboard:
|
||||
# 1. 之前这类异常只会停在控制台日志,前端继续显示旧的“等待扫码/等待验证”文案;
|
||||
# 2. 用户已经扫码后,最需要的是第一时间知道“为什么卡住了”;
|
||||
# 3. 因此这里把错误直接回写成登录态,让弹窗成为当前环境登录的真实看板。
|
||||
qr_error_message = str(e).strip()
|
||||
if "该链接已绑定微信号" in qr_error_message and "在线状态良好" in qr_error_message:
|
||||
# 当 864 服务端明确提示“当前 key 已在线”时,优先尝试直接复用现有登录态:
|
||||
# 1. 这通常出现在切换二维码模式过快、旧连接尚未完全退出的瞬间;
|
||||
# 2. 若此时直接抛错,线程会把“已经在线”误判成获取二维码失败;
|
||||
# 3. 因此这里先转成“尝试接管现有会话”,成功后直接收口,不再强制继续申请新二维码。
|
||||
identity_ready = await self._refresh_identity_from_profile(logger=logger)
|
||||
if identity_ready:
|
||||
ipad_config["wxid"] = self.wxid
|
||||
ipad_config["login_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self._save_runtime_state(
|
||||
state_path=state_path,
|
||||
state_payload={"wxid": self.wxid, "login_time": ipad_config["login_time"]},
|
||||
logger=logger,
|
||||
)
|
||||
await self._safe_callback(
|
||||
on_login_qr_cleared,
|
||||
{
|
||||
"status": "logged_in",
|
||||
"status_text": "864 服务端已存在可用登录态,已自动复用当前会话",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "logged_in",
|
||||
"connection_ready": True,
|
||||
"login_required": False,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_cleared",
|
||||
)
|
||||
return
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"status": "unavailable",
|
||||
"status_text": self._normalize_login_runtime_message(qr_error_message),
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "status_unavailable",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
raise
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
effective_time = 0
|
||||
raw_state = 0
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": None,
|
||||
"status": "waiting",
|
||||
"status_text": "等待扫码登录",
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "waiting_scan",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
login_completed = False
|
||||
while True:
|
||||
try:
|
||||
is_logged_in, login_status = await self.check_login_status()
|
||||
except Exception as e:
|
||||
error_message = self._normalize_login_runtime_message(str(e))
|
||||
# 轮询阶段失败时只更新前端,不中断 provider 主线程:
|
||||
# 1. 线上最常见的是本轮登录连接已被服务端丢弃,继续崩线程只会让运维反复重启;
|
||||
# 2. 这里先把明确错误展示给前端,再回到外层重新申请二维码;
|
||||
# 3. 这样用户在新环境里可以直接继续扫码,不需要手工重启 ABOT。
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"status": "unavailable",
|
||||
"status_text": error_message,
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "status_unavailable",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
logger.warning(f"server_864 登录状态轮询失败,本轮二维码作废并等待重新登录: {error_message}")
|
||||
await asyncio.sleep(2)
|
||||
break
|
||||
|
||||
if is_logged_in:
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": effective_time if effective_time > 0 else None,
|
||||
"status": "confirmed",
|
||||
# 864 进入 `state=2` 后,已经拿到了 wxid / wxnewpass 等关键登录信息:
|
||||
# 1. 这说明扫码本身已经成功,但并不代表 ABOT 后续初始化已经全部完成;
|
||||
# 2. 若这里直接关闭弹窗,后面的版本过低/初始化失败就无法同步给前端;
|
||||
# 3. 因此先保持弹窗可见,把阶段切到“登录收口中”,等真正初始化完成后再清理。
|
||||
"status_text": "扫码成功,等待服务端完成登录",
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "login_finalizing",
|
||||
"connection_ready": True,
|
||||
"login_required": False,
|
||||
"raw_state": max(raw_state, 2),
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
login_completed = True
|
||||
break
|
||||
|
||||
# 864 的登录状态查询会回传当前 uuid 和有效期:
|
||||
# 1. 真实联调中已确认 `CheckLoginStatus` 会返回 `uuid/effective_time`;
|
||||
# 2. 这些值应优先作为 Dashboard 的二维码倒计时与当前扫码目标来源;
|
||||
# 3. 一旦 server 侧切换了新的 uuid,这里也要及时覆盖本地展示态,避免前端一直盯着旧码。
|
||||
latest_uuid = str(login_status.get("uuid", "") or uuid).strip() or uuid
|
||||
effective_time = int(login_status.get("effective_time", 0) or 0)
|
||||
raw_state = int(login_status.get("raw_state", login_status.get("state", 0)) or 0)
|
||||
verification_url = str(login_status.get("verification_url", "") or "").strip()
|
||||
nick_name = str(login_status.get("nick_name", "") or "").strip()
|
||||
head_img_url = str(login_status.get("head_img_url", "") or "").strip()
|
||||
if latest_uuid != uuid:
|
||||
uuid = latest_uuid
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
url = f"https://api.2dcode.biz/v1/create-qr-code?data={scan_url}" if scan_url else url
|
||||
|
||||
# 864 的 `CheckLoginStatus` 存在一类“状态缓存已终态,但服务端连接没有继续推进”的情况:
|
||||
# 1. 实测里扫码并完成安全验证后,接口可能停在 `state=4 + VerificationUrl`,前端会一直看见旧状态;
|
||||
# 2. 结合服务端源码,这类 `state=4` 更接近终态/失效态,而不是可继续等待的中间态;
|
||||
# 3. 因此这里把它单独识别出来,避免 Dashboard 长时间卡在“等待安全验证”的旧文案。
|
||||
raw_status_text = str(login_status.get("msg") or login_status.get("loginState") or "").strip()
|
||||
if raw_state == 0:
|
||||
# 864 有时会把上一轮登录留下的 VerificationUrl 一起带回来:
|
||||
# 1. 但当 `raw_state=0` 时,真实含义仍然是“当前二维码等待扫码”;
|
||||
# 2. 若继续优先展示旧验证链接,前端就会出现“明明没扫却提示去安全验证”的误导;
|
||||
# 3. 因此这里明确以原始状态为准,并主动忽略这类陈旧 VerificationUrl。
|
||||
provider_stage = "waiting_scan" if uuid else "login_required"
|
||||
status = "waiting"
|
||||
status_text = raw_status_text or "等待扫码登录"
|
||||
verification_url = ""
|
||||
nick_name = ""
|
||||
head_img_url = ""
|
||||
elif raw_state == 4:
|
||||
provider_stage = "login_required"
|
||||
status = "expired"
|
||||
if verification_url:
|
||||
status_text = "安全验证链路已结束,但服务端未完成登录收口,正在准备刷新二维码"
|
||||
else:
|
||||
status_text = raw_status_text or "二维码状态已结束,正在准备刷新二维码"
|
||||
elif raw_state == 1:
|
||||
# 864 在 `state=1` 时通常已经识别出扫码账号,但还没推进到最终登录完成:
|
||||
# 1. 这时接口经常会同时回传昵称、头像以及 VerificationUrl;
|
||||
# 2. 但从用户体验上看,它更接近“已扫码,等待服务端确认”,不应该继续只提示去点链接;
|
||||
# 3. 因此这里单独映射成可读阶段,方便 Dashboard 展示更准确的过程状态。
|
||||
provider_stage = "scan_confirmed"
|
||||
status = "waiting"
|
||||
display_name = nick_name or "当前微信账号"
|
||||
status_text = raw_status_text or f"已扫码:{display_name},等待服务端确认登录"
|
||||
elif verification_url:
|
||||
provider_stage = "verification_required"
|
||||
status = "waiting"
|
||||
if not raw_status_text:
|
||||
status_text = "扫码已完成,请继续打开验证链接完成安全验证"
|
||||
else:
|
||||
status_text = raw_status_text
|
||||
else:
|
||||
provider_stage = "waiting_scan" if uuid else "login_required"
|
||||
status = "waiting"
|
||||
status_text = raw_status_text or "等待扫码登录"
|
||||
|
||||
if verification_url and raw_state not in {0, 4} and not raw_status_text:
|
||||
status_text = "扫码已完成,请继续打开验证链接完成安全验证"
|
||||
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": effective_time if effective_time > 0 else None,
|
||||
"status": status,
|
||||
"status_text": status_text,
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": provider_stage,
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"verification_url": verification_url,
|
||||
"raw_state": raw_state,
|
||||
"nick_name": nick_name,
|
||||
"head_img_url": head_img_url,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
# 若 server 已明确告知二维码失效或停在终态缓存,则立即结束本轮、回到外层申请新二维码:
|
||||
# 1. 这能避免 Dashboard 一直展示一张已经不可扫的旧二维码;
|
||||
# 2. 864 某些版本即使 `effective_time` 还大于 0,也可能已经停在 `state=4` 终态缓存里;
|
||||
# 3. 因此这里补充 `raw_state == 4` 的刷新条件,让前端能尽快得到新的登录入口。
|
||||
if effective_time <= 0 or raw_state == 4:
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": None,
|
||||
"status": "waiting",
|
||||
"status_text": "二维码已失效,正在准备新二维码",
|
||||
"login_source": "refresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "waiting_scan",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
break
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if not login_completed:
|
||||
continue
|
||||
|
||||
try:
|
||||
await self._wait_init_ready(logger=logger)
|
||||
identity_ready = await self._wait_login_identity_ready(
|
||||
logger=logger,
|
||||
on_login_qr_update=on_login_qr_update,
|
||||
uuid=uuid,
|
||||
url=url,
|
||||
scan_url=scan_url,
|
||||
login_qr_api=login_qr_api,
|
||||
login_way=login_way,
|
||||
)
|
||||
if not identity_ready:
|
||||
raise RuntimeError("扫码完成后未获取到可用账号身份,请重新扫码登录")
|
||||
ipad_config["wxid"] = self.wxid
|
||||
ipad_config["login_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self._save_runtime_state(
|
||||
state_path=state_path,
|
||||
state_payload={"wxid": self.wxid, "login_time": ipad_config["login_time"]},
|
||||
logger=logger,
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
error_message = self._normalize_post_scan_failure_message(str(e))
|
||||
# 扫码成功后的收口阶段同样需要把异常同步给前端:
|
||||
# 1. 这类错误往往发生在“state=2 之后但正式进入业务前”,最容易被误判成前端没刷新;
|
||||
# 2. 例如当前用户遇到的“客户端版本过低”就是在这个阶段由服务端主动断开;
|
||||
# 3. 这里不再直接抛异常退出,而是把错误留在弹窗里并继续等待下一轮扫码。
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
@@ -230,194 +544,25 @@ class Server864RuntimeMixin:
|
||||
"provider_stage": "status_unavailable",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"raw_state": max(raw_state, 2),
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
raise RuntimeError(error_message) from e
|
||||
if is_logged_in:
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": effective_time if effective_time > 0 else None,
|
||||
"status": "confirmed",
|
||||
# 864 进入 `state=2` 后,已经拿到了 wxid / wxnewpass 等关键登录信息:
|
||||
# 1. 这说明扫码本身已经成功,但并不代表 ABOT 后续初始化已经全部完成;
|
||||
# 2. 若这里直接关闭弹窗,后面的版本过低/初始化失败就无法同步给前端;
|
||||
# 3. 因此先保持弹窗可见,把阶段切到“登录收口中”,等真正初始化完成后再清理。
|
||||
"status_text": "扫码成功,等待服务端完成登录",
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "login_finalizing",
|
||||
"connection_ready": True,
|
||||
"login_required": False,
|
||||
"raw_state": 2,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
break
|
||||
logger.warning(f"server_864 登录收口失败,继续停留在扫码引导态等待重新登录: {error_message}")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 864 的登录状态查询会回传当前 uuid 和有效期:
|
||||
# 1. 真实联调中已确认 `CheckLoginStatus` 会返回 `uuid/effective_time`;
|
||||
# 2. 这些值应优先作为 Dashboard 的二维码倒计时与当前扫码目标来源;
|
||||
# 3. 一旦 server 侧切换了新的 uuid,这里也要及时覆盖本地展示态,避免前端一直盯着旧码。
|
||||
latest_uuid = str(login_status.get("uuid", "") or uuid).strip() or uuid
|
||||
effective_time = int(login_status.get("effective_time", 0) or 0)
|
||||
raw_state = int(login_status.get("raw_state", login_status.get("state", 0)) or 0)
|
||||
verification_url = str(login_status.get("verification_url", "") or "").strip()
|
||||
nick_name = str(login_status.get("nick_name", "") or "").strip()
|
||||
head_img_url = str(login_status.get("head_img_url", "") or "").strip()
|
||||
if latest_uuid != uuid:
|
||||
uuid = latest_uuid
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
url = f"https://api.2dcode.biz/v1/create-qr-code?data={scan_url}" if scan_url else url
|
||||
|
||||
# 864 的 `CheckLoginStatus` 存在一类“状态缓存已终态,但服务端连接没有继续推进”的情况:
|
||||
# 1. 实测里扫码并完成安全验证后,接口可能停在 `state=4 + VerificationUrl`,前端会一直看见旧状态;
|
||||
# 2. 结合服务端源码,这类 `state=4` 更接近终态/失效态,而不是可继续等待的中间态;
|
||||
# 3. 因此这里把它单独识别出来,避免 Dashboard 长时间卡在“等待安全验证”的旧文案。
|
||||
raw_status_text = str(login_status.get("msg") or login_status.get("loginState") or "").strip()
|
||||
if raw_state == 0:
|
||||
# 864 有时会把上一轮登录留下的 VerificationUrl 一起带回来:
|
||||
# 1. 但当 `raw_state=0` 时,真实含义仍然是“当前二维码等待扫码”;
|
||||
# 2. 若继续优先展示旧验证链接,前端就会出现“明明没扫却提示去安全验证”的误导;
|
||||
# 3. 因此这里明确以原始状态为准,并主动忽略这类陈旧 VerificationUrl。
|
||||
provider_stage = "waiting_scan" if uuid else "login_required"
|
||||
status = "waiting"
|
||||
status_text = raw_status_text or "等待扫码登录"
|
||||
verification_url = ""
|
||||
nick_name = ""
|
||||
head_img_url = ""
|
||||
elif raw_state == 4:
|
||||
provider_stage = "login_required"
|
||||
status = "expired"
|
||||
if verification_url:
|
||||
status_text = "安全验证链路已结束,但服务端未完成登录收口,正在准备刷新二维码"
|
||||
else:
|
||||
status_text = raw_status_text or "二维码状态已结束,正在准备刷新二维码"
|
||||
elif raw_state == 1:
|
||||
# 864 在 `state=1` 时通常已经识别出扫码账号,但还没推进到最终登录完成:
|
||||
# 1. 这时接口经常会同时回传昵称、头像以及 VerificationUrl;
|
||||
# 2. 但从用户体验上看,它更接近“已扫码,等待服务端确认”,不应该继续只提示去点链接;
|
||||
# 3. 因此这里单独映射成可读阶段,方便 Dashboard 展示更准确的过程状态。
|
||||
provider_stage = "scan_confirmed"
|
||||
status = "waiting"
|
||||
display_name = nick_name or "当前微信账号"
|
||||
status_text = raw_status_text or f"已扫码:{display_name},等待服务端确认登录"
|
||||
elif verification_url:
|
||||
provider_stage = "verification_required"
|
||||
status = "waiting"
|
||||
if not raw_status_text:
|
||||
status_text = "扫码已完成,请继续打开验证链接完成安全验证"
|
||||
else:
|
||||
status_text = raw_status_text
|
||||
else:
|
||||
provider_stage = "waiting_scan" if uuid else "login_required"
|
||||
status = "waiting"
|
||||
status_text = raw_status_text or "等待扫码登录"
|
||||
|
||||
if verification_url and raw_state not in {0, 4} and not raw_status_text:
|
||||
status_text = "扫码已完成,请继续打开验证链接完成安全验证"
|
||||
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": effective_time if effective_time > 0 else None,
|
||||
"status": status,
|
||||
"status_text": status_text,
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": provider_stage,
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"verification_url": verification_url,
|
||||
"raw_state": raw_state,
|
||||
"nick_name": nick_name,
|
||||
"head_img_url": head_img_url,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
# 若 server 已明确告知二维码失效或停在终态缓存,则立即重新申请一张新码:
|
||||
# 1. 这能避免 Dashboard 一直展示一张已经不可扫的旧二维码;
|
||||
# 2. 864 某些版本即使 `effective_time` 还大于 0,也可能已经停在 `state=4` 终态缓存里;
|
||||
# 3. 因此这里补充 `raw_state == 4` 的刷新条件,让前端能尽快得到新的登录入口。
|
||||
if effective_time <= 0 or raw_state == 4:
|
||||
uuid, url = await self.get_qr_code(print_qr=True, login_qr_api=login_qr_api, login_way=login_way)
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"expires_in": None,
|
||||
"status": "waiting",
|
||||
"status_text": "二维码已刷新,等待扫码登录",
|
||||
"login_source": "refresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "waiting_scan",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
try:
|
||||
await self._wait_init_ready(logger=logger)
|
||||
await self._refresh_identity_from_profile(logger=logger)
|
||||
ipad_config["wxid"] = self.wxid
|
||||
ipad_config["login_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self._save_runtime_state(
|
||||
state_path=state_path,
|
||||
state_payload={"wxid": self.wxid, "login_time": ipad_config["login_time"]},
|
||||
logger=logger,
|
||||
)
|
||||
except Exception as e:
|
||||
error_message = self._normalize_post_scan_failure_message(str(e))
|
||||
# 扫码成功后的收口阶段同样需要把异常同步给前端:
|
||||
# 1. 这类错误往往发生在“state=2 之后但正式进入业务前”,最容易被误判成前端没刷新;
|
||||
# 2. 例如当前用户遇到的“客户端版本过低”就是在这个阶段由服务端主动断开;
|
||||
# 3. 因此这里明确把阶段标成 `status_unavailable`,让弹窗继续停留并展示真实原因。
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
"uuid": uuid,
|
||||
"url": url,
|
||||
"scan_url": scan_url,
|
||||
"status": "unavailable",
|
||||
"status_text": error_message,
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "status_unavailable",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"raw_state": 2,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
raise RuntimeError(error_message) from e
|
||||
@staticmethod
|
||||
def _has_login_identity(login_identity: dict[str, Any] | None) -> bool:
|
||||
"""判断当前 provider 是否已经拿到可用于进入业务层的账号身份。"""
|
||||
payload = dict(login_identity or {})
|
||||
# 这里把“可用身份”的标准压到最小:
|
||||
# 1. 864 有些接口可能先拿到昵称、后拿到 wxid,也可能顺序相反;
|
||||
# 2. 但两者都为空时,业务层无法知道当前到底是谁登录了;
|
||||
# 3. 因此至少要拿到 `wxid` 或 `nickname` 之一,才允许继续进入 Robot 初始化。
|
||||
return bool(str(payload.get("wxid", "") or "").strip() or str(payload.get("nickname", "") or "").strip())
|
||||
|
||||
async def _probe_login_stage(self) -> dict[str, Any]:
|
||||
"""探测 864 当前登录阶段,供 Dashboard 展示更准确的运维状态。"""
|
||||
@@ -495,18 +640,46 @@ class Server864RuntimeMixin:
|
||||
logger.warning(f"server_864 检查初始化状态失败: {error_message}")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def _refresh_identity_from_profile(self, *, logger) -> None:
|
||||
async def _refresh_identity_from_profile(self, *, logger) -> bool:
|
||||
"""从 864 的资料接口刷新当前登录身份。"""
|
||||
try:
|
||||
profile = await self.get_profile()
|
||||
except Exception as e:
|
||||
error_message = str(e).strip()
|
||||
# `GetProfile` 失败时,再用 `GetLoginStatus` 补做一次登录态与身份判定:
|
||||
# 1. 用户当前联调里已经确认 `GetLoginStatus` 可用,而 `GetProfile` / `GetInItStatus` 在部分阶段会直接报“该链接不存在”;
|
||||
# 2. 若此时登录状态接口其实还能返回在线和账号字段,就没必要仅因资料接口异常而误判整轮登录失败;
|
||||
# 3. 因此这里把它作为资料接口失败后的第一优先补偿探针,尽量保住已经完成的登录链路。
|
||||
try:
|
||||
login_status = await self.get_login_status(auto_login=False)
|
||||
except Exception as status_error:
|
||||
logger.warning(
|
||||
f"server_864 登录状态补偿探针也失败,无法确认当前账号身份: {error_message}; "
|
||||
f"GetLoginStatus={status_error}"
|
||||
)
|
||||
else:
|
||||
if self._is_online_from_login_status_payload(login_status):
|
||||
identity = self._extract_login_identity_from_status(login_status)
|
||||
self.wxid = identity.get("wxid", self.wxid)
|
||||
self.nickname = identity.get("nickname", self.nickname)
|
||||
self.alias = identity.get("alias", self.alias)
|
||||
self.phone = identity.get("phone", self.phone)
|
||||
self.signature = identity.get("signature", self.signature)
|
||||
if self.wxid or self.nickname:
|
||||
logger.info(
|
||||
"server_864 资料接口失败,但已通过 GetLoginStatus 补确认当前账号身份: "
|
||||
f"wxid={self.wxid} nickname={self.nickname}"
|
||||
)
|
||||
return True
|
||||
# 864 有些版本在消息链路可用后,资料接口仍可能短时间不可用:
|
||||
# 1. 此时若直接抛异常,会让“已经登录成功”的启动流程被资料查询反向拖垮;
|
||||
# 2. 项目主链路真正依赖的是后续消息同步与发送能力,而不是这里的展示性资料;
|
||||
# 3. 所以这里改成降级告警,保留已有身份字段,等后续再由可用接口补齐。
|
||||
logger.warning(f"server_864 刷新登录账号资料失败,继续沿用当前缓存身份: {error_message}")
|
||||
return
|
||||
# 2. 但如果当前连 `wxid/nickname` 都没有,就不能再假装“已经有可用身份”;
|
||||
# 3. 因此这里返回布尔值,由上层决定是继续使用已知身份,还是把登录流程判定为失败。
|
||||
if self.wxid or self.nickname:
|
||||
logger.warning(f"server_864 刷新登录账号资料失败,继续沿用当前已获取身份: {error_message}")
|
||||
return True
|
||||
logger.warning(f"server_864 刷新登录账号资料失败,当前尚未拿到可用账号身份: {error_message}")
|
||||
return False
|
||||
self.wxid = str(
|
||||
profile.get("UserName")
|
||||
or profile.get("userName")
|
||||
@@ -530,6 +703,7 @@ class Server864RuntimeMixin:
|
||||
logger.info(
|
||||
f"server_864 登录账号信息: wxid: {self.wxid} 昵称: {self.nickname} 微信号: {self.alias} 手机号: {self.phone}"
|
||||
)
|
||||
return bool(self.wxid or self.nickname)
|
||||
|
||||
@staticmethod
|
||||
def _save_runtime_state(*, state_path: str, state_payload: dict[str, Any], logger) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from loguru import logger
|
||||
|
||||
from wechat_ipad.errors import UserLoggedOut
|
||||
|
||||
from wechat_ipad.providers.server_864.base import Server864APIClientBase
|
||||
|
||||
|
||||
@@ -39,8 +41,33 @@ class UserMixin(Server864APIClientBase):
|
||||
"""检查 864 当前账号是否在线。"""
|
||||
del wxid
|
||||
try:
|
||||
# 优先使用 864 自己的登录状态接口判断在线态:
|
||||
# 1. `GetProfile` 在某些版本里会比真实登录态更早失效,容易把“已登录但资料接口异常”误判成未登录;
|
||||
# 2. 用户当前给出的 `GetLoginStatus` 正是更贴近 server 自身会话状态的一条探针;
|
||||
# 3. 因此这里先走登录状态接口,只有它也无法确认时,才回退到资料接口兜底。
|
||||
login_status = await self.get_login_status(auto_login=False)
|
||||
if self._is_online_from_login_status_payload(login_status):
|
||||
identity = self._extract_login_identity_from_status(login_status)
|
||||
if identity.get("wxid"):
|
||||
self.wxid = identity["wxid"]
|
||||
if identity.get("nickname"):
|
||||
self.nickname = identity["nickname"]
|
||||
if identity.get("alias"):
|
||||
self.alias = identity["alias"]
|
||||
if identity.get("phone"):
|
||||
self.phone = identity["phone"]
|
||||
if identity.get("signature"):
|
||||
self.signature = identity["signature"]
|
||||
return True
|
||||
await self.get_profile()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("server_864 is_logged_in:{}", e)
|
||||
except UserLoggedOut as e:
|
||||
# “未登录 / 需要重新登录”是 864 登录引导中的常规状态,不应该长期污染 error 日志:
|
||||
# 1. 当前首页会主动引导扫码,这类返回本质上只是“当前还没登录完成”;
|
||||
# 2. 若仍按 error 记录,运维排查时很难分清真正异常和正常登录态切换;
|
||||
# 3. 因此这里降级为 info,只保留可读状态文本。
|
||||
logger.info("server_864 is_logged_in:{}", e)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("server_864 is_logged_in:{}", e)
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user