车东 Che, Dong

Thu 01 October, 2026

22:57 fix: cache_fts 的 title 列补回 cache 表,虚拟表恢复可查询 » Recent Commits to phpman:master
fix: cache_fts 的 title 列补回 cache 表,虚拟表恢复可查询

cache_fts 建表时写的是 fts5(mode, name, section, title, content='cache'),
但 cache 表从来没有 title 列。外部内容表(external content)自己不存列值
—— 它按列名从 cache 里读回来。于是 syncFts() 每次写入都在索引一个标题,
却没有任何查询能把它读出来:SELECT COUNT(*) FROM cache_fts 直接
"no such column: T.title",只有 MATCH 能用。

cache 表补上 title 列(schema v6 → v7),set() 与索引一起写,标题抽取
从 syncFts() 提到 extractTitle(),两边拿到同一个值。列名本来就写在虚拟
表的声明里,缺的一直是实体列。

应用里没有任何地方查这张表(检索走 search_fts),所以这是潜伏问题而非
用户可见故障;只有单元测试在用它。

在 263MB 的 man 分片和 30MB 的 info 分片的生产快照上演练过 v5 → v7 全
程(生产还停在 v5):行数不变(40958 / 4869),COUNT(*) 由失败变为
40958,MATCH 与 fts integrity-check 均正常,带 title 的 UPSERT 两种路径
都可执行。

Co-Authored-By: Claude code 2.1.285 with deepseek-flash <noreply@taotoken.net>
20:02 fix: 错段号与退役的 /mcp 后缀 301;移除 cache.generator_version;补 cmd-input label » Recent Commits to phpman:master
fix: 错段号与退役的 /mcp 后缀 301;移除 cache.generator_version;补 cmd-input label

/man/ 下每天约 6,700 条 404 里约一半来自两件事:错段号(/man/mount/1
404,而 /man/mount/8 200),和 78d2a11 退役后仍被当作 section 解析的
/mcp 后缀。命令只存在于唯一一个 section 时,现在 301 到它的规范 URL 并
保留格式后缀(/man/dmsetup/mcp → /man/dmsetup/8);跨多个 section 的命令
仍然 404 —— 它指的是哪一个,URL 没说。

cache.generator_version 只写不读:PageCache::get() 只取
id/content/status/ttl/updated_at,全库没有一处按它过滤,所以它从未做过
注释里声称的"哪一版产生的这条缓存"追踪。列与写入代码一并删除,schema
5 → 6。分片没有 meta 表,版本号改存 SQLite 头的 PRAGMA user_version,
首次连接时迁移。

showForm() 的文本框补上视觉隐藏的 <label for="cmd-input">(.sr-only)。

test_user_scenarios.php 的 U07/U08/U09/U11 期望值与代码脱节:/tldr 路由已
在 e70318a 移除、CSS/JS 已在 b908fcb 外链、404 文案在 10939fd 改过。

Co-Authored-By: Claude code 2.1.285 with glm-5.3-flash <noreply@taotoken.net>
18:31 fix: OSC 8 链接限制 scheme 白名单,并修 OSC 剥离的顺序问题 » Recent Commits to phpman:master
fix: OSC 8 链接限制 scheme 白名单,并修 OSC 剥离的顺序问题

安全审查指出 OSC 8 转链接引入了新的 href 出口:原来的 URL 链接器只匹配
scheme://,而转链接会原样输出任意 URI。改为白名单:http/https/ftp/mailto
绝对地址,或无 scheme 的相对地址;其余(javascript:、data:、vbscript:)
不匹配,序列被丢弃、文字保留。URI 字符集同时排除引号和所有空白/控制字节
——浏览器解析 URL 前会剥掉前导空白,否则 " javascript:" 能绕过 scheme 检查。
mailto: 单独列出,因为它没有 "//"。

