Portable Tools
便携工具 - 跨设备开发方法论
构建能在不同设备、命名方案和配置下工作的工具的方法论。基于OAuth刷新器调试会话的经验教训(2026-01-23)。
核心原则
切勿假设你的设备是唯一设备。

你的本地设置只是众多可能配置中的一种。为通用情况而构建,而非特定实例。
三大问题(在编写代码前)
1. "设备之间有何差异?"
在编写任何读取配置、数据或凭证的代码之前:
请思考:
- 文件路径?(macOS与Linux,不同的主目录)
- 账户名称?(user123与default与oauth)
- 服务名称?(拼写/大小写的细微差异)
- 数据结构?(不同版本,不同格式)
- 环境?(不同的shell,可用的不同工具)
来自OAuth刷新器的示例:
- ❌ 错误假设:账户始终是"claude"
- ✅ 实际情况:可能是"claude"、"Claude Code"、"default"等。
操作:列出变量,使其可配置或可自动发现
2. "我如何证明这是有效的?"
在声明成功之前:
要求:
- 具体的"之前"状态(确切值)
- 具体的"之后"状态(确切值)
- 证明它们不同(并排比较)
OAuth 刷新器的示例:
BEFORE:
- Access Token: POp5z1fi...eSN9VAAA
- Expires: 1769189639000
AFTER:
- Access Token: 01v0RrFG...eOE9QAA ✅ Different
- Expires: 1769190268000 ✅ Extended
操作:始终使用真实值展示数据转换过程
3. "当它出错时会发生什么?"
在推送到生产环境之前:
测试:
- 错误配置(故意破坏配置)
- 数据缺失(移除预期字段)
- 多个条目(模糊情况)
- 边界情况(空值、特殊字符)
OAuth 刷新器的示例:
- 使用以下内容进行测试
keychain_account: "wrong-name"回退机制应能正常工作 - 测试密钥链数据不完整的情况 → 应能优雅地失败并给出有用的错误提示
操作:不仅要测试正常路径,还要测试故障模式
强制模式
模式1:显式优于隐式
❌ 错误:
# Ambiguous - returns first match
security find-generic-password -s "Service" -w
✅ 正确:
# Explicit - returns specific entry
security find-generic-password -s "Service" -a "account" -w
规则:如果命令可能产生歧义,应使其明确。
模式2:使用前验证
❌ 错误:
DATA=$(read_config)
USE_VALUE="$DATA" # Hope it's valid
✅ 正确:
DATA=$(read_config)
if ! validate_structure "$DATA"; then
error "Invalid data structure"
fi
USE_VALUE="$DATA"
规则:切勿假设数据具有预期的结构。
模式3:回退链
❌ 错误:
ACCOUNT="claude" # Hardcoded
✅ 正确:
# Try configured → Try common → Error with help
ACCOUNT="${CONFIG_ACCOUNT}"
if ! has_data "$ACCOUNT"; then
for fallback in "claude" "default" "oauth"; do
if has_data "$fallback"; then
ACCOUNT="$fallback"
break
fi
done
fi
[[ -z "$ACCOUNT" ]] && error "No account found. Tried: ..."
规则:为常见变体提供自动回退机制。
模式4:有用的错误提示
❌ 错误:
[[ -z "$TOKEN" ]] && error "No token"
✅ 正确:
[[ -z "$TOKEN" ]] && error "No token found
Checked:
- Config: $CONFIG_FILE
- Field: $FIELD_NAME
- Expected: { \"tokens\": { \"refresh\": \"...\" } }
Verify with:
cat $CONFIG_FILE | jq '.tokens'
"
规则:错误信息应帮助用户诊断和修复问题。
调试方法论(Patrick的方法)
第一步:获取精确数据
不要问:“它坏了吗?”
要问:“你看到的具体数值是什么?存在多少条记录?哪一条包含数据?”
示例:
# Vague
"Check keychain"
# Specific
"Run: security find-generic-password -l 'Service' | grep 'acct'"
"Tell me: 1. How many entries 2. Which has tokens 3. Last modified"
第二步:用具体实例证明
不要说:“现在应该可以了”
要展示:“这是之前的状态(POp5z...),这是之后的状态(01v0R...),它们不同”
模板:
BEFORE:
- Field1: <exact_value>
- Field2: <exact_value>
AFTER:
- Field1: <new_value> ✅ Changed
- Field2: <new_value> ✅ Changed
PROOF: Values are different
第三步:立即考虑跨设备情况
不要想:“在我机器上能运行”
要想:“如果他们的设置在[X]方面不同怎么办?”
检查清单:
- 账户名是否不同?
- 不同的文件路径?
- 不同的工具/版本?
- 不同的权限?
- 不同的数据格式?
预发布检查清单(发布前)
发现阶段
- 列出所有外部依赖(文件、命令、服务)
- 记录每个依赖提供的内容
- 识别哪些部分可能在设备间存在差异
实施阶段
- 使变量可配置(并提供合理的默认值)
- 为每个输入添加验证
- 为常见变量构建回退链
- 添加
--dry-run或--test模式
测试阶段
- 使用正确配置测试 → 应正常工作
- 使用错误配置测试 → 应回退或优雅地失败
- 测试数据缺失的情况 → 应提供有用的错误信息
- 测试多个条目 → 应能处理模糊性
文档编写阶段
- 记录默认假设
- 记录如何验证本地设置
- 记录常见变体及其处理方法
- 包含数据流图
- 添加故障排除部分
真实案例:OAuth 刷新机制
原始(已损坏的)版本
# Assumes single entry, no validation, no fallback
KEYCHAIN_DATA=$(security find-generic-password -s "Service" -w)
REFRESH_TOKEN=$(echo "$KEYCHAIN_DATA" | jq -r '.refreshToken')
# Use token (hope it's valid)
问题:
- 返回首个字母匹配项(错误条目)
- 无验证(可能为空/格式错误)
- 无回退机制(账户名称不同时即失败)
修复(可移植)版本
# Explicit account with validation and fallback
validate_data() {
echo "$1" | jq -e '.claudeAiOauth.refreshToken' > /dev/null 2>&1
}
# Try configured account
DATA=$(security find-generic-password -s "$SERVICE" -a "$ACCOUNT" -w 2>&1)
if validate_data "$DATA"; then
log "✓ Using account: $ACCOUNT"
else
log "⚠ Trying fallback accounts..."
for fallback in "claude" "Claude Code" "default"; do
DATA=$(security find-generic-password -s "$SERVICE" -a "$fallback" -w 2>&1)
if validate_data "$DATA"; then
ACCOUNT="$fallback"
log "✓ Found data in: $fallback"
break
fi
done
fi
[[ -z "$DATA" ]] || ! validate_data "$DATA" && error "No valid data found
Tried accounts: $ACCOUNT, claude, Claude Code, default
Verify with: security find-generic-password -l '$SERVICE'"
REFRESH_TOKEN=$(echo "$DATA" | jq -r '.claudeAiOauth.refreshToken')
改进点:
- ✅ 明确的账户参数
- ✅ 验证数据结构
- ✅ 自动回退至通用名称
- ✅ 附带验证命令的有用错误提示
常见反模式
反模式一:"在我机器上能跑"
FILE="/Users/patrick/.config/app.json" # Hardcoded path
修复方案:使用$HOME环境变量、检测操作系统,或使其可配置
反模式二:"但愿它在那里"
TOKEN=$(cat config.json | jq -r '.token')
# What if .token doesn't exist? Script continues with empty value
修复方案:使用前先验证
TOKEN=$(cat config.json | jq -r '.token // empty')
[[ -z "$TOKEN" ]] && error "No token in config"
反模式三:"首个匹配即正确"
# If multiple entries exist, which one?
ENTRY=$(find_entry "service")
修复方案:明确指定或枚举所有可能
ENTRY=$(find_entry "service" "account") # Specific
# OR
ALL=$(find_all_entries "service")
for entry in $ALL; do
validate_and_use "$entry"
done
反模式四:"静默失败"
process_data || true # Ignore errors
修复方案:附带上下文信息明确报错
process_data || error "Failed to process
Data: $DATA
Expected: { ... }
Check: command_to_verify"
与现有工作流集成
在 sprint-plan.md 中
添加到测试环节:
## Cross-Device Testing
- [ ] Test with different account names
- [ ] Test with wrong config values
- [ ] Test with missing data
- [ ] Document fallback behavior
在 PRIVACY-CHECKLIST.md 中
发布前添加:
## Portability Check
- [ ] No hardcoded paths (use $HOME, detect OS)
- [ ] No hardcoded names (use config or fallback)
- [ ] Validation on all inputs
- [ ] Helpful errors for common issues
在技能创建器中
构建新技能时:
- 列出设备间的差异项
- 使其可配置或能自动发现
- 使用错误配置进行测试
- 文档问题排查
快速参考卡
编写代码前:
- 设备间有何差异?
- 如何证明此方案有效?
- 发生故障时会出现什么情况?
强制性模式:
- 显式优于隐式
- 使用前验证
- 回退链
- 有用的错误信息
测试:
- 正确配置 → 正常工作
- 错误配置 → 回退机制或有用的错误信息
- 数据缺失 → 清晰的诊断信息
文档:
- 数据流程图
- 常见变体
- 故障排查指南
成功标准
当工具具备以下特性时:可移植性即视为:
- ✅ 无需修改即可在不同设备上运行
- ✅ 自动发现配置中的常见变体
- ✅ 优雅失败并提供可操作的错误信息
- ✅ 通过阅读错误输出即可调试
- ✅ 文档涵盖“如果我的设置不同怎么办”
测试:交给配置不同的人使用。如果他们需要向你提问,说明该工具尚未具备可移植性。
起源故事
该方法论源于调试OAuth刷新器时(2026-01-23):
- 脚本读取了错误的钥匙串条目(未指定账户)
- 假设存在单一条目(实际存在多个)
- 缺乏验证(使用了空数据)
- 没有备用方案(在不同账户名称上失败)
帕特里克的方法:
- 要求提供精确数据(有多少条目,哪个包含令牌)
- 要求提供证明(显示刷新前/后的令牌)
- 考虑跨设备情况(如果命名方式不同怎么办?)
结果:该工具从仅限单设备/故障状态升级为通用/生产就绪状态。
关键洞察:问题不在逻辑本身——而在于预设条件。
何时使用此技能
适用于以下情况:
- 构建读取系统配置的工具时
- 处理密钥链、凭证、环境变量时
- 创建在多台机器上运行的脚本时
- 在ClawdHub发布技能时(供他人使用)
实施步骤:
- 实施前:回答三个问题
- 实施中:采用强制模式
- 测试前:执行起飞前检查清单
- 测试后:记录差异与故障排除方案
牢记:您的设备只是特例。请为通用情况构建解决方案。


微信扫一扫,打赏作者吧~