koishi-plugin-aka-ai-generator 0.7.5 → 0.7.7

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
@@ -914,7 +914,7 @@ var YunwuVideoProvider = class {
914
914
  } else {
915
915
  duration = 25;
916
916
  }
917
- const requestBody = {
917
+ const buildRequestBody = /* @__PURE__ */ __name((watermark) => ({
918
918
  images: [`data:${mimeType};base64,${imageBase64}`],
919
919
  model: this.config.modelId,
920
920
  orientation,
@@ -922,29 +922,42 @@ var YunwuVideoProvider = class {
922
922
  size: "large",
923
923
  // 高清1080p
924
924
  duration,
925
- watermark: true,
926
- // 默认优先无水印,出错会兜底到有水印
925
+ watermark,
926
+ // 优先无水印;若接口不允许则降级有水印
927
927
  private: false
928
928
  // 默认视频会发布
929
- };
929
+ }), "buildRequestBody");
930
930
  logger?.info("提交视频生成任务", {
931
931
  model: this.config.modelId,
932
932
  promptLength: prompt.length,
933
- duration: requestBody.duration,
934
- orientation: requestBody.orientation
933
+ duration,
934
+ orientation
935
935
  });
936
- const response = await ctx.http.post(
937
- `${this.config.apiBase}/v1/video/create`,
938
- requestBody,
939
- {
940
- headers: {
941
- "Authorization": `Bearer ${this.config.apiKey}`,
942
- "Content-Type": "application/json",
943
- "Accept": "application/json"
944
- },
945
- timeout: this.config.apiTimeout * 1e3
946
- }
947
- );
936
+ const doCreate = /* @__PURE__ */ __name(async (watermark) => {
937
+ return await ctx.http.post(
938
+ `${this.config.apiBase}/v1/video/create`,
939
+ buildRequestBody(watermark),
940
+ {
941
+ headers: {
942
+ "Authorization": `Bearer ${this.config.apiKey}`,
943
+ "Content-Type": "application/json",
944
+ "Accept": "application/json"
945
+ },
946
+ timeout: this.config.apiTimeout * 1e3
947
+ }
948
+ );
949
+ }, "doCreate");
950
+ let response;
951
+ try {
952
+ response = await doCreate(false);
953
+ } catch (e) {
954
+ logger?.warn("无水印创建任务失败,尝试有水印", { error: sanitizeError(e) });
955
+ response = await doCreate(true);
956
+ }
957
+ if (response?.error || response?.status && response.status >= 400) {
958
+ logger?.warn("无水印创建任务返回错误,尝试有水印", { status: response?.status, error: response?.error, response: response?.data });
959
+ response = await doCreate(true);
960
+ }
948
961
  if (response.error) {
949
962
  const errorMsg = response.error.message || response.error.type || "创建任务失败";
950
963
  throw new Error(sanitizeString(errorMsg));
@@ -1067,6 +1080,12 @@ var YunwuVideoProvider = class {
1067
1080
  await new Promise((resolve) => setTimeout(resolve, pollInterval * 1e3));
1068
1081
  }
1069
1082
  }
1083
+ /**
1084
+ * 等待指定任务完成并返回视频 URL(供外部在拿到 taskId 后自行控制扣费/提示)
1085
+ */
1086
+ async waitForVideo(taskId, maxWaitTime = 300) {
1087
+ return await this.pollTaskCompletion(taskId, maxWaitTime);
1088
+ }
1070
1089
  /**
1071
1090
  * 生成视频(主入口)
1072
1091
  */
@@ -1077,7 +1096,7 @@ var YunwuVideoProvider = class {
1077
1096
  const taskId = await this.createVideoTask(prompt, imageUrl, options);
1078
1097
  logger?.debug("任务已创建,等待 3 秒后开始查询", { taskId });
1079
1098
  await new Promise((resolve) => setTimeout(resolve, 3e3));
1080
- const videoUrl = await this.pollTaskCompletion(taskId, maxWaitTime);
1099
+ const videoUrl = await this.waitForVideo(taskId, maxWaitTime);
1081
1100
  logger?.info("视频生成完成", { taskId, videoUrl });
1082
1101
  return videoUrl;
1083
1102
  } catch (error) {
@@ -1157,11 +1176,14 @@ var UserManager = class {
1157
1176
  dataFile;
1158
1177
  backupFile;
1159
1178
  rechargeHistoryFile;
1179
+ pendingVideoTasksFile;
1160
1180
  logger;
1161
1181
  dataLock = new AsyncLock();
1162
1182
  historyLock = new AsyncLock();
1183
+ pendingLock = new AsyncLock();
1163
1184
  // 内存缓存
1164
1185
  usersCache = null;
1186
+ pendingVideoCache = null;
1165
1187
  activeTasks = /* @__PURE__ */ new Map();
1166
1188
  // userId -> requestId
1167
1189
  rateLimitMap = /* @__PURE__ */ new Map();
@@ -1176,6 +1198,7 @@ var UserManager = class {
1176
1198
  this.dataFile = (0, import_path.join)(this.dataDir, "users_data.json");
1177
1199
  this.backupFile = (0, import_path.join)(this.dataDir, "users_data.json.backup");
1178
1200
  this.rechargeHistoryFile = (0, import_path.join)(this.dataDir, "recharge_history.json");
1201
+ this.pendingVideoTasksFile = (0, import_path.join)(this.dataDir, "pending_video_tasks.json");
1179
1202
  if (!(0, import_fs.existsSync)(this.dataDir)) {
1180
1203
  (0, import_fs.mkdirSync)(this.dataDir, { recursive: true });
1181
1204
  }
@@ -1237,6 +1260,95 @@ var UserManager = class {
1237
1260
  throw error;
1238
1261
  }
1239
1262
  }
1263
+ // --- 待结算视频任务(防止超时套利) ---
1264
+ async loadPendingVideoTasks() {
1265
+ if (this.pendingVideoCache) return this.pendingVideoCache;
1266
+ return await this.pendingLock.acquire(async () => {
1267
+ if (this.pendingVideoCache) return this.pendingVideoCache;
1268
+ try {
1269
+ if ((0, import_fs.existsSync)(this.pendingVideoTasksFile)) {
1270
+ const data = await import_fs.promises.readFile(this.pendingVideoTasksFile, "utf-8");
1271
+ const parsed = JSON.parse(data);
1272
+ this.pendingVideoCache = {
1273
+ version: parsed?.version || "1.0.0",
1274
+ lastUpdate: parsed?.lastUpdate || (/* @__PURE__ */ new Date()).toISOString(),
1275
+ tasks: parsed?.tasks || {}
1276
+ };
1277
+ return this.pendingVideoCache;
1278
+ }
1279
+ } catch (error) {
1280
+ this.logger.error("读取待结算视频任务失败", error);
1281
+ }
1282
+ this.pendingVideoCache = {
1283
+ version: "1.0.0",
1284
+ lastUpdate: (/* @__PURE__ */ new Date()).toISOString(),
1285
+ tasks: {}
1286
+ };
1287
+ return this.pendingVideoCache;
1288
+ });
1289
+ }
1290
+ async savePendingVideoTasksInternal() {
1291
+ if (!this.pendingVideoCache) return;
1292
+ try {
1293
+ this.pendingVideoCache.lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
1294
+ await import_fs.promises.writeFile(this.pendingVideoTasksFile, JSON.stringify(this.pendingVideoCache, null, 2), "utf-8");
1295
+ } catch (error) {
1296
+ this.logger.error("保存待结算视频任务失败", error);
1297
+ throw error;
1298
+ }
1299
+ }
1300
+ /**
1301
+ * 记录一个待结算的视频任务(任务创建成功后立即写入,避免“超时但最终成功”不扣费)
1302
+ */
1303
+ async addPendingVideoTask(task) {
1304
+ await this.loadPendingVideoTasks();
1305
+ await this.pendingLock.acquire(async () => {
1306
+ if (!this.pendingVideoCache) {
1307
+ this.pendingVideoCache = { version: "1.0.0", lastUpdate: (/* @__PURE__ */ new Date()).toISOString(), tasks: {} };
1308
+ }
1309
+ if (this.pendingVideoCache.tasks[task.taskId]) return;
1310
+ this.pendingVideoCache.tasks[task.taskId] = task;
1311
+ await this.savePendingVideoTasksInternal();
1312
+ });
1313
+ }
1314
+ async getPendingVideoTask(taskId) {
1315
+ const data = await this.loadPendingVideoTasks();
1316
+ return data.tasks[taskId] || null;
1317
+ }
1318
+ async markPendingVideoTaskCharged(taskId) {
1319
+ await this.loadPendingVideoTasks();
1320
+ return await this.pendingLock.acquire(async () => {
1321
+ if (!this.pendingVideoCache) return null;
1322
+ const task = this.pendingVideoCache.tasks[taskId];
1323
+ if (!task) return null;
1324
+ if (task.charged) return task;
1325
+ task.charged = true;
1326
+ task.chargedAt = (/* @__PURE__ */ new Date()).toISOString();
1327
+ await this.savePendingVideoTasksInternal();
1328
+ return task;
1329
+ });
1330
+ }
1331
+ async deletePendingVideoTask(taskId) {
1332
+ await this.loadPendingVideoTasks();
1333
+ await this.pendingLock.acquire(async () => {
1334
+ if (!this.pendingVideoCache) return;
1335
+ if (!this.pendingVideoCache.tasks[taskId]) return;
1336
+ delete this.pendingVideoCache.tasks[taskId];
1337
+ await this.savePendingVideoTasksInternal();
1338
+ });
1339
+ }
1340
+ /**
1341
+ * 获取某个用户最近一次未扣费的待结算视频任务
1342
+ */
1343
+ async getLatestPendingVideoTaskForUser(userId) {
1344
+ const data = await this.loadPendingVideoTasks();
1345
+ const tasks = Object.values(data.tasks).filter((t) => t.userId === userId && !t.charged).sort((a, b) => {
1346
+ const ta = Date.parse(a.createdAt || "") || 0;
1347
+ const tb = Date.parse(b.createdAt || "") || 0;
1348
+ return tb - ta;
1349
+ });
1350
+ return tasks[0] || null;
1351
+ }
1240
1352
  // 获取特定用户数据
1241
1353
  async getUserData(userId, userName) {
1242
1354
  await this.loadUsersData();
@@ -2230,6 +2342,7 @@ ${infoParts.join("\n")}`;
2230
2342
  if (!userManager.startTask(userId)) {
2231
2343
  return "您有一个任务正在进行中,请等待完成";
2232
2344
  }
2345
+ let createdTaskId = null;
2233
2346
  try {
2234
2347
  const inputResult = await getInputData(session, img, "single");
2235
2348
  if ("error" in inputResult) {
@@ -2271,24 +2384,41 @@ ${infoParts.join("\n")}`;
2271
2384
  💡 提示:生成过程中可继续使用其他功能`
2272
2385
  );
2273
2386
  const startTime = Date.now();
2274
- const videoUrl = await videoProvider.generateVideo(
2387
+ const taskId = await videoProvider.createVideoTask(
2275
2388
  prompt,
2276
2389
  imageUrls[0],
2277
2390
  {
2278
2391
  duration,
2279
2392
  aspectRatio: ratio
2280
- },
2281
- config.videoMaxWaitTime
2393
+ }
2282
2394
  );
2283
- await recordUserUsage(session, "图生视频", videoCredits, false);
2395
+ createdTaskId = taskId;
2396
+ await userManager.addPendingVideoTask({
2397
+ taskId,
2398
+ userId,
2399
+ userName,
2400
+ commandName: "图生视频",
2401
+ credits: videoCredits,
2402
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2403
+ charged: false
2404
+ });
2405
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
2406
+ const videoUrl = await videoProvider.waitForVideo(taskId, config.videoMaxWaitTime);
2284
2407
  await session.send(import_koishi2.h.video(videoUrl));
2408
+ await recordUserUsage(session, "图生视频", videoCredits, false);
2409
+ await userManager.markPendingVideoTaskCharged(taskId);
2410
+ await userManager.deletePendingVideoTask(taskId);
2285
2411
  const totalTime = Math.floor((Date.now() - startTime) / 1e3);
2286
2412
  await session.send(`✅ 视频生成完成!(耗时 ${totalTime} 秒)`);
2287
2413
  } catch (error) {
2288
2414
  logger.error("视频生成失败", { userId, error: sanitizeError(error) });
2289
2415
  const errorMsg = error.message || "";
2290
2416
  if (errorMsg.includes("任务ID:")) {
2291
- return errorMsg;
2417
+ return "⏳ 视频生成超时(任务仍在后台继续生成),请稍后发送“查询视频”获取结果";
2418
+ }
2419
+ try {
2420
+ if (createdTaskId) await userManager.deletePendingVideoTask(createdTaskId);
2421
+ } catch {
2292
2422
  }
2293
2423
  return `视频生成失败:${sanitizeString(errorMsg)}`;
2294
2424
  } finally {
@@ -2297,28 +2427,38 @@ ${infoParts.join("\n")}`;
2297
2427
  });
2298
2428
  }
2299
2429
  if (config.enableVideoGeneration && videoProvider) {
2300
- ctx.command("查询视频 <taskId:string>", "根据任务ID查询视频生成状态").action(async ({ session }, taskId) => {
2430
+ ctx.command("查询视频 [taskId:string]", "查询视频生成状态(不传任务ID则查询自己最近一次任务)").action(async ({ session }, taskId) => {
2301
2431
  if (!session?.userId) return "会话无效";
2302
- if (!taskId || !taskId.trim()) {
2303
- return "请提供任务ID,格式:查询视频 <任务ID>";
2432
+ const trimmedTaskId = (taskId || "").trim();
2433
+ const resolvedTaskId = trimmedTaskId ? trimmedTaskId : (await userManager.getLatestPendingVideoTaskForUser(session.userId))?.taskId;
2434
+ if (!resolvedTaskId) {
2435
+ return "你当前没有可查询的待生成视频任务";
2304
2436
  }
2305
2437
  try {
2306
2438
  await session.send("🔍 正在查询视频生成状态...");
2307
- const status = await videoProvider.queryTaskStatus(taskId.trim());
2439
+ const status = await videoProvider.queryTaskStatus(resolvedTaskId);
2308
2440
  if (status.status === "completed" && status.videoUrl) {
2441
+ const pending = await userManager.getPendingVideoTask(resolvedTaskId);
2442
+ if (pending && pending.userId && pending.userId !== session.userId) {
2443
+ return "该任务ID不属于当前用户,无法查询";
2444
+ }
2309
2445
  await session.send(import_koishi2.h.video(status.videoUrl));
2446
+ if (pending && !pending.charged) {
2447
+ await recordUserUsage(session, pending.commandName, pending.credits, false);
2448
+ await userManager.markPendingVideoTaskCharged(resolvedTaskId);
2449
+ await userManager.deletePendingVideoTask(resolvedTaskId);
2450
+ }
2310
2451
  return "✅ 视频已生成完成!";
2311
2452
  } else if (status.status === "processing" || status.status === "pending") {
2312
2453
  const progressText = status.progress ? `(进度:${status.progress}%)` : "";
2313
- return `⏳ 视频正在生成中${progressText},请稍后再次查询
2314
- 任务ID:${taskId}`;
2454
+ return `⏳ 视频正在生成中${progressText},请稍后再次查询`;
2315
2455
  } else if (status.status === "failed") {
2316
2456
  return `❌ 视频生成失败:${status.error || "未知错误"}`;
2317
2457
  } else {
2318
2458
  return `❓ 未知状态:${status.status}`;
2319
2459
  }
2320
2460
  } catch (error) {
2321
- logger.error("查询视频任务失败", { taskId, error: sanitizeError(error) });
2461
+ logger.error("查询视频任务失败", { taskId: resolvedTaskId, error: sanitizeError(error) });
2322
2462
  return `查询失败:${sanitizeString(error.message)}`;
2323
2463
  }
2324
2464
  });
@@ -2343,6 +2483,7 @@ ${infoParts.join("\n")}`;
2343
2483
  if (!userManager.startTask(userId)) {
2344
2484
  return "您有一个任务正在进行中,请等待完成";
2345
2485
  }
2486
+ let createdTaskId = null;
2346
2487
  try {
2347
2488
  const inputResult = await getInputData(session, img, "single");
2348
2489
  if ("error" in inputResult) {
@@ -2361,23 +2502,40 @@ ${infoParts.join("\n")}`;
2361
2502
  📝 描述:${finalPrompt}
2362
2503
  ⏱️ 预计需要 1-3 分钟`
2363
2504
  );
2364
- const videoUrl = await videoProvider.generateVideo(
2505
+ const taskId = await videoProvider.createVideoTask(
2365
2506
  finalPrompt,
2366
2507
  imageUrls[0],
2367
2508
  {
2368
2509
  duration: style.duration || 15,
2369
2510
  aspectRatio: style.aspectRatio || "16:9"
2370
- },
2371
- config.videoMaxWaitTime
2511
+ }
2372
2512
  );
2373
- await recordUserUsage(session, style.commandName, videoCredits, false);
2513
+ createdTaskId = taskId;
2514
+ await userManager.addPendingVideoTask({
2515
+ taskId,
2516
+ userId,
2517
+ userName,
2518
+ commandName: style.commandName,
2519
+ credits: videoCredits,
2520
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2521
+ charged: false
2522
+ });
2523
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
2524
+ const videoUrl = await videoProvider.waitForVideo(taskId, config.videoMaxWaitTime);
2374
2525
  await session.send(import_koishi2.h.video(videoUrl));
2526
+ await recordUserUsage(session, style.commandName, videoCredits, false);
2527
+ await userManager.markPendingVideoTaskCharged(taskId);
2528
+ await userManager.deletePendingVideoTask(taskId);
2375
2529
  await session.send(`✅ 视频生成完成!`);
2376
2530
  } catch (error) {
2377
2531
  logger.error("视频风格转换失败", { userId, style: style.commandName, error: sanitizeError(error) });
2378
2532
  const errorMsg = error.message || "";
2379
2533
  if (errorMsg.includes("任务ID:")) {
2380
- return errorMsg;
2534
+ return "⏳ 视频生成超时(任务仍在后台继续生成),请稍后发送“查询视频”获取结果";
2535
+ }
2536
+ try {
2537
+ if (createdTaskId) await userManager.deletePendingVideoTask(createdTaskId);
2538
+ } catch {
2381
2539
  }
2382
2540
  return `视频生成失败:${sanitizeString(errorMsg)}`;
2383
2541
  } finally {
@@ -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,19 @@ 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>;
89
+ /**
90
+ * 获取某个用户最近一次未扣费的待结算视频任务
91
+ */
92
+ getLatestPendingVideoTaskForUser(userId: string): Promise<PendingVideoTask | null>;
62
93
  getUserData(userId: string, userName: string): Promise<UserData>;
63
94
  getAllUsers(): Promise<UsersData>;
64
95
  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.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [