Appearance
ai-server
服务端 AI 客户端(Node 22+)—— 能力注册表(DeepSeek / DashScope)+ OpenAI SDK 文本客户端 + DashScope 原生图片编辑。错误为框架无关的 AiClientError,由消费方在边界映射。
bash
pnpm add @flowporr/ai-server环境变量
| 变量 | 用途 | 缺省 |
|---|---|---|
DASHSCOPE_API_KEY | 阿里百炼 API Key | 空 = 图片/视觉/向量不可用 |
DASHSCOPE_BASE_URL | MaaS 业务空间端点 | 默认 MaaS 端点 |
DEEPSEEK_API_KEY | DeepSeek 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-generation | deepseek-v4-pro | deepseek-v4-pro, qwen3-vl-plus |
vision-analysis | qwen3-vl-plus | qwen3-vl-plus |
image-generation | qwen-image-2.0 | qwen-image-2.0, wan2.7-Image |
embedding | qwen3.7-text-embedding | qwen3.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)
错误处理
AiClientError 带 status / code,不绑定任何框架:
| code | status | 含义 |
|---|---|---|
UNKNOWN_MODEL | 500 | 未知模型名 |
NOT_CONFIGURED | 503 | 对应 API Key 未配置 |
UPSTREAM_ERROR | 502 | 上游 AI 请求失败 |
INVALID_RESPONSE | 502 | AI 返回格式异常 |
TIMEOUT | 504 | 异步任务轮询超时 |
DOWNLOAD_FAILED | 502 | 下载 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
}