本文面向在引擎上继续开发的工程师:环境与命令、代码导航、四类常见扩展的完整步骤、测试约定、性能要点与发布流程。部署与机制原理见架构与运维手册;写剧本/插件/客户端分别看剧本编写指南、插件开发指南、协议与错误码参考。

1. 环境准备与命令全表#

pnpm install        # 安装依赖(better-sqlite3 在 optionalDependencies,安装失败不影响 file 后端)
pnpm dev            # 启动服务(:3000,GET / 即示例客户端)

package.json scripts 逐条:

命令 实际执行 说明
pnpm dev tsx src/server.ts 开发启动;tsx 直跑 .ts,无 watch,改代码需自行重启
pnpm build tsc -p tsconfig.json 产出 dist/;生产 node dist/server.js,worker 编译池以 dist 为准
pnpm typecheck tsc --noEmit 仅类型检查
pnpm lint tsc --noEmit && tsx src/cli/lint.ts 类型检查 + 枚举 STORIES_DIR 全量剧本静态检查;有 error 退出码 1
pnpm test vitest run 全量测试(21 个文件、360 个用例)
pnpm coverage vitest run --coverage v8 覆盖率,阈值 80%(见 §4)
pnpm play tsx src/cli/play.ts 无头试玩:pnpm play stories/xxx.story;自动选第一个选项、自动应答 host、推进计时器
pnpm e2e:restart tsx scripts/e2e-restart.ts 端到端:停机 → 重启 → 旧 sessionId + lastSeq 重连续玩
pnpm e2e:plugins tsx scripts/e2e-plugins.ts 端到端:真实服务器 + 插件指令 + 两路事件 + 插件存档落盘
pnpm bench tsx scripts/bench.ts WS 压测,参数与解读见架构与运维手册 §8.3

scripts/ 下另有(无 npm script,用 npx tsx 直跑):

  • scripts/check-story.ts [file]:写章自检——严格解析、编译零 error、标签重复/坏跳转检查、统计节点与指令数。
  • scripts/smoke-isekai.ts:《晨星之环》全分支自动播放冒烟(注册全部游戏插件)。

2. 代码结构导航#

src/
├── server.ts               # 装配根:config → loader/存储 → 插件 → manager → ws/http → watcher
├── config.ts               # 环境变量 → ServerConfig(全部默认值唯一出处)
├── logger.ts               # pino 封装,moduleLogger(module) 建 child logger
├── engine/                 # 纯引擎,不依赖 session/transport
│   ├── lexer/              # token.ts(关键字表 KEYWORDS)→ lexer.ts(行感知扫描)
│   ├── parser/             # ast.ts(节点定义)→ parser.ts(递归下降 + recover 模式)
│   ├── compiler/           # compiler.ts(AST→指令)· optimizer.ts(折叠/不可达)
│   │                       # resolver.ts(标签/事件引用检查)· diagnostics.ts · instruction.ts
│   ├── vm/                 # vm.ts(状态机)· expr.ts(AST 求值器)· state.ts · vm-error.ts
│   ├── loader/             # story-loader.ts(jailPath 路径监狱 + sha1 缓存键 + reload)
│   ├── program-cache.ts    # 多会话共享只读 CompiledProgram
│   ├── compile-pool.ts     # worker 线程池(预热用,COMPILE_WORKERS>0)
│   ├── compile-worker.ts   # worker 入口(只 import 编译链)
│   ├── runtime/            # scheduler.ts(60Hz) · host-api.ts(注册表) · event-bus.ts
│   │                       # hot-reload.ts(chokidar+预热) · world.ts(形状助手)
│   └── loc/                # localization.ts(locId 查表)
├── host/                   # 机制层:server-host.ts(内置 API 装配) · combat/ · ai/bt.ts
│                           # pathfind.ts · audio-stub.ts
├── session/                # session.ts · session-manager.ts · session-store.ts · session-error.ts
├── transport/              # ws-server.ts · codec.ts · protocol.ts(zod) · http-routes.ts
├── persist/                # save-manager.ts · migrate.ts · schema.ts · storage.ts
│                           # fs-storage.ts · sqlite-storage.ts
├── kernel/plugin-system/   # registry/lifecycle/manager/capability/loader/manifest-*/dsl-*
└── util/                   # metrics.ts · async-queue.ts · sha1.ts · id.ts

