车东 Che, Dong

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
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>
02:47 docs: CLAUDE.md 增加 commit 署名格式约定 » Recent Commits to phpman:master
docs: CLAUDE.md 增加 commit 署名格式约定

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

Sat 26 September, 2026

23:26 docs: 回填 CHANGELOG 4.7→4.11,05-PLAN 按实现同步 » Recent Commits to phpman:master
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>
22:39 Merge branch 'worktree-fix-makefile-tag' » Recent Commits to phpman:master
Merge branch 'worktree-fix-makefile-tag'
22:33 docs: 补齐 PHPMAN_ADSENSE_ID,清理 05-PLAN 的过期配置 » Recent Commits to phpman:master
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>
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
03:49 fix(#226): title 改为 Name - description - mode(section) - [phpMan] » Recent Commits to phpman:master

Fri 25 September, 2026

22:20 zsh 的 PATH 该写哪个文件:.zshenv / .zprofile / .zshrc 加载时机,及 ssh/pdsh 报 command not found 的根因 » 学习笔记|Tech Notes on 笔记 by 车东
15:24 refactor: remove LLM-era dead code and stale docs » Recent Commits to phpman:master

Thu 24 September, 2026

01:30 给 Agent 开 GCP service account 访问 Search Console 和 GA4 数据 » 学习笔记|Tech Notes on 笔记 by 车东
00:32 feat: unified cache TTL — PageCache found + TLDR cache expire on same… » Recent Commits to phpman:master