图片大模型实践:可灵(Kling)文生图前后端实现
本文讲图片模型 里「可灵文生图」这一条链路:鉴权、代理、前端如何拼 URL 、如何从异步任务结果里取出最终图片地址。语音或其它模型后续再单独开章节。
建议阅读顺序 :先看下面「快速跑通」与「架构与数据流」,需要对照实现时再打开附录里的核心摘录 或 GitHub 完整文件------不必在中间通读近千行粘贴代码。
可以先看下文本模型的文章,这篇是后续。
模型的使用,大差不差,去模型网站买额度,然后生成key,然后接口调用。
效果图

先去申请 可灵的 Key,可以的话充点小钱做实验。
一、快速跑通(三文件 + Git)
准备一个新目录,放入下面三个文件即可跑通可灵文生图(.env.local 勿提交到 Git)。
| 文件 | 作用 |
|---|---|
index-keling.html |
前端单页:拼 URL、轮询、用 img 展示结果图 |
server.js |
后端:读环境变量、签 JWT、转发 /kling/v1/... |
.env.local(自建) |
配置 ACCESS_KEY_ID、ACCESS_KEY_SECRET 等 |
克隆仓库:
bash
git clone https://github.com/frontzhm/text-model.git
cd text-model
仓库主页: github.com/frontzhm/te...
.env.local 示例(与 server.js 同目录):
env
ACCESS_KEY_ID=你的AccessKey
ACCESS_KEY_SECRET=你的SecretKey
# 可选:KLING_API_ORIGIN=https://api-beijing.klingai.com
启动:
bash
node server.js
# 另开终端,用静态服务打开页面(避免 file:// 下 ES Module 限制)
npx --yes serve .
# 浏览器访问 /index-keling.html,「代理」填 http://127.0.0.1:3000
二、为什么要有「后端」这一层?
可灵 API 与很多厂商一样,要求:
- 鉴权 :用 AccessKey + SecretKey 按固定规则生成 JWT ,放在
Authorization: Bearer <token>里; - HTTPS + 指定域名 :国内新系统常用
https://api-beijing.klingai.com(与旧域名不同,用错域容易出现401 / Auth failed); - 浏览器限制:Secret 不能进前端;也不适合在页面里实现签名逻辑。
因此加一层 BFF :本仓库的 server.js 负责读 .env.local、签发 JWT 、把 /kling/v1/... 转发到可灵域名;浏览器只访问本地 http://127.0.0.1:3000。
三、后端:server.js 里三件事
3.1 读环境变量
从项目根目录的 .env.local / .env 按行解析 KEY=value,例如:
ACCESS_KEY_ID/ACCESS_KEY_SECRET(或KLING_*别名)- 可选:
KLING_API_ORIGIN(默认https://api-beijing.klingai.com)
3.2 生成 JWT(与官方 Python jwt.encode 一致)
- Header :
alg=HS256,typ=JWT - Payload :
iss= AccessKeyId,exp= now+1800s,nbf= now−5s - Signature :对
base64url(header).base64url(payload)做 HMAC-SHA256,再 Base64URL
使用 Node 内置 crypto.createHmac ,无需 jsonwebtoken 包。
3.3 反向代理:路径「前缀剥离」+ 上游拼接
浏览器请求例如:http://127.0.0.1:3000/kling/v1/images/generations。
- 剥前缀
/kling→ 可灵 REST 路径/v1/images/generations; - 拼上游 :
KLING_API_ORIGIN + restPath + search; - 带上
Authorization: Bearer <刚签的 JWT>转发fetch,原样回写 status 与 body。
restPath 必须 /v1/ 开头且不含 ..,防止代理滥用。
四、前端:index-keling.html 在做什么?
技术栈:Vue 3(CDN ESM) 。页面不存 AK/SK ,只填代理根地址、Prompt、resolution / aspect_ratio 等。
4.1 创建任务(POST)
base = 代理根(去掉末尾 /),拼接提交地址:
text
endpoint = base + "/kling/v1/images/generations"
body 为 JSON payload (字段以官方文档为准),示例含 prompt、negative_prompt、aspect_ratio、resolution(1k 一般比 2k 更省)。
响应里取 data.task_id。
4.2 轮询(GET)------URL 拼接
text
resultUrl = endpoint + "/" + encodeURIComponent(task_id)
对 resultUrl 定时 GET,读 data.task_status :submitted / processing 继续;failed 报错;否则解析 data.task_result.images[0].url。
4.3 「图片拼接」指什么?(不是多图拼画布)
- 接口 URL :
base+ 固定路径 +/+encodeURIComponent(id); - 展示 :先把
imgUrl设为 loading 图,成功后改为结果里的 HTTPS 图片 URL ;<img :src="imgUrl">由浏览器再去拉 CDN 图。
五、一次点击「Generate」的时序
六、省钱与排错
- 分辨率 :
payload.resolution用1k通常比2k更省(以官方计费为准)。 - 401 / Auth failed :核对 北京域 、AK/SK、重启
node server.js后是否读到.env.local。 - 422 / 字段错误 :对照当前模型文档改
payload字段名。
七、仓库文件对照
| 内容 | 文件 |
|---|---|
| 前端单页 | index-keling.html |
| JWT + 代理 + DeepSeek 其它路由 | server.js |
| 环境说明 | README.md |
八、后续(语音等)
可按同一模板扩展:鉴权方式 → 是否需代理 → 前端拼 URL 还是拼流 ;语音若走流式或 WebSocket,「拼接」更多在 chunk 缓冲与解码,建议另开一篇写。
附录 A:核心代码摘录(与仓库一致)
完整可运行代码请以仓库为准;下面仅保留与可灵最相关的片段。
A.1 server.js:JWT + 代理(节选)
js
const KLING_API_ORIGIN = (
process.env.KLING_API_ORIGIN || 'https://api-beijing.klingai.com'
).trim()
const KLING_PATH_PREFIX = '/kling'
function signKlingJwt(accessKeyId, accessKeySecret) {
const now = Math.floor(Date.now() / 1000)
const header = { alg: 'HS256', typ: 'JWT' }
const payload = { iss: accessKeyId, exp: now + 1800, nbf: now - 5 }
const h = toBase64Url(JSON.stringify(header))
const p = toBase64Url(JSON.stringify(payload))
const signingInput = `${h}.${p}`
const sig = crypto
.createHmac('sha256', accessKeySecret)
.update(signingInput)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
return `${signingInput}.${sig}`
}
// createServer 内:pathname 以 /kling 开头则 await proxyKlingRequest(...)
// proxyKlingRequest:restPath = pathname 去掉 /kling;拼 targetUrl;Bearer 调用 fetch(upstream)
(toBase64Url、readRequestBody、CORS、loadDotEnv 及 DeepSeek 路由见仓库文件。)
A.2 index-keling.html:提交与轮询 URL(节选)
js
const endpoint = `${base}/kling/v1/images/generations`
const payload = {
prompt: prompt.value.trim(),
negative_prompt: negativeWords,
aspect_ratio: aspectRatio.value,
resolution: resolution.value
}
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(payload) })
const id = (await res.json()).data?.task_id
const resultUrl = `${endpoint}/${encodeURIComponent(id)}`
// 循环 fetch(resultUrl) 直到非 processing/submitted ...
(Vue template、<style>、localStorage 与错误处理见仓库完整 HTML。)
附录 B:完整源码一键打开(Raw)
便于整文件复制: