结论先说:tri-coding 拿哪套编码规范写你的代码,不取决于它多聪明,而取决于
tech-skills/registry.md那张 25 行表格的排列顺序。我一个鸿蒙加微信小程序双端的项目,两端特征文件都命中了,它只加载了排在第 67 行的arkts,把wxml。
起因:design.md 里一个 wxml 都没有
雷达鸭是我自己在做的 App,收录中国一人公司和超级个体的真实赚钱案例,技术栈是 Uni-app 配 arkTS 插件加 UniCloud,微信小程序和鸿蒙版本同步在跑。日常就是靠 tri-xxx 这一票 skill 帮着打理开发流程。
那天要给它加一个「案例收藏夹」,双端都要。按 tri-coding 的流程走,门①需求审批过了,到门②拿到 design.md,我扫「编码规范」那一节,越看越不对劲:@Component、@State、build()、@Prop是单数别写成复数……全是 ArkTS 那一套。小程序侧的app.json分包、setData批量更新、wxml里不能写复杂表达式,一条都没有。
我头一个念头是需求写漏了。回头翻 requirements.md,双端写得清清楚楚。
顺着链路往上翻,卡点在氛围校准
tri-coding 的技术栈不写死在 SKILL.md 里。它在产出 design.md 的「氛围校准」步骤做一次匹配,链路是这样:读tech-skills/registry.md→ 扫项目特征文件 → 按注册表的触发规则逐条匹配 → 命中的技术栈规范注入 design.md 的「技术选型」与「编码规范」两个环节。执行阶段编码时,遵循的就是这批已加载的规范。
我那项目的特征文件是齐的,按注册表的触发规则一条条对:
| 我项目里的特征文件 | 该命中的 tech_id | 分类 | 实际加载了吗 |
|---|---|---|---|
tsconfig.json、*.ts | typescript | language | 是 |
*.ets、oh-package.json5、module.json5 | arkts | platform | 是 |
project.config.json、*.wxml | wechat | platform | 没有 |
.git/、.gitignore | git | tool | 是 |
| 始终加载 | general | base | 是 |
四个该命中的,三个到位,wechat凭空少了。
注册表里那条我之前没细读的规则
翻到 registry.md 的「加载规则」往下,有一段标题写着互斥规则(同类互斥,仅加载首个命中项):
- 框架层互斥组:
react↔vuejs - 框架层互斥组:
django↔fastapi↔flask - 平台层互斥组:
arkts↔wechat↔h5 - 互斥组内多个命中时,优先加载注册表中靠前的条目,其余跳过并记录告警
平台层那条后面还跟了句解释:「同一项目通常面向单一平台」。
这个假设对大多数项目成立,对跨端项目不成立。Uni-app 整个存在的意义就是一套代码编译到多端,arkts和wechat同时命中是它的常态,而不是异常输入。
再看「优先加载注册表中靠前的条目」——这句话的意思是,表格行序本身就是优先级。我去数了 platform 分类那几行在注册表里的位置:
| 注册表行号 | tech_id | 分类 | 互斥组内结果 |
|---|---|---|---|
| 67 | arkts | platform | 命中且靠前 →加载 |
| 68 | electron | platform | 未命中 |
| 69 | h5 | platform | 未命中 |
| 70 | wechat | platform | 命中但靠后 →跳过 |
arkts在第 67 行,wechat在第 70 行。arkts 赢了,赢的理由只是它在表里写得更早。
我个人挺不喜欢这种设计。把优先级藏在表格行序里,而不写成显式的priority字段,读的人不去数行号根本发现不了。
三个解法,前两个我都否了
| 方案 | 做法 | 结果 |
|---|---|---|
| 一 | 把wechat那行剪到arkts前面 | 小程序规范上来了,鸿蒙侧没了,方向掉个头而已 |
| 二 | 删掉平台层那条互斥组,让两个叠加 | 两端语法混进同一份「编码规范」,执行阶段分不清哪条管哪端 |
| 三 | 新增一个跨端 tech_id 显式注册 | 采用 |
方案一我真试了,把wechat提到arkts上面,重跑氛围校准,这回 design.md 里全是小程序那套,鸿蒙侧的build()和状态装饰器又没了。互斥组只放一个进来这件事没变。
方案二更糟。ArkTS 的build()链式调用和小程序的wxml模板语法混在同一节里,执行阶段 AI 分不清哪条约束管哪一端,我真怕它给我写出wxml里套Column()的东西。
方案三是我最后用的:加一个新的 tech_id,把「跨端」这件事显式注册进去。registry.md 自己写了它是「唯一扩展入口」,新增技术栈无需改 tri-coding/SKILL.md,那就用它给的路子走。
在注册表追加一行:
| uniapp-multiend | Uni-app 跨端(小程序+鸿蒙) | platform | package.json 含 `@dcloudio/uni-app` 且同时存在 `*.ets` 与 `project.config.json` | `uniapp-multiend.md` |再在tech-skills/uniapp-multiend.md里把两端约束按目录分区写清楚,而不是混成一锅:哪些规范管src/pages/(小程序侧),哪些管harmony/下的.ets(鸿蒙侧)。分区这一步是关键,不分区就退回方案二的问题了。
落到代码上,差别在这
加载对了之后,鸿蒙侧给的还是那套 ArkTS 写法,这部分本来就没错:
@Componentexportstruct FavoriteCard{@PropcaseTitle:string='';@PropmonthlyRevenue:number=0;@Linkstarred:boolean;build(){Row(){Column(){Text(this.caseTitle).fontSize(16).fontWeight(FontWeight.Medium);Text(`月流水 ¥${this.monthlyRevenue}`).fontSize(13).fontColor('#8A8A8E');}.alignItems(HorizontalAlign.Start).layoutWeight(1);Image(this.starred?$r('app.media.star_on'):$r('app.media.star_off')).width(24).height(24).onClick(()=>{this.starred=!this.starred;});}.width('100%').padding(12);}}小程序侧的规范补回来之后,多出来的是这类约束,也正是之前 design.md 里完全缺失的那部分:
// 小程序侧:收藏状态变更必须合并 setData,逐条调用会触发多次渲染Page({data:{list:[]},toggleStar(e){constidx=e.currentTarget.dataset.index;constlist=this.data.list;// 只推送变更路径,不整包重设 listthis.setData({[`list[${idx}].starred`]:!list[idx].starred,[`list[${idx}].starredAt`]:Date.now()});}});wxml里不写复杂表达式、setData走变更路径而不是整包重设,这些条目 ArkTS 那份规范里一个字都不会提。
顺手写了个脚本,别再靠肉眼数行号
数行号这活儿干一次就够了。写了个小脚本,扫注册表加项目特征,把命中的和被互斥吃掉的都打出来:
importjson,re,sysfrompathlibimportPath MUTEX_GROUPS=[{"react","vuejs"},{"django","fastapi","flask"},{"arkts","wechat","h5"},]# tech_id -> 用于判定的特征文件 glob(简化版,够用即可)FEATURES={"typescript":["tsconfig.json","**/*.ts"],"arkts":["**/*.ets","**/oh-package.json5","**/module.json5"],"wechat":["**/project.config.json","**/*.wxml"],"h5":["**/*.html"],"git":[".gitignore"],}defparse_registry(md:Path):"""按注册表出现顺序取出 (行号, tech_id, 分类)"""rows=[]forlineno,lineinenumerate(md.read_text(encoding="utf-8").splitlines(),1):m=re.match(r"^\|\s*([a-z0-9\-]+)\s*\|[^|]*\|\s*(base|language|framework|platform|tool)\s*\|",line)ifm:rows.append((lineno,m.group(1),m.group(2)))returnrowsdefhits(project:Path,tech_id:str)->bool:forpatinFEATURES.get(tech_id,[]):ifnext(project.glob(pat),None):returnTruereturnFalsedefresolve(project:Path,registry:Path):loaded,skipped=["general"],[]claimed=set()forlineno,tech_id,categoryinparse_registry(registry):iftech_id=="general"ornothits(project,tech_id):continuegroup=next((gforginMUTEX_GROUPSiftech_iding),None)ifgroupandgroup&claimed:winner=(group&claimed).pop()skipped.append((tech_id,lineno,winner))continueifgroup:claimed.add(tech_id)loaded.append(tech_id)returnloaded,skippedif__name__=="__main__":proj=Path(sys.argv[1]iflen(sys.argv)>1else".")reg=Path(sys.argv[2]iflen(sys.argv)>2else"tri-coding/tech-skills/registry.md")loaded,skipped=resolve(proj,reg)print("已加载:"," + ".join(loaded))fortech_id,lineno,winnerinskipped:print(f"[告警]{tech_id}(第{lineno}行) 被互斥跳过,让位给更靠前的{winner}")在我那项目根目录跑一下:
已加载: general + typescript + arkts + git [告警] wechat(第70行) 被互斥跳过,让位给更靠前的 arkts那句「其余跳过并记录告警」注册表里是写了的,但告警得落到我看得见的地方才算数。现在这行[告警]我在门②之前就能看到,不用等 design.md 交上来才发现少了半套。
一句话
registry 驱动这套设计的可扩展性是真的,加一行就能扩,SKILL.md 一个字不用改。但可扩展不等于会自动判断对:它的平台层互斥规则里压着一个「同一项目通常面向单一平台」的假设,跨端项目撞上去,AI 会安安静静地少给你半套规范,design.md 照样交得漂漂亮亮,格式齐全、章节完整、一处不缺。门②那关别只看格式,先数一下技术栈加载对没对。
我是老三,10 年软件开发经验,软件设计师、人工智能应用工程师,主要做鸿蒙应用(ArkTS)北向开发和 Web 前端,也在折腾 AI 自动化,不定期在 CSDN 写点鸿蒙和 AI 方向的东西。
本文遵循 MIT 协议,转载请注明出处即可。
这个系列的文章都来自开源的 tri 技能库。整套 tri-xxx 技能都能在 skillhub 找到并安装,一条命令装完即用,比如skillhub install tri-coding。装完每个技能都有 README,想摸清它到底能干嘛,读那个就够了。