Compare commits
14
Commits
ddebe075d7
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffd0b2a16 | ||
|
|
6375ab5c79 | ||
|
|
22f5240fa7 | ||
|
|
645a282b22 | ||
|
|
e4365841ed | ||
|
|
5a3c4fb94c | ||
|
|
5e309f0b8f | ||
|
|
ed2fd0200f | ||
|
|
8670745235 | ||
|
|
eb31446076 | ||
|
|
5300898bc3 | ||
|
|
ac630576c7 | ||
|
|
3a7c11e44d | ||
|
|
e6d6ddf1c6 |
+123
@@ -1,5 +1,128 @@
|
||||
# Changelog
|
||||
|
||||
## beta 2.0.5.1023 - 2026-10-23 17:16:12
|
||||
|
||||
- 21 点胜率显示修复完成:`/摩拉` 里的 21 点战绩现在会和 `/21点 胜率` 使用同一套统计口径。
|
||||
- 修复 `/摩拉` 误读旧版通用胜负字段的问题,避免旧抽奖等历史数据污染 21 点胜率展示。
|
||||
- 超时认输、主动认输、单人结算和多人牌桌仍统一写入 `blackjack_stats`,本次只调整展示读取来源。
|
||||
|
||||
## beta 2.0.4.1023 - 2026-10-23 16:56:33
|
||||
|
||||
- 许愿池和 21 点小版本更新完成:`/抽奖` 已并入许愿池,21 点新增群内胜率统计并调整下注上限。
|
||||
- 旧 `/抽奖` 返倍玩法合并进许愿池,保留 `/抽奖 金额` 与 `/draw 金额` 入口,使用方式与 `/许愿池 金额` 一致。
|
||||
- 许愿池每 1000 摩拉结算 1 愿,单次支持 1000~10000 摩拉,可能获得摩拉回响、扫码概率券、Taffy 发话额度或原石。
|
||||
- 许愿池新增公共池水、8 愿无道具保底和池底大愿;游戏记录和 Taffy 摩拉统计会按“许愿池”归档。
|
||||
- 21 点单人下注范围调整为 1~2000,多人牌桌下注范围调整为 1~2000;`/21点 胜率` 会展示当前群玩家和 Taffy 的胜/负/平、胜率与净收支。
|
||||
|
||||
## beta 2.0.3.1023 - 2026-10-23 16:09:38
|
||||
|
||||
- 冷门知识问答小版本更新完成:题库外置为 500 题,并会优先抽取更少出现过的题。
|
||||
- 知识问答基础题库迁移到 `jwebot_kernel/data/trivia_questions.json`,运行时启动校验并展开为 500 道选项题。
|
||||
- 题库补充更多机制、数值、版本与活动细节题,覆盖反恐精英、顶点英雄、守望先锋、英雄联盟、少女乐团派对、韩流、原神、缪斯学园偶像和我的世界。
|
||||
- 出题历史新增 `ask_counts`,每次选题优先从当前主题内出现次数最低的基础题中抽取,并继续避开最近题目。
|
||||
- 问答题头新增 `出现次数:N` 标志,复看当前题时也能看到这题已经出现过几次。
|
||||
|
||||
## beta 2.0.2.1021 - 2026-10-21 18:11:35
|
||||
|
||||
- 上下文追问小版本更新完成:Taffy 现在会优先相信本地游戏状态,并能把短句追评接到上一条 21 点或主动发言。
|
||||
- 普通聊天入口新增本地游戏事实抢答:命中“某人在玩什么/现在咋样”等问题时,直接读取本地账本回复,不再排队生成可能互相矛盾的第二条闲聊最终回复。
|
||||
- 新增群级 `active_threads` 线程锚点,Taffy 本地命令结果、聊天最终回复和主动广播都会短时记录为当前话题,供“还真是”“胆子大”等短追评优先引用。
|
||||
- 上下文打分增强 `active_thread` 标记,并在系统提示中要求模型优先采用 `local_result`/`active_thread` 本地事实,只有多个候选冲突时才追问。
|
||||
- 单人和多人 21 点状态现在可被本地摘要复用,回答会包含当前牌局、手牌点数、下注、阶段和轮到谁操作等关键信息。
|
||||
|
||||
## beta 2.0.1.1021 - 2026-10-21 17:32:46
|
||||
|
||||
- 上下文管理小版本更新完成:Taffy 现在会按发送者、点名对象和游戏结果结构化选取上下文,低置信身份不再写入记忆或执行绑定命令。
|
||||
- 聊天生成改用 `STRUCTURED_RECENT_GROUP_CONTEXT`,历史片段带有
|
||||
speaker、route、reason、score 和 tags,避免把不同群友的消息当成同一个人连续发言。
|
||||
- 上下文候选扩容并加入本地相关性打分:同发送者、被点名人物、词面重叠、
|
||||
本地游戏/命令结果和追问语义会被优先保留;自指问题默认不混入其他人的历史。
|
||||
- Edge 事件和本地上下文历史新增 sender/body confidence、source 和 observed_at
|
||||
持久化;低置信 sender 只保留文本证据,不绑定人物记忆、账户或奖励类命令。
|
||||
- Taffy 的可见回复和已送达主动广播会写入上下文历史,后续追问能引用;
|
||||
主动冒泡仍跳过 Taffy 自己的话,避免自我循环。
|
||||
|
||||
## 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
|
||||
|
||||
- 冷门知识问答题库整体重写:改为更偏游戏内机制、数值、版本和活动细节的深度题。
|
||||
- Trivia display now allows necessary proper nouns while preferring
|
||||
Chinese-first names such as 中文(English) when a common translation exists.
|
||||
|
||||
## beta 1.2.7.1019 - 2026-10-19 22:37:40
|
||||
|
||||
- 冷门知识问答新增缪斯学园偶像和我的世界题库,并支持 LoveLive 与 Minecraft 主题别名。
|
||||
- LoveLive trivia is limited to μ's-only facts, while displayed trivia content
|
||||
remains localized in Chinese.
|
||||
|
||||
## beta 1.2.6.1019 - 2026-10-19 22:26:26
|
||||
|
||||
- 冷门知识问答新增少女乐团派对、韩流和原神题库,并支持 `/知识问答 主题` 定向开题。
|
||||
- Unknown trivia topics now return the supported topic list, while automatic
|
||||
quiz broadcasts still draw from the full trivia bank.
|
||||
|
||||
## beta 1.2.5.1019 - 2026-10-19 22:12:52
|
||||
|
||||
- 冷门知识问答题库改为中文展示:题干、选项和解释不再混用英文词。
|
||||
- Keep trivia answer letters unchanged while localizing displayed trivia
|
||||
domains, prompts, choices, and explanations.
|
||||
|
||||
## beta 1.2.4.1019 - 2026-10-19 18:15:01
|
||||
|
||||
- 增强 Taffy 对本地游戏结果的理解:闲聊上下文扩大到 25 轮,并明确标注本地命令结果的发送者和类型。
|
||||
- Format local command history as tagged context entries so follow-up phrases
|
||||
like "这一把" can link back to recent blackjack and other game results.
|
||||
|
||||
## beta 1.2.3.1019 - 2026-10-19 18:04:57
|
||||
|
||||
- 扩大 Taffy 闲聊可读取的上下文窗口:后续回复现在最多参考最近 15 轮相关对话。
|
||||
- Increase the default direct-chat context window from 6 to 15 retained turns.
|
||||
|
||||
## beta 1.2.2.1019 - 2026-10-19 17:31:56
|
||||
|
||||
- 修复本地命令后的闲聊断上下文,新增 `/知识问答` 强制开题,并让多条回复按 0.5~1 秒随机间隔逐条发送。
|
||||
- Include non-tool local command results in the follow-up chat context so game
|
||||
and account actions like `/原石祈愿` can be referenced naturally afterward.
|
||||
- Exclude utility local commands such as `/version`, `/状态`, `/debug`, and
|
||||
feature-list output from chat context to avoid noisy system details.
|
||||
- Add randomized spacing between queued multi-part replies and immediate local
|
||||
multi-reply sends.
|
||||
- Add `/知识问答` as a Kernel-owned local command that starts a cold-knowledge
|
||||
quiz immediately while reusing the active five-minute question when one is
|
||||
already running.
|
||||
|
||||
## beta 1.2.1.1019 - 2026-10-19 17:23:58
|
||||
|
||||
- 修复 Taffy 短句拆分只换行不分条的问题:多句闲聊现在会按多条消息逐条发送。
|
||||
- Encode multi-sentence chat replies with the existing multi-reply marker so
|
||||
direct and cloud chat results enter the multi-message delivery path.
|
||||
- Split queued delivery helper calls per message part, giving each part a
|
||||
stable per-part event id while preserving structured local command output.
|
||||
|
||||
## beta 1.2.0.1019 - 2026-10-19 17:05:47
|
||||
|
||||
- 优化 Taffy 群聊语气:回复更短、更像群友,减少 AI 式自我解释,并保留主动插话、冷门问答和更新状态提示。
|
||||
- Add a chat-specific reply style layer that removes common AI-like padding,
|
||||
keeps direct unknown-subject clarification, and splits long casual replies
|
||||
into shorter lines.
|
||||
- Apply the style rule to direct chat and cloud-agent replies while leaving
|
||||
structured local command output unchanged.
|
||||
- This 1.2 release grants each existing account both 200 Mora and
|
||||
1 primogem.
|
||||
|
||||
## beta 1.1.2.1019 - 2026-10-19 16:38:10
|
||||
|
||||
- 新增低频主动插话、随机冷门知识问答和更新生命周期提示:Taffy 会偶尔自然冒泡,抛出限时答题奖励摩拉,并在停机、上线和更新就绪时主动说明状态。
|
||||
|
||||
@@ -8,12 +8,13 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||
并通过带视觉确认的 GUI 发送链路回复。
|
||||
|
||||
当前版本:`beta 1.1.2.1019`,更新时间:2026-10-19 16:38:10。
|
||||
当前版本:`beta 2.0.5.1023`,更新时间:2026-10-23 17:16:12。
|
||||
|
||||
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
|
||||
Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major
|
||||
自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则
|
||||
已经外置为包内 JSON,虚拟摩拉支持转账、贷款、还款和群内排行榜。
|
||||
已经外置为包内 JSON,知识问答题库也以 500 题 JSON 数据随 Kernel
|
||||
发布,虚拟摩拉支持转账、贷款、还款和群内排行榜。
|
||||
|
||||
完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见
|
||||
[CHANGELOG](CHANGELOG.md)。
|
||||
@@ -82,14 +83,15 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
/功能列表
|
||||
/上下文 [100|200|300](/context)
|
||||
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
||||
/摩拉(兼容 /金币、/wallet);/抽奖 [金额](/draw)
|
||||
/摩拉(兼容 /金币、/wallet);/许愿池 金额(兼 /许愿、/抽奖、/draw)
|
||||
/大富翁榜(/摩拉排行,兼容 /金币排行)
|
||||
/转账 对方 金额(/transfer name amount)
|
||||
/贷款 [金额](/loan amount);/还款 [金额|全部](/repay)
|
||||
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
||||
/21点 开始 [1~500];开局后 /21点 要牌|停牌|状态|结束
|
||||
/21点 开始 [1~2000];开局后 /21点 要牌|停牌|状态|结束;/21点 胜率
|
||||
/21点 多人 启动;/21点 加入;/21点 多人 开始;/21点 放200
|
||||
/摩拉(兼 /原石);/原石祈愿;/原石榜;/扫码加权 人名(24小时+5);/Taffy发话 内容
|
||||
/知识问答 [主题];500 题冷门题库,优先抽取少出现过的题并显示出现次数;支持主题:反恐精英、顶点英雄、守望先锋、英雄联盟、少女乐团派对、韩流、原神、缪斯学园偶像、我的世界;/回答 A
|
||||
/游戏记录 [页码](/gamehistory)
|
||||
/游戏记录 [页码](/gamehistory)
|
||||
/游戏帮助(/games)
|
||||
@@ -192,9 +194,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 服务保留安装,但保持禁用。
|
||||
|
||||
### 仓库结构
|
||||
@@ -269,16 +272,17 @@ visually confirmed GUI bridge.
|
||||
Current version:
|
||||
|
||||
```text
|
||||
beta 1.1.2.1019
|
||||
beta 2.0.5.1023
|
||||
```
|
||||
|
||||
Updated: 2026-10-19 16:38:10.
|
||||
Updated: 2026-10-23 17:16:12.
|
||||
|
||||
The 1.0 Beta baseline consolidates the stable local command set, person and
|
||||
context indexes, Steam presence/playtime queries, virtual coin games,
|
||||
multiplayer lottery, blackjack, CS Major autochess, and production deployment
|
||||
gates. CS Major autochess game content is now packaged as JSON data, and
|
||||
virtual coins support transfer, loans, repayment, and group leaderboards.
|
||||
gates. CS Major autochess game content and the 500-question trivia bank are
|
||||
now packaged as JSON data, and virtual coins support transfer, loans,
|
||||
repayment, and group leaderboards.
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
@@ -459,10 +463,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
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ user_unit_source_dir="$source_dir/deploy/systemd/user"
|
||||
config_source_dir="$source_dir/packages/jwebot-config/src/jwebot_config"
|
||||
sdk_source_dir="$source_dir/packages/jwebot-sdk/src/jwebot_sdk"
|
||||
kernel_source_dir="$source_dir/packages/jwebot-kernel/src/jwebot_kernel"
|
||||
kernel_data_source_dir="$kernel_source_dir/data"
|
||||
official_source_dir="$source_dir/packages/jwebot-official-extensions/src/jwebot_official_extensions"
|
||||
official_adapter_source_dir="$official_source_dir/adapters"
|
||||
official_autochess_data_source_dir="$official_source_dir/data/cs_autochess"
|
||||
@@ -358,6 +359,7 @@ install -d -m 0700 \
|
||||
"$target_dir/jwebot_config" \
|
||||
"$target_dir/jwebot_sdk" \
|
||||
"$target_dir/jwebot_kernel" \
|
||||
"$target_dir/jwebot_kernel/data" \
|
||||
"$target_dir/jwebot_official_extensions" \
|
||||
"$target_dir/jwebot_official_extensions/adapters" \
|
||||
"$target_dir/jwebot_official_extensions/data" \
|
||||
@@ -371,6 +373,10 @@ done
|
||||
for path in "$kernel_source_dir"/*.py; do
|
||||
install -m 0600 "$path" "$target_dir/jwebot_kernel/$(basename "$path")"
|
||||
done
|
||||
for path in "$kernel_data_source_dir"/*.json; do
|
||||
[ -e "$path" ] || continue
|
||||
install -m 0600 "$path" "$target_dir/jwebot_kernel/data/$(basename "$path")"
|
||||
done
|
||||
for path in "$official_source_dir"/*.py; do
|
||||
install -m 0600 "$path" "$target_dir/jwebot_official_extensions/$(basename "$path")"
|
||||
done
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# JWeBot Command Catalog
|
||||
|
||||
Current baseline: `beta 1.1.2.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
|
||||
@@ -51,22 +51,24 @@ descriptor inventory; plugin code delegates to reviewed service profiles.
|
||||
| `/扫码加权 人名` | `/扫码概率 人名` | Spend a scan-boost ticket or 1 primogem to give that person +5 hourly scan weight for 24 hours. |
|
||||
| `/Taffy发话 内容` | `/taffysay content` | Spend Taffy speech credit; 1 primogem converts to 2 credits when needed. |
|
||||
| `/Taffy发话 定时 HH:MM 内容` | - | Schedule a Taffy speech through the guarded clock delivery queue. |
|
||||
| `/知识问答 [主题]` | - | Start a five-minute cold-knowledge quiz immediately, optionally limited to one supported topic. Supported topics: 反恐精英, 顶点英雄, 守望先锋, 英雄联盟, 少女乐团派对, 韩流, 原神, 缪斯学园偶像, 我的世界. English aliases like `LoveLive` and `Minecraft` are accepted as input. |
|
||||
| `/回答 A` | `/answer A` | Answer the active five-minute cold-knowledge quiz for a rarity-based Mora reward. |
|
||||
| `/抽奖 [金额]` | `/draw` | Play a single-player prize draw. |
|
||||
| `/许愿池 金额` | `/许愿`, `/抽奖`, `/draw` | Put 1000-10000 Mora into the wish pool in 1000-Mora units for Mora echoes, scan-boost tickets, Taffy speech credits, primogems, pool jackpot, and pity progress. |
|
||||
| `/彩票 开始` | `/lottery start` | Open a multiplayer lottery round. |
|
||||
| `/彩票 报名 数字 金额` | `/lottery join` | Join or update a lottery entry. |
|
||||
| `/彩票 状态` | `/lottery status` | Show the current lottery round. |
|
||||
| `/彩票 开奖` | `/lottery draw` | Host-only draw for the current lottery round. |
|
||||
| `/彩票 取消` | `/lottery cancel` | Host-only cancellation with refunds. |
|
||||
| `/21点 开始 [1-500]` | `/blackjack start` | Start a blackjack round with the chosen stake. |
|
||||
| `/21点 开始 [1-2000]` | `/blackjack start` | Start a blackjack round with the chosen stake. |
|
||||
| `/21点 要牌` | `/blackjack hit` | Draw one blackjack card. |
|
||||
| `/21点 停牌` | `/blackjack stand` | Stand and settle against the dealer. |
|
||||
| `/21点 状态` | `/blackjack status` | Show the current blackjack round. |
|
||||
| `/21点 结束` | `/blackjack end` | End the current blackjack round. |
|
||||
| `/21点 胜率` | `/blackjack stats` | Show every player and Taffy's group-scoped win rate and net Mora result. |
|
||||
| `/21点 多人 启动` | - | Open a Taffy-hosted blackjack table and auto-seat the host. |
|
||||
| `/21点 加入` | - | Join the current blackjack table before betting starts. |
|
||||
| `/21点 多人 开始` | - | Host-only close joining and start ordered betting prompts. |
|
||||
| `/21点 放金额` | - | Place the prompted table stake, 1-10000 coins. |
|
||||
| `/21点 放金额` | - | Place the prompted table stake, 1-2000 Mora. |
|
||||
| `/游戏记录 [页码]` | `/gamehistory` | Show indexed personal game history. |
|
||||
| `/游戏帮助` | `/games` | Show local game rules. |
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
## 个人局号与记录
|
||||
|
||||
抽奖、彩票和 21 点分别维护本人的游戏局号。第一次参与某种游戏显示
|
||||
许愿池、彩票和 21 点分别维护本人的游戏局号。第一次参与某种游戏显示
|
||||
`Jacky #1:...`,下一把显示 `Jacky #2:...`。配置别名解析到同一人物时,
|
||||
共用同一账户、局号和历史。
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
`/gamehistory [page]` 查看最近记录,每页五条。记录包含游戏、个人局号、
|
||||
结果、下注、返还和净变化,最多保留最近 50 条。
|
||||
|
||||
21 点下注范围同步调整为 1~500。`/21点 开始 100` 只表示本局下注 100;
|
||||
21 点下注范围现为 1~2000。`/21点 开始 100` 只表示本局下注 100;
|
||||
发牌后再单独发送 `/21点 要牌` 或 `/21点 停牌`。
|
||||
|
||||
## 持久化与并发
|
||||
|
||||
@@ -11,7 +11,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,
|
||||
| coin-ranking | `/大富翁榜`、`/摩拉排行`(兼容 `/金币排行`) | `/richlist` |
|
||||
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
|
||||
| coin-loan | `/贷款 [金额]`、`/还款 [金额\|全部]` | `/loan amount`、`/repay [amount\|all]` |
|
||||
| single-player-games | `/抽奖 [金额]` | `/draw [金额]` |
|
||||
| single-player-games | `/许愿池 金额`(兼容 `/许愿`、`/抽奖`) | `/draw 金额` |
|
||||
| single-player-games | `/彩票 开始/报名/当前状态/开奖/取消` | `/lottery start/join/status/draw/cancel` |
|
||||
| single-player-games | `/21点 开始 [金额]`(空格可省略) | `/blackjack start [金额]`(空格可省略) |
|
||||
| single-player-games | `/21点 要牌/停牌/状态/结束` | `/blackjack hit/stand/status/end` |
|
||||
@@ -60,8 +60,9 @@ person_key。相同人物的已配置别名共用摩拉和牌局,其他群使
|
||||
## 虚拟摩拉与游戏管理
|
||||
|
||||
每个群内人物首次使用账户时一次性获得 1000 虚拟摩拉。再次查询、
|
||||
重启服务或切换已配置别名都不会重复领取。抽奖下注为 2~100,彩票和
|
||||
21 点下注为 1~500;余额不足或参数非法不扣款。
|
||||
重启服务或切换已配置别名都不会重复领取。许愿池投入为 1000~10000,
|
||||
按 1000 摩拉递增;彩票下注为 1~500,21 点下注为 1~2000;
|
||||
余额不足或参数非法不扣款。
|
||||
|
||||
`/转账 对方 金额` 会把本人的虚拟摩拉转给同群已初始化账户,英文为
|
||||
`/transfer name amount`。金额范围 1~100000;不允许给自己转账。
|
||||
@@ -76,12 +77,14 @@ person_key。相同人物的已配置别名共用摩拉和牌局,其他群使
|
||||
调用者账户。榜单按当前摩拉余额降序排列,人数范围 1~20;账户存在
|
||||
未还贷款时会在名字后标注贷款本金。
|
||||
|
||||
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
||||
`/许愿池 金额` 会把摩拉投入公共许愿池,每 1000 摩拉结算 1 愿;
|
||||
`/许愿`、`/抽奖` 和 `/draw` 是同一功能入口。许愿可能获得摩拉回响、
|
||||
扫码概率券、Taffy 发话额度、原石或池底大愿,8 愿没有道具类奖励后触发保底。
|
||||
alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数字顺序无关,
|
||||
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
||||
[多人彩票与游戏记录](group-lottery-0.6.3.md)。
|
||||
|
||||
21 点默认下注 50、范围 1~500。`/21点 开始 100` 表示拿 100 摩拉作为本局下注进场,
|
||||
21 点默认下注 50、范围 1~2000。`/21点 开始 100` 表示拿 100 摩拉作为本局下注进场,
|
||||
等价的 OCR 紧凑形式是 `/21点开始100`。开局会扣除下注并发两张牌,
|
||||
然后根据牌面单独发送 `/21点 要牌` 或 `/21点 停牌`;开局与操作写在
|
||||
同一条消息时不会扣款,而会返回分步提示。牌局使用洗牌后的单副 52 张牌,
|
||||
@@ -93,12 +96,15 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数
|
||||
|
||||
多人 21 点使用 `/21点 多人 启动` 创建 Taffy 牌桌,开桌者自动入座。
|
||||
其他玩家在下注前用 `/21点 加入` 进入当前牌桌;主持人发送 `/21点 多人 开始`
|
||||
后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~10000。
|
||||
后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~2000。
|
||||
所有玩家下注后统一发牌,轮到本人时用 `/21点 要牌` 或 `/21点 停牌` 操作;
|
||||
结算按座位顺序列出每个人的下注、返还、净变化和当前摩拉。
|
||||
自然 21 点立即结算,返还 2.5 倍下注向下取整;普通胜利返还两倍,
|
||||
平局返还下注,失败不返还。每人最多一局;牌局进行中不接受另一笔
|
||||
抽奖或 21 点开局,多人彩票报名仍可独立进行。
|
||||
许愿池或 21 点开局,多人彩票报名仍可独立进行。
|
||||
|
||||
`/21点 胜率` 会列出当前群所有有已结算记录的玩家,以及 Taffy 的
|
||||
胜/负/平、胜率和累计净赚净亏;主动认输和超时计负,牌桌取消不计入统计。
|
||||
|
||||
十分钟无有效游戏操作的牌局在下一次账户操作时按认输结束;查询状态
|
||||
不延长有效期。主动结束同样按认输处理,不退下注。其他人可以各自
|
||||
|
||||
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
@@ -6,7 +6,7 @@ Author: `jwebot`
|
||||
|
||||
Help:
|
||||
|
||||
- `/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助`
|
||||
- `/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助`
|
||||
|
||||
Registered commands:
|
||||
|
||||
@@ -14,6 +14,8 @@ Registered commands:
|
||||
- `/金币`
|
||||
- `/wallet`
|
||||
- `/原石`
|
||||
- `/许愿池`
|
||||
- `/许愿`
|
||||
- `/抽奖`
|
||||
- `/draw`
|
||||
- `/彩票`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
id = "single-player-games"
|
||||
author = "jwebot"
|
||||
description = "摩拉游戏"
|
||||
help = ["/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助"]
|
||||
help = ["/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助"]
|
||||
version = "1.0.0"
|
||||
api_version = "1"
|
||||
entrypoint = "single_player_games:plugin"
|
||||
type = "command"
|
||||
|
||||
[capabilities]
|
||||
commands = ["/摩拉", "/金币", "/wallet", "/原石", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏帮助", "/games"]
|
||||
commands = ["/摩拉", "/金币", "/wallet", "/原石", "/许愿池", "/许愿", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏帮助", "/games"]
|
||||
storage = false
|
||||
network_profiles = []
|
||||
service_profiles = ["single-player-games"]
|
||||
|
||||
@@ -1,45 +1,18 @@
|
||||
"""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'
|
||||
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(
|
||||
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
|
||||
HELP_ENTRIES = ('/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助',)
|
||||
COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/许愿池', '/许愿', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
@@ -20,9 +20,9 @@ class PluginTests(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(AUTHOR, 'jwebot')
|
||||
self.assertEqual(DESCRIPTION, '摩拉游戏')
|
||||
self.assertEqual(HELP_ENTRIES, ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',))
|
||||
self.assertEqual(HELP_ENTRIES, ('/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助',))
|
||||
self.assertIn('/摩拉', registered)
|
||||
self.assertEqual(len(registered), 14)
|
||||
self.assertEqual(len(registered), 16)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+32
-14
@@ -32,9 +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
|
||||
@@ -126,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 的支持与理解,祝大家游戏愉快!"
|
||||
@@ -144,10 +150,16 @@ def build_announcement(release: str, summary: str) -> str:
|
||||
|
||||
def build_lifecycle_message(release: str, stage: str) -> str:
|
||||
if stage == "shutdown":
|
||||
message = (
|
||||
f"Taffy 准备停机更新到 {release},会短暂离线一下;"
|
||||
"别慌,我很快回来喵"
|
||||
)
|
||||
if release == "beta 2.0.0.1021":
|
||||
message = (
|
||||
"Taffy 准备停机更新,本次更新是架构级大版本更新,"
|
||||
f"预期版本{release},离线的时候保持耐心迎接Taffy的归来哦~"
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
f"Taffy 准备停机更新到 {release},会短暂离线一下;"
|
||||
"别慌,我很快回来喵"
|
||||
)
|
||||
elif stage == "startup":
|
||||
message = (
|
||||
f"Taffy 刚上线,正在做 {release} 的最后自检;"
|
||||
@@ -164,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))
|
||||
|
||||
@@ -195,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)
|
||||
@@ -203,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)
|
||||
):
|
||||
@@ -286,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} 版本更新补偿"
|
||||
@@ -301,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(
|
||||
@@ -334,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": (
|
||||
@@ -593,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),
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-config"
|
||||
version = "1.1.2b1+1019"
|
||||
version = "2.0.5b1+1023"
|
||||
description = "Profile-based configuration compiler for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
|
||||
@@ -36,4 +36,4 @@ __all__ = [
|
||||
"write_default_profile",
|
||||
]
|
||||
|
||||
__version__ = "1.1.2b1+1019"
|
||||
__version__ = "2.0.5b1+1023"
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-extension-host"
|
||||
version = "1.1.2b1+1019"
|
||||
version = "2.0.5b1+1023"
|
||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==1.1.2b1+1019",
|
||||
"jwebot-kernel==1.1.2b1+1019",
|
||||
"jwebot-sdk==2.0.5b1+1023",
|
||||
"jwebot-kernel==2.0.5b1+1023",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -16,4 +16,4 @@ __all__ = [
|
||||
"process_request",
|
||||
]
|
||||
|
||||
__version__ = "1.1.2b1+1019"
|
||||
__version__ = "2.0.5b1+1023"
|
||||
|
||||
@@ -4,15 +4,18 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-kernel"
|
||||
version = "1.1.2b1+1019"
|
||||
version = "2.0.5b1+1023"
|
||||
description = "Stable routing and extension authority for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-config==1.1.2b1+1019",
|
||||
"jwebot-sdk==1.1.2b1+1019",
|
||||
"jwebot-official-extensions==1.1.2b1+1019",
|
||||
"jwebot-config==2.0.5b1+1023",
|
||||
"jwebot-sdk==2.0.5b1+1023",
|
||||
"jwebot-official-extensions==2.0.5b1+1023",
|
||||
"tomli>=2; python_version < '3.11'",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
jwebot_kernel = ["data/*.json"]
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "1.1.2b1+1019"
|
||||
__version__ = "2.0.5b1+1023"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "1.1.2b1+1019"
|
||||
version = "2.0.5b1+1023"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==1.1.2b1+1019",
|
||||
"jwebot-sdk==2.0.5b1+1023",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -90,4 +90,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "1.1.2b1+1019"
|
||||
__version__ = "2.0.5b1+1023"
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+8
-35
@@ -1,45 +1,18 @@
|
||||
"""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'
|
||||
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(
|
||||
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
|
||||
HELP_ENTRIES = ('/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助',)
|
||||
COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/许愿池', '/许愿', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
+6
-33
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
|
||||
@@ -63,6 +63,7 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"扫码概率": "scan_boost",
|
||||
"Taffy发话": "taffy_say",
|
||||
"taffysay": "taffy_say",
|
||||
"知识问答": "trivia_start",
|
||||
"回答": "trivia_answer",
|
||||
"answer": "trivia_answer",
|
||||
"转账": "transfer",
|
||||
@@ -73,6 +74,8 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"borrow": "loan",
|
||||
"还款": "repay",
|
||||
"repay": "repay",
|
||||
"许愿池": "prize_draw",
|
||||
"许愿": "prize_draw",
|
||||
"抽奖": "prize_draw",
|
||||
"draw": "prize_draw",
|
||||
"彩票": "lottery",
|
||||
@@ -151,6 +154,11 @@ DIRECT_KERNEL_COMMANDS = (
|
||||
help_entries=("/余额",),
|
||||
command_kinds=("deepseek_balance",),
|
||||
),
|
||||
KernelCommandDescriptor(
|
||||
description="冷门问答",
|
||||
help_entries=("/知识问答 [主题]",),
|
||||
command_kinds=("trivia_start",),
|
||||
),
|
||||
)
|
||||
DIRECT_KERNEL_COMMAND_KINDS = frozenset(
|
||||
kind
|
||||
@@ -171,7 +179,7 @@ LOCAL_EXTENSIONS = (
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="摩拉游戏",
|
||||
help_entries=(
|
||||
"/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助",
|
||||
"/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助",
|
||||
),
|
||||
command_kinds=(
|
||||
"wallet",
|
||||
|
||||
+406
-56
@@ -9,15 +9,21 @@ from typing import Any
|
||||
|
||||
|
||||
INITIAL_COINS = 1000
|
||||
MIN_DRAW_BET = 2
|
||||
MAX_DRAW_BET = 100
|
||||
WISH_POOL_GAME_KEY = "wish_pool"
|
||||
WISH_POOL_UNIT = 1000
|
||||
MIN_WISH_POOL_AMOUNT = 1000
|
||||
MAX_WISH_POOL_AMOUNT = 10_000
|
||||
WISH_POOL_CONTRIBUTION = 200
|
||||
WISH_POOL_PITY_LIMIT = 8
|
||||
WISH_POOL_JACKPOT_PERCENT = 20
|
||||
WISH_POOL_JACKPOT_CAP = 20_000
|
||||
MIN_LOTTERY_BET = 1
|
||||
MAX_LOTTERY_BET = 500
|
||||
DEFAULT_LOTTERY_BET = 10
|
||||
MIN_BLACKJACK_BET = 1
|
||||
MAX_BLACKJACK_BET = 500
|
||||
MAX_BLACKJACK_BET = 2_000
|
||||
MIN_BLACKJACK_TABLE_BET = 1
|
||||
MAX_BLACKJACK_TABLE_BET = 10_000
|
||||
MAX_BLACKJACK_TABLE_BET = 2_000
|
||||
MAX_BLACKJACK_TABLE_PLAYERS = 8
|
||||
MIN_TRANSFER_AMOUNT = 1
|
||||
MAX_TRANSFER_AMOUNT = 100_000
|
||||
@@ -51,7 +57,7 @@ WISH_MORA_TABLE = (
|
||||
)
|
||||
WISH_SCAN_TICKET_PERCENT = 10
|
||||
GAME_LABELS = {
|
||||
"prize_draw": "抽奖",
|
||||
"prize_draw": "许愿池",
|
||||
"lottery": "彩票",
|
||||
"blackjack": "21点",
|
||||
}
|
||||
@@ -66,14 +72,14 @@ BLACKJACK_ACTIONS = {
|
||||
"end": "end",
|
||||
}
|
||||
BLACKJACK_START = re.compile(
|
||||
r"^(?:开始|start)(?P<bet>\d{1,3})?$"
|
||||
r"^(?:开始|start)(?P<bet>\d{1,4})?$"
|
||||
)
|
||||
BLACKJACK_COMBINED_ACTION = re.compile(
|
||||
r"^(?:开始|start)(?P<bet>\d{1,3})?"
|
||||
r"^(?:开始|start)(?P<bet>\d{1,4})?"
|
||||
r"(?:要牌|hit|停牌|stand)$"
|
||||
)
|
||||
BLACKJACK_TABLE_BET = re.compile(
|
||||
r"^(?:放|下注|bet)(?P<bet>\d{1,5})$",
|
||||
r"^(?:放|下注|bet)(?P<bet>\d{1,4})$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
TRANSFER_MARKED = re.compile(
|
||||
@@ -232,6 +238,19 @@ def recent_primogem_event_lines(
|
||||
return lines
|
||||
|
||||
|
||||
def blackjack_outcome_stat(outcome: object) -> str | None:
|
||||
text = str(outcome or "")
|
||||
if not text or "取消" in text:
|
||||
return None
|
||||
if "平" in text:
|
||||
return "ties"
|
||||
if any(token in text for token in ("输", "认输", "失败", "爆")):
|
||||
return "losses"
|
||||
if "赢" in text or "获胜" in text:
|
||||
return "wins"
|
||||
return None
|
||||
|
||||
|
||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
account.setdefault("balance", INITIAL_COINS)
|
||||
account.setdefault("game", None)
|
||||
@@ -245,6 +264,12 @@ def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
account["scan_boost_tickets"] = (
|
||||
scan_tickets if type(scan_tickets) is int and scan_tickets >= 0 else 0
|
||||
)
|
||||
wish_pool_pity = account.get("wish_pool_pity", 0)
|
||||
account["wish_pool_pity"] = (
|
||||
wish_pool_pity
|
||||
if type(wish_pool_pity) is int and wish_pool_pity >= 0
|
||||
else 0
|
||||
)
|
||||
blackjack_streak = account.get("blackjack_taffy_win_streak", 0)
|
||||
account["blackjack_taffy_win_streak"] = (
|
||||
blackjack_streak
|
||||
@@ -289,6 +314,45 @@ def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
account["game_history"] = [
|
||||
entry for entry in history if isinstance(entry, dict)
|
||||
][-GAME_HISTORY_LIMIT:]
|
||||
blackjack_stats = account.get("blackjack_stats")
|
||||
if not isinstance(blackjack_stats, dict):
|
||||
blackjack_stats = {
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"ties": 0,
|
||||
"stake": 0,
|
||||
"payout": 0,
|
||||
}
|
||||
for entry in account["game_history"]:
|
||||
if str(entry.get("kind", "")) != "blackjack":
|
||||
continue
|
||||
stat = blackjack_outcome_stat(entry.get("outcome"))
|
||||
if stat is None:
|
||||
continue
|
||||
try:
|
||||
stake = int(entry.get("stake", 0))
|
||||
payout = int(entry.get("payout", 0))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
blackjack_stats[stat] += 1
|
||||
blackjack_stats["stake"] += max(0, stake)
|
||||
blackjack_stats["payout"] += max(0, payout)
|
||||
else:
|
||||
blackjack_stats = {
|
||||
key: (
|
||||
int(blackjack_stats.get(key, 0))
|
||||
if type(blackjack_stats.get(key, 0)) is int
|
||||
and int(blackjack_stats.get(key, 0)) >= 0
|
||||
else 0
|
||||
)
|
||||
for key in ("wins", "losses", "ties", "stake", "payout")
|
||||
}
|
||||
blackjack_stats["hands"] = (
|
||||
blackjack_stats["wins"]
|
||||
+ blackjack_stats["losses"]
|
||||
+ blackjack_stats["ties"]
|
||||
)
|
||||
account["blackjack_stats"] = blackjack_stats
|
||||
events = account.get("coin_events")
|
||||
if not isinstance(events, list):
|
||||
events = []
|
||||
@@ -364,6 +428,24 @@ def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
return account
|
||||
|
||||
|
||||
def record_blackjack_result(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
outcome: str,
|
||||
stake: int,
|
||||
payout: int,
|
||||
) -> None:
|
||||
stat = blackjack_outcome_stat(outcome)
|
||||
if stat is None:
|
||||
return
|
||||
normalize_player_account(account)
|
||||
stats = account["blackjack_stats"]
|
||||
stats[stat] += 1
|
||||
stats["hands"] += 1
|
||||
stats["stake"] += max(0, int(stake))
|
||||
stats["payout"] += max(0, int(payout))
|
||||
|
||||
|
||||
def next_game_index(account: dict[str, Any], kind: str) -> int:
|
||||
normalize_player_account(account)
|
||||
counts = account["game_counts"]
|
||||
@@ -525,6 +607,39 @@ class PlayerGames:
|
||||
self.display_label = display_label
|
||||
self.rng = random.SystemRandom()
|
||||
|
||||
@staticmethod
|
||||
def wish_pool_usage() -> str:
|
||||
return (
|
||||
"用法:/许愿池 1000~10000(兼容 /许愿、/抽奖、/draw);"
|
||||
"金额必须按1000递增,每1000摩拉算1愿"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def normalize_wish_pool_state(state: dict[str, Any]) -> dict[str, Any]:
|
||||
balance = state.get("balance", 0)
|
||||
state["balance"] = balance if type(balance) is int and balance >= 0 else 0
|
||||
total_wishes = state.get("total_wishes", 0)
|
||||
state["total_wishes"] = (
|
||||
total_wishes
|
||||
if type(total_wishes) is int and total_wishes >= 0
|
||||
else 0
|
||||
)
|
||||
return state
|
||||
|
||||
@staticmethod
|
||||
def parse_wish_pool_amount(arguments: str) -> int | None:
|
||||
value = arguments.strip()
|
||||
if not value:
|
||||
return 0
|
||||
match = re.fullmatch(
|
||||
r"(?P<amount>\d{1,5})(?:\s*(?:摩拉|金币|coin|coins))?",
|
||||
value,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if match is None:
|
||||
return None
|
||||
return int(match["amount"])
|
||||
|
||||
@staticmethod
|
||||
def rules() -> str:
|
||||
return (
|
||||
@@ -538,17 +653,20 @@ class PlayerGames:
|
||||
"(理论期望3600摩拉)并可能获得扫码概率券;"
|
||||
"/扫码加权 人名 在24小时内给TA整点扫码+5权重;"
|
||||
"/Taffy发话 内容 或 /Taffy发话 定时 HH:MM 内容。\n"
|
||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||
"许愿池:/许愿池 1000~10000(兼容 /许愿、/抽奖、/draw),"
|
||||
"每1000摩拉1愿,可能获得摩拉回响、扫码概率券、"
|
||||
"Taffy发话额度或原石,8愿无道具后触发保底。\n"
|
||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
||||
"命中1/2/3个返下注的50%/120%/300%。\n"
|
||||
"21点先用 /21点 开始 [1~500] 进场,金额是本局下注,默认50;空格可省略。\n"
|
||||
"21点先用 /21点 开始 [1~2000] 进场,金额是本局下注,默认50;空格可省略。\n"
|
||||
"看到牌后再单独发送 /21点 要牌|停牌|状态|结束"
|
||||
"(/blackjack hit|stand|status|end);"
|
||||
"/21点 胜率 查看本群所有玩家及Taffy的21点胜率和净收支;"
|
||||
"每人最多一局,10分钟无操作视为认输,结束不退下注。\n"
|
||||
"多人21点:/21点 多人 启动 创建牌桌;/21点 加入;"
|
||||
"主持人 /21点 多人 开始 后按提示 /21点 放200"
|
||||
"(1~10000);轮到自己 /21点 要牌|停牌。\n"
|
||||
"(1~2000);轮到自己 /21点 要牌|停牌。\n"
|
||||
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
|
||||
"以上均含本金;虚拟摩拉不能充值或兑换。"
|
||||
)
|
||||
@@ -582,10 +700,11 @@ class PlayerGames:
|
||||
if kind == "wallet" and arguments:
|
||||
return "用法:/摩拉(兼容 /金币、/wallet),查看本人的虚拟摩拉;/游戏帮助 查看规则"
|
||||
if kind == "prize_draw":
|
||||
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
||||
return "用法:/抽奖 [2~100摩拉](/draw),默认10摩拉"
|
||||
bet = int(arguments or 10)
|
||||
elif kind == "blackjack":
|
||||
return self.wish_pool(row, arguments)
|
||||
if kind == "blackjack":
|
||||
compacted = re.sub(r"\s+", "", arguments.casefold())
|
||||
if compacted in {"胜率", "战绩", "stats", "rate", "rates"}:
|
||||
return self.blackjack_win_rates(row)
|
||||
table_command = self.parse_blackjack_table_command(arguments)
|
||||
if table_command is not None:
|
||||
table_action, table_bet = table_command
|
||||
@@ -595,7 +714,7 @@ class PlayerGames:
|
||||
table_bet,
|
||||
arguments,
|
||||
)
|
||||
compacted = re.sub(r"\s+", "", arguments.casefold()) or "状态"
|
||||
compacted = compacted or "状态"
|
||||
start = BLACKJACK_START.fullmatch(compacted)
|
||||
if start is not None:
|
||||
action = "start"
|
||||
@@ -613,8 +732,9 @@ class PlayerGames:
|
||||
"/21点 要牌 或 /21点 停牌"
|
||||
)
|
||||
return (
|
||||
"用法:先 /21点 开始 [1~500](金额是本局下注,默认50);"
|
||||
"开局后再单独发送 /21点 要牌|停牌|状态|结束;空格可省略"
|
||||
"用法:先 /21点 开始 [1~2000](金额是本局下注,默认50);"
|
||||
"开局后再单独发送 /21点 要牌|停牌|状态|结束;"
|
||||
"/21点 胜率 查看群内战绩;空格可省略"
|
||||
)
|
||||
group = str(row["group_fingerprint"])
|
||||
if (
|
||||
@@ -640,14 +760,12 @@ class PlayerGames:
|
||||
)
|
||||
else:
|
||||
bet = 0
|
||||
if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET:
|
||||
return "抽奖下注必须为 2~100 的整数摩拉"
|
||||
if (
|
||||
kind == "blackjack"
|
||||
and action == "start"
|
||||
and not MIN_BLACKJACK_BET <= bet <= MAX_BLACKJACK_BET
|
||||
):
|
||||
return "21点下注必须为 1~500 的整数摩拉"
|
||||
return "21点下注必须为 1~2000 的整数摩拉"
|
||||
person = self.ledger.resolve_person(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
@@ -684,6 +802,12 @@ class PlayerGames:
|
||||
stake=int(game["bet"]),
|
||||
payout=0,
|
||||
)
|
||||
record_blackjack_result(
|
||||
account,
|
||||
outcome="超时认输",
|
||||
stake=int(game["bet"]),
|
||||
payout=0,
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
account["blackjack_taffy_win_streak"] = 0
|
||||
@@ -692,8 +816,12 @@ class PlayerGames:
|
||||
state = "进行中" if account.get("game") else "无进行中的牌局"
|
||||
loan = account.get("loan")
|
||||
loan_text = f"未还贷款 {loan['principal']};" if loan else "无贷款;"
|
||||
total = account["wins"] + account["losses"] + account["ties"]
|
||||
win_rate = (account["wins"] / total * 100) if total else 0.0
|
||||
blackjack_stats = account["blackjack_stats"]
|
||||
total = int(blackjack_stats["hands"])
|
||||
wins = int(blackjack_stats["wins"])
|
||||
losses = int(blackjack_stats["losses"])
|
||||
ties = int(blackjack_stats["ties"])
|
||||
win_rate = (wins / total * 100) if total else 0.0
|
||||
lines = [
|
||||
(
|
||||
f"{player_label}:{expired}当前摩拉 {account['balance']};"
|
||||
@@ -702,8 +830,8 @@ class PlayerGames:
|
||||
f"{loan_text}"
|
||||
),
|
||||
(
|
||||
f"21点 {state};已结算胜 {account['wins']} / "
|
||||
f"负 {account['losses']} / 平 {account['ties']},"
|
||||
f"21点 {state};已结算胜 {wins} / "
|
||||
f"负 {losses} / 平 {ties},"
|
||||
f"胜率 {win_rate:.2f}%。"
|
||||
),
|
||||
"最近10次账户变动:",
|
||||
@@ -743,39 +871,210 @@ class PlayerGames:
|
||||
index,
|
||||
)
|
||||
return player_reply(expired + self.show_hand(game), index)
|
||||
index = next_game_index(account, "prize_draw")
|
||||
roll = self.rng.randrange(100)
|
||||
payout = bet * (5 if roll < 5 else 2 if roll < 25 else 0)
|
||||
result = "大奖" if roll < 5 else "小奖" if roll < 25 else "未中奖"
|
||||
account["balance"] += payout
|
||||
account["wins" if payout > bet else "losses"] += 1
|
||||
append_game_record(
|
||||
account,
|
||||
kind="prize_draw",
|
||||
index=index,
|
||||
outcome=result,
|
||||
stake=bet,
|
||||
payout=payout,
|
||||
summary=f"抽奖结果 {result}",
|
||||
)
|
||||
append_game_coin_event(
|
||||
account,
|
||||
kind="prize_draw",
|
||||
outcome=result,
|
||||
stake=bet,
|
||||
payout=payout,
|
||||
)
|
||||
return player_reply(
|
||||
expired
|
||||
+ f"{result};下注 {bet},返还 {payout},"
|
||||
f"净变化 {payout - bet:+d};摩拉 {account['balance']}",
|
||||
index,
|
||||
)
|
||||
raise LocalCommandError("未知本地游戏指令")
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
str(row["group_fingerprint"]), str(row["sender"]),
|
||||
str(row["event_id"]), kind + ":" + arguments, execute)
|
||||
|
||||
def wish_pool(self, row: dict[str, Any], arguments: str) -> str:
|
||||
amount = self.parse_wish_pool_amount(arguments)
|
||||
if amount is None:
|
||||
return self.wish_pool_usage()
|
||||
if (
|
||||
amount != 0
|
||||
and (
|
||||
amount < MIN_WISH_POOL_AMOUNT
|
||||
or amount > MAX_WISH_POOL_AMOUNT
|
||||
or amount % WISH_POOL_UNIT != 0
|
||||
)
|
||||
):
|
||||
return (
|
||||
"许愿池投入必须为 1000~10000 的整数摩拉,"
|
||||
"且按 1000 递增"
|
||||
)
|
||||
group = str(row["group_fingerprint"])
|
||||
sender = str(row["sender"])
|
||||
event_id = str(row["event_id"])
|
||||
person = self.ledger.resolve_person(group, sender)
|
||||
player_label = self.display_label(person["display_name"])
|
||||
|
||||
def player_reply(text: str, index: int | None = None) -> str:
|
||||
suffix = f" #{index}" if index is not None else ""
|
||||
return f"{player_label}{suffix}:{text}"
|
||||
|
||||
def action(
|
||||
account: dict[str, Any],
|
||||
pool_state: dict[str, Any],
|
||||
) -> str:
|
||||
normalize_player_account(account)
|
||||
self.normalize_wish_pool_state(pool_state)
|
||||
if amount == 0:
|
||||
return (
|
||||
f"{self.wish_pool_usage()};当前池水 {pool_state['balance']};"
|
||||
f"你的保底 {account['wish_pool_pity']}/{WISH_POOL_PITY_LIMIT}"
|
||||
)
|
||||
if account.get("game"):
|
||||
return "你已有一局21点,请先 /21点 停牌 或 /21点 结束"
|
||||
if account["balance"] < amount:
|
||||
return f"摩拉不足:余额 {account['balance']},本次需要 {amount}"
|
||||
|
||||
wishes = amount // WISH_POOL_UNIT
|
||||
index = next_game_index(account, "prize_draw")
|
||||
account["balance"] -= amount
|
||||
pool_gain = 0
|
||||
mora_payout = 0
|
||||
jackpot_payout = 0
|
||||
scan_tickets = 0
|
||||
taffy_credits = 0
|
||||
primogems = 0
|
||||
dry_wishes = 0
|
||||
small_returns = 0
|
||||
medium_returns = 0
|
||||
big_returns = 0
|
||||
pity_hits = 0
|
||||
jackpot_hits = 0
|
||||
|
||||
for _ in range(wishes):
|
||||
pool_state["balance"] += WISH_POOL_CONTRIBUTION
|
||||
pool_state["total_wishes"] += 1
|
||||
pool_gain += WISH_POOL_CONTRIBUTION
|
||||
force_pity = account["wish_pool_pity"] >= WISH_POOL_PITY_LIMIT
|
||||
item_awarded = False
|
||||
if force_pity:
|
||||
pity_hits += 1
|
||||
gift_roll = self.rng.randrange(100)
|
||||
if gift_roll < 70:
|
||||
scan_tickets += 1
|
||||
elif gift_roll < 95:
|
||||
taffy_credits += 1
|
||||
else:
|
||||
primogems += 1
|
||||
item_awarded = True
|
||||
else:
|
||||
roll = self.rng.randrange(1000)
|
||||
if roll < 440:
|
||||
dry_wishes += 1
|
||||
elif roll < 660:
|
||||
small_returns += 1
|
||||
mora_payout += self.rng.randrange(200, 601)
|
||||
elif roll < 780:
|
||||
medium_returns += 1
|
||||
mora_payout += self.rng.randrange(800, 1401)
|
||||
elif roll < 860:
|
||||
scan_tickets += 1
|
||||
item_awarded = True
|
||||
elif roll < 920:
|
||||
taffy_credits += 1
|
||||
item_awarded = True
|
||||
elif roll < 950:
|
||||
primogems += 1
|
||||
item_awarded = True
|
||||
elif roll < 980:
|
||||
big_returns += 1
|
||||
mora_payout += self.rng.randrange(2000, 3501)
|
||||
elif roll < 995:
|
||||
scan_tickets += 1
|
||||
taffy_credits += 1
|
||||
item_awarded = True
|
||||
elif roll < 999:
|
||||
primogems += 2
|
||||
item_awarded = True
|
||||
else:
|
||||
primogems += 2
|
||||
jackpot = min(
|
||||
pool_state["balance"] * WISH_POOL_JACKPOT_PERCENT
|
||||
// 100,
|
||||
WISH_POOL_JACKPOT_CAP,
|
||||
)
|
||||
pool_state["balance"] -= jackpot
|
||||
jackpot_payout += jackpot
|
||||
jackpot_hits += 1
|
||||
item_awarded = True
|
||||
if item_awarded:
|
||||
account["wish_pool_pity"] = 0
|
||||
else:
|
||||
account["wish_pool_pity"] += 1
|
||||
|
||||
total_mora_payout = mora_payout + jackpot_payout
|
||||
account["balance"] += total_mora_payout
|
||||
account["scan_boost_tickets"] += scan_tickets
|
||||
account["taffy_say_credits"] += taffy_credits
|
||||
if primogems:
|
||||
account["primogems"] += primogems
|
||||
append_primogem_event(account, primogems, "许愿池")
|
||||
outcome_parts: list[str] = []
|
||||
if jackpot_hits:
|
||||
outcome_parts.append("池底大愿")
|
||||
if scan_tickets or taffy_credits or primogems or pity_hits:
|
||||
outcome_parts.append("祝福")
|
||||
if big_returns:
|
||||
outcome_parts.append("大回响")
|
||||
elif medium_returns or small_returns:
|
||||
outcome_parts.append("回响")
|
||||
if not outcome_parts:
|
||||
outcome_parts.append("沉底")
|
||||
outcome = "、".join(dict.fromkeys(outcome_parts))
|
||||
append_game_record(
|
||||
account,
|
||||
kind="prize_draw",
|
||||
index=index,
|
||||
outcome=outcome,
|
||||
stake=amount,
|
||||
payout=total_mora_payout,
|
||||
summary=(
|
||||
f"许愿池 {wishes}愿;池水+{pool_gain};"
|
||||
f"摩拉+{total_mora_payout};扫码券+{scan_tickets};"
|
||||
f"发话+{taffy_credits};原石+{primogems}"
|
||||
),
|
||||
)
|
||||
append_game_coin_event(
|
||||
account,
|
||||
kind="prize_draw",
|
||||
outcome=outcome,
|
||||
stake=amount,
|
||||
payout=total_mora_payout,
|
||||
)
|
||||
|
||||
result_parts: list[str] = []
|
||||
if total_mora_payout:
|
||||
mora_text = f"摩拉 +{total_mora_payout}"
|
||||
if jackpot_payout:
|
||||
mora_text += f"(含池底 {jackpot_payout})"
|
||||
result_parts.append(mora_text)
|
||||
if scan_tickets:
|
||||
result_parts.append(f"扫码概率券 +{scan_tickets} 张")
|
||||
if taffy_credits:
|
||||
result_parts.append(f"Taffy发话额度 +{taffy_credits} 次")
|
||||
if primogems:
|
||||
result_parts.append(f"原石 +{primogems} 个")
|
||||
if not result_parts:
|
||||
result_parts.append("池水起了点波纹,啥也没捞到")
|
||||
if pity_hits:
|
||||
result_parts.append(f"触发保底 {pity_hits} 次")
|
||||
return player_reply(
|
||||
(
|
||||
f"投入 {amount} 摩拉,向许愿池许了 {wishes} 愿;"
|
||||
f"结果:{','.join(result_parts)};"
|
||||
f"当前摩拉 {account['balance']},"
|
||||
f"原石 {account['primogems']} 个,"
|
||||
f"扫码概率券 {account['scan_boost_tickets']} 张,"
|
||||
f"Taffy发话额度 {account['taffy_say_credits']} 次;"
|
||||
f"池水 {pool_state['balance']};"
|
||||
f"保底 {account['wish_pool_pity']}/{WISH_POOL_PITY_LIMIT}"
|
||||
),
|
||||
index,
|
||||
)
|
||||
|
||||
return self.ledger.apply_player_local_game_action(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
"prize_draw:" + str(amount or ""),
|
||||
WISH_POOL_GAME_KEY,
|
||||
action,
|
||||
)
|
||||
|
||||
def primogem_command(
|
||||
self,
|
||||
row: dict[str, Any],
|
||||
@@ -1055,6 +1354,45 @@ class PlayerGames:
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def blackjack_net_text(net: int) -> str:
|
||||
if net > 0:
|
||||
return f"净赚 {net} 摩拉"
|
||||
if net < 0:
|
||||
return f"净亏 {-net} 摩拉"
|
||||
return "净赚净亏 0 摩拉"
|
||||
|
||||
def blackjack_win_rates(self, row: dict[str, Any]) -> str:
|
||||
stats = self.ledger.player_blackjack_stats(
|
||||
str(row["group_fingerprint"])
|
||||
)
|
||||
hands = int(stats["hands"])
|
||||
if hands == 0:
|
||||
return "当前群还没有已结算的21点记录"
|
||||
taffy_wins = int(stats["taffy_wins"])
|
||||
taffy_losses = int(stats["taffy_losses"])
|
||||
ties = int(stats["ties"])
|
||||
taffy_rate = taffy_wins / hands * 100
|
||||
lines = [
|
||||
f"21点胜率:本群已结算 {hands} 手",
|
||||
(
|
||||
f"Taffy:胜 {taffy_wins} / 负 {taffy_losses} / 平 {ties},"
|
||||
f"胜率 {taffy_rate:.2f}%;"
|
||||
f"{self.blackjack_net_text(int(stats['taffy_net']))}"
|
||||
),
|
||||
"玩家:",
|
||||
]
|
||||
for index, player in enumerate(stats["players"], start=1):
|
||||
player_hands = int(player["hands"])
|
||||
win_rate = int(player["wins"]) / player_hands * 100
|
||||
lines.append(
|
||||
f"{index}. {player['display_name']}:"
|
||||
f"胜 {player['wins']} / 负 {player['losses']} / "
|
||||
f"平 {player['ties']},胜率 {win_rate:.2f}%;"
|
||||
f"{self.blackjack_net_text(int(player['net']))}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def parse_blackjack_table_command(
|
||||
arguments: str,
|
||||
@@ -1117,7 +1455,7 @@ class PlayerGames:
|
||||
def blackjack_prompt(player: dict[str, Any]) -> str:
|
||||
return (
|
||||
f"请{player['display_name']} 放自己的筹码\n"
|
||||
"输入/21点 放200(1~10000)"
|
||||
"输入/21点 放200(1~2000)"
|
||||
)
|
||||
|
||||
def group_blackjack(
|
||||
@@ -1135,7 +1473,7 @@ class PlayerGames:
|
||||
return (
|
||||
"多人21点用法:/21点 多人 启动;/21点 加入;"
|
||||
"主持人 /21点 多人 开始;按提示 /21点 放200"
|
||||
"(1~10000);轮到自己 /21点 要牌 或 /21点 停牌"
|
||||
"(1~2000);轮到自己 /21点 要牌 或 /21点 停牌"
|
||||
)
|
||||
if action == "start":
|
||||
return self.ledger.start_group_blackjack(
|
||||
@@ -1181,7 +1519,7 @@ class PlayerGames:
|
||||
)
|
||||
if action == "bet":
|
||||
if not MIN_BLACKJACK_TABLE_BET <= amount <= MAX_BLACKJACK_TABLE_BET:
|
||||
return "21点多人牌桌筹码必须为 1~10000 的整数摩拉"
|
||||
return "21点多人牌桌筹码必须为 1~2000 的整数摩拉"
|
||||
deck = list(range(52))
|
||||
self.rng.shuffle(deck)
|
||||
return self.ledger.place_group_blackjack_bet(
|
||||
@@ -1868,6 +2206,12 @@ class PlayerGames:
|
||||
stake=bet,
|
||||
payout=0,
|
||||
)
|
||||
record_blackjack_result(
|
||||
account,
|
||||
outcome="主动认输",
|
||||
stake=bet,
|
||||
payout=0,
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
account["blackjack_taffy_win_streak"] = 0
|
||||
@@ -1922,6 +2266,12 @@ class PlayerGames:
|
||||
stake=int(bet),
|
||||
payout=int(payout),
|
||||
)
|
||||
record_blackjack_result(
|
||||
account,
|
||||
outcome=outcome,
|
||||
stake=int(bet),
|
||||
payout=int(payout),
|
||||
)
|
||||
achievements = blackjack_achievement_lines(
|
||||
account,
|
||||
outcome=outcome,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-sdk"
|
||||
version = "1.1.2b1+1019"
|
||||
version = "2.0.5b1+1023"
|
||||
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.1.2b1+1019"
|
||||
__version__ = "2.0.5b1+1023"
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
[tool.jwebot]
|
||||
release = "beta 1.1.2.1019"
|
||||
release = "beta 2.0.5.1023"
|
||||
production_deployments_enabled_by_default = false
|
||||
|
||||
[tool.jwebot.workspace]
|
||||
|
||||
@@ -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()
|
||||
@@ -62,6 +62,8 @@ class DeploymentScriptTests(unittest.TestCase):
|
||||
"jwebot_config",
|
||||
"jwebot_sdk",
|
||||
"jwebot_kernel",
|
||||
"jwebot_kernel/data",
|
||||
"kernel_data_source_dir",
|
||||
"jwebot_official_extensions",
|
||||
"data/cs_autochess",
|
||||
"official_autochess_data_source_dir",
|
||||
|
||||
+502
-5
@@ -211,6 +211,10 @@ class RoutingTests(unittest.TestCase):
|
||||
MODULE.classify_question("/steam 在线"),
|
||||
("local", "local_steam_friends"),
|
||||
)
|
||||
self.assertEqual(
|
||||
MODULE.classify_question("/知识问答"),
|
||||
("local", "local_trivia_start"),
|
||||
)
|
||||
|
||||
def test_reply_has_one_suffix_within_delivery_limit(self):
|
||||
self.assertEqual(MODULE.clean_reply("收到。"), "收到喵")
|
||||
@@ -219,6 +223,35 @@ class RoutingTests(unittest.TestCase):
|
||||
self.assertEqual(len(MODULE.clean_reply("好" * 900)), 900)
|
||||
self.assertEqual(MODULE.FAST_ACKNOWLEDGEMENT, "喵......")
|
||||
|
||||
def test_chat_reply_style_removes_ai_like_padding(self):
|
||||
self.assertEqual(
|
||||
MODULE.clean_chat_reply(
|
||||
"李神是啥我都不清楚,怎么比,总不能凭空给自己打满分吧\n"
|
||||
"反正我就是个会唠嗑的,聊聊天、记记小账还行,"
|
||||
"别的功能想都别想喵"
|
||||
),
|
||||
"李神是啥喵",
|
||||
)
|
||||
self.assertEqual(
|
||||
MODULE.clean_chat_reply("作为AI我无法比较。\n李神是谁?"),
|
||||
"李神是谁喵",
|
||||
)
|
||||
self.assertEqual(
|
||||
MODULE.clean_chat_reply("你说“一切”是啥意思喵?做题就 /回答 A喵"),
|
||||
"你说“一切”是啥意思喵",
|
||||
)
|
||||
self.assertEqual(
|
||||
MODULE.unpack_reply_parts(MODULE.clean_chat_reply(
|
||||
"这波像是脑子知道要撤,但是手完全没同意,"
|
||||
"而且还顺手把队友也拖下水了。"
|
||||
)),
|
||||
[
|
||||
"这波像是脑子知道要撤喵",
|
||||
"但是手完全没同意喵",
|
||||
"而且还顺手把队友也拖下水了喵",
|
||||
],
|
||||
)
|
||||
|
||||
def test_profile_memory_rejects_bare_versions_but_keeps_audio_context(self):
|
||||
self.assertFalse(MODULE.profile_fact_is_safe("5.1"))
|
||||
self.assertTrue(MODULE.profile_fact_is_safe("最近在研究 5.1 音响配置"))
|
||||
@@ -277,12 +310,288 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(first["text"], MODULE.FAST_ACKNOWLEDGEMENT)
|
||||
self.assertEqual(second["text"], MODULE.FAST_ACKNOWLEDGEMENT)
|
||||
self.assertEqual(observed_context, [("你好", "第一条回复喵")])
|
||||
self.assertEqual(len(observed_context), 1)
|
||||
self.assertEqual(observed_context[0]["speaker"], "Alice")
|
||||
self.assertEqual(observed_context[0]["user_text"], "你好")
|
||||
self.assertEqual(observed_context[0]["taffy_text"], "第一条回复喵")
|
||||
self.assertIn("same_sender", observed_context[0]["tags"])
|
||||
self.assertEqual(
|
||||
self.application.ledger.row("event-2")["response_text"],
|
||||
"第二条回复喵",
|
||||
)
|
||||
|
||||
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(
|
||||
[(item["speaker"], item["user_text"], item["taffy_text"])
|
||||
for item in observed["group-second"]],
|
||||
[("Alice", "群里的第一条", "group-first 回复喵")],
|
||||
)
|
||||
self.assertEqual(
|
||||
[(item["speaker"], item["user_text"], item["taffy_text"])
|
||||
for item in observed["direct-second"]],
|
||||
[("Alice", "私聊里的第一条", "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 = []
|
||||
|
||||
def complete(**kwargs):
|
||||
observed_context.extend(kwargs["context"])
|
||||
return "我看到了喵"
|
||||
|
||||
self.application.deepseek.complete = complete
|
||||
self.application.accept(event("version-context", "@JWeBot /version"))
|
||||
local = self.application.accept(event("coin-context", "@JWeBot /翻硬币"))
|
||||
self.application.accept(event("chat-context", "@JWeBot 刚才是什么结果"))
|
||||
self.application.process_direct_chat_once()
|
||||
|
||||
self.assertTrue(
|
||||
any(
|
||||
item["route"] == "local"
|
||||
and item["route_reason"] == "local_coin"
|
||||
and item["speaker"] == "Alice"
|
||||
and item["user_text"] == "/翻硬币"
|
||||
and item["taffy_text"] == local["text"]
|
||||
for item in observed_context
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(item["user_text"] != "/version" for item in observed_context)
|
||||
)
|
||||
|
||||
def test_self_referential_context_does_not_cross_senders(self):
|
||||
observed_context = []
|
||||
|
||||
def complete(**kwargs):
|
||||
observed_context[:] = list(kwargs["context"])
|
||||
return "我只看你的喵"
|
||||
|
||||
self.application.deepseek.complete = complete
|
||||
self.application.deliver = mock.Mock()
|
||||
self.application.accept(event("alice-first", "@JWeBot Alice 的事", sender="Alice"))
|
||||
self.application.process_direct_chat_once()
|
||||
self.application.accept(event("bob-first", "@JWeBot Bob 的事", sender="Bob"))
|
||||
self.application.process_direct_chat_once()
|
||||
self.application.accept(event("bob-follow", "@JWeBot 我刚才说啥", sender="Bob"))
|
||||
self.application.process_direct_chat_once()
|
||||
|
||||
self.assertTrue(observed_context)
|
||||
self.assertEqual(
|
||||
{item["speaker"] for item in observed_context},
|
||||
{"Bob"},
|
||||
)
|
||||
self.assertTrue(
|
||||
all("Alice 的事" not in item["user_text"] for item in observed_context)
|
||||
)
|
||||
|
||||
def test_mentioned_person_context_can_cross_senders(self):
|
||||
observed_context = []
|
||||
|
||||
def complete(**kwargs):
|
||||
observed_context[:] = list(kwargs["context"])
|
||||
return "看 Bob 那句喵"
|
||||
|
||||
self.application.deepseek.complete = complete
|
||||
self.application.deliver = mock.Mock()
|
||||
self.application.accept(event("bob-mentioned", "@JWeBot Bob 的事", sender="Bob"))
|
||||
self.application.process_direct_chat_once()
|
||||
self.application.accept(
|
||||
event("alice-asks-bob", "@JWeBot Bob 刚才说啥", sender="Alice")
|
||||
)
|
||||
self.application.process_direct_chat_once()
|
||||
|
||||
self.assertTrue(
|
||||
any(
|
||||
item["speaker"] == "Bob"
|
||||
and item["user_text"] == "Bob 的事"
|
||||
and "mentioned_person" in item["tags"]
|
||||
for item in observed_context
|
||||
)
|
||||
)
|
||||
|
||||
def test_low_confidence_sender_does_not_receive_identity_memory(self):
|
||||
captured = {}
|
||||
|
||||
def complete(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return "先确认是谁喵"
|
||||
|
||||
self.application.deepseek.complete = complete
|
||||
self.application.deliver = mock.Mock()
|
||||
self.application.accept(
|
||||
{
|
||||
**event("low-conf-chat", "@JWeBot 我是谁", sender="alice"),
|
||||
"sender_confidence": 0.42,
|
||||
"body_confidence": 0.98,
|
||||
"source": "rapidocr-local",
|
||||
}
|
||||
)
|
||||
self.application.process_direct_chat_once()
|
||||
|
||||
self.assertEqual(captured["person_memory"], ())
|
||||
self.assertIsNone(
|
||||
self.application.ledger.person_profile(
|
||||
"group-a",
|
||||
MODULE.person_scope_key("group-a", "Alice"),
|
||||
)
|
||||
)
|
||||
|
||||
def test_low_confidence_identity_bound_local_command_is_rejected(self):
|
||||
response = self.application.accept(
|
||||
{
|
||||
**event("low-conf-draw", "@JWeBot /抽奖 10", sender="alice"),
|
||||
"sender_confidence": 0.42,
|
||||
"body_confidence": 0.98,
|
||||
"source": "rapidocr-local",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response["route"], "local")
|
||||
self.assertIn("没认稳", response["text"])
|
||||
self.assertEqual(
|
||||
self.application.ledger.row("low-conf-draw")["status"],
|
||||
"local_rejected",
|
||||
)
|
||||
|
||||
def test_delivered_taffy_broadcast_is_available_to_context(self):
|
||||
self.application.ledger.reserve_clock_broadcast(
|
||||
group_fingerprint="group-a",
|
||||
slot_key="hourly:2026-10-21T17",
|
||||
local_date="2026-10-21",
|
||||
kind="ambient_chat",
|
||||
message="我刚刚路过说了一句喵",
|
||||
)
|
||||
self.assertTrue(
|
||||
self.application.ledger.complete_clock_broadcast(
|
||||
"group-a",
|
||||
"hourly:2026-10-21T17",
|
||||
)
|
||||
)
|
||||
|
||||
context = self.application.ledger.conversation_context(
|
||||
"group-a",
|
||||
exclude_event_id="current",
|
||||
sender="Alice",
|
||||
question="Taffy 刚刚说了什么",
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
any(
|
||||
item["speaker"] == "Taffy"
|
||||
and item["taffy_text"] == "我刚刚路过说了一句喵"
|
||||
and "taffy_broadcast" in item["tags"]
|
||||
for item in context
|
||||
)
|
||||
)
|
||||
|
||||
def test_local_game_status_question_answers_without_direct_chat(self):
|
||||
person = self.application.ledger.resolve_person("group-a", "Bob")
|
||||
account = MODULE.normalize_player_account({})
|
||||
account["game"] = {
|
||||
"bet": 50,
|
||||
"deck": [1, 2, 3],
|
||||
"player": [5, 10],
|
||||
"dealer": [8, 2],
|
||||
"updated_at": time.time(),
|
||||
"index": 1,
|
||||
}
|
||||
with self.application.ledger.lock, self.application.ledger.connection:
|
||||
self.application.ledger._save_player_account(
|
||||
"group-a",
|
||||
str(person["person_key"]),
|
||||
account,
|
||||
MODULE.utc_now(),
|
||||
)
|
||||
self.application.deepseek.complete = mock.Mock(
|
||||
side_effect=AssertionError("direct chat should not run")
|
||||
)
|
||||
|
||||
response = self.application.accept(
|
||||
event("bob-local-status", "@JWeBot Bob 在玩什么吗", sender="Alice")
|
||||
)
|
||||
|
||||
self.assertEqual(response["text"].count("喵"), 1)
|
||||
self.assertIn("bob 在玩单人21点 #1", response["text"])
|
||||
self.assertIn("16点", response["text"])
|
||||
self.assertEqual(response["execution_path"], "local-command")
|
||||
self.assertEqual(
|
||||
self.application.ledger.row("bob-local-status")["status"],
|
||||
"chat_completed",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.application.ledger.status()["direct_chat_pending"],
|
||||
0,
|
||||
)
|
||||
self.application.deepseek.complete.assert_not_called()
|
||||
|
||||
def test_short_evaluation_uses_active_taffy_thread(self):
|
||||
observed_context = []
|
||||
|
||||
def complete(**kwargs):
|
||||
observed_context[:] = list(kwargs["context"])
|
||||
return "他那手确实挺敢喵"
|
||||
|
||||
self.application.deepseek.complete = complete
|
||||
self.application.deliver = mock.Mock()
|
||||
self.application.ledger.reserve_clock_broadcast(
|
||||
group_fingerprint="group-a",
|
||||
slot_key="hourly:2026-10-21T18",
|
||||
local_date="2026-10-21",
|
||||
kind="ambient_chat",
|
||||
message="21点那把16点还敢停牌喵",
|
||||
)
|
||||
self.assertTrue(
|
||||
self.application.ledger.complete_clock_broadcast(
|
||||
"group-a",
|
||||
"hourly:2026-10-21T18",
|
||||
)
|
||||
)
|
||||
|
||||
self.application.accept(
|
||||
event("short-active-thread", "@JWeBot 还真是 胆子大", sender="Alice")
|
||||
)
|
||||
self.application.process_direct_chat_once()
|
||||
|
||||
self.assertTrue(
|
||||
any(
|
||||
"active_thread" in item["tags"]
|
||||
and "16点还敢停牌" in item["taffy_text"]
|
||||
for item in observed_context
|
||||
)
|
||||
)
|
||||
|
||||
def test_route_and_ack_telemetry_is_privacy_safe(self):
|
||||
observed = []
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -412,6 +721,63 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
"chat_delivered",
|
||||
)
|
||||
|
||||
def test_delivery_sends_multi_reply_as_separate_helper_calls(self):
|
||||
runtime_python = Path(self.temporary.name) / "python"
|
||||
delivery_helper = Path(self.temporary.name) / "jwebot_edge_delivery.py"
|
||||
runtime_python.write_text("# runtime\n", encoding="utf-8")
|
||||
delivery_helper.write_text("# helper\n", encoding="utf-8")
|
||||
result = mock.Mock(returncode=0, stdout="", stderr="")
|
||||
reply = MODULE.pack_reply_parts(["第一段喵", "第二段喵"])
|
||||
|
||||
with mock.patch.object(MODULE, "DELIVERY_HELPER", delivery_helper), \
|
||||
mock.patch.dict(
|
||||
os.environ,
|
||||
{"JWEBOT_RUNTIME_PYTHON": str(runtime_python)},
|
||||
), \
|
||||
mock.patch.object(
|
||||
MODULE.random,
|
||||
"uniform",
|
||||
return_value=0.75,
|
||||
) as uniform, \
|
||||
mock.patch.object(MODULE.time, "sleep") as sleep, \
|
||||
mock.patch.object(
|
||||
MODULE.subprocess,
|
||||
"run",
|
||||
return_value=result,
|
||||
) as run:
|
||||
self.application.deliver(
|
||||
{
|
||||
"event_id": "event-1",
|
||||
"group_fingerprint": "group-a",
|
||||
"result_text": reply,
|
||||
}
|
||||
)
|
||||
|
||||
payloads = [
|
||||
json.loads(call.kwargs["input"])
|
||||
for call in run.call_args_list
|
||||
]
|
||||
self.assertEqual(
|
||||
payloads,
|
||||
[
|
||||
{
|
||||
"event_id": "event-1:part-1",
|
||||
"group_fingerprint": "group-a",
|
||||
"reply": "第一段喵",
|
||||
},
|
||||
{
|
||||
"event_id": "event-1:part-2",
|
||||
"group_fingerprint": "group-a",
|
||||
"reply": "第二段喵",
|
||||
},
|
||||
],
|
||||
)
|
||||
uniform.assert_called_once_with(
|
||||
MODULE.MULTI_REPLY_DELAY_MIN_SECONDS,
|
||||
MODULE.MULTI_REPLY_DELAY_MAX_SECONDS,
|
||||
)
|
||||
sleep.assert_called_once_with(0.75)
|
||||
|
||||
def test_two_direct_generations_run_in_parallel(self):
|
||||
entered = []
|
||||
entered_lock = threading.Lock()
|
||||
@@ -925,9 +1291,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
self.assertEqual(version["route"], "local")
|
||||
self.assertIn("JWeBot 版本", version["text"])
|
||||
self.assertIn("kernel", version["text"])
|
||||
self.assertIn("最新版本:beta 1.1.2.1019", version["text"])
|
||||
self.assertIn("更新时间:2026-10-19 16:38:10", version["text"])
|
||||
self.assertIn("更新摘要:新增低频主动插话、随机冷门知识问答", version["text"])
|
||||
self.assertIn("最新版本:beta 2.0.5.1023", version["text"])
|
||||
self.assertIn("更新时间:2026-10-23 17:16:12", version["text"])
|
||||
self.assertIn("更新摘要:21 点胜率显示修复完成", version["text"])
|
||||
self.assertEqual(debug["route"], "local")
|
||||
self.assertIn("调试埋点", debug["text"])
|
||||
self.assertEqual(balance["route"], "local")
|
||||
@@ -1207,7 +1573,34 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_trivia_quiz_broadcast_and_answer_rewards_mora(self):
|
||||
self.assertGreater(len(MODULE.trivia_question_bank()), 200)
|
||||
bank = MODULE.trivia_question_bank()
|
||||
self.assertEqual(len(bank), 500)
|
||||
display_text = "\n".join(
|
||||
"\n".join(
|
||||
[
|
||||
str(question["domain"]),
|
||||
str(question["question"]),
|
||||
*[str(option) for option in question["options"]],
|
||||
str(question["explanation"]),
|
||||
]
|
||||
)
|
||||
for question in bank
|
||||
)
|
||||
self.assertIn("新牛仔(NewJeans)", display_text)
|
||||
self.assertIn("碧碧(BiBi)", display_text)
|
||||
self.assertNotIn("地平线一直想回去见到的儿子", display_text)
|
||||
self.assertNotIn("蒙德风神的真实魔神名", display_text)
|
||||
topics = {str(question["topic"]) for question in bank}
|
||||
self.assertIn("缪斯学园偶像", topics)
|
||||
self.assertIn("我的世界", topics)
|
||||
self.assertEqual(
|
||||
MODULE.canonical_trivia_topic("LoveLive!"),
|
||||
"缪斯学园偶像",
|
||||
)
|
||||
self.assertEqual(
|
||||
MODULE.canonical_trivia_topic("Minecraft"),
|
||||
"我的世界",
|
||||
)
|
||||
ledger = self.application.ledger
|
||||
observed_at = MODULE.datetime(
|
||||
2026,
|
||||
@@ -1247,10 +1640,12 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
delivery = self.application.deliver.call_args.args[0]
|
||||
self.assertIn("冷门知识问答", delivery["result_text"])
|
||||
self.assertIn("限时5分钟", delivery["result_text"])
|
||||
self.assertIn("出现次数:1", delivery["result_text"])
|
||||
self.assertIn("/回答 A", delivery["result_text"])
|
||||
state = ledger.local_game("group-a", MODULE.TRIVIA_GAME_KEY)
|
||||
self.assertIsNotNone(state)
|
||||
assert state is not None
|
||||
self.assertEqual(state["ask_count"], 1)
|
||||
state["expires_at"] = time.time() + 300
|
||||
ledger.save_local_game("group-a", MODULE.TRIVIA_GAME_KEY, state)
|
||||
answer = str(state["answer"])
|
||||
@@ -1271,6 +1666,87 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
account = ledger._load_player_account("group-a", str(person["person_key"]))
|
||||
self.assertEqual(account["balance"], 1000 + reward)
|
||||
|
||||
def test_trivia_quiz_prefers_least_asked_base_id(self):
|
||||
group = "group-trivia-balance"
|
||||
topic = "原神"
|
||||
topic_base_ids = sorted(
|
||||
{
|
||||
str(question["base_id"])
|
||||
for question in MODULE.trivia_question_bank()
|
||||
if question["topic"] == topic
|
||||
}
|
||||
)
|
||||
self.assertGreater(len(topic_base_ids), 3)
|
||||
least_asked = topic_base_ids[2]
|
||||
ask_counts = {base_id: 4 for base_id in topic_base_ids}
|
||||
ask_counts[least_asked] = 1
|
||||
self.application.ledger.save_local_game(
|
||||
group,
|
||||
MODULE.TRIVIA_HISTORY_KEY,
|
||||
{
|
||||
"recent_base_ids": topic_base_ids[: MODULE.TRIVIA_RECENT_BASE_LIMIT],
|
||||
"ask_counts": ask_counts,
|
||||
},
|
||||
)
|
||||
|
||||
selected = self.application.clock.select_trivia_question(
|
||||
group=group,
|
||||
local_now=MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
23,
|
||||
13,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
),
|
||||
topic=topic,
|
||||
)
|
||||
|
||||
self.assertEqual(selected["base_id"], least_asked)
|
||||
self.assertEqual(selected["ask_count"], 2)
|
||||
self.assertIn(
|
||||
"出现次数:2",
|
||||
self.application.clock.render_trivia_message(selected),
|
||||
)
|
||||
|
||||
def test_trivia_quiz_can_be_started_by_local_command(self):
|
||||
self.application.deepseek.complete = mock.Mock(side_effect=AssertionError)
|
||||
|
||||
unknown = self.application.accept(
|
||||
event("trivia-force-unknown", "@JWeBot /知识问答 星际")
|
||||
)
|
||||
first = self.application.accept(
|
||||
event("trivia-force", "@JWeBot /知识问答 LoveLive!")
|
||||
)
|
||||
second = self.application.accept(
|
||||
event("trivia-force-existing", "@JWeBot /知识问答 Minecraft")
|
||||
)
|
||||
|
||||
self.assertIn("支持主题:", unknown["text"])
|
||||
self.assertIn("原神", unknown["text"])
|
||||
self.assertIn("少女乐团派对", unknown["text"])
|
||||
self.assertIn("缪斯学园偶像", unknown["text"])
|
||||
self.assertIn("我的世界", unknown["text"])
|
||||
self.assertEqual(first["route"], "local")
|
||||
self.assertEqual(first["execution_path"], "local-command")
|
||||
self.assertIn("冷门知识问答", first["text"])
|
||||
self.assertIn("缪斯学园偶像", first["text"])
|
||||
self.assertIn("回答格式:/回答 A", first["text"])
|
||||
self.assertIn("已经有一题在进行中", second["text"])
|
||||
self.assertIn("缪斯学园偶像", second["text"])
|
||||
self.assertEqual(
|
||||
self.application.ledger.row("trivia-force")["status"],
|
||||
"local_completed",
|
||||
)
|
||||
state = self.application.ledger.local_game(
|
||||
"group-a",
|
||||
MODULE.TRIVIA_GAME_KEY,
|
||||
)
|
||||
self.assertIsNotNone(state)
|
||||
assert state is not None
|
||||
self.assertEqual(state["topic"], "缪斯学园偶像")
|
||||
self.application.deepseek.complete.assert_not_called()
|
||||
|
||||
def test_ambient_chat_cooldown_keeps_next_hour_scan_broadcast(self):
|
||||
ledger = self.application.ledger
|
||||
observed_at = MODULE.datetime(
|
||||
@@ -2175,6 +2651,10 @@ class DirectClientTests(unittest.TestCase):
|
||||
self.assertNotIn("temperature", payload)
|
||||
self.assertEqual(request.get_header("Authorization"), "Bearer test-key")
|
||||
self.assertIn(MODULE.BOT_IDENTITY_NOTE, payload["messages"][0]["content"])
|
||||
self.assertIn(
|
||||
MODULE.TAFFY_CHAT_STYLE_NOTE,
|
||||
payload["messages"][0]["content"],
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
message["content"].startswith("CURRENT_GROUP_SENDER_LABEL:")
|
||||
@@ -2763,6 +3243,23 @@ class CloudRunnerTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(MODULE.cloud_reply_text(nested), "嵌套内容喵")
|
||||
|
||||
def test_cloud_protocol_applies_chat_reply_style(self):
|
||||
self.assertEqual(
|
||||
MODULE.cloud_reply_text(
|
||||
json.dumps(
|
||||
{
|
||||
"route": "reply",
|
||||
"text": (
|
||||
"李神是啥我都不清楚,怎么比,"
|
||||
"总不能凭空给自己打满分吧"
|
||||
),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
),
|
||||
"李神是啥喵",
|
||||
)
|
||||
|
||||
def test_cloud_protocol_ignores_a_suffix_after_the_json_object(self):
|
||||
self.assertEqual(
|
||||
MODULE.cloud_reply_text(
|
||||
|
||||
@@ -152,6 +152,7 @@ class LocalExtensionInventoryTests(unittest.TestCase):
|
||||
{
|
||||
"feature_list",
|
||||
"deepseek_balance",
|
||||
"trivia_start",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
+14
-13
@@ -40,6 +40,7 @@ class Release061Tests(unittest.TestCase):
|
||||
|
||||
def test_aliases_are_corresponding_and_case_insensitive(self):
|
||||
pairs = [("上下文", "Context"), ("摩拉", "wallet"), ("抽奖", "draw"),
|
||||
("许愿池", "draw"), ("许愿", "draw"),
|
||||
("买彩票", "lottery"), ("21点", "blackjack"),
|
||||
("人物列表", "people"), ("人物", "person"), ("游戏帮助", "games")]
|
||||
for chinese, english in pairs:
|
||||
@@ -84,46 +85,46 @@ class Release061Tests(unittest.TestCase):
|
||||
def test_initial_funds_aliases_groups_and_replay(self):
|
||||
self.assertIn("摩拉 1000", self.command("/wallet", event_id="wallet-1"))
|
||||
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
||||
first = self.command("/draw 10", event_id="draw-1")
|
||||
replay = self.command("/draw 10", sender="a1", event_id="draw-1")
|
||||
first = self.command("/draw 1000", event_id="draw-1")
|
||||
replay = self.command("/draw 1000", sender="a1", event_id="draw-1")
|
||||
self.assertEqual(first, replay)
|
||||
self.assertEqual(self.state("a1")["balance"], 990)
|
||||
self.assertIn("许愿池", first)
|
||||
self.assertEqual(self.state("a1")["balance"], 0)
|
||||
self.assertIn("摩拉 1000", self.command("/wallet", sender="Bob"))
|
||||
self.assertIn("摩拉 1000", self.command("/wallet", group="group-b"))
|
||||
self.engine.ledger.connection.close()
|
||||
self.engine = MODULE.EdgeApplication(config(self.root))
|
||||
self.local = self.engine.local
|
||||
self.assertIn("摩拉 990", self.command("/wallet", event_id="restart"))
|
||||
self.assertIn("摩拉 0", self.command("/wallet", event_id="restart"))
|
||||
|
||||
def test_concurrent_replay_debits_once_and_rolls_back_failure(self):
|
||||
replies = []
|
||||
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
||||
threads = [threading.Thread(target=lambda: replies.append(
|
||||
self.command("/draw 100", event_id="concurrent"))) for _ in range(8)]
|
||||
self.command("/draw 1000", event_id="concurrent"))) for _ in range(8)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
self.assertEqual(len(replies), 8)
|
||||
self.assertEqual(len(set(replies)), 1)
|
||||
self.assertEqual(self.state()["balance"], 900)
|
||||
self.assertEqual(self.state()["balance"], 0)
|
||||
def broken(state):
|
||||
state["balance"] = -1
|
||||
return "bad"
|
||||
with self.assertRaises(MODULE.LocalCommandError):
|
||||
self.engine.ledger.apply_player_action("group-a", "Alice", "broken", "test", broken)
|
||||
self.assertEqual(self.state()["balance"], 900)
|
||||
self.assertEqual(self.state()["balance"], 0)
|
||||
with self.assertRaises(MODULE.LocalCommandError):
|
||||
self.command("/draw 100", sender="Bob", event_id="concurrent")
|
||||
self.command("/draw 1000", sender="Bob", event_id="concurrent")
|
||||
|
||||
def test_invalid_bets_insufficient_funds_and_lottery(self):
|
||||
for question in ("/draw -1", "/draw 0", "/draw 101", "/lottery xyz",
|
||||
"/blackjack start -1", "/blackjack hit 10"):
|
||||
self.assertRegex(self.command(question), "用法|下注")
|
||||
self.assertRegex(self.command(question), "用法|投入|下注")
|
||||
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
||||
for _ in range(10):
|
||||
self.command("/draw 100")
|
||||
self.assertIn("摩拉不足", self.command("/draw 2"))
|
||||
self.command("/draw 1000")
|
||||
self.assertIn("摩拉不足", self.command("/draw 1000"))
|
||||
self.assertEqual(self.state()["balance"], 0)
|
||||
|
||||
def test_blackjack_is_private_persistent_and_settles_exactly_once(self):
|
||||
@@ -136,7 +137,7 @@ class Release061Tests(unittest.TestCase):
|
||||
self.assertIn("暗牌", start)
|
||||
self.assertEqual(self.state()["balance"], 950)
|
||||
self.assertIn("没有进行中", self.command("/blackjack status", sender="Bob"))
|
||||
self.assertIn("已有一局", self.command("/draw"))
|
||||
self.assertIn("已有一局", self.command("/draw 1000"))
|
||||
self.engine.ledger.connection.close()
|
||||
self.engine = MODULE.EdgeApplication(config(self.root))
|
||||
self.local = self.engine.local
|
||||
|
||||
+77
-17
@@ -154,7 +154,20 @@ class Release063GameTests(unittest.TestCase):
|
||||
self.assertEqual(self.state()["balance"], 500)
|
||||
self.assertIn("已报名 1 人", self.command("/彩票状态"))
|
||||
|
||||
def test_blackjack_uses_1_to_500_and_indexed_history(self):
|
||||
def test_blackjack_uses_1_to_2000_and_indexed_history(self):
|
||||
self.command("/摩拉")
|
||||
account = self.state()
|
||||
account["balance"] = 2500
|
||||
person_key = self.engine.ledger.resolve_person(
|
||||
"group-a",
|
||||
"Alice",
|
||||
)["person_key"]
|
||||
self.engine.ledger._save_player_account(
|
||||
"group-a",
|
||||
person_key,
|
||||
account,
|
||||
MODULE.utc_now(),
|
||||
)
|
||||
def tied_deck(cards):
|
||||
first = [9, 6, 8, 7]
|
||||
cards[:] = first + [card for card in cards if card not in first]
|
||||
@@ -164,17 +177,36 @@ class Release063GameTests(unittest.TestCase):
|
||||
"shuffle",
|
||||
side_effect=tied_deck,
|
||||
):
|
||||
started = self.command("/21点开始500")
|
||||
started = self.command("/21点开始2000")
|
||||
self.assertTrue(started.startswith("alice #1:"))
|
||||
self.assertEqual(self.state()["balance"], 500)
|
||||
settled = self.command("/21点停牌")
|
||||
self.assertTrue(settled.startswith("alice #1:"))
|
||||
self.assertIn("平局", settled)
|
||||
self.assertEqual(self.state()["balance"], 1000)
|
||||
self.assertEqual(self.state()["balance"], 2500)
|
||||
history = self.command("/游戏记录")
|
||||
self.assertIn("alice #1:21点 平局", history)
|
||||
self.assertIn("下注 500,返还 500", history)
|
||||
self.assertIn("1~500", self.command("/21点开始501"))
|
||||
self.assertIn("下注 2000,返还 2000", history)
|
||||
rates = self.command("/21点胜率")
|
||||
self.assertIn("Taffy:胜 0 / 负 0 / 平 1,胜率 0.00%", rates)
|
||||
self.assertIn(
|
||||
"alice:胜 0 / 负 0 / 平 1,胜率 0.00%;净赚净亏 0 摩拉",
|
||||
rates,
|
||||
)
|
||||
account = self.state()
|
||||
account["wins"] = 12
|
||||
account["losses"] = 15
|
||||
account["ties"] = 3
|
||||
self.engine.ledger._save_player_account(
|
||||
"group-a",
|
||||
person_key,
|
||||
account,
|
||||
MODULE.utc_now(),
|
||||
)
|
||||
wallet = self.command("/摩拉")
|
||||
self.assertIn("21点 无进行中的牌局;已结算胜 0 / 负 0 / 平 1", wallet)
|
||||
self.assertNotIn("已结算胜 12 / 负 15 / 平 3", wallet)
|
||||
self.assertIn("1~2000", self.command("/21点开始2001"))
|
||||
|
||||
def test_multiplayer_blackjack_table_guides_turns_and_settles(self):
|
||||
self.assertIn("黑杰克Blackjack 21点牌桌", self.command("/21点 多人 启动"))
|
||||
@@ -225,26 +257,54 @@ class Release063GameTests(unittest.TestCase):
|
||||
"bob #1:21点 你输了",
|
||||
self.command("/游戏记录", sender="Bob"),
|
||||
)
|
||||
rates = self.command("/21点 胜率")
|
||||
self.assertIn("本群已结算 2 手", rates)
|
||||
self.assertIn(
|
||||
"Taffy:胜 1 / 负 1 / 平 0,胜率 50.00%;净亏 100 摩拉",
|
||||
rates,
|
||||
)
|
||||
self.assertIn(
|
||||
"alice:胜 1 / 负 0 / 平 0,胜率 100.00%;净赚 200 摩拉",
|
||||
rates,
|
||||
)
|
||||
self.assertIn(
|
||||
"bob:胜 0 / 负 1 / 平 0,胜率 0.00%;净亏 100 摩拉",
|
||||
rates,
|
||||
)
|
||||
self.assertIn("+200:21点 获胜", self.command("/金币"))
|
||||
self.assertIn(
|
||||
"-100:21点 失败",
|
||||
self.command("/金币", sender="Bob"),
|
||||
)
|
||||
|
||||
def test_prize_draw_has_indexed_history(self):
|
||||
def test_wish_pool_has_indexed_history(self):
|
||||
self.command("/摩拉")
|
||||
account = self.state()
|
||||
account["balance"] = 2000
|
||||
person_key = self.engine.ledger.resolve_person(
|
||||
"group-a",
|
||||
"Alice",
|
||||
)["person_key"]
|
||||
self.engine.ledger._save_player_account(
|
||||
"group-a",
|
||||
person_key,
|
||||
account,
|
||||
MODULE.utc_now(),
|
||||
)
|
||||
self.engine.ledger.connection.commit()
|
||||
with mock.patch.object(
|
||||
self.engine.local.players.rng,
|
||||
"randrange",
|
||||
return_value=99,
|
||||
):
|
||||
first = self.command("/抽奖10")
|
||||
second = self.command("/抽奖10")
|
||||
first = self.command("/许愿1000")
|
||||
second = self.command("/抽奖1000")
|
||||
|
||||
self.assertTrue(first.startswith("alice #1:"))
|
||||
self.assertTrue(second.startswith("alice #2:"))
|
||||
history = self.command("/gamehistory")
|
||||
self.assertIn("alice #2:抽奖 未中奖", history)
|
||||
self.assertIn("alice #1:抽奖 未中奖", history)
|
||||
self.assertIn("alice #2:许愿池 沉底", history)
|
||||
self.assertIn("alice #1:许愿池 沉底", history)
|
||||
|
||||
def test_transfer_requires_initialized_target_and_replays_once(self):
|
||||
self.assertIn("摩拉 1000", self.command("/摩拉"))
|
||||
@@ -345,21 +405,21 @@ class Release063GameTests(unittest.TestCase):
|
||||
"randrange",
|
||||
return_value=99,
|
||||
):
|
||||
self.command("/抽奖10")
|
||||
self.command("/抽奖1000")
|
||||
with mock.patch.object(
|
||||
self.engine.local.players.rng,
|
||||
"randrange",
|
||||
return_value=0,
|
||||
side_effect=[950, 3000],
|
||||
):
|
||||
self.command("/抽奖10", sender="Bob")
|
||||
self.command("/抽奖1000", sender="Bob")
|
||||
|
||||
stats = self.command("/Taffy摩拉")
|
||||
|
||||
self.assertIn("玩家总下注 20,总返还 50", stats)
|
||||
self.assertIn("Taffy累计倒贴 30 摩拉", stats)
|
||||
self.assertIn("玩家总下注 2000,总返还 3000", stats)
|
||||
self.assertIn("Taffy累计倒贴 1000 摩拉", stats)
|
||||
self.assertIn("最大上贡者排行(含负数):", stats)
|
||||
self.assertIn("alice:上贡 10", stats)
|
||||
self.assertIn("bob:从Taffy赢走 40", stats)
|
||||
self.assertIn("alice:上贡 1000", stats)
|
||||
self.assertIn("bob:从Taffy赢走 2000", stats)
|
||||
self.assertIn("Taffy摩拉人数必须为 1~20", self.command("/Taffy摩拉 0"))
|
||||
|
||||
def test_open_round_and_history_survive_restart(self):
|
||||
|
||||
@@ -20,7 +20,8 @@ SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class ReleaseUpdateTests(unittest.TestCase):
|
||||
release = "beta 1.1.2.1019"
|
||||
release = "beta 2.0.5.1023"
|
||||
major_release = "beta 2.0.0.1021"
|
||||
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
@@ -112,15 +113,26 @@ class ReleaseUpdateTests(unittest.TestCase):
|
||||
announcement = MODULE.build_announcement(self.release, summary)
|
||||
self.assertIn(f"{self.release} 更新现已完成", announcement)
|
||||
self.assertIn("更新补偿:摩拉 x200", announcement)
|
||||
self.assertNotIn("原石 x1", announcement)
|
||||
self.assertNotIn("原石 x", announcement)
|
||||
self.assertNotIn("第二条", announcement)
|
||||
|
||||
def test_policy_release_announces_primogem(self):
|
||||
for release in ("beta 1.0.5.1016", "beta 1.1.0.1016"):
|
||||
for release in (
|
||||
"beta 1.0.5.1016",
|
||||
"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.major_release), 400)
|
||||
self.assertEqual(MODULE.primogem_compensation_amount(self.major_release), 2)
|
||||
announcement = MODULE.build_announcement(self.major_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)
|
||||
@@ -145,11 +157,24 @@ 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],
|
||||
f"Taffy 准备停机更新到 {self.release},会短暂离线一下;别慌,我很快回来喵",
|
||||
)
|
||||
self.assertIn("shutdown", delivered[0][3])
|
||||
self.assertEqual([state["balance"] for state in self.account_states()], [1000, 50])
|
||||
|
||||
def test_major_release_shutdown_message_is_ceremonial(self):
|
||||
message = MODULE.build_lifecycle_message(self.major_release, "shutdown")
|
||||
|
||||
self.assertEqual(
|
||||
message,
|
||||
(
|
||||
"Taffy 准备停机更新,本次更新是架构级大版本更新,"
|
||||
f"预期版本{self.major_release},离线的时候保持耐心迎接Taffy的归来哦~"
|
||||
),
|
||||
)
|
||||
|
||||
def test_release_update_grants_once_and_announces_once(self):
|
||||
delivered = []
|
||||
|
||||
@@ -193,12 +218,7 @@ class ReleaseUpdateTests(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0]["created_at"], applied_at.timestamp())
|
||||
primogem_matches = [
|
||||
event
|
||||
for event in state["primogem_events"]
|
||||
if event["reason"] == f"{self.release} 版本更新补偿"
|
||||
]
|
||||
self.assertEqual(primogem_matches, [])
|
||||
self.assertEqual(state["primogem_events"], [])
|
||||
self.assertEqual(self.announcement_marker()["state"], "delivered")
|
||||
self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
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
|
||||
ADAPTER = ServiceCommandAdapter(
|
||||
PROFILE,
|
||||
metadata={{"local_extension": PROFILE, "author": AUTHOR}},
|
||||
)
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
ADAPTER.register(context, COMMANDS)
|
||||
'''
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
@@ -43,6 +44,8 @@ GUI_SENDER_TIMEOUT_SECONDS = float(
|
||||
os.environ.get("JWEBOT_GUI_SENDER_TIMEOUT_SECONDS", "55")
|
||||
)
|
||||
GUI_SENDER_MAX_PAYLOAD_BYTES = 65536
|
||||
MULTI_REPLY_DELAY_MIN_SECONDS = 0.5
|
||||
MULTI_REPLY_DELAY_MAX_SECONDS = 1.0
|
||||
|
||||
BASE_WIDTH = 1440
|
||||
BASE_HEIGHT = 873
|
||||
@@ -1052,6 +1055,13 @@ def run() -> None:
|
||||
event_id=event_id,
|
||||
source=f"local-multi-{index}",
|
||||
)
|
||||
if index < len(texts):
|
||||
time.sleep(
|
||||
random.uniform(
|
||||
MULTI_REPLY_DELAY_MIN_SECONDS,
|
||||
MULTI_REPLY_DELAY_MAX_SECONDS,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logging.warning(
|
||||
"multi reply GUI delivery failed event_hash=%s error_type=%s",
|
||||
|
||||
+154
-30
@@ -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,60 +1713,89 @@ 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):
|
||||
event = history_event(
|
||||
message,
|
||||
observed_at=observed_at,
|
||||
group_fingerprint=group,
|
||||
ordinal=ordinal,
|
||||
)
|
||||
if (
|
||||
self.queue_enabled
|
||||
and event["mention_bot"]
|
||||
and message.sender != "unknown"
|
||||
and message.sender_confidence >= MIN_SENDER_CONFIDENCE
|
||||
):
|
||||
queue_event(event)
|
||||
queued += 1
|
||||
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,
|
||||
group_fingerprint=group,
|
||||
ordinal=ordinal,
|
||||
)
|
||||
if (
|
||||
self.queue_enabled
|
||||
and event["mention_bot"]
|
||||
and message.sender != "unknown"
|
||||
and message.sender_confidence >= MIN_SENDER_CONFIDENCE
|
||||
):
|
||||
queue_event(event)
|
||||
queued += 1
|
||||
write_history_event(event)
|
||||
appended += 1
|
||||
logging.info(
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user