koishi-plugin-aka-ai-generator 0.7.5 → 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
@@ -1067,6 +1067,12 @@ var YunwuVideoProvider = class {
1067
1067
  await new Promise((resolve) => setTimeout(resolve, pollInterval * 1e3));
1068
1068
  }
1069
1069
  }
1070
+ /**
1071
+ * 等待指定任务完成并返回视频 URL(供外部在拿到 taskId 后自行控制扣费/提示)
1072
+ */
1073
+ async waitForVideo(taskId, maxWaitTime = 300) {
1074
+ return await this.pollTaskCompletion(taskId, maxWaitTime);
1075
+ }
1070
1076
  /**
1071
1077
  * 生成视频(主入口)
1072
1078
  */
@@ -1077,7 +1083,7 @@ var YunwuVideoProvider = class {
1077
1083
  const taskId = await this.createVideoTask(prompt, imageUrl, options);
1078
1084
  logger?.debug("任务已创建,等待 3 秒后开始查询", { taskId });
1079
1085
  await new Promise((resolve) => setTimeout(resolve, 3e3));
1080
- const videoUrl = await this.pollTaskCompletion(taskId, maxWaitTime);
1086
+ const videoUrl = await this.waitForVideo(taskId, maxWaitTime);
1081
1087
  logger?.info("视频生成完成", { taskId, videoUrl });
1082
1088
  return videoUrl;
1083
1089
  } catch (error) {
@@ -1157,11 +1163,14 @@ var UserManager = class {
1157
1163
  dataFile;
1158
1164
  backupFile;
1159
1165
  rechargeHistoryFile;
1166
+ pendingVideoTasksFile;
1160
1167
  logger;
1161
1168
  dataLock = new AsyncLock();
1162
1169
  historyLock = new AsyncLock();
1170
+ pendingLock = new AsyncLock();
1163
1171
  // 内存缓存
1164
1172
  usersCache = null;
1173
+ pendingVideoCache = null;
1165
1174
  activeTasks = /* @__PURE__ */ new Map();
1166
1175
  // userId -> requestId
1167
1176
  rateLimitMap = /* @__PURE__ */ new Map();
@@ -1176,6 +1185,7 @@ var UserManager = class {
1176
1185
  this.dataFile = (0, import_path.join)(this.dataDir, "users_data.json");
1177
1186
  this.backupFile = (0, import_path.join)(this.dataDir, "users_data.json.backup");
1178
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");
1179
1189
  if (!(0, import_fs.existsSync)(this.dataDir)) {
1180
1190
  (0, import_fs.mkdirSync)(this.dataDir, { recursive: true });
1181
1191
  }
@@ -1237,6 +1247,83 @@ var UserManager = class {
1237
1247
  throw error;
1238
1248
  }
1239
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
+ }
1240
1327
  // 获取特定用户数据
1241
1328
  async getUserData(userId, userName) {
1242
1329
  await this.loadUsersData();
@@ -2230,6 +2317,7 @@ ${infoParts.join("\n")}`;
2230
2317
  if (!userManager.startTask(userId)) {
2231
2318
  return "您有一个任务正在进行中,请等待完成";
2232
2319
  }
2320
+ let createdTaskId = null;
2233
2321
  try {
2234
2322
  const inputResult = await getInputData(session, img, "single");
2235
2323
  if ("error" in inputResult) {
@@ -2271,17 +2359,30 @@ ${infoParts.join("\n")}`;
2271
2359
  💡 提示:生成过程中可继续使用其他功能`
2272
2360
  );
2273
2361
  const startTime = Date.now();
2274
- const videoUrl = await videoProvider.generateVideo(
2362
+ const taskId = await videoProvider.createVideoTask(
2275
2363
  prompt,
2276
2364
  imageUrls[0],
2277
2365
  {
2278
2366
  duration,
2279
2367
  aspectRatio: ratio
2280
- },
2281
- config.videoMaxWaitTime
2368
+ }
2282
2369
  );
2283
- 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);
2284
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);
2285
2386
  const totalTime = Math.floor((Date.now() - startTime) / 1e3);
2286
2387
  await session.send(`✅ 视频生成完成!(耗时 ${totalTime} 秒)`);
2287
2388
  } catch (error) {
@@ -2290,6 +2391,10 @@ ${infoParts.join("\n")}`;
2290
2391
  if (errorMsg.includes("任务ID:")) {
2291
2392
  return errorMsg;
2292
2393
  }
2394
+ try {
2395
+ if (createdTaskId) await userManager.deletePendingVideoTask(createdTaskId);
2396
+ } catch {
2397
+ }
2293
2398
  return `视频生成失败:${sanitizeString(errorMsg)}`;
2294
2399
  } finally {
2295
2400
  userManager.endTask(userId);
@@ -2306,7 +2411,17 @@ ${infoParts.join("\n")}`;
2306
2411
  await session.send("🔍 正在查询视频生成状态...");
2307
2412
  const status = await videoProvider.queryTaskStatus(taskId.trim());
2308
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
+ }
2309
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
+ }
2310
2425
  return "✅ 视频已生成完成!";
2311
2426
  } else if (status.status === "processing" || status.status === "pending") {
2312
2427
  const progressText = status.progress ? `(进度:${status.progress}%)` : "";
@@ -2343,6 +2458,7 @@ ${infoParts.join("\n")}`;
2343
2458
  if (!userManager.startTask(userId)) {
2344
2459
  return "您有一个任务正在进行中,请等待完成";
2345
2460
  }
2461
+ let createdTaskId = null;
2346
2462
  try {
2347
2463
  const inputResult = await getInputData(session, img, "single");
2348
2464
  if ("error" in inputResult) {
@@ -2361,17 +2477,30 @@ ${infoParts.join("\n")}`;
2361
2477
  📝 描述:${finalPrompt}
2362
2478
  ⏱️ 预计需要 1-3 分钟`
2363
2479
  );
2364
- const videoUrl = await videoProvider.generateVideo(
2480
+ const taskId = await videoProvider.createVideoTask(
2365
2481
  finalPrompt,
2366
2482
  imageUrls[0],
2367
2483
  {
2368
2484
  duration: style.duration || 15,
2369
2485
  aspectRatio: style.aspectRatio || "16:9"
2370
- },
2371
- config.videoMaxWaitTime
2486
+ }
2372
2487
  );
2373
- 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);
2374
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);
2375
2504
  await session.send(`✅ 视频生成完成!`);
2376
2505
  } catch (error) {
2377
2506
  logger.error("视频风格转换失败", { userId, style: style.commandName, error: sanitizeError(error) });
@@ -2379,6 +2508,10 @@ ${infoParts.join("\n")}`;
2379
2508
  if (errorMsg.includes("任务ID:")) {
2380
2509
  return errorMsg;
2381
2510
  }
2511
+ try {
2512
+ if (createdTaskId) await userManager.deletePendingVideoTask(createdTaskId);
2513
+ } catch {
2514
+ }
2382
2515
  return `视频生成失败:${sanitizeString(errorMsg)}`;
2383
2516
  } finally {
2384
2517
  userManager.endTask(userId);
@@ -21,6 +21,10 @@ export declare class YunwuVideoProvider implements VideoProvider {
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.5",
4
+ "version": "0.7.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [