基于
underestimatedme/ai-assist(v0.1.0)。这是一份劝退指南。把内部平台包成 Claude Code 技能这件事,从"我周末就能搞定"到"算了不做了",中间有八个固定的坑,每个坑都有一批人留在里面。下面按你会遇到的顺序讲:这一关长什么样、多少人死在这、以及这个仓库为它准备了什么。
前情提要:仓库里所有 host 是虚构的
*.internal.example.com,所有 API 形状是编的。它不是能跑的工具,是一副骨架加一套判断。
5 分钟"] --> B["② 拿到凭证
💀 死人最多"] B --> C["③ 第一个真实请求
比想象容易"] C --> D["④ 写成技能"] D --> E["⑤ 活过第二天
💀 用户在这放弃你"] E --> F["⑥ 学会说不知道
💀 信任在这死掉"] F --> G["⑦ 给第二个人用
💀 真正的劝退点"] G --> H["⑧ 升级不把人登出"]
劝退点 0:它其实不给你任何权限
先把期待压下去。这套东西不授予任何访问权。它复用你浏览器里已经存在的会话,以你的身份、在你已有的权限内、在你自己的机器上跑。
所以如果你的目标是"让 AI 帮我查我本来查不到的东西"——现在放弃,省下一周。
它真正解决的是另一个问题:内部平台都有 Web UI、没有 API token、登录走 SSO,跨四个平台回答一个问题要开八个标签页。把每个平台包一次,那一下午变成一句话。这才是回报所在。
第一关:跑起来(这关不会死人)
git clone git@github.com:underestimatedme/ai-assist.git
cd ai-assist && make dev # = claude --plugin-dir $(pwd)没有 build,没有 install。改文件、重启 Claude、再试——这就是全部开发循环。另外两个目标:make validate 校验结构,make clean 删掉所有运行时数据。
对着仓库自带的虚构 host,/datastore-query setup 会在 DNS 阶段失败。这是预期行为,不是坏了。在这里以为自己装错了而放弃的人,是第一批。
第二关:拿到凭证 —— 死人最多的一关
这关之所以劝退,是因为大部分人把它想复杂了:以为要在 agent 和浏览器之间搭一座桥。
没有桥。 Claude Code 是跑在你机器上的一个进程,它的 shell 以你的用户执行命令,和你自己敲没区别。交接物是磁盘上的一个文件。
SSO 通常直接放行,零输入 U-->>B: 需要时登录 S->>S: 每秒轮询,直到必需的 cookie 名都出现 S->>F: 写文件,关浏览器,exit 0 Note over C,F: 之后:curl -b "$(cat
Claude 全程没看见凭证的值
想通这一点,剩下的都是普通工作。想不通,就会在这里放弃。
这一关的四个具体死法
| 症状 | 真实原因 | 出路 |
|---|---|---|
| 窗口根本开不出来 | 在容器 / 纯 SSH / 云 runner 里,没有显示器 | 这是硬依赖。退回手工粘贴 cookie |
Chrome profile unavailable | Chrome 正在运行,profile 目录被它锁着 | 关掉 Chrome,或接受全新浏览器 + 手动登录 |
ERR_CERT_AUTHORITY_INVALID | 内网 ingress 自签证书,登录页压根没渲染出来 | 加 --insecure,只对这类 host |
| 轮询到超时,cookie 一直不出现 | cookie 名写错了,或者你那个 SSO 租户的名字不一样 | DevTools > Application > Cookies 自己看 |
你的平台是哪种鉴权
scripts/capture-cookie.py 覆盖三种,按 DevTools 里看到的选:
| 你看到 | --mode | 备注 |
|---|---|---|
Cookie: sessionid=... | cookie(默认) | 传逗号分隔的 cookie 名 |
Authorization: Basic ... | basic-auth | 从成功响应上截 header |
Authorization: Bearer ... | token | 配 --match-url /api/login |
| 自定义头 / 签名 / mTLS | 自己加第四种 | 约 15 行:一个 attach + 一个 check |
几个不写出来你会踩一遍的细节:
- 复用 Chrome profile,所以登录通常是零交互的——这是这套方案能被日常使用的原因。
- 清掉目标 cookie,但保留上游 IdP 的 cookie。这个组合让活着的 SSO 会话静默换出一个新的平台会话。全清会把你踢回登录页。
basic-auth跳过 401/403 响应:那是挑战不是成功,从挑战上截的 header 可能是错的。- 诊断只打 cookie 的名字,永不打值。这些输出会进终端、日志、可能进 bug 报告。
- 永远不要让用户把密码敲进对话——那会落进 transcript。让他在自己终端里跑命令。
退出码是契约:0 成功,1 Playwright 起不来,3 超时或窗口被关。flows/setup.md 按这三个码分支到"验证"或"手工兜底"。
第三关:第一个真实请求(比你以为的容易)
这关的正确姿势是先离开技能,回到你自己的 shell:
- 在浏览器里做那件事,开着 Network 面板。
- 找到带答案的那个请求,右键 → Copy as cURL。
- 粘进终端,确认能跑。
- 把除
Cookie和Content-Type之外的 header 全删掉,看还能不能跑。通常能。 - 剩下的东西改写成
scripts/query.sh,URL 从config.yml读,cookie 从.local/读。
你不是在逆向一个 API,你是在重放一个浏览器已经展示给你的请求。这个区别决定了这一关是一天还是一周。
完成判据很硬:bash skills/datastore-query/scripts/query.sh -e prod -d <db> "SELECT 1" 打印出真东西。
第四关:写成技能(这里开始有设计)
两条分工规则,违反了不会立刻报错,但会在第三个 flow 的时候把你埋掉:
SKILL.md 拥有一切共享的东西——参数解析、环境解析、鉴权预检、分发。全部只做一次,在路由之前做完。
flow 文件只拥有自己那几步。它假定输入已归一化、预检已通过,绝不复述共享逻辑。
还有一条隐蔽的:description 决定你的技能会不会被调用。写"什么时候用它",不是"它是什么"。
✅Query internal databases. Use when the user asks to look up rows, check whether data exists, or inspect table structure.
❌A datastore querying tool.
以及:删掉你没写的 flow。一个指向不存在文件的引用,比缺一个功能更糟。
第五关:让它活过第二天 —— 用户在这里放弃你
凭证会过期,通常每天一次。没有自动恢复,每天早上第一次查询都会失败,然后大家就不再打开这个工具了。你不会收到任何反馈,工具只是安静地死了。
契约只有一句:恢复一次,或者明说失败了什么。
这条在每一个调用点都成立,不只在 dispatch 的预检。dispatch 把控制权交给 flow 之后,规则不失效。
一个容易做错的细节:探针返回 302 时要读 Location 的目标,不是只看状态码。把所有重定向当失败,会把用户送进无意义的重登循环。而"连不上"根本不是鉴权问题——用户没连 VPN,重新登录一百次也没用。
第六关:学会说"不知道" —— 信任在这里死掉
毁掉这类工具最快的方式,是给出一个自信的错误数字。一次就够。
所以 service-profile 把 UNKNOWN 写成了一个合法判定:
UNKNOWN是一个真实的判定,当它为真时必须使用。因为"没有明显失败"就把它降级成HEALTHY,正是一个监控工具变得比没有更糟的方式。
判断下线的那个 flow 说得更直接:
一次失败的查询和一个真实的零,在报告里长得一模一样,含义却相反。
同一类的判断还有:
- 零不自动等于业务事实。报"一条都没有"之前先排除:环境错了、时区导致时间窗错位、软删除标志过滤光了、只读副本没追上。并且说明你排除了哪几个。
- 空的 Prometheus series 不等于 0。指标名拼错和"真的没有"返回一模一样。先
count(<metric>)确认指标存不存在。 - 不要对分位数求平均。
avg(p99)不是任何东西的 p99。 - 每个数字都带上时间窗和环境。把 staging 的数当 prod 报,比不报更糟——因为它会被相信。
只读闸门要放在脚本里
case "$FIRST_WORD" in
SELECT|SHOW|EXPLAIN|DESC|DESCRIBE) ;;
*) echo "Error: '$FIRST_WORD' is not permitted..." >&2; exit 2 ;;
esac脚本里那句注释说完了理由:一条只是"请求模型遵守"的规则,会在上下文变长的那天失效。
同理,第一个技能务必是只读、高频、低风险的。早期一次糟糕的写操作会杀死整个项目,而信任没有第二次机会。
第七关:给第二个人用 —— 真正的劝退点
不是给团队。给一个同事,坐你旁边,用他自己的机器。
他前十分钟踩的每个坑,都是你的 backlog,而且不会是你预期的那些。常见的:没装 Python;Chrome 开着所以 profile 被锁;VPN 配置不同;他那个 SSO 租户的 cookie 名不一样。
很多项目死在这里的原因是:作者觉得"我这边明明是好的"。你这边当然是好的,那是你调了一周的那台机器。
第八关:升级把所有人登出
插件管理器装 v0.2.0 时会铺一个新目录,v0.1.0 那个(装着大家的 cookie 和 schema)留在旁边不动。没有迁移的话,每次升级静默登出所有人——用户的反应是不再升级。
skills/x/.local/
cookies · schemas"] -.->|"ls -dt 找最新的兄弟安装"| C{".migrated
在吗"} C -->|在| D[立即返回,零成本] C -->|不在| E["按契约 cp -r"] E --> B["…/ai-assist-v0.2.0/
skills/x/.local/"]
bash "${CLAUDE_PLUGIN_ROOT}/scripts/migrate-local.sh" datastore-query cookies schemas尾部参数是这个技能的迁移契约。它放在调用点(SKILL.md 里)而不是脚本内部,这样每个技能拥有自己的清单,且清单出现在自己的 diff 里。语义三条:只在源有、本地无时才拷;原样拷贝无格式翻译;sentinel 门控所以放在每次 dispatch 最前面是免费的。
顺带一条运营现实:用户只有在版本号变化时才会收到更新。一个忘了 bump plugin.json 的修复,等于没发布。
如果你没放弃:值得抄走的结构
三个技能,其实是三种原型
编排型 · 自己不碰平台"] DQ["datastore-query
能力型 · Cookie 鉴权"] MQ["metrics-query
能力型 · Basic 鉴权"] SP -->|"公开接口 /datastore-query ..."| DQ SP -->|"公开接口 /metrics-query ..."| MQ DQ --> P1[(数据查询平台)] MQ --> P2[(指标平台)]
两个能力型技能除了中间那段(请求怎么发、响应怎么解)完全同构。写完第一个,第二个就是把中间换掉。
编排型才是价值密度最高的:能力型回答"这个数是多少",编排型回答"这个服务还好吗"——后者才是人真正会问的。它有一条硬规则:只通过公开接口调别的技能,绝不读别人的 .local/、绝不直接调它的脚本、绝不复述它的规则。以及它必须容忍局部失败——每个源都跑,失败的在输出里点名,绝不用一个看起来合理的值填空。
五层,只有一层和 Claude Code 绑定
| 层 | 位置 | 拥有 |
|---|---|---|
| 分发 | plugin.json、SKILL.md frontmatter | harness 如何发现和路由 |
| 配置 | config.yml | 每个 URL、凭证文件名、环境差异 |
| 凭证 | capture-cookie.py、.local/ | 获取和存放会话凭证 |
| 能力 | <skill>/scripts/* | 确定性执行:HTTP、解析、格式化 |
| 知识 | SKILL.md、flows/、shared/ | 做什么、什么顺序、结果怎么读 |
只有分发层是 Claude Code 专属的,换 harness 时另外四层原样搬走。撑住这个性质的是两条规则:
- 脚本从
$0推自己的路径,不读${CLAUDE_PLUGIN_ROOT}这类 harness 变量(markdown 里可以用,脚本里不行)。这样它在技能里、cron 里、测试里、你自己 shell 里跑得一样。 - 环境标识进去,其余内部解析。
query.sh -e prod -d orders "SELECT ..."✅;把 cookie 路径当参数传进来 ❌。目的是把基础设施管道从模型手里拿走,不是把意图拿走——服务名、查询、时间范围仍然是调用方的责任。
真正的资产是 markdown,不是脚本
脚本是简单的部分,几百行 curl 和 JSON。值得留下来的东西是那些判断:一个零可能是滞后的副本;一个空 series 通常是标签拼错;UNKNOWN 是真实结论。这些不在任何厂商文档里,是你团队搞错一次学到的。
docs/ADAPTING.md 给了可以直接套的模板——症状 / 实测边界(你真测过什么,不是你假设什么)/ 绕过方式 / 误导信号——以及一份该写的清单:哪些权限失败是静默的、哪些端点有门禁且界线实际在哪、读请求是否打副本落后多久、每个平台用哪个时区报时(混用两个会产出自信而完全错误的分析)、状态码和枚举值用人话是什么意思。
判断标准:每当你发现自己在给同事解释一个坑,那段解释就该进 flow 文件。
适配清单:只有五处要改
按依赖顺序:config.yml(URL、cookie 名、环境)→ 凭证捕获模式 → 能力脚本 → flow 文件(你真正的工作在这) → 重命名插件。
能力脚本要换的正好三处,别多换:请求(URL、方法、payload)、响应解析、错误映射("凭证过期" / "你没这个授权" / "平台挂了"是三个不同问题,混成一个会把用户送去重新登录,而真正的问题是缺一个授权)。
明确不要改的四样:自定位脚本、环境标识契约、脚本里的只读闸门、失败处理策略。
什么时候真的该放弃
诚实地说,这几种情况下别做:
- 你的 Claude Code 不在有桌面的本机上跑。凭证捕获这一环就不成立,剩下的都是硬撑。
- 你想包的第一个平台是会写数据的。换一个只读高频的先做。
- 你不打算写 flow 文件,只想要脚本。那你要的是几个 shell 别名,不是这套东西——而且别名更省事。
- 没有第二个用户。只有你自己用的话,投入产出比不如直接把那几条
curl存成 alias。
选题公式:频率 × 上下文切换成本 × 结果的确定性。第一个一定要只读、高频、低风险。
反过来,如果你撑过了第七关——有第二个人在自己机器上用起来了——那基本就稳了。之后第二个技能是复制粘贴换中间,第三个开始共享部分不再变动。
附录
坑清单速查
| 现象 | 真实原因 | 处理 |
|---|---|---|
| 浏览器窗口开不出来 | 容器 / SSH / 云 runner,没有显示器 | 硬依赖,退回手工粘贴 |
Chrome profile unavailable | Chrome 在跑,profile 被锁 | 关掉 Chrome |
ERR_CERT_AUTHORITY_INVALID | 内网自签证书 | --insecure,仅限此类 host |
| 探针 302,用户反复被要求重登 | 把所有重定向当失败了 | 读 Location 目标 |
| 报"会话过期",重登没用 | 根本没连 VPN | 区分开,指向 references.vpn-setup |
| 指标查询返回 0 | 空 series ≠ 0,多半是名字拼错 | 先 count(<metric>) |
| 查出 0 行,报"没有数据" | 环境 / 时区 / 软删除 / 副本滞后 | 逐个排除并说明 |
| 升级后所有人被登出 | 没调 migrate-local.sh | 加到 dispatch 入口 |
mapfile: command not found | macOS /bin/bash 还是 3.2 | 用 while IFS= read -r |
| 发了新版但没人收到 | 忘了 bump plugin.json | 版本号是唯一的更新触发器 |
命令
make dev # 加载插件启动 Claude Code
make validate # 校验插件结构
make clean # 清空所有 .local/ 运行时数据/datastore-query setup [env] | setup-check [env] | sync [env] <db>... | explain <SQL> | <自然语言问题>
/metrics-query setup [env] | health <service> | traffic <service> | promql <expr>
/service-profile <service> [health|usage] [env]五条最容易忘、代价最大的规则
- 脚本从
$0推路径,不读 harness 变量。 - 环境标识进去,URL 和凭证路径在脚本内部解析。
- 只读闸门在脚本里,不在 prompt 里。
- 恢复一次,或者明说失败了什么。绝不静默降级。
- 已经在别处存在的事实,按位置引用,不复述。