[开发]forum_required_tag 插件 · 开发计划、自动化改造与质量优化

👑Lv.11 元老 🌞 骨干会员
2026-10-25 00:59:29

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:合并篇(见正文内 merged_from 列表)。

forum_required_tag 插件 · 开发计划、自动化改造与质量优化

本篇由 4 篇原始文档合并去重而成:08-27 开发计划 + 10-13 自动化改造 + 10-13 质量优化分析 + 最终版插件说明(v1.4.1)。

一、插件定位

forum_required_tag(版块强制 Tag)解决一个具体问题:部分版块需要规范发帖格式——发帖必须携带预设 Tag,或标题必须带上前缀(如 [求助]、[分享]),否则不允许提交。核心本身不提供这类版块级约束,以插件形态实现,不修改任何核心文件。

三条设计原则贯穿全部版本:

  • 不侵入核心:只通过钩子(Hook)工作,app/ 零改动;
  • 插件独立库:数据存 plugins/forum_required_tag/data/forum_required_tag.sqlite(Plugin::db('forum_required_tag')),frt_ 表前缀防与其他插件冲突;
  • 前端零内联:样式/JS 收敛到插件 assets/,提示元素由 script.js 动态创建,不修改核心视图。

未配置任何约束的版块,行为与未安装插件完全一致(零影响)。

功能演进:初版(v1.0/1.1)强制 Tag + 标题前缀 → v1.1.0 前缀彩色徽章 → v1.3.0 标签自动化(失焦推荐)→ v1.4.0 jieba 通用词典 + FMM 分词 → v1.4.1 自动填入简化交互。最终版为 1.4.1(兼容 FlintHub 1.0 SplitDB)。

二、初版设计与落地(08-27,v1.0/v1.1)

2.1 关键约束:核心没有"提交前"钩子

草案(v1.0)依赖 thread_create_before / thread_edit_before,grep 核实核心并不存在这两个钩子——核心实际提供的相关钩子全部是 _after:

钩子名触发时机注入参数
route_before_dispatch路由分发前(app/Core/Router.php:108)无(钩子内读 $_POST/$_SERVER)
thread_create_after发帖成功后(PostController.php:160)thread_id、user_id、category_id、title
thread_edit_after编辑帖成功后(ThreadController.php:313)thread_id、user_id
post_create_after / post_edit_after回帖 / 编辑回帖后post_id、thread_id、user_id

修订方案(v1.1):校验拦截改走 route_before_dispatch(参照 plugins/mod_system/hook/route_before_dispatch.php 拦截先例),记录改放 _after 钩子(此时 thread_id 才存在)。这是整个插件的地基,后续所有版本未再动摇。

2.2 校验流程(route_before_dispatch)

  1. 非 POST → 直接 return(读操作零开销);
  2. 剥离 BASE_PATH 后解析路径:只命中 POST /post/new(发帖)或 POST /thread/{id}/edit(编辑帖)才继续,其余一律放行;
  3. 取版块 ID:发帖从 $_POST['category_id'];编辑帖优先取 POST 提交的新版块(用户可能把帖子从版块 A 改到 B,校验必须按新版块规则),未携带时才回退 (new \app\Models\Thread())->getById($id)(实例方法调用,禁止静态调用);
  4. 该版块未配置任何强制项 → 放行(0 影响);
  5. 校验 $_POST['tags'] 与 $_POST['title'];失败 → http_response_code(422) + 独立错误页 views/error.php(错误文案 + 返回按钮)。

已决策:不重定向回发帖页带错误参数——强制校验属"拦截"而非表单校验,重定向会丢失已填内容(回填列入后续优化)。

性能红线:非发帖/编辑请求 0 查询(路径白名单短路),发帖/编辑页 ≤3 次查询,符合"每页 Hook 查询 ≤ 5"。

2.3 数据表(初版 3 张,SQLite 独立库)

frt_forum_tags     -- 版块 × 强制 Tag 绑定
  id / forum_id / tag_name / sort_order / enabled / created_at, UNIQUE(forum_id, tag_name)
frt_forum_prefixes -- 版块 × 标题前缀绑定(prefix 存名不含方括号,校验/展示为 [xxx])
  id / forum_id / prefix / sort_order / enabled / created_at, UNIQUE(forum_id, prefix)
