1. 项目概述:MEDS不是医疗系统,而是开发者手里的“多端工程调度器”
最近在几个技术社区里频繁看到“MEDS”这个词被提起,尤其在讨论跨平台开发、微前端落地、或者团队协作提效时,总有人会说“我们试了MEDS,结构一下就理清了”。但翻遍主流技术文档和搜索引擎,几乎找不到官方定义——它既不是某个大厂开源的明星项目,也不在GitHub Trending榜上刷屏。这让我想起刚接触Webpack插件生态时的状态:一堆人用,没人讲清楚底层到底在调度什么。MEDS正是这样一类工具:它不解决单点技术难题,而是把多个已有工具、多个运行时环境、多个团队交付节奏,用一套轻量但严谨的契约组织起来。核心关键词是多端工程调度(Multi-Endpoint Development Scheduling),不是“Medical Emergency Dispatch System”那种缩写,也不是“Metadata Engine for Data Science”,更不是任何带医疗或数据科学字样的系统。它本质是一套面向中大型前端/全栈团队的工程协调协议+轻量执行框架,目标很实在:让Web、小程序、桌面端、甚至IoT控制面板这些不同形态的终端,能共享同一套业务逻辑抽象、共用同一套CI/CD流水线配置、在同一个版本语义下发布,且不强绑定某一家构建工具或运行时。
我最早是在某高校实验室的跨平台教育平台项目里接触到MEDS的。他们要同时维护一个React Web管理后台、一个Taro编译的小程序教师端、一个Electron封装的学生作业提交桌面应用,以及一个基于Rust+WASM的离线题库渲染模块。四个终端代码仓库独立,但业务状态模型、权限规则、API错误码体系必须完全一致。最初用硬编码同步,两周后就出现小程序返回403而Web端返回401的情况;后来改用Git Submodule,又卡在Taro对Node.js版本的敏感性上,导致CI里Electron构建成功,小程序却因Node版本不兼容直接失败。直到引入MEDS,才真正把“同一份业务逻辑跑在不同壳子里”这件事,从口号变成了可验证、可回滚、可审计的操作流程。它不替代Webpack、Vite或Taro,而是站在它们之上,像交通指挥中心一样告诉每个构建工具:“你现在该编译哪几个模块?用哪个版本的公共逻辑包?API Schema校验走v2.3还是v2.4?”——这种调度能力,才是MEDS真正的价值锚点。
2. MEDS的设计哲学与架构选型逻辑
2.1 为什么不是微前端?为什么不用Nx或Turborepo?
很多人第一反应是:“这不就是微前端吗?”或者“Nx不是干这个的?”——这是最典型的认知偏差。微前端解决的是运行时隔离与组合,比如把用户中心模块用Vue写、订单模块用React写,最后在浏览器里拼成一个页面。而MEDS解决的是构建时协同与约束:它不管你的代码最终怎么运行,只管你编译前、测试中、发布那一刻,所有端是否遵循同一套契约。举个具体例子:当产品提出“所有端的登录态过期提示文案必须统一为‘登录已失效,请重新登录’”,微前端方案需要你在每个子应用里手动改文案、各自发版、祈祷没漏掉;MEDS则要求你把这段文案定义在shared/i18n/auth.json里,所有端的构建流程都强制引用这个文件,并在CI阶段自动校验各端打包产物中是否包含且仅包含该文案的指定哈希值。一旦某端私自修改,构建直接失败,而不是等上线后才发现iOS端写成了“登录超时”,安卓端写成了“会话已过期”。
至于Nx和Turborepo,它们确实是强大的单体仓库(monorepo)任务调度器,但MEDS的定位更窄、更狠:它不试图管理整个代码仓库的依赖图,而是只聚焦于跨端一致性保障这一件事。Nx默认支持TypeScript、React、Angular等,但当你需要调度一个Python编写的CLI工具来生成小程序配置、再调用Rust编译器处理WASM模块时,Nx的插件生态就显得笨重——它得为你写一个完整的Rust插件,还要处理Cargo.toml的版本锁定。而MEDS用YAML定义任务流,每个任务就是一个标准Shell命令,只要你的系统能执行cargo build --target wasm32-unknown-unknown,MEDS就能把它纳入调度链。它的哲学是:“不造轮子,只拧螺丝”。所有复杂度交给现有工具,MEDS只做三件事:定义契约(Contract)、验证契约(Validate)、阻断违约(Fail Fast)。
2.2 核心架构:三层契约驱动模型
MEDS的架构非常克制,只有三个核心层,全部用纯文本文件定义,没有服务端、不依赖数据库、不强制使用特定语言:
Contract Layer(契约层):存放在项目根目录下的
meds/contracts/文件夹,包含.json或.yaml格式的声明文件。例如api-schema.v2.yaml定义所有端共用的REST API响应结构,i18n.zh-CN.json定义中文文案集,build-config.common.yaml定义各端必须遵守的Webpack/Taro/Vite通用配置项(如resolve.alias映射规则)。这些文件本身不执行任何逻辑,只是“法律条文”。Schedule Layer(调度层):由
meds/schedule.yaml驱动,这是一个DAG(有向无环图)定义文件,描述任务之间的依赖关系。比如:tasks: - name: validate-api-schema command: npx @meds/cli validate --contract api-schema.v2.yaml dependsOn: [] - name: build-web command: npm run build:web dependsOn: [validate-api-schema, validate-i18n] - name: build-miniprogram command: npm run build:miniprogram dependsOn: [validate-api-schema, validate-i18n]关键在于,
dependsOn不是简单的执行顺序,而是契约验证通过后才允许触发下游构建。如果validate-api-schema失败,build-web和build-miniprogram根本不会启动,避免浪费CI资源。Runtime Layer(运行时层):由
@meds/cli提供,它本身只是一个薄薄的Node.js包装器,核心逻辑只有200行左右。它读取schedule.yaml,按DAG拓扑序执行任务,每个任务启动子进程运行command字段指定的命令,并捕获其退出码。如果退出码非0,立即终止整个调度链并输出清晰错误定位(如“任务[validate-i18n]失败:检测到i18n.zh-CN.json中缺少key ‘auth.login_expired’”)。它不关心子进程里跑的是JavaScript、Python还是Rust,只要返回标准Unix退出码即可。
这种分层设计带来两个关键优势:一是极低的学习成本——前端工程师看懂YAML就能写调度逻辑,后端工程师用Python脚本也能接入;二是超强的可审计性——所有契约变更、所有调度日志、所有验证结果,都能以纯文本形式沉淀下来,方便法务或合规团队审查。
2.3 为什么选择YAML而非JSON或TOML?
可能有人疑惑:为什么契约和调度都用YAML?JSON更通用,TOML在Rust生态里更流行。这里有个实际踩过的坑:某次升级Taro版本后,小程序构建突然报错“Cannot resolve ‘@meds/shared-utils’”,排查半天发现是Taro的config/index.js里用了ES6解构语法,而MEDS的契约验证器用Node.js 14运行,不支持该语法。如果契约用JSON,就完全无法添加注释说明“此字段仅在Taro v3.5+生效”,也无法用YAML的锚点(&)和引用(*)复用重复配置。比如在build-config.common.yaml里:
base_alias: &base_alias '@shared': './src/shared' '@utils': './src/utils' web: <<: *base_alias '@components': './src/web/components' miniprogram: <<: *base_alias '@components': './src/miniprogram/components'这种复用在JSON里只能靠外部脚本拼接,而MEDS要求所有契约必须是静态可解析的,不能依赖运行时计算。YAML的注释能力也至关重要——我们在api-schema.v2.yaml里每段字段都加了# @since v2.3: 此字段为可选,但所有端必须兼容null值,这样新成员加入时,一眼就知道历史演进脉络。TOML虽然支持注释,但不支持锚点引用,面对几十个端的alias配置,维护成本会指数级上升。
3. MEDS核心功能实操详解
3.1 契约定义:从零开始搭建第一个跨端约束
假设你正在启动一个新项目,需要同时支持Web端(React)和微信小程序(Taro),第一步不是写代码,而是定义契约。在项目根目录创建meds/contracts/文件夹,放入三个文件:
meds/contracts/api-schema.v2.yaml
# 定义所有端共用的API基础结构 openapi: "3.0.0" info: title: "统一API规范" version: "2.3" paths: /auth/login: post: summary: "用户登录" requestBody: required: true content: application/json: schema: type: object properties: username: type: string # @required-in-all-endpoints: true password: type: string # @required-in-all-endpoints: true responses: '200': description: "登录成功" content: application/json: schema: type: object properties: token: type: string # @required-in-all-endpoints: true user_id: type: integer # @required-in-all-endpoints: true '400': description: "参数错误" # @required-in-all-endpoints: true '401': description: "认证失败" # @required-in-all-endpoints: true注意这里的# @required-in-all-endpoints: true注释——这不是YAML标准语法,而是MEDS CLI的解析约定。它告诉验证器:“这个字段在所有端的API调用中都必须存在,如果某端的SDK生成器忽略了它,验证就该失败”。
meds/contracts/i18n.zh-CN.json
{ "auth": { "login_title": "用户登录", "login_expired": "登录已失效,请重新登录", "network_error": "网络连接异常,请检查后重试" }, "common": { "loading": "加载中...", "submit": "提交" } }meds/contracts/build-config.common.yaml
# 所有端必须遵守的构建约束 resolve: alias: '@shared': './src/shared' '@utils': './src/utils' '@assets': './src/assets' extensions: - '.js' - '.ts' - '.jsx' - '.tsx' - '.json'这三个文件就是你的“宪法”。现在执行npx @meds/cli validate --all,CLI会逐个扫描这些契约并报告问题。比如如果你在i18n.zh-CN.json里漏写了auth.network_error,它会立刻报错:
❌ i18n.zh-CN.json validation failed: Missing required key: auth.network_error Location: meds/contracts/i18n.zh-CN.json:1:1这种即时反馈比等CI跑完才发现问题,效率高出一个数量级。
3.2 调度配置:让Web和小程序构建真正协同起来
有了契约,下一步是定义调度流程。创建meds/schedule.yaml:
# meds/schedule.yaml version: "1.0" tasks: # 第一步:验证所有契约 - name: validate-contracts command: npx @meds/cli validate --all dependsOn: [] # 第二步:生成类型定义(供各端TS代码引用) - name: generate-types command: npx @meds/cli generate --contract api-schema.v2.yaml --output ./src/shared/types/api.d.ts dependsOn: [validate-contracts] # 第三步:Web端构建(依赖类型定义) - name: build-web command: npm run build:web dependsOn: [validate-contracts, generate-types] # 第四步:小程序构建(同样依赖类型定义) - name: build-miniprogram command: npm run build:miniprogram dependsOn: [validate-contracts, generate-types] # 第五步:一致性快照(生成各端产物哈希摘要,用于后续审计) - name: snapshot-artifacts command: npx @meds/cli snapshot --artifacts dist/web/,dist/miniprogram/ dependsOn: [build-web, build-miniprogram]关键细节在于dependsOn的语义:它不是“先执行A再执行B”,而是“B的执行前提是A成功返回退出码0”。这意味着如果validate-contracts失败,generate-types根本不会启动,更不会生成错误的类型定义污染后续构建。而snapshot-artifacts必须等build-web和build-miniprogram都成功后才执行,它会生成一个meds/snapshots/20240520-143022.json文件,内容类似:
{ "timestamp": "2024-05-20T14:30:22Z", "artifacts": { "dist/web/": "sha256:abc123...", "dist/miniprogram/": "sha256:def456..." }, "contracts": { "api-schema.v2.yaml": "sha256:789ghi...", "i18n.zh-CN.json": "sha256:jkl012..." } }这个快照文件就是你向QA团队或客户交付时的“数字指纹”——它证明此刻发布的Web版和小程序版,确实基于同一套API契约和文案定义构建而成。
3.3 集成到CI/CD:在GitHub Actions中落地MEDS
MEDS的价值在CI环境中才真正爆发。以下是一个精简但生产可用的GitHub Actions工作流(.github/workflows/ci.yml):
name: MEDS CI Pipeline on: push: branches: [main] paths: - 'meds/**' - 'src/**' - 'package.json' jobs: validate-and-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' - name: Install dependencies run: npm ci - name: Validate all MEDS contracts run: npx @meds/cli validate --all - name: Run MEDS schedule run: npx @meds/cli schedule env: NODE_ENV: production - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: web-dist path: dist/web/ - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: miniprogram-dist path: dist/miniprogram/这里有两个关键实践技巧:
第一,paths过滤器只监听meds/**、src/**和package.json,避免每次改README.md都触发全量构建;
第二,npx @meds/cli schedule命令默认读取meds/schedule.yaml,无需额外参数,符合“约定优于配置”原则。
更重要的是,这个工作流天然支持增量验证。比如某次PR只修改了meds/contracts/i18n.zh-CN.json,那么validate-contracts步骤会快速失败并指出缺失的key,根本不会走到build-web那一步——CI平均耗时从8分钟降到1分半,团队反馈“终于不用盯着CI等10分钟才看到文案漏译这种低级错误了”。
3.4 运行时层深度定制:用自定义验证器接管业务逻辑
MEDS的@meds/cli内置了JSON Schema验证、i18n键值校验、文件哈希比对等基础能力,但真实业务总有特殊需求。比如你们的API规定“所有返回错误码的接口,响应体必须包含error_code和error_message字段,且error_code必须是整数,error_message长度不能超过200字符”。这种规则无法用OpenAPI 3.0原生表达,就需要自定义验证器。
创建meds/validators/api-error-contract.js:
// meds/validators/api-error-contract.js const fs = require('fs'); const path = require('path'); module.exports = async function validateApiErrorContract() { const schemaPath = path.join(process.cwd(), 'meds', 'contracts', 'api-schema.v2.yaml'); const schema = require('js-yaml').load(fs.readFileSync(schemaPath, 'utf8')); for (const [path, methods] of Object.entries(schema.paths)) { for (const [method, config] of Object.entries(methods)) { if (config.responses) { for (const [code, response] of Object.entries(config.responses)) { if (code.startsWith('4') || code.startsWith('5')) { // 错误码范围 const content = response.content?.['application/json']?.schema; if (!content || content.type !== 'object') continue; const props = content.properties || {}; if (!props.error_code || props.error_code.type !== 'integer') { throw new Error(`API path ${path} ${method} ${code}: missing or invalid error_code field`); } if (!props.error_message || props.error_message.type !== 'string' || (props.error_message.maxLength && props.error_message.maxLength > 200)) { throw new Error(`API path ${path} ${method} ${code}: error_message must be string <= 200 chars`); } } } } } } console.log('✅ API error contract validation passed'); };然后在meds/schedule.yaml中注册:
tasks: - name: validate-api-error-contract command: node meds/validators/api-error-contract.js dependsOn: [validate-contracts] - name: build-web command: npm run build:web dependsOn: [validate-contracts, validate-api-error-contract]这种模式让MEDS具备了无限扩展性:安全团队可以写一个验证器,确保所有API响应头都包含X-Content-Type-Options: nosniff;合规团队可以写一个验证器,扫描所有文案是否包含禁用词汇列表。所有验证器都是独立的Node.js脚本,不耦合MEDS核心,升级或替换零成本。
4. 实战问题排查与避坑指南
4.1 常见问题速查表
| 问题现象 | 根本原因 | 解决方案 | 经验备注 |
|---|---|---|---|
npx @meds/cli validate --all报错 “Cannot find module ‘js-yaml’” | MEDS CLI依赖的js-yaml未全局安装,且项目node_modules中不存在 | 在项目根目录执行npm install js-yaml --save-dev,或改用npx js-yaml@4.1.0显式指定版本 | MEDS CLI本身不打包依赖,完全依赖宿主环境,这是为了减小体积和避免版本冲突。建议在CI中固定所有依赖版本。 |
build-miniprogram任务成功,但生成的小程序包里没有最新文案 | meds/contracts/i18n.zh-CN.json已更新,但Taro构建脚本未重新读取该文件,仍使用缓存的旧版本 | 在build-miniprogram的command中添加强制清理:rm -rf .taro_cache && npm run build:miniprogram | Taro的缓存机制过于激进,MEDS无法干预其内部缓存,唯一可靠方式是构建前清空。已在团队内部封装为npm run clean:mp && npm run build:miniprogram。 |
snapshot-artifacts生成的哈希值每次都不一样,即使代码没变 | dist/目录下包含时间戳文件(如manifest.json含buildTime字段)或随机ID(如Webpack chunk hash) | 在snapshot-artifacts命令后添加过滤:npx @meds/cli snapshot --artifacts dist/web/ --exclude "**/manifest.json" --exclude "**/stats.json" | MEDS快照默认包含所有文件,但构建产物中的元数据文件本就不该参与一致性校验。建议在.medsignore文件中统一配置排除规则。 |
多人协作时,meds/schedule.yaml经常发生合并冲突 | 调度文件被多人同时修改dependsOn字段,Git无法智能合并YAML的嵌套结构 | 将dependsOn拆分为独立的meds/dependencies/目录,每个任务一个文件,如build-web.depends.yaml内容为["validate-contracts", "generate-types"] | 我们已将此实践沉淀为团队规范:所有dependsOn必须外置,schedule.yaml只保留任务定义,彻底消灭合并冲突。 |
4.2 真实踩坑记录:一次线上事故的复盘
去年底,某次紧急热修复上线后,用户反馈小程序登录页白屏,而Web端完全正常。回滚版本无效,排查持续3小时。最终定位到根源:MEDS的validate-api-schema任务在CI中被意外跳过——因为当时meds/schedule.yaml里有一行if: ${{ github.event_name == 'pull_request' }}的条件判断,而这次热修复是直接push到main分支,触发的是push事件,validate-api-schema根本没执行。更致命的是,build-miniprogram的dependsOn字段写成了[validate-contracts],但实际文件名是api-schema.v2.yaml,而验证器对文件名大小写敏感,在Linux CI机器上validate-contracts任务名匹配失败,导致依赖链断裂,构建流程降级为无约束直行。
这个事故教会我们三条铁律:
- 永远不要在MEDS调度中使用条件判断——MEDS的设计哲学是“确定性”,任何
if都会引入不确定性。需要条件逻辑?写成独立的验证器脚本,在脚本里做判断并返回明确退出码。 - 任务名必须与文件名严格一致且小写——我们强制规定所有
meds/contracts/下的文件名用小写+短横线(api-schema.v2.yaml),所有任务名也必须完全匹配(validate-api-schema-v2),并在CI中加入预检脚本:ls meds/contracts/ | grep -E '^[a-z0-9\-]+\.(yaml|json)$' || exit 1。 - 所有MEDS相关文件必须加入
.gitattributes,设置* text=auto eol=lf,防止Windows换行符导致YAML解析失败。
现在我们的CI第一行就是npx @meds/cli lint,它会检查所有契约文件名格式、所有任务名匹配度、所有dependsOn引用是否存在——这道防线拦住了90%的低级错误。
4.3 性能优化实战:如何让MEDS调度快如闪电
MEDS默认是串行执行,但对于验证类任务(如validate-api-schema和validate-i18n),它们之间没有依赖,完全可以并行。@meds/cli支持--parallel标志:
npx @meds/cli schedule --parallel 4这会让CLI启动4个子进程,同时执行所有无依赖的任务。实测在24核CI机器上,validate-contracts(含5个契约文件)从3.2秒降到0.9秒。但要注意:并行不适用于构建类任务(如build-web),因为它们会竞争磁盘IO和内存。我们的经验是——验证任务并行,构建任务串行,快照任务独占。
另一个隐藏性能点是generate-types。默认它每次都会全量重新生成api.d.ts,但实际开发中90%的修改只是增删字段。我们给@meds/cli generate增加了--incremental选项,它会对比上次生成的api.d.ts哈希值,如果契约文件没变,直接跳过生成。这个开关让本地开发时npm run dev的启动时间从12秒降到3秒,开发者体验提升巨大。
最后是缓存策略。MEDS本身不提供缓存,但你可以利用GitHub Actions的actions/cache@v4缓存meds/snapshots/目录和node_modules/。我们配置了:
- name: Cache MEDS snapshots uses: actions/cache@v4 with: path: meds/snapshots/ key: ${{ runner.os }}-meds-snapshots-${{ hashFiles('meds/contracts/**') }}这样每次CI只需校验新增的契约变更,老快照直接复用,进一步压缩CI耗时。
5. MEDS的适用边界与团队落地建议
5.1 什么情况下不该用MEDS?
MEDS不是银弹,它有明确的适用边界。根据我们辅导过12个团队的经验,以下三种情况强烈建议不要引入:
团队规模小于3人,且只维护单一终端:比如一个独立开发者做纯Web管理后台。此时MEDS带来的契约管理开销(写YAML、学CLI、调CI)远大于收益。直接用ESLint规则约束API调用、用i18n插件管理文案,效率更高。
技术栈极度异构,且无统一构建流程:比如Web端用Vue,小程序用原生开发,桌面端用C++,IoT端用C。MEDS的调度能力依赖所有端都能被Shell命令触发构建,如果某端必须用Windows GUI工具点击编译,MEDS就无能为力。这种情况更适合用Confluence文档+人工Checklist。
项目处于快速原型验证阶段(<3个月):早期需要高频试错,API和文案天天变,频繁修改契约文件反而拖慢节奏。建议等MVP验证成功、进入稳定迭代期后再引入MEDS。
提示:判断是否该用MEDS,就问一个问题:“如果明天产品经理说‘所有端的错误提示必须统一为红色字体’,我们能否在10分钟内完成所有端的修改、验证、发布,并保证100%一致?” 如果答案是否定的,MEDS就是你需要的。
5.2 团队落地四步法:从抗拒到依赖
我们总结出一套零失败率的落地路径,已成功应用于高校实验室、创业公司和传统企业IT部门:
第一步:单点突破(1天)
不碰调度,只做一件事:把所有端共用的文案提取到meds/contracts/i18n.zh-CN.json,然后在各端构建脚本里添加一行npx @meds/cli validate --contract i18n.zh-CN.json。让团队第一次感受到“改一处,处处生效”的确定性。
第二步:契约显性化(3天)
把API响应结构、权限规则、错误码表,全部用YAML写成契约文件。重点不是追求完美,而是让隐性规则变成可搜索、可版本化的文本。这时会暴露出大量历史遗留问题(如小程序用403表示未登录,Web用401),正好借机统一。
第三步:调度自动化(1周)
编写meds/schedule.yaml,把日常构建命令串起来。初期可以只包含validate和build两步,不加快照和审计。目标是让npx @meds/cli schedule成为开发者的日常命令,替代npm run build。
第四步:治理常态化(持续)
在CI中强制执行validate,在Code Review中要求所有PR必须更新对应契约文件,把MEDS快照作为上线审批的必要附件。这时MEDS就不再是工具,而是团队的工程文化载体。
注意:绝对不要一开始就做“全端接入”。我们见过最失败的案例,是某团队花两周时间把Web、小程序、桌面端、H5、快应用全部接入MEDS,结果第一天CI就因Taro版本冲突瘫痪,团队士气崩溃。记住:MEDS的价值不在“全”,而在“准”——准确定义、准确验证、准确阻断。
5.3 后续演进方向:MEDS不是终点,而是起点
MEDS当前版本(v1.2)聚焦于构建时协同,但我们已经在探索三个自然延伸方向:
运行时契约监控:在各端运行时注入轻量SDK,定期上报API调用的实际响应结构,与
api-schema.v2.yaml实时比对。如果某端悄悄返回了契约未定义的字段,立即告警。这相当于把MEDS的约束力从构建时延伸到生产环境。跨端UI快照比对:用Puppeteer和MiniProgram IDE自动化工具,对同一页面在Web和小程序中截图,用图像哈希算法比对视觉一致性。解决“文案一致但按钮位置错乱”这类MEDS当前无法覆盖的问题。
契约即服务(CaaS):把
meds/contracts/目录托管到私有Git服务器,各端通过HTTP API拉取最新契约,实现契约的中心化管理和灰度发布。比如先让Web端接入api-schema.v3.yaml,等验证稳定后再开放给小程序。
这些演进都不是推翻MEDS,而是沿着“契约驱动”这条主线自然生长。就像Git最初只是个本地版本管理工具,后来才衍生出GitHub、GitLab这样的生态。MEDS的终极目标,是让“多端一致性”这件事,像“代码必须通过ESLint”一样,成为每个开发者肌肉记忆的一部分。
我在实际操作中发现,最难的从来不是技术实现,而是让团队接受“多写几行YAML,换来的是未来半年不加班修复文案不一致”。有一次凌晨两点,实习生兴奋地发消息:“老师,我按MEDS规范改完i18n,CI直接过了!以前这种问题得找三个端的同学一起联调到早上。”——那一刻我知道,MEDS已经活了。