Author SHA1 Message Date
JWeBot Contributors 22edf6ff15 Cache OCR-negative bubbles
check / local-gate (push) Failing after 42s
2026-10-16 11:38:03 +08:00
JWeBot Contributors 572159f571 Fix OCR thread test isolation
check / local-gate (push) Failing after 17m10s
2026-10-16 11:19:55 +08:00
JWeBot Contributors 99a251c19a Limit OCR runtime threads
check / local-gate (push) Failing after 44s
2026-10-16 11:16:23 +08:00
JWeBot Contributors 3fa04755d0 Add multiplayer blackjack table flow
check / local-gate (push) Failing after 51s
2026-10-16 11:03:32 +08:00
JWeBot Contributors afc71e63a7 Release beta 1.0.0 baseline
check / local-gate (push) Failing after 1m35s
2026-10-16 01:07:07 +08:00
JWeBot Contributors bf4f28f48e Add coin leaderboard command
check / local-gate (push) Failing after 1m34s
2026-10-15 19:00:29 +08:00
JWeBot Contributors 148ae672f4 Install autochess data in runtime
check / local-gate (push) Failing after 1m39s
2026-10-15 18:48:48 +08:00
JWeBot Contributors bbddb5be6f Add virtual coin loans
check / local-gate (push) Failing after 42s
2026-10-15 18:43:49 +08:00
JWeBot Contributors 2bb2effc04 Externalize autochess game data
check / local-gate (push) Failing after 58s
2026-10-15 18:37:41 +08:00
JWeBot Contributors 5125b827c0 Release alpha 0.6.8 autochess legacy state fix
check / local-gate (push) Failing after 1m33s
2026-10-15 18:18:00 +08:00
JWeBot Contributors 3dea42b268 Release alpha 0.6.7 autochess pacing
check / local-gate (push) Failing after 32s
2026-10-15 18:08:55 +08:00
JWeBot Contributors a74147e7f1 Release alpha 0.6.6 autochess flow
check / local-gate (push) Failing after 40s
2026-10-15 17:10:28 +08:00
JWeBot Contributors 9fff57134f feat: add two-human AI autochess mode
check / local-gate (push) Failing after 50s
2026-10-15 16:08:59 +08:00
JWeBot Contributors e713ff369a feat: add CS Major autochess tournament
check / local-gate (push) Failing after 2m1s
2026-10-15 15:17:32 +08:00
JWeBot Contributors f07ae653c7 feat: add multiplayer lottery and game records
check / local-gate (push) Failing after 1m35s
2026-10-15 13:57:32 +08:00
JWeBot Contributors 0731e5cebb fix: bound chat latency and clarify blackjack flow 2026-10-15 13:37:57 +08:00
JWeBot Contributors f7fedc54e4 release: alpha 0.6.1 local player plugins and capture reconciliation
check / local-gate (push) Failing after 3m24s
2026-10-15 05:01:40 +08:00
126 changed files with 15173 additions and 91 deletions
+1
View File
@@ -67,6 +67,7 @@ id_ed25519*
# Sensitive local notes
homeserver-apikey-*.md
debug-*.md
# macOS/editor
.DS_Store
+4 -2
View File
@@ -4,9 +4,11 @@ These rules are mandatory for every automated change in this repository.
## Versioning
- Use `alpha X.Y.Z.MMDD`.
- Use `beta X.Y.Z.MMDD` for current beta releases.
- Python package versions use PEP 440 pre-release form, for example
`X.Y.Zb1+MMDD`.
- Small changes increment only `Z`.
- Do not move to a new minor version without explicit user approval.
- Do not move to a new minor or stable version without explicit user approval.
- Keep `VERSION`, root `pyproject.toml`, package versions, package
`__version__`, `README.md`, and `CHANGELOG.md` synchronized.
+783 -12
View File
@@ -1,5 +1,213 @@
# Changelog
## beta 1.0.2.1016 - 2026-10-16
- Cache OCR-negative visible bubbles and reuse stable visual signatures as the
OCR cache key so unreadable or lightly re-rendered bubbles do not trigger
repeated 10+ second RapidOCR passes during active chat windows.
## beta 1.0.1.1016 - 2026-10-16
- Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the
adaptive OCR recorder no longer saturates CPU after restart and slow recovery
windows do not stretch local command detection into 10+ second scans.
## beta 1.0.0.1016 - 2026-10-16
- Promote JWeBot from alpha to beta after the local, staging, privacy, and
production deployment gates stabilized.
- Consolidate the current reviewed local command set, including virtual coin
transfer, loans, repayment, group leaderboard, multiplayer lottery,
blackjack, Steam queries, and CS Major autochess.
- Externalize CS Major autochess game content into package JSON data files for
cards, coaches, narratives, draft/shop/economy rules, match formats, and
MR15/overtime settings.
- Add a maintained command catalog and keep debug session notes out of the
repository by default.
- Keep release validation on `tools/sync-local-extensions.py --check`,
`tools/check-local.sh`, and `tools/check-secrets.py --history`.
## alpha 0.6.11.1015
- Add `/大富翁榜`, `/金币排行`, `/金币排行榜`, and `/richlist` for a
group-scoped virtual coin leaderboard. Rankings are read-only, sorted by
current coin balance, recover display names from configured aliases and
observed profiles, and mark active loan principal when present.
- Split leaderboard command ownership into a dedicated reviewed
`coin-ranking` local extension.
## alpha 0.6.10.1015
- Add virtual coin loans with `/贷款 [金额]` and `/loan amount`, plus
repayments through `/还款 [金额|全部]` and `/repay`. Loans are stored in
the same group-scoped virtual coin account, capped at 500 coins, limited to
one active loan per account, and replay-safe through the existing ledger
transaction path.
- Split loan command ownership into a dedicated reviewed `coin-loan` local
extension so the single-player game plugin stays under the command-count
validation limit.
## alpha 0.6.9.1015
- Move CS Major autochess public game data out of `cs_autochess.py` and into
package data files under `jwebot_official_extensions/data/cs_autochess/`.
Player cards, coach cards, narrative fragments, shop/economy odds, draft
pacing, synergy thresholds, match formats, MR15/overtime settings, and
special event rewards are now data driven.
- Add startup validation for the externalized data so missing fields,
duplicate card IDs, invalid schema versions, or an undersized narrative
library fail fast before the command engine runs.
- Add regression coverage that verifies the JSON files are packaged, readable,
and consistent with the runtime card, coach, match-format, and narrative
inventories.
- Add virtual coin transfers with `/转账 对方 金额` and `/transfer name amount`.
Transfers are single-transaction ledger updates, require the receiver to
initialize `/金币` first, reject self-transfers, and replay safely by event ID.
## alpha 0.6.8.1015
- Add an atomic compatibility path for old active CS autochess runs. When a
group still has a pre-schema-3 active tournament, `/自走棋开始` now archives
that old run as cancelled and opens a fresh schema-3 season in the same
SQLite transaction.
- Preserve old story and battle archive rows while ensuring the new season is
the only active autochess run selected by future commands.
## alpha 0.6.7.1015
- Expand CS Major autochess content to 100+ player cards and an 18-coach
metadata pool while keeping the opening coach draft to 10 visible choices.
- Replace abstract training targets with real round-robin training rounds.
Each round resolves participant-vs-participant or participant-vs-AI BO1
scrims, then opens an operation window for D rolling, locking, lineup swaps,
tactics, and readiness.
- Replace fixed score bands with deterministic MR15 round simulation. Map
scores now come from individual round outcomes, including 15-15 overtime,
instead of direct mappings such as 16-6 or 16-9.
- Add a 120+ line local narrative library for openings, streaks, pauses,
clutches, economy beats, closes, training hooks, and media/sponsor
challenges so match reports vary across maps.
- Hide raw target/performance calculation text from training, match, and
challenge reports while preserving structured battle facts in the archive.
- Start drafted players as two-star素材 and rebalance roster completion toward
low/mid-cost cards, leaving upgrades and synergies to the shop loop.
## alpha 0.6.6.1015
- Rebuild CS Major autochess around multi-message hosting: tournament
welcomes, seed order, draft boards, status panels, training results, match
reports, standings, brackets, and awards are emitted as separate bounded
replies.
- Replace instant full rosters with three draft rounds: round one selects from
a fixed 10-coach pool, rounds two and three draft players, then each team is
completed to five starters, one coach, and three bench slots.
- Run the full event as training camp, BO3 single-round-robin group stage,
one media/sponsor challenge after group round one or two, BO5 semifinals,
BO5 final, and final awards.
- Let readiness advance phases automatically. Group matches wait for all real
players, while knockout matches only wait for real players still in the
current pairing, so eliminated players are not prompted to prepare.
- Add local multi-text reply support across the SDK, official extension
adapter, Edge response contract, and recorder GUI sender while preserving
single-message compatibility.
## alpha 0.6.5.1015
- Extend the AI test path for CS Major autochess to support two-human games:
`/自走棋加AI战队` now adds AI 战队 A and AI 战队 B when exactly two
real players have registered.
- Keep the same command for two- and three-human test modes. The tournament
engine selects 2 人+2 AI or 3 人+1 AI from the current registration count.
- Lock the roster after AI teams join, seed the four-team bracket as 1v4 and
2v3, and keep AI teams out of the human preparation order while preserving
independent AI operations and story archives.
## alpha 0.6.4.1015
- Add a reviewed 2-8 player CS Major autochess extension with a fixed
five-player starting lineup, one always-active coach, an eight-slot bench,
and a six-candidate shop that shows complete game data.
- Ship a fixed 60-player local pool with rifle, sniper, and in-game-leader
roles; team and nation synergies; shared copy counts; and deterministic
three-copy/two-star and nine-copy/three-star upgrades.
- Keep all five player slots occupied. Lineup changes are one-for-one swaps,
starters cannot be sold directly, and coach upgrades only add a 2%-10%
team bonus.
- Add player-selected firepower, stability, and clutch tactics. Resolve
stability over firepower, firepower over clutch, clutch over stability,
plus sniper/rifle/leader role counters before bounded +/-2% map variance.
- Generate the competition format once at tournament start: two players go
directly to the final, three play a round robin for two final places, four
use seeded 1v4 and 2v3 semifinals, and five to eight play a complete round
robin for four semifinal places.
- Add an explicit three-human-plus-one-AI test mode. The host can add one
deterministic local AI as seed four; it buys players, upgrades its coach,
swaps its strongest lineup, and selects a counter tactic without an online
model.
- Give the bot a persistent host state machine: welcome and format briefing,
rotating human preparation order, turn enforcement, explicit handoffs,
round introductions, matchup calls, overtime narration, advancement, and
championship presentation.
- Run group matches as BO3 and knockout matches as BO5. Simulate every map
under MR15, including 15-15 overtime, 19-17/22-20 scores, and separately
archived overtime stories.
- Persist run state, idempotent action responses, structured battles, and
every generated story in serialized SQLite transactions. Invalid archives
roll back the full action.
## alpha 0.6.3.1015
- Replace instant personal lottery tickets with one persistent multiplayer
lottery round per group: start, register/update, status, draw, and cancel.
- Treat each ticket as three distinct digits from 0-9. Sort digits before
storage so `347` and `743` represent the same selection.
- Accept spaced and compact OCR forms. Ticket stakes are 1-500 coins with a
default of 10 when omitted.
- Return 0%, 50%, 120%, or 300% of the stake for matching zero, one, two, or
three drawn digits. All settlement values remain integer coins.
- Commit entry updates, stake differences, all-player payouts, cancellation
refunds, and event replay responses in serialized SQLite transactions.
- Raise the blackjack stake range to 1-500 coins.
- Add per-game personal sequence numbers and `/游戏记录` (`/gamehistory`) for
prize draws, lottery rounds, and blackjack hands.
- Prefix game output with the configured player label and game number, such as
`Player #1:...`; aliases share the same counter and account.
## alpha 0.6.2.1015
- Bound direct DeepSeek SSE reads to 30 seconds of total wall time in addition
to the existing 18-second socket inactivity timeout.
- Stop reading as soon as the SSE `[DONE]` marker arrives instead of waiting
for the upstream connection to close.
- Prevent a trickling or unfinished upstream response from occupying both
generation workers for about 15 minutes and delaying later final replies.
- Preserve fast ACK delivery, three-attempt retry behavior, two generation
workers, and strict final-reply ordering.
- Treat `/21点 开始 100` as a 100-coin stake and accept OCR forms with missing
whitespace, such as `/21点开始100` and `/blackjackstart100`.
- Keep blackjack play as explicit steps: start and deal first, then hit or
stand in a separate command. Combined forms return this workflow without
opening a game or deducting funds.
- Prefix blackjack hands, status and settlement replies with the current
group-scoped player's configured display label, such as `player:...`.
## alpha 0.6.1.1015
- Add three reviewed plugins: local context summaries for 100/200/300 messages,
person and alias lookup, and single-player virtual coin games.
- Provide corresponding Chinese and English commands, including `/context`,
`/people`, `/person`, `/wallet`, `/draw`, `/lottery`, and `/blackjack`.
- Grant each group-scoped person 1000 virtual coins once; persist account state,
private blackjack decks, timeout/forfeit rules, and win/loss statistics.
- Commit funds, game state and event replay responses in one SQLite transaction;
retries never debit twice, and aliases share the canonical person's account.
- Reconcile the message image after a two-second idle wait so a missed final
XDamage notification does not strand an isolated message; drain buffered Xlib
events before waiting on the socket. Preserve visual deduplication and guards.
- Keep the generated feature list within the sender's 900-character boundary.
- Document local extractive-summary coverage, OCR identity limitations,
game payouts, persistence and rollback behavior.
## alpha 0.6.0.1015
- Replace repository-local and hardcoded deployment values with independent,
@@ -32,18 +240,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.
+42 -3
View File
@@ -8,7 +8,15 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。
当前版本:`alpha 0.6.0.1015`。
当前版本:`beta 1.0.2.1016`,更新日期:2026-10-16。
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则
已经外置为包内 JSON,虚拟金币支持转账、贷款、还款和群内排行榜。
完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见
[CHANGELOG](CHANGELOG.md)。
### 运行流程
@@ -72,6 +80,21 @@ 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点 要牌|停牌|状态|结束
/21点 多人 启动;/21点 加入;/21点 多人 开始;/21点 放200
/游戏记录 [页码](/gamehistory)
/游戏帮助(/games)
/自走棋开始|报名|开赛|状态|档案
/自走棋加AI战队(两名或三名真人测试模式)
/选1~6(教练轮为 /选1~10);/刷新;/锁定
/战术 火力|稳定|残局;/阵容;/上阵 备战编号 [首发位];/出售 备战编号;/结束回合
/丢骰子 [NdM]
/猜数字 开始 [上限]
/石头剪刀布 [开始|加入|出|状态|结算|结束]
@@ -240,9 +263,17 @@ visually confirmed GUI bridge.
Current version:
```text
alpha 0.6.0.1015
beta 1.0.2.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 +339,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 +369,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
+1 -1
View File
@@ -1 +1 @@
alpha 0.6.0.1015
beta 1.0.2.1016
+17 -1
View File
@@ -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" \
+126
View File
@@ -0,0 +1,126 @@
# JWeBot Command Catalog
Current baseline: `beta 1.0.1.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. |
| `/21点 多人 启动` | - | Open a Taffy-hosted blackjack table and auto-seat the host. |
| `/21点 加入` | - | Join the current blackjack table before betting starts. |
| `/21点 多人 开始` | - | Host-only close joining and start ordered betting prompts. |
| `/21点 放金额` | - | Place the prompted table stake, 1-10000 coins. |
| `/游戏记录 [页码]` | `/gamehistory` | Show indexed personal game history. |
| `/游戏帮助` | `/games` | Show local game rules. |
## Random And Session Games
| Command | Alias | Purpose |
|---|---|---|
| `/丢骰子 [NdM]` | - | Roll dice. |
| `/丢骰子挑战` | `/骰子挑战` | Run a dice challenge. |
| `/翻硬币` | - | Flip a coin. |
| `/随机数 [范围]` | - | Draw a random number. |
| `/抽签 [内容...]` | - | Draw lots from provided options. |
| `/今日运势` | - | Show a deterministic daily fortune. |
| `/猜数字 开始 [上限]` | - | Start a group guessing game. |
| `/猜数字 数字` | - | Guess in the active guessing game. |
| `/石头剪刀布 开始` | - | Start a rock-paper-scissors session. |
| `/石头剪刀布 加入` | - | Join the active session. |
| `/石头剪刀布 出 石头|剪刀|布` | - | Submit a move. |
| `/石头剪刀布 状态` | - | Show session status. |
| `/石头剪刀布 结算` | - | Settle the session. |
| `/石头剪刀布 结束` | - | End the session. |
## Steam
| Command | Alias | Purpose |
|---|---|---|
| `/steam 在线` | - | Show configured Steam online status. |
| `/steam 游戏` | - | Show current games. |
| `/steam 全部` | - | Show full configured Steam presence. |
| `/steam 刷新` | - | Refresh cached Steam data. |
| `/steam 时长 游戏名` | `/steam时长游戏名` | Query playtime for a game. |
| `/steam时长游戏名、玩家名` | `/steam时长游戏名人=玩家名` | Query one player's game playtime. |
## CS Major Autochess
| Command | Alias | Purpose |
|---|---|---|
| `/自走棋开始` | - | Create a new IEM 盐田 Major season. |
| `/自走棋报名` | - | Register the current player. |
| `/自走棋加AI战队` | - | Add local deterministic AI teams for testing. |
| `/自走棋开赛` | - | Start the event from registration. |
| `/自走棋状态` | - | Show the current event state. |
| `/自走棋档案` | - | Show archived event information. |
| `/选1` ... `/选10` | - | Draft coaches, players, or shop cards depending on phase. |
| `/刷新` | - | Refresh the shop. |
| `/锁定` | - | Lock or unlock the shop. |
| `/阵容` | - | Show lineup, bench, coach, gold, and synergies. |
| `/赛程` | - | Show the current event schedule. |
| `/升级教练` | - | Upgrade the team's coach level when rules allow it. |
| `/上阵 备战编号 [首发位]` | - | Move a bench player into the starting lineup. |
| `/下阵 首发位` | - | Move a starting player out when rules allow it. |
| `/出售 备战编号` | - | Sell a bench player. |
| `/战术 火力|稳定|残局` | - | Set the team's tactical focus. |
| `/结束回合` | - | Mark readiness and advance when required players are ready. |
## Notes
- Commands must be sent in the configured WeChat group and normally mention the
bot, depending on deployment configuration.
- OCR whitespace loss is tolerated for selected compact forms such as
`/steam时长Dota2` and `/21点开始100`.
- Real profiles, Steam IDs, person context, message payloads, and secrets are
never stored in this repository.
+9 -3
View File
@@ -1,7 +1,7 @@
# Command Registration And Execution Paths
Status: authoritative local registration and routing contract for
`alpha 0.6.x`.
`beta 1.0.x`.
## Three Execution Paths
@@ -25,7 +25,8 @@ error. They never fall through to DeepSeek or OpenClaw.
The local command path is:
1. XDamage reports a relevant message-region change.
1. XDamage reports a relevant message-region change, or a two-second idle
reconciliation capture detects a missed final drawing.
2. OpenCV segments incoming bubbles without running OCR over the whole screen.
3. The OCR engine reuses exact visual-fingerprint matches from a bounded LRU.
4. Only new or changed sender/body crops use RapidOCR.
@@ -45,6 +46,10 @@ because each bubble position still produces its own `IncomingMessage`.
Changed pixels force fresh OCR. The cache is bounded and periodically
revalidated.
For the local plugins introduced during the 0.6.x line, including bilingual
commands, person identity limitations and atomic single-player game settlement,
see [local plugin rules](local-plugins-0.6.1.md).
## Bundled Plugin Registration
Bundled command metadata has one source:
@@ -141,7 +146,8 @@ descriptors unless they are strictly Kernel-only. The current official
operational commands are:
- `/状态` and `/status`: server, Edge, OCR, and queue status.
- `/version`: installed JWeBot package versions.
- `/version`: installed JWeBot package versions plus the latest changelog date
and summary.
- `/debug`: recent privacy-safe telemetry p50/p95/max summary.
These commands delegate to Kernel-owned service profiles through generated
+9
View File
@@ -90,6 +90,15 @@ Steam uses `JWEBOT_EDGE_STEAM_ID64` and
- Host timeout and fallback;
- GUI sender timeout.
The OCR runtime also accepts optional environment overrides for constrained
hosts:
- `WECHAT_OCR_ONNX_INTRA_OP_THREADS` defaults to `1`;
- `WECHAT_OCR_ONNX_INTER_OP_THREADS` defaults to `1`.
These values limit RapidOCR's ONNXRuntime CPU thread pools so service restarts
do not temporarily saturate the host while the OCR cache is cold.
### Debug
`components/debug.toml` generates:
+678
View File
@@ -0,0 +1,678 @@
# CS Major 自走棋设计稿
本文档定义一个完全本地、确定性结算、以 CS 职业选手为棋子的群聊
小游戏方案。赛事包装统一为“第 n 届 IEM 盐田 Major”。AI 默认负责主持、
裁判、播报和剧情生成;主持人也可显式加入一支本地确定性 AI 战队。
## 1. 设计目标
1. 做成一个多人报名、多人运营、多人对战的群聊自走棋。
2. 玩法重心放在每轮选秀和阵容运营,而不是复杂微操。
3. 用 Major 赛事包装训练赛、小组单循环、淘汰赛和终局剧情。
4. 结算逻辑必须完全本地、可重放、可持久化、可回档验证。
5. 每一段剧情都要落档,且剧情只能在数值结算之后生成。
## 2. 核心体验
一句话定义:
`三轮赛前选秀 -> 轮次训练赛拿资源 -> D 牌与布阵 -> BO3 小组单循环 -> BO5 淘汰赛 -> 保存每轮赛报与剧情`
玩家感受到的乐趣应当主要来自:
- 第一轮从固定 10 人教练池里抢体系教练;
- 第二、三轮公开选手池里抢关键卡;
- 决定要不要保经济还是强行 D 牌;
- 赌阵容成型时点;
- 看小组单循环和淘汰赛赛报;
- 看最终冠军剧情与本届 Major 档案。
## 3. 对局对象与规模
### 3.1 报名
- 开局命令由任意玩家发起。
- 其他玩家在报名窗口内加入。
- 建议一局支持 `2~8` 名真人玩家。
- 主持人可在报名阶段发送 `/自走棋加AI战队`:
- 两名真人时加入 AI 战队 A、B;
- 三名真人时加入一支 AI 战队。
- 两种测试模式都补满四队,先进行 BO3 小组单循环,再按排名生成
`1v4`、`2v3` 半决赛。
- AI 占一个参赛席位,但不对应群成员,也不调用在线模型。
- AI 使用确定性规则选秀、买牌、调整首发和选择克制战术;为便于测试,
AI 阵容战力会有明确折扣。
### 3.2 玩家初始状态
每位玩家开局拥有:
- 生命值:`100`
- 金币:`10`
- 教练:赛前第一轮选秀决定
- 初始选手:赛前第二、三轮选秀决定,系统随后补齐阵容
- 阵容位:固定 `5 个选手位 + 1 个教练位`,比赛时必须全部上场
- 备战席:`3 格`
- 连胜/连败计数:`0`
- 本局独立事件档案编号
三轮选秀完成后,系统按已选核心确定性补齐每队 `5 名首发 + 3 名备战`
和常驻教练,保证训练赛开始时就是完整阵容。
## 4. 选手池设计
### 4.1 选手来源
只使用“近期非常出名”的职业选手。第一期池子优先覆盖以下俱乐部与明星:
- Spirit
- Vitality
- Falcons
- G2
- FaZe
- NAVI
- MOUZ
- The MongolZ
- Astralis
- Liquid
- FURIA
- Eternal Fire
选手数据不是对真实比赛数据的一比一复刻,而是基于公众认知做游戏化映射。
0.6.7 起本地维护 `100+` 名棋子和 `18` 名教练元数据;运行期不在线抓取
或改写数值。首轮教练选秀仍固定展示 10 个候选,保证聊天窗口可读。
0.6.9 起,公开游戏内容不再内联在 `cs_autochess.py` 中,而是维护在
`packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/`:
`players.json` 保存选手卡,`coaches.json` 保存教练卡,
`narratives.json` 保存战报和剧情素材,`rules.json` 保存经济、商店、
选秀、羁绊、赛制、MR15/加时和特殊事件规则。运行时启动会校验 schema、
重复 ID、必要字段和叙事素材数量。
### 4.2 费用层级
选手分为 `1~5 费`,越高费越稀有,基础数值和成长更强。
示例分层:
- `5 费`:donk、ZywOo
- `4 费`:m0NESY、NiKo、ropz、frozen
- `3 费`:apEX、b1t、xertioN、w0nderful、malbsMd
- `2 费`:辅助型或中坚选手
- `1 费`:前期过渡与羁绊补齐牌
正式实现时应维护一份固定的选手静态表,不允许运行期随意漂移。
### 4.3 定位
只保留三个核心定位:
- `步枪手`
- `狙击手`
- `指挥`
定位不是羁绊,而是战斗中的属性倾向与技能触发来源。
### 4.4 羁绊
只保留两类羁绊:
1. `战队羁绊`
2. `国家羁绊`
战队羁绊可来自:
- 当前主战队;
- 历史待过的关键战队;
- 每名选手最多挂 `3` 条战队标签,避免过度泛化。
国家羁绊只有 `1` 条。
## 5. 选手数值
### 5.1 基础数值轴
为避免面板过宽,第一期只展示三个主数值:
- `火力`
- `稳定`
- `残局`
其中指挥位额外拥有隐藏或显式的 `指挥` 修正值,用于影响全队。
### 5.2 数值范围
- 1 费:`52~68`
- 2 费:`60~76`
- 3 费:`68~84`
- 4 费:`78~92`
- 5 费:`88~98`
要求同费位内部也要有明显差异,不能所有 4 费都只有一两点波动。
### 5.3 派生战斗权重
可先采用以下简单模型:
`单卡战力 = 火力 * 0.45 + 稳定 * 0.30 + 残局 * 0.25 + 星级修正 + 羁绊修正 + 教练修正`
若定位是狙击手,可给火力更高上限;
若定位是指挥,可给团队修正更高权重。
### 5.4 属性与定位克制
战斗不能只比较总战力后掷骰子。三项主属性形成闭环:
- `稳定克火力`:稳定阵容能吸收强攻的第一波冲击;
- `火力克残局`:前期人数优势会压缩残局发挥空间;
- `残局克稳定`:高残局能力能打破按部就班的默认局面。
三个定位也形成轻量闭环:
- `狙击手克步枪手`
- `步枪手克指挥`
- `指挥克狙击手`
玩家每回合可选择 `/战术 火力|稳定|残局`。战术选择会强化对应属性,
并在克制对方战术时获得额外修正。属性、定位和战术的总克制修正上限
应明显高于随机波动,让调阵和判断对手比运气更重要。
## 6. 升星规则
- `3 张同名 = 2 星`
- `9 张同名 = 3 星`
升星收益建议:
- 2 星:基础战力乘 `1.35`
- 3 星:基础战力乘 `1.80`
升星后保留原有羁绊与定位,不改变标签。
## 7. 教练系统
教练位不是普通战斗单位,但始终与 5 名选手一起上场。
### 7.1 教练等级作用
教练只提供额外的全队战术加成,不控制上场人数,也不占用选手位。
建议加成:
- 1 级:全队战力 `+2%`
- 2 级:全队战力 `+4%`
- 3 级:全队战力 `+6%`
- 4 级:全队战力 `+8%`
- 5 级:全队战力 `+10%`
### 7.2 教练获取
教练由第一轮公共选秀决定,本届赛事内不能用金币升级教练。不同教练
自带等级、属性倾向和全队加成。
高费选手的商店概率随赛事进程推进,不再绑定教练等级。
## 8. 每轮 6 人选秀
这是本模式的核心。
### 8.1 固定规则
- 每轮只展示 `6` 名候选选手。
- 这 `6` 人对所有玩家公开可见。
- 每位玩家始终保有 5 名首发;购买的新选手进入备战席或与同名选手合成。
- 调整阵容时必须用备战席选手替换首发,不能让首发少于 5 人。
- 第一版采用“公开同池、各自购买”的简化方案:
- 所有人看到同一组 6 人;
- 某个玩家选择后,不会把该候选从其他玩家界面里抢空;
- 但底层共享总卡池仍然会影响后续刷新概率。
- 每名候选都必须展示完整数据:
- 编号 `1~6`
- 选手名
- 费用
- 定位
- 国家
- 主战队
- 历史战队
- 主数值
- 玩家通过编号选择,不输入复杂自然语言。
### 8.2 选秀指令
建议指令面如下:
```text
/自走棋开始
/自走棋报名
/自走棋加AI战队
/自走棋状态
/选1
/选2
/选3
/选4
/选5
/选6
/选7
/选8
/选9
/选10
/刷新
/锁定
/战术 火力|稳定|残局
/上阵 备战编号 [首发位]
/出售 备战编号
/阵容
/赛程
/结束回合
```
中文是主命令,英文别名可以后补。
### 8.3 刷新与锁定
- 第一轮教练选秀展示固定 10 名教练;第二、三轮选手选秀展示 6 名选手。
- 训练赛和正赛运营阶段默认展示一组 6 人商店。
- 玩家可支付金币执行刷新。
- 玩家可锁定本轮 6 人,使下一轮仍保留同一组候选。
### 8.4 顺位
- 开赛后三轮公共选秀,第一轮选教练,第二、三轮选选手。
- 第二轮使用反向顺序,避免种子靠前者连续先手。
- AI 轮次会自动选择,真人只在轮到自己时使用 `/选N`。
第一期建议先做“公开同池、各自结算”,避免群聊交互过重。
## 9. 回合流程
建议一整回合拆成五段:
1. `回合开始播报`
2. `6 人选秀 / 刷新 / 升级 / 调阵`
3. `训练赛 / 赛事挑战 / PvP 结算`
4. `回合结算与经济发放`
5. `剧情归档`
## 10. 训练赛与赛事挑战
训练赛不再是阈值型 PvE。它是正式小组赛前的多轮真实对阵,用来给玩家
检验阵容、获取金币并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内
插曲出现,但只提供资源变动,不淘汰玩家。
### 10.1 活动类型
- 训练赛
- 媒体日
- 赞助挑战
- 本地陪练队热身
### 10.2 训练赛作用
训练赛按参赛队伍生成 round-robin 轮次。四队测试局会有三轮训练赛,
每队在训练阶段遇到另外三支队伍;奇数队伍时轮空队会与本地陪练队热身。
训练赛只发放金币,不记录小组胜负,也不扣生命。
每轮训练赛结束后进入运营窗口:
- `/刷新` 即 D 牌,消耗金币换一组 6 人商店;
- `/选1~/选6` 买入商店选手,重复卡自动累积星级;
- `/锁定` 保留当前商店;
- `/上阵` 调整 5 人首发;
- `/战术 火力|稳定|残局` 改变下一场的回合倾向。
### 10.3 训练赛结算逻辑
训练赛使用与正式比赛相同的单图 MR15 逐回合模拟,只是赛制为 BO1。
胜方获得更多金币,负方也有保底金币,保证测试局能继续运营。
媒体日和赞助挑战根据阵容完整度、教练、战术和本地确定性随机因子发放
金币,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。
## 11. PvP 赛制
### 11.1 赛事包装
整局赛事名称固定为:
`第 n 届 IEM 盐田 Major`
### 11.2 按参赛人数生成赛制
- `2 人`:直接进入决赛。
- `3 支队伍`:进行完整单循环,前 2 名进入决赛。
- `4 人`:不打小组赛,按报名种子直接进行 `1v4`、`2v3` 半决赛。
- `5~8 人`:进行完整小组单循环,每两支队伍只交手一次,前 4 名晋级。
奇数队伍的小组比赛日会有一队轮空,但不会获得胜场。
小组赛每场采用 `BO3`。
### 11.3 小组排名
小组赛首先按胜场排序;同胜场依次比较:
1. 相互交手结果;
2. 当前完整五人阵容战力;
3. 剩余生命值;
4. 固定种子编号。
所有配对在开赛时一次性生成并落档,不允许运行中根据结果重排赛程。
### 11.4 淘汰赛
- 四强固定为 `小组第 1 vs 第 4`、`第 2 vs 第 3`。
- 3 人赛制由小组第 1、2 名直接进入决赛。
- 4 人赛制使用报名种子 `1v4`、`2v3`。
- 2 人赛制直接进入决赛。
- 半决赛胜者进入总决赛,决出本届冠军。
- 半决赛和总决赛均采用 `BO5`。
## 12. 战斗结算
### 12.1 目标
战斗要做到:
- 本地确定性;
- 单轮可解释;
- 支持战报;
- 不依赖在线模型。
### 12.2 简化模型
建议第一期采用“阵容总分 + 对位修正”的轻量模型。
#### 步骤 1:计算阵容基础战力
`阵容基础战力 = 5 名首发选手战力总和`
#### 步骤 2:加上团队修正
团队修正来源:
- 战队羁绊
- 国家羁绊
- 教练等级提供的全队百分比加成
- 装备/战术卡
- 连胜或特殊事件 buff
#### 步骤 3:计算克制修正
依次计算:
1. `稳定 > 火力 > 残局 > 稳定` 的属性对位;
2. `狙击手 > 步枪手 > 指挥 > 狙击手` 的定位对位;
3. 双方本回合主动选择的战术对位。
克制结果必须进入结构化战报,明确展示双方修正值。
#### 步骤 4:加入小幅波动
只允许 `-2% ~ +2%` 的确定性哈希波动。单次运气不能覆盖成型阵容的
属性、定位和战术克制优势。
#### 步骤 5:计算比分
单图采用 `MR15`:
- 常规时间先到 `16` 分获胜;
- 常规时间 `15-15` 进入加时;
- 加时可产生 `19-17`,多轮加时可产生 `22-20` 等比分;
- 每次加时除结构化单图结果外,必须额外生成一段加时剧情并落档。
小组赛按 BO3 先赢 2 图,淘汰赛按 BO5 先赢 3 图。系列赛战报必须展示
大比分、每张图比分、加时次数和克制修正。
## 13. 剧情系统与落档
这是本模式的强制要求。
### 13.1 归档原则
必须先结算,后写剧情。
即:
1. 固化数值结果;
2. 存档回合事实;
3. 基于事实生成文字赛报;
4. 再把文字赛报作为附加档案保存。
### 13.2 必须落档的节点
- 开局报名完成
- 每轮 6 人选秀结果
- 每次刷新和关键升星
- 每轮训练赛和赛事挑战结果
- 每场 PvP 结果
- 小组赛排名、晋级和淘汰节点
- 淘汰赛对阵与结果
- 冠军总结
### 13.3 档案结构
建议至少保存两类记录:
1. `结构化事件档`
2. `文本剧情档`
结构化事件档用于回放和调试;
文本剧情档用于玩家阅读。
示例:
```json
{
"run_id": "major_0001",
"round": 4,
"phase": "group_2",
"event_type": "pvp_result",
"player_a": "Jacky",
"player_b": "Alice",
"winner": "Jacky",
"score_band": "close_win",
"roster_a_power": 418,
"roster_b_power": 403
}
```
## 14. 数据持久化设计
建议新增以下 SQLite 表。
### 14.1 对局主表
`autochess_runs`
- run_id
- group_fingerprint
- season_number
- state
- created_at
- updated_at
### 14.2 玩家状态表
`autochess_players`
- run_id
- person_key
- display_name
- hp
- gold
- coach_level
- coach_xp
- wins
- losses
- streak
- board_json
- bench_json
- shop_json
- locked_shop
### 14.3 选秀记录表
`autochess_shop_events`
- run_id
- round_number
- person_key
- visible_pool_json
- action_type
- action_payload_json
- created_at
### 14.4 战斗结果表
`autochess_battles`
- run_id
- round_number
- phase
- battle_type
- player_a
- player_b
- result_json
- created_at
### 14.5 剧情归档表
`autochess_story_logs`
- run_id
- round_number
- phase
- story_type
- story_text
- facts_json
- source_event_id
- created_at
### 14.6 幂等与重放表
`autochess_action_replay`
- run_id
- person_key
- round_number
- event_id
- action_kind
- response_text
- created_at
## 15. 回放与幂等
因为群聊环境会出现重复事件、OCR 重读和命令重放,必须保证:
- 同一事件只执行一次;
- 同一回合不能重复发奖或重复扣款;
- 同一段剧情不能重复写多份版本;
- 回合失败时整段事务回滚。
建议沿用现有本地游戏的幂等思路,为自走棋单独增加 action replay 表。
## 16. 指令交互风格
建议所有回复都遵循现有小游戏风格:
多段回复优先。一次主持动作可以拆成欢迎语、参赛名单、种子顺序、候选池、
状态面板、操作指引等多条消息,每条保持在发送上限内。
例如:
```text
欢迎来到第一轮选秀,本轮选教练。
选择顺序:Jacky、Alice、AI 战队 A、AI 战队 B
1. zonic|5级教练|冠军体系|火78 稳92 残88|全队+9%
...
现在轮到 @Jacky 做出选择,使用 /选1~/选10。
```
### 16.1 Taffy 主持状态机
主持人必须承担完整赛事主持,而不是只在结算时出现;输出不需要每句都带
`Taffy:` 前缀。
开赛时必须包含:
- `欢迎大家来到第 n 届 IEM 盐田 Major`
- 本届参赛者和种子
- 当前人数对应的完整赛制
- 小组赛 BO3、淘汰赛 BO5、单图 MR15 和加时规则
- 三轮选秀规则和当前选择玩家
- 训练赛、小组赛和淘汰赛的当前阶段
赛前选秀按顺序逐人选择。进入阵容准备、训练赛运营和小组赛运营后,
所有相关真人发送 `/结束回合` 即可自动推进,不需要主持人手动结算。
淘汰赛只等待当前对阵中的真人,已经淘汰的玩家不会再被提示准备。
```text
Jacky 选择了 zonic!
现在轮到 @Alice 做出选择,使用 /选1~/选10。
训练赛第 1/3 轮对阵:Team Jacky vs Team Alayak;AI 战队 A vs AI 战队 B
本轮训练结束。现在是运营窗口:/刷新 是 D 牌,/选1~/选6 是买店内选手;
所有真人发送 /结束回合 后进入下一轮训练。
```
以下节点都必须由 Taffy 主持并落档:
- 报名开启和新选手加入
- AI 测试战队加入
- 赛制与种子公布
- 三轮选秀候选池、选择结果和选秀完成
- 每轮训练赛对阵、比分、金币和运营提示
- 媒体日或赞助挑战
- 每场 BO3/BO5 对阵与结果
- 每张加时地图
- 小组排名和前四晋级
- 1v4、2v3 半决赛
- 总决赛和冠军颁奖
## 17. 实现顺序建议
建议按三个阶段落地。
### 第一阶段:最小可玩版
- 建局 / 报名 / 状态
- 每轮 6 人选秀
- 买卡、卖卡、升星
- 教练等级
- 简化训练赛
- 简化 PvP
- 基础剧情归档
### 第二阶段:赛事化增强
- 小组单循环完整配对
- 1v4、2v3 淘汰赛树
- 羁绊与装备系统细化
- 战报模板优化
### 第三阶段:内容扩展
- 在 100+ 人本地卡池基础上增加新赛季棋子
- 更多俱乐部和国家
- 特殊赛事事件
- 更细的剧情风格与本届 Major 年鉴
## 18. 第一版明确不做的内容
为保证能上线,第一版先不做:
- 实时地图微操
- 复杂技能动画
- 过多职业分类
- 在线爬取实时战队数据
- 大量开放文本输入解析
## 19. 最终结论
这套模式最适合按“强运营、轻操作、重赛报”路线实现。
最重要的三个锚点已经确定:
1. `每轮固定 6 人公开选秀`
2. `训练赛与挑战负责资源,PvP 负责小组单循环与淘汰赛`
3. `每一段剧情都必须落档`
如果按这个设计推进,实现出来的成品会更像一场可回放、可复盘、
可连载的本地 Major 模拟器,而不是一个只有数值结算的普通抽卡游戏。
+68
View File
@@ -0,0 +1,68 @@
# alpha 0.6.3.1015 多人彩票与游戏记录
## 一把彩票怎么玩
1. 任意群友发送 `/彩票 开始`。发起人是本轮主持人。
2. 每个人发送 `/彩票 报名 3 4 7 100`,表示选择 3、4、7,下注 100
金币。也可简写为 `/彩票347100`;省略金额时默认下注 10。
3. 发送 `/彩票 当前状态` 查看本轮主持人、报名人数、每个人的号码和下注。
4. 主持人发送 `/彩票 开奖`。机器人从 0~9 中抽取三个互不重复的数字,
统一排序后结算。
5. 主持人也可在开奖前发送 `/彩票 取消`,所有报名者全额退款。
命令关键词和数字之间的空格均可省略。`/买彩票` 和 `/lottery` 保留为
对应别名。
## 选号与报名
- 每张票必须是三个互不重复的 0~9 数字。
- 数字顺序不影响结果:`347`、`743` 和 `3 4 7` 都保存为 `3 4 7`。
- 每人每把只有一张票。再次报名会更新号码和下注,不增加个人局号。
- 更新到更高下注时只补扣差额;降低下注时退回差额。
- 下注范围是 1~500,省略时为 10。
- 没有进行中的彩票时不能报名;没有报名者时不能开奖。
- 只有创建本轮的主持人可以开奖或取消,状态查询对群内所有人开放。
支持的报名示例:
```text
/彩票 347
/彩票 743 100
/彩票 报名 3 4 7 100
/彩票报名347金额100
/彩票347100
/lottery join 347 100
```
## 返还规则
报名时先扣除下注。开奖号和玩家号码按集合比较,返还包含本金:
| 命中数 | 返还 |
|---|---:|
| 0 个 | 0% |
| 1 个 | 50% |
| 2 个 | 120% |
| 3 个 | 300% |
金币只使用整数;百分比计算的小数部分向下取整。例如下注 100,命中两个
返还 120,净变化为 `+20`。
## 个人局号与记录
抽奖、彩票和 21 点分别维护本人的游戏局号。第一次参与某种游戏显示
`Jacky #1:...`,下一把显示 `Jacky #2:...`。配置别名解析到同一人物时,
共用同一账户、局号和历史。
发送 `/游戏记录 [页码]`、`/个人记录 [页码]` 或
`/gamehistory [page]` 查看最近记录,每页五条。记录包含游戏、个人局号、
结果、下注、返还和净变化,最多保留最近 50 条。
21 点下注范围同步调整为 1~500。`/21点 开始 100` 只表示本局下注 100;
发牌后再单独发送 `/21点 要牌` 或 `/21点 停牌`。
## 持久化与并发
彩票轮次、报名和结算保存在 Edge SQLite。报名/改注、所有玩家的开奖返还、
取消退款和事件重放响应都在 `BEGIN IMMEDIATE` 事务中提交。重复事件不会
重复扣款、发奖或增加局号;进程重启后未开奖轮次和个人记录仍然保留。
+131
View File
@@ -0,0 +1,131 @@
# 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 点使用 `/21点 多人 启动` 创建 Taffy 牌桌,开桌者自动入座。
其他玩家在下注前用 `/21点 加入` 进入当前牌桌;主持人发送 `/21点 多人 开始`
后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~10000。
所有玩家下注后统一发牌,轮到本人时用 `/21点 要牌` 或 `/21点 停牌` 操作;
结算按座位顺序列出每个人的下注、返还、净变化和当前金币。
自然 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 -1
View File
@@ -1,6 +1,6 @@
# Plugin Contract
Status: authoritative for the `alpha 0.6.x` extension API.
Status: authoritative for the `beta 1.0.x` extension API.
## Scope
+5 -3
View File
@@ -6,9 +6,11 @@ exception.
## Release Discipline
- Version format is `alpha X.Y.Z.MMDD`.
- Patch changes increment `Z`; minor version changes require explicit user
approval.
- Version format is `beta X.Y.Z.MMDD` for current beta releases.
- Python package versions use the matching PEP 440 beta form, such as
`X.Y.Zb1+MMDD`.
- Patch changes increment `Z`; minor, major, or stable-release changes require
explicit user approval.
- A release change must update:
- `VERSION`;
- root `pyproject.toml`;
+1 -1
View File
@@ -1,6 +1,6 @@
# Runtime Telemetry And ACK Semantics
Status: local instrumentation contract for `alpha 0.6.x`.
Status: local instrumentation contract for `beta 1.0.x`.
## What ACK Means
+10 -6
View File
@@ -1,20 +1,24 @@
# Versioning
JWeBot uses an alpha version string:
JWeBot uses a human-readable product version string:
```text
alpha X.Y.Z.MMDD
beta X.Y.Z.MMDD
```
Rules:
- `alpha` means the public API and deployment process may still change.
- `beta` means the project has passed the local, staging, privacy, and
production-safety gates expected for wider testing, while still allowing
controlled breaking changes before a stable release.
- `X.Y.Z` is the project version.
- `MMDD` is the delivery date.
- Python package versions use PEP 440 pre-release form. For beta builds,
`beta 1.0.1.1016` maps to `1.0.1b1+1016`.
- Normal small updates increment only the patch version, for example
`alpha 0.0.1.1014` to `alpha 0.0.2.1014`.
- Minor versions such as `0.1.x` require explicit user review and milestone
approval.
`beta 1.0.1.1016` to `beta 1.0.2.1016`.
- Minor, major, or stable-release changes require explicit user review and
milestone approval.
- Local repository migration does not imply a production deployment.
- Production services must not be changed unless the operator explicitly asks
for deployment or runtime verification.
+22
View File
@@ -0,0 +1,22 @@
# coin-loan
Author: `jwebot`
贷款
Help:
- `/贷款;/还款`
Registered commands:
- `/贷款`
- `/借款`
- `/loan`
- `/borrow`
- `/还款`
- `/repay`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-loan` Kernel service profile
and returns a structured `ReplyPlan`.
+14
View File
@@ -0,0 +1,14 @@
id = "coin-loan"
author = "jwebot"
description = "贷款"
help = ["/贷款;/还款"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_loan:plugin"
type = "command"
[capabilities]
commands = ["/贷款", "/借款", "/loan", "/borrow", "/还款", "/repay"]
storage = false
network_profiles = []
service_profiles = ["coin-loan"]
@@ -0,0 +1,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()
+20
View File
@@ -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()
+18
View File
@@ -0,0 +1,18 @@
# coin-taffy
Author: `jwebot`
Taffy金币
Help:
- `/Taffy金币`
Registered commands:
- `/Taffy金币`
- `/taffycoins`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-taffy` Kernel service profile
and returns a structured `ReplyPlan`.
+14
View File
@@ -0,0 +1,14 @@
id = "coin-taffy"
author = "jwebot"
description = "Taffy金币"
help = ["/Taffy金币"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_taffy:plugin"
type = "command"
[capabilities]
commands = ["/Taffy金币", "/taffycoins"]
storage = false
network_profiles = []
service_profiles = ["coin-taffy"]
@@ -0,0 +1,45 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'coin-taffy'
AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy金币'
HELP_ENTRIES = ('/Taffy金币',)
COMMANDS = ('/Taffy金币', '/taffycoins')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
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_taffy import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, 'jwebot')
self.assertEqual(DESCRIPTION, 'Taffy金币')
self.assertEqual(HELP_ENTRIES, ('/Taffy金币',))
self.assertIn('/Taffy金币', registered)
self.assertEqual(len(registered), 2)
if __name__ == "__main__":
unittest.main()
+18
View File
@@ -0,0 +1,18 @@
# coin-transfer
Author: `jwebot`
金币转账
Help:
- `/转账 对方 金额;/transfer`
Registered commands:
- `/转账`
- `/transfer`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `coin-transfer` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "coin-transfer"
author = "jwebot"
description = "金币转账"
help = ["/转账 对方 金额;/transfer"]
version = "1.0.0"
api_version = "1"
entrypoint = "coin_transfer:plugin"
type = "command"
[capabilities]
commands = ["/转账", "/transfer"]
storage = false
network_profiles = []
service_profiles = ["coin-transfer"]
@@ -0,0 +1,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()
+8
View File
@@ -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")
+8
View File
@@ -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,35 @@
# single-player-games
Author: `jwebot`
金币游戏
Help:
- `/金币 /wallet;/游戏记录 /gamehistory`
- `/抽奖 [金额];/draw`
- `/彩票 开始|报名|状态|开奖|取消;/lottery`
- `/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200`
- `/游戏帮助;/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点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", "/游戏帮助;/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点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/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点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/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`
上下文总结
Help:
- `/上下文 [100|200|300];/context [100|200|300]`
Registered commands:
- `/上下文`
- `/context`
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `context-summary` Kernel service profile
and returns a structured `ReplyPlan`.
@@ -0,0 +1,14 @@
id = "context-summary"
author = "official"
description = "上下文总结"
help = ["/上下文 [100|200|300];/context [100|200|300]"]
version = "1.0.0"
api_version = "1"
entrypoint = "context_summary:plugin"
type = "command"
[capabilities]
commands = ["/上下文", "/context"]
storage = false
network_profiles = []
service_profiles = ["context-summary"]
@@ -0,0 +1,45 @@
"""Generated reviewed adapter for context-summary."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'context-summary'
AUTHOR = 'official'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
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, '上下文总结')
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")
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "0.6.0a0+1015"
version = "1.0.2b1+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.2b1+1016"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "0.6.0a0+1015"
version = "1.0.2b1+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.2b1+1016",
"jwebot-kernel==1.0.2b1+1016",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "0.6.0a0+1015"
__version__ = "1.0.2b1+1016"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.0a0+1015"
version = "1.0.2b1+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.2b1+1016",
"jwebot-sdk==1.0.2b1+1016",
"jwebot-official-extensions==1.0.2b1+1016",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.0a0+1015"
__version__ = "1.0.2b1+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.2b1+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.2b1+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.2b1+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")
@@ -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,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,45 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'coin-taffy'
AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy金币'
HELP_ENTRIES = ('/Taffy金币',)
COMMANDS = ('/Taffy金币', '/taffycoins')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
@@ -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,45 @@
"""Generated reviewed adapter for context-summary."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'context-summary'
AUTHOR = 'official'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
@@ -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))
@@ -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,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))
@@ -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,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点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/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))
@@ -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,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
}
@@ -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
}
@@ -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
}
}

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