frt_post_tags      -- 发帖/编辑使用记录(thread_id / forum_id / tag_name / prefix / user_id / created_at)
-- 索引:idx_frt_forum_tags(forum_id, enabled)、idx_frt_forum_prefixes(forum_id, enabled)、
--       idx_frt_post_tags(thread_id)、idx_frt_post_tags_forum(forum_id, created_at)

DDL 拆成 Plugin.php 的 ddlTags() / ddlPrefixes() / ddlPostTags(),activate() 内逐个 ensureSchema('forum_required_tag', $ddl)(幂等,参照 plugins/points_mall/Plugin.php 的多方法 + ensureSchema 写法)。

2.4 主类与前后台

Plugin\ForumRequiredTag\Plugin:db()、三个 ddl 方法、activate() / deactivate() / uninstall()(卸载删 3 表),业务方法 getForumTags() / getForumPrefixes() / validateSubmit()(返回 null=通过,字符串=多语言错误文案)/ logPostTag()。

  • 前台:layout_head_end 仅对模板 post/create、thread/edit 输出 #frt-config 数据容器(JSON 编码)+ ?v=filemtime 资源引用;script.js 读取后动态渲染提示条(「本版块需选择 Tag:…」)与前缀选择器(点击拼 [前缀] 入标题框)。服务端是最终防线,前端选择器只是 UX 辅助,即使被绕过仍会 422 拦截;
  • 后台:/admin/forum-required-tag(admin_url 入口,不注册 admin_sidebar_links),3 个 Tab:强制 Tag / 标题前缀 / 使用记录;每行启用/禁用开关、排序、一键复制配置;POST 全 CSRF、输出全 $this->e()、ID 全 (int) 强转;
  • 多语言:lang/zh.php、en.php、zh_tw.php,键前缀 plugin.forum_required_tag.,前台交互文案用 _act 后缀。

初版非目标(列入后续可选,多数一直未做):Tag 自动补全、多 Tag 选择、Tag 热度统计、前台搜索筛选、拦截回填。

三、标签自动化改造(10-13,v1.3.0)

目标:标题失焦后自动推荐标签,用户点选即填入标签框。约束:全部改动落在 plugins/forum_required_tag/ 内,核心 app/ 零改动。

3.1 读码后的三个关键发现(直接改变方案形状)

  1. 「新词自动入 tags 表」核心已经做了,插件不用自己写库。app/Controllers/PostController.php:145(编辑帖为 ThreadController.php:457)在发帖事务里调 Tag::saveForThread($threadId, trim($_POST['tags']));app/Helpers/Tag.php:87-97 负责 INSERT INTO tags ... ON CONFLICT(name) DO NOTHING + 写 thread_tags(含并发保护、单帖上限 10 个、长度按 limit_tag_name 设置夹取)。→ 插件只要把标签名填进 input[name="tags"],落库全由核心完成,一行 INSERT 都不用写;
  2. 要抄的分词器是 app/Helpers/Segmenter.php,不是 SearchIndexStore。后者(app/SplitDB/)是分片倒排索引存储层(建表/写索引/查索引),没有分词逻辑;真正分词在 Segmenter::bigram()(app/Helpers/Segmenter.php:24-60):中文按连续 2 汉字滑窗、英文/数字整词切分并 strtolower;Search.php:277 先 Segmenter::tokenize() 再喂索引;
  3. bigram 有固有缺陷:「电脑软件」会切出 电脑/脑软/软件,"脑软"是伪词,纯词频区分不了。→ 解法:拿核心 tags 表当轻量词典(当时 39 个真实标签),命中 token 大幅加权,伪词自然沉底——正好满足「命中已有标签优先」,一举两得。

3.2 改动范围

项数量
核心 app/ 文件改动0
插件新增文件2(Tokenizer.php、data/stopwords.txt)
插件修改文件11
新增接口1(GET /api/forum-required-tag/suggest,hook/route_register.php 注册,requireLogin() 鉴权,只读 GET 免 CSRF)
新增后台路由2(保存自动化设置 / 保存停用词表)
新增数据表1(frt_settings KV)
新增 i18n 键约 18 × 3 语

