Release beta 2.0.0.1021
check / local-gate (push) Failing after 1m38s

This commit is contained in:
JWeBot Contributors
2026-10-21 17:05:09 +08:00
parent 5a3c4fb94c
commit e4365841ed
79 changed files with 1572 additions and 1840 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## beta 2.0.0.1021 - 2026-10-21 16:57:39
- Beta 2.0 架构更新完成:插件 SDK 统一服务适配器上线,本地命令插件联动改为受控服务调用,私聊 shadow 测试保持只观察不发送,安全边界和时延打点一起收口。
- 新增公开 SDK `ServiceCommandAdapter`,生成插件不再重复手写 service-broker
转发逻辑;插件仍只能返回结构化 `ReplyPlan` 和受控 metadata。
- 同步更新官方本地命令适配器生成器,所有 reviewed command adapter 统一走
`ServiceCommandAdapter`,避免插件联动接口漂移。
- 私聊 shadow 测试继续 fail-closed:只接受显式配置的会话标题哈希,生成
`shadow_only=true`、`dispatch_allowed=false` 观察事件,Edge 在路由和账本写入前拒绝。
- 保留现有多条消息发送策略:Kernel 统一拆分 `multi_text`,逐条视觉确认,并在消息间随机等待
0.5 到 1.0 秒,插件不能绕过 GUI 发送链路。
- 发布前后保留性能基线对比要求,重点观察本地命令、chat ack、direct queue 和
extension-host 打点,避免架构封装影响运行时延。
## beta 1.2.8.1019 - 2026-10-19 22:49:30
- 冷门知识问答题库整体重写:改为更偏游戏内机制、数值、版本和活动细节的深度题。
+12 -10
View File
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。
当前版本:`beta 1.2.8.1019`,更新时间:2026-10-19 22:49:30。
当前版本:`beta 2.0.0.1021`,更新时间:2026-10-21 16:57:39。
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major
@@ -193,9 +193,10 @@ sudo ./deploy/scripts/deploy-jwebot-production.sh /path/to/JWeBot jwebot
部署必须通过微信窗口就绪、队列清空、Edge 健康、SQLite 完整性和自适应
录制服务重启稳定性检查。切换成功后,部署器按版本幂等地向每个既有账户
发放 200 摩拉、写入账户流水,并使用当前 changelog 的第一条摘要发布群公告;
`beta 1.0.5.1016` 与 `beta 1.1.0.1016` 额外发放 1 原石;
后续普通小版本不额外发放原石。
发放摩拉、写入账户流水,并使用当前 changelog 的第一条摘要发布群公告。
普通小版本发放 200 摩拉;`beta 1.0.5.1016`、`beta 1.1.0.1016`
和 `beta 1.2.0.1019` 额外发放 1 原石;`beta 2.0.0.1021`
作为架构级大版本发放 400 摩拉和 2 原石。
旧 OCR 服务保留安装,但保持禁用。
### 仓库结构
@@ -270,10 +271,10 @@ visually confirmed GUI bridge.
Current version:
```text
beta 1.2.8.1019
beta 2.0.0.1021
```
Updated: 2026-10-19 22:49:30.
Updated: 2026-10-21 16:57:39.
The 1.0 Beta baseline consolidates the stable local command set, person and
context indexes, Steam presence/playtime queries, virtual coin games,
@@ -460,10 +461,11 @@ sudo ./deploy/scripts/deploy-jwebot-production.sh /path/to/JWeBot jwebot
The deployer refuses cutover unless the ready-window, empty-queue, Edge health,
SQLite integrity, and adaptive restart-stability gates pass. Legacy OCR units
remain installed but disabled. After cutover, version-idempotent release
settlement grants 200 Mora to every existing account, records the movement,
and sends the first changelog bullet as the guarded group announcement.
For `beta 1.0.5.1016` and `beta 1.1.0.1016`, the settlement also grants
1 primogem once; ordinary later patch releases do not grant extra primogems.
settlement grants Mora to every existing account, records the movement, and
sends the first changelog bullet as the guarded group announcement. Ordinary
patch releases grant 200 Mora. `beta 1.0.5.1016`, `beta 1.1.0.1016`, and
`beta 1.2.0.1019` also grant 1 primogem once. `beta 2.0.0.1021` is the
architecture-level major release and grants 400 Mora plus 2 primogems.
## Repository Map
+1 -1
View File
@@ -1 +1 @@
beta 1.2.8.1019
beta 2.0.0.1021
+1 -1
View File
@@ -1,6 +1,6 @@
# JWeBot Command Catalog
Current baseline: `beta 1.2.8.1019`.
Current baseline: `beta 2.0.0.1021`.
This catalog records the reviewed commands shipped with the built-in local
extensions. Runtime ownership is frozen at startup from the generated
+61
View File
@@ -0,0 +1,61 @@
# Private Conversation Shadow Testing
This prototype observes one manually selected private conversation without
changing the production group workflow.
It is intentionally unable to switch conversations or send replies:
- The recorder never enqueues matching shadow events.
- Every observation carries `shadow_only=true` and `dispatch_allowed=false`.
- JWeBot Edge ignores marked observations before routing or persistence.
- The shadow registry accepts private targets only. The main group cannot be
added to it.
- No setting in the shadow registry can enable delivery.
## Local Configuration
Keep the real file outside the repository:
```text
~/.config/jwebot/conversation-shadow.json
```
Example with placeholder values:
```json
{
"format_version": 1,
"enabled": true,
"targets": [
{
"key": "test-direct",
"kind": "direct",
"title_fingerprint": "<64 lowercase SHA-256 characters>",
"sender": "<local test label>"
}
]
}
```
Set `JWEBOT_CONVERSATION_SHADOW_CONFIG` to that absolute path only in a local
test process. Without the variable, the feature is not imported or enabled.
The title fingerprint is the SHA-256 value returned by
`title_fingerprint(image)` for a screenshot where the intended conversation is
selected. It is an opaque routing value, not a trusted contact identity.
## Controlled Test
1. Keep production services unchanged.
2. Run the recorder in a separate local test state directory.
3. Select the allowlisted private conversation manually.
4. Let the first capture establish a baseline.
5. Send `/状态` or `JWeBot: 测试` from the other account.
6. Inspect the local history for `shadow_only=true`.
7. Assert that the OCR queue remains empty and no outgoing bubble appears.
Ordinary private messages are observed but do not set `mention_bot`. Only slash
commands and the configured wake word are treated as potential triggers.
Do not point this prototype at production state paths and do not add the
environment variable to a deployed runtime profile.
+4 -31
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-loan."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-loan'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '贷款'
HELP_ENTRIES = ('/贷款;/还款',)
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-ranking'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '富榜'
HELP_ENTRIES = ('/大富翁榜',)
COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+4 -31
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-taffy'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy摩拉'
HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',)
COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-transfer."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-transfer'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '摩拉转账'
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
COMMANDS = ('/转账', '/transfer')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+4 -31
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抛一枚硬币'
HELP_ENTRIES = ('/翻硬币',)
COMMANDS = ('/翻硬币',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for cs-major-autochess."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'cs-major-autochess'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '自走棋'
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for daily-fortune."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'daily-fortune'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '生成当天稳定一致的运势'
HELP_ENTRIES = ('/今日运势',)
COMMANDS = ('/今日运势',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+4 -31
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for dice."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'dice'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '骰子和多人骰子挑战'
HELP_ENTRIES = ('/丢骰子 [NdM]', '/丢骰子挑战 [5d6|5d20|状态|结算|战绩|结束]')
COMMANDS = ('/丢骰子', '/骰子', '/丢骰子挑战', '/骰子挑战')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+4 -31
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for draw-lot."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'draw-lot'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抽取一条本地签文'
HELP_ENTRIES = ('/抽签',)
COMMANDS = ('/抽签',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for game-history."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'game-history'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '游戏记录'
HELP_ENTRIES = ('/游戏记录 /个人记录 /gamehistory',)
COMMANDS = ('/游戏记录', '/个人记录', '/gamehistory')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for guess-number."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'guess-number'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '群内持久化猜数字游戏'
HELP_ENTRIES = ('/猜数字 开始 [上限];/猜 数字;/猜数字 [状态|结束]',)
COMMANDS = ('/猜数字', '/猜')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for history-search."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'history-search'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '搜索本地群聊文字记录'
HELP_ENTRIES = ('/查记录 关键词',)
COMMANDS = ('/查记录', '/历史搜索')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for primogem-economy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'primogem-economy'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '原石系统'
HELP_ENTRIES = ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',)
COMMANDS = ('/原石祈愿', '/祈愿', '/wish', '/原石榜', '/primogem榜', '/扫码加权', '/扫码概率', '/Taffy发话', '/taffysay')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for random-number."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'random-number'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '在指定整数范围内取随机数'
HELP_ENTRIES = ('/随机数 最小值 最大值',)
COMMANDS = ('/随机数',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for repeat-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'repeat-ranking'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '查看北京时间今日复读排行'
HELP_ENTRIES = ('/复读榜',)
COMMANDS = ('/复读榜', '/复读')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for rock-paper-scissors."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'rock-paper-scissors'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '多人石头剪刀布游戏'
HELP_ENTRIES = ('/石头剪刀布 [开始|加入|出 选择|状态|结算|结束]',)
COMMANDS = ('/石头剪刀布',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for scan-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'scan-ranking'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '查看北京时间今日扫码排行'
HELP_ENTRIES = ('/扫码排行',)
COMMANDS = ('/扫码排行', '/扫码榜', '/今日扫码')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for single-player-games."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'single-player-games'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '摩拉游戏'
HELP_ENTRIES = ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',)
COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for steam-presence."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'steam-presence'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = 'Steam 状态与时长'
HELP_ENTRIES = ('/steam;/steam时长 游戏[、玩家]',)
COMMANDS = ('/steam',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for trivia-quiz."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'trivia-quiz'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '冷门知识问答'
HELP_ENTRIES = ('/回答 A;/answer A',)
COMMANDS = ('/回答', '/answer')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for context-summary."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'context-summary'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for debug-telemetry."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'debug-telemetry'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '埋点 p50/p95'
HELP_ENTRIES = ('/debug',)
COMMANDS = ('/debug',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for jwebot-version."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'jwebot-version'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = 'JWeBot 版本'
HELP_ENTRIES = ('/version /版本',)
COMMANDS = ('/版本', '/version')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for person-directory."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'person-directory'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '人物索引'
HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)
COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for person-memory."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'person-memory'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '管理个人和同群人物记忆'
HELP_ENTRIES = ('/记住 内容;/我的记忆;/忘记 关键词或全部;/人物记忆 [显示名]',)
COMMANDS = ('/记住', '/我的记忆', '/忘记', '/人物记忆', '/记忆帮助')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for server-status."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'server-status'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '服务器状态'
HELP_ENTRIES = ('/状态;/status',)
COMMANDS = ('/状态', '/status')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+27 -10
View File
@@ -32,10 +32,14 @@ from jwebot_official_extensions.player_games import ( # noqa: E402
COMPENSATION_AMOUNT = 200
MORA_COMPENSATION_RELEASES = {
"beta 2.0.0.1021": 400,
}
PRIMOGEM_COMPENSATION_RELEASES = {
"beta 1.0.5.1016": 1,
"beta 1.1.0.1016": 1,
"beta 1.2.0.1019": 1,
"beta 2.0.0.1021": 2,
}
MAX_BALANCE = 1_000_000_000
MAX_SUMMARY_CHARS = 300
@@ -127,13 +131,14 @@ def read_changelog_summary(path: Path, release: str) -> str:
def build_announcement(release: str, summary: str) -> str:
mora = mora_compensation_amount(release)
primogems = primogem_compensation_amount(release)
extra = f",原石 x{primogems}" if primogems else ""
announcement = (
"亲爱的群友:\n"
f"{release} 更新现已完成。\n"
f"更新内容:{summary}\n"
f"更新补偿:摩拉 x{COMPENSATION_AMOUNT}{extra}\n"
f"更新补偿:摩拉 x{mora}{extra}\n"
"补偿范围:截至本公告发布前已创建摩拉账户的玩家\n"
"发放方式:补偿已直接到账,无需领取\n"
"感谢大家对 Taffy 的支持与理解,祝大家游戏愉快!"
@@ -145,6 +150,12 @@ def build_announcement(release: str, summary: str) -> str:
def build_lifecycle_message(release: str, stage: str) -> str:
if stage == "shutdown":
if release == "beta 2.0.0.1021":
message = (
"Taffy 准备停机更新,本次更新是架构级大版本更新,"
f"预期版本{release},离线的时候保持耐心迎接Taffy的归来哦~"
)
else:
message = (
f"Taffy 准备停机更新到 {release},会短暂离线一下;"
"别慌,我很快回来喵"
@@ -165,6 +176,10 @@ def compensation_key(release: str) -> str:
return f"release_compensation:{release}"
def mora_compensation_amount(release: str) -> int:
return int(MORA_COMPENSATION_RELEASES.get(release, COMPENSATION_AMOUNT))
def primogem_compensation_amount(release: str) -> int:
return int(PRIMOGEM_COMPENSATION_RELEASES.get(release, 0))
@@ -196,6 +211,7 @@ def release_groups(database_path: Path) -> list[str]:
def checked_marker(raw: str, release: str) -> dict[str, Any]:
expected_mora = mora_compensation_amount(release)
expected_primogems = primogem_compensation_amount(release)
try:
marker = json.loads(raw)
@@ -204,8 +220,8 @@ def checked_marker(raw: str, release: str) -> dict[str, Any]:
if (
not isinstance(marker, dict)
or marker.get("release") != release
or marker.get("amount") != COMPENSATION_AMOUNT
or marker.get("mora_amount") not in {None, COMPENSATION_AMOUNT}
or marker.get("amount") != expected_mora
or marker.get("mora_amount") not in {None, expected_mora}
or marker.get("primogem_amount") not in {None, expected_primogems}
or not isinstance(marker.get("group_fingerprints"), list)
):
@@ -287,6 +303,7 @@ def apply_compensation(
groups = sorted({str(row["group_fingerprint"]) for row in rows})
old_balance_sum = 0
old_primogem_sum = 0
mora_amount = mora_compensation_amount(release)
primogem_amount = primogem_compensation_amount(release)
timestamp = isoformat_utc(applied_at)
event_reason = f"{release} 版本更新补偿"
@@ -302,14 +319,14 @@ def apply_compensation(
balance = state.get("balance")
if (
type(balance) is not int
or not 0 <= balance <= MAX_BALANCE - COMPENSATION_AMOUNT
or not 0 <= balance <= MAX_BALANCE - mora_amount
):
raise ReleaseUpdateError("Mora account balance is invalid")
old_balance_sum += balance
old_primogem_sum += int(state["primogems"])
state["balance"] = balance + COMPENSATION_AMOUNT
state["balance"] = balance + mora_amount
state["primogems"] += primogem_amount
append_coin_event(state, COMPENSATION_AMOUNT, event_reason)
append_coin_event(state, mora_amount, event_reason)
state["coin_events"][-1]["created_at"] = applied_at.timestamp()
if primogem_amount:
append_primogem_event(
@@ -335,14 +352,14 @@ def apply_compensation(
)
marker = {
"release": release,
"amount": COMPENSATION_AMOUNT,
"mora_amount": COMPENSATION_AMOUNT,
"amount": mora_amount,
"mora_amount": mora_amount,
"primogem_amount": primogem_amount,
"accounts": len(rows),
"group_fingerprints": groups,
"old_balance_sum": old_balance_sum,
"new_balance_sum": (
old_balance_sum + COMPENSATION_AMOUNT * len(rows)
old_balance_sum + mora_amount * len(rows)
),
"old_primogem_sum": old_primogem_sum,
"new_primogem_sum": (
@@ -594,7 +611,7 @@ def run_release_update(
"status": "ok",
"release": release,
"compensation": compensation_state,
"amount_each": COMPENSATION_AMOUNT,
"amount_each": mora_compensation_amount(release),
"primogems_each": primogem_compensation_amount(release),
"accounts": int(marker["accounts"]),
"groups": len(groups),
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "1.2.8b1+1019"
version = "2.0.0b1+1021"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "1.2.8b1+1019"
__version__ = "2.0.0b1+1021"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "1.2.8b1+1019"
version = "2.0.0b1+1021"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==1.2.8b1+1019",
"jwebot-kernel==1.2.8b1+1019",
"jwebot-sdk==2.0.0b1+1021",
"jwebot-kernel==2.0.0b1+1021",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "1.2.8b1+1019"
__version__ = "2.0.0b1+1021"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "1.2.8b1+1019"
version = "2.0.0b1+1021"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==1.2.8b1+1019",
"jwebot-sdk==1.2.8b1+1019",
"jwebot-official-extensions==1.2.8b1+1019",
"jwebot-config==2.0.0b1+1021",
"jwebot-sdk==2.0.0b1+1021",
"jwebot-official-extensions==2.0.0b1+1021",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "1.2.8b1+1019"
__version__ = "2.0.0b1+1021"
@@ -0,0 +1,248 @@
"""Fail-closed conversation identities for local OCR shadow testing."""
from __future__ import annotations
import hashlib
import json
import os
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Mapping
SHADOW_CONFIG_ENV = "JWEBOT_CONVERSATION_SHADOW_CONFIG"
SHADOW_CONFIG_VERSION = 1
MAX_SHADOW_TARGETS = 8
MAX_EVENT_TEXT = 5_000
CONVERSATION_KEY = re.compile(r"^[a-z0-9][a-z0-9._-]{0,63}$")
TITLE_FINGERPRINT = re.compile(r"^[0-9a-f]{64}$")
class ConversationConfigError(ValueError):
"""Raised when an opt-in shadow conversation registry is invalid."""
class ConversationKind(str, Enum):
GROUP = "group"
DIRECT = "direct"
def _required_text(value: object, *, name: str, limit: int) -> str:
text = str(value or "").strip()
if not text:
raise ConversationConfigError(f"{name} is required")
if len(text) > limit:
raise ConversationConfigError(f"{name} exceeds {limit} characters")
return text
def _trigger_question(text: str, *, bot_mention: str, direct: bool) -> str:
mention_prefix = r"(?:[@@]\s*)?" if direct else r"[@@]\s*"
mention = re.compile(
rf"^\s*{mention_prefix}{re.escape(bot_mention)}"
r"(?:\s*[::,,])?\s*(\S.*)$",
re.IGNORECASE,
)
for line in text.splitlines():
stripped = line.strip()
if direct and stripped.startswith("/"):
return stripped
match = mention.match(line)
if match:
return match.group(1).strip()
return ""
@dataclass(frozen=True)
class ConversationTarget:
"""One explicitly registered title fingerprint in shadow mode."""
key: str
kind: ConversationKind
title_fingerprint: str
sender: str = ""
def __post_init__(self) -> None:
if not CONVERSATION_KEY.fullmatch(self.key):
raise ConversationConfigError("conversation key must be an opaque slug")
if not TITLE_FINGERPRINT.fullmatch(self.title_fingerprint):
raise ConversationConfigError(
"title_fingerprint must be a lowercase SHA-256 digest"
)
if self.kind is ConversationKind.DIRECT:
_required_text(self.sender, name="direct sender", limit=120)
elif self.sender:
raise ConversationConfigError("group targets cannot define a fixed sender")
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "ConversationTarget":
if not isinstance(value, Mapping):
raise ConversationConfigError("conversation target must be an object")
unknown = set(value) - {
"key",
"kind",
"title_fingerprint",
"sender",
}
if unknown:
raise ConversationConfigError("conversation target has unknown fields")
try:
kind = ConversationKind(str(value.get("kind", "")))
except ValueError as error:
raise ConversationConfigError("conversation kind is invalid") from error
return cls(
key=_required_text(value.get("key"), name="conversation key", limit=64),
kind=kind,
title_fingerprint=_required_text(
value.get("title_fingerprint"),
name="title_fingerprint",
limit=64,
).casefold(),
sender=str(value.get("sender", "")).strip(),
)
def question(self, text: str, *, bot_mention: str) -> str:
return _trigger_question(
text,
bot_mention=bot_mention,
direct=self.kind is ConversationKind.DIRECT,
)
@dataclass(frozen=True)
class ShadowConversationRegistry:
"""A registry that can observe but can never authorize delivery."""
enabled: bool = False
targets: tuple[ConversationTarget, ...] = ()
def __post_init__(self) -> None:
keys = [target.key for target in self.targets]
fingerprints = [target.title_fingerprint for target in self.targets]
if len(keys) != len(set(keys)):
raise ConversationConfigError("conversation keys must be unique")
if len(fingerprints) != len(set(fingerprints)):
raise ConversationConfigError("title fingerprints must be unique")
@classmethod
def disabled(cls) -> "ShadowConversationRegistry":
return cls()
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "ShadowConversationRegistry":
if not isinstance(value, Mapping):
raise ConversationConfigError("shadow registry must be an object")
unknown = set(value) - {"format_version", "enabled", "targets"}
if unknown:
raise ConversationConfigError("shadow registry has unknown fields")
if value.get("format_version") != SHADOW_CONFIG_VERSION:
raise ConversationConfigError("unsupported shadow registry version")
enabled = value.get("enabled", False)
if not isinstance(enabled, bool):
raise ConversationConfigError("shadow registry enabled must be boolean")
raw_targets = value.get("targets", [])
if not isinstance(raw_targets, list):
raise ConversationConfigError("shadow registry targets must be a list")
if len(raw_targets) > MAX_SHADOW_TARGETS:
raise ConversationConfigError("shadow registry has too many targets")
targets = tuple(ConversationTarget.from_mapping(item) for item in raw_targets)
if any(target.kind is not ConversationKind.DIRECT for target in targets):
raise ConversationConfigError("shadow registry accepts direct targets only")
return cls(enabled=enabled, targets=targets)
@classmethod
def from_environment(
cls,
environment: Mapping[str, str] | None = None,
) -> "ShadowConversationRegistry":
source = environment if environment is not None else os.environ
raw_path = str(source.get(SHADOW_CONFIG_ENV, "")).strip()
if not raw_path:
return cls.disabled()
path = Path(raw_path).expanduser()
if path.is_symlink() or not path.is_file():
raise ConversationConfigError("shadow registry path must be a regular file")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ConversationConfigError("shadow registry cannot be read") from error
if not isinstance(value, Mapping):
raise ConversationConfigError("shadow registry must be an object")
return cls.from_mapping(value)
def target_for_fingerprint(
self,
fingerprint: str,
) -> ConversationTarget | None:
if not self.enabled:
return None
normalized = str(fingerprint).casefold()
return next(
(
target
for target in self.targets
if target.title_fingerprint == normalized
),
None,
)
def build_shadow_event(
target: ConversationTarget,
*,
observed_at: str,
text: str,
body_confidence: float,
visual_signature: str,
bot_mention: str,
observed_sender: str = "",
sender_confidence: float = 0.0,
ordinal: int = 0,
) -> dict[str, Any]:
"""Build a non-dispatchable observation for synthetic and shadow tests."""
content = _required_text(text, name="event text", limit=MAX_EVENT_TEXT)
question = target.question(content, bot_mention=bot_mention)
sender = (
target.sender
if target.kind is ConversationKind.DIRECT
else _required_text(observed_sender, name="observed sender", limit=120)
)
effective_sender_confidence = (
1.0
if target.kind is ConversationKind.DIRECT
else max(0.0, min(1.0, float(sender_confidence)))
)
effective_body_confidence = max(0.0, min(1.0, float(body_confidence)))
seed = (
f"{target.key}\n{observed_at}\n{ordinal}\n{visual_signature}\n"
f"{sender}\n{content}"
)
return {
"version": 1,
"event_id": f"shadow-{hashlib.sha256(seed.encode()).hexdigest()[:24]}",
"observed_at": observed_at,
"sender": sender,
"text": content,
"question": question,
"confidence": min(
effective_sender_confidence,
effective_body_confidence,
),
"sender_confidence": effective_sender_confidence,
"body_confidence": effective_body_confidence,
"side": "incoming",
"mention_bot": bool(question),
"group_fingerprint": target.title_fingerprint,
"conversation_key": target.key,
"conversation_kind": target.kind.value,
"source": (
"rapidocr-private-shadow"
if target.kind is ConversationKind.DIRECT
else "rapidocr-group-shadow"
),
"shadow_only": True,
"dispatch_allowed": False,
}
@@ -8009,6 +8009,20 @@ class EdgeApplication:
started = time.monotonic()
event_id = str(event.get("event_id", ""))
group_id = str(event.get("group_fingerprint", ""))
if event.get("shadow_only") or event.get("dispatch_allowed") is False:
self.telemetry.elapsed(
"edge.event.accepted",
started,
status="shadow_ignored",
event_id=event_id,
group_id=group_id,
route="none",
execution_path="none",
)
return {
"status": "ignored",
"reason": "shadow_only",
}
question = event_question(event)
decision = route_question(question)
route = decision.route.value
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "1.2.8b1+1019"
version = "2.0.0b1+1021"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==1.2.8b1+1019",
"jwebot-sdk==2.0.0b1+1021",
]
[tool.setuptools.packages.find]
@@ -90,4 +90,4 @@ __all__ = [
"render_feature_list",
]
__version__ = "1.2.8b1+1019"
__version__ = "2.0.0b1+1021"
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抛一枚硬币'
HELP_ENTRIES = ('/翻硬币',)
COMMANDS = ('/翻硬币',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-loan."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-loan'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '贷款'
HELP_ENTRIES = ('/贷款;/还款',)
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-ranking'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '富榜'
HELP_ENTRIES = ('/大富翁榜',)
COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-taffy'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy摩拉'
HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',)
COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin-transfer."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-transfer'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '摩拉转账'
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
COMMANDS = ('/转账', '/transfer')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for context-summary."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'context-summary'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for cs-major-autochess."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'cs-major-autochess'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '自走棋'
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for daily-fortune."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'daily-fortune'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '生成当天稳定一致的运势'
HELP_ENTRIES = ('/今日运势',)
COMMANDS = ('/今日运势',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for debug-telemetry."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'debug-telemetry'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '埋点 p50/p95'
HELP_ENTRIES = ('/debug',)
COMMANDS = ('/debug',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for dice."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'dice'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '骰子和多人骰子挑战'
HELP_ENTRIES = ('/丢骰子 [NdM]', '/丢骰子挑战 [5d6|5d20|状态|结算|战绩|结束]')
COMMANDS = ('/丢骰子', '/骰子', '/丢骰子挑战', '/骰子挑战')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for draw-lot."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'draw-lot'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抽取一条本地签文'
HELP_ENTRIES = ('/抽签',)
COMMANDS = ('/抽签',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for game-history."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'game-history'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '游戏记录'
HELP_ENTRIES = ('/游戏记录 /个人记录 /gamehistory',)
COMMANDS = ('/游戏记录', '/个人记录', '/gamehistory')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for guess-number."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'guess-number'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '群内持久化猜数字游戏'
HELP_ENTRIES = ('/猜数字 开始 [上限];/猜 数字;/猜数字 [状态|结束]',)
COMMANDS = ('/猜数字', '/猜')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for history-search."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'history-search'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '搜索本地群聊文字记录'
HELP_ENTRIES = ('/查记录 关键词',)
COMMANDS = ('/查记录', '/历史搜索')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for jwebot-version."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'jwebot-version'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = 'JWeBot 版本'
HELP_ENTRIES = ('/version /版本',)
COMMANDS = ('/版本', '/version')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for person-directory."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'person-directory'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '人物索引'
HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)
COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for person-memory."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'person-memory'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '管理个人和同群人物记忆'
HELP_ENTRIES = ('/记住 内容;/我的记忆;/忘记 关键词或全部;/人物记忆 [显示名]',)
COMMANDS = ('/记住', '/我的记忆', '/忘记', '/人物记忆', '/记忆帮助')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for primogem-economy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'primogem-economy'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '原石系统'
HELP_ENTRIES = ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',)
COMMANDS = ('/原石祈愿', '/祈愿', '/wish', '/原石榜', '/primogem榜', '/扫码加权', '/扫码概率', '/Taffy发话', '/taffysay')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for random-number."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'random-number'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '在指定整数范围内取随机数'
HELP_ENTRIES = ('/随机数 最小值 最大值',)
COMMANDS = ('/随机数',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for repeat-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'repeat-ranking'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '查看北京时间今日复读排行'
HELP_ENTRIES = ('/复读榜',)
COMMANDS = ('/复读榜', '/复读')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for rock-paper-scissors."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'rock-paper-scissors'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '多人石头剪刀布游戏'
HELP_ENTRIES = ('/石头剪刀布 [开始|加入|出 选择|状态|结算|结束]',)
COMMANDS = ('/石头剪刀布',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for scan-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'scan-ranking'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '查看北京时间今日扫码排行'
HELP_ENTRIES = ('/扫码排行',)
COMMANDS = ('/扫码排行', '/扫码榜', '/今日扫码')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for server-status."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'server-status'
@@ -8,38 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '服务器状态'
HELP_ENTRIES = ('/状态;/status',)
COMMANDS = ('/状态', '/status')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for single-player-games."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'single-player-games'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '摩拉游戏'
HELP_ENTRIES = ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',)
COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for steam-presence."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'steam-presence'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = 'Steam 状态与时长'
HELP_ENTRIES = ('/steam;/steam时长 游戏[、玩家]',)
COMMANDS = ('/steam',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for trivia-quiz."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'trivia-quiz'
@@ -8,38 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '冷门知识问答'
HELP_ENTRIES = ('/回答 A;/answer A',)
COMMANDS = ('/回答', '/answer')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-sdk"
version = "1.2.8b1+1019"
version = "2.0.0b1+1021"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -8,6 +8,7 @@ from .context import (
PluginLogger,
PluginStorage,
Scheduler,
ServiceCommandAdapter,
SingleGroupContext,
)
from .contracts import (
@@ -35,7 +36,8 @@ __all__ = [
"PluginStorage",
"ReplyPlan",
"Scheduler",
"ServiceCommandAdapter",
"SingleGroupContext",
]
__version__ = "1.2.8b1+1019"
__version__ = "2.0.0b1+1021"
+66 -2
View File
@@ -2,10 +2,10 @@
from __future__ import annotations
from collections.abc import Callable, Mapping
from collections.abc import Callable, Iterable, Mapping
from typing import Protocol
from .contracts import Event, ExtensionResult, Fact
from .contracts import Event, ExtensionResult, Fact, ReplyPlan
CommandHandler = Callable[[Event, str], ExtensionResult]
@@ -68,3 +68,67 @@ class SingleGroupContext(Protocol):
facts: FactWriter
scheduler: Scheduler
logger: PluginLogger
class ServiceCommandAdapter:
"""Register commands that delegate only to declared kernel service profiles."""
def __init__(
self,
profile: str,
*,
operation: str = "execute",
metadata: Mapping[str, str] | None = None,
) -> None:
self.profile = self._bounded_text(profile, "profile")
self.operation = self._bounded_text(operation, "operation")
self.metadata = {
str(key): str(value)
for key, value in (metadata or {}).items()
}
@staticmethod
def _bounded_text(value: object, name: str) -> str:
text = str(value or "").strip()
if not text or len(text) > 80:
raise ValueError(f"{name} is invalid")
return text
def _handler(self, context: SingleGroupContext, command: str) -> CommandHandler:
command_name = self._bounded_text(command, "command")
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
self.profile,
operation=self.operation,
payload={
"command": command_name,
"arguments": str(arguments),
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts:
if not all(isinstance(item, str) and item.strip() for item in texts):
raise ValueError("service returned invalid text parts")
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata=self.metadata,
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata=self.metadata,
)
return handle
def register(
self,
context: SingleGroupContext,
commands: Iterable[str],
) -> None:
for command in commands:
context.commands.register(command, self._handler(context, command))
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.jwebot]
release = "beta 1.2.8.1019"
release = "beta 2.0.0.1021"
production_deployments_enabled_by_default = false
[tool.jwebot.workspace]
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
import hashlib
import json
import sys
import tempfile
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(PROJECT_ROOT / "packages" / "jwebot-config" / "src"),
str(PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"),
str(PROJECT_ROOT / "packages" / "jwebot-official-extensions" / "src"),
str(PROJECT_ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel.conversation_scope import ( # noqa: E402
ConversationConfigError,
ConversationKind,
ConversationTarget,
ShadowConversationRegistry,
build_shadow_event,
)
def fingerprint(label: str) -> str:
return hashlib.sha256(label.encode()).hexdigest()
class ConversationTargetTests(unittest.TestCase):
def test_group_requires_an_explicit_mention(self):
target = ConversationTarget(
key="main-group",
kind=ConversationKind.GROUP,
title_fingerprint=fingerprint("main-group"),
)
self.assertEqual(
target.question("@JWeBot hello", bot_mention="JWeBot"),
"hello",
)
self.assertEqual(target.question("hello", bot_mention="JWeBot"), "")
def test_direct_chat_accepts_only_commands_or_wake_words(self):
target = ConversationTarget(
key="test-direct",
kind=ConversationKind.DIRECT,
title_fingerprint=fingerprint("test-direct"),
sender="Test User",
)
self.assertEqual(
target.question("/状态", bot_mention="JWeBot"),
"/状态",
)
self.assertEqual(
target.question("JWeBot: hello", bot_mention="JWeBot"),
"hello",
)
self.assertEqual(target.question("普通聊天", bot_mention="JWeBot"), "")
def test_direct_chat_requires_a_configured_sender(self):
with self.assertRaisesRegex(ConversationConfigError, "direct sender"):
ConversationTarget(
key="test-direct",
kind=ConversationKind.DIRECT,
title_fingerprint=fingerprint("test-direct"),
)
def test_title_identity_must_be_a_sha256_digest(self):
with self.assertRaisesRegex(ConversationConfigError, "SHA-256"):
ConversationTarget(
key="test-direct",
kind=ConversationKind.DIRECT,
title_fingerprint="visible-contact-name",
sender="Test User",
)
class ShadowRegistryTests(unittest.TestCase):
def test_registry_is_disabled_without_an_explicit_path(self):
registry = ShadowConversationRegistry.from_environment({})
self.assertFalse(registry.enabled)
self.assertEqual(registry.targets, ())
def test_disabled_registry_never_matches_targets(self):
digest = fingerprint("test-direct")
registry = ShadowConversationRegistry.from_mapping(
{
"format_version": 1,
"enabled": False,
"targets": [
{
"key": "test-direct",
"kind": "direct",
"title_fingerprint": digest,
"sender": "Test User",
}
],
}
)
self.assertIsNone(registry.target_for_fingerprint(digest))
def test_explicit_config_loads_only_shadow_targets(self):
digest = fingerprint("test-direct")
config = {
"format_version": 1,
"enabled": True,
"targets": [
{
"key": "test-direct",
"kind": "direct",
"title_fingerprint": digest,
"sender": "Test User",
}
],
}
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "shadow.json"
path.write_text(json.dumps(config), encoding="utf-8")
registry = ShadowConversationRegistry.from_environment(
{"JWEBOT_CONVERSATION_SHADOW_CONFIG": str(path)}
)
self.assertTrue(registry.enabled)
self.assertEqual(registry.target_for_fingerprint(digest), registry.targets[0])
def test_duplicate_title_fingerprints_are_rejected(self):
digest = fingerprint("duplicate")
with self.assertRaisesRegex(ConversationConfigError, "fingerprints"):
ShadowConversationRegistry.from_mapping(
{
"format_version": 1,
"enabled": True,
"targets": [
{
"key": "direct-a",
"kind": "direct",
"title_fingerprint": digest,
"sender": "Test A",
},
{
"key": "direct-b",
"kind": "direct",
"title_fingerprint": digest,
"sender": "Test B",
},
],
}
)
def test_group_targets_are_rejected_in_private_shadow_phase(self):
with self.assertRaisesRegex(ConversationConfigError, "direct targets only"):
ShadowConversationRegistry.from_mapping(
{
"format_version": 1,
"enabled": True,
"targets": [
{
"key": "main-group",
"kind": "group",
"title_fingerprint": fingerprint("main-group"),
}
],
}
)
def test_unknown_delivery_field_is_rejected(self):
with self.assertRaisesRegex(ConversationConfigError, "unknown fields"):
ShadowConversationRegistry.from_mapping(
{
"format_version": 1,
"enabled": True,
"delivery_enabled": True,
"targets": [],
}
)
class ShadowEventTests(unittest.TestCase):
def test_direct_event_uses_verified_conversation_identity(self):
digest = fingerprint("test-direct")
target = ConversationTarget(
key="test-direct",
kind=ConversationKind.DIRECT,
title_fingerprint=digest,
sender="Test User",
)
event = build_shadow_event(
target,
observed_at="2026-10-20T12:00:00+08:00",
text="/状态",
body_confidence=0.98,
visual_signature="visual-1",
bot_mention="JWeBot",
)
self.assertEqual(event["sender"], "Test User")
self.assertEqual(event["sender_confidence"], 1.0)
self.assertEqual(event["question"], "/状态")
self.assertTrue(event["mention_bot"])
self.assertEqual(event["group_fingerprint"], digest)
self.assertEqual(event["conversation_kind"], "direct")
self.assertTrue(event["shadow_only"])
self.assertFalse(event["dispatch_allowed"])
def test_unaddressed_direct_message_is_observed_but_not_triggered(self):
target = ConversationTarget(
key="test-direct",
kind=ConversationKind.DIRECT,
title_fingerprint=fingerprint("test-direct"),
sender="Test User",
)
event = build_shadow_event(
target,
observed_at="2026-10-20T12:00:00+08:00",
text="普通聊天",
body_confidence=0.98,
visual_signature="visual-2",
bot_mention="JWeBot",
)
self.assertEqual(event["question"], "")
self.assertFalse(event["mention_bot"])
self.assertTrue(event["shadow_only"])
self.assertFalse(event["dispatch_allowed"])
if __name__ == "__main__":
unittest.main()
+45 -3
View File
@@ -316,6 +316,48 @@ class LedgerAndApplicationTests(unittest.TestCase):
"第二条回复喵",
)
def test_conversation_context_is_isolated_by_fingerprint(self):
observed = {}
def complete(**kwargs):
observed[kwargs["event_id"]] = list(kwargs["context"])
return f"{kwargs['event_id']} 回复喵"
self.application.deepseek.complete = complete
self.application.deliver = mock.Mock()
for event_id, group, text in (
("group-first", "group-a", "@JWeBot 群里的第一条"),
("direct-first", "direct-a", "私聊里的第一条"),
("group-second", "group-a", "@JWeBot 群里继续"),
("direct-second", "direct-a", "私聊继续"),
):
self.application.accept(event(event_id, text, group=group))
self.application.process_direct_chat_once()
self.assertEqual(
observed["group-second"],
[("群里的第一条", "group-first 回复喵")],
)
self.assertEqual(
observed["direct-second"],
[("私聊里的第一条", "direct-first 回复喵")],
)
def test_shadow_event_is_rejected_before_routing_or_persistence(self):
response = self.application.accept(
{
**event("shadow-1", "/状态", group="direct-a"),
"shadow_only": True,
"dispatch_allowed": False,
}
)
self.assertEqual(
response,
{"status": "ignored", "reason": "shadow_only"},
)
self.assertIsNone(self.application.ledger.row("shadow-1"))
def test_local_game_command_result_is_available_to_followup_chat(self):
observed_context = []
@@ -1039,9 +1081,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
self.assertEqual(version["route"], "local")
self.assertIn("JWeBot 版本", version["text"])
self.assertIn("kernel", version["text"])
self.assertIn("最新版本:beta 1.2.8.1019", version["text"])
self.assertIn("更新时间:2026-10-19 22:49:30", version["text"])
self.assertIn("更新摘要:冷门知识问答题库整体重写", version["text"])
self.assertIn("最新版本:beta 2.0.0.1021", version["text"])
self.assertIn("更新时间:2026-10-21 16:57:39", version["text"])
self.assertIn("更新摘要:Beta 2.0 架构更新完成", version["text"])
self.assertEqual(debug["route"], "local")
self.assertIn("调试埋点", debug["text"])
self.assertEqual(balance["route"], "local")
+29 -13
View File
@@ -20,7 +20,7 @@ SPEC.loader.exec_module(MODULE)
class ReleaseUpdateTests(unittest.TestCase):
release = "beta 1.2.8.1019"
release = "beta 2.0.0.1021"
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
@@ -111,8 +111,7 @@ class ReleaseUpdateTests(unittest.TestCase):
self.assertEqual(summary, "第一行更新摘要, 同一条摘要可以换行。")
announcement = MODULE.build_announcement(self.release, summary)
self.assertIn(f"{self.release} 更新现已完成", announcement)
self.assertIn("更新补偿:摩拉 x200", announcement)
self.assertNotIn("原石 x1", announcement)
self.assertIn("更新补偿:摩拉 x400,原石 x2", announcement)
self.assertNotIn("第二条", announcement)
def test_policy_release_announces_primogem(self):
@@ -121,10 +120,17 @@ class ReleaseUpdateTests(unittest.TestCase):
"beta 1.1.0.1016",
"beta 1.2.0.1019",
):
self.assertEqual(MODULE.mora_compensation_amount(release), 200)
self.assertEqual(MODULE.primogem_compensation_amount(release), 1)
announcement = MODULE.build_announcement(release, "原石系统上线。")
self.assertIn("更新补偿:摩拉 x200,原石 x1", announcement)
def test_major_release_announces_larger_compensation(self):
self.assertEqual(MODULE.mora_compensation_amount(self.release), 400)
self.assertEqual(MODULE.primogem_compensation_amount(self.release), 2)
announcement = MODULE.build_announcement(self.release, "架构更新完成。")
self.assertIn("更新补偿:摩拉 x400,原石 x2", announcement)
def test_patch_release_without_policy_does_not_announce_primogem(self):
release = "beta 1.1.2.1019"
self.assertEqual(MODULE.primogem_compensation_amount(release), 0)
@@ -149,8 +155,13 @@ class ReleaseUpdateTests(unittest.TestCase):
self.assertEqual(result["stage"], "shutdown")
self.assertEqual(result["delivered"], 1)
self.assertEqual(delivered[0][1], "group-a")
self.assertIn("准备停机更新", delivered[0][2])
self.assertIn(self.release, delivered[0][2])
self.assertEqual(
delivered[0][2],
(
"Taffy 准备停机更新,本次更新是架构级大版本更新,"
f"预期版本{self.release},离线的时候保持耐心迎接Taffy的归来哦~"
),
)
self.assertIn("shutdown", delivered[0][3])
self.assertEqual([state["balance"] for state in self.account_states()], [1000, 50])
@@ -186,13 +197,13 @@ class ReleaseUpdateTests(unittest.TestCase):
self.assertEqual(len(delivered), 1)
self.assertEqual(delivered[0][1], "group-a")
self.assertIn("更新摩拉账户并修复重复回复问题", delivered[0][2])
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
self.assertEqual([state["balance"] for state in self.account_states()], [1400, 450])
for state in self.account_states():
self.assertEqual(state["primogems"], 0)
self.assertEqual(state["primogems"], 2)
matches = [
event
for event in state["coin_events"]
if event["delta"] == 200
if event["delta"] == 400
and event["reason"] == f"{self.release} 版本更新补偿"
]
self.assertEqual(len(matches), 1)
@@ -202,7 +213,12 @@ class ReleaseUpdateTests(unittest.TestCase):
for event in state["primogem_events"]
if event["reason"] == f"{self.release} 版本更新补偿"
]
self.assertEqual(primogem_matches, [])
self.assertEqual(len(primogem_matches), 1)
self.assertEqual(primogem_matches[0]["delta"], 2)
self.assertEqual(
primogem_matches[0]["created_at"],
applied_at.timestamp(),
)
self.assertEqual(self.announcement_marker()["state"], "delivered")
self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1)
@@ -219,8 +235,8 @@ class ReleaseUpdateTests(unittest.TestCase):
delivery_helper=self.delivery_helper,
deliverer=uncertain,
)
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
self.assertEqual([state["primogems"] for state in self.account_states()], [0, 0])
self.assertEqual([state["balance"] for state in self.account_states()], [1400, 450])
self.assertEqual([state["primogems"] for state in self.account_states()], [2, 2])
self.assertEqual(self.announcement_marker()["state"], "uncertain")
called = []
@@ -250,8 +266,8 @@ class ReleaseUpdateTests(unittest.TestCase):
)
self.assertEqual(result["compensation"], "already_applied")
self.assertEqual(len(called), 1)
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
self.assertEqual([state["primogems"] for state in self.account_states()], [0, 0])
self.assertEqual([state["balance"] for state in self.account_states()], [1400, 450])
self.assertEqual([state["primogems"] for state in self.account_states()], [2, 2])
self.assertEqual(self.announcement_marker()["state"], "delivered")
+66
View File
@@ -16,6 +16,7 @@ from jwebot_sdk import ( # noqa: E402
Fact,
PlanKind,
ReplyPlan,
ServiceCommandAdapter,
)
@@ -108,5 +109,70 @@ class PlanContractTests(unittest.TestCase):
ExtensionResult.from_mapping(value)
class ServiceCommandAdapterTests(unittest.TestCase):
def event(self):
return Event(
event_id="event-1",
observed_at="2026-10-14T10:00:00+08:00",
sender="Alice",
text="@JWeBot /coin",
group_fingerprint="group-a",
mention_bot=True,
)
def context(self, response):
class Commands:
def __init__(self):
self.handlers = {}
def register(self, command, handler):
self.handlers[command] = handler
class Services:
def __init__(self):
self.calls = []
def call(self, profile, *, operation, payload):
self.calls.append((profile, operation, payload))
return response
class Context:
pass
context = Context()
context.commands = Commands()
context.services = Services()
return context
def test_adapter_delegates_to_declared_service_profile(self):
context = self.context({"texts": ["第一条", "第二条"]})
adapter = ServiceCommandAdapter(
"coin",
metadata={"local_extension": "coin"},
)
adapter.register(context, ["/翻硬币"])
result = context.commands.handlers["/翻硬币"](self.event(), "")
self.assertEqual(result.reply.kind, PlanKind.MULTI_TEXT)
self.assertEqual(result.reply.messages, ("第一条", "第二条"))
self.assertEqual(dict(result.metadata), {"local_extension": "coin"})
self.assertEqual(context.services.calls[0][0], "coin")
self.assertEqual(context.services.calls[0][1], "execute")
self.assertEqual(context.services.calls[0][2]["command"], "/翻硬币")
self.assertEqual(
context.services.calls[0][2]["event"]["event_id"],
"event-1",
)
def test_adapter_rejects_invalid_service_text(self):
context = self.context({"texts": ["ok", ""]})
adapter = ServiceCommandAdapter("coin")
adapter.register(context, ["/翻硬币"])
with self.assertRaisesRegex(ValueError, "invalid text parts"):
context.commands.handlers["/翻硬币"](self.event(), "")
if __name__ == "__main__":
unittest.main()
+257
View File
@@ -931,6 +931,263 @@ class RecorderSignatureMigrationTests(unittest.TestCase):
engine.warmup.assert_called_once()
class RecorderConversationStateTests(unittest.TestCase):
def test_legacy_active_state_migrates_only_to_its_matching_conversation(self):
engine = mock.Mock()
with mock.patch.object(
MODULE,
"ensure_directories",
), mock.patch.object(
MODULE,
"OcrEngine",
return_value=engine,
), mock.patch.object(
MODULE,
"read_json",
return_value={
"initialized": True,
"last_group_fingerprint": "group-a",
"last_visual_hash": "visual-a",
"visible_signatures": ["message-a"],
"recent_trigger_signatures": ["trigger-a"],
"message_signature_scheme": MODULE.MESSAGE_SIGNATURE_SCHEME,
},
):
recorder = MODULE.Recorder(queue_enabled=True)
group_a = recorder.conversation_state("group-a")
group_b = recorder.conversation_state("group-b")
self.assertTrue(group_a["initialized"])
self.assertEqual(group_a["visible_signatures"], ["message-a"])
self.assertFalse(group_b["initialized"])
self.assertEqual(group_b["visible_signatures"], [])
def test_switching_conversations_does_not_cross_deduplicate_messages(self):
engine = mock.Mock()
first_a = MODULE.IncomingMessage(
"Alice",
"@JWeBot first",
0.98,
0.99,
0.98,
100,
"a-first",
)
first_b = MODULE.IncomingMessage(
"Bob",
"@JWeBot first",
0.98,
0.99,
0.98,
100,
"b-first",
)
second_a = MODULE.IncomingMessage(
"Alice",
"@JWeBot second",
0.98,
0.99,
0.98,
160,
"a-second",
)
engine.recognize_messages.side_effect = [
([first_a], 0.01),
([first_b], 0.01),
([first_a, second_a], 0.01),
]
engine.last_cache_hits = 0
engine.last_cache_misses = 1
engine.last_cache_refresh = False
queued = []
image = MODULE.Image.new(
"RGB",
(MODULE.EXPECTED_WIDTH, MODULE.EXPECTED_HEIGHT),
"white",
)
with mock.patch.object(
MODULE,
"ensure_directories",
), mock.patch.object(
MODULE,
"OcrEngine",
return_value=engine,
), mock.patch.object(
MODULE,
"read_json",
return_value={},
), mock.patch.object(
MODULE,
"incoming_mask_and_bubbles",
return_value=(None, []),
), mock.patch.object(
MODULE,
"write_history_event",
), mock.patch.object(
MODULE,
"queue_event",
side_effect=queued.append,
):
recorder = MODULE.Recorder(queue_enabled=True)
recorder.save_state = mock.Mock()
recorder.process_stable_image(
image,
"layout-a-1",
visual_hash="visual-a-1",
observed_at="2026-10-20T12:00:00+08:00",
group_fingerprint="group-a",
)
recorder.process_stable_image(
image,
"layout-b-1",
visual_hash="visual-b-1",
observed_at="2026-10-20T12:00:01+08:00",
group_fingerprint="group-b",
)
recorder.process_stable_image(
image,
"layout-a-2",
visual_hash="visual-a-2",
observed_at="2026-10-20T12:00:02+08:00",
group_fingerprint="group-a",
)
self.assertEqual(len(queued), 1)
self.assertEqual(queued[0]["group_fingerprint"], "group-a")
self.assertEqual(queued[0]["text"], "@JWeBot second")
states = recorder.state["conversation_states"]
self.assertEqual(states["group-a"]["visible_signatures"], ["a-first", "a-second"])
self.assertEqual(states["group-b"]["visible_signatures"], ["b-first"])
def test_shadow_direct_messages_are_observed_without_queueing(self):
class ShadowTarget:
def question(self, text, *, bot_mention):
del bot_mention
return text if text.startswith("/") else ""
class ShadowRegistry:
def target_for_fingerprint(self, fingerprint):
return ShadowTarget() if fingerprint == "direct-a" else None
def build_shadow_event(
target,
*,
observed_at,
text,
body_confidence,
visual_signature,
bot_mention,
observed_sender,
sender_confidence,
ordinal,
):
del (
target,
body_confidence,
visual_signature,
bot_mention,
observed_sender,
sender_confidence,
ordinal,
)
return {
"event_id": "shadow-event",
"observed_at": observed_at,
"sender": "Test User",
"text": text,
"mention_bot": True,
"group_fingerprint": "direct-a",
"shadow_only": True,
"dispatch_allowed": False,
}
engine = mock.Mock()
first = MODULE.IncomingMessage(
"unknown",
"/状态",
0.0,
0.0,
0.98,
100,
"direct-first",
)
second = MODULE.IncomingMessage(
"unknown",
"/版本",
0.0,
0.0,
0.98,
160,
"direct-second",
)
engine.recognize_messages.side_effect = [
([first], 0.01),
([first, second], 0.01),
]
engine.last_cache_hits = 0
engine.last_cache_misses = 1
engine.last_cache_refresh = False
history = []
image = MODULE.Image.new(
"RGB",
(MODULE.EXPECTED_WIDTH, MODULE.EXPECTED_HEIGHT),
"white",
)
with mock.patch.object(
MODULE,
"ensure_directories",
), mock.patch.object(
MODULE,
"OcrEngine",
return_value=engine,
), mock.patch.object(
MODULE,
"read_json",
return_value={},
), mock.patch.object(
MODULE,
"load_conversation_shadow_support",
return_value=(ShadowRegistry(), build_shadow_event),
), mock.patch.object(
MODULE,
"incoming_mask_and_bubbles",
return_value=(None, []),
), mock.patch.object(
MODULE,
"write_history_event",
side_effect=history.append,
), mock.patch.object(
MODULE,
"queue_event",
) as queue_event:
recorder = MODULE.Recorder(queue_enabled=True)
recorder.save_state = mock.Mock()
recorder.process_stable_image(
image,
"layout-direct-1",
visual_hash="visual-direct-1",
observed_at="2026-10-20T12:00:00+08:00",
group_fingerprint="direct-a",
)
recorder.process_stable_image(
image,
"layout-direct-2",
visual_hash="visual-direct-2",
observed_at="2026-10-20T12:00:01+08:00",
group_fingerprint="direct-a",
)
queue_event.assert_not_called()
self.assertEqual(len(history), 1)
self.assertEqual(history[0]["text"], "/版本")
self.assertTrue(history[0]["shadow_only"])
self.assertFalse(history[0]["dispatch_allowed"])
self.assertEqual(recorder.state["shadowed_total"], 1)
class StorageTests(unittest.TestCase):
def test_elapsed_since_iso_is_timezone_aware_and_bounded(self):
now = datetime(2026, 10, 14, 12, 0, 1, tzinfo=timezone.utc)
+4 -31
View File
@@ -68,7 +68,7 @@ def module_text(descriptor) -> str:
extension_id = descriptor.extension_id
return f'''"""Generated reviewed adapter for {extension_id}."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = {extension_id!r}
@@ -76,41 +76,14 @@ AUTHOR = {descriptor.author!r}
DESCRIPTION = {descriptor.description!r}
HELP_ENTRIES = {descriptor.help_entries!r}
COMMANDS = {descriptor.commands!r}
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={{
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
}},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={{"local_extension": PROFILE, "author": AUTHOR}},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={{"local_extension": PROFILE, "author": AUTHOR}},
)
return handle
)
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
'''
+140 -16
View File
@@ -116,6 +116,30 @@ class RouterInvocationUnavailable(RuntimeError):
"""A transient route invocation failure that must be retried by the queue."""
def load_conversation_shadow_support() -> tuple[object | None, Callable[..., Any] | None]:
if not os.environ.get("JWEBOT_CONVERSATION_SHADOW_CONFIG", "").strip():
return None, None
root = Path(__file__).resolve().parent
source_roots = [
root / "packages" / package / "src"
for package in (
"jwebot-config",
"jwebot-sdk",
"jwebot-official-extensions",
"jwebot-kernel",
)
]
for source_root in reversed(source_roots):
if source_root.is_dir():
sys.path.insert(0, str(source_root))
from jwebot_kernel.conversation_scope import ( # noqa: PLC0415
ShadowConversationRegistry,
build_shadow_event,
)
return ShadowConversationRegistry.from_environment(), build_shadow_event
def router_error_message(
*,
result: subprocess.CompletedProcess[str] | None = None,
@@ -1577,27 +1601,81 @@ class Recorder:
self.state["last_visual_hash"] = ""
self.state["visible_signatures"] = []
self.state["recent_trigger_signatures"] = []
raw_conversations = self.state.get("conversation_states")
if not isinstance(raw_conversations, dict):
raw_conversations = {}
self.state["conversation_states"] = {
str(fingerprint): value
for fingerprint, value in raw_conversations.items()
if isinstance(value, dict)
and value.get("message_signature_scheme") == MESSAGE_SIGNATURE_SCHEME
}
self.pending_hash = ""
self.pending_count = 0
self.pending_frames: dict[str, tuple[str, int]] = {}
self.no_overlap_recovery_ready = False
self.recovery_ready_conversations: set[str] = set()
(
self.shadow_conversations,
self.build_shadow_event,
) = load_conversation_shadow_support()
def save_state(self) -> None:
write_json(RECORDER_STATE, self.state)
def conversation_state(self, fingerprint: str) -> dict[str, Any]:
conversations = self.state["conversation_states"]
value = conversations.get(fingerprint)
if isinstance(value, dict):
return value
if (
fingerprint == self.state.get("last_group_fingerprint")
and self.state.get("message_signature_scheme")
== MESSAGE_SIGNATURE_SCHEME
):
value = {
"initialized": bool(self.state.get("initialized")),
"last_layout_hash": str(self.state.get("last_layout_hash", "")),
"last_visual_hash": str(self.state.get("last_visual_hash", "")),
"visible_signatures": list(
self.state.get("visible_signatures", [])
),
"recent_trigger_signatures": list(
self.state.get("recent_trigger_signatures", [])
),
"message_signature_scheme": MESSAGE_SIGNATURE_SCHEME,
}
else:
value = {
"initialized": False,
"last_layout_hash": "",
"last_visual_hash": "",
"visible_signatures": [],
"recent_trigger_signatures": [],
"message_signature_scheme": MESSAGE_SIGNATURE_SCHEME,
}
conversations[fingerprint] = value
return value
def capture_cycle(self, *, stable_frames: int) -> None:
started = time.monotonic()
image = capture_image()
crop = message_image(image)
layout_hash, visual_hash = incoming_frame_hashes(crop)
if visual_hash != self.pending_hash:
self.pending_hash = visual_hash
self.pending_count = 1
group = title_fingerprint(image)
conversation = self.conversation_state(group)
pending_hash, pending_count = self.pending_frames.get(group, ("", 0))
if visual_hash != pending_hash:
pending_count = 1
else:
self.pending_count += 1
pending_count += 1
self.pending_frames[group] = (visual_hash, pending_count)
self.pending_hash = visual_hash
self.pending_count = pending_count
outcome = "pending"
if (
self.pending_count >= stable_frames
and visual_hash != self.state.get("last_visual_hash")
pending_count >= stable_frames
and visual_hash != conversation.get("last_visual_hash")
):
outcome = "process"
self.process_stable_image(
@@ -1605,12 +1683,13 @@ class Recorder:
layout_hash,
visual_hash=visual_hash,
observed_at=now_iso(),
group_fingerprint=group,
)
telemetry_event(
"ocr.capture",
elapsed_ms=(time.monotonic() - started) * 1000,
outcome=outcome,
count=self.pending_count,
count=pending_count,
source="screen",
)
@@ -1634,46 +1713,75 @@ class Recorder:
*,
visual_hash: str | None = None,
observed_at: str | None = None,
group_fingerprint: str | None = None,
) -> None:
pipeline_started = time.monotonic()
crop = message_image(image)
_, bubbles = incoming_mask_and_bubbles(crop)
messages, elapsed = self.engine.recognize_messages(crop, bubbles)
group = group_fingerprint or title_fingerprint(image)
conversation = self.conversation_state(group)
shadow_target = (
self.shadow_conversations.target_for_fingerprint(group)
if self.shadow_conversations is not None
else None
)
previous = [
value
for value in self.state.get("visible_signatures", [])
for value in conversation.get("visible_signatures", [])
if isinstance(value, str)
]
recent_trigger_signatures = [
value
for value in self.state.get("recent_trigger_signatures", [])
for value in conversation.get("recent_trigger_signatures", [])
if isinstance(value, str)
]
new_messages, current_signatures, overlap, resynced = new_sequence_suffix(
previous,
messages,
recent_trigger_signatures=recent_trigger_signatures,
recover_unseen_trigger=self.no_overlap_recovery_ready,
recover_unseen_trigger=group in self.recovery_ready_conversations,
)
initialized = bool(self.state.get("initialized"))
initialized = bool(conversation.get("initialized"))
observed_at = observed_at or now_iso()
if not initialized:
new_messages = []
resynced = True
self.state["initialized"] = True
group = title_fingerprint(image)
conversation["initialized"] = True
current_trigger_signatures = [
message.signature
for message in messages
if trigger_question(message.text) is not None
if (
shadow_target.question(message.text, bot_mention=BOT_MENTION)
if shadow_target is not None
else trigger_question(message.text)
)
]
recent_trigger_signatures = (
recent_trigger_signatures + current_trigger_signatures
)[-MAX_RECENT_TRIGGER_SIGNATURES:]
self.recovery_ready_conversations.add(group)
self.no_overlap_recovery_ready = True
appended = 0
queued = 0
shadowed = 0
for ordinal, message in enumerate(new_messages):
if shadow_target is not None:
if self.build_shadow_event is None:
raise RuntimeError("shadow event builder is unavailable")
event = self.build_shadow_event(
shadow_target,
observed_at=observed_at,
text=message.text,
body_confidence=message.body_confidence,
visual_signature=message.signature,
bot_mention=BOT_MENTION,
observed_sender=message.sender,
sender_confidence=message.sender_confidence,
ordinal=ordinal,
)
shadowed += 1
else:
event = history_event(
message,
observed_at=observed_at,
@@ -1698,9 +1806,23 @@ class Recorder:
message.confidence,
bool(queued),
)
conversation.update(
{
"last_capture_at": observed_at,
"last_layout_hash": layout_hash,
"last_visual_hash": visual_hash or layout_hash,
"last_ocr_ms": round(elapsed * 1000),
"visible_signatures": current_signatures,
"recent_trigger_signatures": recent_trigger_signatures,
"message_signature_scheme": MESSAGE_SIGNATURE_SCHEME,
"visible_count": len(messages),
"last_overlap": overlap,
"last_resynced": resynced,
}
)
self.state.update(
{
"version": 1,
"version": 2,
"last_capture_at": observed_at,
"last_layout_hash": layout_hash,
"last_visual_hash": visual_hash or layout_hash,
@@ -1714,6 +1836,8 @@ class Recorder:
"last_resynced": resynced,
"events_total": int(self.state.get("events_total", 0)) + appended,
"queued_total": int(self.state.get("queued_total", 0)) + queued,
"shadowed_total": int(self.state.get("shadowed_total", 0))
+ shadowed,
"resync_total": int(self.state.get("resync_total", 0))
+ int(resynced and initialized),
"errors_total": int(self.state.get("errors_total", 0)),
@@ -1747,7 +1871,7 @@ class Recorder:
group_id=group,
count=appended,
depth=queued,
outcome="queued" if queued else "observed",
outcome="shadowed" if shadowed else "queued" if queued else "observed",
source="rapidocr",
)