library-picturebook-activity/lesingle-aicreate-client/demo/CreationApi.js
2026-04-03 20:55:51 +08:00

176 lines
5.8 KiB
JavaScript

/**
* 乐读派 AI 创作系统 — C 端 API 封装
*
* 调用流程:
* 1. B1 校验额度 → 2. A1/A2/A3 创作 → 3. 进度追踪 → 4. B2 获取结果
*
* 所有 API 均需 HMAC-SHA256 签名(除 B6 画风列表)
*/
import { generateSignHeaders } from './HmacSigner.js'
export default class CreationApi {
/**
* @param {string} serverUrl - 服务器地址
* @param {string} appKey - 机构 App Key (orgId)
* @param {string} appSecret - 机构密钥
*/
constructor(serverUrl, appKey, appSecret) {
this.serverUrl = serverUrl
this.appKey = appKey
this.appSecret = appSecret
}
// ─── 通用请求方法 ─────────────────────────────────────────────────
async _get(path, queryParams = {}) {
const signHeaders = generateSignHeaders(this.appKey, this.appSecret, queryParams)
const qs = Object.entries(queryParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
const url = `${this.serverUrl}${path}${qs ? '?' + qs : ''}`
const res = await fetch(url, {
method: 'GET',
headers: { ...signHeaders, 'Content-Type': 'application/json' }
})
return this._handleResponse(res)
}
async _post(path, body, queryParams = {}) {
// POST 的 JSON body 不参与签名,只有 query params 参与
const signHeaders = generateSignHeaders(this.appKey, this.appSecret, queryParams)
const qs = Object.entries(queryParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
const url = `${this.serverUrl}${path}${qs ? '?' + qs : ''}`
const res = await fetch(url, {
method: 'POST',
headers: { ...signHeaders, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
return this._handleResponse(res)
}
async _handleResponse(res) {
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
const json = await res.json()
if (json.code !== 200) {
const err = new Error(json.msg || 'API Error')
err.code = json.code
throw err
}
return json.data
}
// ─── B 类接口(查询) ─────────────────────────────────────────────
/**
* B1 - 创作请求校验(检查额度是否充足)
* @param {string} orgId
* @param {string} phone
* @returns {Promise<{valid: boolean, reason?: string, remainQuota?: number}>}
*/
async validate(orgId, phone) {
return this._post('/api/v1/query/validate', { orgId, phone })
}
/**
* B2 - 查询作品详情(含实时进度)
* @param {string} workId
* @returns {Promise<CreationResultVO>}
*
* CreationResultVO 结构:
* {
* workId, status, title, author, pages, style,
* videoStatus, videoUrl, failReason, durationMs, retryCount,
* progress, // 0-100 进度百分比,-1 表示失败
* progressMessage, // "正在绘制第3/7页..."
* pageList: [{pageNum, text, imageUrl, audioUrl}] // 仅 COMPLETED 时有值
* }
*/
async getWork(workId) {
return this._get(`/api/v1/query/work/${workId}`, { orgId: this.appKey })
}
/**
* B3 - 作品列表
*/
async listWorks(phone, page = 1, pageSize = 20) {
return this._get('/api/v1/query/works', {
orgId: this.appKey, phone, page, pageSize
})
}
/**
* B6 - 画风列表(无需签名)
*/
async getStyles() {
const res = await fetch(`${this.serverUrl}/api/v1/query/styles`)
return this._handleResponse(res)
}
// ─── A 类接口(创作) ─────────────────────────────────────────────
/**
* A1 - 一句话创作
* @param {Object} params
* @param {string} params.orgId
* @param {string} params.phone
* @param {string} params.sentence - 故事描述
* @param {string} params.style - 画风标识 (如 style_cartoon)
* @param {boolean} [params.enableVoice=false] - 是否配音
* @param {string} [params.author] - 作者名
* @param {number} [params.age] - 适龄
* @returns {Promise<{workId: string, status: string}>}
*/
async createBySentence(params) {
return this._post('/api/v1/creation/one-sentence', params)
}
/**
* A2 - 四要素创作
* @param {Object} params
* @param {string} params.orgId
* @param {string} params.phone
* @param {string} params.time - 时间
* @param {string} params.place - 地点
* @param {string} params.character - 角色
* @param {string} params.event - 事件
* @param {string} params.style
* @param {boolean} [params.enableVoice=false]
* @returns {Promise<{workId: string, status: string}>}
*/
async createByElements(params) {
return this._post('/api/v1/creation/scene-elements', params)
}
/**
* A3 - 图片故事创作
* @param {Object} params
* @param {string} params.orgId
* @param {string} params.phone
* @param {string} params.imageUrl - 参考图片 URL
* @param {string} [params.storyHint] - 故事方向提示
* @param {string} params.style
* @param {boolean} [params.enableVoice=false]
* @param {string} [params.heroCharId] - 主角 charId (来自 A5/A6)
* @param {Array<{charId, imageUrl, name}>} [params.characterRefs] - 角色引用列表
* @returns {Promise<{workId: string, status: string}>}
*/
async createByImage(params) {
return this._post('/api/v1/creation/image-story', params)
}
/**
* A4 - 视频合成
* @param {Object} params
* @param {string} params.orgId
* @param {string} params.phone
* @param {string} params.workId
* @returns {Promise<{workId: string}>}
*/
async createVideo(params) {
return this._post('/api/v1/creation/video', params)
}
}