plugin.json version → 1.3.0;hooks 清单不新增(不新增钩子名)→ 不需要重建 plugins_cache.json,发布后仍惯例走一次 Plugin::refresh()。保留不动:标题前缀全链、强制 Tag 全链、既有 tags/thread_tags 数据(只读)、frt_post_tags 语义、422 校验流程。

3.3 分词与排序(v1.3.0 版算法)

Tokenizer.php 纯函数实现、不 use / 不 require 任何核心类:

  • bigram():算法与 Segmenter::bigram() 一致(按 [a-zA-Z0-9]+ 切块、英文整词、中文 2 字滑窗、^[\x{4e00}-\x{9fff}]{2}$、array_unique);
  • 双层停用词(data/stopwords.txt,约 60 行默认表):1 字条目(的/了/是/在/我/你…)在切词前替换成空格(顺带切断跨停用词的伪 bigram,「我的电脑」→「电脑」);≥2 字条目(这是/我的/可以…)在切词后过滤。兜底:内置最小默认表(只放虚词),文件内容是追加不是替换,读失败/为空回落默认表,单文件读取上限 64 KB;
  • 打分:score = freqW × posW × dictW:freqW = 1 + log(出现次数);posW = 开头 6 字内 1.5 / 结尾 6 字内 1.3 / 其余 1.0;dictW = 命中本版块预设 Tag(frt_forum_tags)4.0 / 命中核心 tags 3.0 / 1.0;
  • 排序四级键(必须完全确定,否则同一标题两次请求结果不同):kind(preset > known > new)→ score 降序 → 首次出现位置升序 → 词面升序(彻底消除浮点比较不确定性);
  • Top N:auto_tag_count(2/3/4,默认 3),有效候选 <2 不硬凑,上限硬夹 4;过滤后为空前端不渲染。

匹配已有标签走公开 API \app\Helpers\Tag::getAll()(39 行,不直查核心表)。三档 kind:preset(版块预设,高亮,点选直接满足强制校验)/ known(核心 tags 已有,复用不涨库)/ new(提交时由核心自动建标签,插件不落库)。

3.4 设置存储:为什么自建 frt_settings 而不写核心 Settings

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

三个理由:① 写核心 Settings 需 plugin.json 声明 system:settings 权限,Settings::update() 有权限门禁(app/Helpers/Settings.php:259-263),未声明会 error_log 越权并静默 return;② settings_cache.php 不会自动补新键,上线新设置项必须手工清一次缓存(本仓踩过的坑);③ 与插件独立库原则一致。代价:设置项不出现在核心设置页(本来也不该出现)。键:auto_tag_enabled(默认关)/ auto_tag_count(默认 3);停用词表存文件 data/stopwords.txt(规范化 LF/去 BOM/去重/单条 ≤64 字/总大小 ≤64 KB,LOCK_EX 写,空表拒绝写入)。

3.5 前端与首屏直出

layout_head_end 在发帖/编辑页恒定直出 #frt-autotag-config(自动化设置是全局的,不能挂在只按需输出的 #frt-config 下),BASE_PATH 复用 meta[name="frt-base"]。script.js 新增 initAutoTag():绑 title 的 blur,三条守卫(标题 <3 字不触发;title === lastGenerated 不触发——只在请求成功后更新,失败可原地重试;forum_id<=0 仍可请求只是不做 preset 匹配)+ 请求序号比对丢弃过期响应。加载态「生成中…」;候选 chip 三档配色(preset 高亮/known 常规/new 虚线);已选 chip 带 ×;单一真源是 input[name="tags"] 的值(逗号分隔,解析口径与 Tag::saveForThread() 一致,所见即所提交)。容器由 JS 动态创建(沿用 insertAfterInput()),不新增 head 占位、不改核心视图。

分期:P0 Tokenizer + suggest 接口 + 失焦候选 + 落库(主链路必做);P1 Tab 4 后台设置 + 停用词编辑 + i18n;P2 auto 自动模式 + 词典最长匹配优先 + 命中率统计。最需要注意的边界:强制 Tag 版块下,若自动填的全是自由标签、一个预设 Tag 都没包含,提交仍会被 route_before_dispatch 422 拦——待拍板 A(preset 永远排最前且不足自动补一个,推荐)/ B(禁用 auto 覆盖)/ C(不管)。

