Files
NewWPSBot/Plugins/WPSAPI.py

1051 lines
34 KiB
Python
Raw Normal View History

2025-11-06 11:01:13 +08:00
from PWF.Convention.Runtime.Config import *
from PWF.CoreModules.plugin_interface import PluginInterface
from PWF.CoreModules.flags import *
2025-11-08 14:10:08 +08:00
from PWF.Convention.Runtime.Architecture import Architecture
2025-11-06 11:01:13 +08:00
from PWF.Convention.Runtime.GlobalConfig import ProjectConfig
from PWF.Convention.Runtime.Web import ToolURL
from PWF.Convention.Runtime.String import LimitStringLength
2025-11-12 22:58:36 +08:00
from fastapi.responses import HTMLResponse
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, List, Optional, Sequence, TypedDict, override, Union
2025-11-06 11:01:13 +08:00
import httpx
2025-11-06 11:46:54 +08:00
import re
2025-11-06 11:01:13 +08:00
2025-11-08 14:10:08 +08:00
logger: ProjectConfig = Architecture.Get(ProjectConfig)
2025-11-07 23:48:10 +08:00
MAIN_WEBHOOK_URL = logger.FindItem("main_webhook_url", "")
logger.SaveProperties()
2025-11-06 11:01:13 +08:00
2025-11-06 16:26:07 +08:00
2025-11-12 22:58:36 +08:00
class GuideEntry(TypedDict, total=False):
"""单条图鉴信息。"""
title: str
identifier: str
description: str
category: str
metadata: Dict[str, str]
icon: str
badge: str
links: Sequence[Dict[str, str]]
tags: Sequence[str]
details: Sequence[Union[str, Dict[str, Any]]]
group: str
@dataclass(frozen=True)
class GuideSection:
"""图鉴章节。"""
title: str
entries: Sequence[GuideEntry] = field(default_factory=tuple)
description: str = ""
layout: str = "grid"
section_id: str | None = None
@dataclass(frozen=True)
class GuidePage:
"""完整图鉴页面。"""
title: str
sections: Sequence[GuideSection] = field(default_factory=tuple)
subtitle: str = ""
metadata: Dict[str, str] = field(default_factory=dict)
related_links: Dict[str, Sequence[Dict[str, str]]] = field(default_factory=dict)
def render_markdown_page(page: GuidePage) -> str:
"""保留 Markdown 渲染(备用)。"""
def _render_section(section: GuideSection) -> str:
lines: List[str] = [f"## {section.title}"]
if section.description:
lines.append(section.description)
if not section.entries:
lines.append("> 暂无内容。")
return "\n".join(lines)
for entry in section.entries:
title = entry.get("title", "未命名")
identifier = entry.get("identifier")
desc = entry.get("description", "")
category = entry.get("category")
metadata = entry.get("metadata", {})
bullet = f"- **{title}**"
if identifier:
bullet += f"`{identifier}`"
if category:
bullet += f"{category}"
lines.append(bullet)
if desc:
lines.append(f" - {desc}")
for meta_key, meta_value in metadata.items():
lines.append(f" - {meta_key}{meta_value}")
return "\n".join(lines)
lines: List[str] = [f"# {page.title}"]
if page.subtitle:
lines.append(page.subtitle)
if page.metadata:
lines.append("")
for key, value in page.metadata.items():
lines.append(f"- {key}{value}")
for section in page.sections:
lines.append("")
lines.append(_render_section(section))
return "\n".join(lines)
def render_html_page(page: GuidePage) -> str:
"""渲染 Apple Store 风格的 HTML 页面。"""
def escape(text: Optional[str]) -> str:
if not text:
return ""
return (
text.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
def render_metadata(metadata: Dict[str, str]) -> str:
if not metadata:
return ""
cards = []
for key, value in metadata.items():
cards.append(
f"""
<div class="meta-card">
<div class="meta-key">{escape(key)}</div>
<div class="meta-value">{escape(value)}</div>
</div>
"""
)
return f'<section class="meta-grid">{"".join(cards)}</section>'
def render_links(links: Optional[Sequence[Dict[str, str]]]) -> str:
if not links:
return ""
items = []
for link in links:
href = escape(link.get("href", "#"))
label = escape(link.get("label", "前往"))
items.append(f'<a class="entry-link" href="{href}" target="_blank">{label}</a>')
return "".join(items)
def render_tags(tags: Optional[Sequence[str]]) -> str:
if not tags:
return ""
chips = "".join(f'<span class="entry-tag">{escape(tag)}</span>' for tag in tags)
return f'<div class="entry-tags-extra">{chips}</div>'
def render_details(details: Optional[Sequence[Union[str, Dict[str, Any]]]]) -> str:
if not details:
return ""
blocks: List[str] = []
for detail in details:
if isinstance(detail, str):
blocks.append(f'<p class="entry-detail-paragraph">{escape(detail)}</p>')
elif isinstance(detail, dict):
kind = detail.get("type")
if kind == "list":
items = "".join(
f'<li>{escape(str(item))}</li>' for item in detail.get("items", [])
)
blocks.append(f'<ul class="entry-detail-list">{items}</ul>')
elif kind == "steps":
items = "".join(
f'<li><span class="step-index">{idx+1}</span><span>{escape(str(item))}</span></li>'
for idx, item in enumerate(detail.get("items", []))
)
blocks.append(f'<ol class="entry-detail-steps">{items}</ol>')
elif kind == "table":
rows = []
for row in detail.get("rows", []):
cols = "".join(f"<td>{escape(str(col))}</td>" for col in row)
rows.append(f"<tr>{cols}</tr>")
head = ""
headers = detail.get("headers")
if headers:
head = "".join(f"<th>{escape(str(col))}</th>" for col in headers)
head = f"<thead><tr>{head}</tr></thead>"
blocks.append(
f'<table class="entry-detail-table">{head}<tbody>{"".join(rows)}</tbody></table>'
)
if not blocks:
return ""
return f'<div class="entry-details">{"".join(blocks)}</div>'
def render_entry(entry: GuideEntry) -> str:
icon = escape(entry.get("icon"))
badge = escape(entry.get("badge"))
title = escape(entry.get("title"))
identifier = escape(entry.get("identifier"))
description = escape(entry.get("description"))
category = escape(entry.get("category"))
metadata_items = []
for meta_key, meta_value in entry.get("metadata", {}).items():
metadata_items.append(
f'<li><span>{escape(meta_key)}</span><span>{escape(str(meta_value))}</span></li>'
)
metadata_html = ""
if metadata_items:
metadata_html = f'<ul class="entry-meta">{"".join(metadata_items)}</ul>'
identifier_html = f'<code class="entry-id">{identifier}</code>' if identifier else ""
category_html = f'<span class="entry-category">{category}</span>' if category else ""
badge_html = f'<span class="entry-badge">{badge}</span>' if badge else ""
icon_html = f'<div class="entry-icon">{icon}</div>' if icon else ""
links_html = render_links(entry.get("links"))
tags_html = render_tags(entry.get("tags"))
details_html = render_details(entry.get("details"))
group = escape(entry.get("group"))
group_attr = f' data-group="{group}"' if group else ""
return f"""
<article class="entry-card"{group_attr}>
{icon_html}
<div class="entry-content">
<header>
<h3>{title}{badge_html}</h3>
<div class="entry-tags">{identifier_html}{category_html}</div>
</header>
<p class="entry-desc">{description}</p>
{metadata_html}
{tags_html}
{details_html}
{links_html}
</div>
</article>
"""
def render_section(section: GuideSection) -> str:
layout_class = "entries-grid" if section.layout == "grid" else "entries-list"
section_attr = f' id="{escape(section.section_id)}"' if section.section_id else ""
cards = "".join(render_entry(entry) for entry in section.entries)
description_html = (
f'<p class="section-desc">{escape(section.description)}</p>'
if section.description
else ""
)
if not cards:
cards = '<div class="empty-placeholder">暂无内容</div>'
return f"""
<section class="guide-section"{section_attr}>
<div class="section-header">
<h2>{escape(section.title)}</h2>
{description_html}
</div>
<div class="{layout_class}">
{cards}
</div>
</section>
"""
def render_related(related: Dict[str, Sequence[Dict[str, str]]]) -> str:
if not related:
return ""
blocks: List[str] = []
for label, links in related.items():
if not links:
continue
items = "".join(
f'<a class="related-link" href="{escape(link.get("href", "#"))}">{escape(link.get("label", ""))}</a>'
for link in links
)
blocks.append(
f"""
<div class="related-block">
<div class="related-label">{escape(label)}</div>
<div class="related-items">{items}</div>
</div>
"""
)
if not blocks:
return ""
return f'<section class="related-section">{"".join(blocks)}</section>'
sections_html = "".join(render_section(section) for section in page.sections)
metadata_html = render_metadata(page.metadata)
related_html = render_related(page.related_links)
subtitle_html = f'<p class="hero-subtitle">{escape(page.subtitle)}</p>' if page.subtitle else ""
return f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{escape(page.title)}</title>
<style>
:root {{
color-scheme: dark;
--bg-primary: radial-gradient(120% 120% at 10% 10%, #222 0%, #050505 100%);
--bg-card: rgba(255, 255, 255, 0.06);
--bg-card-hover: rgba(255, 255, 255, 0.12);
--border-soft: rgba(255, 255, 255, 0.1);
--text-main: #f5f5f7;
--text-sub: rgba(245, 245, 247, 0.64);
--accent: linear-gradient(135deg, #4b7bec, #34e7e4);
--accent-strong: linear-gradient(135deg, #ff9f1a, #ff3f34);
}}
* {{
box-sizing: border-box;
}}
body {{
margin: 0;
font-family: "SF Pro Display", "SF Pro SC", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg-primary);
color: var(--text-main);
min-height: 100vh;
padding: 0 0 64px;
}}
a {{
color: #9fc9ff;
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
header.hero {{
padding: 80px 24px 40px;
text-align: center;
position: relative;
}}
header.hero::after {{
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.18), transparent 55%);
pointer-events: none;
z-index: -1;
}}
.hero-title {{
font-size: clamp(32px, 5vw, 48px);
font-weight: 700;
margin: 0;
background: var(--accent);
-webkit-background-clip: text;
color: transparent;
letter-spacing: 0.8px;
}}
.hero-subtitle {{
margin: 16px auto 0;
max-width: 640px;
font-size: 18px;
color: var(--text-sub);
line-height: 1.6;
}}
main {{
width: min(1100px, 92vw);
margin: 0 auto;
}}
.meta-grid {{
display: grid;
gap: 20px;
margin: 0 auto 48px;
padding: 0 8px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}}
.related-section {{
margin: 0 auto 48px;
padding: 0 8px;
display: grid;
gap: 18px;
}}
.related-block {{
display: grid;
gap: 8px;
}}
.related-label {{
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-sub);
}}
.related-items {{
display: flex;
gap: 10px;
flex-wrap: wrap;
}}
.related-link {{
display: inline-flex;
align-items: center;
padding: 6px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
font-size: 13px;
letter-spacing: 0.4px;
transition: background 0.2s ease;
}}
.related-link:hover {{
background: rgba(255, 255, 255, 0.16);
text-decoration: none;
}}
.meta-card {{
border-radius: 18px;
padding: 22px;
border: 1px solid var(--border-soft);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(16px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
}}
.meta-key {{
font-size: 13px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-sub);
}}
.meta-value {{
margin-top: 6px;
font-size: 22px;
font-weight: 600;
}}
.guide-section {{
margin: 0 auto 56px;
padding: 0 8px;
}}
.section-header {{
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 28px;
}}
.section-header h2 {{
margin: 0;
font-size: clamp(26px, 3vw, 32px);
letter-spacing: 0.5px;
}}
.section-desc {{
margin: 0;
font-size: 16px;
color: var(--text-sub);
max-width: 640px;
}}
.entries-grid {{
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}}
.entries-list {{
display: flex;
flex-direction: column;
gap: 18px;
}}
.entry-card {{
display: flex;
gap: 18px;
border-radius: 20px;
padding: 24px;
border: 1px solid transparent;
background: var(--bg-card);
transition: border 0.2s ease, background 0.2s ease, transform 0.2s ease;
}}
.entry-card:hover {{
background: var(--bg-card-hover);
border-color: rgba(255, 255, 255, 0.18);
transform: translateY(-4px);
}}
.entry-icon {{
font-size: 36px;
}}
.entry-content {{
flex: 1;
}}
.entry-content header {{
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
}}
.entry-content h3 {{
margin: 0;
font-size: 20px;
display: inline-flex;
align-items: center;
gap: 10px;
}}
.entry-badge {{
display: inline-flex;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
letter-spacing: 0.5px;
background: var(--accent-strong);
}}
.entry-tags {{
display: flex;
gap: 12px;
flex-wrap: wrap;
}}
.entry-tags-extra {{
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}}
.entry-id {{
background: rgba(255, 255, 255, 0.1);
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
letter-spacing: 0.5px;
}}
.entry-category {{
font-size: 12px;
color: var(--text-sub);
}}
.entry-desc {{
margin: 0 0 12px;
color: var(--text-sub);
line-height: 1.6;
}}
.entry-meta {{
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 6px;
font-size: 13px;
color: var(--text-sub);
}}
.entry-meta li {{
display: flex;
justify-content: space-between;
}}
.entry-link {{
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 10px;
font-size: 14px;
}}
.entry-tag {{
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
font-size: 12px;
letter-spacing: 0.4px;
}}
.entry-details {{
margin-top: 14px;
display: grid;
gap: 10px;
}}
.entry-detail-paragraph {{
margin: 0;
color: var(--text-sub);
line-height: 1.6;
}}
.entry-detail-list, .entry-detail-steps {{
margin: 0;
padding-left: 20px;
color: var(--text-sub);
}}
.entry-detail-steps {{
list-style: none;
padding-left: 0;
}}
.entry-detail-steps li {{
display: grid;
grid-template-columns: 28px 1fr;
align-items: start;
gap: 10px;
margin-bottom: 6px;
}}
.entry-detail-steps .step-index {{
width: 28px;
height: 28px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.12);
font-size: 12px;
}}
.entry-detail-table {{
width: 100%;
border-collapse: collapse;
border-radius: 14px;
overflow: hidden;
}}
.entry-detail-table th,
.entry-detail-table td {{
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 12px;
font-size: 13px;
text-align: left;
}}
.entry-detail-table th {{
background: rgba(255, 255, 255, 0.08);
font-weight: 600;
}}
.entry-detail-table tr:nth-child(even) {{
background: rgba(255, 255, 255, 0.04);
}}
.empty-placeholder {{
padding: 40px;
text-align: center;
border-radius: 20px;
border: 1px dashed rgba(255, 255, 255, 0.2);
color: var(--text-sub);
}}
@media (max-width: 680px) {{
.entry-card {{
flex-direction: column;
}}
.entry-content h3 {{
font-size: 18px;
}}
.meta-grid {{
grid-template-columns: 1fr 1fr;
}}
}}
@media (max-width: 480px) {{
.meta-grid {{
grid-template-columns: 1fr;
}}
}}
</style>
</head>
<body>
<header class="hero">
<h1 class="hero-title">{escape(page.title)}</h1>
{subtitle_html}
</header>
<main>
{metadata_html}
{related_html}
{sections_html}
</main>
</body>
</html>
"""
2025-11-06 11:01:13 +08:00
class MessageSender:
"""消息发送器"""
def __init__(self, webhook_url: str):
"""初始化消息发送器
Args:
webhook_url: Webhook URL
"""
self.webhook_url = webhook_url
self.client: Optional[httpx.AsyncClient] = None
async def _get_client(self) -> httpx.AsyncClient:
"""获取HTTP客户端懒加载"""
if self.client is None:
self.client = httpx.AsyncClient(timeout=10.0)
return self.client
async def send_message(self, message: Dict[str, Any]) -> bool:
"""发送消息到WPS
Args:
message: 消息字典
Returns:
是否发送成功
"""
try:
client = await self._get_client()
response = await client.post(self.webhook_url, json=message)
if response.status_code == 200:
2025-11-07 23:48:10 +08:00
logger.Log("Info", f"消息发送成功: {message.get('msgtype')}")
2025-11-06 11:01:13 +08:00
return True
else:
2025-11-07 23:48:10 +08:00
logger.Log("Error", f"消息发送失败: status={response.status_code}, body={response.text}")
2025-11-06 11:01:13 +08:00
return False
except Exception as e:
2025-11-07 23:48:10 +08:00
logger.Log("Error", f"发送消息异常: {e}")
2025-11-06 11:01:13 +08:00
return False
async def send_text(self, content: str, at_user_id: Optional[int] = None) -> bool:
"""发送文本消息
Args:
content: 文本内容
at_user_id: @用户ID可选
Returns:
是否发送成功
"""
# 如果需要@人
if at_user_id:
content = f'<at user_id="{at_user_id}"></at> {content}'
message = {
"msgtype": "text",
"text": {
"content": content
}
}
return await self.send_message(message)
async def send_markdown(self, text: str) -> bool:
"""发送Markdown消息
Args:
text: Markdown文本
Returns:
是否发送成功
"""
message = {
"msgtype": "markdown",
"markdown": {
"text": text
}
}
return await self.send_message(message)
async def send_link(self, title: str, text: str,
message_url: str = "", btn_title: str = "查看详情") -> bool:
"""发送链接消息
Args:
title: 标题
text: 文本内容
message_url: 跳转URL
btn_title: 按钮文字
Returns:
是否发送成功
"""
message = {
"msgtype": "link",
"link": {
"title": title,
"text": text,
"messageUrl": message_url,
"btnTitle": btn_title
}
}
return await self.send_message(message)
async def close(self):
"""关闭HTTP客户端"""
if self.client:
await self.client.aclose()
self.client = None
class BasicWPSInterface(PluginInterface):
2025-11-13 14:23:31 +08:00
user_id_to_username: Optional[Callable[[int], str]] = None
2025-11-06 11:01:13 +08:00
@override
def is_enable_plugin(self) -> bool:
return False
def get_webhook_url(self, message: str, user_id: int) -> str:
'''
根据消息和用户ID获取Webhook URL, 返回空字符串表示不需要回复消息
Args:
message: 消息内容
user_id: 用户ID
Returns:
Webhook URL
'''
return ""
def get_webhook_request(self, data:Any|None) -> None:
pass
def get_message_sender(self, webhook_url: str) -> MessageSender:
return MessageSender(webhook_url)
def get_message_sender_type(self) -> Literal["text", "markdown", "link"]:
return "markdown"
def get_message_sender_function(self, webhook_url: str, type: Literal["text", "markdown", "link"]) -> Coroutine[Any, Any, bool]:
if type == "text":
return self.get_message_sender(webhook_url).send_text
elif type == "markdown":
return self.get_message_sender(webhook_url).send_markdown
elif type == "link":
return self.get_message_sender(webhook_url).send_link
else:
raise ValueError(f"Invalid message sender type: {type}")
2025-11-06 16:26:07 +08:00
def parse_message_after_at(self, message: str) -> str:
'''
已过时
'''
2025-11-06 16:26:07 +08:00
return message
async def send_markdown_message(self, message: str, chat_id: int, user_id: int) -> str|None:
webhook_url = self.get_webhook_url(message, user_id)
if get_internal_debug():
2025-11-07 23:48:10 +08:00
logger.Log("Info", f"Webhook URL: {webhook_url}, Message: {LimitStringLength(message)}, User ID: {user_id}")
2025-11-06 16:26:07 +08:00
if webhook_url == "" or webhook_url == None:
return None
2025-11-06 11:46:54 +08:00
2025-11-13 14:23:31 +08:00
username: str = self.user_id_to_username(user_id) if self.user_id_to_username else ""
send_message = f"""## <at user_id="{user_id}">{username}</at>
2025-11-08 15:34:44 +08:00
---
{message}
2025-11-13 14:23:31 +08:00
"""
result = await self.get_message_sender_function(webhook_url, self.get_message_sender_type())(send_message)
2025-11-07 15:53:09 +08:00
if get_internal_verbose():
2025-11-07 23:48:10 +08:00
logger.Log("Info", f"Webhook URL: {webhook_url}, Message: {LimitStringLength(message)}, Result: {result}")
2025-11-06 11:01:13 +08:00
return None
2025-11-06 16:26:07 +08:00
@override
async def callback(self, message: str, chat_id: int, user_id: int) -> str|None:
message = self.parse_message_after_at(message)
if message == "":
return None
return await self.send_markdown_message(message, chat_id, user_id)
2025-11-06 11:01:13 +08:00
class WPSAPI(BasicWPSInterface):
2025-11-12 22:58:36 +08:00
"""核心 WPS 插件基类,提供图鉴模板设施。"""
guide_section_labels: Dict[str, str] = {
"commands": "指令一览",
"items": "物品与资源",
"recipes": "配方与合成",
"guides": "系统指引",
}
def get_guide_title(self) -> str:
return self.__class__.__name__
def get_guide_subtitle(self) -> str:
return ""
def get_guide_metadata(self) -> Dict[str, str]:
return {}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return ()
def collect_item_entries(self) -> Sequence[GuideEntry]:
return ()
def collect_recipe_entries(self) -> Sequence[GuideEntry]:
return ()
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return ()
def collect_additional_sections(self) -> Sequence[GuideSection]:
return ()
def collect_guide_sections(self) -> Sequence[GuideSection]:
sections: List[GuideSection] = []
command_entries = tuple(self.collect_command_entries())
if command_entries:
sections.append(
GuideSection(
title=self.guide_section_labels["commands"],
entries=command_entries,
layout="list",
)
)
item_entries = tuple(self.collect_item_entries())
if item_entries:
sections.append(
GuideSection(
title=self.guide_section_labels["items"],
entries=item_entries,
)
)
recipe_entries = tuple(self.collect_recipe_entries())
if recipe_entries:
sections.append(
GuideSection(
title=self.guide_section_labels["recipes"],
entries=recipe_entries,
)
)
guide_entries = tuple(self.collect_guide_entries())
if guide_entries:
sections.append(
GuideSection(
title=self.guide_section_labels["guides"],
entries=guide_entries,
layout="list",
)
)
additional_sections = tuple(self.collect_additional_sections())
if additional_sections:
sections.extend(additional_sections)
return tuple(sections)
def build_guide_page(self) -> GuidePage:
metadata: Dict[str, str] = {}
for key, value in self.get_guide_metadata().items():
metadata[key] = str(value)
related = self.get_related_links()
return GuidePage(
title=self.get_guide_title(),
subtitle=self.get_guide_subtitle(),
sections=self.collect_guide_sections(),
metadata=metadata,
related_links=related,
)
def render_guide_page(self, page: GuidePage) -> str:
return render_html_page(page)
def render_guide(self) -> str:
return self.render_guide_page(self.build_guide_page())
def get_guide_response(self, content: str) -> HTMLResponse:
return HTMLResponse(content)
@override
def generate_router_illustrated_guide(self):
async def handler() -> HTMLResponse:
return self.get_guide_response(self.render_guide())
return handler
def get_related_links(self) -> Dict[str, Sequence[Dict[str, str]]]:
links: Dict[str, Sequence[Dict[str, str]]] = {}
parents = []
for base in self.__class__.__mro__[1:]:
if not issubclass(base, WPSAPI):
continue
if base.__module__.startswith("Plugins."):
parents.append(base)
if base is WPSAPI:
break
if parents:
parents_links = [self._build_class_link(cls) for cls in reversed(parents)]
links["父类链"] = tuple(filter(None, parents_links))
child_links = [self._build_class_link(child) for child in self._iter_subclasses(self.__class__)]
child_links = [link for link in child_links if link]
if child_links:
links["子类"] = tuple(child_links)
return links
def _build_class_link(self, cls: type) -> Optional[Dict[str, str]]:
if not hasattr(cls, "__module__") or not cls.__module__.startswith("Plugins."):
return None
path = f"/api/{cls.__name__}"
return {
"label": cls.__name__,
"href": path,
}
def _iter_subclasses(self, cls: type) -> List[type]:
collected: List[type] = []
for subclass in cls.__subclasses__():
if not issubclass(subclass, WPSAPI):
continue
if not subclass.__module__.startswith("Plugins."):
continue
collected.append(subclass)
collected.extend(self._iter_subclasses(subclass))
return collected
def get_guide_subtitle(self) -> str:
return "核心 Webhook 转发插件"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"Webhook 状态": "已配置" if MAIN_WEBHOOK_URL else "未配置",
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "say",
"identifier": "say",
"description": "将后续消息内容以 Markdown 形式发送到主 Webhook。",
"metadata": {"别名": ""},
"icon": "🗣️",
"badge": "核心",
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "Webhook 绑定",
"description": (
"在项目配置中设置 `main_webhook_url` 后插件自动启用,"
"所有注册的命令将调用 `send_markdown_message` 发送富文本。"
),
"icon": "🔗",
},
{
"title": "消息格式",
"description": (
"默认使用 Markdown 模式发送,支持 `聊天ID` 与 `用户ID` 的 @ 提醒。"
),
"icon": "📝",
},
)
2025-11-06 11:01:13 +08:00
@override
def is_enable_plugin(self) -> bool:
if MAIN_WEBHOOK_URL == "":
2025-11-07 23:48:10 +08:00
logger.Log("Error", f"{ConsoleFrontColor.RED}WPSAPI未配置主Webhook URL{ConsoleFrontColor.RESET}")
2025-11-06 11:01:13 +08:00
return MAIN_WEBHOOK_URL != ""
@override
def get_webhook_url(self, message: str, user_id: int) -> str:
return MAIN_WEBHOOK_URL
@override
def get_webhook_request(self, data:Any|None) -> None:
return None
@override
def wake_up(self) -> None:
2025-11-07 23:48:10 +08:00
logger.Log("Info", f"{ConsoleFrontColor.GREEN}WPSAPI核心插件已加载{ConsoleFrontColor.RESET}")
2025-11-06 11:01:13 +08:00
self.register_plugin("say")
2025-11-06 11:46:54 +08:00
self.register_plugin("")
2025-11-06 11:01:13 +08:00
class WPSAPIHelp(WPSAPI):
@override
def dependencies(self) -> List[Type]:
return [WPSAPI]
@override
def is_enable_plugin(self) -> bool:
return True
@override
async def callback(self, message: str, chat_id: int, user_id: int) -> str|None:
2025-11-10 21:44:28 +08:00
mapper: Dict[str, List[str]] = {}
for key in PluginInterface.plugin_instances.keys():
plugin = PluginInterface.plugin_instances.get(key)
if plugin:
if plugin.__class__.__name__ not in mapper:
mapper[plugin.__class__.__name__] = []
mapper[plugin.__class__.__name__].append(key)
desc = ""
for plugin_name, keys in mapper.items():
desc += f"- {plugin_name}: {", ".join(keys)}\n"
return await self.send_markdown_message(f"""# 指令入口
{desc}
""", chat_id, user_id)
@override
def wake_up(self) -> None:
logger.Log("Info", f"{ConsoleFrontColor.GREEN}WPSAPIHelp 插件已加载{ConsoleFrontColor.RESET}")
self.register_plugin("help")
self.register_plugin("帮助")
2025-11-07 23:48:10 +08:00
logger.SaveProperties()