版本:v1.0(2026-11-08) 本版替换 v0.1。v0.1 将目标平台误判为另一个独立程序,其插件形态、钩子名、路由注册、数据表前缀、后台接入方式均已作废;本版按 FlintHub(
D:\phpstudy_pro\WWW根目录程序)的真实结构重写。 唯一权威规范:docs/插件开发规范.mdv1.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/ | 已定(用户拍板) |
| D2 | Node 仅构建期,运行时不部署 Node/Express/MySQL | 成立(FlintHub 为 PHP,无 Node 运行时入口) |
| D3 | 官方地图双轨:自研地图 + 授权导入 | 成立(仓库无地图文件,硬阻断不变) |
| D4 | 插件 assets/game/ 静态直出 | 成立且简化:FlintHub 的 assets/ 本身可被 Web 直接访问(规范 §2、§7.1),不需要额外的静态路由或回调输出 |
| D5 | LLM 必须走 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 三个硬阻断(必须先解决)
- 地图缺失:仓库内无任何
.mmmap/.fpmap文件;官方地图需在爱发电支持作者后获取(mine-monopoly-main/README.md:318-322)。无地图则游戏无法开局,M1 前必须有自研地图或书面授权。 - LLM 必须代理:CSP 未声明
connect-src,回退default-src 'self'(app/Core/SecurityHeaders.php:150),浏览器直连外部 API 必被阻断;Plugin::http()(app/Helpers/Plugin.php:503)禁止内网与 localhost,本地 Ollama 方案在 FlintHub 下不可行。 - i18n 缺口:规范 §8 要求至少
lang/zh.php,而游戏 UI 无 i18n 框架(仓库检索vue-i18n|locale无匹配),全部中文硬编码。插件外壳可 i18n,游戏本体短期只能固定中文。
1. FlintHub 平台契约(按真实结构核实)
1.1 运行形态
| 项 | 实际情况 | 证据 |
|---|---|---|
| 语言/运行时 | PHP 8 + SQLite | index.php、app/Core/Database.php:5 |
| 核心版本 | V1.5.11 | version.php:8 |
| 许可证 | MIT | LICENSE: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.mdP0-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 只在构建机出现)
- base:
apps/client/vite.config.ts现默认 web base 为/monopoly/(24–30 行),改为相对./或/plugins/monopoly/assets/game/。站点可能装在子目录,推荐base: './',配合 hash 路由,避免BASE_PATH变化导致资源 404。 - 路由:
createWebHistory→createWebHashHistory,iframe 内#/...永不请求 PHP。 - Worker:Vite 默认输出同源 chunk,满足
worker-src 'self';不要引入 CDN worker。 - draco:
decoderPath不能用相对./(它相对文档而非模块),由视图注入data-game-base,bundle 读取后用new URL(..., document.baseURI)拼绝对地址。 - 音频/字体:全部落到
assets/game/同源目录;CSP 无media-src/font-src,CDN 一律不可用。 - manualChunks(
vite.config.ts:75-90)保留,three/vue 分包,首屏更快。 - 产物清理:
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)兜底。 - 约束:
temperature0.2、max_tokens512、超时 ≤ 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) |
| LLM | RateLimiter::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 必需
| 项 | 要求 |
|---|---|
| PHP | 8.0+(与核心一致),扩展 pdo_sqlite、curl、mbstring、json、openssl |
| Web | Nginx/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 | /monopoly | FrontController::index | 大厅(登录) |
| GET | /monopoly/play | FrontController::play | iframe 容器(登录) |
| POST | /monopoly/api/start | FrontController::start | 创建对局与 matchToken |
| POST | /monopoly/api/llm | FrontController::llm | LLM 代理 |
| POST | /monopoly/api/finish | FrontController::finish | 结算、发积分、写排行 |
| GET | /monopoly/rank | FrontController::rank | 排行榜 |
| GET | /admin/monopoly | AdminController::index | 后台设置 |
| POST | /admin/monopoly/save | AdminController::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,游戏本体中文硬编码。选项:
- 首版仅中文,
en.php/zh_tw.php只翻译插件外壳与报错(推荐,成本最低)。 - 先做游戏内文案抽取(工作量大,需改大量 Vue 模板)。
- 不做语言包(不符合规范 §8,不可取)。
D8 同源 iframe(成立)
frame-ancestors 'self' 允许;postMessage 使用一次性 matchToken。
D9 首版不含联机(新增)
P2P 需要外部信令与 TURN,CSP 会阻断外部 WebSocket,且 FlintHub 无 Node 运行时。联机若要做,需自建同源信令服务并改 CSP,超出本插件范围。
9. 证据索引
| 结论 | 证据 |
|---|---|
| 核心版本 V1.5.11 | version.php:8 |
| 核心 MIT | LICENSE:1 |
| 业务库 SQLite | app/Core/Database.php:5 |
| 插件类名硬拼 | app/Helpers/Plugin.php:699、:750、:846、:1793 |
| 自动加载回退 | app/Core/Autoloader.php:36-70 |
Plugin::http() / SSRF | app/Helpers/Plugin.php:503 |
| 控制器 API | app/Core/Controller.php:20、:159、:238 |
| CSRF | app/Helpers/Csrf.php:16、:60 |
| 积分 | app/Helpers/Points.php:207、:239 |
| 限流 | app/Helpers/RateLimiter.php:250、:362 |
| CSP 全文 | app/Core/SecurityHeaders.php:150 |
图标 dice | app/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(最高级别红线) |