[开发] FlintHub 大富翁插件开发规划

👑Lv.11 元老 🌞 骨干会员
2026-11-09 17:46:44

版本:v1.0(2026-11-08) 本版替换 v0.1。v0.1 将目标平台误判为另一个独立程序,其插件形态、钩子名、路由注册、数据表前缀、后台接入方式均已作废;本版按 FlintHub(D:\phpstudy_pro\WWW 根目录程序)的真实结构重写。 唯一权威规范:docs/插件开发规范.md v1.8.2(2026-11-07)。 本轮只做规划,不改任何业务代码。


0. 结论摘要

0.1 平台与范围

项结论
目标平台FlintHub,核心 V1.5.11(version.php:8),PHP 8 + SQLite,MIT(LICENSE:1)
插件落位plugins/monopoly/,命名空间 Plugin\Monopoly,显示名「矿工大富翁」
游戏来源mine-monopoly-main/(Vue 3 + TS + Three.js,GPL-3.0),791 文件 / 20.2 MB,512 个 TS/Vue 文件 / 97,257 行
首版范围单人 + 1–3 个 AI 对手;P2P 联机不接入(依赖 Express + PeerJS + coturn,超出 PHP 插件边界)
构建方式Node 仅构建期使用;运行时不需要 Node/MySQL/coturn
交付形态插件内同源 iframe 承载游戏静态产物;LLM 由 PHP 同源代理转发

0.2 决策状态一览

编号决策状态
D1目标平台 = FlintHub,插件落 WWW/plugins/已定(用户拍板)
D2Node 仅构建期,运行时不部署 Node/Express/MySQL成立(FlintHub 为 PHP,无 Node 运行时入口)
D3官方地图双轨:自研地图 + 授权导入成立(仓库无地图文件,硬阻断不变)
D4插件 assets/game/ 静态直出成立且简化:FlintHub 的 assets/ 本身可被 Web 直接访问(规范 §2、§7.1),不需要额外的静态路由或回调输出
D5LLM 必须走 PHP 同源代理由「可选」升级为「事实必选」:CSP connect-src 回退 'self';Plugin::http() 有 SSRF 防护,禁内网/localhost,本地模型不可行
D6插件保持 GPL-3.0,与 MIT 核心共存成立(两许可不冲突),新增资源授权风险
D7插件外壳 i18n,游戏本体硬编码中文新增缺口,待拍板
D8同源 iframe 承载游戏成立(CSP frame-ancestors 'self')
D9首版不含 P2P 联机新增范围裁剪(CSP connect-src 'self' 也会阻断 PeerJS 信令)

0.3 三个硬阻断(必须先解决)

  1. 地图缺失:仓库内无任何 .mmmap / .fpmap 文件;官方地图需在爱发电支持作者后获取(mine-monopoly-main/README.md:318-322)。无地图则游戏无法开局,M1 前必须有自研地图或书面授权。
  2. LLM 必须代理:CSP 未声明 connect-src,回退 default-src 'self'(app/Core/SecurityHeaders.php:150),浏览器直连外部 API 必被阻断;Plugin::http()(app/Helpers/Plugin.php:503)禁止内网与 localhost,本地 Ollama 方案在 FlintHub 下不可行。
  3. i18n 缺口:规范 §8 要求至少 lang/zh.php,而游戏 UI 无 i18n 框架(仓库检索 vue-i18n|locale 无匹配),全部中文硬编码。插件外壳可 i18n,游戏本体短期只能固定中文。

1. FlintHub 平台契约(按真实结构核实)

1.1 运行形态

项实际情况证据
语言/运行时PHP 8 + SQLiteindex.php、app/Core/Database.php:5
核心版本V1.5.11version.php:8
许可证MITLICENSE:1
业务库business.sqlite(SplitDB)app/Core/Database.php:5
插件库每插件独立 plugins/{name}/data/{name}.sqlite规范 §2、§10
模板自定义 TemplateCompiler,视图 section('title'/'css'/'content')规范 §6
前端资源plugins/{name}/assets/ 可被 Web 直接访问规范 §2、§7.1
路由控制器注册:$router->get('/path', [Controller::class, 'method'])规范 §5、plugins/board_games/hook/route_register.php
后台独立 AdminController + admin_route_register 钩子规范 §3、plugins/board_games/hook/admin_route_register.php

结论:FlintHub 是 PHP 单体 + SQLite,不是 Go,也不是 MySQL;无 Node 运行时。游戏侧 apps/server(Express + TypeORM + MySQL + PeerJS)在 FlintHub 部署中不参与运行。

1.2 插件目录与生命周期

规范 §2 规定一目录一插件,目录名小写下划线:

plugins/monopoly/
├── plugin.json
├── Plugin.php
├── FrontController.php
├── AdminController.php
├── hook/
├── views/
├── assets/
├── lang/
└── data/

