koishi-plugin-aka-ai-generator 0.7.4 → 0.7.6

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.js CHANGED
@@ -954,12 +954,12 @@ var YunwuVideoProvider = class {
954
954
  logger?.error("API 返回错误状态", { status: response.status, error: errorMsg, response: response.data });
955
955
  throw new Error(sanitizeString(errorMsg));
956
956
  }
957
- const taskId = response.id || response.data?.id;
957
+ const taskId = response.id;
958
958
  if (!taskId) {
959
959
  logger?.error("未能获取任务ID", { response });
960
960
  throw new Error("未能获取任务ID,请检查 API 响应格式");
961
961
  }
962
- logger?.info("视频任务已创建", { taskId });
962
+ logger?.info("视频任务已创建", { taskId, status: response.status });
963
963
  return taskId;
964
964
  } catch (error) {
965
965
  logger?.error("创建视频任务失败", { error: sanitizeError(error) });
@@ -979,12 +979,12 @@ var YunwuVideoProvider = class {
979
979
  }
980
980
  /**
981
981
  * 查询任务状态
982
- * API: GET /v1/video/{taskId} (根据创建端点的模式推断)
982
+ * API: GET /v1/video/query?id={taskId}
983
983
  */
984
984
  async queryTaskStatus(taskId) {
985
985
  const { logger, ctx } = this.config;
986
986
  try {
987
- const endpoint = `/v1/video/${taskId}`;
987
+ const endpoint = `/v1/video/query?id=${encodeURIComponent(taskId)}`;
988
988
  logger?.debug("查询任务状态", { taskId, endpoint });
989
989
  const response = await ctx.http.get(
990
990
  `${this.config.apiBase}${endpoint}`,
@@ -996,14 +996,14 @@ var YunwuVideoProvider = class {
996
996
  timeout: this.config.apiTimeout * 1e3
997
997
  }
998
998
  );
999
- const status = response.status || response.data?.status || "pending";
1000
- const videoUrl = response.video_url || response.url || response.data?.video_url || response.data?.url;
999
+ const status = response.status || "pending";
1000
+ const videoUrl = response.video_url || null;
1001
1001
  return {
1002
1002
  status,
1003
- taskId,
1004
- videoUrl,
1005
- error: response.error || response.data?.error,
1006
- progress: response.progress || response.data?.progress
1003
+ taskId: response.id || taskId,
1004
+ videoUrl: videoUrl || void 0,
1005
+ error: response.error,
1006
+ progress: response.progress
1007
1007
  };
1008
1008
  } catch (error) {
1009
1009
  logger?.error("查询任务状态失败", { taskId, error: sanitizeError(error) });
@@ -1020,6 +1020,8 @@ var YunwuVideoProvider = class {
1020
1020
  async pollTaskCompletion(taskId, maxWaitTime = 300, pollInterval = 3, onProgress) {
1021
1021
  const { logger } = this.config;
1022
1022
  const startTime = Date.now();
1023
+ let consecutiveFailures = 0;
1024
+ const maxConsecutiveFailures = 5;
1023
1025
  while (true) {
1024
1026
  const elapsed = (Date.now() - startTime) / 1e3;
1025
1027
  if (elapsed > maxWaitTime) {
@@ -1027,21 +1029,50 @@ var YunwuVideoProvider = class {
1027
1029
  throw new Error(`视频生成超时(已等待${Math.floor(elapsed)}秒),任务ID: ${taskId}
1028
1030
  请使用"查询视频 ${taskId}"命令稍后查询结果`);
1029
1031
  }
1030
- const status = await this.queryTaskStatus(taskId);
1031
- logger?.debug("任务状态", { taskId, status: status.status, elapsed: Math.floor(elapsed) });
1032
- if (onProgress) {
1033
- await onProgress(status);
1034
- }
1035
- if (status.status === "completed" && status.videoUrl) {
1036
- logger?.info("视频生成完成", { taskId, elapsed: Math.floor(elapsed) });
1037
- return status.videoUrl;
1038
- }
1039
- if (status.status === "failed") {
1040
- throw new Error(status.error || "视频生成失败");
1032
+ try {
1033
+ const status = await this.queryTaskStatus(taskId);
1034
+ consecutiveFailures = 0;
1035
+ logger?.debug("任务状态", { taskId, status: status.status, elapsed: Math.floor(elapsed) });
1036
+ if (onProgress) {
1037
+ await onProgress(status);
1038
+ }
1039
+ if (status.status === "completed" && status.videoUrl) {
1040
+ logger?.info("视频生成完成", { taskId, elapsed: Math.floor(elapsed) });
1041
+ return status.videoUrl;
1042
+ }
1043
+ if (status.status === "failed") {
1044
+ throw new Error(status.error || "视频生成失败");
1045
+ }
1046
+ } catch (error) {
1047
+ consecutiveFailures++;
1048
+ logger?.warn("查询任务状态失败,继续重试", {
1049
+ taskId,
1050
+ consecutiveFailures,
1051
+ maxConsecutiveFailures,
1052
+ error: sanitizeError(error),
1053
+ elapsed: Math.floor(elapsed)
1054
+ });
1055
+ if (consecutiveFailures >= maxConsecutiveFailures) {
1056
+ logger?.error("连续查询失败次数过多,终止轮询", {
1057
+ taskId,
1058
+ consecutiveFailures,
1059
+ elapsed: Math.floor(elapsed)
1060
+ });
1061
+ throw new Error(`查询任务状态连续失败 ${consecutiveFailures} 次,任务ID: ${taskId}
1062
+ 请稍后使用"查询视频 ${taskId}"命令手动查询结果`);
1063
+ }
1064
+ await new Promise((resolve) => setTimeout(resolve, pollInterval * 1e3 * 2));
1065
+ continue;
1041
1066
  }
1042
1067
  await new Promise((resolve) => setTimeout(resolve, pollInterval * 1e3));
1043
1068
  }
1044
1069
  }
1070
+ /**
1071
+ * 等待指定任务完成并返回视频 URL(供外部在拿到 taskId 后自行控制扣费/提示)
1072
+ */
1073
+ async waitForVideo(taskId, maxWaitTime = 300) {
1074
+ return await this.pollTaskCompletion(taskId, maxWaitTime);
1075
+ }
1045
1076
  /**
1046
1077
  * 生成视频(主入口)
1047
1078
  */
@@ -1052,7 +1083,7 @@ var YunwuVideoProvider = class {
1052
1083
  const taskId = await this.createVideoTask(prompt, imageUrl, options);
1053
1084
  logger?.debug("任务已创建,等待 3 秒后开始查询", { taskId });
1054
1085
  await new Promise((resolve) => setTimeout(resolve, 3e3));
1055
- const videoUrl = await this.pollTaskCompletion(taskId, maxWaitTime);
1086
+ const videoUrl = await this.waitForVideo(taskId, maxWaitTime);
1056
1087
  logger?.info("视频生成完成", { taskId, videoUrl });
1057
1088
  return videoUrl;
1058
1089
  } catch (error) {
@@ -1132,11 +1163,14 @@ var UserManager = class {
1132
1163
  dataFile;
1133
1164
  backupFile;
1134
1165
  rechargeHistoryFile;
1166
+ pendingVideoTasksFile;
1135
1167
  logger;
1136
1168
  dataLock = new AsyncLock();
1137
1169
  historyLock = new AsyncLock();
1170
+ pendingLock = new AsyncLock();
1138
1171
  // 内存缓存
1139
1172
  usersCache = null;
1173
+ pendingVideoCache = null;
1140
1174
  activeTasks = /* @__PURE__ */ new Map();
1141
1175
  // userId -> requestId
1142
1176
  rateLimitMap = /* @__PURE__ */ new Map();
@@ -1151,6 +1185,7 @@ var UserManager = class {
1151
1185
  this.dataFile = (0, import_path.join)(this.dataDir, "users_data.json");
1152
1186
  this.backupFile = (0, import_path.join)(this.dataDir, "users_data.json.backup");
1153
1187
  this.rechargeHistoryFile = (0, import_path.join)(this.dataDir, "recharge_history.json");
1188
+ this.pendingVideoTasksFile = (0, import_path.join)(this.dataDir, "pending_video_tasks.json");
1154
1189
  if (!(0, import_fs.existsSync)(this.dataDir)) {
1155
1190
  (0, import_fs.mkdirSync)(this.dataDir, { recursive: true });
1156
1191
  }
@@ -1212,6 +1247,83 @@ var UserManager = class {
1212
1247
  throw error;
1213
1248
  }
1214
1249
  }
1250
+ // --- 待结算视频任务(防止超时套利) ---
1251
+ async loadPendingVideoTasks() {
1252
+ if (this.pendingVideoCache) return this.pendingVideoCache;
1253
+ return await this.pendingLock.acquire(async () => {
1254
+ if (this.pendingVideoCache) return this.pendingVideoCache;
1255
+ try {
1256
+ if ((0, import_fs.existsSync)(this.pendingVideoTasksFile)) {
1257
+ const data = await import_fs.promises.readFile(this.pendingVideoTasksFile, "utf-8");
1258
+ const parsed = JSON.parse(data);
1259
+ this.pendingVideoCache = {
1260
+ version: parsed?.version || "1.0.0",
1261
+ lastUpdate: parsed?.lastUpdate || (/* @__PURE__ */ new Date()).toISOString(),
1262
+ tasks: parsed?.tasks || {}
1263
+ };
1264
+ return this.pendingVideoCache;
1265
+ }
1266
+ } catch (error) {
1267
+ this.logger.error("读取待结算视频任务失败", error);
1268
+ }
1269
+ this.pendingVideoCache = {
1270
+ version: "1.0.0",
1271
+ lastUpdate: (/* @__PURE__ */ new Date()).toISOString(),
1272
+ tasks: {}
1273
+ };
1274
+ return this.pendingVideoCache;
1275
+ });
1276
+ }
1277
+ async savePendingVideoTasksInternal() {
1278
+ if (!this.pendingVideoCache) return;
1279
+ try {
1280
+ this.pendingVideoCache.lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
1281
+ await import_fs.promises.writeFile(this.pendingVideoTasksFile, JSON.stringify(this.pendingVideoCache, null, 2), "utf-8");
1282
+ } catch (error) {
1283
+ this.logger.error("保存待结算视频任务失败", error);
1284
+ throw error;
1285
+ }
1286
+ }
1287
+ /**
1288
+ * 记录一个待结算的视频任务(任务创建成功后立即写入,避免“超时但最终成功”不扣费)
1289
+ */
1290
+ async addPendingVideoTask(task) {
1291
+ await this.loadPendingVideoTasks();
1292
+ await this.pendingLock.acquire(async () => {
1293
+ if (!this.pendingVideoCache) {
1294
+ this.pendingVideoCache = { version: "1.0.0", lastUpdate: (/* @__PURE__ */ new Date()).toISOString(), tasks: {} };
1295
+ }
1296
+ if (this.pendingVideoCache.tasks[task.taskId]) return;
1297
+ this.pendingVideoCache.tasks[task.taskId] = task;
1298
+ await this.savePendingVideoTasksInternal();
1299
+ });
1300
+ }
1301
+ async getPendingVideoTask(taskId) {
1302
+ const data = await this.loadPendingVideoTasks();
1303
+ return data.tasks[taskId] || null;
1304
+ }
1305
+ async markPendingVideoTaskCharged(taskId) {
1306
+ await this.loadPendingVideoTasks();
1307
+ return await this.pendingLock.acquire(async () => {
1308
+ if (!this.pendingVideoCache) return null;
1309
+ const task = this.pendingVideoCache.tasks[taskId];
1310
+ if (!task) return null;
1311
+ if (task.charged) return task;
1312
+ task.charged = true;
1313
+ task.chargedAt = (/* @__PURE__ */ new Date()).toISOString();
1314
+ await this.savePendingVideoTasksInternal();
1315
+ return task;
1316
+ });
1317
+ }
1318
+ async deletePendingVideoTask(taskId) {
1319
+ await this.loadPendingVideoTasks();
1320
+ await this.pendingLock.acquire(async () => {
1321
+ if (!this.pendingVideoCache) return;
1322
+ if (!this.pendingVideoCache.tasks[taskId]) return;
1323
+ delete this.pendingVideoCache.tasks[taskId];
1324
+ await this.savePendingVideoTasksInternal();
1325
+ });
1326
+ }
1215
1327
  // 获取特定用户数据
1216
1328
  async getUserData(userId, userName) {
1217
1329
  await this.loadUsersData();
@@ -2205,6 +2317,7 @@ ${infoParts.join("\n")}`;
2205
2317
  if (!userManager.startTask(userId)) {
2206
2318
  return "您有一个任务正在进行中,请等待完成";
2207
2319
  }
2320
+ let createdTaskId = null;
2208
2321
  try {
2209
2322
  const inputResult = await getInputData(session, img, "single");
2210
2323
  if ("error" in inputResult) {
@@ -2246,17 +2359,30 @@ ${infoParts.join("\n")}`;
2246
2359
  💡 提示:生成过程中可继续使用其他功能`
2247
2360
  );
2248
2361
  const startTime = Date.now();
2249
- const videoUrl = await videoProvider.generateVideo(
2362
+ const taskId = await videoProvider.createVideoTask(
2250
2363
  prompt,
2251
2364
  imageUrls[0],
2252
2365
  {
2253
2366
  duration,
2254
2367
  aspectRatio: ratio
2255
- },
2256
- config.videoMaxWaitTime
2368
+ }
2257
2369
  );
2258
- await recordUserUsage(session, "图生视频", videoCredits, false);
2370
+ createdTaskId = taskId;
2371
+ await userManager.addPendingVideoTask({
2372
+ taskId,
2373
+ userId,
2374
+ userName,
2375
+ commandName: "图生视频",
2376
+ credits: videoCredits,
2377
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2378
+ charged: false
2379
+ });
2380
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
2381
+ const videoUrl = await videoProvider.waitForVideo(taskId, config.videoMaxWaitTime);
2259
2382
  await session.send(import_koishi2.h.video(videoUrl));
2383
+ await recordUserUsage(session, "图生视频", videoCredits, false);
2384
+ await userManager.markPendingVideoTaskCharged(taskId);
2385
+ await userManager.deletePendingVideoTask(taskId);
2260
2386
  const totalTime = Math.floor((Date.now() - startTime) / 1e3);
2261
2387
  await session.send(`✅ 视频生成完成!(耗时 ${totalTime} 秒)`);
2262
2388
  } catch (error) {
@@ -2265,6 +2391,10 @@ ${infoParts.join("\n")}`;
2265
2391
  if (errorMsg.includes("任务ID:")) {
2266
2392
  return errorMsg;
2267
2393
  }
2394
+ try {
2395
+ if (createdTaskId) await userManager.deletePendingVideoTask(createdTaskId);
2396
+ } catch {
2397
+ }
2268
2398
  return `视频生成失败:${sanitizeString(errorMsg)}`;
2269
2399
  } finally {
2270
2400
  userManager.endTask(userId);
@@ -2281,7 +2411,17 @@ ${infoParts.join("\n")}`;
2281
2411
  await session.send("🔍 正在查询视频生成状态...");
2282
2412
  const status = await videoProvider.queryTaskStatus(taskId.trim());
2283
2413
  if (status.status === "completed" && status.videoUrl) {
2414
+ const trimmedTaskId = taskId.trim();
2415
+ const pending = await userManager.getPendingVideoTask(trimmedTaskId);
2416
+ if (pending && pending.userId && pending.userId !== session.userId) {
2417
+ return "该任务ID不属于当前用户,无法查询";
2418
+ }
2284
2419
  await session.send(import_koishi2.h.video(status.videoUrl));
2420
+ if (pending && !pending.charged) {
2421
+ await recordUserUsage(session, pending.commandName, pending.credits, false);
2422
+ await userManager.markPendingVideoTaskCharged(trimmedTaskId);
2423
+ await userManager.deletePendingVideoTask(trimmedTaskId);
2424
+ }
2285
2425
  return "✅ 视频已生成完成!";
2286
2426
  } else if (status.status === "processing" || status.status === "pending") {
2287
2427
  const progressText = status.progress ? `(进度:${status.progress}%)` : "";
@@ -2318,6 +2458,7 @@ ${infoParts.join("\n")}`;
2318
2458
  if (!userManager.startTask(userId)) {
2319
2459
  return "您有一个任务正在进行中,请等待完成";
2320
2460
  }
2461
+ let createdTaskId = null;
2321
2462
  try {
2322
2463
  const inputResult = await getInputData(session, img, "single");
2323
2464
  if ("error" in inputResult) {
@@ -2336,17 +2477,30 @@ ${infoParts.join("\n")}`;
2336
2477
  📝 描述:${finalPrompt}
2337
2478
  ⏱️ 预计需要 1-3 分钟`
2338
2479
  );
2339
- const videoUrl = await videoProvider.generateVideo(
2480
+ const taskId = await videoProvider.createVideoTask(
2340
2481
  finalPrompt,
2341
2482
  imageUrls[0],
2342
2483
  {
2343
2484
  duration: style.duration || 15,
2344
2485
  aspectRatio: style.aspectRatio || "16:9"
2345
- },
2346
- config.videoMaxWaitTime
2486
+ }
2347
2487
  );
2348
- await recordUserUsage(session, style.commandName, videoCredits, false);
2488
+ createdTaskId = taskId;
2489
+ await userManager.addPendingVideoTask({
2490
+ taskId,
2491
+ userId,
2492
+ userName,
2493
+ commandName: style.commandName,
2494
+ credits: videoCredits,
2495
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2496
+ charged: false
2497
+ });
2498
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
2499
+ const videoUrl = await videoProvider.waitForVideo(taskId, config.videoMaxWaitTime);
2349
2500
  await session.send(import_koishi2.h.video(videoUrl));
2501
+ await recordUserUsage(session, style.commandName, videoCredits, false);
2502
+ await userManager.markPendingVideoTaskCharged(taskId);
2503
+ await userManager.deletePendingVideoTask(taskId);
2350
2504
  await session.send(`✅ 视频生成完成!`);
2351
2505
  } catch (error) {
2352
2506
  logger.error("视频风格转换失败", { userId, style: style.commandName, error: sanitizeError(error) });
@@ -2354,6 +2508,10 @@ ${infoParts.join("\n")}`;
2354
2508
  if (errorMsg.includes("任务ID:")) {
2355
2509
  return errorMsg;
2356
2510
  }
2511
+ try {
2512
+ if (createdTaskId) await userManager.deletePendingVideoTask(createdTaskId);
2513
+ } catch {
2514
+ }
2357
2515
  return `视频生成失败:${sanitizeString(errorMsg)}`;
2358
2516
  } finally {
2359
2517
  userManager.endTask(userId);
@@ -14,13 +14,17 @@ export declare class YunwuVideoProvider implements VideoProvider {
14
14
  createVideoTask(prompt: string, imageUrl: string, options?: VideoGenerationOptions): Promise<string>;
15
15
  /**
16
16
  * 查询任务状态
17
- * API: GET /v1/video/{taskId} (根据创建端点的模式推断)
17
+ * API: GET /v1/video/query?id={taskId}
18
18
  */
19
19
  queryTaskStatus(taskId: string): Promise<VideoTaskStatus>;
20
20
  /**
21
21
  * 轮询等待任务完成
22
22
  */
23
23
  private pollTaskCompletion;
24
+ /**
25
+ * 等待指定任务完成并返回视频 URL(供外部在拿到 taskId 后自行控制扣费/提示)
26
+ */
27
+ waitForVideo(taskId: string, maxWaitTime?: number): Promise<string>;
24
28
  /**
25
29
  * 生成视频(主入口)
26
30
  */
@@ -39,15 +39,33 @@ export interface RechargeHistory {
39
39
  lastUpdate: string;
40
40
  records: RechargeRecord[];
41
41
  }
42
+ export interface PendingVideoTask {
43
+ taskId: string;
44
+ userId: string;
45
+ userName: string;
46
+ commandName: string;
47
+ credits: number;
48
+ createdAt: string;
49
+ charged: boolean;
50
+ chargedAt?: string;
51
+ }
52
+ export interface PendingVideoTasksData {
53
+ version: string;
54
+ lastUpdate: string;
55
+ tasks: Record<string, PendingVideoTask>;
56
+ }
42
57
  export declare class UserManager {
43
58
  private dataDir;
44
59
  private dataFile;
45
60
  private backupFile;
46
61
  private rechargeHistoryFile;
62
+ private pendingVideoTasksFile;
47
63
  private logger;
48
64
  private dataLock;
49
65
  private historyLock;
66
+ private pendingLock;
50
67
  private usersCache;
68
+ private pendingVideoCache;
51
69
  private activeTasks;
52
70
  private rateLimitMap;
53
71
  private securityBlockMap;
@@ -59,6 +77,15 @@ export declare class UserManager {
59
77
  isAdmin(userId: string, config: Config): boolean;
60
78
  private loadUsersData;
61
79
  private saveUsersDataInternal;
80
+ private loadPendingVideoTasks;
81
+ private savePendingVideoTasksInternal;
82
+ /**
83
+ * 记录一个待结算的视频任务(任务创建成功后立即写入,避免“超时但最终成功”不扣费)
84
+ */
85
+ addPendingVideoTask(task: PendingVideoTask): Promise<void>;
86
+ getPendingVideoTask(taskId: string): Promise<PendingVideoTask | null>;
87
+ markPendingVideoTaskCharged(taskId: string): Promise<PendingVideoTask | null>;
88
+ deletePendingVideoTask(taskId: string): Promise<void>;
62
89
  getUserData(userId: string, userName: string): Promise<UserData>;
63
90
  getAllUsers(): Promise<UsersData>;
64
91
  updateUsersBatch(updates: (data: UsersData) => void): Promise<void>;
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.4",
4
+ "version": "0.7.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [