根据自己的需求,研究一下吧,我自己用了好长时间了。
版本:1.0.0 | 适用:FlintHub 社区系统 |
日期:2026-08-26
本文档记录:插件说明、目录结构、数据表、核心机制、开发过程、踩坑记录、性能优化演进、部署与验证。
一、插件简介
AI 助手插件:配置多个"AI 会员"(每个绑定一个真实论坛账号),按触发规则在指定板块自动回复新帖/新回复。回复以绑定会员身份发布(+2 积分,受每日上限约束),支持敏感词过滤、广告检测、可选全量审核模式,所有自动回复操作全量记日志。
设计定位:论坛非即时聊天性质,AI 回复不追求秒回,重可控、重审核、重日志。
核心功能
| 功能 | 说明 |
|---|---|
| AI 会员管理 | 多个 AI 会员,各自独立 API 地址/Key/模型/回复风格/温度/最大长度 |
| 触发规则 | 会员 + 板块(0=全部)+ 回复类型(新帖/新回复/两者)+ 延迟 + 每帖上限 + 冷却 + 排除板块 |
| 敏感词过滤 | 词库 + 白名单,ArrayFilter / TrieFilter 双引擎可切换,TXT 批量导入 |
| 广告/灌水检测 | 启发式(空内容/过短/多链接/纯链接/重复字符),可开关 |
| 审核模式 | 可选:所有回复先进"待审核",日志页"通过发布"才发出 |
| 积分 | +2/条,daily_points_limit 每日上限(0=不限) |
| 全量日志 | pending/success/failed/skipped/pending_review/restored 全状态记录 |
| 异步消费 | 发帖/回帖后自动派发后台子进程生成,页面永不卡;日志页锚点 + 手动按钮兜底 |
二、目录结构
plugins/ai_assistant/
├── plugin.json # 插件配置(name/version/hooks/admin_url,icon: robot)
├── Plugin.php # 主类:6 表 DDL + 生命周期 + 队列消费 + AI 调用 + 过滤 + 积分 + 派发
├── AdminController.php # 后台控制器:四 tab(会员/规则/敏感词/日志)+ 设置 + 审核/恢复/分批处理
├── SensitiveWordFilter.php # 敏感词过滤引擎(接口 + ArrayFilter + TrieFilter + 工厂缓存)
├── WordTrie.php # 前缀树实现(大词库引擎)
├── hook/
│ ├── init_after.php # 惰性建表兜底(正确大小写 activate())
│ ├── admin_route_register.php# 后台路由(含 flush-batch/approve/restore)
│ ├── thread_create_after.php # 发帖后:匹配规则入队 + 自动派发子进程
│ ├── post_create_after.php # 回帖后:同上
│ └── route_after_dispatch.php# 【已停用占位】原页面访问自动消费,改为 return
├── cli/
│ ├── worker.php # 队列消费端(--once 全量 / --max N 限批,CLI-only 守卫)
│ └── diag_thread.php # 主题读取链路诊断(只读)
├── views/admin.php # 后台四 tab 视图
├── assets/
│ ├── style.css # 后台样式(卡片化表格/徽标/标签)
│ └── script.js # 表单确认 + "立即处理"AJAX 分批轮询
├── lang/ # zh.php / en.php / zh_tw.php(plugin.ai_assistant.* 前缀)
└── data/ # 自动生成:ai_assistant.sqlite + 队列文件 + 锁文件(.htaccess 拦 Web)
三、数据表(SQLite,独立库)
| 表 | 用途 | 关键字段 |
|---|---|---|
ai_assistant_members |
AI 会员配置 | user_id(绑定真实账号,UNIQUE), api_url, api_key, model, system_prompt, temperature, max_tokens, enabled |
ai_assistant_rules |
触发规则 | member_id, category_id(0=全部), reply_type(thread/reply/both), delay_seconds, per_thread_limit, cooldown_minutes, exclude_category_ids(JSON), enabled |
ai_assistant_logs |
自动回复日志 | rule_id, member_id, user_id, trigger_type/id, thread_id, status, reason, request_payload, reply_content, reply_post_id, points_awarded, created_at, processed_at |
ca_sensitive_words |
敏感词库 | word(UNIQUE), category, enabled |
ca_sensitive_word_whitelist |
白名单 | word(UNIQUE), enabled |
ai_assistant_settings |
全局设置 | key/value:daily_points_limit, filter_driver, spam_check, ai_timeout, review_mode, max_batch_size |
ai_assistant_marker |
持久化标记 | key/value/updated_at:pending_flag, last_processed_id, last_processed_at |
队列:复用核心 Queue::push($type, $data, $queueIndex, $dataPath),dataPath 指向插件 data/,队列文件落在 data/meta/task_queue/queue_0~2.sqlite(.htaccess 已拦 Web)。
四、核心机制
4.1 异步消费链(页面永不卡)
发帖/回帖 → hook 入队(毫秒级)→ spawnWorkerThrottled() 派发后台子进程 → 页面立即返回
↓
后台子进程(独立 php.exe):调 AI(1~10s)→ 敏感词过滤 → 审核模式判断 → 发布回复 → 更新日志 → 队列清空退出
消费触发源(三级):
- 发帖/回帖钩子自动派发(
spawnWorkerThrottled,≥5s 节流)—— 主路径 - 后台日志页锚点(
AdminController::index()中$tab==='logs'时 spawn)—— 兜底 - "立即处理"按钮(手动,proc_open → popen → AJAX 每批 2 轮询)
4.2 子进程派发三级降级(spawnWorkerOnce)
① proc_open(stdin/stdout/stderr 重定向 NUL,防阻塞/防弹窗)→ ② popen → ③ 返回 false → AJAX 分批
拦截逻辑:无任务标记不派发 / worker 正在跑不重复派发 / 无到期任务不派发。
4.3 队列任务状态机
pending → processing → done/failed;CAS 抢占(UPDATE ... WHERE status='pending')防多 worker 重复处理;processing 超时 300s 自动恢复重试(≤3 次)。
4.4 审核与过滤
- 敏感词命中 →
skipped(原文保留)→ 日志页"恢复发布" review_mode=1→ 全部回复进pending_review(不发布)→ 日志页"通过发布"- spam 检测命中 →
skipped(记录具体原因)
4.5 积分
Points::award(+2),受 daily_points_limit 每日上限约束(达上限回复照发但 points_awarded=0)。
五、后台使用指南
后台入口:插件管理 → AI 助手(/admin/ai-assistant,未注册侧边栏,符合新插件规范)。
Tab 1:AI 会员配置
- 新增:绑定真实会员 ID(数字,后台用户管理里查)、昵称(后台显示用)、API 地址(完整 chat/completions 端点)、Key、模型、回复风格(system prompt)、温度、最大长度
- DeepSeek 示例:URL
https://api.deepseek.com/chat/completions,模型deepseek-v4-flash(旧名 deepseek-chat 已停用)
Tab 2:触发规则
- 板块 = 白名单范围(0=全部);排除板块 = 范围内剔除(黑名单);两者配合
- 回复类型:仅新帖 / 仅新回复 / 两者
- 每帖上限(per_thread_limit)、冷却分钟(同会员跨主题全局)、延迟秒(due_at 最早处理时间)
- 全局设置:每日积分上限、过滤引擎、广告检测、AI 超时、全量审核开关、单次处理任务上限(max_batch_size,默认 5,低配服务器防一次性大量 AI 调用)
Tab 3:敏感词管理
- 增删改 + TXT 批量导入(每行一词,可
词|分类,# 开头为注释)+ 白名单
Tab 4:自动回复日志
- 全状态徽标:待处理/已回复/失败/已拦截/待审核/已恢复
- 操作:立即处理(AJAX 分批)、通过发布(pending_review)、恢复发布(skipped)、删除
- 标题旁"待处理 N 个任务"角标
CLI
php plugins/ai_assistant/cli/worker.php --once # 全量消费到队列清空
php plugins/ai_assistant/cli/worker.php --once --max 20 # 限批(cron 场景)
php plugins/ai_assistant/cli/diag_thread.php 123 # 主题读取链路诊断
六、开发过程与架构演进(关键决策时间线)
- 初版:按需求实现会员/规则/日志三表 + thread_create_after / post_create_after 钩子入队 + 敏感词过滤(内置)+ 积分。
- 异步化:核心队列消费者只认
rebuild_search/stats类型,自定义类型会被丢弃 → 改用Queue::push的$dataPath参数,任务推进插件自有队列;route_after_dispatch(index.php 现成钩子)页面访问自动消费,不依赖 cron。 - 回包优化:消费前先回包(fastcgi_finish_request / Connection:close)避免页面等待 AI。
- 子进程化:IIS 下回包技巧失效 →
spawnWorkerOnce()用start /B派发独立 php.exe 跑 worker,页面零等待;后升级为 proc_open 优先 + popen 降级。 - marker 持久化:页面路径从"每次扫 SQLite 队列"优化为"纯文件/数据库标记检查"(
ai_pending_marker文件 →ai_assistant_marker表),零 SQLite 打开。 - 大队列分批:
drainDue加maxTasks参数,worker 全量/限批可选,防一次性卡死。 - 触发点收敛(方案 A):
route_after_dispatch停用(改 return 占位),唯一锚点 = 日志页。 - 自动回复回归(最终):发帖/回帖钩子直接
spawnWorkerThrottled()自动派发(≥5s 节流),实现"发帖后自动回复、页面不卡",日志页锚点 + 手动按钮保留兜底。
架构演进主线:同步调 AI → 队列异步 → 页面回包 → 子进程 → 触发点收敛 → 钩子自动派发。核心不变式:消费永不占用页面请求进程。
七、踩过的坑(开发记录,防再犯)
7.1 钩子名与需求不一致
需求写的是 thread_created_after / reply_created_after,核心实际钩子是 thread_create_after / post_create_after(参数 thread_id/user_id/category_id、post_id/thread_id/user_id)。开发前必须先 grep 核心代码确认钩子真实名称,不能照需求字面写。
7.2 核心 Model 的 find() 是实例方法(严重)
\app\Models\Thread::find() / Post::find() 是实例方法,静态调用会抛 Non-static method ... cannot be called statically。曾导致 worker 读主题全部失败,被 catch 吞成笼统的 "thread not found",排查数轮才发现。
教训:核心 Model 一律 (new \app\Models\Thread())->find();异常信息不能笼统吞掉,要记录真实原因(后改为 thread read error: <真实异常>)。
7.3 enqueue 去重写死"每主题只回 1 条"
入队去重逻辑写成"同规则同主题有任一条日志(含 success)就跳过",把 per_thread_limit 完全架空——即使设置"每主题 5 条"也只回 1 条。
修复:去重只拦截未处理的 pending;已回复条数由消费端按 per_thread_limit 统计(success/restored ≥ 上限才拦)。
7.4 spam 检测误伤正常帖子
原规则"单链接且正文 <60 字"判广告,程序介绍帖(短正文 + 1 个下载链接)被误拦。
修复:改为"链接之外说明文字 <8 字才算纯链接广告";链接 ≥3 才判多链接广告;日志记录具体命中原因(spam: many_urls(3) / link_only(1))。
7.5 语言键缺失显示键名原文
AI 会员下拉框占位用了 plugin.ai_assistant.please_select,三个语言包都缺这个键 → 下拉框直接显示键名。修复后补脚本做了"视图用键 vs 语言包"全量比对(87 键 × 3 语言零缺失)。
7.6 IIS 环境 fastcgi_finish_request 不存在
该函数是 PHP-FPM 专属,IIS + FastCGI 下不存在;Connection: close 回包技巧也被 IIS 接管失效(PHP 进程不退出,IIS 不关响应流)。IIS 环境必须走子进程/AJAX,不能依赖回包。
7.7 spawnWorkerOnce 盲目派发导致"再次进帖卡"
原实现只要页面访问间隔超节流就无条件启动 php.exe(哪怕 worker 正在跑/没任务),Windows 进程创建 + PHP 框架加载几百 ms 叠加在每次访问上。 修复:拦截① 无任务标记不派发;拦截② worker 在跑(flush 锁探测)不派发;拦截③ 无到期任务不派发。
7.8 spawn 拦截①与持久化标记不一致
spawnWorkerOnce 拦截①原用 is_file() 检查文件信号,而 enqueue 持久化后只写数据库标记时会被误拦(实测发现)。
修复:统一改用 hasPendingWork()(数据库 pending_flag 优先 → 文件信号兼容 → 队列实查兜底)。
7.9 时间预算无法中断单个 AI 调用(设计缺陷)
曾做"小队列 ≤50 同步消费(预算 2s + max 5)",但预算只在任务之间检查,单个 AI 调用 1~10s 无法被中断 → 页面内直接调 AI,仍会卡。 修复:彻底移除页面内同步消费,任何队列规模都走子进程(子进程启动 ~百毫秒远优于等 AI)。
7.10 核心 Plugin::activate 大小写问题(项目已知坑)
核心 \app\Helpers\Plugin::activate('ai_assistant') 用全小写类名做 class_exists,本环境命名空间段大小写敏感导致失败跳过建表。建表必须靠 init_after 钩子直接调正确大小写 \Plugin\AiAssistant\Plugin::activate()。
7.11 大队列一次性全量消费卡后台
"立即处理"按钮原为同步全量(上限 100 个 × AI 最长 10s ≈ 16 分钟最坏),后台页面挂死。
修复:flushPending 改为派发子进程(页面立即返回);drainDue 加 maxTasks 参数分批;AJAX 降级每批 ≤2。
八、性能优化点汇总
| 优化 | 手段 | 收益 |
|---|---|---|
| 异步队列 | Queue::push + 插件自有 dataPath |
入队毫秒级,页面不等待 AI |
| 消费触发源收敛 | 钩子自动派发 / 日志页锚点 / 手动按钮 | 行为可预期,无随机触发 |
| 子进程隔离 | proc_open → popen → AJAX 三级降级 | 消费永不占页面请求进程 |
| 页面零 SQLite | marker 持久化 + hasPendingWork() 短路 |
普通用户页面路径 ≈ 文件 stat 毫秒级 |
| 队列防堆积 | CAS 状态机 + 300s 超时重试 + 每批限量 | 不重复、不遗漏、不卡死 |
| 大队列分流 | 计数截断(countPendingTasks)+ 子进程全量 | 小队列快、大队列稳 |
| 节流防抖 | spawn ≥5s 节流 + spawn/flush 文件锁 | 高频发帖/并发访问不重复起进程 |
| 延迟可控 | due_at 最早处理时间 | 回复节奏可调(防秒回显假) |
九、部署与验证
9.1 上传清单(完整部署)
plugins/ai_assistant/ 整个目录上传
9.2 部署后必做
- OPcache 刷新(IIS 重启 PHP 进程池 / 等 TTL 过期),否则跑的是旧代码;
- plugins_cache.json:仅在改动
plugin.json的 hooks 清单时需要重建(删服务器plugins/plugins_cache.json让其自动重建)。日常改代码不需要; - 数据库表:
init_after钩子自动幂等建表(含 marker 表),无需手动迁移,真实数据不受影响。
9.3 验证清单
| 步骤 | 预期 |
|---|---|
| 后台插件管理页 | AI 助手正常加载,图标 robot |
| 新增 AI 会员(真实 UID) | 保存成功,列表显示 UID |
| 新增触发规则(全部板块 / 新帖+新回复) | 保存成功 |
| 发新帖 | 页面立即返回(无卡顿),日志页出现任务 |
| 进日志页 | 标题旁显示"待处理 N 个任务"角标,任务自动生成 |
| AI 回复发布 | 以绑定会员身份发出,+2 积分(受每日上限) |
| 敏感词命中 | 日志 skipped + 原文保留 → 可"恢复发布" |
| review_mode 开启 | 全部回复 pending_review → "通过发布"后发出 |
| 大队列 | 子进程全量消化 / 立即处理按钮 AJAX 分批,页面不卡 |
9.4 常用 CLI
php plugins/ai_assistant/cli/worker.php --once # 手动全量消费
php plugins/ai_assistant/cli/worker.php --once --max 20 # cron 限批(可配计划任务)
php plugins/ai_assistant/cli/diag_thread.php 123 # 主题读取链路诊断
十、已知权衡与后续建议
10.1 已知权衡
- 子进程依赖命令执行能力:
proc_open/popen被disable_functions禁用时降级 AJAX 分批(功能不丢,交互变轮询); - IIS 回包限制:IIS + FastCGI 下 PHP 进程不退出、IIS 不关响应流 → 必须走子进程/AJAX,不能依赖
Connection: close(已按此设计); - 子进程资源占用:大队列时子进程全量消费可能跑几分钟,但不占页面请求进程;可用 cron
--max N分批; - 回复时效:发帖后自动派发即生成(秒级~分钟级),若想更及时可配 cron 常驻/定时。
10.2 后续可选优化
| 方向 | 思路 |
|---|---|
| 常驻 worker | supervisor/systemd / Windows 计划任务常驻 cli/worker.php 循环消费,回复最及时 |
| 定时 cron | 每分钟 --once --max 20 自动消化,不依赖页面访问 |
| 消息队列中间件 | 高并发吞吐需求时换 Redis/Beanstalkd(当前论坛量级属过度设计) |
| 待处理角标强化 | 日志页加"仅看待审核/已拦截"筛选器,批量通过/驳回 |
| AI 风格模板 | 按板块/会员预置多套 system prompt 模板,后台一键切换 |
附录:安全与规范符合性
- ✅ 三条硬性规则:无 basename 场景(无用户文件名落盘);全部 SQL
prepare+ 参数绑定 +(int)强转;输出全$this->e()/htmlspecialchars - ✅ 八条红线:无 unserialize/eval;路径全
__DIR__拼接;渲染钩子只 return;无上传(TXT 导入只读内容 + basename + 扩展名白名单);IN 查询强转占位符;PDO 属性不覆盖;AJAX/表单全 CSRF - ✅ 零内联 CSS/JS(收敛 assets/);不改核心文件;一目录一插件;语言键
plugin.ai_assistant.* - ✅ 插件间零耦合(外部过滤经钩子事件解耦,预留
ai_assistant_content_filter扩展点) - ✅ 卸载自检:7 表 + 队列文件 + 标记全清理,
plugins_cache.json由系统重建