新增插件指引网页

This commit is contained in:
2025-11-12 22:58:36 +08:00
parent 4a3beb2153
commit 7332141a92
34 changed files with 2373 additions and 8984 deletions

View File

@@ -5,6 +5,9 @@ from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ProjectConfig
from PWF.Convention.Runtime.Web import ToolURL
from PWF.Convention.Runtime.String import LimitStringLength
from fastapi.responses import HTMLResponse
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, List, Optional, Sequence, TypedDict, override, Union
import httpx
import re
@@ -13,6 +16,613 @@ MAIN_WEBHOOK_URL = logger.FindItem("main_webhook_url", "")
logger.SaveProperties()
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>
"""
class MessageSender:
"""消息发送器"""
@@ -190,6 +800,198 @@ class BasicWPSInterface(PluginInterface):
class WPSAPI(BasicWPSInterface):
"""核心 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": "📝",
},
)
@override
def is_enable_plugin(self) -> bool:
if MAIN_WEBHOOK_URL == "":

View File

@@ -13,7 +13,7 @@ from PWF.CoreModules.database import get_db, STATUS_COMPLETED
from PWF.CoreModules.plugin_interface import DatabaseModel
from PWF.CoreModules.flags import get_internal_debug
from .WPSAPI import WPSAPI
from .WPSAPI import GuideEntry, GuideSection, WPSAPI
from .WPSBackpackSystem import (
BackpackItemDefinition,
BackpackItemTier,
@@ -73,6 +73,210 @@ class WPSAlchemyGame(WPSAPI):
self._max_points_per_batch = MAX_POINTS_PER_BATCH
logger.SaveProperties()
def get_guide_subtitle(self) -> str:
return "积分炼金与材料合成系统"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"配方数量": str(len(self._recipes)),
"冷却时间(分钟)": str(self._cooldown_minutes),
"单次积分上限": str(self._max_points_per_batch),
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "炼金",
"identifier": "炼金",
"description": "投入积分或三件材料,等待冷却后获取结果。",
"metadata": {"别名": "alchemy"},
},
)
def collect_item_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": self.ASH_ITEM_NAME,
"identifier": self.ASH_ITEM_ID,
"description": "炼金失败后获得的基础材料,可再次参与配方或出售。",
},
{
"title": self.SLAG_ITEM_NAME,
"identifier": self.SLAG_ITEM_ID,
"description": "由 `炉灰` 合成,部分园艺/商店配方会引用该物品。",
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "积分炼金",
"description": (
f"`炼金 <积分>` 消耗积分尝试炼金,单次上限 {self._max_points_per_batch}"
"结果与运势、阶段概率表 `_PHASE_TABLE` 相关。"
),
"icon": "💎",
},
{
"title": "材料炼金",
"description": (
"`炼金 <材料1> <材料2> <材料3> [次数]` 支持批量执行,配方信息可通过 `炼金配方` 查询。"
),
"icon": "🧪",
},
{
"title": "冷却与恢复",
"description": (
f"默认冷却 {self._cooldown_minutes} 分钟,任务调度用于结算完成;"
"重启后会自动恢复未结算记录。"
),
"icon": "⏲️",
},
)
def collect_additional_sections(self) -> Sequence[GuideSection]:
sections = list(super().collect_additional_sections())
sections.append(
GuideSection(
title="基础配方",
entries=self._build_core_recipes(),
layout="grid",
section_id="alchemy-core",
description="系统内置的基础配方,可在没有额外模块时直接使用。",
)
)
garden_recipes = self._build_garden_wine_recipes()
if garden_recipes:
sections.append(
GuideSection(
title="果酒配方",
entries=garden_recipes,
layout="grid",
section_id="alchemy-garden",
description="菜园系统提供的果酒炼金配方,使用三份果实即可酿造果酒。",
)
)
crystal_recipes = self._build_crystal_chain_recipes()
if crystal_recipes:
sections.append(
GuideSection(
title="水晶链路",
entries=crystal_recipes,
layout="list",
section_id="alchemy-crystal",
description="水晶系统的多阶段炼金链路,最终可获得黑水晶核心。",
)
)
return tuple(sections)
def _build_core_recipes(self) -> Sequence[GuideEntry]:
entries: List[GuideEntry] = []
entries.append(
GuideEntry(
title="炉灰 → 炉渣",
identifier=f"{self.ASH_ITEM_ID} × 3",
description="循环利用基础产物,将多余炉灰转化为更稀有的炉渣。",
category="基础配方",
metadata={
"成功率": "100%",
"失败产物": self.ASH_ITEM_ID,
},
icon="🔥",
details=[
{
"type": "list",
"items": [
f"材料:{self.ASH_ITEM_ID} × 3",
f"成功产物:{self.SLAG_ITEM_ID}",
f"失败产物:{self.ASH_ITEM_ID}",
],
}
],
)
)
return entries
def _build_garden_wine_recipes(self) -> Sequence[GuideEntry]:
try:
from Plugins.WPSGardenSystem.garden_models import GARDEN_CROPS
except ImportError:
return ()
entries: List[GuideEntry] = []
for crop in GARDEN_CROPS.values():
if not crop.wine_item_id:
continue
items = [
f"材料:{crop.fruit_id} × 3",
f"成功产物:{crop.wine_item_id}",
"失败产物garden_item_rot_fruit",
"基础成功率75%",
]
entries.append(
GuideEntry(
title=f"{crop.display_name}果酒",
identifier=f"{crop.fruit_id} ×3",
description=f"使用 {crop.display_name} 的果实炼制同名果酒。",
category="果酒配方",
metadata={
"果酒稀有度": crop.wine_tier or "rare",
},
icon="🍷",
tags=(crop.tier.title(),),
details=[{"type": "list", "items": items}],
)
)
return entries
def _build_crystal_chain_recipes(self) -> Sequence[GuideEntry]:
try:
from Plugins.WPSCrystalSystem.crystal_models import (
DEFAULT_CRYSTAL_COLOR_MAP,
DEFAULT_CRYSTAL_EXCHANGE_ENTRIES,
)
except ImportError:
return ()
entries: List[GuideEntry] = []
for color_def in DEFAULT_CRYSTAL_COLOR_MAP.values():
stage_details: List[str] = []
for stage in color_def.chain_stages:
mats = " + ".join(stage.materials)
stage_details.append(
f"{stage.identifier}{mats}{stage.result_item}(成功率 {stage.base_success_rate*100:.0f}%"
)
stage_details.append(
f"等待阶段:消耗 {', '.join(f'{k}×{v}' for k, v in color_def.wait_stage.consumed_items.items())}"
f"耗时 {color_def.wait_stage.delay_minutes} 分钟"
)
fusion = color_def.final_fusion
stage_details.append(
f"最终融合:{', '.join(fusion.materials)}{fusion.result_item}(成功率 {fusion.base_success_rate*100:.0f}%"
)
entries.append(
GuideEntry(
title=color_def.display_name,
identifier=color_def.color_key,
description="水晶染色与融合的完整链路。",
category="水晶链路",
icon="💠",
details=[{"type": "list", "items": stage_details}],
)
)
for exchange in DEFAULT_CRYSTAL_EXCHANGE_ENTRIES.values():
items = ", ".join(f"{item_id}×{qty}" for item_id, qty in exchange.required_items.items())
entries.append(
GuideEntry(
title=exchange.metadata.get("display_name", exchange.identifier),
identifier=exchange.identifier,
description=f"兑换奖励:{exchange.reward_item}",
category="水晶兑换",
icon="🔁",
details=[f"需求:{items}"],
)
)
return entries
@override
def dependencies(self) -> List[type]:
return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem, WPSStoreSystem]
@@ -858,6 +1062,31 @@ class WPSAlchemyRecipeLookup(WPSAPI):
self._alchemy: Optional[WPSAlchemyGame] = None
self._backpack: Optional[WPSBackpackSystem] = None
def get_guide_subtitle(self) -> str:
return "查询指定物品涉及的炼金配方"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "炼金配方",
"identifier": "炼金配方",
"description": "展示物品作为材料、成功产物或失败产物的所有配方。",
"metadata": {"别名": "alchemy_recipe"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "查询格式",
"description": "`炼金配方 <物品ID>` 或 `炼金配方 <物品名称>`,忽略大小写。",
},
{
"title": "输出结构",
"description": "结果按材料/成功/失败三类分组,列出配方材料与成功率。",
},
)
def dependencies(self) -> List[type]:
return [WPSAlchemyGame, WPSBackpackSystem]

