GitHub 趋势 · 第 48 期

README 只有 25 字节,论文在另一个仓库

#插件框架#依赖注入#热重载

本期判断:cordis 是本轮月榜里动量最高的未写项目 —— 8,353 star,本月 +8,270。但它的根目录 README 全文只有 25 字节,理论单独开了一个仓库,官方文档挂在别人的文档站上。这不是摆烂,而是把三种不同的东西分别放到了它们最合适的位置。它值得读,但别指望靠一个仓库把它读懂。

插件式系统的老毛病:注册的副作用卸不干净,热重载之后旧监听器还挂着;插件之间有依赖,加载顺序却靠人手工编排,漏一个就静默失败;一个模块路径拼错,进程照跑,只是那个插件悄悄没生效。cordis 把这三件事当框架的第一性问题来解决 —— 而且它把「会静默失败」这件事也写进了文档。

它是什么

cordis 是 cordiverse 组织维护的插件框架,自述一句话:A Meta-Framework of Spatiotemporal Composability(时空可组合性的元框架)。仓库 2022 年 5 月创建,TypeScript 写成,MIT 许可,最新版本号停在 v4.0.0-rc.10,44 个未关闭 issue。

先看它最反常规的地方 —— 门面、理论、文档分别外置:

也就是说,想搞懂这套框架,得同时读四个地方:25 字节的指针、packages/core 里的短说明、另一个仓库的论文、别人家的文档站。根目录 README 的实际内容就是这样一行:

./packages/core/README.md

真正的说明只有三段,其中一段是警告

把那个指针跟到底,packages/core/README.md 的正文(Markdown 原文)是这样的:

Cordis

A Meta-Framework of Spatiotemporal Composability.

**Cordis is under active development. The API is not yet stable and may change without notice.**

- Paper: _A Programming Paradigm for Spatiotemporal Composability_ [arXiv] [repository]
- Documentation: [cordis-primer] (official documentation is still under construction)

中间那句加粗的话是本期必须写出来的:官方自己说明 API 不稳定、可能变更、且不另行通知。而版本号也确实还在 rc 线上。

更值得留意的是它对 rc 的处理方式。最新 release 是 v4.0.0-rc.10,发布于 2026-09-08,但这条 release 在 GitHub 上的 prerelease 标记是 false —— 也就是版本号里写着 rc,平台却把它算作正式发布。自动升级工具如果只看这个标记,会把它当稳定版收进依赖树。

而 rc 线里确实还有不兼容变更。v4.0.0-rc.10 的 release 说明第一条就是 Breaking Changes:loader 的 EntryTree.write()EntryTree.commit(change: EntryChange) 取代,子类每收到一次树上变更就会拿到一个 EntryChange,里面带着条目 id、所属分组、新 options 以及此前的 options。顺带一提,这份 release 说明是中英双语的,英文段和中文段各写一遍。

五个核心概念,写得相当克制

官方给的概念参考只有五个词条,一条一句,不铺垫:

五种事件分发模式,方法名不能混用

每个事件只有一种分发模式,且只能用对应方法分发。官方给了一张对照表:

模式是否 await分发顺序有返回值
emit监听器按注册顺序观察
waterfall监听器按注册顺序观察
parallel所有监听器并行观察事件
serial监听器按注册顺序观察
bail按注册顺序观察,直到某个监听器返回 bail 值

其中 waterfall 是环绕中间件。监听器收到的是 (...args, next),调用 next() 才执行下游;下游的返回值会通过 next() 回到当前这一层,可以包装之后继续往外传;不调 next() 直接返回就是短路。

官方对短路的说法很明确:对单决策事件,短路是设计意图。有决策权的策略监听器可以不调 next() 直接返回,只做标注或观察的监听器则必须委托下去。另外 prepend: true 只在监听器必须抢在普通注册之前运行时才用。

三种插件形态也一并给了。函数形态最常用,类形态留给要对外暴露服务的时候:

import { Service, type Context } from '@deepseek-ai/cordis'

// 1. Function plugin
export function apply(ctx: Context) {}

// 2. Object plugin: an object with an apply method.
export const objectPlugin = {
  name: 'object-plugin',
  apply(ctx: Context) {},
}

// 3. Class plugin: a Service subclass
export class MyService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'myTutorialService')
  }
}

上手:先写一个插件,再把它写进 YAML

官方教程的路径是在 deepseek-harness 仓库里跑一份 vendor 版启动器,而不是 npm 装一个包就完事(这件事后面单独说)。准备工作原文如下:

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial

然后写两个文件。插件本体是一个函数,导出 apply,cordis 加载时会用一个上下文去调用它:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello'

export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}

应用本身由配置组装,就是一个 YAML 列表:

- name: './hello.ts'

