Skip to content

ai-server

服务端 AI 客户端(Node 22+)—— 能力注册表(DeepSeek / DashScope)+ OpenAI SDK 文本客户端 + DashScope 原生图片编辑。错误为框架无关的 AiClientError,由消费方在边界映射。

bash
pnpm add @flowporr/ai-server

环境变量

变量用途缺省
DASHSCOPE_API_KEY阿里百炼 API Key空 = 图片/视觉/向量不可用
DASHSCOPE_BASE_URLMaaS 业务空间端点默认 MaaS 端点
DEEPSEEK_API_KEYDeepSeek API Key空 = 文本生成不可用
AI_IMAGE_MODEL图片编辑默认模型qwen-image-2.0

类型

ts
/** AI 能力维度 — 调用方按能力请求模型 */
type AiCapability = 'text-generation' | 'vision-analysis' | 'image-generation' | 'embedding'

interface AiProviderConfig {
  provider: string    // 可读标签(日志用)
  baseURL: string     // OpenAI-compatible 端点
  model: string       // 模型名
  apiKey: string      // API Key(空 = 未配置)
}

能力注册表

所有 AI 调用通过能力维度获取模型,禁止在业务代码硬编码模型名 / 端点:

能力默认模型可选模型
text-generationdeepseek-v4-prodeepseek-v4-pro, qwen3-vl-plus
vision-analysisqwen3-vl-plusqwen3-vl-plus
image-generationqwen-image-2.0qwen-image-2.0, wan2.7-Image
embeddingqwen3.7-text-embeddingqwen3.7-text-embedding
ts
import { getAiConfig, createAiClient, getAvailableModels } from '@flowporr/ai-server'

const cfg = getAiConfig('text-generation')              // → deepseek-v4-pro
const client = createAiClient('text-generation')        // OpenAI SDK(兼容端点)
const models = getAvailableModels('image-generation')   // ['qwen-image-2.0', 'wan2.7-Image']

新增模型:在 ai-server 的能力注册表加一条记录 + 更新 CAPABILITY_MODELS,业务代码零改动。

图片编辑(DashScope 原生 API)

ts
import { getImageAiConfig, callAiEdit, downloadToBase64 } from '@flowporr/ai-server'

const config = getImageAiConfig()                 // 默认 AI_IMAGE_MODEL || qwen-image-2.0
const imageUrl = await callAiEdit(base64Image, prompt, config)
const resultBase64 = await downloadToBase64(imageUrl)
  • qwen-image-2.0:同步调用(multimodal-generation 端点,延迟低,默认)
  • wan2.7-Image:异步任务(image-generation 端点)→ pollTask 轮询,默认 2s × 45 = 90s 超时
  • 端点通过 getDashScopeNativeBaseUrl 从兼容端点派生原生端点(/compatible-mode/v1/api/v1

错误处理

AiClientErrorstatus / code,不绑定任何框架:

codestatus含义
UNKNOWN_MODEL500未知模型名
NOT_CONFIGURED503对应 API Key 未配置
UPSTREAM_ERROR502上游 AI 请求失败
INVALID_RESPONSE502AI 返回格式异常
TIMEOUT504异步任务轮询超时
DOWNLOAD_FAILED502下载 AI 结果图片失败
ts
import { AiClientError } from '@flowporr/ai-server'

try {
  getImageAiConfig()
} catch (e) {
  if (e instanceof AiClientError) {
    // e.status / e.code / e.message
  }
}

消费方映射:Fastify 侧转 AppError(503→SERVICE_UNAVAILABLE、504→TIMEOUT、其余→AI_SERVICE_ERROR),h3 侧转 createError,状态码原样保留。

导出 API

  • 配置:getAiConfig(capability, model?) / getImageAiConfig(model?) / isAiAvailable(capability?) / getAvailableModels(capability)
  • 客户端:createAiClient(capability, model?)
  • 端点:getDashScopeNativeBaseUrl(config)(别名 getNativeBaseUrl
  • 图片编辑:callAiEdit(imageBase64, prompt, config) / editWithQwenImage / editWithWanImage / pollTask(taskId, nativeBase, apiKey, maxRetries=45) / downloadToBase64(imageUrl)
  • 错误:AiClientError / AiErrors(便捷构造)

消费方接入示例

Fastify(one-tools-box 的兼容层模式):

ts
// apps/server/src/ai/config.ts — AiClientError → AppError 转换
function toAppError(err: unknown): AppError {
  if (err instanceof AppError) return err
  if (err?.name === 'AiClientError') {
    const e = err as { status: number; message: string }
    const code = e.status === 503 ? ErrorCode.SERVICE_UNAVAILABLE
      : e.status === 504 ? ErrorCode.TIMEOUT
      : ErrorCode.AI_SERVICE_ERROR
    return new AppError(code, e.message, e.status)
  }
  return new AppError(ErrorCode.AI_SERVICE_ERROR, (err as Error)?.message || 'AI 服务异常', 502)
}

h3 / Nitro(flowporr 的模式):

ts
try {
  const config = getImageAiConfig()
  const imageUrl = await callAiEdit(image, prompt, config)
  const resultBase64 = await downloadToBase64(imageUrl)
  return { success: true, image: resultBase64 }
} catch (err) {
  if (err instanceof AiClientError) {
    throw createError({ statusCode: err.status, message: err.message })
  }
  throw err
}