View File

@@ -2,14 +2,14 @@ from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Dict, List, Optional, override
from typing import Dict, List, Optional, Sequence, override
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.database import get_db
from PWF.CoreModules.flags import get_internal_debug
from .WPSAPI import WPSAPI
from .WPSAPI import GuideEntry, GuideSection, WPSAPI
logger: ProjectConfig = Architecture.Get(ProjectConfig)
@@ -58,6 +58,48 @@ class WPSBackpackSystem(WPSAPI):
ITEMS_TABLE = "backpack_items"
USER_ITEMS_TABLE = "backpack_user_items"
def get_guide_subtitle(self) -> str:
return "管理物品注册、背包存储与查询的核心系统"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"物品缓存数": str(len(self._item_cache)),
"数据表": f"{self.ITEMS_TABLE}, {self.USER_ITEMS_TABLE}",
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "背包",
"identifier": "背包",
"description": "以稀有度分组展示用户当前携带物品。",
"metadata": {"别名": "backpack"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
tier_labels = ", ".join(tier.display_name for tier in BackpackItemTier)
return (
{
"title": "物品注册",
"description": (
"`register_item(item_id, name, tier, description)` "
"将物品写入背包表,重复调用会更新名称、稀有度和描述。"
),
},
{
"title": "稀有度体系",
"description": f"支持稀有度:{tier_labels},调用 `to_markdown_label` 可渲染彩色标签。",
},
{
"title": "库存操作",
"description": (
"`add_item` / `set_item_quantity` / `_get_user_quantity` "
"确保用户物品数量保持非负,并自动创建记录。"
),
},
)
def __init__(self) -> None:
super().__init__()
self._item_cache: Dict[str, BackpackItemDefinition] = {}
@@ -124,6 +166,50 @@ class WPSBackpackSystem(WPSAPI):
description=description,
)
def _iter_registered_items(self) -> Sequence[BackpackItemDefinition]:
try:
if not self._item_cache:
self._warm_item_cache()
except Exception:
return ()
return tuple(self._item_cache.values())
def collect_additional_sections(self) -> Sequence[GuideSection]:
sections = list(super().collect_additional_sections())
item_entries: List[GuideEntry] = []
tier_icons = {
BackpackItemTier.COMMON: "🪙",
BackpackItemTier.RARE: "💠",
BackpackItemTier.EPIC: "",
BackpackItemTier.LEGENDARY: "🌟",
}
for definition in self._iter_registered_items():
item_entries.append(
GuideEntry(
title=definition.name,
identifier=definition.item_id,
description=definition.description or "(暂无描述)",
category="背包物品",
metadata={
"稀有度": definition.tier.display_name,
},
icon=tier_icons.get(definition.tier, "🎁"),
tags=(definition.tier.display_name,),
group=definition.tier.display_name,
)
)
if item_entries:
sections.append(
GuideSection(
title="物品图鉴",
entries=item_entries,
layout="grid",
section_id="backpack-items",
description="当前已注册的背包物品列表,按稀有度分组展示。",
)
)
return tuple(sections)
# region 对外接口
def register_item(

View File

@@ -3,11 +3,13 @@
from __future__ import annotations
from datetime import datetime
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry, GuideSection
from .combat_plugin_base import WPSCombatBase
from .combat_models import CombatConfig
logger: ProjectConfig = ProjectConfig()
@@ -16,6 +18,133 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatAdventure(WPSCombatBase):
"""冒险系统插件"""
def get_guide_subtitle(self) -> str:
return "阶段式 PVE 冒险,产出装备与素材"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="冒险 开始",
identifier="冒险 开始 [食物...]",
description="启动第 1 阶段冒险,可额外投入食物缩短时间并提升成功率。",
metadata={"别名": "start"},
icon="🚀",
tags=("阶段1", "需未受伤"),
details=[
{
"type": "steps",
"items": [
"检查自身状态,确认未处于受伤或其他冒险中。",
"可选:准备食物 `food_id` 列表;每个食物将被立即消耗。",
"系统计算预计耗时、成功率并生成冒险记录。",
"创建时钟任务,时间到后自动结算。",
],
},
"结算时会根据装备强度与运势发放奖励。",
],
),
GuideEntry(
title="继续冒险",
identifier="冒险 / 继续冒险",
description="在已有链路下进入下一阶段或查看剩余时间。",
metadata={"别名": "adventure / continue"},
icon="⏱️",
tags=("阶段推进",),
details=[
{
"type": "steps",
"items": [
"查询当前冒险记录,若已在倒计时阶段则返回剩余时间。",
"如冒险链完成并准备进入下一阶段,可再次投喂食物并启动。",
"系统保持阶段编号,累计奖励与时间。",
],
}
],
),
GuideEntry(
title="冒险 停止",
identifier="冒险 停止 / 放弃",
description="放弃当前冒险链,结算状态并清空倒计时。",
metadata={"别名": "放弃 / 停止"},
icon="🛑",
tags=("风险",),
details=[
"放弃后当前阶段奖励作废,未来可从第 1 阶段重开。",
"若系统已开始结算,则命令会提示等待完成。",
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="冒险阶段",
description="Adventure 链路包含多个阶段,难度逐渐提升,奖励也随之增加。",
icon="⚙️",
details=[
{
"type": "list",
"items": [
"阶段时间:基础 15 分钟,最高不超过 24 小时。",
"投喂食物:每份食物提供额外时间与成功率加成。",
"装备影响:装备强度越高,成功率越高且时间越短。",
"运势影响:由运势系统提供当前整点的幸运值,用于修正成功率。",
],
}
],
),
GuideEntry(
title="奖励构成",
description="冒险完成后会发放积分、装备、材料、纪念品等。",
icon="🎁",
details=[
{
"type": "list",
"items": [
"基础积分奖励随阶段提升。",
"装备掉落根据稀有度加权抽取。",
"部分阶段触发事件可获得药剂、种子或纪念品。",
],
}
],
),
)
def collect_additional_sections(self) -> Sequence[GuideSection]:
sections = list(super().collect_additional_sections())
adventure_config_entries: List[GuideEntry] = []
config_labels = {
"combat_adventure_base_time": "阶段起始时间 (分钟)",
"combat_adventure_max_time": "时间上限 (分钟)",
"combat_food_support_time": "单个食物提供时间 (分钟)",
"combat_adventure_base_success_rate": "基础成功率",
"combat_adventure_stage_penalty": "阶段成功率衰减",
"combat_adventure_equipment_coeff": "装备强度加成系数",
"combat_adventure_fortune_coeff": "运势加成系数",
"combat_time_reduction_divisor": "时间缩减除数",
}
for key, label in config_labels.items():
value = CombatConfig.get(key)
adventure_config_entries.append(
GuideEntry(
title=label,
identifier=key,
description=f"{value}",
category="配置项",
icon="📐",
)
)
sections.append(
GuideSection(
title="冒险参数一览",
entries=adventure_config_entries,
layout="grid",
section_id="adventure-config",
description="核心公式与系数决定冒险耗时、成功率与奖励结构。",
)
)
return tuple(sections)
def is_enable_plugin(self) -> bool:
return True

