npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pi-web/media-kit

v0.1.0

Published

多 provider 媒体生成工具套件:模型目录是 JSON 数据,方言是 TS 代码。第一期能力面是图像。

Readme

@pi-web/media-kit

多 provider 媒体工具套件。内置图片生成、图片编辑、视频生成、图片理解与视频理解工具, 背后可接 Cloudflare、DashScope、火山方舟等厂商接口。

包名取 media 而不是 image,是因为目录里的 capabilities 和结果类型 PickedResult 从第一天就给视频、音频留了位置:将来扩媒体类型只是加方言加工具,包的骨架不动。

一句话说清它跟别的做法的区别

模型目录是数据,不是代码。 provider、模型、端点声明全部写在 JSON 文件里, 由一份从代码生成的 schema 校验。TypeScript 只写两类东西:

  • 方言(dialect):一种上游线格式一个小模块,实现没法序列化的那一半 —— 构造请求体、从响应里取结果、判错、轮询语义、SSE 帧累积。
  • 引擎与编排器:与目录内容完全无关的通用执行逻辑。

于是新增一个模型 = 改几行 JSON,不写代码、不重新构建、不发新版包。

三级扩展面(能用浅的就别用深的)

一、只加模型(纯数据,零代码)

在 agent source 或者本机的 agent 目录下放一个 media/catalog/,和内置目录同一套 schema。装配方把它经 catalogDirs 传进来:

makeMediaExtension(host, { catalogDirs: [".../media/catalog"] });

目录可以是"目录式"(provider.json + models/*.json,一模型一文件,路由键必须等于 文件名),也可以是"单文件简式"(provider 和 routes 写在一个 JSON 里,适合临时加一个模型)。

每个工具还自动带一条同名斜杠命令(/image_generation 一只戴礼帽的柴犬), 不经过 LLM 直接跑一次;不带参数时用交互卡片问你要什么。它与工具同生共死(工具没注册 就没有这条命令),command: false 可以单独关掉。见 registerToolCommand。

目录按 内置 → agent source → 本机用户 的顺序叠加,后面的层想覆盖前面的同名路由, 要在那条路由上显式写 "replace": true。

还有第四层 project(<cwd>/.pi/media/catalog/,经 projectDir 传):默认不存在, 而且只能新增、不能覆盖——那一层的文件来自使用者 cd 进去的项目,可能是别人写的。 开它的前提是那个项目受信,细节见 catalog/README.md。

二、加方言(一小段 TS)

内置方言不覆盖你的上游线格式时才用得上:

makeMediaExtension(host, {
  catalogDirs: [".../media/catalog"],
  dialects: [myRelayDialect], // 满足 Dialect 契约
});

和内置方言重名的注入会被整个拒绝并告警 —— 同一份目录在不同 agent 下行为不同 是排查噩梦。给自己的方言起个带前缀的名字,比如 myagent-relay。

三、加工具(复用编排器,不 fork)

要加 video_generation 这类新工具时,不要复制编排器 —— 把它的声明交给装配入口就行:

makeMediaExtension(host, {
  extraTools: [
    {
      toolName: "video_generation",
      // 从活跃路由里自己筛(也可以直接给一个数组)
      routes: (pool) => pool.filter((route) => route.provider === "myvendor"),
      baseDescription: "按文字描述生成一段视频。",
      requiredParams: [{ name: "prompt", kind: "input", title: "想要一段什么样的视频?" }],
      rememberParams: ["model"],
    },
  ],
});

偏好回填、必选项交互补全、中断处理、落盘 —— 整条流水线对新工具原样生效。 内置的三个工具自己就是同一个注册器的三次调用(见 src/tools/image-generation.ts,整个文件就是那一次调用), 不存在"内置工具专用"的私有路径。

自己接线装配(不走 makeMediaExtension)时,直接用底下那个注册器:

registerMediaTool(pi, host, {
  toolName: "video_generation",
  routes,             // 已按用户设置过滤的活跃路由
  baseDescription,
  requiredParams,
  dialects,           // 方言表
  env: process.env,   // ${VAR} 到执行期才展开
});

★ 活跃路由为零的工具不会被注册(不做"全禁时悄悄保留默认模型"那一套), 同时清单里会带上原因:是目录里本来就没有,还是被用户禁光了 —— 两种情况给用户的 下一步完全不同。

★★ 自己接线时,扩展工厂要从 defineExtension 过一道:

import { defineExtension } from "@pi-web/extension-kit";

export default defineExtension((pi) => {
  registerMediaTool(pi, host, { /* … */ });
  publishMediaCatalogState(pi.state, catalog);   // 不过那一道的话,这里恒为 undefined
});

