Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from .history import router as history_router
from .info import router as info_router
from .ocr import router as ocr_router
from .openclaw_qq import router as openclaw_qq_router
from .openclaw_weixin import router as openclaw_weixin_router
from .plan import router as plan_router
from .queue import router as queue_router
from .scripts import router as scripts_router
Expand All @@ -53,5 +55,7 @@
"setting_router",
"update_router",
"ocr_router",
"openclaw_qq_router",
"openclaw_weixin_router",
"qr_login_router",
]
132 changes: 132 additions & 0 deletions app/api/openclaw_qq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025-2026 AUTO-MAS Team

# This file is part of AUTO-MAS.

# AUTO-MAS is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# AUTO-MAS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.

# Contact: DLmaster_361@163.com

"""QQ 官方机器人扫码绑定 API。"""

from fastapi import APIRouter, Body

from app.models.schema import (
OpenClawQQQrCheckIn,
OpenClawQQQrCheckOut,
OpenClawQQQrStartOut,
OpenClawQQStatusOut,
OutBase,
)
from app.services.openclaw_qq import openclaw_qq_manager

router = APIRouter(prefix="/api/setting/openclaw-qq", tags=["QQ 官方机器人通知"])


@router.post(
"/status",
summary="查询 QQ 官方机器人绑定状态",
response_model=OpenClawQQStatusOut,
)
async def get_status() -> OpenClawQQStatusOut:
"""返回 QQ 绑定状态,不返回协议凭据。"""

try:
state = openclaw_qq_manager.status()
except Exception as exc:
return OpenClawQQStatusOut(
code=500,
status="error",
message=f"查询 QQ 登录状态失败: {type(exc).__name__}: {exc}",
)
return OpenClawQQStatusOut(
enabled=state.enabled,
connected=state.connected,
state=state.state,
message=state.message,
)


@router.post(
"/login/start",
summary="创建 QQ 官方机器人登录二维码",
response_model=OpenClawQQQrStartOut,
)
async def start_login() -> OpenClawQQQrStartOut:
"""创建二维码;App ID 和客户端密钥只在后台登录确认后保存。"""

try:
result = await openclaw_qq_manager.start_login()
except ValueError as exc:
return OpenClawQQQrStartOut(code=400, status="error", message=str(exc))
except Exception as exc:
return OpenClawQQQrStartOut(
code=500,
status="error",
message=f"创建 QQ 二维码失败: {type(exc).__name__}: {exc}",
)
return OpenClawQQQrStartOut(
sessionId=result.session_id,
qrUrl=result.qr_url,
message="请使用 QQ 扫描二维码",
)


@router.post(
"/login/check",
summary="查询 QQ 官方机器人登录状态",
response_model=OpenClawQQQrCheckOut,
)
async def check_login(
body: OpenClawQQQrCheckIn = Body(...),
) -> OpenClawQQQrCheckOut:
"""轮询二维码状态;确认后自动保存 QQ 机器人凭据。"""

try:
result = await openclaw_qq_manager.check_login(session_id=body.sessionId)
except Exception as exc:
return OpenClawQQQrCheckOut(
code=500,
status="error",
sessionId=body.sessionId,
state="error",
message=f"查询 QQ 登录状态失败: {type(exc).__name__}: {exc}",
)
return OpenClawQQQrCheckOut(
sessionId=result.session_id,
state=result.state,
connected=result.connected,
message=result.message,
)


@router.post(
"/unbind",
summary="解除 QQ 官方机器人绑定",
response_model=OutBase,
)
async def unbind() -> OutBase:
"""解除绑定并清理本地保存的 QQ 协议状态。"""

try:
await openclaw_qq_manager.unbind()
except Exception as exc:
return OutBase(
code=500,
status="error",
message=f"解除 QQ 绑定失败: {type(exc).__name__}: {exc}",
)
return OutBase(message="QQ 官方机器人已解除绑定")
137 changes: 137 additions & 0 deletions app/api/openclaw_weixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025-2026 AUTO-MAS Team

# This file is part of AUTO-MAS.

# AUTO-MAS is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# AUTO-MAS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.

# Contact: DLmaster_361@163.com

"""微信 Claw 扫码绑定 API。"""

from fastapi import APIRouter, Body

from app.models.schema import (
OpenClawWeixinQrCheckIn,
OpenClawWeixinQrCheckOut,
OpenClawWeixinQrStartOut,
OpenClawWeixinStatusOut,
OutBase,
)
from app.services.openclaw_weixin import openclaw_weixin_manager

router = APIRouter(
prefix="/api/setting/openclaw-weixin", tags=["微信 Claw 通知"]
)


@router.post(
"/status",
summary="查询微信 Claw 绑定状态",
response_model=OpenClawWeixinStatusOut,
)
async def get_status() -> OpenClawWeixinStatusOut:
"""返回微信绑定状态,不返回协议凭据。"""