View File

@@ -2,13 +2,13 @@
from __future__ import annotations
from typing import List, Type
from typing import Dict, List, Type, Union
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.plugin_interface import DatabaseModel
from Plugins.WPSAPI import WPSAPI
from Plugins.WPSAPI import GuideEntry, GuideSection, WPSAPI
from Plugins.WPSBackpackSystem import BackpackItemTier, WPSBackpackSystem
from Plugins.WPSStoreSystem import WPSStoreSystem
from Plugins.WPSConfigSystem import WPSConfigAPI
@@ -46,6 +46,270 @@ class WPSCombatBase(WPSAPI):
_service: CombatService | None = None
_initialized: bool = False
def collect_additional_sections(self) -> Sequence[GuideSection]:
sections = list(super().collect_additional_sections())
equipment_entries = self._build_equipment_entries()
if equipment_entries:
sections.append(
GuideSection(
title="装备详单",
entries=equipment_entries,
layout="grid",
section_id="combat-equipment",
description="战斗系统预置的装备清单及属性效果。",
)
)
potion_entries = self._build_simple_item_entries(
COMBAT_POTIONS,
category="战斗药剂",
)
if potion_entries:
sections.append(
GuideSection(
title="药剂与增益",
entries=potion_entries,
layout="grid",
section_id="combat-potions",
description="药剂品质影响价格与效果,部分由冒险掉落。",
)
)
material_entries = self._build_simple_item_entries(
ADVENTURE_MATERIALS,
category="冒险材料",
)
if material_entries:
sections.append(
GuideSection(
title="冒险材料",
entries=material_entries,
layout="grid",
section_id="combat-materials",
description="材料主要来源于冒险战斗,稀有度决定获取概率。",
)
)
souvenir_entries = self._build_souvenir_entries()
if souvenir_entries:
sections.append(
GuideSection(
title="纪念品一览",
entries=souvenir_entries,
layout="grid",
section_id="combat-souvenirs",
description="纪念品可在营地或商店出售兑换积分。",
)
)
seed_entries = self._build_simple_item_entries(
ADVENTURE_SEEDS,
category="冒险种子",
)
if seed_entries:
sections.append(
GuideSection(
title="冒险种子",
entries=seed_entries,
layout="grid",
section_id="combat-seeds",
description="冒险专属种子,可在菜园种植获取增益或任务物资。",
)
)
skill_entries = self._build_skill_entries()
if skill_entries:
sections.append(
GuideSection(
title="技能图鉴",
entries=skill_entries,
layout="list",
section_id="combat-skills",
description="装备附带的技能可在战斗中释放,冷却时间以回合计算。",
)
)
return tuple(sections)
def _format_tier(self, tier: BackpackItemTier) -> str:
return f"{tier.display_name}"
def _build_equipment_entries(self) -> List[GuideEntry]:
entries: List[GuideEntry] = []
attr_names = {
"HP": "生命值",
"ATK": "攻击力",
"DEF": "防御力",
"SPD": "速度",
"CRIT": "暴击率",
"CRIT_DMG": "暴击伤害",
}
tier_icons = {
BackpackItemTier.COMMON: "⚔️",
BackpackItemTier.RARE: "🛡️",
BackpackItemTier.EPIC: "🔥",
BackpackItemTier.LEGENDARY: "🌟",
}
for eq in EQUIPMENT_REGISTRY.values():
attr_list = []
for key, value in eq.attributes.items():
label = attr_names.get(key, key)
suffix = "%" if key in ("CRIT", "CRIT_DMG") else ""
attr_list.append(f"{label}+{value}{suffix}")
skills = []
for skill_id in eq.skill_ids:
skill = SKILL_REGISTRY.get(skill_id)
if skill:
skills.append(f"{skill.icon} {skill.name}")
metadata = {
"槽位": eq.slot,
"稀有度": self._format_tier(eq.tier),
}
details: List[Dict[str, Any] | str] = []
if attr_list:
details.append({"type": "list", "items": attr_list})
if skills:
details.append({"type": "list", "items": [f"技能:{info}" for info in skills]})
if eq.description:
details.append(eq.description)
entries.append(
GuideEntry(
title=eq.name,
identifier=eq.item_id,
description=eq.description or "标准装备。",
category="装备",
metadata=metadata,
icon=tier_icons.get(eq.tier, "⚔️"),
tags=skills,
details=details,
)
)
return entries
def _build_simple_item_entries(
self,
registry: Dict[str, tuple],
*,
category: str,
) -> List[GuideEntry]:
entries: List[GuideEntry] = []
icon_map = {
"战斗药剂": "🧪",
"冒险材料": "🪨",
"纪念品": "🎖️",
"冒险种子": "🌱",
}
for item_id, payload in registry.items():
name, tier, *rest = payload
description = rest[-1] if rest else ""
metadata = {"稀有度": self._format_tier(tier)}
if category == "战斗药剂":
price_lookup = {
BackpackItemTier.COMMON: 50,
BackpackItemTier.RARE: 150,
BackpackItemTier.EPIC: 500,
}
price = price_lookup.get(tier)
if price:
metadata["默认售价"] = f"{price}"
entries.append(
GuideEntry(
title=name,
identifier=item_id,
description=description,
category=category,
metadata=metadata,
icon=icon_map.get(category, "📦"),
)
)
return entries
def _build_souvenir_entries(self) -> List[GuideEntry]:
entries: List[GuideEntry] = []
for item_id, (name, tier, price, desc) in ADVENTURE_SOUVENIRS.items():
entries.append(
GuideEntry(
title=name,
identifier=item_id,
description=desc,
category="纪念品",
metadata={
"稀有度": self._format_tier(tier),
"基础售价": f"{price}",
},
icon="🎖️",
)
)
return entries
def _build_skill_entries(self) -> List[GuideEntry]:
entries: List[GuideEntry] = []
for skill in SKILL_REGISTRY.values():
details: List[Union[str, Dict[str, Any]]] = [
{"type": "list", "items": [effect.get("description", str(effect)) for effect in skill.effects]},
]
if skill.cooldown:
details.append(f"冷却:{skill.cooldown} 回合")
entries.append(
GuideEntry(
title=skill.name,
identifier=skill.skill_id,
description=skill.description,
category="技能",
icon=skill.icon,
metadata={},
details=details,
)
)
return entries
def get_guide_subtitle(self) -> str:
return "冒险、战斗与装备体系的基础能力"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"装备数量": str(len(EQUIPMENT_REGISTRY)),
"药剂数量": str(len(COMBAT_POTIONS)),
"纪念品数量": str(len(ADVENTURE_SOUVENIRS)),
"冒险材料": str(len(ADVENTURE_MATERIALS)),
}
def collect_item_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "装备库",
"description": f"{len(EQUIPMENT_REGISTRY)} 件装备,自动注册至背包并带有属性描述。",
},
{
"title": "药剂与材料",
"description": (
f"{len(COMBAT_POTIONS)} 种药剂、{len(ADVENTURE_MATERIALS)} 种冒险素材,"
"部分可在商店购买或冒险获得。"
),
},
{
"title": "纪念品",
"description": f"{len(ADVENTURE_SOUVENIRS)} 种纪念品可在营地出售换取积分。",
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "冒险流程",
"description": "冒险按阶段推进,使用食物缩短时间并依赖运势与装备强度影响结果。",
},
{
"title": "装备体系",
"description": "装备提供属性与技能加成,通过 `装备`/`战斗属性` 等指令查看详情。",
},
{
"title": "积分与资源",
"description": "冒险和战斗会产出积分、装备、材料等,通过商店与营地完成循环。",
},
)
@classmethod
def service(cls) -> CombatService:

View File

@@ -2,10 +2,11 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from .combat_plugin_base import WPSCombatBase
@@ -15,6 +16,69 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatBattle(WPSCombatBase):
"""PVP对战插件"""
def get_guide_subtitle(self) -> str:
return "玩家间回合制对战指令集"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="挑战",
identifier="挑战 <目标用户ID>",
description="向指定玩家发起 PVP 挑战。",
metadata={"别名": "challenge"},
icon="⚔️",
details=[
{"type": "list", "items": ["不可挑战自己。", "挑战在超时前需对方接受,否则自动失效。"]},
],
),
GuideEntry(
title="接受挑战",
identifier="接受挑战 <挑战ID>",
description="接受待处理的挑战并初始化战斗。",
metadata={"别名": "accept"},
icon="",
details=[
{"type": "steps", "items": ["输入挑战列表中的 ID。", "系统创建战斗记录并通知双方。"]},
],
),
GuideEntry(
title="拒绝挑战",
identifier="拒绝挑战 <挑战ID>",
description="拒绝尚未开始的挑战请求。",
metadata={"别名": "reject"},
icon="🚫",
),
GuideEntry(
title="战斗动作",
identifier="战斗 <战斗ID> <技能名>",
description="在战斗中释放技能或执行普攻。",
metadata={"别名": "battle"},
icon="🌀",
details=[
{"type": "list", "items": ["技能冷却与效果可在 `技能列表` 中查看。", "战斗为回合制,按顺序执行。"]},
],
),
GuideEntry(
title="投降",
identifier="投降 <战斗ID>",
description="主动认输并结束当前战斗。",
metadata={"别名": "surrender"},
icon="🏳️",
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "挑战生命周期",
"description": "挑战需在配置的超时前被接受,超时将自动失效。",
},
{
"title": "战斗指令",
"description": "战斗中按回合输入技能,系统根据属性与技能效果计算伤害。",
},
)
def is_enable_plugin(self) -> bool:
return True

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSConfigSystem import WPSConfigAPI
@@ -27,6 +28,35 @@ class WPSCombatCamp(WPSCombatBase):
meta[0].lower(): item_id for item_id, meta in self._souvenir_by_id.items()
}
def get_guide_subtitle(self) -> str:
return "营地寄售区:纪念品快速变现"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="营地 售出",
identifier="营地 售出 <纪念品> <数量>",
description="将纪念品出售给系统换取积分。",
metadata={"别名": "camp"},
icon="🏕️",
details=[
{"type": "steps", "items": ["输入纪念品名称或 ID。", "校验背包数量并扣除。", "根据基础售价发放积分。"]},
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "销售指令",
"description": "`营地 售出 <纪念品> <数量>`名称可用中文或物品ID。",
},
{
"title": "积分结算",
"description": "根据纪念品基础售价乘以数量发放积分,并扣除背包库存。",
},
)
def is_enable_plugin(self) -> bool:
return True