四、质量优化分析(10-13,v1.4.0 候选方案)

触发:用户反馈 v1.3.0「效果太差」,提出「用 jieba 最小的那个词典 + 优化调用」。实测环境 PHP 8.2.9nts;数据来自本机真实标题与真实库(tags 39 行、frt_forum_tags 24 行)。

4.1 问题的量化根因

12 条论坛真实风格标题实测 v1.3.0 的 suggestTags():10 条含伪词,平均 1.1 个/条:

标题v1.3.0 实际输出伪词问题
求助:我的电脑软件打不开了怎么办电脑软件、求助、不开不开
PHP教程:如何配置SQLite数据库连接php、教程、数据数据库被切成 数据/据库,只剩半个
这个模板在手机上显示错位了模板、手机、机上机上
建议增加暗色主题切换按钮建议、议增、增加议增;主题/切换/按钮全丢
为什么发帖时提示标签不能为空发帖、帖时、时提两个伪词;标签丢

根因一句话:词典只有「39 标签 + 8 版块预设」,绝大多数位置匹配不到 → 落到 bigram 兜底 → bigram 对 ≥3 字真词必然切出伪词(数据库→数据/据库、打不开→打不/不开、建议增加→建议/议增/增加)。不是打分没调好,是词典覆盖不足。

4.2 jieba 最小词典可行性

三本候选词典实测对照:

词典体积词条许可
jieba extra_dict/dict.txt.small(推荐)1.48 MB109,750MIT(仓库整体)
jieba jieba/dict.txt 全量4.84 MB349,046MIT
HanLP CoreNatureDictionary.mini.txt(备选)1.08 MB85,583Apache-2.0

dict.txt.small 画像:1,552,161 B,全部 3 列(词 词频 词性),词长分布 1字 8,701 / 2字 59,842 / 3字 27,402 / 4字 11,556 / 5字+ 2,520,含拉丁词条 0 个(PHP/SQLite/SplitDB 必须继续靠「英文整词 + 站内标签库」,好在现有代码已经这么做)。丢弃 1 字词条后剩 101,049 词。覆盖实测:数据库/编辑器/主题/管理/用户/部署 等通用词全收;但 打不开/闪退/电脑软件/双层缓存/程序发布 不收——通用词典收通用词,站内 tags 表 + 版块预设收领域词,两者合并成一典才能整词命中 双层缓存。许可提示:jieba 数据来源语料授权有社区争议(MIT 只覆盖仓库文件),随包附 dict/NOTICE-jieba-dict.txt(来源仓库、版本、许可证全文链接);彻底避开则换 HanLP mini(Apache-2.0,代价:词条少 24%、格式多列要多一步解析)。

4.3 算法必须跟着换:只换词典不行

原流程「词典最长匹配 → bigram 兜底」下,未知跨度仍丢给 bigram → 打不开 照样出 打不/不开 伪词。v1.4.0 改后流程:

① 剥离单字停用词(两层机制不变)
② 正向最长匹配 FMM:词典 = jieba ∪ 站内 tags ∪ 版块预设
③ 未知跨度处理(关键,替代无脑 bigram):
     · 中文段 2~4 字 → 整段作为 1 个候选(打不开 / 闪退 才有意义)
     · 中文段 >4 字   → 才降级 bigram
     · 拉丁段         → 整段 1 个候选(保留原大小写)
④ 过滤:多字停用词 / 中文单字 / 单个纯字母
⑤ 打分:score = freqW × posW × dictW(freqW 改用真实词频对数归一)
⑥ Top N(四级排序键不变,仍完全确定)

「未知跨度整段」是关键:打不开 整段出来是有意义的新标签候选,打不/不开 是纯噪音——这是「伪词 → 0」的直接原因。同一批 12 条标题仿真:伪词 10 条 → 0 条,标签命中(known/preset)全部正确,「合并词典」设计成立。残留小瑕疵:打不开 会作为候选(合理,不想要就加停用词);好用/对比/说明 这类泛词词频不低会进候选,无客观标准可切。

4.4 「调用」怎么优化(基准实测,四种方案)

