37 Commits
Author SHA1 Message Date
JWeBot Contributors 0ffd0b2a16 Release beta 2.0.5.1023
check / local-gate (push) Failing after 1m9s
2026-10-23 17:19:23 +08:00
JWeBot Contributors 6375ab5c79 Release beta 2.0.4.1023 2026-10-23 17:18:29 +08:00
JWeBot Contributors 22f5240fa7 Release beta 2.0.2.1021
check / local-gate (push) Failing after 1m56s
2026-10-21 18:17:12 +08:00
JWeBot Contributors 645a282b22 Release beta 2.0.1.1021
check / local-gate (push) Failing after 5m16s
2026-10-21 17:38:47 +08:00
JWeBot Contributors e4365841ed Release beta 2.0.0.1021
check / local-gate (push) Failing after 1m38s
2026-10-21 17:05:09 +08:00
JWeBot Contributors 5a3c4fb94c Release beta 1.2.8.1019
check / local-gate (push) Failing after 2m49s
2026-10-19 22:56:36 +08:00
JWeBot Contributors 5e309f0b8f Release beta 1.2.7.1019
check / local-gate (push) Failing after 1m53s
2026-10-19 22:41:48 +08:00
JWeBot Contributors ed2fd0200f Release beta 1.2.6.1019
check / local-gate (push) Failing after 2m29s
2026-10-19 22:29:36 +08:00
JWeBot Contributors 8670745235 Release beta 1.2.5.1019
check / local-gate (push) Failing after 1m49s
2026-10-19 22:14:35 +08:00
JWeBot Contributors eb31446076 Release beta 1.2.4.1019
check / local-gate (push) Failing after 41s
2026-10-19 18:17:51 +08:00
JWeBot Contributors 5300898bc3 Release beta 1.2.3.1019
check / local-gate (push) Failing after 1m6s
2026-10-19 18:07:17 +08:00
JWeBot Contributors ac630576c7 Release beta 1.2.2.1019
check / local-gate (push) Failing after 59s
2026-10-19 17:40:46 +08:00
JWeBot Contributors 3a7c11e44d Release beta 1.2.1.1019
check / local-gate (push) Failing after 16m58s
2026-10-19 17:26:44 +08:00
JWeBot Contributors e6d6ddf1c6 Release beta 1.2.0.1019
check / local-gate (push) Failing after 15s
2026-10-19 17:09:40 +08:00
JWeBot Contributors ddebe075d7 Release beta 1.1.2.1019
check / local-gate (push) Failing after 53s
2026-10-19 16:56:59 +08:00
JWeBot Contributors 522e349491 Release beta 1.1.1 stacked scan boosts 2026-10-16 17:31:45 +08:00
JWeBot Contributors 35ef82f14f Release beta 1.1.0 scan and duplicate fixes 2026-10-16 17:14:41 +08:00
JWeBot Contributors b1d8e4e8e7 Release beta 1.0.5 primogem economy 2026-10-16 16:46:44 +08:00
JWeBot Contributors 127fea8cd0 Release beta 1.0.4 update settlement
check / local-gate (push) Failing after 1m7s
2026-10-16 16:10:19 +08:00
JWeBot Contributors bf6b738baa Release beta 1.0.3 Mora wallet updates 2026-10-16 15:44:34 +08:00
JWeBot Contributors 22edf6ff15 Cache OCR-negative bubbles
check / local-gate (push) Failing after 42s
2026-10-16 11:38:03 +08:00
JWeBot Contributors 572159f571 Fix OCR thread test isolation
check / local-gate (push) Failing after 17m10s
2026-10-16 11:19:55 +08:00
JWeBot Contributors 99a251c19a Limit OCR runtime threads
check / local-gate (push) Failing after 44s
2026-10-16 11:16:23 +08:00
JWeBot Contributors 3fa04755d0 Add multiplayer blackjack table flow
check / local-gate (push) Failing after 51s
2026-10-16 11:03:32 +08:00
JWeBot Contributors afc71e63a7 Release beta 1.0.0 baseline
check / local-gate (push) Failing after 1m35s
2026-10-16 01:07:07 +08:00
JWeBot Contributors bf4f28f48e Add coin leaderboard command
check / local-gate (push) Failing after 1m34s
2026-10-15 19:00:29 +08:00
JWeBot Contributors 148ae672f4 Install autochess data in runtime
check / local-gate (push) Failing after 1m39s
2026-10-15 18:48:48 +08:00
JWeBot Contributors bbddb5be6f Add virtual coin loans
check / local-gate (push) Failing after 42s
2026-10-15 18:43:49 +08:00
JWeBot Contributors 2bb2effc04 Externalize autochess game data
check / local-gate (push) Failing after 58s
2026-10-15 18:37:41 +08:00
JWeBot Contributors 5125b827c0 Release alpha 0.6.8 autochess legacy state fix
check / local-gate (push) Failing after 1m33s
2026-10-15 18:18:00 +08:00
JWeBot Contributors 3dea42b268 Release alpha 0.6.7 autochess pacing
check / local-gate (push) Failing after 32s
2026-10-15 18:08:55 +08:00
JWeBot Contributors a74147e7f1 Release alpha 0.6.6 autochess flow
check / local-gate (push) Failing after 40s
2026-10-15 17:10:28 +08:00
JWeBot Contributors 9fff57134f feat: add two-human AI autochess mode
check / local-gate (push) Failing after 50s
2026-10-15 16:08:59 +08:00
JWeBot Contributors e713ff369a feat: add CS Major autochess tournament
check / local-gate (push) Failing after 2m1s
2026-10-15 15:17:32 +08:00
JWeBot Contributors f07ae653c7 feat: add multiplayer lottery and game records
check / local-gate (push) Failing after 1m35s
2026-10-15 13:57:32 +08:00
JWeBot Contributors 0731e5cebb fix: bound chat latency and clarify blackjack flow 2026-10-15 13:37:57 +08:00
JWeBot Contributors f7fedc54e4 release: alpha 0.6.1 local player plugins and capture reconciliation
check / local-gate (push) Failing after 3m24s
2026-10-15 05:01:40 +08:00
155 changed files with 24174 additions and 1010 deletions
+1
View File
@@ -67,6 +67,7 @@ id_ed25519*
# Sensitive local notes
homeserver-apikey-*.md
debug-*.md
# macOS/editor
.DS_Store
+20 -2
View File
@@ -4,9 +4,11 @@ These rules are mandatory for every automated change in this repository.
## Versioning
- Use `alpha X.Y.Z.MMDD`.
- Use `beta X.Y.Z.MMDD` for current beta releases.
- Python package versions use PEP 440 pre-release form, for example
`X.Y.Zb1+MMDD`.
- Small changes increment only `Z`.
- Do not move to a new minor version without explicit user approval.
- Do not move to a new minor or stable version without explicit user approval.
- Keep `VERSION`, root `pyproject.toml`, package versions, package
`__version__`, `README.md`, and `CHANGELOG.md` synchronized.
@@ -22,6 +24,22 @@ These rules are mandatory for every automated change in this repository.
or adaptive restart-stability gates.
- If a deployment gate fails, verify rollback state before retrying.
## Mandatory Release Settlement
- Every successful production update must run `jwebot_release_update.py`
after cutover gates pass and before the release is declared complete.
- Each release grants exactly 200 Mora to every account that exists when the
settlement starts. The grant and account movement are idempotent by version.
- The first bullet in each release's `CHANGELOG.md` section must be one concise,
user-facing sentence suitable for the group update announcement.
- After crediting accounts, send the version, first changelog bullet, and
`Mora x200` compensation notice through the guarded group sender.
- Never blindly retry an uncertain announcement. Verify the group visually,
then use `--retry-uncertain-announcement` only when the prior message did not
land.
- A production release is incomplete until compensation, announcement,
SQLite integrity, empty queues, Edge health, and service stability pass.
## Configuration And Access
- Real profiles live under `~/.config/jwebot/`, never in the repository.
+972 -12
View File
@@ -1,5 +1,402 @@
# 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 会偶尔自然冒泡,抛出限时答题奖励摩拉,并在停机、上线和更新就绪时主动说明状态。
- Use recent safe group context to replace some hourly scan slots with an
ambient chat message during active daytime windows.
- Add a 200+ question CS, Apex, Overwatch, and LoL trivia bank with
five-minute `/回答 A/B/C/D` answers, recent-question avoidance, and
rarity-based Mora rewards.
- Limit ambient chat with deterministic probability, multi-hour cooldown, and
minimum recent-speaker activity so proactive replies feel occasional.
- Send guarded shutdown and startup lifecycle notices during production
cutover before the normal ready-stage update announcement and compensation.
## beta 1.1.1.1016 - 2026-10-16 17:19:45
- 修复扫码加权重复购买只刷新不叠加的问题。
- Change scan boost semantics to stack each successful purchase as an
independent 24-hour +5 weight entry.
- Preserve legacy boost entries by deriving their 24-hour expiry from their
creation time.
## beta 1.1.0.1016 - 2026-10-16 17:04:25
- 修复 OCR 偶发重复识别消耗型命令导致重复回复的问题,并完善摩拉/扫码榜展示。
- Extend OCR semantic duplicate suppression for resource-mutating local
commands so a visible command bubble re-detected after the short 8 second
window is still ignored.
- Keep ordinary local commands on the short duplicate window to avoid blocking
intentional repeated game/query commands.
- Show scan-boost coupon count in `/摩拉`, and make `/扫码榜` list every
configured scan candidate plus next-hour scan weights.
- This major settlement grants each existing account both 200 Mora and
1 primogem.
## beta 1.0.5.1016 - 2026-10-16 16:30:44
- 新增原石系统:成就可获得原石,并开放祈愿、扫码加权、Taffy发话和原石榜。
- Add primogem account state, primogem movement history, wish records, scan
boost coupons, Taffy speech credits, scheduled speech delivery, and
primogem-source ranking without a SQLite schema migration.
- Award primogems from blackjack natural 21, blackjack 20-to-21 hit challenges,
Taffy blackjack win streaks, and personal consecutive lottery high-hit pairs.
- Extend post-release settlement so this version grants each existing account
both 200 Mora and 1 primogem, and show `/version` update time down to seconds.
## beta 1.0.4.1016 - 2026-10-16
- 新增版本更新补偿流程:每次生产更新后向所有既有账户发放 200 摩拉,并在目标群发布更新公告。
- Add a version-idempotent post-release settlement tool that creates an online
SQLite backup, grants Mora through the production account schema, records a
version-labelled movement, and tracks announcement delivery independently.
- Run release settlement only after coordinated cutover and rollback gates are
disarmed, keeping a healthy runtime active when GUI delivery is uncertain.
- Require the first changelog bullet to be a concise user-facing announcement
summary and require visual confirmation before explicitly retrying an
uncertain announcement.
## beta 1.0.3.1016 - 2026-10-16
- Rename the virtual currency user-facing copy to Mora (`摩拉`) while keeping
the existing `/金币`, `/金币排行`, and `/Taffy金币` commands as aliases.
- Add recent account movement details to `/摩拉` and `/金币`, covering the
latest transfer, loan, repayment, lottery, draw, and blackjack net changes.
- Suppress short-window semantic duplicate OCR events so one visible command
bubble cannot trigger the same local reply multiple times under new event IDs.
## beta 1.0.2.1016 - 2026-10-16
- Cache OCR-negative visible bubbles and reuse stable visual signatures as the
OCR cache key so unreadable or lightly re-rendered bubbles do not trigger
repeated 10+ second RapidOCR passes during active chat windows.
## beta 1.0.1.1016 - 2026-10-16
- Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the
adaptive OCR recorder no longer saturates CPU after restart and slow recovery
windows do not stretch local command detection into 10+ second scans.
## beta 1.0.0.1016 - 2026-10-16
- Promote JWeBot from alpha to beta after the local, staging, privacy, and
production deployment gates stabilized.
- Consolidate the current reviewed local command set, including virtual coin
transfer, loans, repayment, group leaderboard, multiplayer lottery,
blackjack, Steam queries, and CS Major autochess.
- Externalize CS Major autochess game content into package JSON data files for
cards, coaches, narratives, draft/shop/economy rules, match formats, and
MR15/overtime settings.
- Add a maintained command catalog and keep debug session notes out of the
repository by default.
- Keep release validation on `tools/sync-local-extensions.py --check`,
`tools/check-local.sh`, and `tools/check-secrets.py --history`.
## alpha 0.6.11.1015
- Add `/大富翁榜`, `/金币排行`, `/金币排行榜`, and `/richlist` for a
group-scoped virtual coin leaderboard. Rankings are read-only, sorted by
current coin balance, recover display names from configured aliases and
observed profiles, and mark active loan principal when present.
- Split leaderboard command ownership into a dedicated reviewed
`coin-ranking` local extension.
## alpha 0.6.10.1015
- Add virtual coin loans with `/贷款 [金额]` and `/loan amount`, plus
repayments through `/还款 [金额|全部]` and `/repay`. Loans are stored in
the same group-scoped virtual coin account, capped at 500 coins, limited to
one active loan per account, and replay-safe through the existing ledger
transaction path.
- Split loan command ownership into a dedicated reviewed `coin-loan` local
extension so the single-player game plugin stays under the command-count
validation limit.
## alpha 0.6.9.1015
- Move CS Major autochess public game data out of `cs_autochess.py` and into
package data files under `jwebot_official_extensions/data/cs_autochess/`.
Player cards, coach cards, narrative fragments, shop/economy odds, draft
pacing, synergy thresholds, match formats, MR15/overtime settings, and
special event rewards are now data driven.
- Add startup validation for the externalized data so missing fields,
duplicate card IDs, invalid schema versions, or an undersized narrative
library fail fast before the command engine runs.
- Add regression coverage that verifies the JSON files are packaged, readable,
and consistent with the runtime card, coach, match-format, and narrative
inventories.
- Add virtual coin transfers with `/转账 对方 金额` and `/transfer name amount`.
Transfers are single-transaction ledger updates, require the receiver to
initialize `/金币` first, reject self-transfers, and replay safely by event ID.
## alpha 0.6.8.1015
- Add an atomic compatibility path for old active CS autochess runs. When a
group still has a pre-schema-3 active tournament, `/自走棋开始` now archives
that old run as cancelled and opens a fresh schema-3 season in the same
SQLite transaction.
- Preserve old story and battle archive rows while ensuring the new season is
the only active autochess run selected by future commands.
## alpha 0.6.7.1015
- Expand CS Major autochess content to 100+ player cards and an 18-coach
metadata pool while keeping the opening coach draft to 10 visible choices.
- Replace abstract training targets with real round-robin training rounds.
Each round resolves participant-vs-participant or participant-vs-AI BO1
scrims, then opens an operation window for D rolling, locking, lineup swaps,
tactics, and readiness.
- Replace fixed score bands with deterministic MR15 round simulation. Map
scores now come from individual round outcomes, including 15-15 overtime,
instead of direct mappings such as 16-6 or 16-9.
- Add a 120+ line local narrative library for openings, streaks, pauses,
clutches, economy beats, closes, training hooks, and media/sponsor
challenges so match reports vary across maps.
- Hide raw target/performance calculation text from training, match, and
challenge reports while preserving structured battle facts in the archive.
- Start drafted players as two-star素材 and rebalance roster completion toward
low/mid-cost cards, leaving upgrades and synergies to the shop loop.
## alpha 0.6.6.1015
- Rebuild CS Major autochess around multi-message hosting: tournament
welcomes, seed order, draft boards, status panels, training results, match
reports, standings, brackets, and awards are emitted as separate bounded
replies.
- Replace instant full rosters with three draft rounds: round one selects from
a fixed 10-coach pool, rounds two and three draft players, then each team is
completed to five starters, one coach, and three bench slots.
- Run the full event as training camp, BO3 single-round-robin group stage,
one media/sponsor challenge after group round one or two, BO5 semifinals,
BO5 final, and final awards.
- Let readiness advance phases automatically. Group matches wait for all real
players, while knockout matches only wait for real players still in the
current pairing, so eliminated players are not prompted to prepare.
- Add local multi-text reply support across the SDK, official extension
adapter, Edge response contract, and recorder GUI sender while preserving
single-message compatibility.
## alpha 0.6.5.1015
- Extend the AI test path for CS Major autochess to support two-human games:
`/自走棋加AI战队` now adds AI 战队 A and AI 战队 B when exactly two
real players have registered.
- Keep the same command for two- and three-human test modes. The tournament
engine selects 2 人+2 AI or 3 人+1 AI from the current registration count.
- Lock the roster after AI teams join, seed the four-team bracket as 1v4 and
2v3, and keep AI teams out of the human preparation order while preserving
independent AI operations and story archives.
## alpha 0.6.4.1015
- Add a reviewed 2-8 player CS Major autochess extension with a fixed
five-player starting lineup, one always-active coach, an eight-slot bench,
and a six-candidate shop that shows complete game data.
- Ship a fixed 60-player local pool with rifle, sniper, and in-game-leader
roles; team and nation synergies; shared copy counts; and deterministic
three-copy/two-star and nine-copy/three-star upgrades.
- Keep all five player slots occupied. Lineup changes are one-for-one swaps,
starters cannot be sold directly, and coach upgrades only add a 2%-10%
team bonus.
- Add player-selected firepower, stability, and clutch tactics. Resolve
stability over firepower, firepower over clutch, clutch over stability,
plus sniper/rifle/leader role counters before bounded +/-2% map variance.
- Generate the competition format once at tournament start: two players go
directly to the final, three play a round robin for two final places, four
use seeded 1v4 and 2v3 semifinals, and five to eight play a complete round
robin for four semifinal places.
- Add an explicit three-human-plus-one-AI test mode. The host can add one
deterministic local AI as seed four; it buys players, upgrades its coach,
swaps its strongest lineup, and selects a counter tactic without an online
model.
- Give the bot a persistent host state machine: welcome and format briefing,
rotating human preparation order, turn enforcement, explicit handoffs,
round introductions, matchup calls, overtime narration, advancement, and
championship presentation.
- Run group matches as BO3 and knockout matches as BO5. Simulate every map
under MR15, including 15-15 overtime, 19-17/22-20 scores, and separately
archived overtime stories.
- Persist run state, idempotent action responses, structured battles, and
every generated story in serialized SQLite transactions. Invalid archives
roll back the full action.
## alpha 0.6.3.1015
- Replace instant personal lottery tickets with one persistent multiplayer
lottery round per group: start, register/update, status, draw, and cancel.
- Treat each ticket as three distinct digits from 0-9. Sort digits before
storage so `347` and `743` represent the same selection.
- Accept spaced and compact OCR forms. Ticket stakes are 1-500 coins with a
default of 10 when omitted.
- Return 0%, 50%, 120%, or 300% of the stake for matching zero, one, two, or
three drawn digits. All settlement values remain integer coins.
- Commit entry updates, stake differences, all-player payouts, cancellation
refunds, and event replay responses in serialized SQLite transactions.
- Raise the blackjack stake range to 1-500 coins.
- Add per-game personal sequence numbers and `/游戏记录` (`/gamehistory`) for
prize draws, lottery rounds, and blackjack hands.
- Prefix game output with the configured player label and game number, such as
`Player #1:...`; aliases share the same counter and account.
## alpha 0.6.2.1015
- Bound direct DeepSeek SSE reads to 30 seconds of total wall time in addition
to the existing 18-second socket inactivity timeout.
- Stop reading as soon as the SSE `[DONE]` marker arrives instead of waiting
for the upstream connection to close.
- Prevent a trickling or unfinished upstream response from occupying both
generation workers for about 15 minutes and delaying later final replies.
- Preserve fast ACK delivery, three-attempt retry behavior, two generation
workers, and strict final-reply ordering.
- Treat `/21点 开始 100` as a 100-coin stake and accept OCR forms with missing
whitespace, such as `/21点开始100` and `/blackjackstart100`.
- Keep blackjack play as explicit steps: start and deal first, then hit or
stand in a separate command. Combined forms return this workflow without
opening a game or deducting funds.
- Prefix blackjack hands, status and settlement replies with the current
group-scoped player's configured display label, such as `player:...`.
## alpha 0.6.1.1015
- Add three reviewed plugins: local context summaries for 100/200/300 messages,
person and alias lookup, and single-player virtual coin games.
- Provide corresponding Chinese and English commands, including `/context`,
`/people`, `/person`, `/wallet`, `/draw`, `/lottery`, and `/blackjack`.
- Grant each group-scoped person 1000 virtual coins once; persist account state,
private blackjack decks, timeout/forfeit rules, and win/loss statistics.
- Commit funds, game state and event replay responses in one SQLite transaction;
retries never debit twice, and aliases share the canonical person's account.
- Reconcile the message image after a two-second idle wait so a missed final
XDamage notification does not strand an isolated message; drain buffered Xlib
events before waiting on the socket. Preserve visual deduplication and guards.
- Keep the generated feature list within the sender's 900-character boundary.
- Document local extractive-summary coverage, OCR identity limitations,
game payouts, persistence and rollback behavior.
## alpha 0.6.0.1015
- Replace repository-local and hardcoded deployment values with independent,
@@ -32,18 +429,581 @@
backup; pre-public operational history remains only in protected local
archives.
## Pre-Public Milestones
## alpha 0.5.11.1015
Earlier alpha work established:
- Reduce the conservative follow-latest scroll burst from a fixed `25ms` to
`1ms` between each of the existing 30 wheel events.
- Reuse the XDamage producer's verified WeChat window identifier in the
adaptive ready-layout gate instead of rediscovering the same window before
each screenshot.
- Preserve the shared GUI lock, ready-window geometry gate, second screenshot,
and loaded-bottom visual confirmation.
- Add privacy-safe per-step capture telemetry and regression coverage for the
optimized slow path.
- adaptive OCR and XDamage capture;
- guarded GUI send with visual confirmation;
- durable ACK/final delivery ordering;
- privacy-safe latency telemetry;
- capability-limited SDK contracts;
- signed extension artifacts and isolated Host execution;
- reviewed command ownership and generated adapters;
- coordinated production backup, health gates, and rollback.
## alpha 0.5.10.1015
Private host, person, address, account, incident, and deployment details are
intentionally omitted from the public changelog.
- Prioritize queued fast acknowledgements and local-command replies over queued
final replies in the guarded GUI sender.
- Keep the active GUI send non-preemptive and preserve strict FIFO ordering
within each priority class through a monotonic sequence number.
- Raise the adaptive recorder CPU quota from `150%` to `250%` to reduce
screenshot subprocess stalls under OCR and GUI contention.
- Add deterministic coverage for ACK/local priority, final FIFO ordering,
single-sender serialization, and clean worker shutdown.
## alpha 0.5.9.1014
- Preserve both capture call contracts in the adaptive ready-window wrapper:
zero arguments for startup/polling and one verified window identifier for the
optimized XDamage producer.
- Fix the alpha 0.5.8 internal `CaptureProducerError` restart loop that began on
the first relevant XDamage event.
- Add direct regression tests for both ready-gated capture signatures.
## alpha 0.5.8.1014
- Recover one unseen bottom-most bot trigger when a viewport resync has no
visual overlap, instead of silently dropping the new command.
- Persist a bounded recent-trigger visual history so an old command is not
replayed by a later no-overlap resync; ordinary messages remain fail-closed.
- Seed that history from one startup baseline before enabling recovery, so a
service restart cannot replay a visible pre-existing command.
- Reuse the XDamage monitor's verified WeChat window identifier and the
maximize gate's geometry result, removing one redundant window lookup and
one redundant geometry subprocess from each changed-frame capture.
- Add regression coverage for unseen-trigger recovery, seen-trigger replay
suppression, signature migration, and the optimized capture path.
## alpha 0.5.7.1014
- Replace the single direct-request worker with two parallel generation workers
and one strictly ordered GUI delivery worker for ordinary chat and Steam
requests.
- Keep final replies in durable `created_at, event_id` order even when a newer
provider call completes first; failed acknowledgements and exhausted requests
remain terminal and do not block later replies.
- Separate generation and delivery retry counters so each phase retains its
complete retry budget.
- Wake generation and delivery workers immediately on enqueue, generation
completion, and acknowledgement instead of adding a one-second idle polling
delay to the normal path.
- Split direct-queue telemetry into `direct_generation` and `direct_delivery`
phases and add concurrency, ordering, ACK-race, retry, Steam, and duplicate
regression coverage.
## alpha 0.5.6.1014
- Preserve both pre-draft and post-draft group fingerprint gates while
capturing only the configured title region instead of decoding the complete
WeChat window twice per GUI send.
- Keep the previous full-window fingerprint path as a compatibility fallback
for older adaptive upstream modules.
- Add GUI sender coverage for the title-region primary path, full-window
fallback, and visual group-mismatch fail-closed handling.
## alpha 0.5.5.1014
- Accept incoming WeChat bubbles up to `900px` wide so long single-line bot
mentions are not discarded before OCR; preserve position, height, area, and
outgoing-bubble exclusion gates.
- Wait for adaptive OCR warmup and a first baseline capture from the current
service start before reporting `adaptive ready` or successful deployment.
- Fail and roll back deployment if the adaptive service exits or cannot
establish its recorder baseline within 90 seconds.
- Retry the WeChat ready-window gate for up to 30 seconds and discover the
exact `Weixin` window by both class and title before failing closed.
- Pin production privilege switching and X11 inspection to verified absolute
executable paths so a restricted root `PATH` cannot masquerade as a missing
WeChat window.
## alpha 0.5.4.1014
- Stabilize visible-message sequence alignment with thresholded binary
sender/body fingerprints while retaining exact RGB keys for OCR cache
invalidation.
- Reset 0.5.3 exact-visual recorder state to a safe no-reply baseline during
the signature migration.
- Prefix coordinated deploy phases with `[JWeBot]`, report explicit
`starting`, `ready`, and `active` states, and run user installation from the
target home to avoid inherited `/root` working-directory warnings.
## alpha 0.5.3.1014
- Align visible message sequences with existing exact bubble visual
fingerprints instead of OCR text, preventing scroll/resync OCR drift from
dropping a newly mentioned slash command.
- Reset legacy text-signature recorder state to a no-reply visual baseline on
startup, avoiding duplicate replay during the signature-scheme migration.
- Preserve repeated identical commands as separate visual occurrences.
## alpha 0.5.2.1014
- Print an ASCII pixel-art project signature only after the coordinated
production deployer passes every cutover gate and disables rollback.
## alpha 0.5.1.1014
- Exclude the static bottom-left input hint from the realtime text ROI so an
empty WeChat input box is not misclassified as dirty.
- Label the corrected ROI as `input_text_core_v2` in bridge telemetry, keeping
pre-fix and post-fix production evidence distinguishable.
## alpha 0.5.0.1014
- Move visible WeChat delivery onto a persistent in-process GUI sender queue
hosted by the adaptive recorder, avoiding per-message bridge subprocesses for
ACK, local-command replies, and final deliveries.
- Add a private AF_UNIX sender socket so Edge delivery helpers submit final
replies to the same serialized GUI queue instead of racing the input box.
- Combine clear, paste, ROI draft flag, post-draft group check, Enter send,
ROI clear confirmation, and send-button fallback into one bridge call.
- Confirm the input ROI is empty after Ctrl-A/Backspace before using it as the
paste baseline, preventing repeated `喵......` ACK drafts from being mistaken
for paste failures.
## alpha 0.4.15.1014
- Force adaptive recorder logging to INFO so GUI bridge timing telemetry is
visible on ACK and final delivery paths.
- Allow ROI, poll, pixel-threshold, flag, and coordinate fields through the
packaged runtime telemetry sanitizer.
## alpha 0.4.14.1014
- Replace full-input send confirmation with a left-side input-text ROI flag so
draft and send confirmation poll only the region that should visibly change.
- Restore the guarded send order to Enter first, then click the scaled send
button only as a fallback when ROI clearing is not visually confirmed.
- Emit ROI coordinates, flag state, flag wait time, confirmation source, and
bridge stderr tail diagnostics for GUI delivery failures.
## alpha 0.4.13.1014
- Emit detailed guarded GUI delivery timings for lock acquisition, pre/post
draft screenshots, group fingerprint checks, bridge subprocess duration,
draft paste confirmation polling, send-button confirmation polling, Enter
fallback, callback hooks, and total send time.
- Return per-step timing metadata from the adaptive GUI bridge so ACK and final
delivery reports can attribute fixed waits, screenshot confirmation, VNC
input, and fallback paths without logging message text.
## alpha 0.4.12.1014
- Optimize guarded WeChat GUI delivery by clicking the scaled send button first
and polling for visual confirmation instead of waiting on fixed sleeps.
- Preserve the Enter-key fallback when the button path is not visually
confirmed.
## alpha 0.4.11.1014
- Add explicit `edge.fast_ack.*` and `recorder.fast_ack.*` telemetry names
alongside the existing ACK metrics, making fast acknowledgement latency
independently filterable without breaking historical dashboards.
## alpha 0.4.10.1014
- Cache configured person-alias rows once per group and make configured
identity persistence idempotent, eliminating thousands of redundant
`FULL`-synchronous SQLite commits during Edge startup.
- Reduce measured production host systemd socket readiness on the production database
snapshot from more than 90 seconds to about one second.
## alpha 0.4.9.1014
- Wait up to 45 seconds for the newly started Edge socket and health response
before deciding that a production cutover failed, while still failing
immediately if systemd marks the service failed.
## alpha 0.4.8.1014
- Keep the adaptive OCR process alive while WeChat is absent or awaiting login,
then resume capture against the ready main window without a systemd restart
storm.
- Install the reviewed `wechat_local_ocr.py` beside the adaptive runtime instead
of importing an independently managed root copy.
- Add a root-coordinated production deploy that records both systemd
topologies, backs up root and user files plus SQLite, disables both legacy OCR
units, and rolls back all layers when a health gate fails.
- Require empty OCR queues, a ready WeChat window, Edge socket health, stable
adaptive restart counts, and SQLite integrity before accepting a production
cutover.
- Preserve incoming service state in legacy install and upgrade tools; legacy
OCR now remains disabled unless an operator explicitly opts in.
- Prevent Extension Host imports from writing bytecode into verified releases,
and exclude only standard interpreter cache files from source packaging.
- Add deployment contract tests and shell-aware syntax validation to the local
release gate.
## alpha 0.4.7.1014
- Prewarm RapidOCR recognition and detection paths before live capture.
- Cache successful bubble recognition by bounded exact visual fingerprint,
recognize only new or changed crops, and periodically revalidate the cache.
- Persist OCR command events through atomic file replacement plus file and
directory `fsync`, then immediately wake the in-process responder while
retaining the one-second recovery scan.
- Name the three execution paths explicitly as `local-command`, `deepseek`, and
`openclaw` in Edge responses and privacy-safe routing telemetry.
- Add signed plugin description/help metadata, validate it through Host health,
generate bundled `/功能列表` output automatically, and update plugin creation
tooling and author documentation.
## alpha 0.4.6.1014
- Add a repeatable local performance baseline for OCR preprocessing, sequence
alignment, durable ACK/queue state, and real signed Host process execution.
- Add a structured telemetry-log analyzer with p50, p95, maximum, status, and
event-count summaries.
- Measure queue waiting, capture-buffer age, ACK end-to-end latency, and final
Edge delivery latency without recording message content.
- Establish the bottleneck order from local measurements and existing runtime
evidence: RapidOCR, provider generation, polling wakeups, then one-shot Host
startup; SQLite and sequence work remain below 2 ms p95 locally.
- Document the evidence, measurement gaps, and optimization order for later
`0.4.x` iterations.
## alpha 0.4.5.1014
- Close the approved repository-only Extension Host migration scope.
- Treat an invalid installed signed release as a startup failure rather than a
preferred-mode fallback; only missing or startup-unhealthy Hosts may fall
back.
- Enforce the end-to-end Host deadline after Kernel service execution and
expand transport-error telemetry.
- Document completed local work, explicitly deferred external operations, and
the next measurement-led OCR/queue/ACK performance-analysis phase.
## alpha 0.4.4.1014
- Add privacy-safe structured telemetry for OCR, routing, ACK preparation and
confirmation, queues, providers, Host processes, service brokering, and final
GUI delivery.
- Hash event and group identifiers, reject message-body fields, and retain only
low-cardinality process-local counts and latency summaries.
- Expose telemetry summaries through Edge health without making telemetry part
of delivery correctness.
- Instrument OCR capture/recognition/queue stages and distinguish ACK GUI
delivery from Edge confirmation.
- Document ACK ordering and the measurement points needed for future OCR,
queue, Host-startup, and provider latency optimization.
## alpha 0.4.3.1014
- Require a signed release to pass isolated Host health before atomic
activation; failed new releases are removed without switching `current`.
- Add explicit local-only health bypass for packaging environments.
- Complete real-process parity coverage for all 12 reviewed plugins, including
memory CRUD, both stateful games, dice challenge, rankings, history, and
Steam fast-ACK final generation.
- Fix Host protocol writes so broker messages bypass redirected plugin stdout
without exposing plugin output to the parent protocol.
## alpha 0.4.2.1014
- Load installed signed plugin manifests into a deterministic startup catalog
and freeze command ownership before routing.
- Add `in-process`, `prefer-host`, and `host-only` Edge execution modes with
subprocess or AF_UNIX Host transports.
- Route signed healthy plugins through the isolated Host while preserving
Kernel-direct commands and Steam fast-ACK queueing.
- Permit startup fallback for missing or unhealthy preferred Hosts, but never
replay an active invocation after a possible stateful service call.
- Add real subprocess end-to-end coverage from Edge through the generated
adapter, Host protocol, Kernel service broker, and final reply.
## alpha 0.4.1.1014
- Add bounded bidirectional JSON-line service brokering between isolated
Extension Host processes and Kernel-owned service profiles.
- Enforce declared profile ownership, call IDs, call count, message size, and
end-to-end process/socket deadlines.
- Add Host health inventory responses and broker support to both subprocess and
AF_UNIX clients.
- Keep service errors generic across the isolation boundary.
## alpha 0.4.0.1014
- Start the approved `0.4.x` local extension-host migration and performance
instrumentation milestone.
- Keep signing-pipeline integration, key rotation, PR rehearsal, target-machine
provisioning, deployment, and Linux verification deferred.
## alpha 0.3.1.1014
- Add OpenSSH `sshsig` publisher signatures for canonical extension release
records.
- Bind the publisher principal, release filename, artifact metadata, source
digest, manifest author, and exact file list through one signed payload.
- Require trusted publisher verification for activation, Host loading, and
rollback; unsigned paths now require an explicit local-test override.
- Persist the canonical signature record with each installed release and
re-verify it before execution or rollback.
- Add signing support to package, verify, activate, rollback, and standalone
release-signing tools.
- Keep production host provisioning, deployment, and systemd validation deferred.
## alpha 0.3.0.1014
- Close the `0.2.x` complete-function migration after the corrected granular
plugin ownership model passed the full local gate.
- Start the approved `0.3.x` baseline with Kernel-direct operational commands,
official-owned person memory, and 11 project-owned plugins.
- Keep Linux deployment and systemd verification explicitly deferred.
## alpha 0.2.6.1014
- Replace grouped plugin domains with one plugin per independently
understandable function.
- Keep dice and dice challenge as one complete project-authored collection.
- Make person-memory the only official-authored command plugin.
- Move feature list, status, and balance to unconditional Kernel-direct
routing.
- Add required manifest author metadata and release-record schema version 2.
- Generate 11 project plugin packages under the reviewed local-extension namespace and keep only
person-memory under `extensions/official`.
- Rename the canonical rollback switch to
`JWEBOT_LOCAL_EXTENSIONS_ENABLED`, while accepting the old environment key
as a compatibility fallback.
## alpha 0.2.5.1014
- Move the complete Steam provider into `jwebot-official-extensions`.
- Migrate fixed roster, group aliases, mode filtering, single-person lookup,
cache, API batching, proxy enforcement, and error classification.
- Preserve the existing public `SteamClient`, `SteamUnavailable`, API base, and
Mihomo default exports through the packaged Edge runtime.
- Keep the no-direct-network-fallback regression tests passing.
## alpha 0.2.4.1014
- Add full official-adapter versus compatibility-path parity tests.
- Cover every stateless command family, all memory operations, every
dice-challenge phase, guess-number lifecycle, RPS lifecycle, status, balance,
Steam generation, and restart persistence.
- Make parity and persistence checks part of the permanent local/CI gate.
## alpha 0.2.3.1014
- Route all known local commands through generated official SDK adapters by
default.
- Execute stateful behavior through named in-process service brokers without
exposing raw Ledger or providers to adapters.
- Preserve the Steam fast-ACK asynchronous path through the official adapter.
- Add `JWEBOT_OFFICIAL_EXTENSIONS_ENABLED=0` as a compatibility rollback to
direct command-engine dispatch.
- Keep unknown slash-command errors on the compatibility path.
- Install generated adapter modules with the official package.
## alpha 0.2.2.1014
- Add reviewed `service_profiles` as a narrow SDK/manifest capability.
- Generate real Host-loadable packages for all seven official function domains.
- Register every existing alias through SDK command adapters.
- Delegate complex stateful behavior through named Kernel service profiles
instead of exposing raw Ledger or provider objects.
- Add synchronization checks so official manifests, adapters, docs, and command
ownership cannot drift.
- Validate, source-scan, and package every official extension in CI.
## alpha 0.2.1.1014
- Move the complete local command engine out of the Edge runtime into the
installable `jwebot-official-extensions` package.
- Preserve all command replies, persistence, deterministic seeds, game state
machines, memory operations, rankings, status, balance, and Steam behavior.
- Inject Ledger, Steam, balance, reply-cleaning, identity, and memory-policy
brokers from Kernel instead of importing Edge internals.
- Add a unique ownership map covering every current command kind and alias.
- Add an authoritative migration matrix documenting every command subfunction
and non-command Kernel responsibility.
## alpha 0.2.0.1014
- Start the approved `0.2` full-function migration milestone.
- Move the complete Edge runtime into `jwebot-kernel`.
- Keep `jwebot_edge.py` as a compatibility CLI and module alias so existing
imports and runtime monkeypatches continue to affect implementation globals.
- Update tests and the installer for the packaged runtime.
- Preserve all existing command, queue, ledger, model, memory, broadcast, and
delivery behavior.
- Do not deploy or validate on Linux in this stage.
## alpha 0.1.11.1014
- Add a bounded AF_UNIX client matching the systemd socket-activated Host
design.
- Send one canonical JSON request, half-close the write side, and read one
bounded response.
- Fail closed on socket timeout, unavailable socket, malformed response, and
Host error status.
- Keep the socket client disconnected from the current production Edge path.
## alpha 0.1.10.1014
- Add uninstalled systemd socket/service templates for the extension Host.
- Specify a fixed unprivileged identity, AF_UNIX-only private network,
read-only system/release paths, empty capabilities, syscall filtering, and
CPU/memory/process/runtime limits.
- Add static sandbox-policy checks and unit tests.
- Require explicit target-host provisioning and systemd verification before
any future enablement.
## alpha 0.1.9.1014
- Add the Kernel-side extension Host client with a minimal inherited
environment and isolated process group.
- Enforce parent wall-time deadlines and kill the entire Host process group on
timeout.
- Bound request, response, and stderr sizes and reject malformed protocol
responses.
- Limit plugin stdout/stderr inside the Host.
- Hash both plugin log messages and field-name sets instead of emitting plugin
text.
- Test timeout child-process cleanup and environment-secret exclusion.
- Keep the client disconnected from the production Edge path.
## alpha 0.1.8.1014
- Add an installable `jwebot-extension-host` package.
- Re-verify installed release content, manifest, and source policy before import.
- Enforce exact declared command registration and freeze it before invocation.
- Add a bounded single-request JSON stdin/stdout protocol.
- Scrub inherited environment values and redirect plugin stdout away from IPC.
- Keep storage, HTTP, and scheduling brokers fail-closed.
- Return generic plugin failure classes without exception text.
- Do not connect the Host to Edge or install it on production host.
## alpha 0.1.7.1014
- Add verified release installation under full source-digest directories.
- Add locked, atomic `current` and `previous` activation pointers.
- Re-verify installed release file lists, records, and source content before
reuse.
- Add explicit local activation and rollback tools with no default production
path.
- Test rollback, tampered artifacts, tampered installed content, unsafe
pointers, and private installed-file permissions.
- Do not execute extension code or modify production host.
## alpha 0.1.6.1014
- Add deterministic, content-addressed extension release artifacts.
- Add canonical source and artifact SHA-256 records.
- Verify artifacts without extraction and reject path traversal, non-regular
members, expansion limits, file-list drift, and content tampering.
- Refuse replacement of an existing digest-addressed artifact with different
bytes.
- Build and verify the example extension artifact in Gitea Actions.
- Explicitly defer publisher signing and production host activation to a later phase.
## alpha 0.1.5.1014
- Add a static extension source-policy preflight before packaging.
- Reject unapproved imports, file/process/socket access, dynamic execution
builtins, and dunder attribute traversal.
- Enforce source count, size, UTF-8, syntax, and symlink limits.
- Make `validate-plugin.py` validate both manifest and source policy.
- Document that static policy is a CI prefilter, not a replacement for OS
process isolation.
## alpha 0.1.4.1014
- Restrict extension command names to Unicode word characters, dots, and
hyphens.
- Reject quote/newline template injection before plugin skeleton files are
created.
- Add manifest and generator regression tests while preserving Chinese command
names.
## alpha 0.1.3.1014
- Add a safe command-extension generator that refuses invalid IDs, invalid
commands, path traversal, and overwriting existing extensions.
- Add official and friend extension namespaces.
- Add an author guide, SDK API reference, and reusable AI-author prompt.
- Include author tooling and required documentation in the repository gate.
## alpha 0.1.2.1014
- Add a deterministic extension catalog that accepts reviewed manifests only
during startup.
- Reject duplicate plugin IDs and case-insensitive command ownership conflicts.
- Freeze registration before runtime command resolution.
- Keep catalog views immutable and require explicit command-token boundaries.
## alpha 0.1.1.1014
- Make `jwebot-kernel` the authoritative pure routing implementation used by
the existing Edge compatibility entrypoint.
- Preserve the existing `jwebot_edge.py` public function shapes for current
callers and tests.
- Update the production host installer to deploy and roll back the SDK and Kernel package
directories together with the compatibility scripts.
- Do not deploy or restart any production service in this repository change.
## alpha 0.1.0.1014
- Start the approved `0.1` milestone with installable `jwebot-sdk` and
`jwebot-kernel` packages.
- Add versioned event, reply, fact, and extension-result contracts.
- Add capability-limited context protocols that expose no GUI, group selector,
raw ledger, environment, shell, or unrestricted network access.
- Add strict plugin manifest validation and pure local/chat/cloud routing
primitives.
- Add a minimal `/hello` command extension and package boundary tests.
- Keep all existing root runtime entrypoints unchanged for production host compatibility.
## alpha 0.0.7.1014
- Move unit tests into `tests/`.
- Update tests to load root compatibility entrypoints from the repository root.
- Update local verification commands to discover tests from `tests/`.
## alpha 0.0.6.1014
- Add repository-safe configuration templates under `config/examples/`.
- Add `docs/configuration-guide.md` for runtime configuration ownership and
deployment destinations.
- Add `tools/check-config.sh` and include it in the local migration gate.
- Keep real secrets and production host runtime files outside Git.
## alpha 0.0.5.1014
- Add a Gitea Actions workflow that runs the standard local migration gate on
push and pull request.
- Add `requirements-dev.txt` for local and CI test dependencies.
- Document dependency installation in `README.md`.
## alpha 0.0.4.1014
- Move home-backup operational material into `ops/home-backup/`.
- Move the TigerVNC config snippet into `deploy/config/`.
- Keep backup material in the repository for reference while separating it from
the bot runtime path.
## alpha 0.0.3.1014
- Add `tools/check-local.sh` as the standard local migration gate.
- Wire the gate to compile Python entrypoints, validate shell scripts, reject
active emergency debug hooks outside their archive docs, and run the full
unittest suite.
- Document the local check command in `README.md`.
## alpha 0.0.2.1014
- Remove temporary emergency HTTP debug hooks from production Python entrypoints.
- Preserve removed hook context in `docs/debug/emergency-hooks-archive.md`.
- Add `docs/configuration-inventory.md` to document hardcoded addresses, paths,
service names, dimensions, models, and their migration policy.
- Keep historical incident markdown unchanged for auditability.
## alpha 0.0.1.1014
- Establish the repository version baseline for the private alpha migration.
- Move operational docs into `docs/`.
- Move deployment scripts, defaults, and systemd units into `deploy/`.
- Move the cloud responder prompt workspace into `agents/wechat-responder/`.
- Move the old direct OpenClaw watcher into `legacy/`.
- Keep current Python runtime entrypoints at repository root to preserve test and
deployment compatibility during the first migration step.
+59 -5
View File
@@ -8,7 +8,16 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。
当前版本:`alpha 0.6.0.1015`。
当前版本:`beta 2.0.5.1023`,更新时间:2026-10-23 17:16:12。
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major
自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则
已经外置为包内 JSON,知识问答题库也以 500 题 JSON 数据随 Kernel
发布,虚拟摩拉支持转账、贷款、还款和群内排行榜。
完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见
[CHANGELOG](CHANGELOG.md)。
### 运行流程
@@ -72,6 +81,24 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
```text
/功能列表
/上下文 [100|200|300](/context)
/人物列表 [页码](/people);/人物 [名称或别名](/person)
/摩拉(兼容 /金币、/wallet);/许愿池 金额(兼 /许愿、/抽奖、/draw)
/大富翁榜(/摩拉排行,兼容 /金币排行)
/转账 对方 金额(/transfer name amount)
/贷款 [金额](/loan amount);/还款 [金额|全部](/repay)
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
/21点 开始 [1~2000];开局后 /21点 要牌|停牌|状态|结束;/21点 胜率
/21点 多人 启动;/21点 加入;/21点 多人 开始;/21点 放200
/摩拉(兼 /原石);/原石祈愿;/原石榜;/扫码加权 人名(24小时+5);/Taffy发话 内容
/知识问答 [主题];500 题冷门题库,优先抽取少出现过的题并显示出现次数;支持主题:反恐精英、顶点英雄、守望先锋、英雄联盟、少女乐团派对、韩流、原神、缪斯学园偶像、我的世界;/回答 A
/游戏记录 [页码](/gamehistory)
/游戏记录 [页码](/gamehistory)
/游戏帮助(/games)
/自走棋开始|报名|开赛|状态|档案
/自走棋加AI战队(两名或三名真人测试模式)
/选1~6(教练轮为 /选1~10);/刷新;/锁定
/战术 火力|稳定|残局;/阵容;/上阵 备战编号 [首发位];/出售 备战编号;/结束回合
/丢骰子 [NdM]
/猜数字 开始 [上限]
/石头剪刀布 [开始|加入|出|状态|结算|结束]
@@ -166,7 +193,12 @@ sudo ./deploy/scripts/deploy-jwebot-production.sh /path/to/JWeBot jwebot
```
部署必须通过微信窗口就绪、队列清空、Edge 健康、SQLite 完整性和自适应
录制服务重启稳定性检查。旧 OCR 服务保留安装,但保持禁用。
录制服务重启稳定性检查。切换成功后,部署器按版本幂等地向每个既有账户
发放摩拉、写入账户流水,并使用当前 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 服务保留安装,但保持禁用。
### 仓库结构
@@ -240,9 +272,18 @@ visually confirmed GUI bridge.
Current version:
```text
alpha 0.6.0.1015
beta 2.0.5.1023
```
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 and the 500-question trivia bank are
now packaged as JSON data, and virtual coins support transfer, loans,
repayment, and group leaderboards.
## Runtime Flow
```text
@@ -308,7 +349,8 @@ migration, debug archives, and root deployment settings.
## Commands
The built-in command inventory is generated from reviewed plugin descriptors.
Common commands include:
See the full [command catalog](docs/command-catalog.md). Common commands
include:
```text
/功能列表
@@ -337,6 +379,13 @@ the final separator shown below:
/steam时长Dota2人=玩家名
```
Chinese aliases share the same implementations as their English counterparts.
Virtual coin funds, game state, transfers, loans, repayments, and replay
responses commit through the same ledger transaction path. Native WeChat
account IDs are unavailable, so accounts are scoped by OCR display identity and
configured aliases. See
[plugin rules and capture reconciliation](docs/local-plugins-0.6.1.md).
Steam prefers private per-person SteamID mappings from the active profile and
uses the corresponding group display IDs and aliases. Without a private roster,
it discovers the configured account's public friend list at runtime. No real
@@ -413,7 +462,12 @@ 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.
remain installed but disabled. After cutover, version-idempotent release
settlement grants Mora to every existing account, records the movement, and
sends the first changelog bullet as the guarded group announcement. Ordinary
patch releases grant 200 Mora. `beta 1.0.5.1016`, `beta 1.1.0.1016`, and
`beta 1.2.0.1019` also grant 1 primogem once. `beta 2.0.0.1021` is the
architecture-level major release and grants 400 Mora plus 2 primogems.
## Repository Map
+1 -1
View File
@@ -1 +1 @@
alpha 0.6.0.1015
beta 2.0.5.1023
+35 -1
View File
@@ -341,6 +341,19 @@ verify_edge_health() {
'import json,sys; value=json.load(sys.stdin); raise SystemExit(0 if value.get("status") == "ok" else 1)'
}
send_release_lifecycle_notice() {
local stage=$1
/usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${target_home}" \
XDG_RUNTIME_DIR="${user_runtime}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=${user_runtime}/bus" \
"${runtime_python}" "${source_dir}/jwebot_release_update.py" \
--release-file "${source_dir}/VERSION" \
--database "${target_edge_state}/edge.sqlite3" \
--delivery-helper "${target_runtime}/jwebot_edge_delivery.py" \
--lifecycle-stage "${stage}"
}
render_root_configuration() {
local output_dir="$1"
sed \
@@ -421,6 +434,7 @@ test -x "${runtime_python}"
for path in \
"${source_dir}/wechat_local_ocr.py" \
"${source_dir}/wechat-gui-bridge" \
"${source_dir}/jwebot_release_update.py" \
"${source_dir}/deploy/scripts/install-wechat-health-watch.sh" \
"${source_dir}/deploy/systemd/system/wechat-local-ocr-recorder.service" \
"${source_dir}/deploy/systemd/system/wechat-local-ocr-responder.service" \
@@ -430,7 +444,9 @@ do
done
bash -n "${source_dir}/wechat-gui-bridge"
sh -n "${source_dir}/deploy/scripts/install-wechat-health-watch.sh"
"${runtime_python}" -m py_compile "${source_dir}/wechat_local_ocr.py"
"${runtime_python}" -m py_compile \
"${source_dir}/wechat_local_ocr.py" \
"${source_dir}/jwebot_release_update.py"
preflight_rendered_dir=$(mktemp -d)
render_root_configuration "${preflight_rendered_dir}"
systemd-analyze verify \
@@ -468,6 +484,9 @@ jwebot_log "backup ready path=${backup_dir}"
armed=1
jwebot_progress 3 8 "stop services"
jwebot_log "cutover starting"
jwebot_log "release lifecycle shutdown notice starting"
send_release_lifecycle_notice shutdown
jwebot_log "release lifecycle shutdown notice ready"
user_systemctl stop \
wechat-ocr-adaptive-recorder.service \
jwebot-edge.service \
@@ -606,6 +625,10 @@ if [[ -f "${target_edge_state}/edge.sqlite3" ]]; then
fi
jwebot_log "database ready quick_check=${db_check:-absent}"
jwebot_log "release lifecycle startup notice starting"
send_release_lifecycle_notice startup
jwebot_log "release lifecycle startup notice ready"
jwebot_progress 7 8 "verify backup"
(
cd "${backup_dir}"
@@ -619,6 +642,17 @@ success=1
armed=0
trap - EXIT HUP INT TERM
jwebot_log "release settlement starting"
release_update_output=$(
/usr/sbin/runuser -u "${target_user}" -- /usr/bin/env \
HOME="${target_home}" \
XDG_RUNTIME_DIR="${user_runtime}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=${user_runtime}/bus" \
"${runtime_python}" "${target_runtime}/jwebot_release_update.py"
)
printf '%s\n' "${release_update_output}"
jwebot_log "release settlement ready"
jwebot_log "deployment ready version=$(cat "${source_dir}/VERSION")"
jwebot_log "backup active path=${backup_dir}"
jwebot_log "legacy OCR inactive"
+33 -3
View File
@@ -7,8 +7,10 @@ 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"
target_dir="$HOME/.local/lib/wechat-health-watch"
unit_dir="$HOME/.config/systemd/user"
config_dir="$HOME/.config/jwebot"
@@ -26,6 +28,7 @@ adaptive_script_name=wechat_adaptive_recorder.py
adaptive_bridge_name=wechat_adaptive_gui_bridge.py
edge_script_name=jwebot_edge.py
edge_delivery_name=jwebot_edge_delivery.py
release_update_name=jwebot_release_update.py
service_name=wechat-health-watch.service
timer_name=wechat-health-watch.timer
adaptive_service_name=wechat-ocr-adaptive-recorder.service
@@ -208,6 +211,7 @@ rollback() {
restore_file "$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name" 0700
restore_file "$edge_script_name" "$target_dir/$edge_script_name" 0700
restore_file "$edge_delivery_name" "$target_dir/$edge_delivery_name" 0700
restore_file "$release_update_name" "$target_dir/$release_update_name" 0700
for package_name in \
jwebot_config \
jwebot_sdk \
@@ -237,12 +241,15 @@ trap rollback EXIT HUP INT TERM
jwebot_log "user runtime preflight starting"
test -x "$runtime_python"
for path in \
"$source_dir/VERSION" \
"$source_dir/CHANGELOG.md" \
"$runtime_source_dir/$ocr_script_name" \
"$runtime_source_dir/$script_name" \
"$runtime_source_dir/$adaptive_script_name" \
"$runtime_source_dir/$adaptive_bridge_name" \
"$runtime_source_dir/$edge_script_name" \
"$runtime_source_dir/$edge_delivery_name" \
"$runtime_source_dir/$release_update_name" \
"$user_unit_source_dir/$service_name" \
"$user_unit_source_dir/$timer_name" \
"$user_unit_source_dir/$adaptive_service_name" \
@@ -262,6 +269,9 @@ do
test -d "$directory"
find "$directory" -maxdepth 1 -type f -name '*.py' | grep -q .
done
test -d "$official_autochess_data_source_dir"
find "$official_autochess_data_source_dir" -maxdepth 1 -type f -name '*.json' \
| grep -q .
test -f "$environment_file"
if systemctl is-active --quiet wechat-local-ocr-recorder.service \
@@ -276,12 +286,15 @@ jwebot_log "user runtime backup starting path=$backup_dir"
install -d -m 0700 "$target_dir" "$unit_dir" "$config_dir" "$backup_dir"
save_unit_states
for name in \
VERSION \
CHANGELOG.md \
"$ocr_script_name" \
"$script_name" \
"$adaptive_script_name" \
"$adaptive_bridge_name" \
"$edge_script_name" \
"$edge_delivery_name"
"$edge_delivery_name" \
"$release_update_name"
do
if [ -f "$target_dir/$name" ]; then
cp -a "$target_dir/$name" "$backup_dir/$name"
@@ -326,12 +339,17 @@ systemctl --user stop \
>/dev/null 2>&1 || true
jwebot_log "user runtime files install starting"
install -m 0600 "$source_dir/VERSION" "$target_dir/VERSION"
install -m 0600 "$source_dir/CHANGELOG.md" "$target_dir/CHANGELOG.md"
install -m 0700 "$runtime_source_dir/$ocr_script_name" "$target_dir/$ocr_script_name"
install -m 0700 "$runtime_source_dir/$script_name" "$target_dir/$script_name"
install -m 0700 "$runtime_source_dir/$adaptive_script_name" "$target_dir/$adaptive_script_name"
install -m 0700 "$runtime_source_dir/$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name"
install -m 0700 "$runtime_source_dir/$edge_script_name" "$target_dir/$edge_script_name"
install -m 0700 "$runtime_source_dir/$edge_delivery_name" "$target_dir/$edge_delivery_name"
install -m 0700 \
"$runtime_source_dir/$release_update_name" \
"$target_dir/$release_update_name"
rm -rf \
"$target_dir/jwebot_config" \
"$target_dir/jwebot_sdk" \
@@ -341,8 +359,11 @@ 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/adapters" \
"$target_dir/jwebot_official_extensions/data" \
"$target_dir/jwebot_official_extensions/data/cs_autochess"
for path in "$config_source_dir"/*.py; do
install -m 0600 "$path" "$target_dir/jwebot_config/$(basename "$path")"
done
@@ -352,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
@@ -359,6 +384,10 @@ for path in "$official_adapter_source_dir"/*.py; do
install -m 0600 "$path" \
"$target_dir/jwebot_official_extensions/adapters/$(basename "$path")"
done
for path in "$official_autochess_data_source_dir"/*.json; do
install -m 0600 "$path" \
"$target_dir/jwebot_official_extensions/data/cs_autochess/$(basename "$path")"
done
for name in \
"$service_name" \
"$timer_name" \
@@ -379,7 +408,8 @@ for name in \
"$adaptive_script_name" \
"$adaptive_bridge_name" \
"$edge_script_name" \
"$edge_delivery_name"
"$edge_delivery_name" \
"$release_update_name"
do
"$runtime_python" -m py_compile "$target_dir/$name"
done
+134
View File
@@ -0,0 +1,134 @@
# JWeBot Command Catalog
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
descriptor inventory; plugin code delegates to reviewed service profiles.
## System
| Command | Alias | Purpose |
|---|---|---|
| `/功能列表` | `/help` | Show the command list. |
| `/状态` | `/status` | Show local runtime status. |
| `/version` | `/版本` | Show installed package versions plus the latest changelog time and summary. |
| `/debug` | - | Show privacy-safe telemetry diagnostics. |
| `/余额` | - | Show configured direct-provider balance status. |
## Context And People
| Command | Alias | Purpose |
|---|---|---|
| `/上下文 [100|200|300]` | `/context` | Summarize recent local text in the current group. |
| `/人物列表 [页码]` | `/people` | List known group-scoped people. |
| `/人物 [名称或别名]` | `/person` | Look up one configured or observed person. |
| `/记住 内容` | - | Store a local memory entry for the current person. |
| `/我的记忆` | - | Show the current person's memory entries. |
| `/人物记忆 显示名` | - | Show memory entries for a person. |
| `/忘记 关键词` | - | Remove matching local memory entries. |
| `/记忆帮助` | - | Show memory command help. |
## Group Insights
| Command | Alias | Purpose |
|---|---|---|
| `/扫码排行` | `/扫码榜`, `/今日扫码` | Show all scan candidates, today's counts, and next-hour scan weights for the current group. |
| `/复读榜` | - | Show repeat ranking for the current group. |
| `/查记录 关键词` | - | Search local group history by keyword. |
## Virtual Coins And Games
| Command | Alias | Purpose |
|---|---|---|
| `/摩拉` | `/金币`, `/原石`, `/wallet` | Initialize or show the current person's Mora and primogem account. |
| `/大富翁榜 [人数]` | `/摩拉排行`, `/摩拉排行榜`, `/金币排行`, `/金币排行榜`, `/richlist` | Show the group-scoped Mora leaderboard. |
| `/转账 对方 金额` | `/transfer name amount` | Transfer virtual coins to an initialized group account. |
| `/贷款 [金额]` | `/loan amount`, `/借款` | Borrow up to 500 virtual coins. |
| `/还款 [金额|全部]` | `/repay [amount|all]` | Repay an active virtual coin loan. |
| `/原石祈愿` | `/祈愿`, `/wish` | Spend 1 primogem to draw 20-10000 Mora with expected value 3600 Mora, with a chance to gain a scan-boost ticket. |
| `/原石榜 [人数]` | `/primogem榜` | Show the group primogem leaderboard with source details. |
| `/扫码加权 人名` | `/扫码概率 人名` | 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` | 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-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-2000 Mora. |
| `/游戏记录 [页码]` | `/gamehistory` | Show indexed personal game history. |
| `/游戏帮助` | `/games` | Show local game rules. |
## Random And Session Games
| Command | Alias | Purpose |
|---|---|---|
| `/丢骰子 [NdM]` | - | Roll dice. |
| `/丢骰子挑战` | `/骰子挑战` | Run a dice challenge. |
| `/翻硬币` | - | Flip a coin. |
| `/随机数 [范围]` | - | Draw a random number. |
| `/抽签 [内容...]` | - | Draw lots from provided options. |
| `/今日运势` | - | Show a deterministic daily fortune. |
| `/猜数字 开始 [上限]` | - | Start a group guessing game. |
| `/猜数字 数字` | - | Guess in the active guessing game. |
| `/石头剪刀布 开始` | - | Start a rock-paper-scissors session. |
| `/石头剪刀布 加入` | - | Join the active session. |
| `/石头剪刀布 出 石头|剪刀|布` | - | Submit a move. |
| `/石头剪刀布 状态` | - | Show session status. |
| `/石头剪刀布 结算` | - | Settle the session. |
| `/石头剪刀布 结束` | - | End the session. |
## Steam
| Command | Alias | Purpose |
|---|---|---|
| `/steam 在线` | - | Show configured Steam online status. |
| `/steam 游戏` | - | Show current games. |
| `/steam 全部` | - | Show full configured Steam presence. |
| `/steam 刷新` | - | Refresh cached Steam data. |
| `/steam 时长 游戏名` | `/steam时长游戏名` | Query playtime for a game. |
| `/steam时长游戏名、玩家名` | `/steam时长游戏名人=玩家名` | Query one player's game playtime. |
## CS Major Autochess
| Command | Alias | Purpose |
|---|---|---|
| `/自走棋开始` | - | Create a new IEM 盐田 Major season. |
| `/自走棋报名` | - | Register the current player. |
| `/自走棋加AI战队` | - | Add local deterministic AI teams for testing. |
| `/自走棋开赛` | - | Start the event from registration. |
| `/自走棋状态` | - | Show the current event state. |
| `/自走棋档案` | - | Show archived event information. |
| `/选1` ... `/选10` | - | Draft coaches, players, or shop cards depending on phase. |
| `/刷新` | - | Refresh the shop. |
| `/锁定` | - | Lock or unlock the shop. |
| `/阵容` | - | Show lineup, bench, coach, gold, and synergies. |
| `/赛程` | - | Show the current event schedule. |
| `/升级教练` | - | Upgrade the team's coach level when rules allow it. |
| `/上阵 备战编号 [首发位]` | - | Move a bench player into the starting lineup. |
| `/下阵 首发位` | - | Move a starting player out when rules allow it. |
| `/出售 备战编号` | - | Sell a bench player. |
| `/战术 火力|稳定|残局` | - | Set the team's tactical focus. |
| `/结束回合` | - | Mark readiness and advance when required players are ready. |
## Notes
- Commands must be sent in the configured WeChat group and normally mention the
bot, depending on deployment configuration.
- OCR whitespace loss is tolerated for selected compact forms such as
`/steam时长Dota2` and `/21点开始100`.
- Real profiles, Steam IDs, person context, message payloads, and secrets are
never stored in this repository.
+9 -3
View File
@@ -1,7 +1,7 @@
# Command Registration And Execution Paths
Status: authoritative local registration and routing contract for
`alpha 0.6.x`.
`beta 1.0.x`.
## Three Execution Paths
@@ -25,7 +25,8 @@ error. They never fall through to DeepSeek or OpenClaw.
The local command path is:
1. XDamage reports a relevant message-region change.
1. XDamage reports a relevant message-region change, or a two-second idle
reconciliation capture detects a missed final drawing.
2. OpenCV segments incoming bubbles without running OCR over the whole screen.
3. The OCR engine reuses exact visual-fingerprint matches from a bounded LRU.
4. Only new or changed sender/body crops use RapidOCR.
@@ -45,6 +46,10 @@ because each bubble position still produces its own `IncomingMessage`.
Changed pixels force fresh OCR. The cache is bounded and periodically
revalidated.
For the local plugins introduced during the 0.6.x line, including bilingual
commands, person identity limitations and atomic single-player game settlement,
see [local plugin rules](local-plugins-0.6.1.md).
## Bundled Plugin Registration
Bundled command metadata has one source:
@@ -141,7 +146,8 @@ descriptors unless they are strictly Kernel-only. The current official
operational commands are:
- `/状态` and `/status`: server, Edge, OCR, and queue status.
- `/version`: installed JWeBot package versions.
- `/version`: installed JWeBot package versions plus the latest changelog date
and summary.
- `/debug`: recent privacy-safe telemetry p50/p95/max summary.
These commands delegate to Kernel-owned service profiles through generated
+9
View File
@@ -90,6 +90,15 @@ Steam uses `JWEBOT_EDGE_STEAM_ID64` and
- Host timeout and fallback;
- GUI sender timeout.
The OCR runtime also accepts optional environment overrides for constrained
hosts:
- `WECHAT_OCR_ONNX_INTRA_OP_THREADS` defaults to `1`;
- `WECHAT_OCR_ONNX_INTER_OP_THREADS` defaults to `1`.
These values limit RapidOCR's ONNXRuntime CPU thread pools so service restarts
do not temporarily saturate the host while the OCR cache is cold.
### Debug
`components/debug.toml` generates:
+61
View File
@@ -0,0 +1,61 @@
# Private Conversation Shadow Testing
This prototype observes one manually selected private conversation without
changing the production group workflow.
It is intentionally unable to switch conversations or send replies:
- The recorder never enqueues matching shadow events.
- Every observation carries `shadow_only=true` and `dispatch_allowed=false`.
- JWeBot Edge ignores marked observations before routing or persistence.
- The shadow registry accepts private targets only. The main group cannot be
added to it.
- No setting in the shadow registry can enable delivery.
## Local Configuration
Keep the real file outside the repository:
```text
~/.config/jwebot/conversation-shadow.json
```
Example with placeholder values:
```json
{
"format_version": 1,
"enabled": true,
"targets": [
{
"key": "test-direct",
"kind": "direct",
"title_fingerprint": "<64 lowercase SHA-256 characters>",
"sender": "<local test label>"
}
]
}
```
Set `JWEBOT_CONVERSATION_SHADOW_CONFIG` to that absolute path only in a local
test process. Without the variable, the feature is not imported or enabled.
The title fingerprint is the SHA-256 value returned by
`title_fingerprint(image)` for a screenshot where the intended conversation is
selected. It is an opaque routing value, not a trusted contact identity.
## Controlled Test
1. Keep production services unchanged.
2. Run the recorder in a separate local test state directory.
3. Select the allowlisted private conversation manually.
4. Let the first capture establish a baseline.
5. Send `/状态` or `JWeBot: 测试` from the other account.
6. Inspect the local history for `shadow_only=true`.
7. Assert that the OCR queue remains empty and no outgoing bubble appears.
Ordinary private messages are observed but do not set `mention_bot`. Only slash
commands and the configured wake word are treated as potential triggers.
Do not point this prototype at production state paths and do not add the
environment variable to a deployed runtime profile.
+678
View File
@@ -0,0 +1,678 @@
# CS Major 自走棋设计稿
本文档定义一个完全本地、确定性结算、以 CS 职业选手为棋子的群聊
小游戏方案。赛事包装统一为“第 n 届 IEM 盐田 Major”。AI 默认负责主持、
裁判、播报和剧情生成;主持人也可显式加入一支本地确定性 AI 战队。
## 1. 设计目标
1. 做成一个多人报名、多人运营、多人对战的群聊自走棋。
2. 玩法重心放在每轮选秀和阵容运营,而不是复杂微操。
3. 用 Major 赛事包装训练赛、小组单循环、淘汰赛和终局剧情。
4. 结算逻辑必须完全本地、可重放、可持久化、可回档验证。
5. 每一段剧情都要落档,且剧情只能在数值结算之后生成。
## 2. 核心体验
一句话定义:
`三轮赛前选秀 -> 轮次训练赛拿资源 -> D 牌与布阵 -> BO3 小组单循环 -> BO5 淘汰赛 -> 保存每轮赛报与剧情`
玩家感受到的乐趣应当主要来自:
- 第一轮从固定 10 人教练池里抢体系教练;
- 第二、三轮公开选手池里抢关键卡;
- 决定要不要保经济还是强行 D 牌;
- 赌阵容成型时点;
- 看小组单循环和淘汰赛赛报;
- 看最终冠军剧情与本届 Major 档案。
## 3. 对局对象与规模
### 3.1 报名
- 开局命令由任意玩家发起。
- 其他玩家在报名窗口内加入。
- 建议一局支持 `2~8` 名真人玩家。
- 主持人可在报名阶段发送 `/自走棋加AI战队`:
- 两名真人时加入 AI 战队 A、B;
- 三名真人时加入一支 AI 战队。
- 两种测试模式都补满四队,先进行 BO3 小组单循环,再按排名生成
`1v4`、`2v3` 半决赛。
- AI 占一个参赛席位,但不对应群成员,也不调用在线模型。
- AI 使用确定性规则选秀、买牌、调整首发和选择克制战术;为便于测试,
AI 阵容战力会有明确折扣。
### 3.2 玩家初始状态
每位玩家开局拥有:
- 生命值:`100`
- 摩拉:`10`
- 教练:赛前第一轮选秀决定
- 初始选手:赛前第二、三轮选秀决定,系统随后补齐阵容
- 阵容位:固定 `5 个选手位 + 1 个教练位`,比赛时必须全部上场
- 备战席:`3 格`
- 连胜/连败计数:`0`
- 本局独立事件档案编号
三轮选秀完成后,系统按已选核心确定性补齐每队 `5 名首发 + 3 名备战`
和常驻教练,保证训练赛开始时就是完整阵容。
## 4. 选手池设计
### 4.1 选手来源
只使用“近期非常出名”的职业选手。第一期池子优先覆盖以下俱乐部与明星:
- Spirit
- Vitality
- Falcons
- G2
- FaZe
- NAVI
- MOUZ
- The MongolZ
- Astralis
- Liquid
- FURIA
- Eternal Fire
选手数据不是对真实比赛数据的一比一复刻,而是基于公众认知做游戏化映射。
0.6.7 起本地维护 `100+` 名棋子和 `18` 名教练元数据;运行期不在线抓取
或改写数值。首轮教练选秀仍固定展示 10 个候选,保证聊天窗口可读。
0.6.9 起,公开游戏内容不再内联在 `cs_autochess.py` 中,而是维护在
`packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/`:
`players.json` 保存选手卡,`coaches.json` 保存教练卡,
`narratives.json` 保存战报和剧情素材,`rules.json` 保存经济、商店、
选秀、羁绊、赛制、MR15/加时和特殊事件规则。运行时启动会校验 schema、
重复 ID、必要字段和叙事素材数量。
### 4.2 费用层级
选手分为 `1~5 费`,越高费越稀有,基础数值和成长更强。
示例分层:
- `5 费`:donk、ZywOo
- `4 费`:m0NESY、NiKo、ropz、frozen
- `3 费`:apEX、b1t、xertioN、w0nderful、malbsMd
- `2 费`:辅助型或中坚选手
- `1 费`:前期过渡与羁绊补齐牌
正式实现时应维护一份固定的选手静态表,不允许运行期随意漂移。
### 4.3 定位
只保留三个核心定位:
- `步枪手`
- `狙击手`
- `指挥`
定位不是羁绊,而是战斗中的属性倾向与技能触发来源。
### 4.4 羁绊
只保留两类羁绊:
1. `战队羁绊`
2. `国家羁绊`
战队羁绊可来自:
- 当前主战队;
- 历史待过的关键战队;
- 每名选手最多挂 `3` 条战队标签,避免过度泛化。
国家羁绊只有 `1` 条。
## 5. 选手数值
### 5.1 基础数值轴
为避免面板过宽,第一期只展示三个主数值:
- `火力`
- `稳定`
- `残局`
其中指挥位额外拥有隐藏或显式的 `指挥` 修正值,用于影响全队。
### 5.2 数值范围
- 1 费:`52~68`
- 2 费:`60~76`
- 3 费:`68~84`
- 4 费:`78~92`
- 5 费:`88~98`
要求同费位内部也要有明显差异,不能所有 4 费都只有一两点波动。
### 5.3 派生战斗权重
可先采用以下简单模型:
`单卡战力 = 火力 * 0.45 + 稳定 * 0.30 + 残局 * 0.25 + 星级修正 + 羁绊修正 + 教练修正`
若定位是狙击手,可给火力更高上限;
若定位是指挥,可给团队修正更高权重。
### 5.4 属性与定位克制
战斗不能只比较总战力后掷骰子。三项主属性形成闭环:
- `稳定克火力`:稳定阵容能吸收强攻的第一波冲击;
- `火力克残局`:前期人数优势会压缩残局发挥空间;
- `残局克稳定`:高残局能力能打破按部就班的默认局面。
三个定位也形成轻量闭环:
- `狙击手克步枪手`
- `步枪手克指挥`
- `指挥克狙击手`
玩家每回合可选择 `/战术 火力|稳定|残局`。战术选择会强化对应属性,
并在克制对方战术时获得额外修正。属性、定位和战术的总克制修正上限
应明显高于随机波动,让调阵和判断对手比运气更重要。
## 6. 升星规则
- `3 张同名 = 2 星`
- `9 张同名 = 3 星`
升星收益建议:
- 2 星:基础战力乘 `1.35`
- 3 星:基础战力乘 `1.80`
升星后保留原有羁绊与定位,不改变标签。
## 7. 教练系统
教练位不是普通战斗单位,但始终与 5 名选手一起上场。
### 7.1 教练等级作用
教练只提供额外的全队战术加成,不控制上场人数,也不占用选手位。
建议加成:
- 1 级:全队战力 `+2%`
- 2 级:全队战力 `+4%`
- 3 级:全队战力 `+6%`
- 4 级:全队战力 `+8%`
- 5 级:全队战力 `+10%`
### 7.2 教练获取
教练由第一轮公共选秀决定,本届赛事内不能用摩拉升级教练。不同教练
自带等级、属性倾向和全队加成。
高费选手的商店概率随赛事进程推进,不再绑定教练等级。
## 8. 每轮 6 人选秀
这是本模式的核心。
### 8.1 固定规则
- 每轮只展示 `6` 名候选选手。
- 这 `6` 人对所有玩家公开可见。
- 每位玩家始终保有 5 名首发;购买的新选手进入备战席或与同名选手合成。
- 调整阵容时必须用备战席选手替换首发,不能让首发少于 5 人。
- 第一版采用“公开同池、各自购买”的简化方案:
- 所有人看到同一组 6 人;
- 某个玩家选择后,不会把该候选从其他玩家界面里抢空;
- 但底层共享总卡池仍然会影响后续刷新概率。
- 每名候选都必须展示完整数据:
- 编号 `1~6`
- 选手名
- 费用
- 定位
- 国家
- 主战队
- 历史战队
- 主数值
- 玩家通过编号选择,不输入复杂自然语言。
### 8.2 选秀指令
建议指令面如下:
```text
/自走棋开始
/自走棋报名
/自走棋加AI战队
/自走棋状态
/选1
/选2
/选3
/选4
/选5
/选6
/选7
/选8
/选9
/选10
/刷新
/锁定
/战术 火力|稳定|残局
/上阵 备战编号 [首发位]
/出售 备战编号
/阵容
/赛程
/结束回合
```
中文是主命令,英文别名可以后补。
### 8.3 刷新与锁定
- 第一轮教练选秀展示固定 10 名教练;第二、三轮选手选秀展示 6 名选手。
- 训练赛和正赛运营阶段默认展示一组 6 人商店。
- 玩家可支付摩拉执行刷新。
- 玩家可锁定本轮 6 人,使下一轮仍保留同一组候选。
### 8.4 顺位
- 开赛后三轮公共选秀,第一轮选教练,第二、三轮选选手。
- 第二轮使用反向顺序,避免种子靠前者连续先手。
- AI 轮次会自动选择,真人只在轮到自己时使用 `/选N`。
第一期建议先做“公开同池、各自结算”,避免群聊交互过重。
## 9. 回合流程
建议一整回合拆成五段:
1. `回合开始播报`
2. `6 人选秀 / 刷新 / 升级 / 调阵`
3. `训练赛 / 赛事挑战 / PvP 结算`
4. `回合结算与经济发放`
5. `剧情归档`
## 10. 训练赛与赛事挑战
训练赛不再是阈值型 PvE。它是正式小组赛前的多轮真实对阵,用来给玩家
检验阵容、获取摩拉并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内
插曲出现,但只提供资源变动,不淘汰玩家。
### 10.1 活动类型
- 训练赛
- 媒体日
- 赞助挑战
- 本地陪练队热身
### 10.2 训练赛作用
训练赛按参赛队伍生成 round-robin 轮次。四队测试局会有三轮训练赛,
每队在训练阶段遇到另外三支队伍;奇数队伍时轮空队会与本地陪练队热身。
训练赛只发放摩拉,不记录小组胜负,也不扣生命。
每轮训练赛结束后进入运营窗口:
- `/刷新` 即 D 牌,消耗摩拉换一组 6 人商店;
- `/选1~/选6` 买入商店选手,重复卡自动累积星级;
- `/锁定` 保留当前商店;
- `/上阵` 调整 5 人首发;
- `/战术 火力|稳定|残局` 改变下一场的回合倾向。
### 10.3 训练赛结算逻辑
训练赛使用与正式比赛相同的单图 MR15 逐回合模拟,只是赛制为 BO1。
胜方获得更多摩拉,负方也有保底摩拉,保证测试局能继续运营。
媒体日和赞助挑战根据阵容完整度、教练、战术和本地确定性随机因子发放
摩拉,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。
## 11. PvP 赛制
### 11.1 赛事包装
整局赛事名称固定为:
`第 n 届 IEM 盐田 Major`
### 11.2 按参赛人数生成赛制
- `2 人`:直接进入决赛。
- `3 支队伍`:进行完整单循环,前 2 名进入决赛。
- `4 人`:不打小组赛,按报名种子直接进行 `1v4`、`2v3` 半决赛。
- `5~8 人`:进行完整小组单循环,每两支队伍只交手一次,前 4 名晋级。
奇数队伍的小组比赛日会有一队轮空,但不会获得胜场。
小组赛每场采用 `BO3`。
### 11.3 小组排名
小组赛首先按胜场排序;同胜场依次比较:
1. 相互交手结果;
2. 当前完整五人阵容战力;
3. 剩余生命值;
4. 固定种子编号。
所有配对在开赛时一次性生成并落档,不允许运行中根据结果重排赛程。
### 11.4 淘汰赛
- 四强固定为 `小组第 1 vs 第 4`、`第 2 vs 第 3`。
- 3 人赛制由小组第 1、2 名直接进入决赛。
- 4 人赛制使用报名种子 `1v4`、`2v3`。
- 2 人赛制直接进入决赛。
- 半决赛胜者进入总决赛,决出本届冠军。
- 半决赛和总决赛均采用 `BO5`。
## 12. 战斗结算
### 12.1 目标
战斗要做到:
- 本地确定性;
- 单轮可解释;
- 支持战报;
- 不依赖在线模型。
### 12.2 简化模型
建议第一期采用“阵容总分 + 对位修正”的轻量模型。
#### 步骤 1:计算阵容基础战力
`阵容基础战力 = 5 名首发选手战力总和`
#### 步骤 2:加上团队修正
团队修正来源:
- 战队羁绊
- 国家羁绊
- 教练等级提供的全队百分比加成
- 装备/战术卡
- 连胜或特殊事件 buff
#### 步骤 3:计算克制修正
依次计算:
1. `稳定 > 火力 > 残局 > 稳定` 的属性对位;
2. `狙击手 > 步枪手 > 指挥 > 狙击手` 的定位对位;
3. 双方本回合主动选择的战术对位。
克制结果必须进入结构化战报,明确展示双方修正值。
#### 步骤 4:加入小幅波动
只允许 `-2% ~ +2%` 的确定性哈希波动。单次运气不能覆盖成型阵容的
属性、定位和战术克制优势。
#### 步骤 5:计算比分
单图采用 `MR15`:
- 常规时间先到 `16` 分获胜;
- 常规时间 `15-15` 进入加时;
- 加时可产生 `19-17`,多轮加时可产生 `22-20` 等比分;
- 每次加时除结构化单图结果外,必须额外生成一段加时剧情并落档。
小组赛按 BO3 先赢 2 图,淘汰赛按 BO5 先赢 3 图。系列赛战报必须展示
大比分、每张图比分、加时次数和克制修正。
## 13. 剧情系统与落档
这是本模式的强制要求。
### 13.1 归档原则
必须先结算,后写剧情。
即:
1. 固化数值结果;
2. 存档回合事实;
3. 基于事实生成文字赛报;
4. 再把文字赛报作为附加档案保存。
### 13.2 必须落档的节点
- 开局报名完成
- 每轮 6 人选秀结果
- 每次刷新和关键升星
- 每轮训练赛和赛事挑战结果
- 每场 PvP 结果
- 小组赛排名、晋级和淘汰节点
- 淘汰赛对阵与结果
- 冠军总结
### 13.3 档案结构
建议至少保存两类记录:
1. `结构化事件档`
2. `文本剧情档`
结构化事件档用于回放和调试;
文本剧情档用于玩家阅读。
示例:
```json
{
"run_id": "major_0001",
"round": 4,
"phase": "group_2",
"event_type": "pvp_result",
"player_a": "Jacky",
"player_b": "Alice",
"winner": "Jacky",
"score_band": "close_win",
"roster_a_power": 418,
"roster_b_power": 403
}
```
## 14. 数据持久化设计
建议新增以下 SQLite 表。
### 14.1 对局主表
`autochess_runs`
- run_id
- group_fingerprint
- season_number
- state
- created_at
- updated_at
### 14.2 玩家状态表
`autochess_players`
- run_id
- person_key
- display_name
- hp
- gold
- coach_level
- coach_xp
- wins
- losses
- streak
- board_json
- bench_json
- shop_json
- locked_shop
### 14.3 选秀记录表
`autochess_shop_events`
- run_id
- round_number
- person_key
- visible_pool_json
- action_type
- action_payload_json
- created_at
### 14.4 战斗结果表
`autochess_battles`
- run_id
- round_number
- phase
- battle_type
- player_a
- player_b
- result_json
- created_at
### 14.5 剧情归档表
`autochess_story_logs`
- run_id
- round_number
- phase
- story_type
- story_text
- facts_json
- source_event_id
- created_at
### 14.6 幂等与重放表
`autochess_action_replay`
- run_id
- person_key
- round_number
- event_id
- action_kind
- response_text
- created_at
## 15. 回放与幂等
因为群聊环境会出现重复事件、OCR 重读和命令重放,必须保证:
- 同一事件只执行一次;
- 同一回合不能重复发奖或重复扣款;
- 同一段剧情不能重复写多份版本;
- 回合失败时整段事务回滚。
建议沿用现有本地游戏的幂等思路,为自走棋单独增加 action replay 表。
## 16. 指令交互风格
建议所有回复都遵循现有小游戏风格:
多段回复优先。一次主持动作可以拆成欢迎语、参赛名单、种子顺序、候选池、
状态面板、操作指引等多条消息,每条保持在发送上限内。
例如:
```text
欢迎来到第一轮选秀,本轮选教练。
选择顺序:Jacky、Alice、AI 战队 A、AI 战队 B
1. zonic|5级教练|冠军体系|火78 稳92 残88|全队+9%
...
现在轮到 @Jacky 做出选择,使用 /选1~/选10。
```
### 16.1 Taffy 主持状态机
主持人必须承担完整赛事主持,而不是只在结算时出现;输出不需要每句都带
`Taffy:` 前缀。
开赛时必须包含:
- `欢迎大家来到第 n 届 IEM 盐田 Major`
- 本届参赛者和种子
- 当前人数对应的完整赛制
- 小组赛 BO3、淘汰赛 BO5、单图 MR15 和加时规则
- 三轮选秀规则和当前选择玩家
- 训练赛、小组赛和淘汰赛的当前阶段
赛前选秀按顺序逐人选择。进入阵容准备、训练赛运营和小组赛运营后,
所有相关真人发送 `/结束回合` 即可自动推进,不需要主持人手动结算。
淘汰赛只等待当前对阵中的真人,已经淘汰的玩家不会再被提示准备。
```text
Jacky 选择了 zonic!
现在轮到 @Alice 做出选择,使用 /选1~/选10。
训练赛第 1/3 轮对阵:Team Jacky vs Team Alayak;AI 战队 A vs AI 战队 B
本轮训练结束。现在是运营窗口:/刷新 是 D 牌,/选1~/选6 是买店内选手;
所有真人发送 /结束回合 后进入下一轮训练。
```
以下节点都必须由 Taffy 主持并落档:
- 报名开启和新选手加入
- AI 测试战队加入
- 赛制与种子公布
- 三轮选秀候选池、选择结果和选秀完成
- 每轮训练赛对阵、比分、摩拉和运营提示
- 媒体日或赞助挑战
- 每场 BO3/BO5 对阵与结果
- 每张加时地图
- 小组排名和前四晋级
- 1v4、2v3 半决赛
- 总决赛和冠军颁奖
## 17. 实现顺序建议
建议按三个阶段落地。
### 第一阶段:最小可玩版
- 建局 / 报名 / 状态
- 每轮 6 人选秀
- 买卡、卖卡、升星
- 教练等级
- 简化训练赛
- 简化 PvP
- 基础剧情归档
### 第二阶段:赛事化增强
- 小组单循环完整配对
- 1v4、2v3 淘汰赛树
- 羁绊与装备系统细化
- 战报模板优化
### 第三阶段:内容扩展
- 在 100+ 人本地卡池基础上增加新赛季棋子
- 更多俱乐部和国家
- 特殊赛事事件
- 更细的剧情风格与本届 Major 年鉴
## 18. 第一版明确不做的内容
为保证能上线,第一版先不做:
- 实时地图微操
- 复杂技能动画
- 过多职业分类
- 在线爬取实时战队数据
- 大量开放文本输入解析
## 19. 最终结论
这套模式最适合按“强运营、轻操作、重赛报”路线实现。
最重要的三个锚点已经确定:
1. `每轮固定 6 人公开选秀`
2. `训练赛与挑战负责资源,PvP 负责小组单循环与淘汰赛`
3. `每一段剧情都必须落档`
如果按这个设计推进,实现出来的成品会更像一场可回放、可复盘、
可连载的本地 Major 模拟器,而不是一个只有数值结算的普通抽卡游戏。
+68
View File
@@ -0,0 +1,68 @@
# alpha 0.6.3.1015 多人彩票与游戏记录
## 一把彩票怎么玩
1. 任意群友发送 `/彩票 开始`。发起人是本轮主持人。
2. 每个人发送 `/彩票 报名 3 4 7 100`,表示选择 3、4、7,下注 100
摩拉。也可简写为 `/彩票347100`;省略金额时默认下注 10。
3. 发送 `/彩票 当前状态` 查看本轮主持人、报名人数、每个人的号码和下注。
4. 主持人发送 `/彩票 开奖`。机器人从 0~9 中抽取三个互不重复的数字,
统一排序后结算。
5. 主持人也可在开奖前发送 `/彩票 取消`,所有报名者全额退款。
命令关键词和数字之间的空格均可省略。`/买彩票` 和 `/lottery` 保留为
对应别名。
## 选号与报名
- 每张票必须是三个互不重复的 0~9 数字。
- 数字顺序不影响结果:`347`、`743` 和 `3 4 7` 都保存为 `3 4 7`。
- 每人每把只有一张票。再次报名会更新号码和下注,不增加个人局号。
- 更新到更高下注时只补扣差额;降低下注时退回差额。
- 下注范围是 1~500,省略时为 10。
- 没有进行中的彩票时不能报名;没有报名者时不能开奖。
- 只有创建本轮的主持人可以开奖或取消,状态查询对群内所有人开放。
支持的报名示例:
```text
/彩票 347
/彩票 743 100
/彩票 报名 3 4 7 100
/彩票报名347金额100
/彩票347100
/lottery join 347 100
```
## 返还规则
报名时先扣除下注。开奖号和玩家号码按集合比较,返还包含本金:
| 命中数 | 返还 |
|---|---:|
| 0 个 | 0% |
| 1 个 | 50% |
| 2 个 | 120% |
| 3 个 | 300% |
摩拉只使用整数;百分比计算的小数部分向下取整。例如下注 100,命中两个
返还 120,净变化为 `+20`。
## 个人局号与记录
许愿池、彩票和 21 点分别维护本人的游戏局号。第一次参与某种游戏显示
`Jacky #1:...`,下一把显示 `Jacky #2:...`。配置别名解析到同一人物时,
共用同一账户、局号和历史。
发送 `/游戏记录 [页码]`、`/个人记录 [页码]` 或
`/gamehistory [page]` 查看最近记录,每页五条。记录包含游戏、个人局号、
结果、下注、返还和净变化,最多保留最近 50 条。
21 点下注范围现为 1~2000。`/21点 开始 100` 只表示本局下注 100;
发牌后再单独发送 `/21点 要牌` 或 `/21点 停牌`。
## 持久化与并发
彩票轮次、报名和结算保存在 Edge SQLite。报名/改注、所有玩家的开奖返还、
取消退款和事件重放响应都在 `BEGIN IMMEDIATE` 事务中提交。重复事件不会
重复扣款、发奖或增加局号;进程重启后未开奖轮次和个人记录仍然保留。
+137
View File
@@ -0,0 +1,137 @@
# alpha 0.6.1.1015 本地插件与消息抓取
本版本增加三个插件。全部由本机命令引擎执行,不调用 DeepSeek 或
OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,OCR 丢失
命令与参数间的空格时仍可解析。正常使用时仍需 @ 当前配置的机器人。
| 插件 | 中文指令 | 英文指令 |
|---|---|---|
| context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` |
| single-player-games | `/摩拉`(兼容 `/金币`) | `/wallet` |
| coin-ranking | `/大富翁榜`、`/摩拉排行`(兼容 `/金币排行`) | `/richlist` |
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
| coin-loan | `/贷款 [金额]`、`/还款 [金额\|全部]` | `/loan amount`、`/repay [amount\|all]` |
| 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` |
| single-player-games | `/游戏记录 [页码]` | `/gamehistory [page]` |
| single-player-games | `/游戏帮助` | `/games` |
| person-directory | `/人物列表 [页码]`、`/别名列表 [页码]` | `/people [页码]` |
| person-directory | `/人物 [名称或别名]`、`/个人属性 [名称或别名]` | `/person [名称或别名]` |
命令所有权来自 `descriptors.py`,由同步工具生成三套受审核 SDK adapter。
每个 adapter 只提交命名 service-profile 请求,由 Kernel 执行数据库操作。
注册在启动期完成,运行期冻结。`/余额` 保持查询提供商余额;
游戏虚拟余额使用 `/摩拉`,`/金币` 保留为兼容别名。
## 上下文总结
默认选择最近 100 条,也可选择 200 或 300。查询限定当前群,排除当前
请求事件,按观察时间和事件 ID 选出最近记录,再恢复时间顺序。
不足指定数量时明确报告实际数量;没有记录时返回空历史提示。
总结采用本地抽取式方法:统计参与人数、时间范围、重复话题,选出五条
具有代表性的内容并按时间排列。它保留原话,不生成未经原文支持的决定
或待办。覆盖范围是本机已经成功记录的文字消息,不包含图片、语音、
未加载的微信历史或本机没观察到的消息。所有历史只保存在本机私有状态。
独立的 `context_history` 保留短消息,避免复读排行的长度过滤影响最近
100/200/300 条计数;启动时从现有复读历史补入旧记录,查询时从 OCR
历史导入已识别发送人的文字。
## 人物与账户索引
当前 OCR 输入只有显示名,没有微信原生账号 ID。Kernel 将昵称或别名
解析到私有配置中的 canonical_name,再以“当前群+规范人物名”计算
person_key。相同人物的已配置别名共用摩拉和牌局,其他群使用独立账户。
没有配置的显示名使用独立临时身份;未识别到发送人的消息不能操作账户。
`/人物` 默认查看本人;指定参数时只查询已配置人物。输出包括展示名、
截短的群内索引、别名和经过安全过滤的基本属性,不返回 SteamID 或密钥。
`/人物列表` 每页五人。真实人物资料仍只存放于 `~/.config/jwebot/`,
仓库没有生产人物或映射文件。
昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域;
应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令
指定他人的账户;转账只支持同群虚拟摩拉账户,收款人需先发送 `/摩拉`
或 `/金币` 初始化。`/贷款` 和 `/还款` 只作用于本人账户。虚拟摩拉不能充值、
赠币或兑换真实资产。
## 虚拟摩拉与游戏管理
每个群内人物首次使用账户时一次性获得 1000 虚拟摩拉。再次查询、
重启服务或切换已配置别名都不会重复领取。许愿池投入为 1000~10000,
按 1000 摩拉递增;彩票下注为 1~500,21 点下注为 1~2000;
余额不足或参数非法不扣款。
`/转账 对方 金额` 会把本人的虚拟摩拉转给同群已初始化账户,英文为
`/transfer name amount`。金额范围 1~100000;不允许给自己转账。
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
不会重复扣款。
`/贷款 [金额]` 会给本人账户借入 1~500 摩拉;同一账户同一时间最多
一笔未还贷款。`/还款 [金额|全部]` 从当前摩拉余额中扣除并减少未还
贷款,借款、还款、响应记录和重试去重同样写在一个 ledger 事务内。
`/大富翁榜` 或 `/摩拉排行 [人数]`(兼容 `/金币排行`)只读取当前群已有摩拉账户,不会创建
调用者账户。榜单按当前摩拉余额降序排列,人数范围 1~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~2000。`/21点 开始 100` 表示拿 100 摩拉作为本局下注进场,
等价的 OCR 紧凑形式是 `/21点开始100`。开局会扣除下注并发两张牌,
然后根据牌面单独发送 `/21点 要牌` 或 `/21点 停牌`;开局与操作写在
同一条消息时不会扣款,而会返回分步提示。牌局使用洗牌后的单副 52 张牌,
牌堆、双方手牌、下注和最后操作时间持久化。A 可作为 1 或 11;
庄家软 17 停牌。开始后只公开一张庄家牌;要牌、停牌、认输和查询都
仅针对本人牌局。发牌、状态和结算回复以当前群内人物的配置展示名开头,
例如 `jacky #1:你的牌 ...`;别名解析到同一人物时沿用相同展示名、
账户、局号和游戏记录。
多人 21 点使用 `/21点 多人 启动` 创建 Taffy 牌桌,开桌者自动入座。
其他玩家在下注前用 `/21点 加入` 进入当前牌桌;主持人发送 `/21点 多人 开始`
后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~2000。
所有玩家下注后统一发牌,轮到本人时用 `/21点 要牌` 或 `/21点 停牌` 操作;
结算按座位顺序列出每个人的下注、返还、净变化和当前摩拉。
自然 21 点立即结算,返还 2.5 倍下注向下取整;普通胜利返还两倍,
平局返还下注,失败不返还。每人最多一局;牌局进行中不接受另一笔
许愿池或 21 点开局,多人彩票报名仍可独立进行。
`/21点 胜率` 会列出当前群所有有已结算记录的玩家,以及 Taffy 的
胜/负/平、胜率和累计净赚净亏;主动认输和超时计负,牌桌取消不计入统计。
十分钟无有效游戏操作的牌局在下一次账户操作时按认输结束;查询状态
不延长有效期。主动结束同样按认输处理,不退下注。其他人可以各自
独立玩,不能加入或结束你的牌局。旧的群游戏保持独立。
Kernel 用一个 `BEGIN IMMEDIATE` 事务同时保存:
1. `player_accounts` 中的摩拉、牌局和胜负统计;
2. `player_actions` 中的事件、命令及响应。
同一群中的同一 event_id 只结算一次,重试返回原响应。相同事件若变更
人物或命令则拒绝。事务失败全部回滚,避免扣款后丢失结果。新的独立
指令会创建新的游戏操作,即使文字相同也不会被当成重试。
数据库迁移仅新增表,旧版本可以忽略它们;运行文件回滚不会主动删除
游戏数据。使用协调部署器回滚整库时,以部署前已验证备份为恢复基线。
## 单条消息抓取修复
独立观察证据表明,新气泡完成绘制后有时没有对应的有效 XDamage
事件;先前截图仍是旧布局,后续消息的绘制才唤醒生产抓取。
保留 XDamage 快速路径,同时将无有效变化时的复核等待窗口缩短到两秒。
空闲超时后走相同截图、跟随最新消息、视觉哈希和缓冲去重流程。
画面没变化时不进入重复 OCR;最终绘制漏事件时也能补抓。等待 X socket
前先检查 Xlib 已缓冲事件,避免用户态已有事件却阻塞等待另一次通知。
新增 `source=reconcile` 遥测用于识别补抓路径。两秒是等待窗口,
不是端到端响应承诺;截图、OCR、生成和 GUI 投递仍有各自耗时。
短周期复核增加空闲截图开销,上线后需观测实际负载。
群指纹、窗口就绪、GUI 串行、视觉确认和投递去重规则保持有效。
+1 -1
View File
@@ -1,6 +1,6 @@
# Plugin Contract
Status: authoritative for the `alpha 0.6.x` extension API.
Status: authoritative for the `beta 1.0.x` extension API.
## Scope
+28 -3
View File
@@ -6,9 +6,11 @@ exception.
## Release Discipline
- Version format is `alpha X.Y.Z.MMDD`.
- Patch changes increment `Z`; minor version changes require explicit user
approval.
- Version format is `beta X.Y.Z.MMDD` for current beta releases.
- Python package versions use the matching PEP 440 beta form, such as
`X.Y.Zb1+MMDD`.
- Patch changes increment `Z`; minor, major, or stable-release changes require
explicit user approval.
- A release change must update:
- `VERSION`;
- root `pyproject.toml`;
@@ -16,6 +18,9 @@ exception.
- every package `__version__`;
- `README.md`;
- `CHANGELOG.md`.
- The first bullet under each release heading in `CHANGELOG.md` must be one
concise, user-facing sentence. It is the canonical group announcement
summary.
- Do not mark production complete merely because code was committed. Production
is complete only after staging, cutover, acceptance, observation, and backup
restore checks pass.
@@ -61,6 +66,26 @@ Before touching production runtime files on production host:
SQLite gates.
- If any gate fails, confirm rollback state before retrying.
## Mandatory Post-Release Settlement
- After cutover and rollback gates pass, run the installed
`jwebot_release_update.py`.
- The settlement grants exactly 200 Mora to every account that exists at the
start of that release settlement and appends a version-labelled account
movement.
- Compensation uses the full release string as its idempotency key. Rerunning
the same release must not grant Mora twice.
- The tool sends one guarded announcement containing the version, the first
changelog bullet, and the `Mora x200` compensation notice.
- Announcement state is tracked separately from compensation. A missing or
uncertain GUI receipt must never roll back a healthy runtime or trigger
another grant.
- If delivery is uncertain, inspect the configured group before using
`--retry-uncertain-announcement`; never retry blindly.
- Production is not complete until the settlement reports success, SQLite
`quick_check` is `ok`, queues are empty, and service restart counts remain
stable.
## Backup And Restore
- Pre-deploy and post-deploy baselines must include runtime files, systemd
+28 -1
View File
@@ -73,7 +73,12 @@ The deployer prints an eight-stage progress bar and:
- leaves legacy OCR disabled;
- verifies Edge health and adaptive restart stability;
- writes and verifies backup SHA-256 checksums;
- rolls back files, state, and unit topology on failure.
- rolls back files, state, and unit topology on cutover failure;
- after rollback is disarmed, grants each existing account 200 Mora, records a
version-labelled movement, and sends the guarded group update announcement.
The first bullet in the current `CHANGELOG.md` release section is the canonical
one-sentence announcement summary. Keep it concise and user-facing.
## Post-Cutover
@@ -104,6 +109,28 @@ PY
Player-visible acceptance must use deliberate synthetic messages in the
configured group and confirm exactly one ACK and one final response.
The deployer also runs the installed release settlement tool. Its successful
JSON result must report the expected account count and announcement delivery.
The operation is idempotent by the full release string, so rerunning the same
command never grants Mora twice:
```sh
/opt/jwebot/venv/bin/python \
~/.local/lib/wechat-health-watch/jwebot_release_update.py
```
If it reports `AnnouncementUncertainError`, inspect the configured group first.
Only when the announcement is confirmed absent may the operator retry:
```sh
/opt/jwebot/venv/bin/python \
~/.local/lib/wechat-health-watch/jwebot_release_update.py \
--retry-uncertain-announcement
```
Do not repeat the full deployment to repair an uncertain announcement. The
runtime cutover and version-keyed compensation may already be complete.
## Rollback
If any gate fails:
+1 -1
View File
@@ -1,6 +1,6 @@
# Runtime Telemetry And ACK Semantics
Status: local instrumentation contract for `alpha 0.6.x`.
Status: local instrumentation contract for `beta 1.0.x`.
## What ACK Means
+10 -6
View File
@@ -1,20 +1,24 @@
# Versioning
JWeBot uses an alpha version string:
JWeBot uses a human-readable product version string:
```text
alpha X.Y.Z.MMDD
beta X.Y.Z.MMDD
```
Rules:
- `alpha` means the public API and deployment process may still change.
- `beta` means the project has passed the local, staging, privacy, and
production-safety gates expected for wider testing, while still allowing
controlled breaking changes before a stable release.
- `X.Y.Z` is the project version.
- `MMDD` is the delivery date.
- Python package versions use PEP 440 pre-release form. For beta builds,
`beta 1.0.1.1016` maps to `1.0.1b1+1016`.
- Normal small updates increment only the patch version, for example
`alpha 0.0.1.1014` to `alpha 0.0.2.1014`.
- Minor versions such as `0.1.x` require explicit user review and milestone
approval.
`beta 1.0.1.1016` to `beta 1.0.2.1016`.
- Minor, major, or stable-release changes require explicit user review and
milestone approval.
- Local repository migration does not imply a production deployment.
- Production services must not be changed unless the operator explicitly asks
for deployment or runtime verification.
+22
View File
@@ -0,0 +1,22 @@
# coin-loan
Author: `jwebot`
贷款
Help:
- `/贷款;/还款`
Registered commands:
- `/贷款`
- `/借款`
- `/loan`
- `/borrow`
- `/还款`
- `/repay`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-loan` Kernel service profile
and returns a structured `ReplyPlan`.
+14
View File
@@ -0,0 +1,14 @@
id = "coin-loan"
author = "jwebot"
description = "贷款"
help = ["/贷款;/还款"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_loan:plugin"
type = "command"
[capabilities]
commands = ["/贷款", "/借款", "/loan", "/borrow", "/还款", "/repay"]
storage = false
network_profiles = []
service_profiles = ["coin-loan"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-loan."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-loan'
AUTHOR = 'jwebot'
DESCRIPTION = '贷款'
HELP_ENTRIES = ('/贷款;/还款',)
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from coin_loan import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '贷款')
self.assertEqual(HELP_ENTRIES, ('/贷款;/还款',))
self.assertIn('/贷款', registered)
self.assertEqual(len(registered), 6)
if __name__ == "__main__":
unittest.main()
+22
View File
@@ -0,0 +1,22 @@
# coin-ranking
Author: `jwebot`
富榜
Help:
- `/大富翁榜`
Registered commands:
- `/摩拉排行`
- `/摩拉排行榜`
- `/金币排行`
- `/金币排行榜`
- `/大富翁榜`
- `/richlist`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-ranking` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "coin-ranking"
author = "jwebot"
description = "富榜"
help = ["/大富翁榜"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_ranking:plugin"
type = "command"
[capabilities]
commands = ["/摩拉排行", "/摩拉排行榜", "/金币排行", "/金币排行榜", "/大富翁榜", "/richlist"]
storage = false
network_profiles = []
service_profiles = ["coin-ranking"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-ranking."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-ranking'
AUTHOR = 'jwebot'
DESCRIPTION = '富榜'
HELP_ENTRIES = ('/大富翁榜',)
COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from coin_ranking import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '富榜')
self.assertEqual(HELP_ENTRIES, ('/大富翁榜',))
self.assertIn('/摩拉排行', registered)
self.assertEqual(len(registered), 6)
if __name__ == "__main__":
unittest.main()
+19
View File
@@ -0,0 +1,19 @@
# coin-taffy
Author: `jwebot`
Taffy摩拉
Help:
- `/Taffy摩拉(兼容 /Taffy金币)`
Registered commands:
- `/Taffy摩拉`
- `/Taffy金币`
- `/taffycoins`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-taffy` Kernel service profile
and returns a structured `ReplyPlan`.
+14
View File
@@ -0,0 +1,14 @@
id = "coin-taffy"
author = "jwebot"
description = "Taffy摩拉"
help = ["/Taffy摩拉(兼容 /Taffy金币)"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_taffy:plugin"
type = "command"
[capabilities]
commands = ["/Taffy摩拉", "/Taffy金币", "/taffycoins"]
storage = false
network_profiles = []
service_profiles = ["coin-taffy"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-taffy'
AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy摩拉'
HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',)
COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from coin_taffy import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, 'Taffy摩拉')
self.assertEqual(HELP_ENTRIES, ('/Taffy摩拉(兼容 /Taffy金币)',))
self.assertIn('/Taffy摩拉', registered)
self.assertEqual(len(registered), 3)
if __name__ == "__main__":
unittest.main()
+18
View File
@@ -0,0 +1,18 @@
# coin-transfer
Author: `jwebot`
摩拉转账
Help:
- `/转账 对方 金额;/transfer`
Registered commands:
- `/转账`
- `/transfer`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-transfer` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "coin-transfer"
author = "jwebot"
description = "摩拉转账"
help = ["/转账 对方 金额;/transfer"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_transfer:plugin"
type = "command"
[capabilities]
commands = ["/转账", "/transfer"]
storage = false
network_profiles = []
service_profiles = ["coin-transfer"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-transfer."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-transfer'
AUTHOR = 'jwebot'
DESCRIPTION = '摩拉转账'
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
COMMANDS = ('/转账', '/transfer')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from coin_transfer import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '摩拉转账')
self.assertEqual(HELP_ENTRIES, ('/转账 对方 金额;/transfer',))
self.assertIn('/转账', registered)
self.assertEqual(len(registered), 2)
if __name__ == "__main__":
unittest.main()
+3 -22
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for coin."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抛一枚硬币'
HELP_ENTRIES = ('/翻硬币',)
COMMANDS = ('/翻硬币',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,30 @@
# cs-major-autochess
Author: `jwebot`
自走棋
Help:
- `/自走棋 开始|报名|选`
Registered commands:
- `/自走棋`
- `/cs自走棋`
- `/autochess`
- `/选`
- `/刷新`
- `/锁定`
- `/战术`
- `/升级教练`
- `/上阵`
- `/下阵`
- `/出售`
- `/阵容`
- `/赛程`
- `/结束回合`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `cs-major-autochess` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "cs-major-autochess"
author = "jwebot"
description = "自走棋"
help = ["/自走棋 开始|报名|选"]
version = "1.0.0"
api_version = "1"
entrypoint = "cs_major_autochess:plugin"
type = "command"
[capabilities]
commands = ["/自走棋", "/cs自走棋", "/autochess", "/选", "/刷新", "/锁定", "/战术", "/升级教练", "/上阵", "/下阵", "/出售", "/阵容", "/赛程", "/结束回合"]
storage = false
network_profiles = []
service_profiles = ["cs-major-autochess"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for cs-major-autochess."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'cs-major-autochess'
AUTHOR = 'jwebot'
DESCRIPTION = '自走棋'
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from cs_major_autochess import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '自走棋')
self.assertEqual(HELP_ENTRIES, ('/自走棋 开始|报名|选',))
self.assertIn('/自走棋', registered)
self.assertEqual(len(registered), 14)
if __name__ == "__main__":
unittest.main()
@@ -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,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '生成当天稳定一致的运势'
HELP_ENTRIES = ('/今日运势',)
COMMANDS = ('/今日运势',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+3 -22
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for dice."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'dice'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '骰子和多人骰子挑战'
HELP_ENTRIES = ('/丢骰子 [NdM]', '/丢骰子挑战 [5d6|5d20|状态|结算|战绩|结束]')
COMMANDS = ('/丢骰子', '/骰子', '/丢骰子挑战', '/骰子挑战')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+3 -22
View File
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for draw-lot."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'draw-lot'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抽取一条本地签文'
HELP_ENTRIES = ('/抽签',)
COMMANDS = ('/抽签',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+19
View File
@@ -0,0 +1,19 @@
# game-history
Author: `jwebot`
游戏记录
Help:
- `/游戏记录 /个人记录 /gamehistory`
Registered commands:
- `/游戏记录`
- `/个人记录`
- `/gamehistory`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `game-history` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "game-history"
author = "jwebot"
description = "游戏记录"
help = ["/游戏记录 /个人记录 /gamehistory"]
version = "1.0.0"
api_version = "1"
entrypoint = "game_history:plugin"
type = "command"
[capabilities]
commands = ["/游戏记录", "/个人记录", "/gamehistory"]
storage = false
network_profiles = []
service_profiles = ["game-history"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for game-history."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'game-history'
AUTHOR = 'jwebot'
DESCRIPTION = '游戏记录'
HELP_ENTRIES = ('/游戏记录 /个人记录 /gamehistory',)
COMMANDS = ('/游戏记录', '/个人记录', '/gamehistory')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from game_history import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '游戏记录')
self.assertEqual(HELP_ENTRIES, ('/游戏记录 /个人记录 /gamehistory',))
self.assertIn('/游戏记录', registered)
self.assertEqual(len(registered), 3)
if __name__ == "__main__":
unittest.main()
@@ -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,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '群内持久化猜数字游戏'
HELP_ENTRIES = ('/猜数字 开始 [上限];/猜 数字;/猜数字 [状态|结束]',)
COMMANDS = ('/猜数字', '/猜')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for history-search."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'history-search'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '搜索本地群聊文字记录'
HELP_ENTRIES = ('/查记录 关键词',)
COMMANDS = ('/查记录', '/历史搜索')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,25 @@
# primogem-economy
Author: `jwebot`
原石系统
Help:
- `/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容`
Registered commands:
- `/原石祈愿`
- `/祈愿`
- `/wish`
- `/原石榜`
- `/primogem榜`
- `/扫码加权`
- `/扫码概率`
- `/Taffy发话`
- `/taffysay`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `primogem-economy` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "primogem-economy"
author = "jwebot"
description = "原石系统"
help = ["/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容"]
version = "1.0.0"
api_version = "1"
entrypoint = "primogem_economy:plugin"
type = "command"
[capabilities]
commands = ["/原石祈愿", "/祈愿", "/wish", "/原石榜", "/primogem榜", "/扫码加权", "/扫码概率", "/Taffy发话", "/taffysay"]
storage = false
network_profiles = []
service_profiles = ["primogem-economy"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for primogem-economy."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'primogem-economy'
AUTHOR = 'jwebot'
DESCRIPTION = '原石系统'
HELP_ENTRIES = ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',)
COMMANDS = ('/原石祈愿', '/祈愿', '/wish', '/原石榜', '/primogem榜', '/扫码加权', '/扫码概率', '/Taffy发话', '/taffysay')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from primogem_economy import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '原石系统')
self.assertEqual(HELP_ENTRIES, ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',))
self.assertIn('/原石祈愿', registered)
self.assertEqual(len(registered), 9)
if __name__ == "__main__":
unittest.main()
@@ -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,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '在指定整数范围内取随机数'
HELP_ENTRIES = ('/随机数 最小值 最大值',)
COMMANDS = ('/随机数',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for repeat-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'repeat-ranking'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '查看北京时间今日复读排行'
HELP_ENTRIES = ('/复读榜',)
COMMANDS = ('/复读榜', '/复读')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for rock-paper-scissors."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'rock-paper-scissors'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '多人石头剪刀布游戏'
HELP_ENTRIES = ('/石头剪刀布 [开始|加入|出 选择|状态|结算|结束]',)
COMMANDS = ('/石头剪刀布',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for scan-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'scan-ranking'
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '查看北京时间今日扫码排行'
HELP_ENTRIES = ('/扫码排行',)
COMMANDS = ('/扫码排行', '/扫码榜', '/今日扫码')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,32 @@
# single-player-games
Author: `jwebot`
摩拉游戏
Help:
- `/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助`
Registered commands:
- `/摩拉`
- `/金币`
- `/wallet`
- `/原石`
- `/许愿池`
- `/许愿`
- `/抽奖`
- `/draw`
- `/彩票`
- `/买彩票`
- `/lottery`
- `/21点`
- `/二十一点`
- `/blackjack`
- `/游戏帮助`
- `/games`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `single-player-games` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "single-player-games"
author = "jwebot"
description = "摩拉游戏"
help = ["/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助"]
version = "1.0.0"
api_version = "1"
entrypoint = "single_player_games:plugin"
type = "command"
[capabilities]
commands = ["/摩拉", "/金币", "/wallet", "/原石", "/许愿池", "/许愿", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏帮助", "/games"]
storage = false
network_profiles = []
service_profiles = ["single-player-games"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for single-player-games."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'single-player-games'
AUTHOR = 'jwebot'
DESCRIPTION = '摩拉游戏'
HELP_ENTRIES = ('/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助',)
COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/许愿池', '/许愿', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from single_player_games import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '摩拉游戏')
self.assertEqual(HELP_ENTRIES, ('/摩拉/原石 /许愿池 /彩票 /21点 /游戏帮助',))
self.assertIn('/摩拉', registered)
self.assertEqual(len(registered), 16)
if __name__ == "__main__":
unittest.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,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = 'Steam 状态与时长'
HELP_ENTRIES = ('/steam;/steam时长 游戏[、玩家]',)
COMMANDS = ('/steam',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+18
View File
@@ -0,0 +1,18 @@
# trivia-quiz
Author: `jwebot`
冷门知识问答
Help:
- `/回答 A;/answer A`
Registered commands:
- `/回答`
- `/answer`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `trivia-quiz` Kernel service profile
and returns a structured `ReplyPlan`.
+14
View File
@@ -0,0 +1,14 @@
id = "trivia-quiz"
author = "jwebot"
description = "冷门知识问答"
help = ["/回答 A;/answer A"]
version = "1.0.0"
api_version = "1"
entrypoint = "trivia_quiz:plugin"
type = "command"
[capabilities]
commands = ["/回答", "/answer"]
storage = false
network_profiles = []
service_profiles = ["trivia-quiz"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for trivia-quiz."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'trivia-quiz'
AUTHOR = 'jwebot'
DESCRIPTION = '冷门知识问答'
HELP_ENTRIES = ('/回答 A;/answer A',)
COMMANDS = ('/回答', '/answer')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from trivia_quiz import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, '冷门知识问答')
self.assertEqual(HELP_ENTRIES, ('/回答 A;/answer A',))
self.assertIn('/回答', registered)
self.assertEqual(len(registered), 2)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,18 @@
# context-summary
Author: `official`
上下文总结
Help:
- `/上下文 [100|200|300];/context [100|200|300]`
Registered commands:
- `/上下文`
- `/context`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `context-summary` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "context-summary"
author = "official"
description = "上下文总结"
help = ["/上下文 [100|200|300];/context [100|200|300]"]
version = "1.0.0"
api_version = "1"
entrypoint = "context_summary:plugin"
type = "command"
[capabilities]
commands = ["/上下文", "/context"]
storage = false
network_profiles = []
service_profiles = ["context-summary"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for context-summary."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'context-summary'
AUTHOR = 'official'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from context_summary import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'official')
self.assertEqual(DESCRIPTION, '上下文总结')
self.assertEqual(HELP_ENTRIES, ('/上下文 [100|200|300];/context [100|200|300]',))
self.assertIn('/上下文', registered)
self.assertEqual(len(registered), 2)
if __name__ == "__main__":
unittest.main()
@@ -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,30 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '埋点 p50/p95'
HELP_ENTRIES = ('/debug',)
COMMANDS = ('/debug',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+2 -1
View File
@@ -6,10 +6,11 @@ JWeBot 版本
Help:
- `/version`
- `/version /版本`
Registered commands:
- `/版本`
- `/version`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
@@ -1,14 +1,14 @@
id = "jwebot-version"
author = "official"
description = "JWeBot 版本"
help = ["/version"]
help = ["/version /版本"]
version = "1.0.0"
api_version = "1"
entrypoint = "jwebot_version:plugin"
type = "command"
[capabilities]
commands = ["/version"]
commands = ["/版本", "/version"]
storage = false
network_profiles = []
service_profiles = ["jwebot-version"]
@@ -1,37 +1,18 @@
"""Generated reviewed adapter for jwebot-version."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'jwebot-version'
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(
HELP_ENTRIES = ('/version /版本',)
COMMANDS = ('/版本', '/version')
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -20,9 +20,9 @@ class PluginTests(unittest.TestCase):
]
self.assertEqual(AUTHOR, 'official')
self.assertEqual(DESCRIPTION, 'JWeBot 版本')
self.assertEqual(HELP_ENTRIES, ('/version',))
self.assertIn('/version', registered)
self.assertEqual(len(registered), 1)
self.assertEqual(HELP_ENTRIES, ('/version /版本',))
self.assertIn('/版本', registered)
self.assertEqual(len(registered), 2)
if __name__ == "__main__":
@@ -0,0 +1,22 @@
# person-directory
Author: `official`
人物索引
Help:
- `/人物列表 [页码] /people;/人物 [名称或别名] /person`
Registered commands:
- `/人物列表`
- `/别名列表`
- `/people`
- `/人物`
- `/个人属性`
- `/person`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `person-directory` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "person-directory"
author = "official"
description = "人物索引"
help = ["/人物列表 [页码] /people;/人物 [名称或别名] /person"]
version = "1.0.0"
api_version = "1"
entrypoint = "person_directory:plugin"
type = "command"
[capabilities]
commands = ["/人物列表", "/别名列表", "/people", "/人物", "/个人属性", "/person"]
storage = false
network_profiles = []
service_profiles = ["person-directory"]
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for person-directory."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'person-directory'
AUTHOR = 'official'
DESCRIPTION = '人物索引'
HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)
COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,29 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from person_directory import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'official')
self.assertEqual(DESCRIPTION, '人物索引')
self.assertEqual(HELP_ENTRIES, ('/人物列表 [页码] /people;/人物 [名称或别名] /person',))
self.assertIn('/人物列表', registered)
self.assertEqual(len(registered), 6)
if __name__ == "__main__":
unittest.main()
@@ -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,30 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '管理个人和同群人物记忆'
HELP_ENTRIES = ('/记住 内容;/我的记忆;/忘记 关键词或全部;/人物记忆 [显示名]',)
COMMANDS = ('/记住', '/我的记忆', '/忘记', '/人物记忆', '/记忆帮助')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -1,6 +1,6 @@
"""Generated reviewed adapter for server-status."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'server-status'
@@ -8,30 +8,11 @@ AUTHOR = 'official'
DESCRIPTION = '服务器状态'
HELP_ENTRIES = ('/状态;/status',)
COMMANDS = ('/状态', '/status')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
+730
View File
@@ -0,0 +1,730 @@
#!/usr/bin/env python3
"""Apply the mandatory release lifecycle notices, grant, and announcement."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import sqlite3
import subprocess
import sys
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-official-extensions" / "src"),
str(ROOT / "packages" / "jwebot-sdk" / "src"),
]
from jwebot_official_extensions import __version__ as OFFICIAL_VERSION # noqa: E402
from jwebot_official_extensions.player_games import ( # noqa: E402
append_coin_event,
append_primogem_event,
normalize_player_account,
)
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
MAX_ANNOUNCEMENT_CHARS = 900
MAX_LIFECYCLE_CHARS = 300
LIFECYCLE_STAGES = frozenset({"shutdown", "startup"})
RELEASE_PATTERN = re.compile(
r"^beta (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<date>\d{4})$"
)
CHANGELOG_HEADING = re.compile(
r"^##\s+(?P<release>beta \d+\.\d+\.\d+\.\d{4})"
r"(?:\s+-\s+\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?)?\s*$"
)
class ReleaseUpdateError(RuntimeError):
"""Raised when release settlement cannot complete safely."""
class AnnouncementUncertainError(ReleaseUpdateError):
"""Raised when an announcement may have been sent without a final receipt."""
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def isoformat_utc(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat(
timespec="milliseconds"
).replace("+00:00", "Z")
def release_to_pep440(release: str) -> str:
match = RELEASE_PATTERN.fullmatch(release)
if match is None:
raise ReleaseUpdateError(f"unsupported release format: {release}")
return (
f"{match.group('major')}.{match.group('minor')}."
f"{match.group('patch')}b1+{match.group('date')}"
)
def read_release(path: Path) -> str:
try:
release = path.read_text(encoding="utf-8").strip()
except OSError as error:
raise ReleaseUpdateError("cannot read VERSION") from error
release_to_pep440(release)
return release
def read_changelog_summary(path: Path, release: str) -> str:
try:
lines = path.read_text(encoding="utf-8").splitlines()
except OSError as error:
raise ReleaseUpdateError("cannot read CHANGELOG.md") from error
section_index: int | None = None
for index, line in enumerate(lines):
match = CHANGELOG_HEADING.fullmatch(line)
if match is not None and match.group("release") == release:
section_index = index
break
if section_index is None:
raise ReleaseUpdateError("release is missing from CHANGELOG.md")
parts: list[str] = []
collecting = False
for line in lines[section_index + 1 :]:
if line.startswith("## "):
break
if line.startswith("- "):
if collecting:
break
collecting = True
parts.append(line[2:].strip())
continue
if collecting:
if line.startswith(" "):
parts.append(line.strip())
continue
if not line.strip():
break
summary = " ".join(" ".join(parts).split())
if not summary:
raise ReleaseUpdateError("release changelog needs a first summary bullet")
if len(summary) > MAX_SUMMARY_CHARS:
raise ReleaseUpdateError("release changelog summary is too long")
return summary
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{mora}{extra}\n"
"补偿范围:截至本公告发布前已创建摩拉账户的玩家\n"
"发放方式:补偿已直接到账,无需领取\n"
"感谢大家对 Taffy 的支持与理解,祝大家游戏愉快!"
)
if len(announcement) > MAX_ANNOUNCEMENT_CHARS:
raise ReleaseUpdateError("release announcement is too long")
return announcement
def build_lifecycle_message(release: str, stage: str) -> str:
if stage == "shutdown":
if release == "beta 2.0.0.1021":
message = (
"Taffy 准备停机更新,本次更新是架构级大版本更新,"
f"预期版本{release},离线的时候保持耐心迎接Taffy的归来哦~"
)
else:
message = (
f"Taffy 准备停机更新到 {release},会短暂离线一下;"
"别慌,我很快回来喵"
)
elif stage == "startup":
message = (
f"Taffy 刚上线,正在做 {release} 的最后自检;"
"确认就绪后会发送更新内容和补偿说明喵"
)
else:
raise ReleaseUpdateError(f"unsupported lifecycle stage: {stage}")
if len(message) > MAX_LIFECYCLE_CHARS:
raise ReleaseUpdateError("release lifecycle message is too long")
return message
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))
def announcement_key(release: str, group_fingerprint: str) -> str:
group_hash = hashlib.sha256(group_fingerprint.encode()).hexdigest()
return f"release_announcement:{release}:{group_hash}"
def release_slug(release: str) -> str:
return re.sub(r"[^a-zA-Z0-9]+", "-", release).strip("-").lower()
def release_groups(database_path: Path) -> list[str]:
connection = sqlite3.connect(f"file:{database_path}?mode=ro", uri=True)
try:
if connection.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise ReleaseUpdateError("Edge SQLite integrity check failed")
rows = connection.execute(
"""
SELECT DISTINCT group_fingerprint
FROM player_accounts
ORDER BY group_fingerprint
"""
).fetchall()
finally:
connection.close()
return [str(row[0]) for row in rows]
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)
except json.JSONDecodeError as error:
raise ReleaseUpdateError("release compensation marker is invalid") from error
if (
not isinstance(marker, dict)
or marker.get("release") != release
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)
):
raise ReleaseUpdateError("release compensation marker does not match")
return marker
def backup_database(
source: sqlite3.Connection,
backup_dir: Path,
release: str,
now: datetime,
) -> Path:
backup_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
os.chmod(backup_dir, 0o700)
stamp = now.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
destination = backup_dir / (
f"edge-before-{release_slug(release)}-compensation-{stamp}.sqlite3"
)
target = sqlite3.connect(destination)
try:
source.backup(target)
if target.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise ReleaseUpdateError("release compensation backup failed integrity check")
finally:
target.close()
destination.chmod(0o600)
return destination
def load_compensation_marker(
connection: sqlite3.Connection,
release: str,
) -> dict[str, Any] | None:
row = connection.execute(
"SELECT value FROM metadata WHERE key = ?",
(compensation_key(release),),
).fetchone()
if row is None:
return None
return checked_marker(str(row[0]), release)
def apply_compensation(
database_path: Path,
backup_dir: Path,
release: str,
*,
now: datetime | None = None,
) -> tuple[dict[str, Any], str]:
applied_at = now or utc_now()
connection = sqlite3.connect(database_path, timeout=30)
connection.row_factory = sqlite3.Row
try:
if connection.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise ReleaseUpdateError("Edge SQLite integrity check failed")
existing = load_compensation_marker(connection, release)
if existing is not None:
return existing, "already_applied"
backup_path = backup_database(
connection,
backup_dir,
release,
applied_at,
)
connection.execute("BEGIN IMMEDIATE")
try:
existing = load_compensation_marker(connection, release)
if existing is not None:
connection.execute("COMMIT")
return existing, "already_applied"
rows = connection.execute(
"""
SELECT group_fingerprint, person_key, state_json
FROM player_accounts
ORDER BY group_fingerprint, person_key
"""
).fetchall()
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} 版本更新补偿"
primogem_reason = f"{release} 版本更新补偿"
for row in rows:
try:
state = json.loads(str(row["state_json"]))
except json.JSONDecodeError as error:
raise ReleaseUpdateError("Mora account state is invalid") from error
if not isinstance(state, dict):
raise ReleaseUpdateError("Mora account state is invalid")
normalize_player_account(state)
balance = state.get("balance")
if (
type(balance) is not int
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 + mora_amount
state["primogems"] += primogem_amount
append_coin_event(state, mora_amount, event_reason)
state["coin_events"][-1]["created_at"] = applied_at.timestamp()
if primogem_amount:
append_primogem_event(
state,
primogem_amount,
primogem_reason,
)
state["primogem_events"][-1]["created_at"] = (
applied_at.timestamp()
)
connection.execute(
"""
UPDATE player_accounts
SET state_json = ?, updated_at = ?
WHERE group_fingerprint = ? AND person_key = ?
""",
(
json.dumps(state, ensure_ascii=False),
timestamp,
row["group_fingerprint"],
row["person_key"],
),
)
marker = {
"release": release,
"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 + mora_amount * len(rows)
),
"old_primogem_sum": old_primogem_sum,
"new_primogem_sum": (
old_primogem_sum + primogem_amount * len(rows)
),
"applied_at": timestamp,
"backup": backup_path.name,
}
connection.execute(
"""
INSERT INTO metadata (key, value, updated_at)
VALUES (?, ?, ?)
""",
(
compensation_key(release),
json.dumps(marker, ensure_ascii=False, separators=(",", ":")),
timestamp,
),
)
connection.execute("COMMIT")
return marker, "applied"
except Exception:
if connection.in_transaction:
connection.execute("ROLLBACK")
raise
finally:
connection.close()
def metadata_value(
connection: sqlite3.Connection,
key: str,
) -> dict[str, Any] | None:
row = connection.execute(
"SELECT value FROM metadata WHERE key = ?",
(key,),
).fetchone()
if row is None:
return None
try:
value = json.loads(str(row[0]))
except json.JSONDecodeError as error:
raise ReleaseUpdateError("announcement marker is invalid") from error
if not isinstance(value, dict):
raise ReleaseUpdateError("announcement marker is invalid")
return value
def store_metadata(
connection: sqlite3.Connection,
key: str,
value: dict[str, Any],
updated_at: str,
) -> None:
connection.execute(
"""
INSERT INTO metadata (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE
SET value = excluded.value, updated_at = excluded.updated_at
""",
(
key,
json.dumps(value, ensure_ascii=False, separators=(",", ":")),
updated_at,
),
)
def run_delivery_helper(
delivery_helper: Path,
group_fingerprint: str,
announcement: str,
event_id: str,
) -> None:
payload = {
"group_fingerprint": group_fingerprint,
"reply": announcement,
"event_id": event_id,
}
try:
result = subprocess.run(
[sys.executable, str(delivery_helper)],
input=json.dumps(payload, ensure_ascii=False),
text=True,
capture_output=True,
timeout=70,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as error:
raise AnnouncementUncertainError("announcement transport failed") from error
if result.returncode != 0:
raise AnnouncementUncertainError(
f"announcement helper failed with return code {result.returncode}"
)
def deliver_announcements(
database_path: Path,
release: str,
groups: list[str],
announcement: str,
delivery_helper: Path,
*,
retry_uncertain: bool = False,
deliverer: Callable[[Path, str, str, str], None] = run_delivery_helper,
now: Callable[[], datetime] = utc_now,
) -> dict[str, int]:
delivered = 0
skipped = 0
for group in groups:
key = announcement_key(release, group)
group_hash = hashlib.sha256(group.encode()).hexdigest()[:12]
connection = sqlite3.connect(database_path, timeout=30)
try:
existing = metadata_value(connection, key)
state = str(existing.get("state", "")) if existing else ""
if state == "delivered":
skipped += 1
continue
if state in {"sending", "uncertain"} and not retry_uncertain:
raise AnnouncementUncertainError(
"previous announcement delivery is uncertain; "
"visually confirm before using --retry-uncertain-announcement"
)
attempt = int(existing.get("attempt", 0)) + 1 if existing else 1
started_at = isoformat_utc(now())
with connection:
store_metadata(
connection,
key,
{
"release": release,
"state": "sending",
"attempt": attempt,
"group_hash": group_hash,
"started_at": started_at,
},
started_at,
)
finally:
connection.close()
event_id = f"release-{release_slug(release)}-{group_hash}"
try:
deliverer(
delivery_helper,
group,
announcement,
event_id,
)
except Exception as error:
failed_at = isoformat_utc(now())
connection = sqlite3.connect(database_path, timeout=30)
try:
with connection:
store_metadata(
connection,
key,
{
"release": release,
"state": "uncertain",
"attempt": attempt,
"group_hash": group_hash,
"started_at": started_at,
"failed_at": failed_at,
"error_type": type(error).__name__[:80],
},
failed_at,
)
finally:
connection.close()
if isinstance(error, ReleaseUpdateError):
raise
raise AnnouncementUncertainError(
"announcement delivery is uncertain"
) from error
completed_at = isoformat_utc(now())
connection = sqlite3.connect(database_path, timeout=30)
try:
with connection:
store_metadata(
connection,
key,
{
"release": release,
"state": "delivered",
"attempt": attempt,
"group_hash": group_hash,
"started_at": started_at,
"delivered_at": completed_at,
},
completed_at,
)
finally:
connection.close()
delivered += 1
return {"delivered": delivered, "skipped": skipped}
def run_release_update(
*,
release_file: Path,
changelog: Path,
database_path: Path,
backup_dir: Path,
delivery_helper: Path,
retry_uncertain: bool = False,
deliverer: Callable[[Path, str, str, str], None] = run_delivery_helper,
applied_at: datetime | None = None,
clock: Callable[[], datetime] = utc_now,
) -> dict[str, Any]:
release = read_release(release_file)
expected_package_version = release_to_pep440(release)
if OFFICIAL_VERSION != expected_package_version:
raise ReleaseUpdateError(
"installed package version does not match VERSION"
)
summary = read_changelog_summary(changelog, release)
announcement = build_announcement(release, summary)
marker, compensation_state = apply_compensation(
database_path,
backup_dir,
release,
now=applied_at,
)
groups = [str(value) for value in marker["group_fingerprints"]]
announcement_result = deliver_announcements(
database_path,
release,
groups,
announcement,
delivery_helper,
retry_uncertain=retry_uncertain,
deliverer=deliverer,
now=clock,
)
connection = sqlite3.connect(
f"file:{database_path}?mode=ro",
uri=True,
)
try:
if connection.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise ReleaseUpdateError("post-release SQLite integrity check failed")
finally:
connection.close()
return {
"status": "ok",
"release": release,
"compensation": compensation_state,
"amount_each": mora_compensation_amount(release),
"primogems_each": primogem_compensation_amount(release),
"accounts": int(marker["accounts"]),
"groups": len(groups),
"announcements_delivered": announcement_result["delivered"],
"announcements_skipped": announcement_result["skipped"],
"backup": str(marker["backup"]),
}
def run_release_lifecycle(
*,
release_file: Path,
database_path: Path,
delivery_helper: Path,
stage: str,
deliverer: Callable[[Path, str, str, str], None] = run_delivery_helper,
) -> dict[str, Any]:
if stage not in LIFECYCLE_STAGES:
raise ReleaseUpdateError(f"unsupported lifecycle stage: {stage}")
release = read_release(release_file)
message = build_lifecycle_message(release, stage)
groups = release_groups(database_path)
delivered = 0
for group in groups:
group_hash = hashlib.sha256(group.encode()).hexdigest()[:12]
event_id = f"release-{release_slug(release)}-{stage}-{group_hash}"
deliverer(delivery_helper, group, message, event_id)
delivered += 1
return {
"status": "ok",
"release": release,
"stage": stage,
"groups": len(groups),
"delivered": delivered,
}
def parse_args() -> argparse.Namespace:
home = Path.home()
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--release-file",
type=Path,
default=ROOT / "VERSION",
)
parser.add_argument(
"--changelog",
type=Path,
default=ROOT / "CHANGELOG.md",
)
parser.add_argument(
"--database",
type=Path,
default=home / ".local/state/jwebot-edge/edge.sqlite3",
)
parser.add_argument(
"--backup-dir",
type=Path,
default=home / ".local/state/jwebot/admin-backups",
)
parser.add_argument(
"--delivery-helper",
type=Path,
default=ROOT / "jwebot_edge_delivery.py",
)
parser.add_argument(
"--retry-uncertain-announcement",
action="store_true",
help="retry only after visually confirming that the prior send did not land",
)
parser.add_argument(
"--lifecycle-stage",
choices=sorted(LIFECYCLE_STAGES),
help="send a guarded lifecycle notice instead of release settlement",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
if args.lifecycle_stage:
result = run_release_lifecycle(
release_file=args.release_file,
database_path=args.database,
delivery_helper=args.delivery_helper,
stage=args.lifecycle_stage,
)
else:
result = run_release_update(
release_file=args.release_file,
changelog=args.changelog,
database_path=args.database,
backup_dir=args.backup_dir,
delivery_helper=args.delivery_helper,
retry_uncertain=args.retry_uncertain_announcement,
)
except ReleaseUpdateError as error:
print(
json.dumps(
{
"status": "error",
"error_type": type(error).__name__,
"message": str(error),
},
ensure_ascii=False,
),
file=sys.stderr,
)
return 1
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "0.6.0a0+1015"
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__ = "0.6.0a0+1015"
__version__ = "2.0.5b1+1023"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "0.6.0a0+1015"
version = "2.0.5b1+1023"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.0a0+1015",
"jwebot-kernel==0.6.0a0+1015",
"jwebot-sdk==2.0.5b1+1023",
"jwebot-kernel==2.0.5b1+1023",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "0.6.0a0+1015"
__version__ = "2.0.5b1+1023"
+7 -4
View File
@@ -4,15 +4,18 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.0a0+1015"
version = "2.0.5b1+1023"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.0a0+1015",
"jwebot-sdk==0.6.0a0+1015",
"jwebot-official-extensions==0.6.0a0+1015",
"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__ = "0.6.0a0+1015"
__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,12 +4,15 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "0.6.0a0+1015"
version = "2.0.5b1+1023"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.0a0+1015",
"jwebot-sdk==2.0.5b1+1023",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
jwebot_official_extensions = ["data/cs_autochess/*.json"]
@@ -1,5 +1,18 @@
"""Reviewed JWeBot local command adapters and broker implementation."""
from .cs_autochess import (
CARD_BY_ID,
COACH_BONUSES,
COACH_POOL,
MAX_PLAYERS,
MIN_PLAYERS,
PLAYER_POOL,
CsMajorAutochess,
build_round_plan,
lineup_power,
matchup_modifier,
parse_autochess_action,
)
from .descriptors import (
DIRECT_KERNEL_COMMANDS,
DIRECT_KERNEL_COMMAND_KINDS,
@@ -20,6 +33,8 @@ from .local_commands import (
LocalCommandError,
deterministic_int,
local_command_parts,
pack_reply_parts,
unpack_reply_parts,
)
from .runtime import (
LocalExtensionError,
@@ -35,6 +50,10 @@ from .steam import (
)
__all__ = [
"CARD_BY_ID",
"COACH_BONUSES",
"COACH_POOL",
"CsMajorAutochess",
"DICE_CHALLENGE_TIMEOUT_SECONDS",
"DEFAULT_STEAM_MIHOMO_PROXY_URL",
"DIRECT_KERNEL_COMMANDS",
@@ -43,8 +62,12 @@ __all__ = [
"LOCAL_COMMAND_ALIASES",
"LOCAL_EXTENSIONS",
"LOCAL_GAME_TIMEOUT_SECONDS",
"MAX_PLAYERS",
"MAX_RPS_PLAYERS",
"MIN_PLAYERS",
"OFFICIAL_AUTHOR",
"PLAYER_POOL",
"build_round_plan",
"KernelCommandDescriptor",
"LocalExtensionDescriptor",
"LocalExtensionError",
@@ -58,8 +81,13 @@ __all__ = [
"deterministic_int",
"local_command_parts",
"local_extension_command_token",
"lineup_power",
"matchup_modifier",
"pack_reply_parts",
"parse_autochess_action",
"unpack_reply_parts",
"extension_for_kind",
"render_feature_list",
]
__version__ = "0.6.0a0+1015"
__version__ = "2.0.5b1+1023"
@@ -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,30 +8,11 @@ AUTHOR = 'jwebot'
DESCRIPTION = '抛一枚硬币'
HELP_ENTRIES = ('/翻硬币',)
COMMANDS = ('/翻硬币',)
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
ADAPTER = ServiceCommandAdapter(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-loan."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-loan'
AUTHOR = 'jwebot'
DESCRIPTION = '贷款'
HELP_ENTRIES = ('/贷款;/还款',)
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-ranking."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-ranking'
AUTHOR = 'jwebot'
DESCRIPTION = '富榜'
HELP_ENTRIES = ('/大富翁榜',)
COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-taffy'
AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy摩拉'
HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',)
COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for coin-transfer."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'coin-transfer'
AUTHOR = 'jwebot'
DESCRIPTION = '摩拉转账'
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
COMMANDS = ('/转账', '/transfer')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for context-summary."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'context-summary'
AUTHOR = 'official'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
@@ -0,0 +1,18 @@
"""Generated reviewed adapter for cs-major-autochess."""
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
PROFILE = 'cs-major-autochess'
AUTHOR = 'jwebot'
DESCRIPTION = '自走棋'
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)

Some files were not shown because too many files have changed in this diff Show More