推荐阅读#
目录#
- 目标:把"手工点点点"变成可复用链路
- 技术方案:为什么选 MCP + Skill
- 实战:从 mcporter 调通第一条链路
- Skill 封装:tb-apk-uploader 的核心流程
- 最终效果:一句话发包
- 踩坑总结:别被细节拖死
这周我被一个很"低级但很费命"的流程折磨了好几次:
- 打开 Teambition
- 找到任务评论
- 下载 APK 附件
- 手动改名(还得对齐内部路径规则)
- 登录内网 Nexus 上传
- 复制下载链接
- 回到 TB 评论里粘贴
说实话,单次也就两三分钟,但一天来个十几次,人就会开始怀疑人生。
我最后干脆把这条链路做成了一个 “一句话触发的 Agent”:
“把 DO-XXXX 评论里的 APK 上传到 Nexus”
剩下的事情:找附件 → 拿签名 URL → 下载 → 文件名标准化 → 上传 → 输出公开链接(甚至可以自动回评论)。
下面把具体怎么做、踩了哪些坑,以及最后怎么封装成 Skill 讲清楚。
目标:把"手工点点点"变成可复用链路#
这类自动化我一般不追求"炫技",就盯两个点:
- 可重复:同样一句话/同样一个 taskId,能稳定跑出来。
- 可维护:别人接手也看得懂;出问题能定位是 TB、下载、还是上传。
而且我不想做"半自动",那种最后还要打开网页点一下确认的,体验很差。
技术方案:为什么选 MCP + Skill#
这里的关键是:Teambition 的任务评论、附件、成员信息,本质上都能 API 化。
我用的是:
- Teambition MCP Server:通过
mcporter接入 OpenClaw,让 Agent 能调用 TB 的开放 API。 - ListTaskActivitiesV3:拿到任务动态/评论,以及评论里附件列表。
- BatchGetFileDetails:把附件资源 ID 转成带签名的下载 URL(可直接
curl)。 - PostV3MemberQuery:把 userId(ObjectId)翻译成"人类看得懂的姓名"。
- apk-release-path Skill:内部规则化的 Nexus 路径生成 + 上传(这一步很关键,避免每个人传出来路径不一样)。
- tb-apk-uploader Skill:把上面全部串起来,做成一句话入口。
整体结构长这样:
flowchart LR A[Teambition 任务 DO-XXXX] --> B[ListTaskActivitiesV3
取评论/附件] B --> C[BatchGetFileDetails
拿签名下载URL] C --> D[curl 下载到 /tmp] D --> E[APK 文件名标准化] E --> F[apk-release-path
生成路径+上传 Nexus] F --> G[输出公开下载链接] G --> H[可选:回写 TB 评论]
实战:从 mcporter 调通第一条链路#
1) 第一个坑:mcporter 的参数格式#
我一开始很自然地写了:
mcporter call teambition-mcp BatchGetFileDetails --body '{"resourceIds":["..."]}'
然后直接炸:SyntaxError。
后来才反应过来:mcporter 不是按你想象的"HTTP body"来收参。
- mcporter 要用
--args,不是--body - 大部分 TB MCP 工具的入参都套在
requestBody里 - 先用最小请求跑通,再往里加字段
正确姿势:
mcporter call teambition-mcp BatchGetFileDetails \
--args '{"requestBody":{"needSign":true,"expireAfterSeconds":1800,"resourceIds":["task:xxxx/activity:yyyy/file:zzzz"]}}'
这一步通了之后,接口会给你一个带签名的下载地址(有过期时间)。接下来就简单了:curl -L。
2) 拿评论和附件:ListTaskActivitiesV3#
评论/动态是从 ListTaskActivitiesV3 拿的,关键是把附件列表捞出来。
命令示例(按任务ID拉最近 50 条动态):
mcporter call teambition-mcp ListTaskActivitiesV3 \
--args '{"taskId":"<TASK_ID>","pageSize":50,"orderBy":"created_desc","language":"zh_CN"}'
你会得到一堆 activities,其中包含评论内容、创建人、以及附件的 fileId / resourceId 等信息。
3) 第二个坑:成员 ID 全是 ObjectId,根本不可读#
creatorId、executorId 这种字段返回的都是 MongoDB ObjectId:
60471fc306c1e046e63759c463d61d1cbde6c83a2ce729d6
这种东西放在日志里完全没意义,排查问题也很痛苦:
- “是谁发的评论?”
- “谁上传的包?”
解决方式我用了两层:
- 先用
ListProjectMembersV3拉项目成员列表(覆盖常见人) - 遇到陌生 ID 再用
PostV3MemberQuery精确查询并刷新缓存
如果你也搞过内部系统,会懂这种"ID 翻译"的重要性——不然自动化只会变成"自动生成一堆没人看的日志"。
ID 映射的两层策略:
ListProjectMembersV3拉项目成员列表(覆盖常见人)- 遇到陌生 ID 用
PostV3MemberQuery精确查询并刷新缓存
4) 第三个坑:APK 文件名太野了,不标准化会出大事#
Teambition 附件下载下来,经常出现:
.apk.1这种后缀(1)这种重复下载的括号- 前缀带
E(比如E4218)
真实例子:
RinoTrack_E4218_3.2.820260326_release(1).apk.1
我想要的标准化结果:
RinoTrack_4218_3.2.820260326_release.apk
如果不做这一步,后面上传 Nexus 的路径和文件名就会失控:
- 同一个版本被传出多个"看起来不一样"的包
- 别人复制链接下载的是错误文件
- 更坑的是:CI/自动脚本按规则匹配文件名时直接找不到
我最终把规则写成了一个"尽量保守"的清洗:
- 去掉末尾
.1/.2这种下载器后缀 - 去掉末尾
(n) - 把
_E4218_规整成_4218_(仅在匹配到E\d+的场景) - 保证最终以
.apk结尾
下面贴个可用的 Python 版本(我的 tb-apk-uploader 里就是类似逻辑):
import re
from pathlib import Path
def normalize_apk_name(filename: str) -> str:
name = filename
# 先干掉可能的"重复下载后缀"
# 例:xxx.apk.1 / xxx.apk.2
name = re.sub(r"(\.apk)\.\d+$", r"\1", name, flags=re.IGNORECASE)
# 干掉 (1) (2) 这种
name = re.sub(r"\(\d+\)(?=\.apk$)", "", name, flags=re.IGNORECASE)
# 规整 E4218 -> 4218(只处理紧跟数字的 E 前缀)
name = re.sub(r"_E(\d+)_", r"_\1_", name)
# 兜底:如果结尾不是 .apk,强行补回去
if not name.lower().endswith(".apk"):
name = re.sub(r"\.+$", "", name) + ".apk"
return name
if __name__ == "__main__":
samples = [
"RinoTrack_E4218_3.2.820260326_release(1).apk.1",
"Demo_E1234_xxx.apk",
]
for s in samples:
print(s, "->", normalize_apk_name(s))
这种规则肯定不是"完美",但目标是:别把"人类给的附件名"原样带进制品仓库。
Skill 封装:tb-apk-uploader 的核心流程#
当所有接口都调通了,就到了我最喜欢的部分:把它封装成能复用、能迭代的 Skill。
我这里的 tb-apk-uploader 其实就是一个 Python 脚本 + 少量 glue:
- 用
ListTaskActivitiesV3找到目标评论里的 APK 附件 - 用
BatchGetFileDetails把附件 resourceId 换成签名 URL curl -L下载到/tmp- 标准化文件名
- 调用
apk-release-path生成标准发布路径并上传 Nexus - 输出可复制的公开下载链接
给一个"你照着就能跑"的 shell 片段(假设你已经拿到了签名 URL):
set -euo pipefail
SIGNED_URL="$1"
RAW_NAME="$2" # 从 TB 附件字段里拿到的原始文件名
TMP_DIR="/tmp/tb-apk"
mkdir -p "$TMP_DIR"
python3 - <<'PY'
import os
from pathlib import Path
from normalize import normalize_apk_name # 你也可以直接把函数内联
raw = os.environ["RAW_NAME"]
out = normalize_apk_name(raw)
print(out)
PY
我自己的实现里更直接:Python 负责从 TB 拉数据并落地下载,shell 只做"串接工具"。
apk-release-path 这一步就不展开了(它是另一个 Skill),你只需要知道:
- 它把"文件名"转换成内部统一的发布路径
- 然后上传到 Nexus
- 最终给你一个可访问的下载 URL
这是我想要的最终体验:
- 我不关心 Nexus 目录怎么分层
- 我不关心文件名怎么对齐规则
- 我只要:发包链接
最终效果:一句话发包#
做到这里,就可以真的把入口收敛成一句话了。
比如:
- “把 DO-12345 评论里的 APK 上传到 Nexus”
Agent 会做:
- 自动找到最新评论里的 APK 附件(或按规则选中某条评论)
- 下载并标准化文件名
- 上传 Nexus
- 返回:
https://nexus.xxx/repository/.../RinoTrack_4218_3.2.820260326_release.apk
如果你愿意,再加一步:
- 自动把链接回写到 TB 评论里(避免手动复制粘贴)
这就从"脚本自动化"变成了"工作流自动化"。
踩坑总结:别被细节拖死#
最后把最实用的坑总结一下,免得你从头踩:
mcporter 入参别想当然#
--body 不是你以为的那种 body。大部分工具入参要套 requestBody,先用最小请求跑通,再往里加字段。ID 翻译是生产力#
- ObjectId 放在日志里没意义
- 建一个成员映射缓存,遇到未知 ID 再刷新
- 排查问题会快很多
文件名标准化必须做#
Skill 封装要有边界#
我封装 tb-apk-uploader 时有个底线:
- 失败就失败,日志说清楚原因
- 不做"半成功"状态(比如上传了一半还返回链接)
- 不要把 Nexus 的规则散落在多个脚本里
这类自动化做多了你会发现:
真正省下来的不是"点几下鼠标"的时间,而是减少中断。
人一旦被打断(找评论、下载、改名、上传、复制链接),注意力就碎了,恢复成本比操作本身大得多。
这次把链路收敛成一句话,算是把"碎片化劳动"按死了。
如果你也有类似的重复流程,建议先从"签名 URL 可下载"这个点切进去,一条链路跑通后再做封装。跑通比优雅重要。
