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>
fix: 移除 mcp 作为公开输出格式
/{mode}/{param}/mcp、/search/{query}/mcp 和 ?format=mcp 回落到 HTML。
`mcp` 从来不是 MCP:参数取自 URL 路径、允许 GET,且绕过了 JSON-RPC 端点的
API key 校验、64KB body 上限和 POST-only 守卫,同时被缓存 7 天。
format 字符串本身保留 —— handleMcp() 仍通过 getManPage(..., "mcp") 渲染 ——
只是不再能从 URL 到达。
顺带把四处重复的 format 判断收敛为 PHPMAN_OUTPUT_FORMATS 常量(照
PHPMAN_CONTENT_MODES 的既有写法),以后增删格式只改一处。
README 的 "MCP Format (REST GET)" 一节、特性列表条目、TEST_MCP.md 的
T13–T16 一并删除;v4.4 历史发布说明保留不改。
BREAKING CHANGE: 使用该后缀的调用方改用 POST /mcp。
Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
docs: CLAUDE.md 增加 commit 署名格式约定 Co-Authored-By: Claude code 2.1.274 with glm-5.3-flash <noreply@taotoken.net>
docs: 回填 CHANGELOG 4.7→4.11,05-PLAN 按实现同步 CHANGELOG 停在 [4.6.0],而项目已到 v4.11.1。按 git log/tag 回填 Unreleased、4.11.1、4.11.0、4.10.0、4.9.26、4.9.0、4.8、4.7 —— 每条都对着提交范围和 tag 日期写,没有靠猜。 05-PLAN: - 配置表补全 PHPMAN_GZIP_MIN_BYTES / PHPMAN_JSON_MAX_CONTENT_BYTES / PHPMAN_JSON_MAX_SECTION_BYTES / PHPMAN_CACHE_TTL_FOUND / PHPMAN_CACHE_TTL_NOT_FOUND - 路线图去掉过期的 "(current)"(还标在 v4.8),补 v4.9 → v4.11.1 说明 - 新增 v4.11 小节(cache 分片、载荷限额、v4.11.1 的 LLM 残留清理) - v4.9 的 CSP nonce 标为 proposed, not shipped —— v4.9.0 实际没实现, src/ 里搜不到 nonce - v4.1 去掉过期的 "(current)" Co-Authored-By: Claude Code <noreply@anthropic.com>
Merge branch 'worktree-fix-makefile-tag'
docs: 补齐 PHPMAN_ADSENSE_ID,清理 05-PLAN 的过期配置 01-PRODUCT.md - 新增 Google AdSense (v4.9.26+) 小节,与 GA 小节对称:说明只注入 adsbygoogle.js loader、依赖 Auto Ads、自动放宽 CSP、staging 不要开 - 默认值示例 + 「What goes where」表补上 PHPMAN_ADSENSE_ID 05-PLAN.md - 「Config overridable constants」表补上 PHPMAN_GA_ID / PHPMAN_ADSENSE_ID - 删掉「Add emoji enhancement」配方 —— LLM_* 已于 v4.10 移除,照着配没用 - 修正配置层示意图:phpman.config.php 在 ~/.phpman/ 而**不是** webroot (secrets 不进 webroot 是这套设计的重点),示例键也从已删除的 LLM_API_KEY 换成 MCP_API_KEY Co-Authored-By: Claude Code <noreply@anthropic.com>