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
该命令会:
- 读取
.ts文件中的 import 语句 - 在服务端运行依赖解析 job
- 生成符合 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 会导致 s3 为 undefined,后续访问 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 拆成 endPoint 和 port 两个字段:
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 脚本的核心流程不复杂,但有几个必须记住的点:
- Lock 文件由
wmill generate-metadata生成,不要手动写 - Bun 的 import 没有
npm:前缀 getResource需要await- 函数参数名与 schema 一致(snake_case)
- MinIO endpoint 要拆成 hostname 和 port
踩过这几个坑之后,TS 脚本在 Windmill 上的开发和部署就和写普通 Node.js 代码一样直接了。