View File

@@ -2,10 +2,11 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from .combat_plugin_base import WPSCombatBase
@@ -15,6 +16,42 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatEquipment(WPSCombatBase):
"""装备管理插件"""
def get_guide_subtitle(self) -> str:
return "穿戴与卸下战斗装备"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="装备",
identifier="装备 [物品名|ID]",
description="穿戴指定装备,或不带参数时查看当前装备。",
metadata={"别名": "equip"},
icon="🛡️",
details=[
{"type": "list", "items": ["名称与 ID 均可匹配装备库条目。", "若未携带该装备会提示不足。"]},
],
),
GuideEntry(
title="卸下",
identifier="卸下 <槽位>",
description="卸下指定槽位weapon/helmet/armor/boots/accessory的装备。",
metadata={"别名": "unequip"},
icon="🧤",
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "槽位说明",
"description": "支持 weapon/helmet/armor/boots/accessory 五个槽位。",
},
{
"title": "属性加成",
"description": "装备属性直接影响战斗与冒险计算,可在 `战斗属性` 指令中查看综合加成。",
},
)
def is_enable_plugin(self) -> bool:
return True

View File

@@ -2,9 +2,11 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSCombatSystem.combat_models import CombatConfig
from .combat_plugin_base import WPSCombatBase
@@ -15,6 +17,32 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatHeal(WPSCombatBase):
"""治疗系统插件"""
def get_guide_subtitle(self) -> str:
return "恢复受伤状态并重返冒险"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="治疗",
identifier="治疗",
description="消耗积分清除受伤状态,使角色可继续冒险或战斗。",
metadata={"别名": "heal / 恢复"},
icon="💊",
details=[
{"type": "list", "items": ["若未受伤会返回提示信息。", "扣除积分后立即清除受伤标记。"]},
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
heal_cost = CombatConfig.get_int("combat_heal_cost")
return (
{
"title": "费用计算",
"description": f"治疗费用由配置 `combat_heal_cost` 决定,当前为 {heal_cost} 分。",
},
)
def is_enable_plugin(self) -> bool:
return True

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.Config import *
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSCombatSystem.combat_models import PlayerStats, EquipmentDefinition
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSCombatSystem.combat_models import EquipmentDefinition, PlayerStats
from .combat_plugin_base import WPSCombatBase
@@ -16,6 +17,44 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatStatus(WPSCombatBase):
"""状态查看插件"""
def get_guide_subtitle(self) -> str:
return "查看战斗属性、装备栏与技能列表"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="战斗属性",
identifier="战斗属性",
description="展示基础属性、装备强度与技能概览。",
metadata={"别名": "combat"},
icon="📊",
),
GuideEntry(
title="装备栏",
identifier="装备栏",
description="仅查看当前装备与各槽位详情。",
icon="🎽",
),
GuideEntry(
title="技能列表",
identifier="技能列表",
description="罗列当前可用技能、描述与冷却时间。",
icon="📜",
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "属性来源",
"description": "数值由基础配置、装备加成与技能被动共同组成。",
},
{
"title": "技能冷却",
"description": "输出中会标注技能冷却回合,用于 PVP 战斗操作参考。",
},
)
def is_enable_plugin(self) -> bool:
return True