之后每一章都跑同一条命令。这个单文件启动器会创建根 Context、挂载 Loader 插件,并让它从当前目录加载 ./cordis.yml--import tsx 让 Node 不必构建就能直接跑 TypeScript:

node --import tsx ../../vendor/cordis/bin.js

预期输出是一行 hello from my first plugin,没有内容继续运行时进程会自己退出。注意插件文件里没有任何框架启动代码 —— 插件只描述自己的贡献,YAML 负责组合应用。

热重载、id,和那个静默的 PENDING

第 6 章把组合、热重载与排错串了起来。配置项除了 name 和 config,还能带别的元数据:id 给条目一个稳定标识,让 loader 能区分「改了现有条目」和「先删再加」;disabled: true 会卸载插件但保留条目,改回来时插件以及所有因等它而处于 PENDING 的插件都会重新加载。

热重载的配置长这样。hmr 要靠 logger 服务记日志,所以必须把 logger-console 一起挂上;它还会 inject timer 服务来做去抖:

- id: logger
  name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
  name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
  name: '@deepseek-ai/cordis-plugin-hmr'
  config:
    root: ['.']
- id: hello
  name: './hello.ts'

改一下 hello.ts 里的日志再保存,输出变成先 reload、再打新日志:

hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin

但这里藏着第一个坑:不带 id 的配置项,每次读取都会拿到一个新生成的 id。所以只要配置文件被编辑过,即使那一条的文本完全没变,也会被当成「先删除再添加」重新挂载一遍。

第二个坑更隐蔽:inject 了没人提供的服务时,插件就一直等,不报错、不输出。官方明确说 PENDING 是合法状态,因为提供方可能稍后才挂载 —— 但代价是这个失败完全静默。官方给的诊断办法是直接枚举插件注册表去看 fiber 状态:

import { FiberState, type Context } from '@deepseek-ai/cordis'

export const name = 'diagnose'

export function apply(ctx: Context) {
  setTimeout(() => {
    for (const runtime of ctx.registry.values()) {
      for (const fiber of runtime.fibers) {
        if (fiber.state === FiberState.PENDING) {
          console.log(`${fiber.name} is PENDING — a required service is missing`)
        }
      }
    }
  }, 500)
}

第三个坑直接写在教程正文里:如果配置项的模块无法被解析,比如路径或包名拼错,cordis 只会通过 logger 服务报个错,不会让进程崩掉;而在启动阶段,这条报告可能在 console 导出器开始观察之前就已经丢了。表现出来就是「新增了一条配置,但什么也没发生」。官方的建议是:先检查拼写。

我的判断

适合谁:正在自建插件式运行时或 agent harness 的团队 —— 它把「注册必须可逆」「依赖决定加载顺序」「事件分发模式固定」这三件事做成了框架级约束,而不是文档里的建议;也适合想看看「把设计理论单独写成论文」的开源实现长什么样的人。

不适合谁:想找一个装上就能用的成品框架的人。API 官方自述不稳定、版本还在 rc,上手路径是先 clone 另一个仓库跑 vendor 启动器,文档自述仍在建设中。

坑清单,全部来自原文实测:

数据来源:GitHub Trending 月榜(同期快照 8,365 star,本月 +8,270)· GitHub REST API 实测 8,353 star · 519 fork · 44 未关闭 issue · 35 watcher

仓库:https://github.com/cordiverse/cordis (TypeScript · 创建于 2022-05-17 · 最近推送 2026-09-08)

许可:MIT(LICENSE 为标准全文,Copyright (c) 2021-present Shigma,无追加自定义条款)

最新版本:v4.0.0-rc.10,2026-09-08 发布;GitHub 上 prerelease 标记为 false

论文:A Programming Paradigm for Spatiotemporal Composability,arXiv 2608.25512;论文仓库 https://github.com/cordiverse/paper (2,979 star,未开 issue 与 PR)

官方文档:https://deepseek-harness.github.io/deepseek-harness/reference/cordis-primer (原文自述 official documentation is still under construction)

抓取时间:2026-09-12;star 数以 REST API 为准,Trending 页快照略高,两者一并列出

本期到这里。如果你正在自建插件运行时,或者只是想看看「把设计理论单独写成论文」的开源项目长什么样,cordis 值得花一个晚上读。也欢迎留言告诉我:你更在意「注册必须可逆」,还是「依赖自动编排加载顺序」?

下期预告:modular/modular(29,685 star,本月 +3,080,Mojo/MAX);备选 NVIDIA-NeMo/Switchyard(2,876 star,本月 +2,628,Python)—— 在模型与供应商之间路由流量,同时保持 OpenAI 与 Anthropic 原生 API 兼容。

#开源框架#TypeScript#GitHub趋势