koishi-plugin-aka-ai-generator 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -42,6 +42,9 @@ export interface Config {
42
42
  securityBlockWindow: number;
43
43
  securityBlockWarningThreshold: number;
44
44
  enableVideoGeneration: boolean;
45
+ videoProvider: 'yunwu';
46
+ videoApiKey: string;
47
+ videoApiBase: string;
45
48
  videoModelId: string;
46
49
  videoMaxWaitTime: number;
47
50
  videoCreditsMultiplier: number;
package/lib/index.js CHANGED
@@ -886,7 +886,7 @@ var YunwuVideoProvider = class {
886
886
  }
887
887
  /**
888
888
  * 创建视频生成任务
889
- * API: POST /v1/videos/generations
889
+ * API: POST /v1/video/create
890
890
  */
891
891
  async createVideoTask(prompt, imageUrl, options) {
892
892
  const { logger, ctx } = this.config;
@@ -898,64 +898,127 @@ var YunwuVideoProvider = class {
898
898
  this.config.apiTimeout,
899
899
  logger
900
900
  );
901
+ let orientation = "landscape";
902
+ if (options?.aspectRatio === "9:16") {
903
+ orientation = "portrait";
904
+ } else if (options?.aspectRatio === "1:1") {
905
+ orientation = "portrait";
906
+ }
907
+ let duration = options?.duration || 15;
908
+ if (duration < 15) {
909
+ duration = 15;
910
+ } else if (duration > 25) {
911
+ duration = 25;
912
+ } else if (duration <= 20) {
913
+ duration = 15;
914
+ } else {
915
+ duration = 25;
916
+ }
901
917
  const requestBody = {
918
+ images: [`data:${mimeType};base64,${imageBase64}`],
902
919
  model: this.config.modelId,
920
+ orientation,
903
921
  prompt,
904
- image: `data:${mimeType};base64,${imageBase64}`,
905
- duration: options?.duration || 5,
906
- aspect_ratio: options?.aspectRatio || "16:9"
922
+ size: "large",
923
+ // 高清1080p
924
+ duration,
925
+ watermark: true,
926
+ // 默认优先无水印,出错会兜底到有水印
927
+ private: false
928
+ // 默认视频会发布
907
929
  };
908
- if (options?.fps) {
909
- requestBody.fps = options.fps;
910
- }
911
930
  logger?.info("提交视频生成任务", {
912
931
  model: this.config.modelId,
913
932
  promptLength: prompt.length,
914
933
  duration: requestBody.duration,
915
- aspectRatio: requestBody.aspect_ratio
934
+ orientation: requestBody.orientation
916
935
  });
917
936
  const response = await ctx.http.post(
918
- `${this.config.apiBase}/v1/videos/generations`,
937
+ `${this.config.apiBase}/v1/video/create`,
919
938
  requestBody,
920
939
  {
921
940
  headers: {
922
941
  "Authorization": `Bearer ${this.config.apiKey}`,
923
- "Content-Type": "application/json"
942
+ "Content-Type": "application/json",
943
+ "Accept": "application/json"
924
944
  },
925
945
  timeout: this.config.apiTimeout * 1e3
926
946
  }
927
947
  );
928
948
  if (response.error) {
929
- throw new Error(sanitizeString(response.error.message || "创建任务失败"));
949
+ const errorMsg = response.error.message || response.error.type || "创建任务失败";
950
+ throw new Error(sanitizeString(errorMsg));
930
951
  }
931
- const taskId = response.id || response.task_id || response.data?.id;
952
+ if (response.status && response.status >= 400) {
953
+ const errorMsg = response.data?.error?.message || response.data?.error || response.statusText || "创建任务失败";
954
+ logger?.error("API 返回错误状态", { status: response.status, error: errorMsg, response: response.data });
955
+ throw new Error(sanitizeString(errorMsg));
956
+ }
957
+ const taskId = response.id || response.data?.id;
932
958
  if (!taskId) {
933
959
  logger?.error("未能获取任务ID", { response });
934
- throw new Error("未能获取任务ID");
960
+ throw new Error("未能获取任务ID,请检查 API 响应格式");
935
961
  }
936
962
  logger?.info("视频任务已创建", { taskId });
937
963
  return taskId;
938
964
  } catch (error) {
939
965
  logger?.error("创建视频任务失败", { error: sanitizeError(error) });
940
- throw new Error(`创建视频任务失败: ${sanitizeString(error.message)}`);
966
+ let errorMessage = error.message || "创建视频任务失败";
967
+ if (error.response) {
968
+ const responseData = error.response.data;
969
+ if (responseData?.error) {
970
+ errorMessage = responseData.error.message || responseData.error.type || errorMessage;
971
+ } else if (responseData?.message) {
972
+ errorMessage = responseData.message;
973
+ }
974
+ } else if (error.response?.data?.error) {
975
+ errorMessage = error.response.data.error.message || error.response.data.error.type || errorMessage;
976
+ }
977
+ throw new Error(`创建视频任务失败: ${sanitizeString(errorMessage)}`);
941
978
  }
942
979
  }