View File

@@ -14,6 +14,41 @@ CHECKIN_POINTS = logger.FindItem("checkin_points", 100)
logger.SaveProperties()
class WPSConfigAPI(WPSAPI):
def get_guide_subtitle(self) -> str:
return "用户基础资料与积分配置接口"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"数据表": "user_info",
"每日签到积分": str(CHECKIN_POINTS),
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "config",
"identifier": "config",
"description": "配置与查询用户昵称、URL、积分等信息。",
"metadata": {"别名": "cfg"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "设置指令",
"description": "`config set user.name <昵称>` / `config set user.url <URL>`",
},
{
"title": "查询指令",
"description": "`config get user.name|user.url|user.point` 返回当前资料或积分。",
},
{
"title": "数据校验",
"description": "内部自动确保用户记录存在,并限制昵称长度与 URL 前缀。",
},
)
@override
def dependencies(self) -> List[Type]:
return [WPSAPI]
@@ -196,6 +231,37 @@ class WPSConfigAPI(WPSAPI):
class WPSCheckinAPI(WPSAPI):
def get_guide_subtitle(self) -> str:
return "每日签到并发放积分的快捷指令"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"数据表": "daily_checkin",
"签到积分": str(CHECKIN_POINTS),
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "checkin",
"identifier": "checkin",
"description": "执行签到流程,发放积分并反馈今日进度。",
"metadata": {"别名": "签到 / 积分"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "签到逻辑",
"description": "同一日多次调用仅第一次成功,并记录在 `daily_checkin` 表内。",
},
{
"title": "积分结算",
"description": "成功签到将通过 `WPSConfigAPI.adjust_user_points` 增加积分。",
},
)
@override
def dependencies(self) -> List[Type]:
return [WPSAPI]

View File

@@ -53,6 +53,54 @@ class WPSCrystalSystem(WPSAPI):
key.lower(): value for key, value in DEFAULT_CRYSTAL_EXCHANGE_ENTRIES.items()
}
def get_guide_subtitle(self) -> str:
return "水晶养成、颜色淬炼与兑换拓展系统"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"颜色数量": str(len(self._colors)),
"水晶物品": str(len(self._items)),
"兑换项目": str(len(self._exchange_entries)),
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "水晶",
"identifier": "水晶",
"description": "查看系统配置或执行变色与兑换等操作。",
"metadata": {"别名": "crystal"},
},
)
def collect_item_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "基础水晶物品",
"description": f"{len(self._items)} 个水晶组件,可由背包/商店系统持有与交易。",
},
{
"title": "颜色链路",
"description": f"{len(self._colors)} 条变色链(包含等待阶段与最终融合)。",
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "变色流程",
"description": "`水晶 变色 <颜色>` 进入等待流程,完成后获得对应水晶部件。",
},
{
"title": "兑换指令",
"description": "`水晶 兑换 <ID>` 消耗配置好的材料换取奖励物品。",
},
{
"title": "菜园扩展",
"description": "系统会向菜园注册水晶树作物,使果实与水晶体系互相联动。",
},
)
# ------------------------------------------------------------------ #
# Plugin lifecycle
# ------------------------------------------------------------------ #

View File

@@ -32,6 +32,35 @@ _FORTUNE_STAGE_TABLE: List[Tuple[float, str]] = [
class WPSFortuneSystem(WPSAPI):
"""基于整点哈希的运势系统,可供其他模块复用"""
def get_guide_subtitle(self) -> str:
return "提供整点运势值及阶段信息的公共组件"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "fortune",
"identifier": "fortune",
"description": "查询当前整点的运势值与阶段文本。",
"metadata": {"别名": "运势"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "运势算法",
"description": "基于用户ID与整点时间的 SHA-256 哈希映射为 [-0.9999, 0.9999] 区间。",
},
{
"title": "阶段划分",
"description": "通过 `_FORTUNE_STAGE_TABLE` 匹配阶段标签,供冒险等系统引用。",
},
{
"title": "复用接口",
"description": "`get_fortune_value / get_fortune_info` 可被其他插件同步或异步调用。",
},
)
@override
def dependencies(self) -> List[Type]:
return [WPSAPI]

View File

