Add Temp file
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m52s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m52s
This commit is contained in:
parent
88ea709bbb
commit
a7b61c05f5
158
.impeccable.md
Normal file
158
.impeccable.md
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# ToonFlow Design Context
|
||||||
|
|
||||||
|
## Design Context
|
||||||
|
|
||||||
|
### Users
|
||||||
|
|
||||||
|
ToonFlow serves creators and production teams turning novels, scripts, and source material into short-form video or drama assets. Primary users include AI video operators, short-drama producers, novel adaptation teams, prompt engineers, and internal content teams that need to move from planning to script, storyboard, assets, video generation, review, and export inside one workspace.
|
||||||
|
|
||||||
|
Their working context is production-heavy rather than casual browsing: users compare long text, inspect generated assets, operate model/provider settings, wait for asynchronous generation, revise prompts, and coordinate many related objects across projects. They need speed, traceability, confidence, and low cognitive friction more than decorative spectacle.
|
||||||
|
|
||||||
|
Core jobs to support:
|
||||||
|
|
||||||
|
- Create and manage short-drama projects from novels or source scripts.
|
||||||
|
- Extract chapter events and turn them into story skeletons, adaptation strategies, and structured scripts.
|
||||||
|
- Use ScriptAgent and ProductionAgent workflows while keeping generated decisions visible and editable.
|
||||||
|
- Organize characters, scenes, props, storyboard panels, generated images, video clips, and timeline-like tracks.
|
||||||
|
- Configure third-party text, image, and video model vendors safely.
|
||||||
|
- Review generated output, correct errors, regenerate selectively, and preserve production continuity across sessions.
|
||||||
|
|
||||||
|
### Brand Personality
|
||||||
|
|
||||||
|
Working personality: capable, cinematic, production-grade.
|
||||||
|
|
||||||
|
ToonFlow should feel like an AI production control room for short-drama creation: efficient enough for repeated daily work, confident enough for professional teams, and cinematic enough to remind users that the end product is video storytelling. The interface voice should be direct, calm, and operational. It should reduce uncertainty during long AI tasks through visible progress, clear next actions, and reversible controls.
|
||||||
|
|
||||||
|
The product should not feel like a generic AI chat toy, a marketing landing page, or a decorative demo. It should feel like a serious creative workstation with enough visual warmth to make long production sessions comfortable.
|
||||||
|
|
||||||
|
### Aesthetic Direction
|
||||||
|
|
||||||
|
Recommended direction: cinematic utilitarian workstation.
|
||||||
|
|
||||||
|
Use a dense but breathable desktop-first layout: persistent navigation, clear work regions, compact controls, object previews, status chips, task cards, editable panels, and canvas/timeline surfaces. The current UI already suggests a minimal black-and-white TDesign/Vue tool surface with rounded panels, left rail navigation, cards, tabs, Monaco editor usage, Vue Flow/infinite-canvas interactions, and production screenshots. The redesign should keep that operational clarity but make hierarchy, spacing, states, and visual rhythm more deliberate.
|
||||||
|
|
||||||
|
Signature move: storyboard-first production surfaces. When possible, make the core object visible: a project card should hint at story/style/progress; a storyboard item should carry thumbnail, duration, track, generation state, and dependencies; Agent output should show decision provenance and next action rather than plain chat blocks. The UI should help users see the production chain from novel to script to storyboard to video.
|
||||||
|
|
||||||
|
Visual guidance:
|
||||||
|
|
||||||
|
- Use the provided AirFlow Studio references as the primary color mood: deep blue-black workspace, soft indigo/violet depth, restrained cyan-teal accents, luminous but controlled data highlights, and cool off-white typography.
|
||||||
|
- Prefer dark/cinematic workspaces for the main product shell and production views, with enough contrast and surface separation for long editing sessions. Use light surfaces only when dense text editing or document review clearly benefits from them.
|
||||||
|
- Use cool-tinted neutrals instead of pure gray, pure black, or pure white.
|
||||||
|
- Use one primary action per surface; secondary actions should be icon buttons or quiet text buttons.
|
||||||
|
- Keep information dense but organized. Avoid oversized hero sections, decorative cards, nested cards, and generic SaaS marketing layouts.
|
||||||
|
- Use real thumbnails, generated assets, story snippets, status, and production metrics as visual material. Do not rely on abstract gradient decoration.
|
||||||
|
- Use motion as feedback: generation started, progress updated, asset completed, error needs intervention. Avoid bounce/elastic motion.
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
1. Production clarity over decoration.
|
||||||
|
Every screen should answer: what project am I in, what stage is this, what changed, what is blocked, and what can I do next?
|
||||||
|
|
||||||
|
2. Preserve the creative chain.
|
||||||
|
Novel events, script decisions, storyboard rows, assets, prompts, and generated videos should feel connected. Users should be able to trace output back to its source and revise without losing context.
|
||||||
|
|
||||||
|
3. Dense, calm, repeatable work surfaces.
|
||||||
|
ToonFlow is used for long-form workflows with many objects. Prefer compact tables, split panes, drawers, tabs, inspector panels, and canvases over marketing-like sections or empty white space.
|
||||||
|
|
||||||
|
4. Human-in-control AI.
|
||||||
|
Agent actions need visible progress, explicit confirmations for destructive changes, undo or rollback where practical, and readable explanations for generated decisions. Never hide model/provider failures behind vague errors.
|
||||||
|
|
||||||
|
5. Media-first proof.
|
||||||
|
For video/image production, thumbnails, previews, track grouping, duration, generation state, and quality review are first-class UI elements. Text-only representations are fallback, not the primary experience.
|
||||||
|
|
||||||
|
6. Accessible by default.
|
||||||
|
Target WCAG AA contrast, visible focus states, keyboard-reachable controls, 44px touch targets where mobile/tablet is supported, and `prefers-reduced-motion` handling for animations.
|
||||||
|
|
||||||
|
7. Avoid AI template fingerprints.
|
||||||
|
Avoid purple-blue gradients, dark neon AI dashboards, glassmorphism everywhere, metric-card hero patterns, nested cards, generic rounded-icon feature blocks, gradient text, and monospace typography as a lazy "technical" signal.
|
||||||
|
|
||||||
|
## Product Surface Inventory
|
||||||
|
|
||||||
|
Known product areas from the current repository and bundled screenshots:
|
||||||
|
|
||||||
|
- Project dashboard: project cards, creation/edit/delete actions, project metadata.
|
||||||
|
- Script workflow: source novel, chapter events, story skeleton, adaptation strategy, generated scripts.
|
||||||
|
- Production workflow: infinite canvas / Vue Flow-style workspace, storyboard panels, derived assets, storyboard images, video tracks, preview and export flow.
|
||||||
|
- Agent chat/work panels: ScriptAgent and ProductionAgent decision, execution, supervision streams.
|
||||||
|
- Asset management: characters, scenes, props, images, audio, video clips, uploads, batch generation.
|
||||||
|
- Image editing flow: node-style image flow editing and image regeneration.
|
||||||
|
- Settings: model selection, model map prompts, vendor configuration, programmable vendor TypeScript code, skill management, memory management, login/user settings, database import/export.
|
||||||
|
- Deployment/runtime: web build embedded in `data/web`, Electron desktop shell, Express/Socket.IO backend, SQLite local data, local `data/oss` assets.
|
||||||
|
|
||||||
|
## Technical UI Context
|
||||||
|
|
||||||
|
- App shell: Electron desktop client plus web build served from `data/web`.
|
||||||
|
- Bundled frontend signals: Vue 3.5, TDesign 1.18, Vue Flow, Monaco editor, Pinia, Vue Router, Socket.IO, Axios, Day.js.
|
||||||
|
- Backend: Node/TypeScript, Express, Socket.IO, SQLite/Knex, AI SDK providers, ONNX embedding model for memory.
|
||||||
|
- The frontend source is not in this repository; this repo includes built assets. For substantial UI rewrites, use the ToonFlow web source repository or migrate/reconstruct frontend source before editing.
|
||||||
|
|
||||||
|
## UI Quality Bar
|
||||||
|
|
||||||
|
The target quality is flagship internal production software, not MVP. Screens should be fast to scan, robust in edge cases, and polished enough that a production operator can trust the tool during multi-step generation runs.
|
||||||
|
|
||||||
|
Required states for major workflows:
|
||||||
|
|
||||||
|
- Empty: explain what to create/import next, with one clear primary action.
|
||||||
|
- Loading/generating: show task name, stage, elapsed/progress where available, and cancellation/continue affordances when safe.
|
||||||
|
- Partial success: show what completed, what failed, and what can be retried.
|
||||||
|
- Error: identify provider/model/action, show actionable recovery, preserve user input.
|
||||||
|
- Success: confirm result and expose the next useful action.
|
||||||
|
|
||||||
|
## Color Direction From References
|
||||||
|
|
||||||
|
The color system should reference the two AirFlow Studio images provided by the product owner:
|
||||||
|
|
||||||
|
- Background mood: near-black with a blue/green undertone, not flat black.
|
||||||
|
- Depth color: deep navy and muted indigo for app shell, panels, charts, and canvas surroundings.
|
||||||
|
- Primary accent: cyan-teal for selected states, active labels, brand marks, and the most important progress signals.
|
||||||
|
- Secondary accent: soft violet/periwinkle for charts, AI activity, generated-state glow, and subtle atmosphere.
|
||||||
|
- Typography: cool off-white for primary text, lavender-gray for secondary text, muted blue-gray for low-emphasis labels.
|
||||||
|
- Surface style: translucent-looking dark panels are acceptable, but avoid heavy glassmorphism. Panels should remain readable, bounded, and utilitarian.
|
||||||
|
|
||||||
|
Suggested starting tokens:
|
||||||
|
|
||||||
|
- `--bg-app`: `#050813`
|
||||||
|
- `--bg-shell`: `#090B18`
|
||||||
|
- `--surface-1`: `#111525`
|
||||||
|
- `--surface-2`: `#1A1D31`
|
||||||
|
- `--surface-3`: `#24263A`
|
||||||
|
- `--border-soft`: `#31354B`
|
||||||
|
- `--text-primary`: `#F1F2FF`
|
||||||
|
- `--text-secondary`: `#B9BCD2`
|
||||||
|
- `--text-muted`: `#7E849D`
|
||||||
|
- `--accent-teal`: `#65D9CB`
|
||||||
|
- `--accent-teal-strong`: `#43C7BD`
|
||||||
|
- `--accent-violet`: `#7B6CFF`
|
||||||
|
- `--accent-lavender`: `#B9A7FF`
|
||||||
|
- `--accent-warning`: `#FFB547`
|
||||||
|
- `--accent-danger`: `#FF6363`
|
||||||
|
- `--accent-success`: `#2BD671`
|
||||||
|
|
||||||
|
Use these as direction tokens, then convert to OKLCH during implementation so hover, active, border, and disabled states can be generated perceptually rather than by arbitrary hex tweaks.
|
||||||
|
|
||||||
|
## Visual System Starting Point
|
||||||
|
|
||||||
|
This is a starting direction, not a locked brand system:
|
||||||
|
|
||||||
|
- Palette: dark cinematic blue-black shell, muted indigo panels, cyan-teal primary accent, violet/periwinkle data and AI-activity highlights, semantic status colors for success/warning/error/info.
|
||||||
|
- Typography: clear Chinese-first sans-serif stack; consider a more distinctive UI font only if it renders Chinese and Latin well without slowing the desktop app. Do not use monospace globally; reserve it for code/config editors.
|
||||||
|
- Shape: current 8-12px radii are acceptable for panels and cards. Reference images use softly rounded dark cards; keep tool buttons stable and compact.
|
||||||
|
- Spacing: 4px base scale; dense screens can use 8/12/16px rhythms, major layout gaps 24/32px.
|
||||||
|
- Motion: 100-150ms for control feedback, 200-300ms for panel state changes, 300-500ms for larger canvas/task transitions, only transform/opacity where possible.
|
||||||
|
|
||||||
|
## Anti-References
|
||||||
|
|
||||||
|
- Marketing landing page hero layouts for the main app.
|
||||||
|
- One-note dark neon "AI dashboard" styling. The reference palette is dark and cinematic, but it must stay restrained and production-focused.
|
||||||
|
- Generic purple/blue gradient branding as the dominant visual language. Violet can be used as atmosphere or chart/data accent, while cyan-teal should carry the clearer product identity.
|
||||||
|
- Card-inside-card structures for dense settings or production panels.
|
||||||
|
- Text-only Agent transcripts when structured task/progress/state UI would be clearer.
|
||||||
|
- Decorative media placeholders when real generated images or project objects are available.
|
||||||
|
|
||||||
|
## Open Questions To Confirm
|
||||||
|
|
||||||
|
- Target users: internal production team only, public creator community, or both?
|
||||||
|
- Brand tone: should AirLabs/ToonFlow feel more enterprise, creator-friendly, cinematic, or experimental?
|
||||||
|
- Theme: should the redesign stay mainly light, add full dark mode, or use hybrid light workspace with dark media review?
|
||||||
|
- References: are there products whose workflow feel you like, such as Runway, CapCut desktop, Figma, Linear, Notion, ComfyUI, or professional NLE tools?
|
||||||
|
- Constraints: are there existing brand colors, logo assets, or AirLabs visual rules that must be preserved?
|
||||||
848
data/vendor/grsai.ts
vendored
848
data/vendor/grsai.ts
vendored
@ -1,424 +1,424 @@
|
|||||||
/**
|
/**
|
||||||
* Toonflow AI供应商模板
|
* Toonflow AI供应商模板
|
||||||
* @version 2.0
|
* @version 2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 类型定义
|
// 类型定义
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
type VideoMode =
|
type VideoMode =
|
||||||
| "singleImage" //单图参考
|
| "singleImage" //单图参考
|
||||||
| "startEndRequired" //首尾帧(两张都得有)
|
| "startEndRequired" //首尾帧(两张都得有)
|
||||||
| "endFrameOptional" //首尾帧(尾帧可选)
|
| "endFrameOptional" //首尾帧(尾帧可选)
|
||||||
| "startFrameOptional" //首尾帧(首帧可选)
|
| "startFrameOptional" //首尾帧(首帧可选)
|
||||||
| "text" //文本
|
| "text" //文本
|
||||||
| (
|
| (
|
||||||
| `videoReference:${number}`
|
| `videoReference:${number}`
|
||||||
| `imageReference:${number}`
|
| `imageReference:${number}`
|
||||||
| `audioReference:${number}`
|
| `audioReference:${number}`
|
||||||
)[]; //多参考(数字代表限制数量)
|
)[]; //多参考(数字代表限制数量)
|
||||||
|
|
||||||
interface TextModel {
|
interface TextModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "text";
|
type: "text";
|
||||||
think: boolean;
|
think: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageModel {
|
interface ImageModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "image";
|
type: "image";
|
||||||
mode: ("text" | "singleImage" | "multiReference")[];
|
mode: ("text" | "singleImage" | "multiReference")[];
|
||||||
associationSkills?: string;
|
associationSkills?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoModel {
|
interface VideoModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "video";
|
type: "video";
|
||||||
mode: VideoMode[];
|
mode: VideoMode[];
|
||||||
associationSkills?: string;
|
associationSkills?: string;
|
||||||
audio: "optional" | false | true;
|
audio: "optional" | false | true;
|
||||||
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TTSModel {
|
interface TTSModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "tts";
|
type: "tts";
|
||||||
voices: { title: string; voice: string }[];
|
voices: { title: string; voice: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VendorConfig {
|
interface VendorConfig {
|
||||||
id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号
|
id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号
|
||||||
version: string; //版本号,格式为x.y,需遵守语义化版本控制
|
version: string; //版本号,格式为x.y,需遵守语义化版本控制
|
||||||
name: string; //供应商名称
|
name: string; //供应商名称
|
||||||
author: string; //作者
|
author: string; //作者
|
||||||
description?: string; //描述,支持Markdown格式
|
description?: string; //描述,支持Markdown格式
|
||||||
icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素
|
icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素
|
||||||
inputs: {
|
inputs: {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: "text" | "password" | "url";
|
type: "text" | "password" | "url";
|
||||||
required: boolean;
|
required: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}[];
|
}[];
|
||||||
inputValues: Record<string, string>;
|
inputValues: Record<string, string>;
|
||||||
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReferenceList =
|
type ReferenceList =
|
||||||
| { type: "image"; sourceType: "base64"; base64: string }
|
| { type: "image"; sourceType: "base64"; base64: string }
|
||||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||||
| { type: "video"; sourceType: "base64"; base64: string };
|
| { type: "video"; sourceType: "base64"; base64: string };
|
||||||
|
|
||||||
interface ImageConfig {
|
interface ImageConfig {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
referenceList?: Extract<ReferenceList, { type: "image" }>[];
|
referenceList?: Extract<ReferenceList, { type: "image" }>[];
|
||||||
size: "1K" | "2K" | "4K";
|
size: "1K" | "2K" | "4K";
|
||||||
aspectRatio: `${number}:${number}`;
|
aspectRatio: `${number}:${number}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoConfig {
|
interface VideoConfig {
|
||||||
duration: number;
|
duration: number;
|
||||||
resolution: string;
|
resolution: string;
|
||||||
aspectRatio: "16:9" | "9:16";
|
aspectRatio: "16:9" | "9:16";
|
||||||
prompt: string;
|
prompt: string;
|
||||||
referenceList?: ReferenceList[];
|
referenceList?: ReferenceList[];
|
||||||
audio?: boolean;
|
audio?: boolean;
|
||||||
mode: VideoMode[];
|
mode: VideoMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TTSConfig {
|
interface TTSConfig {
|
||||||
text: string;
|
text: string;
|
||||||
voice: string;
|
voice: string;
|
||||||
speechRate: number;
|
speechRate: number;
|
||||||
pitchRate: number;
|
pitchRate: number;
|
||||||
volume: number;
|
volume: number;
|
||||||
referenceList?: Extract<ReferenceList, { type: "audio" }>[];
|
referenceList?: Extract<ReferenceList, { type: "audio" }>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PollResult {
|
interface PollResult {
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
data?: string;
|
data?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 全局声明
|
// 全局声明
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
declare const axios: any; // HTTP请求库
|
declare const axios: any; // HTTP请求库
|
||||||
declare const logger: (msg: string) => void; // 日志函数
|
declare const logger: (msg: string) => void; // 日志函数
|
||||||
declare const jsonwebtoken: any; // JWT处理库
|
declare const jsonwebtoken: any; // JWT处理库
|
||||||
declare const zipImage: (base64: string, size: number) => Promise<string>; // 图片压缩函数,返回有头base64字符串
|
declare const zipImage: (base64: string, size: number) => Promise<string>; // 图片压缩函数,返回有头base64字符串
|
||||||
declare const zipImageResolution: (
|
declare const zipImageResolution: (
|
||||||
base64: string,
|
base64: string,
|
||||||
w: number,
|
w: number,
|
||||||
h: number,
|
h: number,
|
||||||
) => Promise<string>; // 图片分辨率调整函数,返回有头base64字符串
|
) => Promise<string>; // 图片分辨率调整函数,返回有头base64字符串
|
||||||
declare const mergeImages: (
|
declare const mergeImages: (
|
||||||
base64Arr: string[],
|
base64Arr: string[],
|
||||||
maxSize?: string,
|
maxSize?: string,
|
||||||
) => Promise<string>; // 图片合成函数,返回有头base64字符串
|
) => Promise<string>; // 图片合成函数,返回有头base64字符串
|
||||||
declare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数,返回有头base64字符串
|
declare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数,返回有头base64字符串
|
||||||
declare const pollTask: (
|
declare const pollTask: (
|
||||||
fn: () => Promise<PollResult>,
|
fn: () => Promise<PollResult>,
|
||||||
interval?: number,
|
interval?: number,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
) => Promise<PollResult>; // 轮询函数,fn为异步函数,interval为轮询间隔,timeout为超时时间,返回fn的结果
|
) => Promise<PollResult>; // 轮询函数,fn为异步函数,interval为轮询间隔,timeout为超时时间,返回fn的结果
|
||||||
declare const createOpenAI: any;
|
declare const createOpenAI: any;
|
||||||
declare const createDeepSeek: any;
|
declare const createDeepSeek: any;
|
||||||
declare const createZhipu: any;
|
declare const createZhipu: any;
|
||||||
declare const createQwen: any;
|
declare const createQwen: any;
|
||||||
declare const createAnthropic: any;
|
declare const createAnthropic: any;
|
||||||
declare const createOpenAICompatible: any;
|
declare const createOpenAICompatible: any;
|
||||||
declare const createXai: any;
|
declare const createXai: any;
|
||||||
declare const createMinimax: any;
|
declare const createMinimax: any;
|
||||||
declare const createGoogleGenerativeAI: any;
|
declare const createGoogleGenerativeAI: any;
|
||||||
declare const exports: {
|
declare const exports: {
|
||||||
vendor: VendorConfig;
|
vendor: VendorConfig;
|
||||||
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; //文本模型
|
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; //文本模型
|
||||||
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>; //图片模型,返回有头base64字符串
|
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>; //图片模型,返回有头base64字符串
|
||||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型,返回有头base64字符串
|
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型,返回有头base64字符串
|
||||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //(暂未开放)语音模型,返回有头base64字符串
|
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //(暂未开放)语音模型,返回有头base64字符串
|
||||||
checkForUpdates?: () => Promise<{
|
checkForUpdates?: () => Promise<{
|
||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
notice: string;
|
notice: string;
|
||||||
}>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式)
|
}>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式)
|
||||||
updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本
|
updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 供应商配置
|
// 供应商配置
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const vendor: VendorConfig = {
|
const vendor: VendorConfig = {
|
||||||
id: "grsai",
|
id: "grsai",
|
||||||
version: "2.1",
|
version: "2.1",
|
||||||
author: "Toonflow",
|
author: "Toonflow",
|
||||||
name: "Grsai",
|
name: "Grsai",
|
||||||
description:
|
description:
|
||||||
"Grsai AI平台适配,支持文生图、图生图、文生视频、Gemini兼容文本模型 \n [前往中转平台](https://tf.grsai.ai/zh)",
|
"Grsai AI平台适配,支持文生图、图生图、文生视频、Gemini兼容文本模型 \n [前往中转平台](https://tf.grsai.ai/zh)",
|
||||||
inputs: [
|
inputs: [
|
||||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
label: "请求地址",
|
label: "请求地址",
|
||||||
type: "url",
|
type: "url",
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: "示例:https://grsai.dakka.com.cn",
|
placeholder: "示例:https://grsai.dakka.com.cn",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
inputValues: { apiKey: "", baseUrl: "https://grsai.dakka.com.cn" },
|
inputValues: { apiKey: "", baseUrl: "https://grsai.dakka.com.cn" },
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
name: "GPT Image 2",
|
name: "GPT Image 2",
|
||||||
modelName: "gpt-image-2",
|
modelName: "gpt-image-2",
|
||||||
type: "image",
|
type: "image",
|
||||||
mode: ["text", "singleImage", "multiReference"],
|
mode: ["text", "singleImage", "multiReference"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nano Banana Fast",
|
name: "Nano Banana Fast",
|
||||||
modelName: "nano-banana-fast",
|
modelName: "nano-banana-fast",
|
||||||
type: "image",
|
type: "image",
|
||||||
mode: ["text", "singleImage", "multiReference"],
|
mode: ["text", "singleImage", "multiReference"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nano Banana 2",
|
name: "Nano Banana 2",
|
||||||
modelName: "nano-banana-2",
|
modelName: "nano-banana-2",
|
||||||
type: "image",
|
type: "image",
|
||||||
mode: ["text", "singleImage", "multiReference"],
|
mode: ["text", "singleImage", "multiReference"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nano Banana Pro",
|
name: "Nano Banana Pro",
|
||||||
modelName: "nano-banana-pro",
|
modelName: "nano-banana-pro",
|
||||||
type: "image",
|
type: "image",
|
||||||
mode: ["text", "singleImage", "multiReference"],
|
mode: ["text", "singleImage", "multiReference"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 辅助工具
|
// 辅助工具
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const getHeaders = () => {
|
const getHeaders = () => {
|
||||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||||
return {
|
return {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 适配器函数
|
// 适配器函数
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const textRequest = (
|
const textRequest = (
|
||||||
model: TextModel,
|
model: TextModel,
|
||||||
think: boolean,
|
think: boolean,
|
||||||
thinkLevel: 0 | 1 | 2 | 3,
|
thinkLevel: 0 | 1 | 2 | 3,
|
||||||
) => {
|
) => {
|
||||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||||
return createGoogleGenerativeAI({
|
return createGoogleGenerativeAI({
|
||||||
baseURL: `${vendor.inputValues.baseUrl}/v1beta`,
|
baseURL: `${vendor.inputValues.baseUrl}/v1beta`,
|
||||||
apiKey,
|
apiKey,
|
||||||
}).chat(model.modelName);
|
}).chat(model.modelName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageRequest = async (
|
const imageRequest = async (
|
||||||
config: ImageConfig,
|
config: ImageConfig,
|
||||||
model: ImageModel,
|
model: ImageModel,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||||
const baseUrl = vendor.inputValues.baseUrl;
|
const baseUrl = vendor.inputValues.baseUrl;
|
||||||
const headers = getHeaders();
|
const headers = getHeaders();
|
||||||
|
|
||||||
// 构造请求参数
|
// 构造请求参数
|
||||||
const requestBody: any = {
|
const requestBody: any = {
|
||||||
model: model.modelName,
|
model: model.modelName,
|
||||||
prompt: config.prompt,
|
prompt: config.prompt,
|
||||||
aspectRatio: config.aspectRatio,
|
aspectRatio: config.aspectRatio,
|
||||||
webHook: "-1",
|
webHook: "-1",
|
||||||
shutProgress: true,
|
shutProgress: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 补充模型专属参数
|
// 补充模型专属参数
|
||||||
if (model.modelName.startsWith("nano-banana")) {
|
if (model.modelName.startsWith("nano-banana")) {
|
||||||
requestBody.imageSize = config.size;
|
requestBody.imageSize = config.size;
|
||||||
} else {
|
} else {
|
||||||
requestBody.size = config.aspectRatio;
|
requestBody.size = config.aspectRatio;
|
||||||
requestBody.variants = 1;
|
requestBody.variants = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理参考图
|
// 处理参考图
|
||||||
if (config.referenceList && config.referenceList.length > 0) {
|
if (config.referenceList && config.referenceList.length > 0) {
|
||||||
requestBody.urls = config.referenceList.map((img) => img.base64);
|
requestBody.urls = config.referenceList.map((img) => img.base64);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择接口路径
|
// 选择接口路径
|
||||||
const apiPath = model.modelName.startsWith("nano-banana")
|
const apiPath = model.modelName.startsWith("nano-banana")
|
||||||
? "/v1/draw/nano-banana"
|
? "/v1/draw/nano-banana"
|
||||||
: "/v1/draw/completions";
|
: "/v1/draw/completions";
|
||||||
|
|
||||||
logger(`开始提交图片生成任务,模型:${model.modelName}`);
|
logger(`开始提交图片生成任务,模型:${model.modelName}`);
|
||||||
const submitResp = await axios.post(`${baseUrl}${apiPath}`, requestBody, {
|
const submitResp = await axios.post(`${baseUrl}${apiPath}`, requestBody, {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
if (submitResp.data.code !== 0)
|
if (submitResp.data.code !== 0)
|
||||||
throw new Error(`任务提交失败:${submitResp.data.msg}`);
|
throw new Error(`任务提交失败:${submitResp.data.msg}`);
|
||||||
|
|
||||||
const taskId = submitResp.data.data.id;
|
const taskId = submitResp.data.data.id;
|
||||||
logger(`图片任务提交成功,任务ID:${taskId}`);
|
logger(`图片任务提交成功,任务ID:${taskId}`);
|
||||||
|
|
||||||
// 轮询结果
|
// 轮询结果
|
||||||
const pollResult = await pollTask(
|
const pollResult = await pollTask(
|
||||||
async () => {
|
async () => {
|
||||||
const resp = await axios.post(
|
const resp = await axios.post(
|
||||||
`${baseUrl}/v1/draw/result`,
|
`${baseUrl}/v1/draw/result`,
|
||||||
{ id: taskId },
|
{ id: taskId },
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
if (resp.data.code !== 0)
|
if (resp.data.code !== 0)
|
||||||
return { completed: true, error: resp.data.msg };
|
return { completed: true, error: resp.data.msg };
|
||||||
|
|
||||||
const taskData = resp.data.data;
|
const taskData = resp.data.data;
|
||||||
if (taskData.status === "failed")
|
if (taskData.status === "failed")
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
error: taskData.failure_reason || taskData.error,
|
error: taskData.failure_reason || taskData.error,
|
||||||
};
|
};
|
||||||
if (taskData.status === "succeeded") {
|
if (taskData.status === "succeeded") {
|
||||||
const imgUrl = taskData.results?.[0]?.url || taskData.url;
|
const imgUrl = taskData.results?.[0]?.url || taskData.url;
|
||||||
return { completed: true, data: imgUrl };
|
return { completed: true, data: imgUrl };
|
||||||
}
|
}
|
||||||
logger(`图片任务生成中,进度:${taskData.progress}%`);
|
logger(`图片任务生成中,进度:${taskData.progress}%`);
|
||||||
return { completed: false };
|
return { completed: false };
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
600000,
|
600000,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pollResult.error) throw new Error(pollResult.error);
|
if (pollResult.error) throw new Error(pollResult.error);
|
||||||
logger(`图片生成完成,开始转换Base64`);
|
logger(`图片生成完成,开始转换Base64`);
|
||||||
return await urlToBase64(pollResult.data!);
|
return await urlToBase64(pollResult.data!);
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoRequest = async (
|
const videoRequest = async (
|
||||||
config: VideoConfig,
|
config: VideoConfig,
|
||||||
model: VideoModel,
|
model: VideoModel,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||||
const baseUrl = vendor.inputValues.baseUrl;
|
const baseUrl = vendor.inputValues.baseUrl;
|
||||||
const headers = getHeaders();
|
const headers = getHeaders();
|
||||||
|
|
||||||
// 构造请求参数
|
// 构造请求参数
|
||||||
const requestBody: any = {
|
const requestBody: any = {
|
||||||
model: model.modelName,
|
model: model.modelName,
|
||||||
prompt: config.prompt,
|
prompt: config.prompt,
|
||||||
aspectRatio: config.aspectRatio,
|
aspectRatio: config.aspectRatio,
|
||||||
webHook: "-1",
|
webHook: "-1",
|
||||||
shutProgress: true,
|
shutProgress: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理参考资源
|
// 处理参考资源
|
||||||
if (config.referenceList && config.referenceList.length > 0) {
|
if (config.referenceList && config.referenceList.length > 0) {
|
||||||
const imageRefs = config.referenceList.filter(
|
const imageRefs = config.referenceList.filter(
|
||||||
(item) => item.type === "image",
|
(item) => item.type === "image",
|
||||||
) as Extract<ReferenceList, { type: "image" }>[];
|
) as Extract<ReferenceList, { type: "image" }>[];
|
||||||
if (config.mode.includes("endFrameOptional") && imageRefs.length >= 1) {
|
if (config.mode.includes("endFrameOptional") && imageRefs.length >= 1) {
|
||||||
requestBody.firstFrameUrl = imageRefs[0].base64;
|
requestBody.firstFrameUrl = imageRefs[0].base64;
|
||||||
if (imageRefs.length >= 2) requestBody.lastFrameUrl = imageRefs[1].base64;
|
if (imageRefs.length >= 2) requestBody.lastFrameUrl = imageRefs[1].base64;
|
||||||
} else if (
|
} else if (
|
||||||
config.mode.some(
|
config.mode.some(
|
||||||
(m) => Array.isArray(m) && m.includes("imageReference:3"),
|
(m) => Array.isArray(m) && m.includes("imageReference:3"),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
requestBody.urls = imageRefs.map((img) => img.base64);
|
requestBody.urls = imageRefs.map((img) => img.base64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger(`开始提交视频生成任务,模型:${model.modelName}`);
|
logger(`开始提交视频生成任务,模型:${model.modelName}`);
|
||||||
const submitResp = await axios.post(`${baseUrl}/v1/video/veo`, requestBody, {
|
const submitResp = await axios.post(`${baseUrl}/v1/video/veo`, requestBody, {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
if (submitResp.data.code !== 0)
|
if (submitResp.data.code !== 0)
|
||||||
throw new Error(`任务提交失败:${submitResp.data.msg}`);
|
throw new Error(`任务提交失败:${submitResp.data.msg}`);
|
||||||
|
|
||||||
const taskId = submitResp.data.data.id;
|
const taskId = submitResp.data.data.id;
|
||||||
logger(`视频任务提交成功,任务ID:${taskId}`);
|
logger(`视频任务提交成功,任务ID:${taskId}`);
|
||||||
|
|
||||||
// 轮询结果
|
// 轮询结果
|
||||||
const pollResult = await pollTask(
|
const pollResult = await pollTask(
|
||||||
async () => {
|
async () => {
|
||||||
const resp = await axios.post(
|
const resp = await axios.post(
|
||||||
`${baseUrl}/v1/draw/result`,
|
`${baseUrl}/v1/draw/result`,
|
||||||
{ id: taskId },
|
{ id: taskId },
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
if (resp.data.code !== 0)
|
if (resp.data.code !== 0)
|
||||||
return { completed: true, error: resp.data.msg };
|
return { completed: true, error: resp.data.msg };
|
||||||
|
|
||||||
const taskData = resp.data.data;
|
const taskData = resp.data.data;
|
||||||
if (taskData.status === "failed")
|
if (taskData.status === "failed")
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
error: taskData.failure_reason || taskData.error,
|
error: taskData.failure_reason || taskData.error,
|
||||||
};
|
};
|
||||||
if (taskData.status === "succeeded") {
|
if (taskData.status === "succeeded") {
|
||||||
return { completed: true, data: taskData.url };
|
return { completed: true, data: taskData.url };
|
||||||
}
|
}
|
||||||
logger(`视频任务生成中,进度:${taskData.progress}%`);
|
logger(`视频任务生成中,进度:${taskData.progress}%`);
|
||||||
return { completed: false };
|
return { completed: false };
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
1800000,
|
1800000,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pollResult.error) throw new Error(pollResult.error);
|
if (pollResult.error) throw new Error(pollResult.error);
|
||||||
logger(`视频生成完成,开始转换Base64`);
|
logger(`视频生成完成,开始转换Base64`);
|
||||||
return await urlToBase64(pollResult.data!);
|
return await urlToBase64(pollResult.data!);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ttsRequest = async (
|
const ttsRequest = async (
|
||||||
config: TTSConfig,
|
config: TTSConfig,
|
||||||
model: TTSModel,
|
model: TTSModel,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForUpdates = async (): Promise<{
|
const checkForUpdates = async (): Promise<{
|
||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
notice: string;
|
notice: string;
|
||||||
}> => {
|
}> => {
|
||||||
return {
|
return {
|
||||||
hasUpdate: false,
|
hasUpdate: false,
|
||||||
latestVersion: "1.0",
|
latestVersion: "1.0",
|
||||||
notice: "## 新版本更新公告",
|
notice: "## 新版本更新公告",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVendor = async (): Promise<string> => {
|
const updateVendor = async (): Promise<string> => {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 导出
|
// 导出
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
exports.vendor = vendor;
|
exports.vendor = vendor;
|
||||||
exports.textRequest = textRequest;
|
exports.textRequest = textRequest;
|
||||||
exports.imageRequest = imageRequest;
|
exports.imageRequest = imageRequest;
|
||||||
exports.videoRequest = videoRequest;
|
exports.videoRequest = videoRequest;
|
||||||
exports.ttsRequest = ttsRequest;
|
exports.ttsRequest = ttsRequest;
|
||||||
exports.checkForUpdates = checkForUpdates;
|
exports.checkForUpdates = checkForUpdates;
|
||||||
exports.updateVendor = updateVendor;
|
exports.updateVendor = updateVendor;
|
||||||
|
|
||||||
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
|
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
668
data/vendor/null.ts
vendored
668
data/vendor/null.ts
vendored
@ -1,334 +1,334 @@
|
|||||||
/**
|
/**
|
||||||
* Toonflow AI供应商模板
|
* Toonflow AI供应商模板
|
||||||
* @version 2.0
|
* @version 2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 类型定义
|
// 类型定义
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
type VideoMode =
|
type VideoMode =
|
||||||
| "singleImage" //单图参考
|
| "singleImage" //单图参考
|
||||||
| "startEndRequired" //首尾帧(两张都得有)
|
| "startEndRequired" //首尾帧(两张都得有)
|
||||||
| "endFrameOptional" //首尾帧(尾帧可选)
|
| "endFrameOptional" //首尾帧(尾帧可选)
|
||||||
| "startFrameOptional" //首尾帧(首帧可选)
|
| "startFrameOptional" //首尾帧(首帧可选)
|
||||||
| "text" //文本
|
| "text" //文本
|
||||||
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量)
|
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量)
|
||||||
|
|
||||||
interface TextModel {
|
interface TextModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "text";
|
type: "text";
|
||||||
think: boolean;
|
think: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageModel {
|
interface ImageModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "image";
|
type: "image";
|
||||||
mode: ("text" | "singleImage" | "multiReference")[];
|
mode: ("text" | "singleImage" | "multiReference")[];
|
||||||
associationSkills?: string;
|
associationSkills?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoModel {
|
interface VideoModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "video";
|
type: "video";
|
||||||
mode: VideoMode[];
|
mode: VideoMode[];
|
||||||
associationSkills?: string;
|
associationSkills?: string;
|
||||||
audio: "optional" | false | true;
|
audio: "optional" | false | true;
|
||||||
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TTSModel {
|
interface TTSModel {
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
type: "tts";
|
type: "tts";
|
||||||
voices: { title: string; voice: string }[];
|
voices: { title: string; voice: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VendorConfig {
|
interface VendorConfig {
|
||||||
id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号
|
id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号
|
||||||
version: string; //版本号,格式为x.y,需遵守语义化版本控制
|
version: string; //版本号,格式为x.y,需遵守语义化版本控制
|
||||||
name: string; //供应商名称
|
name: string; //供应商名称
|
||||||
author: string; //作者
|
author: string; //作者
|
||||||
description?: string; //描述,支持Markdown格式
|
description?: string; //描述,支持Markdown格式
|
||||||
icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素
|
icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素
|
||||||
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
|
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
|
||||||
inputValues: Record<string, string>;
|
inputValues: Record<string, string>;
|
||||||
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReferenceList =
|
type ReferenceList =
|
||||||
| { type: "image"; sourceType: "base64"; base64: string }
|
| { type: "image"; sourceType: "base64"; base64: string }
|
||||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||||
| { type: "video"; sourceType: "base64"; base64: string };
|
| { type: "video"; sourceType: "base64"; base64: string };
|
||||||
|
|
||||||
interface ImageConfig {
|
interface ImageConfig {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
referenceList?: Extract<ReferenceList, { type: "image" }>[];
|
referenceList?: Extract<ReferenceList, { type: "image" }>[];
|
||||||
size: "1K" | "2K" | "4K";
|
size: "1K" | "2K" | "4K";
|
||||||
aspectRatio: `${number}:${number}`;
|
aspectRatio: `${number}:${number}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoConfig {
|
interface VideoConfig {
|
||||||
duration: number;
|
duration: number;
|
||||||
resolution: string;
|
resolution: string;
|
||||||
aspectRatio: "16:9" | "9:16";
|
aspectRatio: "16:9" | "9:16";
|
||||||
prompt: string;
|
prompt: string;
|
||||||
referenceList?: ReferenceList[];
|
referenceList?: ReferenceList[];
|
||||||
audio?: boolean;
|
audio?: boolean;
|
||||||
mode: VideoMode[];
|
mode: VideoMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TTSConfig {
|
interface TTSConfig {
|
||||||
text: string;
|
text: string;
|
||||||
voice: string;
|
voice: string;
|
||||||
speechRate: number;
|
speechRate: number;
|
||||||
pitchRate: number;
|
pitchRate: number;
|
||||||
volume: number;
|
volume: number;
|
||||||
referenceList?: Extract<ReferenceList, { type: "audio" }>[];
|
referenceList?: Extract<ReferenceList, { type: "audio" }>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PollResult {
|
interface PollResult {
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
data?: string;
|
data?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 全局声明
|
// 全局声明
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
declare const axios: any; // HTTP请求库
|
declare const axios: any; // HTTP请求库
|
||||||
declare const logger: (msg: string) => void; // 日志函数
|
declare const logger: (msg: string) => void; // 日志函数
|
||||||
declare const jsonwebtoken: any; // JWT处理库
|
declare const jsonwebtoken: any; // JWT处理库
|
||||||
declare const zipImage: (base64: string, size: number) => Promise<string>; // 图片压缩函数,返回有头base64字符串
|
declare const zipImage: (base64: string, size: number) => Promise<string>; // 图片压缩函数,返回有头base64字符串
|
||||||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>; // 图片分辨率调整函数,返回有头base64字符串
|
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>; // 图片分辨率调整函数,返回有头base64字符串
|
||||||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>; // 图片合成函数,返回有头base64字符串
|
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>; // 图片合成函数,返回有头base64字符串
|
||||||
declare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数,返回有头base64字符串
|
declare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数,返回有头base64字符串
|
||||||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>; // 轮询函数,fn为异步函数,interval为轮询间隔,timeout为超时时间,返回fn的结果
|
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>; // 轮询函数,fn为异步函数,interval为轮询间隔,timeout为超时时间,返回fn的结果
|
||||||
declare const createOpenAI: any;
|
declare const createOpenAI: any;
|
||||||
declare const createDeepSeek: any;
|
declare const createDeepSeek: any;
|
||||||
declare const createZhipu: any;
|
declare const createZhipu: any;
|
||||||
declare const createQwen: any;
|
declare const createQwen: any;
|
||||||
declare const createAnthropic: any;
|
declare const createAnthropic: any;
|
||||||
declare const createOpenAICompatible: any;
|
declare const createOpenAICompatible: any;
|
||||||
declare const createXai: any;
|
declare const createXai: any;
|
||||||
declare const createMinimax: any;
|
declare const createMinimax: any;
|
||||||
declare const createGoogleGenerativeAI: any;
|
declare const createGoogleGenerativeAI: any;
|
||||||
declare const exports: {
|
declare const exports: {
|
||||||
vendor: VendorConfig;
|
vendor: VendorConfig;
|
||||||
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; //文本模型
|
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; //文本模型
|
||||||
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>; //图片模型,返回有头base64字符串
|
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>; //图片模型,返回有头base64字符串
|
||||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型,返回有头base64字符串
|
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型,返回有头base64字符串
|
||||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //(暂未开放)语音模型,返回有头base64字符串
|
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //(暂未开放)语音模型,返回有头base64字符串
|
||||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式)
|
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式)
|
||||||
updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本
|
updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 供应商配置
|
// 供应商配置
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const vendor: VendorConfig = {
|
const vendor: VendorConfig = {
|
||||||
id: "null",
|
id: "null",
|
||||||
version: "2.0",
|
version: "2.0",
|
||||||
author: "Toonflow",
|
author: "Toonflow",
|
||||||
name: "空模板",
|
name: "空模板",
|
||||||
description: "## 开发模板,您可以使用此模板进行Vibe Coding",
|
description: "## 开发模板,您可以使用此模板进行Vibe Coding",
|
||||||
inputs: [
|
inputs: [
|
||||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||||
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://api.openai.com/v1" },
|
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://api.openai.com/v1" },
|
||||||
],
|
],
|
||||||
inputValues: { apiKey: "", baseUrl: "https://api.openai.com/v1" },
|
inputValues: { apiKey: "", baseUrl: "https://api.openai.com/v1" },
|
||||||
models: [{ name: "GPT-4o", modelName: "gpt-4o", type: "text", think: false }],
|
models: [{ name: "GPT-4o", modelName: "gpt-4o", type: "text", think: false }],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 适配器函数
|
// 适配器函数
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
|
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
|
||||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||||
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
|
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
|
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
|
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
|
||||||
return { hasUpdate: false, latestVersion: "2.0", notice: "## 新版本更新公告" };
|
return { hasUpdate: false, latestVersion: "2.0", notice: "## 新版本更新公告" };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVendor = async (): Promise<string> => {
|
const updateVendor = async (): Promise<string> => {
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 导出
|
// 导出
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
exports.vendor = vendor;
|
exports.vendor = vendor;
|
||||||
exports.textRequest = textRequest;
|
exports.textRequest = textRequest;
|
||||||
exports.imageRequest = imageRequest;
|
exports.imageRequest = imageRequest;
|
||||||
exports.videoRequest = videoRequest;
|
exports.videoRequest = videoRequest;
|
||||||
exports.ttsRequest = ttsRequest;
|
exports.ttsRequest = ttsRequest;
|
||||||
exports.checkForUpdates = checkForUpdates;
|
exports.checkForUpdates = checkForUpdates;
|
||||||
exports.updateVendor = updateVendor;
|
exports.updateVendor = updateVendor;
|
||||||
|
|
||||||
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
|
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ============================================================
|
* ============================================================
|
||||||
* AI 代码生成指南
|
* AI 代码生成指南
|
||||||
* ============================================================
|
* ============================================================
|
||||||
*
|
*
|
||||||
* 【概述】
|
* 【概述】
|
||||||
* 本文件是 Toonflow AI 供应商适配模板。AI 在生成新供应商适配代码时,
|
* 本文件是 Toonflow AI 供应商适配模板。AI 在生成新供应商适配代码时,
|
||||||
* 必须严格遵守以下规则,并要求用户提供目标平台的 curl 调用示例或 API 文档信息作为输入依据。
|
* 必须严格遵守以下规则,并要求用户提供目标平台的 curl 调用示例或 API 文档信息作为输入依据。
|
||||||
*
|
*
|
||||||
* 【前置要求】
|
* 【前置要求】
|
||||||
* 在生成代码前,请向用户索要以下信息(至少其一):
|
* 在生成代码前,请向用户索要以下信息(至少其一):
|
||||||
* 1. 目标 API 的 curl 请求示例(包含请求地址、Headers、Body 结构、响应结构)
|
* 1. 目标 API 的 curl 请求示例(包含请求地址、Headers、Body 结构、响应结构)
|
||||||
* 2. 目标 API 的官方文档链接或文档截图/文本内容
|
* 2. 目标 API 的官方文档链接或文档截图/文本内容
|
||||||
* 3. 需要适配的模型类型(text / image / video / tts)及其能力说明
|
* 3. 需要适配的模型类型(text / image / video / tts)及其能力说明
|
||||||
* 没有足够信息时,应主动追问,不要凭空编造 API 结构。
|
* 没有足够信息时,应主动追问,不要凭空编造 API 结构。
|
||||||
*
|
*
|
||||||
* 【代码规则】
|
* 【代码规则】
|
||||||
*
|
*
|
||||||
* 1. 禁止引入任何外部包
|
* 1. 禁止引入任何外部包
|
||||||
* 不可使用 import / require,仅能使用本文件「全局声明」区域中已声明的方法和对象,
|
* 不可使用 import / require,仅能使用本文件「全局声明」区域中已声明的方法和对象,
|
||||||
* 包括:axios、logger、jsonwebtoken、zipImage、zipImageResolution、mergeImages、
|
* 包括:axios、logger、jsonwebtoken、zipImage、zipImageResolution、mergeImages、
|
||||||
* urlToBase64、pollTask,以及 createOpenAI、createDeepSeek、createZhipu、createQwen、
|
* urlToBase64、pollTask,以及 createOpenAI、createDeepSeek、createZhipu、createQwen、
|
||||||
* createAnthropic、createOpenAICompatible、createXai、createMinimax、
|
* createAnthropic、createOpenAICompatible、createXai、createMinimax、
|
||||||
* createGoogleGenerativeAI 等 AI SDK 工厂函数。
|
* createGoogleGenerativeAI 等 AI SDK 工厂函数。
|
||||||
*
|
*
|
||||||
* 2. 禁止在 exports.* 函数外部声明离散的全大写常量
|
* 2. 禁止在 exports.* 函数外部声明离散的全大写常量
|
||||||
* 错误示例:const API_URL = "https://..."; const MAX_RETRY = 3;
|
* 错误示例:const API_URL = "https://..."; const MAX_RETRY = 3;
|
||||||
* 如果确实需要可配置的常量值,必须将其声明在 vendor.inputValues 中,
|
* 如果确实需要可配置的常量值,必须将其声明在 vendor.inputValues 中,
|
||||||
* 通过 vendor.inputValues.xxx 访问,让用户可在界面上配置。
|
* 通过 vendor.inputValues.xxx 访问,让用户可在界面上配置。
|
||||||
* 如果是纯逻辑内部使用的临时变量,应内联在对应的 exports.* 函数体内部,使用小驼峰命名。
|
* 如果是纯逻辑内部使用的临时变量,应内联在对应的 exports.* 函数体内部,使用小驼峰命名。
|
||||||
*
|
*
|
||||||
* 3. 逻辑尽量聚合在 exports.* 对应的函数内部
|
* 3. 逻辑尽量聚合在 exports.* 对应的函数内部
|
||||||
* 每个适配函数(textRequest / imageRequest / videoRequest / ttsRequest)
|
* 每个适配函数(textRequest / imageRequest / videoRequest / ttsRequest)
|
||||||
* 应自包含,将请求构造、发送、轮询、结果解析等逻辑写在函数体内,避免拆分出大量外部辅助函数。
|
* 应自包含,将请求构造、发送、轮询、结果解析等逻辑写在函数体内,避免拆分出大量外部辅助函数。
|
||||||
* 如果多个函数确实存在公共逻辑(如签名计算、Token 生成、请求头构造),
|
* 如果多个函数确实存在公共逻辑(如签名计算、Token 生成、请求头构造),
|
||||||
* 可提取为文件内的小驼峰命名函数,放在「适配器函数」区块之前的「辅助工具」区块中,
|
* 可提取为文件内的小驼峰命名函数,放在「适配器函数」区块之前的「辅助工具」区块中,
|
||||||
* 且不可使用全大写命名。
|
* 且不可使用全大写命名。
|
||||||
*
|
*
|
||||||
* 4. 命名规范
|
* 4. 命名规范
|
||||||
* 所有变量、函数一律使用小驼峰命名(camelCase),禁止使用 UPPER_SNAKE_CASE。
|
* 所有变量、函数一律使用小驼峰命名(camelCase),禁止使用 UPPER_SNAKE_CASE。
|
||||||
*
|
*
|
||||||
* 5. 不需要重新声明类型
|
* 5. 不需要重新声明类型
|
||||||
* 本文件顶部已完整定义了所有接口和类型(VendorConfig、ImageConfig、VideoConfig、
|
* 本文件顶部已完整定义了所有接口和类型(VendorConfig、ImageConfig、VideoConfig、
|
||||||
* TTSConfig、TextModel、ImageModel、VideoModel、TTSModel、ReferenceList、PollResult 等),
|
* TTSConfig、TextModel、ImageModel、VideoModel、TTSModel、ReferenceList、PollResult 等),
|
||||||
* AI 生成代码时直接使用即可,不要重复声明。
|
* AI 生成代码时直接使用即可,不要重复声明。
|
||||||
*
|
*
|
||||||
* 6. 返回值规范
|
* 6. 返回值规范
|
||||||
* - textRequest(model):返回 AI SDK 的 chat model 实例(通过 createOpenAI 等工厂函数创建)。
|
* - textRequest(model):返回 AI SDK 的 chat model 实例(通过 createOpenAI 等工厂函数创建)。
|
||||||
* - imageRequest(config, model):返回有头 base64 字符串(如 "data:image/png;base64,...")。
|
* - imageRequest(config, model):返回有头 base64 字符串(如 "data:image/png;base64,...")。
|
||||||
* config.referenceList 为 Extract<ReferenceList, { type: "image" }>[] 类型,
|
* config.referenceList 为 Extract<ReferenceList, { type: "image" }>[] 类型,
|
||||||
* 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。
|
* 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。
|
||||||
* - videoRequest(config, model):返回有头 base64 字符串(如 "data:video/mp4;base64,...")。
|
* - videoRequest(config, model):返回有头 base64 字符串(如 "data:video/mp4;base64,...")。
|
||||||
* config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用,
|
* config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用,
|
||||||
* 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。
|
* 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。
|
||||||
* config.mode 为当前激活的视频模式数组,需根据 mode 决定如何使用 referenceList。
|
* config.mode 为当前激活的视频模式数组,需根据 mode 决定如何使用 referenceList。
|
||||||
* - ttsRequest(config, model):返回有头 base64 字符串(如 "data:audio/mp3;base64,...")。
|
* - ttsRequest(config, model):返回有头 base64 字符串(如 "data:audio/mp3;base64,...")。
|
||||||
* config.referenceList 为 Extract<ReferenceList, { type: "audio" }>[] 类型(音频参考)。
|
* config.referenceList 为 Extract<ReferenceList, { type: "audio" }>[] 类型(音频参考)。
|
||||||
* 当 API 返回的是 URL 而非二进制数据时,使用 urlToBase64(url) 转换。
|
* 当 API 返回的是 URL 而非二进制数据时,使用 urlToBase64(url) 转换。
|
||||||
*
|
*
|
||||||
* 7. ReferenceList 与 VideoMode 说明
|
* 7. ReferenceList 与 VideoMode 说明
|
||||||
* ReferenceList 是统一的多媒体引用类型,每个条目包含:
|
* ReferenceList 是统一的多媒体引用类型,每个条目包含:
|
||||||
* - type: "image" | "audio" | "video"(媒体类型)
|
* - type: "image" | "audio" | "video"(媒体类型)
|
||||||
* - sourceType: "base64"(当前模板固定为 base64)
|
* - sourceType: "base64"(当前模板固定为 base64)
|
||||||
* - base64(对应的数据)
|
* - base64(对应的数据)
|
||||||
*
|
*
|
||||||
* VideoMode 定义了视频模型支持的输入模式:
|
* VideoMode 定义了视频模型支持的输入模式:
|
||||||
* - "text":纯文本生成视频
|
* - "text":纯文本生成视频
|
||||||
* - "singleImage":单张首帧图片
|
* - "singleImage":单张首帧图片
|
||||||
* - "startEndRequired":首尾帧(两张都必须提供)
|
* - "startEndRequired":首尾帧(两张都必须提供)
|
||||||
* - "endFrameOptional":首尾帧(尾帧可选)
|
* - "endFrameOptional":首尾帧(尾帧可选)
|
||||||
* - "startFrameOptional":首尾帧(首帧可选)
|
* - "startFrameOptional":首尾帧(首帧可选)
|
||||||
* - 数组形式如 ["imageReference:9", "videoReference:3", "audioReference:3"]:
|
* - 数组形式如 ["imageReference:9", "videoReference:3", "audioReference:3"]:
|
||||||
* 多模态参考模式,数字表示该类型的最大数量限制。
|
* 多模态参考模式,数字表示该类型的最大数量限制。
|
||||||
*
|
*
|
||||||
* 在 videoRequest 中,config.mode 表示当前选择的模式,需根据其值决定:
|
* 在 videoRequest 中,config.mode 表示当前选择的模式,需根据其值决定:
|
||||||
* - 如何从 config.referenceList 中提取对应类型的引用
|
* - 如何从 config.referenceList 中提取对应类型的引用
|
||||||
* - 如何构造 API 请求体中的图片/视频/音频参数
|
* - 如何构造 API 请求体中的图片/视频/音频参数
|
||||||
*
|
*
|
||||||
* 8. 异步任务处理
|
* 8. 异步任务处理
|
||||||
* 对于视频生成等需要轮询的异步任务,使用全局的 pollTask 函数:
|
* 对于视频生成等需要轮询的异步任务,使用全局的 pollTask 函数:
|
||||||
* const result = await pollTask(async () => {
|
* const result = await pollTask(async () => {
|
||||||
* const resp = await axios.get(...);
|
* const resp = await axios.get(...);
|
||||||
* if (resp.data.status === "SUCCESS") return { completed: true, data: resp.data.url };
|
* if (resp.data.status === "SUCCESS") return { completed: true, data: resp.data.url };
|
||||||
* if (resp.data.status === "FAILED") return { completed: true, error: resp.data.message };
|
* if (resp.data.status === "FAILED") return { completed: true, error: resp.data.message };
|
||||||
* return { completed: false };
|
* return { completed: false };
|
||||||
* }, 5000, 600000); // 每5秒轮询,10分钟超时
|
* }, 5000, 600000); // 每5秒轮询,10分钟超时
|
||||||
* if (result.error) throw new Error(result.error);
|
* if (result.error) throw new Error(result.error);
|
||||||
* return await urlToBase64(result.data!);
|
* return await urlToBase64(result.data!);
|
||||||
*
|
*
|
||||||
* 9. 错误处理
|
* 9. 错误处理
|
||||||
* 在每个函数开头校验必需参数(如 API Key),缺失时使用 throw new Error("...") 抛出。
|
* 在每个函数开头校验必需参数(如 API Key),缺失时使用 throw new Error("...") 抛出。
|
||||||
* API 请求失败时,从响应中提取有意义的错误信息抛出,不要吞掉异常。
|
* API 请求失败时,从响应中提取有意义的错误信息抛出,不要吞掉异常。
|
||||||
*
|
*
|
||||||
* 10. 日志输出
|
* 10. 日志输出
|
||||||
* 在关键步骤使用 logger("...") 输出日志(如"开始提交任务"、"任务ID: xxx"、"轮询中..."),
|
* 在关键步骤使用 logger("...") 输出日志(如"开始提交任务"、"任务ID: xxx"、"轮询中..."),
|
||||||
* 便于调试。
|
* 便于调试。
|
||||||
*
|
*
|
||||||
* 11. vendor 配置填写
|
* 11. vendor 配置填写
|
||||||
* - id:纯英文小写,作为文件名使用,禁止特殊符号和空格。
|
* - id:纯英文小写,作为文件名使用,禁止特殊符号和空格。
|
||||||
* - version:语义化版本格式 "x.y"。
|
* - version:语义化版本格式 "x.y"。
|
||||||
* - inputs:根据目标 API 所需的认证信息配置(API Key、Secret、请求地址等)。
|
* - inputs:根据目标 API 所需的认证信息配置(API Key、Secret、请求地址等)。
|
||||||
* - models:根据目标平台支持的模型列表填写,注意正确设置 type 和各模型特有字段。
|
* - models:根据目标平台支持的模型列表填写,注意正确设置 type 和各模型特有字段。
|
||||||
* - VideoModel 的 mode 对应 API 支持的输入模式(参见规则 7 的 VideoMode 说明)。
|
* - VideoModel 的 mode 对应 API 支持的输入模式(参见规则 7 的 VideoMode 说明)。
|
||||||
* - VideoModel 的 audio 字段:true(始终生成音频)、false(不生成)、"optional"(用户可选)。
|
* - VideoModel 的 audio 字段:true(始终生成音频)、false(不生成)、"optional"(用户可选)。
|
||||||
* - VideoModel 的 durationResolutionMap 对应各时长下可选的分辨率。
|
* - VideoModel 的 durationResolutionMap 对应各时长下可选的分辨率。
|
||||||
* - VideoModel 的 associationSkills 可选,用于描述模型的特殊能力。
|
* - VideoModel 的 associationSkills 可选,用于描述模型的特殊能力。
|
||||||
* - ImageModel 的 mode 对应 API 支持的生图模式("text" 纯文本、"singleImage" 单图参考、"multiReference" 多图参考)。
|
* - ImageModel 的 mode 对应 API 支持的生图模式("text" 纯文本、"singleImage" 单图参考、"multiReference" 多图参考)。
|
||||||
* - TTSModel 的 voices 对应可选的音色列表。
|
* - TTSModel 的 voices 对应可选的音色列表。
|
||||||
*
|
*
|
||||||
* 12. 图片处理
|
* 12. 图片处理
|
||||||
* - 需要压缩图片体积时使用 zipImage(base64, maxSizeKB)。
|
* - 需要压缩图片体积时使用 zipImage(base64, maxSizeKB)。
|
||||||
* - 需要调整图片分辨率时使用 zipImageResolution(base64, width, height)。
|
* - 需要调整图片分辨率时使用 zipImageResolution(base64, width, height)。
|
||||||
* - 需要将多张图片拼合为一张时使用 mergeImages(base64Arr, maxSize)。
|
* - 需要将多张图片拼合为一张时使用 mergeImages(base64Arr, maxSize)。
|
||||||
* - 以上函数均接收和返回有头 base64 字符串。
|
* - 以上函数均接收和返回有头 base64 字符串。
|
||||||
*
|
*
|
||||||
* 13. 文件结构
|
* 13. 文件结构
|
||||||
* 生成的代码必须保持本模板的整体结构:
|
* 生成的代码必须保持本模板的整体结构:
|
||||||
* 类型定义区 → 全局声明区 → 供应商配置区 → [辅助工具区(可选)] → 适配器函数区 → 导出区
|
* 类型定义区 → 全局声明区 → 供应商配置区 → [辅助工具区(可选)] → 适配器函数区 → 导出区
|
||||||
* 不要打乱顺序,不要删除已有的结构注释分隔线。
|
* 不要打乱顺序,不要删除已有的结构注释分隔线。
|
||||||
* 辅助工具区用于放置多个适配器函数共享的小驼峰命名辅助函数(如 getHeaders、getBaseUrl)。
|
* 辅助工具区用于放置多个适配器函数共享的小驼峰命名辅助函数(如 getHeaders、getBaseUrl)。
|
||||||
*
|
*
|
||||||
* 14. 导出规范
|
* 14. 导出规范
|
||||||
* 必须导出以下字段(通过 exports.xxx = xxx 赋值):
|
* 必须导出以下字段(通过 exports.xxx = xxx 赋值):
|
||||||
* - exports.vendor(必须)
|
* - exports.vendor(必须)
|
||||||
* - exports.textRequest(必须)
|
* - exports.textRequest(必须)
|
||||||
* - exports.imageRequest(必须)
|
* - exports.imageRequest(必须)
|
||||||
* - exports.videoRequest(必须)
|
* - exports.videoRequest(必须)
|
||||||
* - exports.ttsRequest(必须)
|
* - exports.ttsRequest(必须)
|
||||||
* - exports.checkForUpdates(可选)
|
* - exports.checkForUpdates(可选)
|
||||||
* - exports.updateVendor(可选)
|
* - exports.updateVendor(可选)
|
||||||
* 未实现的适配器函数保留空实现(return ""),不可省略导出。
|
* 未实现的适配器函数保留空实现(return ""),不可省略导出。
|
||||||
* 文件末尾必须包含 export {}; 以确保文件被识别为模块。
|
* 文件末尾必须包含 export {}; 以确保文件被识别为模块。
|
||||||
*
|
*
|
||||||
* 【生成流程】
|
* 【生成流程】
|
||||||
* 当用户请求生成新的供应商适配时:
|
* 当用户请求生成新的供应商适配时:
|
||||||
* 1. 确认用户已提供 curl 示例或 API 文档。
|
* 1. 确认用户已提供 curl 示例或 API 文档。
|
||||||
* 2. 分析 API 的认证方式、端点地址、请求/响应结构。
|
* 2. 分析 API 的认证方式、端点地址、请求/响应结构。
|
||||||
* 3. 基于本模板结构,填充 vendor 配置和对应的适配器函数。
|
* 3. 基于本模板结构,填充 vendor 配置和对应的适配器函数。
|
||||||
* 4. 根据当前模板的 ReferenceList 定义,按 base64 形式构造和消费 referenceList。
|
* 4. 根据当前模板的 ReferenceList 定义,按 base64 形式构造和消费 referenceList。
|
||||||
* 5. 仅实现用户需要的模型类型,未用到的函数保留空实现(return "")。
|
* 5. 仅实现用户需要的模型类型,未用到的函数保留空实现(return "")。
|
||||||
* 6. 生成完整可用的代码,确保无语法错误、无遗漏导出。
|
* 6. 生成完整可用的代码,确保无语法错误、无遗漏导出。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
1.1.7
|
1.1.7
|
||||||
Loading…
x
Reference in New Issue
Block a user