车东 Che, Dong

Thu 24 September, 2026

01:30 给 Agent 开 GCP service account 访问 Search Console 和 GA4 数据 » 笔记 by 车东

TLDR

想让 Claude Code 之类的 Agent 直接读 Google Search Console 和 GA4 的统计做 SEO 分析,不能靠 OAuth 浏览器授权(Agent 没有交互式登录),正确做法是 GCP service account:建一个服务账号 → 启用两个 API → 把这个服务账号邮箱加到 Search Console / GA4 里授权 → Agent 拿 JSON key 调 REST API。本文记录完整流程和踩的坑。

架构

Agent (Claude Code)
  → CLI 脚本 (gsc.py / ga.py)
  → service account JSON key 认证
  → Search Console API / Analytics Data API
  → 你的站点数据

关键点:service account 不是「你的 Google 账号」,而是 GCP 项目里的一个机器身份。权限不是靠账号密码,而是靠「把这个机器身份的邮箱加进目标资源」。

00:32 feat: unified cache TTL — PageCache found + TLDR cache expire on same… » Recent Commits to phpman:master
feat: unified cache TTL — PageCache found + TLDR cache expire on same schedule (default 7 months)

PageCache found entries and the TLDR cache both used 604800s (7 days) as
separate hardcoded literals — they had already drifted conceptually and
would diverge silently if either changed. Extract PHPMAN_CACHE_TTL_FOUND
(= PHPMAN_CACHE_TTL_MONTHS × 30 days, default 7 months = 18144000s) in
src/config.php and switch both call sites to it:

- src/cache.php: found + search-not_found TTLs use the constant
  (not_found stays 1 day, emoji formats stay never-expire)
- src/tldr.php: read-side expiry check uses the constant (was inline 604800)
- phpman.config.php.example: document PHPMAN_CACHE_TTL_MONTHS override
- test_page_cache.php: TTL assertions reference the constants
- docs (03-CACHE / 01-PRODUCT / AGENTS / README): describe current behavior;
  historical changelog lines kept as-is

Local suite: 389 passed, 0 failed.
00:07 perf: markdown formatter fast path — hoist baseUrl/closures out of lo… » Recent Commits to phpman:master
perf: markdown formatter fast path — hoist baseUrl/closures out of loop, guard regex passes with strpos

Staging staged-timing showed md formatter CPU cost was comparable to HTML
(160-370ms vs 200-230ms on 30k-line pages), but per-line regex passes with
per-match baseUrl()+urlencode() added avoidable overhead.