try:
state = openclaw_weixin_manager.status()
except Exception as exc:
return OpenClawWeixinStatusOut(
code=500,
status="error",
message=f"查询微信绑定状态失败: {type(exc).__name__}: {exc}",
)
return OpenClawWeixinStatusOut(
enabled=state.enabled,
connected=state.connected,
state=state.state,
message=state.message,
)


@router.post(
"/login/start",
summary="创建微信 Claw 登录二维码",
response_model=OpenClawWeixinQrStartOut,
)
async def start_login() -> OpenClawWeixinQrStartOut:
"""创建二维码;Bot Token 等凭据只在后台登录确认后保存。"""

try:
result = await openclaw_weixin_manager.start_login()
except ValueError as exc:
return OpenClawWeixinQrStartOut(code=400, status="error", message=str(exc))
except Exception as exc:
return OpenClawWeixinQrStartOut(
code=500,
status="error",
message=f"创建微信二维码失败: {type(exc).__name__}: {exc}",
)
return OpenClawWeixinQrStartOut(
sessionId=result.session_id,
qrUrl=result.qr_url,
message="请使用微信扫描二维码",
)


@router.post(
"/login/check",
summary="查询微信 Claw 登录状态",
response_model=OpenClawWeixinQrCheckOut,
)
async def check_login(
body: OpenClawWeixinQrCheckIn = Body(...),
) -> OpenClawWeixinQrCheckOut:
"""查询二维码状态;确认后自动保存账号凭据。"""

try:
result = await openclaw_weixin_manager.check_login(
session_id=body.sessionId,
verify_code=body.verifyCode,
)
except Exception as exc:
return OpenClawWeixinQrCheckOut(
code=500,
status="error",
sessionId=body.sessionId,
state="error",
message=f"查询微信登录状态失败: {type(exc).__name__}: {exc}",
)
return OpenClawWeixinQrCheckOut(
sessionId=result.session_id,
state=result.state,
connected=result.connected,
message=result.message,
)


@router.post(
"/unbind",
summary="解除微信 Claw 绑定",
response_model=OutBase,
)
async def unbind() -> OutBase:
"""解除绑定并清理本地保存的微信协议状态。"""

try:
await openclaw_weixin_manager.unbind()
except Exception as exc:
return OutBase(
code=500,
status="error",
message=f"解除微信绑定失败: {type(exc).__name__}: {exc}",
)
return OutBase(message="微信 Claw 已解除绑定")
38 changes: 38 additions & 0 deletions app/core/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ def koishi_content(self) -> str:
return self.koishi_text
return f"{self.title}\n\n{self.signed_text}"

@property
def openclaw_weixin_content(self) -> str:
"""返回微信(iLink)正文。"""

return f"{self.title}\n\n{self.signed_text}"

@property
def openclaw_qq_content(self) -> str:
"""返回 QQ 官方机器人正文。"""

return f"{self.title}\n\n{self.signed_text}"

@property
def system_content(self) -> str:
"""返回系统通知正文。"""
Expand All @@ -117,6 +129,8 @@ class NotifyTarget:
serverchan_key: str | None = None
webhooks: Iterable[tuple[str, Any]] = ()
koishi: bool = False
openclaw_weixin: bool = False
openclaw_qq: bool = False
empty_policy: EmptyPolicy = "send"


Expand Down Expand Up @@ -166,6 +180,8 @@ def global_target(
),
webhooks=_webhooks(Config.Notify_CustomWebhooks),
koishi=bool(Config.get("Notify", "IfKoishiSupport")),
openclaw_weixin=bool(Config.get("Notify", "IfOpenClawWeixin")),
openclaw_qq=bool(Config.get("Notify", "IfOpenClawQQ")),
empty_policy=empty_policy,
)

Expand Down Expand Up @@ -257,6 +273,10 @@ def target_channel_names(target: NotifyTarget) -> tuple[str, ...]:
)
if target.koishi:
names.append(f"{target.name} Koishi")
if target.openclaw_weixin:
names.append(f"{target.name} 微信(iLink)")
if target.openclaw_qq:
names.append(f"{target.name} QQ(官方机器人)")
return tuple(names)


Expand Down Expand Up @@ -412,6 +432,24 @@ def miss(channel: str) -> None:
lambda: Notify.send_koishi(payload.koishi_content),
)

if target.openclaw_weixin:
await attempt(
f"{target.name} 微信(iLink)",
lambda: Notify.send_openclaw_weixin(
title=payload.title,
content=payload.openclaw_weixin_content,
),
)

if target.openclaw_qq:
await attempt(
f"{target.name} QQ(官方机器人)",
lambda: Notify.send_openclaw_qq(
title=payload.title,
content=payload.openclaw_qq_content,
),
)

return DispatchResult(
attempted=attempted,
succeeded=tuple(succeeded),
Expand Down
Loading
Loading