@@ -3,13 +3,13 @@
from __future__ import annotations
import json
from typing import List, Optional, Type
from typing import Any, Dict, List, Optional, Sequence, Type, Union
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.plugin_interface import DatabaseModel
from Plugins.WPSAPI import WPSAPI
from Plugins.WPSAPI import GuideEntry, GuideSection, WPSAPI
from Plugins.WPSBackpackSystem import (
BackpackItemTier,
WPSBackpackSystem,
@@ -33,6 +33,166 @@ class WPSGardenBase(WPSAPI):
_service: GardenService | None = None
_initialized: bool = False
def get_guide_subtitle(self) -> str:
return "菜园作物种植与联动系统的核心服务"
def get_guide_metadata(self) -> Dict[str, str]:
service = self.service()
config = service.config
return {
"作物数量": str(len(GARDEN_CROPS)),
"最大地块": str(config.max_plots),
"出售倍率": str(config.sale_multiplier),
}
def collect_item_entries(self) -> Sequence[GuideEntry]:
tier_counter: Dict[str, int] = {}
wine_counter: int = 0
for crop in GARDEN_CROPS.values():
tier_counter[crop.tier] = tier_counter.get(crop.tier, 0) + 1
if crop.wine_item_id:
wine_counter += 1
entries: List[GuideEntry] = []
for tier, count in sorted(tier_counter.items()):
entries.append(
{
"title": f"{tier.title()} 作物",
"description": f"{count} 种作物,可收获果实与额外奖励。",
}
)
entries.append(
{
"title": "果酒配方",
"description": f"{wine_counter} 种作物支持果酒配方,并与战斗系统的果酒增益联动。",
}
)
if GARDEN_MISC_ITEMS:
entries.append(
{
"title": "杂项素材",
"description": f"{len(GARDEN_MISC_ITEMS)} 种额外素材,可用于任务或商店出售。",
}
)
return tuple(entries)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
service = self.service()
return (
{
"title": "成长流程",
"description": (
"种植后根据作物 `growth_minutes` 决定成熟时间,系统会在成熟时通过时钟任务提醒。"
),
},
{
"title": "收获收益",
"description": (
"收获基础产量由 `base_yield` 决定,额外奖励受运势与 `extra_reward` 配置影响。"
),
},
{
"title": "果实售出",
"description": (
f"通过 `菜园 售出` 指令以 {service.config.sale_multiplier} 倍种子价格出售果实并获取积分。"
),
},
)
def collect_additional_sections(self) -> Sequence[GuideSection]:
sections = list(super().collect_additional_sections())
crop_entries: List[GuideEntry] = []
tier_icon = {
"common": "🌱",
"rare": "🌳",
"epic": "🍷",
"legendary": "🏵️",
}
for crop in GARDEN_CROPS.values():
reward_desc = ""
if crop.extra_reward.kind == "points":
payload = crop.extra_reward.payload
reward_desc = (
f"额外积分 {payload.get('min', 0)}~{payload.get('max', 0)}"
f"触发率 {crop.extra_reward.base_rate*100:.0f}%"
)
elif crop.extra_reward.kind == "item":
payload = crop.extra_reward.payload
reward_desc = (
f"额外物品 `{crop.extra_item_id}` 数量 {payload.get('min', 0)}~{payload.get('max', 0)}"
f"触发率 {crop.extra_reward.base_rate*100:.0f}%"
)
details: List[Union[str, Dict[str, Any]]] = [
{
"type": "list",
"items": [
f"生长时间:{crop.growth_minutes} 分钟",
f"基础产量:{crop.base_yield} 个果实",
reward_desc or "无额外奖励",
f"种子售价:{crop.seed_price}",
],
}
]
if crop.wine_item_id:
details.append(
{
"type": "list",
"items": [
f"果酒:{crop.wine_item_id}(稀有度 {crop.wine_tier or 'rare'}",
"炼金配方:三份果实 + 炼金坩埚 → 果酒。",
],
}
)
crop_entries.append(
GuideEntry(
title=crop.display_name,
identifier=crop.seed_id,
description=f"果实 ID{crop.fruit_id}",
category="作物",
metadata={
"稀有度": crop.tier,
"果实ID": crop.fruit_id,
},
icon=tier_icon.get(crop.tier.lower(), "🌿"),
tags=(crop.tier.title(),),
details=details,
)
)
if crop_entries:
sections.append(
GuideSection(
title="作物图鉴",
entries=crop_entries,
layout="grid",
section_id="garden-crops",
description="每种作物的成长周期、产出与额外奖励说明。",
)
)
if GARDEN_MISC_ITEMS:
misc_entries: List[GuideEntry] = []
for item_id, meta in GARDEN_MISC_ITEMS.items():
misc_entries.append(
GuideEntry(
title=meta.get("name", item_id),
identifier=item_id,
description=meta.get("description", ""),
category="杂项素材",
icon="🧺",
)
)
sections.append(
GuideSection(
title="杂项素材",
entries=misc_entries,
layout="grid",
section_id="garden-misc",
description="园艺相关的任务或合成所需的特殊素材。",
)
)
return tuple(sections)
@classmethod
def service(cls) -> GardenService:
if cls._service is None:

View File

@@ -2,10 +2,11 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSConfigSystem import WPSConfigAPI
from Plugins.WPSFortuneSystem import WPSFortuneSystem
@@ -14,6 +15,42 @@ from .garden_plugin_base import WPSGardenBase
class WPSGardenHarvest(WPSGardenBase):
def get_guide_subtitle(self) -> str:
return "收获成熟作物并处理额外奖励"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="收获",
identifier="收获 <地块序号>",
description="从成熟地块采摘果实并发放额外奖励。",
metadata={"别名": "harvest"},
icon="🧺",
details=[
{
"type": "steps",
"items": [
"输入正整数地块序号。",
"系统校验成熟状态,计算基础果实数量。",
"发放额外奖励:积分或额外物品会自动结算。",
],
}
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "指令格式",
"description": "`收获 <地块序号>`,序号需为正整数。",
},
{
"title": "收益构成",
"description": "基础果实直接入背包,额外奖励可能为积分或额外物品。",
},
)
def wake_up(self) -> None:
super().wake_up()
self.register_plugin("harvest")

View File

@@ -2,16 +2,54 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem
from .garden_plugin_base import WPSGardenBase
class WPSGardenPlant(WPSGardenBase):
def get_guide_subtitle(self) -> str:
return "种植作物并分配地块"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="种植",
identifier="种植 <种子> [地块序号]",
description="在指定地块种下一颗种子,默认选取下一个空地。",
metadata={"别名": "plant"},
icon="🌱",
details=[
{
"type": "steps",
"items": [
"确认背包中持有对应种子。",
"可选:指定地块序号(正整数),否则使用下一个空地。",
"系统校验库存并写入菜园数据库,生成成熟计时。",
],
},
"种植成功后立即扣除种子数量并返回成熟时间。",
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "指令格式",
"description": "`种植 <种子ID|名称> [地块序号]`,默认选择下一个空地。",
},
{
"title": "库存校验",
"description": "会检查背包种子数量,不足时返回提示而不消耗资源。",
},
)
def wake_up(self) -> None:
super().wake_up()
self.register_plugin("plant")

View File

