Compare commits
37
Commits
Alpha_0.6.0.1015
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffd0b2a16 | ||
|
|
6375ab5c79 | ||
|
|
22f5240fa7 | ||
|
|
645a282b22 | ||
|
|
e4365841ed | ||
|
|
5a3c4fb94c | ||
|
|
5e309f0b8f | ||
|
|
ed2fd0200f | ||
|
|
8670745235 | ||
|
|
eb31446076 | ||
|
|
5300898bc3 | ||
|
|
ac630576c7 | ||
|
|
3a7c11e44d | ||
|
|
e6d6ddf1c6 | ||
|
|
ddebe075d7 | ||
|
|
522e349491 | ||
|
|
35ef82f14f | ||
|
|
b1d8e4e8e7 | ||
|
|
127fea8cd0 | ||
|
|
bf6b738baa | ||
|
|
22edf6ff15 | ||
|
|
572159f571 | ||
|
|
99a251c19a | ||
|
|
3fa04755d0 | ||
|
|
afc71e63a7 | ||
|
|
bf4f28f48e | ||
|
|
148ae672f4 | ||
|
|
bbddb5be6f | ||
|
|
2bb2effc04 | ||
|
|
5125b827c0 | ||
|
|
3dea42b268 | ||
|
|
a74147e7f1 | ||
|
|
9fff57134f | ||
|
|
e713ff369a | ||
|
|
f07ae653c7 | ||
|
|
0731e5cebb | ||
|
|
f7fedc54e4 |
@@ -67,6 +67,7 @@ id_ed25519*
|
|||||||
|
|
||||||
# Sensitive local notes
|
# Sensitive local notes
|
||||||
homeserver-apikey-*.md
|
homeserver-apikey-*.md
|
||||||
|
debug-*.md
|
||||||
|
|
||||||
# macOS/editor
|
# macOS/editor
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ These rules are mandatory for every automated change in this repository.
|
|||||||
|
|
||||||
## Versioning
|
## 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`.
|
- 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
|
- Keep `VERSION`, root `pyproject.toml`, package versions, package
|
||||||
`__version__`, `README.md`, and `CHANGELOG.md` synchronized.
|
`__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.
|
or adaptive restart-stability gates.
|
||||||
- If a deployment gate fails, verify rollback state before retrying.
|
- 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
|
## Configuration And Access
|
||||||
|
|
||||||
- Real profiles live under `~/.config/jwebot/`, never in the repository.
|
- Real profiles live under `~/.config/jwebot/`, never in the repository.
|
||||||
|
|||||||
+972
-12
@@ -1,5 +1,402 @@
|
|||||||
# Changelog
|
# 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
|
## alpha 0.6.0.1015
|
||||||
|
|
||||||
- Replace repository-local and hardcoded deployment values with independent,
|
- Replace repository-local and hardcoded deployment values with independent,
|
||||||
@@ -32,18 +429,581 @@
|
|||||||
backup; pre-public operational history remains only in protected local
|
backup; pre-public operational history remains only in protected local
|
||||||
archives.
|
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;
|
## alpha 0.5.10.1015
|
||||||
- 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.
|
|
||||||
|
|
||||||
Private host, person, address, account, incident, and deployment details are
|
- Prioritize queued fast acknowledgements and local-command replies over queued
|
||||||
intentionally omitted from the public changelog.
|
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.
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
|||||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||||
并通过带视觉确认的 GUI 发送链路回复。
|
并通过带视觉确认的 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
|
```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]
|
/丢骰子 [NdM]
|
||||||
/猜数字 开始 [上限]
|
/猜数字 开始 [上限]
|
||||||
/石头剪刀布 [开始|加入|出|状态|结算|结束]
|
/石头剪刀布 [开始|加入|出|状态|结算|结束]
|
||||||
@@ -166,7 +193,12 @@ sudo ./deploy/scripts/deploy-jwebot-production.sh /path/to/JWeBot jwebot
|
|||||||
```
|
```
|
||||||
|
|
||||||
部署必须通过微信窗口就绪、队列清空、Edge 健康、SQLite 完整性和自适应
|
部署必须通过微信窗口就绪、队列清空、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:
|
Current version:
|
||||||
|
|
||||||
```text
|
```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
|
## Runtime Flow
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -308,7 +349,8 @@ migration, debug archives, and root deployment settings.
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
The built-in command inventory is generated from reviewed plugin descriptors.
|
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
|
```text
|
||||||
/功能列表
|
/功能列表
|
||||||
@@ -337,6 +379,13 @@ the final separator shown below:
|
|||||||
/steam时长Dota2人=玩家名
|
/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
|
Steam prefers private per-person SteamID mappings from the active profile and
|
||||||
uses the corresponding group display IDs and aliases. Without a private roster,
|
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
|
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,
|
The deployer refuses cutover unless the ready-window, empty-queue, Edge health,
|
||||||
SQLite integrity, and adaptive restart-stability gates pass. Legacy OCR units
|
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
|
## Repository Map
|
||||||
|
|
||||||
|
|||||||
@@ -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)'
|
'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() {
|
render_root_configuration() {
|
||||||
local output_dir="$1"
|
local output_dir="$1"
|
||||||
sed \
|
sed \
|
||||||
@@ -421,6 +434,7 @@ test -x "${runtime_python}"
|
|||||||
for path in \
|
for path in \
|
||||||
"${source_dir}/wechat_local_ocr.py" \
|
"${source_dir}/wechat_local_ocr.py" \
|
||||||
"${source_dir}/wechat-gui-bridge" \
|
"${source_dir}/wechat-gui-bridge" \
|
||||||
|
"${source_dir}/jwebot_release_update.py" \
|
||||||
"${source_dir}/deploy/scripts/install-wechat-health-watch.sh" \
|
"${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-recorder.service" \
|
||||||
"${source_dir}/deploy/systemd/system/wechat-local-ocr-responder.service" \
|
"${source_dir}/deploy/systemd/system/wechat-local-ocr-responder.service" \
|
||||||
@@ -430,7 +444,9 @@ do
|
|||||||
done
|
done
|
||||||
bash -n "${source_dir}/wechat-gui-bridge"
|
bash -n "${source_dir}/wechat-gui-bridge"
|
||||||
sh -n "${source_dir}/deploy/scripts/install-wechat-health-watch.sh"
|
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)
|
preflight_rendered_dir=$(mktemp -d)
|
||||||
render_root_configuration "${preflight_rendered_dir}"
|
render_root_configuration "${preflight_rendered_dir}"
|
||||||
systemd-analyze verify \
|
systemd-analyze verify \
|
||||||
@@ -468,6 +484,9 @@ jwebot_log "backup ready path=${backup_dir}"
|
|||||||
armed=1
|
armed=1
|
||||||
jwebot_progress 3 8 "stop services"
|
jwebot_progress 3 8 "stop services"
|
||||||
jwebot_log "cutover starting"
|
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 \
|
user_systemctl stop \
|
||||||
wechat-ocr-adaptive-recorder.service \
|
wechat-ocr-adaptive-recorder.service \
|
||||||
jwebot-edge.service \
|
jwebot-edge.service \
|
||||||
@@ -606,6 +625,10 @@ if [[ -f "${target_edge_state}/edge.sqlite3" ]]; then
|
|||||||
fi
|
fi
|
||||||
jwebot_log "database ready quick_check=${db_check:-absent}"
|
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"
|
jwebot_progress 7 8 "verify backup"
|
||||||
(
|
(
|
||||||
cd "${backup_dir}"
|
cd "${backup_dir}"
|
||||||
@@ -619,6 +642,17 @@ success=1
|
|||||||
armed=0
|
armed=0
|
||||||
trap - EXIT HUP INT TERM
|
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 "deployment ready version=$(cat "${source_dir}/VERSION")"
|
||||||
jwebot_log "backup active path=${backup_dir}"
|
jwebot_log "backup active path=${backup_dir}"
|
||||||
jwebot_log "legacy OCR inactive"
|
jwebot_log "legacy OCR inactive"
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ user_unit_source_dir="$source_dir/deploy/systemd/user"
|
|||||||
config_source_dir="$source_dir/packages/jwebot-config/src/jwebot_config"
|
config_source_dir="$source_dir/packages/jwebot-config/src/jwebot_config"
|
||||||
sdk_source_dir="$source_dir/packages/jwebot-sdk/src/jwebot_sdk"
|
sdk_source_dir="$source_dir/packages/jwebot-sdk/src/jwebot_sdk"
|
||||||
kernel_source_dir="$source_dir/packages/jwebot-kernel/src/jwebot_kernel"
|
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_source_dir="$source_dir/packages/jwebot-official-extensions/src/jwebot_official_extensions"
|
||||||
official_adapter_source_dir="$official_source_dir/adapters"
|
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"
|
target_dir="$HOME/.local/lib/wechat-health-watch"
|
||||||
unit_dir="$HOME/.config/systemd/user"
|
unit_dir="$HOME/.config/systemd/user"
|
||||||
config_dir="$HOME/.config/jwebot"
|
config_dir="$HOME/.config/jwebot"
|
||||||
@@ -26,6 +28,7 @@ adaptive_script_name=wechat_adaptive_recorder.py
|
|||||||
adaptive_bridge_name=wechat_adaptive_gui_bridge.py
|
adaptive_bridge_name=wechat_adaptive_gui_bridge.py
|
||||||
edge_script_name=jwebot_edge.py
|
edge_script_name=jwebot_edge.py
|
||||||
edge_delivery_name=jwebot_edge_delivery.py
|
edge_delivery_name=jwebot_edge_delivery.py
|
||||||
|
release_update_name=jwebot_release_update.py
|
||||||
service_name=wechat-health-watch.service
|
service_name=wechat-health-watch.service
|
||||||
timer_name=wechat-health-watch.timer
|
timer_name=wechat-health-watch.timer
|
||||||
adaptive_service_name=wechat-ocr-adaptive-recorder.service
|
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 "$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name" 0700
|
||||||
restore_file "$edge_script_name" "$target_dir/$edge_script_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 "$edge_delivery_name" "$target_dir/$edge_delivery_name" 0700
|
||||||
|
restore_file "$release_update_name" "$target_dir/$release_update_name" 0700
|
||||||
for package_name in \
|
for package_name in \
|
||||||
jwebot_config \
|
jwebot_config \
|
||||||
jwebot_sdk \
|
jwebot_sdk \
|
||||||
@@ -237,12 +241,15 @@ trap rollback EXIT HUP INT TERM
|
|||||||
jwebot_log "user runtime preflight starting"
|
jwebot_log "user runtime preflight starting"
|
||||||
test -x "$runtime_python"
|
test -x "$runtime_python"
|
||||||
for path in \
|
for path in \
|
||||||
|
"$source_dir/VERSION" \
|
||||||
|
"$source_dir/CHANGELOG.md" \
|
||||||
"$runtime_source_dir/$ocr_script_name" \
|
"$runtime_source_dir/$ocr_script_name" \
|
||||||
"$runtime_source_dir/$script_name" \
|
"$runtime_source_dir/$script_name" \
|
||||||
"$runtime_source_dir/$adaptive_script_name" \
|
"$runtime_source_dir/$adaptive_script_name" \
|
||||||
"$runtime_source_dir/$adaptive_bridge_name" \
|
"$runtime_source_dir/$adaptive_bridge_name" \
|
||||||
"$runtime_source_dir/$edge_script_name" \
|
"$runtime_source_dir/$edge_script_name" \
|
||||||
"$runtime_source_dir/$edge_delivery_name" \
|
"$runtime_source_dir/$edge_delivery_name" \
|
||||||
|
"$runtime_source_dir/$release_update_name" \
|
||||||
"$user_unit_source_dir/$service_name" \
|
"$user_unit_source_dir/$service_name" \
|
||||||
"$user_unit_source_dir/$timer_name" \
|
"$user_unit_source_dir/$timer_name" \
|
||||||
"$user_unit_source_dir/$adaptive_service_name" \
|
"$user_unit_source_dir/$adaptive_service_name" \
|
||||||
@@ -262,6 +269,9 @@ do
|
|||||||
test -d "$directory"
|
test -d "$directory"
|
||||||
find "$directory" -maxdepth 1 -type f -name '*.py' | grep -q .
|
find "$directory" -maxdepth 1 -type f -name '*.py' | grep -q .
|
||||||
done
|
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"
|
test -f "$environment_file"
|
||||||
|
|
||||||
if systemctl is-active --quiet wechat-local-ocr-recorder.service \
|
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"
|
install -d -m 0700 "$target_dir" "$unit_dir" "$config_dir" "$backup_dir"
|
||||||
save_unit_states
|
save_unit_states
|
||||||
for name in \
|
for name in \
|
||||||
|
VERSION \
|
||||||
|
CHANGELOG.md \
|
||||||
"$ocr_script_name" \
|
"$ocr_script_name" \
|
||||||
"$script_name" \
|
"$script_name" \
|
||||||
"$adaptive_script_name" \
|
"$adaptive_script_name" \
|
||||||
"$adaptive_bridge_name" \
|
"$adaptive_bridge_name" \
|
||||||
"$edge_script_name" \
|
"$edge_script_name" \
|
||||||
"$edge_delivery_name"
|
"$edge_delivery_name" \
|
||||||
|
"$release_update_name"
|
||||||
do
|
do
|
||||||
if [ -f "$target_dir/$name" ]; then
|
if [ -f "$target_dir/$name" ]; then
|
||||||
cp -a "$target_dir/$name" "$backup_dir/$name"
|
cp -a "$target_dir/$name" "$backup_dir/$name"
|
||||||
@@ -326,12 +339,17 @@ systemctl --user stop \
|
|||||||
>/dev/null 2>&1 || true
|
>/dev/null 2>&1 || true
|
||||||
|
|
||||||
jwebot_log "user runtime files install starting"
|
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/$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/$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_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/$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_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/$edge_delivery_name" "$target_dir/$edge_delivery_name"
|
||||||
|
install -m 0700 \
|
||||||
|
"$runtime_source_dir/$release_update_name" \
|
||||||
|
"$target_dir/$release_update_name"
|
||||||
rm -rf \
|
rm -rf \
|
||||||
"$target_dir/jwebot_config" \
|
"$target_dir/jwebot_config" \
|
||||||
"$target_dir/jwebot_sdk" \
|
"$target_dir/jwebot_sdk" \
|
||||||
@@ -341,8 +359,11 @@ install -d -m 0700 \
|
|||||||
"$target_dir/jwebot_config" \
|
"$target_dir/jwebot_config" \
|
||||||
"$target_dir/jwebot_sdk" \
|
"$target_dir/jwebot_sdk" \
|
||||||
"$target_dir/jwebot_kernel" \
|
"$target_dir/jwebot_kernel" \
|
||||||
|
"$target_dir/jwebot_kernel/data" \
|
||||||
"$target_dir/jwebot_official_extensions" \
|
"$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
|
for path in "$config_source_dir"/*.py; do
|
||||||
install -m 0600 "$path" "$target_dir/jwebot_config/$(basename "$path")"
|
install -m 0600 "$path" "$target_dir/jwebot_config/$(basename "$path")"
|
||||||
done
|
done
|
||||||
@@ -352,6 +373,10 @@ done
|
|||||||
for path in "$kernel_source_dir"/*.py; do
|
for path in "$kernel_source_dir"/*.py; do
|
||||||
install -m 0600 "$path" "$target_dir/jwebot_kernel/$(basename "$path")"
|
install -m 0600 "$path" "$target_dir/jwebot_kernel/$(basename "$path")"
|
||||||
done
|
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
|
for path in "$official_source_dir"/*.py; do
|
||||||
install -m 0600 "$path" "$target_dir/jwebot_official_extensions/$(basename "$path")"
|
install -m 0600 "$path" "$target_dir/jwebot_official_extensions/$(basename "$path")"
|
||||||
done
|
done
|
||||||
@@ -359,6 +384,10 @@ for path in "$official_adapter_source_dir"/*.py; do
|
|||||||
install -m 0600 "$path" \
|
install -m 0600 "$path" \
|
||||||
"$target_dir/jwebot_official_extensions/adapters/$(basename "$path")"
|
"$target_dir/jwebot_official_extensions/adapters/$(basename "$path")"
|
||||||
done
|
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 \
|
for name in \
|
||||||
"$service_name" \
|
"$service_name" \
|
||||||
"$timer_name" \
|
"$timer_name" \
|
||||||
@@ -379,7 +408,8 @@ for name in \
|
|||||||
"$adaptive_script_name" \
|
"$adaptive_script_name" \
|
||||||
"$adaptive_bridge_name" \
|
"$adaptive_bridge_name" \
|
||||||
"$edge_script_name" \
|
"$edge_script_name" \
|
||||||
"$edge_delivery_name"
|
"$edge_delivery_name" \
|
||||||
|
"$release_update_name"
|
||||||
do
|
do
|
||||||
"$runtime_python" -m py_compile "$target_dir/$name"
|
"$runtime_python" -m py_compile "$target_dir/$name"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Command Registration And Execution Paths
|
# Command Registration And Execution Paths
|
||||||
|
|
||||||
Status: authoritative local registration and routing contract for
|
Status: authoritative local registration and routing contract for
|
||||||
`alpha 0.6.x`.
|
`beta 1.0.x`.
|
||||||
|
|
||||||
## Three Execution Paths
|
## Three Execution Paths
|
||||||
|
|
||||||
@@ -25,7 +25,8 @@ error. They never fall through to DeepSeek or OpenClaw.
|
|||||||
|
|
||||||
The local command path is:
|
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.
|
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.
|
3. The OCR engine reuses exact visual-fingerprint matches from a bounded LRU.
|
||||||
4. Only new or changed sender/body crops use RapidOCR.
|
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
|
Changed pixels force fresh OCR. The cache is bounded and periodically
|
||||||
revalidated.
|
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 Plugin Registration
|
||||||
|
|
||||||
Bundled command metadata has one source:
|
Bundled command metadata has one source:
|
||||||
@@ -141,7 +146,8 @@ descriptors unless they are strictly Kernel-only. The current official
|
|||||||
operational commands are:
|
operational commands are:
|
||||||
|
|
||||||
- `/状态` and `/status`: server, Edge, OCR, and queue status.
|
- `/状态` 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.
|
- `/debug`: recent privacy-safe telemetry p50/p95/max summary.
|
||||||
|
|
||||||
These commands delegate to Kernel-owned service profiles through generated
|
These commands delegate to Kernel-owned service profiles through generated
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ Steam uses `JWEBOT_EDGE_STEAM_ID64` and
|
|||||||
- Host timeout and fallback;
|
- Host timeout and fallback;
|
||||||
- GUI sender timeout.
|
- 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
|
### Debug
|
||||||
|
|
||||||
`components/debug.toml` generates:
|
`components/debug.toml` generates:
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 模拟器,而不是一个只有数值结算的普通抽卡游戏。
|
||||||
@@ -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` 事务中提交。重复事件不会
|
||||||
|
重复扣款、发奖或增加局号;进程重启后未开奖轮次和个人记录仍然保留。
|
||||||
@@ -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,6 +1,6 @@
|
|||||||
# Plugin Contract
|
# Plugin Contract
|
||||||
|
|
||||||
Status: authoritative for the `alpha 0.6.x` extension API.
|
Status: authoritative for the `beta 1.0.x` extension API.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
|
|||||||
+28
-3
@@ -6,9 +6,11 @@ exception.
|
|||||||
|
|
||||||
## Release Discipline
|
## Release Discipline
|
||||||
|
|
||||||
- Version format is `alpha X.Y.Z.MMDD`.
|
- Version format is `beta X.Y.Z.MMDD` for current beta releases.
|
||||||
- Patch changes increment `Z`; minor version changes require explicit user
|
- Python package versions use the matching PEP 440 beta form, such as
|
||||||
approval.
|
`X.Y.Zb1+MMDD`.
|
||||||
|
- Patch changes increment `Z`; minor, major, or stable-release changes require
|
||||||
|
explicit user approval.
|
||||||
- A release change must update:
|
- A release change must update:
|
||||||
- `VERSION`;
|
- `VERSION`;
|
||||||
- root `pyproject.toml`;
|
- root `pyproject.toml`;
|
||||||
@@ -16,6 +18,9 @@ exception.
|
|||||||
- every package `__version__`;
|
- every package `__version__`;
|
||||||
- `README.md`;
|
- `README.md`;
|
||||||
- `CHANGELOG.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
|
- Do not mark production complete merely because code was committed. Production
|
||||||
is complete only after staging, cutover, acceptance, observation, and backup
|
is complete only after staging, cutover, acceptance, observation, and backup
|
||||||
restore checks pass.
|
restore checks pass.
|
||||||
@@ -61,6 +66,26 @@ Before touching production runtime files on production host:
|
|||||||
SQLite gates.
|
SQLite gates.
|
||||||
- If any gate fails, confirm rollback state before retrying.
|
- 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
|
## Backup And Restore
|
||||||
|
|
||||||
- Pre-deploy and post-deploy baselines must include runtime files, systemd
|
- Pre-deploy and post-deploy baselines must include runtime files, systemd
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ The deployer prints an eight-stage progress bar and:
|
|||||||
- leaves legacy OCR disabled;
|
- leaves legacy OCR disabled;
|
||||||
- verifies Edge health and adaptive restart stability;
|
- verifies Edge health and adaptive restart stability;
|
||||||
- writes and verifies backup SHA-256 checksums;
|
- 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
|
## Post-Cutover
|
||||||
|
|
||||||
@@ -104,6 +109,28 @@ PY
|
|||||||
Player-visible acceptance must use deliberate synthetic messages in the
|
Player-visible acceptance must use deliberate synthetic messages in the
|
||||||
configured group and confirm exactly one ACK and one final response.
|
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
|
## Rollback
|
||||||
|
|
||||||
If any gate fails:
|
If any gate fails:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Runtime Telemetry And ACK Semantics
|
# 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
|
## What ACK Means
|
||||||
|
|
||||||
|
|||||||
+10
-6
@@ -1,20 +1,24 @@
|
|||||||
# Versioning
|
# Versioning
|
||||||
|
|
||||||
JWeBot uses an alpha version string:
|
JWeBot uses a human-readable product version string:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
alpha X.Y.Z.MMDD
|
beta X.Y.Z.MMDD
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules:
|
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.
|
- `X.Y.Z` is the project version.
|
||||||
- `MMDD` is the delivery date.
|
- `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
|
- Normal small updates increment only the patch version, for example
|
||||||
`alpha 0.0.1.1014` to `alpha 0.0.2.1014`.
|
`beta 1.0.1.1016` to `beta 1.0.2.1016`.
|
||||||
- Minor versions such as `0.1.x` require explicit user review and milestone
|
- Minor, major, or stable-release changes require explicit user review and
|
||||||
approval.
|
milestone approval.
|
||||||
- Local repository migration does not imply a production deployment.
|
- Local repository migration does not imply a production deployment.
|
||||||
- Production services must not be changed unless the operator explicitly asks
|
- Production services must not be changed unless the operator explicitly asks
|
||||||
for deployment or runtime verification.
|
for deployment or runtime verification.
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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`.
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for coin."""
|
"""Generated reviewed adapter for coin."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'coin'
|
PROFILE = 'coin'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '抛一枚硬币'
|
DESCRIPTION = '抛一枚硬币'
|
||||||
HELP_ENTRIES = ('/翻硬币',)
|
HELP_ENTRIES = ('/翻硬币',)
|
||||||
COMMANDS = ('/翻硬币',)
|
COMMANDS = ('/翻硬币',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -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."""
|
"""Generated reviewed adapter for daily-fortune."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'daily-fortune'
|
PROFILE = 'daily-fortune'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '生成当天稳定一致的运势'
|
DESCRIPTION = '生成当天稳定一致的运势'
|
||||||
HELP_ENTRIES = ('/今日运势',)
|
HELP_ENTRIES = ('/今日运势',)
|
||||||
COMMANDS = ('/今日运势',)
|
COMMANDS = ('/今日运势',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for dice."""
|
"""Generated reviewed adapter for dice."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'dice'
|
PROFILE = 'dice'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '骰子和多人骰子挑战'
|
DESCRIPTION = '骰子和多人骰子挑战'
|
||||||
HELP_ENTRIES = ('/丢骰子 [NdM]', '/丢骰子挑战 [5d6|5d20|状态|结算|战绩|结束]')
|
HELP_ENTRIES = ('/丢骰子 [NdM]', '/丢骰子挑战 [5d6|5d20|状态|结算|战绩|结束]')
|
||||||
COMMANDS = ('/丢骰子', '/骰子', '/丢骰子挑战', '/骰子挑战')
|
COMMANDS = ('/丢骰子', '/骰子', '/丢骰子挑战', '/骰子挑战')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for draw-lot."""
|
"""Generated reviewed adapter for draw-lot."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'draw-lot'
|
PROFILE = 'draw-lot'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '抽取一条本地签文'
|
DESCRIPTION = '抽取一条本地签文'
|
||||||
HELP_ENTRIES = ('/抽签',)
|
HELP_ENTRIES = ('/抽签',)
|
||||||
COMMANDS = ('/抽签',)
|
COMMANDS = ('/抽签',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -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."""
|
"""Generated reviewed adapter for guess-number."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'guess-number'
|
PROFILE = 'guess-number'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '群内持久化猜数字游戏'
|
DESCRIPTION = '群内持久化猜数字游戏'
|
||||||
HELP_ENTRIES = ('/猜数字 开始 [上限];/猜 数字;/猜数字 [状态|结束]',)
|
HELP_ENTRIES = ('/猜数字 开始 [上限];/猜 数字;/猜数字 [状态|结束]',)
|
||||||
COMMANDS = ('/猜数字', '/猜')
|
COMMANDS = ('/猜数字', '/猜')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for history-search."""
|
"""Generated reviewed adapter for history-search."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'history-search'
|
PROFILE = 'history-search'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '搜索本地群聊文字记录'
|
DESCRIPTION = '搜索本地群聊文字记录'
|
||||||
HELP_ENTRIES = ('/查记录 关键词',)
|
HELP_ENTRIES = ('/查记录 关键词',)
|
||||||
COMMANDS = ('/查记录', '/历史搜索')
|
COMMANDS = ('/查记录', '/历史搜索')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -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."""
|
"""Generated reviewed adapter for random-number."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'random-number'
|
PROFILE = 'random-number'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '在指定整数范围内取随机数'
|
DESCRIPTION = '在指定整数范围内取随机数'
|
||||||
HELP_ENTRIES = ('/随机数 最小值 最大值',)
|
HELP_ENTRIES = ('/随机数 最小值 最大值',)
|
||||||
COMMANDS = ('/随机数',)
|
COMMANDS = ('/随机数',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for repeat-ranking."""
|
"""Generated reviewed adapter for repeat-ranking."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'repeat-ranking'
|
PROFILE = 'repeat-ranking'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '查看北京时间今日复读排行'
|
DESCRIPTION = '查看北京时间今日复读排行'
|
||||||
HELP_ENTRIES = ('/复读榜',)
|
HELP_ENTRIES = ('/复读榜',)
|
||||||
COMMANDS = ('/复读榜', '/复读')
|
COMMANDS = ('/复读榜', '/复读')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for rock-paper-scissors."""
|
"""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'
|
PROFILE = 'rock-paper-scissors'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '多人石头剪刀布游戏'
|
DESCRIPTION = '多人石头剪刀布游戏'
|
||||||
HELP_ENTRIES = ('/石头剪刀布 [开始|加入|出 选择|状态|结算|结束]',)
|
HELP_ENTRIES = ('/石头剪刀布 [开始|加入|出 选择|状态|结算|结束]',)
|
||||||
COMMANDS = ('/石头剪刀布',)
|
COMMANDS = ('/石头剪刀布',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for scan-ranking."""
|
"""Generated reviewed adapter for scan-ranking."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'scan-ranking'
|
PROFILE = 'scan-ranking'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '查看北京时间今日扫码排行'
|
DESCRIPTION = '查看北京时间今日扫码排行'
|
||||||
HELP_ENTRIES = ('/扫码排行',)
|
HELP_ENTRIES = ('/扫码排行',)
|
||||||
COMMANDS = ('/扫码排行', '/扫码榜', '/今日扫码')
|
COMMANDS = ('/扫码排行', '/扫码榜', '/今日扫码')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -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."""
|
"""Generated reviewed adapter for steam-presence."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'steam-presence'
|
PROFILE = 'steam-presence'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = 'Steam 状态与时长'
|
DESCRIPTION = 'Steam 状态与时长'
|
||||||
HELP_ENTRIES = ('/steam;/steam时长 游戏[、玩家]',)
|
HELP_ENTRIES = ('/steam;/steam时长 游戏[、玩家]',)
|
||||||
COMMANDS = ('/steam',)
|
COMMANDS = ('/steam',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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."""
|
"""Generated reviewed adapter for debug-telemetry."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'debug-telemetry'
|
PROFILE = 'debug-telemetry'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'official'
|
|||||||
DESCRIPTION = '埋点 p50/p95'
|
DESCRIPTION = '埋点 p50/p95'
|
||||||
HELP_ENTRIES = ('/debug',)
|
HELP_ENTRIES = ('/debug',)
|
||||||
COMMANDS = ('/debug',)
|
COMMANDS = ('/debug',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ JWeBot 版本
|
|||||||
|
|
||||||
Help:
|
Help:
|
||||||
|
|
||||||
- `/version`
|
- `/version /版本`
|
||||||
|
|
||||||
Registered commands:
|
Registered commands:
|
||||||
|
|
||||||
|
- `/版本`
|
||||||
- `/version`
|
- `/version`
|
||||||
|
|
||||||
The adapter has no direct Ledger, GUI, environment, shell, or network access.
|
The adapter has no direct Ledger, GUI, environment, shell, or network access.
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
id = "jwebot-version"
|
id = "jwebot-version"
|
||||||
author = "official"
|
author = "official"
|
||||||
description = "JWeBot 版本"
|
description = "JWeBot 版本"
|
||||||
help = ["/version"]
|
help = ["/version /版本"]
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
api_version = "1"
|
api_version = "1"
|
||||||
entrypoint = "jwebot_version:plugin"
|
entrypoint = "jwebot_version:plugin"
|
||||||
type = "command"
|
type = "command"
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
commands = ["/version"]
|
commands = ["/版本", "/version"]
|
||||||
storage = false
|
storage = false
|
||||||
network_profiles = []
|
network_profiles = []
|
||||||
service_profiles = ["jwebot-version"]
|
service_profiles = ["jwebot-version"]
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
"""Generated reviewed adapter for jwebot-version."""
|
"""Generated reviewed adapter for jwebot-version."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'jwebot-version'
|
PROFILE = 'jwebot-version'
|
||||||
AUTHOR = 'official'
|
AUTHOR = 'official'
|
||||||
DESCRIPTION = 'JWeBot 版本'
|
DESCRIPTION = 'JWeBot 版本'
|
||||||
HELP_ENTRIES = ('/version',)
|
HELP_ENTRIES = ('/version /版本',)
|
||||||
COMMANDS = ('/version',)
|
COMMANDS = ('/版本', '/version')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ class PluginTests(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(AUTHOR, 'official')
|
self.assertEqual(AUTHOR, 'official')
|
||||||
self.assertEqual(DESCRIPTION, 'JWeBot 版本')
|
self.assertEqual(DESCRIPTION, 'JWeBot 版本')
|
||||||
self.assertEqual(HELP_ENTRIES, ('/version',))
|
self.assertEqual(HELP_ENTRIES, ('/version /版本',))
|
||||||
self.assertIn('/version', registered)
|
self.assertIn('/版本', registered)
|
||||||
self.assertEqual(len(registered), 1)
|
self.assertEqual(len(registered), 2)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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."""
|
"""Generated reviewed adapter for person-memory."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'person-memory'
|
PROFILE = 'person-memory'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'official'
|
|||||||
DESCRIPTION = '管理个人和同群人物记忆'
|
DESCRIPTION = '管理个人和同群人物记忆'
|
||||||
HELP_ENTRIES = ('/记住 内容;/我的记忆;/忘记 关键词或全部;/人物记忆 [显示名]',)
|
HELP_ENTRIES = ('/记住 内容;/我的记忆;/忘记 关键词或全部;/人物记忆 [显示名]',)
|
||||||
COMMANDS = ('/记住', '/我的记忆', '/忘记', '/人物记忆', '/记忆帮助')
|
COMMANDS = ('/记住', '/我的记忆', '/忘记', '/人物记忆', '/记忆帮助')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for server-status."""
|
"""Generated reviewed adapter for server-status."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'server-status'
|
PROFILE = 'server-status'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'official'
|
|||||||
DESCRIPTION = '服务器状态'
|
DESCRIPTION = '服务器状态'
|
||||||
HELP_ENTRIES = ('/状态;/status',)
|
HELP_ENTRIES = ('/状态;/status',)
|
||||||
COMMANDS = ('/状态', '/status')
|
COMMANDS = ('/状态', '/status')
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-config"
|
name = "jwebot-config"
|
||||||
version = "0.6.0a0+1015"
|
version = "2.0.5b1+1023"
|
||||||
description = "Profile-based configuration compiler for JWeBot"
|
description = "Profile-based configuration compiler for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ __all__ = [
|
|||||||
"write_default_profile",
|
"write_default_profile",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.0a0+1015"
|
__version__ = "2.0.5b1+1023"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-extension-host"
|
name = "jwebot-extension-host"
|
||||||
version = "0.6.0a0+1015"
|
version = "2.0.5b1+1023"
|
||||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==0.6.0a0+1015",
|
"jwebot-sdk==2.0.5b1+1023",
|
||||||
"jwebot-kernel==0.6.0a0+1015",
|
"jwebot-kernel==2.0.5b1+1023",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ __all__ = [
|
|||||||
"process_request",
|
"process_request",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.0a0+1015"
|
__version__ = "2.0.5b1+1023"
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-kernel"
|
name = "jwebot-kernel"
|
||||||
version = "0.6.0a0+1015"
|
version = "2.0.5b1+1023"
|
||||||
description = "Stable routing and extension authority for JWeBot"
|
description = "Stable routing and extension authority for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-config==0.6.0a0+1015",
|
"jwebot-config==2.0.5b1+1023",
|
||||||
"jwebot-sdk==0.6.0a0+1015",
|
"jwebot-sdk==2.0.5b1+1023",
|
||||||
"jwebot-official-extensions==0.6.0a0+1015",
|
"jwebot-official-extensions==2.0.5b1+1023",
|
||||||
"tomli>=2; python_version < '3.11'",
|
"tomli>=2; python_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
jwebot_kernel = ["data/*.json"]
|
||||||
|
|||||||
@@ -107,4 +107,4 @@ __all__ = [
|
|||||||
"verify_installed_release_signature",
|
"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]
|
[project]
|
||||||
name = "jwebot-official-extensions"
|
name = "jwebot-official-extensions"
|
||||||
version = "0.6.0a0+1015"
|
version = "2.0.5b1+1023"
|
||||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==0.6.0a0+1015",
|
"jwebot-sdk==2.0.5b1+1023",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
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."""
|
"""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 (
|
from .descriptors import (
|
||||||
DIRECT_KERNEL_COMMANDS,
|
DIRECT_KERNEL_COMMANDS,
|
||||||
DIRECT_KERNEL_COMMAND_KINDS,
|
DIRECT_KERNEL_COMMAND_KINDS,
|
||||||
@@ -20,6 +33,8 @@ from .local_commands import (
|
|||||||
LocalCommandError,
|
LocalCommandError,
|
||||||
deterministic_int,
|
deterministic_int,
|
||||||
local_command_parts,
|
local_command_parts,
|
||||||
|
pack_reply_parts,
|
||||||
|
unpack_reply_parts,
|
||||||
)
|
)
|
||||||
from .runtime import (
|
from .runtime import (
|
||||||
LocalExtensionError,
|
LocalExtensionError,
|
||||||
@@ -35,6 +50,10 @@ from .steam import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"CARD_BY_ID",
|
||||||
|
"COACH_BONUSES",
|
||||||
|
"COACH_POOL",
|
||||||
|
"CsMajorAutochess",
|
||||||
"DICE_CHALLENGE_TIMEOUT_SECONDS",
|
"DICE_CHALLENGE_TIMEOUT_SECONDS",
|
||||||
"DEFAULT_STEAM_MIHOMO_PROXY_URL",
|
"DEFAULT_STEAM_MIHOMO_PROXY_URL",
|
||||||
"DIRECT_KERNEL_COMMANDS",
|
"DIRECT_KERNEL_COMMANDS",
|
||||||
@@ -43,8 +62,12 @@ __all__ = [
|
|||||||
"LOCAL_COMMAND_ALIASES",
|
"LOCAL_COMMAND_ALIASES",
|
||||||
"LOCAL_EXTENSIONS",
|
"LOCAL_EXTENSIONS",
|
||||||
"LOCAL_GAME_TIMEOUT_SECONDS",
|
"LOCAL_GAME_TIMEOUT_SECONDS",
|
||||||
|
"MAX_PLAYERS",
|
||||||
"MAX_RPS_PLAYERS",
|
"MAX_RPS_PLAYERS",
|
||||||
|
"MIN_PLAYERS",
|
||||||
"OFFICIAL_AUTHOR",
|
"OFFICIAL_AUTHOR",
|
||||||
|
"PLAYER_POOL",
|
||||||
|
"build_round_plan",
|
||||||
"KernelCommandDescriptor",
|
"KernelCommandDescriptor",
|
||||||
"LocalExtensionDescriptor",
|
"LocalExtensionDescriptor",
|
||||||
"LocalExtensionError",
|
"LocalExtensionError",
|
||||||
@@ -58,8 +81,13 @@ __all__ = [
|
|||||||
"deterministic_int",
|
"deterministic_int",
|
||||||
"local_command_parts",
|
"local_command_parts",
|
||||||
"local_extension_command_token",
|
"local_extension_command_token",
|
||||||
|
"lineup_power",
|
||||||
|
"matchup_modifier",
|
||||||
|
"pack_reply_parts",
|
||||||
|
"parse_autochess_action",
|
||||||
|
"unpack_reply_parts",
|
||||||
"extension_for_kind",
|
"extension_for_kind",
|
||||||
"render_feature_list",
|
"render_feature_list",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.0a0+1015"
|
__version__ = "2.0.5b1+1023"
|
||||||
|
|||||||
+4
-23
@@ -1,6 +1,6 @@
|
|||||||
"""Generated reviewed adapter for coin."""
|
"""Generated reviewed adapter for coin."""
|
||||||
|
|
||||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
from jwebot_sdk import ServiceCommandAdapter, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
PROFILE = 'coin'
|
PROFILE = 'coin'
|
||||||
@@ -8,30 +8,11 @@ AUTHOR = 'jwebot'
|
|||||||
DESCRIPTION = '抛一枚硬币'
|
DESCRIPTION = '抛一枚硬币'
|
||||||
HELP_ENTRIES = ('/翻硬币',)
|
HELP_ENTRIES = ('/翻硬币',)
|
||||||
COMMANDS = ('/翻硬币',)
|
COMMANDS = ('/翻硬币',)
|
||||||
|
ADAPTER = ServiceCommandAdapter(
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
|
||||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
||||||
response = context.services.call(
|
|
||||||
PROFILE,
|
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},
|
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||||
)
|
)
|
||||||
|
|
||||||
return handle
|
|
||||||
|
|
||||||
|
|
||||||
def plugin(context: SingleGroupContext) -> None:
|
def plugin(context: SingleGroupContext) -> None:
|
||||||
for command in COMMANDS:
|
ADAPTER.register(context, COMMANDS)
|
||||||
context.commands.register(command, _handler(context, command))
|
|
||||||
|
|||||||
+18
@@ -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)
|
||||||
+18
@@ -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)
|
||||||
+18
@@ -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)
|
||||||
+18
@@ -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)
|
||||||
+18
@@ -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)
|
||||||
+18
@@ -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
Reference in New Issue
Block a user