生命周期(规范 §4、§4.1):核心以目录名硬拼类名 "\\Plugin\\{$name}\\Plugin"(app/Helpers/Plugin.php 中 activate :699、deactivate :750、uninstall :846,类名拼接 :1793)。

  • 下划线陷阱:目录名含下划线的插件会拼出非法类名而静默落空,必须 class_alias(规范 §4.1)。
  • 本插件目录名取 monopoly(无下划线),命名空间 Plugin\Monopoly,天然规避该陷阱,无需 class_alias。
  • 自动加载器支持驼峰 ↔ 下划线回退(app/Core/Autoloader.php:36-70),但不要依赖它来处理生命周期拼接。

1.3 plugin.json 契约(规范 §3)

必填:name、version、author、description、activated、hooks。可选:min_core、category、sort_order、icon、admin_url、permissions。

  • min_core 不填时兜底按 1.1.2,用了新能力必须在运行期报错的版本上显式声明。
  • category 必须取 10 类之一(content/community/identity/economy/operation/governance/tools/integration/entertainment/other),否则归入「未分类」。
  • icon 必须是 app/Core/TemplateCompiler.php 图标表中真实存在的名字,否则渲染 。dice 已核实存在(app/Core/TemplateCompiler.php:752)。
  • permissions 为强制模式:写全局设置需 system:settings、注册后台路由需 route:admin、发起外部 HTTP 需 net:fetch。
  • hooks 路径必须用 /;新增或改动 hooks 后必须删除 plugins/plugins_cache.json 或调用 Plugin::refresh(),否则钩子不生效。

1.4 控制器、路由与 URL

  • 控制器继承 app\Core\Controller(app/Core/Controller.php),可用 $this->view()(:20)、$this->json()(:159)、$this->redirect()、$this->requireLogin()(:238)。无 layout 参数。
  • URL 分两类(规范红线 2、§6.2.1):路由/接口地址用 $this->url() / \app\Helpers\Url::to();只有静态资源才能用 BASE_PATH 拼接。
  • 写操作顺序(规范 §11.28):requireLogin() → Csrf::verifyOrDie()(app/Helpers/Csrf.php:60)→ 业务。顺序颠倒会在会话过期时卡 403。
  • AJAX 必须带 CSRF(表单 csrf 字段或 X-CSRF-Token),核心红线 8。

1.5 视图与资源

  • 视图解析到 plugins/monopoly/views/{view}.php,模板写法见规范 §6;前台容器 .mn-section 无内边距,内容需自带容器(规范 §6.4)。
  • assets/ 由 Web 直接访问;引用必须 BASE_PATH + 路径 + ?v=filemtime(...)(规范 §7.1)。
  • 零内联(规范 §7.2):插件视图不得写 <style> / <script> 内联块,样式与脚本一律收敛到 assets/style.css、assets/script.js。数据通过 data-* 属性传给脚本。
  • 禁止在插件目录外写文件,禁止改核心文件(规范 §2)。

1.6 语言包(规范 §8)

  • lang/zh.php 至少,en.php / zh_tw.php 建议;键名 plugin.monopoly.*,自动合并,无需钩子。
  • 三份语言包键集合必须一致(打包门禁会比对)。

1.7 独立数据库与事务(规范 §4、§10)

  • \app\Helpers\Plugin::db('monopoly') 返回原生 PDO,指向 plugins/monopoly/data/monopoly.sqlite。
  • \app\Helpers\Plugin::ensureSchema('monopoly', $ddl) 幂等建表,activate() 与 init_after 都调用(规范 §11.15 自愈)。
  • PDO 已由核心设 ERRMODE_EXCEPTION、EMULATE_PREPARES=false、busy_timeout=5000,插件不得覆盖(红线 7)。
  • 事务:beginTransaction() / commit() / rollBack(),回滚前先 inTransaction();跨库(核心积分库 + 插件库)不可原子,积分动作放在插件事务 commit() 之后(参考 docs/specs/draw_guess/spec.md P0-1 教训)。

1.8 钩子(规范 §9)

全表 49 个。本插件只需 4 个:

钩子用途
init_after建表自愈、配置兜底
route_register注册前台路由
admin_route_register注册后台路由(需 route:admin)
nav_plugin_links「应用」入口卡片(规范 §9.3,注意双触发)

board_games 另外用了 layout_head_end / layout_body_end / controller_view_before,那是因为它把入口分散在站点页面上;本插件所有页面都在自己的视图内,资源在视图里按需引用即可,不额外挂布局钩子。 渲染类钩子内禁止 die()/exit()/header()(红线 3);钩子内不做重 DB 操作(规范 §11.4)。

1.9 CSP 与浏览器能力边界(关键约束)

app/Core/SecurityHeaders.php:150 实际下发:

default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
worker-src 'self' blob:;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
frame-ancestors 'self'

推导出的能力边界:

指令对游戏的影响
未声明 connect-src → 回退 default-src 'self'所有 fetch/XHR/WebSocket 只能同源:外部 LLM 直连被阻断、PeerJS 外部信令被阻断、外部 CDN 不可用
script-src 'unsafe-eval'允许游戏运行时编译 TS(apps/client/src/utils/index.ts:4 引入 typescript,compileTsToJs 339–360 需 new Function),此能力是游戏能跑的前提
worker-src 'self' blob:Worker 可以同源文件或 blob 方式加载;Vite 产物默认同源文件,满足
img-src 'self' data: https:图片可同源、data URI、https;但游戏图片实际全部本地,无影响
未声明 media-src / font-src → 回退 'self'音频、字体必须同源:public/assets/audio/(0.95 MB)与字体(1.5 MB)必须随包落位,不能用 CDN
style-src 'unsafe-inline'兼容 Vue 运行时注入样式
frame-ancestors 'self'同源 iframe 允许,D8 成立

1.10 现有插件先例

  • plugins/board_games/:与本插件最接近(棋牌、娱乐类、积分、人机、排行、后台)。可参考其 plugin.json(category: entertainment、icon: dice、7 个 hooks、permissions: ["route:admin"])、hook/route_register.php、hook/nav_plugin_links.php、FrontController 的 requireLogin → Csrf → RateLimiter → json 顺序、assets/script.js 从隐藏 csrf input 取值的方式。
  • plugins/ai_assistant/Plugin.php(约 1313–1390):LLM 调用先例,使用 Plugin::http() 转发、配置项 api_url/api_key/model/temperature/max_tokens、后台超时约 10s。
  • 结论:LLM 代理在 FlintHub 内有现成范式,不必自造 HTTP 客户端。

2. 游戏现状实录(平台无关)

2.1 规模

指标数值
文件总数791
仓库体积20.2 MB
TS/Vue 文件512
TS/Vue 总行数97,257(@(Get-Content).Count 统计)
客户端静态资源3.57 MB(audio 0.95 / 图片 0.43 / glb 0.21 / draco 1.9)
字体1.5 MB

2.2 技术栈

  • 前端:Vue 3.5、TypeScript 5.9、Vite 5、Pinia 2、Vue Router。
  • 渲染:Three.js 0.179、GSAP、PixiJS、cannon-es;音频 Howler。
  • 服务端(不复用):Express 4、TypeORM、MySQL、JWT、ws、PeerJS;coturn 中继。
  • 壳:Electron(Windows)、Capacitor(计划中)。
  • 工程:pnpm workspaces + Changesets。

2.3 现有运行架构

中央服务器只负责认证与房间路由;游戏逻辑跑在主机客户端的 Web Worker 中(apps/client/src/core/worker/GameProcessWorker.ts,4,644 行),玩家之间 WebRTC 直连。HostBridge 在 GameProcessWorker.ts:148-170。

FlintHub 版去掉 P2P 与信令:Worker 与 UI 仍在同一浏览器上下文,AI 座位由本地 Worker 驱动,只有 LLM 决策走同源代理。

2.4 规则与状态机落点

引擎只提供积木,具体规则由地图 TS 脚本注入:

关注点位置
游戏主循环GameProcessWorker.ts:2084(gameLoop)
破产与资产释放GameProcessWorker.ts:2045-2055;class/Player.ts:196-222、309
胜负判定GameProcessWorker.ts:4296-4304(checkGameOver)
买地/升级/过路费class/Property.ts:82-142
骰子class/Dice.ts:22-37
阶段与脚本执行class/GamePhase.ts:24-45
AI 默认动作GameProcessWorker.ts:3514(buildAIDefaultOperationResult)
AI 决策入口GameProcessWorker.ts:4199(runAIDecision)

→ 插件不得在 PHP 侧重写规则;PHP 只做房间/积分/配额/代理,规则权威在浏览器 Worker。

2.5 地图格式与获取(硬阻断)

  • 格式:.mmmap / .fpmap,protobuf + 加密/压缩;加载解析在 apps/client/src/utils/file/game-map.ts:201-289(服务端 URL / 本地文件、解密解压、校验)。
  • 仓库内没有任何地图文件。
  • README.md:318-322:官方地图源文件需通过爱发电支持作者获取。
  • 影响:没有地图 = 无法开局。选项见 §8 D3。

2.6 存档

apps/client/src/core/save/IndexedDBSaveStorage.ts(库名 mine-monopoly-saves,3、17-20)。纯浏览器 IndexedDB,无服务端存档;FlintHub 版可直接沿用,服务端只记录对局结果。

2.7 AI 现状

项现状
请求结构packages/types/interfaces/game/ai.ts:489-516(AIDecisionRequest),快照 424–484,选项集 544–568
输出协议只返回 JSON(ReadableAIPrompt.ts:225-280,274 明确「只回 JSON」),字段 optionId
远端配置ai-decision-config.ts:8 超时 30000ms;:14/:24/:40 默认与归一化;上下文记忆默认 6 条(:71-74)
提供方OpenAICompatibleDecisionProvider.ts,max_tokens/temperature 在 349–350,超时 427,归一化 474
人格ai.ts:361-393 角色、332-359 策略;每 AI 可绑不同 remoteProfileId(656-725)
用量ai.ts:588-654
HTTP 出口apps/client/src/core/ai/remote-http.ts(1,947 字节)——改代理的唯一 patch 点
策略记忆StrategyStateManager.ts,1,008 行

