跳过正文
  1. 博客文章/

从 MCP 到一键发包:把 Teambition 评论里的 APK 自动上传 Nexus 的那些坑

·537 字·3 分钟·
技术实践 AI Agent MCP 自动化 Teambition Devops Nexus
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录

推荐阅读
#


目录
#


这周我被一个很"低级但很费命"的流程折磨了好几次:

  • 打开 Teambition
  • 找到任务评论
  • 下载 APK 附件
  • 手动改名(还得对齐内部路径规则)
  • 登录内网 Nexus 上传
  • 复制下载链接
  • 回到 TB 评论里粘贴

说实话,单次也就两三分钟,但一天来个十几次,人就会开始怀疑人生。

我最后干脆把这条链路做成了一个 “一句话触发的 Agent”:

“把 DO-XXXX 评论里的 APK 上传到 Nexus”

剩下的事情:找附件 → 拿签名 URL → 下载 → 文件名标准化 → 上传 → 输出公开链接(甚至可以自动回评论)。

下面把具体怎么做、踩了哪些坑,以及最后怎么封装成 Skill 讲清楚。

目标:把"手工点点点"变成可复用链路
#

这类自动化我一般不追求"炫技",就盯两个点:

  1. 可重复:同样一句话/同样一个 taskId,能稳定跑出来。
  2. 可维护:别人接手也看得懂;出问题能定位是 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,根本不可读
#

creatorIdexecutorId 这种字段返回的都是 MongoDB ObjectId:

  • 60471fc306c1e046e63759c4
  • 63d61d1cbde6c83a2ce729d6

这种东西放在日志里完全没意义,排查问题也很痛苦:

  • “是谁发的评论?”
  • “谁上传的包?”

解决方式我用了两层:

  1. 先用 ListProjectMembersV3 拉项目成员列表(覆盖常见人)
  2. 遇到陌生 ID 再用 PostV3MemberQuery 精确查询并刷新缓存

如果你也搞过内部系统,会懂这种"ID 翻译"的重要性——不然自动化只会变成"自动生成一堆没人看的日志"。

ID 映射的两层策略:

  1. ListProjectMembersV3 拉项目成员列表(覆盖常见人)
  2. 遇到陌生 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:

  1. ListTaskActivitiesV3 找到目标评论里的 APK 附件
  2. BatchGetFileDetails 把附件 resourceId 换成签名 URL
  3. curl -L 下载到 /tmp
  4. 标准化文件名
  5. 调用 apk-release-path 生成标准发布路径并上传 Nexus
  6. 输出可复制的公开下载链接

给一个"你照着就能跑"的 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 可下载"这个点切进去,一条链路跑通后再做封装。跑通比优雅重要。

相关文章

每日技术实践简报 - 2026-03-28:从 MCP 到一键 APK 发布的 Skill 封装
·119 字·1 分钟
实践记录 AI MCP 自动化 Skill 每日总结
每日技术实践简报 - 2026-03-25
·35 字·1 分钟
实践记录 每日总结 工程治理 Devops 知识管理
OpenClaw Skills Registry 安全架构与企业级实践指南
·780 字·4 分钟
技术实践 OpenClaw Skills 安全 企业级 Nacos