oh-my-pi 内置规则解析用 net.JoinHostPort 取代 fmt.Sprintf 构造 Go 网络地址【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi在 oh-my-pi 这个把 IDE 接入编码 Agent的项目中packages/coding-agent/src/discovery/builtin-rules/目录内置了一批针对 Go / Rust / TypeScript 的编码约束规则。其中go-join-hostport.md专门纠正一个高频且隐蔽的 Go 网络编程错误用fmt.Sprintf(%s:%d, host, port)拼接host:port地址会在 IPv6 场景下产出不可解析的字符串。读完本文你将掌握net.JoinHostPort的正确用法、IPv6 地址加方括号的底层原理并理解这条规则在 oh-my-pi 的 TTSRTime Traveling Stream Rules机制中如何被自动发现、触发与注入以及如何按需禁用或覆盖它。问题根源IPv6 字面量自带的冒号网络地址中的端口号之前需要冒号分隔但 IPv6 字面量本身就包含冒号。以环回地址::1为例fmt.Sprintf(%s:%d, ::1, 80) // 结果: ::1:80::1:80存在歧义究竟是地址::1加端口80还是地址::加端口1:80标准库的net.Dial无法解析这种形式。一个本意是[::1]:80的连接串被拼成了无法 parse 的::1:80网络请求会直接失败。这正是原规则文档指出的核心问题An IPv6 literal like::1has its own colons, sofmt.Sprintf(%s:%d, ::1, 80)yields::1:80— unparseable bynet.Dial.Go 标准库对此的标准答案是 net.JoinHostPort当 host 中含有冒号时自动补上方括号RFC 3986 的 IPv6 字面量表示法对普通 IPv4 地址和主机名则原样保留不做任何画蛇添足的处理。禁止写法Avoid原规则文档给出的反面示例是 Agent 在生成 Go 代码时经常顺手写出的模式addr : fmt.Sprintf(%s:%d, host, port) conn, err : net.Dial(tcp, addr)这段代码在 host 为 IPv4如127.0.0.1或域名如example.com时工作正常一旦 host 变成 IPv6 字面量::1、2001:db8::1等就会静默产出非法地址。这种平时没事、特定场景才炸的写法是最难排查的一类缺陷因此在规则中被标记为高风险模式。推荐写法Use// port is a string here; convert an int with strconv.Itoa. addr : net.JoinHostPort(host, strconv.Itoa(port)) conn, err : net.Dial(tcp, addr)注意net.JoinHostPort的端口参数类型是string而非int。如果端口来自int变量需要先用strconv.Itoa转换。原规则文档特别强调该函数在所有受支持的 Go 版本中均可用自 Go 1.0 起就是标准库的一部分不存在引入新依赖或要求最低 Go 版本的门槛可以直接替换。补充解析侧同样要配套与构造侧对称解析一个host:port字符串时应使用net.SplitHostPort而不是strings.Split或手动查找冒号——net.SplitHostPort能正确剥离 IPv6 字面量的方括号并区分[::1]:80中的地址与端口。构造用JoinHostPort、解析用SplitHostPort二者配套才能保证 IPv6 场景下的往返一致。静态防线go vet 的 hostport 分析器原规则文档还提到了一个重要的自动化检查手段Go 1.25 的go vet新增了hostport分析器专门标记fmt.Sprintf(%s:%d, host, port)这类拼接模式。也就是说除了依赖代码审查和 Agent 规则开发者还可以把go vet或go vet -vettool自定义分析器接入 CI从工具链层面拦住这类问题形成Agent 规则 静态检查的双重防线。规则如何在 oh-my-pi 中生效frontmatter 语义go-join-hostport.md不是一篇普通的 Markdown 笔记它的 frontmatter 定义了规则被 oh-my-pi 发现和触发的方式--- description: Build network addresses with net.JoinHostPort, not fmt.Sprintf(\%s:%d\, host, port) — the Sprintf form breaks on IPv6 condition: fmt\.Sprintf\(%s:%d scope: tool:edit(*.go), tool:write(*.go) interruptMode: never ---四个字段分别控制字段值含义description规则的一句话说明进入 rulebook供 Agent 在生成代码时参考同时也是规则能被检索/引用的依据conditionfmt\.Sprintf\(%s:%d正则匹配条件命中即触发 TTSR 拦截scopetool:edit(*.go), tool:write(*.go)限定触发流仅在edit/write工具处理*.go文件时生效interruptModenever命中时不中断当前生成流改为在工具结果前注入提醒condition、scope、interruptMode等字段的解析逻辑位于 packages/coding-agent/src/capability/rule.tsparseRuleConditionAndScope负责把condition/scope转成规则对象interruptMode支持never、prose-only、tool-only、always四种取值其中never表示匹配到也不中断仅静默提醒。scope: tool:edit(*.go), tool:write(*.go)的含义是只有当 Agent 调用edit或write工具、且目标文件是*.go时该规则的匹配才生效。这意味着 Agent 在写普通文本或编辑非 Go 文件时这条规则不会产生任何干扰一旦开始生成/修改 Go 源码并出现fmt.Sprintf(%s:%d模式规则立刻被激活。interruptMode: never 的实际效果结合 docs/ttsr-injection-lifecycle.md 对触发决策的描述interruptMode: never的规则在命中时不会 abort 当前流而是走非中断注入路径若匹配发生在edit/write这类工具调用流上TTSR 会在该工具返回结果时把渲染好的ttsr-tool-reminder.md提醒块前置到toolResult.content之前即system-reminder reasonrule_violation ...包装的规则正文Agent 拿到结果时就能看到请改用net.JoinHostPort的提醒若匹配发生在正文/思考流上则排队一个隐藏的注入消息在成功的助手消息之后通过agent.followUp()补发。选择never的理由也很清晰这类写法不合规但代码仍可运行的问题不需要打断生成流程重试注入一条提醒让 Agent 自我纠偏即可兼顾了正确性与生成效率。内置规则的嵌入与优先级这 27 条规则Go 8 条、Rust 5 条、TypeScript 14 条并非运行时从磁盘读取的散装文件而是在编译期通过with { type: text }嵌入进二进制见 packages/coding-agent/src/discovery/builtin-rules/index.tsimport goJoinHostport from ./go-join-hostport.md with { type: text }; // ... { name: go-join-hostport, content: goJoinHostport },这样bun build --compile产出的二进制不带任何零散规则文件规则文本全部内嵌原生安装包同样读取这一组模块。随后 packages/coding-agent/src/discovery/builtin-defaults.ts 以PRIORITY 1的最低优先级把它们注册为builtin-defaultsprovider// Lowest priority: every other rule provider wins a name conflict. const PRIORITY 1;最低优先级意味着同名覆盖机制任何来自用户级、项目级或工具级的同名规则如自定义一个go-join-hostport都会按 first-wins 规则优先生效覆盖内置副本。规则的分流逻辑集中在 packages/coding-agent/src/capability/rule-buckets.ts 的bucketRules中处理顺序为disabledRules剔除 →builtinRules false时整体剔除 →agents范围过滤 → 注册为 TTSR 规则 → 进入 always-apply / rulebook 分桶。触发链路从流式检测到注入规则注册进TtsrManager后见 packages/coding-agent/src/export/ttsr.ts 的addRule其condition会被编译为正则RegExpcompileRuleCondition还兼容(?i)这类 PCRE 内联旗标并在会话流式输出期间实时匹配turn_start时重置流缓冲区message_update期间按 source/tool 隔离缓冲区对edit/write工具流用重建的源码快照调用checkSnapshot匹配正则命中即返回匹配规则触发决策依据各规则的interruptMode聚合处理never规则走非中断注入路径。完整生命周期发现 → 注册 → 流式匹配 → 触发决策 → 重试调度 → 持久化 → 事件广播可参考 docs/ttsr-injection-lifecycle.md其中明确描述了condition正则编译失败时的降级行为该条件被跳过并记录 warning其余规则不受影响会话正常启动。如何禁用或覆盖这条规则go-join-hostport这类内置规则默认开启ttsr.builtinRules默认true用户有三种方式调整配置项定义见 packages/coding-agent/src/config/settings-schema.ts1. 整体关闭所有内置规则{ ttsr: { builtinRules: false } }这会丢掉整个builtin-defaults集合27 条全部不生效用户/项目规则仍正常加载。2. 单独禁用某一条{ ttsr: { disabledRules: [go-join-hostport] } }规则在分桶前即被剔除既不会作为 TTSR 匹配也不会出现在 rulebook 中。3. 用同名规则覆盖在任意更高优先级的来源项目级.cursor/.windsurf/.cline等目录或用户级配置放置一个同名的go-join-hostport规则文件即可用自定义内容覆盖内置副本——例如放宽匹配范围、修改为interruptMode: tool-only或补充自定义的违规示例。小结net.JoinHostPort与fmt.Sprintf(%s:%d, ...)之争的实质是手工拼接与标准库语义化构造的差异前者在 IPv6 面前必然失守后者天然免疫且零版本门槛。在 oh-my-pi 中go-join-hostport规则把这一结论编译进了 Agent 的行为约束——通过 frontmatter 声明的condition/scope/interruptMode配合 TTSR 流式检测与never模式的非中断提醒让 Agent 在编辑任何*.go文件时自动避开这个坑同时不打断正常的生成节奏。理解这条规则也就理解了 oh-my-pi 内置规则体系Markdown 声明式 低优先级可覆盖 流式注入的整体设计思路可直接迁移到自定义规则与团队编码规范的落地实践。【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考