自己 export default (pi) => … 也能被 pi 装载,但 pi 交给工厂的那个 api 对象上 没有 state(它是 pi 的私有对象字面量,宿主插不进去),装配期的 pi.state 是 defineExtension 在调用工厂那一刻补上的。少了这一层,清单一次都下发不出去, 而且不报任何错 —— 工具都在、日志全正常,只有设置页永远空着。 makeMediaExtension 已经在里面包好了,走它就不用管这件事。

两个入口

| 入口 | 里面有什么 | 谁引它 | |---|---|---| | @pi-web/media-kit | 纯类型、zod schema、目录加载器 | 设置页、测试、装配代码 | | @pi-web/media-kit/runtime | 方言实现、执行引擎、编排器、工具注册、makeMediaExtension | 只有 agent 进程 |

运行层是声明层的超集,agent 侧写一个 import 就够。

没有宿主也能先跑起来:createDefaultHost() 给一份本机实现(附件落本地目录、 预览空实现、日志走 stderr),写例子和跑集成测试不必先实现三个接口。

attachmentDir 收两种形状:给一个字符串 = 所有产物落同一个目录;给一份 AttachmentDirs = 按媒体大类分(image/* / video/* / audio/* 各一处, 其余落必填的 fallback)。分类看的是产物的 mimeType,不是哪个工具出的它 —— 一张图无论来自 image_generation 还是将来某个 video_thumbnail,都落同一处。

createDefaultHost({
  attachmentDir: { image: "./images", video: "./videos", fallback: "./media" },
});

★ read(ref) 是靠扫目录把引用兑回文件的,分目录之后它会在配置里出现过的每个目录 里找(image_edit 拿历史 id 找文件那条路因此不受布局影响)。这也是这一层今天就存在、 而不是等视频工具落地再加的理由:现在改是零迁移,将来改要处理"旧产物在老目录"。

会话共享状态(下发模型清单、记住用户选过的模型和尺寸)不在宿主能力里: 它是公共面,写作面在 @pi-web/agent-kit 的 SessionState,入口是扩展装配期的 pi.state 与执行期的 ctx.state,唯一实现在 packages/runner。本包只负责载荷 —— 放在哪个键、什么形状(见 src/state.ts)。

装配期那个 pi.state 要宿主先把状态交出去(pi-web 里是 packages/runner 启动时那一步), 再由 defineExtension 补到工厂手上。换成不做这件事的裸 pi,pi.state 运行期就是缺的 —— 这是预期内的降级:工具照常注册,只是清单不下发、偏好记不住,装配会留一句告警说明。

目录结构

catalog/          数据面:全部是 JSON
  schema/         生成物,入库;CI 校验"重新生成后零 diff"
  builtin/        内置目录,一 provider 一目录
  parked/         验证过但主动摘下来的路由(比如吃免费额度的),loader 不扫这里
  tools.json      工具级 UI 数据(尺寸档位、必选项补全声明、偏好键白名单)
src/              代码面
  index.ts        声明层入口:类型 + zod schema + 目录加载器
  runtime.ts      运行层入口:方言、引擎、编排器、工具注册、装配(只在 agent 进程加载)
  types.ts        执行层与目录共享的纯类型(零运行时导入)
  catalog/        目录 JSON 的 zod schema
  dialect/        方言契约
  host.ts         宿主能力注入面 + 会话共享状态的载荷形状
  host-default.ts 宿主能力的默认本机实现(脱离宿主也能跑通)
  engine/         与目录内容无关的执行内核
  dialects/       一方言一目录:实现、旋钮、真机响应样本、测试同居
  loader/         三层收集 → 校验 → env 门控 → 合并去重 → 编译
  orchestrator/   主流水线
  tools/          通用注册器 + 两个内置工具(它们自己就是那个注册器的两次调用)
  extension.ts    装配入口:设置 → 方言 → 目录 → 注册工具 → 下发清单
scripts/
  gen-schema.mjs  遍历方言的 options.ts 生成 catalog/schema/

几条必须守住的规矩

  • JSON 里不长表达式语言。 想在目录里写"取哪个字段""条件拼接"的时候停下, 把它写成方言代码。JSON 里只允许两种模板:${VAR} / ${VAR:-默认值}(执行期展开) 和 {baseUrl}(加载期展开)。
  • 方言按线格式划分,不按厂商。 NewAPI、sufy、自建网关说的都是 OpenAI Images 那套话,就该共用一个方言,差异全在 JSON 里。方言数量要收在个位数。
  • 加载期不取凭据的值。 env 门控只查变量存不存在,${VAR} 一路保留到执行期。 所以编译后的目录可以放心打日志、dump 给设置页,不会带出密钥。
  • 坏文件跳过,不炸目录。 目录坏一角不该让整个工具用不了;但告警必须说清是哪个 文件的哪个字段、期望是什么。
  • schema 生成物入库。 改了方言的旋钮忘了重新生成,git diff catalog/schema/ 会在 CI 里报出来。