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>
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>
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> 变 <script>),不需要额外转义,否则会二次转义。 Co-Authored-By: Claude code 2.1.285 with glm-5.3-flash <noreply@taotoken.net>
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>
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>
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>
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>
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>
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>
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>