方案耗时常驻内存结论
A 每请求 file()+explode() 载入 PHP 数组109~159 ms9.4 MB❌ 2核2G 并发不可接受
B 预编译 serialize + 每请求 unserialize18~25 ms9.4 MB❌ 内存一样
C SQLite 表 + 每位置 1 次前缀范围查询0.05~0.09 ms/条≈0✅ 选它
D SQLite 表 + 每位置 7 次点查(L=2..8)0.15~0.17 ms/条≈0✅ 可行但慢 3 倍

附带:SQLite 一次性导入 101,049 词 365~442 ms(仅首次);词典表落盘 1.61 MB;C 与 D 切词结果逐字节一致(已交叉校验)。

已踩过的硬坑:前缀范围查询下界必须取 2 字前缀——SELECT word FROM frt_dict WHERE word >= :a AND word < :a || x'EFBFBF'(上界加 U+FFFF);下界取 8 字前缀会把所有更短的词排除掉(曾因此漏掉 4 字标签 双层缓存)。

导入门控:frt_dict DDL 进 ensureSchema(),但导入动作单独在 frt_settings 写 dict_version 标记门控(绝不能挂进每请求都跑的 ensureSchema(),否则每请求都 0.4 s)。

4.5 改动面与风险

改动全在插件内:新增 dict/jieba_dict_small.txt + dict/NOTICE-jieba-dict.txt;改 Plugin.php(+frt_dict DDL、+词典导入 marker 门控、+dictLookup() 桥、打分接真实词频)、Tokenizer.php(bigram 兜底 → FMM + 未知跨度)、data/stopwords.txt(补 怎么办/为什么/什么样/有没有 等标题高频疑问短语);前端 / 后台 / 接口 / 语言包不改(suggest 返回结构不变,Tab 4 不变)。兼容兜底:词典文件缺失/导入失败/SQLite 不可用 → 自动退化为静态词典(站内标签 + 版块预设),绝不白屏。风险:插件包 +1.5 MB(低)、首次导入 0.4 s(低)、词典许可争议(中)、未知跨度整段可能出短语型候选(低,停用词表兜)。

五、最终形态(v1.4.1 插件说明提取)

5.1 功能清单

功能说明入口
强制 Tag 管理版块增删 Tag、启用/禁用、排序Tab 1
标题前缀管理增删前缀(自动 [ ] 包裹)、开关、徽章配色Tab 2
前缀徽章展示标题开头 [前缀] 渲染为彩色徽章(方括号不显示),文字色按底色亮度自动适配自动生效
发帖/编辑强制校验不满足约束 → 422 拦截(服务端兜底)自动生效
使用记录frt_post_tags 按版块筛选/分页/删除Tab 3
分类懒加载提示未预选分类时,选分类后拉 /api/forum-required-tag/config?forum_id=N前台自动
标签自动化标题失焦后把标签直接填入标签框(v1.4.0 起 FMM 通用词典约 10 万词;v1.4.1 起取消候选区点击)Tab 4
一键复制配置版块间复制 Tag/前缀(含颜色),重复项跳过Tab 1/2

5.2 数据表(最终 5 张,独立 SQLite 库)

前 3 张同初版;frt_forum_prefixes v1.1.0 增 color 列(徽章底色,存量库由 ensureSchema() 内 ensureColumn() 探测 PRAGMA table_info 后 ALTER TABLE ... ADD COLUMN 幂等补列,无需手工迁移脚本,访问任意页面一次即完成):

frt_settings (key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '')          -- v1.3.0 KV 设置
frt_dict (word TEXT PRIMARY KEY, freq INTEGER NOT NULL DEFAULT 1,
          pos TEXT NOT NULL DEFAULT '') WITHOUT ROWID                        -- v1.4.0 通用词典

  • frt_dict 导入参数:DICT_VERSION='2'(加 pos 列后 +1,标记不匹配即 DROP 重建);导入后实收 100,928 词(源 109,750 行,丢单字 8,701),词频区间 10~142,747;frt_settings 另存 dict_ln_min / dict_ln_span 归一化常数;
  • 刻意不写核心 settings 表(权限门禁 + 缓存不自动补键 + 独立库原则,见 §3.4)。