2.8 许可

  • 游戏仓库 LICENSE:GPL-3.0。
  • FlintHub:MIT。
  • 音频目录 README 明确要求使用合法授权素材;官方地图与音频的再分发授权可能独立于 GPL,需单独确认。

3. 复用边界与 FlintHub 落位

3.1 复用率

约 70%:渲染层、地图加载/解析、Worker 引擎、TS 脚本编译、UI 组件、音频、AI 决策框架可直接复用;服务端、联机、存档后端、Electron/Capacitor 壳不复用。

3.2 复用 / 改写 / 废弃

模块处置说明
packages/types、packages/utils、packages/components、packages/style复用改为本地 workspace 引用
apps/client/src/core/worker/**复用引擎与状态机不改
core/ai/**复用 + 改出口仅替换 remote-http.ts 的 base URL 与鉴权头
utils/file/game-map.ts复用地图加载逻辑不变,仅改资源路径
apps/client/src/router/index.ts改写createWebHistory(import.meta.env.BASE_URL)(34–35)→ createWebHashHistory(),否则 iframe 内深链 404
core/monopoly-host/**、PeerJS、apps/server、Electron废弃首版单人 vs AI,无联机
IndexedDBSaveStorage复用前端本地存档
登录/房间 UI改写由 FlintHub 视图提供大厅,游戏只负责对局

3.3 目录落位

plugins/monopoly/
├── plugin.json
├── Plugin.php                    # Plugin\Monopoly
├── FrontController.php
├── AdminController.php
├── hook/
│   ├── init_after.php
│   ├── route_register.php
│   ├── admin_route_register.php
│   └── nav_plugin_links.php
├── views/
│   ├── index.php                 # 大厅
│   ├── play.php                  # iframe 容器
│   ├── rank.php                  # 排行
│   └── admin.php                 # 后台设置
├── assets/
│   ├── style.css
│   ├── script.js
│   └── game/                     # Vite 构建产物(dist)
│       ├── index.html
│       ├── assets/*.js|css
│       ├── draco/
│       ├── audio/
│       ├── images/
│       └── map/default.mmmap     # 自研或授权地图
├── lang/{zh,en,zh_tw}.php
└── data/monopoly.sqlite

3.4 构建期处理(Node 只在构建机出现)

  1. base:apps/client/vite.config.ts 现默认 web base 为 /monopoly/(24–30 行),改为相对 ./ 或 /plugins/monopoly/assets/game/。站点可能装在子目录,推荐 base: './',配合 hash 路由,避免 BASE_PATH 变化导致资源 404。
  2. 路由:createWebHistory → createWebHashHistory,iframe 内 #/... 永不请求 PHP。
  3. Worker:Vite 默认输出同源 chunk,满足 worker-src 'self';不要引入 CDN worker。
  4. draco:decoderPath 不能用相对 ./(它相对文档而非模块),由视图注入 data-game-base,bundle 读取后用 new URL(..., document.baseURI) 拼绝对地址。
  5. 音频/字体:全部落到 assets/game/ 同源目录;CSP 无 media-src/font-src,CDN 一律不可用。
  6. manualChunks(vite.config.ts:75-90)保留,three/vue 分包,首屏更快。
  7. 产物清理:outDir 指向插件 assets/game/,构建前后清空,避免残留旧 hash 文件。

3.5 D4 说明:为什么在 FlintHub 下更简单

规范 §2 与 §7.1 明确 plugins/{name}/assets/ 可被 Web 直接访问。因此:

  • 不需要「assets 回调返回源码」或自定义静态路由;
  • views/play.php 只需输出一个 <iframe src="{BASE_PATH}/plugins/monopoly/assets/game/index.html">;
  • 资源版本仍按 §7.1 给 style.css / script.js 加 ?v=filemtime;game/ 产物本身带 hash,无需额外处理。

代价:assets/game/ 公开可读,任何人可直接访问。因此产物内不得包含 API Key、地图密钥、运营配置;地图文件按 §8 D3 的授权策略处理。

3.6 iframe 与宿主页通信

  • 同源 iframe(frame-ancestors 'self' 允许)。
  • 宿主 → iframe:postMessage({type:'mm:init', matchToken, baseUrl, locale})。
  • iframe → 宿主:mm:ready、mm:finished(结果、回合数、时长)、mm:error。
  • matchToken 一次性、绑定 user_id 与过期时间,用于 LLM 代理与结算鉴权;不使用 Cookie 直传,避免 iframe 内 CSRF 语义混乱。

4. AI 对手接入设计

4.1 信息契约

沿用现有结构,不新造协议:

  • 输入:AIDecisionRequest(快照 + 合法选项集 + 可选策略记忆),见 packages/types/interfaces/game/ai.ts:489-516。
  • 输出:严格 JSON,至少含 optionId;解析失败/超时 → {} → buildAIDefaultOperationResult(GameProcessWorker.ts:3514)兜底。
  • 约束:temperature 0.2、max_tokens 512、超时 ≤ 12s(沿用 30s 会拖死 PHP-FPM,见 §5.3)。
  • 防作弊:选项集由 Worker 生成,模型只能选,不得返回坐标/金额等自由字段。

4.2 调用链路

iframe(游戏 Worker)
  └─ remote-http.ts  ──POST /monopoly/api/llm──▶  FrontController::llm()
       headers: X-MM-Token: matchToken
       body:    {provider, model, messages, max_tokens, temperature}
                                                     │
                                     鉴权/CSRF/限流/配额
                                                     │
                                        Plugin::http() ──▶ 外部 LLM API
                                                     │
                                         归一化 JSON 返回
  • 浏览器侧不持有 API Key;Key 只存插件 SQLite mm_profiles,后台管理。
  • Plugin::http() 自带 SSRF 防护(禁内网/localhost)与超时配置,需 net:fetch 权限。
  • 不引入 function calling;继续用「选项集 + optionId」。

4.3 代理端点契约

POST /monopoly/api/llm

校验失败返回
requireLogin()401 跳登录
Csrf::verifyOrDie()403
X-MM-Token 对应 mm_matches 未结束、未过期、属主匹配403
RateLimiter::check('mm_llm', 60, 60)(每用户每分钟)429
mm_quota 当日调用/点数未超限429
上游超时/非 200返回 {} + 记录 mm_ai_log,游戏走默认动作

请求体只允许白名单字段;base_url 与 api_key 一律以服务端 mm_profiles 为准,忽略客户端传入,防止 SSRF/密钥外泄。

4.4 三种 AI 人格

人格风格提示词侧重
aggressive抢地、快升级、愿意付高租金优先买地/升级,容忍低现金
balanced稳健现金流保留安全现金,均衡买地与留钱
conservative攒钱、避风险现金低于阈值不买地

落库字段 mm_profiles.persona;每座位可绑不同 profile,复用现有 remoteProfileId 机制(ai.ts:656-725)。人格只影响系统提示词与策略记忆权重,不改协议。

4.5 失败降级

场景行为
超时返回 {},本回合用默认动作,不阻塞游戏
429/配额耗尽该座位自动切「本地启发式」决策(现成默认逻辑),前端提示一次
模型输出非 JSON丢弃,重试 0 次,走默认动作
上游 5xx熔断 60s,期间该 profile 直接走默认动作
未配置任何 profile大厅禁用「AI 对手」人数选择,只允许观战/回放(若实现)

4.6 成本量级(估算,需实测校准)

  • 单次决策输入约 800–1,500 tokens(快照裁剪后),输出 50–100 tokens。
  • 单局 30–60 回合、每回合 1–3 次决策 → 单 AI 单局约 3–9 万 tokens。
  • 3 个 AI、每局 5 万 tokens/座位量级 → 单局 10–15 万 tokens。
  • 按主流小模型价格折算,单局成本在分级(分/角)量级,不是元级。正式上线前用 mm_ai_log 的 tokens_in/tokens_out 实测替换估算。

4.7 限流与配额

层级手段
开局RateLimiter::check('mm_start', 10, 60)
LLMRateLimiter::check('mm_llm', 60, 60)(每用户每分钟)
每日mm_quota(user_id, day_key, calls, tokens, points_spent)
全局后台可设「单局最大 AI 调用次数」与「每日总调用上限」
积分可配置「每局消耗 N 积分」或「免费额度 M 局/日」,超限拒绝开局

4.8 安全

  • 密钥只存服务端;日志脱敏(不记 prompt 全文,只记长度与哈希)。
  • 所有 SQL 预处理;IN (...) 用占位符(红线 6)。
  • 上传/导入地图走白名单 + 强随机名(红线 4),或首版不开放上传。

5. 服务器配置评估

5.1 必需

项要求
PHP8.0+(与核心一致),扩展 pdo_sqlite、curl、mbstring、json、openssl
WebNginx/Apache 任一,能直出静态文件
磁盘插件约 10–20 MB(游戏产物 + 音频 + 字体 + 地图)
数据库SQLite,无需 MySQL

5.2 不需要

  • 不需要 Node.js / Express / PM2。
  • 不需要 MySQL。
  • 不需要 coturn / TURN 服务(首版无联机)。
  • 不需要 GPU(AI 走外部 API)。

5.3 并发与超时(最关键)

LLM 代理会在一次请求内占住一个 PHP-FPM worker 直到上游返回:

  • 代理超时设 12s(远低于游戏默认 30s),并保证 max_execution_time > 15s。
  • pm.max_children 建议 ≥ 10;若站点同时承载论坛流量,建议为插件单独限流,避免 AI 请求打满 worker。
  • 单局最多 3 个 AI 座位并发调用;同一用户并发上限设 3,超过即排队/拒绝。
  • 前端 Worker 侧对同一座位的请求串行化,避免一次掷骰触发多次并发。

5.4 带宽与首屏

  • 首屏资源约 3–6 MB(gzip 后 1.5–2.5 MB 量级),含 three.js 与音频。
  • 音频按需加载(Howler 延迟加载),不要首屏全量拉取。
  • 20 人同时开局时,主要是静态资源带宽;建议开 Cache-Control 长缓存(产物带 hash)。

5.5 运维

  • 备份:plugins/monopoly/data/monopoly.sqlite 纳入备份。
  • 清理:mm_matches / mm_ai_log 保留 90 天,结算时按低概率触发清理或由站点 cron 调用;避免每回合写日志,AI 调用只按局聚合。
  • SQLite 写量很低(每局几行),busy_timeout=5000 足够;不要另开长事务。

6. 插件形态设计

6.1 目录树

见 §3.3。

6.2 plugin.json

{
    "name": "矿工大富翁",
    "version": "1.0.0",
    "min_core": "1.5.11",
    "author": "FlintHub",
    "description": "3D 大富翁:单人对战 1-3 个 AI 对手,掷骰买地、收取租金,支持积分与排行。",
    "activated": true,
    "category": "entertainment",
    "icon": "dice",
    "admin_url": "/admin/monopoly",
    "hooks": {
        "init_after": "hook/init_after.php",
        "route_register": "hook/route_register.php",
        "admin_route_register": "hook/admin_route_register.php",
        "nav_plugin_links": "hook/nav_plugin_links.php"
    },
    "permissions": ["net:fetch", "route:admin"]
}

说明:

  • 不申请 system:settings:所有配置存插件自有 mm_settings,不写核心全局设置。
  • min_core 显式写 1.5.11,避免 §3 的兜底 1.1.2 问题。
  • 新增插件后必须清理 plugins/plugins_cache.json。

6.3 路由

方法路径控制器方法说明
GET/monopolyFrontController::index大厅(登录)
GET/monopoly/playFrontController::playiframe 容器(登录)
POST/monopoly/api/startFrontController::start创建对局与 matchToken
POST/monopoly/api/llmFrontController::llmLLM 代理
POST/monopoly/api/finishFrontController::finish结算、发积分、写排行
GET/monopoly/rankFrontController::rank排行榜
GET/admin/monopolyAdminController::index后台设置
POST/admin/monopoly/saveAdminController::save保存设置(CSRF)

所有写接口顺序:requireLogin() → Csrf::verifyOrDie() → RateLimiter → 业务。

6.4 钩子文件职责

文件职责
hook/init_after.php调 Plugin::ensureSchema() 自愈;不查业务数据
hook/route_register.php注册上表前台路由
hook/admin_route_register.php注册后台路由
hook/nav_plugin_links.php输出「应用」入口,URL 用 Url::to('/monopoly');注意 §9.3 双触发,避免重复输出

6.5 数据表

CREATE TABLE IF NOT EXISTS mm_settings (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL DEFAULT ''
);

CREATE TABLE IF NOT EXISTS mm_profiles (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL,
    base_url    TEXT    NOT NULL DEFAULT '',
    api_key     TEXT    NOT NULL DEFAULT '',
    model       TEXT    NOT NULL DEFAULT '',
    temperature REAL    NOT NULL DEFAULT 0.2,
    max_tokens  INTEGER NOT NULL DEFAULT 512,
    timeout_ms  INTEGER NOT NULL DEFAULT 12000,
    persona     TEXT    NOT NULL DEFAULT 'balanced',
    enabled     INTEGER NOT NULL DEFAULT 1,
    created_at  TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS mm_matches (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    match_token  TEXT    NOT NULL UNIQUE,
    user_id      INTEGER NOT NULL,
    ai_count     INTEGER NOT NULL DEFAULT 1,
    difficulty   TEXT    NOT NULL DEFAULT 'normal',
    result       TEXT    NOT NULL DEFAULT '',
    rounds       INTEGER NOT NULL DEFAULT 0,
    duration_sec INTEGER NOT NULL DEFAULT 0,
    tokens_in    INTEGER NOT NULL DEFAULT 0,
    tokens_out   INTEGER NOT NULL DEFAULT 0,
    points_delta INTEGER NOT NULL DEFAULT 0,
    ip_hash      TEXT    NOT NULL DEFAULT '',
    created_at   TEXT    NOT NULL,
    finished_at  TEXT
);

CREATE TABLE IF NOT EXISTS mm_ai_log (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    match_id   INTEGER NOT NULL,
    ai_seat    INTEGER NOT NULL DEFAULT 0,
    profile_id INTEGER NOT NULL DEFAULT 0,
    model      TEXT    NOT NULL DEFAULT '',
    latency_ms INTEGER NOT NULL DEFAULT 0,
    ok         INTEGER NOT NULL DEFAULT 0,
    tokens_in  INTEGER NOT NULL DEFAULT 0,
    tokens_out INTEGER NOT NULL DEFAULT 0,
    error      TEXT    NOT NULL DEFAULT '',
    created_at TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS mm_quota (
    user_id      INTEGER NOT NULL,
    day_key      TEXT    NOT NULL,
    calls        INTEGER NOT NULL DEFAULT 0,
    tokens       INTEGER NOT NULL DEFAULT 0,
    points_spent INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, day_key)
);

约束:

  • 所有表带 IF NOT EXISTS,activate() 与 init_after 均调用(规范 §11.15)。
  • 索引:mm_matches(user_id, created_at)、mm_ai_log(match_id)、mm_quota(day_key)。
  • 计数类更新用原子 UPDATE + rowCount()(红线 5)。
  • 积分:Points::award($userId, $points, 'monopoly_win', $matchId, 'monopoly')(app/Helpers/Points.php:207);扣费 Points::deduct(...)(:239,返回 bool)。积分在插件事务提交后执行。

6.6 后台

  • 设置项:默认 profile、每局 AI 数上限、每日免费局数、每局消耗积分、单局最大 AI 调用次数、地图来源说明。
  • API Key 输入框回显掩码,留空表示不修改。
  • 页面容器按规范 §6.4 处理内边距;样式与脚本全部外置。
  • 所有表单带 CSRF;保存后 redirect + 提示。

6.7 积分与排行

  • 排行维度:胜场、胜率、最快通关回合、累计对局;数据源 mm_matches。
  • 发奖时机:finish 校验 match_token 与结果合法性后一次性发放;同一 match_id 只发一次(唯一约束或状态位)。
  • 防刷:开局令牌一次性;结算时校验 duration_sec 下限与回合数下限;异常局不发奖。

6.8 语言包

  • 键:plugin.monopoly.title、.desc、.nav、.lobby_*、.play_*、.rank_*、.admin_*、.error_*。
  • zh.php / en.php / zh_tw.php 键集合一致。
  • 游戏 iframe 内部仍是中文硬编码(D7),文档与后台需明示「游戏内文案暂仅中文」。

6.9 安全清单

项做法
SQL全部预处理(红线 2)
输出$this->e() / htmlspecialchars(红线 1)
文件basename() + 白名单 + 随机名(红线 1、4)
事务先 inTransaction() 再回滚(§10)
钩子渲染钩子不 die(红线 3)
CSRF所有写接口校验(红线 8),顺序按 §11.28
路径__DIR__ / BASE_PATH,禁 DOCUMENT_ROOT(红线 2)
密钥仅服务端,不进入前端产物与日志

7. 分阶段计划

M0 插件骨架(不接游戏)

  • 建 plugins/monopoly/,plugin.json + Plugin.php + 两个控制器 + 4 个钩子 + 4 个视图 + 空 lang/。
  • ensureSchema 建 5 张表;后台可保存 profile。
  • 验收:插件可激活/停用;/monopoly 与 /admin/monopoly 可访问;php -l 全通过;清理 plugins_cache.json 后钩子生效。

M1 单机游戏跑通

  • 先解决地图(自研最小地图或授权地图)。
  • Vite 产物落 assets/game/,hash 路由,iframe 可开局、可结束。
  • 验收:能完整走完一局(无 AI);结算写入 mm_matches。

M2 AI 接入

  • remote-http.ts 改指向 /monopoly/api/llm;代理端点 + 限流 + 配额 + 日志。
  • 3 种人格;超时/失败降级。
  • 验收:1–3 个 AI 可完成整局;断网/超时不影响游戏继续;mm_ai_log 有耗时与 token 记录。

M3 积分与排行

  • 开局扣费、结算发奖、每日额度、排行榜页面。
  • 验收:同一局不重复发奖;超额开局被拒并有提示。

M4 发布准备

  • i18n 三语言包键集合校验;零内联检查;?v=filemtime 检查。
  • 文档:安装说明、地图授权说明、GPL-3.0 源码获取说明、隐私说明(LLM 会发送对局快照)。
  • 打包,版本号三处一致。

8. 决策状态与理由

D1 目标平台(已定)

FlintHub,plugins/monopoly/,Plugin\Monopoly。v0.1 的旧路径/钩子/API 全部作废。

D2 Node 仅构建期(成立)

FlintHub 无 Node 运行时入口,插件是 PHP。构建产物是静态文件。唯一注意:typescript 会被打进前端包用于运行时编译地图脚本,这是浏览器端依赖,不是服务端 Node。

D3 官方地图双轨(成立)

  • 轨道 A:自研最小可玩地图(assets/game/map/default.mmmap),规避授权问题。
  • 轨道 B:用户/站长导入已授权地图,落 data/maps/(Web 不可直接访问)或 assets/game/map/(需评估是否公开)。
  • 未取得地图前不得发布「可玩」版本。

D4 静态直出(成立且简化)

见 §3.5。FlintHub 的 assets/ 天然可 Web 访问,不需要额外路由;代价是产物公开。

D5 LLM 必须代理(事实必选)

CSP connect-src 回退 'self';Plugin::http() 禁内网。因此:外部 API + PHP 同源代理是唯一路径。若未来要接本地模型,只能由站点在反代层另开同源路径,且 Plugin::http() 的 SSRF 规则不允许 127.0.0.1,需评估核心是否放行——本版不做。

D6 GPL-3.0(成立)

  • 插件整体作为 GPL-3.0 衍生作品分发;FlintHub 核心仍 MIT,两者不冲突。
  • 插件目录需附 LICENSE(GPL-3.0)与源码获取方式。
  • 地图/音频/模型素材的授权独立于代码许可,必须单独确认,不能默认随 GPL 分发。

D7 i18n 缺口(新增,待拍板)

插件外壳可 i18n,游戏本体中文硬编码。选项:

  1. 首版仅中文,en.php/zh_tw.php 只翻译插件外壳与报错(推荐,成本最低)。
  2. 先做游戏内文案抽取(工作量大,需改大量 Vue 模板)。
  3. 不做语言包(不符合规范 §8,不可取)。

D8 同源 iframe(成立)

frame-ancestors 'self' 允许;postMessage 使用一次性 matchToken。

D9 首版不含联机(新增)

P2P 需要外部信令与 TURN,CSP 会阻断外部 WebSocket,且 FlintHub 无 Node 运行时。联机若要做,需自建同源信令服务并改 CSP,超出本插件范围。


9. 证据索引

结论证据
核心版本 V1.5.11version.php:8
核心 MITLICENSE:1
业务库 SQLiteapp/Core/Database.php:5
插件类名硬拼app/Helpers/Plugin.php:699、:750、:846、:1793
自动加载回退app/Core/Autoloader.php:36-70
Plugin::http() / SSRFapp/Helpers/Plugin.php:503
控制器 APIapp/Core/Controller.php:20、:159、:238
CSRFapp/Helpers/Csrf.php:16、:60
积分app/Helpers/Points.php:207、:239
限流app/Helpers/RateLimiter.php:250、:362
CSP 全文app/Core/SecurityHeaders.php:150
图标 diceapp/Core/TemplateCompiler.php:752
插件先例plugins/board_games/plugin.json、hook/*.php、FrontController.php
LLM 先例plugins/ai_assistant/Plugin.php:1313-1390
游戏规模791 文件 / 20.2 MB / 512 TS+Vue / 97,257 行
主循环GameProcessWorker.ts:2084
破产GameProcessWorker.ts:2045-2055、class/Player.ts:196-222、:309
胜负GameProcessWorker.ts:4296-4304
默认动作GameProcessWorker.ts:3514
AI 入口GameProcessWorker.ts:4199
地产/骰子/阶段class/Property.ts:82-142、class/Dice.ts:22-37、class/GamePhase.ts:24-45
AI 契约packages/types/interfaces/game/ai.ts:424-484、:489-516、:544-568、:588-654、:656-725
AI 超时/参数ai-decision-config.ts:8、OpenAICompatibleDecisionProvider.ts:349-350、:427、:474
提示词约束ReadableAIPrompt.ts:73、:225-280、:274
代理改造点apps/client/src/core/ai/remote-http.ts
存档库名IndexedDBSaveStorage.ts:3、:17-20
地图加载utils/file/game-map.ts:201-289
TS 运行时编译apps/client/src/utils/index.ts:4、:339-360
Vite base/分包vite.config.ts:24-30、:68、:75-90
路由模式router/index.ts:34-35
地图缺失/付费mine-monopoly-main/README.md:318-322
许可mine-monopoly-main/LICENSE(GPL-3.0)

10. 规范章节对照

设计点FlintHub 规范章节
目录结构、一目录一插件§2
plugin.json 字段、分类、图标、权限§3
Plugin.php、ddl()、ensureSchema§4
生命周期、下划线陷阱§4.1
控制器、命名空间、requireLogin§5
URL 两类、前台容器§6.2.1、§6.4
视图 section§6
assets 路径、?v=filemtime、零内联§7.1–§7.3
语言包§8
钩子清单与陷阱§9、§9.1、§9.3
独立库、事务、inTransaction§10
CSRF 顺序、建表自愈、软 404§11.28、§11.15、§11.30
开发流程、php -l、缓存清理§12
三条硬性安全规则§1
八条红线§1.5(最高级别红线)
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 68 次 | 回覆 1 次

全部回覆 (1)

👑Lv.11 元老 🌞 骨干会员
2026-11-09 17:48:12
这一版规划给我搞乱套了。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
#1 樓

請 登入