Windmill 支持 TypeScript 脚本,默认 Bun 运行时。本文记录一次从本地开发到部署的完整流程,重点说明 lock 文件、参数传参和 MinIO 客户端三个容易踩坑的地方。

目录结构 链接到标题

Windmill 的 folder sync 要求每个脚本放在 f/ 目录下,由三个文件组成:

f/my_module/my_script.ts          # 源码
f/my_module/my_script.script.yaml  # 元数据和参数 schema
f/my_module/my_script.script.lock  # 依赖锁文件

wmill.yaml 中通过 includes 控制同步范围,defaultTs: bun 指定 TypeScript 运行时。

Lock 文件:不要手动写 链接到标题

这是最容易卡住的一步。TypeScript 脚本的 .script.lock 文件禁止手动编辑,必须通过 Windmill CLI 生成:

wmill generate-metadata --lock-only

该命令会:

  1. 读取 .ts 文件中的 import 语句
  2. 在服务端运行依赖解析 job
  3. 生成符合 Bun 规范的 lock 文件

生成后的 .script.lock 结构大致如下:

{
  "dependencies": {
    "minio": "latest",
    "windmill-client": "1.752.0"
  }
}
//bun.lock
{
  "lockfileVersion": 1,
  ...
}

前半部分是 package.json//bun.lock 是分隔标记,后半部分是 Bun lockfile 内容。两部分合在一起才是合法的文件。

运行时注意点 链接到标题

1. Bun 的 import 语法 链接到标题

Bun 运行时直接支持 npm 包,不需要 npm: 前缀:

// ✅ 正确
import { Client as MinioClient } from "minio";

// ❌ 错误(Deno 语法)
import { Client as MinioClient } from "npm:minio";

npm: 前缀是 Deno 的导入方式,在 Bun 下会报 404。

2. getResource 是 async 函数 链接到标题

Windmill 的 getResource 返回 Promise,必须 await

const s3 = await getResource("f/minio-s3/config");

缺少 await 会导致 s3undefined,后续访问 s3.endpoint_url 时抛出 TypeError

3. 参数名必须匹配 schema 链接到标题

.script.yaml 中定义的参数名(snake_case)必须与 main 函数签名一致:

# .script.yaml
schema:
  properties:
    book_name:
      type: string
// ✅ 正确
export async function main(book_name: string) { ... }

// ❌ 错误 — Windmill 传参时找不到 bookName
export async function main(bookName: string) { ... }

Windmill 不会做 camelCase ↔ snake_case 的自动转换,参数名不一致时值为 undefined

4. MinIO endpoint 解析 链接到标题

minio npm 包的构造函数需要将 endpoint URL 拆成 endPointport 两个字段:

const endpointUrl = new URL(s3.endpoint_url);
const client = new MinioClient({
  endPoint: endpointUrl.hostname,
  port: parseInt(endpointUrl.port) || 80,
  accessKey: s3.access_key,
  secretKey: s3.secret_key,
  useSSL: endpointUrl.protocol === 'https:',
});

不能直接传 "monkey:9000" 作为 endPoint,新版本 minio 包会报 InvalidEndpointError

简化示例 链接到标题

以下是一个完整的 TS 脚本示例,从 MinIO 读取 JSON、修改、写回:

import { Client as MinioClient } from "minio";
import { getResource } from "windmill-client";

interface ContentItem {
  id: string;
  title: string;
}

export async function main(bucket: string, object_key: string) {
  const s3 = await getResource("f/minio-s3/config");
  const endpointUrl = new URL(s3.endpoint_url);
  const client = new MinioClient({
    endPoint: endpointUrl.hostname,
    port: parseInt(endpointUrl.port) || 80,
    accessKey: s3.access_key,
    secretKey: s3.secret_key,
    useSSL: endpointUrl.protocol === 'https:',
  });

  // 读取
  const stream = await client.getObject(bucket, object_key);
  const text = await stream.text();
  const data: ContentItem[] = JSON.parse(text);

  // 处理
  const updated = data.map(item => ({ ...item, processed: true }));

  // 写回
  const output = JSON.stringify(updated, null, 2);
  await client.putObject(
    bucket,
    object_key,
    Buffer.from(output, "utf-8"),
    Buffer.byteLength(output, "utf-8"),
    { "Content-Type": "application/json; charset=utf-8" }
  );

  return { status: "ok", count: updated.length };
}

部署步骤:

# 1. 生成 lock 文件
wmill generate-metadata --lock-only

# 2. 同步到 Windmill
wmill sync push

# 3. 在 UI 或通过 API 运行
curl -X POST http://<windmill-host>/api/w/default/jobs/run/p/f/my_module/my_script \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"bucket": "ebook", "object_key": "data/test.json"}'

总结 链接到标题

Windmill 上部署 TypeScript 脚本的核心流程不复杂,但有几个必须记住的点:

  1. Lock 文件由 wmill generate-metadata 生成,不要手动写
  2. Bun 的 import 没有 npm: 前缀
  3. getResource 需要 await
  4. 函数参数名与 schema 一致(snake_case)
  5. MinIO endpoint 要拆成 hostname 和 port

踩过这几个坑之后,TS 脚本在 Windmill 上的开发和部署就和写普通 Node.js 代码一样直接了。