5.3 目录结构(关键在 dict/ 与 data/ 之分)

plugins/forum_required_tag/
├── plugin.json            # 版本 + 7 个 hooks 清单
├── Plugin.php             # db()/ddl×5/ensureSchema(+补列)/activate/deactivate/uninstall
│                          #  + 业务方法 + 徽章配色工具(normalizeHex/contrastText)
│                          #  + 标签自动化(设置 KV/停用词/suggestTags)+ 词典(dictPath/importDictIfNeeded/dictReady/buildLookup)
├── Tokenizer.php          # 纯分词器(FMM + 未知跨度 + 双层停用词 + 词性加权打分),与核心零耦合
├── AdminController.php / FrontController.php
├── hook/                  # init_after / admin_route_register / route_register / route_before_dispatch
│                          # / thread_create_after / thread_edit_after / layout_head_end
├── views/                 # admin.php(4 Tab)+ error.php(422 页)
├── assets/                # style.css / script.js
├── lang/                  # zh.php / en.php / zh_tw.php
├── dict/                  # v1.4.0 随包静态资源:jieba_dict_small.txt + NOTICE-jieba-dict.txt
│                          # + .htaccess / web.config(拒 HTTP 下载)
└── data/                  # 运行数据:forum_required_tag.sqlite + stopwords.txt + frt_badge_map.json

踩坑记录:词典必须放 dict/ 不能放 data/——打包器(cli/release_build.php + Updater::PLUGIN_EXCLUDES)硬排除 plugins/<名>/data/(运行数据,覆盖会毁站),词典留在 data/ 就不进包 → 用户站 dictReady() 永远 false → 分词器静默退回静态词典:不报错、不白屏、最难发现。v1.4.1 已加自检:词典必须出现在发布包 manifest.files 中,且「只有包内文件」的全新站点上 dictReady() 能变 true。

5.4 徽章渲染机制(v1.1.0 起)

核心 8 处视图(forum/index.php、forum/category.php、home/index_default.php、thread/show.php、search/index.php、profile/index.php、tags/show.php、_components/right_sidebar.php)标题都经 $this->e() 转义,服务端注入 HTML 会被转义 → 采用「服务端直出映射 + 浏览器侧渲染」:

① layout_head_end 直出 
   (JSON_HEX_TAG / JSON_HEX_AMP 转义 < > &,前缀名含  也无法提前闭合)
② script.js 扫描标题元素,把开头 [前缀] 的文本节点替换为 
   文字色 = pickTextColor(底色),与 PHP Plugin::contrastText() 同一算法(WCAG 对比度取高者,216 色全量比对一致)
③ 幂等:处理过的元素打 data-frt-badge="1";MutationObserver(200ms 防抖)+ htmx:afterSwap 覆盖局部刷新

扫描范围与 BADGE_SELECTOR 一致:a[href*="/thread/"]、.thread-title(详情 h1)、.mn-thread-card-title(右栏卡片);只处理元素内第一个非空文本节点,只替换命中映射的前缀。颜色:normalizeHex() 校验 #rgb/#rrggbb,非法/空 → 默认 #1d4ed8;后台仅提供内置固定色板(PRESET_BADGE_COLORS 约 8 个深色阶,统一白字),不允许任意取色。缓存:正常前台0 次查库只读 data/frt_badge_map.json,后台任一写操作 flushCaches() 删文件 + PageCache::invalidate()(否则静态页缓存导致"改了颜色前台不变");CSS/JS ?v=filemtime 且仅存在启用前缀时才输出(未配置前缀的站点零额外请求)。同名前缀跨版块按前缀名全局化,取 id 最小的一条。代价:首帧短暂显示原始 [前缀]、JS 禁用退化为纯文本(内容与 SEO 不受影响)——这是零核心侵入约束下的必然取舍。

