npm workspaces monorepo (root package.json has workspaces):
| Path | What | Runtime |
|---|---|---|
db-server/ |
SQLite HTTP REST backend (plain node:http + node:sqlite) |
Node.js >=22.13 |
qq-bridge/ |
QQ bridge (LLBot OneBot 11, WS 3002) | Node.js |
bds-tools/ |
BDS auto-updater + behavior-pack assembler | Node.js |
sfmc/ |
CLI management tool (REPL + supervisor), assembles SAPI BP at deploy time | Node.js |
modules/packages/<id>/ |
Per-module packages; each one a first-class citizen | Node.js + SAPI |
modules/sdk/@sfmc-sdk/ |
Shared SDK consumed by modules | mixed |
shared/sfmc-logs/ |
Shared logging library @sfmc-bds/logs |
Node.js |
panel/ no longer exists — replaced by sfmc/ CLI (the old AGENTS.md was stale).
The behavior pack is assembled at deploy time by sfmc behavior-pack build →
bds-tools/pack-manager#assembleBehaviorPack. The bundle entry walks every
enabled module's sapi/src/index.ts and emits a single scripts/main.js.
Init phases (inside the bundled main.js):
system.beforeEvents.startup—ConfigManager.init()→ register permissions & commands viaModuleRegistryworld.afterEvents.worldLoad—ModuleRegistry.bootAfterWorldLoad()+MonitorReporter+syncWorldData()world.afterEvents.playerSpawn(initialSpawn) — Peace, Fly, AFK resetworld.afterEvents.playerSpawn— SpawnProtectworld.beforeEvents.chatSend— intercept!/!commands
The BP has no checked-in shell. Everything lives in modules and SDK:
sfmc behavior-pack build # esbuild bundles modules/packages/<id>/sapi/src/index.ts → <ROOT>/packs/_build/sfmc-modules/
sfmc behavior-pack deploy # copies packs/_build/sfmc-modules/ into <BDS>/worlds/<level>/behavior_packs/sfmc-modules/Each module exports a ModuleRegistry.register({ id, lifecycle }) call from its
sapi/src/index.ts. The build pipeline walks every enabled module's entry and
bundles them in one go. To make changes load, run build && deploy and restart BDS.
npm run start # node index.js → sfmc CLI
npm run build # npm run build --workspaces
npm run lint # eslint . --ext .ts,.tsxTo build all workspaces: npm run build.
cd db-server
npm run dev # tsx src/index.ts
npm run start # node dist/index.js
npm run build # tsc -p tsconfig.json
npm run test # node --test src/*.test.js
DB_PORT=4000 npm run dev # override portPort defaults to 3001. Config: configs/db_config.json (db_port key). Auth via http_auth in same file or HTTP_AUTH env var.
cd bds-tools
npm run build # tsc → dist/
npm run update # node dist/check-update.js
npm run update:check # --check-only
npm run update:force # --force (+ download + overwrite)
npm run rollback # node recovery.js
npm run start|stop|status|watch # bds-manager commandsnode index.js # REPL (interactive)
node index.js status # print status and exit
node index.js start <svc> # start a service
node index.js stop <svc> # stop a service
node index.js restart <svc> # restart a service
node index.js init # setup wizard
node index.js update # BDS updateServices managed by SFMC_SERVICE env: db, qq, update, manager.
Source of truth: modules/catalog.json (local mirror projected from installed packages) + modules/module-lock.json (enable state). Business modules live in Tanya7z/sfmc-modules and are installed via tools/fetch-module.mjs.
tools/fetch-module.mjs install|uninstall|search— registry install; syncs catalog + locktools/catalog-sync.mjs— scanpackages/*/sapi/manifest.json→ rewrite catalogtools/check-modules.mjs— validate catalog + v2 manifests (empty catalog OK)tools/smoke-modules.mjs— module API smoke (needs live db-server)tools/check-ootb.mjs— platform readiness self-check
Runtime wiring: modules/sdk/@sfmc-sdk/src/module-loader/. To add a module:
- Publish in
Tanya7z/sfmc-modules(or install withfetch-module) ModuleRegistry.register({ id, lifecycle: { ... } })insapi/src/index.tssfmc behavior-pack build && sfmc behavior-pack deploy→ restart BDS
Plain JSON under configs/ (generated by each service on first ensure; JSON Schema under @sfmc-bds/sdk/schemas). No SQLite for configs.
- db-server reads
configs/db_config.json+configs/qq_config.jsondirectly at startup - SAPI calls
GET /api/sfmc/configs/allonce viaConfigManager.init(), caches in memory for process lifetime - Edit config → restart BDS. No polling, no reload command, no hot-reload.
- Module toggle via Panel→
POST /api/sfmc/modules/:id/{enable|disable}→ db-server writesmodule-lock.json→ SAPI callsConfigManager.refreshModules()
Key config endpoints:
| Route | Fallback |
|---|---|
GET /api/sfmc/settings/{key} |
bridge_channel_id → qq_config.json, land:* → land.json |
GET /api/sfmc/{areas,permissions,banned_items,clean,grids,peace_filters,qa} |
matching JSON file |
POST /api/sfmc/modules/:id/{enable|disable} is the only runtime config write.
- Message display:
Msg.info/success/error/warning/tips()from@sfmc-bds/sdk/sapi/runtime(adds§f[*]/§a[√]/§c[x]/§e[!]/§7[!]prefixes). Never useplayer.sendMessage()directly. - Form body:
ListFormInfo(string[])fromgui/— first line gets[*]prefix, indented lines are plain - Button/Form titles: No formatting codes (except
返回for back buttons) - Money: Scoreboard-based, unit from
Money.UNIT(节操) - Commands:
!<command>syntax intercepted inbeforeEvents.chatSend - Permissions:
Permission.register(name, level)at startup. Levels: Any=0, Member=1, OP=2, Admin=3 - Module guard:
Command.triggercallsmoduleGuardinternally — disabled modules block their commands - db-server HTTP: Via
HttpDBclass inlibs/HttpDB.ts, targets127.0.0.1:3001(hardcoded, not configurable from SAPI) - db-server auth:
http_authinconfigs/db_config.json→ Bearer token on all non-GET/POST-module endpoints
File: qq-bridge/index.js (shim → dist/index.js). Source in src/, compile with npm run build.
- Only exposes WS on port 3002 (LLBot reverse-ws). No HTTP port.
- MC→QQ goes directly from db-server to LLBot HTTP (port 3004 by default).
- Config:
configs/qq_config.json(keys:qq_ws_port,qq_group_id,bridge_channel_id,llbot_host/port/token,mctoqq_prefix)
QQ → MC: LLBot ─WS:3002──→ qq-bridge ─POST──→ db-server:3001/api/sfmc/messages
MC → QQ: db-server ─HTTP──→ LLBot:3004/send_group_msg
- self_id filter: drops messages where
sender.user_id === self_id - 5 second dedup: message_id short-term cache
1. db-server (node db-server/index.js or db-server/dist/index.js)
2. qq-bridge (node qq-bridge/index.js)
3. BDS
node tools/check-ootb.mjs # validate environment readiness
node tools/catalog-sync.mjs # project installed packages → catalog.json
node tools/check-modules.mjs # validate catalog + v2 manifests
node tools/smoke-modules.mjs # module regression (needs live db-server)
node tools/sim-new-user.mjs # isolated SFMC_ROOT smoke
node tools/fetch-module.mjs install <id>.github/workflows/ootb.yml — on push/PR to main/refactor/**:
npm installat repo rootnode tools/check-ootb.mjs- Spin up db-server, wait for
/api/health200, runtools/smoke-modules.mjs
.github/workflows/changeset-release.yml — on push to main: opens/updates Version Packages PR; merging it runs npm run ci-release-packages (publish + tag + GitHub Release; currently beta via pre mode).
Local: npm run prerelease-packages (pre/beta) or npm run release-packages (after changeset pre exit).
.github/workflows/npm-publish.yml — emergency workflow_dispatch single-package publish (default --tag beta). Do not publish latest while .changeset/pre.json is in pre mode.
Shipping rule: any PR that changes a publishable package's public API/behavior must include a changeset (npx changeset), or the Version PR will not bump that package.
{ "trailingComma": "es5", "tabWidth": 2, "semi": true, "singleQuote": false,
"bracketSpacing": true, "arrowParens": "always", "printWidth": 120,
"endOfLine": "crlf", "plugins": ["prettier-plugin-organize-imports"] }- configs/ and data/ are gitignored. Missing config files are created by the owning service with built-in defaults (+
$schemafor IDE). SFMC_ROOTenv var: db-server readsconfigs/from this root. Used bysim-new-user.mjsfor isolation testing.- JSON Schema: see
modules/sdk/@sfmc-sdk/schemas/and committed.vscode/settings.jsonjson.schemasmappings. process.stdin.isTTY=falsemakes Ink-based TUI crash —panel/is gone, but sfmc CLI with--no-tuior--cliavoids stdin issues.endOfLine: "crlf"in prettier — Windows repo convention.- No test framework in SAPI or db-server proper. db-server has
node --testforsrc/runtime.test.jsonly.
The Cloud VM is Linux; the repo primarily targets Windows, but the Node services run fine on Linux (Node ≥22.13 provides unflagged node:sqlite; 22.5–22.12 require the --experimental-sqlite CLI flag or db-server crashes on startup with ERR_UNKNOWN_BUILTIN_MODULE). The update script only runs npm install. Everything below is required each session before running/verifying services.
- Build before running.
dist/is gitignored for@sfmc-bds/sdk,db-server,bds-tools, etc., and services run fromdist/. Runnpm run build --workspaces --if-present(builds the SDK first, then the services) afternpm install. Without it, imports like@sfmc-bds/sdk/node/configfail. configs/is gitignored — first start of db-server / sfmc / bds-tools writes missing JSON with built-in defaults. Noconfigs-default/tree.modulesDirdefault is"modules"(relative toSFMC_ROOT).- Run db-server (main service, port 3001):
SFMC_ROOT=$PWD node db-server/dist/index.js. Health:GET http://127.0.0.1:3001/api/health. The module/config REST surface is JSON-backed and is the CI-tested core path (GET /api/sfmc/modules/catalog,POST /api/sfmc/modules/:id/{enable|disable}). node:sqliteneeds Node ≥22.13, not just ≥22.5.db-serverimportsnode:sqliteat module scope. On 22.5.0–22.12.x the module is still gated behind--experimental-sqlite; importing it unflagged throwsERR_UNKNOWN_BUILTIN_MODULEand db-server exits immediately on startup. This is exactly what caused theootbGitHub Actions workflow to fail repeatedly (setup-nodewas pinned tonode-version: '22.5', which resolves to22.5.1) —check-ootb's "db-server 启动 + 模块接口" step timed out waiting for/api/health, andsim-new-user/smoke-modulescascaded into the same timeout. Fixed by pinning CI to22.13+.- Pre-existing bugs (not environment issues), fixed as of this note — kept here for history:
tools/check-catalog.jsused top-level ESMimportsyntax while the repo rootpackage.jsonhas no"type": "module"→SyntaxError: Cannot use import statement outside a moduleunder plainnode. Converted to CommonJSrequire()to match every othertools/*.jsscript.tools/check-ootb.js,tools/sim-new-user.js, andtools/test-db-api.jsspawneddb-server/index.js, which does not exist — the real entry isdb-server/dist/index.js. This made thedb-server 启动andsim-new-userchecks intools/check-ootb.jstime out, so a healthy env used to show check-ootb 5/6 pass; both are now fixed and should pass 6/6 given a built workspace +--experimental-sqlite-capable Node.- The SQLite-backed gameplay/
economypath previously returnednear "?": syntax errorbecause table names were interpolated throughsql-template-strings(FROM ${TABLE}→FROM ?). Fixed viasql-helpers.sql()embedding trusted identifiers (#19/#21); keep usingsql()/.append(raw(...)), neverSQL\…${table}…`or bareSQL`…${raw(t)}…``.
npm run lintis broken out-of-the-box — ESLint v10 needs a flateslint.config.jsand none exists in the repo. Use per-workspacenpm run typecheckfor static checking instead.