- baseUrl() is request-constant: compute once before the loop
- link closures hoisted out of the loop (was created per line)
- 4 regex passes guarded by strpos triggers (@/http/(/::): plain prose
  lines (the majority) now do a single C-level scan instead of 4 preg passes

Verified on staging: zshall/gcc-12/ls markdown output byte-identical
(1.66MB/1.39MB/8.9KB), local test suite 389/389 passed.

Wed 23 September, 2026

00:22 fix: filter stale info dir-menu entries by real .info files » Recent Commits to phpman:master
fix: filter stale info dir-menu entries by real .info files

getInfoIndex() ran `info` and blindly linked every (name)/(group)node entry
in the dir menu. On a shared host the dir file lists packages that were
uninstalled without updating the menu, so ~180 of 252 entries had no backing
.info file — producing dead links (empty page → noindex, 92.3% of info mode).

Add getValidInfoFiles() (glob /usr/share/info/*.info*, strip .info/.gz/-N
suffixes) and intersect the menu against it across all three formats
(markdown, json/mcp, html). Entries without a backing file stay as plain text.

Co-Authored-By: Claude Code <noreply@anthropic.com>

Tue 22 September, 2026

20:38 v4.11: shard page cache per mode to eliminate SQLite write-lock conte… » Recent Commits to phpman:master
v4.11: shard page cache per mode to eliminate SQLite write-lock contention

Production was throwing 'database is locked' / 'cache set retries exhausted'
in phpman_error.log under concurrent bot/man traffic — all modes (man,
perldoc, info, pydoc, ri, search) wrote to ONE SQLite file (phpman_cache.db)
alongside the search infrastructure.

Refactor: each mode now gets its own page-cache file
phpman_cache_{mode}.db holding just the cache + cache_fts tables. The central
DB keeps only the search/tldr infra (meta, search_fts, search_index_meta,
tldr_cache), which is written mostly during batch reindex (single writer).

Changes:
- src/cache.php: add pageCacheModes() + pageCacheDb(); route PageCache
  get/set/delete/deleteEntry/syncFts through the mode shard; clear()/stats()
  and cacheOrExecute TTL cleanup iterate all shards.
- phpMan.php: ETag emoji check + emoji staleness query now target the mode shard.
- src/search_index.php: clear the 'search' shard on reindex instead of central.
- cli/build-sitemap.php: iterate per-mode shards (with legacy central fallback).
- test/unit/test_page_cache.php: verify against pageCacheDb() shards.

All 389 unit + integration tests pass.

Sun 20 September, 2026

23:41 fix: MCP 信封也要暴露限额(formatMcpStructured 是白名单式挑字段) » Recent Commits to phpman:master
fix: MCP 信封也要暴露限额(formatMcpStructured 是白名单式挑字段)

formatMcpStructured() 只挑固定字段返回,所以加在 IR 顶层的
content_truncated / content_budget_bytes / content_retained_bytes 在 MCP 响应里
被丢掉了——staging 实测 ?format=json 有标记、/mcp 没有。section 级标记因为
sections 整体透传所以没受影响。

- src/format_mcp.php: formatMcpStructured() 在截断时透出三个字段;
  formatMcpMarkdown() 的收尾说明在截断时改为告知「本节文本已封顶,
  带 truncated 标记的条目附 content_bytes/content_lines,章节列表完整」,
  不再承诺不再携带的 full documentation
- test/integration/test_json_content_cap.php: 补 5 条 MCP 信封断言

验证:7 个页面的 MCP 信封与改动前逐字节一致(含 perlfunc 324KB subsection);
测试 384 → 389 通过。
23:33 feat: json/mcp 载荷限额,派生字段改为折行时单遍计算 » Recent Commits to phpman:master
feat: json/mcp 载荷限额,派生字段改为折行时单遍计算

info py 的 section 正文共 14.4MB(单 "Index" 一节就有 1.37MB),原本要整页
驻留内存才能序列化。现在折行阶段就按预算丢弃超出部分:全局 1MB
(PHPMAN_JSON_MAX_CONTENT_BYTES) + 单节 512KB (PHPMAN_JSON_MAX_SECTION_BYTES),
两个常量都可在 phpman.config.php 覆盖。

因为超出预算的正文不再驻留,summary/synopsis/flags/examples/see_also 改为在
折行时从完整行流里累积,而不是事后从 sections 再扫一遍——否则被丢弃的
OPTIONS/EXAMPLES 正文会让 flags 丢失。被截断的条目带 truncated /
content_bytes / content_lines,顶层带 content_truncated / content_budget_bytes
/ content_retained_bytes;section_outline 仍然完整,导航不受影响。

单节上限取 512KB 是实测结论:73 个常见 man 页里 perlfunc 有 324KB 的
subsection(bash 36KB、sudoers 59KB 的 section),32KB 会误伤这类总量远低于
预算的正常页面。放得下的页面必须逐字节不变。

- src/format_json.php: buildJsonData() 折行时按预算保留正文并累积派生字段
- src/config.php: 新增两个限额常量
- phpman.config.php.example: 记录这两个开关
- test/integration/test_json_content_cap.php: 17 条断言覆盖标记、预算上限、
  数组/非 JSON 边界,以及最关键的「截断后派生字段仍完整」

验证:62 个 man 页 + perldoc 与改动前逐字节一致(脚本比对 HEAD 与工作区);
小预算下 flags/summary/examples/see_also 与不限额时完全相同;真实 info py 在
生产环境 PHP 8.5 下 JSON 16.1MB → 2.88MB(-82%)、峰值 67.4 → 46.5MB。
23:10 fix: 索引的 url 字段指向可用的地址(/man/json 实际是页面查询) » Recent Commits to phpman:master
fix: 索引的 url 字段指向可用的地址(/man/json 实际是页面查询)

getManIndex() / getInfoIndex() 输出的 url 自引用写的是 /man/json、/info/json,
但索引没有 command 段,PATH_INFO 表达不了格式——这两个地址会被路由成「名为
json 的页面」并返回 HTML。索引取格式只能走查询参数,phpMan.php 里页脚的格式
链接用的就是 ?mode=X&format=json。

- src/source_man.php / src/source_info.php: url 改为 ?mode=man|info&format=json

验证:四个地址均返回合法 JSON(man 索引 count=10、info 索引 count=195,
mcp 形式为合法信封)。
23:01 fix: 大页面的 MCP/JSON 不再经由 IR 字符串(info py 内存超限) » Recent Commits to phpman:master
fix: 大页面的 MCP/JSON 不再经由 IR 字符串(info py 内存超限)

info py 是 430k 行 / 19.5MB 的页面。MCP 路径原本把 IR 序列化成 ~15MB 字符
串、再由 formatForOutput() 解码回来,转换过程中同时持有三份整页文本,在
128MB memory_limit 下直接 fatal(生产日志累计 8 次,最近 02-Sep)。

- src/format_json.php: formatToJSON() 拆出 buildJsonData(): array;行缓冲折
  进 $sections 后立即释放,编码前 unset($sections)。释放必须通过引用写空
  ($lines = array())——unset() 只去掉本地别名,调用方的数组仍然存活
- src/format_mcp.php: 抽出 formatMcpEnvelope(array),新增 formatPageOutput()
  —— mcp 直传数组,json 走原路径;数组已在手的 search/索引页同样跳过
  encode+decode
- source_{man,info,perldoc,pydoc,ri}.php: 9 处调用点改走 formatPageOutput
- test/integration/test_formatter_mcp.php: 锁定「数组路径 == 字符串路径」

验证:新旧路径输出字节级一致(mcp/json × 页面/索引,仅 generated 时间戳因
跨秒不同);info py chunk 预留峰值 127.4 → 104.0MB,peak_real 不变(67.4MB);
staging 真实请求 3×200,日志零新增。
23:01 fix: ?debug=1 不再为追加 _profiling 键重序列化整个响应 » Recent Commits to phpman:master
fix: ?debug=1 不再为追加 _profiling 键重序列化整个响应

phpMan.php 原本用 json_decode() + json_encode() 给 json/mcp 响应加一个
_profiling 键。对 info py 这种 19.5MB 的响应,decoded 数组本身就要 44MB,
单独测峰值 62MB real / 127.6MB chunk,自己就贴着 128MB 上限——staging
(PHPMAN_DEBUG=true)对该页面因此稳定 500,而生产因为 debug 关闭不受影响。

- src/cache.php: 新增 appendProfilingJson(),把 _profiling 注入到已序列化的
  JSON 对象末尾(结果仍是合法 JSON);JSON 数组与非 JSON 响应原样返回,
  避免旧实现把数组响应悄悄变成对象
- phpMan.php: 改调 appendProfilingJson()
- test/unit/test_profiling_append.php: 15 条断言覆盖普通/紧凑/空对象、
  尾随空白、JSON 数组、非 JSON、空响应、2000-section 大响应

验证:staging(debug 开启)连续 3 次未缓存请求全部 200,JSON 合法、
_profiling 为最后一个键、10757 个 section 完整,日志零新增。
00:23 fix: FTS5 查询把名字里的 and/not 误判为操作符(no such column) » Recent Commits to phpman:master

Mon 14 September, 2026

18:20 OpenClaw 2026.9.3→2026.9.4 升级反复失败:package swap 完整性扫描 30s 超时(绕过 + 根治) » 笔记 by 车东
00:30 ssh-add "Could not open a connection to your authentication agent":macOS 私钥口令 + ssh-agent/钥匙串配置手册 » 笔记 by 车东

Sun 13 September, 2026

09:00 Homebrew 停止 Intel 支持:迁移到 MacPorts 的完整笔记 » 笔记 by 车东

Tue 08 September, 2026

15:30 OpenClaw 2026.9.3 升级后 Gateway 无响应:SQLite canonical index no such column workspace_dir 修复 » 笔记 by 车东

Tue 01 September, 2026

23:14 feat: cache-bust CSS/JS with version query + improve MCP search descr… » Recent Commits to phpman:master
10:15 OpenClaw 升级后 Telegram 无响应:修复 invalid persisted session row 与 Legacy exec approvals 迁移 » 笔记 by 车东
00:21 docs: CLAUDE.md 分支约定 — 明确 master 为主干,勿 push 其他分支名 » Recent Commits to phpman:master
00:16 docs: CLAUDE.md 分支约定 — default 计划统一为 main(过渡期仍 push master) » Recent Commits to phpman:master

Mon 31 August, 2026

00:22 fix: showForm 传入 markdown/json URL 修复 not-found 兜底链接失效 » Recent Commits to phpman:master