使用 Func 构建站点后端能力
面向 AI 编程智能体的 Func 完整实现规范:涵盖文件与方法、运行时、数据、鉴权、密钥、第三方服务、支付、资源、超时、原生 SSE 与 SSR 边界。
Func 是 Creght / Talizen 的项目级后端运行时,用于把必须在服务端完成的小型业务流程放进站点项目:受保护的数据写入、预约与候补、用户资料更新、第三方 API、Webhook、支付对接,以及需要密钥或流式输出的 AI 请求。
适用范围
适合 Func
预约、候补、RSVP、线索分发、资料更新、可用性检查、登录后操作、第三方 API、Webhook、支付和简单 JSON 数据读写。
优先用现成能力
普通内容展示使用 CMS;静态联系表单使用 talizen/form;登录界面与会话状态使用 talizen/auth。
不适合 Func
脱离请求继续执行的后台任务、重型文件处理、无限流、定时器轮询、自建账号/会话系统、OAuth 回调或令牌交换。
项目隔离
Func 天然属于当前项目。输入、代码和分支逻辑中都不应出现 project_id、site_id 或内部表 ID。
文件、键与方法
Func 文件位于 /backend/func。文件的无扩展名路径就是函数键:/backend/func/booking.ts 对应 booking,/backend/func/profile/settings.ts 对应 profile/settings。
点号保留给导出方法。单一操作可导出 main;相关操作应从同一文件直接导出多个方法,不要自行编写分发器。
// /backend/func/booking.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'
export function create(input, ctx: TalizenFuncContext) {
if (!input?.startAt) throw new Error('startAt is required')
const user = ctx.auth.requireUser()
return ctx.db.insert('appointments', {
startAt: input.startAt,
userId: user.id,
})
}
export function availability(input, ctx: TalizenFuncContext) {
return ctx.db.query('appointments', { date: input.date })
}
invoke('booking.create', input) // key booking, method create
invoke('profile/settings.update', input)
invoke('booking', input) // key booking, method main
运行时与代码规则
- 使用 ESM 导出:
export function method(input, ctx);只在需要时从talizen/func-runtime导入 TypeScript 类型。 - 所有平台能力都从
ctx获取;不要使用旧式全局变量data、db、auth或cache。 - 在 Func 内验证、裁剪和规范化全部输入。预期业务状态返回结构化 JSON;无效请求或意外故障才抛出错误。
- Func 不是完整 Node.js 运行时。不要依赖 Node 内置模块;计时器
setTimeout/setInterval不受支持,不能用于等待、轮询或重试。 - 标准
fetch、Response、TextDecoder与 Web Crypto 可用于第三方 HTTP、响应读取和签名校验。
ctx 能力速查
数据
ctx.db.get/query/insert/update/delete 操作项目 JSON 表;所有写入受表的 JSON Schema 校验。
鉴权
ctx.auth.currentUser() 读取当前用户;ctx.auth.requireUser() 在未登录时直接拒绝。
缓存
ctx.cache.get/set/del/incr/expire 适合短期结果、计数器与过期状态,不能替代持久化表。
请求与响应
ctx.request.host/ip/method/path 提供请求信息;原始请求体可按 Fetch 语义读取。使用 ctx.response.status(code) 设置状态码。
Cookie
通过 ctx.cookies 读取、设置或删除 Cookie。SSE 第一个事件发送后响应头已提交,不能再修改 Cookie。
资源
ctx.assets.upload({ filename, mimeType, base64 }) 上传运行时文件,并返回可持久化的 URL、路径和大小元数据。
流式事件
ctx.sse.send(event, data) 发送有界 SSE 事件;平台负责最终 done 或 error 事件。
诊断
使用 console.log/warn/error 输出日志,并使用 ctx.trace_id 关联一次调用,避免记录密钥与敏感正文。
返回值与错误
Func HTTP 层返回 { "result": ... } 或 { "error": "..." }。浏览器里的 invoke() 会解包成功的 result,失败时抛出 TalizenFuncError。业务上可预期的“无库存”“重复提交”等状态建议作为明确对象返回。
import { invoke, TalizenFuncError } from 'talizen/func'
try {
const booking = await invoke('booking.create', input)
} catch (error) {
const message = error instanceof TalizenFuncError
? error.message
: '提交失败,请稍后重试'
}
JSON 表与持久化
写入前必须存在表定义。表文件位于 /platform/table/<key>.json,文件名就是表键;使用普通文件工具创建或修改,不存在单独的表结构工具。
{
"name": "Appointments",
"desc": "Booked appointment slots",
"json_schema": {
"type": "object",
"properties": {
"startAt": { "type": "string" },
"userId": { "type": "string" },
"status": { "type": "string" }
},
"required": ["startAt", "userId"]
}
}
Schema 必须是具有非空 properties 的对象,required 只能引用已声明字段,业务字段不能叫 key。写入仅允许声明过的字段。表不可直接改名;仍有记录时不可删除。记录管理使用 list_table_records、create_table_record 等记录工具。
用户数据使用平台
user.id作为归属键,不要以邮箱充当身份主键,也不要创建users、auth_users等账号身份表。
鉴权、密钥与第三方服务
页面的登录 UI 使用 useAuth();Func 只负责保护后端动作。密钥通过 process.env.NAME 读取,由用户在 Creght「Backend / Env」面板 panel/backend/env 配置。智能体不得声称已代管环境变量,也不得把密钥写入配置、代码、组件、示例、注释或生成结果。
export async function main(input, ctx) {
ctx.auth.requireUser()
const response = await fetch('https://api.example.com/v1/generate', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.EXAMPLE_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok) {
ctx.response.status(502)
throw new Error('Upstream request failed')
}
return response.json()
}
接收 Webhook 时,按供应商规范读取原始正文并使用 Web Crypto 校验签名,再解析 JSON;不要先重写正文。支付也属于自定义服务端 Func:密钥放环境变量,验证回调签名与幂等键,返回明确订单状态。平台没有内置支付 SDK。
资源上传
const asset = await ctx.assets.upload({
filename: 'report.pdf',
mimeType: 'application/pdf',
base64: input.base64,
})
await ctx.db.insert('reports', {
userId: ctx.auth.requireUser().id,
url: asset.url,
path: asset.path,
size: asset.size,
})
大文件应上传后只保存 URL、路径和大小等元数据,不要把大段 base64 存入表或作为 Func JSON 结果返回。
超时配置与诊断
invoke 默认超时是 5 秒,Func runner 的默认最大执行时间是 300 秒。有界的模型生成或第三方请求可以在调用端设置更长时间;这不意味着可以启动脱离请求继续执行的后台任务。
const result = await invoke('image.generate', input, {
timeoutMs: 120000,
})
遇到 context deadline exceeded 或 context timeout 时:
- 先检查页面实际传给
invoke(..., { timeoutMs })的值。 - 用相同输入、相同模型提高超时重试。
run_func.timeout_ms只影响这次自测,不能证明平台存在同样的硬限制。 - 如果提高后成功,就提高生产调用端的
timeoutMs并验证真实路径。
不要把缩短输出、降低
max_tokens、切换模型/供应商、创建新表或伪造后台任务当成超时修复。
原生 Fetch + SSE 流式响应
普通 invoke() 等待一个完整 JSON 结果。需要有界增量输出时,服务端通过 ctx.sse.send 发事件,浏览器直接使用原生 fetch 和 ReadableStream;不需要 invokeStream 封装。
// /backend/func/writer.ts
export async function main(input, ctx) {
ctx.sse.send('token', { text: 'Hello' })
ctx.sse.send('token', { text: ' world' })
return { ok: true }
}
const response = await fetch('/func/writer?stream=1&timeout_ms=120000', {
method: 'POST',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok || !response.body) throw new Error('Func stream failed')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split(/\r?\n\r?\n/)
buffer = frames.pop() || ''
for (const frame of frames) {
const event = frame.match(/^event:\s*(.+)$/m)?.[1]
const data = frame.match(/^data:\s*(.+)$/m)?.[1]
if (event === 'token' && data) {
output += JSON.parse(data).text
}
}
}
reader.read() 返回的是任意字节块,不保证一个块就是一个事件,必须跨读取缓存,并且只按空行分隔 SSE frame。平台最终发送 done 或 error;调用超时仍然生效。第一个事件发出后不能再更改 Cookie。
浏览器调用与 SSR 边界
写入操作应从浏览器事件处理器调用 Func,持久化状态留在 Func/JSON 表,而不是只放 React state。公开 HTTP 路径使用 /func/<key>;不要在页面路由中占用 /func/*。
不要在 getServerSideProps 中调用 Func。SSR 只提供适合首屏公共数据的请求/Cookie 辅助能力,刻意不暴露 ctx.auth、ctx.func、ctx.db 或 ctx.cache。鉴权、私有数据、写入和缓存/数据库逻辑应留在 Func 与浏览器交互流程。
开发与验收流程
- 确认 CMS 或
talizen/form不能满足需求。 - 创建或核对
/platform/table下需要的 JSON 表。 - 在
/backend/func编写 ESM 导出,验证输入并从process.env读取密钥。 - 页面使用
invoke('key.method', input);仅流式场景使用原生 Fetch/SSE。 - 使用
run_func或 Creght CLI 的 Func 运行命令传入样例做后端自测;记住测试超时只作用于本次运行。 - 修改页面或组件后运行 lint,并在真实页面验证成功、业务失败、未登录、第三方失败、超时和流式结束路径。
- 没有项目/站点/内部表 ID 进入浏览器 payload。
- 没有硬编码密钥、旧式全局变量、手动方法分发或计时器。
- 表已存在,Schema 与实际写入字段一致。
- 受保护操作调用
requireUser(),记录使用user.id。 - 大文件走资源上传,第三方/Webhook/支付校验错误与签名。
- 普通调用返回结构化 JSON;SSE 正确缓冲 frame,并处理
done/error。 - 调用端超时与实际任务时长匹配,生产路径已经验证。
