mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat(weixin_oc): add WeChat login registration and QR code handling
This commit is contained in:
167
astrbot/core/platform/sources/weixin_oc/login_registration.py
Normal file
167
astrbot/core/platform/sources/weixin_oc/login_registration.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from .weixin_oc_client import WeixinOCClient
|
||||
|
||||
DEFAULT_WEIXIN_OC_BASE_URL = "https://ilinkai.weixin.qq.com"
|
||||
DEFAULT_WEIXIN_OC_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c"
|
||||
DEFAULT_WEIXIN_OC_BOT_TYPE = "3"
|
||||
DEFAULT_WEIXIN_OC_QR_POLL_INTERVAL = 1
|
||||
DEFAULT_WEIXIN_OC_LONG_POLL_TIMEOUT_MS = 35_000
|
||||
DEFAULT_WEIXIN_OC_API_TIMEOUT_MS = 15_000
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeixinOCLoginRegistration:
|
||||
qrcode: str
|
||||
qrcode_img_content: str
|
||||
interval: int
|
||||
|
||||
|
||||
def normalize_weixin_oc_base_url(base_url: str | None) -> str:
|
||||
return (base_url or DEFAULT_WEIXIN_OC_BASE_URL).strip().rstrip("/")
|
||||
|
||||
|
||||
def _string_field(data: dict[str, Any], key: str) -> str:
|
||||
value = data.get(key)
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _int_config(value: Any, default: int, minimum: int) -> int:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError):
|
||||
parsed = default
|
||||
return max(parsed, minimum)
|
||||
|
||||
|
||||
def weixin_oc_login_result(
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
default_base_url: str,
|
||||
) -> dict[str, Any]:
|
||||
raw_status = _string_field(data, "status") or "wait"
|
||||
if raw_status == "confirmed":
|
||||
bot_token = _string_field(data, "bot_token")
|
||||
if not bot_token:
|
||||
return {"status": "error", "message": "登录成功但未返回 token"}
|
||||
base_url = _string_field(data, "baseurl") or default_base_url
|
||||
return {
|
||||
"status": "created",
|
||||
"qr_status": raw_status,
|
||||
"weixin_oc_token": bot_token,
|
||||
"weixin_oc_account_id": _string_field(data, "ilink_bot_id"),
|
||||
"weixin_oc_base_url": normalize_weixin_oc_base_url(base_url),
|
||||
"weixin_oc_user_id": _string_field(data, "ilink_user_id"),
|
||||
}
|
||||
if raw_status == "expired":
|
||||
return {"status": "expired", "qr_status": raw_status, "message": "二维码已过期"}
|
||||
if raw_status in {"cancel", "canceled", "denied"}:
|
||||
return {"status": "denied", "qr_status": raw_status, "message": "用户取消登录"}
|
||||
return {"status": "pending", "qr_status": raw_status}
|
||||
|
||||
|
||||
def _client(
|
||||
*,
|
||||
adapter_id: str,
|
||||
base_url: str,
|
||||
api_timeout_ms: int,
|
||||
) -> WeixinOCClient:
|
||||
return WeixinOCClient(
|
||||
adapter_id=adapter_id,
|
||||
base_url=base_url,
|
||||
cdn_base_url=DEFAULT_WEIXIN_OC_CDN_BASE_URL,
|
||||
api_timeout_ms=api_timeout_ms,
|
||||
)
|
||||
|
||||
|
||||
async def request_weixin_oc_login_qr(
|
||||
platform_config: dict[str, Any],
|
||||
) -> WeixinOCLoginRegistration:
|
||||
base_url = normalize_weixin_oc_base_url(
|
||||
_string_field(platform_config, "weixin_oc_base_url")
|
||||
)
|
||||
bot_type = _string_field(platform_config, "weixin_oc_bot_type")
|
||||
if not bot_type:
|
||||
bot_type = DEFAULT_WEIXIN_OC_BOT_TYPE
|
||||
api_timeout_ms = _int_config(
|
||||
platform_config.get("weixin_oc_api_timeout_ms"),
|
||||
DEFAULT_WEIXIN_OC_API_TIMEOUT_MS,
|
||||
1_000,
|
||||
)
|
||||
interval = _int_config(
|
||||
platform_config.get("weixin_oc_qr_poll_interval"),
|
||||
DEFAULT_WEIXIN_OC_QR_POLL_INTERVAL,
|
||||
1,
|
||||
)
|
||||
|
||||
client = _client(
|
||||
adapter_id=str(platform_config.get("id") or "weixin_oc"),
|
||||
base_url=base_url,
|
||||
api_timeout_ms=api_timeout_ms,
|
||||
)
|
||||
try:
|
||||
data = await client.request_json(
|
||||
"GET",
|
||||
"ilink/bot/get_bot_qrcode",
|
||||
params={"bot_type": bot_type},
|
||||
token_required=False,
|
||||
timeout_ms=15_000,
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
qrcode = _string_field(data, "qrcode")
|
||||
qrcode_img_content = _string_field(data, "qrcode_img_content")
|
||||
if not qrcode or not qrcode_img_content:
|
||||
raise RuntimeError("个人微信二维码响应格式异常")
|
||||
|
||||
return WeixinOCLoginRegistration(
|
||||
qrcode=qrcode,
|
||||
qrcode_img_content=qrcode_img_content,
|
||||
interval=interval,
|
||||
)
|
||||
|
||||
|
||||
async def poll_weixin_oc_login_once(
|
||||
*,
|
||||
platform_config: dict[str, Any],
|
||||
qrcode: str,
|
||||
) -> dict[str, Any]:
|
||||
if not qrcode:
|
||||
raise ValueError("Missing qrcode")
|
||||
|
||||
base_url = normalize_weixin_oc_base_url(
|
||||
_string_field(platform_config, "weixin_oc_base_url")
|
||||
)
|
||||
api_timeout_ms = _int_config(
|
||||
platform_config.get("weixin_oc_api_timeout_ms"),
|
||||
DEFAULT_WEIXIN_OC_API_TIMEOUT_MS,
|
||||
1_000,
|
||||
)
|
||||
long_poll_timeout_ms = _int_config(
|
||||
platform_config.get("weixin_oc_long_poll_timeout_ms"),
|
||||
DEFAULT_WEIXIN_OC_LONG_POLL_TIMEOUT_MS,
|
||||
1_000,
|
||||
)
|
||||
|
||||
client = _client(
|
||||
adapter_id=str(platform_config.get("id") or "weixin_oc"),
|
||||
base_url=base_url,
|
||||
api_timeout_ms=api_timeout_ms,
|
||||
)
|
||||
try:
|
||||
data = await client.request_json(
|
||||
"GET",
|
||||
"ilink/bot/get_qrcode_status",
|
||||
params={"qrcode": qrcode},
|
||||
token_required=False,
|
||||
timeout_ms=long_poll_timeout_ms,
|
||||
headers={"iLink-App-ClientVersion": "1"},
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
return weixin_oc_login_result(data, default_base_url=base_url)
|
||||
@@ -13,6 +13,10 @@ from astrbot.core.platform.sources.lark.app_registration import (
|
||||
request_app_registration,
|
||||
)
|
||||
from astrbot.core.platform.sources.lark.bot_info import request_lark_bot_info
|
||||
from astrbot.core.platform.sources.weixin_oc.login_registration import (
|
||||
poll_weixin_oc_login_once,
|
||||
request_weixin_oc_login_qr,
|
||||
)
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
@@ -117,61 +121,117 @@ class PlatformRoute(Route):
|
||||
action = str(payload.get("action", "")).strip().lower()
|
||||
if not action:
|
||||
return Response().error("Missing action").__dict__, 400
|
||||
if platform_type != "lark":
|
||||
return Response().error(
|
||||
f"Unsupported platform registration: {platform_type}"
|
||||
).__dict__, 404
|
||||
|
||||
platform_config = payload.get("platform_config")
|
||||
if not isinstance(platform_config, dict):
|
||||
platform_config = {}
|
||||
domain = str(platform_config.get("domain") or "").strip()
|
||||
|
||||
if action == "start":
|
||||
registration = await request_app_registration(domain)
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"status": "pending",
|
||||
"device_code": registration.device_code,
|
||||
"registration_code": registration.device_code,
|
||||
"user_code": registration.user_code,
|
||||
"verification_uri": registration.verification_uri,
|
||||
"verification_uri_complete": registration.verification_uri_complete,
|
||||
"expires_in": registration.expires_in,
|
||||
"interval": registration.interval,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
if platform_type == "lark":
|
||||
return await self._handle_lark_registration(
|
||||
action,
|
||||
payload,
|
||||
platform_config,
|
||||
)
|
||||
if platform_type == "weixin_oc":
|
||||
return await self._handle_weixin_oc_registration(
|
||||
action,
|
||||
payload,
|
||||
platform_config,
|
||||
)
|
||||
|
||||
if action == "poll":
|
||||
device_code = str(
|
||||
payload.get("device_code") or payload.get("registration_code") or ""
|
||||
).strip()
|
||||
if not device_code:
|
||||
return Response().error("Missing device_code").__dict__, 400
|
||||
result = await poll_app_registration_once(
|
||||
domain=domain,
|
||||
device_code=device_code,
|
||||
)
|
||||
if result.get("status") == "created":
|
||||
try:
|
||||
bot_info = await request_lark_bot_info(
|
||||
domain=str(result.get("domain") or domain),
|
||||
app_id=str(result.get("app_id") or ""),
|
||||
app_secret=str(result.get("app_secret") or ""),
|
||||
)
|
||||
if bot_info.app_name:
|
||||
result["bot_name"] = bot_info.app_name
|
||||
if bot_info.open_id:
|
||||
result["bot_open_id"] = bot_info.open_id
|
||||
except Exception as e:
|
||||
logger.error(f"获取飞书机器人信息失败: {e}", exc_info=True)
|
||||
return Response().ok(result).__dict__
|
||||
|
||||
return Response().error(f"Unsupported action: {action}").__dict__, 400
|
||||
return Response().error(
|
||||
f"Unsupported platform registration: {platform_type}"
|
||||
).__dict__, 404
|
||||
except Exception as e:
|
||||
logger.error(f"处理平台一键创建请求失败: {e}", exc_info=True)
|
||||
return Response().error(str(e)).__dict__, 500
|
||||
|
||||
async def _handle_lark_registration(
|
||||
self,
|
||||
action: str,
|
||||
payload: dict,
|
||||
platform_config: dict,
|
||||
):
|
||||
domain = str(platform_config.get("domain") or "").strip()
|
||||
|
||||
if action == "start":
|
||||
registration = await request_app_registration(domain)
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"status": "pending",
|
||||
"device_code": registration.device_code,
|
||||
"registration_code": registration.device_code,
|
||||
"user_code": registration.user_code,
|
||||
"verification_uri": registration.verification_uri,
|
||||
"verification_uri_complete": registration.verification_uri_complete,
|
||||
"expires_in": registration.expires_in,
|
||||
"interval": registration.interval,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
|
||||
if action == "poll":
|
||||
device_code = str(
|
||||
payload.get("device_code") or payload.get("registration_code") or ""
|
||||
).strip()
|
||||
if not device_code:
|
||||
return Response().error("Missing device_code").__dict__, 400
|
||||
result = await poll_app_registration_once(
|
||||
domain=domain,
|
||||
device_code=device_code,
|
||||
)
|
||||
if result.get("status") == "created":
|
||||
try:
|
||||
bot_info = await request_lark_bot_info(
|
||||
domain=str(result.get("domain") or domain),
|
||||
app_id=str(result.get("app_id") or ""),
|
||||
app_secret=str(result.get("app_secret") or ""),
|
||||
)
|
||||
if bot_info.app_name:
|
||||
result["bot_name"] = bot_info.app_name
|
||||
if bot_info.open_id:
|
||||
result["bot_open_id"] = bot_info.open_id
|
||||
except Exception as e:
|
||||
logger.error(f"获取飞书机器人信息失败: {e}", exc_info=True)
|
||||
return Response().ok(result).__dict__
|
||||
|
||||
return Response().error(f"Unsupported action: {action}").__dict__, 400
|
||||
|
||||
async def _handle_weixin_oc_registration(
|
||||
self,
|
||||
action: str,
|
||||
payload: dict,
|
||||
platform_config: dict,
|
||||
):
|
||||
if action == "start":
|
||||
registration = await request_weixin_oc_login_qr(platform_config)
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"status": "pending",
|
||||
"registration_code": registration.qrcode,
|
||||
"qrcode": registration.qrcode,
|
||||
"qrcode_img_content": registration.qrcode_img_content,
|
||||
"interval": registration.interval,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
|
||||
if action == "poll":
|
||||
qrcode = str(
|
||||
payload.get("qrcode") or payload.get("registration_code") or ""
|
||||
).strip()
|
||||
if not qrcode:
|
||||
return Response().error("Missing qrcode").__dict__, 400
|
||||
result = await poll_weixin_oc_login_once(
|
||||
platform_config=platform_config,
|
||||
qrcode=qrcode,
|
||||
)
|
||||
return Response().ok(result).__dict__
|
||||
|
||||
return Response().error(f"Unsupported action: {action}").__dict__, 400
|
||||
|
||||
@@ -30,30 +30,52 @@
|
||||
|
||||
</v-select>
|
||||
<div class="mt-3" v-if="selectedPlatformConfig">
|
||||
<div v-if="isLarkPlatform" class="lark-creation-title mt-4 mb-1">
|
||||
{{ tm('registrationAction.mode.title') }}
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-if="isLarkPlatform"
|
||||
v-model="larkCreationMode"
|
||||
class="lark-creation-mode"
|
||||
hide-details
|
||||
>
|
||||
<v-radio value="scan" :label="tm('registrationAction.mode.scan')"></v-radio>
|
||||
<v-radio value="manual" :label="tm('registrationAction.mode.manual')"></v-radio>
|
||||
</v-radio-group>
|
||||
<div v-if="isLarkPlatform">
|
||||
<div class="lark-creation-title mt-4 mb-1">
|
||||
{{ tm('registrationAction.mode.title') }}
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="larkCreationMode"
|
||||
class="lark-creation-mode"
|
||||
hide-details
|
||||
>
|
||||
<v-radio value="scan" :label="tm('registrationAction.mode.scan')"></v-radio>
|
||||
<v-radio value="manual" :label="tm('registrationAction.mode.manual')"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="isLarkPlatform && larkCreationMode === 'scan'" class="lark-registration-inline mt-3">
|
||||
<div v-if="larkCreationMode === 'scan'" class="lark-registration-inline mt-3">
|
||||
<PlatformRegistrationAction
|
||||
:platform-config="selectedPlatformConfig"
|
||||
:active="larkCreationMode === 'scan'"
|
||||
@created="handlePlatformRegistrationCreated"
|
||||
@success="showSuccess"
|
||||
@error="showError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="larkCreationMode === 'manual'" class="mt-2">
|
||||
<div class="platform-action-row">
|
||||
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
|
||||
<v-icon start>mdi-book-open-variant</v-icon>
|
||||
{{ tm('dialog.viewTutorial') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<AstrBotConfig :iterable="selectedPlatformConfig" :metadata="metadata['platform_group']?.metadata"
|
||||
metadataKey="platform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isWeixinOcPlatform" class="weixin-oc-registration-inline mt-4">
|
||||
<PlatformRegistrationAction
|
||||
:platform-config="selectedPlatformConfig"
|
||||
:active="larkCreationMode === 'scan'"
|
||||
:active="isWeixinOcPlatform"
|
||||
@created="handlePlatformRegistrationCreated"
|
||||
@success="showSuccess"
|
||||
@error="showError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isLarkPlatform || larkCreationMode === 'manual'" class="mt-2">
|
||||
<div v-else class="mt-2">
|
||||
<div class="platform-action-row">
|
||||
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
|
||||
<v-icon start>mdi-book-open-variant</v-icon>
|
||||
@@ -447,6 +469,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isWeixinOcPlatform && !this.selectedPlatformConfig?.weixin_oc_token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果是使用现有配置文件模式
|
||||
if (this.aBConfigRadioVal === '0') {
|
||||
return !!this.selectedAbConfId;
|
||||
@@ -482,13 +508,16 @@ export default {
|
||||
},
|
||||
isLarkPlatform() {
|
||||
return this.selectedPlatformConfig?.type === 'lark';
|
||||
},
|
||||
isWeixinOcPlatform() {
|
||||
return this.selectedPlatformConfig?.type === 'weixin_oc';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedPlatformType(newType) {
|
||||
if (newType && this.platformTemplates[newType]) {
|
||||
this.selectedPlatformConfig = JSON.parse(JSON.stringify(this.platformTemplates[newType]));
|
||||
this.larkCreationMode = this.selectedPlatformConfig?.type === 'lark' ? '' : 'manual';
|
||||
this.larkCreationMode = '';
|
||||
} else {
|
||||
this.selectedPlatformConfig = null;
|
||||
this.larkCreationMode = '';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="action" class="platform-registration-panel">
|
||||
<div class="registration-scan-title">
|
||||
{{ tm('registrationAction.scanTitle') }}
|
||||
{{ tm(action.scanTitleKey) }}
|
||||
</div>
|
||||
|
||||
<div class="registration-scan-content">
|
||||
@@ -11,8 +11,8 @@
|
||||
:class="{ 'registration-qr-shell-created': flow.status === 'created' }"
|
||||
>
|
||||
<QrCodeViewer
|
||||
v-if="flow.verification_uri_complete"
|
||||
:value="flow.verification_uri_complete"
|
||||
v-if="qrValue"
|
||||
:value="qrValue"
|
||||
:alt="tm(action.titleKey)"
|
||||
:size="150"
|
||||
:margin="1"
|
||||
@@ -55,6 +55,16 @@ const REGISTRATION_ACTIONS = {
|
||||
endpoint: '/api/platform/registration/lark',
|
||||
icon: 'mdi-qrcode',
|
||||
titleKey: 'registrationAction.lark.title',
|
||||
scanTitleKey: 'registrationAction.lark.scanTitle',
|
||||
successKey: 'registrationAction.created',
|
||||
},
|
||||
weixin_oc: {
|
||||
endpoint: '/api/platform/registration/weixin_oc',
|
||||
icon: 'mdi-qrcode',
|
||||
titleKey: 'registrationAction.weixinOc.title',
|
||||
scanTitleKey: 'registrationAction.weixinOc.scanTitle',
|
||||
successKey: 'registrationAction.weixinOc.created',
|
||||
statusKeyPrefix: 'registrationAction.weixinOc.status',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -92,6 +102,12 @@ export default {
|
||||
selectedDomain() {
|
||||
return this.platformConfig?.domain || FEISHU_DOMAIN;
|
||||
},
|
||||
qrValue() {
|
||||
return this.flow.verification_uri_complete
|
||||
|| this.flow.qrcode_img_content
|
||||
|| this.flow.qrcode
|
||||
|| '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
active: {
|
||||
@@ -196,7 +212,7 @@ export default {
|
||||
this.applyRegistrationResult(data);
|
||||
this.stopPolling();
|
||||
this.$emit('created', data);
|
||||
this.$emit('success', this.tm('registrationAction.created'));
|
||||
this.$emit('success', this.tm(this.action.successKey || 'registrationAction.created'));
|
||||
return;
|
||||
}
|
||||
if (this.flow.status === 'pending' || this.flow.status === 'slow_down') {
|
||||
@@ -231,9 +247,26 @@ export default {
|
||||
if (data.domain) {
|
||||
this.platformConfig.domain = data.domain;
|
||||
}
|
||||
if (data.weixin_oc_token) {
|
||||
this.platformConfig.weixin_oc_token = data.weixin_oc_token;
|
||||
}
|
||||
if (data.weixin_oc_account_id) {
|
||||
this.platformConfig.weixin_oc_account_id = data.weixin_oc_account_id;
|
||||
}
|
||||
if (data.weixin_oc_base_url) {
|
||||
this.platformConfig.weixin_oc_base_url = data.weixin_oc_base_url;
|
||||
}
|
||||
},
|
||||
getStatusText(status) {
|
||||
return this.tm(`registrationAction.status.${status || 'idle'}`);
|
||||
const normalizedStatus = status || 'idle';
|
||||
if (this.action?.statusKeyPrefix) {
|
||||
const platformStatusKey = `${this.action.statusKeyPrefix}.${normalizedStatus}`;
|
||||
const platformStatusText = this.tm(platformStatusKey);
|
||||
if (platformStatusText && platformStatusText !== platformStatusKey) {
|
||||
return platformStatusText;
|
||||
}
|
||||
}
|
||||
return this.tm(`registrationAction.status.${normalizedStatus}`);
|
||||
},
|
||||
getStatusColor(status) {
|
||||
switch (status) {
|
||||
|
||||
@@ -134,9 +134,24 @@
|
||||
"lark": {
|
||||
"title": "One-click QR Setup",
|
||||
"show": "One-click QR Setup",
|
||||
"scanTitle": "Scan with Feishu on your phone",
|
||||
"feishu": "Feishu China",
|
||||
"lark": "Lark Global"
|
||||
},
|
||||
"weixinOc": {
|
||||
"title": "Personal WeChat QR Login",
|
||||
"scanTitle": "Scan with WeChat on your phone",
|
||||
"created": "Login Complete",
|
||||
"status": {
|
||||
"idle": "Not Started",
|
||||
"starting": "Getting QR Code",
|
||||
"pending": "Waiting for Scan",
|
||||
"created": "Login Complete - remember to click the save button!",
|
||||
"denied": "Canceled",
|
||||
"expired": "Expired",
|
||||
"error": "Login Failed"
|
||||
}
|
||||
},
|
||||
"start": "Start Setup",
|
||||
"created": "Setup Complete",
|
||||
"startFailed": "Failed to start QR setup",
|
||||
|
||||
@@ -134,9 +134,24 @@
|
||||
"lark": {
|
||||
"title": "Настройка по QR",
|
||||
"show": "Настройка по QR",
|
||||
"scanTitle": "Отсканируйте в мобильном Feishu",
|
||||
"feishu": "Feishu China",
|
||||
"lark": "Lark Global"
|
||||
},
|
||||
"weixinOc": {
|
||||
"title": "QR вход в WeChat",
|
||||
"scanTitle": "Отсканируйте в мобильном WeChat",
|
||||
"created": "Вход выполнен",
|
||||
"status": {
|
||||
"idle": "Не начато",
|
||||
"starting": "Получение QR",
|
||||
"pending": "Ожидание сканирования",
|
||||
"created": "Вход выполнен - не забудьте нажать кнопку сохранения!",
|
||||
"denied": "Отменено",
|
||||
"expired": "Истекло",
|
||||
"error": "Ошибка входа"
|
||||
}
|
||||
},
|
||||
"start": "Начать настройку",
|
||||
"created": "Настройка завершена",
|
||||
"startFailed": "Не удалось начать QR настройку",
|
||||
|
||||
@@ -134,9 +134,24 @@
|
||||
"lark": {
|
||||
"title": "一键扫码创建",
|
||||
"show": "一键扫码创建",
|
||||
"scanTitle": "请使用手机飞书扫码",
|
||||
"feishu": "飞书国内版",
|
||||
"lark": "Lark 海外版"
|
||||
},
|
||||
"weixinOc": {
|
||||
"title": "个人微信扫码登录",
|
||||
"scanTitle": "请使用手机微信扫码",
|
||||
"created": "登录成功",
|
||||
"status": {
|
||||
"idle": "未开始",
|
||||
"starting": "正在获取二维码",
|
||||
"pending": "等待扫码确认",
|
||||
"created": "登录成功,记得点击下方保存按钮!",
|
||||
"denied": "用户取消",
|
||||
"expired": "已过期",
|
||||
"error": "登录失败"
|
||||
}
|
||||
},
|
||||
"start": "开始创建",
|
||||
"created": "创建成功",
|
||||
"startFailed": "发起扫码创建失败",
|
||||
|
||||
@@ -25,6 +25,8 @@ AstrBot supports connecting a personal WeChat account through the `Personal WeCh
|
||||
2. Click `Bots` in the left sidebar.
|
||||
3. Click `+ Create Bot` in the upper-right corner.
|
||||
4. Select `Personal WeChat`.
|
||||
5. The login QR code is shown directly. Scan it with WeChat on your phone and confirm the login inside WeChat.
|
||||
6. After login succeeds, click `Save`.
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
@@ -44,15 +46,12 @@ Leave the remaining options at their default values unless you explicitly know y
|
||||
|
||||
## QR Login
|
||||
|
||||
1. Fill in the configuration and click `Save`.
|
||||
2. Return to the bot list. AstrBot will automatically request a login QR code from WeChat.
|
||||
3. On the bot card, click `View QR Code` to open the QR dialog.
|
||||
4. Scan it with WeChat on your phone, then confirm the login inside WeChat.
|
||||
After you select `Personal WeChat`, AstrBot automatically requests a login QR code from WeChat and shows it directly in the create-bot dialog. Scan it with WeChat on your phone and confirm the login. When the QR area shows the login-success state, click `Save` to finish creating the bot.
|
||||
|
||||
After login succeeds, AstrBot will automatically persist the login state. On later restarts, if the session is still valid, you usually do not need to scan again.
|
||||
After login succeeds and the bot is saved, AstrBot will automatically persist the login state. On later restarts, if the session is still valid, you usually do not need to scan again.
|
||||
|
||||
> [!NOTE]
|
||||
> If the QR code expires, AstrBot will automatically request a new one. Please scan the refreshed QR code instead of the old one.
|
||||
> If the QR code expires, close and reopen the create-bot dialog, or select `Personal WeChat` again to request a new QR code.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配
|
||||
2. 点击左侧栏 `机器人`。
|
||||
3. 点击右上角 `+ 创建机器人`。
|
||||
4. 选择 `个人微信`。
|
||||
5. 页面会直接显示登录二维码,使用手机微信扫码,并在微信内确认登录。
|
||||
6. 登录成功后点击 `保存`。
|
||||
|
||||
## 配置项说明
|
||||
|
||||
@@ -42,18 +44,12 @@ AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配
|
||||
|
||||
## 扫码登录
|
||||
|
||||
1. 填好配置后点击 `保存`。
|
||||
2. 返回机器人列表,AstrBot 会自动向微信接口申请登录二维码。
|
||||
3. 在**机器人卡片**中点击 “查看二维码” 按钮,会弹出二维码对话框。(点击保存之后可能需要等 5 到 10 秒左右才会出现这个按钮)
|
||||
4. 使用手机微信扫码,并在微信内确认登录。
|
||||
选择 `个人微信` 后,AstrBot 会自动向微信接口申请登录二维码,并直接显示在创建机器人弹窗中。使用手机微信扫码确认后,二维码会显示登录成功状态,此时点击 `保存` 即可完成创建。
|
||||
|
||||

|
||||
|
||||
登录成功后,AstrBot 会自动保存登录态。后续重启时,如果登录态仍有效,通常不需要再次扫码。
|
||||
登录成功并保存后,AstrBot 会自动保存登录态。后续重启时,如果登录态仍有效,通常不需要再次扫码。
|
||||
|
||||
> [!NOTE]
|
||||
> 1. 如果二维码过期,AstrBot 会自动重新申请新的二维码。刷新后请使用新的二维码重新扫码。
|
||||
> 2. 如果 WebUI 没看到 “查看二维码” 按钮,可以前往终端或者 WebUI 控制台,找到 `请使用手机微信扫码登录,二维码有效期 5 分钟,过期后会自动刷新。` 对应的日志,附近会显示二维码扫码链接和终端直接输出的二维码,直选择一种方式扫码即可。
|
||||
> 如果二维码过期,请关闭并重新打开创建机器人弹窗,或重新选择 `个人微信` 以获取新的二维码。
|
||||
|
||||
## 验证
|
||||
|
||||
@@ -77,4 +73,3 @@ AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配
|
||||
|
||||
- 该适配器通过扫码登录个人微信,接入方式与微信公众号、企业微信不同。
|
||||
- 不需要配置公网回调地址,也不需要开启统一 Webhook 模式。
|
||||
|
||||
|
||||
47
tests/test_weixin_oc_login_registration.py
Normal file
47
tests/test_weixin_oc_login_registration.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from astrbot.core.platform.sources.weixin_oc.login_registration import (
|
||||
DEFAULT_WEIXIN_OC_BASE_URL,
|
||||
normalize_weixin_oc_base_url,
|
||||
weixin_oc_login_result,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_weixin_oc_base_url_uses_default_and_strips_slash():
|
||||
assert normalize_weixin_oc_base_url("") == DEFAULT_WEIXIN_OC_BASE_URL
|
||||
assert (
|
||||
normalize_weixin_oc_base_url("https://ilinkai.weixin.qq.com/")
|
||||
== DEFAULT_WEIXIN_OC_BASE_URL
|
||||
)
|
||||
|
||||
|
||||
def test_weixin_oc_login_result_maps_confirmed_payload():
|
||||
result = weixin_oc_login_result(
|
||||
{
|
||||
"status": "confirmed",
|
||||
"bot_token": "token",
|
||||
"ilink_bot_id": "bot-id",
|
||||
"baseurl": "https://example.com/",
|
||||
"ilink_user_id": "user-id",
|
||||
},
|
||||
default_base_url=DEFAULT_WEIXIN_OC_BASE_URL,
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"status": "created",
|
||||
"qr_status": "confirmed",
|
||||
"weixin_oc_token": "token",
|
||||
"weixin_oc_account_id": "bot-id",
|
||||
"weixin_oc_base_url": "https://example.com",
|
||||
"weixin_oc_user_id": "user-id",
|
||||
}
|
||||
|
||||
|
||||
def test_weixin_oc_login_result_maps_wait_and_expired_payloads():
|
||||
assert weixin_oc_login_result(
|
||||
{"status": "wait"},
|
||||
default_base_url=DEFAULT_WEIXIN_OC_BASE_URL,
|
||||
) == {"status": "pending", "qr_status": "wait"}
|
||||
|
||||
assert weixin_oc_login_result(
|
||||
{"status": "expired"},
|
||||
default_base_url=DEFAULT_WEIXIN_OC_BASE_URL,
|
||||
) == {"status": "expired", "qr_status": "expired", "message": "二维码已过期"}
|
||||
Reference in New Issue
Block a user