Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc71e63a7 | ||
|
|
bf4f28f48e | ||
|
|
148ae672f4 | ||
|
|
bbddb5be6f | ||
|
|
2bb2effc04 | ||
|
|
5125b827c0 | ||
|
|
3dea42b268 | ||
|
|
a74147e7f1 | ||
|
|
9fff57134f | ||
|
|
e713ff369a | ||
|
|
f07ae653c7 | ||
|
|
0731e5cebb | ||
|
|
f7fedc54e4 |
@@ -67,6 +67,7 @@ id_ed25519*
|
||||
|
||||
# Sensitive local notes
|
||||
homeserver-apikey-*.md
|
||||
debug-*.md
|
||||
|
||||
# macOS/editor
|
||||
.DS_Store
|
||||
|
||||
@@ -4,9 +4,11 @@ These rules are mandatory for every automated change in this repository.
|
||||
|
||||
## Versioning
|
||||
|
||||
- Use `alpha X.Y.Z.MMDD`.
|
||||
- Use `beta X.Y.Z.MMDD` for current beta releases.
|
||||
- Python package versions use PEP 440 pre-release form, for example
|
||||
`X.Y.Zb1+MMDD`.
|
||||
- Small changes increment only `Z`.
|
||||
- Do not move to a new minor version without explicit user approval.
|
||||
- Do not move to a new minor or stable version without explicit user approval.
|
||||
- Keep `VERSION`, root `pyproject.toml`, package versions, package
|
||||
`__version__`, `README.md`, and `CHANGELOG.md` synchronized.
|
||||
|
||||
|
||||
+771
-12
@@ -1,5 +1,201 @@
|
||||
# Changelog
|
||||
|
||||
## beta 1.0.0.1016 - 2026-10-16
|
||||
|
||||
- Promote JWeBot from alpha to beta after the local, staging, privacy, and
|
||||
production deployment gates stabilized.
|
||||
- Consolidate the current reviewed local command set, including virtual coin
|
||||
transfer, loans, repayment, group leaderboard, multiplayer lottery,
|
||||
blackjack, Steam queries, and CS Major autochess.
|
||||
- Externalize CS Major autochess game content into package JSON data files for
|
||||
cards, coaches, narratives, draft/shop/economy rules, match formats, and
|
||||
MR15/overtime settings.
|
||||
- Add a maintained command catalog and keep debug session notes out of the
|
||||
repository by default.
|
||||
- Keep release validation on `tools/sync-local-extensions.py --check`,
|
||||
`tools/check-local.sh`, and `tools/check-secrets.py --history`.
|
||||
|
||||
## alpha 0.6.11.1015
|
||||
|
||||
- Add `/大富翁榜`, `/金币排行`, `/金币排行榜`, and `/richlist` for a
|
||||
group-scoped virtual coin leaderboard. Rankings are read-only, sorted by
|
||||
current coin balance, recover display names from configured aliases and
|
||||
observed profiles, and mark active loan principal when present.
|
||||
- Split leaderboard command ownership into a dedicated reviewed
|
||||
`coin-ranking` local extension.
|
||||
|
||||
## alpha 0.6.10.1015
|
||||
|
||||
- Add virtual coin loans with `/贷款 [金额]` and `/loan amount`, plus
|
||||
repayments through `/还款 [金额|全部]` and `/repay`. Loans are stored in
|
||||
the same group-scoped virtual coin account, capped at 500 coins, limited to
|
||||
one active loan per account, and replay-safe through the existing ledger
|
||||
transaction path.
|
||||
- Split loan command ownership into a dedicated reviewed `coin-loan` local
|
||||
extension so the single-player game plugin stays under the command-count
|
||||
validation limit.
|
||||
|
||||
## alpha 0.6.9.1015
|
||||
|
||||
- Move CS Major autochess public game data out of `cs_autochess.py` and into
|
||||
package data files under `jwebot_official_extensions/data/cs_autochess/`.
|
||||
Player cards, coach cards, narrative fragments, shop/economy odds, draft
|
||||
pacing, synergy thresholds, match formats, MR15/overtime settings, and
|
||||
special event rewards are now data driven.
|
||||
- Add startup validation for the externalized data so missing fields,
|
||||
duplicate card IDs, invalid schema versions, or an undersized narrative
|
||||
library fail fast before the command engine runs.
|
||||
- Add regression coverage that verifies the JSON files are packaged, readable,
|
||||
and consistent with the runtime card, coach, match-format, and narrative
|
||||
inventories.
|
||||
- Add virtual coin transfers with `/转账 对方 金额` and `/transfer name amount`.
|
||||
Transfers are single-transaction ledger updates, require the receiver to
|
||||
initialize `/金币` first, reject self-transfers, and replay safely by event ID.
|
||||
|
||||
## alpha 0.6.8.1015
|
||||
|
||||
- Add an atomic compatibility path for old active CS autochess runs. When a
|
||||
group still has a pre-schema-3 active tournament, `/自走棋开始` now archives
|
||||
that old run as cancelled and opens a fresh schema-3 season in the same
|
||||
SQLite transaction.
|
||||
- Preserve old story and battle archive rows while ensuring the new season is
|
||||
the only active autochess run selected by future commands.
|
||||
|
||||
## alpha 0.6.7.1015
|
||||
|
||||
- Expand CS Major autochess content to 100+ player cards and an 18-coach
|
||||
metadata pool while keeping the opening coach draft to 10 visible choices.
|
||||
- Replace abstract training targets with real round-robin training rounds.
|
||||
Each round resolves participant-vs-participant or participant-vs-AI BO1
|
||||
scrims, then opens an operation window for D rolling, locking, lineup swaps,
|
||||
tactics, and readiness.
|
||||
- Replace fixed score bands with deterministic MR15 round simulation. Map
|
||||
scores now come from individual round outcomes, including 15-15 overtime,
|
||||
instead of direct mappings such as 16-6 or 16-9.
|
||||
- Add a 120+ line local narrative library for openings, streaks, pauses,
|
||||
clutches, economy beats, closes, training hooks, and media/sponsor
|
||||
challenges so match reports vary across maps.
|
||||
- Hide raw target/performance calculation text from training, match, and
|
||||
challenge reports while preserving structured battle facts in the archive.
|
||||
- Start drafted players as two-star素材 and rebalance roster completion toward
|
||||
low/mid-cost cards, leaving upgrades and synergies to the shop loop.
|
||||
|
||||
## alpha 0.6.6.1015
|
||||
|
||||
- Rebuild CS Major autochess around multi-message hosting: tournament
|
||||
welcomes, seed order, draft boards, status panels, training results, match
|
||||
reports, standings, brackets, and awards are emitted as separate bounded
|
||||
replies.
|
||||
- Replace instant full rosters with three draft rounds: round one selects from
|
||||
a fixed 10-coach pool, rounds two and three draft players, then each team is
|
||||
completed to five starters, one coach, and three bench slots.
|
||||
- Run the full event as training camp, BO3 single-round-robin group stage,
|
||||
one media/sponsor challenge after group round one or two, BO5 semifinals,
|
||||
BO5 final, and final awards.
|
||||
- Let readiness advance phases automatically. Group matches wait for all real
|
||||
players, while knockout matches only wait for real players still in the
|
||||
current pairing, so eliminated players are not prompted to prepare.
|
||||
- Add local multi-text reply support across the SDK, official extension
|
||||
adapter, Edge response contract, and recorder GUI sender while preserving
|
||||
single-message compatibility.
|
||||
|
||||
## alpha 0.6.5.1015
|
||||
|
||||
- Extend the AI test path for CS Major autochess to support two-human games:
|
||||
`/自走棋加AI战队` now adds AI 战队 A and AI 战队 B when exactly two
|
||||
real players have registered.
|
||||
- Keep the same command for two- and three-human test modes. The tournament
|
||||
engine selects 2 人+2 AI or 3 人+1 AI from the current registration count.
|
||||
- Lock the roster after AI teams join, seed the four-team bracket as 1v4 and
|
||||
2v3, and keep AI teams out of the human preparation order while preserving
|
||||
independent AI operations and story archives.
|
||||
|
||||
## alpha 0.6.4.1015
|
||||
|
||||
- Add a reviewed 2-8 player CS Major autochess extension with a fixed
|
||||
five-player starting lineup, one always-active coach, an eight-slot bench,
|
||||
and a six-candidate shop that shows complete game data.
|
||||
- Ship a fixed 60-player local pool with rifle, sniper, and in-game-leader
|
||||
roles; team and nation synergies; shared copy counts; and deterministic
|
||||
three-copy/two-star and nine-copy/three-star upgrades.
|
||||
- Keep all five player slots occupied. Lineup changes are one-for-one swaps,
|
||||
starters cannot be sold directly, and coach upgrades only add a 2%-10%
|
||||
team bonus.
|
||||
- Add player-selected firepower, stability, and clutch tactics. Resolve
|
||||
stability over firepower, firepower over clutch, clutch over stability,
|
||||
plus sniper/rifle/leader role counters before bounded +/-2% map variance.
|
||||
- Generate the competition format once at tournament start: two players go
|
||||
directly to the final, three play a round robin for two final places, four
|
||||
use seeded 1v4 and 2v3 semifinals, and five to eight play a complete round
|
||||
robin for four semifinal places.
|
||||
- Add an explicit three-human-plus-one-AI test mode. The host can add one
|
||||
deterministic local AI as seed four; it buys players, upgrades its coach,
|
||||
swaps its strongest lineup, and selects a counter tactic without an online
|
||||
model.
|
||||
- Give the bot a persistent host state machine: welcome and format briefing,
|
||||
rotating human preparation order, turn enforcement, explicit handoffs,
|
||||
round introductions, matchup calls, overtime narration, advancement, and
|
||||
championship presentation.
|
||||
- Run group matches as BO3 and knockout matches as BO5. Simulate every map
|
||||
under MR15, including 15-15 overtime, 19-17/22-20 scores, and separately
|
||||
archived overtime stories.
|
||||
- Persist run state, idempotent action responses, structured battles, and
|
||||
every generated story in serialized SQLite transactions. Invalid archives
|
||||
roll back the full action.
|
||||
|
||||
## alpha 0.6.3.1015
|
||||
|
||||
- Replace instant personal lottery tickets with one persistent multiplayer
|
||||
lottery round per group: start, register/update, status, draw, and cancel.
|
||||
- Treat each ticket as three distinct digits from 0-9. Sort digits before
|
||||
storage so `347` and `743` represent the same selection.
|
||||
- Accept spaced and compact OCR forms. Ticket stakes are 1-500 coins with a
|
||||
default of 10 when omitted.
|
||||
- Return 0%, 50%, 120%, or 300% of the stake for matching zero, one, two, or
|
||||
three drawn digits. All settlement values remain integer coins.
|
||||
- Commit entry updates, stake differences, all-player payouts, cancellation
|
||||
refunds, and event replay responses in serialized SQLite transactions.
|
||||
- Raise the blackjack stake range to 1-500 coins.
|
||||
- Add per-game personal sequence numbers and `/游戏记录` (`/gamehistory`) for
|
||||
prize draws, lottery rounds, and blackjack hands.
|
||||
- Prefix game output with the configured player label and game number, such as
|
||||
`Player #1:...`; aliases share the same counter and account.
|
||||
|
||||
## alpha 0.6.2.1015
|
||||
|
||||
- Bound direct DeepSeek SSE reads to 30 seconds of total wall time in addition
|
||||
to the existing 18-second socket inactivity timeout.
|
||||
- Stop reading as soon as the SSE `[DONE]` marker arrives instead of waiting
|
||||
for the upstream connection to close.
|
||||
- Prevent a trickling or unfinished upstream response from occupying both
|
||||
generation workers for about 15 minutes and delaying later final replies.
|
||||
- Preserve fast ACK delivery, three-attempt retry behavior, two generation
|
||||
workers, and strict final-reply ordering.
|
||||
- Treat `/21点 开始 100` as a 100-coin stake and accept OCR forms with missing
|
||||
whitespace, such as `/21点开始100` and `/blackjackstart100`.
|
||||
- Keep blackjack play as explicit steps: start and deal first, then hit or
|
||||
stand in a separate command. Combined forms return this workflow without
|
||||
opening a game or deducting funds.
|
||||
- Prefix blackjack hands, status and settlement replies with the current
|
||||
group-scoped player's configured display label, such as `player:...`.
|
||||
|
||||
## alpha 0.6.1.1015
|
||||
|
||||
- Add three reviewed plugins: local context summaries for 100/200/300 messages,
|
||||
person and alias lookup, and single-player virtual coin games.
|
||||
- Provide corresponding Chinese and English commands, including `/context`,
|
||||
`/people`, `/person`, `/wallet`, `/draw`, `/lottery`, and `/blackjack`.
|
||||
- Grant each group-scoped person 1000 virtual coins once; persist account state,
|
||||
private blackjack decks, timeout/forfeit rules, and win/loss statistics.
|
||||
- Commit funds, game state and event replay responses in one SQLite transaction;
|
||||
retries never debit twice, and aliases share the canonical person's account.
|
||||
- Reconcile the message image after a two-second idle wait so a missed final
|
||||
XDamage notification does not strand an isolated message; drain buffered Xlib
|
||||
events before waiting on the socket. Preserve visual deduplication and guards.
|
||||
- Keep the generated feature list within the sender's 900-character boundary.
|
||||
- Document local extractive-summary coverage, OCR identity limitations,
|
||||
game payouts, persistence and rollback behavior.
|
||||
|
||||
## alpha 0.6.0.1015
|
||||
|
||||
- Replace repository-local and hardcoded deployment values with independent,
|
||||
@@ -32,18 +228,581 @@
|
||||
backup; pre-public operational history remains only in protected local
|
||||
archives.
|
||||
|
||||
## Pre-Public Milestones
|
||||
## alpha 0.5.11.1015
|
||||
|
||||
Earlier alpha work established:
|
||||
- Reduce the conservative follow-latest scroll burst from a fixed `25ms` to
|
||||
`1ms` between each of the existing 30 wheel events.
|
||||
- Reuse the XDamage producer's verified WeChat window identifier in the
|
||||
adaptive ready-layout gate instead of rediscovering the same window before
|
||||
each screenshot.
|
||||
- Preserve the shared GUI lock, ready-window geometry gate, second screenshot,
|
||||
and loaded-bottom visual confirmation.
|
||||
- Add privacy-safe per-step capture telemetry and regression coverage for the
|
||||
optimized slow path.
|
||||
|
||||
- adaptive OCR and XDamage capture;
|
||||
- guarded GUI send with visual confirmation;
|
||||
- durable ACK/final delivery ordering;
|
||||
- privacy-safe latency telemetry;
|
||||
- capability-limited SDK contracts;
|
||||
- signed extension artifacts and isolated Host execution;
|
||||
- reviewed command ownership and generated adapters;
|
||||
- coordinated production backup, health gates, and rollback.
|
||||
## alpha 0.5.10.1015
|
||||
|
||||
Private host, person, address, account, incident, and deployment details are
|
||||
intentionally omitted from the public changelog.
|
||||
- Prioritize queued fast acknowledgements and local-command replies over queued
|
||||
final replies in the guarded GUI sender.
|
||||
- Keep the active GUI send non-preemptive and preserve strict FIFO ordering
|
||||
within each priority class through a monotonic sequence number.
|
||||
- Raise the adaptive recorder CPU quota from `150%` to `250%` to reduce
|
||||
screenshot subprocess stalls under OCR and GUI contention.
|
||||
- Add deterministic coverage for ACK/local priority, final FIFO ordering,
|
||||
single-sender serialization, and clean worker shutdown.
|
||||
|
||||
## alpha 0.5.9.1014
|
||||
|
||||
- Preserve both capture call contracts in the adaptive ready-window wrapper:
|
||||
zero arguments for startup/polling and one verified window identifier for the
|
||||
optimized XDamage producer.
|
||||
- Fix the alpha 0.5.8 internal `CaptureProducerError` restart loop that began on
|
||||
the first relevant XDamage event.
|
||||
- Add direct regression tests for both ready-gated capture signatures.
|
||||
|
||||
## alpha 0.5.8.1014
|
||||
|
||||
- Recover one unseen bottom-most bot trigger when a viewport resync has no
|
||||
visual overlap, instead of silently dropping the new command.
|
||||
- Persist a bounded recent-trigger visual history so an old command is not
|
||||
replayed by a later no-overlap resync; ordinary messages remain fail-closed.
|
||||
- Seed that history from one startup baseline before enabling recovery, so a
|
||||
service restart cannot replay a visible pre-existing command.
|
||||
- Reuse the XDamage monitor's verified WeChat window identifier and the
|
||||
maximize gate's geometry result, removing one redundant window lookup and
|
||||
one redundant geometry subprocess from each changed-frame capture.
|
||||
- Add regression coverage for unseen-trigger recovery, seen-trigger replay
|
||||
suppression, signature migration, and the optimized capture path.
|
||||
|
||||
## alpha 0.5.7.1014
|
||||
|
||||
- Replace the single direct-request worker with two parallel generation workers
|
||||
and one strictly ordered GUI delivery worker for ordinary chat and Steam
|
||||
requests.
|
||||
- Keep final replies in durable `created_at, event_id` order even when a newer
|
||||
provider call completes first; failed acknowledgements and exhausted requests
|
||||
remain terminal and do not block later replies.
|
||||
- Separate generation and delivery retry counters so each phase retains its
|
||||
complete retry budget.
|
||||
- Wake generation and delivery workers immediately on enqueue, generation
|
||||
completion, and acknowledgement instead of adding a one-second idle polling
|
||||
delay to the normal path.
|
||||
- Split direct-queue telemetry into `direct_generation` and `direct_delivery`
|
||||
phases and add concurrency, ordering, ACK-race, retry, Steam, and duplicate
|
||||
regression coverage.
|
||||
|
||||
## alpha 0.5.6.1014
|
||||
|
||||
- Preserve both pre-draft and post-draft group fingerprint gates while
|
||||
capturing only the configured title region instead of decoding the complete
|
||||
WeChat window twice per GUI send.
|
||||
- Keep the previous full-window fingerprint path as a compatibility fallback
|
||||
for older adaptive upstream modules.
|
||||
- Add GUI sender coverage for the title-region primary path, full-window
|
||||
fallback, and visual group-mismatch fail-closed handling.
|
||||
|
||||
## alpha 0.5.5.1014
|
||||
|
||||
- Accept incoming WeChat bubbles up to `900px` wide so long single-line bot
|
||||
mentions are not discarded before OCR; preserve position, height, area, and
|
||||
outgoing-bubble exclusion gates.
|
||||
- Wait for adaptive OCR warmup and a first baseline capture from the current
|
||||
service start before reporting `adaptive ready` or successful deployment.
|
||||
- Fail and roll back deployment if the adaptive service exits or cannot
|
||||
establish its recorder baseline within 90 seconds.
|
||||
- Retry the WeChat ready-window gate for up to 30 seconds and discover the
|
||||
exact `Weixin` window by both class and title before failing closed.
|
||||
- Pin production privilege switching and X11 inspection to verified absolute
|
||||
executable paths so a restricted root `PATH` cannot masquerade as a missing
|
||||
WeChat window.
|
||||
|
||||
## alpha 0.5.4.1014
|
||||
|
||||
- Stabilize visible-message sequence alignment with thresholded binary
|
||||
sender/body fingerprints while retaining exact RGB keys for OCR cache
|
||||
invalidation.
|
||||
- Reset 0.5.3 exact-visual recorder state to a safe no-reply baseline during
|
||||
the signature migration.
|
||||
- Prefix coordinated deploy phases with `[JWeBot]`, report explicit
|
||||
`starting`, `ready`, and `active` states, and run user installation from the
|
||||
target home to avoid inherited `/root` working-directory warnings.
|
||||
|
||||
## alpha 0.5.3.1014
|
||||
|
||||
- Align visible message sequences with existing exact bubble visual
|
||||
fingerprints instead of OCR text, preventing scroll/resync OCR drift from
|
||||
dropping a newly mentioned slash command.
|
||||
- Reset legacy text-signature recorder state to a no-reply visual baseline on
|
||||
startup, avoiding duplicate replay during the signature-scheme migration.
|
||||
- Preserve repeated identical commands as separate visual occurrences.
|
||||
|
||||
## alpha 0.5.2.1014
|
||||
|
||||
- Print an ASCII pixel-art project signature only after the coordinated
|
||||
production deployer passes every cutover gate and disables rollback.
|
||||
|
||||
## alpha 0.5.1.1014
|
||||
|
||||
- Exclude the static bottom-left input hint from the realtime text ROI so an
|
||||
empty WeChat input box is not misclassified as dirty.
|
||||
- Label the corrected ROI as `input_text_core_v2` in bridge telemetry, keeping
|
||||
pre-fix and post-fix production evidence distinguishable.
|
||||
|
||||
## alpha 0.5.0.1014
|
||||
|
||||
- Move visible WeChat delivery onto a persistent in-process GUI sender queue
|
||||
hosted by the adaptive recorder, avoiding per-message bridge subprocesses for
|
||||
ACK, local-command replies, and final deliveries.
|
||||
- Add a private AF_UNIX sender socket so Edge delivery helpers submit final
|
||||
replies to the same serialized GUI queue instead of racing the input box.
|
||||
- Combine clear, paste, ROI draft flag, post-draft group check, Enter send,
|
||||
ROI clear confirmation, and send-button fallback into one bridge call.
|
||||
- Confirm the input ROI is empty after Ctrl-A/Backspace before using it as the
|
||||
paste baseline, preventing repeated `喵......` ACK drafts from being mistaken
|
||||
for paste failures.
|
||||
|
||||
## alpha 0.4.15.1014
|
||||
|
||||
- Force adaptive recorder logging to INFO so GUI bridge timing telemetry is
|
||||
visible on ACK and final delivery paths.
|
||||
- Allow ROI, poll, pixel-threshold, flag, and coordinate fields through the
|
||||
packaged runtime telemetry sanitizer.
|
||||
|
||||
## alpha 0.4.14.1014
|
||||
|
||||
- Replace full-input send confirmation with a left-side input-text ROI flag so
|
||||
draft and send confirmation poll only the region that should visibly change.
|
||||
- Restore the guarded send order to Enter first, then click the scaled send
|
||||
button only as a fallback when ROI clearing is not visually confirmed.
|
||||
- Emit ROI coordinates, flag state, flag wait time, confirmation source, and
|
||||
bridge stderr tail diagnostics for GUI delivery failures.
|
||||
|
||||
## alpha 0.4.13.1014
|
||||
|
||||
- Emit detailed guarded GUI delivery timings for lock acquisition, pre/post
|
||||
draft screenshots, group fingerprint checks, bridge subprocess duration,
|
||||
draft paste confirmation polling, send-button confirmation polling, Enter
|
||||
fallback, callback hooks, and total send time.
|
||||
- Return per-step timing metadata from the adaptive GUI bridge so ACK and final
|
||||
delivery reports can attribute fixed waits, screenshot confirmation, VNC
|
||||
input, and fallback paths without logging message text.
|
||||
|
||||
## alpha 0.4.12.1014
|
||||
|
||||
- Optimize guarded WeChat GUI delivery by clicking the scaled send button first
|
||||
and polling for visual confirmation instead of waiting on fixed sleeps.
|
||||
- Preserve the Enter-key fallback when the button path is not visually
|
||||
confirmed.
|
||||
|
||||
## alpha 0.4.11.1014
|
||||
|
||||
- Add explicit `edge.fast_ack.*` and `recorder.fast_ack.*` telemetry names
|
||||
alongside the existing ACK metrics, making fast acknowledgement latency
|
||||
independently filterable without breaking historical dashboards.
|
||||
|
||||
## alpha 0.4.10.1014
|
||||
|
||||
- Cache configured person-alias rows once per group and make configured
|
||||
identity persistence idempotent, eliminating thousands of redundant
|
||||
`FULL`-synchronous SQLite commits during Edge startup.
|
||||
- Reduce measured production host systemd socket readiness on the production database
|
||||
snapshot from more than 90 seconds to about one second.
|
||||
|
||||
## alpha 0.4.9.1014
|
||||
|
||||
- Wait up to 45 seconds for the newly started Edge socket and health response
|
||||
before deciding that a production cutover failed, while still failing
|
||||
immediately if systemd marks the service failed.
|
||||
|
||||
## alpha 0.4.8.1014
|
||||
|
||||
- Keep the adaptive OCR process alive while WeChat is absent or awaiting login,
|
||||
then resume capture against the ready main window without a systemd restart
|
||||
storm.
|
||||
- Install the reviewed `wechat_local_ocr.py` beside the adaptive runtime instead
|
||||
of importing an independently managed root copy.
|
||||
- Add a root-coordinated production deploy that records both systemd
|
||||
topologies, backs up root and user files plus SQLite, disables both legacy OCR
|
||||
units, and rolls back all layers when a health gate fails.
|
||||
- Require empty OCR queues, a ready WeChat window, Edge socket health, stable
|
||||
adaptive restart counts, and SQLite integrity before accepting a production
|
||||
cutover.
|
||||
- Preserve incoming service state in legacy install and upgrade tools; legacy
|
||||
OCR now remains disabled unless an operator explicitly opts in.
|
||||
- Prevent Extension Host imports from writing bytecode into verified releases,
|
||||
and exclude only standard interpreter cache files from source packaging.
|
||||
- Add deployment contract tests and shell-aware syntax validation to the local
|
||||
release gate.
|
||||
|
||||
## alpha 0.4.7.1014
|
||||
|
||||
- Prewarm RapidOCR recognition and detection paths before live capture.
|
||||
- Cache successful bubble recognition by bounded exact visual fingerprint,
|
||||
recognize only new or changed crops, and periodically revalidate the cache.
|
||||
- Persist OCR command events through atomic file replacement plus file and
|
||||
directory `fsync`, then immediately wake the in-process responder while
|
||||
retaining the one-second recovery scan.
|
||||
- Name the three execution paths explicitly as `local-command`, `deepseek`, and
|
||||
`openclaw` in Edge responses and privacy-safe routing telemetry.
|
||||
- Add signed plugin description/help metadata, validate it through Host health,
|
||||
generate bundled `/功能列表` output automatically, and update plugin creation
|
||||
tooling and author documentation.
|
||||
|
||||
## alpha 0.4.6.1014
|
||||
|
||||
- Add a repeatable local performance baseline for OCR preprocessing, sequence
|
||||
alignment, durable ACK/queue state, and real signed Host process execution.
|
||||
- Add a structured telemetry-log analyzer with p50, p95, maximum, status, and
|
||||
event-count summaries.
|
||||
- Measure queue waiting, capture-buffer age, ACK end-to-end latency, and final
|
||||
Edge delivery latency without recording message content.
|
||||
- Establish the bottleneck order from local measurements and existing runtime
|
||||
evidence: RapidOCR, provider generation, polling wakeups, then one-shot Host
|
||||
startup; SQLite and sequence work remain below 2 ms p95 locally.
|
||||
- Document the evidence, measurement gaps, and optimization order for later
|
||||
`0.4.x` iterations.
|
||||
|
||||
## alpha 0.4.5.1014
|
||||
|
||||
- Close the approved repository-only Extension Host migration scope.
|
||||
- Treat an invalid installed signed release as a startup failure rather than a
|
||||
preferred-mode fallback; only missing or startup-unhealthy Hosts may fall
|
||||
back.
|
||||
- Enforce the end-to-end Host deadline after Kernel service execution and
|
||||
expand transport-error telemetry.
|
||||
- Document completed local work, explicitly deferred external operations, and
|
||||
the next measurement-led OCR/queue/ACK performance-analysis phase.
|
||||
|
||||
## alpha 0.4.4.1014
|
||||
|
||||
- Add privacy-safe structured telemetry for OCR, routing, ACK preparation and
|
||||
confirmation, queues, providers, Host processes, service brokering, and final
|
||||
GUI delivery.
|
||||
- Hash event and group identifiers, reject message-body fields, and retain only
|
||||
low-cardinality process-local counts and latency summaries.
|
||||
- Expose telemetry summaries through Edge health without making telemetry part
|
||||
of delivery correctness.
|
||||
- Instrument OCR capture/recognition/queue stages and distinguish ACK GUI
|
||||
delivery from Edge confirmation.
|
||||
- Document ACK ordering and the measurement points needed for future OCR,
|
||||
queue, Host-startup, and provider latency optimization.
|
||||
|
||||
## alpha 0.4.3.1014
|
||||
|
||||
- Require a signed release to pass isolated Host health before atomic
|
||||
activation; failed new releases are removed without switching `current`.
|
||||
- Add explicit local-only health bypass for packaging environments.
|
||||
- Complete real-process parity coverage for all 12 reviewed plugins, including
|
||||
memory CRUD, both stateful games, dice challenge, rankings, history, and
|
||||
Steam fast-ACK final generation.
|
||||
- Fix Host protocol writes so broker messages bypass redirected plugin stdout
|
||||
without exposing plugin output to the parent protocol.
|
||||
|
||||
## alpha 0.4.2.1014
|
||||
|
||||
- Load installed signed plugin manifests into a deterministic startup catalog
|
||||
and freeze command ownership before routing.
|
||||
- Add `in-process`, `prefer-host`, and `host-only` Edge execution modes with
|
||||
subprocess or AF_UNIX Host transports.
|
||||
- Route signed healthy plugins through the isolated Host while preserving
|
||||
Kernel-direct commands and Steam fast-ACK queueing.
|
||||
- Permit startup fallback for missing or unhealthy preferred Hosts, but never
|
||||
replay an active invocation after a possible stateful service call.
|
||||
- Add real subprocess end-to-end coverage from Edge through the generated
|
||||
adapter, Host protocol, Kernel service broker, and final reply.
|
||||
|
||||
## alpha 0.4.1.1014
|
||||
|
||||
- Add bounded bidirectional JSON-line service brokering between isolated
|
||||
Extension Host processes and Kernel-owned service profiles.
|
||||
- Enforce declared profile ownership, call IDs, call count, message size, and
|
||||
end-to-end process/socket deadlines.
|
||||
- Add Host health inventory responses and broker support to both subprocess and
|
||||
AF_UNIX clients.
|
||||
- Keep service errors generic across the isolation boundary.
|
||||
|
||||
## alpha 0.4.0.1014
|
||||
|
||||
- Start the approved `0.4.x` local extension-host migration and performance
|
||||
instrumentation milestone.
|
||||
- Keep signing-pipeline integration, key rotation, PR rehearsal, target-machine
|
||||
provisioning, deployment, and Linux verification deferred.
|
||||
|
||||
## alpha 0.3.1.1014
|
||||
|
||||
- Add OpenSSH `sshsig` publisher signatures for canonical extension release
|
||||
records.
|
||||
- Bind the publisher principal, release filename, artifact metadata, source
|
||||
digest, manifest author, and exact file list through one signed payload.
|
||||
- Require trusted publisher verification for activation, Host loading, and
|
||||
rollback; unsigned paths now require an explicit local-test override.
|
||||
- Persist the canonical signature record with each installed release and
|
||||
re-verify it before execution or rollback.
|
||||
- Add signing support to package, verify, activate, rollback, and standalone
|
||||
release-signing tools.
|
||||
- Keep production host provisioning, deployment, and systemd validation deferred.
|
||||
|
||||
## alpha 0.3.0.1014
|
||||
|
||||
- Close the `0.2.x` complete-function migration after the corrected granular
|
||||
plugin ownership model passed the full local gate.
|
||||
- Start the approved `0.3.x` baseline with Kernel-direct operational commands,
|
||||
official-owned person memory, and 11 project-owned plugins.
|
||||
- Keep Linux deployment and systemd verification explicitly deferred.
|
||||
|
||||
## alpha 0.2.6.1014
|
||||
|
||||
- Replace grouped plugin domains with one plugin per independently
|
||||
understandable function.
|
||||
- Keep dice and dice challenge as one complete project-authored collection.
|
||||
- Make person-memory the only official-authored command plugin.
|
||||
- Move feature list, status, and balance to unconditional Kernel-direct
|
||||
routing.
|
||||
- Add required manifest author metadata and release-record schema version 2.
|
||||
- Generate 11 project plugin packages under the reviewed local-extension namespace and keep only
|
||||
person-memory under `extensions/official`.
|
||||
- Rename the canonical rollback switch to
|
||||
`JWEBOT_LOCAL_EXTENSIONS_ENABLED`, while accepting the old environment key
|
||||
as a compatibility fallback.
|
||||
|
||||
## alpha 0.2.5.1014
|
||||
|
||||
- Move the complete Steam provider into `jwebot-official-extensions`.
|
||||
- Migrate fixed roster, group aliases, mode filtering, single-person lookup,
|
||||
cache, API batching, proxy enforcement, and error classification.
|
||||
- Preserve the existing public `SteamClient`, `SteamUnavailable`, API base, and
|
||||
Mihomo default exports through the packaged Edge runtime.
|
||||
- Keep the no-direct-network-fallback regression tests passing.
|
||||
|
||||
## alpha 0.2.4.1014
|
||||
|
||||
- Add full official-adapter versus compatibility-path parity tests.
|
||||
- Cover every stateless command family, all memory operations, every
|
||||
dice-challenge phase, guess-number lifecycle, RPS lifecycle, status, balance,
|
||||
Steam generation, and restart persistence.
|
||||
- Make parity and persistence checks part of the permanent local/CI gate.
|
||||
|
||||
## alpha 0.2.3.1014
|
||||
|
||||
- Route all known local commands through generated official SDK adapters by
|
||||
default.
|
||||
- Execute stateful behavior through named in-process service brokers without
|
||||
exposing raw Ledger or providers to adapters.
|
||||
- Preserve the Steam fast-ACK asynchronous path through the official adapter.
|
||||
- Add `JWEBOT_OFFICIAL_EXTENSIONS_ENABLED=0` as a compatibility rollback to
|
||||
direct command-engine dispatch.
|
||||
- Keep unknown slash-command errors on the compatibility path.
|
||||
- Install generated adapter modules with the official package.
|
||||
|
||||
## alpha 0.2.2.1014
|
||||
|
||||
- Add reviewed `service_profiles` as a narrow SDK/manifest capability.
|
||||
- Generate real Host-loadable packages for all seven official function domains.
|
||||
- Register every existing alias through SDK command adapters.
|
||||
- Delegate complex stateful behavior through named Kernel service profiles
|
||||
instead of exposing raw Ledger or provider objects.
|
||||
- Add synchronization checks so official manifests, adapters, docs, and command
|
||||
ownership cannot drift.
|
||||
- Validate, source-scan, and package every official extension in CI.
|
||||
|
||||
## alpha 0.2.1.1014
|
||||
|
||||
- Move the complete local command engine out of the Edge runtime into the
|
||||
installable `jwebot-official-extensions` package.
|
||||
- Preserve all command replies, persistence, deterministic seeds, game state
|
||||
machines, memory operations, rankings, status, balance, and Steam behavior.
|
||||
- Inject Ledger, Steam, balance, reply-cleaning, identity, and memory-policy
|
||||
brokers from Kernel instead of importing Edge internals.
|
||||
- Add a unique ownership map covering every current command kind and alias.
|
||||
- Add an authoritative migration matrix documenting every command subfunction
|
||||
and non-command Kernel responsibility.
|
||||
|
||||
## alpha 0.2.0.1014
|
||||
|
||||
- Start the approved `0.2` full-function migration milestone.
|
||||
- Move the complete Edge runtime into `jwebot-kernel`.
|
||||
- Keep `jwebot_edge.py` as a compatibility CLI and module alias so existing
|
||||
imports and runtime monkeypatches continue to affect implementation globals.
|
||||
- Update tests and the installer for the packaged runtime.
|
||||
- Preserve all existing command, queue, ledger, model, memory, broadcast, and
|
||||
delivery behavior.
|
||||
- Do not deploy or validate on Linux in this stage.
|
||||
|
||||
## alpha 0.1.11.1014
|
||||
|
||||
- Add a bounded AF_UNIX client matching the systemd socket-activated Host
|
||||
design.
|
||||
- Send one canonical JSON request, half-close the write side, and read one
|
||||
bounded response.
|
||||
- Fail closed on socket timeout, unavailable socket, malformed response, and
|
||||
Host error status.
|
||||
- Keep the socket client disconnected from the current production Edge path.
|
||||
|
||||
## alpha 0.1.10.1014
|
||||
|
||||
- Add uninstalled systemd socket/service templates for the extension Host.
|
||||
- Specify a fixed unprivileged identity, AF_UNIX-only private network,
|
||||
read-only system/release paths, empty capabilities, syscall filtering, and
|
||||
CPU/memory/process/runtime limits.
|
||||
- Add static sandbox-policy checks and unit tests.
|
||||
- Require explicit target-host provisioning and systemd verification before
|
||||
any future enablement.
|
||||
|
||||
## alpha 0.1.9.1014
|
||||
|
||||
- Add the Kernel-side extension Host client with a minimal inherited
|
||||
environment and isolated process group.
|
||||
- Enforce parent wall-time deadlines and kill the entire Host process group on
|
||||
timeout.
|
||||
- Bound request, response, and stderr sizes and reject malformed protocol
|
||||
responses.
|
||||
- Limit plugin stdout/stderr inside the Host.
|
||||
- Hash both plugin log messages and field-name sets instead of emitting plugin
|
||||
text.
|
||||
- Test timeout child-process cleanup and environment-secret exclusion.
|
||||
- Keep the client disconnected from the production Edge path.
|
||||
|
||||
## alpha 0.1.8.1014
|
||||
|
||||
- Add an installable `jwebot-extension-host` package.
|
||||
- Re-verify installed release content, manifest, and source policy before import.
|
||||
- Enforce exact declared command registration and freeze it before invocation.
|
||||
- Add a bounded single-request JSON stdin/stdout protocol.
|
||||
- Scrub inherited environment values and redirect plugin stdout away from IPC.
|
||||
- Keep storage, HTTP, and scheduling brokers fail-closed.
|
||||
- Return generic plugin failure classes without exception text.
|
||||
- Do not connect the Host to Edge or install it on production host.
|
||||
|
||||
## alpha 0.1.7.1014
|
||||
|
||||
- Add verified release installation under full source-digest directories.
|
||||
- Add locked, atomic `current` and `previous` activation pointers.
|
||||
- Re-verify installed release file lists, records, and source content before
|
||||
reuse.
|
||||
- Add explicit local activation and rollback tools with no default production
|
||||
path.
|
||||
- Test rollback, tampered artifacts, tampered installed content, unsafe
|
||||
pointers, and private installed-file permissions.
|
||||
- Do not execute extension code or modify production host.
|
||||
|
||||
## alpha 0.1.6.1014
|
||||
|
||||
- Add deterministic, content-addressed extension release artifacts.
|
||||
- Add canonical source and artifact SHA-256 records.
|
||||
- Verify artifacts without extraction and reject path traversal, non-regular
|
||||
members, expansion limits, file-list drift, and content tampering.
|
||||
- Refuse replacement of an existing digest-addressed artifact with different
|
||||
bytes.
|
||||
- Build and verify the example extension artifact in Gitea Actions.
|
||||
- Explicitly defer publisher signing and production host activation to a later phase.
|
||||
|
||||
## alpha 0.1.5.1014
|
||||
|
||||
- Add a static extension source-policy preflight before packaging.
|
||||
- Reject unapproved imports, file/process/socket access, dynamic execution
|
||||
builtins, and dunder attribute traversal.
|
||||
- Enforce source count, size, UTF-8, syntax, and symlink limits.
|
||||
- Make `validate-plugin.py` validate both manifest and source policy.
|
||||
- Document that static policy is a CI prefilter, not a replacement for OS
|
||||
process isolation.
|
||||
|
||||
## alpha 0.1.4.1014
|
||||
|
||||
- Restrict extension command names to Unicode word characters, dots, and
|
||||
hyphens.
|
||||
- Reject quote/newline template injection before plugin skeleton files are
|
||||
created.
|
||||
- Add manifest and generator regression tests while preserving Chinese command
|
||||
names.
|
||||
|
||||
## alpha 0.1.3.1014
|
||||
|
||||
- Add a safe command-extension generator that refuses invalid IDs, invalid
|
||||
commands, path traversal, and overwriting existing extensions.
|
||||
- Add official and friend extension namespaces.
|
||||
- Add an author guide, SDK API reference, and reusable AI-author prompt.
|
||||
- Include author tooling and required documentation in the repository gate.
|
||||
|
||||
## alpha 0.1.2.1014
|
||||
|
||||
- Add a deterministic extension catalog that accepts reviewed manifests only
|
||||
during startup.
|
||||
- Reject duplicate plugin IDs and case-insensitive command ownership conflicts.
|
||||
- Freeze registration before runtime command resolution.
|
||||
- Keep catalog views immutable and require explicit command-token boundaries.
|
||||
|
||||
## alpha 0.1.1.1014
|
||||
|
||||
- Make `jwebot-kernel` the authoritative pure routing implementation used by
|
||||
the existing Edge compatibility entrypoint.
|
||||
- Preserve the existing `jwebot_edge.py` public function shapes for current
|
||||
callers and tests.
|
||||
- Update the production host installer to deploy and roll back the SDK and Kernel package
|
||||
directories together with the compatibility scripts.
|
||||
- Do not deploy or restart any production service in this repository change.
|
||||
|
||||
## alpha 0.1.0.1014
|
||||
|
||||
- Start the approved `0.1` milestone with installable `jwebot-sdk` and
|
||||
`jwebot-kernel` packages.
|
||||
- Add versioned event, reply, fact, and extension-result contracts.
|
||||
- Add capability-limited context protocols that expose no GUI, group selector,
|
||||
raw ledger, environment, shell, or unrestricted network access.
|
||||
- Add strict plugin manifest validation and pure local/chat/cloud routing
|
||||
primitives.
|
||||
- Add a minimal `/hello` command extension and package boundary tests.
|
||||
- Keep all existing root runtime entrypoints unchanged for production host compatibility.
|
||||
|
||||
## alpha 0.0.7.1014
|
||||
|
||||
- Move unit tests into `tests/`.
|
||||
- Update tests to load root compatibility entrypoints from the repository root.
|
||||
- Update local verification commands to discover tests from `tests/`.
|
||||
|
||||
## alpha 0.0.6.1014
|
||||
|
||||
- Add repository-safe configuration templates under `config/examples/`.
|
||||
- Add `docs/configuration-guide.md` for runtime configuration ownership and
|
||||
deployment destinations.
|
||||
- Add `tools/check-config.sh` and include it in the local migration gate.
|
||||
- Keep real secrets and production host runtime files outside Git.
|
||||
|
||||
## alpha 0.0.5.1014
|
||||
|
||||
- Add a Gitea Actions workflow that runs the standard local migration gate on
|
||||
push and pull request.
|
||||
- Add `requirements-dev.txt` for local and CI test dependencies.
|
||||
- Document dependency installation in `README.md`.
|
||||
|
||||
## alpha 0.0.4.1014
|
||||
|
||||
- Move home-backup operational material into `ops/home-backup/`.
|
||||
- Move the TigerVNC config snippet into `deploy/config/`.
|
||||
- Keep backup material in the repository for reference while separating it from
|
||||
the bot runtime path.
|
||||
|
||||
## alpha 0.0.3.1014
|
||||
|
||||
- Add `tools/check-local.sh` as the standard local migration gate.
|
||||
- Wire the gate to compile Python entrypoints, validate shell scripts, reject
|
||||
active emergency debug hooks outside their archive docs, and run the full
|
||||
unittest suite.
|
||||
- Document the local check command in `README.md`.
|
||||
|
||||
## alpha 0.0.2.1014
|
||||
|
||||
- Remove temporary emergency HTTP debug hooks from production Python entrypoints.
|
||||
- Preserve removed hook context in `docs/debug/emergency-hooks-archive.md`.
|
||||
- Add `docs/configuration-inventory.md` to document hardcoded addresses, paths,
|
||||
service names, dimensions, models, and their migration policy.
|
||||
- Keep historical incident markdown unchanged for auditability.
|
||||
|
||||
## alpha 0.0.1.1014
|
||||
|
||||
- Establish the repository version baseline for the private alpha migration.
|
||||
- Move operational docs into `docs/`.
|
||||
- Move deployment scripts, defaults, and systemd units into `deploy/`.
|
||||
- Move the cloud responder prompt workspace into `agents/wechat-responder/`.
|
||||
- Move the old direct OpenClaw watcher into `legacy/`.
|
||||
- Keep current Python runtime entrypoints at repository root to preserve test and
|
||||
deployment compatibility during the first migration step.
|
||||
|
||||
@@ -8,7 +8,15 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||
并通过带视觉确认的 GUI 发送链路回复。
|
||||
|
||||
当前版本:`alpha 0.6.0.1015`。
|
||||
当前版本:`beta 1.0.0.1016`,更新日期:2026-10-16。
|
||||
|
||||
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
|
||||
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
|
||||
自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则
|
||||
已经外置为包内 JSON,虚拟金币支持转账、贷款、还款和群内排行榜。
|
||||
|
||||
完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见
|
||||
[CHANGELOG](CHANGELOG.md)。
|
||||
|
||||
### 运行流程
|
||||
|
||||
@@ -72,6 +80,20 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
|
||||
```text
|
||||
/功能列表
|
||||
/上下文 [100|200|300](/context)
|
||||
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
||||
/金币(/wallet);/抽奖 [金额](/draw)
|
||||
/大富翁榜(/金币排行)
|
||||
/转账 对方 金额(/transfer name amount)
|
||||
/贷款 [金额](/loan amount);/还款 [金额|全部](/repay)
|
||||
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
||||
/21点 开始 [1~500];开局后 /21点 要牌|停牌|状态|结束
|
||||
/游戏记录 [页码](/gamehistory)
|
||||
/游戏帮助(/games)
|
||||
/自走棋开始|报名|开赛|状态|档案
|
||||
/自走棋加AI战队(两名或三名真人测试模式)
|
||||
/选1~6(教练轮为 /选1~10);/刷新;/锁定
|
||||
/战术 火力|稳定|残局;/阵容;/上阵 备战编号 [首发位];/出售 备战编号;/结束回合
|
||||
/丢骰子 [NdM]
|
||||
/猜数字 开始 [上限]
|
||||
/石头剪刀布 [开始|加入|出|状态|结算|结束]
|
||||
@@ -240,9 +262,17 @@ visually confirmed GUI bridge.
|
||||
Current version:
|
||||
|
||||
```text
|
||||
alpha 0.6.0.1015
|
||||
beta 1.0.0.1016
|
||||
```
|
||||
|
||||
Updated: 2026-10-16.
|
||||
|
||||
The 1.0 Beta baseline consolidates the stable local command set, person and
|
||||
context indexes, Steam presence/playtime queries, virtual coin games,
|
||||
multiplayer lottery, blackjack, CS Major autochess, and production deployment
|
||||
gates. CS Major autochess game content is now packaged as JSON data, and
|
||||
virtual coins support transfer, loans, repayment, and group leaderboards.
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
```text
|
||||
@@ -308,7 +338,8 @@ migration, debug archives, and root deployment settings.
|
||||
## Commands
|
||||
|
||||
The built-in command inventory is generated from reviewed plugin descriptors.
|
||||
Common commands include:
|
||||
See the full [command catalog](docs/command-catalog.md). Common commands
|
||||
include:
|
||||
|
||||
```text
|
||||
/功能列表
|
||||
@@ -337,6 +368,13 @@ the final separator shown below:
|
||||
/steam时长Dota2人=玩家名
|
||||
```
|
||||
|
||||
Chinese aliases share the same implementations as their English counterparts.
|
||||
Virtual coin funds, game state, transfers, loans, repayments, and replay
|
||||
responses commit through the same ledger transaction path. Native WeChat
|
||||
account IDs are unavailable, so accounts are scoped by OCR display identity and
|
||||
configured aliases. See
|
||||
[plugin rules and capture reconciliation](docs/local-plugins-0.6.1.md).
|
||||
|
||||
Steam prefers private per-person SteamID mappings from the active profile and
|
||||
uses the corresponding group display IDs and aliases. Without a private roster,
|
||||
it discovers the configured account's public friend list at runtime. No real
|
||||
|
||||
@@ -9,6 +9,7 @@ sdk_source_dir="$source_dir/packages/jwebot-sdk/src/jwebot_sdk"
|
||||
kernel_source_dir="$source_dir/packages/jwebot-kernel/src/jwebot_kernel"
|
||||
official_source_dir="$source_dir/packages/jwebot-official-extensions/src/jwebot_official_extensions"
|
||||
official_adapter_source_dir="$official_source_dir/adapters"
|
||||
official_autochess_data_source_dir="$official_source_dir/data/cs_autochess"
|
||||
target_dir="$HOME/.local/lib/wechat-health-watch"
|
||||
unit_dir="$HOME/.config/systemd/user"
|
||||
config_dir="$HOME/.config/jwebot"
|
||||
@@ -237,6 +238,8 @@ trap rollback EXIT HUP INT TERM
|
||||
jwebot_log "user runtime preflight starting"
|
||||
test -x "$runtime_python"
|
||||
for path in \
|
||||
"$source_dir/VERSION" \
|
||||
"$source_dir/CHANGELOG.md" \
|
||||
"$runtime_source_dir/$ocr_script_name" \
|
||||
"$runtime_source_dir/$script_name" \
|
||||
"$runtime_source_dir/$adaptive_script_name" \
|
||||
@@ -262,6 +265,9 @@ do
|
||||
test -d "$directory"
|
||||
find "$directory" -maxdepth 1 -type f -name '*.py' | grep -q .
|
||||
done
|
||||
test -d "$official_autochess_data_source_dir"
|
||||
find "$official_autochess_data_source_dir" -maxdepth 1 -type f -name '*.json' \
|
||||
| grep -q .
|
||||
test -f "$environment_file"
|
||||
|
||||
if systemctl is-active --quiet wechat-local-ocr-recorder.service \
|
||||
@@ -276,6 +282,8 @@ jwebot_log "user runtime backup starting path=$backup_dir"
|
||||
install -d -m 0700 "$target_dir" "$unit_dir" "$config_dir" "$backup_dir"
|
||||
save_unit_states
|
||||
for name in \
|
||||
VERSION \
|
||||
CHANGELOG.md \
|
||||
"$ocr_script_name" \
|
||||
"$script_name" \
|
||||
"$adaptive_script_name" \
|
||||
@@ -326,6 +334,8 @@ systemctl --user stop \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
jwebot_log "user runtime files install starting"
|
||||
install -m 0600 "$source_dir/VERSION" "$target_dir/VERSION"
|
||||
install -m 0600 "$source_dir/CHANGELOG.md" "$target_dir/CHANGELOG.md"
|
||||
install -m 0700 "$runtime_source_dir/$ocr_script_name" "$target_dir/$ocr_script_name"
|
||||
install -m 0700 "$runtime_source_dir/$script_name" "$target_dir/$script_name"
|
||||
install -m 0700 "$runtime_source_dir/$adaptive_script_name" "$target_dir/$adaptive_script_name"
|
||||
@@ -342,7 +352,9 @@ install -d -m 0700 \
|
||||
"$target_dir/jwebot_sdk" \
|
||||
"$target_dir/jwebot_kernel" \
|
||||
"$target_dir/jwebot_official_extensions" \
|
||||
"$target_dir/jwebot_official_extensions/adapters"
|
||||
"$target_dir/jwebot_official_extensions/adapters" \
|
||||
"$target_dir/jwebot_official_extensions/data" \
|
||||
"$target_dir/jwebot_official_extensions/data/cs_autochess"
|
||||
for path in "$config_source_dir"/*.py; do
|
||||
install -m 0600 "$path" "$target_dir/jwebot_config/$(basename "$path")"
|
||||
done
|
||||
@@ -359,6 +371,10 @@ for path in "$official_adapter_source_dir"/*.py; do
|
||||
install -m 0600 "$path" \
|
||||
"$target_dir/jwebot_official_extensions/adapters/$(basename "$path")"
|
||||
done
|
||||
for path in "$official_autochess_data_source_dir"/*.json; do
|
||||
install -m 0600 "$path" \
|
||||
"$target_dir/jwebot_official_extensions/data/cs_autochess/$(basename "$path")"
|
||||
done
|
||||
for name in \
|
||||
"$service_name" \
|
||||
"$timer_name" \
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# JWeBot Command Catalog
|
||||
|
||||
Current baseline: `beta 1.0.0.1016`.
|
||||
|
||||
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 date 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 scan ranking 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 virtual coin account. |
|
||||
| `/大富翁榜 [人数]` | `/金币排行`, `/金币排行榜`, `/richlist` | Show the group-scoped coin 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. |
|
||||
| `/抽奖 [金额]` | `/draw` | Play a single-player prize draw. |
|
||||
| `/彩票 开始` | `/lottery start` | Open a multiplayer lottery round. |
|
||||
| `/彩票 报名 数字 金额` | `/lottery join` | Join or update a lottery entry. |
|
||||
| `/彩票 状态` | `/lottery status` | Show the current lottery round. |
|
||||
| `/彩票 开奖` | `/lottery draw` | Host-only draw for the current lottery round. |
|
||||
| `/彩票 取消` | `/lottery cancel` | Host-only cancellation with refunds. |
|
||||
| `/21点 开始 [1-500]` | `/blackjack start` | Start a blackjack round with the chosen stake. |
|
||||
| `/21点 要牌` | `/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. |
|
||||
| `/游戏记录 [页码]` | `/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
|
||||
|
||||
Status: authoritative local registration and routing contract for
|
||||
`alpha 0.6.x`.
|
||||
`beta 1.0.x`.
|
||||
|
||||
## Three Execution Paths
|
||||
|
||||
@@ -25,7 +25,8 @@ error. They never fall through to DeepSeek or OpenClaw.
|
||||
|
||||
The local command path is:
|
||||
|
||||
1. XDamage reports a relevant message-region change.
|
||||
1. XDamage reports a relevant message-region change, or a two-second idle
|
||||
reconciliation capture detects a missed final drawing.
|
||||
2. OpenCV segments incoming bubbles without running OCR over the whole screen.
|
||||
3. The OCR engine reuses exact visual-fingerprint matches from a bounded LRU.
|
||||
4. Only new or changed sender/body crops use RapidOCR.
|
||||
@@ -45,6 +46,10 @@ because each bubble position still produces its own `IncomingMessage`.
|
||||
Changed pixels force fresh OCR. The cache is bounded and periodically
|
||||
revalidated.
|
||||
|
||||
For the local plugins introduced during the 0.6.x line, including bilingual
|
||||
commands, person identity limitations and atomic single-player game settlement,
|
||||
see [local plugin rules](local-plugins-0.6.1.md).
|
||||
|
||||
## Bundled Plugin Registration
|
||||
|
||||
Bundled command metadata has one source:
|
||||
@@ -141,7 +146,8 @@ descriptors unless they are strictly Kernel-only. The current official
|
||||
operational commands are:
|
||||
|
||||
- `/状态` and `/status`: server, Edge, OCR, and queue status.
|
||||
- `/version`: installed JWeBot package versions.
|
||||
- `/version`: installed JWeBot package versions plus the latest changelog date
|
||||
and summary.
|
||||
- `/debug`: recent privacy-safe telemetry p50/p95/max summary.
|
||||
|
||||
These commands delegate to Kernel-owned service profiles through generated
|
||||
|
||||
@@ -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~500。`/21点 开始 100` 只表示本局下注 100;
|
||||
发牌后再单独发送 `/21点 要牌` 或 `/21点 停牌`。
|
||||
|
||||
## 持久化与并发
|
||||
|
||||
彩票轮次、报名和结算保存在 Edge SQLite。报名/改注、所有玩家的开奖返还、
|
||||
取消退款和事件重放响应都在 `BEGIN IMMEDIATE` 事务中提交。重复事件不会
|
||||
重复扣款、发奖或增加局号;进程重启后未开奖轮次和个人记录仍然保留。
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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 虚拟金币。再次查询、
|
||||
重启服务或切换已配置别名都不会重复领取。抽奖下注为 2~100,彩票和
|
||||
21 点下注为 1~500;余额不足或参数非法不扣款。
|
||||
|
||||
`/转账 对方 金额` 会把本人的虚拟金币转给同群已初始化账户,英文为
|
||||
`/transfer name amount`。金额范围 1~100000;不允许给自己转账。
|
||||
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
|
||||
不会重复扣款。
|
||||
|
||||
`/贷款 [金额]` 会给本人账户借入 1~500 金币;同一账户同一时间最多
|
||||
一笔未还贷款。`/还款 [金额|全部]` 从当前金币余额中扣除并减少未还
|
||||
贷款,借款、还款、响应记录和重试去重同样写在一个 ledger 事务内。
|
||||
|
||||
`/大富翁榜` 或 `/金币排行 [人数]` 只读取当前群已有金币账户,不会创建
|
||||
调用者账户。榜单按当前金币余额降序排列,人数范围 1~20;账户存在
|
||||
未还贷款时会在名字后标注贷款本金。
|
||||
|
||||
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
||||
alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数字顺序无关,
|
||||
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
||||
[多人彩票与游戏记录](group-lottery-0.6.3.md)。
|
||||
|
||||
21 点默认下注 50、范围 1~500。`/21点 开始 100` 表示拿 100 金币作为本局下注进场,
|
||||
等价的 OCR 紧凑形式是 `/21点开始100`。开局会扣除下注并发两张牌,
|
||||
然后根据牌面单独发送 `/21点 要牌` 或 `/21点 停牌`;开局与操作写在
|
||||
同一条消息时不会扣款,而会返回分步提示。牌局使用洗牌后的单副 52 张牌,
|
||||
牌堆、双方手牌、下注和最后操作时间持久化。A 可作为 1 或 11;
|
||||
庄家软 17 停牌。开始后只公开一张庄家牌;要牌、停牌、认输和查询都
|
||||
仅针对本人牌局。发牌、状态和结算回复以当前群内人物的配置展示名开头,
|
||||
例如 `jacky #1:你的牌 ...`;别名解析到同一人物时沿用相同展示名、
|
||||
账户、局号和游戏记录。
|
||||
自然 21 点立即结算,返还 2.5 倍下注向下取整;普通胜利返还两倍,
|
||||
平局返还下注,失败不返还。每人最多一局;牌局进行中不接受另一笔
|
||||
抽奖或 21 点开局,多人彩票报名仍可独立进行。
|
||||
|
||||
十分钟无有效游戏操作的牌局在下一次账户操作时按认输结束;查询状态
|
||||
不延长有效期。主动结束同样按认输处理,不退下注。其他人可以各自
|
||||
独立玩,不能加入或结束你的牌局。旧的群游戏保持独立。
|
||||
|
||||
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
|
||||
|
||||
Status: authoritative for the `alpha 0.6.x` extension API.
|
||||
Status: authoritative for the `beta 1.0.x` extension API.
|
||||
|
||||
## Scope
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ exception.
|
||||
|
||||
## Release Discipline
|
||||
|
||||
- Version format is `alpha X.Y.Z.MMDD`.
|
||||
- Patch changes increment `Z`; minor version changes require explicit user
|
||||
approval.
|
||||
- Version format is `beta X.Y.Z.MMDD` for current beta releases.
|
||||
- Python package versions use the matching PEP 440 beta form, such as
|
||||
`X.Y.Zb1+MMDD`.
|
||||
- Patch changes increment `Z`; minor, major, or stable-release changes require
|
||||
explicit user approval.
|
||||
- A release change must update:
|
||||
- `VERSION`;
|
||||
- root `pyproject.toml`;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Runtime Telemetry And ACK Semantics
|
||||
|
||||
Status: local instrumentation contract for `alpha 0.6.x`.
|
||||
Status: local instrumentation contract for `beta 1.0.x`.
|
||||
|
||||
## What ACK Means
|
||||
|
||||
|
||||
+10
-6
@@ -1,20 +1,24 @@
|
||||
# Versioning
|
||||
|
||||
JWeBot uses an alpha version string:
|
||||
JWeBot uses a human-readable product version string:
|
||||
|
||||
```text
|
||||
alpha X.Y.Z.MMDD
|
||||
beta X.Y.Z.MMDD
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `alpha` means the public API and deployment process may still change.
|
||||
- `beta` means the project has passed the local, staging, privacy, and
|
||||
production-safety gates expected for wider testing, while still allowing
|
||||
controlled breaking changes before a stable release.
|
||||
- `X.Y.Z` is the project version.
|
||||
- `MMDD` is the delivery date.
|
||||
- Python package versions use PEP 440 pre-release form. For beta builds,
|
||||
`beta 1.0.0.1016` maps to `1.0.0b1+1016`.
|
||||
- Normal small updates increment only the patch version, for example
|
||||
`alpha 0.0.1.1014` to `alpha 0.0.2.1014`.
|
||||
- Minor versions such as `0.1.x` require explicit user review and milestone
|
||||
approval.
|
||||
`beta 1.0.0.1016` to `beta 1.0.1.1016`.
|
||||
- Minor, major, or stable-release changes require explicit user review and
|
||||
milestone approval.
|
||||
- Local repository migration does not imply a production deployment.
|
||||
- Production services must not be changed unless the operator explicitly asks
|
||||
for deployment or runtime verification.
|
||||
|
||||
@@ -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,45 @@
|
||||
"""Generated reviewed adapter for coin-loan."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-loan'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '贷款'
|
||||
HELP_ENTRIES = ('/贷款;/还款',)
|
||||
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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,20 @@
|
||||
# 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,45 @@
|
||||
"""Generated reviewed adapter for coin-ranking."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-ranking'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '富榜'
|
||||
HELP_ENTRIES = ('/大富翁榜',)
|
||||
COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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), 4)
|
||||
|
||||
|
||||
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,45 @@
|
||||
"""Generated reviewed adapter for coin-transfer."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-transfer'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '金币转账'
|
||||
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
||||
COMMANDS = ('/转账', '/transfer')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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()
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -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,45 @@
|
||||
"""Generated reviewed adapter for cs-major-autochess."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'cs-major-autochess'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '自走棋'
|
||||
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
|
||||
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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()
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# single-player-games
|
||||
|
||||
Author: `jwebot`
|
||||
|
||||
金币游戏
|
||||
|
||||
Help:
|
||||
|
||||
- `/金币 /wallet;/游戏记录 /gamehistory`
|
||||
- `/抽奖 [金额];/draw`
|
||||
- `/彩票 开始|报名|状态|开奖|取消;/lottery`
|
||||
- `/21点 开始 [1~500];/blackjack start`
|
||||
- `/21点 要牌|停牌|状态|结束;/blackjack`
|
||||
- `/游戏帮助;/games`
|
||||
|
||||
Registered commands:
|
||||
|
||||
- `/金币`
|
||||
- `/wallet`
|
||||
- `/抽奖`
|
||||
- `/draw`
|
||||
- `/彩票`
|
||||
- `/买彩票`
|
||||
- `/lottery`
|
||||
- `/21点`
|
||||
- `/二十一点`
|
||||
- `/blackjack`
|
||||
- `/游戏记录`
|
||||
- `/个人记录`
|
||||
- `/gamehistory`
|
||||
- `/游戏帮助`
|
||||
- `/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 = ["/金币 /wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 开始 [1~500];/blackjack start", "/21点 要牌|停牌|状态|结束;/blackjack", "/游戏帮助;/games"]
|
||||
version = "1.0.0"
|
||||
api_version = "1"
|
||||
entrypoint = "single_player_games:plugin"
|
||||
type = "command"
|
||||
|
||||
[capabilities]
|
||||
commands = ["/金币", "/wallet", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏记录", "/个人记录", "/gamehistory", "/游戏帮助", "/games"]
|
||||
storage = false
|
||||
network_profiles = []
|
||||
service_profiles = ["single-player-games"]
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for single-player-games."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'single-player-games'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '金币游戏'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games')
|
||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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, ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games'))
|
||||
self.assertIn('/金币', registered)
|
||||
self.assertEqual(len(registered), 15)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# context-summary
|
||||
|
||||
Author: `official`
|
||||
|
||||
提取当前群最近100、200或300条本地文字的上下文
|
||||
|
||||
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 = "提取当前群最近100、200或300条本地文字的上下文"
|
||||
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,45 @@
|
||||
"""Generated reviewed adapter for context-summary."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'context-summary'
|
||||
AUTHOR = 'official'
|
||||
DESCRIPTION = '提取当前群最近100、200或300条本地文字的上下文'
|
||||
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
|
||||
COMMANDS = ('/上下文', '/context')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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, '提取当前群最近100、200或300条本地文字的上下文')
|
||||
self.assertEqual(HELP_ENTRIES, ('/上下文 [100|200|300];/context [100|200|300]',))
|
||||
self.assertIn('/上下文', registered)
|
||||
self.assertEqual(len(registered), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -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,45 @@
|
||||
"""Generated reviewed adapter for person-directory."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'person-directory'
|
||||
AUTHOR = 'official'
|
||||
DESCRIPTION = '查询当前群已配置人物、基本属性和别名'
|
||||
HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)
|
||||
COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
@@ -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()
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-config"
|
||||
version = "0.6.0a0+1015"
|
||||
version = "1.0.0b1+1016"
|
||||
description = "Profile-based configuration compiler for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
|
||||
@@ -36,4 +36,4 @@ __all__ = [
|
||||
"write_default_profile",
|
||||
]
|
||||
|
||||
__version__ = "0.6.0a0+1015"
|
||||
__version__ = "1.0.0b1+1016"
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-extension-host"
|
||||
version = "0.6.0a0+1015"
|
||||
version = "1.0.0b1+1016"
|
||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.0a0+1015",
|
||||
"jwebot-kernel==0.6.0a0+1015",
|
||||
"jwebot-sdk==1.0.0b1+1016",
|
||||
"jwebot-kernel==1.0.0b1+1016",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -16,4 +16,4 @@ __all__ = [
|
||||
"process_request",
|
||||
]
|
||||
|
||||
__version__ = "0.6.0a0+1015"
|
||||
__version__ = "1.0.0b1+1016"
|
||||
|
||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-kernel"
|
||||
version = "0.6.0a0+1015"
|
||||
version = "1.0.0b1+1016"
|
||||
description = "Stable routing and extension authority for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-config==0.6.0a0+1015",
|
||||
"jwebot-sdk==0.6.0a0+1015",
|
||||
"jwebot-official-extensions==0.6.0a0+1015",
|
||||
"jwebot-config==1.0.0b1+1016",
|
||||
"jwebot-sdk==1.0.0b1+1016",
|
||||
"jwebot-official-extensions==1.0.0b1+1016",
|
||||
"tomli>=2; python_version < '3.11'",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "0.6.0a0+1015"
|
||||
__version__ = "1.0.0b1+1016"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,15 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "0.6.0a0+1015"
|
||||
version = "1.0.0b1+1016"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.0a0+1015",
|
||||
"jwebot-sdk==1.0.0b1+1016",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
jwebot_official_extensions = ["data/cs_autochess/*.json"]
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
"""Reviewed JWeBot local command adapters and broker implementation."""
|
||||
|
||||
from .cs_autochess import (
|
||||
CARD_BY_ID,
|
||||
COACH_BONUSES,
|
||||
COACH_POOL,
|
||||
MAX_PLAYERS,
|
||||
MIN_PLAYERS,
|
||||
PLAYER_POOL,
|
||||
CsMajorAutochess,
|
||||
build_round_plan,
|
||||
lineup_power,
|
||||
matchup_modifier,
|
||||
parse_autochess_action,
|
||||
)
|
||||
from .descriptors import (
|
||||
DIRECT_KERNEL_COMMANDS,
|
||||
DIRECT_KERNEL_COMMAND_KINDS,
|
||||
@@ -20,6 +33,8 @@ from .local_commands import (
|
||||
LocalCommandError,
|
||||
deterministic_int,
|
||||
local_command_parts,
|
||||
pack_reply_parts,
|
||||
unpack_reply_parts,
|
||||
)
|
||||
from .runtime import (
|
||||
LocalExtensionError,
|
||||
@@ -35,6 +50,10 @@ from .steam import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CARD_BY_ID",
|
||||
"COACH_BONUSES",
|
||||
"COACH_POOL",
|
||||
"CsMajorAutochess",
|
||||
"DICE_CHALLENGE_TIMEOUT_SECONDS",
|
||||
"DEFAULT_STEAM_MIHOMO_PROXY_URL",
|
||||
"DIRECT_KERNEL_COMMANDS",
|
||||
@@ -43,8 +62,12 @@ __all__ = [
|
||||
"LOCAL_COMMAND_ALIASES",
|
||||
"LOCAL_EXTENSIONS",
|
||||
"LOCAL_GAME_TIMEOUT_SECONDS",
|
||||
"MAX_PLAYERS",
|
||||
"MAX_RPS_PLAYERS",
|
||||
"MIN_PLAYERS",
|
||||
"OFFICIAL_AUTHOR",
|
||||
"PLAYER_POOL",
|
||||
"build_round_plan",
|
||||
"KernelCommandDescriptor",
|
||||
"LocalExtensionDescriptor",
|
||||
"LocalExtensionError",
|
||||
@@ -58,8 +81,13 @@ __all__ = [
|
||||
"deterministic_int",
|
||||
"local_command_parts",
|
||||
"local_extension_command_token",
|
||||
"lineup_power",
|
||||
"matchup_modifier",
|
||||
"pack_reply_parts",
|
||||
"parse_autochess_action",
|
||||
"unpack_reply_parts",
|
||||
"extension_for_kind",
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "0.6.0a0+1015"
|
||||
__version__ = "1.0.0b1+1016"
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for coin-loan."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-loan'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '贷款'
|
||||
HELP_ENTRIES = ('/贷款;/还款',)
|
||||
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for coin-ranking."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-ranking'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '富榜'
|
||||
HELP_ENTRIES = ('/大富翁榜',)
|
||||
COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for coin-transfer."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-transfer'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '金币转账'
|
||||
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
||||
COMMANDS = ('/转账', '/transfer')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for context-summary."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'context-summary'
|
||||
AUTHOR = 'official'
|
||||
DESCRIPTION = '提取当前群最近100、200或300条本地文字的上下文'
|
||||
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
|
||||
COMMANDS = ('/上下文', '/context')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for cs-major-autochess."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'cs-major-autochess'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '自走棋'
|
||||
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
|
||||
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for person-directory."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'person-directory'
|
||||
AUTHOR = 'official'
|
||||
DESCRIPTION = '查询当前群已配置人物、基本属性和别名'
|
||||
HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)
|
||||
COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for single-player-games."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'single-player-games'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '金币游戏'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games')
|
||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
PROFILE,
|
||||
operation="execute",
|
||||
payload={
|
||||
"command": command,
|
||||
"arguments": arguments,
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
|
||||
def plugin(context: SingleGroupContext) -> None:
|
||||
for command in COMMANDS:
|
||||
context.commands.register(command, _handler(context, command))
|
||||
+8
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts and all(
|
||||
isinstance(item, str) and item.strip() for item in texts
|
||||
):
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata={"local_extension": PROFILE, "author": AUTHOR},
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("local service returned invalid text")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+185
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"coaches": [
|
||||
{
|
||||
"bonus": 9,
|
||||
"clutch": 88,
|
||||
"firepower": 78,
|
||||
"id": "zonic",
|
||||
"level": 5,
|
||||
"name": "zonic",
|
||||
"stability": 92,
|
||||
"style": "冠军体系"
|
||||
},
|
||||
{
|
||||
"bonus": 9,
|
||||
"clutch": 84,
|
||||
"firepower": 72,
|
||||
"id": "blade",
|
||||
"level": 5,
|
||||
"name": "B1ad3",
|
||||
"stability": 96,
|
||||
"style": "结构控图"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 80,
|
||||
"firepower": 86,
|
||||
"id": "xtqzzz",
|
||||
"level": 4,
|
||||
"name": "XTQZZZ",
|
||||
"stability": 78,
|
||||
"style": "进攻提速"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 90,
|
||||
"firepower": 76,
|
||||
"id": "neo",
|
||||
"level": 4,
|
||||
"name": "NEO",
|
||||
"stability": 82,
|
||||
"style": "残局纪律"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 82,
|
||||
"firepower": 80,
|
||||
"id": "saw",
|
||||
"level": 3,
|
||||
"name": "sAw",
|
||||
"stability": 80,
|
||||
"style": "临场调整"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 78,
|
||||
"firepower": 74,
|
||||
"id": "robban",
|
||||
"level": 3,
|
||||
"name": "RobbaN",
|
||||
"stability": 86,
|
||||
"style": "更衣室管理"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 76,
|
||||
"firepower": 84,
|
||||
"id": "kane",
|
||||
"level": 2,
|
||||
"name": "kane",
|
||||
"stability": 72,
|
||||
"style": "明星激活"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 74,
|
||||
"firepower": 70,
|
||||
"id": "trace",
|
||||
"level": 2,
|
||||
"name": "trace",
|
||||
"stability": 86,
|
||||
"style": "防守复盘"
|
||||
},
|
||||
{
|
||||
"bonus": 5,
|
||||
"clutch": 72,
|
||||
"firepower": 76,
|
||||
"id": "ash",
|
||||
"level": 1,
|
||||
"name": "ash",
|
||||
"stability": 74,
|
||||
"style": "新人培养"
|
||||
},
|
||||
{
|
||||
"bonus": 5,
|
||||
"clutch": 70,
|
||||
"firepower": 70,
|
||||
"id": "mithr",
|
||||
"level": 1,
|
||||
"name": "mithR",
|
||||
"stability": 78,
|
||||
"style": "基础纪律"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 82,
|
||||
"firepower": 74,
|
||||
"id": "kuben",
|
||||
"level": 4,
|
||||
"name": "kuben",
|
||||
"stability": 88,
|
||||
"style": "慢控默认"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 78,
|
||||
"firepower": 72,
|
||||
"id": "groove",
|
||||
"level": 3,
|
||||
"name": "groove",
|
||||
"stability": 84,
|
||||
"style": "学院体系"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 80,
|
||||
"firepower": 86,
|
||||
"id": "guerri",
|
||||
"level": 4,
|
||||
"name": "guerri",
|
||||
"stability": 76,
|
||||
"style": "巴西爆弹"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 82,
|
||||
"firepower": 76,
|
||||
"id": "xizt",
|
||||
"level": 4,
|
||||
"name": "Xizt",
|
||||
"stability": 86,
|
||||
"style": "默认纪律"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 78,
|
||||
"firepower": 80,
|
||||
"id": "sycrone",
|
||||
"level": 3,
|
||||
"name": "sycrone",
|
||||
"stability": 80,
|
||||
"style": "青年提拔"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 80,
|
||||
"firepower": 82,
|
||||
"id": "malek",
|
||||
"level": 3,
|
||||
"name": "maLeK",
|
||||
"stability": 78,
|
||||
"style": "明星平衡"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 76,
|
||||
"firepower": 78,
|
||||
"id": "peacemaker",
|
||||
"level": 2,
|
||||
"name": "peacemaker",
|
||||
"stability": 74,
|
||||
"style": "临时救火"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 74,
|
||||
"firepower": 72,
|
||||
"id": "kassad",
|
||||
"level": 2,
|
||||
"name": "kassad",
|
||||
"stability": 82,
|
||||
"style": "纪律重建"
|
||||
}
|
||||
],
|
||||
"schema_version": 1
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"fragments": {
|
||||
"challenges": [
|
||||
"媒体日问题很尖锐,队伍需要在镜头前保持状态。",
|
||||
"赞助商临时加码,要求队伍用指定战术打出观赏性。",
|
||||
"主办方安排了签名会,状态管理成了额外考验。",
|
||||
"训练馆网络短暂波动,队伍必须快速适应备用机器。",
|
||||
"赛前采访拖长,指挥需要把注意力重新拉回比赛。",
|
||||
"赞助商想看激进打法,但稳住节奏的人会拿到更高回报。",
|
||||
"媒体日不是战斗回合,却会影响队伍的运营资源。",
|
||||
"队伍如果阵容完整,就更容易把这次插曲处理成奖金。",
|
||||
"这次挑战只出现一次,处理完就回到小组赛主线。",
|
||||
"主舞台彩排占用了热身时间,教练的管理能力开始生效。",
|
||||
"赞助商要求展示新战术,选错方向也不会淘汰,只会少拿金币。",
|
||||
"现场采访提到了前一场失利,队员心态需要稳住。",
|
||||
"这类挑战不扣生命,目的只是给运营阶段增加变数。",
|
||||
"处理好这次插曲的队伍,会在下一场前多一点经济余地。"
|
||||
],
|
||||
"closers": [
|
||||
"{team} 没有给翻盘窗口,最后一波回防处理得很完整。",
|
||||
"{team} 在赛点局选择最熟悉的默认,用纪律结束地图。",
|
||||
"{team} 最后一回合没有贪枪,稳稳把人数优势转成胜势。",
|
||||
"{team} 的收官很干净,地图尾声没有出现多余失误。",
|
||||
"{team} 用一波双拉打开包点,最后比分定格。",
|
||||
"{team} 把比赛拖进自己舒服的节奏,收尾阶段非常稳。",
|
||||
"{team} 赛点局的道具层次很清楚,对手没有舒服的回防路线。",
|
||||
"{team} 终于等到对手前压,抓住这个窗口终结地图。",
|
||||
"{team} 收下最后一分时没有冒进,这就是强队式结尾。",
|
||||
"{team} 最后一枪来得很快,但前面的信息铺垫已经足够。",
|
||||
"{team} 在关键经济局之后顺势收掉地图。",
|
||||
"{team} 先控退路再进包点,结尾没有留下残局悬念。",
|
||||
"{team} 把上半场积累的优势守到最后。",
|
||||
"{team} 用一波慢夹收官,比分看起来合理,过程更像真实比赛。",
|
||||
"{team} 没让对手连追,赛点局直接把节奏抢回来。",
|
||||
"{team} 的最后一分赢在补枪,枪线没有断。",
|
||||
"{team} 收官回合把每个角落都清干净,耐心给了回报。",
|
||||
"{team} 把悬念留到最后一刻,但关键枪没有手软。"
|
||||
],
|
||||
"clutches": [
|
||||
"{player} 留到最后一个残局,靠一颗烟把拆包时间偷了出来。",
|
||||
"{player} 没有急着补枪,等脚步声靠近后才横拉收掉残局。",
|
||||
"{player} 在 1v2 里先断退路,再回头处理包点。",
|
||||
"{player} 的残局处理很冷,假拆骗出第一枪后直接反清。",
|
||||
"{player} 被逼到死角,但一颗反弹闪把局面重新打开。",
|
||||
"{player} 没有追击残血,而是先保住雷包,判断非常老练。",
|
||||
"{player} 关键回合双杀,把本来要崩的经济线救回来。",
|
||||
"{player} 在读秒阶段完成补枪,队友的拉扯给得刚刚好。",
|
||||
"{player} 先拿信息再开枪,这个残局赢在耐心。",
|
||||
"{player} 硬吃白光完成换人,比分没有被对手咬住。",
|
||||
"{player} 从连接位摸出,正好打掉回防节奏。",
|
||||
"{player} 这次不追求集锦,只把枪线封死等队友到位。",
|
||||
"{player} 的狙击枪没有空枪,关键首杀让整回合变得简单。",
|
||||
"{player} 残血仍然敢看第一枪位,队伍因此拿到赛点。",
|
||||
"{player} 拿下残局后没有庆祝,马上提醒队友保枪经济。",
|
||||
"{player} 靠脚步假信息骗出转点,最后一枪很稳。",
|
||||
"{player} 在包点里连续换位,对手始终没能定位。",
|
||||
"{player} 把闪光背身吃完后仍然补到第二枪,这是关键转折。",
|
||||
"{player} 一个人拖住整条回防路线,为队伍争到拆包窗口。",
|
||||
"{player} 最后选择不 peek,让时间替自己赢下这一分。"
|
||||
],
|
||||
"economy": [
|
||||
"{team} 赢下这一分后可以完整起枪,下一回合选择会更多。",
|
||||
"{team} 输掉长枪局后经济见底,可能要交出一回合调整空间。",
|
||||
"{team} 没有强行追梦,选择保枪把下一回合留住。",
|
||||
"{team} 的连败补偿到位,下一回合有机会靠道具质量反打。",
|
||||
"{team} 这回合缴下两把枪,经济收益比比分更重要。",
|
||||
"{team} 虽然丢分,但保下主狙,下一回合还有破局点。",
|
||||
"{team} 的半甲手枪没有打成,但至少逼出了对手不少道具。",
|
||||
"{team} 经济优势开始显现,可以更大胆地争夺第一枪位。",
|
||||
"{team} 如果下一分再丢,就必须进入半起调整。",
|
||||
"{team} 这一波选择全甲全弹,明显是要在中期一次打穿。",
|
||||
"{team} 保下来的两把枪让下回合不至于完全被动。",
|
||||
"{team} 连续换枪成功,把对手的存款一点点磨掉。",
|
||||
"{team} 用低成本回合换到下包,经济线还没有断。",
|
||||
"{team} 这波赢的不只是分数,还有下一回合的主动权。",
|
||||
"{team} 被重置经济后节奏放慢,先求稳住半场尾声。",
|
||||
"{team} 靠残局胜利拿到额外空间,可以考虑继续 D 牌补强。"
|
||||
],
|
||||
"openings": [
|
||||
"{team} 开局没有急着提速,先把默认控图铺开。",
|
||||
"{team} 第一波提速很坚决,外场道具落点干净。",
|
||||
"{team} 在手枪局后选择慢摸,试图逼出对手的前压信息。",
|
||||
"{team} 的狙击手早早架住第一交火点,节奏被压得很低。",
|
||||
"{team} 连续用两颗闪光清掉近点,进攻面一下打开。",
|
||||
"{team} 防守开局站位很深,把回防路线留得很完整。",
|
||||
"{team} 先拿地图控制权,再把包点压力一点点推上去。",
|
||||
"{team} 开局没有赌点,靠信息互换慢慢读阵型。",
|
||||
"{team} 在第一枪位赢下后没有贪,马上回收人数优势。",
|
||||
"{team} 用一波假打骗出轮转,第二时间才开始真正爆弹。",
|
||||
"{team} 选择保守开局,宁愿少拿信息也不送首杀。",
|
||||
"{team} 的指挥把默认拉得很散,让对手很难判断重心。",
|
||||
"{team} 手枪局道具很少,但站位互补做得很扎实。",
|
||||
"{team} 没有被前几回合影响,依旧按自己的节奏展开。",
|
||||
"{team} 的第一套进攻明显针对了对手弱侧。",
|
||||
"{team} 在经济局也保留了两颗关键闪,准备打后点反清。",
|
||||
"{team} 上来就抢中路,想用主动权换后续转点空间。",
|
||||
"{team} 先把狙击枪放在安全位,围绕它慢慢组织推进。"
|
||||
],
|
||||
"pauses": [
|
||||
"比分被拉开后,双方都慢了半拍,像是在重新读这张图。",
|
||||
"这里适合停一下,刚才几回合的经济差已经开始影响下一枪。",
|
||||
"暂停回来之后,防守方明显把第一接触点后撤了半格。",
|
||||
"连续交火后节奏突然变慢,双方都在等对方先犯错。",
|
||||
"这一段没有夸张集锦,但每一颗道具都在改变下一回合。",
|
||||
"场面安静了几秒,真正决定胜负的是下一次首杀。",
|
||||
"队伍语音应该都在压节奏,没人愿意在这里送掉经济。",
|
||||
"这不是简单的比分变化,战术重心已经从爆弹转到控图。",
|
||||
"现场进入短暂拉扯,地图控制权一点点被重新分配。",
|
||||
"两边都知道下一分很贵,所以这回合开局格外谨慎。",
|
||||
"这一小段空白很重要,强队往往就是在这里调整默认。",
|
||||
"比赛没有马上进入下一波交火,指挥正在重新排优先级。",
|
||||
"刚才的连分让对手不得不换站位,防线开始出现试探。",
|
||||
"经济压力让双方都少了一点容错,下一波道具会很关键。",
|
||||
"这里没有急躁的前压,只有一点点互相试探。",
|
||||
"暂停不是为了拖时间,而是在给下一套进攻换答案。",
|
||||
"双方都把节奏压住,地图像被重新归零。",
|
||||
"这一分之前的几秒钟很长,所有人都在等第一个脚步声。",
|
||||
"场上没有无意义的 peek,每个人都在守自己的触发条件。",
|
||||
"这段过渡让比赛更像真正的 BO 系列赛,而不是单纯掷骰子。"
|
||||
],
|
||||
"runs": [
|
||||
"{team} 连拿 {count} 分之后,现场短暂安静下来。",
|
||||
"{team} 把经济滚起来后,连续 {count} 个回合没有给对手完整开枪空间。",
|
||||
"{team} 这一波 {count} 连分不是纯枪法碾压,而是每回合都换到正确信息。",
|
||||
"{team} 的节奏忽快忽慢,{count} 个回合里每次进点方式都不一样。",
|
||||
"{team} 靠补枪链吃下 {count} 分,队伍语音应该已经很热。",
|
||||
"{team} 的中期处理开始成型,连续 {count} 分把对手钱打空。",
|
||||
"{team} 没有急着结束比赛,而是用 {count} 连分把地图控制牢牢握住。",
|
||||
"{team} 这段 {count} 连分很关键,尤其是第二个残局稳定了军心。",
|
||||
"{team} 在暂停之后突然提速,直接打出 {count} 连分。",
|
||||
"{team} 先抓前压,再抓回防,{count} 个回合把读秒处理做满。",
|
||||
"{team} 的防守交叉火力越站越舒服,{count} 连分让比分拉开。",
|
||||
"{team} 把对手的道具节奏读透,连续 {count} 分都避开了主防区。",
|
||||
"{team} 经济起飞后没有乱买,{count} 连分中枪线一直保持完整。",
|
||||
"{team} 在被首杀的回合仍然能翻,{count} 连分含金量很高。",
|
||||
"{team} 的默认推进开始收利息,{count} 个回合每次都能换到便宜。",
|
||||
"{team} 靠一波小连胜把压力交回去,{count} 分之后对手必须叫停。",
|
||||
"{team} 的二楼夹击反复奏效,{count} 连分让对手开始犹豫。",
|
||||
"{team} 连续 {count} 回合把残局拖到优势人数,后点纪律很稳。",
|
||||
"{team} 打出 {count} 连分后节奏放慢,像是在等对手自己露破绽。",
|
||||
"{team} 这波 {count} 连分让地图气质变了,所有枪位都开始往前压。"
|
||||
],
|
||||
"training_hooks": [
|
||||
"训练赛不计入小组胜负,但会发金币,方便后续刷新商店。",
|
||||
"这轮训练更像实战热身,每队都会遇到同场参赛对手。",
|
||||
"训练赛重点不是淘汰谁,而是检验当前阵容能不能打完整张图。",
|
||||
"这轮结束后可以 D 牌、锁店或调首发,下一轮再检验成果。",
|
||||
"AI 战队也会参与训练,对局结果会写入档案。",
|
||||
"如果阵容缺少指挥,训练赛的中期回合会更容易失控。",
|
||||
"如果你想追羁绊,现在是最适合刷新商店的窗口。",
|
||||
"训练赛的金币很重要,别把每次刷新都当成免费试错。",
|
||||
"这一阶段没有生命扣除,重点是运营和阵容成型。",
|
||||
"训练赛的地图会轮换,不会一直用同一个固定比分模板。",
|
||||
"每轮训练结束后,真人都需要发送 /结束回合 才会进入下一轮。",
|
||||
"训练赛会按轮次播报,避免一次性刷出一大串结果。",
|
||||
"如果出现轮空,会安排本地陪练队补一张 BO1。",
|
||||
"训练阶段适合观察战术克制,火力、稳定、残局都会影响回合走向。",
|
||||
"连胜会带来金币,但失败也有保底,保证测试局能继续运营。",
|
||||
"这部分是纯文字自走棋的节奏层,不再是单条目标数值判定。"
|
||||
]
|
||||
},
|
||||
"schema_version": 1
|
||||
}
|
||||
+1852
File diff suppressed because it is too large
Load Diff
+309
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"ai": {
|
||||
"power_multiplier": 0.82,
|
||||
"primary_display_name": "AI 战队",
|
||||
"primary_person_key": "__local_ai_team__",
|
||||
"secondary_display_name": "AI 战队 B",
|
||||
"secondary_person_key": "__local_ai_team_2__"
|
||||
},
|
||||
"battle": {
|
||||
"damage": {
|
||||
"bo5_sweep": 18,
|
||||
"non_sweep": 10,
|
||||
"sweep": 15
|
||||
},
|
||||
"maps": [
|
||||
"Mirage",
|
||||
"Inferno",
|
||||
"Nuke",
|
||||
"Ancient",
|
||||
"Dust2",
|
||||
"Anubis",
|
||||
"Overpass"
|
||||
],
|
||||
"match_best_of": {
|
||||
"final": 5,
|
||||
"group": 3,
|
||||
"semifinal": 5,
|
||||
"training": 1
|
||||
},
|
||||
"mr_rounds": 30,
|
||||
"overtime_safety_cap": 72,
|
||||
"overtime_segment_rounds": 6,
|
||||
"overtime_start_score": 15,
|
||||
"overtime_target_score": 19,
|
||||
"overtime_win_by": 2,
|
||||
"round_probability": {
|
||||
"base": 0.5,
|
||||
"first_half_side_swing": 0.018,
|
||||
"jitter_center": 350,
|
||||
"jitter_divisor": 10000,
|
||||
"jitter_mod": 701,
|
||||
"max": 0.68,
|
||||
"min": 0.32,
|
||||
"power_edge_cap": 0.16,
|
||||
"power_edge_scale": 0.3,
|
||||
"pressure_cap": 0.04,
|
||||
"pressure_scale": 0.004,
|
||||
"second_half_side_swing": -0.012
|
||||
},
|
||||
"score_bands": [
|
||||
{
|
||||
"label": "压制",
|
||||
"max_loser_score": 6
|
||||
},
|
||||
{
|
||||
"label": "优势",
|
||||
"max_loser_score": 10
|
||||
},
|
||||
{
|
||||
"label": "小胜",
|
||||
"max_loser_score": 13
|
||||
},
|
||||
{
|
||||
"label": "险胜",
|
||||
"max_loser_score": 999
|
||||
}
|
||||
],
|
||||
"target_score": 16
|
||||
},
|
||||
"coach": {
|
||||
"fallback_bonuses": {
|
||||
"1": 2,
|
||||
"2": 4,
|
||||
"3": 6,
|
||||
"4": 8,
|
||||
"5": 10
|
||||
},
|
||||
"upgrade_costs": {
|
||||
"1": 6,
|
||||
"2": 10,
|
||||
"3": 16,
|
||||
"4": 24
|
||||
}
|
||||
},
|
||||
"draft": {
|
||||
"coach_choice_count": 10,
|
||||
"coach_round": 1,
|
||||
"later_round_preferred_costs": [
|
||||
3,
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
5
|
||||
],
|
||||
"player_choice_count": 6,
|
||||
"player_copy_count": 3,
|
||||
"round_two_preferred_costs": [
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"rounds": 3
|
||||
},
|
||||
"economy": {
|
||||
"round_income": {
|
||||
"base": 5,
|
||||
"interest_cap": 5,
|
||||
"interest_divisor": 10
|
||||
},
|
||||
"series_rewards": {
|
||||
"group_win_gold": 5,
|
||||
"playoff_win_gold": 6
|
||||
},
|
||||
"starting_gold": 10,
|
||||
"starting_hp": 100,
|
||||
"training_rewards": {
|
||||
"bye_gold": 2,
|
||||
"loss_gold": 1,
|
||||
"win_gold": 3
|
||||
}
|
||||
},
|
||||
"pve_events": {
|
||||
"pve_media": {
|
||||
"loss_gold": 3,
|
||||
"name": "媒体日挑战",
|
||||
"target": 335,
|
||||
"win_gold": 6
|
||||
},
|
||||
"pve_sponsor": {
|
||||
"loss_gold": 4,
|
||||
"name": "赞助挑战",
|
||||
"target": 395,
|
||||
"win_gold": 8
|
||||
},
|
||||
"pve_training": {
|
||||
"loss_gold": 2,
|
||||
"name": "训练赛",
|
||||
"target": 275,
|
||||
"win_gold": 4
|
||||
}
|
||||
},
|
||||
"roster": {
|
||||
"bench_limit": 3,
|
||||
"nation_synergy": {
|
||||
"max_sources": 1,
|
||||
"thresholds": [
|
||||
{
|
||||
"bonus": 18,
|
||||
"min_count": 5
|
||||
},
|
||||
{
|
||||
"bonus": 12,
|
||||
"min_count": 4
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"min_count": 3
|
||||
},
|
||||
{
|
||||
"bonus": 3,
|
||||
"min_count": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"star_multipliers": {
|
||||
"1": 1.0,
|
||||
"2": 1.35,
|
||||
"3": 1.8
|
||||
},
|
||||
"starter_count": 5,
|
||||
"team_synergy": {
|
||||
"max_sources": 2,
|
||||
"thresholds": [
|
||||
{
|
||||
"bonus": 22,
|
||||
"min_count": 5
|
||||
},
|
||||
{
|
||||
"bonus": 15,
|
||||
"min_count": 4
|
||||
},
|
||||
{
|
||||
"bonus": 9,
|
||||
"min_count": 3
|
||||
},
|
||||
{
|
||||
"bonus": 4,
|
||||
"min_count": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit_power_weights": {
|
||||
"clutch": 0.25,
|
||||
"firepower": 0.45,
|
||||
"leadership_scale": 0.12,
|
||||
"leadership_threshold": 70,
|
||||
"stability": 0.3
|
||||
}
|
||||
},
|
||||
"schema_version": 1,
|
||||
"shop": {
|
||||
"cost_pool_copies": {
|
||||
"1": 18,
|
||||
"2": 15,
|
||||
"3": 12,
|
||||
"4": 9,
|
||||
"5": 7
|
||||
},
|
||||
"odds": {
|
||||
"1": {
|
||||
"1": 70,
|
||||
"2": 25,
|
||||
"3": 5,
|
||||
"4": 0,
|
||||
"5": 0
|
||||
},
|
||||
"2": {
|
||||
"1": 50,
|
||||
"2": 30,
|
||||
"3": 18,
|
||||
"4": 2,
|
||||
"5": 0
|
||||
},
|
||||
"3": {
|
||||
"1": 30,
|
||||
"2": 35,
|
||||
"3": 25,
|
||||
"4": 9,
|
||||
"5": 1
|
||||
},
|
||||
"4": {
|
||||
"1": 15,
|
||||
"2": 30,
|
||||
"3": 30,
|
||||
"4": 20,
|
||||
"5": 5
|
||||
},
|
||||
"5": {
|
||||
"1": 5,
|
||||
"2": 20,
|
||||
"3": 30,
|
||||
"4": 30,
|
||||
"5": 15
|
||||
}
|
||||
},
|
||||
"refresh_cost": 2,
|
||||
"size": 6
|
||||
},
|
||||
"special_events": {
|
||||
"media": {
|
||||
"loss_gold": 1,
|
||||
"name": "媒体日挑战",
|
||||
"target": 320,
|
||||
"win_gold": 4
|
||||
},
|
||||
"sponsor": {
|
||||
"loss_gold": 1,
|
||||
"name": "赞助挑战",
|
||||
"target": 340,
|
||||
"win_gold": 4
|
||||
}
|
||||
},
|
||||
"tactics": {
|
||||
"attribute_counters": [
|
||||
[
|
||||
"稳定",
|
||||
"火力"
|
||||
],
|
||||
[
|
||||
"火力",
|
||||
"残局"
|
||||
],
|
||||
[
|
||||
"残局",
|
||||
"稳定"
|
||||
]
|
||||
],
|
||||
"axis_percent_cap": 8.0,
|
||||
"axis_percent_divisor": 12.5,
|
||||
"counters": {
|
||||
"残局": "稳定",
|
||||
"火力": "残局",
|
||||
"稳定": "火力"
|
||||
},
|
||||
"names": [
|
||||
"火力",
|
||||
"稳定",
|
||||
"残局"
|
||||
],
|
||||
"role_counters": {
|
||||
"指挥": "狙击手",
|
||||
"步枪手": "指挥",
|
||||
"狙击手": "步枪手"
|
||||
},
|
||||
"role_percent_cap": 5.0,
|
||||
"role_percent_scale": 0.8,
|
||||
"selected_axis_multiplier": 1.08,
|
||||
"tactic_counter_percent": 6.0,
|
||||
"total_percent_cap": 15.0
|
||||
},
|
||||
"tournament": {
|
||||
"event_title_template": "第 {season} 届 IEM 盐田 Major",
|
||||
"host_name": "Taffy",
|
||||
"max_players": 8,
|
||||
"min_players": 2
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,55 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"debug": "telemetry_debug",
|
||||
"余额": "deepseek_balance",
|
||||
"steam": "steam_friends",
|
||||
"上下文": "context_summary",
|
||||
"context": "context_summary",
|
||||
"金币": "wallet",
|
||||
"wallet": "wallet",
|
||||
"金币排行": "coin_ranking",
|
||||
"金币排行榜": "coin_ranking",
|
||||
"大富翁榜": "coin_ranking",
|
||||
"richlist": "coin_ranking",
|
||||
"转账": "transfer",
|
||||
"transfer": "transfer",
|
||||
"贷款": "loan",
|
||||
"借款": "loan",
|
||||
"loan": "loan",
|
||||
"borrow": "loan",
|
||||
"还款": "repay",
|
||||
"repay": "repay",
|
||||
"抽奖": "prize_draw",
|
||||
"draw": "prize_draw",
|
||||
"彩票": "lottery",
|
||||
"买彩票": "lottery",
|
||||
"lottery": "lottery",
|
||||
"21点": "blackjack",
|
||||
"二十一点": "blackjack",
|
||||
"blackjack": "blackjack",
|
||||
"游戏记录": "game_history",
|
||||
"个人记录": "game_history",
|
||||
"gamehistory": "game_history",
|
||||
"游戏帮助": "game_rules",
|
||||
"games": "game_rules",
|
||||
"人物列表": "person_directory",
|
||||
"别名列表": "person_directory",
|
||||
"people": "person_directory",
|
||||
"人物": "person_info",
|
||||
"个人属性": "person_info",
|
||||
"person": "person_info",
|
||||
"自走棋": "cs_autochess",
|
||||
"cs自走棋": "cs_autochess",
|
||||
"autochess": "cs_autochess",
|
||||
"选": "cs_autochess",
|
||||
"刷新": "cs_autochess",
|
||||
"锁定": "cs_autochess",
|
||||
"战术": "cs_autochess",
|
||||
"升级教练": "cs_autochess",
|
||||
"上阵": "cs_autochess",
|
||||
"下阵": "cs_autochess",
|
||||
"出售": "cs_autochess",
|
||||
"阵容": "cs_autochess",
|
||||
"赛程": "cs_autochess",
|
||||
"结束回合": "cs_autochess",
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +140,71 @@ DIRECT_KERNEL_COMMAND_KINDS = frozenset(
|
||||
)
|
||||
|
||||
LOCAL_EXTENSIONS = (
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="context-summary",
|
||||
author=OFFICIAL_AUTHOR,
|
||||
description="提取当前群最近100、200或300条本地文字的上下文",
|
||||
help_entries=("/上下文 [100|200|300];/context [100|200|300]",),
|
||||
command_kinds=("context_summary",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="single-player-games",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="金币游戏",
|
||||
help_entries=(
|
||||
"/金币 /wallet;/游戏记录 /gamehistory",
|
||||
"/抽奖 [金额];/draw",
|
||||
"/彩票 开始|报名|状态|开奖|取消;/lottery",
|
||||
"/21点 开始 [1~500];/blackjack start",
|
||||
"/21点 要牌|停牌|状态|结束;/blackjack",
|
||||
"/游戏帮助;/games",
|
||||
),
|
||||
command_kinds=(
|
||||
"wallet",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="coin-transfer",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="金币转账",
|
||||
help_entries=("/转账 对方 金额;/transfer",),
|
||||
command_kinds=("transfer",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="coin-ranking",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="富榜",
|
||||
help_entries=("/大富翁榜",),
|
||||
command_kinds=("coin_ranking",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="coin-loan",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="贷款",
|
||||
help_entries=("/贷款;/还款",),
|
||||
command_kinds=("loan", "repay"),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="cs-major-autochess",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="自走棋",
|
||||
help_entries=(
|
||||
"/自走棋 开始|报名|选",
|
||||
),
|
||||
command_kinds=("cs_autochess",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="person-directory",
|
||||
author=OFFICIAL_AUTHOR,
|
||||
description="查询当前群已配置人物、基本属性和别名",
|
||||
help_entries=("/人物列表 [页码] /people;/人物 [名称或别名] /person",),
|
||||
command_kinds=("person_directory", "person_info"),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="person-memory",
|
||||
author=OFFICIAL_AUTHOR,
|
||||
@@ -237,7 +351,6 @@ def render_feature_list() -> str:
|
||||
)
|
||||
for descriptor in LOCAL_EXTENSIONS:
|
||||
lines.append(
|
||||
f"[{descriptor.author}/{descriptor.extension_id}] "
|
||||
f"{descriptor.description}:"
|
||||
+ ";".join(descriptor.help_entries)
|
||||
)
|
||||
|
||||
+200
-1
@@ -9,11 +9,15 @@ import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from .cs_autochess import CsMajorAutochess
|
||||
from .descriptors import LOCAL_COMMAND_ALIASES, render_feature_list
|
||||
from .player_games import PlayerGames
|
||||
|
||||
|
||||
LOCAL_GAME_TIMEOUT_SECONDS = 10 * 60
|
||||
@@ -22,6 +26,8 @@ MAX_RPS_PLAYERS = 8
|
||||
BEIJING_TIMEZONE = ZoneInfo("Asia/Shanghai")
|
||||
DICE_PATTERN = re.compile(r"^(?:(\d{1,2})[dD](\d{1,3}))?$")
|
||||
RANDOM_NUMBER_PATTERN = re.compile(r"^(-?\d{1,6})\s+(-?\d{1,6})$")
|
||||
MULTI_REPLY_MARKER = "\x1eJWEBOT_MULTI_REPLY_V1\x1e"
|
||||
CHANGELOG_HEADING = re.compile(r"^##\s+(.+?)(?:\s+-\s+(\d{4}-\d{2}-\d{2}))?\s*$")
|
||||
|
||||
|
||||
class LocalCommandError(RuntimeError):
|
||||
@@ -50,6 +56,36 @@ def deterministic_int(seed: str, lower: int, upper: int) -> int:
|
||||
return lower + value % (upper - lower + 1)
|
||||
|
||||
|
||||
def pack_reply_parts(parts: list[str] | tuple[str, ...]) -> str:
|
||||
cleaned = [str(part).strip() for part in parts if str(part).strip()]
|
||||
if not cleaned:
|
||||
raise LocalCommandError("本地命令没有可发送回复")
|
||||
if len(cleaned) == 1:
|
||||
return cleaned[0]
|
||||
return MULTI_REPLY_MARKER + json.dumps(
|
||||
cleaned,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
|
||||
def unpack_reply_parts(value: object) -> list[str] | None:
|
||||
text = str(value or "")
|
||||
if not text.startswith(MULTI_REPLY_MARKER):
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(text[len(MULTI_REPLY_MARKER) :])
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
if (
|
||||
not isinstance(payload, list)
|
||||
or not payload
|
||||
or any(not isinstance(item, str) or not item.strip() for item in payload)
|
||||
):
|
||||
return None
|
||||
return [item.strip() for item in payload]
|
||||
|
||||
|
||||
def local_command_parts(question: str) -> tuple[str, str]:
|
||||
normalized = question.lstrip()
|
||||
if not normalized.startswith("/"):
|
||||
@@ -104,6 +140,8 @@ class LocalCommandEngine:
|
||||
self.person_display_label = person_display_label
|
||||
self.profile_fact_is_safe = profile_fact_is_safe
|
||||
self.profile_expiry = profile_expiry
|
||||
self.players = PlayerGames(ledger, person_display_label)
|
||||
self.autochess = CsMajorAutochess(ledger)
|
||||
|
||||
def handle(self, row: dict[str, Any], event: dict[str, Any]) -> str:
|
||||
question = str(row["question"])
|
||||
@@ -112,6 +150,29 @@ class LocalCommandEngine:
|
||||
raise LocalCommandError("本地命令不存在,发 /功能列表 查看可用本地功能")
|
||||
if kind == "feature_list":
|
||||
return self.reply_cleaner(self.feature_list())
|
||||
if kind == "context_summary":
|
||||
return self.reply_cleaner(self.context_summary(row, arguments))
|
||||
if kind in {"person_directory", "person_info"}:
|
||||
return self.reply_cleaner(self.person_index(row, kind, arguments))
|
||||
if kind in {
|
||||
"wallet",
|
||||
"coin_ranking",
|
||||
"transfer",
|
||||
"loan",
|
||||
"repay",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
}:
|
||||
return self.reply_cleaner(self.players.handle(row, kind, arguments))
|
||||
if kind == "cs_autochess":
|
||||
parts = [
|
||||
self.reply_cleaner(part)
|
||||
for part in self.autochess.handle_parts(row)
|
||||
]
|
||||
return pack_reply_parts(parts)
|
||||
if kind == "dice":
|
||||
return self.reply_cleaner(self.dice(row, arguments))
|
||||
if kind == "dice_challenge":
|
||||
@@ -229,6 +290,82 @@ class LocalCommandEngine:
|
||||
)
|
||||
return f"本地记录命中“{query}”:{listing}"
|
||||
|
||||
def context_summary(self, row: dict[str, Any], arguments: str) -> str:
|
||||
arguments = arguments.strip()
|
||||
if arguments.strip() not in {"", "100", "200", "300"}:
|
||||
return "用法:/上下文 100|200|300(/context),默认100条"
|
||||
limit = int(arguments or 100)
|
||||
self.ledger.bootstrap_repeat_history()
|
||||
records = self.ledger.recent_context_messages(
|
||||
str(row["group_fingerprint"]), limit,
|
||||
exclude_event_id=str(row["event_id"]))
|
||||
if not records:
|
||||
return "当前群还没有可总结的本地文字记录"
|
||||
# Local extractive summary: rank representative messages, retaining chronology.
|
||||
stop = {"这个", "那个", "就是", "然后", "现在", "我们", "你们", "可以",
|
||||
"不是", "一个", "什么", "怎么", "感觉", "还是", "已经", "没有",
|
||||
"the", "and", "you", "that", "this", "jwebot"}
|
||||
texts = [re.sub(r"^@\S+\s+", "", normalize_text(record["text"])) for record in records]
|
||||
tokens = [set(re.findall(r"[a-zA-Z][a-zA-Z0-9_-]{2,}|[\u4e00-\u9fff]{2}", text.casefold())) - stop
|
||||
for text in texts]
|
||||
frequency = Counter(token for message in tokens for token in message)
|
||||
topics = [token for token, count in frequency.most_common(6) if count > 1]
|
||||
scored = sorted(range(len(records)), key=lambda i: (
|
||||
-sum(frequency[token] for token in tokens[i]),
|
||||
-int(bool(re.search(r"决定|计划|问题|需要|建议|修复|明天|[??]", texts[i]))),
|
||||
-i))
|
||||
selected: list[int] = []
|
||||
seen: set[str] = set()
|
||||
for index in scored:
|
||||
if texts[index] in seen or texts[index].lstrip().startswith("/"):
|
||||
continue
|
||||
selected.append(index)
|
||||
seen.add(texts[index])
|
||||
if len(selected) == 5:
|
||||
break
|
||||
if not selected:
|
||||
selected = list(range(max(0, len(records) - 3), len(records)))
|
||||
people = len({record["display_name"] for record in records})
|
||||
lines = [f"上下文总结(本地提取):请求最近{limit}条,实际{len(records)}条,{people}人。",
|
||||
f"范围 {records[0]['observed_at'][:16]} ~ {records[-1]['observed_at'][:16]}。",
|
||||
"主要话题:" + ("、".join(topics) if topics else "话题分散,以下为代表性内容。")]
|
||||
for index in sorted(selected):
|
||||
record = records[index]
|
||||
lines.append(f"• {record['display_name']}:{texts[index][:100]}")
|
||||
lines.append("仅覆盖本机已记录的当前群文字;问题或计划按原话提取。")
|
||||
return "\n".join(lines)
|
||||
|
||||
def person_index(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
||||
group = str(row["group_fingerprint"])
|
||||
directory = self.ledger.person_directory(group)
|
||||
if kind == "person_directory":
|
||||
if arguments and not re.fullmatch(r"[1-9]\d{0,2}", arguments):
|
||||
return "用法:/人物列表 [页码](/people),每页5人"
|
||||
page = int(arguments or 1)
|
||||
pages = max(1, (len(directory) + 4) // 5)
|
||||
if page > pages:
|
||||
return f"只有 {pages} 页;/人物列表 1"
|
||||
lines = [f"当前群人物与别名({page}/{pages},共{len(directory)}人)"]
|
||||
for person in directory[(page - 1) * 5:page * 5]:
|
||||
aliases = "、".join(person["aliases"])[:80] or "未配置"
|
||||
prefixes = "、".join(person["prefixes"])[:40]
|
||||
lines.append(f"{person['display_name']}:{aliases}" + (f";前缀 {prefixes}*" if prefixes else ""))
|
||||
lines.append("/人物 [名称或别名](/person)查看基本属性;/人物列表 页码 翻页")
|
||||
return "\n".join(lines)
|
||||
requested = normalize_text(arguments) or str(row["sender"])
|
||||
person = self.ledger.resolve_person(group, requested)
|
||||
entry = next((item for item in directory if item["person_key"] == person["person_key"]), None)
|
||||
if arguments and entry is None:
|
||||
return "当前群没有这个已配置人物或别名;/人物列表 查看索引"
|
||||
facts = str(person.get("identity_fact", ""))
|
||||
if facts and not self.profile_fact_is_safe(facts):
|
||||
facts = ""
|
||||
aliases = "、".join(entry["aliases"])[:160] if entry else "未配置"
|
||||
return (f"人物:{self.person_display_label(person['display_name'])};"
|
||||
f"群内索引 {person['person_key'][:12]};别名:{aliases or '未配置'}。\n"
|
||||
f"基本属性:{facts or '暂无配置'}。\n"
|
||||
"本人金币 /金币;个人记忆 /我的记忆。身份由本地昵称映射解析,无法读取微信原生账号ID。")
|
||||
|
||||
@staticmethod
|
||||
def service_state(service_name: str) -> str:
|
||||
try:
|
||||
@@ -266,13 +403,75 @@ class LocalCommandEngine:
|
||||
return "unknown"
|
||||
return str(getattr(module, "__version__", "unknown"))
|
||||
|
||||
@staticmethod
|
||||
def changelog_candidates() -> list[Path]:
|
||||
candidates: list[Path] = []
|
||||
module_path = Path(__file__).resolve()
|
||||
for parent in (module_path.parent, *module_path.parents):
|
||||
candidates.append(parent / "CHANGELOG.md")
|
||||
candidates.append(Path.cwd() / "CHANGELOG.md")
|
||||
seen: set[Path] = set()
|
||||
unique: list[Path] = []
|
||||
for candidate in candidates:
|
||||
if candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
unique.append(candidate)
|
||||
return unique
|
||||
|
||||
@classmethod
|
||||
def latest_changelog(cls) -> dict[str, str]:
|
||||
for path in cls.changelog_candidates():
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
for index, line in enumerate(lines):
|
||||
match = CHANGELOG_HEADING.match(line)
|
||||
if not match:
|
||||
continue
|
||||
version = match.group(1).strip()
|
||||
updated_at = (match.group(2) or "").strip()
|
||||
bullet_parts: list[str] = []
|
||||
collecting = False
|
||||
for item in lines[index + 1 :]:
|
||||
if item.startswith("## "):
|
||||
break
|
||||
if item.startswith("- "):
|
||||
if collecting:
|
||||
break
|
||||
collecting = True
|
||||
bullet_parts.append(item[2:].strip())
|
||||
continue
|
||||
if collecting:
|
||||
if item.startswith(" "):
|
||||
bullet_parts.append(item.strip())
|
||||
continue
|
||||
if not item.strip():
|
||||
break
|
||||
summary = normalize_text(" ".join(bullet_parts))
|
||||
return {
|
||||
"version": version,
|
||||
"updated_at": updated_at or "unknown",
|
||||
"summary": summary or "暂无更新摘要",
|
||||
}
|
||||
return {
|
||||
"version": "unknown",
|
||||
"updated_at": "unknown",
|
||||
"summary": "未找到 CHANGELOG.md",
|
||||
}
|
||||
|
||||
def jwebot_version(self) -> str:
|
||||
changelog = self.latest_changelog()
|
||||
return (
|
||||
"JWeBot 版本:"
|
||||
f"kernel {self.package_version('jwebot_kernel')},"
|
||||
f"sdk {self.package_version('jwebot_sdk')},"
|
||||
"official "
|
||||
f"{self.package_version('jwebot_official_extensions')}"
|
||||
f"{self.package_version('jwebot_official_extensions')}\n"
|
||||
f"最新版本:{changelog['version']}\n"
|
||||
f"更新时间:{changelog['updated_at']}\n"
|
||||
f"更新摘要:{changelog['summary']}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -0,0 +1,787 @@
|
||||
"""Single-player virtual currency rules, invoked by the reviewed Kernel service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
INITIAL_COINS = 1000
|
||||
MIN_DRAW_BET = 2
|
||||
MAX_DRAW_BET = 100
|
||||
MIN_LOTTERY_BET = 1
|
||||
MAX_LOTTERY_BET = 500
|
||||
DEFAULT_LOTTERY_BET = 10
|
||||
MIN_BLACKJACK_BET = 1
|
||||
MAX_BLACKJACK_BET = 500
|
||||
MIN_TRANSFER_AMOUNT = 1
|
||||
MAX_TRANSFER_AMOUNT = 100_000
|
||||
MIN_LOAN_AMOUNT = 1
|
||||
MAX_LOAN_AMOUNT = 500
|
||||
GAME_TIMEOUT_SECONDS = 600
|
||||
GAME_HISTORY_LIMIT = 50
|
||||
GAME_HISTORY_PAGE_SIZE = 5
|
||||
GAME_LABELS = {
|
||||
"prize_draw": "抽奖",
|
||||
"lottery": "彩票",
|
||||
"blackjack": "21点",
|
||||
}
|
||||
BLACKJACK_ACTIONS = {
|
||||
"要牌": "hit",
|
||||
"hit": "hit",
|
||||
"停牌": "stand",
|
||||
"stand": "stand",
|
||||
"状态": "status",
|
||||
"status": "status",
|
||||
"结束": "end",
|
||||
"end": "end",
|
||||
}
|
||||
BLACKJACK_START = re.compile(
|
||||
r"^(?:开始|start)(?P<bet>\d{1,3})?$"
|
||||
)
|
||||
BLACKJACK_COMBINED_ACTION = re.compile(
|
||||
r"^(?:开始|start)(?P<bet>\d{1,3})?"
|
||||
r"(?:要牌|hit|停牌|stand)$"
|
||||
)
|
||||
TRANSFER_MARKED = re.compile(
|
||||
r"^(?:给|to)?\s*(?P<target>.+?)\s*"
|
||||
r"(?:金额|金币|coin|coins|:|:)\s*(?P<amount>\d{1,6})$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
account.setdefault("balance", INITIAL_COINS)
|
||||
account.setdefault("game", None)
|
||||
loan = account.get("loan")
|
||||
try:
|
||||
loan_principal = int(loan.get("principal", 0) or 0) if isinstance(loan, dict) else 0
|
||||
except (TypeError, ValueError):
|
||||
loan_principal = 0
|
||||
if loan_principal <= 0:
|
||||
account["loan"] = None
|
||||
else:
|
||||
account["loan"] = {
|
||||
"principal": loan_principal,
|
||||
"borrowed_at": float(loan.get("borrowed_at", time.time())),
|
||||
}
|
||||
account.setdefault("wins", 0)
|
||||
account.setdefault("losses", 0)
|
||||
account.setdefault("ties", 0)
|
||||
counts = account.get("game_counts")
|
||||
if not isinstance(counts, dict):
|
||||
counts = {}
|
||||
account["game_counts"] = counts
|
||||
for kind in GAME_LABELS:
|
||||
value = counts.get(kind, 0)
|
||||
counts[kind] = value if type(value) is int and value >= 0 else 0
|
||||
history = account.get("game_history")
|
||||
if not isinstance(history, list):
|
||||
history = []
|
||||
account["game_history"] = [
|
||||
entry for entry in history if isinstance(entry, dict)
|
||||
][-GAME_HISTORY_LIMIT:]
|
||||
return account
|
||||
|
||||
|
||||
def next_game_index(account: dict[str, Any], kind: str) -> int:
|
||||
normalize_player_account(account)
|
||||
counts = account["game_counts"]
|
||||
counts[kind] = int(counts.get(kind, 0)) + 1
|
||||
return int(counts[kind])
|
||||
|
||||
|
||||
def append_game_record(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
kind: str,
|
||||
index: int,
|
||||
outcome: str,
|
||||
stake: int,
|
||||
payout: int,
|
||||
summary: str,
|
||||
) -> None:
|
||||
normalize_player_account(account)
|
||||
history = [
|
||||
entry
|
||||
for entry in account["game_history"]
|
||||
if not (
|
||||
entry.get("kind") == kind
|
||||
and int(entry.get("index", 0)) == index
|
||||
)
|
||||
]
|
||||
history.append(
|
||||
{
|
||||
"kind": kind,
|
||||
"index": index,
|
||||
"outcome": outcome[:80],
|
||||
"stake": stake,
|
||||
"payout": payout,
|
||||
"summary": summary[:240],
|
||||
"completed_at": time.time(),
|
||||
}
|
||||
)
|
||||
account["game_history"] = history[-GAME_HISTORY_LIMIT:]
|
||||
|
||||
|
||||
def card_label(card: int) -> str:
|
||||
return ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")[card % 13]
|
||||
|
||||
|
||||
def hand_value(cards: list[int]) -> int:
|
||||
total = sum(min(card % 13 + 1, 10) if card % 13 else 11 for card in cards)
|
||||
aces = sum(card % 13 == 0 for card in cards)
|
||||
while total > 21 and aces:
|
||||
total -= 10
|
||||
aces -= 1
|
||||
return total
|
||||
|
||||
|
||||
class PlayerGames:
|
||||
def __init__(self, ledger: Any, display_label: Any) -> None:
|
||||
self.ledger = ledger
|
||||
self.display_label = display_label
|
||||
self.rng = random.SystemRandom()
|
||||
|
||||
@staticmethod
|
||||
def rules() -> str:
|
||||
return (
|
||||
"每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n"
|
||||
"转账:/转账 对方 金额,或 /transfer name amount;"
|
||||
"对方需先用 /金币 初始化账户。\n"
|
||||
"贷款:/贷款 [1~500] 借入金币;/还款 [金额|全部] 归还,"
|
||||
"同一时间最多一笔未还贷款。\n"
|
||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
||||
"命中1/2/3个返下注的50%/120%/300%。\n"
|
||||
"21点先用 /21点 开始 [1~500] 进场,金额是本局下注,默认50;空格可省略。\n"
|
||||
"看到牌后再单独发送 /21点 要牌|停牌|状态|结束"
|
||||
"(/blackjack hit|stand|status|end);"
|
||||
"每人最多一局,10分钟无操作视为认输,结束不退下注。\n"
|
||||
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
|
||||
"以上均含本金;虚拟金币不能充值或兑换。"
|
||||
)
|
||||
|
||||
def handle(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
||||
arguments = arguments.strip()
|
||||
if kind == "game_rules":
|
||||
return self.rules()
|
||||
if arguments.casefold() in {"帮助", "规则", "help", "rules"}:
|
||||
return self.rules()
|
||||
if kind == "game_history":
|
||||
return self.game_history(row, arguments)
|
||||
if kind == "lottery":
|
||||
return self.group_lottery(row, arguments)
|
||||
if kind == "coin_ranking":
|
||||
return self.coin_ranking(row, arguments)
|
||||
if kind == "transfer":
|
||||
return self.transfer(row, arguments)
|
||||
if kind in {"loan", "repay"}:
|
||||
return self.loan_or_repay(row, kind, arguments)
|
||||
if kind == "wallet" and arguments:
|
||||
return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则"
|
||||
if kind == "prize_draw":
|
||||
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
||||
return "用法:/抽奖 [2~100金币](/draw),默认10金币"
|
||||
bet = int(arguments or 10)
|
||||
elif kind == "blackjack":
|
||||
compacted = re.sub(r"\s+", "", arguments.casefold()) or "状态"
|
||||
start = BLACKJACK_START.fullmatch(compacted)
|
||||
if start is not None:
|
||||
action = "start"
|
||||
bet = int(start["bet"] or 50)
|
||||
else:
|
||||
action = BLACKJACK_ACTIONS.get(compacted, "")
|
||||
bet = 0
|
||||
if not action:
|
||||
combined = BLACKJACK_COMBINED_ACTION.fullmatch(compacted)
|
||||
if combined is not None:
|
||||
amount = combined["bet"] or "50"
|
||||
return (
|
||||
"21点要分两步:先发送 /21点 开始 "
|
||||
f"{amount}({amount} 是本局下注);看到牌后再单独发送 "
|
||||
"/21点 要牌 或 /21点 停牌"
|
||||
)
|
||||
return (
|
||||
"用法:先 /21点 开始 [1~500](金额是本局下注,默认50);"
|
||||
"开局后再单独发送 /21点 要牌|停牌|状态|结束;空格可省略"
|
||||
)
|
||||
else:
|
||||
bet = 0
|
||||
if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET:
|
||||
return "抽奖下注必须为 2~100 的整数金币"
|
||||
if (
|
||||
kind == "blackjack"
|
||||
and action == "start"
|
||||
and not MIN_BLACKJACK_BET <= bet <= MAX_BLACKJACK_BET
|
||||
):
|
||||
return "21点下注必须为 1~500 的整数金币"
|
||||
person = self.ledger.resolve_person(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
)
|
||||
player_label = self.display_label(person["display_name"])
|
||||
|
||||
def player_reply(text: str, index: int | None = None) -> str:
|
||||
suffix = f" #{index}" if index is not None else ""
|
||||
return f"{player_label}{suffix}:{text}"
|
||||
|
||||
def execute(account: dict[str, Any]) -> str:
|
||||
normalize_player_account(account)
|
||||
expired = ""
|
||||
game = account.get("game")
|
||||
if game and (
|
||||
type(game.get("index")) is not int
|
||||
or int(game["index"]) < 1
|
||||
):
|
||||
game["index"] = next_game_index(account, "blackjack")
|
||||
if game and time.time() - float(game["updated_at"]) >= GAME_TIMEOUT_SECONDS:
|
||||
append_game_record(
|
||||
account,
|
||||
kind="blackjack",
|
||||
index=int(game["index"]),
|
||||
outcome="超时认输",
|
||||
stake=int(game["bet"]),
|
||||
payout=0,
|
||||
summary="10分钟无有效操作,按认输结束",
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
expired = "上一局已超时,按认输结束;"
|
||||
if kind == "wallet":
|
||||
state = "进行中" if account.get("game") else "无进行中的牌局"
|
||||
loan = account.get("loan")
|
||||
loan_text = f"未还贷款 {loan['principal']};" if loan else "无贷款;"
|
||||
return (
|
||||
f"{expired}金币 {account['balance']};{loan_text}21点 {state};"
|
||||
f"已结算胜 {account['wins']} / 负 {account['losses']} / 平 {account['ties']}。"
|
||||
"首次赠送1000仅发一次;/游戏帮助 查看规则"
|
||||
)
|
||||
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
||||
active = account.get("game")
|
||||
index = int(active["index"]) if active else None
|
||||
return player_reply(
|
||||
expired + self.play_blackjack(account, action),
|
||||
index,
|
||||
)
|
||||
if account.get("game"):
|
||||
message = expired + "你已有一局21点,请先 /21点 停牌 或 /21点 结束"
|
||||
return (
|
||||
player_reply(message, int(account["game"]["index"]))
|
||||
if kind == "blackjack"
|
||||
else message
|
||||
)
|
||||
if account["balance"] < bet:
|
||||
message = expired + f"金币不足:余额 {account['balance']},本次需要 {bet}"
|
||||
return player_reply(message) if kind == "blackjack" else message
|
||||
account["balance"] -= bet
|
||||
if kind == "blackjack":
|
||||
index = next_game_index(account, "blackjack")
|
||||
deck = list(range(52))
|
||||
self.rng.shuffle(deck)
|
||||
game = {"bet": bet, "deck": deck[4:], "player": deck[:2],
|
||||
"dealer": deck[2:4], "updated_at": time.time(),
|
||||
"index": index}
|
||||
account["game"] = game
|
||||
if hand_value(game["player"]) == 21 or hand_value(game["dealer"]) == 21:
|
||||
return player_reply(
|
||||
expired + self.finish_blackjack(account, natural=True),
|
||||
index,
|
||||
)
|
||||
return player_reply(expired + self.show_hand(game), index)
|
||||
index = next_game_index(account, "prize_draw")
|
||||
roll = self.rng.randrange(100)
|
||||
payout = bet * (5 if roll < 5 else 2 if roll < 25 else 0)
|
||||
result = "大奖" if roll < 5 else "小奖" if roll < 25 else "未中奖"
|
||||
account["balance"] += payout
|
||||
account["wins" if payout > bet else "losses"] += 1
|
||||
append_game_record(
|
||||
account,
|
||||
kind="prize_draw",
|
||||
index=index,
|
||||
outcome=result,
|
||||
stake=bet,
|
||||
payout=payout,
|
||||
summary=f"抽奖结果 {result}",
|
||||
)
|
||||
return player_reply(
|
||||
expired
|
||||
+ f"{result};下注 {bet},返还 {payout},"
|
||||
f"净变化 {payout - bet:+d};金币 {account['balance']}",
|
||||
index,
|
||||
)
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
str(row["group_fingerprint"]), str(row["sender"]),
|
||||
str(row["event_id"]), kind + ":" + arguments, execute)
|
||||
|
||||
def coin_ranking(self, row: dict[str, Any], arguments: str) -> str:
|
||||
value = arguments.strip()
|
||||
if value and not re.fullmatch(r"\d{1,2}", value):
|
||||
return "用法:/大富翁榜 或 /金币排行 [人数]"
|
||||
limit = int(value or 10)
|
||||
if not 1 <= limit <= 20:
|
||||
return "金币排行人数必须为 1~20"
|
||||
records = self.ledger.player_coin_ranking(
|
||||
str(row["group_fingerprint"]),
|
||||
limit=limit,
|
||||
)
|
||||
if not records:
|
||||
return "当前群还没有金币账户;发送 /金币 初始化"
|
||||
lines = ["大富翁榜:"]
|
||||
for index, record in enumerate(records, start=1):
|
||||
loan = int(record.get("loan", 0))
|
||||
loan_text = f"(未还贷款 {loan})" if loan else ""
|
||||
lines.append(
|
||||
f"{index}. {record['display_name']} {record['balance']} 金币"
|
||||
f"{loan_text}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def parse_transfer(
|
||||
arguments: str,
|
||||
) -> tuple[str, int] | None:
|
||||
value = arguments.strip()
|
||||
if not value:
|
||||
return None
|
||||
marked = TRANSFER_MARKED.fullmatch(value)
|
||||
if marked is not None:
|
||||
target = marked["target"]
|
||||
amount = int(marked["amount"])
|
||||
else:
|
||||
tokens = value.split()
|
||||
if len(tokens) < 2:
|
||||
return None
|
||||
if re.fullmatch(r"\d{1,6}", tokens[0]):
|
||||
amount = int(tokens[0])
|
||||
target = " ".join(tokens[1:])
|
||||
elif re.fullmatch(r"\d{1,6}", tokens[-1]):
|
||||
amount = int(tokens[-1])
|
||||
target = " ".join(tokens[:-1])
|
||||
else:
|
||||
return None
|
||||
target = re.sub(r"^(?:给|to)\s*", "", target.strip(), flags=re.IGNORECASE)
|
||||
target = target.lstrip("@@").strip()
|
||||
if not target:
|
||||
return None
|
||||
return target, amount
|
||||
|
||||
def transfer(self, row: dict[str, Any], arguments: str) -> str:
|
||||
parsed = self.parse_transfer(arguments)
|
||||
if parsed is None:
|
||||
return (
|
||||
"用法:/转账 对方 金额,例如 /转账 Bob 100;"
|
||||
"英文 /transfer Bob 100。金额范围 1~100000,"
|
||||
"对方需先发送 /金币 初始化账户"
|
||||
)
|
||||
target, amount = parsed
|
||||
if not MIN_TRANSFER_AMOUNT <= amount <= MAX_TRANSFER_AMOUNT:
|
||||
return "转账金额必须为 1~100000 的整数金币"
|
||||
|
||||
def render(data: dict[str, Any]) -> str:
|
||||
source = data["source_display_name"]
|
||||
target_name = data["target_display_name"]
|
||||
if data["status"] == "self":
|
||||
return f"{source}:不能给自己转账"
|
||||
if data["status"] == "target_missing":
|
||||
return (
|
||||
f"{source}:{target_name} 还没有金币账户,"
|
||||
"请先让对方发送 /金币 初始化"
|
||||
)
|
||||
if data["status"] == "insufficient":
|
||||
return (
|
||||
f"{source}:金币不足,余额 {data['source_balance']},"
|
||||
f"本次需要 {data['amount']}"
|
||||
)
|
||||
return (
|
||||
f"{source}:已向 {target_name} 转账 {data['amount']} 金币;"
|
||||
f"本人金币 {data['source_balance']};"
|
||||
f"{target_name} 金币 {data['target_balance']}"
|
||||
)
|
||||
|
||||
return self.ledger.transfer_player_coins(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
str(row["event_id"]),
|
||||
"transfer:" + arguments,
|
||||
target,
|
||||
amount,
|
||||
render,
|
||||
)
|
||||
|
||||
def loan_or_repay(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
||||
value = re.sub(r"\s+", "", arguments.casefold())
|
||||
if kind == "loan":
|
||||
if not re.fullmatch(r"\d{1,3}", value):
|
||||
return "用法:/贷款 [1~500];同一时间最多一笔未还贷款"
|
||||
amount = int(value)
|
||||
if not MIN_LOAN_AMOUNT <= amount <= MAX_LOAN_AMOUNT:
|
||||
return "贷款金额必须为 1~500 的整数金币"
|
||||
else:
|
||||
amount = 0
|
||||
if value not in {"", "全部", "all"}:
|
||||
if not re.fullmatch(r"\d{1,6}", value):
|
||||
return "用法:/还款 [金额|全部]"
|
||||
amount = int(value)
|
||||
if amount <= 0:
|
||||
return "还款金额必须为正整数金币"
|
||||
|
||||
group = str(row["group_fingerprint"])
|
||||
sender = str(row["sender"])
|
||||
person = self.ledger.resolve_person(group, sender)
|
||||
player_label = self.display_label(person["display_name"])
|
||||
|
||||
def execute(account: dict[str, Any]) -> str:
|
||||
normalize_player_account(account)
|
||||
loan = account.get("loan")
|
||||
if kind == "loan":
|
||||
if loan:
|
||||
return (
|
||||
f"{player_label}:已有未还贷款 {loan['principal']} 金币;"
|
||||
"请先 /还款 [金额|全部]"
|
||||
)
|
||||
account["balance"] += amount
|
||||
account["loan"] = {
|
||||
"principal": amount,
|
||||
"borrowed_at": time.time(),
|
||||
}
|
||||
return (
|
||||
f"{player_label}:贷款 {amount} 金币已到账;"
|
||||
f"当前金币 {account['balance']};未还贷款 {amount}"
|
||||
)
|
||||
if not loan:
|
||||
return f"{player_label}:当前没有未还贷款"
|
||||
debt = int(loan["principal"])
|
||||
if amount <= 0:
|
||||
pay = min(int(account["balance"]), debt)
|
||||
else:
|
||||
pay = min(amount, debt)
|
||||
if pay <= 0:
|
||||
return f"{player_label}:金币不足,无法还款;未还贷款 {debt}"
|
||||
if pay > int(account["balance"]):
|
||||
return (
|
||||
f"{player_label}:金币不足,余额 {account['balance']},"
|
||||
f"本次还款需要 {pay}"
|
||||
)
|
||||
account["balance"] -= pay
|
||||
debt -= pay
|
||||
if debt:
|
||||
account["loan"] = {
|
||||
"principal": debt,
|
||||
"borrowed_at": float(loan.get("borrowed_at", time.time())),
|
||||
}
|
||||
return (
|
||||
f"{player_label}:已还款 {pay} 金币;"
|
||||
f"剩余贷款 {debt};当前金币 {account['balance']}"
|
||||
)
|
||||
account["loan"] = None
|
||||
return (
|
||||
f"{player_label}:已还清贷款 {pay} 金币;"
|
||||
f"当前金币 {account['balance']}"
|
||||
)
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
group,
|
||||
sender,
|
||||
str(row["event_id"]),
|
||||
f"{kind}:{arguments}",
|
||||
execute,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_lottery_entry(
|
||||
arguments: str,
|
||||
) -> tuple[tuple[int, int, int], int] | None:
|
||||
value = arguments.strip().casefold()
|
||||
for prefix in ("报名", "join"):
|
||||
if value.startswith(prefix):
|
||||
value = value[len(prefix):].strip()
|
||||
break
|
||||
if not value:
|
||||
return None
|
||||
marked = re.fullmatch(
|
||||
r"([0-9])\s*([0-9])\s*([0-9])"
|
||||
r"(?:金额|下注|bet)(\d{1,3})",
|
||||
re.sub(r"[,,、]", "", value),
|
||||
)
|
||||
stake: int
|
||||
if marked is not None:
|
||||
digits = tuple(int(marked[index]) for index in range(1, 4))
|
||||
stake = int(marked[4])
|
||||
else:
|
||||
tokens = re.sub(r"[,,、]", " ", value).split()
|
||||
digits: tuple[int, int, int]
|
||||
if len(tokens) == 1 and tokens[0].isdigit():
|
||||
compact = tokens[0]
|
||||
if not 3 <= len(compact) <= 6:
|
||||
return None
|
||||
digits = tuple(int(item) for item in compact[:3])
|
||||
stake = int(compact[3:] or DEFAULT_LOTTERY_BET)
|
||||
elif (
|
||||
len(tokens) == 2
|
||||
and re.fullmatch(r"\d{3}", tokens[0])
|
||||
and re.fullmatch(r"\d{1,3}", tokens[1])
|
||||
):
|
||||
digits = tuple(int(item) for item in tokens[0])
|
||||
stake = int(tokens[1])
|
||||
elif (
|
||||
len(tokens) in {3, 4}
|
||||
and all(re.fullmatch(r"\d", item) for item in tokens[:3])
|
||||
and (
|
||||
len(tokens) == 3
|
||||
or re.fullmatch(r"\d{1,3}", tokens[3])
|
||||
)
|
||||
):
|
||||
digits = tuple(int(item) for item in tokens[:3])
|
||||
stake = (
|
||||
int(tokens[3])
|
||||
if len(tokens) == 4
|
||||
else DEFAULT_LOTTERY_BET
|
||||
)
|
||||
else:
|
||||
return None
|
||||
if len(set(digits)) != 3:
|
||||
raise ValueError("彩票的三个数字必须互不重复")
|
||||
if not MIN_LOTTERY_BET <= stake <= MAX_LOTTERY_BET:
|
||||
raise ValueError("彩票下注必须为 1~500 的整数金币")
|
||||
return tuple(sorted(digits)), stake
|
||||
|
||||
def group_lottery(self, row: dict[str, Any], arguments: str) -> str:
|
||||
group = str(row["group_fingerprint"])
|
||||
sender = str(row["sender"])
|
||||
event_id = str(row["event_id"])
|
||||
command = "lottery:" + arguments
|
||||
compacted = re.sub(r"\s+", "", arguments.casefold())
|
||||
|
||||
def status_text(data: dict[str, Any]) -> str:
|
||||
if data["status"] == "no_round":
|
||||
return "当前没有进行中的彩票;发送 /彩票 开始 创建新一把"
|
||||
entries = data["entries"]
|
||||
lines = [
|
||||
f"彩票第 {data['round_number']} 把正在报名;"
|
||||
f"主持人 {data['host_display_name']};已报名 {len(entries)} 人"
|
||||
]
|
||||
for entry in entries:
|
||||
lines.append(
|
||||
f"{entry['display_name']} #{entry['player_game_index']}:"
|
||||
f"{' '.join(map(str, entry['numbers']))},下注 {entry['stake']}"
|
||||
)
|
||||
if not entries:
|
||||
lines.append("暂无报名;/彩票 报名 3 4 7 [金额]")
|
||||
return "\n".join(lines)
|
||||
|
||||
if compacted in {"开始", "start"}:
|
||||
return self.ledger.start_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
lambda data: (
|
||||
f"彩票第 {data['round_number']} 把"
|
||||
+ ("已经在报名中" if data["status"] == "already_open" else "已开始")
|
||||
+ f";主持人 {data['host_display_name']}。\n"
|
||||
"报名:/彩票 3 4 7 [金额](金额1~500,默认10,空格可省略);"
|
||||
"查询:/彩票 当前状态;主持人开奖:/彩票 开奖"
|
||||
),
|
||||
)
|
||||
if compacted in {"当前状态", "状态", "status"}:
|
||||
return self.ledger.group_lottery_status(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
status_text,
|
||||
)
|
||||
if compacted in {"开奖", "draw"}:
|
||||
drawn = tuple(sorted(self.rng.sample(range(10), 3)))
|
||||
|
||||
def draw_text(data: dict[str, Any]) -> str:
|
||||
if data["status"] == "no_round":
|
||||
return "当前没有可以开奖的彩票"
|
||||
if data["status"] == "not_host":
|
||||
return (
|
||||
f"只有本轮主持人 {data['host_display_name']} 可以开奖"
|
||||
)
|
||||
if data["status"] == "empty":
|
||||
return "本轮还没有人报名,不能开奖"
|
||||
lines = [
|
||||
f"彩票第 {data['round_number']} 把开奖号:"
|
||||
f"{' '.join(map(str, data['drawn_numbers']))}"
|
||||
]
|
||||
for entry in data["entries"]:
|
||||
lines.append(
|
||||
f"{entry['display_name']} #{entry['player_game_index']}:"
|
||||
f"命中 {entry['matches']} 个;下注 {entry['stake']},"
|
||||
f"返还 {entry['payout']},净变化 "
|
||||
f"{entry['payout'] - entry['stake']:+d};"
|
||||
f"金币 {entry['balance']}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
return self.ledger.draw_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
drawn,
|
||||
draw_text,
|
||||
)
|
||||
if compacted in {"取消", "cancel"}:
|
||||
return self.ledger.cancel_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
lambda data: (
|
||||
"当前没有进行中的彩票"
|
||||
if data["status"] == "no_round"
|
||||
else (
|
||||
f"只有本轮主持人 {data['host_display_name']} 可以取消"
|
||||
if data["status"] == "not_host"
|
||||
else (
|
||||
f"彩票第 {data['round_number']} 把已取消;"
|
||||
f"{len(data['entries'])} 人的下注已全额退回"
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
try:
|
||||
parsed = self.parse_lottery_entry(arguments)
|
||||
except ValueError as error:
|
||||
return str(error)
|
||||
if parsed is None:
|
||||
return (
|
||||
"用法:先 /彩票 开始;再 /彩票 报名 3 4 7 [金额]"
|
||||
"(也可 /彩票347,金额1~500、默认10);"
|
||||
"/彩票 当前状态;主持人 /彩票 开奖"
|
||||
)
|
||||
numbers, stake = parsed
|
||||
|
||||
def register_text(data: dict[str, Any]) -> str:
|
||||
if data["status"] == "no_round":
|
||||
return "当前没有进行中的彩票;先发送 /彩票 开始"
|
||||
if data["status"] == "insufficient":
|
||||
return (
|
||||
f"{data['display_name']}:金币不足,余额 {data['balance']},"
|
||||
f"报名需要 {stake}"
|
||||
)
|
||||
action = "报名已更新" if data["status"] == "updated" else "报名成功"
|
||||
return (
|
||||
f"{data['display_name']} #{data['player_game_index']}:"
|
||||
f"彩票第 {data['round_number']} 把{action};号码 "
|
||||
f"{' '.join(map(str, data['numbers']))};下注 {data['stake']};"
|
||||
f"金币 {data['balance']}"
|
||||
)
|
||||
|
||||
return self.ledger.register_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
numbers,
|
||||
stake,
|
||||
register_text,
|
||||
)
|
||||
|
||||
def game_history(self, row: dict[str, Any], arguments: str) -> str:
|
||||
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
||||
return "用法:/游戏记录 [页码](/gamehistory),每页5条"
|
||||
page = int(arguments or 1)
|
||||
if page < 1:
|
||||
return "游戏记录页码必须从 1 开始"
|
||||
data = self.ledger.player_game_history(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
)
|
||||
records = data["history"]
|
||||
pages = max(1, (len(records) + GAME_HISTORY_PAGE_SIZE - 1) // GAME_HISTORY_PAGE_SIZE)
|
||||
if page > pages:
|
||||
return f"{data['display_name']} 只有 {pages} 页游戏记录"
|
||||
start = (page - 1) * GAME_HISTORY_PAGE_SIZE
|
||||
lines = [f"{data['display_name']} 的游戏记录 {page}/{pages}:"]
|
||||
for record in records[start:start + GAME_HISTORY_PAGE_SIZE]:
|
||||
label = GAME_LABELS.get(str(record["kind"]), str(record["kind"]))
|
||||
lines.append(
|
||||
f"{data['display_name']} #{record['index']}:{label} {record['outcome']};"
|
||||
f"下注 {record['stake']},返还 {record['payout']},"
|
||||
f"净变化 {record['payout'] - record['stake']:+d}"
|
||||
)
|
||||
if not records:
|
||||
lines.append("暂无已结算记录")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def show_hand(game: dict[str, Any]) -> str:
|
||||
player = " ".join(card_label(card) for card in game["player"])
|
||||
return (f"21点:你的牌 {player}({hand_value(game['player'])}点);"
|
||||
f"庄家 {card_label(game['dealer'][0])} + 暗牌;下注 {game['bet']}。\n"
|
||||
"/21点 要牌(hit)或 /21点 停牌(stand)")
|
||||
|
||||
def play_blackjack(self, account: dict[str, Any], action: str) -> str:
|
||||
game = account.get("game")
|
||||
if not game:
|
||||
return "本人没有进行中的21点;/21点 开始 [金额](/blackjack start)"
|
||||
if action == "status":
|
||||
return self.show_hand(game)
|
||||
if action == "end":
|
||||
index = int(game["index"])
|
||||
bet = int(game["bet"])
|
||||
append_game_record(
|
||||
account,
|
||||
kind="blackjack",
|
||||
index=index,
|
||||
outcome="主动认输",
|
||||
stake=bet,
|
||||
payout=0,
|
||||
summary="主动结束牌局,不退下注",
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
return f"本人的21点已认输结束,不退下注;金币 {account['balance']}"
|
||||
game["updated_at"] = time.time()
|
||||
if action == "hit":
|
||||
game["player"].append(game["deck"].pop())
|
||||
if hand_value(game["player"]) > 21:
|
||||
return self.finish_blackjack(account)
|
||||
return self.show_hand(game)
|
||||
while hand_value(game["dealer"]) < 17:
|
||||
game["dealer"].append(game["deck"].pop())
|
||||
return self.finish_blackjack(account)
|
||||
|
||||
@staticmethod
|
||||
def finish_blackjack(account: dict[str, Any], *, natural: bool = False) -> str:
|
||||
game = account["game"]
|
||||
player = hand_value(game["player"])
|
||||
dealer = hand_value(game["dealer"])
|
||||
bet = game["bet"]
|
||||
if player > 21 or natural and dealer == 21 and player != 21:
|
||||
outcome, payout, stat = "你输了", 0, "losses"
|
||||
elif player == dealer:
|
||||
outcome, payout, stat = "平局", bet, "ties"
|
||||
elif natural and player == 21:
|
||||
outcome, payout, stat = "自然21点获胜", bet * 5 // 2, "wins"
|
||||
elif dealer > 21 or player > dealer:
|
||||
outcome, payout, stat = "你赢了", bet * 2, "wins"
|
||||
else:
|
||||
outcome, payout, stat = "你输了", 0, "losses"
|
||||
account["balance"] += payout
|
||||
account[stat] += 1
|
||||
append_game_record(
|
||||
account,
|
||||
kind="blackjack",
|
||||
index=int(game["index"]),
|
||||
outcome=outcome,
|
||||
stake=int(bet),
|
||||
payout=int(payout),
|
||||
summary=(
|
||||
f"玩家{player}点,庄家{dealer}点"
|
||||
),
|
||||
)
|
||||
account["game"] = None
|
||||
return (f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}({player}点),"
|
||||
f"庄家 {' '.join(map(card_label, game['dealer']))}({dealer}点);"
|
||||
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};金币 {account['balance']}")
|
||||
@@ -14,6 +14,8 @@ from .local_commands import (
|
||||
LocalCommandEngine,
|
||||
local_command_parts,
|
||||
utc_now,
|
||||
pack_reply_parts,
|
||||
unpack_reply_parts,
|
||||
)
|
||||
|
||||
|
||||
@@ -74,7 +76,11 @@ class LocalServiceBroker:
|
||||
"question": f"{command} {arguments}".rstrip(),
|
||||
"created_at": str(event.get("observed_at") or utc_now()),
|
||||
}
|
||||
return {"text": self.engine.handle(row, event)}
|
||||
text = self.engine.handle(row, event)
|
||||
parts = unpack_reply_parts(text)
|
||||
if parts is not None:
|
||||
return {"text": parts[0], "texts": parts}
|
||||
return {"text": text}
|
||||
|
||||
|
||||
class _Context:
|
||||
@@ -162,6 +168,8 @@ class LocalExtensionRuntime:
|
||||
result = handler(sdk_event, arguments)
|
||||
if not isinstance(result, ExtensionResult):
|
||||
raise LocalExtensionError("local adapter returned invalid result")
|
||||
if result.reply.kind is PlanKind.MULTI_TEXT:
|
||||
return pack_reply_parts(list(result.reply.messages))
|
||||
if result.reply.kind is not PlanKind.TEXT:
|
||||
raise LocalExtensionError("local command returned no visible reply")
|
||||
return result.reply.message
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-sdk"
|
||||
version = "0.6.0a0+1015"
|
||||
version = "1.0.0b1+1016"
|
||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = []
|
||||
|
||||
@@ -38,4 +38,4 @@ __all__ = [
|
||||
"SingleGroupContext",
|
||||
]
|
||||
|
||||
__version__ = "0.6.0a0+1015"
|
||||
__version__ = "1.0.0b1+1016"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user