另外修两处顺序/匹配问题:
- 通用 OSC 剥离必须在 &<> 变成 \x05\x06\x07 占位符之前跑。它接受 BEL 作
  终止符,而 ">" 的占位符正是 \x07,会满足该判断,把剥离截断在序列中间、
  漏出后半段(data: 带 <script> 的用例就是这么暴露的)。
- 链接文字里的 HTML 仍由占位符机制转义(新增用例确认 <script> 变
  &lt;script&gt;),不需要额外转义,否则会二次转义。

Co-Authored-By: Claude code 2.1.285 with glm-5.3-flash <noreply@taotoken.net>
18:20 fix: man 页的 OSC 8 超链接转成真链接,不再泄漏成 ]8;; 垃圾 » Recent Commits to phpman:master
fix: man 页的 OSC 8 超链接转成真链接,不再泄漏成 ]8;; 垃圾

groff 对使用 .UR/.UE 的 man 页(netpbm 等)会输出 OSC 8 超链接:
ESC ] 8 ; params ; URI ST  TEXT  ESC ] 8 ; ; ST,整对序列原样进了页面,
pbmtoybm(1) 上直接显示成 "]8;;index.html#commonoptions\ Common Options]8;;\"。

转成真链接而不是剥掉:下面的 URL 链接器只匹配绝对的 scheme:// 形式,
而这些目标多是相对路径(index.html#commonoptions),剥掉就把目标丢了。
URI 经 h() 转义,字符集排除引号,无法逃出 href 属性;残余 OSC
(窗口标题、未闭合的 8)直接丢弃。markdown/JSON 路径输出 [text](URI)。

注意顺序:OSC 链接必须在主模式串之前用 preg_replace_callback 摘出来,
否则插进文本的 <a href="https://…"> 会被 URL 链接器二次匹配,
把 anchor 套进 href 里(和上一版 34m 那个 bug 同一类)。用哨兵字符
\x0e/\x0f 暂存 URI,模式串跑完再还原。

Co-Authored-By: Claude code 2.1.285 with glm-5.3-flash <noreply@taotoken.net>
17:29 fix: man 页的 ANSI 颜色码不再泄漏成 [34m 和坏链接 » Recent Commits to phpman:master
fix: man 页的 ANSI 颜色码不再泄漏成 [34m 和坏链接

groff 对使用颜色转义的 man 页(util-linux、systemd)输出 SGR 颜色码,
但只处理了粗体 ESC[1m 和下划线 ESC[4m,ESC[34m…ESC[0m 整对留了下来:
正文里显示成 [34m / [0m;更糟的是 HTML 的 URL 自动链接正则匹配
[\w]+://,把 ESC[34mhttps://… 里的 34m 吞进 href,生成
<a href="34mhttps://…"> 这种相对链接,爬虫跟进后变成
/man/<cmd>/34mhttps://… —— 一天访问日志里 122 个 403 全是 util-linux 命令。

在 formatManPerlDoc() 和 cleanTerminalOutput() 里直接剥掉颜色序列,
粗体/下划线仍按原样转换。

Co-Authored-By: Claude code 2.1.285 with glm-5.3-flash <noreply@taotoken.net>

Sun 27 September, 2026

22:54 feat(#225): sitemap 拆分 html/markdown-json + 生成 llms.txt » Recent Commits to phpman:master
feat(#225): sitemap 拆分 html/markdown-json + 生成 llms.txt

- Makefile: reindex/release-reindex/reindex-staging 拆成两次 build-sitemap
  - html → sitemap-phpman.xml.gz(给 Google,进 sitemap index)
  - markdown,json → sitemap-phpman-ai.xml.gz(给 AI 爬虫)
  Google 只爬 html,省 2/3 爬取预算(65K → ~22K URL)
- build-sitemap.php: 新增 --llms-output 生成 llms.txt,
  引导 AI 爬虫访问 MCP / search / markdown-json sitemap

Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
15:54 fix: 从非默认 home 运行的 CLI 不再把数据写到 ~/.phpman » Recent Commits to phpman:master
fix: 从非默认 home 运行的 CLI 不再把数据写到 ~/.phpman

cli/_bootstrap.php 在配置没有定义 PHPMAN_HOME 时无条件回落到 $HOME/.phpman。
线上两个 home 的 phpman.config.php 都没写这个常量(部署时的 sed 匹配的是
一行不存在的 define,静默失配),于是:

  cd ~/.phpman_test && php cli/build-index.php

—— 也就是 make reindex-staging / make staging-reindex 干的事 ——
解析出的 PHPMAN_HOME 是 /home/chedong/.phpman,重建的是**生产**的索引,
staging 的索引原地不动,而且整个过程没有任何提示。

实测(staging 主机,改动前后):
  改前  staging CLI home=/home/chedong/.phpman      cache=/home/chedong/.phpman/db
  改后  staging CLI home=/home/chedong/.phpman_test  cache=/home/chedong/.phpman_test/db
  生产两版都是 /home/chedong/.phpman,不变

规则改成:PHPMAN_HOME 就是所加载的 phpman.config.php 所在目录 —— 每个部署
都是 `cd <home> && php cli/...`,代码和配置并排放在 <home> 里;只有找不到
任何配置时才回落到 $HOME/.phpman。路径先过 realpath(),否则 __DIR__/.. 会
以 ".../cli/.." 的形式漏进常量。

Web 路径不受影响:phpMan.php 的 PHPMAN_HOME 是部署时 sed 替换的
(src/config.php:81),实测 staging 站点 home/json 均 200,
phpMan.php 里仍是 /home/chedong/.phpman_test。staging 上跑
cli/build-sitemap.php 输出的也是 test.chedong.com 的 URL,读的是 staging 的数据。

Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
15:33 fix: 部署时的配置检查不再误报 PHPMAN_BASE_URL » Recent Commits to phpman:master
fix: 部署时的配置检查不再误报 PHPMAN_BASE_URL

检查用 `define('KEY'` 只匹配单引号,而线上两个 phpman.config.php 的
PHPMAN_BASE_URL 都是双引号写的,于是每次部署都多打一行
"New config options not in your phpman.config.php: PHPMAN_BASE_URL"。
把两处(staging / release)的模式改成 `define(.KEY.`,`.` 匹配任意引号字符,
告警才名副其实 —— 常驻的假告警会让人忽略这个检查本身。

实测(staging 主机,同一份 config):
- 旧模式:PHPMAN_BASE_URL 等 7 项被报为缺失
- 新模式:PHPMAN_BASE_URL 不再出现,其余 6 项确实未配置,保留

另:test_agent_scenarios.php 的 A09(ETag 304)在 PHPMAN_DEBUG 目标上跳过。
staging 开着 debug,响应体里带 _profiling 的逐次计时,内容哈希每次都变,
ETag 因此每次不同,304 在设计上不可达。此前这条在 staging 上恒红,
在默认目标(生产)上正常。生产实测:两次请求 ETag 一致,回放 If-None-Match 得 304。

staging 上两个 e2e 套件:agent 22 passed / 0 failed,security 23 passed / 0 failed。

Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
15:04 fix: MCP 客户端错误返回 -32602,未知命令返回空结果而非内部错误 » Recent Commits to phpman:master
fix: MCP 客户端错误返回 -32602,未知命令返回空结果而非内部错误

三处 MCP 错误语义修正:

1. cli_help 查不到任何页面时,fallback 级联最后返回 "",
   handleMcpToolsCall() 解不出 JSON,报成 -32603 "Internal error:
   invalid MCP output"。调用方无法区分是命令拼错还是服务端坏了。
   现在返回空信封(summary: null、sections: [])—— 调用本身成功,
   只是这个名字在此没有 man/perldoc/info/pydoc/ri 页。

2. 未知工具名、cli_help/cli_search 缺必填参数,原先落进通用的
   catch (Throwable),同样报 -32603 "Internal error" 且细节被吞掉。
   这些是调用方的错,改抛 McpInvalidParams,返回 -32602
   "Invalid params: Unknown tool: X" / "Missing required parameter: command",
   直接点名要改什么。

3. TEST_MCP.md 的断言读错了字段:把 result.content[0].text 当 JSON 解析,
   而它是 markdown 渲染,结构化载荷在同级的 result.structuredContent。
   12 个用例的断言全部改读 structuredContent,并补上错误码说明;
   test/e2e/test_agent_scenarios.php 增加 structuredContent 断言和
   A11(不存在的命令 → 空结果)。

本地 PHP built-in server 实证:
- 不存在的命令        200,structuredContent.sections == [],summary == null
- 未知工具名          -32602 "Invalid params: Unknown tool: nonexistent"
- cli_help 缺 command -32602 "Invalid params: Missing required parameter: command"
- cli_search 缺 query -32602 "Invalid params: Missing required parameter: query"

test/run_all.php: 182 passed, 0 failed(7 个文件 0 passed 是本机 MacPorts PHP
缺 sqlite3/mbstring/curl 的既有现象,与本次改动无关)。

Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
08:38 fix: MCP API key 校验改为 fail-closed » Recent Commits to phpman:master
fix: MCP API key 校验改为 fail-closed

handleMcp() 原先在 MCP_API_KEY 为空时整个跳过鉴权(`if (MCP_API_KEY !== '')`),
配置缺失或被清空会让 POST /mcp 静默对全网开放,而不是拒绝。现在空 key 一律 401,
与 phpMan.php 的 status 端点已有写法一致。

两处比较同时由 `!==` 换成 hash_equals(),不再通过响应时间泄露 key 的长度和匹配前缀。

Breaking: 之前有意以无鉴权方式跑 MCP 的部署,需在 ~/.phpman/phpman.config.php
里设置 MCP_API_KEY。

随附文档同步:README 的 MCP 章节补上鉴权说明(客户端配置加 headers)、
config.example 注明 key 是必需项、TEST_MCP.md 的 12 个 curl 例子补上
X-Api-Key,两个 e2e 测试支持 PHPMAN_TEST_MCP_KEY(无 key 的目标返回 401 时
P09 视为已跳过)。

本地 PHP built-in server 实证(改前 → 改后):
- key 未设置 + 无 header   200 → 401
- key 已设置 + 无 header        401
- key 已设置 + 错误 key         401
- key 已设置 + 正确 key         200,tools/list 返回 2 个工具
- 401 响应体是合法 JSON-RPC error (-32001)

Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
05:28 fix: 移除 mcp 作为公开输出格式 » Recent Commits to phpman:master
02:47 docs: CLAUDE.md 增加 commit 署名格式约定 » Recent Commits to phpman:master

Sat 26 September, 2026

23:26 docs: 回填 CHANGELOG 4.7→4.11,05-PLAN 按实现同步 » Recent Commits to phpman:master
22:39 Merge branch 'worktree-fix-makefile-tag' » Recent Commits to phpman:master
22:33 docs: 补齐 PHPMAN_ADSENSE_ID,清理 05-PLAN 的过期配置 » Recent Commits to phpman:master
22:30 fix: make tag 递归后不再产生杂散 v 标签 » Recent Commits to phpman:master
21:47 fix: 配置检查的 sed 转义 bug + 补 PHPMAN_ADSENSE_ID 到 config.example » Recent Commits to phpman:master
20:42 chore: gitignore .codex/ agent local state » Recent Commits to phpman:master
19:09 fix: title description 支持 perldoc(name 在 search_fts 的 3perl/3pm 段) » Recent Commits to phpman:master
09:40 fix: CSP 放行 GA4 的 doubleclick / ga-audiences 信标 » Recent Commits to phpman:master