@@ -2,12 +2,44 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from Plugins.WPSAPI import GuideEntry
from .garden_plugin_base import WPSGardenBase
class WPSGardenRemove(WPSGardenBase):
def get_guide_subtitle(self) -> str:
return "清理地块以重新种植"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="铲除",
identifier="铲除 <地块序号>",
description="清空指定地块,移除作物与成熟计时。",
metadata={"别名": "remove"},
icon="🧹",
details=[
{
"type": "list",
"items": [
"常用于处理枯萎或不再需要的作物。",
"操作不可逆,被铲除的作物不会返还种子。",
],
}
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "使用场景",
"description": "用于清理枯萎或不再需要的作物,释放地块。",
},
)
def wake_up(self) -> None:
super().wake_up()
self.register_plugin("remove")

View File

@@ -2,17 +2,55 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.CoreModules.database import get_db
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem
from .garden_plugin_base import WPSGardenBase
class WPSGardenSteal(WPSGardenBase):
def get_guide_subtitle(self) -> str:
return "偷取其他用户成熟果实的互动指令"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="偷取",
identifier="偷取 <用户> <地块序号>",
description="从其他用户的成熟作物中偷取果实。",
metadata={"别名": "steal"},
icon="🕵️",
details=[
{
"type": "steps",
"items": [
"输入目标用户 ID 或昵称,以及成熟地块序号。",
"系统校验目标用户菜园与成熟状态。",
"成功后获得 1 个果实并通知对方被偷记录。",
],
},
"同一地块可多次被不同用户偷取,超限后将提示果实不足。",
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "指令格式",
"description": "`偷取 <用户ID|昵称> <地块序号>`,不可针对自己。",
},
{
"title": "通知机制",
"description": "成功偷取后会向目标用户推送警报消息,包含剩余果实数量。",
},
)
def wake_up(self) -> None:
super().wake_up()
self.register_plugin("steal")

View File

@@ -2,10 +2,11 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSConfigSystem import WPSConfigAPI
@@ -13,6 +14,58 @@ from .garden_plugin_base import WPSGardenBase
class WPSGardenView(WPSGardenBase):
def get_guide_subtitle(self) -> str:
return "查看菜园概览与果实售出"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
GuideEntry(
title="菜园 概览",
identifier="菜园",
description="显示当前所有地块状态与剩余果实。",
metadata={"别名": "garden"},
icon="🗺️",
details=[
{
"type": "list",
"items": [
"列出地块序号、作物名称、成熟时间与被偷情况。",
"空地块会提示剩余可用数量。",
],
}
],
),
GuideEntry(
title="菜园 售出",
identifier="菜园 售出 <果实> <数量>",
description="售出成熟果实并换取积分。",
metadata={"别名": "garden sell"},
icon="💰",
details=[
{
"type": "steps",
"items": [
"检查背包持有的果实数量。",
"输入欲出售的果实 ID/名称与数量。",
"系统按配置的售价乘以数量发放积分,同时扣除背包库存。",
],
}
],
),
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "概览视图",
"description": "默认输出地块编号、成熟状态、剩余果实与被偷记录。",
},
{
"title": "果实售出",
"description": "`菜园 售出 <果实> <数量>`,自动结算积分并扣除背包库存。",
},
)
def wake_up(self) -> None:
super().wake_up()
self.register_plugin("garden")

View File

@@ -83,6 +83,42 @@ class WPSStoreSystem(WPSAPI):
logger.SaveProperties()
self._permanent_mode_ids: set[str] = set()
def get_guide_subtitle(self) -> str:
return "系统商品与玩家寄售的统一商店"
def get_guide_metadata(self) -> Dict[str, str]:
return {
"已注册模式": str(len(self._mode_registry)),
"永久模式": str(len(self._permanent_mode_ids)),
"数据表": f"{self.SYSTEM_TABLE}, {self.PLAYER_TABLE}",
}
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "store",
"identifier": "store",
"description": "查看系统刷新的商品列表,包含系统和玩家寄售。",
"metadata": {"别名": "商店"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "模式注册",
"description": "`register_mode/register_permanent_mode` 将背包物品以指定库存与价格投放至系统表。",
},
{
"title": "刷新机制",
"description": "每小时根据 `store_hourly_count` 配置刷新系统库存,同时同步永久模式。",
},
{
"title": "玩家寄售",
"description": "`sell_item` 将玩家物品挂售至寄售表,`purchase_item` 支持购买系统或玩家商品。",
},
)
@override
def dependencies(self) -> List[type]:
return [WPSAPI, WPSConfigAPI, WPSBackpackSystem]
@@ -960,6 +996,31 @@ class WPSStoreSystem(WPSAPI):
class WPSStoreBuyCommand(WPSAPI):
def get_guide_subtitle(self) -> str:
return "购买商店及玩家寄售物品"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "buy",
"identifier": "buy",
"description": "购买系统或玩家寄售商品,数量需为正整数。",
"metadata": {"别名": "购买"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "参数格式",
"description": "`购买 <物品名称或ID> <数量>`,内部支持模糊匹配模式名称。",
},
{
"title": "权限校验",
"description": "调用 `purchase_item` 时校验库存、积分并自动扣除商品库存。",
},
)
@override
def dependencies(self) -> list[type]:
return [WPSStoreSystem]
@@ -1016,6 +1077,31 @@ class WPSStoreBuyCommand(WPSAPI):
class WPSStoreSellCommand(WPSAPI):
def get_guide_subtitle(self) -> str:
return "挂售物品至商店寄售区"
def collect_command_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "sell",
"identifier": "sell",
"description": "将背包物品以指定数量和单价挂售。",
"metadata": {"别名": "出售"},
},
)
def collect_guide_entries(self) -> Sequence[GuideEntry]:
return (
{
"title": "参数格式",
"description": "`出售 <物品名称或ID> <数量> <单价>`。",
},
{
"title": "寄售生命周期",
"description": "寄售记录写入玩家表,状态变更后定期清理无效记录。",
},
)
@override
def dependencies(self) -> list[type]:
return [WPSStoreSystem]