依赖方向:engine ← host ← session ← transport/kernel ← server。engine 不反向依赖任何上层;session 通过 SessionSocket/SchedulerLike 等最小接口解耦传输与调度。

3. 常见扩展怎么做#

3.1 新增一个内置宿主 API#

  1. 打开 src/host/server-host.ts,在 registerAll() 里仿现有条目调用:

ts registerHostAPI(registry, { name: 'your_api', params: [{ name: 'id', type: 'str' }, { name: 'count', type: 'num', optional: true }], returns: 'num', // void | str | num | bool | any | promise clientSide: false, // true = 转发客户端执行,回 host_result 恢复 handler: (ctx, id, count) => { /* 同步执行 */ }, });

  1. 约束:handler 必须同步(要异步用 returns: 'promise',VM 会挂起等 resolve,超时由会话层按 HOST_TIMEOUT_MS 处理);返回值与写入 world 的数据必须是纯数据(itemsOf/statsOf/storageOf/entitiesOf/combatsOf 助手见 engine/runtime/world.ts,只有 world 随存档保存);handler 内禁止 eval/new Function/动态 require(tests/no-eval.test.ts 扫描全 src)。
  2. returns 决定该 API 是否进入表达式内置表:仅 serverSide 且返回 str/num/bool/any 的 API 可在剧本表达式中作为函数调用(engine/runtime/host-api.ts 的 buildExprBuiltins)。
  3. 补测试(仿 tests/combat.test.ts:从 registry 取 def 直接调 handler 或走 VM),并同步《剧本编写指南》§3 的 API 表。

3.2 新增一个插件指令(零引擎改动)#

不需要动引擎。在任意插件的 onLoad 中:

ctx.script.registerDirective('your.cmd', (args, sessionId, session) => { ... });

剧本即可写 !your.cmd(1, "a")。链路:词法 !name(args) → 编译为 PCALL 指令 → VM execPluginDirective → 会话 callDirective → PluginManager.callDirective(同步执行、无返回值、计数 storyengine_directives_total、配额与健康度自动计入)。规则与坑见《插件开发指南》§7。

3.3 新增一个语法关键字(引擎改动,慎用)#

四层 + 文档都要动,完整步骤:

  1. 词法:src/engine/lexer/token.ts 的 KEYWORDS 集合加关键字(若是标点,加入 SINGLE_PUNCT/DOUBLE_PUNCT);lexer.ts 的行型探测(detectLineKind)与对应 tokenizeXxx 规则按需扩展。
  2. 语法:src/engine/parser/ast.ts 定义新节点;parser.ts 加产生式(注意优先级层次 or < and < cmp < add < mul < unary,与块终止符 BLOCK_TERMINATORS)。
  3. 编译:src/engine/compiler/compiler.ts 的 compileNode switch 加分支——优先降级为现有 OpCode 组合,确有必要再在 instruction.ts 的 OpCode 联合里加新码(arg 必须纯数据,表达式以 AST 内联)。
  4. 运行时:src/engine/vm/vm.ts 的 exec() switch 加分支;注意等待态必须落 WaitState(纯数据),预算计数照常生效。
  5. 静态检查:若引入新引用关系,src/engine/compiler/resolver.ts 补符号收集与检查。
  6. 测试与文档:tests/lexer.test.ts/parser.test.ts/compiler.test.ts/vm.test.ts 补用例;《剧本编写指南》§1 语法表同步。

现有 !插件指令 语法槽(语法固定、语义开放)已覆盖绝大多数需求,先确认它不够用再走这条路。

3.4 新增一条静态检查规则#

  1. src/engine/compiler/resolver.ts(引用类检查)或 optimizer.ts(折叠/不可达类)实现规则,经 DiagnosticCollector.error/warning 上报,附 code/message/span/hint。
  2. 错误码按 §6 的"先定义后使用"流程取得;编译期码不得落入会话层(E4xxx)段位。
  3. tests/error-codes.test.ts 补边界用例(正例/反例各一);src/cli/lint.ts 无需改动(统一走 loader 诊断输出)。
  4. 《协议与错误码参考》§4 表格同步。

3.5 存档版本迁移#

改了 SaveState 结构时:src/persist/migrate.ts 追加 registerMigration(N, fn)(vN → vN+1,函数必须把 version 精确提升到 N+1)并提升 CURRENT_SAVE_VERSION;迁移要宽松补默认值(旧档缺字段)。范式见 tests/save.test.ts。运行期 migrateSave 只查表,链中断/未来版本一律 E5003。

4. 测试约定#

4.1 组织与分桶#

vitest.config.ts:include: ['tests/**/*.test.ts'],node 环境。当前 21 个测试文件、360 个用例,分桶逻辑:

文件 覆盖
lexer / parser / compiler.test.ts 编译链(含 recover 模式、深度防御)
vm.test.ts 指令语义、预算熔断、宿主调用、热重载 pc 调整
session.test.ts / ws-protocol.test.ts / server.test.ts 会话生命周期、协议握手/限流/重连、HTTP 路由
save.test.ts 存读档、迁移链、SQLite 后端(绑定可用时)
scheduler.test.ts / events.test.ts / hot-reload.test.ts 调度预算、事件总线、热重载
plugin-system.test.ts / directives.test.ts 插件注册表/时序/健康度/权限/配额;!指令 全链路
combat.test.ts / rpg-kit.test.ts / legend.test.ts / stories.test.ts 机制层、游戏插件、剧情剧本、全剧本可玩冒烟
no-eval.test.ts 红线:全 src 禁 eval/new Function/动态 require
error-codes.test.ts 错误码边界
audit-fixes.test.ts / fixes.test.ts 回归修复锁定

4.2 如何为新功能补测试#

  1. 按被测层选文件(见上表);纯引擎测试不 import session/transport。
  2. 夹具习惯:临时目录用 mkdtempSync(tmpdir());插件测试直接写 manifest.json + src/index.js 后 PluginManager.boot();Session 测试注入假时钟(now: () => 可控值)与假 socket(send 收集到数组断言);调度器测试用 tickOnce() 手动驱动。
  3. 断言错误时用 err.code(不要匹配 message 文案);新增错误码必须在 tests/error-codes.test.ts 有边界用例。

4.3 覆盖率#

pnpm coverage(v8 provider,统计 src/**/*.ts),阈值全部 80%:

thresholds: { lines: 80, functions: 80, branches: 80, statements: 80 }

纯类型文件 src/engine/parser/ast.ts、src/engine/vm/state.ts 不参与统计。低于阈值即测试失败。

5. 性能要点#

5.1 预算体系#

预算 默认 触发点 后果
MAX_OPS_PER_TICK(VM maxOpsPerTick) 1000 单次 vm.step() 内累计指令数 E2001
LOOP_BUDGET(VM loopBudget) 10000 while 回跳计数(键为 JMPF 的 pc) E2001;RET/选项跳出/前跳越过出口时计数作废
MAX_OPS_PER_SECOND(调度器) 200000 每会话自然秒窗口 ops 求和 onOpsExceeded → E2002 并断开
TICK_HZ 60 setInterval(1000/60) 驱动全部会话 tick timer 精度 <50ms
事件循环停顿 ≥5s — 整窗 ops 不可比 跳过该窗超限判定(防误杀)

调优思路:单会话 CPU 密集优先检查剧本循环(E2001/E2002 日志会带提示);全局吞吐不足再看 /metrics 的消息/会话曲线与压测分位。

5.2 编译缓存命中率#

  • 命中条件:缓存条目 sourceHash === sha1(当前源码)(engine/loader/story-loader.ts)。命中则跳过 parse 之后的全部工作(parse 本身仍会执行——缓存键需要它)。
  • 保障手段:启动预热把全部剧本装入缓存(COMPILE_WORKERS>0 时 worker 池并行编译,主线程校验 hash 后回填);重启后旧产物在缓存中,读档 getByHash 才能恢复旧存档。
  • 排查"永不命中":确认对比双方都是 sha1 口径(历史教训:FNV-1a 的 hashSource() 对比 sha1 永不相等)。

5.3 压测方法#

pnpm dev &
pnpm bench 10 10 dialogue.story      # 并发 / 秒 / 剧本,BENCH_URL 可换目标

关注:plays/s 与 msg/s 吞吐、choice 延迟 p50/p95/p99、错误数(含预期的 E6003 建会话限速)。改动调度器/会话/存档路径后建议前后各跑一轮对比。详见架构与运维手册 §8.3。

6. 发布流程#

pnpm lint        # 1. 类型检查 + 全量剧本零 error
pnpm test        # 2. 360 用例全绿
pnpm coverage    # 3. 覆盖率 ≥80%
# 4. 版本号:package.json "version"(当前 0.1.0)
pnpm build       # 5. 产出 dist/;生产 node dist/server.js
pnpm e2e:restart && pnpm e2e:plugins   # 6. 关键链路端到端(可选但推荐)

注意事项:

  • worker 编译池在生产 dist 下才完整生效;tsx 开发态自动降级主线程编译,属预期。
  • SAVE_BACKEND=sqlite 需 better-sqlite3 原生绑定与 Node ABI 匹配(prebuilt 未覆盖时 pnpm rebuild better-sqlite3);绑定不可用启动自动回退 file。
  • 打包交付排除 node_modules/、coverage/、saves/、core dump;目标机 pnpm install && pnpm test && pnpm lint 全绿即为可用状态。
  • 存档结构有变更时,发布说明必须写明迁移链覆盖范围(见 §3.5 与架构与运维手册 §6.3)。

7. 贡献规范#

7.1 提交信息#

使用祈使句、一行主题 + 空行 + 正文说明动机与影响面,例如:

fix(vm): RET 跨调用边界作废循环计数器

重入同一 while 时继承旧计数会提前误报 E2001,且无界增长进存档。
RET 时清空 loopCounters,选项/JMP 前跳越过出口的清理逻辑不变。

一次提交只做一件事:修复、功能、重构、文档分开;行为变更必须附带测试。

7.2 错误码:先定义、后使用#

  1. 新码先在对应常量表定义(集中管理,禁止散落魔法字符串): - 编译期:engine/compiler/(E1xxx 为主,静态检查经 DiagnosticCollector); - 运行时:engine/vm/vm-error.ts 的 ERROR_CODES(E2xxx/E3xxx/E5xxx/E7xxx); - 会话层:session/session-error.ts 的 SESSION_ERROR_CODES(E4xxx); - 传输层:transport/protocol.ts / ws-server.ts(E60xx); - 插件:kernel/plugin-system/errors.ts 的 PluginErrorCode(E-XXXNNN-R/F/W,R 可恢复 / F 致命 / W 警告)。
  2. 再在使用处引用常量;tests/error-codes.test.ts 补边界用例。
  3. 《协议与错误码参考》§4 同步登记;码位冲突(如 E6003 同时用于出站校验失败与会话创建限速)需在文档中注明两个用法。

7.3 文档同步要求#

  • 改默认值/环境变量 → src/config.ts 与《协议与错误码参考》§6、架构与运维手册 §9.1 三处一致。
  • 改协议消息/HTTP 路由 → 《协议与错误码参考》。
  • 改语法/宿主 API → 《剧本编写指南》。
  • 改插件契约 → 《插件开发指南》。
  • 改机制/运维行为(存档、热重载、指标、预算)→ 架构与运维手册。
  • 文档以源码为唯一依据:写进文档的默认值、命令、阈值必须能在源码中核实。