5.5 标签自动化最终形态

  • v1.4.1 交互简化:前台没有候选区 / 已选 chips。标题失焦(≥3 字、与上次生成标题不同)→ /api/forum-required-tag/suggest → 直接把标签写进 input[name="tags"](逗号分隔);想删就在框里删、想加就手打。用户一旦手动编辑过标签框(含编辑页预填的已有标签)→ userTouched 标志后本页不再自动覆盖。接口与数据层完全不变,默认开启(Tab 4 可关);
  • 分词内核(v1.4.0):FMM 词典 = 站内标签 ∪ 版块预设 ∪ jieba(frt_dict SQLite 查询);buildLookup() 先在内存用几十个标签做最长匹配,标签没命中才查通用词典(否则 插件开发 会吃掉标签 插件——本功能目的是把已有标签顶出来,不是做完美切词);未知跨度 2~4 字整段(GAP_WEIGHT 0.7)、>4 字 bigram、拉丁段整段;
  • 打分:score = w × posW × dictW × kindW:w = 词典词归一化词频 0.6~1.0(同词多次出现 ×(1+0.1×(次数-1))),未知跨度词 0.7;posW 开头6字 1.5/结尾6字 1.3/其余 1.0;dictW preset 4.0/known 3.0/其它 1.0;kindW 名词(n 打头)1.25 / 动名词(vn,an)1.15 / 其余有标注(v,a,d)0.85 / 无标注 1.0。排序四级键:kind(preset>known>new) → score 降序 → 首次出现位置 → 词面升序(完全确定,同一标题多次请求 md5 一致,CLI 单测守着);
  • 保底路径:buildLookup() 在词典未就绪 / prepare() 失败 / 单次查询失败时返回 null → Tokenizer 退化为静态词典(v1.3.0 行为);导入失败不写标记,下次请求自动重试;首次导入约 0.6 s(一次性),之后每请求只做极轻量标记比对(<1 ms);
  • 新标签进 tags 表:插件一行 INSERT 都没写,全靠核心 Tag::saveForThread();frt_post_tags 语义不变(只记实际命中的预设 Tag/前缀)。

5.6 验证资产(复用)

位于 .workbuddy-ai/tmp/:frt_v14_probe.php(v1.4.0 逻辑探针:伪词黑名单 + 真词必中 + 输出确定性 + 单条耗时 + 保底路径)、frt_smoke.sh(HTTP 冒烟,38 PASS 基线:Tab4/开关/停用词读写/候选接口/CSRF 负例/越界夹取)、frt_regress.sh(回归,33 PASS:前缀链路/旧接口/后台 Tab1~3)、frt_autofill_jsdom.cjs(v1.4.1 jsdom 回归,11 组/20 断言:失焦写入/未变不请求/手动改过不覆盖/编辑页预填不覆盖/开关关闭零请求/无候选区 DOM)、frt_state.php / frt_mksess.php(快照还原/会话夹具)、徽章专项 verify_frt_badge.php 等。i18n 三语各 65 键全量对齐(fh_i18n_check.php)。

六、遗留与后续

  • 422 拦截不回填已填表单:初版决策(强制校验非表单校验,重定向丢内容),回填至今仍列在后续优化;
  • 多 Tag 选择 / Tag 热度统计 / 前台搜索筛选:初版非目标,一直未做;
  • 候选泛词无客观切分标准:好用/对比/说明/新版 词频不低、位置不差,只能靠停用词表人工兜;
  • jieba 词典许可争议:已随包附 NOTICE-jieba-dict.txt;彻底避开需换 HanLP mini(Apache-2.0,词条少 24%、词 词性 词频 格式解析多一步);
  • 词典替换流程:换词表需同步改 importDictIfNeeded() 的行解析格式并把 DICT_VERSION +1 → 下次请求自动重导入;
  • 徽章服务端直出:若核心愿意提供标题渲染钩子(把 8 处 $this->e() 换成可过滤的标题渲染助手),可去掉"首帧原始前缀"的取舍,平滑切换;
  • 插件包 +1.5 MB:dict/jieba_dict_small.txt 随包的体积代价(已决策随包:生产是纯 Web 无 SSH,联网下载不可靠;裁剪高频子集会误删 编辑器(词频 25) 这类词导致重新退化);
  • 三语 65 键持续同步:每次新增文案需 zh/en/zh_tw 同改,靠 i18n 全量比对兜底。

本文件由 4 篇原始文档合并去重而成,保留全部表名、文件路径、配置键与函数名;演进口径以最终版插件说明(v1.4.1)为准。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 1 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录