943
980
  /**
944
981
  * 查询任务状态
945
- * API: GET /v1/videos/generations/{taskId}
982
+ * API: GET /v1/video/{taskId} (根据创建端点的模式推断)
946
983
  */
947
984
  async queryTaskStatus(taskId) {
948
985
  const { logger, ctx } = this.config;
949
986
  try {
950
- const response = await ctx.http.get(
951
- `${this.config.apiBase}/v1/videos/generations/${taskId}`,
952
- {
953
- headers: {
954
- "Authorization": `Bearer ${this.config.apiKey}`
955
- },
956
- timeout: this.config.apiTimeout * 1e3
987
+ const possibleEndpoints = [
988
+ `/v1/video/${taskId}`,
989
+ `/v1/video/status/${taskId}`,
990
+ `/v1/videos/generations/${taskId}`
991
+ ];
992
+ let response = null;
993
+ let lastError = null;
994
+ for (const endpoint of possibleEndpoints) {
995
+ try {
996
+ response = await ctx.http.get(
997
+ `${this.config.apiBase}${endpoint}`,
998
+ {
999
+ headers: {
1000
+ "Authorization": `Bearer ${this.config.apiKey}`,
1001
+ "Accept": "application/json"
1002
+ },
1003
+ timeout: this.config.apiTimeout * 1e3
1004
+ }
1005
+ );
1006
+ if (response && !response.error) {
1007
+ logger?.debug("查询任务状态成功", { taskId, endpoint });
1008
+ break;
1009
+ }
1010
+ } catch (err) {
1011
+ lastError = err;
1012
+ if (err.response?.status === 404 && possibleEndpoints.indexOf(endpoint) < possibleEndpoints.length - 1) {
1013
+ logger?.debug("尝试下一个查询端点", { taskId, endpoint, nextEndpoint: possibleEndpoints[possibleEndpoints.indexOf(endpoint) + 1] });
1014
+ continue;
1015
+ }
1016
+ throw err;
957
1017
  }
958
- );
1018
+ }
1019
+ if (!response) {
1020
+ throw lastError || new Error("所有查询端点都失败");
1021
+ }
959
1022
  const status = response.status || response.data?.status || "pending";
960
1023
  const videoUrl = response.video_url || response.url || response.data?.video_url || response.data?.url;
961
1024
  return {
@@ -967,7 +1030,11 @@ var YunwuVideoProvider = class {
967
1030
  };
968
1031
  } catch (error) {
969
1032
  logger?.error("查询任务状态失败", { taskId, error: sanitizeError(error) });
970
- throw new Error(`查询任务失败: ${sanitizeString(error.message)}`);
1033
+ let errorMessage = error.message || "查询任务失败";
1034
+ if (error.response?.data?.error) {
1035
+ errorMessage = error.response.data.error.message || error.response.data.error.type || errorMessage;
1036
+ }
1037
+ throw new Error(`查询任务失败: ${sanitizeString(errorMessage)}`);
971
1038
  }
972
1039
  }
973
1040
  /**
@@ -1586,28 +1653,27 @@ var Config = import_koishi2.Schema.intersect([
1586
1653
  prompts: import_koishi2.Schema.array(StyleItemSchema).role("table").default([]).description("属于该类型的 prompt 列表")
1587
1654
  })).role("table").default({}).description("按类型管理的 prompt 组,键名即为分组名称")
1588
1655
  }),
1589
- // 视频生成配置
1656
+ // 视频生成配置(独立于图像生成配置)
1590
1657
  import_koishi2.Schema.object({
1591
1658
  enableVideoGeneration: import_koishi2.Schema.boolean().default(false).description("启用图生成视频功能(消耗较大,需谨慎开启)"),
1659
+ videoProvider: import_koishi2.Schema.union([
1660
+ import_koishi2.Schema.const("yunwu").description("云雾服务")
1661
+ ]).default("yunwu").description("视频生成供应商(目前只支持云雾)"),
1662
+ videoApiKey: import_koishi2.Schema.string().description("视频生成 API 密钥(独立于图像生成配置)").role("secret").default(""),
1663
+ videoApiBase: import_koishi2.Schema.string().default("https://yunwu.ai").description("视频生成 API 地址"),
1592
1664
  videoModelId: import_koishi2.Schema.string().default("sora-2").description("视频生成模型ID (sora-2 或 sora-2-pro)"),
1593
1665
  videoMaxWaitTime: import_koishi2.Schema.number().default(300).min(60).max(600).description("视频生成最大等待时间(秒),超时后可异步查询"),
1594
1666
  videoCreditsMultiplier: import_koishi2.Schema.number().default(5).min(1).max(20).description("视频生成积分倍数(相对于图片生成,默认5倍)"),
1595
1667
  videoStyles: import_koishi2.Schema.array(import_koishi2.Schema.object({
1596
1668
  commandName: import_koishi2.Schema.string().required().description("命令名称").role("table-cell", { width: 100 }),
1597
1669
  prompt: import_koishi2.Schema.string().role("textarea", { rows: 2 }).required().description("视频描述 prompt"),
1598
- duration: import_koishi2.Schema.number().min(3).max(10).description("视频时长(秒)"),
1670
+ duration: import_koishi2.Schema.number().default(15).description("视频时长(秒,仅支持 15 或 25)"),
1599
1671
  aspectRatio: import_koishi2.Schema.string().description("宽高比(如 16:9)")
1600
1672
  })).role("table").default([
1601
1673
  {
1602
- commandName: "慢动作视频",
1603
- prompt: "缓慢流畅的运镜,展现细节和质感",
1604
- duration: 8,
1605
- aspectRatio: "16:9"
1606
- },
1607
- {
1608
- commandName: "快节奏视频",
1609
- prompt: "快速动态的场景变化,充满活力",
1610
- duration: 5,
1674
+ commandName: "变视频",
1675
+ prompt: "将该图片生成一段符合产品展现的流畅视频",
1676
+ duration: 15,
1611
1677
  aspectRatio: "16:9"
1612
1678
  }
1613
1679
  ]).description("视频风格预设")
@@ -1636,16 +1702,22 @@ function apply(ctx, config) {
1636
1702
  const modelMappingIndex = buildModelMappingIndex(config.modelMappings);
1637
1703
  let videoProvider = null;
1638
1704
  if (config.enableVideoGeneration) {
1639
- videoProvider = new YunwuVideoProvider({
1640
- apiKey: config.yunwuApiKey,
1641
- modelId: config.videoModelId,
1642
- apiBase: "https://yunwu.ai",
1643
- apiTimeout: config.apiTimeout,
1644
- logLevel: config.logLevel,
1645
- logger,
1646
- ctx
1647
- });
1648
- logger.info(`视频生成功能已启用 (模型: ${config.videoModelId})`);
1705
+ if (!config.videoApiKey) {
1706
+ logger.warn("视频生成功能已启用,但未配置视频 API 密钥,视频功能将不可用");
1707
+ } else if (config.videoProvider !== "yunwu") {
1708
+ logger.warn(`视频生成供应商 ${config.videoProvider} 暂不支持,仅支持 yunwu`);
1709
+ } else {
1710
+ videoProvider = new YunwuVideoProvider({
1711
+ apiKey: config.videoApiKey,
1712
+ modelId: config.videoModelId,
1713
+ apiBase: config.videoApiBase,
1714
+ apiTimeout: config.apiTimeout,
1715
+ logLevel: config.logLevel,
1716
+ logger,
1717
+ ctx
1718
+ });
1719
+ logger.info(`视频生成功能已启用 (供应商: ${config.videoProvider}, 模型: ${config.videoModelId}, API: ${config.videoApiBase})`);
1720
+ }
1649
1721
  }
1650
1722
  const styleDefinitions = collectStyleDefinitions();
1651
1723
  function collectStyleDefinitions() {
@@ -2137,7 +2209,7 @@ ${infoParts.join("\n")}`;
2137
2209
  }
2138
2210
  }
2139
2211
  if (config.enableVideoGeneration && videoProvider) {
2140
- ctx.command("图生视频 [img:text]", "根据图片和描述生成视频").option("duration", "-d <duration:number> 视频时长(3-10秒)").option("ratio", "-r <ratio:string> 宽高比(16:9, 9:16, 1:1)").action(async ({ session, options }, img) => {
2212
+ ctx.command("图生视频 [img:text]", "根据图片和描述生成视频").option("duration", "-d <duration:number> 视频时长(15 或 25 秒)").option("ratio", "-r <ratio:string> 宽高比(16:9, 9:16, 1:1)").action(async ({ session, options }, img) => {
2141
2213
  if (!session?.userId) return "会话无效";
2142
2214
  const userId = session.userId;
2143
2215
  const userName = session.username || userId || "未知用户";
@@ -2177,9 +2249,9 @@ ${infoParts.join("\n")}`;
2177
2249
  }
2178
2250
  prompt = text;
2179
2251
  }
2180
- const duration = options?.duration || 5;
2181
- if (duration < 3 || duration > 10) {
2182
- return "视频时长必须在 3-10 秒之间";
2252
+ const duration = options?.duration || 15;
2253
+ if (duration !== 15 && duration !== 25) {
2254
+ return "视频时长必须是 15 或 25 秒";
2183
2255
  }
2184
2256
  const ratio = options?.ratio || "16:9";
2185
2257
  const validRatios = ["16:9", "9:16", "1:1"];
@@ -2289,7 +2361,7 @@ ${infoParts.join("\n")}`;
2289
2361
  finalPrompt,
2290
2362
  imageUrls[0],
2291
2363
  {
2292
- duration: style.duration || 5,
2364
+ duration: style.duration || 15,
2293
2365
  aspectRatio: style.aspectRatio || "16:9"
2294
2366
  },
2295
2367
  config.videoMaxWaitTime
@@ -9,12 +9,12 @@ export declare class YunwuVideoProvider implements VideoProvider {
9
9
  constructor(config: YunwuVideoConfig);
10
10
  /**
11
11
  * 创建视频生成任务
12
- * API: POST /v1/videos/generations
12
+ * API: POST /v1/video/create
13
13
  */
14
14
  createVideoTask(prompt: string, imageUrl: string, options?: VideoGenerationOptions): Promise<string>;
15
15
  /**
16
16
  * 查询任务状态
17
- * API: GET /v1/videos/generations/{taskId}
17
+ * API: GET /v1/video/{taskId} (根据创建端点的模式推断)
18
18
  */
19
19
  queryTaskStatus(taskId: string): Promise<VideoTaskStatus>;
20
20
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-aka-ai-generator",
3
3
  "description": "自用AI生成插件(GPTGod & Yunwu)",
4
- "version": "0.7.0",
4
+ "version": "0.7.3",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [