🌐 中文 · English
一句话描述你的需求 → 得到一个真实、可逐行审计的飞书多维表格插件(TypeScript),可直接上传到飞书。
🔗 在线介绍页:https://scott987-cmd.github.io/feishu-plugin-platform/
一个面向使用飞书的企业(信创 / 政企 / 国企等)的自然语言 → basekit 插件生成器:它们拥有多维表格和插件能力,但没有插件市场,也没有内部研发团队。你只需输入一句话,平台便会产出一个标准、人类可读的 basekit TypeScript 工程,走飞书正常的上传 + 审核链路。
它同时交付企业最关心的交付侧能力:一个自托管的 execute 运行时(连接器 / 字段捷径通过你自己的服务器调用外部 API——无需外部函数托管),以及一键发布 + 部署(scripts/release.sh)。详见 OPERATIONS.md。
一段真实、未剪辑的屏幕录像——输入一句话,模型即生成并校验,你得到一个可审计的 src/index.ts,外加一键工程下载。该片段还展示了切换到 自动化 (addAction) 并生成一个自动化动作。
| ① 用一句话描述需求 | ② 生成结果概览(输入 / 出网白名单 / 鉴权 / 输出列) |
|---|---|
![]() |
![]() |
| ③ 可逐行审计的源码 + 一键下载工程 | ④ 切到「自动化」生成 addAction 动作 |
![]() |
![]() |
完整图文走查见
docs/index.html(含上面的录屏与逐步截图,可作为发给客户的使用指南;用 GitHub Pages 指向/docs即可在线访问)。
企业飞书自带 basekit 插件能力,但没有公开的插件市场——没有任何地方可以"安装"一个社区字段捷径或自动化。要自己构建,就意味着雇一个懂 basekit SDK 的人。而这些组织(信创 / 政企 / 国企)大多两样都没有。
本平台填补了这个空白。一个非工程师用通俗语言陈述需求,平台便产出一个真实的 basekit 工程,其源码在上传之前就可由安全 / 合规团队逐行审阅。生成的 TypeScript 就是交付物——不是一个不透明的二进制,也不是一个你不得不信任的托管运行时。对信创 / 政企客户而言,"自己审计,再提交审核"是卖点,而非缺陷说明。
生成器只产出正确、标准、可审计的工程,运行时是飞书自有的 basekit FaaS。在容器渲染轨里,「连接器 / 字段捷径」要调外部 API 算出一列时,这段执行逻辑跑在一个你自行部署的自托管 execute 运行时上(cmd/execute-runner:一个 DSL 解释器,而非代码沙箱——无用户 JS、已做 SSRF 防护、出网集中可审计;详见 docs/EXECUTE_RUNTIME.md)。无论哪种方式,安全 / 合规团队审阅的交付物都是生成的 TypeScript,而非一个不透明的二进制或一个你不得不信任的托管运行时。
两种生成器,均面向官方 SDK @lark-opdev/block-basekit-server-api(固定到 1.0.6,CLI 1.0.5):
产出边界:平台生成字段捷径(
addField)与自动化动作(addAction)两类 server 能力,外加一个容器(数据表视图)渲染器承载只读视图与逐行 enrich。它不生成飞书的边栏插件或数据连接器。
选择输入列 → 调用一个外部 API → 写回一个或多个输出列。
- 4 种鉴权类型,由终端用户在配置时填写(绝不硬编码):
HeaderBearerToken、QueryParamToken、CustomHeaderToken、Basic GET/POST/PUT/PATCH/DELETE(读取或写入外部系统),支持扁平或嵌套 JSON 请求体(bodyJson),以及可选的自定义请求头- 多步链式调用(
steps,≤3):后一个请求可以使用前一个请求的响应——{stepId.json.path}把某步的输出流转到下一步的 URL / 请求头 / 请求体(例如获取一个 token → 用它调用一个 API;地理编码 → 天气) - 表达式映射,作用于两个命名空间:
in.<inputKey>(输入)和res.<dotted.json.path>(响应),外加+ - * / % ( )、数组下标(res.list.0.x)、rand()、字符串 / 数字函数(concat/upper/trim/substr/round/floor/…),以及函数形式的条件逻辑——eq/gt/and/if(cond,a,b)/coalesce——因此分支无需用裸的< > = ? :运算符 - 多属性 Object 结果(一次产出多个派生列),可选
NumberFormatter;列类型支持Text/Number/DateTime/Checkbox/SingleSelect/Phone/Email/Currency/Progress/Rating/Barcode/Url(可点击链接,渲染为{text,link}单元格值)/MultiSelect(一个string[]——用split(field, ','));主列按 SDK 要求为Text/Number
示例——自然语言 → 生成的字段捷径
"我有一个人民币金额字段,按实时汇率换算成美元,输出美元金额和当前汇率两列" (I have an RMB-amount field; convert it to USD at the live rate and write back two columns: the USD amount and the current rate.)
会产出这份 DSL(LLM 的结构化中间形态)……
{
"id": "exchange-rate",
"title": { "zh_CN": "汇率换算", "en_US": "Exchange Rate" },
"domains": ["api.exchangerate-api.com"],
"formItems": [
{ "key": "account", "label": { "zh_CN": "人民币金额" },
"component": "FieldSelect", "supportType": ["Number"], "required": true }
],
"result": {
"kind": "object",
"properties": [
{ "key": "id", "type": "Text", "groupByKey": true, "hidden": true, "expr": "rand()" },
{ "key": "usd", "type": "Number", "label": { "zh_CN": "美元金额" },
"primary": true, "formatter": "DIGITAL_ROUNDED_2", "expr": "in.account * res.rates.USD" },
{ "key": "rate", "type": "Number", "label": { "zh_CN": "汇率" },
"formatter": "DIGITAL_ROUNDED_4", "expr": "res.rates.USD" }
]
},
"execute": { "url": "https://api.exchangerate-api.com/v4/latest/CNY", "method": "GET" }
}……它会编译成一个可审计的 src/index.ts,调用 basekit.addDomainList([...]) + basekit.addField({...}),其中 expr 被降级(lower)为安全的可选链 JS(in.account * res?.rates?.USD)。
配置输入 → 调用一个外部 API → 返回一个供下游自动化步骤消费的结果对象。
APIKey鉴权(运行时注入),GET/POST/PUT/PATCH/DELETE+ 扁平或嵌套(bodyJson)请求体 + 自定义请求头- 同一套
expr语法(in.<inputKey>、res.<json.path>、算术、函数、if/eq/gt… 条件、rand()) - 结果是一个以你的输出键为键的普通对象,带有一个带类型的
resultType
示例——自然语言 → 生成的动作
"自动化:当记录新增时,拿城市字段查实时天气,把温度和天气描述写进结果供后续步骤使用" (Automation: on new record, take the city field, look up live weather, and return temperature + description for later steps.)
会产出一份 Action DSL(inputs / result / execute),它编译成一个可审计的 src/register.ts,带有 basekit.addAction({ formItems, execute, resultType })。
NL prompt
│
DeepSeek (default) │ forced function call — the function's JSON-schema
or Claude (opt-in) │ parameters ARE the DSL schema (same source as the
▼ validator), so the model can only emit structured DSL
┌──────────────────┐ validate → if invalid, feed the error
│ Constrained DSL │◀── back as a tool result and retry (≤ 2 rounds)
│ (JSON, the IR) │
└────────┬─────────┘
│ Go renderer (standard library only)
▼
┌──────────────────────────────┐
│ auditable basekit TS project │ src/index.ts | src/register.ts,
│ + provenance header │ package.json, tsconfig, test/, README
└────────┬─────────────────────┘
│ testField / testAction → real outbound API call
▼
npm install → build → pack → upload to Feishu
- DSL 是一种中间表示,而非运行时。 它存在的唯一目的,是让 LLM 有一个结构化、可校验的目标,并让 Go 渲染器有一个稳定的输入。
NL → DSL → TypeScript → testField → pack。 - NL → DSL 默认使用 DeepSeek(OpenAI 兼容,强制函数调用;该工具的
parametersschema 由校验器所检查的同一套枚举构建而成,因此 schema 与校验器永不漂移)。当输出非法时,校验错误会作为工具结果回喂,模型重试——自动修复,≤ 2 轮。Claude 为可选项(LLM_PROVIDER=anthropic)。DeepSeek 是国内端点,所以它的客户端绕过任何代理。 - 三道编译期护栏(这正是让输出可信赖的原因):
- 出网域名白名单,静态预检 —— 每个
execute.url的 host 都必须被domains(addDomainList)覆盖。SDK 在运行时会硬性拒绝任何不在该列表内的 fetch;我们先在编译期就把它拦下。 - 表达式白名单 —— 绝不
eval,绝不执行任意代码。expr是一种极小的语法(number | 'string' | rand() | in.<key> | res.<path>,配合+ - * / % ( )以及一组白名单化的纯 JS 函数,包括比较 / 布尔 / 条件辅助函数eq/gt/and/if/coalesce)。被禁的 token(; = [ ] { } $ \" \ // ?: & | ! < >`)会被直接拒绝——所以即便是条件分支也走白名单函数,绝不走裸运算符。表达式是生成器唯一可能夹带 JS 的地方,因此它被白名单化,而非被解释执行。 - URL 占位符校验 —— URL 或 POST 请求体中的每个
{placeholder}都必须引用一个已声明的输入。
- 出网域名白名单,静态预检 —— 每个
- 存储就是飞书多维表格本身 —— 零外部数据库。 平台自身的数据(应用 / 插件定义 + 按用户的归属)存放在一个 多维表格 里,而非 Postgres/Redis。对一个企业自托管的 信创/政企 产品而言,这是特性而非妥协:少一个要部署、加固和备份的组件;持久性由飞书提供(且 Base 可被导出 / 快照以做留存);管理员可以在熟悉的表格 UI 里检视 / 审计每一条存储的定义——平台 dogfood(自食其力地使用)了它自己所售卖的能力。读取走一个短 TTL 的缓存,搭配按表作用域的查询(
GET /api/apps?tableId=),因此能扛住"读多写少"的现实(许多查看者,少数作者)。规模边界,老实说: 写是低频的管理动作(发布一个插件),受飞书单应用 QPS 限制;跨副本的读可能会有至多一个缓存 TTL 的陈旧。一个位于同一store.Store接口之后的 Postgres 后端,是为写多 / 严格容灾部署准备的可选逃生口——隔离、可插拔,且不是前置条件。
前置条件:Go 1.24+。构建生成的插件需要:Node.js + npm。自然语言生成需要:一个 DEEPSEEK_API_KEY。
go test ./...覆盖 DSL 校验器(拒绝不符 schema 的输入)、表达式白名单、URL/域名预检,以及渲染器输出。固定的 basekit 版本与 expr 降级,已在一次真实的 basekit 上传中端到端验证。
生成器会向 basekit SDK 枚举发出引用(FieldType.<KEY>、NumberFormatter.<KEY>、AuthorizationType.<KEY>、FieldComponent.<KEY>,以及一个 addAction 鉴权字面量)。一个不在列表内的值会被编译成 undefined,并在运行时悄无声息地搞坏已发布的插件,没有任何编译错误——曾经就是这样混进了一个幻影 PERCENT_ROUNDED_2 formatter。为了让这一类 bug 根本无法被发布:
scripts/refresh-sdk-enums.sh解析 SDK 的dist/index.d.ts,把权威的枚举键写入internal/shortcut/testdata/basekit_sdk_enums.json(黄金基准)。internal/shortcut/sdk_reconcile_test.go(在go test ./...下运行)断言每一个生成器白名单值都是对应 SDK 集合的子集,并顺带打印覆盖缺口(尚未支持的 SDK 值)。- CI 双向兜底:若某个白名单偏离了黄金基准,测试就失败;
sdk-drift任务会从固定的 SDK 重新提取,若黄金基准自身陈旧则失败(.github/workflows/ci.yml)。
升级 SDK 之后:make sdk-enums,审阅 diff,测试会指出任何需要更新的白名单。
# JSON DSL → scaffolded basekit project (no LLM)
go run ./cmd/shortcutgen -out /tmp/exchange-rate \
internal/shortcut/testdata/exchange_rate.json
# Natural language → field shortcut (needs DEEPSEEK_API_KEY)
DEEPSEEK_API_KEY=sk-... go run ./cmd/shortcutgen \
-nl "把人民币金额按实时汇率换算成美元,输出美元金额和汇率" \
-out /tmp/exchange-rate -dump
# Natural language → automation action
DEEPSEEK_API_KEY=sk-... go run ./cmd/shortcutgen -action \
-nl "拿城市字段查实时天气,返回温度和天气描述供后续步骤使用" \
-out /tmp/weather-action标志:-out(必填)脚手架目录 · -nl 自然语言请求 · -action 将输入当作一个自动化 Action 处理 · -dump 同时把生成的 DSL JSON 打印到 stderr。
两个服务:一个 BFF 网关(cmd/api)和 NL→DSL 生成器(cmd/generator,持有 LLM 密钥)。
# Terminal A — generator (holds the LLM key)
DEEPSEEK_API_KEY=sk-... PORT=8090 go run ./cmd/generator
# Terminal B — BFF + static web platform
PORT=8080 GENERATOR_URL=http://localhost:8090 WEB_DIR=./web go run ./cmd/api打开 http://localhost:8080/shortcut.html —— 在 字段捷径 / 自动化 之间切换,输入一个请求,点击 一键生成,审阅可审计的源码,并下载工程 .zip(或只下载 DSL .json)。字段捷径还可在 UI 内试运行:用样例输入把生成的 DSL POST 到内联 /api/execute,上传送审前先真跑一遍。
每个人都可以用自己的飞书身份登录,这样他们创建的插件就归属于、并由他们所有(源码 + dsl.json 中会渲染一行创建者信息,且每个用户在"我的插件"下只看到自己的插件)。在平台上配置飞书 OAuth 即可启用:
FEISHU_APP_ID=cli_xxx FEISHU_APP_SECRET=xxx \
FEISHU_BASE_DOMAIN=feishu.cn \
OAUTH_REDIRECT_URI=https://your-host/auth/callback \
SESSION_SECRET="$(openssl rand -hex 32)" \
PORT=8080 GENERATOR_URL=http://localhost:8090 WEB_DIR=./web go run ./cmd/api把 OAUTH_REDIRECT_URI 注册到飞书应用的重定向 URL 白名单里。当未设置时,登录被禁用,平台保持匿名(行为不变)。路由:GET /auth/login、GET /auth/callback、POST /auth/logout、GET /api/me,以及需 cookie 鉴权的 GET/POST /api/my/plugins + DELETE /api/my/plugins/{id}。身份使用一个无状态、HMAC 签名的会话 cookie。
归属持久化:默认情况下按用户的插件存储在进程内(重启即丢)。要让归属在重启后持久化,把它指向一个飞书多维表格的表——加上 FEISHU_BITABLE_APP_TOKEN(平台的 Base)+ FEISHU_PLUGINS_TABLE_ID(一个带文本字段 id、owner_open_id、owner_name、title、kind、dsl、created_at 的表)。每个插件是一条记录;用户永远只看到自己的(按属主作用域读取)。
要用 Claude 代替 DeepSeek:
LLM_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-ant-... MODEL=claude-opus-4-8 \
PORT=8090 go run ./cmd/generator在任意脚手架 / 下载下来的工程内:
npm install
npm run build # type-check against the real basekit SDK types
npm run pack # block-basekit-cli pack:field → output/*.zip然后在你的飞书开发者控制台(字段捷径能力)上传这个 zip,并由管理员审批通过。(自动化动作通过 testAction 验证,并用 block-basekit-cli upload 上传;该 CLI 没有 pack:action。)
生成的源码是为了经受合规团队的敌意审阅而设计的。
- 无
eval,无任意代码。 取值来自一套白名单化的表达式语法,被降级为安全 JS,并对(不可信的)响应使用可选链。LLM、DSL 或一个恶意提示,都没有任何路径能把可执行代码注入到渲染出的execute()里。 - 出网域名白名单。 每个外部 host 都在
domains中声明,并被发出为单个addDomainList([...]);在渲染任何 TypeScript 之前,URL 的 host 就已被静态地对照它检查。basekit 运行时强制执行同一份列表——没有第二条、隐藏的出网通道。 - 凭据绝不硬编码。 API 密钥 / token 被声明为
auth,由终端用户在配置时输入、并由飞书运行时注入;它们绝不出现在生成的 URL 或源码里。 - 可审计 + 溯源。 每个文件都带有一行 "Generated by feishu-plugin-platform … Human-auditable." 的头注。输出是朴素、可读的 TypeScript——diff 它,读它,然后提交审核。
- 能力分离鉴权(B1)。 客户端 bundle 只内嵌只读
PLATFORM_READ_TOKEN(GET /api/apps*+POST /api/execute);写/管理操作(POST/DELETE /api/apps、/api/generate)要求仅存于服务端的 adminPLATFORM_API_TOKEN;/api/me与/api/my/*走会话。即便客户端 token 泄露也只能读(IDOR 被消除),path.Clean防//api绕过。(internal/api/server.go、cmd/api/main.go) - 审计 + 出网账本。
GET /api/audit(仅 admin,最新优先)暴露一条 append-only 审计流,落库到一张专用audit_log多维表格(FEISHU_AUDIT_TABLE_ID,留空=仅 stdout;落库失败绝不让请求失败)。每一次出网调用都会在同一张表里记一条action=execute.egress(SSRF/重定向拦截记为outcome=error),记录点在execrt.fetch收口处,热路径安全(stdout 恒发 + 异步缓冲,关机时优雅 drain)。(internal/store/audit_bitable.go、internal/execrt/engine.go)
.
├── cmd/
│ ├── api/ BFF / gateway: app CRUD, NL-generation proxy, /api/execute forward, auth, audit ledger
│ ├── generator/ NL → DSL service (holds the LLM key); /shortcut/* and /action/* endpoints
│ ├── execute-runner/ self-hosted execute runtime for the container/connector track (auditable; no external function hosting)
│ ├── shortcutgen/ CLI: -nl (NL) / -action (automation) / -out (scaffold)
│ └── bitable-bootstrap/ one-shot helper to create the backing Bitable via app credentials
├── internal/
│ ├── shortcut/ field-shortcut + action DSL: validation, expr allowlist, render, scaffold, zip
│ ├── execrt/ DSL interpreter behind execute-runner (no user JS; SSRF-guarded)
│ ├── generator/ LLM integration: DeepSeek (default) + Claude (opt-in), forced tool call + auto-repair
│ ├── dsl/ AppDefinition DSL for the container renderer
│ ├── store/ definitions + per-user plugins (Bitable-backed, read-cached, table-scoped)
│ ├── auth/ Feishu OAuth + signed session
│ └── api/ · httpx/ BFF handlers + HTTP server helpers
├── plugin/block/ the in-Bitable container widget (opdev) — renders an AppDefinition / enrich DSL live
├── plugin-center/ catalog of example generated plugins (one directory each)
├── web/ shortcut.html (NL authoring UI) + index.html (mock renderer; dev only)
├── publisher/ opdev / console publishing automation (RPA)
├── deploy/ docker compose (prod) + k8s manifests + Caddy
├── scripts/ release / deploy / publish-plugin / manage-plugins / refresh-sdk-enums / backup-defs
└── docs/ index.html (landing) + PRODUCTION · OPERATIONS · EXECUTE_RUNTIME · ROADMAP · design
当前两条并行的线,均已上线:(1)生成器——NL → 可审计的 basekit 工程(
internal/shortcut+cmd/shortcutgen+web/shortcut.html),走飞书正常的审核链路上传;以及(2)容器渲染器——plugin/block(opdev SDK)渲染一份从平台 API 拉取的AppDefinition/enrichDSL,直接在一个多维表格内部渲染,因此一个小团队作者发布的是一份定义(数据),而非每次都新过一遍审核的插件。internal/dsl+internal/store+ 该容器属于这条线。一个更早的独立@lark-base-open/js-sdk渲染器(frontend/)已被plugin/block取代并移除;web/index.html仅作为开发用的 mock 渲染器保留。
- ✅
go test ./...全绿:DSL 校验器、表达式白名单、URL/域名护栏、渲染器输出。 - ✅ 生成的工程对照真实的 basekit SDK 类型完成类型检查(
block-basekit-server-api 1.0.6)。 - ✅
testField/testAction发起真实的出网调用(汇率、天气、httpbin)并正确写回。 - ✅ 已通过 Web 平台在浏览器中端到端验证。
- ✅ 全链路已证实: 一个生成的自动化动作被上传(
block-basekit-cli upload)、发布,随后在一个真实的飞书 Base 自动化中被安装、配置并启用。 - ✅ 生产部署已端到端验证(2026-06-25): 平台以单节点 docker compose + Caddy 自动 TLS(Let's Encrypt,经
<ip>.sslip.io魔法 DNS host)跑在一台 AWS EC2 上,STORE=bitable。生产 compose 是deploy/compose/docker-compose.prod.yml;仓库根的docker-compose.yml仅供本地开发(STORE=memory、CORS=*)。k8s(deploy/k8s/)是可选的横向扩展路径,非主线。部署步骤、必填 token 与ALLOWED_ORIGIN坑见deploy/compose/DEPLOY.md(GENERATOR_TOKEN/EXECUTE_RUNNER_TOKEN/PLATFORM_READ_TOKEN为 compose 必填;auth 开启时ALLOWED_ORIGIN=*会让 api 拒绝启动)。 - ✅ 人类可读的错误:
generator.Explain把 TS2554 / 域名白名单 / 主列类型 / 缺字段 / 模型+网络 / 修复耗尽等映射成朴素提示({error:hint, detail:raw}),UI 展示提示 + 可折叠原文。 - ✅ AI 出网可控:
AI_ENABLED总开关、DEEPSEEK_BASE_URL端点固定、启动时出网透明日志;容灾备份=飞书 Base 副本 +scripts/backup-defs.sh。
- 字段捷径: 更多
FieldComponent类型与单一 Object 结果之外的结果种类;更广的NumberFormatter覆盖。 - 自动化动作:
APIKey之外的鉴权(动作的授权形态与字段不同且文档不足——推迟到一次经验证的后续跟进)。 - 表达式语法: 在保持严格白名单不变量的前提下,谨慎地拓宽原子 / 运算符。
- 平台: 生成定义的持久化;从生成 → 审核 → 上传的一键路径。
(参见 docs/ROADMAP.md,了解对飞书插件生态更广的能力盘点——注意它在很大程度上勾勒的是更早的容器 / DSL 视图扩展方向。)
MIT.




